diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..330891c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + unit-and-package: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm test + - run: npm run package + - name: Verify VSIX archive + run: unzip -t mcpp-vscode-*.vsix + + extension-host-e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - name: Run Extension Host smoke tests + run: xvfb-run -a npm run test:e2e diff --git a/README.md b/README.md index 4e0d263..326d8a7 100644 --- a/README.md +++ b/README.md @@ -114,9 +114,11 @@ TextMate 语法规则提供。 检查可以区分模块可用、PCM 不匹配、模块产物不可用、语言模式错误和一般检查失败。 完整命令及输出会写入 `mcpp` 输出频道。 -当 clangd 与编译器不匹配时,**mcpp: 一键配置模块代码提示** 会通过 xlings 自动下载 -匹配版本的 llvm-tools(含 clangd),完成后配置 clangd 并刷新状态栏,无需 Reload -Window。非 LLVM 工具链(GCC/MSVC)项目会得到清晰的引导说明,不做自动切换。 +**mcpp: 一键配置模块代码提示** 会先读取工具链状态,再显示一次模态确认。确认后, +它可以自动安装 host LLVM、设置 mcpp 全局默认、执行 `mcpp build`,并通过已安装的 +xlings 补齐匹配版本的 llvm-tools(含 clangd),最后重新读取 CDB、配置 clangd 和检查 +模块状态,无需 Reload Window。项目在 `mcpp.toml` 或 target 中显式固定 GCC/MSVC 时, +一键流程会停止并提示手动处理,绝不修改项目配置或伪装成全局切换成功。 ### 自动协调与多根工作区 @@ -142,8 +144,9 @@ Window。非 LLVM 工具链(GCC/MSVC)项目会得到清晰的引导说明, 状态和可安装版本。 - 安装 mcpp 支持的工具链 spec,或从已安装/系统项中选择全局默认工具链。 -安装和修改默认值都需要确认。取消操作不会被当成成功,工具链安装和默认值修改共享 -全局互斥锁。 +安装和修改默认值都需要确认。取消操作不会被当成成功,工具链安装、默认值修改和构建 +共享一个全局互斥锁;一键配置只在开始处确认一次,之后不会弹出工具链选择或“是否刷新 +编译数据库”的二次询问。 ## 命令 @@ -160,7 +163,7 @@ Window。非 LLVM 工具链(GCC/MSVC)项目会得到清晰的引导说明, | **mcpp: 配置 clangd** | 手动重新应用当前 LLVM 工程的 clangd 配置 | | **mcpp: 刷新编译数据库** | 执行 `mcpp build`,随后重新读取 CDB 并协调 clangd | | **mcpp: 检查模块支持** | 立即执行 clangd 直接检查并刷新模块状态 | -| **mcpp: 一键配置模块代码提示** | 自动检测匹配 clangd,缺失时通过 xlings 安装 llvm-tools 并配置 | +| **mcpp: 一键配置模块代码提示** | 一次确认后自动安装/切换 host LLVM、执行 `mcpp build`,通过 xlings 补齐匹配 llvm-tools,重载 CDB 并配置 clangd;项目显式固定非 LLVM 工具链时停止 | ## 设置 diff --git a/docs/superpowers/plans/2026-08-08-issues-6-7-implementation.md b/docs/superpowers/plans/2026-08-08-issues-6-7-implementation.md new file mode 100644 index 0000000..eebb40f --- /dev/null +++ b/docs/superpowers/plans/2026-08-08-issues-6-7-implementation.md @@ -0,0 +1,411 @@ +# Issues 6 And 7 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make module code completion genuinely one-click and add deterministic PR unit/E2E CI for the VS Code extension. + +**Architecture:** Issue #6 gets a pure `moduleSetup` planner/state machine plus a non-interactive, locked mcpp preparation method. The VS Code layer performs one confirmation, then reloads the new CDB before configuring clangd. Issue #7 adds a Linux Extension Host smoke lane using fake mcpp and a stub clangd dependency, while the existing Node tests and VSIX packaging run in a separate CI job. + +**Tech Stack:** TypeScript, Node `node:test`, Mocha inside `@vscode/test-electron`, GitHub Actions, VS Code extension APIs. + +--- + +## File Map + +| File | Responsibility | +| --- | --- | +| `src/moduleSetup.ts` | Pure plan construction, command list, confirmation text, and injected execution state machine. | +| `test/moduleSetup.test.ts` | RED/GREEN coverage for plan branches, command order, failure stops, and build degradation. | +| `src/cliController.ts` | Non-interactive locked execution of `toolchain install llvm`, `toolchain default llvm`, and `build`. | +| `test/artifacts.test.ts` | Manifest/source assertions for the new one-click flow and no extra interactive prompt. | +| `src/extension.ts` | Replace the eight-step wizard with the single-confirmation adapter and reload the post-build context. | +| `README.md` | Update command behavior and explain the explicit project-toolchain boundary. | +| `package.json`, `package-lock.json` | Add E2E scripts and runner dependencies. | +| `test/e2e/runTest.ts` | Download/start VS Code and prepare isolated user/extensions directories. | +| `test/e2e/suite/index.ts`, `test/e2e/suite/extension.test.ts` | Extension Host smoke test entry and cases. | +| `test/e2e/fixtures/project/mcpp.toml` | Minimal workspace fixture. | +| `test/e2e/fixtures/fake-mcpp.js` | Deterministic executable recording argv and returning success. | +| `test/e2e/fixtures/clangd-stub/` | Installed-looking `llvm-vs-code-extensions.vscode-clangd` dependency stub. | +| `.github/workflows/ci.yml` | PR/main checks for unit/package and Extension Host E2E. | + +## Task 1: Add The Failing Pure Workflow Tests + +**Files:** +- Create: `test/moduleSetup.test.ts` +- Create: `src/moduleSetup.ts` only after the RED command fails + +- [ ] **Step 1: Define test fixtures and the planner contract** + +Add a minimal inventory factory using the existing `ToolchainInventory` shape and tests that import the not-yet-existing functions: + +```ts +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + buildModuleSetupPlan, + executeModuleSetup, + mcppModuleSetupCommands, + type ModuleSetupOperations, +} from "../src/moduleSetup"; +import type { ToolchainInventory } from "../src/cli"; + +function inventory(overrides: Partial = {}): ToolchainInventory { + return { + installed: [{ family: "gcc", version: "16.1.0", spec: "gcc@16.1.0", source: "managed", effective: true }], + available: [{ family: "llvm", version: "22.1.8", spec: "llvm@22.1.8", source: "managed", effective: false }], + targets: [], + effective: { family: "gcc", version: "16.1.0", spec: "gcc@16.1.0", source: "managed", effective: true }, + effectiveTarget: undefined, + globalDefaultSpec: "gcc@16.1.0", + projectOverridesGlobal: false, + recognized: true, + rawOutput: "fixture", + ...overrides, + }; +} + +test("plans install, switch and build for an unfixed GCC project", () => { + const plan = buildModuleSetupPlan(inventory(), "syntax-only", true, false); + assert.deepEqual(plan, { state: "ready", installLlvm: true, switchDefault: true }); + if (plan.state !== "ready") throw new Error("expected a ready setup plan"); + assert.deepEqual( + mcppModuleSetupCommands(plan), + [ + { stage: "install", mode: "task", args: ["toolchain", "install", "llvm"] }, + { stage: "default", mode: "process", args: ["toolchain", "default", "llvm"] }, + { stage: "build", mode: "task", args: ["build"] }, + ], + ); +}); + +test("blocks a project-pinned non-LLVM toolchain without operations", () => { + const decision = buildModuleSetupPlan(inventory({ projectOverridesGlobal: true }), "syntax-only", true, false); + assert.deepEqual(decision, { state: "blocked", reason: "project-toolchain-override" }); +}); +``` + +- [ ] **Step 2: Add execution-order and failure tests** + +Use an injected operation recorder. Cover an installed LLVM plan, a cancelled install, a failed default, a failed build with no usable CDB, and a failed build whose reload returns a full LLVM context. Assert that later operations are absent and that the latter returns `degraded` after clangd/check succeed. + +```ts +function recorder(log: string[], result: Partial = {}): ModuleSetupOperations { + return { + prepare: async () => { log.push("prepare"); return { state: "succeeded", stage: "build" }; }, + reload: async () => { log.push("reload"); return { state: "succeeded" }; }, + ensureClangd: async () => { log.push("clangd"); return { state: "succeeded" }; }, + checkModules: async () => { log.push("check"); return { state: "succeeded" }; }, + ...result, + }; +} + +test("executes prepare, reload, clangd and module check exactly once", async () => { + const log: string[] = []; + const outcome = await executeModuleSetup( + { state: "ready", installLlvm: false, switchDefault: false }, + recorder(log), + ); + assert.equal(outcome.state, "succeeded"); + assert.deepEqual(log, ["prepare", "reload", "clangd", "check"]); +}); + +test("does not continue after a non-build preparation failure", async () => { + const log: string[] = []; + const outcome = await executeModuleSetup( + { state: "ready", installLlvm: true, switchDefault: true }, + recorder(log, { prepare: async () => { log.push("prepare"); return { state: "failed", stage: "default" }; } }), + ); + assert.equal(outcome.state, "failed"); + assert.deepEqual(log, ["prepare"]); +}); +``` + +- [ ] **Step 3: Run the focused test and verify RED** + +Run: `npm run clean && npm run compile && node --test dist/test/moduleSetup.test.js` + +Expected: TypeScript compilation fails because `src/moduleSetup.ts` and its exported contract do not exist. Do not add implementation before observing this failure. + +## Task 2: Implement The Pure Workflow + +**Files:** +- Create: `src/moduleSetup.ts` +- Modify: `test/moduleSetup.test.ts` only for assertions clarified by the implementation contract + +- [ ] **Step 1: Implement inventory planning** + +Export the following types and function signatures: + +```ts +export type ModuleSetupStage = "install" | "default" | "build" | "reload" | "clangd" | "check"; +export type ModuleSetupStepState = "succeeded" | "failed" | "cancelled"; +export type ModuleSetupBlockedReason = "untrusted" | "busy" | "project-toolchain-override" | "unrecognized-inventory"; +export type ModuleSetupDecision = + | { state: "ready"; installLlvm: boolean; switchDefault: boolean } + | { state: "blocked"; reason: ModuleSetupBlockedReason }; + +export function buildModuleSetupPlan( + inventory: ToolchainInventory, + capability: "full" | "syntax-only" | "unavailable", + trusted: boolean, + busy: boolean, +): ModuleSetupDecision; +``` + +Return `untrusted` before reading mutable state, `unrecognized-inventory` before reading inventory fields, and `busy` before planning mutations. Block project overrides only when `projectOverridesGlobal` is true and the effective toolchain is not LLVM. Treat an installed host LLVM as `family.toLowerCase() === "llvm"`; the available list does not count as installed. + +- [ ] **Step 2: Implement fixed command construction and Chinese confirmation text** + +Export: + +```ts +export type ModuleSetupCommand = { + stage: "install" | "default" | "build"; + mode: "task" | "process"; + args: string[]; +}; + +export function mcppModuleSetupCommands( + plan: Extract, +): ModuleSetupCommand[]; + +export function moduleSetupConfirmation( + capability: "full" | "syntax-only" | "unavailable", + plan: Extract, +): { message: string; detail: string }; +``` + +Always append `{ stage: "build", mode: "task", args: ["build"] }`. Use argv arrays, never shell strings. The detail must state that global default changes are not project manifest changes and mention the possible LLVM/llvm-tools download. + +- [ ] **Step 3: Implement injected execution and the build-degradation exception** + +Export: + +```ts +export interface ModuleSetupStepResult { + state: ModuleSetupStepState; + stage?: ModuleSetupStage; + message?: string; +} + +export interface ModuleSetupOperations { + prepare: () => Promise; + reload: () => Promise; + ensureClangd: () => Promise; + checkModules: () => Promise; +} + +export type ModuleSetupOutcome = ModuleSetupStepResult & { degraded?: boolean }; + +export function executeModuleSetup( + decision: Extract, + operations: ModuleSetupOperations, +): Promise; +``` + +Stop on cancelled or failed `prepare` except a failed build; invoke `reload` after a failed build to determine whether the new CDB is usable. Only a successful reload, clangd configuration, and module check may produce `succeeded`; a successful remainder after a failed build produces `degraded: true`. Never call `ensureClangd` or `checkModules` after a failed reload. + +- [ ] **Step 4: Run the focused tests and the full Node suite** + +Run: `npm test` + +Expected: the new planner/state-machine tests and all pre-existing tests pass. If a test fails, fix the implementation or its contract before proceeding to VS Code integration. + +## Task 3: Add The Locked Non-Interactive Controller Operation + +**Files:** +- Modify: `src/cliController.ts` +- Modify: `test/artifacts.test.ts` +- Modify: `test/tasks.test.ts` if lock behavior needs a focused pure assertion + +- [ ] **Step 1: Add a failing source-contract test** + +Assert that `src/cliController.ts` exposes an automated setup method which uses `mcppModuleSetupCommands`, calls `beginGlobal` before the first command, and calls `finishGlobal` in a `finally` block. Also assert that the method does not call `pickInstallSpec`, `selectDefaultToolchainFromInventory`, `showQuickPick`, or `showWarningMessage`. + +- [ ] **Step 2: Implement the method with one operation token** + +Make the existing inventory reader public without changing its parsing/error behavior, then add: + +```ts +public async readToolchainInventory( + project: McppProjectDiscovery | undefined = this.options.currentProject(), +): Promise; + +public async runAutomaticModuleSetup( + plan: Extract, +): Promise; +``` + +The method must require a project and trust, acquire one global token, and execute `mcppModuleSetupCommands(plan)` in order. `task` commands use the existing `executeTask`; `process` commands use `runProcess`; append all output through the existing helpers. Return the exact failing stage and distinguish `cancelled` from `failed`. The `build` task executes directly under the global token instead of calling `runProjectTask`, avoiding a nested project-lock attempt. Release the token before returning even when `executeTask` throws. + +Use Chinese comments only for the lock lifetime and the direct-build reason; the method itself should remain self-explanatory. + +- [ ] **Step 3: Run focused controller/task tests** + +Run: `npm run clean && npm run compile && node --test dist/test/artifacts.test.js dist/test/tasks.test.js dist/test/moduleSetup.test.js` + +Expected: PASS with the new source/lock assertions and no regression in existing task-lock tests. + +## Task 4: Replace The Eight-Step Wizard + +**Files:** +- Modify: `src/extension.ts` +- Modify: `test/artifacts.test.ts` +- Modify: `README.md` + +- [ ] **Step 1: Add failing source assertions for the user contract** + +Extend `test/artifacts.test.ts` to assert that the one-click path calls `readToolchainInventory`, `buildModuleSetupPlan`, `moduleSetupConfirmation`, `runAutomaticModuleSetup`, and `executeModuleSetup`; assert that the wizard no longer calls the generic install/default commands or displays the post-build “刷新编译数据库” choice. + +- [ ] **Step 2: Add the planner/controller imports and a post-build context holder** + +Import the pure module types/functions. Keep the existing `ProjectContext` local to the extension and use `let currentContext = context` inside the workflow closure. The reload operation must call `loadProjectContext(context.project)` after the controller returns; it must update status before clangd resolution. + +- [ ] **Step 3: Implement one preflight modal** + +Replace the current `autoConfigureModulesWizard` body with this sequence: + +```ts +const inventory = await cliController.readToolchainInventory(context.project); +if (inventory === undefined) return; +const decision = buildModuleSetupPlan( + inventory, + context.analysis.capability, + vscode.workspace.isTrusted, + cliController.isBusy(), +); +if (decision.state === "blocked") { + await vscode.window.showWarningMessage(moduleSetupBlockedMessage(decision.reason)); + return; +} +const confirmation = moduleSetupConfirmation(context.analysis.capability, decision); +const choice = await vscode.window.showWarningMessage( + confirmation.message, + { modal: true, detail: confirmation.detail }, + "确认一键配置", +); +if (choice !== "确认一键配置") return; +``` + +The preflight modal is the only confirmation. Do not call the existing interactive install/default commands from this path. + +Add a local pure mapping helper with exhaustive cases: + +```ts +function moduleSetupBlockedMessage(reason: ModuleSetupBlockedReason): string { + switch (reason) { + case "project-toolchain-override": + return "当前项目显式固定了非 LLVM 工具链;一键配置不会修改 mcpp.toml,请先手动切换项目工具链。"; + case "untrusted": + return "当前工作区未受信任,不会执行外部程序。请先信任工作区。"; + case "busy": + return "已有 mcpp 操作正在运行,请等待完成后再试。"; + case "unrecognized-inventory": + return "无法识别 mcpp 工具链状态,请查看 mcpp 输出频道并检查 mcpp 版本。"; + } +} +``` + +- [ ] **Step 4: Implement the injected execution closures** + +Use `executeModuleSetup` with: + +- `prepare`: `cliController.runAutomaticModuleSetup(decision)`. +- `reload`: reload the project context, require a full LLVM analysis and return a failed result when the CDB is missing or remains GCC/MSVC. +- `ensureClangd`: resolve current clangd; if compatible, call `configureClangd(currentContext, status, output, "automatic", true)`. Otherwise require `findXlingsExecutable()` and a compiler LLVM identity, run the existing xlings task with `xlingsInstallArgs`, reload the context again, then configure clangd automatically. +- `checkModules`: call `runModuleSupportCheck(currentContext, status, output, "automatic")` and return failed when no available module status is produced. + +The closure must not call `maybeDisableCppTools`, `showQuickPick`, `showInputBox`, or any interactive clangd mode. + +- [ ] **Step 5: Report one final result and update README** + +Map `succeeded` to one information message, `degraded` to one warning containing “构建失败,语言服务已刷新”, and `failed/cancelled` to one error/warning. Update the README command table and workflow section to say the command performs a single confirmation and refuses project-pinned toolchains instead of claiming it can silently switch them. + +- [ ] **Step 6: Run the full Node suite and package build** + +Run: `npm test && npm run package` + +Expected: all tests pass and `vsce package` creates a version-matched VSIX without including `test/**` or `dist/test/**`. + +## Task 5: Add Deterministic Extension Host E2E + +**Files:** +- Modify: `package.json`, `package-lock.json` +- Create: `test/e2e/runTest.ts` +- Create: `test/e2e/suite/index.ts` +- Create: `test/e2e/suite/extension.test.ts` +- Create: `test/e2e/fixtures/project/mcpp.toml` +- Create: `test/e2e/fixtures/project/main.cpp` +- Create: `test/e2e/fixtures/fake-mcpp.js` +- Create: `test/e2e/fixtures/clangd-stub/package.json` +- Create: `test/e2e/fixtures/clangd-stub/extension.js` + +- [ ] **Step 1: Add the E2E dependencies and scripts** + +Add `@vscode/test-electron`, `mocha`, and `@types/mocha` as dev dependencies. Add scripts: + +```json +"test:e2e": "npm run compile && node dist/test/e2e/runTest.js", +"test:all": "npm test && npm run test:e2e" +``` + +Run `npm install --save-dev @vscode/test-electron@3.1.0 mocha @types/mocha` and verify only the intended manifest/lockfile changes occur. + +- [ ] **Step 2: Add the fake CLI and dependency stub** + +The fake CLI writes its `process.argv.slice(2).join(" ")` to `process.env.MCPP_E2E_LOG`, exits zero for `build`, and exits one for no other unneeded command. The stub package id is `llvm-vs-code-extensions.vscode-clangd`, its activation registers `clangd.restart`, and it has no external process behavior. `runTest.ts` copies/chmods the fake CLI and places the stub under an isolated extensions directory with a versioned folder name. + +- [ ] **Step 3: Add the Extension Host runner and suite** + +`runTest.ts` calls `runTests` with the compiled extension root, compiled suite path, fixture workspace, `--user-data-dir`, `--extensions-dir`, `--disable-updates`, `--skip-welcome`, and `--disable-workspace-trust` launch args. Set `MCPP_E2E_FAKE_MCPP` and `MCPP_E2E_LOG` through `extensionTestsEnv`. + +The suite activates the mcpp extension, asserts `mcpp.build` and `mcpp.autoConfigureModules` are registered, writes workspace-scoped `mcpp.path` to the fake executable, executes `mcpp.build`, waits for the log file, and asserts the recorded argv is exactly `build`. + +- [ ] **Step 4: Run E2E locally and fix only harness issues** + +Run: `npm run test:e2e` + +Expected: VS Code downloads once, the Extension Host exits zero, and the suite reports all smoke cases passing. A missing display must be handled by running `xvfb-run -a npm run test:e2e`; do not weaken the test to skip activation. + +## Task 6: Add PR CI + +**Files:** +- Create: `.github/workflows/ci.yml` +- Modify: `test/artifacts.test.ts` if workflow assertions are useful + +- [ ] **Step 1: Add workflow source assertions** + +Add tests that parse `.github/workflows/ci.yml` as text and assert `pull_request`, `push` to `main`, `npm ci`, `npm test`, `npm run package`, `npm run test:e2e`, `xvfb-run`, `contents: read`, and a concurrency group are present. Keep the release workflow test unchanged. + +- [ ] **Step 2: Implement the two-job workflow** + +Use Node 22 and `actions/checkout@v4`/`actions/setup-node@v4`. The `unit-and-package` job runs `npm ci`, `npm test`, `npm run package`, and checks the generated VSIX. The `extension-host-e2e` job runs `npm ci`, then `xvfb-run -a npm run test:e2e`. Set `permissions: contents: read` and cancel superseded runs per PR/ref. + +- [ ] **Step 3: Run local workflow-adjacent verification** + +Run: `npm test && npm run package && npm run test:e2e` (with `xvfb-run -a` when needed), then `git diff --check` and `git status --short`. + +Expected: both local jobs' command sequences pass, the VSIX remains valid, and no unrelated files are modified. + +## Task 7: Final Verification And Commits + +- [ ] **Step 1: Run the complete local command** + +Run: `npm run test:all` (prefix the E2E command with `xvfb-run -a` on headless Linux), followed by `npm run package` and `unzip -t mcpp-vscode-*.vsix`. + +- [ ] **Step 2: Review the diff against the approved specs** + +Check that #6 has one modal only, never edits project manifests, reloads context after build, and keeps manual commands unchanged. Check that #7 adds only PR/main CI and deterministic smoke dependencies. + +- [ ] **Step 3: Commit in focused changes** + +```bash +git add src/moduleSetup.ts test/moduleSetup.test.ts src/cliController.ts src/extension.ts test/artifacts.test.ts README.md +git commit -m "feat: make module setup one click" + +git add package.json package-lock.json test/e2e .github/workflows/ci.yml test/artifacts.test.ts +git commit -m "test: add pull request extension checks" +``` + +Do not push or create a PR until the user requests that integration step. diff --git a/docs/superpowers/specs/2026-08-08-one-click-module-setup-design.md b/docs/superpowers/specs/2026-08-08-one-click-module-setup-design.md new file mode 100644 index 0000000..79eadd0 --- /dev/null +++ b/docs/superpowers/specs/2026-08-08-one-click-module-setup-design.md @@ -0,0 +1,143 @@ +# Issue #6 一键配置模块代码提示设计 + +## 背景 + +当前 `mcpp.autoConfigureModules` 把 LLVM 工具链安装、全局默认切换、 +`compile_commands.json` 刷新和 clangd 安装拆成多轮选择与确认。Issue #6 要求把 +入口收敛为“用户主动执行命令,再确认一次”,确认后自动完成其余步骤。 + +本设计基于 `origin/main@1a8ae33`。该基线不包含 `mcpp ide configure` 集成,因此 +CDB 仍由 `mcpp build` 生成;不依赖尚未进入 mcpp 主线的 IDE snapshot 协议。 + +## 目标 + +- 从命令面板或现有快捷入口执行“一键配置模块代码提示”后,最多出现一次模态确认。 +- 确认后自动安装所需 LLVM 工具链、切换全局默认、构建并刷新 CDB、安装匹配的 + clangd、重新配置语言服务并检查模块状态。 +- 不静默修改 `mcpp.toml`、workspace manifest 或 target 配置。 +- 安装、default、clangd 安装或配置失败后立即停止;构建失败仅在产生可用 LLVM CDB + 时进入明确的降级刷新路径,不继续使用旧上下文宣称成功。 +- 保留现有手动“安装工具链”“选择全局默认”“刷新编译数据库”命令。 + +## 非目标 + +- 不引入或回移 `mcpp ide configure`。 +- 不自动安装 xlings 本体,不执行在线安装脚本。 +- 不覆盖项目 `[toolchain]` 或 `[target.*].toolchain`。 +- 不在本次改动中解决真实 LLVM/clangd 跨平台兼容矩阵。 +- 不升级与 issue 无关的 npm 依赖或修复现有 `npm audit` 报告。 + +## 用户流程 + +### 预检 + +扩展在显示确认框前完成以下只读检查: + +1. 当前资源属于 mcpp 工程且工作区已受信任。 +2. 没有其他 mcpp 操作正在运行。 +3. `mcpp toolchain list` 能被当前解析器识别。 +4. 当前项目是否通过 manifest 或 target 覆盖全局默认。 +5. host target 是否已有可用 LLVM 工具链。 + +若当前项目显式固定 GCC/MSVC,流程直接停止并提示用户手动修改项目配置。扩展不能 +通过修改全局默认伪装成切换成功,也不能改写受版本控制的 manifest。 + +### 唯一确认 + +预检通过后显示一个居中的 modal。主文案说明当前工具链和目标 LLVM;detail 列出 +即将执行的动作:可能下载 LLVM、修改 mcpp 全局默认及 host target、执行完整构建、 +可能下载匹配的 llvm-tools,并重启 clangd。用户取消后不产生任何修改。 + +已使用 LLVM 但 clangd/CDB 需要修复时沿用同一 modal,只隐藏不需要的工具链动作。 + +### 自动执行 + +确认后按顺序执行: + +1. host LLVM 不存在时运行 `mcpp toolchain install llvm`,由 mcpp 选择当前索引中的 + 最高可用版本,扩展不复制版本选择策略。 +2. 当前有效工具链不是 LLVM 时运行 `mcpp toolchain default llvm`。mcpp 从已安装 + 版本中选择最高匹配并清空旧的 global `default_target`。 +3. 运行 `mcpp build` 生成与新工具链一致的 CDB 和模块产物。 +4. 构建结束后重新发现工程并重新分析 CDB;禁止继续使用切换前的 `ProjectContext`。 +5. 若没有兼容 clangd,通过已安装的 xlings 执行匹配 LLVM 版本的 + `xim:llvm-tools` 安装。 +6. 再次解析 clangd,写入官方 clangd 扩展配置,重启语言服务并运行模块检查。 +7. 只显示一次最终成功通知;任务过程和失败细节写入现有 mcpp 输出频道/任务终端。 + +构建失败时仍重新读取 CDB:若 CDB 已生成且 clangd 可配置,可以保留编辑能力,但 +最终结果必须说明“构建失败,语言服务已刷新”,不能报告完整成功。 + +## 代码结构 + +### `src/moduleSetup.ts` + +新增纯 TypeScript 模块,负责: + +- 根据工具链 inventory 和当前 capability 生成 `ModuleSetupPlan`。 +- 返回 `project-toolchain-override`、`busy`、`unrecognized-inventory` 等明确阻断原因。 +- 生成 modal 所需的动作摘要。 +- 编排注入的安装、切换、构建、重载、clangd 配置和检查操作。安装/default/clangd + 失败立即终止;构建失败只允许先重载并验证新 CDB,再决定终止或降级继续。 + +该模块不导入 `vscode`,Node 单元测试可以覆盖完整状态机。 + +### `src/cliController.ts` + +增加供一键流程使用的非交互复合操作,不复用会弹 QuickPick 的公开命令。复合操作 +持有一个全局 operation token,直接使用现有 `executeTask`/`runProcess` 依次执行 +LLVM 安装、全局默认切换和构建,完成后释放 token并返回每一阶段的 +`TaskCompletion`。构建后的 clangd 重协调在 token 释放后进行,避免现有 +`McppOperationRegistry` 的 global/project 互斥形成重入死锁。 + +手动命令保持原行为;公共底层执行逻辑只在确实消除重复时抽取。 + +### `src/extension.ts` + +`autoConfigureModulesWizard` 缩减为 VS Code 适配层:加载 inventory、调用 planner、 +显示唯一 modal、执行计划、重新加载 context、调用现有 clangd 配置与模块检查。 +现有 workspace clangd 串行器继续防止两个向导同时修改窗口级 clangd 设置。 + +### `src/cli.ts` 与 `src/tasks.ts` + +只增加 planner 需要的纯判断或执行结果类型;不改变当前 CLI 输出兼容解析规则。 + +## 并发与错误处理 + +- 预检和执行前都检查 operation registry,消除确认期间出现的竞争窗口。 +- mcpp 安装、default 和 build 对其他项目/全局操作表现为一个独占事务。 +- xlings/clangd 阶段仍受 workspace clangd 串行器保护。 +- `cancelled` 与 `failed` 分开呈现;取消不显示错误。 +- xlings 缺失时停止并提供官网链接或设置入口,但不再进入第二轮“安装/跳过”选择。 +- LLVM 安装后仍无法切换、构建后仍是 GCC/MSVC、或 clangd identity 不兼容时均视为失败。 +- 未受信任工作区不执行任何外部程序。 + +## 测试 + +### 单元测试 + +新增 `test/moduleSetup.test.ts`,至少覆盖: + +- 已是 LLVM 且 clangd 就绪,只构建、重载和检查。 +- GCC 且未安装 LLVM,顺序为 install -> default -> build -> reload -> clangd -> check。 +- GCC 且已有 LLVM,跳过 install。 +- 项目显式固定 GCC/MSVC 时阻断且没有任何副作用。 +- 用户取消、operation busy、inventory 不可识别。 +- 安装/default/build/重载/clangd 任一步失败后不执行后续步骤。 +- 构建失败但产生可用 LLVM CDB 时返回降级结果而非完整成功。 + +扩展现有 controller/workflow 测试补充命令参数、锁释放顺序和旧上下文失效行为。 + +### Extension Host E2E + +Issue #7 引入的 Extension Host smoke test 使用 fake mcpp CLI 验证扩展可激活、命令已注册、 +普通项目任务可从配置的 `mcpp.path` 执行。modal 选择和下载分支由纯状态机单测覆盖, +不在 PR CI 中下载真实工具链。 + +## 验收标准 + +- 普通未固定 GCC/MSVC 工程从命令入口到开始执行只有一次 modal 确认。 +- 确认后不出现工具链 QuickPick、默认选择或“是否刷新 CDB”提示。 +- 显式固定工具链的工程不会修改任何 manifest 或全局默认。 +- 自动流程结束时使用重新加载后的 LLVM CDB 和 clangd identity。 +- 所有新增单元测试、现有 `npm test`、VSIX 打包和 Extension Host smoke E2E 通过。 diff --git a/docs/superpowers/specs/2026-08-08-pr-ci-design.md b/docs/superpowers/specs/2026-08-08-pr-ci-design.md new file mode 100644 index 0000000..427a1be --- /dev/null +++ b/docs/superpowers/specs/2026-08-08-pr-ci-design.md @@ -0,0 +1,112 @@ +# Issue #7 PR CI 与 Extension Host E2E 设计 + +## 背景 + +仓库当前只有 `.github/workflows/release.yml`,仅在 `v*` tag push 时执行测试、打包和 +Release 上传。普通 pull request 没有自动检查。现有 `npm test` 是 TypeScript 编译加 +Node `node:test`,没有启动真实 VS Code Extension Host 的 E2E。 + +Issue #7 要求 PR 自动触发单元测试和 E2E,提前发现扩展打包、激活和 VS Code API +接线问题。 + +## 目标 + +- 对 pull request 自动运行编译、全部 Node 单元测试、VSIX 打包校验和 Extension Host + smoke E2E。 +- 对 `main` push 重跑相同检查,确保合并后的提交仍可复现。 +- 单元/打包与 E2E 使用独立 job,失败来源清晰,可分别设为 required check。 +- E2E 在干净 runner 上可重复,不依赖用户缓存或真实工具链下载。 + +## 非目标 + +- 不替代 tag release workflow。 +- 不在每个 PR 下载 mcpp LLVM 工具链、xlings 包或真实 clangd。 +- 不承诺 macOS/Windows 的真实语言服务集成覆盖;本次 smoke lane 使用 Linux。 +- 不在 workflow 中自动修改依赖或执行 `npm audit fix`。 + +## Workflow + +新增 `.github/workflows/ci.yml`: + +- 触发:`pull_request` 和 `push` 到 `main`。 +- 权限:`contents: read`。 +- concurrency:同一 workflow/ref 只保留最新运行,PR 更新时取消旧任务。 +- Node:与发布流程一致使用 Node.js 22 和 npm cache。 + +### `unit-and-package` + +1. `actions/checkout@v4` +2. `actions/setup-node@v4`,Node 22,`cache: npm` +3. `npm ci` +4. `npm test` +5. `npm run package` +6. 校验只生成一个与 `package.json.version` 一致的 VSIX,并执行 ZIP 完整性检查 + +该 job 复用发布前的核心检查,但不创建 Release,也不需要写权限。 + +### `extension-host-e2e` + +1. checkout、setup-node、`npm ci` +2. `npm run compile` +3. 通过 `xvfb-run -a npm run test:e2e` 启动 VS Code Extension Host + +`@vscode/test-electron` 负责下载并启动稳定版 VS Code。首次实现固定 npm lockfile 中的 +测试 runner 版本,不额外固定 VS Code patch;runner 下载失败应表现为明确的 job 失败。 + +## E2E 结构 + +### `test/e2e/runTest.ts` + +Node 启动器解析扩展根目录、编译后的测试入口和 fixture workspace,调用 +`@vscode/test-electron` 的 `runTests`。启动参数关闭更新和遥测,并使用隔离的临时 +user-data/extensions 目录,防止本机配置污染。隔离 extensions 目录只放置测试 fixture +提供的 `llvm-vs-code-extensions.vscode-clangd` stub,满足被测扩展的 dependency,而不 +从 Marketplace 下载真实 clangd 扩展。 + +### `test/e2e/suite/index.ts` + +Extension Host 内的测试入口,负责发现和运行 smoke cases,并在失败时返回非零退出码。 +测试框架只服务 Extension Host E2E;现有纯 Node 测试继续使用 `node:test`。 + +### `test/e2e/suite/extension.test.ts` + +首批 smoke cases: + +- 在包含 `mcpp.toml` 的 fixture 中找到并激活 `mcpp-community.mcpp-vscode`。 +- 验证 `mcpp.autoConfigureModules`、`mcpp.build` 和其他 manifest 命令实际注册到 VS Code。 +- 设置 workspace scoped `mcpp.path` 指向 fake CLI,执行一个无 modal 的项目命令,验证 + fake CLI 收到参数且扩展没有依赖开发机 PATH。 +- 验证 mcpp 工程 context key 驱动的命令路径在 Extension Host 中可用。 + +### `test/e2e/fixtures/` + +提供最小 `mcpp.toml`、源文件、可执行 fake mcpp 和 no-op clangd dependency stub。 +fake CLI 只实现 smoke case 需要的参数,把 argv 写入 fixture 临时记录文件并返回确定 +退出码;stub 只提供扩展 id 和 `clangd.restart` 命令。二者都不伪装真实编译、CDB 或 +clangd 兼容性。 + +## npm Scripts 与依赖 + +- 增加 `test:e2e`,只运行 Extension Host 测试。 +- 保留 `test` 的现有含义,避免本地每次单测都下载 VS Code。 +- 增加 `test:all` 作为本地完整验证入口:`npm test && npm run test:e2e`。 +- 开发依赖增加当前稳定的 `@vscode/test-electron` 及其测试 runner 所需的最小依赖; + 所有版本写入 `package-lock.json`。 + +## 稳定性与安全边界 + +- PR E2E 不访问真实 mcpp/xlings registry,不依赖缓存命中。 +- fake CLI 的路径由测试显式设置,不修改用户级 VS Code 配置。 +- fixture 和日志写入测试临时目录,测试结束后清理。 +- CI 不接收 secrets,不运行来自 issue/PR 文本的 shell 内容。 +- workflow shell 命令只处理仓库内固定路径和从 `package.json` 读取的版本。 +- E2E 超时必须有限,Extension Host 退出失败不能被吞掉。 + +## 验收标准 + +- 新 PR 自动出现独立的 `unit-and-package` 与 `extension-host-e2e` checks。 +- 任一单元测试、TypeScript 编译、VSIX 打包或 Extension Host smoke case 失败时 workflow + 返回失败。 +- 本地 `npm test` 继续快速运行且不下载 VS Code。 +- 本地 `npm run test:e2e` 可在支持的 macOS/Linux 图形或 Xvfb 环境复现。 +- tag release workflow 保持原发布职责,不混入 PR 权限或 E2E 副作用。 diff --git a/package-lock.json b/package-lock.json index 7cd3fcc..ae908dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,12 @@ "version": "0.2.5", "license": "Apache-2.0", "devDependencies": { + "@types/mocha": "^10.0.10", "@types/node": "^24.0.0", "@types/vscode": "^1.90.0", + "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^3.9.2", + "mocha": "^11.8.0", "typescript": "^5.9.0" }, "engines": { @@ -228,6 +231,80 @@ "node": ">=6.9.0" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -266,6 +343,17 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@secretlint/config-creator": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", @@ -537,6 +625,13 @@ "@textlint/ast-node-types": "15.7.1" } }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.13.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", @@ -583,6 +678,23 @@ "node": ">=22.0.0" } }, + "node_modules/@vscode/test-electron": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-3.1.0.tgz", + "integrity": "sha512-CRqv5u+YYoseuNVJ6Tyo4k0sF0mx4qnKMihRB0PjsUF8Dc0WKtCXo6CNL6nWWm5esfFQsQA/pejMj4ZbpJVLTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "jszip": "^3.10.1", + "ora": "^8.1.0", + "semver": "^7.6.2" + }, + "engines": { + "node": ">=22" + } + }, "node_modules/@vscode/vsce": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.2.tgz", @@ -983,6 +1095,13 @@ "node": ">=8" } }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -1073,6 +1192,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1134,6 +1266,22 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -1142,6 +1290,73 @@ "license": "ISC", "optional": true }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cockatiel": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", @@ -1195,6 +1410,28 @@ "node": ">=18" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -1243,6 +1480,19 @@ } } }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -1335,6 +1585,16 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -1409,6 +1669,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -1543,6 +1810,29 @@ "node": ">= 0.4" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -1618,6 +1908,50 @@ "node": ">=8" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", @@ -1668,6 +2002,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1839,6 +2196,16 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -1958,6 +2325,13 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, "node_modules/index-to-position": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", @@ -1976,8 +2350,7 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", @@ -2055,6 +2428,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -2065,22 +2451,69 @@ "node": ">=0.12.0" } }, - "node_modules/is-wsl": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, "license": "MIT", - "dependencies": { - "is-inside-container": "^1.0.0" - }, "engines": { - "node": ">=16" - }, + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/istextorbinary": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", @@ -2099,6 +2532,22 @@ "url": "https://bevry.me/fund" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2192,6 +2641,52 @@ "npm": ">=6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -2238,6 +2733,16 @@ "node": ">=6" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/linkify-it": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", @@ -2258,6 +2763,22 @@ "uc.micro": "^2.0.0" } }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", @@ -2321,6 +2842,23 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -2439,6 +2977,19 @@ "node": ">= 0.6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -2498,6 +3049,151 @@ "license": "MIT", "optional": true }, + "node_modules/mocha": { + "version": "11.8.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.8.0.tgz", + "integrity": "sha512-VyCeUdGN3A9lmCTTgG4yuvY9ixxaDk+xt2R/7/+1AP6EqNG+G9OKkzBwhVtVYoNX8YsxNSgAl8mOv3IAeOpFbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2628,6 +3324,22 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/open": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", @@ -2647,6 +3359,143 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-map": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.6.tgz", @@ -2660,6 +3509,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, "node_modules/parse-json": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", @@ -2751,6 +3614,26 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -2857,6 +3740,13 @@ "node": ">=10" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -2917,6 +3807,16 @@ ], "license": "MIT" }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -3009,6 +3909,30 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3019,6 +3943,23 @@ "node": ">=0.10.0" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -3140,6 +4081,46 @@ "node": ">=10" } }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", @@ -3216,6 +4197,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -3332,6 +4326,19 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -3358,6 +4365,45 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string-width/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -3397,6 +4443,30 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -3719,8 +4789,7 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/validate-npm-package-license": { "version": "3.0.4", @@ -3770,6 +4839,112 @@ "node": ">=18" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3818,6 +4993,16 @@ "node": ">=4.0" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -3825,6 +5010,51 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/yauzl": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", @@ -3847,6 +5077,19 @@ "dependencies": { "buffer-crc32": "~0.2.3" } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 071823f..4ccbd35 100644 --- a/package.json +++ b/package.json @@ -210,12 +210,17 @@ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"", "compile": "tsc -p tsconfig.json", "test": "npm run clean && npm run compile && node --test dist/test/*.test.js", - "package": "npm run clean && npm run compile && vsce package" + "package": "npm run clean && npm run compile && vsce package", + "test:e2e": "npm run compile && node dist/test/e2e/runTest.js", + "test:all": "npm test && npm run test:e2e" }, "devDependencies": { + "@types/mocha": "^10.0.10", "@types/node": "^24.0.0", "@types/vscode": "^1.90.0", + "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^3.9.2", + "mocha": "^11.8.0", "typescript": "^5.9.0" } } diff --git a/src/cliController.ts b/src/cliController.ts index 2e53172..ef77e6a 100644 --- a/src/cliController.ts +++ b/src/cliController.ts @@ -26,6 +26,12 @@ import { } from "./tasks"; import { CLI_COMMANDS, quickMenuItems, quickMenuStatusText } from "./commands"; import { runNewProjectFlow, validateNewProjectName } from "./newProject"; +import { + mcppModuleSetupCommands, + type ModuleSetupCommand, + type ModuleSetupDecision, + type ModuleSetupStepResult, +} from "./moduleSetup"; export interface McppCliControllerOptions { output: vscode.OutputChannel; @@ -112,6 +118,48 @@ export class McppCliController { return this.operations.hasActive(); } + public async runAutomaticModuleSetup( + plan: Extract, + ): Promise { + const project = this.requireProject(); + const commands = mcppModuleSetupCommands(plan); + const firstCommand = commands[0]; + if (project === undefined || !this.requireTrusted() || firstCommand === undefined) { + return { + stage: firstCommand?.stage ?? "build", + state: "failed", + detail: "当前工作区无法执行自动模块配置。", + }; + } + + const token: OperationToken = {}; + if (this.operations.beginGlobal(token) !== undefined) { + return { + stage: firstCommand.stage, + state: "failed", + detail: "已有 mcpp 操作正在运行。", + }; + } + + const executable = this.mcppExecutable(project); + try { + for (const command of commands) { + const result = await this.executeAutomaticModuleSetupCommand( + project, + executable, + command, + ); + if (result.state !== "succeeded") { + return result; + } + } + return { stage: "build", state: "succeeded" }; + } finally { + // 整个 install/default/build 事务共用一个 token,异常时也必须释放。 + this.operations.finishGlobal(token); + } + } + public async runProjectTask(kind: ProjectTaskKind): Promise { const project = this.requireProject(); if (project === undefined || !this.requireTrusted()) { @@ -598,8 +646,8 @@ export class McppCliController { return folder !== undefined && folder.uri.fsPath !== project.root; } - private async readToolchainInventory( - project: McppProjectDiscovery | undefined, + public async readToolchainInventory( + project: McppProjectDiscovery | undefined = this.options.currentProject(), ): Promise { const executable = this.mcppExecutable(project); const args = mcppCommandArguments("toolchain", "list"); @@ -622,6 +670,50 @@ export class McppCliController { return inventory; } + private async executeAutomaticModuleSetupCommand( + project: McppProjectDiscovery, + executable: string, + command: ModuleSetupCommand, + ): Promise { + if (command.mode === "process") { + const result = await runProcess(executable, command.args, project.root); + this.appendShortCommand(`自动模块配置:${command.stage}`, executable, command.args, result); + return { + stage: command.stage, + state: result.exitCode === 0 ? "succeeded" : "failed", + exitCode: result.exitCode, + }; + } + + // build 直接复用任务执行器,避免在 global token 内再次申请 project token。 + let completion: TaskCompletion; + try { + completion = await this.executeTask( + project.root, + executable, + "mcpp: 自动模块配置构建", + command.args, + ); + } catch (error) { + return { + stage: command.stage, + state: "failed", + detail: error instanceof Error ? error.message : String(error), + }; + } + this.appendTaskCompletion( + project.root, + "mcpp: 自动模块配置构建", + command.args, + completion, + ); + return { + stage: command.stage, + state: completion.state, + exitCode: completion.exitCode, + }; + } + private async executeTask( root: string, executable: string, diff --git a/src/extension.ts b/src/extension.ts index 802b186..199dbcf 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -45,6 +45,13 @@ import { } from "./workflow"; import { classifyTaskExit, type TaskCompletion } from "./tasks"; import { MCPP_MANIFEST_GLOB, registerInProjectContext } from "./inProject"; +import { + buildModuleSetupPlan, + executeModuleSetup, + moduleSetupConfirmation, + type ModuleSetupBlockedReason, + type ModuleSetupStepResult, +} from "./moduleSetup"; const COMMAND_CONFIGURE = "mcpp.configureClangd"; const COMMAND_REFRESH = "mcpp.refreshCompilationDatabase"; @@ -134,6 +141,19 @@ function loadProjectContext(project: McppProjectDiscovery | undefined = findCurr } } +function moduleSetupBlockedMessage(reason: ModuleSetupBlockedReason): string { + switch (reason) { + case "project-toolchain-override": + return "当前项目显式固定了非 LLVM 工具链;一键配置不会修改 mcpp.toml,请先手动切换项目工具链。"; + case "untrusted": + return "当前工作区未受信任,不会执行外部程序。请先信任工作区。"; + case "busy": + return "已有 mcpp 操作正在运行,请等待完成后再试。"; + case "unrecognized-inventory": + return "无法识别 mcpp 工具链状态,请查看 mcpp 输出频道并检查 mcpp 版本。"; + } +} + function configurationTarget(uri: vscode.Uri): vscode.ConfigurationTarget { return vscode.workspace.getWorkspaceFolder(uri) === undefined ? vscode.ConfigurationTarget.Workspace @@ -685,171 +705,124 @@ async function autoConfigureModulesWizard( ): Promise { appendOutputLine(output, "[一键配置] 开始一键配置模块代码提示..."); - // Step 1: Guard checks - if (context.analysis.capability === "unavailable") { - await vscode.window.showErrorMessage(context.analysis.reason); + if (!vscode.workspace.isTrusted) { + await vscode.window.showWarningMessage(moduleSetupBlockedMessage("untrusted")); return; } - if (context.analysis.capability === "syntax-only") { - const kind = context.analysis.kind.toUpperCase(); - const install = "安装 LLVM 工具链"; - const select = "选择默认工具链"; - const choice = await vscode.window.showErrorMessage( - `当前工具链是 ${kind},其模块产物不能被 clangd 消费,模块代码提示功能不可用。\n\n如需使用模块代码提示,请先切换到 LLVM 工具链后重新构建项目。`, - install, - select, - "关闭", - ); - if (choice === install) { - await vscode.commands.executeCommand(CLI_COMMANDS.installToolchain); - } else if (choice === select) { - await vscode.commands.executeCommand(CLI_COMMANDS.selectDefaultToolchain); - } + const inventory = await cliController.readToolchainInventory(context.project); + if (inventory === undefined) { return; } - if (context.analysis.compilerPath === undefined) { - await vscode.window.showErrorMessage("compile_commands.json 中没有可用的编译器路径。"); + const decision = buildModuleSetupPlan( + inventory, + context.analysis.capability, + vscode.workspace.isTrusted, + cliController.isBusy(), + ); + if (decision.kind === "blocked") { + await vscode.window.showWarningMessage(moduleSetupBlockedMessage(decision.reason)); return; } - if (!workspaceAllowsToolExecution(vscode.workspace.isTrusted)) { - await vscode.window.showWarningMessage( - "当前工作区未受信任,不会执行外部程序。请先信任工作区。", - ); + + const confirmation = moduleSetupConfirmation(context.analysis.capability, decision); + const choice = await vscode.window.showWarningMessage( + confirmation.message, + { modal: true, detail: confirmation.detail }, + "确认一键配置", + ); + if (choice !== "确认一键配置") { return; } - // Step 2: Check current clangd state - const compilerVersion = await runToolVersion(context.analysis.compilerPath); - appendOutputLine( - output, - `[一键配置] 编译器版本:LLVM ${llvmToolsVersionSpec(compilerVersion.identity ?? { major: 0, minor: 0, patch: 0 })}`, - ); + let currentContext = context; + const outcome = await executeModuleSetup(decision, { + preparePlan: () => cliController.runAutomaticModuleSetup(decision), + reload: async (): Promise => { + const refreshed = loadProjectContext(context.project); + if (refreshed === undefined) { + return { stage: "reload", state: "failed", detail: "无法重新加载 mcpp 工程。" }; + } + currentContext = refreshed; + updateStatusBar(status, currentContext); + if (currentContext.analysis.capability !== "full" || currentContext.analysis.kind !== "llvm") { + return { + stage: "reload", + state: "failed", + detail: "构建后没有得到可供 clangd 使用的 LLVM 编译数据库。", + }; + } + return { stage: "reload", state: "succeeded" }; + }, + ensureClangd: async (): Promise => { + const clangd = await resolveClangd(currentContext); + if (clangd?.comparison.compatible) { + const configured = await configureClangd(currentContext, status, output, "automatic", true); + return configured + ? { stage: "clangd", state: "succeeded" } + : { stage: "clangd", state: "failed", detail: "clangd 配置未完成。" }; + } - const clangd = await resolveClangd(context); - if (clangd?.comparison.compatible) { - appendOutputLine(output, "[一键配置] 已找到兼容的 clangd,跳过安装步骤。"); - // Skip to configuration - } else { - if (clangd !== undefined) { + const xlingsPath = findXlingsExecutable(); + const compilerPath = currentContext.analysis.compilerPath; + if (xlingsPath === undefined || compilerPath === undefined) { + return { + stage: "clangd", + state: "failed", + detail: "未找到 xlings 或 LLVM 编译器,无法安装匹配的 llvm-tools。", + }; + } + const compilerVersion = await runToolVersion(compilerPath); + const installArgs = xlingsInstallArgs( + compilerVersion.identity === undefined ? undefined : llvmToolsVersionSpec(compilerVersion.identity), + ); + const installed = await executeXlingsInstallTask(xlingsPath, installArgs, currentContext.project.root); appendOutputLine( output, - `[一键配置] 未找到兼容的 clangd。原因:${clangd.comparison.reason}`, + `[一键配置] xlings install 完成(退出码 ${installed.exitCode ?? "未知"})`, ); - } else { - appendOutputLine(output, "[一键配置] 未找到任何可用的 clangd。"); - } - - // Step 3: Check xlings - const xlingsPath = findXlingsExecutable(); - if (xlingsPath === undefined) { - const installXlings = "安装 xlings"; - const setPath = "设置自定义 clangd 路径"; - const choice = await vscode.window.showErrorMessage( - "未找到 xlings,无法自动下载匹配的 clangd。请先安装 xlings(https://xlings.org),\n或手动安装与编译器版本匹配的 llvm-tools,或在设置中指定 clangd 路径。", - installXlings, - setPath, - "跳过", - ); - if (choice === installXlings) { - await vscode.env.openExternal( - vscode.Uri.parse("https://xlings.org"), - ); - } else if (choice === setPath) { - await vscode.commands.executeCommand( - "workbench.action.openSettings", - "mcpp.clangd.path", - ); + if (installed.state !== "succeeded") { + return { + stage: "clangd", + state: installed.state, + exitCode: installed.exitCode, + detail: installed.state === "cancelled" ? "llvm-tools 安装已取消。" : "llvm-tools 安装失败。", + }; } - return; - } - // Step 4: Offer installation - const version = compilerVersion.identity !== undefined - ? llvmToolsVersionSpec(compilerVersion.identity) - : "latest"; - const install = "$(cloud-download) 安装 llvm-tools(推荐)"; - const setPath = "$(settings-gear) 设置自定义 clangd 路径"; - const choice = await vscode.window.showWarningMessage( - `未找到与 mcpp 编译器匹配的 clangd(LLVM ${version})。是否通过 xlings 安装 llvm-tools?`, - { modal: true, detail: "xlings 将自动下载并安装匹配版本的 clangd、clang-format 和 clang-tidy。" }, - install, - setPath, - "跳过", - ); - if (choice === setPath) { - await vscode.commands.executeCommand( - "workbench.action.openSettings", - "mcpp.clangd.path", - ); - return; - } - if (choice !== install) { - return; - } - - // Check concurrency - if (cliController.isBusy()) { - await vscode.window.showWarningMessage( - "已有 mcpp 操作正在运行,请等待完成后再试。", - ); - return; - } - - // Note: xlings install is not tracked by McppOperationRegistry because it - // operates on xlings-managed paths (not mcpp). The serial executor - // (executeWithWorkspaceClangd) prevents concurrent wizard runs, and the - // isBusy() check catches active mcpp operations before install starts. + const refreshed = loadProjectContext(currentContext.project); + if (refreshed === undefined) { + return { stage: "clangd", state: "failed", detail: "安装 llvm-tools 后无法重新加载工程。" }; + } + currentContext = refreshed; + updateStatusBar(status, currentContext); + const resolved = await resolveClangd(currentContext); + if (resolved === undefined || !resolved.comparison.compatible) { + return { stage: "clangd", state: "failed", detail: "未找到与 LLVM 编译器匹配的 clangd。" }; + } + const configured = await configureClangd(currentContext, status, output, "automatic", true); + return configured + ? { stage: "clangd", state: "succeeded" } + : { stage: "clangd", state: "failed", detail: "clangd 配置未完成。" }; + }, + checkModules: async (): Promise => { + const moduleStatus = await runModuleSupportCheck(currentContext, status, output, "automatic"); + return moduleStatus?.state === "available" + ? { stage: "check", state: "succeeded" } + : { stage: "check", state: "failed", detail: "模块支持检查未通过。" }; + }, + }); - // Execute xlings install - appendOutputLine(output, `[一键配置] 正在通过 xlings 安装 llvm-tools(${xlingsPath})...`); - const installArgs = xlingsInstallArgs( - compilerVersion.identity !== undefined ? version : undefined, - ); - const xlingsResult = await executeXlingsInstallTask( - xlingsPath, - installArgs, - context.project.root, - ); - appendOutputLine( - output, - `[一键配置] xlings install 完成(退出码 ${xlingsResult.exitCode ?? "未知"})`, - ); - if (xlingsResult.state !== "succeeded") { - await vscode.window.showErrorMessage( - `llvm-tools 安装${xlingsResult.state === "cancelled" ? "已取消" : "失败"}。请查看任务终端获取详细信息。`, - ); - return; + if (outcome.state === "succeeded") { + if (outcome.degraded) { + await vscode.window.showWarningMessage("构建失败,语言服务已刷新。请查看任务终端获取构建错误。"); + } else { + await vscode.window.showInformationMessage("mcpp 模块代码提示一键配置完成。"); } + } else if (outcome.state === "cancelled") { + await vscode.window.showWarningMessage(`一键配置已取消(${outcome.stage})。`); + } else { + await vscode.window.showErrorMessage(`一键配置失败(${outcome.stage})。${outcome.steps.at(-1)?.detail ?? "请查看 mcpp 输出频道。"}`); } - - // Step 5: Re-resolve clangd - const resolvedClangd = await resolveClangd(context); - if (resolvedClangd === undefined || !resolvedClangd.comparison.compatible) { - await vscode.window.showErrorMessage( - "llvm-tools 安装完成,但未找到匹配的 clangd。请检查 xlings 安装日志或手动设置 mcpp.clangd.path。", - ); - return; - } - appendOutputLine(output, `[一键配置] 已找到 clangd:${resolvedClangd.path}`); - - // Step 6: Configure clangd - await configureClangd(context, status, output, "interactive", true); - - // Step 7: Offer rebuild - const rebuild = "$(sync) 刷新编译数据库"; - const done = "$(close) 完成"; - const postChoice = await vscode.window.showInformationMessage( - "clangd 配置完成。建议刷新编译数据库以生成模块 PCM。", - rebuild, - done, - ); - if (postChoice === rebuild) { - await cliController.runProjectTask("build"); - } - - // Step 8: Refresh module status and update status bar - await runModuleSupportCheck(context, status, output, "automatic"); - appendOutputLine(output, `[一键配置] 一键配置完成。clangd:${resolvedClangd.path}`); } export async function activate(extensionContext: vscode.ExtensionContext): Promise { diff --git a/src/moduleSetup.ts b/src/moduleSetup.ts new file mode 100644 index 0000000..14d962a --- /dev/null +++ b/src/moduleSetup.ts @@ -0,0 +1,207 @@ +import { hostDefaultToolchains, type ToolchainInventory } from "./cli"; + +export type ModuleSetupStage = "install" | "default" | "build" | "reload" | "clangd" | "check"; +export type ModuleSetupStepState = "succeeded" | "failed" | "cancelled"; +export type ModuleSetupBlockedReason = + | "untrusted" + | "busy" + | "project-toolchain-override" + | "unrecognized-inventory"; + +export type ModuleSetupDecision = + | { + kind: "ready"; + installLlvm: boolean; + switchDefault: boolean; + } + | { + kind: "blocked"; + reason: ModuleSetupBlockedReason; + }; + +type ModuleSetupPlan = Extract; +type ModuleCapability = "full" | "syntax-only" | "unavailable"; + +export function buildModuleSetupPlan( + inventory: ToolchainInventory, + _capability: ModuleCapability, + trusted: boolean, + busy: boolean, +): ModuleSetupDecision { + if (!trusted) { + return { kind: "blocked", reason: "untrusted" }; + } + if (!inventory.recognized) { + return { kind: "blocked", reason: "unrecognized-inventory" }; + } + if (busy) { + return { kind: "blocked", reason: "busy" }; + } + + const effectiveIsLlvm = inventory.effective?.family.toLowerCase() === "llvm"; + if (inventory.projectOverridesGlobal && inventory.effective !== undefined && !effectiveIsLlvm) { + return { kind: "blocked", reason: "project-toolchain-override" }; + } + + // target-only payload 不能成为 host 默认工具链,因此必须复用现有筛选规则。 + const installedHostLlvm = hostDefaultToolchains(inventory) + .some((toolchain) => toolchain.family.toLowerCase() === "llvm"); + return { + kind: "ready", + installLlvm: !installedHostLlvm, + switchDefault: !effectiveIsLlvm, + }; +} + +export interface ModuleSetupCommand { + stage: "install" | "default" | "build"; + mode: "task" | "process"; + args: string[]; +} + +export function mcppModuleSetupCommands(plan: ModuleSetupPlan): ModuleSetupCommand[] { + const commands: ModuleSetupCommand[] = []; + if (plan.installLlvm) { + commands.push({ + stage: "install", + mode: "task", + args: ["toolchain", "install", "llvm"], + }); + } + if (plan.switchDefault) { + commands.push({ + stage: "default", + mode: "process", + args: ["toolchain", "default", "llvm"], + }); + } + commands.push({ stage: "build", mode: "task", args: ["build", "--no-cache"] }); + return commands; +} + +export interface ModuleSetupConfirmation { + message: string; + detail: string; +} + +export function moduleSetupConfirmation( + capability: ModuleCapability, + plan: ModuleSetupPlan, +): ModuleSetupConfirmation { + const message = capability === "full" + ? "当前已具备 LLVM 模块能力,是否重新构建并配置模块代码提示?" + : capability === "syntax-only" + ? "当前仅提供模块语法高亮,是否切换到 LLVM 并配置模块代码提示?" + : "当前缺少可用的模块编译数据库,是否配置 LLVM 并构建项目?"; + const installDetail = plan.installLlvm + ? "流程可能下载 LLVM/llvm-tools" + : "将复用已安装的 LLVM;补齐配套组件时仍可能下载 LLVM/llvm-tools"; + const defaultDetail = plan.switchDefault + ? "并把 LLVM 设为全局默认工具链" + : "不会修改全局默认工具链"; + return { + message, + detail: `${installDetail},${defaultDetail};不会编辑项目清单 mcpp.toml。`, + }; +} + +export interface ModuleSetupStepResult { + stage: ModuleSetupStage; + state: ModuleSetupStepState; + detail?: string; + exitCode?: number; +} + +export interface ModuleSetupOperations { + prepare?: (command: ModuleSetupCommand) => Promise; + preparePlan?: () => Promise; + reload(): Promise; + ensureClangd(): Promise; + checkModules(): Promise; +} + +export type ModuleSetupOutcome = + | { + state: "succeeded"; + degraded: boolean; + steps: ModuleSetupStepResult[]; + } + | { + state: "failed" | "cancelled"; + stage: ModuleSetupStage; + degraded: false; + steps: ModuleSetupStepResult[]; + }; + +function stoppedOutcome( + result: ModuleSetupStepResult, + steps: ModuleSetupStepResult[], +): Exclude { + return { + state: result.state === "cancelled" ? "cancelled" : "failed", + stage: result.stage, + degraded: false, + steps, + }; +} + +export async function executeModuleSetup( + plan: ModuleSetupPlan, + operations: ModuleSetupOperations, +): Promise { + const steps: ModuleSetupStepResult[] = []; + let buildFailed = false; + + const commands = mcppModuleSetupCommands(plan); + if (operations.preparePlan !== undefined) { + const result = await operations.preparePlan(); + steps.push(result); + if (result.state !== "succeeded") { + if (result.stage === "build" && result.state === "failed") { + buildFailed = true; + } else { + return stoppedOutcome(result, steps); + } + } + } else { + if (operations.prepare === undefined) { + const firstCommand = commands[0]; + if (firstCommand === undefined) { + return stoppedOutcome({ stage: "build", state: "failed" }, steps); + } + return stoppedOutcome({ stage: firstCommand.stage, state: "failed" }, steps); + } + for (const command of commands) { + const result = await operations.prepare(command); + steps.push(result); + if (result.state === "succeeded") { + continue; + } + if (command.stage === "build" && result.state === "failed") { + buildFailed = true; + break; + } + return stoppedOutcome(result, steps); + } + } + + // 构建失败后仍尝试恢复现有 CDB;只有完整恢复链成功才算降级成功。 + const recoveryOperations: Array<() => Promise> = [ + () => operations.reload(), + () => operations.ensureClangd(), + () => operations.checkModules(), + ]; + for (const operation of recoveryOperations) { + const result = await operation(); + steps.push(result); + if (result.state !== "succeeded") { + return stoppedOutcome(result, steps); + } + } + + return { + state: "succeeded", + degraded: buildFailed, + steps, + }; +} diff --git a/test/artifacts.test.ts b/test/artifacts.test.ts index e9f304b..4ab77e4 100644 --- a/test/artifacts.test.ts +++ b/test/artifacts.test.ts @@ -63,6 +63,43 @@ test("declares the official clangd dependency and mcpp commands", () => { }); }); +test("自动模块配置使用非交互复合操作和单一全局锁", () => { + const source = readFileSync(path.join(root, "src/cliController.ts"), "utf8"); + assert.match(source, /public async readToolchainInventory\s*\(/); + const start = source.indexOf("public async runAutomaticModuleSetup"); + assert.notEqual(start, -1); + const end = source.indexOf("public async ", start + 10); + const method = source.slice(start, end === -1 ? source.length : end); + assert.match(method, /mcppModuleSetupCommands\(plan\)/); + assert.match(method, /beginGlobal\(token\)/); + assert.match(method, /finally\s*\{[\s\S]*finishGlobal\(token\)/); + assert.doesNotMatch(method, /pickInstallSpec|selectDefaultToolchainFromInventory|showQuickPick|showWarningMessage/); +}); + +test("一键向导只有一次确认并在构建后重载上下文", () => { + const source = readFileSync(path.join(root, "src/extension.ts"), "utf8"); + const start = source.indexOf("async function autoConfigureModulesWizard"); + const end = source.indexOf("export async function activate", start); + assert.notEqual(start, -1); + assert.notEqual(end, -1); + const wizard = source.slice(start, end); + for (const expected of [ + "readToolchainInventory", + "buildModuleSetupPlan", + "moduleSetupConfirmation", + "runAutomaticModuleSetup", + "executeModuleSetup", + "loadProjectContext", + ]) { + assert.match(wizard, new RegExp(expected)); + } + assert.match(wizard, /modal:\s*true/); + assert.match(wizard, /let currentContext/); + assert.doesNotMatch(wizard, /CLI_COMMANDS\.(installToolchain|selectDefaultToolchain)/); + assert.doesNotMatch(wizard, /刷新编译数据库|showQuickPick|showInputBox|maybeDisableCppTools/); + assert.doesNotMatch(wizard, /configureClangd\([^\n]*"interactive"/); +}); + test("shows editor title buttons only inside mcpp projects", () => { const manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")) as PackageManifest; assert.deepEqual(manifest.contributes?.menus?.["editor/title"], [ @@ -297,3 +334,19 @@ test("tag release 工作流校验版本并发布 VSIX", () => { assert.match(workflow, /gh release create/); assert.match(workflow, /gh release upload.*--clobber/s); }); + +test("PR CI 分离单元打包和 Extension Host E2E", () => { + const workflow = readFileSync(path.join(root, ".github/workflows/ci.yml"), "utf8"); + assert.match(workflow, /pull_request:/); + assert.match(workflow, /push:\s*\n\s+branches:\s*\n\s+- main/); + assert.match(workflow, /permissions:\s*\n\s+contents: read/); + assert.match(workflow, /concurrency:[\s\S]*cancel-in-progress: true/); + assert.match(workflow, /node-version: 22/); + assert.match(workflow, /unit-and-package:/); + assert.match(workflow, /extension-host-e2e:/); + assert.match(workflow, /npm ci/); + assert.match(workflow, /npm test/); + assert.match(workflow, /npm run package/); + assert.match(workflow, /unzip -t/); + assert.match(workflow, /xvfb-run -a npm run test:e2e/); +}); diff --git a/test/e2e/fixtures/clangd-stub/extension.js b/test/e2e/fixtures/clangd-stub/extension.js new file mode 100644 index 0000000..97cc7ef --- /dev/null +++ b/test/e2e/fixtures/clangd-stub/extension.js @@ -0,0 +1,9 @@ +const vscode = require("vscode"); + +function activate(context) { + context.subscriptions.push( + vscode.commands.registerCommand("clangd.restart", () => undefined), + ); +} + +module.exports = { activate }; diff --git a/test/e2e/fixtures/clangd-stub/package.json b/test/e2e/fixtures/clangd-stub/package.json new file mode 100644 index 0000000..19a5588 --- /dev/null +++ b/test/e2e/fixtures/clangd-stub/package.json @@ -0,0 +1,9 @@ +{ + "name": "vscode-clangd", + "displayName": "clangd stub", + "version": "0.0.0", + "publisher": "llvm-vs-code-extensions", + "engines": { "vscode": "^1.90.0" }, + "main": "./extension.js", + "activationEvents": ["onCommand:clangd.restart"] +} diff --git a/test/e2e/fixtures/fake-mcpp.js b/test/e2e/fixtures/fake-mcpp.js new file mode 100644 index 0000000..76daadf --- /dev/null +++ b/test/e2e/fixtures/fake-mcpp.js @@ -0,0 +1,9 @@ +#!/usr/bin/env node + +const fs = require("node:fs"); + +const logPath = process.env.MCPP_E2E_LOG; +if (typeof logPath === "string") { + fs.appendFileSync(logPath, `${process.argv.slice(2).join(" ")}\n`); +} +process.exit(process.argv.slice(2).join(" ") === "build" ? 0 : 1); diff --git a/test/e2e/fixtures/project/main.cpp b/test/e2e/fixtures/project/main.cpp new file mode 100644 index 0000000..76e8197 --- /dev/null +++ b/test/e2e/fixtures/project/main.cpp @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/test/e2e/fixtures/project/mcpp.toml b/test/e2e/fixtures/project/mcpp.toml new file mode 100644 index 0000000..055092f --- /dev/null +++ b/test/e2e/fixtures/project/mcpp.toml @@ -0,0 +1,3 @@ +[package] +name = "mcpp-vscode-e2e" +version = "0.0.0" diff --git a/test/e2e/runTest.ts b/test/e2e/runTest.ts new file mode 100644 index 0000000..e7d5699 --- /dev/null +++ b/test/e2e/runTest.ts @@ -0,0 +1,57 @@ +import { chmodSync, copyFileSync, mkdirSync, mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; + +import { runTests } from "@vscode/test-electron"; + +async function main(): Promise { + const repositoryRoot = resolve(__dirname, "../../.."); + const fixtureRoot = join(repositoryRoot, "test/e2e/fixtures"); + const tempRoot = mkdtempSync(join(tmpdir(), "mcpp-vscode-e2e-")); + const userDataDir = join(tempRoot, "user-data"); + const extensionsDir = join(tempRoot, "extensions"); + const workspaceDir = join(tempRoot, "project"); + const fakeMcpp = join(tempRoot, "mcpp"); + const logPath = join(tempRoot, "mcpp.log"); + const clangdStub = join(extensionsDir, "llvm-vs-code-extensions.vscode-clangd-0.0.0"); + + mkdirSync(userDataDir, { recursive: true }); + mkdirSync(extensionsDir, { recursive: true }); + mkdirSync(workspaceDir, { recursive: true }); + mkdirSync(clangdStub, { recursive: true }); + copyFileSync(join(fixtureRoot, "fake-mcpp.js"), fakeMcpp); + chmodSync(fakeMcpp, 0o755); + copyFileSync(join(fixtureRoot, "clangd-stub/package.json"), join(clangdStub, "package.json")); + copyFileSync(join(fixtureRoot, "clangd-stub/extension.js"), join(clangdStub, "extension.js")); + copyFileSync(join(fixtureRoot, "project/mcpp.toml"), join(workspaceDir, "mcpp.toml")); + copyFileSync(join(fixtureRoot, "project/main.cpp"), join(workspaceDir, "main.cpp")); + + try { + await runTests({ + version: "1.90.2", + extensionDevelopmentPath: repositoryRoot, + extensionTestsPath: join(repositoryRoot, "dist/test/e2e/suite/index.js"), + launchArgs: [ + workspaceDir, + "--user-data-dir", + userDataDir, + "--extensions-dir", + extensionsDir, + "--disable-updates", + "--skip-welcome", + "--disable-workspace-trust", + ], + extensionTestsEnv: { + MCPP_E2E_FAKE_MCPP: fakeMcpp, + MCPP_E2E_LOG: logPath, + }, + }); + } finally { + rmSync(tempRoot, { recursive: true, force: true }); + } +} + +void main().catch((error: unknown) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/test/e2e/suite/extension.test.ts b/test/e2e/suite/extension.test.ts new file mode 100644 index 0000000..4859237 --- /dev/null +++ b/test/e2e/suite/extension.test.ts @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; +import { existsSync, readFileSync } from "node:fs"; +import * as path from "node:path"; +import * as vscode from "vscode"; + +suite("mcpp extension smoke", () => { + test("activates, registers commands and executes a configured build", async () => { + const extension = vscode.extensions.getExtension("mcpp-community.mcpp-vscode"); + assert.ok(extension, "mcpp extension should be installed in development host"); + await extension.activate(); + + const commands = await vscode.commands.getCommands(true); + assert.ok(commands.includes("mcpp.build")); + assert.ok(commands.includes("mcpp.autoConfigureModules")); + + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + assert.ok(workspaceFolder, "fixture workspace should be open"); + const fakeMcpp = process.env.MCPP_E2E_FAKE_MCPP; + const logPath = process.env.MCPP_E2E_LOG; + assert.ok(fakeMcpp); + assert.ok(logPath); + await vscode.workspace.getConfiguration("mcpp", workspaceFolder.uri) + .update("path", fakeMcpp, vscode.ConfigurationTarget.Workspace); + + void vscode.commands.executeCommand("mcpp.build"); + const deadline = Date.now() + 15_000; + while (!existsSync(logPath) && Date.now() < deadline) { + await new Promise((resolvePromise) => setTimeout(resolvePromise, 100)); + } + assert.ok(existsSync(logPath), "fake mcpp should have been invoked"); + assert.equal(readFileSync(logPath, "utf8").trim(), "build"); + assert.equal(path.basename(workspaceFolder.uri.fsPath), "project"); + }); +}); diff --git a/test/e2e/suite/index.ts b/test/e2e/suite/index.ts new file mode 100644 index 0000000..7181a3d --- /dev/null +++ b/test/e2e/suite/index.ts @@ -0,0 +1,16 @@ +import Mocha from "mocha"; +import path from "node:path"; + +export function run(): Promise { + const mocha = new Mocha({ ui: "tdd", color: true, timeout: 30_000 }); + mocha.addFile(path.resolve(__dirname, "extension.test.js")); + return new Promise((resolvePromise, reject) => { + mocha.run((failures) => { + if (failures === 0) { + resolvePromise(); + } else { + reject(new Error(`${failures} Extension Host smoke test(s) failed.`)); + } + }); + }); +} diff --git a/test/moduleSetup.test.ts b/test/moduleSetup.test.ts new file mode 100644 index 0000000..c6fad77 --- /dev/null +++ b/test/moduleSetup.test.ts @@ -0,0 +1,441 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import type { ToolchainInventory, ToolchainItem } from "../src/cli"; +import { + buildModuleSetupPlan, + executeModuleSetup, + mcppModuleSetupCommands, + moduleSetupConfirmation, + type ModuleSetupDecision, + type ModuleSetupOperations, + type ModuleSetupStage, + type ModuleSetupStepResult, + type ModuleSetupStepState, +} from "../src/moduleSetup"; + +function toolchain(family: string, effective = false): ToolchainItem { + return { + family, + version: "1.0.0", + spec: `${family}@1.0.0`, + source: "managed", + effective, + }; +} + +function inventory( + overrides: Partial = {}, +): ToolchainInventory { + return { + installed: [], + available: [], + targets: [], + effective: undefined, + effectiveTarget: undefined, + globalDefaultSpec: undefined, + projectOverridesGlobal: false, + recognized: true, + rawOutput: "", + ...overrides, + }; +} + +function ready(decision: ModuleSetupDecision): Extract { + if (decision.kind !== "ready") { + assert.fail(`expected ready plan, got ${decision.reason}`); + } + return decision; +} + +test("GCC 生效且没有已安装的 host LLVM 时计划安装并切换默认工具链", () => { + const gcc = toolchain("gcc", true); + const plan = buildModuleSetupPlan( + inventory({ installed: [gcc], effective: gcc }), + "unavailable", + true, + false, + ); + + assert.deepEqual(plan, { + kind: "ready", + installLlvm: true, + switchDefault: true, + }); +}); + +test("已安装 host LLVM 但 GCC 生效时只切换默认工具链", () => { + const gcc = toolchain("gcc", true); + const llvm = toolchain("llvm"); + const plan = buildModuleSetupPlan( + inventory({ installed: [gcc, llvm], effective: gcc }), + "syntax-only", + true, + false, + ); + + assert.deepEqual(plan, { + kind: "ready", + installLlvm: false, + switchDefault: true, + }); +}); + +test("LLVM 已生效时不重复安装或切换", () => { + const llvm = toolchain("llvm", true); + const plan = buildModuleSetupPlan( + inventory({ installed: [llvm], effective: llvm }), + "full", + true, + false, + ); + + assert.deepEqual(plan, { + kind: "ready", + installLlvm: false, + switchDefault: false, + }); +}); + +test("没有 effective 工具链时安装 LLVM 后设置全局默认", () => { + const plan = buildModuleSetupPlan( + inventory(), + "unavailable", + true, + false, + ); + + assert.deepEqual(plan, { + kind: "ready", + installLlvm: true, + switchDefault: true, + }); +}); + +test("available LLVM 不算已安装的 host 工具链", () => { + const gcc = toolchain("gcc", true); + const availableLlvm = toolchain("llvm"); + const plan = buildModuleSetupPlan( + inventory({ installed: [gcc], available: [availableLlvm], effective: gcc }), + "syntax-only", + true, + false, + ); + + assert.equal(ready(plan).installLlvm, true); +}); + +test("只有 target payload 的 LLVM 不算可设为默认的 host 工具链", () => { + const gcc = toolchain("gcc", true); + const llvm = toolchain("llvm"); + const plan = buildModuleSetupPlan( + inventory({ + installed: [gcc, llvm], + effective: gcc, + targets: [{ + target: "x86_64-linux-musl", + note: "static", + toolchainSpec: llvm.spec, + status: "installed", + effective: false, + }], + }), + "syntax-only", + true, + false, + ); + + assert.equal(ready(plan).installLlvm, true); +}); + +test("项目显式固定非 LLVM 时阻止修改全局默认", () => { + const gcc = toolchain("gcc", true); + const plan = buildModuleSetupPlan( + inventory({ + installed: [gcc], + effective: gcc, + projectOverridesGlobal: true, + }), + "syntax-only", + true, + false, + ); + + assert.deepEqual(plan, { + kind: "blocked", + reason: "project-toolchain-override", + }); +}); + +test("项目显式固定 LLVM 时仍允许继续配置", () => { + const llvm = toolchain("llvm", true); + const plan = buildModuleSetupPlan( + inventory({ + installed: [llvm], + effective: llvm, + projectOverridesGlobal: true, + }), + "full", + true, + false, + ); + + assert.deepEqual(plan, { + kind: "ready", + installLlvm: false, + switchDefault: false, + }); +}); + +test("阻止原因按未信任、未知清单、忙碌、项目覆盖的顺序判定", () => { + const gcc = toolchain("gcc", true); + const conflicting = inventory({ + installed: [gcc], + effective: gcc, + projectOverridesGlobal: true, + recognized: false, + }); + + assert.deepEqual( + buildModuleSetupPlan(conflicting, "unavailable", false, true), + { kind: "blocked", reason: "untrusted" }, + ); + assert.deepEqual( + buildModuleSetupPlan(conflicting, "unavailable", true, true), + { kind: "blocked", reason: "unrecognized-inventory" }, + ); + assert.deepEqual( + buildModuleSetupPlan({ ...conflicting, recognized: true }, "unavailable", true, true), + { kind: "blocked", reason: "busy" }, + ); +}); + +test("模块设置命令使用固定 argv 数组且始终构建", () => { + assert.deepEqual( + mcppModuleSetupCommands({ kind: "ready", installLlvm: true, switchDefault: true }), + [ + { stage: "install", mode: "task", args: ["toolchain", "install", "llvm"] }, + { stage: "default", mode: "process", args: ["toolchain", "default", "llvm"] }, + { stage: "build", mode: "task", args: ["build", "--no-cache"] }, + ], + ); + assert.deepEqual( + mcppModuleSetupCommands({ kind: "ready", installLlvm: false, switchDefault: false }), + [{ stage: "build", mode: "task", args: ["build", "--no-cache"] }], + ); +}); + +test("确认文案按能力解释现状并完整说明副作用边界", () => { + const plan = { kind: "ready", installLlvm: true, switchDefault: true } as const; + const confirmations = [ + moduleSetupConfirmation("full", plan), + moduleSetupConfirmation("syntax-only", plan), + moduleSetupConfirmation("unavailable", plan), + ]; + + assert.match(confirmations[0].message, /模块代码提示/); + assert.match(confirmations[1].message, /语法高亮/); + assert.match(confirmations[2].message, /编译数据库/); + assert.equal(new Set(confirmations.map((item) => item.message)).size, 3); + for (const confirmation of confirmations) { + assert.match(confirmation.detail, /LLVM\/llvm-tools/); + assert.match(confirmation.detail, /全局默认/); + assert.match(confirmation.detail, /不会编辑项目.*mcpp\.toml/); + } +}); + +test("确认文案说明将复用已安装的 LLVM", () => { + const confirmation = moduleSetupConfirmation( + "syntax-only", + { kind: "ready", installLlvm: false, switchDefault: true }, + ); + + assert.match(confirmation.detail, /将复用已安装的 LLVM/); + assert.match(confirmation.detail, /补齐配套组件时仍可能下载 LLVM\/llvm-tools/); +}); + +test("确认文案在无需切换时明确不修改全局默认值", () => { + const llvm = toolchain("llvm", true); + const plan = ready(buildModuleSetupPlan( + inventory({ installed: [llvm], effective: llvm }), + "full", + true, + false, + )); + const confirmation = moduleSetupConfirmation( + "unavailable", + plan, + ); + + assert.match(confirmation.detail, /不会修改全局默认工具链/); + assert.doesNotMatch(confirmation.detail, /设为全局默认工具链/); + assert.doesNotMatch(confirmation.detail, /当前 LLVM 已生效/); +}); + +interface OperationFixture { + calls: ModuleSetupStage[]; + operations: ModuleSetupOperations; +} + +function result(stage: ModuleSetupStage, state: ModuleSetupStepState): ModuleSetupStepResult { + return { stage, state }; +} + +function operationsWith( + states: Partial> = {}, +): OperationFixture { + const calls: ModuleSetupStage[] = []; + const run = async (stage: ModuleSetupStage): Promise => { + calls.push(stage); + return result(stage, states[stage] ?? "succeeded"); + }; + + return { + calls, + operations: { + prepare: async (command: { stage: ModuleSetupStage }) => run(command.stage), + reload: async () => run("reload"), + ensureClangd: async () => run("clangd"), + checkModules: async () => run("check"), + }, + }; +} + +test("成功流程严格按安装、默认、构建、重载、clangd、检查执行", async () => { + const fixture = operationsWith(); + const outcome = await executeModuleSetup( + { kind: "ready", installLlvm: true, switchDefault: true }, + fixture.operations, + ); + + assert.deepEqual(fixture.calls, ["install", "default", "build", "reload", "clangd", "check"]); + assert.equal(outcome.state, "succeeded"); + assert.equal(outcome.degraded, false); + assert.deepEqual( + outcome.steps.map((step: ModuleSetupStepResult) => step.stage), + fixture.calls, + ); +}); + +test("批量准备操作只执行一次并继续恢复链", async () => { + const fixture = operationsWith(); + let preparePlanCalls = 0; + const outcome = await executeModuleSetup( + { kind: "ready", installLlvm: true, switchDefault: true }, + { + ...fixture.operations, + preparePlan: async () => { + preparePlanCalls += 1; + return result("build", "succeeded"); + }, + }, + ); + + assert.equal(preparePlanCalls, 1); + assert.deepEqual(fixture.calls, ["reload", "clangd", "check"]); + assert.equal(outcome.state, "succeeded"); +}); + +test("安装或默认步骤取消、失败后立即停止", async () => { + const cases: Array<{ + stage: "install" | "default"; + state: "cancelled" | "failed"; + plan: Extract; + expectedCalls: ModuleSetupStage[]; + }> = [ + { + stage: "install", + state: "cancelled", + plan: { kind: "ready", installLlvm: true, switchDefault: true }, + expectedCalls: ["install"], + }, + { + stage: "install", + state: "failed", + plan: { kind: "ready", installLlvm: true, switchDefault: true }, + expectedCalls: ["install"], + }, + { + stage: "default", + state: "cancelled", + plan: { kind: "ready", installLlvm: false, switchDefault: true }, + expectedCalls: ["default"], + }, + { + stage: "default", + state: "failed", + plan: { kind: "ready", installLlvm: false, switchDefault: true }, + expectedCalls: ["default"], + }, + ]; + + for (const item of cases) { + const fixture = operationsWith({ [item.stage]: item.state }); + const outcome = await executeModuleSetup(item.plan, fixture.operations); + assert.deepEqual(fixture.calls, item.expectedCalls); + assert.equal(outcome.state, item.state); + assert.equal(outcome.degraded, false); + assert.equal(outcome.stage, item.stage); + } +}); + +test("构建取消后停止,构建失败则尝试后续恢复", async () => { + const fixture = operationsWith({ build: "cancelled" }); + const outcome = await executeModuleSetup( + { kind: "ready", installLlvm: false, switchDefault: false }, + fixture.operations, + ); + + assert.deepEqual(fixture.calls, ["build"]); + assert.equal(outcome.state, "cancelled"); + assert.equal(outcome.degraded, false); +}); + +test("构建失败后的重载失败会终止恢复流程", async () => { + const fixture = operationsWith({ build: "failed", reload: "failed" }); + const outcome = await executeModuleSetup( + { kind: "ready", installLlvm: false, switchDefault: false }, + fixture.operations, + ); + + assert.deepEqual(fixture.calls, ["build", "reload"]); + assert.equal(outcome.state, "failed"); + assert.equal(outcome.degraded, false); + assert.equal(outcome.stage, "reload"); +}); + +test("构建失败但 IDE 恢复链成功时返回降级成功", async () => { + const fixture = operationsWith({ build: "failed" }); + const outcome = await executeModuleSetup( + { kind: "ready", installLlvm: false, switchDefault: false }, + fixture.operations, + ); + + assert.deepEqual(fixture.calls, ["build", "reload", "clangd", "check"]); + assert.equal(outcome.state, "succeeded"); + assert.equal(outcome.degraded, true); +}); + +test("clangd 失败后不检查模块", async () => { + const fixture = operationsWith({ clangd: "failed" }); + const outcome = await executeModuleSetup( + { kind: "ready", installLlvm: false, switchDefault: false }, + fixture.operations, + ); + + assert.deepEqual(fixture.calls, ["build", "reload", "clangd"]); + assert.equal(outcome.state, "failed"); + assert.equal(outcome.stage, "clangd"); +}); + +test("模块检查失败时返回检查阶段", async () => { + const fixture = operationsWith({ check: "failed" }); + const outcome = await executeModuleSetup( + { kind: "ready", installLlvm: false, switchDefault: false }, + fixture.operations, + ); + + assert.deepEqual(fixture.calls, ["build", "reload", "clangd", "check"]); + assert.equal(outcome.state, "failed"); + assert.equal(outcome.stage, "check"); +});