@multiplatform.one/cli 7.11.0 → 7.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -111
- package/bin/createMultiplatformApp.mjs +39 -37
- package/lib/bin/multiplatformOne.mjs +65 -75
- package/lib/commands/addApp.mjs +78 -0
- package/lib/commands/adoptApp.mjs +11 -7
- package/lib/commands/doctypeTypes.mjs +148 -0
- package/lib/commands/e2e.mjs +2 -2
- package/lib/commands/init.mjs +142 -459
- package/lib/commands/merge.mjs +359 -0
- package/lib/commands/pieces.mjs +236 -0
- package/lib/commands/provenance.mjs +108 -0
- package/lib/commands/scaffold.mjs +105 -0
- package/lib/commands/updateApp.mjs +211 -528
- package/lib/source.json +5 -0
- package/lib/source.mjs +63 -0
- package/package.json +6 -6
- package/scripts/frappe-bootstrap.sh +29 -3
- package/scripts/frappe-dev.sh +1 -2
- package/scripts/frappe-helpers.sh +50 -2
- package/scripts/frappe.sh +5 -0
- package/scripts/scaffold.sh +937 -0
- package/scripts/stamp-source.mjs +51 -0
- package/scripts/stamp-source.spec.ts +86 -0
- package/src/bin/multiplatformOne.ts +116 -226
- package/src/commands/addApp.ts +102 -0
- package/src/commands/adoptApp.spec.ts +5 -1
- package/src/commands/adoptApp.ts +15 -10
- package/src/commands/doctypeTypes.spec.ts +155 -0
- package/src/commands/doctypeTypes.ts +214 -0
- package/src/commands/init.spec.ts +66 -0
- package/src/commands/init.ts +201 -643
- package/src/commands/merge.spec.ts +81 -0
- package/src/commands/merge.ts +411 -0
- package/src/commands/pieces.spec.ts +62 -0
- package/src/commands/pieces.ts +340 -0
- package/src/commands/provenance.spec.ts +92 -0
- package/src/commands/provenance.ts +169 -0
- package/src/commands/scaffold.ts +131 -0
- package/src/commands/updateApp.spec.ts +540 -331
- package/src/commands/updateApp.ts +255 -668
- package/src/source.spec.ts +45 -0
- package/src/source.ts +88 -0
- package/templates/AGENTS.md +161 -0
- package/templates/README.md +62 -0
- package/templates/pieces/frappe/universal/README.md.partial +7 -32
- package/templates/pieces/frappe/universal/env.example.partial +3 -3
- package/templates/pieces/frappe-bench/universal/README.md.partial +28 -0
- package/templates/pieces/{frappe → frappe-bench}/universal/docker/compose.frappe.yaml +2 -2
- package/templates/pieces/tauri/universal/README.md.partial +7 -5
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/src/dev.rs +463 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/src/lib.rs +6 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/tauri.conf.json +2 -1
- package/templates/pieces/webext/universal/apps/__NAME__/package.json.partial +1 -0
- package/templates/pieces/webext/universal/apps/__NAME__/vite.config.webext.ts +6 -2
- package/types/bin/multiplatformOne.d.ts.map +1 -1
- package/types/commands/addApp.d.ts +20 -0
- package/types/commands/addApp.d.ts.map +1 -0
- package/types/commands/adoptApp.d.ts +4 -3
- package/types/commands/adoptApp.d.ts.map +1 -1
- package/types/commands/doctypeTypes.d.ts +40 -0
- package/types/commands/doctypeTypes.d.ts.map +1 -0
- package/types/commands/init.d.ts +30 -13
- package/types/commands/init.d.ts.map +1 -1
- package/types/commands/merge.d.ts +54 -0
- package/types/commands/merge.d.ts.map +1 -0
- package/types/commands/pieces.d.ts +54 -0
- package/types/commands/pieces.d.ts.map +1 -0
- package/types/commands/provenance.d.ts +52 -0
- package/types/commands/provenance.d.ts.map +1 -0
- package/types/commands/scaffold.d.ts +43 -0
- package/types/commands/scaffold.d.ts.map +1 -0
- package/types/commands/updateApp.d.ts +19 -27
- package/types/commands/updateApp.d.ts.map +1 -1
- package/types/source.d.ts +35 -0
- package/types/source.d.ts.map +1 -0
- package/lib/commands/initApp.mjs +0 -431
- package/lib/generateVscode.mjs +0 -184
- package/scripts/clone.sh +0 -17
- package/scripts/update.sh +0 -46
- package/src/commands/initApp.spec.ts +0 -720
- package/src/commands/initApp.ts +0 -706
- package/src/generateVscode.ts +0 -238
- package/templates/app/AGENTS.md +0 -142
- package/templates/app/README.md +0 -62
- package/templates/app/apps/__NAME__/index.html +0 -36
- package/templates/app/apps/__NAME__/package.json +0 -31
- package/templates/app/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
- package/templates/app/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
- package/templates/app/apps/__NAME__/src/App.tsx +0 -9
- package/templates/app/apps/__NAME__/src/main.tsx +0 -14
- package/templates/app/apps/__NAME__/tsconfig.json +0 -8
- package/templates/app/apps/__NAME__/vite.config.ts +0 -17
- package/templates/app/features/__NAME__/package.json +0 -34
- package/templates/app/features/__NAME__/src/HomeScreen.tsx +0 -70
- package/templates/app/features/__NAME__/src/index.ts +0 -1
- package/templates/app/features/__NAME__/tsconfig.json +0 -5
- package/templates/app/gitignore +0 -9
- package/templates/app/npmrc +0 -2
- package/templates/app/oxlintrc.json +0 -39
- package/templates/app/package.json +0 -38
- package/templates/app/packages/config/config.json +0 -4
- package/templates/app/packages/config/index.ts +0 -5
- package/templates/app/packages/config/package.json +0 -17
- package/templates/app/packages/config/tsconfig.json +0 -5
- package/templates/app/pnpm-workspace.yaml +0 -4
- package/templates/app/tsconfig.base.json +0 -16
- package/templates/universal/AGENTS.md +0 -142
- package/templates/universal/README.md +0 -97
- package/templates/universal/apps/__NAME__/app.config.js +0 -31
- package/templates/universal/apps/__NAME__/config/tamagui.config.ts +0 -4
- package/templates/universal/apps/__NAME__/package.json +0 -62
- package/templates/universal/apps/__NAME__/polyfills.native.ts +0 -23
- package/templates/universal/apps/__NAME__/polyfills.ts +0 -3
- package/templates/universal/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
- package/templates/universal/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
- package/templates/universal/apps/__NAME__/routes/_layout.tsx +0 -22
- package/templates/universal/apps/__NAME__/routes/_layout.web.tsx +0 -21
- package/templates/universal/apps/__NAME__/routes/about.tsx +0 -3
- package/templates/universal/apps/__NAME__/routes/index.tsx +0 -3
- package/templates/universal/apps/__NAME__/src/app.ts +0 -10
- package/templates/universal/apps/__NAME__/tamagui.css +0 -3
- package/templates/universal/apps/__NAME__/tsconfig.json +0 -8
- package/templates/universal/apps/__NAME__/types.d.ts +0 -9
- package/templates/universal/apps/__NAME__/vite.config.ts +0 -20
- package/templates/universal/features/__NAME__/package.json +0 -35
- package/templates/universal/features/__NAME__/src/AboutScreen.tsx +0 -27
- package/templates/universal/features/__NAME__/src/AppLayout.tsx +0 -50
- package/templates/universal/features/__NAME__/src/ContactForm.spec.tsx +0 -22
- package/templates/universal/features/__NAME__/src/ContactForm.tsx +0 -55
- package/templates/universal/features/__NAME__/src/HomeScreen.tsx +0 -40
- package/templates/universal/features/__NAME__/src/index.ts +0 -4
- package/templates/universal/features/__NAME__/src/screens.spec.ts +0 -12
- package/templates/universal/features/__NAME__/tsconfig.json +0 -5
- package/templates/universal/features/__NAME__/vitest.config.mjs +0 -7
- package/templates/universal/gitignore +0 -14
- package/templates/universal/npmrc +0 -3
- package/templates/universal/oxlintrc.json +0 -39
- package/templates/universal/package.json +0 -51
- package/templates/universal/packages/config/config.json +0 -4
- package/templates/universal/packages/config/index.ts +0 -5
- package/templates/universal/packages/config/package.json +0 -17
- package/templates/universal/packages/config/tsconfig.json +0 -5
- package/templates/universal/packages/i18n/en/common.json +0 -30
- package/templates/universal/packages/i18n/es/common.json +0 -30
- package/templates/universal/packages/i18n/i18n.d.ts +0 -16
- package/templates/universal/packages/i18n/index.ts +0 -26
- package/templates/universal/packages/i18n/package.json +0 -20
- package/templates/universal/packages/i18n/resources.ts +0 -13
- package/templates/universal/packages/i18n/tsconfig.json +0 -5
- package/templates/universal/packages/themes/accent.ts +0 -32
- package/templates/universal/packages/themes/base.ts +0 -50
- package/templates/universal/packages/themes/fonts.ts +0 -8
- package/templates/universal/packages/themes/index.ts +0 -33
- package/templates/universal/packages/themes/package.json +0 -23
- package/templates/universal/packages/themes/tamagui.config.ts +0 -14
- package/templates/universal/packages/themes/tsconfig.json +0 -5
- package/templates/universal/pnpm-workspace.yaml +0 -4
- package/templates/universal/tsconfig.base.json +0 -21
- package/templates/universal/types/env.d.ts +0 -9
- package/types/commands/initApp.d.ts +0 -114
- package/types/commands/initApp.d.ts.map +0 -1
- package/types/generateVscode.d.ts +0 -7
- package/types/generateVscode.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @multiplatform.one/cli
|
|
2
2
|
|
|
3
|
-
The multiplatform.one command line: scaffold
|
|
4
|
-
(`mpo init` / `create-multiplatform-app`),
|
|
5
|
-
|
|
3
|
+
The multiplatform.one command line: scaffold a project from the monorepo
|
|
4
|
+
(`mpo init` / `create-multiplatform-app`), merge the monorepo's changes back
|
|
5
|
+
in (`mpo update`), and drive the dev environment, Frappe and E2E workflows.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -13,156 +13,166 @@ npx --package=@multiplatform.one/cli create-multiplatform-app my-app
|
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Two binaries: **`mpo`** (full CLI) and **`create-multiplatform-app`**
|
|
16
|
-
(
|
|
16
|
+
(`mpo init` only).
|
|
17
17
|
|
|
18
|
-
## Scaffold a project
|
|
18
|
+
## Scaffold a project
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
mpo init my-app
|
|
21
|
+
mpo init my-app # prompts for the singleton apps to keep
|
|
22
|
+
mpo init my-app --service frappe --yes # non-interactive
|
|
23
|
+
mpo init my-app --app admin # two product apps: my-app and admin
|
|
22
24
|
cd my-app
|
|
23
|
-
pnpm
|
|
24
|
-
pnpm dev # web + native dev server
|
|
25
|
-
pnpm ios # build & run the iOS dev client (pnpm dev in another terminal)
|
|
25
|
+
pnpm dev
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
`
|
|
28
|
+
A project is the multiplatform.one monorepo at one commit, pruned:
|
|
29
|
+
|
|
30
|
+
- `public/` is gone; `@multiplatform.one/*` come from npmjs, the default
|
|
31
|
+
registry, at `^<cli version>` (`--mpo-version` overrides the range).
|
|
32
|
+
- The singleton apps are ethereum, frappe, keycloak, solana, storybook,
|
|
33
|
+
storybook-expo, sui, uxpin and vocs; only the ones asked for stay.
|
|
34
|
+
`--frappe` and `--keycloak` are shorthands for `--service`.
|
|
35
|
+
- Without `--service frappe` the project keeps the Frappe client
|
|
36
|
+
(`createApp`'s wiring, the `# frappe client` keys in `.env.example`) and
|
|
37
|
+
drops the bench: `apps/frappe`, its `.env` keys, and the `docker/compose.yaml`
|
|
38
|
+
services in the `frappe` profile with the volumes only they used.
|
|
39
|
+
- Without `--service keycloak` the keycloak compose service, `postgres` and
|
|
40
|
+
its `.env` keys, the `docker-bake.hcl` target and the `docker/Makefile`
|
|
41
|
+
cert block go. With neither service, `docker/compose.yaml` is
|
|
42
|
+
`services: {}` and there is no `pnpm backend`.
|
|
43
|
+
- `apps/one`, the One platform app, becomes `apps/my-app` (package
|
|
44
|
+
`@app/my-app`, app.config slug and scheme, bundle id, tauri identifier, the
|
|
45
|
+
`Makefile` `my-app/%` rules, `.vscode`, `.gitignore`), with
|
|
46
|
+
`features/my-app` beside it. Every `--app` is another copy. The
|
|
47
|
+
delivery targets (`gnome/`, `tauri/`, `vscode/`, `webext/`, the Frappe SPA)
|
|
48
|
+
ride inside each product app; there is nothing to add for them.
|
|
49
|
+
- `packages/themes` is the app's one theme config, and it carries a declared
|
|
50
|
+
theme subset (`subset.ts`, `subset.tamagui.config.ts`). A narrow surface
|
|
51
|
+
ships only the themes it can enter, because every theme is CSS in the served
|
|
52
|
+
document; `packages/themes/README.md` has the measurement and the switch.
|
|
53
|
+
- `docs/`, `deploy/`, `.changeset`, `typedoc.json` and `.gitlab-ci.yml` are
|
|
54
|
+
monorepo concerns and go too. `AGENTS.md` is the consumer copy
|
|
55
|
+
(`templates/AGENTS.md`), which names only paths and scripts the project has.
|
|
56
|
+
`README.md` is the consumer copy too (`templates/README.md`): a block
|
|
57
|
+
between `<!-- service:<app> -->` and `<!-- /service:<app> -->` stays only
|
|
58
|
+
when that service was kept, a `no-service:<app>` block only when it was not.
|
|
59
|
+
- `.mpo.json` records the source commit and the selection; `mpo update` and
|
|
60
|
+
`mpo add-app` read it.
|
|
61
|
+
|
|
62
|
+
The commit comes from the CLI itself: a published build carries the monorepo
|
|
63
|
+
commit it was cut from (`lib/source.json`); a CLI running inside a monorepo
|
|
64
|
+
checkout uses that checkout's `HEAD`; a build without a stamp falls back to
|
|
65
|
+
the `v<version>` tag, then `main`, and says so. `--source <path|url>` and
|
|
66
|
+
`--checkout <ref>` override it.
|
|
67
|
+
|
|
68
|
+
### Into an existing directory
|
|
32
69
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
(`--app` is a back-compat alias; `create-multiplatform-app` without flags also
|
|
39
|
-
produces it — pass `--universal` there for the full project).
|
|
40
|
-
|
|
41
|
-
Options: `--universal`, `--web`/`--app`, `--skip-install`, `--skip-git`,
|
|
42
|
-
`--mpo-version <range>` (defaults to `^<cli version>`), `-y/--yes`.
|
|
43
|
-
|
|
44
|
-
### Pieces
|
|
45
|
-
|
|
46
|
-
Composable fragments overlaid onto the base template. Pass any of them to skip
|
|
47
|
-
the interactive prompt; they are recorded in `.mpo.json` and reproduced by
|
|
48
|
-
`mpo update`, so pick them at init time.
|
|
70
|
+
```bash
|
|
71
|
+
cd my-python-service
|
|
72
|
+
mpo init --into # product app named after the directory
|
|
73
|
+
mpo init --into --app dashboard . # or name it
|
|
74
|
+
```
|
|
49
75
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
| `--gnome` | GNOME desktop: **real GTK4 widgets** via react-gnome, no webview | gjs, gtk4, libadwaita |
|
|
55
|
-
| `--vscode` | VS Code extension — extension host + Tamagui webview | — |
|
|
56
|
-
| `--webext` | Browser extension — MV3 popup + background | — |
|
|
76
|
+
Paths that already exist are kept byte for byte, listed, and pinned in
|
|
77
|
+
`.updateignore` so no later update rewrites them. Nothing is committed; the
|
|
78
|
+
directory is yours to commit. `mpo adopt` is not the way in for this: it only
|
|
79
|
+
registers hand-copied piece files (see below).
|
|
57
80
|
|
|
58
|
-
`--
|
|
59
|
-
|
|
60
|
-
|
|
81
|
+
Options: `--app <name>` (repeatable), `--service <name>` (repeatable),
|
|
82
|
+
`--frappe`, `--keycloak`, `--into`, `--source`, `--checkout`, `--mpo-version`,
|
|
83
|
+
`--skip-install`, `--skip-git`, `--yes`, `--verbose`.
|
|
61
84
|
|
|
62
85
|
## Update a project
|
|
63
86
|
|
|
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
87
|
```bash
|
|
82
|
-
mpo
|
|
88
|
+
mpo update # requires a clean tree
|
|
89
|
+
git status # the merge sits in the index and worktree
|
|
90
|
+
git add -A && git commit -m "chore: mpo update <old> → <new>"
|
|
83
91
|
```
|
|
84
92
|
|
|
85
|
-
|
|
86
|
-
`.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
A three-way merge: **base** is the scaffold at the commit recorded in
|
|
94
|
+
`.mpo.json` with the recorded apps and services, **ours** is your `HEAD`,
|
|
95
|
+
**theirs** is the same scaffold at the commit this CLI was built from (or
|
|
96
|
+
`--source`/`--checkout`). Your commits are preserved, the monorepo's changes
|
|
97
|
+
land as the diff, every product app receives the One app's changes, and
|
|
98
|
+
nothing is committed. Conflicts land with normal markers; `pnpm install` runs
|
|
99
|
+
on a clean merge (`--skip-install` to skip it).
|
|
100
|
+
|
|
101
|
+
`.updateignore` entries (exact files or directory prefixes, not globs) and
|
|
102
|
+
`pnpm-lock.yaml` stay pinned to your version. Dependency-range drift in
|
|
103
|
+
`package.json` files resolves to the newer minimum.
|
|
91
104
|
|
|
92
|
-
|
|
105
|
+
**Idempotency:** update, commit, update again: zero changes.
|
|
93
106
|
|
|
94
|
-
|
|
95
|
-
was scaffolded with instead of hand-writing the file:
|
|
107
|
+
## Add a product app
|
|
96
108
|
|
|
97
109
|
```bash
|
|
98
|
-
mpo
|
|
110
|
+
mpo add-app kiosk
|
|
99
111
|
```
|
|
100
112
|
|
|
101
|
-
|
|
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.
|
|
113
|
+
Scaffolds `apps/kiosk` + `features/kiosk` from `apps/one` at the recorded
|
|
114
|
+
commit and merges only that in, uncommitted like `mpo update`.
|
|
109
115
|
|
|
110
|
-
|
|
111
|
-
successful update makes zero changes (clean `git status`). Run it, see no
|
|
112
|
-
changes, know you're current.
|
|
116
|
+
## Adopt hand-copied pieces
|
|
113
117
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
(webext, tauri, …) and uses `@multiplatform.one/*` from npm — has no
|
|
118
|
-
provenance. Bring it under `mpo update` management:
|
|
118
|
+
A project that was never scaffolded and hand-copied a delivery target from
|
|
119
|
+
`templates/pieces` (webext, gnome, tauri, vscode, keycloak wiring, the Frappe
|
|
120
|
+
client as `frappe` and its dev bench as `frappe-bench`) has no provenance:
|
|
119
121
|
|
|
120
122
|
```bash
|
|
121
|
-
mpo adopt --webext --name my-app #
|
|
123
|
+
mpo adopt --webext --name my-app # clean git tree; at least one piece
|
|
122
124
|
```
|
|
123
125
|
|
|
124
126
|
Adoption writes `.mpo.json` (template `"none"` + pieces) and seeds
|
|
125
|
-
`.updateignore`;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
`
|
|
127
|
+
`.updateignore`; it changes no project file. The first `mpo update` reconciles
|
|
128
|
+
the piece files from an empty base (diffs and conflicts are expected); later
|
|
129
|
+
updates merge three-way against the pieces of the recorded CLI version. An
|
|
130
|
+
adoption recorded while the `frappe` piece still carried the bench gains
|
|
131
|
+
`frappe-bench` on its next update, so the bench stays.
|
|
132
|
+
|
|
133
|
+
A project from the retired template scaffolder (`.mpo.json` with `template:
|
|
134
|
+
"universal"` or `"app"`) has no reproducible base left: its next `mpo update`
|
|
135
|
+
merges the monorepo's One app in from an empty base once, then it is
|
|
136
|
+
clone-derived.
|
|
130
137
|
|
|
131
|
-
|
|
138
|
+
## The scaffold script
|
|
132
139
|
|
|
133
|
-
|
|
134
|
-
was scaffolded with instead of hand-writing the file:
|
|
140
|
+
`scripts/scaffold.sh` is POSIX sh and needs only git, node and a shell:
|
|
135
141
|
|
|
136
142
|
```bash
|
|
137
|
-
|
|
143
|
+
sh scripts/scaffold.sh [--app <name>]... [--service <name>]... [--mpo-version <range>] [--into] \
|
|
144
|
+
<path-or-url> <ref> <target>
|
|
138
145
|
```
|
|
139
146
|
|
|
140
147
|
## Other commands
|
|
141
148
|
|
|
142
|
-
| Command
|
|
143
|
-
|
|
|
144
|
-
| `mpo
|
|
145
|
-
| `mpo
|
|
146
|
-
| `mpo
|
|
147
|
-
| `mpo
|
|
148
|
-
| `mpo frappe
|
|
149
|
-
| `mpo frappe
|
|
150
|
-
| `mpo
|
|
151
|
-
| `mpo
|
|
152
|
-
| `mpo build` | Run the build command |
|
|
153
|
-
| `mpo test` | Run tests (E2E flags: `--up-only`, `--down`, `--filter`, …) |
|
|
149
|
+
| Command | Purpose |
|
|
150
|
+
| ----------------------------- | --------------------------------------------------------------------------------- |
|
|
151
|
+
| `mpo wait` | Wait for a service (frappe, postgres, mariadb, keycloak) to be ready |
|
|
152
|
+
| `mpo frappe bootstrap` | Bootstrap the Frappe development environment |
|
|
153
|
+
| `mpo frappe dev` | Start the Frappe development server |
|
|
154
|
+
| `mpo frappe bench -- <args>` | Run a bench command |
|
|
155
|
+
| `mpo frappe clean` | Clean Frappe bench artifacts |
|
|
156
|
+
| `mpo frappe types <doctype…>` | Emit doctype TypeScript types from getdoctype; `--check -o <file>` fails on drift |
|
|
157
|
+
| `mpo build` | Run the build command |
|
|
158
|
+
| `mpo test` | Run tests (E2E flags: `--up-only`, `--down`, `--filter`, …) |
|
|
154
159
|
|
|
155
160
|
## Development
|
|
156
161
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
162
|
+
`pnpm --filter @multiplatform.one/cli test` runs the vitest specs: the
|
|
163
|
+
provenance and merge units, and `updateApp.spec.ts`, which scaffolds from
|
|
164
|
+
this checkout at `HEAD`, updates from a second clone that moved ahead, and
|
|
165
|
+
adds an app. `make -C public/cli test/bats` runs `tests/*.bats` against
|
|
166
|
+
`scripts/scaffold.sh` directly (needs `bats`). `pnpm build` stamps
|
|
167
|
+
`lib/source.json` from `git rev-parse HEAD` (`MPO_SOURCE_SHA` /
|
|
168
|
+
`MPO_SOURCE_URL` override it), unless `HEAD` carries another CLI version;
|
|
169
|
+
the release pack stamps the version commit.
|
|
160
170
|
|
|
161
171
|
## What it must not do
|
|
162
172
|
|
|
163
|
-
-
|
|
164
|
-
`@multiplatform.one
|
|
165
|
-
|
|
173
|
+
- A scaffolded project never carries `public/` or `workspace:*` ranges on
|
|
174
|
+
`@multiplatform.one/*`; consumers upgrade via the registry
|
|
175
|
+
(`pnpm update "@multiplatform.one/*"`) and `mpo update`.
|
|
166
176
|
|
|
167
177
|
## License
|
|
168
178
|
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* create-multiplatform-app
|
|
5
|
-
*
|
|
4
|
+
* create-multiplatform-app: the npm create / npx entry. Same command as
|
|
5
|
+
* `mpo init` (the monorepo at one commit, pruned into a project).
|
|
6
6
|
*
|
|
7
|
-
* Defaults to the web-only app template (back-compat). Pass --universal for
|
|
8
|
-
* the full web + iOS + Android project (same as `mpo init <name>`).
|
|
9
|
-
*
|
|
10
|
-
* Usage:
|
|
11
7
|
* npx --package=@multiplatform.one/cli create-multiplatform-app my-app
|
|
12
|
-
* npx --package=@multiplatform.one/cli create-multiplatform-app my-app --
|
|
13
|
-
* mpo init my-app # universal
|
|
14
|
-
* mpo init --web my-app # web-only
|
|
8
|
+
* npx --package=@multiplatform.one/cli create-multiplatform-app my-app --service frappe
|
|
15
9
|
*/
|
|
16
10
|
import { createRequire } from "node:module";
|
|
17
11
|
import { dirname, resolve } from "node:path";
|
|
@@ -20,25 +14,23 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
20
14
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
21
15
|
const require = createRequire(import.meta.url);
|
|
22
16
|
|
|
23
|
-
function
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
function flagValues(argv, name) {
|
|
18
|
+
const values = [];
|
|
19
|
+
for (let i = 0; i < argv.length; i++) {
|
|
20
|
+
if (argv[i] === name && argv[i + 1] !== undefined) values.push(argv[++i]);
|
|
21
|
+
}
|
|
22
|
+
return values;
|
|
27
23
|
}
|
|
28
24
|
|
|
29
25
|
async function main() {
|
|
30
26
|
const argv = process.argv.slice(2);
|
|
31
27
|
if (argv[0] === "--help" || argv[0] === "-h") {
|
|
32
|
-
console.log(`create-multiplatform-app [name] [--
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
(not workspace:*):
|
|
28
|
+
console.log(`create-multiplatform-app [name] [--app <name>]... [--service <name>]... [--into]
|
|
29
|
+
[--source <path|url>] [--checkout <ref>] [--mpo-version <range>]
|
|
30
|
+
[--skip-install] [--skip-git] [--yes]
|
|
36
31
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
(adds packages/i18n + packages/themes)
|
|
40
|
-
|
|
41
|
-
Same as: mpo init --web [name] / mpo init [name]
|
|
32
|
+
Scaffold a project: the multiplatform.one monorepo at one commit, pruned.
|
|
33
|
+
Same as: mpo init [name] ...
|
|
42
34
|
`);
|
|
43
35
|
return;
|
|
44
36
|
}
|
|
@@ -46,27 +38,37 @@ Same as: mpo init --web [name] / mpo init [name]
|
|
|
46
38
|
console.log(require("../package.json").version);
|
|
47
39
|
return;
|
|
48
40
|
}
|
|
41
|
+
const valued = new Set(["--app", "--service", "--source", "--checkout", "--mpo-version"]);
|
|
42
|
+
let name;
|
|
43
|
+
for (let i = 0; i < argv.length; i++) {
|
|
44
|
+
if (valued.has(argv[i])) {
|
|
45
|
+
i++;
|
|
46
|
+
} else if (!argv[i].startsWith("-")) {
|
|
47
|
+
name = argv[i];
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
49
51
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
const universal = argv.includes("--universal");
|
|
53
|
-
const version = flagValue(argv, "--mpo-version");
|
|
54
|
-
const name = argv.find((a) => !a.startsWith("-"));
|
|
55
|
-
|
|
56
|
-
const libPath = resolve(here, "../lib/commands/initApp.mjs");
|
|
57
|
-
let initApp;
|
|
52
|
+
const libPath = resolve(here, "../lib/commands/init.mjs");
|
|
53
|
+
let init;
|
|
58
54
|
try {
|
|
59
|
-
({
|
|
55
|
+
({ init } = await import(pathToFileURL(libPath).href));
|
|
60
56
|
} catch {
|
|
61
57
|
throw new Error(
|
|
62
|
-
"Could not load
|
|
58
|
+
"Could not load init. Build @multiplatform.one/cli first (pnpm --filter @multiplatform.one/cli build).",
|
|
63
59
|
);
|
|
64
60
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
const services = flagValues(argv, "--service");
|
|
62
|
+
const yes = argv.includes("--yes") || argv.includes("-y");
|
|
63
|
+
await init(name, {
|
|
64
|
+
apps: flagValues(argv, "--app"),
|
|
65
|
+
services: services.length || yes ? services : undefined,
|
|
66
|
+
source: flagValues(argv, "--source")[0],
|
|
67
|
+
checkout: flagValues(argv, "--checkout")[0],
|
|
68
|
+
mpoVersion: flagValues(argv, "--mpo-version")[0],
|
|
69
|
+
skipInstall: argv.includes("--skip-install"),
|
|
70
|
+
skipGit: argv.includes("--skip-git"),
|
|
71
|
+
into: argv.includes("--into"),
|
|
70
72
|
yes,
|
|
71
73
|
});
|
|
72
74
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { INIT_APP_PIECES
|
|
1
|
+
import { addApp } from "../commands/addApp.mjs";
|
|
2
|
+
import { INIT_APP_PIECES } from "../commands/pieces.mjs";
|
|
3
3
|
import { adoptApp } from "../commands/adoptApp.mjs";
|
|
4
|
+
import { generateDoctypeTypes, typesDrift } from "../commands/doctypeTypes.mjs";
|
|
4
5
|
import { discoverE2EApp, runE2ESession } from "../commands/e2e.mjs";
|
|
5
|
-
import {
|
|
6
|
+
import { SINGLETON_APPS, init } from "../commands/init.mjs";
|
|
6
7
|
import { updateApp } from "../commands/updateApp.mjs";
|
|
7
8
|
import { createRequire } from "node:module";
|
|
8
|
-
import fs from "node:fs/promises";
|
|
9
|
-
import path from "node:path";
|
|
10
9
|
import fsSync from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
|
+
import fs from "node:fs/promises";
|
|
12
13
|
import { formatServiceList, lookupProjectRoot, waitForFrappe, waitForKeycloak, waitForPostgres, waitServices } from "@multiplatform.one/utils/dev";
|
|
13
14
|
import { Command } from "commander";
|
|
14
15
|
import spawn from "nano-spawn";
|
|
@@ -472,88 +473,47 @@ async function getFrappeApps(root) {
|
|
|
472
473
|
const program = new Command();
|
|
473
474
|
program.name("mpo");
|
|
474
475
|
program.version(JSON.parse(fsSync.readFileSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json"), "utf8"))?.version);
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if (options.apps || options.services) throw new Error("--apps/--services select monorepo pieces and require --monorepo (legacy clone mode)");
|
|
487
|
-
const webOnly = Boolean(options.web || options.app);
|
|
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");
|
|
490
|
-
const pieces = INIT_APP_PIECES.filter((piece) => Boolean(options[piece]));
|
|
491
|
-
await initApp(name, {
|
|
476
|
+
const collect = (value, previous) => [...previous, value];
|
|
477
|
+
program.command("init").argument("[name]", "the project (and product app) name; with --into, the directory").option("--app <name>", "one more product app derived from apps/one (repeatable)", collect, []).option("--service <name>", `singleton app to keep (repeatable): ${SINGLETON_APPS.join(", ")}`, collect, []).option("--frappe", "same as --service frappe").option("--keycloak", "same as --service keycloak").option("--into", "scaffold into an existing directory (the argument, default .); paths already there are kept and pinned in .updateignore").option("--source <path|url>", "monorepo to scaffold from (default: the commit this CLI was built from)").option("-c, --checkout <ref>", "commit, tag or branch at the source").option("--mpo-version <range>", "semver range for @multiplatform.one/* (default: ^<cli version at the source>)").option("--skip-install", "skip pnpm install after scaffolding").option("--skip-git", "skip git init + scaffold commit").option("-y, --yes", "non-interactive: no prompts, no services unless passed").option("--verbose", "show what the scaffold removes and skips").description("scaffold a project: the multiplatform.one monorepo at one commit, pruned (no public/, only the services asked for, apps/one renamed to the product app, @multiplatform.one/* at semver ranges)").action(async (name, options) => {
|
|
478
|
+
const services = [...options.service];
|
|
479
|
+
if (options.frappe) services.push("frappe");
|
|
480
|
+
if (options.keycloak) services.push("keycloak");
|
|
481
|
+
await init(name, {
|
|
482
|
+
apps: options.app,
|
|
483
|
+
services: services.length || options.yes || !process.stdout.isTTY ? services : void 0,
|
|
484
|
+
source: options.source,
|
|
485
|
+
checkout: options.checkout,
|
|
486
|
+
mpoVersion: options.mpoVersion,
|
|
492
487
|
skipInstall: Boolean(options.skipInstall),
|
|
493
488
|
skipGit: Boolean(options.skipGit),
|
|
494
|
-
version: options.mpoVersion,
|
|
495
|
-
template: options.piecesOnly ? "none" : webOnly ? "app" : options.universal ? "universal" : void 0,
|
|
496
489
|
yes: Boolean(options.yes),
|
|
497
|
-
|
|
490
|
+
into: Boolean(options.into),
|
|
491
|
+
verbose: Boolean(options.verbose)
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
program.command("add-app").argument("<name>", "the new product app (apps/<name> + features/<name>)").option("--source <path|url>", "monorepo holding the recorded source commit (default: the recorded url)").option("--skip-install", "skip pnpm install after the merge").option("--verbose", "show the scaffold's progress").description("add another product app from apps/one at the project's recorded source commit; the result is left uncommitted like mpo update").action(async (name, options) => {
|
|
495
|
+
await addApp(name, {
|
|
496
|
+
source: options.source,
|
|
497
|
+
skipInstall: Boolean(options.skipInstall),
|
|
498
|
+
verbose: Boolean(options.verbose)
|
|
498
499
|
});
|
|
499
500
|
});
|
|
500
|
-
program.command("adopt").option("--frappe", "the project adopted the Frappe
|
|
501
|
-
const pieces = INIT_APP_PIECES.filter((piece) => Boolean(options[piece]));
|
|
501
|
+
program.command("adopt").option("--frappe", "the project adopted the Frappe client piece (env keys, provider)").option("--frappe-bench", "the project adopted the Frappe dev bench piece (docker compose)").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 that hand-copied piece files under `mpo update` management: writes .mpo.json provenance (and seeds .updateignore) without changing any project file — the FIRST `mpo update` afterwards reconciles piece files (real diffs expected). For the whole monorepo scaffold use `mpo init --into`").action(async (options) => {
|
|
502
|
+
const pieces = INIT_APP_PIECES.filter((piece) => Boolean(options[piece.replace(/-(\w)/gu, (_, letter) => letter.toUpperCase())]));
|
|
502
503
|
await adoptApp({
|
|
503
504
|
pieces: pieces.length ? pieces : void 0,
|
|
504
505
|
name: options.name,
|
|
505
506
|
yes: Boolean(options.yes)
|
|
506
507
|
});
|
|
507
508
|
});
|
|
508
|
-
program.command("update").option("
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
version: options.mpoVersion,
|
|
516
|
-
assumeVersion: options.assumeVersion,
|
|
517
|
-
assumeTemplate: options.template
|
|
518
|
-
});
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
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> [...]");
|
|
522
|
-
if (await spawn("git", [
|
|
523
|
-
"diff",
|
|
524
|
-
"--cached",
|
|
525
|
-
"--quiet"
|
|
526
|
-
]).then(() => false, () => true)) throw new Error("mpo cannot be updated with uncommitted changes (staged changes present)");
|
|
527
|
-
if (await spawn("git", ["diff", "--quiet"]).then(() => false, () => true)) throw new Error("multiplatform.one cannot be updated with uncommitted changes (unstaged changes present)");
|
|
528
|
-
const rootPkgPath = path.resolve(projectRoot, "package.json");
|
|
529
|
-
const featuresPkgPath = path.resolve(projectRoot, "features/package.json");
|
|
530
|
-
try {
|
|
531
|
-
if (!(await fs.stat(rootPkgPath)).isFile()) throw new Error("mpo update requires a valid project: root package.json not found");
|
|
532
|
-
} catch (err) {
|
|
533
|
-
if (err instanceof Error && err.message.includes("valid project")) throw err;
|
|
534
|
-
throw new Error("mpo update requires a valid project: root package.json not found");
|
|
535
|
-
}
|
|
536
|
-
try {
|
|
537
|
-
if (!(await fs.stat(featuresPkgPath)).isFile()) throw new Error("mpo update requires a valid project: features/package.json not found");
|
|
538
|
-
} catch (err) {
|
|
539
|
-
if (err instanceof Error && err.message.includes("valid project")) throw err;
|
|
540
|
-
throw new Error("mpo update requires a valid project: features/package.json not found");
|
|
541
|
-
}
|
|
542
|
-
const featuresPkg = JSON.parse(await fs.readFile(featuresPkgPath, "utf8"));
|
|
543
|
-
if (!featuresPkg?.dependencies?.["multiplatform.one"] || !String(featuresPkg.dependencies["multiplatform.one"]).length) throw new Error("mpo update requires a valid project: features must depend on multiplatform.one");
|
|
544
|
-
await spawn("sh", [
|
|
545
|
-
path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../scripts/update.sh"),
|
|
546
|
-
options.remote,
|
|
547
|
-
options.checkout
|
|
548
|
-
], {
|
|
549
|
-
cwd: projectRoot,
|
|
550
|
-
stdio: "inherit",
|
|
551
|
-
shell: true
|
|
509
|
+
program.command("update").option("--source <path|url>", "monorepo to update from (default: the commit this CLI was built from)").option("-c, --checkout <ref>", "commit, tag or branch at the source").option("--skip-install", "skip pnpm install after the merge").option("--mpo-version <range>", "semver range for @multiplatform.one/* (default: ^<cli version at the source>)").option("--verbose", "show the scaffold's progress").description("three-way merge the monorepo's changes into this project (base = the scaffold at the recorded source commit, ours = HEAD, theirs = the scaffold at the current commit); lands in the index and worktree, never commits; refuses a dirty tree").action(async (options) => {
|
|
510
|
+
await updateApp({
|
|
511
|
+
source: options.source,
|
|
512
|
+
checkout: options.checkout,
|
|
513
|
+
skipInstall: Boolean(options.skipInstall),
|
|
514
|
+
version: options.mpoVersion,
|
|
515
|
+
verbose: Boolean(options.verbose)
|
|
552
516
|
});
|
|
553
|
-
runModifyStep(projectRoot, await getPresentServices(projectRoot), await getPresentApps(projectRoot));
|
|
554
|
-
try {
|
|
555
|
-
await generateVscodeConfig(projectRoot);
|
|
556
|
-
} catch {}
|
|
557
517
|
});
|
|
558
518
|
program.command("wait").description("wait for a service to be ready").option("-i, --interval <interval>", "interval to wait for", "1000").option("-t, --timeout <timeout>", "timeout to wait for", "600000").option("-e, --dotenv <dotenv>", "dotenv file path", ".env").argument("<services>", `the services to wait for (${waitServices.join(", ")})`).action(async (servicesString, options) => {
|
|
559
519
|
import_main.default.config({ path: options.dotenv || defaultDotenvPath });
|
|
@@ -612,6 +572,36 @@ frappe.command("bench").description("run bench command").action(async () => {
|
|
|
612
572
|
cwd: projectRoot
|
|
613
573
|
});
|
|
614
574
|
});
|
|
575
|
+
frappe.command("types").description("generate TypeScript types for doctypes from getdoctype (auth: $FRAPPE_API_KEY, $FRAPPE_API_SECRET)").argument("<doctypes...>", "doctype names; child tables come along").option("-o, --out <file>", "write the module here instead of stdout").option("--check", "exit 1 and print the diff when --out differs from the server").option("--url <url>", "frappe base URL (default $FRAPPE_URL or http://localhost:8000)").action(async (doctypes, options) => {
|
|
576
|
+
const parentOptions = frappe.opts();
|
|
577
|
+
import_main.default.config({
|
|
578
|
+
path: parentOptions.dotenv || defaultDotenvPath,
|
|
579
|
+
quiet: true
|
|
580
|
+
});
|
|
581
|
+
if (options.check && !options.out) {
|
|
582
|
+
console.error("--check needs --out <file>");
|
|
583
|
+
process.exit(2);
|
|
584
|
+
}
|
|
585
|
+
const generated = await generateDoctypeTypes(doctypes, {
|
|
586
|
+
baseURL: options.url || process.env.FRAPPE_URL || "http://localhost:8000",
|
|
587
|
+
apiKey: process.env.FRAPPE_API_KEY,
|
|
588
|
+
apiSecret: process.env.FRAPPE_API_SECRET
|
|
589
|
+
});
|
|
590
|
+
if (!options.out) {
|
|
591
|
+
process.stdout.write(generated);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const out = path.resolve(process.cwd(), options.out);
|
|
595
|
+
if (options.check) {
|
|
596
|
+
const drift = typesDrift(fsSync.existsSync(out) ? await fs.readFile(out, "utf8") : "", generated);
|
|
597
|
+
if (drift.length > 0) {
|
|
598
|
+
console.error(`${options.out} drifted from the server:\n${drift.join("\n")}`);
|
|
599
|
+
process.exit(1);
|
|
600
|
+
}
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
await fs.writeFile(out, generated);
|
|
604
|
+
});
|
|
615
605
|
program.command("build").argument("[args...]", `build to run: ${[...availableServices, "packages"].join(", ")} (default: all)`).description("run build command").action(async (args) => {
|
|
616
606
|
const parsed = args.flatMap((arg) => arg.split(","));
|
|
617
607
|
const packages = parsed.includes("packages");
|