@north-light/crouter 0.3.220 → 0.3.221
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 +9 -0
- package/dist/api/client.js +10 -0
- package/dist/api/dto/chat-inventory.d.ts +43 -0
- package/dist/api/dto/chat-inventory.js +11 -0
- package/dist/api/dto/profiles.d.ts +19 -5
- package/dist/api/dto/profiles.js +2 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/build-root.d.ts +2 -6
- package/dist/build-root.js +51 -4
- package/dist/builtin-memory/internal/agent-shaping.md +3 -1
- package/dist/builtin-memory/internal/memory-loading.md +4 -0
- package/dist/builtin-memory/plan/roadmap.md +7 -1
- package/dist/builtin-memory/spec/guide.md +7 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +7 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +7 -2
- package/dist/clients/attach/__tests__/ref-autocomplete.test.js +1 -1
- package/dist/clients/attach/__tests__/titled-editor-preview.test.js +1 -1
- package/dist/clients/attach/overlays/file-review.js +2 -2
- package/dist/clients/attach/session/keys.d.ts +1 -1
- package/dist/clients/attach/session/profile-files.js +1 -1
- package/dist/clients/attach/viewer.js +690 -690
- package/dist/clients/inbox/review/launch.d.ts +8 -4
- package/dist/clients/inbox/review/launch.js +55 -5
- package/dist/clients/inbox/review/review-client.d.ts +1 -0
- package/dist/clients/inbox/review/review-client.js +4 -0
- package/dist/clients/inbox/review-adapter.d.ts +1 -8
- package/dist/clients/inbox/review-adapter.js +4 -52
- package/dist/commands/memory/lint.js +2 -1
- package/dist/commands/memory/read.js +1 -0
- package/dist/commands/memory.js +1 -1
- package/dist/commands/pkg/market-manage.js +165 -75
- package/dist/commands/pkg/plugin-inspect.js +19 -2
- package/dist/commands/pkg/plugin-manage.d.ts +8 -3
- package/dist/commands/pkg/plugin-manage.js +72 -24
- package/dist/commands/profile/default.js +6 -10
- package/dist/commands/profile/list.js +5 -3
- package/dist/commands/profile/new.js +21 -8
- package/dist/commands/profile/project.js +25 -19
- package/dist/commands/profile/show.js +3 -3
- package/dist/commands/surface-inbox.js +1 -0
- package/dist/commands/sys/__tests__/migrate.test.js +16 -5
- package/dist/commands/sys/doctor.js +35 -5
- package/dist/commands/sys/migrate.js +38 -19
- package/dist/commands/sys/setup-core.js +1 -1
- package/dist/commands/sys/sync-project-guidance.js +1 -1
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +1 -1
- package/dist/core/__tests__/fixtures/c5-command-boundary-ext.js +24 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -18
- package/dist/core/__tests__/fixtures/fake-engine.js +8 -1
- package/dist/core/__tests__/inline-memory-refs.test.js +36 -2
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +217 -0
- package/dist/core/__tests__/serial/broker-sdk-wiring.test.js +102 -2
- package/dist/core/bootstrap.js +6 -0
- package/dist/core/canvas/browse/app.js +5 -2
- package/dist/core/canvas/browse/model.d.ts +25 -15
- package/dist/core/canvas/browse/model.js +86 -65
- package/dist/core/canvas/render-source.d.ts +6 -0
- package/dist/core/canvas/render-source.js +7 -1
- package/dist/core/canvas/render.js +10 -2
- package/dist/core/command-hooks/artifact.d.ts +10 -0
- package/dist/core/command-hooks/artifact.js +129 -0
- package/dist/core/command-hooks/catalog.d.ts +14 -0
- package/dist/core/command-hooks/catalog.js +38 -0
- package/dist/core/command-hooks/compose.d.ts +15 -0
- package/dist/core/command-hooks/compose.js +99 -0
- package/dist/core/command-hooks/discovery.d.ts +87 -0
- package/dist/core/command-hooks/discovery.js +174 -0
- package/dist/core/command-hooks/help.d.ts +5 -0
- package/dist/core/command-hooks/help.js +18 -0
- package/dist/core/command-hooks/index.d.ts +6 -0
- package/dist/core/command-hooks/index.js +6 -0
- package/dist/core/command-hooks/report.d.ts +23 -0
- package/dist/core/command-hooks/report.js +19 -0
- package/dist/core/command-hooks/schema.d.ts +27 -0
- package/dist/core/command-hooks/schema.js +68 -0
- package/dist/core/command-hooks/transport/exec-invoke.d.ts +22 -0
- package/dist/core/command-hooks/transport/exec-invoke.js +274 -0
- package/dist/core/command-plugins/presence.d.ts +2 -0
- package/dist/core/command-plugins/presence.js +17 -0
- package/dist/core/command-plugins/transport/exec-invoke.d.ts +5 -0
- package/dist/core/command-plugins/transport/exec-invoke.js +58 -5
- package/dist/core/command.d.ts +8 -1
- package/dist/core/command.js +12 -10
- package/dist/core/help.d.ts +7 -1
- package/dist/core/io.d.ts +9 -1
- package/dist/core/io.js +44 -2
- package/dist/core/memory/inline-ref-inventory.d.ts +2 -1
- package/dist/core/memory/inline-ref-inventory.js +15 -8
- package/dist/core/memory-resolver.d.ts +13 -1
- package/dist/core/memory-resolver.js +25 -19
- package/dist/core/profiles/manifest.d.ts +13 -2
- package/dist/core/profiles/manifest.js +84 -18
- package/dist/core/profiles/select.js +9 -9
- package/dist/core/render.js +11 -0
- package/dist/core/runtime/advertised-command-invocation.d.ts +20 -0
- package/dist/core/runtime/advertised-command-invocation.js +233 -0
- package/dist/core/runtime/bearings.js +1 -1
- package/dist/core/runtime/broker/event-projection.js +7 -0
- package/dist/core/runtime/broker/frame-dispatch.d.ts +1 -1
- package/dist/core/runtime/broker/frame-dispatch.js +15 -10
- package/dist/core/runtime/broker/read-ops.d.ts +4 -0
- package/dist/core/runtime/broker/read-ops.js +6 -2
- package/dist/core/runtime/broker-extension-render.js +1 -1
- package/dist/core/runtime/broker-inventory.d.ts +5 -0
- package/dist/core/runtime/broker-inventory.js +191 -0
- package/dist/core/runtime/broker-protocol.d.ts +9 -2
- package/dist/core/runtime/broker.js +10 -1
- package/dist/core/runtime/command-surface.d.ts +33 -0
- package/dist/core/runtime/command-surface.js +81 -0
- package/dist/core/runtime/node-read.js +5 -0
- package/dist/core/scope.d.ts +26 -1
- package/dist/core/scope.js +52 -12
- package/dist/core/substrate/on-read.d.ts +7 -1
- package/dist/core/substrate/on-read.js +13 -4
- package/dist/core/substrate/render.js +14 -5
- package/dist/core/substrate/schema.d.ts +11 -1
- package/dist/core/substrate/schema.js +11 -2
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +4 -4
- package/dist/daemon/api/handlers/chat-inventory.d.ts +2 -0
- package/dist/daemon/api/handlers/chat-inventory.js +25 -0
- package/dist/daemon/api/handlers/profiles.js +7 -1
- package/dist/daemon/api/map.d.ts +2 -1
- package/dist/daemon/api/map.js +3 -2
- package/dist/daemon/api/server.js +2 -0
- package/dist/hook-authoring.d.ts +75 -0
- package/dist/hook-authoring.js +358 -0
- package/dist/hook-process.d.ts +7 -0
- package/dist/hook-process.js +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/migrations/002-profile-project-memory.d.ts +2 -0
- package/dist/migrations/002-profile-project-memory.js +71 -0
- package/dist/migrations/profile-manifests.d.ts +30 -0
- package/dist/migrations/profile-manifests.js +70 -0
- package/dist/migrations/registry.js +10 -5
- package/dist/migrations/types.d.ts +28 -1
- package/dist/migrations/types.js +15 -9
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +21 -4
- package/dist/pi-extensions/canvas-structured-output.js +85 -2
- package/dist/types.d.ts +15 -6
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/clients/attach/__tests__/file-review-focus.test.js +0 -49
- /package/dist/{clients/attach/__tests__/file-review-focus.test.d.ts → core/__tests__/profile-project-memory-delivery.test.d.ts} +0 -0
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// GET /v1/profiles/:name resolve one profile by name or id
|
|
8
8
|
// DELETE /v1/profiles/:name required-force cascade or detach
|
|
9
9
|
import { assertInstalledKind } from '../../../core/config.js';
|
|
10
|
-
import { createProfile, ensureRootProfile, listProfiles, loadProfileManifest, } from '../../../core/profiles/manifest.js';
|
|
10
|
+
import { assertProfileProjects, createProfile, ensureRootProfile, listProfiles, loadProfileManifest, } from '../../../core/profiles/manifest.js';
|
|
11
11
|
import { requestProfileDeletion } from '../../profile-delete.js';
|
|
12
12
|
import { toProfileDTO } from '../map.js';
|
|
13
13
|
const ROOT_PROFILE_NAME = 'root';
|
|
@@ -19,6 +19,12 @@ const ROOT_PROFILE_NAME = 'root';
|
|
|
19
19
|
function handleEnsure(ctx) {
|
|
20
20
|
const name = ctx.params['name'];
|
|
21
21
|
const req = (ctx.body ?? {});
|
|
22
|
+
// Shape check runs before the idempotent returns, so a legacy string array
|
|
23
|
+
// cannot slip through an ensure of root or an existing profile. Existence
|
|
24
|
+
// and realpath normalization stay create-only, so a valid entry naming a
|
|
25
|
+
// since-deleted directory still returns the existing profile.
|
|
26
|
+
if (req.projects !== undefined)
|
|
27
|
+
assertProfileProjects(req.projects);
|
|
22
28
|
if (name === ROOT_PROFILE_NAME) {
|
|
23
29
|
return { status: 200, body: toProfileDTO(ensureRootProfile()) };
|
|
24
30
|
}
|
package/dist/daemon/api/map.d.ts
CHANGED
|
@@ -60,7 +60,8 @@ export declare function toCronRunDTO(r: CronRunRecord): CronRunDTO;
|
|
|
60
60
|
/** Project a profile-manifest entry into its DTO. `default_dir` (the directory
|
|
61
61
|
* this profile is pinned as default for) is a reverse lookup not carried on the
|
|
62
62
|
* entry, so it is left null here — a caller that needs it resolves it
|
|
63
|
-
* separately.
|
|
63
|
+
* separately. Project entries are cloned: the manifest entry is shared cache
|
|
64
|
+
* state, so handing out its objects would let a DTO consumer mutate it. */
|
|
64
65
|
export declare function toProfileDTO(entry: ProfileEntry): ProfileDTO;
|
|
65
66
|
/** Extra daemon-derived review fields whose source is outside the canonical row. */
|
|
66
67
|
export interface ReviewDTOExtras {
|
package/dist/daemon/api/map.js
CHANGED
|
@@ -258,12 +258,13 @@ export function toCronRunDTO(r) {
|
|
|
258
258
|
/** Project a profile-manifest entry into its DTO. `default_dir` (the directory
|
|
259
259
|
* this profile is pinned as default for) is a reverse lookup not carried on the
|
|
260
260
|
* entry, so it is left null here — a caller that needs it resolves it
|
|
261
|
-
* separately.
|
|
261
|
+
* separately. Project entries are cloned: the manifest entry is shared cache
|
|
262
|
+
* state, so handing out its objects would let a DTO consumer mutate it. */
|
|
262
263
|
export function toProfileDTO(entry) {
|
|
263
264
|
return {
|
|
264
265
|
id: entry.profileId,
|
|
265
266
|
name: entry.manifest.name,
|
|
266
|
-
projects:
|
|
267
|
+
projects: entry.manifest.projects.map((p) => ({ ...p })),
|
|
267
268
|
home: entry.manifest.home,
|
|
268
269
|
paused_at: entry.manifest.paused_at,
|
|
269
270
|
default_kind: entry.manifest.default_kind ?? 'general',
|
|
@@ -22,6 +22,7 @@ import { attachRoutes } from './handlers/attach.js';
|
|
|
22
22
|
import { bashJobRoutes } from './handlers/bash-jobs.js';
|
|
23
23
|
import { brokerOperationRoutes } from './handlers/broker-ops.js';
|
|
24
24
|
import { canvasRoutes } from './handlers/canvas.js';
|
|
25
|
+
import { chatInventoryRoutes } from './handlers/chat-inventory.js';
|
|
25
26
|
import { daemonRoutes } from './handlers/daemon.js';
|
|
26
27
|
import { fileRoutes } from './handlers/files.js';
|
|
27
28
|
import { focusRoutes } from './handlers/focus.js';
|
|
@@ -66,6 +67,7 @@ function buildRouter() {
|
|
|
66
67
|
.registerAll(reviewRoutes)
|
|
67
68
|
.registerAll(reviewCommentRoutes)
|
|
68
69
|
.registerAll(inboxRoutes)
|
|
70
|
+
.registerAll(chatInventoryRoutes)
|
|
69
71
|
.registerAll(feedbackCommentRoutes);
|
|
70
72
|
}
|
|
71
73
|
/** Is this request an attach upgrade — `GET /v1/nodes/{id}/attach` — and if so,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export type HookJson = null | boolean | number | string | HookJson[] | {
|
|
2
|
+
[key: string]: HookJson;
|
|
3
|
+
};
|
|
4
|
+
export type HookPhase = 'before' | 'after' | 'replace';
|
|
5
|
+
export interface HookContext {
|
|
6
|
+
cwd: string;
|
|
7
|
+
providedParams: string[];
|
|
8
|
+
}
|
|
9
|
+
interface HookRequestBase {
|
|
10
|
+
protocolVersion: 1;
|
|
11
|
+
op: string;
|
|
12
|
+
command: string[];
|
|
13
|
+
operationId: string;
|
|
14
|
+
input: {
|
|
15
|
+
[key: string]: HookJson;
|
|
16
|
+
};
|
|
17
|
+
context: HookContext;
|
|
18
|
+
}
|
|
19
|
+
export interface BeforeHookRequest extends HookRequestBase {
|
|
20
|
+
phase: 'before';
|
|
21
|
+
}
|
|
22
|
+
export interface AfterHookRequest extends HookRequestBase {
|
|
23
|
+
phase: 'after';
|
|
24
|
+
result: HookJson;
|
|
25
|
+
}
|
|
26
|
+
export interface ReplaceHookRequest extends HookRequestBase {
|
|
27
|
+
phase: 'replace';
|
|
28
|
+
}
|
|
29
|
+
export type HookRequest = BeforeHookRequest | AfterHookRequest | ReplaceHookRequest;
|
|
30
|
+
export interface HookSuccess {
|
|
31
|
+
protocolVersion: 1;
|
|
32
|
+
ok: true;
|
|
33
|
+
result?: HookJson;
|
|
34
|
+
}
|
|
35
|
+
export interface HookError {
|
|
36
|
+
code: string;
|
|
37
|
+
message: string;
|
|
38
|
+
field?: string;
|
|
39
|
+
next?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface HookFailure {
|
|
42
|
+
protocolVersion: 1;
|
|
43
|
+
ok: false;
|
|
44
|
+
error: HookError;
|
|
45
|
+
}
|
|
46
|
+
export type HookResponse = HookSuccess | HookFailure;
|
|
47
|
+
export type HookHandler = (request: HookRequest) => HookResponse | Promise<HookResponse>;
|
|
48
|
+
export type HookHandlers = Record<string, HookHandler>;
|
|
49
|
+
export interface RunHookExecutableOptions {
|
|
50
|
+
stdin?: NodeJS.ReadableStream;
|
|
51
|
+
stdout?: NodeJS.WritableStream;
|
|
52
|
+
stderr?: NodeJS.WritableStream;
|
|
53
|
+
}
|
|
54
|
+
export interface LocalHookProcessOptions {
|
|
55
|
+
timeoutMs: number;
|
|
56
|
+
cwd?: string;
|
|
57
|
+
env?: NodeJS.ProcessEnv;
|
|
58
|
+
input?: string | Uint8Array;
|
|
59
|
+
}
|
|
60
|
+
export interface LocalHookProcessSuccess {
|
|
61
|
+
ok: true;
|
|
62
|
+
stdout: string;
|
|
63
|
+
}
|
|
64
|
+
export type LocalHookProcessResult = LocalHookProcessSuccess | HookFailure;
|
|
65
|
+
export interface ForwardHookOptions {
|
|
66
|
+
headers?: HeadersInit;
|
|
67
|
+
bearer?: string;
|
|
68
|
+
timeoutMs: number;
|
|
69
|
+
maxResponseBytes: number;
|
|
70
|
+
}
|
|
71
|
+
export declare function hookError(code: string, message: string, details?: Omit<HookError, 'code' | 'message'>): HookFailure;
|
|
72
|
+
export declare function runHookExecutable(handlers: HookHandlers, options?: RunHookExecutableOptions): Promise<void>;
|
|
73
|
+
export declare function runLocalHookProcess(argv: readonly string[], options: LocalHookProcessOptions): Promise<LocalHookProcessResult>;
|
|
74
|
+
export declare function forwardHook(endpoint: string | URL, request: HookRequest, options: ForwardHookOptions): Promise<HookResponse>;
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { Console } from 'node:console';
|
|
3
|
+
import { posixProcessGroupSpawnOptions, terminateProcessGroup } from './hook-process.js';
|
|
4
|
+
export function hookError(code, message, details = {}) {
|
|
5
|
+
return { protocolVersion: 1, ok: false, error: { code, message, ...details } };
|
|
6
|
+
}
|
|
7
|
+
export async function runHookExecutable(handlers, options = {}) {
|
|
8
|
+
const stdin = options.stdin ?? process.stdin;
|
|
9
|
+
const stdout = options.stdout ?? process.stdout;
|
|
10
|
+
const stderr = options.stderr ?? process.stderr;
|
|
11
|
+
let response;
|
|
12
|
+
try {
|
|
13
|
+
const raw = await readStream(stdin);
|
|
14
|
+
let value;
|
|
15
|
+
try {
|
|
16
|
+
value = JSON.parse(raw);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
response = hookError('invalid_hook_request', 'stdin must contain one JSON hook request');
|
|
20
|
+
writeResponse(stdout, response);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const request = validateRequest(value);
|
|
24
|
+
if (!request.valid) {
|
|
25
|
+
response = hookError('invalid_hook_request', request.message);
|
|
26
|
+
writeResponse(stdout, response);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const handler = handlers[request.value.op];
|
|
30
|
+
if (handler === undefined) {
|
|
31
|
+
response = hookError('unsupported_hook_op', `no handler is registered for ${request.value.op}`);
|
|
32
|
+
writeResponse(stdout, response);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
response = await withConsoleStdoutOnStderr(stderr, () => handler(request.value));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
stderr.write(`hook handler ${request.value.op} failed: ${describeError(error)}\n`);
|
|
40
|
+
response = hookError('hook_handler_failed', `handler failed for ${request.value.op}`);
|
|
41
|
+
}
|
|
42
|
+
const validated = validateResponse(response, request.value);
|
|
43
|
+
if (!validated.valid) {
|
|
44
|
+
stderr.write(`hook handler ${request.value.op} returned an invalid response: ${validated.message}\n`);
|
|
45
|
+
response = hookError('invalid_hook_response', validated.message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
stderr.write(`hook executable failed: ${describeError(error)}\n`);
|
|
50
|
+
response = hookError('hook_executable_failed', 'hook executable could not read its request');
|
|
51
|
+
}
|
|
52
|
+
writeResponse(stdout, response);
|
|
53
|
+
}
|
|
54
|
+
export async function runLocalHookProcess(argv, options) {
|
|
55
|
+
const executable = argv[0];
|
|
56
|
+
if (executable === undefined || executable === '') {
|
|
57
|
+
return hookError('local_process_invalid', 'local process argv[0] must not be empty');
|
|
58
|
+
}
|
|
59
|
+
if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) {
|
|
60
|
+
return hookError('local_process_invalid_options', 'timeoutMs must be greater than zero');
|
|
61
|
+
}
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
let settled = false;
|
|
64
|
+
let timedOut = false;
|
|
65
|
+
let closed = false;
|
|
66
|
+
let cleanupComplete = false;
|
|
67
|
+
let timeout;
|
|
68
|
+
const stdout = [];
|
|
69
|
+
const settle = (result) => {
|
|
70
|
+
if (settled)
|
|
71
|
+
return;
|
|
72
|
+
settled = true;
|
|
73
|
+
if (timeout !== undefined)
|
|
74
|
+
clearTimeout(timeout);
|
|
75
|
+
resolve(result);
|
|
76
|
+
};
|
|
77
|
+
let child;
|
|
78
|
+
try {
|
|
79
|
+
child = spawn(executable, argv.slice(1), {
|
|
80
|
+
cwd: options.cwd,
|
|
81
|
+
env: options.env,
|
|
82
|
+
shell: false,
|
|
83
|
+
stdio: [options.input === undefined ? 'ignore' : 'pipe', 'pipe', 'inherit'],
|
|
84
|
+
...posixProcessGroupSpawnOptions(),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
settle(hookError('local_process_spawn_failed', `could not start ${executable}: ${describeError(error)}`));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const settleTimeout = () => {
|
|
92
|
+
if (timedOut && closed && cleanupComplete) {
|
|
93
|
+
settle(hookError('local_process_timeout', `${executable} did not finish within ${options.timeoutMs}ms`));
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
timeout = setTimeout(() => {
|
|
97
|
+
timedOut = true;
|
|
98
|
+
void terminateProcessGroup(child).then(() => {
|
|
99
|
+
cleanupComplete = true;
|
|
100
|
+
settleTimeout();
|
|
101
|
+
}, (error) => {
|
|
102
|
+
cleanupComplete = true;
|
|
103
|
+
settle(hookError('local_process_cleanup_failed', `could not stop ${executable}: ${describeError(error)}`));
|
|
104
|
+
});
|
|
105
|
+
}, options.timeoutMs);
|
|
106
|
+
child.stdout.on('data', (chunk) => stdout.push(chunk));
|
|
107
|
+
child.once('error', (error) => {
|
|
108
|
+
if (child.pid !== undefined)
|
|
109
|
+
return;
|
|
110
|
+
settle(hookError('local_process_spawn_failed', `could not start ${executable}: ${describeError(error)}`));
|
|
111
|
+
});
|
|
112
|
+
child.once('close', (code, signal) => {
|
|
113
|
+
closed = true;
|
|
114
|
+
if (timedOut) {
|
|
115
|
+
settleTimeout();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (code === 0) {
|
|
119
|
+
settle({ ok: true, stdout: Buffer.concat(stdout).toString('utf8') });
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const status = signal === null ? `exit code ${code ?? 'unknown'}` : `signal ${signal}`;
|
|
123
|
+
settle(hookError('local_process_failed', `${executable} exited with ${status}`));
|
|
124
|
+
});
|
|
125
|
+
if (options.input !== undefined && child.stdin !== null) {
|
|
126
|
+
child.stdin.end(options.input);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
export async function forwardHook(endpoint, request, options) {
|
|
131
|
+
if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) {
|
|
132
|
+
return hookError('hook_forward_invalid_options', 'timeoutMs must be greater than zero');
|
|
133
|
+
}
|
|
134
|
+
if (!Number.isSafeInteger(options.maxResponseBytes) || options.maxResponseBytes <= 0) {
|
|
135
|
+
return hookError('hook_forward_invalid_options', 'maxResponseBytes must be a positive integer');
|
|
136
|
+
}
|
|
137
|
+
const headers = new Headers(options.headers);
|
|
138
|
+
if (!headers.has('content-type'))
|
|
139
|
+
headers.set('content-type', 'application/json');
|
|
140
|
+
if (options.bearer !== undefined)
|
|
141
|
+
headers.set('authorization', `Bearer ${options.bearer}`);
|
|
142
|
+
const controller = new AbortController();
|
|
143
|
+
let timedOut = false;
|
|
144
|
+
const timeout = setTimeout(() => {
|
|
145
|
+
timedOut = true;
|
|
146
|
+
controller.abort();
|
|
147
|
+
}, options.timeoutMs);
|
|
148
|
+
try {
|
|
149
|
+
const response = await fetch(endpoint, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers,
|
|
152
|
+
body: JSON.stringify(request),
|
|
153
|
+
signal: controller.signal,
|
|
154
|
+
});
|
|
155
|
+
const body = await readResponseBody(response, options.maxResponseBytes);
|
|
156
|
+
let value;
|
|
157
|
+
try {
|
|
158
|
+
value = JSON.parse(body);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return hookError('hook_forward_invalid_response', `hook endpoint returned invalid JSON (HTTP ${response.status})`);
|
|
162
|
+
}
|
|
163
|
+
// A valid protocol envelope is authoritative regardless of HTTP status.
|
|
164
|
+
const validated = validateResponse(value, request);
|
|
165
|
+
if (!validated.valid)
|
|
166
|
+
return hookError('hook_forward_invalid_response', `${validated.message} (HTTP ${response.status})`);
|
|
167
|
+
return validated.value;
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (timedOut)
|
|
171
|
+
return hookError('hook_forward_timeout', `hook endpoint did not respond within ${options.timeoutMs}ms`);
|
|
172
|
+
if (error instanceof ResponseTooLargeError) {
|
|
173
|
+
return hookError('hook_forward_response_too_large', `hook endpoint exceeded ${options.maxResponseBytes} response bytes`);
|
|
174
|
+
}
|
|
175
|
+
return hookError('hook_forward_failed', `hook endpoint request failed: ${describeError(error)}`);
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
clearTimeout(timeout);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const STDOUT_CONSOLE_METHODS = ['log', 'info', 'debug', 'dir', 'table', 'group', 'groupCollapsed'];
|
|
182
|
+
async function withConsoleStdoutOnStderr(stderr, run) {
|
|
183
|
+
const redirected = new Console({ stdout: stderr, stderr, colorMode: false });
|
|
184
|
+
const original = new Map();
|
|
185
|
+
for (const method of STDOUT_CONSOLE_METHODS) {
|
|
186
|
+
original.set(method, console[method]);
|
|
187
|
+
console[method] = redirected[method].bind(redirected);
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
return await run();
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
for (const method of STDOUT_CONSOLE_METHODS)
|
|
194
|
+
console[method] = original.get(method);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function writeResponse(stdout, response) {
|
|
198
|
+
stdout.write(`${JSON.stringify(response)}\n`);
|
|
199
|
+
}
|
|
200
|
+
async function readStream(stream) {
|
|
201
|
+
const chunks = [];
|
|
202
|
+
for await (const chunk of stream)
|
|
203
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
204
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
205
|
+
}
|
|
206
|
+
async function readResponseBody(response, maxBytes) {
|
|
207
|
+
if (response.body === null)
|
|
208
|
+
return '';
|
|
209
|
+
const reader = response.body.getReader();
|
|
210
|
+
const chunks = [];
|
|
211
|
+
let total = 0;
|
|
212
|
+
try {
|
|
213
|
+
for (;;) {
|
|
214
|
+
const { done, value } = await reader.read();
|
|
215
|
+
if (done)
|
|
216
|
+
break;
|
|
217
|
+
total += value.byteLength;
|
|
218
|
+
if (total > maxBytes) {
|
|
219
|
+
await reader.cancel();
|
|
220
|
+
throw new ResponseTooLargeError();
|
|
221
|
+
}
|
|
222
|
+
chunks.push(value);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
reader.releaseLock();
|
|
227
|
+
}
|
|
228
|
+
return new TextDecoder().decode(concatBytes(chunks, total));
|
|
229
|
+
}
|
|
230
|
+
function concatBytes(chunks, total) {
|
|
231
|
+
const result = new Uint8Array(total);
|
|
232
|
+
let offset = 0;
|
|
233
|
+
for (const chunk of chunks) {
|
|
234
|
+
result.set(chunk, offset);
|
|
235
|
+
offset += chunk.byteLength;
|
|
236
|
+
}
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
function validateRequest(value) {
|
|
240
|
+
if (!isRecord(value))
|
|
241
|
+
return invalid('request must be an object');
|
|
242
|
+
if (!hasOnlyKeys(value, ['protocolVersion', 'op', 'phase', 'command', 'operationId', 'input', 'context', 'result'])) {
|
|
243
|
+
return invalid('request contains unknown fields');
|
|
244
|
+
}
|
|
245
|
+
if (value['protocolVersion'] !== 1)
|
|
246
|
+
return invalid('unsupported hook protocol version');
|
|
247
|
+
if (typeof value['op'] !== 'string' || value['op'] === '')
|
|
248
|
+
return invalid('op must be a non-empty string');
|
|
249
|
+
if (!isStringArray(value['command']) || value['command'].length === 0)
|
|
250
|
+
return invalid('command must be a non-empty string array');
|
|
251
|
+
if (typeof value['operationId'] !== 'string' || !/^[0-9a-f]{32}$/.test(value['operationId'])) {
|
|
252
|
+
return invalid('operationId must be a 32-character lowercase hexadecimal string');
|
|
253
|
+
}
|
|
254
|
+
if (!isRecord(value['input']) || !isJson(value['input']))
|
|
255
|
+
return invalid('input must be a JSON object');
|
|
256
|
+
if (!isContext(value['context']))
|
|
257
|
+
return invalid('context must contain cwd and providedParams');
|
|
258
|
+
if (value['phase'] === 'after') {
|
|
259
|
+
if (!Object.hasOwn(value, 'result') || !isJson(value['result']))
|
|
260
|
+
return invalid('after requests require a JSON result');
|
|
261
|
+
return valid({
|
|
262
|
+
protocolVersion: 1,
|
|
263
|
+
op: value['op'],
|
|
264
|
+
phase: 'after',
|
|
265
|
+
command: value['command'],
|
|
266
|
+
operationId: value['operationId'],
|
|
267
|
+
input: value['input'],
|
|
268
|
+
context: value['context'],
|
|
269
|
+
result: value['result'],
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (value['phase'] !== 'before' && value['phase'] !== 'replace')
|
|
273
|
+
return invalid('phase must be before, after, or replace');
|
|
274
|
+
if (Object.hasOwn(value, 'result'))
|
|
275
|
+
return invalid('only after requests may contain result');
|
|
276
|
+
return valid({
|
|
277
|
+
protocolVersion: 1,
|
|
278
|
+
op: value['op'],
|
|
279
|
+
phase: value['phase'],
|
|
280
|
+
command: value['command'],
|
|
281
|
+
operationId: value['operationId'],
|
|
282
|
+
input: value['input'],
|
|
283
|
+
context: value['context'],
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
function validateResponse(value, request) {
|
|
287
|
+
if (!isRecord(value))
|
|
288
|
+
return invalid('response must be an object');
|
|
289
|
+
if (value['protocolVersion'] !== 1)
|
|
290
|
+
return invalid('response has an unsupported hook protocol version');
|
|
291
|
+
if (value['ok'] === true) {
|
|
292
|
+
if (!hasOnlyKeys(value, ['protocolVersion', 'ok', 'result']))
|
|
293
|
+
return invalid('successful response contains unknown fields');
|
|
294
|
+
const hasResult = Object.hasOwn(value, 'result');
|
|
295
|
+
if (request.phase === 'replace' && !hasResult)
|
|
296
|
+
return invalid('replace response requires result');
|
|
297
|
+
if (request.phase !== 'replace' && hasResult)
|
|
298
|
+
return invalid('only replace responses may contain result');
|
|
299
|
+
if (hasResult) {
|
|
300
|
+
const result = value['result'];
|
|
301
|
+
if (!isJson(result))
|
|
302
|
+
return invalid('response result must be JSON');
|
|
303
|
+
return valid({ protocolVersion: 1, ok: true, result });
|
|
304
|
+
}
|
|
305
|
+
return valid({ protocolVersion: 1, ok: true });
|
|
306
|
+
}
|
|
307
|
+
if (value['ok'] !== false || !hasOnlyKeys(value, ['protocolVersion', 'ok', 'error'])) {
|
|
308
|
+
return invalid('response must contain protocolVersion, ok, and error or result');
|
|
309
|
+
}
|
|
310
|
+
if (!isHookError(value['error']))
|
|
311
|
+
return invalid('response error must contain code and message strings');
|
|
312
|
+
return valid({ protocolVersion: 1, ok: false, error: value['error'] });
|
|
313
|
+
}
|
|
314
|
+
function isContext(value) {
|
|
315
|
+
return isRecord(value)
|
|
316
|
+
&& hasOnlyKeys(value, ['cwd', 'providedParams'])
|
|
317
|
+
&& typeof value['cwd'] === 'string'
|
|
318
|
+
&& isStringArray(value['providedParams']);
|
|
319
|
+
}
|
|
320
|
+
function isHookError(value) {
|
|
321
|
+
return isRecord(value)
|
|
322
|
+
&& hasOnlyKeys(value, ['code', 'message', 'field', 'next'])
|
|
323
|
+
&& typeof value['code'] === 'string'
|
|
324
|
+
&& value['code'] !== ''
|
|
325
|
+
&& typeof value['message'] === 'string'
|
|
326
|
+
&& value['message'] !== ''
|
|
327
|
+
&& (value['field'] === undefined || typeof value['field'] === 'string')
|
|
328
|
+
&& (value['next'] === undefined || typeof value['next'] === 'string');
|
|
329
|
+
}
|
|
330
|
+
function isRecord(value) {
|
|
331
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
332
|
+
}
|
|
333
|
+
function isStringArray(value) {
|
|
334
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
335
|
+
}
|
|
336
|
+
function isJson(value) {
|
|
337
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string')
|
|
338
|
+
return true;
|
|
339
|
+
if (typeof value === 'number')
|
|
340
|
+
return Number.isFinite(value);
|
|
341
|
+
if (Array.isArray(value))
|
|
342
|
+
return value.every(isJson);
|
|
343
|
+
return isRecord(value) && Object.values(value).every(isJson);
|
|
344
|
+
}
|
|
345
|
+
function hasOnlyKeys(value, keys) {
|
|
346
|
+
return Object.keys(value).every((key) => keys.includes(key));
|
|
347
|
+
}
|
|
348
|
+
function valid(value) {
|
|
349
|
+
return { valid: true, value };
|
|
350
|
+
}
|
|
351
|
+
function invalid(message) {
|
|
352
|
+
return { valid: false, message };
|
|
353
|
+
}
|
|
354
|
+
function describeError(error) {
|
|
355
|
+
return error instanceof Error ? error.message : String(error);
|
|
356
|
+
}
|
|
357
|
+
class ResponseTooLargeError extends Error {
|
|
358
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
export declare const PROCESS_GROUP_KILL_GRACE_MS = 250;
|
|
3
|
+
export declare function posixProcessGroupSpawnOptions(): {
|
|
4
|
+
detached: true;
|
|
5
|
+
};
|
|
6
|
+
/** Signal the isolated group, then unconditionally reap survivors after grace. */
|
|
7
|
+
export declare function terminateProcessGroup(child: ChildProcess, graceMs?: number): Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const PROCESS_GROUP_KILL_GRACE_MS = 250;
|
|
2
|
+
export function posixProcessGroupSpawnOptions() {
|
|
3
|
+
if (process.platform === 'win32') {
|
|
4
|
+
throw new Error('hook process execution requires POSIX process groups');
|
|
5
|
+
}
|
|
6
|
+
return { detached: true };
|
|
7
|
+
}
|
|
8
|
+
/** Signal the isolated group, then unconditionally reap survivors after grace. */
|
|
9
|
+
export async function terminateProcessGroup(child, graceMs = PROCESS_GROUP_KILL_GRACE_MS) {
|
|
10
|
+
const pid = child.pid;
|
|
11
|
+
if (pid === undefined)
|
|
12
|
+
return;
|
|
13
|
+
signalProcessGroup(pid, 'SIGTERM');
|
|
14
|
+
await delay(graceMs);
|
|
15
|
+
signalProcessGroup(pid, 'SIGKILL');
|
|
16
|
+
}
|
|
17
|
+
function signalProcessGroup(pid, signal) {
|
|
18
|
+
try {
|
|
19
|
+
process.kill(-pid, signal);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (!isMissingProcess(error))
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function isMissingProcess(error) {
|
|
27
|
+
return typeof error === 'object'
|
|
28
|
+
&& error !== null
|
|
29
|
+
&& 'code' in error
|
|
30
|
+
&& error.code === 'ESRCH';
|
|
31
|
+
}
|
|
32
|
+
function delay(ms) {
|
|
33
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
34
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export type { CrtrError } from './core/errors.js';
|
|
|
21
21
|
export { nowIso } from './core/fs-utils.js';
|
|
22
22
|
export { validateOperationId, buildEnvelope, operationIdContext, errorClassCodes, serializeEventRecord, errorClassFromError, } from './core/events/index.js';
|
|
23
23
|
export type { BuildEnvelopeInput, ErrorClass, ErrorClassCode, ErrorClassHints, EventComponent, EventEnvelope, EventError, EventField, EventLevel, EventOutcome, OperationId, OperationIdContext, RetryDisposition, } from './core/events/index.js';
|
|
24
|
+
export { forwardHook, hookError, runHookExecutable, runLocalHookProcess } from './hook-authoring.js';
|
|
25
|
+
export type { AfterHookRequest, BeforeHookRequest, ForwardHookOptions, HookContext, HookError, HookFailure, HookHandler, HookHandlers, HookJson, HookPhase, HookRequest, HookResponse, HookSuccess, LocalHookProcessOptions, LocalHookProcessResult, LocalHookProcessSuccess, ReplaceHookRequest, RunHookExecutableOptions, } from './hook-authoring.js';
|
|
24
26
|
export { validateCommandManifest } from './core/command-manifests/manifest.js';
|
|
25
27
|
export type { CommandManifestValidation, ValidatedCommandManifest } from './core/command-manifests/manifest.js';
|
|
26
28
|
export type { CommandManifestIssue, TransportKind } from './core/command-manifests/schema.js';
|
package/dist/index.js
CHANGED
|
@@ -41,6 +41,8 @@ export { nowIso } from './core/fs-utils.js';
|
|
|
41
41
|
// Pure envelope helpers only. Local source binding and canonical append remain
|
|
42
42
|
// internal so package consumers cannot select crouter runtime file paths.
|
|
43
43
|
export { validateOperationId, buildEnvelope, operationIdContext, errorClassCodes, serializeEventRecord, errorClassFromError, } from './core/events/index.js';
|
|
44
|
+
// ── Hook authoring ───────────────────────────────────────────────────────
|
|
45
|
+
export { forwardHook, hookError, runHookExecutable, runLocalHookProcess } from './hook-authoring.js';
|
|
44
46
|
// ── Command-manifest compatibility ───────────────────────────────────────
|
|
45
47
|
export { validateCommandManifest } from './core/command-manifests/manifest.js';
|
|
46
48
|
export { validatePluginBundle } from './core/command-plugins/bundle.js';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// 002 — profile projects gain an explicit memory value. Historical manifests
|
|
2
|
+
// stored `projects` as bare directory strings; the current contract is
|
|
3
|
+
// `{ path, memory }`, where memory caps automatic boot/workspace-open
|
|
4
|
+
// disclosure from that project's stores.
|
|
5
|
+
//
|
|
6
|
+
// Mapping: a string entry on the root profile becomes `preview` (root is an
|
|
7
|
+
// aggregate identity — it points at everything and should not absorb every
|
|
8
|
+
// project's full corpus), every other profile's string entries become
|
|
9
|
+
// `content` (they were authored as that profile's own working context).
|
|
10
|
+
// Root is classified ONLY by the fixed profile directory id: a manifest
|
|
11
|
+
// `name` is mutable, so `name === 'root'` would misclassify a renamed profile.
|
|
12
|
+
//
|
|
13
|
+
// Mixed manifests converge: already-valid objects pass through byte-identical
|
|
14
|
+
// and only string entries convert. Anything else — a non-array `projects`, a
|
|
15
|
+
// non-string/non-object entry, an object without a path, an unknown memory
|
|
16
|
+
// value — throws naming the file, because a guessed cap silently widens or
|
|
17
|
+
// narrows what every node under that profile is shown.
|
|
18
|
+
import { PROFILE_PROJECT_MEMORY_VALUES } from '../api/dto/profiles.js';
|
|
19
|
+
import { usage } from '../core/errors.js';
|
|
20
|
+
/** Frozen copy of `ROOT_PROFILE_ID` (core/profiles/manifest.ts). Migrations
|
|
21
|
+
* stay out of that module's import graph — its readers reject the very
|
|
22
|
+
* manifests this lane exists to convert. */
|
|
23
|
+
const ROOT_PROFILE_ID = 'root-00000000';
|
|
24
|
+
function malformed(input, detail) {
|
|
25
|
+
return usage(`profile manifest cannot be migrated: ${detail}`, {
|
|
26
|
+
received: input.path,
|
|
27
|
+
field: 'projects',
|
|
28
|
+
next: `Fix ${input.path} by hand — every project entry is a string (pre-migration) or {"path": "/abs/dir", "memory": one of ${PROFILE_PROJECT_MEMORY_VALUES.join(', ')}} — then re-run \`crtr sys migrate\`.`,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function isMemory(value) {
|
|
32
|
+
return typeof value === 'string' && PROFILE_PROJECT_MEMORY_VALUES.includes(value);
|
|
33
|
+
}
|
|
34
|
+
export const profileProjectMemoryMigration = {
|
|
35
|
+
lane: 'profile-manifest',
|
|
36
|
+
description: 'profile projects carry an explicit memory value',
|
|
37
|
+
apply(input) {
|
|
38
|
+
const stored = input.raw['projects'];
|
|
39
|
+
if (!Array.isArray(stored)) {
|
|
40
|
+
throw malformed(input, `projects is ${stored === undefined ? 'missing' : typeof stored}, not an array`);
|
|
41
|
+
}
|
|
42
|
+
const legacyMemory = input.profileId === ROOT_PROFILE_ID ? 'preview' : 'content';
|
|
43
|
+
let converted = false;
|
|
44
|
+
const projects = stored.map((entry) => {
|
|
45
|
+
if (typeof entry === 'string') {
|
|
46
|
+
if (entry.trim() === '')
|
|
47
|
+
throw malformed(input, 'a project entry is an empty path');
|
|
48
|
+
converted = true;
|
|
49
|
+
return { path: entry, memory: legacyMemory };
|
|
50
|
+
}
|
|
51
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
52
|
+
throw malformed(input, `a project entry is ${entry === null ? 'null' : typeof entry}`);
|
|
53
|
+
}
|
|
54
|
+
const { path, memory } = entry;
|
|
55
|
+
if (typeof path !== 'string' || path.trim() === '') {
|
|
56
|
+
throw malformed(input, `a project entry has no path: ${JSON.stringify(entry)}`);
|
|
57
|
+
}
|
|
58
|
+
if (!isMemory(memory)) {
|
|
59
|
+
throw malformed(input, `project ${path} has an unknown memory value: ${JSON.stringify(memory)}`);
|
|
60
|
+
}
|
|
61
|
+
// Byte-identical pass-through: an already-valid entry keeps whatever
|
|
62
|
+
// else it carries, so re-running plans no change.
|
|
63
|
+
return entry;
|
|
64
|
+
});
|
|
65
|
+
if (!converted)
|
|
66
|
+
return null;
|
|
67
|
+
// Spread preserves the manifest's stored key order, so the rewrite's diff
|
|
68
|
+
// is the projects array and nothing else.
|
|
69
|
+
return { ...input.raw, projects };
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ProfileManifestSnapshot, StateMigration } from './types.js';
|
|
2
|
+
export interface ProfileManifestRunResult {
|
|
3
|
+
/** Profile manifests found under the profiles root. */
|
|
4
|
+
profiles: number;
|
|
5
|
+
/** One row per migration that rewrote a manifest, in registry order. */
|
|
6
|
+
applied: {
|
|
7
|
+
profileId: string;
|
|
8
|
+
path: string;
|
|
9
|
+
migration: string;
|
|
10
|
+
}[];
|
|
11
|
+
/** Post-migration manifest per readable profile, in scan order — coherent
|
|
12
|
+
* under `dryRun`, where the same fold ran but nothing was written. */
|
|
13
|
+
snapshots: ProfileManifestSnapshot[];
|
|
14
|
+
/** Manifests left untouched because they are not valid JSON. Unreadable
|
|
15
|
+
* bytes are not a structure this lane can convert, and failing the run over
|
|
16
|
+
* one would block every other profile's convergence. */
|
|
17
|
+
skipped: {
|
|
18
|
+
profileId: string;
|
|
19
|
+
path: string;
|
|
20
|
+
error: string;
|
|
21
|
+
}[];
|
|
22
|
+
}
|
|
23
|
+
export declare function runProfileManifestMigrations(profilesRoot: string, opts: {
|
|
24
|
+
/** Held for the whole re-read/fold/write of one manifest — the same lock
|
|
25
|
+
* add/remove take, so a concurrent mutation either lands before the
|
|
26
|
+
* re-read or serializes after the write, never under it. */
|
|
27
|
+
withLock: <T>(profileId: string, fn: () => T) => T;
|
|
28
|
+
dryRun?: boolean;
|
|
29
|
+
migrations?: readonly StateMigration[];
|
|
30
|
+
}): ProfileManifestRunResult;
|