@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.
Files changed (131) hide show
  1. package/CHANGELOG.md +195 -0
  2. package/build/commands/browse.js +5 -9
  3. package/build/commands/browse.js.map +1 -1
  4. package/build/commands/install.js +2 -2
  5. package/build/commands/install.js.map +1 -1
  6. package/build/commands/list.js +2 -2
  7. package/build/commands/list.js.map +1 -1
  8. package/build/commands/login.js +2 -2
  9. package/build/commands/login.js.map +1 -1
  10. package/build/commands/register.js +3 -3
  11. package/build/commands/register.js.map +1 -1
  12. package/build/commands/sync.d.ts.map +1 -1
  13. package/build/commands/sync.js +16 -11
  14. package/build/commands/sync.js.map +1 -1
  15. package/build/commands/uninstall.d.ts +9 -1
  16. package/build/commands/uninstall.d.ts.map +1 -1
  17. package/build/commands/uninstall.js +23 -26
  18. package/build/commands/uninstall.js.map +1 -1
  19. package/build/commands/update.d.ts +9 -1
  20. package/build/commands/update.d.ts.map +1 -1
  21. package/build/commands/update.js +54 -14
  22. package/build/commands/update.js.map +1 -1
  23. package/build/components/InstallConfirmBar.d.ts.map +1 -1
  24. package/build/components/InstallConfirmBar.js +2 -1
  25. package/build/components/InstallConfirmBar.js.map +1 -1
  26. package/build/components/ListActionBar.d.ts.map +1 -1
  27. package/build/components/ListActionBar.js +3 -2
  28. package/build/components/ListActionBar.js.map +1 -1
  29. package/build/components/RunStoppedHint.d.ts +13 -0
  30. package/build/components/RunStoppedHint.d.ts.map +1 -0
  31. package/build/components/RunStoppedHint.js +12 -0
  32. package/build/components/RunStoppedHint.js.map +1 -0
  33. package/build/components/SkippedAssetSection.d.ts +2 -0
  34. package/build/components/SkippedAssetSection.d.ts.map +1 -1
  35. package/build/components/SkippedAssetSection.js +1 -1
  36. package/build/components/SkippedAssetSection.js.map +1 -1
  37. package/build/components/SyncDriftedList.d.ts +3 -2
  38. package/build/components/SyncDriftedList.d.ts.map +1 -1
  39. package/build/components/SyncDriftedList.js +11 -3
  40. package/build/components/SyncDriftedList.js.map +1 -1
  41. package/build/components/index.d.ts +1 -0
  42. package/build/components/index.d.ts.map +1 -1
  43. package/build/components/index.js +1 -0
  44. package/build/components/index.js.map +1 -1
  45. package/build/hooks/useCommand.js +2 -2
  46. package/build/hooks/useCommand.js.map +1 -1
  47. package/build/hooks/useInitState.js +5 -5
  48. package/build/hooks/useInitState.js.map +1 -1
  49. package/build/hooks/useListActions.d.ts.map +1 -1
  50. package/build/hooks/useListActions.js +34 -18
  51. package/build/hooks/useListActions.js.map +1 -1
  52. package/build/hooks/usePruneState.js +7 -7
  53. package/build/hooks/usePruneState.js.map +1 -1
  54. package/build/hooks/usePublishState.js +3 -3
  55. package/build/hooks/usePublishState.js.map +1 -1
  56. package/build/hooks/useRegistryChangeState.js +2 -2
  57. package/build/hooks/useRegistryChangeState.js.map +1 -1
  58. package/build/hooks/useSetupState.js +3 -3
  59. package/build/hooks/useSetupState.js.map +1 -1
  60. package/build/hooks/useUninstallState.d.ts +16 -29
  61. package/build/hooks/useUninstallState.d.ts.map +1 -1
  62. package/build/hooks/useUninstallState.js +147 -140
  63. package/build/hooks/useUninstallState.js.map +1 -1
  64. package/build/lib/adapter.d.ts.map +1 -1
  65. package/build/lib/adapter.js +3 -3
  66. package/build/lib/adapter.js.map +1 -1
  67. package/build/lib/asset-tools.d.ts +5 -0
  68. package/build/lib/asset-tools.d.ts.map +1 -1
  69. package/build/lib/asset-tools.js +7 -0
  70. package/build/lib/asset-tools.js.map +1 -1
  71. package/build/lib/auth.d.ts.map +1 -1
  72. package/build/lib/auth.js +3 -3
  73. package/build/lib/auth.js.map +1 -1
  74. package/build/lib/checksum.d.ts.map +1 -1
  75. package/build/lib/checksum.js +2 -1
  76. package/build/lib/checksum.js.map +1 -1
  77. package/build/lib/diagnostics.d.ts.map +1 -1
  78. package/build/lib/diagnostics.js +6 -5
  79. package/build/lib/diagnostics.js.map +1 -1
  80. package/build/lib/diff-command.d.ts.map +1 -1
  81. package/build/lib/diff-command.js +10 -6
  82. package/build/lib/diff-command.js.map +1 -1
  83. package/build/lib/entra.d.ts.map +1 -1
  84. package/build/lib/entra.js +3 -2
  85. package/build/lib/entra.js.map +1 -1
  86. package/build/lib/errors.d.ts +9 -0
  87. package/build/lib/errors.d.ts.map +1 -1
  88. package/build/lib/errors.js +29 -0
  89. package/build/lib/errors.js.map +1 -1
  90. package/build/lib/file-snapshot.d.ts +17 -0
  91. package/build/lib/file-snapshot.d.ts.map +1 -0
  92. package/build/lib/file-snapshot.js +41 -0
  93. package/build/lib/file-snapshot.js.map +1 -0
  94. package/build/lib/installer.d.ts +61 -0
  95. package/build/lib/installer.d.ts.map +1 -1
  96. package/build/lib/installer.js +148 -43
  97. package/build/lib/installer.js.map +1 -1
  98. package/build/lib/lockfile.d.ts +23 -4
  99. package/build/lib/lockfile.d.ts.map +1 -1
  100. package/build/lib/lockfile.js +32 -14
  101. package/build/lib/lockfile.js.map +1 -1
  102. package/build/lib/org.d.ts +16 -0
  103. package/build/lib/org.d.ts.map +1 -1
  104. package/build/lib/org.js +18 -0
  105. package/build/lib/org.js.map +1 -1
  106. package/build/lib/owned-paths.d.ts +9 -0
  107. package/build/lib/owned-paths.d.ts.map +1 -0
  108. package/build/lib/owned-paths.js +23 -0
  109. package/build/lib/owned-paths.js.map +1 -0
  110. package/build/lib/publisher.d.ts.map +1 -1
  111. package/build/lib/publisher.js +2 -1
  112. package/build/lib/publisher.js.map +1 -1
  113. package/build/lib/registry.d.ts.map +1 -1
  114. package/build/lib/registry.js +10 -6
  115. package/build/lib/registry.js.map +1 -1
  116. package/build/lib/setup.d.ts.map +1 -1
  117. package/build/lib/setup.js +4 -3
  118. package/build/lib/setup.js.map +1 -1
  119. package/build/lib/sync.d.ts +17 -4
  120. package/build/lib/sync.d.ts.map +1 -1
  121. package/build/lib/sync.js +110 -41
  122. package/build/lib/sync.js.map +1 -1
  123. package/build/lib/uninstaller.d.ts +21 -4
  124. package/build/lib/uninstaller.d.ts.map +1 -1
  125. package/build/lib/uninstaller.js +45 -36
  126. package/build/lib/uninstaller.js.map +1 -1
  127. package/build/lib/updater.d.ts +64 -3
  128. package/build/lib/updater.d.ts.map +1 -1
  129. package/build/lib/updater.js +355 -83
  130. package/build/lib/updater.js.map +1 -1
  131. package/package.json +1 -1
@@ -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"}
@@ -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
@@ -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"}
@@ -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;AAiBpD,OAAO,EAAE,YAAY,EAAiB,MAAM,aAAa,CAAC;AAS1D,MAAM,WAAW,cAAc;IAC7B,mCAAmC;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,kEAAkE;IAClE,YAAY,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/D,yEAAyE;IACzE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qCAAqC;IACrC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,8CAA8C;IAC9C,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,8BAA8B;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/D,oBAAoB,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/F,WAAW,EAAE,KAAK,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5D,aAAa,EAAE,YAAY,GAAG,QAAQ,CAAC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAQD,qBAAa,cAAe,SAAQ,YAAY;IACrC,IAAI,SAAoB;CAClC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,WAAW,EACjB,WAAW,EAAE,KAAK,CAAC;IAAE,OAAO,EAAE,aAAa,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GAC7D,IAAI,CAUN;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAiH7G"}
1
+ {"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../../src/lib/installer.ts"],"names":[],"mappings":"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"}
@@ -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
- // Build set of ATK-managed file paths from the lockfile for conflict detection
56
- const managedPaths = buildManagedPathsSet(lockfile, options.projectDir, options.adapter);
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} — ${error instanceof Error ? error.message : String(error)}`);
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 — ${error instanceof Error ? error.message : String(error)}`);
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} — ${error instanceof Error ? error.message : String(error)}`);
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 wrapped in InstallerError with rollback context,
129
- // keeping any API error code and recovery hint/command (e.g. from an API auth failure).
130
- const originalMessage = error instanceof Error ? error.message : String(error);
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} — ${error instanceof Error ? error.message : String(error)}`);
258
+ onProgress?.(`Warning: Failed to update enabled list ${enabledListFilePath} — ${errorMessage(error)}`);
171
259
  }
172
260
  }
173
261
  /**
174
- * Build a set of all file paths managed by ATK from the given lockfile.
175
- * Used for conflict detection — files in this set can be safely overwritten.
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 buildManagedPathsSet(lockfile, projectDir, adapter) {
179
- const paths = new Set();
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.add(join(projectDir, file.installedPath));
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 an existing non-ATK file.
190
- * Throws InstallerError if the file exists and is not tracked in the lockfile.
191
- * ATK-managed files (in the lockfile) can be safely overwritten (e.g., reinstall/update).
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
- if (managedPaths.has(destPath)) {
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 InstallerError(`File conflict: "${destPath}" already exists and was not installed by ATK. Use --force to overwrite.`);
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 = error instanceof Error ? error.message : String(error);
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
- const { entrypoint: declaredEntrypoint, files: assetFiles } = await fetchAssetFiles(asset, entry.version, entry.asset.org);
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
- for (const file of assetFiles) {
302
- const destPath = resolveFilePath(placement.path, asset.name, basePath, file.relativePath, asset.type, placement.entrypointName, declaredEntrypoint);
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
- for (const file of assetFiles) {
326
- const destPath = resolveFilePath(placement.path, asset.name, basePath, file.relativePath, asset.type, undefined, declaredEntrypoint);
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} — ${error instanceof Error ? error.message : String(error)}`);
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} — ${error instanceof Error ? error.message : String(error)}`);
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} — ${error instanceof Error ? error.message : String(error)}`);
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} — ${error instanceof Error ? error.message : String(error)}`);
788
+ onProgress?.(`Warning: Failed to update config file ${configFilePath} — ${errorMessage(error)}`);
684
789
  return null;
685
790
  }
686
791
  }