@north-light/crouter 0.3.219 → 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/panels/broker-limits-panel.js +3 -3
- 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__/helpers/broker-clients.d.ts +1 -0
- package/dist/core/__tests__/helpers/broker-clients.js +1 -0
- 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__/seam/dormancy-release.test.js +37 -1
- 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/client-registry.d.ts +6 -3
- package/dist/core/runtime/broker/client-registry.js +6 -4
- 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 +16 -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 +13 -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/dist/types.js +5 -1
- 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
|
@@ -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';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PluginManifest } from '../../types.js';
|
|
2
|
+
import type { HookDiscoveryIssue, HookPluginValidation } from './discovery.js';
|
|
3
|
+
import type { DeclaredHook } from './schema.js';
|
|
4
|
+
export declare const HOOK_TRUST_WARNING = "Hooks receive normalized inputs; after hooks receive results. They run implicitly and may block or replace declared targets. The local executable runs with caller authority and can transmit received data.";
|
|
5
|
+
export interface HookDeclarationReport {
|
|
6
|
+
target: string;
|
|
7
|
+
phase: DeclaredHook['phase'];
|
|
8
|
+
op: string;
|
|
9
|
+
description: string;
|
|
10
|
+
effects: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
/** One static hook artifact report shared by plugin inspection and lifecycle
|
|
13
|
+
* output. It only reads declarations and filesystem metadata; it never runs
|
|
14
|
+
* the hook executable. */
|
|
15
|
+
export interface HookReport {
|
|
16
|
+
manifestPath?: string;
|
|
17
|
+
executablePath?: string;
|
|
18
|
+
declarations: readonly HookDeclarationReport[];
|
|
19
|
+
issues: readonly HookDiscoveryIssue[];
|
|
20
|
+
trust: typeof HOOK_TRUST_WARNING;
|
|
21
|
+
}
|
|
22
|
+
export declare function hasDeclaredHooks(manifest: Pick<PluginManifest, 'hooks' | 'hookExecutable'>): boolean;
|
|
23
|
+
export declare function hookReport(validation: HookPluginValidation, issues?: readonly HookDiscoveryIssue[]): HookReport;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const HOOK_TRUST_WARNING = 'Hooks receive normalized inputs; after hooks receive results. They run implicitly and may block or replace declared targets. The local executable runs with caller authority and can transmit received data.';
|
|
2
|
+
export function hasDeclaredHooks(manifest) {
|
|
3
|
+
return manifest.hooks !== undefined || manifest.hookExecutable !== undefined;
|
|
4
|
+
}
|
|
5
|
+
export function hookReport(validation, issues = validation.issues) {
|
|
6
|
+
return Object.freeze({
|
|
7
|
+
...(validation.manifestPath === undefined ? {} : { manifestPath: validation.manifestPath }),
|
|
8
|
+
...(validation.executablePath === undefined ? {} : { executablePath: validation.executablePath }),
|
|
9
|
+
declarations: Object.freeze((validation.manifest?.hooks ?? []).map((hook) => Object.freeze({
|
|
10
|
+
target: hook.target,
|
|
11
|
+
phase: hook.phase,
|
|
12
|
+
op: hook.op,
|
|
13
|
+
description: hook.description,
|
|
14
|
+
effects: Object.freeze([...hook.effects]),
|
|
15
|
+
}))),
|
|
16
|
+
issues: Object.freeze([...issues]),
|
|
17
|
+
trust: HOOK_TRUST_WARNING,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type HookPhase = 'before' | 'after' | 'replace';
|
|
2
|
+
export interface DeclaredHook {
|
|
3
|
+
target: string;
|
|
4
|
+
phase: HookPhase;
|
|
5
|
+
op: string;
|
|
6
|
+
description: string;
|
|
7
|
+
effects: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface ValidatedHookManifest {
|
|
10
|
+
schemaVersion: 1;
|
|
11
|
+
hooks: DeclaredHook[];
|
|
12
|
+
}
|
|
13
|
+
export type HookIssueCode = 'hook_manifest_unreadable' | 'hook_manifest_invalid' | 'hook_schema_version' | 'hook_path_unsafe' | 'hook_not_executable';
|
|
14
|
+
export interface HookManifestIssue {
|
|
15
|
+
code: HookIssueCode;
|
|
16
|
+
path?: string;
|
|
17
|
+
message: string;
|
|
18
|
+
received: string;
|
|
19
|
+
expected: string;
|
|
20
|
+
next: string;
|
|
21
|
+
}
|
|
22
|
+
export interface HookManifestValidation {
|
|
23
|
+
manifest?: ValidatedHookManifest;
|
|
24
|
+
issues: HookManifestIssue[];
|
|
25
|
+
}
|
|
26
|
+
/** Validates the declarative hooks.json payload without filesystem access. */
|
|
27
|
+
export declare function validateHookManifest(raw: unknown): HookManifestValidation;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { isRecord } from '../../shared/predicates.js';
|
|
2
|
+
function typeName(value) {
|
|
3
|
+
if (value === null)
|
|
4
|
+
return 'null';
|
|
5
|
+
if (Array.isArray(value))
|
|
6
|
+
return 'array';
|
|
7
|
+
return typeof value;
|
|
8
|
+
}
|
|
9
|
+
/** Validates the declarative hooks.json payload without filesystem access. */
|
|
10
|
+
export function validateHookManifest(raw) {
|
|
11
|
+
const issues = [];
|
|
12
|
+
const issue = (code, message, received, expected, next, path) => {
|
|
13
|
+
issues.push({ code, message, received, expected, next, ...(path === undefined ? {} : { path }) });
|
|
14
|
+
};
|
|
15
|
+
if (!isRecord(raw)) {
|
|
16
|
+
issue('hook_manifest_invalid', 'manifest must be an object', typeName(raw), '{ schemaVersion, hooks }', 'Provide a valid hooks.json manifest.');
|
|
17
|
+
return { issues };
|
|
18
|
+
}
|
|
19
|
+
const unknown = Object.keys(raw).filter((key) => key !== 'schemaVersion' && key !== 'hooks');
|
|
20
|
+
if (unknown.length > 0) {
|
|
21
|
+
issue('hook_manifest_invalid', 'unknown top-level keys', unknown.join(', '), 'only: schemaVersion, hooks', 'Remove the unknown keys.');
|
|
22
|
+
return { issues };
|
|
23
|
+
}
|
|
24
|
+
if (raw['schemaVersion'] !== 1) {
|
|
25
|
+
issue('hook_schema_version', 'schemaVersion must be exactly 1', String(raw['schemaVersion']), '1', 'Update the manifest schema version to 1.', 'schemaVersion');
|
|
26
|
+
return { issues };
|
|
27
|
+
}
|
|
28
|
+
if (!Array.isArray(raw['hooks']) || raw['hooks'].length === 0) {
|
|
29
|
+
issue('hook_manifest_invalid', 'hooks must be a non-empty array', typeName(raw['hooks']), 'a non-empty array of hook declarations', 'Declare at least one hook.', 'hooks');
|
|
30
|
+
return { issues };
|
|
31
|
+
}
|
|
32
|
+
const hooks = [];
|
|
33
|
+
for (let index = 0; index < raw['hooks'].length; index++) {
|
|
34
|
+
const value = raw['hooks'][index];
|
|
35
|
+
const path = `hooks[${index}]`;
|
|
36
|
+
if (!isRecord(value)) {
|
|
37
|
+
issue('hook_manifest_invalid', 'hook must be an object', typeName(value), '{ target, phase, op, description, effects }', 'Fix the hook declaration.', path);
|
|
38
|
+
return { issues };
|
|
39
|
+
}
|
|
40
|
+
const keys = Object.keys(value).filter((key) => !['target', 'phase', 'op', 'description', 'effects'].includes(key));
|
|
41
|
+
if (keys.length > 0) {
|
|
42
|
+
issue('hook_manifest_invalid', 'unknown hook keys', keys.join(', '), 'only: target, phase, op, description, effects', 'Remove the unknown keys.', path);
|
|
43
|
+
return { issues };
|
|
44
|
+
}
|
|
45
|
+
for (const field of ['target', 'op', 'description']) {
|
|
46
|
+
if (typeof value[field] !== 'string' || value[field].length === 0) {
|
|
47
|
+
issue('hook_manifest_invalid', `${field} must be a non-empty string`, typeName(value[field]), 'a non-empty string', `Set ${field}.`, `${path}.${field}`);
|
|
48
|
+
return { issues };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (value['phase'] !== 'before' && value['phase'] !== 'after' && value['phase'] !== 'replace') {
|
|
52
|
+
issue('hook_manifest_invalid', 'phase must be before|after|replace', String(value['phase']), 'before | after | replace', 'Set phase to a supported hook phase.', `${path}.phase`);
|
|
53
|
+
return { issues };
|
|
54
|
+
}
|
|
55
|
+
if (!Array.isArray(value['effects']) || value['effects'].length === 0 || !value['effects'].every((effect) => typeof effect === 'string' && effect.length > 0)) {
|
|
56
|
+
issue('hook_manifest_invalid', 'effects must be a non-empty string array', typeName(value['effects']), 'a non-empty array of non-empty strings', 'Declare every persistent effect.', `${path}.effects`);
|
|
57
|
+
return { issues };
|
|
58
|
+
}
|
|
59
|
+
hooks.push({
|
|
60
|
+
target: value['target'],
|
|
61
|
+
phase: value['phase'],
|
|
62
|
+
op: value['op'],
|
|
63
|
+
description: value['description'],
|
|
64
|
+
effects: value['effects'],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return { manifest: { schemaVersion: 1, hooks }, issues };
|
|
68
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { HookPhase } from '../../../hook-authoring.js';
|
|
2
|
+
import type { Field } from '../../help.js';
|
|
3
|
+
export declare const EXEC_HOOK_TIMEOUT_MS = 15000;
|
|
4
|
+
export interface ExecHookSpec {
|
|
5
|
+
plugin: string;
|
|
6
|
+
/** Absolute, validated hookExecutable path. */
|
|
7
|
+
executable: string;
|
|
8
|
+
op: string;
|
|
9
|
+
phase: HookPhase;
|
|
10
|
+
commandPath: readonly string[];
|
|
11
|
+
/** Required for replacement hooks; ignored for before and after hooks. */
|
|
12
|
+
output?: Field[];
|
|
13
|
+
}
|
|
14
|
+
export interface ExecHookInvocation {
|
|
15
|
+
operationId: string;
|
|
16
|
+
input: Record<string, unknown>;
|
|
17
|
+
providedParams: readonly string[];
|
|
18
|
+
/** The completed primary result, present only for an after hook. */
|
|
19
|
+
result?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
/** Invoke one hook executable; after failures retain the completed primary result. */
|
|
22
|
+
export declare function invokeExecHook(spec: ExecHookSpec, invocation: ExecHookInvocation): Promise<Record<string, unknown> | void>;
|