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
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { SubWorkerRole } from './protocol';
|
|
2
|
+
|
|
3
|
+
export interface WaveSubWorkerEntry {
|
|
4
|
+
role: SubWorkerRole;
|
|
5
|
+
/** peerId of the sub-worker peer, set on hello with waveId+subWorkerRole. */
|
|
6
|
+
peerId?: string;
|
|
7
|
+
/** Epoch ms of last observed heartbeat from this sub-worker. */
|
|
8
|
+
lastHeartbeatMs: number;
|
|
9
|
+
/** Whether this sub-worker has published its *.complete event. */
|
|
10
|
+
complete: boolean;
|
|
11
|
+
/** NodeJS timer handle for the violation detector. */
|
|
12
|
+
violationTimer?: ReturnType<typeof setTimeout>;
|
|
13
|
+
/** Terminal ID created by this sub-worker (set by server create handler). */
|
|
14
|
+
terminalId?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface WaveRecord {
|
|
18
|
+
waveId: string;
|
|
19
|
+
/** Human-readable layers or goals this wave covers. */
|
|
20
|
+
layers: string[];
|
|
21
|
+
/** Expected sub-worker roles, tracking entry keyed by role. */
|
|
22
|
+
subWorkers: Map<SubWorkerRole, WaveSubWorkerEntry>;
|
|
23
|
+
/** peerId of the LEAD peer that created this wave. */
|
|
24
|
+
leadPeerId: string;
|
|
25
|
+
createdAt: number;
|
|
26
|
+
completedAt?: number;
|
|
27
|
+
summary?: string;
|
|
28
|
+
commits?: string[];
|
|
29
|
+
regressionClean?: boolean;
|
|
30
|
+
complete: boolean;
|
|
31
|
+
/** Optional parent wave ID when this wave was spawned by another wave's LEAD. */
|
|
32
|
+
parentWave?: string;
|
|
33
|
+
/** Terminal IDs spawned by sub-worker peers affiliated with this wave. */
|
|
34
|
+
subWorkerTerminals: string[];
|
|
35
|
+
/** Epoch ms when auto-harvest ran (wave.complete triggered terminal closures). */
|
|
36
|
+
harvestedAt?: number;
|
|
37
|
+
/** Terminal IDs that were still open at harvest time and were force-closed. */
|
|
38
|
+
orphanedTerminals: string[];
|
|
39
|
+
/** Lead-silence violation timer. Fires when LEAD is silent AND has active sub-worker terminals. */
|
|
40
|
+
leadViolationTimer?: ReturnType<typeof setTimeout>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Callback invoked by WaveRegistry when a sub-worker has been silent
|
|
45
|
+
* longer than VIOLATION_THRESHOLD_MS. The emitter should publish a
|
|
46
|
+
* wave.<waveId>.violation event on the bus.
|
|
47
|
+
*/
|
|
48
|
+
export type ViolationCallback = (waveId: string, role: SubWorkerRole, silentMs: number) => void;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Callback invoked when a LEAD has been silent for VIOLATION_THRESHOLD_MS
|
|
52
|
+
* while the wave still has un-harvested sub-worker terminals.
|
|
53
|
+
*/
|
|
54
|
+
export type LeadViolationCallback = (waveId: string, subWorkerCount: number) => void;
|
|
55
|
+
|
|
56
|
+
const DEFAULT_VIOLATION_THRESHOLD_MS = 25_000;
|
|
57
|
+
|
|
58
|
+
export class WaveRegistry {
|
|
59
|
+
private readonly waves = new Map<string, WaveRecord>();
|
|
60
|
+
private readonly onViolation: ViolationCallback;
|
|
61
|
+
private readonly onLeadViolation?: LeadViolationCallback;
|
|
62
|
+
private readonly violationThresholdMs: number;
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
onViolation: ViolationCallback,
|
|
66
|
+
onLeadViolation?: LeadViolationCallback,
|
|
67
|
+
violationThresholdMs: number = DEFAULT_VIOLATION_THRESHOLD_MS,
|
|
68
|
+
) {
|
|
69
|
+
this.onViolation = onViolation;
|
|
70
|
+
this.onLeadViolation = onLeadViolation;
|
|
71
|
+
this.violationThresholdMs = violationThresholdMs;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Create a new wave. Idempotent — returns existing wave if waveId already registered. */
|
|
75
|
+
createWave(
|
|
76
|
+
waveId: string,
|
|
77
|
+
layers: string[],
|
|
78
|
+
manifest: SubWorkerRole[],
|
|
79
|
+
leadPeerId: string,
|
|
80
|
+
parentWave?: string,
|
|
81
|
+
): WaveRecord {
|
|
82
|
+
const existing = this.waves.get(waveId);
|
|
83
|
+
if (existing) return existing;
|
|
84
|
+
|
|
85
|
+
const subWorkers = new Map<SubWorkerRole, WaveSubWorkerEntry>();
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
for (const role of manifest) {
|
|
88
|
+
const entry: WaveSubWorkerEntry = { role, lastHeartbeatMs: now, complete: false };
|
|
89
|
+
const timer = setTimeout(() => {
|
|
90
|
+
this._checkViolation(waveId, role);
|
|
91
|
+
}, this.violationThresholdMs);
|
|
92
|
+
entry.violationTimer = timer;
|
|
93
|
+
subWorkers.set(role, entry);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const record: WaveRecord = {
|
|
97
|
+
waveId,
|
|
98
|
+
layers,
|
|
99
|
+
subWorkers,
|
|
100
|
+
leadPeerId,
|
|
101
|
+
createdAt: now,
|
|
102
|
+
complete: false,
|
|
103
|
+
parentWave,
|
|
104
|
+
subWorkerTerminals: [],
|
|
105
|
+
orphanedTerminals: [],
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (this.onLeadViolation) {
|
|
109
|
+
record.leadViolationTimer = this._scheduleLeadViolation(waveId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.waves.set(waveId, record);
|
|
113
|
+
return record;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Record a heartbeat from a sub-worker, resetting its violation timer. */
|
|
117
|
+
recordHeartbeat(waveId: string, role: SubWorkerRole, peerId?: string): void {
|
|
118
|
+
const wave = this.waves.get(waveId);
|
|
119
|
+
if (!wave) return;
|
|
120
|
+
const entry = wave.subWorkers.get(role);
|
|
121
|
+
if (!entry) return;
|
|
122
|
+
|
|
123
|
+
entry.lastHeartbeatMs = Date.now();
|
|
124
|
+
if (peerId) entry.peerId = peerId;
|
|
125
|
+
|
|
126
|
+
if (entry.violationTimer !== undefined) clearTimeout(entry.violationTimer);
|
|
127
|
+
if (!entry.complete && !wave.complete) {
|
|
128
|
+
entry.violationTimer = setTimeout(() => {
|
|
129
|
+
this._checkViolation(waveId, role);
|
|
130
|
+
}, this.violationThresholdMs);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Record a heartbeat from the LEAD peer, resetting the lead-violation timer.
|
|
136
|
+
* Called by the server whenever the LEAD peer issues any command.
|
|
137
|
+
*/
|
|
138
|
+
recordLeadHeartbeat(waveId: string): void {
|
|
139
|
+
const wave = this.waves.get(waveId);
|
|
140
|
+
if (!wave || wave.complete) return;
|
|
141
|
+
if (wave.leadViolationTimer !== undefined) clearTimeout(wave.leadViolationTimer);
|
|
142
|
+
if (this.onLeadViolation) {
|
|
143
|
+
wave.leadViolationTimer = this._scheduleLeadViolation(waveId);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Mark a sub-worker as complete and cancel its violation timer. */
|
|
148
|
+
markSubWorkerComplete(waveId: string, role: SubWorkerRole): void {
|
|
149
|
+
const wave = this.waves.get(waveId);
|
|
150
|
+
if (!wave) return;
|
|
151
|
+
const entry = wave.subWorkers.get(role);
|
|
152
|
+
if (!entry) return;
|
|
153
|
+
|
|
154
|
+
entry.complete = true;
|
|
155
|
+
if (entry.violationTimer !== undefined) {
|
|
156
|
+
clearTimeout(entry.violationTimer);
|
|
157
|
+
entry.violationTimer = undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Used by the violation callback to silence further violations once the sub-worker is auto-closed.
|
|
163
|
+
* Idempotent — safe to call twice.
|
|
164
|
+
*/
|
|
165
|
+
markSubWorkerAutoClosed(waveId: string, role: SubWorkerRole): { terminalId: string | undefined; found: boolean } {
|
|
166
|
+
const wave = this.waves.get(waveId);
|
|
167
|
+
if (!wave) return { terminalId: undefined, found: false };
|
|
168
|
+
const entry = wave.subWorkers.get(role);
|
|
169
|
+
if (!entry) return { terminalId: undefined, found: false };
|
|
170
|
+
if (entry.violationTimer !== undefined) {
|
|
171
|
+
clearTimeout(entry.violationTimer);
|
|
172
|
+
entry.violationTimer = undefined;
|
|
173
|
+
}
|
|
174
|
+
entry.complete = true;
|
|
175
|
+
// Prune terminal from subWorkerTerminals so _checkLeadViolation no longer
|
|
176
|
+
// counts this auto-closed terminal as "active". Without this, lead-silence
|
|
177
|
+
// violations keep firing every threshold cycle even after the sub-worker
|
|
178
|
+
// is dead. Verified live in Phase 4 of LH-1 verification (events 7-9 noise).
|
|
179
|
+
if (entry.terminalId) {
|
|
180
|
+
const idx = wave.subWorkerTerminals.indexOf(entry.terminalId);
|
|
181
|
+
if (idx >= 0) wave.subWorkerTerminals.splice(idx, 1);
|
|
182
|
+
}
|
|
183
|
+
return { terminalId: entry.terminalId, found: true };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Track a terminal ID spawned by a sub-worker peer affiliated with this wave.
|
|
188
|
+
* Optionally associates the TID with a specific sub-worker role entry.
|
|
189
|
+
* Silently ignored if wave doesn't exist or is already complete.
|
|
190
|
+
*/
|
|
191
|
+
trackTerminal(waveId: string, terminalId: string, role?: SubWorkerRole): void {
|
|
192
|
+
const wave = this.waves.get(waveId);
|
|
193
|
+
if (!wave || wave.complete) return;
|
|
194
|
+
if (!wave.subWorkerTerminals.includes(terminalId)) {
|
|
195
|
+
wave.subWorkerTerminals.push(terminalId);
|
|
196
|
+
}
|
|
197
|
+
if (role) {
|
|
198
|
+
const entry = wave.subWorkers.get(role);
|
|
199
|
+
if (entry && !entry.terminalId) entry.terminalId = terminalId;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Harvest a wave: collect all un-closed sub-worker terminals, mark them as
|
|
205
|
+
* orphaned, set harvestedAt. Returns the terminal ID list so the server can
|
|
206
|
+
* call terminalManager.close() on each. Only valid once per wave.
|
|
207
|
+
*/
|
|
208
|
+
harvestWave(waveId: string): string[] {
|
|
209
|
+
const wave = this.waves.get(waveId);
|
|
210
|
+
if (!wave || wave.harvestedAt) return [];
|
|
211
|
+
wave.harvestedAt = Date.now();
|
|
212
|
+
wave.orphanedTerminals = [...wave.subWorkerTerminals];
|
|
213
|
+
return wave.orphanedTerminals;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Mark the wave as complete. Clears all sub-worker violation timers and the
|
|
218
|
+
* lead violation timer. Returns the wave record (including subWorkerTerminals
|
|
219
|
+
* for harvest) or null if the wave was already complete.
|
|
220
|
+
* Only the LEAD should call this.
|
|
221
|
+
*/
|
|
222
|
+
completeWave(
|
|
223
|
+
waveId: string,
|
|
224
|
+
summary: string,
|
|
225
|
+
commits?: string[],
|
|
226
|
+
regressionClean?: boolean,
|
|
227
|
+
): WaveRecord | null {
|
|
228
|
+
const wave = this.waves.get(waveId);
|
|
229
|
+
if (!wave || wave.complete) return null;
|
|
230
|
+
|
|
231
|
+
for (const entry of wave.subWorkers.values()) {
|
|
232
|
+
if (entry.violationTimer !== undefined) {
|
|
233
|
+
clearTimeout(entry.violationTimer);
|
|
234
|
+
entry.violationTimer = undefined;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (wave.leadViolationTimer !== undefined) {
|
|
238
|
+
clearTimeout(wave.leadViolationTimer);
|
|
239
|
+
wave.leadViolationTimer = undefined;
|
|
240
|
+
}
|
|
241
|
+
wave.complete = true;
|
|
242
|
+
wave.completedAt = Date.now();
|
|
243
|
+
wave.summary = summary;
|
|
244
|
+
wave.commits = commits;
|
|
245
|
+
wave.regressionClean = regressionClean;
|
|
246
|
+
return wave;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Get a snapshot of a wave. Returns null if wave does not exist. */
|
|
250
|
+
getWave(waveId: string): WaveRecord | null {
|
|
251
|
+
return this.waves.get(waveId) ?? null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** List all waves (shallow copy of records). */
|
|
255
|
+
listWaves(): WaveRecord[] {
|
|
256
|
+
return [...this.waves.values()];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Handle peer disconnect — clear violation timers for any sub-worker registered under that peerId. */
|
|
260
|
+
handlePeerDisconnect(peerId: string): void {
|
|
261
|
+
for (const wave of this.waves.values()) {
|
|
262
|
+
if (wave.complete) continue;
|
|
263
|
+
for (const entry of wave.subWorkers.values()) {
|
|
264
|
+
if (entry.peerId === peerId && !entry.complete) {
|
|
265
|
+
if (entry.violationTimer !== undefined) clearTimeout(entry.violationTimer);
|
|
266
|
+
entry.violationTimer = undefined;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Dispose — clear all timers. */
|
|
273
|
+
dispose(): void {
|
|
274
|
+
for (const wave of this.waves.values()) {
|
|
275
|
+
for (const entry of wave.subWorkers.values()) {
|
|
276
|
+
if (entry.violationTimer !== undefined) clearTimeout(entry.violationTimer);
|
|
277
|
+
}
|
|
278
|
+
if (wave.leadViolationTimer !== undefined) clearTimeout(wave.leadViolationTimer);
|
|
279
|
+
}
|
|
280
|
+
this.waves.clear();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private _scheduleLeadViolation(waveId: string): ReturnType<typeof setTimeout> {
|
|
284
|
+
return setTimeout(() => {
|
|
285
|
+
this._checkLeadViolation(waveId);
|
|
286
|
+
}, this.violationThresholdMs);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private _checkLeadViolation(waveId: string): void {
|
|
290
|
+
const wave = this.waves.get(waveId);
|
|
291
|
+
if (!wave || wave.complete || wave.harvestedAt) return;
|
|
292
|
+
const activeCount = wave.subWorkerTerminals.length;
|
|
293
|
+
if (activeCount > 0 && this.onLeadViolation) {
|
|
294
|
+
this.onLeadViolation(waveId, activeCount);
|
|
295
|
+
// Reschedule so violations keep firing until resolved.
|
|
296
|
+
wave.leadViolationTimer = this._scheduleLeadViolation(waveId);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private _checkViolation(waveId: string, role: SubWorkerRole): void {
|
|
301
|
+
const wave = this.waves.get(waveId);
|
|
302
|
+
if (!wave || wave.complete) return;
|
|
303
|
+
const entry = wave.subWorkers.get(role);
|
|
304
|
+
if (!entry || entry.complete) return;
|
|
305
|
+
|
|
306
|
+
const silentMs = Date.now() - entry.lastHeartbeatMs;
|
|
307
|
+
if (silentMs >= this.violationThresholdMs) {
|
|
308
|
+
this.onViolation(waveId, role, silentMs);
|
|
309
|
+
if (!entry.complete) {
|
|
310
|
+
entry.violationTimer = setTimeout(() => this._checkViolation(waveId, role), this.violationThresholdMs);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L19 TRANSPORT-X — WebSocket server that adapts the ws:// protocol to the
|
|
3
|
+
* same newline-delimited JSON frame contract used by the Unix socket transport.
|
|
4
|
+
*
|
|
5
|
+
* Each WebSocket message = one JSON frame (no newline needed; WebSocket has
|
|
6
|
+
* its own message boundaries). The adapter wraps each WebSocket connection
|
|
7
|
+
* in a thin net.Socket-compatible shim so `ClawsServer.handleConnection()`
|
|
8
|
+
* can be reused without modification.
|
|
9
|
+
*
|
|
10
|
+
* Loaded lazily — require('ws') is only called when webSocket.enabled=true,
|
|
11
|
+
* so the extension imposes no load-time cost when WebSocket is disabled.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as net from 'net';
|
|
15
|
+
import * as http from 'http';
|
|
16
|
+
import * as https from 'https';
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import { EventEmitter } from 'events';
|
|
19
|
+
|
|
20
|
+
/** Minimal subset of ws.WebSocket we use — avoids a hard import at the top level. */
|
|
21
|
+
interface WsSocket extends EventEmitter {
|
|
22
|
+
send(data: string): void;
|
|
23
|
+
terminate(): void;
|
|
24
|
+
readyState: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Minimal subset of ws.WebSocketServer options we use. */
|
|
28
|
+
interface WsServerOptions {
|
|
29
|
+
server: http.Server | https.Server;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Minimal subset of ws.WebSocketServer we use. */
|
|
33
|
+
interface WsServer extends EventEmitter {
|
|
34
|
+
close(cb?: () => void): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** WsSocketAdapter wraps a ws.WebSocket in a net.Socket-like interface. */
|
|
38
|
+
class WsSocketAdapter extends EventEmitter {
|
|
39
|
+
private _destroyed = false;
|
|
40
|
+
|
|
41
|
+
constructor(private readonly ws: WsSocket) {
|
|
42
|
+
super();
|
|
43
|
+
ws.on('message', (data: Buffer | string) => {
|
|
44
|
+
// Each WebSocket message is one complete JSON frame; append \n so the
|
|
45
|
+
// server's line-buffering logic sees a complete line.
|
|
46
|
+
const str = typeof data === 'string' ? data : data.toString('utf8');
|
|
47
|
+
this.emit('data', Buffer.from(str + '\n'));
|
|
48
|
+
});
|
|
49
|
+
ws.on('close', () => {
|
|
50
|
+
if (!this._destroyed) {
|
|
51
|
+
this._destroyed = true;
|
|
52
|
+
this.emit('end');
|
|
53
|
+
this.emit('close');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
ws.on('error', (err: Error) => {
|
|
57
|
+
this.emit('error', err);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
write(data: string | Buffer): boolean {
|
|
62
|
+
if (this._destroyed) return false;
|
|
63
|
+
const str = typeof data === 'string' ? data : data.toString('utf8');
|
|
64
|
+
// Strip the trailing \n that the server appends — WebSocket framing
|
|
65
|
+
// handles message boundaries without it.
|
|
66
|
+
const frame = str.endsWith('\n') ? str.slice(0, -1) : str;
|
|
67
|
+
try {
|
|
68
|
+
this.ws.send(frame);
|
|
69
|
+
} catch { /* connection may have closed between check and send */ }
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
destroy(): void {
|
|
74
|
+
if (this._destroyed) return;
|
|
75
|
+
this._destroyed = true;
|
|
76
|
+
try { this.ws.terminate(); } catch { /* ignore */ }
|
|
77
|
+
this.emit('close');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get destroyed(): boolean { return this._destroyed; }
|
|
81
|
+
|
|
82
|
+
/** Compatibility stub — WebSocket has its own backpressure, always report no pressure. */
|
|
83
|
+
get writableLength(): number { return 0; }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface WebSocketTransportOptions {
|
|
87
|
+
port: number;
|
|
88
|
+
certPath?: string;
|
|
89
|
+
keyPath?: string;
|
|
90
|
+
logger: (msg: string) => void;
|
|
91
|
+
/** Called for each new WebSocket connection with the adapted socket shim. */
|
|
92
|
+
onConnection: (socket: net.Socket) => void;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class WebSocketTransport {
|
|
96
|
+
private wsServer: WsServer | null = null;
|
|
97
|
+
private httpServer: http.Server | https.Server | null = null;
|
|
98
|
+
|
|
99
|
+
start(opts: WebSocketTransportOptions): Promise<void> {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
102
|
+
let WS: new (opts: WsServerOptions) => WsServer;
|
|
103
|
+
try {
|
|
104
|
+
const wsModule = require('ws') as { WebSocketServer: typeof WS };
|
|
105
|
+
WS = wsModule.WebSocketServer;
|
|
106
|
+
} catch {
|
|
107
|
+
return reject(new Error(
|
|
108
|
+
'ws module not available — install it as an optional dependency: npm install ws',
|
|
109
|
+
));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const useTls = Boolean(opts.certPath && opts.keyPath);
|
|
113
|
+
if (useTls) {
|
|
114
|
+
let cert: Buffer;
|
|
115
|
+
let key: Buffer;
|
|
116
|
+
try {
|
|
117
|
+
cert = fs.readFileSync(opts.certPath!);
|
|
118
|
+
key = fs.readFileSync(opts.keyPath!);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
return reject(new Error(`WebSocket TLS: failed to read cert/key — ${(err as Error).message}`));
|
|
121
|
+
}
|
|
122
|
+
this.httpServer = https.createServer({ cert, key });
|
|
123
|
+
} else {
|
|
124
|
+
this.httpServer = http.createServer();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.wsServer = new WS({ server: this.httpServer });
|
|
128
|
+
|
|
129
|
+
this.wsServer.on('connection', (ws: WsSocket) => {
|
|
130
|
+
const adapter = new WsSocketAdapter(ws);
|
|
131
|
+
// Cast: our adapter satisfies the interface ClawsServer.handleConnection expects
|
|
132
|
+
opts.onConnection(adapter as unknown as net.Socket);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
this.httpServer.once('error', (err) => {
|
|
136
|
+
reject(err);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
this.httpServer.listen(opts.port, '127.0.0.1', () => {
|
|
140
|
+
const proto = useTls ? 'wss' : 'ws';
|
|
141
|
+
opts.logger(`[claws/ws] listening on ${proto}://127.0.0.1:${opts.port}`);
|
|
142
|
+
resolve();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
stop(): void {
|
|
148
|
+
try { this.wsServer?.close(); } catch { /* ignore */ }
|
|
149
|
+
try { this.httpServer?.close(); } catch { /* ignore */ }
|
|
150
|
+
this.wsServer = null;
|
|
151
|
+
this.httpServer = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"outDir": "out",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noImplicitAny": true,
|
|
10
|
+
"noImplicitReturns": true,
|
|
11
|
+
"noFallthroughCasesInSwitch": true,
|
|
12
|
+
"noUnusedLocals": true,
|
|
13
|
+
"noUnusedParameters": false,
|
|
14
|
+
"esModuleInterop": true,
|
|
15
|
+
"skipLibCheck": true,
|
|
16
|
+
"forceConsistentCasingInFileNames": true,
|
|
17
|
+
"resolveJsonModule": true,
|
|
18
|
+
"sourceMap": true,
|
|
19
|
+
"declaration": false
|
|
20
|
+
},
|
|
21
|
+
"include": ["src/**/*.ts"],
|
|
22
|
+
"exclude": ["node_modules", "out", "dist", "test"]
|
|
23
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { dryRunLog } = require('./platform.js');
|
|
6
|
+
|
|
7
|
+
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
8
|
+
const COMMANDS_SRC = path.join(REPO_ROOT, '.claude', 'commands');
|
|
9
|
+
const SKILLS_SRC = path.join(REPO_ROOT, '.claude', 'skills');
|
|
10
|
+
const RULES_SRC = path.join(REPO_ROOT, 'rules');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Install all capabilities (commands + skills + rules) into targetRoot/.claude/.
|
|
14
|
+
* Runs Bug 1 + Bug 2 sweeps before copying.
|
|
15
|
+
* @param {string} targetRoot - e.g. os.homedir() for global install
|
|
16
|
+
* @param {boolean} [dryRun]
|
|
17
|
+
*/
|
|
18
|
+
function installCapabilities(targetRoot, dryRun = false) {
|
|
19
|
+
installCommands(targetRoot, dryRun);
|
|
20
|
+
installSkills(targetRoot, dryRun);
|
|
21
|
+
installRules(targetRoot, dryRun);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Bug 1 sweep + copy claws-*.md commands into targetRoot/.claude/commands/.
|
|
26
|
+
* @param {string} targetRoot
|
|
27
|
+
* @param {boolean} [dryRun]
|
|
28
|
+
*/
|
|
29
|
+
function installCommands(targetRoot, dryRun = false) {
|
|
30
|
+
const cmdDir = path.join(targetRoot, '.claude', 'commands');
|
|
31
|
+
|
|
32
|
+
if (dryRun) {
|
|
33
|
+
dryRunLog(`mkdir ${cmdDir}`);
|
|
34
|
+
} else {
|
|
35
|
+
fs.mkdirSync(cmdDir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
sweepCommands(cmdDir, dryRun);
|
|
39
|
+
|
|
40
|
+
if (!fs.existsSync(COMMANDS_SRC)) return;
|
|
41
|
+
const files = fs.readdirSync(COMMANDS_SRC).filter(
|
|
42
|
+
f => f === 'claws.md' || (f.startsWith('claws-') && f.endsWith('.md'))
|
|
43
|
+
);
|
|
44
|
+
for (const f of files) {
|
|
45
|
+
const src = path.join(COMMANDS_SRC, f);
|
|
46
|
+
const dest = path.join(cmdDir, f);
|
|
47
|
+
if (dryRun) { dryRunLog(`copy ${src} → ${dest}`); continue; }
|
|
48
|
+
fs.copyFileSync(src, dest);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Bug 2 sweep + copy claws-* skill dirs into targetRoot/.claude/skills/.
|
|
54
|
+
* @param {string} targetRoot
|
|
55
|
+
* @param {boolean} [dryRun]
|
|
56
|
+
*/
|
|
57
|
+
function installSkills(targetRoot, dryRun = false) {
|
|
58
|
+
const skillsDir = path.join(targetRoot, '.claude', 'skills');
|
|
59
|
+
|
|
60
|
+
if (dryRun) {
|
|
61
|
+
dryRunLog(`mkdir ${skillsDir}`);
|
|
62
|
+
} else {
|
|
63
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
sweepSkills(skillsDir, dryRun);
|
|
67
|
+
|
|
68
|
+
if (!fs.existsSync(SKILLS_SRC)) return;
|
|
69
|
+
const dirs = fs.readdirSync(SKILLS_SRC).filter(
|
|
70
|
+
d => (d.startsWith('claws-') || d.startsWith('dev-protocol-')) &&
|
|
71
|
+
fs.statSync(path.join(SKILLS_SRC, d)).isDirectory()
|
|
72
|
+
);
|
|
73
|
+
for (const d of dirs) {
|
|
74
|
+
const src = path.join(SKILLS_SRC, d);
|
|
75
|
+
const dest = path.join(skillsDir, d);
|
|
76
|
+
if (path.resolve(src) === path.resolve(dest)) continue; // self-collision guard
|
|
77
|
+
if (dryRun) { dryRunLog(`copy ${src}/ → ${dest}/`); continue; }
|
|
78
|
+
fs.cpSync(src, dest, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Copy claws-default-behavior.md rule into targetRoot/.claude/rules/.
|
|
84
|
+
* @param {string} targetRoot
|
|
85
|
+
* @param {boolean} [dryRun]
|
|
86
|
+
*/
|
|
87
|
+
function installRules(targetRoot, dryRun = false) {
|
|
88
|
+
const rulesDir = path.join(targetRoot, '.claude', 'rules');
|
|
89
|
+
const ruleSrc = path.join(RULES_SRC, 'claws-default-behavior.md');
|
|
90
|
+
|
|
91
|
+
if (!fs.existsSync(ruleSrc)) return;
|
|
92
|
+
|
|
93
|
+
if (dryRun) {
|
|
94
|
+
dryRunLog(`mkdir ${rulesDir}`);
|
|
95
|
+
dryRunLog(`copy ${ruleSrc} → ${path.join(rulesDir, 'claws-default-behavior.md')}`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fs.mkdirSync(rulesDir, { recursive: true });
|
|
100
|
+
fs.copyFileSync(ruleSrc, path.join(rulesDir, 'claws-default-behavior.md'));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Bug 1: remove stale claws-*.md files from cmdDir.
|
|
105
|
+
* @param {string} cmdDir
|
|
106
|
+
* @param {boolean} [dryRun]
|
|
107
|
+
*/
|
|
108
|
+
function sweepCommands(cmdDir, dryRun = false) {
|
|
109
|
+
if (!fs.existsSync(cmdDir)) return;
|
|
110
|
+
const stale = fs.readdirSync(cmdDir).filter(
|
|
111
|
+
f => f === 'claws.md' || (f.startsWith('claws-') && f.endsWith('.md'))
|
|
112
|
+
);
|
|
113
|
+
for (const f of stale) {
|
|
114
|
+
const p = path.join(cmdDir, f);
|
|
115
|
+
if (dryRun) { dryRunLog(`sweep stale command ${p}`); continue; }
|
|
116
|
+
fs.rmSync(p);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Bug 2: remove stale claws-* skill dirs from skillsDir.
|
|
122
|
+
* @param {string} skillsDir
|
|
123
|
+
* @param {boolean} [dryRun]
|
|
124
|
+
*/
|
|
125
|
+
function sweepSkills(skillsDir, dryRun = false) {
|
|
126
|
+
if (!fs.existsSync(skillsDir)) return;
|
|
127
|
+
const stale = fs.readdirSync(skillsDir).filter(
|
|
128
|
+
d => d.startsWith('claws-') &&
|
|
129
|
+
fs.statSync(path.join(skillsDir, d)).isDirectory()
|
|
130
|
+
);
|
|
131
|
+
for (const d of stale) {
|
|
132
|
+
const p = path.join(skillsDir, d);
|
|
133
|
+
if (dryRun) { dryRunLog(`sweep stale skill dir ${p}`); continue; }
|
|
134
|
+
fs.rmSync(p, { recursive: true });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = {
|
|
139
|
+
installCapabilities,
|
|
140
|
+
installCommands,
|
|
141
|
+
installSkills,
|
|
142
|
+
installRules,
|
|
143
|
+
sweepCommands,
|
|
144
|
+
sweepSkills,
|
|
145
|
+
};
|
package/lib/dry-run.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { dryRunLog } = require('./platform.js');
|
|
5
|
+
|
|
6
|
+
function mkdir(dir, dryRun) {
|
|
7
|
+
if (dryRun) { dryRunLog(`mkdir ${dir}`); return; }
|
|
8
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function copyFile(src, dest, dryRun) {
|
|
12
|
+
if (dryRun) { dryRunLog(`copy ${src} → ${dest}`); return; }
|
|
13
|
+
fs.copyFileSync(src, dest);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function copyDir(src, dest, dryRun) {
|
|
17
|
+
if (dryRun) { dryRunLog(`copy ${src}/ → ${dest}/`); return; }
|
|
18
|
+
fs.cpSync(src, dest, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeFile(filePath, content, dryRun) {
|
|
22
|
+
if (dryRun) { dryRunLog(`write ${filePath}`); return; }
|
|
23
|
+
const tmp = filePath + '.claws-tmp.' + process.pid;
|
|
24
|
+
fs.writeFileSync(tmp, content, 'utf8');
|
|
25
|
+
fs.renameSync(tmp, filePath);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function removeFile(filePath, dryRun) {
|
|
29
|
+
if (dryRun) { dryRunLog(`rm ${filePath}`); return; }
|
|
30
|
+
fs.rmSync(filePath);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function removeDir(dirPath, dryRun) {
|
|
34
|
+
if (dryRun) { dryRunLog(`rm -rf ${dirPath}`); return; }
|
|
35
|
+
fs.rmSync(dirPath, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function spawn(label, fn, dryRun) {
|
|
39
|
+
if (dryRun) { dryRunLog(label); return; }
|
|
40
|
+
fn();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { mkdir, copyFile, copyDir, writeFile, removeFile, removeDir, spawn };
|