triflux 3.3.0-dev.7 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +108 -199
- package/README.md +108 -199
- package/bin/triflux.mjs +2415 -1762
- package/hooks/keyword-rules.json +361 -354
- package/hooks/pipeline-stop.mjs +5 -2
- package/hub/assign-callbacks.mjs +136 -136
- package/hub/bridge.mjs +734 -708
- package/hub/delegator/contracts.mjs +38 -0
- package/hub/delegator/index.mjs +14 -0
- package/hub/delegator/schema/delegator-tools.schema.json +250 -0
- package/hub/delegator/service.mjs +302 -0
- package/hub/delegator/tool-definitions.mjs +35 -0
- package/hub/hitl.mjs +67 -67
- package/hub/paths.mjs +28 -0
- package/hub/pipe.mjs +589 -561
- package/hub/pipeline/state.mjs +23 -0
- package/hub/public/dashboard.html +349 -0
- package/hub/public/tray-icon.ico +0 -0
- package/hub/public/tray-icon.png +0 -0
- package/hub/router.mjs +782 -782
- package/hub/schema.sql +40 -40
- package/hub/server.mjs +810 -637
- package/hub/store.mjs +706 -706
- package/hub/team/cli/commands/attach.mjs +37 -0
- package/hub/team/cli/commands/control.mjs +43 -0
- package/hub/team/cli/commands/debug.mjs +74 -0
- package/hub/team/cli/commands/focus.mjs +53 -0
- package/hub/team/cli/commands/interrupt.mjs +36 -0
- package/hub/team/cli/commands/kill.mjs +37 -0
- package/hub/team/cli/commands/list.mjs +24 -0
- package/hub/team/cli/commands/send.mjs +37 -0
- package/hub/team/cli/commands/start/index.mjs +87 -0
- package/hub/team/cli/commands/start/parse-args.mjs +32 -0
- package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
- package/hub/team/cli/commands/start/start-mux.mjs +73 -0
- package/hub/team/cli/commands/start/start-wt.mjs +69 -0
- package/hub/team/cli/commands/status.mjs +87 -0
- package/hub/team/cli/commands/stop.mjs +31 -0
- package/hub/team/cli/commands/task.mjs +30 -0
- package/hub/team/cli/commands/tasks.mjs +13 -0
- package/hub/team/{cli.mjs → cli/help.mjs} +38 -99
- package/hub/team/cli/index.mjs +39 -0
- package/hub/team/cli/manifest.mjs +28 -0
- package/hub/team/cli/render.mjs +30 -0
- package/hub/team/cli/services/attach-fallback.mjs +54 -0
- package/hub/team/cli/services/hub-client.mjs +171 -0
- package/hub/team/cli/services/member-selector.mjs +30 -0
- package/hub/team/cli/services/native-control.mjs +115 -0
- package/hub/team/cli/services/runtime-mode.mjs +60 -0
- package/hub/team/cli/services/state-store.mjs +34 -0
- package/hub/team/cli/services/task-model.mjs +30 -0
- package/hub/team/native-supervisor.mjs +69 -63
- package/hub/team/native.mjs +367 -266
- package/hub/team/nativeProxy.mjs +217 -173
- package/hub/team/pane.mjs +149 -149
- package/hub/team/psmux.mjs +946 -946
- package/hub/team/session.mjs +608 -608
- package/hub/team/staleState.mjs +369 -299
- package/hub/tools.mjs +107 -107
- package/hub/tray.mjs +332 -0
- package/hub/workers/claude-worker.mjs +446 -446
- package/hub/workers/codex-mcp.mjs +414 -414
- package/hub/workers/delegator-mcp.mjs +1045 -1045
- package/hub/workers/factory.mjs +21 -21
- package/hub/workers/gemini-worker.mjs +349 -349
- package/hub/workers/interface.mjs +41 -41
- package/package.json +61 -60
- package/scripts/__tests__/keyword-detector.test.mjs +234 -234
- package/scripts/hub-ensure.mjs +102 -101
- package/scripts/keyword-detector.mjs +272 -272
- package/scripts/keyword-rules-expander.mjs +521 -521
- package/scripts/lib/keyword-rules.mjs +168 -168
- package/scripts/lib/mcp-filter.mjs +642 -642
- package/scripts/lib/mcp-server-catalog.mjs +118 -118
- package/scripts/mcp-check.mjs +126 -126
- package/scripts/preflight-cache.mjs +19 -0
- package/scripts/run.cjs +62 -62
- package/scripts/setup.mjs +68 -31
- package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
- package/scripts/tfx-route-worker.mjs +161 -161
- package/scripts/tfx-route.sh +1360 -1326
- package/skills/tfx-auto/SKILL.md +196 -196
- package/skills/tfx-auto-codex/SKILL.md +77 -77
- package/skills/tfx-multi/SKILL.md +378 -378
- package/hub/team/cli-team-common.mjs +0 -348
- package/hub/team/cli-team-control.mjs +0 -393
- package/hub/team/cli-team-start.mjs +0 -516
- package/hub/team/cli-team-status.mjs +0 -283
- package/skills/auto-verify/SKILL.md +0 -145
- package/skills/manage-skills/SKILL.md +0 -192
- package/skills/verify-implementation/SKILL.md +0 -138
package/bin/triflux.mjs
CHANGED
|
@@ -1,1762 +1,2415 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// triflux CLI — setup, doctor, version
|
|
3
|
-
import { copyFileSync, existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, readdirSync, unlinkSync, rmSync, statSync, openSync, closeSync } from "fs";
|
|
4
|
-
import { join, dirname } from "path";
|
|
5
|
-
import { homedir } from "os";
|
|
6
|
-
import { execSync, execFileSync, spawn } from "child_process";
|
|
7
|
-
import { fileURLToPath } from "url";
|
|
8
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
"
|
|
492
|
-
"
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
if (
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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
|
-
const
|
|
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
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
${DIM}
|
|
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
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
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
|
-
const
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
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
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
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
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
const
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// triflux CLI — setup, doctor, version
|
|
3
|
+
import { copyFileSync, existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, readdirSync, unlinkSync, rmSync, statSync, openSync, closeSync } from "fs";
|
|
4
|
+
import { join, dirname } from "path";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { execSync, execFileSync, spawn } from "child_process";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
9
|
+
import { loadDelegatorSchemaBundle } from "../hub/delegator/tool-definitions.mjs";
|
|
10
|
+
import { detectMultiplexer, getSessionAttachedCount, killSession, listSessions, tmuxExec } from "../hub/team/session.mjs";
|
|
11
|
+
import { forceCleanupTeam } from "../hub/team/nativeProxy.mjs";
|
|
12
|
+
import { cleanupStaleOmcTeams, inspectStaleOmcTeams } from "../hub/team/staleState.mjs";
|
|
13
|
+
import { getPipelineStateDbPath } from "../hub/pipeline/state.mjs";
|
|
14
|
+
|
|
15
|
+
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
16
|
+
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
17
|
+
const CODEX_DIR = join(homedir(), ".codex");
|
|
18
|
+
const CODEX_CONFIG_PATH = join(CODEX_DIR, "config.toml");
|
|
19
|
+
const PKG = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
|
|
20
|
+
|
|
21
|
+
const REQUIRED_CODEX_PROFILES = [
|
|
22
|
+
{
|
|
23
|
+
name: "xhigh",
|
|
24
|
+
lines: [
|
|
25
|
+
'model = "gpt-5.3-codex"',
|
|
26
|
+
'model_reasoning_effort = "xhigh"',
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "spark_fast",
|
|
31
|
+
lines: [
|
|
32
|
+
'model = "gpt-5.1-codex-mini"',
|
|
33
|
+
'model_reasoning_effort = "low"',
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// ── 색상 체계 (triflux brand: amber/orange accent) ──
|
|
39
|
+
const CYAN = "\x1b[36m";
|
|
40
|
+
const GREEN = "\x1b[32m";
|
|
41
|
+
const RED = "\x1b[31m";
|
|
42
|
+
const YELLOW = "\x1b[33m";
|
|
43
|
+
const DIM = "\x1b[2m";
|
|
44
|
+
const BOLD = "\x1b[1m";
|
|
45
|
+
const RESET = "\x1b[0m";
|
|
46
|
+
const AMBER = "\x1b[38;5;214m";
|
|
47
|
+
const BLUE = "\x1b[38;5;39m";
|
|
48
|
+
const WHITE_BRIGHT = "\x1b[97m";
|
|
49
|
+
const GRAY = "\x1b[38;5;245m";
|
|
50
|
+
const GREEN_BRIGHT = "\x1b[38;5;82m";
|
|
51
|
+
const RED_BRIGHT = "\x1b[38;5;196m";
|
|
52
|
+
|
|
53
|
+
// ── 브랜드 요소 ──
|
|
54
|
+
const BRAND = `${AMBER}${BOLD}triflux${RESET}`;
|
|
55
|
+
const VER = `${DIM}v${PKG.version}${RESET}`;
|
|
56
|
+
const LINE = `${GRAY}${"─".repeat(48)}${RESET}`;
|
|
57
|
+
const DOT = `${GRAY}·${RESET}`;
|
|
58
|
+
const STALE_TEAM_MAX_AGE_SEC = 3600;
|
|
59
|
+
const ANSI_PATTERN = /\x1B\[[0-?]*[ -/]*[@-~]/g;
|
|
60
|
+
|
|
61
|
+
const EXIT_SUCCESS = 0;
|
|
62
|
+
const EXIT_ERROR = 1;
|
|
63
|
+
const EXIT_ARG_ERROR = 2;
|
|
64
|
+
const EXIT_CLI_MISSING = 3;
|
|
65
|
+
const EXIT_HUB_ERROR = 4;
|
|
66
|
+
const EXIT_CONFIG_ERROR = 5;
|
|
67
|
+
|
|
68
|
+
const RAW_ARGS = process.argv.slice(2);
|
|
69
|
+
const JSON_OUTPUT = RAW_ARGS.includes("--json");
|
|
70
|
+
const NORMALIZED_ARGS = RAW_ARGS.filter((arg) => arg !== "--json");
|
|
71
|
+
|
|
72
|
+
const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
73
|
+
setup: {
|
|
74
|
+
usage: "tfx setup [--dry-run]",
|
|
75
|
+
description: "파일 동기화 + HUD/MCP 설정",
|
|
76
|
+
options: [
|
|
77
|
+
{ name: "--dry-run", type: "boolean", description: "실제 변경 없이 예정 작업을 JSON으로 출력" },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
doctor: {
|
|
81
|
+
usage: "tfx doctor [--fix] [--reset] [--json]",
|
|
82
|
+
description: "설치 상태 진단 및 자동 복구",
|
|
83
|
+
options: [
|
|
84
|
+
{ name: "--fix", type: "boolean", description: "파일/캐시 자동 복구 후 재진단" },
|
|
85
|
+
{ name: "--reset", type: "boolean", description: "캐시 초기화 후 재생성" },
|
|
86
|
+
{ name: "--json", type: "boolean", description: "구조화된 진단 결과 JSON 출력" },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
version: {
|
|
90
|
+
usage: "tfx version [--json]",
|
|
91
|
+
description: "triflux 및 동기화된 스크립트 버전 표시",
|
|
92
|
+
options: [
|
|
93
|
+
{ name: "--json", type: "boolean", description: "버전 정보를 JSON으로 출력" },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
list: {
|
|
97
|
+
usage: "tfx list [--json]",
|
|
98
|
+
description: "패키지 스킬과 사용자 스킬 목록 표시",
|
|
99
|
+
options: [
|
|
100
|
+
{ name: "--json", type: "boolean", description: "스킬 목록을 JSON으로 출력" },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
schema: {
|
|
104
|
+
usage: "tfx schema [command-or-tool]",
|
|
105
|
+
description: "CLI 커맨드 파라미터와 Hub delegator schema 번들 출력",
|
|
106
|
+
options: [
|
|
107
|
+
{ name: "command-or-tool", type: "string", description: "예: doctor, setup, delegate, delegate-reply, status" },
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
hub: {
|
|
111
|
+
usage: "tfx hub <start|stop|status> [--port N] [--json]",
|
|
112
|
+
description: "tfx-hub 프로세스 제어",
|
|
113
|
+
subcommands: {
|
|
114
|
+
start: { usage: "tfx hub start [--port N]" },
|
|
115
|
+
stop: { usage: "tfx hub stop" },
|
|
116
|
+
status: {
|
|
117
|
+
usage: "tfx hub status [--json]",
|
|
118
|
+
options: [{ name: "--json", type: "boolean", description: "허브 상태를 JSON으로 출력" }],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
multi: {
|
|
123
|
+
usage: "tfx multi <subcommand>",
|
|
124
|
+
description: "멀티-CLI 팀 모드",
|
|
125
|
+
subcommands: {
|
|
126
|
+
status: {
|
|
127
|
+
usage: "tfx multi status [--json]",
|
|
128
|
+
options: [{ name: "--json", type: "boolean", description: "팀 상태를 JSON으로 출력" }],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ── 유틸리티 ──
|
|
135
|
+
|
|
136
|
+
function ok(msg) { console.log(` ${GREEN_BRIGHT}✓${RESET} ${msg}`); }
|
|
137
|
+
function warn(msg) { console.log(` ${YELLOW}⚠${RESET} ${msg}`); }
|
|
138
|
+
function fail(msg) { console.log(` ${RED_BRIGHT}✗${RESET} ${msg}`); }
|
|
139
|
+
function info(msg) { console.log(` ${GRAY}${msg}${RESET}`); }
|
|
140
|
+
function section(title) { console.log(`\n ${AMBER}▸${RESET} ${BOLD}${title}${RESET}`); }
|
|
141
|
+
function stripAnsi(value) { return String(value ?? "").replace(ANSI_PATTERN, ""); }
|
|
142
|
+
function printJson(payload) { process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); }
|
|
143
|
+
|
|
144
|
+
function withConsoleSilenced(enabled, fn) {
|
|
145
|
+
if (!enabled) return fn();
|
|
146
|
+
const originalLog = console.log;
|
|
147
|
+
const originalError = console.error;
|
|
148
|
+
console.log = () => {};
|
|
149
|
+
console.error = () => {};
|
|
150
|
+
try {
|
|
151
|
+
return fn();
|
|
152
|
+
} finally {
|
|
153
|
+
console.log = originalLog;
|
|
154
|
+
console.error = originalError;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function createCliError(message, {
|
|
159
|
+
exitCode = EXIT_ERROR,
|
|
160
|
+
reason = "error",
|
|
161
|
+
fix = null,
|
|
162
|
+
cause = null,
|
|
163
|
+
} = {}) {
|
|
164
|
+
const error = new Error(message);
|
|
165
|
+
error.exitCode = exitCode;
|
|
166
|
+
error.reason = reason;
|
|
167
|
+
error.fix = fix;
|
|
168
|
+
if (cause) error.cause = cause;
|
|
169
|
+
return error;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function inferExitCode(error) {
|
|
173
|
+
if (Number.isInteger(error?.exitCode)) return error.exitCode;
|
|
174
|
+
if (error?.code === "ENOENT") return EXIT_CLI_MISSING;
|
|
175
|
+
return EXIT_ERROR;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function inferReason(error, exitCode) {
|
|
179
|
+
if (typeof error?.reason === "string" && error.reason) return error.reason;
|
|
180
|
+
if (exitCode === EXIT_ARG_ERROR) return "argError";
|
|
181
|
+
if (exitCode === EXIT_CLI_MISSING) return "cliMissing";
|
|
182
|
+
if (exitCode === EXIT_HUB_ERROR) return "hubError";
|
|
183
|
+
if (exitCode === EXIT_CONFIG_ERROR) return "configError";
|
|
184
|
+
return "error";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function inferFix(error, exitCode) {
|
|
188
|
+
if (typeof error?.fix === "string" && error.fix) return error.fix;
|
|
189
|
+
if (exitCode === EXIT_ARG_ERROR) return "tfx --help";
|
|
190
|
+
if (exitCode === EXIT_CLI_MISSING) return "필수 CLI를 설치한 뒤 `tfx doctor`로 상태를 다시 확인하세요.";
|
|
191
|
+
if (exitCode === EXIT_HUB_ERROR) return "`tfx hub start`로 허브를 다시 시작하거나 설치 상태를 확인하세요.";
|
|
192
|
+
if (exitCode === EXIT_CONFIG_ERROR) return "설정 파일 JSON/TOML 문법을 수정한 뒤 다시 실행하세요.";
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function handleFatalError(error, { json = false } = {}) {
|
|
197
|
+
const exitCode = inferExitCode(error);
|
|
198
|
+
const message = stripAnsi(error?.message || "알 수 없는 오류");
|
|
199
|
+
const reason = inferReason(error, exitCode);
|
|
200
|
+
const fix = inferFix(error, exitCode);
|
|
201
|
+
|
|
202
|
+
if (json) {
|
|
203
|
+
printJson({
|
|
204
|
+
error: {
|
|
205
|
+
code: exitCode,
|
|
206
|
+
message,
|
|
207
|
+
reason,
|
|
208
|
+
...(fix ? { fix } : {}),
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
} else {
|
|
212
|
+
console.error(message);
|
|
213
|
+
if (fix) console.error(`fix: ${fix}`);
|
|
214
|
+
}
|
|
215
|
+
process.exit(exitCode);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function which(cmd) {
|
|
219
|
+
try {
|
|
220
|
+
const result = process.platform === "win32"
|
|
221
|
+
? execFileSync("where", [cmd], { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "ignore"] })
|
|
222
|
+
: execFileSync("which", [cmd], { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "ignore"] });
|
|
223
|
+
return result.trim().split(/\r?\n/)[0] || null;
|
|
224
|
+
} catch { return null; }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function whichInShell(cmd, shell) {
|
|
228
|
+
const shellArgs = {
|
|
229
|
+
bash: ["bash", ["-c", `source ~/.bashrc 2>/dev/null && command -v "${cmd}" 2>/dev/null`]],
|
|
230
|
+
cmd: ["cmd", ["/c", "where", cmd]],
|
|
231
|
+
pwsh: ["pwsh", ["-NoProfile", "-c", `(Get-Command '${cmd.replace(/'/g, "''")}' -EA SilentlyContinue).Source`]],
|
|
232
|
+
};
|
|
233
|
+
const entry = shellArgs[shell];
|
|
234
|
+
if (!entry) return null;
|
|
235
|
+
try {
|
|
236
|
+
const result = execFileSync(entry[0], entry[1], {
|
|
237
|
+
encoding: "utf8",
|
|
238
|
+
timeout: 8000,
|
|
239
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
240
|
+
}).trim();
|
|
241
|
+
return result.split(/\r?\n/)[0] || null;
|
|
242
|
+
} catch { return null; }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function isDevUpdateRequested(argv = process.argv) {
|
|
246
|
+
return argv.includes("--dev") || argv.includes("@dev") || argv.includes("dev");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function checkShellAvailable(shell) {
|
|
250
|
+
const cmds = { bash: "bash --version", cmd: "cmd /c echo ok", pwsh: "pwsh -NoProfile -c echo ok" };
|
|
251
|
+
try {
|
|
252
|
+
execSync(cmds[shell], { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "ignore"] });
|
|
253
|
+
return true;
|
|
254
|
+
} catch { return false; }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function getVersion(filePath) {
|
|
258
|
+
try {
|
|
259
|
+
const content = readFileSync(filePath, "utf8");
|
|
260
|
+
const match = content.match(/VERSION\s*=\s*"([^"]+)"/);
|
|
261
|
+
return match ? match[1] : null;
|
|
262
|
+
} catch { return null; }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function parseSessionCreated(rawValue) {
|
|
266
|
+
const value = String(rawValue || "").trim();
|
|
267
|
+
if (!value) return null;
|
|
268
|
+
|
|
269
|
+
const numeric = Number(value);
|
|
270
|
+
if (Number.isFinite(numeric) && numeric > 0) {
|
|
271
|
+
return numeric > 1e12 ? Math.floor(numeric / 1000) : Math.floor(numeric);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const parsed = Date.parse(value);
|
|
275
|
+
if (Number.isFinite(parsed)) {
|
|
276
|
+
return Math.floor(parsed / 1000);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const normalized = value.replace(/^(\d{2})-(\d{2})-(\d{2})(\s+)/, "20$1-$2-$3$4");
|
|
280
|
+
const reparsed = Date.parse(normalized);
|
|
281
|
+
if (Number.isFinite(reparsed)) {
|
|
282
|
+
return Math.floor(reparsed / 1000);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function formatElapsedAge(ageSec) {
|
|
289
|
+
if (!Number.isFinite(ageSec) || ageSec < 0) return "알 수 없음";
|
|
290
|
+
if (ageSec < 60) return `${ageSec}초`;
|
|
291
|
+
if (ageSec < 3600) return `${Math.floor(ageSec / 60)}분`;
|
|
292
|
+
if (ageSec < 86400) return `${Math.floor(ageSec / 3600)}시간`;
|
|
293
|
+
return `${Math.floor(ageSec / 86400)}일`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function readTeamSessionCreatedMap() {
|
|
297
|
+
const createdMap = new Map();
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const output = tmuxExec('list-sessions -F "#{session_name} #{session_created}"');
|
|
301
|
+
for (const line of output.split(/\r?\n/)) {
|
|
302
|
+
const trimmed = line.trim();
|
|
303
|
+
if (!trimmed) continue;
|
|
304
|
+
|
|
305
|
+
const firstSpace = trimmed.indexOf(" ");
|
|
306
|
+
if (firstSpace === -1) continue;
|
|
307
|
+
|
|
308
|
+
const sessionName = trimmed.slice(0, firstSpace);
|
|
309
|
+
const createdRaw = trimmed.slice(firstSpace + 1).trim();
|
|
310
|
+
const createdAt = parseSessionCreated(createdRaw);
|
|
311
|
+
createdMap.set(sessionName, {
|
|
312
|
+
createdAt,
|
|
313
|
+
createdRaw,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
} catch {
|
|
317
|
+
// session_created 포맷을 읽지 못하면 stale 판정만 완화한다.
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return createdMap;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function inspectTeamSessions() {
|
|
324
|
+
const mux = detectMultiplexer();
|
|
325
|
+
if (!mux) {
|
|
326
|
+
return { mux: null, sessions: [] };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const sessionNames = listSessions();
|
|
330
|
+
if (sessionNames.length === 0) {
|
|
331
|
+
return { mux, sessions: [] };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const createdMap = readTeamSessionCreatedMap();
|
|
335
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
336
|
+
const sessions = sessionNames.map((sessionName) => {
|
|
337
|
+
const createdInfo = createdMap.get(sessionName) || { createdAt: null, createdRaw: "" };
|
|
338
|
+
const attachedCount = getSessionAttachedCount(sessionName);
|
|
339
|
+
const ageSec = createdInfo.createdAt == null ? null : Math.max(0, nowSec - createdInfo.createdAt);
|
|
340
|
+
const stale = ageSec != null && ageSec >= STALE_TEAM_MAX_AGE_SEC && attachedCount === 0;
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
sessionName,
|
|
344
|
+
attachedCount,
|
|
345
|
+
ageSec,
|
|
346
|
+
createdAt: createdInfo.createdAt,
|
|
347
|
+
createdRaw: createdInfo.createdRaw,
|
|
348
|
+
stale,
|
|
349
|
+
};
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return { mux, sessions };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function cleanupStaleTeamSessions(staleSessions) {
|
|
356
|
+
let cleaned = 0;
|
|
357
|
+
let failed = 0;
|
|
358
|
+
|
|
359
|
+
for (const session of staleSessions) {
|
|
360
|
+
let removed = false;
|
|
361
|
+
|
|
362
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
363
|
+
killSession(session.sessionName);
|
|
364
|
+
const stillAlive = listSessions().includes(session.sessionName);
|
|
365
|
+
if (!stillAlive) {
|
|
366
|
+
removed = true;
|
|
367
|
+
cleaned++;
|
|
368
|
+
ok(`stale 세션 정리: ${session.sessionName}`);
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (attempt < 3) {
|
|
373
|
+
await delay(1000);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (!removed) {
|
|
378
|
+
failed++;
|
|
379
|
+
fail(`세션 정리 실패: ${session.sessionName} — 수동 정리 필요`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
info(`${cleaned}개 stale 세션 정리 완료`);
|
|
384
|
+
|
|
385
|
+
return { cleaned, failed };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function escapeRegExp(value) {
|
|
389
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function hasProfileSection(tomlContent, profileName) {
|
|
393
|
+
const section = `^\\[profiles\\.${escapeRegExp(profileName)}\\]\\s*$`;
|
|
394
|
+
return new RegExp(section, "m").test(tomlContent);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function ensureCodexProfiles() {
|
|
398
|
+
try {
|
|
399
|
+
if (!existsSync(CODEX_DIR)) mkdirSync(CODEX_DIR, { recursive: true });
|
|
400
|
+
|
|
401
|
+
const original = existsSync(CODEX_CONFIG_PATH)
|
|
402
|
+
? readFileSync(CODEX_CONFIG_PATH, "utf8")
|
|
403
|
+
: "";
|
|
404
|
+
|
|
405
|
+
let updated = original;
|
|
406
|
+
let added = 0;
|
|
407
|
+
|
|
408
|
+
for (const profile of REQUIRED_CODEX_PROFILES) {
|
|
409
|
+
if (hasProfileSection(updated, profile.name)) continue;
|
|
410
|
+
|
|
411
|
+
if (updated.length > 0 && !updated.endsWith("\n")) updated += "\n";
|
|
412
|
+
if (updated.trim().length > 0) updated += "\n";
|
|
413
|
+
updated += `[profiles.${profile.name}]\n${profile.lines.join("\n")}\n`;
|
|
414
|
+
added++;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (added > 0) {
|
|
418
|
+
writeFileSync(CODEX_CONFIG_PATH, updated, "utf8");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return { ok: true, added };
|
|
422
|
+
} catch (e) {
|
|
423
|
+
return { ok: false, added: 0, message: e.message };
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function previewCodexProfiles() {
|
|
428
|
+
const original = existsSync(CODEX_CONFIG_PATH)
|
|
429
|
+
? readFileSync(CODEX_CONFIG_PATH, "utf8")
|
|
430
|
+
: "";
|
|
431
|
+
const missingProfiles = REQUIRED_CODEX_PROFILES
|
|
432
|
+
.filter((profile) => !hasProfileSection(original, profile.name))
|
|
433
|
+
.map((profile) => profile.name);
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
path: CODEX_CONFIG_PATH,
|
|
437
|
+
missingProfiles,
|
|
438
|
+
change: missingProfiles.length > 0 ? (original ? "update" : "create") : "noop",
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function syncFile(src, dst, label) {
|
|
443
|
+
const dstDir = dirname(dst);
|
|
444
|
+
if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
|
|
445
|
+
|
|
446
|
+
if (!existsSync(src)) {
|
|
447
|
+
fail(`${label}: 소스 파일 없음 (${src})`);
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const srcVer = getVersion(src);
|
|
452
|
+
const dstVer = existsSync(dst) ? getVersion(dst) : null;
|
|
453
|
+
|
|
454
|
+
if (!existsSync(dst)) {
|
|
455
|
+
copyFileSync(src, dst);
|
|
456
|
+
try { chmodSync(dst, 0o755); } catch {}
|
|
457
|
+
ok(`${label}: 설치됨 ${srcVer ? `(v${srcVer})` : ""}`);
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const srcContent = readFileSync(src, "utf8");
|
|
462
|
+
const dstContent = readFileSync(dst, "utf8");
|
|
463
|
+
if (srcContent !== dstContent) {
|
|
464
|
+
copyFileSync(src, dst);
|
|
465
|
+
try { chmodSync(dst, 0o755); } catch {}
|
|
466
|
+
const verInfo = (srcVer && dstVer && srcVer !== dstVer)
|
|
467
|
+
? `(v${dstVer} → v${srcVer})`
|
|
468
|
+
: srcVer ? `(v${srcVer}, 내용 변경)` : "(내용 변경)";
|
|
469
|
+
ok(`${label}: 업데이트됨 ${verInfo}`);
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
ok(`${label}: 최신 상태 ${srcVer ? `(v${srcVer})` : ""}`);
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function describeSyncAction(src, dst, label) {
|
|
478
|
+
if (!existsSync(src)) {
|
|
479
|
+
throw createCliError(`${label}: 소스 파일 없음 (${src})`, {
|
|
480
|
+
exitCode: EXIT_CONFIG_ERROR,
|
|
481
|
+
reason: "configError",
|
|
482
|
+
fix: "패키지 파일이 손상되지 않았는지 확인한 뒤 triflux를 다시 설치하세요.",
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const srcVer = getVersion(src);
|
|
487
|
+
const dstExists = existsSync(dst);
|
|
488
|
+
const change = !dstExists
|
|
489
|
+
? "create"
|
|
490
|
+
: readFileSync(src, "utf8") !== readFileSync(dst, "utf8")
|
|
491
|
+
? "update"
|
|
492
|
+
: "noop";
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
type: "sync",
|
|
496
|
+
label,
|
|
497
|
+
from: src,
|
|
498
|
+
to: dst,
|
|
499
|
+
change,
|
|
500
|
+
version: srcVer,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ── 크로스 셸 진단 ──
|
|
505
|
+
|
|
506
|
+
function checkCliCrossShell(cmd, installHint) {
|
|
507
|
+
const shells = process.platform === "win32" ? ["bash", "cmd", "pwsh"] : ["bash"];
|
|
508
|
+
let anyFound = false;
|
|
509
|
+
let bashMissing = false;
|
|
510
|
+
const shellResults = [];
|
|
511
|
+
|
|
512
|
+
for (const shell of shells) {
|
|
513
|
+
if (!checkShellAvailable(shell)) {
|
|
514
|
+
info(`${shell}: ${DIM}셸 없음 (건너뜀)${RESET}`);
|
|
515
|
+
shellResults.push({ shell, status: "unavailable", path: null });
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const p = whichInShell(cmd, shell);
|
|
519
|
+
if (p) {
|
|
520
|
+
ok(`${shell}: ${p}`);
|
|
521
|
+
anyFound = true;
|
|
522
|
+
shellResults.push({ shell, status: "ok", path: p });
|
|
523
|
+
} else {
|
|
524
|
+
fail(`${shell}: 미발견`);
|
|
525
|
+
if (shell === "bash") bashMissing = true;
|
|
526
|
+
shellResults.push({ shell, status: "missing", path: null, fix: installHint });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (!anyFound) {
|
|
531
|
+
info(`미설치 (선택사항) — ${installHint}`);
|
|
532
|
+
info("없으면 Claude 네이티브 에이전트로 fallback");
|
|
533
|
+
return {
|
|
534
|
+
issues: 1,
|
|
535
|
+
anyFound,
|
|
536
|
+
bashMissing,
|
|
537
|
+
shells: shellResults,
|
|
538
|
+
status: "missing",
|
|
539
|
+
fix: installHint,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
if (bashMissing) {
|
|
543
|
+
warn("bash에서 미발견 — tfx-route.sh 실행 불가");
|
|
544
|
+
info('→ ~/.bashrc에 추가: export PATH="$PATH:$APPDATA/npm"');
|
|
545
|
+
return {
|
|
546
|
+
issues: 1,
|
|
547
|
+
anyFound,
|
|
548
|
+
bashMissing,
|
|
549
|
+
shells: shellResults,
|
|
550
|
+
status: "degraded",
|
|
551
|
+
fix: 'bash PATH를 정리한 뒤 `tfx doctor`를 다시 실행하세요.',
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
issues: 0,
|
|
556
|
+
anyFound,
|
|
557
|
+
bashMissing,
|
|
558
|
+
shells: shellResults,
|
|
559
|
+
status: "ok",
|
|
560
|
+
fix: null,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ── 명령어 ──
|
|
565
|
+
|
|
566
|
+
function getSetupSyncTargets() {
|
|
567
|
+
return [
|
|
568
|
+
{
|
|
569
|
+
src: join(PKG_ROOT, "scripts", "tfx-route.sh"),
|
|
570
|
+
dst: join(CLAUDE_DIR, "scripts", "tfx-route.sh"),
|
|
571
|
+
label: "tfx-route.sh",
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
src: join(PKG_ROOT, "hud", "hud-qos-status.mjs"),
|
|
575
|
+
dst: join(CLAUDE_DIR, "hud", "hud-qos-status.mjs"),
|
|
576
|
+
label: "hud-qos-status.mjs",
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
src: join(PKG_ROOT, "scripts", "notion-read.mjs"),
|
|
580
|
+
dst: join(CLAUDE_DIR, "scripts", "notion-read.mjs"),
|
|
581
|
+
label: "notion-read.mjs",
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
src: join(PKG_ROOT, "scripts", "tfx-route-post.mjs"),
|
|
585
|
+
dst: join(CLAUDE_DIR, "scripts", "tfx-route-post.mjs"),
|
|
586
|
+
label: "tfx-route-post.mjs",
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
src: join(PKG_ROOT, "scripts", "tfx-batch-stats.mjs"),
|
|
590
|
+
dst: join(CLAUDE_DIR, "scripts", "tfx-batch-stats.mjs"),
|
|
591
|
+
label: "tfx-batch-stats.mjs",
|
|
592
|
+
},
|
|
593
|
+
];
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function listSkillSyncActions() {
|
|
597
|
+
const skillsSrc = join(PKG_ROOT, "skills");
|
|
598
|
+
if (!existsSync(skillsSrc)) return [];
|
|
599
|
+
|
|
600
|
+
const actions = [];
|
|
601
|
+
for (const name of readdirSync(skillsSrc).sort()) {
|
|
602
|
+
const src = join(skillsSrc, name, "SKILL.md");
|
|
603
|
+
const dst = join(CLAUDE_DIR, "skills", name, "SKILL.md");
|
|
604
|
+
if (!existsSync(src)) continue;
|
|
605
|
+
actions.push(describeSyncAction(src, dst, `skill:${name}`));
|
|
606
|
+
}
|
|
607
|
+
return actions;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function previewStatusLineAction() {
|
|
611
|
+
const settingsPath = join(CLAUDE_DIR, "settings.json");
|
|
612
|
+
const hudPath = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
613
|
+
|
|
614
|
+
let settings = {};
|
|
615
|
+
if (existsSync(settingsPath)) {
|
|
616
|
+
try {
|
|
617
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
618
|
+
} catch (error) {
|
|
619
|
+
throw createCliError(`settings.json 처리 실패: ${error.message}`, {
|
|
620
|
+
exitCode: EXIT_CONFIG_ERROR,
|
|
621
|
+
reason: "configError",
|
|
622
|
+
fix: `${settingsPath}의 JSON 문법을 수정하세요.`,
|
|
623
|
+
cause: error,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const currentCmd = settings.statusLine?.command || "";
|
|
629
|
+
return {
|
|
630
|
+
type: "statusLine",
|
|
631
|
+
path: settingsPath,
|
|
632
|
+
change: currentCmd.includes("hud-qos-status.mjs") ? "noop" : (currentCmd ? "update" : "create"),
|
|
633
|
+
current: currentCmd || null,
|
|
634
|
+
target: hudPath,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function previewMcpRegistrationActions(mcpUrl) {
|
|
639
|
+
const actions = [];
|
|
640
|
+
|
|
641
|
+
if (which("codex")) {
|
|
642
|
+
actions.push({
|
|
643
|
+
type: "mcp-register",
|
|
644
|
+
cli: "codex",
|
|
645
|
+
target: "tfx-hub",
|
|
646
|
+
url: mcpUrl,
|
|
647
|
+
change: "check",
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
if (which("gemini")) {
|
|
651
|
+
actions.push({
|
|
652
|
+
type: "mcp-register",
|
|
653
|
+
cli: "gemini",
|
|
654
|
+
target: "tfx-hub",
|
|
655
|
+
url: mcpUrl,
|
|
656
|
+
change: "check",
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
actions.push({
|
|
661
|
+
type: "mcp-register",
|
|
662
|
+
cli: "claude",
|
|
663
|
+
target: "tfx-hub",
|
|
664
|
+
path: join(PKG_ROOT, ".mcp.json"),
|
|
665
|
+
url: mcpUrl,
|
|
666
|
+
change: "check",
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
return actions;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function buildSetupDryRunPlan() {
|
|
673
|
+
const actions = [
|
|
674
|
+
...getSetupSyncTargets().map(({ src, dst, label }) => describeSyncAction(src, dst, label)),
|
|
675
|
+
...listSkillSyncActions(),
|
|
676
|
+
];
|
|
677
|
+
const codexProfiles = previewCodexProfiles();
|
|
678
|
+
actions.push({
|
|
679
|
+
type: "codex-profiles",
|
|
680
|
+
path: codexProfiles.path,
|
|
681
|
+
change: codexProfiles.change,
|
|
682
|
+
profiles: codexProfiles.missingProfiles,
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
const defaultHubUrl = `http://127.0.0.1:${process.env.TFX_HUB_PORT || "27888"}/mcp`;
|
|
686
|
+
actions.push(...previewMcpRegistrationActions(defaultHubUrl));
|
|
687
|
+
actions.push(previewStatusLineAction());
|
|
688
|
+
|
|
689
|
+
return {
|
|
690
|
+
dry_run: true,
|
|
691
|
+
actions,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function cmdSetup(options = {}) {
|
|
696
|
+
const { dryRun = false } = options;
|
|
697
|
+
if (dryRun) {
|
|
698
|
+
printJson(buildSetupDryRunPlan());
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
console.log(`\n${BOLD}triflux setup${RESET}\n`);
|
|
703
|
+
|
|
704
|
+
for (const target of getSetupSyncTargets()) {
|
|
705
|
+
syncFile(target.src, target.dst, target.label);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// 스킬 동기화 (~/.claude/skills/{name}/SKILL.md)
|
|
709
|
+
const skillsSrc = join(PKG_ROOT, "skills");
|
|
710
|
+
const skillsDst = join(CLAUDE_DIR, "skills");
|
|
711
|
+
if (existsSync(skillsSrc)) {
|
|
712
|
+
let skillCount = 0;
|
|
713
|
+
let skillTotal = 0;
|
|
714
|
+
for (const name of readdirSync(skillsSrc)) {
|
|
715
|
+
const src = join(skillsSrc, name, "SKILL.md");
|
|
716
|
+
const dst = join(skillsDst, name, "SKILL.md");
|
|
717
|
+
if (!existsSync(src)) continue;
|
|
718
|
+
skillTotal++;
|
|
719
|
+
|
|
720
|
+
const dstDir = dirname(dst);
|
|
721
|
+
if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
|
|
722
|
+
|
|
723
|
+
if (!existsSync(dst)) {
|
|
724
|
+
copyFileSync(src, dst);
|
|
725
|
+
skillCount++;
|
|
726
|
+
} else {
|
|
727
|
+
const srcContent = readFileSync(src, "utf8");
|
|
728
|
+
const dstContent = readFileSync(dst, "utf8");
|
|
729
|
+
if (srcContent !== dstContent) {
|
|
730
|
+
copyFileSync(src, dst);
|
|
731
|
+
skillCount++;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (skillCount > 0) {
|
|
736
|
+
ok(`스킬: ${skillCount}/${skillTotal}개 업데이트됨`);
|
|
737
|
+
} else {
|
|
738
|
+
ok(`스킬: ${skillTotal}개 최신 상태`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const codexProfileResult = ensureCodexProfiles();
|
|
743
|
+
if (!codexProfileResult.ok) {
|
|
744
|
+
warn(`Codex profiles 설정 실패: ${codexProfileResult.message}`);
|
|
745
|
+
} else if (codexProfileResult.added > 0) {
|
|
746
|
+
ok(`Codex profiles: ${codexProfileResult.added}개 추가됨 (~/.codex/config.toml)`);
|
|
747
|
+
} else {
|
|
748
|
+
ok("Codex profiles: 이미 준비됨");
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// hub MCP 사전 등록 (서버 미실행이어도 설정만 등록 — hub start 시 즉시 사용 가능)
|
|
752
|
+
if (existsSync(join(PKG_ROOT, "hub", "server.mjs"))) {
|
|
753
|
+
const defaultHubUrl = `http://127.0.0.1:${process.env.TFX_HUB_PORT || "27888"}/mcp`;
|
|
754
|
+
autoRegisterMcp(defaultHubUrl);
|
|
755
|
+
console.log("");
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// HUD statusLine 설정
|
|
759
|
+
console.log(`${CYAN}[HUD 설정]${RESET}`);
|
|
760
|
+
const settingsPath = join(CLAUDE_DIR, "settings.json");
|
|
761
|
+
const hudPath = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
762
|
+
|
|
763
|
+
if (existsSync(hudPath)) {
|
|
764
|
+
try {
|
|
765
|
+
let settings = {};
|
|
766
|
+
if (existsSync(settingsPath)) {
|
|
767
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const currentCmd = settings.statusLine?.command || "";
|
|
771
|
+
if (currentCmd.includes("hud-qos-status.mjs")) {
|
|
772
|
+
ok("statusLine 이미 설정됨");
|
|
773
|
+
} else {
|
|
774
|
+
const nodePath = process.execPath.replace(/\\/g, "/");
|
|
775
|
+
const hudForward = hudPath.replace(/\\/g, "/");
|
|
776
|
+
const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
|
|
777
|
+
const hudRef = hudForward.includes(" ") ? `"${hudForward}"` : hudForward;
|
|
778
|
+
|
|
779
|
+
if (currentCmd) {
|
|
780
|
+
warn(`기존 statusLine 덮어쓰기: ${currentCmd}`);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
settings.statusLine = {
|
|
784
|
+
type: "command",
|
|
785
|
+
command: `${nodeRef} ${hudRef}`,
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
789
|
+
ok("statusLine 설정 완료 — 세션 재시작 후 HUD 표시");
|
|
790
|
+
}
|
|
791
|
+
} catch (e) {
|
|
792
|
+
throw createCliError(`settings.json 처리 실패: ${e.message}`, {
|
|
793
|
+
exitCode: EXIT_CONFIG_ERROR,
|
|
794
|
+
reason: "configError",
|
|
795
|
+
fix: `${settingsPath}의 JSON 문법을 수정하세요.`,
|
|
796
|
+
cause: e,
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
} else {
|
|
800
|
+
warn("HUD 파일 없음 — 먼저 파일 동기화 필요");
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
console.log(`\n${DIM}설치 위치: ${CLAUDE_DIR}${RESET}\n`);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function addDoctorCheck(report, entry) {
|
|
807
|
+
report.checks.push(entry);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
async function cmdDoctor(options = {}) {
|
|
811
|
+
const { fix = false, reset = false, json = false } = options;
|
|
812
|
+
const report = {
|
|
813
|
+
status: "ok",
|
|
814
|
+
mode: reset ? "reset" : fix ? "fix" : "check",
|
|
815
|
+
checks: [],
|
|
816
|
+
actions: [],
|
|
817
|
+
issue_count: 0,
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
return await withConsoleSilenced(json, async () => {
|
|
821
|
+
const modeLabel = reset ? ` ${RED}--reset${RESET}` : fix ? ` ${YELLOW}--fix${RESET}` : "";
|
|
822
|
+
console.log(`\n ${AMBER}${BOLD}⬡ triflux doctor${RESET} ${VER}${modeLabel}\n`);
|
|
823
|
+
console.log(` ${LINE}`);
|
|
824
|
+
|
|
825
|
+
// ── reset 모드: 캐시 전체 초기화 ──
|
|
826
|
+
if (reset) {
|
|
827
|
+
section("Cache Reset");
|
|
828
|
+
const cacheDir = join(CLAUDE_DIR, "cache");
|
|
829
|
+
const resetFiles = [
|
|
830
|
+
"claude-usage-cache.json",
|
|
831
|
+
".claude-refresh-lock",
|
|
832
|
+
"codex-rate-limits-cache.json",
|
|
833
|
+
"gemini-quota-cache.json",
|
|
834
|
+
"gemini-project-id.json",
|
|
835
|
+
"gemini-session-cache.json",
|
|
836
|
+
"gemini-rpm-tracker.json",
|
|
837
|
+
"sv-accumulator.json",
|
|
838
|
+
"mcp-inventory.json",
|
|
839
|
+
"cli-issues.jsonl",
|
|
840
|
+
"triflux-update-check.json",
|
|
841
|
+
];
|
|
842
|
+
let cleared = 0;
|
|
843
|
+
for (const name of resetFiles) {
|
|
844
|
+
const fp = join(cacheDir, name);
|
|
845
|
+
if (existsSync(fp)) {
|
|
846
|
+
try {
|
|
847
|
+
unlinkSync(fp);
|
|
848
|
+
cleared++;
|
|
849
|
+
report.actions.push({ type: "delete", path: fp, status: "ok" });
|
|
850
|
+
ok(`삭제됨: ${name}`);
|
|
851
|
+
} catch (e) {
|
|
852
|
+
report.actions.push({ type: "delete", path: fp, status: "failed", message: e.message });
|
|
853
|
+
fail(`삭제 실패: ${name} — ${e.message}`);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
if (cleared === 0) {
|
|
858
|
+
ok("삭제할 캐시 파일 없음 (이미 깨끗함)");
|
|
859
|
+
} else {
|
|
860
|
+
console.log("");
|
|
861
|
+
ok(`${BOLD}${cleared}개${RESET} 캐시 파일 초기화 완료`);
|
|
862
|
+
}
|
|
863
|
+
console.log("");
|
|
864
|
+
section("Cache Rebuild");
|
|
865
|
+
const mcpCheck = join(PKG_ROOT, "scripts", "mcp-check.mjs");
|
|
866
|
+
if (existsSync(mcpCheck)) {
|
|
867
|
+
try {
|
|
868
|
+
execFileSync(process.execPath, [mcpCheck], { timeout: 15000, stdio: "ignore" });
|
|
869
|
+
report.actions.push({ type: "rebuild", name: "mcp-inventory", status: "ok" });
|
|
870
|
+
ok("MCP 인벤토리 재생성됨");
|
|
871
|
+
} catch {
|
|
872
|
+
report.actions.push({ type: "rebuild", name: "mcp-inventory", status: "failed" });
|
|
873
|
+
warn("MCP 인벤토리 재생성 실패 — 다음 세션에서 자동 재시도");
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const hudScript = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
877
|
+
if (existsSync(hudScript)) {
|
|
878
|
+
try {
|
|
879
|
+
execFileSync(process.execPath, [hudScript, "--refresh-claude-usage"], { timeout: 20000, stdio: "ignore" });
|
|
880
|
+
report.actions.push({ type: "rebuild", name: "claude-usage-cache", status: "ok" });
|
|
881
|
+
ok("Claude 사용량 캐시 재생성됨");
|
|
882
|
+
} catch {
|
|
883
|
+
report.actions.push({ type: "rebuild", name: "claude-usage-cache", status: "failed" });
|
|
884
|
+
warn("Claude 사용량 캐시 재생성 실패 — 다음 API 호출 시 자동 생성");
|
|
885
|
+
}
|
|
886
|
+
try {
|
|
887
|
+
execFileSync(process.execPath, [hudScript, "--refresh-codex-rate-limits"], { timeout: 15000, stdio: "ignore" });
|
|
888
|
+
report.actions.push({ type: "rebuild", name: "codex-rate-limits-cache", status: "ok" });
|
|
889
|
+
ok("Codex 레이트 리밋 캐시 재생성됨");
|
|
890
|
+
} catch {
|
|
891
|
+
report.actions.push({ type: "rebuild", name: "codex-rate-limits-cache", status: "failed" });
|
|
892
|
+
warn("Codex 레이트 리밋 캐시 재생성 실패");
|
|
893
|
+
}
|
|
894
|
+
try {
|
|
895
|
+
execFileSync(process.execPath, [hudScript, "--refresh-gemini-quota"], { timeout: 15000, stdio: "ignore" });
|
|
896
|
+
report.actions.push({ type: "rebuild", name: "gemini-quota-cache", status: "ok" });
|
|
897
|
+
ok("Gemini 쿼터 캐시 재생성됨");
|
|
898
|
+
} catch {
|
|
899
|
+
report.actions.push({ type: "rebuild", name: "gemini-quota-cache", status: "failed" });
|
|
900
|
+
warn("Gemini 쿼터 캐시 재생성 실패");
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
console.log(`\n ${LINE}`);
|
|
904
|
+
console.log(` ${GREEN_BRIGHT}${BOLD}✓ 캐시 초기화 + 재생성 완료${RESET}\n`);
|
|
905
|
+
report.status = report.actions.some((action) => action.status === "failed") ? "issues" : "ok";
|
|
906
|
+
report.issue_count = report.actions.filter((action) => action.status === "failed").length;
|
|
907
|
+
if (json) printJson(report);
|
|
908
|
+
return report;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// ── fix 모드: 파일 동기화 + 캐시 정리 후 진단 ──
|
|
912
|
+
if (fix) {
|
|
913
|
+
section("Auto Fix");
|
|
914
|
+
syncFile(
|
|
915
|
+
join(PKG_ROOT, "scripts", "tfx-route.sh"),
|
|
916
|
+
join(CLAUDE_DIR, "scripts", "tfx-route.sh"),
|
|
917
|
+
"tfx-route.sh"
|
|
918
|
+
);
|
|
919
|
+
syncFile(
|
|
920
|
+
join(PKG_ROOT, "hud", "hud-qos-status.mjs"),
|
|
921
|
+
join(CLAUDE_DIR, "hud", "hud-qos-status.mjs"),
|
|
922
|
+
"hud-qos-status.mjs"
|
|
923
|
+
);
|
|
924
|
+
syncFile(
|
|
925
|
+
join(PKG_ROOT, "scripts", "notion-read.mjs"),
|
|
926
|
+
join(CLAUDE_DIR, "scripts", "notion-read.mjs"),
|
|
927
|
+
"notion-read.mjs"
|
|
928
|
+
);
|
|
929
|
+
// 스킬 동기화
|
|
930
|
+
const fSkillsSrc = join(PKG_ROOT, "skills");
|
|
931
|
+
const fSkillsDst = join(CLAUDE_DIR, "skills");
|
|
932
|
+
if (existsSync(fSkillsSrc)) {
|
|
933
|
+
let sc = 0, st = 0;
|
|
934
|
+
for (const name of readdirSync(fSkillsSrc)) {
|
|
935
|
+
const src = join(fSkillsSrc, name, "SKILL.md");
|
|
936
|
+
const dst = join(fSkillsDst, name, "SKILL.md");
|
|
937
|
+
if (!existsSync(src)) continue;
|
|
938
|
+
st++;
|
|
939
|
+
const dstDir = dirname(dst);
|
|
940
|
+
if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
|
|
941
|
+
if (!existsSync(dst)) { copyFileSync(src, dst); sc++; }
|
|
942
|
+
else if (readFileSync(src, "utf8") !== readFileSync(dst, "utf8")) { copyFileSync(src, dst); sc++; }
|
|
943
|
+
}
|
|
944
|
+
if (sc > 0) ok(`스킬: ${sc}/${st}개 업데이트됨`);
|
|
945
|
+
else ok(`스킬: ${st}개 최신 상태`);
|
|
946
|
+
}
|
|
947
|
+
const profileFix = ensureCodexProfiles();
|
|
948
|
+
if (!profileFix.ok) {
|
|
949
|
+
warn(`Codex Profiles 자동 복구 실패: ${profileFix.message}`);
|
|
950
|
+
} else if (profileFix.added > 0) {
|
|
951
|
+
ok(`Codex Profiles: ${profileFix.added}개 추가됨`);
|
|
952
|
+
} else {
|
|
953
|
+
info("Codex Profiles: 이미 최신 상태");
|
|
954
|
+
}
|
|
955
|
+
// 에러/스테일 캐시 정리
|
|
956
|
+
const fCacheDir = join(CLAUDE_DIR, "cache");
|
|
957
|
+
const staleNames = ["claude-usage-cache.json", ".claude-refresh-lock", "codex-rate-limits-cache.json"];
|
|
958
|
+
let cleaned = 0;
|
|
959
|
+
for (const name of staleNames) {
|
|
960
|
+
const fp = join(fCacheDir, name);
|
|
961
|
+
if (!existsSync(fp)) continue;
|
|
962
|
+
try {
|
|
963
|
+
const parsed = JSON.parse(readFileSync(fp, "utf8"));
|
|
964
|
+
if (parsed.error || name.startsWith(".")) { unlinkSync(fp); cleaned++; ok(`에러 캐시 정리: ${name}`); }
|
|
965
|
+
} catch { try { unlinkSync(fp); cleaned++; ok(`손상된 캐시 정리: ${name}`); } catch {} }
|
|
966
|
+
}
|
|
967
|
+
if (cleaned === 0) info("에러 캐시 없음");
|
|
968
|
+
console.log(`\n ${LINE}`);
|
|
969
|
+
info("수정 완료 — 아래 진단 결과를 확인하세요");
|
|
970
|
+
console.log("");
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
let issues = 0;
|
|
974
|
+
|
|
975
|
+
// 1. tfx-route.sh
|
|
976
|
+
section("tfx-route.sh");
|
|
977
|
+
const routeSh = join(CLAUDE_DIR, "scripts", "tfx-route.sh");
|
|
978
|
+
if (existsSync(routeSh)) {
|
|
979
|
+
const ver = getVersion(routeSh);
|
|
980
|
+
addDoctorCheck(report, { name: "tfx-route.sh", status: "ok", path: routeSh, version: ver });
|
|
981
|
+
ok(`설치됨 ${ver ? `${DIM}v${ver}${RESET}` : ""}`);
|
|
982
|
+
} else {
|
|
983
|
+
addDoctorCheck(report, { name: "tfx-route.sh", status: "missing", path: routeSh, fix: "tfx setup" });
|
|
984
|
+
fail("미설치 — tfx setup 실행 필요");
|
|
985
|
+
issues++;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// 2. HUD
|
|
989
|
+
section("HUD");
|
|
990
|
+
const hud = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
991
|
+
if (existsSync(hud)) {
|
|
992
|
+
addDoctorCheck(report, { name: "hud-qos-status.mjs", status: "ok", path: hud });
|
|
993
|
+
ok("설치됨");
|
|
994
|
+
} else {
|
|
995
|
+
addDoctorCheck(report, { name: "hud-qos-status.mjs", status: "missing", path: hud, optional: true, fix: "tfx setup" });
|
|
996
|
+
warn("미설치 ${GRAY}(선택사항)${RESET}");
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// 3. Codex CLI
|
|
1000
|
+
section(`Codex CLI ${WHITE_BRIGHT}●${RESET}`);
|
|
1001
|
+
const codexCli = checkCliCrossShell("codex", "npm install -g @openai/codex");
|
|
1002
|
+
issues += codexCli.issues;
|
|
1003
|
+
addDoctorCheck(report, {
|
|
1004
|
+
name: "codex",
|
|
1005
|
+
status: codexCli.status,
|
|
1006
|
+
shells: codexCli.shells,
|
|
1007
|
+
...(codexCli.fix ? { fix: codexCli.fix } : {}),
|
|
1008
|
+
});
|
|
1009
|
+
if (which("codex")) {
|
|
1010
|
+
if (process.env.OPENAI_API_KEY) {
|
|
1011
|
+
ok("OPENAI_API_KEY 설정됨");
|
|
1012
|
+
} else {
|
|
1013
|
+
warn(`OPENAI_API_KEY 미설정 ${GRAY}(Pro 구독이면 불필요)${RESET}`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// 4. Codex Profiles
|
|
1018
|
+
section("Codex Profiles");
|
|
1019
|
+
if (existsSync(CODEX_CONFIG_PATH)) {
|
|
1020
|
+
const codexConfig = readFileSync(CODEX_CONFIG_PATH, "utf8");
|
|
1021
|
+
const missingProfiles = [];
|
|
1022
|
+
for (const profile of REQUIRED_CODEX_PROFILES) {
|
|
1023
|
+
if (hasProfileSection(codexConfig, profile.name)) {
|
|
1024
|
+
ok(`${profile.name}: 정상`);
|
|
1025
|
+
} else {
|
|
1026
|
+
missingProfiles.push(profile.name);
|
|
1027
|
+
warn(`${profile.name}: 미설정`);
|
|
1028
|
+
issues++;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
addDoctorCheck(report, {
|
|
1032
|
+
name: "codex-profiles",
|
|
1033
|
+
status: missingProfiles.length === 0 ? "ok" : "missing",
|
|
1034
|
+
path: CODEX_CONFIG_PATH,
|
|
1035
|
+
missing_profiles: missingProfiles,
|
|
1036
|
+
...(missingProfiles.length > 0 ? { fix: "tfx setup" } : {}),
|
|
1037
|
+
});
|
|
1038
|
+
} else {
|
|
1039
|
+
addDoctorCheck(report, { name: "codex-profiles", status: "missing", path: CODEX_CONFIG_PATH, fix: "tfx setup" });
|
|
1040
|
+
warn("config.toml 미존재");
|
|
1041
|
+
issues++;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// 5. Gemini CLI
|
|
1045
|
+
section(`Gemini CLI ${BLUE}●${RESET}`);
|
|
1046
|
+
const geminiCli = checkCliCrossShell("gemini", "npm install -g @google/gemini-cli");
|
|
1047
|
+
issues += geminiCli.issues;
|
|
1048
|
+
addDoctorCheck(report, {
|
|
1049
|
+
name: "gemini",
|
|
1050
|
+
status: geminiCli.status,
|
|
1051
|
+
shells: geminiCli.shells,
|
|
1052
|
+
...(geminiCli.fix ? { fix: geminiCli.fix } : {}),
|
|
1053
|
+
});
|
|
1054
|
+
if (which("gemini")) {
|
|
1055
|
+
if (process.env.GEMINI_API_KEY) {
|
|
1056
|
+
ok("GEMINI_API_KEY 설정됨");
|
|
1057
|
+
} else {
|
|
1058
|
+
warn(`GEMINI_API_KEY 미설정 ${GRAY}(gemini auth login)${RESET}`);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// 6. Claude Code
|
|
1063
|
+
section(`Claude Code ${AMBER}●${RESET}`);
|
|
1064
|
+
const claudePath = which("claude");
|
|
1065
|
+
if (claudePath) {
|
|
1066
|
+
addDoctorCheck(report, { name: "claude", status: "ok", path: claudePath });
|
|
1067
|
+
ok("설치됨");
|
|
1068
|
+
} else {
|
|
1069
|
+
addDoctorCheck(report, { name: "claude", status: "missing", fix: "Claude Code를 설치한 뒤 `tfx doctor`를 다시 실행하세요." });
|
|
1070
|
+
fail("미설치 (필수)");
|
|
1071
|
+
issues++;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// 7. 스킬 설치 상태
|
|
1075
|
+
section("Skills");
|
|
1076
|
+
const skillsSrc = join(PKG_ROOT, "skills");
|
|
1077
|
+
const skillsDst = join(CLAUDE_DIR, "skills");
|
|
1078
|
+
if (existsSync(skillsSrc)) {
|
|
1079
|
+
let installed = 0;
|
|
1080
|
+
let total = 0;
|
|
1081
|
+
const missing = [];
|
|
1082
|
+
for (const name of readdirSync(skillsSrc)) {
|
|
1083
|
+
if (!existsSync(join(skillsSrc, name, "SKILL.md"))) continue;
|
|
1084
|
+
total++;
|
|
1085
|
+
if (existsSync(join(skillsDst, name, "SKILL.md"))) {
|
|
1086
|
+
installed++;
|
|
1087
|
+
} else {
|
|
1088
|
+
missing.push(name);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
if (installed === total) {
|
|
1092
|
+
addDoctorCheck(report, { name: "skills", status: "ok", installed, total });
|
|
1093
|
+
ok(`${installed}/${total}개 설치됨`);
|
|
1094
|
+
} else {
|
|
1095
|
+
addDoctorCheck(report, { name: "skills", status: "missing", installed, total, missing, fix: "tfx setup" });
|
|
1096
|
+
warn(`${installed}/${total}개 설치됨 — 미설치: ${missing.join(", ")}`);
|
|
1097
|
+
info("triflux setup으로 동기화 가능");
|
|
1098
|
+
issues++;
|
|
1099
|
+
}
|
|
1100
|
+
} else {
|
|
1101
|
+
addDoctorCheck(report, { name: "skills", status: "missing", installed: 0, total: 0, fix: "패키지 skills 디렉토리를 확인하세요." });
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// 8. 플러그인 등록
|
|
1105
|
+
section("Plugin");
|
|
1106
|
+
const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
|
|
1107
|
+
if (existsSync(pluginsFile)) {
|
|
1108
|
+
const content = readFileSync(pluginsFile, "utf8");
|
|
1109
|
+
if (content.includes("triflux")) {
|
|
1110
|
+
addDoctorCheck(report, { name: "plugin", status: "ok", path: pluginsFile });
|
|
1111
|
+
ok("triflux 플러그인 등록됨");
|
|
1112
|
+
} else {
|
|
1113
|
+
addDoctorCheck(report, { name: "plugin", status: "missing", path: pluginsFile, optional: true, fix: "/plugin marketplace add <repo-url>" });
|
|
1114
|
+
warn("triflux 플러그인 미등록 — npm 단독 사용 중");
|
|
1115
|
+
info("플러그인 등록: /plugin marketplace add <repo-url>");
|
|
1116
|
+
}
|
|
1117
|
+
} else {
|
|
1118
|
+
addDoctorCheck(report, { name: "plugin", status: "unavailable", optional: true });
|
|
1119
|
+
info("플러그인 시스템 감지 안 됨 — npm 단독 사용");
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// 9. MCP 인벤토리
|
|
1123
|
+
section("MCP Inventory");
|
|
1124
|
+
const mcpCache = join(CLAUDE_DIR, "cache", "mcp-inventory.json");
|
|
1125
|
+
if (existsSync(mcpCache)) {
|
|
1126
|
+
try {
|
|
1127
|
+
const inv = JSON.parse(readFileSync(mcpCache, "utf8"));
|
|
1128
|
+
addDoctorCheck(report, {
|
|
1129
|
+
name: "mcp-inventory",
|
|
1130
|
+
status: "ok",
|
|
1131
|
+
path: mcpCache,
|
|
1132
|
+
codex_servers: inv.codex?.servers?.length || 0,
|
|
1133
|
+
gemini_servers: inv.gemini?.servers?.length || 0,
|
|
1134
|
+
});
|
|
1135
|
+
ok(`캐시 존재 (${inv.timestamp})`);
|
|
1136
|
+
if (inv.codex?.servers?.length) {
|
|
1137
|
+
const names = inv.codex.servers.map(s => s.name).join(", ");
|
|
1138
|
+
info(`Codex: ${inv.codex.servers.length}개 서버 (${names})`);
|
|
1139
|
+
}
|
|
1140
|
+
if (inv.gemini?.servers?.length) {
|
|
1141
|
+
const names = inv.gemini.servers.map(s => s.name).join(", ");
|
|
1142
|
+
info(`Gemini: ${inv.gemini.servers.length}개 서버 (${names})`);
|
|
1143
|
+
}
|
|
1144
|
+
} catch {
|
|
1145
|
+
addDoctorCheck(report, { name: "mcp-inventory", status: "invalid", path: mcpCache, fix: `node ${join(PKG_ROOT, "scripts", "mcp-check.mjs")}` });
|
|
1146
|
+
warn("캐시 파일 파싱 실패");
|
|
1147
|
+
}
|
|
1148
|
+
} else {
|
|
1149
|
+
addDoctorCheck(report, { name: "mcp-inventory", status: "missing", path: mcpCache, fix: `node ${join(PKG_ROOT, "scripts", "mcp-check.mjs")}` });
|
|
1150
|
+
warn("캐시 없음 — 다음 세션 시작 시 자동 생성");
|
|
1151
|
+
info(`수동: node ${join(PKG_ROOT, "scripts", "mcp-check.mjs")}`);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// 10. CLI 이슈 트래커
|
|
1155
|
+
section("CLI Issues");
|
|
1156
|
+
const issuesFile = join(CLAUDE_DIR, "cache", "cli-issues.jsonl");
|
|
1157
|
+
if (existsSync(issuesFile)) {
|
|
1158
|
+
try {
|
|
1159
|
+
const lines = readFileSync(issuesFile, "utf8").trim().split("\n").filter(Boolean);
|
|
1160
|
+
const entries = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
1161
|
+
const unresolved = entries.filter(e => !e.resolved);
|
|
1162
|
+
|
|
1163
|
+
if (unresolved.length === 0) {
|
|
1164
|
+
addDoctorCheck(report, { name: "cli-issues", status: "ok", path: issuesFile, unresolved: 0 });
|
|
1165
|
+
ok("미해결 이슈 없음");
|
|
1166
|
+
} else {
|
|
1167
|
+
// 패턴별 그룹핑
|
|
1168
|
+
const groups = {};
|
|
1169
|
+
for (const e of unresolved) {
|
|
1170
|
+
const key = `${e.cli}:${e.pattern}`;
|
|
1171
|
+
if (!groups[key]) groups[key] = { ...e, count: 0 };
|
|
1172
|
+
groups[key].count++;
|
|
1173
|
+
if (e.ts > groups[key].ts) { groups[key].ts = e.ts; groups[key].snippet = e.snippet; }
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// 알려진 해결 버전 (패턴별 수정된 triflux 버전)
|
|
1177
|
+
const KNOWN_FIXES = {
|
|
1178
|
+
"gemini:deprecated_flag": "1.8.9", // -p → --prompt
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
const currentVer = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8")).version;
|
|
1182
|
+
let cleaned = 0;
|
|
1183
|
+
|
|
1184
|
+
for (const [key, g] of Object.entries(groups)) {
|
|
1185
|
+
const fixVer = KNOWN_FIXES[key];
|
|
1186
|
+
if (fixVer && currentVer >= fixVer) {
|
|
1187
|
+
// 해결된 이슈 — 자동 정리
|
|
1188
|
+
cleaned += g.count;
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
const age = Date.now() - g.ts;
|
|
1192
|
+
const ago = age < 3600000 ? `${Math.round(age / 60000)}분 전` :
|
|
1193
|
+
age < 86400000 ? `${Math.round(age / 3600000)}시간 전` :
|
|
1194
|
+
`${Math.round(age / 86400000)}일 전`;
|
|
1195
|
+
const sev = g.severity === "error" ? `${RED}ERROR${RESET}` : `${YELLOW}WARN${RESET}`;
|
|
1196
|
+
warn(`[${sev}] ${g.cli}/${g.pattern} x${g.count} (최근: ${ago})`);
|
|
1197
|
+
if (g.snippet) info(` ${g.snippet.substring(0, 120)}`);
|
|
1198
|
+
if (fixVer) info(` 해결: triflux >= v${fixVer} (npm update -g triflux)`);
|
|
1199
|
+
issues++;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// 해결된 이슈 자동 정리
|
|
1203
|
+
if (cleaned > 0) {
|
|
1204
|
+
const remaining = entries.filter(e => {
|
|
1205
|
+
const key = `${e.cli}:${e.pattern}`;
|
|
1206
|
+
const fixVer = KNOWN_FIXES[key];
|
|
1207
|
+
return !(fixVer && currentVer >= fixVer);
|
|
1208
|
+
});
|
|
1209
|
+
writeFileSync(issuesFile, remaining.map(e => JSON.stringify(e)).join("\n") + (remaining.length ? "\n" : ""));
|
|
1210
|
+
ok(`${cleaned}개 해결된 이슈 자동 정리됨`);
|
|
1211
|
+
}
|
|
1212
|
+
addDoctorCheck(report, { name: "cli-issues", status: unresolved.length === 0 ? "ok" : "issues", path: issuesFile, unresolved: unresolved.length });
|
|
1213
|
+
}
|
|
1214
|
+
} catch (e) {
|
|
1215
|
+
addDoctorCheck(report, { name: "cli-issues", status: "invalid", path: issuesFile, fix: "cli-issues.jsonl 형식을 확인하세요." });
|
|
1216
|
+
warn(`이슈 파일 읽기 실패: ${e.message}`);
|
|
1217
|
+
}
|
|
1218
|
+
} else {
|
|
1219
|
+
addDoctorCheck(report, { name: "cli-issues", status: "ok", path: issuesFile, unresolved: 0 });
|
|
1220
|
+
ok("이슈 로그 없음 (정상)");
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// 11. Team Sessions
|
|
1224
|
+
section("Team Sessions");
|
|
1225
|
+
const teamSessionReport = inspectTeamSessions();
|
|
1226
|
+
if (!teamSessionReport.mux) {
|
|
1227
|
+
addDoctorCheck(report, { name: "team-sessions", status: "skipped", detail: "tmux/psmux unavailable" });
|
|
1228
|
+
info("tmux/psmux 미감지 — 팀 세션 검사 건너뜀");
|
|
1229
|
+
} else if (teamSessionReport.sessions.length === 0) {
|
|
1230
|
+
addDoctorCheck(report, { name: "team-sessions", status: "ok", multiplexer: teamSessionReport.mux, sessions: 0 });
|
|
1231
|
+
ok(`활성 팀 세션 없음 ${DIM}(${teamSessionReport.mux})${RESET}`);
|
|
1232
|
+
} else {
|
|
1233
|
+
addDoctorCheck(report, {
|
|
1234
|
+
name: "team-sessions",
|
|
1235
|
+
status: teamSessionReport.sessions.some((session) => session.stale) ? "issues" : "ok",
|
|
1236
|
+
multiplexer: teamSessionReport.mux,
|
|
1237
|
+
sessions: teamSessionReport.sessions.map((session) => ({
|
|
1238
|
+
name: session.sessionName,
|
|
1239
|
+
attached: session.attachedCount,
|
|
1240
|
+
age_sec: session.ageSec,
|
|
1241
|
+
stale: session.stale,
|
|
1242
|
+
})),
|
|
1243
|
+
});
|
|
1244
|
+
info(`multiplexer: ${teamSessionReport.mux}`);
|
|
1245
|
+
|
|
1246
|
+
for (const session of teamSessionReport.sessions) {
|
|
1247
|
+
const attachedLabel = session.attachedCount == null ? "?" : `${session.attachedCount}`;
|
|
1248
|
+
const ageLabel = formatElapsedAge(session.ageSec);
|
|
1249
|
+
|
|
1250
|
+
if (session.stale) {
|
|
1251
|
+
warn(`${session.sessionName}: stale 추정 (attach=${attachedLabel}, 경과=${ageLabel})`);
|
|
1252
|
+
} else {
|
|
1253
|
+
ok(`${session.sessionName}: 정상 (attach=${attachedLabel}, 경과=${ageLabel})`);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (session.createdAt == null) {
|
|
1257
|
+
info(`${session.sessionName}: session_created 파싱 실패${session.createdRaw ? ` (${session.createdRaw})` : ""}`);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const staleSessions = teamSessionReport.sessions.filter((session) => session.stale);
|
|
1262
|
+
if (staleSessions.length > 0) {
|
|
1263
|
+
if (fix) {
|
|
1264
|
+
const cleanupResult = await cleanupStaleTeamSessions(staleSessions);
|
|
1265
|
+
issues += cleanupResult.failed;
|
|
1266
|
+
} else {
|
|
1267
|
+
info("정리: tfx doctor --fix");
|
|
1268
|
+
issues += staleSessions.length;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// 12. OMC stale team 상태
|
|
1274
|
+
section("OMC Stale Teams");
|
|
1275
|
+
const omcTeamReport = inspectStaleOmcTeams({
|
|
1276
|
+
startDir: process.cwd(),
|
|
1277
|
+
maxAgeMs: STALE_TEAM_MAX_AGE_SEC * 1000,
|
|
1278
|
+
liveSessionNames: teamSessionReport.sessions.map((session) => session.sessionName),
|
|
1279
|
+
});
|
|
1280
|
+
if (!omcTeamReport.stateRoot && !omcTeamReport.teamsRoot) {
|
|
1281
|
+
addDoctorCheck(report, { name: "omc-stale-teams", status: "skipped" });
|
|
1282
|
+
info(".omc/state 및 ~/.claude/teams 없음 — 검사 건너뜀");
|
|
1283
|
+
} else if (omcTeamReport.entries.length === 0) {
|
|
1284
|
+
addDoctorCheck(report, { name: "omc-stale-teams", status: "ok", entries: 0 });
|
|
1285
|
+
const roots = [omcTeamReport.stateRoot, omcTeamReport.teamsRoot].filter(Boolean).join(", ");
|
|
1286
|
+
ok(`stale team 없음 ${DIM}(${roots})${RESET}`);
|
|
1287
|
+
} else {
|
|
1288
|
+
addDoctorCheck(report, { name: "omc-stale-teams", status: "issues", entries: omcTeamReport.entries.length, fix: "tfx doctor --fix" });
|
|
1289
|
+
warn(`${omcTeamReport.entries.length}개 stale team 발견`);
|
|
1290
|
+
|
|
1291
|
+
for (const entry of omcTeamReport.entries) {
|
|
1292
|
+
const ageLabel = formatElapsedAge(entry.ageSec);
|
|
1293
|
+
const scopeLabel = entry.scope === "root"
|
|
1294
|
+
? "root-state"
|
|
1295
|
+
: entry.scope === "claude_team"
|
|
1296
|
+
? `claude-team:${entry.teamName || entry.sessionId}`
|
|
1297
|
+
: entry.sessionId;
|
|
1298
|
+
warn(`${scopeLabel}: stale team (경과=${ageLabel}, 프로세스 없음)`);
|
|
1299
|
+
if (entry.teamName) info(`팀: ${entry.teamName}`);
|
|
1300
|
+
info(`파일: ${entry.stateFile || entry.cleanupPath}`);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
if (fix) {
|
|
1304
|
+
const cleanupResult = await cleanupStaleOmcTeams(omcTeamReport.entries);
|
|
1305
|
+
for (const result of cleanupResult.results) {
|
|
1306
|
+
if (result.ok) {
|
|
1307
|
+
const label = result.entry.scope === "root"
|
|
1308
|
+
? "root-state"
|
|
1309
|
+
: result.entry.scope === "claude_team"
|
|
1310
|
+
? (result.entry.teamName || result.entry.sessionId)
|
|
1311
|
+
: result.entry.sessionId;
|
|
1312
|
+
ok(`stale team 정리: ${label}`);
|
|
1313
|
+
} else {
|
|
1314
|
+
const label = result.entry.scope === "root"
|
|
1315
|
+
? "root-state"
|
|
1316
|
+
: result.entry.scope === "claude_team"
|
|
1317
|
+
? (result.entry.teamName || result.entry.sessionId)
|
|
1318
|
+
: result.entry.sessionId;
|
|
1319
|
+
fail(`stale team 정리 실패: ${label} — ${result.error.message}`);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
issues += cleanupResult.failed;
|
|
1323
|
+
} else {
|
|
1324
|
+
info("정리: tfx doctor --fix");
|
|
1325
|
+
issues += omcTeamReport.entries.length;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// 13. Stale Teams (Claude teams/ + tasks/ 자동 감지)
|
|
1330
|
+
section("Stale Teams");
|
|
1331
|
+
const teamsDir = join(CLAUDE_DIR, "teams");
|
|
1332
|
+
const tasksDir = join(CLAUDE_DIR, "tasks");
|
|
1333
|
+
if (existsSync(teamsDir)) {
|
|
1334
|
+
try {
|
|
1335
|
+
const teamDirs = readdirSync(teamsDir).filter(d => {
|
|
1336
|
+
try { return statSync(join(teamsDir, d)).isDirectory(); } catch { return false; }
|
|
1337
|
+
});
|
|
1338
|
+
if (teamDirs.length === 0) {
|
|
1339
|
+
addDoctorCheck(report, { name: "stale-teams", status: "ok", entries: 0 });
|
|
1340
|
+
ok("잔존 팀 없음");
|
|
1341
|
+
} else {
|
|
1342
|
+
const nowMs = Date.now();
|
|
1343
|
+
const staleMaxAgeMs = STALE_TEAM_MAX_AGE_SEC * 1000;
|
|
1344
|
+
const staleTeams = [];
|
|
1345
|
+
const activeTeams = [];
|
|
1346
|
+
|
|
1347
|
+
for (const d of teamDirs) {
|
|
1348
|
+
const teamPath = join(teamsDir, d);
|
|
1349
|
+
const configPath = join(teamPath, "config.json");
|
|
1350
|
+
let teamConfig = null;
|
|
1351
|
+
let configMtimeMs = null;
|
|
1352
|
+
let missingConfig = false;
|
|
1353
|
+
|
|
1354
|
+
// config.json 읽기 — createdAt 또는 mtime으로 나이 판정
|
|
1355
|
+
try {
|
|
1356
|
+
const configStat = statSync(configPath);
|
|
1357
|
+
configMtimeMs = configStat.mtimeMs;
|
|
1358
|
+
teamConfig = JSON.parse(readFileSync(configPath, "utf8"));
|
|
1359
|
+
} catch {
|
|
1360
|
+
missingConfig = true;
|
|
1361
|
+
// config.json 없으면 표시용 경과 시간만 디렉토리 기준으로 계산
|
|
1362
|
+
try { configMtimeMs = statSync(teamPath).mtimeMs; } catch {}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
const createdAtMs = teamConfig?.createdAt ?? configMtimeMs;
|
|
1366
|
+
const ageMs = createdAtMs != null ? Math.max(0, nowMs - createdAtMs) : null;
|
|
1367
|
+
const ageSec = ageMs != null ? Math.floor(ageMs / 1000) : null;
|
|
1368
|
+
const aged = ageMs != null && ageMs >= staleMaxAgeMs;
|
|
1369
|
+
|
|
1370
|
+
// 활성 멤버 확인 — leadSessionId 또는 멤버 agentId로 프로세스 검색
|
|
1371
|
+
let hasActiveMember = false;
|
|
1372
|
+
if (teamConfig?.members?.length > 0) {
|
|
1373
|
+
const searchTokens = [];
|
|
1374
|
+
if (teamConfig.leadSessionId) searchTokens.push(teamConfig.leadSessionId.toLowerCase());
|
|
1375
|
+
if (teamConfig.name) searchTokens.push(teamConfig.name.toLowerCase());
|
|
1376
|
+
for (const member of teamConfig.members) {
|
|
1377
|
+
if (member.agentId) searchTokens.push(member.agentId.split("@")[0].toLowerCase());
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// tmux 세션 이름과 매칭
|
|
1381
|
+
const liveSessionNames = teamSessionReport.sessions.map(s => s.sessionName.toLowerCase());
|
|
1382
|
+
hasActiveMember = searchTokens.some(token =>
|
|
1383
|
+
liveSessionNames.some(name => name.includes(token))
|
|
1384
|
+
);
|
|
1385
|
+
|
|
1386
|
+
// 프로세스 명령줄에서 세션 ID 매칭 (tmux 없는 in-process 팀 지원)
|
|
1387
|
+
if (!hasActiveMember && teamConfig.leadSessionId) {
|
|
1388
|
+
try {
|
|
1389
|
+
const sessionToken = teamConfig.leadSessionId.toLowerCase();
|
|
1390
|
+
// Claude Code 프로세스에서 세션 ID 검색
|
|
1391
|
+
if (process.platform === "win32") {
|
|
1392
|
+
const psOut = execSync(
|
|
1393
|
+
`powershell -NoProfile -Command "$ErrorActionPreference='SilentlyContinue'; Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match '${teamConfig.leadSessionId.slice(0, 8)}' } | Select-Object ProcessId | ConvertTo-Json -Compress"`,
|
|
1394
|
+
{ encoding: "utf8", timeout: 8000, stdio: ["ignore", "pipe", "ignore"], windowsHide: true },
|
|
1395
|
+
).trim();
|
|
1396
|
+
if (psOut && psOut !== "null") {
|
|
1397
|
+
const parsed = JSON.parse(psOut);
|
|
1398
|
+
const procs = Array.isArray(parsed) ? parsed : [parsed];
|
|
1399
|
+
hasActiveMember = procs.some(p => p.ProcessId > 0);
|
|
1400
|
+
}
|
|
1401
|
+
} else {
|
|
1402
|
+
const psOut = execSync(
|
|
1403
|
+
`ps -ax -o pid=,command= | grep -i '${teamConfig.leadSessionId.slice(0, 8)}' | grep -v grep`,
|
|
1404
|
+
{ encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] },
|
|
1405
|
+
).trim();
|
|
1406
|
+
hasActiveMember = psOut.length > 0;
|
|
1407
|
+
}
|
|
1408
|
+
} catch {
|
|
1409
|
+
// 프로세스 검색 실패 — stale로 간주하지 않음 (보수적)
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const stale = missingConfig || (aged && !hasActiveMember);
|
|
1415
|
+
const teamEntry = {
|
|
1416
|
+
name: d,
|
|
1417
|
+
teamName: teamConfig?.name || d,
|
|
1418
|
+
description: teamConfig?.description || null,
|
|
1419
|
+
memberCount: teamConfig?.members?.length || 0,
|
|
1420
|
+
ageSec,
|
|
1421
|
+
stale,
|
|
1422
|
+
hasActiveMember,
|
|
1423
|
+
missingConfig,
|
|
1424
|
+
};
|
|
1425
|
+
|
|
1426
|
+
if (stale) {
|
|
1427
|
+
staleTeams.push(teamEntry);
|
|
1428
|
+
} else {
|
|
1429
|
+
activeTeams.push(teamEntry);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// 활성 팀 표시
|
|
1434
|
+
for (const t of activeTeams) {
|
|
1435
|
+
const ageLabel = formatElapsedAge(t.ageSec);
|
|
1436
|
+
const memberLabel = `${t.memberCount}명`;
|
|
1437
|
+
ok(`${t.name}: 활성 (경과=${ageLabel}, 멤버=${memberLabel})`);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// stale 팀 표시 및 정리
|
|
1441
|
+
if (staleTeams.length === 0 && activeTeams.length > 0) {
|
|
1442
|
+
addDoctorCheck(report, { name: "stale-teams", status: "ok", active: activeTeams.length, stale: 0 });
|
|
1443
|
+
ok("stale 팀 없음");
|
|
1444
|
+
} else if (staleTeams.length > 0) {
|
|
1445
|
+
addDoctorCheck(report, { name: "stale-teams", status: "issues", active: activeTeams.length, stale: staleTeams.length, fix: "tfx doctor --fix" });
|
|
1446
|
+
warn(`${staleTeams.length}개 stale 팀 발견`);
|
|
1447
|
+
for (const t of staleTeams) {
|
|
1448
|
+
const ageLabel = formatElapsedAge(t.ageSec);
|
|
1449
|
+
const reasonLabel = t.missingConfig ? "config.json 없음" : "활성 프로세스 없음";
|
|
1450
|
+
warn(`${t.name}: stale (경과=${ageLabel}, 멤버=${t.memberCount}명, ${reasonLabel})`);
|
|
1451
|
+
if (t.description) info(`설명: ${t.description}`);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
if (fix) {
|
|
1455
|
+
let cleaned = 0;
|
|
1456
|
+
for (const t of staleTeams) {
|
|
1457
|
+
try {
|
|
1458
|
+
await forceCleanupTeam(t.name);
|
|
1459
|
+
cleaned++;
|
|
1460
|
+
ok(`stale 팀 정리: ${t.name}`);
|
|
1461
|
+
} catch (e) {
|
|
1462
|
+
fail(`팀 정리 실패: ${t.name} — ${e.message}`);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
info(`${cleaned}/${staleTeams.length}개 stale 팀 정리 완료`);
|
|
1466
|
+
} else {
|
|
1467
|
+
info("정리: tfx doctor --fix");
|
|
1468
|
+
issues += staleTeams.length;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
} catch (e) {
|
|
1473
|
+
addDoctorCheck(report, { name: "stale-teams", status: "invalid", fix: "teams 디렉토리 구조를 확인하세요." });
|
|
1474
|
+
warn(`teams 디렉토리 읽기 실패: ${e.message}`);
|
|
1475
|
+
}
|
|
1476
|
+
} else {
|
|
1477
|
+
addDoctorCheck(report, { name: "stale-teams", status: "ok", entries: 0 });
|
|
1478
|
+
ok("잔존 팀 없음");
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// 결과
|
|
1482
|
+
console.log(`\n ${LINE}`);
|
|
1483
|
+
if (issues === 0) {
|
|
1484
|
+
console.log(` ${GREEN_BRIGHT}${BOLD}✓ 모든 검사 통과${RESET}\n`);
|
|
1485
|
+
} else {
|
|
1486
|
+
console.log(` ${YELLOW}${BOLD}⚠ ${issues}개 항목 확인 필요${RESET}\n`);
|
|
1487
|
+
}
|
|
1488
|
+
report.issue_count = issues;
|
|
1489
|
+
report.status = issues === 0 ? "ok" : "issues";
|
|
1490
|
+
if (json) printJson(report);
|
|
1491
|
+
return report;
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function cmdUpdate() {
|
|
1496
|
+
const isDev = isDevUpdateRequested(NORMALIZED_ARGS);
|
|
1497
|
+
const tagLabel = isDev ? ` ${YELLOW}--dev${RESET}` : "";
|
|
1498
|
+
console.log(`\n${BOLD}triflux update${RESET}${tagLabel}\n`);
|
|
1499
|
+
|
|
1500
|
+
// 1. 설치 방식 감지
|
|
1501
|
+
const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
|
|
1502
|
+
let installMode = "unknown";
|
|
1503
|
+
let pluginPath = null;
|
|
1504
|
+
|
|
1505
|
+
// 플러그인 모드 감지
|
|
1506
|
+
if (existsSync(pluginsFile)) {
|
|
1507
|
+
try {
|
|
1508
|
+
const plugins = JSON.parse(readFileSync(pluginsFile, "utf8"));
|
|
1509
|
+
for (const [key, entries] of Object.entries(plugins.plugins || {})) {
|
|
1510
|
+
if (key.startsWith("triflux")) {
|
|
1511
|
+
pluginPath = entries[0]?.installPath;
|
|
1512
|
+
installMode = "plugin";
|
|
1513
|
+
break;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
} catch {}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// PKG_ROOT가 플러그인 캐시 내에 있으면 플러그인 모드
|
|
1520
|
+
if (installMode === "unknown" && PKG_ROOT.includes(join(".claude", "plugins"))) {
|
|
1521
|
+
installMode = "plugin";
|
|
1522
|
+
pluginPath = PKG_ROOT;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// npm global 감지
|
|
1526
|
+
if (installMode === "unknown") {
|
|
1527
|
+
try {
|
|
1528
|
+
const npmList = execSync("npm list -g triflux --depth=0", {
|
|
1529
|
+
encoding: "utf8",
|
|
1530
|
+
timeout: 10000,
|
|
1531
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1532
|
+
});
|
|
1533
|
+
if (npmList.includes("triflux")) installMode = "npm-global";
|
|
1534
|
+
} catch {}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// npm local 감지
|
|
1538
|
+
if (installMode === "unknown") {
|
|
1539
|
+
const localPkg = join(process.cwd(), "node_modules", "triflux");
|
|
1540
|
+
if (existsSync(localPkg)) installMode = "npm-local";
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// git 저장소 직접 사용
|
|
1544
|
+
if (installMode === "unknown" && existsSync(join(PKG_ROOT, ".git"))) {
|
|
1545
|
+
installMode = "git-local";
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
info(`검색: ${installMode === "plugin" ? "플러그인" : installMode === "npm-global" ? "npm global" : installMode === "npm-local" ? "npm local" : installMode === "git-local" ? "git 로컬 저장소" : "알 수 없음"} 설치 감지`);
|
|
1549
|
+
|
|
1550
|
+
// 2. 설치 방식에 따라 업데이트
|
|
1551
|
+
const oldVer = PKG.version;
|
|
1552
|
+
let updated = false;
|
|
1553
|
+
let stoppedHubInfo = null;
|
|
1554
|
+
|
|
1555
|
+
try {
|
|
1556
|
+
switch (installMode) {
|
|
1557
|
+
case "plugin": {
|
|
1558
|
+
const gitDir = pluginPath || PKG_ROOT;
|
|
1559
|
+
const result = execSync("git pull", {
|
|
1560
|
+
encoding: "utf8",
|
|
1561
|
+
timeout: 30000,
|
|
1562
|
+
cwd: gitDir,
|
|
1563
|
+
}).trim();
|
|
1564
|
+
ok(`git pull — ${result}`);
|
|
1565
|
+
updated = true;
|
|
1566
|
+
break;
|
|
1567
|
+
}
|
|
1568
|
+
case "npm-global": {
|
|
1569
|
+
stoppedHubInfo = stopHubForUpdate();
|
|
1570
|
+
if (stoppedHubInfo?.pid) {
|
|
1571
|
+
info(`실행 중 hub 정지 (PID ${stoppedHubInfo.pid})`);
|
|
1572
|
+
}
|
|
1573
|
+
const npmCmd = isDev ? "npm install -g triflux@dev" : "npm update -g triflux";
|
|
1574
|
+
const result = execSync(npmCmd, {
|
|
1575
|
+
encoding: "utf8",
|
|
1576
|
+
timeout: 60000,
|
|
1577
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1578
|
+
}).trim().split(/\r?\n/)[0];
|
|
1579
|
+
ok(`${isDev ? "npm install -g triflux@dev" : "npm update -g triflux"} — ${result || "완료"}`);
|
|
1580
|
+
updated = true;
|
|
1581
|
+
break;
|
|
1582
|
+
}
|
|
1583
|
+
case "npm-local": {
|
|
1584
|
+
const npmLocalCmd = isDev ? "npm install triflux@dev" : "npm update triflux";
|
|
1585
|
+
const result = execSync(npmLocalCmd, {
|
|
1586
|
+
encoding: "utf8",
|
|
1587
|
+
timeout: 60000,
|
|
1588
|
+
cwd: process.cwd(),
|
|
1589
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1590
|
+
}).trim().split(/\r?\n/)[0];
|
|
1591
|
+
ok(`${isDev ? "npm install triflux@dev" : "npm update triflux"} — ${result || "완료"}`);
|
|
1592
|
+
updated = true;
|
|
1593
|
+
break;
|
|
1594
|
+
}
|
|
1595
|
+
case "git-local": {
|
|
1596
|
+
const result = execSync("git pull", {
|
|
1597
|
+
encoding: "utf8",
|
|
1598
|
+
timeout: 30000,
|
|
1599
|
+
cwd: PKG_ROOT,
|
|
1600
|
+
}).trim();
|
|
1601
|
+
ok(`git pull — ${result}`);
|
|
1602
|
+
updated = true;
|
|
1603
|
+
break;
|
|
1604
|
+
}
|
|
1605
|
+
default:
|
|
1606
|
+
fail("설치 방식을 감지할 수 없음");
|
|
1607
|
+
info("수동 업데이트: cd <triflux-dir> && git pull");
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
} catch (e) {
|
|
1611
|
+
if (stoppedHubInfo && startHubAfterUpdate(stoppedHubInfo)) {
|
|
1612
|
+
info("업데이트 실패 후 hub 재기동 시도");
|
|
1613
|
+
}
|
|
1614
|
+
fail(`업데이트 실패: ${e.message}`);
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// 3. setup 재실행 (tfx-route.sh, HUD, 스킬 동기화)
|
|
1619
|
+
if (updated) {
|
|
1620
|
+
console.log("");
|
|
1621
|
+
// 업데이트 후 새 버전 읽기
|
|
1622
|
+
let newVer = oldVer;
|
|
1623
|
+
try {
|
|
1624
|
+
const newPkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
|
|
1625
|
+
newVer = newPkg.version;
|
|
1626
|
+
} catch {}
|
|
1627
|
+
|
|
1628
|
+
if (newVer !== oldVer) {
|
|
1629
|
+
ok(`버전: v${oldVer} → v${newVer}`);
|
|
1630
|
+
} else {
|
|
1631
|
+
ok(`버전: v${oldVer} (이미 최신)`);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// setup 재실행
|
|
1635
|
+
console.log("");
|
|
1636
|
+
info("setup 재실행 중...");
|
|
1637
|
+
cmdSetup();
|
|
1638
|
+
|
|
1639
|
+
if (stoppedHubInfo) {
|
|
1640
|
+
if (startHubAfterUpdate(stoppedHubInfo)) info("hub 재기동 완료");
|
|
1641
|
+
else warn("hub 재기동 실패 — `tfx hub start`로 수동 시작 필요");
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
console.log(`${GREEN}${BOLD}업데이트 완료${RESET}\n`);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
function cmdList(options = {}) {
|
|
1649
|
+
const { json = false } = options;
|
|
1650
|
+
const pluginSkills = join(PKG_ROOT, "skills");
|
|
1651
|
+
const installedSkills = join(CLAUDE_DIR, "skills");
|
|
1652
|
+
const packageSkills = [];
|
|
1653
|
+
const userSkills = [];
|
|
1654
|
+
|
|
1655
|
+
if (existsSync(pluginSkills)) {
|
|
1656
|
+
for (const name of readdirSync(pluginSkills).sort()) {
|
|
1657
|
+
const src = join(pluginSkills, name, "SKILL.md");
|
|
1658
|
+
if (!existsSync(src)) continue;
|
|
1659
|
+
const dst = join(installedSkills, name, "SKILL.md");
|
|
1660
|
+
packageSkills.push({ name, installed: existsSync(dst) });
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
const pkgNames = new Set(existsSync(pluginSkills) ? readdirSync(pluginSkills) : []);
|
|
1665
|
+
if (existsSync(installedSkills)) {
|
|
1666
|
+
for (const name of readdirSync(installedSkills).sort()) {
|
|
1667
|
+
if (pkgNames.has(name)) continue;
|
|
1668
|
+
const skill = join(installedSkills, name, "SKILL.md");
|
|
1669
|
+
if (!existsSync(skill)) continue;
|
|
1670
|
+
userSkills.push(name);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
if (json) {
|
|
1675
|
+
printJson({
|
|
1676
|
+
package_skills: packageSkills,
|
|
1677
|
+
user_skills: userSkills,
|
|
1678
|
+
install_path: installedSkills,
|
|
1679
|
+
});
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
console.log(`\n ${AMBER}${BOLD}⬡ triflux list${RESET} ${VER}\n`);
|
|
1684
|
+
console.log(` ${LINE}`);
|
|
1685
|
+
|
|
1686
|
+
section("패키지 스킬");
|
|
1687
|
+
for (const skill of packageSkills) {
|
|
1688
|
+
if (skill.installed) {
|
|
1689
|
+
console.log(` ${GREEN_BRIGHT}✓${RESET} ${BOLD}${skill.name}${RESET}`);
|
|
1690
|
+
} else {
|
|
1691
|
+
console.log(` ${RED_BRIGHT}✗${RESET} ${DIM}${skill.name}${RESET} ${GRAY}(미설치)${RESET}`);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
section("사용자 스킬");
|
|
1696
|
+
for (const name of userSkills) {
|
|
1697
|
+
console.log(` ${AMBER}◆${RESET} ${name}`);
|
|
1698
|
+
}
|
|
1699
|
+
if (userSkills.length === 0) console.log(` ${GRAY}없음${RESET}`);
|
|
1700
|
+
|
|
1701
|
+
console.log(`\n ${LINE}`);
|
|
1702
|
+
console.log(` ${GRAY}${installedSkills}${RESET}\n`);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
function cmdVersion(options = {}) {
|
|
1706
|
+
const { json = false } = options;
|
|
1707
|
+
const routeVer = getVersion(join(CLAUDE_DIR, "scripts", "tfx-route.sh"));
|
|
1708
|
+
const hudVer = getVersion(join(CLAUDE_DIR, "hud", "hud-qos-status.mjs"));
|
|
1709
|
+
if (json) {
|
|
1710
|
+
printJson({
|
|
1711
|
+
triflux: PKG.version,
|
|
1712
|
+
tfx_route: routeVer,
|
|
1713
|
+
hud: hudVer,
|
|
1714
|
+
node: process.versions.node,
|
|
1715
|
+
});
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
console.log(`\n ${AMBER}${BOLD}⬡ triflux${RESET} ${WHITE_BRIGHT}v${PKG.version}${RESET}`);
|
|
1719
|
+
if (routeVer) console.log(` ${GRAY}tfx-route${RESET} v${routeVer}`);
|
|
1720
|
+
if (hudVer) console.log(` ${GRAY}hud${RESET} v${hudVer}`);
|
|
1721
|
+
console.log("");
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function cmdSchema(args = []) {
|
|
1725
|
+
const bundle = loadDelegatorSchemaBundle();
|
|
1726
|
+
const selector = String(args[0] || "").trim();
|
|
1727
|
+
const toolEntry = Array.isArray(bundle["x-triflux-mcp-tools"])
|
|
1728
|
+
? bundle["x-triflux-mcp-tools"].find((tool) => tool.name === selector)
|
|
1729
|
+
: null;
|
|
1730
|
+
|
|
1731
|
+
if (!selector) {
|
|
1732
|
+
printJson({
|
|
1733
|
+
$schema: bundle.$schema,
|
|
1734
|
+
title: "Triflux CLI Schema Bundle",
|
|
1735
|
+
global_options: [
|
|
1736
|
+
{ name: "--json", type: "boolean", description: "지원 커맨드의 출력을 JSON으로 전환" },
|
|
1737
|
+
],
|
|
1738
|
+
commands: CLI_COMMAND_SCHEMAS,
|
|
1739
|
+
hub_tools: bundle,
|
|
1740
|
+
});
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
if (CLI_COMMAND_SCHEMAS[selector]) {
|
|
1745
|
+
printJson({
|
|
1746
|
+
command: selector,
|
|
1747
|
+
...CLI_COMMAND_SCHEMAS[selector],
|
|
1748
|
+
});
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
if (toolEntry) {
|
|
1753
|
+
printJson({
|
|
1754
|
+
tool: toolEntry.name,
|
|
1755
|
+
description: toolEntry.description,
|
|
1756
|
+
pipeAction: toolEntry.pipeAction,
|
|
1757
|
+
inputSchema: bundle.$defs?.[toolEntry.inputSchemaDef] || null,
|
|
1758
|
+
outputSchema: bundle.$defs?.[toolEntry.outputSchemaDef] || null,
|
|
1759
|
+
});
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
throw createCliError(`알 수 없는 schema 대상: ${selector}`, {
|
|
1764
|
+
exitCode: EXIT_ARG_ERROR,
|
|
1765
|
+
reason: "argError",
|
|
1766
|
+
fix: "tfx schema 또는 tfx schema <command>를 실행해 사용 가능한 대상을 확인하세요.",
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function checkForUpdate() {
|
|
1771
|
+
const cacheFile = join(CLAUDE_DIR, "cache", "triflux-update-check.json");
|
|
1772
|
+
const cacheDir = dirname(cacheFile);
|
|
1773
|
+
|
|
1774
|
+
// 캐시 확인 (1시간 이내면 캐시 사용)
|
|
1775
|
+
try {
|
|
1776
|
+
if (existsSync(cacheFile)) {
|
|
1777
|
+
const cache = JSON.parse(readFileSync(cacheFile, "utf8"));
|
|
1778
|
+
if (Date.now() - cache.timestamp < 3600000) {
|
|
1779
|
+
return cache.latest !== PKG.version ? cache.latest : null;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
} catch {}
|
|
1783
|
+
|
|
1784
|
+
// npm registry 조회
|
|
1785
|
+
try {
|
|
1786
|
+
const result = execSync("npm view triflux version", {
|
|
1787
|
+
encoding: "utf8",
|
|
1788
|
+
timeout: 5000,
|
|
1789
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1790
|
+
}).trim();
|
|
1791
|
+
|
|
1792
|
+
if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true });
|
|
1793
|
+
writeFileSync(cacheFile, JSON.stringify({ latest: result, timestamp: Date.now() }));
|
|
1794
|
+
|
|
1795
|
+
return result !== PKG.version ? result : null;
|
|
1796
|
+
} catch {
|
|
1797
|
+
return null;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
function cmdHelp() {
|
|
1802
|
+
const latestVer = checkForUpdate();
|
|
1803
|
+
const updateNotice = latestVer
|
|
1804
|
+
? `\n ${YELLOW}${BOLD}↑ v${latestVer} 사용 가능${RESET} ${GRAY}npm update -g triflux${RESET}\n`
|
|
1805
|
+
: "";
|
|
1806
|
+
|
|
1807
|
+
console.log(`
|
|
1808
|
+
${AMBER}${BOLD}⬡ triflux${RESET} ${DIM}v${PKG.version}${RESET}
|
|
1809
|
+
${GRAY}CLI-first multi-model orchestrator for Claude Code${RESET}
|
|
1810
|
+
${updateNotice}
|
|
1811
|
+
${LINE}
|
|
1812
|
+
|
|
1813
|
+
${BOLD}Commands${RESET}
|
|
1814
|
+
|
|
1815
|
+
${WHITE_BRIGHT}tfx setup${RESET} ${GRAY}파일 동기화 + HUD 설정${RESET}
|
|
1816
|
+
${DIM} --dry-run${RESET} ${GRAY}변경 예정 작업을 JSON으로 미리보기${RESET}
|
|
1817
|
+
${WHITE_BRIGHT}tfx doctor${RESET} ${GRAY}CLI 진단 + 이슈 확인${RESET}
|
|
1818
|
+
${DIM} --fix${RESET} ${GRAY}진단 + 자동 수정${RESET}
|
|
1819
|
+
${DIM} --reset${RESET} ${GRAY}캐시 전체 초기화${RESET}
|
|
1820
|
+
${DIM} --json${RESET} ${GRAY}구조화된 진단 결과 JSON 출력${RESET}
|
|
1821
|
+
${WHITE_BRIGHT}tfx update${RESET} ${GRAY}최신 안정 버전으로 업데이트${RESET}
|
|
1822
|
+
${DIM} --dev / dev${RESET} ${GRAY}dev 태그로 업데이트${RESET}
|
|
1823
|
+
${WHITE_BRIGHT}tfx list${RESET} ${GRAY}설치된 스킬 목록${RESET}
|
|
1824
|
+
${WHITE_BRIGHT}tfx schema${RESET} ${GRAY}CLI/Hub schema JSON 출력${RESET}
|
|
1825
|
+
${WHITE_BRIGHT}tfx hub${RESET} ${GRAY}MCP 메시지 버스 관리 (start/stop/status)${RESET}
|
|
1826
|
+
${WHITE_BRIGHT}tfx tray${RESET} ${GRAY}Windows 시스템 트레이 실행${RESET}
|
|
1827
|
+
${DIM} --detach${RESET} ${GRAY}백그라운드 트레이 프로세스로 분리${RESET}
|
|
1828
|
+
${WHITE_BRIGHT}tfx multi${RESET} ${GRAY}멀티-CLI 팀 모드 (tmux + Hub)${RESET}
|
|
1829
|
+
${WHITE_BRIGHT}tfx codex-team${RESET} ${GRAY}Codex 전용 팀 모드 (기본 lead/agents: codex)${RESET}
|
|
1830
|
+
${WHITE_BRIGHT}tfx notion-read${RESET} ${GRAY}Notion 페이지 → 마크다운 (Codex/Gemini MCP)${RESET}
|
|
1831
|
+
${WHITE_BRIGHT}tfx version${RESET} ${GRAY}버전 표시${RESET}
|
|
1832
|
+
|
|
1833
|
+
${BOLD}Skills${RESET} ${GRAY}(Claude Code 슬래시 커맨드)${RESET}
|
|
1834
|
+
|
|
1835
|
+
${AMBER}/tfx-auto${RESET} ${GRAY}자동 분류 + 병렬 실행${RESET}
|
|
1836
|
+
${WHITE_BRIGHT}/tfx-auto-codex${RESET} ${GRAY}Codex 리드 + Gemini 유지 (no-Claude-native)${RESET}
|
|
1837
|
+
${WHITE_BRIGHT}/tfx-codex${RESET} ${GRAY}Codex 전용 모드${RESET}
|
|
1838
|
+
${BLUE}/tfx-gemini${RESET} ${GRAY}Gemini 전용 모드${RESET}
|
|
1839
|
+
${AMBER}/tfx-setup${RESET} ${GRAY}HUD 설정 + 진단${RESET}
|
|
1840
|
+
${YELLOW}/tfx-doctor${RESET} ${GRAY}진단 + 수리 + 캐시 초기화${RESET}
|
|
1841
|
+
|
|
1842
|
+
${LINE}
|
|
1843
|
+
${GRAY}github.com/tellang/triflux${RESET}
|
|
1844
|
+
`);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
async function cmdCodexTeam(args = []) {
|
|
1848
|
+
const sub = String(args[0] || "").toLowerCase();
|
|
1849
|
+
const passthrough = new Set([
|
|
1850
|
+
"status", "attach", "stop", "kill", "send", "list", "help", "--help", "-h",
|
|
1851
|
+
"tasks", "task", "focus", "interrupt", "control", "debug",
|
|
1852
|
+
]);
|
|
1853
|
+
|
|
1854
|
+
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
1855
|
+
console.log(`
|
|
1856
|
+
${AMBER}${BOLD}⬡ tfx codex-team${RESET}
|
|
1857
|
+
|
|
1858
|
+
${WHITE_BRIGHT}tfx codex-team "작업"${RESET} ${GRAY}Codex 리드 + 워커 2개로 팀 시작${RESET}
|
|
1859
|
+
${WHITE_BRIGHT}tfx codex-team --layout 1xN "작업"${RESET} ${GRAY}(세로 분할 컬럼)${RESET}
|
|
1860
|
+
${WHITE_BRIGHT}tfx codex-team --layout Nx1 "작업"${RESET} ${GRAY}(가로 분할 스택)${RESET}
|
|
1861
|
+
${WHITE_BRIGHT}tfx codex-team status${RESET}
|
|
1862
|
+
${WHITE_BRIGHT}tfx codex-team debug --lines 30${RESET}
|
|
1863
|
+
${WHITE_BRIGHT}tfx codex-team send N "msg"${RESET}
|
|
1864
|
+
|
|
1865
|
+
${DIM}내부적으로 tfx multi을 호출하며, 시작 시 --lead codex --agents codex,codex를 기본 주입합니다.${RESET}
|
|
1866
|
+
`);
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
const hasAgents = args.includes("--agents");
|
|
1871
|
+
const hasLead = args.includes("--lead");
|
|
1872
|
+
const hasLayout = args.includes("--layout");
|
|
1873
|
+
const isControl = passthrough.has(sub);
|
|
1874
|
+
const normalizedArgs = isControl && args.length ? [sub, ...args.slice(1)] : args;
|
|
1875
|
+
const inject = [];
|
|
1876
|
+
if (!isControl && !hasLead) inject.push("--lead", "codex");
|
|
1877
|
+
if (!isControl && !hasAgents) inject.push("--agents", "codex,codex");
|
|
1878
|
+
if (!isControl && !hasLayout) inject.push("--layout", "1xN");
|
|
1879
|
+
const forwarded = isControl ? normalizedArgs : [...inject, ...args];
|
|
1880
|
+
|
|
1881
|
+
const prevArgv = process.argv;
|
|
1882
|
+
const prevProfile = process.env.TFX_TEAM_PROFILE;
|
|
1883
|
+
process.env.TFX_TEAM_PROFILE = "codex-team";
|
|
1884
|
+
const { pathToFileURL } = await import("node:url");
|
|
1885
|
+
const { cmdTeam } = await import(pathToFileURL(join(PKG_ROOT, "hub", "team", "cli", "index.mjs")).href);
|
|
1886
|
+
process.argv = [prevArgv[0], prevArgv[1], "team", ...forwarded];
|
|
1887
|
+
try {
|
|
1888
|
+
await cmdTeam();
|
|
1889
|
+
} finally {
|
|
1890
|
+
process.argv = prevArgv;
|
|
1891
|
+
if (typeof prevProfile === "string") process.env.TFX_TEAM_PROFILE = prevProfile;
|
|
1892
|
+
else delete process.env.TFX_TEAM_PROFILE;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// ── Hub preflight 체크 (multi/auto 실행 전) ──
|
|
1897
|
+
|
|
1898
|
+
async function checkHubRunning() {
|
|
1899
|
+
const port = Number(process.env.TFX_HUB_PORT || "27888");
|
|
1900
|
+
try {
|
|
1901
|
+
const res = await fetch(`http://127.0.0.1:${port}/status`, {
|
|
1902
|
+
signal: AbortSignal.timeout(2000),
|
|
1903
|
+
});
|
|
1904
|
+
if (res.ok) return true;
|
|
1905
|
+
} catch {}
|
|
1906
|
+
console.log("");
|
|
1907
|
+
warn(`${AMBER}tfx-hub${RESET}가 실행되고 있지 않습니다.`);
|
|
1908
|
+
info(`Hub 없이 실행하면 Claude 네이티브 에이전트로 폴백되어 토큰이 소비됩니다.`);
|
|
1909
|
+
info(`Codex(무료) 위임을 활용하려면 먼저 Hub를 시작하세요:\n`);
|
|
1910
|
+
console.log(` ${WHITE_BRIGHT}tfx hub start${RESET}\n`);
|
|
1911
|
+
return false;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// ── hub 서브커맨드 ──
|
|
1915
|
+
|
|
1916
|
+
const HUB_PID_DIR = join(homedir(), ".claude", "cache", "tfx-hub");
|
|
1917
|
+
const HUB_PID_FILE = join(HUB_PID_DIR, "hub.pid");
|
|
1918
|
+
|
|
1919
|
+
function sleepMs(ms) {
|
|
1920
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
function stopHubForUpdate() {
|
|
1924
|
+
if (!existsSync(HUB_PID_FILE)) return null;
|
|
1925
|
+
let info = null;
|
|
1926
|
+
try {
|
|
1927
|
+
info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
1928
|
+
process.kill(info.pid, 0);
|
|
1929
|
+
} catch {
|
|
1930
|
+
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1931
|
+
return null;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
try {
|
|
1935
|
+
if (process.platform === "win32") {
|
|
1936
|
+
execFileSync("taskkill", ["/PID", String(info.pid), "/T", "/F"], {
|
|
1937
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1938
|
+
timeout: 10000,
|
|
1939
|
+
});
|
|
1940
|
+
} else {
|
|
1941
|
+
process.kill(info.pid, "SIGTERM");
|
|
1942
|
+
}
|
|
1943
|
+
} catch {
|
|
1944
|
+
try { process.kill(info.pid, "SIGKILL"); } catch {}
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
// Windows에서 better-sqlite3.node 파일 핸들 해제 대기
|
|
1948
|
+
// taskkill 후 프로세스 종료 + 파일 핸들 해제까지 최대 5초
|
|
1949
|
+
const sqliteNode = join(PKG_ROOT, "node_modules", "better-sqlite3", "build", "Release", "better_sqlite3.node");
|
|
1950
|
+
for (let i = 0; i < 10; i++) {
|
|
1951
|
+
sleepMs(500);
|
|
1952
|
+
try { process.kill(info.pid, 0); } catch { break; }
|
|
1953
|
+
}
|
|
1954
|
+
// 파일 잠금 해제 확인 (Windows EBUSY 방지)
|
|
1955
|
+
if (existsSync(sqliteNode)) {
|
|
1956
|
+
for (let i = 0; i < 6; i++) {
|
|
1957
|
+
try {
|
|
1958
|
+
const fd = openSync(sqliteNode, "r");
|
|
1959
|
+
closeSync(fd);
|
|
1960
|
+
break;
|
|
1961
|
+
} catch {
|
|
1962
|
+
sleepMs(500);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1967
|
+
return info;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
function startHubAfterUpdate(info) {
|
|
1971
|
+
if (!info) return false;
|
|
1972
|
+
const serverPath = join(PKG_ROOT, "hub", "server.mjs");
|
|
1973
|
+
if (!existsSync(serverPath)) return false;
|
|
1974
|
+
const port = Number(info?.port) > 0 ? String(info.port) : String(process.env.TFX_HUB_PORT || "27888");
|
|
1975
|
+
|
|
1976
|
+
try {
|
|
1977
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
1978
|
+
env: { ...process.env, TFX_HUB_PORT: port },
|
|
1979
|
+
stdio: "ignore",
|
|
1980
|
+
detached: true,
|
|
1981
|
+
windowsHide: true,
|
|
1982
|
+
});
|
|
1983
|
+
child.unref();
|
|
1984
|
+
return true;
|
|
1985
|
+
} catch {
|
|
1986
|
+
return false;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
// 설치된 CLI에 tfx-hub MCP 서버 자동 등록 (1회 설정, 이후 재실행 불필요)
|
|
1991
|
+
function autoRegisterMcp(mcpUrl) {
|
|
1992
|
+
section("MCP 자동 등록");
|
|
1993
|
+
|
|
1994
|
+
// Codex — codex mcp add
|
|
1995
|
+
if (which("codex")) {
|
|
1996
|
+
try {
|
|
1997
|
+
// 이미 등록됐는지 확인
|
|
1998
|
+
const list = execSync("codex mcp list 2>&1", { encoding: "utf8", timeout: 10000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1999
|
+
if (list.includes("tfx-hub")) {
|
|
2000
|
+
ok("Codex: 이미 등록됨");
|
|
2001
|
+
} else {
|
|
2002
|
+
execFileSync("codex", ["mcp", "add", "tfx-hub", "--url", mcpUrl], { timeout: 10000, stdio: "ignore" });
|
|
2003
|
+
ok("Codex: MCP 등록 완료");
|
|
2004
|
+
}
|
|
2005
|
+
} catch {
|
|
2006
|
+
// mcp list/add 미지원 → 설정 파일 직접 수정
|
|
2007
|
+
try {
|
|
2008
|
+
const codexDir = join(homedir(), ".codex");
|
|
2009
|
+
const configFile = join(codexDir, "config.json");
|
|
2010
|
+
let config = {};
|
|
2011
|
+
if (existsSync(configFile)) config = JSON.parse(readFileSync(configFile, "utf8"));
|
|
2012
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
2013
|
+
if (!config.mcpServers["tfx-hub"]) {
|
|
2014
|
+
config.mcpServers["tfx-hub"] = { url: mcpUrl };
|
|
2015
|
+
if (!existsSync(codexDir)) mkdirSync(codexDir, { recursive: true });
|
|
2016
|
+
writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n");
|
|
2017
|
+
ok("Codex: config.json에 등록 완료");
|
|
2018
|
+
} else {
|
|
2019
|
+
ok("Codex: 이미 등록됨");
|
|
2020
|
+
}
|
|
2021
|
+
} catch (e) { warn(`Codex 등록 실패: ${e.message}`); }
|
|
2022
|
+
}
|
|
2023
|
+
} else {
|
|
2024
|
+
info("Codex: 미설치 (건너뜀)");
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// Gemini — settings.json 직접 수정
|
|
2028
|
+
if (which("gemini")) {
|
|
2029
|
+
try {
|
|
2030
|
+
const geminiDir = join(homedir(), ".gemini");
|
|
2031
|
+
const settingsFile = join(geminiDir, "settings.json");
|
|
2032
|
+
let settings = {};
|
|
2033
|
+
if (existsSync(settingsFile)) settings = JSON.parse(readFileSync(settingsFile, "utf8"));
|
|
2034
|
+
if (!settings.mcpServers) settings.mcpServers = {};
|
|
2035
|
+
if (!settings.mcpServers["tfx-hub"]) {
|
|
2036
|
+
settings.mcpServers["tfx-hub"] = { url: mcpUrl };
|
|
2037
|
+
if (!existsSync(geminiDir)) mkdirSync(geminiDir, { recursive: true });
|
|
2038
|
+
writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + "\n");
|
|
2039
|
+
ok("Gemini: settings.json에 등록 완료");
|
|
2040
|
+
} else {
|
|
2041
|
+
ok("Gemini: 이미 등록됨");
|
|
2042
|
+
}
|
|
2043
|
+
} catch (e) { warn(`Gemini 등록 실패: ${e.message}`); }
|
|
2044
|
+
} else {
|
|
2045
|
+
info("Gemini: 미설치 (건너뜀)");
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
// Claude — 프로젝트 .mcp.json에 등록 (오케스트레이터용)
|
|
2049
|
+
try {
|
|
2050
|
+
const mcpJsonPath = join(PKG_ROOT, ".mcp.json");
|
|
2051
|
+
let mcpJson = {};
|
|
2052
|
+
if (existsSync(mcpJsonPath)) mcpJson = JSON.parse(readFileSync(mcpJsonPath, "utf8"));
|
|
2053
|
+
if (!mcpJson.mcpServers) mcpJson.mcpServers = {};
|
|
2054
|
+
if (!mcpJson.mcpServers["tfx-hub"]) {
|
|
2055
|
+
mcpJson.mcpServers["tfx-hub"] = { type: "url", url: mcpUrl };
|
|
2056
|
+
writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2) + "\n");
|
|
2057
|
+
ok("Claude: .mcp.json에 등록 완료");
|
|
2058
|
+
} else {
|
|
2059
|
+
ok("Claude: 이미 등록됨");
|
|
2060
|
+
}
|
|
2061
|
+
} catch (e) { warn(`Claude 등록 실패: ${e.message}`); }
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
async function cmdHub(args = [], options = {}) {
|
|
2065
|
+
const { json = false } = options;
|
|
2066
|
+
const sub = args[0] || "status";
|
|
2067
|
+
const defaultPortRaw = Number(process.env.TFX_HUB_PORT || "27888");
|
|
2068
|
+
const probePort = Number.isFinite(defaultPortRaw) && defaultPortRaw > 0 ? defaultPortRaw : 27888;
|
|
2069
|
+
const formatHostForUrl = (host) => host.includes(":") ? `[${host}]` : host;
|
|
2070
|
+
const probeHubStatus = async (host = "127.0.0.1", port = probePort, timeoutMs = 3000) => {
|
|
2071
|
+
try {
|
|
2072
|
+
const res = await fetch(`http://${formatHostForUrl(host)}:${port}/status`, {
|
|
2073
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
2074
|
+
});
|
|
2075
|
+
if (!res.ok) return null;
|
|
2076
|
+
const data = await res.json();
|
|
2077
|
+
return data?.hub ? data : null;
|
|
2078
|
+
} catch {
|
|
2079
|
+
return null;
|
|
2080
|
+
}
|
|
2081
|
+
};
|
|
2082
|
+
const recoverPidFile = (statusData, defaultHost = "127.0.0.1") => {
|
|
2083
|
+
const pid = Number(statusData?.pid);
|
|
2084
|
+
const port = Number(statusData?.port) || probePort;
|
|
2085
|
+
if (!Number.isFinite(pid) || pid <= 0) return;
|
|
2086
|
+
try {
|
|
2087
|
+
mkdirSync(HUB_PID_DIR, { recursive: true });
|
|
2088
|
+
writeFileSync(HUB_PID_FILE, JSON.stringify({
|
|
2089
|
+
pid,
|
|
2090
|
+
port,
|
|
2091
|
+
host: defaultHost,
|
|
2092
|
+
url: `http://${formatHostForUrl(defaultHost)}:${port}/mcp`,
|
|
2093
|
+
started: Date.now(),
|
|
2094
|
+
}));
|
|
2095
|
+
} catch {}
|
|
2096
|
+
};
|
|
2097
|
+
const emitHubStatus = (payload) => {
|
|
2098
|
+
if (!json) return false;
|
|
2099
|
+
printJson(payload);
|
|
2100
|
+
return true;
|
|
2101
|
+
};
|
|
2102
|
+
|
|
2103
|
+
switch (sub) {
|
|
2104
|
+
case "start": {
|
|
2105
|
+
// 이미 실행 중인지 확인
|
|
2106
|
+
if (existsSync(HUB_PID_FILE)) {
|
|
2107
|
+
try {
|
|
2108
|
+
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
2109
|
+
process.kill(info.pid, 0); // 프로세스 존재 확인
|
|
2110
|
+
console.log(`\n ${YELLOW}⚠${RESET} hub 이미 실행 중 (PID ${info.pid}, ${info.url})\n`);
|
|
2111
|
+
return;
|
|
2112
|
+
} catch {
|
|
2113
|
+
// PID 파일 있지만 프로세스 없음 — 정리
|
|
2114
|
+
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
const portArg = args.indexOf("--port");
|
|
2119
|
+
const port = portArg !== -1 ? args[portArg + 1] : "27888";
|
|
2120
|
+
const serverPath = join(PKG_ROOT, "hub", "server.mjs");
|
|
2121
|
+
|
|
2122
|
+
if (!existsSync(serverPath)) {
|
|
2123
|
+
throw createCliError("hub/server.mjs 없음 — hub 모듈이 설치되지 않음", {
|
|
2124
|
+
exitCode: EXIT_HUB_ERROR,
|
|
2125
|
+
reason: "hubError",
|
|
2126
|
+
fix: "hub 모듈이 포함된 triflux 설치본인지 확인한 뒤 다시 실행하세요.",
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
2131
|
+
env: { ...process.env, TFX_HUB_PORT: port },
|
|
2132
|
+
stdio: "ignore",
|
|
2133
|
+
detached: true,
|
|
2134
|
+
windowsHide: true,
|
|
2135
|
+
});
|
|
2136
|
+
child.unref();
|
|
2137
|
+
|
|
2138
|
+
// PID 파일 확인 (최대 3초 대기, 100ms 폴링)
|
|
2139
|
+
let started = false;
|
|
2140
|
+
const deadline = Date.now() + 3000;
|
|
2141
|
+
while (Date.now() < deadline) {
|
|
2142
|
+
if (existsSync(HUB_PID_FILE)) { started = true; break; }
|
|
2143
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
if (started) {
|
|
2147
|
+
const hubInfo = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
2148
|
+
console.log(`\n ${GREEN_BRIGHT}✓${RESET} ${BOLD}tfx-hub 시작${RESET}`);
|
|
2149
|
+
console.log(` URL: ${AMBER}${hubInfo.url}${RESET}`);
|
|
2150
|
+
console.log(` PID: ${hubInfo.pid}`);
|
|
2151
|
+
console.log(` DB: ${DIM}${getPipelineStateDbPath(PKG_ROOT)}${RESET}`);
|
|
2152
|
+
console.log("");
|
|
2153
|
+
autoRegisterMcp(hubInfo.url);
|
|
2154
|
+
console.log("");
|
|
2155
|
+
} else {
|
|
2156
|
+
// 직접 포그라운드 모드로 안내
|
|
2157
|
+
console.log(`\n ${YELLOW}⚠${RESET} 백그라운드 시작 실패 — 포그라운드로 실행:`);
|
|
2158
|
+
console.log(` ${DIM}TFX_HUB_PORT=${port} node ${serverPath}${RESET}\n`);
|
|
2159
|
+
}
|
|
2160
|
+
break;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
case "stop": {
|
|
2164
|
+
if (!existsSync(HUB_PID_FILE)) {
|
|
2165
|
+
const probed = await probeHubStatus("127.0.0.1", probePort, 1500)
|
|
2166
|
+
|| (probePort === 27888 ? null : await probeHubStatus("127.0.0.1", 27888, 1500));
|
|
2167
|
+
if (probed && Number.isFinite(Number(probed.pid))) {
|
|
2168
|
+
try {
|
|
2169
|
+
process.kill(Number(probed.pid), "SIGTERM");
|
|
2170
|
+
console.log(`\n ${GREEN_BRIGHT}✓${RESET} hub 종료됨 (PID ${probed.pid})${DIM} (probe)${RESET}\n`);
|
|
2171
|
+
return;
|
|
2172
|
+
} catch {}
|
|
2173
|
+
}
|
|
2174
|
+
console.log(`\n ${DIM}hub 미실행${RESET}\n`);
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
try {
|
|
2178
|
+
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
2179
|
+
process.kill(info.pid, "SIGTERM");
|
|
2180
|
+
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
2181
|
+
console.log(`\n ${GREEN_BRIGHT}✓${RESET} hub 종료됨 (PID ${info.pid})\n`);
|
|
2182
|
+
} catch (e) {
|
|
2183
|
+
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
2184
|
+
console.log(`\n ${DIM}hub 프로세스 없음 — PID 파일 정리됨${RESET}\n`);
|
|
2185
|
+
}
|
|
2186
|
+
break;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
case "status": {
|
|
2190
|
+
if (!existsSync(HUB_PID_FILE)) {
|
|
2191
|
+
const probed = await probeHubStatus();
|
|
2192
|
+
if (!probed) {
|
|
2193
|
+
const fallback = probePort === 27888 ? null : await probeHubStatus("127.0.0.1", 27888, 1500);
|
|
2194
|
+
if (fallback) {
|
|
2195
|
+
recoverPidFile(fallback, "127.0.0.1");
|
|
2196
|
+
if (emitHubStatus({
|
|
2197
|
+
status: "online",
|
|
2198
|
+
source: "default-port-probe",
|
|
2199
|
+
url: `http://127.0.0.1:${fallback.port || 27888}/mcp`,
|
|
2200
|
+
pid: fallback.pid,
|
|
2201
|
+
state: fallback.hub?.state || null,
|
|
2202
|
+
sessions: fallback.sessions,
|
|
2203
|
+
})) return;
|
|
2204
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET} ${DIM}(default port probe 성공)${RESET}`);
|
|
2205
|
+
console.log(` URL: http://127.0.0.1:${fallback.port || 27888}/mcp`);
|
|
2206
|
+
if (fallback.pid !== undefined) console.log(` PID: ${fallback.pid}`);
|
|
2207
|
+
if (fallback.hub?.state) console.log(` State: ${fallback.hub.state}`);
|
|
2208
|
+
if (fallback.sessions !== undefined) console.log(` Sessions: ${fallback.sessions}`);
|
|
2209
|
+
console.log("");
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
if (emitHubStatus({ status: "offline", source: "probe", url: null, pid: null, state: null, sessions: 0 })) return;
|
|
2213
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${RED}offline${RESET}\n`);
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
recoverPidFile(probed, "127.0.0.1");
|
|
2217
|
+
if (emitHubStatus({
|
|
2218
|
+
status: "online",
|
|
2219
|
+
source: "probe",
|
|
2220
|
+
url: `http://127.0.0.1:${probed.port || probePort}/mcp`,
|
|
2221
|
+
pid: probed.pid,
|
|
2222
|
+
state: probed.hub?.state || null,
|
|
2223
|
+
sessions: probed.sessions,
|
|
2224
|
+
})) return;
|
|
2225
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET} ${DIM}(pid file 없음 / probe 성공)${RESET}`);
|
|
2226
|
+
console.log(` URL: http://127.0.0.1:${probed.port || probePort}/mcp`);
|
|
2227
|
+
if (probed.pid !== undefined) console.log(` PID: ${probed.pid}`);
|
|
2228
|
+
if (probed.hub?.state) console.log(` State: ${probed.hub.state}`);
|
|
2229
|
+
if (probed.sessions !== undefined) console.log(` Sessions: ${probed.sessions}`);
|
|
2230
|
+
console.log("");
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
try {
|
|
2234
|
+
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
2235
|
+
process.kill(info.pid, 0); // 생존 확인
|
|
2236
|
+
const uptime = Date.now() - info.started;
|
|
2237
|
+
const uptimeStr = uptime < 60000 ? `${Math.round(uptime / 1000)}초`
|
|
2238
|
+
: uptime < 3600000 ? `${Math.round(uptime / 60000)}분`
|
|
2239
|
+
: `${Math.round(uptime / 3600000)}시간`;
|
|
2240
|
+
|
|
2241
|
+
let data = null;
|
|
2242
|
+
try {
|
|
2243
|
+
const host = typeof info.host === "string" ? info.host : "127.0.0.1";
|
|
2244
|
+
const port = Number(info.port) || probePort;
|
|
2245
|
+
data = await probeHubStatus(host, port, 3000);
|
|
2246
|
+
} catch {}
|
|
2247
|
+
|
|
2248
|
+
if (emitHubStatus({
|
|
2249
|
+
status: "online",
|
|
2250
|
+
source: "pid-file",
|
|
2251
|
+
url: info.url,
|
|
2252
|
+
pid: info.pid,
|
|
2253
|
+
uptime_ms: uptime,
|
|
2254
|
+
state: data?.hub?.state || null,
|
|
2255
|
+
sessions: data?.sessions,
|
|
2256
|
+
})) return;
|
|
2257
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET}`);
|
|
2258
|
+
console.log(` URL: ${info.url}`);
|
|
2259
|
+
console.log(` PID: ${info.pid}`);
|
|
2260
|
+
console.log(` Uptime: ${uptimeStr}`);
|
|
2261
|
+
if (data?.hub) {
|
|
2262
|
+
console.log(` State: ${data.hub.state}`);
|
|
2263
|
+
}
|
|
2264
|
+
if (data?.sessions !== undefined) {
|
|
2265
|
+
console.log(` Sessions: ${data.sessions}`);
|
|
2266
|
+
}
|
|
2267
|
+
console.log("");
|
|
2268
|
+
} catch {
|
|
2269
|
+
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
2270
|
+
const probed = await probeHubStatus();
|
|
2271
|
+
if (!probed) {
|
|
2272
|
+
if (emitHubStatus({ status: "offline", source: "stale-pid", url: null, pid: null, state: null, sessions: 0 })) break;
|
|
2273
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${RED}offline${RESET} ${DIM}(stale PID 정리됨)${RESET}\n`);
|
|
2274
|
+
break;
|
|
2275
|
+
}
|
|
2276
|
+
recoverPidFile(probed, "127.0.0.1");
|
|
2277
|
+
if (emitHubStatus({
|
|
2278
|
+
status: "online",
|
|
2279
|
+
source: "stale-pid-probe",
|
|
2280
|
+
url: `http://127.0.0.1:${probed.port || probePort}/mcp`,
|
|
2281
|
+
pid: probed.pid,
|
|
2282
|
+
state: probed.hub?.state || null,
|
|
2283
|
+
sessions: probed.sessions,
|
|
2284
|
+
})) break;
|
|
2285
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET} ${DIM}(stale PID 정리 후 probe 성공)${RESET}`);
|
|
2286
|
+
console.log(` URL: http://127.0.0.1:${probed.port || probePort}/mcp`);
|
|
2287
|
+
if (probed.pid !== undefined) console.log(` PID: ${probed.pid}`);
|
|
2288
|
+
if (probed.hub?.state) console.log(` State: ${probed.hub.state}`);
|
|
2289
|
+
if (probed.sessions !== undefined) console.log(` Sessions: ${probed.sessions}`);
|
|
2290
|
+
console.log("");
|
|
2291
|
+
}
|
|
2292
|
+
break;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
default:
|
|
2296
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET}\n`);
|
|
2297
|
+
console.log(` ${WHITE_BRIGHT}tfx hub start${RESET} ${GRAY}허브 데몬 시작${RESET}`);
|
|
2298
|
+
console.log(` ${DIM} --port N${RESET} ${GRAY}포트 지정 (기본 27888)${RESET}`);
|
|
2299
|
+
console.log(` ${WHITE_BRIGHT}tfx hub stop${RESET} ${GRAY}허브 중지${RESET}`);
|
|
2300
|
+
console.log(` ${WHITE_BRIGHT}tfx hub status${RESET} ${GRAY}상태 확인${RESET}\n`);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
// ── 메인 ──
|
|
2305
|
+
|
|
2306
|
+
async function main() {
|
|
2307
|
+
const cmd = NORMALIZED_ARGS[0] || "help";
|
|
2308
|
+
const cmdArgs = NORMALIZED_ARGS.slice(1);
|
|
2309
|
+
|
|
2310
|
+
switch (cmd) {
|
|
2311
|
+
case "setup":
|
|
2312
|
+
cmdSetup({ dryRun: cmdArgs.includes("--dry-run") });
|
|
2313
|
+
return;
|
|
2314
|
+
case "doctor": {
|
|
2315
|
+
const fix = cmdArgs.includes("--fix");
|
|
2316
|
+
const reset = cmdArgs.includes("--reset");
|
|
2317
|
+
await cmdDoctor({ fix, reset, json: JSON_OUTPUT });
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
case "schema":
|
|
2321
|
+
cmdSchema(cmdArgs);
|
|
2322
|
+
return;
|
|
2323
|
+
case "update":
|
|
2324
|
+
cmdUpdate();
|
|
2325
|
+
return;
|
|
2326
|
+
case "list":
|
|
2327
|
+
case "ls":
|
|
2328
|
+
cmdList({ json: JSON_OUTPUT });
|
|
2329
|
+
return;
|
|
2330
|
+
case "hub":
|
|
2331
|
+
await cmdHub(cmdArgs, { json: JSON_OUTPUT && (cmdArgs[0] || "status") === "status" });
|
|
2332
|
+
return;
|
|
2333
|
+
case "tray": {
|
|
2334
|
+
const trayUrl = new URL("../hub/tray.mjs", import.meta.url);
|
|
2335
|
+
const trayPath = fileURLToPath(trayUrl);
|
|
2336
|
+
if (cmdArgs.includes("--detach")) {
|
|
2337
|
+
const child = spawn(process.execPath, [trayPath], {
|
|
2338
|
+
detached: true,
|
|
2339
|
+
stdio: "ignore",
|
|
2340
|
+
windowsHide: true,
|
|
2341
|
+
});
|
|
2342
|
+
child.unref();
|
|
2343
|
+
console.log(`\n ${GREEN_BRIGHT}✓${RESET} tray 시작됨 (PID ${child.pid})\n`);
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
const { startTray } = await import(trayUrl.href);
|
|
2347
|
+
await startTray();
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
case "multi": {
|
|
2351
|
+
const subcommand = cmdArgs[0] || "";
|
|
2352
|
+
if (JSON_OUTPUT) process.env.TFX_OUTPUT_JSON = "1";
|
|
2353
|
+
else delete process.env.TFX_OUTPUT_JSON;
|
|
2354
|
+
if (subcommand !== "status") {
|
|
2355
|
+
await checkHubRunning();
|
|
2356
|
+
}
|
|
2357
|
+
const { pathToFileURL } = await import("node:url");
|
|
2358
|
+
const { cmdTeam } = await import(pathToFileURL(join(PKG_ROOT, "hub", "team", "cli", "index.mjs")).href);
|
|
2359
|
+
const prevArgv = process.argv;
|
|
2360
|
+
process.argv = [prevArgv[0], prevArgv[1], "team", ...cmdArgs];
|
|
2361
|
+
try {
|
|
2362
|
+
await cmdTeam();
|
|
2363
|
+
} finally {
|
|
2364
|
+
process.argv = prevArgv;
|
|
2365
|
+
delete process.env.TFX_OUTPUT_JSON;
|
|
2366
|
+
}
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
case "codex-team":
|
|
2370
|
+
if (JSON_OUTPUT) process.env.TFX_OUTPUT_JSON = "1";
|
|
2371
|
+
else delete process.env.TFX_OUTPUT_JSON;
|
|
2372
|
+
await checkHubRunning();
|
|
2373
|
+
try {
|
|
2374
|
+
await cmdCodexTeam(cmdArgs);
|
|
2375
|
+
} finally {
|
|
2376
|
+
delete process.env.TFX_OUTPUT_JSON;
|
|
2377
|
+
}
|
|
2378
|
+
return;
|
|
2379
|
+
case "notion-read":
|
|
2380
|
+
case "nr": {
|
|
2381
|
+
const scriptPath = join(PKG_ROOT, "scripts", "notion-read.mjs");
|
|
2382
|
+
try {
|
|
2383
|
+
execFileSync(process.execPath, [scriptPath, ...cmdArgs], { stdio: "inherit", timeout: 660000 });
|
|
2384
|
+
} catch (e) {
|
|
2385
|
+
throw createCliError(e.message || "notion-read 실행 실패", {
|
|
2386
|
+
exitCode: e.status || EXIT_ERROR,
|
|
2387
|
+
reason: "error",
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2392
|
+
case "version":
|
|
2393
|
+
case "--version":
|
|
2394
|
+
case "-v":
|
|
2395
|
+
cmdVersion({ json: JSON_OUTPUT });
|
|
2396
|
+
return;
|
|
2397
|
+
case "help":
|
|
2398
|
+
case "--help":
|
|
2399
|
+
case "-h":
|
|
2400
|
+
cmdHelp();
|
|
2401
|
+
return;
|
|
2402
|
+
default:
|
|
2403
|
+
throw createCliError(`알 수 없는 명령: ${cmd}`, {
|
|
2404
|
+
exitCode: EXIT_ARG_ERROR,
|
|
2405
|
+
reason: "argError",
|
|
2406
|
+
fix: "tfx --help",
|
|
2407
|
+
});
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
try {
|
|
2412
|
+
await main();
|
|
2413
|
+
} catch (error) {
|
|
2414
|
+
handleFatalError(error, { json: JSON_OUTPUT });
|
|
2415
|
+
}
|