makersclaw 0.1.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 +68 -0
- package/bin/makersclaw.mjs +10 -0
- package/package.json +34 -0
- package/registry/blocks/board/README.md +130 -0
- package/registry/blocks/board/block.json +10 -0
- package/registry/blocks/board/board.jsx +435 -0
- package/registry/blocks/board/demo.jsx +50 -0
- package/registry/blocks/board/index.js +4 -0
- package/registry/blocks/job-panel/README.md +68 -0
- package/registry/blocks/job-panel/block.json +10 -0
- package/registry/blocks/job-panel/demo.jsx +75 -0
- package/registry/blocks/job-panel/index.js +3 -0
- package/registry/blocks/job-panel/job-panel.jsx +110 -0
- package/src/cli.mjs +301 -0
- package/src/fsutil.mjs +76 -0
- package/src/install.mjs +457 -0
- package/src/registry.mjs +191 -0
- package/templates/next/files/README.md +626 -0
- package/templates/next/files/app/auto-refresh.jsx +26 -0
- package/templates/next/files/app/error-relay.jsx +53 -0
- package/templates/next/files/app/error.jsx +44 -0
- package/templates/next/files/app/global-error.jsx +36 -0
- package/templates/next/files/app/globals.css +178 -0
- package/templates/next/files/app/layout.jsx +51 -0
- package/templates/next/files/app/not-found.jsx +23 -0
- package/templates/next/files/app/page.jsx +22 -0
- package/templates/next/files/app/s/[key]/page.jsx +47 -0
- package/templates/next/files/app/shell.jsx +64 -0
- package/templates/next/files/check/run.mjs +683 -0
- package/templates/next/files/components/parts/fixture.jsx +60 -0
- package/templates/next/files/components/parts/index.js +5 -0
- package/templates/next/files/components/parts/record-switcher.jsx +49 -0
- package/templates/next/files/components/ui/button.jsx +81 -0
- package/templates/next/files/components/ui/cx.js +4 -0
- package/templates/next/files/components/ui/depiction.jsx +66 -0
- package/templates/next/files/components/ui/field.jsx +126 -0
- package/templates/next/files/components/ui/icon.jsx +87 -0
- package/templates/next/files/components/ui/index.js +12 -0
- package/templates/next/files/components/ui/menu.jsx +123 -0
- package/templates/next/files/components/ui/notice.jsx +86 -0
- package/templates/next/files/components/ui/sheet.jsx +89 -0
- package/templates/next/files/components/ui/tabs.jsx +81 -0
- package/templates/next/files/lib/actions.js +187 -0
- package/templates/next/files/lib/claw.js +219 -0
- package/templates/next/files/lib/data.js +171 -0
- package/templates/next/files/lib/db.js +72 -0
- package/templates/next/files/lib/hooks.js +218 -0
- package/templates/next/files/lib/platform.js +59 -0
- package/templates/next/files/lib/sample.js +12 -0
- package/templates/next/files/middleware.js +42 -0
- package/templates/next/files/next.config.mjs +41 -0
- package/templates/next/files/package.json +19 -0
- package/templates/next/files/postcss.config.mjs +3 -0
- package/templates/next/files/schema.sql +22 -0
- package/templates/next/files/surfaces/README.md +32 -0
- package/templates/next/files/surfaces.js +53 -0
- package/templates/next/template.json +118 -0
package/src/registry.mjs
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// The registry: the templates `init` lays and the items `add` installs.
|
|
2
|
+
//
|
|
3
|
+
// Two kinds of item. COMPONENTS are the kit (components/ui) and the parts
|
|
4
|
+
// (components/parts): they come with every app, because `init` lays the
|
|
5
|
+
// whole template — `add button` only restores one. BLOCKS are the bigger
|
|
6
|
+
// pieces a surface is assembled from (a board, a timeline, a canvas): no
|
|
7
|
+
// app has one until `add` puts it there, so the agent searches for what
|
|
8
|
+
// it needs and installs just that. Everything here ships inside this
|
|
9
|
+
// package, so the version you run is the version you get.
|
|
10
|
+
|
|
11
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { globToRegExp, readJson, walk } from "./fsutil.mjs";
|
|
15
|
+
|
|
16
|
+
export const PKG_ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
17
|
+
const TEMPLATES = join(PKG_ROOT, "templates");
|
|
18
|
+
const BLOCKS = join(PKG_ROOT, "registry", "blocks");
|
|
19
|
+
|
|
20
|
+
/** Where an installed block lives in an app: components/blocks/<name>/. */
|
|
21
|
+
export const BLOCK_DIR = "components/blocks";
|
|
22
|
+
|
|
23
|
+
const NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
24
|
+
const BLOCK_FILE = /^[a-z0-9]+(?:-[a-z0-9]+)*\.(?:jsx|js)$/;
|
|
25
|
+
|
|
26
|
+
let pkg;
|
|
27
|
+
export function packageInfo() {
|
|
28
|
+
pkg ??= readJson(join(PKG_ROOT, "package.json"));
|
|
29
|
+
return { name: pkg.name, version: pkg.version };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function templateNames() {
|
|
33
|
+
if (!existsSync(TEMPLATES)) return [];
|
|
34
|
+
return readdirSync(TEMPLATES)
|
|
35
|
+
.filter((n) => existsSync(join(TEMPLATES, n, "template.json")))
|
|
36
|
+
.sort();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Every file a template lays belongs to exactly one class:
|
|
41
|
+
* fixed ours: pinned by hash in check/pins.json, replaced by update
|
|
42
|
+
* docs ours: replaced by update, not pinned (reading, not code)
|
|
43
|
+
* starter laid once when missing, the app's from then on
|
|
44
|
+
* merged package.json: our scripts and dependencies merged into the app's
|
|
45
|
+
*/
|
|
46
|
+
export const FILE_CLASSES = ["fixed", "docs", "starter", "merged"];
|
|
47
|
+
|
|
48
|
+
export function loadTemplate(name) {
|
|
49
|
+
const dir = join(TEMPLATES, name);
|
|
50
|
+
const meta = readJson(join(dir, "template.json"));
|
|
51
|
+
if (!meta || !NAME.test(name)) return null;
|
|
52
|
+
const root = join(dir, "files");
|
|
53
|
+
const patterns = FILE_CLASSES.map((cls) => [cls, (meta.files?.[cls] ?? []).map(globToRegExp)]);
|
|
54
|
+
const files = walk(root).map((path) => {
|
|
55
|
+
const hits = patterns.filter(([, res]) => res.some((re) => re.test(path))).map(([cls]) => cls);
|
|
56
|
+
if (hits.length !== 1) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`template ${name}: ${path} is ${hits.length ? `in ${hits.join(" and ")}` : "in no class"} — template.json must name every file exactly once`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return { path, class: hits[0], from: join(root, path) };
|
|
62
|
+
});
|
|
63
|
+
const components = (meta.components ?? []).map((c) => ({
|
|
64
|
+
name: c.name,
|
|
65
|
+
type: c.type ?? "component",
|
|
66
|
+
title: c.title ?? c.name,
|
|
67
|
+
description: c.description ?? "",
|
|
68
|
+
tags: c.tags ?? [],
|
|
69
|
+
useWhen: c.useWhen ?? "",
|
|
70
|
+
requires: [],
|
|
71
|
+
dependencies: {},
|
|
72
|
+
files: (c.files ?? []).map((path) => {
|
|
73
|
+
const f = files.find((x) => x.path === path);
|
|
74
|
+
if (!f || f.class !== "fixed") throw new Error(`template ${name}: component ${c.name} names ${path}, which is not a fixed file`);
|
|
75
|
+
return { from: f.from, to: path };
|
|
76
|
+
}),
|
|
77
|
+
readme: null,
|
|
78
|
+
}));
|
|
79
|
+
return { name, title: meta.title ?? name, description: meta.description ?? "", root, files, components };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function loadBlocks() {
|
|
83
|
+
if (!existsSync(BLOCKS)) return [];
|
|
84
|
+
return readdirSync(BLOCKS)
|
|
85
|
+
.filter((n) => statSync(join(BLOCKS, n)).isDirectory())
|
|
86
|
+
.sort()
|
|
87
|
+
.map((folder) => {
|
|
88
|
+
const dir = join(BLOCKS, folder);
|
|
89
|
+
const meta = readJson(join(dir, "block.json"));
|
|
90
|
+
if (!meta) throw new Error(`block ${folder}: no block.json`);
|
|
91
|
+
if (meta.name !== folder || !NAME.test(folder)) throw new Error(`block ${folder}: name must be the folder's, lowercase and dashed`);
|
|
92
|
+
const files = (meta.files ?? []).map((f) => {
|
|
93
|
+
if (!BLOCK_FILE.test(f)) throw new Error(`block ${folder}: ${f} — block files are lowercase-dashed .jsx/.js at the block's root`);
|
|
94
|
+
if (!existsSync(join(dir, f))) throw new Error(`block ${folder}: ${f} is listed but missing`);
|
|
95
|
+
return { from: join(dir, f), to: `${BLOCK_DIR}/${folder}/${f}` };
|
|
96
|
+
});
|
|
97
|
+
if (!files.some((f) => f.to.endsWith("/index.js"))) throw new Error(`block ${folder}: needs index.js — what a surface imports`);
|
|
98
|
+
return {
|
|
99
|
+
name: folder,
|
|
100
|
+
type: "block",
|
|
101
|
+
title: meta.title ?? folder,
|
|
102
|
+
description: meta.description ?? "",
|
|
103
|
+
tags: meta.tags ?? [],
|
|
104
|
+
useWhen: meta.useWhen ?? "",
|
|
105
|
+
requires: meta.requires ?? [],
|
|
106
|
+
dependencies: meta.dependencies ?? {},
|
|
107
|
+
exports: meta.exports ?? [],
|
|
108
|
+
files,
|
|
109
|
+
readme: existsSync(join(dir, "README.md")) ? join(dir, "README.md") : null,
|
|
110
|
+
demo: existsSync(join(dir, "demo.jsx")) ? join(dir, "demo.jsx") : null,
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Everything `add` can install for this template: its kit, then the blocks. */
|
|
116
|
+
export function loadItems(template) {
|
|
117
|
+
return [...template.components, ...loadBlocks()];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function readmeOf(item) {
|
|
121
|
+
return item.readme ? readFileSync(item.readme, "utf8") : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---- search -------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
const STOP = new Set(
|
|
127
|
+
"a an the of to and or for with in on by my our i we you that this it is are be as at from into between some each all any show shows need needs want wants like one app apps thing things where which what when how can".split(" "),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
function words(text) {
|
|
131
|
+
return String(text ?? "").toLowerCase().match(/[a-z0-9]+/g) ?? [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Close enough for matching: "stages" finds "stage", "dragging" finds "drag". */
|
|
135
|
+
function stem(w) {
|
|
136
|
+
if (w.length <= 3) return w;
|
|
137
|
+
return w.replace(/(?:ing|ies|es|ed|s)$/, (m) => (m === "ies" ? "y" : "")).replace(/(.)\1$/, "$1");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Items ranked by how well they match the words: the name first, then
|
|
141
|
+
* tags, the title, and the description and when-to-use prose. */
|
|
142
|
+
export function searchItems(items, query, { type, limit = 8 } = {}) {
|
|
143
|
+
const raw = words(query);
|
|
144
|
+
const terms = [...new Set(raw.filter((w) => !STOP.has(w)).map(stem))];
|
|
145
|
+
if (terms.length === 0) return [];
|
|
146
|
+
const whole = raw.join("-");
|
|
147
|
+
return items
|
|
148
|
+
.filter((item) => !type || item.type === type)
|
|
149
|
+
.map((item) => {
|
|
150
|
+
const name = words(item.name.replace(/-/g, " ")).map(stem);
|
|
151
|
+
const tags = item.tags.flatMap((t) => words(t)).map(stem);
|
|
152
|
+
const title = words(item.title).map(stem);
|
|
153
|
+
const prose = new Set([...words(item.description), ...words(item.useWhen)].map(stem));
|
|
154
|
+
let score = whole === item.name ? 30 : 0;
|
|
155
|
+
for (const t of terms) {
|
|
156
|
+
if (name.includes(t)) score += 10;
|
|
157
|
+
if (tags.includes(t)) score += 6;
|
|
158
|
+
if (title.includes(t)) score += 3;
|
|
159
|
+
if (prose.has(t)) score += 1;
|
|
160
|
+
}
|
|
161
|
+
return { item, score };
|
|
162
|
+
})
|
|
163
|
+
.filter((m) => m.score > 0)
|
|
164
|
+
.sort((a, b) => b.score - a.score || a.item.name.localeCompare(b.item.name))
|
|
165
|
+
.slice(0, limit);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function distance(a, b) {
|
|
169
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
170
|
+
for (let i = 1; i <= a.length; i++) {
|
|
171
|
+
let prev = row[0];
|
|
172
|
+
row[0] = i;
|
|
173
|
+
for (let j = 1; j <= b.length; j++) {
|
|
174
|
+
const next = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
175
|
+
prev = row[j];
|
|
176
|
+
row[j] = next;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return row[b.length];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Names close to a mistyped one, for the error that names the right one. */
|
|
183
|
+
export function suggest(items, name) {
|
|
184
|
+
const close = items
|
|
185
|
+
.map((i) => ({ name: i.name, d: i.name.includes(name) || name.includes(i.name) ? 0 : distance(name, i.name) }))
|
|
186
|
+
.filter((x) => x.d <= 2)
|
|
187
|
+
.sort((a, b) => a.d - b.d)
|
|
188
|
+
.map((x) => x.name);
|
|
189
|
+
if (close.length) return close.slice(0, 3);
|
|
190
|
+
return searchItems(items, name, { limit: 3 }).map((m) => m.item.name);
|
|
191
|
+
}
|