oh-my-opencode 4.9.1 → 4.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/opencode-qa/SKILL.md +1 -0
- package/.agents/skills/opencode-qa/scripts/lib/common.sh +39 -1
- package/.agents/skills/opencode-qa/scripts/lib/fake-openai-branches.mjs +39 -0
- package/.agents/skills/opencode-qa/scripts/lib/fake-openai-events.mjs +106 -0
- package/.agents/skills/opencode-qa/scripts/lib/fake-openai-server.mjs +117 -0
- package/.agents/skills/opencode-qa/scripts/serve-wake-split-probe.sh +716 -0
- package/.agents/skills/tech-debt-audit/SKILL.md +277 -0
- package/.agents/skills/work-with-pr-workspace/iteration-1/eval-5/with_skill/outputs/execution-plan.md +1 -1
- package/.opencode/skills/work-with-pr-workspace/iteration-1/eval-5/with_skill/outputs/execution-plan.md +1 -1
- package/bin/platform.js +5 -0
- package/bin/platform.test.ts +56 -0
- package/dist/agents/atlas/agent.d.ts +4 -3
- package/dist/agents/gpt-apply-patch-guard.d.ts +2 -2
- package/dist/agents/hephaestus/agent.d.ts +5 -0
- package/dist/agents/hephaestus/index.d.ts +1 -1
- package/dist/agents/metis.d.ts +1 -0
- package/dist/agents/prometheus/system-prompt.d.ts +1 -1
- package/dist/agents/sisyphus/kimi-k2-7.d.ts +17 -0
- package/dist/agents/sisyphus-junior/agent.d.ts +1 -1
- package/dist/agents/sisyphus-junior/kimi-k2-7.d.ts +11 -0
- package/dist/agents/types.d.ts +2 -2
- package/dist/cli/doctor/checks/codex-components.d.ts +13 -0
- package/dist/cli/doctor/checks/tui-plugin-config.d.ts +1 -0
- package/dist/cli/doctor/constants.d.ts +1 -1
- package/dist/cli/index.js +32329 -31437
- package/dist/cli/install-codex/codex-cleanup.d.ts +4 -0
- package/dist/cli/install-codex/install-codex-test-fixtures.d.ts +34 -0
- package/dist/cli/install-codex/link-cached-plugin-agents.d.ts +4 -0
- package/dist/cli/model-fallback.d.ts +1 -0
- package/dist/cli/provider-availability.d.ts +2 -0
- package/dist/cli-node/index.js +32329 -31437
- package/dist/config/schema/agent-overrides.d.ts +80 -16
- package/dist/config/schema/experimental.d.ts +1 -1
- package/dist/config/schema/hooks.d.ts +0 -1
- package/dist/config/schema/internal/permission.d.ts +5 -1
- package/dist/config/schema/oh-my-opencode-config.d.ts +76 -16
- package/dist/create-hooks.d.ts +0 -1
- package/dist/features/background-agent/index.d.ts +1 -1
- package/dist/features/background-agent/manager.d.ts +6 -0
- package/dist/features/background-agent/types.d.ts +2 -0
- package/dist/features/claude-code-plugin-loader/types.d.ts +3 -0
- package/dist/features/claude-code-session-state/state.d.ts +1 -0
- package/dist/features/skill-mcp-manager/manager.d.ts +11 -7
- package/dist/features/team-mode/team-mailbox/pending-delivery-recovery.d.ts +31 -0
- package/dist/features/team-mode/team-runtime/delete-team.d.ts +2 -1
- package/dist/features/team-mode/tools/lifecycle-inline-spec.d.ts +2 -2
- package/dist/features/tmux-subagent/stale-tmux-resource-sweeper.d.ts +12 -0
- package/dist/features/tool-metadata-store/store.d.ts +5 -0
- package/dist/hooks/anthropic-context-window-limit-recovery/storage/constants.d.ts +3 -0
- package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/messages-reader.d.ts +1 -1
- package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/part-content.d.ts +1 -1
- package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/parts-reader.d.ts +1 -1
- package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery/storage}/types.d.ts +0 -13
- package/dist/hooks/auto-update-checker/checker/bundled-version.d.ts +1 -0
- package/dist/hooks/auto-update-checker/checker.d.ts +1 -0
- package/dist/hooks/auto-update-checker/constants.d.ts +3 -3
- package/dist/hooks/auto-update-checker/hook.d.ts +2 -1
- package/dist/hooks/claude-code-hooks/types.d.ts +4 -0
- package/dist/hooks/index.d.ts +0 -1
- package/dist/hooks/team-session-events/team-idle-wake-hint.d.ts +5 -0
- package/dist/index.js +6061 -3714
- package/dist/oh-my-opencode.schema.json +123 -18
- package/dist/plugin/build-team-idle-wake-hint-client.d.ts +2 -0
- package/dist/plugin/event-session-lifecycle.d.ts +0 -3
- package/dist/plugin/hooks/create-continuation-hooks.d.ts +0 -6
- package/dist/plugin/hooks/create-core-hooks.d.ts +0 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts +1 -2
- package/dist/shared/command-executor/execute-hook-command.d.ts +7 -0
- package/dist/shared/internal-initiator-marker.d.ts +7 -0
- package/dist/shared/live-server-route.d.ts +24 -0
- package/dist/shared/plugin-identity.d.ts +2 -2
- package/dist/shared/prompt-async-gate/prompt-message-state.d.ts +1 -0
- package/dist/shared/tmux/tmux-utils/server-health.d.ts +2 -1
- package/dist/shared/tmux/tmux-utils/stale-attach-pane-sweep.d.ts +16 -0
- package/dist/shared/tmux/tmux-utils.d.ts +1 -0
- package/dist/testing/create-plugin-module.d.ts +4 -0
- package/dist/tools/background-task/clients.d.ts +2 -0
- package/dist/tools/background-task/full-session-format.d.ts +1 -0
- package/dist/tools/background-task/types.d.ts +1 -0
- package/dist/tools/delegate-task/sync-prompt-sender.d.ts +1 -1
- package/dist/tools/delegate-task/sync-session-lifecycle.d.ts +2 -1
- package/dist/tools/look-at/look-at-input-preparer.d.ts +6 -2
- package/dist/tools/look-at/look-at-prompt.d.ts +2 -1
- package/dist/tools/look-at/look-at-session-runner.d.ts +3 -4
- package/dist/tools/look-at/types.d.ts +2 -0
- package/dist/tools/session-manager/types.d.ts +1 -0
- package/dist/tools/skill-mcp/types.d.ts +1 -0
- package/package.json +14 -13
- package/packages/ast-grep-mcp/dist/cli.js +50 -17
- package/packages/lsp-daemon/dist/cli.js +8 -5
- package/packages/lsp-daemon/dist/index.js +8 -5
- package/packages/lsp-tools-mcp/dist/lsp/connection.js +1 -1
- package/packages/lsp-tools-mcp/dist/lsp/server-definitions.js +2 -2
- package/packages/lsp-tools-mcp/dist/lsp/transport.d.ts +10 -1
- package/packages/lsp-tools-mcp/dist/lsp/transport.js +6 -3
- package/packages/omo-codex/lazycodex-repository/.github/workflows/pr-source-guidance.yml +11 -12
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +2583 -0
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +17 -0
- package/packages/omo-codex/plugin/components/bootstrap/manifests/ast-grep.json +22 -0
- package/packages/omo-codex/plugin/components/bootstrap/manifests/node.json +10 -0
- package/packages/omo-codex/plugin/components/bootstrap/package.json +20 -0
- package/packages/omo-codex/plugin/components/bootstrap/scripts/bootstrap.ps1 +310 -0
- package/packages/omo-codex/plugin/components/bootstrap/scripts/build.mjs +35 -0
- package/packages/omo-codex/plugin/components/bootstrap/scripts/generate-manifests.mjs +115 -0
- package/packages/omo-codex/plugin/components/bootstrap/src/cli.ts +153 -0
- package/packages/omo-codex/plugin/components/bootstrap/src/download.ts +212 -0
- package/packages/omo-codex/plugin/components/bootstrap/src/environment.ts +286 -0
- package/packages/omo-codex/plugin/components/bootstrap/src/hook.ts +108 -0
- package/packages/omo-codex/plugin/components/bootstrap/src/provision.ts +243 -0
- package/packages/omo-codex/plugin/components/bootstrap/src/setup.ts +294 -0
- package/packages/omo-codex/plugin/components/bootstrap/src/worker.ts +279 -0
- package/packages/omo-codex/plugin/components/bootstrap/test/download.test.ts +295 -0
- package/packages/omo-codex/plugin/components/bootstrap/test/environment.test.ts +375 -0
- package/packages/omo-codex/plugin/components/bootstrap/test/provision.test.ts +464 -0
- package/packages/omo-codex/plugin/components/bootstrap/tsconfig.json +25 -0
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +4 -4
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +6 -10
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +4 -4
- package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-tools.test.mjs +8 -3
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +5 -8
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-cli.test.ts +24 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md +3 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +4 -4
- package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +35 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +4 -4
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +4 -4
- package/packages/omo-codex/plugin/components/ultrawork/biome.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +155 -99
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +4 -4
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +19 -51
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +46 -51
- package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +19 -0
- package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +0 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +9 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +18 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +1 -3
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +6 -2
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +19 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-commands.test.ts +6 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-complete-goals.test.ts +26 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-json-errors.test.ts +89 -0
- package/packages/omo-codex/plugin/hooks/hooks.json +27 -16
- package/packages/omo-codex/plugin/package-lock.json +193 -193
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/auto-update-state.d.mts +20 -0
- package/packages/omo-codex/plugin/scripts/auto-update.mjs +28 -8
- package/packages/omo-codex/plugin/scripts/build-components.mjs +36 -5
- package/packages/omo-codex/plugin/scripts/install-flow.mjs +43 -0
- package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/SKILL.md +79 -28
- package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/agents/openai.yaml +2 -2
- package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +7 -6
- package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +19 -51
- package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +46 -51
- package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +1 -0
- package/packages/omo-codex/plugin/test/auto-update.test.mjs +145 -0
- package/packages/omo-codex/plugin/test/bootstrap-binlinks.test.mjs +250 -0
- package/packages/omo-codex/plugin/test/bootstrap-hooks.test.mjs +166 -0
- package/packages/omo-codex/plugin/test/bootstrap-orchestration.test.mjs +371 -0
- package/packages/omo-codex/plugin/test/bootstrap-ps-guard.test.mjs +134 -0
- package/packages/omo-codex/plugin/test/bootstrap-setup.test.mjs +249 -0
- package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +10 -1
- package/packages/omo-codex/plugin/test/ulw-plan-skill.test.mjs +46 -0
- package/packages/omo-codex/scripts/atomic-write.test.mjs +82 -0
- package/packages/omo-codex/scripts/install/agents.d.mts +18 -0
- package/packages/omo-codex/scripts/install/agents.mjs +78 -5
- package/packages/omo-codex/scripts/install/atomic-write.mjs +59 -0
- package/packages/omo-codex/scripts/install/bin-dir.d.mts +7 -0
- package/packages/omo-codex/scripts/install/bin-links.d.mts +18 -0
- package/packages/omo-codex/scripts/install/config.d.mts +35 -0
- package/packages/omo-codex/scripts/install/config.mjs +13 -3
- package/packages/omo-codex/scripts/install/git-bash-mcp-env.d.mts +5 -0
- package/packages/omo-codex/scripts/install/git-bash.d.mts +23 -0
- package/packages/omo-codex/scripts/install/hook-trust.d.mts +10 -0
- package/packages/omo-codex/scripts/install-agent-links.test.mjs +41 -0
- package/packages/omo-codex/scripts/install-local.mjs +3 -2
- package/packages/shared-skills/skills/lcx-contribute-bug-fix/SKILL.md +79 -28
- package/packages/shared-skills/skills/lcx-contribute-bug-fix/agents/openai.yaml +2 -2
- package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +7 -6
- package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +1 -1
- package/dist/hooks/session-recovery/constants.d.ts +0 -4
- package/dist/hooks/session-recovery/detect-error-type.d.ts +0 -4
- package/dist/hooks/session-recovery/error-recovery.d.ts +0 -4
- package/dist/hooks/session-recovery/hook-types.d.ts +0 -22
- package/dist/hooks/session-recovery/hook.d.ts +0 -4
- package/dist/hooks/session-recovery/index.d.ts +0 -5
- package/dist/hooks/session-recovery/interrupted-idle-message-fetch-timeout.d.ts +0 -7
- package/dist/hooks/session-recovery/interrupted-tool-results.d.ts +0 -3
- package/dist/hooks/session-recovery/message-state.d.ts +0 -4
- package/dist/hooks/session-recovery/recover-thinking-block-order.d.ts +0 -5
- package/dist/hooks/session-recovery/recover-thinking-disabled-violation.d.ts +0 -5
- package/dist/hooks/session-recovery/recover-tool-result-missing.d.ts +0 -10
- package/dist/hooks/session-recovery/recover-unavailable-tool.d.ts +0 -5
- package/dist/hooks/session-recovery/resume.d.ts +0 -7
- package/dist/hooks/session-recovery/storage/latest-assistant-message.d.ts +0 -5
- package/dist/hooks/session-recovery/storage/orphan-thinking-search.d.ts +0 -2
- package/dist/hooks/session-recovery/storage/thinking-block-search.d.ts +0 -2
- package/dist/hooks/session-recovery/storage/thinking-prepend.d.ts +0 -33
- package/dist/hooks/session-recovery/storage/thinking-strip.d.ts +0 -11
- package/dist/hooks/session-recovery/storage.d.ts +0 -20
- package/dist/plugin/event-session-recovery.d.ts +0 -9
- package/dist/plugin/user-abort-interrupted-recovery-guard.d.ts +0 -6
- /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/empty-messages.d.ts +0 -0
- /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/empty-text.d.ts +0 -0
- /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/message-dir.d.ts +0 -0
- /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/part-id.d.ts +0 -0
- /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/text-part-injector.d.ts +0 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { readState, writeState } from "../../../scripts/auto-update-state.mjs";
|
|
7
|
+
import { bootstrapLocks, resolveBootstrapStatePath, resolveCodexHome } from "./environment.ts";
|
|
8
|
+
import { runSgProvision } from "./provision.ts";
|
|
9
|
+
import type { SgProvisionSeams } from "./provision.ts";
|
|
10
|
+
import { runWorkerSetup } from "./setup.ts";
|
|
11
|
+
|
|
12
|
+
export const BOOTSTRAP_DOCTOR_HINT = "npx lazycodex-ai doctor";
|
|
13
|
+
|
|
14
|
+
export type BootstrapRunStatus = "success" | "degraded";
|
|
15
|
+
|
|
16
|
+
export interface BootstrapDegradedEntry {
|
|
17
|
+
readonly component: string;
|
|
18
|
+
readonly reason: string;
|
|
19
|
+
readonly hint?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface BootstrapState {
|
|
23
|
+
readonly completedForVersion?: string;
|
|
24
|
+
readonly lastAttemptAt?: number;
|
|
25
|
+
readonly lastStatus?: BootstrapRunStatus;
|
|
26
|
+
readonly degraded?: readonly BootstrapDegradedEntry[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BootstrapWorkerFlags {
|
|
30
|
+
readonly codexHome?: string;
|
|
31
|
+
readonly manifestDir?: string;
|
|
32
|
+
readonly once: boolean;
|
|
33
|
+
readonly only?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface BootstrapWorkerContext {
|
|
37
|
+
readonly codexHome: string;
|
|
38
|
+
readonly env: Record<string, string | undefined>;
|
|
39
|
+
readonly flags: BootstrapWorkerFlags;
|
|
40
|
+
readonly now: number;
|
|
41
|
+
readonly platform: NodeJS.Platform;
|
|
42
|
+
readonly pluginData: string;
|
|
43
|
+
readonly pluginRoot: string;
|
|
44
|
+
readonly pluginVersion: string | undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface BootstrapStepOutcome {
|
|
48
|
+
readonly degraded: readonly BootstrapDegradedEntry[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface BootstrapWorkerStep {
|
|
52
|
+
readonly name: string;
|
|
53
|
+
readonly run: (context: BootstrapWorkerContext) => Promise<BootstrapStepOutcome>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type BootstrapWorkerSkipReason = "locked" | "already-completed";
|
|
57
|
+
|
|
58
|
+
export type BootstrapWorkerResult =
|
|
59
|
+
| { readonly ran: false; readonly reason: BootstrapWorkerSkipReason }
|
|
60
|
+
| {
|
|
61
|
+
readonly ran: true;
|
|
62
|
+
readonly status: BootstrapRunStatus;
|
|
63
|
+
readonly degraded: readonly BootstrapDegradedEntry[];
|
|
64
|
+
readonly statePath: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export interface RunBootstrapWorkerOptions {
|
|
68
|
+
readonly argv?: readonly string[];
|
|
69
|
+
readonly env?: Record<string, string | undefined>;
|
|
70
|
+
readonly now?: number;
|
|
71
|
+
readonly platform?: NodeJS.Platform;
|
|
72
|
+
readonly steps?: readonly BootstrapWorkerStep[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function parseWorkerFlags(argv: readonly string[]): BootstrapWorkerFlags {
|
|
76
|
+
let codexHome: string | undefined;
|
|
77
|
+
let manifestDir: string | undefined;
|
|
78
|
+
let once = false;
|
|
79
|
+
let only: string | undefined;
|
|
80
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
81
|
+
const flag = argv[index];
|
|
82
|
+
if (flag === "--once") {
|
|
83
|
+
once = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (flag === "--codex-home") {
|
|
87
|
+
codexHome = requireFlagValue(argv, index, flag);
|
|
88
|
+
index += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (flag === "--only") {
|
|
92
|
+
only = requireFlagValue(argv, index, flag);
|
|
93
|
+
index += 1;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (flag === "--manifest-dir") {
|
|
97
|
+
manifestDir = requireFlagValue(argv, index, flag);
|
|
98
|
+
index += 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
throw new Error(`unknown worker flag: ${flag}`);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
once,
|
|
105
|
+
...(codexHome === undefined ? {} : { codexHome }),
|
|
106
|
+
...(manifestDir === undefined ? {} : { manifestDir }),
|
|
107
|
+
...(only === undefined ? {} : { only }),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function resolvePluginDataRoot(env: Record<string, string | undefined>): string {
|
|
112
|
+
const fromEnv = env["PLUGIN_DATA"]?.trim();
|
|
113
|
+
if (fromEnv !== undefined && fromEnv.length > 0) return fromEnv;
|
|
114
|
+
return join(homedir(), ".local", "share", "lazycodex");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function readPluginVersion(pluginRoot: string): Promise<string | undefined> {
|
|
118
|
+
try {
|
|
119
|
+
const parsed: unknown = JSON.parse(await readFile(join(pluginRoot, ".codex-plugin", "plugin.json"), "utf8"));
|
|
120
|
+
if (typeof parsed !== "object" || parsed === null) return undefined;
|
|
121
|
+
const version = (parsed as Record<string, unknown>)["version"];
|
|
122
|
+
if (typeof version !== "string") return undefined;
|
|
123
|
+
const trimmed = version.trim();
|
|
124
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
125
|
+
} catch {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function readBootstrapState(statePath: string): Promise<BootstrapState> {
|
|
131
|
+
return parseBootstrapState(await readState(statePath));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function parseBootstrapState(raw: Record<string, unknown>): BootstrapState {
|
|
135
|
+
const completedForVersion = typeof raw["completedForVersion"] === "string" ? raw["completedForVersion"] : undefined;
|
|
136
|
+
const lastAttemptAt = typeof raw["lastAttemptAt"] === "number" ? raw["lastAttemptAt"] : undefined;
|
|
137
|
+
const lastStatus = raw["lastStatus"] === "success" || raw["lastStatus"] === "degraded" ? raw["lastStatus"] : undefined;
|
|
138
|
+
const degraded = parseDegradedEntries(raw["degraded"]);
|
|
139
|
+
return {
|
|
140
|
+
...(completedForVersion === undefined ? {} : { completedForVersion }),
|
|
141
|
+
...(lastAttemptAt === undefined ? {} : { lastAttemptAt }),
|
|
142
|
+
...(lastStatus === undefined ? {} : { lastStatus }),
|
|
143
|
+
...(degraded === undefined ? {} : { degraded }),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface DefaultWorkerStepsSeams {
|
|
148
|
+
readonly sg?: SgProvisionSeams;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function defaultWorkerSteps(seams: DefaultWorkerStepsSeams = {}): readonly BootstrapWorkerStep[] {
|
|
152
|
+
return [
|
|
153
|
+
{
|
|
154
|
+
name: "setup",
|
|
155
|
+
run: (context) => runWorkerSetup(context),
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "sg",
|
|
159
|
+
run: (context) => runSgProvision(context, seams.sg),
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function runBootstrapWorker(options: RunBootstrapWorkerOptions = {}): Promise<BootstrapWorkerResult> {
|
|
165
|
+
const env = options.env ?? process.env;
|
|
166
|
+
const now = options.now ?? Date.now();
|
|
167
|
+
const platform = options.platform ?? process.platform;
|
|
168
|
+
const flags = parseWorkerFlags(options.argv ?? []);
|
|
169
|
+
const steps = options.steps ?? defaultWorkerSteps();
|
|
170
|
+
const pluginRoot = resolvePluginRoot(env);
|
|
171
|
+
const pluginData = resolvePluginDataRoot(env);
|
|
172
|
+
const statePath = resolveBootstrapStatePath(pluginData);
|
|
173
|
+
// Pin BOTH lock paths (bootstrap + auto-update) under the resolved plugin
|
|
174
|
+
// data root even when PLUGIN_DATA is missing from the environment.
|
|
175
|
+
const lockEnv = { ...env, PLUGIN_DATA: pluginData };
|
|
176
|
+
|
|
177
|
+
const locks = await bootstrapLocks({ env: lockEnv, now, pluginData });
|
|
178
|
+
if (locks === null) return { ran: false, reason: "locked" };
|
|
179
|
+
try {
|
|
180
|
+
const pluginVersion = await readPluginVersion(pluginRoot);
|
|
181
|
+
const marker = await readBootstrapState(statePath);
|
|
182
|
+
// TOCTOU re-check under lock: another worker may have completed between
|
|
183
|
+
// the hook's unlocked read and this acquisition.
|
|
184
|
+
if (!flags.once && pluginVersion !== undefined && marker.completedForVersion === pluginVersion) {
|
|
185
|
+
await appendBootstrapLog(pluginData, now, "worker-skipped", { reason: "already-completed", version: pluginVersion });
|
|
186
|
+
return { ran: false, reason: "already-completed" };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const codexHome = flags.codexHome ?? (await resolveCodexHome({ env, pluginRoot })).path;
|
|
190
|
+
const context: BootstrapWorkerContext = { codexHome, env, flags, now, platform, pluginData, pluginRoot, pluginVersion };
|
|
191
|
+
await appendBootstrapLog(pluginData, now, "worker-started", { version: pluginVersion ?? "unknown" });
|
|
192
|
+
|
|
193
|
+
const degraded: BootstrapDegradedEntry[] = [];
|
|
194
|
+
if (pluginVersion === undefined) {
|
|
195
|
+
degraded.push({
|
|
196
|
+
component: "bootstrap",
|
|
197
|
+
hint: BOOTSTRAP_DOCTOR_HINT,
|
|
198
|
+
reason: `plugin version unresolved from ${join(pluginRoot, ".codex-plugin", "plugin.json")}`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
for (const step of steps) {
|
|
202
|
+
if (flags.only !== undefined && step.name !== flags.only) continue;
|
|
203
|
+
degraded.push(...(await runStep(step, context)));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const status: BootstrapRunStatus = degraded.length === 0 ? "success" : "degraded";
|
|
207
|
+
const state: BootstrapState = {
|
|
208
|
+
...(pluginVersion === undefined ? {} : { completedForVersion: pluginVersion }),
|
|
209
|
+
degraded,
|
|
210
|
+
lastAttemptAt: now,
|
|
211
|
+
lastStatus: status,
|
|
212
|
+
};
|
|
213
|
+
await writeState(statePath, state);
|
|
214
|
+
await appendBootstrapLog(pluginData, now, "worker-finished", { degradedCount: degraded.length, status });
|
|
215
|
+
return { degraded, ran: true, statePath, status };
|
|
216
|
+
} finally {
|
|
217
|
+
await locks.release();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function runStep(step: BootstrapWorkerStep, context: BootstrapWorkerContext): Promise<readonly BootstrapDegradedEntry[]> {
|
|
222
|
+
try {
|
|
223
|
+
return (await step.run(context)).degraded;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return [
|
|
226
|
+
{
|
|
227
|
+
component: step.name,
|
|
228
|
+
hint: BOOTSTRAP_DOCTOR_HINT,
|
|
229
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
230
|
+
},
|
|
231
|
+
];
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function resolvePluginRoot(env: Record<string, string | undefined>): string {
|
|
236
|
+
const fromEnv = env["PLUGIN_ROOT"]?.trim();
|
|
237
|
+
if (fromEnv !== undefined && fromEnv.length > 0) return fromEnv;
|
|
238
|
+
// dist/cli.js lives at <pluginRoot>/components/bootstrap/dist/cli.js.
|
|
239
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function appendBootstrapLog(
|
|
243
|
+
pluginData: string,
|
|
244
|
+
now: number,
|
|
245
|
+
event: string,
|
|
246
|
+
details: Record<string, unknown>,
|
|
247
|
+
): Promise<void> {
|
|
248
|
+
try {
|
|
249
|
+
const logPath = join(pluginData, "bootstrap", "bootstrap.log");
|
|
250
|
+
await mkdir(dirname(logPath), { recursive: true });
|
|
251
|
+
await appendFile(logPath, `${JSON.stringify({ timestamp: new Date(now).toISOString(), event, ...details })}\n`);
|
|
252
|
+
} catch {
|
|
253
|
+
// Logging must never fail the worker.
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function parseDegradedEntries(raw: unknown): readonly BootstrapDegradedEntry[] | undefined {
|
|
258
|
+
if (!Array.isArray(raw)) return undefined;
|
|
259
|
+
const entries: BootstrapDegradedEntry[] = [];
|
|
260
|
+
for (const candidate of raw) {
|
|
261
|
+
if (typeof candidate !== "object" || candidate === null) continue;
|
|
262
|
+
const record = candidate as Record<string, unknown>;
|
|
263
|
+
if (typeof record["component"] !== "string" || typeof record["reason"] !== "string") continue;
|
|
264
|
+
entries.push({
|
|
265
|
+
component: record["component"],
|
|
266
|
+
reason: record["reason"],
|
|
267
|
+
...(typeof record["hint"] === "string" ? { hint: record["hint"] } : {}),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return entries;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function requireFlagValue(argv: readonly string[], index: number, flag: string): string {
|
|
274
|
+
const value = argv[index + 1];
|
|
275
|
+
if (value === undefined || value.startsWith("--")) {
|
|
276
|
+
throw new Error(`${flag} requires a value`);
|
|
277
|
+
}
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { basename, join } from "node:path";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
ChecksumMismatchError,
|
|
10
|
+
DownloadError,
|
|
11
|
+
downloadChecksummedAsset,
|
|
12
|
+
downloadFromManifest,
|
|
13
|
+
loadAssetManifest,
|
|
14
|
+
UnsupportedPlatformError,
|
|
15
|
+
type FetchLike,
|
|
16
|
+
} from "../src/download.ts";
|
|
17
|
+
|
|
18
|
+
const temporaryDirectories: string[] = [];
|
|
19
|
+
|
|
20
|
+
function createTemporaryDirectory(prefix: string): string {
|
|
21
|
+
const directory = mkdtempSync(join(tmpdir(), prefix));
|
|
22
|
+
temporaryDirectories.push(directory);
|
|
23
|
+
return directory;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
for (const directory of temporaryDirectories.splice(0)) {
|
|
28
|
+
rmSync(directory, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function sha256Hex(bytes: Uint8Array): string {
|
|
33
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fetchReturning(bytes: Uint8Array, status = 200): { fetchImpl: FetchLike; calls: string[] } {
|
|
37
|
+
const calls: string[] = [];
|
|
38
|
+
const fetchImpl: FetchLike = async (url) => {
|
|
39
|
+
calls.push(url);
|
|
40
|
+
return new Response(status === 200 ? bytes : null, { status });
|
|
41
|
+
};
|
|
42
|
+
return { calls, fetchImpl };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function writeManifestFixture(
|
|
46
|
+
manifestsDir: string,
|
|
47
|
+
platforms: Record<string, { url: string; sha256: string }>,
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
await mkdir(manifestsDir, { recursive: true });
|
|
50
|
+
const manifest = { name: "tool", platforms, version: "1.0.0" };
|
|
51
|
+
await writeFile(join(manifestsDir, "tool.json"), JSON.stringify(manifest, null, "\t"), "utf8");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("downloadChecksummedAsset", () => {
|
|
55
|
+
it("#given a fetch that returns the pinned bytes #when downloading #then the destination holds exactly those bytes and no partial files remain", async () => {
|
|
56
|
+
// given
|
|
57
|
+
const directory = createTemporaryDirectory("omo-bootstrap-download-");
|
|
58
|
+
const payload = new TextEncoder().encode("pinned asset payload");
|
|
59
|
+
const destination = join(directory, "asset.zip");
|
|
60
|
+
const { calls, fetchImpl } = fetchReturning(payload);
|
|
61
|
+
|
|
62
|
+
// when
|
|
63
|
+
const written = await downloadChecksummedAsset({
|
|
64
|
+
destination,
|
|
65
|
+
fetchImpl,
|
|
66
|
+
sha256: sha256Hex(payload),
|
|
67
|
+
url: "https://example.invalid/assets/asset.zip",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// then
|
|
71
|
+
expect(written).toBe(destination);
|
|
72
|
+
expect(calls).toEqual(["https://example.invalid/assets/asset.zip"]);
|
|
73
|
+
expect(new Uint8Array(await readFile(destination))).toEqual(payload);
|
|
74
|
+
expect(await readdir(directory)).toEqual(["asset.zip"]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("#given a fetch that returns corrupted bytes #when downloading #then a checksum mismatch error names both hashes and the partial file is deleted", async () => {
|
|
78
|
+
// given
|
|
79
|
+
const directory = createTemporaryDirectory("omo-bootstrap-download-");
|
|
80
|
+
const pinned = new TextEncoder().encode("pinned asset payload");
|
|
81
|
+
const corrupted = new TextEncoder().encode("corrupted asset payload");
|
|
82
|
+
const destination = join(directory, "asset.zip");
|
|
83
|
+
const expectedSha256 = sha256Hex(pinned);
|
|
84
|
+
const actualSha256 = sha256Hex(corrupted);
|
|
85
|
+
|
|
86
|
+
// when
|
|
87
|
+
const failure = downloadChecksummedAsset({
|
|
88
|
+
destination,
|
|
89
|
+
fetchImpl: fetchReturning(corrupted).fetchImpl,
|
|
90
|
+
sha256: expectedSha256,
|
|
91
|
+
url: "https://example.invalid/assets/asset.zip",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// then
|
|
95
|
+
const error = (await failure.then(
|
|
96
|
+
() => {
|
|
97
|
+
throw new Error("expected downloadChecksummedAsset to reject");
|
|
98
|
+
},
|
|
99
|
+
(caught: unknown) => caught,
|
|
100
|
+
)) as ChecksumMismatchError;
|
|
101
|
+
expect(error).toBeInstanceOf(ChecksumMismatchError);
|
|
102
|
+
expect(error.code).toBe("checksum-mismatch");
|
|
103
|
+
expect(error.message).toMatch(/checksum mismatch/i);
|
|
104
|
+
expect(error.message).toContain(expectedSha256);
|
|
105
|
+
expect(error.message).toContain(actualSha256);
|
|
106
|
+
expect(error.expectedSha256).toBe(expectedSha256);
|
|
107
|
+
expect(error.actualSha256).toBe(actualSha256);
|
|
108
|
+
expect(await readdir(directory)).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("#given a non-2xx response #when downloading #then a typed download-failed error is thrown and nothing is written", async () => {
|
|
112
|
+
// given
|
|
113
|
+
const directory = createTemporaryDirectory("omo-bootstrap-download-");
|
|
114
|
+
const destination = join(directory, "asset.zip");
|
|
115
|
+
|
|
116
|
+
// when
|
|
117
|
+
const failure = downloadChecksummedAsset({
|
|
118
|
+
destination,
|
|
119
|
+
fetchImpl: fetchReturning(new Uint8Array(), 404).fetchImpl,
|
|
120
|
+
sha256: sha256Hex(new Uint8Array()),
|
|
121
|
+
url: "https://example.invalid/assets/missing.zip",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// then
|
|
125
|
+
const error = (await failure.then(
|
|
126
|
+
() => {
|
|
127
|
+
throw new Error("expected downloadChecksummedAsset to reject");
|
|
128
|
+
},
|
|
129
|
+
(caught: unknown) => caught,
|
|
130
|
+
)) as DownloadError;
|
|
131
|
+
expect(error).toBeInstanceOf(DownloadError);
|
|
132
|
+
expect(error.code).toBe("download-failed");
|
|
133
|
+
expect(error.message).toContain("404");
|
|
134
|
+
expect(await readdir(directory)).toEqual([]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("#given HTTPS_PROXY is set #when the direct download attempt fails #then the error names the v1 proxy limitation", async () => {
|
|
138
|
+
// given
|
|
139
|
+
const directory = createTemporaryDirectory("omo-bootstrap-download-");
|
|
140
|
+
const calls: string[] = [];
|
|
141
|
+
const fetchImpl: FetchLike = async (url) => {
|
|
142
|
+
calls.push(url);
|
|
143
|
+
throw new Error("connect ETIMEDOUT 203.0.113.7:443");
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// when
|
|
147
|
+
const failure = downloadChecksummedAsset({
|
|
148
|
+
destination: join(directory, "asset.zip"),
|
|
149
|
+
env: { HTTPS_PROXY: "http://proxy.corp.example:3128" },
|
|
150
|
+
fetchImpl,
|
|
151
|
+
sha256: sha256Hex(new Uint8Array()),
|
|
152
|
+
url: "https://example.invalid/assets/asset.zip",
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// then
|
|
156
|
+
const error = (await failure.then(
|
|
157
|
+
() => {
|
|
158
|
+
throw new Error("expected downloadChecksummedAsset to reject");
|
|
159
|
+
},
|
|
160
|
+
(caught: unknown) => caught,
|
|
161
|
+
)) as DownloadError;
|
|
162
|
+
expect(calls).toHaveLength(1);
|
|
163
|
+
expect(error).toBeInstanceOf(DownloadError);
|
|
164
|
+
expect(error.code).toBe("download-failed");
|
|
165
|
+
expect(error.message).toContain("ETIMEDOUT");
|
|
166
|
+
expect(error.message).toContain("HTTPS_PROXY");
|
|
167
|
+
expect(error.message).toMatch(/does not tunnel through/i);
|
|
168
|
+
expect(await readdir(directory)).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("#given no proxy env #when the direct download attempt fails #then the error does not mention proxies", async () => {
|
|
172
|
+
// given
|
|
173
|
+
const directory = createTemporaryDirectory("omo-bootstrap-download-");
|
|
174
|
+
const fetchImpl: FetchLike = async () => {
|
|
175
|
+
throw new Error("getaddrinfo ENOTFOUND example.invalid");
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// when
|
|
179
|
+
const failure = downloadChecksummedAsset({
|
|
180
|
+
destination: join(directory, "asset.zip"),
|
|
181
|
+
env: {},
|
|
182
|
+
fetchImpl,
|
|
183
|
+
sha256: sha256Hex(new Uint8Array()),
|
|
184
|
+
url: "https://example.invalid/assets/asset.zip",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// then
|
|
188
|
+
const error = (await failure.then(
|
|
189
|
+
() => {
|
|
190
|
+
throw new Error("expected downloadChecksummedAsset to reject");
|
|
191
|
+
},
|
|
192
|
+
(caught: unknown) => caught,
|
|
193
|
+
)) as DownloadError;
|
|
194
|
+
expect(error.code).toBe("download-failed");
|
|
195
|
+
expect(error.message).not.toContain("HTTPS_PROXY");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("downloadFromManifest", () => {
|
|
200
|
+
it("#given a manifest with a platform entry #when downloading for that platform #then the asset lands under its URL basename in the destination dir", async () => {
|
|
201
|
+
// given
|
|
202
|
+
const directory = createTemporaryDirectory("omo-bootstrap-manifest-");
|
|
203
|
+
const manifestsDir = join(directory, "manifests");
|
|
204
|
+
const destinationDir = join(directory, "downloads");
|
|
205
|
+
const payload = new TextEncoder().encode("tool binary zip bytes");
|
|
206
|
+
await writeManifestFixture(manifestsDir, {
|
|
207
|
+
"darwin-arm64": {
|
|
208
|
+
sha256: sha256Hex(payload),
|
|
209
|
+
url: "https://example.invalid/releases/tool-1.0.0-darwin-arm64.zip",
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
const { calls, fetchImpl } = fetchReturning(payload);
|
|
213
|
+
|
|
214
|
+
// when
|
|
215
|
+
const destination = await downloadFromManifest({
|
|
216
|
+
destinationDir,
|
|
217
|
+
fetchImpl,
|
|
218
|
+
manifestName: "tool",
|
|
219
|
+
manifestsDir,
|
|
220
|
+
platformKey: "darwin-arm64",
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// then
|
|
224
|
+
expect(destination).toBe(join(destinationDir, "tool-1.0.0-darwin-arm64.zip"));
|
|
225
|
+
expect(calls).toEqual(["https://example.invalid/releases/tool-1.0.0-darwin-arm64.zip"]);
|
|
226
|
+
expect(new Uint8Array(await readFile(destination))).toEqual(payload);
|
|
227
|
+
expect(basename(destination)).toBe("tool-1.0.0-darwin-arm64.zip");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("#given a manifest without the requested platform #when downloading #then a typed unsupported-platform error is thrown before any fetch", async () => {
|
|
231
|
+
// given
|
|
232
|
+
const directory = createTemporaryDirectory("omo-bootstrap-manifest-");
|
|
233
|
+
const manifestsDir = join(directory, "manifests");
|
|
234
|
+
await writeManifestFixture(manifestsDir, {
|
|
235
|
+
"darwin-arm64": { sha256: sha256Hex(new Uint8Array()), url: "https://example.invalid/tool.zip" },
|
|
236
|
+
});
|
|
237
|
+
const { calls, fetchImpl } = fetchReturning(new Uint8Array());
|
|
238
|
+
|
|
239
|
+
// when
|
|
240
|
+
const failure = downloadFromManifest({
|
|
241
|
+
destinationDir: join(directory, "downloads"),
|
|
242
|
+
fetchImpl,
|
|
243
|
+
manifestName: "tool",
|
|
244
|
+
manifestsDir,
|
|
245
|
+
platformKey: "linux-x64",
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// then
|
|
249
|
+
const error = (await failure.then(
|
|
250
|
+
() => {
|
|
251
|
+
throw new Error("expected downloadFromManifest to reject");
|
|
252
|
+
},
|
|
253
|
+
(caught: unknown) => caught,
|
|
254
|
+
)) as UnsupportedPlatformError;
|
|
255
|
+
expect(error).toBeInstanceOf(UnsupportedPlatformError);
|
|
256
|
+
expect(error.code).toBe("unsupported-platform");
|
|
257
|
+
expect(error.platformKey).toBe("linux-x64");
|
|
258
|
+
expect(error.message).toContain('"linux-x64"');
|
|
259
|
+
expect(error.message).toContain("darwin-arm64");
|
|
260
|
+
expect(calls).toEqual([]);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe("committed manifests", () => {
|
|
265
|
+
it("#given the committed ast-grep manifest #when loaded #then every required platform pins an https URL and a sha256", async () => {
|
|
266
|
+
// given / when
|
|
267
|
+
const manifest = await loadAssetManifest("ast-grep");
|
|
268
|
+
|
|
269
|
+
// then
|
|
270
|
+
expect(manifest.name).toBe("ast-grep");
|
|
271
|
+
expect(manifest.version).toMatch(/^\d+\.\d+\.\d+$/);
|
|
272
|
+
for (const platformKey of ["darwin-arm64", "darwin-x64", "linux-x64", "win32-x64"]) {
|
|
273
|
+
const asset = manifest.platforms[platformKey];
|
|
274
|
+
if (asset === undefined) throw new Error(`missing platform ${platformKey}`);
|
|
275
|
+
expect(asset.url).toStartWith("https://");
|
|
276
|
+
expect(asset.url).toContain(manifest.version);
|
|
277
|
+
expect(asset.sha256).toMatch(/^[a-f0-9]{64}$/);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("#given the committed node manifest #when loaded #then the win32-x64 LTS zip pins an https URL and a sha256", async () => {
|
|
282
|
+
// given / when
|
|
283
|
+
const manifest = await loadAssetManifest("node");
|
|
284
|
+
|
|
285
|
+
// then
|
|
286
|
+
expect(manifest.name).toBe("node");
|
|
287
|
+
expect(manifest.version).toMatch(/^\d+\.\d+\.\d+$/);
|
|
288
|
+
const asset = manifest.platforms["win32-x64"];
|
|
289
|
+
if (asset === undefined) throw new Error("missing platform win32-x64");
|
|
290
|
+
expect(asset.url).toStartWith("https://nodejs.org/dist/");
|
|
291
|
+
expect(asset.url).toEndWith("-win-x64.zip");
|
|
292
|
+
expect(asset.url).toContain(manifest.version);
|
|
293
|
+
expect(asset.sha256).toMatch(/^[a-f0-9]{64}$/);
|
|
294
|
+
});
|
|
295
|
+
});
|