@detergent-software/atk 8.1.2-dev.3 → 8.1.2-dev.30
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 +195 -0
- package/build/commands/browse.js +5 -9
- package/build/commands/browse.js.map +1 -1
- package/build/commands/install.js +2 -2
- package/build/commands/install.js.map +1 -1
- package/build/commands/list.js +2 -2
- package/build/commands/list.js.map +1 -1
- package/build/commands/login.js +2 -2
- package/build/commands/login.js.map +1 -1
- package/build/commands/register.js +3 -3
- package/build/commands/register.js.map +1 -1
- package/build/commands/sync.d.ts.map +1 -1
- package/build/commands/sync.js +16 -11
- package/build/commands/sync.js.map +1 -1
- package/build/commands/uninstall.d.ts +9 -1
- package/build/commands/uninstall.d.ts.map +1 -1
- package/build/commands/uninstall.js +23 -26
- package/build/commands/uninstall.js.map +1 -1
- package/build/commands/update.d.ts +9 -1
- package/build/commands/update.d.ts.map +1 -1
- package/build/commands/update.js +54 -14
- package/build/commands/update.js.map +1 -1
- package/build/components/InstallConfirmBar.d.ts.map +1 -1
- package/build/components/InstallConfirmBar.js +2 -1
- package/build/components/InstallConfirmBar.js.map +1 -1
- package/build/components/ListActionBar.d.ts.map +1 -1
- package/build/components/ListActionBar.js +3 -2
- package/build/components/ListActionBar.js.map +1 -1
- package/build/components/RunStoppedHint.d.ts +13 -0
- package/build/components/RunStoppedHint.d.ts.map +1 -0
- package/build/components/RunStoppedHint.js +12 -0
- package/build/components/RunStoppedHint.js.map +1 -0
- package/build/components/SkippedAssetSection.d.ts +2 -0
- package/build/components/SkippedAssetSection.d.ts.map +1 -1
- package/build/components/SkippedAssetSection.js +1 -1
- package/build/components/SkippedAssetSection.js.map +1 -1
- package/build/components/SyncDriftedList.d.ts +3 -2
- package/build/components/SyncDriftedList.d.ts.map +1 -1
- package/build/components/SyncDriftedList.js +11 -3
- package/build/components/SyncDriftedList.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/useCommand.js +2 -2
- package/build/hooks/useCommand.js.map +1 -1
- package/build/hooks/useInitState.js +5 -5
- package/build/hooks/useInitState.js.map +1 -1
- package/build/hooks/useListActions.d.ts.map +1 -1
- package/build/hooks/useListActions.js +34 -18
- package/build/hooks/useListActions.js.map +1 -1
- package/build/hooks/usePruneState.js +7 -7
- package/build/hooks/usePruneState.js.map +1 -1
- package/build/hooks/usePublishState.js +3 -3
- package/build/hooks/usePublishState.js.map +1 -1
- package/build/hooks/useRegistryChangeState.js +2 -2
- package/build/hooks/useRegistryChangeState.js.map +1 -1
- package/build/hooks/useSetupState.js +3 -3
- package/build/hooks/useSetupState.js.map +1 -1
- package/build/hooks/useUninstallState.d.ts +16 -29
- package/build/hooks/useUninstallState.d.ts.map +1 -1
- package/build/hooks/useUninstallState.js +147 -140
- package/build/hooks/useUninstallState.js.map +1 -1
- package/build/lib/adapter.d.ts.map +1 -1
- package/build/lib/adapter.js +3 -3
- package/build/lib/adapter.js.map +1 -1
- package/build/lib/asset-tools.d.ts +5 -0
- package/build/lib/asset-tools.d.ts.map +1 -1
- package/build/lib/asset-tools.js +7 -0
- package/build/lib/asset-tools.js.map +1 -1
- package/build/lib/auth.d.ts.map +1 -1
- package/build/lib/auth.js +3 -3
- package/build/lib/auth.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/diagnostics.d.ts.map +1 -1
- package/build/lib/diagnostics.js +6 -5
- package/build/lib/diagnostics.js.map +1 -1
- package/build/lib/diff-command.d.ts.map +1 -1
- package/build/lib/diff-command.js +10 -6
- package/build/lib/diff-command.js.map +1 -1
- package/build/lib/entra.d.ts.map +1 -1
- package/build/lib/entra.js +3 -2
- package/build/lib/entra.js.map +1 -1
- package/build/lib/errors.d.ts +9 -0
- package/build/lib/errors.d.ts.map +1 -1
- package/build/lib/errors.js +29 -0
- package/build/lib/errors.js.map +1 -1
- package/build/lib/file-snapshot.d.ts +17 -0
- package/build/lib/file-snapshot.d.ts.map +1 -0
- package/build/lib/file-snapshot.js +41 -0
- package/build/lib/file-snapshot.js.map +1 -0
- package/build/lib/installer.d.ts +61 -0
- package/build/lib/installer.d.ts.map +1 -1
- package/build/lib/installer.js +148 -43
- package/build/lib/installer.js.map +1 -1
- package/build/lib/lockfile.d.ts +23 -4
- package/build/lib/lockfile.d.ts.map +1 -1
- package/build/lib/lockfile.js +32 -14
- package/build/lib/lockfile.js.map +1 -1
- package/build/lib/org.d.ts +16 -0
- package/build/lib/org.d.ts.map +1 -1
- package/build/lib/org.js +18 -0
- package/build/lib/org.js.map +1 -1
- package/build/lib/owned-paths.d.ts +9 -0
- package/build/lib/owned-paths.d.ts.map +1 -0
- package/build/lib/owned-paths.js +23 -0
- package/build/lib/owned-paths.js.map +1 -0
- package/build/lib/publisher.d.ts.map +1 -1
- package/build/lib/publisher.js +2 -1
- package/build/lib/publisher.js.map +1 -1
- package/build/lib/registry.d.ts.map +1 -1
- package/build/lib/registry.js +10 -6
- package/build/lib/registry.js.map +1 -1
- package/build/lib/setup.d.ts.map +1 -1
- package/build/lib/setup.js +4 -3
- package/build/lib/setup.js.map +1 -1
- package/build/lib/sync.d.ts +17 -4
- package/build/lib/sync.d.ts.map +1 -1
- package/build/lib/sync.js +110 -41
- package/build/lib/sync.js.map +1 -1
- package/build/lib/uninstaller.d.ts +21 -4
- package/build/lib/uninstaller.d.ts.map +1 -1
- package/build/lib/uninstaller.js +45 -36
- package/build/lib/uninstaller.js.map +1 -1
- package/build/lib/updater.d.ts +64 -3
- package/build/lib/updater.d.ts.map +1 -1
- package/build/lib/updater.js +355 -83
- package/build/lib/updater.js.map +1 -1
- package/package.json +1 -1
package/build/lib/errors.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ export declare abstract class ApiCodeError extends GuidedError {
|
|
|
45
45
|
export declare class SetupRequiredError extends GuidedError {
|
|
46
46
|
name: string;
|
|
47
47
|
}
|
|
48
|
+
/** Text as a sentence: a period is added only when it doesn't already end with one. */
|
|
49
|
+
export declare function asSentence(text: string): string;
|
|
48
50
|
/**
|
|
49
51
|
* Read `code`, `command` and `hint` off any thrown value (ApiRequestError,
|
|
50
52
|
* AuthError, SetupRequiredError, ...), keeping each only when it is a string.
|
|
@@ -52,4 +54,11 @@ export declare class SetupRequiredError extends GuidedError {
|
|
|
52
54
|
* error state and JSON payload; rewraps and failure entries carry it on.
|
|
53
55
|
*/
|
|
54
56
|
export declare function errorGuidance(err: unknown): ErrorGuidance;
|
|
57
|
+
/** The message of a thrown value: an Error's `message`, anything else as a string. */
|
|
58
|
+
export declare function errorMessage(err: unknown): string;
|
|
59
|
+
/**
|
|
60
|
+
* Whether a failure's API code fails every asset of a run, so bulk update and sync
|
|
61
|
+
* stop at it instead of repeating the same failure for each remaining asset.
|
|
62
|
+
*/
|
|
63
|
+
export declare function isRunWideFailure({ code }: ErrorGuidance): boolean;
|
|
55
64
|
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,MAAM,WAAW,aAAc,SAAQ,gBAAgB;IACrD,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,gBAAgB,GAAG;IAAE,IAAI,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC;AAErE,yFAAyF;AACzF,8BAAsB,WAAY,SAAQ,KAAK;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;gBAEF,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB;CAK9D;AAED;;;GAGG;AACH,8BAAsB,YAAa,SAAQ,WAAW;IACpD,IAAI,CAAC,EAAE,MAAM,CAAC;gBAEF,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB;CAIzD;AAED;;;;;;GAMG;AACH,qBAAa,kBAAmB,SAAQ,WAAW;IACxC,IAAI,SAAwB;CACtC;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,CAWzD"}
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,MAAM,WAAW,aAAc,SAAQ,gBAAgB;IACrD,kFAAkF;IAClF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,gBAAgB,GAAG;IAAE,IAAI,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC;AAErE,yFAAyF;AACzF,8BAAsB,WAAY,SAAQ,KAAK;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;gBAEF,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB;CAK9D;AAED;;;GAGG;AACH,8BAAsB,YAAa,SAAQ,WAAW;IACpD,IAAI,CAAC,EAAE,MAAM,CAAC;gBAEF,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB;CAIzD;AAED;;;;;;GAMG;AACH,qBAAa,kBAAmB,SAAQ,WAAW;IACxC,IAAI,SAAwB;CACtC;AAED,uFAAuF;AACvF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,CAWzD;AAiBD,sFAAsF;AACtF,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAEjD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,IAAI,EAAE,EAAE,aAAa,GAAG,OAAO,CAEjE"}
|
package/build/lib/errors.js
CHANGED
|
@@ -29,6 +29,10 @@ export class ApiCodeError extends GuidedError {
|
|
|
29
29
|
export class SetupRequiredError extends GuidedError {
|
|
30
30
|
name = 'SetupRequiredError';
|
|
31
31
|
}
|
|
32
|
+
/** Text as a sentence: a period is added only when it doesn't already end with one. */
|
|
33
|
+
export function asSentence(text) {
|
|
34
|
+
return /[.!?]$/.test(text) ? text : `${text}.`;
|
|
35
|
+
}
|
|
32
36
|
/**
|
|
33
37
|
* Read `code`, `command` and `hint` off any thrown value (ApiRequestError,
|
|
34
38
|
* AuthError, SetupRequiredError, ...), keeping each only when it is a string.
|
|
@@ -43,4 +47,29 @@ export function errorGuidance(err) {
|
|
|
43
47
|
...(typeof hint === 'string' ? { hint } : {}),
|
|
44
48
|
};
|
|
45
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* API codes that fail every request of a run the same way, whatever the asset: an
|
|
52
|
+
* outdated CLI, a rejected credential, or a caller outside the org.
|
|
53
|
+
*/
|
|
54
|
+
const RUN_WIDE_CODES = new Set([
|
|
55
|
+
'cli_outdated',
|
|
56
|
+
'github_auth_retired',
|
|
57
|
+
'guest_not_allowed',
|
|
58
|
+
'invalid_token',
|
|
59
|
+
'not_org_member',
|
|
60
|
+
'org_membership_unverifiable',
|
|
61
|
+
'unauthenticated',
|
|
62
|
+
'unsupported_token',
|
|
63
|
+
]);
|
|
64
|
+
/** The message of a thrown value: an Error's `message`, anything else as a string. */
|
|
65
|
+
export function errorMessage(err) {
|
|
66
|
+
return err instanceof Error ? err.message : String(err);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Whether a failure's API code fails every asset of a run, so bulk update and sync
|
|
70
|
+
* stop at it instead of repeating the same failure for each remaining asset.
|
|
71
|
+
*/
|
|
72
|
+
export function isRunWideFailure({ code }) {
|
|
73
|
+
return code !== undefined && RUN_WIDE_CODES.has(code);
|
|
74
|
+
}
|
|
46
75
|
//# sourceMappingURL=errors.js.map
|
package/build/lib/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"AAwBA,yFAAyF;AACzF,MAAM,OAAgB,WAAY,SAAQ,KAAK;IAC7C,OAAO,CAAU;IACjB,IAAI,CAAU;IAEd,YAAY,OAAe,EAAE,UAA8B,EAAE;QAC3D,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QACzB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IACjC,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAgB,YAAa,SAAQ,WAAW;IACpD,IAAI,CAAU;IAEd,YAAY,OAAe,EAAE,UAAyB,EAAE;QACtD,KAAK,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;QACjE,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC3B,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,kBAAmB,SAAQ,WAAW;IACxC,IAAI,GAAG,oBAAoB,CAAC;CACtC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,GAAY;IACxC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAIlF,CAAC;IACF,OAAO;QACL,GAAG,CAAC,GAAG,YAAY,YAAY,IAAI,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5E,GAAG,CAAC,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnD,GAAG,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9C,CAAC;AACJ,CAAC"}
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"AAwBA,yFAAyF;AACzF,MAAM,OAAgB,WAAY,SAAQ,KAAK;IAC7C,OAAO,CAAU;IACjB,IAAI,CAAU;IAEd,YAAY,OAAe,EAAE,UAA8B,EAAE;QAC3D,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QACzB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IACjC,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAgB,YAAa,SAAQ,WAAW;IACpD,IAAI,CAAU;IAEd,YAAY,OAAe,EAAE,UAAyB,EAAE;QACtD,KAAK,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;QACjE,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAC3B,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,kBAAmB,SAAQ,WAAW;IACxC,IAAI,GAAG,oBAAoB,CAAC;CACtC;AAED,uFAAuF;AACvF,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC;AACjD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,GAAY;IACxC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAIlF,CAAC;IACF,OAAO;QACL,GAAG,CAAC,GAAG,YAAY,YAAY,IAAI,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5E,GAAG,CAAC,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnD,GAAG,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9C,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,cAAc,GAAwB,IAAI,GAAG,CAAC;IAClD,cAAc;IACd,qBAAqB;IACrB,mBAAmB;IACnB,eAAe;IACf,gBAAgB;IAChB,6BAA6B;IAC7B,iBAAiB;IACjB,mBAAmB;CACpB,CAAC,CAAC;AAEH,sFAAsF;AACtF,MAAM,UAAU,YAAY,CAAC,GAAY;IACvC,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAC1D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAAE,IAAI,EAAiB;IACtD,OAAO,IAAI,KAAK,SAAS,IAAI,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACxD,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** The bytes and mode of files captured before a change, by absolute path. */
|
|
2
|
+
export type FileSnapshot = ReadonlyMap<string, {
|
|
3
|
+
content: Buffer;
|
|
4
|
+
mode: number;
|
|
5
|
+
}>;
|
|
6
|
+
/**
|
|
7
|
+
* Write every file of a snapshot back as it was, recreating its directory and mode (a hook
|
|
8
|
+
* script keeps its exec bit). Files created since the snapshot are left alone. A file that
|
|
9
|
+
* can't be written doesn't stop the others; the first error is thrown once all were tried.
|
|
10
|
+
*/
|
|
11
|
+
export declare function restoreFiles(snapshot: FileSnapshot): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* Capture the bytes and mode of each file in `paths` that exists. A missing path is skipped,
|
|
14
|
+
* including one under a file where a directory would be (`ENOTDIR` on POSIX, `ENOENT` on Windows).
|
|
15
|
+
*/
|
|
16
|
+
export declare function snapshotFiles(paths: Iterable<string>): Promise<FileSnapshot>;
|
|
17
|
+
//# sourceMappingURL=file-snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-snapshot.d.ts","sourceRoot":"","sources":["../../src/lib/file-snapshot.ts"],"names":[],"mappings":"AAIA,8EAA8E;AAC9E,MAAM,MAAM,YAAY,GAAG,WAAW,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAElF;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAWxE;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAYlF"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { chmod, readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { writeBinaryFile } from './write-file.js';
|
|
3
|
+
/**
|
|
4
|
+
* Write every file of a snapshot back as it was, recreating its directory and mode (a hook
|
|
5
|
+
* script keeps its exec bit). Files created since the snapshot are left alone. A file that
|
|
6
|
+
* can't be written doesn't stop the others; the first error is thrown once all were tried.
|
|
7
|
+
*/
|
|
8
|
+
export async function restoreFiles(snapshot) {
|
|
9
|
+
const errors = [];
|
|
10
|
+
for (const [path, { content, mode }] of snapshot) {
|
|
11
|
+
try {
|
|
12
|
+
await writeBinaryFile(path, content);
|
|
13
|
+
await chmod(path, mode);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
errors.push(error);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (errors.length > 0)
|
|
20
|
+
throw errors[0];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Capture the bytes and mode of each file in `paths` that exists. A missing path is skipped,
|
|
24
|
+
* including one under a file where a directory would be (`ENOTDIR` on POSIX, `ENOENT` on Windows).
|
|
25
|
+
*/
|
|
26
|
+
export async function snapshotFiles(paths) {
|
|
27
|
+
const snapshot = new Map();
|
|
28
|
+
for (const path of new Set(paths)) {
|
|
29
|
+
const stats = await stat(path).catch((error) => {
|
|
30
|
+
const code = error.code;
|
|
31
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
32
|
+
return undefined;
|
|
33
|
+
throw error;
|
|
34
|
+
});
|
|
35
|
+
if (!stats?.isFile())
|
|
36
|
+
continue;
|
|
37
|
+
snapshot.set(path, { content: await readFile(path), mode: stats.mode });
|
|
38
|
+
}
|
|
39
|
+
return snapshot;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=file-snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-snapshot.js","sourceRoot":"","sources":["../../src/lib/file-snapshot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAEzD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAKlD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAsB;IACvD,MAAM,MAAM,GAAc,EAAE,CAAC;IAC7B,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,IAAI,QAAQ,EAAE,CAAC;QACjD,IAAI,CAAC;YACH,MAAM,eAAe,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YACrC,MAAM,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC1B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,MAAM,CAAC,CAAC,CAAC,CAAC;AACzC,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAuB;IACzD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA6C,CAAC;IACtE,KAAK,MAAM,IAAI,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;QAClC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YACtD,MAAM,IAAI,GAAI,KAA+B,CAAC,IAAI,CAAC;YACnD,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,SAAS;gBAAE,OAAO,SAAS,CAAC;YAC9D,MAAM,KAAK,CAAC;QACd,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE;YAAE,SAAS;QAC/B,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IAC1E,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
package/build/lib/installer.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { InstallPlan } from './resolver.js';
|
|
2
2
|
import type { Adapter } from './schemas/adapter.js';
|
|
3
|
+
import type { RegistryAsset } from './schemas/index.js';
|
|
4
|
+
import type { Lockfile } from './schemas/lockfile.js';
|
|
3
5
|
import { ApiCodeError } from './errors.js';
|
|
6
|
+
import { type AssetIdentity } from './lockfile.js';
|
|
4
7
|
export interface InstallOptions {
|
|
5
8
|
/** Adapter for the target tool. */
|
|
6
9
|
adapter: Adapter;
|
|
@@ -10,8 +13,16 @@ export interface InstallOptions {
|
|
|
10
13
|
org?: string;
|
|
11
14
|
version: string;
|
|
12
15
|
};
|
|
16
|
+
/** Files already downloaded with {@link fetchPlanFiles}, for every entry this tool can place. */
|
|
17
|
+
files?: PlanFiles;
|
|
13
18
|
/** Skip file conflict detection, allowing overwrite of non-ATK files. */
|
|
14
19
|
force?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* The lockfile as this tool has it, for deciding which files are ATK-managed. In a multi-tool
|
|
22
|
+
* update the shared lockfile can already list the files of a version an earlier tool installed,
|
|
23
|
+
* which a later tool does not have yet. Defaults to the lockfile on disk.
|
|
24
|
+
*/
|
|
25
|
+
managedFrom?: Lockfile;
|
|
15
26
|
/** Callback for progress updates. */
|
|
16
27
|
onProgress?: (message: string) => void;
|
|
17
28
|
/** Pin the asset at the installed version. */
|
|
@@ -47,9 +58,46 @@ export interface InstallResult {
|
|
|
47
58
|
type: string;
|
|
48
59
|
version: string;
|
|
49
60
|
}
|
|
61
|
+
/** The downloaded files of a plan's entries, keyed by {@link planEntryKey}. */
|
|
62
|
+
export type PlanFiles = Map<string, AssetFiles>;
|
|
63
|
+
interface AssetFile {
|
|
64
|
+
/** Raw file bytes — asset files may be binary (images, spreadsheets), never assume text. */
|
|
65
|
+
content: Buffer;
|
|
66
|
+
relativePath: string;
|
|
67
|
+
}
|
|
68
|
+
interface AssetFiles {
|
|
69
|
+
entrypoint?: string;
|
|
70
|
+
files: AssetFile[];
|
|
71
|
+
}
|
|
50
72
|
export declare class InstallerError extends ApiCodeError {
|
|
51
73
|
name: string;
|
|
52
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* A file the install would place at `path` exists and is not ATK-managed, or belongs to `owner`,
|
|
77
|
+
* another installed asset; nothing was written there.
|
|
78
|
+
*/
|
|
79
|
+
export declare class FileConflictError extends InstallerError {
|
|
80
|
+
name: string;
|
|
81
|
+
readonly owner?: AssetIdentity;
|
|
82
|
+
readonly path: string;
|
|
83
|
+
constructor(path: string, owner?: AssetIdentity);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* An install plan that failed at `asset` with `cause`, after rolling back the
|
|
87
|
+
* `rolledBack` assets it had already installed. Keeps the cause's API code and
|
|
88
|
+
* guidance, so callers that name the asset themselves can quote just the cause.
|
|
89
|
+
*/
|
|
90
|
+
export declare class InstallPlanError extends InstallerError {
|
|
91
|
+
readonly asset: Pick<RegistryAsset, 'name' | 'org' | 'type'>;
|
|
92
|
+
readonly cause: unknown;
|
|
93
|
+
name: string;
|
|
94
|
+
constructor(asset: Pick<RegistryAsset, 'name' | 'org' | 'type'>, cause: unknown, rolledBack: number);
|
|
95
|
+
}
|
|
96
|
+
/** A plan whose files for `asset` could not be downloaded; nothing was written. */
|
|
97
|
+
export declare class PlanDownloadError extends InstallPlanError {
|
|
98
|
+
name: string;
|
|
99
|
+
constructor(asset: Pick<RegistryAsset, 'name' | 'org' | 'type'>, cause: unknown);
|
|
100
|
+
}
|
|
53
101
|
/**
|
|
54
102
|
* Guard for multi-tool installs run with `skipUnsupported`. Each tool skips the asset
|
|
55
103
|
* types it cannot place, which is fine as long as some tool placed something. If every
|
|
@@ -67,4 +115,17 @@ export declare function assertInstalledByAnyTool(plan: InstallPlan, toolResults:
|
|
|
67
115
|
* an inconsistent state.
|
|
68
116
|
*/
|
|
69
117
|
export declare function executeInstallPlan(plan: InstallPlan, options: InstallOptions): Promise<InstallResult[]>;
|
|
118
|
+
/**
|
|
119
|
+
* Download the files of every plan entry this tool can place, without writing anything. A
|
|
120
|
+
* caller that replaces an installed asset downloads first, so a refused download leaves the
|
|
121
|
+
* installed version in place, then passes the result to {@link executeInstallPlan} as `files`.
|
|
122
|
+
*/
|
|
123
|
+
export declare function fetchPlanFiles(plan: InstallPlan, adapter: Adapter): Promise<PlanFiles>;
|
|
124
|
+
/**
|
|
125
|
+
* The message of an install failure for `asset`, for a caller whose own message already names
|
|
126
|
+
* it: a plan that failed at that asset gives just its cause; one that failed at another asset
|
|
127
|
+
* keeps the installer's message, which names that asset.
|
|
128
|
+
*/
|
|
129
|
+
export declare function installFailureMessage(error: unknown, asset: Pick<RegistryAsset, 'name' | 'org' | 'type'>): string;
|
|
130
|
+
export {};
|
|
70
131
|
//# sourceMappingURL=installer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../../src/lib/installer.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAoB,MAAM,eAAe,CAAC;AACnE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../../src/lib/installer.ts"],"names":[],"mappings":"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"}
|
package/build/lib/installer.js
CHANGED
|
@@ -6,17 +6,59 @@ import { carryExcludedTools } from './asset-tools.js';
|
|
|
6
6
|
import { computeConfigEntryChecksum, computeHookGroupChecksum, hashBytes } from './checksum.js';
|
|
7
7
|
import { mergeConfigEntriesForTool } from './config-entries.js';
|
|
8
8
|
import { detectConfigFormat, normalizeForFormat, readConfigFile, resolveConfigFormat, writeConfigFile, } from './config-file.js';
|
|
9
|
-
import { ApiCodeError, errorGuidance } from './errors.js';
|
|
9
|
+
import { ApiCodeError, errorGuidance, errorMessage } from './errors.js';
|
|
10
10
|
import { ensureGitignoreEntries } from './gitignore.js';
|
|
11
11
|
import { buildHookGroup, deriveHookCommand, hookGroupOwnsCommand, removeHookGroup } from './hook-config.js';
|
|
12
12
|
import { hookScriptPath, makeExecutable } from './hook-script.js';
|
|
13
|
-
import { addAssetToLockfile, readLockfile, removeAssetFromLockfile, writeLockfile } from './lockfile.js';
|
|
13
|
+
import { addAssetToLockfile, readLockfile, removeAssetFromLockfile, writeLockfile, } from './lockfile.js';
|
|
14
|
+
import { formatOrgName } from './org.js';
|
|
15
|
+
import { pathsOwnedByOthers } from './owned-paths.js';
|
|
14
16
|
import { toRelativePosix } from './paths.js';
|
|
15
17
|
import { fetchAssetFileBytes, fetchAssetManifest, listAssetFiles } from './registry.js';
|
|
16
18
|
import { writeBinaryFile, writeTextFile } from './write-file.js';
|
|
17
19
|
export class InstallerError extends ApiCodeError {
|
|
18
20
|
name = 'InstallerError';
|
|
19
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* A file the install would place at `path` exists and is not ATK-managed, or belongs to `owner`,
|
|
24
|
+
* another installed asset; nothing was written there.
|
|
25
|
+
*/
|
|
26
|
+
export class FileConflictError extends InstallerError {
|
|
27
|
+
name = 'FileConflictError';
|
|
28
|
+
owner;
|
|
29
|
+
path;
|
|
30
|
+
constructor(path, owner) {
|
|
31
|
+
super(owner
|
|
32
|
+
? `File conflict: "${path}" belongs to the installed ${owner.type} "${formatOrgName(owner.org, owner.name)}".`
|
|
33
|
+
: `File conflict: "${path}" already exists and was not installed by ATK. Use --force to overwrite.`, owner ? { hint: 'Uninstall it first if you want this one instead.' } : {});
|
|
34
|
+
this.owner = owner;
|
|
35
|
+
this.path = path;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* An install plan that failed at `asset` with `cause`, after rolling back the
|
|
40
|
+
* `rolledBack` assets it had already installed. Keeps the cause's API code and
|
|
41
|
+
* guidance, so callers that name the asset themselves can quote just the cause.
|
|
42
|
+
*/
|
|
43
|
+
export class InstallPlanError extends InstallerError {
|
|
44
|
+
asset;
|
|
45
|
+
cause;
|
|
46
|
+
name = 'InstallPlanError';
|
|
47
|
+
constructor(asset, cause, rolledBack) {
|
|
48
|
+
const rollback = rolledBack > 0 ? ` and ${rolledBack} previously installed asset(s) were rolled back` : '';
|
|
49
|
+
super(`Install failed at asset "${formatOrgName(asset.org, asset.name)}"${rollback}: ${errorMessage(cause)}`, errorGuidance(cause));
|
|
50
|
+
this.asset = asset;
|
|
51
|
+
this.cause = cause;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** A plan whose files for `asset` could not be downloaded; nothing was written. */
|
|
55
|
+
export class PlanDownloadError extends InstallPlanError {
|
|
56
|
+
name = 'PlanDownloadError';
|
|
57
|
+
constructor(asset, cause) {
|
|
58
|
+
super(asset, cause, 0);
|
|
59
|
+
this.message = `Download failed for asset "${formatOrgName(asset.org, asset.name)}": ${errorMessage(cause)}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
20
62
|
/**
|
|
21
63
|
* Guard for multi-tool installs run with `skipUnsupported`. Each tool skips the asset
|
|
22
64
|
* types it cannot place, which is fine as long as some tool placed something. If every
|
|
@@ -52,8 +94,8 @@ export async function executeInstallPlan(plan, options) {
|
|
|
52
94
|
// Read lockfile once — used for both conflict detection and per-asset install logic
|
|
53
95
|
let lockfile = await readLockfile(options.projectDir);
|
|
54
96
|
const initialLockfile = lockfile;
|
|
55
|
-
//
|
|
56
|
-
const managedPaths =
|
|
97
|
+
// Map each ATK-managed file path to the assets that installed it, for conflict detection
|
|
98
|
+
const managedPaths = buildManagedPaths(options.managedFrom ?? lockfile, options.projectDir, options.adapter);
|
|
57
99
|
// In multi-tool projects the same plan runs once per tool. A tool that has no
|
|
58
100
|
// placement for an asset type (e.g. Codex CLI has no `rule` placement) skips
|
|
59
101
|
// those entries instead of aborting the whole run for that tool. When the tool
|
|
@@ -75,6 +117,9 @@ export async function executeInstallPlan(plan, options) {
|
|
|
75
117
|
for (const entry of supportedEntries) {
|
|
76
118
|
const { lockfile: updatedLockfile, result } = await installAsset(entry, options, managedPaths, lockfile);
|
|
77
119
|
results.push(result);
|
|
120
|
+
// A later entry of this plan may land on these files, and must see whose they are.
|
|
121
|
+
for (const file of result.filesPlaced)
|
|
122
|
+
addManagedPath(managedPaths, file.destination, entry.asset);
|
|
78
123
|
lockfile = updatedLockfile;
|
|
79
124
|
}
|
|
80
125
|
}
|
|
@@ -84,16 +129,21 @@ export async function executeInstallPlan(plan, options) {
|
|
|
84
129
|
options.onProgress?.(`Install failed, rolling back ${results.length} installed asset(s)...`);
|
|
85
130
|
for (let i = results.length - 1; i >= 0; i--) {
|
|
86
131
|
const result = results[i];
|
|
132
|
+
const rolledBack = supportedEntries[i].asset;
|
|
87
133
|
options.onProgress?.(`Rolling back ${result.name}...`);
|
|
88
|
-
// Delete placed files (best-effort)
|
|
134
|
+
// Delete placed files (best-effort), except one another asset installed before this plan
|
|
135
|
+
// also owns: a global and an org asset of one name and type can share a placement.
|
|
136
|
+
const ownedByOthers = pathsOwnedByOthers(initialLockfile, rolledBack, options.adapter, options.projectDir);
|
|
89
137
|
for (const file of result.filesPlaced) {
|
|
138
|
+
if (ownedByOthers.has(file.destination))
|
|
139
|
+
continue;
|
|
90
140
|
try {
|
|
91
141
|
if (existsSync(file.destination)) {
|
|
92
142
|
await unlink(file.destination);
|
|
93
143
|
}
|
|
94
144
|
}
|
|
95
145
|
catch (error) {
|
|
96
|
-
options.onProgress?.(`Warning: Failed to delete file during rollback: ${file.destination} — ${
|
|
146
|
+
options.onProgress?.(`Warning: Failed to delete file during rollback: ${file.destination} — ${errorMessage(error)}`);
|
|
97
147
|
}
|
|
98
148
|
}
|
|
99
149
|
// Revert config file entries (best-effort)
|
|
@@ -104,7 +154,7 @@ export async function executeInstallPlan(plan, options) {
|
|
|
104
154
|
await revertConfigEntry(configPath, entry.key, entry.command, format, options.onProgress);
|
|
105
155
|
}
|
|
106
156
|
catch (error) {
|
|
107
|
-
options.onProgress?.(`Warning: Failed to revert config entry ${entry.key} during rollback — ${
|
|
157
|
+
options.onProgress?.(`Warning: Failed to revert config entry ${entry.key} during rollback — ${errorMessage(error)}`);
|
|
108
158
|
}
|
|
109
159
|
}
|
|
110
160
|
// Revert the lockfile entry using the in-memory lockfile (best-effort). An asset that
|
|
@@ -112,7 +162,6 @@ export async function executeInstallPlan(plan, options) {
|
|
|
112
162
|
// tool's pass in a multi-tool install) gets its previous entry back, so other tools'
|
|
113
163
|
// config entries survive; a newly added asset is removed.
|
|
114
164
|
try {
|
|
115
|
-
const rolledBack = supportedEntries[i].asset;
|
|
116
165
|
const previousEntry = (initialLockfile.assets ?? []).find((a) => a.name === rolledBack.name && a.type === rolledBack.type && a.org === rolledBack.org);
|
|
117
166
|
lockfile = previousEntry
|
|
118
167
|
? addAssetToLockfile(lockfile, previousEntry)
|
|
@@ -120,19 +169,58 @@ export async function executeInstallPlan(plan, options) {
|
|
|
120
169
|
await writeLockfile(options.projectDir, lockfile);
|
|
121
170
|
}
|
|
122
171
|
catch (error) {
|
|
123
|
-
options.onProgress?.(`Warning: Failed to remove lockfile entry during rollback for ${result.name} — ${
|
|
172
|
+
options.onProgress?.(`Warning: Failed to remove lockfile entry during rollback for ${result.name} — ${errorMessage(error)}`);
|
|
124
173
|
}
|
|
125
174
|
}
|
|
126
175
|
options.onProgress?.(`Rollback complete.`);
|
|
127
176
|
}
|
|
128
|
-
// Re-throw the original error
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
throw new InstallerError(`Install failed at asset "${supportedEntries[results.length]?.asset.name ?? 'unknown'}" ` +
|
|
132
|
-
`and ${results.length} previously installed asset(s) were rolled back: ${originalMessage}`, errorGuidance(error));
|
|
177
|
+
// Re-throw the original error with rollback context, keeping any API error code and
|
|
178
|
+
// recovery hint/command (e.g. from an API auth failure).
|
|
179
|
+
throw new InstallPlanError(supportedEntries[results.length].asset, error, results.length);
|
|
133
180
|
}
|
|
134
181
|
return results;
|
|
135
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Download the files of every plan entry this tool can place, without writing anything. A
|
|
185
|
+
* caller that replaces an installed asset downloads first, so a refused download leaves the
|
|
186
|
+
* installed version in place, then passes the result to {@link executeInstallPlan} as `files`.
|
|
187
|
+
*/
|
|
188
|
+
export async function fetchPlanFiles(plan, adapter) {
|
|
189
|
+
const files = new Map();
|
|
190
|
+
for (const entry of plan.entries) {
|
|
191
|
+
if (!getPlacement(adapter, entry.asset.type))
|
|
192
|
+
continue;
|
|
193
|
+
try {
|
|
194
|
+
files.set(planEntryKey(entry), await fetchAssetFiles(entry.asset, entry.version, entry.asset.org));
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
throw new PlanDownloadError(entry.asset, error);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return files;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The message of an install failure for `asset`, for a caller whose own message already names
|
|
204
|
+
* it: a plan that failed at that asset gives just its cause; one that failed at another asset
|
|
205
|
+
* keeps the installer's message, which names that asset.
|
|
206
|
+
*/
|
|
207
|
+
export function installFailureMessage(error, asset) {
|
|
208
|
+
const atAsset = error instanceof InstallPlanError &&
|
|
209
|
+
error.asset.name === asset.name &&
|
|
210
|
+
error.asset.type === asset.type &&
|
|
211
|
+
error.asset.org === asset.org;
|
|
212
|
+
return errorMessage(atAsset ? error.cause : error);
|
|
213
|
+
}
|
|
214
|
+
/** Record `owner` as one of the assets that placed the file at `path`. */
|
|
215
|
+
function addManagedPath(paths, path, owner) {
|
|
216
|
+
const owners = paths.get(path);
|
|
217
|
+
if (owners) {
|
|
218
|
+
owners.push(owner);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
paths.set(path, [owner]);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
136
224
|
/**
|
|
137
225
|
* Append entry names to a string-array key in a config file (e.g., `enabledMcpjsonServers`
|
|
138
226
|
* in `.claude/settings.local.json`), creating the file/array as needed and de-duplicating.
|
|
@@ -167,37 +255,39 @@ async function addToEnabledList(enabledListFilePath, enabledListKey, names, onPr
|
|
|
167
255
|
}
|
|
168
256
|
catch (error) {
|
|
169
257
|
// Non-fatal — log but don't fail the install
|
|
170
|
-
onProgress?.(`Warning: Failed to update enabled list ${enabledListFilePath} — ${
|
|
258
|
+
onProgress?.(`Warning: Failed to update enabled list ${enabledListFilePath} — ${errorMessage(error)}`);
|
|
171
259
|
}
|
|
172
260
|
}
|
|
173
261
|
/**
|
|
174
|
-
*
|
|
175
|
-
* Used for conflict detection —
|
|
262
|
+
* Map every file path managed by ATK in the given lockfile to the assets that installed it.
|
|
263
|
+
* Used for conflict detection — an asset may overwrite only the files it installed itself.
|
|
176
264
|
* Resolves installed paths dynamically via the adapter rather than reading them from the lockfile.
|
|
177
265
|
*/
|
|
178
|
-
function
|
|
179
|
-
const paths = new
|
|
266
|
+
function buildManagedPaths(lockfile, projectDir, adapter) {
|
|
267
|
+
const paths = new Map();
|
|
180
268
|
for (const asset of lockfile.assets ?? []) {
|
|
181
269
|
const resolved = resolveInstalledPaths(asset, adapter, projectDir);
|
|
182
270
|
for (const file of resolved.files) {
|
|
183
|
-
paths
|
|
271
|
+
addManagedPath(paths, join(projectDir, file.installedPath), asset);
|
|
184
272
|
}
|
|
185
273
|
}
|
|
186
274
|
return paths;
|
|
187
275
|
}
|
|
188
276
|
/**
|
|
189
|
-
* Check if a file at the destination path would conflict with
|
|
190
|
-
* Throws
|
|
191
|
-
*
|
|
277
|
+
* Check if a file at the destination path would conflict with a file `asset` didn't install.
|
|
278
|
+
* Throws FileConflictError if the file exists and is not tracked in the lockfile, or is tracked
|
|
279
|
+
* only for other assets (a global and an org asset of one name and type share a placement).
|
|
280
|
+
* The asset's own files can be safely overwritten (e.g., reinstall/update).
|
|
192
281
|
*/
|
|
193
|
-
async function checkFileConflict(destPath, managedPaths) {
|
|
282
|
+
async function checkFileConflict(destPath, managedPaths, asset) {
|
|
194
283
|
if (!existsSync(destPath)) {
|
|
195
284
|
return;
|
|
196
285
|
}
|
|
197
|
-
|
|
286
|
+
const owners = managedPaths.get(destPath) ?? [];
|
|
287
|
+
if (owners.some((o) => o.name === asset.name && o.type === asset.type && o.org === asset.org)) {
|
|
198
288
|
return;
|
|
199
289
|
}
|
|
200
|
-
throw new
|
|
290
|
+
throw new FileConflictError(destPath, owners[0]);
|
|
201
291
|
}
|
|
202
292
|
/**
|
|
203
293
|
* Fetch all files for an asset version from the ATK API, plus its
|
|
@@ -225,7 +315,7 @@ async function fetchAssetFiles(asset, version, org) {
|
|
|
225
315
|
if (error instanceof InstallerError) {
|
|
226
316
|
throw error;
|
|
227
317
|
}
|
|
228
|
-
const message =
|
|
318
|
+
const message = errorMessage(error);
|
|
229
319
|
throw new InstallerError(message, errorGuidance(error));
|
|
230
320
|
}
|
|
231
321
|
}
|
|
@@ -259,8 +349,14 @@ async function installAsset(entry, options, managedPaths, lockfile) {
|
|
|
259
349
|
}
|
|
260
350
|
// Determine base path
|
|
261
351
|
const basePath = projectDir;
|
|
262
|
-
// Fetch asset files from the ATK API (version-aware, org-aware)
|
|
263
|
-
|
|
352
|
+
// Fetch asset files from the ATK API (version-aware, org-aware), unless the caller already did.
|
|
353
|
+
// A caller that downloaded first may have removed the installed files since, so a gap in its
|
|
354
|
+
// download must not turn into a fresh request that can be refused.
|
|
355
|
+
const prefetched = options.files?.get(planEntryKey(entry));
|
|
356
|
+
if (options.files && !prefetched) {
|
|
357
|
+
throw new InstallerError(`The files of "${asset.name}"@${entry.version} were not downloaded before the install.`);
|
|
358
|
+
}
|
|
359
|
+
const { entrypoint: declaredEntrypoint, files: assetFiles } = prefetched ?? (await fetchAssetFiles(asset, entry.version, entry.asset.org));
|
|
264
360
|
// Find the entrypoint file
|
|
265
361
|
const entrypointFile = assetFiles.find((f) => isEntrypoint(f.relativePath, asset.type, declaredEntrypoint));
|
|
266
362
|
// Branch on placement type
|
|
@@ -298,12 +394,15 @@ async function installAsset(entry, options, managedPaths, lockfile) {
|
|
|
298
394
|
}
|
|
299
395
|
else if (placement.entrypointName) {
|
|
300
396
|
// Directory-based placement (e.g., skills)
|
|
301
|
-
|
|
302
|
-
|
|
397
|
+
const destinations = assetFiles.map((file) => resolveFilePath(placement.path, asset.name, basePath, file.relativePath, asset.type, placement.entrypointName, declaredEntrypoint));
|
|
398
|
+
// Check every destination before writing any, so a conflict leaves none of the asset's files behind.
|
|
399
|
+
if (!force) {
|
|
400
|
+
for (const destPath of destinations)
|
|
401
|
+
await checkFileConflict(destPath, managedPaths, asset);
|
|
402
|
+
}
|
|
403
|
+
for (const [index, file] of assetFiles.entries()) {
|
|
404
|
+
const destPath = destinations[index];
|
|
303
405
|
const destDir = dirname(destPath);
|
|
304
|
-
if (!force) {
|
|
305
|
-
await checkFileConflict(destPath, managedPaths);
|
|
306
|
-
}
|
|
307
406
|
if (!existsSync(destDir)) {
|
|
308
407
|
await mkdir(destDir, { recursive: true });
|
|
309
408
|
}
|
|
@@ -322,12 +421,14 @@ async function installAsset(entry, options, managedPaths, lockfile) {
|
|
|
322
421
|
}
|
|
323
422
|
else {
|
|
324
423
|
// Default single-file placement
|
|
325
|
-
|
|
326
|
-
|
|
424
|
+
const destinations = assetFiles.map((file) => resolveFilePath(placement.path, asset.name, basePath, file.relativePath, asset.type, undefined, declaredEntrypoint));
|
|
425
|
+
if (!force) {
|
|
426
|
+
for (const destPath of destinations)
|
|
427
|
+
await checkFileConflict(destPath, managedPaths, asset);
|
|
428
|
+
}
|
|
429
|
+
for (const [index, file] of assetFiles.entries()) {
|
|
430
|
+
const destPath = destinations[index];
|
|
327
431
|
const destDir = dirname(destPath);
|
|
328
|
-
if (!force) {
|
|
329
|
-
await checkFileConflict(destPath, managedPaths);
|
|
330
|
-
}
|
|
331
432
|
if (!existsSync(destDir)) {
|
|
332
433
|
await mkdir(destDir, { recursive: true });
|
|
333
434
|
}
|
|
@@ -502,7 +603,7 @@ async function mergeFragmentConfig(configFilePath, configKey, format, projectDir
|
|
|
502
603
|
}
|
|
503
604
|
catch (error) {
|
|
504
605
|
// Non-fatal — log but don't fail the install
|
|
505
|
-
onProgress?.(`Warning: Failed to merge config fragment into ${configFilePath} — ${
|
|
606
|
+
onProgress?.(`Warning: Failed to merge config fragment into ${configFilePath} — ${errorMessage(error)}`);
|
|
506
607
|
return empty;
|
|
507
608
|
}
|
|
508
609
|
}
|
|
@@ -557,10 +658,14 @@ async function mergeHookConfig(configFilePath, configKey, format, hookConfig, co
|
|
|
557
658
|
}
|
|
558
659
|
catch (error) {
|
|
559
660
|
// Non-fatal — log but don't fail the install
|
|
560
|
-
onProgress?.(`Warning: Failed to merge hook config into ${configFilePath} — ${
|
|
661
|
+
onProgress?.(`Warning: Failed to merge hook config into ${configFilePath} — ${errorMessage(error)}`);
|
|
561
662
|
return [];
|
|
562
663
|
}
|
|
563
664
|
}
|
|
665
|
+
/** The {@link PlanFiles} key of a plan entry: one asset version. */
|
|
666
|
+
function planEntryKey(entry) {
|
|
667
|
+
return `${entry.asset.type}:${entry.asset.org ?? ''}:${entry.asset.name}@${entry.version}`;
|
|
668
|
+
}
|
|
564
669
|
/**
|
|
565
670
|
* Resolve the destination file path for an asset file.
|
|
566
671
|
* For directory-based placement (entrypointName set), the path is a directory template.
|
|
@@ -622,7 +727,7 @@ async function revertConfigEntry(configFilePath, keyPath, command, format, onPro
|
|
|
622
727
|
await writeConfigFile(configFilePath, config, format, onProgress);
|
|
623
728
|
}
|
|
624
729
|
catch (error) {
|
|
625
|
-
onProgress?.(`Warning: Failed to revert config entry ${keyPath} in ${configFilePath} — ${
|
|
730
|
+
onProgress?.(`Warning: Failed to revert config entry ${keyPath} in ${configFilePath} — ${errorMessage(error)}`);
|
|
626
731
|
}
|
|
627
732
|
}
|
|
628
733
|
/**
|
|
@@ -680,7 +785,7 @@ async function updateConfigFile(configFilePath, configKey, format, assetName, as
|
|
|
680
785
|
}
|
|
681
786
|
catch (error) {
|
|
682
787
|
// Non-fatal — log but don't fail the install
|
|
683
|
-
onProgress?.(`Warning: Failed to update config file ${configFilePath} — ${
|
|
788
|
+
onProgress?.(`Warning: Failed to update config file ${configFilePath} — ${errorMessage(error)}`);
|
|
684
789
|
return null;
|
|
685
790
|
}
|
|
686
791
|
}
|