@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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Writes lib/source.json: the monorepo commit this build of the CLI came
|
|
3
|
+
// from, so a published `mpo init` clones the exact tree its templates and
|
|
4
|
+
// packages were cut from. Runs as the last step of `pnpm build`, and again
|
|
5
|
+
// from `package-train.mjs pack`, because a release builds before its version
|
|
6
|
+
// commit exists. A commit whose package.json carries another version is never
|
|
7
|
+
// stamped: scaffold.sh reads the @multiplatform.one range from that file at the
|
|
8
|
+
// stamped commit (MPO-346).
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { dirname, join, resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
16
|
+
const url = process.env.MPO_SOURCE_URL || pkg.repository.url.replace(/^git\+/, "");
|
|
17
|
+
|
|
18
|
+
function git(args) {
|
|
19
|
+
return execFileSync("git", args, {
|
|
20
|
+
cwd: root,
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
23
|
+
}).trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function committedVersion(rev) {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(git(["show", `${rev}:./package.json`])).version;
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let sha = process.env.MPO_SOURCE_SHA;
|
|
35
|
+
let skipped = "";
|
|
36
|
+
if (!sha) {
|
|
37
|
+
let head;
|
|
38
|
+
try {
|
|
39
|
+
head = git(["rev-parse", "HEAD"]);
|
|
40
|
+
} catch {
|
|
41
|
+
head = undefined;
|
|
42
|
+
}
|
|
43
|
+
const version = head && committedVersion(head);
|
|
44
|
+
if (version === pkg.version) sha = head;
|
|
45
|
+
else if (head)
|
|
46
|
+
skipped = ` (HEAD ${head.slice(0, 12)} carries ${version ?? "no"} cli version, not ${pkg.version})`;
|
|
47
|
+
}
|
|
48
|
+
const stamp = { cliVersion: pkg.version, url, ...(sha ? { sha } : {}) };
|
|
49
|
+
mkdirSync(join(root, "lib"), { recursive: true });
|
|
50
|
+
writeFileSync(join(root, "lib/source.json"), `${JSON.stringify(stamp, null, 2)}\n`);
|
|
51
|
+
process.stdout.write(`source: ${sha ?? "(no git sha)"}${skipped} ${url}\n`);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
7
|
+
|
|
8
|
+
const script = join(dirname(fileURLToPath(import.meta.url)), "stamp-source.mjs");
|
|
9
|
+
|
|
10
|
+
interface Stamp {
|
|
11
|
+
cliVersion: string;
|
|
12
|
+
sha?: string;
|
|
13
|
+
url: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("stamp-source", () => {
|
|
17
|
+
let repo: string;
|
|
18
|
+
let cli: string;
|
|
19
|
+
|
|
20
|
+
const git = (...args: string[]) =>
|
|
21
|
+
execFileSync("git", args, {
|
|
22
|
+
cwd: repo,
|
|
23
|
+
encoding: "utf8",
|
|
24
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
25
|
+
}).trim();
|
|
26
|
+
const setVersion = (version: string) =>
|
|
27
|
+
writeFileSync(
|
|
28
|
+
join(cli, "package.json"),
|
|
29
|
+
`${JSON.stringify({ name: "@multiplatform.one/cli", version, repository: { url: "git+https://git.example.invalid/mpo.git" } })}\n`,
|
|
30
|
+
);
|
|
31
|
+
const commit = (message: string) => {
|
|
32
|
+
git("add", "-A");
|
|
33
|
+
git(
|
|
34
|
+
"-c",
|
|
35
|
+
"user.name=fixture",
|
|
36
|
+
"-c",
|
|
37
|
+
"user.email=fixture@example.invalid",
|
|
38
|
+
"commit",
|
|
39
|
+
"-qm",
|
|
40
|
+
message,
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
const stamp = (): Stamp => {
|
|
44
|
+
execFileSync(process.execPath, [join(cli, "scripts/stamp-source.mjs")], {
|
|
45
|
+
env: { PATH: process.env.PATH },
|
|
46
|
+
stdio: "ignore",
|
|
47
|
+
});
|
|
48
|
+
return JSON.parse(readFileSync(join(cli, "lib/source.json"), "utf8")) as Stamp;
|
|
49
|
+
};
|
|
50
|
+
const cliVersionAt = (sha: string) =>
|
|
51
|
+
(JSON.parse(git("show", `${sha}:public/cli/package.json`)) as { version: string }).version;
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
repo = mkdtempSync(join(tmpdir(), "mpo-stamp-"));
|
|
55
|
+
cli = join(repo, "public/cli");
|
|
56
|
+
mkdirSync(join(cli, "scripts"), { recursive: true });
|
|
57
|
+
writeFileSync(join(cli, "scripts/stamp-source.mjs"), readFileSync(script));
|
|
58
|
+
writeFileSync(join(repo, ".gitignore"), "lib/\n");
|
|
59
|
+
setVersion("7.14.1");
|
|
60
|
+
git("init", "-q");
|
|
61
|
+
commit("7.14.1");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
rmSync(repo, { recursive: true, force: true });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("never names a commit whose CLI carries another version than the one it stamps", () => {
|
|
69
|
+
setVersion("7.14.2");
|
|
70
|
+
const written = stamp();
|
|
71
|
+
expect(written.cliVersion).toBe("7.14.2");
|
|
72
|
+
expect(written.sha && cliVersionAt(written.sha)).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("names the version commit once it exists", () => {
|
|
76
|
+
setVersion("8.0.0");
|
|
77
|
+
commit("chore(release): version packages");
|
|
78
|
+
const written = stamp();
|
|
79
|
+
expect(written).toEqual({
|
|
80
|
+
cliVersion: "8.0.0",
|
|
81
|
+
url: "https://git.example.invalid/mpo.git",
|
|
82
|
+
sha: git("rev-parse", "HEAD"),
|
|
83
|
+
});
|
|
84
|
+
expect(cliVersionAt(written.sha!)).toBe("8.0.0");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -2,7 +2,6 @@ import fsSync from "node:fs";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { generateVscodeConfig } from "../generateVscode.js";
|
|
6
5
|
import {
|
|
7
6
|
formatServiceList,
|
|
8
7
|
lookupProjectRoot,
|
|
@@ -17,10 +16,12 @@ import dotenv from "dotenv";
|
|
|
17
16
|
import spawn from "nano-spawn";
|
|
18
17
|
import YAML from "yaml";
|
|
19
18
|
import yoctoSpinner from "yocto-spinner";
|
|
19
|
+
import { addApp } from "../commands/addApp";
|
|
20
20
|
import { adoptApp } from "../commands/adoptApp";
|
|
21
|
+
import { generateDoctypeTypes, typesDrift } from "../commands/doctypeTypes";
|
|
21
22
|
import { discoverE2EApp, runE2ESession } from "../commands/e2e";
|
|
22
|
-
import {
|
|
23
|
-
import { INIT_APP_PIECES
|
|
23
|
+
import { SINGLETON_APPS, init } from "../commands/init";
|
|
24
|
+
import { INIT_APP_PIECES } from "../commands/pieces";
|
|
24
25
|
import { updateApp } from "../commands/updateApp";
|
|
25
26
|
|
|
26
27
|
const projectRoot = lookupProjectRoot();
|
|
@@ -217,100 +218,82 @@ program.version(
|
|
|
217
218
|
)?.version,
|
|
218
219
|
);
|
|
219
220
|
|
|
221
|
+
const collect = (value: string, previous: string[]) => [...previous, value];
|
|
222
|
+
|
|
220
223
|
program
|
|
221
224
|
.command("init")
|
|
222
|
-
.
|
|
223
|
-
.option("--
|
|
224
|
-
.option("--app", "alias of --web (back-compat)")
|
|
225
|
-
.option("--frappe", "add the Frappe backend piece (env keys, provider, docker compose bench)")
|
|
226
|
-
.option("--keycloak", "add the Keycloak auth piece (env keys, provider, docker compose + realm)")
|
|
225
|
+
.argument("[name]", "the project (and product app) name; with --into, the directory")
|
|
226
|
+
.option("--app <name>", "one more product app derived from apps/one (repeatable)", collect, [])
|
|
227
227
|
.option(
|
|
228
|
-
"--
|
|
229
|
-
|
|
228
|
+
"--service <name>",
|
|
229
|
+
`singleton app to keep (repeatable): ${SINGLETON_APPS.join(", ")}`,
|
|
230
|
+
collect,
|
|
231
|
+
[],
|
|
230
232
|
)
|
|
233
|
+
.option("--frappe", "same as --service frappe")
|
|
234
|
+
.option("--keycloak", "same as --service keycloak")
|
|
231
235
|
.option(
|
|
232
|
-
"--
|
|
233
|
-
"
|
|
236
|
+
"--into",
|
|
237
|
+
"scaffold into an existing directory (the argument, default .); paths already there are kept and pinned in .updateignore",
|
|
234
238
|
)
|
|
235
|
-
.option("--vscode", "add the VS Code extension target piece")
|
|
236
|
-
.option("--webext", "add the browser extension target piece (MV3 popup + background)")
|
|
237
239
|
.option(
|
|
238
|
-
"--
|
|
239
|
-
"
|
|
240
|
+
"--source <path|url>",
|
|
241
|
+
"monorepo to scaffold from (default: the commit this CLI was built from)",
|
|
240
242
|
)
|
|
241
|
-
.option("--
|
|
242
|
-
.option("--skip-git", "skip git init + scaffold commit")
|
|
243
|
+
.option("-c, --checkout <ref>", "commit, tag or branch at the source")
|
|
243
244
|
.option(
|
|
244
245
|
"--mpo-version <range>",
|
|
245
|
-
"semver range for @multiplatform.one/* (default: ^<cli version>)",
|
|
246
|
+
"semver range for @multiplatform.one/* (default: ^<cli version at the source>)",
|
|
246
247
|
)
|
|
247
|
-
.option("-
|
|
248
|
-
.option(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
)
|
|
252
|
-
.option("-c, --checkout <branch>", "branch, tag or commit to checkout (--monorepo only)", "main")
|
|
253
|
-
.option("-a, --apps <apps>", "comma-separated apps to include (--monorepo only)")
|
|
254
|
-
.option("-s, --services <services>", "comma-separated services to include (--monorepo only)")
|
|
255
|
-
.argument("[name]", "the name of the project")
|
|
248
|
+
.option("--skip-install", "skip pnpm install after scaffolding")
|
|
249
|
+
.option("--skip-git", "skip git init + scaffold commit")
|
|
250
|
+
.option("-y, --yes", "non-interactive: no prompts, no services unless passed")
|
|
251
|
+
.option("--verbose", "show what the scaffold removes and skips")
|
|
256
252
|
.description(
|
|
257
|
-
"scaffold a
|
|
253
|
+
"scaffold a project: the multiplatform.one monorepo at one commit, pruned (no public/, only the services asked for, apps/one renamed to the product app, @multiplatform.one/* at semver ranges)",
|
|
258
254
|
)
|
|
259
255
|
.action(async (name: string | undefined, options) => {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
)
|
|
270
|
-
) {
|
|
271
|
-
throw new Error("mpo cannot be initialized inside a git repository");
|
|
272
|
-
}
|
|
273
|
-
const cloneScript = path.resolve(
|
|
274
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
275
|
-
"../../scripts/clone.sh",
|
|
276
|
-
);
|
|
277
|
-
await init(name, { ...options, cloneScript });
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
if (options.apps || options.services) {
|
|
281
|
-
throw new Error(
|
|
282
|
-
"--apps/--services select monorepo pieces and require --monorepo (legacy clone mode)",
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
const webOnly = Boolean(options.web || options.app);
|
|
286
|
-
if (webOnly && options.universal) {
|
|
287
|
-
throw new Error("Pass either --web/--app or --universal, not both");
|
|
288
|
-
}
|
|
289
|
-
if (options.piecesOnly && (webOnly || options.universal)) {
|
|
290
|
-
throw new Error("--pieces-only scaffolds no base template — drop --web/--universal");
|
|
291
|
-
}
|
|
292
|
-
// Piece flags are multi-select; passing any of them skips the interactive
|
|
293
|
-
// pieces prompt. No flags + --yes (or non-TTY) → no pieces.
|
|
294
|
-
const pieces = INIT_APP_PIECES.filter((piece) => Boolean(options[piece]));
|
|
295
|
-
await initApp(name, {
|
|
256
|
+
const services: string[] = [...options.service];
|
|
257
|
+
if (options.frappe) services.push("frappe");
|
|
258
|
+
if (options.keycloak) services.push("keycloak");
|
|
259
|
+
await init(name, {
|
|
260
|
+
apps: options.app,
|
|
261
|
+
services: services.length || options.yes || !process.stdout.isTTY ? services : undefined,
|
|
262
|
+
source: options.source,
|
|
263
|
+
checkout: options.checkout,
|
|
264
|
+
mpoVersion: options.mpoVersion,
|
|
296
265
|
skipInstall: Boolean(options.skipInstall),
|
|
297
266
|
skipGit: Boolean(options.skipGit),
|
|
298
|
-
version: options.mpoVersion,
|
|
299
|
-
template: options.piecesOnly
|
|
300
|
-
? "none"
|
|
301
|
-
: webOnly
|
|
302
|
-
? "app"
|
|
303
|
-
: options.universal
|
|
304
|
-
? "universal"
|
|
305
|
-
: undefined,
|
|
306
267
|
yes: Boolean(options.yes),
|
|
307
|
-
|
|
268
|
+
into: Boolean(options.into),
|
|
269
|
+
verbose: Boolean(options.verbose),
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
program
|
|
274
|
+
.command("add-app")
|
|
275
|
+
.argument("<name>", "the new product app (apps/<name> + features/<name>)")
|
|
276
|
+
.option(
|
|
277
|
+
"--source <path|url>",
|
|
278
|
+
"monorepo holding the recorded source commit (default: the recorded url)",
|
|
279
|
+
)
|
|
280
|
+
.option("--skip-install", "skip pnpm install after the merge")
|
|
281
|
+
.option("--verbose", "show the scaffold's progress")
|
|
282
|
+
.description(
|
|
283
|
+
"add another product app from apps/one at the project's recorded source commit; the result is left uncommitted like mpo update",
|
|
284
|
+
)
|
|
285
|
+
.action(async (name: string, options) => {
|
|
286
|
+
await addApp(name, {
|
|
287
|
+
source: options.source,
|
|
288
|
+
skipInstall: Boolean(options.skipInstall),
|
|
289
|
+
verbose: Boolean(options.verbose),
|
|
308
290
|
});
|
|
309
291
|
});
|
|
310
292
|
|
|
311
293
|
program
|
|
312
294
|
.command("adopt")
|
|
313
|
-
.option("--frappe", "the project adopted the Frappe
|
|
295
|
+
.option("--frappe", "the project adopted the Frappe client piece (env keys, provider)")
|
|
296
|
+
.option("--frappe-bench", "the project adopted the Frappe dev bench piece (docker compose)")
|
|
314
297
|
.option("--keycloak", "the project adopted the Keycloak auth piece")
|
|
315
298
|
.option("--gnome", "the project adopted the GNOME desktop target piece")
|
|
316
299
|
.option("--tauri", "the project adopted the Tauri desktop target piece")
|
|
@@ -322,10 +305,12 @@ program
|
|
|
322
305
|
)
|
|
323
306
|
.option("-y, --yes", "non-interactive: never prompt (pieces must come from flags)")
|
|
324
307
|
.description(
|
|
325
|
-
"bring an existing, never-scaffolded project under `mpo update` management: writes .mpo.json provenance (and seeds .updateignore) without changing any project file — the FIRST `mpo update` afterwards reconciles piece files (real diffs expected)",
|
|
308
|
+
"bring an existing, never-scaffolded project that hand-copied piece files under `mpo update` management: writes .mpo.json provenance (and seeds .updateignore) without changing any project file — the FIRST `mpo update` afterwards reconciles piece files (real diffs expected). For the whole monorepo scaffold use `mpo init --into`",
|
|
326
309
|
)
|
|
327
310
|
.action(async (options) => {
|
|
328
|
-
const pieces = INIT_APP_PIECES.filter((piece) =>
|
|
311
|
+
const pieces = INIT_APP_PIECES.filter((piece) =>
|
|
312
|
+
Boolean(options[piece.replace(/-(\w)/gu, (_, letter: string) => letter.toUpperCase())]),
|
|
313
|
+
);
|
|
329
314
|
await adoptApp({
|
|
330
315
|
pieces: pieces.length ? pieces : undefined,
|
|
331
316
|
name: options.name,
|
|
@@ -333,164 +318,31 @@ program
|
|
|
333
318
|
});
|
|
334
319
|
});
|
|
335
320
|
|
|
336
|
-
const defaultUpdateRemote = "https://gitlab.com/bitspur/frappe/multiplatform.one.git";
|
|
337
|
-
|
|
338
321
|
program
|
|
339
322
|
.command("update")
|
|
340
323
|
.option(
|
|
341
|
-
"
|
|
342
|
-
"
|
|
343
|
-
"main",
|
|
324
|
+
"--source <path|url>",
|
|
325
|
+
"monorepo to update from (default: the commit this CLI was built from)",
|
|
344
326
|
)
|
|
345
|
-
.option("-
|
|
346
|
-
.option("--skip-install", "skip pnpm install after the
|
|
327
|
+
.option("-c, --checkout <ref>", "commit, tag or branch at the source")
|
|
328
|
+
.option("--skip-install", "skip pnpm install after the merge")
|
|
347
329
|
.option(
|
|
348
330
|
"--mpo-version <range>",
|
|
349
|
-
"semver range for @multiplatform.one/* (default: ^<cli version>)",
|
|
350
|
-
)
|
|
351
|
-
.option(
|
|
352
|
-
"--assume-version <cliVersion>",
|
|
353
|
-
"bootstrap missing .mpo.json: assume the project was scaffolded by this exact CLI version, record provenance, then update",
|
|
354
|
-
)
|
|
355
|
-
.option(
|
|
356
|
-
"--template <template>",
|
|
357
|
-
"template recorded by --assume-version (universal|app, default universal)",
|
|
331
|
+
"semver range for @multiplatform.one/* (default: ^<cli version at the source>)",
|
|
358
332
|
)
|
|
333
|
+
.option("--verbose", "show the scaffold's progress")
|
|
359
334
|
.description(
|
|
360
|
-
"
|
|
335
|
+
"three-way merge the monorepo's changes into this project (base = the scaffold at the recorded source commit, ours = HEAD, theirs = the scaffold at the current commit); lands in the index and worktree, never commits; refuses a dirty tree",
|
|
361
336
|
)
|
|
362
|
-
.action(
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
skipInstall
|
|
367
|
-
mpoVersion
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
// Update prechecks: must be in a git repo
|
|
372
|
-
if (
|
|
373
|
-
await spawn("git", ["rev-parse", "--is-inside-work-tree"]).then(
|
|
374
|
-
() => false,
|
|
375
|
-
() => true,
|
|
376
|
-
)
|
|
377
|
-
) {
|
|
378
|
-
throw new Error("mpo cannot be updated outside of a git repository");
|
|
379
|
-
}
|
|
380
|
-
if (options.template && !options.assumeVersion) {
|
|
381
|
-
throw new Error("--template only applies to the --assume-version bootstrap");
|
|
382
|
-
}
|
|
383
|
-
if (options.template && options.template !== "universal" && options.template !== "app") {
|
|
384
|
-
throw new Error(`--template must be universal or app, got "${options.template}"`);
|
|
385
|
-
}
|
|
386
|
-
// Scaffolded consumer projects carry .mpo.json provenance — use the
|
|
387
|
-
// copier-style three-way template update (--assume-version bootstraps
|
|
388
|
-
// the provenance for pre-6.3 scaffolds first). Monorepo forks
|
|
389
|
-
// (identified by the legacy features/package.json marker) fall through
|
|
390
|
-
// to the upstream-merge flow below; anything else gets the provenance
|
|
391
|
-
// guidance instead of the fork flow's unrelated errors.
|
|
392
|
-
if (readProvenance(projectRoot) || options.assumeVersion) {
|
|
393
|
-
await updateApp({
|
|
394
|
-
skipInstall: options.skipInstall,
|
|
395
|
-
version: options.mpoVersion,
|
|
396
|
-
assumeVersion: options.assumeVersion,
|
|
397
|
-
assumeTemplate: options.template as "universal" | "app" | undefined,
|
|
398
|
-
});
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
const legacyFork = await fs.stat(path.resolve(projectRoot, "features/package.json")).then(
|
|
402
|
-
(stat) => stat.isFile(),
|
|
403
|
-
() => false,
|
|
404
|
-
);
|
|
405
|
-
if (!legacyFork) {
|
|
406
|
-
throw new Error(
|
|
407
|
-
".mpo.json not found — this project predates scaffold provenance.\n" +
|
|
408
|
-
" - scaffolded by an old CLI (pre-6.3)? bootstrap it:\n" +
|
|
409
|
-
" mpo update --assume-version <cliVersion> [--template universal|app]\n" +
|
|
410
|
-
" - never scaffolded (hand-adopted pieces)? bring it under management:\n" +
|
|
411
|
-
" mpo adopt --<piece> [...]",
|
|
412
|
-
);
|
|
413
|
-
}
|
|
414
|
-
// Require clean working tree (no staged or unstaged changes)
|
|
415
|
-
if (
|
|
416
|
-
await spawn("git", ["diff", "--cached", "--quiet"]).then(
|
|
417
|
-
() => false,
|
|
418
|
-
() => true,
|
|
419
|
-
)
|
|
420
|
-
) {
|
|
421
|
-
throw new Error("mpo cannot be updated with uncommitted changes (staged changes present)");
|
|
422
|
-
}
|
|
423
|
-
if (
|
|
424
|
-
await spawn("git", ["diff", "--quiet"]).then(
|
|
425
|
-
() => false,
|
|
426
|
-
() => true,
|
|
427
|
-
)
|
|
428
|
-
) {
|
|
429
|
-
throw new Error(
|
|
430
|
-
"multiplatform.one cannot be updated with uncommitted changes (unstaged changes present)",
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
// Validate multiplatform.one project with clear errors per failure
|
|
434
|
-
const rootPkgPath = path.resolve(projectRoot, "package.json");
|
|
435
|
-
const featuresPkgPath = path.resolve(projectRoot, "features/package.json");
|
|
436
|
-
try {
|
|
437
|
-
const stat = await fs.stat(rootPkgPath);
|
|
438
|
-
if (!stat.isFile()) {
|
|
439
|
-
throw new Error("mpo update requires a valid project: root package.json not found");
|
|
440
|
-
}
|
|
441
|
-
} catch (err) {
|
|
442
|
-
if (err instanceof Error && err.message.includes("valid project")) {
|
|
443
|
-
throw err;
|
|
444
|
-
}
|
|
445
|
-
throw new Error("mpo update requires a valid project: root package.json not found");
|
|
446
|
-
}
|
|
447
|
-
try {
|
|
448
|
-
const stat = await fs.stat(featuresPkgPath);
|
|
449
|
-
if (!stat.isFile()) {
|
|
450
|
-
throw new Error("mpo update requires a valid project: features/package.json not found");
|
|
451
|
-
}
|
|
452
|
-
} catch (err) {
|
|
453
|
-
if (err instanceof Error && err.message.includes("valid project")) {
|
|
454
|
-
throw err;
|
|
455
|
-
}
|
|
456
|
-
throw new Error("mpo update requires a valid project: features/package.json not found");
|
|
457
|
-
}
|
|
458
|
-
const featuresPkg = JSON.parse(await fs.readFile(featuresPkgPath, "utf8")) as {
|
|
459
|
-
dependencies?: Record<string, string>;
|
|
460
|
-
};
|
|
461
|
-
if (
|
|
462
|
-
!featuresPkg?.dependencies?.["multiplatform.one"] ||
|
|
463
|
-
!String(featuresPkg.dependencies["multiplatform.one"]).length
|
|
464
|
-
) {
|
|
465
|
-
throw new Error(
|
|
466
|
-
"mpo update requires a valid project: features must depend on multiplatform.one",
|
|
467
|
-
);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Merge from upstream (update.sh: add remote, fetch, merge, remove remote, restore .updateignore list, pnpm lint)
|
|
471
|
-
const updateScript = path.resolve(
|
|
472
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
473
|
-
"../../scripts/update.sh",
|
|
474
|
-
);
|
|
475
|
-
await spawn("sh", [updateScript, options.remote, options.checkout], {
|
|
476
|
-
cwd: projectRoot,
|
|
477
|
-
stdio: "inherit",
|
|
478
|
-
shell: true,
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
// Re-apply modify step with auto-detected apps and services
|
|
482
|
-
const presentServices = await getPresentServices(projectRoot);
|
|
483
|
-
const presentApps = await getPresentApps(projectRoot);
|
|
484
|
-
runModifyStep(projectRoot, presentServices, presentApps);
|
|
485
|
-
|
|
486
|
-
// Post-update refresh: .vscode from current workspace
|
|
487
|
-
try {
|
|
488
|
-
await generateVscodeConfig(projectRoot);
|
|
489
|
-
} catch {
|
|
490
|
-
// may fail in older projects without apps/ layout
|
|
491
|
-
}
|
|
492
|
-
},
|
|
493
|
-
);
|
|
337
|
+
.action(async (options) => {
|
|
338
|
+
await updateApp({
|
|
339
|
+
source: options.source,
|
|
340
|
+
checkout: options.checkout,
|
|
341
|
+
skipInstall: Boolean(options.skipInstall),
|
|
342
|
+
version: options.mpoVersion,
|
|
343
|
+
verbose: Boolean(options.verbose),
|
|
344
|
+
});
|
|
345
|
+
});
|
|
494
346
|
|
|
495
347
|
program
|
|
496
348
|
.command("wait")
|
|
@@ -598,6 +450,44 @@ frappe
|
|
|
598
450
|
);
|
|
599
451
|
});
|
|
600
452
|
|
|
453
|
+
frappe
|
|
454
|
+
.command("types")
|
|
455
|
+
.description(
|
|
456
|
+
"generate TypeScript types for doctypes from getdoctype (auth: $FRAPPE_API_KEY, $FRAPPE_API_SECRET)",
|
|
457
|
+
)
|
|
458
|
+
.argument("<doctypes...>", "doctype names; child tables come along")
|
|
459
|
+
.option("-o, --out <file>", "write the module here instead of stdout")
|
|
460
|
+
.option("--check", "exit 1 and print the diff when --out differs from the server")
|
|
461
|
+
.option("--url <url>", "frappe base URL (default $FRAPPE_URL or http://localhost:8000)")
|
|
462
|
+
.action(async (doctypes: string[], options) => {
|
|
463
|
+
const parentOptions = frappe.opts();
|
|
464
|
+
dotenv.config({ path: parentOptions.dotenv || defaultDotenvPath, quiet: true });
|
|
465
|
+
if (options.check && !options.out) {
|
|
466
|
+
console.error("--check needs --out <file>");
|
|
467
|
+
process.exit(2);
|
|
468
|
+
}
|
|
469
|
+
const generated = await generateDoctypeTypes(doctypes, {
|
|
470
|
+
baseURL: options.url || process.env.FRAPPE_URL || "http://localhost:8000",
|
|
471
|
+
apiKey: process.env.FRAPPE_API_KEY,
|
|
472
|
+
apiSecret: process.env.FRAPPE_API_SECRET,
|
|
473
|
+
});
|
|
474
|
+
if (!options.out) {
|
|
475
|
+
process.stdout.write(generated);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const out = path.resolve(process.cwd(), options.out);
|
|
479
|
+
if (options.check) {
|
|
480
|
+
const committed = fsSync.existsSync(out) ? await fs.readFile(out, "utf8") : "";
|
|
481
|
+
const drift = typesDrift(committed, generated);
|
|
482
|
+
if (drift.length > 0) {
|
|
483
|
+
console.error(`${options.out} drifted from the server:\n${drift.join("\n")}`);
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
await fs.writeFile(out, generated);
|
|
489
|
+
});
|
|
490
|
+
|
|
601
491
|
program
|
|
602
492
|
.command("build")
|
|
603
493
|
.argument(
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mpo add-app <name>`: one more product app from apps/one, merged in at the
|
|
3
|
+
* commit the project was scaffolded from.
|
|
4
|
+
*
|
|
5
|
+
* base = scaffold(recorded sha, recorded apps)
|
|
6
|
+
* theirs = scaffold(recorded sha, recorded apps + name)
|
|
7
|
+
* ours = HEAD
|
|
8
|
+
*
|
|
9
|
+
* Same commit on both sides, so the diff is exactly the new app and the
|
|
10
|
+
* root wiring that names it; the project's own changes are untouched. The
|
|
11
|
+
* result stays uncommitted like `mpo update`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { rmSync } from "node:fs";
|
|
15
|
+
import { basename, join } from "node:path";
|
|
16
|
+
import { finishMerge, landTree, mergeScaffolds, projectRootOf, requireCleanTree } from "./merge";
|
|
17
|
+
import {
|
|
18
|
+
PROVENANCE_FILE,
|
|
19
|
+
cliVersion,
|
|
20
|
+
packageJsonName,
|
|
21
|
+
readProvenance,
|
|
22
|
+
validateName,
|
|
23
|
+
writeProvenance,
|
|
24
|
+
} from "./provenance";
|
|
25
|
+
import { scaffoldToTemp } from "./scaffold";
|
|
26
|
+
|
|
27
|
+
export interface AddAppOptions {
|
|
28
|
+
/** Path or url holding the recorded source commit (default: the recorded url). */
|
|
29
|
+
source?: string;
|
|
30
|
+
skipInstall?: boolean;
|
|
31
|
+
verbose?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function addApp(nameArg: string, options: AddAppOptions = {}): Promise<void> {
|
|
35
|
+
const name = validateName(nameArg);
|
|
36
|
+
const projectDir = await projectRootOf("mpo add-app");
|
|
37
|
+
await requireCleanTree(projectDir, "mpo add-app");
|
|
38
|
+
|
|
39
|
+
const provenance = readProvenance(projectDir);
|
|
40
|
+
if (!provenance) {
|
|
41
|
+
throw new Error(`${PROVENANCE_FILE} not found — mpo add-app needs a project from mpo init`);
|
|
42
|
+
}
|
|
43
|
+
if (!provenance.source) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
"mpo add-app needs a project scaffolded from the monorepo (a source commit in " +
|
|
46
|
+
`${PROVENANCE_FILE}); adopted and template-era projects run mpo update first`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (provenance.apps.includes(name)) {
|
|
50
|
+
throw new Error(`apps/${name} is already a product app of this project`);
|
|
51
|
+
}
|
|
52
|
+
const source = options.source ?? provenance.source.url;
|
|
53
|
+
if (!source) {
|
|
54
|
+
throw new Error(`${PROVENANCE_FILE} records no source url — pass --source <path or url>`);
|
|
55
|
+
}
|
|
56
|
+
const { sha } = provenance.source;
|
|
57
|
+
const common = {
|
|
58
|
+
source,
|
|
59
|
+
ref: sha,
|
|
60
|
+
services: provenance.services,
|
|
61
|
+
mpoVersion: `^${provenance.cliVersion}`,
|
|
62
|
+
name: packageJsonName(projectDir) ?? basename(projectDir),
|
|
63
|
+
verbose: options.verbose,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
console.log(
|
|
67
|
+
`\nmpo add-app ${name}: ${provenance.apps.join(", ")} + ${name} @ ${sha.slice(0, 12)}`,
|
|
68
|
+
);
|
|
69
|
+
const base = await scaffoldToTemp({ ...common, apps: provenance.apps });
|
|
70
|
+
const theirs = await scaffoldToTemp({ ...common, apps: [...provenance.apps, name] }).catch(
|
|
71
|
+
(err: unknown) => {
|
|
72
|
+
base.cleanup();
|
|
73
|
+
throw err;
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
try {
|
|
77
|
+
for (const dir of [base.dir, theirs.dir]) {
|
|
78
|
+
rmSync(join(dir, PROVENANCE_FILE), { force: true });
|
|
79
|
+
}
|
|
80
|
+
const outcome = await mergeScaffolds({
|
|
81
|
+
projectDir,
|
|
82
|
+
baseDir: base.dir,
|
|
83
|
+
theirsDir: theirs.dir,
|
|
84
|
+
baseLabel: `mpo scaffold ${sha} (${provenance.apps.join(",")})`,
|
|
85
|
+
theirsLabel: `mpo scaffold ${sha} (${[...provenance.apps, name].join(",")})`,
|
|
86
|
+
});
|
|
87
|
+
if (!outcome.changed && !outcome.conflicts.length) {
|
|
88
|
+
console.log("\n✅ nothing to add (the project already carries this app)");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
await landTree(projectDir, outcome.mergedTree);
|
|
92
|
+
writeProvenance(projectDir, {
|
|
93
|
+
...provenance,
|
|
94
|
+
cliVersion: cliVersion() ?? provenance.cliVersion,
|
|
95
|
+
apps: [...provenance.apps, name],
|
|
96
|
+
});
|
|
97
|
+
await finishMerge(projectDir, outcome, `feat: add app ${name} (mpo add-app)`, options);
|
|
98
|
+
} finally {
|
|
99
|
+
base.cleanup();
|
|
100
|
+
theirs.cleanup();
|
|
101
|
+
}
|
|
102
|
+
}
|