@ai-driven-dev/cli 3.1.0 → 3.1.1

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 (2) hide show
  1. package/dist/cli.js +1035 -710
  2. package/package.json +4 -1
package/dist/cli.js CHANGED
@@ -1986,7 +1986,7 @@ var CliUpdaterAdapter = class {
1986
1986
  // package.json
1987
1987
  var package_default = {
1988
1988
  name: "@ai-driven-dev/cli",
1989
- version: "3.1.0",
1989
+ version: "3.1.1",
1990
1990
  description: "AI-Driven Development CLI \u2014 distribute the AIDD framework across AI coding assistants",
1991
1991
  type: "module",
1992
1992
  main: "dist/cli.js",
@@ -2005,6 +2005,9 @@ var package_default = {
2005
2005
  build: "tsup",
2006
2006
  dev: "tsup --watch",
2007
2007
  test: "pnpm build && vitest run",
2008
+ "test:unit": "vitest run --project=unit",
2009
+ "test:integration": "vitest run --project=integration",
2010
+ "test:e2e": "pnpm build && vitest run --project=e2e",
2008
2011
  "test:watch": "vitest",
2009
2012
  typecheck: "tsc --noEmit",
2010
2013
  lint: "biome check .",
@@ -2142,7 +2145,7 @@ var FileSystemAdapter = class {
2142
2145
  let existing = {};
2143
2146
  try {
2144
2147
  const raw = await readFile3(path, "utf-8");
2145
- existing = JSON.parse(raw);
2148
+ existing = JSON.parse(stripJsoncComments(raw));
2146
2149
  } catch (err) {
2147
2150
  const code = err.code;
2148
2151
  if (code !== "ENOENT") {
@@ -2208,6 +2211,14 @@ function stripJsoncComments(content) {
2208
2211
  i += 2;
2209
2212
  continue;
2210
2213
  }
2214
+ if (ch === ",") {
2215
+ let j = i + 1;
2216
+ while (j < content.length && " \n\r".includes(content[j])) j++;
2217
+ if (content[j] === "}" || content[j] === "]") {
2218
+ i++;
2219
+ continue;
2220
+ }
2221
+ }
2211
2222
  result += ch;
2212
2223
  i++;
2213
2224
  }
@@ -3619,6 +3630,29 @@ ${invocation} ${SCRIPT_RELATIVE_PATH}
3619
3630
  }
3620
3631
  };
3621
3632
 
3633
+ // src/application/use-cases/shared/post-install-pipeline-use-case.ts
3634
+ var PostInstallPipelineUseCase = class {
3635
+ constructor(fs, manifestRepo, hasher, git) {
3636
+ this.fs = fs;
3637
+ this.manifestRepo = manifestRepo;
3638
+ this.hasher = hasher;
3639
+ this.git = git;
3640
+ }
3641
+ async execute(options) {
3642
+ const { projectRoot, version, descriptor, contentFiles, manifest, docsDir } = options;
3643
+ await new MemoryScriptUseCase(this.fs, this.hasher, this.git).execute({
3644
+ projectRoot,
3645
+ version,
3646
+ descriptor,
3647
+ contentFiles,
3648
+ manifest
3649
+ });
3650
+ await this.manifestRepo.save(manifest);
3651
+ await new CatalogUseCase(this.fs).execute({ manifest, docsDir, projectRoot });
3652
+ await new GitignoreUseCase(this.fs).execute(projectRoot, [".aidd/cache/"]);
3653
+ }
3654
+ };
3655
+
3622
3656
  // src/application/use-cases/install-use-case.ts
3623
3657
  var InstallUseCase = class {
3624
3658
  constructor(fs, manifestRepo, loader, hasher, logger, git, platform3, prompter) {
@@ -3633,100 +3667,115 @@ var InstallUseCase = class {
3633
3667
  }
3634
3668
  async execute(options) {
3635
3669
  const { frameworkPath, version, projectRoot, force = false, repo } = options;
3636
- const interactive = options.interactive ?? false;
3637
3670
  const manifest = await this.manifestRepo.load();
3638
- if (manifest === null) {
3639
- throw new NoManifestError(repo);
3640
- }
3671
+ if (manifest === null) throw new NoManifestError(repo);
3641
3672
  const docsDir = options.docsDir ?? manifest.docsDir;
3642
- let toolIds;
3643
- if (options.all) {
3644
- toolIds = [...VALID_TOOL_IDS];
3645
- } else if (options.toolIds !== void 0 && options.toolIds.length > 0) {
3646
- toolIds = options.toolIds;
3647
- } else if (interactive && this.prompter !== void 0) {
3648
- const installedIds = manifest.getInstalledToolIds();
3649
- const choices = VALID_TOOL_IDS.map(
3650
- (id) => installedIds.includes(id) ? { name: id, value: id, checked: true, disabled: "(already installed)" } : { name: id, value: id, checked: false }
3651
- );
3652
- const selected = await this.prompter.checkbox("Which tools do you want to install?", choices);
3653
- if (selected.length === 0) throw new Error("No tools selected.");
3654
- toolIds = selected;
3655
- } else {
3656
- throw new Error(
3657
- `At least one tool ID is required. Valid tools: ${VALID_TOOL_IDS.join(", ")}`
3658
- );
3659
- }
3673
+ const toolIds = await this.resolveToolIds(options, manifest);
3660
3674
  assertValidToolIds(toolIds);
3661
3675
  const { descriptor, contentFiles } = await this.loader.loadFromDirectory(
3662
3676
  frameworkPath,
3663
3677
  version
3664
3678
  );
3679
+ const results = await this.installAllTools(
3680
+ toolIds,
3681
+ manifest,
3682
+ descriptor,
3683
+ contentFiles,
3684
+ docsDir,
3685
+ projectRoot,
3686
+ force
3687
+ );
3688
+ await new PostInstallPipelineUseCase(this.fs, this.manifestRepo, this.hasher, this.git).execute(
3689
+ { projectRoot, version, descriptor, contentFiles, manifest, docsDir }
3690
+ );
3691
+ return results;
3692
+ }
3693
+ async installAllTools(toolIds, manifest, descriptor, contentFiles, docsDir, projectRoot, force) {
3665
3694
  const results = [];
3666
3695
  for (const toolId of toolIds) {
3667
- if (manifest.hasTool(toolId) && !force) {
3668
- results.push({ toolId, fileCount: 0, files: [], skipped: true, warnings: [] });
3669
- continue;
3670
- }
3671
- const config = getToolConfig(toolId);
3672
- const warnings = [];
3673
- if (!manifest.hasTool(toolId) && force) {
3674
- const toolDir = join19(projectRoot, config.directory);
3675
- if (await this.fs.fileExists(toolDir)) {
3676
- warnings.push(
3677
- `Directory ${config.directory} exists but tool is not in manifest. Files will be overwritten.`
3678
- );
3679
- }
3680
- }
3681
- this.logger.info(`Generating ${toolId} distribution...`);
3682
- const generated = await generateDistribution(
3696
+ const result = await this.installOneTool(
3697
+ toolId,
3698
+ manifest,
3683
3699
  descriptor,
3684
- config,
3685
- docsDir,
3686
3700
  contentFiles,
3687
- this.hasher,
3688
- this.platform,
3701
+ docsDir,
3689
3702
  projectRoot,
3690
- this.fs
3703
+ force
3691
3704
  );
3692
- if (manifest.hasTool(toolId)) {
3693
- const newPaths = new Set(generated.map((f) => f.relativePath));
3694
- for (const oldFile of manifest.getToolFiles(toolId)) {
3695
- if (!newPaths.has(oldFile.relativePath)) {
3696
- await this.fs.deleteFile(join19(projectRoot, oldFile.relativePath));
3697
- }
3698
- }
3699
- }
3700
- const { files: finalFiles, userFileConflicts } = await this.writeToolFiles(
3701
- generated,
3702
- projectRoot,
3703
- manifest
3705
+ results.push(result);
3706
+ }
3707
+ return results;
3708
+ }
3709
+ /** Resolves which tool IDs to install from the 4-branch selection logic. */
3710
+ async resolveToolIds(options, manifest) {
3711
+ const interactive = options.interactive ?? false;
3712
+ if (options.all) return [...VALID_TOOL_IDS];
3713
+ if (options.toolIds !== void 0 && options.toolIds.length > 0) return options.toolIds;
3714
+ if (interactive && this.prompter !== void 0) return this.promptToolIds(manifest);
3715
+ throw new Error(`At least one tool ID is required. Valid tools: ${VALID_TOOL_IDS.join(", ")}`);
3716
+ }
3717
+ async promptToolIds(manifest) {
3718
+ if (this.prompter === void 0) throw new Error("Prompter is required for interactive mode.");
3719
+ const installedIds = manifest.getInstalledToolIds();
3720
+ const choices = VALID_TOOL_IDS.map(
3721
+ (id) => installedIds.includes(id) ? { name: id, value: id, checked: true, disabled: "(already installed)" } : { name: id, value: id, checked: false }
3722
+ );
3723
+ const selected = await this.prompter.checkbox("Which tools do you want to install?", choices);
3724
+ if (selected.length === 0) throw new Error("No tools selected.");
3725
+ return selected;
3726
+ }
3727
+ /** Installs a single tool and updates the manifest in place. */
3728
+ async installOneTool(toolId, manifest, descriptor, contentFiles, docsDir, projectRoot, force) {
3729
+ if (manifest.hasTool(toolId) && !force) {
3730
+ return { toolId, fileCount: 0, files: [], skipped: true, warnings: [] };
3731
+ }
3732
+ const config = getToolConfig(toolId);
3733
+ const warnings = await this.checkForceWarning(toolId, config, manifest, projectRoot, force);
3734
+ this.logger.info(`Generating ${toolId} distribution...`);
3735
+ const generated = await generateDistribution(
3736
+ descriptor,
3737
+ config,
3738
+ docsDir,
3739
+ contentFiles,
3740
+ this.hasher,
3741
+ this.platform,
3742
+ projectRoot,
3743
+ this.fs
3744
+ );
3745
+ await this.removeStaleFiles(toolId, manifest, generated, projectRoot);
3746
+ const { files: finalFiles, userFileConflicts } = await this.writeToolFiles(
3747
+ generated,
3748
+ projectRoot,
3749
+ manifest
3750
+ );
3751
+ for (const relativePath of userFileConflicts) {
3752
+ warnings.push(
3753
+ `\`${relativePath}\` already exists and was not installed by AIDD \u2014 skipped to preserve user file`
3704
3754
  );
3705
- for (const relativePath of userFileConflicts) {
3755
+ }
3756
+ manifest.addTool(toolId, descriptor.version, finalFiles);
3757
+ return { toolId, fileCount: generated.length, files: generated, skipped: false, warnings };
3758
+ }
3759
+ async checkForceWarning(toolId, config, manifest, projectRoot, force) {
3760
+ const warnings = [];
3761
+ if (!manifest.hasTool(toolId) && force) {
3762
+ const toolDir = join19(projectRoot, config.directory);
3763
+ if (await this.fs.fileExists(toolDir)) {
3706
3764
  warnings.push(
3707
- `\`${relativePath}\` already exists and was not installed by AIDD \u2014 skipped to preserve user file`
3765
+ `Directory ${config.directory} exists but tool is not in manifest. Files will be overwritten.`
3708
3766
  );
3709
3767
  }
3710
- manifest.addTool(toolId, descriptor.version, finalFiles);
3711
- results.push({
3712
- toolId,
3713
- fileCount: generated.length,
3714
- files: generated,
3715
- skipped: false,
3716
- warnings
3717
- });
3718
3768
  }
3719
- await new MemoryScriptUseCase(this.fs, this.hasher, this.git).execute({
3720
- projectRoot,
3721
- version,
3722
- descriptor,
3723
- contentFiles,
3724
- manifest
3725
- });
3726
- await this.manifestRepo.save(manifest);
3727
- await new CatalogUseCase(this.fs).execute({ manifest, docsDir, projectRoot });
3728
- await new GitignoreUseCase(this.fs).execute(projectRoot, [".aidd/cache/"]);
3729
- return results;
3769
+ return warnings;
3770
+ }
3771
+ async removeStaleFiles(toolId, manifest, generated, projectRoot) {
3772
+ if (!manifest.hasTool(toolId)) return;
3773
+ const newPaths = new Set(generated.map((f) => f.relativePath));
3774
+ for (const oldFile of manifest.getToolFiles(toolId)) {
3775
+ if (!newPaths.has(oldFile.relativePath)) {
3776
+ await this.fs.deleteFile(join19(projectRoot, oldFile.relativePath));
3777
+ }
3778
+ }
3730
3779
  }
3731
3780
  async writeToolFiles(generated, projectRoot, manifest) {
3732
3781
  const filesByPath = /* @__PURE__ */ new Map();
@@ -3922,14 +3971,14 @@ function buildDocsDistribution(docsFiles, docsDir, hasher) {
3922
3971
 
3923
3972
  // src/application/use-cases/restore-use-case.ts
3924
3973
  var RestoreUseCase = class {
3925
- constructor(fs, manifestRepo, loader, hasher, logger, prompter, platform3) {
3974
+ constructor(fs, manifestRepo, loader, hasher, logger, platform3, prompter) {
3926
3975
  this.fs = fs;
3927
3976
  this.manifestRepo = manifestRepo;
3928
3977
  this.loader = loader;
3929
3978
  this.hasher = hasher;
3930
3979
  this.logger = logger;
3931
- this.prompter = prompter;
3932
3980
  this.platform = platform3;
3981
+ this.prompter = prompter;
3933
3982
  }
3934
3983
  async execute(options) {
3935
3984
  const {
@@ -3942,67 +3991,136 @@ var RestoreUseCase = class {
3942
3991
  repo
3943
3992
  } = options;
3944
3993
  const docsOnly = options.docsOnly ?? false;
3945
- const fileFilter = buildFileFilter(options.files);
3946
3994
  const manifest = options.manifest ?? await this.manifestRepo.load();
3947
3995
  if (manifest === null) throw new NoManifestError(repo);
3948
- const toolIds = docsOnly ? [] : options.toolIds && options.toolIds.length > 0 ? options.toolIds : manifest.getInstalledToolIds();
3949
3996
  const { descriptor, contentFiles, docsFiles } = await this.loader.loadFromDirectory(
3950
3997
  frameworkPath,
3951
3998
  version
3952
3999
  );
4000
+ const fileFilter = buildFileFilter(options.files);
4001
+ return this.executeRestore({
4002
+ options,
4003
+ docsOnly,
4004
+ manifest,
4005
+ descriptor,
4006
+ contentFiles,
4007
+ docsFiles,
4008
+ docsDir,
4009
+ projectRoot,
4010
+ version,
4011
+ force,
4012
+ interactive,
4013
+ fileFilter
4014
+ });
4015
+ }
4016
+ async executeRestore(ctx) {
4017
+ const {
4018
+ options,
4019
+ docsOnly,
4020
+ manifest,
4021
+ descriptor,
4022
+ contentFiles,
4023
+ docsFiles,
4024
+ docsDir,
4025
+ projectRoot,
4026
+ version,
4027
+ force,
4028
+ interactive,
4029
+ fileFilter
4030
+ } = ctx;
4031
+ const toolIds = this.resolveToolIds(options, docsOnly, manifest);
4032
+ const toolResults = await this.restoreAllTools(
4033
+ toolIds,
4034
+ manifest,
4035
+ descriptor,
4036
+ contentFiles,
4037
+ docsDir,
4038
+ projectRoot,
4039
+ version,
4040
+ force,
4041
+ interactive,
4042
+ fileFilter
4043
+ );
4044
+ const hasExplicitToolFilter = !docsOnly && options.toolIds !== void 0 && options.toolIds.length > 0;
4045
+ const docsResult = hasExplicitToolFilter ? null : await this.restoreDocs(
4046
+ manifest,
4047
+ docsFiles,
4048
+ docsDir,
4049
+ projectRoot,
4050
+ version,
4051
+ force,
4052
+ interactive,
4053
+ fileFilter
4054
+ );
4055
+ const hasChanges = toolResults.some((t) => t.restored.length > 0) || docsResult !== null && docsResult.restored.length > 0;
4056
+ if (hasChanges) await this.manifestRepo.save(manifest);
4057
+ return this.buildRestoreTotals(toolResults, docsResult);
4058
+ }
4059
+ resolveToolIds(options, docsOnly, manifest) {
4060
+ if (docsOnly) return [];
4061
+ return options.toolIds?.length ? options.toolIds : manifest.getInstalledToolIds();
4062
+ }
4063
+ async restoreAllTools(toolIds, manifest, descriptor, contentFiles, docsDir, projectRoot, version, force, interactive, fileFilter) {
3953
4064
  const toolResults = [];
3954
4065
  for (const toolId of toolIds) {
3955
- this.logger.info(`Checking ${toolId} for files to restore...`);
3956
- const config = getToolConfig(toolId);
3957
- const manifestFiles = manifest.getToolFiles(toolId);
3958
- const distribution = await generateDistribution(
4066
+ const result = await this.restoreOneTool(
4067
+ toolId,
4068
+ manifest,
3959
4069
  descriptor,
3960
- config,
3961
- docsDir,
3962
4070
  contentFiles,
3963
- this.hasher,
3964
- this.platform,
3965
- projectRoot,
3966
- this.fs
3967
- );
3968
- const distMap = new Map(distribution.map((f) => [f.relativePath, f]));
3969
- const drift = await this.collectDrift(manifestFiles, distMap, projectRoot, fileFilter);
3970
- if (drift.length === 0) {
3971
- toolResults.push({ toolId, nothingToRestore: true, restored: [], kept: [] });
3972
- continue;
3973
- }
3974
- const { restored, kept, updatedHashMap } = await this.applyRestorations(
3975
- drift,
3976
- new Map(manifestFiles.map((f) => [f.relativePath, f.hash])),
4071
+ docsDir,
3977
4072
  projectRoot,
4073
+ version,
3978
4074
  force,
3979
- interactive
4075
+ interactive,
4076
+ fileFilter
3980
4077
  );
3981
- manifest.addTool(
3982
- toolId,
3983
- manifest.getToolVersion(toolId) ?? version,
3984
- Array.from(updatedHashMap.entries()).map(
3985
- ([relativePath, hash]) => new GeneratedFile({ relativePath, content: "", hash })
3986
- )
3987
- );
3988
- toolResults.push({ toolId, nothingToRestore: false, restored, kept });
4078
+ toolResults.push(result);
3989
4079
  }
3990
- const hasExplicitToolFilter = !docsOnly && options.toolIds !== void 0 && options.toolIds.length > 0;
3991
- const docsResult = hasExplicitToolFilter ? null : await this.restoreDocs(
3992
- manifest,
3993
- docsFiles,
4080
+ return toolResults;
4081
+ }
4082
+ async restoreOneTool(toolId, manifest, descriptor, contentFiles, docsDir, projectRoot, version, force, interactive, fileFilter) {
4083
+ this.logger.info(`Checking ${toolId} for files to restore...`);
4084
+ const config = getToolConfig(toolId);
4085
+ const manifestFiles = manifest.getToolFiles(toolId);
4086
+ const distribution = await generateDistribution(
4087
+ descriptor,
4088
+ config,
3994
4089
  docsDir,
4090
+ contentFiles,
4091
+ this.hasher,
4092
+ this.platform,
4093
+ projectRoot,
4094
+ this.fs
4095
+ );
4096
+ const distMap = new Map(distribution.map((f) => [f.relativePath, f]));
4097
+ const section = await this.restoreSection(
4098
+ manifestFiles,
4099
+ distMap,
3995
4100
  projectRoot,
3996
- version,
3997
4101
  force,
3998
4102
  interactive,
3999
4103
  fileFilter
4000
4104
  );
4001
- const hasChanges = toolResults.some((t) => t.restored.length > 0) || docsResult !== null && docsResult.restored.length > 0;
4002
- if (hasChanges) await this.manifestRepo.save(manifest);
4003
- const totalRestored = toolResults.reduce((s, t) => s + t.restored.length, 0) + (docsResult?.restored.length ?? 0);
4004
- const totalKept = toolResults.reduce((s, t) => s + t.kept.length, 0) + (docsResult?.kept.length ?? 0);
4005
- return { tools: toolResults, docs: docsResult, totalRestored, totalKept };
4105
+ if (section === null) return { toolId, nothingToRestore: true, restored: [], kept: [] };
4106
+ manifest.addTool(toolId, manifest.getToolVersion(toolId) ?? version, section.updatedFiles);
4107
+ return { toolId, nothingToRestore: false, restored: section.restored, kept: section.kept };
4108
+ }
4109
+ /** Shared restoration logic for both tool files and docs files. Returns null when nothing to restore. */
4110
+ async restoreSection(manifestFiles, distMap, projectRoot, force, interactive, fileFilter) {
4111
+ const drift = await this.collectDrift(manifestFiles, distMap, projectRoot, fileFilter);
4112
+ if (drift.length === 0) return null;
4113
+ const { restored, kept, updatedHashMap } = await this.applyRestorations(
4114
+ drift,
4115
+ new Map(manifestFiles.map((f) => [f.relativePath, f.hash])),
4116
+ projectRoot,
4117
+ force,
4118
+ interactive
4119
+ );
4120
+ const updatedFiles = Array.from(updatedHashMap.entries()).map(
4121
+ ([relativePath, hash]) => new GeneratedFile({ relativePath, content: "", hash })
4122
+ );
4123
+ return { restored, kept, updatedFiles };
4006
4124
  }
4007
4125
  async restoreDocs(manifest, docsFiles, docsDir, projectRoot, version, force, interactive, fileFilter) {
4008
4126
  const docsManifestFiles = manifest.getDocsFiles();
@@ -4011,22 +4129,22 @@ var RestoreUseCase = class {
4011
4129
  this.logger.info("Checking docs for files to restore...");
4012
4130
  const distribution = buildDocsDistribution(docsFiles, docsDir, this.hasher);
4013
4131
  const distMap = new Map(distribution.map((f) => [f.relativePath, f]));
4014
- const drift = await this.collectDrift(docsManifestFiles, distMap, projectRoot, fileFilter);
4015
- if (drift.length === 0) return { nothingToRestore: true, restored: [], kept: [] };
4016
- const { restored, kept, updatedHashMap } = await this.applyRestorations(
4017
- drift,
4018
- new Map(docsManifestFiles.map((f) => [f.relativePath, f.hash])),
4132
+ const section = await this.restoreSection(
4133
+ docsManifestFiles,
4134
+ distMap,
4019
4135
  projectRoot,
4020
4136
  force,
4021
- interactive
4022
- );
4023
- manifest.addDocs(
4024
- manifest.getDocsVersion() ?? version,
4025
- Array.from(updatedHashMap.entries()).map(
4026
- ([relativePath, hash]) => new GeneratedFile({ relativePath, content: "", hash })
4027
- )
4137
+ interactive,
4138
+ fileFilter
4028
4139
  );
4029
- return { nothingToRestore: false, restored, kept };
4140
+ if (section === null) return { nothingToRestore: true, restored: [], kept: [] };
4141
+ manifest.addDocs(manifest.getDocsVersion() ?? version, section.updatedFiles);
4142
+ return { nothingToRestore: false, restored: section.restored, kept: section.kept };
4143
+ }
4144
+ buildRestoreTotals(toolResults, docsResult) {
4145
+ const totalRestored = toolResults.reduce((s, t) => s + t.restored.length, 0) + (docsResult?.restored.length ?? 0);
4146
+ const totalKept = toolResults.reduce((s, t) => s + t.kept.length, 0) + (docsResult?.kept.length ?? 0);
4147
+ return { tools: toolResults, docs: docsResult, totalRestored, totalKept };
4030
4148
  }
4031
4149
  async collectDrift(manifestFiles, distMap, projectRoot, fileFilter) {
4032
4150
  const drift = [];
@@ -4240,8 +4358,8 @@ function registerRestoreCommand(program2) {
4240
4358
  deps.loader,
4241
4359
  deps.hasher,
4242
4360
  deps.logger,
4243
- prompter,
4244
- deps.platform
4361
+ deps.platform,
4362
+ prompter
4245
4363
  );
4246
4364
  const result = await restoreUseCase.execute({
4247
4365
  frameworkPath,
@@ -4370,6 +4488,45 @@ var AdoptUseCase = class {
4370
4488
  }
4371
4489
  async execute(options) {
4372
4490
  const { toolIds, frameworkPath, docsDir, projectRoot, version } = options;
4491
+ this.validateToolIds(toolIds);
4492
+ const existing = await this.manifestRepo.load();
4493
+ if (existing !== null) throw new Error("Already initialized. Use `aidd update` to upgrade.");
4494
+ await this.deleteLegacyConfig(projectRoot);
4495
+ const { descriptor, contentFiles, docsFiles } = await this.loader.loadFromDirectory(
4496
+ frameworkPath,
4497
+ version
4498
+ );
4499
+ const manifest = Manifest.create(docsDir);
4500
+ const toolResults = await this.registerAllTools(
4501
+ toolIds,
4502
+ manifest,
4503
+ descriptor,
4504
+ contentFiles,
4505
+ docsDir,
4506
+ projectRoot,
4507
+ version
4508
+ );
4509
+ const docsRegistered = await this.registerDocs(
4510
+ manifest,
4511
+ docsFiles,
4512
+ docsDir,
4513
+ projectRoot,
4514
+ version
4515
+ );
4516
+ await this.persistAdopt(manifest, docsDir, projectRoot, version);
4517
+ return {
4518
+ tools: toolResults,
4519
+ totalRegistered: toolResults.reduce((sum, r) => sum + r.registered.length, 0),
4520
+ docsRegistered
4521
+ };
4522
+ }
4523
+ /** Finalizes catalog, saves manifest, and writes gitignore entry. */
4524
+ async persistAdopt(manifest, docsDir, projectRoot, version) {
4525
+ await this.finalizeCatalog(manifest, docsDir, projectRoot, version);
4526
+ await this.manifestRepo.save(manifest);
4527
+ await new GitignoreUseCase(this.fs).execute(projectRoot, [".aidd/cache/"]);
4528
+ }
4529
+ validateToolIds(toolIds) {
4373
4530
  const invalid = toolIds.filter((t) => !VALID_TOOL_IDS.includes(t));
4374
4531
  if (invalid.length > 0) {
4375
4532
  throw new Error(
@@ -4379,16 +4536,8 @@ var AdoptUseCase = class {
4379
4536
  if (toolIds.length === 0) {
4380
4537
  throw new Error("No tools specified. Use --tools to specify at least one tool.");
4381
4538
  }
4382
- const existing = await this.manifestRepo.load();
4383
- if (existing !== null) {
4384
- throw new Error("Already initialized. Use `aidd update` to upgrade.");
4385
- }
4386
- await this.deleteLegacyConfig(projectRoot);
4387
- const { descriptor, contentFiles, docsFiles } = await this.loader.loadFromDirectory(
4388
- frameworkPath,
4389
- version
4390
- );
4391
- const manifest = Manifest.create(docsDir);
4539
+ }
4540
+ async registerAllTools(toolIds, manifest, descriptor, contentFiles, docsDir, projectRoot, version) {
4392
4541
  const toolResults = [];
4393
4542
  for (const toolId of toolIds) {
4394
4543
  this.logger.info(`Adopting ${toolId}...`);
@@ -4416,37 +4565,33 @@ var AdoptUseCase = class {
4416
4565
  manifest.addTool(toolId, version, registeredFiles);
4417
4566
  toolResults.push({ toolId, registered: registeredFiles.map((f) => f.relativePath) });
4418
4567
  }
4419
- let docsRegistered = 0;
4568
+ return toolResults;
4569
+ }
4570
+ async registerDocs(manifest, docsFiles, docsDir, projectRoot, version) {
4420
4571
  const docsAbsDir = join22(projectRoot, docsDir);
4421
- if (await this.fs.fileExists(docsAbsDir)) {
4422
- this.logger.info("Adopting docs...");
4423
- const docsDistribution = buildDocsDistribution(docsFiles, docsDir, this.hasher);
4424
- const registeredFiles = await this.matchDistributionToDisk(docsDistribution, projectRoot);
4425
- manifest.addDocs(version, registeredFiles);
4426
- docsRegistered = registeredFiles.length;
4427
- }
4572
+ if (!await this.fs.fileExists(docsAbsDir)) return 0;
4573
+ this.logger.info("Adopting docs...");
4574
+ const docsDistribution = buildDocsDistribution(docsFiles, docsDir, this.hasher);
4575
+ const registeredFiles = await this.matchDistributionToDisk(docsDistribution, projectRoot);
4576
+ manifest.addDocs(version, registeredFiles);
4577
+ return registeredFiles.length;
4578
+ }
4579
+ async finalizeCatalog(manifest, docsDir, projectRoot, version) {
4428
4580
  await new CatalogUseCase(this.fs).execute({ manifest, docsDir, projectRoot });
4429
4581
  const catalogRelPath = `${docsDir}/CATALOG.md`;
4430
4582
  const catalogAbsPath = join22(projectRoot, catalogRelPath);
4431
- if (await this.fs.fileExists(catalogAbsPath)) {
4432
- const catalogHash = await this.fs.readFileHash(catalogAbsPath);
4433
- const currentDocsFiles = manifest.getDocsFiles();
4434
- const updatedDocsFiles = currentDocsFiles.map(
4435
- (f) => f.relativePath === catalogRelPath ? new GeneratedFile({ relativePath: f.relativePath, content: "", hash: catalogHash }) : new GeneratedFile({ relativePath: f.relativePath, content: "", hash: f.hash })
4583
+ if (!await this.fs.fileExists(catalogAbsPath)) return;
4584
+ const catalogHash = await this.fs.readFileHash(catalogAbsPath);
4585
+ const currentDocsFiles = manifest.getDocsFiles();
4586
+ const updatedDocsFiles = currentDocsFiles.map(
4587
+ (f) => f.relativePath === catalogRelPath ? new GeneratedFile({ relativePath: f.relativePath, content: "", hash: catalogHash }) : new GeneratedFile({ relativePath: f.relativePath, content: "", hash: f.hash })
4588
+ );
4589
+ if (!currentDocsFiles.some((f) => f.relativePath === catalogRelPath)) {
4590
+ updatedDocsFiles.push(
4591
+ new GeneratedFile({ relativePath: catalogRelPath, content: "", hash: catalogHash })
4436
4592
  );
4437
- if (!currentDocsFiles.some((f) => f.relativePath === catalogRelPath)) {
4438
- updatedDocsFiles.push(
4439
- new GeneratedFile({ relativePath: catalogRelPath, content: "", hash: catalogHash })
4440
- );
4441
- }
4442
- manifest.addDocs(manifest.getDocsVersion() ?? version, updatedDocsFiles);
4443
4593
  }
4444
- await this.manifestRepo.save(manifest);
4445
- return {
4446
- tools: toolResults,
4447
- totalRegistered: toolResults.reduce((sum, r) => sum + r.registered.length, 0),
4448
- docsRegistered
4449
- };
4594
+ manifest.addDocs(manifest.getDocsVersion() ?? version, updatedDocsFiles);
4450
4595
  }
4451
4596
  async matchDistributionToDisk(distribution, projectRoot) {
4452
4597
  const result = [];
@@ -4514,39 +4659,71 @@ var InitUseCase = class {
4514
4659
  }
4515
4660
  async execute(options) {
4516
4661
  const { frameworkPath, version, projectRoot, force = false } = options;
4517
- const interactive = options.interactive ?? false;
4518
- let docsDir = options.docsDir;
4519
- let explicitDocsDir = options.explicitDocsDir;
4520
- let repo = options.repo;
4521
- if (interactive && !force && docsDir === void 0 && this.prompter !== void 0) {
4522
- const docsDirInput = await this.prompter.input(
4523
- "Documentation directory name:",
4524
- Manifest.DEFAULT_DOCS_DIR
4525
- );
4526
- docsDir = docsDirInput || Manifest.DEFAULT_DOCS_DIR;
4527
- explicitDocsDir = docsDir;
4528
- const repoInput = await this.prompter.input(
4529
- "Framework repository (owner/repo, leave blank to skip):",
4530
- options.repo ?? ""
4531
- );
4532
- if (repoInput !== "") {
4533
- repo = repoInput.trim();
4534
- }
4535
- }
4662
+ const { docsDir, explicitDocsDir, repo } = await this.resolveInitConfig(options);
4536
4663
  const resolvedInputDocsDir = docsDir ?? Manifest.DEFAULT_DOCS_DIR;
4537
4664
  Manifest.validateDocsDir(resolvedInputDocsDir);
4538
- await this.checkPreconditions({ docsDir: resolvedInputDocsDir, projectRoot, force, repo });
4539
4665
  const existing = await this.manifestRepo.load();
4666
+ await this.checkPreconditions({ docsDir: resolvedInputDocsDir, projectRoot, force, repo });
4540
4667
  const resolvedDocsDir = force && existing !== null && explicitDocsDir === void 0 ? existing.docsDir : resolvedInputDocsDir;
4541
4668
  const { descriptor, docsFiles } = await this.loader.loadFromDirectory(frameworkPath, version);
4669
+ const generated = await this.writeDocsFiles(
4670
+ docsFiles,
4671
+ resolvedDocsDir,
4672
+ projectRoot,
4673
+ force,
4674
+ existing
4675
+ );
4676
+ if (force && existing !== null)
4677
+ await this.removeStaleDocsFiles(generated, existing, projectRoot);
4678
+ const manifest = this.buildManifest(existing, resolvedDocsDir, repo, force);
4679
+ manifest.addDocs(descriptor.version, generated);
4680
+ await this.persistInit(manifest, resolvedDocsDir, projectRoot, force);
4681
+ return { docsDir: resolvedDocsDir, fileCount: generated.length, manifest };
4682
+ }
4683
+ buildManifest(existing, resolvedDocsDir, repo, force) {
4684
+ return force && existing !== null ? existing.withDocsDir(resolvedDocsDir) : Manifest.create(resolvedDocsDir, repo);
4685
+ }
4686
+ /** Saves manifest, regenerates catalog, and conditionally adds gitignore entry. */
4687
+ async persistInit(manifest, docsDir, projectRoot, force) {
4688
+ await this.manifestRepo.save(manifest);
4689
+ await new CatalogUseCase(this.fs).execute({ manifest, docsDir, projectRoot });
4690
+ if (!force) {
4691
+ await new GitignoreUseCase(this.fs).execute(projectRoot, [".aidd/cache/"]);
4692
+ }
4693
+ }
4694
+ /** Resolves interactive config (docsDir, repo) if prompted, otherwise uses options as-is. */
4695
+ async resolveInitConfig(options) {
4696
+ const interactive = options.interactive ?? false;
4697
+ const force = options.force ?? false;
4698
+ if (!interactive || force || options.docsDir !== void 0 || this.prompter === void 0) {
4699
+ return {
4700
+ docsDir: options.docsDir,
4701
+ explicitDocsDir: options.explicitDocsDir,
4702
+ repo: options.repo
4703
+ };
4704
+ }
4705
+ const docsDirInput = await this.prompter.input(
4706
+ "Documentation directory name:",
4707
+ Manifest.DEFAULT_DOCS_DIR
4708
+ );
4709
+ const docsDir = docsDirInput || Manifest.DEFAULT_DOCS_DIR;
4710
+ const repoInput = await this.prompter.input(
4711
+ "Framework repository (owner/repo, leave blank to skip):",
4712
+ options.repo ?? ""
4713
+ );
4714
+ const repo = repoInput !== "" ? repoInput.trim() : options.repo;
4715
+ return { docsDir, explicitDocsDir: docsDir, repo };
4716
+ }
4717
+ /** Writes docs files to disk, skipping CATALOG.md. Returns the list of generated files. */
4718
+ async writeDocsFiles(docsFiles, docsDir, projectRoot, force, existing) {
4542
4719
  const generated = [];
4543
4720
  for (const [frameworkRelPath, rawContent] of docsFiles.entries()) {
4544
4721
  if (frameworkRelPath.endsWith("CATALOG.md")) continue;
4545
- const outputRelPath = remapDocsPath(frameworkRelPath, resolvedDocsDir);
4722
+ const outputRelPath = remapDocsPath(frameworkRelPath, docsDir);
4546
4723
  const outputPath = join23(projectRoot, outputRelPath);
4547
- const content = rewriteDocsContent(rawContent, resolvedDocsDir);
4724
+ const content = rewriteDocsContent(rawContent, docsDir);
4548
4725
  const newHash = this.hasher.hash(content);
4549
- if (force && await this.fs.fileExists(outputPath)) {
4726
+ if (force && existing !== null && await this.fs.fileExists(outputPath)) {
4550
4727
  const diskHash = await this.fs.readFileHash(outputPath);
4551
4728
  if (!diskHash.equals(newHash)) {
4552
4729
  this.logger.warn(`Overwriting modified file: ${outputRelPath}`);
@@ -4557,28 +4734,74 @@ var InitUseCase = class {
4557
4734
  }
4558
4735
  generated.push(new GeneratedFile({ relativePath: outputRelPath, content, hash: newHash }));
4559
4736
  }
4560
- if (force && existing !== null) {
4561
- const newPaths = new Set(generated.map((f) => f.relativePath));
4562
- for (const oldFile of existing.getDocsFiles()) {
4563
- if (!newPaths.has(oldFile.relativePath)) {
4564
- await this.fs.deleteFile(join23(projectRoot, oldFile.relativePath));
4565
- }
4737
+ return generated;
4738
+ }
4739
+ async removeStaleDocsFiles(generated, existing, projectRoot) {
4740
+ const newPaths = new Set(generated.map((f) => f.relativePath));
4741
+ for (const oldFile of existing.getDocsFiles()) {
4742
+ if (!newPaths.has(oldFile.relativePath)) {
4743
+ await this.fs.deleteFile(join23(projectRoot, oldFile.relativePath));
4566
4744
  }
4567
4745
  }
4568
- const manifest = force && existing !== null ? existing.withDocsDir(resolvedDocsDir) : Manifest.create(resolvedDocsDir, repo);
4569
- manifest.addDocs(descriptor.version, generated);
4570
- await this.manifestRepo.save(manifest);
4571
- await new CatalogUseCase(this.fs).execute({ manifest, docsDir: resolvedDocsDir, projectRoot });
4572
- if (!force) {
4573
- await new GitignoreUseCase(this.fs).execute(projectRoot, [".aidd/cache/"]);
4746
+ }
4747
+ };
4748
+
4749
+ // src/application/use-cases/shared/setup-state-detector.ts
4750
+ var SetupStateDetector = class {
4751
+ constructor(manifestRepo, fs, resolver) {
4752
+ this.manifestRepo = manifestRepo;
4753
+ this.fs = fs;
4754
+ this.resolver = resolver;
4755
+ }
4756
+ async detect(projectRoot) {
4757
+ const manifest = await this.manifestRepo.load();
4758
+ if (manifest === null) {
4759
+ return this.detectWithoutManifest(projectRoot);
4574
4760
  }
4575
- return { docsDir: resolvedDocsDir, fileCount: generated.length, manifest };
4761
+ const installedIds = manifest.getInstalledToolIds();
4762
+ if (installedIds.length === 0) {
4763
+ return { kind: "needs-install" };
4764
+ }
4765
+ return this.detectUpdateState(manifest, installedIds);
4766
+ }
4767
+ async detectWithoutManifest(projectRoot) {
4768
+ for (const tool of getAllRegisteredTools().values()) {
4769
+ if (await hasToolSignals(this.fs, tool, projectRoot)) return { kind: "needs-adopt" };
4770
+ }
4771
+ return { kind: "needs-init" };
4772
+ }
4773
+ async detectUpdateState(manifest, installedIds) {
4774
+ try {
4775
+ const latestVersion = await this.resolver.fetchLatestVersion(manifest.repo);
4776
+ const installedVersions = installedIds.map((id) => manifest.getToolVersion(id)).filter((v) => v !== void 0);
4777
+ const currentVersion2 = installedVersions[0] ?? "unknown";
4778
+ const needsUpdate = isSemver(latestVersion) && installedVersions.some((v) => !isSemver(v) || compareSemver(v, latestVersion) < 0);
4779
+ if (needsUpdate) {
4780
+ return { kind: "needs-update", currentVersion: currentVersion2, latestVersion };
4781
+ }
4782
+ } catch {
4783
+ }
4784
+ return { kind: "up-to-date" };
4576
4785
  }
4577
4786
  };
4578
4787
 
4579
4788
  // src/application/use-cases/update-use-case.ts
4580
4789
  import { join as join24 } from "path";
4581
4790
 
4791
+ // src/domain/models/update-scope.ts
4792
+ function parseUpdateScope(raw) {
4793
+ if (raw === "all") return { kind: "all" };
4794
+ if (raw === "docs") return { kind: "docs" };
4795
+ if (raw.startsWith("tool:")) {
4796
+ const toolId = raw.slice(5);
4797
+ return { kind: "tool", toolId };
4798
+ }
4799
+ throw new Error(`Invalid update scope: "${raw}"`);
4800
+ }
4801
+ function formatToolScopeValue(toolId) {
4802
+ return `tool:${toolId}`;
4803
+ }
4804
+
4582
4805
  // src/application/use-cases/conflict-resolution-use-case.ts
4583
4806
  var ConflictResolutionUseCase = class {
4584
4807
  constructor(prompter) {
@@ -4642,51 +4865,70 @@ var UpdateUseCase = class {
4642
4865
  }
4643
4866
  async execute(options) {
4644
4867
  const { force = false, dryRun = false } = options;
4645
- const interactive = options.interactive ?? false;
4646
4868
  const conflictResolution = new ConflictResolutionUseCase(this.prompter);
4647
- const isInteractive = interactive && !force && !dryRun && options.toolIds === void 0 && !(options.docsOnly ?? false);
4648
- if (isInteractive) {
4649
- const dryRunResult = await this.executeInternal(options, {
4650
- dryRun: true,
4651
- force: false,
4652
- conflictResolution
4653
- });
4654
- const changedTools = dryRunResult.tools.filter(
4655
- (t) => t.diff.some((d) => d.kind !== "unchanged")
4656
- );
4657
- const docsChanged = dryRunResult.docs?.diff.some((d) => d.kind !== "unchanged") ?? false;
4658
- if (changedTools.length === 0 && !docsChanged) {
4659
- return this.executeInternal(
4660
- { ...options, force: true },
4661
- { dryRun: false, force: true, conflictResolution }
4662
- ).then((r) => ({ ...r, cancelled: false, version: options.version }));
4663
- }
4664
- const scopeChoices = [
4665
- { name: "All", value: "all" },
4666
- ...changedTools.map((t) => ({ name: `${t.toolId} only`, value: `tool:${t.toolId}` })),
4667
- ...docsChanged ? [{ name: "docs only", value: "docs" }] : []
4668
- ];
4669
- const scopeSelection = await this.prompter.select("What to update?", scopeChoices);
4670
- const confirmed = await this.prompter.confirm("Apply update?");
4671
- if (!confirmed) {
4672
- return { ...dryRunResult, cancelled: true, version: options.version };
4673
- }
4674
- let toolIds;
4675
- let docsOnly = false;
4676
- if (scopeSelection === "docs") {
4677
- docsOnly = true;
4678
- } else if (scopeSelection.startsWith("tool:")) {
4679
- toolIds = [scopeSelection.slice(5)];
4680
- }
4681
- return this.executeInternal(
4682
- { ...options, toolIds, docsOnly, force: true },
4869
+ const isInteractive = this.resolveInteractiveFlag(options, force, dryRun);
4870
+ if (!isInteractive) {
4871
+ return this.executeInternal(options, { dryRun, force, conflictResolution }).then((r) => ({
4872
+ ...r,
4873
+ version: options.version
4874
+ }));
4875
+ }
4876
+ return this.executeInteractive(options, conflictResolution);
4877
+ }
4878
+ resolveInteractiveFlag(options, force, dryRun) {
4879
+ return (options.interactive ?? false) && !force && !dryRun && options.toolIds === void 0 && !(options.docsOnly ?? false);
4880
+ }
4881
+ async executeInteractive(options, conflictResolution) {
4882
+ const dryRunResult = await this.executeInternal(options, {
4883
+ dryRun: true,
4884
+ force: false,
4885
+ conflictResolution
4886
+ });
4887
+ const outcome = await this.buildInteractiveScope(dryRunResult, options, conflictResolution);
4888
+ if (outcome.kind === "already-applied") {
4889
+ return { ...outcome.result, version: options.version };
4890
+ }
4891
+ if (outcome.kind === "cancelled") {
4892
+ return { ...dryRunResult, cancelled: true, version: options.version };
4893
+ }
4894
+ return this.executeInternal(
4895
+ { ...options, toolIds: outcome.toolIds, docsOnly: outcome.docsOnly, force: true },
4896
+ { dryRun: false, force: true, conflictResolution }
4897
+ ).then((r) => ({ ...r, cancelled: false, version: options.version }));
4898
+ }
4899
+ /** Resolves the interactive update scope after a dry-run. */
4900
+ async buildInteractiveScope(dryRunResult, options, conflictResolution) {
4901
+ const changedTools = dryRunResult.tools.filter(
4902
+ (t) => t.diff.some((d) => d.kind !== "unchanged")
4903
+ );
4904
+ const docsChanged = dryRunResult.docs?.diff.some((d) => d.kind !== "unchanged") ?? false;
4905
+ if (changedTools.length === 0 && !docsChanged) {
4906
+ const result = await this.executeInternal(
4907
+ { ...options, force: true },
4683
4908
  { dryRun: false, force: true, conflictResolution }
4684
- ).then((r) => ({ ...r, cancelled: false, version: options.version }));
4909
+ );
4910
+ return { kind: "already-applied", result };
4911
+ }
4912
+ const scopeChoices = [
4913
+ { name: "All", value: "all" },
4914
+ ...changedTools.map((t) => ({
4915
+ name: `${t.toolId} only`,
4916
+ value: formatToolScopeValue(t.toolId)
4917
+ })),
4918
+ ...docsChanged ? [{ name: "docs only", value: "docs" }] : []
4919
+ ];
4920
+ const scopeSelection = await this.prompter.select("What to update?", scopeChoices);
4921
+ const confirmed = await this.prompter.confirm("Apply update?");
4922
+ if (!confirmed) return { kind: "cancelled" };
4923
+ const scope = parseUpdateScope(scopeSelection);
4924
+ let toolIds;
4925
+ let docsOnly = false;
4926
+ if (scope.kind === "docs") {
4927
+ docsOnly = true;
4928
+ } else if (scope.kind === "tool") {
4929
+ toolIds = [scope.toolId];
4685
4930
  }
4686
- return this.executeInternal(options, { dryRun, force, conflictResolution }).then((r) => ({
4687
- ...r,
4688
- version: options.version
4689
- }));
4931
+ return { kind: "scope", toolIds, docsOnly };
4690
4932
  }
4691
4933
  async executeInternal(options, internal) {
4692
4934
  const { frameworkPath, version, projectRoot, repo } = options;
@@ -4699,78 +4941,18 @@ var UpdateUseCase = class {
4699
4941
  frameworkPath,
4700
4942
  version
4701
4943
  );
4702
- const toolResults = [];
4703
- if (options.toolIds && options.toolIds.length > 0) {
4704
- const installedIds = new Set(manifest.getInstalledToolIds());
4705
- const notInstalled = options.toolIds.filter((id) => !installedIds.has(id));
4706
- if (notInstalled.length > 0) {
4707
- throw new Error(
4708
- `${notInstalled.join(", ")} ${notInstalled.length === 1 ? "is" : "are"} not installed. Use \`aidd install ${notInstalled.join(" ")}\` first.`
4709
- );
4710
- }
4711
- }
4712
- const effectiveToolIds = docsOnly ? [] : options.toolIds && options.toolIds.length > 0 ? options.toolIds : manifest.getInstalledToolIds();
4713
- for (const toolId of effectiveToolIds) {
4714
- this.logger.debug(`Checking ${toolId} for updates...`);
4715
- const config = getToolConfig(toolId);
4716
- const manifestFiles = manifest.getToolFiles(toolId);
4717
- const manifestMap = new Map(manifestFiles.map((f) => [f.relativePath, f.hash]));
4718
- const newDistribution = await generateDistribution(
4719
- descriptor,
4720
- config,
4721
- docsDir,
4722
- contentFiles,
4723
- this.hasher,
4724
- this.platform,
4725
- projectRoot,
4726
- this.fs
4727
- );
4728
- const newDistMap = new Map(newDistribution.map((f) => [f.relativePath, f]));
4729
- const diff = await this.computeDiff(newDistribution, newDistMap, manifestMap, projectRoot);
4730
- let result = {
4731
- kept: [],
4732
- written: [],
4733
- deleted: [],
4734
- backedUp: [],
4735
- userFileConflicts: []
4736
- };
4737
- if (!dryRun) {
4738
- const mergedHashMap = /* @__PURE__ */ new Map();
4739
- for (const newFile of newDistribution) {
4740
- if (!newFile.merge) continue;
4741
- const outputPath = join24(projectRoot, newFile.relativePath);
4742
- await this.fs.mergeJsonFile(outputPath, newFile.content);
4743
- const diskHash = await this.fs.readFileHash(outputPath);
4744
- manifest.syncFileHashAcrossTools(newFile.relativePath, diskHash);
4745
- mergedHashMap.set(newFile.relativePath, diskHash);
4746
- }
4747
- const conflictDecisions = await this.resolveConflicts(diff, force, conflictResolution);
4748
- result = await this.applyDiff(diff, newDistMap, projectRoot, conflictDecisions, manifest);
4749
- for (const relativePath of result.userFileConflicts) {
4750
- this.logger.warn(
4751
- `\`${relativePath}\` already exists and was not installed by AIDD \u2014 skipped to preserve user file`
4752
- );
4753
- }
4754
- const nonMergedFinal = newDistribution.filter((f) => !f.merge).filter(
4755
- (f) => !result.deleted.includes(f.relativePath) && !result.kept.includes(f.relativePath) && !result.userFileConflicts.includes(f.relativePath)
4756
- );
4757
- const keptFiles = manifestFiles.filter((f) => result.kept.includes(f.relativePath)).map(
4758
- (f) => new GeneratedFile({ relativePath: f.relativePath, content: "", hash: f.hash })
4759
- );
4760
- const mergedFiles = manifestFiles.filter((f) => newDistMap.get(f.relativePath)?.merge === true).map((f) => {
4761
- const hash = mergedHashMap.get(f.relativePath) ?? f.hash;
4762
- return new GeneratedFile({ relativePath: f.relativePath, content: "", hash });
4763
- });
4764
- manifest.addTool(toolId, version, [...nonMergedFinal, ...keptFiles, ...mergedFiles]);
4765
- }
4766
- toolResults.push({
4767
- toolId,
4768
- alreadyUpToDate: !diff.some((d) => d.kind !== "unchanged"),
4769
- dryRun,
4770
- diff,
4771
- ...result
4772
- });
4773
- }
4944
+ this.validateToolIds(options.toolIds, manifest);
4945
+ const effectiveToolIds = this.resolveEffectiveToolIds(options, docsOnly, manifest);
4946
+ const toolResults = await this.updateAllTools(
4947
+ effectiveToolIds,
4948
+ manifest,
4949
+ descriptor,
4950
+ contentFiles,
4951
+ docsDir,
4952
+ projectRoot,
4953
+ version,
4954
+ internal
4955
+ );
4774
4956
  const hasExplicitToolFilter = !docsOnly && options.toolIds !== void 0 && options.toolIds.length > 0;
4775
4957
  const docsResult = hasExplicitToolFilter ? null : await this.updateDocs(
4776
4958
  manifest,
@@ -4783,17 +4965,131 @@ var UpdateUseCase = class {
4783
4965
  conflictResolution
4784
4966
  );
4785
4967
  if (!dryRun) {
4786
- await new MemoryScriptUseCase(this.fs, this.hasher, this.git).execute({
4968
+ await new PostInstallPipelineUseCase(
4969
+ this.fs,
4970
+ this.manifestRepo,
4971
+ this.hasher,
4972
+ this.git
4973
+ ).execute({
4787
4974
  projectRoot,
4788
4975
  version,
4789
4976
  descriptor,
4790
4977
  contentFiles,
4791
- manifest
4978
+ manifest,
4979
+ docsDir
4792
4980
  });
4793
- await this.manifestRepo.save(manifest);
4794
- await new CatalogUseCase(this.fs).execute({ manifest, docsDir, projectRoot });
4795
- await new GitignoreUseCase(this.fs).execute(projectRoot, [".aidd/cache/"]);
4796
4981
  }
4982
+ return this.buildTotals(toolResults, docsResult, dryRun);
4983
+ }
4984
+ validateToolIds(toolIds, manifest) {
4985
+ if (!toolIds || toolIds.length === 0) return;
4986
+ const installedIds = new Set(manifest.getInstalledToolIds());
4987
+ const notInstalled = toolIds.filter((id) => !installedIds.has(id));
4988
+ if (notInstalled.length > 0) {
4989
+ throw new Error(
4990
+ `${notInstalled.join(", ")} ${notInstalled.length === 1 ? "is" : "are"} not installed. Use \`aidd install ${notInstalled.join(" ")}\` first.`
4991
+ );
4992
+ }
4993
+ }
4994
+ resolveEffectiveToolIds(options, docsOnly, manifest) {
4995
+ if (docsOnly) return [];
4996
+ if (options.toolIds && options.toolIds.length > 0) return options.toolIds;
4997
+ return manifest.getInstalledToolIds();
4998
+ }
4999
+ async updateAllTools(toolIds, manifest, descriptor, contentFiles, docsDir, projectRoot, version, internal) {
5000
+ const toolResults = [];
5001
+ for (const toolId of toolIds) {
5002
+ this.logger.debug(`Checking ${toolId} for updates...`);
5003
+ const result = await this.updateToolSection(
5004
+ toolId,
5005
+ manifest,
5006
+ descriptor,
5007
+ contentFiles,
5008
+ docsDir,
5009
+ projectRoot,
5010
+ version,
5011
+ internal
5012
+ );
5013
+ toolResults.push(result);
5014
+ }
5015
+ return toolResults;
5016
+ }
5017
+ async updateToolSection(toolId, manifest, descriptor, contentFiles, docsDir, projectRoot, version, internal) {
5018
+ const { dryRun, force, conflictResolution } = internal;
5019
+ const config = getToolConfig(toolId);
5020
+ const manifestFiles = manifest.getToolFiles(toolId);
5021
+ const manifestMap = new Map(manifestFiles.map((f) => [f.relativePath, f.hash]));
5022
+ const newDistribution = await generateDistribution(
5023
+ descriptor,
5024
+ config,
5025
+ docsDir,
5026
+ contentFiles,
5027
+ this.hasher,
5028
+ this.platform,
5029
+ projectRoot,
5030
+ this.fs
5031
+ );
5032
+ const newDistMap = new Map(newDistribution.map((f) => [f.relativePath, f]));
5033
+ const diff = await this.computeDiff(newDistribution, newDistMap, manifestMap, projectRoot);
5034
+ let result = this.emptyApplyDiffResult();
5035
+ if (!dryRun) {
5036
+ const mergedHashMap = await this.applyMergeFiles(newDistribution, projectRoot, manifest);
5037
+ const conflictDecisions = await this.resolveConflicts(diff, force, conflictResolution);
5038
+ result = await this.applyDiff(diff, newDistMap, projectRoot, conflictDecisions, manifest);
5039
+ this.warnUserFileConflicts(result.userFileConflicts);
5040
+ this.registerToolFiles(
5041
+ toolId,
5042
+ version,
5043
+ manifest,
5044
+ newDistribution,
5045
+ manifestFiles,
5046
+ result,
5047
+ mergedHashMap,
5048
+ newDistMap
5049
+ );
5050
+ }
5051
+ return {
5052
+ toolId,
5053
+ alreadyUpToDate: !diff.some((d) => d.kind !== "unchanged"),
5054
+ dryRun,
5055
+ diff,
5056
+ ...result
5057
+ };
5058
+ }
5059
+ emptyApplyDiffResult() {
5060
+ return { kept: [], written: [], deleted: [], backedUp: [], userFileConflicts: [] };
5061
+ }
5062
+ warnUserFileConflicts(conflicts) {
5063
+ for (const relativePath of conflicts) {
5064
+ this.logger.warn(
5065
+ `\`${relativePath}\` already exists and was not installed by AIDD \u2014 skipped to preserve user file`
5066
+ );
5067
+ }
5068
+ }
5069
+ async applyMergeFiles(newDistribution, projectRoot, manifest) {
5070
+ const mergedHashMap = /* @__PURE__ */ new Map();
5071
+ for (const newFile of newDistribution) {
5072
+ if (!newFile.merge) continue;
5073
+ const outputPath = join24(projectRoot, newFile.relativePath);
5074
+ await this.fs.mergeJsonFile(outputPath, newFile.content);
5075
+ const diskHash = await this.fs.readFileHash(outputPath);
5076
+ manifest.syncFileHashAcrossTools(newFile.relativePath, diskHash);
5077
+ mergedHashMap.set(newFile.relativePath, diskHash);
5078
+ }
5079
+ return mergedHashMap;
5080
+ }
5081
+ registerToolFiles(toolId, version, manifest, newDistribution, manifestFiles, result, mergedHashMap, newDistMap) {
5082
+ const nonMergedFinal = newDistribution.filter((f) => !f.merge).filter(
5083
+ (f) => !result.deleted.includes(f.relativePath) && !result.kept.includes(f.relativePath) && !result.userFileConflicts.includes(f.relativePath)
5084
+ );
5085
+ const keptFiles = manifestFiles.filter((f) => result.kept.includes(f.relativePath)).map((f) => new GeneratedFile({ relativePath: f.relativePath, content: "", hash: f.hash }));
5086
+ const mergedFiles = manifestFiles.filter((f) => newDistMap.get(f.relativePath)?.merge === true).map((f) => {
5087
+ const hash = mergedHashMap.get(f.relativePath) ?? f.hash;
5088
+ return new GeneratedFile({ relativePath: f.relativePath, content: "", hash });
5089
+ });
5090
+ manifest.addTool(toolId, version, [...nonMergedFinal, ...keptFiles, ...mergedFiles]);
5091
+ }
5092
+ buildTotals(toolResults, docsResult, dryRun) {
4797
5093
  const totalWritten = toolResults.reduce((s, t) => s + t.written.length, 0) + (docsResult?.written.length ?? 0);
4798
5094
  const totalDeleted = toolResults.reduce((s, t) => s + t.deleted.length, 0) + (docsResult?.deleted.length ?? 0);
4799
5095
  const toolCount = toolResults.filter((t) => !t.alreadyUpToDate).length;
@@ -4822,21 +5118,11 @@ var UpdateUseCase = class {
4822
5118
  const manifestFiles = manifest.getDocsFiles();
4823
5119
  const manifestMap = new Map(manifestFiles.map((f) => [f.relativePath, f.hash]));
4824
5120
  const diff = await this.computeDiff(newDistribution, newDistMap, manifestMap, projectRoot);
4825
- let result = {
4826
- kept: [],
4827
- written: [],
4828
- deleted: [],
4829
- backedUp: [],
4830
- userFileConflicts: []
4831
- };
5121
+ let result = this.emptyApplyDiffResult();
4832
5122
  if (!dryRun) {
4833
5123
  const conflictDecisions = await this.resolveConflicts(diff, force, conflictResolution);
4834
5124
  result = await this.applyDiff(diff, newDistMap, projectRoot, conflictDecisions, manifest);
4835
- for (const relativePath of result.userFileConflicts) {
4836
- this.logger.warn(
4837
- `\`${relativePath}\` already exists and was not installed by AIDD \u2014 skipped to preserve user file`
4838
- );
4839
- }
5125
+ this.warnUserFileConflicts(result.userFileConflicts);
4840
5126
  const finalFiles = newDistribution.filter(
4841
5127
  (f) => !result.deleted.includes(f.relativePath) && !result.kept.includes(f.relativePath) && !result.userFileConflicts.includes(f.relativePath)
4842
5128
  );
@@ -4965,10 +5251,7 @@ var SetupUseCase = class {
4965
5251
  }
4966
5252
  frameworkResolver;
4967
5253
  async execute(options) {
4968
- const state = await detectSetupState(
4969
- this.manifestRepo,
4970
- this.fs,
4971
- this.resolver,
5254
+ const state = await new SetupStateDetector(this.manifestRepo, this.fs, this.resolver).detect(
4972
5255
  options.projectRoot
4973
5256
  );
4974
5257
  switch (state.kind) {
@@ -4985,79 +5268,23 @@ var SetupUseCase = class {
4985
5268
  }
4986
5269
  }
4987
5270
  async handleInit(options) {
4988
- const { projectRoot, release, repo } = options;
4989
- let docsDir;
4990
- let explicitDocsDir;
4991
- if (options.docsDir !== void 0) {
4992
- docsDir = options.docsDir;
4993
- explicitDocsDir = options.docsDir;
4994
- Manifest.validateDocsDir(docsDir);
4995
- } else if (!options.interactive) {
4996
- docsDir = Manifest.DEFAULT_DOCS_DIR;
4997
- explicitDocsDir = "";
4998
- } else {
4999
- const docsDirInput = await this.prompter.input(
5000
- "Documentation directory name:",
5001
- Manifest.DEFAULT_DOCS_DIR
5002
- );
5003
- docsDir = docsDirInput || Manifest.DEFAULT_DOCS_DIR;
5004
- explicitDocsDir = docsDirInput;
5005
- Manifest.validateDocsDir(docsDir);
5006
- }
5007
- let frameworkPath;
5008
- let frameworkRepo;
5009
- if (options.path !== void 0) {
5010
- if (options.path) {
5011
- if (isLocalPath(options.path)) {
5012
- frameworkPath = options.path;
5013
- } else {
5014
- frameworkRepo = options.path;
5015
- }
5016
- }
5017
- } else {
5018
- const existingManifest = await this.manifestRepo.load();
5019
- const sourceDefault = existingManifest?.repo ?? this.resolver.getDefaultRepo() ?? "";
5020
- const sourceInput = options.interactive ? await this.prompter.input("Framework source (owner/repo or local path):", sourceDefault) : sourceDefault;
5021
- if (sourceInput) {
5022
- if (isLocalPath(sourceInput)) {
5023
- frameworkPath = sourceInput;
5024
- } else {
5025
- frameworkRepo = sourceInput;
5026
- }
5027
- }
5028
- }
5029
- let resolvedRelease = release;
5030
- if (!frameworkPath && !release) {
5031
- if (options.interactive) {
5032
- const latest = await this.resolver.fetchLatestVersion(frameworkRepo).catch(() => "");
5033
- resolvedRelease = await this.prompter.input(
5034
- latest ? `Framework release tag (latest: ${latest}):` : "Framework release tag:",
5035
- latest
5036
- ) || latest || void 0;
5037
- } else {
5038
- resolvedRelease = await this.resolver.fetchLatestVersion(frameworkRepo).catch(() => void 0);
5039
- }
5040
- }
5271
+ const { projectRoot, repo } = options;
5272
+ const { docsDir, explicitDocsDir } = await this.resolveDocsDir(options);
5273
+ const { frameworkPath, frameworkRepo } = await this.resolveFrameworkSource(options);
5274
+ const resolvedRelease = await this.resolveRelease(frameworkRepo, options);
5041
5275
  const resolved = await this.frameworkResolver.execute({
5042
5276
  path: frameworkPath,
5043
5277
  release: resolvedRelease
5044
5278
  });
5045
5279
  const repoForManifest = frameworkRepo ?? repo;
5046
- const initResult = await new InitUseCase(
5047
- this.fs,
5048
- this.manifestRepo,
5049
- this.loader,
5050
- this.hasher,
5051
- this.logger
5052
- ).execute({
5053
- frameworkPath: resolved.path,
5054
- version: resolved.version,
5280
+ const initResult = await this.runInit(
5281
+ resolved.path,
5282
+ resolved.version,
5055
5283
  docsDir,
5056
5284
  explicitDocsDir,
5057
5285
  projectRoot,
5058
- force: false,
5059
- repo: repoForManifest
5060
- });
5286
+ repoForManifest
5287
+ );
5061
5288
  const installResults = await this.runInstall(
5062
5289
  resolved.path,
5063
5290
  resolved.version,
@@ -5073,40 +5300,94 @@ var SetupUseCase = class {
5073
5300
  install: { results: installResults }
5074
5301
  };
5075
5302
  }
5303
+ async runInit(frameworkPath, version, docsDir, explicitDocsDir, projectRoot, repo) {
5304
+ return new InitUseCase(
5305
+ this.fs,
5306
+ this.manifestRepo,
5307
+ this.loader,
5308
+ this.hasher,
5309
+ this.logger
5310
+ ).execute({
5311
+ frameworkPath,
5312
+ version,
5313
+ docsDir,
5314
+ explicitDocsDir,
5315
+ projectRoot,
5316
+ force: false,
5317
+ repo
5318
+ });
5319
+ }
5320
+ async resolveDocsDir(options) {
5321
+ if (options.docsDir !== void 0) {
5322
+ Manifest.validateDocsDir(options.docsDir);
5323
+ return { docsDir: options.docsDir, explicitDocsDir: options.docsDir };
5324
+ }
5325
+ if (!options.interactive) {
5326
+ return { docsDir: Manifest.DEFAULT_DOCS_DIR, explicitDocsDir: "" };
5327
+ }
5328
+ const docsDirInput = await this.prompter.input(
5329
+ "Documentation directory name:",
5330
+ Manifest.DEFAULT_DOCS_DIR
5331
+ );
5332
+ const docsDir = docsDirInput || Manifest.DEFAULT_DOCS_DIR;
5333
+ Manifest.validateDocsDir(docsDir);
5334
+ return { docsDir, explicitDocsDir: docsDirInput };
5335
+ }
5336
+ async resolveFrameworkSource(options) {
5337
+ if (options.path !== void 0) {
5338
+ if (!options.path) return {};
5339
+ if (isLocalPath(options.path)) return { frameworkPath: options.path };
5340
+ return { frameworkRepo: options.path };
5341
+ }
5342
+ const existingManifest = await this.manifestRepo.load();
5343
+ const sourceDefault = existingManifest?.repo ?? this.resolver.getDefaultRepo() ?? "";
5344
+ const sourceInput = options.interactive ? await this.prompter.input("Framework source (owner/repo or local path):", sourceDefault) : sourceDefault;
5345
+ if (!sourceInput) return {};
5346
+ if (isLocalPath(sourceInput)) return { frameworkPath: sourceInput };
5347
+ return { frameworkRepo: sourceInput };
5348
+ }
5349
+ async resolveRelease(frameworkRepo, options) {
5350
+ if (options.path && isLocalPath(options.path)) return options.release;
5351
+ if (options.release) return options.release;
5352
+ if (options.interactive) {
5353
+ const latest = await this.resolver.fetchLatestVersion(frameworkRepo).catch(() => "");
5354
+ const label = latest ? `Framework release tag (latest: ${latest}):` : "Framework release tag:";
5355
+ return await this.prompter.input(label, latest) || latest || void 0;
5356
+ }
5357
+ return this.resolver.fetchLatestVersion(frameworkRepo).catch(() => void 0);
5358
+ }
5076
5359
  async handleAdopt(options) {
5077
5360
  const { projectRoot, repo } = options;
5361
+ this.validateAdoptNonInteractive(options, repo);
5362
+ const selected = await this.resolveAdoptTools(options);
5363
+ const fromInput = await this.resolveAdoptFrom(options, repo);
5364
+ const { path: frameworkPath, version } = await this.frameworkResolver.execute({
5365
+ from: fromInput
5366
+ });
5367
+ const adoptResult = await this.runAdopt(
5368
+ selected,
5369
+ frameworkPath,
5370
+ projectRoot,
5371
+ version
5372
+ );
5373
+ return {
5374
+ kind: "adopted",
5375
+ version,
5376
+ toolCount: adoptResult.tools.length,
5377
+ totalRegistered: adoptResult.totalRegistered,
5378
+ docsRegistered: adoptResult.docsRegistered
5379
+ };
5380
+ }
5381
+ validateAdoptNonInteractive(options, repo) {
5078
5382
  if (!options.interactive) {
5079
5383
  if (!options.toolIds || options.toolIds.length === 0) {
5080
5384
  throw new Error("--tools <ids> is required for adopt in non-interactive mode.");
5081
5385
  }
5082
- if (options.from === void 0) {
5083
- throw new AdoptRequiresVersionError(repo);
5084
- }
5085
- }
5086
- let selected;
5087
- if (options.toolIds !== void 0 && options.toolIds.length > 0) {
5088
- selected = options.toolIds;
5089
- } else {
5090
- const choices = VALID_TOOL_IDS.map((id) => ({ name: id, value: id, checked: false }));
5091
- const checkedIds = await this.prompter.checkbox("Which tools do you want to adopt?", choices);
5092
- if (checkedIds.length === 0) throw new Error("No tools selected.");
5093
- selected = checkedIds;
5094
- }
5095
- let fromInput;
5096
- if (options.from !== void 0) {
5097
- fromInput = options.from;
5098
- if (!fromInput) throw new AdoptRequiresVersionError(repo);
5099
- } else {
5100
- fromInput = await this.prompter.input(
5101
- "Which version of the framework do you already have installed? (e.g. v1.2.3 or local path):",
5102
- ""
5103
- );
5104
- if (!fromInput) throw new AdoptRequiresVersionError(repo);
5386
+ if (options.from === void 0) throw new AdoptRequiresVersionError(repo);
5105
5387
  }
5106
- const { path: frameworkPath, version } = await this.frameworkResolver.execute({
5107
- from: fromInput
5108
- });
5109
- const adoptResult = await new AdoptUseCase(
5388
+ }
5389
+ async runAdopt(toolIds, frameworkPath, projectRoot, version) {
5390
+ return new AdoptUseCase(
5110
5391
  this.fs,
5111
5392
  this.manifestRepo,
5112
5393
  this.loader,
@@ -5114,19 +5395,33 @@ var SetupUseCase = class {
5114
5395
  this.logger,
5115
5396
  this.platform
5116
5397
  ).execute({
5117
- toolIds: selected,
5398
+ toolIds,
5118
5399
  frameworkPath,
5119
5400
  docsDir: Manifest.DEFAULT_DOCS_DIR,
5120
5401
  projectRoot,
5121
5402
  version
5122
5403
  });
5123
- return {
5124
- kind: "adopted",
5125
- version,
5126
- toolCount: adoptResult.tools.length,
5127
- totalRegistered: adoptResult.totalRegistered,
5128
- docsRegistered: adoptResult.docsRegistered
5129
- };
5404
+ }
5405
+ async resolveAdoptTools(options) {
5406
+ if (options.toolIds !== void 0 && options.toolIds.length > 0) {
5407
+ return options.toolIds;
5408
+ }
5409
+ const choices = VALID_TOOL_IDS.map((id) => ({ name: id, value: id, checked: false }));
5410
+ const checkedIds = await this.prompter.checkbox("Which tools do you want to adopt?", choices);
5411
+ if (checkedIds.length === 0) throw new Error("No tools selected.");
5412
+ return checkedIds;
5413
+ }
5414
+ async resolveAdoptFrom(options, repo) {
5415
+ if (options.from !== void 0) {
5416
+ if (!options.from) throw new AdoptRequiresVersionError(repo);
5417
+ return options.from;
5418
+ }
5419
+ const fromInput = await this.prompter.input(
5420
+ "Which version of the framework do you already have installed? (e.g. v1.2.3 or local path):",
5421
+ ""
5422
+ );
5423
+ if (!fromInput) throw new AdoptRequiresVersionError(repo);
5424
+ return fromInput;
5130
5425
  }
5131
5426
  async handleInstall(options) {
5132
5427
  const { projectRoot, path, release, repo } = options;
@@ -5168,9 +5463,17 @@ var SetupUseCase = class {
5168
5463
  interactive: options.interactive ?? false,
5169
5464
  repo
5170
5465
  });
5171
- if (updateResult.cancelled) {
5172
- return { kind: "update-cancelled" };
5173
- }
5466
+ if (updateResult.cancelled) return { kind: "update-cancelled" };
5467
+ return this.buildUpdateResult(
5468
+ updateResult,
5469
+ frameworkPath,
5470
+ version,
5471
+ projectRoot,
5472
+ repo,
5473
+ options.interactive
5474
+ );
5475
+ }
5476
+ async buildUpdateResult(updateResult, frameworkPath, version, projectRoot, repo, interactive) {
5174
5477
  const updatedManifest = await this.manifestRepo.load();
5175
5478
  const updatedInstalledIds = updatedManifest?.getInstalledToolIds() ?? [];
5176
5479
  const missingTools = VALID_TOOL_IDS.filter((id) => !updatedInstalledIds.includes(id));
@@ -5180,7 +5483,7 @@ var SetupUseCase = class {
5180
5483
  version,
5181
5484
  projectRoot,
5182
5485
  repo,
5183
- options.interactive
5486
+ interactive
5184
5487
  );
5185
5488
  return {
5186
5489
  kind: "updated",
@@ -5196,16 +5499,13 @@ var SetupUseCase = class {
5196
5499
  const manifest = await this.manifestRepo.load();
5197
5500
  const installedIds = manifest?.getInstalledToolIds() ?? [];
5198
5501
  const missingTools = VALID_TOOL_IDS.filter((id) => !installedIds.includes(id));
5199
- if (missingTools.length === 0) {
5200
- return { kind: "up-to-date", hasAdditionalTools: false };
5201
- }
5202
- if (!options.interactive) {
5203
- return { kind: "up-to-date", hasAdditionalTools: true };
5204
- }
5502
+ if (missingTools.length === 0) return { kind: "up-to-date", hasAdditionalTools: false };
5503
+ if (!options.interactive) return { kind: "up-to-date", hasAdditionalTools: true };
5205
5504
  const wantsMore = await this.prompter.confirm("Install additional tools?");
5206
- if (!wantsMore) {
5207
- return { kind: "up-to-date", hasAdditionalTools: true };
5208
- }
5505
+ if (!wantsMore) return { kind: "up-to-date", hasAdditionalTools: true };
5506
+ return this.installAdditionalTools(path, release, repo, projectRoot);
5507
+ }
5508
+ async installAdditionalTools(path, release, repo, projectRoot) {
5209
5509
  const { path: frameworkPath, version } = await this.frameworkResolver.execute({
5210
5510
  path,
5211
5511
  release,
@@ -5264,30 +5564,6 @@ var SetupUseCase = class {
5264
5564
  });
5265
5565
  }
5266
5566
  };
5267
- async function detectSetupState(manifestRepo, fs, resolver, projectRoot) {
5268
- const manifest = await manifestRepo.load();
5269
- if (manifest === null) {
5270
- for (const tool of getAllRegisteredTools().values()) {
5271
- if (await hasToolSignals(fs, tool, projectRoot)) return { kind: "needs-adopt" };
5272
- }
5273
- return { kind: "needs-init" };
5274
- }
5275
- const installedIds = manifest.getInstalledToolIds();
5276
- if (installedIds.length === 0) {
5277
- return { kind: "needs-install" };
5278
- }
5279
- try {
5280
- const latestVersion = await resolver.fetchLatestVersion(manifest.repo);
5281
- const installedVersions = installedIds.map((id) => manifest.getToolVersion(id)).filter((v) => v !== void 0);
5282
- const currentVersion2 = installedVersions[0] ?? "unknown";
5283
- const needsUpdate = isSemver(latestVersion) && installedVersions.some((v) => !isSemver(v) || compareSemver(v, latestVersion) < 0);
5284
- if (needsUpdate) {
5285
- return { kind: "needs-update", currentVersion: currentVersion2, latestVersion };
5286
- }
5287
- } catch {
5288
- }
5289
- return { kind: "up-to-date" };
5290
- }
5291
5567
 
5292
5568
  // src/application/commands/setup.ts
5293
5569
  function displayInstall(output, results, verbose) {
@@ -5427,7 +5703,7 @@ function registerStatusCommand(program2) {
5427
5703
  filterDocs,
5428
5704
  repo
5429
5705
  });
5430
- if (report.tools.length === 0 && !filterToolId) {
5706
+ if (report.tools.length === 0 && !filterToolId && !filterDocs) {
5431
5707
  output.print("No tools installed. Run `aidd install <tool>` to get started.");
5432
5708
  if (report.inSync) return;
5433
5709
  } else if (report.inSync) {
@@ -5460,6 +5736,27 @@ docs (v${report.docs.version}):`);
5460
5736
 
5461
5737
  // src/application/use-cases/sync-use-case.ts
5462
5738
  import { join as join26 } from "path";
5739
+
5740
+ // src/domain/models/sync-exclusions.ts
5741
+ var SYNC_EXCLUDED_FILES = /* @__PURE__ */ new Set([
5742
+ "CLAUDE.md",
5743
+ "AGENTS.md",
5744
+ ".github/copilot-instructions.md",
5745
+ ".mcp.json",
5746
+ ".cursor/mcp.json",
5747
+ ".vscode/mcp.json",
5748
+ "opencode.json",
5749
+ "opencode.jsonc"
5750
+ ]);
5751
+ function isSyncExcluded(relativePath, docsDir) {
5752
+ if (SYNC_EXCLUDED_FILES.has(relativePath)) return true;
5753
+ if (relativePath.startsWith(".vscode/")) return true;
5754
+ if (relativePath.startsWith(`${docsDir}/`)) return true;
5755
+ if (relativePath.startsWith(".aidd/")) return true;
5756
+ return false;
5757
+ }
5758
+
5759
+ // src/application/use-cases/sync-use-case.ts
5463
5760
  function getSectionKeyFromFrameworkPath(frameworkPath) {
5464
5761
  if (frameworkPath.startsWith("agents/"))
5465
5762
  return { section: "agents", key: frameworkPath.slice("agents/".length) };
@@ -5482,23 +5779,6 @@ function transformContent(content, sourceConfig, targetConfig, sectionKey, docsD
5482
5779
  const targetBody = targetConfig.rewriteContent(canonicalBody, docsDir);
5483
5780
  return serializeFrontmatter(targetFrontmatter, targetBody);
5484
5781
  }
5485
- var EXCLUDED_FILES = /* @__PURE__ */ new Set([
5486
- "CLAUDE.md",
5487
- "AGENTS.md",
5488
- ".github/copilot-instructions.md",
5489
- ".mcp.json",
5490
- ".cursor/mcp.json",
5491
- ".vscode/mcp.json",
5492
- "opencode.json",
5493
- "opencode.jsonc"
5494
- ]);
5495
- function isExcluded(relativePath, docsDir) {
5496
- if (EXCLUDED_FILES.has(relativePath)) return true;
5497
- if (relativePath.startsWith(".vscode/")) return true;
5498
- if (relativePath.startsWith(`${docsDir}/`)) return true;
5499
- if (relativePath.startsWith(".aidd/")) return true;
5500
- return false;
5501
- }
5502
5782
  function buildTargetPath(targetConfig, sectionKey) {
5503
5783
  return targetConfig[sectionKey.section]().buildFilePath(sectionKey.key);
5504
5784
  }
@@ -5512,143 +5792,166 @@ var SyncUseCase = class {
5512
5792
  }
5513
5793
  async execute(options) {
5514
5794
  const { projectRoot, force = false, includeUserFiles = false, repo } = options;
5515
- const interactive = options.interactive ?? false;
5516
5795
  const manifest = await this.manifestRepo.load();
5517
- if (manifest === null) {
5518
- throw new NoManifestError(repo);
5519
- }
5796
+ if (manifest === null) throw new NoManifestError(repo);
5520
5797
  const docsDir = options.docsDir ?? manifest.docsDir;
5521
- let sourceTool;
5522
- let targetTools = options.targetTools;
5523
- if (options.sourceTool === void 0) {
5524
- if (!interactive || this.prompter === void 0) {
5525
- throw new Error("Source tool required in non-interactive mode.");
5526
- }
5527
- const { SyncStatusUseCase: SyncStatusUseCase2 } = await Promise.resolve().then(() => (init_sync_status_use_case(), sync_status_use_case_exports));
5528
- const installedIds = manifest.getInstalledToolIds();
5529
- if (installedIds.length < 2) {
5530
- throw new Error("Sync requires at least 2 installed tools.");
5531
- }
5532
- const modCounts = await new SyncStatusUseCase2(this.fs).execute(
5533
- manifest,
5534
- installedIds,
5535
- projectRoot
5536
- );
5537
- const hasAnyChanges = installedIds.some((id) => {
5538
- const { modified, deleted } = modCounts[id] ?? { modified: 0, deleted: 0 };
5539
- return modified > 0 || deleted > 0;
5540
- });
5541
- if (!hasAnyChanges) {
5542
- return {
5543
- sourceTool: installedIds[0],
5544
- tools: [],
5545
- totalWritten: 0,
5546
- totalDeleted: 0,
5547
- totalConflicts: 0,
5548
- totalSkipped: 0
5549
- };
5550
- }
5551
- const sourceChoices = installedIds.map((id) => {
5552
- const { modified, deleted } = modCounts[id] ?? { modified: 0, deleted: 0 };
5553
- const hasChanges = modified > 0 || deleted > 0;
5554
- const parts = [];
5555
- if (modified > 0) parts.push(`${modified} modified`);
5556
- if (deleted > 0) parts.push(`${deleted} deleted`);
5557
- const label = hasChanges ? ` (${parts.join(", ")})` : "";
5558
- return {
5559
- name: `${id}${label}`,
5560
- value: id,
5561
- disabled: hasChanges ? false : "(no changes)"
5562
- };
5563
- });
5564
- sourceTool = await this.prompter.select("Source tool to sync from?", sourceChoices);
5565
- const targetChoices = installedIds.filter((id) => id !== sourceTool).map((id) => ({ name: id, value: id }));
5566
- targetTools = await this.prompter.checkbox("Target tools?", targetChoices);
5567
- if (targetTools.length === 0) throw new Error("No target tools selected.");
5568
- } else {
5569
- sourceTool = options.sourceTool;
5570
- if (!manifest.hasTool(sourceTool)) {
5571
- throw new Error(`Source tool '${sourceTool}' is not installed.`);
5572
- }
5573
- const installedToolIds = manifest.getInstalledToolIds();
5574
- if (installedToolIds.length < 2) {
5575
- throw new Error("Sync requires at least 2 installed tools.");
5576
- }
5577
- targetTools = targetTools && targetTools.length > 0 ? targetTools : installedToolIds.filter((id) => id !== sourceTool);
5578
- for (const target of targetTools) {
5579
- if (target === sourceTool) {
5580
- throw new Error("Source and target cannot be the same tool.");
5581
- }
5582
- if (!manifest.hasTool(target)) {
5583
- throw new Error(`Target tool '${target}' is not installed.`);
5584
- }
5585
- }
5586
- }
5798
+ const { sourceTool, targetTools } = await this.selectSyncScope(
5799
+ options,
5800
+ manifest,
5801
+ options.interactive ?? false
5802
+ );
5587
5803
  const sourceConfig = getToolConfig(sourceTool);
5588
5804
  const sourceManifestFiles = manifest.getToolFiles(sourceTool);
5589
5805
  const sourceManifestMap = new Map(sourceManifestFiles.map((f) => [f.relativePath, f.hash]));
5806
+ const toolResults = await this.syncAllTargets(
5807
+ targetTools,
5808
+ sourceTool,
5809
+ sourceConfig,
5810
+ sourceManifestFiles,
5811
+ sourceManifestMap,
5812
+ manifest,
5813
+ projectRoot,
5814
+ docsDir,
5815
+ force,
5816
+ includeUserFiles
5817
+ );
5818
+ return this.buildSyncTotals(sourceTool, toolResults);
5819
+ }
5820
+ async syncAllTargets(targetTools, sourceTool, sourceConfig, sourceManifestFiles, sourceManifestMap, manifest, projectRoot, docsDir, force, includeUserFiles) {
5590
5821
  const toolResults = [];
5591
- for (const targetToolId of targetTools ?? []) {
5822
+ for (const targetToolId of targetTools) {
5592
5823
  this.logger.info(`Syncing ${sourceTool} \u2192 ${targetToolId}...`);
5593
- const targetConfig = getToolConfig(targetToolId);
5594
- const targetManifestFiles = manifest.getToolFiles(targetToolId);
5595
- const targetManifestMap = new Map(targetManifestFiles.map((f) => [f.relativePath, f.hash]));
5596
- const targetByFrameworkPath = new Map(
5597
- targetManifestFiles.filter((f) => f.frameworkPath !== void 0).map((f) => [f.frameworkPath, f.relativePath])
5598
- );
5599
- const fileResults = [];
5600
- await this.propagateModified({
5601
- sourceManifestFiles,
5824
+ const result = await this.syncOneTool(
5825
+ targetToolId,
5602
5826
  sourceConfig,
5603
- targetConfig,
5604
- targetManifestMap,
5605
- targetByFrameworkPath,
5606
- fileResults,
5607
- projectRoot,
5608
- docsDir,
5609
- force
5610
- });
5611
- await this.propagateAdded({
5827
+ sourceManifestFiles,
5612
5828
  sourceManifestMap,
5613
- sourceConfig,
5614
- targetConfig,
5615
- fileResults,
5829
+ manifest,
5616
5830
  projectRoot,
5617
5831
  docsDir,
5832
+ force,
5618
5833
  includeUserFiles
5619
- });
5620
- await this.propagateDeleted({
5621
- sourceManifestFiles,
5622
- targetByFrameworkPath,
5623
- fileResults,
5624
- projectRoot,
5625
- docsDir
5626
- });
5627
- toolResults.push({ targetToolId, files: fileResults });
5834
+ );
5835
+ toolResults.push(result);
5628
5836
  }
5629
- const totalWritten = toolResults.reduce(
5630
- (s, t) => s + t.files.filter((f) => f.written).length,
5631
- 0
5632
- );
5633
- const totalDeleted = toolResults.reduce(
5634
- (s, t) => s + t.files.filter((f) => f.deleted).length,
5635
- 0
5636
- );
5637
- const totalConflicts = toolResults.reduce(
5638
- (s, t) => s + t.files.filter((f) => f.conflict && !f.written).length,
5639
- 0
5837
+ return toolResults;
5838
+ }
5839
+ async syncOneTool(targetToolId, sourceConfig, sourceManifestFiles, sourceManifestMap, manifest, projectRoot, docsDir, force, includeUserFiles) {
5840
+ const targetConfig = getToolConfig(targetToolId);
5841
+ const targetManifestFiles = manifest.getToolFiles(targetToolId);
5842
+ const targetManifestMap = new Map(targetManifestFiles.map((f) => [f.relativePath, f.hash]));
5843
+ const targetByFrameworkPath = new Map(
5844
+ targetManifestFiles.filter((f) => f.frameworkPath !== void 0).map((f) => [f.frameworkPath, f.relativePath])
5640
5845
  );
5641
- const totalSkipped = toolResults.reduce(
5642
- (s, t) => s + t.files.filter((f) => f.skipped).length,
5643
- 0
5846
+ const fileResults = [];
5847
+ await this.propagateModified({
5848
+ sourceManifestFiles,
5849
+ sourceConfig,
5850
+ targetConfig,
5851
+ targetManifestMap,
5852
+ targetByFrameworkPath,
5853
+ fileResults,
5854
+ projectRoot,
5855
+ docsDir,
5856
+ force
5857
+ });
5858
+ await this.propagateAdded({
5859
+ sourceManifestMap,
5860
+ sourceConfig,
5861
+ targetConfig,
5862
+ fileResults,
5863
+ projectRoot,
5864
+ docsDir,
5865
+ includeUserFiles
5866
+ });
5867
+ await this.propagateDeleted({
5868
+ sourceManifestFiles,
5869
+ targetByFrameworkPath,
5870
+ fileResults,
5871
+ projectRoot,
5872
+ docsDir
5873
+ });
5874
+ return { targetToolId, files: fileResults };
5875
+ }
5876
+ /** Resolves sourceTool and targetTools from options — prompts if interactive. */
5877
+ async selectSyncScope(options, manifest, interactive) {
5878
+ if (options.sourceTool !== void 0) {
5879
+ return this.resolveExplicitScope(options, manifest);
5880
+ }
5881
+ return this.resolveInteractiveScope(manifest, interactive, options.projectRoot);
5882
+ }
5883
+ resolveExplicitScope(options, manifest) {
5884
+ const sourceTool = options.sourceTool;
5885
+ if (!manifest.hasTool(sourceTool)) {
5886
+ throw new Error(`Source tool '${sourceTool}' is not installed.`);
5887
+ }
5888
+ const installedToolIds = manifest.getInstalledToolIds();
5889
+ if (installedToolIds.length < 2) {
5890
+ throw new Error("Sync requires at least 2 installed tools.");
5891
+ }
5892
+ const targetTools = options.targetTools && options.targetTools.length > 0 ? options.targetTools : installedToolIds.filter((id) => id !== sourceTool);
5893
+ for (const target of targetTools) {
5894
+ if (target === sourceTool) {
5895
+ throw new Error("Source and target cannot be the same tool.");
5896
+ }
5897
+ if (!manifest.hasTool(target)) {
5898
+ throw new Error(`Target tool '${target}' is not installed.`);
5899
+ }
5900
+ }
5901
+ return { sourceTool, targetTools };
5902
+ }
5903
+ async resolveInteractiveScope(manifest, interactive, projectRoot) {
5904
+ if (!interactive || this.prompter === void 0) {
5905
+ throw new Error("Source tool required in non-interactive mode.");
5906
+ }
5907
+ const { SyncStatusUseCase: SyncStatusUseCase2 } = await Promise.resolve().then(() => (init_sync_status_use_case(), sync_status_use_case_exports));
5908
+ const installedIds = manifest.getInstalledToolIds();
5909
+ if (installedIds.length < 2) {
5910
+ throw new Error("Sync requires at least 2 installed tools.");
5911
+ }
5912
+ const modCounts = await new SyncStatusUseCase2(this.fs).execute(
5913
+ manifest,
5914
+ installedIds,
5915
+ projectRoot
5644
5916
  );
5917
+ const hasAnyChanges = installedIds.some((id) => {
5918
+ const { modified, deleted } = modCounts[id] ?? { modified: 0, deleted: 0 };
5919
+ return modified > 0 || deleted > 0;
5920
+ });
5921
+ if (!hasAnyChanges) {
5922
+ return { sourceTool: installedIds[0], targetTools: [] };
5923
+ }
5924
+ return this.promptSyncScope(installedIds, modCounts, this.prompter);
5925
+ }
5926
+ async promptSyncScope(installedIds, modCounts, prompter) {
5927
+ const sourceChoices = installedIds.map((id) => {
5928
+ const { modified, deleted } = modCounts[id] ?? { modified: 0, deleted: 0 };
5929
+ const hasChanges = modified > 0 || deleted > 0;
5930
+ const parts = [];
5931
+ if (modified > 0) parts.push(`${modified} modified`);
5932
+ if (deleted > 0) parts.push(`${deleted} deleted`);
5933
+ const label = hasChanges ? ` (${parts.join(", ")})` : "";
5934
+ return {
5935
+ name: `${id}${label}`,
5936
+ value: id,
5937
+ disabled: hasChanges ? false : "(no changes)"
5938
+ };
5939
+ });
5940
+ const sourceTool = await prompter.select("Source tool to sync from?", sourceChoices);
5941
+ const targetChoices = installedIds.filter((id) => id !== sourceTool).map((id) => ({ name: id, value: id }));
5942
+ const targetTools = await prompter.checkbox("Target tools?", targetChoices);
5943
+ if (targetTools.length === 0) throw new Error("No target tools selected.");
5944
+ return { sourceTool, targetTools };
5945
+ }
5946
+ buildSyncTotals(sourceTool, toolResults) {
5947
+ const count = (pred) => toolResults.reduce((s, t) => s + t.files.filter(pred).length, 0);
5645
5948
  return {
5646
5949
  sourceTool,
5647
5950
  tools: toolResults,
5648
- totalWritten,
5649
- totalDeleted,
5650
- totalConflicts,
5651
- totalSkipped
5951
+ totalWritten: count((f) => f.written),
5952
+ totalDeleted: count((f) => Boolean(f.deleted)),
5953
+ totalConflicts: count((f) => f.conflict && !f.written),
5954
+ totalSkipped: count((f) => f.skipped)
5652
5955
  };
5653
5956
  }
5654
5957
  async propagateModified(ctx) {
@@ -5664,64 +5967,71 @@ var SyncUseCase = class {
5664
5967
  force
5665
5968
  } = ctx;
5666
5969
  for (const sourceManifestFile of sourceManifestFiles) {
5667
- const { relativePath, hash: manifestHash, frameworkPath } = sourceManifestFile;
5668
- if (isExcluded(relativePath, docsDir)) continue;
5669
- if (frameworkPath === void 0) continue;
5670
- const diskSourcePath = join26(projectRoot, relativePath);
5671
- if (!await this.fs.fileExists(diskSourcePath)) continue;
5672
- const diskSourceHash = await this.fs.readFileHash(diskSourcePath);
5673
- if (diskSourceHash.value === manifestHash.value) continue;
5674
- const sectionKey = getSectionKeyFromFrameworkPath(frameworkPath);
5675
- if (sectionKey === null) continue;
5676
- const targetRelativePath = targetByFrameworkPath.get(frameworkPath);
5677
- if (targetRelativePath === void 0) continue;
5678
- const diskSourceContent = await this.fs.readFile(diskSourcePath);
5679
- const targetContent = transformContent(
5680
- diskSourceContent,
5970
+ await this.propagateOneModified(
5971
+ sourceManifestFile,
5681
5972
  sourceConfig,
5682
5973
  targetConfig,
5683
- sectionKey,
5684
- docsDir
5974
+ targetManifestMap,
5975
+ targetByFrameworkPath,
5976
+ fileResults,
5977
+ projectRoot,
5978
+ docsDir,
5979
+ force
5685
5980
  );
5686
- const diskTargetPath = join26(projectRoot, targetRelativePath);
5687
- const diskTargetExists = await this.fs.fileExists(diskTargetPath);
5688
- let conflict = false;
5689
- if (diskTargetExists) {
5690
- const diskTargetHash = await this.fs.readFileHash(diskTargetPath);
5691
- const targetManifestHash = targetManifestMap.get(targetRelativePath);
5692
- if (targetManifestHash !== void 0 && diskTargetHash.value !== targetManifestHash.value) {
5693
- conflict = true;
5694
- }
5695
- }
5696
- if (diskTargetExists) {
5697
- const currentTargetContent = await this.fs.readFile(diskTargetPath);
5698
- if (currentTargetContent === targetContent) {
5699
- fileResults.push({
5700
- relativePath: targetRelativePath,
5701
- conflict: false,
5702
- skipped: true,
5703
- written: false
5704
- });
5705
- continue;
5706
- }
5707
- }
5708
- if (conflict && !force) {
5709
- fileResults.push({
5710
- relativePath: targetRelativePath,
5711
- conflict: true,
5712
- skipped: false,
5713
- written: false
5714
- });
5715
- continue;
5716
- }
5717
- await this.fs.writeFile(diskTargetPath, targetContent);
5981
+ }
5982
+ }
5983
+ async propagateOneModified(sourceManifestFile, sourceConfig, targetConfig, targetManifestMap, targetByFrameworkPath, fileResults, projectRoot, docsDir, force) {
5984
+ const { relativePath, hash: manifestHash, frameworkPath } = sourceManifestFile;
5985
+ if (isSyncExcluded(relativePath, docsDir) || frameworkPath === void 0) return;
5986
+ const diskSourcePath = join26(projectRoot, relativePath);
5987
+ if (!await this.fs.fileExists(diskSourcePath)) return;
5988
+ const diskSourceHash = await this.fs.readFileHash(diskSourcePath);
5989
+ if (diskSourceHash.value === manifestHash.value) return;
5990
+ const sectionKey = getSectionKeyFromFrameworkPath(frameworkPath);
5991
+ const targetRelativePath = targetByFrameworkPath.get(frameworkPath);
5992
+ if (sectionKey === null || targetRelativePath === void 0) return;
5993
+ const diskSourceContent = await this.fs.readFile(diskSourcePath);
5994
+ const targetContent = transformContent(
5995
+ diskSourceContent,
5996
+ sourceConfig,
5997
+ targetConfig,
5998
+ sectionKey,
5999
+ docsDir
6000
+ );
6001
+ const diskTargetPath = join26(projectRoot, targetRelativePath);
6002
+ const diskTargetExists = await this.fs.fileExists(diskTargetPath);
6003
+ const conflict = await this.detectTargetConflict(
6004
+ diskTargetExists,
6005
+ diskTargetPath,
6006
+ targetRelativePath,
6007
+ targetManifestMap
6008
+ );
6009
+ if (diskTargetExists && await this.fs.readFile(diskTargetPath) === targetContent) {
5718
6010
  fileResults.push({
5719
6011
  relativePath: targetRelativePath,
5720
- conflict,
6012
+ conflict: false,
6013
+ skipped: true,
6014
+ written: false
6015
+ });
6016
+ return;
6017
+ }
6018
+ if (conflict && !force) {
6019
+ fileResults.push({
6020
+ relativePath: targetRelativePath,
6021
+ conflict: true,
5721
6022
  skipped: false,
5722
- written: true
6023
+ written: false
5723
6024
  });
6025
+ return;
5724
6026
  }
6027
+ await this.fs.writeFile(diskTargetPath, targetContent);
6028
+ fileResults.push({ relativePath: targetRelativePath, conflict, skipped: false, written: true });
6029
+ }
6030
+ async detectTargetConflict(diskTargetExists, diskTargetPath, targetRelativePath, targetManifestMap) {
6031
+ if (!diskTargetExists) return false;
6032
+ const diskTargetHash = await this.fs.readFileHash(diskTargetPath);
6033
+ const targetManifestHash = targetManifestMap.get(targetRelativePath);
6034
+ return targetManifestHash !== void 0 && diskTargetHash.value !== targetManifestHash.value;
5725
6035
  }
5726
6036
  async propagateAdded(ctx) {
5727
6037
  const {
@@ -5738,65 +6048,80 @@ var SyncUseCase = class {
5738
6048
  if (!sourceDirExists) return;
5739
6049
  const sourceDiskFiles = await this.fs.listDirectory(join26(projectRoot, sourceConfig.directory));
5740
6050
  for (const diskRelative of sourceDiskFiles) {
5741
- const sourceRelativePath = `${sourceConfig.directory}${diskRelative}`;
5742
- if (isExcluded(sourceRelativePath, docsDir)) continue;
5743
- if (sourceManifestMap.has(sourceRelativePath)) continue;
5744
- const sectionKey = sourceConfig.detectUserFileSectionKey(sourceRelativePath);
5745
- if (sectionKey === null) continue;
5746
- const targetRelativePath = buildTargetPath(targetConfig, sectionKey);
5747
- if (targetRelativePath === null) continue;
5748
- const diskSourceContent = await this.fs.readFile(join26(projectRoot, sourceRelativePath));
5749
- const targetContent = transformContent(
5750
- diskSourceContent,
6051
+ await this.propagateOneAdded(
6052
+ diskRelative,
6053
+ sourceManifestMap,
5751
6054
  sourceConfig,
5752
6055
  targetConfig,
5753
- sectionKey,
6056
+ fileResults,
6057
+ projectRoot,
5754
6058
  docsDir
5755
6059
  );
5756
- const diskTargetPath = join26(projectRoot, targetRelativePath);
5757
- const diskTargetExists = await this.fs.fileExists(diskTargetPath);
5758
- if (diskTargetExists) {
5759
- const current = await this.fs.readFile(diskTargetPath);
5760
- if (current === targetContent) {
5761
- fileResults.push({
5762
- relativePath: targetRelativePath,
5763
- conflict: false,
5764
- skipped: true,
5765
- written: false
5766
- });
5767
- continue;
5768
- }
5769
- }
5770
- await this.fs.writeFile(diskTargetPath, targetContent);
6060
+ }
6061
+ }
6062
+ async propagateOneAdded(diskRelative, sourceManifestMap, sourceConfig, targetConfig, fileResults, projectRoot, docsDir) {
6063
+ const sourceRelativePath = `${sourceConfig.directory}${diskRelative}`;
6064
+ if (isSyncExcluded(sourceRelativePath, docsDir) || sourceManifestMap.has(sourceRelativePath))
6065
+ return;
6066
+ const sectionKey = sourceConfig.detectUserFileSectionKey(sourceRelativePath);
6067
+ if (sectionKey === null) return;
6068
+ const targetRelativePath = buildTargetPath(targetConfig, sectionKey);
6069
+ if (targetRelativePath === null) return;
6070
+ const diskSourceContent = await this.fs.readFile(join26(projectRoot, sourceRelativePath));
6071
+ const targetContent = transformContent(
6072
+ diskSourceContent,
6073
+ sourceConfig,
6074
+ targetConfig,
6075
+ sectionKey,
6076
+ docsDir
6077
+ );
6078
+ const diskTargetPath = join26(projectRoot, targetRelativePath);
6079
+ if (await this.fs.fileExists(diskTargetPath) && await this.fs.readFile(diskTargetPath) === targetContent) {
5771
6080
  fileResults.push({
5772
6081
  relativePath: targetRelativePath,
5773
6082
  conflict: false,
5774
- skipped: false,
5775
- written: true
6083
+ skipped: true,
6084
+ written: false
5776
6085
  });
6086
+ return;
5777
6087
  }
6088
+ await this.fs.writeFile(diskTargetPath, targetContent);
6089
+ fileResults.push({
6090
+ relativePath: targetRelativePath,
6091
+ conflict: false,
6092
+ skipped: false,
6093
+ written: true
6094
+ });
5778
6095
  }
5779
6096
  async propagateDeleted(ctx) {
5780
6097
  const { sourceManifestFiles, targetByFrameworkPath, fileResults, projectRoot, docsDir } = ctx;
5781
6098
  for (const sourceManifestFile of sourceManifestFiles) {
5782
- const { relativePath, frameworkPath } = sourceManifestFile;
5783
- if (isExcluded(relativePath, docsDir)) continue;
5784
- if (frameworkPath === void 0) continue;
5785
- if (await this.fs.fileExists(join26(projectRoot, relativePath))) continue;
5786
- const targetRelativePath = targetByFrameworkPath.get(frameworkPath);
5787
- if (targetRelativePath === void 0) continue;
5788
- const diskTargetPath = join26(projectRoot, targetRelativePath);
5789
- if (!await this.fs.fileExists(diskTargetPath)) continue;
5790
- await this.fs.deleteFile(diskTargetPath);
5791
- fileResults.push({
5792
- relativePath: targetRelativePath,
5793
- conflict: false,
5794
- skipped: false,
5795
- written: false,
5796
- deleted: true
5797
- });
6099
+ await this.propagateOneDeleted(
6100
+ sourceManifestFile,
6101
+ targetByFrameworkPath,
6102
+ fileResults,
6103
+ projectRoot,
6104
+ docsDir
6105
+ );
5798
6106
  }
5799
6107
  }
6108
+ async propagateOneDeleted(sourceManifestFile, targetByFrameworkPath, fileResults, projectRoot, docsDir) {
6109
+ const { relativePath, frameworkPath } = sourceManifestFile;
6110
+ if (isSyncExcluded(relativePath, docsDir) || frameworkPath === void 0) return;
6111
+ if (await this.fs.fileExists(join26(projectRoot, relativePath))) return;
6112
+ const targetRelativePath = targetByFrameworkPath.get(frameworkPath);
6113
+ if (targetRelativePath === void 0) return;
6114
+ const diskTargetPath = join26(projectRoot, targetRelativePath);
6115
+ if (!await this.fs.fileExists(diskTargetPath)) return;
6116
+ await this.fs.deleteFile(diskTargetPath);
6117
+ fileResults.push({
6118
+ relativePath: targetRelativePath,
6119
+ conflict: false,
6120
+ skipped: false,
6121
+ written: false,
6122
+ deleted: true
6123
+ });
6124
+ }
5800
6125
  };
5801
6126
 
5802
6127
  // src/application/commands/sync.ts