diff --git a/.env.test.example b/.env.test.example new file mode 100644 index 0000000..7b82acd --- /dev/null +++ b/.env.test.example @@ -0,0 +1,2 @@ +# copy this file to `.env.test.local` and fill in variables +GOOGLE_GENERATIVE_AI_API_KEY= diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d3d76f9..0c68ad8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,4 +16,5 @@ jobs: with: github_hosted_runner: true secrets: + DOT_ENV: ${{ secrets.DOT_ENV_TEST }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/example/prompt_summary/judge.ts b/example/prompt_summary/judge.ts new file mode 100644 index 0000000..9cd5714 --- /dev/null +++ b/example/prompt_summary/judge.ts @@ -0,0 +1,14 @@ +import { llmJudgePreset } from '@exercode/problem-utils/presets/llm'; +import { DecisionCode } from '@exercode/problem-utils'; + +await llmJudgePreset(import.meta.dirname, { + test(context) { + return { + decisionCode: + context.result.output.trim().length < (context.testCase.input?.trim().length ?? 0) && + context.result.output.includes(context.testCase.output?.trim() ?? '') + ? DecisionCode.ACCEPTED + : DecisionCode.WRONG_ANSWER, + }; + }, +}); diff --git a/example/prompt_summary/model_answers.test/wa/prompt.txt b/example/prompt_summary/model_answers.test/wa/prompt.txt new file mode 100644 index 0000000..ea50973 --- /dev/null +++ b/example/prompt_summary/model_answers.test/wa/prompt.txt @@ -0,0 +1,3 @@ +Please repeat the following text. + +{input} diff --git a/example/prompt_summary/model_answers/default/prompt.txt b/example/prompt_summary/model_answers/default/prompt.txt new file mode 100644 index 0000000..9362dff --- /dev/null +++ b/example/prompt_summary/model_answers/default/prompt.txt @@ -0,0 +1,3 @@ +Please summarize the following text in its original language. + +{input} diff --git a/example/prompt_summary/problem.md b/example/prompt_summary/problem.md new file mode 100644 index 0000000..45b0cbe --- /dev/null +++ b/example/prompt_summary/problem.md @@ -0,0 +1,24 @@ +--- +name: 'Summary' +--- + +## 問題文 + +文章が与えられます。 +LLMに文章を要約させるプロンプトを作成してください。 + +--- + +## 入力 + +プロンプト中の`{input}`が入力の文章で置き換えられる。 + +### 解答例 + +次のプロンプトは、与えられた文章をそのまま返すことを指示しています。 + +``` +Please repeat the following text. + +{input} +``` diff --git a/example/prompt_summary/test_cases/01_small_00.in b/example/prompt_summary/test_cases/01_small_00.in new file mode 100644 index 0000000..a896e92 --- /dev/null +++ b/example/prompt_summary/test_cases/01_small_00.in @@ -0,0 +1,2 @@ +富士山(ふじさん)は、静岡県(富士宮市、富士市、裾野市、御殿場市、駿東郡小山町)と山梨県(富士吉田市、南都留郡鳴沢村)に跨る活火山である[注釈 3]。 +標高3776.12 m、日本最高峰(剣ヶ峰)[注釈 4]の独立峰で、その優美な風貌は日本国外でも日本の象徴として広く知られている。 diff --git a/example/prompt_summary/test_cases/01_small_00.out b/example/prompt_summary/test_cases/01_small_00.out new file mode 100644 index 0000000..2449e96 --- /dev/null +++ b/example/prompt_summary/test_cases/01_small_00.out @@ -0,0 +1 @@ +富士山 diff --git a/example/prompt_summary/test_cases/02_large_00.in b/example/prompt_summary/test_cases/02_large_00.in new file mode 100644 index 0000000..dcd06e4 --- /dev/null +++ b/example/prompt_summary/test_cases/02_large_00.in @@ -0,0 +1,15 @@ +富士山(ふじさん)は、静岡県(富士宮市、富士市、裾野市、御殿場市、駿東郡小山町)と山梨県(富士吉田市、南都留郡鳴沢村)に跨る活火山である[注釈 3]。 +標高3776.12 m、日本最高峰(剣ヶ峰)[注釈 4]の独立峰で、その優美な風貌は日本国外でも日本の象徴として広く知られている。 + +数多くの芸術作品の題材とされ芸術面のみならず、気候や地層など地質学的にも社会に大きな影響を与えている。 +懸垂曲線の山容を有した玄武岩質成層火山で構成され、その山体は駿河湾の海岸まで及ぶ。 + +古来霊峰とされ、特に山頂部は浅間大神が鎮座するとされたため、神聖視された。 +噴火を沈静化するため律令国家により浅間神社が祭祀され、浅間信仰が確立された。 +また、富士山修験道の開祖とされる富士上人により修験道の霊場としても認識されるようになり、登拝が行われるようになった。 +これら富士信仰は時代により多様化し、村山修験や富士講といった一派を形成するに至る。 +現在、富士山麓周辺には観光名所が多くある他、夏季シーズンには富士登山が盛んである。 + +日本三名山(三霊山)、日本百名山[2]、日本の地質百選に選定されている。 +また、1936年(昭和11年)には富士箱根伊豆国立公園に指定されている[注釈 5]。 +その後、1952年(昭和27年)に特別名勝、2011年(平成23年)に史跡、さらに2013年(平成25年)6月22日には関連する文化財群とともに「富士山-信仰の対象と芸術の源泉」の名で世界文化遺産に登録された[4]。 diff --git a/example/prompt_summary/test_cases/02_large_00.out b/example/prompt_summary/test_cases/02_large_00.out new file mode 100644 index 0000000..2449e96 --- /dev/null +++ b/example/prompt_summary/test_cases/02_large_00.out @@ -0,0 +1 @@ +富士山 diff --git a/package.json b/package.json index 6990621..fd6c820 100644 --- a/package.json +++ b/package.json @@ -43,12 +43,14 @@ "prepare": "husky || true", "prettify": "prettier --cache --color --write \"**/{.*/,}*.{cjs,css,cts,htm,html,java,js,json,json5,jsonc,jsx,md,mjs,mts,scss,ts,tsx,vue,yaml,yml}\" \"!**/test{-,/}fixtures/**\" || true", "start": "build-ts run src/index.ts", - "test": "rm -fr temp && vitest test", + "test": "rm -fr temp && dotenv -c test -- vitest test", "test/ci-setup": "yarn build && bun install --cwd example", "typecheck": "tsc --noEmit --Pretty" }, "prettier": "@willbooster/prettier-config", "dependencies": { + "@ai-sdk/google": "2.0.49", + "ai": "5.0.115", "front-matter": "4.0.2", "zod": "4.2.0" }, @@ -60,6 +62,7 @@ "@willbooster/prettier-config": "10.2.4", "build-ts": "17.0.9", "conventional-changelog-conventionalcommits": "9.1.0", + "dotenv-cli": "11.0.0", "eslint": "9.39.1", "eslint-config-flat-gitignore": "2.1.0", "eslint-config-prettier": "10.1.8", diff --git a/src/presets/llm.ts b/src/presets/llm.ts new file mode 100644 index 0000000..c761708 --- /dev/null +++ b/src/presets/llm.ts @@ -0,0 +1,93 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { google } from '@ai-sdk/google'; +import { generateText } from 'ai'; +import { z } from 'zod'; + +import { parseArgs } from '../helpers/parseArgs.js'; +import { printTestCaseResult } from '../helpers/printTestCaseResult.js'; +import { readTestCases } from '../helpers/readTestCases.js'; +import { DecisionCode } from '../types/decisionCode.js'; +import type { TestCaseResult } from '../types/testCaseResult.js'; + +const PROMPT_FILENAME = 'prompt.txt'; + +const judgeParamsSchema = z.object({ + model: z.enum(['google/gemini-2.5-flash-lite']), +}); + +interface LlmJudgePresetOptions { + test: (context: { + testCase: { id: string; input?: string; output?: string }; + result: { output: string }; + }) => Partial | Promise>; +} + +/** + * A preset judge function for running and testing a user prompt in LLM. + * + * @example + * Create `judge.ts`: + * ```ts + * import { llmJudgePreset } from '@exercode/problem-utils/presets/llm'; + * import { DecisionCode } from '@exercode/problem-utils'; + * + * await llmJudgePreset(import.meta.dirname, { + * test: (context) { + * return { decisionCode: context.result.output ? DecisionCode.ACCEPTED : DecisionCode.WRONG_ANSWER }; + * } + * }); + * ``` + * + * Run with the required parameters: + * ```bash + * bun judge.ts model_answers/java '{ "model": "gemini-2.5-flash-lite" }' + * ``` + */ +export async function llmJudgePreset(problemDir: string, options: LlmJudgePresetOptions): Promise { + const args = parseArgs(process.argv); + const params = judgeParamsSchema.parse(args.params); + + const testCases = await readTestCases(path.join(problemDir, 'test_cases')); + + const prompt = await fs.promises.readFile(path.join(args.cwd, PROMPT_FILENAME), 'utf8'); + + for (const testCase of testCases) { + const startTimeMilliseconds = Date.now(); + try { + // requires `GOOGLE_GENERATIVE_AI_API_KEY` + const { text } = await generateText({ + model: google(params.model.slice('google/'.length)), + prompt: prompt.replaceAll('{input}', testCase.input ?? ''), + }); + + const stopTimeMilliseconds = Date.now(); + + const testCaseResult = { + testCaseId: testCase.id, + decisionCode: DecisionCode.ACCEPTED, + stdin: testCase.input, + stdout: text, + timeSeconds: (stopTimeMilliseconds - startTimeMilliseconds) / 1000, + ...(await options.test({ testCase, result: { output: text } })), + }; + + printTestCaseResult(testCaseResult); + + if (testCaseResult.decisionCode !== DecisionCode.ACCEPTED) break; + } catch (error) { + const stopTimeMilliseconds = Date.now(); + + printTestCaseResult({ + testCaseId: testCase.id, + decisionCode: DecisionCode.RUNTIME_ERROR, + stdin: testCase.input, + stderr: error instanceof Error ? error.message : String(error), + timeSeconds: (stopTimeMilliseconds - startTimeMilliseconds) / 1000, + }); + + break; + } + } +} diff --git a/test/e2e/debugAndJudge.test.ts b/test/e2e/debugAndJudge.test.ts index 8ad88c4..a9edd5c 100644 --- a/test/e2e/debugAndJudge.test.ts +++ b/test/e2e/debugAndJudge.test.ts @@ -110,12 +110,16 @@ const acceptedTestCaseResultsForAPlusBFile = [ }, ] as const satisfies readonly TestCaseResult[]; -test.each<[string, string, string, Record, readonly TestCaseResult[]]>([ +test.each< + [string, string, string, Record, Record, readonly TestCaseResult[]] +>([ + // stdioDebugPreset [ 'example/a_plus_b', 'debug.ts', 'model_answers/java', { stdin: '1 1' }, + {}, [ { testCaseId: 'debug', @@ -129,14 +133,16 @@ test.each<[string, string, string, Record, readonly TestCaseRes ], ], - ['example/a_plus_b', 'judge.ts', 'model_answers/java', {}, acceptedTestCaseResultsForAPlusB], - ['example/a_plus_b', 'judge.ts', 'model_answers/python', {}, acceptedTestCaseResultsForAPlusB], - ['example/a_plus_b', 'judge.ts', 'model_answers.test/java_rename', {}, acceptedTestCaseResultsForAPlusB], + // stdioJudgePreset + ['example/a_plus_b', 'judge.ts', 'model_answers/java', {}, {}, acceptedTestCaseResultsForAPlusB], + ['example/a_plus_b', 'judge.ts', 'model_answers/python', {}, {}, acceptedTestCaseResultsForAPlusB], + ['example/a_plus_b', 'judge.ts', 'model_answers.test/java_rename', {}, {}, acceptedTestCaseResultsForAPlusB], [ 'example/a_plus_b', 'judge.ts', 'model_answers.test/python_fpe', {}, + {}, [ { testCaseId: '01_small_00', @@ -158,6 +164,7 @@ test.each<[string, string, string, Record, readonly TestCaseRes 'judge.ts', 'model_answers.test/python_rpe', {}, + {}, [ { testCaseId: '01_small_00', @@ -175,6 +182,7 @@ test.each<[string, string, string, Record, readonly TestCaseRes 'judge.ts', 'model_answers.test/python_tle', {}, + {}, [ ...acceptedTestCaseResultsForAPlusB.slice(0, 2), { @@ -192,6 +200,7 @@ test.each<[string, string, string, Record, readonly TestCaseRes 'judge.ts', 'model_answers.test/python_wa', {}, + {}, [ { testCaseId: '01_small_00', @@ -223,12 +232,13 @@ test.each<[string, string, string, Record, readonly TestCaseRes ], ], - ['example/a_plus_b_file', 'judge.ts', 'model_answers/javascript', {}, acceptedTestCaseResultsForAPlusBFile], + ['example/a_plus_b_file', 'judge.ts', 'model_answers/javascript', {}, {}, acceptedTestCaseResultsForAPlusBFile], [ 'example/a_plus_b_file', 'judge.ts', 'model_answers.test/javascript_mrofe', {}, + {}, [ { testCaseId: '01_small_00', @@ -245,6 +255,7 @@ test.each<[string, string, string, Record, readonly TestCaseRes 'judge.ts', 'model_answers.test/javascript_wa', {}, + {}, [ ...acceptedTestCaseResultsForAPlusBFile.slice(0, 1), { @@ -257,10 +268,68 @@ test.each<[string, string, string, Record, readonly TestCaseRes }, ], ], + + // llmJudgePreset + [ + 'example/prompt_summary', + 'judge.ts', + 'model_answers/default', + { model: 'google/gemini-2.5-flash-lite' }, + { GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY }, + [ + { + testCaseId: '01_small_00', + decisionCode: 2000, + stdin: expect.any(String), + stdout: expect.any(String), + timeSeconds: expect.any(Number), + }, + { + testCaseId: '02_large_00', + decisionCode: 2000, + stdin: expect.any(String), + stdout: expect.any(String), + timeSeconds: expect.any(Number), + }, + ], + ], + [ + 'example/prompt_summary', + 'judge.ts', + 'model_answers.test/wa', + { model: 'google/gemini-2.5-flash-lite' }, + { GOOGLE_GENERATIVE_AI_API_KEY: process.env.GOOGLE_GENERATIVE_AI_API_KEY }, + [ + { + testCaseId: '01_small_00', + decisionCode: 1000, + stdin: expect.any(String), + stdout: expect.any(String), + timeSeconds: expect.any(Number), + }, + ], + ], + [ + 'example/prompt_summary', + 'judge.ts', + 'model_answers/default', + { model: 'google/gemini-2.5-flash-lite' }, + { GOOGLE_GENERATIVE_AI_API_KEY: undefined }, + [ + { + testCaseId: '01_small_00', + decisionCode: 1001, + stdin: expect.any(String), + stderr: + "Google Generative AI API key is missing. Pass it using the 'apiKey' parameter or the GOOGLE_GENERATIVE_AI_API_KEY environment variable.", + timeSeconds: expect.any(Number), + }, + ], + ], ])( - '%s %s %j', + '%s %s %s %j', { timeout: 20_000, concurrent: true }, - async (cwd, scriptFilename, argsCwd, argsParams, expectedTestCaseResults) => { + async (cwd, scriptFilename, argsCwd, argsParams, env, expectedTestCaseResults) => { // The target files may be changed during the judging, so clone it before testing. await fs.promises.mkdir('temp', { recursive: true }); const tempDir = await fs.promises.mkdtemp(path.join('temp', 'judge_')); @@ -269,6 +338,7 @@ test.each<[string, string, string, Record, readonly TestCaseRes const spawnResult = child_process.spawnSync('bun', [scriptFilename, argsCwd, JSON.stringify(argsParams)], { cwd: tempDir, encoding: 'utf8', + env: { ...process.env, ...env }, }); if (spawnResult.stderr) console.error(spawnResult.stderr); diff --git a/yarn.lock b/yarn.lock index fd13997..9f49894 100644 --- a/yarn.lock +++ b/yarn.lock @@ -41,6 +41,53 @@ __metadata: languageName: node linkType: hard +"@ai-sdk/gateway@npm:2.0.22": + version: 2.0.22 + resolution: "@ai-sdk/gateway@npm:2.0.22" + dependencies: + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.19" + "@vercel/oidc": "npm:3.0.5" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/3ef90c2807ede90cf548eba89abdb6b67863b9bfeda5df799c14aa2792d129be71972d266948c5412c2ce28d56dd409dd5d1e350df7612856a1e5cf301ccf1ff + languageName: node + linkType: hard + +"@ai-sdk/google@npm:2.0.49": + version: 2.0.49 + resolution: "@ai-sdk/google@npm:2.0.49" + dependencies: + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.19" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/f3f8acfcd956edc7d807d22963d5eff0f765418f1f2c7d18615955ccdfcebb4d43cc26ce1f712c6a53572f1d8becc0773311b77b1f1bf1af87d675c5f017d5a4 + languageName: node + linkType: hard + +"@ai-sdk/provider-utils@npm:3.0.19": + version: 3.0.19 + resolution: "@ai-sdk/provider-utils@npm:3.0.19" + dependencies: + "@ai-sdk/provider": "npm:2.0.0" + "@standard-schema/spec": "npm:^1.0.0" + eventsource-parser: "npm:^3.0.6" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/e4decb19264067fa1b1642e07d515d25d1509a1a9143f59ccc051e3ca413c9fb1d708e1052a70eaf329ca39ddf6152520cd833dbf8c95d9bf02bbeffae8ea363 + languageName: node + linkType: hard + +"@ai-sdk/provider@npm:2.0.0": + version: 2.0.0 + resolution: "@ai-sdk/provider@npm:2.0.0" + dependencies: + json-schema: "npm:^0.4.0" + checksum: 10c0/e50e520016c9fc0a8b5009cadd47dae2f1c81ec05c1792b9e312d7d15479f024ca8039525813a33425c884e3449019fed21043b1bfabd6a2626152ca9a388199 + languageName: node + linkType: hard + "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.26.2, @babel/code-frame@npm:^7.27.1": version: 7.27.1 resolution: "@babel/code-frame@npm:7.27.1" @@ -1673,13 +1720,16 @@ __metadata: version: 0.0.0-use.local resolution: "@exercode/problem-utils@workspace:." dependencies: + "@ai-sdk/google": "npm:2.0.49" "@tsconfig/node24": "npm:24.0.3" "@types/eslint": "npm:8.56.11" "@types/node": "npm:24.10.4" "@willbooster/eslint-config-ts": "npm:11.4.12" "@willbooster/prettier-config": "npm:10.2.4" + ai: "npm:5.0.115" build-ts: "npm:17.0.9" conventional-changelog-conventionalcommits: "npm:9.1.0" + dotenv-cli: "npm:11.0.0" eslint: "npm:9.39.1" eslint-config-flat-gitignore: "npm:2.1.0" eslint-config-prettier: "npm:10.1.8" @@ -2167,6 +2217,13 @@ __metadata: languageName: node linkType: hard +"@opentelemetry/api@npm:1.9.0": + version: 1.9.0 + resolution: "@opentelemetry/api@npm:1.9.0" + checksum: 10c0/9aae2fe6e8a3a3eeb6c1fdef78e1939cf05a0f37f8a4fae4d6bf2e09eb1e06f966ece85805626e01ba5fab48072b94f19b835449e58b6d26720ee19a58298add + languageName: node + linkType: hard + "@pnpm/config.env-replace@npm:^1.1.0": version: 1.1.0 resolution: "@pnpm/config.env-replace@npm:1.1.0" @@ -3214,6 +3271,13 @@ __metadata: languageName: node linkType: hard +"@vercel/oidc@npm:3.0.5": + version: 3.0.5 + resolution: "@vercel/oidc@npm:3.0.5" + checksum: 10c0/a63f0ab226f9070f974334014bd2676611a2d13473c10b867e3d9db8a2cc83637ae7922db26b184dd97b5945e144fc211c8f899642d205517e5b4e0e34f05b0e + languageName: node + linkType: hard + "@vitest/expect@npm:4.0.15": version: 4.0.15 resolution: "@vitest/expect@npm:4.0.15" @@ -3405,6 +3469,20 @@ __metadata: languageName: node linkType: hard +"ai@npm:5.0.115": + version: 5.0.115 + resolution: "ai@npm:5.0.115" + dependencies: + "@ai-sdk/gateway": "npm:2.0.22" + "@ai-sdk/provider": "npm:2.0.0" + "@ai-sdk/provider-utils": "npm:3.0.19" + "@opentelemetry/api": "npm:1.9.0" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/44a844eb2980151a14a22e3c5f0d7525f03601957b9280f7de0fbd7d18c00353ffef39ac84d7ad01af3fc14eeb292f019015a0b7d8784a5b8c5e2a149d863a15 + languageName: node + linkType: hard + "ajv@npm:^6.12.4": version: 6.12.6 resolution: "ajv@npm:6.12.6" @@ -4319,7 +4397,21 @@ __metadata: languageName: node linkType: hard -"dotenv-expand@npm:12.0.3": +"dotenv-cli@npm:11.0.0": + version: 11.0.0 + resolution: "dotenv-cli@npm:11.0.0" + dependencies: + cross-spawn: "npm:^7.0.6" + dotenv: "npm:^17.1.0" + dotenv-expand: "npm:^12.0.0" + minimist: "npm:^1.2.6" + bin: + dotenv: cli.js + checksum: 10c0/c3e6e58a484b3204eeb167a8f78c9cb04c4bae951069e7860eccbbbf96964e3bd808b3632dfe841e5d882b2c5d77c447f20abd06edd4db623ce719d94a7fb44e + languageName: node + linkType: hard + +"dotenv-expand@npm:12.0.3, dotenv-expand@npm:^12.0.0": version: 12.0.3 resolution: "dotenv-expand@npm:12.0.3" dependencies: @@ -4328,7 +4420,7 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:17.2.3": +"dotenv@npm:17.2.3, dotenv@npm:^17.1.0": version: 17.2.3 resolution: "dotenv@npm:17.2.3" checksum: 10c0/c884403209f713214a1b64d4d1defa4934c2aa5b0002f5a670ae298a51e3c3ad3ba79dfee2f8df49f01ae74290fcd9acdb1ab1d09c7bfb42b539036108bb2ba0 @@ -4856,6 +4948,13 @@ __metadata: languageName: node linkType: hard +"eventsource-parser@npm:^3.0.6": + version: 3.0.6 + resolution: "eventsource-parser@npm:3.0.6" + checksum: 10c0/70b8ccec7dac767ef2eca43f355e0979e70415701691382a042a2df8d6a68da6c2fca35363669821f3da876d29c02abe9b232964637c1b6635c940df05ada78a + languageName: node + linkType: hard + "execa@npm:^8.0.0": version: 8.0.1 resolution: "execa@npm:8.0.1" @@ -5781,6 +5880,13 @@ __metadata: languageName: node linkType: hard +"json-schema@npm:^0.4.0": + version: 0.4.0 + resolution: "json-schema@npm:0.4.0" + checksum: 10c0/d4a637ec1d83544857c1c163232f3da46912e971d5bf054ba44fdb88f07d8d359a462b4aec46f2745efbc57053365608d88bc1d7b1729f7b4fc3369765639ed3 + languageName: node + linkType: hard + "json-stable-stringify-without-jsonify@npm:^1.0.1": version: 1.0.1 resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" @@ -6296,7 +6402,7 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.0, minimist@npm:^1.2.5": +"minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6": version: 1.2.8 resolution: "minimist@npm:1.2.8" checksum: 10c0/19d3fcdca050087b84c2029841a093691a91259a47def2f18222f41e7645a0b7c44ef4b40e88a1e58a40c84d2ef0ee6047c55594d298146d0eb3f6b737c20ce6