dreamteamer 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/collections/collections.collection.yaml +27 -4
- package/collections/repos.collection.yaml +6 -0
- package/package.json +4 -3
- package/skills/using-dreamteamer/SKILL.md +1 -0
- package/skills/using-dreamteamer/references/getting-started.md +21 -0
- package/src/cli.js +89 -12
- package/src/collections-cli.js +10 -0
- package/src/commit.js +3 -2
- package/src/compile.js +115 -5
- package/src/containers.js +422 -0
- package/src/harnesses.js +83 -18
- package/src/init.js +4 -1
- package/src/presentation.js +23 -4
- package/src/ref.js +28 -6
- package/src/schema-ops.js +3 -0
- package/src/workspace.js +50 -0
package/src/presentation.js
CHANGED
|
@@ -82,6 +82,10 @@ function collectionRow(d) {
|
|
|
82
82
|
// it — the extension reads the presentation contract, not raw descriptors.
|
|
83
83
|
if (typeof d.sort_field === 'string') meta.sort_field = d.sort_field;
|
|
84
84
|
|
|
85
|
+
// The collection's PARTITION, and the reserved value `system` is how a surface knows to draw this
|
|
86
|
+
// collection as the workspace's machinery rather than in the record tree. It is carried in `meta`
|
|
87
|
+
// beside the other presentation keys because that is the question it answers; the top-level
|
|
88
|
+
// `system` below answers a different one and they must never be folded together.
|
|
85
89
|
if (typeof d.group === 'string') meta.group = d.group;
|
|
86
90
|
if (typeof d.description === 'string' && d.description.length > 0) meta.description = d.description;
|
|
87
91
|
// ⚠ `system` IS NOT `readonly`, AND SAYING SO COST A RELEASE. Until 0.19.0 the two were the
|
|
@@ -107,10 +111,25 @@ function collectionRow(d) {
|
|
|
107
111
|
// already answers with its own sentence naming the fix, which is the contract the schema
|
|
108
112
|
// surfaces are built on.
|
|
109
113
|
//
|
|
110
|
-
// `system` STAYS and is unchanged: the CLI and REST dispatch key on it
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
+
// `system` STAYS and is unchanged: the CLI and REST dispatch key on it. A consumer that disables
|
|
115
|
+
// editing must key on `meta.readonly` (per field, as `id`, `last-modified` and relation mirrors
|
|
116
|
+
// do) or on the verb it is about to offer — never on `system`.
|
|
117
|
+
//
|
|
118
|
+
// ⚠ AND `system` IS NO LONGER WHAT PUTS A KIND ON THE SCHEMA SURFACE — that clause stood here
|
|
119
|
+
// until `repos` and belongs to `meta.group` now. TWO QUESTIONS, KEPT SEPARATE, exactly as
|
|
120
|
+
// `buildCollectionsIndex` in src/harnesses.js keeps them:
|
|
121
|
+
//
|
|
122
|
+
// `system` decides HOW A WRITE IS PERFORMED. Is this collection's storage the compiled
|
|
123
|
+
// runtime? The CLI and the REST layer dispatch on it, and a surface that gets
|
|
124
|
+
// it wrong answers 400 to a write the store would have accepted.
|
|
125
|
+
// `meta.group` decides WHERE THE COLLECTION IS DRAWN. Its reserved value `system` says this
|
|
126
|
+
// is the workspace's machinery rather than one of its domain nouns, and folds
|
|
127
|
+
// the collection out of the record tree and onto the schema surface.
|
|
128
|
+
//
|
|
129
|
+
// The two agreed for every collection until `repos` — machinery whose records are ordinary,
|
|
130
|
+
// hand-edited files under `data/` — and it is why they must never be collapsed into one: a
|
|
131
|
+
// surface routing a `repos` write by the partition sends it to the system write path, whose
|
|
132
|
+
// entity ops know no such kind, and the write dies as a 400. Measured.
|
|
114
133
|
const system = d.storage?.base === 'runtime';
|
|
115
134
|
return { collection: d.name, meta, system };
|
|
116
135
|
}
|
package/src/ref.js
CHANGED
|
@@ -1,15 +1,37 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// The words a collection answers to on the command line: its declared name and its `singular`
|
|
2
|
+
// (compile stamps one on every descriptor — derived by inflection, authorable where inflection is
|
|
3
|
+
// wrong — and refuses two collections whose words collide). `dt add task …` and `dt add tasks …`
|
|
4
|
+
// are the same call. ⚠ TYPED INPUT ONLY: a reference VALUE inside a record (`tasks/kickoff`) is
|
|
5
|
+
// parsed by namespace.js's parseRef against declared names and never learns the singular, so
|
|
6
|
+
// `task/kickoff` in a field stays the dangling reference `check` reports it as.
|
|
7
|
+
function* words(descriptors) {
|
|
8
|
+
for (const [name, d] of descriptors) {
|
|
9
|
+
yield [name, name];
|
|
10
|
+
if (d?.singular && d.singular !== name) yield [d.singular, name];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The declared collection a typed word names — the name itself or its singular — else null. */
|
|
15
|
+
export function canonicalCollection(descriptors, word) {
|
|
16
|
+
if (descriptors.has(word)) return word;
|
|
17
|
+
for (const [w, name] of words(descriptors)) if (w === word) return name;
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// split "<collection>/<id>" against the DECLARED collections and their singulars — longest prefix
|
|
22
|
+
// at a "/" boundary, because both collection names and ids may contain slashes (namespaces;
|
|
23
|
+
// path-shaped ids). The collection returned is always the declared NAME, whichever word was typed.
|
|
3
24
|
export function splitRef(descriptors, ref) {
|
|
4
25
|
let best = null;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
26
|
+
let bestName = null;
|
|
27
|
+
for (const [w, name] of words(descriptors)) {
|
|
28
|
+
if (ref === w || ref.startsWith(w + '/')) {
|
|
29
|
+
if (!best || w.length > best.length) { best = w; bestName = name; }
|
|
8
30
|
}
|
|
9
31
|
}
|
|
10
32
|
if (!best) throw new Error(`unknown collection in reference "${ref}" (known: ${[...descriptors.keys()].sort().join(', ')})`);
|
|
11
33
|
if (ref === best) throw new Error(`reference "${ref}" names a collection but no record id`);
|
|
12
|
-
return { collection:
|
|
34
|
+
return { collection: bestName, id: ref.slice(best.length + 1) };
|
|
13
35
|
}
|
|
14
36
|
|
|
15
37
|
/**
|
package/src/schema-ops.js
CHANGED
|
@@ -755,6 +755,9 @@ const COLLECTION_SETTABLE = {
|
|
|
755
755
|
title: (v) => String(v),
|
|
756
756
|
title_template: (v) => String(v),
|
|
757
757
|
icon: (v) => String(v),
|
|
758
|
+
// The collection's partition. `group=system` is the reserved value: it moves the collection out
|
|
759
|
+
// of the block's domain listing and onto a surface's schema surface, and changes nothing about
|
|
760
|
+
// where its records live or whether they can be written.
|
|
758
761
|
group: (v) => String(v),
|
|
759
762
|
sort_field: (v) => String(v),
|
|
760
763
|
order: (v) => {
|
package/src/workspace.js
CHANGED
|
@@ -49,3 +49,53 @@ export function nestedAsModule(outer, inner) {
|
|
|
49
49
|
.split(path.sep)
|
|
50
50
|
.some((seg) => MODULE_SEGMENTS.has(seg));
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
/** The VS Code-family extension for this workspace, as the editor learns of it: a recommendation
|
|
54
|
+
* in `.vscode/extensions.json`, which VS Code, Cursor and code-server all read on open and offer to
|
|
55
|
+
* install. Written by `init` and kept current by `compile`, because a compiled workspace that never
|
|
56
|
+
* names its editor left a first-run agent searching a registry for the id (measured 2026-09-24:
|
|
57
|
+
* three install attempts, two of them into a directory the running editor did not read). An
|
|
58
|
+
* existing file is MERGED, never replaced: a file the operator authored (comments included) is left
|
|
59
|
+
* alone whenever it already names the extension; one that does not is re-read as JSON and gains the
|
|
60
|
+
* id, and one that is neither is left with a warning rather than clobbered. */
|
|
61
|
+
export const EDITOR_EXTENSION_ID = 'dreamteamer.dreamteamer-vscode';
|
|
62
|
+
export function ensureEditorRecommendation(root, warn = console.warn) {
|
|
63
|
+
const file = path.join(root, '.vscode', 'extensions.json');
|
|
64
|
+
if (!fs.existsSync(file)) {
|
|
65
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
66
|
+
fs.writeFileSync(file, JSON.stringify({ recommendations: [EDITOR_EXTENSION_ID] }, null, '\t') + '\n');
|
|
67
|
+
return 'written';
|
|
68
|
+
}
|
|
69
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
70
|
+
if (text.includes(EDITOR_EXTENSION_ID)) return 'present';
|
|
71
|
+
let parsed;
|
|
72
|
+
try { parsed = JSON.parse(text); } catch {
|
|
73
|
+
warn(`⚠ .vscode/extensions.json does not recommend ${EDITOR_EXTENSION_ID} and is not plain JSON, so it was left alone — add the id to its "recommendations" by hand`);
|
|
74
|
+
return 'left';
|
|
75
|
+
}
|
|
76
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) { warn(`⚠ .vscode/extensions.json is not an object — left alone; add ${EDITOR_EXTENSION_ID} to "recommendations" by hand`); return 'left'; }
|
|
77
|
+
parsed.recommendations = [...(Array.isArray(parsed.recommendations) ? parsed.recommendations : []), EDITOR_EXTENSION_ID];
|
|
78
|
+
fs.writeFileSync(file, JSON.stringify(parsed, null, '\t') + '\n');
|
|
79
|
+
return 'merged';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** `.env.example` lists every env key the installed modules declare — the file the missing-key
|
|
83
|
+
* warning points at, which used to carry two comment lines and none of the keys it was cited for.
|
|
84
|
+
* Append-only and idempotent: a key already named in the file (as `KEY=` or `# KEY`) is not added
|
|
85
|
+
* again, and nothing an operator wrote is touched. Values never appear here — an example does. */
|
|
86
|
+
export function ensureEnvExample(root, entries, header = '') {
|
|
87
|
+
if (!entries.length) return [];
|
|
88
|
+
const file = path.join(root, '.env.example');
|
|
89
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : header;
|
|
90
|
+
const named = new Set([...existing.matchAll(/^\s*#?\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/gm)].map((m) => m[1]));
|
|
91
|
+
const added = [];
|
|
92
|
+
let out = existing;
|
|
93
|
+
for (const e of entries) {
|
|
94
|
+
if (named.has(e.key)) continue;
|
|
95
|
+
const who = e.modules?.length ? ` (module ${e.modules.join(', ')})` : '';
|
|
96
|
+
out = out.trimEnd() + `\n\n# ${e.description ?? `declared by ${e.modules.join(', ')}`}${e.description ? who : ''}\n${e.key}=${e.example ?? ''}\n`;
|
|
97
|
+
added.push(e.key);
|
|
98
|
+
}
|
|
99
|
+
if (added.length) fs.writeFileSync(file, out.replace(/^\n+/, ''));
|
|
100
|
+
return added;
|
|
101
|
+
}
|