@multiplatform.one/cli 6.1.0 → 6.3.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 (104) hide show
  1. package/README.md +72 -0
  2. package/bin/createMultiplatformApp.mjs +77 -0
  3. package/lib/bin/multiplatformOne.mjs +23 -9
  4. package/lib/commands/e2e.mjs +3 -3
  5. package/lib/commands/init.mjs +16 -12
  6. package/lib/commands/initApp.mjs +145 -0
  7. package/lib/generateVscode.mjs +27 -10
  8. package/package.json +23 -12
  9. package/src/bin/multiplatformOne.ts +54 -16
  10. package/src/commands/init.ts +7 -18
  11. package/src/commands/initApp.spec.ts +210 -0
  12. package/src/commands/initApp.ts +228 -0
  13. package/src/generateVscode.ts +31 -9
  14. package/templates/app/AGENTS.md +142 -0
  15. package/templates/app/README.md +62 -0
  16. package/templates/app/apps/__NAME__/index.html +36 -0
  17. package/templates/app/apps/__NAME__/package.json +30 -0
  18. package/templates/app/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
  19. package/templates/app/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
  20. package/templates/app/apps/__NAME__/src/App.tsx +9 -0
  21. package/templates/app/apps/__NAME__/src/main.tsx +14 -0
  22. package/templates/app/apps/__NAME__/tsconfig.json +8 -0
  23. package/templates/app/apps/__NAME__/vite.config.ts +17 -0
  24. package/templates/app/features/__NAME__/package.json +34 -0
  25. package/templates/app/features/__NAME__/src/HomeScreen.tsx +70 -0
  26. package/templates/app/features/__NAME__/src/index.ts +1 -0
  27. package/templates/app/features/__NAME__/tsconfig.json +5 -0
  28. package/templates/app/gitignore +8 -0
  29. package/templates/app/npmrc +2 -0
  30. package/templates/app/oxlintrc.json +39 -0
  31. package/templates/app/package.json +20 -0
  32. package/templates/app/packages/config/config.json +4 -0
  33. package/templates/app/packages/config/index.ts +5 -0
  34. package/templates/app/packages/config/package.json +17 -0
  35. package/templates/app/packages/config/tsconfig.json +5 -0
  36. package/templates/app/pnpm-workspace.yaml +4 -0
  37. package/templates/app/tsconfig.base.json +16 -0
  38. package/templates/universal/AGENTS.md +142 -0
  39. package/templates/universal/README.md +97 -0
  40. package/templates/universal/apps/__NAME__/app.config.js +31 -0
  41. package/templates/universal/apps/__NAME__/config/tamagui.config.ts +4 -0
  42. package/templates/universal/apps/__NAME__/package.json +61 -0
  43. package/templates/universal/apps/__NAME__/polyfills.native.ts +23 -0
  44. package/templates/universal/apps/__NAME__/polyfills.ts +3 -0
  45. package/templates/universal/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
  46. package/templates/universal/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
  47. package/templates/universal/apps/__NAME__/routes/_layout.tsx +22 -0
  48. package/templates/universal/apps/__NAME__/routes/_layout.web.tsx +21 -0
  49. package/templates/universal/apps/__NAME__/routes/about.tsx +3 -0
  50. package/templates/universal/apps/__NAME__/routes/index.tsx +3 -0
  51. package/templates/universal/apps/__NAME__/src/app.ts +10 -0
  52. package/templates/universal/apps/__NAME__/tamagui.css +3 -0
  53. package/templates/universal/apps/__NAME__/tsconfig.json +8 -0
  54. package/templates/universal/apps/__NAME__/types.d.ts +9 -0
  55. package/templates/universal/apps/__NAME__/vite.config.ts +66 -0
  56. package/templates/universal/features/__NAME__/package.json +35 -0
  57. package/templates/universal/features/__NAME__/src/AboutScreen.tsx +27 -0
  58. package/templates/universal/features/__NAME__/src/AppLayout.tsx +50 -0
  59. package/templates/universal/features/__NAME__/src/ContactForm.spec.tsx +22 -0
  60. package/templates/universal/features/__NAME__/src/ContactForm.tsx +55 -0
  61. package/templates/universal/features/__NAME__/src/HomeScreen.tsx +40 -0
  62. package/templates/universal/features/__NAME__/src/index.ts +4 -0
  63. package/templates/universal/features/__NAME__/src/screens.spec.ts +12 -0
  64. package/templates/universal/features/__NAME__/tsconfig.json +5 -0
  65. package/templates/universal/features/__NAME__/vitest.config.mjs +7 -0
  66. package/templates/universal/gitignore +12 -0
  67. package/templates/universal/npmrc +3 -0
  68. package/templates/universal/oxlintrc.json +39 -0
  69. package/templates/universal/package.json +27 -0
  70. package/templates/universal/packages/config/config.json +4 -0
  71. package/templates/universal/packages/config/index.ts +5 -0
  72. package/templates/universal/packages/config/package.json +17 -0
  73. package/templates/universal/packages/config/tsconfig.json +5 -0
  74. package/templates/universal/packages/i18n/en/common.json +30 -0
  75. package/templates/universal/packages/i18n/es/common.json +30 -0
  76. package/templates/universal/packages/i18n/i18n.d.ts +16 -0
  77. package/templates/universal/packages/i18n/index.ts +26 -0
  78. package/templates/universal/packages/i18n/package.json +20 -0
  79. package/templates/universal/packages/i18n/resources.ts +13 -0
  80. package/templates/universal/packages/i18n/tsconfig.json +5 -0
  81. package/templates/universal/packages/themes/accent.ts +32 -0
  82. package/templates/universal/packages/themes/base.ts +50 -0
  83. package/templates/universal/packages/themes/fonts.ts +8 -0
  84. package/templates/universal/packages/themes/index.ts +33 -0
  85. package/templates/universal/packages/themes/package.json +23 -0
  86. package/templates/universal/packages/themes/tamagui.config.ts +14 -0
  87. package/templates/universal/packages/themes/tsconfig.json +5 -0
  88. package/templates/universal/pnpm-workspace.yaml +4 -0
  89. package/templates/universal/tsconfig.base.json +21 -0
  90. package/templates/universal/types/env.d.ts +9 -0
  91. package/types/bin/multiplatformOne.d.ts +11 -0
  92. package/types/bin/multiplatformOne.d.ts.map +1 -0
  93. package/types/commands/e2e.d.ts +14 -0
  94. package/types/commands/e2e.d.ts.map +1 -0
  95. package/types/commands/init.d.ts +16 -0
  96. package/types/commands/init.d.ts.map +1 -0
  97. package/types/commands/initApp.d.ts +25 -0
  98. package/types/commands/initApp.d.ts.map +1 -0
  99. package/types/generateVscode.d.ts +7 -0
  100. package/types/generateVscode.d.ts.map +1 -0
  101. package/types/index.d.ts +2 -0
  102. package/types/index.d.ts.map +1 -0
  103. package/types/types.d.ts +6 -0
  104. 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,17 @@
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 } from "../commands/initApp.mjs";
4
5
  import { createRequire } from "node:module";
5
6
  import fs from "node:fs/promises";
6
7
  import path from "node:path";
7
- import spawn from "nano-spawn";
8
- import yoctoSpinner from "yocto-spinner";
9
- import YAML from "yaml";
10
8
  import fsSync from "node:fs";
11
9
  import { fileURLToPath } from "node:url";
12
10
  import { formatServiceList, lookupProjectRoot, waitForFrappe, waitForKeycloak, waitForPostgres, waitServices } from "@multiplatform.one/utils/dev";
13
11
  import { Command } from "commander";
12
+ import spawn from "nano-spawn";
13
+ import YAML from "yaml";
14
+ import yoctoSpinner from "yocto-spinner";
14
15
 
15
16
  //#region \0rolldown/runtime.js
16
17
  var __create = Object.create;
@@ -469,12 +470,25 @@ async function getFrappeApps(root) {
469
470
  const program = new Command();
470
471
  program.name("mpo");
471
472
  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
473
+ 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) => {
474
+ if (options.monorepo) {
475
+ console.warn("⚠️ `mpo init --monorepo` clones the whole multiplatform.one monorepo (legacy). New projects should use the default consumer scaffold instead.");
476
+ if (await spawn("git", ["rev-parse", "--is-inside-work-tree"]).then(() => true, () => false)) throw new Error("mpo cannot be initialized inside a git repository");
477
+ const cloneScript = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../scripts/clone.sh");
478
+ await init(name, {
479
+ ...options,
480
+ cloneScript
481
+ });
482
+ return;
483
+ }
484
+ if (options.apps || options.services) throw new Error("--apps/--services select monorepo pieces and require --monorepo (legacy clone mode)");
485
+ const webOnly = Boolean(options.web || options.app);
486
+ if (webOnly && options.universal) throw new Error("Pass either --web/--app or --universal, not both");
487
+ await initApp(name, {
488
+ skipInstall: Boolean(options.skipInstall),
489
+ version: options.mpoVersion,
490
+ template: webOnly ? "app" : options.universal ? "universal" : void 0,
491
+ yes: Boolean(options.yes)
478
492
  });
479
493
  });
480
494
  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) => {
@@ -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,145 @@
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
+ try {
20
+ const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), "../../package.json");
21
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
22
+ if (pkg.version) return `^${pkg.version}`;
23
+ } catch {}
24
+ return "^6.1.0";
25
+ }
26
+ function templatesRoot(template) {
27
+ return resolve(dirname(fileURLToPath(import.meta.url)), "../../templates", template);
28
+ }
29
+ /**
30
+ * npm always strips .gitignore / .npmrc from published tarballs, so the
31
+ * template stores them without the leading dot; restore it at scaffold time.
32
+ */
33
+ const DOTFILE_NAMES = {
34
+ gitignore: ".gitignore",
35
+ npmrc: ".npmrc"
36
+ };
37
+ function render(content, vars) {
38
+ 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);
39
+ }
40
+ /**
41
+ * Assets copied byte-for-byte — running them through the utf-8 render pass
42
+ * would corrupt them (fonts ship in the templates for LC-33 stable mount).
43
+ */
44
+ const BINARY_EXTENSIONS = new Set([
45
+ ".woff2",
46
+ ".woff",
47
+ ".ttf",
48
+ ".otf",
49
+ ".png",
50
+ ".jpg",
51
+ ".ico"
52
+ ]);
53
+ function writeTree(srcDir, destDir, vars) {
54
+ if (!existsSync(srcDir)) throw new Error(`Project template missing at ${srcDir}`);
55
+ mkdirSync(destDir, { recursive: true });
56
+ for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
57
+ const from = join(srcDir, entry.name);
58
+ const to = join(destDir, render(DOTFILE_NAMES[entry.name] ?? entry.name, vars));
59
+ if (entry.isDirectory()) {
60
+ writeTree(from, to, vars);
61
+ continue;
62
+ }
63
+ if (BINARY_EXTENSIONS.has(extname(entry.name).toLowerCase())) {
64
+ copyFileSync(from, to);
65
+ continue;
66
+ }
67
+ writeFileSync(to, render(readFileSync(from, "utf-8"), vars));
68
+ }
69
+ }
70
+ async function resolveTemplate(options) {
71
+ if (options.template) return options.template;
72
+ if (options.yes || !process.stdout.isTTY) return "universal";
73
+ return (await inquirer.prompt([{
74
+ message: "Which platforms should this project target?",
75
+ name: "template",
76
+ type: "list",
77
+ choices: [{
78
+ name: "Universal — web + iOS + Android (one)",
79
+ value: "universal"
80
+ }, {
81
+ name: "Web only — Vite + React",
82
+ value: "app"
83
+ }],
84
+ default: "universal"
85
+ }])).template;
86
+ }
87
+ /**
88
+ * Scaffold a consumer project (universal by default, web-only with
89
+ * template: "app") that depends on @multiplatform.one/* at semver ranges
90
+ * (never workspace:*). This is NOT a monorepo fork.
91
+ */
92
+ async function initApp(nameArg, options = {}) {
93
+ let projectName = nameArg;
94
+ if (!projectName) if (options.yes || !process.stdout.isTTY) projectName = "my-app";
95
+ else projectName = (await inquirer.prompt([{
96
+ message: "What is the app name?",
97
+ name: "name",
98
+ type: "input",
99
+ default: "my-app"
100
+ }])).name;
101
+ const name = validateName(projectName);
102
+ const template = await resolveTemplate(options);
103
+ const targetDir = resolve(name);
104
+ if (existsSync(targetDir)) {
105
+ if (readdirSync(targetDir).length > 0) throw new Error(`Directory "${targetDir}" already exists and is not empty`);
106
+ }
107
+ const vars = {
108
+ NAME: name,
109
+ NAME_PASCAL: toPascalCase(name),
110
+ NAME_APPID: name.replaceAll("-", ""),
111
+ MPO_VERSION: resolveMpoVersion(options.version),
112
+ YEAR: String((/* @__PURE__ */ new Date()).getFullYear())
113
+ };
114
+ console.log(`\nScaffolding ${template === "universal" ? "universal (web + iOS + Android)" : "web-only"} project at ${targetDir}`);
115
+ console.log(` apps/${name}`);
116
+ console.log(` features/${name}`);
117
+ console.log(` packages/config`);
118
+ if (template === "universal") {
119
+ console.log(` packages/i18n`);
120
+ console.log(` packages/themes`);
121
+ }
122
+ console.log(` @multiplatform.one/* → ${vars.MPO_VERSION}\n`);
123
+ writeTree(templatesRoot(template), targetDir, vars);
124
+ if (!options.skipInstall) {
125
+ console.log("Installing dependencies...");
126
+ await spawn("pnpm", ["install"], {
127
+ cwd: targetDir,
128
+ stdio: "inherit"
129
+ });
130
+ } else console.log("Skipped pnpm install (--skip-install).");
131
+ console.log(`\n✅ Project created at ${targetDir}`);
132
+ console.log("\nNext steps:");
133
+ console.log(` cd ${name}`);
134
+ if (options.skipInstall) console.log(" pnpm install");
135
+ console.log(" pnpm dev # web dev server");
136
+ if (template === "universal") {
137
+ console.log(" pnpm ios # iOS (requires pnpm dev in another terminal)");
138
+ console.log(" pnpm android # Android");
139
+ }
140
+ console.log("\nThis project consumes published @multiplatform.one/* packages.");
141
+ console.log("It is not a fork of the multiplatform.one monorepo.");
142
+ }
143
+
144
+ //#endregion
145
+ export { initApp };
@@ -41,14 +41,30 @@ function launchConfigForApp(app) {
41
41
  const { name, path: relPath, scripts } = app;
42
42
  const configs = [];
43
43
  const wf = "${workspaceFolder}";
44
- if (name === "vscode" && scripts.dev) configs.push({
45
- name: "vscode dev",
46
- type: "extensionHost",
44
+ if (scripts["dev:vscode"]) {
45
+ configs.push({
46
+ name: "vscode dev",
47
+ type: "extensionHost",
48
+ request: "launch",
49
+ args: ["--disable-extensions", `--extensionDevelopmentPath=${wf}/${relPath}/dist-vscode`],
50
+ cwd: `${wf}/${relPath}`,
51
+ outFiles: [`${wf}/${relPath}/dist-vscode/extension/*.js`],
52
+ preLaunchTask: "npm: dev:vscode"
53
+ });
54
+ configs.push({
55
+ name: "vscode dev",
56
+ type: "node-terminal",
57
+ request: "launch",
58
+ cwd: `${wf}/${relPath}`,
59
+ command: "pnpm dev:vscode"
60
+ });
61
+ }
62
+ if (scripts["dev:webext"]) configs.push({
63
+ name: "webext dev",
64
+ type: "node-terminal",
47
65
  request: "launch",
48
- args: ["--disable-extensions", `--extensionDevelopmentPath=${wf}/${relPath}`],
49
66
  cwd: `${wf}/${relPath}`,
50
- outFiles: [`${wf}/${relPath}/dist/extension/*.js`],
51
- preLaunchTask: "npm: dev"
67
+ command: "pnpm dev:webext"
52
68
  });
53
69
  if (scripts.dev) configs.push({
54
70
  name: `${name} dev`,
@@ -117,12 +133,13 @@ async function generateLaunchJson(root) {
117
133
  };
118
134
  }
119
135
  async function generateTasksJson(apps) {
120
- if (!apps.find((a) => a.name === "vscode")?.scripts?.dev) return null;
136
+ const hostApp = apps.find((a) => a.scripts?.["dev:vscode"]);
137
+ if (!hostApp) return null;
121
138
  return {
122
139
  version: "2.0.0",
123
140
  tasks: [{
124
141
  type: "npm",
125
- script: "dev",
142
+ script: "dev:vscode",
126
143
  problemMatcher: {
127
144
  owner: "typescript",
128
145
  fileLocation: "relative",
@@ -146,7 +163,7 @@ async function generateTasksJson(apps) {
146
163
  kind: "build",
147
164
  isDefault: true
148
165
  },
149
- options: { cwd: "${workspaceFolder}/apps/vscode" }
166
+ options: { cwd: `\${workspaceFolder}/${hostApp.path}` }
150
167
  }]
151
168
  };
152
169
  }
@@ -160,7 +177,7 @@ async function generateVscodeConfig(root) {
160
177
  if (tasks) {
161
178
  await fs.writeFile(path.join(vsCodeDir, "tasks.json"), JSON.stringify(tasks, null, 2), "utf8");
162
179
  console.log("Wrote .vscode/tasks.json");
163
- } else console.log("Skipped .vscode/tasks.json (no apps/vscode with dev script)");
180
+ } else console.log("Skipped .vscode/tasks.json (no app with a dev:vscode script)");
164
181
  }
165
182
 
166
183
  //#endregion
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "@multiplatform.one/cli",
3
- "version": "6.1.0",
4
- "description": "multiplatform.one cli",
5
- "keywords": [],
3
+ "version": "6.3.0",
4
+ "description": "multiplatform.one cli — mpo init / create-multiplatform-app",
5
+ "keywords": [
6
+ "create-multiplatform-app",
7
+ "mpo",
8
+ "multiplatform.one",
9
+ "scaffold"
10
+ ],
6
11
  "bugs": {
7
12
  "url": "https://gitlab.com/bitspur/multiplatform.one/multiplatform.one/issues"
8
13
  },
@@ -16,9 +21,11 @@
16
21
  ],
17
22
  "repository": {
18
23
  "type": "git",
19
- "url": "https://gitlab.com/bitspur/multiplatform.one/multiplatform.one.git"
24
+ "url": "git+https://gitlab.com/bitspur/frappe/multiplatform.one.git",
25
+ "directory": "public/cli"
20
26
  },
21
27
  "bin": {
28
+ "create-multiplatform-app": "./bin/createMultiplatformApp.mjs",
22
29
  "mpo": "./bin/multiplatformOne.mjs"
23
30
  },
24
31
  "source": "src/index.ts",
@@ -27,19 +34,21 @@
27
34
  "lib",
28
35
  "scripts",
29
36
  "src",
30
- "types"
37
+ "templates",
38
+ "types",
39
+ "!types/.tsbuildinfo"
31
40
  ],
32
41
  "type": "module",
33
42
  "module": "lib/index.mjs",
34
43
  "types": "types/index.d.ts",
35
44
  "exports": {
36
45
  ".": {
37
- "import": "./lib/index.mjs",
38
- "types": "./types/index.d.ts"
46
+ "types": "./types/index.d.ts",
47
+ "import": "./lib/index.mjs"
39
48
  },
40
49
  "./bin/multiplatformOne": {
41
- "import": "./lib/bin/multiplatformOne.mjs",
42
- "types": "./types/bin/multiplatformOne.d.ts"
50
+ "types": "./types/bin/multiplatformOne.d.ts",
51
+ "import": "./lib/bin/multiplatformOne.mjs"
43
52
  },
44
53
  "./package.json": "./package.json"
45
54
  },
@@ -52,18 +61,20 @@
52
61
  "nano-spawn": "^2.1.0",
53
62
  "yaml": "^2.8.3",
54
63
  "yocto-spinner": "^1.1.0",
55
- "@multiplatform.one/utils": "6.1.0"
64
+ "@multiplatform.one/utils": "6.3.0"
56
65
  },
57
66
  "devDependencies": {
58
67
  "@types/inquirer": "^9.0.9",
59
68
  "@types/node": "^25.6.0",
60
- "typescript": "~5.9.3"
69
+ "typescript": "~5.9.3",
70
+ "vitest": "^4.1.5"
61
71
  },
62
72
  "engines": {
63
73
  "node": ">= 16.0"
64
74
  },
65
75
  "scripts": {
66
- "build": "rm -rf lib types 2>/dev/null && tsc -b --emitDeclarationOnly && tsdown",
76
+ "build": "rm -rf lib types 2>/dev/null && tsc -p tsconfig.build.json && tsdown",
77
+ "test": "vitest run",
67
78
  "typecheck": "tsc --noEmit"
68
79
  }
69
80
  }
@@ -19,6 +19,7 @@ import YAML from "yaml";
19
19
  import yoctoSpinner from "yocto-spinner";
20
20
  import { discoverE2EApp, runE2ESession } from "../commands/e2e";
21
21
  import { init, runModifyStep } from "../commands/init";
22
+ import { initApp } from "../commands/initApp";
22
23
 
23
24
  const projectRoot = lookupProjectRoot();
24
25
  const availableServices = ["api", "frappe", "solana", "ethereum", "sui"];
@@ -216,25 +217,62 @@ program.version(
216
217
 
217
218
  program
218
219
  .command("init")
219
- .option("-c, --checkout <branch>", "branch, tag or commit to checkout", "main")
220
- .option("-a, --apps <apps>", "comma-separated apps to include")
221
- .option("-s, --services <services>", "comma-separated services to include")
220
+ .option("--universal", "scaffold the universal template (web + iOS + Android; default)")
221
+ .option("--web", "scaffold the web-only template (Vite + React)")
222
+ .option("--app", "alias of --web (back-compat)")
223
+ .option("--skip-install", "skip pnpm install after scaffolding")
224
+ .option(
225
+ "--mpo-version <range>",
226
+ "semver range for @multiplatform.one/* (default: ^<cli version>)",
227
+ )
228
+ .option("-y, --yes", "non-interactive: accept defaults instead of prompting")
229
+ .option(
230
+ "--monorepo",
231
+ "DEPRECATED: clone/modify the full multiplatform.one monorepo instead of scaffolding a consumer project",
232
+ )
233
+ .option("-c, --checkout <branch>", "branch, tag or commit to checkout (--monorepo only)", "main")
234
+ .option("-a, --apps <apps>", "comma-separated apps to include (--monorepo only)")
235
+ .option("-s, --services <services>", "comma-separated services to include (--monorepo only)")
222
236
  .argument("[name]", "the name of the project")
223
- .description("clone multiplatform.one and apply selected apps/services")
237
+ .description(
238
+ "scaffold a new multiplatform project (universal web + native by default; --web for web-only; semver @multiplatform.one/* deps)",
239
+ )
224
240
  .action(async (name: string | undefined, options) => {
225
- if (
226
- await spawn("git", ["rev-parse", "--is-inside-work-tree"]).then(
227
- () => true,
228
- () => false,
229
- )
230
- ) {
231
- throw new Error("mpo cannot be initialized inside a git repository");
241
+ if (options.monorepo) {
242
+ console.warn(
243
+ "⚠️ `mpo init --monorepo` clones the whole multiplatform.one monorepo (legacy). " +
244
+ "New projects should use the default consumer scaffold instead.",
245
+ );
246
+ if (
247
+ await spawn("git", ["rev-parse", "--is-inside-work-tree"]).then(
248
+ () => true,
249
+ () => false,
250
+ )
251
+ ) {
252
+ throw new Error("mpo cannot be initialized inside a git repository");
253
+ }
254
+ const cloneScript = path.resolve(
255
+ path.dirname(fileURLToPath(import.meta.url)),
256
+ "../../scripts/clone.sh",
257
+ );
258
+ await init(name, { ...options, cloneScript });
259
+ return;
232
260
  }
233
- const cloneScript = path.resolve(
234
- path.dirname(fileURLToPath(import.meta.url)),
235
- "../../scripts/clone.sh",
236
- );
237
- await init(name, { ...options, cloneScript });
261
+ if (options.apps || options.services) {
262
+ throw new Error(
263
+ "--apps/--services select monorepo pieces and require --monorepo (legacy clone mode)",
264
+ );
265
+ }
266
+ const webOnly = Boolean(options.web || options.app);
267
+ if (webOnly && options.universal) {
268
+ throw new Error("Pass either --web/--app or --universal, not both");
269
+ }
270
+ await initApp(name, {
271
+ skipInstall: Boolean(options.skipInstall),
272
+ version: options.mpoVersion,
273
+ template: webOnly ? "app" : options.universal ? "universal" : undefined,
274
+ yes: Boolean(options.yes),
275
+ });
238
276
  });
239
277
 
240
278
  const defaultUpdateRemote = "https://gitlab.com/bitspur/multiplatform.one/multiplatform.one.git";