@north-light/crouter 0.3.170 → 0.3.171
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 +1 -16
- package/dist/build-root.js +1 -25
- package/dist/builtin-memory/insights/capture.md +66 -0
- package/dist/builtin-memory/insights/init.md +45 -0
- package/dist/builtin-memory/internal/plugins.md +12 -11
- package/dist/cli.js +2 -6
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +7 -1
- package/dist/clients/attach/__tests__/context-message.test.js +50 -3
- package/dist/clients/attach/__tests__/diagram.test.js +1 -1
- package/dist/clients/attach/chrome/header.d.ts +10 -0
- package/dist/clients/attach/chrome/header.js +35 -0
- package/dist/clients/attach/chrome/status-line.d.ts +3 -0
- package/dist/clients/attach/chrome/status-line.js +5 -0
- package/dist/clients/attach/chrome/widgets.js +7 -18
- package/dist/clients/attach/input/controller.d.ts +10 -0
- package/dist/clients/attach/input/controller.js +3 -0
- package/dist/clients/attach/overlays/help.d.ts +33 -0
- package/dist/clients/attach/overlays/help.js +204 -0
- package/dist/clients/attach/render/chat-view.d.ts +81 -9
- package/dist/clients/attach/render/chat-view.js +434 -90
- package/dist/clients/attach/render/context-message.js +3 -3
- package/dist/clients/attach/render/diagram.js +2 -2
- package/dist/clients/attach/render/measured-container.d.ts +94 -0
- package/dist/clients/attach/render/measured-container.js +307 -0
- package/dist/clients/attach/render/transcript-copy.d.ts +9 -0
- package/dist/clients/attach/render/transcript-copy.js +81 -0
- package/dist/clients/attach/render/viewport.d.ts +29 -0
- package/dist/clients/attach/render/viewport.js +160 -0
- package/dist/clients/attach/session/bindings.d.ts +5 -1
- package/dist/clients/attach/session/bindings.js +16 -0
- package/dist/clients/attach/session/context.d.ts +5 -3
- package/dist/clients/attach/session/frame.d.ts +34 -0
- package/dist/clients/attach/session/frame.js +173 -0
- package/dist/clients/attach/session/frames.d.ts +2 -0
- package/dist/clients/attach/session/frames.js +3 -0
- package/dist/clients/attach/session/input-wiring.d.ts +10 -4
- package/dist/clients/attach/session/input-wiring.js +13 -13
- package/dist/clients/attach/session/keys.d.ts +4 -1
- package/dist/clients/attach/session/keys.js +9 -4
- package/dist/clients/attach/session/layout.d.ts +10 -8
- package/dist/clients/attach/session/layout.js +32 -31
- package/dist/clients/attach/session/mouse.d.ts +15 -0
- package/dist/clients/attach/session/mouse.js +61 -0
- package/dist/clients/attach/slash/dispatch.d.ts +10 -0
- package/dist/clients/attach/slash/dispatch.js +35 -2
- package/dist/clients/attach/viewer.js +605 -581
- package/dist/clients/web/dev-server.d.ts +4 -1
- package/dist/clients/web/dev-server.js +8 -19
- package/dist/clients/web/web-cmd.js +1 -1
- package/dist/commands/human/prompts.js +2 -1
- package/dist/commands/pkg/plugin-manage.js +143 -68
- package/dist/commands/sys/__tests__/setup-core.test.js +4 -2
- package/dist/commands/sys/setup-core.js +21 -16
- package/dist/core/clipboard-text.d.ts +8 -0
- package/dist/core/clipboard-text.js +18 -0
- package/dist/core/command-manifests/registry.d.ts +1 -3
- package/dist/core/command-plugins/bundle.d.ts +29 -0
- package/dist/core/command-plugins/bundle.js +198 -0
- package/dist/core/command-plugins/discovery.d.ts +0 -8
- package/dist/core/command-plugins/discovery.js +2 -39
- package/dist/core/command-plugins/transport/http-fetch.d.ts +4 -30
- package/dist/core/command-plugins/transport/http-fetch.js +17 -95
- package/dist/core/command.d.ts +1 -13
- package/dist/core/command.js +1 -16
- package/dist/core/config.js +2 -1
- package/dist/core/keybindings/__tests__/resolve.test.js +4 -0
- package/dist/core/keybindings/attach-control.d.ts +3 -0
- package/dist/core/keybindings/attach-control.js +1 -0
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +8 -0
- package/dist/core/runtime/broker-protocol.d.ts +13 -1
- package/dist/core/runtime/broker.js +213 -5
- package/dist/core/runtime/canvas-extensions.d.ts +3 -0
- package/dist/core/runtime/canvas-extensions.js +3 -0
- package/dist/core/runtime/front-door.js +4 -4
- package/dist/core/runtime/kickoff.d.ts +1 -5
- package/dist/core/runtime/kickoff.js +1 -5
- package/dist/core/runtime/naming.d.ts +7 -0
- package/dist/core/runtime/naming.js +5 -2
- package/dist/core/runtime/node-read.js +15 -0
- package/dist/core/runtime/package-health.d.ts +6 -21
- package/dist/core/runtime/package-health.js +12 -123
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +3 -3
- package/dist/core/runtime/tool-group-summary.d.ts +19 -0
- package/dist/core/runtime/tool-group-summary.js +117 -0
- package/dist/core/termrender/code-doc.d.ts +12 -0
- package/dist/core/termrender/code-doc.js +91 -0
- package/dist/core/termrender/display.d.ts +12 -0
- package/dist/core/termrender/display.js +19 -0
- package/dist/core/termrender/termrender.d.ts +73 -0
- package/dist/core/termrender/termrender.js +795 -0
- package/dist/core/termrender/version.d.ts +1 -0
- package/dist/core/termrender/version.js +1 -0
- package/dist/core/user-settings.d.ts +14 -0
- package/dist/core/user-settings.js +20 -0
- package/dist/daemon/crtrd.js +5 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +63 -0
- package/dist/pi-extensions/canvas-stophook.js +2 -2
- package/dist/pi-extensions/canvas-structured-output.js +52 -8
- package/dist/pi-extensions/summary-tool.d.ts +33 -0
- package/dist/pi-extensions/summary-tool.js +62 -0
- package/dist/shared/generated-context.d.ts +19 -4
- package/dist/shared/generated-context.js +98 -7
- package/dist/shared/tool-groups.d.ts +23 -0
- package/dist/shared/tool-groups.js +60 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +1 -0
- package/dist/web-client/assets/{index-U_NZ66VE.js → index-D7I3gHCL.js} +26 -19
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +3 -1
- package/runtime.lock.json +62 -3
- package/scripts/postinstall.mjs +14 -1
- package/dist/core/command-plugins/store.d.ts +0 -16
- package/dist/core/command-plugins/store.js +0 -64
- package/dist/core/runtime/fault-recovery-nudge.d.ts +0 -3
- package/dist/core/runtime/fault-recovery-nudge.js +0 -3
|
@@ -3,5 +3,8 @@ import type { ViteDevServer } from 'vite';
|
|
|
3
3
|
/** Create a Vite dev server in middleware mode, HMR bound to `httpServer`.
|
|
4
4
|
* Mirrors the old `view serve` resolve setup so the shell's bare imports
|
|
5
5
|
* (react, the `@north-light/crouter/web` self-subpath) resolve from the
|
|
6
|
-
* generated/shell root.
|
|
6
|
+
* generated/shell root. It loads the shell's Vite config rather than duplicating
|
|
7
|
+
* its plugins: that config supplies the PWA virtual-module resolver as well as
|
|
8
|
+
* the production build settings. Lazily imports vite so the shipped path never
|
|
9
|
+
* loads it. */
|
|
7
10
|
export declare function createDevServer(httpServer: HttpServer): Promise<ViteDevServer>;
|
|
@@ -24,30 +24,19 @@ function resolveShellRoot(packageRoot) {
|
|
|
24
24
|
/** Create a Vite dev server in middleware mode, HMR bound to `httpServer`.
|
|
25
25
|
* Mirrors the old `view serve` resolve setup so the shell's bare imports
|
|
26
26
|
* (react, the `@north-light/crouter/web` self-subpath) resolve from the
|
|
27
|
-
* generated/shell root.
|
|
27
|
+
* generated/shell root. It loads the shell's Vite config rather than duplicating
|
|
28
|
+
* its plugins: that config supplies the PWA virtual-module resolver as well as
|
|
29
|
+
* the production build settings. Lazily imports vite so the shipped path never
|
|
30
|
+
* loads it. */
|
|
28
31
|
export async function createDevServer(httpServer) {
|
|
29
32
|
const packageRoot = resolve(HERE, '../../..');
|
|
30
|
-
const nodeModules = join(packageRoot, 'node_modules');
|
|
31
33
|
const shellRoot = resolveShellRoot(packageRoot);
|
|
32
34
|
const { createServer } = await import('vite');
|
|
33
|
-
const react = (await import('@vitejs/plugin-react')).default;
|
|
34
|
-
const tailwindcss = (await import('@tailwindcss/vite')).default;
|
|
35
35
|
return createServer({
|
|
36
|
-
configFile:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
resolve: {
|
|
41
|
-
alias: {
|
|
42
|
-
react: join(nodeModules, 'react'),
|
|
43
|
-
'react-dom': join(nodeModules, 'react-dom'),
|
|
44
|
-
'@': shellRoot,
|
|
45
|
-
// The crtr web runtime is a self-subpath of crouter's own package;
|
|
46
|
-
// pin it to the built barrel so the shell can import it from any root.
|
|
47
|
-
'@north-light/crouter/web': join(packageRoot, 'dist', 'web', 'index.js'),
|
|
48
|
-
},
|
|
49
|
-
dedupe: ['react', 'react-dom'],
|
|
50
|
-
},
|
|
36
|
+
configFile: join(shellRoot, 'vite.config.ts'),
|
|
37
|
+
// The integrated middleware already owns /node, /v1, and /__crtr. The
|
|
38
|
+
// shell config uses this mode to omit its standalone-server proxies.
|
|
39
|
+
mode: 'middleware',
|
|
51
40
|
server: {
|
|
52
41
|
middlewareMode: true,
|
|
53
42
|
// Share our HTTP server for HMR; server.ts's upgrade handler hands every
|
|
@@ -100,7 +100,7 @@ const webServeLeaf = defineLeaf({
|
|
|
100
100
|
const open = input['open'] ?? false;
|
|
101
101
|
const dev = input['dev'] ?? false;
|
|
102
102
|
const server = await startWebServer({ port, host, token, dev });
|
|
103
|
-
const nodeUrlShape = `${server.url}/
|
|
103
|
+
const nodeUrlShape = `${server.url}/nodes/<id>`;
|
|
104
104
|
const browserUrl = token !== undefined ? `${server.url}?token=${encodeURIComponent(token)}` : server.url;
|
|
105
105
|
process.stdout.write(`crtr surface web serving on ${server.url}${dev ? ' (--dev: Vite middleware)' : ''}\n` +
|
|
106
106
|
` open a node: ${nodeUrlShape}\n` +
|
|
@@ -13,7 +13,8 @@ import { randomBytes, randomUUID } from 'node:crypto';
|
|
|
13
13
|
import { tmuxServerReachable } from '../../core/spawn.js';
|
|
14
14
|
import { BrokerClient, BrokerUnavailableError } from '../../core/broker-client/index.js';
|
|
15
15
|
import { markStopSignal } from '../../core/runtime/stop-signals.js';
|
|
16
|
-
import { validateDeck, resolveDeckBodyPaths, notifyDeck, atomicWriteJson, submitDeck, submitReview,
|
|
16
|
+
import { validateDeck, resolveDeckBodyPaths, notifyDeck, atomicWriteJson, submitDeck, submitReview, } from '@crouton-kit/humanloop';
|
|
17
|
+
import { display } from '../../core/termrender/display.js';
|
|
17
18
|
import { BODY_PATH_CONTRACT, DECK_SCHEMA_HINT, registerCrouterRoot, resolveMaxPanes } from './shared.js';
|
|
18
19
|
import { inboxOpenInstruction, inboxPopupHint } from '../../core/keybindings/index.js';
|
|
19
20
|
/** The asking node's id, or null when run from a bare shell (no parent to route to). */
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { join, resolve, isAbsolute } from 'node:path';
|
|
2
|
-
import { renameSync } from 'node:fs';
|
|
1
|
+
import { join, resolve, isAbsolute, sep } from 'node:path';
|
|
2
|
+
import { renameSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
import { defineLeaf } from '../../core/command.js';
|
|
4
|
-
import {
|
|
5
|
+
import { notFound, usage, general, network } from '../../core/errors.js';
|
|
5
6
|
import { findMarketplaceByName, findPluginByName, listAllPlugins } from '../../core/resolver.js';
|
|
6
7
|
import { pluginsDir, ensureProjectScopeRoot, userScopeRoot, resolveScopeArg, projectScopeRoot, requireScopeRoot, } from '../../core/scope.js';
|
|
7
8
|
import { updateConfig, updateState, ensureScopeInitialized } from '../../core/config.js';
|
|
8
9
|
import { pathExists, ensureDir, removePath, nowIso, linkOrCopy, isSymlink, atomicWriteJson } from '../../core/fs-utils.js';
|
|
9
10
|
import { clone, pull, deriveNameFromUrl, currentSha, isGitRepo } from '../../core/git.js';
|
|
10
|
-
import {
|
|
11
|
+
import { readMarketplaceManifest, readPluginManifest } from '../../core/manifest.js';
|
|
11
12
|
import { validatePluginCommands, validateEffectiveCommandPlugins } from '../../core/command-plugins/discovery.js';
|
|
12
13
|
import { normalizeEndpoint, validateEndpoint, validateHttpPluginTransport } from '../../core/command-plugins/endpoint.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
14
|
+
import { validatePluginBundle } from '../../core/command-plugins/bundle.js';
|
|
15
|
+
import { fetchHttpPluginBundle } from '../../core/command-plugins/transport/http-fetch.js';
|
|
15
16
|
import { SUBTREE_NAMES } from '../../build-root.js';
|
|
16
|
-
import { ExitCode } from '../../types.js';
|
|
17
17
|
import { isGitUrl, setPluginEnabled, resolveInstallScope } from './shared.js';
|
|
18
18
|
/** Validate a just-installed/updated plugin's command manifest, if it declares
|
|
19
19
|
* one. Returns undefined for plugins without a `commands` pointer. Never
|
|
@@ -32,66 +32,137 @@ function commandReport(name, scope) {
|
|
|
32
32
|
return { mounts: v.contributions.map((c) => c.node.name), issues: v.issues };
|
|
33
33
|
}
|
|
34
34
|
const PLUGIN_NAME_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
35
|
-
function unsafeHttpManifestPath(name, commands) {
|
|
36
|
-
throw new CrtrError('command_path_unsafe', `HTTP plugin "${name}" commands pointer escapes the plugin root or is not a regular file`, ExitCode.USAGE, { received: commands, field: 'commands', next: 'Fix plugin.json.commands.' });
|
|
37
|
-
}
|
|
38
35
|
function validateHttpInstall(name, endpoint, authEnv) {
|
|
39
|
-
if (typeof name !== 'string' || !PLUGIN_NAME_RE.test(name))
|
|
36
|
+
if (typeof name !== 'string' || !PLUGIN_NAME_RE.test(name))
|
|
40
37
|
throw usage('invalid --name: expected a lowercase kebab-case plugin handle');
|
|
41
|
-
}
|
|
42
38
|
const endpointValidation = validateEndpoint(endpoint);
|
|
43
|
-
if (!endpointValidation.isValid)
|
|
39
|
+
if (!endpointValidation.isValid)
|
|
44
40
|
throw usage(`invalid --endpoint: ${endpointValidation.errors.join('; ')}`);
|
|
45
|
-
}
|
|
46
41
|
const normalizedEndpoint = normalizeEndpoint(endpoint);
|
|
47
|
-
const transportValidation = validateHttpPluginTransport({
|
|
48
|
-
|
|
49
|
-
endpoint: normalizedEndpoint,
|
|
50
|
-
...(authEnv !== undefined ? { authEnv } : {}),
|
|
51
|
-
});
|
|
52
|
-
if (transportValidation.transport === undefined) {
|
|
42
|
+
const transportValidation = validateHttpPluginTransport({ kind: 'http', endpoint: normalizedEndpoint, ...(authEnv !== undefined ? { authEnv } : {}) });
|
|
43
|
+
if (transportValidation.transport === undefined)
|
|
53
44
|
throw usage(`invalid HTTP plugin transport: ${transportValidation.errors.join('; ')}`);
|
|
54
|
-
}
|
|
55
45
|
return transportValidation.transport;
|
|
56
46
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
47
|
+
function invocationTransport(bundle) {
|
|
48
|
+
return { kind: 'http', endpoint: new URL(bundle.endpoint).origin, ...(bundle.authEnv !== undefined ? { authEnv: bundle.authEnv } : {}) };
|
|
49
|
+
}
|
|
50
|
+
function stagedPath(stagingRoot, relative) {
|
|
51
|
+
const path = resolve(stagingRoot, relative);
|
|
52
|
+
if (path === stagingRoot || !path.startsWith(stagingRoot + sep)) {
|
|
53
|
+
throw general(`bundle member path escapes the staging directory: ${relative}`);
|
|
63
54
|
}
|
|
64
|
-
|
|
65
|
-
|
|
55
|
+
return path;
|
|
56
|
+
}
|
|
57
|
+
function writeStagedBundle(stagingRoot, manifest, bundle) {
|
|
58
|
+
atomicWriteJson(stagedPath(stagingRoot, '.crouter-plugin/plugin.json'), manifest);
|
|
59
|
+
for (const directory of bundle.directories)
|
|
60
|
+
ensureDir(stagedPath(stagingRoot, directory.path));
|
|
61
|
+
writeFileSync(stagedPath(stagingRoot, 'commands.json'), bundle.commandsBytes);
|
|
62
|
+
for (const document of bundle.memory) {
|
|
63
|
+
const target = stagedPath(stagingRoot, document.path);
|
|
64
|
+
ensureDir(resolve(target, '..'));
|
|
65
|
+
writeFileSync(target, document.bytes);
|
|
66
66
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
}
|
|
68
|
+
function validateStagedBundle(name, scope, root, manifest) {
|
|
69
|
+
const staged = { name, scope, root, manifest, enabled: true };
|
|
70
|
+
const validation = validatePluginCommands(staged, new Set(SUBTREE_NAMES));
|
|
71
|
+
if (validation.issues.length > 0) {
|
|
72
|
+
throw general(`bundle staged package for "${name}" is invalid`, { issues: validation.issues });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function isSourceInstalled(plugin) {
|
|
76
|
+
return plugin.sourceMarketplace !== undefined || isGitRepo(plugin.root) || isSymlink(plugin.root);
|
|
77
|
+
}
|
|
78
|
+
function rejectLegacyHttpPlugin(name) {
|
|
79
|
+
throw general(`HTTP plugin "${name}" uses the removed commands-only endpoint format`, { next: `Run \`crtr pkg plugin remove ${name}\`, then reinstall it with \`crtr pkg plugin install --name ${name} --endpoint <archive-url>\`.` });
|
|
80
|
+
}
|
|
81
|
+
function existingBundleRoot(name, scope, root) {
|
|
82
|
+
if (!pathExists(root))
|
|
83
|
+
return undefined;
|
|
84
|
+
const existing = findPluginByName(name, scope);
|
|
85
|
+
if (existing?.manifest.bundle !== undefined)
|
|
86
|
+
return existing;
|
|
87
|
+
if (existing?.manifest.transport?.kind === 'http' && existing !== undefined && !isSourceInstalled(existing))
|
|
88
|
+
rejectLegacyHttpPlugin(name);
|
|
89
|
+
throw general(`plugin "${name}" already exists at ${root}; run \`crtr pkg plugin remove ${name}\` before installing the archive plugin.`);
|
|
90
|
+
}
|
|
91
|
+
async function replaceBundlePlugin(name, bundleSource, scope, options) {
|
|
92
|
+
const fetchTransport = validateHttpInstall(name, bundleSource.endpoint, bundleSource.authEnv);
|
|
93
|
+
const bundle = { endpoint: fetchTransport.endpoint, ...(fetchTransport.authEnv !== undefined ? { authEnv: fetchTransport.authEnv } : {}) };
|
|
94
|
+
const transport = invocationTransport(bundle);
|
|
95
|
+
const scopeRootPath = scope === 'project' ? ensureProjectScopeRoot() : userScopeRoot();
|
|
96
|
+
const root = join(scopeRootPath, 'plugins', name);
|
|
97
|
+
existingBundleRoot(name, scope, root);
|
|
98
|
+
const fetched = await fetchHttpPluginBundle({ name, endpoint: bundle.endpoint, ...(bundle.authEnv !== undefined ? { authEnv: bundle.authEnv } : {}) });
|
|
72
99
|
if (fetched.status !== 200) {
|
|
73
|
-
const message = `HTTP plugin
|
|
100
|
+
const message = `HTTP plugin bundle fetch failed for "${name}": ${fetched.message}`;
|
|
74
101
|
if (fetched.status === 'auth_env_missing')
|
|
75
102
|
throw usage(message);
|
|
76
103
|
if (fetched.status === 'cli_unreachable')
|
|
77
104
|
throw network(message);
|
|
78
105
|
throw general(message);
|
|
79
106
|
}
|
|
80
|
-
|
|
81
|
-
|
|
107
|
+
const validated = await validatePluginBundle(fetched.raw, { reservedCoreNames: new Set(SUBTREE_NAMES) });
|
|
108
|
+
if (validated.bundle === undefined)
|
|
109
|
+
throw general(`HTTP plugin bundle for "${name}" is invalid`, { issues: validated.issues });
|
|
110
|
+
const version = createHash('sha256').update(fetched.raw).digest('hex').slice(0, 12);
|
|
111
|
+
const manifest = {
|
|
82
112
|
name,
|
|
113
|
+
version,
|
|
83
114
|
commands: 'commands.json',
|
|
84
115
|
transport,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
116
|
+
bundle,
|
|
117
|
+
};
|
|
118
|
+
const tmpRoot = join(scopeRootPath, 'tmp');
|
|
119
|
+
const staging = join(tmpRoot, `${name}.${process.pid}`);
|
|
120
|
+
const previous = `${staging}.prev`;
|
|
121
|
+
if (pathExists(previous))
|
|
122
|
+
throw general(`cannot replace plugin "${name}": prior interrupted replacement remains at ${previous}`);
|
|
123
|
+
ensureDir(tmpRoot);
|
|
124
|
+
ensureDir(join(scopeRootPath, 'plugins'));
|
|
125
|
+
removePath(staging);
|
|
126
|
+
try {
|
|
127
|
+
ensureDir(staging);
|
|
128
|
+
writeStagedBundle(staging, manifest, validated.bundle);
|
|
129
|
+
validateStagedBundle(name, scope, staging, manifest);
|
|
130
|
+
let movedPrevious = false;
|
|
131
|
+
if (pathExists(root)) {
|
|
132
|
+
renameSync(root, previous);
|
|
133
|
+
movedPrevious = true;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
renameSync(staging, root);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (movedPrevious)
|
|
140
|
+
renameSync(previous, root);
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
if (movedPrevious)
|
|
144
|
+
removePath(previous);
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
removePath(staging);
|
|
148
|
+
}
|
|
149
|
+
ensureScopeInitialized(scope, scopeRootPath);
|
|
90
150
|
updateConfig(scope, (cfg) => {
|
|
91
|
-
cfg.plugins[name] = { enabled: true };
|
|
151
|
+
cfg.plugins[name] = { enabled: options.enable ? true : (cfg.plugins[name]?.enabled ?? true), version };
|
|
152
|
+
});
|
|
153
|
+
updateState(scope, (state) => {
|
|
154
|
+
if (state.plugins[name] === undefined)
|
|
155
|
+
state.plugins[name] = {};
|
|
156
|
+
state.plugins[name].last_updated = nowIso();
|
|
92
157
|
});
|
|
93
158
|
const commands = commandReport(name, scope);
|
|
94
|
-
|
|
159
|
+
if (commands === undefined)
|
|
160
|
+
throw general(`bundle plugin "${name}" has no staged command report`);
|
|
161
|
+
return { name, scope, path: root, transport: 'http', version, docs: validated.bundle.memory.length, commands };
|
|
162
|
+
}
|
|
163
|
+
async function installHttpPlugin(name, endpoint, authEnv, scope) {
|
|
164
|
+
const source = validateHttpInstall(name, endpoint, authEnv);
|
|
165
|
+
return replaceBundlePlugin(name, { endpoint: source.endpoint, ...(source.authEnv !== undefined ? { authEnv: source.authEnv } : {}) }, scope, { enable: true });
|
|
95
166
|
}
|
|
96
167
|
function classifyInstallRef(raw) {
|
|
97
168
|
if (raw.startsWith('./') || raw.startsWith('../') || raw.startsWith('/')) {
|
|
@@ -269,22 +340,13 @@ export function installFromMarketplace(ref, scope) {
|
|
|
269
340
|
return { name: ref.plugin, scope, path: destPluginDir };
|
|
270
341
|
}
|
|
271
342
|
async function updateOnePlugin(plugin, marketplaceCache, opts) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
const store = resolveHttpPluginManifestStore({ root: plugin.root, commands: commandsPath });
|
|
278
|
-
if (store === null)
|
|
279
|
-
unsafeHttpManifestPath(plugin.name, commandsPath);
|
|
280
|
-
await refreshHttpPlugin({
|
|
281
|
-
name: plugin.name,
|
|
282
|
-
endpoint: plugin.manifest.transport.endpoint,
|
|
283
|
-
...(plugin.manifest.transport.authEnv !== undefined ? { authEnv: plugin.manifest.transport.authEnv } : {}),
|
|
284
|
-
}, store);
|
|
285
|
-
const commands = commandReport(plugin.name, plugin.scope);
|
|
286
|
-
return { name: plugin.name, transport: 'http', updated: true, ...(commands !== undefined ? { commands } : {}) };
|
|
343
|
+
const sourceInstalled = isSourceInstalled(plugin);
|
|
344
|
+
if (!sourceInstalled && plugin.manifest.bundle !== undefined) {
|
|
345
|
+
const replaced = await replaceBundlePlugin(plugin.name, plugin.manifest.bundle, plugin.scope, { enable: plugin.enabled });
|
|
346
|
+
return { name: plugin.name, transport: 'http', updated: true, version: replaced.version, docs: replaced.docs, commands: replaced.commands };
|
|
287
347
|
}
|
|
348
|
+
if (plugin.manifest.transport?.kind === 'http' && !sourceInstalled)
|
|
349
|
+
rejectLegacyHttpPlugin(plugin.name);
|
|
288
350
|
let contentUpdate = { updated: false, sha: currentSha(plugin.root) ?? '' };
|
|
289
351
|
let marketplaceVersion;
|
|
290
352
|
if (plugin.sourceMarketplace !== undefined) {
|
|
@@ -309,7 +371,7 @@ async function updateOnePlugin(plugin, marketplaceCache, opts) {
|
|
|
309
371
|
const commands = commandReport(plugin.name, plugin.scope);
|
|
310
372
|
return {
|
|
311
373
|
name: plugin.name,
|
|
312
|
-
...(plugin.manifest.transport
|
|
374
|
+
...(plugin.manifest.transport !== undefined ? { transport: plugin.manifest.transport.kind } : {}),
|
|
313
375
|
updated: contentUpdate.updated,
|
|
314
376
|
sha: contentUpdate.sha,
|
|
315
377
|
...(commands !== undefined ? { commands } : {}),
|
|
@@ -318,13 +380,13 @@ async function updateOnePlugin(plugin, marketplaceCache, opts) {
|
|
|
318
380
|
export const pluginInstall = defineLeaf({
|
|
319
381
|
name: 'install',
|
|
320
382
|
description: 'install from a ref or HTTP endpoint',
|
|
321
|
-
whenToUse: 'installing one plugin from a marketplace ref, git URL, local path, or an HTTP
|
|
383
|
+
whenToUse: 'installing one plugin from a marketplace ref, git URL, local path, or an HTTP directory-archive endpoint',
|
|
322
384
|
help: {
|
|
323
385
|
name: 'pkg plugin install',
|
|
324
386
|
summary: 'install a plugin from a ref or HTTP endpoint',
|
|
325
387
|
params: [
|
|
326
388
|
{ kind: 'positional', name: 'install-ref', type: 'string', required: false, constraint: 'Plugin install ref. Provide exactly one of INSTALL-REF or --endpoint. Accepted ref forms: marketplace/plugin; git URL starting https://, ssh://, git@, file://, or ending .git; local path starting ./, ../, or /.' },
|
|
327
|
-
{ kind: 'flag', name: 'endpoint', type: 'string', required: false, constraint: 'HTTP plugin endpoint. Provide exactly one of --endpoint or INSTALL-REF. Must be an absolute https: URL; http: is permitted only for localhost, 127.0.0.1, [::1], ::1, or host.docker.internal. Credentials and fragments are rejected.' },
|
|
389
|
+
{ kind: 'flag', name: 'endpoint', type: 'string', required: false, constraint: 'HTTP plugin directory-archive endpoint. Provide exactly one of --endpoint or INSTALL-REF. Must be an absolute https: URL; http: is permitted only for localhost, 127.0.0.1, [::1], ::1, or host.docker.internal. Credentials and fragments are rejected.' },
|
|
328
390
|
{ kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Required with --endpoint and rejected without it. Lowercase kebab-case plugin handle.' },
|
|
329
391
|
{ kind: 'flag', name: 'auth-env', type: 'string', required: false, constraint: 'Optional environment variable holding the HTTP bearer token. Accepted only with --endpoint; its value is never stored.' },
|
|
330
392
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Default: project if available, else user.' },
|
|
@@ -335,12 +397,14 @@ export const pluginInstall = defineLeaf({
|
|
|
335
397
|
{ name: 'scope', type: 'string', required: true, constraint: 'Scope the plugin was installed into.' },
|
|
336
398
|
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to the installed plugin directory.' },
|
|
337
399
|
{ name: 'transport', type: 'string', required: false, constraint: 'exec or http when the plugin declares commands. Always http for an --endpoint install.' },
|
|
400
|
+
{ name: 'version', type: 'string', required: false, constraint: 'First 12 hex characters of the fetched archive SHA-256 for an --endpoint install.' },
|
|
401
|
+
{ name: 'docs', type: 'integer', required: false, constraint: 'Memory document count written from an --endpoint archive.' },
|
|
338
402
|
{ name: 'commands', type: 'object', required: false, constraint: 'Present only when the plugin declares a command manifest. {mounts: string[] (accepted top-level command names now live for the next invocation), issues: object[] (typed validation issues that rejected a contribution — {code, path?, message, received, expected, next})}. Validated statically; commands are never executed.' },
|
|
339
403
|
],
|
|
340
404
|
outputKind: 'object',
|
|
341
405
|
effects: [
|
|
342
406
|
'A ref install clones, links, or copies the plugin into the scope plugins directory and registers it with enabled=true. Marketplace installs refresh the source marketplace before resolving the plugin entry.',
|
|
343
|
-
'An --endpoint install validates
|
|
407
|
+
'An --endpoint install fetches and validates one authenticated uncompressed tar archive before writing. It replaces the complete plugin directory with synthesized provenance and invocation metadata, commands.json, and memory docs; a fetch or validation failure preserves the prior package.',
|
|
344
408
|
'If the plugin declares a command manifest, its commands go live on the next crtr invocation. Exec commands run trusted local code only when explicitly invoked; HTTP commands call their declared endpoint only when explicitly invoked.',
|
|
345
409
|
],
|
|
346
410
|
},
|
|
@@ -478,7 +542,7 @@ export const pluginDisable = defineLeaf({
|
|
|
478
542
|
export const pluginUpdate = defineLeaf({
|
|
479
543
|
name: 'update',
|
|
480
544
|
description: 'update installed plugin content',
|
|
481
|
-
whenToUse: 'refreshing one named plugin or all installed plugins:
|
|
545
|
+
whenToUse: 'refreshing one named plugin or all installed plugins: archive plugins replace commands and memory together, while source-installed plugins retain their source update behavior',
|
|
482
546
|
help: {
|
|
483
547
|
name: 'pkg plugin update',
|
|
484
548
|
summary: 'refresh one or all installed plugins',
|
|
@@ -486,10 +550,10 @@ export const pluginUpdate = defineLeaf({
|
|
|
486
550
|
{ kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Plugin name to update. Omit to update all installed plugins.' },
|
|
487
551
|
],
|
|
488
552
|
output: [
|
|
489
|
-
{ name: 'updated', type: 'object[]', required: true, constraint: 'One entry per plugin processed: {name, transport?, updated, sha?, commands?}.
|
|
553
|
+
{ name: 'updated', type: 'object[]', required: true, constraint: 'One entry per plugin processed: {name, transport?, updated, sha?, version?, docs?, commands?, error?}. Archive plugins report transport=http and replace their complete directory after an authenticated refetch; version is the archive content identifier and docs is the written memory-document count. sha is present for git updates. A bulk update reports and skips a failed archive plugin. commands is present for validated command plugins: {mounts: string[], issues: object[]}.' },
|
|
490
554
|
],
|
|
491
555
|
outputKind: 'object',
|
|
492
|
-
effects: ['
|
|
556
|
+
effects: ['Archive plugins unconditionally re-fetch, validate, and replace their complete package; a named failure exits nonzero and preserves the prior package. Bulk update reports a failed archive plugin and continues. Source-installed plugins retain their source update, version, and last_updated behavior.'],
|
|
493
557
|
},
|
|
494
558
|
run: async (input) => {
|
|
495
559
|
const name = input['name'];
|
|
@@ -503,8 +567,19 @@ export const pluginUpdate = defineLeaf({
|
|
|
503
567
|
}
|
|
504
568
|
const targets = listAllPlugins().filter((p) => p.scope !== 'builtin');
|
|
505
569
|
const results = [];
|
|
506
|
-
for (const plugin of targets)
|
|
507
|
-
|
|
570
|
+
for (const plugin of targets) {
|
|
571
|
+
try {
|
|
572
|
+
results.push(await updateOnePlugin(plugin, marketplaceCache, { strict: false }));
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
results.push({
|
|
576
|
+
name: plugin.name,
|
|
577
|
+
...(plugin.manifest.transport !== undefined ? { transport: plugin.manifest.transport.kind } : {}),
|
|
578
|
+
updated: false,
|
|
579
|
+
error: error instanceof Error ? error.message : String(error),
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
508
583
|
return { updated: results };
|
|
509
584
|
},
|
|
510
585
|
});
|
|
@@ -20,8 +20,10 @@ test('setup keybinding draft edits, disables, resets, and discards transactional
|
|
|
20
20
|
const draft = new SetupKeybindingsDraft({ 'crtr.attach.graph.toggle': ['alt+x'] });
|
|
21
21
|
assert.equal(draft.expression('crtr.attach.graph.toggle'), 'alt+x');
|
|
22
22
|
assert.equal(draft.dirty, false);
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// M-U (not M-Y): alt+shift+y is now the crtr.attach.copy.message default, so
|
|
24
|
+
// the uppercase-normalization probe must use a gesture no catalog entry owns.
|
|
25
|
+
assert.equal(draft.edit('crtr.attach.graph.toggle', 'M-U, ctrl+g').ok, true);
|
|
26
|
+
assert.deepEqual(draft.overrides()['crtr.attach.graph.toggle'], ['alt+shift+u', 'ctrl+g']);
|
|
25
27
|
assert.equal(draft.dirty, true);
|
|
26
28
|
assert.equal(draft.unbind('crtr.attach.graph.toggle').ok, true);
|
|
27
29
|
assert.deepEqual(draft.overrides()['crtr.attach.graph.toggle'], []);
|
|
@@ -10,7 +10,7 @@ import { readJsonIfExists } from '../../core/fs-utils.js';
|
|
|
10
10
|
import { resolveBundledPiCliPath } from '../../core/runtime/pi-cli.js';
|
|
11
11
|
import { installTmuxBindings } from '../../core/runtime/tmux-chrome.js';
|
|
12
12
|
import { detectNerdFont, nerdFontInstallCommand, NERD_FONT_SELECT_HINT } from '../../core/runtime/nerd-font.js';
|
|
13
|
-
import {
|
|
13
|
+
import { ensureRenderer, isRendererReady } from '../../core/termrender/termrender.js';
|
|
14
14
|
import { registerCrouterRoot } from '../human/shared.js';
|
|
15
15
|
import { installFromMarketplace } from '../pkg/plugin-manage.js';
|
|
16
16
|
import { findPluginByName } from '../../core/resolver.js';
|
|
@@ -468,17 +468,15 @@ function installPiPackage(source, piCliPath = resolveBundledPiCliPath()) {
|
|
|
468
468
|
throw new Error(`pi install failed: ${source}`);
|
|
469
469
|
}
|
|
470
470
|
}
|
|
471
|
-
/**
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
runShellCommand(command);
|
|
481
|
-
return true;
|
|
471
|
+
/** Provision the pinned termrender venv — crouter's one remaining out-of-tree
|
|
472
|
+
* dependency. Best-effort: a renderer that will not provision (no `uv`, no
|
|
473
|
+
* network) reports as skipped and never fails setup. */
|
|
474
|
+
function ensureManagedRenderer() {
|
|
475
|
+
if (isRendererReady())
|
|
476
|
+
return 'current';
|
|
477
|
+
process.stdout.write('Provisioning the pinned termrender renderer…\n');
|
|
478
|
+
ensureRenderer();
|
|
479
|
+
return isRendererReady() ? 'provisioned' : 'unavailable';
|
|
482
480
|
}
|
|
483
481
|
export async function writeExaKey(exaKeyPath, exaKey) {
|
|
484
482
|
await mkdir(dirname(exaKeyPath), { recursive: true });
|
|
@@ -572,10 +570,17 @@ export async function executeSetupSubmission(submission) {
|
|
|
572
570
|
runShellCommand(plan.command);
|
|
573
571
|
installedSystemDeps.push(plan.tool);
|
|
574
572
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
573
|
+
switch (ensureManagedRenderer()) {
|
|
574
|
+
case 'provisioned':
|
|
575
|
+
installedPackages.push('termrender renderer');
|
|
576
|
+
break;
|
|
577
|
+
case 'current':
|
|
578
|
+
skippedPackages.push('termrender renderer (current)');
|
|
579
|
+
break;
|
|
580
|
+
case 'unavailable':
|
|
581
|
+
skippedPackages.push('termrender renderer (unavailable — install uv)');
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
579
584
|
const exaKeyWritten = submission.exaKey.trim() !== '';
|
|
580
585
|
if (exaKeyWritten) {
|
|
581
586
|
await writeExaKey(submission.exaKeyPath, submission.exaKey);
|
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
/** Write `text` to the system clipboard. Returns true on success, false when no
|
|
2
2
|
* clipboard tool is available or every attempt failed. Never throws. */
|
|
3
3
|
export declare function writeClipboardText(text: string): boolean;
|
|
4
|
+
/** Write `text` to the system clipboard AND, inside tmux, to the tmux paste
|
|
5
|
+
* buffer via `tmux load-buffer -w -` (which makes tmux emit OSC 52 outward on
|
|
6
|
+
* the app's behalf). Never emits OSC 52 from this process. Never throws.
|
|
7
|
+
* Returns which operations succeeded. */
|
|
8
|
+
export declare function writeClipboardTextEverywhere(text: string): {
|
|
9
|
+
system: boolean;
|
|
10
|
+
tmux: boolean;
|
|
11
|
+
};
|
|
@@ -37,3 +37,21 @@ export function writeClipboardText(text) {
|
|
|
37
37
|
}
|
|
38
38
|
return false;
|
|
39
39
|
}
|
|
40
|
+
/** Write `text` to the system clipboard AND, inside tmux, to the tmux paste
|
|
41
|
+
* buffer via `tmux load-buffer -w -` (which makes tmux emit OSC 52 outward on
|
|
42
|
+
* the app's behalf). Never emits OSC 52 from this process. Never throws.
|
|
43
|
+
* Returns which operations succeeded. */
|
|
44
|
+
export function writeClipboardTextEverywhere(text) {
|
|
45
|
+
const system = writeClipboardText(text);
|
|
46
|
+
let tmux = false;
|
|
47
|
+
if (process.env.TMUX !== undefined) {
|
|
48
|
+
const result = spawnSync('tmux', ['load-buffer', '-w', '-'], {
|
|
49
|
+
input: text,
|
|
50
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
51
|
+
encoding: 'utf-8',
|
|
52
|
+
});
|
|
53
|
+
// Success if no error and exit status 0.
|
|
54
|
+
tmux = !result.error && result.status === 0;
|
|
55
|
+
}
|
|
56
|
+
return { system, tmux };
|
|
57
|
+
}
|
|
@@ -25,9 +25,7 @@ export interface ContributorIssue extends CommandManifestIssue {
|
|
|
25
25
|
contributor: CommandContributorRef;
|
|
26
26
|
}
|
|
27
27
|
/** Invocation-local registry snapshot after collision resolution: the accepted
|
|
28
|
-
* contributions and every discovered issue.
|
|
29
|
-
* machinery, so there is no owner-routing map — an unknown-token miss hydrates
|
|
30
|
-
* ALL absent-store registrations once (§5.1), never a single known owner. */
|
|
28
|
+
* contributions and every discovered issue. */
|
|
31
29
|
export interface CommandRegistrySnapshot {
|
|
32
30
|
/** Accepted top-level contributions, deterministically ordered by
|
|
33
31
|
* contributor name. */
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ValidatedCommandManifest } from '../command-manifests/manifest.js';
|
|
2
|
+
export interface BundleIssue {
|
|
3
|
+
code: string;
|
|
4
|
+
path?: string;
|
|
5
|
+
message: string;
|
|
6
|
+
received: string;
|
|
7
|
+
expected: string;
|
|
8
|
+
next: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ValidatedBundle {
|
|
11
|
+
bundleVersion: 1;
|
|
12
|
+
commands: ValidatedCommandManifest;
|
|
13
|
+
commandsBytes: Uint8Array;
|
|
14
|
+
directories: ReadonlyArray<{
|
|
15
|
+
path: string;
|
|
16
|
+
}>;
|
|
17
|
+
memory: ReadonlyArray<{
|
|
18
|
+
path: string;
|
|
19
|
+
bytes: Uint8Array;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
export interface PluginBundleValidation {
|
|
23
|
+
bundle?: ValidatedBundle;
|
|
24
|
+
issues: BundleIssue[];
|
|
25
|
+
}
|
|
26
|
+
/** Validate a Core-served plugin directory archive entirely in memory. */
|
|
27
|
+
export declare function validatePluginBundle(archive: Uint8Array, options: {
|
|
28
|
+
reservedCoreNames: ReadonlySet<string>;
|
|
29
|
+
}): Promise<PluginBundleValidation>;
|