orca-agent-cleanup 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +38 -22
- package/README.md +30 -12
- package/package.json +42 -42
- package/scripts/orca-agent-cleanup.mjs +221 -221
- package/scripts/orca-cleanup-core.mjs +1852 -1842
|
@@ -1,1842 +1,1852 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import { createHash, randomUUID } from 'node:crypto';
|
|
6
|
-
|
|
7
|
-
export const OFFICIAL_ORCA_SKILLS = Object.freeze([
|
|
8
|
-
'computer-use',
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'orca-
|
|
12
|
-
'orca-emulator
|
|
13
|
-
'orca-
|
|
14
|
-
'orca-
|
|
15
|
-
'
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'.
|
|
23
|
-
'.
|
|
24
|
-
'.
|
|
25
|
-
'.
|
|
26
|
-
'.
|
|
27
|
-
'.
|
|
28
|
-
'.
|
|
29
|
-
'.
|
|
30
|
-
'.
|
|
31
|
-
'.gemini/
|
|
32
|
-
'.
|
|
33
|
-
'.
|
|
34
|
-
'.
|
|
35
|
-
'.
|
|
36
|
-
'.
|
|
37
|
-
'.
|
|
38
|
-
'.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
'.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
'.
|
|
49
|
-
'.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
'
|
|
90
|
-
'
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
'
|
|
95
|
-
'
|
|
96
|
-
'
|
|
97
|
-
'
|
|
98
|
-
'
|
|
99
|
-
]);
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
'
|
|
105
|
-
'
|
|
106
|
-
'
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
'
|
|
113
|
-
'
|
|
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
|
-
return
|
|
145
|
-
} catch {
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function
|
|
151
|
-
try {
|
|
152
|
-
return
|
|
153
|
-
} catch {
|
|
154
|
-
return
|
|
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
|
-
|
|
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
|
-
['json
|
|
228
|
-
['
|
|
229
|
-
['
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
]
|
|
236
|
-
|
|
237
|
-
['json',
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
if (
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
return
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const
|
|
525
|
-
const
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
];
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
...
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
...
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
];
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
{ kind: '
|
|
601
|
-
{ kind: '
|
|
602
|
-
]),
|
|
603
|
-
...
|
|
604
|
-
{ kind: '
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
...
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
],
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
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
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
if (
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
if (
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
if (
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
}
|
|
848
|
-
return
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
} catch {
|
|
868
|
-
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
} catch {
|
|
878
|
-
return false;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
if (candidate.
|
|
897
|
-
if (candidate.type === '
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
if (candidate.type === '
|
|
908
|
-
if (candidate.type === '
|
|
909
|
-
if (candidate.type === '
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
function
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
function
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
const
|
|
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
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
if (
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
if (
|
|
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
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
if (
|
|
1070
|
-
findings.push(finding('
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
function
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
function
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
const
|
|
1197
|
-
const
|
|
1198
|
-
const
|
|
1199
|
-
if (
|
|
1200
|
-
const backup = backupConfig(target, context, dryRun);
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
const
|
|
1208
|
-
const
|
|
1209
|
-
if (
|
|
1210
|
-
const backup = backupConfig(target, context, dryRun);
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
const
|
|
1227
|
-
|
|
1228
|
-
return
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
const
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
const
|
|
1253
|
-
if (!
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
if (
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
if (
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
return
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
function
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
const
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
const backup =
|
|
1474
|
-
action = { action: dryRun ? 'would-
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
action
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
if (!
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
if (
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
if (!
|
|
1564
|
-
throw new Error(
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
if (!
|
|
1571
|
-
throw new Error(
|
|
1572
|
-
}
|
|
1573
|
-
if (
|
|
1574
|
-
throw new Error(`manifest
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
throw new Error('manifest
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
if (
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
const
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
if (
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
if (
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
throw
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
const
|
|
1738
|
-
const
|
|
1739
|
-
|
|
1740
|
-
: null;
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
const
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
export const OFFICIAL_ORCA_SKILLS = Object.freeze([
|
|
8
|
+
'computer-use',
|
|
9
|
+
'jira-tickets',
|
|
10
|
+
'linear-tickets',
|
|
11
|
+
'orca-cli',
|
|
12
|
+
'orca-emulator',
|
|
13
|
+
'orca-emulator-android',
|
|
14
|
+
'orca-jira',
|
|
15
|
+
'orca-linear',
|
|
16
|
+
'orca-per-workspace-env',
|
|
17
|
+
'orchestration',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
// Orca src/main/skills/skill-discovery-sources.ts 기준 사용자 홈 스킬 루트.
|
|
21
|
+
export const HOME_SKILL_ROOTS = Object.freeze([
|
|
22
|
+
'.codex/skills',
|
|
23
|
+
'.agents/skills',
|
|
24
|
+
'.claude/skills',
|
|
25
|
+
'.grok/skills',
|
|
26
|
+
'.config/opencode/skills',
|
|
27
|
+
'.pi/agent/skills',
|
|
28
|
+
'.omp/agent/skills',
|
|
29
|
+
'.hermes/skills',
|
|
30
|
+
'.prime/agent/skills',
|
|
31
|
+
'.gemini/skills',
|
|
32
|
+
'.gemini/config/skills',
|
|
33
|
+
'.gemini/antigravity/skills',
|
|
34
|
+
'.cursor/skills',
|
|
35
|
+
'.factory/skills',
|
|
36
|
+
'.continue/skills',
|
|
37
|
+
'.trae-cn/skills',
|
|
38
|
+
'.augment/skills',
|
|
39
|
+
'.config/muse/skills',
|
|
40
|
+
'.zcode/skills',
|
|
41
|
+
'.codebuddy/skills',
|
|
42
|
+
'.kiro/skills',
|
|
43
|
+
'.openclaude/skills',
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
// Orca가 프로젝트에서 추가로 검사하는 스킬 루트.
|
|
47
|
+
export const PROJECT_SKILL_ROOTS = Object.freeze([
|
|
48
|
+
'.agents/skills',
|
|
49
|
+
'.claude/skills',
|
|
50
|
+
'.factory/skills',
|
|
51
|
+
'.continue/skills',
|
|
52
|
+
'.trae/skills',
|
|
53
|
+
'.grok/skills',
|
|
54
|
+
'.augment/skills',
|
|
55
|
+
'.codebuddy/skills',
|
|
56
|
+
'.kiro/skills',
|
|
57
|
+
'.openclaude/skills',
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const LINUX_CLI_DISPATCHER_MARKER = '# orca-serve-bare-orca-dispatcher';
|
|
61
|
+
const LINUX_CLI_COMMAND_NAME = 'orca-ide';
|
|
62
|
+
const PI_EXTENSION_MARKER = '@orca-managed-pi-extension';
|
|
63
|
+
const PLUGIN_CACHE_MAX_DEPTH = 9;
|
|
64
|
+
const PLUGIN_CACHE_MAX_ENTRIES = 16_384;
|
|
65
|
+
const PLUGIN_CACHE_MAX_CANDIDATES = 64;
|
|
66
|
+
const CODEX_LEGACY_START = '# BEGIN ORCA AGENT STATUS HOOKS';
|
|
67
|
+
const CODEX_LEGACY_END = '# END ORCA AGENT STATUS HOOKS';
|
|
68
|
+
|
|
69
|
+
const ORCA_SOURCE_PATTERN = /(?:^|[\s/:])stablyai\/orca(?:\.git)?(?:$|[\s/#?])/i;
|
|
70
|
+
const ORCA_SKILL_SIGNATURES = [
|
|
71
|
+
ORCA_SOURCE_PATTERN,
|
|
72
|
+
/\bORCA\s+skills\s+get\b/i,
|
|
73
|
+
/\bEngage\s+Orca\b/i,
|
|
74
|
+
/Use Orca(?:'s|’s)? computer-use CLI/i,
|
|
75
|
+
];
|
|
76
|
+
const ORCA_HOOK_PATTERN = /(?:[\\/]\.orca(?:-wsl|-relay)?[\\/]agent-hooks[\\/]|ORCA_AGENT_HOOK)/i;
|
|
77
|
+
const MANAGED_FILE_PATTERN = /Managed by Orca\. Do not edit; changes may be overwritten\./i;
|
|
78
|
+
const KIMI_START = '# >>> orca-managed-kimi-hooks (managed by Orca; do not edit) >>>';
|
|
79
|
+
const KIMI_END = '# <<< orca-managed-kimi-hooks <<<';
|
|
80
|
+
// Orca src/main/dsh/dsh-home-patch.ts의 관리 블록 표식.
|
|
81
|
+
const DSH_START = '# >>> orca-managed-dsh-hooks (managed by Orca; do not edit) >>>';
|
|
82
|
+
const DSH_END = '# <<< orca-managed-dsh-hooks <<<';
|
|
83
|
+
// 경로 구분자를 '/'로 정규화한 뒤 검사한다. PowerShell 명령은 '.orca\agent-hooks\'처럼
|
|
84
|
+
// 앞에 구분자 없이 따옴표로 시작하므로 ORCA_HOOK_PATTERN만으로는 놓친다.
|
|
85
|
+
const ORCA_HOOK_PATH_PATTERN = /(?:^|[^A-Za-z0-9_.-])\.orca(?:-wsl|-relay)?\/agent-hooks\//i;
|
|
86
|
+
// Orca src/main/muse/hook-config-json.ts의 MUSE_MANAGED_HOOK_ENV_VARS 중 Orca 전용 이름.
|
|
87
|
+
// USERPROFILE은 사용자가 직접 넣었을 수도 있어 남긴다.
|
|
88
|
+
const MUSE_ORCA_ENV_VARS = new Set([
|
|
89
|
+
'ORCA_AGENT_HOOK_PORT',
|
|
90
|
+
'ORCA_AGENT_HOOK_TOKEN',
|
|
91
|
+
'ORCA_AGENT_HOOK_ENV',
|
|
92
|
+
'ORCA_AGENT_HOOK_VERSION',
|
|
93
|
+
'ORCA_AGENT_HOOK_TRANSPORT',
|
|
94
|
+
'ORCA_AGENT_HOOK_ENDPOINT',
|
|
95
|
+
'ORCA_PANE_KEY',
|
|
96
|
+
'ORCA_TAB_ID',
|
|
97
|
+
'ORCA_WORKTREE_ID',
|
|
98
|
+
'ORCA_AGENT_LAUNCH_TOKEN',
|
|
99
|
+
]);
|
|
100
|
+
// Orca가 사용자 OpenCode 설정 폴더의 plugins/에 직접 쓰는 상태 플러그인.
|
|
101
|
+
const OPENCODE_PLUGIN_ENTRIES = Object.freeze([
|
|
102
|
+
'orca-opencode-status.js',
|
|
103
|
+
'orca-opencode2-status.js',
|
|
104
|
+
'orca-opencode-status-tui',
|
|
105
|
+
'orca-opencode2-status-tui',
|
|
106
|
+
'orca-opencode-startup-prompt',
|
|
107
|
+
]);
|
|
108
|
+
const OPENCODE_PLUGIN_SIGNATURE = /ORCA_OPENCODE_AGENT|ORCA_STATUS_AGENT|["']orca-opencode(?:2-status|-status|-startup-prompt)["']/;
|
|
109
|
+
const OPENCODE_TUI_ENTRY_PATTERN = /[\\/]plugins[\\/](?:orca-opencode2?-status-tui|orca-opencode-startup-prompt)[\\/]tui\.js$/i;
|
|
110
|
+
// ~/.orca 안의 Orca 전용 자격증명. Orca 앱 키로 암호화돼 앱 없이는 쓸 수 없다.
|
|
111
|
+
const ORCA_CREDENTIAL_FILES = Object.freeze([
|
|
112
|
+
'openai-speech-token.enc',
|
|
113
|
+
'bitbucket-credential.enc',
|
|
114
|
+
'bitbucket-credential.json',
|
|
115
|
+
'linear-token.enc',
|
|
116
|
+
'linear-workspaces.json',
|
|
117
|
+
'jira-tokens',
|
|
118
|
+
'jira-sites.json',
|
|
119
|
+
'zcode-plan-api-key.enc',
|
|
120
|
+
'opencode-go-api-key.enc',
|
|
121
|
+
'minimax-api-key.enc',
|
|
122
|
+
'minimax-session-cookie.enc',
|
|
123
|
+
]);
|
|
124
|
+
const ORCA_VOICE_MODEL_IDS = new Set([
|
|
125
|
+
'parakeet-tdt-0.6b-v3-int8',
|
|
126
|
+
'parakeet-tdt-0.6b-v2-int8',
|
|
127
|
+
'zipformer-bilingual-zh-en',
|
|
128
|
+
'paraformer-bilingual-zh-en',
|
|
129
|
+
'zipformer-streaming-en-20m',
|
|
130
|
+
'zipformer-streaming-zh-14m',
|
|
131
|
+
'zipformer-streaming-korean',
|
|
132
|
+
'parakeet-tdt-ctc-0.6b-ja-int8',
|
|
133
|
+
'whisper-tiny',
|
|
134
|
+
'sense-voice-zh-en-ja-ko-yue',
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
function unique(values) {
|
|
138
|
+
return [...new Set(values.filter(Boolean).map((value) => path.resolve(value)))];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function pathExists(target) {
|
|
142
|
+
try {
|
|
143
|
+
fs.lstatSync(target);
|
|
144
|
+
return true;
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function readText(target) {
|
|
151
|
+
try {
|
|
152
|
+
return fs.readFileSync(target, 'utf8');
|
|
153
|
+
} catch {
|
|
154
|
+
return '';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function readJson(target) {
|
|
159
|
+
try {
|
|
160
|
+
return JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Orca createManagedCommandMatcher와 같은 방식: -EncodedCommand(UTF-16LE base64)를 풀어 함께 본다.
|
|
167
|
+
function hookSearchText(value) {
|
|
168
|
+
const decoded = [...value.matchAll(/\s-EncodedCommand\s+([A-Za-z0-9+/=]+)/gi)].map((match) => {
|
|
169
|
+
try {
|
|
170
|
+
return Buffer.from(match[1], 'base64').toString('utf16le');
|
|
171
|
+
} catch {
|
|
172
|
+
return '';
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
return [value, ...decoded].join('\n').replace(/[\\/]+/g, '/');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isOrcaHookString(value) {
|
|
179
|
+
if (typeof value !== 'string') return false;
|
|
180
|
+
return ORCA_HOOK_PATTERN.test(value) || ORCA_HOOK_PATH_PATTERN.test(hookSearchText(value));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function defaultHomeSkillRoots(home) {
|
|
184
|
+
return HOME_SKILL_ROOTS.map((relative) => path.join(home, ...relative.split('/')));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function existingFilesystemIdentity(target) {
|
|
188
|
+
try {
|
|
189
|
+
const stat = fs.lstatSync(target);
|
|
190
|
+
if (stat.isSymbolicLink()) return `link:${path.resolve(target)}`;
|
|
191
|
+
const real = fs.realpathSync.native(target);
|
|
192
|
+
return `real:${process.platform === 'win32' ? real.toLowerCase() : real}`;
|
|
193
|
+
} catch {
|
|
194
|
+
return `path:${path.resolve(target)}`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function environmentSkillRoots(platform, home, env) {
|
|
199
|
+
const roots = [
|
|
200
|
+
env.CLAUDE_CONFIG_DIR && path.join(env.CLAUDE_CONFIG_DIR, 'skills'),
|
|
201
|
+
env.GROK_HOME && path.join(env.GROK_HOME, 'skills'),
|
|
202
|
+
env.HERMES_HOME && path.join(env.HERMES_HOME, 'skills'),
|
|
203
|
+
env.KIRO_HOME && path.join(env.KIRO_HOME, 'skills'),
|
|
204
|
+
env.OPENCLAUDE_CONFIG_DIR && path.join(env.OPENCLAUDE_CONFIG_DIR, 'skills'),
|
|
205
|
+
];
|
|
206
|
+
if (platform === 'win32' && env.LOCALAPPDATA) {
|
|
207
|
+
roots.push(path.join(env.LOCALAPPDATA, 'hermes', 'skills'));
|
|
208
|
+
}
|
|
209
|
+
return unique(roots);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function defaultProjectSkillRoots(projects) {
|
|
213
|
+
return projects.flatMap((project) => PROJECT_SKILL_ROOTS.map((relative) => path.join(project, ...relative.split('/'))));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function defaultHookCandidates(home, env) {
|
|
217
|
+
const kimiHome = env.KIMI_CODE_HOME || path.join(home, '.kimi-code');
|
|
218
|
+
const hermesHomes = unique([
|
|
219
|
+
env.HERMES_HOME,
|
|
220
|
+
path.join(home, '.hermes'),
|
|
221
|
+
env.LOCALAPPDATA && path.join(env.LOCALAPPDATA, 'hermes'),
|
|
222
|
+
]);
|
|
223
|
+
const configHomes = unique([env.XDG_CONFIG_HOME, path.join(home, '.config')]);
|
|
224
|
+
const codexHomes = unique([env.CODEX_HOME, path.join(home, '.codex')]);
|
|
225
|
+
const kiroAgentsDir = path.join(env.KIRO_HOME || path.join(home, '.kiro'), 'agents');
|
|
226
|
+
const candidates = [
|
|
227
|
+
['json', path.join(home, '.claude', 'settings.json')],
|
|
228
|
+
['json', path.join(home, '.openclaude', 'settings.json')],
|
|
229
|
+
...codexHomes.map((codexHome) => ['json', path.join(codexHome, 'hooks.json')]),
|
|
230
|
+
// Orca가 Claude 형식 설정을 그대로 쓰는 에이전트(src/main/claude/hook-settings.ts).
|
|
231
|
+
['json', path.join(home, '.qoder', 'settings.json')],
|
|
232
|
+
['json', path.join(home, '.qoder-cn', 'settings.json')],
|
|
233
|
+
['json', path.join(home, '.qwen', 'settings.json')],
|
|
234
|
+
['json', path.join(home, '.codebuddy', 'settings.json')],
|
|
235
|
+
['json', path.join(home, '.zcode', 'cli', 'config.json')],
|
|
236
|
+
// Kiro는 전역 훅 설정이 없어 Orca가 에이전트 파일마다 훅을 넣는다.
|
|
237
|
+
['json-dir', kiroAgentsDir],
|
|
238
|
+
['jcode', path.join(env.JCODE_HOME || path.join(home, '.jcode'), 'config.toml')],
|
|
239
|
+
['dsh', path.join(env.DSH_HOME || path.join(home, '.dsh'), 'cordis.patch.yml')],
|
|
240
|
+
...configHomes.flatMap((configHome) => [
|
|
241
|
+
['muse', path.join(configHome, 'muse', 'settings.json')],
|
|
242
|
+
['opencode-tui', path.join(configHome, 'opencode', 'tui.json')],
|
|
243
|
+
['opencode-tui', path.join(configHome, 'opencode', 'tui.jsonc')],
|
|
244
|
+
...OPENCODE_PLUGIN_ENTRIES.map((name) => ['opencode-plugin', path.join(configHome, 'opencode', 'plugins', name)]),
|
|
245
|
+
]),
|
|
246
|
+
['json', path.join(home, '.gemini', 'settings.json')],
|
|
247
|
+
['json', path.join(home, '.gemini', 'config', 'hooks.json')],
|
|
248
|
+
['json', path.join(home, '.cursor', 'hooks.json')],
|
|
249
|
+
['json', path.join(home, '.commandcode', 'settings.json')],
|
|
250
|
+
['json', path.join(home, '.factory', 'settings.json')],
|
|
251
|
+
// Orca가 통째로 만드는 파일. Orca 항목만 있던 파일은 빈 껍데기를 남기지 않고 격리한다.
|
|
252
|
+
['json', path.join(env.COPILOT_HOME || path.join(home, '.copilot'), 'hooks', 'orca.json'), true],
|
|
253
|
+
['json', path.join(env.GROK_HOME || path.join(home, '.grok'), 'hooks', 'orca-status.json'), true],
|
|
254
|
+
['json', path.join(home, '.config', 'devin', 'config.json')],
|
|
255
|
+
['kimi', path.join(kimiHome, 'config.toml')],
|
|
256
|
+
['amp', path.join(home, '.config', 'amp', 'plugins', 'orca-agent-status.ts')],
|
|
257
|
+
['codex-legacy', path.join(home, '.codex', 'orca-agent-status.config.toml')],
|
|
258
|
+
['codex-legacy', path.join(home, '.codex', 'config.toml')],
|
|
259
|
+
];
|
|
260
|
+
for (const hermesHome of hermesHomes) {
|
|
261
|
+
candidates.push(['hermes', path.join(hermesHome, 'plugins', 'orca-status')]);
|
|
262
|
+
candidates.push(['hermes-config', path.join(hermesHome, 'config.yaml')]);
|
|
263
|
+
}
|
|
264
|
+
if (env.APPDATA) candidates.push(['json', path.join(env.APPDATA, 'Devin', 'config.json')]);
|
|
265
|
+
return candidates.map(([type, filePath, ownedFile = false]) => ({ type, path: filePath, ownedFile }));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function defaultExtensionCandidates(home) {
|
|
269
|
+
const files = [
|
|
270
|
+
['.pi', 'orca-agent-status.ts'],
|
|
271
|
+
['.pi', 'orca-prefill.ts'],
|
|
272
|
+
['.pi', 'orca-titlebar-spinner.ts'],
|
|
273
|
+
['.omp', 'orca-agent-status.ts'],
|
|
274
|
+
['.omp', 'orca-prefill.ts'],
|
|
275
|
+
['.omp', 'orca-titlebar-spinner.ts'],
|
|
276
|
+
['.prime', 'orca-agent-status.ts'],
|
|
277
|
+
];
|
|
278
|
+
return files.map(([agent, name]) => path.join(home, agent, 'agent', 'extensions', name));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function defaultAppDataCandidates(platform, home, env) {
|
|
282
|
+
if (platform === 'darwin') {
|
|
283
|
+
return [
|
|
284
|
+
path.join(home, 'Library', 'Application Support', 'Orca'),
|
|
285
|
+
path.join(home, 'Library', 'Application Support', 'orca'),
|
|
286
|
+
path.join(home, 'Library', 'Caches', 'com.stablyai.orca'),
|
|
287
|
+
path.join(home, 'Library', 'Caches', 'com.stablyai.orca.ShipIt'),
|
|
288
|
+
path.join(home, 'Library', 'Caches', 'orca-updater'),
|
|
289
|
+
path.join(home, 'Library', 'HTTPStorages', 'com.stablyai.orca'),
|
|
290
|
+
path.join(home, 'Library', 'Preferences', 'com.stablyai.orca.plist'),
|
|
291
|
+
path.join(home, 'Library', 'Saved Application State', 'com.stablyai.orca.savedState'),
|
|
292
|
+
];
|
|
293
|
+
}
|
|
294
|
+
if (platform === 'win32') {
|
|
295
|
+
return [
|
|
296
|
+
env.APPDATA && path.join(env.APPDATA, 'Orca'),
|
|
297
|
+
env.LOCALAPPDATA && path.join(env.LOCALAPPDATA, 'Orca'),
|
|
298
|
+
env.LOCALAPPDATA && path.join(env.LOCALAPPDATA, 'orca-updater'),
|
|
299
|
+
].filter(Boolean);
|
|
300
|
+
}
|
|
301
|
+
const configHome = env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
302
|
+
const cacheHome = env.XDG_CACHE_HOME || path.join(home, '.cache');
|
|
303
|
+
return unique([
|
|
304
|
+
path.join(configHome, 'Orca'),
|
|
305
|
+
path.join(configHome, 'orca'),
|
|
306
|
+
path.join(cacheHome, 'Orca'),
|
|
307
|
+
path.join(cacheHome, 'orca', 'appimage'),
|
|
308
|
+
path.join(cacheHome, 'orca-updater'),
|
|
309
|
+
path.join(home, '.config', 'Orca'),
|
|
310
|
+
path.join(home, '.config', 'orca'),
|
|
311
|
+
path.join(home, '.cache', 'Orca'),
|
|
312
|
+
path.join(home, '.cache', 'orca', 'appimage'),
|
|
313
|
+
path.join(home, '.cache', 'orca-updater'),
|
|
314
|
+
]);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function pluginCacheOrcaSkills(root) {
|
|
318
|
+
if (!pathExists(root)) return [];
|
|
319
|
+
let resolvedRoot;
|
|
320
|
+
try {
|
|
321
|
+
resolvedRoot = fs.realpathSync.native(root);
|
|
322
|
+
} catch {
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
const candidates = [];
|
|
326
|
+
const visited = new Set();
|
|
327
|
+
let entriesSeen = 0;
|
|
328
|
+
|
|
329
|
+
function visit(directory, depth) {
|
|
330
|
+
if (depth > PLUGIN_CACHE_MAX_DEPTH
|
|
331
|
+
|| entriesSeen >= PLUGIN_CACHE_MAX_ENTRIES
|
|
332
|
+
|| candidates.length >= PLUGIN_CACHE_MAX_CANDIDATES) return;
|
|
333
|
+
let resolved;
|
|
334
|
+
let entries;
|
|
335
|
+
try {
|
|
336
|
+
resolved = fs.realpathSync.native(directory);
|
|
337
|
+
if (!isSameOrDescendant(resolvedRoot, resolved) || visited.has(resolved)) return;
|
|
338
|
+
visited.add(resolved);
|
|
339
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
340
|
+
} catch {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
entriesSeen += entries.length;
|
|
344
|
+
if (entriesSeen > PLUGIN_CACHE_MAX_ENTRIES) return;
|
|
345
|
+
|
|
346
|
+
if (path.basename(directory) === 'skills') {
|
|
347
|
+
for (const name of OFFICIAL_ORCA_SKILLS) {
|
|
348
|
+
const candidate = path.join(directory, name);
|
|
349
|
+
if (pathExists(candidate) && hasOrcaSkillSignature(candidate)) candidates.push(candidate);
|
|
350
|
+
if (candidates.length >= PLUGIN_CACHE_MAX_CANDIDATES) return;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
for (const entry of entries) {
|
|
354
|
+
if (entry.name === 'node_modules' || entry.isSymbolicLink() || !entry.isDirectory()) continue;
|
|
355
|
+
visit(path.join(directory, entry.name), depth + 1);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
visit(resolvedRoot, 0);
|
|
360
|
+
return unique(candidates);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function defaultVoiceCandidates(platform, env) {
|
|
364
|
+
if (platform !== 'win32') return [];
|
|
365
|
+
const roots = [
|
|
366
|
+
env.PROGRAMDATA,
|
|
367
|
+
env.ProgramData,
|
|
368
|
+
env.ALLUSERSPROFILE,
|
|
369
|
+
env.PUBLIC && path.join(env.PUBLIC, 'Documents'),
|
|
370
|
+
env.PUBLIC,
|
|
371
|
+
env.SystemDrive && path.join(env.SystemDrive, 'ProgramData'),
|
|
372
|
+
'C:\\ProgramData',
|
|
373
|
+
];
|
|
374
|
+
return unique(roots.map((root) => root && path.join(root, 'Orca', 'speech-models')));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function readWindowsUserPath(platform) {
|
|
378
|
+
if (platform !== 'win32' || process.platform !== 'win32') return '';
|
|
379
|
+
const script = "[Console]::OutputEncoding=[Text.UTF8Encoding]::new(); [Environment]::GetEnvironmentVariable('Path','User')";
|
|
380
|
+
const result = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], { encoding: 'utf8' });
|
|
381
|
+
return result.status === 0 ? result.stdout.trim() : '';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function decodeProcessOutput(value) {
|
|
385
|
+
if (!value) return '';
|
|
386
|
+
const buffer = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
387
|
+
const hasNul = buffer.subarray(0, Math.min(buffer.length, 256)).includes(0);
|
|
388
|
+
return buffer.toString(hasNul ? 'utf16le' : 'utf8').replace(/^\uFEFF/, '');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function discoverRunningWslHomes(platform) {
|
|
392
|
+
if (platform !== 'win32' || process.platform !== 'win32') return [];
|
|
393
|
+
try {
|
|
394
|
+
const listed = spawnSync('wsl.exe', ['--list', '--running', '--quiet'], {
|
|
395
|
+
windowsHide: true,
|
|
396
|
+
timeout: 8000,
|
|
397
|
+
});
|
|
398
|
+
if (listed.status !== 0) return [];
|
|
399
|
+
const distros = decodeProcessOutput(listed.stdout).split(/\r?\n/).map((value) => value.trim()).filter(Boolean);
|
|
400
|
+
const homes = [];
|
|
401
|
+
for (const distro of distros) {
|
|
402
|
+
const probed = spawnSync('wsl.exe', ['-d', distro, '--', 'sh', '-lc', 'printf %s "$HOME"'], {
|
|
403
|
+
windowsHide: true,
|
|
404
|
+
timeout: 8000,
|
|
405
|
+
});
|
|
406
|
+
const linuxHome = decodeProcessOutput(probed.stdout).trim();
|
|
407
|
+
if (probed.status !== 0 || !/^\/(?:[^\0\\]+)$/.test(linuxHome) || linuxHome.includes('..')) continue;
|
|
408
|
+
const relative = linuxHome.replace(/^\/+/, '').split('/');
|
|
409
|
+
homes.push(path.join(`\\\\wsl.localhost\\${distro}`, ...relative));
|
|
410
|
+
}
|
|
411
|
+
return unique(homes);
|
|
412
|
+
} catch {
|
|
413
|
+
return [];
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Orca가 아직 설치돼 있거나 실행 중이면 앱이 켜질 때 훅·플러그인을 다시 설치하고,
|
|
418
|
+
// 쓰던 자격증명을 격리하면 앱이 망가진다. 정리 전에 이 상태를 확인한다.
|
|
419
|
+
function runPowerShellJson(script) {
|
|
420
|
+
const result = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command',
|
|
421
|
+
`[Console]::OutputEncoding=[Text.UTF8Encoding]::new(); ${script}`], {
|
|
422
|
+
encoding: 'utf8', windowsHide: true, timeout: 20000,
|
|
423
|
+
});
|
|
424
|
+
if (result.status !== 0 || !result.stdout.trim()) return [];
|
|
425
|
+
try {
|
|
426
|
+
const value = JSON.parse(result.stdout);
|
|
427
|
+
return Array.isArray(value) ? value : [value];
|
|
428
|
+
} catch {
|
|
429
|
+
return [];
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function listRunningProcesses(platform) {
|
|
434
|
+
if (platform === 'win32') {
|
|
435
|
+
return runPowerShellJson('Get-CimInstance Win32_Process | Select-Object ProcessId,Name,ExecutablePath | ConvertTo-Json -Compress')
|
|
436
|
+
.map((item) => ({ pid: item.ProcessId, name: item.Name || '', path: item.ExecutablePath || '' }));
|
|
437
|
+
}
|
|
438
|
+
if (platform === 'linux') {
|
|
439
|
+
return listMatchingChildren('/proc', /^\d+$/).map((entry) => {
|
|
440
|
+
const pid = Number(path.basename(entry));
|
|
441
|
+
try {
|
|
442
|
+
const exe = fs.readlinkSync(path.join(entry, 'exe'));
|
|
443
|
+
return { pid, name: path.basename(exe), path: exe };
|
|
444
|
+
} catch {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
}).filter(Boolean);
|
|
448
|
+
}
|
|
449
|
+
const result = spawnSync('ps', ['-axo', 'pid=,comm='], { encoding: 'utf8', timeout: 10000 });
|
|
450
|
+
if (result.status !== 0) return [];
|
|
451
|
+
return result.stdout.split('\n').map((line) => line.match(/^\s*(\d+)\s+(.+?)\s*$/)).filter(Boolean)
|
|
452
|
+
.map((match) => ({ pid: Number(match[1]), name: path.basename(match[2]), path: match[2] }));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function listOrcaInstallations(platform, home, env) {
|
|
456
|
+
if (platform === 'win32') {
|
|
457
|
+
const fixed = [
|
|
458
|
+
env.LOCALAPPDATA && path.join(env.LOCALAPPDATA, 'Programs', 'orca'),
|
|
459
|
+
env.ProgramFiles && path.join(env.ProgramFiles, 'Orca'),
|
|
460
|
+
env.PROGRAMFILES && path.join(env.PROGRAMFILES, 'Orca'),
|
|
461
|
+
].filter((dir) => dir && pathExists(path.join(dir, 'Orca.exe')));
|
|
462
|
+
// electron-builder NSIS 제거 정보. 사용자가 다른 폴더(/D)에 설치했어도 여기서 찾는다.
|
|
463
|
+
const registered = runPowerShellJson([
|
|
464
|
+
"$keys = 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',",
|
|
465
|
+
"'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*',",
|
|
466
|
+
"'HKLM:\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*';",
|
|
467
|
+
"Get-ItemProperty $keys -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -eq 'Orca' -and $_.UninstallString -match 'Uninstall Orca\\.exe' } |",
|
|
468
|
+
'Select-Object InstallLocation,UninstallString | ConvertTo-Json -Compress',
|
|
469
|
+
].join(' ')).map((item) => item.InstallLocation || path.dirname(String(item.UninstallString || '').replace(/^"|"(?:\s.*)?$/g, '')));
|
|
470
|
+
return unique([...fixed, ...registered.filter(Boolean)]);
|
|
471
|
+
}
|
|
472
|
+
if (platform === 'darwin') {
|
|
473
|
+
return ['/Applications/Orca.app', path.join(home, 'Applications', 'Orca.app')]
|
|
474
|
+
.filter((app) => readText(path.join(app, 'Contents', 'Info.plist')).includes('com.stablyai.orca'));
|
|
475
|
+
}
|
|
476
|
+
return [];
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function probeRuntime(platform, home, env) {
|
|
480
|
+
return { installations: listOrcaInstallations(platform, home, env), processes: listRunningProcesses(platform) };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// 설치 폴더 안의 프로세스는 앱 본체, 앱 데이터 아래(daemon-host 등)로 옮겨 돌던 프로세스는 남은 데몬이다.
|
|
484
|
+
function classifyOrcaProcesses(context) {
|
|
485
|
+
const runtime = context.runtime || { installations: [], processes: [] };
|
|
486
|
+
const installDirs = unique([
|
|
487
|
+
...runtime.installations,
|
|
488
|
+
...(context.platform === 'win32' && context.env.LOCALAPPDATA ? [path.join(context.env.LOCALAPPDATA, 'Programs', 'orca')] : []),
|
|
489
|
+
...(context.platform === 'darwin' ? ['/Applications/Orca.app', path.join(context.home, 'Applications', 'Orca.app')] : []),
|
|
490
|
+
]);
|
|
491
|
+
const dataDirs = unique(defaultAppDataCandidates(context.platform, context.home, context.env));
|
|
492
|
+
const inside = (dirs, target) => dirs.some((dir) => isSameOrDescendant(dir, target)
|
|
493
|
+
|| (context.platform !== 'linux' && isSameOrDescendant(dir.toLowerCase(), target.toLowerCase())));
|
|
494
|
+
const app = [];
|
|
495
|
+
const leftover = [];
|
|
496
|
+
for (const item of runtime.processes) {
|
|
497
|
+
if (!item?.path || item.pid === process.pid) continue;
|
|
498
|
+
const target = path.resolve(item.path);
|
|
499
|
+
const appImage = context.platform === 'linux' && /^\/tmp\/\.mount_orca/i.test(item.path);
|
|
500
|
+
if (inside(installDirs, target) || appImage) app.push(item);
|
|
501
|
+
else if (inside(dataDirs, target) || /^orca-terminal-daemon(?:\.exe)?$/i.test(item.name)) leftover.push(item);
|
|
502
|
+
}
|
|
503
|
+
return { installations: runtime.installations, app, leftover };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function stopProcess(pid, platform) {
|
|
507
|
+
if (platform === 'win32') {
|
|
508
|
+
const result = spawnSync('taskkill.exe', ['/PID', String(pid), '/T', '/F'], { encoding: 'utf8', windowsHide: true });
|
|
509
|
+
if (result.status !== 0) throw new Error(`프로세스를 끄지 못했습니다(PID ${pid}): ${(result.stderr || result.stdout).trim()}`);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
process.kill(pid, 'SIGTERM');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function isWindowsOrcaPathEntry(entry, context) {
|
|
516
|
+
if (context.platform !== 'win32') return false;
|
|
517
|
+
const expanded = entry.replace(/%([^%]+)%/g, (_, name) => context.env[name] || context.env[name.toUpperCase()] || `%${name}%`);
|
|
518
|
+
const candidate = path.normalize(expanded.replace(/^"|"$/g, '')).replace(/[\\/]+$/, '');
|
|
519
|
+
const normalized = candidate.toLowerCase();
|
|
520
|
+
const expected = context.env.LOCALAPPDATA
|
|
521
|
+
? path.normalize(path.join(context.env.LOCALAPPDATA, 'Programs', 'orca', 'resources', 'bin')).toLowerCase()
|
|
522
|
+
: '';
|
|
523
|
+
if (expected && normalized === expected) return true;
|
|
524
|
+
const wrapper = path.join(candidate, 'orca.cmd');
|
|
525
|
+
const launcher = path.join(candidate, 'orca.exe');
|
|
526
|
+
const wrapperText = readText(wrapper);
|
|
527
|
+
return pathExists(launcher)
|
|
528
|
+
&& /native Orca CLI launcher/i.test(wrapperText)
|
|
529
|
+
&& /orca\.cmd cannot safely forward orchestration message bodies/i.test(wrapperText);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function buildContext(overrides = {}) {
|
|
533
|
+
const platform = overrides.platform || process.platform;
|
|
534
|
+
const home = path.resolve(overrides.home || os.homedir());
|
|
535
|
+
const env = { ...process.env, ...(overrides.env || {}) };
|
|
536
|
+
const projects = unique(overrides.projects || []);
|
|
537
|
+
const explicitWslHomes = unique(overrides.wslHomes || []);
|
|
538
|
+
const wslHomes = unique([
|
|
539
|
+
...explicitWslHomes,
|
|
540
|
+
...(overrides.includeWsl && explicitWslHomes.length === 0 ? discoverRunningWslHomes(platform) : []),
|
|
541
|
+
]);
|
|
542
|
+
const remoteHomes = unique(overrides.remoteHomes || []);
|
|
543
|
+
const scopedHomes = [
|
|
544
|
+
...wslHomes.map((scopedHome) => ({ home: scopedHome, requires: 'includeWsl' })),
|
|
545
|
+
...remoteHomes.map((scopedHome) => ({ home: scopedHome, requires: 'includeRemote' })),
|
|
546
|
+
];
|
|
547
|
+
const homeSkillRoots = overrides.homeSkillRoots || unique([
|
|
548
|
+
...defaultHomeSkillRoots(home),
|
|
549
|
+
...environmentSkillRoots(platform, home, env),
|
|
550
|
+
]);
|
|
551
|
+
const projectSkillRoots = defaultProjectSkillRoots(projects);
|
|
552
|
+
const scopedSkillRoots = scopedHomes.flatMap((scope) => defaultHomeSkillRoots(scope.home)
|
|
553
|
+
.map((root) => ({ root, requires: scope.requires })));
|
|
554
|
+
const skillRootRequirements = new Map(scopedSkillRoots.map((item) => [path.resolve(item.root), item.requires]));
|
|
555
|
+
const lockFiles = overrides.lockFiles || unique([
|
|
556
|
+
path.join(home, '.agents', '.skill-lock.json'),
|
|
557
|
+
...projects.map((project) => path.join(project, '.agents', '.skill-lock.json')),
|
|
558
|
+
...scopedHomes.map((scope) => path.join(scope.home, '.agents', '.skill-lock.json')),
|
|
559
|
+
]);
|
|
560
|
+
const lockFileRequirements = new Map(scopedHomes.map((scope) => [
|
|
561
|
+
path.resolve(path.join(scope.home, '.agents', '.skill-lock.json')),
|
|
562
|
+
scope.requires,
|
|
563
|
+
]));
|
|
564
|
+
const hookCandidates = [
|
|
565
|
+
...defaultHookCandidates(home, env),
|
|
566
|
+
...scopedHomes.flatMap((scope) => defaultHookCandidates(scope.home, {})
|
|
567
|
+
.map((candidate) => ({ ...candidate, requires: scope.requires }))),
|
|
568
|
+
];
|
|
569
|
+
const extensionCandidates = [
|
|
570
|
+
...defaultExtensionCandidates(home).map((target) => ({ path: target, requires: null })),
|
|
571
|
+
...scopedHomes.flatMap((scope) => defaultExtensionCandidates(scope.home)
|
|
572
|
+
.map((target) => ({ path: target, requires: scope.requires }))),
|
|
573
|
+
];
|
|
574
|
+
const grokHome = env.GROK_HOME || path.join(home, '.grok');
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
platform,
|
|
578
|
+
home,
|
|
579
|
+
env,
|
|
580
|
+
projects,
|
|
581
|
+
wslHomes,
|
|
582
|
+
remoteHomes,
|
|
583
|
+
skillRoots: unique([...homeSkillRoots, ...projectSkillRoots, ...scopedSkillRoots.map((item) => item.root)]),
|
|
584
|
+
pluginCacheRoots: unique(overrides.pluginCacheRoots || [path.join(home, '.codex', 'plugins', 'cache')]),
|
|
585
|
+
skillRootRequirements,
|
|
586
|
+
lockFiles,
|
|
587
|
+
lockFileRequirements,
|
|
588
|
+
hookCandidates: overrides.hookCandidates || hookCandidates,
|
|
589
|
+
extensionCandidates: overrides.extensionCandidates || extensionCandidates,
|
|
590
|
+
filesystemIdentity: overrides.filesystemIdentity || existingFilesystemIdentity,
|
|
591
|
+
sharedStateCandidates: unique(overrides.sharedStateCandidates || [
|
|
592
|
+
path.join(home, '.orca', 'agent-hooks'),
|
|
593
|
+
path.join(home, '.orca', 'claude-agent-teams-bin'),
|
|
594
|
+
path.join(home, '.orca', 'managed-hook-install.lock'),
|
|
595
|
+
...ORCA_CREDENTIAL_FILES.map((name) => path.join(home, '.orca', name)),
|
|
596
|
+
path.join(grokHome, 'hooks', 'orca-status.json.orca-cleaned-symlink'),
|
|
597
|
+
]),
|
|
598
|
+
conditionalStateCandidates: overrides.conditionalStateCandidates || [
|
|
599
|
+
...projects.flatMap((project) => [
|
|
600
|
+
{ kind: 'project-state', path: path.join(project, '.orca'), requires: 'includeProjectState', reason: '프로젝트 Orca issue-command·drop 상태' },
|
|
601
|
+
{ kind: 'project-state', path: path.join(path.dirname(project), '.orca-worktree-trash'), requires: 'includeProjectState', reason: '프로젝트 인접 Orca worktree 휴지통' },
|
|
602
|
+
]),
|
|
603
|
+
...wslHomes.flatMap((scopedHome) => [
|
|
604
|
+
{ kind: 'wsl-state', path: path.join(scopedHome, '.orca-wsl'), requires: 'includeWsl', reason: 'WSL Orca 릴레이·훅 상태' },
|
|
605
|
+
{ kind: 'wsl-state', path: path.join(scopedHome, '.local', 'share', 'orca'), requires: 'includeWsl', reason: 'WSL Orca 브리지·관리형 계정 상태' },
|
|
606
|
+
]),
|
|
607
|
+
...unique([home, ...remoteHomes]).flatMap((scopedHome) => [
|
|
608
|
+
{ kind: 'remote-state', path: path.join(scopedHome, '.orca-remote'), requires: 'includeRemote', reason: 'SSH Orca 버전별 릴레이·업로드 상태' },
|
|
609
|
+
{ kind: 'remote-state', path: path.join(scopedHome, '.orca-relay'), requires: 'includeRemote', reason: 'SSH Orca CLI·훅 오버레이' },
|
|
610
|
+
{ kind: 'remote-state', path: path.join(scopedHome, '.orca', 'sessions'), requires: 'includeRemote', reason: '원격 Orca 세션 상태' },
|
|
611
|
+
{ kind: 'remote-state', path: path.join(scopedHome, '.orca', 'skill-installs'), requires: 'includeRemote', reason: '원격 Orca Skills 설치 상태' },
|
|
612
|
+
]),
|
|
613
|
+
...remoteHomes.map((scopedHome) => (
|
|
614
|
+
{ kind: 'remote-state', path: path.join(scopedHome, '.orca', 'agent-hooks'), requires: 'includeRemote', reason: '원격 Orca 관리형 에이전트 훅' }
|
|
615
|
+
)),
|
|
616
|
+
],
|
|
617
|
+
userStateCandidates: unique(overrides.userStateCandidates || [path.join(home, '.orca')]),
|
|
618
|
+
appDataCandidates: unique(overrides.appDataCandidates || defaultAppDataCandidates(platform, home, env)),
|
|
619
|
+
voiceCandidates: unique([...(overrides.voiceCandidates || defaultVoiceCandidates(platform, env)), ...(overrides.customVoicePaths || [])]),
|
|
620
|
+
customVoicePaths: unique(overrides.customVoicePaths || []),
|
|
621
|
+
cliCandidates: unique(overrides.cliCandidates || defaultCliCandidates(platform, home)),
|
|
622
|
+
conditionalCliCandidates: overrides.conditionalCliCandidates || [
|
|
623
|
+
...wslHomes.flatMap((scopedHome) => defaultCliCandidates('linux', scopedHome)
|
|
624
|
+
.map((target) => ({ path: target, requires: 'includeWsl' }))),
|
|
625
|
+
...remoteHomes.flatMap((scopedHome) => defaultCliCandidates('linux', scopedHome)
|
|
626
|
+
.map((target) => ({ path: target, requires: 'includeRemote' }))),
|
|
627
|
+
],
|
|
628
|
+
userPath: overrides.userPath ?? readWindowsUserPath(platform),
|
|
629
|
+
// 실제 사용자 홈을 검사할 때만 이 PC의 설치·프로세스 상태를 읽는다(임시 홈을 쓰는 테스트는 제외).
|
|
630
|
+
runtime: overrides.runtime || (platform === process.platform && home === path.resolve(os.homedir())
|
|
631
|
+
? probeRuntime(platform, home, env)
|
|
632
|
+
: { installations: [], processes: [] }),
|
|
633
|
+
stopProcess: overrides.stopProcess || ((pid) => stopProcess(pid, platform)),
|
|
634
|
+
backupRoot: path.resolve(overrides.backupRoot || path.join(home, 'OrcaAgentCleanupBackups', timestamp())),
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function timestamp() {
|
|
639
|
+
const time = new Date().toISOString().replace(/[-:]/g, '').replace(/T/, '-').replace(/\..+/, '');
|
|
640
|
+
return `${time}-${randomUUID()}`;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function operationId(now = new Date(), uuid = randomUUID()) {
|
|
644
|
+
return `${now.toISOString().replace(/[-:]/g, '').replace(/T/, '-').replace(/Z$/, '').replace('.', '-')}-${process.pid}-${uuid}`;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function cleanupManifestPayload(manifest) {
|
|
648
|
+
return {
|
|
649
|
+
manifestVersion: manifest.manifestVersion,
|
|
650
|
+
createdAt: manifest.createdAt,
|
|
651
|
+
platform: manifest.platform,
|
|
652
|
+
backupRoot: manifest.backupRoot,
|
|
653
|
+
cleanupStatus: manifest.cleanupStatus,
|
|
654
|
+
actions: manifest.actions,
|
|
655
|
+
errors: manifest.errors,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function cleanupManifestDigest(manifest) {
|
|
660
|
+
return createHash('sha256').update(JSON.stringify(cleanupManifestPayload(manifest))).digest('hex');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function signedCleanupManifest(value) {
|
|
664
|
+
return {
|
|
665
|
+
...value,
|
|
666
|
+
integrity: { algorithm: 'sha256', digest: cleanupManifestDigest(value) },
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function updateDigestField(hash, value) {
|
|
671
|
+
const bytes = Buffer.from(String(value), 'utf8');
|
|
672
|
+
hash.update(`${bytes.length}:`);
|
|
673
|
+
hash.update(bytes);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function updateDigestForPath(hash, target, relative = '.') {
|
|
677
|
+
const stat = fs.lstatSync(target);
|
|
678
|
+
if (stat.isSymbolicLink()) {
|
|
679
|
+
updateDigestField(hash, 'link');
|
|
680
|
+
updateDigestField(hash, relative);
|
|
681
|
+
updateDigestField(hash, fs.readlinkSync(target));
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (stat.isDirectory()) {
|
|
685
|
+
updateDigestField(hash, 'directory');
|
|
686
|
+
updateDigestField(hash, relative);
|
|
687
|
+
for (const name of fs.readdirSync(target).sort()) {
|
|
688
|
+
updateDigestForPath(hash, path.join(target, name), path.join(relative, name));
|
|
689
|
+
}
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (!stat.isFile()) throw new Error(`지원하지 않는 백업 파일 형식입니다: ${target}`);
|
|
693
|
+
updateDigestField(hash, 'file');
|
|
694
|
+
updateDigestField(hash, relative);
|
|
695
|
+
updateDigestField(hash, stat.size);
|
|
696
|
+
const handle = fs.openSync(target, 'r');
|
|
697
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
698
|
+
try {
|
|
699
|
+
for (;;) {
|
|
700
|
+
const bytesRead = fs.readSync(handle, buffer, 0, buffer.length, null);
|
|
701
|
+
if (bytesRead === 0) break;
|
|
702
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
703
|
+
}
|
|
704
|
+
} finally {
|
|
705
|
+
fs.closeSync(handle);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function pathIntegrity(target) {
|
|
710
|
+
const hash = createHash('sha256');
|
|
711
|
+
updateDigestForPath(hash, target);
|
|
712
|
+
return { algorithm: 'sha256', digest: hash.digest('hex') };
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function integrityMatches(target, integrity) {
|
|
716
|
+
return integrity?.algorithm === 'sha256'
|
|
717
|
+
&& /^[a-f0-9]{64}$/u.test(integrity.digest || '')
|
|
718
|
+
&& pathIntegrity(target).digest === integrity.digest;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function entriesFromLock(lock) {
|
|
722
|
+
if (!lock || typeof lock !== 'object') return [];
|
|
723
|
+
const containers = [lock.skills, lock.installed, lock.dependencies, lock];
|
|
724
|
+
const result = [];
|
|
725
|
+
for (const container of containers) {
|
|
726
|
+
if (!container || typeof container !== 'object' || Array.isArray(container)) continue;
|
|
727
|
+
for (const [name, value] of Object.entries(container)) {
|
|
728
|
+
if (name === 'skills' || name === 'installed' || name === 'dependencies') continue;
|
|
729
|
+
const details = typeof value === 'string' ? { source: value } : (value || {});
|
|
730
|
+
const source = [details.source, details.sourceUrl, details.repository, details.repo, details.url]
|
|
731
|
+
.filter(Boolean).join(' ');
|
|
732
|
+
result.push({ name, source });
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return result;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function lockedOrcaSkillNamesByRoot(context) {
|
|
739
|
+
const namesByRoot = new Map();
|
|
740
|
+
for (const lockFile of context.lockFiles) {
|
|
741
|
+
const skillRoot = path.resolve(path.dirname(lockFile), 'skills');
|
|
742
|
+
const names = namesByRoot.get(skillRoot) || new Set();
|
|
743
|
+
for (const entry of entriesFromLock(readJson(lockFile))) {
|
|
744
|
+
if (ORCA_SOURCE_PATTERN.test(entry.source)) names.add(entry.name);
|
|
745
|
+
}
|
|
746
|
+
namesByRoot.set(skillRoot, names);
|
|
747
|
+
}
|
|
748
|
+
return namesByRoot;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function hasOrcaSkillSignature(skillPath) {
|
|
752
|
+
const text = [
|
|
753
|
+
readText(path.join(skillPath, 'SKILL.md')),
|
|
754
|
+
readText(path.join(skillPath, 'README.md')),
|
|
755
|
+
readText(path.join(skillPath, 'package.json')),
|
|
756
|
+
].join('\n');
|
|
757
|
+
return ORCA_SKILL_SIGNATURES.some((pattern) => pattern.test(text));
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function defaultCliCandidates(platform, home) {
|
|
761
|
+
if (platform === 'darwin') {
|
|
762
|
+
return ['/usr/local/bin/orca', path.join(home, '.local', 'bin', 'orca')];
|
|
763
|
+
}
|
|
764
|
+
if (platform === 'linux') {
|
|
765
|
+
// Linux 공식 명령은 orca-ide다. /usr/bin/orca는 GNOME 스크린 리더라 건드리지 않는다.
|
|
766
|
+
return [
|
|
767
|
+
path.join(home, '.local', 'bin', LINUX_CLI_COMMAND_NAME),
|
|
768
|
+
path.join(home, '.local', 'bin', 'orca'),
|
|
769
|
+
];
|
|
770
|
+
}
|
|
771
|
+
return [];
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function isOrcaCli(candidate) {
|
|
775
|
+
if (!pathExists(candidate)) return false;
|
|
776
|
+
let linkTarget = '';
|
|
777
|
+
try {
|
|
778
|
+
const stat = fs.lstatSync(candidate);
|
|
779
|
+
if (stat.isSymbolicLink()) linkTarget = fs.readlinkSync(candidate);
|
|
780
|
+
} catch {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
const text = readText(candidate);
|
|
784
|
+
const haystack = `${linkTarget}\n${text}`;
|
|
785
|
+
if (/Orca\.app[\\/]Contents[\\/]Resources[\\/]bin/i.test(haystack)) return true;
|
|
786
|
+
if (haystack.includes(LINUX_CLI_DISPATCHER_MARKER)) return true;
|
|
787
|
+
if (/stablyai\/orca|Managed by Orca/i.test(haystack)) return true;
|
|
788
|
+
if (/[\\/]resources[\\/]bin[\\/]orca-ide(?:\.exe)?$/i.test(linkTarget)) return true;
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function stripDelimitedBlock(content, startMarker, endMarker) {
|
|
793
|
+
const start = content.indexOf(startMarker);
|
|
794
|
+
if (start < 0) return content;
|
|
795
|
+
const endMarkerIndex = content.indexOf(endMarker, start + startMarker.length);
|
|
796
|
+
const end = endMarkerIndex < 0 ? content.length : endMarkerIndex + endMarker.length;
|
|
797
|
+
const before = content.slice(0, start).replace(/[ \t]*(?:\r?\n)*$/, '');
|
|
798
|
+
const after = content.slice(end).replace(/^(?:\r?\n)+/, '');
|
|
799
|
+
if (!before) return after;
|
|
800
|
+
if (!after) return before.endsWith('\n') ? before : `${before}\n`;
|
|
801
|
+
return `${before}\n\n${after}`;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function splitLinesWithEol(text) {
|
|
805
|
+
return { lines: text.split('\n').map((line) => line.replace(/\r$/, '')), eol: text.includes('\r\n') ? '\r\n' : '\n' };
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function parseTomlSingleLineString(valueText) {
|
|
809
|
+
const literal = valueText.match(/^'([^']*)'\s*(?:#.*)?$/);
|
|
810
|
+
if (literal) return literal[1];
|
|
811
|
+
const basic = valueText.match(/^"((?:[^"\\]|\\.)*)"\s*(?:#.*)?$/);
|
|
812
|
+
if (!basic) return null;
|
|
813
|
+
const escapes = { b: '\b', t: '\t', n: '\n', f: '\f', r: '\r', '"': '"', '\\': '\\' };
|
|
814
|
+
return basic[1].replace(/\\(u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|.)/g, (_, escape) => (
|
|
815
|
+
escape.length > 1 ? String.fromCodePoint(Number.parseInt(escape.slice(1), 16)) : (escapes[escape] ?? escape)
|
|
816
|
+
));
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// jcode ~/.jcode/config.toml의 [hooks] 표에서 Orca 스크립트를 부르는 줄 번호(src/main/jcode/hook-config.ts).
|
|
820
|
+
function jcodeManagedHookLines(text) {
|
|
821
|
+
const { lines } = splitLinesWithEol(text);
|
|
822
|
+
const managed = new Set();
|
|
823
|
+
let inHooks = false;
|
|
824
|
+
let multiline = null;
|
|
825
|
+
lines.forEach((line, index) => {
|
|
826
|
+
if (multiline) {
|
|
827
|
+
if (line.includes(multiline)) multiline = null;
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
if (/^\s*\[\[/.test(line)) {
|
|
831
|
+
inHooks = false;
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
const header = line.match(/^\s*\[\s*([^[\]]+?)\s*\]\s*(?:#.*)?$/);
|
|
835
|
+
if (header) {
|
|
836
|
+
inHooks = header[1] === 'hooks';
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const pair = line.match(/^\s*[A-Za-z0-9_-]+\s*=\s*(.*)$/);
|
|
840
|
+
if (!pair) return;
|
|
841
|
+
const opener = ['"""', "'''"].find((quote) => pair[1].startsWith(quote));
|
|
842
|
+
if (opener) {
|
|
843
|
+
if (pair[1].indexOf(opener, opener.length) < 0) multiline = opener;
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (inHooks && isOrcaHookString(parseTomlSingleLineString(pair[1]))) managed.add(index);
|
|
847
|
+
});
|
|
848
|
+
return managed;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// dsh-home-patch.ts와 같이 가장 가까운 시작 표식과 그 뒤 첫 끝 표식을 짝짓는다.
|
|
852
|
+
function dshManagedRegion(lines) {
|
|
853
|
+
let start = -1;
|
|
854
|
+
for (const [index, line] of lines.entries()) {
|
|
855
|
+
if (line.trim() === DSH_START) start = index;
|
|
856
|
+
else if (line.trim() === DSH_END && start !== -1) return { start, end: index };
|
|
857
|
+
}
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function isOrcaOpenCodeTuiEntry(item) {
|
|
862
|
+
const raw = Array.isArray(item) ? item[0] : item;
|
|
863
|
+
if (typeof raw !== 'string') return false;
|
|
864
|
+
let decoded = raw;
|
|
865
|
+
try {
|
|
866
|
+
decoded = decodeURIComponent(raw);
|
|
867
|
+
} catch {
|
|
868
|
+
// 잘못된 퍼센트 인코딩이면 원문으로 판단한다.
|
|
869
|
+
}
|
|
870
|
+
return OPENCODE_TUI_ENTRY_PATTERN.test(decoded);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function hasOpenCodePluginSignature(target) {
|
|
874
|
+
try {
|
|
875
|
+
const file = fs.lstatSync(target).isDirectory() ? path.join(target, 'tui.js') : target;
|
|
876
|
+
return OPENCODE_PLUGIN_SIGNATURE.test(readText(file));
|
|
877
|
+
} catch {
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Orca grok-hook-config-cleanup.ts의 isOrcaOwnedRemnant와 같은 기준: Orca 항목을 빼면 hooks가 비고
|
|
883
|
+
// version 말고 남는 키가 없다. 사용자가 dotfiles로 연결한 심볼릭 링크는 파일을 지우지 않고 고친다.
|
|
884
|
+
function isOrcaOwnedRemnant(target) {
|
|
885
|
+
try {
|
|
886
|
+
if (fs.lstatSync(target).isSymbolicLink()) return false;
|
|
887
|
+
} catch {
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
const value = readJson(target);
|
|
891
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
892
|
+
return Object.keys(pruneJsonHooks(value)).every((key) => key === 'version');
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function hasHookMarker(candidate) {
|
|
896
|
+
if (!pathExists(candidate.path)) return false;
|
|
897
|
+
if (candidate.type === 'hermes') {
|
|
898
|
+
const markerFiles = ['plugin.json', 'README.md', '__init__.py', 'orca-status.py'];
|
|
899
|
+
return markerFiles.some((name) => {
|
|
900
|
+
const text = readText(path.join(candidate.path, name));
|
|
901
|
+
return MANAGED_FILE_PATTERN.test(text) || ORCA_HOOK_PATTERN.test(text);
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
if (candidate.type === 'opencode-plugin') return hasOpenCodePluginSignature(candidate.path);
|
|
905
|
+
const text = readText(candidate.path);
|
|
906
|
+
if (candidate.type === 'kimi') return text.includes(KIMI_START) && text.includes(KIMI_END);
|
|
907
|
+
if (candidate.type === 'dsh') return dshManagedRegion(splitLinesWithEol(text).lines) !== null;
|
|
908
|
+
if (candidate.type === 'jcode') return jcodeManagedHookLines(text).size > 0;
|
|
909
|
+
if (candidate.type === 'muse') return isOrcaHookString(readJson(candidate.path)?.managed_hooks_path);
|
|
910
|
+
if (candidate.type === 'opencode-tui') {
|
|
911
|
+
const plugins = readJson(candidate.path)?.plugin;
|
|
912
|
+
// 주석이 있는 JSONC는 해석하지 못하지만 Orca 항목이 보이면 알린다. 수정 단계에서 오류로 멈춘다.
|
|
913
|
+
return Array.isArray(plugins)
|
|
914
|
+
? plugins.some(isOrcaOpenCodeTuiEntry)
|
|
915
|
+
: /plugins[\\/]+(?:orca-opencode2?-status-tui|orca-opencode-startup-prompt)[\\/]+tui\.js/i.test(text);
|
|
916
|
+
}
|
|
917
|
+
if (candidate.type === 'codex-legacy') return text.includes(CODEX_LEGACY_START);
|
|
918
|
+
if (candidate.type === 'amp') return MANAGED_FILE_PATTERN.test(text);
|
|
919
|
+
if (candidate.type === 'hermes-config') return /^\s*-\s*orca-status\s*$/m.test(text);
|
|
920
|
+
return isOrcaHookString(text) || MANAGED_FILE_PATTERN.test(text);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function hasManagedExtensionMarker(target) {
|
|
924
|
+
return pathExists(target) && readText(target).includes(PI_EXTENSION_MARKER);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function listMatchingChildren(directory, pattern) {
|
|
928
|
+
try {
|
|
929
|
+
return fs.readdirSync(directory, { withFileTypes: true })
|
|
930
|
+
.filter((entry) => pattern.test(entry.name))
|
|
931
|
+
.map((entry) => path.join(directory, entry.name));
|
|
932
|
+
} catch {
|
|
933
|
+
return [];
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function managedHookLockResidues(home) {
|
|
938
|
+
return listMatchingChildren(path.join(home, '.orca'),
|
|
939
|
+
/^managed-hook-install\.(?:owner(?:-draft)?|claimed|claim)-.+\.json$/u);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function managedSkillTransactionResidues(root) {
|
|
943
|
+
return listMatchingChildren(root,
|
|
944
|
+
/^(?:\.orca-skill-extract-.+|\..+\.orca-(?:staging|backup|copy|skill-delete|remove-backup|placement-backup|placement-staging)-.+)$/u);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function hasOrcaVoiceModelSignature(target) {
|
|
948
|
+
const containsKnownModel = (directory) => {
|
|
949
|
+
try {
|
|
950
|
+
return fs.readdirSync(directory, { withFileTypes: true }).some((entry) => {
|
|
951
|
+
const normalized = entry.name.replace(/(?:\.partial|\.tar\.bz2)$/i, '');
|
|
952
|
+
return ORCA_VOICE_MODEL_IDS.has(normalized);
|
|
953
|
+
});
|
|
954
|
+
} catch {
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
if (containsKnownModel(target)) return true;
|
|
959
|
+
try {
|
|
960
|
+
return fs.readdirSync(target, { withFileTypes: true })
|
|
961
|
+
.filter((entry) => entry.isDirectory() && /^[a-f0-9]{16}$/i.test(entry.name))
|
|
962
|
+
.some((entry) => containsKnownModel(path.join(target, entry.name)));
|
|
963
|
+
} catch {
|
|
964
|
+
return false;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function finding(kind, target, reason, requires = null, meta = {}) {
|
|
969
|
+
return { kind, path: path.resolve(target), reason, requires, ...meta };
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
export function scanOrcaResidue(context) {
|
|
973
|
+
const findings = [];
|
|
974
|
+
const runtime = classifyOrcaProcesses(context);
|
|
975
|
+
for (const target of runtime.installations) {
|
|
976
|
+
findings.push(finding('app-installed', target, 'Orca 앱이 아직 설치돼 있습니다. 운영체제의 제거 기능으로 먼저 지우세요', 'never'));
|
|
977
|
+
}
|
|
978
|
+
for (const item of runtime.app) {
|
|
979
|
+
findings.push(finding('app-running', item.path, `Orca 앱이 실행 중입니다(PID ${item.pid}). 앱을 끄고 제거한 뒤 정리하세요`, 'never', { pid: item.pid }));
|
|
980
|
+
}
|
|
981
|
+
for (const item of runtime.leftover) {
|
|
982
|
+
findings.push(finding('process', item.path, `앱 없이 남아 돌고 있는 Orca 백그라운드 프로세스(PID ${item.pid})`, 'stopProcesses', { pid: item.pid }));
|
|
983
|
+
}
|
|
984
|
+
const lockedNamesByRoot = lockedOrcaSkillNamesByRoot(context);
|
|
985
|
+
|
|
986
|
+
for (const root of context.skillRoots) {
|
|
987
|
+
const requirement = context.skillRootRequirements?.get(path.resolve(root)) || null;
|
|
988
|
+
const lockedNames = lockedNamesByRoot.get(path.resolve(root)) || new Set();
|
|
989
|
+
for (const name of OFFICIAL_ORCA_SKILLS) {
|
|
990
|
+
const skillPath = path.join(root, name);
|
|
991
|
+
if (!pathExists(skillPath)) continue;
|
|
992
|
+
const isAgentsSkillRoot = path.basename(root) === 'skills' && path.basename(path.dirname(root)) === '.agents';
|
|
993
|
+
if ((isAgentsSkillRoot && lockedNames.has(name)) || hasOrcaSkillSignature(skillPath)) {
|
|
994
|
+
findings.push(finding('skill', skillPath, `Orca 공식 스킬(${name}) 출처 확인`, requirement));
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
for (const target of managedSkillTransactionResidues(root)) {
|
|
998
|
+
findings.push(finding('skill-transaction', target, '중단된 Orca Skills 설치·삭제 트랜잭션', requirement));
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
for (const root of context.pluginCacheRoots || []) {
|
|
1003
|
+
for (const skillPath of pluginCacheOrcaSkills(root)) {
|
|
1004
|
+
findings.push(finding('skill', skillPath, 'Codex 플러그인 캐시의 Orca 공식 스킬 출처 확인'));
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
for (const lockFile of context.lockFiles) {
|
|
1009
|
+
const entries = entriesFromLock(readJson(lockFile)).filter((entry) => ORCA_SOURCE_PATTERN.test(entry.source));
|
|
1010
|
+
if (entries.length) {
|
|
1011
|
+
const requirement = context.lockFileRequirements?.get(path.resolve(lockFile)) || null;
|
|
1012
|
+
findings.push(finding('skill-lock', lockFile, 'stablyai/orca 잠금 항목', requirement, { names: entries.map((entry) => entry.name) }));
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const hookCandidates = context.hookCandidates.flatMap((candidate) => (candidate.type === 'json-dir'
|
|
1017
|
+
? listMatchingChildren(candidate.path, /\.json$/i).map((file) => ({ ...candidate, type: 'json', path: file }))
|
|
1018
|
+
: [candidate]));
|
|
1019
|
+
for (const candidate of hookCandidates) {
|
|
1020
|
+
if (candidate.type === 'hermes-config') {
|
|
1021
|
+
const plugin = context.hookCandidates.find((item) => item.type === 'hermes'
|
|
1022
|
+
&& path.dirname(path.dirname(item.path)) === path.dirname(candidate.path));
|
|
1023
|
+
if (!plugin || !hasHookMarker(plugin)) continue;
|
|
1024
|
+
}
|
|
1025
|
+
if (hasHookMarker(candidate)) {
|
|
1026
|
+
const removeWhole = (candidate.type === 'codex-legacy'
|
|
1027
|
+
&& stripDelimitedBlock(readText(candidate.path), CODEX_LEGACY_START, CODEX_LEGACY_END).trim() === '')
|
|
1028
|
+
|| (candidate.ownedFile && isOrcaOwnedRemnant(candidate.path));
|
|
1029
|
+
findings.push(finding('hook', candidate.path, `Orca 관리형 ${candidate.type} 훅`, candidate.requires || null,
|
|
1030
|
+
{ hookType: candidate.type, removeWhole }));
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
for (const candidate of context.extensionCandidates || []) {
|
|
1035
|
+
if (hasManagedExtensionMarker(candidate.path)) {
|
|
1036
|
+
findings.push(finding('extension', candidate.path, 'Orca 관리형 Pi·OMP·Prime 확장', candidate.requires || null));
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
for (const target of context.sharedStateCandidates) {
|
|
1041
|
+
if (pathExists(target)) {
|
|
1042
|
+
findings.push(finding('shared-state', target, '경로가 고정된 Orca 훅·암호화 자격증명·관리 파일'));
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
for (const target of managedHookLockResidues(context.home)) {
|
|
1046
|
+
findings.push(finding('shared-state', target, '중단된 Orca 관리형 훅 잠금 복구 파일'));
|
|
1047
|
+
}
|
|
1048
|
+
for (const candidate of context.conditionalStateCandidates || []) {
|
|
1049
|
+
if (pathExists(candidate.path)) {
|
|
1050
|
+
findings.push(finding(candidate.kind, candidate.path, candidate.reason, candidate.requires));
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
for (const target of context.userStateCandidates || []) {
|
|
1054
|
+
if (pathExists(target)) {
|
|
1055
|
+
findings.push(finding('user-state', target, 'Orca 자격증명·세션·키 설정을 포함한 전체 사용자 상태', 'includeUserState'));
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
const seenAppData = new Set();
|
|
1060
|
+
for (const target of context.appDataCandidates) {
|
|
1061
|
+
if (!pathExists(target)) continue;
|
|
1062
|
+
const identity = context.filesystemIdentity(target);
|
|
1063
|
+
if (seenAppData.has(identity)) continue;
|
|
1064
|
+
seenAppData.add(identity);
|
|
1065
|
+
findings.push(finding('app-data', target, 'Orca 애플리케이션 데이터', 'includeAppData'));
|
|
1066
|
+
}
|
|
1067
|
+
for (const target of context.voiceCandidates) {
|
|
1068
|
+
if (!pathExists(target)) continue;
|
|
1069
|
+
if (hasOrcaVoiceModelSignature(target)) {
|
|
1070
|
+
findings.push(finding('voice-data', target, 'Orca 음성 모델 캐시', 'includeVoiceData'));
|
|
1071
|
+
} else {
|
|
1072
|
+
findings.push(finding('unverified', target, 'Orca 음성 모델 서명을 확인할 수 없어 자동 정리하지 않음', 'never'));
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
for (const target of context.cliCandidates) {
|
|
1076
|
+
if (isOrcaCli(target)) findings.push(finding('cli', target, 'Orca 앱 또는 공식 런처를 가리키는 CLI'));
|
|
1077
|
+
}
|
|
1078
|
+
for (const candidate of context.conditionalCliCandidates || []) {
|
|
1079
|
+
if (isOrcaCli(candidate.path)) {
|
|
1080
|
+
findings.push(finding('cli', candidate.path, 'Orca 앱 또는 공식 런처를 가리키는 CLI', candidate.requires));
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
for (const entry of (context.userPath || '').split(';').filter(Boolean)) {
|
|
1084
|
+
if (isWindowsOrcaPathEntry(entry, context)) {
|
|
1085
|
+
findings.push(finding('path-entry', entry, 'Windows 사용자 PATH의 Orca CLI 경로', null, { rawPathEntry: entry }));
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const uniqueFindings = new Map();
|
|
1090
|
+
for (const item of findings) uniqueFindings.set(`${item.kind}\0${item.path}\0${item.pid ?? ''}`, item);
|
|
1091
|
+
return [...uniqueFindings.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function safeRelative(target) {
|
|
1095
|
+
const parsed = path.parse(target);
|
|
1096
|
+
const withoutRoot = target.slice(parsed.root.length).replace(/:/g, '_');
|
|
1097
|
+
const rootLabel = parsed.root.replace(/[\\/:]+/g, '_') || 'root';
|
|
1098
|
+
return path.join(rootLabel, withoutRoot);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function isSameOrDescendant(parent, candidate) {
|
|
1102
|
+
const relative = path.relative(path.resolve(parent), path.resolve(candidate));
|
|
1103
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function assertSafeQuarantineSource(source, context) {
|
|
1107
|
+
const resolved = path.resolve(source);
|
|
1108
|
+
if (resolved === path.parse(resolved).root || resolved === path.resolve(context.home)) {
|
|
1109
|
+
throw new Error(`사용자 홈 또는 파일시스템 루트는 격리할 수 없습니다: ${resolved}`);
|
|
1110
|
+
}
|
|
1111
|
+
if (isSameOrDescendant(resolved, context.backupRoot)) {
|
|
1112
|
+
throw new Error(`백업 폴더가 정리 대상 내부에 있어 작업을 거부했습니다: ${resolved}`);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function ensureParent(target) {
|
|
1117
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function quarantine(source, context, category, dryRun) {
|
|
1121
|
+
assertSafeQuarantineSource(source, context);
|
|
1122
|
+
const destination = path.join(context.backupRoot, category, safeRelative(source));
|
|
1123
|
+
if (dryRun) return destination;
|
|
1124
|
+
ensureParent(destination);
|
|
1125
|
+
if (pathExists(destination)) throw new Error(`백업 대상이 이미 존재합니다: ${destination}`);
|
|
1126
|
+
try {
|
|
1127
|
+
fs.renameSync(source, destination);
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
if (error.code !== 'EXDEV') throw error;
|
|
1130
|
+
const stat = fs.lstatSync(source);
|
|
1131
|
+
if (stat.isSymbolicLink()) {
|
|
1132
|
+
fs.symlinkSync(fs.readlinkSync(source), destination);
|
|
1133
|
+
fs.unlinkSync(source);
|
|
1134
|
+
} else {
|
|
1135
|
+
fs.cpSync(source, destination, { recursive: true, force: false, errorOnExist: true });
|
|
1136
|
+
fs.rmSync(source, { recursive: true, force: false });
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return destination;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function backupConfig(source, context, dryRun) {
|
|
1143
|
+
const destination = path.join(context.backupRoot, 'config-original', safeRelative(source));
|
|
1144
|
+
if (!dryRun) {
|
|
1145
|
+
ensureParent(destination);
|
|
1146
|
+
fs.copyFileSync(source, destination, fs.constants.COPYFILE_EXCL);
|
|
1147
|
+
}
|
|
1148
|
+
return destination;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function directManagedString(value) {
|
|
1152
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
1153
|
+
return Object.values(value).some((item) => isOrcaHookString(item));
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function pruneJsonHooks(value) {
|
|
1157
|
+
if (Array.isArray(value)) {
|
|
1158
|
+
const next = [];
|
|
1159
|
+
for (const item of value) {
|
|
1160
|
+
if (isOrcaHookString(item)) continue;
|
|
1161
|
+
if (directManagedString(item)) continue;
|
|
1162
|
+
const cleaned = pruneJsonHooks(item);
|
|
1163
|
+
if (item && typeof item === 'object' && Array.isArray(item.hooks)
|
|
1164
|
+
&& (!cleaned.hooks || cleaned.hooks.length === 0)) continue;
|
|
1165
|
+
if (cleaned !== undefined) next.push(cleaned);
|
|
1166
|
+
}
|
|
1167
|
+
return next;
|
|
1168
|
+
}
|
|
1169
|
+
if (!value || typeof value !== 'object') return value;
|
|
1170
|
+
const next = {};
|
|
1171
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1172
|
+
if (isOrcaHookString(child)) continue;
|
|
1173
|
+
if (directManagedString(child)) continue;
|
|
1174
|
+
const cleaned = pruneJsonHooks(child);
|
|
1175
|
+
if (cleaned === undefined) continue;
|
|
1176
|
+
if (Array.isArray(cleaned) && cleaned.length === 0) continue;
|
|
1177
|
+
if (cleaned && typeof cleaned === 'object' && !Array.isArray(cleaned) && Object.keys(cleaned).length === 0) continue;
|
|
1178
|
+
next[key] = cleaned;
|
|
1179
|
+
}
|
|
1180
|
+
return next;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function writeAtomic(target, text) {
|
|
1184
|
+
const temp = `${target}.orca-cleanup-${process.pid}-${randomUUID()}.tmp`;
|
|
1185
|
+
fs.writeFileSync(temp, text, { encoding: 'utf8', flag: 'wx' });
|
|
1186
|
+
try {
|
|
1187
|
+
fs.renameSync(temp, target);
|
|
1188
|
+
} finally {
|
|
1189
|
+
try { fs.unlinkSync(temp); } catch { /* rename 성공 시 이미 사라진다. */ }
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function cleanJsonHook(target, context, dryRun) {
|
|
1194
|
+
const value = readJson(target);
|
|
1195
|
+
if (!value) throw new Error(`JSON을 해석할 수 없어 자동 수정하지 않았습니다: ${target}`);
|
|
1196
|
+
const cleaned = pruneJsonHooks(value);
|
|
1197
|
+
const before = `${JSON.stringify(value, null, 2)}\n`;
|
|
1198
|
+
const after = `${JSON.stringify(cleaned, null, 2)}\n`;
|
|
1199
|
+
if (before === after) return null;
|
|
1200
|
+
const backup = backupConfig(target, context, dryRun);
|
|
1201
|
+
if (!dryRun) writeAtomic(target, after);
|
|
1202
|
+
return backup;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function cleanKimiHook(target, context, dryRun) {
|
|
1206
|
+
const text = readText(target);
|
|
1207
|
+
const start = text.indexOf(KIMI_START);
|
|
1208
|
+
const end = text.indexOf(KIMI_END, start + KIMI_START.length);
|
|
1209
|
+
if (start < 0 || end < 0) return null;
|
|
1210
|
+
const backup = backupConfig(target, context, dryRun);
|
|
1211
|
+
const after = `${text.slice(0, start)}${text.slice(end + KIMI_END.length)}`.replace(/\n{3,}/g, '\n\n');
|
|
1212
|
+
if (!dryRun) writeAtomic(target, after);
|
|
1213
|
+
return backup;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function cleanCodexLegacyHook(target, context, dryRun) {
|
|
1217
|
+
const text = readText(target);
|
|
1218
|
+
const after = stripDelimitedBlock(text, CODEX_LEGACY_START, CODEX_LEGACY_END);
|
|
1219
|
+
if (after === text) return null;
|
|
1220
|
+
const backup = backupConfig(target, context, dryRun);
|
|
1221
|
+
if (!dryRun) writeAtomic(target, after);
|
|
1222
|
+
return backup;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function cleanHermesConfig(target, context, dryRun) {
|
|
1226
|
+
const text = readText(target);
|
|
1227
|
+
const after = text.split(/(?<=\n)/).filter((line) => !/^\s*-\s*orca-status\s*(?:\r?\n)?$/.test(line)).join('');
|
|
1228
|
+
if (after === text) return null;
|
|
1229
|
+
const backup = backupConfig(target, context, dryRun);
|
|
1230
|
+
if (!dryRun) writeAtomic(target, after);
|
|
1231
|
+
return backup;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function editText(target, after, context, dryRun) {
|
|
1235
|
+
if (after === readText(target)) return null;
|
|
1236
|
+
const backup = backupConfig(target, context, dryRun);
|
|
1237
|
+
if (!dryRun) writeAtomic(target, after);
|
|
1238
|
+
return backup;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function cleanJcodeHook(target, context, dryRun) {
|
|
1242
|
+
const text = readText(target);
|
|
1243
|
+
const managed = jcodeManagedHookLines(text);
|
|
1244
|
+
if (managed.size === 0) return null;
|
|
1245
|
+
const { lines, eol } = splitLinesWithEol(text);
|
|
1246
|
+
return editText(target, lines.filter((_, index) => !managed.has(index)).join(eol), context, dryRun);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function cleanDshHook(target, context, dryRun) {
|
|
1250
|
+
const text = readText(target);
|
|
1251
|
+
const { lines, eol } = splitLinesWithEol(text);
|
|
1252
|
+
const region = dshManagedRegion(lines);
|
|
1253
|
+
if (!region) return null;
|
|
1254
|
+
const kept = [...lines.slice(0, region.start), ...lines.slice(region.end + 1)];
|
|
1255
|
+
while (kept.length > 0 && kept[kept.length - 1].trim() === '') kept.pop();
|
|
1256
|
+
// 주석만 남으면 DSH가 읽지 못하므로 Orca와 같이 빈 목록을 되돌려 놓는다.
|
|
1257
|
+
if (kept.every((line) => line.trim() === '' || line.trim().startsWith('#'))) kept.push('[]');
|
|
1258
|
+
return editText(target, `${kept.join(eol)}${eol}`, context, dryRun);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function cleanMuseHook(target, context, dryRun) {
|
|
1262
|
+
const value = readJson(target);
|
|
1263
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
1264
|
+
throw new Error(`Muse 설정을 해석할 수 없어 자동 수정하지 않았습니다: ${target}`);
|
|
1265
|
+
}
|
|
1266
|
+
if (!isOrcaHookString(value.managed_hooks_path)) return null;
|
|
1267
|
+
const next = { ...value };
|
|
1268
|
+
delete next.managed_hooks_path;
|
|
1269
|
+
if (Array.isArray(next.managed_hooks_env_vars)) {
|
|
1270
|
+
const kept = next.managed_hooks_env_vars.filter((name) => !MUSE_ORCA_ENV_VARS.has(name));
|
|
1271
|
+
if (kept.length > 0) next.managed_hooks_env_vars = kept;
|
|
1272
|
+
else delete next.managed_hooks_env_vars;
|
|
1273
|
+
}
|
|
1274
|
+
return editText(target, `${JSON.stringify(next, null, 2)}\n`, context, dryRun);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function cleanOpenCodeTuiConfig(target, context, dryRun) {
|
|
1278
|
+
const value = readJson(target);
|
|
1279
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || !Array.isArray(value.plugin)) {
|
|
1280
|
+
throw new Error(`OpenCode TUI 설정을 해석할 수 없어 자동 수정하지 않았습니다: ${target}`);
|
|
1281
|
+
}
|
|
1282
|
+
const kept = value.plugin.filter((item) => !isOrcaOpenCodeTuiEntry(item));
|
|
1283
|
+
if (kept.length === value.plugin.length) return null;
|
|
1284
|
+
const next = { ...value, plugin: kept };
|
|
1285
|
+
if (kept.length === 0) delete next.plugin;
|
|
1286
|
+
return editText(target, `${JSON.stringify(next, null, 2)}\n`, context, dryRun);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
const HOOK_EDITORS = Object.freeze({
|
|
1290
|
+
json: cleanJsonHook,
|
|
1291
|
+
kimi: cleanKimiHook,
|
|
1292
|
+
'hermes-config': cleanHermesConfig,
|
|
1293
|
+
jcode: cleanJcodeHook,
|
|
1294
|
+
dsh: cleanDshHook,
|
|
1295
|
+
muse: cleanMuseHook,
|
|
1296
|
+
'opencode-tui': cleanOpenCodeTuiConfig,
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
function cleanWindowsUserPath(context, dryRun) {
|
|
1300
|
+
const parts = (context.userPath || '').split(';');
|
|
1301
|
+
const next = parts.filter((entry) => !isWindowsOrcaPathEntry(entry, context)).join(';');
|
|
1302
|
+
if (next === context.userPath) return null;
|
|
1303
|
+
const backup = path.join(context.backupRoot, 'config-original', 'windows-user-path.txt');
|
|
1304
|
+
if (!dryRun) {
|
|
1305
|
+
ensureParent(backup);
|
|
1306
|
+
fs.writeFileSync(backup, `${context.userPath}\n`, { encoding: 'utf8', flag: 'wx' });
|
|
1307
|
+
if (process.platform !== 'win32') throw new Error('Windows 사용자 PATH는 Windows에서만 수정할 수 있습니다.');
|
|
1308
|
+
const script = "[Environment]::SetEnvironmentVariable('Path',$env:ORCA_CLEAN_PATH,'User')";
|
|
1309
|
+
const result = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
1310
|
+
encoding: 'utf8', env: { ...process.env, ORCA_CLEAN_PATH: next },
|
|
1311
|
+
});
|
|
1312
|
+
if (result.status !== 0) throw new Error(`Windows 사용자 PATH 수정 실패: ${result.stderr.trim()}`);
|
|
1313
|
+
context.userPath = next;
|
|
1314
|
+
}
|
|
1315
|
+
return backup;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function removeLockEntries(target, names, context, dryRun) {
|
|
1319
|
+
const value = readJson(target);
|
|
1320
|
+
if (!value) throw new Error(`잠금 JSON을 해석할 수 없습니다: ${target}`);
|
|
1321
|
+
const wanted = new Set(names);
|
|
1322
|
+
let changed = false;
|
|
1323
|
+
const containers = [value.skills, value.installed, value.dependencies, value];
|
|
1324
|
+
for (const container of containers) {
|
|
1325
|
+
if (!container || typeof container !== 'object' || Array.isArray(container)) continue;
|
|
1326
|
+
for (const [name, details] of Object.entries(container)) {
|
|
1327
|
+
if (!wanted.has(name)) continue;
|
|
1328
|
+
const source = typeof details === 'string'
|
|
1329
|
+
? details
|
|
1330
|
+
: [details?.source, details?.sourceUrl, details?.repository, details?.repo, details?.url]
|
|
1331
|
+
.filter(Boolean).join(' ');
|
|
1332
|
+
if (ORCA_SOURCE_PATTERN.test(source)) {
|
|
1333
|
+
delete container[name];
|
|
1334
|
+
changed = true;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (!changed) return null;
|
|
1339
|
+
const backup = backupConfig(target, context, dryRun);
|
|
1340
|
+
if (!dryRun) writeAtomic(target, `${JSON.stringify(value, null, 2)}\n`);
|
|
1341
|
+
return backup;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function selected(finding, options) {
|
|
1345
|
+
return !finding.requires || Boolean(options[finding.requires]);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function plannedMutationForFinding(item) {
|
|
1349
|
+
if (item.kind === 'skill-lock' || item.kind === 'path-entry') return 'edit';
|
|
1350
|
+
if (item.kind !== 'hook' || item.removeWhole) return 'quarantine';
|
|
1351
|
+
if (Object.hasOwn(HOOK_EDITORS, item.hookType) || item.hookType === 'codex-legacy') return 'edit';
|
|
1352
|
+
return 'quarantine';
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function plannedBackupForFinding(item, context, mutation) {
|
|
1356
|
+
if (mutation === 'edit') {
|
|
1357
|
+
return item.kind === 'path-entry'
|
|
1358
|
+
? path.join(context.backupRoot, 'config-original', 'windows-user-path.txt')
|
|
1359
|
+
: path.join(context.backupRoot, 'config-original', safeRelative(item.path));
|
|
1360
|
+
}
|
|
1361
|
+
const category = item.kind.replace(/[^a-z0-9-]/gi, '-');
|
|
1362
|
+
return path.join(context.backupRoot, category, safeRelative(item.path));
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function cleanupJournalValue(context, createdAt, cleanupStatus, actions, errors) {
|
|
1366
|
+
return signedCleanupManifest({
|
|
1367
|
+
manifestVersion: 2,
|
|
1368
|
+
createdAt,
|
|
1369
|
+
platform: context.platform,
|
|
1370
|
+
backupRoot: context.backupRoot,
|
|
1371
|
+
cleanupStatus,
|
|
1372
|
+
actions,
|
|
1373
|
+
errors,
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function initializeCleanupJournal(context, createdAt, actions, errors) {
|
|
1378
|
+
if (pathExists(context.backupRoot) && fs.readdirSync(context.backupRoot).length > 0) {
|
|
1379
|
+
throw new Error(`비어 있지 않은 백업 폴더는 사용할 수 없습니다: ${context.backupRoot}`);
|
|
1380
|
+
}
|
|
1381
|
+
fs.mkdirSync(context.backupRoot, { recursive: true });
|
|
1382
|
+
const manifestPath = path.join(context.backupRoot, 'manifest.json');
|
|
1383
|
+
const manifest = cleanupJournalValue(context, createdAt, 'in-progress', actions, errors);
|
|
1384
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' });
|
|
1385
|
+
return manifestPath;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function updateCleanupJournal(manifestPath, context, createdAt, cleanupStatus, actions, errors) {
|
|
1389
|
+
const manifest = cleanupJournalValue(context, createdAt, cleanupStatus, actions, errors);
|
|
1390
|
+
writeAtomic(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
export function cleanOrcaResidue(context, options = {}) {
|
|
1394
|
+
const dryRun = Boolean(options.dryRun);
|
|
1395
|
+
const findings = scanOrcaResidue(context);
|
|
1396
|
+
const actions = [];
|
|
1397
|
+
const errors = [];
|
|
1398
|
+
const createdAt = new Date().toISOString();
|
|
1399
|
+
const blockers = findings.filter((item) => item.kind === 'app-installed' || item.kind === 'app-running');
|
|
1400
|
+
const blocked = blockers.length > 0 && !options.force;
|
|
1401
|
+
if (blocked && !dryRun) {
|
|
1402
|
+
errors.push({
|
|
1403
|
+
path: blockers[0].path,
|
|
1404
|
+
message: 'Orca 앱이 아직 설치돼 있거나 실행 중이라 정리하지 않았습니다. 앱이 켜질 때 훅을 다시 설치하기 때문입니다. 앱을 먼저 제거하고, 그래도 진행하려면 --force를 붙이세요.',
|
|
1405
|
+
});
|
|
1406
|
+
return { findings, actions, errors, backupRoot: context.backupRoot, dryRun, blocked };
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// 백그라운드 프로세스가 앱 데이터를 붙잡고 있으면 격리가 실패하므로 먼저 끈다. 되돌릴 수 없는 작업이라 복원 대상이 아니다.
|
|
1410
|
+
for (const item of findings.filter((candidate) => candidate.kind === 'process' && selected(candidate, options))) {
|
|
1411
|
+
if (dryRun) {
|
|
1412
|
+
actions.push({ action: 'would-stop', ...item });
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
try {
|
|
1416
|
+
context.stopProcess(item.pid);
|
|
1417
|
+
actions.push({ action: 'stopped', ...item });
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
errors.push({ path: item.path, message: error.message });
|
|
1420
|
+
actions.push({ action: 'error', error: error.message, ...item });
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
const hasSelectedWork = findings.some((item) => item.kind !== 'process' && selected(item, options));
|
|
1425
|
+
let manifestPath = null;
|
|
1426
|
+
if (!dryRun && hasSelectedWork) {
|
|
1427
|
+
try {
|
|
1428
|
+
for (const item of findings.filter((candidate) => selected(candidate, options))) {
|
|
1429
|
+
if (plannedMutationForFinding(item) === 'quarantine') assertSafeQuarantineSource(item.path, context);
|
|
1430
|
+
}
|
|
1431
|
+
manifestPath = initializeCleanupJournal(context, createdAt, actions, errors);
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
errors.push({ path: context.backupRoot, message: error.message });
|
|
1434
|
+
actions.push({ action: 'error', path: context.backupRoot, error: error.message });
|
|
1435
|
+
return { findings, actions, errors, backupRoot: context.backupRoot, dryRun };
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
const selectedUserStateRoots = findings
|
|
1440
|
+
.filter((item) => item.kind === 'user-state' && selected(item, options))
|
|
1441
|
+
.map((item) => item.path);
|
|
1442
|
+
|
|
1443
|
+
for (const item of findings) {
|
|
1444
|
+
if (item.kind === 'process' && selected(item, options)) continue;
|
|
1445
|
+
if (item.kind !== 'user-state'
|
|
1446
|
+
&& selectedUserStateRoots.some((root) => isSameOrDescendant(root, item.path))) {
|
|
1447
|
+
actions.push({ action: 'covered', ...item });
|
|
1448
|
+
if (manifestPath) updateCleanupJournal(manifestPath, context, createdAt, 'in-progress', actions, errors);
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
if (!selected(item, options)) {
|
|
1452
|
+
actions.push({ action: 'skipped', ...item });
|
|
1453
|
+
if (manifestPath) updateCleanupJournal(manifestPath, context, createdAt, 'in-progress', actions, errors);
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
let pendingIndex = -1;
|
|
1457
|
+
if (manifestPath) {
|
|
1458
|
+
const mutation = plannedMutationForFinding(item);
|
|
1459
|
+
const pending = {
|
|
1460
|
+
action: mutation === 'edit' ? 'pending-edit' : 'pending-quarantine',
|
|
1461
|
+
backup: plannedBackupForFinding(item, context, mutation),
|
|
1462
|
+
...item,
|
|
1463
|
+
};
|
|
1464
|
+
pendingIndex = actions.push(pending) - 1;
|
|
1465
|
+
updateCleanupJournal(manifestPath, context, createdAt, 'in-progress', actions, errors);
|
|
1466
|
+
}
|
|
1467
|
+
try {
|
|
1468
|
+
let action;
|
|
1469
|
+
if (item.kind === 'hook' && !item.removeWhole && Object.hasOwn(HOOK_EDITORS, item.hookType)) {
|
|
1470
|
+
const backup = HOOK_EDITORS[item.hookType](item.path, context, dryRun);
|
|
1471
|
+
action = { action: backup ? (dryRun ? 'would-edit' : 'edited') : 'unchanged', backup, ...item };
|
|
1472
|
+
} else if (item.kind === 'hook' && item.hookType === 'codex-legacy' && !item.removeWhole) {
|
|
1473
|
+
const backup = cleanCodexLegacyHook(item.path, context, dryRun);
|
|
1474
|
+
action = { action: backup ? (dryRun ? 'would-edit' : 'edited') : 'unchanged', backup, ...item };
|
|
1475
|
+
} else if (item.kind === 'skill-lock') {
|
|
1476
|
+
const backup = removeLockEntries(item.path, item.names, context, dryRun);
|
|
1477
|
+
action = { action: backup ? (dryRun ? 'would-edit' : 'edited') : 'unchanged', backup, ...item };
|
|
1478
|
+
} else if (item.kind === 'path-entry') {
|
|
1479
|
+
const backup = cleanWindowsUserPath(context, dryRun);
|
|
1480
|
+
action = { action: backup ? (dryRun ? 'would-edit' : 'edited') : 'unchanged', backup, ...item };
|
|
1481
|
+
} else {
|
|
1482
|
+
const category = item.kind.replace(/[^a-z0-9-]/gi, '-');
|
|
1483
|
+
const backup = quarantine(item.path, context, category, dryRun);
|
|
1484
|
+
action = { action: dryRun ? 'would-quarantine' : 'quarantined', backup, ...item };
|
|
1485
|
+
}
|
|
1486
|
+
if (!dryRun && ['edited', 'quarantined'].includes(action.action)) {
|
|
1487
|
+
action.backupIntegrity = pathIntegrity(action.backup);
|
|
1488
|
+
}
|
|
1489
|
+
if (pendingIndex >= 0) actions[pendingIndex] = action;
|
|
1490
|
+
else actions.push(action);
|
|
1491
|
+
} catch (error) {
|
|
1492
|
+
errors.push({ path: item.path, message: error.message });
|
|
1493
|
+
if (pendingIndex >= 0) actions[pendingIndex] = { ...actions[pendingIndex], error: error.message };
|
|
1494
|
+
else actions.push({ action: 'error', error: error.message, ...item });
|
|
1495
|
+
}
|
|
1496
|
+
if (manifestPath) updateCleanupJournal(manifestPath, context, createdAt, 'in-progress', actions, errors);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
if (manifestPath) updateCleanupJournal(manifestPath, context, createdAt, 'complete', actions, errors);
|
|
1500
|
+
return { findings, actions, errors, backupRoot: context.backupRoot, dryRun, blocked };
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
function assertSafeRestorePath(target, backupRoot, home) {
|
|
1504
|
+
const resolved = path.resolve(target);
|
|
1505
|
+
if (!path.isAbsolute(target) || resolved === path.parse(resolved).root || resolved === path.resolve(home)) {
|
|
1506
|
+
throw new Error(`복원 대상이 안전하지 않습니다: ${target}`);
|
|
1507
|
+
}
|
|
1508
|
+
if (isSameOrDescendant(backupRoot, resolved) || isSameOrDescendant(resolved, backupRoot)) {
|
|
1509
|
+
throw new Error(`복원 대상과 백업 폴더가 겹칩니다: ${resolved}`);
|
|
1510
|
+
}
|
|
1511
|
+
return resolved;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function assertBackupBelongsToManifest(backup, backupRoot) {
|
|
1515
|
+
if (!backup || !path.isAbsolute(backup)) throw new Error(`유효하지 않은 백업 경로입니다: ${backup || '(없음)'}`);
|
|
1516
|
+
const resolved = path.resolve(backup);
|
|
1517
|
+
if (!isSameOrDescendant(backupRoot, resolved) || resolved === backupRoot) {
|
|
1518
|
+
throw new Error(`manifest 밖의 백업 경로는 복원하지 않습니다: ${resolved}`);
|
|
1519
|
+
}
|
|
1520
|
+
return resolved;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
const QUARANTINED_RESTORE_KINDS = new Set([
|
|
1524
|
+
'app-data', 'cli', 'extension', 'hook', 'project-state', 'remote-state',
|
|
1525
|
+
'shared-state', 'skill', 'skill-transaction', 'user-state', 'voice-data', 'wsl-state',
|
|
1526
|
+
]);
|
|
1527
|
+
const EDITED_RESTORE_KINDS = new Set(['hook', 'path-entry', 'skill-lock']);
|
|
1528
|
+
|
|
1529
|
+
function sameManifestPath(left, right, platform) {
|
|
1530
|
+
const normalizedLeft = path.resolve(left);
|
|
1531
|
+
const normalizedRight = path.resolve(right);
|
|
1532
|
+
return platform === 'win32'
|
|
1533
|
+
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
|
|
1534
|
+
: normalizedLeft === normalizedRight;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
function expectedBackupForRestoreItem(item, backupRoot) {
|
|
1538
|
+
const sourceAction = item.action === 'pending-quarantine'
|
|
1539
|
+
? 'quarantined'
|
|
1540
|
+
: item.action === 'pending-edit' ? 'edited' : item.action;
|
|
1541
|
+
if (sourceAction === 'quarantined' && QUARANTINED_RESTORE_KINDS.has(item.kind)) {
|
|
1542
|
+
const category = item.kind.replace(/[^a-z0-9-]/gi, '-');
|
|
1543
|
+
return path.join(backupRoot, category, safeRelative(item.path));
|
|
1544
|
+
}
|
|
1545
|
+
if (sourceAction === 'edited' && EDITED_RESTORE_KINDS.has(item.kind)) {
|
|
1546
|
+
return item.kind === 'path-entry'
|
|
1547
|
+
? path.join(backupRoot, 'config-original', 'windows-user-path.txt')
|
|
1548
|
+
: path.join(backupRoot, 'config-original', safeRelative(item.path));
|
|
1549
|
+
}
|
|
1550
|
+
throw new Error(`지원하지 않는 복원 작업입니다: ${item.action}/${item.kind}`);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function assertBackupContentIntegrity(item, backup, manifestVersion) {
|
|
1554
|
+
if (manifestVersion < 2 || item.action.startsWith('pending-')) return;
|
|
1555
|
+
if (!integrityMatches(backup, item.backupIntegrity)) {
|
|
1556
|
+
throw new Error(`백업 내용 무결성 검증에 실패했습니다: ${backup}`);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
function assertExpectedBackupForItem(item, state) {
|
|
1561
|
+
const backup = assertBackupBelongsToManifest(item.backup, state.backupRoot);
|
|
1562
|
+
const expected = expectedBackupForRestoreItem(item, state.backupRoot);
|
|
1563
|
+
if (!sameManifestPath(backup, expected, state.platform)) {
|
|
1564
|
+
throw new Error(`대상 경로와 백업 경로의 매핑이 일치하지 않습니다: ${item.path}`);
|
|
1565
|
+
}
|
|
1566
|
+
return backup;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
function validateCleanupManifest(manifest, resolvedManifest, platform) {
|
|
1570
|
+
if (![1, 2].includes(manifest.manifestVersion)) {
|
|
1571
|
+
throw new Error(`지원하지 않는 manifest 버전입니다: ${manifest.manifestVersion ?? '(없음)'}`);
|
|
1572
|
+
}
|
|
1573
|
+
if (!['win32', 'darwin', 'linux'].includes(manifest.platform) || manifest.platform !== platform) {
|
|
1574
|
+
throw new Error(`manifest 운영체제가 현재 복원 환경과 다릅니다: ${manifest.platform ?? '(없음)'} -> ${platform}`);
|
|
1575
|
+
}
|
|
1576
|
+
const backupRoot = path.dirname(resolvedManifest);
|
|
1577
|
+
if (!manifest.backupRoot || !sameManifestPath(manifest.backupRoot, backupRoot, platform)) {
|
|
1578
|
+
throw new Error('manifest의 backupRoot가 manifest 위치와 일치하지 않습니다.');
|
|
1579
|
+
}
|
|
1580
|
+
if (!Array.isArray(manifest.actions) || !Array.isArray(manifest.errors)) {
|
|
1581
|
+
throw new Error('manifest의 actions 또는 errors 형식이 올바르지 않습니다.');
|
|
1582
|
+
}
|
|
1583
|
+
if (manifest.manifestVersion === 2 && !['in-progress', 'complete'].includes(manifest.cleanupStatus)) {
|
|
1584
|
+
throw new Error(`manifest 정리 상태가 올바르지 않습니다: ${manifest.cleanupStatus ?? '(없음)'}`);
|
|
1585
|
+
}
|
|
1586
|
+
if (manifest.integrity?.algorithm !== 'sha256'
|
|
1587
|
+
|| manifest.integrity.digest !== cleanupManifestDigest(manifest)) {
|
|
1588
|
+
throw new Error('manifest 무결성 검증에 실패했습니다. 파일이 수정되었을 수 있습니다.');
|
|
1589
|
+
}
|
|
1590
|
+
return backupRoot;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
export function verifyOrcaBackup(manifestPath, options = {}) {
|
|
1594
|
+
const resolvedManifest = path.resolve(manifestPath);
|
|
1595
|
+
const manifest = readJson(resolvedManifest);
|
|
1596
|
+
if (!manifest) throw new Error(`유효한 Orca 정리 manifest가 아닙니다: ${resolvedManifest}`);
|
|
1597
|
+
const platform = options.platform || process.platform;
|
|
1598
|
+
const backupRoot = validateCleanupManifest(manifest, resolvedManifest, platform);
|
|
1599
|
+
const actions = [];
|
|
1600
|
+
const errors = [];
|
|
1601
|
+
const restorable = manifest.actions.filter((item) => [
|
|
1602
|
+
'quarantined', 'edited', 'pending-quarantine', 'pending-edit',
|
|
1603
|
+
].includes(item.action));
|
|
1604
|
+
|
|
1605
|
+
for (const item of restorable) {
|
|
1606
|
+
try {
|
|
1607
|
+
const backup = assertExpectedBackupForItem(item, { backupRoot, platform });
|
|
1608
|
+
if (!pathExists(backup)) {
|
|
1609
|
+
if (item.action.startsWith('pending-') && pathExists(item.path)) {
|
|
1610
|
+
actions.push({ path: item.path, backup, status: 'not-applied' });
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
throw new Error(`백업 항목이 없습니다: ${backup}`);
|
|
1614
|
+
}
|
|
1615
|
+
if (!item.backupIntegrity) {
|
|
1616
|
+
actions.push({ path: item.path, backup, status: item.action.startsWith('pending-')
|
|
1617
|
+
? 'recoverable-unverified' : 'not-recorded' });
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
if (!integrityMatches(backup, item.backupIntegrity)) {
|
|
1621
|
+
throw new Error(`백업 내용 무결성 검증에 실패했습니다: ${backup}`);
|
|
1622
|
+
}
|
|
1623
|
+
actions.push({ path: item.path, backup, status: 'verified' });
|
|
1624
|
+
} catch (error) {
|
|
1625
|
+
errors.push({ path: item.path, message: error.message });
|
|
1626
|
+
actions.push({ path: item.path, backup: item.backup, status: 'error', error: error.message });
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
return {
|
|
1630
|
+
manifestPath: resolvedManifest,
|
|
1631
|
+
manifestVersion: manifest.manifestVersion,
|
|
1632
|
+
cleanupStatus: manifest.cleanupStatus || 'legacy-complete',
|
|
1633
|
+
backupRoot,
|
|
1634
|
+
actions,
|
|
1635
|
+
errors,
|
|
1636
|
+
valid: errors.length === 0,
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function copyForRestore(source, destination) {
|
|
1641
|
+
const stat = fs.lstatSync(source);
|
|
1642
|
+
ensureParent(destination);
|
|
1643
|
+
if (stat.isSymbolicLink()) {
|
|
1644
|
+
fs.symlinkSync(fs.readlinkSync(source), destination);
|
|
1645
|
+
} else if (stat.isDirectory()) {
|
|
1646
|
+
fs.cpSync(source, destination, { recursive: true, force: false, errorOnExist: true });
|
|
1647
|
+
} else if (stat.isFile()) {
|
|
1648
|
+
fs.copyFileSync(source, destination, fs.constants.COPYFILE_EXCL);
|
|
1649
|
+
} else {
|
|
1650
|
+
throw new Error(`지원하지 않는 백업 파일 형식입니다: ${source}`);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
function filesHaveSameBytes(left, right, size) {
|
|
1655
|
+
const leftHandle = fs.openSync(left, 'r');
|
|
1656
|
+
const rightHandle = fs.openSync(right, 'r');
|
|
1657
|
+
const leftBuffer = Buffer.allocUnsafe(64 * 1024);
|
|
1658
|
+
const rightBuffer = Buffer.allocUnsafe(64 * 1024);
|
|
1659
|
+
try {
|
|
1660
|
+
let offset = 0;
|
|
1661
|
+
while (offset < size) {
|
|
1662
|
+
const length = Math.min(leftBuffer.length, size - offset);
|
|
1663
|
+
const leftRead = fs.readSync(leftHandle, leftBuffer, 0, length, offset);
|
|
1664
|
+
const rightRead = fs.readSync(rightHandle, rightBuffer, 0, length, offset);
|
|
1665
|
+
if (leftRead === 0 || rightRead === 0) return false;
|
|
1666
|
+
if (leftRead !== rightRead || !leftBuffer.subarray(0, leftRead).equals(rightBuffer.subarray(0, rightRead))) return false;
|
|
1667
|
+
offset += leftRead;
|
|
1668
|
+
}
|
|
1669
|
+
return true;
|
|
1670
|
+
} finally {
|
|
1671
|
+
fs.closeSync(leftHandle);
|
|
1672
|
+
fs.closeSync(rightHandle);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
function pathsEquivalent(left, right) {
|
|
1677
|
+
try {
|
|
1678
|
+
const leftStat = fs.lstatSync(left);
|
|
1679
|
+
const rightStat = fs.lstatSync(right);
|
|
1680
|
+
if (leftStat.isSymbolicLink() || rightStat.isSymbolicLink()) {
|
|
1681
|
+
return leftStat.isSymbolicLink() && rightStat.isSymbolicLink()
|
|
1682
|
+
&& fs.readlinkSync(left) === fs.readlinkSync(right);
|
|
1683
|
+
}
|
|
1684
|
+
if (leftStat.isFile() || rightStat.isFile()) {
|
|
1685
|
+
return leftStat.isFile() && rightStat.isFile() && leftStat.size === rightStat.size
|
|
1686
|
+
&& filesHaveSameBytes(left, right, leftStat.size);
|
|
1687
|
+
}
|
|
1688
|
+
if (!leftStat.isDirectory() || !rightStat.isDirectory()) return false;
|
|
1689
|
+
const leftEntries = fs.readdirSync(left).sort();
|
|
1690
|
+
const rightEntries = fs.readdirSync(right).sort();
|
|
1691
|
+
return leftEntries.length === rightEntries.length
|
|
1692
|
+
&& leftEntries.every((name, index) => name === rightEntries[index]
|
|
1693
|
+
&& pathsEquivalent(path.join(left, name), path.join(right, name)));
|
|
1694
|
+
} catch {
|
|
1695
|
+
return false;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function removePartialRestore(target) {
|
|
1700
|
+
if (!pathExists(target)) return;
|
|
1701
|
+
fs.rmSync(target, { recursive: true, force: false });
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
function setWindowsUserPath(value) {
|
|
1705
|
+
if (process.platform !== 'win32') throw new Error('Windows 사용자 PATH는 Windows에서만 복원할 수 있습니다.');
|
|
1706
|
+
const script = "[Environment]::SetEnvironmentVariable('Path',$env:ORCA_RESTORE_PATH,'User')";
|
|
1707
|
+
const result = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
1708
|
+
encoding: 'utf8', env: { ...process.env, ORCA_RESTORE_PATH: value },
|
|
1709
|
+
});
|
|
1710
|
+
if (result.status !== 0) throw new Error(`Windows 사용자 PATH 복원 실패: ${result.stderr.trim()}`);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
function restoreWindowsPathEntry(item, state) {
|
|
1714
|
+
const backup = assertExpectedBackupForItem(item, state);
|
|
1715
|
+
if (!pathExists(backup)) {
|
|
1716
|
+
if (item.action === 'pending-edit') return { action: 'already-restored', conflictBackup: null };
|
|
1717
|
+
throw new Error(`PATH 백업이 없습니다: ${backup}`);
|
|
1718
|
+
}
|
|
1719
|
+
assertBackupContentIntegrity(item, backup, state.manifestVersion);
|
|
1720
|
+
const original = readText(backup).replace(/\r?\n$/, '');
|
|
1721
|
+
const current = state.readUserPath();
|
|
1722
|
+
if (current === original) return { action: 'already-restored', conflictBackup: null };
|
|
1723
|
+
const conflictBackup = path.join(state.conflictRoot, 'windows-user-path.txt');
|
|
1724
|
+
if (state.dryRun) return { action: 'would-restore', conflictBackup };
|
|
1725
|
+
ensureParent(conflictBackup);
|
|
1726
|
+
fs.writeFileSync(conflictBackup, `${current}\n`, { encoding: 'utf8', flag: 'wx' });
|
|
1727
|
+
try {
|
|
1728
|
+
state.writeUserPath(original);
|
|
1729
|
+
} catch (error) {
|
|
1730
|
+
try { state.writeUserPath(current); } catch { /* 원래 PATH 백업은 conflictBackup에 남긴다. */ }
|
|
1731
|
+
throw error;
|
|
1732
|
+
}
|
|
1733
|
+
return { action: 'restored', conflictBackup };
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
function restoreFilesystemItem(item, state) {
|
|
1737
|
+
const source = assertExpectedBackupForItem(item, state);
|
|
1738
|
+
const target = assertSafeRestorePath(item.path, state.backupRoot, state.home);
|
|
1739
|
+
if (!pathExists(source)) {
|
|
1740
|
+
if (pathExists(target)) return { action: 'already-restored', conflictBackup: null };
|
|
1741
|
+
throw new Error(`복원할 백업이 없습니다: ${source}`);
|
|
1742
|
+
}
|
|
1743
|
+
assertBackupContentIntegrity(item, source, state.manifestVersion);
|
|
1744
|
+
if (pathExists(target) && pathsEquivalent(source, target)) {
|
|
1745
|
+
return { action: 'already-restored', conflictBackup: null };
|
|
1746
|
+
}
|
|
1747
|
+
const conflictContext = { home: state.home, backupRoot: state.conflictRoot };
|
|
1748
|
+
const conflictBackup = pathExists(target)
|
|
1749
|
+
? quarantine(target, conflictContext, 'current', state.dryRun)
|
|
1750
|
+
: null;
|
|
1751
|
+
if (state.dryRun) return { action: 'would-restore', conflictBackup };
|
|
1752
|
+
try {
|
|
1753
|
+
copyForRestore(source, target);
|
|
1754
|
+
} catch (error) {
|
|
1755
|
+
try {
|
|
1756
|
+
removePartialRestore(target);
|
|
1757
|
+
if (conflictBackup && pathExists(conflictBackup)) copyForRestore(conflictBackup, target);
|
|
1758
|
+
} catch (rollbackError) {
|
|
1759
|
+
throw new Error(`${error.message}; 현재 파일 자동 복귀도 실패했습니다: ${rollbackError.message}`);
|
|
1760
|
+
}
|
|
1761
|
+
throw error;
|
|
1762
|
+
}
|
|
1763
|
+
return { action: 'restored', conflictBackup };
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
export function restoreOrcaBackup(manifestPath, options = {}) {
|
|
1767
|
+
const resolvedManifest = path.resolve(manifestPath);
|
|
1768
|
+
const manifest = readJson(resolvedManifest);
|
|
1769
|
+
if (!manifest) {
|
|
1770
|
+
throw new Error(`유효한 Orca 정리 manifest가 아닙니다: ${resolvedManifest}`);
|
|
1771
|
+
}
|
|
1772
|
+
const platform = options.platform || process.platform;
|
|
1773
|
+
const backupRoot = validateCleanupManifest(manifest, resolvedManifest, platform);
|
|
1774
|
+
const dryRun = Boolean(options.dryRun);
|
|
1775
|
+
const conflictRoot = path.join(backupRoot, 'restore-conflicts', operationId());
|
|
1776
|
+
const state = {
|
|
1777
|
+
backupRoot,
|
|
1778
|
+
conflictRoot,
|
|
1779
|
+
dryRun,
|
|
1780
|
+
home: path.resolve(options.home || os.homedir()),
|
|
1781
|
+
platform,
|
|
1782
|
+
manifestVersion: manifest.manifestVersion,
|
|
1783
|
+
readUserPath: options.readUserPath || (() => readWindowsUserPath('win32')),
|
|
1784
|
+
writeUserPath: options.writeUserPath || setWindowsUserPath,
|
|
1785
|
+
};
|
|
1786
|
+
const actions = [];
|
|
1787
|
+
const errors = [];
|
|
1788
|
+
const restorable = manifest.actions.filter((item) => [
|
|
1789
|
+
'quarantined', 'edited', 'pending-quarantine', 'pending-edit',
|
|
1790
|
+
].includes(item.action));
|
|
1791
|
+
|
|
1792
|
+
for (const item of restorable) {
|
|
1793
|
+
const base = {
|
|
1794
|
+
kind: item.kind,
|
|
1795
|
+
path: item.path,
|
|
1796
|
+
backup: item.backup,
|
|
1797
|
+
sourceAction: item.action,
|
|
1798
|
+
};
|
|
1799
|
+
try {
|
|
1800
|
+
const result = item.kind === 'path-entry'
|
|
1801
|
+
? restoreWindowsPathEntry(item, state)
|
|
1802
|
+
: restoreFilesystemItem(item, state);
|
|
1803
|
+
actions.push({ ...base, ...result });
|
|
1804
|
+
} catch (error) {
|
|
1805
|
+
errors.push({ path: item.path, message: error.message });
|
|
1806
|
+
actions.push({ ...base, action: 'error', error: error.message });
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
let reportPath = null;
|
|
1811
|
+
if (!dryRun) {
|
|
1812
|
+
reportPath = path.join(backupRoot, `restore-manifest-${operationId()}.json`);
|
|
1813
|
+
fs.writeFileSync(reportPath, `${JSON.stringify({
|
|
1814
|
+
restoreVersion: 1,
|
|
1815
|
+
restoredAt: new Date().toISOString(),
|
|
1816
|
+
sourceManifest: resolvedManifest,
|
|
1817
|
+
conflictRoot: actions.some((item) => item.conflictBackup) ? conflictRoot : null,
|
|
1818
|
+
actions,
|
|
1819
|
+
errors,
|
|
1820
|
+
}, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' });
|
|
1821
|
+
}
|
|
1822
|
+
return {
|
|
1823
|
+
manifestPath: resolvedManifest,
|
|
1824
|
+
manifestVersion: manifest.manifestVersion,
|
|
1825
|
+
cleanupStatus: manifest.cleanupStatus || 'legacy-complete',
|
|
1826
|
+
backupRoot,
|
|
1827
|
+
conflictRoot,
|
|
1828
|
+
reportPath,
|
|
1829
|
+
actions,
|
|
1830
|
+
errors,
|
|
1831
|
+
dryRun,
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
export const internals = {
|
|
1836
|
+
CODEX_LEGACY_END,
|
|
1837
|
+
CODEX_LEGACY_START,
|
|
1838
|
+
DSH_END,
|
|
1839
|
+
DSH_START,
|
|
1840
|
+
KIMI_START,
|
|
1841
|
+
KIMI_END,
|
|
1842
|
+
isOrcaHookString,
|
|
1843
|
+
jcodeManagedHookLines,
|
|
1844
|
+
LINUX_CLI_DISPATCHER_MARKER,
|
|
1845
|
+
LINUX_CLI_COMMAND_NAME,
|
|
1846
|
+
hasOrcaSkillSignature,
|
|
1847
|
+
isOrcaCli,
|
|
1848
|
+
operationId,
|
|
1849
|
+
pruneJsonHooks,
|
|
1850
|
+
signedCleanupManifest,
|
|
1851
|
+
stripDelimitedBlock,
|
|
1852
|
+
};
|