@ran-sh/dsh-crew 0.3.6 → 0.3.8
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-plugin/plugin.json +8 -8
- package/.mcp.json +8 -8
- package/LICENSE +21 -21
- package/README.de.md +359 -359
- package/README.es.md +359 -359
- package/README.fr.md +359 -359
- package/README.hi.md +359 -359
- package/README.id.md +359 -359
- package/README.ja.md +359 -359
- package/README.ko.md +359 -359
- package/README.md +94 -180
- package/README.pt.md +359 -359
- package/README.ru.md +359 -359
- package/README.th.md +359 -359
- package/README.tr.md +359 -359
- package/README.vi.md +359 -359
- package/README.zh-TW.md +359 -359
- package/README.zh.md +94 -180
- package/agents/ds-flash.md +26 -26
- package/agents/ds-pro.md +32 -32
- package/agents/ds-reviewer.md +23 -23
- package/agents/ds-worker.md +22 -22
- package/codex/agents/ds-flash.toml +30 -30
- package/codex/agents/ds-pro.toml +31 -31
- package/codex/agents/ds-reviewer.toml +28 -28
- package/codex/agents/ds-worker.toml +28 -28
- package/codex/prompts/dsh-config.md +3 -3
- package/codex/prompts/dsh-status.md +1 -1
- package/commands/config.md +11 -11
- package/commands/off.md +5 -5
- package/commands/on.md +5 -5
- package/commands/status.md +5 -5
- package/cordis.patch.yml +4 -4
- package/lib/client.js +1655 -974
- package/official-web-bridge/cordis.patch.yml +4 -0
- package/official-web-bridge/entry.mjs +1 -0
- package/official-web-bridge/lib/client.js +3446 -0
- package/official-web-bridge/package.json +26 -0
- package/package.json +133 -131
- package/scripts/build-client.mjs +32 -28
- package/scripts/live-crew-smoke.mjs +39 -39
- package/scripts/live-policy-matrix.mjs +177 -177
- package/scripts/policy-probe.mjs +101 -101
- package/scripts/setup.mjs +284 -284
- package/scripts/smoke-real.mjs +110 -110
- package/scripts/smoke.mjs +78 -78
- package/scripts/verify-installer-fix.mjs +26 -26
- package/scripts/verify-official-bridge-e2e.mjs +159 -0
- package/src/adaptive-routing.mjs +260 -260
- package/src/client/activation-summary.tsx +64 -64
- package/src/client/collapsible-sections.mjs +55 -0
- package/src/client/entry.tsx +236 -236
- package/src/client/index.tsx +1213 -1120
- package/src/config-readiness.mjs +59 -59
- package/src/delivery.mjs +205 -205
- package/src/dsh-cli-runtime.mjs +458 -447
- package/src/failure-classification.mjs +172 -172
- package/src/hub/entry.mjs +98 -98
- package/src/hub-client.mjs +132 -132
- package/src/hub-compatibility.mjs +49 -49
- package/src/i18n.mjs +19 -19
- package/src/install/cli.mjs +28 -28
- package/src/install/install-legacy.mjs +462 -462
- package/src/install/install.mjs +451 -451
- package/src/install/npx-lifecycle.mjs +1119 -1055
- package/src/install/official-web.mjs +132 -0
- package/src/mcp-runtime.mjs +257 -257
- package/src/model-catalog.mjs +173 -173
- package/src/model-routing.mjs +391 -391
- package/src/official-web-bridge.mjs +196 -0
- package/src/policy.mjs +197 -197
- package/src/readiness-matrix.mjs +169 -169
- package/src/runtime-controls.mjs +90 -90
- package/src/runtime-identity.mjs +108 -108
- package/src/server.mjs +477 -477
- package/src/status-shard.mjs +52 -52
- package/src/structured-error-code.mjs +38 -38
- package/src/vision-route.mjs +138 -138
- package/src/workflow-runtime.mjs +573 -573
- package/src/workflow.mjs +160 -160
- package/src/workspace-audit.mjs +231 -231
- package/src/workspace-isolation.mjs +365 -365
- package/statusline/statusline.sh +14 -14
- package/statusline/worker-segment.sh +35 -35
- package/worker.cordis.yml +77 -77
|
@@ -1,1055 +1,1119 @@
|
|
|
1
|
-
// Packaged global-launcher lifecycle: install / status / update / uninstall.
|
|
2
|
-
//
|
|
3
|
-
// Public UX:
|
|
4
|
-
// npm install -g @ran-sh/dsh-crew@latest
|
|
5
|
-
// dsh-crew install|status|update|uninstall
|
|
6
|
-
//
|
|
7
|
-
// A package-manager launcher may run from a replaceable global/cache path.
|
|
8
|
-
// This module therefore persists the already-built package payload into
|
|
9
|
-
// Crew-owned state BEFORE registering it with the Harness profile, so the
|
|
10
|
-
// registration never depends on the cache, tarball, or temp extraction dir:
|
|
11
|
-
//
|
|
12
|
-
// <home>/.config/dsh-crew/app/current.json active-release pointer
|
|
13
|
-
// <home>/.config/dsh-crew/app/releases/<stamp>/ durable installed payloads
|
|
14
|
-
//
|
|
15
|
-
// Everything else (config.json, credentials, backups, the isolated Harness
|
|
16
|
-
// home) is never touched by install/update; uninstall removes only the
|
|
17
|
-
// Crew-managed payload above plus registrations/integrations, and keeps the
|
|
18
|
-
// normal Crew config unless --purge is explicitly requested.
|
|
19
|
-
//
|
|
20
|
-
// Source checkouts keep using scripts/setup.mjs; this module is for the
|
|
21
|
-
// packaged npm payload and never requires pnpm lockfiles, devDependencies,
|
|
22
|
-
// or a client rebuild.
|
|
23
|
-
|
|
24
|
-
import { spawnSync } from 'node:child_process';
|
|
25
|
-
import {
|
|
26
|
-
cpSync,
|
|
27
|
-
existsSync,
|
|
28
|
-
lstatSync,
|
|
29
|
-
mkdirSync,
|
|
30
|
-
readdirSync,
|
|
31
|
-
readFileSync,
|
|
32
|
-
realpathSync,
|
|
33
|
-
rmSync,
|
|
34
|
-
writeFileSync,
|
|
35
|
-
} from 'node:fs';
|
|
36
|
-
import { createRequire } from 'node:module';
|
|
37
|
-
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
38
|
-
import { fileURLToPath } from 'node:url';
|
|
39
|
-
import { homedir } from 'node:os';
|
|
40
|
-
import * as realInstaller from './install.mjs';
|
|
41
|
-
import { crewProfileDir } from './install.mjs';
|
|
42
|
-
import { ensureCrewDshRuntime, ensureCrewPluginRegistration, removeCrewPluginRegistration } from '../dsh-cli-runtime.mjs';
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
export
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
export function
|
|
66
|
-
return
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (!
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
*
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (!
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
stagedManifest
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
if (!
|
|
328
|
-
rmSync(stageDir, { recursive: true, force: true });
|
|
329
|
-
return { ok: false, code: '
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
return
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
function
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
if (
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const
|
|
537
|
-
if (!
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const
|
|
585
|
-
if (!
|
|
586
|
-
log(
|
|
587
|
-
return { ok: false, error:
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
return { ok:
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
log
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
return { ok: false, code: 'CANDIDATE_MANIFEST_INVALID', detail:
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
ok
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
log('');
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
if (
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
const
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
const
|
|
937
|
-
const
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
if (
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
log(
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
const
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1
|
+
// Packaged global-launcher lifecycle: install / status / update / uninstall.
|
|
2
|
+
//
|
|
3
|
+
// Public UX:
|
|
4
|
+
// npm install -g @ran-sh/dsh-crew@latest
|
|
5
|
+
// dsh-crew install|status|update|uninstall
|
|
6
|
+
//
|
|
7
|
+
// A package-manager launcher may run from a replaceable global/cache path.
|
|
8
|
+
// This module therefore persists the already-built package payload into
|
|
9
|
+
// Crew-owned state BEFORE registering it with the Harness profile, so the
|
|
10
|
+
// registration never depends on the cache, tarball, or temp extraction dir:
|
|
11
|
+
//
|
|
12
|
+
// <home>/.config/dsh-crew/app/current.json active-release pointer
|
|
13
|
+
// <home>/.config/dsh-crew/app/releases/<stamp>/ durable installed payloads
|
|
14
|
+
//
|
|
15
|
+
// Everything else (config.json, credentials, backups, the isolated Harness
|
|
16
|
+
// home) is never touched by install/update; uninstall removes only the
|
|
17
|
+
// Crew-managed payload above plus registrations/integrations, and keeps the
|
|
18
|
+
// normal Crew config unless --purge is explicitly requested.
|
|
19
|
+
//
|
|
20
|
+
// Source checkouts keep using scripts/setup.mjs; this module is for the
|
|
21
|
+
// packaged npm payload and never requires pnpm lockfiles, devDependencies,
|
|
22
|
+
// or a client rebuild.
|
|
23
|
+
|
|
24
|
+
import { spawnSync } from 'node:child_process';
|
|
25
|
+
import {
|
|
26
|
+
cpSync,
|
|
27
|
+
existsSync,
|
|
28
|
+
lstatSync,
|
|
29
|
+
mkdirSync,
|
|
30
|
+
readdirSync,
|
|
31
|
+
readFileSync,
|
|
32
|
+
realpathSync,
|
|
33
|
+
rmSync,
|
|
34
|
+
writeFileSync,
|
|
35
|
+
} from 'node:fs';
|
|
36
|
+
import { createRequire } from 'node:module';
|
|
37
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
38
|
+
import { fileURLToPath } from 'node:url';
|
|
39
|
+
import { homedir } from 'node:os';
|
|
40
|
+
import * as realInstaller from './install.mjs';
|
|
41
|
+
import { crewProfileDir } from './install.mjs';
|
|
42
|
+
import { ensureCrewDshRuntime, ensureCrewPluginRegistration, removeCrewPluginRegistration } from '../dsh-cli-runtime.mjs';
|
|
43
|
+
import {
|
|
44
|
+
ensureOfficialWebIntegration,
|
|
45
|
+
officialWebIntegrationStatus,
|
|
46
|
+
removeOfficialWebIntegration,
|
|
47
|
+
} from './official-web.mjs';
|
|
48
|
+
|
|
49
|
+
export const CREW_APP_DIRNAME = 'app';
|
|
50
|
+
export const RELEASES_DIRNAME = 'releases';
|
|
51
|
+
export const CURRENT_POINTER_FILENAME = 'current.json';
|
|
52
|
+
export const KEEP_RELEASES = 2;
|
|
53
|
+
export const INCOMPLETE_MARKER = '.dsh-crew-incomplete';
|
|
54
|
+
|
|
55
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
56
|
+
|
|
57
|
+
export function crewAppRoot({ home = homedir() } = {}) {
|
|
58
|
+
return join(home, '.config', 'dsh-crew', CREW_APP_DIRNAME);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function crewReleasesDir({ home = homedir() } = {}) {
|
|
62
|
+
return join(crewAppRoot({ home }), RELEASES_DIRNAME);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function currentPointerFile({ home = homedir() } = {}) {
|
|
66
|
+
return join(crewAppRoot({ home }), CURRENT_POINTER_FILENAME);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Package root of the currently executing (candidate) instance. */
|
|
70
|
+
export function runningPackageRoot() {
|
|
71
|
+
return PACKAGE_ROOT;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readManifest(root) {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function timestampStamp(now = new Date()) {
|
|
83
|
+
const pad = (n, w = 2) => String(n).padStart(w, '0');
|
|
84
|
+
return `${now.getUTCFullYear()}${pad(now.getUTCMonth() + 1)}${pad(now.getUTCDate())}T${pad(now.getUTCHours())}${pad(now.getUTCMinutes())}${pad(now.getUTCSeconds())}Z`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let releaseSalt = 0;
|
|
88
|
+
function uniqueReleaseName(version) {
|
|
89
|
+
// Second-resolution timestamps can collide across rapid sequential runs in
|
|
90
|
+
// the same process; a process-local counter keeps release names unique so
|
|
91
|
+
// an update never reuses (and thus never clobbers) an existing release.
|
|
92
|
+
releaseSalt += 1;
|
|
93
|
+
return `${timestampStamp()}-${process.pid}-${releaseSalt.toString(36)}-${version}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isoNow() {
|
|
97
|
+
const offsetMinutes = -new Date().getTimezoneOffset();
|
|
98
|
+
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
99
|
+
const abs = Math.abs(offsetMinutes);
|
|
100
|
+
const pad = (n, w = 2) => String(n).padStart(w, '0');
|
|
101
|
+
return `${new Date().getFullYear()}-${pad(new Date().getMonth() + 1)}-${pad(new Date().getDate())}T${pad(new Date().getHours())}:${pad(new Date().getMinutes())}:${pad(new Date().getSeconds())}${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---- installed-payload pointer ----------------------------------------------
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Read the active installed-release pointer. Returns null when no valid
|
|
108
|
+
* pointer exists. The pointed-at directory must still exist with a matching
|
|
109
|
+
* manifest to count as installed.
|
|
110
|
+
*/
|
|
111
|
+
export function readCurrentPointer({ home = homedir() } = {}) {
|
|
112
|
+
const file = currentPointerFile({ home });
|
|
113
|
+
if (!existsSync(file)) return null;
|
|
114
|
+
let raw;
|
|
115
|
+
try { raw = JSON.parse(readFileSync(file, 'utf8')); } catch { return null; }
|
|
116
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
117
|
+
if (typeof raw.name !== 'string' || typeof raw.version !== 'string' || typeof raw.path !== 'string') return null;
|
|
118
|
+
if (!isAbsolute(raw.path)) return null;
|
|
119
|
+
return raw;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function writeCurrentPointer({ home, name, version, path }) {
|
|
123
|
+
mkdirSync(dirname(currentPointerFile({ home })), { recursive: true });
|
|
124
|
+
const pointer = { name, version, path, installed_at: isoNow(), managed_by: 'npx' };
|
|
125
|
+
writeFileSync(currentPointerFile({ home }), JSON.stringify(pointer, null, 2) + '\n');
|
|
126
|
+
return pointer;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---- dependency tree materialization ----------------------------------------
|
|
130
|
+
|
|
131
|
+
function listPackageEdges(manifest) {
|
|
132
|
+
// Returns [{name, optional}] covering dependencies and
|
|
133
|
+
// required peerDependencies. Packages in the DSH cohort use peers for
|
|
134
|
+
// shared protocol/runtime modules, but a persisted Crew payload has no host
|
|
135
|
+
// node_modules to supply them, so their transitive peers are runtime edges.
|
|
136
|
+
// Platform-specific optional bits and optional peers stay non-fatal.
|
|
137
|
+
const edges = [];
|
|
138
|
+
for (const [name] of Object.entries(manifest?.dependencies ?? {})) {
|
|
139
|
+
edges.push({ name, optional: false });
|
|
140
|
+
}
|
|
141
|
+
for (const [name] of Object.entries(manifest?.optionalDependencies ?? {})) {
|
|
142
|
+
if (!edges.some((edge) => edge.name === name)) edges.push({ name, optional: true });
|
|
143
|
+
}
|
|
144
|
+
for (const [name] of Object.entries(manifest?.peerDependencies ?? {})) {
|
|
145
|
+
if (edges.some((edge) => edge.name === name)) continue;
|
|
146
|
+
edges.push({ name, optional: manifest?.peerDependenciesMeta?.[name]?.optional === true });
|
|
147
|
+
}
|
|
148
|
+
return edges.filter((edge) => typeof edge.name === 'string' && edge.name.trim());
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function findPackageDir(fromRoot, name, exists = existsSync) {
|
|
152
|
+
// Node-style upward resolution: for each ancestor directory of fromRoot,
|
|
153
|
+
// probe <ancestor>/node_modules/<name>. Covers npm's hoisted npx cache and
|
|
154
|
+
// pnpm's virtual store (once a package is realpathed into .pnpm/<pkg>/
|
|
155
|
+
// node_modules, walking up finds its exact sibling closure).
|
|
156
|
+
const parts = name.split('/');
|
|
157
|
+
const seen = new Set();
|
|
158
|
+
let cursor = fromRoot;
|
|
159
|
+
while (true) {
|
|
160
|
+
const candidate = join(cursor, 'node_modules', ...parts);
|
|
161
|
+
if (exists(candidate)) {
|
|
162
|
+
try {
|
|
163
|
+
const real = realpathSync(candidate);
|
|
164
|
+
if (!seen.has(real)) { seen.add(real); return real; }
|
|
165
|
+
} catch { /* skip unreadable entry */ }
|
|
166
|
+
}
|
|
167
|
+
const parent = dirname(cursor);
|
|
168
|
+
if (parent === cursor) return null;
|
|
169
|
+
cursor = parent;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Copy the dependency closure of `names` out of the running instance's
|
|
175
|
+
* dependency tree into `<toRoot>/node_modules`. Offline-safe: it replicates
|
|
176
|
+
* exactly the dependency bits the candidate just executed with. Optional
|
|
177
|
+
* edges (e.g. cross-platform native binaries) are copied when present but
|
|
178
|
+
* reported separately when absent, so npm can reconcile them per-platform.
|
|
179
|
+
* Returns { copied: Map<name, realSource>, missing: string[], missingOptional: string[] }.
|
|
180
|
+
*/
|
|
181
|
+
export function copyProductionDependencyTree({
|
|
182
|
+
fromRoot,
|
|
183
|
+
toRoot,
|
|
184
|
+
names,
|
|
185
|
+
exists = existsSync,
|
|
186
|
+
copyDir = (src, dest) => cpSync(src, dest, { recursive: true, force: true, dereference: true }),
|
|
187
|
+
} = {}) {
|
|
188
|
+
const toNodeModules = join(toRoot, 'node_modules');
|
|
189
|
+
mkdirSync(toNodeModules, { recursive: true });
|
|
190
|
+
const copied = new Map();
|
|
191
|
+
const missing = [];
|
|
192
|
+
const missingOptional = [];
|
|
193
|
+
const queue = names.map((entry) => (typeof entry === 'string'
|
|
194
|
+
? { name: entry, fromRoot, optional: false }
|
|
195
|
+
: { ...entry, fromRoot }));
|
|
196
|
+
while (queue.length > 0) {
|
|
197
|
+
const { name, fromRoot: searchRoot, optional } = queue.shift();
|
|
198
|
+
if (copied.has(name) || missing.includes(name) || missingOptional.includes(name)) continue;
|
|
199
|
+
const source = findPackageDir(searchRoot ?? fromRoot, name, exists);
|
|
200
|
+
if (!source) {
|
|
201
|
+
if (optional) missingOptional.push(name);
|
|
202
|
+
else missing.push(name);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const dest = join(toNodeModules, ...name.split('/'));
|
|
206
|
+
copyDir(source, dest);
|
|
207
|
+
copied.set(name, source);
|
|
208
|
+
const depManifest = readManifest(source);
|
|
209
|
+
for (const edge of listPackageEdges(depManifest)) {
|
|
210
|
+
if (!copied.has(edge.name) && !missing.includes(edge.name) && !missingOptional.includes(edge.name)) {
|
|
211
|
+
// Resolve children relative to the copied package's own physical
|
|
212
|
+
// location so nested/virtual-store layouts resolve correctly.
|
|
213
|
+
queue.push({ name: edge.name, fromRoot: dirname(source), optional: edge.optional || optional });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return { copied, missing, missingOptional };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function defaultNpmInstaller(stageRoot, log) {
|
|
221
|
+
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
222
|
+
const result = spawnSync(npm, [
|
|
223
|
+
'install', '--prefix', stageRoot,
|
|
224
|
+
'--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund',
|
|
225
|
+
'--no-package-lock', '--loglevel=error',
|
|
226
|
+
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], shell: process.platform === 'win32', timeout: 600_000, windowsHide: true, env: sanitizedPackageManagerEnv() });
|
|
227
|
+
if (result.status !== 0) {
|
|
228
|
+
log(`- npm fallback install failed (${result.status}): ${(result.stderr || result.stdout || '').trim().slice(-300)}`);
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ---- payload staging ---------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Stage the candidate into its FINAL release directory, guarded by an
|
|
238
|
+
* incompleteness marker. Committing = removing the marker (a single-file
|
|
239
|
+
* operation that is reliable on all platforms); activation = writing the
|
|
240
|
+
* pointer last. This avoids directory renames, which Windows can refuse with
|
|
241
|
+
* transient EPERM while a freshly written tree is being scanned.
|
|
242
|
+
*
|
|
243
|
+
* The persisted manifest merges the exact-pinned DSH peer cohort into
|
|
244
|
+
* `dependencies` (peer declarations are dropped): the installed payload runs
|
|
245
|
+
* standalone — `src/server.mjs` statically imports DSH peers — so those
|
|
246
|
+
* packages must exist inside the release, not be assumed from a host.
|
|
247
|
+
*/
|
|
248
|
+
export function stageCandidatePayload({
|
|
249
|
+
sourceRoot = runningPackageRoot(),
|
|
250
|
+
home = homedir(),
|
|
251
|
+
log = () => {},
|
|
252
|
+
now = new Date(),
|
|
253
|
+
npmInstaller = defaultNpmInstaller,
|
|
254
|
+
copyTree = copyProductionDependencyTree,
|
|
255
|
+
smoke = defaultPayloadSmoke,
|
|
256
|
+
} = {}) {
|
|
257
|
+
const manifest = readManifest(sourceRoot);
|
|
258
|
+
if (!manifest?.name || !manifest?.version) return { ok: false, code: 'CANDIDATE_MANIFEST_INVALID' };
|
|
259
|
+
if (!existsSync(join(sourceRoot, 'src', 'server.mjs')) || !existsSync(join(sourceRoot, 'cordis.patch.yml'))) {
|
|
260
|
+
return { ok: false, code: 'CANDIDATE_NOT_RUNNABLE' };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const releasesDir = crewReleasesDir({ home });
|
|
264
|
+
mkdirSync(releasesDir, { recursive: true });
|
|
265
|
+
const stageDir = join(releasesDir, uniqueReleaseName(manifest.version));
|
|
266
|
+
rmSync(stageDir, { recursive: true, force: true });
|
|
267
|
+
mkdirSync(stageDir, { recursive: true });
|
|
268
|
+
writeFileSync(join(stageDir, INCOMPLETE_MARKER), String(Date.now()) + '\n');
|
|
269
|
+
|
|
270
|
+
// Copy every top-level entry referenced by manifest.files (directories are
|
|
271
|
+
// copied whole; single-segment wildcards match within their directory).
|
|
272
|
+
const entries = new Set(['package.json']);
|
|
273
|
+
for (const pattern of manifest.files ?? []) {
|
|
274
|
+
if (typeof pattern !== 'string' || !pattern.trim()) continue;
|
|
275
|
+
if (!pattern.includes('*')) { entries.add(pattern); continue; }
|
|
276
|
+
const base = dirname(pattern);
|
|
277
|
+
const regex = new RegExp(`^${pattern.split('*').map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('[^/]*')}$`);
|
|
278
|
+
const scanDir = join(sourceRoot, base);
|
|
279
|
+
if (!existsSync(scanDir)) continue;
|
|
280
|
+
for (const item of readdirSync(scanDir)) {
|
|
281
|
+
const candidatePath = base === '.' ? item : `${base.replace(/\\/g, '/')}/${item}`;
|
|
282
|
+
if (regex.test(candidatePath)) entries.add(candidatePath);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
for (const entry of entries) {
|
|
287
|
+
const source = join(sourceRoot, entry);
|
|
288
|
+
if (!existsSync(source)) continue;
|
|
289
|
+
cpSync(source, join(stageDir, entry), { recursive: true, force: true, dereference: true });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Persist an adjusted manifest: identity/runtime fields stay; production
|
|
293
|
+
// dependencies gain the exact-pinned peer cohort so the payload runs
|
|
294
|
+
// standalone (src/server.mjs statically imports DSH peers). Optional
|
|
295
|
+
// dependencies keep their own section so npm reconciles them per-platform.
|
|
296
|
+
const stagedManifest = { ...manifest };
|
|
297
|
+
delete stagedManifest.devDependencies;
|
|
298
|
+
delete stagedManifest.peerDependencies;
|
|
299
|
+
delete stagedManifest.peerDependenciesMeta;
|
|
300
|
+
const optionalNames = new Set(Object.keys(manifest.optionalDependencies ?? {}));
|
|
301
|
+
stagedManifest.dependencies = {
|
|
302
|
+
...(manifest.peerDependencies ?? {}),
|
|
303
|
+
...(manifest.dependencies ?? {}),
|
|
304
|
+
};
|
|
305
|
+
writeFileSync(join(stageDir, 'package.json'), JSON.stringify(stagedManifest, null, 2) + '\n');
|
|
306
|
+
|
|
307
|
+
// Materialize dependencies: prefer replicating the exact bits the candidate
|
|
308
|
+
// ran with (offline-safe); fall back to npm for the rest (under npx this is
|
|
309
|
+
// normally the exact-pinned DSH peer cohort).
|
|
310
|
+
const depEdges = [
|
|
311
|
+
...Object.keys(stagedManifest.dependencies).map((name) => ({ name, optional: false })),
|
|
312
|
+
...Object.keys(manifest.optionalDependencies ?? {}).map((name) => ({ name, optional: true })),
|
|
313
|
+
];
|
|
314
|
+
const { missing, missingOptional } = copyTree({ fromRoot: sourceRoot, toRoot: stageDir, names: depEdges });
|
|
315
|
+
if (missingOptional.length > 0) {
|
|
316
|
+
log(`- platform-optional dependencies left to npm (${missingOptional.slice(0, 4).join(', ')}${missingOptional.length > 4 ? ', …' : ''})`);
|
|
317
|
+
}
|
|
318
|
+
if (missing.length > 0) {
|
|
319
|
+
log(`- local dependency replication incomplete (${missing.join(', ')}); falling back to npm`);
|
|
320
|
+
if (!npmInstaller(stageDir, log)) {
|
|
321
|
+
rmSync(stageDir, { recursive: true, force: true });
|
|
322
|
+
return { ok: false, code: 'DEPENDENCY_MATERIALIZE_FAILED', missing };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const validated = validateInstalledPayload(stageDir, { expectedName: manifest.name, expectedVersion: manifest.version, allowIncomplete: true });
|
|
327
|
+
if (!validated.ok) {
|
|
328
|
+
rmSync(stageDir, { recursive: true, force: true });
|
|
329
|
+
return { ok: false, code: 'STAGE_VALIDATION_FAILED', detail: validated.errors };
|
|
330
|
+
}
|
|
331
|
+
const smoked = smoke(stageDir);
|
|
332
|
+
if (!smoked.ok) {
|
|
333
|
+
rmSync(stageDir, { recursive: true, force: true });
|
|
334
|
+
return { ok: false, code: 'STAGE_SMOKE_FAILED', detail: smoked.detail };
|
|
335
|
+
}
|
|
336
|
+
return { ok: true, stageDir, manifest, releasesDir };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Boot-smoke a staged payload with the real Node binary: the CLI entry must
|
|
341
|
+
* start and answer --help from inside the staged tree alone.
|
|
342
|
+
*/
|
|
343
|
+
export function defaultPayloadSmoke(dir, { nodePath = process.execPath, runner = spawnSync } = {}) {
|
|
344
|
+
const cli = runner(nodePath, [join(dir, 'bin', 'dsh-crew.mjs'), '--help'], {
|
|
345
|
+
encoding: 'utf8', timeout: 120_000, windowsHide: true,
|
|
346
|
+
env: { ...process.env },
|
|
347
|
+
});
|
|
348
|
+
if (cli.status !== 0) {
|
|
349
|
+
return { ok: false, detail: `bin --help exited ${cli.status}: ${(cli.stderr || cli.stdout || '').trim().slice(-300)}` };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const initialize = {
|
|
353
|
+
jsonrpc: '2.0',
|
|
354
|
+
id: 1,
|
|
355
|
+
method: 'initialize',
|
|
356
|
+
params: {
|
|
357
|
+
protocolVersion: '2024-11-05',
|
|
358
|
+
capabilities: {},
|
|
359
|
+
clientInfo: { name: 'dsh-crew-payload-smoke', version: '1.0.0' },
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
const mcp = runner(nodePath, [join(dir, 'src', 'server.mjs')], {
|
|
363
|
+
encoding: 'utf8', timeout: 120_000, windowsHide: true,
|
|
364
|
+
input: JSON.stringify(initialize) + '\n',
|
|
365
|
+
env: { ...process.env },
|
|
366
|
+
});
|
|
367
|
+
if (mcp.status !== 0) {
|
|
368
|
+
return { ok: false, detail: `MCP initialize exited ${mcp.status}: ${(mcp.stderr || mcp.stdout || '').trim().slice(-300)}` };
|
|
369
|
+
}
|
|
370
|
+
let response;
|
|
371
|
+
try {
|
|
372
|
+
response = String(mcp.stdout ?? '').split(/\r?\n/).filter(Boolean)
|
|
373
|
+
.map((line) => JSON.parse(line)).find((entry) => entry?.id === initialize.id);
|
|
374
|
+
} catch { /* handled by the fail-closed response check below */ }
|
|
375
|
+
if (response?.result?.serverInfo?.name !== 'dsh-crew') {
|
|
376
|
+
return { ok: false, detail: `MCP initialize returned no valid dsh-crew response: ${String(mcp.stdout ?? '').trim().slice(-300)}` };
|
|
377
|
+
}
|
|
378
|
+
return { ok: true };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Keyword boundaries reject identifiers containing the keywords
|
|
382
|
+
// ('legacy-import') and member calls (.from); captures stay on one line and
|
|
383
|
+
// bounded — real specifiers never span lines.
|
|
384
|
+
const IMPORT_SPECIFIER_RE = /(?<![\w.\-])(?:from|import|require)\b\s*\(?\s*["']([^"'\n]{1,200})["']/g;
|
|
385
|
+
|
|
386
|
+
function collectExternalSpecifiers(dir) {
|
|
387
|
+
const specifiers = new Set();
|
|
388
|
+
const walk = (root) => {
|
|
389
|
+
if (!existsSync(root)) return;
|
|
390
|
+
for (const item of readdirSync(root, { withFileTypes: true })) {
|
|
391
|
+
const full = join(root, item.name);
|
|
392
|
+
if (item.isDirectory()) walk(full);
|
|
393
|
+
else if (/\.(?:mjs|js)$/.test(item.name)) {
|
|
394
|
+
let content = '';
|
|
395
|
+
try { content = readFileSync(full, 'utf8'); } catch { continue; }
|
|
396
|
+
for (const match of content.matchAll(IMPORT_SPECIFIER_RE)) {
|
|
397
|
+
specifiers.add(match[1]);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
walk(join(dir, 'src'));
|
|
403
|
+
walk(join(dir, 'bin'));
|
|
404
|
+
return [...specifiers].filter((spec) =>
|
|
405
|
+
!spec.startsWith('.') && !spec.startsWith('/') && !spec.startsWith('node:') && !isAbsolute(spec));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Prove a payload directory is runnable on its own: correct identity, all
|
|
410
|
+
* shipped runtime artifacts present, every declared dependency vendored, and
|
|
411
|
+
* every external module specifier the payload's source actually imports
|
|
412
|
+
* resolvable FROM THE PERSISTED LOCATION (never from a cache or checkout).
|
|
413
|
+
*/
|
|
414
|
+
export function validateInstalledPayload(dir, { expectedName, expectedVersion, allowIncomplete = false } = {}) {
|
|
415
|
+
const errors = [];
|
|
416
|
+
const manifest = readManifest(dir);
|
|
417
|
+
if (!manifest) errors.push('package.json missing or invalid');
|
|
418
|
+
else {
|
|
419
|
+
if (expectedName && manifest.name !== expectedName) errors.push(`package name mismatch (${manifest.name})`);
|
|
420
|
+
if (expectedVersion && manifest.version !== expectedVersion) errors.push(`package version mismatch (${manifest.version})`);
|
|
421
|
+
for (const dep of Object.keys(manifest.dependencies ?? {})) {
|
|
422
|
+
if (!existsSync(join(dir, 'node_modules', ...dep.split('/'), 'package.json'))) {
|
|
423
|
+
errors.push(`vendored dependency missing from payload: ${dep}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (manifest) {
|
|
428
|
+
const requireFromPayload = createRequire(join(dir, 'package.json'));
|
|
429
|
+
for (const spec of collectExternalSpecifiers(dir)) {
|
|
430
|
+
try {
|
|
431
|
+
requireFromPayload.resolve(spec);
|
|
432
|
+
} catch {
|
|
433
|
+
errors.push(`import target not resolvable from payload: ${spec}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
for (const rel of [
|
|
438
|
+
'cordis.patch.yml', 'src/server.mjs', 'src/hub/entry.mjs', 'lib/client.js', 'bin/dsh-crew.mjs',
|
|
439
|
+
'official-web-bridge/package.json', 'official-web-bridge/cordis.patch.yml',
|
|
440
|
+
'official-web-bridge/entry.mjs', 'official-web-bridge/lib/client.js',
|
|
441
|
+
]) {
|
|
442
|
+
if (!existsSync(join(dir, rel))) errors.push(`payload artifact missing: ${rel}`);
|
|
443
|
+
}
|
|
444
|
+
if (!allowIncomplete && existsSync(join(dir, INCOMPLETE_MARKER))) errors.push('release is still marked incomplete');
|
|
445
|
+
return { ok: errors.length === 0, errors, manifest };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ---- release commit / activation ---------------------------------------------
|
|
449
|
+
|
|
450
|
+
function commitStagedRelease({ stageDir, manifest, home }) {
|
|
451
|
+
rmSync(join(stageDir, INCOMPLETE_MARKER));
|
|
452
|
+
writeCurrentPointer({ home, name: manifest.name, version: manifest.version, path: stageDir });
|
|
453
|
+
gcOldReleases({ home });
|
|
454
|
+
return stageDir;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const STALE_INCOMPLETE_MS = 24 * 60 * 60 * 1000;
|
|
458
|
+
|
|
459
|
+
function gcOldReleases({ home, keep = KEEP_RELEASES }) {
|
|
460
|
+
const pointer = readCurrentPointer({ home });
|
|
461
|
+
const releasesDir = crewReleasesDir({ home });
|
|
462
|
+
if (!existsSync(releasesDir)) return;
|
|
463
|
+
const removed = [];
|
|
464
|
+
const dirs = readdirSync(releasesDir)
|
|
465
|
+
.map((name) => join(releasesDir, name))
|
|
466
|
+
.filter((dir) => !pointer || dir !== pointer.path);
|
|
467
|
+
const incomplete = [];
|
|
468
|
+
const complete = [];
|
|
469
|
+
for (const dir of dirs) {
|
|
470
|
+
if (existsSync(join(dir, INCOMPLETE_MARKER))) {
|
|
471
|
+
// Never race a concurrent staging; only reap clearly abandoned ones.
|
|
472
|
+
try {
|
|
473
|
+
if (Date.now() - lstatSync(dir).mtimeMs > STALE_INCOMPLETE_MS) incomplete.push(dir);
|
|
474
|
+
} catch { /* ignore */ }
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
complete.push(dir);
|
|
478
|
+
}
|
|
479
|
+
complete.sort();
|
|
480
|
+
while (complete.length > Math.max(0, keep - 1)) {
|
|
481
|
+
const victim = complete.shift();
|
|
482
|
+
try { rmSync(victim, { recursive: true, force: true }); removed.push(victim); } catch { /* best effort */ }
|
|
483
|
+
}
|
|
484
|
+
for (const victim of incomplete) {
|
|
485
|
+
try { rmSync(victim, { recursive: true, force: true }); removed.push(victim); } catch { /* best effort */ }
|
|
486
|
+
}
|
|
487
|
+
return removed;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function registrationLinkPath({ home, name }) {
|
|
491
|
+
return join(crewProfileDir({ home }), 'node_modules', ...name.split('/'));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function registrationHealthy({ home, name, releaseDir }) {
|
|
495
|
+
const link = registrationLinkPath({ home, name });
|
|
496
|
+
if (!existsSync(link)) return false;
|
|
497
|
+
try { return realpathSync(link) === realpathSync(releaseDir); } catch { return false; }
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Environment for child package managers. Under `npm exec`/npx the process
|
|
502
|
+
* inherits dozens of npm_config_* variables describing the transient exec
|
|
503
|
+
* context; leaking them into pnpm/npm children can misdirect their stores and
|
|
504
|
+
* config resolution. Package-manager children must see the user's real
|
|
505
|
+
* environment, not our execution context.
|
|
506
|
+
*/
|
|
507
|
+
export function sanitizedPackageManagerEnv(baseEnv = process.env) {
|
|
508
|
+
const env = {};
|
|
509
|
+
for (const [key, value] of Object.entries(baseEnv)) {
|
|
510
|
+
if (/^npm_(config_|lifecycle|package_|execpath$|node_execpath$)/i.test(key)) continue;
|
|
511
|
+
env[key] = value;
|
|
512
|
+
}
|
|
513
|
+
return env;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function ensureRuntimeStep({ home, log, ensureRuntime }) {
|
|
517
|
+
const ensure = ensureRuntime ?? ((opts) => {
|
|
518
|
+
const r = ensureCrewDshRuntime({ ...opts, env: sanitizedPackageManagerEnv() });
|
|
519
|
+
if (!r.ok && r.stderrTail) {
|
|
520
|
+
log(` (runtime installer said: ${r.stderrTail})`);
|
|
521
|
+
}
|
|
522
|
+
return r.ok ? { ok: true, version: r.cli?.version ?? null } : { ok: false, error: r.error ?? r.code ?? 'runtime bootstrap failed' };
|
|
523
|
+
});
|
|
524
|
+
const r = await ensure({ home });
|
|
525
|
+
if (!r?.ok) {
|
|
526
|
+
log(`✗ reusable Crew DSH runtime unavailable: ${r?.error ?? 'unknown error'}`);
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
log(`✓ reusable Crew DSH runtime${r.version ? ` (@${r.version})` : ''}`);
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ---- commands -----------------------------------------------------------------
|
|
534
|
+
|
|
535
|
+
function currentInstallationHealth({ home }) {
|
|
536
|
+
const pointer = readCurrentPointer({ home });
|
|
537
|
+
if (!pointer) return { installed: false, healthy: false };
|
|
538
|
+
if (!existsSync(pointer.path)) return { installed: false, healthy: false, pointer };
|
|
539
|
+
const validated = validateInstalledPayload(pointer.path, { expectedName: pointer.name, expectedVersion: pointer.version });
|
|
540
|
+
const registered = registrationHealthy({ home, name: pointer.name, releaseDir: pointer.path });
|
|
541
|
+
return { installed: true, healthy: validated.ok && registered, validated, registered, pointer };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async function activateRelease({ home, releaseDir, manifest, log, installer }) {
|
|
545
|
+
const registration = ensureCrewPluginRegistration({ home, root: releaseDir, name: manifest.name });
|
|
546
|
+
if (!registration.ok) {
|
|
547
|
+
log(`✗ Harness registration failed (${registration.code ?? 'unknown'})`);
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
log(`✓ Harness plugin registered (dedicated dsh-crew profile → ${releaseDir})`);
|
|
551
|
+
|
|
552
|
+
const codex = installer.installCodex({ home, root: releaseDir });
|
|
553
|
+
if (codex.ok === false) {
|
|
554
|
+
log(`✗ Codex Desktop integration failed: ${(codex.actions ?? []).join('; ')}`);
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
log('✓ Codex Desktop integration');
|
|
558
|
+
|
|
559
|
+
const claude = await installer.installClaudeCode({ home, root: releaseDir });
|
|
560
|
+
if (claude.ok === false) {
|
|
561
|
+
log(`✗ Claude Code integration failed`);
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
log('✓ Claude Code integration');
|
|
565
|
+
|
|
566
|
+
const official = officialWebIntegrationStatus({ home });
|
|
567
|
+
if (official.enabled) {
|
|
568
|
+
const repaired = ensureOfficialWebIntegration({ home, releaseDir });
|
|
569
|
+
if (!repaired.ok) {
|
|
570
|
+
log(`✗ official 3080 bridge repair failed (${repaired.code ?? 'unknown'})`);
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
log('✓ official 3080 UI bridge → isolated Crew backend on 3210');
|
|
574
|
+
}
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export async function npxIntegrate({ home = homedir(), log = console.log } = {}) {
|
|
579
|
+
const pointer = readCurrentPointer({ home });
|
|
580
|
+
if (!pointer || !existsSync(pointer.path)) {
|
|
581
|
+
log('✗ install DSH Crew before enabling the official 3080 integration');
|
|
582
|
+
return { ok: false, error: 'DSH Crew is not installed' };
|
|
583
|
+
}
|
|
584
|
+
const validated = validateInstalledPayload(pointer.path, { expectedName: pointer.name, expectedVersion: pointer.version });
|
|
585
|
+
if (!validated.ok) {
|
|
586
|
+
log('✗ installed DSH Crew payload is damaged; run dsh-crew update first');
|
|
587
|
+
return { ok: false, error: 'installed payload invalid' };
|
|
588
|
+
}
|
|
589
|
+
const result = ensureOfficialWebIntegration({ home, releaseDir: pointer.path });
|
|
590
|
+
if (!result.ok) {
|
|
591
|
+
log(`✗ official 3080 integration failed (${result.code ?? 'unknown'})`);
|
|
592
|
+
return { ok: false, error: result.code ?? 'integration failed' };
|
|
593
|
+
}
|
|
594
|
+
log(result.changed
|
|
595
|
+
? '✓ official 3080 UI connected to the isolated Crew backend on 3210'
|
|
596
|
+
: '- official 3080 UI integration already healthy');
|
|
597
|
+
log(` backup: ${result.backupFile}`);
|
|
598
|
+
return { ok: true, changed: result.changed, backupFile: result.backupFile };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export async function npxDetach({ home = homedir(), log = console.log } = {}) {
|
|
602
|
+
const result = removeOfficialWebIntegration({ home });
|
|
603
|
+
if (!result.ok) {
|
|
604
|
+
log(`✗ official 3080 integration removal failed (${result.code ?? 'unknown'})`);
|
|
605
|
+
return { ok: false, error: result.code ?? 'detach failed' };
|
|
606
|
+
}
|
|
607
|
+
log(result.removed ? '✓ official 3080 bridge removed; isolated 3210 mode remains available' : '- official 3080 bridge already absent');
|
|
608
|
+
return { ok: true, removed: result.removed };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export async function npxInstall({
|
|
612
|
+
home = homedir(),
|
|
613
|
+
log = console.log,
|
|
614
|
+
sourceRoot,
|
|
615
|
+
installer = realInstaller,
|
|
616
|
+
ensureRuntime,
|
|
617
|
+
npmInstaller,
|
|
618
|
+
} = {}) {
|
|
619
|
+
log('DSH Crew installer (npx-managed)');
|
|
620
|
+
const candidateRoot = sourceRoot ?? runningPackageRoot();
|
|
621
|
+
const manifest = readManifest(candidateRoot);
|
|
622
|
+
if (!manifest?.name || !manifest?.version) return { ok: false, error: 'candidate package manifest invalid' };
|
|
623
|
+
|
|
624
|
+
const health = currentInstallationHealth({ home });
|
|
625
|
+
if (health.installed && health.pointer.version === manifest.version && health.validated.ok) {
|
|
626
|
+
// Same-version reinstall: repair activation surfaces without restaging.
|
|
627
|
+
log(`- installed payload ${manifest.version} already present and valid; repairing activation`);
|
|
628
|
+
const activated = await activateRelease({ home, releaseDir: health.pointer.path, manifest, log, installer });
|
|
629
|
+
if (!activated) return { ok: false, error: 'activation failed' };
|
|
630
|
+
if (!await ensureRuntimeStep({ home, log, ensureRuntime })) return { ok: false, error: 'Crew DSH runtime bootstrap failed' };
|
|
631
|
+
log('');
|
|
632
|
+
log('Done.');
|
|
633
|
+
return { ok: true, repaired: true, version: manifest.version, path: health.pointer.path };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const staged = stageCandidatePayload({ sourceRoot: candidateRoot, home, log, npmInstaller });
|
|
637
|
+
if (!staged.ok) {
|
|
638
|
+
log(`✗ staging failed (${staged.code})${staged.detail ? `: ${staged.detail.join('; ')}` : ''}`);
|
|
639
|
+
return { ok: false, error: `staging failed (${staged.code})` };
|
|
640
|
+
}
|
|
641
|
+
log(`✓ candidate payload staged (${manifest.version})`);
|
|
642
|
+
|
|
643
|
+
const releaseDir = commitStagedRelease({ stageDir: staged.stageDir, manifest, home });
|
|
644
|
+
log(`✓ durable release committed under Crew-owned state`);
|
|
645
|
+
|
|
646
|
+
const activated = await activateRelease({ home, releaseDir, manifest, log, installer });
|
|
647
|
+
if (!activated) return { ok: false, error: 'activation failed' };
|
|
648
|
+
|
|
649
|
+
if (!await ensureRuntimeStep({ home, log, ensureRuntime })) return { ok: false, error: 'Crew DSH runtime bootstrap failed' };
|
|
650
|
+
|
|
651
|
+
log('');
|
|
652
|
+
log('Done.');
|
|
653
|
+
log('Restart DeepSeek Harness and Codex Desktop.');
|
|
654
|
+
return { ok: true, version: manifest.version, path: releaseDir };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export const UPDATE_PACKAGE_NAME = '@ran-sh/dsh-crew';
|
|
658
|
+
export const UPDATE_DEFAULT_SPEC = `${UPDATE_PACKAGE_NAME}@latest`;
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Extract a packed npm tarball with the platform `tar` binary (bsdtar ships
|
|
662
|
+
* with Windows 10+, macOS, and Linux) into destDir; npm tarballs always root
|
|
663
|
+
* at `package/`.
|
|
664
|
+
*/
|
|
665
|
+
export function extractPackageTarball(tgzPath, destDir, { runner = spawnSync } = {}) {
|
|
666
|
+
mkdirSync(destDir, { recursive: true });
|
|
667
|
+
const result = runner('tar', ['-xzf', String(tgzPath), '-C', destDir], {
|
|
668
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
|
|
669
|
+
});
|
|
670
|
+
const root = join(destDir, 'package');
|
|
671
|
+
if (result.status !== 0 || !existsSync(join(root, 'package.json'))) {
|
|
672
|
+
return { ok: false, detail: `tar extract failed (${result.status}): ${(result.stderr || result.stdout || '').trim().slice(-200)}` };
|
|
673
|
+
}
|
|
674
|
+
return { ok: true, sourceRoot: root };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* The globally installed launcher intentionally does not self-replace. When
|
|
679
|
+
* the payload is newer, keep it authoritative and give the exact launcher
|
|
680
|
+
* refresh command. A newer launcher is handled by npxUpdate before this point.
|
|
681
|
+
*/
|
|
682
|
+
function noteLauncherDivergence({ log, home = homedir() }) {
|
|
683
|
+
const launcherVersion = readManifest(runningPackageRoot())?.version ?? null;
|
|
684
|
+
let installedVersion = null;
|
|
685
|
+
try { installedVersion = readCurrentPointer({ home }).version ?? null; } catch { installedVersion = null; }
|
|
686
|
+
if (launcherVersion && installedVersion && compareVersions(installedVersion, launcherVersion) > 0) {
|
|
687
|
+
log('');
|
|
688
|
+
log(`- note: managed payload ${installedVersion} is newer than the global launcher ${launcherVersion}; the payload remains authoritative.`);
|
|
689
|
+
log(` Refresh the launcher when convenient: npm install -g ${UPDATE_PACKAGE_NAME}@${installedVersion}`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Resolve an update candidate WITHOUT a source checkout:
|
|
695
|
+
* - explicit candidate path: a payload directory or a packed .tgz file;
|
|
696
|
+
* - otherwise npm registry mode: `npm pack <spec>` using the user's own
|
|
697
|
+
* configured registry/auth (no policy or config mutation), extracted to a
|
|
698
|
+
* disposable directory the caller cleans up via returned cleanup().
|
|
699
|
+
*/
|
|
700
|
+
export function resolveUpdateCandidate({
|
|
701
|
+
candidate,
|
|
702
|
+
spec = UPDATE_DEFAULT_SPEC,
|
|
703
|
+
home = homedir(),
|
|
704
|
+
log = () => {},
|
|
705
|
+
runner = spawnSync,
|
|
706
|
+
} = {}) {
|
|
707
|
+
const candidateValue = candidate ?? process.env.DSH_CREW_CANDIDATE ?? undefined;
|
|
708
|
+
if (candidateValue !== undefined && typeof candidateValue !== 'string') {
|
|
709
|
+
return { ok: false, code: 'INVALID_CANDIDATE', detail: 'candidate must be a directory or .tgz path' };
|
|
710
|
+
}
|
|
711
|
+
if (typeof candidateValue === 'string' && candidateValue.trim()) {
|
|
712
|
+
const value = resolve(candidateValue.trim());
|
|
713
|
+
if (!existsSync(value)) return { ok: false, code: 'CANDIDATE_NOT_FOUND', detail: value };
|
|
714
|
+
if (existsSync(join(value, 'package.json'))) {
|
|
715
|
+
const manifest = readManifest(value);
|
|
716
|
+
if (!manifest?.name || !manifest?.version) return { ok: false, code: 'CANDIDATE_MANIFEST_INVALID', detail: value };
|
|
717
|
+
return { ok: true, sourceRoot: value, version: manifest.version, cleanup: null };
|
|
718
|
+
}
|
|
719
|
+
if (/\.tgz$/i.test(value)) {
|
|
720
|
+
const tmpRoot = join(crewReleasesDir({ home }), `.candidate-${timestampStamp()}-${process.pid}`);
|
|
721
|
+
const extracted = extractPackageTarball(value, tmpRoot, { runner });
|
|
722
|
+
if (!extracted.ok) {
|
|
723
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
724
|
+
return { ok: false, code: 'CANDIDATE_EXTRACT_FAILED', detail: extracted.detail };
|
|
725
|
+
}
|
|
726
|
+
const manifest = readManifest(extracted.sourceRoot);
|
|
727
|
+
if (!manifest?.name || !manifest?.version) {
|
|
728
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
729
|
+
return { ok: false, code: 'CANDIDATE_MANIFEST_INVALID', detail: value };
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
ok: true,
|
|
733
|
+
sourceRoot: extracted.sourceRoot,
|
|
734
|
+
version: manifest.version,
|
|
735
|
+
cleanup: () => rmSync(tmpRoot, { recursive: true, force: true }),
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
return { ok: false, code: 'UNSUPPORTED_CANDIDATE', detail: value };
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Registry mode.
|
|
742
|
+
const tmpDir = join(crewReleasesDir({ home }), `.candidate-${timestampStamp()}-${process.pid}`);
|
|
743
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
744
|
+
try {
|
|
745
|
+
const effectiveSpec = process.env.DSH_CREW_UPDATE_SPEC ?? spec;
|
|
746
|
+
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
747
|
+
const packArgs = ['pack', effectiveSpec, '--pack-destination', tmpDir, '--json', '--loglevel=error'];
|
|
748
|
+
log(`- resolving update candidate from the configured npm registry (${effectiveSpec})`);
|
|
749
|
+
const packed = runner(npm, packArgs, {
|
|
750
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], shell: process.platform === 'win32',
|
|
751
|
+
timeout: 600_000, windowsHide: true, env: sanitizedPackageManagerEnv(),
|
|
752
|
+
});
|
|
753
|
+
if (packed.status !== 0) {
|
|
754
|
+
return { ok: false, code: 'REGISTRY_PACK_FAILED', detail: `${effectiveSpec}: ${(packed.stderr || packed.stdout || '').trim().slice(-300)}` };
|
|
755
|
+
}
|
|
756
|
+
let info;
|
|
757
|
+
try { info = JSON.parse(packed.stdout)[0]; } catch {
|
|
758
|
+
return { ok: false, code: 'REGISTRY_PACK_UNPARSEABLE', detail: String(packed.stdout).slice(-200) };
|
|
759
|
+
}
|
|
760
|
+
if (!info?.filename) return { ok: false, code: 'REGISTRY_PACK_UNPARSEABLE', detail: 'pack output missing filename' };
|
|
761
|
+
const tgzPath = join(tmpDir, info.filename);
|
|
762
|
+
const extracted = extractPackageTarball(tgzPath, join(tmpDir, 'x'), { runner });
|
|
763
|
+
if (!extracted.ok) {
|
|
764
|
+
return { ok: false, code: 'CANDIDATE_EXTRACT_FAILED', detail: extracted.detail };
|
|
765
|
+
}
|
|
766
|
+
const manifest = readManifest(extracted.sourceRoot);
|
|
767
|
+
if (!manifest?.name || !manifest?.version) {
|
|
768
|
+
return { ok: false, code: 'CANDIDATE_MANIFEST_INVALID', detail: info.filename };
|
|
769
|
+
}
|
|
770
|
+
if (info.name !== manifest.name || info.version !== manifest.version) {
|
|
771
|
+
return { ok: false, code: 'CANDIDATE_IDENTITY_MISMATCH', detail: `pack ${info.name}@${info.version} vs manifest ${manifest.name}@${manifest.version}` };
|
|
772
|
+
}
|
|
773
|
+
return {
|
|
774
|
+
ok: true,
|
|
775
|
+
sourceRoot: extracted.sourceRoot,
|
|
776
|
+
version: manifest.version,
|
|
777
|
+
cleanup: () => rmSync(tmpDir, { recursive: true, force: true }),
|
|
778
|
+
};
|
|
779
|
+
} catch (error) {
|
|
780
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
781
|
+
return { ok: false, code: 'REGISTRY_PACK_FAILED', detail: String(error?.message ?? error).slice(-300) };
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
export async function npxUpdate({
|
|
786
|
+
home = homedir(),
|
|
787
|
+
log = console.log,
|
|
788
|
+
sourceRoot,
|
|
789
|
+
candidate,
|
|
790
|
+
spec,
|
|
791
|
+
installer = realInstaller,
|
|
792
|
+
ensureRuntime,
|
|
793
|
+
npmInstaller,
|
|
794
|
+
runner = spawnSync,
|
|
795
|
+
} = {}) {
|
|
796
|
+
log('DSH Crew updater');
|
|
797
|
+
// Candidate resolution: explicit path/dir override > a newer validated
|
|
798
|
+
// running launcher > configured npm registry (@latest). This makes the
|
|
799
|
+
// supported legacy bootstrap (`npm install -g ...@latest`, then `update`)
|
|
800
|
+
// independent of registry propagation after the launcher refresh.
|
|
801
|
+
const explicitCandidate = candidate ?? sourceRoot;
|
|
802
|
+
const initialHealth = currentInstallationHealth({ home });
|
|
803
|
+
const launcherRoot = runningPackageRoot();
|
|
804
|
+
const launcherManifest = readManifest(launcherRoot);
|
|
805
|
+
const launcherCanConverge = explicitCandidate === undefined
|
|
806
|
+
&& initialHealth.pointer?.version
|
|
807
|
+
&& launcherManifest?.name === UPDATE_PACKAGE_NAME
|
|
808
|
+
&& launcherManifest?.version
|
|
809
|
+
&& compareVersions(launcherManifest.version, initialHealth.pointer.version) > 0;
|
|
810
|
+
let resolved;
|
|
811
|
+
if (launcherCanConverge) {
|
|
812
|
+
log(`- newer launcher ${launcherManifest.version}; converging managed payload ${initialHealth.pointer.version} before registry resolution`);
|
|
813
|
+
resolved = { ok: true, sourceRoot: launcherRoot, version: launcherManifest.version, cleanup: null };
|
|
814
|
+
} else {
|
|
815
|
+
resolved = resolveUpdateCandidate({ candidate: explicitCandidate, spec, home, log, runner });
|
|
816
|
+
}
|
|
817
|
+
if (!resolved.ok) {
|
|
818
|
+
log(`✗ candidate resolution failed (${resolved.code})${resolved.detail ? `: ${resolved.detail}` : ''}`);
|
|
819
|
+
return { ok: false, error: `candidate resolution failed (${resolved.code})` };
|
|
820
|
+
}
|
|
821
|
+
try {
|
|
822
|
+
const manifest = readManifest(resolved.sourceRoot);
|
|
823
|
+
if (!manifest?.name || !manifest?.version) return { ok: false, error: 'candidate package manifest invalid' };
|
|
824
|
+
|
|
825
|
+
const health = currentInstallationHealth({ home });
|
|
826
|
+
|
|
827
|
+
if (health.installed && health.healthy && health.pointer.version === manifest.version) {
|
|
828
|
+
log(`- already current (${manifest.version}); repairing registration/integrations idempotently`);
|
|
829
|
+
const activated = await activateRelease({ home, releaseDir: health.pointer.path, manifest, log, installer });
|
|
830
|
+
if (!activated) return { ok: false, error: 'activation failed' };
|
|
831
|
+
if (!await ensureRuntimeStep({ home, log, ensureRuntime })) return { ok: false, error: 'Crew DSH runtime bootstrap failed' };
|
|
832
|
+
log('');
|
|
833
|
+
log('Done.');
|
|
834
|
+
return { ok: true, idempotent: true, version: manifest.version, path: health.pointer.path };
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (health.installed && health.healthy && compareVersions(manifest.version, health.pointer.version) < 0) {
|
|
838
|
+
const source = explicitCandidate === undefined && !launcherCanConverge ? 'registry latest' : 'candidate';
|
|
839
|
+
log(`- ${source} (${manifest.version}) is not newer than the installed payload (${health.pointer.version}); nothing to update`);
|
|
840
|
+
const activated = await activateRelease({ home, releaseDir: health.pointer.path, manifest: readManifest(health.pointer.path), log, installer });
|
|
841
|
+
if (!activated) return { ok: false, error: 'activation failed' };
|
|
842
|
+
return { ok: true, idempotent: true, version: health.pointer.version, path: health.pointer.path };
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Stale, unhealthy, older, or missing installation: stage the candidate
|
|
846
|
+
// fully and validate it before switching. The previous usable release is
|
|
847
|
+
// left in place until the replacement has been committed and activated.
|
|
848
|
+
if (health.installed && !health.healthy) {
|
|
849
|
+
log('- existing installation is stale or incomplete; repairing via fresh candidate staging');
|
|
850
|
+
} else if (health.installed) {
|
|
851
|
+
log(`- updating managed payload ${health.pointer.version} -> ${manifest.version}`);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const staged = stageCandidatePayload({ sourceRoot: resolved.sourceRoot, home, log, npmInstaller });
|
|
855
|
+
if (!staged.ok) {
|
|
856
|
+
log(`✗ staging failed (${staged.code})${staged.detail ? `: ${staged.detail.join('; ')}` : ''}`);
|
|
857
|
+
return { ok: false, error: `staging failed (${staged.code})` };
|
|
858
|
+
}
|
|
859
|
+
log(`✓ candidate payload staged and validated (${manifest.version})`);
|
|
860
|
+
|
|
861
|
+
const releaseDir = commitStagedRelease({ stageDir: staged.stageDir, manifest, home });
|
|
862
|
+
log('✓ durable release committed under Crew-owned state');
|
|
863
|
+
|
|
864
|
+
const activated = await activateRelease({ home, releaseDir, manifest, log, installer });
|
|
865
|
+
if (!activated) return { ok: false, error: 'activation failed' };
|
|
866
|
+
|
|
867
|
+
if (!await ensureRuntimeStep({ home, log, ensureRuntime })) return { ok: false, error: 'Crew DSH runtime bootstrap failed' };
|
|
868
|
+
|
|
869
|
+
noteLauncherDivergence({ log, home });
|
|
870
|
+
log('');
|
|
871
|
+
log('Done.');
|
|
872
|
+
log('Restart DeepSeek Harness and Codex Desktop.');
|
|
873
|
+
return { ok: true, updated: true, version: manifest.version, path: releaseDir };
|
|
874
|
+
} finally {
|
|
875
|
+
resolved.cleanup?.();
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Compare dotted numeric versions; returns -1/0/1. Non-numeric segments fall
|
|
881
|
+
* back to string comparison so prerelease identifiers stay deterministic.
|
|
882
|
+
*/
|
|
883
|
+
export function compareVersions(left, right) {
|
|
884
|
+
const asParts = (value) => String(value ?? '').split('.');
|
|
885
|
+
const a = asParts(left);
|
|
886
|
+
const b = asParts(right);
|
|
887
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
888
|
+
const x = a[index] ?? '0';
|
|
889
|
+
const y = b[index] ?? '0';
|
|
890
|
+
const xn = /^\d+$/.test(x);
|
|
891
|
+
const yn = /^\d+$/.test(y);
|
|
892
|
+
if (xn && yn) {
|
|
893
|
+
const diff = Number(x) - Number(y);
|
|
894
|
+
if (diff !== 0) return diff < 0 ? -1 : 1;
|
|
895
|
+
} else if (xn !== yn) {
|
|
896
|
+
// npm semantics: a numeric release segment outranks a prerelease segment.
|
|
897
|
+
return xn ? 1 : -1;
|
|
898
|
+
} else if (x !== y) {
|
|
899
|
+
return x < y ? -1 : 1;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return 0;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
export function npxStatus({
|
|
906
|
+
home = homedir(),
|
|
907
|
+
log = console.log,
|
|
908
|
+
sourceRoot,
|
|
909
|
+
installer = realInstaller,
|
|
910
|
+
} = {}) {
|
|
911
|
+
const candidateRoot = sourceRoot ?? runningPackageRoot();
|
|
912
|
+
const manifest = readManifest(candidateRoot);
|
|
913
|
+
const candidateVersion = manifest?.version ?? null;
|
|
914
|
+
|
|
915
|
+
const pointer = readCurrentPointer({ home });
|
|
916
|
+
let installedLine = 'not installed';
|
|
917
|
+
let installedVersion = null;
|
|
918
|
+
if (pointer) {
|
|
919
|
+
if (existsSync(pointer.path)) {
|
|
920
|
+
const validated = validateInstalledPayload(pointer.path, { expectedName: pointer.name, expectedVersion: pointer.version });
|
|
921
|
+
if (validated.ok) {
|
|
922
|
+
installedVersion = pointer.version;
|
|
923
|
+
installedLine = `${pointer.version} (${pointer.path})`;
|
|
924
|
+
} else {
|
|
925
|
+
installedVersion = pointer.version;
|
|
926
|
+
installedLine = `${pointer.version} at ${pointer.path} (unverifiable/damaged)`;
|
|
927
|
+
}
|
|
928
|
+
} else {
|
|
929
|
+
installedLine = `pointer references missing payload (${pointer.path})`;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Read-only DSH plugin state from the dedicated Crew profile only; the
|
|
934
|
+
// official web profile layout is intentionally never inspected.
|
|
935
|
+
let dshPlugin = 'not installed';
|
|
936
|
+
const profilePkgFile = join(crewProfileDir({ home }), 'package.json');
|
|
937
|
+
const packageName = manifest?.name ?? pointer?.name ?? null;
|
|
938
|
+
if (packageName && existsSync(profilePkgFile)) {
|
|
939
|
+
try {
|
|
940
|
+
const pkg = JSON.parse(readFileSync(profilePkgFile, 'utf8'));
|
|
941
|
+
const listed = Boolean(pkg.dependencies?.[packageName]) || Boolean(pkg.dsh?.profile?.bundles?.includes?.(packageName));
|
|
942
|
+
const linkOk = packageName && existsSync(registrationLinkPath({ home, name: packageName }));
|
|
943
|
+
dshPlugin = listed && linkOk ? 'installed' : listed ? 'registered but payload link missing' : 'not installed';
|
|
944
|
+
} catch { dshPlugin = 'unknown'; }
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const st = installer.installStatus ? installer.installStatus({ home }) : realInstaller.installStatus({ home });
|
|
948
|
+
const codex = st?.codex?.installed ? 'installed' : 'not installed';
|
|
949
|
+
const claude = st?.claude?.installed ? 'installed' : 'not installed';
|
|
950
|
+
const official = officialWebIntegrationStatus({ home, releaseDir: pointer?.path });
|
|
951
|
+
const officialWeb = !official.enabled ? 'disabled' : official.healthy ? 'installed' : 'needs repair';
|
|
952
|
+
|
|
953
|
+
log(`DSH Crew launcher/candidate: ${candidateVersion ?? 'unknown'}`);
|
|
954
|
+
log(`Installed DSH Crew payload: ${installedLine}`);
|
|
955
|
+
if (candidateVersion && installedVersion && candidateVersion !== installedVersion) {
|
|
956
|
+
const direction = compareVersions(candidateVersion, installedVersion);
|
|
957
|
+
if (direction > 0) {
|
|
958
|
+
log(`- launcher ${candidateVersion} is newer than the managed payload ${installedVersion}.`);
|
|
959
|
+
log(' Run: dsh-crew update');
|
|
960
|
+
} else {
|
|
961
|
+
log(`- managed payload ${installedVersion} is newer than the launcher ${candidateVersion}; the payload remains authoritative.`);
|
|
962
|
+
log(` Refresh the launcher with: npm install -g ${UPDATE_PACKAGE_NAME}@${installedVersion}`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile on 3210)`);
|
|
966
|
+
log(`Official 3080 UI bridge: ${officialWeb}`);
|
|
967
|
+
log(`Codex Desktop integration: ${codex}`);
|
|
968
|
+
log(`Claude Code integration: ${claude}`);
|
|
969
|
+
|
|
970
|
+
return {
|
|
971
|
+
ok: true,
|
|
972
|
+
candidateVersion,
|
|
973
|
+
installedVersion,
|
|
974
|
+
installedPath: pointer?.path ?? null,
|
|
975
|
+
dshPlugin,
|
|
976
|
+
officialWeb,
|
|
977
|
+
codex,
|
|
978
|
+
claude,
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
export async function npxUninstall({
|
|
983
|
+
home = homedir(),
|
|
984
|
+
purge = false,
|
|
985
|
+
log = console.log,
|
|
986
|
+
installer = realInstaller,
|
|
987
|
+
} = {}) {
|
|
988
|
+
log('DSH Crew uninstaller (npx-managed)');
|
|
989
|
+
const failures = [];
|
|
990
|
+
const fail = (text) => { log(`✗ ${text}`); failures.push(text); };
|
|
991
|
+
|
|
992
|
+
const pointer = readCurrentPointer({ home });
|
|
993
|
+
const name = pointer?.name ?? readManifest(runningPackageRoot())?.name;
|
|
994
|
+
|
|
995
|
+
const cx = installer.uninstallCodex({ home });
|
|
996
|
+
if (cx.ok !== false) log('✓ Codex Desktop integration removed');
|
|
997
|
+
else fail('Codex Desktop integration removal failed');
|
|
998
|
+
|
|
999
|
+
const cl = installer.uninstallClaudeCode ? await installer.uninstallClaudeCode({ home }) : realInstaller.uninstallClaudeCode({ home });
|
|
1000
|
+
if (cl.ok !== false) log('✓ Claude Code integration removed');
|
|
1001
|
+
else fail('Claude Code integration removal failed');
|
|
1002
|
+
|
|
1003
|
+
const official = removeOfficialWebIntegration({ home, preserveIntent: !purge, remember: !purge });
|
|
1004
|
+
if (!official.ok) fail(`official 3080 bridge removal failed (${official.code ?? 'unknown'})`);
|
|
1005
|
+
else log(official.removed ? '✓ official 3080 bridge removed' : '- official 3080 bridge already absent');
|
|
1006
|
+
|
|
1007
|
+
if (name) {
|
|
1008
|
+
const removed = removeCrewPluginRegistration({ home, name });
|
|
1009
|
+
if (!removed.ok) fail(`Harness registration removal failed (${removed.code ?? 'unknown'})`);
|
|
1010
|
+
else log(removed.removed ? '✓ Harness plugin registration removed (offline, Crew-owned state)' : '- Harness plugin registration already absent');
|
|
1011
|
+
} else {
|
|
1012
|
+
fail('installed package name unknown; registration not removed');
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
if (purge) {
|
|
1016
|
+
try {
|
|
1017
|
+
rmSync(join(home, '.config', 'dsh-crew'), { recursive: true, force: true });
|
|
1018
|
+
log('✓ ~/.config/dsh-crew purged');
|
|
1019
|
+
} catch { fail('could not purge ~/.config/dsh-crew'); }
|
|
1020
|
+
} else {
|
|
1021
|
+
// Remove ONLY the Crew-managed installed payload; config, credentials,
|
|
1022
|
+
// backups, and the isolated Harness home stay untouched.
|
|
1023
|
+
try {
|
|
1024
|
+
rmSync(crewAppRoot({ home }), { recursive: true, force: true });
|
|
1025
|
+
log('✓ Crew-managed installed payload removed (config/backups kept)');
|
|
1026
|
+
} catch { fail('could not remove the Crew-managed installed payload'); }
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if (failures.length > 0) {
|
|
1030
|
+
log('');
|
|
1031
|
+
log('FAILED: uninstall incomplete');
|
|
1032
|
+
return { ok: false, failures };
|
|
1033
|
+
}
|
|
1034
|
+
log('');
|
|
1035
|
+
log('Done.');
|
|
1036
|
+
return { ok: true, failures: [] };
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// ---- CLI dispatch --------------------------------------------------------------
|
|
1040
|
+
|
|
1041
|
+
export const USAGE = `usage: dsh-crew <command> [--purge] [--candidate <path>]
|
|
1042
|
+
|
|
1043
|
+
Commands:
|
|
1044
|
+
install persist the candidate package into Crew-owned state and register it
|
|
1045
|
+
integrate show Crew inside the official 3080 UI; backend stays isolated on 3210
|
|
1046
|
+
detach remove only the official 3080 bridge; isolated 3210 mode remains available
|
|
1047
|
+
status read-only report of launcher/installed versions and integrations
|
|
1048
|
+
update resolve the newest permitted package from the configured npm registry (or
|
|
1049
|
+
--candidate), stage and validate it, then activate; idempotent when current
|
|
1050
|
+
uninstall remove the Crew-managed payload, registration, and integrations (config kept)
|
|
1051
|
+
|
|
1052
|
+
Options:
|
|
1053
|
+
--candidate <path> update from a local payload directory or packed .tgz instead of the registry
|
|
1054
|
+
--purge with uninstall: also remove ~/.config/dsh-crew config/backups (destructive)
|
|
1055
|
+
--help show this help
|
|
1056
|
+
|
|
1057
|
+
Primary install: npm install -g @ran-sh/dsh-crew (then run: dsh-crew install)
|
|
1058
|
+
Source checkouts use scripts/setup.mjs instead.`;
|
|
1059
|
+
|
|
1060
|
+
function normalizeCommand(argv) {
|
|
1061
|
+
const flags = argv.slice(1);
|
|
1062
|
+
let candidate;
|
|
1063
|
+
for (let index = 0; index < flags.length; index += 1) {
|
|
1064
|
+
if (flags[index] === '--candidate') {
|
|
1065
|
+
candidate = flags[index + 1];
|
|
1066
|
+
flags.splice(index, 2);
|
|
1067
|
+
index -= 1;
|
|
1068
|
+
} else if (flags[index]?.startsWith('--candidate=')) {
|
|
1069
|
+
candidate = flags[index].slice('--candidate='.length);
|
|
1070
|
+
flags.splice(index, 1);
|
|
1071
|
+
index -= 1;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
const knownFlags = new Set(['--purge']);
|
|
1075
|
+
const unknown = flags.filter((f) => f.startsWith('--') && !knownFlags.has(f));
|
|
1076
|
+
return { command: argv[0], purge: flags.includes('--purge'), candidate, unknown };
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* CLI dispatcher used by bin/dsh-crew.mjs. Returns a process exit code.
|
|
1081
|
+
*/
|
|
1082
|
+
export async function runNpxCli({
|
|
1083
|
+
argv = process.argv.slice(2),
|
|
1084
|
+
log = console.log,
|
|
1085
|
+
error = console.error,
|
|
1086
|
+
commands = {},
|
|
1087
|
+
} = {}) {
|
|
1088
|
+
const { command, purge, candidate, unknown } = normalizeCommand(argv);
|
|
1089
|
+
if (command === '--help' || command === '-h' || command === 'help') {
|
|
1090
|
+
log(USAGE);
|
|
1091
|
+
return 0;
|
|
1092
|
+
}
|
|
1093
|
+
if (!command) {
|
|
1094
|
+
error(USAGE);
|
|
1095
|
+
return 1;
|
|
1096
|
+
}
|
|
1097
|
+
if (unknown.length > 0 || !['install', 'integrate', 'detach', 'status', 'update', 'uninstall'].includes(command)) {
|
|
1098
|
+
error(`unknown command: ${command ?? '<none>'}\n\n${USAGE}`);
|
|
1099
|
+
return 1;
|
|
1100
|
+
}
|
|
1101
|
+
try {
|
|
1102
|
+
const actions = {
|
|
1103
|
+
install: commands.install ?? npxInstall,
|
|
1104
|
+
integrate: commands.integrate ?? npxIntegrate,
|
|
1105
|
+
detach: commands.detach ?? npxDetach,
|
|
1106
|
+
status: commands.status ?? npxStatus,
|
|
1107
|
+
update: commands.update ?? npxUpdate,
|
|
1108
|
+
uninstall: commands.uninstall ?? npxUninstall,
|
|
1109
|
+
};
|
|
1110
|
+
let result;
|
|
1111
|
+
if (command === 'uninstall') result = await actions.uninstall({ purge, log });
|
|
1112
|
+
else if (command === 'update') result = await actions.update({ candidate, log });
|
|
1113
|
+
else result = await actions[command]({ log });
|
|
1114
|
+
return result?.ok === false ? 1 : 0;
|
|
1115
|
+
} catch (err) {
|
|
1116
|
+
error(`dsh-crew ${command} failed: ${err?.message ?? err}`);
|
|
1117
|
+
return 1;
|
|
1118
|
+
}
|
|
1119
|
+
}
|