dreamteamer 0.19.0 → 0.19.1
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 +1 -1
- package/src/cli.js +18 -2
- package/src/collections-cli.js +191 -81
- package/src/schema-ops.js +79 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dreamteamer",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "A workspace compiler for coding agents — schema-validated records as plain files over git, compiled into every harness",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Gilad Khen <giladkhen@gmail.com>",
|
package/src/cli.js
CHANGED
|
@@ -29,7 +29,7 @@ import { envContext, renderTemplate } from './env-vars.js';
|
|
|
29
29
|
const QUIET = ['ignore', 'pipe', 'ignore'];
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
const USAGE = `usage: dreamteamer <verb> [<target>] [flags]
|
|
32
|
+
export const USAGE = `usage: dreamteamer <verb> [<target>] [flags]
|
|
33
33
|
|
|
34
34
|
record verbs (hard validation — invalid writes are rejected before disk).
|
|
35
35
|
A <target> is either a collection name or a <collection>/<id> reference; the reference splits at
|
|
@@ -89,8 +89,10 @@ The commit lands in the repo that holds the source, so a write into a git module
|
|
|
89
89
|
health/doctors; a module declaring exactly ONE
|
|
90
90
|
namespace infers it, and the resolved name is echoed.
|
|
91
91
|
--namespace '' means no namespace)
|
|
92
|
-
add modules --name <id> [--description "…"]
|
|
92
|
+
add modules --name <id> [--description "…"] [--namespace <ns>]
|
|
93
93
|
(modules/<id>/ + every kind folder + package.json.
|
|
94
|
+
--namespace DECLARES it in the module (§8), so every
|
|
95
|
+
later \`add collections --module <id>\` infers it.
|
|
94
96
|
folder = package name = id, so a module never forks.
|
|
95
97
|
the git shape is \`install --clone <url> [name]\`)
|
|
96
98
|
add skills --name <id> --description "…" (skills/<id>/SKILL.md — --description is required,
|
|
@@ -192,9 +194,23 @@ const EITHER_VERBS = new Set(['move', 'commands']);
|
|
|
192
194
|
// verbs, and `collectionCommand`'s interceptors are the whole dispatch (§4).
|
|
193
195
|
const FIELD_VERBS = ['add-field', 'update-field', 'remove-field', 'rename-field'];
|
|
194
196
|
|
|
197
|
+
// WORKSPACE VERBS take a closed set of options, and nothing downstream of here would notice a
|
|
198
|
+
// misspelling — `dt commit --dryrun` COMMITTED, because `rest.includes('--dry-run')` is false for a
|
|
199
|
+
// flag nobody typed correctly. The record/system/field verbs are checked in `collections-cli.js`,
|
|
200
|
+
// beside the parser they share; these nine have no shared parser, so the table is here.
|
|
201
|
+
export const WORKSPACE_FLAGS = {
|
|
202
|
+
init: ['name', 'data-path', 'harnesses', 'workspace-module'], install: ['clone'], update: [],
|
|
203
|
+
start: ['port'], compile: ['watch'], check: [], status: [],
|
|
204
|
+
changes: ['since', 'json'], commit: ['dry-run', 'json'],
|
|
205
|
+
};
|
|
206
|
+
|
|
195
207
|
export function run(argv) {
|
|
196
208
|
const [cmd, ...rest] = argv;
|
|
197
209
|
try {
|
|
210
|
+
if (cmd in WORKSPACE_FLAGS) {
|
|
211
|
+
const bad = rest.filter((a) => a.startsWith('--')).map((a) => a.slice(2).split('=')[0]).find((f) => !WORKSPACE_FLAGS[cmd].includes(f));
|
|
212
|
+
if (bad) throw new Error(`unknown flag "--${bad}" on \`dt ${cmd}\`\n known: ${WORKSPACE_FLAGS[cmd].map((f) => `--${f}`).join(', ') || '(none — this verb takes no flags)'}`);
|
|
213
|
+
}
|
|
198
214
|
if (cmd === '--version' || cmd === '-v' || cmd === 'version') {
|
|
199
215
|
// works OUTSIDE a workspace — the post-install "did it land?" affordance
|
|
200
216
|
const p = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
package/src/collections-cli.js
CHANGED
|
@@ -78,6 +78,7 @@ const LIST_META_FLAGS = new Set(['json', 'filter', 'where', 'sort']);
|
|
|
78
78
|
export function collectionCommand(ws, collection, verb, args) {
|
|
79
79
|
const store = new Store(ws);
|
|
80
80
|
const { flags, pos } = parseArgs(args);
|
|
81
|
+
refuseUnknownFlags(store, collection, verb, flags);
|
|
81
82
|
|
|
82
83
|
// ---- system verbs: source writes, never the runtime ----------
|
|
83
84
|
// These MUST come before the generic switch: their collections are system-stored, so the
|
|
@@ -137,6 +138,17 @@ export function collectionCommand(ws, collection, verb, args) {
|
|
|
137
138
|
// panel that produced that JSON cannot disagree about which records match.
|
|
138
139
|
const whereJson = oneValue(flags, 'where');
|
|
139
140
|
const where = whereJson ? load(whereJson) : null;
|
|
141
|
+
// ⚠ A FLAG NAME IS NOT THE ONLY THING THAT CAN BE MISSPELLED. `--filter nmae=Ada` and
|
|
142
|
+
// `--sort nmae` are correctly spelled FLAGS whose VALUE names a field that does not exist,
|
|
143
|
+
// and both answer at exit 0 — an empty listing and an unsorted one. Same silent-empty class,
|
|
144
|
+
// worse than a bad write because there is nothing to notice. `--where` gets the type check
|
|
145
|
+
// for the same reason: `--where 'name _eq Ada'` yaml-parses to a STRING, and matchesFilter
|
|
146
|
+
// then matched every row.
|
|
147
|
+
if (whereJson && (typeof where !== 'object' || where === null)) throw new Error(`--where takes ONE filter OBJECT and got a ${where === null ? 'null' : typeof where}: ${whereJson}\n a condition is {"<field>":{"_eq":"<value>"}} — the shorthand for one equality is --filter <field>=<value>`);
|
|
148
|
+
const sort = oneValue(flags, 'sort');
|
|
149
|
+
const vocab = ['id', ...Object.keys(d.schema?.properties ?? {})];
|
|
150
|
+
const stray = [...filters.map(([k]) => k), ...(sort ? [String(sort).replace(/^-/, '')] : [])].find((f) => !vocab.includes(f));
|
|
151
|
+
if (stray) throw new Error(`${collection} has no field "${stray}"${nearest(stray, vocab) ? ` — did you mean "${nearest(stray, vocab)}"?` : ''} (dt get collections/${collection} lists them)`);
|
|
140
152
|
const resolve = where ? recordResolver(store) : null;
|
|
141
153
|
const bf = bodyField(d);
|
|
142
154
|
const rows = [];
|
|
@@ -149,7 +161,6 @@ export function collectionCommand(ws, collection, verb, args) {
|
|
|
149
161
|
// sorting was studio-only until now: the browse table ordered records and no CLI
|
|
150
162
|
// invocation could. Same `sortRows` the server and api.ts call, so `--sort -starts`
|
|
151
163
|
// orders date-times by INSTANT across mixed offsets rather than by string.
|
|
152
|
-
const sort = oneValue(flags, 'sort');
|
|
153
164
|
if (sort) sortRows(rows, sort);
|
|
154
165
|
if (flags.json) { emit(JSON.stringify(rows, null, 2)); return 0; }
|
|
155
166
|
const cols = ['id', ...(d.list_fields ?? []).filter((c) => c !== 'id')];
|
|
@@ -224,6 +235,7 @@ export function collectionCommand(ws, collection, verb, args) {
|
|
|
224
235
|
}
|
|
225
236
|
case 'rm': {
|
|
226
237
|
const id = need(pos, 0, 'id');
|
|
238
|
+
if (flags['dry-run']) return dryRunPlan(`rm ${collection}/${id}`, { records: store.ids(collection).has(id) ? 1 : 0, refs: store.findInboundRefs(`${collection}/${id}`).length });
|
|
227
239
|
const { inboundIgnored } = store.rm(collection, id, { force: !!flags.force });
|
|
228
240
|
flags.json ? emit(JSON.stringify({ id, removed: true, inboundIgnored })) : console.log(`✔ removed${inboundIgnored ? ` (${inboundIgnored} inbound reference(s) left dangling — run \`dreamteamer check\`)` : ''}`);
|
|
229
241
|
return 0;
|
|
@@ -240,9 +252,11 @@ export function collectionCommand(ws, collection, verb, args) {
|
|
|
240
252
|
// no enum describes (operator: "still no dropdown for many things, visibility, status").
|
|
241
253
|
case 'values': {
|
|
242
254
|
const field = need(pos, 0, 'field');
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
255
|
+
// ⚠ A BARE `--limit` parses as the boolean `true`, and `Number(true)` is 1 — so it
|
|
256
|
+
// truncated the vocabulary to one value while the footer claimed "showing 1", which reads
|
|
257
|
+
// as a fact about the collection. Refused, in `--drop`'s words.
|
|
258
|
+
if ('limit' in flags && !Number.isFinite(Number(oneValue(flags, 'limit')))) throw new Error(`--limit needs a number: dreamteamer values ${collection} ${field} --limit 20`);
|
|
259
|
+
const out = distinctValues(store, collection, field, { limit: flags.limit === undefined ? undefined : Number(flags.limit) });
|
|
246
260
|
if (flags.json) { emit(JSON.stringify(out, null, 2)); return 0; }
|
|
247
261
|
if (out.skipped) { console.log(`(${collection}.${field} is a ${out.skipped} field — no value vocabulary)`); return 0; }
|
|
248
262
|
if (!out.values.length) { console.log(`(no values set on ${collection}.${field})`); return 0; }
|
|
@@ -326,10 +340,20 @@ function metaReposEnsure(ws, flags, pos) {
|
|
|
326
340
|
|
|
327
341
|
// `dreamteamer add modules --name core [--description "…"]`
|
|
328
342
|
function metaModulesAdd(ws, store, flags) {
|
|
329
|
-
|
|
330
|
-
|
|
343
|
+
// ⚠ BEFORE the write, as `add collections` does it and for the same reason: naming the mistake
|
|
344
|
+
// after the call lands a committed module and then exits 1.
|
|
345
|
+
if (flags.namespace === true) throw new Error("--namespace takes a value: dreamteamer add modules --name <id> --namespace <ns>");
|
|
346
|
+
const out = createModule(ws, store, {
|
|
347
|
+
name: oneValue(flags, 'name'),
|
|
348
|
+
description: oneValue(flags, 'description'),
|
|
349
|
+
namespace: oneValue(flags, 'namespace'),
|
|
350
|
+
});
|
|
331
351
|
if (flags.json) { emit(JSON.stringify(out)); return 0; }
|
|
332
352
|
console.log(`✔ ${out.root}/ — package.json + ${KIND_COUNT} kind folder(s)`);
|
|
353
|
+
// §6.2 promises the namespace is DECLARED IN THE MODULE, and §8 makes every later
|
|
354
|
+
// `add collections --module <id>` infer it — so the declaration is echoed, because an inferred
|
|
355
|
+
// identity the operator did not type is one they must be able to read back.
|
|
356
|
+
if (out.namespace) console.log(`✔ declared namespace "${out.namespace}" in modules/${out.id}`);
|
|
333
357
|
console.log('✔ compiled — the module is live (add a collection with `dreamteamer add collections --name <c> --module ' + out.id + '`)');
|
|
334
358
|
reportCommits(out.commits);
|
|
335
359
|
return 0;
|
|
@@ -337,18 +361,14 @@ function metaModulesAdd(ws, store, flags) {
|
|
|
337
361
|
|
|
338
362
|
// `dreamteamer rm modules/core [--force] [--dry-run]`
|
|
339
363
|
function metaModulesRm(ws, store, flags, pos) {
|
|
340
|
-
refuseRepeats(flags);
|
|
341
364
|
const id = need(pos, 0, 'module id');
|
|
342
365
|
const out = removeModule(ws, store, id, { force: !!flags.force, dryRun: !!flags['dry-run'] });
|
|
343
366
|
if (flags.json) { emit(JSON.stringify(out)); return 0; }
|
|
344
|
-
if (out.dryRun) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (out.dependents.length) console.log(` dependencies entry dropped from: ${out.dependents.join(', ')}`);
|
|
350
|
-
return 0;
|
|
351
|
-
}
|
|
367
|
+
if (out.dryRun) return dryRunPlan(`rm modules/${id} --force`, out, [
|
|
368
|
+
out.collections.length ? `sources removed for: ${out.collections.join(', ')}` : null,
|
|
369
|
+
out.withRecords.length ? `records left in place and UNINDEXED: ${out.withRecords.join(', ')}` : null,
|
|
370
|
+
out.dependents.length ? `dependencies entry dropped from: ${out.dependents.join(', ')}` : null,
|
|
371
|
+
]);
|
|
352
372
|
console.log(`✔ removed module ${out.removed}`);
|
|
353
373
|
if (out.withRecords.length) console.log(` ⚠ records remain and are now unindexed: ${out.withRecords.join(', ')}`);
|
|
354
374
|
if (out.dependents.length) console.log(` dropped it from ${out.dependents.join(', ')}'s dependencies`);
|
|
@@ -359,7 +379,6 @@ function metaModulesRm(ws, store, flags, pos) {
|
|
|
359
379
|
|
|
360
380
|
// `dreamteamer rename modules/core shared`
|
|
361
381
|
function metaModulesRename(ws, store, flags, pos) {
|
|
362
|
-
refuseRepeats(flags);
|
|
363
382
|
const oldId = need(pos, 0, 'module id');
|
|
364
383
|
const out = renameModule(ws, store, oldId, need(pos, 1, 'new module id'));
|
|
365
384
|
if (flags.json) { emit(JSON.stringify(out)); return 0; }
|
|
@@ -373,12 +392,12 @@ function metaModulesRename(ws, store, flags, pos) {
|
|
|
373
392
|
|
|
374
393
|
// `dreamteamer set modules/hr description="…" dependencies=modules/core`
|
|
375
394
|
function metaModulesSet(ws, store, flags, pos) {
|
|
376
|
-
refuseRepeats(flags);
|
|
377
395
|
const id = need(pos, 0, 'module id');
|
|
378
396
|
const changes = { ...pairs(pos.slice(1)), ...stripMeta(flags) };
|
|
379
397
|
if (!Object.keys(changes).length) throw new Error('nothing to set — pass key=value pairs (description, dependencies, peerDependencies)');
|
|
380
398
|
const out = setModule(ws, store, id, changes);
|
|
381
399
|
if (flags.json) { emit(JSON.stringify(out)); return 0; }
|
|
400
|
+
if (out.unchanged) return alreadyThat(`modules/${id} ${out.changed.join(', ')}`);
|
|
382
401
|
console.log(`✔ ${rel(ws.root, out.file)} — ${out.changed.join(', ')}`);
|
|
383
402
|
console.log('✔ compiled — the module record is up to date');
|
|
384
403
|
reportCommits(out.commits);
|
|
@@ -389,11 +408,26 @@ function metaModulesSet(ws, store, flags, pos) {
|
|
|
389
408
|
* KINDS so the sentence cannot go stale against the list it describes. */
|
|
390
409
|
const KIND_COUNT = KINDS.length;
|
|
391
410
|
|
|
392
|
-
/**
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
|
|
396
|
-
|
|
411
|
+
/**
|
|
412
|
+
* THE ONE PLAN PRINTER, for every verb that moves records or clears values — one shape, `records N ·
|
|
413
|
+
* refs M · descriptors K · values cleared V`, because a reader comparing two dry runs must not have
|
|
414
|
+
* to work out whether a missing term means zero or means "this verb does not count that".
|
|
415
|
+
*
|
|
416
|
+
* ⚠ FOUR OF THE SIX SPELLINGS DOCUMENTED `--dry-run` AND EXECUTED ANYWAY, and the self-commit made
|
|
417
|
+
* it durable: `dt rm collections/widgets --dry-run --force` printed "✔ removed collection widgets",
|
|
418
|
+
* deleted the source and committed it, at exit 0, against a `dt help` that spells `rm <system>/<id>
|
|
419
|
+
* [--force] [--dry-run]` verbatim. `rm modules/<id>` was the ONE that honoured it. A flag a verb
|
|
420
|
+
* advertises and ignores is worse than one it does not have — the operator's whole reason for typing
|
|
421
|
+
* it is that they are not sure yet.
|
|
422
|
+
*
|
|
423
|
+
* It counts only what it can count WITHOUT doing the op; an unmeasured term is stated as such in
|
|
424
|
+
* `extra` rather than guessed at, per `rename collections`' dry run.
|
|
425
|
+
*/
|
|
426
|
+
function dryRunPlan(what, plan, extra) {
|
|
427
|
+
console.log(`dry run — dreamteamer ${what} would:`);
|
|
428
|
+
console.log(` records ${plan.records ?? 0} · refs ${plan.refs ?? 0} · descriptors ${plan.descriptors ?? 0} · values cleared ${plan.cleared ?? 0}`);
|
|
429
|
+
for (const line of extra ?? []) if (line) console.log(` ${line}`);
|
|
430
|
+
return 0;
|
|
397
431
|
}
|
|
398
432
|
|
|
399
433
|
/**
|
|
@@ -417,7 +451,6 @@ function reportCommits(commits) {
|
|
|
417
451
|
|
|
418
452
|
// `dreamteamer collections add --name research-docs --template docs`
|
|
419
453
|
function metaCollectionsAdd(ws, store, flags) {
|
|
420
|
-
refuseRepeats(flags);
|
|
421
454
|
// ⚠ BEFORE the write, not after. `--namespace=` is the empty STRING (clear it); a bare
|
|
422
455
|
// `--namespace` parses as `true`, which is a mistake worth naming — and naming it AFTER the call
|
|
423
456
|
// lands a committed collection and then exits 1, which is the one report shape that lies twice.
|
|
@@ -452,7 +485,6 @@ function metaCollectionsAdd(ws, store, flags) {
|
|
|
452
485
|
* half-applies or has to be explained, and the operator gets no signal about which happened.
|
|
453
486
|
*/
|
|
454
487
|
function metaCollectionsSet(ws, store, flags, pos) {
|
|
455
|
-
refuseRepeats(flags);
|
|
456
488
|
const name = need(pos, 0, 'collection name');
|
|
457
489
|
const positional = pairs(pos.slice(1));
|
|
458
490
|
const changes = { ...positional, ...stripMeta(flags) };
|
|
@@ -465,12 +497,7 @@ function metaCollectionsSet(ws, store, flags, pos) {
|
|
|
465
497
|
if (moveTo !== undefined) {
|
|
466
498
|
const out = moveCollection(ws, store, name, String(moveTo), { dryRun: !!flags['dry-run'] });
|
|
467
499
|
if (flags.json) { emit(JSON.stringify(out)); return 0; }
|
|
468
|
-
if (out.dryRun) {
|
|
469
|
-
console.log(`dry run — dreamteamer set collections/${name} module=${moveTo} would:`);
|
|
470
|
-
console.log(planLine(out));
|
|
471
|
-
console.log(` descriptor ${out.from} → ${out.to} (records stay where they are)`);
|
|
472
|
-
return 0;
|
|
473
|
-
}
|
|
500
|
+
if (out.dryRun) return dryRunPlan(`set collections/${name} module=${moveTo}`, out, [`descriptor ${out.from} → ${out.to} (records stay where they are)`]);
|
|
474
501
|
if (!out.moved) { console.log(`✔ ${name} — already owned by ${out.from}, nothing to do`); return 0; }
|
|
475
502
|
console.log(`✔ ${name}: ${out.from} → ${out.to}`);
|
|
476
503
|
console.log(` records ${out.records} left in place — a move never changes an id`);
|
|
@@ -482,6 +509,7 @@ function metaCollectionsSet(ws, store, flags, pos) {
|
|
|
482
509
|
if (!Object.keys(changes).length) throw new Error('nothing to set — pass key=value pairs, or module=<id> to move it');
|
|
483
510
|
const out = setCollectionScalars(ws, store, name, changes, { moduleId: undefined });
|
|
484
511
|
if (flags.json) { emit(JSON.stringify(out)); return 0; }
|
|
512
|
+
if (out.unchanged) return alreadyThat(`collections/${name} ${out.changed.join(', ')}`);
|
|
485
513
|
console.log(`✔ ${rel(ws.root, out.file)} — ${out.changed.join(', ')}`);
|
|
486
514
|
console.log('✔ compiled — the descriptor is up to date');
|
|
487
515
|
reportCommits(out.commits);
|
|
@@ -500,7 +528,6 @@ function metaCollectionsSet(ws, store, flags, pos) {
|
|
|
500
528
|
* it writes ONE descriptor and keeps the numbers legible.
|
|
501
529
|
*/
|
|
502
530
|
function metaCollectionsMove(ws, store, flags, pos) {
|
|
503
|
-
refuseRepeats(flags);
|
|
504
531
|
const name = need(pos, 0, 'collection name');
|
|
505
532
|
store.descriptor(name);
|
|
506
533
|
const rows = [...store.readAll('collections')]
|
|
@@ -541,7 +568,6 @@ function metaCollectionsMove(ws, store, flags, pos) {
|
|
|
541
568
|
* this is the only way to see what a given module actually wrote, which is the question an overlay
|
|
542
569
|
* makes unanswerable from the runtime alone. */
|
|
543
570
|
function metaCollectionsGet(ws, store, flags, pos) {
|
|
544
|
-
refuseRepeats(flags);
|
|
545
571
|
const name = need(pos, 0, 'collection name');
|
|
546
572
|
const moduleId = oneValue(flags, 'module');
|
|
547
573
|
const { file } = collectionSourceFileFor(ws, store, name, moduleId);
|
|
@@ -554,7 +580,6 @@ function metaCollectionsGet(ws, store, flags, pos) {
|
|
|
554
580
|
// The whole point is that namespacing EXISTING data is one command instead of a six-step hand
|
|
555
581
|
// migration whose last step (rewriting references) dangles everything when forgotten.
|
|
556
582
|
function metaCollectionsRename(ws, store, flags, pos) {
|
|
557
|
-
refuseRepeats(flags);
|
|
558
583
|
const [oldName, explicitNew] = pos;
|
|
559
584
|
if (!oldName) throw new Error('usage: dreamteamer rename collections/<old> <new> | collections/<old> --namespace <ns>');
|
|
560
585
|
// `--namespace health` on its own moves the collection INTO that namespace keeping its bare name,
|
|
@@ -566,15 +591,14 @@ function metaCollectionsRename(ws, store, flags, pos) {
|
|
|
566
591
|
if (flags['dry-run']) {
|
|
567
592
|
const d = store.descriptor(oldName);
|
|
568
593
|
const records = store.ids(oldName).size;
|
|
569
|
-
console.log(`dry run — dreamteamer rename collections/${oldName} ${newName} would:`);
|
|
570
|
-
console.log(planLine({ records, refs: 0, descriptors: 1, cleared: 0 }));
|
|
571
|
-
console.log(` records ${d.storage.path} → ${defaultStoragePath(newName, store.namespaces, ws.pkg.dreamteamer?.['data-path'] ?? 'data')}`);
|
|
572
594
|
// ⚠ `refs` is honestly 0 here and the line says so. Counting them would mean running the batch
|
|
573
595
|
// rewrite to find out, which IS the op — and a number the plan cannot know is worse than a
|
|
574
596
|
// stated gap: the plan line has a fixed shape precisely so a reader never has to guess whether
|
|
575
597
|
// a term is zero or unmeasured.
|
|
576
|
-
|
|
577
|
-
|
|
598
|
+
return dryRunPlan(`rename collections/${oldName} ${newName}`, { records, descriptors: 1 }, [
|
|
599
|
+
`records ${d.storage.path} → ${defaultStoragePath(newName, store.namespaces, ws.pkg.dreamteamer?.['data-path'] ?? 'data')}`,
|
|
600
|
+
'refs are counted only by the real run — the rewrite is what discovers them',
|
|
601
|
+
]);
|
|
578
602
|
}
|
|
579
603
|
const out = renameCollection(ws, store, oldName, newName);
|
|
580
604
|
if (flags.json) { emit(JSON.stringify(out)); return 0; }
|
|
@@ -592,8 +616,8 @@ function metaCollectionsRename(ws, store, flags, pos) {
|
|
|
592
616
|
// `dreamteamer collections rm widgets [--force]` — --force is required to drop a collection
|
|
593
617
|
// that still has records (removeCollection refuses otherwise, and says so).
|
|
594
618
|
function metaCollectionsRm(ws, store, flags, pos) {
|
|
595
|
-
refuseRepeats(flags);
|
|
596
619
|
const name = need(pos, 0, 'collection name');
|
|
620
|
+
if (flags['dry-run']) return dryRunPlan(`rm collections/${name}`, { records: store.ids(name).size, descriptors: 1 }, [`descriptor removed; records under ${store.descriptor(name).storage.path} stay in place and become unindexed`]);
|
|
597
621
|
const out = removeCollection(ws, store, name, { force: !!flags.force });
|
|
598
622
|
flags.json ? emit(JSON.stringify(out)) : console.log(`✔ removed collection ${out.removed}`);
|
|
599
623
|
console.log('✔ compiled — the collection is gone');
|
|
@@ -603,7 +627,6 @@ function metaCollectionsRm(ws, store, flags, pos) {
|
|
|
603
627
|
|
|
604
628
|
// `dreamteamer tasks add-field --name urgent --type boolean --default-value false`
|
|
605
629
|
function metaAddField(ws, store, collection, flags) {
|
|
606
|
-
refuseRepeats(flags);
|
|
607
630
|
const prop = fieldDef(store, flags, collection);
|
|
608
631
|
// fieldDef DEFERS every relation flag it has no reference to attach to, because on update-field
|
|
609
632
|
// the target is carried in afterwards. add-field has nothing to carry, so a relation flag that
|
|
@@ -611,7 +634,7 @@ function metaAddField(ws, store, collection, flags) {
|
|
|
611
634
|
const stray = (prop.items ?? prop)['x-reference'] === undefined && relationFlagsStated(flags);
|
|
612
635
|
if (stray) throw new Error(`--${stray} needs a --type <collection> reference.`);
|
|
613
636
|
const out = addField(ws, store, collection, { name: flags.name, prop, required: flags.required === 'true', moduleId: oneValue(flags, 'module') });
|
|
614
|
-
if (out.unchanged) return alreadyThat(collection
|
|
637
|
+
if (out.unchanged) return alreadyThat(`${collection}.${flags.name}`);
|
|
615
638
|
console.log(`✔ ${rel(ws.root, out.file)}${out.extends ? ` (extends ${out.extends})` : ''}`);
|
|
616
639
|
console.log('✔ compiled — the field is live');
|
|
617
640
|
reportCommits(out.commits);
|
|
@@ -622,8 +645,8 @@ function metaAddField(ws, store, collection, flags) {
|
|
|
622
645
|
|
|
623
646
|
/** The idempotent answer, in `rename-collection`'s words — a command that asks for what is already
|
|
624
647
|
* there succeeded, and the operator needs to know which field it was talking about. */
|
|
625
|
-
function alreadyThat(
|
|
626
|
-
console.log(`✔ ${
|
|
648
|
+
function alreadyThat(subject) {
|
|
649
|
+
console.log(`✔ ${subject} — already exactly that, nothing to do`);
|
|
627
650
|
return 0;
|
|
628
651
|
}
|
|
629
652
|
|
|
@@ -655,7 +678,6 @@ function reportMirror(store, collection, fieldName, prop) {
|
|
|
655
678
|
// Same flag vocabulary as add-field (one `fieldDef`), so the two read as one operation with two
|
|
656
679
|
// preconditions rather than two dialects.
|
|
657
680
|
function metaUpdateField(ws, store, collection, flags) {
|
|
658
|
-
refuseRepeats(flags);
|
|
659
681
|
if (!flags.name) throw new Error('missing --name <field>');
|
|
660
682
|
const prop = fieldDef(store, flags, collection);
|
|
661
683
|
// tri-state: omitting --required leaves requiredness ALONE, rather than silently clearing it
|
|
@@ -663,7 +685,7 @@ function metaUpdateField(ws, store, collection, flags) {
|
|
|
663
685
|
// `flags` for the VALUES and `stated` for what the caller meant to restate: updateField carries
|
|
664
686
|
// every unstated relation keyword forward from the previous prop.
|
|
665
687
|
const out = updateField(ws, store, collection, flags.name, { prop, required, flags, stated: statedKeywords(flags), moduleId: oneValue(flags, 'module') });
|
|
666
|
-
if (out.unchanged) return alreadyThat(collection
|
|
688
|
+
if (out.unchanged) return alreadyThat(`${collection}.${flags.name}`);
|
|
667
689
|
console.log(`✔ ${rel(ws.root, out.file)}${out.extends ? ` (extends ${out.extends})` : ''}`);
|
|
668
690
|
console.log('✔ compiled — the field is updated');
|
|
669
691
|
reportCommits(out.commits);
|
|
@@ -677,16 +699,12 @@ function metaUpdateField(ws, store, collection, flags) {
|
|
|
677
699
|
|
|
678
700
|
// `dreamteamer tasks remove-field --name urgent`
|
|
679
701
|
function metaRemoveField(ws, store, collection, flags) {
|
|
680
|
-
refuseRepeats(flags);
|
|
681
702
|
const name = flags.name ?? flags.field;
|
|
682
703
|
if (!name) throw new Error('missing --name <field>');
|
|
683
704
|
const moduleId = oneValue(flags, 'module');
|
|
684
705
|
if (flags['dry-run']) {
|
|
685
706
|
const plan = removeFieldPlan(store, collection, name);
|
|
686
|
-
|
|
687
|
-
console.log(planLine(plan));
|
|
688
|
-
if (plan.staleViews.length) console.log(` ui-views still listing it as a column: ${plan.staleViews.join(', ')}`);
|
|
689
|
-
return 0;
|
|
707
|
+
return dryRunPlan(`remove-field ${collection} --name ${name}`, plan, [plan.staleViews.length ? `ui-views still listing it as a column: ${plan.staleViews.join(', ')}` : null]);
|
|
690
708
|
}
|
|
691
709
|
const out = removeField(ws, store, collection, name, { moduleId });
|
|
692
710
|
flags.json ? emit(JSON.stringify(out)) : console.log(`✔ removed field ${collection}.${out.removed}`);
|
|
@@ -709,7 +727,6 @@ const ENTITY_KINDS = new Set(['skills', 'agents', 'commands', 'command-bindings'
|
|
|
709
727
|
const SCAFFOLDABLE = new Set(['skills']);
|
|
710
728
|
|
|
711
729
|
function metaEntityVerb(ws, store, kind, verb, flags, pos) {
|
|
712
|
-
refuseRepeats(flags);
|
|
713
730
|
const one = kind.replace(/s$/, '');
|
|
714
731
|
if (verb === 'add') {
|
|
715
732
|
const name = oneValue(flags, 'name');
|
|
@@ -726,7 +743,9 @@ function metaEntityVerb(ws, store, kind, verb, flags, pos) {
|
|
|
726
743
|
return 0;
|
|
727
744
|
}
|
|
728
745
|
if (verb === 'rm') {
|
|
729
|
-
const
|
|
746
|
+
const id0 = need(pos, 0, `${one} id`);
|
|
747
|
+
if (flags['dry-run']) return dryRunPlan(`rm ${kind}/${id0}`, { descriptors: 1 }, [`the ${one} source is removed; nothing else moves`]);
|
|
748
|
+
const out = removeEntity(ws, store, kind, id0);
|
|
730
749
|
if (flags.json) { emit(JSON.stringify(out)); return 0; }
|
|
731
750
|
console.log(`✔ removed ${one} ${out.removed}`);
|
|
732
751
|
console.log('✔ compiled — it is gone');
|
|
@@ -748,6 +767,7 @@ function metaEntityVerb(ws, store, kind, verb, flags, pos) {
|
|
|
748
767
|
if (!Object.keys(changes).length) throw new Error(`nothing to set — pass key=value pairs (a ${one}'s frontmatter keys)`);
|
|
749
768
|
const out = setEntityFrontmatter(ws, store, kind, id, changes);
|
|
750
769
|
if (flags.json) { emit(JSON.stringify(out)); return 0; }
|
|
770
|
+
if (out.unchanged) return alreadyThat(`${kind}/${id} ${out.changed.join(', ')}`);
|
|
751
771
|
console.log(`✔ ${rel(ws.root, out.file)} — ${out.changed.join(', ')}`);
|
|
752
772
|
console.log('✔ compiled — the change is live');
|
|
753
773
|
reportCommits(out.commits);
|
|
@@ -756,25 +776,27 @@ function metaEntityVerb(ws, store, kind, verb, flags, pos) {
|
|
|
756
776
|
|
|
757
777
|
/** The path `revert`'s refusal names — where a human edits this compiled entity. */
|
|
758
778
|
function sourceHintFor(store, collection) {
|
|
759
|
-
|
|
760
|
-
|
|
779
|
+
// ⚠ `modules` is the one system collection that is PROJECTED rather than stored as a kind folder:
|
|
780
|
+
// its source is each module's package.json. Deriving the hint from `storage.path` gave
|
|
781
|
+
// `modules/*/modules/`, which `git ls-files` matches nothing at all — a correct refusal handing
|
|
782
|
+
// over an unusable remedy.
|
|
783
|
+
if (collection === 'modules') return 'modules/*/package.json';
|
|
784
|
+
return `modules/*/${store.descriptors.get(collection)?.storage?.path ?? collection}/`;
|
|
761
785
|
}
|
|
762
786
|
|
|
763
787
|
// `dreamteamer rename-field people --name employer --to company`
|
|
764
788
|
function metaRenameField(ws, store, collection, flags) {
|
|
765
|
-
refuseRepeats(flags);
|
|
766
789
|
const from = oneValue(flags, 'name') ?? oneValue(flags, 'field');
|
|
767
790
|
if (!from) throw new Error(`missing --name <field>: dreamteamer rename-field ${collection} --name <field> --to <new-name>`);
|
|
768
791
|
const to = oneValue(flags, 'to');
|
|
769
792
|
if (flags['dry-run']) {
|
|
770
793
|
const plan = renameFieldPlan(store, collection, from);
|
|
771
|
-
console.log(`dry run — dreamteamer rename-field ${collection} --name ${from} --to ${to ?? '<new-name>'} would:`);
|
|
772
|
-
console.log(planLine(plan));
|
|
773
794
|
// ⚠ The same honesty the `rename collections` dry run needs, for the same reason: a number the
|
|
774
795
|
// plan cannot know is worse than a stated gap.
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
796
|
+
return dryRunPlan(`rename-field ${collection} --name ${from} --to ${to ?? '<new-name>'}`, plan, [
|
|
797
|
+
'descriptors, ui-views and command-bindings naming it are counted by the real run —',
|
|
798
|
+
'the rewrite is what discovers which of them carry the name',
|
|
799
|
+
]);
|
|
778
800
|
}
|
|
779
801
|
const out = renameField(ws, store, collection, from, to, { moduleId: oneValue(flags, 'module') });
|
|
780
802
|
if (flags.json) { emit(JSON.stringify(out)); return 0; }
|
|
@@ -877,11 +899,16 @@ function assignViewValue(view, key, raw) {
|
|
|
877
899
|
assignPath(view, key, parseViewValue(raw, key), typeof raw === 'string' && raw.trim().startsWith('"'));
|
|
878
900
|
}
|
|
879
901
|
|
|
880
|
-
|
|
902
|
+
// ⚠ `module` is a VERB OPTION, exactly as it is on every other verb that takes one (see META_FLAGS)
|
|
903
|
+
// — it says WHERE the source lands and is never a key of the view. Omitting it here is what wrote
|
|
904
|
+
// `module: core` into the yaml as a field.
|
|
905
|
+
const VIEW_META_FLAGS = new Set(['id', 'json', 'force', 'module']);
|
|
881
906
|
|
|
882
907
|
function metaUiView(ws, store, verb, flags, pos) {
|
|
883
908
|
if (verb === 'rm') {
|
|
884
|
-
const
|
|
909
|
+
const viewId = need(pos, 0, 'ui-view id');
|
|
910
|
+
if (flags['dry-run']) return dryRunPlan(`rm ui-views/${viewId}`, { descriptors: 1 }, ['the view source is removed; its route stops resolving']);
|
|
911
|
+
const out = removeUiView(ws, store, viewId);
|
|
885
912
|
flags.json ? emit(JSON.stringify(out)) : console.log(`✔ removed ui-view ${out.removed}`);
|
|
886
913
|
console.log('✔ compiled — the route is gone');
|
|
887
914
|
reportCommits(out.commits);
|
|
@@ -919,8 +946,10 @@ function metaUiView(ws, store, verb, flags, pos) {
|
|
|
919
946
|
// saved from the CLI and one saved from the panel land on the SAME record.
|
|
920
947
|
id ??= slug(view.path);
|
|
921
948
|
|
|
922
|
-
const out = saveUiView(ws, store, { id, view });
|
|
923
|
-
flags.json
|
|
949
|
+
const out = saveUiView(ws, store, { id, view, moduleId: oneValue(flags, 'module') });
|
|
950
|
+
if (flags.json) { emit(JSON.stringify(out)); return 0; }
|
|
951
|
+
if (out.unchanged) return alreadyThat(`ui-views/${id}`);
|
|
952
|
+
console.log(`✔ ${rel(ws.root, out.file)}`);
|
|
924
953
|
console.log(`✔ compiled — ${view.path} is live`);
|
|
925
954
|
reportCommits(out.commits);
|
|
926
955
|
return 0;
|
|
@@ -938,7 +967,9 @@ function metaUiView(ws, store, verb, flags, pos) {
|
|
|
938
967
|
export function relationsCommand(ws, args) {
|
|
939
968
|
const store = new Store(ws);
|
|
940
969
|
const { flags, pos } = parseArgs(args);
|
|
941
|
-
|
|
970
|
+
const rebuilding = pos[0] === 'rebuild';
|
|
971
|
+
refuseUnknownFlags(store, (rebuilding ? pos[1] : pos[0]) ?? '', rebuilding ? 'rebuild' : 'relations', flags);
|
|
972
|
+
if (rebuilding) return relationsRebuild(store, flags, pos);
|
|
942
973
|
|
|
943
974
|
// `store.relations()` is `relationsOf(this.descriptors)` memoized per Store — going through it
|
|
944
975
|
// rather than calling relationsOf here keeps one decoder for the whole process.
|
|
@@ -1093,22 +1124,6 @@ function oneValue(flags, key) {
|
|
|
1093
1124
|
return typeof v === 'string' ? v : undefined;
|
|
1094
1125
|
}
|
|
1095
1126
|
|
|
1096
|
-
/**
|
|
1097
|
-
* A schema verb takes ONE value per flag, and a repeat is refused before anything is written.
|
|
1098
|
-
*
|
|
1099
|
-
* None of them means a list by repetition — `--options a,b` is one value, and `--name` names the
|
|
1100
|
-
* single thing being written — so every repeat is a mistake. It has to be caught here because the
|
|
1101
|
-
* ones that matter are IDENTITY: `--name x --name y` wrote a field called `y` before the promotion
|
|
1102
|
-
* and one called `x,y` after it, and both of those are a source file the operator then has to find
|
|
1103
|
-
* and edit by hand.
|
|
1104
|
-
*/
|
|
1105
|
-
function refuseRepeats(flags) {
|
|
1106
|
-
const dup = Object.entries(flags).find(([, v]) => Array.isArray(v));
|
|
1107
|
-
if (!dup) return;
|
|
1108
|
-
const [k, v] = dup;
|
|
1109
|
-
throw new Error(`--${k} was given ${v.length} times, and a schema verb takes ONE value per flag: ${v.map((x) => `--${k} ${x}`).join(' ')}`);
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
1127
|
/** `field=value` positionals, with the same promote-on-repeat rule the flags have. It was
|
|
1113
1128
|
* `Object.fromEntries`, which keeps the LAST pair — `dt set c/id tags=a tags=b` wrote `[b]`. */
|
|
1114
1129
|
function pairs(list) {
|
|
@@ -1158,3 +1173,98 @@ const fmtCell = (v) => (v === undefined ? '-' : Array.isArray(v) ? v.join(',') :
|
|
|
1158
1173
|
function rel(root, p) {
|
|
1159
1174
|
return p.startsWith(root) ? p.slice(root.length + 1) : p;
|
|
1160
1175
|
}
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* THE ONE PLACE A FLAG NAME IS VALIDATED — every record, system and field verb goes through
|
|
1179
|
+
* `collectionCommand`, so the check lives beside the parser they share rather than being
|
|
1180
|
+
* re-invented per verb.
|
|
1181
|
+
*
|
|
1182
|
+
* ⚠ IT EXISTS BECAUSE THE PARSER USED TO SWALLOW EVERYTHING. `dt add modules --name hr --bogusflag x`
|
|
1183
|
+
* succeeded silently, so a misspelled flag and a supported one were indistinguishable — and that is
|
|
1184
|
+
* what let `add modules --namespace hr` be accepted-and-ignored for a whole release with nobody
|
|
1185
|
+
* noticing. The worst instance was `--dryrun`, where the swallowed flag is the one standing between
|
|
1186
|
+
* a plan and a self-committed delete.
|
|
1187
|
+
*
|
|
1188
|
+
* TWO VOCABULARIES, and the split is what keeps the refusal honest. A verb's OPTIONS are closed and
|
|
1189
|
+
* enumerated here; an entity's own KEYS are open and cannot be — `dt list people --status todo` is a
|
|
1190
|
+
* shorthand filter on a declared field, `dt add people --name Ada` writes one, and a ui-view's
|
|
1191
|
+
* `options.*` is an open bag by design. So the allowlist is the table PLUS the target's declared
|
|
1192
|
+
* properties, and nothing is refused that either half can name.
|
|
1193
|
+
*
|
|
1194
|
+
* ⚠ A REFUSAL THAT REJECTS A VALID FLAG IS WORSE THAN THE SILENCE IT REPLACES, which is why a verb
|
|
1195
|
+
* with no table entry is left exactly as it was rather than being guessed at — and why the field
|
|
1196
|
+
* verbs' row is the whole `fieldDef` vocabulary, read off nothing, so a flag added there has to be
|
|
1197
|
+
* added here too or its own test fails.
|
|
1198
|
+
*
|
|
1199
|
+
* A key is `<collection>:<verb>` where the system entity has its own interceptor, `<verb>` otherwise.
|
|
1200
|
+
*/
|
|
1201
|
+
export const FIELD_FLAGS = ['json', 'module', 'name', 'field', 'type', 'options', 'default-value', 'default', 'required', 'description', 'many', 'inverse', 'inverse-description', 'unique', 'body', 'on-delete', 'mirror-of', 'target'];
|
|
1202
|
+
const JSON_ONLY = ['json'];
|
|
1203
|
+
const FORCE_RM = ['json', 'force', 'dry-run'];
|
|
1204
|
+
const NAV_MOVE = ['json', 'after', 'before', 'top', 'bottom'];
|
|
1205
|
+
|
|
1206
|
+
export const VERB_FLAGS = {
|
|
1207
|
+
list: ['json', 'filter', 'where', 'sort'], get: JSON_ONLY, add: ['json', 'id', 'from', 'force'], set: JSON_ONLY,
|
|
1208
|
+
rm: FORCE_RM, rename: JSON_ONLY, move: [...NAV_MOVE, 'init'], values: ['json', 'limit'],
|
|
1209
|
+
history: JSON_ONLY, diff: ['json', 'hash'], revert: ['json', 'hash'],
|
|
1210
|
+
ensure: ['json', 'all'], for: ['json', 'ids'], relations: JSON_ONLY, rebuild: ['json', 'drop'],
|
|
1211
|
+
'add-field': FIELD_FLAGS, 'update-field': FIELD_FLAGS,
|
|
1212
|
+
'remove-field': ['json', 'module', 'name', 'field', 'dry-run'],
|
|
1213
|
+
'rename-field': ['json', 'module', 'name', 'field', 'to', 'dry-run'],
|
|
1214
|
+
'collections:add': ['json', 'module', 'name', 'namespace', 'template', 'description', 'suffix', 'id-shape'],
|
|
1215
|
+
'collections:get': ['json', 'module'], 'collections:set': ['json', 'module', 'dry-run'],
|
|
1216
|
+
'collections:rm': FORCE_RM, 'collections:rename': ['json', 'namespace', 'dry-run'], 'collections:move': NAV_MOVE,
|
|
1217
|
+
'modules:add': ['json', 'name', 'description', 'namespace'], 'modules:rename': JSON_ONLY, 'modules:rm': FORCE_RM,
|
|
1218
|
+
'modules:set': ['json', 'description', 'namespaces', 'dependencies', 'peerDependencies'],
|
|
1219
|
+
// the identity kinds: `add` scaffolds, `rm`/`rename` fall through to the generic rows, and `set`
|
|
1220
|
+
// is deliberately UNCHECKED — a skill's frontmatter is an open document (`allowed-tools`, `model`,
|
|
1221
|
+
// whatever a harness reads), so there is no closed set to check it against.
|
|
1222
|
+
'skills:add': ['json', 'module', 'name', 'description'],
|
|
1223
|
+
'ui-views:add': ['json', 'module', 'id', 'force'], 'ui-views:set': ['json', 'module', 'id', 'force'], 'ui-views:rm': FORCE_RM,
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
/** Edit distance, capped — enough to turn `--fliter` into "did you mean --filter?", and to refuse to
|
|
1227
|
+
* guess at anything further away than a typo. */
|
|
1228
|
+
function nearest(word, candidates) {
|
|
1229
|
+
const distance = (c) => {
|
|
1230
|
+
let prev = [...Array(word.length + 1).keys()];
|
|
1231
|
+
for (let i = 1; i <= c.length; i++) {
|
|
1232
|
+
const row = [i];
|
|
1233
|
+
for (let j = 1; j <= word.length; j++) row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (c[i - 1] === word[j - 1] ? 0 : 1));
|
|
1234
|
+
prev = row;
|
|
1235
|
+
}
|
|
1236
|
+
return prev[word.length];
|
|
1237
|
+
};
|
|
1238
|
+
const cap = Math.min(3, Math.ceil(word.length / 2));
|
|
1239
|
+
return candidates.map((c) => [c, distance(c)]).filter(([, n]) => n <= cap).sort((a, b) => a[1] - b[1])[0]?.[0] ?? null;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function refuseUnknownFlags(store, collection, verb, flags) {
|
|
1243
|
+
const d = store.descriptors.get(collection);
|
|
1244
|
+
const known = VERB_FLAGS[`${collection}:${verb}`]
|
|
1245
|
+
?? (ENTITY_KINDS.has(collection) && verb === 'add' ? VERB_FLAGS['skills:add'] : VERB_FLAGS[verb]);
|
|
1246
|
+
if (!known) return; // no declared vocabulary — left exactly as it was rather than guessed at
|
|
1247
|
+
// The OPEN half: a data collection's own fields (shorthand filters and field writes), and the
|
|
1248
|
+
// declared keys of an entity `set` writes (`--layout` on a view, and dotted `options.sort`).
|
|
1249
|
+
const system = d?.storage?.base === 'runtime';
|
|
1250
|
+
const openOf = !system ? (['list', 'add', 'set'].includes(verb) ? `field of ${collection}` : null)
|
|
1251
|
+
: (collection === 'ui-views' && verb !== 'rm') || (ENTITY_KINDS.has(collection) && verb === 'set') ? `declared key of ${collection}` : null;
|
|
1252
|
+
const open = openOf ? Object.keys(d?.schema?.properties ?? {}) : [];
|
|
1253
|
+
const allowed = new Set([...known, ...open]);
|
|
1254
|
+
for (const f of Object.keys(flags)) {
|
|
1255
|
+
if (allowed.has(f) || (openOf && allowed.has(f.split('.')[0]))) continue;
|
|
1256
|
+
const near = nearest(f, [...allowed]);
|
|
1257
|
+
throw new Error(`unknown flag "--${f}" on \`dt ${verb} ${collection}\`${near ? ` — did you mean --${near}?` : ''}\n known: ${[...known].sort().map((k) => `--${k}`).join(', ')}${openOf ? `, plus any ${openOf}` : ''}`);
|
|
1258
|
+
}
|
|
1259
|
+
// ⚠ AND THE REPEAT REFUSAL, which used to be 15 hand-written `refuseRepeats(flags)` lines — one
|
|
1260
|
+
// per meta verb, and a new verb had to remember it. A SCHEMA verb takes one value per flag (none
|
|
1261
|
+
// of them means a list by repetition), so the same table that says which flags exist says which
|
|
1262
|
+
// verbs the rule applies to: everything with a closed vocabulary EXCEPT the open halves, where a
|
|
1263
|
+
// repeat legitimately composes (`--filter a=1 --filter b=2`, `--tags a --tags b`). `ui-views`
|
|
1264
|
+
// keeps its own message, which names both spellings of the fix. ⚠ THE FIELD VERBS COUNT AS SCHEMA
|
|
1265
|
+
// VERBS EVEN THOUGH THEIR TARGET IS A DATA COLLECTION — `add-field notes --name x --name y` is the
|
|
1266
|
+
// case the old helper was written for, and keying the rule on the target alone excused it.
|
|
1267
|
+
if (collection === 'ui-views' || (!system && !verb.endsWith('-field'))) return;
|
|
1268
|
+
const dup = Object.entries(flags).find(([, v]) => Array.isArray(v));
|
|
1269
|
+
if (dup) throw new Error(`--${dup[0]} was given ${dup[1].length} times, and a schema verb takes ONE value per flag: ${dup[1].map((x) => `--${dup[0]} ${x}`).join(' ')}`);
|
|
1270
|
+
}
|
package/src/schema-ops.js
CHANGED
|
@@ -98,6 +98,22 @@ function writeGated(ws, store, files, subject, mutate, after, { commentsMayDecre
|
|
|
98
98
|
throw e;
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
// ⚠ A WRITE THAT CHANGED NO BYTES MUST NOT REACH `git commit`. `git commit` on an empty index
|
|
102
|
+
// exits NON-ZERO, so the catch below read a successful no-op as a failed op and reported
|
|
103
|
+
// `✖ git commit failed — the schema change was rolled back` with the raw git command appended,
|
|
104
|
+
// at exit 1 — for `dt set collections/people description=<the value it already has>`, which
|
|
105
|
+
// asked for nothing and got nothing. Every sibling verb already has the graceful spelling
|
|
106
|
+
// ("already named that, nothing to do"), and the documented namespace cleanup path
|
|
107
|
+
// (`dt set modules/<m> namespaces=<ns>` where it is already declared) ran straight into it.
|
|
108
|
+
//
|
|
109
|
+
// Compared as BYTES, after the gate compile: an op whose mutation is a re-serialization can
|
|
110
|
+
// produce an identical file, and "identical" is the only definition of no-op the caller can
|
|
111
|
+
// act on.
|
|
112
|
+
const moved = snapshots.some(({ f, prev }) => {
|
|
113
|
+
const now = fs.existsSync(f) ? fs.readFileSync(f) : null;
|
|
114
|
+
return now === null ? prev !== null : prev === null || !now.equals(prev);
|
|
115
|
+
});
|
|
116
|
+
if (!moved && !extra.files.length) return { ...extra, commits: [], unchanged: true };
|
|
101
117
|
const rels = [...files, ...extra.files].map((f) => path.relative(ws.root, f));
|
|
102
118
|
// Schema ops commit UNCONDITIONALLY — `auto-commit` governs RECORD writes only. A source
|
|
103
119
|
// change is inseparable from the compile that validated it, and `dt commit` scopes itself
|
|
@@ -240,7 +256,7 @@ function editWorkspacePkg(ws, mutate) {
|
|
|
240
256
|
return file;
|
|
241
257
|
}
|
|
242
258
|
|
|
243
|
-
export function createModule(ws, store, { name, description }) {
|
|
259
|
+
export function createModule(ws, store, { name, description, namespace }) {
|
|
244
260
|
if (!name || name === true) throw new Error('missing module name — dreamteamer add modules --name <id>');
|
|
245
261
|
if (!MODULE_ID.test(name)) {
|
|
246
262
|
throw new Error(`invalid module id "${name}" — lowercase alphanumeric with single hyphens. It becomes a folder name, a package name and a record id at once, so there is only one spelling.`);
|
|
@@ -253,6 +269,23 @@ export function createModule(ws, store, { name, description }) {
|
|
|
253
269
|
|
|
254
270
|
const dt = {};
|
|
255
271
|
if (typeof description === 'string' && description) dt.description = description;
|
|
272
|
+
// §6.2: `--namespace hr` DECLARES THE NAMESPACE IN THE MODULE, which is the whole point of §8 —
|
|
273
|
+
// the workspace's effective set is the union of what its modules declare, so a module that owns a
|
|
274
|
+
// namespace can be copied alone into a bare workspace and still compile.
|
|
275
|
+
//
|
|
276
|
+
// ⚠ It used to be dropped on the floor. `dt add modules --name hr --namespace hr` reported ✔,
|
|
277
|
+
// shipped `"dreamteamer": {}`, left the manifest's `namespaces: []`, and the next
|
|
278
|
+
// `add collections --module hr` then created an UNPREFIXED collection — four steps of silence
|
|
279
|
+
// behind one accepted-and-ignored flag. Normalized here rather than trusted: a leading or
|
|
280
|
+
// trailing slash in a declaration re-splits every reference under it.
|
|
281
|
+
const ns = typeof namespace === 'string' ? namespace.replace(/^\/+|\/+$/g, '') : '';
|
|
282
|
+
if (ns) {
|
|
283
|
+
const owner = moduleRows(store).find((r) => normalizeNamespaces(r.fields.namespaces).includes(ns));
|
|
284
|
+
// compile refuses two declarations of one namespace, and refusing it HERE names the verb that
|
|
285
|
+
// fixes it instead of rolling a created module back over a compile error.
|
|
286
|
+
if (owner) throw new Error(`namespace "${ns}" is already declared by ${owner.id} — one owner. Remove it there first (dt set modules/${owner.id} namespaces=…), or pick another namespace.`);
|
|
287
|
+
dt.namespaces = [ns];
|
|
288
|
+
}
|
|
256
289
|
// `files` is the npm publish surface: every kind a module CAN ship, so a kind added to the engine
|
|
257
290
|
// does not silently stop being packaged.
|
|
258
291
|
const mpkg = { name, private: true, version: '0.0.1', files: [...KINDS], dreamteamer: dt };
|
|
@@ -272,7 +305,7 @@ export function createModule(ws, store, { name, description }) {
|
|
|
272
305
|
},
|
|
273
306
|
undo: () => fs.rmSync(root, { recursive: true, force: true }),
|
|
274
307
|
});
|
|
275
|
-
return { id: name, root: path.relative(ws.root, root), file: pkgFile, commits: out.commits };
|
|
308
|
+
return { id: name, root: path.relative(ws.root, root), file: pkgFile, namespace: ns || null, commits: out.commits };
|
|
276
309
|
}
|
|
277
310
|
|
|
278
311
|
/** The settable fields of a `modules` record, and how each translates from the record-shaped value
|
|
@@ -338,7 +371,7 @@ export function setModule(ws, store, id, changes) {
|
|
|
338
371
|
});
|
|
339
372
|
}
|
|
340
373
|
}, undefined, { commentsMayDecrease: true });
|
|
341
|
-
return { id, file, changed, commits: gate.commits };
|
|
374
|
+
return { id, file, changed, commits: gate.commits, unchanged: gate.unchanged };
|
|
342
375
|
}
|
|
343
376
|
|
|
344
377
|
export function removeModule(ws, store, id, { force = false, dryRun = false } = {}) {
|
|
@@ -755,6 +788,15 @@ export function setCollectionScalars(ws, store, name, changes, { moduleId } = {}
|
|
|
755
788
|
throw new Error(`${key}: ${collectionMissingFields(name, missing)} — declare it first (dreamteamer add-field ${name} --name ${missing[0]} --type <t>).`);
|
|
756
789
|
}
|
|
757
790
|
}
|
|
791
|
+
// ⚠ NAMED, not resolved to an overlay. `collectionSourceFile` falls back to a workspace-module
|
|
792
|
+
// path for a base it cannot write, which is right for `add-field` (an overlay IS the remedy) and
|
|
793
|
+
// wrong here: a collection-level scalar has no overlay spelling, so the fallback produced
|
|
794
|
+
// `modules/default/collections/repos.collection.yaml is not on disk — run compile and re-run`,
|
|
795
|
+
// and compiling will never help. `rm` and `rename` already say the true sentence.
|
|
796
|
+
const owned = baseDescriptorSource(ws, name).base;
|
|
797
|
+
if (owned && IN_NODE_MODULES(owned)) {
|
|
798
|
+
throw new Error(`"${name}" ships from node_modules (${owned}) — a write there is erased by the next \`npm install\`, and a collection-level scalar has no overlay spelling. Add "<module>/${name}" to dreamteamer.disable and declare your own instead.`);
|
|
799
|
+
}
|
|
758
800
|
const { file } = collectionSourceFile(ws, store, name, moduleId, { subject: name });
|
|
759
801
|
if (!fs.existsSync(file)) throw new Error(`${path.relative(ws.root, file)} is not on disk — run \`dreamteamer compile\` and re-run.`);
|
|
760
802
|
const previousText = fs.readFileSync(file, 'utf8');
|
|
@@ -770,7 +812,7 @@ export function setCollectionScalars(ws, store, name, changes, { moduleId } = {}
|
|
|
770
812
|
}
|
|
771
813
|
fs.writeFileSync(file, writeSource(previousText, doc));
|
|
772
814
|
});
|
|
773
|
-
return { name, file, changed, commits: gate.commits };
|
|
815
|
+
return { name, file, changed, commits: gate.commits, unchanged: gate.unchanged };
|
|
774
816
|
}
|
|
775
817
|
|
|
776
818
|
// ---- rename-field ------------------------------------------------------------------------------
|
|
@@ -965,10 +1007,19 @@ export function renameField(ws, store, collection, from, to, { moduleId, dryRun
|
|
|
965
1007
|
if (String(doc.collection ?? '') !== `collections/${collection}`) continue;
|
|
966
1008
|
const keys = kind === 'ui-views' ? ['filter'] : ['can-enter', 'can-exit'];
|
|
967
1009
|
let changed = false;
|
|
968
|
-
|
|
969
|
-
|
|
1010
|
+
// The field-name LISTS: `columns` is honoured by every layout, `ref_fields` and
|
|
1011
|
+
// `value_fields` are the diagram's link-by pickers. Same vocabulary `list_fields` uses.
|
|
1012
|
+
for (const key of ['columns', 'ref_fields', 'value_fields']) {
|
|
1013
|
+
if (!Array.isArray(doc.options?.[key]) || !doc.options[key].includes(from)) continue;
|
|
1014
|
+
doc.options[key] = doc.options[key].map((c) => (c === from ? to : c));
|
|
970
1015
|
changed = true;
|
|
971
1016
|
}
|
|
1017
|
+
// ⚠ AND `options.sort`, which is a field name with an optional `-` in front of it. It
|
|
1018
|
+
// was the one §3.2 surface the rename missed, and the miss is invisible: `dt check`
|
|
1019
|
+
// reports 0 violations for a view sorting on a field that no longer exists, so the
|
|
1020
|
+
// listing silently falls back to an arbitrary order.
|
|
1021
|
+
const sorted = /^(-?)(.+)$/.exec(typeof doc.options?.sort === 'string' ? doc.options.sort : '');
|
|
1022
|
+
if (sorted && sorted[2] === from) { doc.options.sort = sorted[1] + to; changed = true; }
|
|
972
1023
|
for (const key of keys) if (rewriteFilterField(doc[key], from, to)) changed = true;
|
|
973
1024
|
if (!changed) continue;
|
|
974
1025
|
const after = writeSource(before, doc);
|
|
@@ -2309,9 +2360,17 @@ function uiViewSourceFile(ws, id) {
|
|
|
2309
2360
|
// saved views (M3): a studio-saved view IS a ui-view record — but ui-views are
|
|
2310
2361
|
// system-stored (sources + compile), so the write goes through the same gate as any
|
|
2311
2362
|
// other schema op. the studio "save view" button lands here.
|
|
2312
|
-
export function saveUiView(ws, store, { id, view }) {
|
|
2363
|
+
export function saveUiView(ws, store, { id, view, moduleId }) {
|
|
2313
2364
|
if (!id || !/^[a-z0-9][a-z0-9-/]*$/.test(id)) throw new Error(`invalid ui-view id "${id}" — lowercase slug required`);
|
|
2314
|
-
|
|
2365
|
+
// §5: `add` on a system collection takes `--module`. It used to be dropped by the parser and then
|
|
2366
|
+
// assigned into the VIEW as a field called `module` — `dt add ui-views … --module core` wrote
|
|
2367
|
+
// `module: core` into the yaml, compiled clean, and self-committed a workspace only `dt check`
|
|
2368
|
+
// would later object to. An explicit target module resolves the destination; without one the
|
|
2369
|
+
// manifest answers (an existing view is edited where it lives), and the workspace module is the
|
|
2370
|
+
// fallback for a new one.
|
|
2371
|
+
const { file: dest, shipped } = moduleId
|
|
2372
|
+
? { file: path.join(kindDir(path.join(ws.root, moduleRecord(store, moduleId).fields.path), 'ui-views'), `${id}.ui-view.yaml`), shipped: null }
|
|
2373
|
+
: uiViewSourceFile(ws, id);
|
|
2315
2374
|
if (shipped && /(^|\/)node_modules\//.test(shipped))
|
|
2316
2375
|
throw new Error(`ui-view "${id}" is shipped by an installed package (${shipped}) — a write there is erased by the next npm install.\n save it under a different name, or disable it (dreamteamer.disable) and re-create it.`);
|
|
2317
2376
|
const existed = fs.existsSync(dest);
|
|
@@ -2325,7 +2384,7 @@ export function saveUiView(ws, store, { id, view }) {
|
|
|
2325
2384
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
2326
2385
|
fs.writeFileSync(dest, writeSource(previous, view));
|
|
2327
2386
|
}, undefined, { commentsMayDecrease: true });
|
|
2328
|
-
return { id, file: dest, updated: existed, commits: gate.commits };
|
|
2387
|
+
return { id, file: dest, updated: existed, commits: gate.commits, unchanged: gate.unchanged };
|
|
2329
2388
|
}
|
|
2330
2389
|
|
|
2331
2390
|
export function removeUiView(ws, store, id) {
|
|
@@ -2716,6 +2775,16 @@ export function setEntityFrontmatter(ws, store, kind, id, changes) {
|
|
|
2716
2775
|
const { dir, file, shipped } = entitySource(ws, kind, id);
|
|
2717
2776
|
if (!shipped) throw new Error(`${kind.replace(/s$/, '')} "${id}" does not exist — dt list ${kind}`);
|
|
2718
2777
|
refuseNpmEntity(kind, id, shipped);
|
|
2778
|
+
// ⚠ THE DESCRIPTOR IS THE AUTHORITY, and the engine was disagreeing with itself: `dt set
|
|
2779
|
+
// skills/<id> descripton="oops"` wrote the typo, compiled, SELF-COMMITTED — and then `dt check`
|
|
2780
|
+
// failed on the very workspace the self-commit exists to keep valid, because the `skills`
|
|
2781
|
+
// descriptor declares a closed set of properties and check validates against it. "Frontmatter is
|
|
2782
|
+
// an open document" was the wrong half to believe: if a key is not in the descriptor, `check`
|
|
2783
|
+
// will reject it, so `set` refuses it first. Its two siblings already read this way
|
|
2784
|
+
// (`setCollectionScalars`, `setModule`), and a body field is not settable from the CLI at all.
|
|
2785
|
+
const props = store.descriptors.get(kind)?.schema?.properties ?? {};
|
|
2786
|
+
const unknown = Object.keys(changes).find((k) => !(k in props) || props[k]?.['x-body'] === true);
|
|
2787
|
+
if (unknown) throw new Error(`"${unknown}" is not a settable key of ${kind} — declared: ${Object.keys(props).filter((k) => props[k]?.['x-body'] !== true).join(', ')}. \`dreamteamer check\` rejects anything else, so this is refused before it is committed.`);
|
|
2719
2788
|
const target = shape.folder ? path.join(dir, 'SKILL.md') : file;
|
|
2720
2789
|
// A YAML source (a binding, a template) is a whole document; a markdown one has frontmatter and
|
|
2721
2790
|
// prose. `writeSource` round-trips both — the difference is only which text it is handed.
|
|
@@ -2740,7 +2809,7 @@ export function setEntityFrontmatter(ws, store, kind, id, changes) {
|
|
|
2740
2809
|
fs.writeFileSync(target, text);
|
|
2741
2810
|
}
|
|
2742
2811
|
}, undefined, { commentsMayDecrease: true });
|
|
2743
|
-
return { id, file: target, changed, commits: gate.commits };
|
|
2812
|
+
return { id, file: target, changed, commits: gate.commits, unchanged: gate.unchanged };
|
|
2744
2813
|
}
|
|
2745
2814
|
|
|
2746
2815
|
/**
|