dreamteamer 0.6.0 → 0.6.2
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 +7 -1
- package/src/commit.js +12 -3
- package/src/compile.js +42 -33
- package/src/init.js +24 -3
- package/src/server.js +7 -1
- package/src/store.js +8 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dreamteamer",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
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
|
@@ -12,6 +12,12 @@ import { deriveEvents } from './events.js';
|
|
|
12
12
|
import { commitPending } from './commit.js';
|
|
13
13
|
import { Store } from './store.js';
|
|
14
14
|
|
|
15
|
+
// git calls whose failure we CATCH must not print git's own error: execFileSync forwards the
|
|
16
|
+
// child's stderr to ours unless told otherwise, so a handled "not a git repository" still
|
|
17
|
+
// reached the user's terminal. stdout stays piped because we read it.
|
|
18
|
+
const QUIET = ['ignore', 'pipe', 'ignore'];
|
|
19
|
+
|
|
20
|
+
|
|
15
21
|
const USAGE = `usage: dreamteamer <command> | dreamteamer <collection> <verb> …
|
|
16
22
|
|
|
17
23
|
commands:
|
|
@@ -230,7 +236,7 @@ export function run(argv) {
|
|
|
230
236
|
}
|
|
231
237
|
|
|
232
238
|
function tryGit(cwd, args) {
|
|
233
|
-
try { return execFileSync('git', args, { cwd }).toString().trim() || null; } catch { return null; }
|
|
239
|
+
try { return execFileSync('git', args, { cwd, stdio: QUIET }).toString().trim() || null; } catch { return null; }
|
|
234
240
|
}
|
|
235
241
|
|
|
236
242
|
function watchAndRecompile(ws) {
|
package/src/commit.js
CHANGED
|
@@ -6,6 +6,12 @@ import fs from 'node:fs';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { pathToRecord } from './events.js';
|
|
8
8
|
|
|
9
|
+
// git calls whose failure we CATCH must not print git's own error: execFileSync forwards the
|
|
10
|
+
// child's stderr to ours unless told otherwise, so a handled "not a git repository" still
|
|
11
|
+
// reached the user's terminal. stdout stays piped because we read it.
|
|
12
|
+
const QUIET = ['ignore', 'pipe', 'ignore'];
|
|
13
|
+
|
|
14
|
+
|
|
9
15
|
const VERB = { A: 'add', M: 'set', D: 'rm', R: 'rename', '?': 'add' };
|
|
10
16
|
|
|
11
17
|
/** Record directories to watch, grouped by owning repo. System-stored collections are excluded:
|
|
@@ -31,7 +37,7 @@ function scopeByRepo(descriptors, only) {
|
|
|
31
37
|
* is NOT a refusal condition — committing dirty records is this verb's entire job. */
|
|
32
38
|
function inProgress(cwd) {
|
|
33
39
|
let gitDir;
|
|
34
|
-
try { gitDir = execFileSync('git', ['rev-parse', '--absolute-git-dir'], { cwd }).toString().trim(); }
|
|
40
|
+
try { gitDir = execFileSync('git', ['rev-parse', '--absolute-git-dir'], { cwd, stdio: QUIET }).toString().trim(); }
|
|
35
41
|
catch { return 'not a git repository'; }
|
|
36
42
|
for (const [marker, label] of [['MERGE_HEAD', 'merge'], ['rebase-merge', 'rebase'], ['rebase-apply', 'rebase'], ['CHERRY_PICK_HEAD', 'cherry-pick'], ['REVERT_HEAD', 'revert']]) {
|
|
37
43
|
if (fs.existsSync(path.join(gitDir, marker))) return `a ${label} is in progress`;
|
|
@@ -42,7 +48,7 @@ function inProgress(cwd) {
|
|
|
42
48
|
/** A commit on a detached HEAD is reachable only by sha. Worth saying out loud before making one;
|
|
43
49
|
* not worth refusing over, since it is sometimes exactly what someone means to do. */
|
|
44
50
|
function detached(cwd) {
|
|
45
|
-
try { return execFileSync('git', ['symbolic-ref', '--quiet', 'HEAD'], { cwd }).toString().trim() === ''; }
|
|
51
|
+
try { return execFileSync('git', ['symbolic-ref', '--quiet', 'HEAD'], { cwd, stdio: QUIET }).toString().trim() === ''; }
|
|
46
52
|
catch { return true; }
|
|
47
53
|
}
|
|
48
54
|
|
|
@@ -57,7 +63,10 @@ function sample(root, repo, dirs, descriptors) {
|
|
|
57
63
|
// `-uall` is load-bearing: by default porcelain COLLAPSES an untracked directory to a single
|
|
58
64
|
// `?? data/notes/` entry, which maps to no record — so the first records of a brand-new
|
|
59
65
|
// collection would be invisible and dt commit would report success having committed nothing.
|
|
60
|
-
|
|
66
|
+
// QUIET: the caller (cli `status`) catches a failure here so it can still print the rest of the
|
|
67
|
+
// report in a non-git folder — but git's own "fatal: not a git repository" was reaching the
|
|
68
|
+
// terminal anyway, which made a handled case look like a crash.
|
|
69
|
+
const out = execFileSync('git', ['status', '--porcelain', '-z', '-uall', '--', ...relDirs], { cwd, stdio: QUIET }).toString();
|
|
61
70
|
const chunks = out.split('\0').filter((c) => c.length > 0);
|
|
62
71
|
const rows = [];
|
|
63
72
|
for (let i = 0; i < chunks.length; i++) {
|
package/src/compile.js
CHANGED
|
@@ -100,7 +100,8 @@ function bothLayouts(root, kind) {
|
|
|
100
100
|
* has `data/` — not a layout knob every module would set identically.
|
|
101
101
|
*/
|
|
102
102
|
const NON_SOURCE_DIRS = new Set([
|
|
103
|
-
'node_modules', 'data', 'state', 'media', 'bin', 'src', 'lib', 'scripts',
|
|
103
|
+
'node_modules', 'data', 'state', 'media', 'bin', 'src', 'lib', 'scripts',
|
|
104
|
+
'ui', 'studio', // the module's UI bundle — 'studio' is the pre-archive name, kept as a fallback
|
|
104
105
|
'docs', 'dist', 'build', 'test', 'tests', 'coverage', 'system', // 'system': the pre-flatten layout
|
|
105
106
|
]);
|
|
106
107
|
|
|
@@ -357,23 +358,21 @@ export function compile({ root, pkg }) {
|
|
|
357
358
|
}
|
|
358
359
|
}
|
|
359
360
|
|
|
360
|
-
// A module that ships only folders the engine does not recognise compiles ✔ and contributes
|
|
361
|
-
// NOTHING. Warn; do not fail, since a module that is temporarily source-free is the
|
|
362
|
-
// operator's business, not the compiler's.
|
|
363
|
-
for (const source of sources) {
|
|
364
|
-
if (contributed.has(source.name)) continue;
|
|
365
|
-
console.warn(`⚠ module "${source.name}" (${rel(source.root)}) contributed no recognised sources — its folder names must match a known kind (${KINDS.join(', ')})`);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
361
|
// ---- stage module UI bundles ---------------------------------------------------
|
|
369
|
-
// modules ship a PRE-BUILT app.js that registers components/layouts against the
|
|
362
|
+
// modules ship a PRE-BUILT app.js that registers components/layouts against the surface's
|
|
370
363
|
// registry (design "the UI": components are module code, never records). staged under
|
|
371
|
-
// .dreamteamer/ui/<module>/app.js; the
|
|
372
|
-
//
|
|
364
|
+
// .dreamteamer/ui/<module>/app.js; the VS Code extension reads it off disk (decision 48) and
|
|
365
|
+
// the legacy server served it at /ui. `dist/app.js` (a built bundle) wins over `app.js`
|
|
366
|
+
// (plain-JS, host-provided Vue).
|
|
367
|
+
//
|
|
368
|
+
// `ui/` is the name — it matches where the bundle STAGES and what it is. `studio/` is the
|
|
369
|
+
// original name and stays a fallback: the studio it referred to is archived (decisions 51, 93),
|
|
370
|
+
// so the folder was named after a surface that no longer exists. Both are in NON_SOURCE_DIRS,
|
|
371
|
+
// so neither trips the unknown-folder gate (decision 179).
|
|
373
372
|
const uiModules = [];
|
|
374
373
|
const uiOwners = new Map(); // shortName -> module name, for a readable collision error
|
|
375
374
|
for (const source of sources) {
|
|
376
|
-
const cand = ['studio/dist/app.js', 'studio/app.js']
|
|
375
|
+
const cand = ['ui/dist/app.js', 'ui/app.js', 'studio/dist/app.js', 'studio/app.js']
|
|
377
376
|
.map((p) => path.join(source.root, p))
|
|
378
377
|
.find((p) => fs.existsSync(p));
|
|
379
378
|
if (!cand) continue;
|
|
@@ -386,6 +385,18 @@ export function compile({ root, pkg }) {
|
|
|
386
385
|
uiOwners.set(shortName, source.name);
|
|
387
386
|
addEntry(path.join('ui', shortName, 'app.js'), cand);
|
|
388
387
|
uiModules.push(shortName);
|
|
388
|
+
// A UI bundle IS a contribution. Counting it here is what keeps the warning below honest —
|
|
389
|
+
// a module whose whole purpose is a layout used to be told it "contributed no recognised
|
|
390
|
+
// sources" while its layout was rendering in the app.
|
|
391
|
+
contributed.add(source.name);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// A module that ships only folders the engine does not recognise compiles ✔ and contributes
|
|
395
|
+
// NOTHING. Warn; do not fail, since a module that is temporarily source-free is the
|
|
396
|
+
// operator's business, not the compiler's. Runs AFTER UI staging so a UI-only module counts.
|
|
397
|
+
for (const source of sources) {
|
|
398
|
+
if (contributed.has(source.name)) continue;
|
|
399
|
+
console.warn(`⚠ module "${source.name}" (${rel(source.root)}) contributed no recognised sources — its folder names must match a known kind (${KINDS.join(', ')}) or it must ship a UI bundle at ui/app.js`);
|
|
389
400
|
}
|
|
390
401
|
|
|
391
402
|
// ---- collection-templates, for `templates:` merging ----------------------------
|
|
@@ -512,29 +523,27 @@ export function compile({ root, pkg }) {
|
|
|
512
523
|
}
|
|
513
524
|
|
|
514
525
|
// ---- ui-view layout validation --------------------------------------------------
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
//
|
|
522
|
-
// `
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
526
|
+
// ⚠ `layout` is NOT validated here, deliberately. The rule: the engine validates a value if and
|
|
527
|
+
// only if the ENGINE INTERPRETS it. It interprets filter operators (`matchesFilter`, and the
|
|
528
|
+
// CLI's `--where`), so a typo'd operator is a real bug it can catch — hence the check below.
|
|
529
|
+
// It interprets `layout` nowhere: the value is opaque payload forwarded to whichever surface
|
|
530
|
+
// renders, and only that surface's registry knows which ids exist.
|
|
531
|
+
//
|
|
532
|
+
// There used to be an allowlist here, hardcoded to mirror dreamteamer-vscode's
|
|
533
|
+
// `lists.register(...)` calls in a DIFFERENT REPO. It was wrong both times it was tested:
|
|
534
|
+
// kanban/calendar/map (2026-07-29) and erd/graph (2026-08-10), each costing an engine edit to
|
|
535
|
+
// add a UI feature. Worse, it BLOCKED the sanctioned extension path — a module's `app.js` gets
|
|
536
|
+
// a `registerList({ id, ... })` API, so it can contribute a layout with no engine involvement,
|
|
537
|
+
// and this check then rejected the very view naming it unless the module also duplicated the id
|
|
538
|
+
// into a `dreamteamer.studio.layouts` key (zero users, in any repo, ever). Proven 2026-08-11 by
|
|
539
|
+
// modules/ui-smoke: the layout rendered in the app while compile refused the view.
|
|
540
|
+
//
|
|
541
|
+
// The descriptor already documented the correct behaviour — ui-views.collection.yaml: "An
|
|
542
|
+
// unregistered id degrades visibly rather than erroring" — and the surface already implements
|
|
543
|
+
// it (presets.ts#resolveRendererEntry falls back to table). Decision 195.
|
|
532
544
|
for (const [rt, e] of entries) {
|
|
533
545
|
if (!rt.startsWith('ui-views/')) continue;
|
|
534
546
|
const view = load(e.bytes.toString('utf8'));
|
|
535
|
-
if (view?.target === 'list' && view?.layout && !registeredLayouts.has(view.layout)) {
|
|
536
|
-
fail(`${rt}: layout "${view.layout}" is not registered (registered: ${[...registeredLayouts].sort().join(', ')}).\n a module registers layouts in its studio app.js AND declares them in package.json under dreamteamer.studio.layouts.`);
|
|
537
|
-
}
|
|
538
547
|
// filters are load-bearing (they narrow what the operator SEES) — typo'd operators
|
|
539
548
|
// fail at compile, not silently at render (review finding 5)
|
|
540
549
|
const badOps = view?.filter ? [...unknownOperators(view.filter)] : [];
|
package/src/init.js
CHANGED
|
@@ -9,6 +9,12 @@ import { slugOrHash } from './template.js';
|
|
|
9
9
|
import { discoverModules, KINDS } from './compile.js';
|
|
10
10
|
import { Store } from './store.js';
|
|
11
11
|
|
|
12
|
+
// git calls whose failure we CATCH must not print git's own error: execFileSync forwards the
|
|
13
|
+
// child's stderr to ours unless told otherwise, so a handled "not a git repository" still
|
|
14
|
+
// reached the user's terminal. stdout stays piped because we read it.
|
|
15
|
+
const QUIET = ['ignore', 'pipe', 'ignore'];
|
|
16
|
+
|
|
17
|
+
|
|
12
18
|
const SKELETON_KINDS = ['collections', 'skills', 'agents', 'commands', 'ui-views'];
|
|
13
19
|
|
|
14
20
|
const GITIGNORE = `node_modules/
|
|
@@ -137,6 +143,7 @@ export function install({ root, pkg }) {
|
|
|
137
143
|
const names = Object.keys(map);
|
|
138
144
|
if (!names.length) { console.log('✔ no git-modules declared — nothing to restore'); return 0; }
|
|
139
145
|
fs.mkdirSync(path.join(root, 'git_modules'), { recursive: true });
|
|
146
|
+
const unreachable = [];
|
|
140
147
|
for (const name of names) {
|
|
141
148
|
const { url, ref = 'main' } = map[name];
|
|
142
149
|
const dest = path.join(root, 'git_modules', name);
|
|
@@ -148,10 +155,24 @@ export function install({ root, pkg }) {
|
|
|
148
155
|
continue;
|
|
149
156
|
}
|
|
150
157
|
console.log(`… cloning ${url} → git_modules/${name} (${ref})`);
|
|
151
|
-
|
|
158
|
+
try {
|
|
159
|
+
execFileSync('git', ['clone', '--branch', ref, url, dest], { stdio: 'inherit' });
|
|
160
|
+
} catch {
|
|
161
|
+
// one unreachable clone must not abandon the rest. the lockfile is a map of PRIVATE
|
|
162
|
+
// repos as often as public ones, so "the current credentials cannot see this one" is
|
|
163
|
+
// ordinary — a fresh machine, a collaborator, a cloud sandbox. aborting there left a
|
|
164
|
+
// half-restored workspace and named only the first failure.
|
|
165
|
+
fs.rmSync(dest, { recursive: true, force: true }); // git leaves the partial dir behind
|
|
166
|
+
unreachable.push(name);
|
|
167
|
+
console.warn(`⚠ git_modules/${name}: clone failed — skipped (${url})`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
152
170
|
buildClone(dest, name);
|
|
153
171
|
}
|
|
154
|
-
|
|
172
|
+
// non-zero, because the workspace is NOT what the lockfile describes: modules are missing and
|
|
173
|
+
// `check` will report references into them as unknown collections.
|
|
174
|
+
if (unreachable.length) console.error(`✖ ${unreachable.length} module(s) could not be cloned: ${unreachable.join(', ')}`);
|
|
175
|
+
return unreachable.length ? 1 : 0;
|
|
155
176
|
}
|
|
156
177
|
|
|
157
178
|
// dreamteamer update [<name>] — pull each lockfile-declared git_modules clone forward
|
|
@@ -200,7 +221,7 @@ function buildClone(dest, name) {
|
|
|
200
221
|
}
|
|
201
222
|
|
|
202
223
|
function tryGit(cwd, args) {
|
|
203
|
-
try { return execFileSync('git', args, { cwd }).toString().trim() || null; } catch { return null; }
|
|
224
|
+
try { return execFileSync('git', args, { cwd, stdio: QUIET }).toString().trim() || null; } catch { return null; }
|
|
204
225
|
}
|
|
205
226
|
|
|
206
227
|
function appendMissing(file, block) {
|
package/src/server.js
CHANGED
|
@@ -16,6 +16,12 @@ import { commandsFor, recordResolver } from './record-commands.js';
|
|
|
16
16
|
import { distinctValues } from './field-values.js';
|
|
17
17
|
import { slugOrHash } from './template.js';
|
|
18
18
|
|
|
19
|
+
// git calls whose failure we CATCH must not print git's own error: execFileSync forwards the
|
|
20
|
+
// child's stderr to ours unless told otherwise, so a handled "not a git repository" still
|
|
21
|
+
// reached the user's terminal. stdout stays piped because we read it.
|
|
22
|
+
const QUIET = ['ignore', 'pipe', 'ignore'];
|
|
23
|
+
|
|
24
|
+
|
|
19
25
|
export function startServer(ws, { port = 8080, host = '127.0.0.1' } = {}) {
|
|
20
26
|
const app = express();
|
|
21
27
|
app.use(express.json({ limit: '10mb' }));
|
|
@@ -42,7 +48,7 @@ export function startServer(ws, { port = 8080, host = '127.0.0.1' } = {}) {
|
|
|
42
48
|
// so `@me` filters in ui-views resolve to the seeded user record.
|
|
43
49
|
let operatorId = null;
|
|
44
50
|
try {
|
|
45
|
-
operatorId = slugOrHash(execFileSync('git', ['config', 'user.name'], { cwd: ws.root }).toString().trim());
|
|
51
|
+
operatorId = slugOrHash(execFileSync('git', ['config', 'user.name'], { cwd: ws.root, stdio: QUIET }).toString().trim());
|
|
46
52
|
} catch { /* no git identity — @me filters simply won't narrow */ }
|
|
47
53
|
|
|
48
54
|
api.get('/info', (req, res) => {
|
package/src/store.js
CHANGED
|
@@ -12,6 +12,12 @@ import { generateId } from './template.js';
|
|
|
12
12
|
import { parseRecord, parseRecordText, patternRe, fmtAjvError, unknownFields, walk, EXT, assertSafeId } from './records.js';
|
|
13
13
|
import { normalizeRecord } from './temporal.js';
|
|
14
14
|
import { NO_RUNTIME, loadDescriptors, runtimeDir, sourceRoots as compiledSourceRoots } from './runtime.js';
|
|
15
|
+
|
|
16
|
+
// git calls whose failure we CATCH must not print git's own error: execFileSync forwards the
|
|
17
|
+
// child's stderr to ours unless told otherwise, so a handled "not a git repository" still
|
|
18
|
+
// reached the user's terminal. stdout stays piped because we read it.
|
|
19
|
+
const QUIET = ['ignore', 'pipe', 'ignore'];
|
|
20
|
+
|
|
15
21
|
export class Store {
|
|
16
22
|
constructor({ root, pkg }) {
|
|
17
23
|
this.root = root;
|
|
@@ -66,7 +72,7 @@ export class Store {
|
|
|
66
72
|
|
|
67
73
|
// current HEAD — one cheap rev-parse per cache check vs a multi-thousand-file walk
|
|
68
74
|
gitHead() {
|
|
69
|
-
try { return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: this.root }).toString().trim(); } catch { return 'no-git'; }
|
|
75
|
+
try { return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: this.root, stdio: QUIET }).toString().trim(); } catch { return 'no-git'; }
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
ids(collection) {
|
|
@@ -128,7 +134,7 @@ export class Store {
|
|
|
128
134
|
const relPath = path.relative(this.root, file);
|
|
129
135
|
let previousContent;
|
|
130
136
|
try {
|
|
131
|
-
previousContent = execFileSync('git', ['show', `${hash}:${relPath}`], { cwd: this.root }).toString();
|
|
137
|
+
previousContent = execFileSync('git', ['show', `${hash}:${relPath}`], { cwd: this.root, stdio: QUIET }).toString();
|
|
132
138
|
} catch {
|
|
133
139
|
throw new Error(`${collection}/${id}: no content at ${hash} for ${relPath} — nothing was reverted.`);
|
|
134
140
|
}
|