@intelligo-dev/cli 1.0.0-beta.13 → 1.0.0-beta.15

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 (188) hide show
  1. package/README.md +65 -2
  2. package/dist/args.d.ts +10 -0
  3. package/dist/args.d.ts.map +1 -0
  4. package/dist/args.js +20 -0
  5. package/dist/args.js.map +1 -0
  6. package/dist/bin.js +103 -22
  7. package/dist/bin.js.map +1 -1
  8. package/dist/commands/add.d.ts +4 -0
  9. package/dist/commands/add.d.ts.map +1 -1
  10. package/dist/commands/add.js +28 -2
  11. package/dist/commands/add.js.map +1 -1
  12. package/dist/commands/create-flow.d.ts +8 -1
  13. package/dist/commands/create-flow.d.ts.map +1 -1
  14. package/dist/commands/create-flow.js +101 -34
  15. package/dist/commands/create-flow.js.map +1 -1
  16. package/dist/commands/create.d.ts +24 -4
  17. package/dist/commands/create.d.ts.map +1 -1
  18. package/dist/commands/create.js +57 -14
  19. package/dist/commands/create.js.map +1 -1
  20. package/dist/commands/doctor.d.ts +2 -0
  21. package/dist/commands/doctor.d.ts.map +1 -1
  22. package/dist/commands/doctor.js +123 -35
  23. package/dist/commands/doctor.js.map +1 -1
  24. package/dist/commands/migrate-check.d.ts +20 -1
  25. package/dist/commands/migrate-check.d.ts.map +1 -1
  26. package/dist/commands/migrate-check.js +35 -6
  27. package/dist/commands/migrate-check.js.map +1 -1
  28. package/dist/commands/migrate.d.ts.map +1 -1
  29. package/dist/commands/migrate.js +2 -8
  30. package/dist/commands/migrate.js.map +1 -1
  31. package/dist/commands/sync-messages.d.ts +37 -0
  32. package/dist/commands/sync-messages.d.ts.map +1 -0
  33. package/dist/commands/sync-messages.js +88 -0
  34. package/dist/commands/sync-messages.js.map +1 -0
  35. package/dist/commands/sync-scaffold.d.ts +36 -0
  36. package/dist/commands/sync-scaffold.d.ts.map +1 -0
  37. package/dist/commands/sync-scaffold.js +53 -0
  38. package/dist/commands/sync-scaffold.js.map +1 -0
  39. package/dist/commands/sync.d.ts +109 -0
  40. package/dist/commands/sync.d.ts.map +1 -0
  41. package/dist/commands/sync.js +392 -0
  42. package/dist/commands/sync.js.map +1 -0
  43. package/dist/commands/upgrade-check.d.ts.map +1 -1
  44. package/dist/commands/upgrade-check.js +4 -1
  45. package/dist/commands/upgrade-check.js.map +1 -1
  46. package/dist/env-files.d.ts +11 -0
  47. package/dist/env-files.d.ts.map +1 -1
  48. package/dist/env-files.js +28 -9
  49. package/dist/env-files.js.map +1 -1
  50. package/dist/manifest.d.ts +27 -0
  51. package/dist/manifest.d.ts.map +1 -1
  52. package/dist/manifest.js +30 -3
  53. package/dist/manifest.js.map +1 -1
  54. package/dist/module-exports.d.ts +63 -0
  55. package/dist/module-exports.d.ts.map +1 -0
  56. package/dist/module-exports.js +231 -0
  57. package/dist/module-exports.js.map +1 -0
  58. package/dist/registry-bundle.d.ts +43 -0
  59. package/dist/registry-bundle.d.ts.map +1 -0
  60. package/dist/registry-bundle.js +74 -0
  61. package/dist/registry-bundle.js.map +1 -0
  62. package/dist/registry-items.d.ts +41 -13
  63. package/dist/registry-items.d.ts.map +1 -1
  64. package/dist/registry-items.js +117 -33
  65. package/dist/registry-items.js.map +1 -1
  66. package/package.json +1 -1
  67. package/src/args.ts +25 -0
  68. package/src/bin.ts +124 -22
  69. package/src/commands/add.ts +39 -12
  70. package/src/commands/create-flow.ts +119 -29
  71. package/src/commands/create.ts +82 -14
  72. package/src/commands/doctor.ts +156 -47
  73. package/src/commands/migrate-check.ts +51 -10
  74. package/src/commands/migrate.ts +2 -8
  75. package/src/commands/sync-messages.ts +114 -0
  76. package/src/commands/sync-scaffold.ts +83 -0
  77. package/src/commands/sync.ts +574 -0
  78. package/src/commands/upgrade-check.ts +4 -1
  79. package/src/env-files.ts +28 -7
  80. package/src/manifest.ts +57 -3
  81. package/src/module-exports.ts +266 -0
  82. package/src/registry-bundle.ts +103 -0
  83. package/src/registry-items.ts +141 -36
  84. package/templates/admin-page/admin-page.tsx.tpl +96 -24
  85. package/templates/app-scaffold/gitignore.tpl +25 -0
  86. package/templates/app-scaffold/intelligo.ts.tpl +6 -47
  87. package/templates/app-scaffold/next.config.mjs.tpl +31 -2
  88. package/templates/app-scaffold/package.json.tpl +2 -2
  89. package/templates/manifest.json +40 -3
  90. package/templates/pnpm-standalone/npmrc.tpl +6 -0
  91. package/templates/pnpm-standalone/pnpm-workspace.yaml.tpl +10 -0
  92. package/templates/registry/ai-agent-activity.json +23 -0
  93. package/templates/registry/ai-agent-progress.json +21 -0
  94. package/templates/registry/ai-approval-card.json +27 -0
  95. package/templates/registry/ai-artifact.json +22 -0
  96. package/templates/registry/ai-branch.json +21 -0
  97. package/templates/registry/ai-citations.json +24 -0
  98. package/templates/registry/ai-code-block.json +23 -0
  99. package/templates/registry/ai-composer-menu.json +19 -0
  100. package/templates/registry/ai-file-diff.json +25 -0
  101. package/templates/registry/ai-image-generation.json +23 -0
  102. package/templates/registry/ai-markdown.json +23 -0
  103. package/templates/registry/ai-message-bubble.json +23 -0
  104. package/templates/registry/ai-message-scroller.json +22 -0
  105. package/templates/registry/ai-message.json +21 -0
  106. package/templates/registry/ai-motion.json +19 -0
  107. package/templates/registry/ai-prompt-input.json +28 -0
  108. package/templates/registry/ai-reasoning-text.json +22 -0
  109. package/templates/registry/ai-reasoning.json +22 -0
  110. package/templates/registry/ai-shimmer-text.json +19 -0
  111. package/templates/registry/ai-sidebar.json +25 -0
  112. package/templates/registry/ai-speech-input.json +21 -0
  113. package/templates/registry/ai-streaming-response.json +24 -0
  114. package/templates/registry/ai-suggestion.json +19 -0
  115. package/templates/registry/ai-todo-list.json +22 -0
  116. package/templates/registry/ai-tool-approval.json +26 -0
  117. package/templates/registry/ai-tool-result.json +25 -0
  118. package/templates/registry/alert-dialog.json +23 -0
  119. package/templates/registry/animated-list.json +21 -0
  120. package/templates/registry/app-shell.json +93 -0
  121. package/templates/registry/artifacts.json +82 -0
  122. package/templates/registry/attachment.json +22 -0
  123. package/templates/registry/auth-email-verification.json +39 -0
  124. package/templates/registry/auth-login.json +72 -0
  125. package/templates/registry/auth-password-reset.json +53 -0
  126. package/templates/registry/auth-signup.json +41 -0
  127. package/templates/registry/billing-settings.json +66 -0
  128. package/templates/registry/button.json +22 -0
  129. package/templates/registry/chat-eve.json +19 -0
  130. package/templates/registry/chat-panel.json +30 -0
  131. package/templates/registry/chat-share.json +42 -0
  132. package/templates/registry/chat-widget.json +34 -0
  133. package/templates/registry/chat.json +326 -0
  134. package/templates/registry/checkbox.json +22 -0
  135. package/templates/registry/checkout.json +40 -0
  136. package/templates/registry/collapsible.json +19 -0
  137. package/templates/registry/command.json +23 -0
  138. package/templates/registry/copy-button.json +21 -0
  139. package/templates/registry/dashboard.json +69 -0
  140. package/templates/registry/dialog.json +24 -0
  141. package/templates/registry/document-viewer.json +22 -0
  142. package/templates/registry/dropdown-menu.json +23 -0
  143. package/templates/registry/expandable-tabs.json +22 -0
  144. package/templates/registry/feature-gating.json +73 -0
  145. package/templates/registry/file-upload.json +24 -0
  146. package/templates/registry/hold-action-button.json +22 -0
  147. package/templates/registry/input-group.json +23 -0
  148. package/templates/registry/input.json +19 -0
  149. package/templates/registry/intelligo.json +187 -0
  150. package/templates/registry/invitation-accept.json +64 -0
  151. package/templates/registry/language-switcher.json +29 -0
  152. package/templates/registry/morphing-modal.json +24 -0
  153. package/templates/registry/notification-stack.json +24 -0
  154. package/templates/registry/notifications.json +87 -0
  155. package/templates/registry/onboarding.json +83 -0
  156. package/templates/registry/otp-input.json +22 -0
  157. package/templates/registry/page-header.json +15 -0
  158. package/templates/registry/payment-poll.json +58 -0
  159. package/templates/registry/popover-morph.json +22 -0
  160. package/templates/registry/popover.json +22 -0
  161. package/templates/registry/pricing.json +117 -0
  162. package/templates/registry/privacy-settings.json +65 -0
  163. package/templates/registry/profile-settings.json +68 -0
  164. package/templates/registry/progress.json +19 -0
  165. package/templates/registry/radio-group.json +22 -0
  166. package/templates/registry/registry.json +2960 -0
  167. package/templates/registry/route-error.json +65 -0
  168. package/templates/registry/select-morph.json +23 -0
  169. package/templates/registry/select.json +23 -0
  170. package/templates/registry/settings-shell.json +47 -0
  171. package/templates/registry/sheet.json +24 -0
  172. package/templates/registry/sidebar.json +28 -0
  173. package/templates/registry/smoke.json +34 -0
  174. package/templates/registry/spinner.json +16 -0
  175. package/templates/registry/stat-card.json +18 -0
  176. package/templates/registry/status-badge.json +18 -0
  177. package/templates/registry/switch.json +20 -0
  178. package/templates/registry/tabs.json +23 -0
  179. package/templates/registry/team-settings.json +97 -0
  180. package/templates/registry/textarea.json +16 -0
  181. package/templates/registry/tooltip.json +22 -0
  182. package/templates/registry/trial-banner.json +43 -0
  183. package/templates/registry/usage.json +84 -0
  184. package/templates/registry/workspace-settings.json +71 -0
  185. package/templates/registry-items.json +3 -3
  186. package/templates/registry-requires.json +30 -4
  187. package/templates/vitest/server-only.ts.tpl +7 -0
  188. package/templates/vitest/vitest.config.ts.tpl +37 -0
@@ -37,7 +37,7 @@
37
37
  * `migrate` can take it over.
38
38
  *
39
39
  * Beside `state` it carries `exitCode`, `chain`, `applied`, `pending`,
40
- * `unknown`, `legacy` and `adoptable`.
40
+ * `unknown`, `legacy`, `legacyMissing` and `adoptable`.
41
41
  */
42
42
 
43
43
  import { createHash } from "node:crypto";
@@ -64,6 +64,11 @@ export type MigrateCheckResult = {
64
64
  legacy: string[];
65
65
  /** Length of the pre-1.0 chain (0 when this checkout ships none). */
66
66
  legacyChainLength: number;
67
+ /**
68
+ * Pre-1.0 migrations the database has not run, by tag, in chain
69
+ * order — empty unless it ran some of them.
70
+ */
71
+ legacyMissing: string[];
67
72
  /**
68
73
  * The database ran the whole pre-1.0 chain and not the baseline:
69
74
  * `migrate` records the baseline as applied without running it.
@@ -74,11 +79,11 @@ export type MigrateCheckResult = {
74
79
  /** The pre-1.0 chain's tags and hashes, if this checkout ships them. */
75
80
  export function readLegacyChain(
76
81
  migrationsDir: string
77
- ): Array<{ tag: string; hash: string }> {
82
+ ): Array<{ tag: string; hash: string; alternates?: string[] }> {
78
83
  const file = path.join(migrationsDir, "legacy-chain.json");
79
84
  if (!existsSync(file)) return [];
80
85
  const parsed = JSON.parse(readFileSync(file, "utf8")) as {
81
- entries?: Array<{ tag: string; hash: string }>;
86
+ entries?: Array<{ tag: string; hash: string; alternates?: string[] }>;
82
87
  };
83
88
  return parsed.entries ?? [];
84
89
  }
@@ -118,7 +123,13 @@ export async function migrateCheck(
118
123
  }
119
124
 
120
125
  const legacyChain = readLegacyChain(migrationsDir);
121
- const legacyByHash = new Map(legacyChain.map((e) => [e.hash, e.tag]));
126
+ // A migration can be recorded under more than one hash: npm's
127
+ // 1.0.0-beta.6 shipped different bytes for three of them.
128
+ const legacyByHash = new Map(
129
+ legacyChain.flatMap((e) =>
130
+ [e.hash, ...(e.alternates ?? [])].map((h) => [h, e.tag] as const)
131
+ )
132
+ );
122
133
 
123
134
  const appliedHashes = new Set(rows.map((r) => r.hash));
124
135
  const applied: string[] = [];
@@ -144,6 +155,10 @@ export async function migrateCheck(
144
155
  unmanaged: tableMissing || appliedHashes.size === 0,
145
156
  legacy,
146
157
  legacyChainLength: legacyChain.length,
158
+ legacyMissing:
159
+ legacy.length > 0
160
+ ? legacyChain.map((e) => e.tag).filter((t) => !legacy.includes(t))
161
+ : [],
147
162
  adoptable:
148
163
  legacyChain.length > 0 &&
149
164
  legacy.length === legacyChain.length &&
@@ -185,12 +200,8 @@ export function formatMigrateCheck(
185
200
  `already there), then applies what follows. Tables the old chain ` +
186
201
  `created that the framework no longer owns are left untouched.`
187
202
  );
188
- } else if (r.legacy.length > 0 && r.legacy.length < r.legacyChainLength) {
189
- lines.push(
190
- `✗ This database ran ${r.legacy.length} of the ${r.legacyChainLength} ` +
191
- `pre-1.0 migrations. Finish that chain with @intelligo-dev/core ` +
192
- `1.0.0-beta.7 (\`intelligo migrate\`) before upgrading.`
193
- );
203
+ } else if (isPartialLegacy(r)) {
204
+ lines.push(`✗ ${partialLegacyMessage(r)}`);
194
205
  }
195
206
 
196
207
  if (r.unknown.length > 0) {
@@ -221,6 +232,35 @@ export function isPartialLegacy(r: MigrateCheckResult): boolean {
221
232
  return r.legacy.length > 0 && r.legacy.length < r.legacyChainLength;
222
233
  }
223
234
 
235
+ /**
236
+ * The last pre-1.0 migration any published `@intelligo-dev/core`
237
+ * carries: 1.0.0-beta.6 shipped the chain through it, and the versions
238
+ * that carried the rest were never published.
239
+ */
240
+ export const LAST_PUBLISHED_LEGACY_TAG = "0042_sessions_active_organization_id";
241
+
242
+ /**
243
+ * Why a partial pre-1.0 chain is refused and where the way forward is
244
+ * written down. No published version finishes the chain, so the
245
+ * message names what is missing and points at the README's options
246
+ * instead of at a version to install.
247
+ */
248
+ export function partialLegacyMessage(r: MigrateCheckResult): string {
249
+ const shown = r.legacyMissing.slice(0, 6);
250
+ const more = r.legacyMissing.length - shown.length;
251
+ return (
252
+ `This database ran ${r.legacy.length} of the ${r.legacyChainLength} ` +
253
+ `pre-1.0 migrations; it has not run ${shown.join(", ")}` +
254
+ `${more > 0 ? ` and ${more} more` : ""}. No published ` +
255
+ `@intelligo-dev/core finishes that chain: 1.0.0-beta.6 ships it ` +
256
+ `through ${LAST_PUBLISHED_LEGACY_TAG}, and the versions that carried ` +
257
+ `the rest never reached npm. Apply the missing migrations' SQL from ` +
258
+ `your own copy of it, or bring the schema to the baseline and record ` +
259
+ `it as applied — see "Databases from before 1.0" in README.md in ` +
260
+ `@intelligo-dev/core's src/db/migrations.`
261
+ );
262
+ }
263
+
224
264
  export type MigrateState =
225
265
  "up_to_date" | "pending" | "fresh" | "ahead" | "unmanaged" | "legacy";
226
266
 
@@ -252,6 +292,7 @@ export function formatMigrateCheckJson(
252
292
  pending: r.pending,
253
293
  unknown: r.unknown,
254
294
  legacy: r.legacy,
295
+ legacyMissing: r.legacyMissing,
255
296
  adoptable: r.adoptable,
256
297
  });
257
298
  }
@@ -34,6 +34,7 @@ import {
34
34
  hashMigration,
35
35
  isPartialLegacy,
36
36
  migrateCheck,
37
+ partialLegacyMessage,
37
38
  type MigrateCheckResult,
38
39
  } from "./migrate-check.js";
39
40
 
@@ -68,14 +69,7 @@ export function decideApply(
68
69
  }
69
70
 
70
71
  if (isPartialLegacy(check)) {
71
- return {
72
- action: "refuse",
73
- reason:
74
- `This database ran ${check.legacy.length} of the ` +
75
- `${check.legacyChainLength} pre-1.0 migrations. Finish that chain ` +
76
- `with @intelligo-dev/core 1.0.0-beta.7 (\`intelligo migrate\`) ` +
77
- `before upgrading.`,
78
- };
72
+ return { action: "refuse", reason: partialLegacyMessage(check) };
79
73
  }
80
74
 
81
75
  if (check.adoptable) {
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Message files under `intelligo sync`: the registry's keys an app's
3
+ * copy lacks, the merge that keeps the app's values, and the other
4
+ * locales checked against the app's English copy.
5
+ */
6
+
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import path from "node:path";
9
+
10
+ export type Json = Record<string, unknown>;
11
+
12
+ const isObject = (v: unknown): v is Json =>
13
+ typeof v === "object" && v !== null && !Array.isArray(v);
14
+
15
+ /** Dotted paths of every leaf in `registry` that `app` lacks. */
16
+ export function missingMessageKeys(
17
+ registry: Json,
18
+ app: Json,
19
+ prefix = ""
20
+ ): string[] {
21
+ return Object.entries(registry).flatMap(([key, value]) => {
22
+ const at = prefix ? `${prefix}.${key}` : key;
23
+ if (!(key in app)) return [at];
24
+ const mine = app[key];
25
+ return isObject(value) && isObject(mine)
26
+ ? missingMessageKeys(value, mine, at)
27
+ : [];
28
+ });
29
+ }
30
+
31
+ /**
32
+ * The registry's messages with the app's copy laid over them: every key
33
+ * the registry has is present, every value the app set wins, and keys
34
+ * only the app has (its own nav entries, product copy) are kept.
35
+ */
36
+ export function mergeMessages(registry: Json, app: Json): Json {
37
+ const out: Json = {};
38
+ for (const [key, value] of Object.entries(registry)) {
39
+ const mine = app[key];
40
+ out[key] =
41
+ key in app
42
+ ? isObject(value) && isObject(mine)
43
+ ? mergeMessages(value, mine)
44
+ : mine
45
+ : value;
46
+ }
47
+ for (const [key, value] of Object.entries(app)) {
48
+ if (!(key in out)) out[key] = value;
49
+ }
50
+ return out;
51
+ }
52
+
53
+ export const isMessages = (target: string) =>
54
+ target.startsWith("messages/") && target.endsWith(".json");
55
+
56
+ /** The locale the registry ships messages in. */
57
+ const SOURCE_LOCALE = "en";
58
+
59
+ /**
60
+ * The app's locales, from `locales: [...]` in i18n/routing.ts — the
61
+ * source locale alone when there is no such file or list.
62
+ */
63
+ export function appLocales(appRoot: string): string[] {
64
+ const file = path.join(appRoot, "i18n", "routing.ts");
65
+ if (!existsSync(file)) return [SOURCE_LOCALE];
66
+ const list = /\blocales\s*:\s*\[([^\]]*)\]/.exec(readFileSync(file, "utf8"));
67
+ const locales = [...(list?.[1] ?? "").matchAll(/["'`]([^"'`]+)["'`]/g)].map(
68
+ (m) => m[1]!
69
+ );
70
+ return locales.length > 0 ? locales : [SOURCE_LOCALE];
71
+ }
72
+
73
+ export type LocaleEntry = {
74
+ item: string;
75
+ path: string;
76
+ state: "current" | "locale-behind";
77
+ missingKeys?: string[];
78
+ };
79
+
80
+ /**
81
+ * Every other locale's copy of the namespaces the registry ships,
82
+ * against the app's English file on disk — the merged copy, product
83
+ * keys included, which is what the other locales translate. A missing
84
+ * file lacks every key.
85
+ */
86
+ export function localeEntries(
87
+ appRoot: string,
88
+ files: readonly { item: string; target: string }[]
89
+ ): LocaleEntry[] {
90
+ const others = appLocales(appRoot).filter((l) => l !== SOURCE_LOCALE);
91
+ if (others.length === 0) return [];
92
+ const prefix = `messages/${SOURCE_LOCALE}/`;
93
+ const entries: LocaleEntry[] = [];
94
+ for (const { item, target } of files) {
95
+ if (!target.startsWith(prefix) || !target.endsWith(".json")) continue;
96
+ const english = path.join(appRoot, target);
97
+ if (!existsSync(english)) continue;
98
+ const source = JSON.parse(readFileSync(english, "utf8")) as Json;
99
+ for (const locale of others) {
100
+ const rel = `messages/${locale}/${target.slice(prefix.length)}`;
101
+ const abs = path.join(appRoot, rel);
102
+ const missingKeys = missingMessageKeys(
103
+ source,
104
+ existsSync(abs) ? (JSON.parse(readFileSync(abs, "utf8")) as Json) : {}
105
+ );
106
+ entries.push(
107
+ missingKeys.length > 0
108
+ ? { item, path: rel, state: "locale-behind", missingKeys }
109
+ : { item, path: rel, state: "current" }
110
+ );
111
+ }
112
+ }
113
+ return entries;
114
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Scaffold files a registry install replaces. `intelligo create`
3
+ * writes app/globals.css, the theme provider and lib/utils.ts; the
4
+ * design-system base and the items overwrite them, and the scaffold's
5
+ * record has to let go of them or `upgrade --check` reports them as
6
+ * the app's own edits forever.
7
+ */
8
+
9
+ import { existsSync, readFileSync } from "node:fs";
10
+ import path from "node:path";
11
+
12
+ import { readManifest } from "../manifest.js";
13
+
14
+ /** The `intelligo create` feature whose files an install may replace. */
15
+ export const SCAFFOLD_FEATURE = "app-scaffold";
16
+
17
+ /**
18
+ * Scaffold files the install extends in place rather than replaces:
19
+ * shadcn adds dependencies to package.json and style fields to
20
+ * components.json, and both stay the app's.
21
+ */
22
+ const EDITED_IN_PLACE: ReadonlySet<string> = new Set([
23
+ "package.json",
24
+ "components.json",
25
+ ]);
26
+
27
+ /**
28
+ * The scaffold files an install has made the registry's: a file an
29
+ * installed item ships, the Tailwind stylesheet once the design-system
30
+ * base is in (it rewrites the tokens), or any scaffold file the install
31
+ * changed (shadcn's own `utils`, `sonner`). Seams stay the app's, and
32
+ * so do the files shadcn only extends. Left in the scaffold's record,
33
+ * each would read `customized` in `upgrade --check` forever.
34
+ */
35
+ export function scaffoldHandover(input: {
36
+ scaffold: readonly string[];
37
+ changed: ReadonlySet<string>;
38
+ shipped: ReadonlySet<string>;
39
+ seams: Readonly<Record<string, string>>;
40
+ css: string | null;
41
+ }): string[] {
42
+ return input.scaffold.filter(
43
+ (file) =>
44
+ !(file in input.seams) &&
45
+ !EDITED_IN_PLACE.has(file) &&
46
+ (input.shipped.has(file) || file === input.css || input.changed.has(file))
47
+ );
48
+ }
49
+
50
+ function readIfExists(file: string): string | null {
51
+ return existsSync(file) ? readFileSync(file, "utf8") : null;
52
+ }
53
+
54
+ /**
55
+ * The scaffold's recorded files as they are before an install, and
56
+ * afterwards the ones the install made the registry's.
57
+ */
58
+ export function snapshotScaffold(appRoot: string): {
59
+ handedOver: (input: {
60
+ shipped: ReadonlySet<string>;
61
+ seams: Readonly<Record<string, string>>;
62
+ css: string | null;
63
+ }) => string[];
64
+ } {
65
+ const scaffold = (
66
+ readManifest(appRoot)?.features[SCAFFOLD_FEATURE]?.files ?? []
67
+ ).map((f) => f.path);
68
+ const before = new Map(
69
+ scaffold.map((f) => [f, readIfExists(path.join(appRoot, f))])
70
+ );
71
+ return {
72
+ handedOver: (input) =>
73
+ scaffoldHandover({
74
+ ...input,
75
+ scaffold,
76
+ changed: new Set(
77
+ scaffold.filter(
78
+ (f) => readIfExists(path.join(appRoot, f)) !== before.get(f)
79
+ )
80
+ ),
81
+ }),
82
+ };
83
+ }