create-m5kdev 0.30.0 → 0.31.0

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 (60) hide show
  1. package/dist/src/catalog.cjs +139 -0
  2. package/dist/src/catalog.cjs.map +1 -0
  3. package/dist/src/catalog.d.cts +30 -0
  4. package/dist/src/changes.cjs +70 -0
  5. package/dist/src/changes.cjs.map +1 -0
  6. package/dist/src/changes.d.cts +26 -0
  7. package/dist/src/create.cjs +37 -23
  8. package/dist/src/create.cjs.map +1 -1
  9. package/dist/src/diagnostics.cjs +33 -0
  10. package/dist/src/diagnostics.cjs.map +1 -0
  11. package/dist/src/diagnostics.d.cts +20 -0
  12. package/dist/src/doctor.cjs +245 -0
  13. package/dist/src/doctor.cjs.map +1 -0
  14. package/dist/src/doctor.d.cts +13 -0
  15. package/dist/src/fs.cjs +56 -26
  16. package/dist/src/fs.cjs.map +1 -1
  17. package/dist/src/fs.d.cts +11 -4
  18. package/dist/src/init.cjs +110 -0
  19. package/dist/src/init.cjs.map +1 -0
  20. package/dist/src/init.d.cts +36 -0
  21. package/dist/src/migrations/ast.cjs +49 -0
  22. package/dist/src/migrations/ast.cjs.map +1 -0
  23. package/dist/src/migrations/ast.d.cts +10 -0
  24. package/dist/src/migrations/registry.cjs +41 -0
  25. package/dist/src/migrations/registry.cjs.map +1 -0
  26. package/dist/src/migrations/registry.d.cts +12 -0
  27. package/dist/src/migrations/types.cjs +0 -0
  28. package/dist/src/migrations/types.d.cts +22 -0
  29. package/dist/src/prompts.cjs +1 -1
  30. package/dist/src/prompts.cjs.map +1 -1
  31. package/dist/src/reconcile.cjs +361 -0
  32. package/dist/src/reconcile.cjs.map +1 -0
  33. package/dist/src/reconcile.d.cts +26 -0
  34. package/dist/src/runCli.cjs +155 -27
  35. package/dist/src/runCli.cjs.map +1 -1
  36. package/dist/src/state.cjs +104 -0
  37. package/dist/src/state.cjs.map +1 -0
  38. package/dist/src/state.d.cts +50 -0
  39. package/dist/src/template.cjs +45 -0
  40. package/dist/src/template.cjs.map +1 -0
  41. package/dist/src/template.d.cts +10 -0
  42. package/dist/src/types.d.cts +20 -1
  43. package/dist/src/update.cjs +91 -0
  44. package/dist/src/update.cjs.map +1 -0
  45. package/dist/src/update.d.cts +33 -0
  46. package/package.json +12 -4
  47. package/templates/minimal-app/README.md.tpl +18 -3
  48. package/templates/minimal-app/apps/email/src/emails/accountDeletionEmail.tsx.tpl +36 -35
  49. package/templates/minimal-app/apps/email/src/emails/organizationInviteEmail.tsx.tpl +35 -34
  50. package/templates/minimal-app/apps/email/src/emails/passwordResetEmail.tsx.tpl +36 -35
  51. package/templates/minimal-app/apps/email/src/emails/verificationEmail.tsx.tpl +38 -37
  52. package/templates/minimal-app/apps/email/src/emails/waitlistEmails.tsx.tpl +88 -87
  53. package/templates/minimal-app/apps/email/src/index.ts.tpl +74 -72
  54. package/templates/minimal-app/apps/email/tsconfig.json.tpl +11 -16
  55. package/templates/minimal-app/apps/server/tsconfig.json.tpl +13 -20
  56. package/templates/minimal-app/apps/shared/tsconfig.json.tpl +12 -16
  57. package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostsList.ts.tpl +98 -98
  58. package/templates/minimal-app/biome.json.tpl +9 -2
  59. package/templates/minimal-app/pnpm-workspace.yaml.tpl +61 -63
  60. package/templates/minimal-app/template.manifest.json +30 -0
@@ -0,0 +1,245 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
3
+ const require_src_catalog = require("./catalog.cjs");
4
+ const require_src_changes = require("./changes.cjs");
5
+ require("./constants.cjs");
6
+ const require_src_paths = require("./paths.cjs");
7
+ const require_src_template = require("./template.cjs");
8
+ const require_src_state = require("./state.cjs");
9
+ const require_src_diagnostics = require("./diagnostics.cjs");
10
+ const require_src_migrations_registry = require("./migrations/registry.cjs");
11
+ let node_path = require("node:path");
12
+ node_path = require_runtime.__toESM(node_path);
13
+ let node_fs_promises = require("node:fs/promises");
14
+ node_fs_promises = require_runtime.__toESM(node_fs_promises);
15
+ let node_child_process = require("node:child_process");
16
+ let node_util = require("node:util");
17
+ let semver = require("semver");
18
+ semver = require_runtime.__toESM(semver);
19
+ //#region src/doctor.ts
20
+ const execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
21
+ const FULL_COMMAND_TIMEOUT_MS = 600 * 1e3;
22
+ async function exists(filePath) {
23
+ return node_fs_promises.default.access(filePath).then(() => true).catch(() => false);
24
+ }
25
+ async function diagnoseManagedRepo(options) {
26
+ const diagnostics = [];
27
+ let state = options.state;
28
+ if (!state) try {
29
+ state = await require_src_state.readManagedState(options.repoRoot);
30
+ } catch (error) {
31
+ diagnostics.push({
32
+ code: "STATE_INVALID",
33
+ severity: "error",
34
+ message: error instanceof Error ? error.message : "Unable to read .m5kdev.json.",
35
+ path: ".m5kdev.json",
36
+ suggestion: "Run m5kdev init to establish a managed baseline."
37
+ });
38
+ return require_src_diagnostics.createDiagnosticReport(diagnostics);
39
+ }
40
+ const cliVersion = require_src_state.getCliVersion();
41
+ if (state.template.name !== "minimal-app") diagnostics.push({
42
+ code: "STATE_TEMPLATE_UNKNOWN",
43
+ severity: "error",
44
+ message: `Unsupported template name: ${state.template.name}`,
45
+ path: ".m5kdev.json"
46
+ });
47
+ if (!semver.default.valid(state.template.version)) diagnostics.push({
48
+ code: "STATE_VERSION_INVALID",
49
+ severity: "error",
50
+ message: `Invalid template version: ${state.template.version}`,
51
+ path: ".m5kdev.json"
52
+ });
53
+ else if (semver.default.gt(state.template.version, cliVersion)) diagnostics.push({
54
+ code: "CLI_TOO_OLD",
55
+ severity: "error",
56
+ message: `This repository is managed by ${state.template.version}, but the running CLI is ${cliVersion}.`,
57
+ suggestion: `Run pnpm dlx create-m5kdev@${state.template.version} doctor.`
58
+ });
59
+ else if (semver.default.lt(state.template.version, cliVersion)) diagnostics.push({
60
+ code: "UPDATE_AVAILABLE",
61
+ severity: "warning",
62
+ message: `Template ${cliVersion} is available; this repository is at ${state.template.version}.`,
63
+ suggestion: `Preview it with pnpm dlx create-m5kdev@${cliVersion} update --dry-run.`
64
+ });
65
+ const manifest = require_src_template.loadTemplateManifest(require_src_paths.getTemplateRoot());
66
+ const seenFeatures = /* @__PURE__ */ new Set();
67
+ for (const feature of state.template.features) {
68
+ if (seenFeatures.has(feature)) diagnostics.push({
69
+ code: "FEATURE_DUPLICATE",
70
+ severity: "error",
71
+ message: `Feature ${feature} is listed more than once.`,
72
+ path: ".m5kdev.json"
73
+ });
74
+ else if (!(feature in manifest.features)) diagnostics.push({
75
+ code: "FEATURE_UNKNOWN",
76
+ severity: "error",
77
+ message: `Unknown managed feature: ${feature}`,
78
+ path: ".m5kdev.json"
79
+ });
80
+ seenFeatures.add(feature);
81
+ }
82
+ const requiredPaths = new Set(manifest.requiredPaths);
83
+ if (state.template.features.includes("webapp")) requiredPaths.add("apps/webapp/package.json");
84
+ if (state.template.features.includes("expo")) requiredPaths.add("apps/expo/package.json");
85
+ if (state.template.features.includes("test-harness")) requiredPaths.add("apps/e2e/package.json");
86
+ for (const relativePath of requiredPaths) {
87
+ const symlink = await require_src_changes.findRepositorySymlink(options.repoRoot, relativePath);
88
+ if (symlink) diagnostics.push({
89
+ code: "MANAGED_PATH_SYMLINK",
90
+ severity: "error",
91
+ message: `Required managed path contains symbolic link ${symlink}.`,
92
+ path: relativePath
93
+ });
94
+ else if (!await exists(node_path.default.join(options.repoRoot, relativePath))) diagnostics.push({
95
+ code: "REQUIRED_PATH_MISSING",
96
+ severity: "error",
97
+ message: "Required managed path is missing.",
98
+ path: relativePath,
99
+ suggestion: "Restore the path or correct the enabled features before initializing/updating."
100
+ });
101
+ }
102
+ const workspaceRelativePath = "pnpm-workspace.yaml";
103
+ const workspacePath = node_path.default.join(options.repoRoot, workspaceRelativePath);
104
+ if (!await require_src_changes.findRepositorySymlink(options.repoRoot, workspaceRelativePath) && await exists(workspacePath)) try {
105
+ const actualCatalog = require_src_catalog.readCatalog(await node_fs_promises.default.readFile(workspacePath, "utf8"));
106
+ for (const [name, expected] of Object.entries(state.catalog)) {
107
+ if (!semver.default.validRange(expected)) diagnostics.push({
108
+ code: "CATALOG_VERSION_INVALID",
109
+ severity: "error",
110
+ message: `${name} has an invalid managed SemVer range: ${expected}.`,
111
+ path: ".m5kdev.json"
112
+ });
113
+ if (!(name in actualCatalog)) diagnostics.push({
114
+ code: "CATALOG_ENTRY_MISSING",
115
+ severity: "error",
116
+ message: `Managed catalog entry ${name} is missing.`,
117
+ path: "pnpm-workspace.yaml",
118
+ suggestion: `Restore ${name}: ${expected}.`
119
+ });
120
+ else if (actualCatalog[name] !== expected) diagnostics.push({
121
+ code: "CATALOG_VERSION_MISMATCH",
122
+ severity: "error",
123
+ message: `${name} is ${actualCatalog[name]}, expected managed version ${expected}.`,
124
+ path: "pnpm-workspace.yaml"
125
+ });
126
+ }
127
+ const appsDirectory = node_path.default.join(options.repoRoot, "apps");
128
+ const packageFiles = [node_path.default.join(options.repoRoot, "package.json")];
129
+ if (await exists(appsDirectory)) packageFiles.push(...require_src_catalog.walkPackageJsonFiles(appsDirectory));
130
+ for (const dependency of require_src_catalog.collectConsumerDependencyNames(packageFiles)) if (!(dependency in actualCatalog)) diagnostics.push({
131
+ code: "CATALOG_REFERENCE_MISSING",
132
+ severity: "error",
133
+ message: `${dependency} uses catalog: but has no catalog entry.`,
134
+ path: "pnpm-workspace.yaml"
135
+ });
136
+ } catch (error) {
137
+ diagnostics.push({
138
+ code: "CATALOG_INVALID",
139
+ severity: "error",
140
+ message: error instanceof Error ? error.message : "Unable to parse the consumer catalog.",
141
+ path: "pnpm-workspace.yaml"
142
+ });
143
+ }
144
+ for (const [relativePath, baseline] of Object.entries(state.files)) {
145
+ if (baseline.policy === "ignore") continue;
146
+ const filePath = node_path.default.join(options.repoRoot, relativePath);
147
+ const symlink = await require_src_changes.findRepositorySymlink(options.repoRoot, relativePath);
148
+ if (symlink) {
149
+ diagnostics.push({
150
+ code: "MANAGED_PATH_SYMLINK",
151
+ severity: "error",
152
+ message: `Managed path contains symbolic link ${symlink}.`,
153
+ path: relativePath
154
+ });
155
+ continue;
156
+ }
157
+ const stat = await node_fs_promises.default.lstat(filePath).catch(() => null);
158
+ if (!stat) {
159
+ if (baseline.policy === "ensure") diagnostics.push({
160
+ code: "ENSURED_PATH_MISSING",
161
+ severity: "error",
162
+ message: "An ensured template path is missing.",
163
+ path: relativePath
164
+ });
165
+ continue;
166
+ }
167
+ if (!stat.isFile()) continue;
168
+ const content = await node_fs_promises.default.readFile(filePath);
169
+ if (baseline.sha256 && require_src_state.sha256(content) !== baseline.sha256) diagnostics.push({
170
+ code: "TEMPLATE_CUSTOMIZED",
171
+ severity: "info",
172
+ message: "Managed template content has local customizations.",
173
+ path: relativePath
174
+ });
175
+ if (baseline.kind === "text") {
176
+ const source = content.toString("utf8");
177
+ if (/\{\{(?:APP_NAME|APP_DESCRIPTION|APP_SLUG|PACKAGE_SCOPE|BETTER_AUTH_SECRET)\}\}/.test(source)) diagnostics.push({
178
+ code: "UNRESOLVED_TEMPLATE_TOKEN",
179
+ severity: "error",
180
+ message: "Unresolved template token found.",
181
+ path: relativePath
182
+ });
183
+ if (/^(<{7}|={7}|>{7})(?: |$)/m.test(source)) diagnostics.push({
184
+ code: "MERGE_CONFLICT_MARKER",
185
+ severity: "error",
186
+ message: "Unresolved merge conflict marker found.",
187
+ path: relativePath
188
+ });
189
+ }
190
+ }
191
+ const pending = require_src_migrations_registry.getPendingMigrations(state, cliVersion);
192
+ for (const migration of pending) diagnostics.push({
193
+ code: "MIGRATION_PENDING",
194
+ severity: "error",
195
+ message: migration.description,
196
+ migrationId: migration.id,
197
+ guide: migration.guide,
198
+ suggestion: `Run pnpm dlx create-m5kdev@${cliVersion} update.`
199
+ });
200
+ diagnostics.push(...await require_src_migrations_registry.runMigrationValidators({
201
+ repoRoot: options.repoRoot,
202
+ state,
203
+ changes: new require_src_changes.ChangeSet(options.repoRoot)
204
+ }));
205
+ if (options.full) {
206
+ const packagePath = node_path.default.join(options.repoRoot, "package.json");
207
+ let packageManifest;
208
+ try {
209
+ packageManifest = JSON.parse(await node_fs_promises.default.readFile(packagePath, "utf8"));
210
+ } catch (error) {
211
+ diagnostics.push({
212
+ code: "PACKAGE_JSON_UNREADABLE",
213
+ severity: "error",
214
+ message: error instanceof Error ? `Unable to read package.json: ${error.message}` : "Unable to read package.json.",
215
+ path: "package.json"
216
+ });
217
+ }
218
+ if (packageManifest) for (const script of [
219
+ "check-types",
220
+ "lint",
221
+ "build"
222
+ ]) {
223
+ if (!packageManifest.scripts?.[script]) continue;
224
+ try {
225
+ await execFileAsync("pnpm", [script], {
226
+ cwd: options.repoRoot,
227
+ env: process.env,
228
+ timeout: FULL_COMMAND_TIMEOUT_MS
229
+ });
230
+ } catch (error) {
231
+ diagnostics.push({
232
+ code: `COMMAND_${script.replace("-", "_").toUpperCase()}_FAILED`,
233
+ severity: "error",
234
+ message: `${script} failed: ${error instanceof Error ? error.message : String(error)}`,
235
+ path: "package.json"
236
+ });
237
+ }
238
+ }
239
+ }
240
+ return require_src_diagnostics.createDiagnosticReport(diagnostics);
241
+ }
242
+ //#endregion
243
+ exports.diagnoseManagedRepo = diagnoseManagedRepo;
244
+
245
+ //# sourceMappingURL=doctor.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.cjs","names":["execFile","fs","readManagedState","createDiagnosticReport","getCliVersion","loadTemplateManifest","getTemplateRoot","findRepositorySymlink","path","readCatalog","walkPackageJsonFiles","collectConsumerDependencyNames","sha256","getPendingMigrations","runMigrationValidators","ChangeSet"],"sources":["../../src/doctor.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\r\nimport fs from \"node:fs/promises\";\r\nimport path from \"node:path\";\r\nimport { promisify } from \"node:util\";\r\nimport semver from \"semver\";\r\nimport { collectConsumerDependencyNames, readCatalog, walkPackageJsonFiles } from \"./catalog\";\r\nimport { ChangeSet, findRepositorySymlink } from \"./changes\";\r\nimport { TEMPLATE_NAME } from \"./constants\";\r\nimport { createDiagnosticReport, type Diagnostic, type DiagnosticReport } from \"./diagnostics\";\r\nimport { getPendingMigrations, runMigrationValidators } from \"./migrations/registry\";\r\nimport { getTemplateRoot } from \"./paths\";\r\nimport { getCliVersion, type ManagedState, readManagedState, sha256 } from \"./state\";\r\nimport { loadTemplateManifest } from \"./template\";\r\n\r\nconst execFileAsync = promisify(execFile);\r\nconst FULL_COMMAND_TIMEOUT_MS = 10 * 60 * 1000;\r\n\r\nexport interface DoctorOptions {\r\n repoRoot: string;\r\n full?: boolean;\r\n state?: ManagedState;\r\n}\r\n\r\nasync function exists(filePath: string): Promise<boolean> {\r\n return fs\r\n .access(filePath)\r\n .then(() => true)\r\n .catch(() => false);\r\n}\r\n\r\nexport async function diagnoseManagedRepo(options: DoctorOptions): Promise<DiagnosticReport> {\r\n const diagnostics: Diagnostic[] = [];\r\n let state = options.state;\r\n if (!state) {\r\n try {\r\n state = await readManagedState(options.repoRoot);\r\n } catch (error) {\r\n diagnostics.push({\r\n code: \"STATE_INVALID\",\r\n severity: \"error\",\r\n message: error instanceof Error ? error.message : \"Unable to read .m5kdev.json.\",\r\n path: \".m5kdev.json\",\r\n suggestion: \"Run m5kdev init to establish a managed baseline.\",\r\n });\r\n return createDiagnosticReport(diagnostics);\r\n }\r\n }\r\n\r\n const cliVersion = getCliVersion();\r\n if (state.template.name !== TEMPLATE_NAME) {\r\n diagnostics.push({\r\n code: \"STATE_TEMPLATE_UNKNOWN\",\r\n severity: \"error\",\r\n message: `Unsupported template name: ${state.template.name}`,\r\n path: \".m5kdev.json\",\r\n });\r\n }\r\n if (!semver.valid(state.template.version)) {\r\n diagnostics.push({\r\n code: \"STATE_VERSION_INVALID\",\r\n severity: \"error\",\r\n message: `Invalid template version: ${state.template.version}`,\r\n path: \".m5kdev.json\",\r\n });\r\n } else if (semver.gt(state.template.version, cliVersion)) {\r\n diagnostics.push({\r\n code: \"CLI_TOO_OLD\",\r\n severity: \"error\",\r\n message: `This repository is managed by ${state.template.version}, but the running CLI is ${cliVersion}.`,\r\n suggestion: `Run pnpm dlx create-m5kdev@${state.template.version} doctor.`,\r\n });\r\n } else if (semver.lt(state.template.version, cliVersion)) {\r\n diagnostics.push({\r\n code: \"UPDATE_AVAILABLE\",\r\n severity: \"warning\",\r\n message: `Template ${cliVersion} is available; this repository is at ${state.template.version}.`,\r\n suggestion: `Preview it with pnpm dlx create-m5kdev@${cliVersion} update --dry-run.`,\r\n });\r\n }\r\n\r\n const manifest = loadTemplateManifest(getTemplateRoot());\r\n const seenFeatures = new Set<string>();\r\n for (const feature of state.template.features) {\r\n if (seenFeatures.has(feature)) {\r\n diagnostics.push({\r\n code: \"FEATURE_DUPLICATE\",\r\n severity: \"error\",\r\n message: `Feature ${feature} is listed more than once.`,\r\n path: \".m5kdev.json\",\r\n });\r\n } else if (!(feature in manifest.features)) {\r\n diagnostics.push({\r\n code: \"FEATURE_UNKNOWN\",\r\n severity: \"error\",\r\n message: `Unknown managed feature: ${feature}`,\r\n path: \".m5kdev.json\",\r\n });\r\n }\r\n seenFeatures.add(feature);\r\n }\r\n const requiredPaths = new Set(manifest.requiredPaths);\r\n if (state.template.features.includes(\"webapp\")) requiredPaths.add(\"apps/webapp/package.json\");\r\n if (state.template.features.includes(\"expo\")) requiredPaths.add(\"apps/expo/package.json\");\r\n if (state.template.features.includes(\"test-harness\")) requiredPaths.add(\"apps/e2e/package.json\");\r\n for (const relativePath of requiredPaths) {\r\n const symlink = await findRepositorySymlink(options.repoRoot, relativePath);\r\n if (symlink) {\r\n diagnostics.push({\r\n code: \"MANAGED_PATH_SYMLINK\",\r\n severity: \"error\",\r\n message: `Required managed path contains symbolic link ${symlink}.`,\r\n path: relativePath,\r\n });\r\n } else if (!(await exists(path.join(options.repoRoot, relativePath)))) {\r\n diagnostics.push({\r\n code: \"REQUIRED_PATH_MISSING\",\r\n severity: \"error\",\r\n message: \"Required managed path is missing.\",\r\n path: relativePath,\r\n suggestion:\r\n \"Restore the path or correct the enabled features before initializing/updating.\",\r\n });\r\n }\r\n }\r\n\r\n const workspaceRelativePath = \"pnpm-workspace.yaml\";\r\n const workspacePath = path.join(options.repoRoot, workspaceRelativePath);\r\n const workspaceSymlink = await findRepositorySymlink(options.repoRoot, workspaceRelativePath);\r\n if (!workspaceSymlink && (await exists(workspacePath))) {\r\n try {\r\n const actualCatalog = readCatalog(await fs.readFile(workspacePath, \"utf8\"));\r\n for (const [name, expected] of Object.entries(state.catalog)) {\r\n if (!semver.validRange(expected)) {\r\n diagnostics.push({\r\n code: \"CATALOG_VERSION_INVALID\",\r\n severity: \"error\",\r\n message: `${name} has an invalid managed SemVer range: ${expected}.`,\r\n path: \".m5kdev.json\",\r\n });\r\n }\r\n if (!(name in actualCatalog)) {\r\n diagnostics.push({\r\n code: \"CATALOG_ENTRY_MISSING\",\r\n severity: \"error\",\r\n message: `Managed catalog entry ${name} is missing.`,\r\n path: \"pnpm-workspace.yaml\",\r\n suggestion: `Restore ${name}: ${expected}.`,\r\n });\r\n } else if (actualCatalog[name] !== expected) {\r\n diagnostics.push({\r\n code: \"CATALOG_VERSION_MISMATCH\",\r\n severity: \"error\",\r\n message: `${name} is ${actualCatalog[name]}, expected managed version ${expected}.`,\r\n path: \"pnpm-workspace.yaml\",\r\n });\r\n }\r\n }\r\n\r\n const appsDirectory = path.join(options.repoRoot, \"apps\");\r\n const packageFiles = [path.join(options.repoRoot, \"package.json\")];\r\n if (await exists(appsDirectory)) packageFiles.push(...walkPackageJsonFiles(appsDirectory));\r\n for (const dependency of collectConsumerDependencyNames(packageFiles)) {\r\n if (!(dependency in actualCatalog)) {\r\n diagnostics.push({\r\n code: \"CATALOG_REFERENCE_MISSING\",\r\n severity: \"error\",\r\n message: `${dependency} uses catalog: but has no catalog entry.`,\r\n path: \"pnpm-workspace.yaml\",\r\n });\r\n }\r\n }\r\n } catch (error) {\r\n diagnostics.push({\r\n code: \"CATALOG_INVALID\",\r\n severity: \"error\",\r\n message: error instanceof Error ? error.message : \"Unable to parse the consumer catalog.\",\r\n path: \"pnpm-workspace.yaml\",\r\n });\r\n }\r\n }\r\n\r\n for (const [relativePath, baseline] of Object.entries(state.files)) {\r\n if (baseline.policy === \"ignore\") continue;\r\n const filePath = path.join(options.repoRoot, relativePath);\r\n const symlink = await findRepositorySymlink(options.repoRoot, relativePath);\r\n if (symlink) {\r\n diagnostics.push({\r\n code: \"MANAGED_PATH_SYMLINK\",\r\n severity: \"error\",\r\n message: `Managed path contains symbolic link ${symlink}.`,\r\n path: relativePath,\r\n });\r\n continue;\r\n }\r\n const stat = await fs.lstat(filePath).catch(() => null);\r\n if (!stat) {\r\n if (baseline.policy === \"ensure\") {\r\n diagnostics.push({\r\n code: \"ENSURED_PATH_MISSING\",\r\n severity: \"error\",\r\n message: \"An ensured template path is missing.\",\r\n path: relativePath,\r\n });\r\n }\r\n continue;\r\n }\r\n if (!stat.isFile()) continue;\r\n const content = await fs.readFile(filePath);\r\n if (baseline.sha256 && sha256(content) !== baseline.sha256) {\r\n diagnostics.push({\r\n code: \"TEMPLATE_CUSTOMIZED\",\r\n severity: \"info\",\r\n message: \"Managed template content has local customizations.\",\r\n path: relativePath,\r\n });\r\n }\r\n if (baseline.kind === \"text\") {\r\n const source = content.toString(\"utf8\");\r\n if (\r\n /\\{\\{(?:APP_NAME|APP_DESCRIPTION|APP_SLUG|PACKAGE_SCOPE|BETTER_AUTH_SECRET)\\}\\}/.test(\r\n source\r\n )\r\n ) {\r\n diagnostics.push({\r\n code: \"UNRESOLVED_TEMPLATE_TOKEN\",\r\n severity: \"error\",\r\n message: \"Unresolved template token found.\",\r\n path: relativePath,\r\n });\r\n }\r\n if (/^(<{7}|={7}|>{7})(?: |$)/m.test(source)) {\r\n diagnostics.push({\r\n code: \"MERGE_CONFLICT_MARKER\",\r\n severity: \"error\",\r\n message: \"Unresolved merge conflict marker found.\",\r\n path: relativePath,\r\n });\r\n }\r\n }\r\n }\r\n\r\n const pending = getPendingMigrations(state, cliVersion);\r\n for (const migration of pending) {\r\n diagnostics.push({\r\n code: \"MIGRATION_PENDING\",\r\n severity: \"error\",\r\n message: migration.description,\r\n migrationId: migration.id,\r\n guide: migration.guide,\r\n suggestion: `Run pnpm dlx create-m5kdev@${cliVersion} update.`,\r\n });\r\n }\r\n diagnostics.push(\r\n ...(await runMigrationValidators({\r\n repoRoot: options.repoRoot,\r\n state,\r\n changes: new ChangeSet(options.repoRoot),\r\n }))\r\n );\r\n\r\n if (options.full) {\r\n const packagePath = path.join(options.repoRoot, \"package.json\");\r\n let packageManifest: { scripts?: Record<string, string> } | undefined;\r\n try {\r\n packageManifest = JSON.parse(await fs.readFile(packagePath, \"utf8\")) as {\r\n scripts?: Record<string, string>;\r\n };\r\n } catch (error) {\r\n diagnostics.push({\r\n code: \"PACKAGE_JSON_UNREADABLE\",\r\n severity: \"error\",\r\n message:\r\n error instanceof Error\r\n ? `Unable to read package.json: ${error.message}`\r\n : \"Unable to read package.json.\",\r\n path: \"package.json\",\r\n });\r\n }\r\n if (packageManifest) {\r\n for (const script of [\"check-types\", \"lint\", \"build\"] as const) {\r\n if (!packageManifest.scripts?.[script]) continue;\r\n try {\r\n await execFileAsync(\"pnpm\", [script], {\r\n cwd: options.repoRoot,\r\n env: process.env,\r\n timeout: FULL_COMMAND_TIMEOUT_MS,\r\n });\r\n } catch (error) {\r\n diagnostics.push({\r\n code: `COMMAND_${script.replace(\"-\", \"_\").toUpperCase()}_FAILED`,\r\n severity: \"error\",\r\n message: `${script} failed: ${error instanceof Error ? error.message : String(error)}`,\r\n path: \"package.json\",\r\n });\r\n }\r\n }\r\n }\r\n }\r\n\r\n return createDiagnosticReport(diagnostics);\r\n}\r\n"],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import { DiagnosticReport } from "./diagnostics.cjs";
2
+ import { ManagedState } from "./state.cjs";
3
+
4
+ //#region src/doctor.d.ts
5
+ interface DoctorOptions {
6
+ repoRoot: string;
7
+ full?: boolean;
8
+ state?: ManagedState;
9
+ }
10
+ declare function diagnoseManagedRepo(options: DoctorOptions): Promise<DiagnosticReport>;
11
+ //#endregion
12
+ export { DoctorOptions, diagnoseManagedRepo };
13
+ //# sourceMappingURL=doctor.d.cts.map
package/dist/src/fs.cjs CHANGED
@@ -3,9 +3,9 @@ const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
3
3
  const require_src_strings = require("./strings.cjs");
4
4
  let node_path = require("node:path");
5
5
  node_path = require_runtime.__toESM(node_path);
6
- let node_child_process = require("node:child_process");
7
6
  let node_fs_promises = require("node:fs/promises");
8
7
  node_fs_promises = require_runtime.__toESM(node_fs_promises);
8
+ let node_child_process = require("node:child_process");
9
9
  //#region src/fs.ts
10
10
  const execFileAsync = (0, require("node:util").promisify)(node_child_process.execFile);
11
11
  const TEXT_EXTENSIONS = new Set([
@@ -32,14 +32,62 @@ const TEXT_BASENAMES = new Set([
32
32
  ]);
33
33
  /** Feature marker blocks: lines between `// m5k:<feature>:start` and `// m5k:<feature>:end`. */
34
34
  const MARKER_PATTERN = /^[ \t]*\/\/ m5k:([a-z-]+):(start|end)[ \t]*$/;
35
+ async function collectTemplateFiles(templateDirectory, context, options = {}, relativeBase = "") {
36
+ const entries = await node_fs_promises.default.readdir(templateDirectory, { withFileTypes: true });
37
+ const excludePrefixes = options.excludePrefixes ?? [];
38
+ const enabledFeatures = options.enabledFeatures ?? /* @__PURE__ */ new Set();
39
+ const files = [];
40
+ for (const entry of entries) {
41
+ const sourcePath = node_path.default.join(templateDirectory, entry.name);
42
+ const targetName = require_src_strings.renderTemplate(entry.name.endsWith(".tpl") ? entry.name.slice(0, -4) : entry.name, context);
43
+ const relativePath = relativeBase ? `${relativeBase}/${targetName}` : targetName;
44
+ if (relativePath === "template.manifest.json") continue;
45
+ const asDirPrefix = `${relativePath}/`;
46
+ if (excludePrefixes.some((prefix) => relativePath === prefix || asDirPrefix === prefix || relativePath.startsWith(prefix))) continue;
47
+ if (entry.isDirectory()) {
48
+ files.push(...await collectTemplateFiles(sourcePath, context, options, relativePath));
49
+ continue;
50
+ }
51
+ if (entry.isSymbolicLink()) throw new Error(`Template symlinks are not supported: ${relativePath}`);
52
+ if (!entry.isFile()) continue;
53
+ if (options.ignoreContentFor?.(relativePath)) {
54
+ files.push({
55
+ content: Buffer.alloc(0),
56
+ kind: isTextFile(entry.name) ? "text" : "binary",
57
+ relativePath
58
+ });
59
+ continue;
60
+ }
61
+ if (!isTextFile(entry.name)) {
62
+ files.push({
63
+ content: await node_fs_promises.default.readFile(sourcePath),
64
+ kind: "binary",
65
+ relativePath
66
+ });
67
+ continue;
68
+ }
69
+ const content = await node_fs_promises.default.readFile(sourcePath, "utf8");
70
+ files.push({
71
+ content: Buffer.from(applyFeatureMarkers(require_src_strings.renderTemplate(content, context), enabledFeatures).replace(/\r\n?/g, "\n"), "utf8"),
72
+ kind: "text",
73
+ relativePath
74
+ });
75
+ }
76
+ return files;
77
+ }
78
+ /**
79
+ * Ensures the target directory exists and may be written.
80
+ * Returns true when this call created the directory (safe to remove on failure).
81
+ */
35
82
  async function ensureDirectoryState(targetDirectory, force) {
36
83
  const stat = await node_fs_promises.default.stat(targetDirectory).catch(() => null);
37
84
  if (!stat) {
38
85
  await node_fs_promises.default.mkdir(targetDirectory, { recursive: true });
39
- return;
86
+ return true;
40
87
  }
41
88
  if (!stat.isDirectory()) throw new Error(`Target path is not a directory: ${targetDirectory}`);
42
89
  if ((await node_fs_promises.default.readdir(targetDirectory)).length > 0 && !force) throw new Error(`Target directory is not empty: ${targetDirectory}. Re-run with --force to overwrite.`);
90
+ return false;
43
91
  }
44
92
  function isTextFile(fileName) {
45
93
  const withoutTpl = fileName.endsWith(".tpl") ? fileName.slice(0, -4) : fileName;
@@ -67,31 +115,12 @@ function applyFeatureMarkers(content, enabledFeatures) {
67
115
  }
68
116
  return out.join("\n");
69
117
  }
70
- async function copyTemplateDirectory(templateDirectory, targetDirectory, context, options = {}, relativeBase = "") {
71
- const entries = await node_fs_promises.default.readdir(templateDirectory, { withFileTypes: true });
72
- const excludePrefixes = options.excludePrefixes ?? [];
73
- const enabledFeatures = options.enabledFeatures ?? /* @__PURE__ */ new Set();
74
- for (const entry of entries) {
75
- const sourcePath = node_path.default.join(templateDirectory, entry.name);
76
- const targetName = require_src_strings.renderTemplate(entry.name.endsWith(".tpl") ? entry.name.slice(0, -4) : entry.name, context);
77
- const relativePath = relativeBase ? `${relativeBase}/${targetName}` : targetName;
78
- if (relativePath === "template.manifest.json") continue;
79
- const asDirPrefix = `${relativePath}/`;
80
- if (excludePrefixes.some((p) => relativePath === p || asDirPrefix === p || relativePath.startsWith(p))) continue;
81
- const targetPath = node_path.default.join(targetDirectory, targetName);
82
- if (entry.isDirectory()) {
83
- await node_fs_promises.default.mkdir(targetPath, { recursive: true });
84
- await copyTemplateDirectory(sourcePath, targetPath, context, options, relativePath);
85
- continue;
86
- }
87
- if (!entry.isFile()) continue;
118
+ async function copyTemplateDirectory(templateDirectory, targetDirectory, context, options = {}) {
119
+ const files = await collectTemplateFiles(templateDirectory, context, options);
120
+ for (const file of files) {
121
+ const targetPath = node_path.default.join(targetDirectory, file.relativePath);
88
122
  await node_fs_promises.default.mkdir(node_path.default.dirname(targetPath), { recursive: true });
89
- if (!isTextFile(entry.name)) {
90
- await node_fs_promises.default.copyFile(sourcePath, targetPath);
91
- continue;
92
- }
93
- const rendered = applyFeatureMarkers(require_src_strings.renderTemplate(await node_fs_promises.default.readFile(sourcePath, "utf8"), context), enabledFeatures).replace(/\r\n?/g, "\n");
94
- await node_fs_promises.default.writeFile(targetPath, rendered, "utf8");
123
+ await node_fs_promises.default.writeFile(targetPath, file.content);
95
124
  }
96
125
  }
97
126
  async function runInstall(targetDirectory) {
@@ -108,6 +137,7 @@ async function runGitInit(targetDirectory) {
108
137
  }
109
138
  //#endregion
110
139
  exports.applyFeatureMarkers = applyFeatureMarkers;
140
+ exports.collectTemplateFiles = collectTemplateFiles;
111
141
  exports.copyTemplateDirectory = copyTemplateDirectory;
112
142
  exports.ensureDirectoryState = ensureDirectoryState;
113
143
  exports.runGitInit = runGitInit;
@@ -1 +1 @@
1
- {"version":3,"file":"fs.cjs","names":["execFile","fs","path","renderTemplate"],"sources":["../../src/fs.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { promisify } from \"node:util\";\nimport type { TemplateContext } from \"./types\";\nimport { renderTemplate } from \"./strings\";\n\nconst execFileAsync = promisify(execFile);\n\nconst TEXT_EXTENSIONS = new Set([\n \".ts\", \".tsx\", \".js\", \".mjs\", \".cjs\", \".json\", \".md\", \".css\", \".html\",\n \".svg\", \".yaml\", \".yml\", \".txt\", \".mdc\",\n]);\nconst TEXT_BASENAMES = new Set([\".gitignore\", \".env\", \".env.example\", \".npmrc\"]);\n\n/** Feature marker blocks: lines between `// m5k:<feature>:start` and `// m5k:<feature>:end`. */\nconst MARKER_PATTERN = /^[ \\t]*\\/\\/ m5k:([a-z-]+):(start|end)[ \\t]*$/;\n\nexport interface CopyTemplateOptions {\n /** Repo-root-relative path prefixes to skip entirely (disabled features). */\n excludePrefixes?: readonly string[];\n /** Features whose marker blocks should be kept (markers removed, content kept). */\n enabledFeatures?: ReadonlySet<string>;\n}\n\nexport async function ensureDirectoryState(\n targetDirectory: string,\n force: boolean\n): Promise<void> {\n const stat = await fs.stat(targetDirectory).catch(() => null);\n\n if (!stat) {\n await fs.mkdir(targetDirectory, { recursive: true });\n return;\n }\n\n if (!stat.isDirectory()) {\n throw new Error(`Target path is not a directory: ${targetDirectory}`);\n }\n\n const entries = await fs.readdir(targetDirectory);\n if (entries.length > 0 && !force) {\n throw new Error(\n `Target directory is not empty: ${targetDirectory}. Re-run with --force to overwrite.`\n );\n }\n}\n\nfunction isTextFile(fileName: string): boolean {\n const withoutTpl = fileName.endsWith(\".tpl\") ? fileName.slice(0, -4) : fileName;\n if (TEXT_BASENAMES.has(withoutTpl)) return true;\n return TEXT_EXTENSIONS.has(path.extname(withoutTpl));\n}\n\n/**\n * Removes feature marker lines. Blocks for disabled features are dropped with\n * their content; blocks for enabled features keep the content, lose the markers.\n */\nexport function applyFeatureMarkers(content: string, enabledFeatures: ReadonlySet<string>): string {\n const lines = content.split(\"\\n\");\n const out: string[] = [];\n let skippingFeature: string | undefined;\n\n for (const line of lines) {\n const match = line.match(MARKER_PATTERN);\n if (!match) {\n if (skippingFeature === undefined) out.push(line);\n continue;\n }\n\n const [, feature, edge] = match;\n if (edge === \"start\") {\n if (!enabledFeatures.has(feature)) skippingFeature = feature;\n } else if (skippingFeature === feature) {\n skippingFeature = undefined;\n }\n // marker lines themselves are never emitted\n }\n\n return out.join(\"\\n\");\n}\n\nexport async function copyTemplateDirectory(\n templateDirectory: string,\n targetDirectory: string,\n context: TemplateContext,\n options: CopyTemplateOptions = {},\n relativeBase = \"\"\n): Promise<void> {\n const entries = await fs.readdir(templateDirectory, { withFileTypes: true });\n const excludePrefixes = options.excludePrefixes ?? [];\n const enabledFeatures = options.enabledFeatures ?? new Set<string>();\n\n for (const entry of entries) {\n const sourcePath = path.join(templateDirectory, entry.name);\n const targetName = renderTemplate(\n entry.name.endsWith(\".tpl\") ? entry.name.slice(0, -4) : entry.name,\n context\n );\n const relativePath = relativeBase ? `${relativeBase}/${targetName}` : targetName;\n\n if (relativePath === \"template.manifest.json\") {\n continue;\n }\n\n const asDirPrefix = `${relativePath}/`;\n if (excludePrefixes.some((p) => relativePath === p || asDirPrefix === p || relativePath.startsWith(p))) {\n continue;\n }\n\n const targetPath = path.join(targetDirectory, targetName);\n\n if (entry.isDirectory()) {\n await fs.mkdir(targetPath, { recursive: true });\n await copyTemplateDirectory(sourcePath, targetPath, context, options, relativePath);\n continue;\n }\n\n if (!entry.isFile()) {\n continue;\n }\n\n await fs.mkdir(path.dirname(targetPath), { recursive: true });\n\n if (!isTextFile(entry.name)) {\n await fs.copyFile(sourcePath, targetPath);\n continue;\n }\n\n const content = await fs.readFile(sourcePath, \"utf8\");\n const rendered = applyFeatureMarkers(\n renderTemplate(content, context),\n enabledFeatures\n ).replace(/\\r\\n?/g, \"\\n\");\n await fs.writeFile(targetPath, rendered, \"utf8\");\n }\n}\n\nexport async function runInstall(targetDirectory: string): Promise<void> {\n await execFileAsync(\"pnpm\", [\"install\"], {\n cwd: targetDirectory,\n env: process.env,\n });\n}\n\nexport async function runGitInit(targetDirectory: string): Promise<void> {\n await execFileAsync(\"git\", [\"init\"], {\n cwd: targetDirectory,\n env: process.env,\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fs.cjs","names":["execFile","fs","path","renderTemplate"],"sources":["../../src/fs.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\r\nimport fs from \"node:fs/promises\";\r\nimport path from \"node:path\";\r\nimport { promisify } from \"node:util\";\r\nimport { renderTemplate } from \"./strings\";\r\nimport type { RenderedTemplateFile, TemplateContext } from \"./types\";\r\n\r\nconst execFileAsync = promisify(execFile);\r\n\r\nconst TEXT_EXTENSIONS = new Set([\r\n \".ts\",\r\n \".tsx\",\r\n \".js\",\r\n \".mjs\",\r\n \".cjs\",\r\n \".json\",\r\n \".md\",\r\n \".css\",\r\n \".html\",\r\n \".svg\",\r\n \".yaml\",\r\n \".yml\",\r\n \".txt\",\r\n \".mdc\",\r\n]);\r\nconst TEXT_BASENAMES = new Set([\".gitignore\", \".env\", \".env.example\", \".npmrc\"]);\r\n\r\n/** Feature marker blocks: lines between `// m5k:<feature>:start` and `// m5k:<feature>:end`. */\r\nconst MARKER_PATTERN = /^[ \\t]*\\/\\/ m5k:([a-z-]+):(start|end)[ \\t]*$/;\r\n\r\nexport interface CopyTemplateOptions {\r\n /** Repo-root-relative path prefixes to skip entirely (disabled features). */\r\n excludePrefixes?: readonly string[];\r\n /** Features whose marker blocks should be kept (markers removed, content kept). */\r\n enabledFeatures?: ReadonlySet<string>;\r\n /** Update inspection can retain ignored-file metadata without rendering secret content. */\r\n ignoreContentFor?: (relativePath: string) => boolean;\r\n}\r\n\r\nexport async function collectTemplateFiles(\r\n templateDirectory: string,\r\n context: TemplateContext,\r\n options: CopyTemplateOptions = {},\r\n relativeBase = \"\"\r\n): Promise<RenderedTemplateFile[]> {\r\n const entries = await fs.readdir(templateDirectory, { withFileTypes: true });\r\n const excludePrefixes = options.excludePrefixes ?? [];\r\n const enabledFeatures = options.enabledFeatures ?? new Set<string>();\r\n const files: RenderedTemplateFile[] = [];\r\n\r\n for (const entry of entries) {\r\n const sourcePath = path.join(templateDirectory, entry.name);\r\n const targetName = renderTemplate(\r\n entry.name.endsWith(\".tpl\") ? entry.name.slice(0, -4) : entry.name,\r\n context\r\n );\r\n const relativePath = relativeBase ? `${relativeBase}/${targetName}` : targetName;\r\n\r\n if (relativePath === \"template.manifest.json\") continue;\r\n\r\n const asDirPrefix = `${relativePath}/`;\r\n if (\r\n excludePrefixes.some(\r\n (prefix) =>\r\n relativePath === prefix || asDirPrefix === prefix || relativePath.startsWith(prefix)\r\n )\r\n ) {\r\n continue;\r\n }\r\n\r\n if (entry.isDirectory()) {\r\n files.push(...(await collectTemplateFiles(sourcePath, context, options, relativePath)));\r\n continue;\r\n }\r\n if (entry.isSymbolicLink()) {\r\n throw new Error(`Template symlinks are not supported: ${relativePath}`);\r\n }\r\n if (!entry.isFile()) continue;\r\n\r\n if (options.ignoreContentFor?.(relativePath)) {\r\n files.push({\r\n content: Buffer.alloc(0),\r\n kind: isTextFile(entry.name) ? \"text\" : \"binary\",\r\n relativePath,\r\n });\r\n continue;\r\n }\r\n\r\n if (!isTextFile(entry.name)) {\r\n files.push({\r\n content: await fs.readFile(sourcePath),\r\n kind: \"binary\",\r\n relativePath,\r\n });\r\n continue;\r\n }\r\n\r\n const content = await fs.readFile(sourcePath, \"utf8\");\r\n files.push({\r\n content: Buffer.from(\r\n applyFeatureMarkers(renderTemplate(content, context), enabledFeatures).replace(\r\n /\\r\\n?/g,\r\n \"\\n\"\r\n ),\r\n \"utf8\"\r\n ),\r\n kind: \"text\",\r\n relativePath,\r\n });\r\n }\r\n\r\n return files;\r\n}\r\n\r\n/**\r\n * Ensures the target directory exists and may be written.\r\n * Returns true when this call created the directory (safe to remove on failure).\r\n */\r\nexport async function ensureDirectoryState(\r\n targetDirectory: string,\r\n force: boolean\r\n): Promise<boolean> {\r\n const stat = await fs.stat(targetDirectory).catch(() => null);\r\n\r\n if (!stat) {\r\n await fs.mkdir(targetDirectory, { recursive: true });\r\n return true;\r\n }\r\n\r\n if (!stat.isDirectory()) {\r\n throw new Error(`Target path is not a directory: ${targetDirectory}`);\r\n }\r\n\r\n const entries = await fs.readdir(targetDirectory);\r\n if (entries.length > 0 && !force) {\r\n throw new Error(\r\n `Target directory is not empty: ${targetDirectory}. Re-run with --force to overwrite.`\r\n );\r\n }\r\n\r\n return false;\r\n}\r\n\r\nfunction isTextFile(fileName: string): boolean {\r\n const withoutTpl = fileName.endsWith(\".tpl\") ? fileName.slice(0, -4) : fileName;\r\n if (TEXT_BASENAMES.has(withoutTpl)) return true;\r\n return TEXT_EXTENSIONS.has(path.extname(withoutTpl));\r\n}\r\n\r\n/**\r\n * Removes feature marker lines. Blocks for disabled features are dropped with\r\n * their content; blocks for enabled features keep the content, lose the markers.\r\n */\r\nexport function applyFeatureMarkers(content: string, enabledFeatures: ReadonlySet<string>): string {\r\n const lines = content.split(\"\\n\");\r\n const out: string[] = [];\r\n let skippingFeature: string | undefined;\r\n\r\n for (const line of lines) {\r\n const match = line.match(MARKER_PATTERN);\r\n if (!match) {\r\n if (skippingFeature === undefined) out.push(line);\r\n continue;\r\n }\r\n\r\n const [, feature, edge] = match;\r\n if (edge === \"start\") {\r\n if (!enabledFeatures.has(feature)) skippingFeature = feature;\r\n } else if (skippingFeature === feature) {\r\n skippingFeature = undefined;\r\n }\r\n // marker lines themselves are never emitted\r\n }\r\n\r\n return out.join(\"\\n\");\r\n}\r\n\r\nexport async function copyTemplateDirectory(\r\n templateDirectory: string,\r\n targetDirectory: string,\r\n context: TemplateContext,\r\n options: CopyTemplateOptions = {}\r\n): Promise<void> {\r\n const files = await collectTemplateFiles(templateDirectory, context, options);\r\n for (const file of files) {\r\n const targetPath = path.join(targetDirectory, file.relativePath);\r\n await fs.mkdir(path.dirname(targetPath), { recursive: true });\r\n await fs.writeFile(targetPath, file.content);\r\n }\r\n}\r\n\r\nexport async function runInstall(targetDirectory: string): Promise<void> {\r\n await execFileAsync(\"pnpm\", [\"install\"], {\r\n cwd: targetDirectory,\r\n env: process.env,\r\n });\r\n}\r\n\r\nexport async function runGitInit(targetDirectory: string): Promise<void> {\r\n await execFileAsync(\"git\", [\"init\"], {\r\n cwd: targetDirectory,\r\n env: process.env,\r\n });\r\n}\r\n"],"mappings":"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"}
package/dist/src/fs.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { TemplateContext } from "./types.cjs";
1
+ import { RenderedTemplateFile, TemplateContext } from "./types.cjs";
2
2
 
3
3
  //#region src/fs.d.ts
4
4
  interface CopyTemplateOptions {
@@ -6,16 +6,23 @@ interface CopyTemplateOptions {
6
6
  excludePrefixes?: readonly string[];
7
7
  /** Features whose marker blocks should be kept (markers removed, content kept). */
8
8
  enabledFeatures?: ReadonlySet<string>;
9
+ /** Update inspection can retain ignored-file metadata without rendering secret content. */
10
+ ignoreContentFor?: (relativePath: string) => boolean;
9
11
  }
10
- declare function ensureDirectoryState(targetDirectory: string, force: boolean): Promise<void>;
12
+ declare function collectTemplateFiles(templateDirectory: string, context: TemplateContext, options?: CopyTemplateOptions, relativeBase?: string): Promise<RenderedTemplateFile[]>;
13
+ /**
14
+ * Ensures the target directory exists and may be written.
15
+ * Returns true when this call created the directory (safe to remove on failure).
16
+ */
17
+ declare function ensureDirectoryState(targetDirectory: string, force: boolean): Promise<boolean>;
11
18
  /**
12
19
  * Removes feature marker lines. Blocks for disabled features are dropped with
13
20
  * their content; blocks for enabled features keep the content, lose the markers.
14
21
  */
15
22
  declare function applyFeatureMarkers(content: string, enabledFeatures: ReadonlySet<string>): string;
16
- declare function copyTemplateDirectory(templateDirectory: string, targetDirectory: string, context: TemplateContext, options?: CopyTemplateOptions, relativeBase?: string): Promise<void>;
23
+ declare function copyTemplateDirectory(templateDirectory: string, targetDirectory: string, context: TemplateContext, options?: CopyTemplateOptions): Promise<void>;
17
24
  declare function runInstall(targetDirectory: string): Promise<void>;
18
25
  declare function runGitInit(targetDirectory: string): Promise<void>;
19
26
  //#endregion
20
- export { CopyTemplateOptions, applyFeatureMarkers, copyTemplateDirectory, ensureDirectoryState, runGitInit, runInstall };
27
+ export { CopyTemplateOptions, applyFeatureMarkers, collectTemplateFiles, copyTemplateDirectory, ensureDirectoryState, runGitInit, runInstall };
21
28
  //# sourceMappingURL=fs.d.cts.map
@@ -0,0 +1,110 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
3
+ const require_src_strings = require("./strings.cjs");
4
+ const require_src_fs = require("./fs.cjs");
5
+ const require_src_paths = require("./paths.cjs");
6
+ const require_src_template = require("./template.cjs");
7
+ const require_src_state = require("./state.cjs");
8
+ const require_src_doctor = require("./doctor.cjs");
9
+ let node_path = require("node:path");
10
+ node_path = require_runtime.__toESM(node_path);
11
+ let node_fs_promises = require("node:fs/promises");
12
+ node_fs_promises = require_runtime.__toESM(node_fs_promises);
13
+ let node_process = require("node:process");
14
+ let node_readline_promises = require("node:readline/promises");
15
+ node_readline_promises = require_runtime.__toESM(node_readline_promises);
16
+ //#region src/init.ts
17
+ async function inferPackageScope(repoRoot, fallbackSlug) {
18
+ const appsRoot = node_path.default.join(repoRoot, "apps");
19
+ const scopes = /* @__PURE__ */ new Map();
20
+ for (const entry of await node_fs_promises.default.readdir(appsRoot, { withFileTypes: true }).catch(() => [])) {
21
+ if (!entry.isDirectory()) continue;
22
+ const packagePath = node_path.default.join(appsRoot, entry.name, "package.json");
23
+ try {
24
+ const match = JSON.parse(await node_fs_promises.default.readFile(packagePath, "utf8")).name?.match(/^(@[^/]+)\//);
25
+ if (match) scopes.set(match[1], (scopes.get(match[1]) ?? 0) + 1);
26
+ } catch {}
27
+ }
28
+ return [...scopes.entries()].sort((left, right) => right[1] - left[1])[0]?.[0] ?? require_src_strings.derivePackageScope(fallbackSlug);
29
+ }
30
+ async function inferContext(repoRoot) {
31
+ const rootPackage = JSON.parse(await node_fs_promises.default.readFile(node_path.default.join(repoRoot, "package.json"), "utf8"));
32
+ if (!rootPackage.name) throw new Error("Cannot infer the app slug: root package.json has no name.");
33
+ const appSlug = rootPackage.name.split("/").at(-1);
34
+ if (!appSlug) throw new Error("Cannot infer the app slug from root package.json.");
35
+ let appName = require_src_strings.toDisplayName(appSlug.replace(/[-_]+/g, " "));
36
+ const constantsPath = node_path.default.join(repoRoot, "apps/shared/src/modules/app/app.constants.ts");
37
+ try {
38
+ appName = (await node_fs_promises.default.readFile(constantsPath, "utf8")).match(/APP_NAME\s*=\s*["']([^"']+)["']/)?.[1] ?? appName;
39
+ } catch {}
40
+ return {
41
+ appName,
42
+ appDescription: rootPackage.description ?? `${appName} uses the m5kdev stack.`,
43
+ appSlug,
44
+ packageScope: await inferPackageScope(repoRoot, appSlug),
45
+ betterAuthSecret: "ignored-by-managed-state"
46
+ };
47
+ }
48
+ async function inferFeatures(repoRoot) {
49
+ const enabled = /* @__PURE__ */ new Set();
50
+ const has = (relativePath) => node_fs_promises.default.access(node_path.default.join(repoRoot, relativePath)).then(() => true).catch(() => false);
51
+ if (await has("apps/webapp")) enabled.add("webapp");
52
+ if (await has("apps/expo")) enabled.add("expo");
53
+ if (await has("apps/e2e")) enabled.add("test-harness");
54
+ return enabled;
55
+ }
56
+ async function confirmInitialization(version, yes) {
57
+ if (yes) return;
58
+ if (!process.stdin.isTTY) throw new Error("Initialization requires confirmation. Re-run with --yes in a non-interactive shell.");
59
+ const prompt = node_readline_promises.default.createInterface({
60
+ input: node_process.stdin,
61
+ output: node_process.stdout
62
+ });
63
+ try {
64
+ const answer = await prompt.question(`Record this repository as template ${version} without applying older migrations? y/N: `);
65
+ if (!/^y(es)?$/i.test(answer.trim())) throw new Error("Initialization cancelled.");
66
+ } finally {
67
+ prompt.close();
68
+ }
69
+ }
70
+ async function initializeManagedRepo(options) {
71
+ const statePath = node_path.default.join(options.repoRoot, require_src_state.STATE_FILE_NAME);
72
+ if (!options.force && await node_fs_promises.default.access(statePath).then(() => true).catch(() => false)) throw new Error(`${require_src_state.STATE_FILE_NAME} already exists. Use --force to re-baseline intentionally.`);
73
+ await node_fs_promises.default.access(node_path.default.join(options.repoRoot, "pnpm-workspace.yaml"));
74
+ const context = await inferContext(options.repoRoot);
75
+ const enabledFeatures = await inferFeatures(options.repoRoot);
76
+ const templateRoot = require_src_paths.getTemplateRoot();
77
+ const manifest = require_src_template.loadTemplateManifest(templateRoot);
78
+ const renderedFiles = await require_src_fs.collectTemplateFiles(templateRoot, context, {
79
+ excludePrefixes: require_src_template.getExcludedFeaturePaths(manifest, enabledFeatures),
80
+ enabledFeatures
81
+ });
82
+ const version = require_src_state.getCliVersion();
83
+ const state = require_src_state.createManagedState({
84
+ templateVersion: version,
85
+ enabledFeatures,
86
+ context,
87
+ renderedFiles,
88
+ manifest
89
+ });
90
+ const report = await require_src_doctor.diagnoseManagedRepo({
91
+ repoRoot: options.repoRoot,
92
+ state
93
+ });
94
+ if (!report.ok) return {
95
+ initialized: false,
96
+ state,
97
+ report
98
+ };
99
+ await confirmInitialization(version, options.yes);
100
+ await require_src_state.writeManagedState(options.repoRoot, state);
101
+ return {
102
+ initialized: true,
103
+ state,
104
+ report
105
+ };
106
+ }
107
+ //#endregion
108
+ exports.initializeManagedRepo = initializeManagedRepo;
109
+
110
+ //# sourceMappingURL=init.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.cjs","names":["path","fs","derivePackageScope","toDisplayName","readline","STATE_FILE_NAME","getTemplateRoot","loadTemplateManifest","collectTemplateFiles","getExcludedFeaturePaths","getCliVersion","createManagedState","diagnoseManagedRepo","writeManagedState"],"sources":["../../src/init.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\r\nimport path from \"node:path\";\r\nimport { stdin as input, stdout as output } from \"node:process\";\r\nimport readline from \"node:readline/promises\";\r\nimport { diagnoseManagedRepo } from \"./doctor\";\r\nimport { collectTemplateFiles } from \"./fs\";\r\nimport { getTemplateRoot } from \"./paths\";\r\nimport { createManagedState, getCliVersion, STATE_FILE_NAME, writeManagedState } from \"./state\";\r\nimport { derivePackageScope, toDisplayName } from \"./strings\";\r\nimport { getExcludedFeaturePaths, loadTemplateManifest } from \"./template\";\r\nimport type { TemplateContext } from \"./types\";\r\n\r\nexport interface InitCommandOptions {\r\n repoRoot: string;\r\n yes: boolean;\r\n force: boolean;\r\n}\r\n\r\nasync function inferPackageScope(repoRoot: string, fallbackSlug: string): Promise<string> {\r\n const appsRoot = path.join(repoRoot, \"apps\");\r\n const scopes = new Map<string, number>();\r\n for (const entry of await fs.readdir(appsRoot, { withFileTypes: true }).catch(() => [])) {\r\n if (!entry.isDirectory()) continue;\r\n const packagePath = path.join(appsRoot, entry.name, \"package.json\");\r\n try {\r\n const manifest = JSON.parse(await fs.readFile(packagePath, \"utf8\")) as { name?: string };\r\n const match = manifest.name?.match(/^(@[^/]+)\\//);\r\n if (match) scopes.set(match[1], (scopes.get(match[1]) ?? 0) + 1);\r\n } catch {\r\n // Optional app directories do not all need package manifests.\r\n }\r\n }\r\n return (\r\n [...scopes.entries()].sort((left, right) => right[1] - left[1])[0]?.[0] ??\r\n derivePackageScope(fallbackSlug)\r\n );\r\n}\r\n\r\nasync function inferContext(repoRoot: string): Promise<TemplateContext> {\r\n const rootPackage = JSON.parse(\r\n await fs.readFile(path.join(repoRoot, \"package.json\"), \"utf8\")\r\n ) as {\r\n name?: string;\r\n description?: string;\r\n };\r\n if (!rootPackage.name)\r\n throw new Error(\"Cannot infer the app slug: root package.json has no name.\");\r\n const appSlug = rootPackage.name.split(\"/\").at(-1);\r\n if (!appSlug) throw new Error(\"Cannot infer the app slug from root package.json.\");\r\n let appName = toDisplayName(appSlug.replace(/[-_]+/g, \" \"));\r\n const constantsPath = path.join(repoRoot, \"apps/shared/src/modules/app/app.constants.ts\");\r\n try {\r\n const constants = await fs.readFile(constantsPath, \"utf8\");\r\n appName = constants.match(/APP_NAME\\s*=\\s*[\"']([^\"']+)[\"']/)?.[1] ?? appName;\r\n } catch {\r\n // The required-path diagnostic will explain missing shared configuration.\r\n }\r\n return {\r\n appName,\r\n appDescription: rootPackage.description ?? `${appName} uses the m5kdev stack.`,\r\n appSlug,\r\n packageScope: await inferPackageScope(repoRoot, appSlug),\r\n betterAuthSecret: \"ignored-by-managed-state\",\r\n };\r\n}\r\n\r\nasync function inferFeatures(repoRoot: string): Promise<Set<string>> {\r\n const enabled = new Set<string>();\r\n const has = (relativePath: string) =>\r\n fs\r\n .access(path.join(repoRoot, relativePath))\r\n .then(() => true)\r\n .catch(() => false);\r\n if (await has(\"apps/webapp\")) enabled.add(\"webapp\");\r\n if (await has(\"apps/expo\")) enabled.add(\"expo\");\r\n if (await has(\"apps/e2e\")) enabled.add(\"test-harness\");\r\n return enabled;\r\n}\r\n\r\nasync function confirmInitialization(version: string, yes: boolean): Promise<void> {\r\n if (yes) return;\r\n if (!process.stdin.isTTY) {\r\n throw new Error(\r\n \"Initialization requires confirmation. Re-run with --yes in a non-interactive shell.\"\r\n );\r\n }\r\n const prompt = readline.createInterface({ input, output });\r\n try {\r\n const answer = await prompt.question(\r\n `Record this repository as template ${version} without applying older migrations? y/N: `\r\n );\r\n if (!/^y(es)?$/i.test(answer.trim())) throw new Error(\"Initialization cancelled.\");\r\n } finally {\r\n prompt.close();\r\n }\r\n}\r\n\r\nexport async function initializeManagedRepo(options: InitCommandOptions) {\r\n const statePath = path.join(options.repoRoot, STATE_FILE_NAME);\r\n if (\r\n !options.force &&\r\n (await fs\r\n .access(statePath)\r\n .then(() => true)\r\n .catch(() => false))\r\n ) {\r\n throw new Error(`${STATE_FILE_NAME} already exists. Use --force to re-baseline intentionally.`);\r\n }\r\n await fs.access(path.join(options.repoRoot, \"pnpm-workspace.yaml\"));\r\n const context = await inferContext(options.repoRoot);\r\n const enabledFeatures = await inferFeatures(options.repoRoot);\r\n const templateRoot = getTemplateRoot();\r\n const manifest = loadTemplateManifest(templateRoot);\r\n const excludePrefixes = getExcludedFeaturePaths(manifest, enabledFeatures);\r\n const renderedFiles = await collectTemplateFiles(templateRoot, context, {\r\n excludePrefixes,\r\n enabledFeatures,\r\n });\r\n const version = getCliVersion();\r\n const state = createManagedState({\r\n templateVersion: version,\r\n enabledFeatures,\r\n context,\r\n renderedFiles,\r\n manifest,\r\n });\r\n const report = await diagnoseManagedRepo({ repoRoot: options.repoRoot, state });\r\n if (!report.ok) return { initialized: false, state, report };\r\n\r\n await confirmInitialization(version, options.yes);\r\n await writeManagedState(options.repoRoot, state);\r\n return { initialized: true, state, report };\r\n}\r\n"],"mappings":"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"}