@multiplatform.one/cli 7.11.0 → 7.16.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 +6 -6
  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,431 +0,0 @@
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
- /**
9
- * Optional composable pieces overlaid onto the base template (additive
10
- * fragments under templates/pieces/<piece>/ — the inverse of the old
11
- * cookiecutter KEEP_SERVICES/KEEP_PLATFORMS removal hook). Keep sorted:
12
- * pieces are recorded sorted in provenance and overlaid in this order.
13
- */
14
- const INIT_APP_PIECES = [
15
- "frappe",
16
- "gnome",
17
- "keycloak",
18
- "tauri",
19
- "vscode",
20
- "webext"
21
- ];
22
- const PIECE_DESCRIPTIONS = {
23
- frappe: "Frappe backend wiring (env keys, provider, docker compose bench)",
24
- gnome: "GNOME desktop target (GTK4/GJS via react-gnome — native widgets, no webview)",
25
- keycloak: "Keycloak auth wiring (env keys, provider, docker compose + realm)",
26
- tauri: "Tauri desktop target (src-tauri skeleton — webview shell, Rust toolchain)",
27
- vscode: "VS Code extension target (extension host + Tamagui webview)",
28
- webext: "Browser extension target (MV3 popup + background)"
29
- };
30
- function toPascalCase(name) {
31
- return name.split(/[-_\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
32
- }
33
- function validateName(name) {
34
- const trimmed = name.trim();
35
- if (!trimmed) throw new Error("Project name is required");
36
- 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).`);
37
- return trimmed;
38
- }
39
- /** Root package.json name with any @scope/ prefix stripped (adopt +
40
- * update-bootstrap provenance name derivation). */
41
- function packageJsonName(projectDir) {
42
- try {
43
- return JSON.parse(readFileSync(join(projectDir, "package.json"), "utf-8")).name?.replace(/^@[^/]+\//u, "");
44
- } catch {
45
- return;
46
- }
47
- }
48
- function resolveMpoVersion(explicit) {
49
- if (explicit) return explicit.startsWith("^") || explicit.startsWith("~") || explicit === "*" ? explicit : `^${explicit}`;
50
- const version = cliVersion();
51
- return version ? `^${version}` : "^6.1.0";
52
- }
53
- function cliVersion() {
54
- try {
55
- const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), "../../package.json");
56
- return JSON.parse(readFileSync(pkgPath, "utf-8")).version;
57
- } catch {
58
- return;
59
- }
60
- }
61
- const PROVENANCE_FILE = ".mpo.json";
62
- function writeProvenance(targetDir, provenance) {
63
- writeFileSync(join(targetDir, PROVENANCE_FILE), `${JSON.stringify({
64
- ...provenance,
65
- pieces: [...provenance.pieces].sort()
66
- }, null, 2)}\n`);
67
- }
68
- function readProvenance(projectDir) {
69
- const file = join(projectDir, PROVENANCE_FILE);
70
- if (!existsSync(file)) return void 0;
71
- try {
72
- const raw = JSON.parse(readFileSync(file, "utf-8"));
73
- if (!raw.template || !raw.cliVersion || !raw.name) return void 0;
74
- return {
75
- template: raw.template,
76
- cliVersion: raw.cliVersion,
77
- name: raw.name,
78
- mpoVersion: raw.mpoVersion ?? `^${raw.cliVersion}`,
79
- pieces: Array.isArray(raw.pieces) ? [...raw.pieces].sort() : [],
80
- ...raw.reconciled === false ? { reconciled: false } : {}
81
- };
82
- } catch {
83
- return;
84
- }
85
- }
86
- function templatesRoot(template) {
87
- return resolve(dirname(fileURLToPath(import.meta.url)), "../../templates", template);
88
- }
89
- function piecesRoot(piece) {
90
- return resolve(dirname(fileURLToPath(import.meta.url)), "../../templates/pieces", piece);
91
- }
92
- /**
93
- * Fragment source dirs for a piece + template: shared/ applies to every
94
- * template, <template>/ only to that one. A piece supports a template when
95
- * at least one of the two exists. Adopted projects (template "none") use the
96
- * universal fragment set — pieces ship universal-layout fragments
97
- * (apps/<name>/…), which is the layout adopted repos follow.
98
- */
99
- function pieceFragmentDirs(piece, template) {
100
- const effective = template === "none" ? "universal" : template;
101
- return [join(piecesRoot(piece), "shared"), join(piecesRoot(piece), effective)].filter((dir) => existsSync(dir));
102
- }
103
- /**
104
- * npm always strips .gitignore / .npmrc (and newer npms .env*) from published
105
- * tarballs, so the template stores them without the leading dot; restore it
106
- * at scaffold time.
107
- */
108
- const DOTFILE_NAMES = {
109
- gitignore: ".gitignore",
110
- npmrc: ".npmrc",
111
- "env.example": ".env.example"
112
- };
113
- function render(content, vars) {
114
- 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);
115
- }
116
- /**
117
- * Assets copied byte-for-byte — running them through the utf-8 render pass
118
- * would corrupt them (fonts ship in the templates for LC-33 stable mount).
119
- */
120
- const BINARY_EXTENSIONS = new Set([
121
- ".woff2",
122
- ".woff",
123
- ".ttf",
124
- ".otf",
125
- ".png",
126
- ".jpg",
127
- ".ico",
128
- ".icns"
129
- ]);
130
- function writeTree(srcDir, destDir, vars) {
131
- if (!existsSync(srcDir)) throw new Error(`Project template missing at ${srcDir}`);
132
- mkdirSync(destDir, { recursive: true });
133
- for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
134
- const from = join(srcDir, entry.name);
135
- const to = join(destDir, render(DOTFILE_NAMES[entry.name] ?? entry.name, vars));
136
- if (entry.isDirectory()) {
137
- writeTree(from, to, vars);
138
- continue;
139
- }
140
- if (BINARY_EXTENSIONS.has(extname(entry.name).toLowerCase())) {
141
- copyFileSync(from, to);
142
- continue;
143
- }
144
- writeFileSync(to, render(readFileSync(from, "utf-8"), vars));
145
- }
146
- }
147
- function isJsonObject(value) {
148
- return typeof value === "object" && value !== null && !Array.isArray(value);
149
- }
150
- /**
151
- * Deep-merge for JSON partials: objects merge recursively (fragment keys
152
- * append after base keys, so e.g. package.json script order stays stable),
153
- * arrays union (base order first — config.json public keys accumulate
154
- * across pieces), scalars/type-mismatches take the fragment value.
155
- */
156
- function mergeJsonValues(base, fragment) {
157
- if (Array.isArray(base) && Array.isArray(fragment)) {
158
- const merged = [...base];
159
- const seen = new Set(base.map((item) => JSON.stringify(item)));
160
- for (const item of fragment) {
161
- const key = JSON.stringify(item);
162
- if (seen.has(key)) continue;
163
- seen.add(key);
164
- merged.push(item);
165
- }
166
- return merged;
167
- }
168
- if (isJsonObject(base) && isJsonObject(fragment)) {
169
- const merged = { ...base };
170
- for (const [key, value] of Object.entries(fragment)) merged[key] = key in merged ? mergeJsonValues(merged[key], value) : value;
171
- return merged;
172
- }
173
- return fragment;
174
- }
175
- /** Dependency maps stay alphabetically sorted after fragment merges (the
176
- * base templates are sorted; multiple pieces appending must not depend on
177
- * overlay order for the final byte content). */
178
- const SORTED_DEPENDENCY_FIELDS = [
179
- "dependencies",
180
- "devDependencies",
181
- "peerDependencies",
182
- "optionalDependencies"
183
- ];
184
- function sortDependencyFields(value) {
185
- if (!isJsonObject(value)) return value;
186
- const result = { ...value };
187
- for (const field of SORTED_DEPENDENCY_FIELDS) {
188
- const deps = result[field];
189
- if (!isJsonObject(deps)) continue;
190
- result[field] = Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
191
- }
192
- return result;
193
- }
194
- /**
195
- * Apply a rendered `<target>.partial` fragment onto the target file:
196
- * - `.json` targets deep-merge (see mergeJsonValues); package.json-style
197
- * dependency maps are re-sorted for deterministic output
198
- * - everything else appends as a text block separated by a blank line
199
- * Missing targets are created from the fragment alone.
200
- */
201
- function applyPartial(targetPath, renderedFragment) {
202
- if (targetPath.endsWith(".json")) {
203
- const fragment = JSON.parse(renderedFragment);
204
- const base = existsSync(targetPath) ? JSON.parse(readFileSync(targetPath, "utf-8")) : void 0;
205
- const merged = sortDependencyFields(base === void 0 ? fragment : mergeJsonValues(base, fragment));
206
- writeFileSync(targetPath, `${JSON.stringify(merged, null, 2)}\n`);
207
- return;
208
- }
209
- const block = `${renderedFragment.replace(/^\n+/u, "").replace(/\n+$/u, "")}\n`;
210
- if (!existsSync(targetPath)) {
211
- writeFileSync(targetPath, block);
212
- return;
213
- }
214
- writeFileSync(targetPath, `${readFileSync(targetPath, "utf-8").replace(/\n+$/u, "")}\n\n${block}`);
215
- }
216
- const PARTIAL_SUFFIX = ".partial";
217
- /**
218
- * Overlay a piece fragment tree onto the scaffold: regular files render and
219
- * overwrite, `<target>.partial` files merge into (or create) their target
220
- * (deep-merge for .json, append for text — see applyPartial).
221
- */
222
- function overlayTree(srcDir, destDir, vars) {
223
- mkdirSync(destDir, { recursive: true });
224
- for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
225
- const from = join(srcDir, entry.name);
226
- if (entry.isDirectory()) {
227
- overlayTree(from, join(destDir, render(DOTFILE_NAMES[entry.name] ?? entry.name, vars)), vars);
228
- continue;
229
- }
230
- if (entry.name.endsWith(PARTIAL_SUFFIX)) {
231
- const targetName = entry.name.slice(0, -8);
232
- applyPartial(join(destDir, render(DOTFILE_NAMES[targetName] ?? targetName, vars)), render(readFileSync(from, "utf-8"), vars));
233
- continue;
234
- }
235
- const to = join(destDir, render(DOTFILE_NAMES[entry.name] ?? entry.name, vars));
236
- if (BINARY_EXTENSIONS.has(extname(entry.name).toLowerCase())) {
237
- copyFileSync(from, to);
238
- continue;
239
- }
240
- writeFileSync(to, render(readFileSync(from, "utf-8"), vars));
241
- }
242
- }
243
- /**
244
- * Validate + canonicalize a piece selection for a template: unknown names
245
- * throw, duplicates collapse, order is always sorted (provenance and overlay
246
- * order must be deterministic), and every piece must ship a fragment for the
247
- * chosen template.
248
- */
249
- function normalizePieces(pieces, template) {
250
- const known = new Set(INIT_APP_PIECES);
251
- const normalized = [...new Set(pieces)].sort();
252
- for (const piece of normalized) {
253
- if (!known.has(piece)) throw new Error(`Unknown piece "${piece}". Available pieces: ${INIT_APP_PIECES.join(", ")}`);
254
- if (pieceFragmentDirs(piece, template).length === 0) throw new Error(`Piece "${piece}" is not available for the ${template} template (pieces currently target the universal template)`);
255
- }
256
- return normalized;
257
- }
258
- /** Pieces that can be offered for a template (fragment exists for it). */
259
- function availablePieces(template) {
260
- return INIT_APP_PIECES.filter((piece) => pieceFragmentDirs(piece, template).length > 0);
261
- }
262
- /** Human-readable piece summaries (adopt + init prompts). */
263
- function pieceDescription(piece) {
264
- return PIECE_DESCRIPTIONS[piece];
265
- }
266
- async function resolvePieces(options, template) {
267
- if (options.pieces) return normalizePieces(options.pieces, template);
268
- if (options.yes || !process.stdout.isTTY) return [];
269
- const choices = availablePieces(template);
270
- if (choices.length === 0) return [];
271
- return normalizePieces((await inquirer.prompt([{
272
- message: "Add optional pieces? (space to toggle, enter to confirm)",
273
- name: "pieces",
274
- type: "checkbox",
275
- choices: choices.map((piece) => ({
276
- name: `${piece} — ${PIECE_DESCRIPTIONS[piece]}`,
277
- value: piece,
278
- checked: false
279
- }))
280
- }])).pieces, template);
281
- }
282
- /**
283
- * Generate apps/<name>/src/app.ts for the selected backend pieces instead of
284
- * storing 2^n template variants: each selected provider adds one createApp
285
- * key (frappe/keycloak read their runtime settings from the packages/config
286
- * public env keys their fragments register). Only called when at least one
287
- * provider piece is selected — the base template file stays otherwise.
288
- */
289
- function composeAppTs(pieces) {
290
- const frappe = pieces.includes("frappe");
291
- const keycloak = pieces.includes("keycloak");
292
- const lines = ["import { createApp } from \"@multiplatform.one/core\";"];
293
- if (frappe || keycloak) lines.push("import { config } from \"@multiplatform.one/platform\";");
294
- lines.push("import { themeConfig } from \"@package/themes\";");
295
- lines.push("", "/**", " * App-wide provider stack. Providers below were enabled by the pieces", " * selected at `mpo init` time — see CreateAppConfig in", " * @multiplatform.one/core for everything else that can be wired here", " * (tanstack, web3, custom providers).", " */", "export const { AppProvider, useAppConfig, withApp } = createApp({", " theme: themeConfig,");
296
- if (frappe) lines.push(" // Frappe backend (mpo init --frappe): FrappeProvider mounts when", " // FRAPPE_ENABLED=1 (a packages/config public env key, loaded from", " // .env — see .env.example). FRAPPE_URL overrides the bench URL", " // (default: BASE_URL, then http://localhost:8000);", " // docker/compose.frappe.yaml runs a dev bench.", " frappe: { baseURL: config.get(\"FRAPPE_URL\") },");
297
- if (keycloak) lines.push(" // Keycloak auth (mpo init --keycloak): KeycloakProvider mounts when", " // KEYCLOAK_ENABLED=1. Realm/client settings come from the KEYCLOAK_*", " // env keys (packages/config, .env.example);", " // docker/compose.keycloak.yaml runs a dev server with an imported", " // realm.", " keycloak: { baseUrl: config.get(\"KEYCLOAK_BASE_URL\") },");
298
- lines.push("});", "");
299
- return lines.join("\n");
300
- }
301
- async function resolveTemplate(options) {
302
- if (options.template) return options.template;
303
- if (options.yes || !process.stdout.isTTY) return "universal";
304
- return (await inquirer.prompt([{
305
- message: "Which platforms should this project target?",
306
- name: "template",
307
- type: "list",
308
- choices: [{
309
- name: "Universal — web + iOS + Android (one)",
310
- value: "universal"
311
- }, {
312
- name: "Web only — Vite + React",
313
- value: "app"
314
- }],
315
- default: "universal"
316
- }])).template;
317
- }
318
- /**
319
- * Scaffold a consumer project (universal by default, web-only with
320
- * template: "app") that depends on @multiplatform.one/* at semver ranges
321
- * (never workspace:*). This is NOT a monorepo fork.
322
- */
323
- async function initApp(nameArg, options = {}) {
324
- let projectName = nameArg;
325
- if (!projectName) if (options.yes || !process.stdout.isTTY) projectName = "my-app";
326
- else projectName = (await inquirer.prompt([{
327
- message: "What is the app name?",
328
- name: "name",
329
- type: "input",
330
- default: "my-app"
331
- }])).name;
332
- const name = validateName(projectName);
333
- const template = await resolveTemplate(options);
334
- const pieces = await resolvePieces(options, template);
335
- if (template === "none" && pieces.length === 0) throw new Error(`A pieces-only scaffold needs at least one piece (${INIT_APP_PIECES.map((piece) => `--${piece}`).join(", ")})`);
336
- const targetDir = resolve(name);
337
- if (existsSync(targetDir)) {
338
- if (readdirSync(targetDir).length > 0) throw new Error(`Directory "${targetDir}" already exists and is not empty`);
339
- }
340
- const vars = {
341
- NAME: name,
342
- NAME_PASCAL: toPascalCase(name),
343
- NAME_APPID: name.replaceAll("-", ""),
344
- MPO_VERSION: resolveMpoVersion(options.version),
345
- YEAR: String((/* @__PURE__ */ new Date()).getFullYear())
346
- };
347
- console.log(`\nScaffolding ${template === "universal" ? "universal (web + iOS + Android)" : template === "app" ? "web-only" : "pieces-only (adopted-project baseline)"} project at ${targetDir}`);
348
- if (template !== "none") {
349
- console.log(` apps/${name}`);
350
- console.log(` features/${name}`);
351
- console.log(` packages/config`);
352
- }
353
- if (template === "universal") {
354
- console.log(` packages/i18n`);
355
- console.log(` packages/themes`);
356
- }
357
- if (pieces.length) console.log(` pieces: ${pieces.join(", ")}`);
358
- console.log(` @multiplatform.one/* → ${vars.MPO_VERSION}\n`);
359
- if (template === "none") mkdirSync(targetDir, { recursive: true });
360
- else writeTree(templatesRoot(template), targetDir, vars);
361
- for (const piece of pieces) for (const fragmentDir of pieceFragmentDirs(piece, template)) overlayTree(fragmentDir, targetDir, vars);
362
- if (pieces.includes("frappe") || pieces.includes("keycloak")) {
363
- mkdirSync(join(targetDir, "apps", name, "src"), { recursive: true });
364
- writeFileSync(join(targetDir, "apps", name, "src/app.ts"), composeAppTs(pieces));
365
- }
366
- writeProvenance(targetDir, {
367
- template,
368
- cliVersion: cliVersion() ?? "0.0.0",
369
- name,
370
- mpoVersion: vars.MPO_VERSION,
371
- pieces
372
- });
373
- let scaffoldCommitted = false;
374
- if (!options.skipGit) {
375
- if (!await spawn("git", ["rev-parse", "--is-inside-work-tree"], { cwd: targetDir }).then(() => true, () => false)) {
376
- const label = template === "universal" ? "universal" : template === "app" ? "web-only" : "pieces-only";
377
- await spawn("git", ["init", "--quiet"], { cwd: targetDir });
378
- await spawn("git", ["add", "-A"], { cwd: targetDir });
379
- scaffoldCommitted = await spawn("git", [
380
- "commit",
381
- "--quiet",
382
- "-m",
383
- `feat: scaffold ${label} multiplatform.one app (mpo init)`
384
- ], { cwd: targetDir }).then(() => true, () => {
385
- console.warn("⚠️ git commit failed (missing git identity?) — repo left staged.");
386
- return false;
387
- });
388
- }
389
- }
390
- if (!options.skipInstall && template !== "none") {
391
- console.log("Installing dependencies...");
392
- await spawn("pnpm", ["install"], {
393
- cwd: targetDir,
394
- stdio: "inherit"
395
- });
396
- if (scaffoldCommitted) await spawn("git", [
397
- "add",
398
- "--",
399
- "pnpm-lock.yaml"
400
- ], { cwd: targetDir }).then(() => spawn("git", [
401
- "commit",
402
- "--amend",
403
- "--no-edit",
404
- "--quiet"
405
- ], { cwd: targetDir })).catch(() => {});
406
- } else if (options.skipInstall) console.log("Skipped pnpm install (--skip-install).");
407
- console.log(`\n✅ Project created at ${targetDir}`);
408
- if (template === "none") {
409
- console.log("\nPieces-only tree (adopted-project baseline) — not a runnable project.");
410
- return;
411
- }
412
- console.log("\nNext steps:");
413
- console.log(` cd ${name}`);
414
- if (options.skipInstall) console.log(" pnpm install");
415
- console.log(" pnpm dev # web dev server");
416
- if (template === "universal") {
417
- console.log(" pnpm ios # iOS (requires pnpm dev in another terminal)");
418
- console.log(" pnpm android # Android");
419
- }
420
- if (pieces.includes("frappe")) console.log(" docker compose -f docker/compose.frappe.yaml up -d # dev bench (see README)");
421
- if (pieces.includes("keycloak")) console.log(" docker compose -f docker/compose.keycloak.yaml up -d # dev keycloak");
422
- if (pieces.includes("gnome")) console.log(" pnpm gnome # GTK4 desktop app (requires gjs + gtk4 — see README)");
423
- if (pieces.includes("tauri")) console.log(" pnpm tauri:dev # Tauri desktop app (requires the Rust toolchain)");
424
- if (pieces.includes("vscode")) console.log(" pnpm build:vscode # stage the VS Code extension into apps dist-vscode/");
425
- if (pieces.includes("webext")) console.log(" pnpm build:webext # stage the browser extension into apps dist-webext/");
426
- console.log("\nThis project consumes published @multiplatform.one/* packages.");
427
- console.log("It is not a fork of the multiplatform.one monorepo.");
428
- }
429
-
430
- //#endregion
431
- export { INIT_APP_PIECES, PROVENANCE_FILE, availablePieces, cliVersion, composeAppTs, initApp, mergeJsonValues, normalizePieces, packageJsonName, pieceDescription, readProvenance, validateName, writeProvenance };
@@ -1,184 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
-
4
- //#region src/generateVscode.ts
5
- /**
6
- * Generate .vscode/launch.json and .vscode/tasks.json from discovered apps.
7
- * Invoked by `mpo update` after merging upstream.
8
- * Safe for subset projects: only includes configs for workspaces that exist.
9
- */
10
- async function readJson(filePath, fallback) {
11
- try {
12
- const raw = await fs.readFile(filePath, "utf8");
13
- return JSON.parse(raw);
14
- } catch {
15
- return fallback;
16
- }
17
- }
18
- async function getPresentApps(root) {
19
- const appsDir = path.join(root, "apps");
20
- try {
21
- const entries = (await fs.readdir(appsDir, { withFileTypes: true })).filter((e) => e.isDirectory());
22
- const out = [];
23
- for (const e of entries) {
24
- const pkgPath = path.join(appsDir, e.name, "package.json");
25
- try {
26
- await fs.access(pkgPath);
27
- const pkg = await readJson(pkgPath, {});
28
- out.push({
29
- name: e.name,
30
- path: path.join("apps", e.name),
31
- scripts: pkg.scripts || {}
32
- });
33
- } catch {}
34
- }
35
- return out;
36
- } catch {
37
- return [];
38
- }
39
- }
40
- function launchConfigForApp(app) {
41
- const { name, path: relPath, scripts } = app;
42
- const configs = [];
43
- const wf = "${workspaceFolder}";
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",
65
- request: "launch",
66
- cwd: `${wf}/${relPath}`,
67
- command: "pnpm dev:webext"
68
- });
69
- if (scripts.dev) configs.push({
70
- name: `${name} dev`,
71
- type: "node-terminal",
72
- request: "launch",
73
- cwd: `${wf}/${relPath}`,
74
- command: "pnpm dev"
75
- });
76
- if (scripts.storybook && name === "keycloak") configs.push({
77
- name: "keycloak storybook",
78
- type: "node-terminal",
79
- request: "launch",
80
- cwd: `${wf}/${relPath}`,
81
- command: "pnpm storybook"
82
- });
83
- if (scripts.localnet && name === "solana") configs.push({
84
- name: "solana localnet",
85
- type: "node-terminal",
86
- request: "launch",
87
- cwd: `${wf}/${relPath}`,
88
- command: "pnpm localnet"
89
- });
90
- return configs;
91
- }
92
- async function generateLaunchJson(root) {
93
- const configurations = (await getPresentApps(root)).flatMap(launchConfigForApp);
94
- const compounds = [];
95
- const frappeDev = configurations.find((c) => c.name === "frappe dev");
96
- const keycloakDev = configurations.find((c) => c.name === "keycloak dev");
97
- const oneDev = configurations.find((c) => c.name === "one dev");
98
- if (frappeDev && keycloakDev) compounds.push({
99
- name: "Backend",
100
- configurations: ["frappe dev", "keycloak dev"]
101
- });
102
- if (oneDev && keycloakDev) compounds.push({
103
- name: "Frontend",
104
- configurations: ["one dev", "keycloak dev"]
105
- });
106
- if (frappeDev && oneDev && keycloakDev) compounds.push({
107
- name: "Web",
108
- configurations: [
109
- "frappe dev",
110
- "one dev",
111
- "keycloak dev"
112
- ]
113
- });
114
- const storybookConfigs = configurations.filter((c) => [
115
- "storybook dev",
116
- "storybook-expo dev",
117
- "keycloak dev",
118
- "keycloak storybook"
119
- ].includes(c.name));
120
- if (storybookConfigs.length) compounds.push({
121
- name: "Storybook",
122
- configurations: storybookConfigs.map((c) => c.name)
123
- });
124
- const solanaConfigs = configurations.filter((c) => c.name?.startsWith("solana"));
125
- if (solanaConfigs.length) compounds.push({
126
- name: "Solana",
127
- configurations: solanaConfigs.map((c) => c.name)
128
- });
129
- return {
130
- version: "0.2.0",
131
- configurations,
132
- ...compounds.length ? { compounds } : {}
133
- };
134
- }
135
- async function generateTasksJson(apps) {
136
- const hostApp = apps.find((a) => a.scripts?.["dev:vscode"]);
137
- if (!hostApp) return null;
138
- return {
139
- version: "2.0.0",
140
- tasks: [{
141
- type: "npm",
142
- script: "dev:vscode",
143
- problemMatcher: {
144
- owner: "typescript",
145
- fileLocation: "relative",
146
- pattern: {
147
- regexp: "^([a-zA-Z]\\:/?([\\w\\-]/?)+\\.\\w+):(\\d+):(\\d+): (ERROR|WARNING)\\: (.*)$",
148
- file: 1,
149
- line: 3,
150
- column: 4,
151
- code: 5,
152
- message: 6
153
- },
154
- background: {
155
- activeOnStart: true,
156
- beginsPattern: ".*",
157
- endsPattern: ".*Local: *http://localhost[^\\s]*"
158
- }
159
- },
160
- isBackground: true,
161
- presentation: { reveal: "never" },
162
- group: {
163
- kind: "build",
164
- isDefault: true
165
- },
166
- options: { cwd: `\${workspaceFolder}/${hostApp.path}` }
167
- }]
168
- };
169
- }
170
- async function generateVscodeConfig(root) {
171
- const launch = await generateLaunchJson(root);
172
- const tasks = await generateTasksJson(await getPresentApps(root));
173
- const vsCodeDir = path.join(root, ".vscode");
174
- await fs.mkdir(vsCodeDir, { recursive: true });
175
- await fs.writeFile(path.join(vsCodeDir, "launch.json"), JSON.stringify(launch, null, 2), "utf8");
176
- console.log("Wrote .vscode/launch.json (%d configurations)", launch.configurations.length);
177
- if (tasks) {
178
- await fs.writeFile(path.join(vsCodeDir, "tasks.json"), JSON.stringify(tasks, null, 2), "utf8");
179
- console.log("Wrote .vscode/tasks.json");
180
- } else console.log("Skipped .vscode/tasks.json (no app with a dev:vscode script)");
181
- }
182
-
183
- //#endregion
184
- export { generateVscodeConfig };
package/scripts/clone.sh DELETED
@@ -1,17 +0,0 @@
1
- #!/bin/sh
2
- # Clone multiplatform.one repo into target directory.
3
- # Usage: clone.sh <remote_url> <branch> <target_dir>
4
- # Example: clone.sh https://gitlab.com/bitspur/frappe/multiplatform.one.git main ./my-app
5
-
6
- set -e
7
-
8
- if [ $# -lt 3 ]; then
9
- echo "Usage: $0 <remote_url> <branch> <target_dir>" >&2
10
- exit 1
11
- fi
12
-
13
- REMOTE_URL="$1"
14
- BRANCH="$2"
15
- TARGET_DIR="$3"
16
-
17
- git clone --depth 1 --branch "$BRANCH" "$REMOTE_URL" "$TARGET_DIR"
package/scripts/update.sh DELETED
@@ -1,46 +0,0 @@
1
- #!/bin/sh
2
- # Merge from upstream. Run from project root.
3
- # Usage: update.sh <remote_url> [branch]
4
- # Example: update.sh https://gitlab.com/bitspur/frappe/multiplatform.one.git main
5
-
6
- set -e
7
-
8
- _get_remote_name() {
9
- _BASE_NAME="multiplatform"
10
- _NUMBER="$(date +%s)"
11
- _REMOTE_NAME="$_BASE_NAME.$_NUMBER"
12
- while git remote | grep -q "^$_REMOTE_NAME$"; do
13
- _NUMBER="$((_NUMBER + 1))"
14
- _REMOTE_NAME="$_BASE_NAME.$_NUMBER"
15
- done
16
- echo "$_REMOTE_NAME"
17
- }
18
-
19
- if [ $# -lt 1 ]; then
20
- echo "Usage: $0 <remote_url> [branch]" >&2
21
- exit 1
22
- fi
23
-
24
- REMOTE_URL="$1"
25
- BRANCH="${2:-main}"
26
- _PROJECT_DIR="$(pwd)"
27
- _INITIAL_COMMIT="$(git rev-parse HEAD)"
28
- _REMOTE_NAME="$(_get_remote_name)"
29
-
30
- git remote add "$_REMOTE_NAME" "$REMOTE_URL"
31
- git fetch "$_REMOTE_NAME" "$BRANCH"
32
- git merge --allow-unrelated-histories "$_REMOTE_NAME/$BRANCH" || true
33
- git remote remove "$_REMOTE_NAME"
34
-
35
- if [ -f .updateignore ]; then
36
- while IFS= read -r pattern; do
37
- find . -name "$pattern" -exec git checkout "$_INITIAL_COMMIT" -- '{}' +
38
- done < .updateignore
39
- fi
40
- while IFS= read -r pattern; do
41
- find . -name "$pattern" -exec git checkout "$_INITIAL_COMMIT" -- '{}' +
42
- done <<EOF
43
- apps/storybook/.lostpixel/baseline
44
- pnpm-lock.yaml
45
- EOF
46
- pnpm lint