@astryxdesign/cli 0.6.3-canary.db4e378 → 0.6.3-canary.ddb63c3

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 (236) hide show
  1. package/README.md +20 -1
  2. package/api/blog/blog.doc.mjs +1 -0
  3. package/api/build/build.doc.mjs +1 -0
  4. package/api/component/component.doc.mjs +1 -0
  5. package/api/discover/discover.doc.mjs +1 -0
  6. package/api/docs/_adapter.d.mts +3 -8
  7. package/api/docs/_adapter.mjs +13 -105
  8. package/api/docs/docs.doc.mjs +1 -0
  9. package/api/docs/list/list.mjs +3 -7
  10. package/api/doctor/doctor.d.mts +25 -3
  11. package/api/doctor/doctor.doc.mjs +1 -0
  12. package/api/doctor/doctor.mjs +185 -13
  13. package/api/doctor/doctor.test.mjs +326 -1
  14. package/api/gap-report/gap-report.doc.mjs +1 -0
  15. package/api/hook/_adapter.mjs +19 -5
  16. package/api/hook/hook.doc.mjs +1 -0
  17. package/api/hook/list/list.d.mts +1 -1
  18. package/api/hook/list/list.mjs +69 -17
  19. package/api/init/init.doc.mjs +1 -0
  20. package/api/integration/add-contribution.mjs +7 -7
  21. package/api/integration/add-contribution.test.mjs +37 -3
  22. package/api/integration/add-theme.mjs +34 -64
  23. package/api/integration/add-theme.test.mjs +105 -21
  24. package/api/integration/authoring-checks.test.mjs +8 -4
  25. package/api/integration/integration-block-exports.test.mjs +10 -6
  26. package/api/integration/integrationAdd.doc.mjs +1 -0
  27. package/api/integration/integrationAddAgentDoc.doc.mjs +1 -0
  28. package/api/integration/integrationAddCodemod.doc.mjs +1 -0
  29. package/api/integration/integrationAddComponent.doc.mjs +1 -0
  30. package/api/integration/integrationAddDoc.doc.mjs +1 -0
  31. package/api/integration/integrationAddTemplate.doc.mjs +1 -0
  32. package/api/integration/integrationAddTheme.doc.mjs +6 -5
  33. package/api/integration/integrationComponentConflicts.doc.mjs +1 -0
  34. package/api/integration/integrationDocConflicts.doc.mjs +1 -0
  35. package/api/integration/integrationPackCheck.doc.mjs +1 -0
  36. package/api/integration/integrationTemplateConflicts.doc.mjs +1 -0
  37. package/api/integration/pack-check.test.mjs +17 -47
  38. package/api/integration/summarizeIssues.doc.mjs +1 -0
  39. package/api/integration/validate-integration-fixes.test.mjs +1389 -0
  40. package/api/integration/validate-integration.mjs +50 -102
  41. package/api/integration/validate-integration.test.mjs +85 -23
  42. package/api/integration/validate-unread-theme-folders.test.mjs +110 -0
  43. package/api/integration/validateIntegration.doc.mjs +1 -0
  44. package/api/json/assertResponse.doc.mjs +1 -0
  45. package/api/json/isError.doc.mjs +1 -0
  46. package/api/json/parseResponse.doc.mjs +1 -0
  47. package/api/layout/layoutCheck.doc.mjs +1 -0
  48. package/api/layout/layoutExpand.doc.mjs +1 -0
  49. package/api/layout/layoutGrammar.doc.mjs +1 -0
  50. package/api/search/search.doc.mjs +1 -0
  51. package/api/search/search.mjs +16 -6
  52. package/api/swizzle/swizzle.doc.mjs +1 -0
  53. package/api/template/template-suffix.test.mjs +41 -21
  54. package/api/template/template.doc.mjs +1 -0
  55. package/api/theme/_adapter.d.mts +2 -3
  56. package/api/theme/_adapter.mjs +4 -5
  57. package/api/theme/add/add.binary.test.mjs +10 -17
  58. package/api/theme/add/add.test.mjs +14 -1
  59. package/api/theme/generateTonalPalette.doc.mjs +1 -0
  60. package/api/theme/integration-themes.test.mjs +39 -28
  61. package/api/theme/list/list.test.mjs +19 -20
  62. package/api/theme/listThemes.doc.mjs +6 -5
  63. package/api/theme/themeAdd.doc.mjs +4 -3
  64. package/api/theme/themeBuild.doc.mjs +1 -0
  65. package/api/theme/themeList.doc.mjs +6 -3
  66. package/api/theme/themeListAvailable.doc.mjs +5 -3
  67. package/api/theme/themePaletteGenerate.doc.mjs +1 -0
  68. package/api/theme/themeTargets.doc.mjs +1 -0
  69. package/api/theme/themeTemplate.doc.mjs +1 -0
  70. package/api/upgrade/_adapter.d.mts +12 -3
  71. package/api/upgrade/_adapter.mjs +81 -66
  72. package/api/upgrade/provider-agreement.test.mjs +152 -0
  73. package/api/upgrade/run/run.mjs +1 -0
  74. package/api/upgrade/upgrade.doc.mjs +1 -0
  75. package/assets/codemods/integration-discovery.mjs +8 -2
  76. package/assets/codemods/integration-discovery.test.mjs +15 -0
  77. package/assets/codemods/runner.mjs +115 -5
  78. package/assets/codemods/transforms/next/__tests__/migrate-theme-catalog-to-descriptors.test.mjs +216 -0
  79. package/assets/codemods/transforms/next/index.mjs +11 -1
  80. package/assets/codemods/transforms/next/migrate-theme-catalog-to-descriptors.mjs +141 -0
  81. package/assets/docs/cli-integrations.doc.mjs +17 -30
  82. package/assets/docs/cli.doc.mjs +15 -0
  83. package/assets/docs/theme.doc.mjs +1 -1
  84. package/assets/templates/themes/butter/butterTheme.doc.mjs +11 -0
  85. package/assets/templates/themes/chocolate/chocolateTheme.doc.mjs +11 -0
  86. package/assets/templates/themes/gothic/gothicTheme.doc.mjs +11 -0
  87. package/assets/templates/themes/matcha/matchaTheme.doc.mjs +11 -0
  88. package/assets/templates/themes/neutral/neutralTheme.doc.mjs +11 -0
  89. package/assets/templates/themes/stone/stoneTheme.doc.mjs +11 -0
  90. package/assets/templates/themes/y2k/y2kTheme.doc.mjs +11 -0
  91. package/authoring/codemod/codemod.doc.mjs +1 -1
  92. package/authoring/config/config.doc.mjs +1 -1
  93. package/authoring/debug/debug.doc.d.mts +11 -0
  94. package/authoring/debug/debug.doc.mjs +182 -0
  95. package/authoring/doctypes/base/graph-fields.doc.mjs +1 -1
  96. package/authoring/doctypes/command/command.doc.mjs +1 -1
  97. package/authoring/doctypes/command/type.ts +1 -1
  98. package/authoring/doctypes/component/type.ts +2 -2
  99. package/authoring/doctypes/doctypes-new.test.mjs +48 -6
  100. package/authoring/doctypes/enum/enum.doc.mjs +1 -1
  101. package/authoring/doctypes/enum/type.ts +1 -1
  102. package/authoring/doctypes/function/function.doc.mjs +1 -1
  103. package/authoring/doctypes/function/type.ts +1 -1
  104. package/authoring/doctypes/hook/type.ts +2 -2
  105. package/authoring/doctypes/load-contract.test.mjs +2 -1
  106. package/authoring/doctypes/parse.d.mts +4 -2
  107. package/authoring/doctypes/parse.mjs +7 -3
  108. package/authoring/doctypes/reference/reference.doc.mjs +1 -1
  109. package/authoring/doctypes/reference/type.ts +2 -2
  110. package/authoring/doctypes/schema/schema.doc.mjs +1 -1
  111. package/authoring/doctypes/schema/type.ts +1 -2
  112. package/authoring/doctypes/template/template.doc.mjs +3 -3
  113. package/authoring/doctypes/theme/parse.d.mts +35 -0
  114. package/authoring/doctypes/theme/parse.mjs +76 -0
  115. package/authoring/doctypes/theme/theme.doc.d.mts +9 -0
  116. package/authoring/doctypes/theme/theme.doc.mjs +79 -0
  117. package/authoring/doctypes/theme/type.ts +42 -0
  118. package/authoring/doctypes/types.ts +2 -1
  119. package/authoring/gap-report/gap-report.doc.d.mts +12 -0
  120. package/authoring/gap-report/gap-report.doc.mjs +183 -0
  121. package/authoring/index.d.mts +1 -0
  122. package/authoring/index.d.ts +6 -4
  123. package/authoring/index.mjs +2 -1
  124. package/authoring/integration/integration.doc.mjs +2 -2
  125. package/authoring/integration/type.ts +3 -9
  126. package/clients/cli/__tests__/cliManifest.test.ts +27 -29
  127. package/clients/cli/commands/blog.doc.mjs +1 -1
  128. package/clients/cli/commands/build.doc.mjs +1 -1
  129. package/clients/cli/commands/component.doc.mjs +1 -1
  130. package/clients/cli/commands/discover.doc.mjs +1 -1
  131. package/clients/cli/commands/docs.doc.mjs +1 -1
  132. package/clients/cli/commands/doctor-integration-components.doc.mjs +1 -1
  133. package/clients/cli/commands/doctor-integration-docs.doc.mjs +1 -1
  134. package/clients/cli/commands/doctor-integration-templates.doc.mjs +1 -1
  135. package/clients/cli/commands/doctor-integration-validate.doc.mjs +1 -1
  136. package/clients/cli/commands/doctor-integration.doc.mjs +1 -1
  137. package/clients/cli/commands/doctor-integration.test.mjs +15 -7
  138. package/clients/cli/commands/doctor.doc.mjs +1 -1
  139. package/clients/cli/commands/gap-report.doc.mjs +1 -1
  140. package/clients/cli/commands/hook.doc.mjs +1 -1
  141. package/clients/cli/commands/init.doc.mjs +1 -1
  142. package/clients/cli/commands/integration-add.controls.test.mjs +1 -1
  143. package/clients/cli/commands/integration-add.doc.mjs +1 -1
  144. package/clients/cli/commands/integration-pack.doc.mjs +1 -1
  145. package/clients/cli/commands/integration-real-world.test.mjs +3 -9
  146. package/clients/cli/commands/integration.doc.mjs +1 -1
  147. package/clients/cli/commands/layout-check.doc.mjs +1 -1
  148. package/clients/cli/commands/layout-expand.doc.mjs +1 -1
  149. package/clients/cli/commands/layout-grammar.doc.mjs +1 -1
  150. package/clients/cli/commands/layout.doc.mjs +1 -1
  151. package/clients/cli/commands/manifest.doc.mjs +1 -1
  152. package/clients/cli/commands/search.doc.mjs +1 -1
  153. package/clients/cli/commands/swizzle.doc.mjs +1 -1
  154. package/clients/cli/commands/template.doc.mjs +1 -1
  155. package/clients/cli/commands/theme-add.doc.mjs +2 -2
  156. package/clients/cli/commands/theme-build.doc.mjs +1 -1
  157. package/clients/cli/commands/theme-list.doc.mjs +2 -2
  158. package/clients/cli/commands/theme-palette-generate.doc.mjs +3 -3
  159. package/clients/cli/commands/theme-palette.doc.mjs +1 -1
  160. package/clients/cli/commands/theme-targets.doc.mjs +1 -1
  161. package/clients/cli/commands/theme-template.doc.mjs +1 -1
  162. package/clients/cli/commands/theme.doc.mjs +1 -1
  163. package/clients/cli/commands/upgrade.doc.mjs +1 -1
  164. package/clients/cli/lib/hook-format.mjs +14 -5
  165. package/foundation/config/project-themes.test.mjs +11 -19
  166. package/foundation/config/project.d.mts +8 -0
  167. package/foundation/config/project.mjs +50 -21
  168. package/foundation/config/project.test.mjs +3 -16
  169. package/foundation/discovery/authoring-self-docs.d.mts +6 -0
  170. package/foundation/discovery/authoring-self-docs.mjs +17 -7
  171. package/foundation/discovery/authoring-surface.d.mts +74 -0
  172. package/foundation/discovery/authoring-surface.mjs +525 -0
  173. package/foundation/discovery/authoring-surface.test.mjs +392 -0
  174. package/foundation/discovery/cli-self-docs.d.mts +113 -0
  175. package/foundation/discovery/cli-self-docs.mjs +514 -0
  176. package/foundation/discovery/cli-self-docs.test.mjs +437 -0
  177. package/foundation/discovery/component-loader.d.mts +35 -38
  178. package/foundation/discovery/component-loader.mjs +53 -222
  179. package/foundation/discovery/docs-discovery.d.mts +3 -2
  180. package/foundation/discovery/docs-discovery.mjs +8 -12
  181. package/foundation/discovery/docs-discovery.test.mjs +8 -5
  182. package/foundation/discovery/template-adapter.d.mts +7 -0
  183. package/foundation/discovery/template-adapter.mjs +27 -40
  184. package/foundation/discovery/template-adapter.test.mjs +15 -0
  185. package/foundation/discovery/theme-discovery.d.mts +67 -7
  186. package/foundation/discovery/theme-discovery.mjs +916 -186
  187. package/foundation/discovery/theme-discovery.test.mjs +613 -219
  188. package/foundation/doc-compiler/bundle.d.mts +47 -0
  189. package/foundation/doc-compiler/bundle.mjs +218 -0
  190. package/foundation/doc-compiler/bundle.test.mjs +255 -0
  191. package/foundation/doc-compiler/compile.d.mts +200 -0
  192. package/foundation/doc-compiler/compile.mjs +254 -5
  193. package/foundation/doc-compiler/diagnostics.d.mts +126 -0
  194. package/foundation/doc-compiler/diagnostics.mjs +277 -0
  195. package/foundation/doc-compiler/doc-compiler.test.mjs +28 -1
  196. package/foundation/doc-compiler/doc-loads.test.mjs +1642 -0
  197. package/foundation/doc-compiler/import.d.mts +24 -0
  198. package/foundation/doc-compiler/import.mjs +59 -0
  199. package/foundation/doc-compiler/inputs.d.mts +102 -0
  200. package/foundation/doc-compiler/inputs.mjs +286 -0
  201. package/foundation/doc-compiler/inputs.test.mjs +299 -0
  202. package/foundation/doc-compiler/ir.d.mts +13 -0
  203. package/foundation/doc-compiler/ir.mjs +189 -12
  204. package/foundation/doc-compiler/lower-doc.test.mjs +492 -0
  205. package/foundation/doc-compiler/overlays.d.mts +37 -0
  206. package/foundation/doc-compiler/overlays.mjs +206 -0
  207. package/foundation/doc-compiler/parse-readable.d.mts +9 -0
  208. package/foundation/doc-compiler/parse-readable.mjs +29 -0
  209. package/foundation/doc-compiler/read.d.mts +126 -0
  210. package/foundation/doc-compiler/read.mjs +320 -0
  211. package/foundation/doc-compiler/read.test.mjs +313 -0
  212. package/foundation/doc-compiler/source.d.mts +33 -0
  213. package/foundation/doc-compiler/source.mjs +128 -0
  214. package/foundation/integrations/autolink.d.mts +58 -1
  215. package/foundation/integrations/autolink.mjs +143 -57
  216. package/foundation/integrations/contribution-fixes.d.mts +145 -0
  217. package/foundation/integrations/contribution-fixes.mjs +1284 -0
  218. package/foundation/integrations/contribution-inventory.d.mts +1 -1
  219. package/foundation/integrations/contribution-inventory.mjs +17 -20
  220. package/foundation/integrations/contribution-inventory.test.mjs +67 -27
  221. package/foundation/integrations/integrations.d.mts +3 -0
  222. package/foundation/integrations/integrations.mjs +11 -97
  223. package/foundation/integrations/provider-ledger.test.mjs +275 -0
  224. package/foundation/integrations/provider-resolution.d.mts +152 -0
  225. package/foundation/integrations/provider-resolution.mjs +576 -0
  226. package/foundation/integrations/provider-resolution.test.mjs +369 -0
  227. package/foundation/integrations/theme-descriptor.d.mts +8 -0
  228. package/foundation/integrations/theme-descriptor.mjs +44 -0
  229. package/foundation/integrations/validate-contributions.mjs +104 -10
  230. package/foundation/response/error-codes.doc.mjs +2 -2
  231. package/foundation/response/error-codes.mjs +1 -1
  232. package/foundation/response/response-types.doc.mjs +1 -1
  233. package/foundation/response/response.doc.mjs +1 -1
  234. package/foundation/text/string-utils.mjs +22 -10
  235. package/package.json +9 -9
  236. package/assets/templates/themes/manifest.json +0 -95
@@ -104,13 +104,14 @@ export function ensureCodemodDeps({ installDeps }?: {
104
104
  * Run the CORE registry codemods. Runs BEFORE the config is loaded so a core
105
105
  * CONFIG codemod can repair a config the strict loader would otherwise reject.
106
106
  * @param {CoreVersionManifest[]} versionManifests
107
- * @param {{apply: boolean, path: string, codemod?: string, skipCodemods: Set<string>}} options
107
+ * @param {{apply: boolean, path: string, codemod?: string, skipCodemods: Set<string>, root?: string}} options
108
108
  */
109
- export function runCoreCodemods(versionManifests: CoreVersionManifest[], { apply, path: srcPath, codemod, skipCodemods }: {
109
+ export function runCoreCodemods(versionManifests: CoreVersionManifest[], { apply, path: srcPath, codemod, skipCodemods, root }: {
110
110
  apply: boolean;
111
111
  path: string;
112
112
  codemod?: string;
113
113
  skipCodemods: Set<string>;
114
+ root?: string;
114
115
  }): Promise<{
115
116
  totalFilesChanged: number;
116
117
  totalTransformsApplied: number;
@@ -139,13 +140,21 @@ export function runCoreCodemods(versionManifests: CoreVersionManifest[], { apply
139
140
  /**
140
141
  * Load the consumer project's config + integrations. Throws on invalid config;
141
142
  * the run leaf decides between the config_fixable preview and a hard abort.
143
+ *
144
+ * Provider identity is resolved by the same resolver Project uses, on top of
145
+ * Project's ledger, under the codemod policy: a package that lists itself runs
146
+ * its installed copy's released codemods, never the work in progress being
147
+ * authored, and autolinked and local packages claim provider IDs without
148
+ * running codemods.
149
+ *
142
150
  * @param {string} cwd
143
151
  * @param {string[]} [extraIntegrationSpecs] explicit `--integration` specs
144
- * @returns {Promise<{postCodemodHooks: import('../../authoring/config/type').PostCodemodHook[], integrations: import('../../foundation/integrations/integrations.mjs').LoadedIntegration[]}>}
152
+ * @returns {Promise<{postCodemodHooks: import('../../authoring/config/type').PostCodemodHook[], integrations: import('../../foundation/integrations/integrations.mjs').LoadedIntegration[], ledger: import('../../foundation/integrations/provider-resolution.mjs').ProviderLedger}>}
145
153
  */
146
154
  export function loadProjectContext(cwd: string, extraIntegrationSpecs?: string[]): Promise<{
147
155
  postCodemodHooks: import("../../authoring/config/type").PostCodemodHook[];
148
156
  integrations: import("../../foundation/integrations/integrations.mjs").LoadedIntegration[];
157
+ ledger: import("../../foundation/integrations/provider-resolution.mjs").ProviderLedger;
149
158
  }>;
150
159
  /**
151
160
  * Non-blocking nudge for integration validation issues. Never throws (a broken
@@ -19,7 +19,10 @@ import * as path from 'node:path';
19
19
  import {execFile} from 'node:child_process';
20
20
  import {promisify} from 'node:util';
21
21
  import {ensureJscodeshift} from '../../assets/codemods/ensure-jscodeshift.mjs';
22
- import {getTransformsBetween, latestVersion} from '../../assets/codemods/registry.mjs';
22
+ import {
23
+ getTransformsBetween,
24
+ latestVersion,
25
+ } from '../../assets/codemods/registry.mjs';
23
26
  import {runCodemods} from '../../assets/codemods/runner.mjs';
24
27
  import {
25
28
  discoverIntegrationCodemods,
@@ -32,17 +35,15 @@ import {
32
35
  renderAgentDocsBlock,
33
36
  } from '../../foundation/agent-docs/agent-docs.mjs';
34
37
  import {formatCliCommand} from '../../foundation/env/package-manager.mjs';
35
- import {Project} from '../../foundation/config/project.mjs';
36
- import {
37
- loadIntegrations,
38
- markProviderConflicts,
39
- } from '../../foundation/integrations/integrations.mjs';
38
+ import {Project, providerLedgerOf} from '../../foundation/config/project.mjs';
39
+ import {loadIntegrations} from '../../foundation/integrations/integrations.mjs';
40
+ import {resolveProviders} from '../../foundation/integrations/provider-resolution.mjs';
40
41
  import {warnOnIntegrationIssues} from '../../foundation/integrations/integration-warnings.mjs';
41
42
  import {logger} from '../logger.mjs';
42
43
 
43
44
  // Re-exported for the run leaf's lightweight agent-docs inspection path
44
45
  // (config_fixable short-circuit, where the full render cannot load config).
45
- export { inspectAgentDocs };
46
+ export {inspectAgentDocs};
46
47
 
47
48
  const execFileAsync = promisify(execFile);
48
49
 
@@ -291,7 +292,13 @@ export async function collectAllCodemods() {
291
292
  );
292
293
  for (const {version, transforms} of manifests) {
293
294
  for (const {name, meta, optional} of transforms) {
294
- codemods.push({name, title: meta.title, version, pr: meta.pr, optional: !!optional});
295
+ codemods.push({
296
+ name,
297
+ title: meta.title,
298
+ version,
299
+ pr: meta.pr,
300
+ optional: !!optional,
301
+ });
295
302
  }
296
303
  }
297
304
  return codemods;
@@ -322,33 +329,35 @@ export async function ensureCodemodDeps({installDeps} = {}) {
322
329
  * Run the CORE registry codemods. Runs BEFORE the config is loaded so a core
323
330
  * CONFIG codemod can repair a config the strict loader would otherwise reject.
324
331
  * @param {CoreVersionManifest[]} versionManifests
325
- * @param {{apply: boolean, path: string, codemod?: string, skipCodemods: Set<string>}} options
332
+ * @param {{apply: boolean, path: string, codemod?: string, skipCodemods: Set<string>, root?: string}} options
326
333
  */
327
- export async function runCoreCodemods(versionManifests, {apply, path: srcPath, codemod, skipCodemods}) {
334
+ export async function runCoreCodemods(
335
+ versionManifests,
336
+ {apply, path: srcPath, codemod, skipCodemods, root},
337
+ ) {
328
338
  return runCodemods(versionManifests, {
329
339
  apply,
330
340
  path: srcPath,
331
341
  codemod,
332
342
  skipCodemods,
333
343
  silent: logger.silent,
344
+ root,
334
345
  });
335
346
  }
336
347
 
337
- /**
338
- * One installed package at one version; two specs that reach it share this.
339
- * @param {{name: string, version?: string}} integration
340
- * @returns {string}
341
- */
342
- function packageIdentity(integration) {
343
- return `${integration.name}\u0000${integration.version ?? ''}`;
344
- }
345
-
346
348
  /**
347
349
  * Load the consumer project's config + integrations. Throws on invalid config;
348
350
  * the run leaf decides between the config_fixable preview and a hard abort.
351
+ *
352
+ * Provider identity is resolved by the same resolver Project uses, on top of
353
+ * Project's ledger, under the codemod policy: a package that lists itself runs
354
+ * its installed copy's released codemods, never the work in progress being
355
+ * authored, and autolinked and local packages claim provider IDs without
356
+ * running codemods.
357
+ *
349
358
  * @param {string} cwd
350
359
  * @param {string[]} [extraIntegrationSpecs] explicit `--integration` specs
351
- * @returns {Promise<{postCodemodHooks: import('../../authoring/config/type').PostCodemodHook[], integrations: import('../../foundation/integrations/integrations.mjs').LoadedIntegration[]}>}
360
+ * @returns {Promise<{postCodemodHooks: import('../../authoring/config/type').PostCodemodHook[], integrations: import('../../foundation/integrations/integrations.mjs').LoadedIntegration[], ledger: import('../../foundation/integrations/provider-resolution.mjs').ProviderLedger}>}
352
361
  */
353
362
  export async function loadProjectContext(cwd, extraIntegrationSpecs = []) {
354
363
  // The CLI debug preflight may already have imported this config before core
@@ -360,23 +369,38 @@ export async function loadProjectContext(cwd, extraIntegrationSpecs = []) {
360
369
  // Configured packages come from Project, which has already resolved provider
361
370
  // identity over configured, autolinked, and local packages. Loading them
362
371
  // again here would let upgrade run codemods from a package Project set aside.
363
- const configured = project.loadedIntegrations.filter(
364
- integration =>
365
- !integration.__autolinked && configuredSpecs.has(integration.__spec),
366
- );
372
+ // Autolinked packages and the package being authored join only as claimants:
373
+ // every other command uses them for their provider IDs, but upgrade has
374
+ // never run their codemods.
375
+ const candidates = project.loadedIntegrations.flatMap(integration => {
376
+ const source =
377
+ !integration.__autolinked && configuredSpecs.has(integration.__spec)
378
+ ? /** @type {const} */ ('configured')
379
+ : integration.__autolinked
380
+ ? /** @type {const} */ ('autolinked')
381
+ : integration.__local
382
+ ? /** @type {const} */ ('local')
383
+ : null;
384
+ return source == null
385
+ ? []
386
+ : [{source, integration, spec: integration.__spec}];
387
+ });
367
388
  // A package that lists itself runs its installed copy's released codemods,
368
- // never the work in progress being authored, so that copy stands in for it.
389
+ // never the work in progress being authored; the resolver puts that copy in
390
+ // its place. Naming the package being authored with --integration asks for
391
+ // the installed copy too.
369
392
  const local = project.loadedIntegrations.find(
370
393
  integration => integration.__local,
371
394
  );
372
- const selfListed = local != null && configured.includes(local);
373
- if (local != null && selfListed && project.configPath) {
374
- const installed = await loadIntegrations([local.__spec], {
375
- cwd: path.dirname(project.configPath),
376
- resolveProviders: false,
377
- });
378
- configured.splice(configured.indexOf(local), 1, ...installed);
379
- }
395
+ const selfListed =
396
+ local != null && !local.__autolinked && configuredSpecs.has(local.__spec);
397
+ const installed =
398
+ local != null && selfListed && project.configPath
399
+ ? await loadIntegrations([local.__spec], {
400
+ cwd: path.dirname(project.configPath),
401
+ resolveProviders: false,
402
+ })
403
+ : [];
380
404
  const extraSpecs = uniqueFiles(extraIntegrationSpecs ?? []).filter(
381
405
  spec => !configuredSpecs.has(spec),
382
406
  );
@@ -384,38 +408,23 @@ export async function loadProjectContext(cwd, extraIntegrationSpecs = []) {
384
408
  extraSpecs.length === 0
385
409
  ? []
386
410
  : await loadIntegrations(extraSpecs, {resolveProviders: false});
387
- // Naming the package being authored with --integration asks for its
388
- // installed copy too, so the local package must not claim against it.
389
- const localStandsIn =
390
- local != null &&
391
- (selfListed || extras.some(extra => extra.name === local.name));
392
- // Autolinked packages and the package being authored join the pass only as
393
- // claimants: every other command uses them for their provider IDs, but
394
- // upgrade has never run their codemods and still does not. An extra that
395
- // names one of them opts it in, so the extra stands in for that claimant.
396
- const extraIdentities = new Set(extras.map(packageIdentity));
397
- const claimantsOnly = project.loadedIntegrations.filter(
398
- integration =>
399
- (integration.__autolinked || (integration.__local && !localStandsIn)) &&
400
- !extraIdentities.has(packageIdentity(integration)),
401
- );
402
- const resolved = markProviderConflicts([
403
- ...configured,
404
- ...claimantsOnly,
405
- ...extras,
406
- ]);
407
- // Claimants leave by package directory, not object identity: the pass can
408
- // return one as a new conflict entry, and upgrade must not warn that a
409
- // package every other command uses contributes nothing.
410
- const claimantDirs = new Set(
411
- claimantsOnly.map(integration => integration.__packageDir),
412
- );
413
- const integrations = resolved.filter(
414
- integration =>
415
- integration.__packageDir == null ||
416
- !claimantDirs.has(integration.__packageDir),
411
+ const {integrations, ledger} = resolveProviders(
412
+ [
413
+ ...candidates,
414
+ ...installed.map(integration => ({
415
+ source: /** @type {const} */ ('installed'),
416
+ integration,
417
+ spec: integration.__spec,
418
+ })),
419
+ ...extras.map(integration => ({
420
+ source: /** @type {const} */ ('extra'),
421
+ integration,
422
+ spec: integration.__spec,
423
+ })),
424
+ ],
425
+ {codemods: true, prior: providerLedgerOf(project)},
417
426
  );
418
- return {postCodemodHooks, integrations};
427
+ return {postCodemodHooks, integrations, ledger};
419
428
  }
420
429
 
421
430
  /**
@@ -450,7 +459,10 @@ export async function selectIntegrationCodemodsFor(integrations, from, to) {
450
459
  try {
451
460
  const byVersion = await discoverIntegrationCodemods([integration]);
452
461
  for (const [version, rawList] of byVersion) {
453
- const list = /** @type {Array<import('../../authoring/codemod/type').CodemodEntry>} */ (/** @type {unknown} */ (rawList));
462
+ const list =
463
+ /** @type {Array<import('../../authoring/codemod/type').CodemodEntry>} */ (
464
+ /** @type {unknown} */ (rawList)
465
+ );
454
466
  const existing = integrationCodemodsByVersion.get(version);
455
467
  if (existing) existing.push(...list);
456
468
  else integrationCodemodsByVersion.set(version, [...list]);
@@ -469,7 +481,10 @@ export async function selectIntegrationCodemodsFor(integrations, from, to) {
469
481
  * @param {Array<{version: string, codemods: import('../../authoring/codemod/type').CodemodEntry[]}>} versionGroups
470
482
  * @param {{apply: boolean, path: string, codemod?: string, skipCodemods: Set<string>}} options
471
483
  */
472
- export async function runIntegrationCodemodsStep(versionGroups, {apply, path: srcPath, codemod, skipCodemods}) {
484
+ export async function runIntegrationCodemodsStep(
485
+ versionGroups,
486
+ {apply, path: srcPath, codemod, skipCodemods},
487
+ ) {
473
488
  const jscodeshift = (await import('jscodeshift')).default;
474
489
  return runIntegrationCodemods(versionGroups, {
475
490
  apply,
@@ -0,0 +1,152 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Where `upgrade` and Project still disagree on a provider ID's winner.
5
+ *
6
+ * Both repros are `it.fails`: each asserts the agreeing result, and fails
7
+ * today. `upgrade` lets an `--integration` extra of the same name@version stand
8
+ * in for an autolinked package, from any directory, and orders that extra with
9
+ * the extras. So another extra can take the provider ID in `upgrade` while
10
+ * every other command gives it to the autolinked package. The provider ledger
11
+ * property test carves out exactly this shape.
12
+ *
13
+ * Fixtures live under a repo-local temp dir, not /tmp, because Vite refuses to
14
+ * dynamically import a module from outside the project root.
15
+ */
16
+
17
+ import {afterEach, beforeEach, describe, expect, it} from 'vitest';
18
+ import * as fs from 'node:fs';
19
+ import * as path from 'node:path';
20
+ import {loadProjectContext} from './_adapter.mjs';
21
+ import {Project, providerLedgerOf} from '../../foundation/config/project.mjs';
22
+
23
+ let tmpDir;
24
+ let originalCwd;
25
+
26
+ beforeEach(() => {
27
+ originalCwd = process.cwd();
28
+ tmpDir = fs.mkdtempSync(
29
+ path.join(process.cwd(), '.astryx-upgrade-agreement-'),
30
+ );
31
+ process.chdir(tmpDir);
32
+ });
33
+
34
+ afterEach(() => {
35
+ process.chdir(originalCwd);
36
+ fs.rmSync(tmpDir, {recursive: true, force: true});
37
+ });
38
+
39
+ /**
40
+ * Install a package under node_modules/<dir>.
41
+ * @param {string} dir
42
+ * @param {{name: string, version: string}} pkg
43
+ * @param {string} manifest manifest module source
44
+ */
45
+ function install(dir, pkg, manifest) {
46
+ const pkgDir = path.join(tmpDir, 'node_modules', ...dir.split('/'));
47
+ fs.mkdirSync(pkgDir, {recursive: true});
48
+ fs.writeFileSync(path.join(pkgDir, 'package.json'), JSON.stringify(pkg));
49
+ fs.writeFileSync(path.join(pkgDir, 'astryx.integration.mjs'), manifest);
50
+ }
51
+
52
+ /** An autolinked `@acme/a` and an `@acme/other` that claims its provider ID. */
53
+ function autolinkedWithRival() {
54
+ fs.writeFileSync(
55
+ path.join(tmpDir, 'package.json'),
56
+ JSON.stringify({name: 'consumer', dependencies: {'@acme/a': '1.0.0'}}),
57
+ );
58
+ install(
59
+ '@acme/a',
60
+ {name: '@acme/a', version: '1.0.0'},
61
+ 'export default {};\n',
62
+ );
63
+ install(
64
+ '@acme/other',
65
+ {name: '@acme/other', version: '1.0.0'},
66
+ "export default {providerId: '@acme/a'};\n",
67
+ );
68
+ }
69
+
70
+ /**
71
+ * The package each resolution lets contribute under a provider ID.
72
+ * @param {Map<string, any>} ledger
73
+ * @param {string} providerId
74
+ */
75
+ function winnerOf(ledger, providerId) {
76
+ return [...ledger.values()].find(
77
+ entry => entry.outcome === 'contributes' && entry.providerId === providerId,
78
+ )?.key;
79
+ }
80
+
81
+ /** @param {Array<import('../../foundation/integrations/integrations.mjs').LoadedIntegration>} integrations */
82
+ function claims(integrations) {
83
+ return integrations.map(integration => [
84
+ integration.name,
85
+ integration.__providerConflict?.claimedBy ?? null,
86
+ ]);
87
+ }
88
+
89
+ describe('upgrade and Project on an autolinked package named with --integration', () => {
90
+ it('agree when the autolinked package is named first', async () => {
91
+ autolinkedWithRival();
92
+ const project = await Project.load(tmpDir, {fresh: true});
93
+ const upgrade = await loadProjectContext(tmpDir, [
94
+ '@acme/a',
95
+ '@acme/other',
96
+ ]);
97
+
98
+ expect(claims(upgrade.integrations)).toEqual([
99
+ ['@acme/a', null],
100
+ ['@acme/other', '@acme/a'],
101
+ ]);
102
+ expect(winnerOf(upgrade.ledger, '@acme/a')).toBe(
103
+ winnerOf(providerLedgerOf(project), '@acme/a'),
104
+ );
105
+ });
106
+
107
+ it.fails(
108
+ 'agree when an extra that claims its ID is named first',
109
+ async () => {
110
+ autolinkedWithRival();
111
+ const project = await Project.load(tmpDir, {fresh: true});
112
+ // Today upgrade runs @acme/other and reports @acme/a, which every other
113
+ // command uses, as contributing nothing.
114
+ const upgrade = await loadProjectContext(tmpDir, [
115
+ '@acme/other',
116
+ '@acme/a',
117
+ ]);
118
+
119
+ expect(winnerOf(upgrade.ledger, '@acme/a')).toBe(
120
+ winnerOf(providerLedgerOf(project), '@acme/a'),
121
+ );
122
+ expect(claims(upgrade.integrations)).toEqual([
123
+ ['@acme/other', '@acme/a'],
124
+ ['@acme/a', null],
125
+ ]);
126
+ },
127
+ );
128
+
129
+ it.fails(
130
+ 'agree when a copy of it in another directory is named',
131
+ async () => {
132
+ autolinkedWithRival();
133
+ // The same name@version in another directory, with a manifest that fails.
134
+ install(
135
+ '@acme/a-copy',
136
+ {name: '@acme/a', version: '1.0.0'},
137
+ "throw new Error('broken copy');\n",
138
+ );
139
+ const project = await Project.load(tmpDir, {fresh: true});
140
+ // Today the broken copy stands in for @acme/a by name@version, so upgrade
141
+ // has no claimant left for the ID and @acme/other takes it.
142
+ const upgrade = await loadProjectContext(tmpDir, [
143
+ '@acme/a-copy',
144
+ '@acme/other',
145
+ ]);
146
+
147
+ expect(winnerOf(upgrade.ledger, '@acme/a')).toBe(
148
+ winnerOf(providerLedgerOf(project), '@acme/a'),
149
+ );
150
+ },
151
+ );
152
+ });
@@ -171,6 +171,7 @@ export async function run(options = {}, {cwd = process.cwd()} = {}) {
171
171
  path: path_,
172
172
  codemod: options.codemod,
173
173
  skipCodemods,
174
+ root: cwd,
174
175
  });
175
176
  const coreResult = codemodResult && 'totalFilesChanged' in codemodResult ? codemodResult : null;
176
177
 
@@ -11,6 +11,7 @@ export const doc = {
11
11
  type: 'function',
12
12
  kind: 'api',
13
13
  name: 'upgrade',
14
+ namespace: 'cli/api',
14
15
  displayName: 'upgrade()',
15
16
  summary: 'Run version migrations and reconcile copied compositions.',
16
17
  description:
@@ -106,7 +106,7 @@ function collectCodemodFiles(versionDir) {
106
106
  /**
107
107
  * Discover file-based codemods contributed by configured integrations.
108
108
  *
109
- * @param {Array<{name?: string, codemods?: string, __spec?: string}>} loadedIntegrations
109
+ * @param {Array<{name?: string, codemods?: string, __spec?: string, __packageDir?: string}>} loadedIntegrations
110
110
  * @returns {Promise<Map<string, Array<{id: string, type: 'code'|'config', codemod: object, package: string}>>>}
111
111
  * Map keyed by version string; each value is the list of discovered codemods
112
112
  * for that version (across all integrations).
@@ -145,7 +145,13 @@ export async function discoverIntegrationCodemods(loadedIntegrations = []) {
145
145
  const seenInVersion = new Set();
146
146
 
147
147
  for (const {id, file} of files) {
148
- const label = `Integration "${pkgLabel}" codemod ${version}/${id} (${file})`;
148
+ const shown = integration.__packageDir
149
+ ? path
150
+ .relative(integration.__packageDir, file)
151
+ .split(path.sep)
152
+ .join('/')
153
+ : file;
154
+ const label = `Integration "${pkgLabel}" codemod ${version}/${id} (${shown})`;
149
155
 
150
156
  if (seenInVersion.has(id)) {
151
157
  throw new Error(
@@ -163,6 +163,21 @@ describe('integration codemod discovery', () => {
163
163
  ).rejects.toThrow(/is invalid/i);
164
164
  });
165
165
 
166
+ it('names a broken codemod by its path inside the package', async () => {
167
+ scaffold({
168
+ '1.1.0/no-default.mjs': `export const nope = 1;\n`,
169
+ });
170
+ const project = await Project.load(tmpDir);
171
+ const failure = await discoverIntegrationCodemods(
172
+ project.loadedIntegrations,
173
+ ).then(
174
+ () => null,
175
+ error => error,
176
+ );
177
+ expect(failure?.message).toContain('(codemods/1.1.0/no-default.mjs)');
178
+ expect(failure?.message).not.toContain(tmpDir);
179
+ });
180
+
166
181
  it('fails discovery when default export is not a codemod envelope', async () => {
167
182
  scaffold({
168
183
  '0.2.0/bad.mjs': `export default { title: 'x', transform: () => null };\n`,
@@ -194,6 +194,89 @@ function toUnifiedEntry(transformEntry, version) {
194
194
  };
195
195
  }
196
196
 
197
+ /**
198
+ * What a core PROJECT codemod plans: whole files to write and delete under the
199
+ * project root, or the problems that stop it. Paths in `writes` and `deletes`
200
+ * are absolute; `problems` name package-relative files.
201
+ *
202
+ * @typedef {object} ProjectCodemodPlan
203
+ * @property {Array<{path: string, contents: string}>} writes
204
+ * @property {string[]} deletes
205
+ * @property {Array<{file: string, message: string}>} problems
206
+ */
207
+
208
+ /**
209
+ * Run one core PROJECT codemod (`meta.codemodType === 'project'`): instead of
210
+ * rewriting the files it is handed, it reads the project and plans whole-file
211
+ * writes and deletions, which this applies (or previews) as one unit. A plan
212
+ * that names a problem changes nothing. Core-only: integration codemods keep
213
+ * the file contract.
214
+ *
215
+ * @param {{name: string, transform: unknown}} transformEntry
216
+ * @param {{apply: boolean, root: string, log: {success: (m: string) => void, warn: (m: string) => void, error: (m: string) => void}}} options
217
+ * @returns {Promise<{filesChanged: number, writtenFiles: string[], errors: Array<{file: string, codemod: string, error: string}>}>}
218
+ */
219
+ async function runProjectCodemod({name, transform}, {apply, root, log}) {
220
+ /** @param {string} file */
221
+ const rel = file => path.relative(root, file).split(path.sep).join('/');
222
+ /** @type {ProjectCodemodPlan} */
223
+ let plan;
224
+ try {
225
+ plan = await /** @type {(root: string) => Promise<ProjectCodemodPlan>} */ (
226
+ transform
227
+ )(root);
228
+ } catch (err) {
229
+ const message = err instanceof Error ? err.message : String(err);
230
+ log.error(` ✗ ${message}`);
231
+ return {
232
+ filesChanged: 0,
233
+ writtenFiles: [],
234
+ errors: [{file: '.', codemod: name, error: message}],
235
+ };
236
+ }
237
+ if (plan.problems.length > 0) {
238
+ for (const {file, message} of plan.problems) {
239
+ log.error(` ✗ ${file} — ${message}`);
240
+ }
241
+ return {
242
+ filesChanged: 0,
243
+ writtenFiles: [],
244
+ errors: plan.problems.map(({file, message}) => ({
245
+ file,
246
+ codemod: name,
247
+ error: message,
248
+ })),
249
+ };
250
+ }
251
+ /** @type {string[]} */
252
+ const writtenFiles = [];
253
+ for (const write of plan.writes) {
254
+ if (apply) {
255
+ fs.mkdirSync(path.dirname(write.path), {recursive: true});
256
+ fs.writeFileSync(write.path, write.contents, 'utf-8');
257
+ writtenFiles.push(write.path);
258
+ log.success(` ✓ ${rel(write.path)}`);
259
+ } else {
260
+ log.warn(` ~ ${rel(write.path)} (would write)`);
261
+ }
262
+ }
263
+ // Deletions go last, so a failed write leaves a state a rerun completes.
264
+ for (const file of plan.deletes) {
265
+ if (apply) {
266
+ fs.rmSync(file);
267
+ writtenFiles.push(file);
268
+ log.success(` ✓ ${rel(file)} (removed)`);
269
+ } else {
270
+ log.warn(` ~ ${rel(file)} (would remove)`);
271
+ }
272
+ }
273
+ return {
274
+ filesChanged: plan.writes.length + plan.deletes.length,
275
+ writtenFiles,
276
+ errors: [],
277
+ };
278
+ }
279
+
197
280
  /**
198
281
  * Run codemods against source files.
199
282
  *
@@ -204,11 +287,19 @@ function toUnifiedEntry(transformEntry, version) {
204
287
  * @param {string|undefined} options.codemod - Run only this specific transform
205
288
  * @param {Set<string>} [options.skipCodemods] - Transform names to exclude
206
289
  * @param {boolean} [options.silent] - Suppress all human-facing output (for --json)
290
+ * @param {string} [options.root] - Project root a project codemod reads (default: the process cwd)
207
291
  * @returns {Promise<{totalFilesChanged: number, totalTransformsApplied: number, totalValidationBlocked: number, writtenFiles: string[], errors: Array<{file: string, codemod: string, error: string}>, skippedOptional: Array<{name: string, meta: {title: string, description?: string, fileExtensions?: string[], codemodType?: string}, version: string}>} | {ok: false, reason: string, resolvedPath: string}>}
208
292
  */
209
293
  export async function runCodemods(
210
294
  versionManifests,
211
- {apply, path: srcPath, codemod, skipCodemods, silent = false},
295
+ {
296
+ apply,
297
+ path: srcPath,
298
+ codemod,
299
+ skipCodemods,
300
+ silent = false,
301
+ root = process.cwd(),
302
+ },
212
303
  ) {
213
304
  // No-op stub object so silent mode skips log output entirely without
214
305
  // littering the body with `if (!silent)` guards.
@@ -221,11 +312,14 @@ export async function runCodemods(
221
312
 
222
313
  const resolvedPath = path.resolve(srcPath);
223
314
 
224
- // Config codemods target the consumer's astryx.config.* and never read
225
- // source files, so a missing --path should not block them. Only hard-fail
226
- // on a missing source path when there is at least one CODE codemod to run.
315
+ // Config and project codemods never read the files under --path, so a
316
+ // missing --path should not block them. Only hard-fail on a missing source
317
+ // path when there is at least one CODE codemod to run.
227
318
  const hasCodeCodemod = versionManifests.some(({transforms}) =>
228
- transforms.some(t => t.meta?.codemodType !== 'config'),
319
+ transforms.some(
320
+ t =>
321
+ t.meta?.codemodType !== 'config' && t.meta?.codemodType !== 'project',
322
+ ),
229
323
  );
230
324
  const sourcePathExists = fs.existsSync(resolvedPath);
231
325
 
@@ -290,6 +384,22 @@ export async function runCodemods(
290
384
  // `(file, api)` contract and targets the consumer's astryx.config.*.
291
385
  // A core entry signals "config" via `meta.codemodType === 'config'`
292
386
  // (see toUnifiedEntry).
387
+ if (meta?.codemodType === 'project') {
388
+ const result = await runProjectCodemod(transformEntry, {
389
+ apply,
390
+ root,
391
+ log,
392
+ });
393
+ if (result.errors.length > 0) {
394
+ errors.push(...result.errors);
395
+ } else if (result.filesChanged > 0) {
396
+ totalFilesChanged += result.filesChanged;
397
+ totalTransformsApplied += 1;
398
+ writtenFiles.push(...result.writtenFiles);
399
+ }
400
+ continue;
401
+ }
402
+
293
403
  if (meta?.codemodType === 'config') {
294
404
  const result = runConfigCodemod(toUnifiedEntry(transformEntry, version), {
295
405
  apply,