@skrr-ai/cli 0.1.119 → 0.1.120
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/dist/base-command.d.ts +31 -0
- package/dist/base-command.js +106 -4
- package/dist/commands/commands.js +11 -0
- package/dist/commands/commitments/action-proposal.d.ts +13 -0
- package/dist/commands/commitments/action-proposal.js +39 -0
- package/dist/commands/commitments/action-proposals/decide.js +8 -1
- package/dist/commands/commitments/action-proposals/execute.js +11 -1
- package/dist/commands/commitments/create.js +2 -2
- package/dist/commands/commitments/doctor.d.ts +8 -0
- package/dist/commands/commitments/doctor.js +22 -0
- package/dist/commands/commitments/execute-remediation.js +12 -1
- package/dist/commands/commitments/observations.js +2 -2
- package/dist/commands/commitments/pack/bind.d.ts +18 -1
- package/dist/commands/commitments/pack/bind.js +66 -8
- package/dist/commands/commitments/pack/plan.d.ts +4 -1
- package/dist/commands/commitments/pack/plan.js +30 -2
- package/dist/commands/commitments/update.js +8 -2
- package/dist/commands/completion/index.js +1 -1
- package/dist/commands/completion/values.js +1 -1
- package/dist/commands/computer-use/audio.d.ts +1 -0
- package/dist/commands/computer-use/audio.js +1 -0
- package/dist/commands/computer-use/browser.d.ts +1 -0
- package/dist/commands/computer-use/browser.js +1 -0
- package/dist/commands/computer-use/disable.d.ts +1 -0
- package/dist/commands/computer-use/disable.js +1 -0
- package/dist/commands/computer-use/enable.d.ts +1 -0
- package/dist/commands/computer-use/enable.js +1 -0
- package/dist/commands/computer-use/index.d.ts +1 -0
- package/dist/commands/computer-use/index.js +1 -0
- package/dist/commands/computer-use/install.d.ts +1 -0
- package/dist/commands/computer-use/install.js +1 -0
- package/dist/commands/computer-use/recover.d.ts +1 -0
- package/dist/commands/computer-use/recover.js +1 -0
- package/dist/commands/computer-use/status.d.ts +1 -0
- package/dist/commands/computer-use/status.js +1 -0
- package/dist/commands/computer-use/stop.d.ts +1 -0
- package/dist/commands/computer-use/stop.js +1 -0
- package/dist/commands/context.js +16 -11
- package/dist/commands/convos/recovery.js +1 -1
- package/dist/commands/daemon/diagnostics.d.ts +9 -0
- package/dist/commands/daemon/diagnostics.js +53 -0
- package/dist/commands/daemon/index.js +2 -0
- package/dist/commands/daemon/maintenance/attest.d.ts +15 -0
- package/dist/commands/daemon/maintenance/attest.js +52 -0
- package/dist/commands/daemon/maintenance/hold.d.ts +17 -0
- package/dist/commands/daemon/maintenance/hold.js +70 -0
- package/dist/commands/daemon/maintenance/index.d.ts +12 -0
- package/dist/commands/daemon/maintenance/index.js +30 -0
- package/dist/commands/daemon/maintenance/resume.d.ts +13 -0
- package/dist/commands/daemon/maintenance/resume.js +36 -0
- package/dist/commands/daemon/maintenance/status.d.ts +13 -0
- package/dist/commands/daemon/maintenance/status.js +36 -0
- package/dist/commands/doctor.d.ts +4 -1
- package/dist/commands/doctor.js +50 -6
- package/dist/commands/followups/plans/cancel-batch.js +6 -3
- package/dist/commands/followups/plans/capacity.js +7 -3
- package/dist/commands/followups/plans/observability.js +11 -3
- package/dist/commands/followups/plans/preferences/set.js +26 -8
- package/dist/commands/followups/plans/preferences.js +1 -1
- package/dist/commands/followups/plans/show.js +1 -1
- package/dist/commands/goals/key-results/assess.d.ts +36 -0
- package/dist/commands/goals/key-results/assess.js +110 -0
- package/dist/commands/goals/key-results/history.d.ts +30 -0
- package/dist/commands/goals/key-results/history.js +104 -0
- package/dist/commands/goals/key-results/list.d.ts +5 -4
- package/dist/commands/goals/key-results/list.js +12 -6
- package/dist/commands/goals/key-results/show.d.ts +4 -2
- package/dist/commands/goals/key-results/show.js +25 -4
- package/dist/commands/goals/key-results/update.d.ts +4 -0
- package/dist/commands/goals/key-results/update.js +18 -24
- package/dist/commands/goals/plan-now.js +12 -1
- package/dist/commands/goals/planner-compare.d.ts +30 -0
- package/dist/commands/goals/planner-compare.js +136 -0
- package/dist/commands/machines/dedicated/service/exec.d.ts +3 -0
- package/dist/commands/machines/dedicated/service/exec.js +70 -11
- package/dist/commands/machines/hosted/destroy.d.ts +1 -0
- package/dist/commands/machines/hosted/destroy.js +11 -1
- package/dist/commands/machines/hosted/exec.d.ts +1 -0
- package/dist/commands/machines/hosted/exec.js +12 -1
- package/dist/commands/machines/hosted/index.js +1 -0
- package/dist/commands/machines/hosted/operation.d.ts +14 -0
- package/dist/commands/machines/hosted/operation.js +51 -0
- package/dist/commands/machines/hosted/pull.js +1 -1
- package/dist/commands/machines/hosted/start.js +6 -2
- package/dist/commands/spaces/auto-executor/compare.d.ts +18 -0
- package/dist/commands/spaces/auto-executor/compare.js +139 -0
- package/dist/commands/spaces/auto-executor/runs.d.ts +3 -0
- package/dist/commands/spaces/auto-executor/runs.js +13 -1
- package/dist/commands/spaces/charter/create-from-description.d.ts +21 -0
- package/dist/commands/spaces/charter/create-from-description.js +77 -0
- package/dist/commands/spaces/charter/show.d.ts +22 -0
- package/dist/commands/spaces/charter/show.js +61 -0
- package/dist/commands/spaces/heartbeats/list.d.ts +1 -16
- package/dist/commands/spaces/heartbeats/list.js +13 -0
- package/dist/commands/spaces/install-template.js +3 -0
- package/dist/commands/spaces/loops/migrate.d.ts +32 -0
- package/dist/commands/spaces/loops/migrate.js +103 -0
- package/dist/commands/spaces/loops/release.d.ts +14 -0
- package/dist/commands/spaces/loops/release.js +59 -0
- package/dist/commands/spaces/notebook.d.ts +25 -0
- package/dist/commands/spaces/notebook.js +75 -0
- package/dist/commands/spaces/proposals/discard.d.ts +17 -0
- package/dist/commands/spaces/proposals/discard.js +17 -0
- package/dist/commands/spaces/proposals/keep.d.ts +17 -0
- package/dist/commands/spaces/proposals/keep.js +17 -0
- package/dist/commands/spaces/proposals/list.d.ts +24 -0
- package/dist/commands/spaces/proposals/list.js +89 -0
- package/dist/commands/spaces/skills/propose.d.ts +29 -0
- package/dist/commands/spaces/skills/propose.js +82 -0
- package/dist/commands/spaces/templates/fork.js +3 -0
- package/dist/commands/spaces/triggers/show.js +6 -0
- package/dist/commands/spaces/workflows/retry.d.ts +1 -0
- package/dist/commands/spaces/workflows/retry.js +8 -1
- package/dist/commands/spaces/workflows/triggers/create.js +3 -2
- package/dist/commands/spaces/workflows/triggers/fire.js +4 -0
- package/dist/commands/spaces/workflows/triggers/update.js +2 -1
- package/dist/commands/spaces/write-policy/set.d.ts +22 -0
- package/dist/commands/spaces/write-policy/set.js +82 -0
- package/dist/commands/spaces/write-policy/show.d.ts +19 -0
- package/dist/commands/spaces/write-policy/show.js +49 -0
- package/dist/commands/store/artifacts/receipt.d.ts +14 -0
- package/dist/commands/store/artifacts/receipt.js +34 -0
- package/dist/commands/store/artifacts/release.d.ts +2 -0
- package/dist/commands/store/artifacts/release.js +11 -0
- package/dist/commands/store/governance/publish-release.d.ts +2 -0
- package/dist/commands/store/governance/publish-release.js +30 -1
- package/dist/commands/store/governance/release-receipt.d.ts +12 -0
- package/dist/commands/store/governance/release-receipt.js +32 -0
- package/dist/commands/tasks/approvals/list.d.ts +11 -5
- package/dist/commands/tasks/approvals/list.js +13 -6
- package/dist/commands/tasks/attachments/facts.d.ts +12 -0
- package/dist/commands/tasks/attachments/facts.js +29 -0
- package/dist/commands/tasks/attachments/upload.d.ts +1 -0
- package/dist/commands/tasks/attachments/upload.js +16 -0
- package/dist/commands/tasks/bulk-update.js +35 -1
- package/dist/commands/tasks/context.d.ts +15 -0
- package/dist/commands/tasks/context.js +69 -0
- package/dist/commands/tasks/copy.d.ts +18 -0
- package/dist/commands/tasks/copy.js +85 -0
- package/dist/commands/tasks/create-tree.js +97 -23
- package/dist/commands/tasks/create.js +9 -1
- package/dist/commands/tasks/deliverable/add.js +20 -10
- package/dist/commands/tasks/move.js +8 -1
- package/dist/commands/tasks/operations/show.d.ts +16 -0
- package/dist/commands/tasks/operations/show.js +98 -0
- package/dist/commands/tasks/operations/wait.d.ts +22 -0
- package/dist/commands/tasks/operations/wait.js +114 -0
- package/dist/commands/tasks/preflight.d.ts +1 -0
- package/dist/commands/tasks/preflight.js +11 -1
- package/dist/commands/tasks/rearm-recurrence.js +4 -0
- package/dist/commands/tasks/result/show.js +22 -0
- package/dist/commands/tasks/result/submit.d.ts +1 -0
- package/dist/commands/tasks/result/submit.js +3 -0
- package/dist/commands/tasks/retry-receipt.d.ts +15 -0
- package/dist/commands/tasks/retry-receipt.js +89 -0
- package/dist/commands/tasks/retry-review.d.ts +16 -0
- package/dist/commands/tasks/retry-review.js +66 -0
- package/dist/commands/tasks/retry.d.ts +4 -8
- package/dist/commands/tasks/retry.js +34 -68
- package/dist/commands/tasks/runs.d.ts +21 -0
- package/dist/commands/tasks/runs.js +95 -0
- package/dist/commands/tasks/show.d.ts +2 -0
- package/dist/commands/tasks/show.js +15 -1
- package/dist/commands/tasks/start-receipt.d.ts +15 -0
- package/dist/commands/tasks/start-receipt.js +95 -0
- package/dist/commands/tasks/start.d.ts +4 -0
- package/dist/commands/tasks/start.js +49 -3
- package/dist/commands/tasks/update.js +3 -0
- package/dist/commands/tasks/upsert.js +17 -3
- package/dist/commands/tasks/watch.d.ts +3 -0
- package/dist/commands/tasks/watch.js +85 -20
- package/dist/commands/wiki/history.d.ts +33 -0
- package/dist/commands/wiki/history.js +134 -0
- package/dist/commands/wiki/ls.d.ts +2 -0
- package/dist/commands/wiki/ls.js +8 -0
- package/dist/commands/wiki/update.d.ts +3 -1
- package/dist/commands/wiki/update.js +24 -2
- package/dist/commands/wiki/write.d.ts +22 -0
- package/dist/commands/wiki/write.js +144 -6
- package/dist/generated/agent-guides.json +81 -81
- package/dist/generated/managed-instruction-guides.json +1 -1
- package/dist/lib/agent-guides.d.ts +9 -0
- package/dist/lib/agent-guides.js +23 -1
- package/dist/lib/agentic-stream.d.ts +5 -0
- package/dist/lib/agentic-stream.js +122 -3
- package/dist/lib/auth-diagnostics.d.ts +39 -0
- package/dist/lib/auth-diagnostics.js +71 -0
- package/dist/lib/cli-client-identity.d.ts +6 -0
- package/dist/lib/cli-client-identity.js +16 -0
- package/dist/lib/cli-installation-diagnostics.d.ts +52 -0
- package/dist/lib/cli-installation-diagnostics.js +254 -0
- package/dist/lib/command-hygiene.d.ts +45 -0
- package/dist/lib/command-hygiene.js +413 -0
- package/dist/lib/commitments.d.ts +64 -3
- package/dist/lib/commitments.js +34 -6
- package/dist/lib/completion-index.d.ts +16 -4
- package/dist/lib/completion-index.js +50 -14
- package/dist/lib/config.js +7 -1
- package/dist/lib/daemon-maintenance-command.d.ts +22 -0
- package/dist/lib/daemon-maintenance-command.js +39 -0
- package/dist/lib/daemon-maintenance.d.ts +275 -0
- package/dist/lib/daemon-maintenance.js +1035 -0
- package/dist/lib/dedicated-service.d.ts +3 -2
- package/dist/lib/dedicated-service.js +11 -3
- package/dist/lib/governed-store.d.ts +8 -0
- package/dist/lib/governed-store.js +5 -0
- package/dist/lib/hosted-machines.d.ts +33 -2
- package/dist/lib/hosted-machines.js +19 -1
- package/dist/lib/json-output.d.ts +1 -1
- package/dist/lib/machines.d.ts +9 -1
- package/dist/lib/machines.js +15 -1
- package/dist/lib/node-adapter.js +13 -0
- package/dist/lib/record-output.d.ts +22 -0
- package/dist/lib/record-output.js +78 -0
- package/dist/lib/roles.d.ts +3 -3
- package/dist/lib/scoped-memory-command.d.ts +18 -0
- package/dist/lib/scoped-memory-command.js +53 -2
- package/dist/lib/space-charter.d.ts +49 -0
- package/dist/lib/space-charter.js +78 -0
- package/dist/lib/space-loop-migration.d.ts +18 -0
- package/dist/lib/space-loop-migration.js +128 -0
- package/dist/lib/space-notebook.d.ts +22 -0
- package/dist/lib/space-notebook.js +140 -0
- package/dist/lib/space-proposal-decision-command.d.ts +36 -0
- package/dist/lib/space-proposal-decision-command.js +108 -0
- package/dist/lib/space-proposals.d.ts +44 -0
- package/dist/lib/space-proposals.js +186 -0
- package/dist/lib/space-templates.d.ts +8 -0
- package/dist/lib/space-templates.js +17 -0
- package/dist/lib/space-write-policy.d.ts +24 -0
- package/dist/lib/space-write-policy.js +103 -0
- package/dist/lib/task-agent-context-capabilities.d.ts +10 -0
- package/dist/lib/task-agent-context-capabilities.js +22 -0
- package/dist/lib/task-agent-context-compatibility.d.ts +9 -0
- package/dist/lib/task-agent-context-compatibility.js +30 -0
- package/dist/lib/task-asset-upload.d.ts +22 -0
- package/dist/lib/task-asset-upload.js +257 -29
- package/dist/lib/task-batch.d.ts +15 -1
- package/dist/lib/task-batch.js +35 -2
- package/dist/lib/task-operation-watch.d.ts +66 -0
- package/dist/lib/task-operation-watch.js +159 -0
- package/dist/lib/task-resolver.d.ts +1 -1
- package/dist/lib/task-resolver.js +4 -3
- package/dist/lib/task-work.d.ts +1 -1
- package/dist/lib/task-work.js +35 -1
- package/dist/lib/task-write-attribution.d.ts +14 -0
- package/dist/lib/task-write-attribution.js +35 -2
- package/dist/lib/triggers.d.ts +11 -0
- package/dist/lib/triggers.js +7 -0
- package/dist/lib/wiki-update.d.ts +1 -0
- package/dist/lib/wiki-update.js +10 -1
- package/dist/lib/wiki.js +4 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.d.ts +9 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.js +31 -15
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonWire.js +13 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/guidedUpdate.d.ts +370 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/guidedUpdate.js +315 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/maintenanceReportWire.d.ts +178 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/maintenanceReportWire.js +262 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/toolApprovalSignature.d.ts +51 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/toolApprovalSignature.js +106 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.d.ts +9 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.js +32 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonWire.js +33 -14
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/guidedUpdate.d.ts +370 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/guidedUpdate.js +404 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/maintenanceReportWire.d.ts +178 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/maintenanceReportWire.js +342 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/toolApprovalSignature.d.ts +51 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/toolApprovalSignature.js +138 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +20 -0
- package/dist/node_modules/@skrr-ai/data-provider/index.js +27085 -20250
- package/oclif.manifest.json +10566 -7247
- package/package.json +20 -2
|
@@ -0,0 +1,1035 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.maintenanceClock = exports.maintenanceApi = exports.MAINTENANCE_MAX_TIMEOUT_MS = exports.MAINTENANCE_DEFAULT_TIMEOUT_MS = exports.MAINTENANCE_POLL_INTERVAL_MS = exports.DAEMON_MAINTENANCE_CLI_SCHEMA = void 0;
|
|
4
|
+
exports.failureFromError = failureFromError;
|
|
5
|
+
exports.attempt = attempt;
|
|
6
|
+
exports.looksLikeRuntimeKey = looksLikeRuntimeKey;
|
|
7
|
+
exports.resolveMaintenanceTarget = resolveMaintenanceTarget;
|
|
8
|
+
exports.stateLine = stateLine;
|
|
9
|
+
exports.waitForReady = waitForReady;
|
|
10
|
+
exports.defaultIo = defaultIo;
|
|
11
|
+
exports.outcomeLines = outcomeLines;
|
|
12
|
+
exports.describeView = describeView;
|
|
13
|
+
exports.explainLines = explainLines;
|
|
14
|
+
exports.runStatus = runStatus;
|
|
15
|
+
exports.runHold = runHold;
|
|
16
|
+
exports.runResume = runResume;
|
|
17
|
+
exports.holdersShown = holdersShown;
|
|
18
|
+
exports.runAttest = runAttest;
|
|
19
|
+
exports.parseTimeout = parseTimeout;
|
|
20
|
+
exports.invalidFlagRun = invalidFlagRun;
|
|
21
|
+
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
22
|
+
const format_1 = require("./format");
|
|
23
|
+
const hosted_machines_1 = require("./hosted-machines");
|
|
24
|
+
const machines_1 = require("./machines");
|
|
25
|
+
/**
|
|
26
|
+
* daemon-maintenance.ts — the owner's pause on one of their own machines, from the
|
|
27
|
+
* terminal (`skrr daemon maintenance status | hold | resume | attest`; OSK-19107,
|
|
28
|
+
* Space OSK-S-524, plan §3.6, journey J12).
|
|
29
|
+
*
|
|
30
|
+
* ## What this file does NOT do
|
|
31
|
+
*
|
|
32
|
+
* It declares no phase, cause, refusal, control or word. All of them are owned by
|
|
33
|
+
* `@skrr-ai/data-provider` (`daemonMaintenance.ts`) and imported: the SERVER decides
|
|
34
|
+
* which phase a machine is in (`deriveMaintenanceJourney`), this file renders what
|
|
35
|
+
* the server answered. A synonym spelled here is the drift that module exists to stop.
|
|
36
|
+
*
|
|
37
|
+
* It never builds an `/api/...` URL. Every call is a `dataService` function
|
|
38
|
+
* (`listDaemonMaintenance` and its three siblings), which is how
|
|
39
|
+
* `domain-api-parity.spec.ts` sees this domain.
|
|
40
|
+
*
|
|
41
|
+
* ## The contract a script can rely on
|
|
42
|
+
*
|
|
43
|
+
* With `--json` every command writes EXACTLY ONE JSON object to stdout and nothing
|
|
44
|
+
* else (progress goes to stderr), with `schema: 1` and a `command` and an `outcome`.
|
|
45
|
+
* The exit code follows `scripts/release-status.js` and is in the document as
|
|
46
|
+
* `exit`: `DAEMON_MAINTENANCE_EXIT_CODES` — 0 ready, 1 refused, 2 still pending at
|
|
47
|
+
* the timeout, 3 unknown.
|
|
48
|
+
*
|
|
49
|
+
* - Only `hold --wait ready` and a command that could not read the state end in 2 or 3.
|
|
50
|
+
* `status`, a `hold` that did not wait, `resume` and `attest` end in 0 when they did
|
|
51
|
+
* what was asked, and in 1 when the server (or this CLI, before asking) refused.
|
|
52
|
+
* - **A timeout NEVER releases the hold**, and the document and the text both say so
|
|
53
|
+
* (`holdInForce: true`). A script cannot mistake "timed out" for "ready", and a
|
|
54
|
+
* person is told the one command that lets go.
|
|
55
|
+
* - A state this CLI could not read (the network, a 5xx, a 429) is `unknown`, exit 3:
|
|
56
|
+
* "I could not tell" is an answer, and it is never rendered as `ready` or as zero.
|
|
57
|
+
*
|
|
58
|
+
* ## Naming a machine, and why it never picks
|
|
59
|
+
*
|
|
60
|
+
* `<machine>` is a machine id, a name, or a runtimeKey. A hold addresses ONE
|
|
61
|
+
* `runtimeKey` (one daemon, so one profile), and one machine can span several, so a
|
|
62
|
+
* machine that is two daemons is ambiguous by construction. Resolution therefore
|
|
63
|
+
* REFUSES with the candidates when a reference matches zero or more than one, and
|
|
64
|
+
* the only inference it makes is the unambiguous one: no reference given and exactly
|
|
65
|
+
* one daemon that qualifies.
|
|
66
|
+
*/
|
|
67
|
+
/** The version of the documents `--json` prints. Bump it when a field's meaning changes. */
|
|
68
|
+
exports.DAEMON_MAINTENANCE_CLI_SCHEMA = 1;
|
|
69
|
+
/** How often `--wait` reads the state, and how long it waits when no timeout is given. */
|
|
70
|
+
exports.MAINTENANCE_POLL_INTERVAL_MS = 5_000;
|
|
71
|
+
exports.MAINTENANCE_DEFAULT_TIMEOUT_MS = 30 * 60_000;
|
|
72
|
+
exports.MAINTENANCE_MAX_TIMEOUT_MS = 24 * 60 * 60_000;
|
|
73
|
+
/** Progress is repeated at least this often (in polls), even when nothing changed. */
|
|
74
|
+
const PROGRESS_EVERY_POLLS = 12;
|
|
75
|
+
/** The server bounds `acknowledged` entries at this length; so does the CLI before it asks. */
|
|
76
|
+
const MAX_ACKNOWLEDGED_LABEL = 160;
|
|
77
|
+
const MAX_ACKNOWLEDGED = 20;
|
|
78
|
+
const MAX_CANDIDATES_LISTED = 20;
|
|
79
|
+
// ── the four calls, and the machine list ───────────────────────────────────
|
|
80
|
+
/**
|
|
81
|
+
* Every call this CLI makes. A plain mutable object so a test replaces one member
|
|
82
|
+
* without touching a module: the real members are `dataService` functions and the
|
|
83
|
+
* machine list, nothing else.
|
|
84
|
+
*/
|
|
85
|
+
exports.maintenanceApi = {
|
|
86
|
+
listMachines: () => (0, machines_1.listMachines)(),
|
|
87
|
+
list: () => data_provider_1.dataService.listDaemonMaintenance(),
|
|
88
|
+
get: (runtimeKey) => data_provider_1.dataService.getDaemonMaintenance(runtimeKey),
|
|
89
|
+
change: (runtimeKey, body) => data_provider_1.dataService.changeDaemonMaintenance(runtimeKey, body),
|
|
90
|
+
attest: (runtimeKey, body) => data_provider_1.dataService.attestDaemonMaintenance(runtimeKey, body),
|
|
91
|
+
};
|
|
92
|
+
/** The clock and the sleep `--wait` uses, replaceable so a timeout takes no time to test. */
|
|
93
|
+
exports.maintenanceClock = {
|
|
94
|
+
now: () => Date.now(),
|
|
95
|
+
sleep: (ms) => (0, hosted_machines_1.sleep)(ms),
|
|
96
|
+
};
|
|
97
|
+
function parseBody(body) {
|
|
98
|
+
if (typeof body !== 'string' || !body.trim())
|
|
99
|
+
return null;
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(body);
|
|
102
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
103
|
+
? parsed
|
|
104
|
+
: null;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const STATUS_CODES = {
|
|
111
|
+
400: 'invalid_request',
|
|
112
|
+
401: 'unauthorized',
|
|
113
|
+
403: 'forbidden',
|
|
114
|
+
404: 'not_found',
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* What a failed call means. Two kinds, and the difference is the whole design:
|
|
118
|
+
*
|
|
119
|
+
* - `refused`: the server ANSWERED no (a 4xx other than 429). The answer carries a
|
|
120
|
+
* code the vocabulary knows, or a transport word (`invalid_request`,
|
|
121
|
+
* `unauthorized`), and a sentence. Repeating the request unchanged repeats it.
|
|
122
|
+
* - `unknown`: there was NO answer (the network, a 5xx, a 429). Nothing is known
|
|
123
|
+
* about the machine's state from it, so it is never reported as a refusal or as
|
|
124
|
+
* "not held".
|
|
125
|
+
*/
|
|
126
|
+
function failureFromError(err) {
|
|
127
|
+
const e = (err && typeof err === 'object' ? err : {});
|
|
128
|
+
const status = typeof e.status === 'number' ? e.status : undefined;
|
|
129
|
+
if (status === undefined || status === 429 || status >= 500 || status === 408) {
|
|
130
|
+
return {
|
|
131
|
+
kind: 'unknown',
|
|
132
|
+
code: status === undefined ? 'unreachable' : `http_${status}`,
|
|
133
|
+
message: status === undefined
|
|
134
|
+
? 'The server could not be reached, so the machine state is not known.'
|
|
135
|
+
: `The server did not answer (HTTP ${status}), so the machine state is not known.`,
|
|
136
|
+
...(status !== undefined ? { status } : {}),
|
|
137
|
+
...(typeof e.retryAfterMs === 'number' ? { retryAfterMs: e.retryAfterMs } : {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const body = parseBody(e.body);
|
|
141
|
+
const code = typeof body?.code === 'string' && body.code
|
|
142
|
+
? body.code
|
|
143
|
+
: (STATUS_CODES[status] ?? `http_${status}`);
|
|
144
|
+
const fromBody = [body?.message, body?.error].find((value) => typeof value === 'string' && value.length > 0);
|
|
145
|
+
const message = status === 401
|
|
146
|
+
? 'You are not signed in. Run `skrr login`, then try again.'
|
|
147
|
+
: fromBody || (0, data_provider_1.maintenanceRefusalMessage)(code);
|
|
148
|
+
const candidates = Array.isArray(body?.candidates)
|
|
149
|
+
? body.candidates.filter((c) => typeof c === 'string')
|
|
150
|
+
: undefined;
|
|
151
|
+
return {
|
|
152
|
+
kind: 'refused',
|
|
153
|
+
code,
|
|
154
|
+
message,
|
|
155
|
+
status,
|
|
156
|
+
...(typeof body?.currentGeneration === 'number'
|
|
157
|
+
? { currentGeneration: body.currentGeneration }
|
|
158
|
+
: {}),
|
|
159
|
+
...(candidates && candidates.length ? { candidates } : {}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** Run one call and turn what it throws into a value. */
|
|
163
|
+
async function attempt(call) {
|
|
164
|
+
try {
|
|
165
|
+
return { ok: true, value: await call() };
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
return { ok: false, error: failureFromError(err) };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// ── naming a machine ───────────────────────────────────────────────────────
|
|
172
|
+
const RUNTIME_KEY_SHAPE = /^rk\d+:[A-Za-z0-9_.:-]+$/;
|
|
173
|
+
/** A runtimeKey needs no lookup: the server decides whether it is one of ours. */
|
|
174
|
+
function looksLikeRuntimeKey(ref) {
|
|
175
|
+
return RUNTIME_KEY_SHAPE.test(ref);
|
|
176
|
+
}
|
|
177
|
+
function afterIdentityKind(value) {
|
|
178
|
+
if (!value)
|
|
179
|
+
return undefined;
|
|
180
|
+
const separator = value.indexOf(':');
|
|
181
|
+
return separator === -1 ? undefined : value.slice(separator + 1);
|
|
182
|
+
}
|
|
183
|
+
function machineDisplayName(machine) {
|
|
184
|
+
return machine.preferences?.name || machine.name;
|
|
185
|
+
}
|
|
186
|
+
function idsOfMachine(machine) {
|
|
187
|
+
return [
|
|
188
|
+
machine.id,
|
|
189
|
+
machine.machineUuid,
|
|
190
|
+
machine.stableDaemonId,
|
|
191
|
+
machine.daemonId,
|
|
192
|
+
afterIdentityKind(machine.id),
|
|
193
|
+
afterIdentityKind(machine.machineUuid),
|
|
194
|
+
...(machine.harnesses ?? []).map((h) => h.id),
|
|
195
|
+
].filter((v) => typeof v === 'string' && v.length > 0);
|
|
196
|
+
}
|
|
197
|
+
/** Build what a reference can match: machines from the list, and holds (orphans live only there). */
|
|
198
|
+
function buildPool(machines, holds) {
|
|
199
|
+
const pool = { candidates: [], unholdable: [], unlisted: 0 };
|
|
200
|
+
const byKey = new Map();
|
|
201
|
+
for (const machine of machines) {
|
|
202
|
+
const ids = idsOfMachine(machine);
|
|
203
|
+
const names = [machine.name, machine.preferences?.name]
|
|
204
|
+
.filter((v) => Boolean(v))
|
|
205
|
+
.map((v) => v.toLowerCase());
|
|
206
|
+
if (machine.lease) {
|
|
207
|
+
pool.unholdable.push({ item: { machine, code: 'managed_machine' }, ids, names });
|
|
208
|
+
}
|
|
209
|
+
else if (machine.daemons === undefined) {
|
|
210
|
+
pool.unlisted += 1;
|
|
211
|
+
}
|
|
212
|
+
else if (machine.daemons.length === 0) {
|
|
213
|
+
pool.unholdable.push({ item: { machine, code: 'no_stable_identity' }, ids, names });
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
for (const daemon of machine.daemons) {
|
|
217
|
+
const entry = {
|
|
218
|
+
candidate: {
|
|
219
|
+
runtimeKey: daemon.runtimeKey,
|
|
220
|
+
machineId: machine.id,
|
|
221
|
+
name: machineDisplayName(machine),
|
|
222
|
+
profile: daemon.profile,
|
|
223
|
+
daemonId: daemon.daemonId,
|
|
224
|
+
held: daemon.maintenance?.held === true,
|
|
225
|
+
},
|
|
226
|
+
ids: [...ids, daemon.daemonId, daemon.runtimeKey],
|
|
227
|
+
names,
|
|
228
|
+
};
|
|
229
|
+
byKey.set(daemon.runtimeKey, entry);
|
|
230
|
+
pool.candidates.push(entry);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// The holds list is the authority on what is held (a primary read), and the only
|
|
235
|
+
// place an orphan — a hold whose machine has no row any more — can be found.
|
|
236
|
+
for (const view of holds) {
|
|
237
|
+
const live = view.hold?.lifecycle === 'held';
|
|
238
|
+
const cover = view.machine?.covers?.[0];
|
|
239
|
+
const existing = byKey.get(view.runtimeKey);
|
|
240
|
+
if (existing) {
|
|
241
|
+
if (live)
|
|
242
|
+
existing.candidate.held = true;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const entry = {
|
|
246
|
+
candidate: {
|
|
247
|
+
runtimeKey: view.runtimeKey,
|
|
248
|
+
machineId: view.machine?.id,
|
|
249
|
+
name: view.machine?.name,
|
|
250
|
+
profile: cover?.profile ?? null,
|
|
251
|
+
daemonId: cover?.daemonId,
|
|
252
|
+
held: live,
|
|
253
|
+
},
|
|
254
|
+
ids: [view.runtimeKey, view.machine?.id, cover?.daemonId].filter((v) => typeof v === 'string' && v.length > 0),
|
|
255
|
+
names: view.machine?.name ? [view.machine.name.toLowerCase()] : [],
|
|
256
|
+
};
|
|
257
|
+
byKey.set(view.runtimeKey, entry);
|
|
258
|
+
pool.candidates.push(entry);
|
|
259
|
+
}
|
|
260
|
+
return pool;
|
|
261
|
+
}
|
|
262
|
+
function listed(candidates) {
|
|
263
|
+
return candidates.slice(0, MAX_CANDIDATES_LISTED);
|
|
264
|
+
}
|
|
265
|
+
function refusal(code, extra = {}) {
|
|
266
|
+
return { code, message: (0, data_provider_1.maintenanceRefusalMessage)(code), ...extra };
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Which daemon a reference names. Pure: the lists are passed in.
|
|
270
|
+
*
|
|
271
|
+
* A reference is matched in three tiers and the FIRST tier with any match decides:
|
|
272
|
+
* an exact id or runtimeKey, then an exact name (any case), then a unique id prefix of
|
|
273
|
+
* at least four characters. Within the tier the answer is the set of matching
|
|
274
|
+
* DAEMONS, and a set of anything but one is a refusal that lists them.
|
|
275
|
+
*
|
|
276
|
+
* `scope: 'held'` (resume, attest) considers only daemons with a live hold: naming a
|
|
277
|
+
* machine that is not held is `not_held`, not a pick of some other machine.
|
|
278
|
+
*
|
|
279
|
+
* With no reference, the one candidate that qualifies is used and anything else is a
|
|
280
|
+
* refusal listing them: `hold` with no argument on an account with a single
|
|
281
|
+
* machine is not a guess, and on an account with two it never picks.
|
|
282
|
+
*/
|
|
283
|
+
function resolveMaintenanceTarget(input) {
|
|
284
|
+
const ref = input.ref?.trim();
|
|
285
|
+
if (ref && looksLikeRuntimeKey(ref))
|
|
286
|
+
return { ok: true, target: { runtimeKey: ref } };
|
|
287
|
+
const pool = buildPool(input.machines ?? [], input.holds ?? []);
|
|
288
|
+
const qualifying = (c) => input.scope === 'any' || c.held === true;
|
|
289
|
+
if (!ref) {
|
|
290
|
+
const candidates = pool.candidates.map((e) => e.candidate).filter(qualifying);
|
|
291
|
+
if (candidates.length === 1)
|
|
292
|
+
return { ok: true, target: candidates[0] };
|
|
293
|
+
if (candidates.length > 1) {
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
refusal: refusal('ambiguous', {
|
|
297
|
+
message: `More than one daemon qualifies (${candidates.length}). Name one: a machine name, a machine id, or a runtimeKey.`,
|
|
298
|
+
candidates: listed(candidates),
|
|
299
|
+
}),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
ok: false,
|
|
304
|
+
refusal: input.scope === 'held'
|
|
305
|
+
? refusal('not_held', { message: 'No machine of yours is held.' })
|
|
306
|
+
: refusal('not_found', {
|
|
307
|
+
message: pool.unlisted > 0
|
|
308
|
+
? "No daemon of yours is listed as holdable. This server does not list machines' daemons: name a runtimeKey."
|
|
309
|
+
: 'No machine of yours can be held: every one is managed by the platform or has no stable identity.',
|
|
310
|
+
}),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const lower = ref.toLowerCase();
|
|
314
|
+
const tiers = [
|
|
315
|
+
(t) => t.ids.includes(ref),
|
|
316
|
+
(t) => t.names.includes(lower),
|
|
317
|
+
(t) => lower.length >= 4 && t.ids.some((id) => id.toLowerCase().startsWith(lower)),
|
|
318
|
+
];
|
|
319
|
+
for (const matches of tiers) {
|
|
320
|
+
const hits = pool.candidates.filter((e) => matches(e));
|
|
321
|
+
const blocked = pool.unholdable.filter((e) => matches(e));
|
|
322
|
+
if (hits.length === 0 && blocked.length === 0)
|
|
323
|
+
continue;
|
|
324
|
+
const wanted = hits.map((e) => e.candidate).filter(qualifying);
|
|
325
|
+
if (wanted.length === 1)
|
|
326
|
+
return { ok: true, target: wanted[0] };
|
|
327
|
+
if (wanted.length > 1) {
|
|
328
|
+
return {
|
|
329
|
+
ok: false,
|
|
330
|
+
refusal: refusal('ambiguous', {
|
|
331
|
+
message: `"${ref}" names ${wanted.length} daemons (a machine can run several profiles). Name one by its runtimeKey.`,
|
|
332
|
+
candidates: listed(wanted),
|
|
333
|
+
}),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
// Matched, but nothing of it qualifies.
|
|
337
|
+
if (hits.length > 0) {
|
|
338
|
+
return {
|
|
339
|
+
ok: false,
|
|
340
|
+
refusal: refusal('not_held', { message: `"${ref}" is not held.` }),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
if (blocked.length === 1) {
|
|
344
|
+
return { ok: false, refusal: refusal(blocked[0].item.code) };
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
ok: false,
|
|
348
|
+
refusal: refusal('ambiguous', {
|
|
349
|
+
message: `"${ref}" matches ${blocked.length} machines the platform manages or that have no stable identity, and none can be held.`,
|
|
350
|
+
candidates: blocked.slice(0, MAX_CANDIDATES_LISTED).map(({ item }) => ({
|
|
351
|
+
runtimeKey: '',
|
|
352
|
+
machineId: item.machine.id,
|
|
353
|
+
name: machineDisplayName(item.machine),
|
|
354
|
+
})),
|
|
355
|
+
}),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
const all = pool.candidates.map((e) => e.candidate).filter(qualifying);
|
|
359
|
+
return {
|
|
360
|
+
ok: false,
|
|
361
|
+
refusal: refusal('not_found', {
|
|
362
|
+
message: `No machine of yours matches "${ref}". ` +
|
|
363
|
+
(pool.unlisted > 0
|
|
364
|
+
? "This server does not list machines' daemons, so only a runtimeKey is accepted. "
|
|
365
|
+
: '') +
|
|
366
|
+
(all.length > 0 ? 'The daemons that qualify are listed.' : 'Run `skrr machines list`.'),
|
|
367
|
+
candidates: listed(all),
|
|
368
|
+
}),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
/** The word and cause a progress line shows: the vocabulary's word, and the cause code as is. */
|
|
372
|
+
function stateLine(view) {
|
|
373
|
+
const { phase, cause, modifiers } = view.journey;
|
|
374
|
+
const word = data_provider_1.DAEMON_MAINTENANCE_WORDS[phase] ?? phase;
|
|
375
|
+
return [word, cause ? `(${cause})` : '', modifiers.length ? `[${modifiers.join(', ')}]` : '']
|
|
376
|
+
.filter(Boolean)
|
|
377
|
+
.join(' ');
|
|
378
|
+
}
|
|
379
|
+
function elapsed(ms) {
|
|
380
|
+
const total = Math.max(0, Math.round(ms / 1000));
|
|
381
|
+
const minutes = Math.floor(total / 60);
|
|
382
|
+
return minutes > 0 ? `${minutes}m${`${total % 60}`.padStart(2, '0')}s` : `${total}s`;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Poll the view until it says `ready`, the hold is gone, or the timeout passes.
|
|
386
|
+
*
|
|
387
|
+
* It stops waiting the moment the machine is `ready`; otherwise it keeps waiting until
|
|
388
|
+
* the timeout, because the server — not this loop — decides that `unknown` or
|
|
389
|
+
* `back_held` is final. At least one read is made even with a zero timeout. A read
|
|
390
|
+
* that cannot be answered (the network, a 5xx, a 429) is retried until the timeout and
|
|
391
|
+
* is never taken for progress, or for readiness; a read the server REFUSES (a 404, a
|
|
392
|
+
* 403) ends the wait.
|
|
393
|
+
*
|
|
394
|
+
* Reaching the timeout does nothing to the hold. This function has no way to release
|
|
395
|
+
* one: it is given a `read` and nothing else.
|
|
396
|
+
*/
|
|
397
|
+
async function waitForReady(io, options) {
|
|
398
|
+
const start = io.now();
|
|
399
|
+
const deadline = start + Math.max(0, options.timeoutMs);
|
|
400
|
+
let polls = 0;
|
|
401
|
+
let view = null;
|
|
402
|
+
let error = null;
|
|
403
|
+
let lastShown = '';
|
|
404
|
+
for (;;) {
|
|
405
|
+
polls += 1;
|
|
406
|
+
const read = await io.read();
|
|
407
|
+
if (read.ok) {
|
|
408
|
+
view = read.value;
|
|
409
|
+
error = null;
|
|
410
|
+
const shown = stateLine(view);
|
|
411
|
+
if (shown !== lastShown || polls % PROGRESS_EVERY_POLLS === 0) {
|
|
412
|
+
io.progress(`waiting for ready: ${shown} (${elapsed(io.now() - start)})`);
|
|
413
|
+
lastShown = shown;
|
|
414
|
+
}
|
|
415
|
+
if ((0, data_provider_1.isMaintenanceReady)(view.journey)) {
|
|
416
|
+
return { outcome: 'ready', view, error: null, polls, waitedMs: io.now() - start };
|
|
417
|
+
}
|
|
418
|
+
if (!view.hold || view.hold.lifecycle !== 'held') {
|
|
419
|
+
return { outcome: 'released', view, error: null, polls, waitedMs: io.now() - start };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
else if (read.error.kind === 'refused') {
|
|
423
|
+
return { outcome: 'refused', view, error: read.error, polls, waitedMs: io.now() - start };
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
error = read.error;
|
|
427
|
+
io.progress(`could not read the state (${read.error.code}); trying again`);
|
|
428
|
+
lastShown = '';
|
|
429
|
+
}
|
|
430
|
+
const remaining = deadline - io.now();
|
|
431
|
+
if (remaining <= 0)
|
|
432
|
+
break;
|
|
433
|
+
await io.sleep(Math.min(options.intervalMs, remaining));
|
|
434
|
+
}
|
|
435
|
+
return { outcome: 'timeout', view, error, polls, waitedMs: io.now() - start };
|
|
436
|
+
}
|
|
437
|
+
function defaultIo(progress) {
|
|
438
|
+
return {
|
|
439
|
+
api: exports.maintenanceApi,
|
|
440
|
+
now: exports.maintenanceClock.now,
|
|
441
|
+
sleep: exports.maintenanceClock.sleep,
|
|
442
|
+
progress,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
function targetDocument(target) {
|
|
446
|
+
if (!target)
|
|
447
|
+
return {};
|
|
448
|
+
return {
|
|
449
|
+
machine: {
|
|
450
|
+
runtimeKey: target.runtimeKey,
|
|
451
|
+
...(target.machineId ? { machineId: target.machineId } : {}),
|
|
452
|
+
...(target.name ? { name: target.name } : {}),
|
|
453
|
+
...(target.profile ? { profile: target.profile } : {}),
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function viewFields(view) {
|
|
458
|
+
if (!view)
|
|
459
|
+
return {};
|
|
460
|
+
return {
|
|
461
|
+
phase: view.journey.phase,
|
|
462
|
+
word: data_provider_1.DAEMON_MAINTENANCE_WORDS[view.journey.phase] ?? view.journey.phase,
|
|
463
|
+
cause: view.journey.cause,
|
|
464
|
+
generation: view.hold?.generation ?? null,
|
|
465
|
+
view,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function candidateLine(candidate) {
|
|
469
|
+
if (typeof candidate === 'string')
|
|
470
|
+
return candidate;
|
|
471
|
+
const label = [candidate.name, candidate.profile ? `profile ${candidate.profile}` : '']
|
|
472
|
+
.filter(Boolean)
|
|
473
|
+
.join(', ');
|
|
474
|
+
return `${candidate.runtimeKey || candidate.machineId || '-'}${label ? ` (${label})` : ''}`;
|
|
475
|
+
}
|
|
476
|
+
/** A refusal or an unknown, as the one document and the lines a person reads. */
|
|
477
|
+
function failedRun(command, failure, context = {}) {
|
|
478
|
+
const unknown = failure.kind === 'unknown';
|
|
479
|
+
const exit = unknown
|
|
480
|
+
? data_provider_1.DAEMON_MAINTENANCE_EXIT_CODES.unknown
|
|
481
|
+
: data_provider_1.DAEMON_MAINTENANCE_EXIT_CODES.refused;
|
|
482
|
+
const stderr = [failure.message];
|
|
483
|
+
if (failure.currentGeneration !== undefined) {
|
|
484
|
+
stderr.push(`The current generation is ${failure.currentGeneration}.`);
|
|
485
|
+
}
|
|
486
|
+
if (failure.candidates?.length) {
|
|
487
|
+
stderr.push('Candidates:');
|
|
488
|
+
for (const candidate of failure.candidates)
|
|
489
|
+
stderr.push(` ${candidateLine(candidate)}`);
|
|
490
|
+
}
|
|
491
|
+
for (const line of context.next ?? [])
|
|
492
|
+
stderr.push(line);
|
|
493
|
+
return {
|
|
494
|
+
document: {
|
|
495
|
+
schema: exports.DAEMON_MAINTENANCE_CLI_SCHEMA,
|
|
496
|
+
command,
|
|
497
|
+
outcome: unknown ? 'unknown' : 'refused',
|
|
498
|
+
exit,
|
|
499
|
+
...targetDocument(context.target),
|
|
500
|
+
refusal: {
|
|
501
|
+
code: failure.code,
|
|
502
|
+
message: failure.message,
|
|
503
|
+
...(failure.status !== undefined ? { status: failure.status } : {}),
|
|
504
|
+
...(failure.currentGeneration !== undefined
|
|
505
|
+
? { currentGeneration: failure.currentGeneration }
|
|
506
|
+
: {}),
|
|
507
|
+
...(failure.candidates?.length ? { candidates: failure.candidates } : {}),
|
|
508
|
+
...(failure.retryAfterMs !== undefined ? { retryAfterMs: failure.retryAfterMs } : {}),
|
|
509
|
+
},
|
|
510
|
+
...viewFields(context.view),
|
|
511
|
+
...(context.extra ?? {}),
|
|
512
|
+
},
|
|
513
|
+
exit,
|
|
514
|
+
stdout: [],
|
|
515
|
+
stderr,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
const countOf = (value) => typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : null;
|
|
519
|
+
/**
|
|
520
|
+
* What became of the work a released hold had parked (OSK-19623): one summary line and, where
|
|
521
|
+
* something could not restart or ran out of time, one line per cause in the CLI's own words.
|
|
522
|
+
*
|
|
523
|
+
* A count that was never measured is "not counted", never zero, and a re-drive that has not
|
|
524
|
+
* finished says so: the numbers are what it has done so far. The causes are closed classes
|
|
525
|
+
* (`DAEMON_MAINTENANCE_RESTART_REFUSAL_WORDS`), so nothing here prints a run id or a reason
|
|
526
|
+
* the server wrote.
|
|
527
|
+
*/
|
|
528
|
+
function outcomeLines(outcome) {
|
|
529
|
+
if (!outcome)
|
|
530
|
+
return [];
|
|
531
|
+
const parked = countOf(outcome.parkedRuns);
|
|
532
|
+
const final = outcome.counts === 'final';
|
|
533
|
+
if (parked === null) {
|
|
534
|
+
return [` restart ${final ? 'not counted' : 'not counted yet'}`];
|
|
535
|
+
}
|
|
536
|
+
if (parked === 0)
|
|
537
|
+
return [' restart nothing was parked'];
|
|
538
|
+
const parts = [];
|
|
539
|
+
const add = (count, text) => {
|
|
540
|
+
const n = countOf(count);
|
|
541
|
+
if (n !== null && n > 0)
|
|
542
|
+
parts.push(`${n} ${text}`);
|
|
543
|
+
};
|
|
544
|
+
add(outcome.redrivenRuns, 'restarted');
|
|
545
|
+
add(outcome.supersededRuns, 'replaced by a newer scheduled run');
|
|
546
|
+
add(outcome.expiredRuns, 'ran out of time');
|
|
547
|
+
add(outcome.refusedRuns, 'could not restart');
|
|
548
|
+
if (final)
|
|
549
|
+
add(outcome.stillParkedRuns, 'still waiting for the machine');
|
|
550
|
+
add(final ? outcome.leftElsewhereRuns : null, 'started or ended without this restart');
|
|
551
|
+
const lines = [
|
|
552
|
+
` restart ${parts.length ? parts.join(', ') : 'none restarted yet'} (of ${parked} parked)` +
|
|
553
|
+
`${final ? '' : ', still being restarted'}`,
|
|
554
|
+
];
|
|
555
|
+
for (const cls of data_provider_1.DAEMON_MAINTENANCE_RESTART_REFUSALS) {
|
|
556
|
+
const n = countOf(outcome.refusedByReason?.[cls]);
|
|
557
|
+
if (n)
|
|
558
|
+
lines.push(` ${n} could not restart: ${data_provider_1.DAEMON_MAINTENANCE_RESTART_REFUSAL_WORDS[cls]}`);
|
|
559
|
+
}
|
|
560
|
+
for (const cls of data_provider_1.DAEMON_MAINTENANCE_EXPIRY_CLASSES) {
|
|
561
|
+
const n = countOf(outcome.expiredByReason?.[cls]);
|
|
562
|
+
if (n)
|
|
563
|
+
lines.push(` ${n} ran out of time: ${data_provider_1.DAEMON_MAINTENANCE_EXPIRY_WORDS[cls]}`);
|
|
564
|
+
}
|
|
565
|
+
return lines;
|
|
566
|
+
}
|
|
567
|
+
/** The lines `status` and the end of `hold` print for one machine. */
|
|
568
|
+
function describeView(view, options = {}) {
|
|
569
|
+
const j = view.journey;
|
|
570
|
+
const lines = [];
|
|
571
|
+
const name = view.machine?.name || view.runtimeKey;
|
|
572
|
+
lines.push(`${name}${view.machine?.name ? ` ${view.runtimeKey}` : ''}`);
|
|
573
|
+
lines.push(` state ${stateLine(view)}`);
|
|
574
|
+
if (view.hold) {
|
|
575
|
+
const h = view.hold;
|
|
576
|
+
const who = h.heldBy ? `${h.heldBy.kind}${h.via ? ` via ${h.via}` : ''}` : '';
|
|
577
|
+
lines.push(` hold generation ${h.generation} ${h.lifecycle}` +
|
|
578
|
+
`${h.requestedAt ? `, since ${(0, format_1.localStamp)(h.requestedAt)} ${(0, format_1.localZone)()}` : ''}${who ? `, by ${who}` : ''}`);
|
|
579
|
+
lines.push(` applied ${h.fence.state === 'confirmed' ? 'on every daemon row' : 'still being applied'} ` +
|
|
580
|
+
`(${h.fence.rowsConfirmed}/${h.fence.rowsTotal})`);
|
|
581
|
+
if (h.reason)
|
|
582
|
+
lines.push(` reason ${h.reason}`);
|
|
583
|
+
if (h.autoResume)
|
|
584
|
+
lines.push(' auto-resume yes, when the machine comes back on a new process');
|
|
585
|
+
if (h.lifecycle === 'released')
|
|
586
|
+
lines.push(...outcomeLines(h.outcome));
|
|
587
|
+
}
|
|
588
|
+
if (view.parked && (view.parked.total > 0 || view.parked.unreadable)) {
|
|
589
|
+
const kinds = Object.entries(view.parked.byKind)
|
|
590
|
+
.map(([kind, count]) => `${kind} ${count}`)
|
|
591
|
+
.join(', ');
|
|
592
|
+
lines.push(view.parked.unreadable
|
|
593
|
+
? ' parked could not be read'
|
|
594
|
+
: ` parked ${view.parked.total}${kinds ? ` (${kinds})` : ''}`);
|
|
595
|
+
}
|
|
596
|
+
if (j.evidenceAt)
|
|
597
|
+
lines.push(` evidence read ${(0, format_1.localStamp)(j.evidenceAt)} ${(0, format_1.localZone)()}`);
|
|
598
|
+
lines.push(` controls ${j.controls.length ? j.controls.join(', ') : 'none'}`);
|
|
599
|
+
if (view.readOnly)
|
|
600
|
+
lines.push(' access read-only (a machine shared with you)');
|
|
601
|
+
if (options.explain)
|
|
602
|
+
lines.push(...explainLines(view));
|
|
603
|
+
return lines;
|
|
604
|
+
}
|
|
605
|
+
function fenceWord(confirmed) {
|
|
606
|
+
if (confirmed === null)
|
|
607
|
+
return 'unknown';
|
|
608
|
+
return confirmed ? 'confirmed' : 'not confirmed';
|
|
609
|
+
}
|
|
610
|
+
/** `--explain`: each source the reading used, in the order it was read, and what each said. */
|
|
611
|
+
function explainLines(view) {
|
|
612
|
+
const reading = view.reading;
|
|
613
|
+
if (!reading) {
|
|
614
|
+
return [' reading none (nothing was read, so nothing is claimed)'];
|
|
615
|
+
}
|
|
616
|
+
const lines = [
|
|
617
|
+
` reading ${reading.state}${reading.cause ? ` (${reading.cause})` : ''}, read ${(0, format_1.localStamp)(reading.readAt)} ${(0, format_1.localZone)()}`,
|
|
618
|
+
` fence ${fenceWord(reading.fence.confirmed)} ` +
|
|
619
|
+
`(${reading.fence.rowsConfirmed}/${reading.fence.rowsTotal})`,
|
|
620
|
+
' sources in the order they were read:',
|
|
621
|
+
];
|
|
622
|
+
for (const step of reading.order)
|
|
623
|
+
lines.push(` ${(0, format_1.localStamp)(step.at)} ${step.step}`);
|
|
624
|
+
for (const source of reading.sources) {
|
|
625
|
+
lines.push(` ${source.source}: ${source.read}${source.cause ? ` (${source.cause})` : ''}, ` +
|
|
626
|
+
`${source.total} found${source.truncated ? ' (list truncated)' : ''}`);
|
|
627
|
+
for (const holder of source.holders) {
|
|
628
|
+
lines.push(` - ${holder.label || holder.ref || holder.kind} (${holder.class})`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return lines;
|
|
632
|
+
}
|
|
633
|
+
// ── commands ───────────────────────────────────────────────────────────────
|
|
634
|
+
async function loadLists(io, needs) {
|
|
635
|
+
const [machines, holds] = await Promise.all([
|
|
636
|
+
needs.machines ? attempt(() => io.api.listMachines()) : Promise.resolve(null),
|
|
637
|
+
needs.holds ? attempt(() => io.api.list()) : Promise.resolve(null),
|
|
638
|
+
]);
|
|
639
|
+
if (machines && !machines.ok)
|
|
640
|
+
return { ok: false, error: machines.error };
|
|
641
|
+
if (holds && !holds.ok)
|
|
642
|
+
return { ok: false, error: holds.error };
|
|
643
|
+
return {
|
|
644
|
+
ok: true,
|
|
645
|
+
machines: machines?.ok ? machines.value : [],
|
|
646
|
+
holds: holds?.ok ? (holds.value?.holds ?? []) : [],
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
async function resolveTarget(io, ref, scope, needsHolds) {
|
|
650
|
+
if (ref && looksLikeRuntimeKey(ref.trim()))
|
|
651
|
+
return { ok: true, target: { runtimeKey: ref.trim() } };
|
|
652
|
+
const lists = await loadLists(io, { machines: true, holds: needsHolds });
|
|
653
|
+
if (!lists.ok)
|
|
654
|
+
return { ok: false, failure: lists.error };
|
|
655
|
+
const resolved = resolveMaintenanceTarget({
|
|
656
|
+
ref,
|
|
657
|
+
machines: lists.machines,
|
|
658
|
+
holds: lists.holds,
|
|
659
|
+
scope,
|
|
660
|
+
});
|
|
661
|
+
return resolved.ok ? resolved : { ok: false, failure: resolved.refusal };
|
|
662
|
+
}
|
|
663
|
+
/** `status`: every hold the owner has, or one machine's state. */
|
|
664
|
+
async function runStatus(input, io) {
|
|
665
|
+
if (!input.ref?.trim()) {
|
|
666
|
+
const listed = await attempt(() => io.api.list());
|
|
667
|
+
if (!listed.ok)
|
|
668
|
+
return failedRun('status', listed.error);
|
|
669
|
+
const holds = listed.value?.holds ?? [];
|
|
670
|
+
const stdout = [];
|
|
671
|
+
if (holds.length === 0)
|
|
672
|
+
stdout.push('No machine is held.');
|
|
673
|
+
for (const view of holds) {
|
|
674
|
+
stdout.push(...describeView(view, { explain: input.explain }));
|
|
675
|
+
stdout.push('');
|
|
676
|
+
}
|
|
677
|
+
return {
|
|
678
|
+
document: {
|
|
679
|
+
schema: exports.DAEMON_MAINTENANCE_CLI_SCHEMA,
|
|
680
|
+
command: 'status',
|
|
681
|
+
outcome: 'list',
|
|
682
|
+
exit: data_provider_1.DAEMON_MAINTENANCE_EXIT_CODES.ready,
|
|
683
|
+
count: holds.length,
|
|
684
|
+
holds,
|
|
685
|
+
},
|
|
686
|
+
exit: data_provider_1.DAEMON_MAINTENANCE_EXIT_CODES.ready,
|
|
687
|
+
stdout: stdout[stdout.length - 1] === '' ? stdout.slice(0, -1) : stdout,
|
|
688
|
+
stderr: [],
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
const resolved = await resolveTarget(io, input.ref, 'any', true);
|
|
692
|
+
if (!resolved.ok)
|
|
693
|
+
return failedRun('status', resolved.failure);
|
|
694
|
+
const got = await attempt(() => io.api.get(resolved.target.runtimeKey));
|
|
695
|
+
if (!got.ok)
|
|
696
|
+
return failedRun('status', got.error, { target: resolved.target });
|
|
697
|
+
return {
|
|
698
|
+
document: {
|
|
699
|
+
schema: exports.DAEMON_MAINTENANCE_CLI_SCHEMA,
|
|
700
|
+
command: 'status',
|
|
701
|
+
outcome: 'ok',
|
|
702
|
+
exit: data_provider_1.DAEMON_MAINTENANCE_EXIT_CODES.ready,
|
|
703
|
+
...targetDocument(resolved.target),
|
|
704
|
+
...viewFields(got.value),
|
|
705
|
+
},
|
|
706
|
+
exit: data_provider_1.DAEMON_MAINTENANCE_EXIT_CODES.ready,
|
|
707
|
+
stdout: describeView(got.value, { explain: input.explain }),
|
|
708
|
+
stderr: [],
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
function normalizeSpaces(text) {
|
|
712
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* `hold`: take the hold, and optionally wait for the machine to be ready.
|
|
716
|
+
*
|
|
717
|
+
* A hold already in force is not a refusal here: asking to hold a machine you have
|
|
718
|
+
* already held is the retry after a lost response, or the re-run after a Ctrl-C
|
|
719
|
+
* during `--wait`. The live hold is reported (`created: false`), its reason is kept
|
|
720
|
+
* and a different `--reason` is said to have been ignored, and `--wait` then waits
|
|
721
|
+
* on it.
|
|
722
|
+
*/
|
|
723
|
+
async function runHold(input, io) {
|
|
724
|
+
const reason = input.reason === undefined ? undefined : normalizeSpaces(input.reason);
|
|
725
|
+
if (reason !== undefined && reason.length > data_provider_1.DAEMON_MAINTENANCE_REASON_MAX_LENGTH) {
|
|
726
|
+
return failedRun('hold', {
|
|
727
|
+
kind: 'refused',
|
|
728
|
+
code: 'invalid_request',
|
|
729
|
+
message: `--reason is ${reason.length} characters; the most it can be is ${data_provider_1.DAEMON_MAINTENANCE_REASON_MAX_LENGTH}.`,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
const resolved = await resolveTarget(io, input.ref, 'any', false);
|
|
733
|
+
if (!resolved.ok)
|
|
734
|
+
return failedRun('hold', resolved.failure);
|
|
735
|
+
const target = resolved.target;
|
|
736
|
+
const body = {
|
|
737
|
+
lifecycle: 'held',
|
|
738
|
+
...(reason ? { reason } : {}),
|
|
739
|
+
...(input.autoResume ? { autoResume: true } : {}),
|
|
740
|
+
};
|
|
741
|
+
const put = await attempt(() => io.api.change(target.runtimeKey, body));
|
|
742
|
+
let view;
|
|
743
|
+
let created = true;
|
|
744
|
+
let reasonIgnored = false;
|
|
745
|
+
if (put.ok) {
|
|
746
|
+
view = put.value;
|
|
747
|
+
}
|
|
748
|
+
else if (put.error.code === 'already_held') {
|
|
749
|
+
const got = await attempt(() => io.api.get(target.runtimeKey));
|
|
750
|
+
if (!got.ok)
|
|
751
|
+
return failedRun('hold', got.error, { target });
|
|
752
|
+
view = got.value;
|
|
753
|
+
created = false;
|
|
754
|
+
reasonIgnored = Boolean(reason) && normalizeSpaces(view.hold?.reason ?? '') !== reason;
|
|
755
|
+
io.progress(`Already held (generation ${view.hold?.generation ?? put.error.currentGeneration}); ` +
|
|
756
|
+
'the existing hold is kept.');
|
|
757
|
+
if (reasonIgnored)
|
|
758
|
+
io.progress('The reason you gave was not applied: the existing hold keeps its own.');
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
return failedRun('hold', put.error, { target });
|
|
762
|
+
}
|
|
763
|
+
if (!view.hold || view.hold.lifecycle !== 'held') {
|
|
764
|
+
return failedRun('hold', {
|
|
765
|
+
kind: 'unknown',
|
|
766
|
+
code: 'unexpected_answer',
|
|
767
|
+
message: 'The server answered without a hold in force, so the machine state is not known.',
|
|
768
|
+
}, { target, view });
|
|
769
|
+
}
|
|
770
|
+
const common = {
|
|
771
|
+
schema: exports.DAEMON_MAINTENANCE_CLI_SCHEMA,
|
|
772
|
+
command: 'hold',
|
|
773
|
+
...targetDocument(target),
|
|
774
|
+
created,
|
|
775
|
+
...(reasonIgnored ? { reasonIgnored: true } : {}),
|
|
776
|
+
holdInForce: true,
|
|
777
|
+
};
|
|
778
|
+
if (!input.wait) {
|
|
779
|
+
return {
|
|
780
|
+
document: {
|
|
781
|
+
...common,
|
|
782
|
+
outcome: 'held',
|
|
783
|
+
exit: data_provider_1.DAEMON_MAINTENANCE_EXIT_CODES.ready,
|
|
784
|
+
wait: { requested: false },
|
|
785
|
+
...viewFields(view),
|
|
786
|
+
note: 'The hold is in force. It does not mean the machine is ready: run `skrr daemon maintenance status` or `hold --wait ready`.',
|
|
787
|
+
},
|
|
788
|
+
// 0 here means the hold was taken, NOT that the machine is ready.
|
|
789
|
+
exit: data_provider_1.DAEMON_MAINTENANCE_EXIT_CODES.ready,
|
|
790
|
+
stdout: [
|
|
791
|
+
`${created ? 'Hold taken' : 'Already held'}: generation ${view.hold.generation}.`,
|
|
792
|
+
...describeView(view, { explain: input.explain }),
|
|
793
|
+
],
|
|
794
|
+
stderr: [],
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
const timeoutMs = input.wait.timeoutMs;
|
|
798
|
+
const waited = await waitForReady({
|
|
799
|
+
read: () => attempt(() => io.api.get(target.runtimeKey)),
|
|
800
|
+
now: io.now,
|
|
801
|
+
sleep: io.sleep,
|
|
802
|
+
progress: io.progress,
|
|
803
|
+
}, { timeoutMs, intervalMs: input.wait.intervalMs ?? exports.MAINTENANCE_POLL_INTERVAL_MS });
|
|
804
|
+
const last = waited.view ?? view;
|
|
805
|
+
const wait = {
|
|
806
|
+
requested: true,
|
|
807
|
+
until: 'ready',
|
|
808
|
+
timeoutMs,
|
|
809
|
+
waitedMs: waited.waitedMs,
|
|
810
|
+
polls: waited.polls,
|
|
811
|
+
timedOut: waited.outcome === 'timeout',
|
|
812
|
+
};
|
|
813
|
+
if (waited.outcome === 'ready') {
|
|
814
|
+
return {
|
|
815
|
+
document: {
|
|
816
|
+
...common,
|
|
817
|
+
outcome: 'ready',
|
|
818
|
+
exit: data_provider_1.DAEMON_MAINTENANCE_EXIT_CODES.ready,
|
|
819
|
+
wait,
|
|
820
|
+
...viewFields(last),
|
|
821
|
+
},
|
|
822
|
+
exit: data_provider_1.DAEMON_MAINTENANCE_EXIT_CODES.ready,
|
|
823
|
+
stdout: [
|
|
824
|
+
`Ready: generation ${last.hold?.generation}.`,
|
|
825
|
+
...(last.journey.modifiers.includes('attested')
|
|
826
|
+
? ["This is the owner's attestation, not evidence from the machine."]
|
|
827
|
+
: []),
|
|
828
|
+
...describeView(last, { explain: input.explain }),
|
|
829
|
+
],
|
|
830
|
+
stderr: [],
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
if (waited.outcome === 'released') {
|
|
834
|
+
return {
|
|
835
|
+
document: {
|
|
836
|
+
...common,
|
|
837
|
+
holdInForce: false,
|
|
838
|
+
outcome: 'released_elsewhere',
|
|
839
|
+
exit: data_provider_1.DAEMON_MAINTENANCE_EXIT_CODES.refused,
|
|
840
|
+
wait,
|
|
841
|
+
...viewFields(last),
|
|
842
|
+
note: 'The hold was released by someone else while waiting, so there is nothing left to wait for.',
|
|
843
|
+
},
|
|
844
|
+
exit: data_provider_1.DAEMON_MAINTENANCE_EXIT_CODES.refused,
|
|
845
|
+
stdout: [],
|
|
846
|
+
stderr: ['The hold was released while waiting. The machine is no longer held.'],
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
if (waited.outcome === 'refused' && waited.error) {
|
|
850
|
+
return failedRun('hold', waited.error, {
|
|
851
|
+
target,
|
|
852
|
+
view: last,
|
|
853
|
+
extra: { holdInForce: true, wait },
|
|
854
|
+
next: [
|
|
855
|
+
`The hold taken above is still in force. \`skrr daemon maintenance resume ${target.runtimeKey}\` releases it.`,
|
|
856
|
+
],
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
// The timeout. What the LAST read saw decides 2 (still moving) or 3 (unknown),
|
|
860
|
+
// exactly `maintenanceExitFor`. If that read FAILED, the state at the deadline is
|
|
861
|
+
// not known, whatever an earlier read said: an old `draining` is not evidence about
|
|
862
|
+
// now, so it is neither shown nor used for the exit.
|
|
863
|
+
const known = waited.view && !waited.error ? waited.view : null;
|
|
864
|
+
const exit = known
|
|
865
|
+
? data_provider_1.DAEMON_MAINTENANCE_EXIT_CODES[(0, data_provider_1.maintenanceExitFor)(known.journey)]
|
|
866
|
+
: data_provider_1.DAEMON_MAINTENANCE_EXIT_CODES.unknown;
|
|
867
|
+
const note = 'The timeout did not release the hold: it is still in force. ' +
|
|
868
|
+
`Run \`skrr daemon maintenance resume ${target.runtimeKey}\` to release it.`;
|
|
869
|
+
return {
|
|
870
|
+
document: {
|
|
871
|
+
...common,
|
|
872
|
+
outcome: 'timeout',
|
|
873
|
+
exit,
|
|
874
|
+
wait,
|
|
875
|
+
...viewFields(known),
|
|
876
|
+
...(waited.error
|
|
877
|
+
? { lastReadError: { code: waited.error.code, message: waited.error.message } }
|
|
878
|
+
: {}),
|
|
879
|
+
note,
|
|
880
|
+
},
|
|
881
|
+
exit,
|
|
882
|
+
stdout: known ? describeView(known, { explain: input.explain }) : [],
|
|
883
|
+
stderr: [
|
|
884
|
+
`Not ready after ${elapsed(waited.waitedMs)}: ${known ? stateLine(known) : 'the last read of the state failed, so it is not known'}.`,
|
|
885
|
+
note,
|
|
886
|
+
],
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
/** `resume`: release the hold. A compare-and-set on the generation. */
|
|
890
|
+
async function runResume(input, io) {
|
|
891
|
+
const resolved = await resolveTarget(io, input.ref, 'held', true);
|
|
892
|
+
if (!resolved.ok)
|
|
893
|
+
return failedRun('resume', resolved.failure);
|
|
894
|
+
const target = resolved.target;
|
|
895
|
+
let generation = input.generation;
|
|
896
|
+
if (generation === undefined) {
|
|
897
|
+
const got = await attempt(() => io.api.get(target.runtimeKey));
|
|
898
|
+
if (!got.ok)
|
|
899
|
+
return failedRun('resume', got.error, { target });
|
|
900
|
+
if (got.value.hold?.lifecycle !== 'held') {
|
|
901
|
+
return failedRun('resume', refusal('not_held'), {
|
|
902
|
+
target,
|
|
903
|
+
view: got.value,
|
|
904
|
+
extra: got.value.hold ? { latestGeneration: got.value.hold.generation } : {},
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
generation = got.value.hold.generation;
|
|
908
|
+
}
|
|
909
|
+
const put = await attempt(() => io.api.change(target.runtimeKey, { lifecycle: 'released', expectedGeneration: generation }));
|
|
910
|
+
if (!put.ok)
|
|
911
|
+
return failedRun('resume', put.error, { target });
|
|
912
|
+
const view = put.value;
|
|
913
|
+
// The server answers with the machine's LATEST hold. Releasing a generation that was
|
|
914
|
+
// already released while a newer one is live leaves the machine held, and "resumed"
|
|
915
|
+
// would be the one claim that is false.
|
|
916
|
+
if (view.hold?.lifecycle === 'held') {
|
|
917
|
+
return failedRun('resume', {
|
|
918
|
+
kind: 'refused',
|
|
919
|
+
...refusal('generation_conflict', {
|
|
920
|
+
currentGeneration: view.hold.generation,
|
|
921
|
+
message: `Generation ${generation} was already released, but the machine is held by generation ${view.hold.generation}.`,
|
|
922
|
+
}),
|
|
923
|
+
}, { target, view, extra: { holdInForce: true } });
|
|
924
|
+
}
|
|
925
|
+
return {
|
|
926
|
+
document: {
|
|
927
|
+
schema: exports.DAEMON_MAINTENANCE_CLI_SCHEMA,
|
|
928
|
+
command: 'resume',
|
|
929
|
+
outcome: 'released',
|
|
930
|
+
exit: data_provider_1.DAEMON_MAINTENANCE_EXIT_CODES.ready,
|
|
931
|
+
...targetDocument(target),
|
|
932
|
+
releasedGeneration: generation,
|
|
933
|
+
holdInForce: false,
|
|
934
|
+
...viewFields(view),
|
|
935
|
+
},
|
|
936
|
+
exit: data_provider_1.DAEMON_MAINTENANCE_EXIT_CODES.ready,
|
|
937
|
+
stdout: [
|
|
938
|
+
`Released: generation ${generation}. New work can start again.`,
|
|
939
|
+
...outcomeLines(view.hold?.outcome),
|
|
940
|
+
],
|
|
941
|
+
stderr: [],
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
/** The labels of the work the reading shows, for a `proceed_anyway` the owner confirmed. */
|
|
945
|
+
function holdersShown(view) {
|
|
946
|
+
const labels = [];
|
|
947
|
+
for (const source of view.reading?.sources ?? []) {
|
|
948
|
+
for (const holder of source.holders) {
|
|
949
|
+
if (holder.class !== 'active' && holder.class !== 'unproven')
|
|
950
|
+
continue;
|
|
951
|
+
const label = (holder.label || holder.ref || holder.kind || '').trim();
|
|
952
|
+
if (label)
|
|
953
|
+
labels.push(label.slice(0, MAX_ACKNOWLEDGED_LABEL));
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return [...new Set(labels)].slice(0, MAX_ACKNOWLEDGED);
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* `attest`: the owner says the machine is ready (`--ready`, only where the server
|
|
960
|
+
* allows it) or proceeds anyway (`--proceed`, naming what was running). The server
|
|
961
|
+
* decides whether an attestation applies; the CLI never decides it from a phase.
|
|
962
|
+
*/
|
|
963
|
+
async function runAttest(input, io) {
|
|
964
|
+
const resolved = await resolveTarget(io, input.ref, 'held', true);
|
|
965
|
+
if (!resolved.ok)
|
|
966
|
+
return failedRun('attest', resolved.failure);
|
|
967
|
+
const target = resolved.target;
|
|
968
|
+
const got = await attempt(() => io.api.get(target.runtimeKey));
|
|
969
|
+
if (!got.ok)
|
|
970
|
+
return failedRun('attest', got.error, { target });
|
|
971
|
+
if (got.value.hold?.lifecycle !== 'held') {
|
|
972
|
+
return failedRun('attest', refusal('not_held'), { target, view: got.value });
|
|
973
|
+
}
|
|
974
|
+
const generation = input.generation ?? got.value.hold.generation;
|
|
975
|
+
const acknowledged = input.kind === 'proceed_anyway' ? holdersShown(got.value) : [];
|
|
976
|
+
if (acknowledged.length > 0) {
|
|
977
|
+
io.progress(`Proceeding anyway while this is running: ${acknowledged.join('; ')}`);
|
|
978
|
+
}
|
|
979
|
+
const post = await attempt(() => io.api.attest(target.runtimeKey, {
|
|
980
|
+
expectedGeneration: generation,
|
|
981
|
+
kind: input.kind,
|
|
982
|
+
...(input.kind === 'proceed_anyway' ? { acknowledged } : {}),
|
|
983
|
+
}));
|
|
984
|
+
if (!post.ok)
|
|
985
|
+
return failedRun('attest', post.error, { target, view: got.value });
|
|
986
|
+
const view = post.value;
|
|
987
|
+
return {
|
|
988
|
+
document: {
|
|
989
|
+
schema: exports.DAEMON_MAINTENANCE_CLI_SCHEMA,
|
|
990
|
+
command: 'attest',
|
|
991
|
+
outcome: 'attested',
|
|
992
|
+
exit: data_provider_1.DAEMON_MAINTENANCE_EXIT_CODES.ready,
|
|
993
|
+
...targetDocument(target),
|
|
994
|
+
kind: input.kind,
|
|
995
|
+
attestedGeneration: generation,
|
|
996
|
+
...(input.kind === 'proceed_anyway' ? { acknowledged } : {}),
|
|
997
|
+
holdInForce: view.hold?.lifecycle === 'held',
|
|
998
|
+
...viewFields(view),
|
|
999
|
+
},
|
|
1000
|
+
exit: data_provider_1.DAEMON_MAINTENANCE_EXIT_CODES.ready,
|
|
1001
|
+
stdout: [
|
|
1002
|
+
input.kind === 'attest_ready'
|
|
1003
|
+
? `Attested: generation ${generation}. This records your word that the machine is ready; it is not evidence from the machine.`
|
|
1004
|
+
: `Recorded: you are proceeding anyway at generation ${generation}${acknowledged.length ? `, with ${acknowledged.length} thing(s) running` : ''}.`,
|
|
1005
|
+
'The hold stays in force until you run `skrr daemon maintenance resume`.',
|
|
1006
|
+
],
|
|
1007
|
+
stderr: [],
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
/** `--timeout`, parsed and bounded. */
|
|
1011
|
+
function parseTimeout(value) {
|
|
1012
|
+
if (value === undefined)
|
|
1013
|
+
return { ok: true, ms: exports.MAINTENANCE_DEFAULT_TIMEOUT_MS };
|
|
1014
|
+
if (!value.trim()) {
|
|
1015
|
+
return { ok: false, message: '--timeout needs a duration, such as 90s, 30m or 2h.' };
|
|
1016
|
+
}
|
|
1017
|
+
let ms;
|
|
1018
|
+
try {
|
|
1019
|
+
ms = (0, hosted_machines_1.parseDurationMs)(value, '--timeout');
|
|
1020
|
+
}
|
|
1021
|
+
catch {
|
|
1022
|
+
return {
|
|
1023
|
+
ok: false,
|
|
1024
|
+
message: `--timeout "${value}" is not a duration. Use a number with a unit, such as 90s, 30m or 2h.`,
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
if (ms === undefined || ms > exports.MAINTENANCE_MAX_TIMEOUT_MS) {
|
|
1028
|
+
return { ok: false, message: '--timeout can be at most 24h.' };
|
|
1029
|
+
}
|
|
1030
|
+
return { ok: true, ms };
|
|
1031
|
+
}
|
|
1032
|
+
/** A failed flag, as a refusal the command prints the way every other refusal is printed. */
|
|
1033
|
+
function invalidFlagRun(command, message) {
|
|
1034
|
+
return failedRun(command, { kind: 'refused', code: 'invalid_request', message });
|
|
1035
|
+
}
|