leglas 0.2.0 → 0.3.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 +71 -19
- package/dist/bin.js +109 -55
- package/dist/index.js +109 -55
- package/dist/shell/assets/{index-r7sGM7WL.js → index-Bl87Vq9G.js} +2 -2
- package/dist/shell/index.html +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ and sessions that clean up after themselves.
|
|
|
52
52
|
- Ask for changes without leaving the comparison: describe what you
|
|
53
53
|
want on the direction you're looking at, and Leglas turns it into a
|
|
54
54
|
precise request for your agent, file path included. Leave
|
|
55
|
-
`leglas watch --run "claude -p {prompt}"` running in another terminal
|
|
55
|
+
`npx leglas watch --run "claude -p {prompt}"` running in another terminal
|
|
56
56
|
and your agent picks each request up as you send it. In Claude Code,
|
|
57
57
|
the Leglas MCP server can also push each request straight into your
|
|
58
58
|
open session as a channel event (channels are a research preview:
|
|
@@ -76,57 +76,81 @@ preview of your app root. Add a config file to compare more than one thing.
|
|
|
76
76
|
|
|
77
77
|
It works with whatever you're building in. Leglas never imports or
|
|
78
78
|
executes your framework, so the target can be Next, Vite, Remix,
|
|
79
|
-
SvelteKit, Astro, or a folder of static files.
|
|
80
|
-
|
|
79
|
+
SvelteKit, Astro, or a folder of static files.
|
|
80
|
+
|
|
81
|
+
## Install
|
|
82
|
+
|
|
83
|
+
There is nothing you have to install: `npx leglas` fetches the CLI on
|
|
84
|
+
first use and starts from npm's cache after that, and every instruction
|
|
85
|
+
Leglas writes for agents uses the same form, so a fresh clone works
|
|
86
|
+
with no setup at all. Requires Node 24 or newer.
|
|
87
|
+
|
|
88
|
+
Two optional upgrades:
|
|
89
|
+
|
|
90
|
+
- `npm install -D leglas` pins the version in a project. Teammates and
|
|
91
|
+
CI get the same Leglas from their normal install, and `npx` resolves
|
|
92
|
+
the local copy from then on.
|
|
93
|
+
- `npm install -g leglas` is for typing `leglas` without the prefix.
|
|
81
94
|
|
|
82
95
|
## Working with coding agents
|
|
83
96
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
The fastest way in is the agent skill:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
npx skills add FredAmartey/leglas
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
One install, and your agent recognises "give me a few directions for the
|
|
104
|
+
pricing page" as a Leglas exploration in any project, including ones
|
|
105
|
+
that have never seen Leglas. It sets the project up itself and gets to
|
|
106
|
+
work.
|
|
107
|
+
|
|
108
|
+
In a project, run `npx leglas init` once. It writes a section into your
|
|
109
|
+
project's `AGENTS.md`, creates a starter config, and gitignores Leglas's
|
|
110
|
+
working directory. That section travels with the repo, so Claude Code,
|
|
111
|
+
Cursor, Codex, or whatever you switch to next opens the project already
|
|
112
|
+
knowing how to add design directions to it. Every command accepts
|
|
113
|
+
`--json` and prints a single machine-readable envelope, so agents drive
|
|
114
|
+
the same CLI you do.
|
|
91
115
|
|
|
92
116
|
The instructions center on one rule: add beside what exists, never
|
|
93
117
|
rewrite it. Two directions that rewrite the same file cannot render from
|
|
94
118
|
one server, and asking an agent to "make the hero calmer" tempts it to
|
|
95
119
|
edit the hero. The supporting commands:
|
|
96
120
|
|
|
97
|
-
- `leglas explore hero --count 6` briefs the exploration: what the set
|
|
121
|
+
- `npx leglas explore hero --count 6` briefs the exploration: what the set
|
|
98
122
|
is for, why it only works if the six genuinely disagree, and how each
|
|
99
123
|
direction registers. Unbriefed, six requests come back as six variants
|
|
100
124
|
of one idea. With `--based-on "Aurora"` the goal flips: six deliberate
|
|
101
125
|
variants of a direction you already like, and drifting into a new
|
|
102
126
|
direction is the failure. The designs themselves are the agent's;
|
|
103
127
|
Leglas prescribes none.
|
|
104
|
-
- `leglas new hero --from src/Hero.tsx` scaffolds a switcher under
|
|
128
|
+
- `npx leglas new hero --from src/Hero.tsx` scaffolds a switcher under
|
|
105
129
|
`.leglas/variants/hero/`. With `--from`, the baseline re-exports your
|
|
106
130
|
real component, so you never compare against a stale copy. Leglas
|
|
107
131
|
prints the one line to add in your component and does not edit it,
|
|
108
132
|
because rewriting a file it does not understand is how a tool breaks a
|
|
109
133
|
codebase. Scaffolded branch points return the fallback in production
|
|
110
134
|
builds, so a committed one cannot expose an unreleased direction.
|
|
111
|
-
- `leglas classify --change package.json --rewrite src/theme.css` answers
|
|
135
|
+
- `npx leglas classify --change package.json --rewrite src/theme.css` answers
|
|
112
136
|
where a direction should live before it is written. Changing
|
|
113
137
|
dependencies, build configuration, or an existing file's behaviour
|
|
114
138
|
cannot be additive, so those directions build on their own git branch
|
|
115
139
|
and register with `leglas add --branch`. Everything else stays in-app,
|
|
116
140
|
where switching is instant.
|
|
117
|
-
- `leglas show "Aurora" --json` answers for one direction: its entry, the
|
|
141
|
+
- `npx leglas show "Aurora" --json` answers for one direction: its entry, the
|
|
118
142
|
source file behind it, the variants based on it, what it is being compared
|
|
119
143
|
against, and anything still pending on it. Copying a direction from the
|
|
120
144
|
rail hands over a block that ends in this command, so an agent given the
|
|
121
145
|
block can go and get the rest.
|
|
122
|
-
- `leglas keep "Aurora" --to src/components/hero.tsx` moves the winner
|
|
146
|
+
- `npx leglas keep "Aurora" --to src/components/hero.tsx` moves the winner
|
|
123
147
|
into real source and ends the exploration.
|
|
124
148
|
|
|
125
149
|
Asking for a change works from the interface too. Type what you want
|
|
126
150
|
changed into the field under the rail (or press `R`) and Leglas composes a
|
|
127
151
|
prompt naming the direction and the file behind it, copies it to your
|
|
128
152
|
clipboard, and queues it. The direction it means is the one highlighted
|
|
129
|
-
directly above the field. Your agent drains the queue with `leglas requests --json` and clears
|
|
153
|
+
directly above the field. Your agent drains the queue with `npx leglas requests --json` and clears
|
|
130
154
|
it with `--clear`. Leglas runs no model of its own; your agent already
|
|
131
155
|
knows your conventions and your taste.
|
|
132
156
|
|
|
@@ -136,6 +160,9 @@ For agent hosts that cannot run shell commands, `leglas-mcp` exposes the
|
|
|
136
160
|
same operations as MCP tools over stdio: `start`, `add`, `list`, `show`,
|
|
137
161
|
`classify`, `explore`, `scaffold`, `keep`, `requests`, and `init`. Each
|
|
138
162
|
tool calls exactly what the CLI calls and returns the same envelope.
|
|
163
|
+
`watch` is the one command with no tool behind it: it is a loop that
|
|
164
|
+
holds a terminal open, and on a host that speaks channels the server
|
|
165
|
+
already pushes each request into the session as it arrives.
|
|
139
166
|
|
|
140
167
|
```sh
|
|
141
168
|
claude mcp add leglas -- npx -y leglas-mcp
|
|
@@ -147,9 +174,34 @@ Or in `.mcp.json`:
|
|
|
147
174
|
{ "mcpServers": { "leglas": { "command": "npx", "args": ["-y", "leglas-mcp"] } } }
|
|
148
175
|
```
|
|
149
176
|
|
|
150
|
-
The host's working directory names the project
|
|
151
|
-
|
|
152
|
-
|
|
177
|
+
The host's working directory names the project, the same contract as the
|
|
178
|
+
CLI. A host that starts the server somewhere else is asked where the
|
|
179
|
+
project is, over MCP roots. The `start` tool boots the viewer and returns
|
|
180
|
+
its URL, and anything it started stops when the session ends.
|
|
181
|
+
|
|
182
|
+
### As an Agent Plugin
|
|
183
|
+
|
|
184
|
+
The repository is also an [Agent Plugin](https://agent-plugins.org), the
|
|
185
|
+
open standard for shipping Agent Skills and MCP server configuration in
|
|
186
|
+
one format. Clients that implement it install the skill and the server
|
|
187
|
+
together, instead of the two steps above. It is a layout rather than a
|
|
188
|
+
build: `plugin.json` and `mcp.json` at the root, the skill in
|
|
189
|
+
`skills/leglas/`, nothing generated.
|
|
190
|
+
|
|
191
|
+
An Agent Plugins client starts a plugin's server in the plugin's own
|
|
192
|
+
install directory rather than the project, so on that path the working
|
|
193
|
+
directory names a copy of Leglas and nothing else. The server therefore
|
|
194
|
+
takes the project from the workspace the host declares over MCP roots,
|
|
195
|
+
and the working directory only when it sits inside one. If a host offers
|
|
196
|
+
neither, `LEGLAS_PROJECT_DIR` names the project outright; without it the
|
|
197
|
+
tools report that there is no project rather than writing into a plugin
|
|
198
|
+
cache. `mcp.json` passes `${PLUGIN_ROOT}` for exactly that check, and
|
|
199
|
+
nothing else.
|
|
200
|
+
|
|
201
|
+
The plugin's version covers the skill and the configuration, not the
|
|
202
|
+
server it launches: `npx` fetches the current `leglas-mcp` the same way
|
|
203
|
+
every `npx leglas` in these instructions fetches the current CLI, which
|
|
204
|
+
keeps both faces of Leglas on one version in a project they share.
|
|
153
205
|
|
|
154
206
|
## Configuration
|
|
155
207
|
|
package/dist/bin.js
CHANGED
|
@@ -51,7 +51,7 @@ function parseNew(rest) {
|
|
|
51
51
|
if (surface === void 0) {
|
|
52
52
|
return {
|
|
53
53
|
kind: "error",
|
|
54
|
-
message: "leglas new needs a surface name, for example: leglas new hero"
|
|
54
|
+
message: "leglas new needs a surface name, for example: npx leglas new hero"
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
return { kind: "new", surface, print, json, from };
|
|
@@ -134,7 +134,7 @@ function parseClassify(rest) {
|
|
|
134
134
|
if (changes.length === 0) {
|
|
135
135
|
return {
|
|
136
136
|
kind: "error",
|
|
137
|
-
message: "leglas classify needs what the direction will touch, for example: leglas classify --change package.json --rewrite src/theme.css"
|
|
137
|
+
message: "leglas classify needs what the direction will touch, for example: npx leglas classify --change package.json --rewrite src/theme.css"
|
|
138
138
|
};
|
|
139
139
|
}
|
|
140
140
|
return { kind: "classify", changes, json };
|
|
@@ -209,7 +209,7 @@ function parseArgs(argv) {
|
|
|
209
209
|
if (title === void 0) {
|
|
210
210
|
return {
|
|
211
211
|
kind: "error",
|
|
212
|
-
message: 'leglas keep needs a direction title, for example: leglas keep "Aurora" --to src/components/hero.tsx'
|
|
212
|
+
message: 'leglas keep needs a direction title, for example: npx leglas keep "Aurora" --to src/components/hero.tsx'
|
|
213
213
|
};
|
|
214
214
|
}
|
|
215
215
|
if (to === void 0) {
|
|
@@ -259,7 +259,7 @@ function parseArgs(argv) {
|
|
|
259
259
|
if (surface === void 0) {
|
|
260
260
|
return {
|
|
261
261
|
kind: "error",
|
|
262
|
-
message: "leglas explore needs a surface name, for example: leglas explore hero --count 6"
|
|
262
|
+
message: "leglas explore needs a surface name, for example: npx leglas explore hero --count 6"
|
|
263
263
|
};
|
|
264
264
|
}
|
|
265
265
|
return { kind: "explore", surface, count, basedOn, json };
|
|
@@ -300,7 +300,7 @@ function parseArgs(argv) {
|
|
|
300
300
|
if (title === void 0) {
|
|
301
301
|
return {
|
|
302
302
|
kind: "error",
|
|
303
|
-
message: 'leglas show needs a direction title, for example: leglas show "Aurora" --json'
|
|
303
|
+
message: 'leglas show needs a direction title, for example: npx leglas show "Aurora" --json'
|
|
304
304
|
};
|
|
305
305
|
}
|
|
306
306
|
return { kind: "show", title, json };
|
|
@@ -522,12 +522,12 @@ function isExplorationFile(path) {
|
|
|
522
522
|
}
|
|
523
523
|
var CHECKOUT_STEPS = [
|
|
524
524
|
"Build the direction on its own branch: git switch -c <branch>, commit it there, switch back.",
|
|
525
|
-
'Register it: leglas add --title "<title>" --url "/" --branch <branch>.',
|
|
525
|
+
'Register it: npx leglas add --title "<title>" --url "/" --branch <branch>.',
|
|
526
526
|
"Make sure the config sets devCommand (with {port}), so Leglas can start the checkout."
|
|
527
527
|
];
|
|
528
528
|
var IN_APP_STEPS = [
|
|
529
529
|
"Author it additively under .leglas/variants/<surface>/, beside the existing directions.",
|
|
530
|
-
'Register it: leglas add --title "<title>" --url "/?v-<surface>=<direction>".'
|
|
530
|
+
'Register it: npx leglas add --title "<title>" --url "/?v-<surface>=<direction>".'
|
|
531
531
|
];
|
|
532
532
|
function classifyDirection(input) {
|
|
533
533
|
const checkout = (reason) => ({ level: "checkout", reason, steps: CHECKOUT_STEPS });
|
|
@@ -983,7 +983,12 @@ async function removeRequest(cwd, id) {
|
|
|
983
983
|
return true;
|
|
984
984
|
}
|
|
985
985
|
async function clearRequests(cwd) {
|
|
986
|
-
await
|
|
986
|
+
const requests = await readRequests(cwd);
|
|
987
|
+
const pending = requests.filter((request) => request.status !== "picked-up");
|
|
988
|
+
const cleared = requests.length - pending.length;
|
|
989
|
+
if (cleared > 0)
|
|
990
|
+
await writeQueue(cwd, pending);
|
|
991
|
+
return { cleared, pending: pending.length };
|
|
987
992
|
}
|
|
988
993
|
|
|
989
994
|
// ../server/dist/renames.js
|
|
@@ -1022,7 +1027,7 @@ function resolveTitle(input, titles, renames) {
|
|
|
1022
1027
|
import { createReadStream, existsSync as existsSync2, statSync } from "fs";
|
|
1023
1028
|
import http2 from "http";
|
|
1024
1029
|
import net3 from "net";
|
|
1025
|
-
import { extname, join as join6, normalize } from "path";
|
|
1030
|
+
import { extname, join as join6, normalize, relative as relative2 } from "path";
|
|
1026
1031
|
var LEGLAS_PREFIX = "/leglas";
|
|
1027
1032
|
var DEFAULT_PORT = 4100;
|
|
1028
1033
|
var PORT_ATTEMPTS = 20;
|
|
@@ -1074,8 +1079,8 @@ function probe(target, timeoutMs = 1e3) {
|
|
|
1074
1079
|
});
|
|
1075
1080
|
}
|
|
1076
1081
|
function serveFrom(res, dir, relativePath) {
|
|
1077
|
-
const
|
|
1078
|
-
const candidate = join6(dir,
|
|
1082
|
+
const relative4 = normalize(relativePath).replace(/^(\.\.[/\\])+/, "");
|
|
1083
|
+
const candidate = join6(dir, relative4);
|
|
1079
1084
|
if (!candidate.startsWith(dir))
|
|
1080
1085
|
return false;
|
|
1081
1086
|
if (!existsSync2(candidate) || !statSync(candidate).isFile())
|
|
@@ -1088,9 +1093,36 @@ function serveFrom(res, dir, relativePath) {
|
|
|
1088
1093
|
return true;
|
|
1089
1094
|
}
|
|
1090
1095
|
function serveShellFile(res, shellDir, urlPath) {
|
|
1091
|
-
const
|
|
1092
|
-
const isRoot =
|
|
1093
|
-
return serveFrom(res, shellDir, isRoot ? "index.html" :
|
|
1096
|
+
const relative4 = normalize(urlPath.slice(LEGLAS_PREFIX.length)).replace(/^(\.\.[/\\])+/, "");
|
|
1097
|
+
const isRoot = relative4 === "" || relative4 === "." || relative4 === "/";
|
|
1098
|
+
return serveFrom(res, shellDir, isRoot ? "index.html" : relative4);
|
|
1099
|
+
}
|
|
1100
|
+
function snapshotConfig(cwd) {
|
|
1101
|
+
const path = findConfigFile(cwd);
|
|
1102
|
+
if (path === null)
|
|
1103
|
+
return null;
|
|
1104
|
+
try {
|
|
1105
|
+
return { path, mtimeMs: statSync(path).mtimeMs };
|
|
1106
|
+
} catch {
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
function configStalenessNotice(cwd, boot, current) {
|
|
1111
|
+
if (boot === null && current === null)
|
|
1112
|
+
return null;
|
|
1113
|
+
if (boot === null && current !== null) {
|
|
1114
|
+
const label = relative2(cwd, current.path) || current.path;
|
|
1115
|
+
return `${label} appeared after Leglas started. Restart leglas to pick it up.`;
|
|
1116
|
+
}
|
|
1117
|
+
if (boot !== null && current === null) {
|
|
1118
|
+
const label = relative2(cwd, boot.path) || boot.path;
|
|
1119
|
+
return `${label} was removed after Leglas started. Restart leglas to run without it.`;
|
|
1120
|
+
}
|
|
1121
|
+
if (boot !== null && current !== null && (boot.path !== current.path || boot.mtimeMs !== current.mtimeMs)) {
|
|
1122
|
+
const label = relative2(cwd, current.path) || current.path;
|
|
1123
|
+
return `${label} changed after Leglas started. Restart leglas to pick it up.`;
|
|
1124
|
+
}
|
|
1125
|
+
return null;
|
|
1094
1126
|
}
|
|
1095
1127
|
var PLACEHOLDER = `<!doctype html>
|
|
1096
1128
|
<meta charset="utf-8">
|
|
@@ -1136,12 +1168,17 @@ async function startServer(options) {
|
|
|
1136
1168
|
const { config, configErrors = [], shellDir = null, project = "", cwd = process.cwd(), fileMounts = /* @__PURE__ */ new Map() } = options;
|
|
1137
1169
|
const target = config?.devServer ?? "http://localhost:3000";
|
|
1138
1170
|
const proxy = createProxyHandler({ target });
|
|
1171
|
+
const bootConfigSnapshot = snapshotConfig(cwd);
|
|
1139
1172
|
let lastSeen = null;
|
|
1140
1173
|
const server = http2.createServer((req, res) => {
|
|
1141
1174
|
const url = req.url ?? "/";
|
|
1142
1175
|
const path = url.split("?")[0] ?? "/";
|
|
1143
1176
|
if (path === `${LEGLAS_PREFIX}/api/config`) {
|
|
1144
1177
|
const boot = config?.previews ?? [];
|
|
1178
|
+
const errors = [...configErrors];
|
|
1179
|
+
const notice = configStalenessNotice(cwd, bootConfigSnapshot, snapshotConfig(cwd));
|
|
1180
|
+
if (notice !== null)
|
|
1181
|
+
errors.push(notice);
|
|
1145
1182
|
return void readLocalPreviews(cwd).then(({ previews: local }) => {
|
|
1146
1183
|
const known = new Set(boot.map((preview) => preview.title));
|
|
1147
1184
|
const fresh = local.filter((preview) => !known.has(preview.title) && preview.branch === void 0 && preview.file === void 0);
|
|
@@ -1149,13 +1186,13 @@ async function startServer(options) {
|
|
|
1149
1186
|
project,
|
|
1150
1187
|
devServer: target,
|
|
1151
1188
|
previews: [...boot, ...fresh],
|
|
1152
|
-
errors
|
|
1189
|
+
errors
|
|
1153
1190
|
});
|
|
1154
1191
|
}).catch(() => sendJson(res, 200, {
|
|
1155
1192
|
project,
|
|
1156
1193
|
devServer: target,
|
|
1157
1194
|
previews: boot,
|
|
1158
|
-
errors
|
|
1195
|
+
errors
|
|
1159
1196
|
}));
|
|
1160
1197
|
}
|
|
1161
1198
|
if (path === `${LEGLAS_PREFIX}/api/request` && req.method === "POST") {
|
|
@@ -1229,21 +1266,28 @@ async function startServer(options) {
|
|
|
1229
1266
|
const rest = path.slice(FILES_PREFIX.length + 1);
|
|
1230
1267
|
const slash = rest.indexOf("/");
|
|
1231
1268
|
const slug = slash === -1 ? rest : rest.slice(0, slash);
|
|
1232
|
-
let
|
|
1269
|
+
let relative4 = slash === -1 ? "" : rest.slice(slash + 1);
|
|
1233
1270
|
try {
|
|
1234
|
-
|
|
1271
|
+
relative4 = decodeURIComponent(relative4);
|
|
1235
1272
|
} catch {
|
|
1236
|
-
|
|
1273
|
+
relative4 = "";
|
|
1237
1274
|
}
|
|
1238
1275
|
const dir = fileMounts.get(slug);
|
|
1239
|
-
if (dir !== void 0 &&
|
|
1276
|
+
if (dir !== void 0 && relative4 !== "" && serveFrom(res, dir, relative4))
|
|
1240
1277
|
return;
|
|
1241
1278
|
res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
1242
1279
|
return res.end("Leglas: no such preview file.");
|
|
1243
1280
|
}
|
|
1281
|
+
if (path.startsWith(`${LEGLAS_PREFIX}/api/`)) {
|
|
1282
|
+
return sendJson(res, 404, { error: "No such Leglas API path." });
|
|
1283
|
+
}
|
|
1244
1284
|
if (path === LEGLAS_PREFIX || path.startsWith(`${LEGLAS_PREFIX}/`)) {
|
|
1245
1285
|
if (shellDir !== null && serveShellFile(res, shellDir, path))
|
|
1246
1286
|
return;
|
|
1287
|
+
if (shellDir !== null) {
|
|
1288
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
1289
|
+
return res.end("Leglas: no such path.");
|
|
1290
|
+
}
|
|
1247
1291
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
1248
1292
|
return res.end(PLACEHOLDER);
|
|
1249
1293
|
}
|
|
@@ -1515,8 +1559,8 @@ The set exists to be chosen from, and the choice only means something if the dir
|
|
|
1515
1559
|
One trap, seen every time this goes wrong: a set collapses toward whichever direction is built first. Decide all ${count} before building any, and if two would read as the same direction at a glance, replace one of them.` : `Build ${count} variations of the "${basedOn}" direction for "${surface}".
|
|
1516
1560
|
|
|
1517
1561
|
The set exists to pick a variant of a direction already chosen, so every variation must stay recognisably that direction. The trap here is drift: change enough and the comparison stops being about the variant. Vary each one deliberately and hold everything else still; if a variation grows into a new direction, it belongs in its own exploration instead.`;
|
|
1518
|
-
const register = basedOn === null ? ` leglas add --title "<name>" --url "/?v-${slug}=<key>" --note "<the idea, one line>"` : ` leglas add --title "<name>" --url "/?v-${slug}=<key>" --based-on ${JSON.stringify(basedOn)} --note "<the idea, one line>"`;
|
|
1519
|
-
const mechanics = `Each one is its own file under .leglas/variants/${slug}/, listed in the DIRECTIONS map in that folder's switch file. If there is no switch file yet, run \`leglas new ${slug}\` first. Register each one the moment it renders, not the set at the end. The interface picks a registration up within seconds, so whoever asked watches the set fill in:
|
|
1562
|
+
const register = basedOn === null ? ` npx leglas add --title "<name>" --url "/?v-${slug}=<key>" --note "<the idea, one line>"` : ` npx leglas add --title "<name>" --url "/?v-${slug}=<key>" --based-on ${JSON.stringify(basedOn)} --note "<the idea, one line>"`;
|
|
1563
|
+
const mechanics = `Each one is its own file under .leglas/variants/${slug}/, listed in the DIRECTIONS map in that folder's switch file. If there is no switch file yet, run \`npx leglas new ${slug}\` first. Register each one the moment it renders, not the set at the end. The interface picks a registration up within seconds, so whoever asked watches the set fill in:
|
|
1520
1564
|
|
|
1521
1565
|
${register}
|
|
1522
1566
|
|
|
@@ -1572,46 +1616,48 @@ When asked for design variations, alternatives, or "a few options":
|
|
|
1572
1616
|
If a direction cannot be additive, because it must change dependencies,
|
|
1573
1617
|
change build configuration, or rewrite an existing file's behaviour, ask
|
|
1574
1618
|
where it should live before writing it:
|
|
1575
|
-
\`leglas classify --change package.json --rewrite src/theme.css --json\`.
|
|
1619
|
+
\`npx leglas classify --change package.json --rewrite src/theme.css --json\`.
|
|
1576
1620
|
When the answer is \`checkout\`, build the direction on its own git branch
|
|
1577
|
-
and register it with \`leglas add --title "\u2026" --url "/" --branch <branch>\`
|
|
1621
|
+
and register it with \`npx leglas add --title "\u2026" --url "/" --branch <branch>\`
|
|
1578
1622
|
(the config needs \`devCommand\` with \`{port}\`). Everything below is the
|
|
1579
1623
|
ordinary, in-app path.
|
|
1580
|
-
2. Run \`leglas explore <surface> --count <n>\` first, adding
|
|
1624
|
+
2. Run \`npx leglas explore <surface> --count <n>\` first, adding
|
|
1581
1625
|
\`--based-on "<title>"\` when the user wants variations of a direction they
|
|
1582
1626
|
already like. It prints what the set needs and how to register it. In
|
|
1583
1627
|
short: new directions must genuinely disagree with each other, variants of
|
|
1584
1628
|
one must not, and either way decide the whole set before building any of
|
|
1585
1629
|
it. The designs themselves are yours.
|
|
1586
1630
|
3. If the surface has no switcher yet, run
|
|
1587
|
-
\`leglas new <surface> --from <the component that renders it today>\`. It
|
|
1631
|
+
\`npx leglas new <surface> --from <the component that renders it today>\`. It
|
|
1588
1632
|
writes one under \`.leglas/variants/<surface>/\` and prints the single line
|
|
1589
1633
|
to add where that surface renders. \`--from\` makes the baseline re-export
|
|
1590
1634
|
the real component rather than copying it, so it stays live.
|
|
1591
|
-
4. Before building, make sure the interface is up. If
|
|
1592
|
-
already running, tell the user to run
|
|
1593
|
-
rather than when the set is done: the rail picks up each
|
|
1594
|
-
within seconds, so they get to watch the exploration fill in.
|
|
1635
|
+
4. Before building, make sure the interface is up. If Leglas is not
|
|
1636
|
+
already running, tell the user to run \`npx leglas\`, and hand them the
|
|
1637
|
+
URL now rather than when the set is done: the rail picks up each
|
|
1638
|
+
registration within seconds, so they get to watch the exploration fill in.
|
|
1595
1639
|
5. Build one direction at a time: its own file beside the others in
|
|
1596
1640
|
\`.leglas/variants/<surface>/\`, listed in the \`DIRECTIONS\` map in that
|
|
1597
1641
|
folder's \`switch\` file, then registered the moment it renders:
|
|
1598
|
-
\`leglas add --title "Aurora" --url "/?v-<surface>=aurora" --note "One line on the idea."\`
|
|
1642
|
+
\`npx leglas add --title "Aurora" --url "/?v-<surface>=aurora" --note "One line on the idea."\`
|
|
1599
1643
|
Register each direction as it lands, never the whole set at the end. To
|
|
1600
1644
|
the user watching the rail, a batch at the end is minutes of nothing and
|
|
1601
1645
|
then everything at once.
|
|
1602
1646
|
|
|
1603
|
-
When the user asks to change one direction, check \`leglas requests --json\`
|
|
1647
|
+
When the user asks to change one direction, check \`npx leglas requests --json\`
|
|
1604
1648
|
first: they may have described it from the interface, and the request names the
|
|
1605
|
-
exact file.
|
|
1649
|
+
exact file. Acknowledge them with \`npx leglas requests --clear\` once done: it
|
|
1650
|
+
drops what you collected and reports anything the user typed while you worked,
|
|
1651
|
+
which is yours to collect and do next.
|
|
1606
1652
|
|
|
1607
1653
|
If the user wants requests handled the moment they are typed, without relaying
|
|
1608
|
-
each one, tell them about \`leglas watch --run "claude -p {prompt}"\` (any
|
|
1654
|
+
each one, tell them about \`npx leglas watch --run "claude -p {prompt}"\` (any
|
|
1609
1655
|
agent command works; {prompt} receives the request). It runs in their
|
|
1610
1656
|
terminal, hands each request to that command as it arrives, and the interface
|
|
1611
1657
|
shows the request's progress.
|
|
1612
1658
|
|
|
1613
1659
|
When the user picks a winner, run
|
|
1614
|
-
\`leglas keep "<title>" --to <path in real source>\`. It moves that direction
|
|
1660
|
+
\`npx leglas keep "<title>" --to <path in real source>\`. It moves that direction
|
|
1615
1661
|
out of the ignored directory, deletes the rest of the exploration, and drops
|
|
1616
1662
|
them from the rail. Then change their component to use the kept component
|
|
1617
1663
|
instead of the switcher.
|
|
@@ -1622,14 +1668,14 @@ Useful to know:
|
|
|
1622
1668
|
ships. Move a direction into real source only when it wins.
|
|
1623
1669
|
- If the project has no running app yet, a direction can be a plain HTML
|
|
1624
1670
|
file: write it under \`.leglas/pages/\` and register it with
|
|
1625
|
-
\`leglas add --title "Aurora" --file .leglas/pages/aurora.html\`. Leglas
|
|
1671
|
+
\`npx leglas add --title "Aurora" --file .leglas/pages/aurora.html\`. Leglas
|
|
1626
1672
|
serves the file itself, so no dev server is needed. Sibling assets in the
|
|
1627
1673
|
same directory resolve normally.
|
|
1628
1674
|
- Titles identify previews and must be unique. The user may rename one in the
|
|
1629
1675
|
rail, which renames it on their machine only; the commands answer to either
|
|
1630
1676
|
name, so use whichever they said.
|
|
1631
|
-
- \`leglas list\` shows every direction, shared and local.
|
|
1632
|
-
- \`leglas show "<title>" --json\` answers for one of them: the file behind it,
|
|
1677
|
+
- \`npx leglas list\` shows every direction, shared and local.
|
|
1678
|
+
- \`npx leglas show "<title>" --json\` answers for one of them: the file behind it,
|
|
1633
1679
|
the variants based on it, what it is being compared against, and anything
|
|
1634
1680
|
they have asked for that is not done yet. Run it when handed a direction you
|
|
1635
1681
|
did not register yourself.
|
|
@@ -1639,7 +1685,7 @@ Useful to know:
|
|
|
1639
1685
|
${AGENTS_MARKER_END}
|
|
1640
1686
|
`;
|
|
1641
1687
|
var STARTER_CONFIG = `// Previews are URLs of your own app. Add one per direction you want to
|
|
1642
|
-
// compare, then run \`leglas\`.
|
|
1688
|
+
// compare, then run \`npx leglas\`.
|
|
1643
1689
|
export default {
|
|
1644
1690
|
// Where your dev server is. Override at the command line with --user-port.
|
|
1645
1691
|
devServer: "http://localhost:3000",
|
|
@@ -1737,7 +1783,7 @@ function planKeep(options) {
|
|
|
1737
1783
|
if (!winner) {
|
|
1738
1784
|
return {
|
|
1739
1785
|
ok: false,
|
|
1740
|
-
error: `No direction called ${JSON.stringify(options.title)}. Run leglas list to see them.`
|
|
1786
|
+
error: `No direction called ${JSON.stringify(options.title)}. Run npx leglas list to see them.`
|
|
1741
1787
|
};
|
|
1742
1788
|
}
|
|
1743
1789
|
const from = targetFor(winner.url);
|
|
@@ -1785,7 +1831,7 @@ function resolveOrExplain(input, titles, renames) {
|
|
|
1785
1831
|
}
|
|
1786
1832
|
return {
|
|
1787
1833
|
ok: false,
|
|
1788
|
-
error: `No direction called ${JSON.stringify(input)}. Renaming one in the rail only renames it here, and it still answers to its title in the config, which its reference block quotes. leglas list shows every title.`
|
|
1834
|
+
error: `No direction called ${JSON.stringify(input)}. Renaming one in the rail only renames it here, and it still answers to its title in the config, which its reference block quotes. npx leglas list shows every title.`
|
|
1789
1835
|
};
|
|
1790
1836
|
}
|
|
1791
1837
|
|
|
@@ -1935,7 +1981,7 @@ async function runNew(options, deps) {
|
|
|
1935
1981
|
deps.log("Then register them so they appear in the interface:");
|
|
1936
1982
|
deps.log("");
|
|
1937
1983
|
for (const preview of plan.previews) {
|
|
1938
|
-
deps.log(` leglas add --title ${JSON.stringify(preview.title)} --url ${JSON.stringify(preview.url)}`);
|
|
1984
|
+
deps.log(` npx leglas add --title ${JSON.stringify(preview.title)} --url ${JSON.stringify(preview.url)}`);
|
|
1939
1985
|
}
|
|
1940
1986
|
return { exitCode: 0, written };
|
|
1941
1987
|
}
|
|
@@ -1964,7 +2010,7 @@ async function runAdd(options, deps) {
|
|
|
1964
2010
|
const local = await readLocalPreviews(options.cwd);
|
|
1965
2011
|
const titles = new Set([...shared, ...local.previews].map((preview) => preview.title));
|
|
1966
2012
|
if (!titles.has(options.preview.basedOn)) {
|
|
1967
|
-
const error = `--based-on names ${JSON.stringify(options.preview.basedOn)}, which is not a registered direction. leglas list shows what exists.`;
|
|
2013
|
+
const error = `--based-on names ${JSON.stringify(options.preview.basedOn)}, which is not a registered direction. npx leglas list shows what exists.`;
|
|
1968
2014
|
if (options.json) envelope(deps, false, { error });
|
|
1969
2015
|
else deps.error(error);
|
|
1970
2016
|
return { exitCode: 1 };
|
|
@@ -1997,6 +2043,7 @@ async function runAdd(options, deps) {
|
|
|
1997
2043
|
local: true,
|
|
1998
2044
|
...options.preview.branch === void 0 ? {} : { branch: options.preview.branch },
|
|
1999
2045
|
...options.preview.file === void 0 ? {} : { file: options.preview.file },
|
|
2046
|
+
note: options.preview.branch === void 0 && options.preview.file === void 0 ? "A running interface picks this up within seconds." : "Restart Leglas to see this preview: branch checkouts and file mounts are built when Leglas starts.",
|
|
2000
2047
|
...needsDevCommand ? { warning: "The config sets no devCommand, so Leglas cannot start this branch yet. Add devCommand (with {port}) to the config." } : {}
|
|
2001
2048
|
});
|
|
2002
2049
|
} else {
|
|
@@ -2012,7 +2059,7 @@ async function runAdd(options, deps) {
|
|
|
2012
2059
|
if (options.preview.branch === void 0 && options.preview.file === void 0) {
|
|
2013
2060
|
deps.log("Local to this machine. A running interface picks it up within seconds.");
|
|
2014
2061
|
} else {
|
|
2015
|
-
deps.log("Local to this machine. Restart Leglas to see it, or run leglas list.");
|
|
2062
|
+
deps.log("Local to this machine. Restart Leglas to see it, or run npx leglas list.");
|
|
2016
2063
|
}
|
|
2017
2064
|
}
|
|
2018
2065
|
return { exitCode: 0 };
|
|
@@ -2047,7 +2094,7 @@ async function runList(options, deps) {
|
|
|
2047
2094
|
return { exitCode: errors.length === 0 ? 0 : 1 };
|
|
2048
2095
|
}
|
|
2049
2096
|
if (previews.length === 0) {
|
|
2050
|
-
deps.log("No previews yet. Add one with leglas add, or list them in leglas.config.ts.");
|
|
2097
|
+
deps.log("No previews yet. Add one with npx leglas add, or list them in leglas.config.ts.");
|
|
2051
2098
|
} else {
|
|
2052
2099
|
const width = Math.max(...previews.map((preview) => preview.title.length));
|
|
2053
2100
|
for (const preview of previews) {
|
|
@@ -2062,9 +2109,16 @@ async function runList(options, deps) {
|
|
|
2062
2109
|
}
|
|
2063
2110
|
async function runRequests(options, deps) {
|
|
2064
2111
|
if (options.clear) {
|
|
2065
|
-
await clearRequests(options.cwd);
|
|
2066
|
-
if (options.json) envelope(deps, true, { cleared
|
|
2067
|
-
else
|
|
2112
|
+
const { cleared, pending } = await clearRequests(options.cwd);
|
|
2113
|
+
if (options.json) envelope(deps, true, { cleared, pending });
|
|
2114
|
+
else {
|
|
2115
|
+
deps.log(cleared === 1 ? "Cleared 1 request." : `Cleared ${cleared} requests.`);
|
|
2116
|
+
if (pending > 0) {
|
|
2117
|
+
deps.log(
|
|
2118
|
+
`${pending} arrived while you worked. Run npx leglas requests to collect ${pending === 1 ? "it" : "them"}.`
|
|
2119
|
+
);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2068
2122
|
return { exitCode: 0 };
|
|
2069
2123
|
}
|
|
2070
2124
|
const requests = await collectRequests(options.cwd);
|
|
@@ -2081,7 +2135,7 @@ async function runRequests(options, deps) {
|
|
|
2081
2135
|
if (request.target !== null) deps.log(` ${request.target}`);
|
|
2082
2136
|
}
|
|
2083
2137
|
deps.log("");
|
|
2084
|
-
deps.log("Run leglas requests --json to get the full prompts, then --clear when done.");
|
|
2138
|
+
deps.log("Run npx leglas requests --json to get the full prompts, then --clear when done.");
|
|
2085
2139
|
return { exitCode: 0 };
|
|
2086
2140
|
}
|
|
2087
2141
|
|
|
@@ -2107,7 +2161,7 @@ function planShow({ title, previews, requests }) {
|
|
|
2107
2161
|
if (!found) {
|
|
2108
2162
|
return {
|
|
2109
2163
|
ok: false,
|
|
2110
|
-
error: `No direction called ${JSON.stringify(title)}. Run leglas list to see them.`
|
|
2164
|
+
error: `No direction called ${JSON.stringify(title)}. Run npx leglas list to see them.`
|
|
2111
2165
|
};
|
|
2112
2166
|
}
|
|
2113
2167
|
const variants = previews.filter((preview) => preview.basedOn === title).map(describe);
|
|
@@ -2186,7 +2240,7 @@ async function runShow(options, deps) {
|
|
|
2186
2240
|
deps.log(` Pending, not yet done (${plan.requests.length}):`);
|
|
2187
2241
|
for (const request of plan.requests) deps.log(` ${request.status} ${request.intent}`);
|
|
2188
2242
|
deps.log("");
|
|
2189
|
-
deps.log(" Run leglas requests --json for the full prompts.");
|
|
2243
|
+
deps.log(" Run npx leglas requests --json for the full prompts.");
|
|
2190
2244
|
}
|
|
2191
2245
|
return { exitCode: 0 };
|
|
2192
2246
|
}
|
|
@@ -2199,7 +2253,7 @@ import { dirname as dirname7, join as join12 } from "path";
|
|
|
2199
2253
|
// src/watch.ts
|
|
2200
2254
|
var WATCH_PATH = ".leglas/watch.json";
|
|
2201
2255
|
var PROMPT_TOKEN = "{prompt}";
|
|
2202
|
-
var EXAMPLE = `leglas watch --run "claude -p ${PROMPT_TOKEN}"`;
|
|
2256
|
+
var EXAMPLE = `npx leglas watch --run "claude -p ${PROMPT_TOKEN}"`;
|
|
2203
2257
|
function tokenize(template) {
|
|
2204
2258
|
const tokens = [];
|
|
2205
2259
|
let current = "";
|
|
@@ -2311,7 +2365,7 @@ async function runWatch(options, deps) {
|
|
|
2311
2365
|
const raw = options.run ?? saved;
|
|
2312
2366
|
if (raw === null) {
|
|
2313
2367
|
deps.error(
|
|
2314
|
-
'Watch needs an agent command the first time: leglas watch --run "claude -p {prompt}"'
|
|
2368
|
+
'Watch needs an agent command the first time: npx leglas watch --run "claude -p {prompt}"'
|
|
2315
2369
|
);
|
|
2316
2370
|
return { exitCode: 1 };
|
|
2317
2371
|
}
|
|
@@ -2398,7 +2452,7 @@ async function runWatch(options, deps) {
|
|
|
2398
2452
|
// src/run.ts
|
|
2399
2453
|
import { existsSync as existsSync5 } from "fs";
|
|
2400
2454
|
import { createRequire } from "module";
|
|
2401
|
-
import { basename as basename3, dirname as dirname8, join as join13, relative as
|
|
2455
|
+
import { basename as basename3, dirname as dirname8, join as join13, relative as relative3 } from "path";
|
|
2402
2456
|
import { fileURLToPath } from "url";
|
|
2403
2457
|
function findShellDir() {
|
|
2404
2458
|
const bundled = join13(dirname8(fileURLToPath(import.meta.url)), "shell");
|
|
@@ -2511,7 +2565,7 @@ async function run2(options, deps) {
|
|
|
2511
2565
|
})
|
|
2512
2566
|
);
|
|
2513
2567
|
} else {
|
|
2514
|
-
const configLabel = loaded.path === null ? "no config file, previewing the app root" :
|
|
2568
|
+
const configLabel = loaded.path === null ? "no config file, previewing the app root" : relative3(options.cwd, loaded.path) || loaded.path;
|
|
2515
2569
|
deps.log(`Leglas ${url}`);
|
|
2516
2570
|
deps.log(
|
|
2517
2571
|
`app ${devServer}${app !== null ? " (started by Leglas)" : health.reachable ? "" : " (not reachable)"}`
|