@multiplatform.one/cli 6.1.0 → 6.4.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 (108) hide show
  1. package/README.md +72 -0
  2. package/bin/createMultiplatformApp.mjs +77 -0
  3. package/lib/bin/multiplatformOne.mjs +35 -10
  4. package/lib/commands/e2e.mjs +3 -3
  5. package/lib/commands/init.mjs +16 -12
  6. package/lib/commands/initApp.mjs +175 -0
  7. package/lib/commands/updateApp.mjs +260 -0
  8. package/lib/generateVscode.mjs +27 -10
  9. package/package.json +23 -12
  10. package/src/bin/multiplatformOne.ts +83 -20
  11. package/src/commands/init.ts +7 -18
  12. package/src/commands/initApp.spec.ts +210 -0
  13. package/src/commands/initApp.ts +274 -0
  14. package/src/commands/updateApp.ts +277 -0
  15. package/src/generateVscode.ts +31 -9
  16. package/templates/app/AGENTS.md +142 -0
  17. package/templates/app/README.md +62 -0
  18. package/templates/app/apps/__NAME__/index.html +36 -0
  19. package/templates/app/apps/__NAME__/package.json +30 -0
  20. package/templates/app/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
  21. package/templates/app/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
  22. package/templates/app/apps/__NAME__/src/App.tsx +9 -0
  23. package/templates/app/apps/__NAME__/src/main.tsx +14 -0
  24. package/templates/app/apps/__NAME__/tsconfig.json +8 -0
  25. package/templates/app/apps/__NAME__/vite.config.ts +17 -0
  26. package/templates/app/features/__NAME__/package.json +34 -0
  27. package/templates/app/features/__NAME__/src/HomeScreen.tsx +70 -0
  28. package/templates/app/features/__NAME__/src/index.ts +1 -0
  29. package/templates/app/features/__NAME__/tsconfig.json +5 -0
  30. package/templates/app/gitignore +8 -0
  31. package/templates/app/npmrc +2 -0
  32. package/templates/app/oxlintrc.json +39 -0
  33. package/templates/app/package.json +20 -0
  34. package/templates/app/packages/config/config.json +4 -0
  35. package/templates/app/packages/config/index.ts +5 -0
  36. package/templates/app/packages/config/package.json +17 -0
  37. package/templates/app/packages/config/tsconfig.json +5 -0
  38. package/templates/app/pnpm-workspace.yaml +4 -0
  39. package/templates/app/tsconfig.base.json +16 -0
  40. package/templates/universal/AGENTS.md +142 -0
  41. package/templates/universal/README.md +97 -0
  42. package/templates/universal/apps/__NAME__/app.config.js +31 -0
  43. package/templates/universal/apps/__NAME__/config/tamagui.config.ts +4 -0
  44. package/templates/universal/apps/__NAME__/package.json +61 -0
  45. package/templates/universal/apps/__NAME__/polyfills.native.ts +23 -0
  46. package/templates/universal/apps/__NAME__/polyfills.ts +3 -0
  47. package/templates/universal/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
  48. package/templates/universal/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
  49. package/templates/universal/apps/__NAME__/routes/_layout.tsx +22 -0
  50. package/templates/universal/apps/__NAME__/routes/_layout.web.tsx +21 -0
  51. package/templates/universal/apps/__NAME__/routes/about.tsx +3 -0
  52. package/templates/universal/apps/__NAME__/routes/index.tsx +3 -0
  53. package/templates/universal/apps/__NAME__/src/app.ts +10 -0
  54. package/templates/universal/apps/__NAME__/tamagui.css +3 -0
  55. package/templates/universal/apps/__NAME__/tsconfig.json +8 -0
  56. package/templates/universal/apps/__NAME__/types.d.ts +9 -0
  57. package/templates/universal/apps/__NAME__/vite.config.ts +66 -0
  58. package/templates/universal/features/__NAME__/package.json +35 -0
  59. package/templates/universal/features/__NAME__/src/AboutScreen.tsx +27 -0
  60. package/templates/universal/features/__NAME__/src/AppLayout.tsx +50 -0
  61. package/templates/universal/features/__NAME__/src/ContactForm.spec.tsx +22 -0
  62. package/templates/universal/features/__NAME__/src/ContactForm.tsx +55 -0
  63. package/templates/universal/features/__NAME__/src/HomeScreen.tsx +40 -0
  64. package/templates/universal/features/__NAME__/src/index.ts +4 -0
  65. package/templates/universal/features/__NAME__/src/screens.spec.ts +12 -0
  66. package/templates/universal/features/__NAME__/tsconfig.json +5 -0
  67. package/templates/universal/features/__NAME__/vitest.config.mjs +7 -0
  68. package/templates/universal/gitignore +12 -0
  69. package/templates/universal/npmrc +3 -0
  70. package/templates/universal/oxlintrc.json +39 -0
  71. package/templates/universal/package.json +27 -0
  72. package/templates/universal/packages/config/config.json +4 -0
  73. package/templates/universal/packages/config/index.ts +5 -0
  74. package/templates/universal/packages/config/package.json +17 -0
  75. package/templates/universal/packages/config/tsconfig.json +5 -0
  76. package/templates/universal/packages/i18n/en/common.json +30 -0
  77. package/templates/universal/packages/i18n/es/common.json +30 -0
  78. package/templates/universal/packages/i18n/i18n.d.ts +16 -0
  79. package/templates/universal/packages/i18n/index.ts +26 -0
  80. package/templates/universal/packages/i18n/package.json +20 -0
  81. package/templates/universal/packages/i18n/resources.ts +13 -0
  82. package/templates/universal/packages/i18n/tsconfig.json +5 -0
  83. package/templates/universal/packages/themes/accent.ts +32 -0
  84. package/templates/universal/packages/themes/base.ts +50 -0
  85. package/templates/universal/packages/themes/fonts.ts +8 -0
  86. package/templates/universal/packages/themes/index.ts +33 -0
  87. package/templates/universal/packages/themes/package.json +23 -0
  88. package/templates/universal/packages/themes/tamagui.config.ts +14 -0
  89. package/templates/universal/packages/themes/tsconfig.json +5 -0
  90. package/templates/universal/pnpm-workspace.yaml +4 -0
  91. package/templates/universal/tsconfig.base.json +21 -0
  92. package/templates/universal/types/env.d.ts +9 -0
  93. package/types/bin/multiplatformOne.d.ts +11 -0
  94. package/types/bin/multiplatformOne.d.ts.map +1 -0
  95. package/types/commands/e2e.d.ts +14 -0
  96. package/types/commands/e2e.d.ts.map +1 -0
  97. package/types/commands/init.d.ts +16 -0
  98. package/types/commands/init.d.ts.map +1 -0
  99. package/types/commands/initApp.d.ts +41 -0
  100. package/types/commands/initApp.d.ts.map +1 -0
  101. package/types/commands/updateApp.d.ts +24 -0
  102. package/types/commands/updateApp.d.ts.map +1 -0
  103. package/types/generateVscode.d.ts +7 -0
  104. package/types/generateVscode.d.ts.map +1 -0
  105. package/types/index.d.ts +2 -0
  106. package/types/index.d.ts.map +1 -0
  107. package/types/types.d.ts +6 -0
  108. package/types/types.d.ts.map +1 -0
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # @multiplatform.one/cli
2
+
3
+ The multiplatform.one command line: scaffold new multiplatform projects
4
+ (`mpo init` / `create-multiplatform-app`), manage the monorepo dev
5
+ environment, and drive Frappe + E2E workflows.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add -g @multiplatform.one/cli
11
+ # or run the scaffolder without installing:
12
+ npx --package=@multiplatform.one/cli create-multiplatform-app my-app
13
+ ```
14
+
15
+ Two binaries: **`mpo`** (full CLI) and **`create-multiplatform-app`**
16
+ (scaffolder only).
17
+
18
+ ## Scaffold a project (recommended)
19
+
20
+ ```bash
21
+ mpo init my-app # universal: web (SSR) + iOS + Android via one
22
+ cd my-app
23
+ pnpm install
24
+ pnpm dev # web + native dev server
25
+ pnpm ios # build & run the iOS dev client (pnpm dev in another terminal)
26
+ ```
27
+
28
+ `mpo init` creates a **consumer** project — not a monorepo fork. It prompts for
29
+ anything omitted (name; platforms, defaulting to universal); pass `--yes` for
30
+ CI. `@multiplatform.one/*` dependencies come from npm at semver ranges, never
31
+ `workspace:*`.
32
+
33
+ Universal layout: `apps/<name>` (routes + providers + vite/expo config),
34
+ `features/<name>` (starter Home/About screens, forms demo, vitest),
35
+ `packages/config`, `packages/i18n` (en/es), `packages/themes` (Tamagui theme).
36
+
37
+ Want web-only? `mpo init --web my-app` scaffolds the Vite + React variant
38
+ (`--app` is a back-compat alias; `create-multiplatform-app` without flags also
39
+ produces it — pass `--universal` there for the full project).
40
+
41
+ Options: `--universal`, `--web`/`--app`, `--skip-install`,
42
+ `--mpo-version <range>` (defaults to `^<cli version>`), `-y/--yes`.
43
+
44
+ ## Other commands
45
+
46
+ | Command | Purpose |
47
+ | ---------------------------- | -------------------------------------------------------------------- |
48
+ | `mpo init --monorepo [name]` | DEPRECATED: clone/modify the full monorepo (framework dev forks) |
49
+ | `mpo update` | Merge upstream and re-apply workspace config |
50
+ | `mpo wait` | Wait for a service (frappe, postgres, mariadb, keycloak) to be ready |
51
+ | `mpo frappe bootstrap` | Bootstrap the Frappe development environment |
52
+ | `mpo frappe dev` | Start the Frappe development server |
53
+ | `mpo frappe bench -- <args>` | Run a bench command |
54
+ | `mpo frappe clean` | Clean Frappe bench artifacts |
55
+ | `mpo build` | Run the build command |
56
+ | `mpo test` | Run tests (E2E flags: `--up-only`, `--down`, `--filter`, …) |
57
+
58
+ ## Development
59
+
60
+ Templates live in `templates/universal/` and `templates/app/`. Generator unit
61
+ specs: `pnpm --filter @multiplatform.one/cli test`
62
+ (`src/commands/initApp.spec.ts` — tree + package.json shape for both templates).
63
+
64
+ ## What it must not do
65
+
66
+ - The project templates never ship `workspace:*` dependencies on
67
+ `@multiplatform.one/*` or copies of the monorepo's `public/` packages —
68
+ consumers upgrade via npm (`pnpm update "@multiplatform.one/*"`)
69
+
70
+ ## License
71
+
72
+ Apache-2.0
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * create-multiplatform-app — npm create / npx entry for consumer scaffolds.
5
+ * Delegates to the same generator as `mpo init`.
6
+ *
7
+ * Defaults to the web-only app template (back-compat). Pass --universal for
8
+ * the full web + iOS + Android project (same as `mpo init <name>`).
9
+ *
10
+ * Usage:
11
+ * npx --package=@multiplatform.one/cli create-multiplatform-app my-app
12
+ * npx --package=@multiplatform.one/cli create-multiplatform-app my-app --universal
13
+ * mpo init my-app # universal
14
+ * mpo init --web my-app # web-only
15
+ */
16
+ import { createRequire } from "node:module";
17
+ import { dirname, resolve } from "node:path";
18
+ import { fileURLToPath, pathToFileURL } from "node:url";
19
+
20
+ const here = dirname(fileURLToPath(import.meta.url));
21
+ const require = createRequire(import.meta.url);
22
+
23
+ function flagValue(argv, name) {
24
+ const idx = argv.indexOf(name);
25
+ if (idx < 0) return undefined;
26
+ return argv[idx + 1];
27
+ }
28
+
29
+ async function main() {
30
+ const argv = process.argv.slice(2);
31
+ if (argv[0] === "--help" || argv[0] === "-h") {
32
+ console.log(`create-multiplatform-app [name] [--universal] [--skip-install] [--mpo-version <range>] [--yes]
33
+
34
+ Scaffold a consumer project with @multiplatform.one/* at semver ranges
35
+ (not workspace:*):
36
+
37
+ (default) apps/<name> + features/<name> + packages/config (web-only)
38
+ --universal one-based web + iOS + Android app
39
+ (adds packages/i18n + packages/themes)
40
+
41
+ Same as: mpo init --web [name] / mpo init [name]
42
+ `);
43
+ return;
44
+ }
45
+ if (argv[0] === "--version" || argv[0] === "-V") {
46
+ console.log(require("../package.json").version);
47
+ return;
48
+ }
49
+
50
+ const skipInstall = argv.includes("--skip-install");
51
+ const yes = argv.includes("--yes") || argv.includes("-y");
52
+ const universal = argv.includes("--universal");
53
+ const version = flagValue(argv, "--mpo-version");
54
+ const name = argv.find((a) => !a.startsWith("-"));
55
+
56
+ const libPath = resolve(here, "../lib/commands/initApp.mjs");
57
+ let initApp;
58
+ try {
59
+ ({ initApp } = await import(pathToFileURL(libPath).href));
60
+ } catch {
61
+ throw new Error(
62
+ "Could not load initApp. Build @multiplatform.one/cli first (pnpm --filter @multiplatform.one/cli build).",
63
+ );
64
+ }
65
+
66
+ await initApp(name, {
67
+ skipInstall,
68
+ version,
69
+ template: universal ? "universal" : "app",
70
+ yes,
71
+ });
72
+ }
73
+
74
+ main().catch((err) => {
75
+ console.error(err instanceof Error ? err.message : err);
76
+ process.exit(1);
77
+ });
@@ -1,16 +1,18 @@
1
1
  import { generateVscodeConfig } from "../generateVscode.mjs";
2
2
  import { discoverE2EApp, runE2ESession } from "../commands/e2e.mjs";
3
3
  import { init, runModifyStep } from "../commands/init.mjs";
4
+ import { initApp, readProvenance } from "../commands/initApp.mjs";
5
+ import { updateApp } from "../commands/updateApp.mjs";
4
6
  import { createRequire } from "node:module";
5
7
  import fs from "node:fs/promises";
6
8
  import path from "node:path";
7
- import spawn from "nano-spawn";
8
- import yoctoSpinner from "yocto-spinner";
9
- import YAML from "yaml";
10
9
  import fsSync from "node:fs";
11
10
  import { fileURLToPath } from "node:url";
12
11
  import { formatServiceList, lookupProjectRoot, waitForFrappe, waitForKeycloak, waitForPostgres, waitServices } from "@multiplatform.one/utils/dev";
13
12
  import { Command } from "commander";
13
+ import spawn from "nano-spawn";
14
+ import YAML from "yaml";
15
+ import yoctoSpinner from "yocto-spinner";
14
16
 
15
17
  //#region \0rolldown/runtime.js
16
18
  var __create = Object.create;
@@ -469,16 +471,39 @@ async function getFrappeApps(root) {
469
471
  const program = new Command();
470
472
  program.name("mpo");
471
473
  program.version(JSON.parse(fsSync.readFileSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json"), "utf8"))?.version);
472
- program.command("init").option("-c, --checkout <branch>", "branch, tag or commit to checkout", "main").option("-a, --apps <apps>", "comma-separated apps to include").option("-s, --services <services>", "comma-separated services to include").argument("[name]", "the name of the project").description("clone multiplatform.one and apply selected apps/services").action(async (name, options) => {
473
- if (await spawn("git", ["rev-parse", "--is-inside-work-tree"]).then(() => true, () => false)) throw new Error("mpo cannot be initialized inside a git repository");
474
- const cloneScript = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../scripts/clone.sh");
475
- await init(name, {
476
- ...options,
477
- cloneScript
474
+ program.command("init").option("--universal", "scaffold the universal template (web + iOS + Android; default)").option("--web", "scaffold the web-only template (Vite + React)").option("--app", "alias of --web (back-compat)").option("--skip-install", "skip pnpm install after scaffolding").option("--mpo-version <range>", "semver range for @multiplatform.one/* (default: ^<cli version>)").option("-y, --yes", "non-interactive: accept defaults instead of prompting").option("--monorepo", "DEPRECATED: clone/modify the full multiplatform.one monorepo instead of scaffolding a consumer project").option("-c, --checkout <branch>", "branch, tag or commit to checkout (--monorepo only)", "main").option("-a, --apps <apps>", "comma-separated apps to include (--monorepo only)").option("-s, --services <services>", "comma-separated services to include (--monorepo only)").argument("[name]", "the name of the project").description("scaffold a new multiplatform project (universal web + native by default; --web for web-only; semver @multiplatform.one/* deps)").action(async (name, options) => {
475
+ if (options.monorepo) {
476
+ console.warn("⚠️ `mpo init --monorepo` clones the whole multiplatform.one monorepo (legacy). New projects should use the default consumer scaffold instead.");
477
+ if (await spawn("git", ["rev-parse", "--is-inside-work-tree"]).then(() => true, () => false)) throw new Error("mpo cannot be initialized inside a git repository");
478
+ const cloneScript = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../scripts/clone.sh");
479
+ await init(name, {
480
+ ...options,
481
+ cloneScript
482
+ });
483
+ return;
484
+ }
485
+ if (options.apps || options.services) throw new Error("--apps/--services select monorepo pieces and require --monorepo (legacy clone mode)");
486
+ const webOnly = Boolean(options.web || options.app);
487
+ if (webOnly && options.universal) throw new Error("Pass either --web/--app or --universal, not both");
488
+ await initApp(name, {
489
+ skipInstall: Boolean(options.skipInstall),
490
+ version: options.mpoVersion,
491
+ template: webOnly ? "app" : options.universal ? "universal" : void 0,
492
+ yes: Boolean(options.yes)
478
493
  });
479
494
  });
480
- program.command("update").option("-c, --checkout <branch>", "branch, tag or commit to merge from upstream", "main").option("-r, --remote <url>", "upstream remote URL", "https://gitlab.com/bitspur/multiplatform.one/multiplatform.one.git").description("merge upstream and re-apply workspace config").action(async (options) => {
495
+ program.command("update").option("-c, --checkout <branch>", "branch, tag or commit to merge from upstream (monorepo forks)", "main").option("-r, --remote <url>", "upstream remote URL (monorepo forks)", "https://gitlab.com/bitspur/multiplatform.one/multiplatform.one.git").option("--skip-install", "skip pnpm install after the update").option("--mpo-version <range>", "semver range for @multiplatform.one/* (default: ^<cli version>)").description("update a scaffolded project to the current template (three-way merge via .mpo.json provenance); monorepo forks fall back to the upstream merge flow").action(async (options) => {
481
496
  if (await spawn("git", ["rev-parse", "--is-inside-work-tree"]).then(() => false, () => true)) throw new Error("mpo cannot be updated outside of a git repository");
497
+ if (readProvenance(projectRoot)) {
498
+ await updateApp({
499
+ skipInstall: options.skipInstall,
500
+ version: options.mpoVersion
501
+ });
502
+ try {
503
+ await generateVscodeConfig(projectRoot);
504
+ } catch {}
505
+ return;
506
+ }
482
507
  if (await spawn("git", [
483
508
  "diff",
484
509
  "--cached",
@@ -1,10 +1,10 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import crypto from "node:crypto";
4
- import os from "node:os";
5
3
  import spawn from "nano-spawn";
6
- import yoctoSpinner from "yocto-spinner";
7
4
  import YAML from "yaml";
5
+ import yoctoSpinner from "yocto-spinner";
6
+ import crypto from "node:crypto";
7
+ import os from "node:os";
8
8
 
9
9
  //#region src/commands/e2e.ts
10
10
  function sessionFileName(portOffset) {
@@ -1,6 +1,6 @@
1
1
  import { join, resolve } from "node:path";
2
- import { execSync } from "node:child_process";
3
2
  import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { execSync } from "node:child_process";
4
4
  import inquirer from "inquirer";
5
5
 
6
6
  //#region src/commands/init.ts
@@ -20,8 +20,6 @@ const apps = [
20
20
  "keycloak",
21
21
  "storybook",
22
22
  "storybook-expo",
23
- "vscode",
24
- "webext",
25
23
  "vocs"
26
24
  ];
27
25
  /** env variable prefixes to remove per service */
@@ -33,12 +31,14 @@ const serviceEnvPrefixes = {
33
31
  };
34
32
  /** env variable prefixes to remove per app */
35
33
  const appEnvPrefixes = {
36
- one: ["ONE_"],
34
+ one: [
35
+ "ONE_",
36
+ "VSCODE_",
37
+ "WEBEXT_"
38
+ ],
37
39
  keycloak: ["KEYCLOAK_"],
38
40
  storybook: ["STORYBOOK_", "VR_DIFFING_ENGINE="],
39
41
  "storybook-expo": [],
40
- vscode: ["VSCODE_"],
41
- webext: ["WEBEXT_"],
42
42
  vocs: ["VOCS_"]
43
43
  };
44
44
  /** env section headers (comments) to remove per app/service */
@@ -49,12 +49,14 @@ const serviceEnvSections = {
49
49
  sui: []
50
50
  };
51
51
  const appEnvSections = {
52
- one: ["# one"],
52
+ one: [
53
+ "# one",
54
+ "# vscode",
55
+ "# webext"
56
+ ],
53
57
  keycloak: ["# keycloak"],
54
58
  storybook: ["# storybook"],
55
59
  "storybook-expo": [],
56
- vscode: ["# vscode"],
57
- webext: ["# webext"],
58
60
  vocs: ["# vocs"]
59
61
  };
60
62
  /** launch.json configuration names per service/app */
@@ -65,12 +67,14 @@ const serviceLaunchNames = {
65
67
  sui: ["sui dev"]
66
68
  };
67
69
  const appLaunchNames = {
68
- one: ["one dev"],
70
+ one: [
71
+ "one dev",
72
+ "vscode dev",
73
+ "webext dev"
74
+ ],
69
75
  keycloak: ["keycloak dev", "keycloak storybook"],
70
76
  storybook: ["storybook dev"],
71
77
  "storybook-expo": ["storybook-expo dev"],
72
- vscode: ["vscode dev"],
73
- webext: ["webext dev"],
74
78
  vocs: ["vocs dev"]
75
79
  };
76
80
  /**
@@ -0,0 +1,175 @@
1
+ import { dirname, extname, join, resolve } from "node:path";
2
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import spawn from "nano-spawn";
5
+ import inquirer from "inquirer";
6
+
7
+ //#region src/commands/initApp.ts
8
+ function toPascalCase(name) {
9
+ return name.split(/[-_\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
10
+ }
11
+ function validateName(name) {
12
+ const trimmed = name.trim();
13
+ if (!trimmed) throw new Error("Project name is required");
14
+ if (!/^[a-z][a-z0-9-]*$/.test(trimmed)) throw new Error(`Invalid project name "${trimmed}". Use lowercase letters, numbers, and hyphens (e.g. my-app).`);
15
+ return trimmed;
16
+ }
17
+ function resolveMpoVersion(explicit) {
18
+ if (explicit) return explicit.startsWith("^") || explicit.startsWith("~") || explicit === "*" ? explicit : `^${explicit}`;
19
+ const version = cliVersion();
20
+ return version ? `^${version}` : "^6.1.0";
21
+ }
22
+ function cliVersion() {
23
+ try {
24
+ const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), "../../package.json");
25
+ return JSON.parse(readFileSync(pkgPath, "utf-8")).version;
26
+ } catch {
27
+ return;
28
+ }
29
+ }
30
+ const PROVENANCE_FILE = ".mpo.json";
31
+ function writeProvenance(targetDir, provenance) {
32
+ writeFileSync(join(targetDir, PROVENANCE_FILE), `${JSON.stringify(provenance, null, 2)}\n`);
33
+ }
34
+ function readProvenance(projectDir) {
35
+ const file = join(projectDir, PROVENANCE_FILE);
36
+ if (!existsSync(file)) return void 0;
37
+ try {
38
+ const raw = JSON.parse(readFileSync(file, "utf-8"));
39
+ if (!raw.template || !raw.cliVersion || !raw.name) return void 0;
40
+ return {
41
+ template: raw.template,
42
+ cliVersion: raw.cliVersion,
43
+ name: raw.name,
44
+ mpoVersion: raw.mpoVersion ?? `^${raw.cliVersion}`
45
+ };
46
+ } catch {
47
+ return;
48
+ }
49
+ }
50
+ function templatesRoot(template) {
51
+ return resolve(dirname(fileURLToPath(import.meta.url)), "../../templates", template);
52
+ }
53
+ /**
54
+ * npm always strips .gitignore / .npmrc from published tarballs, so the
55
+ * template stores them without the leading dot; restore it at scaffold time.
56
+ */
57
+ const DOTFILE_NAMES = {
58
+ gitignore: ".gitignore",
59
+ npmrc: ".npmrc"
60
+ };
61
+ function render(content, vars) {
62
+ return content.replaceAll("__NAME_PASCAL__", vars.NAME_PASCAL).replaceAll("__NAME_APPID__", vars.NAME_APPID).replaceAll("__NAME__", vars.NAME).replaceAll("__MPO_VERSION__", vars.MPO_VERSION).replaceAll("__YEAR__", vars.YEAR);
63
+ }
64
+ /**
65
+ * Assets copied byte-for-byte — running them through the utf-8 render pass
66
+ * would corrupt them (fonts ship in the templates for LC-33 stable mount).
67
+ */
68
+ const BINARY_EXTENSIONS = new Set([
69
+ ".woff2",
70
+ ".woff",
71
+ ".ttf",
72
+ ".otf",
73
+ ".png",
74
+ ".jpg",
75
+ ".ico"
76
+ ]);
77
+ function writeTree(srcDir, destDir, vars) {
78
+ if (!existsSync(srcDir)) throw new Error(`Project template missing at ${srcDir}`);
79
+ mkdirSync(destDir, { recursive: true });
80
+ for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
81
+ const from = join(srcDir, entry.name);
82
+ const to = join(destDir, render(DOTFILE_NAMES[entry.name] ?? entry.name, vars));
83
+ if (entry.isDirectory()) {
84
+ writeTree(from, to, vars);
85
+ continue;
86
+ }
87
+ if (BINARY_EXTENSIONS.has(extname(entry.name).toLowerCase())) {
88
+ copyFileSync(from, to);
89
+ continue;
90
+ }
91
+ writeFileSync(to, render(readFileSync(from, "utf-8"), vars));
92
+ }
93
+ }
94
+ async function resolveTemplate(options) {
95
+ if (options.template) return options.template;
96
+ if (options.yes || !process.stdout.isTTY) return "universal";
97
+ return (await inquirer.prompt([{
98
+ message: "Which platforms should this project target?",
99
+ name: "template",
100
+ type: "list",
101
+ choices: [{
102
+ name: "Universal — web + iOS + Android (one)",
103
+ value: "universal"
104
+ }, {
105
+ name: "Web only — Vite + React",
106
+ value: "app"
107
+ }],
108
+ default: "universal"
109
+ }])).template;
110
+ }
111
+ /**
112
+ * Scaffold a consumer project (universal by default, web-only with
113
+ * template: "app") that depends on @multiplatform.one/* at semver ranges
114
+ * (never workspace:*). This is NOT a monorepo fork.
115
+ */
116
+ async function initApp(nameArg, options = {}) {
117
+ let projectName = nameArg;
118
+ if (!projectName) if (options.yes || !process.stdout.isTTY) projectName = "my-app";
119
+ else projectName = (await inquirer.prompt([{
120
+ message: "What is the app name?",
121
+ name: "name",
122
+ type: "input",
123
+ default: "my-app"
124
+ }])).name;
125
+ const name = validateName(projectName);
126
+ const template = await resolveTemplate(options);
127
+ const targetDir = resolve(name);
128
+ if (existsSync(targetDir)) {
129
+ if (readdirSync(targetDir).length > 0) throw new Error(`Directory "${targetDir}" already exists and is not empty`);
130
+ }
131
+ const vars = {
132
+ NAME: name,
133
+ NAME_PASCAL: toPascalCase(name),
134
+ NAME_APPID: name.replaceAll("-", ""),
135
+ MPO_VERSION: resolveMpoVersion(options.version),
136
+ YEAR: String((/* @__PURE__ */ new Date()).getFullYear())
137
+ };
138
+ console.log(`\nScaffolding ${template === "universal" ? "universal (web + iOS + Android)" : "web-only"} project at ${targetDir}`);
139
+ console.log(` apps/${name}`);
140
+ console.log(` features/${name}`);
141
+ console.log(` packages/config`);
142
+ if (template === "universal") {
143
+ console.log(` packages/i18n`);
144
+ console.log(` packages/themes`);
145
+ }
146
+ console.log(` @multiplatform.one/* → ${vars.MPO_VERSION}\n`);
147
+ writeTree(templatesRoot(template), targetDir, vars);
148
+ writeProvenance(targetDir, {
149
+ template,
150
+ cliVersion: cliVersion() ?? "0.0.0",
151
+ name,
152
+ mpoVersion: vars.MPO_VERSION
153
+ });
154
+ if (!options.skipInstall) {
155
+ console.log("Installing dependencies...");
156
+ await spawn("pnpm", ["install"], {
157
+ cwd: targetDir,
158
+ stdio: "inherit"
159
+ });
160
+ } else console.log("Skipped pnpm install (--skip-install).");
161
+ console.log(`\n✅ Project created at ${targetDir}`);
162
+ console.log("\nNext steps:");
163
+ console.log(` cd ${name}`);
164
+ if (options.skipInstall) console.log(" pnpm install");
165
+ console.log(" pnpm dev # web dev server");
166
+ if (template === "universal") {
167
+ console.log(" pnpm ios # iOS (requires pnpm dev in another terminal)");
168
+ console.log(" pnpm android # Android");
169
+ }
170
+ console.log("\nThis project consumes published @multiplatform.one/* packages.");
171
+ console.log("It is not a fork of the multiplatform.one monorepo.");
172
+ }
173
+
174
+ //#endregion
175
+ export { PROVENANCE_FILE, cliVersion, initApp, readProvenance, writeProvenance };