@detergent-software/atk 8.6.2-dev.1 → 8.7.0-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +109 -0
- package/build/commands/audit.d.ts.map +1 -1
- package/build/commands/audit.js +34 -11
- package/build/commands/audit.js.map +1 -1
- package/build/commands/browse.d.ts.map +1 -1
- package/build/commands/browse.js +23 -15
- package/build/commands/browse.js.map +1 -1
- package/build/commands/freeze.d.ts.map +1 -1
- package/build/commands/freeze.js +11 -6
- package/build/commands/freeze.js.map +1 -1
- package/build/commands/index.d.ts.map +1 -1
- package/build/commands/index.js +1 -0
- package/build/commands/index.js.map +1 -1
- package/build/commands/install.d.ts.map +1 -1
- package/build/commands/install.js +91 -26
- package/build/commands/install.js.map +1 -1
- package/build/commands/list.d.ts +2 -1
- package/build/commands/list.d.ts.map +1 -1
- package/build/commands/list.js +82 -66
- package/build/commands/list.js.map +1 -1
- package/build/commands/move.d.ts +28 -0
- package/build/commands/move.d.ts.map +1 -0
- package/build/commands/move.js +180 -0
- package/build/commands/move.js.map +1 -0
- package/build/commands/outdated.d.ts +2 -1
- package/build/commands/outdated.d.ts.map +1 -1
- package/build/commands/outdated.js +67 -24
- package/build/commands/outdated.js.map +1 -1
- package/build/commands/pin.d.ts.map +1 -1
- package/build/commands/pin.js +12 -7
- package/build/commands/pin.js.map +1 -1
- package/build/commands/prune.d.ts.map +1 -1
- package/build/commands/prune.js +17 -10
- package/build/commands/prune.js.map +1 -1
- package/build/commands/schema.d.ts +1 -1
- package/build/commands/sync.d.ts.map +1 -1
- package/build/commands/sync.js +22 -13
- package/build/commands/sync.js.map +1 -1
- package/build/commands/thaw.d.ts.map +1 -1
- package/build/commands/thaw.js +11 -6
- package/build/commands/thaw.js.map +1 -1
- package/build/commands/uninstall.d.ts.map +1 -1
- package/build/commands/uninstall.js +20 -15
- package/build/commands/uninstall.js.map +1 -1
- package/build/commands/unpin.d.ts.map +1 -1
- package/build/commands/unpin.js +11 -6
- package/build/commands/unpin.js.map +1 -1
- package/build/commands/update.d.ts.map +1 -1
- package/build/commands/update.js +11 -11
- package/build/commands/update.js.map +1 -1
- package/build/components/CrossScopeWarning.d.ts +12 -0
- package/build/components/CrossScopeWarning.d.ts.map +1 -0
- package/build/components/CrossScopeWarning.js +13 -0
- package/build/components/CrossScopeWarning.js.map +1 -0
- package/build/components/DryRunBanner.d.ts +3 -1
- package/build/components/DryRunBanner.d.ts.map +1 -1
- package/build/components/DryRunBanner.js +2 -2
- package/build/components/DryRunBanner.js.map +1 -1
- package/build/components/InstallConfirmBar.d.ts.map +1 -1
- package/build/components/InstallConfirmBar.js +1 -3
- package/build/components/InstallConfirmBar.js.map +1 -1
- package/build/components/InstallSummary.d.ts +3 -1
- package/build/components/InstallSummary.d.ts.map +1 -1
- package/build/components/InstallSummary.js +2 -2
- package/build/components/InstallSummary.js.map +1 -1
- package/build/components/ListActionBar.d.ts +4 -2
- package/build/components/ListActionBar.d.ts.map +1 -1
- package/build/components/ListActionBar.js +5 -4
- package/build/components/ListActionBar.js.map +1 -1
- package/build/components/ListBrowseList.d.ts +7 -6
- package/build/components/ListBrowseList.d.ts.map +1 -1
- package/build/components/ListBrowseList.js +10 -6
- package/build/components/ListBrowseList.js.map +1 -1
- package/build/components/StatusBadge.d.ts +1 -1
- package/build/components/StatusBadge.d.ts.map +1 -1
- package/build/components/StatusBadge.js +1 -0
- package/build/components/StatusBadge.js.map +1 -1
- package/build/components/SyncAllInSync.d.ts +3 -1
- package/build/components/SyncAllInSync.d.ts.map +1 -1
- package/build/components/SyncAllInSync.js +2 -2
- package/build/components/SyncAllInSync.js.map +1 -1
- package/build/components/SyncDriftedList.d.ts +3 -1
- package/build/components/SyncDriftedList.d.ts.map +1 -1
- package/build/components/SyncDriftedList.js +3 -3
- package/build/components/SyncDriftedList.js.map +1 -1
- package/build/components/SyncNoAssets.d.ts +3 -1
- package/build/components/SyncNoAssets.d.ts.map +1 -1
- package/build/components/SyncNoAssets.js +2 -2
- package/build/components/SyncNoAssets.js.map +1 -1
- package/build/components/index.d.ts +1 -0
- package/build/components/index.d.ts.map +1 -1
- package/build/components/index.js +1 -0
- package/build/components/index.js.map +1 -1
- package/build/hooks/useInitState.d.ts.map +1 -1
- package/build/hooks/useInitState.js +4 -2
- package/build/hooks/useInitState.js.map +1 -1
- package/build/hooks/useListActions.d.ts +19 -6
- package/build/hooks/useListActions.d.ts.map +1 -1
- package/build/hooks/useListActions.js +36 -30
- package/build/hooks/useListActions.js.map +1 -1
- package/build/hooks/useListState.d.ts +29 -12
- package/build/hooks/useListState.d.ts.map +1 -1
- package/build/hooks/useListState.js +22 -6
- package/build/hooks/useListState.js.map +1 -1
- package/build/hooks/usePruneState.d.ts +6 -0
- package/build/hooks/usePruneState.d.ts.map +1 -1
- package/build/hooks/usePruneState.js +6 -1
- package/build/hooks/usePruneState.js.map +1 -1
- package/build/hooks/useResolvedScope.d.ts +21 -0
- package/build/hooks/useResolvedScope.d.ts.map +1 -0
- package/build/hooks/useResolvedScope.js +15 -0
- package/build/hooks/useResolvedScope.js.map +1 -0
- package/build/hooks/useUninstallState.d.ts +6 -0
- package/build/hooks/useUninstallState.d.ts.map +1 -1
- package/build/hooks/useUninstallState.js +6 -1
- package/build/hooks/useUninstallState.js.map +1 -1
- package/build/lib/adapter.d.ts +47 -1
- package/build/lib/adapter.d.ts.map +1 -1
- package/build/lib/adapter.js +153 -10
- package/build/lib/adapter.js.map +1 -1
- package/build/lib/checksum.d.ts.map +1 -1
- package/build/lib/checksum.js +2 -1
- package/build/lib/checksum.js.map +1 -1
- package/build/lib/config-entries.d.ts +5 -3
- package/build/lib/config-entries.d.ts.map +1 -1
- package/build/lib/config-entries.js +7 -4
- package/build/lib/config-entries.js.map +1 -1
- package/build/lib/config-file.d.ts +15 -4
- package/build/lib/config-file.d.ts.map +1 -1
- package/build/lib/config-file.js +35 -10
- package/build/lib/config-file.js.map +1 -1
- package/build/lib/config-lock.d.ts +22 -0
- package/build/lib/config-lock.d.ts.map +1 -0
- package/build/lib/config-lock.js +67 -0
- package/build/lib/config-lock.js.map +1 -0
- package/build/lib/cross-scope.d.ts +50 -0
- package/build/lib/cross-scope.d.ts.map +1 -0
- package/build/lib/cross-scope.js +132 -0
- package/build/lib/cross-scope.js.map +1 -0
- package/build/lib/diagnostics.d.ts +15 -2
- package/build/lib/diagnostics.d.ts.map +1 -1
- package/build/lib/diagnostics.js +152 -10
- package/build/lib/diagnostics.js.map +1 -1
- package/build/lib/diff-command.d.ts.map +1 -1
- package/build/lib/diff-command.js +7 -7
- package/build/lib/diff-command.js.map +1 -1
- package/build/lib/hook-config.d.ts +7 -0
- package/build/lib/hook-config.d.ts.map +1 -1
- package/build/lib/hook-config.js +11 -0
- package/build/lib/hook-config.js.map +1 -1
- package/build/lib/init.d.ts.map +1 -1
- package/build/lib/init.js +2 -1
- package/build/lib/init.js.map +1 -1
- package/build/lib/installed-for-tool.d.ts +2 -1
- package/build/lib/installed-for-tool.d.ts.map +1 -1
- package/build/lib/installed-for-tool.js +7 -4
- package/build/lib/installed-for-tool.js.map +1 -1
- package/build/lib/installer.d.ts +8 -4
- package/build/lib/installer.d.ts.map +1 -1
- package/build/lib/installer.js +153 -57
- package/build/lib/installer.js.map +1 -1
- package/build/lib/lockfile.d.ts +1 -1
- package/build/lib/lockfile.d.ts.map +1 -1
- package/build/lib/lockfile.js +2 -2
- package/build/lib/lockfile.js.map +1 -1
- package/build/lib/mover.d.ts +103 -0
- package/build/lib/mover.d.ts.map +1 -0
- package/build/lib/mover.js +375 -0
- package/build/lib/mover.js.map +1 -0
- package/build/lib/multi-tool-install.js +1 -1
- package/build/lib/multi-tool-install.js.map +1 -1
- package/build/lib/owned-paths.d.ts.map +1 -1
- package/build/lib/owned-paths.js +8 -8
- package/build/lib/owned-paths.js.map +1 -1
- package/build/lib/read-scopes.d.ts +32 -0
- package/build/lib/read-scopes.d.ts.map +1 -0
- package/build/lib/read-scopes.js +55 -0
- package/build/lib/read-scopes.js.map +1 -0
- package/build/lib/root-path.d.ts +7 -0
- package/build/lib/root-path.d.ts.map +1 -0
- package/build/lib/root-path.js +10 -0
- package/build/lib/root-path.js.map +1 -0
- package/build/lib/schemas/adapter.d.ts +80 -1
- package/build/lib/schemas/adapter.d.ts.map +1 -1
- package/build/lib/schemas/adapter.js +48 -0
- package/build/lib/schemas/adapter.js.map +1 -1
- package/build/lib/schemas/lockfile.d.ts +2 -0
- package/build/lib/schemas/lockfile.d.ts.map +1 -1
- package/build/lib/schemas/lockfile.js +8 -2
- package/build/lib/schemas/lockfile.js.map +1 -1
- package/build/lib/scope.d.ts +11 -0
- package/build/lib/scope.d.ts.map +1 -1
- package/build/lib/scope.js +12 -0
- package/build/lib/scope.js.map +1 -1
- package/build/lib/sync.d.ts +4 -3
- package/build/lib/sync.d.ts.map +1 -1
- package/build/lib/sync.js +12 -5
- package/build/lib/sync.js.map +1 -1
- package/build/lib/toml-edit.d.ts +10 -0
- package/build/lib/toml-edit.d.ts.map +1 -0
- package/build/lib/toml-edit.js +311 -0
- package/build/lib/toml-edit.js.map +1 -0
- package/build/lib/tool-resolver.d.ts +5 -0
- package/build/lib/tool-resolver.d.ts.map +1 -1
- package/build/lib/tool-resolver.js +16 -12
- package/build/lib/tool-resolver.js.map +1 -1
- package/build/lib/uninstaller.d.ts +14 -1
- package/build/lib/uninstaller.d.ts.map +1 -1
- package/build/lib/uninstaller.js +73 -28
- package/build/lib/uninstaller.js.map +1 -1
- package/build/lib/updater.d.ts +5 -4
- package/build/lib/updater.d.ts.map +1 -1
- package/build/lib/updater.js +18 -12
- package/build/lib/updater.js.map +1 -1
- package/build/lib/write-file.d.ts +17 -4
- package/build/lib/write-file.d.ts.map +1 -1
- package/build/lib/write-file.js +60 -10
- package/build/lib/write-file.js.map +1 -1
- package/package.json +1 -1
- package/tool-adapters/claude-code.json +44 -0
- package/tool-adapters/codex-cli.json +28 -0
- package/tool-adapters/copilot-cli.json +46 -2
package/build/lib/installer.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { mkdir, readFile, unlink } from 'node:fs/promises';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
|
-
import { getPlacement, isEntrypoint, resolveInstalledPaths } from './adapter.js';
|
|
4
|
+
import { adapterAsPlaced, configDirMove, getPlacement, isEntrypoint, recordUserConfigDir, resolveInstalledPaths, } from './adapter.js';
|
|
5
5
|
import { carryExclusions, declarationAllows, declaresTool, isExcludedForTool } from './asset-tools.js';
|
|
6
|
-
import { computeConfigEntryChecksum, computeHookGroupChecksum, hashBytes } from './checksum.js';
|
|
6
|
+
import { computeConfigEntryChecksum, computeHookGroupChecksum, hashBytes, verifyChecksum } from './checksum.js';
|
|
7
7
|
import { getConfigEntriesForTool, getOtherToolsConfigEntries, mergeConfigEntriesForTool, normalizeConfigPath, } from './config-entries.js';
|
|
8
|
-
import { normalizeForFormat, readConfigFile, resolveConfigFormat, writeConfigFile, } from './config-file.js';
|
|
8
|
+
import { normalizeForFormat, readConfigFile, removeEmptiedConfigFile, resolveConfigFormat, writeConfigFile, } from './config-file.js';
|
|
9
|
+
import { configLockPath, withConfigLock } from './config-lock.js';
|
|
9
10
|
import { addToEnabledList, removeFromEnabledList } from './enabled-list.js';
|
|
10
11
|
import { ApiCodeError, errorGuidance, errorMessage } from './errors.js';
|
|
11
12
|
import { ensureGitignoreEntries } from './gitignore.js';
|
|
12
|
-
import { buildHookGroup,
|
|
13
|
+
import { buildHookGroup, hookCommand, hookGroupOwnsCommand, removeHookGroup } from './hook-config.js';
|
|
13
14
|
import { hookScriptPath, makeExecutable } from './hook-script.js';
|
|
14
15
|
import { isInstalledForTool } from './installed-for-tool.js';
|
|
15
16
|
import { addAssetToLockfile, findInstalledAsset, readLockfile, removeAssetFromLockfile, writeLockfile, } from './lockfile.js';
|
|
@@ -17,10 +18,18 @@ import { formatOrgName } from './org.js';
|
|
|
17
18
|
import { configEntriesOwnedByOthers, holdsAnotherOwnersValue, pathsOwnedByOthers } from './owned-paths.js';
|
|
18
19
|
import { toRelativePosix } from './paths.js';
|
|
19
20
|
import { fetchAssetFileBytes, fetchAssetManifest, listAssetFiles } from './registry.js';
|
|
21
|
+
import { fromRoot } from './root-path.js';
|
|
22
|
+
import { scopedCommand } from './scope.js';
|
|
23
|
+
import { removePlacedFiles } from './uninstaller.js';
|
|
20
24
|
import { writeBinaryFile, writeTextFile } from './write-file.js';
|
|
21
25
|
export class InstallerError extends ApiCodeError {
|
|
22
26
|
name = 'InstallerError';
|
|
23
27
|
}
|
|
28
|
+
/** The guidance for an asset type no tool of the user scope has a user-level placement for. */
|
|
29
|
+
const NO_USER_PLACEMENT = {
|
|
30
|
+
command: 'atk setup --user',
|
|
31
|
+
hint: 'Install it in a project (without --user), or add a tool to the user scope that has one.',
|
|
32
|
+
};
|
|
24
33
|
/**
|
|
25
34
|
* A file the install would place at `path` exists and is not ATK-managed, or belongs to `owner`,
|
|
26
35
|
* another installed asset; nothing was written there. With `entry`, the conflict is that part of
|
|
@@ -73,19 +82,22 @@ export class PlanDownloadError extends InstallPlanError {
|
|
|
73
82
|
* Guard for multi-tool installs run with `skipUnsupported`. Each tool skips the asset
|
|
74
83
|
* types it cannot place, which is fine as long as some tool placed something. If every
|
|
75
84
|
* tool skipped every entry, nothing was installed — surface that as an error rather
|
|
76
|
-
* than reporting a successful no-op.
|
|
85
|
+
* than reporting a successful no-op. The refusal's setup command is `scope`'s.
|
|
77
86
|
*/
|
|
78
|
-
export function assertInstalledByAnyTool(plan, toolResults) {
|
|
87
|
+
export function assertInstalledByAnyTool(plan, toolResults, scope = 'project') {
|
|
79
88
|
if (plan.entries.length === 0 || toolResults.some((r) => r.results.length > 0)) {
|
|
80
89
|
return;
|
|
81
90
|
}
|
|
82
|
-
const undeclared = undeclaredHookError(plan, toolResults.map((r) => r.tool));
|
|
91
|
+
const undeclared = undeclaredHookError(plan, toolResults.map((r) => r.tool), scope);
|
|
83
92
|
if (undeclared)
|
|
84
93
|
throw undeclared;
|
|
85
94
|
const types = [...new Set(plan.entries.map((e) => e.asset.type))].join(', ');
|
|
86
95
|
const tools = toolResults.map((r) => r.tool).join(', ');
|
|
96
|
+
if (scope === 'user') {
|
|
97
|
+
throw new InstallerError(`None of the user scope's tools (${tools}) has a user-level ${types}.`, NO_USER_PLACEMENT);
|
|
98
|
+
}
|
|
87
99
|
throw new InstallerError(`None of the configured tools (${tools}) support asset type "${types}"`, {
|
|
88
|
-
command: 'atk setup',
|
|
100
|
+
command: scopedCommand('atk setup', scope),
|
|
89
101
|
hint: 'Add a tool that supports this asset type, or install it in a project configured for one.',
|
|
90
102
|
});
|
|
91
103
|
}
|
|
@@ -101,18 +113,21 @@ export function assertInstalledByAnyTool(plan, toolResults) {
|
|
|
101
113
|
*/
|
|
102
114
|
export async function executeInstallPlan(plan, options) {
|
|
103
115
|
const results = [];
|
|
104
|
-
// Ensure .gitignore has entries for ATK-managed asset directories (non-fatal)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
116
|
+
// Ensure .gitignore has entries for ATK-managed asset directories (non-fatal). The user root
|
|
117
|
+
// isn't a project, and its .gitignore is often git's global excludes file, so it's left alone.
|
|
118
|
+
if (options.scope !== 'user') {
|
|
119
|
+
try {
|
|
120
|
+
await ensureGitignoreEntries(options.projectDir, options.adapter);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
options.onProgress?.('Warning: Failed to update .gitignore — continuing with install');
|
|
124
|
+
}
|
|
110
125
|
}
|
|
111
126
|
// Read lockfile once — used for both conflict detection and per-asset install logic
|
|
112
127
|
let lockfile = await readLockfile(options.projectDir);
|
|
113
128
|
const initialLockfile = lockfile;
|
|
114
129
|
// Map each ATK-managed file path to the assets that installed it, for conflict detection
|
|
115
|
-
const managedPaths = buildManagedPaths(options.managedFrom ?? lockfile, options.projectDir, options.adapter);
|
|
130
|
+
const managedPaths = await buildManagedPaths(options.managedFrom ?? lockfile, options.projectDir, options.adapter);
|
|
116
131
|
// ...and each block or keyed config entry to the assets that wrote it
|
|
117
132
|
const managedEntries = buildManagedEntries(options.managedFrom ?? lockfile, options.projectDir, options.adapter);
|
|
118
133
|
// ...and each keyed config entry of other tools to its owners, who may share it for this tool
|
|
@@ -125,7 +140,9 @@ export async function executeInstallPlan(plan, options) {
|
|
|
125
140
|
const supportedEntries = placeableEntries(plan, options.adapter);
|
|
126
141
|
for (const entry of plan.entries) {
|
|
127
142
|
if (!getPlacement(options.adapter, entry.asset.type)) {
|
|
128
|
-
options.onProgress?.(
|
|
143
|
+
options.onProgress?.(options.scope === 'user'
|
|
144
|
+
? `Skipping ${entry.asset.name}: ${options.adapter.displayName} has no user-level ${entry.asset.type}`
|
|
145
|
+
: `Skipping ${entry.asset.name}: ${options.adapter.displayName} does not support ${entry.asset.type} assets`);
|
|
129
146
|
}
|
|
130
147
|
else if (!declarationAllows(entry.asset.type, entry.versionData.tools, options.adapter.tool)) {
|
|
131
148
|
const tools = entry.versionData.tools.join(', ');
|
|
@@ -133,9 +150,12 @@ export async function executeInstallPlan(plan, options) {
|
|
|
133
150
|
}
|
|
134
151
|
}
|
|
135
152
|
if (supportedEntries.length === 0 && plan.entries.length > 0 && !options.skipUnsupported) {
|
|
136
|
-
const undeclared = undeclaredHookError(plan, [options.adapter.tool]);
|
|
153
|
+
const undeclared = undeclaredHookError(plan, [options.adapter.tool], options.scope);
|
|
137
154
|
if (undeclared)
|
|
138
155
|
throw undeclared;
|
|
156
|
+
if (options.scope === 'user') {
|
|
157
|
+
throw new InstallerError(`${options.adapter.displayName} has no user-level ${plan.entries[0].asset.type}.`, NO_USER_PLACEMENT);
|
|
158
|
+
}
|
|
139
159
|
throw new InstallerError(`Tool "${options.adapter.tool}" does not support asset type "${plan.entries[0].asset.type}"`);
|
|
140
160
|
}
|
|
141
161
|
// The assets this tool had in place before the plan, by plan entry, which a failure keeps.
|
|
@@ -148,11 +168,16 @@ export async function executeInstallPlan(plan, options) {
|
|
|
148
168
|
}
|
|
149
169
|
// The files the asset being placed wrote where no file was, which a failure removes.
|
|
150
170
|
let created = [];
|
|
171
|
+
// The previous copies left in a user config directory the tool no longer reads, removed once the
|
|
172
|
+
// plan is in place: a failure restores their lockfile entries, which still point at them.
|
|
173
|
+
const movedCopies = [];
|
|
151
174
|
try {
|
|
152
175
|
for (const entry of supportedEntries) {
|
|
153
176
|
created = [];
|
|
154
|
-
const { lockfile: updatedLockfile, result, sharedEntries, } = await installAsset(entry, options, managedPaths, managedEntries, ownersElsewhere, lockfile, created);
|
|
177
|
+
const { lockfile: updatedLockfile, movedCopy, result, sharedEntries, } = await installAsset(entry, options, managedPaths, managedEntries, ownersElsewhere, lockfile, created);
|
|
155
178
|
results.push(result);
|
|
179
|
+
if (movedCopy)
|
|
180
|
+
movedCopies.push({ previous: movedCopy, written: result.filesPlaced.map((f) => f.destination) });
|
|
156
181
|
// A later entry of this plan may land on these files or entries, and must see whose they are.
|
|
157
182
|
for (const file of result.filesPlaced)
|
|
158
183
|
addOwner(managedPaths, file.destination, entry.asset);
|
|
@@ -209,6 +234,13 @@ export async function executeInstallPlan(plan, options) {
|
|
|
209
234
|
}
|
|
210
235
|
throw new InstallPlanError(failed, error, added.length);
|
|
211
236
|
}
|
|
237
|
+
for (const { previous, written } of movedCopies) {
|
|
238
|
+
await removePlacedFiles(previous, adapterAsPlaced(options.adapter, previous), options.projectDir, {
|
|
239
|
+
keep: written,
|
|
240
|
+
lockfile,
|
|
241
|
+
onProgress: options.onProgress,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
212
244
|
return results;
|
|
213
245
|
}
|
|
214
246
|
/**
|
|
@@ -285,10 +317,13 @@ export async function rollBackInstalledAssets(installed, before, lockfile, optio
|
|
|
285
317
|
kept.add(entry.key);
|
|
286
318
|
continue;
|
|
287
319
|
}
|
|
288
|
-
const configPath =
|
|
320
|
+
const configPath = fromRoot(options.projectDir, entry.file);
|
|
289
321
|
const format = resolveConfigFormat(configPath, placement);
|
|
290
322
|
const created = result.configFilesCreated?.includes(entry.file) ?? false;
|
|
291
|
-
|
|
323
|
+
const lockFile = configLockPath(placement, options.projectDir, entry.file);
|
|
324
|
+
await withConfigLock(lockFile, () => revertConfigEntry(configPath, entry.key, entry.command, format, created, options.onProgress, {
|
|
325
|
+
atomic: lockFile !== undefined,
|
|
326
|
+
}));
|
|
292
327
|
}
|
|
293
328
|
catch (error) {
|
|
294
329
|
options.onProgress?.(`Warning: Failed to revert config entry ${entry.key} during rollback — ${errorMessage(error)}`);
|
|
@@ -303,7 +338,7 @@ export async function rollBackInstalledAssets(installed, before, lockfile, optio
|
|
|
303
338
|
const { configKey, enabledListFile, enabledListKey } = placement ?? {};
|
|
304
339
|
const names = (result.enabledListAdded ?? []).filter((name) => !kept.has(`${configKey}.${name}`));
|
|
305
340
|
if (enabledListFile && enabledListKey && names.length > 0) {
|
|
306
|
-
await removeFromEnabledList(
|
|
341
|
+
await removeFromEnabledList(fromRoot(options.projectDir, enabledListFile), enabledListKey, names, result.configFilesCreated?.includes(enabledListFile) ?? false, options.onProgress);
|
|
307
342
|
}
|
|
308
343
|
// Revert the lockfile entry using the in-memory lockfile (best-effort). An asset that
|
|
309
344
|
// was already recorded before this plan ran (a sync/update reinstall, or a second
|
|
@@ -377,7 +412,7 @@ function buildManagedEntries(lockfile, projectDir, adapter) {
|
|
|
377
412
|
// A hook's event entry is an array every hook shares; its own group is found by its command.
|
|
378
413
|
if (entry.command !== undefined)
|
|
379
414
|
continue;
|
|
380
|
-
addOwner(entries, sharedEntryKey({ file:
|
|
415
|
+
addOwner(entries, sharedEntryKey({ file: fromRoot(projectDir, entry.file), key: entry.key }), asset);
|
|
381
416
|
}
|
|
382
417
|
}
|
|
383
418
|
return entries;
|
|
@@ -387,15 +422,25 @@ function buildManagedEntries(lockfile, projectDir, adapter) {
|
|
|
387
422
|
* Used for conflict detection — an asset may overwrite only the files it installed itself.
|
|
388
423
|
* Resolves installed paths dynamically via the adapter rather than reading them from the lockfile.
|
|
389
424
|
*/
|
|
390
|
-
function buildManagedPaths(lockfile, projectDir, adapter) {
|
|
425
|
+
async function buildManagedPaths(lockfile, projectDir, adapter) {
|
|
391
426
|
const paths = new Map();
|
|
392
427
|
for (const asset of lockfile.assets ?? []) {
|
|
393
428
|
// Uninstalled for this tool, or a hook never placed by it: a file at its path is not ATK's.
|
|
394
429
|
if (isExcludedForTool(asset, adapter.tool))
|
|
395
430
|
continue;
|
|
396
|
-
const resolved = resolveInstalledPaths(asset, adapter, projectDir);
|
|
431
|
+
const resolved = resolveInstalledPaths(asset, adapterAsPlaced(adapter, asset), projectDir);
|
|
397
432
|
for (const file of resolved.files) {
|
|
398
|
-
addOwner(paths,
|
|
433
|
+
addOwner(paths, fromRoot(projectDir, file.installedPath), asset);
|
|
434
|
+
}
|
|
435
|
+
// Its tool's user config directory moved since (the user moved or copied it): a file in the
|
|
436
|
+
// new one that holds what ATK placed is the asset's own copy, so moving the asset there may
|
|
437
|
+
// write over it. Any other file there is not ATK's.
|
|
438
|
+
if (configDirMove(adapter, asset)) {
|
|
439
|
+
for (const file of resolveInstalledPaths(asset, adapter, projectDir).files) {
|
|
440
|
+
const path = fromRoot(projectDir, file.installedPath);
|
|
441
|
+
if (await verifyChecksum(path, file.checksum).catch(() => false))
|
|
442
|
+
addOwner(paths, path, asset);
|
|
443
|
+
}
|
|
399
444
|
}
|
|
400
445
|
}
|
|
401
446
|
return paths;
|
|
@@ -578,7 +623,7 @@ async function installAsset(entry, options, managedPaths, managedEntries, owners
|
|
|
578
623
|
// The target config file's serialization format (JSON by default; TOML for tools like Codex
|
|
579
624
|
// CLI), resolved once here and threaded into the entry checks and every merge helper.
|
|
580
625
|
const configFormat = placement.configFile
|
|
581
|
-
? resolveConfigFormat(
|
|
626
|
+
? resolveConfigFormat(fromRoot(basePath, placement.configFile), placement)
|
|
582
627
|
: 'json';
|
|
583
628
|
// Check the block or config entries the asset writes into shared files before writing anything,
|
|
584
629
|
// so a conflict leaves another asset's entry, or one the user wrote, as it was.
|
|
@@ -687,7 +732,7 @@ async function installAsset(entry, options, managedPaths, managedEntries, owners
|
|
|
687
732
|
}
|
|
688
733
|
// The tool runs a registered hook script directly, so it needs the exec bit.
|
|
689
734
|
const scriptPath = hookScriptPath(placement, asset.name);
|
|
690
|
-
const scriptDest = scriptPath ?
|
|
735
|
+
const scriptDest = scriptPath ? fromRoot(basePath, scriptPath) : undefined;
|
|
691
736
|
if (scriptDest && filesPlaced.some((f) => f.destination === scriptDest)) {
|
|
692
737
|
await makeExecutable(scriptDest);
|
|
693
738
|
}
|
|
@@ -698,17 +743,20 @@ async function installAsset(entry, options, managedPaths, managedEntries, owners
|
|
|
698
743
|
// The config files the writes below can create, which a rollback then removes once empty.
|
|
699
744
|
const absentConfigFiles = [placement.configFile, placement.enabledListFile]
|
|
700
745
|
.filter((file) => !!file)
|
|
701
|
-
.map((file) =>
|
|
746
|
+
.map((file) => fromRoot(basePath, file))
|
|
702
747
|
.filter((path) => !existsSync(path));
|
|
703
748
|
// The names the install adds to the placement's enabled list, which a rollback takes out again.
|
|
704
749
|
let enabledListAdded = [];
|
|
750
|
+
// A config file the tool rewrites while it runs is written under its lock, through a rename.
|
|
751
|
+
const lockFile = placement.configFile ? configLockPath(placement, basePath, placement.configFile) : undefined;
|
|
752
|
+
const writeOptions = { atomic: lockFile !== undefined };
|
|
705
753
|
// Handle config file updates if needed.
|
|
706
754
|
if (placement.configFile && placement.configKey && placement.configHookMerge) {
|
|
707
755
|
// Hook placement: merge the asset's hookConfig event bindings into the tool
|
|
708
756
|
// config file as valid event arrays (e.g. settings.json hooks.Stop[]), with the
|
|
709
757
|
// command derived from the installed script path.
|
|
710
|
-
const
|
|
711
|
-
const hookEntries = await mergeHookConfig(
|
|
758
|
+
const { configFile, configKey } = placement;
|
|
759
|
+
const hookEntries = await withConfigLock(lockFile, () => mergeHookConfig(fromRoot(basePath, configFile), configKey, configFormat, entry.versionData.hookConfig, hookCommand(placement, asset.name), projectDir, onProgress, writeOptions));
|
|
712
760
|
configEntries.push(...hookEntries);
|
|
713
761
|
}
|
|
714
762
|
else if (placement.configFile && placement.configKey) {
|
|
@@ -717,16 +765,18 @@ async function installAsset(entry, options, managedPaths, managedEntries, owners
|
|
|
717
765
|
// (e.g. a whole .mcp.json). Merge the entries inside content[configKey] into
|
|
718
766
|
// configFile[configKey] keyed by each entry's own name, instead of nesting the
|
|
719
767
|
// whole content under the asset name.
|
|
720
|
-
const {
|
|
768
|
+
const { configCommandEntryDefaults, configEntryDefaults, configFile, configFragmentKey, configKey } = placement;
|
|
769
|
+
const { entries, names } = await withConfigLock(lockFile, () => mergeFragmentConfig(fromRoot(basePath, configFile), configKey, configFormat, projectDir, mergeContent, configFragmentKey, onProgress, writeOptions, configEntryDefaults, configCommandEntryDefaults));
|
|
721
770
|
configEntries.push(...entries);
|
|
722
771
|
// Auto-register the merged entry names into an enabled-list (e.g. enabledMcpjsonServers
|
|
723
772
|
// in settings.local.json) so the tool auto-approves them.
|
|
724
773
|
if (names.length > 0 && placement.enabledListFile && placement.enabledListKey) {
|
|
725
|
-
enabledListAdded = await addToEnabledList(
|
|
774
|
+
enabledListAdded = await addToEnabledList(fromRoot(basePath, placement.enabledListFile), placement.enabledListKey, names, onProgress);
|
|
726
775
|
}
|
|
727
776
|
}
|
|
728
777
|
else {
|
|
729
|
-
const
|
|
778
|
+
const { configFile, configKey } = placement;
|
|
779
|
+
const configResult = await withConfigLock(lockFile, () => updateConfigFile(fromRoot(basePath, configFile), configKey, configFormat, asset.name, asset.type, projectDir, mergeContent, onProgress, writeOptions));
|
|
730
780
|
if (configResult) {
|
|
731
781
|
configEntries.push(configResult);
|
|
732
782
|
}
|
|
@@ -736,10 +786,21 @@ async function installAsset(entry, options, managedPaths, managedEntries, owners
|
|
|
736
786
|
const configFilesCreated = absentConfigFiles
|
|
737
787
|
.filter((path) => existsSync(path))
|
|
738
788
|
.map((path) => toRelativePosix(projectDir, path));
|
|
789
|
+
const previousEntry = (lockfile.assets ?? []).find((a) => a.name === asset.name && a.type === asset.type && a.org === entry.asset.org);
|
|
790
|
+
// Only once the asset is registered where the placement says: a merge that failed (it warns and
|
|
791
|
+
// writes nothing) must not take away the old entry as well.
|
|
792
|
+
if (configEntries.length > 0) {
|
|
793
|
+
await removeMovedConfigEntries(previousEntry, adapter, placement, lockfile, projectDir, onProgress);
|
|
794
|
+
}
|
|
795
|
+
// The tool's user config directory moved since the previous copy was placed (`install --force`,
|
|
796
|
+
// another version, a reinstall): this one is where the tool reads it now, so the old copy's files
|
|
797
|
+
// go too, once the whole plan is in place (see executeInstallPlan).
|
|
798
|
+
const movedCopy = previousEntry && !isExcludedForTool(previousEntry, adapter.tool) && configDirMove(adapter, previousEntry)
|
|
799
|
+
? previousEntry
|
|
800
|
+
: undefined;
|
|
739
801
|
// Config entries are tool-specific but the lockfile is shared across tools: tag this
|
|
740
802
|
// tool's entries and carry over whatever other tools already registered for this asset,
|
|
741
803
|
// so a multi-tool install/sync/update never drops another tool's registration record.
|
|
742
|
-
const previousEntry = (lockfile.assets ?? []).find((a) => a.name === asset.name && a.type === asset.type && a.org === entry.asset.org);
|
|
743
804
|
const lockfileConfigEntries = mergeConfigEntriesForTool(previousEntry, adapter, configEntries);
|
|
744
805
|
// Installing for a tool re-includes it; exclusions recorded for other tools carry over.
|
|
745
806
|
const exclusions = carryExclusions(previousEntry, adapter.tool);
|
|
@@ -753,6 +814,8 @@ async function installAsset(entry, options, managedPaths, managedEntries, owners
|
|
|
753
814
|
? previousEntry.pinnedVersion
|
|
754
815
|
: undefined;
|
|
755
816
|
const bundleOrigin = bundleOriginFor(previousEntry, options.bundleOrigin);
|
|
817
|
+
// Where this tool's user config directory is, so its copy is found there once the directory moves.
|
|
818
|
+
const userConfigDirs = recordUserConfigDir(previousEntry?.userConfigDirs, adapter);
|
|
756
819
|
// Update lockfile — use resolved version and per-version dependency data
|
|
757
820
|
const installedAsset = {
|
|
758
821
|
...(bundleOrigin ? { bundleOrigin } : {}),
|
|
@@ -772,6 +835,7 @@ async function installAsset(entry, options, managedPaths, managedEntries, owners
|
|
|
772
835
|
...(pinnedVersion ? { pinnedVersion } : {}),
|
|
773
836
|
type: asset.type,
|
|
774
837
|
...(exclusions.undeclaredTools ? { undeclaredTools: exclusions.undeclaredTools } : {}),
|
|
838
|
+
...(userConfigDirs ? { userConfigDirs } : {}),
|
|
775
839
|
version: entry.version,
|
|
776
840
|
};
|
|
777
841
|
const updatedLockfile = addAssetToLockfile(lockfile, installedAsset);
|
|
@@ -779,6 +843,7 @@ async function installAsset(entry, options, managedPaths, managedEntries, owners
|
|
|
779
843
|
onProgress?.(`Installed ${asset.name}@${entry.version}`);
|
|
780
844
|
return {
|
|
781
845
|
lockfile: updatedLockfile,
|
|
846
|
+
...(movedCopy ? { movedCopy } : {}),
|
|
782
847
|
result: {
|
|
783
848
|
...(blockWritten ? { blockWritten } : {}),
|
|
784
849
|
...(bundleOrigin ? { bundleOrigin } : {}),
|
|
@@ -805,7 +870,7 @@ function isRecord(value) {
|
|
|
805
870
|
/** The `<!-- ATK: {name} -->` block a memory-template asset writes into its placement's file. */
|
|
806
871
|
function memoryTemplateBlock(placement, name, basePath) {
|
|
807
872
|
return {
|
|
808
|
-
file:
|
|
873
|
+
file: fromRoot(basePath, placement.path.replace(/\{name\}/g, name)),
|
|
809
874
|
key: `ATK:${name}`,
|
|
810
875
|
label: `"${name}" block`,
|
|
811
876
|
};
|
|
@@ -819,9 +884,12 @@ function memoryTemplateBlock(placement, name, basePath) {
|
|
|
819
884
|
* with the `_managedBy: 'atk'` marker. Returns one config entry per merged name (for lockfile
|
|
820
885
|
* tracking / checksum verification) plus the list of names that were merged.
|
|
821
886
|
*
|
|
822
|
-
* The entries are found by {@link fragmentEntries}.
|
|
887
|
+
* The entries are found by {@link fragmentEntries}. `entryDefaults` (the placement's
|
|
888
|
+
* `configEntryDefaults`) fills in the keys an entry leaves out, and `commandEntryDefaults`
|
|
889
|
+
* (`configCommandEntryDefaults`) those of an entry with a `command`, a local stdio server; the
|
|
890
|
+
* entry's own keys win.
|
|
823
891
|
*/
|
|
824
|
-
async function mergeFragmentConfig(configFilePath, configKey, format, projectDir, mergeContent, fragmentKey, onProgress) {
|
|
892
|
+
async function mergeFragmentConfig(configFilePath, configKey, format, projectDir, mergeContent, fragmentKey, onProgress, writeOptions = {}, entryDefaults = {}, commandEntryDefaults = {}) {
|
|
825
893
|
const empty = { entries: [], names: [] };
|
|
826
894
|
if (!mergeContent) {
|
|
827
895
|
return empty;
|
|
@@ -846,8 +914,10 @@ async function mergeFragmentConfig(configFilePath, configKey, format, projectDir
|
|
|
846
914
|
}
|
|
847
915
|
const section = config[configKey];
|
|
848
916
|
const entries = [];
|
|
917
|
+
const commandDefaults = { ...entryDefaults, ...commandEntryDefaults };
|
|
849
918
|
for (const [name, fragment] of inner) {
|
|
850
|
-
const
|
|
919
|
+
const defaults = 'command' in fragment ? commandDefaults : entryDefaults;
|
|
920
|
+
const value = normalizeForFormat({ ...defaults, ...fragment, _managedBy: 'atk' }, format);
|
|
851
921
|
section[name] = value;
|
|
852
922
|
entries.push({
|
|
853
923
|
checksum: computeConfigEntryChecksum(value),
|
|
@@ -855,7 +925,7 @@ async function mergeFragmentConfig(configFilePath, configKey, format, projectDir
|
|
|
855
925
|
key: `${configKey}.${name}`,
|
|
856
926
|
});
|
|
857
927
|
}
|
|
858
|
-
await writeConfigFile(configFilePath, config, format, onProgress);
|
|
928
|
+
await writeConfigFile(configFilePath, config, format, onProgress, writeOptions);
|
|
859
929
|
return { entries, names: inner.map(([name]) => name) };
|
|
860
930
|
}
|
|
861
931
|
catch (error) {
|
|
@@ -874,7 +944,7 @@ async function mergeFragmentConfig(configFilePath, configKey, format, projectDir
|
|
|
874
944
|
* owned by this command is dropped before appending, so repeated install/sync doesn't duplicate.
|
|
875
945
|
* Returns one lockfile config entry per event, checksummed over the ATK-owned group only.
|
|
876
946
|
*/
|
|
877
|
-
async function mergeHookConfig(configFilePath, configKey, format, hookConfig, command, projectDir, onProgress) {
|
|
947
|
+
async function mergeHookConfig(configFilePath, configKey, format, hookConfig, command, projectDir, onProgress, writeOptions = {}) {
|
|
878
948
|
const events = hookConfig ? Object.keys(hookConfig) : [];
|
|
879
949
|
if (events.length === 0) {
|
|
880
950
|
// No hookConfig (e.g. legacy asset or un-rebuilt registry index) — write nothing.
|
|
@@ -910,7 +980,7 @@ async function mergeHookConfig(configFilePath, configKey, format, hookConfig, co
|
|
|
910
980
|
key: `${configKey}.${event}`,
|
|
911
981
|
});
|
|
912
982
|
}
|
|
913
|
-
await writeConfigFile(configFilePath, config, format, onProgress);
|
|
983
|
+
await writeConfigFile(configFilePath, config, format, onProgress, writeOptions);
|
|
914
984
|
return entries;
|
|
915
985
|
}
|
|
916
986
|
catch (error) {
|
|
@@ -943,12 +1013,37 @@ function plannedSharedEntries(name, type, placement, basePath, mergeContent) {
|
|
|
943
1013
|
...entries,
|
|
944
1014
|
...names.map((entryName) => ({
|
|
945
1015
|
entry: { name: entryName, section: configKey },
|
|
946
|
-
file:
|
|
1016
|
+
file: fromRoot(basePath, configFile),
|
|
947
1017
|
key: `${configKey}.${entryName}`,
|
|
948
1018
|
label: `"${configKey}.${entryName}" entry`,
|
|
949
1019
|
})),
|
|
950
1020
|
];
|
|
951
1021
|
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Remove the config entries `previous` recorded for the adapter's tool in a file other than the
|
|
1024
|
+
* placement's `configFile`. An adapter that moved its config file (copilot-cli's mcp-config, from
|
|
1025
|
+
* `copilot/mcp-config.json` to `.vscode/mcp.json`) left them where the tool doesn't read them, and
|
|
1026
|
+
* this install has just registered the asset where it does. As with an uninstall, an entry holding
|
|
1027
|
+
* the value of another asset that owns it stays, for that asset's own sync to move. A file left
|
|
1028
|
+
* empty is deleted. (`atk sync` removes the asset before reinstalling it, so there it finds the
|
|
1029
|
+
* entries gone already; it is `install` and `update` that need it.)
|
|
1030
|
+
*/
|
|
1031
|
+
async function removeMovedConfigEntries(previous, adapter, placement, lockfile, projectDir, onProgress) {
|
|
1032
|
+
if (!previous || !placement.configFile)
|
|
1033
|
+
return;
|
|
1034
|
+
const configFile = normalizeConfigPath(placement.configFile);
|
|
1035
|
+
const moved = getConfigEntriesForTool(previous, adapter).filter((e) => normalizeConfigPath(e.file) !== configFile);
|
|
1036
|
+
if (moved.length === 0)
|
|
1037
|
+
return;
|
|
1038
|
+
const owners = configEntriesOwnedByOthers(lockfile, previous, adapter, projectDir);
|
|
1039
|
+
for (const entry of moved) {
|
|
1040
|
+
const configPath = fromRoot(projectDir, entry.file);
|
|
1041
|
+
if (await holdsAnotherOwnersValue(entry, owners, projectDir))
|
|
1042
|
+
continue;
|
|
1043
|
+
// The old file's own format: the placement's describes the file it uses now.
|
|
1044
|
+
await revertConfigEntry(configPath, entry.key, entry.command, resolveConfigFormat(configPath), true, onProgress);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
952
1047
|
/**
|
|
953
1048
|
* Resolve the destination file path for an asset file.
|
|
954
1049
|
* For directory-based placement (entrypointName set), the path is a directory template.
|
|
@@ -959,18 +1054,18 @@ function resolveFilePath(pathTemplate, assetName, basePath, relativePath, assetT
|
|
|
959
1054
|
const resolvedTemplate = pathTemplate.replace(/\{name\}/g, assetName);
|
|
960
1055
|
if (entrypointName) {
|
|
961
1056
|
// Directory-based placement: path is a directory, entrypoint gets the fixed name
|
|
962
|
-
const dirPath = resolvedTemplate;
|
|
1057
|
+
const dirPath = fromRoot(basePath, resolvedTemplate);
|
|
963
1058
|
if (isEntrypoint(relativePath, assetType, declaredEntrypoint)) {
|
|
964
|
-
return join(
|
|
1059
|
+
return join(dirPath, entrypointName);
|
|
965
1060
|
}
|
|
966
|
-
return join(
|
|
1061
|
+
return join(dirPath, relativePath);
|
|
967
1062
|
}
|
|
968
1063
|
// Default: single-file placement
|
|
969
|
-
const
|
|
1064
|
+
const templatePath = fromRoot(basePath, resolvedTemplate);
|
|
970
1065
|
if (isEntrypoint(relativePath, assetType, declaredEntrypoint)) {
|
|
971
|
-
return
|
|
1066
|
+
return templatePath;
|
|
972
1067
|
}
|
|
973
|
-
return join(
|
|
1068
|
+
return join(dirname(templatePath), relativePath);
|
|
974
1069
|
}
|
|
975
1070
|
/**
|
|
976
1071
|
* Revert a config file entry created during installation.
|
|
@@ -978,7 +1073,7 @@ function resolveFilePath(pathTemplate, assetName, basePath, relativePath, assetT
|
|
|
978
1073
|
* and writes it back. If the parent section becomes empty, it is also removed. A file the
|
|
979
1074
|
* install created (`deleteWhenEmpty`) is deleted instead once nothing is left in it.
|
|
980
1075
|
*/
|
|
981
|
-
async function revertConfigEntry(configFilePath, keyPath, command, format, deleteWhenEmpty, onProgress) {
|
|
1076
|
+
async function revertConfigEntry(configFilePath, keyPath, command, format, deleteWhenEmpty, onProgress, writeOptions = {}) {
|
|
982
1077
|
if (!existsSync(configFilePath)) {
|
|
983
1078
|
return;
|
|
984
1079
|
}
|
|
@@ -1009,10 +1104,10 @@ async function revertConfigEntry(configFilePath, keyPath, command, format, delet
|
|
|
1009
1104
|
}
|
|
1010
1105
|
}
|
|
1011
1106
|
if (deleteWhenEmpty && Object.keys(config).length === 0) {
|
|
1012
|
-
await
|
|
1107
|
+
await removeEmptiedConfigFile(configFilePath, format, writeOptions);
|
|
1013
1108
|
return;
|
|
1014
1109
|
}
|
|
1015
|
-
await writeConfigFile(configFilePath, config, format, onProgress);
|
|
1110
|
+
await writeConfigFile(configFilePath, config, format, onProgress, writeOptions);
|
|
1016
1111
|
}
|
|
1017
1112
|
catch (error) {
|
|
1018
1113
|
onProgress?.(`Warning: Failed to revert config entry ${keyPath} in ${configFilePath} — ${errorMessage(error)}`);
|
|
@@ -1024,7 +1119,7 @@ async function revertConfigEntry(configFilePath, keyPath, command, format, delet
|
|
|
1024
1119
|
* created it.
|
|
1025
1120
|
*/
|
|
1026
1121
|
async function revertMemoryBlock(projectDir, assetName, { created, file, replaced }, onProgress) {
|
|
1027
|
-
const path =
|
|
1122
|
+
const path = fromRoot(projectDir, file);
|
|
1028
1123
|
try {
|
|
1029
1124
|
if (!existsSync(path))
|
|
1030
1125
|
return;
|
|
@@ -1062,14 +1157,15 @@ function stripAtkBlock(content, assetName) {
|
|
|
1062
1157
|
* The refusal of a plan that none of `tools` installs anything of because it holds a hook whose
|
|
1063
1158
|
* manifest declares none of them; undefined when the plan has no such hook.
|
|
1064
1159
|
*/
|
|
1065
|
-
function undeclaredHookError(plan, tools) {
|
|
1160
|
+
function undeclaredHookError(plan, tools, scope) {
|
|
1066
1161
|
const hook = plan.entries.find((entry) => entry.asset.type === 'hook' && !tools.some((tool) => declaresTool(entry.versionData.tools, tool)));
|
|
1067
1162
|
if (!hook)
|
|
1068
1163
|
return undefined;
|
|
1164
|
+
const setup = scopedCommand('atk setup', scope);
|
|
1069
1165
|
return new InstallerError(`Hook "${formatOrgName(hook.asset.org, hook.asset.name)}" is for ${hook.versionData.tools.join(', ')} only, ` +
|
|
1070
1166
|
`so it is not installed for ${tools.join(', ')}.`, {
|
|
1071
|
-
command:
|
|
1072
|
-
hint:
|
|
1167
|
+
command: setup,
|
|
1168
|
+
hint: `A hook is installed only for the tools its manifest lists. Add one with \`${setup}\`, or install it in a project set up for one.`,
|
|
1073
1169
|
});
|
|
1074
1170
|
}
|
|
1075
1171
|
/**
|
|
@@ -1077,7 +1173,7 @@ function undeclaredHookError(plan, tools) {
|
|
|
1077
1173
|
* When mergeContent is provided, it is merged into the config section with _managedBy marker.
|
|
1078
1174
|
* Otherwise, a simple marker is written.
|
|
1079
1175
|
*/
|
|
1080
|
-
async function updateConfigFile(configFilePath, configKey, format, assetName, assetType, projectDir, mergeContent, onProgress) {
|
|
1176
|
+
async function updateConfigFile(configFilePath, configKey, format, assetName, assetType, projectDir, mergeContent, onProgress, writeOptions = {}) {
|
|
1081
1177
|
try {
|
|
1082
1178
|
let config = {};
|
|
1083
1179
|
if (existsSync(configFilePath)) {
|
|
@@ -1108,7 +1204,7 @@ async function updateConfigFile(configFilePath, configKey, format, assetName, as
|
|
|
1108
1204
|
type: assetType,
|
|
1109
1205
|
};
|
|
1110
1206
|
}
|
|
1111
|
-
await writeConfigFile(configFilePath, config, format, onProgress);
|
|
1207
|
+
await writeConfigFile(configFilePath, config, format, onProgress, writeOptions);
|
|
1112
1208
|
const checksum = computeConfigEntryChecksum(section[assetName]);
|
|
1113
1209
|
return { checksum, file: toRelativePosix(projectDir, configFilePath), key: `${configKey}.${assetName}` };
|
|
1114
1210
|
}
|