create-m5kdev 0.25.2 → 0.26.1

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 (143) hide show
  1. package/dist/src/create.cjs +18 -1
  2. package/dist/src/create.cjs.map +1 -1
  3. package/dist/src/fs.cjs +65 -5
  4. package/dist/src/fs.cjs.map +1 -1
  5. package/dist/src/fs.d.cts +13 -2
  6. package/dist/src/index.cjs +0 -0
  7. package/dist/src/paths.cjs +2 -2
  8. package/dist/src/prompts.cjs +22 -0
  9. package/dist/src/prompts.cjs.map +1 -1
  10. package/dist/src/runCli.cjs +13 -2
  11. package/dist/src/runCli.cjs.map +1 -1
  12. package/dist/src/types.d.cts +9 -1
  13. package/package.json +8 -6
  14. package/templates/minimal-app/.cursor/rules/frontend-data-guide.mdc +46 -3
  15. package/templates/minimal-app/.cursor/rules/module-db-guide.mdc +15 -1
  16. package/templates/minimal-app/.cursor/rules/module-dto-guide.mdc +60 -0
  17. package/templates/minimal-app/.cursor/rules/module-repository-guide.mdc +98 -193
  18. package/templates/minimal-app/.cursor/rules/module-schema-guide.mdc +53 -173
  19. package/templates/minimal-app/.cursor/rules/module-service-guide.mdc +133 -240
  20. package/templates/minimal-app/.cursor/rules/module-trpc-guide.mdc +71 -113
  21. package/templates/minimal-app/README.md.tpl +3 -3
  22. package/templates/minimal-app/apps/.gitignore.tpl +13 -0
  23. package/templates/minimal-app/apps/e2e/package.json.tpl +23 -0
  24. package/templates/minimal-app/apps/e2e/playwright.config.ts.tpl +39 -0
  25. package/templates/minimal-app/apps/e2e/playwright.expo.config.ts.tpl +38 -0
  26. package/templates/minimal-app/apps/e2e/servers.ts.tpl +100 -0
  27. package/templates/minimal-app/apps/e2e/tests/email.spec.ts.tpl +47 -0
  28. package/templates/minimal-app/apps/e2e/tests/expo-helpers.ts.tpl +73 -0
  29. package/templates/minimal-app/apps/e2e/tests/expo-standard.spec.ts.tpl +517 -0
  30. package/templates/minimal-app/apps/e2e/tests/expo-waitlist.spec.ts.tpl +94 -0
  31. package/templates/minimal-app/apps/e2e/tests/helpers.ts.tpl +289 -0
  32. package/templates/minimal-app/apps/e2e/tests/standard.spec.ts.tpl +516 -0
  33. package/templates/minimal-app/apps/e2e/tests/waitlist.spec.ts.tpl +107 -0
  34. package/templates/minimal-app/apps/e2e/tsconfig.json.tpl +10 -0
  35. package/templates/minimal-app/apps/email/package.json.tpl +34 -34
  36. package/templates/minimal-app/apps/email/src/emails/accountDeletionEmail.tsx.tpl +35 -34
  37. package/templates/minimal-app/apps/email/src/emails/organizationInviteEmail.tsx.tpl +34 -33
  38. package/templates/minimal-app/apps/email/src/emails/passwordResetEmail.tsx.tpl +35 -34
  39. package/templates/minimal-app/apps/email/src/emails/verificationEmail.tsx.tpl +37 -36
  40. package/templates/minimal-app/apps/email/src/emails/waitlistEmails.tsx.tpl +87 -0
  41. package/templates/minimal-app/apps/email/src/index.ts.tpl +72 -32
  42. package/templates/minimal-app/apps/email/src/resources.ts.tpl +65 -36
  43. package/templates/minimal-app/apps/email/tsconfig.json.tpl +16 -11
  44. package/templates/minimal-app/apps/expo/.gitignore.tpl +44 -0
  45. package/templates/minimal-app/apps/expo/.prettierignore +13 -0
  46. package/templates/minimal-app/apps/expo/.prettierrc.json.tpl +9 -0
  47. package/templates/minimal-app/apps/expo/CHANGELOG.md.tpl +156 -0
  48. package/templates/minimal-app/apps/expo/README.md.tpl +44 -0
  49. package/templates/minimal-app/apps/expo/app.json.tpl +36 -0
  50. package/templates/minimal-app/apps/expo/assets/images/adaptive-icon.png +0 -0
  51. package/templates/minimal-app/apps/expo/assets/images/favicon.png +0 -0
  52. package/templates/minimal-app/apps/expo/assets/images/icon.png +0 -0
  53. package/templates/minimal-app/apps/expo/assets/images/splash-icon.png +0 -0
  54. package/templates/minimal-app/apps/expo/eslint.config.js.tpl +8 -0
  55. package/templates/minimal-app/apps/expo/expo-env.d.ts.tpl +3 -0
  56. package/templates/minimal-app/apps/expo/metro.config.js.tpl +16 -0
  57. package/templates/minimal-app/apps/expo/package.json.tpl +59 -0
  58. package/templates/minimal-app/apps/expo/src/app/_layout.tsx.tpl +31 -0
  59. package/templates/minimal-app/apps/expo/src/app/claim-account.tsx.tpl +151 -0
  60. package/templates/minimal-app/apps/expo/src/app/forgot-password.tsx.tpl +53 -0
  61. package/templates/minimal-app/apps/expo/src/app/index.tsx.tpl +8 -0
  62. package/templates/minimal-app/apps/expo/src/app/login.tsx.tpl +71 -0
  63. package/templates/minimal-app/apps/expo/src/app/logout.tsx.tpl +19 -0
  64. package/templates/minimal-app/apps/expo/src/app/organization/accept-invitation.tsx.tpl +85 -0
  65. package/templates/minimal-app/apps/expo/src/app/posts.tsx.tpl +40 -0
  66. package/templates/minimal-app/apps/expo/src/app/reset-password.tsx.tpl +75 -0
  67. package/templates/minimal-app/apps/expo/src/app/signup.tsx.tpl +193 -0
  68. package/templates/minimal-app/apps/expo/src/components/AuthScaffold.tsx.tpl +34 -0
  69. package/templates/minimal-app/apps/expo/src/components/FormTextField.tsx.tpl +37 -0
  70. package/templates/minimal-app/apps/expo/src/components/LoadingScreen.tsx.tpl +10 -0
  71. package/templates/minimal-app/apps/expo/src/components/StatusText.tsx.tpl +22 -0
  72. package/templates/minimal-app/apps/expo/src/config.ts.tpl +9 -0
  73. package/templates/minimal-app/apps/expo/src/global.css.tpl +11 -0
  74. package/templates/minimal-app/apps/expo/src/lib/trpc.ts.tpl +20 -0
  75. package/templates/minimal-app/apps/expo/tsconfig.json.tpl +11 -0
  76. package/templates/minimal-app/apps/expo/uniwind-env.d.ts.tpl +1 -0
  77. package/templates/minimal-app/apps/expo/uniwind-types.d.ts.tpl +10 -0
  78. package/templates/minimal-app/apps/server/AGENTS.md.tpl +39 -39
  79. package/templates/minimal-app/apps/server/drizzle/0000_wealthy_absorbing_man.sql +268 -0
  80. package/templates/minimal-app/apps/server/drizzle/db.ts.tpl +26 -0
  81. package/templates/minimal-app/apps/server/drizzle/meta/0000_snapshot.json.tpl +1890 -0
  82. package/templates/minimal-app/apps/server/drizzle/meta/_journal.json.tpl +13 -0
  83. package/templates/minimal-app/apps/server/drizzle/reset.ts.tpl +33 -0
  84. package/templates/minimal-app/apps/server/drizzle/seed.e2e.ts.tpl +244 -0
  85. package/templates/minimal-app/apps/server/drizzle/seed.ts.tpl +181 -180
  86. package/templates/minimal-app/apps/server/drizzle/sync.ts.tpl +14 -21
  87. package/templates/minimal-app/apps/server/drizzle.config.ts.tpl +33 -35
  88. package/templates/minimal-app/apps/server/package.json.tpl +56 -52
  89. package/templates/minimal-app/apps/server/src/app.ts.tpl +142 -126
  90. package/templates/minimal-app/apps/server/src/index.ts.tpl +58 -58
  91. package/templates/minimal-app/apps/server/src/lib/auth.ts.tpl +12 -12
  92. package/templates/minimal-app/apps/server/src/modules/posts/posts.db.ts.tpl +25 -25
  93. package/templates/minimal-app/apps/server/src/modules/posts/posts.dto.ts.tpl +23 -0
  94. package/templates/minimal-app/apps/server/src/modules/posts/posts.grants.ts.tpl +30 -40
  95. package/templates/minimal-app/apps/server/src/modules/posts/posts.module.ts.tpl +54 -54
  96. package/templates/minimal-app/apps/server/src/modules/posts/posts.repository.ts.tpl +67 -112
  97. package/templates/minimal-app/apps/server/src/modules/posts/posts.service.ts.tpl +104 -170
  98. package/templates/minimal-app/apps/server/src/modules/posts/posts.trpc.ts.tpl +37 -48
  99. package/templates/minimal-app/apps/server/src/modules/posts/posts.utils.ts.tpl +19 -0
  100. package/templates/minimal-app/apps/server/src/modules/test-harness/test-harness.module.ts.tpl +279 -0
  101. package/templates/minimal-app/apps/server/src/schema.ts.tpl +20 -0
  102. package/templates/minimal-app/apps/server/src/types.ts.tpl +1 -1
  103. package/templates/minimal-app/apps/server/tsconfig.json.tpl +20 -14
  104. package/templates/minimal-app/apps/server/tsup.config.ts.tpl +9 -9
  105. package/templates/minimal-app/apps/shared/package.json.tpl +24 -24
  106. package/templates/minimal-app/apps/shared/src/modules/app/app.constants.ts.tpl +28 -2
  107. package/templates/minimal-app/apps/shared/src/modules/notification/notification.schema.ts.tpl +3 -3
  108. package/templates/minimal-app/apps/shared/src/modules/posts/posts.constants.ts.tpl +5 -5
  109. package/templates/minimal-app/apps/shared/src/modules/posts/posts.schema.ts.tpl +25 -76
  110. package/templates/minimal-app/apps/shared/tsconfig.json.tpl +16 -12
  111. package/templates/minimal-app/apps/webapp/index.html.tpl +22 -22
  112. package/templates/minimal-app/apps/webapp/package.json.tpl +53 -52
  113. package/templates/minimal-app/apps/webapp/public/{push-sw.js → push-sw.js.tpl} +34 -34
  114. package/templates/minimal-app/apps/webapp/src/App.tsx.tpl +44 -13
  115. package/templates/minimal-app/apps/webapp/src/Layout.tsx.tpl +18 -12
  116. package/templates/minimal-app/apps/webapp/src/Router.tsx.tpl +33 -40
  117. package/templates/minimal-app/apps/webapp/src/index.css.tpl +15 -75
  118. package/templates/minimal-app/apps/webapp/src/main.tsx.tpl +26 -26
  119. package/templates/minimal-app/apps/webapp/src/modules/posts/PostsRoute.tsx.tpl +278 -307
  120. package/templates/minimal-app/apps/webapp/src/modules/posts/components/PostCard.tsx.tpl +96 -138
  121. package/templates/minimal-app/apps/webapp/src/modules/posts/components/PostEditorModal.tsx.tpl +118 -124
  122. package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostActions.ts.tpl +108 -116
  123. package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostsList.ts.tpl +15 -9
  124. package/templates/minimal-app/apps/webapp/src/modules/posts/hooks/usePostsRoute.ts.tpl +13 -13
  125. package/templates/minimal-app/apps/webapp/src/modules/posts/posts.utils.ts.tpl +16 -16
  126. package/templates/minimal-app/apps/webapp/src/utils/i18n.ts.tpl +13 -13
  127. package/templates/minimal-app/apps/webapp/src/utils/trpc.ts.tpl +10 -6
  128. package/templates/minimal-app/apps/webapp/src/vite-env.d.ts.tpl +1 -1
  129. package/templates/minimal-app/apps/webapp/translations/{app.json.tpl → en/app.json.tpl} +7 -7
  130. package/templates/minimal-app/apps/webapp/translations/en/{blog-app.json.tpl → {{APP_SLUG}}.json.tpl} +135 -135
  131. package/templates/minimal-app/apps/webapp/tsconfig.json.tpl +17 -17
  132. package/templates/minimal-app/apps/webapp/vite.config.ts.tpl +31 -31
  133. package/templates/minimal-app/template.manifest.json +21 -0
  134. package/templates/minimal-app/apps/server/drizzle/generate-schema.ts.tpl +0 -31
  135. package/templates/minimal-app/apps/server/src/db.ts.tpl +0 -7
  136. package/templates/minimal-app/apps/server/src/generated/schema.ts.tpl +0 -13
  137. package/templates/minimal-app/apps/server/src/utils/trpc.ts.tpl +0 -8
  138. package/templates/minimal-app/apps/shared/src/modules/app/locale.constants.ts.tpl +0 -7
  139. package/templates/minimal-app/apps/shared/src/modules/app/roles.constants.ts.tpl +0 -17
  140. package/templates/minimal-app/apps/webapp/src/Providers.tsx.tpl +0 -36
  141. package/templates/minimal-app/apps/webapp/src/modules/notification/PushNotificationsPanel.tsx.tpl +0 -89
  142. package/templates/minimal-app/apps/webapp/src/navigation/navigation.config.tsx.tpl +0 -85
  143. /package/templates/minimal-app/apps/webapp/public/{logo.svg → logo.svg.tpl} +0 -0
@@ -5,9 +5,16 @@ const require_src_strings = require("./strings.cjs");
5
5
  const require_src_fs = require("./fs.cjs");
6
6
  const require_src_paths = require("./paths.cjs");
7
7
  const require_src_prompts = require("./prompts.cjs");
8
+ let node_fs = require("node:fs");
9
+ node_fs = require_runtime.__toESM(node_fs);
8
10
  let node_path = require("node:path");
9
11
  node_path = require_runtime.__toESM(node_path);
10
12
  //#region src/create.ts
13
+ function loadFeatureManifest(templateDirectory) {
14
+ const manifestPath = node_path.default.join(templateDirectory, "template.manifest.json");
15
+ if (!node_fs.default.existsSync(manifestPath)) return { features: {} };
16
+ return JSON.parse(node_fs.default.readFileSync(manifestPath, "utf8"));
17
+ }
11
18
  async function scaffoldProject(initialOptions) {
12
19
  const options = await require_src_prompts.resolveCreateCommandOptions(initialOptions);
13
20
  const appSlug = require_src_strings.slugifyAppName(options.appName);
@@ -20,8 +27,18 @@ async function scaffoldProject(initialOptions) {
20
27
  packageScope: require_src_strings.derivePackageScope(appSlug),
21
28
  betterAuthSecret: require_src_strings.createBetterAuthSecret()
22
29
  };
30
+ const platform = options.platform ?? "web";
31
+ const enabledFeatures = /* @__PURE__ */ new Set();
32
+ if (platform !== "expo") enabledFeatures.add("webapp");
33
+ if (platform !== "web") enabledFeatures.add("expo");
34
+ if (options.testHarness) enabledFeatures.add("test-harness");
35
+ const manifest = loadFeatureManifest(templateDirectory);
36
+ const excludePrefixes = Object.entries(manifest.features).filter(([feature]) => !enabledFeatures.has(feature)).flatMap(([, config]) => config.paths);
23
37
  await require_src_fs.ensureDirectoryState(targetDirectory, options.force);
24
- await require_src_fs.copyTemplateDirectory(templateDirectory, targetDirectory, context);
38
+ await require_src_fs.copyTemplateDirectory(templateDirectory, targetDirectory, context, {
39
+ excludePrefixes,
40
+ enabledFeatures
41
+ });
25
42
  if (!options.skipGit) await require_src_fs.runGitInit(targetDirectory);
26
43
  if (!options.skipInstall) await require_src_fs.runInstall(targetDirectory);
27
44
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"create.cjs","names":["resolveCreateCommandOptions","slugifyAppName","path","getTemplateRoot","derivePackageScope","createBetterAuthSecret","ensureDirectoryState","copyTemplateDirectory","runGitInit","runInstall","TEMPLATE_NAME"],"sources":["../../src/create.ts"],"sourcesContent":["import path from \"node:path\";\r\nimport { TEMPLATE_NAME } from \"./constants\";\r\nimport { copyTemplateDirectory, ensureDirectoryState, runGitInit, runInstall } from \"./fs\";\r\nimport { getTemplateRoot } from \"./paths\";\r\nimport { resolveCreateCommandOptions } from \"./prompts\";\r\nimport { createBetterAuthSecret, derivePackageScope, slugifyAppName } from \"./strings\";\r\nimport type { CreateCommandOptions, TemplateContext } from \"./types\";\r\n\r\nexport interface ScaffoldResult {\r\n context: TemplateContext;\r\n targetDirectory: string;\r\n templateName: string;\r\n}\r\n\r\nexport async function scaffoldProject(initialOptions: CreateCommandOptions): Promise<ScaffoldResult> {\r\n const options = await resolveCreateCommandOptions(initialOptions);\r\n const appSlug = slugifyAppName(options.appName!);\r\n const targetDirectory = path.resolve(process.cwd(), options.targetDirectory!);\r\n const templateDirectory = getTemplateRoot();\r\n\r\n const context: TemplateContext = {\r\n appName: options.appName!,\r\n appDescription: options.appDescription!,\r\n appSlug,\r\n packageScope: derivePackageScope(appSlug),\r\n betterAuthSecret: createBetterAuthSecret(),\r\n };\r\n\r\n await ensureDirectoryState(targetDirectory, options.force);\r\n await copyTemplateDirectory(templateDirectory, targetDirectory, context);\r\n\r\n if (!options.skipGit) {\r\n await runGitInit(targetDirectory);\r\n }\r\n\r\n if (!options.skipInstall) {\r\n await runInstall(targetDirectory);\r\n }\r\n\r\n return {\r\n context,\r\n targetDirectory,\r\n templateName: TEMPLATE_NAME,\r\n };\r\n}\r\n"],"mappings":";;;;;;;;;;AAcA,eAAsB,gBAAgB,gBAA+D;CACnG,MAAM,UAAU,MAAMA,oBAAAA,4BAA4B,eAAe;CACjE,MAAM,UAAUC,oBAAAA,eAAe,QAAQ,QAAS;CAChD,MAAM,kBAAkBC,UAAAA,QAAK,QAAQ,QAAQ,KAAK,EAAE,QAAQ,gBAAiB;CAC7E,MAAM,oBAAoBC,kBAAAA,iBAAiB;CAE3C,MAAM,UAA2B;EAC/B,SAAS,QAAQ;EACjB,gBAAgB,QAAQ;EACxB;EACA,cAAcC,oBAAAA,mBAAmB,QAAQ;EACzC,kBAAkBC,oBAAAA,wBAAwB;EAC3C;AAED,OAAMC,eAAAA,qBAAqB,iBAAiB,QAAQ,MAAM;AAC1D,OAAMC,eAAAA,sBAAsB,mBAAmB,iBAAiB,QAAQ;AAExE,KAAI,CAAC,QAAQ,QACX,OAAMC,eAAAA,WAAW,gBAAgB;AAGnC,KAAI,CAAC,QAAQ,YACX,OAAMC,eAAAA,WAAW,gBAAgB;AAGnC,QAAO;EACL;EACA;EACA,cAAcC,sBAAAA;EACf"}
1
+ {"version":3,"file":"create.cjs","names":["path","fs","resolveCreateCommandOptions","slugifyAppName","getTemplateRoot","derivePackageScope","createBetterAuthSecret","ensureDirectoryState","copyTemplateDirectory","runGitInit","runInstall","TEMPLATE_NAME"],"sources":["../../src/create.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { TEMPLATE_NAME } from \"./constants\";\nimport { copyTemplateDirectory, ensureDirectoryState, runGitInit, runInstall } from \"./fs\";\nimport { getTemplateRoot } from \"./paths\";\nimport { resolveCreateCommandOptions } from \"./prompts\";\nimport { createBetterAuthSecret, derivePackageScope, slugifyAppName } from \"./strings\";\nimport type {\n CreateCommandOptions,\n TemplateContext,\n TemplateFeatureManifest,\n} from \"./types\";\n\nexport interface ScaffoldResult {\n context: TemplateContext;\n targetDirectory: string;\n templateName: string;\n}\n\nfunction loadFeatureManifest(templateDirectory: string): TemplateFeatureManifest {\n const manifestPath = path.join(templateDirectory, \"template.manifest.json\");\n if (!fs.existsSync(manifestPath)) {\n return { features: {} };\n }\n return JSON.parse(fs.readFileSync(manifestPath, \"utf8\")) as TemplateFeatureManifest;\n}\n\nexport async function scaffoldProject(initialOptions: CreateCommandOptions): Promise<ScaffoldResult> {\n const options = await resolveCreateCommandOptions(initialOptions);\n const appSlug = slugifyAppName(options.appName!);\n const targetDirectory = path.resolve(process.cwd(), options.targetDirectory!);\n const templateDirectory = getTemplateRoot();\n\n const context: TemplateContext = {\n appName: options.appName!,\n appDescription: options.appDescription!,\n appSlug,\n packageScope: derivePackageScope(appSlug),\n betterAuthSecret: createBetterAuthSecret(),\n };\n\n const platform = options.platform ?? \"web\";\n const enabledFeatures = new Set<string>();\n if (platform !== \"expo\") enabledFeatures.add(\"webapp\");\n if (platform !== \"web\") enabledFeatures.add(\"expo\");\n if (options.testHarness) enabledFeatures.add(\"test-harness\");\n\n const manifest = loadFeatureManifest(templateDirectory);\n const excludePrefixes = Object.entries(manifest.features)\n .filter(([feature]) => !enabledFeatures.has(feature))\n .flatMap(([, config]) => config.paths);\n\n await ensureDirectoryState(targetDirectory, options.force);\n await copyTemplateDirectory(templateDirectory, targetDirectory, context, {\n excludePrefixes,\n enabledFeatures,\n });\n\n if (!options.skipGit) {\n await runGitInit(targetDirectory);\n }\n\n if (!options.skipInstall) {\n await runInstall(targetDirectory);\n }\n\n return {\n context,\n targetDirectory,\n templateName: TEMPLATE_NAME,\n };\n}\n"],"mappings":";;;;;;;;;;;;AAmBA,SAAS,oBAAoB,mBAAoD;CAC/E,MAAM,eAAeA,UAAAA,QAAK,KAAK,mBAAmB,yBAAyB;AAC3E,KAAI,CAACC,QAAAA,QAAG,WAAW,aAAa,CAC9B,QAAO,EAAE,UAAU,EAAE,EAAE;AAEzB,QAAO,KAAK,MAAMA,QAAAA,QAAG,aAAa,cAAc,OAAO,CAAC;;AAG1D,eAAsB,gBAAgB,gBAA+D;CACnG,MAAM,UAAU,MAAMC,oBAAAA,4BAA4B,eAAe;CACjE,MAAM,UAAUC,oBAAAA,eAAe,QAAQ,QAAS;CAChD,MAAM,kBAAkBH,UAAAA,QAAK,QAAQ,QAAQ,KAAK,EAAE,QAAQ,gBAAiB;CAC7E,MAAM,oBAAoBI,kBAAAA,iBAAiB;CAE3C,MAAM,UAA2B;EAC/B,SAAS,QAAQ;EACjB,gBAAgB,QAAQ;EACxB;EACA,cAAcC,oBAAAA,mBAAmB,QAAQ;EACzC,kBAAkBC,oBAAAA,wBAAwB;EAC3C;CAED,MAAM,WAAW,QAAQ,YAAY;CACrC,MAAM,kCAAkB,IAAI,KAAa;AACzC,KAAI,aAAa,OAAQ,iBAAgB,IAAI,SAAS;AACtD,KAAI,aAAa,MAAO,iBAAgB,IAAI,OAAO;AACnD,KAAI,QAAQ,YAAa,iBAAgB,IAAI,eAAe;CAE5D,MAAM,WAAW,oBAAoB,kBAAkB;CACvD,MAAM,kBAAkB,OAAO,QAAQ,SAAS,SAAS,CACtD,QAAQ,CAAC,aAAa,CAAC,gBAAgB,IAAI,QAAQ,CAAC,CACpD,SAAS,GAAG,YAAY,OAAO,MAAM;AAExC,OAAMC,eAAAA,qBAAqB,iBAAiB,QAAQ,MAAM;AAC1D,OAAMC,eAAAA,sBAAsB,mBAAmB,iBAAiB,SAAS;EACvE;EACA;EACD,CAAC;AAEF,KAAI,CAAC,QAAQ,QACX,OAAMC,eAAAA,WAAW,gBAAgB;AAGnC,KAAI,CAAC,QAAQ,YACX,OAAMC,eAAAA,WAAW,gBAAgB;AAGnC,QAAO;EACL;EACA;EACA,cAAcC,sBAAAA;EACf"}
package/dist/src/fs.cjs CHANGED
@@ -8,7 +8,30 @@ let node_fs_promises = require("node:fs/promises");
8
8
  node_fs_promises = require_runtime.__toESM(node_fs_promises);
9
9
  //#region src/fs.ts
10
10
  const execFileAsync = (0, require("node:util").promisify)(node_child_process.execFile);
11
- const TEMPLATE_LINE_ENDING = "\r\n";
11
+ const TEXT_EXTENSIONS = new Set([
12
+ ".ts",
13
+ ".tsx",
14
+ ".js",
15
+ ".mjs",
16
+ ".cjs",
17
+ ".json",
18
+ ".md",
19
+ ".css",
20
+ ".html",
21
+ ".svg",
22
+ ".yaml",
23
+ ".yml",
24
+ ".txt",
25
+ ".mdc"
26
+ ]);
27
+ const TEXT_BASENAMES = new Set([
28
+ ".gitignore",
29
+ ".env",
30
+ ".env.example",
31
+ ".npmrc"
32
+ ]);
33
+ /** Feature marker blocks: lines between `// m5k:<feature>:start` and `// m5k:<feature>:end`. */
34
+ const MARKER_PATTERN = /^[ \t]*\/\/ m5k:([a-z-]+):(start|end)[ \t]*$/;
12
35
  async function ensureDirectoryState(targetDirectory, force) {
13
36
  const stat = await node_fs_promises.default.stat(targetDirectory).catch(() => null);
14
37
  if (!stat) {
@@ -18,20 +41,56 @@ async function ensureDirectoryState(targetDirectory, force) {
18
41
  if (!stat.isDirectory()) throw new Error(`Target path is not a directory: ${targetDirectory}`);
19
42
  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.`);
20
43
  }
21
- async function copyTemplateDirectory(templateDirectory, targetDirectory, context) {
44
+ function isTextFile(fileName) {
45
+ const withoutTpl = fileName.endsWith(".tpl") ? fileName.slice(0, -4) : fileName;
46
+ if (TEXT_BASENAMES.has(withoutTpl)) return true;
47
+ return TEXT_EXTENSIONS.has(node_path.default.extname(withoutTpl));
48
+ }
49
+ /**
50
+ * Removes feature marker lines. Blocks for disabled features are dropped with
51
+ * their content; blocks for enabled features keep the content, lose the markers.
52
+ */
53
+ function applyFeatureMarkers(content, enabledFeatures) {
54
+ const lines = content.split("\n");
55
+ const out = [];
56
+ let skippingFeature;
57
+ for (const line of lines) {
58
+ const match = line.match(MARKER_PATTERN);
59
+ if (!match) {
60
+ if (skippingFeature === void 0) out.push(line);
61
+ continue;
62
+ }
63
+ const [, feature, edge] = match;
64
+ if (edge === "start") {
65
+ if (!enabledFeatures.has(feature)) skippingFeature = feature;
66
+ } else if (skippingFeature === feature) skippingFeature = void 0;
67
+ }
68
+ return out.join("\n");
69
+ }
70
+ async function copyTemplateDirectory(templateDirectory, targetDirectory, context, options = {}, relativeBase = "") {
22
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();
23
74
  for (const entry of entries) {
24
75
  const sourcePath = node_path.default.join(templateDirectory, entry.name);
25
- const targetName = entry.name.endsWith(".tpl") ? entry.name.slice(0, -4) : 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;
26
81
  const targetPath = node_path.default.join(targetDirectory, targetName);
27
82
  if (entry.isDirectory()) {
28
83
  await node_fs_promises.default.mkdir(targetPath, { recursive: true });
29
- await copyTemplateDirectory(sourcePath, targetPath, context);
84
+ await copyTemplateDirectory(sourcePath, targetPath, context, options, relativePath);
30
85
  continue;
31
86
  }
32
87
  if (!entry.isFile()) continue;
33
- const rendered = require_src_strings.renderTemplate(await node_fs_promises.default.readFile(sourcePath, "utf8"), context).replace(/\r?\n/g, TEMPLATE_LINE_ENDING);
34
88
  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");
35
94
  await node_fs_promises.default.writeFile(targetPath, rendered, "utf8");
36
95
  }
37
96
  }
@@ -48,6 +107,7 @@ async function runGitInit(targetDirectory) {
48
107
  });
49
108
  }
50
109
  //#endregion
110
+ exports.applyFeatureMarkers = applyFeatureMarkers;
51
111
  exports.copyTemplateDirectory = copyTemplateDirectory;
52
112
  exports.ensureDirectoryState = ensureDirectoryState;
53
113
  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\";\r\nimport fs from \"node:fs/promises\";\r\nimport path from \"node:path\";\r\nimport { promisify } from \"node:util\";\r\nimport type { TemplateContext } from \"./types\";\r\nimport { renderTemplate } from \"./strings\";\r\n\r\nconst execFileAsync = promisify(execFile);\r\nconst TEMPLATE_LINE_ENDING = \"\\r\\n\";\r\n\r\nexport async function ensureDirectoryState(\r\n targetDirectory: string,\r\n force: boolean\r\n): Promise<void> {\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;\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\r\nexport async function copyTemplateDirectory(\r\n templateDirectory: string,\r\n targetDirectory: string,\r\n context: TemplateContext\r\n): Promise<void> {\r\n const entries = await fs.readdir(templateDirectory, { withFileTypes: true });\r\n\r\n for (const entry of entries) {\r\n const sourcePath = path.join(templateDirectory, entry.name);\r\n const targetName = entry.name.endsWith(\".tpl\") ? entry.name.slice(0, -4) : entry.name;\r\n const targetPath = path.join(targetDirectory, targetName);\r\n\r\n if (entry.isDirectory()) {\r\n await fs.mkdir(targetPath, { recursive: true });\r\n await copyTemplateDirectory(sourcePath, targetPath, context);\r\n continue;\r\n }\r\n\r\n if (!entry.isFile()) {\r\n continue;\r\n }\r\n\r\n const content = await fs.readFile(sourcePath, \"utf8\");\r\n const rendered = renderTemplate(content, context).replace(/\\r?\\n/g, TEMPLATE_LINE_ENDING);\r\n await fs.mkdir(path.dirname(targetPath), { recursive: true });\r\n await fs.writeFile(targetPath, rendered, \"utf8\");\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":";;;;;;;;;AAOA,MAAM,iBAAA,wBAAA,WAA0BA,mBAAAA,SAAS;AACzC,MAAM,uBAAuB;AAE7B,eAAsB,qBACpB,iBACA,OACe;CACf,MAAM,OAAO,MAAMC,iBAAAA,QAAG,KAAK,gBAAgB,CAAC,YAAY,KAAK;AAE7D,KAAI,CAAC,MAAM;AACT,QAAMA,iBAAAA,QAAG,MAAM,iBAAiB,EAAE,WAAW,MAAM,CAAC;AACpD;;AAGF,KAAI,CAAC,KAAK,aAAa,CACrB,OAAM,IAAI,MAAM,mCAAmC,kBAAkB;AAIvE,MADgB,MAAMA,iBAAAA,QAAG,QAAQ,gBAAgB,EACrC,SAAS,KAAK,CAAC,MACzB,OAAM,IAAI,MACR,kCAAkC,gBAAgB,qCACnD;;AAIL,eAAsB,sBACpB,mBACA,iBACA,SACe;CACf,MAAM,UAAU,MAAMA,iBAAAA,QAAG,QAAQ,mBAAmB,EAAE,eAAe,MAAM,CAAC;AAE5E,MAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,aAAaC,UAAAA,QAAK,KAAK,mBAAmB,MAAM,KAAK;EAC3D,MAAM,aAAa,MAAM,KAAK,SAAS,OAAO,GAAG,MAAM,KAAK,MAAM,GAAG,GAAG,GAAG,MAAM;EACjF,MAAM,aAAaA,UAAAA,QAAK,KAAK,iBAAiB,WAAW;AAEzD,MAAI,MAAM,aAAa,EAAE;AACvB,SAAMD,iBAAAA,QAAG,MAAM,YAAY,EAAE,WAAW,MAAM,CAAC;AAC/C,SAAM,sBAAsB,YAAY,YAAY,QAAQ;AAC5D;;AAGF,MAAI,CAAC,MAAM,QAAQ,CACjB;EAIF,MAAM,WAAWE,oBAAAA,eADD,MAAMF,iBAAAA,QAAG,SAAS,YAAY,OAAO,EACZ,QAAQ,CAAC,QAAQ,UAAU,qBAAqB;AACzF,QAAMA,iBAAAA,QAAG,MAAMC,UAAAA,QAAK,QAAQ,WAAW,EAAE,EAAE,WAAW,MAAM,CAAC;AAC7D,QAAMD,iBAAAA,QAAG,UAAU,YAAY,UAAU,OAAO;;;AAIpD,eAAsB,WAAW,iBAAwC;AACvE,OAAM,cAAc,QAAQ,CAAC,UAAU,EAAE;EACvC,KAAK;EACL,KAAK,QAAQ;EACd,CAAC;;AAGJ,eAAsB,WAAW,iBAAwC;AACvE,OAAM,cAAc,OAAO,CAAC,OAAO,EAAE;EACnC,KAAK;EACL,KAAK,QAAQ;EACd,CAAC"}
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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"}
package/dist/src/fs.d.cts CHANGED
@@ -1,10 +1,21 @@
1
1
  import { TemplateContext } from "./types.cjs";
2
2
 
3
3
  //#region src/fs.d.ts
4
+ interface CopyTemplateOptions {
5
+ /** Repo-root-relative path prefixes to skip entirely (disabled features). */
6
+ excludePrefixes?: readonly string[];
7
+ /** Features whose marker blocks should be kept (markers removed, content kept). */
8
+ enabledFeatures?: ReadonlySet<string>;
9
+ }
4
10
  declare function ensureDirectoryState(targetDirectory: string, force: boolean): Promise<void>;
5
- declare function copyTemplateDirectory(templateDirectory: string, targetDirectory: string, context: TemplateContext): Promise<void>;
11
+ /**
12
+ * Removes feature marker lines. Blocks for disabled features are dropped with
13
+ * their content; blocks for enabled features keep the content, lose the markers.
14
+ */
15
+ 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>;
6
17
  declare function runInstall(targetDirectory: string): Promise<void>;
7
18
  declare function runGitInit(targetDirectory: string): Promise<void>;
8
19
  //#endregion
9
- export { copyTemplateDirectory, ensureDirectoryState, runGitInit, runInstall };
20
+ export { CopyTemplateOptions, applyFeatureMarkers, copyTemplateDirectory, ensureDirectoryState, runGitInit, runInstall };
10
21
  //# sourceMappingURL=fs.d.cts.map
File without changes
@@ -1,10 +1,10 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
3
3
  const require_src_constants = require("./constants.cjs");
4
- let node_path = require("node:path");
5
- node_path = require_runtime.__toESM(node_path);
6
4
  let node_fs = require("node:fs");
7
5
  node_fs = require_runtime.__toESM(node_fs);
6
+ let node_path = require("node:path");
7
+ node_path = require_runtime.__toESM(node_path);
8
8
  //#region src/paths.ts
9
9
  function getTemplateRoot() {
10
10
  const sourcePath = node_path.default.resolve(__dirname, "../templates", require_src_constants.TEMPLATE_NAME);
@@ -21,6 +21,10 @@ async function promptValue(question, fallback) {
21
21
  rl.close();
22
22
  }
23
23
  }
24
+ function parsePlatform(value) {
25
+ const normalized = value.trim().toLowerCase();
26
+ if (normalized === "web" || normalized === "expo" || normalized === "both") return normalized;
27
+ }
24
28
  async function resolveCreateCommandOptions(options) {
25
29
  const resolved = { ...options };
26
30
  if (!resolved.appName) {
@@ -37,6 +41,24 @@ async function resolveCreateCommandOptions(options) {
37
41
  const fallback = require_src_constants.getDefaultDescription(resolved.appName);
38
42
  resolved.appDescription = resolved.yes ? fallback : await promptValue("App description", fallback);
39
43
  }
44
+ if (!resolved.platform) {
45
+ requireInteractive(resolved.yes);
46
+ if (resolved.yes) resolved.platform = "web";
47
+ else {
48
+ const answer = await promptValue("App platform — web, expo, or both", "web");
49
+ const platform = parsePlatform(answer);
50
+ if (!platform) throw new Error(`Invalid platform "${answer}". Use web, expo, or both.`);
51
+ resolved.platform = platform;
52
+ }
53
+ }
54
+ if (resolved.testHarness === void 0) {
55
+ requireInteractive(resolved.yes);
56
+ if (resolved.yes) resolved.testHarness = false;
57
+ else {
58
+ const answer = await promptValue("Include the e2e test harness? y/N", "n");
59
+ resolved.testHarness = /^y(es)?$/i.test(answer.trim());
60
+ }
61
+ }
40
62
  return resolved;
41
63
  }
42
64
  function pathBaseName(value) {
@@ -1 +1 @@
1
- {"version":3,"file":"prompts.cjs","names":["readline","toDisplayName","DEFAULT_APP_NAME","slugifyAppName","getDefaultDescription"],"sources":["../../src/prompts.ts"],"sourcesContent":["import readline from \"node:readline/promises\";\r\nimport { stdin as input, stdout as output } from \"node:process\";\r\nimport { DEFAULT_APP_NAME, getDefaultDescription } from \"./constants\";\r\nimport { slugifyAppName, toDisplayName } from \"./strings\";\r\nimport type { CreateCommandOptions } from \"./types\";\r\n\r\nfunction requireInteractive(yes: boolean): void {\r\n if (!yes && !process.stdin.isTTY) {\r\n throw new Error(\"Missing required values in a non-interactive shell. Pass --yes or provide flags.\");\r\n }\r\n}\r\n\r\nasync function promptValue(question: string, fallback?: string): Promise<string> {\r\n const rl = readline.createInterface({ input, output });\r\n\r\n try {\r\n const suffix = fallback ? ` (${fallback})` : \"\";\r\n const response = await rl.question(`${question}${suffix}: `);\r\n return response.trim() || fallback || \"\";\r\n } finally {\r\n rl.close();\r\n }\r\n}\r\n\r\nexport async function resolveCreateCommandOptions(\r\n options: CreateCommandOptions\r\n): Promise<CreateCommandOptions> {\r\n const resolved = { ...options };\r\n\r\n if (!resolved.appName) {\r\n requireInteractive(resolved.yes);\r\n resolved.appName = resolved.yes\r\n ? resolved.targetDirectory\r\n ? toDisplayName(pathBaseName(resolved.targetDirectory))\r\n : DEFAULT_APP_NAME\r\n : await promptValue(\"App name\", resolved.targetDirectory ? pathBaseName(resolved.targetDirectory) : DEFAULT_APP_NAME);\r\n }\r\n\r\n resolved.appName = toDisplayName(resolved.appName);\r\n\r\n if (!resolved.targetDirectory) {\r\n requireInteractive(resolved.yes);\r\n resolved.targetDirectory = resolved.yes\r\n ? slugifyAppName(resolved.appName)\r\n : await promptValue(\"Target directory\", slugifyAppName(resolved.appName));\r\n }\r\n\r\n if (!resolved.appDescription) {\r\n requireInteractive(resolved.yes);\r\n const fallback = getDefaultDescription(resolved.appName);\r\n resolved.appDescription = resolved.yes\r\n ? fallback\r\n : await promptValue(\"App description\", fallback);\r\n }\r\n\r\n return resolved;\r\n}\r\n\r\nfunction pathBaseName(value: string): string {\r\n const normalized = value.replace(/[\\\\/]+$/g, \"\");\r\n const segments = normalized.split(/[\\\\/]/).filter(Boolean);\r\n return segments.at(-1) || DEFAULT_APP_NAME;\r\n}\r\n"],"mappings":";;;;;;;;AAMA,SAAS,mBAAmB,KAAoB;AAC9C,KAAI,CAAC,OAAO,CAAC,QAAQ,MAAM,MACzB,OAAM,IAAI,MAAM,mFAAmF;;AAIvG,eAAe,YAAY,UAAkB,UAAoC;CAC/E,MAAM,KAAKA,uBAAAA,QAAS,gBAAgB;EAAE,OAAA,aAAA;EAAO,QAAA,aAAA;EAAQ,CAAC;AAEtD,KAAI;EACF,MAAM,SAAS,WAAW,KAAK,SAAS,KAAK;AAE7C,UADiB,MAAM,GAAG,SAAS,GAAG,WAAW,OAAO,IAAI,EAC5C,MAAM,IAAI,YAAY;WAC9B;AACR,KAAG,OAAO;;;AAId,eAAsB,4BACpB,SAC+B;CAC/B,MAAM,WAAW,EAAE,GAAG,SAAS;AAE/B,KAAI,CAAC,SAAS,SAAS;AACrB,qBAAmB,SAAS,IAAI;AAChC,WAAS,UAAU,SAAS,MACxB,SAAS,kBACPC,oBAAAA,cAAc,aAAa,SAAS,gBAAgB,CAAC,GACrDC,sBAAAA,mBACF,MAAM,YAAY,YAAY,SAAS,kBAAkB,aAAa,SAAS,gBAAgB,GAAGA,sBAAAA,iBAAiB;;AAGzH,UAAS,UAAUD,oBAAAA,cAAc,SAAS,QAAQ;AAElD,KAAI,CAAC,SAAS,iBAAiB;AAC7B,qBAAmB,SAAS,IAAI;AAChC,WAAS,kBAAkB,SAAS,MAChCE,oBAAAA,eAAe,SAAS,QAAQ,GAChC,MAAM,YAAY,oBAAoBA,oBAAAA,eAAe,SAAS,QAAQ,CAAC;;AAG7E,KAAI,CAAC,SAAS,gBAAgB;AAC5B,qBAAmB,SAAS,IAAI;EAChC,MAAM,WAAWC,sBAAAA,sBAAsB,SAAS,QAAQ;AACxD,WAAS,iBAAiB,SAAS,MAC/B,WACA,MAAM,YAAY,mBAAmB,SAAS;;AAGpD,QAAO;;AAGT,SAAS,aAAa,OAAuB;AAG3C,QAFmB,MAAM,QAAQ,YAAY,GAAG,CACpB,MAAM,QAAQ,CAAC,OAAO,QAAQ,CAC1C,GAAG,GAAG,IAAA"}
1
+ {"version":3,"file":"prompts.cjs","names":["readline","toDisplayName","DEFAULT_APP_NAME","slugifyAppName","getDefaultDescription"],"sources":["../../src/prompts.ts"],"sourcesContent":["import readline from \"node:readline/promises\";\nimport { stdin as input, stdout as output } from \"node:process\";\nimport { DEFAULT_APP_NAME, getDefaultDescription } from \"./constants\";\nimport { slugifyAppName, toDisplayName } from \"./strings\";\nimport type { AppPlatform, CreateCommandOptions } from \"./types\";\n\nfunction requireInteractive(yes: boolean): void {\n if (!yes && !process.stdin.isTTY) {\n throw new Error(\"Missing required values in a non-interactive shell. Pass --yes or provide flags.\");\n }\n}\n\nasync function promptValue(question: string, fallback?: string): Promise<string> {\n const rl = readline.createInterface({ input, output });\n\n try {\n const suffix = fallback ? ` (${fallback})` : \"\";\n const response = await rl.question(`${question}${suffix}: `);\n return response.trim() || fallback || \"\";\n } finally {\n rl.close();\n }\n}\n\nfunction parsePlatform(value: string): AppPlatform | undefined {\n const normalized = value.trim().toLowerCase();\n if (normalized === \"web\" || normalized === \"expo\" || normalized === \"both\") {\n return normalized;\n }\n return undefined;\n}\n\nexport async function resolveCreateCommandOptions(\n options: CreateCommandOptions\n): Promise<CreateCommandOptions> {\n const resolved = { ...options };\n\n if (!resolved.appName) {\n requireInteractive(resolved.yes);\n resolved.appName = resolved.yes\n ? resolved.targetDirectory\n ? toDisplayName(pathBaseName(resolved.targetDirectory))\n : DEFAULT_APP_NAME\n : await promptValue(\"App name\", resolved.targetDirectory ? pathBaseName(resolved.targetDirectory) : DEFAULT_APP_NAME);\n }\n\n resolved.appName = toDisplayName(resolved.appName);\n\n if (!resolved.targetDirectory) {\n requireInteractive(resolved.yes);\n resolved.targetDirectory = resolved.yes\n ? slugifyAppName(resolved.appName)\n : await promptValue(\"Target directory\", slugifyAppName(resolved.appName));\n }\n\n if (!resolved.appDescription) {\n requireInteractive(resolved.yes);\n const fallback = getDefaultDescription(resolved.appName);\n resolved.appDescription = resolved.yes\n ? fallback\n : await promptValue(\"App description\", fallback);\n }\n\n if (!resolved.platform) {\n requireInteractive(resolved.yes);\n if (resolved.yes) {\n resolved.platform = \"web\";\n } else {\n const answer = await promptValue(\"App platform — web, expo, or both\", \"web\");\n const platform = parsePlatform(answer);\n if (!platform) {\n throw new Error(`Invalid platform \"${answer}\". Use web, expo, or both.`);\n }\n resolved.platform = platform;\n }\n }\n\n if (resolved.testHarness === undefined) {\n requireInteractive(resolved.yes);\n if (resolved.yes) {\n resolved.testHarness = false;\n } else {\n const answer = await promptValue(\"Include the e2e test harness? y/N\", \"n\");\n resolved.testHarness = /^y(es)?$/i.test(answer.trim());\n }\n }\n\n return resolved;\n}\n\nfunction pathBaseName(value: string): string {\n const normalized = value.replace(/[\\\\/]+$/g, \"\");\n const segments = normalized.split(/[\\\\/]/).filter(Boolean);\n return segments.at(-1) || DEFAULT_APP_NAME;\n}\n"],"mappings":";;;;;;;;AAMA,SAAS,mBAAmB,KAAoB;AAC9C,KAAI,CAAC,OAAO,CAAC,QAAQ,MAAM,MACzB,OAAM,IAAI,MAAM,mFAAmF;;AAIvG,eAAe,YAAY,UAAkB,UAAoC;CAC/E,MAAM,KAAKA,uBAAAA,QAAS,gBAAgB;EAAE,OAAA,aAAA;EAAO,QAAA,aAAA;EAAQ,CAAC;AAEtD,KAAI;EACF,MAAM,SAAS,WAAW,KAAK,SAAS,KAAK;AAE7C,UADiB,MAAM,GAAG,SAAS,GAAG,WAAW,OAAO,IAAI,EAC5C,MAAM,IAAI,YAAY;WAC9B;AACR,KAAG,OAAO;;;AAId,SAAS,cAAc,OAAwC;CAC7D,MAAM,aAAa,MAAM,MAAM,CAAC,aAAa;AAC7C,KAAI,eAAe,SAAS,eAAe,UAAU,eAAe,OAClE,QAAO;;AAKX,eAAsB,4BACpB,SAC+B;CAC/B,MAAM,WAAW,EAAE,GAAG,SAAS;AAE/B,KAAI,CAAC,SAAS,SAAS;AACrB,qBAAmB,SAAS,IAAI;AAChC,WAAS,UAAU,SAAS,MACxB,SAAS,kBACPC,oBAAAA,cAAc,aAAa,SAAS,gBAAgB,CAAC,GACrDC,sBAAAA,mBACF,MAAM,YAAY,YAAY,SAAS,kBAAkB,aAAa,SAAS,gBAAgB,GAAGA,sBAAAA,iBAAiB;;AAGzH,UAAS,UAAUD,oBAAAA,cAAc,SAAS,QAAQ;AAElD,KAAI,CAAC,SAAS,iBAAiB;AAC7B,qBAAmB,SAAS,IAAI;AAChC,WAAS,kBAAkB,SAAS,MAChCE,oBAAAA,eAAe,SAAS,QAAQ,GAChC,MAAM,YAAY,oBAAoBA,oBAAAA,eAAe,SAAS,QAAQ,CAAC;;AAG7E,KAAI,CAAC,SAAS,gBAAgB;AAC5B,qBAAmB,SAAS,IAAI;EAChC,MAAM,WAAWC,sBAAAA,sBAAsB,SAAS,QAAQ;AACxD,WAAS,iBAAiB,SAAS,MAC/B,WACA,MAAM,YAAY,mBAAmB,SAAS;;AAGpD,KAAI,CAAC,SAAS,UAAU;AACtB,qBAAmB,SAAS,IAAI;AAChC,MAAI,SAAS,IACX,UAAS,WAAW;OACf;GACL,MAAM,SAAS,MAAM,YAAY,qCAAqC,MAAM;GAC5E,MAAM,WAAW,cAAc,OAAO;AACtC,OAAI,CAAC,SACH,OAAM,IAAI,MAAM,qBAAqB,OAAO,4BAA4B;AAE1E,YAAS,WAAW;;;AAIxB,KAAI,SAAS,gBAAgB,KAAA,GAAW;AACtC,qBAAmB,SAAS,IAAI;AAChC,MAAI,SAAS,IACX,UAAS,cAAc;OAClB;GACL,MAAM,SAAS,MAAM,YAAY,qCAAqC,IAAI;AAC1E,YAAS,cAAc,YAAY,KAAK,OAAO,MAAM,CAAC;;;AAI1D,QAAO;;AAGT,SAAS,aAAa,OAAuB;AAG3C,QAFmB,MAAM,QAAQ,YAAY,GAAG,CACpB,MAAM,QAAQ,CAAC,OAAO,QAAQ,CAC1C,GAAG,GAAG,IAAA"}
@@ -16,7 +16,7 @@ async function runCli(argv) {
16
16
  console.log("Next steps:");
17
17
  console.log(` cd ${result.targetDirectory}`);
18
18
  if (options.skipInstall) console.log(" pnpm install");
19
- console.log(" pnpm --filter ./apps/server sync");
19
+ console.log(" pnpm --filter ./apps/server drizzle:migrate");
20
20
  console.log(" pnpm dev");
21
21
  }
22
22
  function parseCli(argv) {
@@ -62,14 +62,23 @@ function isBooleanFlag(value) {
62
62
  "yes",
63
63
  "force",
64
64
  "skip-install",
65
- "skip-git"
65
+ "skip-git",
66
+ "with-test-harness"
66
67
  ].includes(value);
67
68
  }
68
69
  function toCreateCommandOptions(parsed) {
70
+ const platform = getStringOption(parsed.options, "platform");
71
+ if (platform && ![
72
+ "web",
73
+ "expo",
74
+ "both"
75
+ ].includes(platform)) throw new Error(`Invalid --platform "${platform}". Use web, expo, or both.`);
69
76
  return {
70
77
  targetDirectory: parsed.directory,
71
78
  appName: getStringOption(parsed.options, "name"),
72
79
  appDescription: getStringOption(parsed.options, "description"),
80
+ platform,
81
+ testHarness: parsed.options["with-test-harness"] ? true : void 0,
73
82
  yes: Boolean(parsed.options.yes),
74
83
  force: Boolean(parsed.options.force),
75
84
  skipInstall: Boolean(parsed.options["skip-install"]),
@@ -91,6 +100,8 @@ function printHelp() {
91
100
  console.log("Options:");
92
101
  console.log(" --name <value> Set the app name");
93
102
  console.log(" --description <value> Set the app description");
103
+ console.log(" --platform <value> App platform: web (default), expo, or both");
104
+ console.log(" --with-test-harness Include the e2e test harness (default: no)");
94
105
  console.log(" --yes Accept defaults for missing prompts");
95
106
  console.log(" --force Allow writing into a non-empty directory");
96
107
  console.log(" --skip-install Skip pnpm install");
@@ -1 +1 @@
1
- {"version":3,"file":"runCli.cjs","names":["scaffoldProject"],"sources":["../../src/runCli.ts"],"sourcesContent":["import { scaffoldProject } from \"./create\";\r\nimport type { CreateCommandOptions } from \"./types\";\r\n\r\ninterface ParsedCli {\r\n command?: string;\r\n directory?: string;\r\n help?: boolean;\r\n options: Record<string, string | boolean>;\r\n}\r\n\r\nexport async function runCli(argv: readonly string[]): Promise<void> {\r\n const parsed = parseCli(argv);\r\n\r\n if (\r\n !parsed.command ||\r\n parsed.help ||\r\n parsed.command === \"--help\" ||\r\n parsed.command === \"-h\" ||\r\n parsed.command === \"help\"\r\n ) {\r\n printHelp();\r\n return;\r\n }\r\n\r\n if (parsed.command !== \"create\") {\r\n throw new Error(`Unknown command: ${parsed.command}`);\r\n }\r\n\r\n const options = toCreateCommandOptions(parsed);\r\n const result = await scaffoldProject(options);\r\n\r\n console.log(\"\");\r\n console.log(`Scaffolded ${result.context.appName} in ${result.targetDirectory}`);\r\n console.log(\"\");\r\n console.log(\"Next steps:\");\r\n console.log(` cd ${result.targetDirectory}`);\r\n if (options.skipInstall) {\r\n console.log(\" pnpm install\");\r\n }\r\n console.log(\" pnpm --filter ./apps/server sync\");\r\n console.log(\" pnpm dev\");\r\n}\r\n\r\nexport function parseCli(argv: readonly string[]): ParsedCli {\r\n const normalizedArgv = normalizeCliArgv(argv);\r\n const [command, ...rest] = normalizedArgv;\r\n const options: Record<string, string | boolean> = {};\r\n let directory: string | undefined;\r\n let help = false;\r\n\r\n for (let index = 0; index < rest.length; index += 1) {\r\n const token = rest[index];\r\n\r\n if (token === \"--help\" || token === \"-h\") {\r\n help = true;\r\n continue;\r\n }\r\n\r\n if (!token.startsWith(\"--\")) {\r\n directory ??= token;\r\n continue;\r\n }\r\n\r\n const [rawKey, inlineValue] = token.slice(2).split(\"=\", 2);\r\n\r\n if (isBooleanFlag(rawKey)) {\r\n options[rawKey] = true;\r\n continue;\r\n }\r\n\r\n const nextValue = inlineValue ?? rest[index + 1];\r\n if (!nextValue || nextValue.startsWith(\"--\")) {\r\n throw new Error(`Missing value for --${rawKey}`);\r\n }\r\n\r\n options[rawKey] = nextValue;\r\n if (inlineValue === undefined) {\r\n index += 1;\r\n }\r\n }\r\n\r\n return { command, directory, help, options };\r\n}\r\n\r\nfunction normalizeCliArgv(argv: readonly string[]): readonly string[] {\r\n const [firstToken] = argv;\r\n\r\n if (!firstToken || firstToken === \"create\" || firstToken === \"--help\" || firstToken === \"-h\") {\r\n return argv;\r\n }\r\n\r\n if (firstToken === \"help\") {\r\n return [\"--help\"];\r\n }\r\n\r\n return [\"create\", ...argv];\r\n}\r\n\r\nfunction isBooleanFlag(value: string): boolean {\r\n return [\"yes\", \"force\", \"skip-install\", \"skip-git\"].includes(value);\r\n}\r\n\r\nfunction toCreateCommandOptions(parsed: ParsedCli): CreateCommandOptions {\r\n return {\r\n targetDirectory: parsed.directory,\r\n appName: getStringOption(parsed.options, \"name\"),\r\n appDescription: getStringOption(parsed.options, \"description\"),\r\n yes: Boolean(parsed.options.yes),\r\n force: Boolean(parsed.options.force),\r\n skipInstall: Boolean(parsed.options[\"skip-install\"]),\r\n skipGit: Boolean(parsed.options[\"skip-git\"]),\r\n };\r\n}\r\n\r\nfunction getStringOption(\r\n options: Record<string, string | boolean>,\r\n key: string\r\n): string | undefined {\r\n const value = options[key];\r\n return typeof value === \"string\" ? value : undefined;\r\n}\r\n\r\nfunction printHelp(): void {\r\n console.log(\"m5kdev\");\r\n console.log(\"\");\r\n console.log(\"Usage:\");\r\n console.log(\" pnpm create m5kdev [directory] [options]\");\r\n console.log(\" m5kdev [directory] [options]\");\r\n console.log(\" m5kdev create [directory] [options]\");\r\n console.log(\"\");\r\n console.log(\"Options:\");\r\n console.log(\" --name <value> Set the app name\");\r\n console.log(\" --description <value> Set the app description\");\r\n console.log(\" --yes Accept defaults for missing prompts\");\r\n console.log(\" --force Allow writing into a non-empty directory\");\r\n console.log(\" --skip-install Skip pnpm install\");\r\n console.log(\" --skip-git Skip git init\");\r\n}\r\n"],"mappings":";;;AAUA,eAAsB,OAAO,MAAwC;CACnE,MAAM,SAAS,SAAS,KAAK;AAE7B,KACE,CAAC,OAAO,WACR,OAAO,QACP,OAAO,YAAY,YACnB,OAAO,YAAY,QACnB,OAAO,YAAY,QACnB;AACA,aAAW;AACX;;AAGF,KAAI,OAAO,YAAY,SACrB,OAAM,IAAI,MAAM,oBAAoB,OAAO,UAAU;CAGvD,MAAM,UAAU,uBAAuB,OAAO;CAC9C,MAAM,SAAS,MAAMA,mBAAAA,gBAAgB,QAAQ;AAE7C,SAAQ,IAAI,GAAG;AACf,SAAQ,IAAI,cAAc,OAAO,QAAQ,QAAQ,MAAM,OAAO,kBAAkB;AAChF,SAAQ,IAAI,GAAG;AACf,SAAQ,IAAI,cAAc;AAC1B,SAAQ,IAAI,QAAQ,OAAO,kBAAkB;AAC7C,KAAI,QAAQ,YACV,SAAQ,IAAI,iBAAiB;AAE/B,SAAQ,IAAI,qCAAqC;AACjD,SAAQ,IAAI,aAAa;;AAG3B,SAAgB,SAAS,MAAoC;CAE3D,MAAM,CAAC,SAAS,GAAG,QADI,iBAAiB,KAAK;CAE7C,MAAM,UAA4C,EAAE;CACpD,IAAI;CACJ,IAAI,OAAO;AAEX,MAAK,IAAI,QAAQ,GAAG,QAAQ,KAAK,QAAQ,SAAS,GAAG;EACnD,MAAM,QAAQ,KAAK;AAEnB,MAAI,UAAU,YAAY,UAAU,MAAM;AACxC,UAAO;AACP;;AAGF,MAAI,CAAC,MAAM,WAAW,KAAK,EAAE;AAC3B,iBAAc;AACd;;EAGF,MAAM,CAAC,QAAQ,eAAe,MAAM,MAAM,EAAE,CAAC,MAAM,KAAK,EAAE;AAE1D,MAAI,cAAc,OAAO,EAAE;AACzB,WAAQ,UAAU;AAClB;;EAGF,MAAM,YAAY,eAAe,KAAK,QAAQ;AAC9C,MAAI,CAAC,aAAa,UAAU,WAAW,KAAK,CAC1C,OAAM,IAAI,MAAM,uBAAuB,SAAS;AAGlD,UAAQ,UAAU;AAClB,MAAI,gBAAgB,KAAA,EAClB,UAAS;;AAIb,QAAO;EAAE;EAAS;EAAW;EAAM;EAAS;;AAG9C,SAAS,iBAAiB,MAA4C;CACpE,MAAM,CAAC,cAAc;AAErB,KAAI,CAAC,cAAc,eAAe,YAAY,eAAe,YAAY,eAAe,KACtF,QAAO;AAGT,KAAI,eAAe,OACjB,QAAO,CAAC,SAAS;AAGnB,QAAO,CAAC,UAAU,GAAG,KAAK;;AAG5B,SAAS,cAAc,OAAwB;AAC7C,QAAO;EAAC;EAAO;EAAS;EAAgB;EAAW,CAAC,SAAS,MAAM;;AAGrE,SAAS,uBAAuB,QAAyC;AACvE,QAAO;EACL,iBAAiB,OAAO;EACxB,SAAS,gBAAgB,OAAO,SAAS,OAAO;EAChD,gBAAgB,gBAAgB,OAAO,SAAS,cAAc;EAC9D,KAAK,QAAQ,OAAO,QAAQ,IAAI;EAChC,OAAO,QAAQ,OAAO,QAAQ,MAAM;EACpC,aAAa,QAAQ,OAAO,QAAQ,gBAAgB;EACpD,SAAS,QAAQ,OAAO,QAAQ,YAAY;EAC7C;;AAGH,SAAS,gBACP,SACA,KACoB;CACpB,MAAM,QAAQ,QAAQ;AACtB,QAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;;AAG7C,SAAS,YAAkB;AACzB,SAAQ,IAAI,SAAS;AACrB,SAAQ,IAAI,GAAG;AACf,SAAQ,IAAI,SAAS;AACrB,SAAQ,IAAI,6CAA6C;AACzD,SAAQ,IAAI,iCAAiC;AAC7C,SAAQ,IAAI,wCAAwC;AACpD,SAAQ,IAAI,GAAG;AACf,SAAQ,IAAI,WAAW;AACvB,SAAQ,IAAI,8CAA8C;AAC1D,SAAQ,IAAI,qDAAqD;AACjE,SAAQ,IAAI,iEAAiE;AAC7E,SAAQ,IAAI,sEAAsE;AAClF,SAAQ,IAAI,+CAA+C;AAC3D,SAAQ,IAAI,2CAA2C"}
1
+ {"version":3,"file":"runCli.cjs","names":["scaffoldProject"],"sources":["../../src/runCli.ts"],"sourcesContent":["import { scaffoldProject } from \"./create\";\r\nimport type { CreateCommandOptions } from \"./types\";\r\n\r\ninterface ParsedCli {\r\n command?: string;\r\n directory?: string;\r\n help?: boolean;\r\n options: Record<string, string | boolean>;\r\n}\r\n\r\nexport async function runCli(argv: readonly string[]): Promise<void> {\r\n const parsed = parseCli(argv);\r\n\r\n if (\r\n !parsed.command ||\r\n parsed.help ||\r\n parsed.command === \"--help\" ||\r\n parsed.command === \"-h\" ||\r\n parsed.command === \"help\"\r\n ) {\r\n printHelp();\r\n return;\r\n }\r\n\r\n if (parsed.command !== \"create\") {\r\n throw new Error(`Unknown command: ${parsed.command}`);\r\n }\r\n\r\n const options = toCreateCommandOptions(parsed);\r\n const result = await scaffoldProject(options);\r\n\r\n console.log(\"\");\r\n console.log(`Scaffolded ${result.context.appName} in ${result.targetDirectory}`);\r\n console.log(\"\");\r\n console.log(\"Next steps:\");\r\n console.log(` cd ${result.targetDirectory}`);\r\n if (options.skipInstall) {\r\n console.log(\" pnpm install\");\r\n }\r\n console.log(\" pnpm --filter ./apps/server drizzle:migrate\");\r\n console.log(\" pnpm dev\");\r\n}\r\n\r\nexport function parseCli(argv: readonly string[]): ParsedCli {\r\n const normalizedArgv = normalizeCliArgv(argv);\r\n const [command, ...rest] = normalizedArgv;\r\n const options: Record<string, string | boolean> = {};\r\n let directory: string | undefined;\r\n let help = false;\r\n\r\n for (let index = 0; index < rest.length; index += 1) {\r\n const token = rest[index];\r\n\r\n if (token === \"--help\" || token === \"-h\") {\r\n help = true;\r\n continue;\r\n }\r\n\r\n if (!token.startsWith(\"--\")) {\r\n directory ??= token;\r\n continue;\r\n }\r\n\r\n const [rawKey, inlineValue] = token.slice(2).split(\"=\", 2);\r\n\r\n if (isBooleanFlag(rawKey)) {\r\n options[rawKey] = true;\r\n continue;\r\n }\r\n\r\n const nextValue = inlineValue ?? rest[index + 1];\r\n if (!nextValue || nextValue.startsWith(\"--\")) {\r\n throw new Error(`Missing value for --${rawKey}`);\r\n }\r\n\r\n options[rawKey] = nextValue;\r\n if (inlineValue === undefined) {\r\n index += 1;\r\n }\r\n }\r\n\r\n return { command, directory, help, options };\r\n}\r\n\r\nfunction normalizeCliArgv(argv: readonly string[]): readonly string[] {\r\n const [firstToken] = argv;\r\n\r\n if (!firstToken || firstToken === \"create\" || firstToken === \"--help\" || firstToken === \"-h\") {\r\n return argv;\r\n }\r\n\r\n if (firstToken === \"help\") {\r\n return [\"--help\"];\r\n }\r\n\r\n return [\"create\", ...argv];\r\n}\r\n\r\nfunction isBooleanFlag(value: string): boolean {\r\n return [\"yes\", \"force\", \"skip-install\", \"skip-git\", \"with-test-harness\"].includes(value);\r\n}\r\n\r\nfunction toCreateCommandOptions(parsed: ParsedCli): CreateCommandOptions {\r\n const platform = getStringOption(parsed.options, \"platform\");\r\n if (platform && ![\"web\", \"expo\", \"both\"].includes(platform)) {\r\n throw new Error(`Invalid --platform \"${platform}\". Use web, expo, or both.`);\r\n }\r\n\r\n return {\r\n targetDirectory: parsed.directory,\r\n appName: getStringOption(parsed.options, \"name\"),\r\n appDescription: getStringOption(parsed.options, \"description\"),\r\n platform: platform as CreateCommandOptions[\"platform\"],\r\n testHarness: parsed.options[\"with-test-harness\"] ? true : undefined,\r\n yes: Boolean(parsed.options.yes),\r\n force: Boolean(parsed.options.force),\r\n skipInstall: Boolean(parsed.options[\"skip-install\"]),\r\n skipGit: Boolean(parsed.options[\"skip-git\"]),\r\n };\r\n}\r\n\r\nfunction getStringOption(\r\n options: Record<string, string | boolean>,\r\n key: string\r\n): string | undefined {\r\n const value = options[key];\r\n return typeof value === \"string\" ? value : undefined;\r\n}\r\n\r\nfunction printHelp(): void {\r\n console.log(\"m5kdev\");\r\n console.log(\"\");\r\n console.log(\"Usage:\");\r\n console.log(\" pnpm create m5kdev [directory] [options]\");\r\n console.log(\" m5kdev [directory] [options]\");\r\n console.log(\" m5kdev create [directory] [options]\");\r\n console.log(\"\");\r\n console.log(\"Options:\");\r\n console.log(\" --name <value> Set the app name\");\r\n console.log(\" --description <value> Set the app description\");\r\n console.log(\" --platform <value> App platform: web (default), expo, or both\");\r\n console.log(\" --with-test-harness Include the e2e test harness (default: no)\");\r\n console.log(\" --yes Accept defaults for missing prompts\");\r\n console.log(\" --force Allow writing into a non-empty directory\");\r\n console.log(\" --skip-install Skip pnpm install\");\r\n console.log(\" --skip-git Skip git init\");\r\n}\r\n"],"mappings":"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"}
@@ -1,8 +1,11 @@
1
1
  //#region src/types.d.ts
2
+ type AppPlatform = "web" | "expo" | "both";
2
3
  interface PromptValues {
3
4
  targetDirectory?: string;
4
5
  appName?: string;
5
6
  appDescription?: string;
7
+ platform?: AppPlatform;
8
+ testHarness?: boolean;
6
9
  }
7
10
  interface CreateCommandOptions extends PromptValues {
8
11
  yes: boolean;
@@ -17,6 +20,11 @@ interface TemplateContext {
17
20
  packageScope: string;
18
21
  betterAuthSecret: string;
19
22
  }
23
+ interface TemplateFeatureManifest {
24
+ features: Record<string, {
25
+ paths: readonly string[];
26
+ }>;
27
+ }
20
28
  //#endregion
21
- export { CreateCommandOptions, PromptValues, TemplateContext };
29
+ export { AppPlatform, CreateCommandOptions, PromptValues, TemplateContext, TemplateFeatureManifest };
22
30
  //# sourceMappingURL=types.d.cts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-m5kdev",
3
- "version": "0.25.2",
3
+ "version": "0.26.1",
4
4
  "license": "GPL-3.0-only",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,8 +13,8 @@
13
13
  "templates"
14
14
  ],
15
15
  "bin": {
16
- "create-m5kdev": "dist/src/index.js",
17
- "m5kdev": "dist/src/index.js"
16
+ "create-m5kdev": "dist/src/index.cjs",
17
+ "m5kdev": "dist/src/index.cjs"
18
18
  },
19
19
  "dependencies": {
20
20
  "tslib": "2.8.1"
@@ -25,14 +25,16 @@
25
25
  "jest": "30.1.3",
26
26
  "ts-jest": "29.4.9",
27
27
  "tsdown": "0.21.7",
28
- "typescript": "6.0.3"
28
+ "typescript": "6.0.3",
29
+ "tsx": "4.19.2"
29
30
  },
30
31
  "scripts": {
31
- "build": "tsdown",
32
+ "build": "tsx scripts/prepare-templates.ts && tsdown",
32
33
  "lint": "biome check .",
33
34
  "lint:fix": "biome check . --write",
34
35
  "check-types": "tsc --noEmit",
35
36
  "test": "jest -c jest.config.ts",
36
- "test:smoke": "CLI_SMOKE=1 jest -c jest.config.ts src/__tests__/create.smoke.test.ts"
37
+ "test:smoke": "CLI_SMOKE=1 jest -c jest.config.ts src/__tests__/create.smoke.test.ts",
38
+ "prepare:templates": "tsx scripts/prepare-templates.ts"
37
39
  }
38
40
  }
@@ -8,6 +8,23 @@ alwaysApply: false
8
8
  All server communication goes through tRPC with the TanStack React Query
9
9
  integration. No `fetch`, no axios, no hand-written API clients.
10
10
 
11
+ ## Types come from the server — never hand-mirror them
12
+
13
+ Row and payload types are **inferred from the router**, not redeclared:
14
+
15
+ ```ts
16
+ import type { inferRouterOutputs } from "@trpc/server";
17
+ import type { AppRouter } from "{{PACKAGE_SCOPE}}/server/types";
18
+
19
+ type PostsListOutput = inferRouterOutputs<AppRouter>["posts"]["list"];
20
+ export type PostRow = PostsListOutput["rows"][number];
21
+ ```
22
+
23
+ Shared Zod schemas (`apps/shared`) are only for runtime needs — form
24
+ validation, or schemas the server DTO also imports. Never write a shared
25
+ schema that just mirrors a server response; it will drift (see the shared
26
+ schema guide).
27
+
11
28
  ## Queries
12
29
 
13
30
  Use `useTRPC()` and the generated `queryOptions`:
@@ -15,15 +32,38 @@ Use `useTRPC()` and the generated `queryOptions`:
15
32
  ```ts
16
33
  const trpc = useTRPC();
17
34
  const { data, isLoading, isFetching } = useQuery(
18
- trpc.posts.list.queryOptions(listInput) as unknown as UseQueryOptions<PostsListOutputSchema>
35
+ trpc.posts.list.queryOptions(listInput) as unknown as UseQueryOptions<PostsListOutput>
19
36
  );
20
37
  ```
21
38
 
22
- - Input and output types come from the shared Zod schemas in `apps/shared` —
23
- never redeclare them locally.
24
39
  - Search inputs that feed queries go through `useDeferredValue`, and URL-state
25
40
  updates are wrapped in `startTransition`.
26
41
 
42
+ ## List endpoints speak the generic query contract
43
+
44
+ Module list procedures accept `QueryInput` from
45
+ `@m5kdev/commons/modules/schemas/query.schema` — `page`, `limit`, `sort`,
46
+ `order`, `q` (global search), and `filters`. Do not invent per-module fields
47
+ like `search` or `status` at the top level; they will be stripped by input
48
+ validation and silently ignored.
49
+
50
+ ```ts
51
+ const listInput = useMemo<QueryInput>(
52
+ () => ({
53
+ page,
54
+ limit: PAGE_SIZE,
55
+ q: deferredSearch || undefined,
56
+ filters:
57
+ status === "all"
58
+ ? undefined
59
+ : [{ columnId: "status", type: "enum", method: "equals", value: status }],
60
+ sort: "updatedAt",
61
+ order: "desc",
62
+ }),
63
+ [deferredSearch, page, status]
64
+ );
65
+ ```
66
+
27
67
  ## Mutations
28
68
 
29
69
  Use `mutationOptions` with success/error handling declared where the mutation
@@ -41,6 +81,9 @@ const createMutation = useMutation(
41
81
  );
42
82
  ```
43
83
 
84
+ Mutation input shapes are enforced by the router's inferred types — no
85
+ `satisfies SomeSchema` casts needed for simple `{ id }` payloads.
86
+
44
87
  ## Invalidation discipline
45
88
 
46
89
  - Invalidate intentionally with the matching `queryFilter()` after mutations.
@@ -5,7 +5,10 @@ alwaysApply: false
5
5
 
6
6
  # Module DB File Style Guide
7
7
 
8
- Conventions for database table definitions in app modules.
8
+ Conventions for database table definitions in app modules. The table is the
9
+ **source of truth** for the module: the DTO (`<module>.dto.ts`) derives its
10
+ Zod schemas from it, and row types flow from there through the router to the
11
+ client.
9
12
 
10
13
  ## Conventions
11
14
 
@@ -23,6 +26,17 @@ Conventions for database table definitions in app modules.
23
26
  - Composite join tables define primary keys with `primaryKey({ columns: [t.aId, t.bId] })`.
24
27
  - Table names are plural; column names are snake_case at DB level, camelCase in code.
25
28
  - Inline union types are acceptable for simple status columns: `.$type<"draft" | "published">()`.
29
+ - Include `deletedAt` when the module needs soft deletes — `queryList`
30
+ automatically excludes soft-deleted rows when the column exists.
31
+
32
+ ## Registration and migrations
33
+
34
+ - Register every new table in `apps/server/src/schema.ts` with a plain
35
+ re-export: `export { posts } from "./modules/posts/posts.db";`. The schema
36
+ file is maintained by hand — there is no generation step.
37
+ - After changing tables, run `pnpm drizzle:generate` (creates a migration in
38
+ `drizzle/`) and `pnpm drizzle:migrate` (applies it). Never write migration
39
+ SQL by hand and never edit files under `drizzle/meta/`.
26
40
 
27
41
  ## File Location
28
42
 
@@ -0,0 +1,60 @@
1
+ ---
2
+ globs: apps/**/server/src/modules/**/*.dto.ts
3
+ alwaysApply: false
4
+ ---
5
+
6
+ # Module DTO Style Guide
7
+
8
+ The DTO is the module's API surface, **derived from the Drizzle table** —
9
+ database schema first. Services, repositories, and tRPC files import the DTO
10
+ *object* (`postSchemas`) and reference `postSchemas.input.x` /
11
+ `postSchemas.output.y`; they do not import hand-written types.
12
+
13
+ ## The pattern
14
+
15
+ ```ts
16
+ import { createZodSchemas } from "@m5kdev/backend/modules/base/base.dto";
17
+ import {
18
+ postCreateInputSchema,
19
+ postUpdateInputSchema,
20
+ } from "{{PACKAGE_SCOPE}}/shared/modules/posts/posts.schema";
21
+ import { z } from "zod";
22
+ import { posts } from "./posts.db";
23
+
24
+ const { output, input } = createZodSchemas(posts);
25
+
26
+ export const postSchemas = {
27
+ output,
28
+ input: {
29
+ ...input,
30
+ // create/update come from shared: the same schemas validate the webapp
31
+ // form, so the API contract and frontend validation cannot drift
32
+ create: postCreateInputSchema,
33
+ update: postUpdateInputSchema,
34
+ publish: z.object({ id: z.string() }),
35
+ },
36
+ };
37
+ ```
38
+
39
+ ## What `createZodSchemas(table)` provides
40
+
41
+ - `output.single` — the row (from `createSelectSchema`).
42
+ - `output.found` — `single.optional()`, for find-style queries that may
43
+ return nothing.
44
+ - `output.list` — `{ rows, total }` envelope.
45
+ - `output.uuid` / `output.uuids` — id acknowledgements.
46
+ - `input.list` — the generic `querySchema` (pagination, sorting, `q`,
47
+ `filters`).
48
+ - `input.read/readMany/delete/deleteMany` — id inputs.
49
+ - `input.create` / `input.update` — raw insert/update schemas from the table.
50
+
51
+ ## Rules
52
+
53
+ - **Always override `input.create` and `input.update`** with narrower schemas:
54
+ the raw insert schema exposes server-managed columns (`id`, timestamps,
55
+ owner ids) that clients must never send. If the frontend validates the same
56
+ form, import the schema from shared instead of redefining it.
57
+ - Add action-specific inputs (`publish`, `archive`) as plain Zod objects here.
58
+ - Extend `input.list` only when a custom list contract is genuinely needed;
59
+ prefer the generic `q`/`filters` contract (see repository guide).
60
+ - One exported `<module>Schemas` object per module — no loose schema exports.