@north-light/crouter 0.3.176 → 0.3.178
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 +4 -4
- package/dist/api/client.js +2 -2
- package/dist/api/dto/lifecycle.d.ts +1 -1
- package/dist/api/dto/reports.d.ts +1 -1
- package/dist/clients/attach/__tests__/frame-selection.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/frame-selection.test.js +84 -0
- package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
- package/dist/clients/attach/ansi-cells.d.ts +31 -0
- package/dist/clients/attach/ansi-cells.js +120 -0
- package/dist/clients/attach/config.js +5 -5
- package/dist/clients/attach/input/clipboard-image.js +2 -2
- package/dist/clients/attach/input/ref-autocomplete.js +1 -1
- package/dist/clients/attach/input/titled-editor.d.ts +12 -13
- package/dist/clients/attach/input/titled-editor.js +14 -15
- package/dist/clients/attach/overlays/dialogs.d.ts +1 -1
- package/dist/clients/attach/overlays/dialogs.js +2 -2
- package/dist/clients/attach/overlays/help.js +2 -1
- package/dist/clients/attach/overlays/mcp.js +5 -12
- package/dist/clients/attach/render/chat-view.d.ts +1 -1
- package/dist/clients/attach/render/chat-view.js +2 -2
- package/dist/clients/attach/render/group-recap.js +7 -0
- package/dist/clients/attach/session/context.js +3 -3
- package/dist/clients/attach/session/editor-inventory.js +6 -6
- package/dist/clients/attach/session/file-links.js +15 -50
- package/dist/clients/attach/session/frame.d.ts +5 -1
- package/dist/clients/attach/session/frame.js +19 -4
- package/dist/clients/attach/session/input-wiring.js +2 -2
- package/dist/clients/attach/session/mouse.d.ts +4 -0
- package/dist/clients/attach/session/mouse.js +14 -3
- package/dist/clients/attach/session/selection.d.ts +28 -0
- package/dist/clients/attach/session/selection.js +108 -0
- package/dist/clients/attach/session/teardown.js +7 -9
- package/dist/clients/attach/slash/dispatch.d.ts +1 -2
- package/dist/clients/attach/slash/dispatch.js +5 -9
- package/dist/clients/attach/viewer.js +552 -550
- package/dist/clients/inbox/controller.js +3 -2
- package/dist/clients/inbox/deck-adapter.js +3 -2
- package/dist/clients/surfaces/host.d.ts +1 -1
- package/dist/commands/api-client.d.ts +12 -13
- package/dist/commands/api-client.js +15 -16
- package/dist/commands/chord.js +1 -2
- package/dist/commands/human/queue.js +6 -5
- package/dist/commands/memory/find.js +3 -3
- package/dist/commands/node/lifecycle.js +1 -1
- package/dist/commands/node-lifecycle-revive.d.ts +1 -1
- package/dist/commands/node-lifecycle-revive.js +3 -3
- package/dist/commands/push.js +1 -1
- package/dist/commands/surface/node/placement.d.ts +1 -1
- package/dist/commands/surface/node/placement.js +1 -1
- package/dist/commands/surface-edit.js +1 -4
- package/dist/commands/sys/panels/panel.d.ts +3 -3
- package/dist/commands/sys/panels/panel.js +3 -3
- package/dist/commands/sys/setup-core.js +1 -7
- package/dist/commands/sys/sync-deps.js +1 -3
- package/dist/commands/sys/sync-project-guidance.js +2 -11
- package/dist/commands/sys/sysprompt.js +1 -3
- package/dist/core/__tests__/fixtures/fake-engine.js +1 -1
- package/dist/core/__tests__/helpers/harness.d.ts +1 -1
- package/dist/core/__tests__/helpers/harness.js +1 -1
- package/dist/core/canvas/boot.js +1 -1
- package/dist/core/canvas/browse/app.js +2 -2
- package/dist/core/canvas/canvas.d.ts +3 -3
- package/dist/core/canvas/canvas.js +3 -3
- package/dist/core/canvas/daemon-owner.js +1 -2
- package/dist/core/canvas/db.js +7 -8
- package/dist/core/canvas/index.js +2 -2
- package/dist/core/canvas/pid.d.ts +12 -14
- package/dist/core/canvas/pid.js +16 -20
- package/dist/core/canvas/remote-transport.js +2 -2
- package/dist/core/canvas/status-glyph.d.ts +1 -1
- package/dist/core/canvas/status-glyph.js +1 -1
- package/dist/core/canvas/types.d.ts +1 -1
- package/dist/core/command-manifests/manifest.js +4 -6
- package/dist/core/command-manifests/schema.js +10 -12
- package/dist/core/command-plugins/transport/exec-invoke.js +5 -7
- package/dist/core/command-plugins/transport/http-invoke.js +4 -6
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.js +2 -2
- package/dist/core/events/read.js +1 -3
- package/dist/core/fault-classifier.js +1 -3
- package/dist/core/fs-utils.d.ts +7 -0
- package/dist/core/fs-utils.js +29 -2
- package/dist/core/host-exports/export.d.ts +4 -5
- package/dist/core/host-exports/export.js +4 -5
- package/dist/core/human/claim.js +3 -2
- package/dist/core/human/convention.d.ts +0 -1
- package/dist/core/human/convention.js +1 -9
- package/dist/core/human/scan.js +5 -4
- package/dist/core/profiles/manifest.d.ts +1 -1
- package/dist/core/profiles/manifest.js +1 -1
- package/dist/core/review/types.d.ts +1 -1
- package/dist/core/review/types.js +1 -1
- package/dist/core/runtime/broker/frame-dispatch.js +4 -4
- package/dist/core/runtime/broker-cli.js +1 -1
- package/dist/core/runtime/broker-protocol.d.ts +9 -10
- package/dist/core/runtime/broker-protocol.js +9 -10
- package/dist/core/runtime/broker-sdk.d.ts +1 -1
- package/dist/core/runtime/broker.js +6 -6
- package/dist/core/runtime/host.js +14 -14
- package/dist/core/runtime/launch.d.ts +1 -1
- package/dist/core/runtime/launch.js +1 -1
- package/dist/core/runtime/lifecycle.js +16 -21
- package/dist/core/runtime/node-read.js +2 -3
- package/dist/core/runtime/package-health.js +1 -8
- package/dist/core/runtime/persona.js +2 -2
- package/dist/core/runtime/pi-vendored.d.ts +4 -2
- package/dist/core/runtime/pi-vendored.js +5 -16
- package/dist/core/runtime/promote.js +1 -1
- package/dist/core/runtime/reopen.js +2 -2
- package/dist/core/runtime/revive-all.d.ts +3 -3
- package/dist/core/runtime/revive-all.js +6 -6
- package/dist/core/runtime/revive.js +1 -1
- package/dist/core/runtime/spawn.d.ts +1 -2
- package/dist/core/runtime/spawn.js +2 -3
- package/dist/core/runtime/structured-output.js +4 -6
- package/dist/core/runtime/tmux-driver.d.ts +2 -2
- package/dist/core/runtime/tmux-driver.js +3 -5
- package/dist/core/runtime/tool-group-summary.js +5 -4
- package/dist/core/runtime/warm-pool.js +1 -1
- package/dist/core/scope.js +2 -9
- package/dist/core/self-update.js +3 -3
- package/dist/core/spawn.d.ts +0 -1
- package/dist/core/spawn.js +1 -4
- package/dist/core/substrate/on-read.js +1 -9
- package/dist/core/wake.js +2 -2
- package/dist/core/worktree.d.ts +3 -3
- package/dist/core/worktree.js +5 -7
- package/dist/daemon/api/bridge.js +1 -1
- package/dist/daemon/api/handlers/broker-ops.js +1 -2
- package/dist/daemon/api/handlers/messages.js +2 -2
- package/dist/daemon/api/handlers/nodes.js +1 -1
- package/dist/daemon/api/handlers/reports.js +1 -1
- package/dist/daemon/api/handlers/validate.d.ts +2 -1
- package/dist/daemon/api/handlers/validate.js +2 -3
- package/dist/daemon/cron-run.js +7 -7
- package/dist/daemon/human/finish.js +3 -2
- package/dist/daemon/manage.d.ts +6 -6
- package/dist/daemon/manage.js +15 -16
- package/dist/pi-extensions/canvas-context-intro.js +1 -14
- package/dist/pi-extensions/canvas-doc-substrate.js +1 -1
- package/dist/pi-extensions/canvas-review-boundary.js +1 -14
- package/dist/pi-extensions/truncate.d.ts +5 -0
- package/dist/pi-extensions/truncate.js +14 -0
- package/dist/shared/predicates.d.ts +2 -0
- package/dist/shared/predicates.js +4 -0
- package/dist/shared/shell-quote.d.ts +3 -0
- package/dist/shared/shell-quote.js +5 -0
- package/dist/shared/tool-groups.d.ts +4 -2
- package/dist/shared/tool-groups.js +3 -3
- package/dist/types.d.ts +4 -5
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -8,8 +8,7 @@ export declare function isPidAlive(pid: number | null | undefined): boolean;
|
|
|
8
8
|
* `composeIdentity` fingerprints are compared (this module's own
|
|
9
9
|
* `recordedPidLiveness`, `captureTeardownSnapshot`'s reuse check, and
|
|
10
10
|
* `killProcessTreePids`'s reuse check) — centralized so no call site can
|
|
11
|
-
* drift from this rule
|
|
12
|
-
* fix).
|
|
11
|
+
* drift from this rule.
|
|
13
12
|
*
|
|
14
13
|
* Two hazards this closes, both in the false-DEAD direction (the dangerous
|
|
15
14
|
* one — a false DEAD lets `reviveNode`'s double-launch guard relaunch a
|
|
@@ -94,7 +93,7 @@ export declare function recordedPidLiveness(pid: number | null | undefined, expe
|
|
|
94
93
|
* swallowing ESRCH (already gone). Used by cron cancellation, timeout, and
|
|
95
94
|
* stale-lease recovery. Lives beside isPidAlive: canvas/ is
|
|
96
95
|
* the lowest shared layer every process-liveness/-teardown primitive sits at.
|
|
97
|
-
* Guards `pid` before signaling
|
|
96
|
+
* Guards `pid` before signaling: never hit pid 0
|
|
98
97
|
* (which means "my own group") or a negative/non-integer value. */
|
|
99
98
|
export declare function killProcessGroup(pid: number, signal?: NodeJS.Signals): void;
|
|
100
99
|
/** The kernel `boot_id` (`/proc/sys/kernel/random/boot_id`) is stable for the
|
|
@@ -155,8 +154,8 @@ export interface PsLivenessSnapshot {
|
|
|
155
154
|
}
|
|
156
155
|
export declare function captureLivenessSnapshot(): PsLivenessSnapshot | null;
|
|
157
156
|
/** Every transitive descendant of `rootPid`, discovered by walking `ps`'s
|
|
158
|
-
* pid/ppid table (BFS) — NOT process-GROUP membership.
|
|
159
|
-
*
|
|
157
|
+
* pid/ppid table (BFS) — NOT process-GROUP membership. The pi SDK's bash tool
|
|
158
|
+
* spawns its shell child with
|
|
160
159
|
* `detached: true` (verified in `@earendil-works/pi-agent-core`'s
|
|
161
160
|
* `harness/env/nodejs.js`), which calls `setsid()` and puts that child in a
|
|
162
161
|
* brand-new process GROUP of its own — so `kill(-rootPid)` never reaches it.
|
|
@@ -173,7 +172,7 @@ export declare function descendantPids(rootPid: number): number[];
|
|
|
173
172
|
* batched `ps -p <list>` call. `lstart` is a standard format keyword on BOTH
|
|
174
173
|
* BSD ps (macOS) and procps-ng (Linux), so this needs no `/proc` parsing or
|
|
175
174
|
* other Linux-only machinery. Used to guard against PID REUSE during the
|
|
176
|
-
* multi-second teardown escalation ladder
|
|
175
|
+
* multi-second teardown escalation ladder: a
|
|
177
176
|
* captured pid can exit and the OS can recycle it for an unrelated process
|
|
178
177
|
* before the SIGKILL rung fires, and a bare `kill(pid, 0)` / `kill(pid, sig)`
|
|
179
178
|
* cannot tell the difference.
|
|
@@ -182,9 +181,9 @@ export declare function descendantPids(rootPid: number): number[];
|
|
|
182
181
|
* could not be spawned, or was killed by the timeout): a probe failure means
|
|
183
182
|
* we have NO information, which is a fundamentally different outcome from a
|
|
184
183
|
* probe that SUCCEEDED and simply found no row for a given pid (that pid is
|
|
185
|
-
* genuinely gone). Collapsing these two into the same empty map
|
|
186
|
-
*
|
|
187
|
-
*
|
|
184
|
+
* genuinely gone). Collapsing these two into the same empty map is unsafe:
|
|
185
|
+
* `killProcessTreePids` cannot tell "pid confirmed gone" from "we don't know"
|
|
186
|
+
* without this
|
|
188
187
|
* distinction, and misreading a failed probe as positive proof of
|
|
189
188
|
* absence/reuse either signals a reused stranger or — worse — silently skips
|
|
190
189
|
* reaping a real live orphan. A successful call may still return a map with
|
|
@@ -210,7 +209,7 @@ export declare function capturePidIdentities(pids: readonly number[]): Map<numbe
|
|
|
210
209
|
* signal. */
|
|
211
210
|
export declare function capturePidCommand(pid: number): string | null;
|
|
212
211
|
/** The full pre-signal snapshot `teardown()` needs, taken ONCE, from ONE `ps`
|
|
213
|
-
* table
|
|
212
|
+
* table: the process TREE rooted at `rootPid`
|
|
214
213
|
* (itself + every transitive descendant) alongside each member's identity
|
|
215
214
|
* fingerprint — PLUS, load-bearing, a launch-time-identity REUSE check on
|
|
216
215
|
* `rootPid` itself before the tree is even walked.
|
|
@@ -259,21 +258,20 @@ export declare function captureTeardownSnapshot(rootPid: number, expectedIdentit
|
|
|
259
258
|
* caller before sending any signal — see the doc on `isProcessTreeAlive` for
|
|
260
259
|
* why re-deriving via `ps` after signaling starts is unsafe). Signals EACH
|
|
261
260
|
* pid's OWN process group (a detached SDK-spawned descendant is its own
|
|
262
|
-
* group leader — the pi bash tool's shell child
|
|
261
|
+
* group leader — the pi bash tool's shell child)
|
|
263
262
|
* PLUS a plain `kill(pid)` on each (belt-and-suspenders for a pid that is
|
|
264
263
|
* NOT its own group leader). Every signal is best-effort — `killProcessGroup`
|
|
265
264
|
* swallows ESRCH and the plain `kill` here does too — so firing this against
|
|
266
265
|
* an already-fully-dead snapshot is a harmless no-op.
|
|
267
266
|
*
|
|
268
|
-
* `identities`, when supplied
|
|
267
|
+
* `identities`, when supplied, is a
|
|
269
268
|
* `capturePidIdentities` snapshot taken alongside the pid list at the SAME
|
|
270
269
|
* moment — `null` if that INITIAL capture's `ps` probe itself failed (see
|
|
271
270
|
* `capturePidIdentities`'s doc). Before signaling EITHER the group or the
|
|
272
271
|
* plain kill for a pid, this re-derives CURRENT identities in one fresh
|
|
273
272
|
* batched call and compares — but ONLY skips on POSITIVE proof of reuse; any
|
|
274
273
|
* failure or absence of evidence falls through to the normal signal path
|
|
275
|
-
* (fail-open, matching this module's
|
|
276
|
-
* crouter#98 signal-safety finding):
|
|
274
|
+
* (fail-open, matching this module's unguarded behavior):
|
|
277
275
|
*
|
|
278
276
|
* - baseline identity present AND the current probe SUCCEEDED AND the
|
|
279
277
|
* current identity for this pid is present AND DIFFERENT — the OS has
|
package/dist/core/canvas/pid.js
CHANGED
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
// LOWEST shared layer — so canvas/, runtime/, AND daemon/ can all import it
|
|
5
5
|
// "down" without a reverse-layer violation (runtime/ sits above canvas/, daemon/
|
|
6
6
|
// above runtime/). It is a pure process-existence utility, NOT data-model access,
|
|
7
|
-
// so it is exempt from the "only canvas.ts touches the db" rule.
|
|
8
|
-
//
|
|
9
|
-
// placement.ts, and crtrd.ts into one (Phase 3 review reuse MINOR-1).
|
|
7
|
+
// so it is exempt from the "only canvas.ts touches the db" rule. canvas.ts,
|
|
8
|
+
// revive.ts, placement.ts, and crtrd.ts all probe liveness through it.
|
|
10
9
|
import { spawnSync } from 'node:child_process';
|
|
11
10
|
import { readFileSync } from 'node:fs';
|
|
12
11
|
import { readKernelBootId } from './boot-id.js';
|
|
@@ -62,8 +61,7 @@ const BOOT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12
|
|
|
62
61
|
* purely-numeric suffix counts as ticks (the shape `procStartTicks` and
|
|
63
62
|
* `composeIdentity` guarantee); a malformed or empty suffix (`A#`, `A#bad`,
|
|
64
63
|
* `A#1#2`) is treated as ABSENT ticks, so the base compare then applies — a
|
|
65
|
-
* garbage suffix must never be trusted as a fine discriminator
|
|
66
|
-
* Minor). */
|
|
64
|
+
* garbage suffix must never be trusted as a fine discriminator. */
|
|
67
65
|
function splitIdentity(identity) {
|
|
68
66
|
const i = identity.indexOf('#');
|
|
69
67
|
if (i === -1)
|
|
@@ -75,8 +73,7 @@ function splitIdentity(identity) {
|
|
|
75
73
|
* `composeIdentity` fingerprints are compared (this module's own
|
|
76
74
|
* `recordedPidLiveness`, `captureTeardownSnapshot`'s reuse check, and
|
|
77
75
|
* `killProcessTreePids`'s reuse check) — centralized so no call site can
|
|
78
|
-
* drift from this rule
|
|
79
|
-
* fix).
|
|
76
|
+
* drift from this rule.
|
|
80
77
|
*
|
|
81
78
|
* Two hazards this closes, both in the false-DEAD direction (the dangerous
|
|
82
79
|
* one — a false DEAD lets `reviveNode`'s double-launch guard relaunch a
|
|
@@ -208,7 +205,7 @@ function logPidLivenessDecision(pid, isPidAliveResult, expectedIdentity, probeOu
|
|
|
208
205
|
* swallowing ESRCH (already gone). Used by cron cancellation, timeout, and
|
|
209
206
|
* stale-lease recovery. Lives beside isPidAlive: canvas/ is
|
|
210
207
|
* the lowest shared layer every process-liveness/-teardown primitive sits at.
|
|
211
|
-
* Guards `pid` before signaling
|
|
208
|
+
* Guards `pid` before signaling: never hit pid 0
|
|
212
209
|
* (which means "my own group") or a negative/non-integer value. */
|
|
213
210
|
export function killProcessGroup(pid, signal = 'SIGTERM') {
|
|
214
211
|
if (!Number.isInteger(pid) || pid <= 0)
|
|
@@ -220,8 +217,8 @@ export function killProcessGroup(pid, signal = 'SIGTERM') {
|
|
|
220
217
|
/* already gone */
|
|
221
218
|
}
|
|
222
219
|
}
|
|
223
|
-
/** Linux-only higher-resolution discriminator layered onto `lstart
|
|
224
|
-
*
|
|
220
|
+
/** Linux-only higher-resolution discriminator layered onto `lstart`:
|
|
221
|
+
* `ps lstart` is only second-granular, so a PID reused
|
|
225
222
|
* within the same wall-clock second compares equal and slips past the reuse
|
|
226
223
|
* guard. `/proc/<pid>/stat`'s `starttime` field is the kernel's own jiffies-
|
|
227
224
|
* since-boot counter for that process — effectively unique per pid even for
|
|
@@ -395,8 +392,8 @@ function descendantPidsFrom(rootPid, childrenOf) {
|
|
|
395
392
|
return out;
|
|
396
393
|
}
|
|
397
394
|
/** Every transitive descendant of `rootPid`, discovered by walking `ps`'s
|
|
398
|
-
* pid/ppid table (BFS) — NOT process-GROUP membership.
|
|
399
|
-
*
|
|
395
|
+
* pid/ppid table (BFS) — NOT process-GROUP membership. The pi SDK's bash tool
|
|
396
|
+
* spawns its shell child with
|
|
400
397
|
* `detached: true` (verified in `@earendil-works/pi-agent-core`'s
|
|
401
398
|
* `harness/env/nodejs.js`), which calls `setsid()` and puts that child in a
|
|
402
399
|
* brand-new process GROUP of its own — so `kill(-rootPid)` never reaches it.
|
|
@@ -420,7 +417,7 @@ export function descendantPids(rootPid) {
|
|
|
420
417
|
* batched `ps -p <list>` call. `lstart` is a standard format keyword on BOTH
|
|
421
418
|
* BSD ps (macOS) and procps-ng (Linux), so this needs no `/proc` parsing or
|
|
422
419
|
* other Linux-only machinery. Used to guard against PID REUSE during the
|
|
423
|
-
* multi-second teardown escalation ladder
|
|
420
|
+
* multi-second teardown escalation ladder: a
|
|
424
421
|
* captured pid can exit and the OS can recycle it for an unrelated process
|
|
425
422
|
* before the SIGKILL rung fires, and a bare `kill(pid, 0)` / `kill(pid, sig)`
|
|
426
423
|
* cannot tell the difference.
|
|
@@ -429,9 +426,9 @@ export function descendantPids(rootPid) {
|
|
|
429
426
|
* could not be spawned, or was killed by the timeout): a probe failure means
|
|
430
427
|
* we have NO information, which is a fundamentally different outcome from a
|
|
431
428
|
* probe that SUCCEEDED and simply found no row for a given pid (that pid is
|
|
432
|
-
* genuinely gone). Collapsing these two into the same empty map
|
|
433
|
-
*
|
|
434
|
-
*
|
|
429
|
+
* genuinely gone). Collapsing these two into the same empty map is unsafe:
|
|
430
|
+
* `killProcessTreePids` cannot tell "pid confirmed gone" from "we don't know"
|
|
431
|
+
* without this
|
|
435
432
|
* distinction, and misreading a failed probe as positive proof of
|
|
436
433
|
* absence/reuse either signals a reused stranger or — worse — silently skips
|
|
437
434
|
* reaping a real live orphan. A successful call may still return a map with
|
|
@@ -529,21 +526,20 @@ export function captureTeardownSnapshot(rootPid, expectedIdentity) {
|
|
|
529
526
|
* caller before sending any signal — see the doc on `isProcessTreeAlive` for
|
|
530
527
|
* why re-deriving via `ps` after signaling starts is unsafe). Signals EACH
|
|
531
528
|
* pid's OWN process group (a detached SDK-spawned descendant is its own
|
|
532
|
-
* group leader — the pi bash tool's shell child
|
|
529
|
+
* group leader — the pi bash tool's shell child)
|
|
533
530
|
* PLUS a plain `kill(pid)` on each (belt-and-suspenders for a pid that is
|
|
534
531
|
* NOT its own group leader). Every signal is best-effort — `killProcessGroup`
|
|
535
532
|
* swallows ESRCH and the plain `kill` here does too — so firing this against
|
|
536
533
|
* an already-fully-dead snapshot is a harmless no-op.
|
|
537
534
|
*
|
|
538
|
-
* `identities`, when supplied
|
|
535
|
+
* `identities`, when supplied, is a
|
|
539
536
|
* `capturePidIdentities` snapshot taken alongside the pid list at the SAME
|
|
540
537
|
* moment — `null` if that INITIAL capture's `ps` probe itself failed (see
|
|
541
538
|
* `capturePidIdentities`'s doc). Before signaling EITHER the group or the
|
|
542
539
|
* plain kill for a pid, this re-derives CURRENT identities in one fresh
|
|
543
540
|
* batched call and compares — but ONLY skips on POSITIVE proof of reuse; any
|
|
544
541
|
* failure or absence of evidence falls through to the normal signal path
|
|
545
|
-
* (fail-open, matching this module's
|
|
546
|
-
* crouter#98 signal-safety finding):
|
|
542
|
+
* (fail-open, matching this module's unguarded behavior):
|
|
547
543
|
*
|
|
548
544
|
* - baseline identity present AND the current probe SUCCEEDED AND the
|
|
549
545
|
* current identity for this pid is present AND DIFFERENT — the OS has
|
|
@@ -21,8 +21,8 @@ export function createRemoteExec(previewEndpoint, relayToken, timeoutMs = DEFAUL
|
|
|
21
21
|
// A malformed stored token (control chars / non-Latin1 bytes) would throw
|
|
22
22
|
// from the Headers constructor with the invalid value IN the thrown
|
|
23
23
|
// message — validate up front so that never happens, and fail with a
|
|
24
|
-
// GENERIC message instead
|
|
25
|
-
//
|
|
24
|
+
// GENERIC message instead: no token bytes may ever reach returned stderr.
|
|
25
|
+
// HTTP header values are ISO-8859-1; reject
|
|
26
26
|
// anything outside `/^[\x20-\x7e]+$/` (printable ASCII, no CR/LF/control).
|
|
27
27
|
const tokenValid = /^[\x20-\x7e]+$/.test(relayToken);
|
|
28
28
|
return async function exec(bin, args) {
|
|
@@ -28,7 +28,7 @@ export declare function resolveNodeVisual(status: NodeStatus, opts: {
|
|
|
28
28
|
* dashboard flags the node for the human. Exported so a recovery verb (e.g.
|
|
29
29
|
* `node lifecycle revive --now`, src/commands/node-lifecycle-revive.ts) can gate on the SAME
|
|
30
30
|
* predicate the dashboard advertises instead of drifting out of sync with it
|
|
31
|
-
*
|
|
31
|
+
* and rejecting exactly the nodes shown as "needs you". */
|
|
32
32
|
export declare function faultNeedsYou(fault: Pick<Fault, 'retry'>): boolean;
|
|
33
33
|
/** Human-friendly active-fault disposition text. `auto` faults show the current
|
|
34
34
|
* retry attempt plus the next scheduled retry; client-owned redials say
|
|
@@ -69,7 +69,7 @@ function retryCountdown(iso, now) {
|
|
|
69
69
|
* dashboard flags the node for the human. Exported so a recovery verb (e.g.
|
|
70
70
|
* `node lifecycle revive --now`, src/commands/node-lifecycle-revive.ts) can gate on the SAME
|
|
71
71
|
* predicate the dashboard advertises instead of drifting out of sync with it
|
|
72
|
-
*
|
|
72
|
+
* and rejecting exactly the nodes shown as "needs you". */
|
|
73
73
|
export function faultNeedsYou(fault) {
|
|
74
74
|
return fault.retry.disposition !== 'auto';
|
|
75
75
|
}
|
|
@@ -191,7 +191,7 @@ export interface NodeRuntime {
|
|
|
191
191
|
/** Stable process-identity fingerprint (`pid.ts`'s `composeIdentity`) captured
|
|
192
192
|
* at the SAME moment `pi_pid` is recorded (`recordPid`) — the LAUNCH-time
|
|
193
193
|
* baseline `headlessBrokerHost.teardown()` compares `pi_pid`'s CURRENT
|
|
194
|
-
* identity against before signaling anything
|
|
194
|
+
* identity against before signaling anything:
|
|
195
195
|
* a mismatch proves the OS has already recycled `pi_pid` for an unrelated
|
|
196
196
|
* process since this node launched it, and teardown must refuse to signal
|
|
197
197
|
* that stranger. `null` for a node booted before this field existed, or if
|
|
@@ -8,14 +8,12 @@
|
|
|
8
8
|
// This is pure validation: no fetch, no registration lookup, no file I/O beyond
|
|
9
9
|
// the raw manifest bytes passed in.
|
|
10
10
|
import { validateCommandNode } from './schema.js';
|
|
11
|
+
import { isRecord } from '../../shared/predicates.js';
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
13
|
// Validation helpers
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
const KEBAB = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
15
16
|
const REST_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
|
|
16
|
-
function isObject(v) {
|
|
17
|
-
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
18
|
-
}
|
|
19
17
|
function typeName(v) {
|
|
20
18
|
if (v === null)
|
|
21
19
|
return 'null';
|
|
@@ -38,7 +36,7 @@ export function validateCommandManifest(raw, options) {
|
|
|
38
36
|
issues.push({ code, message, received, expected, next, ...(path ? { path } : {}) });
|
|
39
37
|
};
|
|
40
38
|
// Top-level structure check
|
|
41
|
-
if (!
|
|
39
|
+
if (!isRecord(raw)) {
|
|
42
40
|
issue('command_manifest_invalid', 'manifest must be an object', typeName(raw), options.transport === 'http' ? '{ schemaVersion, baseUrl?, timeouts?, mounts }' : '{ schemaVersion, mounts }', 'Provide a valid JSON manifest object.');
|
|
43
41
|
return { issues };
|
|
44
42
|
}
|
|
@@ -126,7 +124,7 @@ export function validateCommandManifest(raw, options) {
|
|
|
126
124
|
// Timeouts validation
|
|
127
125
|
// ---------------------------------------------------------------------------
|
|
128
126
|
function validateTimeouts(raw, issue) {
|
|
129
|
-
if (!
|
|
127
|
+
if (!isRecord(raw)) {
|
|
130
128
|
issue('command_manifest_invalid', 'timeouts must be an object', typeName(raw), '{ connectMs?, requestMs?, streamIdleMs? }', 'Fix timeouts.', 'timeouts');
|
|
131
129
|
return null;
|
|
132
130
|
}
|
|
@@ -150,7 +148,7 @@ function validateTimeouts(raw, issue) {
|
|
|
150
148
|
}
|
|
151
149
|
function validateMount(raw, index, transport, issue) {
|
|
152
150
|
const path = `mounts[${index}]`;
|
|
153
|
-
if (!
|
|
151
|
+
if (!isRecord(raw)) {
|
|
154
152
|
issue('command_manifest_invalid', 'mount must be an object', typeName(raw), '{ parent, node }', 'Fix the mount.', path);
|
|
155
153
|
return null;
|
|
156
154
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Unified command-manifest schema. Transport is the sole leaf-dialect discriminator.
|
|
2
|
+
import { isRecord } from '../../shared/predicates.js';
|
|
2
3
|
// ---------------------------------------------------------------------------
|
|
3
4
|
// Validation helpers
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
@@ -41,9 +42,6 @@ const OUTPUT_KEYS = new Set(['name', 'type', 'required', 'constraint']);
|
|
|
41
42
|
* may name, so a typo'd lowercase key fails at authoring time rather than
|
|
42
43
|
* silently never resolving. */
|
|
43
44
|
const ENV_VAR_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
44
|
-
function isObject(v) {
|
|
45
|
-
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
46
|
-
}
|
|
47
45
|
function typeName(v) {
|
|
48
46
|
if (v === null)
|
|
49
47
|
return 'null';
|
|
@@ -60,7 +58,7 @@ function checkKeys(raw, allowed, path, issue) {
|
|
|
60
58
|
return true;
|
|
61
59
|
}
|
|
62
60
|
function validateRootEntry(raw, path, issue) {
|
|
63
|
-
if (!
|
|
61
|
+
if (!isRecord(raw)) {
|
|
64
62
|
issue('command_node_invalid', `rootEntry must be an object`, typeName(raw), 'a { concept, description, whenToUse } object', 'Fix rootEntry.', path);
|
|
65
63
|
return null;
|
|
66
64
|
}
|
|
@@ -91,7 +89,7 @@ function validateParams(raw, path, issue) {
|
|
|
91
89
|
for (let i = 0; i < raw.length; i++) {
|
|
92
90
|
const p = raw[i];
|
|
93
91
|
const pp = `${path}[${i}]`;
|
|
94
|
-
if (!
|
|
92
|
+
if (!isRecord(p)) {
|
|
95
93
|
issue('command_node_invalid', `param must be an object`, typeName(p), 'a param object', 'Fix the param.', pp);
|
|
96
94
|
return null;
|
|
97
95
|
}
|
|
@@ -289,7 +287,7 @@ function validateOutput(raw, path, issue) {
|
|
|
289
287
|
for (let i = 0; i < raw.length; i++) {
|
|
290
288
|
const f = raw[i];
|
|
291
289
|
const fp = `${path}[${i}]`;
|
|
292
|
-
if (!
|
|
290
|
+
if (!isRecord(f)) {
|
|
293
291
|
issue('command_node_invalid', `output field must be an object`, typeName(f), 'a { name, type, required, constraint } object', 'Fix the output field.', fp);
|
|
294
292
|
return null;
|
|
295
293
|
}
|
|
@@ -337,7 +335,7 @@ function checkCommon(raw, path, issue) {
|
|
|
337
335
|
}
|
|
338
336
|
export function validateCommandNode(raw, path, topLevel, transport, issue) {
|
|
339
337
|
const pathString = path.join('.');
|
|
340
|
-
if (!
|
|
338
|
+
if (!isRecord(raw)) {
|
|
341
339
|
issue('command_node_invalid', 'node must be an object', typeName(raw), 'a branch or leaf node object', 'Fix the node.', pathString);
|
|
342
340
|
return null;
|
|
343
341
|
}
|
|
@@ -424,7 +422,7 @@ function validateBranch(raw, path, topLevel, transport, issue) {
|
|
|
424
422
|
}
|
|
425
423
|
/** `passthrough: { bin, installHint }` — both required non-empty strings. */
|
|
426
424
|
function validatePassthrough(raw, path, issue) {
|
|
427
|
-
if (!
|
|
425
|
+
if (!isRecord(raw)) {
|
|
428
426
|
issue('command_node_invalid', 'passthrough must be an object', typeName(raw), 'an object { bin, installHint }', 'Fix or remove passthrough.', path);
|
|
429
427
|
return null;
|
|
430
428
|
}
|
|
@@ -482,7 +480,7 @@ const HTTP_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
|
482
480
|
const REST_PARAM_PLACEMENT_SET = new Set(['path', 'query', 'body', 'header']);
|
|
483
481
|
function validateRestMapping(raw, basePath, params, issue) {
|
|
484
482
|
const path = `${basePath}.rest`;
|
|
485
|
-
if (!
|
|
483
|
+
if (!isRecord(raw)) {
|
|
486
484
|
issue('command_rest_invalid', `rest must be an object`, typeName(raw), '{ method, path, streaming?, params }', 'Provide a valid rest mapping.', path);
|
|
487
485
|
return null;
|
|
488
486
|
}
|
|
@@ -523,7 +521,7 @@ function validateRestMapping(raw, basePath, params, issue) {
|
|
|
523
521
|
}
|
|
524
522
|
// Validate params mapping (required object)
|
|
525
523
|
const restParams = raw['params'];
|
|
526
|
-
if (!
|
|
524
|
+
if (!isRecord(restParams)) {
|
|
527
525
|
issue('command_rest_invalid', `params must be an object`, typeName(restParams), 'a mapping of param names to placement specs', 'Provide params.', `${path}.params`);
|
|
528
526
|
return null;
|
|
529
527
|
}
|
|
@@ -542,7 +540,7 @@ function validateRestMapping(raw, basePath, params, issue) {
|
|
|
542
540
|
return null;
|
|
543
541
|
}
|
|
544
542
|
declaredNames.add(paramName);
|
|
545
|
-
if (!
|
|
543
|
+
if (!isRecord(mapping)) {
|
|
546
544
|
issue('command_rest_invalid', `param mapping must be an object`, typeName(mapping), '{ in, as? }', 'Fix the param mapping.', `${path}.params.${paramName}`);
|
|
547
545
|
return null;
|
|
548
546
|
}
|
|
@@ -644,7 +642,7 @@ function validateRestMapping(raw, basePath, params, issue) {
|
|
|
644
642
|
return null;
|
|
645
643
|
}
|
|
646
644
|
const rawBody = raw['body'];
|
|
647
|
-
if (!
|
|
645
|
+
if (!isRecord(rawBody)) {
|
|
648
646
|
issue('command_rest_invalid', `body must be an object`, typeName(rawBody), 'a map of constant literal body fields (string | number | boolean)', 'Fix body.', `${path}.body`);
|
|
649
647
|
return null;
|
|
650
648
|
}
|
|
@@ -11,6 +11,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
11
11
|
import { CrtrError } from '../../errors.js';
|
|
12
12
|
import { diag } from '../../io.js';
|
|
13
13
|
import { ExitCode } from '../../../types.js';
|
|
14
|
+
import { isRecord } from '../../../shared/predicates.js';
|
|
14
15
|
const MAX_STDOUT = 10 * 1024 * 1024; // 10 MiB hard cap on plugin stdout.
|
|
15
16
|
const RESERVED_CODES = new Set(['internal', 'unknown_path', 'command_collision', 'plugin_protocol_error']);
|
|
16
17
|
// Strict snake_case: no leading/trailing underscore, no consecutive underscores.
|
|
@@ -77,7 +78,7 @@ export function executeExternalLeaf(spec, input) {
|
|
|
77
78
|
catch {
|
|
78
79
|
throw protocolError(spec, `stdout was not a single JSON envelope`, 'exactly one JSON envelope on stdout');
|
|
79
80
|
}
|
|
80
|
-
if (!
|
|
81
|
+
if (!isRecord(envelope)) {
|
|
81
82
|
throw protocolError(spec, `stdout was not a JSON object`, 'a { protocolVersion: 1, ok: boolean, ... } envelope');
|
|
82
83
|
}
|
|
83
84
|
if (envelope['protocolVersion'] !== 1) {
|
|
@@ -96,7 +97,7 @@ export function executeExternalLeaf(spec, input) {
|
|
|
96
97
|
}
|
|
97
98
|
if (ok) {
|
|
98
99
|
const result = envelope['result'];
|
|
99
|
-
if (!
|
|
100
|
+
if (!isRecord(result)) {
|
|
100
101
|
throw protocolError(spec, `success envelope has no result object`, 'result: an object matching the declared output');
|
|
101
102
|
}
|
|
102
103
|
validateResult(spec, result);
|
|
@@ -104,7 +105,7 @@ export function executeExternalLeaf(spec, input) {
|
|
|
104
105
|
}
|
|
105
106
|
// Recoverable command failure → structured CrtrError with the plugin's code.
|
|
106
107
|
const error = envelope['error'];
|
|
107
|
-
if (!
|
|
108
|
+
if (!isRecord(error) || typeof error['code'] !== 'string' || typeof error['message'] !== 'string') {
|
|
108
109
|
throw protocolError(spec, `error envelope is missing a { code, message }`, 'error: { code, message, ... }');
|
|
109
110
|
}
|
|
110
111
|
const code = error['code'];
|
|
@@ -146,7 +147,7 @@ function typeMatches(type, v) {
|
|
|
146
147
|
if (t === 'array' || t.endsWith('[]'))
|
|
147
148
|
return Array.isArray(v);
|
|
148
149
|
if (t === 'object')
|
|
149
|
-
return
|
|
150
|
+
return isRecord(v);
|
|
150
151
|
return true;
|
|
151
152
|
}
|
|
152
153
|
function protocolError(spec, received, expected) {
|
|
@@ -155,6 +156,3 @@ function protocolError(spec, received, expected) {
|
|
|
155
156
|
function label(spec) {
|
|
156
157
|
return `plugin "${spec.plugin.name}" command \`${spec.commandPath.join(' ')}\``;
|
|
157
158
|
}
|
|
158
|
-
function isObject(v) {
|
|
159
|
-
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
160
|
-
}
|
|
@@ -19,6 +19,7 @@ import { validateDeclaredResult } from './exec-invoke.js';
|
|
|
19
19
|
import { CrtrError } from '../../errors.js';
|
|
20
20
|
import { diag, recordPreviewError, recordPreviewJsonLine, writeStdout } from '../../io.js';
|
|
21
21
|
import { ExitCode } from '../../../types.js';
|
|
22
|
+
import { isRecord } from '../../../shared/predicates.js';
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
// Constants
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
@@ -320,7 +321,7 @@ function mapUnary2xx(spec, body) {
|
|
|
320
321
|
catch {
|
|
321
322
|
throw protocolError(spec, 'response body was not valid JSON', 'a JSON object matching the declared output');
|
|
322
323
|
}
|
|
323
|
-
if (!
|
|
324
|
+
if (!isRecord(parsed)) {
|
|
324
325
|
throw protocolError(spec, `response body was not a JSON object (got ${typeName(parsed)})`, 'a JSON object matching the declared output');
|
|
325
326
|
}
|
|
326
327
|
const issue = validateDeclaredResult(spec.output, parsed);
|
|
@@ -482,9 +483,9 @@ function throwNon2xx(spec, status, body) {
|
|
|
482
483
|
catch {
|
|
483
484
|
parsed = undefined;
|
|
484
485
|
}
|
|
485
|
-
if (
|
|
486
|
+
if (isRecord(parsed)) {
|
|
486
487
|
const err = parsed['error'];
|
|
487
|
-
if (
|
|
488
|
+
if (isRecord(err) && typeof err['code'] === 'string' && typeof err['message'] === 'string') {
|
|
488
489
|
const code = err['code'];
|
|
489
490
|
const message = err['message'];
|
|
490
491
|
const field = typeof err['field'] === 'string' ? err['field'] : undefined;
|
|
@@ -543,9 +544,6 @@ function readAll(res) {
|
|
|
543
544
|
res.on('error', () => resolve(Buffer.concat(chunks).toString('utf8'))); // best-effort
|
|
544
545
|
});
|
|
545
546
|
}
|
|
546
|
-
function isObject(v) {
|
|
547
|
-
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
548
|
-
}
|
|
549
547
|
function typeName(v) {
|
|
550
548
|
if (v === null)
|
|
551
549
|
return 'null';
|
package/dist/core/config.d.ts
CHANGED
|
@@ -81,7 +81,7 @@ export declare function readMergedLaunchConfig(targetCwd?: string, targetProfile
|
|
|
81
81
|
* `developer`, or sub-kind, e.g. `plan/reviewers/security`), across
|
|
82
82
|
* project > user > builtin precedence. Returns `undefined` for a kind no
|
|
83
83
|
* scope registers — existence is deliberately NOT validated here (kind
|
|
84
|
-
* existence/launch-menu enumeration is a
|
|
84
|
+
* existence/launch-menu enumeration is a caller concern); this only
|
|
85
85
|
* resolves the config for a kind the caller already knows about. */
|
|
86
86
|
export declare function resolveKindConfig(kind: string): KindConfig | undefined;
|
|
87
87
|
/** The sub-kinds available to spawn FROM a given top-level kind — every
|
package/dist/core/config.js
CHANGED
|
@@ -560,7 +560,7 @@ export function readMergedLaunchConfig(targetCwd = process.cwd(), targetProfileI
|
|
|
560
560
|
* `developer`, or sub-kind, e.g. `plan/reviewers/security`), across
|
|
561
561
|
* project > user > builtin precedence. Returns `undefined` for a kind no
|
|
562
562
|
* scope registers — existence is deliberately NOT validated here (kind
|
|
563
|
-
* existence/launch-menu enumeration is a
|
|
563
|
+
* existence/launch-menu enumeration is a caller concern); this only
|
|
564
564
|
* resolves the config for a kind the caller already knows about. */
|
|
565
565
|
export function resolveKindConfig(kind) {
|
|
566
566
|
return readMergedLaunchConfig().kinds[kind];
|
|
@@ -622,7 +622,7 @@ function sparseModelLaddersToPersist(rawLadders, baseline, mutated) {
|
|
|
622
622
|
* sparse override stays sparse), and only keys the mutation actually changed
|
|
623
623
|
* are written. `modelLadders` gets sub-field-granular sparse handling via
|
|
624
624
|
* `sparseModelLaddersToPersist`. Result: a fresh bootstrap or any unrelated
|
|
625
|
-
* `sys config set`
|
|
625
|
+
* `sys config set` never freezes a full concrete `modelLadders` to disk,
|
|
626
626
|
* and a direct ladder-rung set still persists only the touched rung -- all
|
|
627
627
|
* through one lock-protected atomic update path. */
|
|
628
628
|
function mutateConfigAtPath(path, mutate) {
|
package/dist/core/events/read.js
CHANGED
|
@@ -4,6 +4,7 @@ import { crtrHome, isSafeNodeId, nodesRoot } from '../canvas/paths.js';
|
|
|
4
4
|
import { validateOperationId } from './operation-id.js';
|
|
5
5
|
import { MAX_EVENT_RECORD_BYTES } from './serialize.js';
|
|
6
6
|
import { errorClassCodes, } from './types.js';
|
|
7
|
+
import { isRecord } from '../../shared/predicates.js';
|
|
7
8
|
const SNAPSHOT_ITEMS = 200;
|
|
8
9
|
const MAX_CANONICAL_SOURCE_BYTES = 6 * 1024 * 1024;
|
|
9
10
|
const MAX_RAW_SOURCE_BYTES = 256 * 1024;
|
|
@@ -41,9 +42,6 @@ const RETRY_DISPOSITIONS = new Set(['auto', 'manual', 'fatal']);
|
|
|
41
42
|
const ERROR_CLASSES = new Set(errorClassCodes);
|
|
42
43
|
const LEVEL_RANK = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
43
44
|
const GENERATION_RANK = { 2: 0, 1: 1, 0: 2 };
|
|
44
|
-
function isRecord(value) {
|
|
45
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
46
|
-
}
|
|
47
45
|
function hasOwn(value, key) {
|
|
48
46
|
return Object.prototype.hasOwnProperty.call(value, key);
|
|
49
47
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hasManagedProviderCoolingDiagnostic } from './runtime/managed-provider-cooling.js';
|
|
2
|
+
import { isRecord } from '../shared/predicates.js';
|
|
2
3
|
const errorClassByFaultKind = {
|
|
3
4
|
'rate-limit': 'rate_limit',
|
|
4
5
|
overloaded: 'overloaded',
|
|
@@ -16,9 +17,6 @@ export function errorClassFromFault(fault) {
|
|
|
16
17
|
retry_disposition: fault.retry.disposition,
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
|
-
function isRecord(value) {
|
|
20
|
-
return typeof value === 'object' && value !== null;
|
|
21
|
-
}
|
|
22
20
|
function text(value) {
|
|
23
21
|
return typeof value === 'string' ? value : '';
|
|
24
22
|
}
|
package/dist/core/fs-utils.d.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
/** `realpathSync`, tolerant of a path that doesn't exist or can't be resolved. */
|
|
2
|
+
export declare function realpathOrSelf(p: string): string;
|
|
3
|
+
/** Expand a leading `~` against `homeDir` (pi's own convention). Other forms
|
|
4
|
+
* — absolute, relative, `~user` — pass through as pi leaves them. */
|
|
5
|
+
export declare function expandTilde(p: string, homeDir?: string): string;
|
|
1
6
|
export declare function ensureDir(dir: string): void;
|
|
2
7
|
export declare function writeJson(path: string, data: unknown): void;
|
|
3
8
|
export declare function atomicWriteJson(path: string, data: unknown): void;
|
|
4
9
|
export declare function atomicWriteText(path: string, content: string | Uint8Array): void;
|
|
5
10
|
export declare function readJson<T = unknown>(path: string): T;
|
|
6
11
|
export declare function readJsonIfExists<T = unknown>(path: string): T | null;
|
|
12
|
+
/** Missing or corrupt reads as absent, so a crash artifact never throws. */
|
|
13
|
+
export declare function readJsonOrNull<T = unknown>(path: string): T | null;
|
|
7
14
|
export declare function readTextIfExists(path: string): string | null;
|
|
8
15
|
export declare function readText(path: string): string;
|
|
9
16
|
export declare function writeText(path: string, content: string): void;
|
package/dist/core/fs-utils.js
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync, cpSync, readlinkSync, renameSync, chmodSync, } from 'node:fs';
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync, cpSync, readlinkSync, renameSync, chmodSync, realpathSync, } from 'node:fs';
|
|
2
2
|
import { dirname, join, relative } from 'node:path';
|
|
3
|
-
import { platform } from 'node:os';
|
|
3
|
+
import { homedir, platform } from 'node:os';
|
|
4
|
+
/** `realpathSync`, tolerant of a path that doesn't exist or can't be resolved. */
|
|
5
|
+
export function realpathOrSelf(p) {
|
|
6
|
+
try {
|
|
7
|
+
return realpathSync(p);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return p;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Expand a leading `~` against `homeDir` (pi's own convention). Other forms
|
|
14
|
+
* — absolute, relative, `~user` — pass through as pi leaves them. */
|
|
15
|
+
export function expandTilde(p, homeDir = homedir()) {
|
|
16
|
+
if (p === '~')
|
|
17
|
+
return homeDir;
|
|
18
|
+
if (p.startsWith('~/') || (platform() === 'win32' && p.startsWith('~\\')))
|
|
19
|
+
return join(homeDir, p.slice(2));
|
|
20
|
+
return p;
|
|
21
|
+
}
|
|
4
22
|
export function ensureDir(dir) {
|
|
5
23
|
mkdirSync(dir, { recursive: true });
|
|
6
24
|
}
|
|
@@ -44,6 +62,15 @@ export function readJsonIfExists(path) {
|
|
|
44
62
|
return null;
|
|
45
63
|
return readJson(path);
|
|
46
64
|
}
|
|
65
|
+
/** Missing or corrupt reads as absent, so a crash artifact never throws. */
|
|
66
|
+
export function readJsonOrNull(path) {
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
47
74
|
export function readTextIfExists(path) {
|
|
48
75
|
if (!existsSync(path))
|
|
49
76
|
return null;
|
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
* generated slash-command templates to `~/.claude/commands` and
|
|
4
4
|
* `~/.pi/agent/prompts`.
|
|
5
5
|
*
|
|
6
|
-
* crouter no
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* user-owned and never removed.
|
|
6
|
+
* crouter exports no Agent Skills / SKILL.md bundles. The export pass prunes
|
|
7
|
+
* marker-bearing `crtr-skills` bundles so hosts stop surfacing pi/Claude native
|
|
8
|
+
* skills. Markerless files are user-owned and never removed.
|
|
10
9
|
*
|
|
11
|
-
* Triggered per-run by cli.ts (argv-gated
|
|
10
|
+
* Triggered per-run by cli.ts (argv-gated) so
|
|
12
11
|
* first-run provisioning is preserved. Best-effort: a provisioning fault must
|
|
13
12
|
* never break an unrelated `crtr` command (the deliberate, pre-existing
|
|
14
13
|
* behavior of the deleted writers).
|
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
* generated slash-command templates to `~/.claude/commands` and
|
|
4
4
|
* `~/.pi/agent/prompts`.
|
|
5
5
|
*
|
|
6
|
-
* crouter no
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* user-owned and never removed.
|
|
6
|
+
* crouter exports no Agent Skills / SKILL.md bundles. The export pass prunes
|
|
7
|
+
* marker-bearing `crtr-skills` bundles so hosts stop surfacing pi/Claude native
|
|
8
|
+
* skills. Markerless files are user-owned and never removed.
|
|
10
9
|
*
|
|
11
|
-
* Triggered per-run by cli.ts (argv-gated
|
|
10
|
+
* Triggered per-run by cli.ts (argv-gated) so
|
|
12
11
|
* first-run provisioning is preserved. Best-effort: a provisioning fault must
|
|
13
12
|
* never break an unrelated `crtr` command (the deliberate, pre-existing
|
|
14
13
|
* behavior of the deleted writers).
|