@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
@@ -0,0 +1,260 @@
1
+ import { cliVersion, initApp, readProvenance, writeProvenance } from "./initApp.mjs";
2
+ import { join } from "node:path";
3
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
4
+ import spawn from "nano-spawn";
5
+ import { tmpdir } from "node:os";
6
+
7
+ //#region src/commands/updateApp.ts
8
+ /**
9
+ * `mpo update` for scaffolded consumer projects — the modern successor of
10
+ * the old cookiecutter flow (init a fresh project, swap your .git in, merge
11
+ * your changes on top), rebuilt with a TRUE three-way merge:
12
+ *
13
+ * base = the ORIGINAL scaffold, regenerated from the CLI version recorded
14
+ * in .mpo.json at init time (published CLIs ship their templates,
15
+ * so `pnpm dlx @multiplatform.one/cli@<old> init` reproduces it)
16
+ * ours = the project's HEAD (all your custom changes)
17
+ * theirs= a fresh scaffold from the CURRENT CLI version
18
+ *
19
+ * `git merge-tree --write-tree --merge-base=<base>` merges template
20
+ * evolution with your customizations; conflicts land in the worktree with
21
+ * normal conflict markers. An `.updateignore` file (one pathspec per line)
22
+ * pins matching paths to your HEAD version, exactly like the old script.
23
+ */
24
+ async function git(projectDir, args, env) {
25
+ return spawn("git", args, {
26
+ cwd: projectDir,
27
+ env: env ? {
28
+ ...process.env,
29
+ ...env
30
+ } : void 0
31
+ });
32
+ }
33
+ async function gitOut(projectDir, args, env) {
34
+ return (await git(projectDir, args, env)).stdout.trim();
35
+ }
36
+ /** Import a directory tree into the project's object database and return a
37
+ * root-less commit for it (temp index; the worktree is never touched). */
38
+ async function commitTreeFromDir(projectDir, dir, message, parent) {
39
+ const indexFile = join(mkdtempSync(join(tmpdir(), "mpo-index-")), "index");
40
+ const env = {
41
+ GIT_INDEX_FILE: indexFile,
42
+ GIT_WORK_TREE: dir
43
+ };
44
+ await git(projectDir, [
45
+ "add",
46
+ "-A",
47
+ "--force",
48
+ "--",
49
+ "."
50
+ ], env);
51
+ const tree = await gitOut(projectDir, ["write-tree"], env);
52
+ const commit = await gitOut(projectDir, parent ? [
53
+ "commit-tree",
54
+ tree,
55
+ "-p",
56
+ parent,
57
+ "-m",
58
+ message
59
+ ] : [
60
+ "commit-tree",
61
+ tree,
62
+ "-m",
63
+ message
64
+ ]);
65
+ rmSync(indexFile, { force: true });
66
+ return commit;
67
+ }
68
+ /** Scaffold a project with a specific published CLI version into tmp and
69
+ * return the generated project dir. Falls back to the local generator when
70
+ * the requested version matches the running CLI (or dlx fails). */
71
+ async function scaffoldBaseline(version, name, template, mpoVersion) {
72
+ const parent = mkdtempSync(join(tmpdir(), "mpo-update-"));
73
+ const cleanup = () => rmSync(parent, {
74
+ recursive: true,
75
+ force: true
76
+ });
77
+ const current = cliVersion();
78
+ const templateFlag = template === "app" ? ["--web"] : ["--universal"];
79
+ if (version !== current) {
80
+ try {
81
+ await spawn("pnpm", [
82
+ "--package",
83
+ `@multiplatform.one/cli@${version}`,
84
+ "dlx",
85
+ "mpo",
86
+ "init",
87
+ name,
88
+ "--yes",
89
+ "--skip-install",
90
+ "--mpo-version",
91
+ mpoVersion,
92
+ ...templateFlag
93
+ ], {
94
+ cwd: parent,
95
+ stdio: "inherit"
96
+ });
97
+ const dir = join(parent, name);
98
+ if (existsSync(join(dir, "package.json"))) return {
99
+ dir,
100
+ cleanup
101
+ };
102
+ console.warn(`⚠️ dlx scaffold for @multiplatform.one/cli@${version} produced no project;`);
103
+ } catch {
104
+ console.warn(`⚠️ could not scaffold baseline with @multiplatform.one/cli@${version} (network?);`);
105
+ }
106
+ console.warn(" falling back to the current CLI's template as the merge base.");
107
+ }
108
+ const previousCwd = process.cwd();
109
+ process.chdir(parent);
110
+ try {
111
+ await initApp(name, {
112
+ skipInstall: true,
113
+ yes: true,
114
+ template,
115
+ version: mpoVersion
116
+ });
117
+ } finally {
118
+ process.chdir(previousCwd);
119
+ }
120
+ return {
121
+ dir: join(parent, name),
122
+ cleanup
123
+ };
124
+ }
125
+ async function updateApp(options = {}) {
126
+ const projectDir = await gitOut(process.cwd(), ["rev-parse", "--show-toplevel"]).catch(() => {
127
+ throw new Error("mpo update must run inside a git repository");
128
+ });
129
+ await git(projectDir, ["diff", "--quiet"]).catch(() => {
130
+ throw new Error("mpo update requires a clean working tree (unstaged changes present)");
131
+ });
132
+ await git(projectDir, [
133
+ "diff",
134
+ "--cached",
135
+ "--quiet"
136
+ ]).catch(() => {
137
+ throw new Error("mpo update requires a clean working tree (staged changes present)");
138
+ });
139
+ const provenance = readProvenance(projectDir);
140
+ if (!provenance) throw new Error(".mpo.json not found — this project predates scaffold provenance. Create it with { template, cliVersion, name } matching how the project was generated (cliVersion = the @multiplatform.one/cli that scaffolded it), then re-run `mpo update`.");
141
+ const currentVersion = cliVersion() ?? "0.0.0";
142
+ const targetMpoRange = options.version ? options.version.startsWith("^") || options.version.startsWith("~") ? options.version : `^${options.version}` : `^${currentVersion}`;
143
+ console.log(`\nmpo update: ${provenance.cliVersion} → ${currentVersion}`);
144
+ console.log(` template: ${provenance.template}`);
145
+ console.log(` project: ${provenance.name}\n`);
146
+ console.log(`Scaffolding merge base (cli@${provenance.cliVersion})...`);
147
+ const base = await scaffoldBaseline(provenance.cliVersion, provenance.name, provenance.template, provenance.mpoVersion);
148
+ console.log(`Scaffolding update target (cli@${currentVersion})...`);
149
+ const next = await scaffoldBaseline(currentVersion, provenance.name, provenance.template, targetMpoRange);
150
+ writeProvenance(next.dir, {
151
+ template: provenance.template,
152
+ cliVersion: currentVersion,
153
+ name: provenance.name,
154
+ mpoVersion: targetMpoRange
155
+ });
156
+ try {
157
+ const head = await gitOut(projectDir, ["rev-parse", "HEAD"]);
158
+ const baseCommit = await commitTreeFromDir(projectDir, base.dir, `mpo scaffold ${provenance.template}@${provenance.cliVersion}`);
159
+ const nextCommit = await commitTreeFromDir(projectDir, next.dir, `mpo scaffold ${provenance.template}@${currentVersion}`, baseCommit);
160
+ let mergedTree;
161
+ let conflicts = [];
162
+ try {
163
+ mergedTree = (await gitOut(projectDir, [
164
+ "merge-tree",
165
+ "--write-tree",
166
+ "--name-only",
167
+ `--merge-base=${baseCommit}`,
168
+ head,
169
+ nextCommit
170
+ ])).split("\n")[0].trim();
171
+ } catch (err) {
172
+ const lines = (err.stdout ?? "").split("\n").filter(Boolean);
173
+ if (!lines.length) throw err;
174
+ mergedTree = lines[0].trim();
175
+ conflicts = lines.slice(1).map((line) => line.trim());
176
+ }
177
+ const finalProvenance = {
178
+ template: provenance.template,
179
+ cliVersion: currentVersion,
180
+ name: provenance.name,
181
+ mpoVersion: targetMpoRange
182
+ };
183
+ if (!conflicts.length) {
184
+ const message = `chore: mpo update ${provenance.cliVersion} → ${currentVersion}`;
185
+ await git(projectDir, [
186
+ "update-ref",
187
+ "HEAD",
188
+ await gitOut(projectDir, [
189
+ "commit-tree",
190
+ mergedTree,
191
+ "-p",
192
+ head,
193
+ "-p",
194
+ nextCommit,
195
+ "-m",
196
+ message
197
+ ])
198
+ ]);
199
+ await git(projectDir, [
200
+ "reset",
201
+ "--hard",
202
+ "HEAD"
203
+ ]);
204
+ writeProvenance(projectDir, finalProvenance);
205
+ await git(projectDir, [
206
+ "add",
207
+ "--",
208
+ ".mpo.json"
209
+ ]);
210
+ await git(projectDir, [
211
+ "commit",
212
+ "--amend",
213
+ "--no-edit",
214
+ "--quiet"
215
+ ]);
216
+ const amended = await gitOut(projectDir, [
217
+ "rev-parse",
218
+ "--short",
219
+ "HEAD"
220
+ ]);
221
+ console.log(`\n✅ merged cleanly → ${amended} ("${message}")`);
222
+ } else {
223
+ await git(projectDir, [
224
+ "read-tree",
225
+ "-u",
226
+ "--reset",
227
+ mergedTree
228
+ ]);
229
+ writeProvenance(projectDir, finalProvenance);
230
+ console.log("\n⚠️ merge conflicts — resolve the markers, then commit:");
231
+ for (const file of conflicts) console.log(` ${file}`);
232
+ console.log(`\n git add -A && git commit -m "chore: mpo update ${provenance.cliVersion} → ${currentVersion}"`);
233
+ }
234
+ const pins = ["pnpm-lock.yaml"];
235
+ const updateignore = join(projectDir, ".updateignore");
236
+ if (existsSync(updateignore)) for (const line of readFileSync(updateignore, "utf-8").split("\n")) {
237
+ const pattern = line.trim();
238
+ if (pattern && !pattern.startsWith("#")) pins.push(pattern);
239
+ }
240
+ for (const pin of pins) await git(projectDir, [
241
+ "checkout",
242
+ head,
243
+ "--",
244
+ pin
245
+ ]).catch(() => {});
246
+ if (!options.skipInstall) {
247
+ console.log("\nInstalling dependencies...");
248
+ await spawn("pnpm", ["install"], {
249
+ cwd: projectDir,
250
+ stdio: "inherit"
251
+ });
252
+ }
253
+ } finally {
254
+ base.cleanup();
255
+ next.cleanup();
256
+ }
257
+ }
258
+
259
+ //#endregion
260
+ export { updateApp };
@@ -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.4.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.4.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,8 @@ 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, readProvenance } from "../commands/initApp";
23
+ import { updateApp } from "../commands/updateApp";
22
24
 
23
25
  const projectRoot = lookupProjectRoot();
24
26
  const availableServices = ["api", "frappe", "solana", "ethereum", "sui"];
@@ -216,35 +218,84 @@ program.version(
216
218
 
217
219
  program
218
220
  .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")
221
+ .option("--universal", "scaffold the universal template (web + iOS + Android; default)")
222
+ .option("--web", "scaffold the web-only template (Vite + React)")
223
+ .option("--app", "alias of --web (back-compat)")
224
+ .option("--skip-install", "skip pnpm install after scaffolding")
225
+ .option(
226
+ "--mpo-version <range>",
227
+ "semver range for @multiplatform.one/* (default: ^<cli version>)",
228
+ )
229
+ .option("-y, --yes", "non-interactive: accept defaults instead of prompting")
230
+ .option(
231
+ "--monorepo",
232
+ "DEPRECATED: clone/modify the full multiplatform.one monorepo instead of scaffolding a consumer project",
233
+ )
234
+ .option("-c, --checkout <branch>", "branch, tag or commit to checkout (--monorepo only)", "main")
235
+ .option("-a, --apps <apps>", "comma-separated apps to include (--monorepo only)")
236
+ .option("-s, --services <services>", "comma-separated services to include (--monorepo only)")
222
237
  .argument("[name]", "the name of the project")
223
- .description("clone multiplatform.one and apply selected apps/services")
238
+ .description(
239
+ "scaffold a new multiplatform project (universal web + native by default; --web for web-only; semver @multiplatform.one/* deps)",
240
+ )
224
241
  .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");
242
+ if (options.monorepo) {
243
+ console.warn(
244
+ "⚠️ `mpo init --monorepo` clones the whole multiplatform.one monorepo (legacy). " +
245
+ "New projects should use the default consumer scaffold instead.",
246
+ );
247
+ if (
248
+ await spawn("git", ["rev-parse", "--is-inside-work-tree"]).then(
249
+ () => true,
250
+ () => false,
251
+ )
252
+ ) {
253
+ throw new Error("mpo cannot be initialized inside a git repository");
254
+ }
255
+ const cloneScript = path.resolve(
256
+ path.dirname(fileURLToPath(import.meta.url)),
257
+ "../../scripts/clone.sh",
258
+ );
259
+ await init(name, { ...options, cloneScript });
260
+ return;
232
261
  }
233
- const cloneScript = path.resolve(
234
- path.dirname(fileURLToPath(import.meta.url)),
235
- "../../scripts/clone.sh",
236
- );
237
- await init(name, { ...options, cloneScript });
262
+ if (options.apps || options.services) {
263
+ throw new Error(
264
+ "--apps/--services select monorepo pieces and require --monorepo (legacy clone mode)",
265
+ );
266
+ }
267
+ const webOnly = Boolean(options.web || options.app);
268
+ if (webOnly && options.universal) {
269
+ throw new Error("Pass either --web/--app or --universal, not both");
270
+ }
271
+ await initApp(name, {
272
+ skipInstall: Boolean(options.skipInstall),
273
+ version: options.mpoVersion,
274
+ template: webOnly ? "app" : options.universal ? "universal" : undefined,
275
+ yes: Boolean(options.yes),
276
+ });
238
277
  });
239
278
 
240
279
  const defaultUpdateRemote = "https://gitlab.com/bitspur/multiplatform.one/multiplatform.one.git";
241
280
 
242
281
  program
243
282
  .command("update")
244
- .option("-c, --checkout <branch>", "branch, tag or commit to merge from upstream", "main")
245
- .option("-r, --remote <url>", "upstream remote URL", defaultUpdateRemote)
246
- .description("merge upstream and re-apply workspace config")
247
- .action(async (options: { checkout: string; remote: string }) => {
283
+ .option("-c, --checkout <branch>", "branch, tag or commit to merge from upstream (monorepo forks)", "main")
284
+ .option("-r, --remote <url>", "upstream remote URL (monorepo forks)", defaultUpdateRemote)
285
+ .option("--skip-install", "skip pnpm install after the update")
286
+ .option(
287
+ "--mpo-version <range>",
288
+ "semver range for @multiplatform.one/* (default: ^<cli version>)",
289
+ )
290
+ .description(
291
+ "update a scaffolded project to the current template (three-way merge via .mpo.json provenance); monorepo forks fall back to the upstream merge flow",
292
+ )
293
+ .action(async (options: {
294
+ checkout: string;
295
+ remote: string;
296
+ skipInstall?: boolean;
297
+ mpoVersion?: string;
298
+ }) => {
248
299
  // Update prechecks: must be in a git repo
249
300
  if (
250
301
  await spawn("git", ["rev-parse", "--is-inside-work-tree"]).then(
@@ -254,6 +305,18 @@ program
254
305
  ) {
255
306
  throw new Error("mpo cannot be updated outside of a git repository");
256
307
  }
308
+ // Scaffolded consumer projects carry .mpo.json provenance — use the
309
+ // copier-style three-way template update. Monorepo forks fall through to
310
+ // the legacy upstream-merge flow below.
311
+ if (readProvenance(projectRoot)) {
312
+ await updateApp({ skipInstall: options.skipInstall, version: options.mpoVersion });
313
+ try {
314
+ await generateVscodeConfig(projectRoot);
315
+ } catch {
316
+ // best-effort
317
+ }
318
+ return;
319
+ }
257
320
  // Require clean working tree (no staged or unstaged changes)
258
321
  if (
259
322
  await spawn("git", ["diff", "--cached", "--quiet"]).then(
@@ -11,15 +11,10 @@ const services = ["frappe", "solana", "ethereum", "sui"] as const;
11
11
  function serviceDir(name: string): string {
12
12
  return `apps/${name}`;
13
13
  }
14
- const apps = [
15
- "one",
16
- "keycloak",
17
- "storybook",
18
- "storybook-expo",
19
- "vscode",
20
- "webext",
21
- "vocs",
22
- ] as const;
14
+ // vscode + webext are no longer standalone apps: they are delivery targets
15
+ // of the One platform app (apps/one/vscode, apps/one/webext) and ride with
16
+ // the "one" selection.
17
+ const apps = ["one", "keycloak", "storybook", "storybook-expo", "vocs"] as const;
23
18
 
24
19
  type Service = (typeof services)[number];
25
20
  type App = (typeof apps)[number];
@@ -34,12 +29,10 @@ const serviceEnvPrefixes: Record<Service, string[]> = {
34
29
 
35
30
  /** env variable prefixes to remove per app */
36
31
  const appEnvPrefixes: Record<App, string[]> = {
37
- one: ["ONE_"],
32
+ one: ["ONE_", "VSCODE_", "WEBEXT_"],
38
33
  keycloak: ["KEYCLOAK_"],
39
34
  storybook: ["STORYBOOK_", "VR_DIFFING_ENGINE="],
40
35
  "storybook-expo": [],
41
- vscode: ["VSCODE_"],
42
- webext: ["WEBEXT_"],
43
36
  vocs: ["VOCS_"],
44
37
  };
45
38
 
@@ -52,12 +45,10 @@ const serviceEnvSections: Record<Service, string[]> = {
52
45
  };
53
46
 
54
47
  const appEnvSections: Record<App, string[]> = {
55
- one: ["# one"],
48
+ one: ["# one", "# vscode", "# webext"],
56
49
  keycloak: ["# keycloak"],
57
50
  storybook: ["# storybook"],
58
51
  "storybook-expo": [],
59
- vscode: ["# vscode"],
60
- webext: ["# webext"],
61
52
  vocs: ["# vocs"],
62
53
  };
63
54
 
@@ -70,12 +61,10 @@ const serviceLaunchNames: Record<Service, string[]> = {
70
61
  };
71
62
 
72
63
  const appLaunchNames: Record<App, string[]> = {
73
- one: ["one dev"],
64
+ one: ["one dev", "vscode dev", "webext dev"],
74
65
  keycloak: ["keycloak dev", "keycloak storybook"],
75
66
  storybook: ["storybook dev"],
76
67
  "storybook-expo": ["storybook-expo dev"],
77
- vscode: ["vscode dev"],
78
- webext: ["webext dev"],
79
68
  vocs: ["vocs dev"],
80
69
  };
81
70