@north-light/crouter 0.3.281 → 0.3.283
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/api/client.d.ts +19 -3
- package/dist/api/client.js +34 -0
- package/dist/api/dto/broker-ops.d.ts +11 -0
- package/dist/api/dto/broker.d.ts +3 -8
- package/dist/api/dto/common.d.ts +1 -1
- package/dist/api/dto/messages.d.ts +2 -2
- package/dist/api/dto/node-outcomes.d.ts +80 -0
- package/dist/api/dto/node-outcomes.js +2 -0
- package/dist/api/dto/nodes.d.ts +9 -0
- package/dist/api/dto/profiles.d.ts +5 -0
- package/dist/api/dto/reports.d.ts +45 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/plugin-manifest-schema.d.ts +208 -0
- package/dist/api/plugin-manifest-schema.js +23 -0
- package/dist/api/routes.d.ts +7 -0
- package/dist/api/routes.js +7 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +24 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +31 -4
- package/dist/cli.js +8 -2
- package/dist/clients/attach/overlays/graph.d.ts +5 -0
- package/dist/clients/attach/overlays/graph.js +65 -47
- package/dist/clients/attach/overlays/pickers.js +8 -9
- package/dist/clients/attach/render/card-presentation.js +1 -1
- package/dist/clients/attach/viewer.js +1286 -1260
- package/dist/commands/api-client.js +22 -23
- package/dist/commands/node/create.js +38 -3
- package/dist/commands/node/inspect.js +3 -7
- package/dist/commands/node/message.js +3 -3
- package/dist/commands/node/outcome.d.ts +2 -0
- package/dist/commands/node/outcome.js +134 -0
- package/dist/commands/node.js +2 -1
- package/dist/commands/pkg/browse/catalog.js +1 -1
- package/dist/commands/pkg/plugin-manage.d.ts +46 -1
- package/dist/commands/pkg/plugin-manage.js +170 -20
- package/dist/commands/profile/pause.js +17 -14
- package/dist/commands/push.js +102 -1
- package/dist/commands/sys/__tests__/setup-core.test.js +4 -1
- package/dist/commands/sys/panels/broker-limits-panel.js +7 -1
- package/dist/commands/sys/panels/models-panel.js +2 -2
- package/dist/commands/sys/settings-shell.d.ts +1 -1
- package/dist/commands/sys/settings-shell.js +6 -0
- package/dist/commands/sys/settings.js +3 -3
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +2 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +42 -4
- package/dist/core/__tests__/helpers/harness.d.ts +1 -0
- package/dist/core/__tests__/helpers/harness.js +9 -0
- package/dist/core/__tests__/integration/plugin-revalidate.test.d.ts +1 -0
- package/dist/core/__tests__/integration/plugin-revalidate.test.js +355 -0
- package/dist/core/__tests__/integration/profile-pause.test.d.ts +1 -0
- package/dist/core/__tests__/integration/profile-pause.test.js +115 -0
- package/dist/core/__tests__/node-outcome.test.d.ts +1 -0
- package/dist/core/__tests__/node-outcome.test.js +184 -0
- package/dist/core/__tests__/revive-parked-fresh.test.js +13 -20
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +6 -2
- package/dist/core/__tests__/seam/dormancy-release.test.js +3 -3
- package/dist/core/__tests__/seam/node-outcome-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/seam/node-outcome-delivery.test.js +153 -0
- package/dist/core/bash-jobs.d.ts +6 -0
- package/dist/core/bash-jobs.js +22 -0
- package/dist/core/canvas/canvas.d.ts +41 -3
- package/dist/core/canvas/canvas.js +153 -39
- package/dist/core/canvas/crons.js +2 -1
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/migrations.js +47 -0
- package/dist/core/canvas/node-outcome-deliveries.d.ts +80 -0
- package/dist/core/canvas/node-outcome-deliveries.js +163 -0
- package/dist/core/canvas/pid.d.ts +7 -0
- package/dist/core/canvas/pid.js +17 -0
- package/dist/core/canvas/render-source.js +2 -2
- package/dist/core/canvas/types.d.ts +42 -1
- package/dist/core/canvas/types.js +2 -1
- package/dist/core/command-manifests/registry.d.ts +3 -1
- package/dist/core/command-manifests/schema.d.ts +2 -75
- package/dist/core/command-manifests/schema.js +5 -0
- package/dist/core/command-plugins/compose.js +1 -1
- package/dist/core/command-plugins/revalidate.d.ts +18 -0
- package/dist/core/command-plugins/revalidate.js +152 -0
- package/dist/core/command-plugins/transport/http-fetch.d.ts +29 -4
- package/dist/core/command-plugins/transport/http-fetch.js +18 -8
- package/dist/core/command-plugins/transport/http-invoke.js +7 -0
- package/dist/core/command.d.ts +8 -0
- package/dist/core/command.js +19 -3
- package/dist/core/config.d.ts +4 -1
- package/dist/core/config.js +25 -1
- package/dist/core/exclusive-lock.d.ts +12 -0
- package/dist/core/exclusive-lock.js +29 -0
- package/dist/core/feed/feed.d.ts +8 -0
- package/dist/core/feed/feed.js +25 -4
- package/dist/core/feed/inbox.d.ts +5 -0
- package/dist/core/feed/inbox.js +13 -0
- package/dist/core/installed-plugins.js +31 -0
- package/dist/core/keybindings/catalog.d.ts +1 -1
- package/dist/core/keybindings/catalog.js +3 -0
- package/dist/core/manifest-recovery.d.ts +15 -0
- package/dist/core/manifest-recovery.js +72 -0
- package/dist/core/manifest-stale.d.ts +18 -0
- package/dist/core/manifest-stale.js +39 -0
- package/dist/core/plugin-swap-lock.d.ts +9 -0
- package/dist/core/plugin-swap-lock.js +31 -0
- package/dist/core/profiles/manifest.d.ts +3 -1
- package/dist/core/profiles/manifest.js +6 -1
- package/dist/core/runtime/broker/daemon-ops.d.ts +3 -1
- package/dist/core/runtime/broker/daemon-ops.js +6 -0
- package/dist/core/runtime/broker/engine-drive.d.ts +2 -3
- package/dist/core/runtime/broker/engine-drive.js +74 -27
- package/dist/core/runtime/broker/frame-dispatch.js +1 -0
- package/dist/core/runtime/broker/read-ops.js +14 -0
- package/dist/core/runtime/broker/rebind.js +0 -7
- package/dist/core/runtime/broker-protocol.d.ts +1 -6
- package/dist/core/runtime/fleet.d.ts +7 -0
- package/dist/core/runtime/headless-pi.d.ts +13 -0
- package/dist/core/runtime/headless-pi.js +10 -2
- package/dist/core/runtime/lifecycle.d.ts +6 -1
- package/dist/core/runtime/lifecycle.js +50 -1
- package/dist/core/runtime/nodes.d.ts +5 -1
- package/dist/core/runtime/nodes.js +2 -1
- package/dist/core/runtime/outcome-document.d.ts +70 -0
- package/dist/core/runtime/outcome-document.js +97 -0
- package/dist/core/runtime/revive.js +16 -21
- package/dist/core/runtime/spawn.d.ts +5 -1
- package/dist/core/runtime/spawn.js +18 -7
- package/dist/core/runtime/stamp/channel.d.ts +3 -5
- package/dist/core/runtime/stamp/channel.js +3 -10
- package/dist/core/runtime/stop-guard.js +2 -2
- package/dist/core/runtime/structured-output.d.ts +24 -3
- package/dist/core/runtime/structured-output.js +42 -3
- package/dist/core/runtime/tmux-bindings.js +4 -0
- package/dist/core/runtime/warm-pool.d.ts +5 -2
- package/dist/core/runtime/warm-pool.js +5 -5
- package/dist/core/subscription-state.js +7 -5
- package/dist/core/substrate/render.js +10 -10
- package/dist/core/user-settings.d.ts +18 -1
- package/dist/core/user-settings.js +37 -0
- package/dist/daemon/__tests__/node-deadline-lane.test.d.ts +1 -0
- package/dist/daemon/__tests__/node-deadline-lane.test.js +76 -0
- package/dist/daemon/__tests__/node-outcome-birth-invariants.test.d.ts +1 -0
- package/dist/daemon/__tests__/node-outcome-birth-invariants.test.js +122 -0
- package/dist/daemon/__tests__/node-outcome-delivery.test.d.ts +1 -0
- package/dist/daemon/__tests__/node-outcome-delivery.test.js +86 -0
- package/dist/daemon/api/__tests__/broker-settle-park.test.js +86 -18
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +10 -1
- package/dist/daemon/api/__tests__/seam/node-outcome-api.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/seam/node-outcome-api.test.js +159 -0
- package/dist/daemon/api/__tests__/submit-result.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/submit-result.test.js +214 -0
- package/dist/daemon/api/handlers/broker-ops.js +53 -24
- package/dist/daemon/api/handlers/canvas.js +2 -2
- package/dist/daemon/api/handlers/messages.js +12 -2
- package/dist/daemon/api/handlers/node-outcomes.d.ts +2 -0
- package/dist/daemon/api/handlers/node-outcomes.js +136 -0
- package/dist/daemon/api/handlers/nodes.js +67 -1
- package/dist/daemon/api/handlers/profiles.js +53 -1
- package/dist/daemon/api/handlers/reports.js +197 -33
- package/dist/daemon/api/map.d.ts +7 -2
- package/dist/daemon/api/map.js +78 -4
- package/dist/daemon/api/server.js +2 -0
- package/dist/daemon/crtrd.js +12 -0
- package/dist/daemon/fleet.d.ts +4 -8
- package/dist/daemon/fleet.js +48 -9
- package/dist/daemon/messaging/node-message.js +2 -1
- package/dist/daemon/node-outcome/deliver-outcome.d.ts +14 -0
- package/dist/daemon/node-outcome/deliver-outcome.js +155 -0
- package/dist/daemon/park-activity.d.ts +9 -0
- package/dist/daemon/park-activity.js +30 -0
- package/dist/daemon/park-pending.d.ts +10 -5
- package/dist/daemon/park-pending.js +18 -8
- package/dist/daemon/reconcilers/bash-deadline.js +6 -0
- package/dist/daemon/reconcilers/broker-supervision.d.ts +3 -7
- package/dist/daemon/reconcilers/broker-supervision.js +29 -33
- package/dist/daemon/reconcilers/node-deadline.d.ts +5 -0
- package/dist/daemon/reconcilers/node-deadline.js +82 -0
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +9 -1
- package/dist/daemon/reconcilers/node-lifecycle/tick.js +12 -3
- package/dist/daemon/reconcilers/node-outcome-delivery-lane.d.ts +10 -0
- package/dist/daemon/reconcilers/node-outcome-delivery-lane.js +41 -0
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +66 -66
- package/dist/pi-extensions/canvas-structured-output.d.ts +9 -5
- package/dist/pi-extensions/canvas-structured-output.js +23 -297
- package/dist/shared/env.d.ts +3 -0
- package/dist/shared/env.js +6 -0
- package/dist/shared/generated-context.d.ts +3 -9
- package/dist/shared/generated-context.js +8 -16
- package/dist/types.d.ts +19 -0
- package/dist/types.js +1 -0
- package/package.json +9 -8
- package/runtime.lock.json +227 -383
|
@@ -19,7 +19,7 @@ import { fullName } from './labels.js';
|
|
|
19
19
|
import { jobDir, contextDir, reportsDir } from './paths.js';
|
|
20
20
|
import { readNodeRecap } from './node-recap.js';
|
|
21
21
|
import { parseFrontmatterGeneric } from '../frontmatter.js';
|
|
22
|
-
import { isPidAlive } from './pid.js';
|
|
22
|
+
import { isPidAlive, isPidPresent } from './pid.js';
|
|
23
23
|
import { isFrozenRow, resolveNodeVisual, faultSummary } from './status-glyph.js';
|
|
24
24
|
import { readFault } from '../runtime/fault.js';
|
|
25
25
|
import { parseCard } from '../../shared/generated-context.js';
|
|
@@ -434,7 +434,7 @@ function assistantContentIsSubstantive(content) {
|
|
|
434
434
|
* (an active node is usually dormant between turns). Read directly off disk
|
|
435
435
|
* (mirrors telemetry) to avoid inverting the canvas→runtime dependency. */
|
|
436
436
|
export function isStreaming(nodeId, piPid) {
|
|
437
|
-
if (!
|
|
437
|
+
if (!isPidPresent(piPid))
|
|
438
438
|
return false;
|
|
439
439
|
try {
|
|
440
440
|
return existsSync(join(jobDir(nodeId), 'busy'));
|
|
@@ -18,7 +18,34 @@ export type Lifecycle = 'terminal' | 'resident';
|
|
|
18
18
|
export type Mode = 'base' | 'orchestrator';
|
|
19
19
|
/** Why a node last stopped — drives the daemon's reap-vs-revive decision. */
|
|
20
20
|
export type ExitIntent = 'done' | 'refresh' | 'idle-release' | 'parked' | null;
|
|
21
|
-
export type TerminalReason = 'finalized' | 'finished' | 'parked' | 'closed' | 'retired' | 'crashed' | 'stranded';
|
|
21
|
+
export type TerminalReason = 'finalized' | 'finished' | 'parked' | 'closed' | 'retired' | 'crashed' | 'stranded' | 'boot_failed' | 'crash_looped' | 'launch_failed' | 'context_overflow' | 'deadline_exceeded' | 'provider_fatal' | 'declined';
|
|
22
|
+
export type NodeOutcomeKind = 'result' | 'failure';
|
|
23
|
+
/** Maximum serialized opaque action payload; enforced at registration in Phase 5. */
|
|
24
|
+
export declare const OUTCOME_PAYLOAD_MAX_BYTES: number;
|
|
25
|
+
export interface NodeOutcomeDetailV1 {
|
|
26
|
+
schema: 'crtr.node-outcome-detail/v1';
|
|
27
|
+
message?: string;
|
|
28
|
+
fault_kind?: import('../fault-classifier.js').FaultKind;
|
|
29
|
+
error_class?: import('../events/types.js').ErrorClassCode;
|
|
30
|
+
retry?: {
|
|
31
|
+
attempt: number;
|
|
32
|
+
max: number;
|
|
33
|
+
};
|
|
34
|
+
respawn_failures?: number;
|
|
35
|
+
deadline?: {
|
|
36
|
+
deadline_at: string;
|
|
37
|
+
elapsed_ms: number;
|
|
38
|
+
};
|
|
39
|
+
/** Present only for reason `declined`: what the node gave `crtr push result --decline`. */
|
|
40
|
+
declined?: DeclinedResult;
|
|
41
|
+
truncated?: true;
|
|
42
|
+
}
|
|
43
|
+
/** A declined structured result. `code` is opaque here — the requester validates it. */
|
|
44
|
+
export interface DeclinedResult {
|
|
45
|
+
reason: string;
|
|
46
|
+
code: string;
|
|
47
|
+
retryable: boolean;
|
|
48
|
+
}
|
|
22
49
|
/** The two structural edges. `subscribes_to` is the load-bearing spine (flow,
|
|
23
50
|
* org chart, views, completion routing). `spawned_by` is audit only. */
|
|
24
51
|
export type EdgeType = 'subscribes_to' | 'spawned_by';
|
|
@@ -266,6 +293,14 @@ export interface NodeRuntime {
|
|
|
266
293
|
final_report?: string | null;
|
|
267
294
|
/** ISO timestamp carried by the committed canonical final report, or null. */
|
|
268
295
|
finalized_at?: string | null;
|
|
296
|
+
/** Durable terminal outcome. `outcome_at` is its write-once latch. */
|
|
297
|
+
outcome_kind?: NodeOutcomeKind | null;
|
|
298
|
+
outcome_reason?: TerminalReason | null;
|
|
299
|
+
outcome_at?: string | null;
|
|
300
|
+
outcome_revision?: number;
|
|
301
|
+
outcome_detail_json?: string | null;
|
|
302
|
+
/** Absolute wall-clock spawn deadline, or null when unbounded. */
|
|
303
|
+
deadline_at?: string | null;
|
|
269
304
|
/** OS pid of the live broker process, recorded on boot (stophook session_start).
|
|
270
305
|
* The daemon's authoritative liveness signal — every node is a detached broker,
|
|
271
306
|
* so `isPidAlive(pi_pid)` is the sole liveness check. Cleared to null by
|
|
@@ -342,6 +377,12 @@ export interface NodeRow {
|
|
|
342
377
|
terminal_reason?: TerminalReason | null;
|
|
343
378
|
final_report?: string | null;
|
|
344
379
|
finalized_at?: string | null;
|
|
380
|
+
outcome_kind?: NodeOutcomeKind | null;
|
|
381
|
+
outcome_reason?: TerminalReason | null;
|
|
382
|
+
outcome_at?: string | null;
|
|
383
|
+
outcome_revision?: number;
|
|
384
|
+
outcome_detail_json?: string | null;
|
|
385
|
+
deadline_at?: string | null;
|
|
345
386
|
pi_pid: number | null;
|
|
346
387
|
/** See `NodeRuntime.pi_pid_identity`. */
|
|
347
388
|
pi_pid_identity: string | null;
|
|
@@ -5,4 +5,5 @@
|
|
|
5
5
|
// `meta.json` is the source of truth for its own row; the db is a queryable
|
|
6
6
|
// index over those metas, plus the authoritative store for the mutable
|
|
7
7
|
// `subscribes_to` edges (which no single meta owns).
|
|
8
|
-
|
|
8
|
+
/** Maximum serialized opaque action payload; enforced at registration in Phase 5. */
|
|
9
|
+
export const OUTCOME_PAYLOAD_MAX_BYTES = 64 * 1024;
|
|
@@ -24,7 +24,9 @@ export type LeafAdapter = (leaf: DeclLeafBase, commandPath: readonly string[]) =
|
|
|
24
24
|
* will produce its leaves' run implementations. */
|
|
25
25
|
export interface CommandContribution {
|
|
26
26
|
contributor: CommandContributorRef;
|
|
27
|
-
|
|
27
|
+
/** Every leaf below this branch carries its transport dialect — a validated
|
|
28
|
+
* contribution is never a bare {@link DeclLeafBase}. */
|
|
29
|
+
node: DeclBranch;
|
|
28
30
|
/** Root name from the manifest, before collision qualification. */
|
|
29
31
|
manifestName: string;
|
|
30
32
|
adaptLeaf: LeafAdapter;
|
|
@@ -1,77 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export
|
|
3
|
-
concept: string;
|
|
4
|
-
description: string;
|
|
5
|
-
whenToUse: string;
|
|
6
|
-
}
|
|
7
|
-
export interface DeclBranch<L = DeclLeafBase> {
|
|
8
|
-
kind: 'branch';
|
|
9
|
-
name: string;
|
|
10
|
-
description: string;
|
|
11
|
-
whenToUse: string;
|
|
12
|
-
tier?: 'normal' | 'common' | 'important';
|
|
13
|
-
/** Required on a top-level branch, forbidden on a nested one. */
|
|
14
|
-
rootEntry?: DeclRootEntry;
|
|
15
|
-
/** Allows the nearest repository fragment to contribute children below this top-level branch. */
|
|
16
|
-
extensible?: true;
|
|
17
|
-
summary: string;
|
|
18
|
-
model?: string;
|
|
19
|
-
/** Exec transport only: forward every argv token after this branch to an
|
|
20
|
-
* external binary instead of parsing children. A passthrough branch is
|
|
21
|
-
* childless by construction. HTTP manifests reject passthrough because an
|
|
22
|
-
* HTTP transport must not name a local binary to execute. */
|
|
23
|
-
passthrough?: DeclPassthrough;
|
|
24
|
-
children: DeclNode<L>[];
|
|
25
|
-
}
|
|
26
|
-
export interface DeclPassthrough {
|
|
27
|
-
bin: string;
|
|
28
|
-
installHint: string;
|
|
29
|
-
}
|
|
30
|
-
export interface DeclLeafBase {
|
|
31
|
-
kind: 'leaf';
|
|
32
|
-
name: string;
|
|
33
|
-
description: string;
|
|
34
|
-
whenToUse: string;
|
|
35
|
-
tier?: 'normal' | 'common' | 'important';
|
|
36
|
-
summary: string;
|
|
37
|
-
params: InputParam[];
|
|
38
|
-
output: Field[];
|
|
39
|
-
effects: string[];
|
|
40
|
-
}
|
|
41
|
-
/** Exec-transport leaf: requires outputKind: 'object'. */
|
|
42
|
-
export interface ExecDeclLeaf extends DeclLeafBase {
|
|
43
|
-
outputKind: 'object';
|
|
44
|
-
}
|
|
45
|
-
/** HTTP-transport plugin REST mapping (placeholder types; full spec in manifest.ts). */
|
|
46
|
-
export type RestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
47
|
-
export type RestParamPlacement = 'path' | 'query' | 'body' | 'header';
|
|
48
|
-
export interface RestParamMapping {
|
|
49
|
-
in: RestParamPlacement;
|
|
50
|
-
as?: string;
|
|
51
|
-
}
|
|
52
|
-
export interface RestMapping {
|
|
53
|
-
method: RestMethod;
|
|
54
|
-
path: string;
|
|
55
|
-
streaming?: boolean;
|
|
56
|
-
/** Constant literal body fields merged into the request body (top-level, alongside
|
|
57
|
-
* any bodyRoot-nested param values). Forbidden on GET. */
|
|
58
|
-
body?: Record<string, string | number | boolean>;
|
|
59
|
-
/** When set, all in:"body" param values nest under this key instead of the body
|
|
60
|
-
* top level (constants from `body` stay top-level regardless). Forbidden on GET. */
|
|
61
|
-
bodyRoot?: string;
|
|
62
|
-
params: Record<string, RestParamMapping>;
|
|
63
|
-
}
|
|
64
|
-
export interface ManifestTimeouts {
|
|
65
|
-
connectMs?: number;
|
|
66
|
-
requestMs?: number;
|
|
67
|
-
streamIdleMs?: number;
|
|
68
|
-
}
|
|
69
|
-
/** HTTP-transport leaf: requires rest (derives outputKind from streaming). */
|
|
70
|
-
export interface HttpDeclLeaf extends DeclLeafBase {
|
|
71
|
-
rest: RestMapping;
|
|
72
|
-
}
|
|
73
|
-
export type DeclLeaf = ExecDeclLeaf | HttpDeclLeaf;
|
|
74
|
-
export type DeclNode<L = DeclLeafBase> = DeclBranch<L> | (L extends DeclLeafBase ? DeclLeaf : never);
|
|
1
|
+
import type { ManifestRootEntry as DeclRootEntry, ManifestPassthrough as DeclPassthrough, ManifestLeafBase as DeclLeafBase, ManifestExecLeaf as ExecDeclLeaf, ManifestHttpLeaf as HttpDeclLeaf, ManifestLeaf as DeclLeaf, ManifestBranch as DeclBranch, ManifestNode as DeclNode, RestMethod, RestParamPlacement, RestParamMapping, RestMapping, ManifestTimeouts } from '../../api/plugin-manifest-schema.js';
|
|
2
|
+
export type { DeclRootEntry, DeclPassthrough, DeclLeafBase, ExecDeclLeaf, HttpDeclLeaf, DeclLeaf, DeclBranch, DeclNode, RestMethod, RestParamPlacement, RestParamMapping, RestMapping, ManifestTimeouts, };
|
|
75
3
|
export interface CommandManifestIssue {
|
|
76
4
|
code: CommandIssueCode;
|
|
77
5
|
path?: string;
|
|
@@ -92,4 +20,3 @@ export interface CommandNodeValidationOptions {
|
|
|
92
20
|
allowExtensible?: boolean;
|
|
93
21
|
}
|
|
94
22
|
export declare function validateCommandNode(raw: unknown, path: string[], topLevel: boolean, transport: TransportKind, issue: IssueFn, options?: CommandNodeValidationOptions): DeclBranch<DeclLeaf> | DeclLeaf | null;
|
|
95
|
-
export {};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
// Unified command-manifest schema. Transport is the sole leaf-dialect discriminator.
|
|
2
|
+
//
|
|
3
|
+
// The SHAPE of a manifest is declared once, in `src/api/plugin-manifest-schema.ts`,
|
|
4
|
+
// and published as `@north-light/crouter-api/plugin-manifest` so a server that
|
|
5
|
+
// serves a plugin bundle compiles against the same types validated here. This
|
|
6
|
+
// module owns the validators and re-exports those types under their in-tree names.
|
|
2
7
|
import { isRecord } from '../../shared/predicates.js';
|
|
3
8
|
// Validation helpers
|
|
4
9
|
const TIERS = new Set(['normal', 'common', 'important']);
|
|
@@ -43,5 +43,5 @@ function buildBranch(contribution, node, path) {
|
|
|
43
43
|
}
|
|
44
44
|
function buildLeaf(contribution, node, path) {
|
|
45
45
|
const adapted = contribution.adaptLeaf(node, path);
|
|
46
|
-
return defineLeaf({ name: node.name, description: node.description, whenToUse: node.whenToUse, ...(node.tier !== undefined ? { tier: node.tier } : {}), help: { name: path.join(' '), summary: node.summary, params: node.params, output: node.output, outputKind: adapted.outputKind, effects: node.effects }, run: adapted.run });
|
|
46
|
+
return defineLeaf({ name: node.name, description: node.description, whenToUse: node.whenToUse, ...(node.tier !== undefined ? { tier: node.tier } : {}), help: { name: path.join(' '), summary: node.summary, params: node.params, output: node.output, outputKind: adapted.outputKind, effects: [...node.effects] }, run: adapted.run });
|
|
47
47
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bring every installed bundle plugin whose last check has aged out back in
|
|
3
|
+
* step with its endpoint. Never throws: a plugin that cannot be revalidated
|
|
4
|
+
* keeps the package already unpacked on disk, which is a complete and valid
|
|
5
|
+
* command tree, and the failure is reported on stderr rather than replacing the
|
|
6
|
+
* caller's command with an error about a background check.
|
|
7
|
+
*/
|
|
8
|
+
export declare function revalidateBundlePlugins(argv: readonly string[]): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* Force one plugin back in step with its endpoint, ignoring both the TTL and
|
|
11
|
+
* the stored validator. Recovery from a server that answered `manifest_stale`:
|
|
12
|
+
* the description the caller invoked from is out of date, so the check that
|
|
13
|
+
* would normally be skipped is exactly the one that has to run.
|
|
14
|
+
*
|
|
15
|
+
* Returns whether the package on disk actually changed, so a caller can tell a
|
|
16
|
+
* recoverable staleness from an operation that is genuinely gone.
|
|
17
|
+
*/
|
|
18
|
+
export declare function forceRevalidateBundlePlugin(name: string): Promise<boolean>;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// TTL-gated revalidation of installed bundle plugins.
|
|
2
|
+
//
|
|
3
|
+
// A bundle plugin's package is fetched once, at install. Without this pass a
|
|
4
|
+
// long-lived machine keeps invoking the command tree it installed on day one,
|
|
5
|
+
// so a leaf the server renamed fails from the client side with no way back
|
|
6
|
+
// except a human running `crtr pkg plugin update`.
|
|
7
|
+
//
|
|
8
|
+
// The pass runs before the command tree is built, so a replacement it performs
|
|
9
|
+
// is what this very invocation dispatches against. Being on that path, it is
|
|
10
|
+
// also bound by what it may DRAG there: this module reaches only the leaf-level
|
|
11
|
+
// scope, config, and installed-plugin readers — never `discovery.js`, which
|
|
12
|
+
// pulls the manifest validators, the registry, and the composer onto every
|
|
13
|
+
// single crtr invocation, nor `plugin-manage.js`, which pulls the whole package
|
|
14
|
+
// command graph. The machinery that fetches and swaps a package is imported
|
|
15
|
+
// only once a plugin is found due.
|
|
16
|
+
import { readState } from '../config.js';
|
|
17
|
+
import { listInstalledPluginsInRoot } from '../installed-plugins.js';
|
|
18
|
+
import { scopeRoot } from '../scope.js';
|
|
19
|
+
import { GLOBAL_TOKENS } from '../command.js';
|
|
20
|
+
const DEFAULT_TTL_MS = 15 * 60 * 1000;
|
|
21
|
+
/** Project first, so a project-scoped package shadows a user-scoped one of the
|
|
22
|
+
* same name exactly as the command tree resolves it. */
|
|
23
|
+
const SCOPES = ['project', 'user'];
|
|
24
|
+
/** How long a recorded check stands before the next invocation re-probes.
|
|
25
|
+
* `CRTR_PLUGIN_REVALIDATE_TTL_MS=0` disables the pass outright — which is how
|
|
26
|
+
* a test, or any process that must not reach the network, opts out. */
|
|
27
|
+
function ttlMs() {
|
|
28
|
+
const raw = process.env['CRTR_PLUGIN_REVALIDATE_TTL_MS'];
|
|
29
|
+
if (raw === undefined || raw.trim() === '')
|
|
30
|
+
return DEFAULT_TTL_MS;
|
|
31
|
+
const parsed = Number(raw);
|
|
32
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
33
|
+
return DEFAULT_TTL_MS;
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
/** A check is fresh for `ttl` after it happened. A `checked_at` in the FUTURE
|
|
37
|
+
* did not happen: a clock that jumped back, or a file copied from another
|
|
38
|
+
* machine, must not be able to suppress the pass until the clock catches up. */
|
|
39
|
+
function isFresh(checkedAt, ttl, now) {
|
|
40
|
+
if (checkedAt === undefined)
|
|
41
|
+
return false;
|
|
42
|
+
const at = Date.parse(checkedAt);
|
|
43
|
+
if (Number.isNaN(at))
|
|
44
|
+
return false;
|
|
45
|
+
const age = now - at;
|
|
46
|
+
return age >= 0 && age < ttl;
|
|
47
|
+
}
|
|
48
|
+
/** A command that manages packages does its own fetching, with its own reporting
|
|
49
|
+
* and its own exit codes. Revalidating underneath it would race that work and
|
|
50
|
+
* make the leaf's result describe a package it did not install. `crtr --json
|
|
51
|
+
* pkg …` is that same command: the global tokens the dispatcher strips are not
|
|
52
|
+
* part of the command path here either. */
|
|
53
|
+
function managesPackages(argv) {
|
|
54
|
+
return argv.slice(2).find((token) => !GLOBAL_TOKENS.has(token)) === 'pkg';
|
|
55
|
+
}
|
|
56
|
+
/** Every bundle plugin whose last check has aged out, resolved from the same
|
|
57
|
+
* scope root whose state ledger records the check — so the plugin read and the
|
|
58
|
+
* `checked_at` write can never describe different roots. */
|
|
59
|
+
function duePlugins(ttl, now) {
|
|
60
|
+
const due = [];
|
|
61
|
+
const claimed = new Set();
|
|
62
|
+
for (const scope of SCOPES) {
|
|
63
|
+
const root = scopeRoot(scope);
|
|
64
|
+
if (root === null)
|
|
65
|
+
continue;
|
|
66
|
+
const state = readState(scope);
|
|
67
|
+
for (const plugin of listInstalledPluginsInRoot(scope, root)) {
|
|
68
|
+
if (claimed.has(plugin.name))
|
|
69
|
+
continue;
|
|
70
|
+
claimed.add(plugin.name);
|
|
71
|
+
const bundle = plugin.manifest.bundle;
|
|
72
|
+
if (!plugin.enabled || bundle === undefined)
|
|
73
|
+
continue;
|
|
74
|
+
if (isFresh(state.plugins[plugin.name]?.checked_at, ttl, now))
|
|
75
|
+
continue;
|
|
76
|
+
due.push({ name: plugin.name, scope, bundle, enabled: plugin.enabled });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return due;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Bring every installed bundle plugin whose last check has aged out back in
|
|
83
|
+
* step with its endpoint. Never throws: a plugin that cannot be revalidated
|
|
84
|
+
* keeps the package already unpacked on disk, which is a complete and valid
|
|
85
|
+
* command tree, and the failure is reported on stderr rather than replacing the
|
|
86
|
+
* caller's command with an error about a background check.
|
|
87
|
+
*/
|
|
88
|
+
export async function revalidateBundlePlugins(argv) {
|
|
89
|
+
const ttl = ttlMs();
|
|
90
|
+
if (ttl === 0 || managesPackages(argv))
|
|
91
|
+
return;
|
|
92
|
+
let due;
|
|
93
|
+
try {
|
|
94
|
+
due = duePlugins(ttl, Date.now());
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Enumeration reads config, state, and every installed plugin.json. A scope
|
|
98
|
+
// this process cannot read is a scope it also cannot revalidate, and the
|
|
99
|
+
// command it was invoked for may not need plugins at all.
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (due.length === 0)
|
|
103
|
+
return;
|
|
104
|
+
const { revalidateBundlePlugin } = await import('../../commands/pkg/plugin-manage.js');
|
|
105
|
+
for (const plugin of due) {
|
|
106
|
+
try {
|
|
107
|
+
await revalidateBundlePlugin(plugin.name, plugin.bundle, plugin.scope, {
|
|
108
|
+
enable: plugin.enabled,
|
|
109
|
+
conditional: true,
|
|
110
|
+
timeoutMs: 2_000,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
115
|
+
process.stderr.write(`crtr: plugin "${plugin.name}" could not be revalidated (${detail}); using the installed package.\n`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Force one plugin back in step with its endpoint, ignoring both the TTL and
|
|
121
|
+
* the stored validator. Recovery from a server that answered `manifest_stale`:
|
|
122
|
+
* the description the caller invoked from is out of date, so the check that
|
|
123
|
+
* would normally be skipped is exactly the one that has to run.
|
|
124
|
+
*
|
|
125
|
+
* Returns whether the package on disk actually changed, so a caller can tell a
|
|
126
|
+
* recoverable staleness from an operation that is genuinely gone.
|
|
127
|
+
*/
|
|
128
|
+
export async function forceRevalidateBundlePlugin(name) {
|
|
129
|
+
let plugin;
|
|
130
|
+
for (const scope of SCOPES) {
|
|
131
|
+
const root = scopeRoot(scope);
|
|
132
|
+
if (root === null)
|
|
133
|
+
continue;
|
|
134
|
+
plugin = listInstalledPluginsInRoot(scope, root).find((candidate) => candidate.name === name);
|
|
135
|
+
if (plugin !== undefined)
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
const bundle = plugin?.manifest.bundle;
|
|
139
|
+
if (plugin === undefined || bundle === undefined)
|
|
140
|
+
return false;
|
|
141
|
+
const { revalidateBundlePlugin } = await import('../../commands/pkg/plugin-manage.js');
|
|
142
|
+
// An unconditional refetch either swaps the package or reports that the bytes
|
|
143
|
+
// the server served are byte-identical to the ones already unpacked. Both
|
|
144
|
+
// answers are the swap's own, so `undefined` here means "nothing changed" —
|
|
145
|
+
// the operation the caller invoked is genuinely gone, and the server's error
|
|
146
|
+
// stands.
|
|
147
|
+
const replaced = await revalidateBundlePlugin(name, bundle, plugin.scope, {
|
|
148
|
+
enable: plugin.enabled,
|
|
149
|
+
conditional: false,
|
|
150
|
+
});
|
|
151
|
+
return replaced !== undefined;
|
|
152
|
+
}
|
|
@@ -3,14 +3,39 @@ import type { HttpPluginFetchTarget } from '../endpoint.js';
|
|
|
3
3
|
export interface FetchSuccess {
|
|
4
4
|
status: 200;
|
|
5
5
|
raw: Uint8Array;
|
|
6
|
+
/** The server's validator for these exact bytes, when it sent one. Storing it
|
|
7
|
+
* is what lets the next fetch be conditional. */
|
|
8
|
+
etag?: string;
|
|
6
9
|
}
|
|
7
|
-
|
|
10
|
+
/** The archive the caller already holds is still current. Only ever returned
|
|
11
|
+
* when the caller supplied `ifNoneMatch`. */
|
|
12
|
+
export interface FetchNotModified {
|
|
13
|
+
status: 304;
|
|
14
|
+
}
|
|
15
|
+
export type FetchResult = FetchSuccess | FetchNotModified | FetchFailure;
|
|
8
16
|
export interface FetchFailure {
|
|
9
17
|
status: 'auth_env_missing' | 'cli_unreachable' | 'cli_protocol_error';
|
|
10
18
|
message: string;
|
|
11
19
|
}
|
|
20
|
+
export interface FetchOptions {
|
|
21
|
+
/** Send as `If-None-Match`, inviting the server to answer 304 instead of
|
|
22
|
+
* resending an archive the caller already has unpacked. */
|
|
23
|
+
ifNoneMatch?: string;
|
|
24
|
+
/** Inactivity timeout. Defaults to ten seconds, which suits an install the
|
|
25
|
+
* caller is waiting on; a revalidation on an ordinary command's latency path
|
|
26
|
+
* passes something far shorter. */
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
}
|
|
12
29
|
/**
|
|
13
|
-
* One authenticated GET for a plugin directory archive
|
|
14
|
-
*
|
|
30
|
+
* One authenticated GET for a plugin directory archive, conditional when the
|
|
31
|
+
* caller supplies a validator. It never retries.
|
|
32
|
+
*
|
|
33
|
+
* A caller that sends no validator cannot be answered 304 — there is nothing
|
|
34
|
+
* for the server to compare against — and the overloads say so, so an
|
|
35
|
+
* unconditional caller handles the two outcomes it can actually get instead of
|
|
36
|
+
* carrying an unreachable branch for the third.
|
|
15
37
|
*/
|
|
16
|
-
export declare function fetchHttpPluginBundle(registration: HttpPluginFetchTarget
|
|
38
|
+
export declare function fetchHttpPluginBundle(registration: HttpPluginFetchTarget, options?: FetchOptions & {
|
|
39
|
+
ifNoneMatch?: never;
|
|
40
|
+
}): Promise<FetchSuccess | FetchFailure>;
|
|
41
|
+
export declare function fetchHttpPluginBundle(registration: HttpPluginFetchTarget, options: FetchOptions): Promise<FetchResult>;
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { URL } from 'node:url';
|
|
2
2
|
import { request as httpsRequest } from 'node:https';
|
|
3
3
|
import { request as httpRequest } from 'node:http';
|
|
4
|
-
|
|
5
|
-
* One authenticated GET for a plugin directory archive. It has one ten-second
|
|
6
|
-
* inactivity timeout and never retries or conditionally revalidates.
|
|
7
|
-
*/
|
|
8
|
-
export async function fetchHttpPluginBundle(registration) {
|
|
4
|
+
export async function fetchHttpPluginBundle(registration, options = {}) {
|
|
9
5
|
let token;
|
|
10
6
|
if (registration.authEnv) {
|
|
11
7
|
token = process.env[registration.authEnv];
|
|
@@ -26,20 +22,34 @@ export async function fetchHttpPluginBundle(registration) {
|
|
|
26
22
|
message: `Invalid endpoint URL: ${registration.endpoint}`,
|
|
27
23
|
};
|
|
28
24
|
}
|
|
25
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
29
26
|
const headers = { Accept: 'application/x-tar' };
|
|
30
27
|
if (token)
|
|
31
28
|
headers['Authorization'] = `Bearer ${token}`;
|
|
29
|
+
if (options.ifNoneMatch !== undefined)
|
|
30
|
+
headers['If-None-Match'] = options.ifNoneMatch;
|
|
32
31
|
return new Promise((resolve) => {
|
|
33
32
|
const request = url.protocol === 'https:' ? httpsRequest : httpRequest;
|
|
34
|
-
const req = request(url, { method: 'GET', headers, timeout:
|
|
33
|
+
const req = request(url, { method: 'GET', headers, timeout: timeoutMs }, (res) => {
|
|
35
34
|
let body = Buffer.alloc(0);
|
|
36
35
|
res.on('data', (chunk) => { body = Buffer.concat([body, chunk]); });
|
|
37
36
|
res.on('end', () => {
|
|
37
|
+
const etag = res.headers['etag'];
|
|
38
38
|
if (!res.statusCode) {
|
|
39
39
|
resolve({ status: 'cli_protocol_error', message: 'No HTTP status code received.' });
|
|
40
40
|
}
|
|
41
|
+
else if (res.statusCode === 304) {
|
|
42
|
+
// Unsolicited: the caller has no archive this could be affirming, so
|
|
43
|
+
// there is nothing on disk for a bare 304 to mean.
|
|
44
|
+
if (options.ifNoneMatch === undefined) {
|
|
45
|
+
resolve({ status: 'cli_protocol_error', message: `HTTP 304 from ${registration.endpoint} without a conditional request` });
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
resolve({ status: 304 });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
41
51
|
else if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
42
|
-
resolve({ status: 200, raw: new Uint8Array(body) });
|
|
52
|
+
resolve({ status: 200, raw: new Uint8Array(body), ...(typeof etag === 'string' ? { etag } : {}) });
|
|
43
53
|
}
|
|
44
54
|
else {
|
|
45
55
|
resolve({ status: 'cli_protocol_error', message: `HTTP ${res.statusCode} from ${registration.endpoint}` });
|
|
@@ -48,7 +58,7 @@ export async function fetchHttpPluginBundle(registration) {
|
|
|
48
58
|
});
|
|
49
59
|
req.on('timeout', () => {
|
|
50
60
|
req.destroy();
|
|
51
|
-
resolve({ status: 'cli_unreachable', message: `Request timeout (
|
|
61
|
+
resolve({ status: 'cli_unreachable', message: `Request timeout (${timeoutMs}ms) fetching ${registration.endpoint}` });
|
|
52
62
|
});
|
|
53
63
|
req.on('error', (error) => {
|
|
54
64
|
resolve({ status: 'cli_unreachable', message: `Network error fetching ${registration.endpoint}: ${error.message}` });
|
|
@@ -17,6 +17,7 @@ import { request as httpRequest } from 'node:http';
|
|
|
17
17
|
import { request as httpsRequest } from 'node:https';
|
|
18
18
|
import { validateDeclaredResult } from './exec-invoke.js';
|
|
19
19
|
import { CrtrError } from '../../errors.js';
|
|
20
|
+
import { envelopeClaimsStale, staleErrorDetails } from '../../manifest-stale.js';
|
|
20
21
|
import { diag, recordPreviewError, recordPreviewJsonLine, writeStdout } from '../../io.js';
|
|
21
22
|
import { ExitCode } from '../../../types.js';
|
|
22
23
|
import { isRecord } from '../../../shared/predicates.js';
|
|
@@ -479,9 +480,15 @@ function throwNon2xx(spec, status, body) {
|
|
|
479
480
|
const field = typeof err['field'] === 'string' ? err['field'] : undefined;
|
|
480
481
|
const next = typeof err['next'] === 'string' ? err['next'] : `Inspect the backend response, then retry if appropriate.`;
|
|
481
482
|
const accepted = SNAKE.test(code) && !RESERVED_CODES.has(code);
|
|
483
|
+
// A generic recovery hint, not a backend-specific code: the server is
|
|
484
|
+
// saying the command description this call was parsed from is out of
|
|
485
|
+
// date. The dispatcher acts on it by refetching the plugin's package and
|
|
486
|
+
// re-running the original argv against the refreshed tree.
|
|
487
|
+
const manifestStale = envelopeClaimsStale(err);
|
|
482
488
|
throw new CrtrError(accepted ? code : 'cli_protocol_error', message, exit, {
|
|
483
489
|
...(err['received'] !== undefined ? { received: err['received'] } : {}),
|
|
484
490
|
...(field !== undefined ? { field } : {}),
|
|
491
|
+
...(manifestStale ? staleErrorDetails(spec.registration.name) : {}),
|
|
485
492
|
http_status: status,
|
|
486
493
|
next,
|
|
487
494
|
});
|
package/dist/core/command.d.ts
CHANGED
|
@@ -173,5 +173,13 @@ export interface ParseArgvOptions {
|
|
|
173
173
|
* Returns a plain object whose keys are camelCase parameter names.
|
|
174
174
|
* Optionally tracks which parameters were explicitly provided via a callback. */
|
|
175
175
|
export declare function parseArgv(params: InputParam[], tokens: string[], options?: ParseArgvOptions): Promise<Record<string, unknown>>;
|
|
176
|
+
/**
|
|
177
|
+
* Tokens handled before dispatch and stripped from what the leaf schema parses
|
|
178
|
+
* (root `-h` renders them as the Globals footer). They belong to no command, so
|
|
179
|
+
* anything reasoning about which command an argv names — the plugin
|
|
180
|
+
* revalidation gate, for one — has to ignore them too. One declaration, so a
|
|
181
|
+
* new global cannot be added here and missed there.
|
|
182
|
+
*/
|
|
183
|
+
export declare const GLOBAL_TOKENS: ReadonlySet<string>;
|
|
176
184
|
export declare function runCli(root: RootDef, argv: string[]): Promise<void>;
|
|
177
185
|
export {};
|
package/dist/core/command.js
CHANGED
|
@@ -8,6 +8,7 @@ import { renderRoot, renderBranch, renderLeafArgv } from './help.js';
|
|
|
8
8
|
import { beginPreview, publishPreviewResult, readStdinRaw, peekStdinRaw, emit, handle, setJsonOutput, isJsonOutput } from './io.js';
|
|
9
9
|
import { renderResult } from './render.js';
|
|
10
10
|
import { CrtrError } from './errors.js';
|
|
11
|
+
import { isManifestStaleError } from './manifest-stale.js';
|
|
11
12
|
import { operationIdContext } from './events/operation-id.js';
|
|
12
13
|
import { ExitCode } from '../types.js';
|
|
13
14
|
import { readFileSync } from 'node:fs';
|
|
@@ -594,6 +595,14 @@ export async function parseArgv(params, tokens, options) {
|
|
|
594
595
|
}
|
|
595
596
|
return result;
|
|
596
597
|
}
|
|
598
|
+
/**
|
|
599
|
+
* Tokens handled before dispatch and stripped from what the leaf schema parses
|
|
600
|
+
* (root `-h` renders them as the Globals footer). They belong to no command, so
|
|
601
|
+
* anything reasoning about which command an argv names — the plugin
|
|
602
|
+
* revalidation gate, for one — has to ignore them too. One declaration, so a
|
|
603
|
+
* new global cannot be added here and missed there.
|
|
604
|
+
*/
|
|
605
|
+
export const GLOBAL_TOKENS = new Set(['--json', '--no-autostart']);
|
|
597
606
|
export async function runCli(root, argv) {
|
|
598
607
|
// argv is process.argv — strip node binary + script path. `--json` is a
|
|
599
608
|
// global: pull it out anywhere it appears so the rest of argv parses against
|
|
@@ -607,10 +616,9 @@ export async function runCli(root, argv) {
|
|
|
607
616
|
// to every API-backed verb, so strip it here rather than declaring it on each
|
|
608
617
|
// leaf schema (an undeclared flag would otherwise be rejected as unknown).
|
|
609
618
|
const rawTokens = argv.slice(2);
|
|
610
|
-
|
|
611
|
-
if (jsonStripped.length !== rawTokens.length)
|
|
619
|
+
if (rawTokens.includes('--json'))
|
|
612
620
|
setJsonOutput(true);
|
|
613
|
-
const tokens =
|
|
621
|
+
const tokens = rawTokens.filter((token) => !GLOBAL_TOKENS.has(token));
|
|
614
622
|
// Bare root invocation or -h at root
|
|
615
623
|
if (tokens.length === 0 || (tokens.length === 1 && (tokens[0] === '-h' || tokens[0] === '--help'))) {
|
|
616
624
|
process.stdout.write(renderRoot(root.help) + '\n');
|
|
@@ -686,6 +694,14 @@ export async function runCli(root, argv) {
|
|
|
686
694
|
// JSONL leaves call emitLine themselves and return void
|
|
687
695
|
}
|
|
688
696
|
catch (e) {
|
|
697
|
+
// A backend that answered `manifest_stale` says this dispatch was parsed
|
|
698
|
+
// from an out-of-date command description. Only the caller above this one
|
|
699
|
+
// can act on that — refetching the package and re-walking the ORIGINAL argv
|
|
700
|
+
// against the refreshed tree — so it is the single error class this
|
|
701
|
+
// dispatcher reports by rethrowing instead of rendering. Whoever declines
|
|
702
|
+
// to recover renders it with the same `handle`.
|
|
703
|
+
if (isManifestStaleError(e))
|
|
704
|
+
throw e;
|
|
689
705
|
handle(e);
|
|
690
706
|
}
|
|
691
707
|
}
|
package/dist/core/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PageComponentRegistration, Scope, ScopeConfig, ScopeState, HumanActionConfig, KindConfig, ModelLaddersConfig, ModelProvider, ModelStrength, ModelRouteConfig, ModelRoutingConfig } from '../types.js';
|
|
1
|
+
import type { PageComponentRegistration, Scope, ScopeConfig, ScopeState, HumanActionConfig, KindConfig, ModelLaddersConfig, ModelProvider, ModelStrength, ModelRouteConfig, ModelRoutingConfig, ProviderOptionsConfig } from '../types.js';
|
|
2
2
|
import { type BindingId } from './keybindings/catalog.js';
|
|
3
3
|
export declare function configPath(scope: Scope): string | null;
|
|
4
4
|
export declare function statePath(scope: Scope): string | null;
|
|
@@ -111,6 +111,9 @@ export declare function readRawScopeConfig(scope: Scope): Partial<ScopeConfig> |
|
|
|
111
111
|
export interface MergedLaunchConfig {
|
|
112
112
|
kinds: Record<string, KindConfig>;
|
|
113
113
|
modelLadders: ModelLaddersConfig;
|
|
114
|
+
/** Per-provider behavior options keyed by runtime provider id, merged
|
|
115
|
+
* per provider across scopes (project stack > profile > user). */
|
|
116
|
+
providerOptions: Record<string, ProviderOptionsConfig>;
|
|
114
117
|
/** Complete declared routes. Every route here has all four strengths. */
|
|
115
118
|
modelRoutes?: Record<string, ModelRouteConfig>;
|
|
116
119
|
/** Read-only compatibility view for sparse routes written before complete
|