ucode-agent 1.36.0 → 1.37.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 +38 -14
- package/package.json +1 -1
- package/src/tools/files.js +10 -9
- package/src/tools/scaffold.js +118 -109
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ corner:
|
|
|
27
27
|
add a dark mode toggle that remembers the choice
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
v1.
|
|
30
|
+
v1.36.0
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
A light crosses the wordmark once as it opens, and the three lines under the box
|
|
@@ -107,12 +107,14 @@ of what ucode is asked to do, and it answers far sooner than the big reasoning
|
|
|
107
107
|
models. Switch to Ultra when a problem needs the million-token window more than
|
|
108
108
|
the speed.
|
|
109
109
|
|
|
110
|
-
**
|
|
111
|
-
requests" is routine
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
**The model you chose is the model you keep.** Free endpoints are shared and
|
|
111
|
+
"too many requests" is routine, so ucode waits it out with growing pauses and
|
|
112
|
+
comes back to the same model. It does not quietly hand your build to a
|
|
113
|
+
different one: a build that starts on one model and finishes on another
|
|
114
|
+
finishes to a different standard, and the swap lands exactly when you are least
|
|
115
|
+
placed to work out why the output changed. Set `UCODE_FALLBACK=1` if you would
|
|
116
|
+
rather it moved down the list — North Mini Code, Nemotron 3.5 Lightning, Super,
|
|
117
|
+
Ultra — when a model stays busy.
|
|
116
118
|
|
|
117
119
|
## What it does
|
|
118
120
|
|
|
@@ -211,6 +213,13 @@ same link. Needs a token from vercel.com/account/tokens in `~/.ucode/.env` as
|
|
|
211
213
|
sunset, graphite, violet or citrus — each a full light and dark palette with its
|
|
212
214
|
own font, so apps stop looking like the same default blue.
|
|
213
215
|
|
|
216
|
+
**Every turn can be taken back.** `/undo` puts back every file the last turn
|
|
217
|
+
changed — a rewritten file returns byte for byte, a file that did not exist
|
|
218
|
+
before is removed again. Each write keeps the original the first time that turn
|
|
219
|
+
touches it, so what comes back is the state before the turn rather than before
|
|
220
|
+
the last of six edits to the same file. An agent that writes to your disk on
|
|
221
|
+
its own should be able to take it back, whether or not the project has git.
|
|
222
|
+
|
|
214
223
|
**It notices when it is going round in circles.** The same failing edit, an edit
|
|
215
224
|
that changes nothing, or a build failing on the same errors three times gets a
|
|
216
225
|
firm, specific note; if that does not work, the turn moves to another model.
|
|
@@ -238,13 +247,23 @@ written, its install starts in the background while the rest of the app is
|
|
|
238
247
|
still being written. An install the model asks for later waits for that one
|
|
239
248
|
instead of running twice, and anything run in that folder waits for it too.
|
|
240
249
|
|
|
241
|
-
**It
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
250
|
+
**It opens what it built and uses it.** Every app build ends with a look — not
|
|
251
|
+
when the model remembers to ask for one, but as part of the same pass that
|
|
252
|
+
type-checks. It opens the app in a real browser (the Edge or Chrome already on
|
|
253
|
+
your machine, so there is nothing extra to download) at 375px and 1440px, and
|
|
254
|
+
serves the folder itself when there is no dev server to point at, which is how
|
|
255
|
+
a three-file app gets checked at all.
|
|
256
|
+
|
|
257
|
+
Then it uses the app. It types into the first field, presses Enter, and clicks
|
|
258
|
+
the button that submits — and if the page gains no elements, changes no text
|
|
259
|
+
and stores nothing, that is reported as the thing to fix before anything else.
|
|
260
|
+
A page that renders and has no working behaviour passes a type check, a syntax
|
|
261
|
+
check and a screenshot; the only way to find out is to press something.
|
|
262
|
+
|
|
263
|
+
It also reports console errors, failed requests, content that spills off a
|
|
264
|
+
phone screen, broken images and unlabeled controls, saves screenshots to
|
|
265
|
+
`.ucode/screenshots`, and has Nemotron Nano Omni review them the way a designer
|
|
266
|
+
would. The model fixes what it finds before calling the app done. Both widths load at once, and the designer review — the slow part — runs
|
|
248
267
|
on the first look at an app in each request and is skipped, not waited on, when
|
|
249
268
|
the vision model is busy. The look after the fixes re-runs only the fast checks:
|
|
250
269
|
a few seconds.
|
|
@@ -355,6 +374,11 @@ Everything after the frontmatter is the instruction.
|
|
|
355
374
|
| `/session delete 2,5` | delete saved conversations by number (or `d d` in the list) |
|
|
356
375
|
| `/new` | save this one and start fresh |
|
|
357
376
|
| `/remember <note>` | add a standing note to this project's `UCODE.md` |
|
|
377
|
+
| `/undo` | put back every file the last turn changed |
|
|
378
|
+
| `/look [url]` | open the running app and report what is on the page |
|
|
379
|
+
| `/deploy [folder]` | put the app online and get its link |
|
|
380
|
+
| `/stats` | time, steps and tokens this session |
|
|
381
|
+
| `/doctor` | check that everything ucode needs is working |
|
|
358
382
|
| `/skills` | what it knows how to do, and what is loaded |
|
|
359
383
|
| `/search <query>` | look something up on the web |
|
|
360
384
|
| `/copy` | last reply to the clipboard |
|
package/package.json
CHANGED
package/src/tools/files.js
CHANGED
|
@@ -298,7 +298,7 @@ async function put(target, content, { diffMax = 16 } = {}) {
|
|
|
298
298
|
|
|
299
299
|
try {
|
|
300
300
|
await fs.mkdir(path.dirname(target.abs), { recursive: true });
|
|
301
|
-
await remember(target.abs);
|
|
301
|
+
await remember(target.abs);
|
|
302
302
|
await fs.writeFile(target.abs, content, 'utf8');
|
|
303
303
|
} catch (err) {
|
|
304
304
|
throw fsFailure(err, attempted, target.show);
|
|
@@ -454,12 +454,13 @@ function replaceOnce(text, { old_string, new_string }, { show, attempted, label
|
|
|
454
454
|
fix: 'edit_file replaces existing text. Use write_file to create a file.',
|
|
455
455
|
});
|
|
456
456
|
}
|
|
457
|
+
// An edit whose two halves are the same asks for the file to stay as it is,
|
|
458
|
+
// which it will. Refusing that was a hard failure, and the model answered it
|
|
459
|
+
// by sending the same edit again — the stuck detector carries a special case
|
|
460
|
+
// for exactly this loop. Saying "already done" ends it in one step.
|
|
457
461
|
if (old_string === new_string) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
failed: `${prefix}old_string and new_string are identical, so the edit would change nothing.`,
|
|
461
|
-
fix: 'Set new_string to the text you actually want there.',
|
|
462
|
-
});
|
|
462
|
+
const found = text.indexOf(old_string);
|
|
463
|
+
return { text, at: found < 0 ? 1 : toLines(text.slice(0, found)).length, loose: false };
|
|
463
464
|
}
|
|
464
465
|
|
|
465
466
|
// Models write \n. A file checked out on Windows is often \r\n, and then an
|
|
@@ -557,7 +558,7 @@ export async function editFile({ path: p, old_string, new_string }) {
|
|
|
557
558
|
});
|
|
558
559
|
|
|
559
560
|
try {
|
|
560
|
-
await remember(target.abs);
|
|
561
|
+
await remember(target.abs);
|
|
561
562
|
await fs.writeFile(target.abs, text, 'utf8');
|
|
562
563
|
} catch (err) {
|
|
563
564
|
throw fsFailure(err, attempted, target.show);
|
|
@@ -631,7 +632,7 @@ export async function multiEdit({ path: p, edits }) {
|
|
|
631
632
|
}
|
|
632
633
|
|
|
633
634
|
try {
|
|
634
|
-
await remember(target.abs);
|
|
635
|
+
await remember(target.abs);
|
|
635
636
|
await fs.writeFile(target.abs, text, 'utf8');
|
|
636
637
|
} catch (err) {
|
|
637
638
|
throw fsFailure(err, attempted, target.show);
|
|
@@ -720,7 +721,7 @@ export async function editFiles({ files }) {
|
|
|
720
721
|
|
|
721
722
|
for (const { target, text } of planned) {
|
|
722
723
|
try {
|
|
723
|
-
await remember(target.abs);
|
|
724
|
+
await remember(target.abs);
|
|
724
725
|
await fs.writeFile(target.abs, text, 'utf8');
|
|
725
726
|
} catch (err) {
|
|
726
727
|
throw fsFailure(err, `editing ${target.show}`, target.show);
|
package/src/tools/scaffold.js
CHANGED
|
@@ -108,88 +108,88 @@ async function copyTree(from, to, fill) {
|
|
|
108
108
|
* globals.css and layout.tsx. Apps stop looking like the same default blue.
|
|
109
109
|
* Returns the preset used, or null when the starter has none.
|
|
110
110
|
*/
|
|
111
|
-
/**
|
|
112
|
-
* The file in each starter that carries the design, not just some of the rules.
|
|
113
|
-
*
|
|
114
|
-
* next-shadcn has globals.css, which applyDesign re-tints. plain-html has one
|
|
115
|
-
* stylesheet and it is the whole design system: the palette, a spacing scale,
|
|
116
|
-
* radii, motion timings, focus rings, a reduced-motion rule and a breakpoint.
|
|
117
|
-
*/
|
|
118
|
-
const TOKEN_FILE = { 'plain-html': 'styles.css' };
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Put the starter's token block back when the app wrote over it without one.
|
|
122
|
-
*
|
|
123
|
-
* The stylesheet in plain-html is a design, not a placeholder — but `files`
|
|
124
|
-
* lands straight on top of the starter, so a model passing its own styles.css
|
|
125
|
-
* replaces the scale, the palette and the timings with whatever it typed. What
|
|
126
|
-
* comes back is hand-rolled CSS with no system behind it, and an app built on
|
|
127
|
-
* raw pixel values has uneven spacing everywhere for the rest of its life.
|
|
128
|
-
*
|
|
129
|
-
* A replacement that declares its own custom properties is left alone: that is
|
|
130
|
-
* a model doing the job properly, and second-guessing it would be worse. Only
|
|
131
|
-
* a stylesheet with no :root variables at all gets the starter's block put
|
|
132
|
-
* back above it, where every rule underneath can reach it.
|
|
133
|
-
*/
|
|
134
|
-
const ASSET_REF = /\b(href|src)=("|')(?!https?:|\/\/|\/|data:|#|mailto:|tel:)([^"']+)\2/g;
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Take the app's own folder back out of its own links.
|
|
138
|
-
*
|
|
139
|
-
* create_app wants paths relative to the project root — "todo/index.html" —
|
|
140
|
-
* because that is where every other file tool works from. The model then
|
|
141
|
-
* carries the same prefix into the markup and writes
|
|
142
|
-
* <link href="todo/styles.css"> inside todo/index.html, where it resolves to
|
|
143
|
-
* todo/todo/styles.css and 404s. The page comes up as bare markup with no
|
|
144
|
-
* stylesheet and no script: no design, no behaviour, nothing in the console
|
|
145
|
-
* but two failed requests.
|
|
146
|
-
*
|
|
147
|
-
* It is the tool's own convention leaking into the file, so the tool takes it
|
|
148
|
-
* back out. Only a leading "<folder>/" on a relative reference, which inside
|
|
149
|
-
* that folder is always wrong — absolute paths, URLs, data: and anchors are
|
|
150
|
-
* left exactly as they are.
|
|
151
|
-
*/
|
|
152
|
-
async function unprefixOwnFolder(appDir, written) {
|
|
153
|
-
const prefix = `${path.basename(appDir)}/`;
|
|
154
|
-
const fixed = [];
|
|
155
|
-
|
|
156
|
-
for (const abs of written) {
|
|
157
|
-
if (!/\.html?$/i.test(abs)) continue;
|
|
158
|
-
const text = await fs.readFile(abs, 'utf8').catch(() => null);
|
|
159
|
-
if (text === null) continue;
|
|
160
|
-
|
|
161
|
-
let hits = 0;
|
|
162
|
-
const next = text.replace(ASSET_REF, (all, attr, quote, value) => {
|
|
163
|
-
if (!value.startsWith(prefix)) return all;
|
|
164
|
-
hits++;
|
|
165
|
-
return `${attr}=${quote}${value.slice(prefix.length)}${quote}`;
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
if (hits) {
|
|
169
|
-
await fs.writeFile(abs, next, 'utf8');
|
|
170
|
-
fixed.push(`${path.relative(appDir, abs).split(path.sep).join('/')} (${hits})`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return fixed;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async function keepDesignTokens(appDir, template, written, starterCss) {
|
|
177
|
-
const rel = TOKEN_FILE[template];
|
|
178
|
-
if (!rel || !starterCss) return null;
|
|
179
|
-
|
|
180
|
-
const abs = path.join(appDir, rel);
|
|
181
|
-
if (!written.has(abs)) return null; // never overwritten
|
|
182
|
-
|
|
183
|
-
const now = await fs.readFile(abs, 'utf8').catch(() => null);
|
|
184
|
-
if (now === null || /:root\s*\{[^}]*--/.test(now)) return null;
|
|
185
|
-
|
|
186
|
-
const block = /:root\s*\{[\s\S]*?\n\}/.exec(starterCss);
|
|
187
|
-
if (!block) return null;
|
|
188
|
-
|
|
189
|
-
await fs.writeFile(abs, `${block[0]}\n\n${now}`, 'utf8');
|
|
190
|
-
return rel;
|
|
191
|
-
}
|
|
192
|
-
|
|
111
|
+
/**
|
|
112
|
+
* The file in each starter that carries the design, not just some of the rules.
|
|
113
|
+
*
|
|
114
|
+
* next-shadcn has globals.css, which applyDesign re-tints. plain-html has one
|
|
115
|
+
* stylesheet and it is the whole design system: the palette, a spacing scale,
|
|
116
|
+
* radii, motion timings, focus rings, a reduced-motion rule and a breakpoint.
|
|
117
|
+
*/
|
|
118
|
+
const TOKEN_FILE = { 'plain-html': 'styles.css' };
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Put the starter's token block back when the app wrote over it without one.
|
|
122
|
+
*
|
|
123
|
+
* The stylesheet in plain-html is a design, not a placeholder — but `files`
|
|
124
|
+
* lands straight on top of the starter, so a model passing its own styles.css
|
|
125
|
+
* replaces the scale, the palette and the timings with whatever it typed. What
|
|
126
|
+
* comes back is hand-rolled CSS with no system behind it, and an app built on
|
|
127
|
+
* raw pixel values has uneven spacing everywhere for the rest of its life.
|
|
128
|
+
*
|
|
129
|
+
* A replacement that declares its own custom properties is left alone: that is
|
|
130
|
+
* a model doing the job properly, and second-guessing it would be worse. Only
|
|
131
|
+
* a stylesheet with no :root variables at all gets the starter's block put
|
|
132
|
+
* back above it, where every rule underneath can reach it.
|
|
133
|
+
*/
|
|
134
|
+
const ASSET_REF = /\b(href|src)=("|')(?!https?:|\/\/|\/|data:|#|mailto:|tel:)([^"']+)\2/g;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Take the app's own folder back out of its own links.
|
|
138
|
+
*
|
|
139
|
+
* create_app wants paths relative to the project root — "todo/index.html" —
|
|
140
|
+
* because that is where every other file tool works from. The model then
|
|
141
|
+
* carries the same prefix into the markup and writes
|
|
142
|
+
* <link href="todo/styles.css"> inside todo/index.html, where it resolves to
|
|
143
|
+
* todo/todo/styles.css and 404s. The page comes up as bare markup with no
|
|
144
|
+
* stylesheet and no script: no design, no behaviour, nothing in the console
|
|
145
|
+
* but two failed requests.
|
|
146
|
+
*
|
|
147
|
+
* It is the tool's own convention leaking into the file, so the tool takes it
|
|
148
|
+
* back out. Only a leading "<folder>/" on a relative reference, which inside
|
|
149
|
+
* that folder is always wrong — absolute paths, URLs, data: and anchors are
|
|
150
|
+
* left exactly as they are.
|
|
151
|
+
*/
|
|
152
|
+
async function unprefixOwnFolder(appDir, written) {
|
|
153
|
+
const prefix = `${path.basename(appDir)}/`;
|
|
154
|
+
const fixed = [];
|
|
155
|
+
|
|
156
|
+
for (const abs of written) {
|
|
157
|
+
if (!/\.html?$/i.test(abs)) continue;
|
|
158
|
+
const text = await fs.readFile(abs, 'utf8').catch(() => null);
|
|
159
|
+
if (text === null) continue;
|
|
160
|
+
|
|
161
|
+
let hits = 0;
|
|
162
|
+
const next = text.replace(ASSET_REF, (all, attr, quote, value) => {
|
|
163
|
+
if (!value.startsWith(prefix)) return all;
|
|
164
|
+
hits++;
|
|
165
|
+
return `${attr}=${quote}${value.slice(prefix.length)}${quote}`;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (hits) {
|
|
169
|
+
await fs.writeFile(abs, next, 'utf8');
|
|
170
|
+
fixed.push(`${path.relative(appDir, abs).split(path.sep).join('/')} (${hits})`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return fixed;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function keepDesignTokens(appDir, template, written, starterCss) {
|
|
177
|
+
const rel = TOKEN_FILE[template];
|
|
178
|
+
if (!rel || !starterCss) return null;
|
|
179
|
+
|
|
180
|
+
const abs = path.join(appDir, rel);
|
|
181
|
+
if (!written.has(abs)) return null; // never overwritten
|
|
182
|
+
|
|
183
|
+
const now = await fs.readFile(abs, 'utf8').catch(() => null);
|
|
184
|
+
if (now === null || /:root\s*\{[^}]*--/.test(now)) return null;
|
|
185
|
+
|
|
186
|
+
const block = /:root\s*\{[\s\S]*?\n\}/.exec(starterCss);
|
|
187
|
+
if (!block) return null;
|
|
188
|
+
|
|
189
|
+
await fs.writeFile(abs, `${block[0]}\n\n${now}`, 'utf8');
|
|
190
|
+
return rel;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
193
|
export async function applyDesign(appDir, design) {
|
|
194
194
|
const dir = path.join(appDir, 'presets');
|
|
195
195
|
const names = (await fs.readdir(dir).catch(() => [])).filter((f) => f.endsWith('.json'));
|
|
@@ -304,12 +304,21 @@ export async function createApp({ folder, name, description, template = 'plain-h
|
|
|
304
304
|
} catch {
|
|
305
305
|
existing = [];
|
|
306
306
|
}
|
|
307
|
-
|
|
307
|
+
// A folder with something already in it, and the whole app passed in
|
|
308
|
+
// alongside: that is the second attempt at a build that half happened.
|
|
309
|
+
// Refusing it is how a Next.js build spent two calls being told no and then
|
|
310
|
+
// started over from nothing. The starter is already on disk — take the files
|
|
311
|
+
// and write them into it.
|
|
312
|
+
//
|
|
313
|
+
// With no files it is still a refusal, because then there is nothing to do
|
|
314
|
+
// but copy a starter over work that is already there.
|
|
315
|
+
const adopt = existing.length > 0 && given.length > 0;
|
|
316
|
+
if (existing.length && !adopt) {
|
|
308
317
|
throw new ToolFailure({
|
|
309
318
|
kind: 'not_empty',
|
|
310
319
|
attempted,
|
|
311
320
|
failed: `${target.show} already has ${existing.length} item(s) in it: ${existing.slice(0, 5).join(', ')}.`,
|
|
312
|
-
fix: 'Pick a new folder name
|
|
321
|
+
fix: 'Pick a new folder name, or pass the app in "files" to write it into the folder that is already there.',
|
|
313
322
|
});
|
|
314
323
|
}
|
|
315
324
|
|
|
@@ -320,23 +329,23 @@ export async function createApp({ folder, name, description, template = 'plain-h
|
|
|
320
329
|
__APP_DESCRIPTION__: plain(description) || display,
|
|
321
330
|
};
|
|
322
331
|
|
|
323
|
-
const copied = await copyTree(path.join(TEMPLATES, template), target.abs, fill);
|
|
332
|
+
const copied = adopt ? [] : await copyTree(path.join(TEMPLATES, template), target.abs, fill);
|
|
324
333
|
// Next.js serves static files from public/; a plain page has no such place
|
|
325
334
|
// and an empty folder in a three-file app is clutter.
|
|
326
|
-
if (template !== 'plain-html') await fs.mkdir(path.join(target.abs, 'public'), { recursive: true });
|
|
327
|
-
const look = await applyDesign(target.abs, design);
|
|
328
|
-
|
|
329
|
-
// Read before the app's own files land on top of it, so the tokens can be
|
|
330
|
-
// put back if the replacement arrives without any.
|
|
331
|
-
const starterCss = TOKEN_FILE[template]
|
|
332
|
-
? await fs.readFile(path.join(target.abs, TOKEN_FILE[template]), 'utf8').catch(() => null)
|
|
335
|
+
if (!adopt && template !== 'plain-html') await fs.mkdir(path.join(target.abs, 'public'), { recursive: true });
|
|
336
|
+
const look = await applyDesign(target.abs, design);
|
|
337
|
+
|
|
338
|
+
// Read before the app's own files land on top of it, so the tokens can be
|
|
339
|
+
// put back if the replacement arrives without any.
|
|
340
|
+
const starterCss = TOKEN_FILE[template]
|
|
341
|
+
? await fs.readFile(path.join(target.abs, TOKEN_FILE[template]), 'utf8').catch(() => null)
|
|
333
342
|
: null;
|
|
334
343
|
|
|
335
344
|
// The starter has been installed on this machine before: hard-link that
|
|
336
345
|
// tree in, which is seconds where npm is a minute. Otherwise install as
|
|
337
346
|
// usual, and keep the result so the next app is instant.
|
|
338
347
|
let linked = 0;
|
|
339
|
-
const needsInstall = template !== 'plain-html';
|
|
348
|
+
const needsInstall = !adopt && template !== 'plain-html';
|
|
340
349
|
if (install && needsInstall) {
|
|
341
350
|
// Keyed on the starter's lockfile, which is the same for every app made
|
|
342
351
|
// from it — the app's own is rewritten by npm as it installs.
|
|
@@ -359,8 +368,8 @@ export async function createApp({ folder, name, description, template = 'plain-h
|
|
|
359
368
|
// one, and round trips are nearly all of the time a build takes.
|
|
360
369
|
const mine = given;
|
|
361
370
|
const wrote = mine.length ? await batchWrite({ files: mine }) : null;
|
|
362
|
-
const written = new Set(mine.map((f) => resolveIn(f.path, 'create_app', 'files').abs));
|
|
363
|
-
const keptTokens = await keepDesignTokens(target.abs, template, written, starterCss);
|
|
371
|
+
const written = new Set(mine.map((f) => resolveIn(f.path, 'create_app', 'files').abs));
|
|
372
|
+
const keptTokens = await keepDesignTokens(target.abs, template, written, starterCss);
|
|
364
373
|
const relinked = await unprefixOwnFolder(target.abs, written);
|
|
365
374
|
|
|
366
375
|
// The starter's own files, in full, so there is never a reason to read them
|
|
@@ -377,20 +386,20 @@ export async function createApp({ folder, name, description, template = 'plain-h
|
|
|
377
386
|
const out = result(
|
|
378
387
|
`Created ${target.show} from the ${template} starter — ${copied.length} files, already known to build.\n` +
|
|
379
388
|
(look ? `Design: the ${look.name} preset (${look.summary}), font ${look.fonts?.sans ?? 'Geist'}.\n` : '') +
|
|
380
|
-
(wrote ? `\nYour ${mine.length} file${mine.length === 1 ? '' : 's'}:\n${wrote.content}\n` : '') +
|
|
381
|
-
(relinked.length
|
|
382
|
-
? `\nFixed in ${relinked.join(', ')}: links that began with "${path.basename(target.abs)}/". ` +
|
|
383
|
-
'Paths in "files" are relative to the project root, but a link inside a page is ' +
|
|
384
|
-
'relative to that page — so "index.html" beside "styles.css" links to it as ' +
|
|
385
|
-
'"styles.css", never "app/styles.css". Written that way the stylesheet and the ' +
|
|
386
|
-
'script 404 and the page comes up as bare markup.\n'
|
|
387
|
-
: '') +
|
|
388
|
-
(keptTokens
|
|
389
|
-
? `\nYour ${keptTokens} arrived with no :root block, so the starter's was kept above it — ` +
|
|
390
|
-
'the palette, the spacing scale (--s1 to --s5), the radius and the motion timings. ' +
|
|
391
|
-
'Build the rest of the stylesheet out of those variables: spacing that comes from a ' +
|
|
392
|
-
'scale is the difference between a designed page and an arranged one. Re-tint the ' +
|
|
393
|
-
'values to suit this app; do not go back to raw pixels.\n'
|
|
389
|
+
(wrote ? `\nYour ${mine.length} file${mine.length === 1 ? '' : 's'}:\n${wrote.content}\n` : '') +
|
|
390
|
+
(relinked.length
|
|
391
|
+
? `\nFixed in ${relinked.join(', ')}: links that began with "${path.basename(target.abs)}/". ` +
|
|
392
|
+
'Paths in "files" are relative to the project root, but a link inside a page is ' +
|
|
393
|
+
'relative to that page — so "index.html" beside "styles.css" links to it as ' +
|
|
394
|
+
'"styles.css", never "app/styles.css". Written that way the stylesheet and the ' +
|
|
395
|
+
'script 404 and the page comes up as bare markup.\n'
|
|
396
|
+
: '') +
|
|
397
|
+
(keptTokens
|
|
398
|
+
? `\nYour ${keptTokens} arrived with no :root block, so the starter's was kept above it — ` +
|
|
399
|
+
'the palette, the spacing scale (--s1 to --s5), the radius and the motion timings. ' +
|
|
400
|
+
'Build the rest of the stylesheet out of those variables: spacing that comes from a ' +
|
|
401
|
+
'scale is the difference between a designed page and an arranged one. Re-tint the ' +
|
|
402
|
+
'values to suit this app; do not go back to raw pixels.\n'
|
|
394
403
|
: '') +
|
|
395
404
|
(linked
|
|
396
405
|
? `Its packages are already in place (${linked.toLocaleString()} files, linked from the starter cache) — ` +
|