@platless/axom-cli 0.1.0 → 0.5.9
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/README.md +1 -1
- package/dist/authScopes.js +19 -0
- package/dist/axom-patch/cli/README.md +132 -0
- package/dist/axom-patch/cli/account-limits/README.md +27 -0
- package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
- package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
- package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
- package/dist/axom-patch/cli/account-limits/generate.py +120 -0
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
- package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
- package/dist/axom-patch/cli/claude-account-limits.json +37 -0
- package/dist/axom-patch/cli/codex-account-limits.json +37 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
- package/dist/axom-patch/cli/nightly-task.md +152 -0
- package/dist/axom-patch/cli/pi-steering.json +37 -0
- package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +38 -0
- package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
- package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +61 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +82 -0
- package/dist/axom-patch/cli/qoder-foreground-output.md +42 -0
- package/dist/axom-patch/cli/qoder-steering.json +52 -0
- package/dist/axom-patch/cli/qoder-steering.md +30 -0
- package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
- package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
- package/dist/axom-patch/cli/runtime.json +63 -0
- package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
- package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
- package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
- package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
- package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
- package/dist/axom-patch/cli/zcode-models.json +38 -0
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
- package/dist/cli.js +36 -1
- package/dist/commands/auth.js +4 -13
- package/dist/commands/execute.js +4 -4
- package/dist/commands/record.js +6 -6
- package/dist/commands/struct.js +4 -4
- package/dist/commands/workstation.js +2 -2
- package/dist/credentials.js +2 -2
- package/dist/install/claude/commands.js +13 -13
- package/dist/keepers/acp/keeper.mjs +3083 -0
- package/dist/keepers/acp/manifest.json +9 -0
- package/dist/keepers/terminal/keeper.mjs +8469 -0
- package/dist/keepers/terminal/manifest.json +9 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
- package/dist/mcp.js +32 -14
- package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
- package/dist/runtime-deps/qoder-sdk/package.json +8 -0
- package/dist/test/keeperKit.js +67 -0
- package/dist/workstation/acp/acpAgents.js +2514 -681
- package/dist/workstation/acp/acpClient.js +95 -318
- package/dist/workstation/acp/activityBusy.js +43 -0
- package/dist/workstation/acp/activityReporting.js +54 -0
- package/dist/workstation/acp/adoption.js +28 -356
- package/dist/workstation/acp/authUrlSniffer.js +36 -0
- package/dist/workstation/acp/binaryInstall.js +100 -27
- package/dist/workstation/acp/builds/gc.js +245 -0
- package/dist/workstation/acp/builds/install/binary.js +28 -0
- package/dist/workstation/acp/builds/install/npm.js +235 -0
- package/dist/workstation/acp/builds/install/uv.js +254 -0
- package/dist/workstation/acp/builds/migrate.js +195 -0
- package/dist/workstation/acp/builds/model.js +128 -0
- package/dist/workstation/acp/builds/nodeGate.js +100 -0
- package/dist/workstation/acp/builds/store.js +352 -0
- package/dist/workstation/acp/commandTableProbe.js +68 -0
- package/dist/workstation/acp/configOptions.js +103 -1
- package/dist/workstation/acp/delegationMcp.js +24 -0
- package/dist/workstation/acp/envSecrets.js +27 -0
- package/dist/workstation/acp/initializeResponse.js +285 -0
- package/dist/workstation/acp/installErrors.js +12 -2
- package/dist/workstation/acp/installProbe.js +1 -1
- package/dist/workstation/acp/managedPatches.js +144 -0
- package/dist/workstation/acp/outbox.js +330 -0
- package/dist/workstation/acp/patchAssets.js +80 -0
- package/dist/workstation/acp/permission.js +335 -37
- package/dist/workstation/acp/processor/checkpoint.js +87 -0
- package/dist/workstation/acp/processor/commands.js +35 -0
- package/dist/workstation/acp/processor/control.js +355 -0
- package/dist/workstation/acp/processor/emit.js +26 -0
- package/dist/workstation/acp/processor/jsonrpc.js +172 -0
- package/dist/workstation/acp/processor/memoryLog.js +49 -0
- package/dist/workstation/acp/processor/origins.js +48 -0
- package/dist/workstation/acp/processor/output.js +67 -0
- package/dist/workstation/acp/processor/projection.js +42 -0
- package/dist/workstation/acp/processor/record.js +128 -0
- package/dist/workstation/acp/processor/runtime.js +181 -0
- package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
- package/dist/workstation/acp/processor/session.js +525 -0
- package/dist/workstation/acp/processor/sideChannel.js +80 -0
- package/dist/workstation/acp/processor/state.js +74 -0
- package/dist/workstation/acp/processor/step.js +459 -0
- package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
- package/dist/workstation/acp/processor/test/harness.js +120 -0
- package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
- package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
- package/dist/workstation/acp/processor/transport/generations.js +76 -0
- package/dist/workstation/acp/processor/transport/keeper.js +130 -0
- package/dist/workstation/acp/processor/transport/local.js +269 -0
- package/dist/workstation/acp/processor/transport/types.js +78 -0
- package/dist/workstation/acp/processor/updates.js +364 -0
- package/dist/workstation/acp/processorSession.js +1512 -0
- package/dist/workstation/acp/qoderStartup.js +45 -0
- package/dist/workstation/acp/registry/catalog.js +385 -4
- package/dist/workstation/acp/registry/detect.js +55 -0
- package/dist/workstation/acp/registry/launch.js +61 -119
- package/dist/workstation/acp/sessionUpdateMapper.js +56 -27
- package/dist/workstation/acp/steering.js +58 -0
- package/dist/workstation/acp/switching.js +114 -0
- package/dist/workstation/acp/terminalLogin.js +171 -0
- package/dist/workstation/acp/terminalLoginSpec.js +41 -0
- package/dist/workstation/acp/test/adapterFixtures.js +256 -0
- package/dist/workstation/acp/test/daemonLife.js +106 -0
- package/dist/workstation/acp/test/fakeAgent.js +185 -9
- package/dist/workstation/acp/test/keeperWorld.js +267 -0
- package/dist/workstation/acp/test/memoryOutbox.js +55 -0
- package/dist/workstation/acp/test/raw/fixtures.js +196 -0
- package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
- package/dist/workstation/acp/test/raw/sanitize.js +344 -0
- package/dist/workstation/acp/test/raw/sources.js +249 -0
- package/dist/workstation/acp/test/realAdapter.js +377 -0
- package/dist/workstation/acp/test/scriptAgent.js +206 -0
- package/dist/workstation/acp/test/sessionFrames.js +42 -0
- package/dist/workstation/acp/timedCommand.js +4 -2
- package/dist/workstation/acp/uvxInstall.js +72 -122
- package/dist/workstation/acp/validate.js +33 -5
- package/dist/workstation/agentFlow.js +103 -0
- package/dist/workstation/agentSessions.js +1686 -348
- package/dist/workstation/channels/agent.js +14 -6
- package/dist/workstation/channels/computer.js +203 -7
- package/dist/workstation/channels/computerExecs.js +200 -0
- package/dist/workstation/channels/management.js +86 -6
- package/dist/workstation/config.js +3 -1
- package/dist/workstation/connection/encryptedChannel.js +88 -21
- package/dist/workstation/daemon.js +309 -6
- package/dist/workstation/daemonctl.js +28 -2
- package/dist/workstation/desktop/binding.js +36 -0
- package/dist/workstation/desktop/bridge.js +212 -0
- package/dist/workstation/desktop/flow-control.js +52 -0
- package/dist/workstation/desktop/focused-text.js +130 -0
- package/dist/workstation/desktop/protocol.js +35 -0
- package/dist/workstation/exec/attach.js +126 -0
- package/dist/workstation/exec/command.js +40 -0
- package/dist/workstation/exec/durable.js +517 -0
- package/dist/workstation/exec/fallback.js +106 -0
- package/dist/workstation/exec/policy.js +837 -0
- package/dist/workstation/exec/shaping.js +211 -0
- package/dist/workstation/exec.js +3 -112
- package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
- package/dist/workstation/files/adapters/toolRequests.js +121 -0
- package/dist/workstation/files/client.js +455 -0
- package/dist/workstation/files/errors.js +28 -0
- package/dist/workstation/files/fence.js +350 -0
- package/dist/workstation/files/find/fileIndex.js +153 -0
- package/dist/workstation/files/find/fuzzy.js +155 -0
- package/dist/workstation/files/host.js +122 -0
- package/dist/workstation/files/hostProtocol.js +2 -0
- package/dist/workstation/files/media/cache.js +620 -0
- package/dist/workstation/files/media/encoding.js +308 -0
- package/dist/workstation/files/media/estimator.js +100 -0
- package/dist/workstation/files/media/media.js +325 -0
- package/dist/workstation/files/media/packedAudio.js +23 -0
- package/dist/workstation/files/media/scheduler.js +551 -0
- package/dist/workstation/files/media/types.js +5 -0
- package/dist/workstation/files/scope.js +45 -0
- package/dist/workstation/files/structured.js +284 -0
- package/dist/workstation/files/text.js +356 -0
- package/dist/workstation/files/uploads.js +351 -0
- package/dist/workstation/journal.js +65 -236
- package/dist/workstation/keepers/acp/client.js +101 -0
- package/dist/workstation/keepers/acp/envelope.js +62 -0
- package/dist/workstation/keepers/acp/host.js +419 -0
- package/dist/workstation/keepers/acp/main.js +18 -0
- package/dist/workstation/keepers/acp/memoryStream.js +121 -0
- package/dist/workstation/keepers/acp/params.js +137 -0
- package/dist/workstation/keepers/acp/protocol.js +66 -0
- package/dist/workstation/keepers/acp/recover.js +120 -0
- package/dist/workstation/keepers/acp/session.js +584 -0
- package/dist/workstation/keepers/acp/stderr.js +154 -0
- package/dist/workstation/keepers/kit/activity.js +103 -0
- package/dist/workstation/keepers/kit/bootstrap.js +164 -0
- package/dist/workstation/keepers/kit/buildVersion.js +51 -0
- package/dist/workstation/keepers/kit/bundle.js +120 -0
- package/dist/workstation/keepers/kit/client.js +432 -0
- package/dist/workstation/keepers/kit/identity.js +21 -0
- package/dist/workstation/keepers/kit/ipc.js +112 -0
- package/dist/workstation/keepers/kit/launchConfig.js +8 -0
- package/dist/workstation/keepers/kit/launcher.js +249 -0
- package/dist/workstation/keepers/kit/lock.js +123 -0
- package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
- package/dist/workstation/keepers/kit/paths.js +75 -0
- package/dist/workstation/keepers/kit/protocol.js +43 -0
- package/dist/workstation/keepers/kit/server.js +518 -0
- package/dist/workstation/keepers/kit/streamLog.js +697 -0
- package/dist/workstation/keepers/legacy.js +296 -0
- package/dist/workstation/keepers/policy.js +576 -0
- package/dist/workstation/keepers/stop.js +34 -0
- package/dist/workstation/keepers/terminal/client.js +213 -0
- package/dist/workstation/keepers/terminal/exec.js +981 -0
- package/dist/workstation/keepers/terminal/generations.js +129 -0
- package/dist/workstation/keepers/terminal/keeper.js +62 -0
- package/dist/workstation/keepers/terminal/main.js +43 -0
- package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
- package/dist/workstation/keepers/terminal/params.js +27 -0
- package/dist/workstation/keepers/terminal/protocol.js +56 -0
- package/dist/workstation/keepers/terminal/pty.js +330 -0
- package/dist/workstation/oauth/deviceAuthorization.js +21 -12
- package/dist/workstation/protocol/accountLimits.js +47 -0
- package/dist/workstation/protocol/agent.js +201 -8
- package/dist/workstation/protocol/common.js +36 -0
- package/dist/workstation/protocol/computer.js +107 -7
- package/dist/workstation/protocol/management.js +50 -2
- package/dist/workstation/qoderAdapter/backgroundProcess.js +118 -0
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +193 -0
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
- package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
- package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
- package/dist/workstation/qoderAdapter/contract.js +78 -0
- package/dist/workstation/qoderAdapter/foregroundOutput.js +36 -0
- package/dist/workstation/qoderAdapter/history.js +85 -0
- package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
- package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
- package/dist/workstation/qoderAdapter/main.js +95 -0
- package/dist/workstation/qoderAdapter/mapper.js +117 -0
- package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
- package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
- package/dist/workstation/qoderAdapter/permissions.js +61 -0
- package/dist/workstation/qoderAdapter/profile.js +93 -0
- package/dist/workstation/qoderAdapter/sdkDriver.js +48 -0
- package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
- package/dist/workstation/qoderAdapter/session.js +319 -0
- package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
- package/dist/workstation/qoderAdapter/subwork.js +85 -0
- package/dist/workstation/qoderAdapter/taskControl.js +38 -0
- package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
- package/dist/workstation/qoderAdapter/types.js +1 -0
- package/dist/workstation/qoderAdapter/usage.js +37 -0
- package/dist/workstation/register.js +4 -0
- package/dist/workstation/systemMetrics.js +110 -1
- package/dist/workstation/tools/browser.js +394 -0
- package/dist/workstation/tools/client.js +277 -0
- package/dist/workstation/tools/info.js +50 -0
- package/dist/workstation/tools/keeperProcess.js +124 -0
- package/dist/workstation/tools/router.js +55 -0
- package/dist/workstation/tools/terminalRouter.js +222 -0
- package/dist/workstation/tools/validation.js +86 -0
- package/dist/workstation/update/coordinator.js +32 -0
- package/dist/workstation/update/npmInstaller.js +2 -0
- package/dist/workstation/update/ptyPreflight.js +45 -0
- package/dist/workstation/update/taskRegistry.js +141 -23
- package/dist/workstation/update/watchdog.js +66 -0
- package/package.json +20 -7
- package/skills/axom/SKILL.md +55 -17
- package/skills/axom-definition/SKILL.md +32 -32
- package/skills/axom-platform/SKILL.md +39 -39
- package/dist/workstation/acp/acpSession.js +0 -1047
- package/dist/workstation/acp/contract.js +0 -494
- package/dist/workstation/acp/draftInterceptor.js +0 -154
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hosting an ACP `terminal` sign-in on the workstation, so the reader gets a link instead of an
|
|
3
|
+
* instruction to go find a terminal (spec 2026-09-24 D6, D7, D9).
|
|
4
|
+
*
|
|
5
|
+
* ## Which command
|
|
6
|
+
*
|
|
7
|
+
* `AuthMethodTerminal` (`@agentclientprotocol/sdk` 1.4.0, `types.gen.d.ts:2237`) says its `args`
|
|
8
|
+
* are appended to "the configured agent invocation". That is wrong for at least one agent in the
|
|
9
|
+
* wild: measured, appending `login` to a binary agent's `acp` launch fails with "unexpected extra
|
|
10
|
+
* argument", while the same agent's `_meta["terminal-auth"]` names the invocation that works. So a
|
|
11
|
+
* `_meta["terminal-auth"]` that carries a `command` is taken as the whole invocation, and only
|
|
12
|
+
* without one does the standard append rule apply. That key appears nowhere in the SDK; it is a
|
|
13
|
+
* convention agents use, and it is matched by **shape**, never by which agent sent it.
|
|
14
|
+
*
|
|
15
|
+
* Trusting the agent's own command adds nothing it could not already do: it is a program this
|
|
16
|
+
* daemon already runs, as the same user, on the same machine. Nothing here composes, quotes or
|
|
17
|
+
* guesses a command, and nothing goes through a shell.
|
|
18
|
+
*
|
|
19
|
+
* ## How it runs
|
|
20
|
+
*
|
|
21
|
+
* Pipes, not a terminal: this client cannot reproduce an interactive terminal, which is also why it
|
|
22
|
+
* does not declare `clientCapabilities.auth.terminal` (`:4399`). What it can host is the common
|
|
23
|
+
* shape — print a link, wait for the reader to approve it somewhere, exit 0 — and that is every
|
|
24
|
+
* device-code login. `PYTHONUNBUFFERED=1` is set because a Python-built CLI block-buffers a pipe
|
|
25
|
+
* and was measured holding its link back for over 40 seconds; it names no agent and is inert for
|
|
26
|
+
* everything else.
|
|
27
|
+
*
|
|
28
|
+
* Every exit ends the **process group**, not just the child. A single-file bundle runs its real work
|
|
29
|
+
* in a grandchild; killing only the bundle's bootstrap was measured leaving that grandchild alive
|
|
30
|
+
* for 23 minutes.
|
|
31
|
+
*/
|
|
32
|
+
import { spawn } from 'node:child_process';
|
|
33
|
+
import { randomUUID } from 'node:crypto';
|
|
34
|
+
import { createAuthUrlScanner } from './authUrlSniffer.js';
|
|
35
|
+
import { withPathPrefix } from './registry/launch.js';
|
|
36
|
+
export { metaTerminalAuthArgs, terminalLoginSpecOf } from './terminalLoginSpec.js';
|
|
37
|
+
/**
|
|
38
|
+
* The launch a terminal sign-in runs under. A `_meta["terminal-auth"]` names its own command and
|
|
39
|
+
* args — but **not its own environment**: the resolved launch's env (and PATH prefix) still apply,
|
|
40
|
+
* because the launchEnv layer must reach the login child exactly as it reaches a session. Claude's
|
|
41
|
+
* `CLAUDE_SECURESTORAGE_CONFIG_DIR` decides which credential slot `--cli auth login` writes into;
|
|
42
|
+
* a meta login launched over an empty env would seed the shared slot and strand the isolated one —
|
|
43
|
+
* and every session on it — logged out. The append shape is the launch itself, unchanged.
|
|
44
|
+
*/
|
|
45
|
+
export function launchForTerminalLogin(terminal, resolved) {
|
|
46
|
+
return terminal.kind === 'meta'
|
|
47
|
+
? { command: terminal.command, args: [], env: resolved.env, ...(resolved.pathPrefix ? { pathPrefix: resolved.pathPrefix } : {}) }
|
|
48
|
+
: resolved;
|
|
49
|
+
}
|
|
50
|
+
export class TerminalLoginFailedError extends Error {
|
|
51
|
+
}
|
|
52
|
+
export class TerminalLoginTimeoutError extends Error {
|
|
53
|
+
}
|
|
54
|
+
export class TerminalLoginCancelledError extends Error {
|
|
55
|
+
}
|
|
56
|
+
/** How many of the process's last output lines a failure quotes. */
|
|
57
|
+
const FAILURE_TAIL_LINES = 20;
|
|
58
|
+
const DEFAULT_KILL_GRACE_MS = 2_000;
|
|
59
|
+
/**
|
|
60
|
+
* The concrete command line. `baseEnv` is the workstation's and the agent's configured environment
|
|
61
|
+
* (`AcpListSpec.env` then `.agentEnv`), layered the way `connectAgent` layers a session's; the
|
|
62
|
+
* method's own `env` goes last because ACP says it overrides the base launch configuration.
|
|
63
|
+
*/
|
|
64
|
+
export function terminalInvocationFor(spec, launch, baseEnv) {
|
|
65
|
+
const env = withPathPrefix({ ...launch.env, ...baseEnv, ...spec.env }, launch.pathPrefix);
|
|
66
|
+
if (spec.kind === 'meta')
|
|
67
|
+
return { command: spec.command, args: [...spec.args], env };
|
|
68
|
+
return { command: launch.command, args: [...launch.args, ...spec.args], env };
|
|
69
|
+
}
|
|
70
|
+
function signalGroup(child, signal) {
|
|
71
|
+
if (child.pid === undefined)
|
|
72
|
+
return;
|
|
73
|
+
try {
|
|
74
|
+
// A negative pid addresses the whole group `detached: true` created. Windows has no process
|
|
75
|
+
// groups; there this reaches the direct child only.
|
|
76
|
+
process.kill(process.platform === 'win32' ? child.pid : -child.pid, signal);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Best effort, and it must stay that way: this runs inside child-process event handlers, where
|
|
80
|
+
// a throw is an uncaught exception that takes the whole daemon down. ESRCH (the group is already
|
|
81
|
+
// gone) is the common case; anything else (EPERM) still leaves nothing this code could do.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Run one terminal sign-in to completion. Resolves on exit 0; rejects with
|
|
86
|
+
* {@link TerminalLoginFailedError} (non-zero exit, signal, or spawn failure — quoting the process's
|
|
87
|
+
* own last lines), {@link TerminalLoginTimeoutError} or {@link TerminalLoginCancelledError}. Every
|
|
88
|
+
* exit ends the process group first.
|
|
89
|
+
*/
|
|
90
|
+
export function runTerminalLogin(invocation, options) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
93
|
+
if (options.signal?.aborted) {
|
|
94
|
+
reject(new TerminalLoginCancelledError('Sign-in was cancelled before it started.'));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const child = spawn(invocation.command, invocation.args, {
|
|
98
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
99
|
+
detached: process.platform !== 'win32',
|
|
100
|
+
env: { ...process.env, ...invocation.env, PYTHONUNBUFFERED: '1' },
|
|
101
|
+
});
|
|
102
|
+
const scan = createAuthUrlScanner();
|
|
103
|
+
const presented = new Set();
|
|
104
|
+
const tail = [];
|
|
105
|
+
let ending;
|
|
106
|
+
let killTimer;
|
|
107
|
+
const onLine = (raw) => {
|
|
108
|
+
const line = raw.replace(/\r$/, '');
|
|
109
|
+
if (line.trim() !== '') {
|
|
110
|
+
// eslint-disable-next-line no-control-regex -- stripping the literal ESC (0x1b) is the point
|
|
111
|
+
tail.push(line.replace(/\x1b\[[0-9;]*[A-Za-z]/g, ''));
|
|
112
|
+
if (tail.length > FAILURE_TAIL_LINES)
|
|
113
|
+
tail.shift();
|
|
114
|
+
}
|
|
115
|
+
const url = scan(line);
|
|
116
|
+
if (url && !presented.has(url)) {
|
|
117
|
+
presented.add(url);
|
|
118
|
+
options.present?.(`terminal-auth-url-${randomUUID()}`, url);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
for (const stream of [child.stdout, child.stderr]) {
|
|
122
|
+
let buffer = '';
|
|
123
|
+
stream?.setEncoding('utf8');
|
|
124
|
+
stream?.on('data', (chunk) => {
|
|
125
|
+
buffer += chunk;
|
|
126
|
+
const lines = buffer.split('\n');
|
|
127
|
+
buffer = lines.pop() ?? '';
|
|
128
|
+
lines.forEach(onLine);
|
|
129
|
+
});
|
|
130
|
+
stream?.on('end', () => { if (buffer !== '')
|
|
131
|
+
onLine(buffer); buffer = ''; });
|
|
132
|
+
}
|
|
133
|
+
const end = (reason) => {
|
|
134
|
+
if (ending)
|
|
135
|
+
return;
|
|
136
|
+
ending = reason;
|
|
137
|
+
signalGroup(child, 'SIGTERM');
|
|
138
|
+
killTimer = setTimeout(() => signalGroup(child, 'SIGKILL'), killGraceMs);
|
|
139
|
+
killTimer.unref();
|
|
140
|
+
};
|
|
141
|
+
const deadline = setTimeout(() => end(new TerminalLoginTimeoutError(`Sign-in did not finish within ${Math.round(options.deadlineMs / 1000)} seconds.`)), options.deadlineMs);
|
|
142
|
+
const onAbort = () => end(new TerminalLoginCancelledError('Sign-in was cancelled.'));
|
|
143
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
144
|
+
const settle = (outcome) => {
|
|
145
|
+
clearTimeout(deadline);
|
|
146
|
+
if (killTimer)
|
|
147
|
+
clearTimeout(killTimer);
|
|
148
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
149
|
+
if (outcome)
|
|
150
|
+
reject(outcome);
|
|
151
|
+
else
|
|
152
|
+
resolve();
|
|
153
|
+
};
|
|
154
|
+
child.once('error', (error) => {
|
|
155
|
+
settle(new TerminalLoginFailedError(`Could not start the sign-in command: ${error.message}`));
|
|
156
|
+
});
|
|
157
|
+
child.once('exit', () => {
|
|
158
|
+
// Whatever the login left behind in its group must not outlive it — see the file comment.
|
|
159
|
+
signalGroup(child, 'SIGKILL');
|
|
160
|
+
});
|
|
161
|
+
child.once('close', (code, signal) => {
|
|
162
|
+
if (ending)
|
|
163
|
+
return settle(ending);
|
|
164
|
+
if (code === 0)
|
|
165
|
+
return settle(undefined);
|
|
166
|
+
const how = code !== null ? `exited with code ${code}` : `was ended by ${signal}`;
|
|
167
|
+
const said = tail.length > 0 ? `: ${tail.join('\n')}` : '';
|
|
168
|
+
return settle(new TerminalLoginFailedError(`The sign-in command ${how}${said}`));
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function stringArray(value) {
|
|
5
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string') ? [...value] : undefined;
|
|
6
|
+
}
|
|
7
|
+
function stringRecord(value) {
|
|
8
|
+
if (!isRecord(value))
|
|
9
|
+
return {};
|
|
10
|
+
return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === 'string'));
|
|
11
|
+
}
|
|
12
|
+
function metaTerminalAuth(method) {
|
|
13
|
+
if (!isRecord(method) || !isRecord(method._meta))
|
|
14
|
+
return undefined;
|
|
15
|
+
const entry = method._meta['terminal-auth'];
|
|
16
|
+
return isRecord(entry) ? entry : undefined;
|
|
17
|
+
}
|
|
18
|
+
/** The `args` a `_meta["terminal-auth"]` entry carries, for display where the top-level `args` is absent. */
|
|
19
|
+
export function metaTerminalAuthArgs(method) {
|
|
20
|
+
return stringArray(metaTerminalAuth(method)?.args);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* How to run this advertised method as a terminal login, or `undefined` when it is not one (spec
|
|
24
|
+
* D6: top-level `type: "terminal"`, or a `_meta["terminal-auth"]` object).
|
|
25
|
+
*/
|
|
26
|
+
export function terminalLoginSpecOf(method) {
|
|
27
|
+
if (!isRecord(method))
|
|
28
|
+
return undefined;
|
|
29
|
+
const meta = metaTerminalAuth(method);
|
|
30
|
+
if (meta && typeof meta.command === 'string' && meta.command.trim() !== '') {
|
|
31
|
+
return { kind: 'meta', command: meta.command, args: stringArray(meta.args) ?? [], env: stringRecord(meta.env) };
|
|
32
|
+
}
|
|
33
|
+
if (meta || method.type === 'terminal') {
|
|
34
|
+
return {
|
|
35
|
+
kind: 'append',
|
|
36
|
+
args: stringArray(method.args) ?? stringArray(meta?.args) ?? [],
|
|
37
|
+
env: { ...stringRecord(meta?.env), ...stringRecord(method.env) },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local, network-free adapter fixtures for the build tests (spec 2026-10-11 §6): npm package
|
|
3
|
+
* tarballs, a fake `npm` that installs them with the **real** npm (offline), and binary archives.
|
|
4
|
+
*
|
|
5
|
+
* The adapter in every fixture keeps lazily loading its own modules for as long as it runs — a
|
|
6
|
+
* fresh `require` of `lib/mod-<n>.js` every few milliseconds, printed to stdout — which is exactly
|
|
7
|
+
* the access pattern a later install must never disturb.
|
|
8
|
+
*/
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
export const LAZY_MODULES = 4;
|
|
14
|
+
/** The adapter program: every 15 ms, drop and re-require one of its own modules and print what it says. */
|
|
15
|
+
export function lazyAdapterSource(version, shebang = '#!/usr/bin/env node') {
|
|
16
|
+
return `${shebang}
|
|
17
|
+
const path = require('node:path');
|
|
18
|
+
let i = 0;
|
|
19
|
+
setInterval(() => {
|
|
20
|
+
const file = path.join(__dirname, 'lib', 'mod-' + (i++ % ${LAZY_MODULES}) + '.js');
|
|
21
|
+
try {
|
|
22
|
+
delete require.cache[require.resolve(file)];
|
|
23
|
+
process.stdout.write(require(file) + '\\n');
|
|
24
|
+
} catch (error) {
|
|
25
|
+
process.stdout.write('LOAD-FAILED ' + error.message + '\\n');
|
|
26
|
+
}
|
|
27
|
+
}, 15);
|
|
28
|
+
process.stdout.write('started ${version}\\n');
|
|
29
|
+
`;
|
|
30
|
+
}
|
|
31
|
+
function lazyModules(version) {
|
|
32
|
+
return Object.fromEntries(Array.from({ length: LAZY_MODULES }, (_, n) => [
|
|
33
|
+
`lib/mod-${n}.js`, `module.exports = ${JSON.stringify(`${version}:mod-${n}`)};\n`,
|
|
34
|
+
]));
|
|
35
|
+
}
|
|
36
|
+
function writeTree(root, files) {
|
|
37
|
+
for (const [relative, content] of Object.entries(files)) {
|
|
38
|
+
const file = path.join(root, relative);
|
|
39
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
40
|
+
writeFileSync(file, content);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** An npm tarball (`package/` root, as `npm pack` writes) for `name@version` whose bin is the lazy adapter. */
|
|
44
|
+
export function npmTarball(directory, name, version, options = {}) {
|
|
45
|
+
const tag = options.tag ? `-${options.tag}` : '';
|
|
46
|
+
const source = path.join(directory, `src-${name.replace(/[@/]/g, '_')}-${version}${tag}`, 'package');
|
|
47
|
+
writeTree(source, {
|
|
48
|
+
'package.json': JSON.stringify({ name, version, bin: { [name.split('/').pop()]: 'bin/adapter.js' }, ...(options.engines ? { engines: options.engines } : {}) }),
|
|
49
|
+
'bin/adapter.js': options.binSource ?? lazyAdapterSource(version).replace("path.join(__dirname, 'lib'", "path.join(__dirname, '..', 'lib'"),
|
|
50
|
+
...lazyModules(version),
|
|
51
|
+
});
|
|
52
|
+
const tarball = path.join(directory, `${name.replace(/[@/]/g, '_')}-${version}${tag}.tgz`);
|
|
53
|
+
execFileSync('tar', ['-czf', tarball, '-C', path.dirname(source), 'package']);
|
|
54
|
+
return tarball;
|
|
55
|
+
}
|
|
56
|
+
/** A `.tar.gz` binary archive: `./adapter` (an executable script) and its lazily loaded `lib/`. */
|
|
57
|
+
export function binaryArchive(directory, version) {
|
|
58
|
+
const source = path.join(directory, `bin-src-${version}`);
|
|
59
|
+
writeTree(source, { adapter: lazyAdapterSource(version, `#!${process.execPath}`), ...lazyModules(version) });
|
|
60
|
+
chmodSync(path.join(source, 'adapter'), 0o755);
|
|
61
|
+
const archive = path.join(directory, `bin-${version}.tar.gz`);
|
|
62
|
+
execFileSync('tar', ['-czf', archive, '-C', source, '.']);
|
|
63
|
+
return readFileSync(archive);
|
|
64
|
+
}
|
|
65
|
+
/** The real npm this machine has, run offline by the fake below. */
|
|
66
|
+
export function realNpm() {
|
|
67
|
+
const sibling = path.join(path.dirname(process.execPath), 'npm');
|
|
68
|
+
return existsSync(sibling) ? sibling : 'npm';
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A stand-in `npm` that only understands `install --prefix <dir> … <spec>`: records every spec it is
|
|
72
|
+
* asked for, then either fails the way npm would or installs the mapped local tarball with the real
|
|
73
|
+
* npm, `--offline` — never the network.
|
|
74
|
+
*/
|
|
75
|
+
export function fakeNpm(directory, options) {
|
|
76
|
+
mkdirSync(directory, { recursive: true });
|
|
77
|
+
const log = path.join(directory, 'calls.txt');
|
|
78
|
+
const pidLog = path.join(directory, 'pids.txt');
|
|
79
|
+
const config = path.join(directory, 'fake-npm.json');
|
|
80
|
+
writeFileSync(config, JSON.stringify({ ...options, realNpm: realNpm(), log, pidLog, cache: path.join(directory, 'npm-cache') }));
|
|
81
|
+
const script = path.join(directory, 'npm');
|
|
82
|
+
writeFileSync(script, `#!${process.execPath}
|
|
83
|
+
const fs = require('node:fs'); const path = require('node:path'); const { spawnSync } = require('node:child_process');
|
|
84
|
+
const cfg = JSON.parse(fs.readFileSync(${JSON.stringify(config)}, 'utf8'));
|
|
85
|
+
const argv = process.argv.slice(2);
|
|
86
|
+
if (argv[0] === 'view') {
|
|
87
|
+
// \`npm view <name> version --json\`: what \`latest\` names, from the same map an unpinned install uses.
|
|
88
|
+
fs.appendFileSync(cfg.log, 'view ' + argv[1] + '\\n');
|
|
89
|
+
const latest = cfg.latest && cfg.latest[argv[1]];
|
|
90
|
+
if (!latest || cfg.noView) { process.stderr.write('npm error code E404\\n'); process.exit(1); }
|
|
91
|
+
process.stdout.write(JSON.stringify(latest.slice(latest.lastIndexOf('@') + 1)) + '\\n');
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
const spec = argv[argv.length - 1];
|
|
95
|
+
const prefix = argv[argv.indexOf('--prefix') + 1];
|
|
96
|
+
fs.appendFileSync(cfg.log, spec + '\\n');
|
|
97
|
+
const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
98
|
+
if (cfg.delayMs) sleep(cfg.delayMs);
|
|
99
|
+
if ((cfg.etarget || []).includes(spec)) {
|
|
100
|
+
process.stderr.write('npm error code ETARGET\\nnpm error notarget No matching version found for ' + spec + '.\\n');
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
if (cfg.fail && cfg.fail[spec] !== undefined) { process.stderr.write(cfg.fail[spec]); process.exit(1); }
|
|
104
|
+
if ((cfg.hang || []).includes(spec)) {
|
|
105
|
+
fs.appendFileSync(cfg.pidLog, process.pid + '\\n');
|
|
106
|
+
const name = spec.slice(0, spec.lastIndexOf('@') > 0 ? spec.lastIndexOf('@') : spec.length);
|
|
107
|
+
const dir = path.join(prefix, 'node_modules', name);
|
|
108
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
109
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name, version: spec.split('@').pop() }));
|
|
110
|
+
for (;;) sleep(1000);
|
|
111
|
+
}
|
|
112
|
+
const pinned = cfg.tarballs[spec] ? spec : cfg.latest && cfg.latest[spec];
|
|
113
|
+
const tarball = pinned && cfg.tarballs[pinned];
|
|
114
|
+
if (!tarball) { process.stderr.write('npm error code E404\\nnpm error 404 Not Found - ' + spec + '\\n'); process.exit(1); }
|
|
115
|
+
const real = spawnSync(cfg.realNpm, [...argv.slice(0, -1), '--offline', tarball], {
|
|
116
|
+
stdio: 'inherit', env: { ...process.env, npm_config_cache: cfg.cache },
|
|
117
|
+
});
|
|
118
|
+
process.exit(real.status === null ? 1 : real.status);
|
|
119
|
+
`);
|
|
120
|
+
chmodSync(script, 0o755);
|
|
121
|
+
const lines = (file) => (existsSync(file) ? readFileSync(file, 'utf8').split('\n').filter(Boolean) : []);
|
|
122
|
+
return { command: script, calls: () => lines(log), pids: () => lines(pidLog).map(Number) };
|
|
123
|
+
}
|
|
124
|
+
/** Every file, directory and symlink under `root` with its content hash / link target — byte-identity of a tree. */
|
|
125
|
+
export function treeDigest(root) {
|
|
126
|
+
const out = {};
|
|
127
|
+
const walk = (relative) => {
|
|
128
|
+
const full = path.join(root, relative);
|
|
129
|
+
const stat = lstatSync(full);
|
|
130
|
+
if (stat.isSymbolicLink())
|
|
131
|
+
out[relative] = `link:${readlinkSync(full)}`;
|
|
132
|
+
else if (stat.isDirectory()) {
|
|
133
|
+
out[relative || '.'] = `dir:${stat.mode.toString(8)}`;
|
|
134
|
+
for (const name of readdirSync(full).sort())
|
|
135
|
+
walk(relative ? path.join(relative, name) : name);
|
|
136
|
+
}
|
|
137
|
+
else
|
|
138
|
+
out[relative] = `file:${stat.mode.toString(8)}:${createHash('sha256').update(readFileSync(full)).digest('hex')}`;
|
|
139
|
+
};
|
|
140
|
+
walk('');
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* A `RunUv` that never spawns anything: `tool install` lays out what `uv tool install` really lays out
|
|
145
|
+
* (measured, `uvxInstall.ts`) under the `UV_TOOL_DIR`/`UV_TOOL_BIN_DIR` it is given — including the
|
|
146
|
+
* absolute paths, so a renamed tree really breaks — and `tool list --show-paths` reports it. Each
|
|
147
|
+
* executable is a shell script that prints `<name> <version> <exe> <args>`, and with `--wait` stays up.
|
|
148
|
+
*/
|
|
149
|
+
export function fakeUv(options) {
|
|
150
|
+
const calls = [];
|
|
151
|
+
const run = async (args, _timeoutMs, env = {}) => {
|
|
152
|
+
calls.push({ args: [...args], env: { ...env } });
|
|
153
|
+
const toolDir = env.UV_TOOL_DIR;
|
|
154
|
+
const binDir = env.UV_TOOL_BIN_DIR;
|
|
155
|
+
if (!toolDir || !binDir)
|
|
156
|
+
return { ok: false, stdout: '', error: 'fake uv refuses to touch the user tool dir' };
|
|
157
|
+
if (args[0] === 'tool' && args[1] === 'list') {
|
|
158
|
+
if (!existsSync(toolDir))
|
|
159
|
+
return { ok: true, stdout: 'No tools installed\n' };
|
|
160
|
+
let out = '';
|
|
161
|
+
for (const name of readdirSync(toolDir).sort()) {
|
|
162
|
+
const receipt = path.join(toolDir, name, 'receipt.json');
|
|
163
|
+
if (!existsSync(receipt))
|
|
164
|
+
continue;
|
|
165
|
+
const { version, executables } = JSON.parse(readFileSync(receipt, 'utf8'));
|
|
166
|
+
out += `${name} v${version} (${path.join(toolDir, name)})\n`;
|
|
167
|
+
for (const exe of executables)
|
|
168
|
+
out += `- ${exe} (${path.join(binDir, exe)})\n`;
|
|
169
|
+
}
|
|
170
|
+
return { ok: true, stdout: out || 'No tools installed\n' };
|
|
171
|
+
}
|
|
172
|
+
if (args[0] !== 'tool' || args[1] !== 'install')
|
|
173
|
+
return { ok: false, stdout: '', error: `fake uv: unexpected ${args.join(' ')}` };
|
|
174
|
+
const requirement = args[args.length - 1];
|
|
175
|
+
const pinned = requirement.includes('==') ? requirement : options.latest?.[requirement];
|
|
176
|
+
const executables = pinned ? options.packages[pinned] : undefined;
|
|
177
|
+
if (!pinned || !executables)
|
|
178
|
+
return { ok: false, stdout: '', error: `uv: no solution for ${requirement}` };
|
|
179
|
+
const [name, version] = pinned.split('==');
|
|
180
|
+
const venv = path.join(toolDir, name);
|
|
181
|
+
mkdirSync(path.join(venv, 'bin'), { recursive: true });
|
|
182
|
+
mkdirSync(binDir, { recursive: true });
|
|
183
|
+
writeFileSync(path.join(venv, 'VERSION'), version);
|
|
184
|
+
if (options.hang?.includes(requirement))
|
|
185
|
+
return new Promise(() => { });
|
|
186
|
+
if (options.fail?.[requirement] !== undefined)
|
|
187
|
+
return { ok: false, stdout: '', error: options.fail[requirement] };
|
|
188
|
+
for (const exe of executables) {
|
|
189
|
+
const script = path.join(venv, 'bin', exe);
|
|
190
|
+
// Like a real console script: an absolute path to its own environment baked in.
|
|
191
|
+
writeFileSync(script, `#!/bin/sh\necho "${name} $(cat '${path.join(venv, 'VERSION')}') ${exe} $*"\n[ "$1" = --wait ] && exec sleep 30\nexit 0\n`);
|
|
192
|
+
chmodSync(script, 0o755);
|
|
193
|
+
symlinkSync(script, path.join(binDir, exe));
|
|
194
|
+
}
|
|
195
|
+
writeFileSync(path.join(venv, 'receipt.json'), JSON.stringify({ version, executables }));
|
|
196
|
+
return { ok: true, stdout: '' };
|
|
197
|
+
};
|
|
198
|
+
return { run, calls };
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* A pure-Python wheel `name-version-py3-none-any.whl` in `directory`, written with the machine's
|
|
202
|
+
* `python3` (stdlib `zipfile`, no network). Its console script `name` prints `<name> <version>
|
|
203
|
+
* <sys.executable>`, and with `--wait` keeps re-importing its own module (printing what it says) — the
|
|
204
|
+
* lazy-loading pattern a later install must not disturb.
|
|
205
|
+
*/
|
|
206
|
+
export function pythonWheel(python, directory, name, version) {
|
|
207
|
+
mkdirSync(directory, { recursive: true });
|
|
208
|
+
const builder = path.join(directory, `make-wheel-${version}.py`);
|
|
209
|
+
writeFileSync(builder, String.raw `
|
|
210
|
+
import base64, hashlib, sys, zipfile
|
|
211
|
+
name, version, out = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
212
|
+
mod = name.replace('-', '_')
|
|
213
|
+
files = {
|
|
214
|
+
mod + '/__init__.py': (
|
|
215
|
+
"import importlib, sys, time\n"
|
|
216
|
+
"def main():\n"
|
|
217
|
+
" print('%s', '%s', sys.executable, flush=True)\n"
|
|
218
|
+
" if '--wait' in sys.argv:\n"
|
|
219
|
+
" from . import lazy\n"
|
|
220
|
+
" for _ in range(3000):\n"
|
|
221
|
+
" importlib.reload(lazy)\n"
|
|
222
|
+
" print(lazy.VALUE, flush=True)\n"
|
|
223
|
+
" time.sleep(0.02)\n"
|
|
224
|
+
) % (name, version),
|
|
225
|
+
mod + '/lazy.py': "VALUE = '%s:lazy'\n" % version,
|
|
226
|
+
'%s-%s.dist-info/METADATA' % (mod, version): 'Metadata-Version: 2.1\nName: %s\nVersion: %s\n' % (name, version),
|
|
227
|
+
'%s-%s.dist-info/WHEEL' % (mod, version): 'Wheel-Version: 1.0\nGenerator: axom-test\nRoot-Is-Purelib: true\nTag: py3-none-any\n',
|
|
228
|
+
'%s-%s.dist-info/entry_points.txt' % (mod, version): '[console_scripts]\n%s = %s:main\n' % (name, mod),
|
|
229
|
+
}
|
|
230
|
+
record = '%s-%s.dist-info/RECORD' % (mod, version)
|
|
231
|
+
lines = []
|
|
232
|
+
for path, text in files.items():
|
|
233
|
+
digest = base64.urlsafe_b64encode(hashlib.sha256(text.encode()).digest()).rstrip(b'=').decode()
|
|
234
|
+
lines.append('%s,sha256=%s,%d' % (path, digest, len(text.encode())))
|
|
235
|
+
lines.append(record + ',,')
|
|
236
|
+
files[record] = '\n'.join(lines) + '\n'
|
|
237
|
+
wheel = '%s/%s-%s-py3-none-any.whl' % (out, mod, version)
|
|
238
|
+
with zipfile.ZipFile(wheel, 'w') as archive:
|
|
239
|
+
for path, text in files.items():
|
|
240
|
+
archive.writestr(path, text)
|
|
241
|
+
print(wheel)
|
|
242
|
+
`);
|
|
243
|
+
return execFileSync(python, ['-I', builder, name, version, directory], { encoding: 'utf8' }).trim();
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* What `npx --yes <name>@<version>` leaves in npm's cache: `<npmCache>/_npx/<hash>/node_modules/…`
|
|
247
|
+
* (with npm's hidden lockfile), made by the real npm, offline, from a local tarball.
|
|
248
|
+
*/
|
|
249
|
+
export function npxCacheEntry(npmCache, hash, tarball) {
|
|
250
|
+
const prefix = path.join(npmCache, '_npx', hash);
|
|
251
|
+
mkdirSync(prefix, { recursive: true });
|
|
252
|
+
execFileSync(realNpm(), ['install', '--prefix', prefix, '--no-save', '--no-audit', '--no-fund', '--offline', tarball], {
|
|
253
|
+
env: { ...process.env, npm_config_cache: path.join(npmCache, '..', `npx-fixture-cache-${hash}`) }, stdio: 'ignore',
|
|
254
|
+
});
|
|
255
|
+
return path.join(prefix, 'node_modules');
|
|
256
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One daemon life in its own process (Task 21's kill -9 test, Task 23's fault gate): AcpAgents + AgentSessionManager over keeper-acp,
|
|
3
|
+
* the way `runDaemon` wires them — adoption at boot, then the server's frames, one JSON per stdin line, handed to the manager in order.
|
|
4
|
+
* Every frame it sends is appended to `framesOut` (one JSON line each): the server's view of what this daemon sent. It never exits on
|
|
5
|
+
* its own: the test kills it with SIGKILL — `{cmd:'offer'}` appends the reconnect offer it would send now — or sends `{cmd:'forceStop'}`, a force update's
|
|
6
|
+
* `TaskRegistry.stopAll()` followed by the restart path's `detachDurableSessions()` (keeper-hosted sessions detach), after which it exits 0.
|
|
7
|
+
* `{cmd:'forceBegin'}` is only the start of a force update (`stopAll()`, before the download): the daemon keeps running.
|
|
8
|
+
*
|
|
9
|
+
* Fault seams (`faults`, Task 23), both hang forever so a SIGKILL lands exactly there:
|
|
10
|
+
* - `holdOutboxEvent` / `holdOutboxText`: `acp.outbox.append` of entries holding a frame with this `eventName` / whose JSON contains
|
|
11
|
+
* this text — the record's commands are already in raw (the driver writes before it records), its frames never reach the outbox
|
|
12
|
+
* ("written into keeper, not recorded in the outbox");
|
|
13
|
+
* - `holdNoteType`: `acp.note` of this type — the daemon took the server's frame, nothing reached keeper-acp.
|
|
14
|
+
* Reaching a seam writes `<configDir>/held-<seam>`.
|
|
15
|
+
*
|
|
16
|
+
* Run with vite-node: `vite-node daemonLife.ts <config JSON>`; config `{configDir, distDir, framesOut, faults?}`.
|
|
17
|
+
*/
|
|
18
|
+
import { appendFileSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { createInterface } from 'node:readline';
|
|
21
|
+
import { AcpKeeperClient } from '../../keepers/acp/client.js';
|
|
22
|
+
import { KeeperLauncher } from '../../keepers/kit/launcher.js';
|
|
23
|
+
import { AgentSessionManager } from '../../agentSessions.js';
|
|
24
|
+
import { AGENT_WIRE_VERSION } from '../../protocol/agent.js';
|
|
25
|
+
import { TaskRegistry } from '../../update/taskRegistry.js';
|
|
26
|
+
import { AcpAgents } from '../acpAgents.js';
|
|
27
|
+
import { keeperGenerations } from '../processor/transport/generations.js';
|
|
28
|
+
import { KeeperTransport } from '../processor/transport/keeper.js';
|
|
29
|
+
const never = new Promise(() => { });
|
|
30
|
+
/** The transport with the configured seams: every other call goes straight through. */
|
|
31
|
+
function withFaults(inner, configDir, faults) {
|
|
32
|
+
if (!faults.holdOutboxEvent && !faults.holdOutboxText && !faults.holdNoteType)
|
|
33
|
+
return inner;
|
|
34
|
+
return new Proxy(inner, {
|
|
35
|
+
get(target, prop) {
|
|
36
|
+
const value = target[prop];
|
|
37
|
+
if (prop === 'appendOutbox' && (faults.holdOutboxEvent || faults.holdOutboxText)) {
|
|
38
|
+
return async (...args) => {
|
|
39
|
+
if (args[1].some((entry) => entry.frame.eventName === faults.holdOutboxEvent) ||
|
|
40
|
+
(faults.holdOutboxText !== undefined && JSON.stringify(args[1]).includes(faults.holdOutboxText))) {
|
|
41
|
+
writeFileSync(path.join(configDir, 'held-outbox'), JSON.stringify(args[1]));
|
|
42
|
+
return never;
|
|
43
|
+
}
|
|
44
|
+
return target.appendOutbox(...args);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (prop === 'note' && faults.holdNoteType) {
|
|
48
|
+
return async (...args) => {
|
|
49
|
+
if (args[1].type === faults.holdNoteType) {
|
|
50
|
+
writeFileSync(path.join(configDir, 'held-note'), JSON.stringify(args[1]));
|
|
51
|
+
return never;
|
|
52
|
+
}
|
|
53
|
+
return target.note(...args);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async function main() {
|
|
61
|
+
const config = JSON.parse(process.argv[process.argv.length - 1]);
|
|
62
|
+
const launcher = new KeeperLauncher({ configDir: config.configDir, workstationId: 'ws-t21', server: 'http://test.invalid', distDir: config.distDir,
|
|
63
|
+
useSystemd: false, startTimeoutMs: 20_000, umask: 0o022 });
|
|
64
|
+
const keeper = withFaults(new KeeperTransport(new AcpKeeperClient({ launcher })), config.configDir, config.faults ?? {});
|
|
65
|
+
const acp = new AcpAgents({ cacheDir: config.configDir, registry: { load: async () => [] }, transports: { keeper },
|
|
66
|
+
keeperGenerations: keeperGenerations({ launcher, current: keeper, log: () => { } }) });
|
|
67
|
+
const tasks = new TaskRegistry();
|
|
68
|
+
const manager = new AgentSessionManager(acp, null, path.join(config.configDir, 'agents'), tasks);
|
|
69
|
+
manager.bindSend((frame) => appendFileSync(config.framesOut, `${JSON.stringify(frame)}\n`), AGENT_WIRE_VERSION);
|
|
70
|
+
await manager.adoptSessions();
|
|
71
|
+
// Frames in wire order, handled without waiting for each other (as the agent channel's dispatcher does).
|
|
72
|
+
for await (const line of createInterface({ input: process.stdin, crlfDelay: Infinity })) {
|
|
73
|
+
if (!line.trim())
|
|
74
|
+
continue;
|
|
75
|
+
const message = JSON.parse(line);
|
|
76
|
+
if ('cmd' in message) {
|
|
77
|
+
if (message.cmd === 'offer') {
|
|
78
|
+
// What this daemon would put in a connection's reconnect offer now (`resumeOfferSessions`), as a frame.
|
|
79
|
+
appendFileSync(config.framesOut, `${JSON.stringify({ t: 'agent_resume_offer', sessions: await manager.resumeOfferSessions() })}\n`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (message.cmd === 'forceBegin') {
|
|
83
|
+
// A forced update starts (UpdateCoordinator: `stopAll()` before DOWNLOADING); this daemon keeps running while it downloads.
|
|
84
|
+
await tasks.stopAll();
|
|
85
|
+
appendFileSync(config.framesOut, `${JSON.stringify({ t: '__force_begun__' })}\n`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (message.cmd === 'forceStop') {
|
|
89
|
+
// The whole forced update as the sessions see it: `stopAll()`, then the restart path's detach right before the hand-off.
|
|
90
|
+
await tasks.stopAll();
|
|
91
|
+
await tasks.detachDurableSessions();
|
|
92
|
+
appendFileSync(config.framesOut, `${JSON.stringify({ t: '__force_stopped__' })}\n`);
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
void manager.handleFrame(message).catch((error) => {
|
|
98
|
+
process.stderr.write(`handling ${message.t} failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
setInterval(() => { }, 1_000);
|
|
102
|
+
}
|
|
103
|
+
main().catch((error) => {
|
|
104
|
+
process.stderr.write(`daemon life failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
});
|