cc4pm 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/.claude-plugin/README.md +17 -0
  2. package/.claude-plugin/plugin.json +25 -0
  3. package/LICENSE +21 -0
  4. package/README.md +157 -0
  5. package/README.zh-CN.md +134 -0
  6. package/contexts/dev.md +20 -0
  7. package/contexts/research.md +26 -0
  8. package/contexts/review.md +22 -0
  9. package/examples/CLAUDE.md +100 -0
  10. package/examples/statusline.json +19 -0
  11. package/examples/user-CLAUDE.md +109 -0
  12. package/install.sh +17 -0
  13. package/manifests/install-components.json +173 -0
  14. package/manifests/install-modules.json +335 -0
  15. package/manifests/install-profiles.json +75 -0
  16. package/package.json +117 -0
  17. package/schemas/ecc-install-config.schema.json +58 -0
  18. package/schemas/hooks.schema.json +197 -0
  19. package/schemas/install-components.schema.json +56 -0
  20. package/schemas/install-modules.schema.json +105 -0
  21. package/schemas/install-profiles.schema.json +45 -0
  22. package/schemas/install-state.schema.json +210 -0
  23. package/schemas/package-manager.schema.json +23 -0
  24. package/schemas/plugin.schema.json +58 -0
  25. package/scripts/ci/catalog.js +83 -0
  26. package/scripts/ci/validate-agents.js +81 -0
  27. package/scripts/ci/validate-commands.js +135 -0
  28. package/scripts/ci/validate-hooks.js +239 -0
  29. package/scripts/ci/validate-install-manifests.js +211 -0
  30. package/scripts/ci/validate-no-personal-paths.js +63 -0
  31. package/scripts/ci/validate-rules.js +81 -0
  32. package/scripts/ci/validate-skills.js +54 -0
  33. package/scripts/claw.js +468 -0
  34. package/scripts/doctor.js +110 -0
  35. package/scripts/ecc.js +194 -0
  36. package/scripts/hooks/auto-tmux-dev.js +88 -0
  37. package/scripts/hooks/check-console-log.js +71 -0
  38. package/scripts/hooks/check-hook-enabled.js +12 -0
  39. package/scripts/hooks/cost-tracker.js +78 -0
  40. package/scripts/hooks/doc-file-warning.js +63 -0
  41. package/scripts/hooks/evaluate-session.js +100 -0
  42. package/scripts/hooks/insaits-security-monitor.py +269 -0
  43. package/scripts/hooks/insaits-security-wrapper.js +88 -0
  44. package/scripts/hooks/post-bash-build-complete.js +27 -0
  45. package/scripts/hooks/post-bash-pr-created.js +36 -0
  46. package/scripts/hooks/post-edit-console-warn.js +54 -0
  47. package/scripts/hooks/post-edit-format.js +109 -0
  48. package/scripts/hooks/post-edit-typecheck.js +96 -0
  49. package/scripts/hooks/pre-bash-dev-server-block.js +187 -0
  50. package/scripts/hooks/pre-bash-git-push-reminder.js +28 -0
  51. package/scripts/hooks/pre-bash-tmux-reminder.js +33 -0
  52. package/scripts/hooks/pre-compact.js +48 -0
  53. package/scripts/hooks/pre-write-doc-warn.js +9 -0
  54. package/scripts/hooks/quality-gate.js +168 -0
  55. package/scripts/hooks/run-with-flags-shell.sh +32 -0
  56. package/scripts/hooks/run-with-flags.js +120 -0
  57. package/scripts/hooks/session-end-marker.js +15 -0
  58. package/scripts/hooks/session-end.js +299 -0
  59. package/scripts/hooks/session-start.js +97 -0
  60. package/scripts/hooks/suggest-compact.js +80 -0
  61. package/scripts/install-apply.js +137 -0
  62. package/scripts/install-plan.js +254 -0
  63. package/scripts/lib/hook-flags.js +74 -0
  64. package/scripts/lib/install/apply.js +23 -0
  65. package/scripts/lib/install/config.js +82 -0
  66. package/scripts/lib/install/request.js +113 -0
  67. package/scripts/lib/install/runtime.js +42 -0
  68. package/scripts/lib/install-executor.js +605 -0
  69. package/scripts/lib/install-lifecycle.js +763 -0
  70. package/scripts/lib/install-manifests.js +305 -0
  71. package/scripts/lib/install-state.js +120 -0
  72. package/scripts/lib/install-targets/antigravity-project.js +9 -0
  73. package/scripts/lib/install-targets/claude-home.js +10 -0
  74. package/scripts/lib/install-targets/codex-home.js +10 -0
  75. package/scripts/lib/install-targets/cursor-project.js +10 -0
  76. package/scripts/lib/install-targets/helpers.js +89 -0
  77. package/scripts/lib/install-targets/opencode-home.js +10 -0
  78. package/scripts/lib/install-targets/registry.js +64 -0
  79. package/scripts/lib/orchestration-session.js +299 -0
  80. package/scripts/lib/package-manager.d.ts +119 -0
  81. package/scripts/lib/package-manager.js +431 -0
  82. package/scripts/lib/project-detect.js +428 -0
  83. package/scripts/lib/resolve-formatter.js +185 -0
  84. package/scripts/lib/session-adapters/canonical-session.js +138 -0
  85. package/scripts/lib/session-adapters/claude-history.js +149 -0
  86. package/scripts/lib/session-adapters/dmux-tmux.js +80 -0
  87. package/scripts/lib/session-adapters/registry.js +111 -0
  88. package/scripts/lib/session-aliases.d.ts +136 -0
  89. package/scripts/lib/session-aliases.js +481 -0
  90. package/scripts/lib/session-manager.d.ts +131 -0
  91. package/scripts/lib/session-manager.js +464 -0
  92. package/scripts/lib/shell-split.js +86 -0
  93. package/scripts/lib/skill-improvement/amendify.js +89 -0
  94. package/scripts/lib/skill-improvement/evaluate.js +59 -0
  95. package/scripts/lib/skill-improvement/health.js +118 -0
  96. package/scripts/lib/skill-improvement/observations.js +108 -0
  97. package/scripts/lib/tmux-worktree-orchestrator.js +491 -0
  98. package/scripts/lib/utils.d.ts +183 -0
  99. package/scripts/lib/utils.js +543 -0
  100. package/scripts/list-installed.js +90 -0
  101. package/scripts/orchestrate-codex-worker.sh +92 -0
  102. package/scripts/orchestrate-worktrees.js +108 -0
  103. package/scripts/orchestration-status.js +62 -0
  104. package/scripts/repair.js +97 -0
  105. package/scripts/session-inspect.js +150 -0
  106. package/scripts/setup-package-manager.js +204 -0
  107. package/scripts/skill-create-output.js +244 -0
  108. package/scripts/uninstall.js +96 -0
@@ -0,0 +1,763 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const { resolveInstallPlan, loadInstallManifests } = require('./install-manifests');
5
+ const { readInstallState, writeInstallState } = require('./install-state');
6
+ const {
7
+ applyInstallPlan,
8
+ createLegacyInstallPlan,
9
+ createManifestInstallPlan,
10
+ } = require('./install-executor');
11
+ const {
12
+ getInstallTargetAdapter,
13
+ listInstallTargetAdapters,
14
+ } = require('./install-targets/registry');
15
+
16
+ const DEFAULT_REPO_ROOT = path.join(__dirname, '../..');
17
+
18
+ function readPackageVersion(repoRoot) {
19
+ try {
20
+ const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
21
+ return packageJson.version || null;
22
+ } catch (_error) {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ function normalizeTargets(targets) {
28
+ if (!Array.isArray(targets) || targets.length === 0) {
29
+ return listInstallTargetAdapters().map(adapter => adapter.target);
30
+ }
31
+
32
+ const normalizedTargets = [];
33
+ for (const target of targets) {
34
+ const adapter = getInstallTargetAdapter(target);
35
+ if (!normalizedTargets.includes(adapter.target)) {
36
+ normalizedTargets.push(adapter.target);
37
+ }
38
+ }
39
+
40
+ return normalizedTargets;
41
+ }
42
+
43
+ function compareStringArrays(left, right) {
44
+ const leftValues = Array.isArray(left) ? left : [];
45
+ const rightValues = Array.isArray(right) ? right : [];
46
+
47
+ if (leftValues.length !== rightValues.length) {
48
+ return false;
49
+ }
50
+
51
+ return leftValues.every((value, index) => value === rightValues[index]);
52
+ }
53
+
54
+ function getManagedOperations(state) {
55
+ return Array.isArray(state && state.operations)
56
+ ? state.operations.filter(operation => operation.ownership === 'managed')
57
+ : [];
58
+ }
59
+
60
+ function resolveOperationSourcePath(repoRoot, operation) {
61
+ if (operation.sourceRelativePath) {
62
+ return path.join(repoRoot, operation.sourceRelativePath);
63
+ }
64
+
65
+ return operation.sourcePath || null;
66
+ }
67
+
68
+ function areFilesEqual(leftPath, rightPath) {
69
+ try {
70
+ const leftStat = fs.statSync(leftPath);
71
+ const rightStat = fs.statSync(rightPath);
72
+ if (!leftStat.isFile() || !rightStat.isFile()) {
73
+ return false;
74
+ }
75
+
76
+ return fs.readFileSync(leftPath).equals(fs.readFileSync(rightPath));
77
+ } catch (_error) {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ function inspectManagedOperation(repoRoot, operation) {
83
+ const destinationPath = operation.destinationPath;
84
+ if (!destinationPath) {
85
+ return {
86
+ status: 'invalid-destination',
87
+ operation,
88
+ };
89
+ }
90
+
91
+ if (!fs.existsSync(destinationPath)) {
92
+ return {
93
+ status: 'missing',
94
+ operation,
95
+ destinationPath,
96
+ };
97
+ }
98
+
99
+ if (operation.kind !== 'copy-file') {
100
+ return {
101
+ status: 'unverified',
102
+ operation,
103
+ destinationPath,
104
+ };
105
+ }
106
+
107
+ const sourcePath = resolveOperationSourcePath(repoRoot, operation);
108
+ if (!sourcePath || !fs.existsSync(sourcePath)) {
109
+ return {
110
+ status: 'missing-source',
111
+ operation,
112
+ destinationPath,
113
+ sourcePath,
114
+ };
115
+ }
116
+
117
+ if (!areFilesEqual(sourcePath, destinationPath)) {
118
+ return {
119
+ status: 'drifted',
120
+ operation,
121
+ destinationPath,
122
+ sourcePath,
123
+ };
124
+ }
125
+
126
+ return {
127
+ status: 'ok',
128
+ operation,
129
+ destinationPath,
130
+ sourcePath,
131
+ };
132
+ }
133
+
134
+ function summarizeManagedOperationHealth(repoRoot, operations) {
135
+ return operations.reduce((summary, operation) => {
136
+ const inspection = inspectManagedOperation(repoRoot, operation);
137
+ if (inspection.status === 'missing') {
138
+ summary.missing.push(inspection);
139
+ } else if (inspection.status === 'drifted') {
140
+ summary.drifted.push(inspection);
141
+ } else if (inspection.status === 'missing-source') {
142
+ summary.missingSource.push(inspection);
143
+ } else if (inspection.status === 'unverified' || inspection.status === 'invalid-destination') {
144
+ summary.unverified.push(inspection);
145
+ }
146
+ return summary;
147
+ }, {
148
+ missing: [],
149
+ drifted: [],
150
+ missingSource: [],
151
+ unverified: [],
152
+ });
153
+ }
154
+
155
+ function buildDiscoveryRecord(adapter, context) {
156
+ const installTargetInput = {
157
+ homeDir: context.homeDir,
158
+ projectRoot: context.projectRoot,
159
+ repoRoot: context.projectRoot,
160
+ };
161
+ const targetRoot = adapter.resolveRoot(installTargetInput);
162
+ const installStatePath = adapter.getInstallStatePath(installTargetInput);
163
+ const exists = fs.existsSync(installStatePath);
164
+
165
+ if (!exists) {
166
+ return {
167
+ adapter: {
168
+ id: adapter.id,
169
+ target: adapter.target,
170
+ kind: adapter.kind,
171
+ },
172
+ targetRoot,
173
+ installStatePath,
174
+ exists: false,
175
+ state: null,
176
+ error: null,
177
+ };
178
+ }
179
+
180
+ try {
181
+ const state = readInstallState(installStatePath);
182
+ return {
183
+ adapter: {
184
+ id: adapter.id,
185
+ target: adapter.target,
186
+ kind: adapter.kind,
187
+ },
188
+ targetRoot,
189
+ installStatePath,
190
+ exists: true,
191
+ state,
192
+ error: null,
193
+ };
194
+ } catch (error) {
195
+ return {
196
+ adapter: {
197
+ id: adapter.id,
198
+ target: adapter.target,
199
+ kind: adapter.kind,
200
+ },
201
+ targetRoot,
202
+ installStatePath,
203
+ exists: true,
204
+ state: null,
205
+ error: error.message,
206
+ };
207
+ }
208
+ }
209
+
210
+ function discoverInstalledStates(options = {}) {
211
+ const context = {
212
+ homeDir: options.homeDir || process.env.HOME,
213
+ projectRoot: options.projectRoot || process.cwd(),
214
+ };
215
+ const targets = normalizeTargets(options.targets);
216
+
217
+ return targets.map(target => {
218
+ const adapter = getInstallTargetAdapter(target);
219
+ return buildDiscoveryRecord(adapter, context);
220
+ });
221
+ }
222
+
223
+ function buildIssue(severity, code, message, extra = {}) {
224
+ return {
225
+ severity,
226
+ code,
227
+ message,
228
+ ...extra,
229
+ };
230
+ }
231
+
232
+ function determineStatus(issues) {
233
+ if (issues.some(issue => issue.severity === 'error')) {
234
+ return 'error';
235
+ }
236
+
237
+ if (issues.some(issue => issue.severity === 'warning')) {
238
+ return 'warning';
239
+ }
240
+
241
+ return 'ok';
242
+ }
243
+
244
+ function analyzeRecord(record, context) {
245
+ const issues = [];
246
+
247
+ if (record.error) {
248
+ issues.push(buildIssue('error', 'invalid-install-state', record.error));
249
+ return {
250
+ ...record,
251
+ status: determineStatus(issues),
252
+ issues,
253
+ };
254
+ }
255
+
256
+ const state = record.state;
257
+ if (!state) {
258
+ return {
259
+ ...record,
260
+ status: 'missing',
261
+ issues,
262
+ };
263
+ }
264
+
265
+ if (!fs.existsSync(state.target.root)) {
266
+ issues.push(buildIssue(
267
+ 'error',
268
+ 'missing-target-root',
269
+ `Target root does not exist: ${state.target.root}`
270
+ ));
271
+ }
272
+
273
+ if (state.target.root !== record.targetRoot) {
274
+ issues.push(buildIssue(
275
+ 'warning',
276
+ 'target-root-mismatch',
277
+ `Recorded target root differs from current target root (${record.targetRoot})`,
278
+ {
279
+ recordedTargetRoot: state.target.root,
280
+ currentTargetRoot: record.targetRoot,
281
+ }
282
+ ));
283
+ }
284
+
285
+ if (state.target.installStatePath !== record.installStatePath) {
286
+ issues.push(buildIssue(
287
+ 'warning',
288
+ 'install-state-path-mismatch',
289
+ `Recorded install-state path differs from current path (${record.installStatePath})`,
290
+ {
291
+ recordedInstallStatePath: state.target.installStatePath,
292
+ currentInstallStatePath: record.installStatePath,
293
+ }
294
+ ));
295
+ }
296
+
297
+ const managedOperations = getManagedOperations(state);
298
+ const operationHealth = summarizeManagedOperationHealth(context.repoRoot, managedOperations);
299
+ const missingManagedOperations = operationHealth.missing;
300
+
301
+ if (missingManagedOperations.length > 0) {
302
+ issues.push(buildIssue(
303
+ 'error',
304
+ 'missing-managed-files',
305
+ `${missingManagedOperations.length} managed file(s) are missing`,
306
+ {
307
+ paths: missingManagedOperations.map(entry => entry.destinationPath),
308
+ }
309
+ ));
310
+ }
311
+
312
+ if (operationHealth.drifted.length > 0) {
313
+ issues.push(buildIssue(
314
+ 'warning',
315
+ 'drifted-managed-files',
316
+ `${operationHealth.drifted.length} managed file(s) differ from the source repo`,
317
+ {
318
+ paths: operationHealth.drifted.map(entry => entry.destinationPath),
319
+ }
320
+ ));
321
+ }
322
+
323
+ if (operationHealth.missingSource.length > 0) {
324
+ issues.push(buildIssue(
325
+ 'error',
326
+ 'missing-source-files',
327
+ `${operationHealth.missingSource.length} source file(s) referenced by install-state are missing`,
328
+ {
329
+ paths: operationHealth.missingSource.map(entry => entry.sourcePath).filter(Boolean),
330
+ }
331
+ ));
332
+ }
333
+
334
+ if (operationHealth.unverified.length > 0) {
335
+ issues.push(buildIssue(
336
+ 'warning',
337
+ 'unverified-managed-operations',
338
+ `${operationHealth.unverified.length} managed operation(s) could not be content-verified`,
339
+ {
340
+ paths: operationHealth.unverified.map(entry => entry.destinationPath).filter(Boolean),
341
+ }
342
+ ));
343
+ }
344
+
345
+ if (state.source.manifestVersion !== context.manifestVersion) {
346
+ issues.push(buildIssue(
347
+ 'warning',
348
+ 'manifest-version-mismatch',
349
+ `Recorded manifest version ${state.source.manifestVersion} differs from current manifest version ${context.manifestVersion}`
350
+ ));
351
+ }
352
+
353
+ if (
354
+ context.packageVersion
355
+ && state.source.repoVersion
356
+ && state.source.repoVersion !== context.packageVersion
357
+ ) {
358
+ issues.push(buildIssue(
359
+ 'warning',
360
+ 'repo-version-mismatch',
361
+ `Recorded repo version ${state.source.repoVersion} differs from current repo version ${context.packageVersion}`
362
+ ));
363
+ }
364
+
365
+ if (!state.request.legacyMode) {
366
+ try {
367
+ const desiredPlan = resolveInstallPlan({
368
+ repoRoot: context.repoRoot,
369
+ projectRoot: context.projectRoot,
370
+ homeDir: context.homeDir,
371
+ target: record.adapter.target,
372
+ profileId: state.request.profile || null,
373
+ moduleIds: state.request.modules || [],
374
+ includeComponentIds: state.request.includeComponents || [],
375
+ excludeComponentIds: state.request.excludeComponents || [],
376
+ });
377
+
378
+ if (
379
+ !compareStringArrays(desiredPlan.selectedModuleIds, state.resolution.selectedModules)
380
+ || !compareStringArrays(desiredPlan.skippedModuleIds, state.resolution.skippedModules)
381
+ ) {
382
+ issues.push(buildIssue(
383
+ 'warning',
384
+ 'resolution-drift',
385
+ 'Current manifest resolution differs from recorded install-state',
386
+ {
387
+ expectedSelectedModules: desiredPlan.selectedModuleIds,
388
+ recordedSelectedModules: state.resolution.selectedModules,
389
+ expectedSkippedModules: desiredPlan.skippedModuleIds,
390
+ recordedSkippedModules: state.resolution.skippedModules,
391
+ }
392
+ ));
393
+ }
394
+ } catch (error) {
395
+ issues.push(buildIssue(
396
+ 'error',
397
+ 'resolution-unavailable',
398
+ error.message
399
+ ));
400
+ }
401
+ }
402
+
403
+ return {
404
+ ...record,
405
+ status: determineStatus(issues),
406
+ issues,
407
+ };
408
+ }
409
+
410
+ function buildDoctorReport(options = {}) {
411
+ const repoRoot = options.repoRoot || DEFAULT_REPO_ROOT;
412
+ const manifests = loadInstallManifests({ repoRoot });
413
+ const records = discoverInstalledStates({
414
+ homeDir: options.homeDir,
415
+ projectRoot: options.projectRoot,
416
+ targets: options.targets,
417
+ }).filter(record => record.exists);
418
+ const context = {
419
+ repoRoot,
420
+ homeDir: options.homeDir || process.env.HOME,
421
+ projectRoot: options.projectRoot || process.cwd(),
422
+ manifestVersion: manifests.modulesVersion,
423
+ packageVersion: readPackageVersion(repoRoot),
424
+ };
425
+ const results = records.map(record => analyzeRecord(record, context));
426
+ const summary = results.reduce((accumulator, result) => {
427
+ const errorCount = result.issues.filter(issue => issue.severity === 'error').length;
428
+ const warningCount = result.issues.filter(issue => issue.severity === 'warning').length;
429
+
430
+ return {
431
+ checkedCount: accumulator.checkedCount + 1,
432
+ okCount: accumulator.okCount + (result.status === 'ok' ? 1 : 0),
433
+ errorCount: accumulator.errorCount + errorCount,
434
+ warningCount: accumulator.warningCount + warningCount,
435
+ };
436
+ }, {
437
+ checkedCount: 0,
438
+ okCount: 0,
439
+ errorCount: 0,
440
+ warningCount: 0,
441
+ });
442
+
443
+ return {
444
+ generatedAt: new Date().toISOString(),
445
+ packageVersion: context.packageVersion,
446
+ manifestVersion: context.manifestVersion,
447
+ results,
448
+ summary,
449
+ };
450
+ }
451
+
452
+ function createRepairPlanFromRecord(record, context) {
453
+ const state = record.state;
454
+ if (!state) {
455
+ throw new Error('No install-state available for repair');
456
+ }
457
+
458
+ if (state.request.legacyMode) {
459
+ const operations = getManagedOperations(state).map(operation => ({
460
+ ...operation,
461
+ sourcePath: resolveOperationSourcePath(context.repoRoot, operation),
462
+ }));
463
+
464
+ const statePreview = {
465
+ ...state,
466
+ operations: operations.map(operation => ({ ...operation })),
467
+ source: {
468
+ ...state.source,
469
+ repoVersion: context.packageVersion,
470
+ manifestVersion: context.manifestVersion,
471
+ },
472
+ lastValidatedAt: new Date().toISOString(),
473
+ };
474
+
475
+ return {
476
+ mode: 'legacy',
477
+ target: record.adapter.target,
478
+ adapter: record.adapter,
479
+ targetRoot: state.target.root,
480
+ installRoot: state.target.root,
481
+ installStatePath: state.target.installStatePath,
482
+ warnings: [],
483
+ languages: Array.isArray(state.request.legacyLanguages)
484
+ ? [...state.request.legacyLanguages]
485
+ : [],
486
+ operations,
487
+ statePreview,
488
+ };
489
+ }
490
+
491
+ const desiredPlan = createManifestInstallPlan({
492
+ sourceRoot: context.repoRoot,
493
+ target: record.adapter.target,
494
+ profileId: state.request.profile || null,
495
+ moduleIds: state.request.modules || [],
496
+ includeComponentIds: state.request.includeComponents || [],
497
+ excludeComponentIds: state.request.excludeComponents || [],
498
+ projectRoot: context.projectRoot,
499
+ homeDir: context.homeDir,
500
+ });
501
+
502
+ return {
503
+ ...desiredPlan,
504
+ statePreview: {
505
+ ...desiredPlan.statePreview,
506
+ installedAt: state.installedAt,
507
+ lastValidatedAt: new Date().toISOString(),
508
+ },
509
+ };
510
+ }
511
+
512
+ function repairInstalledStates(options = {}) {
513
+ const repoRoot = options.repoRoot || DEFAULT_REPO_ROOT;
514
+ const manifests = loadInstallManifests({ repoRoot });
515
+ const context = {
516
+ repoRoot,
517
+ homeDir: options.homeDir || process.env.HOME,
518
+ projectRoot: options.projectRoot || process.cwd(),
519
+ manifestVersion: manifests.modulesVersion,
520
+ packageVersion: readPackageVersion(repoRoot),
521
+ };
522
+ const records = discoverInstalledStates({
523
+ homeDir: context.homeDir,
524
+ projectRoot: context.projectRoot,
525
+ targets: options.targets,
526
+ }).filter(record => record.exists);
527
+
528
+ const results = records.map(record => {
529
+ if (record.error) {
530
+ return {
531
+ adapter: record.adapter,
532
+ status: 'error',
533
+ installStatePath: record.installStatePath,
534
+ repairedPaths: [],
535
+ plannedRepairs: [],
536
+ error: record.error,
537
+ };
538
+ }
539
+
540
+ try {
541
+ const desiredPlan = createRepairPlanFromRecord(record, context);
542
+ const operationHealth = summarizeManagedOperationHealth(context.repoRoot, desiredPlan.operations);
543
+
544
+ if (operationHealth.missingSource.length > 0) {
545
+ return {
546
+ adapter: record.adapter,
547
+ status: 'error',
548
+ installStatePath: record.installStatePath,
549
+ repairedPaths: [],
550
+ plannedRepairs: [],
551
+ error: `Missing source file(s): ${operationHealth.missingSource.map(entry => entry.sourcePath).join(', ')}`,
552
+ };
553
+ }
554
+
555
+ const repairOperations = [
556
+ ...operationHealth.missing.map(entry => ({ ...entry.operation })),
557
+ ...operationHealth.drifted.map(entry => ({ ...entry.operation })),
558
+ ];
559
+ const plannedRepairs = repairOperations.map(operation => operation.destinationPath);
560
+
561
+ if (options.dryRun) {
562
+ return {
563
+ adapter: record.adapter,
564
+ status: plannedRepairs.length > 0 ? 'planned' : 'ok',
565
+ installStatePath: record.installStatePath,
566
+ repairedPaths: [],
567
+ plannedRepairs,
568
+ stateRefreshed: plannedRepairs.length === 0,
569
+ error: null,
570
+ };
571
+ }
572
+
573
+ if (repairOperations.length > 0) {
574
+ applyInstallPlan({
575
+ ...desiredPlan,
576
+ operations: repairOperations,
577
+ statePreview: desiredPlan.statePreview,
578
+ });
579
+ } else {
580
+ writeInstallState(desiredPlan.installStatePath, desiredPlan.statePreview);
581
+ }
582
+
583
+ return {
584
+ adapter: record.adapter,
585
+ status: repairOperations.length > 0 ? 'repaired' : 'ok',
586
+ installStatePath: record.installStatePath,
587
+ repairedPaths: plannedRepairs,
588
+ plannedRepairs: [],
589
+ stateRefreshed: true,
590
+ error: null,
591
+ };
592
+ } catch (error) {
593
+ return {
594
+ adapter: record.adapter,
595
+ status: 'error',
596
+ installStatePath: record.installStatePath,
597
+ repairedPaths: [],
598
+ plannedRepairs: [],
599
+ error: error.message,
600
+ };
601
+ }
602
+ });
603
+
604
+ const summary = results.reduce((accumulator, result) => ({
605
+ checkedCount: accumulator.checkedCount + 1,
606
+ repairedCount: accumulator.repairedCount + (result.status === 'repaired' ? 1 : 0),
607
+ plannedRepairCount: accumulator.plannedRepairCount + (result.status === 'planned' ? 1 : 0),
608
+ errorCount: accumulator.errorCount + (result.status === 'error' ? 1 : 0),
609
+ }), {
610
+ checkedCount: 0,
611
+ repairedCount: 0,
612
+ plannedRepairCount: 0,
613
+ errorCount: 0,
614
+ });
615
+
616
+ return {
617
+ dryRun: Boolean(options.dryRun),
618
+ generatedAt: new Date().toISOString(),
619
+ results,
620
+ summary,
621
+ };
622
+ }
623
+
624
+ function cleanupEmptyParentDirs(filePath, stopAt) {
625
+ let currentPath = path.dirname(filePath);
626
+ const normalizedStopAt = path.resolve(stopAt);
627
+
628
+ while (
629
+ currentPath
630
+ && path.resolve(currentPath).startsWith(normalizedStopAt)
631
+ && path.resolve(currentPath) !== normalizedStopAt
632
+ ) {
633
+ if (!fs.existsSync(currentPath)) {
634
+ currentPath = path.dirname(currentPath);
635
+ continue;
636
+ }
637
+
638
+ const stat = fs.lstatSync(currentPath);
639
+ if (!stat.isDirectory() || fs.readdirSync(currentPath).length > 0) {
640
+ break;
641
+ }
642
+
643
+ fs.rmdirSync(currentPath);
644
+ currentPath = path.dirname(currentPath);
645
+ }
646
+ }
647
+
648
+ function uninstallInstalledStates(options = {}) {
649
+ const records = discoverInstalledStates({
650
+ homeDir: options.homeDir,
651
+ projectRoot: options.projectRoot,
652
+ targets: options.targets,
653
+ }).filter(record => record.exists);
654
+
655
+ const results = records.map(record => {
656
+ if (record.error || !record.state) {
657
+ return {
658
+ adapter: record.adapter,
659
+ status: 'error',
660
+ installStatePath: record.installStatePath,
661
+ removedPaths: [],
662
+ plannedRemovals: [],
663
+ error: record.error || 'No valid install-state available',
664
+ };
665
+ }
666
+
667
+ const state = record.state;
668
+ const plannedRemovals = Array.from(new Set([
669
+ ...getManagedOperations(state).map(operation => operation.destinationPath),
670
+ state.target.installStatePath,
671
+ ]));
672
+
673
+ if (options.dryRun) {
674
+ return {
675
+ adapter: record.adapter,
676
+ status: 'planned',
677
+ installStatePath: record.installStatePath,
678
+ removedPaths: [],
679
+ plannedRemovals,
680
+ error: null,
681
+ };
682
+ }
683
+
684
+ try {
685
+ const removedPaths = [];
686
+ const cleanupTargets = [];
687
+ const filePaths = Array.from(new Set(
688
+ getManagedOperations(state).map(operation => operation.destinationPath)
689
+ )).sort((left, right) => right.length - left.length);
690
+
691
+ for (const filePath of filePaths) {
692
+ if (!fs.existsSync(filePath)) {
693
+ continue;
694
+ }
695
+
696
+ const stat = fs.lstatSync(filePath);
697
+ if (stat.isDirectory()) {
698
+ throw new Error(`Refusing to remove managed directory path without explicit support: ${filePath}`);
699
+ }
700
+
701
+ fs.rmSync(filePath, { force: true });
702
+ removedPaths.push(filePath);
703
+ cleanupTargets.push(filePath);
704
+ }
705
+
706
+ if (fs.existsSync(state.target.installStatePath)) {
707
+ fs.rmSync(state.target.installStatePath, { force: true });
708
+ removedPaths.push(state.target.installStatePath);
709
+ cleanupTargets.push(state.target.installStatePath);
710
+ }
711
+
712
+ for (const cleanupTarget of cleanupTargets) {
713
+ cleanupEmptyParentDirs(cleanupTarget, state.target.root);
714
+ }
715
+
716
+ return {
717
+ adapter: record.adapter,
718
+ status: 'uninstalled',
719
+ installStatePath: record.installStatePath,
720
+ removedPaths,
721
+ plannedRemovals: [],
722
+ error: null,
723
+ };
724
+ } catch (error) {
725
+ return {
726
+ adapter: record.adapter,
727
+ status: 'error',
728
+ installStatePath: record.installStatePath,
729
+ removedPaths: [],
730
+ plannedRemovals,
731
+ error: error.message,
732
+ };
733
+ }
734
+ });
735
+
736
+ const summary = results.reduce((accumulator, result) => ({
737
+ checkedCount: accumulator.checkedCount + 1,
738
+ uninstalledCount: accumulator.uninstalledCount + (result.status === 'uninstalled' ? 1 : 0),
739
+ plannedRemovalCount: accumulator.plannedRemovalCount + (result.status === 'planned' ? 1 : 0),
740
+ errorCount: accumulator.errorCount + (result.status === 'error' ? 1 : 0),
741
+ }), {
742
+ checkedCount: 0,
743
+ uninstalledCount: 0,
744
+ plannedRemovalCount: 0,
745
+ errorCount: 0,
746
+ });
747
+
748
+ return {
749
+ dryRun: Boolean(options.dryRun),
750
+ generatedAt: new Date().toISOString(),
751
+ results,
752
+ summary,
753
+ };
754
+ }
755
+
756
+ module.exports = {
757
+ DEFAULT_REPO_ROOT,
758
+ buildDoctorReport,
759
+ discoverInstalledStates,
760
+ normalizeTargets,
761
+ repairInstalledStates,
762
+ uninstallInstalledStates,
763
+ };