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.
- package/README.md +73 -7
- package/package.json +3 -2
- package/r/annotate.json +4 -4
- package/r/approval-request.json +3 -3
- package/r/audio-player.json +3 -3
- package/r/auth-form.json +2 -2
- package/r/board.json +4 -4
- package/r/calendar-view.json +4 -4
- package/r/cart.json +3 -3
- package/r/chart.json +3 -3
- package/r/chat.json +3 -3
- package/r/code-editor.json +4 -4
- package/r/comment-box.json +4 -4
- package/r/contact-section.json +3 -3
- package/r/core.json +3 -3
- package/r/csv-import.json +4 -4
- package/r/data-table.json +3 -3
- package/r/diagram.json +3 -3
- package/r/doc-editor.json +4 -4
- package/r/doc-page.json +4 -4
- package/r/email-render.json +2 -2
- package/r/faq.json +2 -2
- package/r/file-upload.json +3 -3
- package/r/flow-canvas.json +4 -4
- package/r/footer.json +3 -3
- package/r/gantt.json +4 -4
- package/r/job-panel.json +3 -3
- package/r/json-editor.json +4 -4
- package/r/map.json +5 -5
- package/r/menubar.json +2 -2
- package/r/mind-map.json +4 -4
- package/r/navigation-menu.json +2 -2
- package/r/order-detail.json +2 -2
- package/r/org-chart.json +4 -4
- package/r/page-builder.json +6 -6
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +6 -6
- package/r/product-detail.json +3 -3
- package/r/product-grid.json +4 -4
- package/r/record-form.json +3 -3
- package/r/record-notes.json +5 -5
- package/r/registry.json +121 -121
- package/r/search-dialog.json +3 -3
- package/r/settings-list.json +3 -3
- package/r/setup-checklist.json +3 -3
- package/r/share-dialog.json +3 -3
- package/r/sidebar.json +2 -2
- package/r/signature-pad.json +3 -3
- package/r/sketch-canvas.json +5 -5
- package/r/spreadsheet.json +3 -3
- package/r/stat-cards.json +4 -4
- package/r/task-list.json +4 -4
- package/r/task-model.json +3 -3
- package/r/testimonials.json +2 -2
- package/r/transaction-list.json +3 -3
- package/r/whiteboard.json +4 -4
- package/registry/blocks/annotate/README.md +14 -5
- package/registry/blocks/approval-request/README.md +15 -8
- package/registry/blocks/audio-player/README.md +17 -10
- package/registry/blocks/board/README.md +13 -9
- package/registry/blocks/board/board.jsx +1 -1
- package/registry/blocks/calendar-view/README.md +28 -4
- package/registry/blocks/cart/README.md +14 -10
- package/registry/blocks/chart/README.md +14 -7
- package/registry/blocks/chat/README.md +43 -13
- package/registry/blocks/code-editor/README.md +4 -0
- package/registry/blocks/code-editor/code-editor.jsx +1 -1
- package/registry/blocks/comment-box/README.md +20 -13
- package/registry/blocks/contact-section/README.md +7 -6
- package/registry/blocks/csv-import/README.md +34 -12
- package/registry/blocks/data-table/README.md +41 -8
- package/registry/blocks/diagram/diagram.jsx +1 -1
- package/registry/blocks/doc-editor/README.md +18 -6
- package/registry/blocks/doc-editor/doc-editor.jsx +1 -1
- package/registry/blocks/doc-page/README.md +16 -6
- package/registry/blocks/file-upload/README.md +7 -1
- package/registry/blocks/flow-canvas/README.md +16 -5
- package/registry/blocks/flow-canvas/flow-canvas.jsx +1 -1
- package/registry/blocks/footer/README.md +4 -2
- package/registry/blocks/gantt/README.md +18 -12
- package/registry/blocks/job-panel/README.md +1 -1
- package/registry/blocks/json-editor/README.md +16 -4
- package/registry/blocks/map/README.md +19 -3
- package/registry/blocks/map/map-view.jsx +1 -1
- package/registry/blocks/map/maplibre.js +1 -1
- package/registry/blocks/mind-map/README.md +16 -5
- package/registry/blocks/org-chart/README.md +10 -7
- package/registry/blocks/page-builder/README.md +13 -6
- package/registry/blocks/page-builder/page-builder.jsx +1 -1
- package/registry/blocks/page-builder/page-view.jsx +1 -1
- package/registry/blocks/page-builder/page.js +1 -1
- package/registry/blocks/pdf-viewer/README.md +22 -20
- package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1 -1
- package/registry/blocks/pdf-viewer/pdfjs.js +1 -1
- package/registry/blocks/pdf-viewer/ssr-shim.js +1 -1
- package/registry/blocks/product-detail/README.md +13 -7
- package/registry/blocks/product-detail/demo.jsx +1 -1
- package/registry/blocks/product-grid/README.md +12 -10
- package/registry/blocks/record-form/README.md +26 -9
- package/registry/blocks/record-notes/README.md +42 -30
- package/registry/blocks/search-dialog/README.md +38 -7
- package/registry/blocks/settings-list/README.md +29 -17
- package/registry/blocks/setup-checklist/README.md +12 -14
- package/registry/blocks/share-dialog/README.md +1 -1
- package/registry/blocks/signature-pad/README.md +15 -4
- package/registry/blocks/sketch-canvas/README.md +13 -4
- package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1 -1
- package/registry/blocks/sketch-canvas/ssr-shim.js +3 -3
- package/registry/blocks/spreadsheet/README.md +17 -8
- package/registry/blocks/stat-cards/README.md +17 -8
- package/registry/blocks/task-list/README.md +14 -11
- package/registry/blocks/task-model/README.md +3 -3
- package/registry/blocks/transaction-list/README.md +15 -7
- package/registry/blocks/whiteboard/README.md +19 -13
- package/src/cli.mjs +65 -9
- package/src/install.mjs +360 -21
- package/src/lockfile.mjs +468 -0
- package/src/pm.mjs +473 -0
- package/src/registry.mjs +125 -1
- package/templates/next/browser/app/auto-refresh.jsx +67 -0
- package/templates/next/browser/app/layout.jsx +94 -0
- package/templates/next/browser/app/page.jsx +60 -0
- package/templates/next/browser/app/s/[key]/page.jsx +95 -0
- package/templates/next/browser/lib/actions.js +16 -0
- package/templates/next/browser/lib/data.js +43 -0
- package/templates/next/files/README.md +818 -82
- package/templates/next/files/app/claw-alive/route.js +2 -2
- package/templates/next/files/app/frame.js +134 -9
- package/templates/next/files/app/layout.jsx +2 -2
- package/templates/next/files/check/run.mjs +1491 -53
- package/templates/next/files/components/ui/item.jsx +2 -2
- package/templates/next/files/components/ui/notice.jsx +1 -1
- package/templates/next/files/components/ui/separator.jsx +1 -1
- package/templates/next/files/lib/actions.js +171 -52
- package/templates/next/files/lib/brain-server.js +96 -0
- package/templates/next/files/lib/brain.js +413 -0
- package/templates/next/files/lib/claw.js +11 -0
- package/templates/next/files/lib/data.js +120 -30
- package/templates/next/files/lib/db.js +97 -17
- package/templates/next/files/lib/door.js +680 -0
- package/templates/next/files/next.config.mjs +85 -9
- package/templates/next/files/package.json +3 -3
- package/templates/next/files/{middleware.js → proxy.js} +13 -6
- package/templates/next/files/surfaces.js +3 -2
- package/templates/next/pnpm-lock.yaml +5870 -0
- package/templates/next/template.json +13 -6
- 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,
|
|
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 {
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
+
}
|