@north-light/crouter 0.3.165 → 0.3.167
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtin-memory/internal/plugins.md +3 -3
- package/dist/clients/attach/__tests__/context-message.test.js +31 -2
- package/dist/clients/attach/render/chat-view.js +0 -2
- package/dist/clients/attach/render/context-message.d.ts +1 -1
- package/dist/clients/attach/render/context-message.js +13 -31
- package/dist/clients/attach/viewer.js +350 -351
- package/dist/commands/node-context.js +3 -2
- package/dist/core/__tests__/command-plugins.test.js +40 -1
- package/dist/core/__tests__/history-inbox.test.js +11 -1
- package/dist/core/__tests__/self-update.test.d.ts +1 -0
- package/dist/core/__tests__/self-update.test.js +46 -0
- package/dist/core/auto-update.js +2 -2
- package/dist/core/canvas/history.js +23 -2
- package/dist/core/command-manifests/manifest.js +7 -0
- package/dist/core/command-plugins/compose.js +6 -2
- package/dist/core/command-plugins/discovery.js +23 -6
- package/dist/core/runtime/broker.js +3 -2
- package/dist/core/runtime/lifecycle.js +2 -2
- package/dist/core/self-update.d.ts +1 -0
- package/dist/core/self-update.js +6 -4
- package/dist/prompts/review.js +4 -2
- package/dist/types.d.ts +2 -2
- package/dist/web-client/assets/index-BgLGlZ3D.css +2 -0
- package/dist/web-client/assets/{index-BMTOGuOZ.js → index-CmoNqcCv.js} +1 -1
- package/dist/web-client/index.html +2 -2
- package/dist/web-client/sw.js +1 -1
- package/package.json +3 -2
- package/runtime.lock.json +2 -2
- package/scripts/self-update.mjs +32 -0
- package/dist/web-client/assets/index-DvA6Zw-R.css +0 -2
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// `crtr node inspect context` — browse this node's context and reports dirs and those of
|
|
2
2
|
// every node it subscribes to, in one read-only nvim file explorer.
|
|
3
3
|
//
|
|
4
|
-
// crtr makes a STRONG CLAIM that nvim is installed (see
|
|
5
|
-
// file explorer + file viewer are nvim, not a
|
|
4
|
+
// crtr makes a STRONG CLAIM that nvim is installed (see
|
|
5
|
+
// .crouter/memory/INDEX.md): the file explorer + file viewer are nvim, not a
|
|
6
|
+
// hand-rolled view-contract surface.
|
|
6
7
|
// We synthesize a temp dir of LABELLED NODE DIRS — `self` first, then one per
|
|
7
8
|
// subscription — and place `context` / `reports` symlinks inside each label dir
|
|
8
9
|
// for the node's existing on-disk dirs. We then open `nvim -R` on the temp tree
|
|
@@ -110,6 +110,28 @@ function commandsJson(topName = 'app') {
|
|
|
110
110
|
],
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
|
+
function passthroughCommandsJson() {
|
|
114
|
+
return {
|
|
115
|
+
schemaVersion: 1,
|
|
116
|
+
mounts: [{
|
|
117
|
+
parent: [],
|
|
118
|
+
node: {
|
|
119
|
+
kind: 'branch',
|
|
120
|
+
name: 'capture',
|
|
121
|
+
description: 'browser automation',
|
|
122
|
+
whenToUse: 'driving a browser through the Capture CLI',
|
|
123
|
+
rootEntry: {
|
|
124
|
+
concept: 'browser automation',
|
|
125
|
+
description: 'browser automation through Capture',
|
|
126
|
+
whenToUse: 'driving a browser through the Capture CLI',
|
|
127
|
+
},
|
|
128
|
+
summary: 'browser automation through Capture',
|
|
129
|
+
passthrough: { bin: 'capture', installHint: 'Install @crouton-kit/capture globally.' },
|
|
130
|
+
children: [],
|
|
131
|
+
},
|
|
132
|
+
}],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
113
135
|
/** Write a plugin into `scopeRoot/plugins/<name>/` with a command manifest +
|
|
114
136
|
* executable. `manifest` is the commands.json object (default fixture). */
|
|
115
137
|
function installPlugin(scopeRoot, name, opts = {}) {
|
|
@@ -122,7 +144,7 @@ function installPlugin(scopeRoot, name, opts = {}) {
|
|
|
122
144
|
version: '0.1.0',
|
|
123
145
|
description: 'fixture',
|
|
124
146
|
commands: pointer,
|
|
125
|
-
transport: { kind: 'exec', executable: opts.executable ?? 'bin/cmd.js' },
|
|
147
|
+
transport: { kind: 'exec', ...(opts.executable === null ? {} : { executable: opts.executable ?? 'bin/cmd.js' }) },
|
|
126
148
|
}));
|
|
127
149
|
writeFileSync(join(root, 'commands.json'), JSON.stringify(opts.manifest ?? commandsJson()));
|
|
128
150
|
const execPath = join(root, 'bin', 'cmd.js');
|
|
@@ -446,6 +468,23 @@ describe('typed validation issues', () => {
|
|
|
446
468
|
const v = validate(commandsJson(), { execBit: false });
|
|
447
469
|
assert.ok(v.issues.some((i) => i.code === 'command_not_executable'));
|
|
448
470
|
});
|
|
471
|
+
test('exec transport omits executable only for a passthrough-only manifest', () => {
|
|
472
|
+
const passthrough = validate(passthroughCommandsJson(), { executable: null });
|
|
473
|
+
assert.equal(passthrough.issues.length, 0, JSON.stringify(passthrough.issues));
|
|
474
|
+
assert.equal(passthrough.contributions.length, 1);
|
|
475
|
+
assert.equal(passthrough.contributions[0].node.passthrough?.bin, 'capture');
|
|
476
|
+
assert.equal(composeExternalSubtrees(adaptPluginContributions(passthrough.contributions))[0].passthrough?.bin, 'capture');
|
|
477
|
+
const nestedPassthrough = passthroughCommandsJson();
|
|
478
|
+
nestedPassthrough.mounts.push({
|
|
479
|
+
parent: ['capture'],
|
|
480
|
+
node: { kind: 'branch', name: 'child', description: 'nested child', whenToUse: 'testing invalid composition', summary: 'nested child', children: [] },
|
|
481
|
+
});
|
|
482
|
+
const rejectedNestedMount = validate(nestedPassthrough, { executable: null });
|
|
483
|
+
assert.ok(rejectedNestedMount.issues.some((issue) => issue.code === 'command_parent_invalid' && /passthrough branch/.test(issue.message)));
|
|
484
|
+
const executableLeaf = validate(commandsJson(), { executable: null });
|
|
485
|
+
assert.equal(executableLeaf.contributions.length, 0);
|
|
486
|
+
assert.ok(executableLeaf.issues.some((i) => i.path === 'transport.executable' && /requires executable/.test(i.message)));
|
|
487
|
+
});
|
|
449
488
|
test('executable escaping the plugin root → command_path_unsafe', () => {
|
|
450
489
|
const v = validate(commandsJson(), { executable: '../../../../bin/sh' });
|
|
451
490
|
assert.ok(v.issues.some((i) => i.code === 'command_path_unsafe'));
|
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
// sender (`from`) is visible in both the read body and the artifact title.
|
|
7
7
|
import { test, before, after, beforeEach } from 'node:test';
|
|
8
8
|
import assert from 'node:assert/strict';
|
|
9
|
-
import { mkdtempSync, rmSync } from 'node:fs';
|
|
9
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
10
|
import { tmpdir } from 'node:os';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { createNode } from '../canvas/canvas.js';
|
|
13
13
|
import { closeDb } from '../canvas/db.js';
|
|
14
|
+
import { contextDir } from '../canvas/paths.js';
|
|
14
15
|
import { appendInbox } from '../feed/inbox.js';
|
|
15
16
|
import { buildCorpus, resolveRef } from '../canvas/history.js';
|
|
16
17
|
let home;
|
|
@@ -39,6 +40,15 @@ after(() => {
|
|
|
39
40
|
rmSync(home, { recursive: true, force: true });
|
|
40
41
|
delete process.env['CRTR_HOME'];
|
|
41
42
|
});
|
|
43
|
+
test('history corpus skips malformed frontmatter in another cwd', () => {
|
|
44
|
+
const broken = createNode(node('broken-frontmatter', '/tmp/hist-broken'));
|
|
45
|
+
const valid = createNode(node('valid-history', '/tmp/hist-valid'));
|
|
46
|
+
writeFileSync(join(contextDir(broken.node_id), 'copied-source.md'), '---\ndesc: "unterminated\n---\nsource text\n');
|
|
47
|
+
writeFileSync(join(contextDir(valid.node_id), 'result.md'), '---\ntitle: result\n---\n0a0afd613\n');
|
|
48
|
+
const corpus = buildCorpus({ allCwds: true }, { types: ['report', 'doc', 'roadmap'] });
|
|
49
|
+
assert.equal(corpus.some((a) => a.ref === `${broken.node_id}:context/copied-source.md`), false, 'invalid artifact is skipped');
|
|
50
|
+
assert.ok(corpus.some((a) => a.loadBody().includes('0a0afd613')), 'valid artifacts remain searchable');
|
|
51
|
+
});
|
|
42
52
|
test('default history excludes inbox; --type inbox includes it, with full sender-attributed bodies', () => {
|
|
43
53
|
const cwd = '/tmp/hist-work';
|
|
44
54
|
createNode(node('sender-1', cwd));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
3
|
+
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import test from 'node:test';
|
|
8
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
9
|
+
const SELF_UPDATE = join(ROOT, 'scripts', 'self-update.mjs');
|
|
10
|
+
test('self-update installs the downloaded release without replacing the stable global shim', () => {
|
|
11
|
+
const directory = mkdtempSync(join(tmpdir(), 'crtr-self-update-test-'));
|
|
12
|
+
try {
|
|
13
|
+
const release = join(directory, 'release');
|
|
14
|
+
const archive = join(directory, 'release.tgz');
|
|
15
|
+
const bin = join(directory, 'bin');
|
|
16
|
+
const stableShim = join(bin, 'crtr');
|
|
17
|
+
const marker = join(directory, 'selected');
|
|
18
|
+
const calls = join(directory, 'npm-calls');
|
|
19
|
+
const fakeBin = join(directory, 'fake-bin');
|
|
20
|
+
mkdirSync(join(release, 'scripts'), { recursive: true });
|
|
21
|
+
mkdirSync(bin);
|
|
22
|
+
mkdirSync(fakeBin);
|
|
23
|
+
writeFileSync(join(release, 'scripts', 'install-runtime.mjs'), `import { writeFileSync } from 'node:fs';\nwriteFileSync(process.env.CRTR_SELF_UPDATE_MARKER, 'selected');\n`);
|
|
24
|
+
execFileSync('tar', ['-czf', archive, '-C', release, '.']);
|
|
25
|
+
symlinkSync('../stable-crtr', stableShim);
|
|
26
|
+
writeFileSync(join(fakeBin, 'npm'), `#!/usr/bin/env node\nimport { copyFileSync, appendFileSync } from 'node:fs';\nimport { basename, join } from 'node:path';\nconst args = process.argv.slice(2);\nappendFileSync(process.env.CRTR_SELF_UPDATE_CALLS, args.join(' ') + '\\n');\nif (args[0] !== 'pack') process.exit(1);\nconst destination = args[args.indexOf('--pack-destination') + 1];\ncopyFileSync(process.env.CRTR_SELF_UPDATE_ARCHIVE, join(destination, basename(process.env.CRTR_SELF_UPDATE_ARCHIVE)));\nprocess.stdout.write(JSON.stringify([{ filename: basename(process.env.CRTR_SELF_UPDATE_ARCHIVE) }]));\n`);
|
|
27
|
+
chmodSync(join(fakeBin, 'npm'), 0o755);
|
|
28
|
+
const result = spawnSync(process.execPath, [SELF_UPDATE], {
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
env: {
|
|
31
|
+
...process.env,
|
|
32
|
+
PATH: `${fakeBin}:${process.env.PATH}`,
|
|
33
|
+
CRTR_SELF_UPDATE_ARCHIVE: archive,
|
|
34
|
+
CRTR_SELF_UPDATE_CALLS: calls,
|
|
35
|
+
CRTR_SELF_UPDATE_MARKER: marker,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
assert.equal(result.status, 0, result.stderr);
|
|
39
|
+
assert.equal(readFileSync(marker, 'utf8'), 'selected');
|
|
40
|
+
assert.equal(readlinkSync(stableShim), '../stable-crtr');
|
|
41
|
+
assert.match(readFileSync(calls, 'utf8'), /^pack @north-light\/crouter@latest --json --pack-destination .+\/crtr-self-update-\S+\n$/);
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
rmSync(directory, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
});
|
package/dist/core/auto-update.js
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { readConfig, readState, updateState } from './config.js';
|
|
3
3
|
import { nowIso } from './fs-utils.js';
|
|
4
4
|
import { info } from './output.js';
|
|
5
|
-
import { selfCheck, contentCheck } from './self-update.js';
|
|
5
|
+
import { SELF_UPDATE_SCRIPT, selfCheck, contentCheck } from './self-update.js';
|
|
6
6
|
const HOUR_MS = 60 * 60 * 1000;
|
|
7
7
|
const SKIP_SUBCOMMANDS = new Set([
|
|
8
8
|
'update',
|
|
@@ -27,7 +27,7 @@ function withinInterval(lastIso, intervalHours) {
|
|
|
27
27
|
return Date.now() - last < intervalHours * HOUR_MS;
|
|
28
28
|
}
|
|
29
29
|
function spawnDetachedSelfUpdate() {
|
|
30
|
-
const child = spawn(
|
|
30
|
+
const child = spawn(process.execPath, [SELF_UPDATE_SCRIPT], {
|
|
31
31
|
detached: true,
|
|
32
32
|
stdio: 'ignore',
|
|
33
33
|
});
|
|
@@ -15,6 +15,7 @@ import { getNode, listNodes, view } from './canvas.js';
|
|
|
15
15
|
import { contextDir, reportsDir, nodeDir, inboxPath } from './paths.js';
|
|
16
16
|
import { readTextIfExists, walkFiles, pathExists } from '../fs-utils.js';
|
|
17
17
|
import { parseFrontmatterGeneric } from '../frontmatter.js';
|
|
18
|
+
import { warn } from '../output.js';
|
|
18
19
|
/** The cwd the caller means by default: the calling node's cwd (resolved from
|
|
19
20
|
* CRTR_NODE_ID via the canvas data layer), falling back to the process cwd
|
|
20
21
|
* when not run as a node. */
|
|
@@ -242,6 +243,11 @@ function listDocFiles(nodeId) {
|
|
|
242
243
|
return [];
|
|
243
244
|
}
|
|
244
245
|
}
|
|
246
|
+
/** Keep one malformed artifact from breaking a cross-node corpus scan. */
|
|
247
|
+
function warnInvalidFrontmatter(path, error) {
|
|
248
|
+
const message = (error instanceof Error ? error.message : String(error)).split('\n')[0];
|
|
249
|
+
warn(`invalid frontmatter in ${path}: ${message}`);
|
|
250
|
+
}
|
|
245
251
|
/** The node desc lives in meta.json, not the row — read it once per survivor. */
|
|
246
252
|
function nodeDescOf(nodeId) {
|
|
247
253
|
const n = getNode(nodeId);
|
|
@@ -281,7 +287,15 @@ export function buildCorpus(scope, corpus) {
|
|
|
281
287
|
out.push(metaArtifact(row, desc));
|
|
282
288
|
if (want.has('report')) {
|
|
283
289
|
for (const f of listReportFiles(row.node_id)) {
|
|
284
|
-
const
|
|
290
|
+
const abs = join(reportsDir(row.node_id), f);
|
|
291
|
+
let a;
|
|
292
|
+
try {
|
|
293
|
+
a = reportArtifact(row, desc, f);
|
|
294
|
+
}
|
|
295
|
+
catch (e) {
|
|
296
|
+
warnInvalidFrontmatter(abs, e);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
285
299
|
if (a === null)
|
|
286
300
|
continue;
|
|
287
301
|
if (wantReportKind !== undefined && a.reportKind !== wantReportKind)
|
|
@@ -291,7 +305,14 @@ export function buildCorpus(scope, corpus) {
|
|
|
291
305
|
}
|
|
292
306
|
if (want.has('doc') || want.has('roadmap')) {
|
|
293
307
|
for (const abs of listDocFiles(row.node_id)) {
|
|
294
|
-
|
|
308
|
+
let a;
|
|
309
|
+
try {
|
|
310
|
+
a = docArtifact(row, desc, abs);
|
|
311
|
+
}
|
|
312
|
+
catch (e) {
|
|
313
|
+
warnInvalidFrontmatter(abs, e);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
295
316
|
if (a.source === 'roadmap' && !want.has('roadmap'))
|
|
296
317
|
continue;
|
|
297
318
|
if (a.source === 'doc' && !want.has('doc'))
|
|
@@ -217,6 +217,7 @@ function materializeForest(mounts, reservedCoreNames, issue) {
|
|
|
217
217
|
...(m.node.rootEntry !== undefined ? { rootEntry: m.node.rootEntry } : {}),
|
|
218
218
|
summary: m.node.summary,
|
|
219
219
|
...(m.node.model !== undefined ? { model: m.node.model } : {}),
|
|
220
|
+
...(m.node.passthrough !== undefined ? { passthrough: m.node.passthrough } : {}),
|
|
220
221
|
children: [...m.node.children],
|
|
221
222
|
}));
|
|
222
223
|
// Attach nested mounts
|
|
@@ -270,6 +271,12 @@ function resolveAndAttachMount(roots, mount, issue) {
|
|
|
270
271
|
}
|
|
271
272
|
current = child;
|
|
272
273
|
}
|
|
274
|
+
// A passthrough owns every token below its path, so nested mounts would make
|
|
275
|
+
// the materialized branch invalid even though its inline children are empty.
|
|
276
|
+
if (current.passthrough !== undefined) {
|
|
277
|
+
issue('command_parent_invalid', 'parent path resolves to a passthrough branch', mount.parent.join('.'), 'a branch without passthrough', 'Remove the nested mount or remove passthrough from its parent branch.', mount.parent.join('.'));
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
273
280
|
// Attach the node
|
|
274
281
|
if (current.children.some((c) => c.name === mount.node.name)) {
|
|
275
282
|
issue('command_node_invalid', `duplicate child name`, mount.node.name, 'unique child names within a branch', 'Rename the node or remove the duplicate.', `${mount.parent.join('.')}.${mount.node.name}`);
|
|
@@ -8,11 +8,15 @@ export function adaptPluginContributions(contributions) {
|
|
|
8
8
|
node: contribution.node,
|
|
9
9
|
adaptLeaf: (leaf, commandPath) => {
|
|
10
10
|
const transport = contribution.transport;
|
|
11
|
-
if (transport.kind === 'exec')
|
|
11
|
+
if (transport.kind === 'exec') {
|
|
12
|
+
const executable = transport.executable;
|
|
13
|
+
if (executable === undefined)
|
|
14
|
+
throw new Error(`exec plugin "${contribution.plugin.name}" has an executable leaf without transport.executable`);
|
|
12
15
|
return {
|
|
13
16
|
outputKind: 'object',
|
|
14
|
-
run: async (input) => executeExternalLeaf({ plugin: contribution.plugin, executable
|
|
17
|
+
run: async (input) => executeExternalLeaf({ plugin: contribution.plugin, executable, commandPath: [...commandPath], output: leaf.output }, input),
|
|
15
18
|
};
|
|
19
|
+
}
|
|
16
20
|
const httpLeaf = leaf;
|
|
17
21
|
return {
|
|
18
22
|
outputKind: httpLeaf.rest.streaming === true ? 'jsonl' : 'object',
|
|
@@ -56,17 +56,29 @@ function transportFor(plugin, issues) {
|
|
|
56
56
|
return undefined;
|
|
57
57
|
}
|
|
58
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
|
|
59
|
+
if (value.kind !== 'exec' || unknown.length || (value.executable !== undefined && (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 a non-empty executable when declared', received: JSON.stringify(raw), expected: '{ kind: "exec", executable?: "plugin-relative path" }', next: 'Fix plugin.json.transport.', path: 'transport' });
|
|
61
61
|
return undefined;
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
return { kind: 'exec', ...(value.executable !== undefined ? { executable: value.executable } : {}) };
|
|
64
|
+
}
|
|
65
|
+
function hasExecutableLeaf(node) {
|
|
66
|
+
return node.kind === 'leaf' || node.children.some(hasExecutableLeaf);
|
|
67
|
+
}
|
|
68
|
+
function validateExecExecutable(plugin, transport, manifest, issues) {
|
|
69
|
+
if (transport.executable === undefined) {
|
|
70
|
+
if (!manifest.roots.some(hasExecutableLeaf))
|
|
71
|
+
return transport;
|
|
72
|
+
issues.push({ code: 'command_manifest_invalid', plugin: plugin.name, message: 'exec transport requires executable when commands.json declares executable leaves', received: '(missing)', expected: '{ kind: "exec", executable: "plugin-relative path" }', next: 'Add plugin.json.transport.executable.', path: 'transport.executable' });
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const executable = safePath(plugin.root, transport.executable);
|
|
64
76
|
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:
|
|
77
|
+
issues.push({ code: 'command_path_unsafe', plugin: plugin.name, message: 'executable escapes the plugin root or is not a regular file', received: transport.executable, expected: 'a relative path to a regular file inside the plugin root', next: 'Fix plugin.json.transport.executable.', path: 'transport.executable' });
|
|
66
78
|
return undefined;
|
|
67
79
|
}
|
|
68
80
|
if ((statSync(executable).mode & 0o111) === 0) {
|
|
69
|
-
issues.push({ code: 'command_not_executable', plugin: plugin.name, message: 'executable lacks the POSIX exec bit', received:
|
|
81
|
+
issues.push({ code: 'command_not_executable', plugin: plugin.name, message: 'executable lacks the POSIX exec bit', received: transport.executable, expected: 'a file with an executable permission bit', next: 'chmod +x the plugin executable, then update the plugin.', path: 'transport.executable' });
|
|
70
82
|
return undefined;
|
|
71
83
|
}
|
|
72
84
|
return { kind: 'exec', executable };
|
|
@@ -122,7 +134,12 @@ export function validatePluginCommands(plugin, reservedNames = new Set()) {
|
|
|
122
134
|
validation.issues.forEach(issueFor(plugin, issues));
|
|
123
135
|
return { plugin, manifestPath, transport, contributions: [], issues };
|
|
124
136
|
}
|
|
125
|
-
|
|
137
|
+
const resolvedTransport = transport.kind === 'exec'
|
|
138
|
+
? validateExecExecutable(plugin, transport, validation.manifest, issues)
|
|
139
|
+
: transport;
|
|
140
|
+
if (resolvedTransport === undefined)
|
|
141
|
+
return { plugin, manifestPath, transport, contributions: [], issues };
|
|
142
|
+
return { plugin, manifestPath, transport: resolvedTransport, contributions: validation.manifest.roots.map((node) => ({ plugin, transport: resolvedTransport, node, manifest: validation.manifest })), issues };
|
|
126
143
|
}
|
|
127
144
|
export function discoverPluginCommandCandidates(startDir = process.cwd(), profileId) {
|
|
128
145
|
return effectiveCommandPlugins(startDir, profileId).map((plugin) => validatePluginCommands(plugin));
|
|
@@ -475,8 +475,9 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
|
|
|
475
475
|
});
|
|
476
476
|
};
|
|
477
477
|
// Defensive: any `crtr` child the engine/extensions spawn must inherit the
|
|
478
|
-
// front-door recursion guard (
|
|
479
|
-
// (T6) also sets this;
|
|
478
|
+
// front-door recursion guard (see
|
|
479
|
+
// src/core/runtime/.crouter/memory/INDEX.md). The host (T6) also sets this;
|
|
480
|
+
// setting it here keeps the broker self-sufficient.
|
|
480
481
|
process.env[FRONT_DOOR_ENV] = '1';
|
|
481
482
|
// A successful in-turn crtr command stamps this process id into its stop
|
|
482
483
|
// signal, so a crash-revived broker cannot consume a prior broker's outcome.
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
// Layering note: lifecycle.ts is runtime, but it is the canvas write surface's
|
|
34
34
|
// `transition` verb (the only writer of status+intent), so it owns its atomic
|
|
35
35
|
// row UPDATE directly via openDb — the one sanctioned exception to "only
|
|
36
|
-
// canvas.ts touches the db" (see canvas/
|
|
37
|
-
// is the sanctioned exception for a data migration.
|
|
36
|
+
// canvas.ts touches the db" (see src/core/canvas/.crouter/memory/INDEX.md),
|
|
37
|
+
// exactly as db.ts's backfill is the sanctioned exception for a data migration.
|
|
38
38
|
import { openDb, getNode, cancelCronsOnWake } from '../canvas/index.js';
|
|
39
39
|
const ANY = '*';
|
|
40
40
|
/** The supervised statuses — a live node the daemon watches. */
|
package/dist/core/self-update.js
CHANGED
|
@@ -15,16 +15,18 @@ const __dirname = dirname(__filename);
|
|
|
15
15
|
// src/core/self-update.ts → up to src/ → up to pkg root
|
|
16
16
|
const PKG_ROOT = join(__dirname, '..', '..');
|
|
17
17
|
const PACKAGE_JSON_PATH = join(PKG_ROOT, 'package.json');
|
|
18
|
+
export const SELF_UPDATE_SCRIPT = join(PKG_ROOT, 'scripts', 'self-update.mjs');
|
|
18
19
|
export function currentVersion() {
|
|
19
20
|
const raw = readFileSync(PACKAGE_JSON_PATH, 'utf8');
|
|
20
21
|
const parsed = JSON.parse(raw);
|
|
21
22
|
return parsed.version;
|
|
22
23
|
}
|
|
23
24
|
export function selfUpdate() {
|
|
24
|
-
const res = spawnSync(
|
|
25
|
-
if (res.
|
|
26
|
-
throw general(
|
|
27
|
-
|
|
25
|
+
const res = spawnSync(process.execPath, [SELF_UPDATE_SCRIPT], { stdio: 'inherit' });
|
|
26
|
+
if (res.error)
|
|
27
|
+
throw general(`self-update failed: ${res.error.message}`);
|
|
28
|
+
if (res.status !== 0)
|
|
29
|
+
throw general(`self-update failed (${res.status ?? res.signal ?? 'unknown'})`);
|
|
28
30
|
}
|
|
29
31
|
/** Check whether a newer crtr version is available on npm.
|
|
30
32
|
* Warns to stderr if network unavailable; returns {current, latest} or null if unreachable. */
|
package/dist/prompts/review.js
CHANGED
|
@@ -48,8 +48,10 @@ seams to fall between — skip it for self-contained specs.
|
|
|
48
48
|
|
|
49
49
|
For larger specs touching established patterns, optionally spawn a Task
|
|
50
50
|
agent (\`general-purpose\`, \`sonnet\`) to cross-check the spec's design
|
|
51
|
-
against
|
|
52
|
-
|
|
51
|
+
against the applicable project memory documents (discover them with
|
|
52
|
+
\`crtr memory list\` and load them with \`crtr memory read\`) and the files they
|
|
53
|
+
reference, looking for contradictions with project conventions. Skip for small
|
|
54
|
+
specs.
|
|
53
55
|
|
|
54
56
|
## Calibration
|
|
55
57
|
|
package/dist/types.d.ts
CHANGED
|
@@ -14,8 +14,8 @@ export interface OwnerRef {
|
|
|
14
14
|
email?: string;
|
|
15
15
|
}
|
|
16
16
|
export type PluginTransport = {
|
|
17
|
-
kind: 'exec';
|
|
18
|
-
executable
|
|
17
|
+
kind: 'exec'; /** Required when the validated command manifest has executable leaves. */
|
|
18
|
+
executable?: string;
|
|
19
19
|
} | {
|
|
20
20
|
kind: 'http';
|
|
21
21
|
endpoint: string;
|