@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.
- package/README.md +121 -111
- package/bin/createMultiplatformApp.mjs +39 -37
- package/lib/bin/multiplatformOne.mjs +65 -75
- package/lib/commands/addApp.mjs +78 -0
- package/lib/commands/adoptApp.mjs +11 -7
- package/lib/commands/doctypeTypes.mjs +148 -0
- package/lib/commands/e2e.mjs +2 -2
- package/lib/commands/init.mjs +142 -459
- package/lib/commands/merge.mjs +359 -0
- package/lib/commands/pieces.mjs +236 -0
- package/lib/commands/provenance.mjs +108 -0
- package/lib/commands/scaffold.mjs +105 -0
- package/lib/commands/updateApp.mjs +211 -528
- package/lib/source.json +5 -0
- package/lib/source.mjs +63 -0
- package/package.json +7 -7
- package/scripts/frappe-bootstrap.sh +29 -3
- package/scripts/frappe-dev.sh +1 -2
- package/scripts/frappe-helpers.sh +50 -2
- package/scripts/frappe.sh +5 -0
- package/scripts/scaffold.sh +937 -0
- package/scripts/stamp-source.mjs +51 -0
- package/scripts/stamp-source.spec.ts +86 -0
- package/src/bin/multiplatformOne.ts +116 -226
- package/src/commands/addApp.ts +102 -0
- package/src/commands/adoptApp.spec.ts +5 -1
- package/src/commands/adoptApp.ts +15 -10
- package/src/commands/doctypeTypes.spec.ts +155 -0
- package/src/commands/doctypeTypes.ts +214 -0
- package/src/commands/init.spec.ts +66 -0
- package/src/commands/init.ts +201 -643
- package/src/commands/merge.spec.ts +81 -0
- package/src/commands/merge.ts +411 -0
- package/src/commands/pieces.spec.ts +62 -0
- package/src/commands/pieces.ts +340 -0
- package/src/commands/provenance.spec.ts +92 -0
- package/src/commands/provenance.ts +169 -0
- package/src/commands/scaffold.ts +131 -0
- package/src/commands/updateApp.spec.ts +540 -331
- package/src/commands/updateApp.ts +255 -668
- package/src/source.spec.ts +45 -0
- package/src/source.ts +88 -0
- package/templates/AGENTS.md +161 -0
- package/templates/README.md +62 -0
- package/templates/pieces/frappe/universal/README.md.partial +7 -32
- package/templates/pieces/frappe/universal/env.example.partial +3 -3
- package/templates/pieces/frappe-bench/universal/README.md.partial +28 -0
- package/templates/pieces/{frappe → frappe-bench}/universal/docker/compose.frappe.yaml +2 -2
- package/templates/pieces/tauri/universal/README.md.partial +7 -5
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/src/dev.rs +463 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/src/lib.rs +6 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/tauri.conf.json +2 -1
- package/templates/pieces/webext/universal/apps/__NAME__/package.json.partial +1 -0
- package/templates/pieces/webext/universal/apps/__NAME__/vite.config.webext.ts +6 -2
- package/types/bin/multiplatformOne.d.ts.map +1 -1
- package/types/commands/addApp.d.ts +20 -0
- package/types/commands/addApp.d.ts.map +1 -0
- package/types/commands/adoptApp.d.ts +4 -3
- package/types/commands/adoptApp.d.ts.map +1 -1
- package/types/commands/doctypeTypes.d.ts +40 -0
- package/types/commands/doctypeTypes.d.ts.map +1 -0
- package/types/commands/init.d.ts +30 -13
- package/types/commands/init.d.ts.map +1 -1
- package/types/commands/merge.d.ts +54 -0
- package/types/commands/merge.d.ts.map +1 -0
- package/types/commands/pieces.d.ts +54 -0
- package/types/commands/pieces.d.ts.map +1 -0
- package/types/commands/provenance.d.ts +52 -0
- package/types/commands/provenance.d.ts.map +1 -0
- package/types/commands/scaffold.d.ts +43 -0
- package/types/commands/scaffold.d.ts.map +1 -0
- package/types/commands/updateApp.d.ts +19 -27
- package/types/commands/updateApp.d.ts.map +1 -1
- package/types/source.d.ts +35 -0
- package/types/source.d.ts.map +1 -0
- package/lib/commands/initApp.mjs +0 -431
- package/lib/generateVscode.mjs +0 -184
- package/scripts/clone.sh +0 -17
- package/scripts/update.sh +0 -46
- package/src/commands/initApp.spec.ts +0 -720
- package/src/commands/initApp.ts +0 -706
- package/src/generateVscode.ts +0 -238
- package/templates/app/AGENTS.md +0 -142
- package/templates/app/README.md +0 -62
- package/templates/app/apps/__NAME__/index.html +0 -36
- package/templates/app/apps/__NAME__/package.json +0 -31
- package/templates/app/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
- package/templates/app/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
- package/templates/app/apps/__NAME__/src/App.tsx +0 -9
- package/templates/app/apps/__NAME__/src/main.tsx +0 -14
- package/templates/app/apps/__NAME__/tsconfig.json +0 -8
- package/templates/app/apps/__NAME__/vite.config.ts +0 -17
- package/templates/app/features/__NAME__/package.json +0 -34
- package/templates/app/features/__NAME__/src/HomeScreen.tsx +0 -70
- package/templates/app/features/__NAME__/src/index.ts +0 -1
- package/templates/app/features/__NAME__/tsconfig.json +0 -5
- package/templates/app/gitignore +0 -9
- package/templates/app/npmrc +0 -2
- package/templates/app/oxlintrc.json +0 -39
- package/templates/app/package.json +0 -38
- package/templates/app/packages/config/config.json +0 -4
- package/templates/app/packages/config/index.ts +0 -5
- package/templates/app/packages/config/package.json +0 -17
- package/templates/app/packages/config/tsconfig.json +0 -5
- package/templates/app/pnpm-workspace.yaml +0 -4
- package/templates/app/tsconfig.base.json +0 -16
- package/templates/universal/AGENTS.md +0 -142
- package/templates/universal/README.md +0 -97
- package/templates/universal/apps/__NAME__/app.config.js +0 -31
- package/templates/universal/apps/__NAME__/config/tamagui.config.ts +0 -4
- package/templates/universal/apps/__NAME__/package.json +0 -62
- package/templates/universal/apps/__NAME__/polyfills.native.ts +0 -23
- package/templates/universal/apps/__NAME__/polyfills.ts +0 -3
- package/templates/universal/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
- package/templates/universal/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
- package/templates/universal/apps/__NAME__/routes/_layout.tsx +0 -22
- package/templates/universal/apps/__NAME__/routes/_layout.web.tsx +0 -21
- package/templates/universal/apps/__NAME__/routes/about.tsx +0 -3
- package/templates/universal/apps/__NAME__/routes/index.tsx +0 -3
- package/templates/universal/apps/__NAME__/src/app.ts +0 -10
- package/templates/universal/apps/__NAME__/tamagui.css +0 -3
- package/templates/universal/apps/__NAME__/tsconfig.json +0 -8
- package/templates/universal/apps/__NAME__/types.d.ts +0 -9
- package/templates/universal/apps/__NAME__/vite.config.ts +0 -20
- package/templates/universal/features/__NAME__/package.json +0 -35
- package/templates/universal/features/__NAME__/src/AboutScreen.tsx +0 -27
- package/templates/universal/features/__NAME__/src/AppLayout.tsx +0 -50
- package/templates/universal/features/__NAME__/src/ContactForm.spec.tsx +0 -22
- package/templates/universal/features/__NAME__/src/ContactForm.tsx +0 -55
- package/templates/universal/features/__NAME__/src/HomeScreen.tsx +0 -40
- package/templates/universal/features/__NAME__/src/index.ts +0 -4
- package/templates/universal/features/__NAME__/src/screens.spec.ts +0 -12
- package/templates/universal/features/__NAME__/tsconfig.json +0 -5
- package/templates/universal/features/__NAME__/vitest.config.mjs +0 -7
- package/templates/universal/gitignore +0 -14
- package/templates/universal/npmrc +0 -3
- package/templates/universal/oxlintrc.json +0 -39
- package/templates/universal/package.json +0 -51
- package/templates/universal/packages/config/config.json +0 -4
- package/templates/universal/packages/config/index.ts +0 -5
- package/templates/universal/packages/config/package.json +0 -17
- package/templates/universal/packages/config/tsconfig.json +0 -5
- package/templates/universal/packages/i18n/en/common.json +0 -30
- package/templates/universal/packages/i18n/es/common.json +0 -30
- package/templates/universal/packages/i18n/i18n.d.ts +0 -16
- package/templates/universal/packages/i18n/index.ts +0 -26
- package/templates/universal/packages/i18n/package.json +0 -20
- package/templates/universal/packages/i18n/resources.ts +0 -13
- package/templates/universal/packages/i18n/tsconfig.json +0 -5
- package/templates/universal/packages/themes/accent.ts +0 -32
- package/templates/universal/packages/themes/base.ts +0 -50
- package/templates/universal/packages/themes/fonts.ts +0 -8
- package/templates/universal/packages/themes/index.ts +0 -33
- package/templates/universal/packages/themes/package.json +0 -23
- package/templates/universal/packages/themes/tamagui.config.ts +0 -14
- package/templates/universal/packages/themes/tsconfig.json +0 -5
- package/templates/universal/pnpm-workspace.yaml +0 -4
- package/templates/universal/tsconfig.base.json +0 -21
- package/templates/universal/types/env.d.ts +0 -9
- package/types/commands/initApp.d.ts +0 -114
- package/types/commands/initApp.d.ts.map +0 -1
- package/types/generateVscode.d.ts +0 -7
- package/types/generateVscode.d.ts.map +0 -1
package/src/commands/initApp.ts
DELETED
|
@@ -1,706 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
copyFileSync,
|
|
3
|
-
existsSync,
|
|
4
|
-
mkdirSync,
|
|
5
|
-
readFileSync,
|
|
6
|
-
readdirSync,
|
|
7
|
-
writeFileSync,
|
|
8
|
-
} from "node:fs";
|
|
9
|
-
import { dirname, extname, join, resolve } from "node:path";
|
|
10
|
-
import { fileURLToPath } from "node:url";
|
|
11
|
-
import inquirer from "inquirer";
|
|
12
|
-
import spawn from "nano-spawn";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Consumer project templates:
|
|
16
|
-
* - "universal": one-based app serving web + iOS + Android from one codebase
|
|
17
|
-
* (apps/<name> + features/<name> + packages/{config,i18n,themes})
|
|
18
|
-
* - "app": web-only Vite + React shell
|
|
19
|
-
* (apps/<name> + features/<name> + packages/config)
|
|
20
|
-
*/
|
|
21
|
-
export type InitAppTemplate = "universal" | "app";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* The provenance template domain: the scaffold templates plus "none" for
|
|
25
|
-
* ADOPTED projects (`mpo adopt`) — existing repos that were never scaffolded.
|
|
26
|
-
* A "none" scaffold is the selected piece fragments overlaid on an EMPTY
|
|
27
|
-
* tree (no universal/app base), which is exactly the tool-owned surface of
|
|
28
|
-
* an adopted project.
|
|
29
|
-
*/
|
|
30
|
-
export type ProvenanceTemplate = InitAppTemplate | "none";
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Optional composable pieces overlaid onto the base template (additive
|
|
34
|
-
* fragments under templates/pieces/<piece>/ — the inverse of the old
|
|
35
|
-
* cookiecutter KEEP_SERVICES/KEEP_PLATFORMS removal hook). Keep sorted:
|
|
36
|
-
* pieces are recorded sorted in provenance and overlaid in this order.
|
|
37
|
-
*/
|
|
38
|
-
export const INIT_APP_PIECES = [
|
|
39
|
-
"frappe",
|
|
40
|
-
"gnome",
|
|
41
|
-
"keycloak",
|
|
42
|
-
"tauri",
|
|
43
|
-
"vscode",
|
|
44
|
-
"webext",
|
|
45
|
-
] as const;
|
|
46
|
-
export type InitAppPiece = (typeof INIT_APP_PIECES)[number];
|
|
47
|
-
|
|
48
|
-
const PIECE_DESCRIPTIONS: Record<InitAppPiece, string> = {
|
|
49
|
-
frappe: "Frappe backend wiring (env keys, provider, docker compose bench)",
|
|
50
|
-
gnome: "GNOME desktop target (GTK4/GJS via react-gnome — native widgets, no webview)",
|
|
51
|
-
keycloak: "Keycloak auth wiring (env keys, provider, docker compose + realm)",
|
|
52
|
-
tauri: "Tauri desktop target (src-tauri skeleton — webview shell, Rust toolchain)",
|
|
53
|
-
vscode: "VS Code extension target (extension host + Tamagui webview)",
|
|
54
|
-
webext: "Browser extension target (MV3 popup + background)",
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export interface InitAppOptions {
|
|
58
|
-
/** Skip pnpm install (useful for CI / dry scaffolding). */
|
|
59
|
-
skipInstall?: boolean;
|
|
60
|
-
/** Skip git init + scaffold commit (mpo update baselines, nested dirs). */
|
|
61
|
-
skipGit?: boolean;
|
|
62
|
-
/** Pin @multiplatform.one/* to this semver range. Default: ^<cli version>. */
|
|
63
|
-
version?: string;
|
|
64
|
-
/**
|
|
65
|
-
* Project template. When omitted: prompt (interactive) or "universal".
|
|
66
|
-
* "none" scaffolds ONLY the selected pieces onto an empty tree (the
|
|
67
|
-
* `--pieces-only` flag) — `mpo update` uses it to rebuild the merge
|
|
68
|
-
* baselines of adopted projects.
|
|
69
|
-
*/
|
|
70
|
-
template?: ProvenanceTemplate;
|
|
71
|
-
/** Accept defaults instead of prompting (non-interactive / CI). */
|
|
72
|
-
yes?: boolean;
|
|
73
|
-
/**
|
|
74
|
-
* Composable pieces overlaid onto the base template. When omitted:
|
|
75
|
-
* checkbox prompt (interactive) or none (--yes / non-TTY).
|
|
76
|
-
*/
|
|
77
|
-
pieces?: InitAppPiece[];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
type TemplateVars = {
|
|
81
|
-
NAME: string;
|
|
82
|
-
NAME_PASCAL: string;
|
|
83
|
-
/** Lowercase alphanumeric form for iOS bundle ids / Android packages. */
|
|
84
|
-
NAME_APPID: string;
|
|
85
|
-
MPO_VERSION: string;
|
|
86
|
-
YEAR: string;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
function toPascalCase(name: string): string {
|
|
90
|
-
return name
|
|
91
|
-
.split(/[-_\s]+/)
|
|
92
|
-
.filter(Boolean)
|
|
93
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
94
|
-
.join("");
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function validateName(name: string): string {
|
|
98
|
-
const trimmed = name.trim();
|
|
99
|
-
if (!trimmed) {
|
|
100
|
-
throw new Error("Project name is required");
|
|
101
|
-
}
|
|
102
|
-
if (!/^[a-z][a-z0-9-]*$/.test(trimmed)) {
|
|
103
|
-
throw new Error(
|
|
104
|
-
`Invalid project name "${trimmed}". Use lowercase letters, numbers, and hyphens (e.g. my-app).`,
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
return trimmed;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** Root package.json name with any @scope/ prefix stripped (adopt +
|
|
111
|
-
* update-bootstrap provenance name derivation). */
|
|
112
|
-
export function packageJsonName(projectDir: string): string | undefined {
|
|
113
|
-
try {
|
|
114
|
-
const pkg = JSON.parse(readFileSync(join(projectDir, "package.json"), "utf-8")) as {
|
|
115
|
-
name?: string;
|
|
116
|
-
};
|
|
117
|
-
return pkg.name?.replace(/^@[^/]+\//u, "");
|
|
118
|
-
} catch {
|
|
119
|
-
return undefined;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function resolveMpoVersion(explicit?: string): string {
|
|
124
|
-
if (explicit) {
|
|
125
|
-
return explicit.startsWith("^") || explicit.startsWith("~") || explicit === "*"
|
|
126
|
-
? explicit
|
|
127
|
-
: `^${explicit}`;
|
|
128
|
-
}
|
|
129
|
-
const version = cliVersion();
|
|
130
|
-
return version ? `^${version}` : "^6.1.0";
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function cliVersion(): string | undefined {
|
|
134
|
-
try {
|
|
135
|
-
const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
136
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { version?: string };
|
|
137
|
-
return pkg.version;
|
|
138
|
-
} catch {
|
|
139
|
-
return undefined;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Scaffold provenance (the copier-answers equivalent): which template, which
|
|
145
|
-
* CLI version and which vars produced this project. `mpo update` regenerates
|
|
146
|
-
* the ORIGINAL scaffold from this record to use as the merge base for a
|
|
147
|
-
* three-way template update.
|
|
148
|
-
*/
|
|
149
|
-
export interface MpoProvenance {
|
|
150
|
-
/** "none" = adopted project (`mpo adopt`): pieces only, no base template. */
|
|
151
|
-
template: ProvenanceTemplate;
|
|
152
|
-
cliVersion: string;
|
|
153
|
-
name: string;
|
|
154
|
-
mpoVersion: string;
|
|
155
|
-
/** Composable pieces overlaid at init time (sorted). */
|
|
156
|
-
pieces: InitAppPiece[];
|
|
157
|
-
/**
|
|
158
|
-
* false = adopted but never reconciled: `mpo adopt` wrote provenance
|
|
159
|
-
* without touching any project file, so no tool-owned content exists yet.
|
|
160
|
-
* The first `mpo update` merges the piece baseline from an EMPTY base
|
|
161
|
-
* (surfacing real diffs against hand-copied files) and then clears the
|
|
162
|
-
* flag. Absent/true = the tree carries the recorded scaffold.
|
|
163
|
-
*/
|
|
164
|
-
reconciled?: boolean;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export const PROVENANCE_FILE = ".mpo.json";
|
|
168
|
-
|
|
169
|
-
export function writeProvenance(targetDir: string, provenance: MpoProvenance): void {
|
|
170
|
-
writeFileSync(
|
|
171
|
-
join(targetDir, PROVENANCE_FILE),
|
|
172
|
-
`${JSON.stringify({ ...provenance, pieces: [...provenance.pieces].sort() }, null, 2)}\n`,
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export function readProvenance(projectDir: string): MpoProvenance | undefined {
|
|
177
|
-
const file = join(projectDir, PROVENANCE_FILE);
|
|
178
|
-
if (!existsSync(file)) return undefined;
|
|
179
|
-
try {
|
|
180
|
-
const raw = JSON.parse(readFileSync(file, "utf-8")) as Partial<MpoProvenance>;
|
|
181
|
-
if (!raw.template || !raw.cliVersion || !raw.name) return undefined;
|
|
182
|
-
return {
|
|
183
|
-
template: raw.template,
|
|
184
|
-
cliVersion: raw.cliVersion,
|
|
185
|
-
name: raw.name,
|
|
186
|
-
mpoVersion: raw.mpoVersion ?? `^${raw.cliVersion}`,
|
|
187
|
-
// Scaffolds from pre-piece CLIs (≤6.4.x) have no pieces field.
|
|
188
|
-
pieces: Array.isArray(raw.pieces) ? [...(raw.pieces as InitAppPiece[])].sort() : [],
|
|
189
|
-
// Only adopted-but-unreconciled provenance carries the flag.
|
|
190
|
-
...(raw.reconciled === false ? { reconciled: false } : {}),
|
|
191
|
-
};
|
|
192
|
-
} catch {
|
|
193
|
-
return undefined;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function templatesRoot(template: InitAppTemplate): string {
|
|
198
|
-
return resolve(dirname(fileURLToPath(import.meta.url)), "../../templates", template);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function piecesRoot(piece: InitAppPiece): string {
|
|
202
|
-
return resolve(dirname(fileURLToPath(import.meta.url)), "../../templates/pieces", piece);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Fragment source dirs for a piece + template: shared/ applies to every
|
|
207
|
-
* template, <template>/ only to that one. A piece supports a template when
|
|
208
|
-
* at least one of the two exists. Adopted projects (template "none") use the
|
|
209
|
-
* universal fragment set — pieces ship universal-layout fragments
|
|
210
|
-
* (apps/<name>/…), which is the layout adopted repos follow.
|
|
211
|
-
*/
|
|
212
|
-
function pieceFragmentDirs(piece: InitAppPiece, template: ProvenanceTemplate): string[] {
|
|
213
|
-
const effective: InitAppTemplate = template === "none" ? "universal" : template;
|
|
214
|
-
return [join(piecesRoot(piece), "shared"), join(piecesRoot(piece), effective)].filter((dir) =>
|
|
215
|
-
existsSync(dir),
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* npm always strips .gitignore / .npmrc (and newer npms .env*) from published
|
|
221
|
-
* tarballs, so the template stores them without the leading dot; restore it
|
|
222
|
-
* at scaffold time.
|
|
223
|
-
*/
|
|
224
|
-
const DOTFILE_NAMES: Record<string, string> = {
|
|
225
|
-
gitignore: ".gitignore",
|
|
226
|
-
npmrc: ".npmrc",
|
|
227
|
-
"env.example": ".env.example",
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
function render(content: string, vars: TemplateVars): string {
|
|
231
|
-
return content
|
|
232
|
-
.replaceAll("__NAME_PASCAL__", vars.NAME_PASCAL)
|
|
233
|
-
.replaceAll("__NAME_APPID__", vars.NAME_APPID)
|
|
234
|
-
.replaceAll("__NAME__", vars.NAME)
|
|
235
|
-
.replaceAll("__MPO_VERSION__", vars.MPO_VERSION)
|
|
236
|
-
.replaceAll("__YEAR__", vars.YEAR);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Assets copied byte-for-byte — running them through the utf-8 render pass
|
|
241
|
-
* would corrupt them (fonts ship in the templates for LC-33 stable mount).
|
|
242
|
-
*/
|
|
243
|
-
const BINARY_EXTENSIONS = new Set([
|
|
244
|
-
".woff2",
|
|
245
|
-
".woff",
|
|
246
|
-
".ttf",
|
|
247
|
-
".otf",
|
|
248
|
-
".png",
|
|
249
|
-
".jpg",
|
|
250
|
-
".ico",
|
|
251
|
-
".icns",
|
|
252
|
-
]);
|
|
253
|
-
|
|
254
|
-
function writeTree(srcDir: string, destDir: string, vars: TemplateVars): void {
|
|
255
|
-
if (!existsSync(srcDir)) {
|
|
256
|
-
throw new Error(`Project template missing at ${srcDir}`);
|
|
257
|
-
}
|
|
258
|
-
mkdirSync(destDir, { recursive: true });
|
|
259
|
-
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
260
|
-
const from = join(srcDir, entry.name);
|
|
261
|
-
// Template files may use __NAME__ in path segments.
|
|
262
|
-
const toName = render(DOTFILE_NAMES[entry.name] ?? entry.name, vars);
|
|
263
|
-
const to = join(destDir, toName);
|
|
264
|
-
if (entry.isDirectory()) {
|
|
265
|
-
writeTree(from, to, vars);
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
if (BINARY_EXTENSIONS.has(extname(entry.name).toLowerCase())) {
|
|
269
|
-
copyFileSync(from, to);
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
const raw = readFileSync(from, "utf-8");
|
|
273
|
-
writeFileSync(to, render(raw, vars));
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// ── piece fragment overlay ─────────────────────────────────────────
|
|
278
|
-
|
|
279
|
-
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
|
280
|
-
|
|
281
|
-
function isJsonObject(value: JsonValue | undefined): value is { [key: string]: JsonValue } {
|
|
282
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Deep-merge for JSON partials: objects merge recursively (fragment keys
|
|
287
|
-
* append after base keys, so e.g. package.json script order stays stable),
|
|
288
|
-
* arrays union (base order first — config.json public keys accumulate
|
|
289
|
-
* across pieces), scalars/type-mismatches take the fragment value.
|
|
290
|
-
*/
|
|
291
|
-
export function mergeJsonValues(base: JsonValue, fragment: JsonValue): JsonValue {
|
|
292
|
-
if (Array.isArray(base) && Array.isArray(fragment)) {
|
|
293
|
-
const merged = [...base];
|
|
294
|
-
const seen = new Set(base.map((item) => JSON.stringify(item)));
|
|
295
|
-
for (const item of fragment) {
|
|
296
|
-
const key = JSON.stringify(item);
|
|
297
|
-
if (seen.has(key)) continue;
|
|
298
|
-
seen.add(key);
|
|
299
|
-
merged.push(item);
|
|
300
|
-
}
|
|
301
|
-
return merged;
|
|
302
|
-
}
|
|
303
|
-
if (isJsonObject(base) && isJsonObject(fragment)) {
|
|
304
|
-
const merged: { [key: string]: JsonValue } = { ...base };
|
|
305
|
-
for (const [key, value] of Object.entries(fragment)) {
|
|
306
|
-
merged[key] = key in merged ? mergeJsonValues(merged[key]!, value) : value;
|
|
307
|
-
}
|
|
308
|
-
return merged;
|
|
309
|
-
}
|
|
310
|
-
return fragment;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/** Dependency maps stay alphabetically sorted after fragment merges (the
|
|
314
|
-
* base templates are sorted; multiple pieces appending must not depend on
|
|
315
|
-
* overlay order for the final byte content). */
|
|
316
|
-
const SORTED_DEPENDENCY_FIELDS = [
|
|
317
|
-
"dependencies",
|
|
318
|
-
"devDependencies",
|
|
319
|
-
"peerDependencies",
|
|
320
|
-
"optionalDependencies",
|
|
321
|
-
];
|
|
322
|
-
|
|
323
|
-
function sortDependencyFields(value: JsonValue): JsonValue {
|
|
324
|
-
if (!isJsonObject(value)) return value;
|
|
325
|
-
const result = { ...value };
|
|
326
|
-
for (const field of SORTED_DEPENDENCY_FIELDS) {
|
|
327
|
-
const deps = result[field];
|
|
328
|
-
if (!isJsonObject(deps)) continue;
|
|
329
|
-
result[field] = Object.fromEntries(
|
|
330
|
-
Object.entries(deps).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
return result;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Apply a rendered `<target>.partial` fragment onto the target file:
|
|
338
|
-
* - `.json` targets deep-merge (see mergeJsonValues); package.json-style
|
|
339
|
-
* dependency maps are re-sorted for deterministic output
|
|
340
|
-
* - everything else appends as a text block separated by a blank line
|
|
341
|
-
* Missing targets are created from the fragment alone.
|
|
342
|
-
*/
|
|
343
|
-
function applyPartial(targetPath: string, renderedFragment: string): void {
|
|
344
|
-
if (targetPath.endsWith(".json")) {
|
|
345
|
-
const fragment = JSON.parse(renderedFragment) as JsonValue;
|
|
346
|
-
const base = existsSync(targetPath)
|
|
347
|
-
? (JSON.parse(readFileSync(targetPath, "utf-8")) as JsonValue)
|
|
348
|
-
: undefined;
|
|
349
|
-
const merged = sortDependencyFields(
|
|
350
|
-
base === undefined ? fragment : mergeJsonValues(base, fragment),
|
|
351
|
-
);
|
|
352
|
-
writeFileSync(targetPath, `${JSON.stringify(merged, null, 2)}\n`);
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
const block = `${renderedFragment.replace(/^\n+/u, "").replace(/\n+$/u, "")}\n`;
|
|
356
|
-
if (!existsSync(targetPath)) {
|
|
357
|
-
writeFileSync(targetPath, block);
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
const existing = readFileSync(targetPath, "utf-8").replace(/\n+$/u, "");
|
|
361
|
-
writeFileSync(targetPath, `${existing}\n\n${block}`);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const PARTIAL_SUFFIX = ".partial";
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Overlay a piece fragment tree onto the scaffold: regular files render and
|
|
368
|
-
* overwrite, `<target>.partial` files merge into (or create) their target
|
|
369
|
-
* (deep-merge for .json, append for text — see applyPartial).
|
|
370
|
-
*/
|
|
371
|
-
function overlayTree(srcDir: string, destDir: string, vars: TemplateVars): void {
|
|
372
|
-
mkdirSync(destDir, { recursive: true });
|
|
373
|
-
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
374
|
-
const from = join(srcDir, entry.name);
|
|
375
|
-
if (entry.isDirectory()) {
|
|
376
|
-
overlayTree(from, join(destDir, render(DOTFILE_NAMES[entry.name] ?? entry.name, vars)), vars);
|
|
377
|
-
continue;
|
|
378
|
-
}
|
|
379
|
-
if (entry.name.endsWith(PARTIAL_SUFFIX)) {
|
|
380
|
-
const targetName = entry.name.slice(0, -PARTIAL_SUFFIX.length);
|
|
381
|
-
const toName = render(DOTFILE_NAMES[targetName] ?? targetName, vars);
|
|
382
|
-
applyPartial(join(destDir, toName), render(readFileSync(from, "utf-8"), vars));
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
const toName = render(DOTFILE_NAMES[entry.name] ?? entry.name, vars);
|
|
386
|
-
const to = join(destDir, toName);
|
|
387
|
-
if (BINARY_EXTENSIONS.has(extname(entry.name).toLowerCase())) {
|
|
388
|
-
copyFileSync(from, to);
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
writeFileSync(to, render(readFileSync(from, "utf-8"), vars));
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Validate + canonicalize a piece selection for a template: unknown names
|
|
397
|
-
* throw, duplicates collapse, order is always sorted (provenance and overlay
|
|
398
|
-
* order must be deterministic), and every piece must ship a fragment for the
|
|
399
|
-
* chosen template.
|
|
400
|
-
*/
|
|
401
|
-
export function normalizePieces(pieces: string[], template: ProvenanceTemplate): InitAppPiece[] {
|
|
402
|
-
const known = new Set<string>(INIT_APP_PIECES);
|
|
403
|
-
const normalized = [...new Set(pieces)].sort() as InitAppPiece[];
|
|
404
|
-
for (const piece of normalized) {
|
|
405
|
-
if (!known.has(piece)) {
|
|
406
|
-
throw new Error(`Unknown piece "${piece}". Available pieces: ${INIT_APP_PIECES.join(", ")}`);
|
|
407
|
-
}
|
|
408
|
-
if (pieceFragmentDirs(piece, template).length === 0) {
|
|
409
|
-
throw new Error(
|
|
410
|
-
`Piece "${piece}" is not available for the ${template} template ` +
|
|
411
|
-
"(pieces currently target the universal template)",
|
|
412
|
-
);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
return normalized;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/** Pieces that can be offered for a template (fragment exists for it). */
|
|
419
|
-
export function availablePieces(template: ProvenanceTemplate): InitAppPiece[] {
|
|
420
|
-
return INIT_APP_PIECES.filter((piece) => pieceFragmentDirs(piece, template).length > 0);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/** Human-readable piece summaries (adopt + init prompts). */
|
|
424
|
-
export function pieceDescription(piece: InitAppPiece): string {
|
|
425
|
-
return PIECE_DESCRIPTIONS[piece];
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
async function resolvePieces(
|
|
429
|
-
options: InitAppOptions,
|
|
430
|
-
template: ProvenanceTemplate,
|
|
431
|
-
): Promise<InitAppPiece[]> {
|
|
432
|
-
if (options.pieces) return normalizePieces(options.pieces, template);
|
|
433
|
-
if (options.yes || !process.stdout.isTTY) return [];
|
|
434
|
-
const choices = availablePieces(template);
|
|
435
|
-
if (choices.length === 0) return [];
|
|
436
|
-
const result = await inquirer.prompt([
|
|
437
|
-
{
|
|
438
|
-
message: "Add optional pieces? (space to toggle, enter to confirm)",
|
|
439
|
-
name: "pieces",
|
|
440
|
-
type: "checkbox",
|
|
441
|
-
choices: choices.map((piece) => ({
|
|
442
|
-
name: `${piece} — ${PIECE_DESCRIPTIONS[piece]}`,
|
|
443
|
-
value: piece,
|
|
444
|
-
checked: false,
|
|
445
|
-
})),
|
|
446
|
-
},
|
|
447
|
-
]);
|
|
448
|
-
return normalizePieces(result.pieces as string[], template);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// ── app.ts composition (frappe / keycloak provider wiring) ─────────
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Generate apps/<name>/src/app.ts for the selected backend pieces instead of
|
|
455
|
-
* storing 2^n template variants: each selected provider adds one createApp
|
|
456
|
-
* key (frappe/keycloak read their runtime settings from the packages/config
|
|
457
|
-
* public env keys their fragments register). Only called when at least one
|
|
458
|
-
* provider piece is selected — the base template file stays otherwise.
|
|
459
|
-
*/
|
|
460
|
-
export function composeAppTs(pieces: InitAppPiece[]): string {
|
|
461
|
-
const frappe = pieces.includes("frappe");
|
|
462
|
-
const keycloak = pieces.includes("keycloak");
|
|
463
|
-
const lines: string[] = ['import { createApp } from "@multiplatform.one/core";'];
|
|
464
|
-
if (frappe || keycloak) {
|
|
465
|
-
lines.push('import { config } from "@multiplatform.one/platform";');
|
|
466
|
-
}
|
|
467
|
-
lines.push('import { themeConfig } from "@package/themes";');
|
|
468
|
-
lines.push(
|
|
469
|
-
"",
|
|
470
|
-
"/**",
|
|
471
|
-
" * App-wide provider stack. Providers below were enabled by the pieces",
|
|
472
|
-
" * selected at `mpo init` time — see CreateAppConfig in",
|
|
473
|
-
" * @multiplatform.one/core for everything else that can be wired here",
|
|
474
|
-
" * (tanstack, web3, custom providers).",
|
|
475
|
-
" */",
|
|
476
|
-
"export const { AppProvider, useAppConfig, withApp } = createApp({",
|
|
477
|
-
" theme: themeConfig,",
|
|
478
|
-
);
|
|
479
|
-
if (frappe) {
|
|
480
|
-
lines.push(
|
|
481
|
-
" // Frappe backend (mpo init --frappe): FrappeProvider mounts when",
|
|
482
|
-
" // FRAPPE_ENABLED=1 (a packages/config public env key, loaded from",
|
|
483
|
-
" // .env — see .env.example). FRAPPE_URL overrides the bench URL",
|
|
484
|
-
" // (default: BASE_URL, then http://localhost:8000);",
|
|
485
|
-
" // docker/compose.frappe.yaml runs a dev bench.",
|
|
486
|
-
' frappe: { baseURL: config.get("FRAPPE_URL") },',
|
|
487
|
-
);
|
|
488
|
-
}
|
|
489
|
-
if (keycloak) {
|
|
490
|
-
lines.push(
|
|
491
|
-
" // Keycloak auth (mpo init --keycloak): KeycloakProvider mounts when",
|
|
492
|
-
" // KEYCLOAK_ENABLED=1. Realm/client settings come from the KEYCLOAK_*",
|
|
493
|
-
" // env keys (packages/config, .env.example);",
|
|
494
|
-
" // docker/compose.keycloak.yaml runs a dev server with an imported",
|
|
495
|
-
" // realm.",
|
|
496
|
-
' keycloak: { baseUrl: config.get("KEYCLOAK_BASE_URL") },',
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
lines.push("});", "");
|
|
500
|
-
return lines.join("\n");
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
async function resolveTemplate(options: InitAppOptions): Promise<ProvenanceTemplate> {
|
|
504
|
-
if (options.template) return options.template;
|
|
505
|
-
if (options.yes || !process.stdout.isTTY) return "universal";
|
|
506
|
-
const result = await inquirer.prompt([
|
|
507
|
-
{
|
|
508
|
-
message: "Which platforms should this project target?",
|
|
509
|
-
name: "template",
|
|
510
|
-
type: "list",
|
|
511
|
-
choices: [
|
|
512
|
-
{ name: "Universal — web + iOS + Android (one)", value: "universal" },
|
|
513
|
-
{ name: "Web only — Vite + React", value: "app" },
|
|
514
|
-
],
|
|
515
|
-
default: "universal",
|
|
516
|
-
},
|
|
517
|
-
]);
|
|
518
|
-
return result.template as InitAppTemplate;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/**
|
|
522
|
-
* Scaffold a consumer project (universal by default, web-only with
|
|
523
|
-
* template: "app") that depends on @multiplatform.one/* at semver ranges
|
|
524
|
-
* (never workspace:*). This is NOT a monorepo fork.
|
|
525
|
-
*/
|
|
526
|
-
export async function initApp(
|
|
527
|
-
nameArg: string | undefined,
|
|
528
|
-
options: InitAppOptions = {},
|
|
529
|
-
): Promise<void> {
|
|
530
|
-
let projectName = nameArg;
|
|
531
|
-
if (!projectName) {
|
|
532
|
-
if (options.yes || !process.stdout.isTTY) {
|
|
533
|
-
projectName = "my-app";
|
|
534
|
-
} else {
|
|
535
|
-
const result = await inquirer.prompt([
|
|
536
|
-
{
|
|
537
|
-
message: "What is the app name?",
|
|
538
|
-
name: "name",
|
|
539
|
-
type: "input",
|
|
540
|
-
default: "my-app",
|
|
541
|
-
},
|
|
542
|
-
]);
|
|
543
|
-
projectName = result.name as string;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
const name = validateName(projectName);
|
|
548
|
-
const template = await resolveTemplate(options);
|
|
549
|
-
const pieces = await resolvePieces(options, template);
|
|
550
|
-
if (template === "none" && pieces.length === 0) {
|
|
551
|
-
throw new Error(
|
|
552
|
-
"A pieces-only scaffold needs at least one piece " +
|
|
553
|
-
`(${INIT_APP_PIECES.map((piece) => `--${piece}`).join(", ")})`,
|
|
554
|
-
);
|
|
555
|
-
}
|
|
556
|
-
const targetDir = resolve(name);
|
|
557
|
-
|
|
558
|
-
if (existsSync(targetDir)) {
|
|
559
|
-
const entries = readdirSync(targetDir);
|
|
560
|
-
if (entries.length > 0) {
|
|
561
|
-
throw new Error(`Directory "${targetDir}" already exists and is not empty`);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const vars: TemplateVars = {
|
|
566
|
-
NAME: name,
|
|
567
|
-
NAME_PASCAL: toPascalCase(name),
|
|
568
|
-
NAME_APPID: name.replaceAll("-", ""),
|
|
569
|
-
MPO_VERSION: resolveMpoVersion(options.version),
|
|
570
|
-
YEAR: String(new Date().getFullYear()),
|
|
571
|
-
};
|
|
572
|
-
|
|
573
|
-
const label =
|
|
574
|
-
template === "universal"
|
|
575
|
-
? "universal (web + iOS + Android)"
|
|
576
|
-
: template === "app"
|
|
577
|
-
? "web-only"
|
|
578
|
-
: "pieces-only (adopted-project baseline)";
|
|
579
|
-
console.log(`\nScaffolding ${label} project at ${targetDir}`);
|
|
580
|
-
if (template !== "none") {
|
|
581
|
-
console.log(` apps/${name}`);
|
|
582
|
-
console.log(` features/${name}`);
|
|
583
|
-
console.log(` packages/config`);
|
|
584
|
-
}
|
|
585
|
-
if (template === "universal") {
|
|
586
|
-
console.log(` packages/i18n`);
|
|
587
|
-
console.log(` packages/themes`);
|
|
588
|
-
}
|
|
589
|
-
if (pieces.length) {
|
|
590
|
-
console.log(` pieces: ${pieces.join(", ")}`);
|
|
591
|
-
}
|
|
592
|
-
console.log(` @multiplatform.one/* → ${vars.MPO_VERSION}\n`);
|
|
593
|
-
|
|
594
|
-
// "none" (adopted-project baselines) overlays pieces on an EMPTY tree —
|
|
595
|
-
// there is no base template to write.
|
|
596
|
-
if (template === "none") {
|
|
597
|
-
mkdirSync(targetDir, { recursive: true });
|
|
598
|
-
} else {
|
|
599
|
-
writeTree(templatesRoot(template), targetDir, vars);
|
|
600
|
-
}
|
|
601
|
-
// Pieces overlay AFTER the base template, in sorted order (normalizePieces)
|
|
602
|
-
// so repeated scaffolds are byte-identical — mpo update regenerates these
|
|
603
|
-
// exact trees as merge baselines.
|
|
604
|
-
for (const piece of pieces) {
|
|
605
|
-
for (const fragmentDir of pieceFragmentDirs(piece, template)) {
|
|
606
|
-
overlayTree(fragmentDir, targetDir, vars);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
// Backend providers are wired by generating src/app.ts from the selection
|
|
610
|
-
// (not by stacking file overwrites per combination).
|
|
611
|
-
if (pieces.includes("frappe") || pieces.includes("keycloak")) {
|
|
612
|
-
// Pieces-only trees have no base template dirs — create the path.
|
|
613
|
-
mkdirSync(join(targetDir, "apps", name, "src"), { recursive: true });
|
|
614
|
-
writeFileSync(join(targetDir, "apps", name, "src/app.ts"), composeAppTs(pieces));
|
|
615
|
-
}
|
|
616
|
-
writeProvenance(targetDir, {
|
|
617
|
-
template,
|
|
618
|
-
cliVersion: cliVersion() ?? "0.0.0",
|
|
619
|
-
name,
|
|
620
|
-
mpoVersion: vars.MPO_VERSION,
|
|
621
|
-
pieces,
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
// Initialize git with a scaffold commit (unless already inside a repo —
|
|
625
|
-
// e.g. scaffolding into an existing project dir). `mpo update` needs the
|
|
626
|
-
// scaffold as a committed baseline to merge on top of.
|
|
627
|
-
let scaffoldCommitted = false;
|
|
628
|
-
if (!options.skipGit) {
|
|
629
|
-
const insideRepo = await spawn("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
630
|
-
cwd: targetDir,
|
|
631
|
-
}).then(
|
|
632
|
-
() => true,
|
|
633
|
-
() => false,
|
|
634
|
-
);
|
|
635
|
-
if (!insideRepo) {
|
|
636
|
-
const label =
|
|
637
|
-
template === "universal" ? "universal" : template === "app" ? "web-only" : "pieces-only";
|
|
638
|
-
await spawn("git", ["init", "--quiet"], { cwd: targetDir });
|
|
639
|
-
await spawn("git", ["add", "-A"], { cwd: targetDir });
|
|
640
|
-
scaffoldCommitted = await spawn(
|
|
641
|
-
"git",
|
|
642
|
-
["commit", "--quiet", "-m", `feat: scaffold ${label} multiplatform.one app (mpo init)`],
|
|
643
|
-
{ cwd: targetDir },
|
|
644
|
-
).then(
|
|
645
|
-
() => true,
|
|
646
|
-
() => {
|
|
647
|
-
console.warn("⚠️ git commit failed (missing git identity?) — repo left staged.");
|
|
648
|
-
return false;
|
|
649
|
-
},
|
|
650
|
-
);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// Pieces-only trees are merge baselines, not runnable projects — never
|
|
655
|
-
// install into them.
|
|
656
|
-
if (!options.skipInstall && template !== "none") {
|
|
657
|
-
console.log("Installing dependencies...");
|
|
658
|
-
await spawn("pnpm", ["install"], {
|
|
659
|
-
cwd: targetDir,
|
|
660
|
-
stdio: "inherit",
|
|
661
|
-
});
|
|
662
|
-
// The lockfile belongs to the scaffold — fold it into the scaffold
|
|
663
|
-
// commit so a fresh init is immediately clean.
|
|
664
|
-
if (scaffoldCommitted) {
|
|
665
|
-
await spawn("git", ["add", "--", "pnpm-lock.yaml"], { cwd: targetDir })
|
|
666
|
-
.then(() => spawn("git", ["commit", "--amend", "--no-edit", "--quiet"], { cwd: targetDir }))
|
|
667
|
-
.catch(() => {});
|
|
668
|
-
}
|
|
669
|
-
} else if (options.skipInstall) {
|
|
670
|
-
console.log("Skipped pnpm install (--skip-install).");
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
console.log(`\n✅ Project created at ${targetDir}`);
|
|
674
|
-
if (template === "none") {
|
|
675
|
-
console.log("\nPieces-only tree (adopted-project baseline) — not a runnable project.");
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
console.log("\nNext steps:");
|
|
679
|
-
console.log(` cd ${name}`);
|
|
680
|
-
if (options.skipInstall) console.log(" pnpm install");
|
|
681
|
-
console.log(" pnpm dev # web dev server");
|
|
682
|
-
if (template === "universal") {
|
|
683
|
-
console.log(" pnpm ios # iOS (requires pnpm dev in another terminal)");
|
|
684
|
-
console.log(" pnpm android # Android");
|
|
685
|
-
}
|
|
686
|
-
if (pieces.includes("frappe")) {
|
|
687
|
-
console.log(" docker compose -f docker/compose.frappe.yaml up -d # dev bench (see README)");
|
|
688
|
-
}
|
|
689
|
-
if (pieces.includes("keycloak")) {
|
|
690
|
-
console.log(" docker compose -f docker/compose.keycloak.yaml up -d # dev keycloak");
|
|
691
|
-
}
|
|
692
|
-
if (pieces.includes("gnome")) {
|
|
693
|
-
console.log(" pnpm gnome # GTK4 desktop app (requires gjs + gtk4 — see README)");
|
|
694
|
-
}
|
|
695
|
-
if (pieces.includes("tauri")) {
|
|
696
|
-
console.log(" pnpm tauri:dev # Tauri desktop app (requires the Rust toolchain)");
|
|
697
|
-
}
|
|
698
|
-
if (pieces.includes("vscode")) {
|
|
699
|
-
console.log(" pnpm build:vscode # stage the VS Code extension into apps dist-vscode/");
|
|
700
|
-
}
|
|
701
|
-
if (pieces.includes("webext")) {
|
|
702
|
-
console.log(" pnpm build:webext # stage the browser extension into apps dist-webext/");
|
|
703
|
-
}
|
|
704
|
-
console.log("\nThis project consumes published @multiplatform.one/* packages.");
|
|
705
|
-
console.log("It is not a fork of the multiplatform.one monorepo.");
|
|
706
|
-
}
|