@crouton-kit/crouter 0.3.61 → 0.3.63
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/builtin-views/canvas/core.mjs +2 -1
- package/dist/clients/attach/__tests__/context-message.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/context-message.test.js +46 -0
- package/dist/clients/attach/__tests__/transport-relay.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/transport-relay.test.js +176 -0
- package/dist/clients/attach/attach-cmd.js +747 -743
- package/dist/clients/attach/canvas-panels.d.ts +4 -4
- package/dist/clients/attach/canvas-panels.js +108 -37
- package/dist/clients/attach/chat-view.js +11 -0
- package/dist/clients/attach/context-message.d.ts +3 -1
- package/dist/clients/attach/context-message.js +30 -21
- package/dist/clients/attach/graph-overlay.d.ts +8 -1
- package/dist/clients/attach/graph-overlay.js +201 -22
- package/dist/clients/attach/transport-relay.d.ts +51 -0
- package/dist/clients/attach/transport-relay.js +196 -0
- package/dist/clients/attach/transport-socket.d.ts +29 -0
- package/dist/clients/attach/transport-socket.js +164 -0
- package/dist/clients/attach/transport.d.ts +32 -0
- package/dist/clients/attach/transport.js +26 -0
- package/dist/clients/attach/view-socket.d.ts +40 -44
- package/dist/clients/attach/view-socket.js +94 -197
- package/dist/commands/memory/delete.js +3 -3
- package/dist/commands/memory/origin.js +1 -1
- package/dist/commands/memory/read.js +3 -3
- package/dist/commands/memory/shared.d.ts +8 -6
- package/dist/commands/memory/shared.js +23 -9
- package/dist/commands/memory/write.js +3 -3
- package/dist/commands/node-worktree.js +9 -9
- package/dist/commands/node.js +71 -23
- package/dist/commands/sys/doctor.js +1 -0
- package/dist/core/__tests__/broker-fork-seam.test.js +17 -2
- package/dist/core/__tests__/broker-snapshot-history.test.js +12 -2
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +79 -0
- package/dist/core/__tests__/context-intro.test.js +11 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +9 -4
- package/dist/core/__tests__/fixtures/fake-engine.js +12 -4
- package/dist/core/__tests__/full/broker-crash-teardown.test.js +17 -3
- package/dist/core/__tests__/kickoff.test.js +21 -0
- package/dist/core/__tests__/spawn-root.test.js +60 -0
- package/dist/core/__tests__/worktree.test.js +24 -1
- package/dist/core/canvas/browse/app.d.ts +2 -0
- package/dist/core/canvas/browse/app.js +83 -48
- package/dist/core/canvas/db.js +3 -2
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/nav-model.d.ts +5 -3
- package/dist/core/canvas/nav-model.js +7 -6
- package/dist/core/canvas/paths.d.ts +8 -0
- package/dist/core/canvas/paths.js +10 -0
- package/dist/core/canvas/render.d.ts +6 -0
- package/dist/core/canvas/render.js +117 -20
- package/dist/core/canvas/source.d.ts +26 -0
- package/dist/core/canvas/source.js +29 -0
- package/dist/core/canvas/status-glyph.js +1 -0
- package/dist/core/canvas/types.d.ts +4 -3
- package/dist/core/config.d.ts +18 -2
- package/dist/core/config.js +88 -10
- package/dist/core/fault-classifier.d.ts +2 -2
- package/dist/core/memory-resolver.d.ts +9 -6
- package/dist/core/memory-resolver.js +28 -4
- package/dist/core/profiles/__tests__/root-profile.test.d.ts +1 -0
- package/dist/core/profiles/__tests__/root-profile.test.js +69 -0
- package/dist/core/profiles/manifest.d.ts +2 -0
- package/dist/core/profiles/manifest.js +40 -7
- package/dist/core/profiles/select.d.ts +4 -4
- package/dist/core/profiles/select.js +13 -13
- package/dist/core/runtime/__tests__/spawn-env.test.d.ts +1 -0
- package/dist/core/runtime/__tests__/spawn-env.test.js +268 -0
- package/dist/core/runtime/bearings.d.ts +5 -4
- package/dist/core/runtime/bearings.js +12 -6
- package/dist/core/runtime/broker.js +59 -7
- package/dist/core/runtime/fault-recovery.js +4 -0
- package/dist/core/runtime/front-door.js +1 -1
- package/dist/core/runtime/host.js +43 -3
- package/dist/core/runtime/kickoff.js +6 -0
- package/dist/core/runtime/launch.d.ts +6 -1
- package/dist/core/runtime/launch.js +7 -1
- package/dist/core/runtime/nodes.d.ts +7 -6
- package/dist/core/runtime/nodes.js +3 -3
- package/dist/core/runtime/situational-context.d.ts +12 -0
- package/dist/core/runtime/situational-context.js +48 -0
- package/dist/core/runtime/spawn-env.d.ts +27 -0
- package/dist/core/runtime/spawn-env.js +169 -0
- package/dist/core/runtime/spawn.d.ts +16 -7
- package/dist/core/runtime/spawn.js +8 -2
- package/dist/core/substrate/session-cache.js +7 -1
- package/dist/core/substrate/subject.d.ts +1 -1
- package/dist/core/view/remote-canvas-target.d.ts +3 -0
- package/dist/core/view/remote-canvas-target.js +25 -0
- package/dist/core/worktree.d.ts +4 -4
- package/dist/core/worktree.js +41 -19
- package/dist/daemon/crtrd.js +32 -0
- package/dist/daemon/predicate-eval.js +12 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/pi-extensions/canvas-bash-valve.d.ts +2 -0
- package/dist/pi-extensions/canvas-bash-valve.js +238 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +12 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +41 -5
- package/dist/types.d.ts +32 -0
- package/dist/types.js +7 -0
- package/dist/web-client/assets/index-BnmSLNLa.css +2 -0
- package/dist/web-client/assets/{index-BQufdMhB.js → index-BvzxXXGU.js} +18 -18
- package/dist/web-client/index.html +2 -2
- package/dist/web-client/sw.js +1 -1
- package/docs/public-api.md +5 -3
- package/package.json +2 -2
- package/dist/web-client/assets/fraunces-latin-ext-wght-normal-Ca2vKHc0.woff2 +0 -0
- package/dist/web-client/assets/fraunces-latin-wght-normal-ukD16Tqj.woff2 +0 -0
- package/dist/web-client/assets/fraunces-vietnamese-wght-normal-CnvboYUG.woff2 +0 -0
- package/dist/web-client/assets/index-FjfN-jvN.css +0 -2
- package/dist/web-client/assets/instrument-sans-latin-ext-wght-normal-B5bTHO_g.woff2 +0 -0
- package/dist/web-client/assets/instrument-sans-latin-wght-normal-BbzFLZTg.woff2 +0 -0
- package/dist/web-client/workbox-98f7a950.js +0 -1
|
@@ -174,6 +174,73 @@ export function renderForest() {
|
|
|
174
174
|
}
|
|
175
175
|
return parts.join('\n\n');
|
|
176
176
|
}
|
|
177
|
+
async function nodeLineFromSource(source, nodeId, indent, connector) {
|
|
178
|
+
const node = await source.getNode(nodeId);
|
|
179
|
+
if (node === null)
|
|
180
|
+
return `${indent}${connector}? <missing meta: ${nodeId}>`;
|
|
181
|
+
const hanging = hangingFor(node);
|
|
182
|
+
const glyph = resolveNodeVisual(node.status, { hanging }).glyph;
|
|
183
|
+
const tel = readNodeTelemetry(nodeId);
|
|
184
|
+
const ctx = fmtCtx(tel.tokens_in);
|
|
185
|
+
const asks = countAsks(nodeId);
|
|
186
|
+
const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
|
|
187
|
+
const faultSuffix = hanging !== null ? ` · ${faultSummary(hanging)}` : '';
|
|
188
|
+
return `${indent}${connector}${glyph} ${fullName(node)} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}${faultSuffix}`;
|
|
189
|
+
}
|
|
190
|
+
async function walkTreeFromSource(source, nodeId, indent, isLast, visited, out) {
|
|
191
|
+
if (visited.has(nodeId))
|
|
192
|
+
return;
|
|
193
|
+
visited.add(nodeId);
|
|
194
|
+
const connector = isLast ? '└─ ' : '├─ ';
|
|
195
|
+
out.push(await nodeLineFromSource(source, nodeId, indent, connector));
|
|
196
|
+
const children = await source.subscriptionsOf(nodeId).catch(() => []);
|
|
197
|
+
const childIndent = indent + (isLast ? ' ' : '│ ');
|
|
198
|
+
for (let i = 0; i < children.length; i++) {
|
|
199
|
+
const child = children[i];
|
|
200
|
+
const childIsLast = i === children.length - 1;
|
|
201
|
+
if (visited.has(child.node_id)) {
|
|
202
|
+
const cycleConnector = childIsLast ? '└─ ' : '├─ ';
|
|
203
|
+
out.push(`${childIndent}${cycleConnector}↺ <cycle: ${child.node_id}>`);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
await walkTreeFromSource(source, child.node_id, childIndent, childIsLast, visited, out);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
export async function renderTreeFromSource(source, rootId) {
|
|
210
|
+
const node = await source.getNode(rootId);
|
|
211
|
+
if (node === null)
|
|
212
|
+
return `? <missing node: ${rootId}>`;
|
|
213
|
+
const tel = readNodeTelemetry(rootId);
|
|
214
|
+
const ctx = fmtCtx(tel.tokens_in);
|
|
215
|
+
const asks = countAsks(rootId);
|
|
216
|
+
const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
|
|
217
|
+
const hanging = hangingFor(node);
|
|
218
|
+
const glyph = resolveNodeVisual(node.status, { hanging }).glyph;
|
|
219
|
+
const faultSuffix = hanging !== null ? ` · ${faultSummary(hanging)}` : '';
|
|
220
|
+
const out = [];
|
|
221
|
+
out.push(`${glyph} ${fullName(node)} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}${faultSuffix}`);
|
|
222
|
+
const visited = new Set([rootId]);
|
|
223
|
+
const children = await source.subscriptionsOf(rootId).catch(() => []);
|
|
224
|
+
for (let i = 0; i < children.length; i++) {
|
|
225
|
+
const child = children[i];
|
|
226
|
+
const isLast = i === children.length - 1;
|
|
227
|
+
await walkTreeFromSource(source, child.node_id, '', isLast, visited, out);
|
|
228
|
+
}
|
|
229
|
+
return out.join('\n');
|
|
230
|
+
}
|
|
231
|
+
export async function renderForestFromSource(source) {
|
|
232
|
+
const all = await source.listNodes();
|
|
233
|
+
if (all.length === 0)
|
|
234
|
+
return '(canvas is empty)';
|
|
235
|
+
const roots = all.filter((n) => n.parent === null && (n.status === 'active' || n.status === 'idle'));
|
|
236
|
+
if (roots.length === 0)
|
|
237
|
+
return '(no live roots)';
|
|
238
|
+
const parts = [];
|
|
239
|
+
for (const r of roots) {
|
|
240
|
+
parts.push(await renderTreeFromSource(source, r.node_id));
|
|
241
|
+
}
|
|
242
|
+
return parts.join('\n\n');
|
|
243
|
+
}
|
|
177
244
|
/** The spawn prompt, read straight off disk (canvas-home state) and capped so a
|
|
178
245
|
* giant initial-prompt.md can't bloat the snapshot. Mirrors how telemetry is
|
|
179
246
|
* read here directly rather than via the runtime layer (which would invert the
|
|
@@ -433,26 +500,31 @@ function sessionMtime(nodeId, created) {
|
|
|
433
500
|
*/
|
|
434
501
|
export function dashboardRowsAll() {
|
|
435
502
|
const focusedNodeIds = new Set(listFocuses().map((f) => f.node_id));
|
|
436
|
-
return listNodes().map((row) =>
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
503
|
+
return listNodes().map((row) => dashboardRowFromNodeRow(row, focusedNodeIds));
|
|
504
|
+
}
|
|
505
|
+
function dashboardRowFromNodeRow(row, focusedNodeIds) {
|
|
506
|
+
const live = row.status === 'active' || row.status === 'idle';
|
|
507
|
+
const hanging = activeFaultForDisplay(row, readFault(row.node_id));
|
|
508
|
+
return {
|
|
509
|
+
node_id: row.node_id,
|
|
510
|
+
name: row.name, // handle only; enrichRow upgrades to fullName (meta.description)
|
|
511
|
+
status: row.status,
|
|
512
|
+
kind: row.kind,
|
|
513
|
+
mode: row.mode,
|
|
514
|
+
ctx_tokens: 0, // lazy: enrichRow
|
|
515
|
+
asks: 0, // lazy: enrichRow / enrichRows
|
|
516
|
+
cwd: row.cwd,
|
|
517
|
+
created: row.created,
|
|
518
|
+
lifecycle: row.lifecycle,
|
|
519
|
+
mtimeMs: sessionMtime(row.node_id, row.created),
|
|
520
|
+
streaming: hanging === null && live ? isStreaming(row.node_id, row.pi_pid) : false,
|
|
521
|
+
hanging,
|
|
522
|
+
viewed: focusedNodeIds.has(row.node_id),
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
export async function dashboardRowsAllFromSource(source) {
|
|
526
|
+
const focusedNodeIds = new Set(listFocuses().map((f) => f.node_id));
|
|
527
|
+
return (await source.listNodes()).map((row) => dashboardRowFromNodeRow(row, focusedNodeIds));
|
|
456
528
|
}
|
|
457
529
|
// ---------------------------------------------------------------------------
|
|
458
530
|
// Lazy enrichment API — fold the expensive per-node reads into rows on demand,
|
|
@@ -493,6 +565,20 @@ export function enrichRows(rows) {
|
|
|
493
565
|
row.enriched = true;
|
|
494
566
|
}
|
|
495
567
|
}
|
|
568
|
+
export async function enrichRowsFromSource(source, rows) {
|
|
569
|
+
const todo = rows.filter((r) => r.enriched !== true);
|
|
570
|
+
if (todo.length === 0)
|
|
571
|
+
return;
|
|
572
|
+
const asks = asksForNodes(todo.map((r) => r.node_id));
|
|
573
|
+
await Promise.all(todo.map(async (row) => {
|
|
574
|
+
const meta = await source.getNode(row.node_id);
|
|
575
|
+
if (meta !== null)
|
|
576
|
+
row.name = fullName(meta);
|
|
577
|
+
row.ctx_tokens = readNodeTelemetry(row.node_id).tokens_in ?? 0;
|
|
578
|
+
row.asks = asks[row.node_id] ?? 0;
|
|
579
|
+
row.enriched = true;
|
|
580
|
+
}));
|
|
581
|
+
}
|
|
496
582
|
/** Load the SELECTED row's preview text: the spawn `goal` (initial-prompt.md) plus
|
|
497
583
|
* the whole-conversation `prompts` and the `lastAssistant` reply — the latter two
|
|
498
584
|
* folded into ONE session-file read (see {@link readSessionParts}). Mutates the row
|
|
@@ -509,3 +595,14 @@ export function loadPreview(row) {
|
|
|
509
595
|
row.previewLoaded = true;
|
|
510
596
|
return row;
|
|
511
597
|
}
|
|
598
|
+
export async function loadPreviewFromSource(source, row) {
|
|
599
|
+
if (row.previewLoaded === true)
|
|
600
|
+
return row;
|
|
601
|
+
row.goal = readGoalText(row.node_id);
|
|
602
|
+
const meta = await source.getNode(row.node_id);
|
|
603
|
+
const { prompts, lastAssistant } = readSessionParts(meta?.pi_session_file);
|
|
604
|
+
row.prompts = prompts;
|
|
605
|
+
row.lastAssistant = lastAssistant;
|
|
606
|
+
row.previewLoaded = true;
|
|
607
|
+
return row;
|
|
608
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { NodeMeta, NodeRow, NodeStatus, SubscriptionRef } from './types.js';
|
|
2
|
+
export interface CanvasSource {
|
|
3
|
+
getNode(nodeId: string): Promise<NodeMeta | null>;
|
|
4
|
+
getRow(nodeId: string): Promise<NodeRow | null>;
|
|
5
|
+
listNodes(filter?: {
|
|
6
|
+
status?: NodeStatus | NodeStatus[];
|
|
7
|
+
}): Promise<NodeRow[]>;
|
|
8
|
+
subscriptionsOf(subscriber: string): Promise<SubscriptionRef[]>;
|
|
9
|
+
subscribersOf(publisher: string): Promise<SubscriptionRef[]>;
|
|
10
|
+
view(root: string): Promise<string[]>;
|
|
11
|
+
askCountsForView(root: string): Promise<Record<string, number>>;
|
|
12
|
+
hasActiveLiveSubscription(nodeId: string): Promise<boolean>;
|
|
13
|
+
}
|
|
14
|
+
export declare class LocalCanvasSource implements CanvasSource {
|
|
15
|
+
getNode(nodeId: string): Promise<NodeMeta | null>;
|
|
16
|
+
getRow(nodeId: string): Promise<NodeRow | null>;
|
|
17
|
+
listNodes(filter?: {
|
|
18
|
+
status?: NodeStatus | NodeStatus[];
|
|
19
|
+
}): Promise<NodeRow[]>;
|
|
20
|
+
subscriptionsOf(subscriber: string): Promise<SubscriptionRef[]>;
|
|
21
|
+
subscribersOf(publisher: string): Promise<SubscriptionRef[]>;
|
|
22
|
+
view(root: string): Promise<string[]>;
|
|
23
|
+
askCountsForView(root: string): Promise<Record<string, number>>;
|
|
24
|
+
hasActiveLiveSubscription(nodeId: string): Promise<boolean>;
|
|
25
|
+
}
|
|
26
|
+
export declare const localCanvasSource: CanvasSource;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getNode, getRow, listNodes, subscriptionsOf, subscribersOf, view, hasActiveLiveSubscription, } from './canvas.js';
|
|
2
|
+
import { asksForNodes } from './attention.js';
|
|
3
|
+
export class LocalCanvasSource {
|
|
4
|
+
async getNode(nodeId) {
|
|
5
|
+
return getNode(nodeId);
|
|
6
|
+
}
|
|
7
|
+
async getRow(nodeId) {
|
|
8
|
+
return getRow(nodeId);
|
|
9
|
+
}
|
|
10
|
+
async listNodes(filter) {
|
|
11
|
+
return listNodes(filter);
|
|
12
|
+
}
|
|
13
|
+
async subscriptionsOf(subscriber) {
|
|
14
|
+
return subscriptionsOf(subscriber);
|
|
15
|
+
}
|
|
16
|
+
async subscribersOf(publisher) {
|
|
17
|
+
return subscribersOf(publisher);
|
|
18
|
+
}
|
|
19
|
+
async view(root) {
|
|
20
|
+
return view(root);
|
|
21
|
+
}
|
|
22
|
+
async askCountsForView(root) {
|
|
23
|
+
return asksForNodes([root, ...view(root)]);
|
|
24
|
+
}
|
|
25
|
+
async hasActiveLiveSubscription(nodeId) {
|
|
26
|
+
return hasActiveLiveSubscription(nodeId);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export const localCanvasSource = new LocalCanvasSource();
|
|
@@ -73,7 +73,8 @@ export interface NodeIdentity {
|
|
|
73
73
|
* to the row (like `cwd`/`host_kind`), NOT re-derived by `rebuildIndex()` —
|
|
74
74
|
* it is chosen once at spawn (inherited from the spawner, or `--profile`) and
|
|
75
75
|
* is the runtime→resolver seam (`CRTR_PROFILE_ID`) the memory/gate substrate
|
|
76
|
-
* reads. NULL for
|
|
76
|
+
* reads. NULL only for historical root/no-profile rows; new root conversations
|
|
77
|
+
* use the stable root profile id. */
|
|
77
78
|
profile_id?: string | null;
|
|
78
79
|
/** Role the node was born as: explore | developer | plan | review | general… */
|
|
79
80
|
kind: string;
|
|
@@ -200,8 +201,8 @@ export interface NodeRow {
|
|
|
200
201
|
cwd: string;
|
|
201
202
|
/** Which HOST launches + supervises this node; see NodeIdentity.host_kind. */
|
|
202
203
|
host_kind: 'tmux' | 'broker' | null;
|
|
203
|
-
/** The profile this node runs under; see NodeIdentity.profile_id. NULL
|
|
204
|
-
* root/no-profile
|
|
204
|
+
/** The profile this node runs under; see NodeIdentity.profile_id. NULL only
|
|
205
|
+
* for historical root/no-profile rows. */
|
|
205
206
|
profile_id: string | null;
|
|
206
207
|
parent: string | null;
|
|
207
208
|
created: string;
|
package/dist/core/config.d.ts
CHANGED
|
@@ -9,6 +9,13 @@ export declare function ensureScopeInitialized(scope: Scope, root: string): void
|
|
|
9
9
|
export interface MergedLaunchConfig {
|
|
10
10
|
kinds: Record<string, KindConfig>;
|
|
11
11
|
modelLadders: ModelLaddersConfig;
|
|
12
|
+
/** The merged `spawnEnv.allow` set (project stack > profile > user >
|
|
13
|
+
* builtin, additive) — read by `buildOperationalEnvBase`
|
|
14
|
+
* (`core/runtime/spawn-env.ts`) as source C of the broker spawn-env
|
|
15
|
+
* boundary. */
|
|
16
|
+
spawnEnv: {
|
|
17
|
+
allow: string[];
|
|
18
|
+
};
|
|
12
19
|
}
|
|
13
20
|
/** Merge launch knobs (`kinds`, `modelLadders`) across scopes in
|
|
14
21
|
* project stack > profile > user > builtin precedence — the same precedence
|
|
@@ -29,8 +36,17 @@ export interface MergedLaunchConfig {
|
|
|
29
36
|
* Callers (launch, kind registry) go through this function rather than
|
|
30
37
|
* `readConfig` directly, so `ScopeConfig.kinds`/`modelLadders`/tools/
|
|
31
38
|
* extensions/`availableTo` all honor the same profile + multi-project
|
|
32
|
-
* precedence.
|
|
33
|
-
|
|
39
|
+
* precedence.
|
|
40
|
+
*
|
|
41
|
+
* `targetCwd`/`targetProfileId` default to this PROCESS's own ambient cwd
|
|
42
|
+
* and `CRTR_PROFILE_ID` — correct for a caller resolving config for itself
|
|
43
|
+
* (front-door commands, kind listing). A caller resolving config on behalf
|
|
44
|
+
* of a DIFFERENT node (the broker spawn-env boundary, predicate-eval — both
|
|
45
|
+
* run inside the daemon, whose ambient cwd/profile is the daemon's own, not
|
|
46
|
+
* the target node's) MUST pass the target's own cwd/profile explicitly, or
|
|
47
|
+
* `spawnEnv.allow` resolves from the wrong scope entirely (the C-1
|
|
48
|
+
* follow-up fix this parameterization exists for). */
|
|
49
|
+
export declare function readMergedLaunchConfig(targetCwd?: string, targetProfileId?: string | null): MergedLaunchConfig;
|
|
34
50
|
/** The effective `KindConfig` for one full kind string (top-level, e.g.
|
|
35
51
|
* `developer`, or sub-kind, e.g. `plan/reviewers/security`), across
|
|
36
52
|
* project > user > builtin precedence. Returns `undefined` for a kind no
|
package/dist/core/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { CONFIG_FILE, STATE_FILE, defaultScopeConfig, defaultScopeState, defaultCanvasNavConfig, defaultModelLaddersConfig, defaultKindsConfig } from '../types.js';
|
|
2
|
+
import { CONFIG_FILE, STATE_FILE, defaultScopeConfig, defaultScopeState, defaultCanvasNavConfig, defaultModelLaddersConfig, defaultKindsConfig, defaultRemoteCanvasConfig } from '../types.js';
|
|
3
3
|
import { readJsonIfExists, writeJson, ensureDir } from './fs-utils.js';
|
|
4
4
|
import { scopeRoot, requireScopeRoot, findProjectScopeRoots } from './scope.js';
|
|
5
5
|
import { profileRoot } from './profiles/manifest.js';
|
|
@@ -176,6 +176,33 @@ function mergeModelLadders(raw, base = defaultModelLaddersConfig()) {
|
|
|
176
176
|
}
|
|
177
177
|
return out;
|
|
178
178
|
}
|
|
179
|
+
/** True when `pattern` (a `spawnEnv.allow` entry) matches EVERY possible env
|
|
180
|
+
* var name — a bare `*`, or any run of nothing but `*` characters (`**`,
|
|
181
|
+
* `***`, …). This is the one glob shape that reopens the inherit-all hole
|
|
182
|
+
* the whole spawn-env boundary exists to close, so it is rejected loud
|
|
183
|
+
* rather than silently treated as literal or silently dropped (per
|
|
184
|
+
* `cto-rejects-fallback-hedges`: fix the config, don't hedge around it). */
|
|
185
|
+
function isInheritAllGlob(pattern) {
|
|
186
|
+
return pattern !== '' && pattern.replace(/\*/g, '') === '';
|
|
187
|
+
}
|
|
188
|
+
/** Merge a raw `spawnEnv.allow` list over `base` (defaulting to empty):
|
|
189
|
+
* additive/union, not override — each scope only ADDS a name/glob it
|
|
190
|
+
* explicitly admits across the broker spawn-env boundary (`buildBrokerEnv`),
|
|
191
|
+
* never removes a lower-precedence scope's entry. Non-string entries are
|
|
192
|
+
* dropped, never thrown, mirroring `mergeKinds`/`mergeModelLadders` — but an
|
|
193
|
+
* inherit-all glob (`isInheritAllGlob`) throws immediately at config read: it
|
|
194
|
+
* is not a shape any legitimate entry can mean, and silently accepting or
|
|
195
|
+
* dropping it would either reopen the leak or hide a config mistake. */
|
|
196
|
+
function mergeSpawnEnv(raw, base = []) {
|
|
197
|
+
const r = raw;
|
|
198
|
+
const extra = Array.isArray(r?.allow) ? r.allow.filter((v) => typeof v === 'string') : [];
|
|
199
|
+
for (const pattern of extra) {
|
|
200
|
+
if (isInheritAllGlob(pattern)) {
|
|
201
|
+
throw new Error(`spawnEnv.allow: "${pattern}" matches every env var name (inherit-all) — this is exactly the leak the spawn-env boundary closes. Name the specific vars/prefixes you actually need instead (e.g. "AWS_*", "GIT_ASKPASS").`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { allow: [...new Set([...base, ...extra])] };
|
|
205
|
+
}
|
|
179
206
|
/** Validate one raw `kinds.<name>` entry into a `KindConfig`, or drop it
|
|
180
207
|
* (return null) rather than throwing — an invalid kind entry in config.json
|
|
181
208
|
* must not break config parsing for every other kind. `whenToUse` is the
|
|
@@ -216,6 +243,41 @@ function mergeKinds(raw, base = defaultKindsConfig()) {
|
|
|
216
243
|
}
|
|
217
244
|
return out;
|
|
218
245
|
}
|
|
246
|
+
/** Validate one raw `remoteCanvas.targets.<name>` entry, or drop it (return
|
|
247
|
+
* null) rather than throwing — same rule `normalizeKindEntry` follows. A
|
|
248
|
+
* valid entry needs a non-empty `previewEndpoint` and `relayTokenEnv`; the
|
|
249
|
+
* token itself is never stored here (see `RemoteCanvasTarget`). */
|
|
250
|
+
function normalizeRemoteCanvasTarget(raw) {
|
|
251
|
+
if (raw === null || typeof raw !== 'object')
|
|
252
|
+
return null;
|
|
253
|
+
const r = raw;
|
|
254
|
+
if (typeof r.previewEndpoint !== 'string' || r.previewEndpoint.trim() === '')
|
|
255
|
+
return null;
|
|
256
|
+
if (typeof r.relayTokenEnv !== 'string' || r.relayTokenEnv.trim() === '')
|
|
257
|
+
return null;
|
|
258
|
+
const out = { previewEndpoint: r.previewEndpoint, relayTokenEnv: r.relayTokenEnv };
|
|
259
|
+
if (typeof r.cpOrigin === 'string' && r.cpOrigin.trim() !== '')
|
|
260
|
+
out.cpOrigin = r.cpOrigin;
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
/** Merge a raw `remoteCanvas` block over the built-in defaults (no targets):
|
|
264
|
+
* each valid `targets.<name>` entry adds or shadows a target by name;
|
|
265
|
+
* invalid entries are dropped, never thrown. Mirrors `mergeKinds`'s
|
|
266
|
+
* layer-over-defaults shape. */
|
|
267
|
+
function mergeRemoteCanvas(raw, base = defaultRemoteCanvasConfig()) {
|
|
268
|
+
const out = { ...base.targets };
|
|
269
|
+
if (raw !== null && typeof raw === 'object') {
|
|
270
|
+
const r = raw;
|
|
271
|
+
if (r.targets !== null && typeof r.targets === 'object') {
|
|
272
|
+
for (const [name, value] of Object.entries(r.targets)) {
|
|
273
|
+
const normalized = normalizeRemoteCanvasTarget(value);
|
|
274
|
+
if (normalized !== null)
|
|
275
|
+
out[name] = normalized;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return { targets: out };
|
|
280
|
+
}
|
|
219
281
|
function normalizeMode(value, fallback) {
|
|
220
282
|
if (value === true)
|
|
221
283
|
return 'notify';
|
|
@@ -247,7 +309,9 @@ function mergeConfig(partial) {
|
|
|
247
309
|
const canvasNav = mergeCanvasNav(partial.canvasNav);
|
|
248
310
|
const modelLadders = mergeModelLadders(partial.modelLadders);
|
|
249
311
|
const kinds = mergeKinds(partial.kinds);
|
|
250
|
-
|
|
312
|
+
const remoteCanvas = mergeRemoteCanvas(partial.remoteCanvas);
|
|
313
|
+
const spawnEnv = mergeSpawnEnv(partial.spawnEnv, defaults.spawnEnv?.allow);
|
|
314
|
+
return { schema_version, marketplaces, plugins, auto_update, max_panes_per_window, canvasNav, modelLadders, kinds, remoteCanvas, spawnEnv };
|
|
251
315
|
}
|
|
252
316
|
/** Raw (un-defaulted) partial config for one scope, or null if the scope has
|
|
253
317
|
* no root or no config.json. Used by `readMergedLaunchConfig` to layer
|
|
@@ -281,9 +345,11 @@ function readRawProfileConfig(profileId) {
|
|
|
281
345
|
* — every ancestor `.crouter/`, widened by a selected profile's `projects`),
|
|
282
346
|
* ordered FARTHEST-first so `readMergedLaunchConfig` can layer them on last
|
|
283
347
|
* and have the NEAREST root win — mirroring the resolver's nearest-project-
|
|
284
|
-
* strongest replacement precedence.
|
|
285
|
-
|
|
286
|
-
|
|
348
|
+
* strongest replacement precedence. Walks from `targetCwd`/`targetProfileId`
|
|
349
|
+
* — the SCOPE THE CONFIG IS FOR, not necessarily the calling process's own
|
|
350
|
+
* ambient cwd/profile (see `readMergedLaunchConfig`). */
|
|
351
|
+
function readRawProjectScopeConfigs(targetCwd, targetProfileId) {
|
|
352
|
+
const nearestFirst = findProjectScopeRoots(targetCwd, targetProfileId);
|
|
287
353
|
const farthestFirst = [...nearestFirst].reverse();
|
|
288
354
|
const out = [];
|
|
289
355
|
for (const root of farthestFirst) {
|
|
@@ -312,23 +378,35 @@ function readRawProjectScopeConfigs() {
|
|
|
312
378
|
* Callers (launch, kind registry) go through this function rather than
|
|
313
379
|
* `readConfig` directly, so `ScopeConfig.kinds`/`modelLadders`/tools/
|
|
314
380
|
* extensions/`availableTo` all honor the same profile + multi-project
|
|
315
|
-
* precedence.
|
|
316
|
-
|
|
381
|
+
* precedence.
|
|
382
|
+
*
|
|
383
|
+
* `targetCwd`/`targetProfileId` default to this PROCESS's own ambient cwd
|
|
384
|
+
* and `CRTR_PROFILE_ID` — correct for a caller resolving config for itself
|
|
385
|
+
* (front-door commands, kind listing). A caller resolving config on behalf
|
|
386
|
+
* of a DIFFERENT node (the broker spawn-env boundary, predicate-eval — both
|
|
387
|
+
* run inside the daemon, whose ambient cwd/profile is the daemon's own, not
|
|
388
|
+
* the target node's) MUST pass the target's own cwd/profile explicitly, or
|
|
389
|
+
* `spawnEnv.allow` resolves from the wrong scope entirely (the C-1
|
|
390
|
+
* follow-up fix this parameterization exists for). */
|
|
391
|
+
export function readMergedLaunchConfig(targetCwd = process.cwd(), targetProfileId = process.env['CRTR_PROFILE_ID'] || null) {
|
|
317
392
|
const defaults = defaultScopeConfig();
|
|
318
393
|
const userRaw = readRawScopeConfig('user');
|
|
319
|
-
const profileRaw = readRawProfileConfig(
|
|
320
|
-
const projectRawsFarthestFirst = readRawProjectScopeConfigs();
|
|
394
|
+
const profileRaw = readRawProfileConfig(targetProfileId);
|
|
395
|
+
const projectRawsFarthestFirst = readRawProjectScopeConfigs(targetCwd, targetProfileId);
|
|
321
396
|
let kinds = mergeKinds(userRaw?.kinds, defaults.kinds);
|
|
322
397
|
let modelLadders = mergeModelLadders(userRaw?.modelLadders, defaults.modelLadders);
|
|
398
|
+
let spawnEnv = mergeSpawnEnv(userRaw?.spawnEnv, defaults.spawnEnv?.allow);
|
|
323
399
|
if (profileRaw !== null) {
|
|
324
400
|
kinds = mergeKinds(profileRaw.kinds, kinds);
|
|
325
401
|
modelLadders = mergeModelLadders(profileRaw.modelLadders, modelLadders);
|
|
402
|
+
spawnEnv = mergeSpawnEnv(profileRaw.spawnEnv, spawnEnv.allow);
|
|
326
403
|
}
|
|
327
404
|
for (const raw of projectRawsFarthestFirst) {
|
|
328
405
|
kinds = mergeKinds(raw.kinds, kinds);
|
|
329
406
|
modelLadders = mergeModelLadders(raw.modelLadders, modelLadders);
|
|
407
|
+
spawnEnv = mergeSpawnEnv(raw.spawnEnv, spawnEnv.allow);
|
|
330
408
|
}
|
|
331
|
-
return { kinds, modelLadders };
|
|
409
|
+
return { kinds, modelLadders, spawnEnv };
|
|
332
410
|
}
|
|
333
411
|
/** The effective `KindConfig` for one full kind string (top-level, e.g.
|
|
334
412
|
* `developer`, or sub-kind, e.g. `plan/reviewers/security`), across
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type FaultLink = 'pi→provider' | 'viewer↔broker' | 'relay↔broker' | 'browser↔relay' | 'cli→exa' | 'daemon→node';
|
|
2
|
-
export type FaultKind = 'rate-limit' | 'overloaded' | 'connection' | 'auth' | 'protocol' | 'other' | 'wedged';
|
|
1
|
+
export type FaultLink = 'pi→provider' | 'viewer↔broker' | 'relay↔broker' | 'browser↔relay' | 'cli→exa' | 'daemon→node' | 'crtr→pi';
|
|
2
|
+
export type FaultKind = 'rate-limit' | 'overloaded' | 'connection' | 'auth' | 'protocol' | 'other' | 'wedged' | 'model-not-found';
|
|
3
3
|
export type FaultRetryDisposition = 'auto' | 'manual' | 'fatal';
|
|
4
4
|
export type FaultRetryOwner = 'sdk' | 'daemon' | 'client';
|
|
5
5
|
export interface FaultRetry {
|
|
@@ -2,7 +2,7 @@ import type { InstalledPlugin, Scope } from '../types.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Thin memory-document resolver for the document substrate. Precedence is the
|
|
4
4
|
* OUTER loop, not a global scan: scopes/sources are tried NEAREST-FIRST
|
|
5
|
-
* (project stack nearest > ... > profile > user > builtin), and WITHIN each
|
|
5
|
+
* (node > project stack nearest > ... > profile > user > builtin), and WITHIN each
|
|
6
6
|
* source, in order: (1) exact substrate-identity match (`doc.name === query`
|
|
7
7
|
* — the explicit frontmatter `name`, or its path-derived fallback), (2) direct
|
|
8
8
|
* `memory/<name>.md` physical-path lookup (plus the bare-dir/bare-plugin-name
|
|
@@ -38,11 +38,14 @@ export interface MemoryDoc {
|
|
|
38
38
|
plugin?: string;
|
|
39
39
|
}
|
|
40
40
|
/** The memory-only scope union: the global `Scope` (`user|project|builtin`)
|
|
41
|
-
* plus `profile`. Confined to the memory resolver/list/read/write/
|
|
42
|
-
* paths — deliberately NOT folded into the global `Scope` union, because
|
|
43
|
-
* would leak
|
|
44
|
-
* sys-config surfaces the spec does not extend.
|
|
45
|
-
|
|
41
|
+
* plus `profile` and `node`. Confined to the memory resolver/list/read/write/
|
|
42
|
+
* render paths — deliberately NOT folded into the global `Scope` union, because
|
|
43
|
+
* that would leak these into config, plugin, marketplace, view, and doctor/
|
|
44
|
+
* sys-config surfaces the spec does not extend. `node` is the this-node store
|
|
45
|
+
* (`nodes/<id>/context/memory/`), available only inside a running node
|
|
46
|
+
* (`CRTR_NODE_ID`), and it is the NEAREST scope — a node doc overrides a
|
|
47
|
+
* same-named doc at any wider scope. */
|
|
48
|
+
export type MemoryScope = Scope | 'profile' | 'node';
|
|
46
49
|
export interface MemoryResolutionOpts {
|
|
47
50
|
/** Restrict resolution to a single scope. Conflicts with a scope prefix on
|
|
48
51
|
* the identifier (e.g. `user/foo` with scope=project) throw. */
|
|
@@ -8,6 +8,7 @@ import { warn } from './output.js';
|
|
|
8
8
|
import { pluginMemoryDir, projectScopeRoot, projectScopeRoots, scopeMemoryDir } from './scope.js';
|
|
9
9
|
import { normalizeDocName, normalizeNameSegment, resolveDocName } from './substrate/schema.js';
|
|
10
10
|
import { loadProfileManifest, profileMemoryDir } from './profiles/manifest.js';
|
|
11
|
+
import { memoryDir as nodeMemoryDir } from './runtime/memory.js';
|
|
11
12
|
/** Canonical, unambiguous identifier for a memory document: `<scope>/<name>`. */
|
|
12
13
|
export function memoryDocId(doc) {
|
|
13
14
|
return `${doc.scope}/${doc.name}`;
|
|
@@ -18,14 +19,24 @@ export function memoryDocId(doc) {
|
|
|
18
19
|
function selectedProfileId() {
|
|
19
20
|
return process.env['CRTR_PROFILE_ID'] || '';
|
|
20
21
|
}
|
|
21
|
-
/** The
|
|
22
|
-
*
|
|
23
|
-
* (
|
|
24
|
-
|
|
22
|
+
/** The current node id from the process env, or '' when not running inside a
|
|
23
|
+
* node. The seam the `node` scope resolves through — its memory dir is
|
|
24
|
+
* `nodeMemoryDir(id)` (`nodes/<id>/context/memory/`). */
|
|
25
|
+
function selectedNodeId() {
|
|
26
|
+
return process.env['CRTR_NODE_ID'] || '';
|
|
27
|
+
}
|
|
28
|
+
/** The memory scopes in resolution precedence: node > project stack > profile >
|
|
29
|
+
* user > builtin. Node is included only inside a running node (`CRTR_NODE_ID`)
|
|
30
|
+
* and ranks NEAREST — a node doc overrides any wider scope. Project is included
|
|
31
|
+
* only when a project scope exists for the cwd (or the selected profile's
|
|
32
|
+
* project stack); profile is included only when a profile is selected. A single
|
|
33
|
+
* `scope` narrows to just that scope. */
|
|
25
34
|
function scopesInPrecedence(scope) {
|
|
26
35
|
if (scope)
|
|
27
36
|
return [scope];
|
|
28
37
|
const out = [];
|
|
38
|
+
if (selectedNodeId() !== '')
|
|
39
|
+
out.push('node');
|
|
29
40
|
if (projectScopeRoot())
|
|
30
41
|
out.push('project');
|
|
31
42
|
if (selectedProfileId() !== '')
|
|
@@ -65,6 +76,13 @@ function memorySourcesInPrecedence(scope) {
|
|
|
65
76
|
}
|
|
66
77
|
}
|
|
67
78
|
}
|
|
79
|
+
else if (s === 'node') {
|
|
80
|
+
// The this-node store — a singleton dir in the node's context, no plugins.
|
|
81
|
+
// Silently absent when not running inside a node (CRTR_NODE_ID unset).
|
|
82
|
+
const nodeId = selectedNodeId();
|
|
83
|
+
if (nodeId !== '')
|
|
84
|
+
out.push({ scope: 'node', memoryDir: nodeMemoryDir(nodeId), plugins: [] });
|
|
85
|
+
}
|
|
68
86
|
else {
|
|
69
87
|
out.push({
|
|
70
88
|
scope: s,
|
|
@@ -147,6 +165,12 @@ export function listMemoryDocs(scope, quiet = false) {
|
|
|
147
165
|
return [];
|
|
148
166
|
}
|
|
149
167
|
}
|
|
168
|
+
if (scope === 'node') {
|
|
169
|
+
const nodeId = selectedNodeId();
|
|
170
|
+
if (nodeId === '')
|
|
171
|
+
return [];
|
|
172
|
+
return listMemoryDocsInDir('node', nodeMemoryDir(nodeId), quiet);
|
|
173
|
+
}
|
|
150
174
|
return listMemoryDocsInDir(scope, scopeMemoryDir(scope), quiet);
|
|
151
175
|
}
|
|
152
176
|
/** All of one plugin's substrate docs, mounted under the virtual `<pluginName>/`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { test, before, after, beforeEach, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { ensureRootProfile, listProfiles, loadProfileManifest, profileManifestPath, profileMemoryDir, profileRoot, ROOT_PROFILE_ID, } from '../manifest.js';
|
|
7
|
+
import { selectProfileForCwd } from '../select.js';
|
|
8
|
+
import { resetScopeCache } from '../../scope.js';
|
|
9
|
+
let home;
|
|
10
|
+
let cwd;
|
|
11
|
+
let previousHome;
|
|
12
|
+
let previousCwd;
|
|
13
|
+
function forceNonInteractive() {
|
|
14
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true, writable: true });
|
|
15
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: false, configurable: true, writable: true });
|
|
16
|
+
}
|
|
17
|
+
function restoreInteractiveFlags() {
|
|
18
|
+
Reflect.deleteProperty(process.stdin, 'isTTY');
|
|
19
|
+
Reflect.deleteProperty(process.stdout, 'isTTY');
|
|
20
|
+
}
|
|
21
|
+
before(() => {
|
|
22
|
+
previousHome = process.env.HOME;
|
|
23
|
+
previousCwd = process.cwd();
|
|
24
|
+
});
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-root-profile-home-'));
|
|
27
|
+
cwd = mkdtempSync(join(tmpdir(), 'crtr-root-profile-cwd-'));
|
|
28
|
+
process.env.HOME = home;
|
|
29
|
+
process.chdir(cwd);
|
|
30
|
+
forceNonInteractive();
|
|
31
|
+
resetScopeCache();
|
|
32
|
+
});
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
restoreInteractiveFlags();
|
|
35
|
+
process.chdir(previousCwd);
|
|
36
|
+
rmSync(home, { recursive: true, force: true });
|
|
37
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
38
|
+
resetScopeCache();
|
|
39
|
+
});
|
|
40
|
+
after(() => {
|
|
41
|
+
process.chdir(previousCwd);
|
|
42
|
+
if (previousHome === undefined)
|
|
43
|
+
delete process.env.HOME;
|
|
44
|
+
else
|
|
45
|
+
process.env.HOME = previousHome;
|
|
46
|
+
resetScopeCache();
|
|
47
|
+
});
|
|
48
|
+
test('ensureRootProfile creates the stable root profile and is idempotent', () => {
|
|
49
|
+
const first = ensureRootProfile();
|
|
50
|
+
const second = ensureRootProfile();
|
|
51
|
+
assert.equal(first.profileId, ROOT_PROFILE_ID);
|
|
52
|
+
assert.equal(second.profileId, ROOT_PROFILE_ID);
|
|
53
|
+
assert.deepEqual(second, first);
|
|
54
|
+
assert.equal(first.manifest.name, 'root');
|
|
55
|
+
assert.deepEqual(first.manifest.projects, []);
|
|
56
|
+
assert.equal(existsSync(profileRoot(ROOT_PROFILE_ID)), true);
|
|
57
|
+
assert.equal(existsSync(profileManifestPath(ROOT_PROFILE_ID)), true);
|
|
58
|
+
assert.equal(existsSync(profileMemoryDir(ROOT_PROFILE_ID)), true);
|
|
59
|
+
assert.deepEqual(listProfiles().map(({ profileId }) => profileId), [ROOT_PROFILE_ID]);
|
|
60
|
+
});
|
|
61
|
+
test('selectProfileForCwd falls back to the stable root profile when nothing covers cwd', async () => {
|
|
62
|
+
const profileId = await selectProfileForCwd(cwd);
|
|
63
|
+
assert.equal(profileId, ROOT_PROFILE_ID);
|
|
64
|
+
const entry = loadProfileManifest(ROOT_PROFILE_ID);
|
|
65
|
+
assert.equal(entry.profileId, ROOT_PROFILE_ID);
|
|
66
|
+
assert.equal(entry.manifest.name, 'root');
|
|
67
|
+
assert.equal(entry.manifest.last_used_at !== null, true);
|
|
68
|
+
assert.equal(existsSync(profileRoot(ROOT_PROFILE_ID)), true);
|
|
69
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ProfileManifest } from '../../types.js';
|
|
2
|
+
export declare const ROOT_PROFILE_ID = "root-00000000";
|
|
2
3
|
export declare function profilesRoot(): string;
|
|
3
4
|
export declare function profileRoot(profileId: string): string;
|
|
4
5
|
export declare function profileManifestPath(profileId: string): string;
|
|
@@ -25,6 +26,7 @@ export declare function loadProfileManifest(profileIdOrName: string): ProfileEnt
|
|
|
25
26
|
* mutations (create/rename/add-project/remove-project/delete/last-used)
|
|
26
27
|
* below run inside this. */
|
|
27
28
|
export declare function withProfileManifestLock<T>(profileId: string, fn: () => T): T;
|
|
29
|
+
export declare function ensureRootProfile(): ProfileEntry;
|
|
28
30
|
export declare function createProfile(name: string, projects?: string[]): ProfileEntry;
|
|
29
31
|
export declare function updateProfileLastUsed(profileId: string): ProfileEntry;
|
|
30
32
|
export declare function renameProfile(profileId: string, name: string): ProfileEntry;
|