@multiplatform.one/cli 7.10.0 → 7.15.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 (163) hide show
  1. package/README.md +121 -111
  2. package/bin/createMultiplatformApp.mjs +39 -37
  3. package/lib/bin/multiplatformOne.mjs +65 -75
  4. package/lib/commands/addApp.mjs +78 -0
  5. package/lib/commands/adoptApp.mjs +11 -7
  6. package/lib/commands/doctypeTypes.mjs +148 -0
  7. package/lib/commands/e2e.mjs +2 -2
  8. package/lib/commands/init.mjs +142 -459
  9. package/lib/commands/merge.mjs +359 -0
  10. package/lib/commands/pieces.mjs +236 -0
  11. package/lib/commands/provenance.mjs +108 -0
  12. package/lib/commands/scaffold.mjs +105 -0
  13. package/lib/commands/updateApp.mjs +211 -528
  14. package/lib/source.json +5 -0
  15. package/lib/source.mjs +63 -0
  16. package/package.json +7 -7
  17. package/scripts/frappe-bootstrap.sh +29 -3
  18. package/scripts/frappe-dev.sh +1 -2
  19. package/scripts/frappe-helpers.sh +50 -2
  20. package/scripts/frappe.sh +5 -0
  21. package/scripts/scaffold.sh +937 -0
  22. package/scripts/stamp-source.mjs +51 -0
  23. package/scripts/stamp-source.spec.ts +86 -0
  24. package/src/bin/multiplatformOne.ts +116 -226
  25. package/src/commands/addApp.ts +102 -0
  26. package/src/commands/adoptApp.spec.ts +5 -1
  27. package/src/commands/adoptApp.ts +15 -10
  28. package/src/commands/doctypeTypes.spec.ts +155 -0
  29. package/src/commands/doctypeTypes.ts +214 -0
  30. package/src/commands/init.spec.ts +66 -0
  31. package/src/commands/init.ts +201 -643
  32. package/src/commands/merge.spec.ts +81 -0
  33. package/src/commands/merge.ts +411 -0
  34. package/src/commands/pieces.spec.ts +62 -0
  35. package/src/commands/pieces.ts +340 -0
  36. package/src/commands/provenance.spec.ts +92 -0
  37. package/src/commands/provenance.ts +169 -0
  38. package/src/commands/scaffold.ts +131 -0
  39. package/src/commands/updateApp.spec.ts +540 -331
  40. package/src/commands/updateApp.ts +255 -668
  41. package/src/source.spec.ts +45 -0
  42. package/src/source.ts +88 -0
  43. package/templates/AGENTS.md +161 -0
  44. package/templates/README.md +62 -0
  45. package/templates/pieces/frappe/universal/README.md.partial +7 -32
  46. package/templates/pieces/frappe/universal/env.example.partial +3 -3
  47. package/templates/pieces/frappe-bench/universal/README.md.partial +28 -0
  48. package/templates/pieces/{frappe → frappe-bench}/universal/docker/compose.frappe.yaml +2 -2
  49. package/templates/pieces/tauri/universal/README.md.partial +7 -5
  50. package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/src/dev.rs +463 -0
  51. package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/src/lib.rs +6 -0
  52. package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/tauri.conf.json +2 -1
  53. package/templates/pieces/webext/universal/apps/__NAME__/package.json.partial +1 -0
  54. package/templates/pieces/webext/universal/apps/__NAME__/vite.config.webext.ts +6 -2
  55. package/types/bin/multiplatformOne.d.ts.map +1 -1
  56. package/types/commands/addApp.d.ts +20 -0
  57. package/types/commands/addApp.d.ts.map +1 -0
  58. package/types/commands/adoptApp.d.ts +4 -3
  59. package/types/commands/adoptApp.d.ts.map +1 -1
  60. package/types/commands/doctypeTypes.d.ts +40 -0
  61. package/types/commands/doctypeTypes.d.ts.map +1 -0
  62. package/types/commands/init.d.ts +30 -13
  63. package/types/commands/init.d.ts.map +1 -1
  64. package/types/commands/merge.d.ts +54 -0
  65. package/types/commands/merge.d.ts.map +1 -0
  66. package/types/commands/pieces.d.ts +54 -0
  67. package/types/commands/pieces.d.ts.map +1 -0
  68. package/types/commands/provenance.d.ts +52 -0
  69. package/types/commands/provenance.d.ts.map +1 -0
  70. package/types/commands/scaffold.d.ts +43 -0
  71. package/types/commands/scaffold.d.ts.map +1 -0
  72. package/types/commands/updateApp.d.ts +19 -27
  73. package/types/commands/updateApp.d.ts.map +1 -1
  74. package/types/source.d.ts +35 -0
  75. package/types/source.d.ts.map +1 -0
  76. package/lib/commands/initApp.mjs +0 -431
  77. package/lib/generateVscode.mjs +0 -184
  78. package/scripts/clone.sh +0 -17
  79. package/scripts/update.sh +0 -46
  80. package/src/commands/initApp.spec.ts +0 -720
  81. package/src/commands/initApp.ts +0 -706
  82. package/src/generateVscode.ts +0 -238
  83. package/templates/app/AGENTS.md +0 -142
  84. package/templates/app/README.md +0 -62
  85. package/templates/app/apps/__NAME__/index.html +0 -36
  86. package/templates/app/apps/__NAME__/package.json +0 -31
  87. package/templates/app/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
  88. package/templates/app/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
  89. package/templates/app/apps/__NAME__/src/App.tsx +0 -9
  90. package/templates/app/apps/__NAME__/src/main.tsx +0 -14
  91. package/templates/app/apps/__NAME__/tsconfig.json +0 -8
  92. package/templates/app/apps/__NAME__/vite.config.ts +0 -17
  93. package/templates/app/features/__NAME__/package.json +0 -34
  94. package/templates/app/features/__NAME__/src/HomeScreen.tsx +0 -70
  95. package/templates/app/features/__NAME__/src/index.ts +0 -1
  96. package/templates/app/features/__NAME__/tsconfig.json +0 -5
  97. package/templates/app/gitignore +0 -9
  98. package/templates/app/npmrc +0 -2
  99. package/templates/app/oxlintrc.json +0 -39
  100. package/templates/app/package.json +0 -38
  101. package/templates/app/packages/config/config.json +0 -4
  102. package/templates/app/packages/config/index.ts +0 -5
  103. package/templates/app/packages/config/package.json +0 -17
  104. package/templates/app/packages/config/tsconfig.json +0 -5
  105. package/templates/app/pnpm-workspace.yaml +0 -4
  106. package/templates/app/tsconfig.base.json +0 -16
  107. package/templates/universal/AGENTS.md +0 -142
  108. package/templates/universal/README.md +0 -97
  109. package/templates/universal/apps/__NAME__/app.config.js +0 -31
  110. package/templates/universal/apps/__NAME__/config/tamagui.config.ts +0 -4
  111. package/templates/universal/apps/__NAME__/package.json +0 -62
  112. package/templates/universal/apps/__NAME__/polyfills.native.ts +0 -23
  113. package/templates/universal/apps/__NAME__/polyfills.ts +0 -3
  114. package/templates/universal/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
  115. package/templates/universal/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
  116. package/templates/universal/apps/__NAME__/routes/_layout.tsx +0 -22
  117. package/templates/universal/apps/__NAME__/routes/_layout.web.tsx +0 -21
  118. package/templates/universal/apps/__NAME__/routes/about.tsx +0 -3
  119. package/templates/universal/apps/__NAME__/routes/index.tsx +0 -3
  120. package/templates/universal/apps/__NAME__/src/app.ts +0 -10
  121. package/templates/universal/apps/__NAME__/tamagui.css +0 -3
  122. package/templates/universal/apps/__NAME__/tsconfig.json +0 -8
  123. package/templates/universal/apps/__NAME__/types.d.ts +0 -9
  124. package/templates/universal/apps/__NAME__/vite.config.ts +0 -20
  125. package/templates/universal/features/__NAME__/package.json +0 -35
  126. package/templates/universal/features/__NAME__/src/AboutScreen.tsx +0 -27
  127. package/templates/universal/features/__NAME__/src/AppLayout.tsx +0 -50
  128. package/templates/universal/features/__NAME__/src/ContactForm.spec.tsx +0 -22
  129. package/templates/universal/features/__NAME__/src/ContactForm.tsx +0 -55
  130. package/templates/universal/features/__NAME__/src/HomeScreen.tsx +0 -40
  131. package/templates/universal/features/__NAME__/src/index.ts +0 -4
  132. package/templates/universal/features/__NAME__/src/screens.spec.ts +0 -12
  133. package/templates/universal/features/__NAME__/tsconfig.json +0 -5
  134. package/templates/universal/features/__NAME__/vitest.config.mjs +0 -7
  135. package/templates/universal/gitignore +0 -14
  136. package/templates/universal/npmrc +0 -3
  137. package/templates/universal/oxlintrc.json +0 -39
  138. package/templates/universal/package.json +0 -51
  139. package/templates/universal/packages/config/config.json +0 -4
  140. package/templates/universal/packages/config/index.ts +0 -5
  141. package/templates/universal/packages/config/package.json +0 -17
  142. package/templates/universal/packages/config/tsconfig.json +0 -5
  143. package/templates/universal/packages/i18n/en/common.json +0 -30
  144. package/templates/universal/packages/i18n/es/common.json +0 -30
  145. package/templates/universal/packages/i18n/i18n.d.ts +0 -16
  146. package/templates/universal/packages/i18n/index.ts +0 -26
  147. package/templates/universal/packages/i18n/package.json +0 -20
  148. package/templates/universal/packages/i18n/resources.ts +0 -13
  149. package/templates/universal/packages/i18n/tsconfig.json +0 -5
  150. package/templates/universal/packages/themes/accent.ts +0 -32
  151. package/templates/universal/packages/themes/base.ts +0 -50
  152. package/templates/universal/packages/themes/fonts.ts +0 -8
  153. package/templates/universal/packages/themes/index.ts +0 -33
  154. package/templates/universal/packages/themes/package.json +0 -23
  155. package/templates/universal/packages/themes/tamagui.config.ts +0 -14
  156. package/templates/universal/packages/themes/tsconfig.json +0 -5
  157. package/templates/universal/pnpm-workspace.yaml +0 -4
  158. package/templates/universal/tsconfig.base.json +0 -21
  159. package/templates/universal/types/env.d.ts +0 -9
  160. package/types/commands/initApp.d.ts +0 -114
  161. package/types/commands/initApp.d.ts.map +0 -1
  162. package/types/generateVscode.d.ts +0 -7
  163. package/types/generateVscode.d.ts.map +0 -1
@@ -1,484 +1,167 @@
1
- import { join, resolve } from "node:path";
2
- import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
1
+ import { mpoRange, validateName } from "./provenance.mjs";
2
+ import { resolveSource } from "../source.mjs";
3
+ import { ScaffoldError, runScaffold } from "./scaffold.mjs";
4
+ import { existsSync, readdirSync } from "node:fs";
5
+ import { basename, resolve } from "node:path";
6
+ import spawn from "nano-spawn";
3
7
  import inquirer from "inquirer";
4
- import { execSync } from "node:child_process";
5
8
 
6
9
  //#region src/commands/init.ts
7
- const repoUrl = "https://gitlab.com/bitspur/frappe/multiplatform.one.git";
8
- const services = [
9
- "frappe",
10
- "solana",
10
+ /**
11
+ * `mpo init`: a consumer project is the multiplatform.one monorepo at one
12
+ * commit, pruned. scripts/scaffold.sh does the work (clone or archive at
13
+ * the ref, drop public/ and the singleton apps not asked for, rename
14
+ * apps/one to each product app, rewrite workspace: ranges to semver); this
15
+ * command picks the commit, asks what to keep, and finishes the repo.
16
+ */
17
+ /** The apps that exist once per project; everything else under apps/ is a product app. */
18
+ const SINGLETON_APPS = [
11
19
  "ethereum",
12
- "sui"
13
- ];
14
- /** Resolve a service name to its relative directory from the project root. */
15
- function serviceDir(name) {
16
- return `apps/${name}`;
17
- }
18
- const apps = [
19
- "one",
20
+ "frappe",
20
21
  "keycloak",
22
+ "solana",
21
23
  "storybook",
22
24
  "storybook-expo",
25
+ "sui",
26
+ "uxpin",
23
27
  "vocs"
24
28
  ];
25
- /** env variable prefixes to remove per service */
26
- const serviceEnvPrefixes = {
27
- frappe: ["FRAPPE_", "MARIADB_"],
28
- solana: [],
29
- ethereum: [],
30
- sui: []
31
- };
32
- /** env variable prefixes to remove per app */
33
- const appEnvPrefixes = {
34
- one: [
35
- "ONE_",
36
- "VSCODE_",
37
- "WEBEXT_"
38
- ],
39
- keycloak: ["KEYCLOAK_"],
40
- storybook: ["STORYBOOK_", "VR_DIFFING_ENGINE="],
41
- "storybook-expo": [],
42
- vocs: ["VOCS_"]
43
- };
44
- /** env section headers (comments) to remove per app/service */
45
- const serviceEnvSections = {
46
- frappe: ["# frappe", "# mariadb"],
47
- solana: [],
48
- ethereum: [],
49
- sui: []
50
- };
51
- const appEnvSections = {
52
- one: [
53
- "# one",
54
- "# vscode",
55
- "# webext"
56
- ],
57
- keycloak: ["# keycloak"],
58
- storybook: ["# storybook"],
59
- "storybook-expo": [],
60
- vocs: ["# vocs"]
29
+ const SINGLETON_DESCRIPTIONS = {
30
+ ethereum: "Hardhat chain + contracts",
31
+ frappe: "Frappe bench (backend, docker compose)",
32
+ keycloak: "Keycloak theme + realm (auth, docker compose)",
33
+ solana: "Anchor programs + localnet",
34
+ storybook: "Storybook (web)",
35
+ "storybook-expo": "Storybook (native)",
36
+ sui: "Move packages + localnet",
37
+ uxpin: "UXPin Merge library",
38
+ vocs: "Docs site"
61
39
  };
62
- /** launch.json configuration names per service/app */
63
- const serviceLaunchNames = {
64
- frappe: ["frappe dev"],
65
- solana: ["solana dev", "solana localnet"],
66
- ethereum: ["ethereum dev"],
67
- sui: ["sui dev"]
68
- };
69
- const appLaunchNames = {
70
- one: [
71
- "one dev",
72
- "vscode dev",
73
- "webext dev"
74
- ],
75
- keycloak: ["keycloak dev", "keycloak storybook"],
76
- storybook: ["storybook dev"],
77
- "storybook-expo": ["storybook-expo dev"],
78
- vocs: ["vocs dev"]
79
- };
80
- /**
81
- * Shared modify step: remove unselected apps/services and rewrite configs.
82
- * Used by both init (with prompted selections) and update (with getPresentApps/getPresentServices).
83
- */
84
- function runModifyStep(dir, selectedServices, selectedApps) {
85
- const services = selectedServices;
86
- const apps = selectedApps;
87
- for (const service of services) if (!services.includes(service)) {
88
- const servicePath = join(dir, serviceDir(service));
89
- if (existsSync(servicePath)) {
90
- rmSync(servicePath, { recursive: true });
91
- console.log(`Removed service: ${service}`);
92
- }
93
- }
94
- for (const app of apps) if (!apps.includes(app)) {
95
- const appPath = join(dir, "apps", app);
96
- if (existsSync(appPath)) {
97
- rmSync(appPath, { recursive: true });
98
- console.log(`Removed app: ${app}`);
99
- }
100
- }
101
- const packagesDir = join(dir, "packages");
102
- if (existsSync(packagesDir)) {
103
- const versionMap = /* @__PURE__ */ new Map();
104
- for (const pkg of readdirSync(packagesDir)) {
105
- const pkgJsonPath = join(packagesDir, pkg, "package.json");
106
- if (existsSync(pkgJsonPath)) {
107
- const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
108
- if (pkgJson.name && pkgJson.version) versionMap.set(pkgJson.name, pkgJson.version);
109
- }
110
- }
111
- for (const pkg of readdirSync(packagesDir)) {
112
- const pkgJsonPath = join(packagesDir, pkg, "package.json");
113
- if (existsSync(pkgJsonPath)) {
114
- const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
115
- if (!pkgJson.private) {
116
- rmSync(join(packagesDir, pkg), { recursive: true });
117
- console.log(`Removed public package: ${pkgJson.name}`);
118
- }
119
- }
120
- }
121
- convertWorkspaceVersions(dir, versionMap);
122
- }
123
- cleanupPnpmWorkspace(dir, services, apps);
124
- cleanupLaunchJson(dir, services, apps);
125
- cleanupEnvFiles(dir, services, apps);
126
- cleanupTsconfigs(dir);
127
- cleanupMisc(dir);
128
- removeLicenseHeaders(dir);
40
+ function unique(values) {
41
+ return [...new Set(values)];
129
42
  }
130
- async function init(nameArg, options) {
131
- let projectName = nameArg;
132
- if (!projectName) projectName = (await inquirer.prompt([{
133
- message: "What is the project name?",
134
- name: "name",
135
- type: "input",
136
- default: "my-app"
137
- }])).name;
138
- let selectedServices;
139
- if (options.services) selectedServices = options.services.split(",").map((s) => s.trim());
140
- else selectedServices = (await inquirer.prompt([{
141
- message: "Select services:",
43
+ function validateServices(services) {
44
+ const known = new Set(SINGLETON_APPS);
45
+ for (const service of services) if (!known.has(service)) throw new Error(`Unknown service "${service}". Services are the singleton apps: ${SINGLETON_APPS.join(", ")}`);
46
+ return unique(services).sort();
47
+ }
48
+ async function promptServices() {
49
+ return validateServices((await inquirer.prompt([{
50
+ message: "Which singleton apps should the project keep? (space to toggle, enter to confirm)",
142
51
  name: "services",
143
52
  type: "checkbox",
144
- choices: services.map((s) => ({
145
- name: s,
146
- value: s
147
- }))
148
- }])).services;
149
- let selectedApps;
150
- if (options.apps) selectedApps = options.apps.split(",").map((s) => s.trim());
151
- else selectedApps = (await inquirer.prompt([{
152
- message: "Select apps:",
153
- name: "apps",
154
- type: "checkbox",
155
- choices: apps.map((a) => ({
156
- name: a,
157
- value: a,
158
- checked: a === "one"
53
+ choices: SINGLETON_APPS.map((service) => ({
54
+ name: `${service} — ${SINGLETON_DESCRIPTIONS[service]}`,
55
+ value: service,
56
+ checked: false
159
57
  }))
160
- }])).apps;
161
- const targetDir = resolve(projectName);
162
- if (existsSync(targetDir)) {
163
- if (readdirSync(targetDir).length > 0) throw new Error(`Directory "${targetDir}" already exists and is not empty`);
164
- }
165
- const branch = options.checkout || "main";
166
- console.log("\nCloning repository...");
167
- if (options.cloneScript) execSync(`sh "${options.cloneScript}" "${repoUrl}" "${branch}" "${targetDir}"`, { stdio: "inherit" });
168
- else execSync(`git clone --depth 1 --branch "${branch}" ${repoUrl} "${targetDir}"`, { stdio: "inherit" });
169
- rmSync(join(targetDir, ".git"), {
170
- recursive: true,
171
- force: true
172
- });
173
- runModifyStep(targetDir, selectedServices, selectedApps);
174
- const rootPkgPath = join(targetDir, "package.json");
175
- if (existsSync(rootPkgPath)) {
176
- const rootPkg = JSON.parse(readFileSync(rootPkgPath, "utf-8"));
177
- rootPkg.name = projectName;
178
- rootPkg.packageManager = void 0;
179
- writeFileSync(rootPkgPath, `${JSON.stringify(rootPkg, null, 2)}\n`);
180
- }
181
- const envDefaultPath = join(targetDir, ".env.example");
182
- const envPath = join(targetDir, ".env");
183
- if (existsSync(envDefaultPath) && !existsSync(envPath)) writeFileSync(envPath, readFileSync(envDefaultPath, "utf-8"));
184
- const lostpixelBaseline = join(targetDir, "apps", "storybook", ".lostpixel", "baseline");
185
- if (existsSync(lostpixelBaseline)) rmSync(lostpixelBaseline, { recursive: true });
186
- const lockfile = join(targetDir, "pnpm-lock.yaml");
187
- if (existsSync(lockfile)) rmSync(lockfile);
188
- for (const artifact of [
189
- "agent-os",
190
- "public/cli/scripts/init.sh",
191
- "public/cli/scripts/update.sh",
192
- ".changeset"
193
- ]) {
194
- const p = join(targetDir, artifact);
195
- if (existsSync(p)) rmSync(p, {
196
- recursive: true,
197
- force: true
198
- });
199
- }
200
- console.log("\nInstalling dependencies...");
201
- execSync("pnpm install", {
202
- cwd: targetDir,
203
- stdio: "inherit"
204
- });
205
- console.log("\nInitializing git repository...");
206
- execSync("git init", {
207
- cwd: targetDir,
208
- stdio: "inherit"
209
- });
210
- execSync("git add -A", {
211
- cwd: targetDir,
212
- stdio: "inherit"
213
- });
214
- execSync("git commit -m \"Initial commit from multiplatform.one\"", {
215
- cwd: targetDir,
216
- stdio: "inherit"
217
- });
218
- console.log(`\n✅ Project created at ${targetDir}`);
219
- console.log("\nNext steps:");
220
- console.log(` cd ${projectName}`);
221
- console.log(" pnpm dev");
58
+ }])).services);
222
59
  }
223
- function convertWorkspaceVersions(dir, versionMap) {
224
- const files = findPackageJsonFiles(dir);
225
- for (const file of files) {
226
- const content = JSON.parse(readFileSync(file, "utf-8"));
227
- let changed = false;
228
- for (const depType of [
229
- "dependencies",
230
- "devDependencies",
231
- "peerDependencies"
232
- ]) {
233
- const deps = content[depType];
234
- if (!deps) continue;
235
- for (const [depName, version] of Object.entries(deps)) if (typeof version === "string" && version.startsWith("workspace:")) {
236
- const realVersion = versionMap.get(depName);
237
- if (realVersion) {
238
- const workspaceRange = version.replace("workspace:", "");
239
- deps[depName] = workspaceRange === "*" || workspaceRange === "^" ? `^${realVersion}` : workspaceRange;
240
- changed = true;
241
- }
242
- }
243
- }
244
- if (changed) writeFileSync(file, `${JSON.stringify(content, null, 2)}\n`);
245
- }
60
+ async function insideGitRepo(dir) {
61
+ return spawn("git", ["rev-parse", "--is-inside-work-tree"], { cwd: dir }).then(() => true, () => false);
246
62
  }
247
- function findPackageJsonFiles(dir) {
248
- const results = [];
249
- function walk(d) {
250
- let entries;
251
- try {
252
- entries = readdirSync(d, { withFileTypes: true });
253
- } catch {
254
- return;
255
- }
256
- for (const entry of entries) {
257
- if (entry.name === "node_modules" || entry.name === ".git") continue;
258
- const full = join(d, entry.name);
259
- if (entry.isDirectory()) walk(full);
260
- else if (entry.name === "package.json") results.push(full);
261
- }
262
- }
263
- walk(dir);
264
- return results;
265
- }
266
- function cleanupPnpmWorkspace(dir, _services, _apps) {
267
- const wsPath = join(dir, "pnpm-workspace.yaml");
268
- if (!existsSync(wsPath)) return;
269
- let content = readFileSync(wsPath, "utf-8");
270
- const appsDir = join(dir, "apps");
271
- if (!existsSync(appsDir) || readdirSync(appsDir).length === 0) content = content.replace(/^\s*-\s*['"]?apps\/\*['"]?\s*$/gm, "");
272
- const pkgDir = join(dir, "packages");
273
- if (!existsSync(pkgDir) || readdirSync(pkgDir).length === 0) content = content.replace(/^\s*-\s*['"]?packages\/\*['"]?\s*$/gm, "");
274
- content = `${content.replace(/\n{3,}/g, "\n\n").trim()}\n`;
275
- writeFileSync(wsPath, content);
276
- }
277
- function cleanupLaunchJson(dir, services, apps) {
278
- const launchPath = join(dir, ".vscode", "launch.json");
279
- if (!existsSync(launchPath)) return;
280
- const launch = JSON.parse(readFileSync(launchPath, "utf-8"));
281
- const removedNames = /* @__PURE__ */ new Set();
282
- for (const service of services) if (!services.includes(service)) for (const configName of serviceLaunchNames[service]) removedNames.add(configName);
283
- for (const app of apps) if (!apps.includes(app)) for (const configName of appLaunchNames[app]) removedNames.add(configName);
284
- if (launch.configurations) launch.configurations = launch.configurations.filter((c) => !removedNames.has(c.name));
285
- if (launch.compounds) {
286
- for (const compound of launch.compounds) if (compound.configurations) compound.configurations = compound.configurations.filter((configName) => !removedNames.has(configName));
287
- launch.compounds = launch.compounds.filter((c) => c.configurations && c.configurations.length > 0);
63
+ async function scaffoldWithFallback(spec, request) {
64
+ try {
65
+ return await runScaffold({
66
+ ...request,
67
+ source: spec.source,
68
+ ref: spec.ref
69
+ });
70
+ } catch (err) {
71
+ if (spec.origin !== "tag" || !(err instanceof ScaffoldError)) throw err;
72
+ console.warn(`⚠️ ${err.message}; retrying from main`);
73
+ return runScaffold({
74
+ ...request,
75
+ source: spec.source,
76
+ ref: "main"
77
+ });
288
78
  }
289
- writeFileSync(launchPath, `${JSON.stringify(launch, null, 2)}\n`);
290
79
  }
291
- function cleanupEnvFiles(dir, services, apps) {
292
- for (const envFile of [".env.example", ".env"]) {
293
- const filePath = join(dir, envFile);
294
- if (!existsSync(filePath)) continue;
295
- let lines = readFileSync(filePath, "utf-8").split("\n");
296
- for (const service of services) if (!services.includes(service)) {
297
- const prefixes = serviceEnvPrefixes[service];
298
- const sections = serviceEnvSections[service];
299
- lines = filterEnvLines(lines, prefixes, sections);
300
- }
301
- for (const app of apps) if (!apps.includes(app)) {
302
- const prefixes = appEnvPrefixes[app];
303
- const sections = appEnvSections[app];
304
- lines = filterEnvLines(lines, prefixes, sections);
305
- }
306
- writeFileSync(filePath, `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trim()}\n`);
80
+ async function init(nameArg, options = {}) {
81
+ const interactive = !options.yes && process.stdout.isTTY;
82
+ let targetDir;
83
+ let name;
84
+ if (options.into) {
85
+ targetDir = resolve(nameArg ?? ".");
86
+ name = validateName(options.apps?.[0] ?? basename(targetDir));
87
+ } else {
88
+ let projectName = nameArg;
89
+ if (!projectName) if (!interactive) projectName = "my-app";
90
+ else projectName = (await inquirer.prompt([{
91
+ message: "What is the app name?",
92
+ name: "name",
93
+ type: "input",
94
+ default: "my-app"
95
+ }])).name;
96
+ name = validateName(projectName);
97
+ targetDir = resolve(name);
307
98
  }
308
- }
309
- function filterEnvLines(lines, prefixes, sections) {
310
- return lines.filter((line) => {
311
- const trimmed = line.trim();
312
- for (const section of sections) if (trimmed === section) return false;
313
- for (const prefix of prefixes) if (prefix.endsWith("=")) {
314
- if (trimmed.startsWith(prefix)) return false;
315
- } else if (trimmed.startsWith(prefix) || trimmed.startsWith(`# ${prefix}`)) return false;
316
- return true;
99
+ const apps = unique([name, ...(options.apps ?? []).map(validateName)]);
100
+ let services;
101
+ if (options.services) services = validateServices(options.services);
102
+ else if (interactive) services = await promptServices();
103
+ else services = [];
104
+ if (existsSync(targetDir) && readdirSync(targetDir).length > 0 && !options.into) throw new Error(`Directory "${targetDir}" already exists and is not empty. Pass --into to scaffold into it (paths that already exist are kept and pinned in .updateignore), or pick another name.`);
105
+ const spec = resolveSource({
106
+ source: options.source,
107
+ checkout: options.checkout
317
108
  });
318
- }
319
- function cleanupTsconfigs(dir) {
320
- const tsconfigFiles = findTsconfigFiles(dir);
321
- for (const file of tsconfigFiles) try {
322
- const content = readFileSync(file, "utf-8");
323
- const json = JSON.parse(stripJsonComments(content));
324
- if (!json.references) continue;
325
- let changed = false;
326
- json.references = json.references.filter((ref) => {
327
- const refPath = resolve(join(file, "..", ref.path));
328
- const pathExists = existsSync(refPath) || existsSync(`${refPath}.json`) || existsSync(join(refPath, "tsconfig.json"));
329
- if (!pathExists) changed = true;
330
- return pathExists;
109
+ if (spec.warning) console.warn(`⚠️ ${spec.warning}`);
110
+ console.log(`\nScaffolding ${targetDir} from ${spec.source} @ ${spec.ref}`);
111
+ console.log(` apps ${apps.join(", ")}`);
112
+ console.log(` services ${services.length ? services.join(", ") : "(none)"}`);
113
+ const sha = (await scaffoldWithFallback(spec, {
114
+ source: spec.source,
115
+ ref: spec.ref,
116
+ target: targetDir,
117
+ apps,
118
+ services,
119
+ mpoVersion: options.mpoVersion ? mpoRange(options.mpoVersion) : void 0,
120
+ into: options.into,
121
+ verbose: options.verbose
122
+ })).source.sha;
123
+ console.log(` source ${sha}`);
124
+ let scaffoldCommitted = false;
125
+ if (!options.skipGit && !await insideGitRepo(targetDir)) {
126
+ await spawn("git", ["init", "--quiet"], { cwd: targetDir });
127
+ await spawn("git", ["add", "-A"], { cwd: targetDir });
128
+ scaffoldCommitted = await spawn("git", [
129
+ "commit",
130
+ "--quiet",
131
+ "-m",
132
+ `feat: scaffold from multiplatform.one ${sha.slice(0, 12)} (mpo init)`
133
+ ], { cwd: targetDir }).then(() => true, () => {
134
+ console.warn("⚠️ git commit failed (missing git identity?) — repo left staged.");
135
+ return false;
331
136
  });
332
- if (changed) writeFileSync(file, `${JSON.stringify(json, null, 2)}\n`);
333
- } catch {}
334
- }
335
- function findTsconfigFiles(dir) {
336
- const results = [];
337
- function walk(d) {
338
- let entries;
339
- try {
340
- entries = readdirSync(d, { withFileTypes: true });
341
- } catch {
342
- return;
343
- }
344
- for (const entry of entries) {
345
- if (entry.name === "node_modules" || entry.name === ".git") continue;
346
- const full = join(d, entry.name);
347
- if (entry.isDirectory()) walk(full);
348
- else if (entry.name.startsWith("tsconfig") && entry.name.endsWith(".json")) results.push(full);
349
- }
350
- }
351
- walk(dir);
352
- return results;
353
- }
354
- function stripJsonComments(str) {
355
- return str.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
356
- }
357
- function cleanupMisc(dir) {
358
- for (const subdir of ["docker/dns"]) {
359
- const p = join(dir, subdir);
360
- if (existsSync(p)) rmSync(p, { recursive: true });
361
137
  }
362
- const settingsPath = join(dir, ".vscode", "settings.json");
363
- if (existsSync(settingsPath)) try {
364
- const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
365
- const psiKeys = Object.keys(settings).filter((k) => k.startsWith("psi-header"));
366
- const cleaned = { ...settings };
367
- for (const key of psiKeys) cleaned[key] = void 0;
368
- const filtered = JSON.parse(JSON.stringify(cleaned));
369
- writeFileSync(settingsPath, `${JSON.stringify(filtered, null, 2)}\n`);
370
- } catch {}
371
- const extPath = join(dir, ".vscode", "extensions.json");
372
- if (existsSync(extPath)) try {
373
- const ext = JSON.parse(readFileSync(extPath, "utf-8"));
374
- if (ext.recommendations) ext.recommendations = ext.recommendations.filter((r) => r !== "psioniq.psi-header");
375
- writeFileSync(extPath, `${JSON.stringify(ext, null, 2)}\n`);
376
- } catch {}
377
- }
378
- const licenseIndicators = [
379
- "File:",
380
- "Project:",
381
- "File Created:",
382
- "Author:",
383
- "Licensed under"
384
- ];
385
- function removeLicenseHeaders(dir) {
386
- const files = findSourceFiles(dir);
387
- for (const file of files) try {
388
- const content = readFileSync(file, "utf-8");
389
- const match = content.match(/^\s*\/\*[\s\S]*?\*\//);
390
- if (match) {
391
- const comment = match[0];
392
- if (licenseIndicators.some((indicator) => comment.includes(indicator)) && match.index !== void 0) writeFileSync(file, content.slice(match.index + comment.length).replace(/^\s*\n/, ""));
393
- }
394
- } catch {}
395
- const hashFiles = findHashCommentFiles(dir);
396
- for (const file of hashFiles) try {
397
- const lines = readFileSync(file, "utf-8").split("\n");
398
- let headerEnd = 0;
399
- let inHeader = false;
400
- for (let i = 0; i < lines.length; i++) {
401
- const line = lines[i];
402
- if (i === 0 && line.startsWith("#!")) continue;
403
- if (line.startsWith("# File:") || line.startsWith("# Project:") || line.startsWith("# File Created:") || line.startsWith("# Author:")) {
404
- inHeader = true;
405
- headerEnd = i + 1;
406
- continue;
407
- }
408
- if (inHeader && line.startsWith("#")) {
409
- headerEnd = i + 1;
410
- continue;
411
- }
412
- if (inHeader && line.trim() === "") {
413
- headerEnd = i + 1;
414
- break;
415
- }
416
- if (inHeader) break;
417
- }
418
- if (inHeader && headerEnd > 0) writeFileSync(file, `${lines[0].startsWith("#!") ? `${lines[0]}\n\n` : ""}${lines.slice(headerEnd).join("\n").replace(/^\s*\n/, "")}`);
419
- } catch {}
420
- }
421
- function findSourceFiles(dir) {
422
- const results = [];
423
- const extensions = new Set([
424
- ".ts",
425
- ".tsx",
426
- ".js",
427
- ".jsx",
428
- ".mts",
429
- ".mjs",
430
- ".cts",
431
- ".cjs"
432
- ]);
433
- function walk(d) {
434
- let entries;
435
- try {
436
- entries = readdirSync(d, { withFileTypes: true });
437
- } catch {
438
- return;
439
- }
440
- for (const entry of entries) {
441
- if (entry.name === "node_modules" || entry.name === ".git") continue;
442
- const full = join(d, entry.name);
443
- if (entry.isDirectory()) walk(full);
444
- else {
445
- const ext = `.${entry.name.split(".").pop()}`;
446
- if (extensions.has(ext)) results.push(full);
447
- }
448
- }
449
- }
450
- walk(dir);
451
- return results;
452
- }
453
- function findHashCommentFiles(dir) {
454
- const results = [];
455
- const extensions = new Set([
456
- ".sh",
457
- ".mk",
458
- ".yaml",
459
- ".yml",
460
- ".py"
461
- ]);
462
- function walk(d) {
463
- let entries;
464
- try {
465
- entries = readdirSync(d, { withFileTypes: true });
466
- } catch {
467
- return;
468
- }
469
- for (const entry of entries) {
470
- if (entry.name === "node_modules" || entry.name === ".git") continue;
471
- const full = join(d, entry.name);
472
- if (entry.isDirectory()) walk(full);
473
- else {
474
- const ext = `.${entry.name.split(".").pop()}`;
475
- if (extensions.has(ext) || entry.name === "Makefile") results.push(full);
476
- }
477
- }
478
- }
479
- walk(dir);
480
- return results;
138
+ if (!options.skipInstall) {
139
+ console.log("\nInstalling dependencies...");
140
+ await spawn("pnpm", ["install"], {
141
+ cwd: targetDir,
142
+ stdio: "inherit"
143
+ });
144
+ if (scaffoldCommitted) await spawn("git", [
145
+ "add",
146
+ "--",
147
+ "pnpm-lock.yaml"
148
+ ], { cwd: targetDir }).then(() => spawn("git", [
149
+ "commit",
150
+ "--amend",
151
+ "--no-edit",
152
+ "--quiet"
153
+ ], { cwd: targetDir })).catch(() => {});
154
+ } else console.log("Skipped pnpm install (--skip-install).");
155
+ console.log(`\n✅ Project created at ${targetDir}`);
156
+ console.log("\nNext steps:");
157
+ if (!options.into) console.log(` cd ${basename(targetDir)}`);
158
+ if (options.skipInstall) console.log(" pnpm install");
159
+ if (options.into || options.skipGit) console.log(" git add -A && git commit -m \"feat: scaffold from multiplatform.one (mpo init)\"");
160
+ console.log(" pnpm dev # web + native dev server");
161
+ if (services.includes("frappe")) console.log(" make frappe/dev # Frappe bench (see apps/frappe)");
162
+ console.log("\nLater: `mpo update` merges monorepo changes into the project without committing;");
163
+ console.log("`mpo add-app <name>` adds another product app.");
481
164
  }
482
165
 
483
166
  //#endregion
484
- export { init, runModifyStep };
167
+ export { SINGLETON_APPS, init, validateServices };