ucode-agent 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +399 -327
- package/package.json +6 -1
- package/skills/ui-ux/SKILL.md +2 -2
- package/src/core/doctor.js +122 -0
- package/src/core/livelog.js +113 -0
- package/src/core/loop.js +2105 -1659
- package/src/core/provider.js +93 -10
- package/src/core/stuck.js +269 -0
- package/src/core/tests.js +86 -0
- package/src/tools/blocks.js +117 -0
- package/src/tools/browser.js +121 -59
- package/src/tools/cache.js +105 -0
- package/src/tools/deploy.js +283 -0
- package/src/tools/files.js +91 -8
- package/src/tools/index.js +634 -495
- package/src/tools/rename.js +157 -0
- package/src/tools/scaffold.js +85 -6
- package/src/tools/shell.js +799 -701
- package/src/tools/symbols.js +218 -0
- package/src/tools/types.js +179 -0
- package/src/ui/activity.js +203 -0
- package/src/ui/plain.js +22 -3
- package/src/ui/screen.js +65 -19
- package/src/ui/theme.js +5 -1
- package/templates/blocks/app-shell.tsx +81 -0
- package/templates/blocks/data-table.tsx +117 -0
- package/templates/blocks/empty-state.tsx +41 -0
- package/templates/blocks/page-header.tsx +27 -0
- package/templates/blocks/stat-cards.tsx +46 -0
- package/templates/next-shadcn/TEMPLATE.md +53 -9
- package/templates/next-shadcn/_package-lock.json +1335 -148
- package/templates/next-shadcn/components.json +1 -1
- package/templates/next-shadcn/next.config.ts +2 -1
- package/templates/next-shadcn/package.json +4 -2
- package/templates/next-shadcn/presets/citrus.json +77 -0
- package/templates/next-shadcn/presets/graphite.json +77 -0
- package/templates/next-shadcn/presets/grove.json +77 -0
- package/templates/next-shadcn/presets/ocean.json +78 -0
- package/templates/next-shadcn/presets/sunset.json +77 -0
- package/templates/next-shadcn/presets/violet.json +77 -0
- package/templates/next-shadcn/src/components/ui/accordion.tsx +80 -0
- package/templates/next-shadcn/src/components/ui/alert-dialog.tsx +34 -22
- package/templates/next-shadcn/src/components/ui/avatar.tsx +7 -4
- package/templates/next-shadcn/src/components/ui/badge.tsx +15 -18
- package/templates/next-shadcn/src/components/ui/button.tsx +12 -3
- package/templates/next-shadcn/src/components/ui/calendar.tsx +1 -0
- package/templates/next-shadcn/src/components/ui/checkbox.tsx +6 -2
- package/templates/next-shadcn/src/components/ui/collapsible.tsx +33 -0
- package/templates/next-shadcn/src/components/ui/command.tsx +1 -2
- package/templates/next-shadcn/src/components/ui/dialog.tsx +34 -26
- package/templates/next-shadcn/src/components/ui/dropdown-menu.tsx +115 -114
- package/templates/next-shadcn/src/components/ui/hover-card.tsx +43 -0
- package/templates/next-shadcn/src/components/ui/input-group.tsx +2 -4
- package/templates/next-shadcn/src/components/ui/input.tsx +1 -2
- package/templates/next-shadcn/src/components/ui/label.tsx +6 -2
- package/templates/next-shadcn/src/components/ui/popover.tsx +27 -28
- package/templates/next-shadcn/src/components/ui/progress.tsx +11 -63
- package/templates/next-shadcn/src/components/ui/radio-group.tsx +43 -0
- package/templates/next-shadcn/src/components/ui/scroll-area.tsx +6 -6
- package/templates/next-shadcn/src/components/ui/select.tsx +55 -64
- package/templates/next-shadcn/src/components/ui/separator.tsx +6 -3
- package/templates/next-shadcn/src/components/ui/sheet.tsx +35 -26
- package/templates/next-shadcn/src/components/ui/slider.tsx +58 -0
- package/templates/next-shadcn/src/components/ui/switch.tsx +3 -2
- package/templates/next-shadcn/src/components/ui/table.tsx +115 -0
- package/templates/next-shadcn/src/components/ui/tabs.tsx +16 -8
- package/templates/next-shadcn/src/components/ui/toggle-group.tsx +89 -0
- package/templates/next-shadcn/src/components/ui/toggle.tsx +46 -0
- package/templates/next-shadcn/src/components/ui/tooltip.tsx +24 -33
- package/templates/next-shadcn/src/lib/utils.ts +6 -1
- package/ucode.js +8 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rename.js — renaming a name everywhere it is that name.
|
|
3
|
+
*
|
|
4
|
+
* Most failed edits are a find-and-replace that matched too much: renaming
|
|
5
|
+
* `id` rewrites `width`, `idle` and every `id` inside a string or a comment,
|
|
6
|
+
* and the model then spends three steps undoing it. The fix is not a bigger
|
|
7
|
+
* regular expression — it is to stop treating code as text.
|
|
8
|
+
*
|
|
9
|
+
* This walks each file as code: it knows where a string, a template literal
|
|
10
|
+
* and a comment begin and end, and only renames an identifier sitting in
|
|
11
|
+
* actual code, whole, not as part of a longer word. That is short of a
|
|
12
|
+
* parser — it cannot tell two different `user` variables in two scopes apart
|
|
13
|
+
* — so it reports exactly what it changed and where, and leaves the model to
|
|
14
|
+
* read the diff. It is the difference between an edit that is usually right
|
|
15
|
+
* and one that is usually wrong.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { promises as fs } from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { ToolFailure } from '../core/failure.js';
|
|
21
|
+
import { resolveIn, guard, result, walk } from './shared.js';
|
|
22
|
+
|
|
23
|
+
const SOURCE = /\.(?:[cm]?[jt]sx?|py)$/i;
|
|
24
|
+
const IDENT = /^[A-Za-z_$][\w$]*$/;
|
|
25
|
+
const wordChar = (c) => c !== undefined && /[\w$]/.test(c);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Rename `from` to `to` in one file's text, skipping strings and comments.
|
|
29
|
+
* Returns the new text and the 1-based lines that changed.
|
|
30
|
+
*/
|
|
31
|
+
export function renameIn(text, from, to) {
|
|
32
|
+
const out = [];
|
|
33
|
+
const lines = [];
|
|
34
|
+
let line = 1;
|
|
35
|
+
let i = 0;
|
|
36
|
+
const n = text.length;
|
|
37
|
+
|
|
38
|
+
// Where we are: code, or inside something that is not code.
|
|
39
|
+
let mode = 'code';
|
|
40
|
+
let quote = '';
|
|
41
|
+
// Template literals can hold ${ code }, so the nesting is tracked.
|
|
42
|
+
const templates = [];
|
|
43
|
+
|
|
44
|
+
while (i < n) {
|
|
45
|
+
const c = text[i];
|
|
46
|
+
const next = text[i + 1];
|
|
47
|
+
if (c === '\n') line++;
|
|
48
|
+
|
|
49
|
+
if (mode === 'line-comment') {
|
|
50
|
+
if (c === '\n') mode = 'code';
|
|
51
|
+
out.push(c); i++; continue;
|
|
52
|
+
}
|
|
53
|
+
if (mode === 'block-comment') {
|
|
54
|
+
if (c === '*' && next === '/') { out.push('*/'); i += 2; mode = 'code'; continue; }
|
|
55
|
+
out.push(c); i++; continue;
|
|
56
|
+
}
|
|
57
|
+
if (mode === 'string') {
|
|
58
|
+
if (c === '\\') { out.push(c, next ?? ''); i += 2; continue; }
|
|
59
|
+
if (c === quote) { mode = 'code'; quote = ''; }
|
|
60
|
+
if (c === '\n' && quote !== '`') { mode = 'code'; quote = ''; } // an unterminated quote
|
|
61
|
+
out.push(c); i++; continue;
|
|
62
|
+
}
|
|
63
|
+
if (mode === 'template') {
|
|
64
|
+
if (c === '\\') { out.push(c, next ?? ''); i += 2; continue; }
|
|
65
|
+
if (c === '`') { mode = templates.pop() ?? 'code'; out.push(c); i++; continue; }
|
|
66
|
+
if (c === '$' && next === '{') { templates.push('template'); mode = 'code'; out.push('${'); i += 2; continue; }
|
|
67
|
+
out.push(c); i++; continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// mode === 'code'
|
|
71
|
+
if (c === '/' && next === '/') { mode = 'line-comment'; out.push('//'); i += 2; continue; }
|
|
72
|
+
if (c === '/' && next === '*') { mode = 'block-comment'; out.push('/*'); i += 2; continue; }
|
|
73
|
+
if (c === '#') { mode = 'line-comment'; out.push(c); i++; continue; } // python
|
|
74
|
+
if (c === '"' || c === "'") { mode = 'string'; quote = c; out.push(c); i++; continue; }
|
|
75
|
+
if (c === '`') { mode = 'template'; out.push(c); i++; continue; }
|
|
76
|
+
if (c === '}' && templates.length) { mode = templates.pop(); out.push(c); i++; continue; }
|
|
77
|
+
|
|
78
|
+
if (/[A-Za-z_$]/.test(c)) {
|
|
79
|
+
let j = i;
|
|
80
|
+
while (j < n && wordChar(text[j])) j++;
|
|
81
|
+
const word = text.slice(i, j);
|
|
82
|
+
if (word === from && !wordChar(text[i - 1])) {
|
|
83
|
+
out.push(to);
|
|
84
|
+
if (lines[lines.length - 1] !== line) lines.push(line);
|
|
85
|
+
} else {
|
|
86
|
+
out.push(word);
|
|
87
|
+
}
|
|
88
|
+
i = j;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
out.push(c);
|
|
93
|
+
i++;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { text: out.join(''), lines };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Rename a name across every source file under a path. */
|
|
100
|
+
export async function renameSymbol({ name, to, path: p = '.' }) {
|
|
101
|
+
const from = String(name ?? '').trim();
|
|
102
|
+
const into = String(to ?? '').trim();
|
|
103
|
+
|
|
104
|
+
if (!IDENT.test(from) || !IDENT.test(into)) {
|
|
105
|
+
throw new ToolFailure({
|
|
106
|
+
kind: 'bad_args',
|
|
107
|
+
attempted: `renaming ${from || '(nothing)'} to ${into || '(nothing)'}`,
|
|
108
|
+
failed: 'Both names must be plain identifiers: letters, digits, _ or $, not starting with a digit.',
|
|
109
|
+
fix: 'To change something that is not an identifier, use edit_file or multi_edit.',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (from === into) {
|
|
113
|
+
throw new ToolFailure({
|
|
114
|
+
kind: 'bad_args',
|
|
115
|
+
attempted: `renaming ${from}`,
|
|
116
|
+
failed: 'The old and new names are the same.',
|
|
117
|
+
fix: 'Pass the name you want it to become.',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const target = resolveIn(p || '.', 'rename_symbol', 'path');
|
|
122
|
+
await guard(target, `rename ${from} to ${into} under ${target.abs}`);
|
|
123
|
+
|
|
124
|
+
const stat = await fs.stat(target.abs).catch(() => null);
|
|
125
|
+
const rels = stat?.isFile() ? [''] : (await walk(target.abs, {})).filter((rel) => SOURCE.test(rel));
|
|
126
|
+
|
|
127
|
+
const changed = [];
|
|
128
|
+
let total = 0;
|
|
129
|
+
|
|
130
|
+
for (const rel of rels) {
|
|
131
|
+
const abs = rel ? path.join(target.abs, rel) : target.abs;
|
|
132
|
+
const before = await fs.readFile(abs, 'utf8').catch(() => null);
|
|
133
|
+
if (before === null || !before.includes(from)) continue;
|
|
134
|
+
const { text, lines } = renameIn(before, from, into);
|
|
135
|
+
if (!lines.length || text === before) continue;
|
|
136
|
+
await fs.writeFile(abs, text, 'utf8');
|
|
137
|
+
changed.push({ rel: rel || target.show, lines });
|
|
138
|
+
total += lines.length;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!changed.length) {
|
|
142
|
+
return result(
|
|
143
|
+
`Nothing to rename: "${from}" does not appear as a name in any code under ${target.show}.\n` +
|
|
144
|
+
'It may only exist in strings or comments, which are deliberately left alone, or be spelled differently.',
|
|
145
|
+
'no occurrences'
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const shown = changed.slice(0, 40).map((f) => `${f.rel} (${f.lines.length} line${f.lines.length === 1 ? '' : 's'}: ${f.lines.slice(0, 12).join(', ')}${f.lines.length > 12 ? '…' : ''})`);
|
|
150
|
+
const more = changed.length > shown.length ? `\n[${changed.length - shown.length} more files]` : '';
|
|
151
|
+
|
|
152
|
+
return result(
|
|
153
|
+
`Renamed ${from} to ${into} in ${changed.length} file${changed.length === 1 ? '' : 's'}:\n\n${shown.join('\n')}${more}\n\n` +
|
|
154
|
+
'Strings and comments were left alone. If the name was meant to change in one of those too, edit it directly.',
|
|
155
|
+
`${total} line${total === 1 ? '' : 's'} in ${changed.length} file${changed.length === 1 ? '' : 's'}`
|
|
156
|
+
);
|
|
157
|
+
}
|
package/src/tools/scaffold.js
CHANGED
|
@@ -15,7 +15,8 @@ import path from 'node:path';
|
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import { ToolFailure } from '../core/failure.js';
|
|
17
17
|
import { resolveIn, guard, result } from './shared.js';
|
|
18
|
-
import { packageJsonWritten } from './shell.js';
|
|
18
|
+
import { packageJsonWritten, installIn } from './shell.js';
|
|
19
|
+
import { restore, populate } from './cache.js';
|
|
19
20
|
|
|
20
21
|
const TEMPLATES = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'templates');
|
|
21
22
|
|
|
@@ -64,7 +65,65 @@ async function copyTree(from, to, fill) {
|
|
|
64
65
|
* @param {string} [o.template]
|
|
65
66
|
* @param {boolean} [o.install] start the background install (tests turn it off)
|
|
66
67
|
*/
|
|
67
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Give the new app its look: one of the hand-picked presets in the starter's
|
|
70
|
+
* presets/ folder — a full light and dark palette and a font — written into
|
|
71
|
+
* globals.css and layout.tsx. Apps stop looking like the same default blue.
|
|
72
|
+
* Returns the preset used, or null when the starter has none.
|
|
73
|
+
*/
|
|
74
|
+
export async function applyDesign(appDir, design) {
|
|
75
|
+
const dir = path.join(appDir, 'presets');
|
|
76
|
+
const names = (await fs.readdir(dir).catch(() => [])).filter((f) => f.endsWith('.json'));
|
|
77
|
+
if (!names.length) return null;
|
|
78
|
+
const presets = await Promise.all(names.map(async (f) => JSON.parse(await fs.readFile(path.join(dir, f), 'utf8'))));
|
|
79
|
+
await fs.rm(dir, { recursive: true, force: true }); // the app needs the result, not the catalogue
|
|
80
|
+
const preset = presets.find((p) => p.name === design) ?? presets.find((p) => p.default) ?? presets[0];
|
|
81
|
+
|
|
82
|
+
const cssFile = path.join(appDir, 'src', 'app', 'globals.css');
|
|
83
|
+
let css = await fs.readFile(cssFile, 'utf8').catch(() => null);
|
|
84
|
+
if (css !== null) {
|
|
85
|
+
const retint = (selector, tokens) => {
|
|
86
|
+
const block = new RegExp(`(${selector}\\s*\\{)([\\s\\S]*?)(\\n\\})`);
|
|
87
|
+
css = css.replace(block, (all, open, body, close) => {
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
let next = body.replace(/(\n\s*)--([\w-]+):\s*[^;]+;/g, (line, lead, key) => {
|
|
90
|
+
if (!(key in tokens)) return line;
|
|
91
|
+
seen.add(key);
|
|
92
|
+
return `${lead}--${key}: ${tokens[key]};`;
|
|
93
|
+
});
|
|
94
|
+
for (const [key, value] of Object.entries(tokens)) if (!seen.has(key)) next += `\n --${key}: ${value};`;
|
|
95
|
+
return open + next + close;
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
retint(':root', { radius: preset.radius, ...preset.light });
|
|
99
|
+
retint('\\.dark', preset.dark);
|
|
100
|
+
await fs.writeFile(cssFile, css);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const sans = preset.fonts?.sans;
|
|
104
|
+
const layoutFile = path.join(appDir, 'src', 'app', 'layout.tsx');
|
|
105
|
+
if (sans && sans !== 'Geist') {
|
|
106
|
+
const id = sans.replace(/\s+/g, '_');
|
|
107
|
+
const layout = await fs.readFile(layoutFile, 'utf8').catch(() => null);
|
|
108
|
+
if (layout !== null) {
|
|
109
|
+
await fs.writeFile(layoutFile, layout
|
|
110
|
+
.replace('import { Geist, Geist_Mono } from "next/font/google";', `import { ${id}, Geist_Mono } from "next/font/google";`)
|
|
111
|
+
.replace('const sans = Geist({', `const sans = ${id}({`));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const guide = path.join(appDir, 'TEMPLATE.md');
|
|
116
|
+
const text = await fs.readFile(guide, 'utf8').catch(() => null);
|
|
117
|
+
if (text !== null) {
|
|
118
|
+
await fs.writeFile(guide, `${text.trimEnd()}\n\n## Design\n\nThis app uses the **${preset.name}** preset — ` +
|
|
119
|
+
`${preset.summary}. Font: ${sans ?? 'Geist'}. The palette lives in globals.css (light and dark): ` +
|
|
120
|
+
'build with the tokens (bg-primary, text-muted-foreground, border, ...) rather than raw colours, ' +
|
|
121
|
+
'so every screen stays in one look.\n');
|
|
122
|
+
}
|
|
123
|
+
return preset;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function createApp({ folder, name, description, template = 'next-shadcn', design, install = true }) {
|
|
68
127
|
if (!TEMPLATE_NAMES.includes(template)) {
|
|
69
128
|
throw new ToolFailure({
|
|
70
129
|
kind: 'bad_args',
|
|
@@ -110,21 +169,41 @@ export async function createApp({ folder, name, description, template = 'next-sh
|
|
|
110
169
|
|
|
111
170
|
const files = await copyTree(path.join(TEMPLATES, template), target.abs, fill);
|
|
112
171
|
await fs.mkdir(path.join(target.abs, 'public'), { recursive: true });
|
|
172
|
+
const look = await applyDesign(target.abs, design);
|
|
113
173
|
|
|
174
|
+
// The starter has been installed on this machine before: hard-link that
|
|
175
|
+
// tree in, which is seconds where npm is a minute. Otherwise install as
|
|
176
|
+
// usual, and keep the result so the next app is instant.
|
|
177
|
+
let linked = 0;
|
|
114
178
|
if (install) {
|
|
115
|
-
|
|
116
|
-
|
|
179
|
+
// Keyed on the starter's lockfile, which is the same for every app made
|
|
180
|
+
// from it — the app's own is rewritten by npm as it installs.
|
|
181
|
+
const lockText = await fs
|
|
182
|
+
.readFile(path.join(TEMPLATES, template, '_package-lock.json'), 'utf8')
|
|
183
|
+
.catch(() => null);
|
|
184
|
+
linked = await restore(target.abs, template, lockText);
|
|
185
|
+
if (!linked) {
|
|
186
|
+
const pkg = path.join(target.abs, 'package.json');
|
|
187
|
+
packageJsonWritten(pkg, await fs.readFile(pkg, 'utf8'));
|
|
188
|
+
installIn(target.abs)?.then((done) => {
|
|
189
|
+
if (done?.code === 0) populate(target.abs, template, lockText);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
117
192
|
}
|
|
118
193
|
|
|
119
194
|
const guide = await fs.readFile(path.join(target.abs, 'TEMPLATE.md'), 'utf8').catch(() => '');
|
|
120
195
|
|
|
121
196
|
return result(
|
|
122
197
|
`Created ${target.show} from the ${template} starter — ${files.length} files, already known to build.\n` +
|
|
123
|
-
(
|
|
198
|
+
(look ? `Design: the ${look.name} preset (${look.summary}), font ${look.fonts?.sans ?? 'Geist'}.\n` : '') +
|
|
199
|
+
(linked
|
|
200
|
+
? `Its packages are already in place (${linked.toLocaleString()} files, linked from the starter cache) — ` +
|
|
201
|
+
'nothing to install: build and run straight away.\n'
|
|
202
|
+
: install
|
|
124
203
|
? `Its packages are installing in the background right now. Keep writing: any command you run in ` +
|
|
125
204
|
`${target.show} waits for that install first, so there is no need to run npm install.\n`
|
|
126
205
|
: '') +
|
|
127
206
|
`Run this app's commands with cwd: "${target.show}" (npm run build, npm run dev).\n\n${guide}`,
|
|
128
|
-
`${files.length} files${install ? ' · installing in the background' : ''}`
|
|
207
|
+
`${files.length} files${linked ? ' · packages ready' : install ? ' · installing in the background' : ''}`
|
|
129
208
|
);
|
|
130
209
|
}
|