@north-light/crouter 0.3.220 → 0.3.222
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 +29 -0
- package/dist/api/client.js +44 -0
- package/dist/api/dto/chat-inventory.d.ts +56 -0
- package/dist/api/dto/chat-inventory.js +11 -0
- package/dist/api/dto/human-requests.d.ts +88 -0
- package/dist/api/dto/human-requests.js +4 -0
- package/dist/api/dto/human.d.ts +3 -0
- package/dist/api/dto/profiles.d.ts +19 -5
- package/dist/api/dto/profiles.js +2 -1
- package/dist/api/dto/reviews.d.ts +2 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/routes.d.ts +8 -0
- package/dist/api/routes.js +11 -0
- package/dist/build-root.d.ts +2 -6
- package/dist/build-root.js +51 -4
- package/dist/builtin-memory/00-runtime-base/00-authoring.md +31 -0
- package/dist/builtin-memory/00-runtime-base/01-escalation.md +14 -0
- package/dist/builtin-memory/{insights/listen.md → 00-runtime-base/02-insight-capture.md} +1 -0
- package/dist/builtin-memory/02-turn-lifecycle/00-ending-a-turn.md +27 -0
- package/dist/builtin-memory/{02-lifecycle/01-resident.md → 02-turn-lifecycle/02-resident.md} +5 -0
- package/dist/builtin-memory/04-base-worker.md +4 -8
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
- package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +1 -0
- package/dist/builtin-memory/05-kinds/design/00-base.md +2 -1
- package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +2 -1
- package/dist/builtin-memory/05-kinds/design/design-contract.md +19 -0
- package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/general/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/00-base.md +2 -1
- package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +2 -1
- package/dist/builtin-memory/05-kinds/plan/plan-contract.md +28 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/lens-contract.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +1 -0
- package/dist/builtin-memory/05-kinds/review/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/review/companion/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/review/security-findings.md +1 -0
- package/dist/builtin-memory/05-kinds/spec/00-base.md +4 -3
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/spec/requirements.md +1 -0
- package/dist/builtin-memory/design/guide.md +35 -0
- package/dist/builtin-memory/design/roadmap.md +21 -0
- package/dist/builtin-memory/insights/capture.md +1 -1
- package/dist/builtin-memory/internal/agent-shaping.md +3 -1
- package/dist/builtin-memory/internal/memory-loading.md +4 -0
- package/dist/builtin-memory/internal/plugins.md +10 -1
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-memory/plan/roadmap.md +6 -22
- package/dist/builtin-memory/spec/guide.md +23 -6
- 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 +34 -16
- 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/render/markdown-source.js +106 -1
- package/dist/clients/attach/session/file-links.d.ts +13 -4
- package/dist/clients/attach/session/file-links.js +54 -58
- 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 +698 -696
- package/dist/clients/inbox/controller.js +1 -1
- package/dist/clients/inbox/resolve.d.ts +1 -0
- 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 +7 -1
- package/dist/clients/inbox/review-adapter.d.ts +1 -8
- package/dist/clients/inbox/review-adapter.js +4 -52
- package/dist/commands/__tests__/human.test.js +2 -2
- package/dist/commands/human/request.d.ts +2 -0
- package/dist/commands/human/request.js +281 -0
- package/dist/commands/human.js +5 -2
- 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/config.js +2 -2
- package/dist/commands/sys/doctor.js +87 -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__/broker-extension-canvas-db-boundary.test.js +7 -4
- 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__/fixtures/memory-slash-live-probe.js +71 -0
- package/dist/core/__tests__/human-action-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/human-action-delivery.test.js +140 -0
- package/dist/core/__tests__/human-actions.test.d.ts +1 -0
- package/dist/core/__tests__/human-actions.test.js +116 -0
- package/dist/core/__tests__/inline-memory-refs.test.js +36 -2
- package/dist/core/__tests__/profile-project-memory-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +217 -0
- package/dist/core/__tests__/prospective-inventory-capability-parity.test.d.ts +1 -0
- package/dist/core/__tests__/prospective-inventory-capability-parity.test.js +91 -0
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.d.ts +1 -0
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +127 -0
- package/dist/core/__tests__/seam/prospective-inventory-stdout.test.d.ts +1 -0
- package/dist/core/__tests__/seam/prospective-inventory-stdout.test.js +31 -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/db.js +23 -0
- package/dist/core/canvas/human-deliveries.d.ts +53 -0
- package/dist/core/canvas/human-deliveries.js +75 -0
- 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/config.d.ts +13 -1
- package/dist/core/config.js +51 -1
- package/dist/core/feed/inbox.d.ts +6 -0
- package/dist/core/feed/inbox.js +9 -1
- package/dist/core/help.d.ts +7 -1
- package/dist/core/human/action-binding.d.ts +21 -0
- package/dist/core/human/action-binding.js +40 -0
- package/dist/core/human/completion.d.ts +38 -0
- package/dist/core/human/completion.js +27 -0
- package/dist/core/human/convention.d.ts +2 -0
- package/dist/core/human/convention.js +2 -0
- package/dist/core/human/tickets.d.ts +25 -6
- package/dist/core/human/tickets.js +19 -13
- package/dist/core/human/types.d.ts +5 -0
- package/dist/core/human-actions.d.ts +25 -0
- package/dist/core/human-actions.js +101 -0
- 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 +26 -20
- package/dist/core/profiles/manifest.d.ts +13 -2
- package/dist/core/profiles/manifest.js +84 -18
- package/dist/core/profiles/select.d.ts +2 -0
- package/dist/core/profiles/select.js +29 -12
- 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 +13 -11
- 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 +4 -0
- package/dist/core/runtime/broker-inventory.js +116 -0
- package/dist/core/runtime/broker-persona-guidance.js +1 -1
- package/dist/core/runtime/broker-protocol.d.ts +9 -2
- package/dist/core/runtime/broker.js +10 -1
- package/dist/core/runtime/chat-inventory-rows.d.ts +8 -0
- package/dist/core/runtime/chat-inventory-rows.js +105 -0
- package/dist/core/runtime/command-surface.d.ts +38 -0
- package/dist/core/runtime/command-surface.js +117 -0
- package/dist/core/runtime/launch-target.d.ts +25 -0
- package/dist/core/runtime/launch-target.js +54 -0
- package/dist/core/runtime/node-read.js +5 -0
- package/dist/core/runtime/persona.js +3 -3
- package/dist/core/runtime/prospective-inventory-cli.d.ts +1 -0
- package/dist/core/runtime/prospective-inventory-cli.js +61 -0
- package/dist/core/runtime/prospective-inventory.d.ts +10 -0
- package/dist/core/runtime/prospective-inventory.js +88 -0
- package/dist/core/runtime/spawn.d.ts +3 -1
- package/dist/core/runtime/spawn.js +5 -3
- 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 +30 -32
- package/dist/core/substrate/render-node.d.ts +3 -2
- package/dist/core/substrate/render-node.js +3 -2
- package/dist/core/substrate/render.js +65 -24
- package/dist/core/substrate/schema.d.ts +16 -2
- package/dist/core/substrate/schema.js +14 -5
- package/dist/core/user-settings.d.ts +4 -0
- package/dist/core/user-settings.js +1 -0
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +56 -7
- 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/human-requests.d.ts +2 -0
- package/dist/daemon/api/handlers/human-requests.js +409 -0
- package/dist/daemon/api/handlers/human.js +3 -0
- package/dist/daemon/api/handlers/inbox.js +3 -0
- package/dist/daemon/api/handlers/nodes.d.ts +1 -3
- package/dist/daemon/api/handlers/nodes.js +11 -46
- package/dist/daemon/api/handlers/profiles.js +7 -1
- package/dist/daemon/api/handlers/prospective-chat-inventory.d.ts +2 -0
- package/dist/daemon/api/handlers/prospective-chat-inventory.js +59 -0
- package/dist/daemon/api/handlers/reviews.js +10 -2
- package/dist/daemon/api/map.d.ts +2 -1
- package/dist/daemon/api/map.js +3 -2
- package/dist/daemon/api/server.js +6 -0
- package/dist/daemon/crtrd.js +6 -0
- package/dist/daemon/human/deliver-action.d.ts +16 -0
- package/dist/daemon/human/deliver-action.js +168 -0
- package/dist/daemon/human/finish.d.ts +8 -5
- package/dist/daemon/human/finish.js +45 -6
- package/dist/daemon/human/sweep.js +4 -1
- package/dist/daemon/reconcilers/human-delivery-lane.d.ts +10 -0
- package/dist/daemon/reconcilers/human-delivery-lane.js +41 -0
- package/dist/daemon/review/finish.d.ts +8 -3
- package/dist/daemon/review/finish.js +19 -1
- 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 +23 -6
- package/dist/types.js +1 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/builtin-memory/00-runtime-base.md +0 -55
- package/dist/builtin-memory/design.md +0 -55
- package/dist/clients/attach/__tests__/file-review-focus.test.js +0 -49
- /package/dist/builtin-memory/{02-lifecycle/00-terminal.md → 02-turn-lifecycle/01-terminal.md} +0 -0
- /package/dist/{clients/attach/__tests__/file-review-focus.test.d.ts → core/__tests__/fixtures/memory-slash-live-probe.d.ts} +0 -0
|
@@ -155,6 +155,8 @@ export function dashboardRows(rootId) {
|
|
|
155
155
|
return [{
|
|
156
156
|
node_id: id,
|
|
157
157
|
name: fullName(node),
|
|
158
|
+
handle: node.name,
|
|
159
|
+
description: node.description,
|
|
158
160
|
status: node.status,
|
|
159
161
|
kind: node.kind,
|
|
160
162
|
mode: node.mode,
|
|
@@ -189,8 +191,11 @@ export function enrichRow(row) {
|
|
|
189
191
|
if (row.enriched === true)
|
|
190
192
|
return row;
|
|
191
193
|
const meta = getNode(row.node_id);
|
|
192
|
-
if (meta !== null)
|
|
194
|
+
if (meta !== null) {
|
|
193
195
|
row.name = fullName(meta);
|
|
196
|
+
row.handle = meta.name;
|
|
197
|
+
row.description = meta.description;
|
|
198
|
+
}
|
|
194
199
|
row.ctx_tokens = readNodeTelemetry(row.node_id).tokens_in ?? 0;
|
|
195
200
|
row.asks = countTickets(row.node_id);
|
|
196
201
|
row.enriched = true;
|
|
@@ -206,8 +211,11 @@ export function enrichRows(rows) {
|
|
|
206
211
|
const asks = ticketCountsForNodes(todo.map((r) => r.node_id));
|
|
207
212
|
for (const row of todo) {
|
|
208
213
|
const meta = getNode(row.node_id);
|
|
209
|
-
if (meta !== null)
|
|
214
|
+
if (meta !== null) {
|
|
210
215
|
row.name = fullName(meta);
|
|
216
|
+
row.handle = meta.name;
|
|
217
|
+
row.description = meta.description;
|
|
218
|
+
}
|
|
211
219
|
row.ctx_tokens = readNodeTelemetry(row.node_id).tokens_in ?? 0;
|
|
212
220
|
row.asks = asks[row.node_id] ?? 0;
|
|
213
221
|
row.enriched = true;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PluginManifest } from '../../types.js';
|
|
2
|
+
import { type HookManifestIssue, type ValidatedHookManifest } from './schema.js';
|
|
3
|
+
export interface HookArtifactValidation {
|
|
4
|
+
manifestPath?: string;
|
|
5
|
+
executablePath?: string;
|
|
6
|
+
manifest?: ValidatedHookManifest;
|
|
7
|
+
issues: HookManifestIssue[];
|
|
8
|
+
}
|
|
9
|
+
/** Statically validates the local hook artifact a source plugin ships. */
|
|
10
|
+
export declare function validatePluginHookArtifact(root: string, manifest: Pick<PluginManifest, 'hooks' | 'hookExecutable'>): HookArtifactValidation;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, resolve, sep } from 'node:path';
|
|
3
|
+
import { validateHookManifest } from './schema.js';
|
|
4
|
+
function safeFilePath(root, pointer) {
|
|
5
|
+
if (isAbsolute(pointer))
|
|
6
|
+
return null;
|
|
7
|
+
try {
|
|
8
|
+
const rootReal = realpathSync(root);
|
|
9
|
+
const path = realpathSync(resolve(root, pointer));
|
|
10
|
+
return (path === rootReal || path.startsWith(rootReal + sep)) && statSync(path).isFile() ? path : null;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Statically validates the local hook artifact a source plugin ships. */
|
|
17
|
+
export function validatePluginHookArtifact(root, manifest) {
|
|
18
|
+
const hooks = manifest.hooks;
|
|
19
|
+
const executable = manifest.hookExecutable;
|
|
20
|
+
const issues = [];
|
|
21
|
+
const hasHooks = hooks !== undefined;
|
|
22
|
+
const hasExecutable = executable !== undefined;
|
|
23
|
+
if (hasHooks !== hasExecutable) {
|
|
24
|
+
issues.push({
|
|
25
|
+
code: 'hook_manifest_invalid',
|
|
26
|
+
path: hasHooks ? 'hookExecutable' : 'hooks',
|
|
27
|
+
message: 'hooks and hookExecutable must be declared together',
|
|
28
|
+
received: hasHooks ? 'hooks without hookExecutable' : 'hookExecutable without hooks',
|
|
29
|
+
expected: 'both hooks and hookExecutable',
|
|
30
|
+
next: hasHooks ? 'Add plugin.json.hookExecutable or remove hooks.' : 'Add plugin.json.hooks or remove hookExecutable.',
|
|
31
|
+
});
|
|
32
|
+
return { issues };
|
|
33
|
+
}
|
|
34
|
+
if (!hasHooks)
|
|
35
|
+
return { issues };
|
|
36
|
+
if (typeof hooks !== 'string' || hooks.length === 0) {
|
|
37
|
+
issues.push({
|
|
38
|
+
code: 'hook_manifest_invalid',
|
|
39
|
+
path: 'hooks',
|
|
40
|
+
message: 'hooks must be a non-empty plugin-root-relative path',
|
|
41
|
+
received: typeof hooks === 'string' ? '(empty)' : Array.isArray(hooks) ? 'array' : typeof hooks,
|
|
42
|
+
expected: 'a non-empty relative path to hooks.json',
|
|
43
|
+
next: 'Fix plugin.json.hooks.',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (typeof executable !== 'string' || executable.length === 0) {
|
|
47
|
+
issues.push({
|
|
48
|
+
code: 'hook_manifest_invalid',
|
|
49
|
+
path: 'hookExecutable',
|
|
50
|
+
message: 'hookExecutable must be a non-empty plugin-root-relative path',
|
|
51
|
+
received: typeof executable === 'string' ? '(empty)' : Array.isArray(executable) ? 'array' : typeof executable,
|
|
52
|
+
expected: 'a non-empty relative path to an executable file',
|
|
53
|
+
next: 'Fix plugin.json.hookExecutable.',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (issues.length > 0)
|
|
57
|
+
return { issues };
|
|
58
|
+
const hooksPointer = hooks;
|
|
59
|
+
const executablePointer = executable;
|
|
60
|
+
const manifestPath = safeFilePath(root, hooksPointer);
|
|
61
|
+
if (manifestPath === null) {
|
|
62
|
+
issues.push({
|
|
63
|
+
code: 'hook_path_unsafe',
|
|
64
|
+
path: 'hooks',
|
|
65
|
+
message: 'hooks pointer escapes the plugin root or is not a regular file',
|
|
66
|
+
received: hooksPointer,
|
|
67
|
+
expected: 'a relative path to a regular file inside the plugin root',
|
|
68
|
+
next: 'Fix plugin.json.hooks.',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const executablePath = safeFilePath(root, executablePointer);
|
|
72
|
+
if (executablePath === null) {
|
|
73
|
+
issues.push({
|
|
74
|
+
code: 'hook_path_unsafe',
|
|
75
|
+
path: 'hookExecutable',
|
|
76
|
+
message: 'hookExecutable escapes the plugin root or is not a regular file',
|
|
77
|
+
received: executablePointer,
|
|
78
|
+
expected: 'a relative path to a regular file inside the plugin root',
|
|
79
|
+
next: 'Fix plugin.json.hookExecutable.',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (executablePath !== null && (statSync(executablePath).mode & 0o111) === 0) {
|
|
83
|
+
issues.push({
|
|
84
|
+
code: 'hook_not_executable',
|
|
85
|
+
path: 'hookExecutable',
|
|
86
|
+
message: 'hookExecutable lacks the POSIX exec bit',
|
|
87
|
+
received: executablePointer,
|
|
88
|
+
expected: 'a file with an executable permission bit',
|
|
89
|
+
next: 'chmod +x the hook executable, then update the plugin.',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (manifestPath === null)
|
|
93
|
+
return { ...(executablePath === null ? {} : { executablePath }), issues };
|
|
94
|
+
let text;
|
|
95
|
+
try {
|
|
96
|
+
text = readFileSync(manifestPath, 'utf8');
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
issues.push({
|
|
100
|
+
code: 'hook_manifest_unreadable',
|
|
101
|
+
message: 'hooks.json is missing or unreadable',
|
|
102
|
+
received: manifestPath,
|
|
103
|
+
expected: 'a readable JSON file',
|
|
104
|
+
next: 'Reinstall or update the plugin.',
|
|
105
|
+
});
|
|
106
|
+
return { manifestPath, ...(executablePath === null ? {} : { executablePath }), issues };
|
|
107
|
+
}
|
|
108
|
+
let raw;
|
|
109
|
+
try {
|
|
110
|
+
raw = JSON.parse(text);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
issues.push({
|
|
114
|
+
code: 'hook_manifest_invalid',
|
|
115
|
+
message: 'hooks.json is not valid JSON',
|
|
116
|
+
received: manifestPath,
|
|
117
|
+
expected: 'a JSON object',
|
|
118
|
+
next: 'Regenerate hooks.json.',
|
|
119
|
+
});
|
|
120
|
+
return { manifestPath, ...(executablePath === null ? {} : { executablePath }), issues };
|
|
121
|
+
}
|
|
122
|
+
const validation = validateHookManifest(raw);
|
|
123
|
+
return {
|
|
124
|
+
manifestPath,
|
|
125
|
+
...(executablePath === null ? {} : { executablePath }),
|
|
126
|
+
...(validation.manifest === undefined ? {} : { manifest: validation.manifest }),
|
|
127
|
+
issues: [...issues, ...validation.issues],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BranchDef, LeafDef } from '../command.js';
|
|
2
|
+
export interface CoreHookTarget {
|
|
3
|
+
/** Exact space-joined core leaf path named by hooks.json. */
|
|
4
|
+
path: string;
|
|
5
|
+
segments: readonly string[];
|
|
6
|
+
leaf: LeafDef;
|
|
7
|
+
}
|
|
8
|
+
export interface CoreHookCatalog {
|
|
9
|
+
targets: readonly CoreHookTarget[];
|
|
10
|
+
byPath: ReadonlyMap<string, CoreHookTarget>;
|
|
11
|
+
has(path: string): boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Projects every composed core subtree to leaves whose objects flow through central rendering. */
|
|
14
|
+
export declare function createCoreHookCatalog(subtrees: readonly BranchDef[]): CoreHookCatalog;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const NON_RETURNING_OBJECT_LEAVES = new Set([
|
|
2
|
+
'pkg browse',
|
|
3
|
+
'sys settings',
|
|
4
|
+
'sys setup',
|
|
5
|
+
'canvas browse',
|
|
6
|
+
'surface inspect',
|
|
7
|
+
]);
|
|
8
|
+
function excluded(path) {
|
|
9
|
+
return path === 'sys doctor'
|
|
10
|
+
|| path === 'pkg plugin'
|
|
11
|
+
|| path.startsWith('pkg plugin ')
|
|
12
|
+
|| NON_RETURNING_OBJECT_LEAVES.has(path);
|
|
13
|
+
}
|
|
14
|
+
/** Projects every composed core subtree to leaves whose objects flow through central rendering. */
|
|
15
|
+
export function createCoreHookCatalog(subtrees) {
|
|
16
|
+
const targets = [];
|
|
17
|
+
const visit = (node, segments) => {
|
|
18
|
+
const path = segments.join(' ');
|
|
19
|
+
if (node.kind === 'leaf') {
|
|
20
|
+
if (node.help.outputKind === 'object' && node.hookEligible !== false && !excluded(path)) {
|
|
21
|
+
targets.push({ path, segments: Object.freeze([...segments]), leaf: node });
|
|
22
|
+
}
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (node.passthrough !== undefined || excluded(path))
|
|
26
|
+
return;
|
|
27
|
+
for (const child of node.children)
|
|
28
|
+
visit(child, [...segments, child.name]);
|
|
29
|
+
};
|
|
30
|
+
for (const subtree of subtrees)
|
|
31
|
+
visit(subtree, [subtree.name]);
|
|
32
|
+
const byPath = new Map(targets.map((target) => [target.path, target]));
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
targets: Object.freeze(targets),
|
|
35
|
+
byPath,
|
|
36
|
+
has: (path) => byPath.has(path),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BranchDef } from '../command.js';
|
|
2
|
+
import type { HookRegistry } from './discovery.js';
|
|
3
|
+
/** Registry compilation never executes hook code. */
|
|
4
|
+
export type HookRegistryLoader = () => Promise<HookRegistry>;
|
|
5
|
+
/** Wrap every hook-eligible core leaf so its run is the before → primary →
|
|
6
|
+
* after pipeline. Nothing else about the leaf changes: path, params, frozen
|
|
7
|
+
* help, renderer, and output declaration are carried through untouched, and
|
|
8
|
+
* ineligible leaves (JSONL, UI/void, `pkg plugin`, `sys doctor`, passthrough)
|
|
9
|
+
* keep their original run function so recovery dispatch never depends on hook
|
|
10
|
+
* discovery.
|
|
11
|
+
*
|
|
12
|
+
* Eligibility is per-path, so projecting the passed subtrees answers it for
|
|
13
|
+
* those subtrees exactly as the complete catalog would; the complete catalog
|
|
14
|
+
* is what the loaded registry validates hook targets against. */
|
|
15
|
+
export declare function composeCoreHooks(subtrees: readonly BranchDef[], loadRegistry: HookRegistryLoader): BranchDef[];
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { ExitCode } from '../../types.js';
|
|
2
|
+
import { CrtrError } from '../errors.js';
|
|
3
|
+
import { operationIdContext } from '../events/operation-id.js';
|
|
4
|
+
import { createCoreHookCatalog } from './catalog.js';
|
|
5
|
+
/** Wrap every hook-eligible core leaf so its run is the before → primary →
|
|
6
|
+
* after pipeline. Nothing else about the leaf changes: path, params, frozen
|
|
7
|
+
* help, renderer, and output declaration are carried through untouched, and
|
|
8
|
+
* ineligible leaves (JSONL, UI/void, `pkg plugin`, `sys doctor`, passthrough)
|
|
9
|
+
* keep their original run function so recovery dispatch never depends on hook
|
|
10
|
+
* discovery.
|
|
11
|
+
*
|
|
12
|
+
* Eligibility is per-path, so projecting the passed subtrees answers it for
|
|
13
|
+
* those subtrees exactly as the complete catalog would; the complete catalog
|
|
14
|
+
* is what the loaded registry validates hook targets against. */
|
|
15
|
+
export function composeCoreHooks(subtrees, loadRegistry) {
|
|
16
|
+
const eligible = createCoreHookCatalog(subtrees);
|
|
17
|
+
const visit = (node, segments) => {
|
|
18
|
+
if (node.kind === 'leaf') {
|
|
19
|
+
const path = segments.join(' ');
|
|
20
|
+
if (!eligible.has(path))
|
|
21
|
+
return node;
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
...node,
|
|
24
|
+
effectiveHelp: projectedHelp(node, path, loadRegistry),
|
|
25
|
+
run: hookedRun(node, segments, loadRegistry),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return { ...node, children: node.children.map((child) => visit(child, [...segments, child.name])) };
|
|
29
|
+
};
|
|
30
|
+
return subtrees.map((subtree) => visit(subtree, [subtree.name]));
|
|
31
|
+
}
|
|
32
|
+
function projectedHelp(leaf, path, loadRegistry) {
|
|
33
|
+
return async () => {
|
|
34
|
+
const { projectEffectiveLeafHelp } = await import('./help.js');
|
|
35
|
+
try {
|
|
36
|
+
const registry = await loadRegistry();
|
|
37
|
+
// Invalid configuration fails closed on dispatch, but help remains the
|
|
38
|
+
// bounded static core contract so recovery is never stranded.
|
|
39
|
+
return projectEffectiveLeafHelp(leaf.help, registry.valid ? registry.plans.get(path) : undefined);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return projectEffectiveLeafHelp(leaf.help);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function hookedRun(leaf, segments, loadRegistry) {
|
|
47
|
+
const core = leaf.run;
|
|
48
|
+
const path = segments.join(' ');
|
|
49
|
+
return async (input, context) => {
|
|
50
|
+
const registry = await loadRegistry();
|
|
51
|
+
if (!registry.valid)
|
|
52
|
+
throw registryError(path, registry.issues);
|
|
53
|
+
const plan = registry.plans.get(path);
|
|
54
|
+
if (plan === undefined)
|
|
55
|
+
return core(input, context);
|
|
56
|
+
// The one fresh operation identity runCli already entered surrounds this
|
|
57
|
+
// whole pipeline; hooks and the core primary share that ambient ID.
|
|
58
|
+
const operationId = operationIdContext.current();
|
|
59
|
+
if (operationId === undefined)
|
|
60
|
+
throw new CrtrError('internal', `\`${path}\` ran outside a crouter operation identity, so its plugin hooks cannot be dispatched.`, ExitCode.GENERAL, { command: [...segments] });
|
|
61
|
+
const { invokeExecHook } = await import('./transport/exec-invoke.js');
|
|
62
|
+
const spec = (hook) => ({
|
|
63
|
+
plugin: hook.plugin.name,
|
|
64
|
+
executable: hook.executable,
|
|
65
|
+
op: hook.op,
|
|
66
|
+
phase: hook.phase,
|
|
67
|
+
commandPath: segments,
|
|
68
|
+
...(hook.phase === 'replace' ? { output: leaf.help.output } : {}),
|
|
69
|
+
});
|
|
70
|
+
const providedParams = [...(context?.providedParams ?? [])];
|
|
71
|
+
for (const hook of plan.before)
|
|
72
|
+
await invokeExecHook(spec(hook), { operationId, input, providedParams });
|
|
73
|
+
const replacement = plan.replacements[0];
|
|
74
|
+
const result = replacement === undefined
|
|
75
|
+
? await core(input, context)
|
|
76
|
+
: await invokeExecHook(spec(replacement), { operationId, input, providedParams });
|
|
77
|
+
// `after` is reverse discovery order, preserving middleware nesting.
|
|
78
|
+
for (const hook of plan.after)
|
|
79
|
+
await invokeExecHook(spec(hook), { operationId, input, providedParams, ...(result === undefined ? {} : { result }) });
|
|
80
|
+
return result;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Fail closed on an invalid manifest, stale target, or replacement collision:
|
|
84
|
+
* silently dropping a declared hook would run the command without behavior the
|
|
85
|
+
* caller expects. Every issue is spelled out in the message, because the
|
|
86
|
+
* central error payload carries only code/message/received/field/next — an
|
|
87
|
+
* issue list parked in `details` would never reach the caller. */
|
|
88
|
+
function registryError(path, issues) {
|
|
89
|
+
const first = issues[0];
|
|
90
|
+
const detail = issues
|
|
91
|
+
.map((issue) => `${issue.plugin} (${issue.scope}) ${issue.path ?? 'hooks'}: ${issue.message} — received ${issue.received}, expected ${issue.expected}`)
|
|
92
|
+
.join('; ');
|
|
93
|
+
const plugins = [...new Set(issues.map((issue) => issue.plugin))];
|
|
94
|
+
return new CrtrError(first.code, `\`${path}\` cannot run: ${String(issues.length)} unusable hook declaration${issues.length === 1 ? '' : 's'} — ${detail}.`, ExitCode.GENERAL, {
|
|
95
|
+
received: first.received,
|
|
96
|
+
...(first.path === undefined ? {} : { field: first.path }),
|
|
97
|
+
next: `${first.next} ${plugins.map((plugin) => `\`crtr pkg plugin show ${plugin}\``).join(' and ')} or \`crtr sys doctor\` reports the same issues; disabling the plugin also clears them.`,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { InstalledPlugin } from '../../types.js';
|
|
2
|
+
import type { CoreHookCatalog } from './catalog.js';
|
|
3
|
+
import type { HookManifestIssue, HookPhase, ValidatedHookManifest } from './schema.js';
|
|
4
|
+
export type HookDiscoveryIssueCode = HookManifestIssue['code'] | 'command_hook_target_invalid' | 'command_hook_collision';
|
|
5
|
+
export interface HookDiscoveryIssue {
|
|
6
|
+
code: HookDiscoveryIssueCode;
|
|
7
|
+
plugin: string;
|
|
8
|
+
scope: 'project' | 'user';
|
|
9
|
+
path?: string;
|
|
10
|
+
message: string;
|
|
11
|
+
received: string;
|
|
12
|
+
expected: string;
|
|
13
|
+
next: string;
|
|
14
|
+
}
|
|
15
|
+
export interface HookPluginAttribution {
|
|
16
|
+
name: string;
|
|
17
|
+
scope: 'project' | 'user';
|
|
18
|
+
root: string;
|
|
19
|
+
}
|
|
20
|
+
/** One declaration resolved to the local executable that will receive it. */
|
|
21
|
+
export interface EffectiveHook {
|
|
22
|
+
executable: string;
|
|
23
|
+
plugin: HookPluginAttribution;
|
|
24
|
+
target: string;
|
|
25
|
+
phase: HookPhase;
|
|
26
|
+
op: string;
|
|
27
|
+
description: string;
|
|
28
|
+
effects: readonly string[];
|
|
29
|
+
/** Index in this plugin's hooks.json declaration array. */
|
|
30
|
+
declarationIndex: number;
|
|
31
|
+
/** Stable discovery order: scope, lexical plugin name, declaration index. */
|
|
32
|
+
order: number;
|
|
33
|
+
}
|
|
34
|
+
export interface EffectiveHookPlan {
|
|
35
|
+
target: string;
|
|
36
|
+
before: readonly EffectiveHook[];
|
|
37
|
+
/** Reverse discovery order: execute directly after the primary. */
|
|
38
|
+
after: readonly EffectiveHook[];
|
|
39
|
+
replacements: readonly EffectiveHook[];
|
|
40
|
+
}
|
|
41
|
+
export interface HookPluginValidation {
|
|
42
|
+
plugin: InstalledPlugin;
|
|
43
|
+
manifestPath?: string;
|
|
44
|
+
executablePath?: string;
|
|
45
|
+
manifest?: ValidatedHookManifest;
|
|
46
|
+
hooks: readonly EffectiveHook[];
|
|
47
|
+
issues: readonly HookDiscoveryIssue[];
|
|
48
|
+
}
|
|
49
|
+
export interface HookRegistry {
|
|
50
|
+
catalog: CoreHookCatalog;
|
|
51
|
+
hooks: readonly EffectiveHook[];
|
|
52
|
+
plans: ReadonlyMap<string, EffectiveHookPlan>;
|
|
53
|
+
validations: readonly HookPluginValidation[];
|
|
54
|
+
issues: readonly HookDiscoveryIssue[];
|
|
55
|
+
valid: boolean;
|
|
56
|
+
}
|
|
57
|
+
export interface HookScope {
|
|
58
|
+
scope: 'project' | 'user';
|
|
59
|
+
root: string;
|
|
60
|
+
}
|
|
61
|
+
/** Project scopes nearest-outward, followed by the user scope. */
|
|
62
|
+
export declare function hookDiscoveryScopes(startDir?: string, profileId?: string | null): HookScope[];
|
|
63
|
+
/** Effective enabled hook plugins, with disabled nearest copies still shadowing outer copies. */
|
|
64
|
+
export declare function effectiveHookPlugins(startDir?: string, profileId?: string | null): InstalledPlugin[];
|
|
65
|
+
/** Effective hook plugins with one source-install candidate taking the exact
|
|
66
|
+
* scope/name slot it would occupy after activation. The candidate remains
|
|
67
|
+
* subject to normal nearer-scope shadowing and no plugin code is executed. */
|
|
68
|
+
export interface HookPluginCandidate {
|
|
69
|
+
plugin: InstalledPlugin;
|
|
70
|
+
scopeRoot: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Statically compiles hooks from already-resolved effective plugins. It reads
|
|
74
|
+
* manifest files to validate them but never executes a plugin executable.
|
|
75
|
+
*/
|
|
76
|
+
export declare function compileHookRegistry(catalog: CoreHookCatalog, plugins: readonly InstalledPlugin[]): HookRegistry;
|
|
77
|
+
/** Discovers and compiles the invocation-local effective registry. */
|
|
78
|
+
export declare function discoverHookRegistry(catalog: CoreHookCatalog, startDir?: string, profileId?: string | null): HookRegistry;
|
|
79
|
+
/** Compile the exact effective set a source candidate would create before its
|
|
80
|
+
* bytes replace the installed plugin. */
|
|
81
|
+
export declare function discoverCandidateHookRegistry(catalog: CoreHookCatalog, candidate: InstalledPlugin, scopeRoot: string, startDir?: string, profileId?: string | null): HookRegistry;
|
|
82
|
+
/** Compile the exact effective set after all supplied source candidates take
|
|
83
|
+
* their scope/name slots. This lets one transaction reject collisions introduced
|
|
84
|
+
* only by the combined revision. */
|
|
85
|
+
export declare function discoverCandidateHookRegistryForCandidates(catalog: CoreHookCatalog, candidates: readonly HookPluginCandidate[], startDir?: string, profileId?: string | null): HookRegistry;
|
|
86
|
+
/** Fixture-friendly discovery seam; production callers should use discoverHookRegistry. */
|
|
87
|
+
export declare function discoverHookRegistryInScopes(catalog: CoreHookCatalog, scopes: readonly HookScope[]): HookRegistry;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { listInstalledPluginsInRoot } from '../resolver.js';
|
|
2
|
+
import { projectScopeRoots, userScopeRoot } from '../scope.js';
|
|
3
|
+
import { validatePluginHookArtifact } from './artifact.js';
|
|
4
|
+
function hasHookDeclaration(plugin) {
|
|
5
|
+
return plugin.manifest.hooks !== undefined || plugin.manifest.hookExecutable !== undefined;
|
|
6
|
+
}
|
|
7
|
+
function orderedPluginsInScopes(scopes) {
|
|
8
|
+
const seen = new Set();
|
|
9
|
+
const result = [];
|
|
10
|
+
for (const scope of scopes) {
|
|
11
|
+
const plugins = listInstalledPluginsInRoot(scope.scope, scope.root).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
12
|
+
for (const plugin of plugins) {
|
|
13
|
+
if (seen.has(plugin.name))
|
|
14
|
+
continue;
|
|
15
|
+
seen.add(plugin.name);
|
|
16
|
+
if (plugin.enabled && hasHookDeclaration(plugin))
|
|
17
|
+
result.push(plugin);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
/** Project scopes nearest-outward, followed by the user scope. */
|
|
23
|
+
export function hookDiscoveryScopes(startDir = process.cwd(), profileId) {
|
|
24
|
+
return [
|
|
25
|
+
...projectScopeRoots(startDir, profileId).map((root) => ({ scope: 'project', root })),
|
|
26
|
+
{ scope: 'user', root: userScopeRoot() },
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
/** Effective enabled hook plugins, with disabled nearest copies still shadowing outer copies. */
|
|
30
|
+
export function effectiveHookPlugins(startDir = process.cwd(), profileId) {
|
|
31
|
+
return orderedPluginsInScopes(hookDiscoveryScopes(startDir, profileId));
|
|
32
|
+
}
|
|
33
|
+
function effectiveHookPluginsWithCandidates(candidates, startDir = process.cwd(), profileId) {
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
const result = [];
|
|
36
|
+
for (const scope of hookDiscoveryScopes(startDir, profileId)) {
|
|
37
|
+
const replacements = candidates.filter((candidate) => candidate.plugin.scope === scope.scope && candidate.scopeRoot === scope.root);
|
|
38
|
+
const replacedNames = new Set(replacements.map((candidate) => candidate.plugin.name));
|
|
39
|
+
const plugins = [
|
|
40
|
+
...listInstalledPluginsInRoot(scope.scope, scope.root).filter((plugin) => !replacedNames.has(plugin.name)),
|
|
41
|
+
...replacements.map((candidate) => candidate.plugin),
|
|
42
|
+
];
|
|
43
|
+
plugins.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
44
|
+
for (const plugin of plugins) {
|
|
45
|
+
if (seen.has(plugin.name))
|
|
46
|
+
continue;
|
|
47
|
+
seen.add(plugin.name);
|
|
48
|
+
if (plugin.enabled && hasHookDeclaration(plugin))
|
|
49
|
+
result.push(plugin);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
function issue(plugin, value) {
|
|
55
|
+
return { ...value, plugin: plugin.name, scope: plugin.scope };
|
|
56
|
+
}
|
|
57
|
+
function artifactIssues(plugin, issues) {
|
|
58
|
+
return issues.map((value) => issue(plugin, value));
|
|
59
|
+
}
|
|
60
|
+
function targetIssues(plugin, hooks, catalog) {
|
|
61
|
+
const issues = [];
|
|
62
|
+
hooks.forEach((hook, index) => {
|
|
63
|
+
if (catalog.has(hook.target))
|
|
64
|
+
return;
|
|
65
|
+
issues.push(issue(plugin, {
|
|
66
|
+
code: 'command_hook_target_invalid',
|
|
67
|
+
path: `hooks[${index}].target`,
|
|
68
|
+
message: `hook target "${hook.target}" is not an eligible core returned-object leaf`,
|
|
69
|
+
received: hook.target,
|
|
70
|
+
expected: 'an exact eligible core leaf path',
|
|
71
|
+
next: 'Choose a current returned-object core leaf; pkg plugin, sys doctor, JSONL, passthrough, and UI/void surfaces are unavailable.',
|
|
72
|
+
}));
|
|
73
|
+
});
|
|
74
|
+
return issues;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Statically compiles hooks from already-resolved effective plugins. It reads
|
|
78
|
+
* manifest files to validate them but never executes a plugin executable.
|
|
79
|
+
*/
|
|
80
|
+
export function compileHookRegistry(catalog, plugins) {
|
|
81
|
+
const validations = [];
|
|
82
|
+
const hooks = [];
|
|
83
|
+
const issues = [];
|
|
84
|
+
let order = 0;
|
|
85
|
+
for (const plugin of plugins) {
|
|
86
|
+
const artifact = validatePluginHookArtifact(plugin.root, plugin.manifest);
|
|
87
|
+
const pluginIssues = artifactIssues(plugin, artifact.issues);
|
|
88
|
+
let compiled = [];
|
|
89
|
+
if (pluginIssues.length === 0 && artifact.manifest !== undefined && artifact.executablePath !== undefined) {
|
|
90
|
+
const invalidTargets = targetIssues(plugin, artifact.manifest.hooks, catalog);
|
|
91
|
+
pluginIssues.push(...invalidTargets);
|
|
92
|
+
if (pluginIssues.length === 0) {
|
|
93
|
+
compiled = artifact.manifest.hooks.map((hook, declarationIndex) => Object.freeze({
|
|
94
|
+
executable: artifact.executablePath,
|
|
95
|
+
plugin: Object.freeze({ name: plugin.name, scope: plugin.scope, root: plugin.root }),
|
|
96
|
+
target: hook.target,
|
|
97
|
+
phase: hook.phase,
|
|
98
|
+
op: hook.op,
|
|
99
|
+
description: hook.description,
|
|
100
|
+
effects: Object.freeze([...hook.effects]),
|
|
101
|
+
declarationIndex,
|
|
102
|
+
order: order++,
|
|
103
|
+
}));
|
|
104
|
+
hooks.push(...compiled);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
validations.push(Object.freeze({
|
|
108
|
+
plugin,
|
|
109
|
+
...(artifact.manifestPath === undefined ? {} : { manifestPath: artifact.manifestPath }),
|
|
110
|
+
...(artifact.executablePath === undefined ? {} : { executablePath: artifact.executablePath }),
|
|
111
|
+
...(artifact.manifest === undefined ? {} : { manifest: artifact.manifest }),
|
|
112
|
+
hooks: Object.freeze(compiled),
|
|
113
|
+
issues: Object.freeze(pluginIssues),
|
|
114
|
+
}));
|
|
115
|
+
issues.push(...pluginIssues);
|
|
116
|
+
}
|
|
117
|
+
const byTarget = new Map();
|
|
118
|
+
for (const hook of hooks) {
|
|
119
|
+
const entries = byTarget.get(hook.target);
|
|
120
|
+
if (entries === undefined)
|
|
121
|
+
byTarget.set(hook.target, [hook]);
|
|
122
|
+
else
|
|
123
|
+
entries.push(hook);
|
|
124
|
+
}
|
|
125
|
+
const plans = new Map();
|
|
126
|
+
for (const [target, entries] of byTarget) {
|
|
127
|
+
const replacements = entries.filter((entry) => entry.phase === 'replace');
|
|
128
|
+
if (replacements.length > 1) {
|
|
129
|
+
for (const replacement of replacements) {
|
|
130
|
+
issues.push(issue(replacement.plugin, {
|
|
131
|
+
code: 'command_hook_collision',
|
|
132
|
+
path: `hooks[${replacement.declarationIndex}]`,
|
|
133
|
+
message: `multiple replacements target "${target}"`,
|
|
134
|
+
received: replacements.map((entry) => `${entry.plugin.name}:${entry.op}`).join(', '),
|
|
135
|
+
expected: 'exactly one replacement for this core leaf',
|
|
136
|
+
next: 'Remove or change one replace declaration; before and after hooks may coexist with one replacement.',
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
plans.set(target, Object.freeze({
|
|
141
|
+
target,
|
|
142
|
+
before: Object.freeze(entries.filter((entry) => entry.phase === 'before')),
|
|
143
|
+
after: Object.freeze(entries.filter((entry) => entry.phase === 'after').reverse()),
|
|
144
|
+
replacements: Object.freeze(replacements),
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
return Object.freeze({
|
|
148
|
+
catalog,
|
|
149
|
+
hooks: Object.freeze(hooks),
|
|
150
|
+
plans,
|
|
151
|
+
validations: Object.freeze(validations),
|
|
152
|
+
issues: Object.freeze(issues),
|
|
153
|
+
valid: issues.length === 0,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/** Discovers and compiles the invocation-local effective registry. */
|
|
157
|
+
export function discoverHookRegistry(catalog, startDir = process.cwd(), profileId) {
|
|
158
|
+
return compileHookRegistry(catalog, effectiveHookPlugins(startDir, profileId));
|
|
159
|
+
}
|
|
160
|
+
/** Compile the exact effective set a source candidate would create before its
|
|
161
|
+
* bytes replace the installed plugin. */
|
|
162
|
+
export function discoverCandidateHookRegistry(catalog, candidate, scopeRoot, startDir = process.cwd(), profileId) {
|
|
163
|
+
return discoverCandidateHookRegistryForCandidates(catalog, [{ plugin: candidate, scopeRoot }], startDir, profileId);
|
|
164
|
+
}
|
|
165
|
+
/** Compile the exact effective set after all supplied source candidates take
|
|
166
|
+
* their scope/name slots. This lets one transaction reject collisions introduced
|
|
167
|
+
* only by the combined revision. */
|
|
168
|
+
export function discoverCandidateHookRegistryForCandidates(catalog, candidates, startDir = process.cwd(), profileId) {
|
|
169
|
+
return compileHookRegistry(catalog, effectiveHookPluginsWithCandidates(candidates, startDir, profileId));
|
|
170
|
+
}
|
|
171
|
+
/** Fixture-friendly discovery seam; production callers should use discoverHookRegistry. */
|
|
172
|
+
export function discoverHookRegistryInScopes(catalog, scopes) {
|
|
173
|
+
return compileHookRegistry(catalog, orderedPluginsInScopes(scopes));
|
|
174
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { EffectiveLeafHelp, LeafHelp } from '../help.js';
|
|
2
|
+
import type { EffectiveHookPlan } from './discovery.js';
|
|
3
|
+
/** Build a frozen help view whose Effects order matches the effective pipeline.
|
|
4
|
+
* Core help is never modified; all non-effects fields retain their references. */
|
|
5
|
+
export declare function projectEffectiveLeafHelp(core: LeafHelp, plan?: EffectiveHookPlan): EffectiveLeafHelp;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function attributedEffects(hook) {
|
|
2
|
+
const attribution = `plugin=${hook.plugin.name} phase=${hook.phase} op=${hook.op}`;
|
|
3
|
+
return hook.effects.map((effect) => `[${attribution}] ${effect}`);
|
|
4
|
+
}
|
|
5
|
+
/** Build a frozen help view whose Effects order matches the effective pipeline.
|
|
6
|
+
* Core help is never modified; all non-effects fields retain their references. */
|
|
7
|
+
export function projectEffectiveLeafHelp(core, plan) {
|
|
8
|
+
const before = plan?.before.flatMap(attributedEffects) ?? [];
|
|
9
|
+
const replacement = plan?.replacements.length === 1 ? plan.replacements[0] : undefined;
|
|
10
|
+
const primary = replacement === undefined
|
|
11
|
+
? core.effects
|
|
12
|
+
: attributedEffects(replacement);
|
|
13
|
+
const after = plan?.after.flatMap(attributedEffects) ?? [];
|
|
14
|
+
return Object.freeze({
|
|
15
|
+
...core,
|
|
16
|
+
effects: Object.freeze([...before, ...primary, ...after]),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createCoreHookCatalog, type CoreHookCatalog, type CoreHookTarget } from './catalog.js';
|
|
2
|
+
export { projectEffectiveLeafHelp } from './help.js';
|
|
3
|
+
export { compileHookRegistry, discoverCandidateHookRegistry, discoverHookRegistry, discoverHookRegistryInScopes, effectiveHookPlugins, hookDiscoveryScopes, type EffectiveHook, type EffectiveHookPlan, type HookDiscoveryIssue, type HookPluginAttribution, type HookPluginValidation, type HookRegistry, type HookScope, } from './discovery.js';
|
|
4
|
+
export { validatePluginHookArtifact, type HookArtifactValidation } from './artifact.js';
|
|
5
|
+
export { hasDeclaredHooks, hookReport, HOOK_TRUST_WARNING, type HookDeclarationReport, type HookReport } from './report.js';
|
|
6
|
+
export { validateHookManifest, type DeclaredHook, type HookManifestIssue, type HookManifestValidation, type HookPhase, type ValidatedHookManifest, } from './schema.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createCoreHookCatalog } from './catalog.js';
|
|
2
|
+
export { projectEffectiveLeafHelp } from './help.js';
|
|
3
|
+
export { compileHookRegistry, discoverCandidateHookRegistry, discoverHookRegistry, discoverHookRegistryInScopes, effectiveHookPlugins, hookDiscoveryScopes, } from './discovery.js';
|
|
4
|
+
export { validatePluginHookArtifact } from './artifact.js';
|
|
5
|
+
export { hasDeclaredHooks, hookReport, HOOK_TRUST_WARNING } from './report.js';
|
|
6
|
+
export { validateHookManifest, } from './schema.js';
|