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
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// The Board with rows in memory — the gallery's and the smoke test's
|
|
4
|
+
// surface. Not installed into apps: `makersclaw add board` lays index.js
|
|
5
|
+
// and board.jsx only.
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { Badge } from "../components/ui";
|
|
8
|
+
import { Board } from "../components/blocks/board";
|
|
9
|
+
|
|
10
|
+
const STAGES = [
|
|
11
|
+
{ key: "lead", label: "Lead" },
|
|
12
|
+
{ key: "call", label: "Call booked" },
|
|
13
|
+
{ key: "proposal", label: "Proposal" },
|
|
14
|
+
{ key: "won", label: "Won" },
|
|
15
|
+
];
|
|
16
|
+
const START = [
|
|
17
|
+
{ id: 1, name: "Northwind", stage: "lead", owner: "Maya", value: 4000 },
|
|
18
|
+
{ id: 2, name: "Acme Ltd", stage: "call", owner: "Leon", value: 12000 },
|
|
19
|
+
{ id: 3, name: "Globex", stage: "call", owner: "Priya", value: 7000 },
|
|
20
|
+
{ id: 4, name: "Initech", stage: "proposal", owner: "Maya", value: 22000 },
|
|
21
|
+
{ id: 5, name: "Umbrella", stage: "paused", owner: "Leon", value: 3000 },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export default function BoardDemo() {
|
|
25
|
+
const [rows, setRows] = useState(START);
|
|
26
|
+
return (
|
|
27
|
+
<Board
|
|
28
|
+
rows={rows}
|
|
29
|
+
groupBy="stage"
|
|
30
|
+
columns={STAGES}
|
|
31
|
+
titleOf={(d) => d.name}
|
|
32
|
+
onMove={async (row, to) => {
|
|
33
|
+
setRows((all) => all.map((r) => (r.id === row.id ? { ...r, stage: to } : r)));
|
|
34
|
+
return { ok: true };
|
|
35
|
+
}}
|
|
36
|
+
onCreate={async (name, stage) => {
|
|
37
|
+
setRows((all) => [...all, { id: Date.now(), name, stage, owner: null, value: 0 }]);
|
|
38
|
+
return { ok: true };
|
|
39
|
+
}}
|
|
40
|
+
renderDetails={(d) => (
|
|
41
|
+
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
42
|
+
<span>{d.owner ?? "Unowned"}</span>
|
|
43
|
+
<Badge>{Number(d.value ?? 0).toLocaleString()}</Badge>
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
limits={{ call: 1 }}
|
|
47
|
+
label="Demo deals by stage"
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# JobPanel
|
|
2
|
+
|
|
3
|
+
The panel that follows work the founder asked the app's agent for. It
|
|
4
|
+
shows each step's label as the agent works, a Stop button while it runs,
|
|
5
|
+
then "done" (closing itself a few seconds later) or the reason it
|
|
6
|
+
stopped. When no job runs it renders nothing — a panel that is always
|
|
7
|
+
there is a monitoring console, and apps do not have one.
|
|
8
|
+
|
|
9
|
+
```jsx
|
|
10
|
+
import { JobPanel } from "../components/blocks/job-panel"; // "./blocks/job-panel" from components/
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
A client component. It takes the object `useJob()` returns (lib/hooks.js,
|
|
14
|
+
README §5 "Commissioning work") and draws it; starting the job stays with
|
|
15
|
+
your surface.
|
|
16
|
+
|
|
17
|
+
## Props
|
|
18
|
+
|
|
19
|
+
- `job` — what `useJob()` returns. Required.
|
|
20
|
+
- `title` — while it runs, what the agent is doing, in the founder's
|
|
21
|
+
words: `"Drafting the launch plan"`. Default `"Working on it"`.
|
|
22
|
+
- `doneTitle` — after a clean finish: `"Launch plan drafted"`.
|
|
23
|
+
- `failedTitle` — when it stopped with an error; the error follows it.
|
|
24
|
+
- `onRetry` — shows "Try again" beside Close when the job failed: start
|
|
25
|
+
the same job again.
|
|
26
|
+
- `autoClose` — milliseconds after a clean finish before the panel
|
|
27
|
+
closes itself (`job.reset()`); default 6000, `false` to keep it until
|
|
28
|
+
Close.
|
|
29
|
+
- `maxSteps` — the newest steps shown, default 6; older ones become one
|
|
30
|
+
"N earlier steps" line.
|
|
31
|
+
- `showText` — the agent's own account under the steps, default true. It
|
|
32
|
+
is what the agent says it did, never the result: the result is rows.
|
|
33
|
+
- `className` — layout around it. Never colour.
|
|
34
|
+
|
|
35
|
+
## Example
|
|
36
|
+
|
|
37
|
+
```jsx
|
|
38
|
+
"use client";
|
|
39
|
+
import { useEffect } from "react";
|
|
40
|
+
import { useJob } from "../lib/hooks";
|
|
41
|
+
import { Button } from "./ui";
|
|
42
|
+
import { JobPanel } from "./blocks/job-panel";
|
|
43
|
+
|
|
44
|
+
const ASK = "Draft a launch plan for the next product in the list, with dates and owners.";
|
|
45
|
+
|
|
46
|
+
export default function Launches({ launches, reload }) {
|
|
47
|
+
const job = useJob();
|
|
48
|
+
useEffect(() => { reload?.(); }, [job.progress]); // rows appear as they land
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="grid gap-4">
|
|
52
|
+
<div className="flex flex-wrap gap-2">
|
|
53
|
+
<Button variant="secondary" onClick={() => job.start(ASK)} disabled={job.running}>Generate a launch plan</Button>
|
|
54
|
+
<Button variant="primary" icon="plus" onClick={openNewLaunch}>New launch plan</Button>
|
|
55
|
+
</div>
|
|
56
|
+
<JobPanel job={job} title="Drafting the launch plan" doneTitle="Launch plan drafted" onRetry={() => job.start(ASK)} />
|
|
57
|
+
{/* the launches list — the job's result lands here */}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The verb sits on the object list beside the manual "New" (README §5, rule
|
|
64
|
+
1), and the panel sits where the result will appear.
|
|
65
|
+
|
|
66
|
+
Its files are fixed: `npm run check` refuses a change, and `makersclaw add
|
|
67
|
+
job-panel` restores them. A different panel is your own component in
|
|
68
|
+
`components/`, built from the kit.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "job-panel",
|
|
3
|
+
"title": "JobPanel",
|
|
4
|
+
"description": "The panel that follows work the founder asked the app's agent for (useJob): each step's label as it runs, Stop, then done or the reason it stopped — and nothing on screen when no job runs.",
|
|
5
|
+
"useWhen": "A surface has a verb that asks the agent to make or find something (\"Generate a launch plan\", \"Find 20 leads\", \"Plan next sprint\") and the founder should watch it happen where the result will land.",
|
|
6
|
+
"tags": ["job", "agent", "generate", "progress", "steps", "running", "working", "commission", "ai", "stop", "status"],
|
|
7
|
+
"exports": ["JobPanel"],
|
|
8
|
+
"files": ["index.js", "job-panel.jsx"],
|
|
9
|
+
"requires": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// JobPanel driven by a pretend job — the gallery's and the smoke test's
|
|
4
|
+
// surface. Not installed into apps.
|
|
5
|
+
import { useEffect, useRef, useState } from "react";
|
|
6
|
+
import { Button } from "../components/ui";
|
|
7
|
+
import { JobPanel } from "../components/blocks/job-panel";
|
|
8
|
+
|
|
9
|
+
const STEPS = ["Reading the product list", "Searching the web", "Drafting the plan", "Saving 6 tasks"];
|
|
10
|
+
const IDLE = { running: false, steps: [], text: "", error: null, finishedAt: null, progress: 0 };
|
|
11
|
+
|
|
12
|
+
function usePretendJob() {
|
|
13
|
+
const [state, setState] = useState(IDLE);
|
|
14
|
+
const timers = useRef([]);
|
|
15
|
+
const clear = () => {
|
|
16
|
+
timers.current.forEach(clearTimeout);
|
|
17
|
+
timers.current = [];
|
|
18
|
+
};
|
|
19
|
+
useEffect(() => clear, []);
|
|
20
|
+
const later = (ms, fn) => timers.current.push(setTimeout(fn, ms));
|
|
21
|
+
function start(fail) {
|
|
22
|
+
clear();
|
|
23
|
+
setState({ ...IDLE, running: true });
|
|
24
|
+
STEPS.forEach((label, i) =>
|
|
25
|
+
later(700 * i + 200, () =>
|
|
26
|
+
setState((s) => ({
|
|
27
|
+
...s,
|
|
28
|
+
steps: [...s.steps.map((x) => ({ ...x, status: "done" })), { name: `step_${i}`, label, status: "running" }],
|
|
29
|
+
text: `${s.text}${label}…\n`,
|
|
30
|
+
})),
|
|
31
|
+
),
|
|
32
|
+
);
|
|
33
|
+
later(700 * STEPS.length + 400, () =>
|
|
34
|
+
setState((s) =>
|
|
35
|
+
fail
|
|
36
|
+
? {
|
|
37
|
+
...s,
|
|
38
|
+
running: false,
|
|
39
|
+
steps: s.steps.map((x, i) => ({ ...x, status: i === s.steps.length - 1 ? "failed" : "done" })),
|
|
40
|
+
error: "The search service did not answer.",
|
|
41
|
+
}
|
|
42
|
+
: { ...s, running: false, steps: s.steps.map((x) => ({ ...x, status: "done" })), finishedAt: new Date().toISOString(), progress: s.progress + 1 },
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
...state,
|
|
48
|
+
start,
|
|
49
|
+
cancel: () => {
|
|
50
|
+
clear();
|
|
51
|
+
setState((s) => ({ ...s, running: false, finishedAt: new Date().toISOString() }));
|
|
52
|
+
},
|
|
53
|
+
reset: () => {
|
|
54
|
+
clear();
|
|
55
|
+
setState(IDLE);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default function JobPanelDemo() {
|
|
61
|
+
const job = usePretendJob();
|
|
62
|
+
return (
|
|
63
|
+
<div className="grid max-w-xl gap-4">
|
|
64
|
+
<div className="flex flex-wrap gap-2">
|
|
65
|
+
<Button variant="secondary" onClick={() => job.start(false)} disabled={job.running}>
|
|
66
|
+
Generate a launch plan
|
|
67
|
+
</Button>
|
|
68
|
+
<Button variant="quiet" onClick={() => job.start(true)} disabled={job.running}>
|
|
69
|
+
Run one that fails
|
|
70
|
+
</Button>
|
|
71
|
+
</div>
|
|
72
|
+
<JobPanel job={job} title="Drafting the launch plan" doneTitle="Launch plan drafted" onRetry={() => job.start(false)} />
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { Button, Icon, cx } from "../../ui";
|
|
5
|
+
|
|
6
|
+
// JobPanel — the panel that follows work the founder asked the app's
|
|
7
|
+
// agent for (useJob in lib/hooks.js, README §5 "Commissioning work"). It
|
|
8
|
+
// keeps the four rules a hand-drawn panel breaks: it is not there when
|
|
9
|
+
// nothing runs, it shows each step's label (the founder's phrase, never
|
|
10
|
+
// the tool's name), the agent's text is its account and not the
|
|
11
|
+
// deliverable (the rows are), and Stop / Close are always one press away.
|
|
12
|
+
//
|
|
13
|
+
// const job = useJob();
|
|
14
|
+
// <Button onClick={() => job.start("Draft a launch plan for Rowhouse")}>Generate a launch plan</Button>
|
|
15
|
+
// <JobPanel job={job} title="Drafting the launch plan" doneTitle="Launch plan drafted" />
|
|
16
|
+
|
|
17
|
+
function StepMark({ status }) {
|
|
18
|
+
if (status === "done") return <Icon name="check" size={14} className="shrink-0 text-success" />;
|
|
19
|
+
if (status === "failed") return <Icon name="warning" size={14} className="shrink-0 text-destructive" />;
|
|
20
|
+
return <span aria-hidden="true" className="mx-[3px] size-2 shrink-0 animate-pulse rounded-full bg-primary" />;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function JobPanel({
|
|
24
|
+
job,
|
|
25
|
+
title = "Working on it",
|
|
26
|
+
doneTitle = "Done",
|
|
27
|
+
failedTitle = "It stopped",
|
|
28
|
+
onRetry,
|
|
29
|
+
autoClose = 6000,
|
|
30
|
+
maxSteps = 6,
|
|
31
|
+
showText = true,
|
|
32
|
+
className,
|
|
33
|
+
}) {
|
|
34
|
+
const textRef = useRef(null);
|
|
35
|
+
const running = Boolean(job?.running);
|
|
36
|
+
const failed = !running && Boolean(job?.error);
|
|
37
|
+
const done = !running && !failed && Boolean(job?.finishedAt);
|
|
38
|
+
|
|
39
|
+
// The agent's account grows at the bottom; keep the newest line in view.
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const el = textRef.current;
|
|
42
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
43
|
+
}, [job?.text]);
|
|
44
|
+
|
|
45
|
+
// A clean finish closes itself: the rows it made are the answer.
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!done || !autoClose || !job?.reset) return undefined;
|
|
48
|
+
const t = setTimeout(() => job.reset(), autoClose);
|
|
49
|
+
return () => clearTimeout(t);
|
|
50
|
+
}, [done, autoClose, job?.finishedAt]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
51
|
+
|
|
52
|
+
if (!job || !(running || failed || done)) return null;
|
|
53
|
+
|
|
54
|
+
const steps = job.steps ?? [];
|
|
55
|
+
const hidden = Math.max(0, steps.length - maxSteps);
|
|
56
|
+
const visible = steps.slice(hidden);
|
|
57
|
+
const heading = running ? title : failed ? failedTitle : doneTitle;
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<section
|
|
61
|
+
aria-label={heading}
|
|
62
|
+
className={cx("rounded-lg border bg-card p-4 text-card-foreground", failed ? "border-destructive/50" : "border-border", className)}
|
|
63
|
+
>
|
|
64
|
+
<div className="flex items-start justify-between gap-3">
|
|
65
|
+
<div className="min-w-0">
|
|
66
|
+
<p aria-live="polite" className="flex items-center gap-2 text-sm font-semibold">
|
|
67
|
+
{running ? <StepMark status="running" /> : <StepMark status={failed ? "failed" : "done"} />}
|
|
68
|
+
<span className="min-w-0">{heading}</span>
|
|
69
|
+
</p>
|
|
70
|
+
{failed ? <p className="mt-1 text-sm text-muted-foreground">{job.error}</p> : null}
|
|
71
|
+
</div>
|
|
72
|
+
<div className="flex shrink-0 gap-2">
|
|
73
|
+
{failed && onRetry ? (
|
|
74
|
+
<Button size="sm" variant="secondary" onClick={onRetry}>
|
|
75
|
+
Try again
|
|
76
|
+
</Button>
|
|
77
|
+
) : null}
|
|
78
|
+
{running ? (
|
|
79
|
+
<Button size="sm" variant="quiet" onClick={() => job.cancel?.()}>
|
|
80
|
+
Stop
|
|
81
|
+
</Button>
|
|
82
|
+
) : (
|
|
83
|
+
<Button size="sm" variant="quiet" onClick={() => job.reset?.()}>
|
|
84
|
+
Close
|
|
85
|
+
</Button>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
{visible.length ? (
|
|
90
|
+
<ol className="mt-3 grid gap-1.5 text-sm">
|
|
91
|
+
{hidden > 0 ? <li className="text-xs text-muted-foreground">{hidden} earlier {hidden === 1 ? "step" : "steps"}</li> : null}
|
|
92
|
+
{visible.map((step, i) => (
|
|
93
|
+
<li key={`${hidden + i}-${step.name}`} className="flex items-center gap-2">
|
|
94
|
+
<StepMark status={step.status} />
|
|
95
|
+
<span className={step.status === "running" ? "text-foreground" : "text-muted-foreground"}>{step.label ?? "Working"}</span>
|
|
96
|
+
</li>
|
|
97
|
+
))}
|
|
98
|
+
</ol>
|
|
99
|
+
) : null}
|
|
100
|
+
{showText && job.text ? (
|
|
101
|
+
<div
|
|
102
|
+
ref={textRef}
|
|
103
|
+
className="mt-3 max-h-32 overflow-y-auto whitespace-pre-wrap border-t border-border pt-3 text-xs leading-relaxed text-muted-foreground"
|
|
104
|
+
>
|
|
105
|
+
{job.text}
|
|
106
|
+
</div>
|
|
107
|
+
) : null}
|
|
108
|
+
</section>
|
|
109
|
+
);
|
|
110
|
+
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// `npx makersclaw <command>` — parse the arguments, run the command in
|
|
2
|
+
// src/install.mjs, say what happened. Text is written for a person or an
|
|
3
|
+
// agent reading a terminal: short lines, what changed, what to run next.
|
|
4
|
+
// --json is the same result as one object, for the platform.
|
|
5
|
+
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { CliError, add, diff, init, remove, update } from "./install.mjs";
|
|
8
|
+
import { loadItems, loadTemplate, packageInfo, readmeOf, searchItems, suggest, templateNames } from "./registry.mjs";
|
|
9
|
+
|
|
10
|
+
const COMMANDS = {
|
|
11
|
+
init: "init",
|
|
12
|
+
add: "add",
|
|
13
|
+
remove: "remove",
|
|
14
|
+
rm: "remove",
|
|
15
|
+
search: "search",
|
|
16
|
+
find: "search",
|
|
17
|
+
view: "view",
|
|
18
|
+
show: "view",
|
|
19
|
+
list: "list",
|
|
20
|
+
ls: "list",
|
|
21
|
+
diff: "diff",
|
|
22
|
+
update: "update",
|
|
23
|
+
upgrade: "update",
|
|
24
|
+
help: "help",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const VALUE_FLAGS = new Map([
|
|
28
|
+
["--cwd", "cwd"],
|
|
29
|
+
["-t", "template"],
|
|
30
|
+
["--template", "template"],
|
|
31
|
+
["--type", "type"],
|
|
32
|
+
["--limit", "limit"],
|
|
33
|
+
]);
|
|
34
|
+
const BOOL_FLAGS = new Map([
|
|
35
|
+
["--json", "json"],
|
|
36
|
+
["--force", "force"],
|
|
37
|
+
["-f", "force"],
|
|
38
|
+
["--major", "major"],
|
|
39
|
+
["--adopt", "adopt"],
|
|
40
|
+
["--yes", "yes"],
|
|
41
|
+
["-y", "yes"],
|
|
42
|
+
["--help", "help"],
|
|
43
|
+
["-h", "help"],
|
|
44
|
+
["--version", "version"],
|
|
45
|
+
["-v", "version"],
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
export function parseArgs(argv) {
|
|
49
|
+
const opts = { positionals: [] };
|
|
50
|
+
for (let i = 0; i < argv.length; i++) {
|
|
51
|
+
const arg = argv[i];
|
|
52
|
+
const eq = arg.indexOf("=");
|
|
53
|
+
const flag = arg.startsWith("--") && eq > 0 ? arg.slice(0, eq) : arg;
|
|
54
|
+
if (VALUE_FLAGS.has(flag)) {
|
|
55
|
+
const value = eq > 0 && arg.startsWith("--") ? arg.slice(eq + 1) : argv[++i];
|
|
56
|
+
if (value === undefined) throw new CliError(`${flag} needs a value`);
|
|
57
|
+
opts[VALUE_FLAGS.get(flag)] = value;
|
|
58
|
+
} else if (BOOL_FLAGS.has(arg)) {
|
|
59
|
+
opts[BOOL_FLAGS.get(arg)] = true;
|
|
60
|
+
} else if (arg.startsWith("-") && arg !== "-") {
|
|
61
|
+
throw new CliError(`unknown option ${arg}`, "makersclaw help lists them");
|
|
62
|
+
} else {
|
|
63
|
+
opts.positionals.push(arg);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const [word, ...rest] = opts.positionals;
|
|
67
|
+
opts.command = word === undefined ? (opts.version ? "version" : "help") : COMMANDS[word] ?? word;
|
|
68
|
+
opts.args = rest;
|
|
69
|
+
return opts;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function helpText() {
|
|
73
|
+
const { version } = packageInfo();
|
|
74
|
+
return `makersclaw ${version} — the Makersclaw app template, its kit and its blocks
|
|
75
|
+
|
|
76
|
+
npx makersclaw init [dir] [-t next] lay the template: base files, the kit, the check
|
|
77
|
+
npx makersclaw add <name...> install blocks, or restore a kit component
|
|
78
|
+
npx makersclaw search <words...> find blocks and components by what they do
|
|
79
|
+
npx makersclaw view <name> one item: props, data, an example
|
|
80
|
+
npx makersclaw list [--type block] everything in this version
|
|
81
|
+
npx makersclaw remove <block...> uninstall blocks no file imports
|
|
82
|
+
npx makersclaw diff what update would change in this app
|
|
83
|
+
npx makersclaw update [--major] lay this version's fixed files and blocks
|
|
84
|
+
[--adopt] …or take on an app laid before makersclaw (kit era)
|
|
85
|
+
|
|
86
|
+
--cwd <dir> the app (default: this folder) --json one JSON object
|
|
87
|
+
--force init over an app that exists (your own files stay)
|
|
88
|
+
templates: ${templateNames().join(", ")}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const clip = (text, n = 88) => {
|
|
92
|
+
const one = String(text ?? "").replace(/\s+/g, " ").trim();
|
|
93
|
+
return one.length > n ? `${one.slice(0, n - 1)}…` : one;
|
|
94
|
+
};
|
|
95
|
+
const plural = (n, word, many = `${word}s`) => `${n} ${n === 1 ? word : many}`;
|
|
96
|
+
const listLine = (paths, n = 6) => (paths.length > n ? `${paths.slice(0, n).join(", ")} … +${paths.length - n}` : paths.join(", "));
|
|
97
|
+
|
|
98
|
+
function packageLines(changes) {
|
|
99
|
+
if (!changes.length) return [];
|
|
100
|
+
return [` package.json: ${changes.map((c) => `${c.name} ${c.from ?? "(new)"} → ${c.to}`).join(" · ")}`];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function layText(r, verb) {
|
|
104
|
+
const head =
|
|
105
|
+
verb === "update"
|
|
106
|
+
? `makersclaw ${r.from ?? "?"} → ${r.version} · template ${r.template}`
|
|
107
|
+
: `makersclaw ${r.version} · template ${r.template}${r.from ? ` (was ${r.from})` : ""}`;
|
|
108
|
+
const lines = [head];
|
|
109
|
+
lines.push(
|
|
110
|
+
r.written.length
|
|
111
|
+
? ` wrote ${plural(r.written.length, "file")}${verb === "init" && r.written.length > 8 ? "" : `: ${listLine(r.written)}`}`
|
|
112
|
+
: " nothing to write — every file already matches this version",
|
|
113
|
+
);
|
|
114
|
+
if (r.kept.length) lines.push(` kept yours: ${r.kept.join(", ")}`);
|
|
115
|
+
if (r.removed.length) lines.push(` removed (no longer shipped): ${listLine(r.removed)}`);
|
|
116
|
+
if (r.blocks.length) lines.push(` blocks: ${r.blocks.join(", ")}`);
|
|
117
|
+
lines.push(...packageLines(r.package));
|
|
118
|
+
lines.push(` ${plural(r.pinned, "fixed file")} pinned in check/pins.json · makersclaw.json records ${r.version}`);
|
|
119
|
+
for (const n of r.notes) lines.push(` note: ${n}`);
|
|
120
|
+
const next = [];
|
|
121
|
+
if (r.install) next.push("npm install");
|
|
122
|
+
if (verb === "init" && !r.from) next.push("design app/shell.jsx and the surfaces", "find blocks: npx makersclaw search <what the surface needs>");
|
|
123
|
+
else next.push("npm run check");
|
|
124
|
+
lines.push(`next: ${next.join(" · ")}`);
|
|
125
|
+
return lines.join("\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function addText(r) {
|
|
129
|
+
const lines = [];
|
|
130
|
+
for (const a of r.added) {
|
|
131
|
+
const where = a.type === "block" ? `components/blocks/${a.name}/` : a.files.join(", ");
|
|
132
|
+
const state = a.changed === 0 ? "already installed, unchanged" : `${plural(a.files.length, "file")} pinned`;
|
|
133
|
+
lines.push(`${a.requested ? "added" : "added (required)"} ${a.name} (${a.type}) → ${where} · ${state}`);
|
|
134
|
+
lines.push(` ${a.import}`);
|
|
135
|
+
}
|
|
136
|
+
lines.push(...packageLines(r.package));
|
|
137
|
+
if (r.install) lines.push("next: npm install");
|
|
138
|
+
for (const u of r.usage) if (u.readme) lines.push("", u.readme.trimEnd());
|
|
139
|
+
return lines.join("\n");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function itemLine(item) {
|
|
143
|
+
return ` ${item.name.padEnd(18)} ${item.type.padEnd(10)} ${clip(item.description, 72)}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function searchText(query, matches) {
|
|
147
|
+
if (!matches.length) return `nothing matches "${query}" — npx makersclaw list shows everything`;
|
|
148
|
+
const lines = [`${plural(matches.length, "match", "matches")} for "${query}"`];
|
|
149
|
+
for (const { item } of matches) {
|
|
150
|
+
lines.push(itemLine(item));
|
|
151
|
+
lines.push(` ${"".padEnd(29)} ${item.type === "block" ? `npx makersclaw add ${item.name}` : "in every app — import it"}`);
|
|
152
|
+
}
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function viewText(item, readme) {
|
|
157
|
+
const { version } = packageInfo();
|
|
158
|
+
const head = `${item.name} · ${item.type} · makersclaw ${version}`;
|
|
159
|
+
if (readme) return `${head}\n\n${readme.trimEnd()}`;
|
|
160
|
+
const folder = item.type === "part" ? "parts" : "ui";
|
|
161
|
+
return [
|
|
162
|
+
head,
|
|
163
|
+
"",
|
|
164
|
+
`${item.title} — ${item.description}`,
|
|
165
|
+
` file: ${item.files.map((f) => f.to).join(", ")} (every app has it; add ${item.name} restores it)`,
|
|
166
|
+
` import { ${item.title.split(",")[0].trim()} } from "../components/${folder}";`,
|
|
167
|
+
].join("\n");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function listText(items) {
|
|
171
|
+
const { version } = packageInfo();
|
|
172
|
+
const groups = [
|
|
173
|
+
["component", "components — in every app (components/ui)"],
|
|
174
|
+
["part", "parts — in every app, bound to the data layer (components/parts)"],
|
|
175
|
+
["block", "blocks — add the one a surface needs (components/blocks/<name>)"],
|
|
176
|
+
];
|
|
177
|
+
const lines = [`makersclaw ${version}`];
|
|
178
|
+
for (const [type, label] of groups) {
|
|
179
|
+
const of = items.filter((i) => i.type === type);
|
|
180
|
+
if (!of.length) continue;
|
|
181
|
+
lines.push("", label);
|
|
182
|
+
for (const i of of) lines.push(` ${i.name.padEnd(18)} ${clip(i.description, 80)}`);
|
|
183
|
+
}
|
|
184
|
+
return lines.join("\n");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function diffText(r) {
|
|
188
|
+
const lines = [`makersclaw ${r.from} → ${r.to} · ${r.compatible ? "compatible" : "changes the contract — update --major"}`];
|
|
189
|
+
if (!r.entries.length && !r.package.length) {
|
|
190
|
+
lines.push(` nothing to update — the app matches makersclaw ${r.to}`);
|
|
191
|
+
return lines.join("\n");
|
|
192
|
+
}
|
|
193
|
+
for (const e of r.entries) lines.push(` ${e.item.padEnd(14)} ${e.status.padEnd(8)} ${e.path}${e.detail ? ` — ${e.detail}` : ""}`);
|
|
194
|
+
for (const c of r.package) lines.push(` ${"package.json".padEnd(14)} ${c.key}.${c.name}: ${c.from ?? "(new)"} → ${c.to}`);
|
|
195
|
+
lines.push(`next: npx makersclaw update${r.compatible ? "" : " --major"}`);
|
|
196
|
+
return lines.join("\n");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function templateFor(opts, fallback = "next") {
|
|
200
|
+
const name = opts.template ?? fallback;
|
|
201
|
+
const tpl = loadTemplate(name);
|
|
202
|
+
if (!tpl) throw new CliError(`no template "${name}"`, `templates: ${templateNames().join(", ")}`);
|
|
203
|
+
return tpl;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Run one command. Returns { result, text } or throws a CliError. */
|
|
207
|
+
export function run(opts) {
|
|
208
|
+
const cwd = resolve(opts.cwd ?? process.cwd());
|
|
209
|
+
switch (opts.command) {
|
|
210
|
+
case "help":
|
|
211
|
+
return { result: { help: helpText() }, text: helpText() };
|
|
212
|
+
case "version":
|
|
213
|
+
return { result: packageInfo(), text: packageInfo().version };
|
|
214
|
+
case "init": {
|
|
215
|
+
const dir = opts.args[0] ? resolve(cwd, opts.args[0]) : cwd;
|
|
216
|
+
const result = init(dir, { template: opts.template ?? "next", force: Boolean(opts.force) });
|
|
217
|
+
return { result: { dir, ...result }, text: layText(result, "init") };
|
|
218
|
+
}
|
|
219
|
+
case "update": {
|
|
220
|
+
const result = update(cwd, { major: Boolean(opts.major), adopt: Boolean(opts.adopt) });
|
|
221
|
+
return { result: { dir: cwd, ...result }, text: layText(result, "update") };
|
|
222
|
+
}
|
|
223
|
+
case "add": {
|
|
224
|
+
const result = add(cwd, opts.args);
|
|
225
|
+
return { result: { dir: cwd, ...result }, text: addText(result) };
|
|
226
|
+
}
|
|
227
|
+
case "remove": {
|
|
228
|
+
const result = remove(cwd, opts.args);
|
|
229
|
+
return {
|
|
230
|
+
result: { dir: cwd, ...result },
|
|
231
|
+
text: `removed ${opts.args.join(", ")} — ${plural(result.removed.length, "file")}; installed now: ${result.blocks.join(", ") || "no blocks"}`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
case "diff": {
|
|
235
|
+
const result = diff(cwd);
|
|
236
|
+
return { result: { dir: cwd, ...result }, text: diffText(result) };
|
|
237
|
+
}
|
|
238
|
+
case "search": {
|
|
239
|
+
const query = opts.args.join(" ").trim();
|
|
240
|
+
if (!query) throw new CliError("search needs words: what the surface has to do", 'npx makersclaw search "drag cards between stages"');
|
|
241
|
+
const items = loadItems(templateFor(opts));
|
|
242
|
+
const limit = opts.limit ? Math.max(1, Number(opts.limit) || 8) : 8;
|
|
243
|
+
const matches = searchItems(items, query, { type: opts.type, limit });
|
|
244
|
+
return {
|
|
245
|
+
result: { query, matches: matches.map(({ item, score }) => ({ ...summary(item), score })) },
|
|
246
|
+
text: searchText(query, matches),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
case "view": {
|
|
250
|
+
const name = opts.args[0];
|
|
251
|
+
if (!name) throw new CliError("view needs a name", "npx makersclaw list shows every name");
|
|
252
|
+
const items = loadItems(templateFor(opts));
|
|
253
|
+
const item = items.find((i) => i.name === name);
|
|
254
|
+
if (!item) {
|
|
255
|
+
const close = suggest(items, name);
|
|
256
|
+
throw new CliError(`no block or component named "${name}"`, close.length ? `did you mean ${close.join(", ")}?` : "npx makersclaw search <words>");
|
|
257
|
+
}
|
|
258
|
+
const readme = readmeOf(item);
|
|
259
|
+
return { result: { item: summary(item), readme }, text: viewText(item, readme) };
|
|
260
|
+
}
|
|
261
|
+
case "list": {
|
|
262
|
+
const items = loadItems(templateFor(opts)).filter((i) => !opts.type || i.type === opts.type);
|
|
263
|
+
return { result: { version: packageInfo().version, items: items.map(summary) }, text: listText(items) };
|
|
264
|
+
}
|
|
265
|
+
default:
|
|
266
|
+
throw new CliError(`unknown command "${opts.command}"`, "makersclaw help lists them");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function summary(item) {
|
|
271
|
+
return {
|
|
272
|
+
name: item.name,
|
|
273
|
+
type: item.type,
|
|
274
|
+
title: item.title,
|
|
275
|
+
description: item.description,
|
|
276
|
+
useWhen: item.useWhen || undefined,
|
|
277
|
+
tags: item.tags,
|
|
278
|
+
requires: item.requires.length ? item.requires : undefined,
|
|
279
|
+
exports: item.exports?.length ? item.exports : undefined,
|
|
280
|
+
files: item.files.map((f) => f.to),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function main(argv, { stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
285
|
+
let opts = { json: argv.includes("--json") };
|
|
286
|
+
try {
|
|
287
|
+
opts = parseArgs(argv);
|
|
288
|
+
if (opts.help && opts.command !== "help") opts = { ...opts, command: "help" };
|
|
289
|
+
const { result, text } = run(opts);
|
|
290
|
+
stdout.write(opts.json ? `${JSON.stringify({ ok: true, command: opts.command, ...result }, null, 2)}\n` : `${text}\n`);
|
|
291
|
+
return 0;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (!(error instanceof CliError)) throw error;
|
|
294
|
+
if (opts.json) {
|
|
295
|
+
stdout.write(`${JSON.stringify({ ok: false, command: opts.command ?? null, error: error.message, next: error.next }, null, 2)}\n`);
|
|
296
|
+
} else {
|
|
297
|
+
stderr.write(`error: ${error.message}\n${error.next ? `next: ${error.next}\n` : ""}`);
|
|
298
|
+
}
|
|
299
|
+
return 1;
|
|
300
|
+
}
|
|
301
|
+
}
|