@multiplatform.one/cli 6.7.0 → 7.0.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 +83 -8
- package/lib/bin/multiplatformOne.mjs +21 -7
- package/lib/commands/adoptApp.mjs +127 -0
- package/lib/commands/init.mjs +1 -1
- package/lib/commands/initApp.mjs +43 -15
- package/lib/commands/updateApp.mjs +84 -15
- package/package.json +2 -2
- package/scripts/frappe-app-name.py +125 -0
- package/scripts/frappe-app-name.spec.ts +191 -0
- package/scripts/frappe-bootstrap.sh +8 -30
- package/src/bin/multiplatformOne.ts +78 -13
- package/src/commands/adoptApp.spec.ts +208 -0
- package/src/commands/adoptApp.ts +185 -0
- package/src/commands/initApp.spec.ts +152 -12
- package/src/commands/initApp.ts +95 -21
- package/src/commands/updateApp.spec.ts +314 -2
- package/src/commands/updateApp.ts +164 -33
- package/templates/app/apps/__NAME__/package.json +1 -1
- package/templates/app/features/__NAME__/package.json +1 -1
- package/templates/pieces/gnome/universal/README.md.partial +5 -5
- package/templates/pieces/gnome/universal/apps/__NAME__/gnome/anchor.tsx +39 -0
- package/templates/pieces/gnome/universal/apps/__NAME__/gnome/main.tsx +4 -2
- package/templates/pieces/gnome/universal/apps/__NAME__/gnome/shims/components.ts +26 -0
- package/templates/pieces/gnome/universal/apps/__NAME__/gnome/shims/forms.ts +26 -0
- package/templates/pieces/gnome/universal/apps/__NAME__/gnome/shims/frappe-ui.ts +17 -0
- package/templates/pieces/gnome/universal/apps/__NAME__/gnome/shims/one.ts +3 -0
- package/templates/pieces/gnome/universal/apps/__NAME__/gnome/shims/theme.ts +21 -0
- package/templates/pieces/gnome/universal/apps/__NAME__/gnome/tamagui-barrel.ts +21 -0
- package/templates/pieces/gnome/universal/apps/__NAME__/vite.config.gnome.ts +49 -7
- package/templates/pieces/keycloak/universal/README.md.partial +13 -0
- package/templates/pieces/keycloak/universal/docker/compose.keycloak.yaml +19 -3
- package/templates/pieces/keycloak/universal/env.example.partial +4 -1
- package/templates/pieces/vscode/universal/apps/__NAME__/package.json.partial +1 -1
- package/templates/pieces/webext/universal/apps/__NAME__/package.json.partial +2 -2
- package/templates/universal/apps/__NAME__/package.json +2 -2
- package/templates/universal/packages/themes/package.json +2 -2
- package/types/bin/multiplatformOne.d.ts.map +1 -1
- package/types/commands/adoptApp.d.ts +25 -0
- package/types/commands/adoptApp.d.ts.map +1 -0
- package/types/commands/initApp.d.ts +33 -5
- package/types/commands/initApp.d.ts.map +1 -1
- package/types/commands/updateApp.d.ts +12 -2
- package/types/commands/updateApp.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -52,23 +52,98 @@ the interactive prompt; they are recorded in `.mpo.json` and reproduced by
|
|
|
52
52
|
| `--frappe` | Frappe backend wiring — env keys, provider, dev bench compose | Docker |
|
|
53
53
|
| `--keycloak` | Keycloak auth — env keys, provider, compose + realm import | Docker |
|
|
54
54
|
| `--gnome` | GNOME desktop: **real GTK4 widgets** via react-gnome, no webview | gjs, gtk4, libadwaita |
|
|
55
|
-
| `--tauri` | Desktop: the web build in a system webview | Rust toolchain |
|
|
56
55
|
| `--vscode` | VS Code extension — extension host + Tamagui webview | — |
|
|
57
56
|
| `--webext` | Browser extension — MV3 popup + background | — |
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
`--gnome` is the desktop piece. It renders your Tamagui components as native
|
|
59
|
+
GTK widgets laid out by Yoga — not the web build in a frame — so the app looks
|
|
60
|
+
and behaves like part of the desktop. Linux-first.
|
|
61
|
+
|
|
62
|
+
## Update a project
|
|
63
|
+
|
|
64
|
+
`mpo update` upgrades a project to the current templates with a three-way
|
|
65
|
+
merge driven by `.mpo.json` (written at init time): base = the original
|
|
66
|
+
scaffold regenerated at the recorded CLI version, ours = your HEAD, theirs =
|
|
67
|
+
a fresh scaffold from the current CLI. `.updateignore` entries (exact files or directory prefixes — not globs) (and
|
|
68
|
+
`pnpm-lock.yaml`) stay pinned to your version; `package.json` dependency-range
|
|
69
|
+
drift auto-resolves to the newer minimum.
|
|
70
|
+
|
|
71
|
+
**Idempotency contract:** re-running `mpo update` immediately after a
|
|
72
|
+
successful update makes zero changes (clean `git status`). Run it, see no
|
|
73
|
+
changes, know you're current.
|
|
74
|
+
|
|
75
|
+
### Adopt an existing project
|
|
76
|
+
|
|
77
|
+
A project that was never scaffolded — it hand-copied a delivery target
|
|
78
|
+
(webext, gnome, …) and uses `@multiplatform.one/*` from npm — has no
|
|
79
|
+
provenance. Bring it under `mpo update` management:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
mpo adopt --webext --name my-app # requires a clean git tree; ≥ 1 piece
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Adoption writes `.mpo.json` (template `"none"` + pieces) and seeds
|
|
86
|
+
`.updateignore`; **it changes no project files**. The FIRST `mpo update`
|
|
87
|
+
afterwards reconciles the piece files from an empty merge base, so diffs and
|
|
88
|
+
conflicts on hand-copied files are expected — resolve and commit. Later
|
|
89
|
+
updates are ordinary three-way merges (baselines come from
|
|
90
|
+
`mpo init --pieces-only` at the recorded version).
|
|
91
|
+
|
|
92
|
+
### Bootstrap missing provenance
|
|
93
|
+
|
|
94
|
+
Scaffolded before `.mpo.json` existed (CLI ≤ 6.3.0)? Assert the version it
|
|
95
|
+
was scaffolded with instead of hand-writing the file:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
mpo update --assume-version 6.3.0 [--template universal|app]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Update a project
|
|
102
|
+
|
|
103
|
+
`mpo update` upgrades a project to the current templates with a three-way
|
|
104
|
+
merge driven by `.mpo.json` (written at init time): base = the original
|
|
105
|
+
scaffold regenerated at the recorded CLI version, ours = your HEAD, theirs =
|
|
106
|
+
a fresh scaffold from the current CLI. `.updateignore` entries (exact files or directory prefixes — not globs) (and
|
|
107
|
+
`pnpm-lock.yaml`) stay pinned to your version; `package.json` dependency-range
|
|
108
|
+
drift auto-resolves to the newer minimum.
|
|
109
|
+
|
|
110
|
+
**Idempotency contract:** re-running `mpo update` immediately after a
|
|
111
|
+
successful update makes zero changes (clean `git status`). Run it, see no
|
|
112
|
+
changes, know you're current.
|
|
113
|
+
|
|
114
|
+
### Adopt an existing project
|
|
115
|
+
|
|
116
|
+
A project that was never scaffolded — it hand-copied a delivery target
|
|
117
|
+
(webext, tauri, …) and uses `@multiplatform.one/*` from npm — has no
|
|
118
|
+
provenance. Bring it under `mpo update` management:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
mpo adopt --webext --name my-app # requires a clean git tree; ≥ 1 piece
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Adoption writes `.mpo.json` (template `"none"` + pieces) and seeds
|
|
125
|
+
`.updateignore`; **it changes no project files**. The FIRST `mpo update`
|
|
126
|
+
afterwards reconciles the piece files from an empty merge base, so diffs and
|
|
127
|
+
conflicts on hand-copied files are expected — resolve and commit. Later
|
|
128
|
+
updates are ordinary three-way merges (baselines come from
|
|
129
|
+
`mpo init --pieces-only` at the recorded version).
|
|
130
|
+
|
|
131
|
+
### Bootstrap missing provenance
|
|
132
|
+
|
|
133
|
+
Scaffolded before `.mpo.json` existed (CLI ≤ 6.3.0)? Assert the version it
|
|
134
|
+
was scaffolded with instead of hand-writing the file:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
mpo update --assume-version 6.3.0 [--template universal|app]
|
|
138
|
+
```
|
|
65
139
|
|
|
66
140
|
## Other commands
|
|
67
141
|
|
|
68
142
|
| Command | Purpose |
|
|
69
143
|
| ---------------------------- | -------------------------------------------------------------------- |
|
|
70
144
|
| `mpo init --monorepo [name]` | DEPRECATED: clone/modify the full monorepo (framework dev forks) |
|
|
71
|
-
| `mpo
|
|
145
|
+
| `mpo adopt` | Write provenance into an existing, never-scaffolded project |
|
|
146
|
+
| `mpo update` | Three-way template update via `.mpo.json` (legacy merge for forks) |
|
|
72
147
|
| `mpo wait` | Wait for a service (frappe, postgres, mariadb, keycloak) to be ready |
|
|
73
148
|
| `mpo frappe bootstrap` | Bootstrap the Frappe development environment |
|
|
74
149
|
| `mpo frappe dev` | Start the Frappe development server |
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { generateVscodeConfig } from "../generateVscode.mjs";
|
|
2
|
+
import { INIT_APP_PIECES, initApp, readProvenance } from "../commands/initApp.mjs";
|
|
3
|
+
import { adoptApp } from "../commands/adoptApp.mjs";
|
|
2
4
|
import { discoverE2EApp, runE2ESession } from "../commands/e2e.mjs";
|
|
3
5
|
import { init, runModifyStep } from "../commands/init.mjs";
|
|
4
|
-
import { INIT_APP_PIECES, initApp, readProvenance } from "../commands/initApp.mjs";
|
|
5
6
|
import { updateApp } from "../commands/updateApp.mjs";
|
|
6
7
|
import { createRequire } from "node:module";
|
|
7
8
|
import fs from "node:fs/promises";
|
|
@@ -471,7 +472,7 @@ async function getFrappeApps(root) {
|
|
|
471
472
|
const program = new Command();
|
|
472
473
|
program.name("mpo");
|
|
473
474
|
program.version(JSON.parse(fsSync.readFileSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json"), "utf8"))?.version);
|
|
474
|
-
program.command("init").option("--universal", "scaffold the universal template (web + iOS + Android; default)").option("--web", "scaffold the web-only template (Vite + React)").option("--app", "alias of --web (back-compat)").option("--frappe", "add the Frappe backend piece (env keys, provider, docker compose bench)").option("--keycloak", "add the Keycloak auth piece (env keys, provider, docker compose + realm)").option("--gnome", "add the GNOME desktop target piece (GTK4/GJS via react-gnome — native widgets, no webview)").option("--tauri", "add the desktop target piece (src-tauri
|
|
475
|
+
program.command("init").option("--universal", "scaffold the universal template (web + iOS + Android; default)").option("--web", "scaffold the web-only template (Vite + React)").option("--app", "alias of --web (back-compat)").option("--frappe", "add the Frappe backend piece (env keys, provider, docker compose bench)").option("--keycloak", "add the Keycloak auth piece (env keys, provider, docker compose + realm)").option("--gnome", "add the GNOME desktop target piece (GTK4/GJS via react-gnome — native widgets, no webview)").option("--tauri", "add the Tauri desktop target piece (src-tauri webview shell — Rust; combines with --gnome)").option("--vscode", "add the VS Code extension target piece").option("--webext", "add the browser extension target piece (MV3 popup + background)").option("--pieces-only", "scaffold ONLY the selected piece fragments onto an empty tree (no base template) — used by `mpo update` to rebuild adopted-project merge baselines").option("--skip-install", "skip pnpm install after scaffolding").option("--skip-git", "skip git init + scaffold commit").option("--mpo-version <range>", "semver range for @multiplatform.one/* (default: ^<cli version>)").option("-y, --yes", "non-interactive: accept defaults instead of prompting").option("--monorepo", "DEPRECATED: clone/modify the full multiplatform.one monorepo instead of scaffolding a consumer project").option("-c, --checkout <branch>", "branch, tag or commit to checkout (--monorepo only)", "main").option("-a, --apps <apps>", "comma-separated apps to include (--monorepo only)").option("-s, --services <services>", "comma-separated services to include (--monorepo only)").argument("[name]", "the name of the project").description("scaffold a new multiplatform project (universal web + native by default; --web for web-only; semver @multiplatform.one/* deps)").action(async (name, options) => {
|
|
475
476
|
if (options.monorepo) {
|
|
476
477
|
console.warn("⚠️ `mpo init --monorepo` clones the whole multiplatform.one monorepo (legacy). New projects should use the default consumer scaffold instead.");
|
|
477
478
|
if (await spawn("git", ["rev-parse", "--is-inside-work-tree"]).then(() => true, () => false)) throw new Error("mpo cannot be initialized inside a git repository");
|
|
@@ -485,26 +486,39 @@ program.command("init").option("--universal", "scaffold the universal template (
|
|
|
485
486
|
if (options.apps || options.services) throw new Error("--apps/--services select monorepo pieces and require --monorepo (legacy clone mode)");
|
|
486
487
|
const webOnly = Boolean(options.web || options.app);
|
|
487
488
|
if (webOnly && options.universal) throw new Error("Pass either --web/--app or --universal, not both");
|
|
489
|
+
if (options.piecesOnly && (webOnly || options.universal)) throw new Error("--pieces-only scaffolds no base template — drop --web/--universal");
|
|
488
490
|
const pieces = INIT_APP_PIECES.filter((piece) => Boolean(options[piece]));
|
|
489
491
|
await initApp(name, {
|
|
490
492
|
skipInstall: Boolean(options.skipInstall),
|
|
491
493
|
skipGit: Boolean(options.skipGit),
|
|
492
494
|
version: options.mpoVersion,
|
|
493
|
-
template: webOnly ? "app" : options.universal ? "universal" : void 0,
|
|
495
|
+
template: options.piecesOnly ? "none" : webOnly ? "app" : options.universal ? "universal" : void 0,
|
|
494
496
|
yes: Boolean(options.yes),
|
|
495
497
|
pieces: pieces.length ? pieces : void 0
|
|
496
498
|
});
|
|
497
499
|
});
|
|
498
|
-
program.command("
|
|
500
|
+
program.command("adopt").option("--frappe", "the project adopted the Frappe backend piece").option("--keycloak", "the project adopted the Keycloak auth piece").option("--gnome", "the project adopted the GNOME desktop target piece").option("--tauri", "the project adopted the Tauri desktop target piece").option("--vscode", "the project adopted the VS Code extension target piece").option("--webext", "the project adopted the browser extension target piece").option("--name <name>", "project name recorded in provenance (default: package.json name, scope stripped) — it renders piece paths (apps/<name>/…), so match your app directory").option("-y, --yes", "non-interactive: never prompt (pieces must come from flags)").description("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)").action(async (options) => {
|
|
501
|
+
const pieces = INIT_APP_PIECES.filter((piece) => Boolean(options[piece]));
|
|
502
|
+
await adoptApp({
|
|
503
|
+
pieces: pieces.length ? pieces : void 0,
|
|
504
|
+
name: options.name,
|
|
505
|
+
yes: Boolean(options.yes)
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
program.command("update").option("-c, --checkout <branch>", "branch, tag or commit to merge from upstream (monorepo forks)", "main").option("-r, --remote <url>", "upstream remote URL (monorepo forks)", "https://gitlab.com/bitspur/frappe/multiplatform.one.git").option("--skip-install", "skip pnpm install after the update").option("--mpo-version <range>", "semver range for @multiplatform.one/* (default: ^<cli version>)").option("--assume-version <cliVersion>", "bootstrap missing .mpo.json: assume the project was scaffolded by this exact CLI version, record provenance, then update").option("--template <template>", "template recorded by --assume-version (universal|app, default universal)").description("update a scaffolded or adopted project to the current template (three-way merge via .mpo.json provenance); monorepo forks fall back to the upstream merge flow").action(async (options) => {
|
|
499
509
|
if (await spawn("git", ["rev-parse", "--is-inside-work-tree"]).then(() => false, () => true)) throw new Error("mpo cannot be updated outside of a git repository");
|
|
500
|
-
if (
|
|
510
|
+
if (options.template && !options.assumeVersion) throw new Error("--template only applies to the --assume-version bootstrap");
|
|
511
|
+
if (options.template && options.template !== "universal" && options.template !== "app") throw new Error(`--template must be universal or app, got "${options.template}"`);
|
|
512
|
+
if (readProvenance(projectRoot) || options.assumeVersion) {
|
|
501
513
|
await updateApp({
|
|
502
514
|
skipInstall: options.skipInstall,
|
|
503
|
-
version: options.mpoVersion
|
|
515
|
+
version: options.mpoVersion,
|
|
516
|
+
assumeVersion: options.assumeVersion,
|
|
517
|
+
assumeTemplate: options.template
|
|
504
518
|
});
|
|
505
519
|
return;
|
|
506
520
|
}
|
|
507
|
-
if (!await fs.stat(path.resolve(projectRoot, "features/package.json")).then((stat) => stat.isFile(), () => false)) throw new Error(".mpo.json not found — this project predates scaffold provenance
|
|
521
|
+
if (!await fs.stat(path.resolve(projectRoot, "features/package.json")).then((stat) => stat.isFile(), () => false)) throw new Error(".mpo.json not found — this project predates scaffold provenance.\n - scaffolded by an old CLI (pre-6.3)? bootstrap it:\n mpo update --assume-version <cliVersion> [--template universal|app]\n - never scaffolded (hand-adopted pieces)? bring it under management:\n mpo adopt --<piece> [...]");
|
|
508
522
|
if (await spawn("git", [
|
|
509
523
|
"diff",
|
|
510
524
|
"--cached",
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { INIT_APP_PIECES, PROVENANCE_FILE, availablePieces, cliVersion, normalizePieces, packageJsonName, pieceDescription, validateName, writeProvenance } from "./initApp.mjs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import spawn from "nano-spawn";
|
|
5
|
+
import inquirer from "inquirer";
|
|
6
|
+
|
|
7
|
+
//#region src/commands/adoptApp.ts
|
|
8
|
+
/**
|
|
9
|
+
* `mpo adopt` — bring an EXISTING, never-scaffolded project under `mpo
|
|
10
|
+
* update` management. Projects like this hand-copied one or more delivery
|
|
11
|
+
* targets (webext, gnome, …) from the mpo templates and consume
|
|
12
|
+
* @multiplatform.one/* from npm, but have no .mpo.json provenance, so
|
|
13
|
+
* `mpo update` refuses to run.
|
|
14
|
+
*
|
|
15
|
+
* Adoption writes provenance ONLY (template "none" + the chosen pieces +
|
|
16
|
+
* the current CLI version) and seeds .updateignore — it never rewrites a
|
|
17
|
+
* project file. The FIRST `mpo update` afterwards reconciles the piece
|
|
18
|
+
* files against the piece templates from an empty merge base, so real
|
|
19
|
+
* diffs (and conflicts) against hand-copied files are expected there;
|
|
20
|
+
* every later update is an ordinary three-way merge.
|
|
21
|
+
*/
|
|
22
|
+
const UPDATEIGNORE_FILE = ".updateignore";
|
|
23
|
+
const UPDATEIGNORE_SEED = `# .updateignore — \`mpo update\` never rewrites paths listed here.
|
|
24
|
+
# One git pathspec per line; matching paths stay pinned to YOUR version
|
|
25
|
+
# during template updates. Lines starting with "#" are comments.
|
|
26
|
+
`;
|
|
27
|
+
async function git(projectDir, args) {
|
|
28
|
+
return spawn("git", args, { cwd: projectDir });
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The multiplatform.one monorepo (and its forks) is the home of the
|
|
32
|
+
* templates themselves — adopting it into consumer-project update
|
|
33
|
+
* management would be circular. It is identified by the CLI package
|
|
34
|
+
* living at public/cli.
|
|
35
|
+
*/
|
|
36
|
+
function isMpoMonorepo(projectDir) {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(readFileSync(join(projectDir, "public/cli/package.json"), "utf-8")).name === "@multiplatform.one/cli";
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function resolveAdoptPieces(options) {
|
|
44
|
+
if (options.pieces?.length) return normalizePieces(options.pieces, "none");
|
|
45
|
+
if (!options.yes && process.stdout.isTTY) return normalizePieces((await inquirer.prompt([{
|
|
46
|
+
message: "Which pieces did this project adopt? (space to toggle, enter to confirm)",
|
|
47
|
+
name: "pieces",
|
|
48
|
+
type: "checkbox",
|
|
49
|
+
choices: availablePieces("none").map((piece) => ({
|
|
50
|
+
name: `${piece} — ${pieceDescription(piece)}`,
|
|
51
|
+
value: piece,
|
|
52
|
+
checked: false
|
|
53
|
+
})),
|
|
54
|
+
validate: (selection) => selection.length > 0 || "Select at least one piece — adopting nothing is meaningless"
|
|
55
|
+
}])).pieces, "none");
|
|
56
|
+
throw new Error(`mpo adopt needs at least one piece (${INIT_APP_PIECES.map((piece) => `--${piece}`).join(", ")}) — adopting nothing is meaningless`);
|
|
57
|
+
}
|
|
58
|
+
async function adoptApp(options = {}) {
|
|
59
|
+
const projectDir = await spawn("git", ["rev-parse", "--show-toplevel"], { cwd: process.cwd() }).then((result) => result.stdout.trim(), () => {
|
|
60
|
+
throw new Error("mpo adopt must run inside a git repository");
|
|
61
|
+
});
|
|
62
|
+
if (isMpoMonorepo(projectDir)) throw new Error("mpo adopt cannot run inside the multiplatform.one monorepo (or a fork of it) — it is for consumer projects that use @multiplatform.one/* from npm");
|
|
63
|
+
if (existsSync(join(projectDir, ".mpo.json"))) throw new Error(`${PROVENANCE_FILE} already exists — this project already has scaffold provenance. Run \`mpo update\` directly.`);
|
|
64
|
+
await git(projectDir, ["rev-parse", "HEAD"]).catch(() => {
|
|
65
|
+
throw new Error("mpo adopt requires at least one commit — commit your project first");
|
|
66
|
+
});
|
|
67
|
+
await git(projectDir, ["diff", "--quiet"]).catch(() => {
|
|
68
|
+
throw new Error("mpo adopt requires a clean working tree (unstaged changes present)");
|
|
69
|
+
});
|
|
70
|
+
await git(projectDir, [
|
|
71
|
+
"diff",
|
|
72
|
+
"--cached",
|
|
73
|
+
"--quiet"
|
|
74
|
+
]).catch(() => {
|
|
75
|
+
throw new Error("mpo adopt requires a clean working tree (staged changes present)");
|
|
76
|
+
});
|
|
77
|
+
const rawName = options.name ?? packageJsonName(projectDir);
|
|
78
|
+
if (!rawName) throw new Error("Could not derive a project name from package.json — pass one with --name <name>");
|
|
79
|
+
let name;
|
|
80
|
+
try {
|
|
81
|
+
name = validateName(rawName);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
throw new Error(`${err.message} The name renders piece file paths (apps/<name>/…), so pick the one matching your app directory: --name <name>.`);
|
|
84
|
+
}
|
|
85
|
+
const pieces = await resolveAdoptPieces(options);
|
|
86
|
+
const currentVersion = cliVersion() ?? "0.0.0";
|
|
87
|
+
writeProvenance(projectDir, {
|
|
88
|
+
template: "none",
|
|
89
|
+
cliVersion: currentVersion,
|
|
90
|
+
name,
|
|
91
|
+
mpoVersion: `^${currentVersion}`,
|
|
92
|
+
pieces,
|
|
93
|
+
reconciled: false
|
|
94
|
+
});
|
|
95
|
+
const files = [PROVENANCE_FILE];
|
|
96
|
+
if (!existsSync(join(projectDir, UPDATEIGNORE_FILE))) {
|
|
97
|
+
writeFileSync(join(projectDir, UPDATEIGNORE_FILE), UPDATEIGNORE_SEED);
|
|
98
|
+
files.push(UPDATEIGNORE_FILE);
|
|
99
|
+
}
|
|
100
|
+
await git(projectDir, [
|
|
101
|
+
"add",
|
|
102
|
+
"--",
|
|
103
|
+
...files
|
|
104
|
+
]);
|
|
105
|
+
const committed = await git(projectDir, [
|
|
106
|
+
"commit",
|
|
107
|
+
"--quiet",
|
|
108
|
+
"-m",
|
|
109
|
+
"chore: adopt multiplatform.one tooling (mpo adopt)"
|
|
110
|
+
]).then(() => true, () => {
|
|
111
|
+
console.warn("⚠️ git commit failed (missing git identity?) — provenance left staged.");
|
|
112
|
+
return false;
|
|
113
|
+
});
|
|
114
|
+
console.log(`\n✅ adopted ${name} (pieces: ${pieces.join(", ")}, cli ${currentVersion})`);
|
|
115
|
+
console.log(` ${files.join(" + ")} ${committed ? "committed" : "written"} — NO project files`);
|
|
116
|
+
console.log(" were changed by adoption itself.\n");
|
|
117
|
+
console.log("Next: run `mpo update` to reconcile the piece files.");
|
|
118
|
+
console.log(" THE FIRST UPDATE SURFACES REAL DIFFS: your hand-copied piece files");
|
|
119
|
+
console.log(" are merged against the piece templates from scratch, so conflicts");
|
|
120
|
+
console.log(" there are expected — resolve the markers and commit. Later updates");
|
|
121
|
+
console.log(" are ordinary three-way merges, and re-running `mpo update` right");
|
|
122
|
+
console.log(" after a successful update makes zero changes.");
|
|
123
|
+
console.log(`Pin anything the updater must never touch in ${UPDATEIGNORE_FILE}.`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
//#endregion
|
|
127
|
+
export { adoptApp };
|
package/lib/commands/init.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { execSync } from "node:child_process";
|
|
4
3
|
import inquirer from "inquirer";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
5
|
|
|
6
6
|
//#region src/commands/init.ts
|
|
7
7
|
const repoUrl = "https://gitlab.com/bitspur/frappe/multiplatform.one.git";
|
package/lib/commands/initApp.mjs
CHANGED
|
@@ -23,7 +23,7 @@ const PIECE_DESCRIPTIONS = {
|
|
|
23
23
|
frappe: "Frappe backend wiring (env keys, provider, docker compose bench)",
|
|
24
24
|
gnome: "GNOME desktop target (GTK4/GJS via react-gnome — native widgets, no webview)",
|
|
25
25
|
keycloak: "Keycloak auth wiring (env keys, provider, docker compose + realm)",
|
|
26
|
-
tauri: "
|
|
26
|
+
tauri: "Tauri desktop target (src-tauri skeleton — webview shell, Rust toolchain)",
|
|
27
27
|
vscode: "VS Code extension target (extension host + Tamagui webview)",
|
|
28
28
|
webext: "Browser extension target (MV3 popup + background)"
|
|
29
29
|
};
|
|
@@ -36,6 +36,15 @@ function validateName(name) {
|
|
|
36
36
|
if (!/^[a-z][a-z0-9-]*$/.test(trimmed)) throw new Error(`Invalid project name "${trimmed}". Use lowercase letters, numbers, and hyphens (e.g. my-app).`);
|
|
37
37
|
return trimmed;
|
|
38
38
|
}
|
|
39
|
+
/** Root package.json name with any @scope/ prefix stripped (adopt +
|
|
40
|
+
* update-bootstrap provenance name derivation). */
|
|
41
|
+
function packageJsonName(projectDir) {
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(readFileSync(join(projectDir, "package.json"), "utf-8")).name?.replace(/^@[^/]+\//u, "");
|
|
44
|
+
} catch {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
39
48
|
function resolveMpoVersion(explicit) {
|
|
40
49
|
if (explicit) return explicit.startsWith("^") || explicit.startsWith("~") || explicit === "*" ? explicit : `^${explicit}`;
|
|
41
50
|
const version = cliVersion();
|
|
@@ -67,7 +76,8 @@ function readProvenance(projectDir) {
|
|
|
67
76
|
cliVersion: raw.cliVersion,
|
|
68
77
|
name: raw.name,
|
|
69
78
|
mpoVersion: raw.mpoVersion ?? `^${raw.cliVersion}`,
|
|
70
|
-
pieces: Array.isArray(raw.pieces) ? [...raw.pieces].sort() : []
|
|
79
|
+
pieces: Array.isArray(raw.pieces) ? [...raw.pieces].sort() : [],
|
|
80
|
+
...raw.reconciled === false ? { reconciled: false } : {}
|
|
71
81
|
};
|
|
72
82
|
} catch {
|
|
73
83
|
return;
|
|
@@ -82,10 +92,13 @@ function piecesRoot(piece) {
|
|
|
82
92
|
/**
|
|
83
93
|
* Fragment source dirs for a piece + template: shared/ applies to every
|
|
84
94
|
* template, <template>/ only to that one. A piece supports a template when
|
|
85
|
-
* at least one of the two exists.
|
|
95
|
+
* at least one of the two exists. Adopted projects (template "none") use the
|
|
96
|
+
* universal fragment set — pieces ship universal-layout fragments
|
|
97
|
+
* (apps/<name>/…), which is the layout adopted repos follow.
|
|
86
98
|
*/
|
|
87
99
|
function pieceFragmentDirs(piece, template) {
|
|
88
|
-
|
|
100
|
+
const effective = template === "none" ? "universal" : template;
|
|
101
|
+
return [join(piecesRoot(piece), "shared"), join(piecesRoot(piece), effective)].filter((dir) => existsSync(dir));
|
|
89
102
|
}
|
|
90
103
|
/**
|
|
91
104
|
* npm always strips .gitignore / .npmrc (and newer npms .env*) from published
|
|
@@ -246,6 +259,10 @@ function normalizePieces(pieces, template) {
|
|
|
246
259
|
function availablePieces(template) {
|
|
247
260
|
return INIT_APP_PIECES.filter((piece) => pieceFragmentDirs(piece, template).length > 0);
|
|
248
261
|
}
|
|
262
|
+
/** Human-readable piece summaries (adopt + init prompts). */
|
|
263
|
+
function pieceDescription(piece) {
|
|
264
|
+
return PIECE_DESCRIPTIONS[piece];
|
|
265
|
+
}
|
|
249
266
|
async function resolvePieces(options, template) {
|
|
250
267
|
if (options.pieces) return normalizePieces(options.pieces, template);
|
|
251
268
|
if (options.yes || !process.stdout.isTTY) return [];
|
|
@@ -315,6 +332,7 @@ async function initApp(nameArg, options = {}) {
|
|
|
315
332
|
const name = validateName(projectName);
|
|
316
333
|
const template = await resolveTemplate(options);
|
|
317
334
|
const pieces = await resolvePieces(options, template);
|
|
335
|
+
if (template === "none" && pieces.length === 0) throw new Error(`A pieces-only scaffold needs at least one piece (${INIT_APP_PIECES.map((piece) => `--${piece}`).join(", ")})`);
|
|
318
336
|
const targetDir = resolve(name);
|
|
319
337
|
if (existsSync(targetDir)) {
|
|
320
338
|
if (readdirSync(targetDir).length > 0) throw new Error(`Directory "${targetDir}" already exists and is not empty`);
|
|
@@ -326,19 +344,25 @@ async function initApp(nameArg, options = {}) {
|
|
|
326
344
|
MPO_VERSION: resolveMpoVersion(options.version),
|
|
327
345
|
YEAR: String((/* @__PURE__ */ new Date()).getFullYear())
|
|
328
346
|
};
|
|
329
|
-
console.log(`\nScaffolding ${template === "universal" ? "universal (web + iOS + Android)" : "web-only"} project at ${targetDir}`);
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
347
|
+
console.log(`\nScaffolding ${template === "universal" ? "universal (web + iOS + Android)" : template === "app" ? "web-only" : "pieces-only (adopted-project baseline)"} project at ${targetDir}`);
|
|
348
|
+
if (template !== "none") {
|
|
349
|
+
console.log(` apps/${name}`);
|
|
350
|
+
console.log(` features/${name}`);
|
|
351
|
+
console.log(` packages/config`);
|
|
352
|
+
}
|
|
333
353
|
if (template === "universal") {
|
|
334
354
|
console.log(` packages/i18n`);
|
|
335
355
|
console.log(` packages/themes`);
|
|
336
356
|
}
|
|
337
357
|
if (pieces.length) console.log(` pieces: ${pieces.join(", ")}`);
|
|
338
358
|
console.log(` @multiplatform.one/* → ${vars.MPO_VERSION}\n`);
|
|
339
|
-
|
|
359
|
+
if (template === "none") mkdirSync(targetDir, { recursive: true });
|
|
360
|
+
else writeTree(templatesRoot(template), targetDir, vars);
|
|
340
361
|
for (const piece of pieces) for (const fragmentDir of pieceFragmentDirs(piece, template)) overlayTree(fragmentDir, targetDir, vars);
|
|
341
|
-
if (pieces.includes("frappe") || pieces.includes("keycloak"))
|
|
362
|
+
if (pieces.includes("frappe") || pieces.includes("keycloak")) {
|
|
363
|
+
mkdirSync(join(targetDir, "apps", name, "src"), { recursive: true });
|
|
364
|
+
writeFileSync(join(targetDir, "apps", name, "src/app.ts"), composeAppTs(pieces));
|
|
365
|
+
}
|
|
342
366
|
writeProvenance(targetDir, {
|
|
343
367
|
template,
|
|
344
368
|
cliVersion: cliVersion() ?? "0.0.0",
|
|
@@ -349,7 +373,7 @@ async function initApp(nameArg, options = {}) {
|
|
|
349
373
|
let scaffoldCommitted = false;
|
|
350
374
|
if (!options.skipGit) {
|
|
351
375
|
if (!await spawn("git", ["rev-parse", "--is-inside-work-tree"], { cwd: targetDir }).then(() => true, () => false)) {
|
|
352
|
-
const label = template === "universal" ? "universal" : "web-only";
|
|
376
|
+
const label = template === "universal" ? "universal" : template === "app" ? "web-only" : "pieces-only";
|
|
353
377
|
await spawn("git", ["init", "--quiet"], { cwd: targetDir });
|
|
354
378
|
await spawn("git", ["add", "-A"], { cwd: targetDir });
|
|
355
379
|
scaffoldCommitted = await spawn("git", [
|
|
@@ -363,7 +387,7 @@ async function initApp(nameArg, options = {}) {
|
|
|
363
387
|
});
|
|
364
388
|
}
|
|
365
389
|
}
|
|
366
|
-
if (!options.skipInstall) {
|
|
390
|
+
if (!options.skipInstall && template !== "none") {
|
|
367
391
|
console.log("Installing dependencies...");
|
|
368
392
|
await spawn("pnpm", ["install"], {
|
|
369
393
|
cwd: targetDir,
|
|
@@ -379,8 +403,12 @@ async function initApp(nameArg, options = {}) {
|
|
|
379
403
|
"--no-edit",
|
|
380
404
|
"--quiet"
|
|
381
405
|
], { cwd: targetDir })).catch(() => {});
|
|
382
|
-
} else console.log("Skipped pnpm install (--skip-install).");
|
|
406
|
+
} else if (options.skipInstall) console.log("Skipped pnpm install (--skip-install).");
|
|
383
407
|
console.log(`\n✅ Project created at ${targetDir}`);
|
|
408
|
+
if (template === "none") {
|
|
409
|
+
console.log("\nPieces-only tree (adopted-project baseline) — not a runnable project.");
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
384
412
|
console.log("\nNext steps:");
|
|
385
413
|
console.log(` cd ${name}`);
|
|
386
414
|
if (options.skipInstall) console.log(" pnpm install");
|
|
@@ -392,7 +420,7 @@ async function initApp(nameArg, options = {}) {
|
|
|
392
420
|
if (pieces.includes("frappe")) console.log(" docker compose -f docker/compose.frappe.yaml up -d # dev bench (see README)");
|
|
393
421
|
if (pieces.includes("keycloak")) console.log(" docker compose -f docker/compose.keycloak.yaml up -d # dev keycloak");
|
|
394
422
|
if (pieces.includes("gnome")) console.log(" pnpm gnome # GTK4 desktop app (requires gjs + gtk4 — see README)");
|
|
395
|
-
if (pieces.includes("tauri")) console.log(" pnpm tauri:dev # desktop app (requires the Rust toolchain)");
|
|
423
|
+
if (pieces.includes("tauri")) console.log(" pnpm tauri:dev # Tauri desktop app (requires the Rust toolchain)");
|
|
396
424
|
if (pieces.includes("vscode")) console.log(" pnpm build:vscode # stage the VS Code extension into apps dist-vscode/");
|
|
397
425
|
if (pieces.includes("webext")) console.log(" pnpm build:webext # stage the browser extension into apps dist-webext/");
|
|
398
426
|
console.log("\nThis project consumes published @multiplatform.one/* packages.");
|
|
@@ -400,4 +428,4 @@ async function initApp(nameArg, options = {}) {
|
|
|
400
428
|
}
|
|
401
429
|
|
|
402
430
|
//#endregion
|
|
403
|
-
export { INIT_APP_PIECES, PROVENANCE_FILE, availablePieces, cliVersion, composeAppTs, initApp, mergeJsonValues, normalizePieces, readProvenance, writeProvenance };
|
|
431
|
+
export { INIT_APP_PIECES, PROVENANCE_FILE, availablePieces, cliVersion, composeAppTs, initApp, mergeJsonValues, normalizePieces, packageJsonName, pieceDescription, readProvenance, validateName, writeProvenance };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { generateVscodeConfig } from "../generateVscode.mjs";
|
|
2
|
-
import { cliVersion, initApp, readProvenance, writeProvenance } from "./initApp.mjs";
|
|
2
|
+
import { PROVENANCE_FILE, cliVersion, initApp, packageJsonName, readProvenance, validateName, writeProvenance } from "./initApp.mjs";
|
|
3
3
|
import { basename, join } from "node:path";
|
|
4
4
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import spawn from "nano-spawn";
|
|
@@ -19,8 +19,9 @@ import { tmpdir } from "node:os";
|
|
|
19
19
|
*
|
|
20
20
|
* `git merge-tree --write-tree --merge-base=<base>` merges template
|
|
21
21
|
* evolution with your customizations; conflicts land in the worktree with
|
|
22
|
-
* normal conflict markers. An `.updateignore` file (one
|
|
23
|
-
*
|
|
22
|
+
* normal conflict markers. An `.updateignore` file (one exact file or
|
|
23
|
+
* directory prefix per line — not globs; `dir/**` is normalized, other
|
|
24
|
+
* glob shapes are refused) pins matching paths to your HEAD version.
|
|
24
25
|
*/
|
|
25
26
|
async function git(projectDir, args, env) {
|
|
26
27
|
return spawn("git", args, {
|
|
@@ -76,14 +77,15 @@ async function scaffoldBaseline(version, name, template, mpoVersion, pieces) {
|
|
|
76
77
|
force: true
|
|
77
78
|
});
|
|
78
79
|
const current = cliVersion();
|
|
79
|
-
const templateFlag = template === "app" ? ["--web"] : ["--universal"];
|
|
80
|
+
const templateFlag = template === "none" ? ["--pieces-only"] : template === "app" ? ["--web"] : ["--universal"];
|
|
80
81
|
const [major = 0, minor = 0] = version.split(".").map(Number);
|
|
81
|
-
const scaffoldCapable = major > 6 || major === 6 && minor >= 3;
|
|
82
|
+
const scaffoldCapable = template === "none" ? major > 6 || major === 6 && minor >= 6 : major > 6 || major === 6 && minor >= 3;
|
|
82
83
|
const piecesCapable = major > 6 || major === 6 && minor >= 5;
|
|
83
84
|
const pieceFlags = piecesCapable ? pieces.map((piece) => `--${piece}`) : [];
|
|
84
85
|
if (pieces.length && scaffoldCapable && !piecesCapable && version !== current) console.warn(`⚠️ @multiplatform.one/cli@${version} predates composable pieces (6.5.0); the merge base is scaffolded without them.`);
|
|
85
|
-
if (!scaffoldCapable)
|
|
86
|
-
|
|
86
|
+
if (!scaffoldCapable) {
|
|
87
|
+
if (version !== current) console.warn(template === "none" ? `⚠️ @multiplatform.one/cli@${version} predates pieces-only baselines (6.6.0); using the current CLI's piece templates as the merge base (the merge degrades to two-way: template changes since that version won't land).` : `⚠️ @multiplatform.one/cli@${version} predates the consumer scaffolder (6.3.0); using the current CLI's template as the merge base.`);
|
|
88
|
+
} else if (version !== current) {
|
|
87
89
|
for (let attempt = 1; attempt <= 2; attempt++) try {
|
|
88
90
|
await spawn("pnpm", [
|
|
89
91
|
"--package",
|
|
@@ -103,7 +105,7 @@ async function scaffoldBaseline(version, name, template, mpoVersion, pieces) {
|
|
|
103
105
|
stdio: "inherit"
|
|
104
106
|
});
|
|
105
107
|
const dir = join(parent, name);
|
|
106
|
-
if (existsSync(join(dir, "package.json"))) return {
|
|
108
|
+
if (existsSync(join(dir, template === "none" ? ".mpo.json" : "package.json"))) return {
|
|
107
109
|
dir,
|
|
108
110
|
cleanup
|
|
109
111
|
};
|
|
@@ -319,6 +321,42 @@ async function autoResolveDependencyConflicts(projectDir, mergedTree, conflicts,
|
|
|
319
321
|
});
|
|
320
322
|
}
|
|
321
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* Pre-provenance bootstrap (`mpo update --assume-version <cliVersion>`):
|
|
326
|
+
* projects scaffolded by CLIs older than the provenance file (≤6.3.0) have
|
|
327
|
+
* no .mpo.json. The user asserts which CLI version (and template) produced
|
|
328
|
+
* the project; provenance is written and committed, then the normal update
|
|
329
|
+
* proceeds against that baseline.
|
|
330
|
+
*/
|
|
331
|
+
async function bootstrapProvenance(projectDir, options) {
|
|
332
|
+
const version = options.assumeVersion;
|
|
333
|
+
if (!/^\d+\.\d+\.\d+(?:-[\w.]+)?$/u.test(version)) throw new Error(`--assume-version must be an exact CLI version (e.g. 6.3.0), got "${version}"`);
|
|
334
|
+
const rawName = packageJsonName(projectDir);
|
|
335
|
+
if (!rawName) throw new Error(`Could not derive the project name from package.json — create ${PROVENANCE_FILE} by hand with { template, cliVersion, name } instead`);
|
|
336
|
+
const provenance = {
|
|
337
|
+
template: options.assumeTemplate ?? "universal",
|
|
338
|
+
cliVersion: version,
|
|
339
|
+
name: validateName(rawName),
|
|
340
|
+
mpoVersion: `^${version}`,
|
|
341
|
+
pieces: []
|
|
342
|
+
};
|
|
343
|
+
console.log(`\nBootstrapping provenance: assuming this project was scaffolded by cli@${version} (${provenance.template} template).`);
|
|
344
|
+
writeProvenance(projectDir, provenance);
|
|
345
|
+
await git(projectDir, [
|
|
346
|
+
"add",
|
|
347
|
+
"--",
|
|
348
|
+
PROVENANCE_FILE
|
|
349
|
+
]);
|
|
350
|
+
await git(projectDir, [
|
|
351
|
+
"commit",
|
|
352
|
+
"--quiet",
|
|
353
|
+
"-m",
|
|
354
|
+
`chore: record assumed mpo scaffold provenance (cli@${version})`
|
|
355
|
+
]).catch(() => {
|
|
356
|
+
console.warn("⚠️ git commit failed (missing git identity?) — provenance left staged.");
|
|
357
|
+
});
|
|
358
|
+
return provenance;
|
|
359
|
+
}
|
|
322
360
|
async function updateApp(options = {}) {
|
|
323
361
|
const projectDir = await gitOut(process.cwd(), ["rev-parse", "--show-toplevel"]).catch(() => {
|
|
324
362
|
throw new Error("mpo update must run inside a git repository");
|
|
@@ -333,16 +371,44 @@ async function updateApp(options = {}) {
|
|
|
333
371
|
]).catch(() => {
|
|
334
372
|
throw new Error("mpo update requires a clean working tree (staged changes present)");
|
|
335
373
|
});
|
|
336
|
-
|
|
337
|
-
if (
|
|
374
|
+
let provenance = readProvenance(projectDir);
|
|
375
|
+
if (options.assumeVersion) {
|
|
376
|
+
if (provenance) throw new Error(`--assume-version is for projects without ${PROVENANCE_FILE} — this project already has provenance (cli@${provenance.cliVersion})`);
|
|
377
|
+
provenance = await bootstrapProvenance(projectDir, options);
|
|
378
|
+
}
|
|
379
|
+
if (!provenance) throw new Error(`${PROVENANCE_FILE} not found — this project predates scaffold provenance.\n - scaffolded by an old CLI (pre-6.3)? bootstrap it:
|
|
380
|
+
mpo update --assume-version <cliVersion> [--template universal|app]
|
|
381
|
+
- never scaffolded (hand-adopted pieces)? bring it under management:
|
|
382
|
+
mpo adopt --<piece> [...]`);
|
|
338
383
|
const currentVersion = cliVersion() ?? "0.0.0";
|
|
339
384
|
const targetMpoRange = options.version ? options.version.startsWith("^") || options.version.startsWith("~") ? options.version : `^${options.version}` : `^${currentVersion}`;
|
|
385
|
+
const adopted = provenance.template === "none";
|
|
386
|
+
const firstReconcile = adopted && provenance.reconciled === false;
|
|
340
387
|
console.log(`\nmpo update: ${provenance.cliVersion} → ${currentVersion}`);
|
|
341
|
-
console.log(` template: ${provenance.template}`);
|
|
388
|
+
console.log(` template: ${provenance.template}${adopted ? " (adopted)" : ""}`);
|
|
342
389
|
if (provenance.pieces.length) console.log(` pieces: ${provenance.pieces.join(", ")}`);
|
|
343
390
|
console.log(` project: ${provenance.name}\n`);
|
|
344
|
-
|
|
345
|
-
|
|
391
|
+
if (firstReconcile) {
|
|
392
|
+
console.log("⚠️ FIRST UPDATE AFTER ADOPTION: piece files are reconciled against the");
|
|
393
|
+
console.log(" piece templates from an empty merge base. Diffs and conflicts on");
|
|
394
|
+
console.log(" hand-copied piece files are EXPECTED — resolve the markers and");
|
|
395
|
+
console.log(" commit. Later updates merge three-way and stay quiet.\n");
|
|
396
|
+
}
|
|
397
|
+
let base;
|
|
398
|
+
if (firstReconcile) {
|
|
399
|
+
console.log("Using an empty merge base (first update after adoption)...");
|
|
400
|
+
const dir = mkdtempSync(join(tmpdir(), "mpo-update-"));
|
|
401
|
+
base = {
|
|
402
|
+
dir,
|
|
403
|
+
cleanup: () => rmSync(dir, {
|
|
404
|
+
recursive: true,
|
|
405
|
+
force: true
|
|
406
|
+
})
|
|
407
|
+
};
|
|
408
|
+
} else {
|
|
409
|
+
console.log(`Scaffolding merge base (cli@${provenance.cliVersion})...`);
|
|
410
|
+
base = await scaffoldBaseline(provenance.cliVersion, provenance.name, provenance.template, provenance.mpoVersion, provenance.pieces);
|
|
411
|
+
}
|
|
346
412
|
console.log(`Scaffolding update target (cli@${currentVersion})...`);
|
|
347
413
|
const next = await scaffoldBaseline(currentVersion, provenance.name, provenance.template, targetMpoRange, provenance.pieces);
|
|
348
414
|
rmSync(join(base.dir, ".mpo.json"), { force: true });
|
|
@@ -380,7 +446,10 @@ async function updateApp(options = {}) {
|
|
|
380
446
|
const updateignore = join(projectDir, ".updateignore");
|
|
381
447
|
if (existsSync(updateignore)) for (const line of readFileSync(updateignore, "utf-8").split("\n")) {
|
|
382
448
|
const pattern = line.trim();
|
|
383
|
-
if (pattern
|
|
449
|
+
if (!pattern || pattern.startsWith("#")) continue;
|
|
450
|
+
const normalized = pattern.replace(/\/\*{1,2}$/, "");
|
|
451
|
+
if (/[*?[\]]/.test(normalized)) throw new Error(`.updateignore: "${pattern}" looks like a glob, but entries are exact files or directory prefixes (a glob here would silently pin nothing). List the file or directory instead.`);
|
|
452
|
+
pins.push(normalized.replace(/\/+$/, ""));
|
|
384
453
|
}
|
|
385
454
|
mergedTree = await pinPathsToHead(projectDir, mergedTree, head, pins);
|
|
386
455
|
conflicts = conflicts.filter((file) => !pins.some((pin) => file === pin || file.startsWith(`${pin}/`)));
|
|
@@ -390,7 +459,7 @@ async function updateApp(options = {}) {
|
|
|
390
459
|
for (const file of autoResolved.resolved) console.log(` auto-resolved: ${file} (dependency ranges)`);
|
|
391
460
|
const headTree = await gitOut(projectDir, ["rev-parse", `${head}^{tree}`]);
|
|
392
461
|
if (!conflicts.length && mergedTree === headTree) {
|
|
393
|
-
if (provenance.cliVersion !== finalProvenance.cliVersion || provenance.mpoVersion !== finalProvenance.mpoVersion) {
|
|
462
|
+
if (provenance.cliVersion !== finalProvenance.cliVersion || provenance.mpoVersion !== finalProvenance.mpoVersion || provenance.reconciled === false) {
|
|
394
463
|
writeProvenance(projectDir, finalProvenance);
|
|
395
464
|
await git(projectDir, [
|
|
396
465
|
"add",
|