@ultimat3/cli 4.0.0 → 5.0.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/package.json +24 -24
- package/src/cmd-errors.ts +5 -4
- package/src/fix-scan.ts +123 -2
- package/src/output.ts +8 -5
- package/src/templates/scaffold-repo.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -35,28 +35,28 @@
|
|
|
35
35
|
"dev": "bun run src/bin.ts dev"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@ultimat3/action": "
|
|
39
|
-
"@ultimat3/admin": "
|
|
40
|
-
"@ultimat3/ai": "
|
|
41
|
-
"@ultimat3/cache": "
|
|
42
|
-
"@ultimat3/core": "
|
|
43
|
-
"@ultimat3/db": "
|
|
44
|
-
"@ultimat3/entity": "
|
|
45
|
-
"@ultimat3/http": "
|
|
46
|
-
"@ultimat3/i18n": "
|
|
47
|
-
"@ultimat3/jobs": "
|
|
48
|
-
"@ultimat3/mail": "
|
|
49
|
-
"@ultimat3/manifest": "
|
|
50
|
-
"@ultimat3/mcp": "
|
|
51
|
-
"@ultimat3/policy": "
|
|
52
|
-
"@ultimat3/pwa": "
|
|
53
|
-
"@ultimat3/query": "
|
|
54
|
-
"@ultimat3/realtime": "
|
|
55
|
-
"@ultimat3/render": "
|
|
56
|
-
"@ultimat3/schema": "
|
|
57
|
-
"@ultimat3/seo": "
|
|
58
|
-
"@ultimat3/storage": "
|
|
59
|
-
"@ultimat3/testing": "
|
|
60
|
-
"@ultimat3/time": "
|
|
38
|
+
"@ultimat3/action": "5.0.0",
|
|
39
|
+
"@ultimat3/admin": "5.0.0",
|
|
40
|
+
"@ultimat3/ai": "5.0.0",
|
|
41
|
+
"@ultimat3/cache": "5.0.0",
|
|
42
|
+
"@ultimat3/core": "5.0.0",
|
|
43
|
+
"@ultimat3/db": "5.0.0",
|
|
44
|
+
"@ultimat3/entity": "5.0.0",
|
|
45
|
+
"@ultimat3/http": "5.0.0",
|
|
46
|
+
"@ultimat3/i18n": "5.0.0",
|
|
47
|
+
"@ultimat3/jobs": "5.0.0",
|
|
48
|
+
"@ultimat3/mail": "5.0.0",
|
|
49
|
+
"@ultimat3/manifest": "5.0.0",
|
|
50
|
+
"@ultimat3/mcp": "5.0.0",
|
|
51
|
+
"@ultimat3/policy": "5.0.0",
|
|
52
|
+
"@ultimat3/pwa": "5.0.0",
|
|
53
|
+
"@ultimat3/query": "5.0.0",
|
|
54
|
+
"@ultimat3/realtime": "5.0.0",
|
|
55
|
+
"@ultimat3/render": "5.0.0",
|
|
56
|
+
"@ultimat3/schema": "5.0.0",
|
|
57
|
+
"@ultimat3/seo": "5.0.0",
|
|
58
|
+
"@ultimat3/storage": "5.0.0",
|
|
59
|
+
"@ultimat3/testing": "5.0.0",
|
|
60
|
+
"@ultimat3/time": "5.0.0"
|
|
61
61
|
}
|
|
62
62
|
}
|
package/src/cmd-errors.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { singleLine } from '@ultimat3/core';
|
|
1
2
|
// `x errors explain <CODE>` / `x errors list` — the error table, programmatically. An agent that
|
|
2
3
|
// hits an `X_*` code should not have to leave the terminal to learn what it means, and a code it
|
|
3
4
|
// invented should come back refused: the answer to an unregistered code is "no such code", never
|
|
@@ -35,9 +36,9 @@ const asJson = (explanation: ErrorExplanation): JsonValue => {
|
|
|
35
36
|
|
|
36
37
|
/** The 3-line contract format, minus the leading blank code line `renderFinding` would add. */
|
|
37
38
|
const detailLines = (explanation: ErrorExplanation): readonly string[] => [
|
|
38
|
-
` cause: ${explanation.cause}`,
|
|
39
|
-
` fix: ${explanation.fix}`,
|
|
40
|
-
` docs: ${explanation.docs}`,
|
|
39
|
+
` cause: ${singleLine(explanation.cause)}`,
|
|
40
|
+
` fix: ${singleLine(explanation.fix)}`,
|
|
41
|
+
` docs: ${singleLine(explanation.docs)}`,
|
|
41
42
|
];
|
|
42
43
|
|
|
43
44
|
function explainOne(code: string): CommandResult {
|
|
@@ -69,7 +70,7 @@ function listAll(catalog: ErrorCatalog): CommandResult {
|
|
|
69
70
|
ok: catalog.failed.length === 0,
|
|
70
71
|
command: 'errors',
|
|
71
72
|
summary: msg('cli.errors.count', { count: all.length }),
|
|
72
|
-
lines: all.map((entry) => ` ${entry.code.padEnd(30)} ${entry.cause}`),
|
|
73
|
+
lines: all.map((entry) => ` ${entry.code.padEnd(30)} ${singleLine(entry.cause)}`),
|
|
73
74
|
findings: catalog.failed,
|
|
74
75
|
data: {
|
|
75
76
|
codes: all.map(asJson),
|
package/src/fix-scan.ts
CHANGED
|
@@ -201,6 +201,109 @@ function helperFixSites(
|
|
|
201
201
|
return sites;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
/**
|
|
205
|
+
* The identifier a `fix:` value LOOKS UP, when the value is a lookup and nothing else:
|
|
206
|
+
* `fix: SQLSTATE_FIXES[code]`, `fix: SQLSTATE_FIXES.X_DB_POOL_EXHAUSTED`, and the `.replace(…)`
|
|
207
|
+
* that `@ultimat3/db` puts after the first of those. The value expression then holds NO literal at
|
|
208
|
+
* its own depth, so `valueLiterals` answered `[]` and the site was dropped without being counted —
|
|
209
|
+
* six shipped fix lines that no rule had ever read, and nothing to catch a seventh (#97).
|
|
210
|
+
*/
|
|
211
|
+
/** A constant's spelling, and the only head whose failure to resolve is worth counting. */
|
|
212
|
+
const TABLE_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
213
|
+
|
|
214
|
+
const LOOKUP_HEAD = /^\s*([A-Za-z_$][\w$]*)\s*[[.]/;
|
|
215
|
+
|
|
216
|
+
/** The one wrapper a table is allowed to arrive in. Any other call is a factory, not a table. */
|
|
217
|
+
const FREEZE = 'Object.freeze(';
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Where the object literal bound to `name` opens in this file, or `undefined` when this file does
|
|
221
|
+
* not declare one. One hop and same-file only, deliberately: a table is a `const` a few lines
|
|
222
|
+
* above the factory that reads it in every instance measured here, and following a chain or a
|
|
223
|
+
* second file is where a text scan starts guessing.
|
|
224
|
+
*
|
|
225
|
+
* Conservative in three ways, and each one answers `undefined` so the caller counts the site
|
|
226
|
+
* instead of reading an unrelated object. **A name declared twice is not resolved at all**: this
|
|
227
|
+
* scan tracks no lexical scope, so a nested `const FIXES` would otherwise be read as the outer one
|
|
228
|
+
* and the gate would judge fix lines from a table the call site cannot reach. **Only a bare `{…}`
|
|
229
|
+
* or the exact `Object.freeze({…})`**: `makeTable({…})` is a call whose result this cannot know,
|
|
230
|
+
* and reading its argument would report the input to a factory as the factory's output. **And only
|
|
231
|
+
* a `const`** — a `let` can be reassigned, and the last assignment is what a call reads.
|
|
232
|
+
*
|
|
233
|
+
* `name` is always `LOOKUP_HEAD`'s capture, `[A-Za-z_$][\w$]*`, so it carries no regex
|
|
234
|
+
* metacharacter and the pattern below is linear whatever a source file contains.
|
|
235
|
+
*/
|
|
236
|
+
function tableOpen(masked: string, name: string): number | undefined {
|
|
237
|
+
const declaration = new RegExp(`(?<![.\\w$])const\\s+${name}\\s*(?::[^=;]*)?=\\s*`, 'g');
|
|
238
|
+
const first = declaration.exec(masked);
|
|
239
|
+
if (first === null || declaration.exec(masked) !== null) return undefined;
|
|
240
|
+
const rest = masked.slice(first.index + first[0].length);
|
|
241
|
+
const value = rest.trimStart();
|
|
242
|
+
const skipped = rest.length - value.length;
|
|
243
|
+
const from = first.index + first[0].length + skipped;
|
|
244
|
+
if (value.startsWith('{')) return from;
|
|
245
|
+
if (!value.startsWith(FREEZE)) return undefined;
|
|
246
|
+
const inner = value.slice(FREEZE.length).trimStart();
|
|
247
|
+
return inner.startsWith('{') ? from + (value.length - inner.length) : undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Where this entry's value stops: the next `,` at the entry's own depth, or the table's `}`. */
|
|
251
|
+
function entryEnd(masked: string, from: number): number {
|
|
252
|
+
let depth = 0;
|
|
253
|
+
for (let i = from; i < masked.length; i += 1) {
|
|
254
|
+
const ch = masked[i] as string;
|
|
255
|
+
if (QUOTES.has(ch)) {
|
|
256
|
+
i = Math.max(i, endOfLiteral(masked, i) - 1);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (OPENERS.has(ch)) depth += 1;
|
|
260
|
+
else if (CLOSERS.has(ch)) {
|
|
261
|
+
if (depth === 0) return i;
|
|
262
|
+
depth -= 1;
|
|
263
|
+
} else if (depth === 0 && ch === ',') return i;
|
|
264
|
+
}
|
|
265
|
+
return masked.length;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Every fix string a table holds, read at its entries' own depth. A value that is a concatenation
|
|
270
|
+
* yields one site per literal, exactly as a `fix:` key does — the rule is per line, and half a fix
|
|
271
|
+
* carrying a banned phrase is still a fix line an agent is handed.
|
|
272
|
+
*/
|
|
273
|
+
function tableFixSites(
|
|
274
|
+
masked: string,
|
|
275
|
+
source: string,
|
|
276
|
+
at: string,
|
|
277
|
+
open: number,
|
|
278
|
+
lineAt: (index: number) => number,
|
|
279
|
+
): readonly FixSite[] {
|
|
280
|
+
const sites: FixSite[] = [];
|
|
281
|
+
let depth = 0;
|
|
282
|
+
for (let i = open; i < masked.length; i += 1) {
|
|
283
|
+
const ch = masked[i] as string;
|
|
284
|
+
// A quoted KEY (`'23505': '…'`) is skipped whole, so its closing quote cannot be read as the
|
|
285
|
+
// opener of the value and shift every literal after it by one.
|
|
286
|
+
if (QUOTES.has(ch)) {
|
|
287
|
+
i = Math.max(i, endOfLiteral(masked, i) - 1);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (OPENERS.has(ch)) depth += 1;
|
|
291
|
+
else if (CLOSERS.has(ch)) {
|
|
292
|
+
depth -= 1;
|
|
293
|
+
if (depth === 0) break;
|
|
294
|
+
} else if (depth === 1 && ch === ':') {
|
|
295
|
+
for (const literal of valueLiterals(masked, source, i + 1, lineAt)) {
|
|
296
|
+
sites.push({ ...literal, at });
|
|
297
|
+
}
|
|
298
|
+
// Past the whole value, because `valueLiterals` already read all of it. A conditional value
|
|
299
|
+
// carries a SECOND colon at this same depth — `a: on ? 'x doctor' : 'x verify'` — and
|
|
300
|
+
// resuming here would read its else branch a second time and report one entry twice.
|
|
301
|
+
i = Math.max(i, entryEnd(masked, i + 1) - 1);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return sites;
|
|
305
|
+
}
|
|
306
|
+
|
|
204
307
|
/**
|
|
205
308
|
* Every string a `fix:` can evaluate to. Searched over the masked source, so a `fix:` written
|
|
206
309
|
* inside a doc comment or interpolated into a message is not mistaken for a declaration. A `fix`
|
|
@@ -227,11 +330,29 @@ export function scanFixSites(
|
|
|
227
330
|
const masked = maskLiterals(source);
|
|
228
331
|
const lineAt = lineIndex(masked);
|
|
229
332
|
const sites: FixSite[] = [];
|
|
333
|
+
const tables = new Set<string>();
|
|
230
334
|
for (const key of masked.matchAll(FIX_KEY)) {
|
|
231
335
|
const start = key.index + key[0].length;
|
|
232
|
-
|
|
233
|
-
|
|
336
|
+
const literals = valueLiterals(masked, source, start, lineAt);
|
|
337
|
+
for (const literal of literals) sites.push({ ...literal, at });
|
|
338
|
+
// A lookup is recorded whether or not the expression also held a literal: `TABLE[k] ?? 'x'`
|
|
339
|
+
// is two answers and both are fix lines. The NAME is collected rather than the table resolved
|
|
340
|
+
// here, so a table read at four call sites is checked once instead of reported four times.
|
|
341
|
+
const head = LOOKUP_HEAD.exec(masked.slice(start, start + 200))?.[1];
|
|
342
|
+
if (head !== undefined) tables.add(head);
|
|
343
|
+
}
|
|
344
|
+
for (const name of tables) {
|
|
345
|
+
const open = tableOpen(masked, name);
|
|
346
|
+
if (open !== undefined) {
|
|
347
|
+
sites.push(...tableFixSites(masked, source, at, open, lineAt));
|
|
348
|
+
continue;
|
|
234
349
|
}
|
|
350
|
+
// Nothing resolved. `init.fix` is a property of a parameter, already read wherever that
|
|
351
|
+
// parameter was filled, and counting it would make the coverage line describe re-passes rather
|
|
352
|
+
// than blind spots. A SCREAMING_SNAKE head is the one that cannot be that: it names a constant,
|
|
353
|
+
// and a constant this file does not declare is a table in another file — a real hole, and the
|
|
354
|
+
// one shape this scan says out loud instead of dropping.
|
|
355
|
+
if (TABLE_NAME.test(name)) unreadable.count += 1;
|
|
235
356
|
}
|
|
236
357
|
// A name declared here wins over one imported under the same name: the declaration is what a
|
|
237
358
|
// call in this file actually reaches, and reading both would report one argument twice.
|
package/src/output.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// and the JSON renderer are projections of it, so `--json` can never drift from the terminal
|
|
3
3
|
// output (axiom 4). The human renderer owns the canonical 3-line error format.
|
|
4
4
|
|
|
5
|
-
import { renderThrowable, stringField } from '@ultimat3/core';
|
|
5
|
+
import { renderThrowable, singleLine, stringField } from '@ultimat3/core';
|
|
6
6
|
import { msg } from './messages';
|
|
7
7
|
|
|
8
8
|
export interface Finding {
|
|
@@ -113,13 +113,16 @@ const summaryOf = (value: UltimateErrorShape): string =>
|
|
|
113
113
|
* ```
|
|
114
114
|
*/
|
|
115
115
|
export function renderFinding(finding: Finding, indent = ''): string {
|
|
116
|
-
|
|
116
|
+
// Every field through `singleLine`: this is the format's only renderer for the terminal and CI
|
|
117
|
+
// logs, and a `cause` carrying a newline would print a line a reader takes for a real finding.
|
|
118
|
+
const code = singleLine(finding.code);
|
|
119
|
+
const head = finding.at === undefined ? code : `${code} (${singleLine(finding.at)})`;
|
|
117
120
|
const lines = [
|
|
118
121
|
`${indent}${head}`,
|
|
119
|
-
`${indent} cause: ${finding.cause}`,
|
|
120
|
-
`${indent} fix: ${finding.fix}`,
|
|
122
|
+
`${indent} cause: ${singleLine(finding.cause)}`,
|
|
123
|
+
`${indent} fix: ${singleLine(finding.fix)}`,
|
|
121
124
|
];
|
|
122
|
-
if (finding.docs !== undefined) lines.push(`${indent} docs: ${finding.docs}`);
|
|
125
|
+
if (finding.docs !== undefined) lines.push(`${indent} docs: ${singleLine(finding.docs)}`);
|
|
123
126
|
return lines.join('\n');
|
|
124
127
|
}
|
|
125
128
|
|
|
@@ -146,7 +146,7 @@ export const config = defineConfig({
|
|
|
146
146
|
// Env KEYS, never the value: the same image deploys to every environment. The database is
|
|
147
147
|
// configured entirely from the environment — \`DATABASE_URL\` and \`DATABASE_POOL_MAX\`.
|
|
148
148
|
cache: { driver: 'memory', tiers: ['memo', 'lru'] },
|
|
149
|
-
jobs: {
|
|
149
|
+
jobs: { queues: ['${app.kebab}-default'], concurrency: 4 },
|
|
150
150
|
// In-process transport by default; set urlEnv and transport: 'nats' to scale past one node.
|
|
151
151
|
realtime: { enabled: true, tier: 'live-queries', transport: 'memory' },
|
|
152
152
|
pwa: { enabled: true, offline: 'runtime' },
|