dreamteamer 0.6.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/LICENSE +202 -0
- package/NOTICE +16 -0
- package/README.md +83 -0
- package/agents/dreamteamer.agent.md +7 -0
- package/bin/dreamteamer.js +65 -0
- package/collection-templates/docs.collection-template.yaml +14 -0
- package/collection-templates/entity.collection-template.yaml +16 -0
- package/collections/agents.collection.yaml +33 -0
- package/collections/collection-templates.collection.yaml +20 -0
- package/collections/collections.collection.yaml +78 -0
- package/collections/command-bindings.collection.yaml +46 -0
- package/collections/commands.collection.yaml +36 -0
- package/collections/repos.collection.yaml +42 -0
- package/collections/skills.collection.yaml +22 -0
- package/collections/ui-views.collection.yaml +48 -0
- package/collections/users.collection.yaml +21 -0
- package/package.json +58 -0
- package/skills/building-dreamteamer/SKILL.md +117 -0
- package/skills/building-dreamteamer/references/agents.md +44 -0
- package/skills/building-dreamteamer/references/before-you-build.md +42 -0
- package/skills/building-dreamteamer/references/collections.md +120 -0
- package/skills/building-dreamteamer/references/commands.md +69 -0
- package/skills/building-dreamteamer/references/skills.md +73 -0
- package/skills/building-dreamteamer/references/ui-components.md +78 -0
- package/skills/building-dreamteamer/references/ui-views.md +59 -0
- package/skills/using-dreamteamer/SKILL.md +100 -0
- package/skills/using-dreamteamer/references/git-events.md +64 -0
- package/skills/using-dreamteamer/references/records.md +102 -0
- package/src/check.js +193 -0
- package/src/cli.js +250 -0
- package/src/collections-cli.js +389 -0
- package/src/commit.js +117 -0
- package/src/compile.js +747 -0
- package/src/events.js +124 -0
- package/src/field-values.js +69 -0
- package/src/filter.js +107 -0
- package/src/harnesses.js +233 -0
- package/src/history.js +64 -0
- package/src/init.js +307 -0
- package/src/presentation.js +190 -0
- package/src/record-commands.js +84 -0
- package/src/records.js +73 -0
- package/src/runtime.js +96 -0
- package/src/schema-ops.js +263 -0
- package/src/semver.js +32 -0
- package/src/server.js +291 -0
- package/src/store.js +450 -0
- package/src/template.js +98 -0
- package/src/temporal.js +149 -0
- package/src/workspace.js +51 -0
- package/src/yaml.js +6 -0
package/src/runtime.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// THE BOUNDARY — `.dreamteamer/` is the one artifact the two halves of this engine share.
|
|
2
|
+
//
|
|
3
|
+
// The workspace compiler WRITES it (compile.js: modules × sources → merged descriptors + manifest).
|
|
4
|
+
// The record layer READS it, and reads nothing else: store.js has never needed to know that
|
|
5
|
+
// modules, channels, `extends` or `templates` exist. That seam was already real — it just wasn't
|
|
6
|
+
// enforceable, because reaching the compiled output meant importing compile.js, which put a
|
|
7
|
+
// record-layer → compiler edge in the graph for what is really a file-format dependency.
|
|
8
|
+
//
|
|
9
|
+
// Everything the record layer actually wanted is already IN the compiled output: the merged
|
|
10
|
+
// descriptors, and (in the manifest) which directories hold the sources behind runtime-based
|
|
11
|
+
// records. So this module owns the runtime's shape, both halves import it, and `npm run layers`
|
|
12
|
+
// fails if the old edge comes back.
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { load } from './yaml.js';
|
|
16
|
+
|
|
17
|
+
export const RUNTIME_DIR = '.dreamteamer';
|
|
18
|
+
|
|
19
|
+
/** One message, two callers with different manners: the store throws it, `check` prints it. */
|
|
20
|
+
export const NO_RUNTIME = 'no compiled runtime — run `dreamteamer compile` first';
|
|
21
|
+
|
|
22
|
+
export function runtimeDir(root) {
|
|
23
|
+
return path.join(root, RUNTIME_DIR);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function readManifest(root) {
|
|
27
|
+
try { return load(fs.readFileSync(path.join(runtimeDir(root), 'manifest.yaml'), 'utf8')); } catch { return null; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A kind's folder inside the compiled runtime. Flat (`.dreamteamer/<kind>`) is what compile writes;
|
|
32
|
+
* `.dreamteamer/system/<kind>` is probed because the runtime on disk may have been compiled by an
|
|
33
|
+
* engine from before the flatten — a stale runtime is the normal state between a `git pull` and the
|
|
34
|
+
* next `dt compile`, and answering "no compiled runtime" for one would be a lie.
|
|
35
|
+
*/
|
|
36
|
+
export function runtimeKindDir(root, kind) {
|
|
37
|
+
const flat = path.join(runtimeDir(root), kind);
|
|
38
|
+
if (fs.existsSync(flat)) return flat;
|
|
39
|
+
const nested = path.join(runtimeDir(root), 'system', kind);
|
|
40
|
+
return fs.existsSync(nested) ? nested : flat;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The merged collection descriptors, keyed by name — or `null` when nothing has been compiled.
|
|
45
|
+
*
|
|
46
|
+
* The ONE place descriptors are read, so the one place `storage.base` is guaranteed present. Both
|
|
47
|
+
* readers (store.js, check.js) had their own copy of this loop; the store's copy threw and check's
|
|
48
|
+
* printed-and-returned-2, so the difference is kept at the call site rather than in the loader.
|
|
49
|
+
*/
|
|
50
|
+
export function loadDescriptors(root) {
|
|
51
|
+
const dir = runtimeKindDir(root, 'collections');
|
|
52
|
+
if (!fs.existsSync(dir)) return null;
|
|
53
|
+
const out = new Map();
|
|
54
|
+
for (const f of fs.readdirSync(dir).sort()) {
|
|
55
|
+
if (!f.endsWith('.collection.yaml')) continue;
|
|
56
|
+
const d = load(fs.readFileSync(path.join(dir, f), 'utf8'));
|
|
57
|
+
d.storage ??= {};
|
|
58
|
+
d.storage.base ??= derivedBase(d);
|
|
59
|
+
out.set(d.name, d);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Which root `storage.path` is relative to — the whole of what the record layer needs to know about
|
|
66
|
+
* the system/data distinction, as DATA rather than as a string test it has to perform. `runtime` =
|
|
67
|
+
* compiled sources (skills, agents, ui-views, the descriptors themselves): generated, gitignored,
|
|
68
|
+
* and therefore not writable through the store. `workspace` = data/ and state/ records.
|
|
69
|
+
*
|
|
70
|
+
* compile.js writes the field. This derivation is compat for a runtime compiled by an OLDER engine,
|
|
71
|
+
* and is not optional: without it those collections resolve under the workspace root, where — in the
|
|
72
|
+
* `workspace-module` layout — they do not exist. That reads as zero records (a silent success) and
|
|
73
|
+
* lets `writableDescriptor` treat a compiled artifact as writable. Wrong in the expensive direction.
|
|
74
|
+
*
|
|
75
|
+
* ⚠ It tests for `system/`, which the flatten removed — that is correct and not a leftover. The only
|
|
76
|
+
* descriptors reaching it are ones compiled BEFORE `base` existed, and those necessarily still spell
|
|
77
|
+
* their runtime paths `system/<kind>`. Every descriptor this engine writes carries `base` explicitly.
|
|
78
|
+
*/
|
|
79
|
+
function derivedBase(d) {
|
|
80
|
+
return String(d.storage?.path ?? '').startsWith('system/') ? 'runtime' : 'workspace';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Absolute directories that may hold the SOURCES behind runtime-based records — every compiled
|
|
85
|
+
* module except npm copies (foreign installed artifacts, never rewrite targets), plus the workspace
|
|
86
|
+
* root. Used by ref surgery: renaming `collections/x` has to reach the descriptor in whichever
|
|
87
|
+
* module ships it, not the merged copy under `.dreamteamer/`.
|
|
88
|
+
*
|
|
89
|
+
* Read from the manifest rather than by re-running module discovery, which is both cheaper and more
|
|
90
|
+
* honest: the manifest records what was actually compiled, so a shadowed copy is already excluded.
|
|
91
|
+
*/
|
|
92
|
+
export function sourceRoots(root) {
|
|
93
|
+
const modules = readManifest(root)?.modules ?? [];
|
|
94
|
+
const roots = [root, ...modules.filter((m) => m.channel !== 'npm').map((m) => path.resolve(root, m.root))];
|
|
95
|
+
return [...new Set(roots)];
|
|
96
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// schema operations — source-writing mutations shared by the CLI meta verbs and the
|
|
2
|
+
// server's schema endpoints. the contract (audit finding 11, clean-room bug 2): an op
|
|
3
|
+
// writes sources, proves them with a REAL compile, and only then commits — an
|
|
4
|
+
// uncompilable source can never land in history. the successful gate compile also
|
|
5
|
+
// leaves the runtime fresh, which kills the add-field-right-after-collections-add
|
|
6
|
+
// papercut (review finding 7): schema ops ARE explicit compiles.
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
10
|
+
import { load, dump } from './yaml.js';
|
|
11
|
+
import { compile, kindDir, titleCase } from './compile.js';
|
|
12
|
+
import { readManifest, runtimeKindDir } from './runtime.js';
|
|
13
|
+
|
|
14
|
+
// ---- the gate -------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
function writeGated(ws, store, files, subject, mutate) {
|
|
17
|
+
// same guarantees as record writes (docs-audit catch): the STORE's cross-process lock
|
|
18
|
+
// serializes schema ops too, and a failed git commit rolls the source back — a schema
|
|
19
|
+
// op fails closed exactly like a record mutation.
|
|
20
|
+
return store.withWriteLock(() => {
|
|
21
|
+
const snapshots = files.map((f) => ({ f, prev: fs.existsSync(f) ? fs.readFileSync(f) : null }));
|
|
22
|
+
const restore = () => {
|
|
23
|
+
for (const { f, prev } of snapshots) {
|
|
24
|
+
if (prev === null) fs.rmSync(f, { force: true });
|
|
25
|
+
else fs.writeFileSync(f, prev);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
mutate();
|
|
29
|
+
try {
|
|
30
|
+
compile(ws); // dry-run that doubles as the materialization — throws CompileError on bad sources
|
|
31
|
+
} catch (e) {
|
|
32
|
+
restore();
|
|
33
|
+
try { compile(ws); } catch { /* runtime was already broken before this op */ }
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
36
|
+
const rels = files.map((f) => path.relative(ws.root, f));
|
|
37
|
+
// Schema ops commit UNCONDITIONALLY — `auto-commit` governs RECORD writes only. A source
|
|
38
|
+
// change is inseparable from the compile that validated it, and `dt commit` scopes itself
|
|
39
|
+
// to record directories, so a deferred source edit would be publishable by nothing.
|
|
40
|
+
// Extending `dt commit` to module sources is the natural follow-on; it is not this wave.
|
|
41
|
+
try {
|
|
42
|
+
execFileSync('git', ['add', '--', ...rels], { cwd: ws.root });
|
|
43
|
+
execFileSync('git', ['commit', '--quiet', '-m', subject, '--', ...rels], { cwd: ws.root });
|
|
44
|
+
} catch (e) {
|
|
45
|
+
try { execFileSync('git', ['reset', '--quiet', '--', ...rels], { cwd: ws.root }); } catch { /* nothing staged */ }
|
|
46
|
+
restore();
|
|
47
|
+
try { compile(ws); } catch { /* pre-op sources were compilable */ }
|
|
48
|
+
throw new Error(`git commit failed — the schema change was rolled back, nothing was changed. (${e.message.split('\n')[0]})`);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The workspace's writable source dir for a kind (workspace-module aware). `kindDir` picks the
|
|
54
|
+
* layout that module already uses and falls back to flat, so a `collections add` never splits a
|
|
55
|
+
* half-moved module across both. */
|
|
56
|
+
export function workspaceSystemDir(ws, kind) {
|
|
57
|
+
const wm = ws.pkg.dreamteamer?.['workspace-module'];
|
|
58
|
+
return kindDir(wm ? path.join(ws.root, 'modules', wm) : ws.root, kind);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---- ops ------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
export function createCollection(ws, store, { name, template }) {
|
|
64
|
+
if (!name) throw new Error('missing collection name');
|
|
65
|
+
if (store.descriptors.has(name)) throw new Error(`collection "${name}" already exists`);
|
|
66
|
+
const dest = path.join(workspaceSystemDir(ws, 'collections'), `${name}.collection.yaml`);
|
|
67
|
+
if (fs.existsSync(dest)) throw new Error(`${path.relative(ws.root, dest)} already exists`);
|
|
68
|
+
|
|
69
|
+
let descriptor = { name };
|
|
70
|
+
if (template) {
|
|
71
|
+
const tplFile = path.join(runtimeKindDir(ws.root, 'collection-templates'), `${template}.collection-template.yaml`);
|
|
72
|
+
if (!fs.existsSync(tplFile)) throw new Error(`unknown collection-template "${template}"`);
|
|
73
|
+
descriptor = { name, ...structuredClone(load(fs.readFileSync(tplFile, 'utf8')).template) };
|
|
74
|
+
} else {
|
|
75
|
+
// templateless: MINIMAL but compilable — grow it with add-field
|
|
76
|
+
descriptor.id = { generate: '{{ name | slug }}' };
|
|
77
|
+
descriptor.schema = { type: 'object', required: ['name'], properties: { name: { type: 'string' } } };
|
|
78
|
+
}
|
|
79
|
+
descriptor.storage = {
|
|
80
|
+
path: `${ws.pkg.dreamteamer?.['data-path'] ?? 'data'}/${name}`,
|
|
81
|
+
codec: 'md', shape: 'file',
|
|
82
|
+
...descriptor.storage,
|
|
83
|
+
suffix: descriptor.storage?.suffix ?? singular(name),
|
|
84
|
+
};
|
|
85
|
+
writeGated(ws, store, [dest], `dreamteamer: collections add ${name}`, () => {
|
|
86
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
87
|
+
fs.writeFileSync(dest, dump(descriptor));
|
|
88
|
+
});
|
|
89
|
+
return { file: dest, descriptor };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function removeCollection(ws, store, name, { force = false } = {}) {
|
|
93
|
+
const d = store.descriptor(name);
|
|
94
|
+
const dest = path.join(workspaceSystemDir(ws, 'collections'), `${name}.collection.yaml`);
|
|
95
|
+
if (!fs.existsSync(dest)) throw new Error(`"${name}" is not workspace-owned — it ships with a module; add "<module>/${name}" to dreamteamer.disable instead`);
|
|
96
|
+
const dataDir = path.join(ws.root, d.storage.path);
|
|
97
|
+
const hasRecords = fs.existsSync(dataDir) && fs.readdirSync(dataDir).some((e) => !e.startsWith('.'));
|
|
98
|
+
if (hasRecords && !force) throw new Error(`collection "${name}" still has records under ${d.storage.path} — remove them first or pass force`);
|
|
99
|
+
writeGated(ws, store, [dest], `dreamteamer: collections rm ${name}`, () => fs.rmSync(dest));
|
|
100
|
+
return { removed: name };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function addField(ws, store, collection, { name: fieldName, prop, required }) {
|
|
104
|
+
store.descriptor(collection); // must exist in the compiled runtime
|
|
105
|
+
if (!fieldName) throw new Error('missing field name');
|
|
106
|
+
if (store.descriptor(collection).schema?.properties?.[fieldName]) throw new Error(`field "${fieldName}" already exists on ${collection}`);
|
|
107
|
+
return upsertField(ws, store, collection, fieldName, prop, required, `add-field ${fieldName}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function updateField(ws, store, collection, fieldName, { prop, required }) {
|
|
111
|
+
const d = store.descriptor(collection);
|
|
112
|
+
if (!d.schema?.properties?.[fieldName]) throw new Error(`no field "${fieldName}" on ${collection}`);
|
|
113
|
+
// upsertField REPLACES the prop, so retyping a field would silently drop its hand-authored
|
|
114
|
+
// `description`. Changing a field's type is not a decision to undocument it. Same for an
|
|
115
|
+
// authored `title` — but ONLY an authored one: a derived title is compile's output, not a
|
|
116
|
+
// human's choice, and `titleCase` is how the two are told apart.
|
|
117
|
+
const previous = d.schema.properties[fieldName];
|
|
118
|
+
if (prop.description === undefined && typeof previous.description === 'string') prop = { ...prop, description: previous.description };
|
|
119
|
+
if (prop.title === undefined && typeof previous.title === 'string' && previous.title !== titleCase(fieldName)) prop = { ...prop, title: previous.title };
|
|
120
|
+
return upsertField(ws, store, collection, fieldName, prop, required, `update-field ${fieldName}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function removeField(ws, store, collection, fieldName) {
|
|
124
|
+
const d = store.descriptor(collection);
|
|
125
|
+
if (!d.schema?.properties?.[fieldName]) throw new Error(`no field "${fieldName}" on ${collection}`);
|
|
126
|
+
const dest = path.join(workspaceSystemDir(ws, 'collections'), `${collection}.collection.yaml`);
|
|
127
|
+
if (!fs.existsSync(dest)) throw new Error(`"${collection}" is module-shipped; the workspace can only OVERRIDE fields (extends), not remove them`);
|
|
128
|
+
const doc = load(fs.readFileSync(dest, 'utf8'));
|
|
129
|
+
if (!doc.schema?.properties?.[fieldName]) throw new Error(`field "${fieldName}" is inherited from the base module — the workspace descriptor doesn't declare it`);
|
|
130
|
+
writeGated(ws, store, [dest], `dreamteamer: ${collection} remove-field ${fieldName}`, () => {
|
|
131
|
+
delete doc.schema.properties[fieldName];
|
|
132
|
+
if (Array.isArray(doc.schema.required)) doc.schema.required = doc.schema.required.filter((r) => r !== fieldName);
|
|
133
|
+
fs.writeFileSync(dest, dump(doc));
|
|
134
|
+
});
|
|
135
|
+
return { collection, removed: fieldName };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function upsertField(ws, store, collection, fieldName, prop, required, verb) {
|
|
139
|
+
if (prop == null || typeof prop !== 'object' || Array.isArray(prop)) {
|
|
140
|
+
throw new Error(`field "${fieldName}": prop must be a JSON-Schema object (got ${Array.isArray(prop) ? 'array' : typeof prop}) — nothing was written.`);
|
|
141
|
+
}
|
|
142
|
+
// compile resolves `prop.title` into the COMPILED descriptor, and both writers rebuild a prop
|
|
143
|
+
// from that projection — this one and the studio's field drawer. Without this, retyping any
|
|
144
|
+
// field through the UI writes the DERIVED label back into the source as though a human chose
|
|
145
|
+
// it, and 51 collections fill with `title: Due Date` noise no longer distinguishable from a
|
|
146
|
+
// real override. The webview applies the identical rule in `lib/field-prop.ts`.
|
|
147
|
+
if (prop.title === titleCase(fieldName)) {
|
|
148
|
+
prop = { ...prop };
|
|
149
|
+
delete prop.title;
|
|
150
|
+
}
|
|
151
|
+
// Same rule for the value template. presentation INHERITS a reference's template from its
|
|
152
|
+
// target collection's `title_template`, so a field drawer that round-trips that projection
|
|
153
|
+
// writes the inherited value back onto the field — hand-recreating exactly the 49 duplicated
|
|
154
|
+
// `x-display` lines the inheritance replaced. Only a template that DIFFERS from the target's
|
|
155
|
+
// is a real authored override.
|
|
156
|
+
const ref = prop['x-reference'] ?? prop.items?.['x-reference'];
|
|
157
|
+
const inherited = ref && ref !== '*' ? store.descriptors.get(ref)?.title_template : undefined;
|
|
158
|
+
if (inherited) {
|
|
159
|
+
if (prop['x-title-template'] === inherited) {
|
|
160
|
+
prop = { ...prop };
|
|
161
|
+
delete prop['x-title-template'];
|
|
162
|
+
}
|
|
163
|
+
if (prop.items?.['x-title-template'] === inherited) {
|
|
164
|
+
prop = { ...prop, items: { ...prop.items } };
|
|
165
|
+
delete prop.items['x-title-template'];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const dest = path.join(workspaceSystemDir(ws, 'collections'), `${collection}.collection.yaml`);
|
|
169
|
+
let doc;
|
|
170
|
+
if (fs.existsSync(dest)) {
|
|
171
|
+
doc = load(fs.readFileSync(dest, 'utf8'));
|
|
172
|
+
} else {
|
|
173
|
+
doc = { name: collection, extends: baseModuleRef(ws.root, collection), schema: { properties: {} } };
|
|
174
|
+
}
|
|
175
|
+
writeGated(ws, store, [dest], `dreamteamer: ${collection} ${verb}`, () => {
|
|
176
|
+
doc.schema ??= { properties: {} };
|
|
177
|
+
doc.schema.properties ??= {};
|
|
178
|
+
doc.schema.properties[fieldName] = prop;
|
|
179
|
+
if (required === true) doc.schema.required = [...new Set([...(doc.schema.required ?? []), fieldName])];
|
|
180
|
+
if (required === false && Array.isArray(doc.schema.required)) doc.schema.required = doc.schema.required.filter((r) => r !== fieldName);
|
|
181
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
182
|
+
fs.writeFileSync(dest, dump(doc));
|
|
183
|
+
});
|
|
184
|
+
return { collection, field: fieldName, file: dest, extends: doc.extends };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// saved views (M3): a studio-saved view IS a ui-view record — but ui-views are
|
|
188
|
+
// system-stored (sources + compile), so the write goes through the same gate as any
|
|
189
|
+
// other schema op. the studio "save view" button lands here.
|
|
190
|
+
export function saveUiView(ws, store, { id, view }) {
|
|
191
|
+
if (!id || !/^[a-z0-9][a-z0-9-/]*$/.test(id)) throw new Error(`invalid ui-view id "${id}" — lowercase slug required`);
|
|
192
|
+
const dest = path.join(workspaceSystemDir(ws, 'ui-views'), `${id}.ui-view.yaml`);
|
|
193
|
+
const existed = fs.existsSync(dest);
|
|
194
|
+
writeGated(ws, store, [dest], `dreamteamer: ui-views ${existed ? 'update' : 'add'} ${id}`, () => {
|
|
195
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
196
|
+
fs.writeFileSync(dest, dump(view));
|
|
197
|
+
});
|
|
198
|
+
return { id, file: dest, updated: existed };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function removeUiView(ws, store, id) {
|
|
202
|
+
const dest = path.join(workspaceSystemDir(ws, 'ui-views'), `${id}.ui-view.yaml`);
|
|
203
|
+
if (!fs.existsSync(dest)) throw new Error(`ui-view "${id}" is not workspace-owned (module-shipped views are removed via dreamteamer.disable)`);
|
|
204
|
+
writeGated(ws, store, [dest], `dreamteamer: ui-views rm ${id}`, () => fs.rmSync(dest));
|
|
205
|
+
return { removed: id };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// base module for an extends pointer — resolved via manifest.modules across ALL channels
|
|
209
|
+
// (audit open finding 1: the old regex only understood inline modules/… paths)
|
|
210
|
+
function baseModuleRef(root, collection) {
|
|
211
|
+
const manifest = readManifest(root) ?? {};
|
|
212
|
+
// entry keys are runtime-relative and lost their `system/` prefix in the flatten; a manifest
|
|
213
|
+
// written by an older engine still carries it, and this reads whatever is on disk
|
|
214
|
+
const entry = manifest.entries?.[`collections/${collection}.collection.yaml`]
|
|
215
|
+
?? manifest.entries?.[`system/collections/${collection}.collection.yaml`];
|
|
216
|
+
const src = entry?.sources?.[0];
|
|
217
|
+
const srcPath = typeof src === 'string' ? src : src?.path;
|
|
218
|
+
if (!srcPath) throw new Error(`cannot determine the base module for "${collection}"`);
|
|
219
|
+
for (const m of manifest.modules ?? []) {
|
|
220
|
+
const modRoot = m.root === '.' ? '' : `${m.root}/`;
|
|
221
|
+
if (modRoot && srcPath.startsWith(modRoot)) return `${m.name}/${collection}`;
|
|
222
|
+
}
|
|
223
|
+
throw new Error(`cannot determine the base module for "${collection}" — its source is ${srcPath}; edit that descriptor directly`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// CLI/API type sugar → JSON Schema property
|
|
227
|
+
export function fieldDef(store, flags) {
|
|
228
|
+
const t = flags.type ?? 'string';
|
|
229
|
+
const def = flags['default-value'] ?? flags.default;
|
|
230
|
+
const p = (() => {
|
|
231
|
+
switch (t) {
|
|
232
|
+
case 'string': case 'text': return { type: 'string' };
|
|
233
|
+
case 'markdown': return { type: 'string', format: 'markdown' };
|
|
234
|
+
case 'boolean': return { type: 'boolean' };
|
|
235
|
+
case 'number': return { type: 'number' };
|
|
236
|
+
case 'integer': return { type: 'integer' };
|
|
237
|
+
case 'date': return { type: 'string', format: 'date' };
|
|
238
|
+
// `timestamp` is the WIRE type presentation.js projects `date-time` to, and therefore what
|
|
239
|
+
// the studio's field drawer round-trips. Accepting it here means the vocabulary you read
|
|
240
|
+
// out of `presentation` is the vocabulary you can type back into the CLI.
|
|
241
|
+
case 'datetime': case 'timestamp': return { type: 'string', format: 'date-time' };
|
|
242
|
+
case 'enum': {
|
|
243
|
+
if (!flags.options) throw new Error('enum needs options "a,b,c"');
|
|
244
|
+
const opts = Array.isArray(flags.options) ? flags.options : flags.options.split(',').map((s) => s.trim());
|
|
245
|
+
return { type: 'string', enum: opts };
|
|
246
|
+
}
|
|
247
|
+
case 'tags': return { type: 'array', items: { type: 'string' } };
|
|
248
|
+
default:
|
|
249
|
+
if (store.descriptors.has(t)) return { type: 'string', 'x-reference': t };
|
|
250
|
+
if (t === 'reference') return { type: 'string', 'x-reference': flags.target ?? '*' };
|
|
251
|
+
throw new Error(`unknown field type "${t}"`);
|
|
252
|
+
}
|
|
253
|
+
})();
|
|
254
|
+
if (def !== undefined) p.default = p.type === 'boolean' ? def === 'true' || def === true : p.type === 'number' || p.type === 'integer' ? Number(def) : def;
|
|
255
|
+
// what the field MEANS, in one line — JSON Schema's own keyword, projected to every surface by
|
|
256
|
+
// presentation.js. A field whose name doesn't say enough is documented here, not in a comment.
|
|
257
|
+
if (typeof flags.description === 'string' && flags.description.length > 0) p.description = flags.description;
|
|
258
|
+
return p;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function singular(name) {
|
|
262
|
+
return name.endsWith('ies') ? name.slice(0, -3) + 'y' : name.endsWith('s') ? name.slice(0, -1) : name;
|
|
263
|
+
}
|
package/src/semver.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// tiny semver-range checker for engine-pin checks — deliberately NOT npm's semver.
|
|
2
|
+
// supported ranges: exact "1.2.3", "^1.2.3", "~1.2.3", ">=1.2.3", "*".
|
|
3
|
+
// NOT supported (returns null, "can't tell"): "||" alternatives, hyphen ranges,
|
|
4
|
+
// x-ranges (1.2.x), combined comparators (">=1.0.0 <2.0.0"), <, <=, >,
|
|
5
|
+
// prerelease/build tags. callers treat null as "warn, don't guess".
|
|
6
|
+
|
|
7
|
+
const parse = (v) => {
|
|
8
|
+
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(String(v).trim());
|
|
9
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const cmp = (a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
13
|
+
|
|
14
|
+
// true = in range, false = out of range, null = version or range not understood
|
|
15
|
+
export function satisfies(version, range) {
|
|
16
|
+
const v = parse(version);
|
|
17
|
+
if (!v) return null;
|
|
18
|
+
const r = String(range).trim();
|
|
19
|
+
if (r === '*') return true;
|
|
20
|
+
const m = /^(>=|\^|~)?(\d+\.\d+\.\d+)$/.exec(r);
|
|
21
|
+
if (!m) return null;
|
|
22
|
+
const base = parse(m[2]);
|
|
23
|
+
switch (m[1]) {
|
|
24
|
+
case '>=': return cmp(v, base) >= 0;
|
|
25
|
+
case '^': // same major, >= base; zero-major pins the minor too (npm's ^0.x rule)
|
|
26
|
+
if (cmp(v, base) < 0) return false;
|
|
27
|
+
return base[0] === 0 ? v[0] === 0 && v[1] === base[1] : v[0] === base[0];
|
|
28
|
+
case '~': // same major.minor, >= base
|
|
29
|
+
return cmp(v, base) >= 0 && v[0] === base[0] && v[1] === base[1];
|
|
30
|
+
default: return cmp(v, base) === 0; // exact
|
|
31
|
+
}
|
|
32
|
+
}
|