@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
|
@@ -3,8 +3,12 @@ import { type ResolvedReview } from './review-client.js';
|
|
|
3
3
|
export interface ReviewSurfaceOptions {
|
|
4
4
|
review: ResolvedReview;
|
|
5
5
|
}
|
|
6
|
-
/**
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
/** Open a dedicated viewer window for a review without replacing the caller's
|
|
7
|
+
* conversation window. The fresh viewer consumes the review id at startup. */
|
|
8
|
+
export declare function openReviewWindow(host: SurfaceHost, review: ResolvedReview): boolean;
|
|
9
|
+
/** Consume the review id handed to a dedicated attach viewer. */
|
|
10
|
+
export declare function takeStartupReviewId(): string | undefined;
|
|
11
|
+
/** Resolve and mount the review seeded into a dedicated viewer window. */
|
|
12
|
+
export declare function openReviewSurfaceById(host: SurfaceHost, reviewId: string): Promise<void>;
|
|
13
|
+
/** Mount the review document inside its dedicated viewer window. */
|
|
10
14
|
export declare function openReviewSurface(host: SurfaceHost, opts: ReviewSurfaceOptions): Promise<void>;
|
|
@@ -1,10 +1,60 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
import { getPaneOption, openNodeWindow, paneLocation, selectWindow, shellQuote, viewerSplitEnv, } from '../../../core/runtime/placement-tmux.js';
|
|
1
3
|
import { ReviewDocumentSurface } from './document-surface.js';
|
|
2
4
|
import { openLiveSource } from './live-source.js';
|
|
3
|
-
import { createReviewClient } from './review-client.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
|
|
5
|
+
import { createReviewClient, ReviewTerminalError, terminalRefusalNotice, } from './review-client.js';
|
|
6
|
+
const ATTACH_REVIEW_ENV = 'CRTR_ATTACH_REVIEW_ID';
|
|
7
|
+
/** Open a dedicated viewer window for a review without replacing the caller's
|
|
8
|
+
* conversation window. The fresh viewer consumes the review id at startup. */
|
|
9
|
+
export function openReviewWindow(host, review) {
|
|
10
|
+
if (host.capabilities.remote) {
|
|
11
|
+
host.notice('The review is unavailable in a remote attach — the file and companion live on the host.');
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const callerPane = host.capabilities.selfPane;
|
|
15
|
+
if (callerPane === undefined || callerPane === '') {
|
|
16
|
+
host.notice('Opening a review needs a local viewer in tmux.');
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const location = paneLocation(callerPane);
|
|
20
|
+
const nodeId = getPaneOption(callerPane, '@crtr_node');
|
|
21
|
+
if (location === null || nodeId === undefined || nodeId === '') {
|
|
22
|
+
host.notice('Could not resolve the current viewer for this review.');
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const opened = openNodeWindow({
|
|
26
|
+
session: location.session,
|
|
27
|
+
name: `review:${basename(review.file)}`,
|
|
28
|
+
cwd: review.cwd,
|
|
29
|
+
env: { ...viewerSplitEnv(), [ATTACH_REVIEW_ENV]: review.reviewId },
|
|
30
|
+
command: `crtr surface attach to ${shellQuote(nodeId)}`,
|
|
31
|
+
});
|
|
32
|
+
if (opened === null) {
|
|
33
|
+
host.notice('tmux could not open the review window.');
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
selectWindow(location.session, opened.window);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
/** Consume the review id handed to a dedicated attach viewer. */
|
|
40
|
+
export function takeStartupReviewId() {
|
|
41
|
+
const reviewId = process.env[ATTACH_REVIEW_ENV];
|
|
42
|
+
delete process.env[ATTACH_REVIEW_ENV];
|
|
43
|
+
return reviewId === undefined || reviewId === '' ? undefined : reviewId;
|
|
44
|
+
}
|
|
45
|
+
/** Resolve and mount the review seeded into a dedicated viewer window. */
|
|
46
|
+
export async function openReviewSurfaceById(host, reviewId) {
|
|
47
|
+
try {
|
|
48
|
+
const review = await createReviewClient().byId(reviewId);
|
|
49
|
+
await openReviewSurface(host, { review });
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
host.notice(error instanceof ReviewTerminalError
|
|
53
|
+
? terminalRefusalNotice(error)
|
|
54
|
+
: `couldn't open review: ${describeError(error)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Mount the review document inside its dedicated viewer window. */
|
|
8
58
|
export async function openReviewSurface(host, opts) {
|
|
9
59
|
const review = opts.review;
|
|
10
60
|
let source;
|
|
@@ -36,6 +36,10 @@ export function createReviewClient(opts = {}) {
|
|
|
36
36
|
}
|
|
37
37
|
};
|
|
38
38
|
return {
|
|
39
|
+
async byId(reviewId) {
|
|
40
|
+
const review = await request(() => cliClient().getReview(reviewId));
|
|
41
|
+
return await resolveReview(review, review.companion_node_id, request);
|
|
42
|
+
},
|
|
39
43
|
async byBridge(bridgeNodeId) {
|
|
40
44
|
const review = await request(() => cliClient().getReviewByBridge(bridgeNodeId));
|
|
41
45
|
return await resolveReview(review, review.companion_node_id, request);
|
|
@@ -3,20 +3,13 @@ export interface ReviewAdapterOptions {
|
|
|
3
3
|
host: SurfaceHost;
|
|
4
4
|
dir: string;
|
|
5
5
|
}
|
|
6
|
-
/**
|
|
7
|
-
* Resolves a durable review ticket to its canonical daemon review and mounts
|
|
8
|
-
* the one terminal review surface. Approval flows entirely through
|
|
9
|
-
* `reviewClient.submit` inside that surface — this adapter owns only the
|
|
10
|
-
* remote refusal, the bridge lookup, and the mounted surface's lifecycle.
|
|
11
|
-
*/
|
|
6
|
+
/** Resolve a durable review ticket and open it in its own tmux window. */
|
|
12
7
|
export declare class ReviewAdapter {
|
|
13
8
|
private readonly opts;
|
|
14
9
|
private running;
|
|
15
10
|
private stopped;
|
|
16
|
-
private mountedHandle;
|
|
17
11
|
constructor(opts: ReviewAdapterOptions);
|
|
18
12
|
start(): Promise<void>;
|
|
19
|
-
/** Toggle, cancellation, and controller teardown all take this same path. */
|
|
20
13
|
stop(): Promise<void>;
|
|
21
14
|
private run;
|
|
22
15
|
}
|
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
2
|
import { ApiError } from '../../api/errors.js';
|
|
3
|
-
import {
|
|
3
|
+
import { openReviewWindow } from './review/launch.js';
|
|
4
4
|
import { createReviewClient, ReviewTerminalError, terminalRefusalNotice } from './review/review-client.js';
|
|
5
|
-
/**
|
|
6
|
-
* Resolves a durable review ticket to its canonical daemon review and mounts
|
|
7
|
-
* the one terminal review surface. Approval flows entirely through
|
|
8
|
-
* `reviewClient.submit` inside that surface — this adapter owns only the
|
|
9
|
-
* remote refusal, the bridge lookup, and the mounted surface's lifecycle.
|
|
10
|
-
*/
|
|
5
|
+
/** Resolve a durable review ticket and open it in its own tmux window. */
|
|
11
6
|
export class ReviewAdapter {
|
|
12
7
|
opts;
|
|
13
8
|
running = null;
|
|
14
9
|
stopped = false;
|
|
15
|
-
mountedHandle;
|
|
16
10
|
constructor(opts) {
|
|
17
11
|
this.opts = opts;
|
|
18
12
|
}
|
|
@@ -22,18 +16,12 @@ export class ReviewAdapter {
|
|
|
22
16
|
this.running = this.run();
|
|
23
17
|
return this.running;
|
|
24
18
|
}
|
|
25
|
-
/** Toggle, cancellation, and controller teardown all take this same path. */
|
|
26
19
|
async stop() {
|
|
27
|
-
if (this.stopped)
|
|
28
|
-
return;
|
|
29
20
|
this.stopped = true;
|
|
30
|
-
this.mountedHandle?.close();
|
|
31
21
|
if (this.running !== null)
|
|
32
22
|
await this.running;
|
|
33
23
|
}
|
|
34
24
|
async run() {
|
|
35
|
-
// The refusal precedes the record read: a remote viewer can reach neither
|
|
36
|
-
// the reviewed file nor the companion's pane.
|
|
37
25
|
if (this.opts.host.capabilities.remote) {
|
|
38
26
|
this.opts.host.notice('The review is unavailable in a remote attach — the file and companion live on the host.');
|
|
39
27
|
return;
|
|
@@ -47,46 +35,10 @@ export class ReviewAdapter {
|
|
|
47
35
|
this.opts.host.notice(noticeForBridgeFailure(error));
|
|
48
36
|
return;
|
|
49
37
|
}
|
|
50
|
-
if (this.stopped)
|
|
51
|
-
|
|
52
|
-
let resolveClosed;
|
|
53
|
-
const closed = new Promise((resolve) => { resolveClosed = resolve; });
|
|
54
|
-
const host = wrapHostForMount(this.opts.host, (handle) => {
|
|
55
|
-
this.mountedHandle = handle;
|
|
56
|
-
handle.onClose(() => {
|
|
57
|
-
this.mountedHandle = undefined;
|
|
58
|
-
resolveClosed();
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
await openReviewSurface(host, { review });
|
|
62
|
-
// A live-source failure reports through `host.notice` and returns without
|
|
63
|
-
// ever mounting.
|
|
64
|
-
if (this.mountedHandle === undefined)
|
|
65
|
-
return;
|
|
66
|
-
if (this.stopped) {
|
|
67
|
-
this.mountedHandle.close();
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
await closed;
|
|
38
|
+
if (!this.stopped)
|
|
39
|
+
openReviewWindow(this.opts.host, review);
|
|
71
40
|
}
|
|
72
41
|
}
|
|
73
|
-
/** Observes the one surface `openReviewSurface` mounts so this adapter can
|
|
74
|
-
* await its close and close it itself from `stop()`, without widening
|
|
75
|
-
* `SurfaceHost` or reaching into the real host's internals. */
|
|
76
|
-
function wrapHostForMount(host, onMount) {
|
|
77
|
-
return {
|
|
78
|
-
capabilities: host.capabilities,
|
|
79
|
-
mount: (component, opts) => {
|
|
80
|
-
const handle = host.mount(component, opts);
|
|
81
|
-
onMount(handle);
|
|
82
|
-
return handle;
|
|
83
|
-
},
|
|
84
|
-
notice: (text, opts) => host.notice(text, opts),
|
|
85
|
-
suspendForChild: () => host.suspendForChild(),
|
|
86
|
-
focusNode: (nodeId) => host.focusNode(nodeId),
|
|
87
|
-
pickReviewFile: (spec) => host.pickReviewFile(spec),
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
42
|
function noticeForBridgeFailure(error) {
|
|
91
43
|
if (error instanceof ReviewTerminalError)
|
|
92
44
|
return terminalRefusalNotice(error);
|
|
@@ -283,8 +283,9 @@ export const lintLeaf = defineLeaf({
|
|
|
283
283
|
const { profileId, manifest } = loadProfileManifest(profileIdOrName);
|
|
284
284
|
profileResolved = true;
|
|
285
285
|
lintDir(profileMemoryDir(profileId), 'user');
|
|
286
|
-
for (const
|
|
286
|
+
for (const project of manifest.projects) {
|
|
287
287
|
profileProjects += 1;
|
|
288
|
+
const dir = project.path;
|
|
288
289
|
const doors = frontDoorDocs(join(dir, '.crouter', 'memory'));
|
|
289
290
|
if (doors.length === 0) {
|
|
290
291
|
findings.push({
|
|
@@ -81,6 +81,7 @@ export const readLeaf = defineLeaf({
|
|
|
81
81
|
outputKind: 'object',
|
|
82
82
|
effects: [
|
|
83
83
|
'Reads the document. A document that embeds shell (`!`cmd`` or a ```! block) runs those commands once, in the current working directory.',
|
|
84
|
+
'Records a content delivery for this node: a document whose profile project caps automatic boot and workspace-open disclosure below content is still returned whole here, and unsolicited channels stay quiet about it afterwards.',
|
|
84
85
|
],
|
|
85
86
|
},
|
|
86
87
|
run: async (input) => {
|
package/dist/commands/memory.js
CHANGED
|
@@ -23,7 +23,7 @@ export function registerMemory() {
|
|
|
23
23
|
help: {
|
|
24
24
|
name: 'memory',
|
|
25
25
|
summary: 'list, read, search, and write memory documents — knowledge and preferences',
|
|
26
|
-
model: 'Documents have path-derived identities and resolve across layered scopes in precedence order: node > project stack > profile > user > builtin. Browse the inventory with `list` to see what is stored; address a document directly with `read` once you know its name — a directory name is a valid read target answering with its listing. One verb per operation: `write` creates, `edit` revises (every revision carries a rationale and is recorded), `move` relocates or renames a doc and rewrites inbound `[[refs]]` corpus-wide, `delete` removes, and `history` reads how a document reached its current state. Beyond listings, delivery is explicit `surfaces` routing: each entry names the event that fires it (boot, workspace-open, read, memory-read, command) and how much delivers (name, preview, content).',
|
|
26
|
+
model: 'Documents have path-derived identities and resolve across layered scopes in precedence order: node > project stack > profile > user > builtin. Browse the inventory with `list` to see what is stored; address a document directly with `read` once you know its name — a directory name is a valid read target answering with its listing. One verb per operation: `write` creates, `edit` revises (every revision carries a rationale and is recorded), `move` relocates or renames a doc and rewrites inbound `[[refs]]` corpus-wide, `delete` removes, and `history` reads how a document reached its current state. Beyond listings, delivery is explicit `surfaces` routing: each entry names the event that fires it (boot, workspace-open, read, memory-read, command) and how much delivers (name, preview, content). A selected profile widens the project stack to every project in its purview, and each of those relationships caps what its stores deliver to the boot and workspace-open events; `read`, find, listings, and file- and command-routed delivery see the full corpus regardless.',
|
|
27
27
|
},
|
|
28
28
|
children: [listLeaf, readLeaf, findLeaf, writeLeaf, editLeaf, moveLeaf, historyLeaf, deleteLeaf, originLeaf, lintLeaf],
|
|
29
29
|
});
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
1
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
2
2
|
import { renameSync } from 'node:fs';
|
|
3
3
|
import { defineLeaf } from '../../core/command.js';
|
|
4
|
-
import { notFound } from '../../core/errors.js';
|
|
4
|
+
import { general, notFound } from '../../core/errors.js';
|
|
5
5
|
import { listAllMarketplaces, listAllPlugins, findMarketplaceByName } from '../../core/resolver.js';
|
|
6
6
|
import { resolveScopeArg, requireScopeRoot } from '../../core/scope.js';
|
|
7
7
|
import { updateConfig, updateState, ensureScopeInitialized, readConfig } from '../../core/config.js';
|
|
8
|
-
import { pathExists, ensureDir, removePath, isSymlink, nowIso } from '../../core/fs-utils.js';
|
|
9
|
-
import { clone,
|
|
10
|
-
import { readMarketplaceManifest } from '../../core/manifest.js';
|
|
8
|
+
import { pathExists, ensureDir, removePath, isSymlink, nowIso, readSymlinkTarget } from '../../core/fs-utils.js';
|
|
9
|
+
import { clone, deriveNameFromUrl, currentSha, gitSync, isGitRepo } from '../../core/git.js';
|
|
10
|
+
import { readMarketplaceManifest, readPluginManifest } from '../../core/manifest.js';
|
|
11
|
+
import { assertPluginCandidateValid, isMarketplaceRelativeSource } from './plugin-manage.js';
|
|
11
12
|
import { resolveInstallScope } from './shared.js';
|
|
12
13
|
/** Clone a marketplace repo into `scope` and register it in that scope's
|
|
13
14
|
* config. Shared by the `pkg market add` leaf and `crtr sys setup`, which
|
|
@@ -146,13 +147,158 @@ export const marketRemove = defineLeaf({
|
|
|
146
147
|
return { name, removed: true };
|
|
147
148
|
},
|
|
148
149
|
});
|
|
150
|
+
function stageGitRoot(url, ref, active, candidate) {
|
|
151
|
+
const result = gitSync(['clone', '--depth', '1', '--branch', ref, url, candidate]);
|
|
152
|
+
if (result.status !== 0) {
|
|
153
|
+
removePath(candidate);
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
return { active, candidate, previous: `${candidate}.previous` };
|
|
157
|
+
}
|
|
158
|
+
function activateStagedRoots(staged) {
|
|
159
|
+
const moved = [];
|
|
160
|
+
const activated = [];
|
|
161
|
+
try {
|
|
162
|
+
for (const root of staged) {
|
|
163
|
+
if (pathExists(root.previous))
|
|
164
|
+
throw general(`prior staged backup remains at ${root.previous}`);
|
|
165
|
+
renameSync(root.active, root.previous);
|
|
166
|
+
moved.push(root);
|
|
167
|
+
}
|
|
168
|
+
for (const root of staged) {
|
|
169
|
+
renameSync(root.candidate, root.active);
|
|
170
|
+
activated.push(root);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
for (const root of [...activated].reverse())
|
|
175
|
+
renameSync(root.active, root.candidate);
|
|
176
|
+
for (const root of [...moved].reverse())
|
|
177
|
+
renameSync(root.previous, root.active);
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
for (const root of staged)
|
|
181
|
+
removePath(root.previous);
|
|
182
|
+
}
|
|
183
|
+
function candidatePlugin(plugin, root, manifest) {
|
|
184
|
+
return { plugin: { ...plugin, root, manifest }, scopeRoot: dirname(dirname(plugin.root)) };
|
|
185
|
+
}
|
|
186
|
+
async function updateMarketplaceTarget(target) {
|
|
187
|
+
const activeSha = currentSha(target.root) ?? '';
|
|
188
|
+
const sourcedPlugins = listAllPlugins().filter((plugin) => plugin.scope === target.scope && plugin.sourceMarketplace === target.name);
|
|
189
|
+
const stagedRoots = [];
|
|
190
|
+
const cleanupRoots = [];
|
|
191
|
+
let manifest;
|
|
192
|
+
let marketplaceCandidate;
|
|
193
|
+
const candidates = [];
|
|
194
|
+
try {
|
|
195
|
+
if (isGitRepo(target.root)) {
|
|
196
|
+
const candidate = join(dirname(target.root), `.${target.name}.${process.pid}.${Date.now()}.candidate`);
|
|
197
|
+
marketplaceCandidate = stageGitRoot(target.url, target.ref, target.root, candidate);
|
|
198
|
+
if (marketplaceCandidate !== undefined) {
|
|
199
|
+
cleanupRoots.push(candidate);
|
|
200
|
+
const candidateManifest = readMarketplaceManifest(candidate);
|
|
201
|
+
if (candidateManifest === null)
|
|
202
|
+
throw notFound(`marketplace manifest not found in candidate checkout: ${candidate}/.crouter-marketplace/marketplace.json`);
|
|
203
|
+
manifest = candidateManifest;
|
|
204
|
+
if (currentSha(candidate) !== activeSha)
|
|
205
|
+
stagedRoots.push(marketplaceCandidate);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
const activeManifest = readMarketplaceManifest(target.root);
|
|
209
|
+
if (activeManifest === null)
|
|
210
|
+
throw notFound(`marketplace manifest not found after refresh: ${target.root}/.crouter-marketplace/marketplace.json`);
|
|
211
|
+
manifest = activeManifest;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
const activeManifest = readMarketplaceManifest(target.root);
|
|
216
|
+
if (activeManifest === null)
|
|
217
|
+
throw notFound(`marketplace manifest not found after refresh: ${target.root}/.crouter-marketplace/marketplace.json`);
|
|
218
|
+
manifest = activeManifest;
|
|
219
|
+
}
|
|
220
|
+
for (const plugin of sourcedPlugins) {
|
|
221
|
+
if (isSymlink(plugin.root)) {
|
|
222
|
+
if (marketplaceCandidate === undefined)
|
|
223
|
+
continue;
|
|
224
|
+
const entry = manifest.plugins.find((candidate) => candidate.name === plugin.name);
|
|
225
|
+
if (entry === undefined || !isMarketplaceRelativeSource(entry.source)) {
|
|
226
|
+
throw general(`marketplace candidate removes relative-source plugin "${plugin.name}" while it is installed`);
|
|
227
|
+
}
|
|
228
|
+
const symlinkTarget = readSymlinkTarget(plugin.root);
|
|
229
|
+
if (symlinkTarget === null)
|
|
230
|
+
throw general(`installed marketplace plugin "${plugin.name}" has no readable symlink target`);
|
|
231
|
+
const observedSource = relative(target.root, resolve(dirname(plugin.root), symlinkTarget));
|
|
232
|
+
const candidateRoot = resolve(marketplaceCandidate.candidate, entry.source);
|
|
233
|
+
if (candidateRoot !== resolve(marketplaceCandidate.candidate, observedSource)) {
|
|
234
|
+
throw general(`marketplace candidate moves installed relative-source plugin "${plugin.name}" from ${observedSource} to ${entry.source}`);
|
|
235
|
+
}
|
|
236
|
+
if (!pathExists(candidateRoot))
|
|
237
|
+
throw notFound(`marketplace candidate source path does not exist for plugin "${plugin.name}": ${candidateRoot}`);
|
|
238
|
+
const candidateManifest = readPluginManifest(candidateRoot);
|
|
239
|
+
if (candidateManifest === null)
|
|
240
|
+
throw notFound(`plugin manifest not found in marketplace candidate: ${candidateRoot}/.crouter-plugin/plugin.json`);
|
|
241
|
+
candidates.push(candidatePlugin(plugin, candidateRoot, candidateManifest));
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (!pathExists(plugin.root) || !isGitRepo(plugin.root))
|
|
245
|
+
continue;
|
|
246
|
+
const remote = gitSync(['config', '--get', 'remote.origin.url'], plugin.root).stdout.trim();
|
|
247
|
+
const branch = gitSync(['symbolic-ref', '--short', 'HEAD'], plugin.root).stdout.trim();
|
|
248
|
+
if (remote === '' || branch === '')
|
|
249
|
+
continue;
|
|
250
|
+
const scopeRoot = plugin.scope === 'project' ? requireScopeRoot('project') : requireScopeRoot('user');
|
|
251
|
+
const candidate = join(scopeRoot, 'tmp', `${plugin.name}.${process.pid}.${Date.now()}.candidate`);
|
|
252
|
+
ensureDir(join(scopeRoot, 'tmp'));
|
|
253
|
+
const staged = stageGitRoot(remote, branch, plugin.root, candidate);
|
|
254
|
+
if (staged === undefined)
|
|
255
|
+
continue;
|
|
256
|
+
cleanupRoots.push(candidate);
|
|
257
|
+
const candidateManifest = readPluginManifest(candidate);
|
|
258
|
+
if (candidateManifest === null)
|
|
259
|
+
throw notFound(`plugin manifest not found in update candidate: ${candidate}/.crouter-plugin/plugin.json`);
|
|
260
|
+
candidates.push(candidatePlugin(plugin, candidate, candidateManifest));
|
|
261
|
+
if (currentSha(candidate) !== currentSha(plugin.root))
|
|
262
|
+
stagedRoots.push(staged);
|
|
263
|
+
}
|
|
264
|
+
for (const candidate of candidates) {
|
|
265
|
+
await assertPluginCandidateValid(candidate.plugin.manifest, candidate.plugin.root, candidate.plugin.scope, candidate.scopeRoot, `${candidate.plugin.root}/.crouter-plugin/plugin.json`, candidates);
|
|
266
|
+
}
|
|
267
|
+
activateStagedRoots(stagedRoots);
|
|
268
|
+
updateState(target.scope, (state) => {
|
|
269
|
+
if (state.marketplaces[target.name] === undefined)
|
|
270
|
+
state.marketplaces[target.name] = {};
|
|
271
|
+
state.marketplaces[target.name].last_updated = nowIso();
|
|
272
|
+
});
|
|
273
|
+
for (const plugin of sourcedPlugins) {
|
|
274
|
+
const entry = manifest.plugins.find((candidate) => candidate.name === plugin.name);
|
|
275
|
+
if (entry?.version !== undefined) {
|
|
276
|
+
updateConfig(plugin.scope, (config) => {
|
|
277
|
+
if (config.plugins[plugin.name] !== undefined)
|
|
278
|
+
config.plugins[plugin.name].version = entry.version;
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
updateState(plugin.scope, (state) => {
|
|
282
|
+
if (state.plugins[plugin.name] === undefined)
|
|
283
|
+
state.plugins[plugin.name] = {};
|
|
284
|
+
state.plugins[plugin.name].last_updated = nowIso();
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
const sha = currentSha(target.root) ?? '';
|
|
288
|
+
return { name: target.name, updated: activeSha !== sha, sha };
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
for (const root of cleanupRoots)
|
|
292
|
+
removePath(root);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
149
295
|
export const marketUpdate = defineLeaf({
|
|
150
296
|
name: 'update',
|
|
151
297
|
description: 'pull latest marketplace source',
|
|
152
298
|
whenToUse: 'refreshing one named marketplace source from git, or all registered marketplace sources when no name is given; also updates installed plugins sourced from refreshed marketplaces',
|
|
153
299
|
help: {
|
|
154
300
|
name: 'pkg market update',
|
|
155
|
-
summary: '
|
|
301
|
+
summary: 'refresh one or all registered marketplaces',
|
|
156
302
|
params: [
|
|
157
303
|
{ kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Marketplace name to update. Omit to update all registered marketplaces.' },
|
|
158
304
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Narrows resolution.' },
|
|
@@ -161,79 +307,23 @@ export const marketUpdate = defineLeaf({
|
|
|
161
307
|
{ name: 'updated', type: 'object[]', required: true, constraint: 'One entry per marketplace processed: {name, updated, sha}.' },
|
|
162
308
|
],
|
|
163
309
|
outputKind: 'object',
|
|
164
|
-
effects: ['
|
|
310
|
+
effects: ['Stages and validates marketplace and sourced-plugin revisions before atomically activating them. Updates installed plugin content/version for plugins sourced from refreshed marketplaces.'],
|
|
165
311
|
},
|
|
166
312
|
run: async (input) => {
|
|
167
313
|
const name = input['name'];
|
|
168
314
|
const scopeInput = input['scope'];
|
|
169
315
|
const scope = scopeInput === undefined ? undefined : resolveScopeArg(scopeInput);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const results = [];
|
|
183
|
-
for (const target of targets) {
|
|
184
|
-
const pullSummary = pullIfGit(target.root);
|
|
185
|
-
const freshManifest = readMarketplaceManifest(target.root);
|
|
186
|
-
if (freshManifest === null) {
|
|
187
|
-
throw notFound(`marketplace manifest not found after refresh: ${target.root}/.crouter-marketplace/marketplace.json`);
|
|
188
|
-
}
|
|
189
|
-
updateState(target.scope, (s) => {
|
|
190
|
-
if (s.marketplaces[target.name] === undefined) {
|
|
191
|
-
s.marketplaces[target.name] = {};
|
|
192
|
-
}
|
|
193
|
-
s.marketplaces[target.name].last_updated = nowIso();
|
|
194
|
-
});
|
|
195
|
-
const sourcedPlugins = listAllPlugins().filter((plugin) => plugin.scope === target.scope && plugin.sourceMarketplace === target.name);
|
|
196
|
-
for (const plugin of sourcedPlugins) {
|
|
197
|
-
if (isSymlink(plugin.root)) {
|
|
198
|
-
// Marketplace pull above updates symlinked relative-source plugins.
|
|
199
|
-
}
|
|
200
|
-
else if (pathExists(plugin.root)) {
|
|
201
|
-
pullIfGit(plugin.root);
|
|
202
|
-
}
|
|
203
|
-
const pluginEntry = freshManifest.plugins.find((p) => p.name === plugin.name);
|
|
204
|
-
if (pluginEntry !== undefined && pluginEntry.version !== undefined) {
|
|
205
|
-
updateConfig(plugin.scope, (c) => {
|
|
206
|
-
if (c.plugins[plugin.name] !== undefined) {
|
|
207
|
-
c.plugins[plugin.name].version = pluginEntry.version;
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
updateState(plugin.scope, (s) => {
|
|
212
|
-
if (s.plugins[plugin.name] === undefined) {
|
|
213
|
-
s.plugins[plugin.name] = {};
|
|
214
|
-
}
|
|
215
|
-
s.plugins[plugin.name].last_updated = nowIso();
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
results.push({ name: target.name, updated: pullSummary.updated, sha: pullSummary.sha });
|
|
219
|
-
}
|
|
220
|
-
return results;
|
|
221
|
-
}
|
|
222
|
-
if (name !== undefined) {
|
|
223
|
-
const found = scope === undefined || scope === 'all' || scope === 'builtin'
|
|
224
|
-
? findMarketplaceByName(name)
|
|
225
|
-
: findMarketplaceByName(name, scope);
|
|
226
|
-
if (!found)
|
|
227
|
-
throw notFound(`marketplace not found: ${name}${scope === undefined || scope === 'all' || scope === 'builtin' ? '' : ` (scope: ${scope})`}`);
|
|
228
|
-
const results = await doUpdate([{ name: found.name, scope: found.scope, root: found.root }]);
|
|
229
|
-
return { updated: results };
|
|
230
|
-
}
|
|
231
|
-
const targets = (scope === undefined || scope === 'all'
|
|
232
|
-
? listAllMarketplaces()
|
|
233
|
-
: scope === 'builtin'
|
|
234
|
-
? []
|
|
235
|
-
: listAllMarketplaces().filter((marketplace) => marketplace.scope === scope)).map((m) => ({ name: m.name, scope: m.scope, root: m.root }));
|
|
236
|
-
const results = await doUpdate(targets);
|
|
237
|
-
return { updated: results };
|
|
316
|
+
const targets = name === undefined
|
|
317
|
+
? (scope === undefined || scope === 'all' ? listAllMarketplaces() : scope === 'builtin' ? [] : listAllMarketplaces().filter((marketplace) => marketplace.scope === scope))
|
|
318
|
+
: (() => {
|
|
319
|
+
const found = scope === undefined || scope === 'all' || scope === 'builtin' ? findMarketplaceByName(name) : findMarketplaceByName(name, scope);
|
|
320
|
+
if (found === null)
|
|
321
|
+
throw notFound(`marketplace not found: ${name}${scope === undefined || scope === 'all' || scope === 'builtin' ? '' : ` (scope: ${scope})`}`);
|
|
322
|
+
return [found];
|
|
323
|
+
})();
|
|
324
|
+
const updated = [];
|
|
325
|
+
for (const target of targets)
|
|
326
|
+
updated.push(await updateMarketplaceTarget(target));
|
|
327
|
+
return { updated };
|
|
238
328
|
},
|
|
239
329
|
});
|
|
@@ -4,8 +4,22 @@ import { paginate } from '../../core/pagination.js';
|
|
|
4
4
|
import { listInstalledPlugins, findPluginByName, } from '../../core/resolver.js';
|
|
5
5
|
import { listPluginMemoryDocs } from '../../core/memory-resolver.js';
|
|
6
6
|
import { validatePluginCommands, validateEffectiveCommandPlugins } from '../../core/command-plugins/discovery.js';
|
|
7
|
-
import {
|
|
7
|
+
import { compileHookRegistry, discoverHookRegistry } from '../../core/command-hooks/discovery.js';
|
|
8
|
+
import { hasDeclaredHooks, hookReport } from '../../core/command-hooks/report.js';
|
|
9
|
+
import { SUBTREE_NAMES, coreCommandPaths, coreHookCatalog } from '../../build-root.js';
|
|
8
10
|
import { resolveScopeArg, projectScopeRoot } from '../../core/scope.js';
|
|
11
|
+
async function hookInspectionReport(plugin) {
|
|
12
|
+
if (!hasDeclaredHooks(plugin.manifest))
|
|
13
|
+
return undefined;
|
|
14
|
+
const catalog = await coreHookCatalog();
|
|
15
|
+
const effective = discoverHookRegistry(catalog);
|
|
16
|
+
const validation = effective.validations.find((entry) => entry.plugin.root === plugin.root);
|
|
17
|
+
if (validation === undefined) {
|
|
18
|
+
const isolated = compileHookRegistry(catalog, [plugin]);
|
|
19
|
+
return hookReport(isolated.validations[0]);
|
|
20
|
+
}
|
|
21
|
+
return hookReport(validation, effective.issues.filter((issue) => issue.plugin === plugin.name && issue.scope === plugin.scope));
|
|
22
|
+
}
|
|
9
23
|
export const pluginList = defineLeaf({
|
|
10
24
|
name: 'list',
|
|
11
25
|
description: 'paginated list of installed plugins',
|
|
@@ -93,9 +107,10 @@ export const pluginShow = defineLeaf({
|
|
|
93
107
|
{ name: 'manifest', type: 'object', required: true, constraint: 'Full plugin.json contents.' },
|
|
94
108
|
{ name: 'docs', type: 'object[]', required: true, constraint: 'Each: {name, path}. Memory docs provided by the plugin (its `<pluginName>/` memory subtree).' },
|
|
95
109
|
{ name: 'commands', type: 'object', required: false, constraint: 'Present only when the plugin declares a command manifest (manifest.commands). {manifestPath: string, mounts: string[] (accepted top-level command names), issues: object[] ({code, path?, message, received, expected, next})}. Validated statically; command execution never occurs.' },
|
|
110
|
+
{ name: 'hooks', type: 'object', required: false, constraint: 'Present for every hook declaration, including hook-only plugins. {manifestPath?, executablePath?, declarations: {target, phase, op, description, effects}[], issues: object[] ({code, path?, message, received, expected, next}), trust: string}. Static only: no hook executable runs.' },
|
|
96
111
|
],
|
|
97
112
|
outputKind: 'object',
|
|
98
|
-
effects: ['None. Read-only. Command manifests are validated statically;
|
|
113
|
+
effects: ['None. Read-only. Command and hook manifests are validated statically; plugin executables are never executed.'],
|
|
99
114
|
},
|
|
100
115
|
run: async (input) => {
|
|
101
116
|
const name = input['name'];
|
|
@@ -134,6 +149,7 @@ export const pluginShow = defineLeaf({
|
|
|
134
149
|
issues: v.issues,
|
|
135
150
|
};
|
|
136
151
|
}
|
|
152
|
+
const hooks = await hookInspectionReport(found);
|
|
137
153
|
return {
|
|
138
154
|
name: found.name,
|
|
139
155
|
scope: found.scope,
|
|
@@ -145,6 +161,7 @@ export const pluginShow = defineLeaf({
|
|
|
145
161
|
path: d.path,
|
|
146
162
|
})),
|
|
147
163
|
...(commands !== undefined ? { commands } : {}),
|
|
164
|
+
...(hooks !== undefined ? { hooks } : {}),
|
|
148
165
|
};
|
|
149
166
|
},
|
|
150
167
|
});
|
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type HookPluginCandidate } from '../../core/command-hooks/discovery.js';
|
|
2
|
+
import { type PluginManifest, type Scope } from '../../types.js';
|
|
2
3
|
interface MarketplaceRef {
|
|
3
4
|
marketplace: string;
|
|
4
5
|
plugin: string;
|
|
5
6
|
}
|
|
7
|
+
/** Validate a package candidate before it gains an active plugin path. The
|
|
8
|
+
* candidate namespace shadows any installed declaration of the same name. */
|
|
9
|
+
export declare function assertPluginCandidateValid(manifest: PluginManifest, root: string, scope: Scope, scopeRootPath: string, where: string, candidates?: readonly HookPluginCandidate[]): Promise<void>;
|
|
10
|
+
export declare function isMarketplaceRelativeSource(source: string): boolean;
|
|
6
11
|
/** Install one marketplace plugin into a scope. Exported so `sys setup` can
|
|
7
12
|
* install the default plugin set through the same path `pkg plugin install`
|
|
8
13
|
* uses — refresh the source, resolve the entry, link/clone, register. */
|
|
9
|
-
export declare function installFromMarketplace(ref: MarketplaceRef, scope: Scope): {
|
|
14
|
+
export declare function installFromMarketplace(ref: MarketplaceRef, scope: Scope): Promise<{
|
|
10
15
|
name: string;
|
|
11
16
|
scope: Scope;
|
|
12
17
|
path: string;
|
|
13
|
-
}
|
|
18
|
+
}>;
|
|
14
19
|
export declare const pluginInstall: import("../../core/command.js").LeafDef;
|
|
15
20
|
export declare const pluginRemove: import("../../core/command.js").LeafDef;
|
|
16
21
|
export declare const pluginEnable: import("../../core/command.js").LeafDef;
|