@north-light/crouter 0.3.164 → 0.3.165
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/build-root.d.ts +6 -6
- package/dist/build-root.js +12 -13
- package/dist/builtin-memory/00-runtime-base.md +3 -4
- package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
- package/dist/builtin-memory/02-lifecycle/00-terminal.md +2 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +16 -18
- package/dist/builtin-memory/05-kinds/advisor/00-base.md +0 -2
- package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +3 -9
- package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -3
- package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +2 -10
- package/dist/builtin-memory/05-kinds/explore/00-base.md +2 -2
- package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +0 -2
- package/dist/builtin-memory/05-kinds/general/00-base.md +3 -5
- package/dist/builtin-memory/05-kinds/general/01-orchestrator.md +0 -4
- package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +1 -3
- package/dist/builtin-memory/05-kinds/plan/reviewers/00-base.md +12 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +0 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +0 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +0 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -1
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +0 -2
- package/dist/builtin-memory/05-kinds/review/00-base.md +0 -2
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -3
- package/dist/builtin-memory/05-kinds/spec/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +2 -4
- package/dist/builtin-memory/05-kinds/spec/requirements.md +1 -1
- package/dist/builtin-memory/advisor/council.md +40 -0
- package/dist/builtin-memory/design.md +8 -11
- package/dist/builtin-memory/development.md +6 -9
- package/dist/builtin-memory/internal/INDEX.md +4 -5
- package/dist/builtin-memory/internal/agent-shaping.md +5 -7
- package/dist/builtin-memory/internal/examples/imessage-assistant.md +3 -3
- package/dist/builtin-memory/internal/marketplaces.md +13 -14
- package/dist/builtin-memory/internal/nodes-and-canvas.md +4 -4
- package/dist/builtin-memory/internal/plugins.md +61 -45
- package/dist/builtin-memory/planning.md +4 -7
- package/dist/builtin-memory/spec.md +9 -12
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +8 -22
- package/dist/cli.js +1 -1
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +17 -4
- package/dist/clients/attach/__tests__/context-message.test.js +31 -20
- package/dist/clients/attach/__tests__/crtr-output.test.js +39 -30
- package/dist/clients/attach/render/chat-view.d.ts +20 -20
- package/dist/clients/attach/render/chat-view.js +42 -54
- package/dist/clients/attach/render/{frozen-history.d.ts → condensed-history.d.ts} +1 -18
- package/dist/clients/attach/render/condensed-history.js +60 -0
- package/dist/clients/attach/render/context-message.js +9 -14
- package/dist/clients/attach/session/bindings.d.ts +5 -3
- package/dist/clients/attach/session/bindings.js +7 -13
- package/dist/clients/attach/session/keys.d.ts +2 -0
- package/dist/clients/attach/session/keys.js +36 -37
- package/dist/clients/attach/session/profile-files.d.ts +8 -0
- package/dist/clients/attach/session/profile-files.js +157 -0
- package/dist/clients/attach/viewer.js +525 -525
- package/dist/commands/node.js +2 -2
- package/dist/commands/pkg/plugin-inspect.js +6 -7
- package/dist/commands/pkg/plugin-manage.d.ts +1 -1
- package/dist/commands/pkg/plugin-manage.js +131 -19
- package/dist/commands/pkg/plugin.js +2 -2
- package/dist/commands/pkg.js +6 -11
- package/dist/commands/profile/env.js +3 -3
- package/dist/commands/sys/config.js +17 -76
- package/dist/commands/sys/doctor.js +5 -91
- package/dist/commands/sys/setup-wizard.d.ts +9 -11
- package/dist/commands/sys/setup-wizard.js +47 -81
- package/dist/core/__tests__/base-worker-prompt.test.js +18 -21
- package/dist/core/__tests__/command-plugins-surfaces.test.js +39 -5
- package/dist/core/__tests__/command-plugins.test.js +36 -16
- package/dist/core/__tests__/review-model-floor.test.js +2 -2
- package/dist/core/__tests__/tmux-surface.test.js +10 -1
- package/dist/core/command-manifests/manifest.d.ts +24 -0
- package/dist/core/{configured-clis → command-manifests}/manifest.js +21 -25
- package/dist/core/command-manifests/registry.d.ts +1 -2
- package/dist/core/command-manifests/registry.js +2 -2
- package/dist/core/command-manifests/schema.d.ts +11 -13
- package/dist/core/command-manifests/schema.js +53 -192
- package/dist/core/command-plugins/compose.d.ts +0 -6
- package/dist/core/command-plugins/compose.js +28 -75
- package/dist/core/command-plugins/discovery.d.ts +25 -58
- package/dist/core/command-plugins/discovery.js +152 -259
- package/dist/core/command-plugins/endpoint.d.ts +24 -0
- package/dist/core/command-plugins/endpoint.js +48 -0
- package/dist/core/command-plugins/store.d.ts +16 -0
- package/dist/core/command-plugins/store.js +64 -0
- package/dist/core/command-plugins/{adapter.d.ts → transport/exec-invoke.d.ts} +3 -3
- package/dist/core/command-plugins/{adapter.js → transport/exec-invoke.js} +4 -4
- package/dist/core/{configured-clis/fetch.d.ts → command-plugins/transport/http-fetch.d.ts} +9 -18
- package/dist/core/{configured-clis/fetch.js → command-plugins/transport/http-fetch.js} +15 -35
- package/dist/core/{configured-clis/invoker.d.ts → command-plugins/transport/http-invoke.d.ts} +7 -7
- package/dist/core/{configured-clis/invoker.js → command-plugins/transport/http-invoke.js} +21 -23
- package/dist/core/command.d.ts +8 -9
- package/dist/core/command.js +6 -9
- package/dist/core/config.js +6 -10
- package/dist/core/env-name.d.ts +6 -0
- package/dist/core/env-name.js +9 -0
- package/dist/core/io.d.ts +1 -1
- package/dist/core/keybindings/__tests__/resolve.test.js +40 -3
- package/dist/core/keybindings/attach-control.d.ts +37 -0
- package/dist/core/keybindings/attach-control.js +38 -0
- package/dist/core/keybindings/catalog.d.ts +5 -4
- package/dist/core/keybindings/catalog.js +16 -8
- package/dist/core/keybindings/index.d.ts +1 -0
- package/dist/core/keybindings/index.js +1 -0
- package/dist/core/keybindings/types.d.ts +1 -1
- package/dist/core/preview-registry.js +41 -74
- package/dist/core/runtime/bearings.d.ts +2 -5
- package/dist/core/runtime/bearings.js +2 -5
- package/dist/core/runtime/front-door.d.ts +1 -1
- package/dist/core/runtime/front-door.js +2 -2
- package/dist/core/runtime/kickoff.d.ts +3 -3
- package/dist/core/runtime/kickoff.js +4 -3
- package/dist/core/runtime/situational-context.d.ts +1 -1
- package/dist/core/runtime/situational-context.js +1 -1
- package/dist/core/runtime/spawn.js +9 -9
- package/dist/core/runtime/tmux.js +29 -2
- package/dist/core/scope.d.ts +0 -5
- package/dist/core/scope.js +0 -10
- package/dist/core/user-settings.d.ts +193 -0
- package/dist/core/user-settings.js +252 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/pi-extensions/canvas-stophook.js +3 -2
- package/dist/shared/generated-context.d.ts +34 -0
- package/dist/shared/generated-context.js +98 -0
- package/dist/types.d.ts +23 -16
- package/dist/types.js +3 -14
- package/dist/web-client/assets/{index-NIuSCOHM.js → index-BMTOGuOZ.js} +19 -19
- package/dist/web-client/assets/index-DvA6Zw-R.css +2 -0
- package/dist/web-client/index.html +2 -2
- package/dist/web-client/sw.js +1 -1
- package/docs/public-api.md +1 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/builtin-memory/05-kinds/product/00-base.md +0 -25
- package/dist/builtin-memory/05-kinds/product/01-orchestrator.md +0 -15
- package/dist/builtin-memory/05-kinds/product/teardown.md +0 -15
- package/dist/builtin-memory/internal/workflow-codification.md +0 -82
- package/dist/builtin-memory/product.md +0 -80
- package/dist/clients/attach/render/frozen-history.js +0 -100
- package/dist/commands/pkg/cli-inspect.d.ts +0 -17
- package/dist/commands/pkg/cli-inspect.js +0 -190
- package/dist/commands/pkg/cli-manage.d.ts +0 -3
- package/dist/commands/pkg/cli-manage.js +0 -206
- package/dist/commands/pkg/cli.d.ts +0 -1
- package/dist/commands/pkg/cli.js +0 -14
- package/dist/core/configured-clis/cache.d.ts +0 -16
- package/dist/core/configured-clis/cache.js +0 -57
- package/dist/core/configured-clis/compose.d.ts +0 -14
- package/dist/core/configured-clis/compose.js +0 -60
- package/dist/core/configured-clis/discovery.d.ts +0 -47
- package/dist/core/configured-clis/discovery.js +0 -173
- package/dist/core/configured-clis/manifest.d.ts +0 -24
- package/dist/core/configured-clis/registration.d.ts +0 -40
- package/dist/core/configured-clis/registration.js +0 -201
- package/dist/web-client/assets/index-CqLKj8Xu.css +0 -2
|
@@ -1,291 +1,184 @@
|
|
|
1
|
-
// Command-plugin discovery + validation.
|
|
2
|
-
//
|
|
3
|
-
// An installed, enabled plugin may declare `commands?: string` in its
|
|
4
|
-
// `plugin.json` — a plugin-root-relative path to one declarative command
|
|
5
|
-
// manifest (`commands.json`). This module reads and validates those manifests
|
|
6
|
-
// WITHOUT executing any plugin code, turning each into either an accepted
|
|
7
|
-
// contribution (a validated top-level branch node) or one or more typed
|
|
8
|
-
// discovery issues. crtr owns parse/help/render/errors; the plugin owns one
|
|
9
|
-
// executable that the adapter direct-spawns per leaf invocation.
|
|
10
|
-
//
|
|
11
|
-
// Everything here is invocation-local and side-effect-free: no cache, no
|
|
12
|
-
// persisted state, no spawning. `discoverCommandContributions` is called only
|
|
13
|
-
// on the fallthrough (non-core-fast-path) command path (see `build-root.ts`);
|
|
14
|
-
// `validateEffectiveCommandPlugins` is the entry point Phase 2 lifecycle
|
|
15
|
-
// surfaces (`pkg plugin show`, `sys doctor`) consume to report issues — it
|
|
16
|
-
// carries cross-plugin collision issues that the per-plugin
|
|
17
|
-
// `validatePluginCommands` structurally cannot see.
|
|
18
1
|
import { realpathSync, readFileSync, statSync } from 'node:fs';
|
|
19
2
|
import { resolve, sep } from 'node:path';
|
|
20
3
|
import { listInstalledPlugins, listInstalledPluginsInRoot } from '../resolver.js';
|
|
21
4
|
import { projectScopeRoots } from '../scope.js';
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
/** The effective, deduped set of enabled plugins that declare a command
|
|
29
|
-
* manifest, in scope-precedence order. Enumerated with existing machinery
|
|
30
|
-
* only: `projectScopeRoots()` (profile-widened, nearest-first) via
|
|
31
|
-
* `listInstalledPluginsInRoot('project', root)`, then user scope. Dedup is by
|
|
32
|
-
* plugin NAME across ALL copies (highest precedence wins) BEFORE the enabled
|
|
33
|
-
* filter — so a disabled higher-precedence copy suppresses lower-precedence
|
|
34
|
-
* copies of the same name. */
|
|
5
|
+
import { validateCommandManifest } from '../command-manifests/manifest.js';
|
|
6
|
+
import { resolveCommandRegistry } from '../command-manifests/registry.js';
|
|
7
|
+
import { adaptPluginContributions } from './compose.js';
|
|
8
|
+
import { validateHttpPluginTransport } from './endpoint.js';
|
|
9
|
+
import { readHttpPluginManifest, resolveHttpPluginManifestStore, writeHttpPluginManifest } from './store.js';
|
|
10
|
+
import { fetchHttpPluginManifest } from './transport/http-fetch.js';
|
|
35
11
|
export function effectiveCommandPlugins(startDir = process.cwd(), profileId) {
|
|
36
12
|
const seen = new Set();
|
|
37
|
-
const
|
|
38
|
-
const consider = (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
for (const p of listInstalledPlugins('user'))
|
|
49
|
-
consider(p);
|
|
50
|
-
return deduped.filter((p) => p.enabled && typeof p.manifest.commands === 'string' && p.manifest.commands.length > 0);
|
|
51
|
-
}
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
// Discovery snapshot (tree composer entry point)
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
/** Get raw plugin candidates before the cross-plugin collision pass.
|
|
56
|
-
* Each plugin validation includes its contributions and per-plugin issues;
|
|
57
|
-
* call resolveCommandRegistry (or a later unified collision handler) to
|
|
58
|
-
* produce the final snapshot with cross-plugin collision detection. */
|
|
59
|
-
export function discoverPluginCommandCandidates(startDir = process.cwd(), profileId) {
|
|
60
|
-
return effectiveCommandPlugins(startDir, profileId).map((p) => validatePluginCommands(p, new Set()));
|
|
13
|
+
const result = [];
|
|
14
|
+
const consider = (plugin) => { if (!seen.has(plugin.name)) {
|
|
15
|
+
seen.add(plugin.name);
|
|
16
|
+
result.push(plugin);
|
|
17
|
+
} };
|
|
18
|
+
for (const root of projectScopeRoots(startDir, profileId))
|
|
19
|
+
for (const plugin of listInstalledPluginsInRoot('project', root))
|
|
20
|
+
consider(plugin);
|
|
21
|
+
for (const plugin of listInstalledPlugins('user'))
|
|
22
|
+
consider(plugin);
|
|
23
|
+
return result.filter((plugin) => plugin.enabled && (plugin.manifest.commands !== undefined || plugin.manifest.transport !== undefined));
|
|
61
24
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
* a contribution claiming one is silently skipped from the tree and recorded
|
|
65
|
-
* as a `command_collision` issue (core always wins). A plugin-vs-plugin
|
|
66
|
-
* top-level name collision drops all claimants with an issue. Accepted
|
|
67
|
-
* contributions are sorted by name for deterministic help/order. */
|
|
68
|
-
export function discoverCommandContributions(reservedNames, startDir = process.cwd(), profileId) {
|
|
69
|
-
const validations = validateEffectiveCommandPlugins(reservedNames, startDir, profileId);
|
|
70
|
-
const contributions = validations.flatMap((v) => v.contributions);
|
|
71
|
-
contributions.sort((a, b) => a.node.name.localeCompare(b.node.name));
|
|
72
|
-
return { contributions, issues: validations.flatMap((v) => v.issues) };
|
|
25
|
+
function issueFor(plugin, issues) {
|
|
26
|
+
return (issue) => issues.push({ ...issue, plugin: plugin.name });
|
|
73
27
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
export function validateEffectiveCommandPlugins(reservedNames, startDir = process.cwd(), profileId) {
|
|
82
|
-
const validations = effectiveCommandPlugins(startDir, profileId).map((p) => validatePluginCommands(p, reservedNames));
|
|
83
|
-
// Plugin-vs-plugin top-level name collision: drop all claimants + flag each.
|
|
84
|
-
const claimants = new Map();
|
|
85
|
-
for (const v of validations) {
|
|
86
|
-
for (const c of v.contributions) {
|
|
87
|
-
const list = claimants.get(c.node.name);
|
|
88
|
-
if (list !== undefined)
|
|
89
|
-
list.push(v);
|
|
90
|
-
else
|
|
91
|
-
claimants.set(c.node.name, [v]);
|
|
92
|
-
}
|
|
28
|
+
function safePath(root, rel) {
|
|
29
|
+
if (typeof rel !== 'string' || rel.length === 0)
|
|
30
|
+
return null;
|
|
31
|
+
try {
|
|
32
|
+
const rootReal = realpathSync(root);
|
|
33
|
+
const real = realpathSync(resolve(root, rel));
|
|
34
|
+
return real === rootReal || real.startsWith(rootReal + sep) ? statSync(real).isFile() ? real : null : null;
|
|
93
35
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
continue;
|
|
97
|
-
const names = vs.map((x) => x.plugin.name).join(', ');
|
|
98
|
-
for (const v of vs) {
|
|
99
|
-
v.contributions = v.contributions.filter((c) => c.node.name !== name);
|
|
100
|
-
v.issues.push({
|
|
101
|
-
code: 'command_collision',
|
|
102
|
-
plugin: v.plugin.name,
|
|
103
|
-
path: name,
|
|
104
|
-
message: `top-level command "${name}" is claimed by multiple plugins: ${names}`,
|
|
105
|
-
received: names,
|
|
106
|
-
expected: 'exactly one plugin per top-level command name',
|
|
107
|
-
next: `Disable or remove all but one of: ${names}.`,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
110
38
|
}
|
|
111
|
-
return validations;
|
|
112
39
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
40
|
+
function transportFor(plugin, issues) {
|
|
41
|
+
const raw = plugin.manifest.transport;
|
|
42
|
+
if (raw === undefined) {
|
|
43
|
+
issues.push({ code: 'command_manifest_invalid', plugin: plugin.name, message: 'commands requires transport in plugin.json', received: '(missing)', expected: 'a transport object', next: 'Add plugin.json.transport.', path: 'transport' });
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
47
|
+
issues.push({ code: 'command_manifest_invalid', plugin: plugin.name, message: 'transport must be an object', received: Array.isArray(raw) ? 'array' : typeof raw, expected: 'an exec or http transport object', next: 'Fix plugin.json.transport.', path: 'transport' });
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const value = raw;
|
|
51
|
+
if (value.kind === 'http') {
|
|
52
|
+
const result = validateHttpPluginTransport(value);
|
|
53
|
+
if (result.transport !== undefined)
|
|
54
|
+
return result.transport;
|
|
55
|
+
issues.push({ code: 'command_manifest_invalid', plugin: plugin.name, message: result.errors.join('; '), received: JSON.stringify(raw), expected: 'a valid HTTP transport', next: 'Fix plugin.json.transport.', path: 'transport' });
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const unknown = Object.keys(value).filter((key) => !['kind', 'executable'].includes(key));
|
|
59
|
+
if (value.kind !== 'exec' || unknown.length || typeof value.executable !== 'string' || value.executable.length === 0) {
|
|
60
|
+
issues.push({ code: 'command_manifest_invalid', plugin: plugin.name, message: unknown.length ? `transport has unknown keys: ${unknown.join(', ')}` : 'exec transport requires executable', received: JSON.stringify(raw), expected: '{ kind: "exec", executable: "plugin-relative path" }', next: 'Fix plugin.json.transport.', path: 'transport' });
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const executable = safePath(plugin.root, value.executable);
|
|
64
|
+
if (executable === null) {
|
|
65
|
+
issues.push({ code: 'command_path_unsafe', plugin: plugin.name, message: 'executable escapes the plugin root or is not a regular file', received: value.executable, expected: 'a relative path to a regular file inside the plugin root', next: 'Fix plugin.json.transport.executable.', path: 'transport.executable' });
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
if ((statSync(executable).mode & 0o111) === 0) {
|
|
69
|
+
issues.push({ code: 'command_not_executable', plugin: plugin.name, message: 'executable lacks the POSIX exec bit', received: value.executable, expected: 'a file with an executable permission bit', next: 'chmod +x the plugin executable, then update the plugin.', path: 'transport.executable' });
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
return { kind: 'exec', executable };
|
|
73
|
+
}
|
|
119
74
|
export function validatePluginCommands(plugin, reservedNames = new Set()) {
|
|
120
75
|
const issues = [];
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
76
|
+
const commands = plugin.manifest.commands;
|
|
77
|
+
const transportRaw = plugin.manifest.transport;
|
|
78
|
+
let manifestPath = resolve(plugin.root, typeof commands === 'string' ? commands : 'commands.json');
|
|
79
|
+
if (commands === undefined && transportRaw === undefined)
|
|
80
|
+
return { plugin, manifestPath, contributions: [], issues };
|
|
81
|
+
if (typeof commands !== 'string' || commands.length === 0) {
|
|
82
|
+
issues.push({ code: 'command_manifest_invalid', plugin: plugin.name, message: 'transport requires commands in plugin.json', received: '(missing)', expected: 'a plugin-root-relative commands path', next: 'Add plugin.json.commands or remove transport.', path: 'commands' });
|
|
83
|
+
return { plugin, manifestPath, contributions: [], issues };
|
|
84
|
+
}
|
|
85
|
+
const transport = transportFor(plugin, issues);
|
|
86
|
+
if (transport === undefined)
|
|
87
|
+
return { plugin, manifestPath, contributions: [], issues };
|
|
88
|
+
const httpManifestStore = transport.kind === 'http'
|
|
89
|
+
? resolveHttpPluginManifestStore({ root: plugin.root, commands }) ?? undefined
|
|
90
|
+
: undefined;
|
|
91
|
+
const safe = transport.kind === 'http' ? httpManifestStore?.path ?? null : safePath(plugin.root, commands);
|
|
129
92
|
if (safe === null) {
|
|
130
|
-
|
|
131
|
-
return { plugin, manifestPath, contributions, issues };
|
|
93
|
+
issues.push({ code: 'command_path_unsafe', plugin: plugin.name, message: 'commands pointer escapes the plugin root or is not a regular file', received: commands, expected: 'a relative path to a regular file inside the plugin root', next: 'Fix plugin.json.commands.', path: 'commands' });
|
|
94
|
+
return { plugin, manifestPath, transport, contributions: [], issues };
|
|
132
95
|
}
|
|
133
|
-
|
|
96
|
+
manifestPath = safe;
|
|
97
|
+
let bytes;
|
|
134
98
|
try {
|
|
135
|
-
|
|
99
|
+
bytes = transport.kind === 'http' ? readHttpPluginManifest(httpManifestStore) : readFileSync(safe);
|
|
136
100
|
}
|
|
137
101
|
catch {
|
|
138
|
-
|
|
139
|
-
return { plugin, manifestPath, contributions, issues };
|
|
102
|
+
bytes = null;
|
|
140
103
|
}
|
|
141
|
-
|
|
104
|
+
if (bytes === null) {
|
|
105
|
+
if (transport.kind === 'http')
|
|
106
|
+
return { plugin, manifestPath, transport, contributions: [], issues, absentStore: true, httpManifestStore };
|
|
107
|
+
issues.push({ code: 'command_manifest_unreadable', plugin: plugin.name, message: 'commands.json is missing or unreadable', received: safe, expected: 'a readable JSON file', next: 'Reinstall or update the plugin.' });
|
|
108
|
+
return { plugin, manifestPath, transport, contributions: [], issues };
|
|
109
|
+
}
|
|
110
|
+
let raw;
|
|
142
111
|
try {
|
|
143
|
-
|
|
112
|
+
raw = JSON.parse(Buffer.from(bytes).toString('utf8'));
|
|
144
113
|
}
|
|
145
114
|
catch {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
issue('command_manifest_invalid', `commands.json must be a JSON object`, typeName(parsed), 'a JSON object', 'Regenerate commands.json.');
|
|
151
|
-
return { plugin, manifestPath, contributions, issues };
|
|
152
|
-
}
|
|
153
|
-
// Top-level shape: exact schemaVersion, no unknown keys.
|
|
154
|
-
const unknown = Object.keys(parsed).filter((k) => !MANIFEST_KEYS.has(k));
|
|
155
|
-
if (unknown.length > 0) {
|
|
156
|
-
issue('command_manifest_invalid', `commands.json has unknown top-level keys`, unknown.join(', '), 'only: schemaVersion, executable, mounts', 'Remove the unknown keys.');
|
|
157
|
-
return { plugin, manifestPath, contributions, issues };
|
|
158
|
-
}
|
|
159
|
-
if (parsed['schemaVersion'] !== 1) {
|
|
160
|
-
issue('command_schema_version', `unsupported command schemaVersion`, String(parsed['schemaVersion']), 'exactly the integer 1', 'Update the plugin to a version compatible with this crtr.');
|
|
161
|
-
return { plugin, manifestPath, contributions, issues };
|
|
162
|
-
}
|
|
163
|
-
// Executable path safety + exec bit. Optional: a manifest whose mounts are all
|
|
164
|
-
// passthrough branches ships no leaf and therefore no protocol binary. A leaf
|
|
165
|
-
// without an executable is rejected per-mount below.
|
|
166
|
-
const execRel = parsed['executable'];
|
|
167
|
-
let execSafe;
|
|
168
|
-
if (execRel !== undefined) {
|
|
169
|
-
if (typeof execRel !== 'string' || execRel.length === 0) {
|
|
170
|
-
issue('command_manifest_invalid', `executable must be a non-empty string`, typeName(execRel), 'a relative path string, or omitted when the manifest declares no leaf', 'Set executable to the plugin-relative path of the command binary.', 'executable');
|
|
171
|
-
return { plugin, manifestPath, contributions, issues };
|
|
172
|
-
}
|
|
173
|
-
const resolved = safePath(plugin.root, execRel);
|
|
174
|
-
if (resolved === null) {
|
|
175
|
-
issue('command_path_unsafe', `executable escapes the plugin root or is not a regular file`, execRel, 'a relative path to a regular file inside the plugin root', 'Fix the executable path in commands.json.', 'executable');
|
|
176
|
-
return { plugin, manifestPath, contributions, issues };
|
|
177
|
-
}
|
|
178
|
-
if (!hasExecBit(resolved)) {
|
|
179
|
-
issue('command_not_executable', `executable lacks the POSIX exec bit`, execRel, 'a file with an executable permission bit', 'chmod +x the plugin\'s command binary, then update the plugin.', 'executable');
|
|
180
|
-
return { plugin, manifestPath, contributions, issues };
|
|
181
|
-
}
|
|
182
|
-
execSafe = resolved;
|
|
183
|
-
}
|
|
184
|
-
// Mounts.
|
|
185
|
-
const mounts = parsed['mounts'];
|
|
186
|
-
if (!Array.isArray(mounts)) {
|
|
187
|
-
issue('command_manifest_invalid', `mounts must be an array`, typeName(mounts), 'an array of mounts', 'Declare at least one mount.', 'mounts');
|
|
188
|
-
return { plugin, manifestPath, ...(execSafe !== undefined ? { executable: execSafe } : {}), contributions, issues };
|
|
115
|
+
if (transport.kind === 'http')
|
|
116
|
+
return { plugin, manifestPath, transport, contributions: [], issues, absentStore: true, httpManifestStore };
|
|
117
|
+
issues.push({ code: 'command_manifest_invalid', plugin: plugin.name, message: 'commands.json is not valid JSON', received: safe, expected: 'a JSON object', next: 'Regenerate commands.json.' });
|
|
118
|
+
return { plugin, manifestPath, transport, contributions: [], issues };
|
|
189
119
|
}
|
|
190
|
-
|
|
191
|
-
|
|
120
|
+
const validation = validateCommandManifest(raw, { transport: transport.kind, reservedCoreNames: reservedNames });
|
|
121
|
+
if (validation.manifest === undefined) {
|
|
122
|
+
validation.issues.forEach(issueFor(plugin, issues));
|
|
123
|
+
return { plugin, manifestPath, transport, contributions: [], issues };
|
|
192
124
|
}
|
|
193
|
-
|
|
194
|
-
mounts.forEach((mount, i) => {
|
|
195
|
-
const pathStr = `mounts[${i}]`;
|
|
196
|
-
if (!isObject(mount)) {
|
|
197
|
-
issue('command_node_invalid', `mount must be an object`, typeName(mount), 'a { parent, node } object', 'Fix the mount.', pathStr);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
const mUnknown = Object.keys(mount).filter((k) => !MOUNT_KEYS.has(k));
|
|
201
|
-
if (mUnknown.length > 0) {
|
|
202
|
-
issue('command_node_invalid', `mount has unknown keys`, mUnknown.join(', '), 'only: parent, node', 'Remove the unknown keys.', pathStr);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
// v1: top-level mounts only — parent must be [].
|
|
206
|
-
if (!Array.isArray(mount['parent']) || mount['parent'].length !== 0) {
|
|
207
|
-
issue('command_parent_invalid', `mount parent must be [] (top-level only in v1)`, JSON.stringify(mount['parent']), 'an empty array []', 'Mount the node at top level; nested mounts are not supported in v1.', `${pathStr}.parent`);
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
const node = validatePluginNode(mount['node'], `${pathStr}.node`, true, issue);
|
|
211
|
-
if (node === null)
|
|
212
|
-
return;
|
|
213
|
-
// node is a top-level branch here (validatePluginNode enforced kind + rootEntry).
|
|
214
|
-
if (topNames.has(node.name)) {
|
|
215
|
-
issue('command_node_invalid', `this plugin declares two top-level commands named "${node.name}"`, node.name, 'a unique top-level command name per plugin', 'Rename one of the duplicate top-level mounts.', `${pathStr}.node.name`);
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
topNames.add(node.name);
|
|
219
|
-
if (reservedNames.has(node.name)) {
|
|
220
|
-
issue('command_collision', `top-level command "${node.name}" collides with a core command`, node.name, 'a name not used by a core crtr command', 'Rename the plugin\'s top-level command.', `${pathStr}.node.name`);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
if (execSafe === undefined && declaresLeaf(node)) {
|
|
224
|
-
issue('command_manifest_invalid', `manifest declares a leaf but no executable`, '(missing)', 'an executable path, or a leafless (passthrough-only) mount', 'Add `executable` to commands.json — every leaf runs the plugin binary over the command protocol.', `${pathStr}.node`);
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
contributions.push({ plugin, ...(execSafe !== undefined ? { executable: execSafe } : {}), node: node });
|
|
228
|
-
});
|
|
229
|
-
return { plugin, manifestPath, ...(execSafe !== undefined ? { executable: execSafe } : {}), contributions, issues };
|
|
125
|
+
return { plugin, manifestPath, transport, contributions: validation.manifest.roots.map((node) => ({ plugin, transport, node, manifest: validation.manifest })), issues };
|
|
230
126
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
function declaresLeaf(node) {
|
|
234
|
-
if (node.kind === 'leaf')
|
|
235
|
-
return true;
|
|
236
|
-
return node.children.some((c) => declaresLeaf(c));
|
|
127
|
+
export function discoverPluginCommandCandidates(startDir = process.cwd(), profileId) {
|
|
128
|
+
return effectiveCommandPlugins(startDir, profileId).map((plugin) => validatePluginCommands(plugin));
|
|
237
129
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
130
|
+
export function validateEffectiveCommandPlugins(reservedNames, startDir = process.cwd(), profileId) {
|
|
131
|
+
const validations = effectiveCommandPlugins(startDir, profileId).map((plugin) => validatePluginCommands(plugin, reservedNames));
|
|
132
|
+
const claims = new Map();
|
|
133
|
+
for (const validation of validations)
|
|
134
|
+
for (const contribution of validation.contributions)
|
|
135
|
+
(claims.get(contribution.node.name) ?? (claims.set(contribution.node.name, []), claims.get(contribution.node.name))).push(validation);
|
|
136
|
+
for (const [name, claimants] of claims)
|
|
137
|
+
if (claimants.length > 1)
|
|
138
|
+
for (const validation of claimants) {
|
|
139
|
+
validation.contributions = validation.contributions.filter((contribution) => contribution.node.name !== name);
|
|
140
|
+
validation.issues.push({ code: 'command_collision', plugin: validation.plugin.name, path: name, message: `top-level command "${name}" is claimed by multiple plugins`, received: claimants.map((claimant) => claimant.plugin.name).join(', '), expected: 'exactly one plugin per top-level command name', next: 'Disable or remove all but one claimant.' });
|
|
141
|
+
}
|
|
142
|
+
return validations;
|
|
243
143
|
}
|
|
244
|
-
function
|
|
245
|
-
|
|
246
|
-
return 'null';
|
|
247
|
-
if (Array.isArray(v))
|
|
248
|
-
return 'array';
|
|
249
|
-
return typeof v;
|
|
144
|
+
function contributorIssue(plugin, issue) {
|
|
145
|
+
return { ...issue, contributor: { name: plugin.name, scope: plugin.scope } };
|
|
250
146
|
}
|
|
251
|
-
function
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
issue('command_node_invalid', `unknown keys`, unknown.join(', '), `only: ${[...allowed].join(', ')}`, 'Remove the unknown keys.', path);
|
|
255
|
-
return false;
|
|
256
|
-
}
|
|
257
|
-
return true;
|
|
147
|
+
export function discoverCommandContributions(reservedNames, startDir = process.cwd(), profileId) {
|
|
148
|
+
const validations = validateEffectiveCommandPlugins(reservedNames, startDir, profileId);
|
|
149
|
+
return { contributions: validations.flatMap((validation) => validation.contributions), issues: validations.flatMap((validation) => validation.issues) };
|
|
258
150
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
let rootReal;
|
|
265
|
-
let real;
|
|
266
|
-
try {
|
|
267
|
-
rootReal = realpathSync(root);
|
|
268
|
-
real = realpathSync(resolve(root, rel));
|
|
269
|
-
}
|
|
270
|
-
catch {
|
|
271
|
-
return null;
|
|
272
|
-
}
|
|
273
|
-
if (real !== rootReal && !real.startsWith(rootReal + sep))
|
|
274
|
-
return null;
|
|
275
|
-
try {
|
|
276
|
-
if (!statSync(real).isFile())
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
279
|
-
catch {
|
|
280
|
-
return null;
|
|
281
|
-
}
|
|
282
|
-
return real;
|
|
151
|
+
export function buildExternalCommandSnapshot(reservedCoreNames) {
|
|
152
|
+
const validations = discoverPluginCommandCandidates();
|
|
153
|
+
const candidates = adaptPluginContributions(validations.flatMap((validation) => validation.contributions)).contributions;
|
|
154
|
+
const resolved = resolveCommandRegistry(candidates, reservedCoreNames);
|
|
155
|
+
return { contributions: resolved.contributions, issues: [...validations.flatMap((validation) => validation.issues.map((issue) => contributorIssue(validation.plugin, issue))), ...resolved.issues] };
|
|
283
156
|
}
|
|
284
|
-
function
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
157
|
+
export async function hydrateAbsentHttpPlugins() {
|
|
158
|
+
let hydratedAny = false;
|
|
159
|
+
const diagnostics = [];
|
|
160
|
+
for (const validation of discoverPluginCommandCandidates()) {
|
|
161
|
+
if (validation.transport?.kind !== 'http')
|
|
162
|
+
continue;
|
|
163
|
+
const unsafePath = validation.issues.find((issue) => issue.code === 'command_path_unsafe');
|
|
164
|
+
if (unsafePath !== undefined) {
|
|
165
|
+
diagnostics.push(`HTTP plugin "${validation.plugin.name}": ${unsafePath.code}: ${unsafePath.message} — ${unsafePath.next}`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!validation.absentStore || validation.httpManifestStore === undefined)
|
|
169
|
+
continue;
|
|
170
|
+
try {
|
|
171
|
+
const result = await fetchHttpPluginManifest({ name: validation.plugin.name, endpoint: validation.transport.endpoint, ...(validation.transport.authEnv !== undefined ? { authEnv: validation.transport.authEnv } : {}) });
|
|
172
|
+
if (result.status === 200) {
|
|
173
|
+
writeHttpPluginManifest(validation.httpManifestStore, result.raw);
|
|
174
|
+
hydratedAny = true;
|
|
175
|
+
}
|
|
176
|
+
else
|
|
177
|
+
diagnostics.push(`HTTP plugin "${validation.plugin.name}": ${result.message} — run \`crtr pkg plugin update --name ${validation.plugin.name}\``);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
diagnostics.push(`HTTP plugin "${validation.plugin.name}": ${error.message} — run \`crtr pkg plugin update --name ${validation.plugin.name}\``);
|
|
181
|
+
}
|
|
290
182
|
}
|
|
183
|
+
return { hydratedAny, diagnostics };
|
|
291
184
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface HttpPluginFetchTarget {
|
|
2
|
+
name: string;
|
|
3
|
+
endpoint: string;
|
|
4
|
+
authEnv?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface HttpPluginRegistration extends HttpPluginFetchTarget {
|
|
7
|
+
root: string;
|
|
8
|
+
commands: string;
|
|
9
|
+
scope: 'user' | 'project';
|
|
10
|
+
}
|
|
11
|
+
export interface EndpointValidation {
|
|
12
|
+
isValid: boolean;
|
|
13
|
+
errors: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function validateEndpoint(endpoint: unknown): EndpointValidation;
|
|
16
|
+
export declare function normalizeEndpoint(endpoint: string): string;
|
|
17
|
+
export declare function validateHttpPluginTransport(raw: unknown): {
|
|
18
|
+
transport?: {
|
|
19
|
+
kind: 'http';
|
|
20
|
+
endpoint: string;
|
|
21
|
+
authEnv?: string;
|
|
22
|
+
};
|
|
23
|
+
errors: string[];
|
|
24
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { validateEnvVarName } from '../env-name.js';
|
|
2
|
+
function isLoopbackHostname(hostname) {
|
|
3
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]' || hostname === '::1' || hostname === 'host.docker.internal';
|
|
4
|
+
}
|
|
5
|
+
export function validateEndpoint(endpoint) {
|
|
6
|
+
const errors = [];
|
|
7
|
+
if (typeof endpoint !== 'string')
|
|
8
|
+
errors.push('endpoint must be a string');
|
|
9
|
+
else {
|
|
10
|
+
try {
|
|
11
|
+
const url = new URL(endpoint);
|
|
12
|
+
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && isLoopbackHostname(url.hostname)))
|
|
13
|
+
errors.push('endpoint must be an absolute https:// URL (http:// permitted for loopback hosts only; no credentials or fragment)');
|
|
14
|
+
else if (url.username || url.password || url.hash)
|
|
15
|
+
errors.push('endpoint must be an absolute https:// URL (http:// permitted for loopback hosts only; no credentials or fragment)');
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
errors.push('endpoint must be an absolute https:// URL (http:// permitted for loopback hosts only; no credentials or fragment)');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return { isValid: errors.length === 0, errors };
|
|
22
|
+
}
|
|
23
|
+
export function normalizeEndpoint(endpoint) {
|
|
24
|
+
try {
|
|
25
|
+
const url = new URL(endpoint);
|
|
26
|
+
const pathname = url.pathname.length > 1 && url.pathname.endsWith('/') ? url.pathname.slice(0, -1) : url.pathname;
|
|
27
|
+
return `${url.protocol}//${url.host}${pathname}${url.search}`;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return endpoint;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function validateHttpPluginTransport(raw) {
|
|
34
|
+
const errors = [];
|
|
35
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
36
|
+
return { errors: ['transport must be an object'] };
|
|
37
|
+
const transport = raw;
|
|
38
|
+
const unknown = Object.keys(transport).filter((key) => !['kind', 'endpoint', 'authEnv'].includes(key));
|
|
39
|
+
if (unknown.length)
|
|
40
|
+
errors.push(`transport has unknown keys: ${unknown.join(', ')}`);
|
|
41
|
+
if (transport.kind !== 'http')
|
|
42
|
+
errors.push('transport.kind must be "http"');
|
|
43
|
+
const endpoint = validateEndpoint(transport.endpoint);
|
|
44
|
+
errors.push(...endpoint.errors);
|
|
45
|
+
if (transport.authEnv !== undefined)
|
|
46
|
+
errors.push(...validateEnvVarName(transport.authEnv).errors);
|
|
47
|
+
return errors.length ? { errors } : { transport: { kind: 'http', endpoint: normalizeEndpoint(transport.endpoint), ...(transport.authEnv !== undefined ? { authEnv: transport.authEnv } : {}) }, errors };
|
|
48
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { HttpPluginRegistration } from './endpoint.js';
|
|
2
|
+
/** A root-confined HTTP manifest location, created only by
|
|
3
|
+
* `resolveHttpPluginManifestStore`. Store operations accept this resolved
|
|
4
|
+
* location rather than a manifest declaration. */
|
|
5
|
+
export declare class HttpPluginManifestStore {
|
|
6
|
+
#private;
|
|
7
|
+
private constructor();
|
|
8
|
+
get path(): string;
|
|
9
|
+
static resolve(plugin: Pick<HttpPluginRegistration, 'root' | 'commands'>): HttpPluginManifestStore | null;
|
|
10
|
+
}
|
|
11
|
+
/** Resolve an HTTP manifest declaration to a path confined to the plugin root.
|
|
12
|
+
* Existing target and ancestor symlinks are resolved before use; an absent target
|
|
13
|
+
* is allowed only beneath an existing real ancestor inside that root. */
|
|
14
|
+
export declare function resolveHttpPluginManifestStore(plugin: Pick<HttpPluginRegistration, 'root' | 'commands'>): HttpPluginManifestStore | null;
|
|
15
|
+
export declare function readHttpPluginManifest(store: HttpPluginManifestStore): Uint8Array | null;
|
|
16
|
+
export declare function writeHttpPluginManifest(store: HttpPluginManifestStore, rawManifest: Uint8Array): void;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve, sep } from 'node:path';
|
|
3
|
+
import { atomicWriteText, pathExists } from '../fs-utils.js';
|
|
4
|
+
/** A root-confined HTTP manifest location, created only by
|
|
5
|
+
* `resolveHttpPluginManifestStore`. Store operations accept this resolved
|
|
6
|
+
* location rather than a manifest declaration. */
|
|
7
|
+
export class HttpPluginManifestStore {
|
|
8
|
+
#path;
|
|
9
|
+
constructor(path) {
|
|
10
|
+
this.#path = path;
|
|
11
|
+
}
|
|
12
|
+
get path() {
|
|
13
|
+
return this.#path;
|
|
14
|
+
}
|
|
15
|
+
static resolve(plugin) {
|
|
16
|
+
if (typeof plugin.commands !== 'string' || plugin.commands.length === 0)
|
|
17
|
+
return null;
|
|
18
|
+
try {
|
|
19
|
+
const root = realpathSync(plugin.root);
|
|
20
|
+
const path = resolve(root, plugin.commands);
|
|
21
|
+
if (!isInside(root, path))
|
|
22
|
+
return null;
|
|
23
|
+
if (existsSync(path)) {
|
|
24
|
+
const real = realpathSync(path);
|
|
25
|
+
if (!isInside(root, real) || !statSync(real).isFile())
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
let ancestor = dirname(path);
|
|
30
|
+
while (!existsSync(ancestor) && ancestor !== dirname(ancestor))
|
|
31
|
+
ancestor = dirname(ancestor);
|
|
32
|
+
const realAncestor = realpathSync(ancestor);
|
|
33
|
+
if (!isInside(root, realAncestor) && realAncestor !== root)
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return new HttpPluginManifestStore(path);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function isInside(root, path) {
|
|
44
|
+
return path !== root && path.startsWith(root + sep);
|
|
45
|
+
}
|
|
46
|
+
/** Resolve an HTTP manifest declaration to a path confined to the plugin root.
|
|
47
|
+
* Existing target and ancestor symlinks are resolved before use; an absent target
|
|
48
|
+
* is allowed only beneath an existing real ancestor inside that root. */
|
|
49
|
+
export function resolveHttpPluginManifestStore(plugin) {
|
|
50
|
+
return HttpPluginManifestStore.resolve(plugin);
|
|
51
|
+
}
|
|
52
|
+
export function readHttpPluginManifest(store) {
|
|
53
|
+
if (!pathExists(store.path))
|
|
54
|
+
return null;
|
|
55
|
+
try {
|
|
56
|
+
return readFileSync(store.path);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function writeHttpPluginManifest(store, rawManifest) {
|
|
63
|
+
atomicWriteText(store.path, rawManifest);
|
|
64
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { InstalledPlugin } from '
|
|
2
|
-
import type { Field } from '
|
|
1
|
+
import type { InstalledPlugin } from '../../../types.js';
|
|
2
|
+
import type { Field } from '../../help.js';
|
|
3
3
|
export interface ExternalLeafSpec {
|
|
4
4
|
plugin: InstalledPlugin;
|
|
5
5
|
/** Absolute, resolved executable path. */
|
|
@@ -11,7 +11,7 @@ export interface ExternalLeafSpec {
|
|
|
11
11
|
}
|
|
12
12
|
/** Validates a result object against declared output field descriptors.
|
|
13
13
|
* Returns null if valid; otherwise returns a { field, receivedType } error.
|
|
14
|
-
* Transport-neutral: used by
|
|
14
|
+
* Transport-neutral: used by exec and HTTP transport adapters. */
|
|
15
15
|
export declare function validateDeclaredResult(output: Field[], result: Record<string, unknown>): {
|
|
16
16
|
field: string;
|
|
17
17
|
receivedType?: string;
|