claws-code 0.8.0
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/.claude/commands/claws-auto.md +90 -0
- package/.claude/commands/claws-bin.md +28 -0
- package/.claude/commands/claws-cleanup.md +28 -0
- package/.claude/commands/claws-do.md +82 -0
- package/.claude/commands/claws-fix.md +40 -0
- package/.claude/commands/claws-goal.md +111 -0
- package/.claude/commands/claws-help.md +54 -0
- package/.claude/commands/claws-plan.md +103 -0
- package/.claude/commands/claws-report.md +29 -0
- package/.claude/commands/claws-status.md +37 -0
- package/.claude/commands/claws-update.md +32 -0
- package/.claude/commands/claws.md +64 -0
- package/.claude/rules/claws-default-behavior.md +76 -0
- package/.claude/settings.json +112 -0
- package/.claude/settings.local.json +19 -0
- package/.claude/skills/claws-auto-engine/SKILL.md +97 -0
- package/.claude/skills/claws-goal-tracker/SKILL.md +106 -0
- package/.claude/skills/claws-prompt-templates/SKILL.md +203 -0
- package/.claude/skills/claws-wave-lead/SKILL.md +126 -0
- package/.claude/skills/claws-wave-subworker/SKILL.md +60 -0
- package/CHANGELOG.md +1949 -0
- package/LICENSE +21 -0
- package/README.md +420 -0
- package/bin/cli.js +84 -0
- package/cli.js +223 -0
- package/docs/ARCHITECTURE.md +511 -0
- package/docs/event-protocol.md +588 -0
- package/docs/features.md +562 -0
- package/docs/guide.md +891 -0
- package/docs/index.html +716 -0
- package/docs/protocol.md +323 -0
- package/extension/.vscodeignore +15 -0
- package/extension/CHANGELOG.md +1906 -0
- package/extension/LICENSE +21 -0
- package/extension/README.md +137 -0
- package/extension/docs/features.md +424 -0
- package/extension/docs/protocol.md +197 -0
- package/extension/esbuild.mjs +25 -0
- package/extension/icon.png +0 -0
- package/extension/native/.metadata.json +10 -0
- package/extension/native/node-pty/LICENSE +69 -0
- package/extension/native/node-pty/README.md +165 -0
- package/extension/native/node-pty/lib/conpty_console_list_agent.js +16 -0
- package/extension/native/node-pty/lib/conpty_console_list_agent.js.map +1 -0
- package/extension/native/node-pty/lib/eventEmitter2.js +47 -0
- package/extension/native/node-pty/lib/eventEmitter2.js.map +1 -0
- package/extension/native/node-pty/lib/index.js +52 -0
- package/extension/native/node-pty/lib/index.js.map +1 -0
- package/extension/native/node-pty/lib/interfaces.js +7 -0
- package/extension/native/node-pty/lib/interfaces.js.map +1 -0
- package/extension/native/node-pty/lib/shared/conout.js +11 -0
- package/extension/native/node-pty/lib/shared/conout.js.map +1 -0
- package/extension/native/node-pty/lib/terminal.js +190 -0
- package/extension/native/node-pty/lib/terminal.js.map +1 -0
- package/extension/native/node-pty/lib/types.js +7 -0
- package/extension/native/node-pty/lib/types.js.map +1 -0
- package/extension/native/node-pty/lib/unixTerminal.js +346 -0
- package/extension/native/node-pty/lib/unixTerminal.js.map +1 -0
- package/extension/native/node-pty/lib/utils.js +39 -0
- package/extension/native/node-pty/lib/utils.js.map +1 -0
- package/extension/native/node-pty/lib/windowsConoutConnection.js +125 -0
- package/extension/native/node-pty/lib/windowsConoutConnection.js.map +1 -0
- package/extension/native/node-pty/lib/windowsPtyAgent.js +320 -0
- package/extension/native/node-pty/lib/windowsPtyAgent.js.map +1 -0
- package/extension/native/node-pty/lib/windowsTerminal.js +199 -0
- package/extension/native/node-pty/lib/windowsTerminal.js.map +1 -0
- package/extension/native/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/extension/native/node-pty/lib/worker/conoutSocketWorker.js.map +1 -0
- package/extension/native/node-pty/package.json +64 -0
- package/extension/native/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/extension/native/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/extension/native/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/extension/native/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/pty.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/winpty-agent.exe +0 -0
- package/extension/native/node-pty/prebuilds/win32-arm64/winpty.dll +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/conpty.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/pty.node +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/winpty-agent.exe +0 -0
- package/extension/native/node-pty/prebuilds/win32-x64/winpty.dll +0 -0
- package/extension/package-lock.json +605 -0
- package/extension/package.json +343 -0
- package/extension/scripts/bundle-native.mjs +104 -0
- package/extension/scripts/deploy-dev.mjs +60 -0
- package/extension/src/ansi-strip.ts +52 -0
- package/extension/src/backends/vscode/claws-pty.ts +483 -0
- package/extension/src/backends/vscode/status-bar.ts +99 -0
- package/extension/src/backends/vscode/vscode-backend.ts +282 -0
- package/extension/src/capture-store.ts +125 -0
- package/extension/src/event-log.ts +629 -0
- package/extension/src/event-schemas.ts +478 -0
- package/extension/src/extension.js +492 -0
- package/extension/src/extension.ts +873 -0
- package/extension/src/lifecycle-engine.ts +60 -0
- package/extension/src/lifecycle-rules.ts +171 -0
- package/extension/src/lifecycle-store.ts +506 -0
- package/extension/src/peer-registry.ts +176 -0
- package/extension/src/pipeline-registry.ts +82 -0
- package/extension/src/platform.ts +64 -0
- package/extension/src/protocol.ts +532 -0
- package/extension/src/server-config.ts +98 -0
- package/extension/src/server.ts +2210 -0
- package/extension/src/task-registry.ts +51 -0
- package/extension/src/terminal-backend.ts +211 -0
- package/extension/src/terminal-manager.ts +395 -0
- package/extension/src/topic-registry.ts +70 -0
- package/extension/src/topic-utils.ts +46 -0
- package/extension/src/transport.ts +45 -0
- package/extension/src/uninstall-cleanup.ts +232 -0
- package/extension/src/wave-registry.ts +314 -0
- package/extension/src/websocket-transport.ts +153 -0
- package/extension/tsconfig.json +23 -0
- package/lib/capabilities.js +145 -0
- package/lib/dry-run.js +43 -0
- package/lib/install.js +1018 -0
- package/lib/mcp-setup.js +92 -0
- package/lib/platform.js +240 -0
- package/lib/preflight.js +152 -0
- package/lib/shell-hook.js +343 -0
- package/lib/uninstall.js +162 -0
- package/lib/verify.js +166 -0
- package/mcp_server.js +3529 -0
- package/package.json +48 -0
- package/rules/claws-default-behavior.md +72 -0
- package/scripts/_helpers/atomic-file.mjs +137 -0
- package/scripts/_helpers/fix-repair.js +64 -0
- package/scripts/_helpers/json-safe.mjs +218 -0
- package/scripts/bump-version.sh +84 -0
- package/scripts/codegen/gen-docs.mjs +61 -0
- package/scripts/codegen/gen-json-schema.mjs +62 -0
- package/scripts/codegen/gen-mcp-tools.mjs +358 -0
- package/scripts/codegen/gen-types.mjs +172 -0
- package/scripts/codegen/index.mjs +42 -0
- package/scripts/dev-hooks/check-extension-dirs.js +77 -0
- package/scripts/dev-hooks/check-open-claws-terminals.js +70 -0
- package/scripts/dev-hooks/check-stale-main.js +55 -0
- package/scripts/dev-hooks/check-tag-pushed.js +51 -0
- package/scripts/dev-hooks/check-tag-vs-main.js +56 -0
- package/scripts/dev-vsix-install.sh +60 -0
- package/scripts/fix.sh +702 -0
- package/scripts/gen-client-types.mjs +81 -0
- package/scripts/git-hooks/pre-commit +31 -0
- package/scripts/hooks/lifecycle-state.js +61 -0
- package/scripts/hooks/package.json +4 -0
- package/scripts/hooks/post-tool-use-claws.js +292 -0
- package/scripts/hooks/pre-bash-no-verify-block.js +72 -0
- package/scripts/hooks/pre-tool-use-claws.js +206 -0
- package/scripts/hooks/session-start-claws.js +97 -0
- package/scripts/hooks/stop-claws.js +88 -0
- package/scripts/inject-claude-md.js +205 -0
- package/scripts/inject-dev-hooks.js +96 -0
- package/scripts/inject-global-claude-md.js +140 -0
- package/scripts/inject-settings-hooks.js +370 -0
- package/scripts/install.ps1 +146 -0
- package/scripts/install.sh +1729 -0
- package/scripts/monitor-arm-watch.js +155 -0
- package/scripts/rebuild-node-pty.sh +245 -0
- package/scripts/report.sh +232 -0
- package/scripts/shell-hook.fish +164 -0
- package/scripts/shell-hook.ps1 +33 -0
- package/scripts/shell-hook.sh +232 -0
- package/scripts/stream-events.js +399 -0
- package/scripts/terminal-wrapper.sh +36 -0
- package/scripts/test-enforcement.sh +132 -0
- package/scripts/test-install.sh +174 -0
- package/scripts/test-installer-parity.sh +135 -0
- package/scripts/test-template-enforcement.sh +76 -0
- package/scripts/uninstall.sh +143 -0
- package/scripts/update.sh +337 -0
- package/scripts/verify-release.sh +323 -0
- package/scripts/verify-wrapped.sh +194 -0
- package/templates/CLAUDE.global.md +135 -0
- package/templates/CLAUDE.project.md +37 -0
package/lib/install.js
ADDED
|
@@ -0,0 +1,1018 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const { findAllEditorClis, longPathPreflight, dryRunLog, detectPlatformArch } = require('./platform.js');
|
|
9
|
+
const preflight = require('./preflight.js');
|
|
10
|
+
const { installCommands, installSkills, installRules, installCapabilities } = require('./capabilities.js');
|
|
11
|
+
const { injectShellHook } = require('./shell-hook.js');
|
|
12
|
+
const { writeMcpJson } = require('./mcp-setup.js');
|
|
13
|
+
const { verify } = require('./verify.js');
|
|
14
|
+
|
|
15
|
+
const HOME = os.homedir();
|
|
16
|
+
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
17
|
+
|
|
18
|
+
const TOTAL = 8;
|
|
19
|
+
let _stepN = 0;
|
|
20
|
+
|
|
21
|
+
function _step(label) { _stepN++; process.stdout.write(`\n\x1b[1m\x1b[34m[${_stepN}/${TOTAL}]\x1b[0m ${label}\n`); }
|
|
22
|
+
function _ok(msg) { process.stdout.write(` \x1b[32m✓\x1b[0m ${msg}\n`); }
|
|
23
|
+
function _warn(msg) { process.stdout.write(` \x1b[33m!\x1b[0m ${msg}\n`); }
|
|
24
|
+
function _info(msg) { process.stdout.write(` \x1b[2m${msg}\x1b[0m\n`); }
|
|
25
|
+
|
|
26
|
+
const _SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));
|
|
27
|
+
function _sleepSync(ms) { Atomics.wait(_SLEEP_BUF, 0, 0, ms); }
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if a process has CLAWS_TERMINAL_CORR_ID in its environment.
|
|
31
|
+
* Worker children have it set (spawned with it by the extension); the outer
|
|
32
|
+
* orchestrator running the installer does not. Only kills children.
|
|
33
|
+
* @param {number} pid
|
|
34
|
+
* @param {string} platform os.platform() result
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
*/
|
|
37
|
+
function _hasCorrelationId(pid, platform) {
|
|
38
|
+
try {
|
|
39
|
+
if (platform === 'darwin') {
|
|
40
|
+
// ps -E -p <pid> appends environment variables to each process line on macOS
|
|
41
|
+
const r = spawnSync('ps', ['-E', '-p', String(pid)], { encoding: 'utf8', stdio: 'pipe' });
|
|
42
|
+
return !!(r.stdout && r.stdout.includes('CLAWS_TERMINAL_CORR_ID'));
|
|
43
|
+
} else if (platform === 'linux') {
|
|
44
|
+
// /proc/<pid>/environ is NUL-delimited env for each process
|
|
45
|
+
const environ = fs.readFileSync(`/proc/${pid}/environ`, 'latin1');
|
|
46
|
+
return environ.includes('CLAWS_TERMINAL_CORR_ID');
|
|
47
|
+
}
|
|
48
|
+
} catch (_) {}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Kill stale worker mcp_server.js and spawn-helper processes from prior sessions.
|
|
54
|
+
* Safe: only kills processes that (a) reference this project's .claws-bin/mcp_server.js
|
|
55
|
+
* path AND (b) have CLAWS_TERMINAL_CORR_ID in their environment (= worker children,
|
|
56
|
+
* not the outer orchestrator running this install). Idempotent — running on a fresh
|
|
57
|
+
* system with no stale workers is a no-op.
|
|
58
|
+
* @param {string} projectRoot
|
|
59
|
+
* @param {boolean} [dryRun]
|
|
60
|
+
*/
|
|
61
|
+
function _cleanStaleWorkerProcesses(projectRoot, dryRun) {
|
|
62
|
+
const platform = os.platform();
|
|
63
|
+
const mcpServerPath = path.join(projectRoot, '.claws-bin', 'mcp_server.js');
|
|
64
|
+
let killedProcs = 0;
|
|
65
|
+
let clearedPids = 0;
|
|
66
|
+
|
|
67
|
+
if (platform === 'darwin' || platform === 'linux') {
|
|
68
|
+
// ── mcp_server.js processes ─────────────────────────────────────────────
|
|
69
|
+
const pgrepMcp = spawnSync('pgrep', ['-af', 'mcp_server\\.js'], { encoding: 'utf8', stdio: 'pipe' });
|
|
70
|
+
if (pgrepMcp.stdout) {
|
|
71
|
+
for (const line of pgrepMcp.stdout.split('\n')) {
|
|
72
|
+
if (!line.trim()) continue;
|
|
73
|
+
// pgrep -af output: "<pid> <command line>"
|
|
74
|
+
const spaceIdx = line.indexOf(' ');
|
|
75
|
+
if (spaceIdx === -1) continue;
|
|
76
|
+
const pid = parseInt(line.slice(0, spaceIdx), 10);
|
|
77
|
+
const cmdLine = line.slice(spaceIdx + 1);
|
|
78
|
+
if (!Number.isFinite(pid)) continue;
|
|
79
|
+
// Must reference this project's mcp_server.js — full path match prevents
|
|
80
|
+
// collateral kills of other projects' MCP servers
|
|
81
|
+
if (!cmdLine.includes(mcpServerPath)) continue;
|
|
82
|
+
// Must be a worker child (CLAWS_TERMINAL_CORR_ID in env) — the outer
|
|
83
|
+
// orchestrator running this install does NOT have that env var
|
|
84
|
+
if (!_hasCorrelationId(pid, platform)) continue;
|
|
85
|
+
if (dryRun) {
|
|
86
|
+
process.stdout.write(` [claws-clean] would kill PID ${pid}: ${cmdLine.slice(0, 80)}\n`);
|
|
87
|
+
} else {
|
|
88
|
+
process.stdout.write(` [claws-clean] kill PID ${pid}: ${cmdLine.slice(0, 80)}\n`);
|
|
89
|
+
spawnSync('kill', ['-TERM', String(pid)], { stdio: 'pipe' });
|
|
90
|
+
_sleepSync(2000); // 2 s grace before SIGKILL
|
|
91
|
+
try { process.kill(pid, 0); spawnSync('kill', ['-KILL', String(pid)], { stdio: 'pipe' }); } catch (_) {}
|
|
92
|
+
}
|
|
93
|
+
killedProcs++;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── spawn-helper processes (Mac/Linux only — Windows uses winpty/conpty) ─
|
|
98
|
+
const pgrepHelper = spawnSync('pgrep', ['-af', 'spawn-helper'], { encoding: 'utf8', stdio: 'pipe' });
|
|
99
|
+
if (pgrepHelper.stdout) {
|
|
100
|
+
for (const line of pgrepHelper.stdout.split('\n')) {
|
|
101
|
+
if (!line.trim()) continue;
|
|
102
|
+
const spaceIdx = line.indexOf(' ');
|
|
103
|
+
if (spaceIdx === -1) continue;
|
|
104
|
+
const pid = parseInt(line.slice(0, spaceIdx), 10);
|
|
105
|
+
if (!Number.isFinite(pid)) continue;
|
|
106
|
+
// Only kill spawn-helpers that are worker children (CORR_ID in env)
|
|
107
|
+
if (!_hasCorrelationId(pid, platform)) continue;
|
|
108
|
+
if (dryRun) {
|
|
109
|
+
process.stdout.write(` [claws-clean] would kill spawn-helper PID ${pid}\n`);
|
|
110
|
+
} else {
|
|
111
|
+
process.stdout.write(` [claws-clean] kill spawn-helper PID ${pid}\n`);
|
|
112
|
+
spawnSync('kill', ['-TERM', String(pid)], { stdio: 'pipe' });
|
|
113
|
+
}
|
|
114
|
+
killedProcs++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} else if (platform === 'win32') {
|
|
118
|
+
// Windows: PowerShell CIM query — Get-CimInstance Win32_Process filters node.exe
|
|
119
|
+
// by CommandLine; Stop-Process kills matching PIDs. Skip our own PID to be safe.
|
|
120
|
+
const ownPid = process.pid;
|
|
121
|
+
const escapedPath = mcpServerPath.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
122
|
+
const psScript = [
|
|
123
|
+
`$myPid = ${ownPid}`,
|
|
124
|
+
`$mcpPath = '${escapedPath}'`,
|
|
125
|
+
`Get-CimInstance Win32_Process -Filter "Name='node.exe'" | ForEach-Object {`,
|
|
126
|
+
` $proc = $_`,
|
|
127
|
+
` if ($proc.CommandLine -and $proc.CommandLine.Contains($mcpPath) -and $proc.ProcessId -ne $myPid) {`,
|
|
128
|
+
` Write-Output ("KILL:" + $proc.ProcessId + ":" + $proc.CommandLine.Substring(0, [Math]::Min(80, $proc.CommandLine.Length)))`,
|
|
129
|
+
` Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue`,
|
|
130
|
+
` }`,
|
|
131
|
+
`}`,
|
|
132
|
+
].join('\n');
|
|
133
|
+
const psResult = spawnSync('powershell', ['-NoProfile', '-Command', psScript],
|
|
134
|
+
{ encoding: 'utf8', stdio: 'pipe' });
|
|
135
|
+
if (psResult.stdout) {
|
|
136
|
+
for (const line of psResult.stdout.split('\n')) {
|
|
137
|
+
const l = line.trim();
|
|
138
|
+
if (!l.startsWith('KILL:')) continue;
|
|
139
|
+
process.stdout.write(` [claws-clean] killed ${l.slice(5)}\n`);
|
|
140
|
+
killedProcs++;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Clear stale .claws/sidecar.pid if the recorded PID is dead ─────────────
|
|
146
|
+
const sidecarpidPath = path.join(projectRoot, '.claws', 'sidecar.pid');
|
|
147
|
+
if (fs.existsSync(sidecarpidPath)) {
|
|
148
|
+
try {
|
|
149
|
+
const sidecarpid = parseInt(fs.readFileSync(sidecarpidPath, 'utf8').trim(), 10);
|
|
150
|
+
let alive = false;
|
|
151
|
+
try { process.kill(sidecarpid, 0); alive = true; } catch (_) {}
|
|
152
|
+
if (!alive) {
|
|
153
|
+
if (!dryRun) fs.unlinkSync(sidecarpidPath);
|
|
154
|
+
process.stdout.write(` [claws-clean] cleared stale sidecar.pid (PID ${sidecarpid} dead)\n`);
|
|
155
|
+
clearedPids++;
|
|
156
|
+
}
|
|
157
|
+
} catch (_) {}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
process.stdout.write(` [claws-clean] killed ${killedProcs} stale processes, cleared ${clearedPids} stale pid files\n`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Run the 8-phase installer.
|
|
165
|
+
* @param {object} [opts]
|
|
166
|
+
* @param {boolean} [opts.dryRun]
|
|
167
|
+
* @param {boolean} [opts.noHooks]
|
|
168
|
+
* @param {string|null} [opts.vscodeCli]
|
|
169
|
+
* @param {boolean} [opts.force]
|
|
170
|
+
*/
|
|
171
|
+
function run(opts = {}) {
|
|
172
|
+
const { dryRun = false, noHooks = false, vscodeCli: cliOverride = null } = opts;
|
|
173
|
+
const projectRoot = process.cwd();
|
|
174
|
+
_stepN = 0;
|
|
175
|
+
|
|
176
|
+
_printBanner();
|
|
177
|
+
|
|
178
|
+
// ── Pre-install: kill stale worker processes from prior sessions ────────────
|
|
179
|
+
_cleanStaleWorkerProcesses(projectRoot, dryRun);
|
|
180
|
+
|
|
181
|
+
const lpWarn = longPathPreflight(HOME);
|
|
182
|
+
if (lpWarn) _warn(lpWarn);
|
|
183
|
+
|
|
184
|
+
// ── Phase 1: validate env ──────────────────────────────────────────────────
|
|
185
|
+
_step('Validate environment');
|
|
186
|
+
const { failures: preflightFailures, warnings: preflightWarnings } =
|
|
187
|
+
preflight.run(cliOverride ? { vscodeCli: cliOverride } : {});
|
|
188
|
+
for (const w of preflightWarnings) _warn(w);
|
|
189
|
+
if (preflightFailures.length > 0) {
|
|
190
|
+
for (const f of preflightFailures) process.stderr.write(` \x1b[31m✗\x1b[0m ${f}\n`);
|
|
191
|
+
process.stderr.write('\nPreflight failed — fix the above before re-running.\n');
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
_info(`Prebuild key: ${detectPlatformArch()}`);
|
|
195
|
+
_ok('Environment OK');
|
|
196
|
+
|
|
197
|
+
// ── Phase 2: prepare .claws-bin/ ──────────────────────────────────────────
|
|
198
|
+
_step('Prepare .claws-bin/');
|
|
199
|
+
_prepareCLawsBin(projectRoot, dryRun);
|
|
200
|
+
_ok('.claws-bin/ ready');
|
|
201
|
+
|
|
202
|
+
// W7h-24: CLAWS_GLOBAL_MCP=1 → also register claws in ~/.claude/settings.json mcpServers.
|
|
203
|
+
// Matches install.sh:1150-1163.
|
|
204
|
+
if (process.env.CLAWS_GLOBAL_MCP === '1') {
|
|
205
|
+
_registerGlobalMcp(dryRun);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── Phase 3: install commands + Bug 1 sweep (project-local, matches bash) ──
|
|
209
|
+
_step('Install commands (Bug 1 sweep)');
|
|
210
|
+
installCommands(projectRoot, dryRun);
|
|
211
|
+
_writeInstallCommand(projectRoot, dryRun);
|
|
212
|
+
_ok('Commands installed');
|
|
213
|
+
|
|
214
|
+
// ── Phase 4: install skills + Bug 2 sweep ────────────────────────────────
|
|
215
|
+
_step('Install skills + rules (Bug 2 sweep)');
|
|
216
|
+
installSkills(projectRoot, dryRun);
|
|
217
|
+
installRules(projectRoot, dryRun);
|
|
218
|
+
_ok('Skills and rules installed (project-local)');
|
|
219
|
+
|
|
220
|
+
// W7h-24: CLAWS_GLOBAL_CONFIG=1 → also install commands/skills/rules into ~/.claude/
|
|
221
|
+
if (process.env.CLAWS_GLOBAL_CONFIG === '1') {
|
|
222
|
+
_step('Install global capabilities (CLAWS_GLOBAL_CONFIG=1)');
|
|
223
|
+
installCapabilities(HOME, dryRun);
|
|
224
|
+
_ok('Global capabilities installed in ~/.claude/');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Phase 5: CLAUDE.md injection (after commands so CMDS_LIST is populated) ─
|
|
228
|
+
_step('Inject CLAUDE.md block');
|
|
229
|
+
_injectClaudeMd(projectRoot, dryRun);
|
|
230
|
+
_ok('CLAUDE.md updated');
|
|
231
|
+
|
|
232
|
+
// ── Phase 6: hooks registration ───────────────────────────────────────────
|
|
233
|
+
if (noHooks) {
|
|
234
|
+
_step('Register lifecycle hooks (--no-hooks: skipped)');
|
|
235
|
+
_info('Skipping ~/.claude/settings.json hook registration');
|
|
236
|
+
} else {
|
|
237
|
+
_step('Register lifecycle hooks');
|
|
238
|
+
const hooksOk = _injectHooks(projectRoot, dryRun);
|
|
239
|
+
if (hooksOk) _ok('Hooks registered');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── Phase 6b: dev-discipline hooks (contributor environments) ─────────────
|
|
243
|
+
// W7h-31: mirrors install.sh:1301-1332 step 6b.
|
|
244
|
+
// Install when: (a) CLAWS_INSTALL_DEV_HOOKS=1, OR (b) the project IS the Claws
|
|
245
|
+
// source tree — detected by checking projectRoot (matches bash's $PROJECT_ROOT check).
|
|
246
|
+
const isDevTree = fs.existsSync(path.join(projectRoot, 'scripts', 'install.sh')) &&
|
|
247
|
+
fs.existsSync(path.join(projectRoot, 'extension', 'src'));
|
|
248
|
+
if ((process.env.CLAWS_INSTALL_DEV_HOOKS === '1' || isDevTree) && !dryRun) {
|
|
249
|
+
_installDevHooks(projectRoot);
|
|
250
|
+
} else if ((process.env.CLAWS_INSTALL_DEV_HOOKS === '1' || isDevTree) && dryRun) {
|
|
251
|
+
dryRunLog('install dev-hooks (contributor environment detected or CLAWS_INSTALL_DEV_HOOKS=1)');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Phase 7: VS Code extension install ───────────────────────────────────
|
|
255
|
+
_step('Install VS Code extension');
|
|
256
|
+
const editorClis = cliOverride
|
|
257
|
+
? [{ label: 'override', cliPath: cliOverride }]
|
|
258
|
+
: findAllEditorClis();
|
|
259
|
+
_installExtension(editorClis, dryRun);
|
|
260
|
+
|
|
261
|
+
// ── Phase 8: shell rc-file hook ───────────────────────────────────────────
|
|
262
|
+
if (noHooks) {
|
|
263
|
+
_step('Inject shell hook (--no-hooks / CLAWS_NO_GLOBAL_HOOKS=1: skipped)');
|
|
264
|
+
_info('Skipping shell rc-file hook injection');
|
|
265
|
+
} else {
|
|
266
|
+
_step('Inject shell hook');
|
|
267
|
+
if (dryRun) {
|
|
268
|
+
dryRunLog('inject claws shell hook into rc file');
|
|
269
|
+
} else {
|
|
270
|
+
try {
|
|
271
|
+
injectShellHook(REPO_ROOT, dryRun);
|
|
272
|
+
_ok('Shell hook injected');
|
|
273
|
+
} catch (err) {
|
|
274
|
+
_warn(`Shell hook injection failed: ${err.message}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── Post-install verify ───────────────────────────────────────────────────
|
|
280
|
+
if (!dryRun) {
|
|
281
|
+
const failures = verify(projectRoot);
|
|
282
|
+
if (failures.length > 0) {
|
|
283
|
+
process.stdout.write('\n');
|
|
284
|
+
for (const f of failures) _warn(f);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
_printSuccess();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
function _prepareCLawsBin(projectRoot, dryRun) {
|
|
294
|
+
const clawsBin = path.join(projectRoot, '.claws-bin');
|
|
295
|
+
const hooksDir = path.join(clawsBin, 'hooks');
|
|
296
|
+
|
|
297
|
+
const filesToCopy = [
|
|
298
|
+
{ src: path.join(REPO_ROOT, 'mcp_server.js'), name: 'mcp_server.js' },
|
|
299
|
+
{ src: path.join(REPO_ROOT, 'scripts', 'stream-events.js'), name: 'stream-events.js' },
|
|
300
|
+
{ src: path.join(REPO_ROOT, 'scripts', 'monitor-arm-watch.js'), name: 'monitor-arm-watch.js' },
|
|
301
|
+
{ src: path.join(REPO_ROOT, 'scripts', 'shell-hook.sh'), name: 'shell-hook.sh' },
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
if (dryRun) {
|
|
305
|
+
dryRunLog(`mkdir ${clawsBin}`);
|
|
306
|
+
dryRunLog(`mkdir ${hooksDir}`);
|
|
307
|
+
for (const { src, name } of filesToCopy) {
|
|
308
|
+
dryRunLog(`copy ${src} → .claws-bin/${name}`);
|
|
309
|
+
}
|
|
310
|
+
dryRunLog(`write .claws-bin/package.json shim`);
|
|
311
|
+
dryRunLog(`write .claws-bin/README.md`);
|
|
312
|
+
if (process.env.CLAWS_SKIP_EXTENSION_COPY !== '1') {
|
|
313
|
+
_copyExtensionArtifacts(projectRoot, true);
|
|
314
|
+
}
|
|
315
|
+
writeMcpJson(projectRoot, dryRun);
|
|
316
|
+
_updateGitignore(projectRoot, dryRun);
|
|
317
|
+
_updateVscodeExtensions(projectRoot, dryRun);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Guard against reparse-point artifacts from macOS tarballs extracted on Windows.
|
|
322
|
+
// rmSync clears any symlink/reparse-point that would cause EPERM on mkdir.
|
|
323
|
+
try {
|
|
324
|
+
fs.rmSync(clawsBin, { recursive: true, force: true });
|
|
325
|
+
} catch (_) { /* ignore — dir may not exist or may be partially removable */ }
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
fs.mkdirSync(clawsBin, { recursive: true });
|
|
329
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
330
|
+
} catch (e) {
|
|
331
|
+
if (e.code === 'EEXIST') {
|
|
332
|
+
process.stdout.write(' [warn] .claws-bin/ directory conflict after cleanup — continuing\n');
|
|
333
|
+
} else {
|
|
334
|
+
throw e;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
for (const { src, name } of filesToCopy) {
|
|
339
|
+
if (fs.existsSync(src)) {
|
|
340
|
+
fs.copyFileSync(src, path.join(clawsBin, name));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Copy scripts/hooks/ into .claws-bin/hooks/ — atomic to avoid partial state
|
|
345
|
+
// if the process is killed mid-copy (M-09 pattern from atomic-file.mjs).
|
|
346
|
+
const hooksSrc = path.join(REPO_ROOT, 'scripts', 'hooks');
|
|
347
|
+
if (fs.existsSync(hooksSrc)) {
|
|
348
|
+
_copyDirAtomic(hooksSrc, hooksDir);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Copy schemas/ (MCP tool schemas + JSON schemas + type definitions)
|
|
352
|
+
const schemasSrc = path.join(REPO_ROOT, 'schemas');
|
|
353
|
+
if (fs.existsSync(schemasSrc)) {
|
|
354
|
+
fs.cpSync(schemasSrc, path.join(clawsBin, 'schemas'), { recursive: true });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Copy claws-sdk.js (typed publish helpers for worker scripts)
|
|
358
|
+
const sdkSrc = path.join(REPO_ROOT, 'claws-sdk.js');
|
|
359
|
+
if (fs.existsSync(sdkSrc)) {
|
|
360
|
+
fs.copyFileSync(sdkSrc, path.join(clawsBin, 'claws-sdk.js'));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// CommonJS shim so Node treats .claws-bin as CJS even in ESM-default workspaces
|
|
364
|
+
const pkgShim = path.join(clawsBin, 'package.json');
|
|
365
|
+
if (!fs.existsSync(pkgShim)) {
|
|
366
|
+
fs.writeFileSync(pkgShim, '{"type":"commonjs"}\n', 'utf8');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// README so teammates can see what's in .claws-bin/
|
|
370
|
+
fs.writeFileSync(path.join(clawsBin, 'README.md'),
|
|
371
|
+
'# .claws-bin/\n\nProject-local Claws runtime. Auto-generated by the installer — do not edit.\n', 'utf8');
|
|
372
|
+
|
|
373
|
+
// Copy extension/ artifacts into .claws-bin/extension/ for visibility (W7h-13).
|
|
374
|
+
// VS Code loads from ~/.vscode/extensions/; this reference copy lets teammates
|
|
375
|
+
// confirm the installed version and see the bundle. Matches install.sh:968-983.
|
|
376
|
+
// Opt out with CLAWS_SKIP_EXTENSION_COPY=1.
|
|
377
|
+
if (process.env.CLAWS_SKIP_EXTENSION_COPY !== '1') {
|
|
378
|
+
_copyExtensionArtifacts(projectRoot, dryRun);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Write/update .mcp.json so the MCP server is registered in the project
|
|
382
|
+
writeMcpJson(projectRoot, dryRun);
|
|
383
|
+
|
|
384
|
+
// Add Claws runtime paths to .gitignore (W7h-9)
|
|
385
|
+
_updateGitignore(projectRoot, dryRun);
|
|
386
|
+
|
|
387
|
+
// Add neunaha.claws to .vscode/extensions.json recommendations (W7h-10)
|
|
388
|
+
_updateVscodeExtensions(projectRoot, dryRun);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Append Claws runtime entries to <projectRoot>/.gitignore if not already present.
|
|
393
|
+
* Matches install.sh lines 1094-1106.
|
|
394
|
+
* @param {string} projectRoot
|
|
395
|
+
* @param {boolean} [dryRun]
|
|
396
|
+
*/
|
|
397
|
+
function _updateGitignore(projectRoot, dryRun) {
|
|
398
|
+
const gitignore = path.join(projectRoot, '.gitignore');
|
|
399
|
+
const entries = ['.claws/', '.mcp.json', '.claws-bin/'];
|
|
400
|
+
|
|
401
|
+
if (dryRun) {
|
|
402
|
+
dryRunLog(`update ${gitignore} with claws entries`);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
let existing = '';
|
|
407
|
+
if (fs.existsSync(gitignore)) {
|
|
408
|
+
existing = fs.readFileSync(gitignore, 'utf8');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const toAdd = entries.filter(e => !existing.includes(e));
|
|
412
|
+
if (toAdd.length === 0) return;
|
|
413
|
+
|
|
414
|
+
const append = '\n# Claws runtime artifacts (auto-added by installer)\n' + toAdd.join('\n') + '\n';
|
|
415
|
+
const tmp = gitignore + '.claws-tmp.' + process.pid;
|
|
416
|
+
fs.writeFileSync(tmp, existing + append, 'utf8');
|
|
417
|
+
fs.renameSync(tmp, gitignore);
|
|
418
|
+
_ok(`.gitignore updated (${toAdd.join(', ')})`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Merge neunaha.claws into .vscode/extensions.json workspace recommendations.
|
|
423
|
+
* Tolerates JSONC comments via inline strip. Matches install.sh lines 1113-1144.
|
|
424
|
+
* @param {string} projectRoot
|
|
425
|
+
* @param {boolean} [dryRun]
|
|
426
|
+
*/
|
|
427
|
+
function _updateVscodeExtensions(projectRoot, dryRun) {
|
|
428
|
+
const vscodeDir = path.join(projectRoot, '.vscode');
|
|
429
|
+
const extFile = path.join(vscodeDir, 'extensions.json');
|
|
430
|
+
const EXT_ID = 'neunaha.claws';
|
|
431
|
+
|
|
432
|
+
if (dryRun) {
|
|
433
|
+
dryRunLog(`merge ${EXT_ID} into ${extFile}`);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
let config = { recommendations: [] };
|
|
438
|
+
if (fs.existsSync(extFile)) {
|
|
439
|
+
try {
|
|
440
|
+
const raw = fs.readFileSync(extFile, 'utf8');
|
|
441
|
+
config = JSON.parse(_stripJsonc(raw));
|
|
442
|
+
} catch {
|
|
443
|
+
_warn('.vscode/extensions.json is malformed — skipping');
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (!Array.isArray(config.recommendations)) config.recommendations = [];
|
|
449
|
+
if (config.recommendations.includes(EXT_ID)) return;
|
|
450
|
+
|
|
451
|
+
config.recommendations.push(EXT_ID);
|
|
452
|
+
fs.mkdirSync(vscodeDir, { recursive: true });
|
|
453
|
+
const tmp = extFile + '.claws-tmp.' + process.pid;
|
|
454
|
+
fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
455
|
+
fs.renameSync(tmp, extFile);
|
|
456
|
+
_ok('.vscode/extensions.json updated');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Simple JSONC comment stripper — removes line comments, block comments,
|
|
460
|
+
// and trailing commas. Sufficient for .vscode/*.json written by editors.
|
|
461
|
+
function _stripJsonc(text) {
|
|
462
|
+
return text
|
|
463
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
464
|
+
.replace(/\/\/[^\n]*/g, '')
|
|
465
|
+
.replace(/,(\s*[}\]])/g, '$1');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Register claws MCP server in ~/.claude/settings.json mcpServers (W7h-24).
|
|
470
|
+
* Matches install.sh:1150-1163. Idempotent — overwrites existing claws entry.
|
|
471
|
+
* @param {boolean} [dryRun]
|
|
472
|
+
*/
|
|
473
|
+
function _registerGlobalMcp(dryRun = false) {
|
|
474
|
+
const settingsPath = path.join(HOME, '.claude', 'settings.json');
|
|
475
|
+
const mcpServerPath = path.join(REPO_ROOT, 'mcp_server.js');
|
|
476
|
+
|
|
477
|
+
if (dryRun) {
|
|
478
|
+
dryRunLog(`register claws MCP in ${settingsPath} (CLAWS_GLOBAL_MCP=1)`);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
let cfg = {};
|
|
483
|
+
if (fs.existsSync(settingsPath)) {
|
|
484
|
+
try { cfg = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (_) {}
|
|
485
|
+
}
|
|
486
|
+
if (!cfg.mcpServers) cfg.mcpServers = {};
|
|
487
|
+
cfg.mcpServers.claws = { command: 'node', args: [mcpServerPath] };
|
|
488
|
+
|
|
489
|
+
fs.mkdirSync(path.join(HOME, '.claude'), { recursive: true });
|
|
490
|
+
const tmp = settingsPath + '.claws-tmp.' + process.pid;
|
|
491
|
+
fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
|
|
492
|
+
fs.renameSync(tmp, settingsPath);
|
|
493
|
+
_ok('Global MCP registered in ~/.claude/settings.json');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function _injectClaudeMd(projectRoot, dryRun) {
|
|
497
|
+
const injectProject = path.join(REPO_ROOT, 'scripts', 'inject-claude-md.js');
|
|
498
|
+
const injectGlobal = path.join(REPO_ROOT, 'scripts', 'inject-global-claude-md.js');
|
|
499
|
+
|
|
500
|
+
// inject-claude-md.js does not support --dry-run; guard the call ourselves.
|
|
501
|
+
if (dryRun) {
|
|
502
|
+
dryRunLog(`inject CLAWS:BEGIN block into ${path.join(projectRoot, 'CLAUDE.md')}`);
|
|
503
|
+
} else if (fs.existsSync(injectProject)) {
|
|
504
|
+
const r = spawnSync(process.execPath, [injectProject, projectRoot], {
|
|
505
|
+
cwd: REPO_ROOT, stdio: 'inherit', encoding: 'utf8',
|
|
506
|
+
});
|
|
507
|
+
if (r.status !== 0) _warn('inject-claude-md.js failed — CLAUDE.md may be stale');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// inject-global-claude-md.js natively supports --dry-run.
|
|
511
|
+
if (fs.existsSync(injectGlobal)) {
|
|
512
|
+
const extraArgs = dryRun ? ['--dry-run'] : [];
|
|
513
|
+
const r = spawnSync(process.execPath, [injectGlobal, ...extraArgs], {
|
|
514
|
+
cwd: REPO_ROOT, stdio: 'inherit', encoding: 'utf8',
|
|
515
|
+
});
|
|
516
|
+
if (r.status !== 0 && !dryRun) _warn('inject-global-claude-md.js failed — ~/.claude/CLAUDE.md may be stale');
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Copy hook scripts from <repo>/scripts/hooks/*.js to a stable global directory
|
|
522
|
+
* ($HOME/.claude/claws/hooks/) so that hooks registered in ~/.claude/settings.json
|
|
523
|
+
* survive project moves or deletions (D02 / W7h-2 Option B).
|
|
524
|
+
* Uses atomic temp+rename per file to avoid partial-copy races.
|
|
525
|
+
* @param {boolean} [dryRun]
|
|
526
|
+
* @returns {string} absolute path to the global hooks dir
|
|
527
|
+
*/
|
|
528
|
+
function installGlobalHooks(dryRun = false) {
|
|
529
|
+
// W7h-30B: return the PARENT of hooks/ (~/.claude/claws), not the hooks/ dir itself.
|
|
530
|
+
// inject-settings-hooks.js receives this as CLAWS_BIN and does path.join(CLAWS_BIN, 'hooks', script),
|
|
531
|
+
// so passing the parent produces the correct ~/.claude/claws/hooks/<script>.js path.
|
|
532
|
+
// Passing hooks/ directly caused ~/.claude/claws/hooks/hooks/<script>.js duplication.
|
|
533
|
+
const clawsParentDir = path.join(HOME, '.claude', 'claws');
|
|
534
|
+
const globalHooksDir = path.join(clawsParentDir, 'hooks');
|
|
535
|
+
const srcHooksDir = path.join(REPO_ROOT, 'scripts', 'hooks');
|
|
536
|
+
|
|
537
|
+
if (dryRun) {
|
|
538
|
+
dryRunLog(`mkdir ${globalHooksDir}`);
|
|
539
|
+
if (fs.existsSync(srcHooksDir)) {
|
|
540
|
+
for (const f of fs.readdirSync(srcHooksDir)) {
|
|
541
|
+
if (f.endsWith('.js')) dryRunLog(`copy hooks/${f} → ${globalHooksDir}/${f}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return clawsParentDir;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
fs.mkdirSync(globalHooksDir, { recursive: true });
|
|
548
|
+
|
|
549
|
+
if (fs.existsSync(srcHooksDir)) {
|
|
550
|
+
for (const file of fs.readdirSync(srcHooksDir)) {
|
|
551
|
+
if (!file.endsWith('.js')) continue;
|
|
552
|
+
const src = path.join(srcHooksDir, file);
|
|
553
|
+
const tmp = path.join(globalHooksDir, file + '.claws-tmp.' + process.pid);
|
|
554
|
+
const dst = path.join(globalHooksDir, file);
|
|
555
|
+
fs.copyFileSync(src, tmp);
|
|
556
|
+
fs.renameSync(tmp, dst);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return clawsParentDir;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function _injectHooks(projectRoot, dryRun, opts = {}) {
|
|
564
|
+
const spawnFn = opts.spawnFn || spawnSync;
|
|
565
|
+
const script = path.join(REPO_ROOT, 'scripts', 'inject-settings-hooks.js');
|
|
566
|
+
const extraArgs = dryRun ? ['--dry-run', '--update'] : ['--update'];
|
|
567
|
+
|
|
568
|
+
if (!fs.existsSync(script)) { _warn('inject-settings-hooks.js not found — hooks skipped'); return false; }
|
|
569
|
+
|
|
570
|
+
// D02 / W7h-2: install hook scripts to a stable global location so that
|
|
571
|
+
// settings.json hook commands survive project moves or deletions.
|
|
572
|
+
const globalHooksDir = installGlobalHooks(dryRun);
|
|
573
|
+
|
|
574
|
+
const r = spawnFn(process.execPath, [script, globalHooksDir, ...extraArgs], {
|
|
575
|
+
cwd: REPO_ROOT, stdio: 'inherit', encoding: 'utf8',
|
|
576
|
+
});
|
|
577
|
+
if (r.status !== 0 && !dryRun) {
|
|
578
|
+
_warn('inject-settings-hooks.js failed — hooks may not be registered');
|
|
579
|
+
_warn('Fix: run `node scripts/inject-settings-hooks.js` manually, then verify ~/.claude/settings.json');
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function _installExtension(editorClis, dryRun) {
|
|
586
|
+
const extDir = path.join(REPO_ROOT, 'extension');
|
|
587
|
+
const isWin = process.platform === 'win32';
|
|
588
|
+
// shell:true on Windows passes the whole command through cmd.exe, which splits on unquoted spaces.
|
|
589
|
+
// Wrap any path that contains spaces so cmd.exe treats it as one token.
|
|
590
|
+
const wq = (p) => (isWin && p.includes(' ') ? `"${p}"` : p);
|
|
591
|
+
|
|
592
|
+
if (dryRun) {
|
|
593
|
+
dryRunLog('check electron ABI drift (extension/native/.metadata.json vs installed editor)');
|
|
594
|
+
dryRunLog('npm install --no-fund --no-audit --loglevel=error (cwd: extension/)');
|
|
595
|
+
dryRunLog('npm run build (cwd: extension/)');
|
|
596
|
+
dryRunLog('npx @vscode/vsce package --skip-license --no-git-tag-version --no-update-package-json --no-dependencies -o <tmpdir>/claws-code-<version>.vsix');
|
|
597
|
+
dryRunLog('check VSIX size >= 50 KB');
|
|
598
|
+
for (const { label, cliPath } of editorClis) {
|
|
599
|
+
dryRunLog(`${cliPath} --install-extension <tmpdir>/claws-code-<version>.vsix --force [${label}]`);
|
|
600
|
+
}
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (editorClis.length === 0) {
|
|
605
|
+
_warn('No editor CLI found — extension not installed automatically');
|
|
606
|
+
_info('Install VS Code/Cursor/Windsurf and add CLI to PATH (or set CLAWS_VSCODE_CLI), then re-run');
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (!fs.existsSync(extDir)) {
|
|
611
|
+
_warn('extension/ not found in source — skipping extension install');
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const version = JSON.parse(fs.readFileSync(path.join(extDir, 'package.json'), 'utf8')).version;
|
|
616
|
+
const vsixPath = path.join(os.tmpdir(), `claws-code-${version}.vsix`);
|
|
617
|
+
|
|
618
|
+
// npm install — must include optionals (node-pty lives in optionalDependencies but is required by bundle-native.mjs)
|
|
619
|
+
_info('npm install (with node-pty)...');
|
|
620
|
+
const npmInstall = spawnSync('npm', ['install', '--no-fund', '--no-audit', '--loglevel=error'],
|
|
621
|
+
{ cwd: extDir, stdio: 'inherit', shell: isWin });
|
|
622
|
+
if (npmInstall.status !== 0) {
|
|
623
|
+
if (isWin) {
|
|
624
|
+
_warn('npm install failed — node-pty likely needs C++ build tools');
|
|
625
|
+
_info('Install: winget install Microsoft.VisualStudio.BuildTools');
|
|
626
|
+
_info('Then re-run the Claws installer');
|
|
627
|
+
} else {
|
|
628
|
+
_warn('npm install failed — extension not installed');
|
|
629
|
+
}
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Build extension bundle + native node-pty
|
|
634
|
+
_info('npm run build...');
|
|
635
|
+
const npmBuild = spawnSync('npm', ['run', 'build'],
|
|
636
|
+
{ cwd: extDir, stdio: 'inherit', shell: isWin });
|
|
637
|
+
if (npmBuild.status !== 0) {
|
|
638
|
+
_warn('Extension build failed — extension not installed');
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// W7h-20: Electron ABI drift detection — warn if built binary targets a
|
|
643
|
+
// different Electron than the currently installed editor. Matches install.sh:403-454.
|
|
644
|
+
_checkElectronAbiDrift(extDir);
|
|
645
|
+
|
|
646
|
+
// Package VSIX — flags match install.sh canonical set (line 686)
|
|
647
|
+
_info('vsce package...');
|
|
648
|
+
spawnSync('npx', ['--yes', '@vscode/vsce', 'package',
|
|
649
|
+
'--skip-license', '--no-git-tag-version', '--no-update-package-json',
|
|
650
|
+
'--no-dependencies', '-o', wq(vsixPath)],
|
|
651
|
+
{ cwd: extDir, stdio: 'inherit', shell: isWin });
|
|
652
|
+
if (!fs.existsSync(vsixPath)) {
|
|
653
|
+
_warn('VSIX not produced — extension not installed');
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// W7h-21: VSIX size check — <50 KB suggests native binary missing from package.
|
|
658
|
+
// Matches install.sh:697-703.
|
|
659
|
+
try {
|
|
660
|
+
const vsixSize = fs.statSync(vsixPath).size;
|
|
661
|
+
if (vsixSize < 50 * 1024) {
|
|
662
|
+
_warn(`VSIX is suspiciously small (${vsixSize} bytes < 50 KB) — native pty.node may be missing`);
|
|
663
|
+
_info('Run: cd extension && npm run build to rebuild with native binary');
|
|
664
|
+
}
|
|
665
|
+
} catch (_) {}
|
|
666
|
+
|
|
667
|
+
// Install extension into each found editor
|
|
668
|
+
let anySucceeded = false;
|
|
669
|
+
for (const { label, cliPath } of editorClis) {
|
|
670
|
+
_info(`installing into ${label}...`);
|
|
671
|
+
// W7h-26: sudo fallback on failure (Linux/macOS only) — matches install.sh:714-719.
|
|
672
|
+
let installOk = false;
|
|
673
|
+
const installR = spawnSync(wq(cliPath), ['--install-extension', wq(vsixPath), '--force'],
|
|
674
|
+
{ stdio: 'inherit', shell: isWin });
|
|
675
|
+
if (installR.status === 0) {
|
|
676
|
+
installOk = true;
|
|
677
|
+
} else if (!isWin) {
|
|
678
|
+
_info(` ${label} install failed — retrying with sudo...`);
|
|
679
|
+
const sudoR = spawnSync('sudo', [cliPath, '--install-extension', vsixPath, '--force'],
|
|
680
|
+
{ stdio: 'inherit' });
|
|
681
|
+
if (sudoR.status === 0) {
|
|
682
|
+
_info(` installed via sudo (extensions dir required elevated permissions)`);
|
|
683
|
+
installOk = true;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (!installOk) {
|
|
688
|
+
_warn(`Extension install failed for ${label} — VSIX at ${vsixPath}`);
|
|
689
|
+
} else {
|
|
690
|
+
const listR = spawnSync(wq(cliPath), ['--list-extensions'], { encoding: 'utf8', shell: isWin });
|
|
691
|
+
if (listR.stdout && listR.stdout.includes('neunaha.claws')) {
|
|
692
|
+
_ok(`neunaha.claws installed into ${label} (verified)`);
|
|
693
|
+
} else {
|
|
694
|
+
_ok(`Extension installed into ${label}`);
|
|
695
|
+
}
|
|
696
|
+
anySucceeded = true;
|
|
697
|
+
|
|
698
|
+
// W7h-25: Stale extension directory cleanup (matches install.sh:740-760).
|
|
699
|
+
// Remove neunaha.claws-<old-version> dirs after successful install.
|
|
700
|
+
_cleanStaleExtensionDirs(label, version);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (!anySucceeded) {
|
|
705
|
+
_warn('Extension install failed for all editors');
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Clean up VSIX from temp dir — matches install.sh /tmp approach
|
|
709
|
+
try { fs.unlinkSync(vsixPath); } catch (_) {}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function _writeInstallCommand(projectRoot, dryRun) {
|
|
713
|
+
const cmdDir = path.join(projectRoot, '.claude', 'commands');
|
|
714
|
+
const dest = path.join(cmdDir, 'claws-install.md');
|
|
715
|
+
const content = [
|
|
716
|
+
'---',
|
|
717
|
+
'name: claws-install',
|
|
718
|
+
'description: Install or update Claws — Terminal Control Bridge for VS Code.',
|
|
719
|
+
'---',
|
|
720
|
+
'',
|
|
721
|
+
'# /claws-install',
|
|
722
|
+
'',
|
|
723
|
+
'Install or update Claws in this project:',
|
|
724
|
+
'',
|
|
725
|
+
'```bash',
|
|
726
|
+
'npx claws-code install',
|
|
727
|
+
'```',
|
|
728
|
+
'',
|
|
729
|
+
'After the script completes:',
|
|
730
|
+
'1. Reload VS Code: Cmd+Shift+P → Developer: Reload Window',
|
|
731
|
+
'2. Restart Claude Code so the project-local `.mcp.json` is picked up.',
|
|
732
|
+
'3. Try `/claws-help` or `/claws-status`.',
|
|
733
|
+
'',
|
|
734
|
+
'If MCP tools don\'t appear, run `/claws-fix` or `/claws-report`.',
|
|
735
|
+
'',
|
|
736
|
+
].join('\n');
|
|
737
|
+
if (dryRun) { dryRunLog(`write ${dest}`); return; }
|
|
738
|
+
fs.mkdirSync(cmdDir, { recursive: true });
|
|
739
|
+
fs.writeFileSync(dest, content, 'utf8');
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Remove stale neunaha.claws-<old-version> dirs from the editor's extensions
|
|
744
|
+
* directory after a successful install (W7h-25). Matches install.sh:740-760.
|
|
745
|
+
* Polls up to 1s for the new version dir to appear (VS Code extracts asynchronously).
|
|
746
|
+
* @param {string} label - editor label: 'code' | 'code-insiders' | 'cursor' | 'windsurf'
|
|
747
|
+
* @param {string} version - current version (the one just installed)
|
|
748
|
+
*/
|
|
749
|
+
function _cleanStaleExtensionDirs(label, version) {
|
|
750
|
+
const extDirMap = {
|
|
751
|
+
'code': path.join(HOME, '.vscode', 'extensions'),
|
|
752
|
+
'code-insiders': path.join(HOME, '.vscode-insiders', 'extensions'),
|
|
753
|
+
'cursor': path.join(HOME, '.cursor', 'extensions'),
|
|
754
|
+
'windsurf': path.join(HOME, '.windsurf', 'extensions'),
|
|
755
|
+
};
|
|
756
|
+
const extDir = extDirMap[label];
|
|
757
|
+
if (!extDir || !fs.existsSync(extDir)) return;
|
|
758
|
+
|
|
759
|
+
const keptDir = path.join(extDir, `neunaha.claws-${version}`);
|
|
760
|
+
// Poll up to 1s (5x200ms) for VS Code to extract the new VSIX before cleanup.
|
|
761
|
+
for (let i = 0; i < 5; i++) {
|
|
762
|
+
if (fs.existsSync(keptDir)) break;
|
|
763
|
+
_sleepSync(200);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (!fs.existsSync(keptDir)) {
|
|
767
|
+
_warn(`${label}: new extension dir not yet present — skipping stale cleanup`);
|
|
768
|
+
_info('(VS Code may still be extracting — stale dirs cleaned on next install)');
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
let entries;
|
|
773
|
+
try { entries = fs.readdirSync(extDir); } catch (_) { return; }
|
|
774
|
+
for (const entry of entries) {
|
|
775
|
+
if (!entry.startsWith('neunaha.claws-')) continue;
|
|
776
|
+
const fullPath = path.join(extDir, entry);
|
|
777
|
+
if (fullPath === keptDir) continue;
|
|
778
|
+
if (!fs.statSync(fullPath).isDirectory()) continue;
|
|
779
|
+
try {
|
|
780
|
+
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
781
|
+
_info(` removed stale install ${entry}`);
|
|
782
|
+
} catch (_) {
|
|
783
|
+
// best-effort; ignore EPERM (running extension may hold file locks)
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Detect Electron ABI drift: compare the last-built Electron version recorded
|
|
790
|
+
* in extension/native/.metadata.json against the currently installed editor's
|
|
791
|
+
* Electron version. Warns if they differ (matching install.sh:403-454, W7h-20).
|
|
792
|
+
* Silently skips when detection is not possible (metadata or editor not found).
|
|
793
|
+
* @param {string} extDir - absolute path to extension/
|
|
794
|
+
*/
|
|
795
|
+
function _checkElectronAbiDrift(extDir) {
|
|
796
|
+
const metaPath = path.join(extDir, 'native', '.metadata.json');
|
|
797
|
+
if (!fs.existsSync(metaPath)) return;
|
|
798
|
+
|
|
799
|
+
let lastElec = '';
|
|
800
|
+
try {
|
|
801
|
+
lastElec = JSON.parse(fs.readFileSync(metaPath, 'utf8')).electronVersion || '';
|
|
802
|
+
} catch (_) { return; }
|
|
803
|
+
if (!lastElec) return;
|
|
804
|
+
|
|
805
|
+
const currElec = _detectCurrentElectronVersion();
|
|
806
|
+
if (!currElec) {
|
|
807
|
+
_warn('Could not detect the current editor Electron version — skipping ABI drift check');
|
|
808
|
+
_info('Set CLAWS_ELECTRON_VERSION=<version> to specify it explicitly');
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (currElec !== lastElec) {
|
|
813
|
+
_warn(`Electron ABI drift detected: built for ${lastElec}, editor is ${currElec}`);
|
|
814
|
+
_warn('Run: cd extension && npm run build to rebuild native pty.node for your editor');
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Detect the current VS Code/Cursor/Windsurf Electron version from the app bundle.
|
|
820
|
+
* macOS: reads Info.plist CFBundleVersion from the Electron Framework.
|
|
821
|
+
* Linux: runs the electron binary with --version.
|
|
822
|
+
* Respects CLAWS_ELECTRON_VERSION env override.
|
|
823
|
+
* @returns {string|null}
|
|
824
|
+
*/
|
|
825
|
+
function _detectCurrentElectronVersion() {
|
|
826
|
+
if (process.env.CLAWS_ELECTRON_VERSION) return process.env.CLAWS_ELECTRON_VERSION;
|
|
827
|
+
|
|
828
|
+
const platform = process.platform;
|
|
829
|
+
const term = process.env.TERM_PROGRAM || '';
|
|
830
|
+
|
|
831
|
+
if (platform === 'darwin') {
|
|
832
|
+
const appOrder = (() => {
|
|
833
|
+
if (term === 'cursor') return ['Cursor', 'Visual Studio Code', 'Visual Studio Code - Insiders', 'Windsurf'];
|
|
834
|
+
if (term === 'windsurf') return ['Windsurf', 'Visual Studio Code', 'Visual Studio Code - Insiders', 'Cursor'];
|
|
835
|
+
return ['Visual Studio Code', 'Visual Studio Code - Insiders', 'Cursor', 'Windsurf'];
|
|
836
|
+
})();
|
|
837
|
+
for (const app of appOrder) {
|
|
838
|
+
const plist = `/Applications/${app}.app/Contents/Frameworks/Electron Framework.framework/Resources/Info.plist`;
|
|
839
|
+
if (fs.existsSync(plist)) {
|
|
840
|
+
const r = spawnSync('plutil', ['-extract', 'CFBundleVersion', 'raw', plist],
|
|
841
|
+
{ encoding: 'utf8', stdio: 'pipe' });
|
|
842
|
+
if (r.status === 0 && r.stdout && r.stdout.trim()) return r.stdout.trim();
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
} else if (platform === 'linux') {
|
|
846
|
+
const epOrder = (() => {
|
|
847
|
+
if (term === 'cursor') return ['/usr/share/cursor/electron', '/opt/cursor/electron', '/usr/share/code/electron', '/usr/share/windsurf/electron'];
|
|
848
|
+
if (term === 'windsurf') return ['/usr/share/windsurf/electron', '/opt/windsurf/electron', '/usr/share/code/electron', '/usr/share/cursor/electron'];
|
|
849
|
+
return ['/usr/share/code/electron', '/usr/lib/code/electron', '/usr/share/cursor/electron', '/usr/share/windsurf/electron'];
|
|
850
|
+
})();
|
|
851
|
+
for (const ep of epOrder) {
|
|
852
|
+
if (fs.existsSync(ep)) {
|
|
853
|
+
const r = spawnSync(ep, ['--version'], { encoding: 'utf8', stdio: 'pipe' });
|
|
854
|
+
if (r.status === 0 && r.stdout) {
|
|
855
|
+
const v = r.stdout.trim().replace(/^v/, '').split('\n')[0];
|
|
856
|
+
if (v) return v;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Copy extension/ artifacts into .claws-bin/extension/ for visibility (W7h-13).
|
|
867
|
+
* Copies: dist, native, package.json, package-lock.json, README.md, CHANGELOG.md,
|
|
868
|
+
* icon.png, .vscodeignore — matches install.sh lines 975-979.
|
|
869
|
+
* @param {string} projectRoot
|
|
870
|
+
* @param {boolean} [dryRun]
|
|
871
|
+
*/
|
|
872
|
+
function _copyExtensionArtifacts(projectRoot, dryRun = false) {
|
|
873
|
+
const srcExtDir = path.join(REPO_ROOT, 'extension');
|
|
874
|
+
const destExtDir = path.join(projectRoot, '.claws-bin', 'extension');
|
|
875
|
+
|
|
876
|
+
const ENTRIES = [
|
|
877
|
+
'dist', 'native', 'package.json', 'package-lock.json',
|
|
878
|
+
'README.md', 'CHANGELOG.md', 'icon.png', '.vscodeignore',
|
|
879
|
+
];
|
|
880
|
+
|
|
881
|
+
if (dryRun) {
|
|
882
|
+
for (const entry of ENTRIES) {
|
|
883
|
+
dryRunLog(`copy extension/${entry} → .claws-bin/extension/${entry}`);
|
|
884
|
+
}
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (!fs.existsSync(srcExtDir)) return;
|
|
889
|
+
|
|
890
|
+
try {
|
|
891
|
+
fs.rmSync(destExtDir, { recursive: true, force: true });
|
|
892
|
+
fs.mkdirSync(destExtDir, { recursive: true });
|
|
893
|
+
} catch (_) {}
|
|
894
|
+
|
|
895
|
+
for (const entry of ENTRIES) {
|
|
896
|
+
const src = path.join(srcExtDir, entry);
|
|
897
|
+
if (!fs.existsSync(src)) continue;
|
|
898
|
+
const dest = path.join(destExtDir, entry);
|
|
899
|
+
try {
|
|
900
|
+
fs.cpSync(src, dest, { recursive: true });
|
|
901
|
+
} catch (_) {}
|
|
902
|
+
}
|
|
903
|
+
_ok('extension/ artifacts copied → .claws-bin/extension/');
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Copy srcDir to destDir atomically via tmp → rename (M-09 pattern).
|
|
908
|
+
* 1. Copy srcDir → destDir.claws-tmp.<pid>
|
|
909
|
+
* 2. Move existing destDir aside → destDir.claws-old.<ts>
|
|
910
|
+
* 3. Rename tmp → destDir
|
|
911
|
+
* 4. Remove the moved-aside old dir (best-effort)
|
|
912
|
+
* If killed before step 3, destDir is left untouched.
|
|
913
|
+
* @param {string} srcDir
|
|
914
|
+
* @param {string} destDir
|
|
915
|
+
*/
|
|
916
|
+
function _copyDirAtomic(srcDir, destDir) {
|
|
917
|
+
const ts = Date.now();
|
|
918
|
+
const tmp = destDir + '.claws-tmp.' + process.pid;
|
|
919
|
+
const old = destDir + '.claws-old.' + ts;
|
|
920
|
+
|
|
921
|
+
try {
|
|
922
|
+
if (fs.existsSync(tmp)) fs.rmSync(tmp, { recursive: true, force: true });
|
|
923
|
+
_copyDirSync(srcDir, tmp);
|
|
924
|
+
} catch (err) {
|
|
925
|
+
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch (_) {}
|
|
926
|
+
throw err;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const destExists = fs.existsSync(destDir);
|
|
930
|
+
try {
|
|
931
|
+
if (destExists) fs.renameSync(destDir, old);
|
|
932
|
+
fs.renameSync(tmp, destDir);
|
|
933
|
+
} catch (err) {
|
|
934
|
+
if (destExists) {
|
|
935
|
+
try {
|
|
936
|
+
if (!fs.existsSync(destDir)) fs.renameSync(old, destDir);
|
|
937
|
+
} catch (_) {}
|
|
938
|
+
}
|
|
939
|
+
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch (_) {}
|
|
940
|
+
throw err;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (destExists) {
|
|
944
|
+
try { fs.rmSync(old, { recursive: true, force: true }); } catch (_) {}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function _copyDirSync(src, dest) {
|
|
949
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
950
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
951
|
+
const s = path.join(src, entry.name);
|
|
952
|
+
const d = path.join(dest, entry.name);
|
|
953
|
+
if (entry.isDirectory()) _copyDirSync(s, d);
|
|
954
|
+
else fs.copyFileSync(s, d);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Install dev-discipline hooks for contributor environments (W7h-31).
|
|
960
|
+
* Copies scripts/dev-hooks/*.js into <project>/.claws-bin/dev-hooks/ and
|
|
961
|
+
* runs inject-dev-hooks.js to register them in <project>/.claude/settings.json.
|
|
962
|
+
* Matches install.sh:1301-1332.
|
|
963
|
+
* @param {string} projectRoot
|
|
964
|
+
*/
|
|
965
|
+
function _installDevHooks(projectRoot) {
|
|
966
|
+
const srcDir = path.join(REPO_ROOT, 'scripts', 'dev-hooks');
|
|
967
|
+
const destDir = path.join(projectRoot, '.claws-bin', 'dev-hooks');
|
|
968
|
+
const injectScript = path.join(REPO_ROOT, 'scripts', 'inject-dev-hooks.js');
|
|
969
|
+
|
|
970
|
+
if (!fs.existsSync(srcDir)) return;
|
|
971
|
+
|
|
972
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
973
|
+
|
|
974
|
+
let count = 0;
|
|
975
|
+
for (const f of fs.readdirSync(srcDir)) {
|
|
976
|
+
if (!f.endsWith('.js')) continue;
|
|
977
|
+
fs.copyFileSync(path.join(srcDir, f), path.join(destDir, f));
|
|
978
|
+
count++;
|
|
979
|
+
}
|
|
980
|
+
_ok(`copied ${count} dev-hook scripts → ${destDir}`);
|
|
981
|
+
|
|
982
|
+
if (fs.existsSync(injectScript)) {
|
|
983
|
+
const r = spawnSync(process.execPath, [injectScript, projectRoot],
|
|
984
|
+
{ cwd: REPO_ROOT, stdio: 'inherit', encoding: 'utf8' });
|
|
985
|
+
if (r.status !== 0) {
|
|
986
|
+
_warn('inject-dev-hooks.js failed — dev hooks not registered');
|
|
987
|
+
}
|
|
988
|
+
} else {
|
|
989
|
+
_warn('inject-dev-hooks.js not found — dev hooks not registered');
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function _printBanner() {
|
|
994
|
+
// On Windows, install.ps1 already printed the ASCII banner before delegating to node.
|
|
995
|
+
// Node's Unicode box-drawing chars garble in CP437/CP1252 consoles, so skip here.
|
|
996
|
+
if (process.platform === 'win32') return;
|
|
997
|
+
const B = '\x1b[38;2;200;90;62m';
|
|
998
|
+
const R = '\x1b[0m';
|
|
999
|
+
process.stdout.write('\n');
|
|
1000
|
+
process.stdout.write(` ${B}╔═══════════════════════════════════════════╗${R}\n`);
|
|
1001
|
+
process.stdout.write(` ${B}║${R} ${B}║${R}\n`);
|
|
1002
|
+
process.stdout.write(` ${B}║${R} \x1b[1mCLAWS\x1b[0m Terminal Control Bridge ${B}║${R}\n`);
|
|
1003
|
+
process.stdout.write(` ${B}║${R} \x1b[2mProject-local orchestration setup\x1b[0m ${B}║${R}\n`);
|
|
1004
|
+
process.stdout.write(` ${B}║${R} ${B}║${R}\n`);
|
|
1005
|
+
process.stdout.write(` ${B}╚═══════════════════════════════════════════╝${R}\n`);
|
|
1006
|
+
process.stdout.write('\n');
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function _printSuccess() {
|
|
1010
|
+
const kbd = process.platform === 'darwin' ? 'Cmd+Shift+P' : 'Ctrl+Shift+P';
|
|
1011
|
+
process.stdout.write('\n \x1b[32m✓ Claws installed successfully\x1b[0m\n\n');
|
|
1012
|
+
process.stdout.write(' \x1b[1mNext steps:\x1b[0m\n');
|
|
1013
|
+
process.stdout.write(` Reload VS Code: ${kbd} → Developer: Reload Window\n`);
|
|
1014
|
+
process.stdout.write(' Then: /claws-help\n');
|
|
1015
|
+
process.stdout.write('\n');
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
module.exports = { run, _injectHooks, installGlobalHooks };
|