@multiplatform.one/cli 6.3.0 → 6.4.1
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/lib/bin/multiplatformOne.mjs +19 -4
- package/lib/commands/initApp.mjs +62 -5
- package/lib/commands/updateApp.mjs +350 -0
- package/package.json +2 -2
- package/src/bin/multiplatformOne.ts +49 -6
- package/src/commands/initApp.ts +89 -3
- package/src/commands/updateApp.ts +401 -0
- package/templates/app/gitignore +1 -0
- package/templates/universal/apps/__NAME__/vite.config.ts +2 -48
- package/templates/universal/gitignore +2 -0
- package/types/bin/multiplatformOne.d.ts.map +1 -1
- package/types/commands/initApp.d.ts +18 -0
- package/types/commands/initApp.d.ts.map +1 -1
- package/types/commands/updateApp.d.ts +24 -0
- package/types/commands/updateApp.d.ts.map +1 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { generateVscodeConfig } from "../generateVscode.mjs";
|
|
2
2
|
import { discoverE2EApp, runE2ESession } from "../commands/e2e.mjs";
|
|
3
3
|
import { init, runModifyStep } from "../commands/init.mjs";
|
|
4
|
-
import { initApp } from "../commands/initApp.mjs";
|
|
4
|
+
import { initApp, readProvenance } from "../commands/initApp.mjs";
|
|
5
|
+
import { updateApp } from "../commands/updateApp.mjs";
|
|
5
6
|
import { createRequire } from "node:module";
|
|
6
7
|
import fs from "node:fs/promises";
|
|
7
8
|
import path from "node:path";
|
|
@@ -470,7 +471,7 @@ async function getFrappeApps(root) {
|
|
|
470
471
|
const program = new Command();
|
|
471
472
|
program.name("mpo");
|
|
472
473
|
program.version(JSON.parse(fsSync.readFileSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json"), "utf8"))?.version);
|
|
473
|
-
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("--skip-install", "skip pnpm install after scaffolding").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) => {
|
|
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("--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) => {
|
|
474
475
|
if (options.monorepo) {
|
|
475
476
|
console.warn("⚠️ `mpo init --monorepo` clones the whole multiplatform.one monorepo (legacy). New projects should use the default consumer scaffold instead.");
|
|
476
477
|
if (await spawn("git", ["rev-parse", "--is-inside-work-tree"]).then(() => true, () => false)) throw new Error("mpo cannot be initialized inside a git repository");
|
|
@@ -486,13 +487,22 @@ program.command("init").option("--universal", "scaffold the universal template (
|
|
|
486
487
|
if (webOnly && options.universal) throw new Error("Pass either --web/--app or --universal, not both");
|
|
487
488
|
await initApp(name, {
|
|
488
489
|
skipInstall: Boolean(options.skipInstall),
|
|
490
|
+
skipGit: Boolean(options.skipGit),
|
|
489
491
|
version: options.mpoVersion,
|
|
490
492
|
template: webOnly ? "app" : options.universal ? "universal" : void 0,
|
|
491
493
|
yes: Boolean(options.yes)
|
|
492
494
|
});
|
|
493
495
|
});
|
|
494
|
-
program.command("update").option("-c, --checkout <branch>", "branch, tag or commit to merge from upstream", "main").option("-r, --remote <url>", "upstream remote URL", "https://gitlab.com/bitspur/multiplatform.one/multiplatform.one.git").description("
|
|
496
|
+
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/multiplatform.one/multiplatform.one.git").option("--skip-install", "skip pnpm install after the update").option("--mpo-version <range>", "semver range for @multiplatform.one/* (default: ^<cli version>)").description("update a scaffolded project to the current template (three-way merge via .mpo.json provenance); monorepo forks fall back to the upstream merge flow").action(async (options) => {
|
|
495
497
|
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");
|
|
498
|
+
if (readProvenance(projectRoot)) {
|
|
499
|
+
await updateApp({
|
|
500
|
+
skipInstall: options.skipInstall,
|
|
501
|
+
version: options.mpoVersion
|
|
502
|
+
});
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
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. Create it with { template, cliVersion, name } matching how the project was generated (cliVersion = the @multiplatform.one/cli that scaffolded it), then re-run `mpo update`.");
|
|
496
506
|
if (await spawn("git", [
|
|
497
507
|
"diff",
|
|
498
508
|
"--cached",
|
|
@@ -812,7 +822,12 @@ program.command("generate").description("run generate command").action(async ()
|
|
|
812
822
|
cwd: projectRoot
|
|
813
823
|
});
|
|
814
824
|
});
|
|
815
|
-
program.
|
|
825
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
826
|
+
const error = err;
|
|
827
|
+
if (process.env.MPO_DEBUG) console.error(error);
|
|
828
|
+
else console.error(`✖ ${error.message ?? String(err)}`);
|
|
829
|
+
process.exit(1);
|
|
830
|
+
});
|
|
816
831
|
async function waitWithSpinner(services, options = {}) {
|
|
817
832
|
const { interval = 1e3, timeout = 6e5 } = options;
|
|
818
833
|
const waitFunctions = {
|
package/lib/commands/initApp.mjs
CHANGED
|
@@ -16,12 +16,36 @@ function validateName(name) {
|
|
|
16
16
|
}
|
|
17
17
|
function resolveMpoVersion(explicit) {
|
|
18
18
|
if (explicit) return explicit.startsWith("^") || explicit.startsWith("~") || explicit === "*" ? explicit : `^${explicit}`;
|
|
19
|
+
const version = cliVersion();
|
|
20
|
+
return version ? `^${version}` : "^6.1.0";
|
|
21
|
+
}
|
|
22
|
+
function cliVersion() {
|
|
19
23
|
try {
|
|
20
24
|
const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
return JSON.parse(readFileSync(pkgPath, "utf-8")).version;
|
|
26
|
+
} catch {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const PROVENANCE_FILE = ".mpo.json";
|
|
31
|
+
function writeProvenance(targetDir, provenance) {
|
|
32
|
+
writeFileSync(join(targetDir, PROVENANCE_FILE), `${JSON.stringify(provenance, null, 2)}\n`);
|
|
33
|
+
}
|
|
34
|
+
function readProvenance(projectDir) {
|
|
35
|
+
const file = join(projectDir, PROVENANCE_FILE);
|
|
36
|
+
if (!existsSync(file)) return void 0;
|
|
37
|
+
try {
|
|
38
|
+
const raw = JSON.parse(readFileSync(file, "utf-8"));
|
|
39
|
+
if (!raw.template || !raw.cliVersion || !raw.name) return void 0;
|
|
40
|
+
return {
|
|
41
|
+
template: raw.template,
|
|
42
|
+
cliVersion: raw.cliVersion,
|
|
43
|
+
name: raw.name,
|
|
44
|
+
mpoVersion: raw.mpoVersion ?? `^${raw.cliVersion}`
|
|
45
|
+
};
|
|
46
|
+
} catch {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
25
49
|
}
|
|
26
50
|
function templatesRoot(template) {
|
|
27
51
|
return resolve(dirname(fileURLToPath(import.meta.url)), "../../templates", template);
|
|
@@ -121,12 +145,45 @@ async function initApp(nameArg, options = {}) {
|
|
|
121
145
|
}
|
|
122
146
|
console.log(` @multiplatform.one/* → ${vars.MPO_VERSION}\n`);
|
|
123
147
|
writeTree(templatesRoot(template), targetDir, vars);
|
|
148
|
+
writeProvenance(targetDir, {
|
|
149
|
+
template,
|
|
150
|
+
cliVersion: cliVersion() ?? "0.0.0",
|
|
151
|
+
name,
|
|
152
|
+
mpoVersion: vars.MPO_VERSION
|
|
153
|
+
});
|
|
154
|
+
let scaffoldCommitted = false;
|
|
155
|
+
if (!options.skipGit) {
|
|
156
|
+
if (!await spawn("git", ["rev-parse", "--is-inside-work-tree"], { cwd: targetDir }).then(() => true, () => false)) {
|
|
157
|
+
const label = template === "universal" ? "universal" : "web-only";
|
|
158
|
+
await spawn("git", ["init", "--quiet"], { cwd: targetDir });
|
|
159
|
+
await spawn("git", ["add", "-A"], { cwd: targetDir });
|
|
160
|
+
scaffoldCommitted = await spawn("git", [
|
|
161
|
+
"commit",
|
|
162
|
+
"--quiet",
|
|
163
|
+
"-m",
|
|
164
|
+
`feat: scaffold ${label} multiplatform.one app (mpo init)`
|
|
165
|
+
], { cwd: targetDir }).then(() => true, () => {
|
|
166
|
+
console.warn("⚠️ git commit failed (missing git identity?) — repo left staged.");
|
|
167
|
+
return false;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
124
171
|
if (!options.skipInstall) {
|
|
125
172
|
console.log("Installing dependencies...");
|
|
126
173
|
await spawn("pnpm", ["install"], {
|
|
127
174
|
cwd: targetDir,
|
|
128
175
|
stdio: "inherit"
|
|
129
176
|
});
|
|
177
|
+
if (scaffoldCommitted) await spawn("git", [
|
|
178
|
+
"add",
|
|
179
|
+
"--",
|
|
180
|
+
"pnpm-lock.yaml"
|
|
181
|
+
], { cwd: targetDir }).then(() => spawn("git", [
|
|
182
|
+
"commit",
|
|
183
|
+
"--amend",
|
|
184
|
+
"--no-edit",
|
|
185
|
+
"--quiet"
|
|
186
|
+
], { cwd: targetDir })).catch(() => {});
|
|
130
187
|
} else console.log("Skipped pnpm install (--skip-install).");
|
|
131
188
|
console.log(`\n✅ Project created at ${targetDir}`);
|
|
132
189
|
console.log("\nNext steps:");
|
|
@@ -142,4 +199,4 @@ async function initApp(nameArg, options = {}) {
|
|
|
142
199
|
}
|
|
143
200
|
|
|
144
201
|
//#endregion
|
|
145
|
-
export { initApp };
|
|
202
|
+
export { PROVENANCE_FILE, cliVersion, initApp, readProvenance, writeProvenance };
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { generateVscodeConfig } from "../generateVscode.mjs";
|
|
2
|
+
import { cliVersion, initApp, readProvenance, writeProvenance } from "./initApp.mjs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
5
|
+
import spawn from "nano-spawn";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
|
|
8
|
+
//#region src/commands/updateApp.ts
|
|
9
|
+
/**
|
|
10
|
+
* `mpo update` for scaffolded consumer projects — the modern successor of
|
|
11
|
+
* the old cookiecutter flow (init a fresh project, swap your .git in, merge
|
|
12
|
+
* your changes on top), rebuilt with a TRUE three-way merge:
|
|
13
|
+
*
|
|
14
|
+
* base = the ORIGINAL scaffold, regenerated from the CLI version recorded
|
|
15
|
+
* in .mpo.json at init time (published CLIs ship their templates,
|
|
16
|
+
* so `pnpm dlx @multiplatform.one/cli@<old> init` reproduces it)
|
|
17
|
+
* ours = the project's HEAD (all your custom changes)
|
|
18
|
+
* theirs= a fresh scaffold from the CURRENT CLI version
|
|
19
|
+
*
|
|
20
|
+
* `git merge-tree --write-tree --merge-base=<base>` merges template
|
|
21
|
+
* evolution with your customizations; conflicts land in the worktree with
|
|
22
|
+
* normal conflict markers. An `.updateignore` file (one pathspec per line)
|
|
23
|
+
* pins matching paths to your HEAD version, exactly like the old script.
|
|
24
|
+
*/
|
|
25
|
+
async function git(projectDir, args, env) {
|
|
26
|
+
return spawn("git", args, {
|
|
27
|
+
cwd: projectDir,
|
|
28
|
+
env: env ? {
|
|
29
|
+
...process.env,
|
|
30
|
+
...env
|
|
31
|
+
} : void 0
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async function gitOut(projectDir, args, env) {
|
|
35
|
+
return (await git(projectDir, args, env)).stdout.trim();
|
|
36
|
+
}
|
|
37
|
+
/** Import a directory tree into the project's object database and return a
|
|
38
|
+
* root-less commit for it (temp index; the worktree is never touched). */
|
|
39
|
+
async function commitTreeFromDir(projectDir, dir, message, parent) {
|
|
40
|
+
const indexFile = join(mkdtempSync(join(tmpdir(), "mpo-index-")), "index");
|
|
41
|
+
const env = {
|
|
42
|
+
GIT_INDEX_FILE: indexFile,
|
|
43
|
+
GIT_WORK_TREE: dir
|
|
44
|
+
};
|
|
45
|
+
await git(projectDir, [
|
|
46
|
+
"add",
|
|
47
|
+
"-A",
|
|
48
|
+
"--force",
|
|
49
|
+
"--",
|
|
50
|
+
"."
|
|
51
|
+
], env);
|
|
52
|
+
const tree = await gitOut(projectDir, ["write-tree"], env);
|
|
53
|
+
const commit = await gitOut(projectDir, parent ? [
|
|
54
|
+
"commit-tree",
|
|
55
|
+
tree,
|
|
56
|
+
"-p",
|
|
57
|
+
parent,
|
|
58
|
+
"-m",
|
|
59
|
+
message
|
|
60
|
+
] : [
|
|
61
|
+
"commit-tree",
|
|
62
|
+
tree,
|
|
63
|
+
"-m",
|
|
64
|
+
message
|
|
65
|
+
]);
|
|
66
|
+
rmSync(indexFile, { force: true });
|
|
67
|
+
return commit;
|
|
68
|
+
}
|
|
69
|
+
/** Scaffold a project with a specific published CLI version into tmp and
|
|
70
|
+
* return the generated project dir. Falls back to the local generator when
|
|
71
|
+
* the requested version matches the running CLI (or dlx fails). */
|
|
72
|
+
async function scaffoldBaseline(version, name, template, mpoVersion) {
|
|
73
|
+
const parent = mkdtempSync(join(tmpdir(), "mpo-update-"));
|
|
74
|
+
const cleanup = () => rmSync(parent, {
|
|
75
|
+
recursive: true,
|
|
76
|
+
force: true
|
|
77
|
+
});
|
|
78
|
+
const current = cliVersion();
|
|
79
|
+
const templateFlag = template === "app" ? ["--web"] : ["--universal"];
|
|
80
|
+
const [major = 0, minor = 0] = version.split(".").map(Number);
|
|
81
|
+
if (!(major > 6 || major === 6 && minor >= 3)) console.warn(`⚠️ @multiplatform.one/cli@${version} predates the consumer scaffolder (6.3.0); using the current CLI's template as the merge base.`);
|
|
82
|
+
else if (version !== current) {
|
|
83
|
+
for (let attempt = 1; attempt <= 2; attempt++) try {
|
|
84
|
+
await spawn("pnpm", [
|
|
85
|
+
"--package",
|
|
86
|
+
`@multiplatform.one/cli@${version}`,
|
|
87
|
+
"dlx",
|
|
88
|
+
"mpo",
|
|
89
|
+
"init",
|
|
90
|
+
name,
|
|
91
|
+
"--yes",
|
|
92
|
+
"--skip-install",
|
|
93
|
+
"--mpo-version",
|
|
94
|
+
mpoVersion,
|
|
95
|
+
...templateFlag
|
|
96
|
+
], {
|
|
97
|
+
cwd: parent,
|
|
98
|
+
stdio: "inherit"
|
|
99
|
+
});
|
|
100
|
+
const dir = join(parent, name);
|
|
101
|
+
if (existsSync(join(dir, "package.json"))) return {
|
|
102
|
+
dir,
|
|
103
|
+
cleanup
|
|
104
|
+
};
|
|
105
|
+
console.warn(`⚠️ dlx scaffold for @multiplatform.one/cli@${version} produced no project`);
|
|
106
|
+
break;
|
|
107
|
+
} catch {
|
|
108
|
+
rmSync(join(parent, name), {
|
|
109
|
+
recursive: true,
|
|
110
|
+
force: true
|
|
111
|
+
});
|
|
112
|
+
if (attempt === 2) console.warn(`⚠️ could not scaffold baseline with @multiplatform.one/cli@${version} (network?);`);
|
|
113
|
+
}
|
|
114
|
+
console.warn(" falling back to the current CLI's template as the merge base.");
|
|
115
|
+
}
|
|
116
|
+
const previousCwd = process.cwd();
|
|
117
|
+
process.chdir(parent);
|
|
118
|
+
try {
|
|
119
|
+
await initApp(name, {
|
|
120
|
+
skipInstall: true,
|
|
121
|
+
skipGit: true,
|
|
122
|
+
yes: true,
|
|
123
|
+
template,
|
|
124
|
+
version: mpoVersion
|
|
125
|
+
});
|
|
126
|
+
} finally {
|
|
127
|
+
process.chdir(previousCwd);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
dir: join(parent, name),
|
|
131
|
+
cleanup
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** Return a tree derived from `tree` where every path matching `pins` is
|
|
135
|
+
* replaced by its content in `headCommit` (removed when absent there).
|
|
136
|
+
* Pure index plumbing — the worktree is untouched. */
|
|
137
|
+
async function pinPathsToHead(projectDir, tree, headCommit, pins) {
|
|
138
|
+
const indexFile = join(mkdtempSync(join(tmpdir(), "mpo-pin-")), "index");
|
|
139
|
+
const env = { GIT_INDEX_FILE: indexFile };
|
|
140
|
+
await git(projectDir, ["read-tree", tree], env);
|
|
141
|
+
for (const pin of pins) {
|
|
142
|
+
const inTree = await gitOut(projectDir, [
|
|
143
|
+
"ls-tree",
|
|
144
|
+
"-r",
|
|
145
|
+
"--name-only",
|
|
146
|
+
tree,
|
|
147
|
+
"--",
|
|
148
|
+
pin
|
|
149
|
+
], env).then((out) => out.split("\n").filter(Boolean)).catch(() => []);
|
|
150
|
+
for (const path of inTree) await git(projectDir, [
|
|
151
|
+
"update-index",
|
|
152
|
+
"--force-remove",
|
|
153
|
+
"--",
|
|
154
|
+
path
|
|
155
|
+
], env).catch(() => {});
|
|
156
|
+
const headEntries = await gitOut(projectDir, [
|
|
157
|
+
"ls-tree",
|
|
158
|
+
"-r",
|
|
159
|
+
headCommit,
|
|
160
|
+
"--",
|
|
161
|
+
pin
|
|
162
|
+
], env).catch(() => "");
|
|
163
|
+
for (const line of headEntries.split("\n").filter(Boolean)) {
|
|
164
|
+
const match = line.match(/^(\d+) \S+ ([0-9a-f]+)\t(.+)$/);
|
|
165
|
+
if (!match) continue;
|
|
166
|
+
await git(projectDir, [
|
|
167
|
+
"update-index",
|
|
168
|
+
"--add",
|
|
169
|
+
"--cacheinfo",
|
|
170
|
+
`${match[1]},${match[2]},${match[3]}`
|
|
171
|
+
], env).catch(() => {});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const pinned = await gitOut(projectDir, ["write-tree"], env);
|
|
175
|
+
rmSync(indexFile, { force: true });
|
|
176
|
+
return pinned;
|
|
177
|
+
}
|
|
178
|
+
async function updateApp(options = {}) {
|
|
179
|
+
const projectDir = await gitOut(process.cwd(), ["rev-parse", "--show-toplevel"]).catch(() => {
|
|
180
|
+
throw new Error("mpo update must run inside a git repository");
|
|
181
|
+
});
|
|
182
|
+
await git(projectDir, ["diff", "--quiet"]).catch(() => {
|
|
183
|
+
throw new Error("mpo update requires a clean working tree (unstaged changes present)");
|
|
184
|
+
});
|
|
185
|
+
await git(projectDir, [
|
|
186
|
+
"diff",
|
|
187
|
+
"--cached",
|
|
188
|
+
"--quiet"
|
|
189
|
+
]).catch(() => {
|
|
190
|
+
throw new Error("mpo update requires a clean working tree (staged changes present)");
|
|
191
|
+
});
|
|
192
|
+
const provenance = readProvenance(projectDir);
|
|
193
|
+
if (!provenance) throw new Error(".mpo.json not found — this project predates scaffold provenance. Create it with { template, cliVersion, name } matching how the project was generated (cliVersion = the @multiplatform.one/cli that scaffolded it), then re-run `mpo update`.");
|
|
194
|
+
const currentVersion = cliVersion() ?? "0.0.0";
|
|
195
|
+
const targetMpoRange = options.version ? options.version.startsWith("^") || options.version.startsWith("~") ? options.version : `^${options.version}` : `^${currentVersion}`;
|
|
196
|
+
console.log(`\nmpo update: ${provenance.cliVersion} → ${currentVersion}`);
|
|
197
|
+
console.log(` template: ${provenance.template}`);
|
|
198
|
+
console.log(` project: ${provenance.name}\n`);
|
|
199
|
+
console.log(`Scaffolding merge base (cli@${provenance.cliVersion})...`);
|
|
200
|
+
const base = await scaffoldBaseline(provenance.cliVersion, provenance.name, provenance.template, provenance.mpoVersion);
|
|
201
|
+
console.log(`Scaffolding update target (cli@${currentVersion})...`);
|
|
202
|
+
const next = await scaffoldBaseline(currentVersion, provenance.name, provenance.template, targetMpoRange);
|
|
203
|
+
rmSync(join(base.dir, ".mpo.json"), { force: true });
|
|
204
|
+
rmSync(join(next.dir, ".mpo.json"), { force: true });
|
|
205
|
+
try {
|
|
206
|
+
const head = await gitOut(projectDir, ["rev-parse", "HEAD"]);
|
|
207
|
+
const baseCommit = await commitTreeFromDir(projectDir, base.dir, `mpo scaffold ${provenance.template}@${provenance.cliVersion}`);
|
|
208
|
+
const nextCommit = await commitTreeFromDir(projectDir, next.dir, `mpo scaffold ${provenance.template}@${currentVersion}`, baseCommit);
|
|
209
|
+
let mergedTree;
|
|
210
|
+
let conflicts = [];
|
|
211
|
+
try {
|
|
212
|
+
mergedTree = (await gitOut(projectDir, [
|
|
213
|
+
"merge-tree",
|
|
214
|
+
"--write-tree",
|
|
215
|
+
"--name-only",
|
|
216
|
+
`--merge-base=${baseCommit}`,
|
|
217
|
+
head,
|
|
218
|
+
nextCommit
|
|
219
|
+
])).split("\n")[0].trim();
|
|
220
|
+
} catch (err) {
|
|
221
|
+
const lines = (err.stdout ?? "").split("\n");
|
|
222
|
+
if (!lines[0]?.trim()) throw err;
|
|
223
|
+
mergedTree = lines[0].trim();
|
|
224
|
+
const blank = lines.indexOf("", 1);
|
|
225
|
+
conflicts = lines.slice(1, blank === -1 ? void 0 : blank).map((line) => line.trim()).filter(Boolean);
|
|
226
|
+
}
|
|
227
|
+
const finalProvenance = {
|
|
228
|
+
template: provenance.template,
|
|
229
|
+
cliVersion: currentVersion,
|
|
230
|
+
name: provenance.name,
|
|
231
|
+
mpoVersion: targetMpoRange
|
|
232
|
+
};
|
|
233
|
+
const pins = ["pnpm-lock.yaml"];
|
|
234
|
+
const updateignore = join(projectDir, ".updateignore");
|
|
235
|
+
if (existsSync(updateignore)) for (const line of readFileSync(updateignore, "utf-8").split("\n")) {
|
|
236
|
+
const pattern = line.trim();
|
|
237
|
+
if (pattern && !pattern.startsWith("#")) pins.push(pattern);
|
|
238
|
+
}
|
|
239
|
+
mergedTree = await pinPathsToHead(projectDir, mergedTree, head, pins);
|
|
240
|
+
conflicts = conflicts.filter((file) => !pins.some((pin) => file === pin || file.startsWith(`${pin}/`)));
|
|
241
|
+
const headTree = await gitOut(projectDir, ["rev-parse", `${head}^{tree}`]);
|
|
242
|
+
if (!conflicts.length && mergedTree === headTree) {
|
|
243
|
+
if (provenance.cliVersion !== finalProvenance.cliVersion || provenance.mpoVersion !== finalProvenance.mpoVersion) {
|
|
244
|
+
writeProvenance(projectDir, finalProvenance);
|
|
245
|
+
await git(projectDir, [
|
|
246
|
+
"add",
|
|
247
|
+
"--",
|
|
248
|
+
".mpo.json"
|
|
249
|
+
]);
|
|
250
|
+
await git(projectDir, [
|
|
251
|
+
"commit",
|
|
252
|
+
"--quiet",
|
|
253
|
+
"-m",
|
|
254
|
+
`chore: record mpo template provenance ${currentVersion}`
|
|
255
|
+
]);
|
|
256
|
+
console.log("\n✅ already up to date (provenance refreshed)");
|
|
257
|
+
} else console.log("\n✅ already up to date");
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (!conflicts.length) {
|
|
261
|
+
const message = `chore: mpo update ${provenance.cliVersion} → ${currentVersion}`;
|
|
262
|
+
await git(projectDir, [
|
|
263
|
+
"update-ref",
|
|
264
|
+
"HEAD",
|
|
265
|
+
await gitOut(projectDir, [
|
|
266
|
+
"commit-tree",
|
|
267
|
+
mergedTree,
|
|
268
|
+
"-p",
|
|
269
|
+
head,
|
|
270
|
+
"-p",
|
|
271
|
+
nextCommit,
|
|
272
|
+
"-m",
|
|
273
|
+
message
|
|
274
|
+
])
|
|
275
|
+
]);
|
|
276
|
+
await git(projectDir, [
|
|
277
|
+
"reset",
|
|
278
|
+
"--hard",
|
|
279
|
+
"HEAD"
|
|
280
|
+
]);
|
|
281
|
+
writeProvenance(projectDir, finalProvenance);
|
|
282
|
+
try {
|
|
283
|
+
await generateVscodeConfig(projectDir);
|
|
284
|
+
} catch {}
|
|
285
|
+
await git(projectDir, [
|
|
286
|
+
"add",
|
|
287
|
+
"--",
|
|
288
|
+
".mpo.json",
|
|
289
|
+
".vscode"
|
|
290
|
+
]);
|
|
291
|
+
await git(projectDir, [
|
|
292
|
+
"commit",
|
|
293
|
+
"--amend",
|
|
294
|
+
"--no-edit",
|
|
295
|
+
"--quiet"
|
|
296
|
+
]);
|
|
297
|
+
const amended = await gitOut(projectDir, [
|
|
298
|
+
"rev-parse",
|
|
299
|
+
"--short",
|
|
300
|
+
"HEAD"
|
|
301
|
+
]);
|
|
302
|
+
console.log(`\n✅ merged cleanly → ${amended} ("${message}")`);
|
|
303
|
+
} else {
|
|
304
|
+
await git(projectDir, [
|
|
305
|
+
"read-tree",
|
|
306
|
+
"-u",
|
|
307
|
+
"--reset",
|
|
308
|
+
mergedTree
|
|
309
|
+
]);
|
|
310
|
+
writeProvenance(projectDir, finalProvenance);
|
|
311
|
+
console.log("\n⚠️ merge conflicts — resolve the markers, then commit:");
|
|
312
|
+
for (const file of conflicts) console.log(` ${file}`);
|
|
313
|
+
console.log(`\n git add -A && git commit -m "chore: mpo update ${provenance.cliVersion} → ${currentVersion}"`);
|
|
314
|
+
}
|
|
315
|
+
if (conflicts.length && !options.skipInstall) console.log("\nSkipping pnpm install until conflicts are resolved (then run: pnpm install).");
|
|
316
|
+
else if (!options.skipInstall) {
|
|
317
|
+
console.log("\nInstalling dependencies...");
|
|
318
|
+
await spawn("pnpm", ["install"], {
|
|
319
|
+
cwd: projectDir,
|
|
320
|
+
stdio: "inherit"
|
|
321
|
+
});
|
|
322
|
+
if (!conflicts.length) {
|
|
323
|
+
if (await git(projectDir, [
|
|
324
|
+
"diff",
|
|
325
|
+
"--quiet",
|
|
326
|
+
"--",
|
|
327
|
+
"pnpm-lock.yaml"
|
|
328
|
+
]).then(() => false, () => true)) {
|
|
329
|
+
await git(projectDir, [
|
|
330
|
+
"add",
|
|
331
|
+
"--",
|
|
332
|
+
"pnpm-lock.yaml"
|
|
333
|
+
]);
|
|
334
|
+
await git(projectDir, [
|
|
335
|
+
"commit",
|
|
336
|
+
"--amend",
|
|
337
|
+
"--no-edit",
|
|
338
|
+
"--quiet"
|
|
339
|
+
]);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
} finally {
|
|
344
|
+
base.cleanup();
|
|
345
|
+
next.cleanup();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
//#endregion
|
|
350
|
+
export { updateApp };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/cli",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.1",
|
|
4
4
|
"description": "multiplatform.one cli — mpo init / create-multiplatform-app",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"create-multiplatform-app",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"nano-spawn": "^2.1.0",
|
|
62
62
|
"yaml": "^2.8.3",
|
|
63
63
|
"yocto-spinner": "^1.1.0",
|
|
64
|
-
"@multiplatform.one/utils": "6.
|
|
64
|
+
"@multiplatform.one/utils": "6.4.1"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@types/inquirer": "^9.0.9",
|
|
@@ -19,7 +19,8 @@ import YAML from "yaml";
|
|
|
19
19
|
import yoctoSpinner from "yocto-spinner";
|
|
20
20
|
import { discoverE2EApp, runE2ESession } from "../commands/e2e";
|
|
21
21
|
import { init, runModifyStep } from "../commands/init";
|
|
22
|
-
import { initApp } from "../commands/initApp";
|
|
22
|
+
import { initApp, readProvenance } from "../commands/initApp";
|
|
23
|
+
import { updateApp } from "../commands/updateApp";
|
|
23
24
|
|
|
24
25
|
const projectRoot = lookupProjectRoot();
|
|
25
26
|
const availableServices = ["api", "frappe", "solana", "ethereum", "sui"];
|
|
@@ -221,6 +222,7 @@ program
|
|
|
221
222
|
.option("--web", "scaffold the web-only template (Vite + React)")
|
|
222
223
|
.option("--app", "alias of --web (back-compat)")
|
|
223
224
|
.option("--skip-install", "skip pnpm install after scaffolding")
|
|
225
|
+
.option("--skip-git", "skip git init + scaffold commit")
|
|
224
226
|
.option(
|
|
225
227
|
"--mpo-version <range>",
|
|
226
228
|
"semver range for @multiplatform.one/* (default: ^<cli version>)",
|
|
@@ -269,6 +271,7 @@ program
|
|
|
269
271
|
}
|
|
270
272
|
await initApp(name, {
|
|
271
273
|
skipInstall: Boolean(options.skipInstall),
|
|
274
|
+
skipGit: Boolean(options.skipGit),
|
|
272
275
|
version: options.mpoVersion,
|
|
273
276
|
template: webOnly ? "app" : options.universal ? "universal" : undefined,
|
|
274
277
|
yes: Boolean(options.yes),
|
|
@@ -279,10 +282,22 @@ const defaultUpdateRemote = "https://gitlab.com/bitspur/multiplatform.one/multip
|
|
|
279
282
|
|
|
280
283
|
program
|
|
281
284
|
.command("update")
|
|
282
|
-
.option("-c, --checkout <branch>", "branch, tag or commit to merge from upstream", "main")
|
|
283
|
-
.option("-r, --remote <url>", "upstream remote URL", defaultUpdateRemote)
|
|
284
|
-
.
|
|
285
|
-
.
|
|
285
|
+
.option("-c, --checkout <branch>", "branch, tag or commit to merge from upstream (monorepo forks)", "main")
|
|
286
|
+
.option("-r, --remote <url>", "upstream remote URL (monorepo forks)", defaultUpdateRemote)
|
|
287
|
+
.option("--skip-install", "skip pnpm install after the update")
|
|
288
|
+
.option(
|
|
289
|
+
"--mpo-version <range>",
|
|
290
|
+
"semver range for @multiplatform.one/* (default: ^<cli version>)",
|
|
291
|
+
)
|
|
292
|
+
.description(
|
|
293
|
+
"update a scaffolded project to the current template (three-way merge via .mpo.json provenance); monorepo forks fall back to the upstream merge flow",
|
|
294
|
+
)
|
|
295
|
+
.action(async (options: {
|
|
296
|
+
checkout: string;
|
|
297
|
+
remote: string;
|
|
298
|
+
skipInstall?: boolean;
|
|
299
|
+
mpoVersion?: string;
|
|
300
|
+
}) => {
|
|
286
301
|
// Update prechecks: must be in a git repo
|
|
287
302
|
if (
|
|
288
303
|
await spawn("git", ["rev-parse", "--is-inside-work-tree"]).then(
|
|
@@ -292,6 +307,27 @@ program
|
|
|
292
307
|
) {
|
|
293
308
|
throw new Error("mpo cannot be updated outside of a git repository");
|
|
294
309
|
}
|
|
310
|
+
// Scaffolded consumer projects carry .mpo.json provenance — use the
|
|
311
|
+
// copier-style three-way template update. Monorepo forks (identified by
|
|
312
|
+
// the legacy features/package.json marker) fall through to the upstream-
|
|
313
|
+
// merge flow below; anything else gets the provenance guidance instead
|
|
314
|
+
// of the fork flow's unrelated errors.
|
|
315
|
+
if (readProvenance(projectRoot)) {
|
|
316
|
+
await updateApp({ skipInstall: options.skipInstall, version: options.mpoVersion });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const legacyFork = await fs.stat(path.resolve(projectRoot, "features/package.json")).then(
|
|
320
|
+
(stat) => stat.isFile(),
|
|
321
|
+
() => false,
|
|
322
|
+
);
|
|
323
|
+
if (!legacyFork) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
".mpo.json not found — this project predates scaffold provenance. " +
|
|
326
|
+
"Create it with { template, cliVersion, name } matching how the project " +
|
|
327
|
+
"was generated (cliVersion = the @multiplatform.one/cli that scaffolded it), " +
|
|
328
|
+
"then re-run `mpo update`.",
|
|
329
|
+
);
|
|
330
|
+
}
|
|
295
331
|
// Require clean working tree (no staged or unstaged changes)
|
|
296
332
|
if (
|
|
297
333
|
await spawn("git", ["diff", "--cached", "--quiet"]).then(
|
|
@@ -781,7 +817,14 @@ program
|
|
|
781
817
|
});
|
|
782
818
|
});
|
|
783
819
|
|
|
784
|
-
|
|
820
|
+
// One-line errors instead of raw stack traces (matches the
|
|
821
|
+
// create-multiplatform-app bin). Stacks stay available via MPO_DEBUG=1.
|
|
822
|
+
program.parseAsync(process.argv).catch((err: unknown) => {
|
|
823
|
+
const error = err as Error;
|
|
824
|
+
if (process.env.MPO_DEBUG) console.error(error);
|
|
825
|
+
else console.error(`✖ ${error.message ?? String(err)}`);
|
|
826
|
+
process.exit(1);
|
|
827
|
+
});
|
|
785
828
|
|
|
786
829
|
async function waitWithSpinner(
|
|
787
830
|
services: string[],
|
package/src/commands/initApp.ts
CHANGED
|
@@ -23,6 +23,8 @@ export type InitAppTemplate = "universal" | "app";
|
|
|
23
23
|
export interface InitAppOptions {
|
|
24
24
|
/** Skip pnpm install (useful for CI / dry scaffolding). */
|
|
25
25
|
skipInstall?: boolean;
|
|
26
|
+
/** Skip git init + scaffold commit (mpo update baselines, nested dirs). */
|
|
27
|
+
skipGit?: boolean;
|
|
26
28
|
/** Pin @multiplatform.one/* to this semver range. Default: ^<cli version>. */
|
|
27
29
|
version?: string;
|
|
28
30
|
/** Project template. When omitted: prompt (interactive) or "universal". */
|
|
@@ -67,14 +69,54 @@ function resolveMpoVersion(explicit?: string): string {
|
|
|
67
69
|
? explicit
|
|
68
70
|
: `^${explicit}`;
|
|
69
71
|
}
|
|
72
|
+
const version = cliVersion();
|
|
73
|
+
return version ? `^${version}` : "^6.1.0";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function cliVersion(): string | undefined {
|
|
70
77
|
try {
|
|
71
78
|
const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
72
79
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { version?: string };
|
|
73
|
-
|
|
80
|
+
return pkg.version;
|
|
74
81
|
} catch {
|
|
75
|
-
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Scaffold provenance (the copier-answers equivalent): which template, which
|
|
88
|
+
* CLI version and which vars produced this project. `mpo update` regenerates
|
|
89
|
+
* the ORIGINAL scaffold from this record to use as the merge base for a
|
|
90
|
+
* three-way template update.
|
|
91
|
+
*/
|
|
92
|
+
export interface MpoProvenance {
|
|
93
|
+
template: InitAppTemplate;
|
|
94
|
+
cliVersion: string;
|
|
95
|
+
name: string;
|
|
96
|
+
mpoVersion: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const PROVENANCE_FILE = ".mpo.json";
|
|
100
|
+
|
|
101
|
+
export function writeProvenance(targetDir: string, provenance: MpoProvenance): void {
|
|
102
|
+
writeFileSync(join(targetDir, PROVENANCE_FILE), `${JSON.stringify(provenance, null, 2)}\n`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function readProvenance(projectDir: string): MpoProvenance | undefined {
|
|
106
|
+
const file = join(projectDir, PROVENANCE_FILE);
|
|
107
|
+
if (!existsSync(file)) return undefined;
|
|
108
|
+
try {
|
|
109
|
+
const raw = JSON.parse(readFileSync(file, "utf-8")) as Partial<MpoProvenance>;
|
|
110
|
+
if (!raw.template || !raw.cliVersion || !raw.name) return undefined;
|
|
111
|
+
return {
|
|
112
|
+
template: raw.template,
|
|
113
|
+
cliVersion: raw.cliVersion,
|
|
114
|
+
name: raw.name,
|
|
115
|
+
mpoVersion: raw.mpoVersion ?? `^${raw.cliVersion}`,
|
|
116
|
+
};
|
|
117
|
+
} catch {
|
|
118
|
+
return undefined;
|
|
76
119
|
}
|
|
77
|
-
return "^6.1.0";
|
|
78
120
|
}
|
|
79
121
|
|
|
80
122
|
function templatesRoot(template: InitAppTemplate): string {
|
|
@@ -203,6 +245,41 @@ export async function initApp(
|
|
|
203
245
|
console.log(` @multiplatform.one/* → ${vars.MPO_VERSION}\n`);
|
|
204
246
|
|
|
205
247
|
writeTree(templatesRoot(template), targetDir, vars);
|
|
248
|
+
writeProvenance(targetDir, {
|
|
249
|
+
template,
|
|
250
|
+
cliVersion: cliVersion() ?? "0.0.0",
|
|
251
|
+
name,
|
|
252
|
+
mpoVersion: vars.MPO_VERSION,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Initialize git with a scaffold commit (unless already inside a repo —
|
|
256
|
+
// e.g. scaffolding into an existing project dir). `mpo update` needs the
|
|
257
|
+
// scaffold as a committed baseline to merge on top of.
|
|
258
|
+
let scaffoldCommitted = false;
|
|
259
|
+
if (!options.skipGit) {
|
|
260
|
+
const insideRepo = await spawn("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
261
|
+
cwd: targetDir,
|
|
262
|
+
}).then(
|
|
263
|
+
() => true,
|
|
264
|
+
() => false,
|
|
265
|
+
);
|
|
266
|
+
if (!insideRepo) {
|
|
267
|
+
const label = template === "universal" ? "universal" : "web-only";
|
|
268
|
+
await spawn("git", ["init", "--quiet"], { cwd: targetDir });
|
|
269
|
+
await spawn("git", ["add", "-A"], { cwd: targetDir });
|
|
270
|
+
scaffoldCommitted = await spawn(
|
|
271
|
+
"git",
|
|
272
|
+
["commit", "--quiet", "-m", `feat: scaffold ${label} multiplatform.one app (mpo init)`],
|
|
273
|
+
{ cwd: targetDir },
|
|
274
|
+
).then(
|
|
275
|
+
() => true,
|
|
276
|
+
() => {
|
|
277
|
+
console.warn("⚠️ git commit failed (missing git identity?) — repo left staged.");
|
|
278
|
+
return false;
|
|
279
|
+
},
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
206
283
|
|
|
207
284
|
if (!options.skipInstall) {
|
|
208
285
|
console.log("Installing dependencies...");
|
|
@@ -210,6 +287,15 @@ export async function initApp(
|
|
|
210
287
|
cwd: targetDir,
|
|
211
288
|
stdio: "inherit",
|
|
212
289
|
});
|
|
290
|
+
// The lockfile belongs to the scaffold — fold it into the scaffold
|
|
291
|
+
// commit so a fresh init is immediately clean.
|
|
292
|
+
if (scaffoldCommitted) {
|
|
293
|
+
await spawn("git", ["add", "--", "pnpm-lock.yaml"], { cwd: targetDir })
|
|
294
|
+
.then(() =>
|
|
295
|
+
spawn("git", ["commit", "--amend", "--no-edit", "--quiet"], { cwd: targetDir }),
|
|
296
|
+
)
|
|
297
|
+
.catch(() => {});
|
|
298
|
+
}
|
|
213
299
|
} else {
|
|
214
300
|
console.log("Skipped pnpm install (--skip-install).");
|
|
215
301
|
}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mpo update` for scaffolded consumer projects — the modern successor of
|
|
3
|
+
* the old cookiecutter flow (init a fresh project, swap your .git in, merge
|
|
4
|
+
* your changes on top), rebuilt with a TRUE three-way merge:
|
|
5
|
+
*
|
|
6
|
+
* base = the ORIGINAL scaffold, regenerated from the CLI version recorded
|
|
7
|
+
* in .mpo.json at init time (published CLIs ship their templates,
|
|
8
|
+
* so `pnpm dlx @multiplatform.one/cli@<old> init` reproduces it)
|
|
9
|
+
* ours = the project's HEAD (all your custom changes)
|
|
10
|
+
* theirs= a fresh scaffold from the CURRENT CLI version
|
|
11
|
+
*
|
|
12
|
+
* `git merge-tree --write-tree --merge-base=<base>` merges template
|
|
13
|
+
* evolution with your customizations; conflicts land in the worktree with
|
|
14
|
+
* normal conflict markers. An `.updateignore` file (one pathspec per line)
|
|
15
|
+
* pins matching paths to your HEAD version, exactly like the old script.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import spawn from "nano-spawn";
|
|
22
|
+
import { generateVscodeConfig } from "../generateVscode";
|
|
23
|
+
import { cliVersion, initApp, readProvenance, writeProvenance } from "./initApp";
|
|
24
|
+
|
|
25
|
+
export interface UpdateAppOptions {
|
|
26
|
+
/** Skip pnpm install after the merge. */
|
|
27
|
+
skipInstall?: boolean;
|
|
28
|
+
/** Override the semver range written for @multiplatform.one/* deps. */
|
|
29
|
+
version?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function git(projectDir: string, args: string[], env?: Record<string, string>) {
|
|
33
|
+
return spawn("git", args, {
|
|
34
|
+
cwd: projectDir,
|
|
35
|
+
env: env ? { ...process.env, ...env } : undefined,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function gitOut(projectDir: string, args: string[], env?: Record<string, string>) {
|
|
40
|
+
const result = await git(projectDir, args, env);
|
|
41
|
+
return result.stdout.trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Import a directory tree into the project's object database and return a
|
|
45
|
+
* root-less commit for it (temp index; the worktree is never touched). */
|
|
46
|
+
async function commitTreeFromDir(
|
|
47
|
+
projectDir: string,
|
|
48
|
+
dir: string,
|
|
49
|
+
message: string,
|
|
50
|
+
parent?: string,
|
|
51
|
+
): Promise<string> {
|
|
52
|
+
const indexFile = join(mkdtempSync(join(tmpdir(), "mpo-index-")), "index");
|
|
53
|
+
const env = { GIT_INDEX_FILE: indexFile, GIT_WORK_TREE: dir };
|
|
54
|
+
await git(projectDir, ["add", "-A", "--force", "--", "."], env);
|
|
55
|
+
const tree = await gitOut(projectDir, ["write-tree"], env);
|
|
56
|
+
const commitArgs = parent
|
|
57
|
+
? ["commit-tree", tree, "-p", parent, "-m", message]
|
|
58
|
+
: ["commit-tree", tree, "-m", message];
|
|
59
|
+
const commit = await gitOut(projectDir, commitArgs);
|
|
60
|
+
rmSync(indexFile, { force: true });
|
|
61
|
+
return commit;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Scaffold a project with a specific published CLI version into tmp and
|
|
65
|
+
* return the generated project dir. Falls back to the local generator when
|
|
66
|
+
* the requested version matches the running CLI (or dlx fails). */
|
|
67
|
+
async function scaffoldBaseline(
|
|
68
|
+
version: string,
|
|
69
|
+
name: string,
|
|
70
|
+
template: "universal" | "app",
|
|
71
|
+
mpoVersion: string,
|
|
72
|
+
): Promise<{ dir: string; cleanup: () => void }> {
|
|
73
|
+
const parent = mkdtempSync(join(tmpdir(), "mpo-update-"));
|
|
74
|
+
const cleanup = () => rmSync(parent, { recursive: true, force: true });
|
|
75
|
+
const current = cliVersion();
|
|
76
|
+
const templateFlag = template === "app" ? ["--web"] : ["--universal"];
|
|
77
|
+
// The consumer scaffolder (initApp + templates) shipped in 6.3.0 —
|
|
78
|
+
// earlier CLIs only had the monorepo-clone init and cannot regenerate a
|
|
79
|
+
// scaffold baseline.
|
|
80
|
+
const [major = 0, minor = 0] = version.split(".").map(Number);
|
|
81
|
+
const scaffoldCapable = major > 6 || (major === 6 && minor >= 3);
|
|
82
|
+
if (!scaffoldCapable) {
|
|
83
|
+
console.warn(
|
|
84
|
+
`⚠️ @multiplatform.one/cli@${version} predates the consumer scaffolder (6.3.0); ` +
|
|
85
|
+
"using the current CLI's template as the merge base.",
|
|
86
|
+
);
|
|
87
|
+
} else if (version !== current) {
|
|
88
|
+
// Older published CLIs reject flags they don't know (--skip-git), so
|
|
89
|
+
// only the stable flag set is passed; a scaffold-side git repo is
|
|
90
|
+
// harmless (index operations never descend into nested .git dirs).
|
|
91
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
92
|
+
try {
|
|
93
|
+
await spawn(
|
|
94
|
+
"pnpm",
|
|
95
|
+
[
|
|
96
|
+
"--package",
|
|
97
|
+
`@multiplatform.one/cli@${version}`,
|
|
98
|
+
"dlx",
|
|
99
|
+
"mpo",
|
|
100
|
+
"init",
|
|
101
|
+
name,
|
|
102
|
+
"--yes",
|
|
103
|
+
"--skip-install",
|
|
104
|
+
"--mpo-version",
|
|
105
|
+
mpoVersion,
|
|
106
|
+
...templateFlag,
|
|
107
|
+
],
|
|
108
|
+
{ cwd: parent, stdio: "inherit" },
|
|
109
|
+
);
|
|
110
|
+
const dir = join(parent, name);
|
|
111
|
+
if (existsSync(join(dir, "package.json"))) return { dir, cleanup };
|
|
112
|
+
console.warn(`⚠️ dlx scaffold for @multiplatform.one/cli@${version} produced no project`);
|
|
113
|
+
break;
|
|
114
|
+
} catch {
|
|
115
|
+
rmSync(join(parent, name), { recursive: true, force: true });
|
|
116
|
+
if (attempt === 2) {
|
|
117
|
+
console.warn(
|
|
118
|
+
`⚠️ could not scaffold baseline with @multiplatform.one/cli@${version} (network?);`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
console.warn(" falling back to the current CLI's template as the merge base.");
|
|
124
|
+
}
|
|
125
|
+
const previousCwd = process.cwd();
|
|
126
|
+
process.chdir(parent);
|
|
127
|
+
try {
|
|
128
|
+
await initApp(name, {
|
|
129
|
+
skipInstall: true,
|
|
130
|
+
skipGit: true,
|
|
131
|
+
yes: true,
|
|
132
|
+
template,
|
|
133
|
+
version: mpoVersion,
|
|
134
|
+
});
|
|
135
|
+
} finally {
|
|
136
|
+
process.chdir(previousCwd);
|
|
137
|
+
}
|
|
138
|
+
return { dir: join(parent, name), cleanup };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Return a tree derived from `tree` where every path matching `pins` is
|
|
142
|
+
* replaced by its content in `headCommit` (removed when absent there).
|
|
143
|
+
* Pure index plumbing — the worktree is untouched. */
|
|
144
|
+
async function pinPathsToHead(
|
|
145
|
+
projectDir: string,
|
|
146
|
+
tree: string,
|
|
147
|
+
headCommit: string,
|
|
148
|
+
pins: string[],
|
|
149
|
+
): Promise<string> {
|
|
150
|
+
const indexFile = join(mkdtempSync(join(tmpdir(), "mpo-pin-")), "index");
|
|
151
|
+
const env = { GIT_INDEX_FILE: indexFile };
|
|
152
|
+
await git(projectDir, ["read-tree", tree], env);
|
|
153
|
+
for (const pin of pins) {
|
|
154
|
+
// Paths under the pin in the merged tree (to drop when HEAD lacks them).
|
|
155
|
+
const inTree = await gitOut(projectDir, ["ls-tree", "-r", "--name-only", tree, "--", pin], env)
|
|
156
|
+
.then((out) => out.split("\n").filter(Boolean))
|
|
157
|
+
.catch(() => [] as string[]);
|
|
158
|
+
for (const path of inTree) {
|
|
159
|
+
await git(projectDir, ["update-index", "--force-remove", "--", path], env).catch(() => {});
|
|
160
|
+
}
|
|
161
|
+
// HEAD's entries for the pin (restored verbatim).
|
|
162
|
+
const headEntries = await gitOut(
|
|
163
|
+
projectDir,
|
|
164
|
+
["ls-tree", "-r", headCommit, "--", pin],
|
|
165
|
+
env,
|
|
166
|
+
).catch(() => "");
|
|
167
|
+
for (const line of headEntries.split("\n").filter(Boolean)) {
|
|
168
|
+
// "<mode> blob <oid>\t<path>"
|
|
169
|
+
const match = line.match(/^(\d+) \S+ ([0-9a-f]+)\t(.+)$/);
|
|
170
|
+
if (!match) continue;
|
|
171
|
+
await git(
|
|
172
|
+
projectDir,
|
|
173
|
+
["update-index", "--add", "--cacheinfo", `${match[1]},${match[2]},${match[3]}`],
|
|
174
|
+
env,
|
|
175
|
+
).catch(() => {});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const pinned = await gitOut(projectDir, ["write-tree"], env);
|
|
179
|
+
rmSync(indexFile, { force: true });
|
|
180
|
+
return pinned;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function updateApp(options: UpdateAppOptions = {}): Promise<void> {
|
|
184
|
+
const projectDir = await gitOut(process.cwd(), ["rev-parse", "--show-toplevel"]).catch(() => {
|
|
185
|
+
throw new Error("mpo update must run inside a git repository");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Clean tree required — the merge lands in the worktree.
|
|
189
|
+
await git(projectDir, ["diff", "--quiet"]).catch(() => {
|
|
190
|
+
throw new Error("mpo update requires a clean working tree (unstaged changes present)");
|
|
191
|
+
});
|
|
192
|
+
await git(projectDir, ["diff", "--cached", "--quiet"]).catch(() => {
|
|
193
|
+
throw new Error("mpo update requires a clean working tree (staged changes present)");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const provenance = readProvenance(projectDir);
|
|
197
|
+
if (!provenance) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
".mpo.json not found — this project predates scaffold provenance. " +
|
|
200
|
+
"Create it with { template, cliVersion, name } matching how the project " +
|
|
201
|
+
"was generated (cliVersion = the @multiplatform.one/cli that scaffolded it), " +
|
|
202
|
+
"then re-run `mpo update`.",
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const currentVersion = cliVersion() ?? "0.0.0";
|
|
207
|
+
const targetMpoRange = options.version
|
|
208
|
+
? options.version.startsWith("^") || options.version.startsWith("~")
|
|
209
|
+
? options.version
|
|
210
|
+
: `^${options.version}`
|
|
211
|
+
: `^${currentVersion}`;
|
|
212
|
+
|
|
213
|
+
console.log(`\nmpo update: ${provenance.cliVersion} → ${currentVersion}`);
|
|
214
|
+
console.log(` template: ${provenance.template}`);
|
|
215
|
+
console.log(` project: ${provenance.name}\n`);
|
|
216
|
+
|
|
217
|
+
// 1. Regenerate the ORIGINAL scaffold (merge base).
|
|
218
|
+
console.log(`Scaffolding merge base (cli@${provenance.cliVersion})...`);
|
|
219
|
+
const base = await scaffoldBaseline(
|
|
220
|
+
provenance.cliVersion,
|
|
221
|
+
provenance.name,
|
|
222
|
+
provenance.template,
|
|
223
|
+
provenance.mpoVersion,
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// 2. Generate the CURRENT scaffold (theirs).
|
|
227
|
+
console.log(`Scaffolding update target (cli@${currentVersion})...`);
|
|
228
|
+
const next = await scaffoldBaseline(
|
|
229
|
+
currentVersion,
|
|
230
|
+
provenance.name,
|
|
231
|
+
provenance.template,
|
|
232
|
+
targetMpoRange,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// .mpo.json is TOOL-owned, never merged: pre-provenance CLIs (≤6.3.0)
|
|
236
|
+
// didn't write it, so leaving it in the scaffolds guarantees an add/add
|
|
237
|
+
// phantom conflict on every honest old-baseline update. It is stripped
|
|
238
|
+
// from both synthetic trees and written authoritatively after the merge.
|
|
239
|
+
rmSync(join(base.dir, ".mpo.json"), { force: true });
|
|
240
|
+
rmSync(join(next.dir, ".mpo.json"), { force: true });
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
// 3. Three-way merge: base → HEAD (ours) vs base → next (theirs).
|
|
244
|
+
const head = await gitOut(projectDir, ["rev-parse", "HEAD"]);
|
|
245
|
+
const baseCommit = await commitTreeFromDir(
|
|
246
|
+
projectDir,
|
|
247
|
+
base.dir,
|
|
248
|
+
`mpo scaffold ${provenance.template}@${provenance.cliVersion}`,
|
|
249
|
+
);
|
|
250
|
+
const nextCommit = await commitTreeFromDir(
|
|
251
|
+
projectDir,
|
|
252
|
+
next.dir,
|
|
253
|
+
`mpo scaffold ${provenance.template}@${currentVersion}`,
|
|
254
|
+
baseCommit,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
let mergedTree: string;
|
|
258
|
+
let conflicts: string[] = [];
|
|
259
|
+
try {
|
|
260
|
+
const out = await gitOut(projectDir, [
|
|
261
|
+
"merge-tree",
|
|
262
|
+
"--write-tree",
|
|
263
|
+
"--name-only",
|
|
264
|
+
`--merge-base=${baseCommit}`,
|
|
265
|
+
head,
|
|
266
|
+
nextCommit,
|
|
267
|
+
]);
|
|
268
|
+
mergedTree = out.split("\n")[0]!.trim();
|
|
269
|
+
} catch (err) {
|
|
270
|
+
// Exit code 1 = conflicts. Output format: <tree>\n<conflicted file
|
|
271
|
+
// names, one per line>\n\n<informational messages> — only the names
|
|
272
|
+
// section (up to the blank line) lists conflicted paths.
|
|
273
|
+
const stdout = (err as { stdout?: string }).stdout ?? "";
|
|
274
|
+
const lines = stdout.split("\n");
|
|
275
|
+
if (!lines[0]?.trim()) throw err;
|
|
276
|
+
mergedTree = lines[0].trim();
|
|
277
|
+
const blank = lines.indexOf("", 1);
|
|
278
|
+
conflicts = lines
|
|
279
|
+
.slice(1, blank === -1 ? undefined : blank)
|
|
280
|
+
.map((line) => line.trim())
|
|
281
|
+
.filter(Boolean);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// The merged .mpo.json can interleave ours/theirs lines (add/add
|
|
285
|
+
// two-way merge) — the post-merge provenance is always written
|
|
286
|
+
// authoritatively below.
|
|
287
|
+
const finalProvenance = {
|
|
288
|
+
template: provenance.template,
|
|
289
|
+
cliVersion: currentVersion,
|
|
290
|
+
name: provenance.name,
|
|
291
|
+
mpoVersion: targetMpoRange,
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// .updateignore pathspecs (+ pnpm-lock.yaml) pin to the pre-update HEAD.
|
|
295
|
+
// Pins are applied to the MERGED TREE before committing, so a clean
|
|
296
|
+
// update never leaves the tree dirty.
|
|
297
|
+
const pins = ["pnpm-lock.yaml"];
|
|
298
|
+
const updateignore = join(projectDir, ".updateignore");
|
|
299
|
+
if (existsSync(updateignore)) {
|
|
300
|
+
for (const line of readFileSync(updateignore, "utf-8").split("\n")) {
|
|
301
|
+
const pattern = line.trim();
|
|
302
|
+
if (pattern && !pattern.startsWith("#")) pins.push(pattern);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
mergedTree = await pinPathsToHead(projectDir, mergedTree, head, pins);
|
|
306
|
+
// Pinning resolves any conflict inside a pinned path (the pre-update
|
|
307
|
+
// HEAD content wins by definition).
|
|
308
|
+
conflicts = conflicts.filter(
|
|
309
|
+
(file) => !pins.some((pin) => file === pin || file.startsWith(`${pin}/`)),
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
// No-op detection: identical trees mean the project already carries the
|
|
313
|
+
// current template — don't stack empty merge commits.
|
|
314
|
+
const headTree = await gitOut(projectDir, ["rev-parse", `${head}^{tree}`]);
|
|
315
|
+
if (!conflicts.length && mergedTree === headTree) {
|
|
316
|
+
if (
|
|
317
|
+
provenance.cliVersion !== finalProvenance.cliVersion ||
|
|
318
|
+
provenance.mpoVersion !== finalProvenance.mpoVersion
|
|
319
|
+
) {
|
|
320
|
+
writeProvenance(projectDir, finalProvenance);
|
|
321
|
+
await git(projectDir, ["add", "--", ".mpo.json"]);
|
|
322
|
+
await git(projectDir, [
|
|
323
|
+
"commit",
|
|
324
|
+
"--quiet",
|
|
325
|
+
"-m",
|
|
326
|
+
`chore: record mpo template provenance ${currentVersion}`,
|
|
327
|
+
]);
|
|
328
|
+
console.log("\n✅ already up to date (provenance refreshed)");
|
|
329
|
+
} else {
|
|
330
|
+
console.log("\n✅ already up to date");
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (!conflicts.length) {
|
|
336
|
+
const message = `chore: mpo update ${provenance.cliVersion} → ${currentVersion}`;
|
|
337
|
+
const mergeCommit = await gitOut(projectDir, [
|
|
338
|
+
"commit-tree",
|
|
339
|
+
mergedTree,
|
|
340
|
+
"-p",
|
|
341
|
+
head,
|
|
342
|
+
"-p",
|
|
343
|
+
nextCommit,
|
|
344
|
+
"-m",
|
|
345
|
+
message,
|
|
346
|
+
]);
|
|
347
|
+
await git(projectDir, ["update-ref", "HEAD", mergeCommit]);
|
|
348
|
+
await git(projectDir, ["reset", "--hard", "HEAD"]);
|
|
349
|
+
// Tool-owned outputs belong to the update commit: provenance + the
|
|
350
|
+
// regenerated .vscode config.
|
|
351
|
+
writeProvenance(projectDir, finalProvenance);
|
|
352
|
+
try {
|
|
353
|
+
await generateVscodeConfig(projectDir);
|
|
354
|
+
} catch {
|
|
355
|
+
// best-effort (older layouts)
|
|
356
|
+
}
|
|
357
|
+
await git(projectDir, ["add", "--", ".mpo.json", ".vscode"]);
|
|
358
|
+
await git(projectDir, ["commit", "--amend", "--no-edit", "--quiet"]);
|
|
359
|
+
const amended = await gitOut(projectDir, ["rev-parse", "--short", "HEAD"]);
|
|
360
|
+
console.log(`\n✅ merged cleanly → ${amended} ("${message}")`);
|
|
361
|
+
} else {
|
|
362
|
+
// Land the merged tree (with conflict markers) in index + worktree for
|
|
363
|
+
// manual resolution; pins are already part of the tree.
|
|
364
|
+
await git(projectDir, ["read-tree", "-u", "--reset", mergedTree]);
|
|
365
|
+
writeProvenance(projectDir, finalProvenance);
|
|
366
|
+
console.log("\n⚠️ merge conflicts — resolve the markers, then commit:");
|
|
367
|
+
for (const file of conflicts) console.log(` ${file}`);
|
|
368
|
+
console.log(
|
|
369
|
+
`\n git add -A && git commit -m "chore: mpo update ${provenance.cliVersion} → ${currentVersion}"`,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (conflicts.length && !options.skipInstall) {
|
|
374
|
+
console.log("\nSkipping pnpm install until conflicts are resolved (then run: pnpm install).");
|
|
375
|
+
} else if (!options.skipInstall) {
|
|
376
|
+
console.log("\nInstalling dependencies...");
|
|
377
|
+
await spawn("pnpm", ["install"], { cwd: projectDir, stdio: "inherit" });
|
|
378
|
+
// The merge changed dependency ranges, so the regenerated lockfile
|
|
379
|
+
// belongs with the update: amend it into the clean-merge commit
|
|
380
|
+
// (conflict resolutions commit manually and pick it up there).
|
|
381
|
+
if (!conflicts.length) {
|
|
382
|
+
const lockDirty = await git(projectDir, [
|
|
383
|
+
"diff",
|
|
384
|
+
"--quiet",
|
|
385
|
+
"--",
|
|
386
|
+
"pnpm-lock.yaml",
|
|
387
|
+
]).then(
|
|
388
|
+
() => false,
|
|
389
|
+
() => true,
|
|
390
|
+
);
|
|
391
|
+
if (lockDirty) {
|
|
392
|
+
await git(projectDir, ["add", "--", "pnpm-lock.yaml"]);
|
|
393
|
+
await git(projectDir, ["commit", "--amend", "--no-edit", "--quiet"]);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
} finally {
|
|
398
|
+
base.cleanup();
|
|
399
|
+
next.cleanup();
|
|
400
|
+
}
|
|
401
|
+
}
|
package/templates/app/gitignore
CHANGED
|
@@ -1,53 +1,8 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
1
|
import { createViteConfig } from "@multiplatform.one/config/vite";
|
|
4
2
|
import { one } from "one/vite";
|
|
5
|
-
import
|
|
6
|
-
import { public as publicConfigKeys } from "../../packages/config/config.json";
|
|
3
|
+
import configJson from "../../packages/config/config.json" with { type: "json" };
|
|
7
4
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Some @multiplatform.one packages at 6.1.0 publish web export targets that
|
|
12
|
-
* don't exist in their tarballs (exports say dist/esm/…/index.js; the build
|
|
13
|
-
* emitted index.mjs), which breaks resolution for anything importing them —
|
|
14
|
-
* e.g. forms → router and frappe-ui → frappe. Resolve those bare imports to
|
|
15
|
-
* the real web builds. Native is unaffected (vxrn's native resolver uses the
|
|
16
|
-
* packages' valid react-native export condition). Remove this plugin once
|
|
17
|
-
* fixed packages are published.
|
|
18
|
-
*/
|
|
19
|
-
const BROKEN_PUBLISHED_ENTRIES: Record<string, { pkg: string; entry: string }> = {
|
|
20
|
-
"@multiplatform.one/router": {
|
|
21
|
-
pkg: "@multiplatform.one/router",
|
|
22
|
-
entry: "dist/esm/index.mjs",
|
|
23
|
-
},
|
|
24
|
-
"@multiplatform.one/frappe": {
|
|
25
|
-
pkg: "@multiplatform.one/frappe",
|
|
26
|
-
entry: "dist/esm/index.mjs",
|
|
27
|
-
},
|
|
28
|
-
"@multiplatform.one/frappe/devtools": {
|
|
29
|
-
pkg: "@multiplatform.one/frappe",
|
|
30
|
-
entry: "dist/esm/devtools/index.mjs",
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
function fixPublishedEsmEntries(): Plugin {
|
|
35
|
-
return {
|
|
36
|
-
name: "fix-multiplatform-published-esm-entries",
|
|
37
|
-
enforce: "pre",
|
|
38
|
-
resolveId(id) {
|
|
39
|
-
const broken = BROKEN_PUBLISHED_ENTRIES[id];
|
|
40
|
-
if (!broken) return null;
|
|
41
|
-
try {
|
|
42
|
-
const pkgJson = require.resolve(`${broken.pkg}/package.json`);
|
|
43
|
-
const entry = pkgJson.replace(/package\.json$/, broken.entry);
|
|
44
|
-
return existsSync(entry) ? entry : null;
|
|
45
|
-
} catch {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
}
|
|
5
|
+
const publicConfigKeys = configJson.public;
|
|
51
6
|
|
|
52
7
|
export default createViteConfig({
|
|
53
8
|
publicConfigKeys,
|
|
@@ -61,6 +16,5 @@ export default createViteConfig({
|
|
|
61
16
|
react: { compiler: true },
|
|
62
17
|
native: { key: "__NAME_PASCAL__" },
|
|
63
18
|
}),
|
|
64
|
-
fixPublishedEsmEntries(),
|
|
65
19
|
],
|
|
66
20
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"multiplatformOne.d.ts","sourceRoot":"","sources":["../../src/bin/multiplatformOne.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"multiplatformOne.d.ts","sourceRoot":"","sources":["../../src/bin/multiplatformOne.ts"],"names":[],"mappings":"AA2FA,gHAAgH;AAChH,MAAM,MAAM,wBAAwB,GAAG;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,kGAAkG;IAClG,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gGAAgG;IAChG,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC,CAAC"}
|
|
@@ -9,6 +9,8 @@ export type InitAppTemplate = "universal" | "app";
|
|
|
9
9
|
export interface InitAppOptions {
|
|
10
10
|
/** Skip pnpm install (useful for CI / dry scaffolding). */
|
|
11
11
|
skipInstall?: boolean;
|
|
12
|
+
/** Skip git init + scaffold commit (mpo update baselines, nested dirs). */
|
|
13
|
+
skipGit?: boolean;
|
|
12
14
|
/** Pin @multiplatform.one/* to this semver range. Default: ^<cli version>. */
|
|
13
15
|
version?: string;
|
|
14
16
|
/** Project template. When omitted: prompt (interactive) or "universal". */
|
|
@@ -16,6 +18,22 @@ export interface InitAppOptions {
|
|
|
16
18
|
/** Accept defaults instead of prompting (non-interactive / CI). */
|
|
17
19
|
yes?: boolean;
|
|
18
20
|
}
|
|
21
|
+
export declare function cliVersion(): string | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Scaffold provenance (the copier-answers equivalent): which template, which
|
|
24
|
+
* CLI version and which vars produced this project. `mpo update` regenerates
|
|
25
|
+
* the ORIGINAL scaffold from this record to use as the merge base for a
|
|
26
|
+
* three-way template update.
|
|
27
|
+
*/
|
|
28
|
+
export interface MpoProvenance {
|
|
29
|
+
template: InitAppTemplate;
|
|
30
|
+
cliVersion: string;
|
|
31
|
+
name: string;
|
|
32
|
+
mpoVersion: string;
|
|
33
|
+
}
|
|
34
|
+
export declare const PROVENANCE_FILE = ".mpo.json";
|
|
35
|
+
export declare function writeProvenance(targetDir: string, provenance: MpoProvenance): void;
|
|
36
|
+
export declare function readProvenance(projectDir: string): MpoProvenance | undefined;
|
|
19
37
|
/**
|
|
20
38
|
* Scaffold a consumer project (universal by default, web-only with
|
|
21
39
|
* template: "app") that depends on @multiplatform.one/* at semver ranges
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"initApp.d.ts","sourceRoot":"","sources":["../../src/commands/initApp.ts"],"names":[],"mappings":"AAaA;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,KAAK,CAAC;AAElD,MAAM,WAAW,cAAc;IAC7B,2DAA2D;IAC3D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,mEAAmE;IACnE,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;
|
|
1
|
+
{"version":3,"file":"initApp.d.ts","sourceRoot":"","sources":["../../src/commands/initApp.ts"],"names":[],"mappings":"AAaA;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,KAAK,CAAC;AAElD,MAAM,WAAW,cAAc;IAC7B,2DAA2D;IAC3D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,mEAAmE;IACnE,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AA0CD,wBAAgB,UAAU,IAAI,MAAM,GAAG,SAAS,CAQ/C;AAED;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,eAAe,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,eAAe,cAAc,CAAC;AAE3C,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,GAAG,IAAI,CAElF;AAED,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAe5E;AAuED;;;;GAIG;AACH,wBAAsB,OAAO,CAC3B,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAmHf"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mpo update` for scaffolded consumer projects — the modern successor of
|
|
3
|
+
* the old cookiecutter flow (init a fresh project, swap your .git in, merge
|
|
4
|
+
* your changes on top), rebuilt with a TRUE three-way merge:
|
|
5
|
+
*
|
|
6
|
+
* base = the ORIGINAL scaffold, regenerated from the CLI version recorded
|
|
7
|
+
* in .mpo.json at init time (published CLIs ship their templates,
|
|
8
|
+
* so `pnpm dlx @multiplatform.one/cli@<old> init` reproduces it)
|
|
9
|
+
* ours = the project's HEAD (all your custom changes)
|
|
10
|
+
* theirs= a fresh scaffold from the CURRENT CLI version
|
|
11
|
+
*
|
|
12
|
+
* `git merge-tree --write-tree --merge-base=<base>` merges template
|
|
13
|
+
* evolution with your customizations; conflicts land in the worktree with
|
|
14
|
+
* normal conflict markers. An `.updateignore` file (one pathspec per line)
|
|
15
|
+
* pins matching paths to your HEAD version, exactly like the old script.
|
|
16
|
+
*/
|
|
17
|
+
export interface UpdateAppOptions {
|
|
18
|
+
/** Skip pnpm install after the merge. */
|
|
19
|
+
skipInstall?: boolean;
|
|
20
|
+
/** Override the semver range written for @multiplatform.one/* deps. */
|
|
21
|
+
version?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function updateApp(options?: UpdateAppOptions): Promise<void>;
|
|
24
|
+
//# sourceMappingURL=updateApp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"updateApp.d.ts","sourceRoot":"","sources":["../../src/commands/updateApp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AASH,MAAM,WAAW,gBAAgB;IAC/B,yCAAyC;IACzC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAyJD,wBAAsB,SAAS,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0N7E"}
|