makersclaw 0.5.2 → 0.7.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 (148) hide show
  1. package/README.md +73 -7
  2. package/package.json +3 -2
  3. package/r/annotate.json +4 -4
  4. package/r/approval-request.json +3 -3
  5. package/r/audio-player.json +3 -3
  6. package/r/auth-form.json +2 -2
  7. package/r/board.json +4 -4
  8. package/r/calendar-view.json +4 -4
  9. package/r/cart.json +3 -3
  10. package/r/chart.json +3 -3
  11. package/r/chat.json +3 -3
  12. package/r/code-editor.json +4 -4
  13. package/r/comment-box.json +4 -4
  14. package/r/contact-section.json +3 -3
  15. package/r/core.json +3 -3
  16. package/r/csv-import.json +4 -4
  17. package/r/data-table.json +3 -3
  18. package/r/diagram.json +3 -3
  19. package/r/doc-editor.json +4 -4
  20. package/r/doc-page.json +4 -4
  21. package/r/email-render.json +2 -2
  22. package/r/faq.json +2 -2
  23. package/r/file-upload.json +3 -3
  24. package/r/flow-canvas.json +4 -4
  25. package/r/footer.json +3 -3
  26. package/r/gantt.json +4 -4
  27. package/r/job-panel.json +3 -3
  28. package/r/json-editor.json +4 -4
  29. package/r/map.json +5 -5
  30. package/r/menubar.json +2 -2
  31. package/r/mind-map.json +4 -4
  32. package/r/navigation-menu.json +2 -2
  33. package/r/order-detail.json +2 -2
  34. package/r/org-chart.json +4 -4
  35. package/r/page-builder.json +6 -6
  36. package/r/payment-methods.json +2 -2
  37. package/r/pdf-document.json +2 -2
  38. package/r/pdf-viewer.json +6 -6
  39. package/r/product-detail.json +3 -3
  40. package/r/product-grid.json +4 -4
  41. package/r/record-form.json +3 -3
  42. package/r/record-notes.json +5 -5
  43. package/r/registry.json +121 -121
  44. package/r/search-dialog.json +3 -3
  45. package/r/settings-list.json +3 -3
  46. package/r/setup-checklist.json +3 -3
  47. package/r/share-dialog.json +3 -3
  48. package/r/sidebar.json +2 -2
  49. package/r/signature-pad.json +3 -3
  50. package/r/sketch-canvas.json +5 -5
  51. package/r/spreadsheet.json +3 -3
  52. package/r/stat-cards.json +4 -4
  53. package/r/task-list.json +4 -4
  54. package/r/task-model.json +3 -3
  55. package/r/testimonials.json +2 -2
  56. package/r/transaction-list.json +3 -3
  57. package/r/whiteboard.json +4 -4
  58. package/registry/blocks/annotate/README.md +14 -5
  59. package/registry/blocks/approval-request/README.md +15 -8
  60. package/registry/blocks/audio-player/README.md +17 -10
  61. package/registry/blocks/board/README.md +13 -9
  62. package/registry/blocks/board/board.jsx +1 -1
  63. package/registry/blocks/calendar-view/README.md +28 -4
  64. package/registry/blocks/cart/README.md +14 -10
  65. package/registry/blocks/chart/README.md +14 -7
  66. package/registry/blocks/chat/README.md +43 -13
  67. package/registry/blocks/code-editor/README.md +4 -0
  68. package/registry/blocks/code-editor/code-editor.jsx +1 -1
  69. package/registry/blocks/comment-box/README.md +20 -13
  70. package/registry/blocks/contact-section/README.md +7 -6
  71. package/registry/blocks/csv-import/README.md +34 -12
  72. package/registry/blocks/data-table/README.md +41 -8
  73. package/registry/blocks/diagram/diagram.jsx +1 -1
  74. package/registry/blocks/doc-editor/README.md +18 -6
  75. package/registry/blocks/doc-editor/doc-editor.jsx +1 -1
  76. package/registry/blocks/doc-page/README.md +16 -6
  77. package/registry/blocks/file-upload/README.md +7 -1
  78. package/registry/blocks/flow-canvas/README.md +16 -5
  79. package/registry/blocks/flow-canvas/flow-canvas.jsx +1 -1
  80. package/registry/blocks/footer/README.md +4 -2
  81. package/registry/blocks/gantt/README.md +18 -12
  82. package/registry/blocks/job-panel/README.md +1 -1
  83. package/registry/blocks/json-editor/README.md +16 -4
  84. package/registry/blocks/map/README.md +19 -3
  85. package/registry/blocks/map/map-view.jsx +1 -1
  86. package/registry/blocks/map/maplibre.js +1 -1
  87. package/registry/blocks/mind-map/README.md +16 -5
  88. package/registry/blocks/org-chart/README.md +10 -7
  89. package/registry/blocks/page-builder/README.md +13 -6
  90. package/registry/blocks/page-builder/page-builder.jsx +1 -1
  91. package/registry/blocks/page-builder/page-view.jsx +1 -1
  92. package/registry/blocks/page-builder/page.js +1 -1
  93. package/registry/blocks/pdf-viewer/README.md +22 -20
  94. package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1 -1
  95. package/registry/blocks/pdf-viewer/pdfjs.js +1 -1
  96. package/registry/blocks/pdf-viewer/ssr-shim.js +1 -1
  97. package/registry/blocks/product-detail/README.md +13 -7
  98. package/registry/blocks/product-detail/demo.jsx +1 -1
  99. package/registry/blocks/product-grid/README.md +12 -10
  100. package/registry/blocks/record-form/README.md +26 -9
  101. package/registry/blocks/record-notes/README.md +42 -30
  102. package/registry/blocks/search-dialog/README.md +38 -7
  103. package/registry/blocks/settings-list/README.md +29 -17
  104. package/registry/blocks/setup-checklist/README.md +12 -14
  105. package/registry/blocks/share-dialog/README.md +1 -1
  106. package/registry/blocks/signature-pad/README.md +15 -4
  107. package/registry/blocks/sketch-canvas/README.md +13 -4
  108. package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1 -1
  109. package/registry/blocks/sketch-canvas/ssr-shim.js +3 -3
  110. package/registry/blocks/spreadsheet/README.md +17 -8
  111. package/registry/blocks/stat-cards/README.md +17 -8
  112. package/registry/blocks/task-list/README.md +14 -11
  113. package/registry/blocks/task-model/README.md +3 -3
  114. package/registry/blocks/transaction-list/README.md +15 -7
  115. package/registry/blocks/whiteboard/README.md +19 -13
  116. package/src/cli.mjs +65 -9
  117. package/src/install.mjs +360 -21
  118. package/src/lockfile.mjs +468 -0
  119. package/src/pm.mjs +473 -0
  120. package/src/registry.mjs +125 -1
  121. package/templates/next/browser/app/auto-refresh.jsx +67 -0
  122. package/templates/next/browser/app/layout.jsx +94 -0
  123. package/templates/next/browser/app/page.jsx +60 -0
  124. package/templates/next/browser/app/s/[key]/page.jsx +95 -0
  125. package/templates/next/browser/lib/actions.js +16 -0
  126. package/templates/next/browser/lib/data.js +43 -0
  127. package/templates/next/files/README.md +818 -82
  128. package/templates/next/files/app/claw-alive/route.js +2 -2
  129. package/templates/next/files/app/frame.js +134 -9
  130. package/templates/next/files/app/layout.jsx +2 -2
  131. package/templates/next/files/check/run.mjs +1491 -53
  132. package/templates/next/files/components/ui/item.jsx +2 -2
  133. package/templates/next/files/components/ui/notice.jsx +1 -1
  134. package/templates/next/files/components/ui/separator.jsx +1 -1
  135. package/templates/next/files/lib/actions.js +171 -52
  136. package/templates/next/files/lib/brain-server.js +96 -0
  137. package/templates/next/files/lib/brain.js +413 -0
  138. package/templates/next/files/lib/claw.js +11 -0
  139. package/templates/next/files/lib/data.js +120 -30
  140. package/templates/next/files/lib/db.js +97 -17
  141. package/templates/next/files/lib/door.js +680 -0
  142. package/templates/next/files/next.config.mjs +85 -9
  143. package/templates/next/files/package.json +3 -3
  144. package/templates/next/files/{middleware.js → proxy.js} +13 -6
  145. package/templates/next/files/surfaces.js +3 -2
  146. package/templates/next/pnpm-lock.yaml +5870 -0
  147. package/templates/next/template.json +13 -6
  148. package/templates/next/files/schema.sql +0 -22
package/src/install.mjs CHANGED
@@ -2,10 +2,21 @@
2
2
  // result and prints nothing: src/cli.mjs says it, as text or as --json.
3
3
  //
4
4
  // Two files record what an app holds:
5
- // makersclaw.json the template and version it was laid from, and each
6
- // block with the version it came from
5
+ // makersclaw.json the template and version it was laid from, each
6
+ // block with the version it came from, where its
7
+ // tables live (`tables`: "own" or "public") and which
8
+ // fixed files it runs on (`mode`: "server" or
9
+ // "browser"). The rest of its keys are the app's
10
+ // (`brain`: the platform's declaration; `moves`: a
11
+ // store package's move map); every command keeps
12
+ // them. `tables` is never "moved": that is the
13
+ // platform's mode, and no command writes it
7
14
  // check/pins.json sha256 of every fixed file, the template's and the
8
15
  // blocks' — `npm run check` refuses one that differs
16
+ // and a third says which package versions it installs:
17
+ // pnpm-lock.yaml cut from the release lock for this package.json
18
+ // (src/lockfile.mjs) by init, add and update; the
19
+ // install writes in what the release does not lock
9
20
  //
10
21
  // Everything this CLI writes is FIXED (ours: pinned, replaced by update),
11
22
  // DOCS (ours, not pinned), a STARTER (laid when missing, the app's from
@@ -15,7 +26,9 @@
15
26
  import { existsSync, readFileSync } from "node:fs";
16
27
  import { join } from "node:path";
17
28
  import { hashFile, readJson, removeFile, sha256, walk, writeFile, writeJson } from "./fsutil.mjs";
18
- import { BLOCK_DIR, loadBlocks, loadItems, loadTemplate, packageInfo, readmeOf, suggest, templateNames } from "./registry.mjs";
29
+ import { LOCKFILE, LockError, deriveLock, parseLock } from "./lockfile.mjs";
30
+ import { NPM_LOCKFILE, PNPM_VERSION, installDeps } from "./pm.mjs";
31
+ import { BASE_MODE, BLOCK_DIR, loadBlocks, loadItems, loadTemplate, modeFiles, packageInfo, readmeOf, suggest, templateNames } from "./registry.mjs";
19
32
 
20
33
  export const MANIFEST = "makersclaw.json";
21
34
  export const PINS = "check/pins.json";
@@ -62,6 +75,7 @@ function readState(dir) {
62
75
 
63
76
  function requireManifest(dir) {
64
77
  const state = readState(dir);
78
+ if (state.manifest?.tables === MOVED) throw movedManifest();
65
79
  if (!state.manifest) {
66
80
  const hasApp = existsSync(join(dir, "package.json"));
67
81
  throw new CliError(
@@ -74,6 +88,142 @@ function requireManifest(dir) {
74
88
  return state;
75
89
  }
76
90
 
91
+ /** Where an app's tables live: "own", its own schema (app_<name>), or
92
+ * "public", the workspace schema every app shared before 0.7.0. */
93
+ const TABLES = ["own", "public"];
94
+
95
+ /**
96
+ * The platform's third mode (claw's MIGRATE-SPEC M17 and §10): a legacy
97
+ * app whose tables the workspace's move put in its own schema, while its
98
+ * code still names public. The platform holds it (the app's blueprint,
99
+ * claw.app_roles); the app's makersclaw.json keeps "public" (or no key)
100
+ * until its rebuild writes "own". So no command writes "moved": `init
101
+ * --tables moved` is refused, and so is every command over a manifest
102
+ * that says it, rather than write it back.
103
+ */
104
+ const MOVED = "moved";
105
+ const MOVED_WHY = "moved is the platform's mode, never a manifest's";
106
+ function movedManifest() {
107
+ return new CliError(
108
+ `${MANIFEST} says tables "moved": ${MOVED_WHY}`,
109
+ `set "tables" to "public" (or take the key out, for an app laid before 0.7.0): a moved app's code still names public, and its rebuild sets "own"`,
110
+ );
111
+ }
112
+
113
+ /**
114
+ * A makersclaw.json a lay wrote. Every lay since 0.2.0 records the
115
+ * `version` it laid, so a manifest without one was written by someone
116
+ * else first: the platform writes its `brain` declaration into a
117
+ * work-first app's new manifest before that app's first `init`
118
+ * (APPS-SPEC 4.2). Such a manifest's keys are kept like any other's, but
119
+ * it is no record of an app from before 0.7.0.
120
+ */
121
+ function writtenByLay(manifest) {
122
+ return Boolean(manifest) && Object.hasOwn(manifest, "version");
123
+ }
124
+
125
+ /**
126
+ * The `tables` init writes. It never flips an app: an app's tables move
127
+ * from public to their own schema only with its workspace (the platform's
128
+ * move), never because a refresh ran `init --force`. So an app's
129
+ * makersclaw.json keeps its value — an absent key in a manifest a lay
130
+ * wrote is an app from before 0.7.0, on public — and a `--tables` that
131
+ * says otherwise is refused. Where no lay wrote a makersclaw.json (there
132
+ * is none, or it holds only the platform's keys) and it names no tables,
133
+ * there is no record to keep: a new app is "own" unless told, and an app
134
+ * laid before makersclaw kept one is "public" unless told (the platform,
135
+ * which holds the truth, always tells).
136
+ */
137
+ function tablesFor(dir, before, asked) {
138
+ if (asked === MOVED) throw new CliError(`--tables moved: ${MOVED_WHY}`, `leave --tables out: a moved app keeps its makersclaw.json's "public" until its rebuild sets "own"`);
139
+ if (before.manifest?.tables === MOVED) throw movedManifest();
140
+ if (asked !== undefined && !TABLES.includes(asked)) {
141
+ throw new CliError(`--tables takes ${TABLES.join(" or ")}, not "${asked}"`, "own: the app's tables in its own schema (every new app) · public: an app from before 0.7.0");
142
+ }
143
+ const named = Boolean(before.manifest) && Object.hasOwn(before.manifest, "tables");
144
+ if (!named && !writtenByLay(before.manifest)) return asked ?? (existsSync(join(dir, "package.json")) ? "public" : "own");
145
+ const had = named ? before.manifest.tables : "public";
146
+ if (!TABLES.includes(had)) {
147
+ throw new CliError(`${MANIFEST} says tables ${JSON.stringify(had)}; an app's tables are ${TABLES.join(" or ")}`, `set "tables" in ${MANIFEST} to the one the platform holds for this app`);
148
+ }
149
+ if (asked !== undefined && asked !== had) {
150
+ throw new CliError(
151
+ (named ? `this app's tables are ${had}, not ${asked}` : `${MANIFEST} names no tables, so this app's are public (laid before 0.7.0), not ${asked}`) +
152
+ (had === "public" ? ": an app's tables move from public to their own schema only with its workspace" : ": an app on its own schema never goes back to public"),
153
+ `leave --tables out, or pass --tables ${had}` + (named ? "" : `; if its tables are already in its own schema, put "tables": "own" back in ${MANIFEST}`),
154
+ );
155
+ }
156
+ return had;
157
+ }
158
+
159
+ /** Which fixed files an app runs on: "server", a Next server in the
160
+ * workspace (files/ as it is; an absent key, every app before 0.7.0), or
161
+ * "browser", code that runs in the member's browser: files the platform
162
+ * serves, reading and writing through its data door (template.json
163
+ * `modes.browser`). */
164
+ const MODES = [BASE_MODE, "browser"];
165
+
166
+ /** The app's mode, from its makersclaw.json. Anything but the two is
167
+ * refused: which fixed files to lay depends on it. */
168
+ function modeOf(manifest) {
169
+ const mode = manifest && Object.hasOwn(manifest, "mode") ? manifest.mode : BASE_MODE;
170
+ if (!MODES.includes(mode)) {
171
+ throw new CliError(`${MANIFEST} says mode ${JSON.stringify(mode)}; an app's mode is ${MODES.join(" or ")}`, `makersclaw mode <${MODES.join("|")}> sets the one the platform holds for this app`);
172
+ }
173
+ return mode;
174
+ }
175
+
176
+ /**
177
+ * The `mode` init writes. Like `tables`, init never changes an app's: a
178
+ * refresh is claw's `init --force` over the app it has, and a flip there
179
+ * would turn a server app into a browser one whose Server Actions then fail
180
+ * its export. So a manifest that names a mode keeps it, whoever wrote it,
181
+ * and so does one a lay wrote (absent: server); a --mode that differs is
182
+ * refused, and `makersclaw mode` is the one switch. A new manifest takes
183
+ * --mode's value, and with none no key: an absent mode is server, the
184
+ * CLI's own default, because outside Makersclaw no door answers a browser
185
+ * app. { mode, key }: the app's mode, and the value to write (undefined:
186
+ * the key stays as it is).
187
+ */
188
+ function modeFor(before, asked) {
189
+ if (asked !== undefined && !MODES.includes(asked)) {
190
+ throw new CliError(`--mode takes ${MODES.join(" or ")}, not "${asked}"`, "server: a Next server in the workspace (the default) · browser: its code runs in the member's browser, and every read and write goes through the platform's data door");
191
+ }
192
+ const named = Boolean(before.manifest) && Object.hasOwn(before.manifest, "mode");
193
+ if (!named && !writtenByLay(before.manifest)) return { mode: asked ?? BASE_MODE, key: asked };
194
+ const had = modeOf(before.manifest);
195
+ if (asked !== undefined && asked !== had) {
196
+ throw new CliError(
197
+ `this app is a ${had} app, not ${asked}: an app changes mode only with makersclaw mode`,
198
+ `leave --mode out, or run makersclaw mode ${asked}: it lays that mode's fixed files and says what the app's own code must change`,
199
+ );
200
+ }
201
+ return { mode: had, key: undefined };
202
+ }
203
+
204
+ /** A browser app's tables are its own: the door serves an app's own schema
205
+ * and nothing else, so a browser app on public would read nothing. `how`
206
+ * says where the pair came from, for the refusal's words. */
207
+ function requireOwnTables(mode, tables, how) {
208
+ if (mode !== "browser" || tables === "own") return;
209
+ const why = "a browser app reads and writes only through the platform's data door, which serves an app's own schema and nothing else";
210
+ if (how === "flags") throw new CliError(`a browser app's tables are own, not ${tables}: ${why}`, "leave --tables out: --mode browser writes tables own");
211
+ if (how === "switch") {
212
+ throw new CliError(
213
+ `${tables === undefined ? `${MANIFEST} names no tables, so this app's are public (laid before 0.7.0)` : `this app's tables are ${tables}`}, not own: ${why}`,
214
+ "an app's tables move to their own schema only with its workspace (the platform's move); until then it stays a server app",
215
+ );
216
+ }
217
+ throw new CliError(`${MANIFEST} says mode browser and tables ${tables}: ${why}`, `if its tables are in its own schema, set "tables": "own"; if not, makersclaw mode server lays it as a server app`);
218
+ }
219
+
220
+ /** The files the template lays in this mode (registry modeFiles). */
221
+ function modeFilesOrFail(tpl, mode) {
222
+ const files = modeFiles(tpl, mode);
223
+ if (!files) throw new CliError(`template ${tpl.name} has no ${mode} mode in makersclaw ${version()}`, `its modes: ${[BASE_MODE, ...Object.keys(tpl.modes ?? {})].join(", ")}`);
224
+ return files;
225
+ }
226
+
77
227
  function loadTemplateOrFail(name) {
78
228
  const tpl = loadTemplate(name);
79
229
  if (!tpl) throw new CliError(`this makersclaw has no template "${name}"`, `templates: ${templateNames().join(", ")}`);
@@ -97,22 +247,84 @@ function mergePackage(app, tpl) {
97
247
  return { out, changes };
98
248
  }
99
249
 
100
- /** Paths a template may remove when a newer version stops shipping them:
101
- * its fixed files and the kit, parts and block folders. Anything else a
102
- * pins.json names is left alone — it is not ours to delete. */
250
+ const releaseLocks = new Map();
251
+ function releaseLock(tpl) {
252
+ if (!releaseLocks.has(tpl.lock)) releaseLocks.set(tpl.lock, parseLock(readFileSync(tpl.lock, "utf8")));
253
+ return releaseLocks.get(tpl.lock);
254
+ }
255
+
256
+ /**
257
+ * The app's pnpm-lock.yaml for `pkg`, cut from the template's release
258
+ * lock: { text, before, covered, carried, left, notes }, or null when the
259
+ * template ships no lock. The app's current lock lends what the release
260
+ * does not lock (src/lockfile.mjs says when); one this reader cannot take
261
+ * lends nothing, and the install resolves those packages again.
262
+ */
263
+ function cutLock(dir, tpl, pkg) {
264
+ if (!tpl.lock || !pkg) return null;
265
+ const path = join(dir, LOCKFILE);
266
+ const before = existsSync(path) ? readFileSync(path, "utf8") : null;
267
+ const notes = [];
268
+ let current = null;
269
+ if (before !== null) {
270
+ try {
271
+ current = parseLock(before);
272
+ } catch (error) {
273
+ if (!(error instanceof LockError)) throw error;
274
+ notes.push(`${LOCKFILE} could not be read (${error.message}) — cut again from the release lock`);
275
+ }
276
+ }
277
+ const cut = deriveLock(releaseLock(tpl), pkg, current);
278
+ // The install resolves what is left; where npm installed the app before,
279
+ // at package-lock.json's versions when they still fit (src/pm.mjs).
280
+ const one = cut.left.length === 1;
281
+ const npmPinned = existsSync(join(dir, NPM_LOCKFILE)) ? `, keeping ${NPM_LOCKFILE}'s version${one ? "" : "s"} where ${one ? "it fits" : "they fit"}` : "";
282
+ if (cut.left.length) notes.push(`${LOCKFILE}: ${cut.left.join(", ")} not in makersclaw ${version()}'s lock — the install resolves ${one ? "it" : "them"}${npmPinned}`);
283
+ return { ...cut, before, notes };
284
+ }
285
+
286
+ /** Write the cut when it differs; say so in `written`. */
287
+ function writeLock(dir, cut, written) {
288
+ if (!cut || cut.text === cut.before) return false;
289
+ writeFile(join(dir, LOCKFILE), cut.text);
290
+ written.push(LOCKFILE);
291
+ return true;
292
+ }
293
+
294
+ const lockSummary = (cut) => (cut ? { covered: cut.covered.length, carried: cut.carried, left: cut.left } : null);
295
+
296
+ /** Paths a template may remove when a newer version stops shipping them,
297
+ * or the app's mode stops laying them: its fixed files (the base's, so
298
+ * the ones a mode drops too), the ones it names as retired (middleware.js
299
+ * since 0.6.0: Next 16 fails a build that has it beside proxy.js), and
300
+ * the kit, parts and block folders. Anything else a pins.json names is
301
+ * left alone — it is not ours to delete. */
103
302
  function ownedPath(path, tpl) {
104
303
  if (/^components\/(?:ui|parts|blocks)\//.test(path) || /^lib\/[^/]+\.js$/.test(path)) return true;
304
+ if ((tpl.retired ?? []).includes(path)) return true;
105
305
  return tpl.files.some((f) => f.class === "fixed" && f.path === path);
106
306
  }
107
307
 
108
308
  /**
109
309
  * Lay a template — and the blocks the app already has — over `dir`: the
110
- * one routine behind init, init --force and update. Fixed files and docs
111
- * are replaced; starters are laid only when missing; package.json is
310
+ * one routine behind init, init --force, update and mode. Fixed files and
311
+ * docs are replaced; starters are laid only when missing; package.json is
112
312
  * merged; blocks come again from this version; what an older version
113
- * pinned and this one no longer ships is removed.
313
+ * pinned and this one no longer ships is removed. makersclaw.json is
314
+ * rewritten over what it held: `template`, `version` and `blocks` are
315
+ * this lay's, `tables` is init's (tablesFor; update leaves it as it was),
316
+ * `mode` is a new manifest's init's or `makersclaw mode`'s (modeFor), and
317
+ * every other key — the platform's `brain` declaration, one a later
318
+ * version adds — survives a refresh and an update. Until 0.7.0 it was
319
+ * rewritten as exactly those three, and a declaration was lost.
320
+ *
321
+ * The files are the app's mode's (modeFiles): a browser app gets the
322
+ * browser overlay's copy of a fixed file where there is one, and none of
323
+ * the files browser mode drops. The pins follow: a file the mode does not
324
+ * lay is not pinned, and one that was pinned is removed as a retired one
325
+ * is, so a switch of mode leaves only the new mode's fixed files.
114
326
  */
115
- function lay(dir, tpl, before) {
327
+ function lay(dir, tpl, before, { tables, mode } = {}) {
116
328
  const v = version();
117
329
  const written = [];
118
330
  const kept = [];
@@ -122,7 +334,13 @@ function lay(dir, tpl, before) {
122
334
  const pins = {};
123
335
  const fresh = !existsSync(join(dir, "package.json"));
124
336
 
125
- for (const f of tpl.files) {
337
+ // Decided before a file is written: a refused mode leaves the app as it was.
338
+ const keys = { ...(tables ? { tables } : {}), ...(mode ? { mode } : {}) };
339
+ const appMode = modeOf({ ...before.manifest, ...keys });
340
+ requireOwnTables(appMode, keys.tables ?? before.manifest?.tables ?? "public", "manifest");
341
+ const files = modeFilesOrFail(tpl, appMode);
342
+
343
+ for (const f of files) {
126
344
  const to = join(dir, f.path);
127
345
  if (f.class === "starter" && existsSync(to)) {
128
346
  kept.push(f.path);
@@ -187,6 +405,11 @@ function lay(dir, tpl, before) {
187
405
  }
188
406
  }
189
407
 
408
+ // The lock last: package.json is final now.
409
+ const cut = cutLock(dir, tpl, readJson(join(dir, "package.json")));
410
+ const lockWritten = writeLock(dir, cut, written);
411
+ if (cut) notes.push(...cut.notes);
412
+
190
413
  for (const p of Object.keys(before.pins)) {
191
414
  if (p in pins) continue;
192
415
  if (p.startsWith(`${BLOCK_DIR}/`) && !(p.split("/")[2] in blocks)) continue;
@@ -199,32 +422,45 @@ function lay(dir, tpl, before) {
199
422
  removed.push(p);
200
423
  }
201
424
  }
425
+ // A file the mode leaves out that no pin names is not the template's to
426
+ // delete (the app wrote it, or a hand laid it), but it is in the way:
427
+ // under output: "export" Next ignores a proxy.js, and lib/db.js is pg.
428
+ const laid = new Set(files.map((f) => f.path));
429
+ for (const f of tpl.files) {
430
+ if (f.class !== "fixed" || laid.has(f.path) || f.path in before.pins || !existsSync(join(dir, f.path))) continue;
431
+ notes.push(`${f.path}: ${appMode} mode lays none, and this one is not pinned — left alone; delete it unless the app's own code needs it`);
432
+ }
202
433
 
203
434
  writeJson(join(dir, PINS), sortKeys(pins));
204
- writeJson(join(dir, MANIFEST), { template: tpl.name, version: v, blocks: sortKeys(blocks) });
435
+ const manifest = { ...before.manifest, template: tpl.name, version: v, blocks: sortKeys(blocks), ...keys };
436
+ writeJson(join(dir, MANIFEST), manifest);
205
437
  return {
206
438
  template: tpl.name,
207
439
  version: v,
440
+ // An absent key is an app from before 0.7.0: its tables are in public.
441
+ tables: manifest.tables ?? "public",
442
+ mode: appMode,
208
443
  from: before.manifest?.version ?? null,
209
444
  written,
210
445
  kept,
211
446
  removed,
212
447
  package: pkg,
213
- install: fresh || pkg.some((c) => c.key !== "scripts"),
448
+ lock: lockSummary(cut),
449
+ install: fresh || lockWritten || pkg.some((c) => c.key !== "scripts"),
214
450
  pinned: Object.keys(pins).length,
215
451
  blocks: Object.keys(blocks),
216
452
  notes,
217
453
  };
218
454
  }
219
455
 
220
- /** `init [dir] [-t next] [--force]` */
221
- export function init(dir, { template = "next", force = false } = {}) {
456
+ /** `init [dir] [-t next] [--force] [--tables own|public] [--mode browser|server]` */
457
+ export function init(dir, { template = "next", force = false, tables, mode: askedMode } = {}) {
222
458
  const tpl = loadTemplateOrFail(template);
223
459
  const before = readState(dir);
224
460
  if (existsSync(join(dir, "package.json")) && !force) {
225
461
  throw new CliError(
226
462
  `${dir} already holds an app`,
227
- before.manifest
463
+ writtenByLay(before.manifest)
228
464
  ? "makersclaw update lays this version over it"
229
465
  : "makersclaw init --force lays the template over it: fixed files and docs are replaced, your own files stay",
230
466
  );
@@ -232,7 +468,13 @@ export function init(dir, { template = "next", force = false } = {}) {
232
468
  if (before.manifest?.template && before.manifest.template !== template) {
233
469
  throw new CliError(`this app was laid from template "${before.manifest.template}", not "${template}"`, `makersclaw init --force -t ${before.manifest.template}`);
234
470
  }
235
- return lay(dir, tpl, before);
471
+ // Decided before a file is written: a refused --tables or --mode leaves
472
+ // the app as it was. A new browser app's tables are own unless --tables
473
+ // says otherwise, and then it is refused.
474
+ const m = modeFor(before, askedMode);
475
+ const t = tablesFor(dir, before, tables ?? (m.key === "browser" ? "own" : undefined));
476
+ if (tables !== undefined) requireOwnTables(m.mode, t, "flags");
477
+ return lay(dir, tpl, before, { tables: t, mode: m.key });
236
478
  }
237
479
 
238
480
  /** `update [--major] [--adopt]` — this version's fixed files, docs and
@@ -269,6 +511,57 @@ export function update(dir, { major = false, adopt = false } = {}) {
269
511
  return lay(dir, tpl, before);
270
512
  }
271
513
 
514
+ /** What an app's own code must change when it moves to a mode. The
515
+ * template lays the fixed files; the surfaces are the app's, and move in
516
+ * the same build (claw's STATIC-SPEC §3, "What the Builder is told"). */
517
+ export const MODE_CHANGES = {
518
+ browser: [
519
+ 'every surface a client component ("use client") that reads through the lib/data hooks (useRows, useRow, useCount, useCountBy, useActivity) and writes through lib/actions; a hook compares its options on every render (a time in filter or since to the minute), so anything else worked out per render (a Date.now() number) is worked out once, with useMemo',
520
+ "view state (?row=) from useSearchParams(): a browser app's page is a file built once and has no request, so a surface gets no searchParams",
521
+ "a write of more than one row is a function in the app's schema ((args jsonb) returns jsonb), called with runAction and named in references/actions.md",
522
+ 'no server code of its own: no "use server" module, no route.js under app/, no proxy.js or middleware.js, no import of next/headers, next/cache, next/server, pg or lib/db',
523
+ "no export const dynamic, revalidate, fetchCache, runtime or dynamicParams in the app's own files",
524
+ "no process.env in the app's own code but CLAW_BASE_PATH, CLAW_PRODUCT and NEXT_PUBLIC_*: a prerendered page prints what it reads into a file anyone who opens the app can fetch",
525
+ ],
526
+ server: [
527
+ "lib/data.js is the server's again (pg, next/headers): a client surface takes its hooks from lib/door.js, or becomes a server component that calls loadRows",
528
+ "lib/actions.js is Server Actions again: a client surface calls them as it did",
529
+ "server code the app needs (a route handler, a Server Action of its own) may come back",
530
+ ],
531
+ };
532
+
533
+ /**
534
+ * `mode <browser|server>` — the one switch of an app's mode. It sets
535
+ * `mode` in makersclaw.json, lays that mode's fixed files, removes the
536
+ * other mode's (as update removes a retired file), and says what the
537
+ * app's own code must change. Browser needs `tables: "own"`: the door
538
+ * serves only an app's own schema. The app must be on this version, as
539
+ * for add: the switch lays this version's fixed files.
540
+ */
541
+ export function mode(dir, to) {
542
+ if (to === undefined) throw new CliError("name a mode: browser or server", "makersclaw mode browser · makersclaw mode server");
543
+ if (!MODES.includes(to)) throw new CliError(`an app's mode is ${MODES.join(" or ")}, not "${to}"`, "server: a Next server in the workspace · browser: its code runs in the member's browser, and every read and write goes through the platform's data door");
544
+ const before = requireManifest(dir);
545
+ const { manifest } = before;
546
+ const v = version();
547
+ if (manifest.version !== v) {
548
+ throw new CliError(
549
+ `this app is on makersclaw ${manifest.version}; this is ${v}, and a switch of mode lays this version's fixed files`,
550
+ newer(manifest.version, v)
551
+ ? `npx makersclaw@${manifest.version} mode ${to}`
552
+ : compatible(manifest.version, v)
553
+ ? `makersclaw update first (${manifest.version} → ${v} is compatible), then mode ${to}`
554
+ : `makersclaw update --major first (makersclaw diff shows what changes), then mode ${to}`,
555
+ );
556
+ }
557
+ const tpl = loadTemplateOrFail(manifest.template);
558
+ // Not checked: a mode this CLI cannot read is what the switch mends.
559
+ const was = Object.hasOwn(manifest, "mode") ? manifest.mode : BASE_MODE;
560
+ requireOwnTables(to, manifest.tables, "switch");
561
+ const r = lay(dir, tpl, before, { mode: to });
562
+ return { ...r, was, changed: was !== to, change: MODE_CHANGES[to] };
563
+ }
564
+
272
565
  function importFor(item) {
273
566
  if (item.type === "block") {
274
567
  const names = item.exports?.length ? item.exports.join(", ") : "…";
@@ -295,6 +588,9 @@ export function add(dir, names) {
295
588
  }
296
589
  const tpl = loadTemplateOrFail(manifest.template);
297
590
  const items = loadItems(tpl);
591
+ // A kit component is restored from the app's mode's copy of its file.
592
+ const appMode = modeOf(manifest);
593
+ const laid = new Map(modeFilesOrFail(tpl, appMode).map((f) => [f.path, f.from]));
298
594
  const order = [];
299
595
  const seen = new Set();
300
596
  const visit = (name, by) => {
@@ -319,7 +615,9 @@ export function add(dir, names) {
319
615
  const files = [];
320
616
  let changed = 0;
321
617
  for (const f of item.files) {
322
- const bytes = readFileSync(f.from);
618
+ const from = item.type === "block" ? f.from : laid.get(f.to);
619
+ if (!from) throw new CliError(`${item.name}: ${f.to} is not laid in ${appMode} mode`);
620
+ const bytes = readFileSync(from);
323
621
  const hash = sha256(bytes);
324
622
  if (hashFile(join(dir, f.to)) !== hash) {
325
623
  writeFile(join(dir, f.to), bytes);
@@ -355,13 +653,19 @@ export function add(dir, names) {
355
653
  if (changes.length) writeJson(path, out);
356
654
  pkg = changes;
357
655
  }
656
+ // A block's packages come with their lock entries: the release resolved
657
+ // them with everything else, so the next install is frozen.
658
+ const cut = cutLock(dir, tpl, readJson(join(dir, "package.json")));
659
+ const lockWritten = writeLock(dir, cut, []);
358
660
  writeJson(join(dir, PINS), sortKeys(pins));
359
661
  writeJson(join(dir, MANIFEST), { ...manifest, blocks: sortKeys(manifest.blocks) });
360
662
  return {
361
663
  version: v,
362
664
  added,
363
665
  package: pkg,
364
- install: pkg.length > 0,
666
+ lock: cut ? { ...lockSummary(cut), written: lockWritten } : null,
667
+ notes: cut?.notes ?? [],
668
+ install: pkg.length > 0 || lockWritten,
365
669
  usage: order.filter((i) => i.type === "block" && names.includes(i.name)).map((i) => ({ name: i.name, readme: readmeOf(i) })),
366
670
  };
367
671
  }
@@ -423,6 +727,8 @@ export function diff(dir) {
423
727
  const { manifest, pins } = requireManifest(dir);
424
728
  const tpl = loadTemplateOrFail(manifest.template);
425
729
  const v = version();
730
+ const appMode = modeOf(manifest);
731
+ const files = modeFilesOrFail(tpl, appMode);
426
732
  const entries = [];
427
733
  const statusOf = (path, have, want, pinned) => {
428
734
  if (have === want) return null;
@@ -431,12 +737,14 @@ export function diff(dir) {
431
737
  return "changed";
432
738
  };
433
739
 
434
- for (const f of tpl.files) {
740
+ // The app's mode's files: update lays those, and removes a pinned file
741
+ // the mode does not lay ("dropped").
742
+ for (const f of files) {
435
743
  if (f.class !== "fixed" && f.class !== "docs") continue;
436
744
  const status = statusOf(f.path, hashFile(join(dir, f.path)), sha256(readFileSync(f.from)), f.class === "fixed" ? pins[f.path] : null);
437
745
  if (status) entries.push({ item: "template", path: f.path, status });
438
746
  }
439
- const shipped = new Set(tpl.files.map((f) => f.path));
747
+ const shipped = new Set(files.map((f) => f.path));
440
748
  for (const p of Object.keys(pins)) {
441
749
  if (!p.startsWith(`${BLOCK_DIR}/`) && !shipped.has(p) && ownedPath(p, tpl)) entries.push({ item: "template", path: p, status: "dropped" });
442
750
  }
@@ -464,12 +772,43 @@ export function diff(dir) {
464
772
  // …and the installed blocks' npm packages, which update lays with them.
465
773
  const blockDeps = Object.assign({}, ...Object.keys(manifest.blocks ?? {}).map((n) => known.find((b) => b.name === n)?.dependencies ?? {}));
466
774
  if (existsSync(pkgPath) && Object.keys(blockDeps).length) pkg.push(...mergePackage(readJson(pkgPath), { dependencies: blockDeps }).changes);
775
+ // …and the lock update would cut for the package.json it would leave.
776
+ if (existsSync(pkgPath) && tplPkg) {
777
+ const after = mergePackage(mergePackage(readJson(pkgPath), readJson(tplPkg.from)).out, { dependencies: blockDeps }).out;
778
+ const cut = cutLock(dir, tpl, after);
779
+ if (cut && cut.text !== cut.before) entries.push({ item: "template", path: LOCKFILE, status: cut.before === null ? "new" : "changed" });
780
+ }
467
781
  return {
468
782
  from: manifest.version,
469
783
  to: v,
470
784
  compatible: compatible(manifest.version, v) && !newer(manifest.version, v),
785
+ mode: appMode,
471
786
  entries,
472
787
  package: pkg,
473
788
  blocks: manifest.blocks ?? {},
474
789
  };
475
790
  }
791
+
792
+ /** `install` — the app's packages: pnpm from its lock, frozen when the
793
+ * lock names exactly package.json's dependencies (src/pm.mjs); npm when
794
+ * no pnpm runs, said in `notes` — but not when the registry did not
795
+ * answer, since npm would need it too. */
796
+ export function install(dir, { env = process.env, onNote } = {}) {
797
+ if (!existsSync(join(dir, "package.json"))) {
798
+ throw new CliError(`${dir} has no package.json — there is no app here`, "makersclaw init lays the template here");
799
+ }
800
+ const r = installDeps(dir, { env, onNote });
801
+ if (r.unreachable) {
802
+ throw new CliError(
803
+ `no pnpm on this machine, and npm could not fetch pnpm ${PNPM_VERSION}: the registry did not answer (${r.unreachable}). An npm install would need the same registry, so nothing was installed\n${r.log}`,
804
+ `name a pnpm ${PNPM_VERSION.split(".")[0]} in MAKERSCLAW_PNPM or put one on PATH, or install again once the registry answers`,
805
+ );
806
+ }
807
+ if (!r.ok) {
808
+ throw new CliError(
809
+ `${r.manager} install failed (exit ${r.code})${r.notes.length ? ` — ${r.notes.join("; ")}` : ""}${r.log ? `\n${r.log}` : ""}`,
810
+ r.manager === "pnpm" && r.frozen ? "a frozen install needs every package in the store or the registry" : null,
811
+ );
812
+ }
813
+ return r;
814
+ }