@multiplatform.one/cli 7.11.0 → 7.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +121 -111
  2. package/bin/createMultiplatformApp.mjs +39 -37
  3. package/lib/bin/multiplatformOne.mjs +65 -75
  4. package/lib/commands/addApp.mjs +78 -0
  5. package/lib/commands/adoptApp.mjs +11 -7
  6. package/lib/commands/doctypeTypes.mjs +148 -0
  7. package/lib/commands/e2e.mjs +2 -2
  8. package/lib/commands/init.mjs +142 -459
  9. package/lib/commands/merge.mjs +359 -0
  10. package/lib/commands/pieces.mjs +236 -0
  11. package/lib/commands/provenance.mjs +108 -0
  12. package/lib/commands/scaffold.mjs +105 -0
  13. package/lib/commands/updateApp.mjs +211 -528
  14. package/lib/source.json +5 -0
  15. package/lib/source.mjs +63 -0
  16. package/package.json +6 -6
  17. package/scripts/frappe-bootstrap.sh +29 -3
  18. package/scripts/frappe-dev.sh +1 -2
  19. package/scripts/frappe-helpers.sh +50 -2
  20. package/scripts/frappe.sh +5 -0
  21. package/scripts/scaffold.sh +937 -0
  22. package/scripts/stamp-source.mjs +51 -0
  23. package/scripts/stamp-source.spec.ts +86 -0
  24. package/src/bin/multiplatformOne.ts +116 -226
  25. package/src/commands/addApp.ts +102 -0
  26. package/src/commands/adoptApp.spec.ts +5 -1
  27. package/src/commands/adoptApp.ts +15 -10
  28. package/src/commands/doctypeTypes.spec.ts +155 -0
  29. package/src/commands/doctypeTypes.ts +214 -0
  30. package/src/commands/init.spec.ts +66 -0
  31. package/src/commands/init.ts +201 -643
  32. package/src/commands/merge.spec.ts +81 -0
  33. package/src/commands/merge.ts +411 -0
  34. package/src/commands/pieces.spec.ts +62 -0
  35. package/src/commands/pieces.ts +340 -0
  36. package/src/commands/provenance.spec.ts +92 -0
  37. package/src/commands/provenance.ts +169 -0
  38. package/src/commands/scaffold.ts +131 -0
  39. package/src/commands/updateApp.spec.ts +540 -331
  40. package/src/commands/updateApp.ts +255 -668
  41. package/src/source.spec.ts +45 -0
  42. package/src/source.ts +88 -0
  43. package/templates/AGENTS.md +161 -0
  44. package/templates/README.md +62 -0
  45. package/templates/pieces/frappe/universal/README.md.partial +7 -32
  46. package/templates/pieces/frappe/universal/env.example.partial +3 -3
  47. package/templates/pieces/frappe-bench/universal/README.md.partial +28 -0
  48. package/templates/pieces/{frappe → frappe-bench}/universal/docker/compose.frappe.yaml +2 -2
  49. package/templates/pieces/tauri/universal/README.md.partial +7 -5
  50. package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/src/dev.rs +463 -0
  51. package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/src/lib.rs +6 -0
  52. package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/tauri.conf.json +2 -1
  53. package/templates/pieces/webext/universal/apps/__NAME__/package.json.partial +1 -0
  54. package/templates/pieces/webext/universal/apps/__NAME__/vite.config.webext.ts +6 -2
  55. package/types/bin/multiplatformOne.d.ts.map +1 -1
  56. package/types/commands/addApp.d.ts +20 -0
  57. package/types/commands/addApp.d.ts.map +1 -0
  58. package/types/commands/adoptApp.d.ts +4 -3
  59. package/types/commands/adoptApp.d.ts.map +1 -1
  60. package/types/commands/doctypeTypes.d.ts +40 -0
  61. package/types/commands/doctypeTypes.d.ts.map +1 -0
  62. package/types/commands/init.d.ts +30 -13
  63. package/types/commands/init.d.ts.map +1 -1
  64. package/types/commands/merge.d.ts +54 -0
  65. package/types/commands/merge.d.ts.map +1 -0
  66. package/types/commands/pieces.d.ts +54 -0
  67. package/types/commands/pieces.d.ts.map +1 -0
  68. package/types/commands/provenance.d.ts +52 -0
  69. package/types/commands/provenance.d.ts.map +1 -0
  70. package/types/commands/scaffold.d.ts +43 -0
  71. package/types/commands/scaffold.d.ts.map +1 -0
  72. package/types/commands/updateApp.d.ts +19 -27
  73. package/types/commands/updateApp.d.ts.map +1 -1
  74. package/types/source.d.ts +35 -0
  75. package/types/source.d.ts.map +1 -0
  76. package/lib/commands/initApp.mjs +0 -431
  77. package/lib/generateVscode.mjs +0 -184
  78. package/scripts/clone.sh +0 -17
  79. package/scripts/update.sh +0 -46
  80. package/src/commands/initApp.spec.ts +0 -720
  81. package/src/commands/initApp.ts +0 -706
  82. package/src/generateVscode.ts +0 -238
  83. package/templates/app/AGENTS.md +0 -142
  84. package/templates/app/README.md +0 -62
  85. package/templates/app/apps/__NAME__/index.html +0 -36
  86. package/templates/app/apps/__NAME__/package.json +0 -31
  87. package/templates/app/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
  88. package/templates/app/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
  89. package/templates/app/apps/__NAME__/src/App.tsx +0 -9
  90. package/templates/app/apps/__NAME__/src/main.tsx +0 -14
  91. package/templates/app/apps/__NAME__/tsconfig.json +0 -8
  92. package/templates/app/apps/__NAME__/vite.config.ts +0 -17
  93. package/templates/app/features/__NAME__/package.json +0 -34
  94. package/templates/app/features/__NAME__/src/HomeScreen.tsx +0 -70
  95. package/templates/app/features/__NAME__/src/index.ts +0 -1
  96. package/templates/app/features/__NAME__/tsconfig.json +0 -5
  97. package/templates/app/gitignore +0 -9
  98. package/templates/app/npmrc +0 -2
  99. package/templates/app/oxlintrc.json +0 -39
  100. package/templates/app/package.json +0 -38
  101. package/templates/app/packages/config/config.json +0 -4
  102. package/templates/app/packages/config/index.ts +0 -5
  103. package/templates/app/packages/config/package.json +0 -17
  104. package/templates/app/packages/config/tsconfig.json +0 -5
  105. package/templates/app/pnpm-workspace.yaml +0 -4
  106. package/templates/app/tsconfig.base.json +0 -16
  107. package/templates/universal/AGENTS.md +0 -142
  108. package/templates/universal/README.md +0 -97
  109. package/templates/universal/apps/__NAME__/app.config.js +0 -31
  110. package/templates/universal/apps/__NAME__/config/tamagui.config.ts +0 -4
  111. package/templates/universal/apps/__NAME__/package.json +0 -62
  112. package/templates/universal/apps/__NAME__/polyfills.native.ts +0 -23
  113. package/templates/universal/apps/__NAME__/polyfills.ts +0 -3
  114. package/templates/universal/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
  115. package/templates/universal/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
  116. package/templates/universal/apps/__NAME__/routes/_layout.tsx +0 -22
  117. package/templates/universal/apps/__NAME__/routes/_layout.web.tsx +0 -21
  118. package/templates/universal/apps/__NAME__/routes/about.tsx +0 -3
  119. package/templates/universal/apps/__NAME__/routes/index.tsx +0 -3
  120. package/templates/universal/apps/__NAME__/src/app.ts +0 -10
  121. package/templates/universal/apps/__NAME__/tamagui.css +0 -3
  122. package/templates/universal/apps/__NAME__/tsconfig.json +0 -8
  123. package/templates/universal/apps/__NAME__/types.d.ts +0 -9
  124. package/templates/universal/apps/__NAME__/vite.config.ts +0 -20
  125. package/templates/universal/features/__NAME__/package.json +0 -35
  126. package/templates/universal/features/__NAME__/src/AboutScreen.tsx +0 -27
  127. package/templates/universal/features/__NAME__/src/AppLayout.tsx +0 -50
  128. package/templates/universal/features/__NAME__/src/ContactForm.spec.tsx +0 -22
  129. package/templates/universal/features/__NAME__/src/ContactForm.tsx +0 -55
  130. package/templates/universal/features/__NAME__/src/HomeScreen.tsx +0 -40
  131. package/templates/universal/features/__NAME__/src/index.ts +0 -4
  132. package/templates/universal/features/__NAME__/src/screens.spec.ts +0 -12
  133. package/templates/universal/features/__NAME__/tsconfig.json +0 -5
  134. package/templates/universal/features/__NAME__/vitest.config.mjs +0 -7
  135. package/templates/universal/gitignore +0 -14
  136. package/templates/universal/npmrc +0 -3
  137. package/templates/universal/oxlintrc.json +0 -39
  138. package/templates/universal/package.json +0 -51
  139. package/templates/universal/packages/config/config.json +0 -4
  140. package/templates/universal/packages/config/index.ts +0 -5
  141. package/templates/universal/packages/config/package.json +0 -17
  142. package/templates/universal/packages/config/tsconfig.json +0 -5
  143. package/templates/universal/packages/i18n/en/common.json +0 -30
  144. package/templates/universal/packages/i18n/es/common.json +0 -30
  145. package/templates/universal/packages/i18n/i18n.d.ts +0 -16
  146. package/templates/universal/packages/i18n/index.ts +0 -26
  147. package/templates/universal/packages/i18n/package.json +0 -20
  148. package/templates/universal/packages/i18n/resources.ts +0 -13
  149. package/templates/universal/packages/i18n/tsconfig.json +0 -5
  150. package/templates/universal/packages/themes/accent.ts +0 -32
  151. package/templates/universal/packages/themes/base.ts +0 -50
  152. package/templates/universal/packages/themes/fonts.ts +0 -8
  153. package/templates/universal/packages/themes/index.ts +0 -33
  154. package/templates/universal/packages/themes/package.json +0 -23
  155. package/templates/universal/packages/themes/tamagui.config.ts +0 -14
  156. package/templates/universal/packages/themes/tsconfig.json +0 -5
  157. package/templates/universal/pnpm-workspace.yaml +0 -4
  158. package/templates/universal/tsconfig.base.json +0 -21
  159. package/templates/universal/types/env.d.ts +0 -9
  160. package/types/commands/initApp.d.ts +0 -114
  161. package/types/commands/initApp.d.ts.map +0 -1
  162. package/types/generateVscode.d.ts +0 -7
  163. 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 new multiplatform projects
4
- (`mpo init` / `create-multiplatform-app`), manage the monorepo dev
5
- environment, and drive Frappe + E2E workflows.
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
- (scaffolder only).
16
+ (`mpo init` only).
17
17
 
18
- ## Scaffold a project (recommended)
18
+ ## Scaffold a project
19
19
 
20
20
  ```bash
21
- mpo init my-app # universal: web (SSR) + iOS + Android via one
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 install
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
- `mpo init` creates a **consumer** project — not a monorepo fork. It prompts for
29
- anything omitted (name; platforms, defaulting to universal); pass `--yes` for
30
- CI. `@multiplatform.one/*` dependencies come from npm at semver ranges, never
31
- `workspace:*`.
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
- Universal layout: `apps/<name>` (routes + providers + vite/expo config),
34
- `features/<name>` (starter Home/About screens, forms demo, vitest),
35
- `packages/config`, `packages/i18n` (en/es), `packages/themes` (Tamagui theme).
36
-
37
- Want web-only? `mpo init --web my-app` scaffolds the Vite + React variant
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
- | Flag | Adds | Needs |
51
- | ------------ | ---------------------------------------------------------------- | --------------------- |
52
- | `--frappe` | Frappe backend wiring — env keys, provider, dev bench compose | Docker |
53
- | `--keycloak` | Keycloak auth — env keys, provider, compose + realm import | Docker |
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
- `--gnome` is the desktop piece. It renders your Tamagui components as native
59
- GTK widgets laid out by Yoga — not the web build in a frame — so the app looks
60
- and behaves like part of the desktop. Linux-first.
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 adopt --webext --name my-app # requires a clean git tree; ≥ 1 piece
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
- Adoption writes `.mpo.json` (template `"none"` + pieces) and seeds
86
- `.updateignore`; **it changes no project files**. The FIRST `mpo update`
87
- afterwards reconciles the piece files from an empty merge base, so diffs and
88
- conflicts on hand-copied files are expected — resolve and commit. Later
89
- updates are ordinary three-way merges (baselines come from
90
- `mpo init --pieces-only` at the recorded version).
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
- ### Bootstrap missing provenance
105
+ **Idempotency:** update, commit, update again: zero changes.
93
106
 
94
- Scaffolded before `.mpo.json` existed (CLI ≤ 6.3.0)? Assert the version it
95
- was scaffolded with instead of hand-writing the file:
107
+ ## Add a product app
96
108
 
97
109
  ```bash
98
- mpo update --assume-version 6.3.0 [--template universal|app]
110
+ mpo add-app kiosk
99
111
  ```
100
112
 
101
- ## Update a project
102
-
103
- `mpo update` upgrades a project to the current templates with a three-way
104
- merge driven by `.mpo.json` (written at init time): base = the original
105
- scaffold regenerated at the recorded CLI version, ours = your HEAD, theirs =
106
- a fresh scaffold from the current CLI. `.updateignore` entries (exact files or directory prefixes — not globs) (and
107
- `pnpm-lock.yaml`) stay pinned to your version; `package.json` dependency-range
108
- drift auto-resolves to the newer minimum.
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
- **Idempotency contract:** re-running `mpo update` immediately after a
111
- successful update makes zero changes (clean `git status`). Run it, see no
112
- changes, know you're current.
116
+ ## Adopt hand-copied pieces
113
117
 
114
- ### Adopt an existing project
115
-
116
- A project that was never scaffolded — it hand-copied a delivery target
117
- (webext, tauri, …) and uses `@multiplatform.one/*` from npm — has no
118
- provenance. Bring it under `mpo update` management:
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 # requires a clean git tree; ≥ 1 piece
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`; **it changes no project files**. The FIRST `mpo update`
126
- afterwards reconciles the piece files from an empty merge base, so diffs and
127
- conflicts on hand-copied files are expected — resolve and commit. Later
128
- updates are ordinary three-way merges (baselines come from
129
- `mpo init --pieces-only` at the recorded version).
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
- ### Bootstrap missing provenance
138
+ ## The scaffold script
132
139
 
133
- Scaffolded before `.mpo.json` existed (CLI ≤ 6.3.0)? Assert the version it
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
- mpo update --assume-version 6.3.0 [--template universal|app]
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 | Purpose |
143
- | ---------------------------- | -------------------------------------------------------------------- |
144
- | `mpo init --monorepo [name]` | DEPRECATED: clone/modify the full monorepo (framework dev forks) |
145
- | `mpo adopt` | Write provenance into an existing, never-scaffolded project |
146
- | `mpo update` | Three-way template update via `.mpo.json` (legacy merge for forks) |
147
- | `mpo wait` | Wait for a service (frappe, postgres, mariadb, keycloak) to be ready |
148
- | `mpo frappe bootstrap` | Bootstrap the Frappe development environment |
149
- | `mpo frappe dev` | Start the Frappe development server |
150
- | `mpo frappe bench -- <args>` | Run a bench command |
151
- | `mpo frappe clean` | Clean Frappe bench artifacts |
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
- Templates live in `templates/universal/` and `templates/app/`. Generator unit
158
- specs: `pnpm --filter @multiplatform.one/cli test`
159
- (`src/commands/initApp.spec.ts` — tree + package.json shape for both templates).
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
- - The project templates never ship `workspace:*` dependencies on
164
- `@multiplatform.one/*` or copies of the monorepo's `public/` packages —
165
- consumers upgrade via npm (`pnpm update "@multiplatform.one/*"`)
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 — npm create / npx entry for consumer scaffolds.
5
- * Delegates to the same generator as `mpo init`.
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 --universal
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 flagValue(argv, name) {
24
- const idx = argv.indexOf(name);
25
- if (idx < 0) return undefined;
26
- return argv[idx + 1];
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] [--universal] [--skip-install] [--mpo-version <range>] [--yes]
33
-
34
- Scaffold a consumer project with @multiplatform.one/* at semver ranges
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
- (default) apps/<name> + features/<name> + packages/config (web-only)
38
- --universal one-based web + iOS + Android app
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 skipInstall = argv.includes("--skip-install");
51
- const yes = argv.includes("--yes") || argv.includes("-y");
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
- ({ initApp } = await import(pathToFileURL(libPath).href));
55
+ ({ init } = await import(pathToFileURL(libPath).href));
60
56
  } catch {
61
57
  throw new Error(
62
- "Could not load initApp. Build @multiplatform.one/cli first (pnpm --filter @multiplatform.one/cli build).",
58
+ "Could not load init. Build @multiplatform.one/cli first (pnpm --filter @multiplatform.one/cli build).",
63
59
  );
64
60
  }
65
-
66
- await initApp(name, {
67
- skipInstall,
68
- version,
69
- template: universal ? "universal" : "app",
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 { generateVscodeConfig } from "../generateVscode.mjs";
2
- import { INIT_APP_PIECES, initApp, readProvenance } from "../commands/initApp.mjs";
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 { init, runModifyStep } from "../commands/init.mjs";
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
- program.command("init").option("--universal", "scaffold the universal template (web + iOS + Android; default)").option("--web", "scaffold the web-only template (Vite + React)").option("--app", "alias of --web (back-compat)").option("--frappe", "add the Frappe backend piece (env keys, provider, docker compose bench)").option("--keycloak", "add the Keycloak auth piece (env keys, provider, docker compose + realm)").option("--gnome", "add the GNOME desktop target piece (GTK4/GJS via react-gnome — native widgets, no webview)").option("--tauri", "add the Tauri desktop target piece (src-tauri webview shell — Rust; combines with --gnome)").option("--vscode", "add the VS Code extension target piece").option("--webext", "add the browser extension target piece (MV3 popup + background)").option("--pieces-only", "scaffold ONLY the selected piece fragments onto an empty tree (no base template) — used by `mpo update` to rebuild adopted-project merge baselines").option("--skip-install", "skip pnpm install after scaffolding").option("--skip-git", "skip git init + scaffold commit").option("--mpo-version <range>", "semver range for @multiplatform.one/* (default: ^<cli version>)").option("-y, --yes", "non-interactive: accept defaults instead of prompting").option("--monorepo", "DEPRECATED: clone/modify the full multiplatform.one monorepo instead of scaffolding a consumer project").option("-c, --checkout <branch>", "branch, tag or commit to checkout (--monorepo only)", "main").option("-a, --apps <apps>", "comma-separated apps to include (--monorepo only)").option("-s, --services <services>", "comma-separated services to include (--monorepo only)").argument("[name]", "the name of the project").description("scaffold a new multiplatform project (universal web + native by default; --web for web-only; semver @multiplatform.one/* deps)").action(async (name, options) => {
476
- if (options.monorepo) {
477
- console.warn("⚠️ `mpo init --monorepo` clones the whole multiplatform.one monorepo (legacy). New projects should use the default consumer scaffold instead.");
478
- if (await spawn("git", ["rev-parse", "--is-inside-work-tree"]).then(() => true, () => false)) throw new Error("mpo cannot be initialized inside a git repository");
479
- const cloneScript = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../scripts/clone.sh");
480
- await init(name, {
481
- ...options,
482
- cloneScript
483
- });
484
- return;
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
- pieces: pieces.length ? pieces : void 0
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 backend piece").option("--keycloak", "the project adopted the Keycloak auth piece").option("--gnome", "the project adopted the GNOME desktop target piece").option("--tauri", "the project adopted the Tauri desktop target piece").option("--vscode", "the project adopted the VS Code extension target piece").option("--webext", "the project adopted the browser extension target piece").option("--name <name>", "project name recorded in provenance (default: package.json name, scope stripped) — it renders piece paths (apps/<name>/…), so match your app directory").option("-y, --yes", "non-interactive: never prompt (pieces must come from flags)").description("bring an existing, never-scaffolded project under `mpo update` management: writes .mpo.json provenance (and seeds .updateignore) without changing any project file — the FIRST `mpo update` afterwards reconciles piece files (real diffs expected)").action(async (options) => {
501
- const pieces = INIT_APP_PIECES.filter((piece) => Boolean(options[piece]));
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("-c, --checkout <branch>", "branch, tag or commit to merge from upstream (monorepo forks)", "main").option("-r, --remote <url>", "upstream remote URL (monorepo forks)", "https://gitlab.com/bitspur/frappe/multiplatform.one.git").option("--skip-install", "skip pnpm install after the update").option("--mpo-version <range>", "semver range for @multiplatform.one/* (default: ^<cli version>)").option("--assume-version <cliVersion>", "bootstrap missing .mpo.json: assume the project was scaffolded by this exact CLI version, record provenance, then update").option("--template <template>", "template recorded by --assume-version (universal|app, default universal)").description("update a scaffolded or adopted project to the current template (three-way merge via .mpo.json provenance); monorepo forks fall back to the upstream merge flow").action(async (options) => {
509
- if (await spawn("git", ["rev-parse", "--is-inside-work-tree"]).then(() => false, () => true)) throw new Error("mpo cannot be updated outside of a git repository");
510
- if (options.template && !options.assumeVersion) throw new Error("--template only applies to the --assume-version bootstrap");
511
- if (options.template && options.template !== "universal" && options.template !== "app") throw new Error(`--template must be universal or app, got "${options.template}"`);
512
- if (readProvenance(projectRoot) || options.assumeVersion) {
513
- await updateApp({
514
- skipInstall: options.skipInstall,
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");