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,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { downloadFromManifest } from "./download.ts";
|
|
6
|
+
import { runSessionStartHook } from "./hook.ts";
|
|
7
|
+
import { runBootstrapWorker } from "./worker.ts";
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
bootstrapLocks,
|
|
11
|
+
detectInstallFlow,
|
|
12
|
+
detectInstallFlowDetailed,
|
|
13
|
+
detectInstallFlowForTest,
|
|
14
|
+
detectInstallFlowFromEnvironment,
|
|
15
|
+
INSTALL_SNAPSHOT_FILENAME,
|
|
16
|
+
resolveBootstrapLockPath,
|
|
17
|
+
resolveBootstrapStatePath,
|
|
18
|
+
resolveCodexHome,
|
|
19
|
+
} from "./environment.ts";
|
|
20
|
+
export type {
|
|
21
|
+
BootstrapLockHandle,
|
|
22
|
+
BootstrapLocksOptions,
|
|
23
|
+
CodexHomeResolution,
|
|
24
|
+
CodexHomeSource,
|
|
25
|
+
ConfigSourceSignal,
|
|
26
|
+
DetectInstallFlowFromEnvironmentOptions,
|
|
27
|
+
DetectInstallFlowOptions,
|
|
28
|
+
InstallFlow,
|
|
29
|
+
InstallFlowDetection,
|
|
30
|
+
ResolveCodexHomeOptions,
|
|
31
|
+
} from "./environment.ts";
|
|
32
|
+
export { BOOTSTRAP_RESTART_NOTICE, executeSessionStartHook, runSessionStartHook } from "./hook.ts";
|
|
33
|
+
export {
|
|
34
|
+
runSgProvision,
|
|
35
|
+
SG_FORCE_PROVISION_ENV_KEY,
|
|
36
|
+
SG_PROVISION_COMPONENT,
|
|
37
|
+
sgProvisionDestination,
|
|
38
|
+
} from "./provision.ts";
|
|
39
|
+
export type { ResolvePreexistingSgOptions, SgProvisionSeams } from "./provision.ts";
|
|
40
|
+
export { GIT_BASH_INSTALL_HINT, runWorkerSetup, SETUP_MARKETPLACE_NAME, SETUP_PLUGIN_NAME } from "./setup.ts";
|
|
41
|
+
export type { SetupRunCommand, WorkerSetupOptions } from "./setup.ts";
|
|
42
|
+
export type {
|
|
43
|
+
SessionStartAction,
|
|
44
|
+
SessionStartHookOptions,
|
|
45
|
+
SessionStartHookResult,
|
|
46
|
+
WorkerSpawnInvocation,
|
|
47
|
+
} from "./hook.ts";
|
|
48
|
+
export {
|
|
49
|
+
appendBootstrapLog,
|
|
50
|
+
BOOTSTRAP_DOCTOR_HINT,
|
|
51
|
+
defaultWorkerSteps,
|
|
52
|
+
parseBootstrapState,
|
|
53
|
+
parseWorkerFlags,
|
|
54
|
+
readBootstrapState,
|
|
55
|
+
readPluginVersion,
|
|
56
|
+
resolvePluginDataRoot,
|
|
57
|
+
runBootstrapWorker,
|
|
58
|
+
} from "./worker.ts";
|
|
59
|
+
export type {
|
|
60
|
+
DefaultWorkerStepsSeams,
|
|
61
|
+
BootstrapDegradedEntry,
|
|
62
|
+
BootstrapRunStatus,
|
|
63
|
+
BootstrapState,
|
|
64
|
+
BootstrapStepOutcome,
|
|
65
|
+
BootstrapWorkerContext,
|
|
66
|
+
BootstrapWorkerFlags,
|
|
67
|
+
BootstrapWorkerResult,
|
|
68
|
+
BootstrapWorkerSkipReason,
|
|
69
|
+
BootstrapWorkerStep,
|
|
70
|
+
RunBootstrapWorkerOptions,
|
|
71
|
+
} from "./worker.ts";
|
|
72
|
+
|
|
73
|
+
const TOP_LEVEL_HELP =
|
|
74
|
+
"Usage:\n omo-bootstrap hook session-start\n omo-bootstrap worker [--codex-home <dir>] [--once] [--only <step>] [--manifest-dir <dir>]\n omo-bootstrap download <manifest> <platform> <destination-dir>\n omo-bootstrap help | --help | -h\n";
|
|
75
|
+
|
|
76
|
+
async function runDownloadCommand(args: readonly string[]): Promise<number> {
|
|
77
|
+
const [manifestName, platformKey, destinationDir] = args;
|
|
78
|
+
if (manifestName === undefined || platformKey === undefined || destinationDir === undefined) {
|
|
79
|
+
process.stderr.write(`[omo-bootstrap] download requires <manifest> <platform> <destination-dir>\n${TOP_LEVEL_HELP}`);
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const destination = await downloadFromManifest({ destinationDir, manifestName, platformKey });
|
|
84
|
+
process.stdout.write(`OK:${destination}\n`);
|
|
85
|
+
return 0;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
process.stderr.write(`[omo-bootstrap] download failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function runWorkerCommand(args: readonly string[]): Promise<number> {
|
|
93
|
+
let result;
|
|
94
|
+
try {
|
|
95
|
+
result = await runBootstrapWorker({ argv: args, env: process.env });
|
|
96
|
+
} catch (error) {
|
|
97
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
98
|
+
if (/flag/.test(message)) {
|
|
99
|
+
process.stderr.write(`[omo-bootstrap] ${message}\n${TOP_LEVEL_HELP}`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
// Runtime worker failures must never surface as non-zero exits; the
|
|
103
|
+
// degraded ledger in state.json is the error channel.
|
|
104
|
+
process.stderr.write(`[omo-bootstrap] worker error: ${message}\n`);
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
process.stdout.write(
|
|
108
|
+
result.ran ? `[omo-bootstrap] worker finished: ${result.status}\n` : `[omo-bootstrap] worker skipped: ${result.reason}\n`,
|
|
109
|
+
);
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function main(): Promise<number> {
|
|
114
|
+
const argv = process.argv.slice(2);
|
|
115
|
+
const command = argv[0];
|
|
116
|
+
if (command === undefined || command === "help" || command === "--help" || command === "-h") {
|
|
117
|
+
process.stdout.write(TOP_LEVEL_HELP);
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
if (command === "hook" && argv[1] === "session-start") {
|
|
121
|
+
return runSessionStartHook({ env: process.env, stdin: process.stdin });
|
|
122
|
+
}
|
|
123
|
+
if (command === "worker") {
|
|
124
|
+
return runWorkerCommand(argv.slice(1));
|
|
125
|
+
}
|
|
126
|
+
if (command === "download") {
|
|
127
|
+
return runDownloadCommand(argv.slice(1));
|
|
128
|
+
}
|
|
129
|
+
process.stderr.write(`[omo-bootstrap] unknown command: ${argv.join(" ")}\n${TOP_LEVEL_HELP}`);
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isProcessEntry(): boolean {
|
|
134
|
+
const entry = process.argv[1];
|
|
135
|
+
if (entry === undefined) return false;
|
|
136
|
+
try {
|
|
137
|
+
return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (isProcessEntry()) {
|
|
144
|
+
main()
|
|
145
|
+
.then((code) => {
|
|
146
|
+
process.exit(code);
|
|
147
|
+
})
|
|
148
|
+
.catch((error: unknown) => {
|
|
149
|
+
// The SessionStart hook path must never fail the session: log and exit 0.
|
|
150
|
+
process.stderr.write(`[omo-bootstrap] ${error instanceof Error ? error.message : String(error)}\n`);
|
|
151
|
+
process.exit(0);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, rename, rm } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
7
|
+
import type { ReadableStream as NodeWebReadableStream } from "node:stream/web";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
export type DownloadErrorCode = "checksum-mismatch" | "download-failed" | "unsupported-platform";
|
|
11
|
+
|
|
12
|
+
export class DownloadError extends Error {
|
|
13
|
+
readonly code: DownloadErrorCode;
|
|
14
|
+
|
|
15
|
+
constructor(code: DownloadErrorCode, message: string) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "DownloadError";
|
|
18
|
+
this.code = code;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ChecksumMismatchError extends DownloadError {
|
|
23
|
+
readonly expectedSha256: string;
|
|
24
|
+
readonly actualSha256: string;
|
|
25
|
+
|
|
26
|
+
constructor(options: { readonly url: string; readonly expectedSha256: string; readonly actualSha256: string }) {
|
|
27
|
+
super(
|
|
28
|
+
"checksum-mismatch",
|
|
29
|
+
`Checksum mismatch for ${options.url}: expected sha256 ${options.expectedSha256} but downloaded sha256 ${options.actualSha256}; deleted the partial download.`,
|
|
30
|
+
);
|
|
31
|
+
this.name = "ChecksumMismatchError";
|
|
32
|
+
this.expectedSha256 = options.expectedSha256;
|
|
33
|
+
this.actualSha256 = options.actualSha256;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class UnsupportedPlatformError extends DownloadError {
|
|
38
|
+
readonly manifestName: string;
|
|
39
|
+
readonly platformKey: string;
|
|
40
|
+
|
|
41
|
+
constructor(options: {
|
|
42
|
+
readonly manifestName: string;
|
|
43
|
+
readonly platformKey: string;
|
|
44
|
+
readonly availablePlatforms: readonly string[];
|
|
45
|
+
}) {
|
|
46
|
+
super(
|
|
47
|
+
"unsupported-platform",
|
|
48
|
+
`Manifest "${options.manifestName}" has no asset for unsupported platform "${options.platformKey}" (available: ${options.availablePlatforms.join(", ")}).`,
|
|
49
|
+
);
|
|
50
|
+
this.name = "UnsupportedPlatformError";
|
|
51
|
+
this.manifestName = options.manifestName;
|
|
52
|
+
this.platformKey = options.platformKey;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type FetchLike = (url: string) => Promise<Response>;
|
|
57
|
+
|
|
58
|
+
export interface ManifestAsset {
|
|
59
|
+
readonly url: string;
|
|
60
|
+
readonly sha256: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface AssetManifest {
|
|
64
|
+
readonly name: string;
|
|
65
|
+
readonly version: string;
|
|
66
|
+
readonly platforms: Readonly<Record<string, ManifestAsset>>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface DownloadChecksummedAssetOptions {
|
|
70
|
+
readonly url: string;
|
|
71
|
+
readonly sha256: string;
|
|
72
|
+
readonly destination: string;
|
|
73
|
+
readonly fetchImpl?: FetchLike;
|
|
74
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface DownloadFromManifestOptions {
|
|
78
|
+
readonly manifestName: string;
|
|
79
|
+
readonly platformKey: string;
|
|
80
|
+
readonly destinationDir: string;
|
|
81
|
+
readonly fetchImpl?: FetchLike;
|
|
82
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
83
|
+
readonly manifestsDir?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const PROXY_ENV_KEYS = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"] as const;
|
|
87
|
+
|
|
88
|
+
function proxyLimitationNote(env: NodeJS.ProcessEnv): string {
|
|
89
|
+
const configuredKey = PROXY_ENV_KEYS.find((key) => (env[key] ?? "").trim().length > 0);
|
|
90
|
+
if (configuredKey === undefined) return "";
|
|
91
|
+
return ` Note: ${configuredKey} is set, but the bootstrap downloader does not tunnel through HTTP(S) proxies in v1; the download was attempted directly.`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function describeFailure(error: unknown): string {
|
|
95
|
+
return error instanceof Error ? error.message : String(error);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function writeBodyToFile(body: NodeWebReadableStream<Uint8Array> | null, tempPath: string): Promise<string> {
|
|
99
|
+
const hash = createHash("sha256");
|
|
100
|
+
if (body === null) {
|
|
101
|
+
await pipeline(Readable.from([]), createWriteStream(tempPath));
|
|
102
|
+
return hash.digest("hex");
|
|
103
|
+
}
|
|
104
|
+
await pipeline(
|
|
105
|
+
Readable.fromWeb(body),
|
|
106
|
+
async function* hashChunks(source: AsyncIterable<Uint8Array>): AsyncGenerator<Buffer> {
|
|
107
|
+
for await (const chunk of source) {
|
|
108
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
109
|
+
hash.update(buffer);
|
|
110
|
+
yield buffer;
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
createWriteStream(tempPath),
|
|
114
|
+
);
|
|
115
|
+
return hash.digest("hex");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function downloadChecksummedAsset(options: DownloadChecksummedAssetOptions): Promise<string> {
|
|
119
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
120
|
+
const env = options.env ?? process.env;
|
|
121
|
+
const expectedSha256 = options.sha256.toLowerCase();
|
|
122
|
+
await mkdir(dirname(options.destination), { recursive: true });
|
|
123
|
+
const tempPath = `${options.destination}.${randomUUID().slice(0, 8)}.partial`;
|
|
124
|
+
|
|
125
|
+
let response: Response;
|
|
126
|
+
try {
|
|
127
|
+
response = await fetchImpl(options.url);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw new DownloadError(
|
|
130
|
+
"download-failed",
|
|
131
|
+
`Download failed for ${options.url}: ${describeFailure(error)}.${proxyLimitationNote(env)}`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
throw new DownloadError(
|
|
136
|
+
"download-failed",
|
|
137
|
+
`Download failed for ${options.url}: HTTP ${response.status}.${proxyLimitationNote(env)}`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let actualSha256: string;
|
|
142
|
+
try {
|
|
143
|
+
actualSha256 = await writeBodyToFile(response.body as NodeWebReadableStream<Uint8Array> | null, tempPath);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
await rm(tempPath, { force: true });
|
|
146
|
+
throw new DownloadError(
|
|
147
|
+
"download-failed",
|
|
148
|
+
`Download failed for ${options.url} while writing the response body: ${describeFailure(error)}.${proxyLimitationNote(env)}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (actualSha256 !== expectedSha256) {
|
|
153
|
+
await rm(tempPath, { force: true });
|
|
154
|
+
throw new ChecksumMismatchError({ actualSha256, expectedSha256, url: options.url });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
await rename(tempPath, options.destination);
|
|
158
|
+
return options.destination;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function resolveDefaultManifestsDir(): string {
|
|
162
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "manifests");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
166
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function parseManifestAsset(value: unknown, manifestName: string, platformKey: string): ManifestAsset {
|
|
170
|
+
if (!isRecord(value) || typeof value["url"] !== "string" || typeof value["sha256"] !== "string") {
|
|
171
|
+
throw new Error(`Manifest "${manifestName}" platform "${platformKey}" must pin both url and sha256 strings.`);
|
|
172
|
+
}
|
|
173
|
+
return { sha256: value["sha256"], url: value["url"] };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function parseAssetManifest(raw: string, manifestName: string): AssetManifest {
|
|
177
|
+
const data: unknown = JSON.parse(raw);
|
|
178
|
+
if (!isRecord(data) || typeof data["name"] !== "string" || typeof data["version"] !== "string" || !isRecord(data["platforms"])) {
|
|
179
|
+
throw new Error(`Manifest "${manifestName}" must declare name, version, and a platforms object.`);
|
|
180
|
+
}
|
|
181
|
+
const platforms: Record<string, ManifestAsset> = {};
|
|
182
|
+
for (const [platformKey, asset] of Object.entries(data["platforms"])) {
|
|
183
|
+
platforms[platformKey] = parseManifestAsset(asset, manifestName, platformKey);
|
|
184
|
+
}
|
|
185
|
+
return { name: data["name"], platforms, version: data["version"] };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function loadAssetManifest(manifestName: string, manifestsDir?: string): Promise<AssetManifest> {
|
|
189
|
+
const directory = manifestsDir ?? resolveDefaultManifestsDir();
|
|
190
|
+
const raw = await readFile(join(directory, `${manifestName}.json`), "utf8");
|
|
191
|
+
return parseAssetManifest(raw, manifestName);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function downloadFromManifest(options: DownloadFromManifestOptions): Promise<string> {
|
|
195
|
+
const manifest = await loadAssetManifest(options.manifestName, options.manifestsDir);
|
|
196
|
+
const asset = manifest.platforms[options.platformKey];
|
|
197
|
+
if (asset === undefined) {
|
|
198
|
+
throw new UnsupportedPlatformError({
|
|
199
|
+
availablePlatforms: Object.keys(manifest.platforms),
|
|
200
|
+
manifestName: options.manifestName,
|
|
201
|
+
platformKey: options.platformKey,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const destination = join(options.destinationDir, basename(new URL(asset.url).pathname));
|
|
205
|
+
return downloadChecksummedAsset({
|
|
206
|
+
destination,
|
|
207
|
+
sha256: asset.sha256,
|
|
208
|
+
url: asset.url,
|
|
209
|
+
...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
|
|
210
|
+
...(options.env === undefined ? {} : { env: options.env }),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { acquireLock, DEFAULT_LOCK_STALE_MS, resolveLockPath, resolveStatePath } from "../../../scripts/auto-update-state.mjs";
|
|
7
|
+
|
|
8
|
+
export type InstallFlow = "npx-local" | "marketplace" | "unknown";
|
|
9
|
+
|
|
10
|
+
export type ConfigSourceSignal = "npx-local" | "marketplace" | "unparsable";
|
|
11
|
+
|
|
12
|
+
export interface DetectInstallFlowOptions {
|
|
13
|
+
readonly pluginRoot: string;
|
|
14
|
+
readonly configToml?: string;
|
|
15
|
+
readonly marketplaceName?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface InstallFlowDetection {
|
|
19
|
+
readonly flow: InstallFlow;
|
|
20
|
+
readonly snapshotPresent: boolean;
|
|
21
|
+
readonly configSource: string | undefined;
|
|
22
|
+
readonly configSignal: ConfigSourceSignal | undefined;
|
|
23
|
+
readonly reason: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const INSTALL_SNAPSHOT_FILENAME = "lazycodex-install.json";
|
|
27
|
+
|
|
28
|
+
const DEFAULT_MARKETPLACE_NAME = "sisyphuslabs";
|
|
29
|
+
const MAX_CODEX_HOME_WALK_UP_LEVELS = 6;
|
|
30
|
+
|
|
31
|
+
export async function detectInstallFlowDetailed(options: DetectInstallFlowOptions): Promise<InstallFlowDetection> {
|
|
32
|
+
const marketplaceName = options.marketplaceName ?? DEFAULT_MARKETPLACE_NAME;
|
|
33
|
+
const snapshotPresent = await isFile(join(options.pluginRoot, INSTALL_SNAPSHOT_FILENAME));
|
|
34
|
+
const snapshotSignal: InstallFlow = snapshotPresent ? "npx-local" : "marketplace";
|
|
35
|
+
const snapshotReason = snapshotPresent
|
|
36
|
+
? `${INSTALL_SNAPSHOT_FILENAME} present at plugin root (written only by the npx installer)`
|
|
37
|
+
: `${INSTALL_SNAPSHOT_FILENAME} absent from plugin root`;
|
|
38
|
+
const scan = options.configToml === undefined ? { kind: "absent" as const } : scanMarketplaceSource(options.configToml, marketplaceName);
|
|
39
|
+
|
|
40
|
+
if (scan.kind === "absent") {
|
|
41
|
+
return {
|
|
42
|
+
configSignal: undefined,
|
|
43
|
+
configSource: undefined,
|
|
44
|
+
flow: snapshotSignal,
|
|
45
|
+
reason: `${snapshotReason}; no [marketplaces.${marketplaceName}] source to cross-check`,
|
|
46
|
+
snapshotPresent,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (scan.kind === "unparsable") {
|
|
50
|
+
return {
|
|
51
|
+
configSignal: "unparsable",
|
|
52
|
+
configSource: undefined,
|
|
53
|
+
flow: "unknown",
|
|
54
|
+
reason: `${snapshotReason}; [marketplaces.${marketplaceName}] source value is unparsable`,
|
|
55
|
+
snapshotPresent,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const configSignal = classifyMarketplaceSource(scan.source);
|
|
59
|
+
if (configSignal === "unparsable") {
|
|
60
|
+
return {
|
|
61
|
+
configSignal,
|
|
62
|
+
configSource: scan.source,
|
|
63
|
+
flow: "unknown",
|
|
64
|
+
reason: `${snapshotReason}; marketplace source ${JSON.stringify(scan.source)} is neither a local absolute path nor a git URL`,
|
|
65
|
+
snapshotPresent,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (configSignal !== snapshotSignal) {
|
|
69
|
+
return {
|
|
70
|
+
configSignal,
|
|
71
|
+
configSource: scan.source,
|
|
72
|
+
flow: "unknown",
|
|
73
|
+
reason: `${snapshotReason}, but marketplace source ${JSON.stringify(scan.source)} indicates ${configSignal}; signals disagree`,
|
|
74
|
+
snapshotPresent,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
configSignal,
|
|
79
|
+
configSource: scan.source,
|
|
80
|
+
flow: snapshotSignal,
|
|
81
|
+
reason: `${snapshotReason}; marketplace source ${JSON.stringify(scan.source)} agrees`,
|
|
82
|
+
snapshotPresent,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function detectInstallFlow(options: DetectInstallFlowOptions): Promise<InstallFlow> {
|
|
87
|
+
return (await detectInstallFlowDetailed(options)).flow;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface DetectInstallFlowFromEnvironmentOptions {
|
|
91
|
+
readonly pluginRoot: string;
|
|
92
|
+
readonly env: Record<string, string | undefined>;
|
|
93
|
+
readonly marketplaceName?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function detectInstallFlowFromEnvironment(
|
|
97
|
+
options: DetectInstallFlowFromEnvironmentOptions,
|
|
98
|
+
): Promise<InstallFlowDetection> {
|
|
99
|
+
const home = await resolveCodexHome({ env: options.env, pluginRoot: options.pluginRoot });
|
|
100
|
+
const configToml = await readOptionalFile(join(home.path, "config.toml"));
|
|
101
|
+
return detectInstallFlowDetailed({
|
|
102
|
+
pluginRoot: options.pluginRoot,
|
|
103
|
+
...(configToml === undefined ? {} : { configToml }),
|
|
104
|
+
...(options.marketplaceName === undefined ? {} : { marketplaceName: options.marketplaceName }),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function detectInstallFlowForTest(pluginRoot: string): Promise<InstallFlow> {
|
|
109
|
+
const home = await resolveCodexHome({ env: {}, pluginRoot });
|
|
110
|
+
const configToml = home.source === "walk-up" ? await readOptionalFile(join(home.path, "config.toml")) : undefined;
|
|
111
|
+
return detectInstallFlow({ pluginRoot, ...(configToml === undefined ? {} : { configToml }) });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type CodexHomeSource = "env" | "walk-up" | "default";
|
|
115
|
+
|
|
116
|
+
export interface CodexHomeResolution {
|
|
117
|
+
readonly path: string;
|
|
118
|
+
readonly source: CodexHomeSource;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ResolveCodexHomeOptions {
|
|
122
|
+
readonly env: Record<string, string | undefined>;
|
|
123
|
+
readonly pluginRoot?: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function resolveCodexHome(options: ResolveCodexHomeOptions): Promise<CodexHomeResolution> {
|
|
127
|
+
const envHome = options.env["CODEX_HOME"]?.trim();
|
|
128
|
+
if (envHome !== undefined && envHome.length > 0) {
|
|
129
|
+
return { path: resolve(envHome), source: "env" };
|
|
130
|
+
}
|
|
131
|
+
if (options.pluginRoot !== undefined) {
|
|
132
|
+
let current = resolve(options.pluginRoot);
|
|
133
|
+
for (let level = 0; level < MAX_CODEX_HOME_WALK_UP_LEVELS; level += 1) {
|
|
134
|
+
const parent = dirname(current);
|
|
135
|
+
if (parent === current) break;
|
|
136
|
+
current = parent;
|
|
137
|
+
if (await isFile(join(current, "config.toml"))) {
|
|
138
|
+
return { path: current, source: "walk-up" };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { path: join(homedir(), ".codex"), source: "default" };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface BootstrapLocksOptions {
|
|
146
|
+
readonly pluginData: string;
|
|
147
|
+
readonly env: Record<string, string | undefined>;
|
|
148
|
+
readonly now?: number;
|
|
149
|
+
readonly staleMs?: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface BootstrapLockHandle {
|
|
153
|
+
readonly statePath: string;
|
|
154
|
+
readonly bootstrapLockPath: string;
|
|
155
|
+
readonly autoUpdateLockPath: string;
|
|
156
|
+
readonly release: () => Promise<void>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function resolveBootstrapStatePath(pluginData: string): string {
|
|
160
|
+
return join(pluginData, "bootstrap", "state.json");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function resolveBootstrapLockPath(pluginData: string): string {
|
|
164
|
+
return `${resolveBootstrapStatePath(pluginData)}.lock`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function bootstrapLocks(options: BootstrapLocksOptions): Promise<BootstrapLockHandle | null> {
|
|
168
|
+
const now = options.now ?? Date.now();
|
|
169
|
+
const staleMs = options.staleMs ?? DEFAULT_LOCK_STALE_MS;
|
|
170
|
+
const statePath = resolveBootstrapStatePath(options.pluginData);
|
|
171
|
+
const bootstrapLockPath = resolveBootstrapLockPath(options.pluginData);
|
|
172
|
+
const autoUpdateLockPath = resolveLockPath(options.env, resolveStatePath(options.env));
|
|
173
|
+
|
|
174
|
+
const bootstrapLock = await acquireLock(bootstrapLockPath, now, staleMs);
|
|
175
|
+
if (bootstrapLock === null) return null;
|
|
176
|
+
if (autoUpdateLockPath === bootstrapLockPath) {
|
|
177
|
+
return { autoUpdateLockPath, bootstrapLockPath, release: () => bootstrapLock.release(), statePath };
|
|
178
|
+
}
|
|
179
|
+
const autoUpdateLock = await acquireLock(autoUpdateLockPath, now, staleMs);
|
|
180
|
+
if (autoUpdateLock === null) {
|
|
181
|
+
await bootstrapLock.release();
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
autoUpdateLockPath,
|
|
186
|
+
bootstrapLockPath,
|
|
187
|
+
release: async () => {
|
|
188
|
+
await autoUpdateLock.release();
|
|
189
|
+
await bootstrapLock.release();
|
|
190
|
+
},
|
|
191
|
+
statePath,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
type MarketplaceSourceScan = { readonly kind: "absent" } | { readonly kind: "unparsable" } | { readonly kind: "source"; readonly source: string };
|
|
196
|
+
|
|
197
|
+
function scanMarketplaceSource(configToml: string, marketplaceName: string): MarketplaceSourceScan {
|
|
198
|
+
const expectedHeaders = new Set([`marketplaces.${marketplaceName}`, `marketplaces.${JSON.stringify(marketplaceName)}`]);
|
|
199
|
+
let inMarketplaceSection = false;
|
|
200
|
+
for (const line of configToml.split("\n")) {
|
|
201
|
+
const header = parseTomlHeader(line);
|
|
202
|
+
if (header !== null) {
|
|
203
|
+
inMarketplaceSection = expectedHeaders.has(header);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (!inMarketplaceSection) continue;
|
|
207
|
+
const valueText = parseSourceAssignment(line);
|
|
208
|
+
if (valueText === null) continue;
|
|
209
|
+
const source = parseTomlStringValue(valueText);
|
|
210
|
+
return source === undefined ? { kind: "unparsable" } : { kind: "source", source };
|
|
211
|
+
}
|
|
212
|
+
return { kind: "absent" };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function parseTomlHeader(line: string): string | null {
|
|
216
|
+
const trimmed = line.trim();
|
|
217
|
+
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return null;
|
|
218
|
+
if (trimmed.startsWith("[[")) return null;
|
|
219
|
+
return trimmed.slice(1, -1).trim();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function parseSourceAssignment(line: string): string | null {
|
|
223
|
+
const match = /^\s*source\s*=\s*(.+)$/.exec(line);
|
|
224
|
+
return match === null ? null : (match[1] ?? null);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function parseTomlStringValue(valueText: string): string | undefined {
|
|
228
|
+
const trimmed = valueText.trim();
|
|
229
|
+
if (trimmed.startsWith('"')) return parseLeadingJsonString(trimmed);
|
|
230
|
+
if (trimmed.startsWith("'")) {
|
|
231
|
+
const closingIndex = trimmed.indexOf("'", 1);
|
|
232
|
+
return closingIndex === -1 ? undefined : trimmed.slice(1, closingIndex);
|
|
233
|
+
}
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function parseLeadingJsonString(value: string): string | undefined {
|
|
238
|
+
let escaped = false;
|
|
239
|
+
for (let index = 1; index < value.length; index += 1) {
|
|
240
|
+
if (escaped) {
|
|
241
|
+
escaped = false;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const char = value[index];
|
|
245
|
+
if (char === "\\") {
|
|
246
|
+
escaped = true;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (char === '"') {
|
|
250
|
+
try {
|
|
251
|
+
const parsed: unknown = JSON.parse(value.slice(0, index + 1));
|
|
252
|
+
return typeof parsed === "string" ? parsed : undefined;
|
|
253
|
+
} catch {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function classifyMarketplaceSource(source: string): ConfigSourceSignal {
|
|
262
|
+
const trimmed = source.trim();
|
|
263
|
+
if (trimmed.length === 0) return "unparsable";
|
|
264
|
+
if (/^(https?|ssh|git):\/\//i.test(trimmed) || trimmed.startsWith("git@")) return "marketplace";
|
|
265
|
+
if (trimmed.startsWith("/") || trimmed.startsWith("~") || trimmed.startsWith("\\\\") || /^[A-Za-z]:[\\/]/.test(trimmed)) {
|
|
266
|
+
return "npx-local";
|
|
267
|
+
}
|
|
268
|
+
if (trimmed.toLowerCase().endsWith(".git")) return "marketplace";
|
|
269
|
+
return "unparsable";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function isFile(path: string): Promise<boolean> {
|
|
273
|
+
try {
|
|
274
|
+
return (await stat(path)).isFile();
|
|
275
|
+
} catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function readOptionalFile(path: string): Promise<string | undefined> {
|
|
281
|
+
try {
|
|
282
|
+
return await readFile(path, "utf8");
|
|
283
|
+
} catch {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
}
|