joinweave 0.1.0 → 0.1.2
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/README.md +102 -47
- package/bin/joinweave.js +40 -13
- package/package.json +4 -2
- package/src/installer.js +168 -35
- package/src/managed-mcp.js +143 -0
- package/src/terminal.js +158 -0
package/README.md
CHANGED
|
@@ -3,70 +3,125 @@
|
|
|
3
3
|
The web wasn't built for agents. Weave was.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
npx joinweave@0.1.
|
|
6
|
+
npx joinweave@0.1.2
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
One command installs the developer runtime: the prebuilt native CLI, a local
|
|
10
|
+
identity, a peer node, your workspace, and a persistent agent MCP server.
|
|
11
|
+
A perspective woven ASCII scene follows the real installation stages. There
|
|
12
|
+
are no simulated progress percentages, success messages or artificial delays.
|
|
13
|
+
No Rust toolchain is required.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
| Component | Installed location / behavior |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| Native CLI 0.1.0 | `~/.weave/bin/weave` |
|
|
18
|
+
| Local identity and data | `~/.weave`; private keys stay on this computer |
|
|
19
|
+
| Peer node | User-level launchd or systemd service |
|
|
20
|
+
| Visible workspace | `~/Weave` |
|
|
21
|
+
| MCP server 0.1.0 | `~/.weave/tools/mcp-0.1.0`; independent of the npx cache |
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
Requirements: Node.js 20 or later with npm; macOS arm64/x64 (binary minimum
|
|
24
|
+
load target macOS 11), or Linux arm64/x64 with glibc 2.39 or later. Linux arm64
|
|
25
|
+
also needs the system OpenSSL 3 libraries (`libssl.so.3`, `libcrypto.so.3`).
|
|
26
|
+
Linux managed services require a logged-in `systemd --user` session. Windows,
|
|
27
|
+
musl and older glibc systems are unsupported. Native optional dependencies must
|
|
28
|
+
be enabled when fetching joinweave's CLI dependency.
|
|
29
|
+
|
|
30
|
+
The installer checks live connectivity with native `weave doctor`. A failed
|
|
31
|
+
check remains an install failure. Your node and recoverable installation receipt
|
|
32
|
+
remain available for a rerun. Doctor is a local runtime/connectivity check;
|
|
33
|
+
independent discovery and sparse replication still require another live peer.
|
|
34
|
+
|
|
35
|
+
The installer does not register an OpenAgentID or Life ID account and does not
|
|
36
|
+
install a desktop application. Its component inventory describes only the
|
|
37
|
+
runtime components actually installed by this release. The public distribution
|
|
38
|
+
repository contains JavaScript tooling and prebuilt executables; Rust source is
|
|
39
|
+
maintained separately in a private repository.
|
|
40
|
+
|
|
41
|
+
## Consent and unattended use
|
|
42
|
+
|
|
43
|
+
The MCP server is installed by default, but connecting an agent client requires
|
|
44
|
+
separate consent. The installer asks before adding a shell PATH line or editing
|
|
45
|
+
client settings. Existing unrelated entries are retained; conflicting files,
|
|
46
|
+
modified installer entries and unsafe symbolic links are refused. Configuration
|
|
47
|
+
edits use atomic replacements and private backup files beside the config.
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
npx joinweave@0.1.2 --yes --json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`--json` produces exactly one JSON result on stdout, with no animation, prompts
|
|
54
|
+
or progress noise. Errors return `ok: false` and a nonzero exit status.
|
|
55
|
+
`--yes` skips all PATH and client configuration questions. To explicitly consent:
|
|
21
56
|
|
|
22
57
|
```sh
|
|
23
|
-
npx joinweave@0.1.
|
|
58
|
+
npx joinweave@0.1.2 --yes --path --mcp-client cursor,codex --json
|
|
24
59
|
```
|
|
25
60
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
61
|
+
The installer supports `claude-desktop`, `claude-code`, `cursor`, and `codex`.
|
|
62
|
+
`one` and `bridge` provide a manual MCP definition; their settings are not
|
|
63
|
+
written because no verified writable schema is available. Restart configured
|
|
64
|
+
clients after installation. Start a new shell after adding PATH, or use the
|
|
65
|
+
absolute binary path printed in the result.
|
|
30
66
|
|
|
31
|
-
|
|
67
|
+
Selected clients run the persistent MCP entry with the absolute Node executable
|
|
68
|
+
used during installation and explicit `WEAVE_HOME` / `WEAVE_BINARY` values.
|
|
69
|
+
They do not download MCP on startup. An existing owned npx-based configuration
|
|
70
|
+
is migrated only when that client is explicitly selected again. Otherwise a
|
|
71
|
+
headless rerun leaves client settings byte-for-byte unchanged. Keep that Node
|
|
72
|
+
installation available for clients using it. If Node/nvm changes its location,
|
|
73
|
+
a rerun with explicit `--mcp-client` consent migrates an intact owned entry to
|
|
74
|
+
the current Node; a headless rerun leaves it unchanged. Uninstall still recognizes
|
|
75
|
+
the exact owned old entry even if its Node executable is gone.
|
|
32
76
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
definition; their configuration files are not edited because no verified
|
|
39
|
-
writable MCP schema is available. These clients are detected when their
|
|
40
|
-
`.one`/`.bridge` directories or macOS application bundles are present.
|
|
41
|
-
- `--no-mcp`: skip agent configuration.
|
|
42
|
-
- `--no-service`: run a detached node with `weave node start` instead of
|
|
43
|
-
installing launchd or systemd. A live node is still required for doctor.
|
|
44
|
-
- `--json`: machine-readable output, with no prompts.
|
|
77
|
+
The MCP installation uses npm with `--ignore-scripts`, a private exclusive
|
|
78
|
+
staging directory, the pinned published package integrity, and an owned
|
|
79
|
+
file/mode census. User npm release-age policy is preserved. If a policy or a
|
|
80
|
+
network failure prevents installation, fix that condition and rerun; no partial
|
|
81
|
+
MCP tree is reported as installed.
|
|
45
82
|
|
|
46
|
-
|
|
83
|
+
## Options and next actions
|
|
84
|
+
|
|
85
|
+
- `--no-animation`: static progress; no cursor control or motion.
|
|
86
|
+
- `NO_COLOR`: disable terminal colors. `TERM=dumb` and redirected output use
|
|
87
|
+
static progress automatically.
|
|
88
|
+
- `--no-mcp`: skip MCP installation and client setup.
|
|
89
|
+
- `--no-service`: start a detached node instead of an OS-managed service.
|
|
90
|
+
- `--path`: consent to the managed shell PATH entry.
|
|
91
|
+
- `--mcp-client CLIENT`: consent to a named client; repeat or separate names
|
|
92
|
+
with commas.
|
|
93
|
+
- `--json`: machine-readable output without prompts.
|
|
94
|
+
- `--yes`: headless installation; no implicit client or PATH consent.
|
|
47
95
|
|
|
48
96
|
```sh
|
|
49
|
-
|
|
97
|
+
~/.weave/bin/weave node status --json
|
|
98
|
+
~/.weave/bin/weave strand create --json
|
|
99
|
+
~/.weave/bin/weave node logs
|
|
50
100
|
```
|
|
51
101
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
102
|
+
Use `weave node start`, `weave node stop`, `weave node status`, and
|
|
103
|
+
`weave node logs` to manage the installed node. `WEAVE_HOME` can select a
|
|
104
|
+
dedicated absolute workspace. See the [quickstart](https://docs.weave.earth/getting-started/quickstart)
|
|
105
|
+
and [MCP reference](https://docs.weave.earth/tooling/mcp) for the supported runtime
|
|
106
|
+
commands and ten MCP tools. Other library models are not added to the MCP tool
|
|
107
|
+
surface by this installer.
|
|
57
108
|
|
|
58
|
-
|
|
59
|
-
and `weave node logs`. Linux service installation requires an available
|
|
60
|
-
`systemd --user` session. On a box without one, use `--no-service`.
|
|
61
|
-
`WEAVE_HOME` can select a dedicated absolute workspace directory.
|
|
109
|
+
## Recovery and removal
|
|
62
110
|
|
|
63
111
|
```sh
|
|
64
|
-
npx joinweave@0.1.
|
|
65
|
-
npx joinweave@0.1.
|
|
112
|
+
npx joinweave@0.1.2 uninstall --yes
|
|
113
|
+
npx joinweave@0.1.2 uninstall --yes --purge
|
|
66
114
|
```
|
|
67
115
|
|
|
68
|
-
Uninstall stops the node and removes only owned service, executable, PATH
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
entries are
|
|
116
|
+
Uninstall stops the node and removes only owned service, executable, PATH,
|
|
117
|
+
client entries and the unchanged managed MCP tree. It retains identity and
|
|
118
|
+
workspace data by default. `--purge` also deletes the dedicated workspace,
|
|
119
|
+
including private identity keys. `~/Weave` is always preserved because it may
|
|
120
|
+
contain unrelated files. Modified entries are preserved and reported; they
|
|
121
|
+
block purge. An interruption preserves recoverable installed components; rerun
|
|
122
|
+
the installer to continue.
|
|
123
|
+
|
|
124
|
+
Cancellation waits for the installer child process to close, with bounded
|
|
125
|
+
SIGTERM/SIGKILL escalation. If termination cannot be confirmed, its persistent
|
|
126
|
+
lock and MCP stage are retained. A new installer refuses the lock while its
|
|
127
|
+
recorded parent or child is alive; do not manually remove it under a live process.
|
package/bin/joinweave.js
CHANGED
|
@@ -1,38 +1,65 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createInterface } from 'node:readline/promises';
|
|
3
|
-
import { Installer, VERSION, parseArgs } from '../src/installer.js';
|
|
3
|
+
import { Installer, InstallError, VERSION, parseArgs } from '../src/installer.js';
|
|
4
|
+
import { TerminalPresenter, installationSummary } from '../src/terminal.js';
|
|
4
5
|
|
|
5
6
|
const jsonRequested = process.argv.slice(2).includes('--json');
|
|
6
|
-
|
|
7
|
+
const controller = new AbortController();
|
|
8
|
+
let prompt, presenter, interrupted;
|
|
9
|
+
const interrupt = (signal) => {
|
|
10
|
+
interrupted ||= signal;
|
|
11
|
+
presenter?.finish();
|
|
12
|
+
controller.abort();
|
|
13
|
+
};
|
|
14
|
+
const onInt = () => interrupt('SIGINT'), onTerm = () => interrupt('SIGTERM');
|
|
7
15
|
try {
|
|
8
16
|
const options = parseArgs(process.argv.slice(2));
|
|
9
17
|
if (options.help) {
|
|
10
|
-
const usage = 'joinweave [uninstall] [--yes] [--json] [--no-service] [--no-mcp] [--path] [--mcp-client CLIENT] [--purge]\n\n--yes installs headlessly; PATH and MCP edits require --path and --mcp-client.\nClients: claude-desktop, claude-code, cursor, codex; one and bridge provide manual setup.\n--no-service starts a detached node. Uninstall keeps your data; --purge removes ~/.weave data.\n';
|
|
18
|
+
const usage = 'joinweave [uninstall] [--yes] [--json] [--no-animation] [--no-service] [--no-mcp] [--path] [--mcp-client CLIENT] [--purge]\n\nInstalls native CLI, local identity, peer node, workspace and a persistent MCP server.\n--yes installs headlessly; PATH and MCP client edits require --path and --mcp-client.\nClients: claude-desktop, claude-code, cursor, codex; one and bridge provide manual setup.\n--no-mcp skips the MCP server and client setup. --no-animation disables terminal motion.\n--no-service starts a detached node. Uninstall keeps your data; --purge removes ~/.weave data.\n';
|
|
11
19
|
console.log(options.json ? JSON.stringify({ ok: true, version: VERSION, usage }) : usage);
|
|
12
20
|
} else if (options.version) console.log(options.json ? JSON.stringify({ ok: true, version: VERSION }) : VERSION);
|
|
13
21
|
else {
|
|
14
|
-
if (!options.yes && !options.json && !process.stdin.isTTY && options.action === 'install') throw
|
|
22
|
+
if (!options.yes && !options.json && !process.stdin.isTTY && options.action === 'install') throw new InstallError('INTERACTION_REQUIRED', 'For unattended installation use --yes. MCP client and PATH edits still need explicit flags.');
|
|
23
|
+
presenter = new TerminalPresenter({ json: options.json, noAnimation: options.noAnimation });
|
|
15
24
|
if (!options.yes && !options.json && process.stdin.isTTY) prompt = createInterface({ input: process.stdin, output: process.stderr });
|
|
25
|
+
process.once('SIGINT', onInt); process.once('SIGTERM', onTerm);
|
|
26
|
+
prompt?.once('SIGINT', onInt);
|
|
16
27
|
const installer = new Installer({
|
|
17
28
|
resolveNative: options.action === 'install' ? (await import('@useweave/cli')).resolveBinary : undefined,
|
|
18
|
-
|
|
19
|
-
|
|
29
|
+
signal: controller.signal,
|
|
30
|
+
confirm: async (question) => {
|
|
31
|
+
if (!prompt) return false;
|
|
32
|
+
presenter.pause();
|
|
33
|
+
try { return /^y(?:es)?$/i.test((await prompt.question(`${question} [y/N] `, { signal: controller.signal })).trim()); }
|
|
34
|
+
finally { if (!controller.signal.aborted) presenter.resume(); }
|
|
35
|
+
},
|
|
36
|
+
stage: (stage) => {
|
|
37
|
+
if (controller.signal.aborted) throw new InstallError('INSTALL_INTERRUPTED', 'Installation interrupted. Your identity and recoverable installed components were preserved; rerun to continue.');
|
|
38
|
+
presenter.stage(stage);
|
|
39
|
+
},
|
|
40
|
+
progress: (message) => presenter.progress(message),
|
|
20
41
|
});
|
|
21
42
|
const result = await installer[options.action](options);
|
|
43
|
+
presenter.finish();
|
|
22
44
|
if (options.json) console.log(JSON.stringify(result));
|
|
23
45
|
else if (options.action === 'install') {
|
|
24
|
-
console.log(`\
|
|
46
|
+
console.log(`\n${installationSummary(result)}\n`);
|
|
25
47
|
for (const manual of result.manualMcpClients) console.log(`${manual.client}: add this MCP server in the client's settings:\n${JSON.stringify({ weave: manual.server }, null, 2)}\n`);
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
48
|
+
} else {
|
|
28
49
|
console.log(result.ok ? `Weave uninstalled. ${result.dataRetained ? 'Your data is preserved.' : 'Workspace data removed.'} Your ~/Weave folder is preserved.` : 'Uninstall preserved modified files.');
|
|
29
50
|
for (const warning of result.warnings) console.error(warning);
|
|
30
51
|
}
|
|
31
52
|
if (!result.ok) process.exitCode = 1;
|
|
32
53
|
}
|
|
33
54
|
} catch (error) {
|
|
34
|
-
|
|
55
|
+
presenter?.finish();
|
|
56
|
+
const result = { ok: false, error: interrupted
|
|
57
|
+
? { code: 'INSTALL_INTERRUPTED', message: 'Installation interrupted. Your identity and recoverable installed components were preserved; rerun to continue.' }
|
|
58
|
+
: { code: error.code || 'INSTALL_FAILED', message: error.message } };
|
|
35
59
|
if (jsonRequested) console.log(JSON.stringify(result));
|
|
36
|
-
else console.error(`joinweave: ${error.message}`);
|
|
37
|
-
process.exitCode = 1;
|
|
38
|
-
} finally {
|
|
60
|
+
else console.error(`joinweave: ${result.error.message}`);
|
|
61
|
+
process.exitCode = interrupted === 'SIGINT' ? 130 : interrupted === 'SIGTERM' ? 143 : 1;
|
|
62
|
+
} finally {
|
|
63
|
+
presenter?.finish(); prompt?.close();
|
|
64
|
+
process.removeListener('SIGINT', onInt); process.removeListener('SIGTERM', onTerm);
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "joinweave",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "One command to join the Weave agent-native web.",
|
|
6
6
|
"license": "MIT OR Apache-2.0",
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
"bin",
|
|
15
15
|
"src",
|
|
16
|
-
"README.md"
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE-APACHE",
|
|
18
|
+
"LICENSE-MIT"
|
|
17
19
|
],
|
|
18
20
|
"dependencies": {
|
|
19
21
|
"@useweave/cli": "0.1.0"
|
package/src/installer.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import { homedir, hostname } from 'node:os';
|
|
3
|
-
import { resolve, join, dirname, isAbsolute, parse } from 'node:path';
|
|
3
|
+
import { resolve, join, dirname, basename, isAbsolute, parse } from 'node:path';
|
|
4
4
|
import { createHash, randomUUID } from 'node:crypto';
|
|
5
5
|
import { execFile } from 'node:child_process';
|
|
6
|
+
import { ManagedMcp } from './managed-mcp.js';
|
|
6
7
|
|
|
7
|
-
export const VERSION = '0.1.
|
|
8
|
+
export const VERSION = '0.1.2';
|
|
9
|
+
const NATIVE_PACKAGE_VERSION = '0.1.0';
|
|
10
|
+
// Published native0.1.0 treats this as its service-ownership protocol version.
|
|
11
|
+
const RECEIPT_VERSION = '0.1.0';
|
|
8
12
|
const PATH_BEGIN = '# >>> joinweave managed PATH >>>';
|
|
9
13
|
const PATH_END = '# <<< joinweave managed PATH <<<' ;
|
|
10
14
|
const MCP_BEGIN = '# >>> joinweave managed MCP >>>';
|
|
@@ -21,7 +25,7 @@ const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
|
21
25
|
const regular = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
22
26
|
|
|
23
27
|
export function parseArgs(args) {
|
|
24
|
-
const options = { action: 'install', yes: false, json: false, noService: false, noMcp: false, path: false, purge: false, mcpClients: [] };
|
|
28
|
+
const options = { action: 'install', yes: false, json: false, noService: false, noMcp: false, noAnimation: false, path: false, purge: false, mcpClients: [] };
|
|
25
29
|
for (let i = 0; i < args.length; i++) {
|
|
26
30
|
const arg = args[i];
|
|
27
31
|
if (arg === 'uninstall' && i === 0) options.action = 'uninstall';
|
|
@@ -29,6 +33,7 @@ export function parseArgs(args) {
|
|
|
29
33
|
else if (arg === '--json') options.json = true;
|
|
30
34
|
else if (arg === '--no-service') options.noService = true;
|
|
31
35
|
else if (arg === '--no-mcp') options.noMcp = true;
|
|
36
|
+
else if (arg === '--no-animation') options.noAnimation = true;
|
|
32
37
|
else if (arg === '--path') options.path = true;
|
|
33
38
|
else if (arg === '--purge') options.purge = true;
|
|
34
39
|
else if (arg === '--help' || arg === '-h') options.help = true;
|
|
@@ -111,10 +116,58 @@ function unlinkIfUnchanged(path, expectedHash) {
|
|
|
111
116
|
return true;
|
|
112
117
|
}
|
|
113
118
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
/** Callback errors can precede close. Ownership is released only after close. */
|
|
120
|
+
export function runCommand(command, args, { env = process.env, timeout = 60_000, cwd, signal } = {}) {
|
|
121
|
+
return new Promise((done) => {
|
|
122
|
+
let child, stdout = '', stderr = '', error, closed = false, settled = false;
|
|
123
|
+
let deadlineTimer, killTimer, closeTimer, stopping = false;
|
|
124
|
+
const finish = (confirmed) => {
|
|
125
|
+
if (settled) return;
|
|
126
|
+
settled = true;
|
|
127
|
+
clearTimeout(deadlineTimer); clearTimeout(killTimer); clearTimeout(closeTimer);
|
|
128
|
+
signal?.removeEventListener('abort', cancel);
|
|
129
|
+
const result = { code: error || stopping ? (typeof error?.code === 'number' ? error.code : 1) : (child?.exitCode ?? 1), stdout, stderr, terminationConfirmed: confirmed };
|
|
130
|
+
if (!confirmed) {
|
|
131
|
+
result.childPid = child?.pid;
|
|
132
|
+
// Keep the persistent lock/stage, but allow the reporting installer to
|
|
133
|
+
// exit. A future installer checks the recorded child's liveness too.
|
|
134
|
+
child?.unref(); child?.stdout?.destroy(); child?.stderr?.destroy();
|
|
135
|
+
}
|
|
136
|
+
done(result);
|
|
137
|
+
};
|
|
138
|
+
const stop = (reason) => {
|
|
139
|
+
if (settled || stopping) return;
|
|
140
|
+
stopping = true; error ||= Object.assign(new Error(reason), { code: reason });
|
|
141
|
+
if (closed) { finish(true); return; }
|
|
142
|
+
child?.kill('SIGTERM');
|
|
143
|
+
killTimer = setTimeout(() => { if (!closed) child?.kill('SIGKILL'); }, 500);
|
|
144
|
+
closeTimer = setTimeout(() => finish(closed), 3_000);
|
|
145
|
+
};
|
|
146
|
+
const cancel = () => stop('ABORT_ERR');
|
|
147
|
+
if (signal?.aborted) { done({ code: 1, stdout, stderr, terminationConfirmed: true }); return; }
|
|
148
|
+
try {
|
|
149
|
+
// Lifecycle/timeout are owned here rather than execFile's early abort
|
|
150
|
+
// callback. No shell, and no lifecycle script descendants for MCP npm.
|
|
151
|
+
child = execFile(command, args, { env, cwd, maxBuffer: 2 * 1024 * 1024, encoding: 'utf8', shell: false }, (failure, out, err) => {
|
|
152
|
+
error ||= failure; stdout = out || ''; stderr = err || '';
|
|
153
|
+
if (failure && !closed && child?.exitCode === null && child?.signalCode === null) stop('COMMAND_ERROR');
|
|
154
|
+
});
|
|
155
|
+
child.once('close', () => { closed = true; finish(true); });
|
|
156
|
+
child.once('error', (failure) => { error ||= failure; stop('SPAWN_ERROR'); });
|
|
157
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
158
|
+
if (signal?.aborted) cancel();
|
|
159
|
+
else deadlineTimer = setTimeout(() => stop('ETIMEDOUT'), Math.max(1, timeout));
|
|
160
|
+
} catch (failure) { error = failure; finish(true); }
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function abortableSleep(ms, signal) {
|
|
165
|
+
return new Promise((done, reject) => {
|
|
166
|
+
if (signal?.aborted) { reject(new InstallError('INSTALL_INTERRUPTED', 'Installation interrupted.')); return; }
|
|
167
|
+
const abort = () => { clearTimeout(timer); reject(new InstallError('INSTALL_INTERRUPTED', 'Installation interrupted.')); };
|
|
168
|
+
const timer = setTimeout(() => { signal?.removeEventListener('abort', abort); done(); }, ms);
|
|
169
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
170
|
+
});
|
|
118
171
|
}
|
|
119
172
|
|
|
120
173
|
const xml = (value) => value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
|
@@ -124,7 +177,7 @@ const systemdPath = (value) => value.replaceAll('%', '%%');
|
|
|
124
177
|
const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
125
178
|
|
|
126
179
|
export class Installer {
|
|
127
|
-
constructor({ home = homedir(), weaveHome = process.env.WEAVE_HOME || join(home, '.weave'), platform = process.platform, uid = process.getuid?.(), env = process.env, run = runCommand, resolveNative, confirm = async () => false, progress = () => {},
|
|
180
|
+
constructor({ home = homedir(), weaveHome = process.env.WEAVE_HOME || join(home, '.weave'), platform = process.platform, uid = process.getuid?.(), env = process.env, run = runCommand, resolveNative, confirm = async () => false, progress = () => {}, stage = () => {}, signal, nodePath = process.execPath, npmCli, sleep = abortableSleep, now = Date.now } = {}) {
|
|
128
181
|
this.home = resolve(home);
|
|
129
182
|
this.weaveHome = resolve(weaveHome);
|
|
130
183
|
if ([this.home, this.weaveHome].some((path) => /[\x00-\x1f\x7f]/.test(path))) fail('UNSAFE_PATH', 'Workspace and home paths cannot contain control characters.');
|
|
@@ -134,21 +187,41 @@ export class Installer {
|
|
|
134
187
|
this.platform = platform;
|
|
135
188
|
this.uid = uid;
|
|
136
189
|
this.env = { ...env, WEAVE_HOME: this.weaveHome };
|
|
137
|
-
this.run =
|
|
190
|
+
this.run = async (...args) => {
|
|
191
|
+
const result = await run(...args);
|
|
192
|
+
if (result.terminationConfirmed === false) {
|
|
193
|
+
this.preserveLock = true;
|
|
194
|
+
if (Number.isInteger(result.childPid) && result.childPid > 0 && this.lockBytes) {
|
|
195
|
+
const owner = JSON.parse(this.lockBytes);
|
|
196
|
+
owner.children = [...new Set([...(owner.children || []), result.childPid])];
|
|
197
|
+
const next = Buffer.from(JSON.stringify(owner));
|
|
198
|
+
atomicWrite(this.lockPath, next, { previous: this.lockBytes });
|
|
199
|
+
this.lockBytes = next;
|
|
200
|
+
}
|
|
201
|
+
fail('CHILD_TERMINATION_UNCONFIRMED', 'Could not confirm installer subprocess termination. Its persistent lock, MCP staging tree and installed data were preserved. Do not remove the lock while its recorded process is alive.');
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
};
|
|
138
205
|
this.resolveNative = resolveNative;
|
|
139
206
|
this.confirm = confirm;
|
|
140
207
|
this.progress = progress;
|
|
208
|
+
this.stage = stage;
|
|
209
|
+
this.signal = signal;
|
|
210
|
+
this.nodePath = nodePath;
|
|
211
|
+
this.npmCli = npmCli;
|
|
212
|
+
this.mcpRuntime = new ManagedMcp(this);
|
|
141
213
|
this.sleep = sleep;
|
|
214
|
+
this.now = now;
|
|
142
215
|
this.binary = join(this.weaveHome, 'bin', 'weave');
|
|
143
216
|
this.manifestPath = join(this.weaveHome, '.joinweave-install.json');
|
|
144
217
|
this.lockPath = join(this.weaveHome, '.joinweave-install.lock');
|
|
145
|
-
this.manifest = { schema: 1, installer: 'joinweave', version: VERSION, binary: null, service: null, path: null, mcp: {} };
|
|
218
|
+
this.manifest = { schema: 1, installer: 'joinweave', version: RECEIPT_VERSION, installerVersion: VERSION, binary: null, service: null, path: null, mcp: {} };
|
|
146
219
|
this.warnings = [];
|
|
147
220
|
this.manualMcp = [];
|
|
148
221
|
}
|
|
149
222
|
|
|
150
223
|
validatePlatform() {
|
|
151
|
-
if (!['darwin', 'linux'].includes(this.platform)) fail('UNSUPPORTED_PLATFORM',
|
|
224
|
+
if (!['darwin', 'linux'].includes(this.platform)) fail('UNSUPPORTED_PLATFORM', `joinweave ${VERSION} supports macOS and Linux. Windows service installation is not supported yet.`);
|
|
152
225
|
}
|
|
153
226
|
|
|
154
227
|
loadManifest() {
|
|
@@ -161,7 +234,8 @@ export class Installer {
|
|
|
161
234
|
}
|
|
162
235
|
|
|
163
236
|
saveManifest() {
|
|
164
|
-
this.manifest.version =
|
|
237
|
+
this.manifest.version = RECEIPT_VERSION;
|
|
238
|
+
this.manifest.installerVersion = VERSION;
|
|
165
239
|
atomicWrite(this.manifestPath, Buffer.from(`${JSON.stringify(this.manifest, null, 2)}\n`), { previous: readFile(this.manifestPath) });
|
|
166
240
|
}
|
|
167
241
|
|
|
@@ -172,8 +246,10 @@ export class Installer {
|
|
|
172
246
|
let owner;
|
|
173
247
|
try { owner = JSON.parse(old); } catch { fail('INSTALL_LOCKED', 'An unrecognized installer lock exists. It will not be removed automatically.'); }
|
|
174
248
|
if (owner.host !== hostname() || !Number.isInteger(owner.pid) || owner.pid < 1) fail('INSTALL_LOCKED', 'Another installer owns this workspace.');
|
|
175
|
-
|
|
176
|
-
|
|
249
|
+
if (owner.children !== undefined && (!Array.isArray(owner.children) || owner.children.some((pid) => !Number.isInteger(pid) || pid < 1))) fail('INSTALL_LOCKED', 'Installer lock has an unrecognized subprocess owner.');
|
|
250
|
+
const alive = [owner.pid, ...(owner.children || [])].some((pid) => {
|
|
251
|
+
try { process.kill(pid, 0); return true; } catch (error) { return error.code !== 'ESRCH'; }
|
|
252
|
+
});
|
|
177
253
|
if (alive) fail('INSTALL_LOCKED', 'Another installer is running in this workspace.');
|
|
178
254
|
if (!unlinkIfUnchanged(this.lockPath, digest(old))) fail('INSTALL_LOCKED', 'Installer lock changed; rerun after the other installer finishes.');
|
|
179
255
|
}
|
|
@@ -182,11 +258,12 @@ export class Installer {
|
|
|
182
258
|
}
|
|
183
259
|
|
|
184
260
|
unlock() {
|
|
185
|
-
if (this.lockBytes) unlinkIfUnchanged(this.lockPath, digest(this.lockBytes));
|
|
261
|
+
if (this.lockBytes && !this.preserveLock) unlinkIfUnchanged(this.lockPath, digest(this.lockBytes));
|
|
186
262
|
}
|
|
187
263
|
|
|
188
264
|
async native(args, label, timeout = 60_000) {
|
|
189
|
-
|
|
265
|
+
if (this.signal?.aborted) fail('INSTALL_INTERRUPTED', 'Installation interrupted.');
|
|
266
|
+
const result = await this.run(this.binary, args, { env: this.env, timeout, signal: this.signal });
|
|
190
267
|
if (result.code !== 0) fail('NATIVE_COMMAND_FAILED', `${label} failed (exit ${result.code}). Run ${this.binary} ${args.join(' ')} to inspect the runtime result.`);
|
|
191
268
|
let data;
|
|
192
269
|
try { data = JSON.parse(result.stdout); } catch { fail('INVALID_NATIVE_RESULT', `${label} did not return valid JSON.`); }
|
|
@@ -199,8 +276,9 @@ export class Installer {
|
|
|
199
276
|
for (let attempt = 0; ; attempt++) {
|
|
200
277
|
try { return await this.native(['doctor', '--json'], 'Weave doctor', 90_000); }
|
|
201
278
|
catch (error) {
|
|
279
|
+
if (this.signal?.aborted) fail('INSTALL_INTERRUPTED', 'Installation interrupted.');
|
|
202
280
|
if (!['NATIVE_COMMAND_FAILED', 'NATIVE_CHECK_FAILED'].includes(error.code) || attempt >= 9 || Date.now() >= deadline) throw error;
|
|
203
|
-
await this.sleep(1_000);
|
|
281
|
+
await this.sleep(1_000, this.signal);
|
|
204
282
|
}
|
|
205
283
|
}
|
|
206
284
|
}
|
|
@@ -213,7 +291,7 @@ export class Installer {
|
|
|
213
291
|
if (old && (!this.manifest.binary || digest(old) !== this.manifest.binary.sha256)) fail('UNMANAGED_BINARY', `An unrelated or modified executable exists at ${this.binary}. It will not be replaced.`);
|
|
214
292
|
if (!old || digest(old) !== sha256) atomicWrite(this.binary, bytes, { previous: old, mode: 0o755 });
|
|
215
293
|
fs.chmodSync(this.binary, 0o755);
|
|
216
|
-
this.manifest.binary = { sha256, version:
|
|
294
|
+
this.manifest.binary = { sha256, version: NATIVE_PACKAGE_VERSION };
|
|
217
295
|
this.saveManifest();
|
|
218
296
|
this.progress('Installed Weave');
|
|
219
297
|
}
|
|
@@ -236,20 +314,31 @@ export class Installer {
|
|
|
236
314
|
return { path: join(this.home, '.config', 'systemd', 'user', 'weave-node.service'), bytes: Buffer.from(`[Unit]\nDescription=Weave peer node\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=${systemdQuote(this.binary, true)} node run\n${entries}WorkingDirectory=${systemdPath(this.weaveHome)}\nRestart=on-failure\nRestartSec=5\nStandardOutput=append:${systemdPath(logPath)}\nStandardError=append:${systemdPath(logPath)}\n\n[Install]\nWantedBy=default.target\n`) };
|
|
237
315
|
}
|
|
238
316
|
|
|
239
|
-
async serviceCommand(args, required = true) {
|
|
317
|
+
async serviceCommand(args, required = true, timeout = 30_000) {
|
|
240
318
|
const command = this.platform === 'darwin' ? 'launchctl' : 'systemctl';
|
|
241
|
-
const result = await this.run(command, args, { env: this.env, timeout:
|
|
319
|
+
const result = await this.run(command, args, { env: this.env, timeout, signal: this.signal });
|
|
242
320
|
if (required && result.code !== 0) fail('SERVICE_COMMAND_FAILED', `${command} ${args.join(' ')} failed. Confirm a logged-in user service session is available; use --no-service for a detached node.`);
|
|
243
321
|
return result;
|
|
244
322
|
}
|
|
245
323
|
|
|
246
324
|
async stopService() {
|
|
247
325
|
if (this.platform === 'darwin') {
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
326
|
+
const deadline = this.now() + 10_000;
|
|
327
|
+
const timeoutMessage = 'Timed out waiting for launchd deregistration. Its binary and configuration were preserved.';
|
|
328
|
+
await this.serviceCommand(['bootout', `gui/${this.uid}/${serviceName}`], false, 10_000);
|
|
329
|
+
for (;;) {
|
|
330
|
+
const remaining = deadline - this.now();
|
|
331
|
+
if (remaining <= 0) fail('SERVICE_STOP_FAILED', timeoutMessage);
|
|
332
|
+
const status = await this.serviceCommand(['print', `gui/${this.uid}/${serviceName}`], false, remaining);
|
|
333
|
+
const checkedAt = this.now();
|
|
334
|
+
if (checkedAt >= deadline) fail('SERVICE_STOP_FAILED', timeoutMessage);
|
|
335
|
+
if (status.code !== 0) {
|
|
336
|
+
const missing = `Could not find service "${serviceName}" in domain for user gui: ${this.uid}`;
|
|
337
|
+
if (status.code !== 113 || !status.stderr.includes(missing)) fail('SERVICE_STOP_UNKNOWN', 'Could not verify launchd shutdown. Its binary and configuration were preserved.');
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
await this.sleep(Math.min(100, deadline - checkedAt), this.signal);
|
|
341
|
+
}
|
|
253
342
|
} else {
|
|
254
343
|
await this.serviceCommand(['--user', 'disable', '--now', 'weave-node.service'], false);
|
|
255
344
|
const status = await this.serviceCommand(['--user', 'is-active', '--quiet', 'weave-node.service'], false);
|
|
@@ -325,12 +414,36 @@ export class Installer {
|
|
|
325
414
|
return detected;
|
|
326
415
|
}
|
|
327
416
|
|
|
328
|
-
|
|
417
|
+
legacyMcpDefinition() {
|
|
329
418
|
return { command: 'npx', args: ['--yes', '@useweave/mcp@0.1.0'], env: { WEAVE_HOME: this.weaveHome, WEAVE_BINARY: this.binary } };
|
|
330
419
|
}
|
|
331
420
|
|
|
332
|
-
|
|
333
|
-
return
|
|
421
|
+
mcpDefinition() {
|
|
422
|
+
return { command: this.nodePath, args: [this.mcpRuntime.entry], env: { WEAVE_HOME: this.weaveHome, WEAVE_BINARY: this.binary } };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
mcpTomlBlock(spec = this.mcpDefinition()) {
|
|
426
|
+
return `${MCP_BEGIN}\n[mcp_servers.weave]\ncommand = ${JSON.stringify(spec.command)}\nargs = [${spec.args.map((arg) => JSON.stringify(arg)).join(', ')}]\n\n[mcp_servers.weave.env]\nWEAVE_HOME = ${JSON.stringify(this.weaveHome)}\nWEAVE_BINARY = ${JSON.stringify(this.binary)}\n${MCP_END}\n`;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
recognizedMcpReceipt(receipt) {
|
|
430
|
+
if (receipt.format === 'json') {
|
|
431
|
+
const spec = receipt.spec;
|
|
432
|
+
if (JSON.stringify(spec) === JSON.stringify(this.legacyMcpDefinition())) return true;
|
|
433
|
+
return regular(spec) && Object.keys(spec).sort().join(',') === 'args,command,env'
|
|
434
|
+
&& typeof spec.command === 'string' && isAbsolute(spec.command) && ['node', 'nodejs'].includes(basename(spec.command)) && !/[\x00-\x1f\x7f]/.test(spec.command)
|
|
435
|
+
&& JSON.stringify(spec.args) === JSON.stringify([this.mcpRuntime.entry])
|
|
436
|
+
&& JSON.stringify(spec.env) === JSON.stringify({ WEAVE_HOME: this.weaveHome, WEAVE_BINARY: this.binary });
|
|
437
|
+
}
|
|
438
|
+
if (receipt.format !== 'toml' || typeof receipt.block !== 'string') return false;
|
|
439
|
+
if (receipt.block === this.mcpTomlBlock(this.legacyMcpDefinition())) return true;
|
|
440
|
+
// Parse only the JSON-encoded command line in our exact generated block.
|
|
441
|
+
// Everything else must equal the regenerated fixed entry/environment.
|
|
442
|
+
const line = receipt.block.match(/^command = (.+)$/m);
|
|
443
|
+
if (!line) return false;
|
|
444
|
+
let command; try { command = JSON.parse(line[1]); } catch { return false; }
|
|
445
|
+
return typeof command === 'string' && isAbsolute(command) && ['node', 'nodejs'].includes(basename(command)) && !/[\x00-\x1f\x7f]/.test(command)
|
|
446
|
+
&& receipt.block === this.mcpTomlBlock({ ...this.mcpDefinition(), command });
|
|
334
447
|
}
|
|
335
448
|
|
|
336
449
|
installMcp(client) {
|
|
@@ -347,7 +460,7 @@ export class Installer {
|
|
|
347
460
|
if (!regular(config) || (config.mcpServers !== undefined && !regular(config.mcpServers))) fail('INVALID_CLIENT_CONFIG', `${client} config has an unsupported MCP structure. It will not be changed.`);
|
|
348
461
|
config.mcpServers ||= {};
|
|
349
462
|
if (Object.hasOwn(config.mcpServers, 'weave')) {
|
|
350
|
-
if (!previous || previous.path !== path ||
|
|
463
|
+
if (!previous || previous.path !== path || !this.recognizedMcpReceipt(previous) || JSON.stringify(config.mcpServers.weave) !== JSON.stringify(previous.spec)) fail('MCP_ENTRY_CONFLICT', `${client} already has an unrelated or modified weave MCP entry. It will not be changed.`);
|
|
351
464
|
}
|
|
352
465
|
config.mcpServers.weave = spec;
|
|
353
466
|
bytes = Buffer.from(`${JSON.stringify(config, null, 2)}\n`);
|
|
@@ -356,8 +469,8 @@ export class Installer {
|
|
|
356
469
|
const text = old?.toString('utf8') || '';
|
|
357
470
|
const block = this.mcpTomlBlock();
|
|
358
471
|
let next = text;
|
|
359
|
-
if (previous?.path === path && previous
|
|
360
|
-
if (previous?.path === path && text.includes(block)) next = text.replace(block, '');
|
|
472
|
+
if (previous?.path === path && !this.recognizedMcpReceipt(previous)) fail('INVALID_MANIFEST', 'The Codex MCP receipt is not a recognized Weave server definition.');
|
|
473
|
+
if (previous?.path === path && text.includes(previous.block)) next = text.replace(previous.block, '');
|
|
361
474
|
// Do not guess at inline/quoted TOML tables or overwrite foreign definitions.
|
|
362
475
|
if (/^\s*\[\[?\s*(?:mcp_servers|["']mcp_servers["'])\s*\./m.test(next) && /^\s*\[\[?\s*(?:mcp_servers|["']mcp_servers["'])\s*\.\s*(?:weave|["']weave["'])\s*(?:\.|\])/m.test(next)) fail('MCP_ENTRY_CONFLICT', 'Codex already has a weave MCP table. It will not be changed.');
|
|
363
476
|
if (next.includes(MCP_BEGIN) || next.includes(MCP_END) || /^\s*(?:mcp_servers|["']mcp_servers["'])\s*=/m.test(next) || /^\s*\[\s*(?:mcp_servers|["']mcp_servers["'])\s*\]\s*$/m.test(next)) fail('INVALID_CLIENT_CONFIG', 'Codex config uses a structure that cannot be merged safely. Add the Weave MCP entry manually.');
|
|
@@ -376,20 +489,30 @@ export class Installer {
|
|
|
376
489
|
this.lock();
|
|
377
490
|
try {
|
|
378
491
|
this.loadManifest();
|
|
492
|
+
this.stage({ id: 'runtime', label: 'Installing the native runtime' });
|
|
379
493
|
this.installBinary();
|
|
380
494
|
ensureDirectory(this.shared, 0o755);
|
|
381
|
-
|
|
495
|
+
this.stage({ id: 'identity', label: 'Opening your workspace and local identity' });
|
|
496
|
+
const initialized = await this.native(['init', '--json'], 'Workspace initialization');
|
|
382
497
|
this.progress('Created your workspace and identity');
|
|
498
|
+
this.stage({ id: 'node', label: options.noService ? 'Starting your peer node' : 'Starting your supervised peer node' });
|
|
383
499
|
if (options.noService) {
|
|
384
500
|
// A rerun switching off service management must not leave two runtime owners.
|
|
385
501
|
if (this.manifest.service) await this.removeService();
|
|
386
502
|
if (this.manifest.service) fail('MODIFIED_SERVICE', 'The modified service was preserved. Stop and remove it before starting a detached node.');
|
|
387
503
|
await this.native(['node', 'start', '--json'], 'Detached node startup');
|
|
504
|
+
this.progress('Started the detached node');
|
|
388
505
|
} else {
|
|
389
506
|
// Stop an earlier detached runtime before the service takes ownership.
|
|
390
507
|
await this.native(['node', 'stop', '--json'], 'Node shutdown before service startup');
|
|
391
508
|
await this.installService();
|
|
392
509
|
}
|
|
510
|
+
this.stage({ id: 'mcp', label: options.noMcp ? 'Keeping MCP installation disabled' : 'Installing the persistent MCP server' });
|
|
511
|
+
if (!options.noMcp) {
|
|
512
|
+
await this.mcpRuntime.install();
|
|
513
|
+
this.progress('Installed the MCP server');
|
|
514
|
+
} else this.progress('MCP installation skipped');
|
|
515
|
+
this.stage({ id: 'clients', label: 'Choosing your agent connections' });
|
|
393
516
|
const detected = this.detectClients();
|
|
394
517
|
if (options.path || (!options.yes && !options.json && await this.confirm('Add Weave to your shell PATH?'))) this.installPath();
|
|
395
518
|
const selected = [...options.mcpClients];
|
|
@@ -398,9 +521,16 @@ export class Installer {
|
|
|
398
521
|
}
|
|
399
522
|
for (const client of selected) this.installMcp(client);
|
|
400
523
|
// Doctor owns connectivity, peer and announce/find validation. Never turn a failed doctor into success.
|
|
524
|
+
this.stage({ id: 'verify', label: 'Verifying live peer connectivity' });
|
|
401
525
|
const doctor = await this.readyDoctor();
|
|
402
526
|
this.progress('Verified Weave connectivity');
|
|
403
|
-
return { ok: true, action: 'install', version: VERSION, binary: this.binary, workspace: this.weaveHome, shared: this.shared, service: { managed: !options.noService, running: true }, pathAdded: Boolean(this.manifest.path), mcpClients: Object.keys(this.manifest.mcp), detectedMcpClients: detected, manualMcpClients: this.manualMcp, doctor,
|
|
527
|
+
return { ok: true, action: 'install', version: VERSION, binary: this.binary, workspace: this.weaveHome, shared: this.shared, service: { managed: !options.noService, running: true }, pathAdded: Boolean(this.manifest.path), mcpClients: Object.keys(this.manifest.mcp), detectedMcpClients: detected, manualMcpClients: this.manualMcp, doctor, components: [
|
|
528
|
+
{ id: 'runtime', name: 'Native CLI', version: NATIVE_PACKAGE_VERSION, status: 'installed', path: this.binary },
|
|
529
|
+
{ id: 'identity', name: 'Local identity', status: 'ready', localOnly: true },
|
|
530
|
+
{ id: 'node', name: 'Peer node', status: 'running', managed: !options.noService },
|
|
531
|
+
{ id: 'workspace', name: 'Workspace', status: 'ready', path: this.shared },
|
|
532
|
+
{ id: 'mcp', name: 'Agent MCP server', version: NATIVE_PACKAGE_VERSION, status: options.noMcp ? 'skipped' : 'installed', ...(options.noMcp ? {} : { command: this.nodePath, entry: this.mcpRuntime.entry }) },
|
|
533
|
+
], initialization: initialized, next: this.manifest.path ? 'weave doctor' : `${this.binary} doctor` };
|
|
404
534
|
} finally { this.unlock(); }
|
|
405
535
|
}
|
|
406
536
|
|
|
@@ -438,7 +568,7 @@ export class Installer {
|
|
|
438
568
|
if (!clients.has(client)) fail('INVALID_MANIFEST', 'Unknown MCP client in the installation receipt.');
|
|
439
569
|
const receipt = this.manifest.mcp[client], expected = this.clientDefinition(client);
|
|
440
570
|
if (receipt.path !== expected.path || receipt.format !== expected.format) fail('INVALID_MANIFEST', 'MCP receipt does not match this user.');
|
|
441
|
-
if (
|
|
571
|
+
if (!this.recognizedMcpReceipt(receipt)) fail('INVALID_MANIFEST', 'MCP receipt is not a recognized Weave server definition.');
|
|
442
572
|
const old = readFile(expected.path);
|
|
443
573
|
if (old) {
|
|
444
574
|
let bytes;
|
|
@@ -453,7 +583,7 @@ export class Installer {
|
|
|
453
583
|
}
|
|
454
584
|
} else {
|
|
455
585
|
const text = old.toString('utf8');
|
|
456
|
-
if (
|
|
586
|
+
if (!this.recognizedMcpReceipt(receipt)) fail('INVALID_MANIFEST', 'MCP block receipt is not a recognized Weave server definition.');
|
|
457
587
|
if (!text.includes(receipt.block)) { this.warnings.push('Preserved modified Codex weave MCP entry.'); return; }
|
|
458
588
|
bytes = Buffer.from(text.replace(receipt.block, ''));
|
|
459
589
|
}
|
|
@@ -473,6 +603,7 @@ export class Installer {
|
|
|
473
603
|
this.loadManifest();
|
|
474
604
|
const known = readFile(this.manifestPath);
|
|
475
605
|
if (!known) fail('NO_INSTALL_RECEIPT', 'No joinweave installation receipt exists. Existing files will not be removed.');
|
|
606
|
+
this.stage({ id: 'node', label: 'Stopping the owned background node' });
|
|
476
607
|
await this.removeService();
|
|
477
608
|
if (this.manifest.service) fail('MODIFIED_SERVICE', 'The modified service was preserved. Stop and remove it before uninstalling the binary.');
|
|
478
609
|
const binary = readFile(this.binary);
|
|
@@ -484,6 +615,8 @@ export class Installer {
|
|
|
484
615
|
} else if (binary) this.warnings.push('Preserved unrelated or modified executable.');
|
|
485
616
|
this.removePath();
|
|
486
617
|
for (const client of Object.keys(this.manifest.mcp)) this.removeMcp(client);
|
|
618
|
+
this.stage({ id: 'mcp', label: 'Removing owned tools and connections' });
|
|
619
|
+
this.mcpRuntime.remove();
|
|
487
620
|
if (options.purge && this.warnings.length) fail('PURGE_BLOCKED', 'Modified installer entries were preserved. Resolve them before purging workspace data.');
|
|
488
621
|
if (!this.warnings.length) {
|
|
489
622
|
const receipt = readFile(this.manifestPath);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import { join, dirname, resolve, isAbsolute } from 'node:path';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { InstallError, assertSafePath } from './installer.js';
|
|
5
|
+
|
|
6
|
+
export const MCP_VERSION = '0.1.0';
|
|
7
|
+
export const MCP_INTEGRITY = 'sha512-l5i5RCgWYSZYtwXWF9AG0Bgpz6X9jcDQFYq4lzlioXkD3ZYYfs0Gxw44D+jLYAae/Dq22CGVKScA6MQRBP7RDg==';
|
|
8
|
+
const ENTRY = 'node_modules/@useweave/mcp/bin/weave-mcp.js';
|
|
9
|
+
const sha = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
10
|
+
const fail = (code, message) => { throw new InstallError(code, message); };
|
|
11
|
+
|
|
12
|
+
/** No links, sockets or special files; bounded, private, immutable runtime tree. */
|
|
13
|
+
export function censusTree(root) {
|
|
14
|
+
assertSafePath(root);
|
|
15
|
+
if (!fs.lstatSync(root).isDirectory()) fail('UNSAFE_MCP_TREE', 'MCP runtime is not a directory.');
|
|
16
|
+
const files = []; let size = 0;
|
|
17
|
+
const visit = (relative) => {
|
|
18
|
+
const path = join(root, relative), stat = fs.lstatSync(path);
|
|
19
|
+
if (stat.isSymbolicLink() || (!stat.isDirectory() && !stat.isFile())) fail('UNSAFE_MCP_TREE', `MCP runtime contains an unsupported file: ${relative}.`);
|
|
20
|
+
if (files.length >= 20_000) fail('MCP_TREE_LIMIT', 'MCP runtime exceeds the file limit.');
|
|
21
|
+
if (stat.isDirectory()) {
|
|
22
|
+
files.push({ path: relative, type: 'directory', mode: stat.mode & 0o777 });
|
|
23
|
+
for (const name of fs.readdirSync(path).sort()) visit(relative ? `${relative}/${name}` : name);
|
|
24
|
+
} else {
|
|
25
|
+
size += stat.size;
|
|
26
|
+
if (size > 128 * 1024 * 1024) fail('MCP_TREE_LIMIT', 'MCP runtime exceeds the size limit.');
|
|
27
|
+
files.push({ path: relative, type: 'file', mode: stat.mode & 0o777, size: stat.size, sha256: sha(fs.readFileSync(path)) });
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
visit('');
|
|
31
|
+
return files;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveNpmCli(env, nodePath) {
|
|
35
|
+
const named = env.npm_execpath;
|
|
36
|
+
const candidates = [named && join(dirname(named), 'npm-cli.js'), join(dirname(nodePath), '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js')].filter(Boolean);
|
|
37
|
+
for (const candidate of candidates) {
|
|
38
|
+
if (!isAbsolute(candidate)) continue;
|
|
39
|
+
try {
|
|
40
|
+
const path = fs.realpathSync(candidate);
|
|
41
|
+
if (fs.statSync(path).isFile() && path.endsWith('/npm-cli.js')) return path;
|
|
42
|
+
} catch { /* Try the npm distribution next to Node. */ }
|
|
43
|
+
}
|
|
44
|
+
fail('NPM_UNAVAILABLE', 'Cannot locate npm beside Node. Run this installer through npx, or use --no-mcp.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function verifyMcpTree(root) {
|
|
48
|
+
const files = censusTree(root);
|
|
49
|
+
const pkg = JSON.parse(fs.readFileSync(join(root, 'node_modules/@useweave/mcp/package.json'), 'utf8'));
|
|
50
|
+
const lock = JSON.parse(fs.readFileSync(join(root, 'package-lock.json'), 'utf8'));
|
|
51
|
+
const top = lock.packages?.['node_modules/@useweave/mcp'];
|
|
52
|
+
if (pkg.name !== '@useweave/mcp' || pkg.version !== MCP_VERSION || pkg.bin?.['weave-mcp'] !== 'bin/weave-mcp.js' || top?.version !== MCP_VERSION || top?.integrity !== MCP_INTEGRITY || !files.some((file) => file.path === ENTRY && file.type === 'file')) fail('INVALID_MCP_PACKAGE', 'MCP package does not match the pinned published release.');
|
|
53
|
+
for (const [path, entry] of Object.entries(lock.packages || {})) {
|
|
54
|
+
if (!path) continue;
|
|
55
|
+
if (entry.link || !/^https:\/\/registry\.npmjs\.org\//.test(entry.resolved || '') || !/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(entry.integrity || '')) fail('INVALID_MCP_LOCK', 'MCP dependency lock contains an unverified package source.');
|
|
56
|
+
}
|
|
57
|
+
return files;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class ManagedMcp {
|
|
61
|
+
constructor(installer) {
|
|
62
|
+
this.owner = installer;
|
|
63
|
+
this.path = join(installer.weaveHome, 'tools', `mcp-${MCP_VERSION}`);
|
|
64
|
+
this.entry = join(this.path, ENTRY);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
validReceipt(receipt) {
|
|
68
|
+
return receipt && receipt.version === MCP_VERSION && receipt.path === this.path && receipt.entry === this.entry && Array.isArray(receipt.files);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
unchanged(receipt) {
|
|
72
|
+
return this.validReceipt(receipt) && JSON.stringify(censusTree(this.path)) === JSON.stringify(receipt.files);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async install() {
|
|
76
|
+
const owner = this.owner;
|
|
77
|
+
let receipt = owner.manifest.mcpRuntime;
|
|
78
|
+
assertSafePath(this.path);
|
|
79
|
+
if (receipt?.staging) {
|
|
80
|
+
if (!this.validReceipt(receipt) || dirname(receipt.staging) !== dirname(this.path) || !/^\.mcp-[a-f0-9-]{36}\.staging$/.test(receipt.staging.split('/').pop())) fail('INVALID_MANIFEST', 'MCP staging receipt is not recognized.');
|
|
81
|
+
assertSafePath(receipt.staging);
|
|
82
|
+
if (fs.existsSync(receipt.staging)) {
|
|
83
|
+
if (fs.existsSync(this.path)) fail('CONCURRENT_EDIT', 'Both staged and installed MCP runtimes exist. They were preserved.');
|
|
84
|
+
if (JSON.stringify(verifyMcpTree(receipt.staging)) !== JSON.stringify(receipt.files)) fail('UNMANAGED_MCP_RUNTIME', 'The recoverable MCP stage was modified. It was preserved.');
|
|
85
|
+
fs.renameSync(receipt.staging, this.path);
|
|
86
|
+
} else if (!fs.existsSync(this.path)) {
|
|
87
|
+
delete owner.manifest.mcpRuntime;
|
|
88
|
+
owner.saveManifest();
|
|
89
|
+
receipt = undefined;
|
|
90
|
+
}
|
|
91
|
+
if (receipt) {
|
|
92
|
+
if (!this.unchanged(receipt)) fail('UNMANAGED_MCP_RUNTIME', 'The recoverable MCP runtime was modified. It was preserved.');
|
|
93
|
+
delete receipt.staging;
|
|
94
|
+
owner.saveManifest();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (fs.existsSync(this.path)) {
|
|
98
|
+
if (!this.unchanged(receipt)) fail('UNMANAGED_MCP_RUNTIME', `An unrelated or modified MCP runtime exists at ${this.path}. It was preserved.`);
|
|
99
|
+
verifyMcpTree(this.path);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (receipt) fail('MISSING_MCP_RUNTIME', 'The recorded MCP runtime is missing. Uninstall the owned receipt before reinstalling.');
|
|
103
|
+
assertSafePath(dirname(this.path));
|
|
104
|
+
fs.mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
|
|
105
|
+
const stage = join(dirname(this.path), `.mcp-${randomUUID()}.staging`);
|
|
106
|
+
fs.mkdirSync(stage, { mode: 0o700 });
|
|
107
|
+
try {
|
|
108
|
+
fs.writeFileSync(join(stage, 'package.json'), JSON.stringify({ name: 'joinweave-managed-mcp', version: MCP_VERSION, private: true, dependencies: { '@useweave/mcp': MCP_VERSION } }), { flag: 'wx', mode: 0o600 });
|
|
109
|
+
const npmCli = owner.npmCli || resolveNpmCli(owner.env, owner.nodePath);
|
|
110
|
+
const result = await owner.run(owner.nodePath, [npmCli, 'install', '--prefix', stage, '--ignore-scripts', '--bin-links=false', '--omit=optional', '--no-audit', '--no-fund', '--save-exact', '--registry=https://registry.npmjs.org', '--loglevel=error'], { env: owner.env, cwd: stage, timeout: 180_000, signal: owner.signal });
|
|
111
|
+
if (result.code !== 0) fail('MCP_INSTALL_FAILED', `MCP installation failed (exit ${result.code}). Your runtime and identity remain available. Check npm connectivity and its existing release-age policy, then rerun.`);
|
|
112
|
+
const files = verifyMcpTree(stage);
|
|
113
|
+
assertSafePath(this.path);
|
|
114
|
+
if (fs.existsSync(this.path)) fail('CONCURRENT_EDIT', 'MCP destination appeared during installation. It was not replaced.');
|
|
115
|
+
// Journal the exact verified stage before rename, so an interruption at
|
|
116
|
+
// either side of the atomic commit remains recoverable without guessing.
|
|
117
|
+
owner.manifest.mcpRuntime = { version: MCP_VERSION, path: this.path, entry: this.entry, files, staging: stage };
|
|
118
|
+
owner.saveManifest();
|
|
119
|
+
fs.renameSync(stage, this.path);
|
|
120
|
+
delete owner.manifest.mcpRuntime.staging;
|
|
121
|
+
owner.saveManifest();
|
|
122
|
+
} finally {
|
|
123
|
+
// Only this process's exclusive random staging directory; never a receipt path.
|
|
124
|
+
if (!owner.preserveLock && fs.existsSync(stage)) fs.rmSync(stage, { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
remove() {
|
|
129
|
+
const owner = this.owner, receipt = owner.manifest.mcpRuntime;
|
|
130
|
+
if (!receipt) return;
|
|
131
|
+
if (!this.validReceipt(receipt)) fail('INVALID_MANIFEST', 'MCP runtime receipt is not recognized.');
|
|
132
|
+
if (receipt.staging) { owner.warnings.push('Preserved a recoverable MCP staging receipt. Rerun installation before uninstalling it.'); return; }
|
|
133
|
+
assertSafePath(this.path);
|
|
134
|
+
if (fs.existsSync(this.path)) {
|
|
135
|
+
if (owner.warnings.length || !this.unchanged(receipt)) { owner.warnings.push('Preserved modified or still-referenced MCP runtime.'); return; }
|
|
136
|
+
// Recheck the entire owned tree immediately before removing it.
|
|
137
|
+
if (!this.unchanged(receipt)) fail('CONCURRENT_EDIT', 'MCP runtime changed while preparing removal.');
|
|
138
|
+
fs.rmSync(this.path, { recursive: true });
|
|
139
|
+
}
|
|
140
|
+
delete owner.manifest.mcpRuntime;
|
|
141
|
+
owner.saveManifest();
|
|
142
|
+
}
|
|
143
|
+
}
|
package/src/terminal.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const RESET = '\x1b[0m', HIDE = '\x1b[?25l', SHOW = '\x1b[?25h';
|
|
2
|
+
const clean = (value) => String(value).replace(/[\x00-\x1f\x7f-\x9f]/g, '');
|
|
3
|
+
const tint = (value, color, enabled) => enabled ? `\x1b[38;5;${color}m${value}${RESET}` : value;
|
|
4
|
+
const stages = ['runtime', 'identity', 'node', 'mcp', 'clients', 'verify'];
|
|
5
|
+
const glyphs = '.:-=+*#';
|
|
6
|
+
|
|
7
|
+
/** A deterministic protocol illustration. Packets describe motion, never live telemetry. */
|
|
8
|
+
export function fabricFrame(width, height, tick = 0, phase = 0, color = true) {
|
|
9
|
+
width = Math.max(24, Math.min(88, Math.floor(width)));
|
|
10
|
+
height = Math.max(8, Math.min(23, Math.floor(height)));
|
|
11
|
+
const cells = Array(width * height).fill(' '), depths = new Float64Array(width * height).fill(-Infinity), colors = new Uint8Array(width * height);
|
|
12
|
+
const angle = -0.48 + Math.sin(tick / 28) * 0.09, ca = Math.cos(angle), sa = Math.sin(angle);
|
|
13
|
+
const plot = (x, y, z, char, hue = 245) => {
|
|
14
|
+
const rx = x * ca + z * sa, rz = z * ca - x * sa;
|
|
15
|
+
const perspective = 1 / (1 - rz * 0.11);
|
|
16
|
+
const px = Math.round(width / 2 + rx * perspective * width * 0.19);
|
|
17
|
+
const py = Math.round(height / 2 - (y * 0.78 - rz * 0.29) * perspective * height * 0.31);
|
|
18
|
+
if (px < 0 || px >= width || py < 0 || py >= height) return;
|
|
19
|
+
const i = py * width + px;
|
|
20
|
+
if (rz < depths[i]) return;
|
|
21
|
+
depths[i] = rz; cells[i] = char; colors[i] = hue;
|
|
22
|
+
};
|
|
23
|
+
// Registration planes below, within and above the fabric.
|
|
24
|
+
for (const y of [-1.34, 0, 1.34]) {
|
|
25
|
+
for (let i = 0; i <= 70; i++) {
|
|
26
|
+
const p = i / 70 * 3.9 - 1.95;
|
|
27
|
+
plot(p, y, -1.08, '.', 240); plot(p, y, 1.08, '.', 240);
|
|
28
|
+
plot(-1.95, y, p * 0.55, '.', 240); plot(1.95, y, p * 0.55, '.', 240);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Counter-wound filaments cross around a signed central spine.
|
|
32
|
+
const count = width < 50 ? 22 : 38;
|
|
33
|
+
for (let strand = 0; strand < count; strand++) {
|
|
34
|
+
const offset = strand / count * Math.PI * 2;
|
|
35
|
+
const handed = strand % 2 ? 1 : -1;
|
|
36
|
+
for (let i = 0; i < 100; i++) {
|
|
37
|
+
const t = i / 99, y = (t - 0.5) * 3.65;
|
|
38
|
+
const radius = 0.80 + 0.48 * Math.pow(Math.sin(t * Math.PI), 2);
|
|
39
|
+
const a = offset + t * Math.PI * 2.1 * handed;
|
|
40
|
+
const x = Math.cos(a) * radius, z = Math.sin(a) * radius;
|
|
41
|
+
const shade = glyphs[Math.round((z / radius + 1) * 0.5 * (glyphs.length - 1))];
|
|
42
|
+
const active = strand % 6 === phase % 6;
|
|
43
|
+
plot(x, y, z, shade, active ? (handed > 0 ? 81 : 141) : z > 0 ? 250 : 240);
|
|
44
|
+
if (active && Math.abs((t + strand * 0.037 - tick / 70) % 1) < 0.016) plot(x, y, z + 0.02, '@', 159);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Individually composed peer modules, not a globe or starfield.
|
|
48
|
+
for (let i = 0; i < 18; i++) {
|
|
49
|
+
const a = i * 2.399963, y = (i % 6 - 2.5) * 0.52;
|
|
50
|
+
const x = Math.cos(a) * (1.50 + (i % 3) * 0.15), z = Math.sin(a) * 1.18;
|
|
51
|
+
const hue = i % 6 === phase % 6 ? 117 : 246;
|
|
52
|
+
plot(x - 0.10, y, z, '[', hue); plot(x, y, z, i % 3 === 0 ? '+' : ':', hue); plot(x + 0.10, y, z, ']', hue);
|
|
53
|
+
}
|
|
54
|
+
for (let i = 0; i < 7; i++) {
|
|
55
|
+
const y = (i - 3) * 0.45;
|
|
56
|
+
plot(-0.14, y, 1.33, '[', 147); plot(0, y, 1.34, phase === 1 || phase === 5 ? '#' : '=', 153); plot(0.14, y, 1.33, ']', 147);
|
|
57
|
+
if (i < 6) plot(0, y + 0.22, 1.32, '|', 245);
|
|
58
|
+
}
|
|
59
|
+
const lines = [];
|
|
60
|
+
for (let y = 0; y < height; y++) {
|
|
61
|
+
let line = '', last = -1;
|
|
62
|
+
for (let x = 0; x < width; x++) {
|
|
63
|
+
const i = y * width + x, hue = colors[i];
|
|
64
|
+
if (color && hue !== last) { line += hue ? `\x1b[38;5;${hue}m` : RESET; last = hue; }
|
|
65
|
+
line += cells[i];
|
|
66
|
+
}
|
|
67
|
+
lines.push(color ? `${line}${RESET}` : line);
|
|
68
|
+
}
|
|
69
|
+
return lines;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class TerminalPresenter {
|
|
73
|
+
constructor({ stream = process.stderr, env = process.env, json = false, noAnimation = false, timers = globalThis } = {}) {
|
|
74
|
+
this.stream = stream; this.json = json; this.timers = timers;
|
|
75
|
+
this.color = !Object.hasOwn(env, 'NO_COLOR') && env.TERM !== 'dumb';
|
|
76
|
+
this.animated = !json && !noAnimation && Boolean(stream.isTTY) && env.TERM !== 'dumb';
|
|
77
|
+
this.active = false; this.closed = false; this.height = 0; this.tick = 0;
|
|
78
|
+
this.phase = 0; this.label = 'Preparing your developer runtime'; this.completed = [];
|
|
79
|
+
this.resize = () => { if (this.active) this.render(); };
|
|
80
|
+
if (this.animated) stream.on?.('resize', this.resize);
|
|
81
|
+
}
|
|
82
|
+
start() {
|
|
83
|
+
if (!this.animated || this.closed || this.active) return;
|
|
84
|
+
this.active = true;
|
|
85
|
+
this.stream.write(HIDE);
|
|
86
|
+
this.render();
|
|
87
|
+
this.timer = this.timers.setInterval(() => { this.tick++; this.render(); }, 120);
|
|
88
|
+
this.timer?.unref?.();
|
|
89
|
+
}
|
|
90
|
+
stage({ id, label }) {
|
|
91
|
+
this.phase = Math.max(0, stages.indexOf(id)); this.label = clean(label);
|
|
92
|
+
if (this.animated) { if (!this.active) this.start(); else this.render(); }
|
|
93
|
+
else if (!this.json && !this.closed) this.stream.write(`\n> ${this.label}\n`);
|
|
94
|
+
}
|
|
95
|
+
progress(message) {
|
|
96
|
+
const text = clean(message);
|
|
97
|
+
this.completed.push(text);
|
|
98
|
+
if (this.active) this.render();
|
|
99
|
+
else if (!this.json && !this.closed) this.stream.write(` + ${text}\n`);
|
|
100
|
+
}
|
|
101
|
+
render() {
|
|
102
|
+
if (!this.active || this.closed) return;
|
|
103
|
+
const columns = Math.max(1, Math.floor(this.stream.columns || 80));
|
|
104
|
+
const width = Math.min(88, columns - 2), rows = Math.floor(this.stream.rows || 36);
|
|
105
|
+
// Tiny terminals use readable status, rather than clipping a sculpture.
|
|
106
|
+
const title = width >= 48 ? 'W E A V E / THE AGENT-NATIVE WEB' : 'W E A V E';
|
|
107
|
+
const lines = [tint(title.slice(0, columns - 1), 147, this.color)];
|
|
108
|
+
if (width >= 26 && rows >= 17) lines.push(...fabricFrame(width, Math.min(23, rows - 11), this.tick, this.phase, this.color));
|
|
109
|
+
lines.push('', tint(this.label.slice(0, columns - 1), 159, this.color));
|
|
110
|
+
const trace = stages.map((id, i) => i === this.phase ? `[${id}]` : id).join(' / ');
|
|
111
|
+
if (trace.length < columns) lines.push(tint(trace, 245, this.color));
|
|
112
|
+
const recent = this.completed.slice(-2);
|
|
113
|
+
for (const message of recent) lines.push(`+ ${message}`.slice(0, columns - 1));
|
|
114
|
+
if (width >= 44) lines.push(tint('Protocol illustration | real installation stages'.slice(0, columns - 1), 240, this.color));
|
|
115
|
+
if (rows < 7) lines.splice(0, lines.length, tint(this.label.slice(0, columns - 1), 159, this.color));
|
|
116
|
+
this.clear();
|
|
117
|
+
for (const line of lines) this.stream.write(`\r\x1b[2K${line}\n`);
|
|
118
|
+
this.widths = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, '').length);
|
|
119
|
+
this.height = lines.length;
|
|
120
|
+
}
|
|
121
|
+
clear() {
|
|
122
|
+
if (!this.height) return;
|
|
123
|
+
const columns = Math.max(1, Math.floor(this.stream.columns || 80));
|
|
124
|
+
const priorRows = this.widths?.reduce((sum, width) => sum + Math.max(1, Math.ceil(width / columns)), 0) || this.height;
|
|
125
|
+
// Reflow may put old illustration lines into scrollback. Clear only the
|
|
126
|
+
// currently visible owned region; never scroll a tall blank frame through
|
|
127
|
+
// a newly tiny terminal.
|
|
128
|
+
const height = Math.min(priorRows, Math.max(1, Math.floor(this.stream.rows || 36) - 1));
|
|
129
|
+
this.stream.write(`\x1b[${height}A`);
|
|
130
|
+
for (let i = 0; i < height; i++) this.stream.write('\r\x1b[2K\n');
|
|
131
|
+
this.stream.write(`\x1b[${height}A\r`);
|
|
132
|
+
this.height = 0;
|
|
133
|
+
}
|
|
134
|
+
pause() {
|
|
135
|
+
if (this.timer !== undefined) { this.timers.clearInterval(this.timer); this.timer = undefined; }
|
|
136
|
+
this.active = false;
|
|
137
|
+
if (this.animated) { this.clear(); this.stream.write(`${RESET}${SHOW}`); }
|
|
138
|
+
}
|
|
139
|
+
resume() { this.start(); }
|
|
140
|
+
finish() {
|
|
141
|
+
if (this.closed) return;
|
|
142
|
+
this.pause(); this.closed = true;
|
|
143
|
+
if (this.animated) this.stream.off?.('resize', this.resize);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const quote = (value) => `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
148
|
+
export function installationSummary(result) {
|
|
149
|
+
const cli = result.pathAdded ? 'weave' : quote(result.binary);
|
|
150
|
+
const lines = ['Weave is ready.', ''];
|
|
151
|
+
for (const component of result.components || []) {
|
|
152
|
+
const status = component.status === 'installed' || component.status === 'ready' ? '+' : component.status === 'running' ? '+' : '-';
|
|
153
|
+
lines.push(`${status} ${component.name}${component.version ? ` ${component.version}` : ''} / ${component.status}${component.id === 'node' ? ` (${component.managed ? 'user service' : 'detached'})` : ''}`);
|
|
154
|
+
}
|
|
155
|
+
lines.push('', `Workspace: ${result.shared}`, `Private identity and data: ${result.workspace}`, `Agent clients: ${result.mcpClients.length ? result.mcpClients.join(', ') : 'none configured; consent is required'}`, '', 'Your next thread:', ` ${cli} node status --json`, ` ${cli} strand create --json`, '', 'Docs: https://docs.weave.earth/getting-started/quickstart', 'Your identity stays local. No account or desktop app was installed.');
|
|
156
|
+
if (!result.pathAdded) lines.push('Add the CLI to PATH with a consented rerun: npx joinweave@0.1.2 --yes --path');
|
|
157
|
+
return lines.map(clean).join('\n');
|
|
158
|
+
}
|