ucode-agent 1.43.0 → 1.45.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.
@@ -16,7 +16,7 @@ import { createApp, TEMPLATE_NAMES, TEMPLATE_NOTES } from './scaffold.js';
16
16
  import { deploy } from './deploy.js';
17
17
  import { clip, READ_LINES } from './shared.js';
18
18
 
19
- export { setRoot, setConfirm, getRoot } from './shared.js';
19
+ export { setRoot, setConfirm, setRequest, getRoot } from './shared.js';
20
20
 
21
21
  const str = (description) => ({ type: 'string', description });
22
22
  const int = (description) => ({ type: 'integer', description });
@@ -85,17 +85,21 @@ export const tools = [
85
85
  'Start a new app AND write it, in one call. Pass "files" with the whole app and this ' +
86
86
  'is the only call the build needs: the starter lands, your files are written over it, ' +
87
87
  'and the result comes back with everything. Two starters. "plain-html" (the default): ' +
88
- 'one index.html, one stylesheet, one ES module — nothing to install, nothing to build, ' +
89
- 'and the stylesheet is a design system already: a palette, a spacing scale, radii, ' +
90
- 'motion timings, focus rings and a breakpoint. Re-tint those tokens to suit the app ' +
91
- 'and compose every rule from them. Replacing it with raw pixel values is what an ' +
88
+ 'one index.html, one stylesheet, one ES module — nothing to install, nothing to build, ' +
89
+ 'and the stylesheet is a design system already: a palette, a spacing scale, radii, ' +
90
+ 'motion timings, focus rings and a breakpoint. Re-tint those tokens to suit the app ' +
91
+ 'and compose every rule from them. Replacing it with raw pixel values is what an ' +
92
92
  'undesigned page is made of. ' +
93
93
  'opens straight in a browser, and its three files come back inside this result so there ' +
94
94
  'is never a reason to read them. Use it for anything that is one page: a tasks app, a ' +
95
95
  'toy, a game, a visualisation, a calculator, a timer. "next-shadcn": Next.js 16, ' +
96
96
  'TypeScript, Tailwind 4 and shadcn with 33 components — only when the app genuinely ' +
97
97
  'needs routes, a database or many screens, because it costs an install and a build. ' +
98
- 'This is how every Next.js app begins - never run create-next-app or shadcn init.',
98
+ 'This is how every Next.js app begins - never run create-next-app or shadcn init. ' +
99
+ 'In "next-shadcn" every path in "files" goes under src/: a page is a route only at ' +
100
+ '<app>/src/app/page.tsx or <app>/src/app/<segment>/page.tsx, an API handler only at ' +
101
+ '<app>/src/app/api/<name>/route.ts, and components at <app>/src/components/<feature>/. ' +
102
+ 'A page.tsx written anywhere else is an ordinary file the router never serves.',
99
103
  parameters: {
100
104
  type: 'object',
101
105
  properties: {
@@ -120,7 +124,10 @@ export const tools = [
120
124
  },
121
125
  template: {
122
126
  type: 'string',
123
- enum: ['next-shadcn', 'plain-html'],
127
+ // The default goes first: a model picking from an enum reaches for
128
+ // the head of the list, and the head of this one used to be the
129
+ // starter that costs an install and a build.
130
+ enum: ['plain-html', 'next-shadcn'],
124
131
  description:
125
132
  'Which starter. "plain-html" (the default) for one page, a toy, a game, or any ' +
126
133
  'app that does not need a server: no install, no build, nothing to wait for. ' +
@@ -14,7 +14,7 @@ import { promises as fs } from 'node:fs';
14
14
  import path from 'node:path';
15
15
  import { fileURLToPath } from 'node:url';
16
16
  import { ToolFailure } from '../core/failure.js';
17
- import { resolveIn, guard, result } from './shared.js';
17
+ import { resolveIn, guard, result, askedForPlainHtml } from './shared.js';
18
18
  import { batchWrite } from './files.js';
19
19
  import { packageJsonWritten, installIn } from './shell.js';
20
20
  import { restore, populate } from './cache.js';
@@ -173,6 +173,60 @@ async function unprefixOwnFolder(appDir, written) {
173
173
  return fixed;
174
174
  }
175
175
 
176
+ /**
177
+ * Files that legitimately sit at the root of a Next.js project.
178
+ *
179
+ * Everything else a model writes there is app code that has missed src/.
180
+ */
181
+ const NEXT_ROOT = /^(?:package(?:-lock)?\.json|next\.config\.[mc]?[jt]s|tsconfig\.json|postcss\.config\.[mc]?js|eslint\.config\.[mc]?js|components\.json|README\.md|TEMPLATE\.md|\.gitignore|next-env\.d\.ts)$/i;
182
+
183
+ /**
184
+ * Put Next.js files where Next.js looks for them.
185
+ *
186
+ * Routing is folder-based and unforgiving: a page is a route only at
187
+ * src/app/<segment>/page.tsx. A traced build wrote page.tsx at the project
188
+ * root and api/expenses/route.ts beside it — the build passed, every route
189
+ * 404ed, and nothing said why.
190
+ *
191
+ * The guide has the tree in it, and the guide cannot help: it comes back
192
+ * inside the create_app result, and the model chooses these paths in the call
193
+ * that produces that result. It is reading the map after it has parked. So the
194
+ * paths are corrected on the way in, and the result says what moved and why,
195
+ * which is the copy that arrives in time to matter for the next file.
196
+ *
197
+ * Only for next-shadcn, only for files under the app folder, and never for the
198
+ * config files that genuinely belong at the root.
199
+ */
200
+ function placeForNext(files, folder) {
201
+ const moved = [];
202
+
203
+ const out = files.map((f) => {
204
+ const rel = String(f.path ?? '').split('\\').join('/');
205
+ const prefix = `${folder}/`;
206
+ if (!rel.startsWith(prefix)) return f;
207
+
208
+ const inside = rel.slice(prefix.length);
209
+ if (!inside || inside.startsWith('src/') || inside.startsWith('public/')) return f;
210
+ if (NEXT_ROOT.test(inside)) return f;
211
+
212
+ // app/... is the right tree written one level too high; everything else
213
+ // that is app code belongs under src/ as it stands.
214
+ const under = inside.startsWith('app/') ? `src/${inside}` : `src/app/${inside}`;
215
+ const isRoute = /(?:^|\/)(?:page|layout|loading|error|not-found)\.[jt]sx?$/.test(inside)
216
+ || /(?:^|\/)route\.[jt]s$/.test(inside);
217
+ const isCode = /^(?:components|lib|hooks|styles|utils)\//.test(inside);
218
+
219
+ if (!isRoute && !isCode) return f;
220
+ const to = isCode ? `${prefix}src/${inside}` : `${prefix}${under}`;
221
+ if (to === rel) return f;
222
+
223
+ moved.push(`${inside} -> ${to.slice(prefix.length)}`);
224
+ return { ...f, path: to };
225
+ });
226
+
227
+ return { files: out, moved };
228
+ }
229
+
176
230
  async function keepDesignTokens(appDir, template, written, starterCss) {
177
231
  const rel = TOKEN_FILE[template];
178
232
  if (!rel || !starterCss) return null;
@@ -286,6 +340,16 @@ export async function createApp({ folder, name, description, template = 'plain-h
286
340
  });
287
341
  }
288
342
 
343
+ // The user said no framework. A starter that costs an npm install and a
344
+ // build is then not a judgement call the model gets to make on their behalf
345
+ // — it is the one thing they ruled out, and the cost of getting it wrong is
346
+ // minutes of their time plus a rebuild from nothing.
347
+ let overrode = false;
348
+ if (template === 'next-shadcn' && askedForPlainHtml()) {
349
+ template = 'plain-html';
350
+ overrode = true;
351
+ }
352
+
289
353
  const target = resolveIn(folder, 'create_app', 'folder');
290
354
  const attempted = `creating an app in ${target.show}`;
291
355
  if (target.show === '.') {
@@ -366,7 +430,9 @@ export async function createApp({ folder, name, description, template = 'plain-h
366
430
 
367
431
  // The app's own files, written in this same call. Two round trips become
368
432
  // one, and round trips are nearly all of the time a build takes.
369
- const mine = given;
433
+ // Next.js only: paths that have missed src/ are corrected before they land.
434
+ const placed = template === 'next-shadcn' ? placeForNext(given, path.basename(target.abs)) : { files: given, moved: [] };
435
+ const mine = placed.files;
370
436
  const wrote = mine.length ? await batchWrite({ files: mine }) : null;
371
437
  const written = new Set(mine.map((f) => resolveIn(f.path, 'create_app', 'files').abs));
372
438
  const keptTokens = await keepDesignTokens(target.abs, template, written, starterCss);
@@ -384,9 +450,21 @@ export async function createApp({ folder, name, description, template = 'plain-h
384
450
  }
385
451
 
386
452
  const out = result(
387
- `Created ${target.show} from the ${template} starter — ${copied.length} files, already known to build.\n` +
453
+ (overrode
454
+ ? 'You asked for next-shadcn, but the request said plain HTML and no framework, so ' +
455
+ 'this is the plain-html starter instead: three files, no install, no build. Write ' +
456
+ 'the app in them.\n'
457
+ : '') +
458
+ `Created ${target.show} from the ${template} starter — ${copied.length} files, already known to build.\n` +
388
459
  (look ? `Design: the ${look.name} preset (${look.summary}), font ${look.fonts?.sans ?? 'Geist'}.\n` : '') +
389
460
  (wrote ? `\nYour ${mine.length} file${mine.length === 1 ? '' : 's'}:\n${wrote.content}\n` : '') +
461
+ (placed.moved.length
462
+ ? `\nMoved into place: ${placed.moved.join(', ')}. In Next.js a page is only a route at `
463
+ + 'src/app/<segment>/page.tsx, an API handler only at src/app/api/<name>/route.ts, and '
464
+ + 'components and libraries live under src/. A file written outside src/ builds fine and '
465
+ + 'is never served, which is the hardest kind of wrong to notice. Write the rest there '
466
+ + 'directly.\n'
467
+ : '') +
390
468
  (relinked.length
391
469
  ? `\nFixed in ${relinked.join(', ')}: links that began with "${path.basename(target.abs)}/". ` +
392
470
  'Paths in "files" are relative to the project root, but a link inside a page is ' +