@detergent-software/atk 8.1.2-dev.9 → 8.1.3-dev.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 (139) hide show
  1. package/CHANGELOG.md +341 -0
  2. package/build/commands/audit.d.ts.map +1 -1
  3. package/build/commands/audit.js +14 -9
  4. package/build/commands/audit.js.map +1 -1
  5. package/build/commands/browse.js +21 -29
  6. package/build/commands/browse.js.map +1 -1
  7. package/build/commands/info.js +2 -2
  8. package/build/commands/info.js.map +1 -1
  9. package/build/commands/install.d.ts.map +1 -1
  10. package/build/commands/install.js +35 -75
  11. package/build/commands/install.js.map +1 -1
  12. package/build/commands/list.js +2 -2
  13. package/build/commands/list.js.map +1 -1
  14. package/build/commands/prune.d.ts.map +1 -1
  15. package/build/commands/prune.js +10 -8
  16. package/build/commands/prune.js.map +1 -1
  17. package/build/commands/sync.d.ts.map +1 -1
  18. package/build/commands/sync.js +10 -9
  19. package/build/commands/sync.js.map +1 -1
  20. package/build/commands/uninstall.d.ts +9 -1
  21. package/build/commands/uninstall.d.ts.map +1 -1
  22. package/build/commands/uninstall.js +40 -35
  23. package/build/commands/uninstall.js.map +1 -1
  24. package/build/commands/update.d.ts +9 -1
  25. package/build/commands/update.d.ts.map +1 -1
  26. package/build/commands/update.js +46 -16
  27. package/build/commands/update.js.map +1 -1
  28. package/build/commands/why.d.ts.map +1 -1
  29. package/build/commands/why.js +13 -5
  30. package/build/commands/why.js.map +1 -1
  31. package/build/components/AssetDetail.js +3 -1
  32. package/build/components/AssetDetail.js.map +1 -1
  33. package/build/components/AssetTable.d.ts.map +1 -1
  34. package/build/components/AssetTable.js +7 -3
  35. package/build/components/AssetTable.js.map +1 -1
  36. package/build/components/DependencyTree.d.ts +1 -1
  37. package/build/components/DependencyTree.d.ts.map +1 -1
  38. package/build/components/DependencyTree.js +4 -2
  39. package/build/components/DependencyTree.js.map +1 -1
  40. package/build/components/InstallSummary.d.ts +1 -0
  41. package/build/components/InstallSummary.d.ts.map +1 -1
  42. package/build/components/InstallSummary.js +3 -1
  43. package/build/components/InstallSummary.js.map +1 -1
  44. package/build/components/ListBrowseList.d.ts.map +1 -1
  45. package/build/components/ListBrowseList.js +4 -2
  46. package/build/components/ListBrowseList.js.map +1 -1
  47. package/build/components/OrphanListItem.d.ts +3 -1
  48. package/build/components/OrphanListItem.d.ts.map +1 -1
  49. package/build/components/OrphanListItem.js +3 -2
  50. package/build/components/OrphanListItem.js.map +1 -1
  51. package/build/components/SkippedAssetSection.d.ts +2 -0
  52. package/build/components/SkippedAssetSection.d.ts.map +1 -1
  53. package/build/components/SkippedAssetSection.js +1 -1
  54. package/build/components/SkippedAssetSection.js.map +1 -1
  55. package/build/components/SyncDriftedList.d.ts +3 -2
  56. package/build/components/SyncDriftedList.d.ts.map +1 -1
  57. package/build/components/SyncDriftedList.js +13 -3
  58. package/build/components/SyncDriftedList.js.map +1 -1
  59. package/build/hooks/useListActions.d.ts.map +1 -1
  60. package/build/hooks/useListActions.js +33 -16
  61. package/build/hooks/useListActions.js.map +1 -1
  62. package/build/hooks/usePruneState.d.ts +13 -15
  63. package/build/hooks/usePruneState.d.ts.map +1 -1
  64. package/build/hooks/usePruneState.js +10 -6
  65. package/build/hooks/usePruneState.js.map +1 -1
  66. package/build/hooks/useUninstallState.d.ts +16 -29
  67. package/build/hooks/useUninstallState.d.ts.map +1 -1
  68. package/build/hooks/useUninstallState.js +146 -139
  69. package/build/hooks/useUninstallState.js.map +1 -1
  70. package/build/lib/adapter.js +1 -1
  71. package/build/lib/adapter.js.map +1 -1
  72. package/build/lib/asset-tools.d.ts +5 -0
  73. package/build/lib/asset-tools.d.ts.map +1 -1
  74. package/build/lib/asset-tools.js +7 -0
  75. package/build/lib/asset-tools.js.map +1 -1
  76. package/build/lib/atk-api/index.d.ts +1 -1
  77. package/build/lib/atk-api/index.d.ts.map +1 -1
  78. package/build/lib/atk-api/index.js +1 -1
  79. package/build/lib/atk-api/index.js.map +1 -1
  80. package/build/lib/atk-api/web-routes.d.ts +5 -1
  81. package/build/lib/atk-api/web-routes.d.ts.map +1 -1
  82. package/build/lib/atk-api/web-routes.js +7 -1
  83. package/build/lib/atk-api/web-routes.js.map +1 -1
  84. package/build/lib/diagnostics.d.ts.map +1 -1
  85. package/build/lib/diagnostics.js +6 -5
  86. package/build/lib/diagnostics.js.map +1 -1
  87. package/build/lib/diff-command.d.ts.map +1 -1
  88. package/build/lib/diff-command.js +10 -7
  89. package/build/lib/diff-command.js.map +1 -1
  90. package/build/lib/enabled-list.d.ts +15 -0
  91. package/build/lib/enabled-list.d.ts.map +1 -0
  92. package/build/lib/enabled-list.js +83 -0
  93. package/build/lib/enabled-list.js.map +1 -0
  94. package/build/lib/file-snapshot.d.ts +17 -0
  95. package/build/lib/file-snapshot.d.ts.map +1 -0
  96. package/build/lib/file-snapshot.js +41 -0
  97. package/build/lib/file-snapshot.js.map +1 -0
  98. package/build/lib/installed-for-tool.d.ts +8 -0
  99. package/build/lib/installed-for-tool.d.ts.map +1 -0
  100. package/build/lib/installed-for-tool.js +18 -0
  101. package/build/lib/installed-for-tool.js.map +1 -0
  102. package/build/lib/installer.d.ts +109 -4
  103. package/build/lib/installer.d.ts.map +1 -1
  104. package/build/lib/installer.js +546 -162
  105. package/build/lib/installer.js.map +1 -1
  106. package/build/lib/lockfile.d.ts +30 -4
  107. package/build/lib/lockfile.d.ts.map +1 -1
  108. package/build/lib/lockfile.js +35 -15
  109. package/build/lib/lockfile.js.map +1 -1
  110. package/build/lib/multi-tool-install.d.ts +23 -0
  111. package/build/lib/multi-tool-install.d.ts.map +1 -0
  112. package/build/lib/multi-tool-install.js +78 -0
  113. package/build/lib/multi-tool-install.js.map +1 -0
  114. package/build/lib/org.d.ts +16 -0
  115. package/build/lib/org.d.ts.map +1 -1
  116. package/build/lib/org.js +18 -0
  117. package/build/lib/org.js.map +1 -1
  118. package/build/lib/owned-paths.d.ts +68 -0
  119. package/build/lib/owned-paths.d.ts.map +1 -0
  120. package/build/lib/owned-paths.js +162 -0
  121. package/build/lib/owned-paths.js.map +1 -0
  122. package/build/lib/registry.js +6 -2
  123. package/build/lib/registry.js.map +1 -1
  124. package/build/lib/resolver.d.ts.map +1 -1
  125. package/build/lib/resolver.js +35 -13
  126. package/build/lib/resolver.js.map +1 -1
  127. package/build/lib/sync.d.ts +8 -1
  128. package/build/lib/sync.d.ts.map +1 -1
  129. package/build/lib/sync.js +81 -28
  130. package/build/lib/sync.js.map +1 -1
  131. package/build/lib/uninstaller.d.ts +28 -4
  132. package/build/lib/uninstaller.d.ts.map +1 -1
  133. package/build/lib/uninstaller.js +95 -97
  134. package/build/lib/uninstaller.js.map +1 -1
  135. package/build/lib/updater.d.ts +59 -3
  136. package/build/lib/updater.d.ts.map +1 -1
  137. package/build/lib/updater.js +353 -80
  138. package/build/lib/updater.js.map +1 -1
  139. package/package.json +2 -2
@@ -1,9 +1,14 @@
1
+ import { join } from 'node:path';
1
2
  import { compare, valid } from 'semver';
2
- import { isExcludedForTool } from './asset-tools.js';
3
+ import { getPlacement, resolveInstalledPaths } from './adapter.js';
4
+ import { isExcludedForTool, supportsType } from './asset-tools.js';
3
5
  import { getCredential } from './auth.js';
6
+ import { getConfigEntriesForTool } from './config-entries.js';
4
7
  import { ApiCodeError, asSentence, errorGuidance, errorMessage, isRunWideFailure, } from './errors.js';
5
- import { executeInstallPlan } from './installer.js';
6
- import { addAssetToLockfile, findDependents, findInstalledAsset, readLockfile, writeLockfile } from './lockfile.js';
8
+ import { restoreFiles, snapshotFiles } from './file-snapshot.js';
9
+ import { executeInstallPlan, fetchPlanFiles, FileConflictError, installFailureMessage, InstallPlanError, } from './installer.js';
10
+ import { addAssetToLockfile, findDependencyAsset, findDependents, findInstalledAsset, getLockfilePath, readLockfile, writeLockfile, } from './lockfile.js';
11
+ import { formatOrgName, parseOrgFromName, scopeSwitchGuidance } from './org.js';
7
12
  import { findAsset } from './registry.js';
8
13
  import { resolveInstallPlan } from './resolver.js';
9
14
  import { uninstallAsset } from './uninstaller.js';
@@ -19,8 +24,9 @@ export function checkOutdated(lockfile, registry, frozenNames) {
19
24
  const outdated = [];
20
25
  for (const installed of lockfile.assets ?? []) {
21
26
  const resolved = findAsset(registry, installed.name, installed.type, undefined, installed.org);
22
- if (!resolved) {
23
- // Asset no longer in registry — skip (could be removed/deprecated)
27
+ if (!resolved || resolved.asset.org !== installed.org) {
28
+ // Asset no longer in registry — skip (could be removed/deprecated). For an org asset the
29
+ // lookup falls back to the global asset of the same name, which update does not switch to.
24
30
  continue;
25
31
  }
26
32
  // resolved.version is the latest version (findAsset defaults to latest)
@@ -39,9 +45,38 @@ export function checkOutdated(lockfile, registry, frozenNames) {
39
45
  }
40
46
  return outdated;
41
47
  }
48
+ /**
49
+ * The installed asset `atk update <spec>` names. `@org/name` names that org's entry only. A
50
+ * bare name resolves the way `install` resolved it: the lockfile org's entry, else the global
51
+ * one. A name installed as more than one type needs `type` to pick one.
52
+ */
53
+ export function findUpdateTarget(lockfile, spec, { type, version } = {}) {
54
+ const { name, org } = parseOrgFromName(spec);
55
+ const named = (lockfile.assets ?? []).filter((a) => a.name === name && (!type || a.type === type));
56
+ let matches = named.filter((a) => a.org === (org ?? lockfile.org));
57
+ if (matches.length === 0 && !org && lockfile.org) {
58
+ matches = named.filter((a) => a.org === undefined);
59
+ }
60
+ const versionSuffix = version ? `@${version}` : '';
61
+ if (matches.length > 1) {
62
+ const types = matches.map((a) => a.type).sort();
63
+ throw new UpdaterError(`"${spec}" is installed as more than one type (${types.join(', ')}). Name the one to update with --type.`, { command: `atk update ${spec}${versionSuffix} --type ${types[0]}` });
64
+ }
65
+ if (matches.length === 1)
66
+ return matches[0];
67
+ const elsewhere = [...new Set(named.map((a) => formatOrgName(a.org, a.name)))].sort();
68
+ if (elsewhere.length > 0) {
69
+ throw new UpdaterError(`Asset "${spec}" is not installed. Installed as: ${elsewhere.join(', ')}.`, elsewhere.length === 1
70
+ ? { command: `atk update ${elsewhere[0]}${versionSuffix}${type ? ` --type ${type}` : ''}` }
71
+ : {});
72
+ }
73
+ throw new UpdaterError(`Asset "${spec}" is not installed.`);
74
+ }
42
75
  /**
43
76
  * Update a single asset to the target version (defaults to latest).
44
- * Uninstalls the current version and installs the target from registry.
77
+ * Downloads the target version, then uninstalls the current version and installs the target.
78
+ * A failed uninstall or install puts back the files the update changed, from a snapshot taken after the
79
+ * download, so the rollback needs no second download.
45
80
  */
46
81
  export async function updateAsset(assetName, installed, registry, options) {
47
82
  const { adapter, onProgress, projectDir, targetVersion } = options;
@@ -53,6 +88,9 @@ export async function updateAsset(assetName, installed, registry, options) {
53
88
  }
54
89
  throw new UpdaterError(`Asset "${assetName}" not found in registry.`);
55
90
  }
91
+ if (resolved.asset.org !== installed.org) {
92
+ throw scopeSwitchError(installed, resolved.asset);
93
+ }
56
94
  const toVersion = resolved.version;
57
95
  if (!isNewer(toVersion, installed.version)) {
58
96
  if (targetVersion) {
@@ -63,71 +101,152 @@ export async function updateAsset(assetName, installed, registry, options) {
63
101
  const fromVersion = installed.version;
64
102
  // Capture old dependencies before uninstalling
65
103
  const oldDeps = installed.dependencies ?? [];
66
- // Uninstall current version without cascade — new version may still need the same deps.
67
- // The lockfile entry is kept so the reinstall below can carry over the config entries
68
- // other tools registered for this asset (the install replaces the entry either way).
69
- onProgress?.(`Removing ${assetName}@${fromVersion}...`);
70
- await uninstallAsset(assetName, {
71
- adapter,
72
- cascade: false,
73
- keepLockfileEntry: true,
74
- onProgress,
75
- org: installed.org,
76
- projectDir,
77
- });
78
- // Install target version (pass explicit version to resolver)
79
- onProgress?.(`Installing ${assetName}@${toVersion}...`);
104
+ // Download the target version before removing anything, so a refused download (an outdated
105
+ // CLI, an expired sign-in, the API down) leaves the installed version in place.
106
+ const plan = planFor(registry, installed, toVersion);
107
+ onProgress?.(`Downloading ${assetName}@${toVersion}...`);
108
+ let files;
109
+ try {
110
+ files = await fetchPlanFiles(plan, adapter);
111
+ }
112
+ catch (downloadError) {
113
+ throw new UpdaterError(`Failed to update "${assetName}" to v${toVersion}: ${failureReason(downloadError, installed)} ` +
114
+ `v${fromVersion} is still installed.`, errorGuidance(downloadError));
115
+ }
116
+ // Capture what the update can change, so a failed uninstall or install restores it without the network.
117
+ let snapshot;
118
+ // The version the lockfile records now. In a multi-tool update an earlier tool may already
119
+ // have moved it past `fromVersion`, which a restore then puts back.
120
+ let lockedVersion;
121
+ // The asset's files as this tool has them, which the uninstall removes.
122
+ let toolEntry;
123
+ try {
124
+ const lockfile = await readLockfile(projectDir);
125
+ lockedVersion = findInstalledAsset(lockfile, assetName, installed.type, installed.org)?.version;
126
+ const toolLockfile = options.toolLockfile ?? lockfile;
127
+ toolEntry = findInstalledAsset(toolLockfile, assetName, installed.type, installed.org) ?? installed;
128
+ snapshot = await snapshotFiles(updateTouchedPaths([lockfile, toolLockfile], plan, [installed, toolEntry], adapter, projectDir));
129
+ }
130
+ catch (snapshotError) {
131
+ throw new UpdaterError(`Failed to update "${assetName}" to v${toVersion}: ${failureReason(snapshotError, installed)} ` +
132
+ `v${fromVersion} is still installed.`, errorGuidance(snapshotError));
133
+ }
80
134
  let results;
81
135
  try {
82
- results = await executeInstallPlan(planFor(registry, installed, toVersion), {
136
+ // Uninstall current version without cascade — new version may still need the same deps.
137
+ // The lockfile entry is kept so the reinstall below can carry over the config entries
138
+ // other tools registered for this asset (the install replaces the entry either way).
139
+ onProgress?.(`Removing ${assetName}@${fromVersion}...`);
140
+ await uninstallAsset(assetName, {
141
+ adapter,
142
+ cascade: false,
143
+ keepLockfileEntry: true,
144
+ ...(options.toolLockfile ? { managedFrom: options.toolLockfile } : {}),
145
+ onProgress,
146
+ org: installed.org,
147
+ plan: { dependents: [], orphanedDependencies: [], primary: toolEntry },
148
+ projectDir,
149
+ type: installed.type,
150
+ });
151
+ // Install target version (pass explicit version to resolver)
152
+ onProgress?.(`Installing ${assetName}@${toVersion}...`);
153
+ // Not forced: the paths this tool has are ATK-managed, so only an unmanaged file or another
154
+ // installed asset's file conflicts.
155
+ results = await executeInstallPlan(plan, {
83
156
  adapter,
84
- force: true,
157
+ files,
158
+ ...(options.toolLockfile ? { managedFrom: options.toolLockfile } : {}),
85
159
  onProgress,
86
160
  projectDir,
87
161
  });
88
- await restoreBundleOrigin(projectDir, installed);
89
162
  }
90
163
  catch (installError) {
91
- // Install failed — attempt to rollback by re-installing the old version
92
- onProgress?.(`Install of ${assetName}@${toVersion} failed, attempting to restore ${assetName}@${fromVersion}...`);
164
+ // `atk update --force` overrides pins, not file conflicts, so the conflict gets its own advice.
165
+ const conflict = installError instanceof InstallPlanError && installError.cause instanceof FileConflictError
166
+ ? { asset: installError.asset, owner: installError.cause.owner, where: installError.cause.where }
167
+ : undefined;
168
+ const owner = conflict?.owner && formatOrgName(conflict.owner.org, conflict.owner.name);
169
+ const installFailure = conflict
170
+ ? `${conflict.where} ` +
171
+ (conflict.owner
172
+ ? `belongs to the installed ${conflict.owner.type} "${owner}"`
173
+ : 'already exists and was not installed by ATK') +
174
+ (isSameAsset(conflict.asset, installed)
175
+ ? '.'
176
+ : `, and the dependency "${formatOrgName(conflict.asset.org, conflict.asset.name)}" goes there.`)
177
+ : failureReason(installError, installed);
178
+ const conflictRemedy = owner ? `Uninstall "${owner}" first` : 'Move or rename it';
179
+ onProgress?.(`Update of ${assetName} to ${toVersion} failed, attempting to restore ${assetName}@${fromVersion}...`);
93
180
  try {
94
- await executeInstallPlan(planFor(registry, installed, fromVersion), {
95
- adapter,
96
- force: true,
97
- onProgress,
98
- projectDir,
99
- });
100
- await restoreBundleOrigin(projectDir, installed);
181
+ await restoreFiles(snapshot);
101
182
  }
102
183
  catch (rollbackError) {
103
- throw new UpdaterError(`Failed to update "${assetName}" to v${toVersion}: ${asSentence(errorMessage(installError))} ` +
104
- `Rollback to v${fromVersion} also failed: ${asSentence(errorMessage(rollbackError))} ` +
184
+ const rollbackFailure = failureReason(rollbackError, installed);
185
+ throw new UpdaterError(`Failed to update "${assetName}" to v${toVersion}: ${installFailure} ` +
186
+ `Rollback to v${fromVersion} also failed` +
187
+ (rollbackFailure === installFailure ? ' for the same reason. ' : `: ${rollbackFailure} `) +
105
188
  `Run "atk sync" to restore the asset.`, { command: 'atk sync', ...errorGuidance(installError) });
106
189
  }
107
- throw new UpdaterError(`Failed to update "${assetName}" to v${toVersion}: ${asSentence(errorMessage(installError))} ` +
108
- `The previous version (v${fromVersion}) has been restored.`, errorGuidance(installError));
190
+ if (lockedVersion !== undefined && lockedVersion !== fromVersion) {
191
+ // The lockfile has one entry for every tool, so this tool's files now trail it.
192
+ const tool = adapter.tool;
193
+ const command = `atk sync --tool ${tool}`;
194
+ throw new UpdaterError(`Failed to update "${assetName}" to v${toVersion} for ${tool}: ${installFailure} ` +
195
+ `Its ${tool} files were restored to v${fromVersion}, but the lockfile records v${lockedVersion}, ` +
196
+ `which the other tools have. Run "${command}" to install v${lockedVersion} for ${tool}.`, conflict
197
+ ? { command, hint: `${conflictRemedy}, then run the command.` }
198
+ : { ...errorGuidance(installError), command });
199
+ }
200
+ throw new UpdaterError(`Failed to update "${assetName}" to v${toVersion}: ${installFailure} ` +
201
+ `The previous version (v${fromVersion}) has been restored.`, conflict ? { hint: `${conflictRemedy}, then run the update again.` } : errorGuidance(installError));
109
202
  }
110
- const directResult = results.find((r) => r.name === assetName);
203
+ // After the restore path: the new version is installed, and restoring the old lockfile would
204
+ // orphan the files of the dependencies it added.
205
+ await restoreBundleOrigin(projectDir, installed);
206
+ // Match org and version too: the plan can hold a same-named dependency from another org.
207
+ const directResult = results.find((r) => r.name === assetName && r.type === installed.type && r.org === installed.org && r.version === toVersion);
111
208
  if (!directResult) {
112
209
  throw new UpdaterError(`Install did not produce a result for "${assetName}".`);
113
210
  }
114
- // Clean up dropped dependencies: deps in old version but not in new version
211
+ // Clean up dropped dependencies: deps in old version but not in new version. One asset's
212
+ // dependency refs carry no org, so type and name identify each.
115
213
  const newDeps = resolved.versionData.dependencies ?? [];
116
214
  const newDepKeys = new Set(newDeps.map((d) => `${d.type}:${d.name}`));
117
215
  const droppedDeps = oldDeps.filter((d) => !newDepKeys.has(`${d.type}:${d.name}`));
216
+ // The new version is installed, so a cleanup failure is a warning, not a failed update.
217
+ const warnings = [];
118
218
  if (droppedDeps.length > 0) {
119
- const updatedLockfile = options.lockfile ?? (await readLockfile(projectDir));
219
+ // Read after the install: before it, the old entry still lists the dropped deps as its own.
220
+ const updatedLockfile = await readLockfile(projectDir);
120
221
  for (const dep of droppedDeps) {
121
- const depAsset = findInstalledAsset(updatedLockfile, dep.name, dep.type, dep.org);
222
+ const depAsset = findDependencyAsset(updatedLockfile, dep, installed.org);
122
223
  if (!depAsset)
123
224
  continue;
124
- // Only remove if it was a dependency and no other asset depends on it
225
+ // Only remove if it was a dependency and no other asset depends on it for this tool
125
226
  if ((depAsset.installReason ?? 'direct') !== 'dependency')
126
227
  continue;
127
- const dependents = findDependents(updatedLockfile, dep.name, dep.type);
228
+ const dependents = findDependents(updatedLockfile, depAsset.name, depAsset.type, depAsset.org).filter((d) => !isExcludedForTool(d, adapter.tool));
128
229
  if (dependents.length === 0) {
129
230
  onProgress?.(`Removing dropped dependency ${dep.name}...`);
130
- await uninstallAsset(dep.name, { adapter, cascade: true, onProgress, org: dep.org, projectDir });
231
+ try {
232
+ await uninstallAsset(dep.name, {
233
+ adapter,
234
+ cascade: true,
235
+ onProgress,
236
+ org: depAsset.org,
237
+ projectDir,
238
+ toolScope: { configuredAdapters: options.configuredAdapters ?? [adapter] },
239
+ type: depAsset.type,
240
+ });
241
+ }
242
+ catch (error) {
243
+ const message = `Could not remove dropped dependency "${dep.name}": ${asSentence(errorMessage(error))}`;
244
+ onProgress?.(`Warning: ${message}`);
245
+ // Other tools may still need the dependency, so the command removes it for this tool only.
246
+ const toolFlag = (options.configuredAdapters?.length ?? 1) > 1 ? ` --tool ${adapter.tool}` : '';
247
+ const spec = `${depAsset.org ? `@${depAsset.org}/` : ''}${dep.name}`;
248
+ warnings.push({ command: `atk uninstall ${spec}${toolFlag}`, message });
249
+ }
131
250
  }
132
251
  }
133
252
  }
@@ -135,8 +254,10 @@ export async function updateAsset(assetName, installed, registry, options) {
135
254
  fromVersion,
136
255
  installResult: directResult,
137
256
  name: assetName,
257
+ ...(installed.org ? { org: installed.org } : {}),
138
258
  toVersion,
139
259
  type: installed.type,
260
+ ...(warnings.length > 0 ? { warnings } : {}),
140
261
  };
141
262
  }
142
263
  /**
@@ -148,23 +269,24 @@ export async function updateAssets(assetNames, lockfile, registry, options) {
148
269
  // When a specific target version is requested for a single asset, bypass outdated check
149
270
  // and delegate directly to updateAsset which handles version comparison.
150
271
  if (options.targetVersion && assetNames && assetNames.length === 1) {
151
- const name = assetNames[0];
152
- const installed = (lockfile.assets ?? []).find((a) => a.name === name);
153
- if (!installed) {
154
- throw new UpdaterError(`Asset "${name}" is not installed.`);
155
- }
272
+ const installed = findUpdateTarget(lockfile, assetNames[0], {
273
+ type: options.type,
274
+ version: options.targetVersion,
275
+ });
276
+ const { name } = installed;
277
+ const spec = assetSpec(installed, options.type);
156
278
  // Check frozen state for explicitly targeted asset
157
279
  const compositeKey = `${installed.type}:${installed.org ?? ''}:${name}`;
158
280
  if (options.frozenAssets?.has(compositeKey)) {
159
- throw new UpdaterError(`Asset "${name}" is frozen. Use "atk thaw ${name}" to unfreeze it before updating.`, {
160
- command: `atk thaw ${name}`,
281
+ throw new UpdaterError(`Asset "${name}" is frozen. Use "atk thaw ${spec}" to unfreeze it before updating.`, {
282
+ command: `atk thaw ${spec}`,
161
283
  hint: 'Thaw the asset before updating, or remove it from your lockfile.',
162
284
  });
163
285
  }
164
286
  // Check pin state for explicitly targeted asset
165
287
  if (installed.pinnedVersion && !options.force) {
166
288
  throw new UpdaterError(`Asset "${name}" is pinned to version "${installed.pinnedVersion}". Use --force to override the pin.`, {
167
- command: `atk unpin ${name}`,
289
+ command: `atk unpin ${spec}`,
168
290
  hint: 'Unpin the asset, or pass --force to override the pin.',
169
291
  });
170
292
  }
@@ -174,10 +296,21 @@ export async function updateAssets(assetNames, lockfile, registry, options) {
174
296
  options.onProgress?.(`Skipping ${name}: uninstalled for ${options.adapter.tool}`);
175
297
  return { excludedSkipped: [toExcludedSkipped(installed)], failures: [], results: [] };
176
298
  }
177
- const currentLockfile = await readLockfile(options.projectDir);
299
+ const failedSkip = failedSkipFor(installed, lockfile, await recordedLockfile(options), options);
300
+ if (failedSkip) {
301
+ return { failedSkipped: [failedSkip], failures: [], results: [] };
302
+ }
303
+ if (!supportsType(options.adapter, installed.type)) {
304
+ reportUnsupported(installed, options);
305
+ return { failures: [], results: [] };
306
+ }
307
+ const dependencySkip = await failedDependencySkipFor(installed, registry, options);
308
+ if (dependencySkip) {
309
+ return { failedSkipped: [dependencySkip], failures: [], results: [] };
310
+ }
178
311
  const result = await updateAsset(name, installed, registry, {
179
312
  ...options,
180
- lockfile: currentLockfile,
313
+ toolLockfile: options.toolLockfile ?? lockfile,
181
314
  });
182
315
  return { failures: [], results: [result] };
183
316
  }
@@ -186,22 +319,24 @@ export async function updateAssets(assetNames, lockfile, registry, options) {
186
319
  const frozenSkipped = [];
187
320
  const skipped = [];
188
321
  if (assetNames && assetNames.length > 0) {
189
- toUpdate = outdated.filter((o) => assetNames.includes(o.name));
322
+ const targets = assetNames.map((spec) => findUpdateTarget(lockfile, spec, { type: options.type }));
323
+ toUpdate = outdated.filter((o) => targets.some((t) => isSameAsset(t, o)));
190
324
  // Check for names that aren't outdated
191
- for (const name of assetNames) {
192
- if (!outdated.find((o) => o.name === name)) {
193
- const installed = (lockfile.assets ?? []).find((a) => a.name === name);
194
- if (!installed) {
195
- throw new UpdaterError(`Asset "${name}" is not installed.`);
325
+ for (const target of targets) {
326
+ if (!outdated.some((o) => isSameAsset(target, o))) {
327
+ const resolved = findAsset(registry, target.name, target.type, undefined, target.org);
328
+ if (resolved && resolved.asset.org !== target.org) {
329
+ throw scopeSwitchError(target, resolved.asset);
196
330
  }
197
- throw new UpdaterError(`Asset "${name}" is already at the latest version (v${installed.version}).`);
331
+ throw new UpdaterError(`Asset "${formatOrgName(target.org, target.name)}" is already at the latest version (v${target.version}).`);
198
332
  }
199
333
  }
200
334
  // For explicitly targeted frozen assets, throw
201
335
  for (const asset of toUpdate) {
202
336
  if (asset.frozen) {
203
- throw new UpdaterError(`Asset "${asset.name}" is frozen. Use "atk thaw ${asset.name}" to unfreeze it before updating.`, {
204
- command: `atk thaw ${asset.name}`,
337
+ const spec = assetSpec(asset, options.type);
338
+ throw new UpdaterError(`Asset "${asset.name}" is frozen. Use "atk thaw ${spec}" to unfreeze it before updating.`, {
339
+ command: `atk thaw ${spec}`,
205
340
  hint: 'Thaw the asset before updating, or remove it from your lockfile.',
206
341
  });
207
342
  }
@@ -210,7 +345,7 @@ export async function updateAssets(assetNames, lockfile, registry, options) {
210
345
  for (const asset of toUpdate) {
211
346
  if (asset.pinned && !options.force) {
212
347
  throw new UpdaterError(`Asset "${asset.name}" is pinned. Use --force to override the pin.`, {
213
- command: `atk unpin ${asset.name}`,
348
+ command: `atk unpin ${assetSpec(asset, options.type)}`,
214
349
  hint: 'Unpin the asset, or pass --force to override the pin.',
215
350
  });
216
351
  }
@@ -235,45 +370,59 @@ export async function updateAssets(assetNames, lockfile, registry, options) {
235
370
  }
236
371
  // Assets uninstalled for this tool are never updated for it (that would reinstall them).
237
372
  const excludedSkipped = [];
373
+ const failedSkipped = [];
374
+ const recorded = await recordedLockfile(options);
238
375
  toUpdate = toUpdate.filter((asset) => {
239
- const installed = (lockfile.assets ?? []).find((a) => a.name === asset.name && a.type === asset.type && a.org === asset.org);
376
+ const installed = lockfileEntryFor(lockfile, asset);
240
377
  if (installed && isExcludedForTool(installed, options.adapter.tool)) {
241
378
  options.onProgress?.(`Skipping ${asset.name}: uninstalled for ${options.adapter.tool}`);
242
379
  excludedSkipped.push(toExcludedSkipped(installed));
243
380
  return false;
244
381
  }
382
+ const failedSkip = failedSkipFor(asset, lockfile, recorded, options);
383
+ if (failedSkip) {
384
+ failedSkipped.push(failedSkip);
385
+ return false;
386
+ }
387
+ // The tool has no placement for the type (a rule under Codex CLI): nothing to update.
388
+ if (!supportsType(options.adapter, asset.type)) {
389
+ reportUnsupported(asset, options);
390
+ return false;
391
+ }
245
392
  return true;
246
393
  });
247
394
  if (toUpdate.length === 0) {
248
395
  return {
249
396
  ...(excludedSkipped.length > 0 ? { excludedSkipped } : {}),
397
+ ...(failedSkipped.length > 0 ? { failedSkipped } : {}),
250
398
  failures: [],
251
399
  frozenSkipped: frozenSkipped.length > 0 ? frozenSkipped : undefined,
252
400
  results: [],
253
401
  skipped: skipped.length > 0 ? skipped : undefined,
254
402
  };
255
403
  }
256
- // Resolve the credential before removing anything. A missing or expired sign-in carries no
257
- // API code for isRunWideFailure, and with a fresh registry cache it would otherwise first
258
- // surface at each asset's download, after that asset's files were removed.
404
+ // Resolve the credential once, up front. A missing or expired sign-in carries no API code for
405
+ // isRunWideFailure, and with a fresh registry cache it would otherwise fail each asset's
406
+ // download in turn.
259
407
  await getCredential();
260
408
  const results = [];
261
409
  const failures = [];
262
410
  let notAttempted;
411
+ // What this tool has: the lockfile the run started from, plus what this tool's updates install.
412
+ let toolLockfile = options.toolLockfile ?? lockfile;
263
413
  for (const [index, asset] of toUpdate.entries()) {
264
- const installed = (lockfile.assets ?? []).find((a) => a.name === asset.name);
414
+ const installed = lockfileEntryFor(lockfile, asset);
265
415
  if (!installed) {
266
416
  throw new UpdaterError(`Asset "${asset.name}" is installed but not found in lockfile.`);
267
417
  }
418
+ let result;
268
419
  try {
269
- // Read the lockfile once per iteration for dropped dependency cleanup,
270
- // rather than letting each updateAsset call re-read it internally.
271
- const currentLockfile = await readLockfile(options.projectDir);
272
- const result = await updateAsset(asset.name, installed, registry, {
273
- ...options,
274
- lockfile: currentLockfile,
275
- });
276
- results.push(result);
420
+ const dependencySkip = await failedDependencySkipFor(installed, registry, options);
421
+ if (dependencySkip) {
422
+ failedSkipped.push(dependencySkip);
423
+ continue;
424
+ }
425
+ result = await updateAsset(asset.name, installed, registry, { ...options, toolLockfile });
277
426
  }
278
427
  catch (error) {
279
428
  // Most errors from updateAsset already name the asset; only frame the ones that don't.
@@ -283,16 +432,27 @@ export async function updateAssets(assetNames, lockfile, registry, options) {
283
432
  : `Failed to update "${asset.name}": ${asSentence(cause)}`;
284
433
  options.onProgress?.(message);
285
434
  const guidance = errorGuidance(error);
286
- failures.push({ error: message, name: asset.name, type: asset.type, ...guidance });
287
- // The same error would fail every remaining asset, after removing its files first.
435
+ failures.push({
436
+ error: message,
437
+ name: asset.name,
438
+ ...(asset.org ? { org: asset.org } : {}),
439
+ type: asset.type,
440
+ ...guidance,
441
+ });
442
+ // The same error would fail every remaining asset.
288
443
  if (isRunWideFailure(guidance)) {
289
444
  notAttempted = toUpdate.slice(index + 1).map((a) => a.name);
290
445
  break;
291
446
  }
447
+ continue;
292
448
  }
449
+ results.push(result);
450
+ // The update succeeded either way; a failed read leaves the next update to report it.
451
+ toolLockfile = await withInstalledEntries(toolLockfile, planFor(registry, installed, result.toVersion), options).catch(() => toolLockfile);
293
452
  }
294
453
  return {
295
454
  ...(excludedSkipped.length > 0 ? { excludedSkipped } : {}),
455
+ ...(failedSkipped.length > 0 ? { failedSkipped } : {}),
296
456
  failures,
297
457
  frozenSkipped: frozenSkipped.length > 0 ? frozenSkipped : undefined,
298
458
  ...(notAttempted ? { notAttempted } : {}),
@@ -300,6 +460,60 @@ export async function updateAssets(assetNames, lockfile, registry, options) {
300
460
  skipped: skipped.length > 0 ? skipped : undefined,
301
461
  };
302
462
  }
463
+ /** How a command names an asset: `@org/name`, with `--type` when the run was given one. */
464
+ function assetSpec(asset, type) {
465
+ return `${formatOrgName(asset.org, asset.name)}${type ? ` --type ${asset.type}` : ''}`;
466
+ }
467
+ /**
468
+ * The skip record for an asset whose update would move a dependency whose own update failed for
469
+ * an earlier tool in the run: that tool kept the version the lockfile records, so moving it here
470
+ * would leave the tools on different versions. A dependency the lockfile already records at the
471
+ * plan's version, or one this tool has no placement for, is not moved, so it does not count.
472
+ */
473
+ async function failedDependencySkipFor(installed, registry, options) {
474
+ if (!options.failedFor?.size)
475
+ return undefined;
476
+ // An asset that does not resolve gets its error from the update itself.
477
+ const resolved = findAsset(registry, installed.name, installed.type, options.targetVersion, installed.org);
478
+ if (!resolved || resolved.asset.org !== installed.org)
479
+ return undefined;
480
+ const lockfile = await readLockfile(options.projectDir);
481
+ for (const { asset, version } of planFor(registry, installed, resolved.version).entries) {
482
+ if (isSameAsset(asset, installed) || !supportsType(options.adapter, asset.type))
483
+ continue;
484
+ const failedFor = options.failedFor.get(`${asset.type}:${asset.org ?? ''}:${asset.name}`);
485
+ if (!failedFor || findInstalledAsset(lockfile, asset.name, asset.type, asset.org)?.version === version)
486
+ continue;
487
+ options.onProgress?.(`Skipping ${installed.name} for ${options.adapter.tool}: its dependency ${asset.name} failed to update for ${failedFor}`);
488
+ return {
489
+ dependency: { name: asset.name, ...(asset.org ? { org: asset.org } : {}), type: asset.type },
490
+ failedFor,
491
+ name: installed.name,
492
+ ...(installed.org ? { org: installed.org } : {}),
493
+ type: installed.type,
494
+ };
495
+ }
496
+ return undefined;
497
+ }
498
+ /**
499
+ * The skip record for an asset whose update failed for an earlier tool in the run, if it did. An
500
+ * asset that another asset's plan moved for that tool after all (the lockfile no longer records
501
+ * the run's starting version) is not skipped: the later tools move it too.
502
+ */
503
+ function failedSkipFor(asset, snapshot, recorded, options) {
504
+ const failedFor = options.failedFor?.get(`${asset.type}:${asset.org ?? ''}:${asset.name}`);
505
+ if (!failedFor)
506
+ return undefined;
507
+ const versionIn = (lockfile) => lockfile && findInstalledAsset(lockfile, asset.name, asset.type, asset.org)?.version;
508
+ if (versionIn(recorded) !== versionIn(snapshot))
509
+ return undefined;
510
+ options.onProgress?.(`Skipping ${asset.name} for ${options.adapter.tool}: its update failed for ${failedFor}`);
511
+ return { failedFor, name: asset.name, ...(asset.org ? { org: asset.org } : {}), type: asset.type };
512
+ }
513
+ /** Why an install for an update failed, as a sentence; the update message already names the asset. */
514
+ function failureReason(error, installed) {
515
+ return asSentence(installFailureMessage(error, installed));
516
+ }
303
517
  /**
304
518
  * Check if the registry version is newer than the installed version.
305
519
  * Falls back to string comparison if semver parsing fails.
@@ -313,19 +527,35 @@ function isNewer(registryVersion, installedVersion) {
313
527
  // silently ignored.
314
528
  return registryVersion !== installedVersion;
315
529
  }
530
+ function isSameAsset(a, b) {
531
+ return a.name === b.name && a.type === b.type && a.org === b.org;
532
+ }
533
+ /** The lockfile entry of an outdated asset: same name, type and org. */
534
+ function lockfileEntryFor(lockfile, asset) {
535
+ return (lockfile.assets ?? []).find((a) => isSameAsset(a, asset));
536
+ }
316
537
  /**
317
538
  * The install plan for one version of an installed asset. The resolver treats the asset as
318
539
  * a fresh top-level request, so keep the install reason its lockfile entry had: updating a
319
- * dependency must not turn it into a direct install.
540
+ * dependency must not turn it into a direct install. Only the requested asset's entry is
541
+ * `direct`, so a same-named entry, such as a dependency of another type or org, keeps its
542
+ * own reason.
320
543
  */
321
544
  function planFor(registry, installed, version) {
322
545
  const plan = resolveInstallPlan(registry, installed.name, installed.type, version, installed.org);
323
546
  const reason = installed.installReason ?? 'direct';
324
547
  return {
325
548
  ...plan,
326
- entries: plan.entries.map((e) => e.asset.name === installed.name && e.asset.type === installed.type ? { ...e, reason } : e),
549
+ entries: plan.entries.map((e) => (e.reason === 'direct' ? { ...e, reason } : e)),
327
550
  };
328
551
  }
552
+ /** The lockfile on disk, read only when an earlier tool in the run had a failed update. */
553
+ async function recordedLockfile(options) {
554
+ return options.failedFor?.size ? readLockfile(options.projectDir) : undefined;
555
+ }
556
+ function reportUnsupported(asset, options) {
557
+ options.onProgress?.(`Skipping ${asset.name}: ${options.adapter.tool} has no placement for ${asset.type} assets`);
558
+ }
329
559
  /**
330
560
  * Put a bundle member's bundle origin back on its updated lockfile entry. Passing it to
331
561
  * executeInstallPlan would also stamp the asset's dependencies, so it is set here instead.
@@ -339,7 +569,50 @@ async function restoreBundleOrigin(projectDir, installed) {
339
569
  return;
340
570
  await writeLockfile(projectDir, addAssetToLockfile(lockfile, { ...entry, bundleOrigin: installed.bundleOrigin }));
341
571
  }
572
+ function scopeSwitchError(installed, fallback) {
573
+ const { message, ...guidance } = scopeSwitchGuidance(installed, fallback, 'update');
574
+ return new UpdaterError(message, guidance);
575
+ }
342
576
  function toExcludedSkipped(installed) {
343
577
  return { name: installed.name, ...(installed.org ? { org: installed.org } : {}), type: installed.type };
344
578
  }
579
+ /**
580
+ * The absolute paths that updating `installed` with `plan` can change for the adapter's tool:
581
+ * the lockfile, the placed files and config files of the asset and of every installed asset the
582
+ * plan replaces (its dependencies), and the config files the plan's new assets register in. Each
583
+ * entry of `installed` and each lockfile of `lockfiles` is a view of what is installed; all count.
584
+ */
585
+ function updateTouchedPaths(lockfiles, plan, installed, adapter, projectDir) {
586
+ const relativePaths = [];
587
+ const replaced = lockfiles.flatMap((lockfile) => plan.entries.flatMap((e) => findInstalledAsset(lockfile, e.asset.name, e.asset.type, e.asset.org) ?? []));
588
+ for (const asset of [...installed, ...replaced]) {
589
+ const resolved = resolveInstalledPaths(asset, adapter, projectDir);
590
+ relativePaths.push(...resolved.files.map((f) => f.installedPath));
591
+ relativePaths.push(...[...getConfigEntriesForTool(asset, adapter), ...resolved.configEntries].map((e) => e.file));
592
+ }
593
+ for (const { asset } of plan.entries) {
594
+ const placement = getPlacement(adapter, asset.type);
595
+ if (!placement)
596
+ continue;
597
+ if (placement.configFile)
598
+ relativePaths.push(placement.configFile);
599
+ if (placement.enabledListFile)
600
+ relativePaths.push(placement.enabledListFile);
601
+ // A memory template is spliced into a shared file at its placement path.
602
+ if (asset.type === 'memory-template')
603
+ relativePaths.push(placement.path.replace(/\{name\}/g, asset.name));
604
+ }
605
+ return [getLockfilePath(projectDir), ...relativePaths.map((p) => join(projectDir, p))];
606
+ }
607
+ /** `toolLockfile` with the on-disk lockfile entries of the assets an update installed with `plan`. */
608
+ async function withInstalledEntries(toolLockfile, plan, { projectDir }) {
609
+ const lockfile = await readLockfile(projectDir);
610
+ let updated = toolLockfile;
611
+ for (const { asset } of plan.entries) {
612
+ const entry = findInstalledAsset(lockfile, asset.name, asset.type, asset.org);
613
+ if (entry)
614
+ updated = addAssetToLockfile(updated, entry);
615
+ }
616
+ return updated;
617
+ }
345
618
  //# sourceMappingURL=updater.js.map