@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/init.ts
CHANGED
|
@@ -1,671 +1,229 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const services = ["frappe", "solana", "ethereum", "sui"] as const;
|
|
9
|
-
|
|
10
|
-
/** Resolve a service name to its relative directory from the project root. */
|
|
11
|
-
function serviceDir(name: string): string {
|
|
12
|
-
return `apps/${name}`;
|
|
13
|
-
}
|
|
14
|
-
// vscode + webext are no longer standalone apps: they are delivery targets
|
|
15
|
-
// of the One platform app (apps/one/vscode, apps/one/webext) and ride with
|
|
16
|
-
// the "one" selection.
|
|
17
|
-
const apps = ["one", "keycloak", "storybook", "storybook-expo", "vocs"] as const;
|
|
18
|
-
|
|
19
|
-
type Service = (typeof services)[number];
|
|
20
|
-
type App = (typeof apps)[number];
|
|
21
|
-
|
|
22
|
-
/** env variable prefixes to remove per service */
|
|
23
|
-
const serviceEnvPrefixes: Record<Service, string[]> = {
|
|
24
|
-
frappe: ["FRAPPE_", "MARIADB_"],
|
|
25
|
-
solana: [],
|
|
26
|
-
ethereum: [],
|
|
27
|
-
sui: [],
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/** env variable prefixes to remove per app */
|
|
31
|
-
const appEnvPrefixes: Record<App, string[]> = {
|
|
32
|
-
one: ["ONE_", "VSCODE_", "WEBEXT_"],
|
|
33
|
-
keycloak: ["KEYCLOAK_"],
|
|
34
|
-
storybook: ["STORYBOOK_", "VR_DIFFING_ENGINE="],
|
|
35
|
-
"storybook-expo": [],
|
|
36
|
-
vocs: ["VOCS_"],
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/** env section headers (comments) to remove per app/service */
|
|
40
|
-
const serviceEnvSections: Record<Service, string[]> = {
|
|
41
|
-
frappe: ["# frappe", "# mariadb"],
|
|
42
|
-
solana: [],
|
|
43
|
-
ethereum: [],
|
|
44
|
-
sui: [],
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const appEnvSections: Record<App, string[]> = {
|
|
48
|
-
one: ["# one", "# vscode", "# webext"],
|
|
49
|
-
keycloak: ["# keycloak"],
|
|
50
|
-
storybook: ["# storybook"],
|
|
51
|
-
"storybook-expo": [],
|
|
52
|
-
vocs: ["# vocs"],
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
/** launch.json configuration names per service/app */
|
|
56
|
-
const serviceLaunchNames: Record<Service, string[]> = {
|
|
57
|
-
frappe: ["frappe dev"],
|
|
58
|
-
solana: ["solana dev", "solana localnet"],
|
|
59
|
-
ethereum: ["ethereum dev"],
|
|
60
|
-
sui: ["sui dev"],
|
|
61
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* `mpo init`: a consumer project is the multiplatform.one monorepo at one
|
|
3
|
+
* commit, pruned. scripts/scaffold.sh does the work (clone or archive at
|
|
4
|
+
* the ref, drop public/ and the singleton apps not asked for, rename
|
|
5
|
+
* apps/one to each product app, rewrite workspace: ranges to semver); this
|
|
6
|
+
* command picks the commit, asks what to keep, and finishes the repo.
|
|
7
|
+
*/
|
|
62
8
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
9
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
10
|
+
import { basename, resolve } from "node:path";
|
|
11
|
+
import inquirer from "inquirer";
|
|
12
|
+
import spawn from "nano-spawn";
|
|
13
|
+
import { resolveSource, type SourceSpec } from "../source";
|
|
14
|
+
import { mpoRange, validateName } from "./provenance";
|
|
15
|
+
import { runScaffold, ScaffoldError } from "./scaffold";
|
|
16
|
+
|
|
17
|
+
/** The apps that exist once per project; everything else under apps/ is a product app. */
|
|
18
|
+
export const SINGLETON_APPS = [
|
|
19
|
+
"ethereum",
|
|
20
|
+
"frappe",
|
|
21
|
+
"keycloak",
|
|
22
|
+
"solana",
|
|
23
|
+
"storybook",
|
|
24
|
+
"storybook-expo",
|
|
25
|
+
"sui",
|
|
26
|
+
"uxpin",
|
|
27
|
+
"vocs",
|
|
28
|
+
] as const;
|
|
29
|
+
export type SingletonApp = (typeof SINGLETON_APPS)[number];
|
|
30
|
+
|
|
31
|
+
const SINGLETON_DESCRIPTIONS: Record<SingletonApp, string> = {
|
|
32
|
+
ethereum: "Hardhat chain + contracts",
|
|
33
|
+
frappe: "Frappe bench (backend, docker compose)",
|
|
34
|
+
keycloak: "Keycloak theme + realm (auth, docker compose)",
|
|
35
|
+
solana: "Anchor programs + localnet",
|
|
36
|
+
storybook: "Storybook (web)",
|
|
37
|
+
"storybook-expo": "Storybook (native)",
|
|
38
|
+
sui: "Move packages + localnet",
|
|
39
|
+
uxpin: "UXPin Merge library",
|
|
40
|
+
vocs: "Docs site",
|
|
69
41
|
};
|
|
70
42
|
|
|
71
|
-
interface InitOptions {
|
|
72
|
-
name
|
|
73
|
-
|
|
74
|
-
apps
|
|
43
|
+
export interface InitOptions {
|
|
44
|
+
/** Product apps besides the project name (each is a renamed apps/one). */
|
|
45
|
+
apps?: string[];
|
|
46
|
+
/** Singleton apps to keep. */
|
|
47
|
+
services?: string[];
|
|
48
|
+
/** Path or url to scaffold from instead of the CLI's recorded source. */
|
|
49
|
+
source?: string;
|
|
50
|
+
/** Commit, tag or branch at that source. */
|
|
75
51
|
checkout?: string;
|
|
76
|
-
/**
|
|
77
|
-
|
|
52
|
+
/** Range for @multiplatform.one/* (default: ^<cli version at the source>). */
|
|
53
|
+
mpoVersion?: string;
|
|
54
|
+
skipInstall?: boolean;
|
|
55
|
+
skipGit?: boolean;
|
|
56
|
+
/** Non-interactive: no prompts, no services unless passed. */
|
|
57
|
+
yes?: boolean;
|
|
58
|
+
/** Scaffold into an existing (non-empty) directory. */
|
|
59
|
+
into?: boolean;
|
|
60
|
+
/** Show the scaffold's per-path progress. */
|
|
61
|
+
verbose?: boolean;
|
|
78
62
|
}
|
|
79
63
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
*/
|
|
84
|
-
export function runModifyStep(
|
|
85
|
-
dir: string,
|
|
86
|
-
selectedServices: string[],
|
|
87
|
-
selectedApps: string[],
|
|
88
|
-
): void {
|
|
89
|
-
const services = selectedServices as Service[];
|
|
90
|
-
const apps = selectedApps as App[];
|
|
64
|
+
function unique(values: string[]): string[] {
|
|
65
|
+
return [...new Set(values)];
|
|
66
|
+
}
|
|
91
67
|
|
|
68
|
+
export function validateServices(services: string[]): SingletonApp[] {
|
|
69
|
+
const known = new Set<string>(SINGLETON_APPS);
|
|
92
70
|
for (const service of services) {
|
|
93
|
-
if (!
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
71
|
+
if (!known.has(service)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Unknown service "${service}". Services are the singleton apps: ${SINGLETON_APPS.join(", ")}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return unique(services).sort() as SingletonApp[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function promptServices(): Promise<SingletonApp[]> {
|
|
81
|
+
const result = await inquirer.prompt([
|
|
82
|
+
{
|
|
83
|
+
message: "Which singleton apps should the project keep? (space to toggle, enter to confirm)",
|
|
84
|
+
name: "services",
|
|
85
|
+
type: "checkbox",
|
|
86
|
+
choices: SINGLETON_APPS.map((service) => ({
|
|
87
|
+
name: `${service} — ${SINGLETON_DESCRIPTIONS[service]}`,
|
|
88
|
+
value: service,
|
|
89
|
+
checked: false,
|
|
90
|
+
})),
|
|
91
|
+
},
|
|
92
|
+
]);
|
|
93
|
+
return validateServices(result.services as string[]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function insideGitRepo(dir: string): Promise<boolean> {
|
|
97
|
+
return spawn("git", ["rev-parse", "--is-inside-work-tree"], { cwd: dir }).then(
|
|
98
|
+
() => true,
|
|
99
|
+
() => false,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function scaffoldWithFallback(
|
|
104
|
+
spec: SourceSpec,
|
|
105
|
+
request: Parameters<typeof runScaffold>[0],
|
|
106
|
+
): Promise<Awaited<ReturnType<typeof runScaffold>>> {
|
|
107
|
+
try {
|
|
108
|
+
return await runScaffold({ ...request, source: spec.source, ref: spec.ref });
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (spec.origin !== "tag" || !(err instanceof ScaffoldError)) throw err;
|
|
111
|
+
console.warn(`⚠️ ${err.message}; retrying from main`);
|
|
112
|
+
return runScaffold({ ...request, source: spec.source, ref: "main" });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function init(nameArg: string | undefined, options: InitOptions = {}): Promise<void> {
|
|
117
|
+
const interactive = !options.yes && process.stdout.isTTY;
|
|
118
|
+
|
|
119
|
+
// --into: the argument is the directory; the product app is named after it.
|
|
120
|
+
let targetDir: string;
|
|
121
|
+
let name: string;
|
|
122
|
+
if (options.into) {
|
|
123
|
+
targetDir = resolve(nameArg ?? ".");
|
|
124
|
+
name = validateName(options.apps?.[0] ?? basename(targetDir));
|
|
125
|
+
} else {
|
|
126
|
+
let projectName = nameArg;
|
|
127
|
+
if (!projectName) {
|
|
128
|
+
if (!interactive) {
|
|
129
|
+
projectName = "my-app";
|
|
130
|
+
} else {
|
|
131
|
+
const result = await inquirer.prompt([
|
|
132
|
+
{ message: "What is the app name?", name: "name", type: "input", default: "my-app" },
|
|
133
|
+
]);
|
|
134
|
+
projectName = result.name as string;
|
|
132
135
|
}
|
|
133
136
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
cleanupPnpmWorkspace(dir, services, apps);
|
|
138
|
-
cleanupLaunchJson(dir, services, apps);
|
|
139
|
-
cleanupEnvFiles(dir, services, apps);
|
|
140
|
-
cleanupTsconfigs(dir);
|
|
141
|
-
cleanupMisc(dir);
|
|
142
|
-
removeLicenseHeaders(dir);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export async function init(nameArg: string | undefined, options: InitOptions) {
|
|
146
|
-
let projectName = nameArg;
|
|
147
|
-
if (!projectName) {
|
|
148
|
-
const result = await inquirer.prompt([
|
|
149
|
-
{
|
|
150
|
-
message: "What is the project name?",
|
|
151
|
-
name: "name",
|
|
152
|
-
type: "input",
|
|
153
|
-
default: "my-app",
|
|
154
|
-
},
|
|
155
|
-
]);
|
|
156
|
-
projectName = result.name as string;
|
|
137
|
+
name = validateName(projectName);
|
|
138
|
+
targetDir = resolve(name);
|
|
157
139
|
}
|
|
140
|
+
const apps = unique([name, ...(options.apps ?? []).map(validateName)]);
|
|
158
141
|
|
|
159
|
-
let
|
|
142
|
+
let services: SingletonApp[];
|
|
160
143
|
if (options.services) {
|
|
161
|
-
|
|
144
|
+
services = validateServices(options.services);
|
|
145
|
+
} else if (interactive) {
|
|
146
|
+
services = await promptServices();
|
|
162
147
|
} else {
|
|
163
|
-
|
|
164
|
-
{
|
|
165
|
-
message: "Select services:",
|
|
166
|
-
name: "services",
|
|
167
|
-
type: "checkbox",
|
|
168
|
-
choices: services.map((s) => ({ name: s, value: s })),
|
|
169
|
-
},
|
|
170
|
-
]);
|
|
171
|
-
selectedServices = result.services as Service[];
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
let selectedApps: App[];
|
|
175
|
-
if (options.apps) {
|
|
176
|
-
selectedApps = options.apps.split(",").map((s) => s.trim()) as App[];
|
|
177
|
-
} else {
|
|
178
|
-
const result = await inquirer.prompt([
|
|
179
|
-
{
|
|
180
|
-
message: "Select apps:",
|
|
181
|
-
name: "apps",
|
|
182
|
-
type: "checkbox",
|
|
183
|
-
choices: apps.map((a) => ({
|
|
184
|
-
name: a,
|
|
185
|
-
value: a,
|
|
186
|
-
checked: a === "one",
|
|
187
|
-
})),
|
|
188
|
-
},
|
|
189
|
-
]);
|
|
190
|
-
selectedApps = result.apps as App[];
|
|
148
|
+
services = [];
|
|
191
149
|
}
|
|
192
150
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (entries.length > 0) {
|
|
199
|
-
throw new Error(`Directory "${targetDir}" already exists and is not empty`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Clone repository (via script when provided by CLI, else inline)
|
|
204
|
-
const branch = options.checkout || "main";
|
|
205
|
-
console.log("\nCloning repository...");
|
|
206
|
-
if (options.cloneScript) {
|
|
207
|
-
execSync(`sh "${options.cloneScript}" "${repoUrl}" "${branch}" "${targetDir}"`, {
|
|
208
|
-
stdio: "inherit",
|
|
209
|
-
});
|
|
210
|
-
} else {
|
|
211
|
-
execSync(`git clone --depth 1 --branch "${branch}" ${repoUrl} "${targetDir}"`, {
|
|
212
|
-
stdio: "inherit",
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Remove .git to start fresh
|
|
217
|
-
rmSync(join(targetDir, ".git"), { recursive: true, force: true });
|
|
218
|
-
|
|
219
|
-
runModifyStep(targetDir, selectedServices, selectedApps);
|
|
220
|
-
|
|
221
|
-
// Init-only: update root package.json name
|
|
222
|
-
const rootPkgPath = join(targetDir, "package.json");
|
|
223
|
-
if (existsSync(rootPkgPath)) {
|
|
224
|
-
const rootPkg = JSON.parse(readFileSync(rootPkgPath, "utf-8"));
|
|
225
|
-
rootPkg.name = projectName;
|
|
226
|
-
rootPkg.packageManager = undefined;
|
|
227
|
-
writeFileSync(rootPkgPath, `${JSON.stringify(rootPkg, null, 2)}\n`);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Copy .env.example to .env if it exists
|
|
231
|
-
const envDefaultPath = join(targetDir, ".env.example");
|
|
232
|
-
const envPath = join(targetDir, ".env");
|
|
233
|
-
if (existsSync(envDefaultPath) && !existsSync(envPath)) {
|
|
234
|
-
writeFileSync(envPath, readFileSync(envDefaultPath, "utf-8"));
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Remove storybook baselines
|
|
238
|
-
const lostpixelBaseline = join(targetDir, "apps", "storybook", ".lostpixel", "baseline");
|
|
239
|
-
if (existsSync(lostpixelBaseline)) {
|
|
240
|
-
rmSync(lostpixelBaseline, { recursive: true });
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Remove pnpm-lock.yaml for fresh install
|
|
244
|
-
const lockfile = join(targetDir, "pnpm-lock.yaml");
|
|
245
|
-
if (existsSync(lockfile)) {
|
|
246
|
-
rmSync(lockfile);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Remove scaffolding artifacts
|
|
250
|
-
for (const artifact of [
|
|
251
|
-
"agent-os",
|
|
252
|
-
"public/cli/scripts/init.sh",
|
|
253
|
-
"public/cli/scripts/update.sh",
|
|
254
|
-
".changeset",
|
|
255
|
-
]) {
|
|
256
|
-
const p = join(targetDir, artifact);
|
|
257
|
-
if (existsSync(p)) {
|
|
258
|
-
rmSync(p, { recursive: true, force: true });
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Initialize project
|
|
263
|
-
console.log("\nInstalling dependencies...");
|
|
264
|
-
execSync("pnpm install", { cwd: targetDir, stdio: "inherit" });
|
|
265
|
-
|
|
266
|
-
console.log("\nInitializing git repository...");
|
|
267
|
-
execSync("git init", { cwd: targetDir, stdio: "inherit" });
|
|
268
|
-
execSync("git add -A", { cwd: targetDir, stdio: "inherit" });
|
|
269
|
-
execSync('git commit -m "Initial commit from multiplatform.one"', {
|
|
270
|
-
cwd: targetDir,
|
|
271
|
-
stdio: "inherit",
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
console.log(`\n✅ Project created at ${targetDir}`);
|
|
275
|
-
console.log("\nNext steps:");
|
|
276
|
-
console.log(` cd ${projectName}`);
|
|
277
|
-
console.log(" pnpm dev");
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function convertWorkspaceVersions(dir: string, versionMap: Map<string, string>) {
|
|
281
|
-
const files = findPackageJsonFiles(dir);
|
|
282
|
-
for (const file of files) {
|
|
283
|
-
const content = JSON.parse(readFileSync(file, "utf-8"));
|
|
284
|
-
let changed = false;
|
|
285
|
-
for (const depType of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
286
|
-
const deps = content[depType] as Record<string, string> | undefined;
|
|
287
|
-
if (!deps) continue;
|
|
288
|
-
for (const [depName, version] of Object.entries(deps)) {
|
|
289
|
-
if (typeof version === "string" && version.startsWith("workspace:")) {
|
|
290
|
-
const realVersion = versionMap.get(depName);
|
|
291
|
-
if (realVersion) {
|
|
292
|
-
// workspace:* -> ^version, workspace:^x -> ^x (keep the range)
|
|
293
|
-
const workspaceRange = version.replace("workspace:", "");
|
|
294
|
-
deps[depName] =
|
|
295
|
-
workspaceRange === "*" || workspaceRange === "^" ? `^${realVersion}` : workspaceRange;
|
|
296
|
-
changed = true;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
if (changed) {
|
|
302
|
-
writeFileSync(file, `${JSON.stringify(content, null, 2)}\n`);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function findPackageJsonFiles(dir: string): string[] {
|
|
308
|
-
const results: string[] = [];
|
|
309
|
-
function walk(d: string) {
|
|
310
|
-
let entries: Dirent[];
|
|
311
|
-
try {
|
|
312
|
-
entries = readdirSync(d, { withFileTypes: true });
|
|
313
|
-
} catch {
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
for (const entry of entries) {
|
|
317
|
-
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
318
|
-
const full = join(d, entry.name);
|
|
319
|
-
if (entry.isDirectory()) walk(full);
|
|
320
|
-
else if (entry.name === "package.json") results.push(full);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
walk(dir);
|
|
324
|
-
return results;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function cleanupPnpmWorkspace(dir: string, _services: Service[], _apps: App[]) {
|
|
328
|
-
const wsPath = join(dir, "pnpm-workspace.yaml");
|
|
329
|
-
if (!existsSync(wsPath)) return;
|
|
330
|
-
|
|
331
|
-
let content = readFileSync(wsPath, "utf-8");
|
|
332
|
-
|
|
333
|
-
// If no apps remain, remove apps/* entry
|
|
334
|
-
const appsDir = join(dir, "apps");
|
|
335
|
-
if (!existsSync(appsDir) || readdirSync(appsDir).length === 0) {
|
|
336
|
-
content = content.replace(/^\s*-\s*['"]?apps\/\*['"]?\s*$/gm, "");
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Check if packages dir still has entries
|
|
340
|
-
const pkgDir = join(dir, "packages");
|
|
341
|
-
if (!existsSync(pkgDir) || readdirSync(pkgDir).length === 0) {
|
|
342
|
-
content = content.replace(/^\s*-\s*['"]?packages\/\*['"]?\s*$/gm, "");
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Clean up multiple blank lines
|
|
346
|
-
content = `${content.replace(/\n{3,}/g, "\n\n").trim()}\n`;
|
|
347
|
-
writeFileSync(wsPath, content);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function cleanupLaunchJson(dir: string, services: Service[], apps: App[]) {
|
|
351
|
-
const launchPath = join(dir, ".vscode", "launch.json");
|
|
352
|
-
if (!existsSync(launchPath)) return;
|
|
353
|
-
|
|
354
|
-
const launch = JSON.parse(readFileSync(launchPath, "utf-8"));
|
|
355
|
-
const removedNames = new Set<string>();
|
|
356
|
-
|
|
357
|
-
// Collect names to remove for unselected services
|
|
358
|
-
for (const service of services) {
|
|
359
|
-
if (!services.includes(service)) {
|
|
360
|
-
for (const configName of serviceLaunchNames[service]) {
|
|
361
|
-
removedNames.add(configName);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Collect names to remove for unselected apps
|
|
367
|
-
for (const app of apps) {
|
|
368
|
-
if (!apps.includes(app)) {
|
|
369
|
-
for (const configName of appLaunchNames[app]) {
|
|
370
|
-
removedNames.add(configName);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Filter configurations
|
|
376
|
-
if (launch.configurations) {
|
|
377
|
-
launch.configurations = launch.configurations.filter(
|
|
378
|
-
(c: { name: string }) => !removedNames.has(c.name),
|
|
151
|
+
if (existsSync(targetDir) && readdirSync(targetDir).length > 0 && !options.into) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Directory "${targetDir}" already exists and is not empty. ` +
|
|
154
|
+
"Pass --into to scaffold into it (paths that already exist are kept and pinned in " +
|
|
155
|
+
".updateignore), or pick another name.",
|
|
379
156
|
);
|
|
380
157
|
}
|
|
381
158
|
|
|
382
|
-
|
|
383
|
-
if (
|
|
384
|
-
for (const compound of launch.compounds) {
|
|
385
|
-
if (compound.configurations) {
|
|
386
|
-
compound.configurations = compound.configurations.filter(
|
|
387
|
-
(configName: string) => !removedNames.has(configName),
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
launch.compounds = launch.compounds.filter(
|
|
392
|
-
(c: { configurations?: string[] }) => c.configurations && c.configurations.length > 0,
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
writeFileSync(launchPath, `${JSON.stringify(launch, null, 2)}\n`);
|
|
397
|
-
}
|
|
159
|
+
const spec = resolveSource({ source: options.source, checkout: options.checkout });
|
|
160
|
+
if (spec.warning) console.warn(`⚠️ ${spec.warning}`);
|
|
398
161
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
if (!existsSync(filePath)) continue;
|
|
162
|
+
console.log(`\nScaffolding ${targetDir} from ${spec.source} @ ${spec.ref}`);
|
|
163
|
+
console.log(` apps ${apps.join(", ")}`);
|
|
164
|
+
console.log(` services ${services.length ? services.join(", ") : "(none)"}`);
|
|
403
165
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Remove lines for unselected apps
|
|
416
|
-
for (const app of apps) {
|
|
417
|
-
if (!apps.includes(app)) {
|
|
418
|
-
const prefixes = appEnvPrefixes[app];
|
|
419
|
-
const sections = appEnvSections[app];
|
|
420
|
-
lines = filterEnvLines(lines, prefixes, sections);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Clean up multiple blank lines
|
|
425
|
-
const content = lines
|
|
426
|
-
.join("\n")
|
|
427
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
428
|
-
.trim();
|
|
429
|
-
writeFileSync(filePath, `${content}\n`);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function filterEnvLines(lines: string[], prefixes: string[], sections: string[]): string[] {
|
|
434
|
-
return lines.filter((line) => {
|
|
435
|
-
const trimmed = line.trim();
|
|
436
|
-
// Remove section headers
|
|
437
|
-
for (const section of sections) {
|
|
438
|
-
if (trimmed === section) return false;
|
|
439
|
-
}
|
|
440
|
-
// Remove lines with matching prefixes
|
|
441
|
-
for (const prefix of prefixes) {
|
|
442
|
-
if (prefix.endsWith("=")) {
|
|
443
|
-
// Exact match like "VR_DIFFING_ENGINE="
|
|
444
|
-
if (trimmed.startsWith(prefix)) return false;
|
|
445
|
-
} else {
|
|
446
|
-
// Prefix match like "FRAPPE_"
|
|
447
|
-
if (trimmed.startsWith(prefix) || trimmed.startsWith(`# ${prefix}`)) return false;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
return true;
|
|
166
|
+
const provenance = await scaffoldWithFallback(spec, {
|
|
167
|
+
source: spec.source,
|
|
168
|
+
ref: spec.ref,
|
|
169
|
+
target: targetDir,
|
|
170
|
+
apps,
|
|
171
|
+
services,
|
|
172
|
+
mpoVersion: options.mpoVersion ? mpoRange(options.mpoVersion) : undefined,
|
|
173
|
+
into: options.into,
|
|
174
|
+
verbose: options.verbose,
|
|
451
175
|
});
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
} catch {
|
|
479
|
-
// Skip files that can't be parsed
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function findTsconfigFiles(dir: string): string[] {
|
|
485
|
-
const results: string[] = [];
|
|
486
|
-
function walk(d: string) {
|
|
487
|
-
let entries: Dirent[];
|
|
488
|
-
try {
|
|
489
|
-
entries = readdirSync(d, { withFileTypes: true });
|
|
490
|
-
} catch {
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
for (const entry of entries) {
|
|
494
|
-
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
495
|
-
const full = join(d, entry.name);
|
|
496
|
-
if (entry.isDirectory()) walk(full);
|
|
497
|
-
else if (entry.name.startsWith("tsconfig") && entry.name.endsWith(".json")) {
|
|
498
|
-
results.push(full);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
walk(dir);
|
|
503
|
-
return results;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function stripJsonComments(str: string): string {
|
|
507
|
-
// Simple JSON comment stripper for tsconfig files
|
|
508
|
-
return str.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
function cleanupMisc(dir: string) {
|
|
512
|
-
// Remove docker dns dir (if generating from template)
|
|
513
|
-
for (const subdir of ["docker/dns"]) {
|
|
514
|
-
const p = join(dir, subdir);
|
|
515
|
-
if (existsSync(p)) {
|
|
516
|
-
rmSync(p, { recursive: true });
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// Remove VSCode psi-header config from settings.json
|
|
521
|
-
const settingsPath = join(dir, ".vscode", "settings.json");
|
|
522
|
-
if (existsSync(settingsPath)) {
|
|
523
|
-
try {
|
|
524
|
-
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
525
|
-
const psiKeys = Object.keys(settings).filter((k) => k.startsWith("psi-header"));
|
|
526
|
-
const cleaned = { ...settings };
|
|
527
|
-
for (const key of psiKeys) {
|
|
528
|
-
cleaned[key] = undefined;
|
|
529
|
-
}
|
|
530
|
-
// Filter out undefined keys by serializing
|
|
531
|
-
const filtered = JSON.parse(JSON.stringify(cleaned));
|
|
532
|
-
writeFileSync(settingsPath, `${JSON.stringify(filtered, null, 2)}\n`);
|
|
533
|
-
} catch {
|
|
534
|
-
// Skip if settings can't be parsed
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Remove psi-header from extensions.json recommendations
|
|
539
|
-
const extPath = join(dir, ".vscode", "extensions.json");
|
|
540
|
-
if (existsSync(extPath)) {
|
|
541
|
-
try {
|
|
542
|
-
const ext = JSON.parse(readFileSync(extPath, "utf-8"));
|
|
543
|
-
if (ext.recommendations) {
|
|
544
|
-
ext.recommendations = ext.recommendations.filter((r: string) => r !== "psioniq.psi-header");
|
|
545
|
-
}
|
|
546
|
-
writeFileSync(extPath, `${JSON.stringify(ext, null, 2)}\n`);
|
|
547
|
-
} catch {
|
|
548
|
-
// Skip if can't be parsed
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
const licenseIndicators = ["File:", "Project:", "File Created:", "Author:", "Licensed under"];
|
|
554
|
-
|
|
555
|
-
function removeLicenseHeaders(dir: string) {
|
|
556
|
-
const files = findSourceFiles(dir);
|
|
557
|
-
for (const file of files) {
|
|
558
|
-
try {
|
|
559
|
-
const content = readFileSync(file, "utf-8");
|
|
560
|
-
// Check for block comment license header at start of file
|
|
561
|
-
const match = content.match(/^\s*\/\*[\s\S]*?\*\//);
|
|
562
|
-
if (match) {
|
|
563
|
-
const comment = match[0];
|
|
564
|
-
const isLicense = licenseIndicators.some((indicator) => comment.includes(indicator));
|
|
565
|
-
if (isLicense && match.index !== undefined) {
|
|
566
|
-
const cleaned = content.slice(match.index + comment.length).replace(/^\s*\n/, "");
|
|
567
|
-
writeFileSync(file, cleaned);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
} catch {
|
|
571
|
-
// Skip files that can't be read
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// Also handle hash-style license headers for shell/yaml/python files
|
|
576
|
-
const hashFiles = findHashCommentFiles(dir);
|
|
577
|
-
for (const file of hashFiles) {
|
|
578
|
-
try {
|
|
579
|
-
const content = readFileSync(file, "utf-8");
|
|
580
|
-
const lines = content.split("\n");
|
|
581
|
-
let headerEnd = 0;
|
|
582
|
-
let inHeader = false;
|
|
583
|
-
|
|
584
|
-
for (let i = 0; i < lines.length; i++) {
|
|
585
|
-
const line = lines[i];
|
|
586
|
-
if (i === 0 && line.startsWith("#!")) {
|
|
587
|
-
// Skip shebang
|
|
588
|
-
continue;
|
|
589
|
-
}
|
|
590
|
-
if (
|
|
591
|
-
line.startsWith("# File:") ||
|
|
592
|
-
line.startsWith("# Project:") ||
|
|
593
|
-
line.startsWith("# File Created:") ||
|
|
594
|
-
line.startsWith("# Author:")
|
|
595
|
-
) {
|
|
596
|
-
inHeader = true;
|
|
597
|
-
headerEnd = i + 1;
|
|
598
|
-
continue;
|
|
599
|
-
}
|
|
600
|
-
if (inHeader && line.startsWith("#")) {
|
|
601
|
-
headerEnd = i + 1;
|
|
602
|
-
continue;
|
|
603
|
-
}
|
|
604
|
-
if (inHeader && line.trim() === "") {
|
|
605
|
-
headerEnd = i + 1;
|
|
606
|
-
break;
|
|
607
|
-
}
|
|
608
|
-
if (inHeader) break;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
if (inHeader && headerEnd > 0) {
|
|
612
|
-
const shebang = lines[0].startsWith("#!") ? `${lines[0]}\n\n` : "";
|
|
613
|
-
const remaining = lines
|
|
614
|
-
.slice(headerEnd)
|
|
615
|
-
.join("\n")
|
|
616
|
-
.replace(/^\s*\n/, "");
|
|
617
|
-
writeFileSync(file, `${shebang}${remaining}`);
|
|
618
|
-
}
|
|
619
|
-
} catch {
|
|
620
|
-
// Skip files that can't be read
|
|
621
|
-
}
|
|
176
|
+
const sha = provenance.source!.sha;
|
|
177
|
+
console.log(` source ${sha}`);
|
|
178
|
+
|
|
179
|
+
// A fresh project gets its scaffold commit; a directory already inside a
|
|
180
|
+
// repository (--into, or nested in another project) is left for the
|
|
181
|
+
// owner to commit.
|
|
182
|
+
let scaffoldCommitted = false;
|
|
183
|
+
if (!options.skipGit && !(await insideGitRepo(targetDir))) {
|
|
184
|
+
await spawn("git", ["init", "--quiet"], { cwd: targetDir });
|
|
185
|
+
await spawn("git", ["add", "-A"], { cwd: targetDir });
|
|
186
|
+
scaffoldCommitted = await spawn(
|
|
187
|
+
"git",
|
|
188
|
+
[
|
|
189
|
+
"commit",
|
|
190
|
+
"--quiet",
|
|
191
|
+
"-m",
|
|
192
|
+
`feat: scaffold from multiplatform.one ${sha.slice(0, 12)} (mpo init)`,
|
|
193
|
+
],
|
|
194
|
+
{ cwd: targetDir },
|
|
195
|
+
).then(
|
|
196
|
+
() => true,
|
|
197
|
+
() => {
|
|
198
|
+
console.warn("⚠️ git commit failed (missing git identity?) — repo left staged.");
|
|
199
|
+
return false;
|
|
200
|
+
},
|
|
201
|
+
);
|
|
622
202
|
}
|
|
623
|
-
}
|
|
624
203
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
for (const entry of entries) {
|
|
636
|
-
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
637
|
-
const full = join(d, entry.name);
|
|
638
|
-
if (entry.isDirectory()) walk(full);
|
|
639
|
-
else {
|
|
640
|
-
const ext = `.${entry.name.split(".").pop()}`;
|
|
641
|
-
if (extensions.has(ext)) results.push(full);
|
|
642
|
-
}
|
|
204
|
+
if (!options.skipInstall) {
|
|
205
|
+
console.log("\nInstalling dependencies...");
|
|
206
|
+
await spawn("pnpm", ["install"], { cwd: targetDir, stdio: "inherit" });
|
|
207
|
+
if (scaffoldCommitted) {
|
|
208
|
+
// The lockfile belongs to the scaffold commit so a fresh init is clean.
|
|
209
|
+
await spawn("git", ["add", "--", "pnpm-lock.yaml"], { cwd: targetDir })
|
|
210
|
+
.then(() => spawn("git", ["commit", "--amend", "--no-edit", "--quiet"], { cwd: targetDir }))
|
|
211
|
+
.catch(() => {});
|
|
643
212
|
}
|
|
213
|
+
} else {
|
|
214
|
+
console.log("Skipped pnpm install (--skip-install).");
|
|
644
215
|
}
|
|
645
|
-
walk(dir);
|
|
646
|
-
return results;
|
|
647
|
-
}
|
|
648
216
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
const full = join(d, entry.name);
|
|
662
|
-
if (entry.isDirectory()) walk(full);
|
|
663
|
-
else {
|
|
664
|
-
const ext = `.${entry.name.split(".").pop()}`;
|
|
665
|
-
if (extensions.has(ext) || entry.name === "Makefile") results.push(full);
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
walk(dir);
|
|
670
|
-
return results;
|
|
217
|
+
console.log(`\n✅ Project created at ${targetDir}`);
|
|
218
|
+
console.log("\nNext steps:");
|
|
219
|
+
if (!options.into) console.log(` cd ${basename(targetDir)}`);
|
|
220
|
+
if (options.skipInstall) console.log(" pnpm install");
|
|
221
|
+
if (options.into || options.skipGit) {
|
|
222
|
+
console.log(' git add -A && git commit -m "feat: scaffold from multiplatform.one (mpo init)"');
|
|
223
|
+
}
|
|
224
|
+
console.log(" pnpm dev # web + native dev server");
|
|
225
|
+
if (services.includes("frappe"))
|
|
226
|
+
console.log(" make frappe/dev # Frappe bench (see apps/frappe)");
|
|
227
|
+
console.log("\nLater: `mpo update` merges monorepo changes into the project without committing;");
|
|
228
|
+
console.log("`mpo add-app <name>` adds another product app.");
|
|
671
229
|
}
|