eidosmd 0.1.0 → 0.2.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 +50 -29
- package/browser/dist/assets/index-C2NMN_D4.css +1 -0
- package/browser/dist/assets/index-C65k1ihb.js +46 -0
- package/browser/dist/favicon.svg +5 -0
- package/browser/dist/index.html +15 -0
- package/browser/dist/mark.svg +4 -0
- package/dist/src/cli.js +7 -0
- package/dist/src/commands/agents.js +1 -1
- package/dist/src/commands/canvas.js +77 -0
- package/dist/src/commands/check.js +1 -1
- package/dist/src/commands/framework.js +24 -5
- package/dist/src/commands/index.js +4 -4
- package/dist/src/commands/init.js +1 -0
- package/dist/src/commands/instructions.js +1 -1
- package/dist/src/commands/list.js +8 -8
- package/dist/src/commands/migrate.js +32 -0
- package/dist/src/commands/new.js +3 -3
- package/dist/src/commands/seeds.js +5 -5
- package/dist/src/commands/setup.js +119 -0
- package/dist/src/commands/version.js +44 -0
- package/dist/src/commands/whoami.js +4 -4
- package/dist/src/context.js +5 -5
- package/dist/src/core/blueprint.js +16 -11
- package/dist/src/core/canvas-schema.js +148 -0
- package/dist/src/core/canvas.js +722 -0
- package/dist/src/core/check.js +127 -51
- package/dist/src/core/convert.js +7 -6
- package/dist/src/core/framework-markdown.js +113 -34
- package/dist/src/core/framework-model.js +27 -10
- package/dist/src/core/framework-structured.js +132 -33
- package/dist/src/core/framework.js +13 -13
- package/dist/src/core/git.js +59 -0
- package/dist/src/core/index-leaf.js +2 -2
- package/dist/src/core/me.js +16 -8
- package/dist/src/core/migrate.js +243 -0
- package/dist/src/core/naming.js +1 -1
- package/dist/src/core/root.js +2 -2
- package/dist/src/core/scaffold.js +19 -19
- package/dist/src/core/seed.js +151 -60
- package/dist/src/core/server.js +1236 -52
- package/dist/src/core/settings.js +181 -0
- package/dist/src/core/store.js +259 -0
- package/dist/src/core/template.js +32 -0
- package/dist/src/core/versions.js +79 -0
- package/dist/src/output.js +4 -1
- package/dist/src/program.js +134 -39
- package/instructions/authoring.md +13 -12
- package/instructions/configuring.md +65 -34
- package/instructions/init-required.md +4 -4
- package/instructions/overview.md +17 -7
- package/instructions/validating.md +5 -4
- package/package.json +21 -12
- package/standard/EIDOS.md +129 -188
- package/standard/seeds/README.md +6 -6
- package/standard/seeds/book/Framework.yaml +81 -0
- package/standard/seeds/book/README.md +9 -5
- package/standard/seeds/book/_gitignore +3 -3
- package/standard/seeds/book/me.md +1 -1
- package/standard/seeds/book/roles/README.md +3 -3
- package/standard/seeds/book/roles/framework-owner.md +2 -2
- package/standard/seeds/book/{shapes → templates}/chapter.full.md +0 -8
- package/standard/seeds/book/{shapes → templates}/chapter.sketch.md +0 -7
- package/standard/seeds/book/{shapes → templates}/frame.market.md +0 -6
- package/standard/seeds/book/templates/frame.premise.md +17 -0
- package/standard/seeds/book/{shapes → templates}/frame.reader.md +0 -6
- package/standard/seeds/book/{shapes → templates}/frame.voice.md +0 -7
- package/standard/seeds/research/Framework.yaml +81 -0
- package/standard/seeds/research/README.md +9 -5
- package/standard/seeds/research/_gitignore +3 -3
- package/standard/seeds/research/me.md +1 -1
- package/standard/seeds/research/roles/README.md +3 -3
- package/standard/seeds/research/roles/framework-owner.md +2 -2
- package/standard/seeds/research/{shapes → templates}/frame.ethics.md +0 -6
- package/standard/seeds/research/{shapes → templates}/frame.method.md +0 -7
- package/standard/seeds/research/{shapes → templates}/frame.prior-work.md +0 -6
- package/standard/seeds/research/{shapes → templates}/frame.question.md +0 -7
- package/standard/seeds/research/{shapes → templates}/investigation.full.md +0 -8
- package/standard/seeds/research/{shapes → templates}/investigation.note.md +0 -7
- package/standard/seeds/software/Framework.yaml +82 -0
- package/standard/seeds/software/README.md +5 -5
- package/standard/seeds/software/_gitignore +3 -3
- package/standard/seeds/software/me.md +1 -1
- package/standard/seeds/software/roles/README.md +3 -3
- package/standard/seeds/software/roles/framework-owner.md +2 -2
- package/standard/seeds/software/roles/project-manager.md +2 -2
- package/standard/seeds/software/roles/stakeholder.md +1 -1
- package/standard/seeds/software/{shapes → templates}/frame.architecture.md +0 -7
- package/standard/seeds/software/{shapes → templates}/frame.audience.md +1 -8
- package/standard/seeds/software/{shapes → templates}/frame.criteria.md +0 -8
- package/standard/seeds/software/{shapes → templates}/frame.market.md +0 -8
- package/standard/seeds/software/{shapes → templates}/spec.full.md +0 -8
- package/standard/seeds/software/{shapes → templates}/spec.micro.md +0 -9
- package/browser/index.html +0 -268
- package/dist/src/commands/convert.js +0 -30
- package/dist/src/core/shape.js +0 -26
- package/standard/seeds/book/Framework.md +0 -87
- package/standard/seeds/book/shapes/frame.premise.md +0 -24
- package/standard/seeds/research/Framework.md +0 -88
- package/standard/seeds/software/Framework.md +0 -88
- /package/standard/seeds/software/{shapes → templates}/.gitkeep +0 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// `eidos migrate`: the mechanical hop from a 4.x root to the standard this CLI
|
|
2
|
+
// carries (5.0.0). Five names move on disk and nothing is dropped: `_eidos/`
|
|
3
|
+
// becomes `.eidos/`, `shapes/` becomes `templates/`, the framework document
|
|
4
|
+
// takes the 5.0.0 keys, every blueprint's `flavor` becomes `variant`, and
|
|
5
|
+
// `eidos_version` is set. A root that kept the markdown Framework.md gets
|
|
6
|
+
// Framework.yaml in its place, the index inside it. A template that opens
|
|
7
|
+
// with frontmatter loses the block; one named off its unit is noted, since
|
|
8
|
+
// only the owner can say which name is right.
|
|
9
|
+
import { existsSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { isMap, isScalar, isSeq, parseDocument } from 'yaml';
|
|
12
|
+
import { FRAMEWORK_DIR, FRAMEWORK_FILES, STANDARD_CORE, unitOf } from './framework-model.js';
|
|
13
|
+
import { convertToYaml } from './convert.js';
|
|
14
|
+
import { annotateYaml } from './framework-structured.js';
|
|
15
|
+
import { parseFramework } from './framework.js';
|
|
16
|
+
import { parseVersions } from './framework-markdown.js';
|
|
17
|
+
import { stripFrontmatter } from './template.js';
|
|
18
|
+
import { readVersions, writeVersions, versionsFile } from './versions.js';
|
|
19
|
+
import { standardVersion } from '../paths.js';
|
|
20
|
+
export const LEGACY_DIR = '_eidos';
|
|
21
|
+
export class MigrationError extends Error {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'MigrationError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// A root for this command is a folder with `.eidos/` or the older `_eidos/`,
|
|
28
|
+
// found upward from `start` the way every other command finds one.
|
|
29
|
+
export function findMigratableRoot(start, explicit) {
|
|
30
|
+
const has = (dir) => existsSync(path.join(dir, FRAMEWORK_DIR)) || existsSync(path.join(dir, LEGACY_DIR));
|
|
31
|
+
if (explicit !== undefined) {
|
|
32
|
+
const resolved = path.resolve(start, explicit);
|
|
33
|
+
if (!has(resolved))
|
|
34
|
+
throw new MigrationError(`${resolved} is not a root: no ${FRAMEWORK_DIR}/ (or ${LEGACY_DIR}/) inside it`);
|
|
35
|
+
return resolved;
|
|
36
|
+
}
|
|
37
|
+
let current = path.resolve(start);
|
|
38
|
+
for (;;) {
|
|
39
|
+
if (has(current))
|
|
40
|
+
return current;
|
|
41
|
+
const parent = path.dirname(current);
|
|
42
|
+
if (parent === current)
|
|
43
|
+
throw new MigrationError(`no root found from ${start}: no ${FRAMEWORK_DIR}/ (or ${LEGACY_DIR}/) here or above`);
|
|
44
|
+
current = parent;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const rel = (root, file) => path.relative(root, file).split(path.sep).join('/');
|
|
48
|
+
function markdownFiles(dir) {
|
|
49
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory())
|
|
50
|
+
return [];
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const entry of readdirSync(dir).sort()) {
|
|
53
|
+
if (entry.startsWith('.'))
|
|
54
|
+
continue;
|
|
55
|
+
const full = path.join(dir, entry);
|
|
56
|
+
if (statSync(full).isDirectory())
|
|
57
|
+
out.push(...markdownFiles(full));
|
|
58
|
+
else if (/\.md$/i.test(entry))
|
|
59
|
+
out.push(full);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
const keyOf = (pair) => (isScalar(pair.key) ? String(pair.key.value) : null);
|
|
64
|
+
const renameKey = (pair, to) => {
|
|
65
|
+
if (isScalar(pair.key))
|
|
66
|
+
pair.key.value = to;
|
|
67
|
+
};
|
|
68
|
+
// The YAML form, key by key, comments kept: `schema` → `properties`,
|
|
69
|
+
// `flavors` → `variants`, each variant's `shape` → `template` (with its path),
|
|
70
|
+
// `unit` gone, the version set, and this CLI's guidance comments refreshed.
|
|
71
|
+
export function migrateYamlDocument(text, version) {
|
|
72
|
+
const document = parseDocument(text);
|
|
73
|
+
const top = document.contents;
|
|
74
|
+
if (!isMap(top))
|
|
75
|
+
return text;
|
|
76
|
+
// the root's versions left the framework document in 5.0.0; migrateRoot moves them
|
|
77
|
+
top.items = top.items.filter((pair) => keyOf(pair) !== 'versions');
|
|
78
|
+
for (const pair of top.items) {
|
|
79
|
+
const key = keyOf(pair);
|
|
80
|
+
if (key === 'schema')
|
|
81
|
+
renameKey(pair, 'properties');
|
|
82
|
+
// the standard's own block is migrate's to rewrite: its rows are the 5.0.0 core
|
|
83
|
+
if ((key === 'schema' || key === 'properties') && isMap(pair.value)) {
|
|
84
|
+
for (const entry of pair.value.items) {
|
|
85
|
+
if (keyOf(entry) === 'core' && isSeq(entry.value) && entry.value.items.length > 0) {
|
|
86
|
+
entry.value = document.createNode(STANDARD_CORE.map(({ name, type, meaning }) => ({ name, type, meaning })));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (key === 'eidos_version' && isScalar(pair.value))
|
|
91
|
+
pair.value.value = version;
|
|
92
|
+
if (key === 'collections' && isSeq(pair.value)) {
|
|
93
|
+
for (const collection of pair.value.items) {
|
|
94
|
+
if (!isMap(collection))
|
|
95
|
+
continue;
|
|
96
|
+
const map = collection;
|
|
97
|
+
map.items = map.items.filter((entry) => keyOf(entry) !== 'unit');
|
|
98
|
+
for (const entry of map.items) {
|
|
99
|
+
if (keyOf(entry) === 'flavors')
|
|
100
|
+
renameKey(entry, 'variants');
|
|
101
|
+
if ((keyOf(entry) === 'variants' || keyOf(entry) === 'flavors') && isSeq(entry.value)) {
|
|
102
|
+
for (const variant of entry.value.items) {
|
|
103
|
+
if (!isMap(variant))
|
|
104
|
+
continue;
|
|
105
|
+
for (const field of variant.items) {
|
|
106
|
+
if (keyOf(field) === 'shape') {
|
|
107
|
+
renameKey(field, 'template');
|
|
108
|
+
if (isScalar(field.value))
|
|
109
|
+
field.value.value = String(field.value.value).replace(/^shapes\//, 'templates/');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
annotateYaml(document);
|
|
119
|
+
return document.toString({ lineWidth: 0 });
|
|
120
|
+
}
|
|
121
|
+
// What a 4.x document recorded under `versions`, in either form, for the move
|
|
122
|
+
// to this CLI's own file.
|
|
123
|
+
export function legacyVersions(text, format) {
|
|
124
|
+
if (format === 'md')
|
|
125
|
+
return parseVersions(text);
|
|
126
|
+
const top = parseDocument(text).contents;
|
|
127
|
+
if (!isMap(top))
|
|
128
|
+
return [];
|
|
129
|
+
const out = [];
|
|
130
|
+
for (const pair of top.items) {
|
|
131
|
+
if (keyOf(pair) !== 'versions' || !isSeq(pair.value))
|
|
132
|
+
continue;
|
|
133
|
+
for (const row of pair.value.items) {
|
|
134
|
+
if (!isMap(row))
|
|
135
|
+
continue;
|
|
136
|
+
const version = String(row.get('version') ?? '').trim();
|
|
137
|
+
const commit = String(row.get('commit') ?? '').trim();
|
|
138
|
+
if (version === '' || commit === '')
|
|
139
|
+
continue;
|
|
140
|
+
const tag = String(row.get('tag') ?? '').trim();
|
|
141
|
+
out.push({ version, commit, tag: tag === '' ? null : tag });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
// A document already on this standard is left alone, except for a `versions`
|
|
147
|
+
// key it may still carry.
|
|
148
|
+
function withoutVersions(text) {
|
|
149
|
+
const document = parseDocument(text);
|
|
150
|
+
if (!isMap(document.contents) || !document.has('versions'))
|
|
151
|
+
return text;
|
|
152
|
+
document.delete('versions');
|
|
153
|
+
return document.toString({ lineWidth: 0 });
|
|
154
|
+
}
|
|
155
|
+
// The markdown form, line by line: the headings, the bullets, the links, the core row, the version.
|
|
156
|
+
export function migrateMarkdownDocument(text, version) {
|
|
157
|
+
return text
|
|
158
|
+
.replace(/^eidos_version:\s*\S+/m, `eidos_version: ${version}`)
|
|
159
|
+
.replace(/^## Schema\s*$/m, '## Properties')
|
|
160
|
+
.replace(/^### Eidos Core\s*$/m, '### Eidos')
|
|
161
|
+
.replace(/^### Custom Properties\s*$/m, '### Custom')
|
|
162
|
+
.replace(/^### (.+?) Properties\s*$/gm, '### $1')
|
|
163
|
+
.replace(/^(\s*)- \*\*Flavors:\*\*/gm, '$1- **Variants:**')
|
|
164
|
+
.replace(/^\s*- \*\*Unit:\*\*[^\n]*\n/gm, '')
|
|
165
|
+
.replace(/\]\(shapes\//g, '](templates/')
|
|
166
|
+
.replace(/^(\|\s*)flavor(\s*\|)/gm, '$1variant$2')
|
|
167
|
+
.replace(/_eidos\//g, '.eidos/');
|
|
168
|
+
}
|
|
169
|
+
export function migrateRoot(root, options) {
|
|
170
|
+
const actions = [];
|
|
171
|
+
const notes = [];
|
|
172
|
+
const version = standardVersion();
|
|
173
|
+
const act = (what, run) => {
|
|
174
|
+
actions.push(what);
|
|
175
|
+
if (!options.dryRun)
|
|
176
|
+
run();
|
|
177
|
+
};
|
|
178
|
+
const legacy = path.join(root, LEGACY_DIR);
|
|
179
|
+
const dir = path.join(root, FRAMEWORK_DIR);
|
|
180
|
+
if (existsSync(legacy) && !existsSync(dir))
|
|
181
|
+
act(`rename ${LEGACY_DIR}/ to ${FRAMEWORK_DIR}/`, () => renameSync(legacy, dir));
|
|
182
|
+
else if (!existsSync(dir))
|
|
183
|
+
throw new MigrationError(`${root} is not a root: no ${FRAMEWORK_DIR}/ (or ${LEGACY_DIR}/) inside it`);
|
|
184
|
+
// on a dry run the folder may still be the old one
|
|
185
|
+
const live = existsSync(dir) ? dir : legacy;
|
|
186
|
+
const shapes = path.join(live, 'shapes');
|
|
187
|
+
const templates = path.join(live, 'templates');
|
|
188
|
+
if (existsSync(shapes) && !existsSync(templates))
|
|
189
|
+
act(`rename ${FRAMEWORK_DIR}/shapes/ to ${FRAMEWORK_DIR}/templates/`, () => renameSync(shapes, templates));
|
|
190
|
+
const liveTemplates = existsSync(templates) ? templates : shapes;
|
|
191
|
+
const found = FRAMEWORK_FILES.map((entry) => ({ ...entry, file: path.join(live, entry.name) })).find((entry) => existsSync(entry.file));
|
|
192
|
+
if (!found)
|
|
193
|
+
throw new MigrationError(`no Framework.yaml or Framework.md under ${rel(root, live)}/`);
|
|
194
|
+
const text = readFileSync(found.file, 'utf8');
|
|
195
|
+
const from = /^eidos_version:\s*([^\s#]+)/m.exec(text)?.[1] ?? null;
|
|
196
|
+
// A YAML root already on this standard is left as it is, its comments included.
|
|
197
|
+
const next = found.format === 'yaml' ? (from === version ? withoutVersions(text) : migrateYamlDocument(text, version)) : migrateMarkdownDocument(text, version);
|
|
198
|
+
if (next !== text)
|
|
199
|
+
act(`rewrite ${FRAMEWORK_DIR}/${found.name} in the ${version} form`, () => writeFileSync(found.file, next, 'utf8'));
|
|
200
|
+
const recorded = legacyVersions(text, found.format);
|
|
201
|
+
if (recorded.length > 0) {
|
|
202
|
+
act(`move ${recorded.length} recorded version${recorded.length === 1 ? '' : 's'} to ${rel(root, versionsFile(root))}, this CLI's own record`, () => {
|
|
203
|
+
const kept = readVersions(root);
|
|
204
|
+
writeVersions(root, [...recorded.filter((entry) => !kept.some((held) => held.version === entry.version)), ...kept]);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const ignore = path.join(root, '.gitignore');
|
|
208
|
+
if (existsSync(ignore)) {
|
|
209
|
+
const held = readFileSync(ignore, 'utf8');
|
|
210
|
+
if (held.includes(`${LEGACY_DIR}/`))
|
|
211
|
+
act(`point .gitignore at ${FRAMEWORK_DIR}/`, () => writeFileSync(ignore, held.replace(new RegExp(`${LEGACY_DIR}/`, 'g'), `${FRAMEWORK_DIR}/`), 'utf8'));
|
|
212
|
+
}
|
|
213
|
+
const framework = parseFramework(next, found.format, root, path.join(dir, found.name));
|
|
214
|
+
// The markdown form left the standard in 5.0.0: the same framework is written
|
|
215
|
+
// as data, and the markdown document and each collection's index.md go.
|
|
216
|
+
if (found.format === 'md') {
|
|
217
|
+
act(`write ${FRAMEWORK_DIR}/Framework.yaml from ${found.name}, the index inside it; remove ${found.name} and each collection's index.md`, () => {
|
|
218
|
+
convertToYaml(framework, false);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
for (const collection of framework.collections) {
|
|
222
|
+
for (const file of markdownFiles(path.join(root, collection.name))) {
|
|
223
|
+
const content = readFileSync(file, 'utf8');
|
|
224
|
+
const block = /^---\r?\n[\s\S]*?\r?\n---/.exec(content);
|
|
225
|
+
if (!block || !/^flavor:/m.test(block[0]))
|
|
226
|
+
continue;
|
|
227
|
+
act(`rename flavor to variant in ${rel(root, file)}`, () => writeFileSync(file, content.replace(block[0], block[0].replace(/^flavor:/m, 'variant:')), 'utf8'));
|
|
228
|
+
}
|
|
229
|
+
const unit = unitOf(collection);
|
|
230
|
+
for (const variant of collection.variants) {
|
|
231
|
+
const file = path.join(liveTemplates, path.basename(variant.template));
|
|
232
|
+
if (existsSync(file)) {
|
|
233
|
+
const body = readFileSync(file, 'utf8');
|
|
234
|
+
if (/^---[ \t]*\r?\n/.test(body))
|
|
235
|
+
act(`strip the frontmatter block from ${FRAMEWORK_DIR}/templates/${path.basename(file)}`, () => writeFileSync(file, stripFrontmatter(body), 'utf8'));
|
|
236
|
+
}
|
|
237
|
+
const expected = unit ? `${unit}.${variant.name}.md` : null;
|
|
238
|
+
if (expected && path.basename(variant.template) !== expected)
|
|
239
|
+
notes.push(`${collection.name}/${variant.name} is at ${variant.template}; a template is named templates/${expected}, and only you can say which name is right`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { root, from, to: version, actions, notes, dryRun: options.dryRun };
|
|
243
|
+
}
|
package/dist/src/core/naming.js
CHANGED
|
@@ -15,7 +15,7 @@ export function kebab(label) {
|
|
|
15
15
|
export function isKebab(value) {
|
|
16
16
|
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
|
|
17
17
|
}
|
|
18
|
-
// `prior work` -> `Prior Work`; a declared
|
|
18
|
+
// `prior work` -> `Prior Work`; a declared variant label as a human title.
|
|
19
19
|
export function titleize(label) {
|
|
20
20
|
return label
|
|
21
21
|
.split(' ')
|
package/dist/src/core/root.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Finding the root: the one folder with
|
|
1
|
+
// Finding the root: the one folder with `.eidos/` inside it, never a folder
|
|
2
2
|
// with a particular name. Looks at the starting folder, then its ancestors,
|
|
3
3
|
// then one and two levels beneath it, so `eidos check` works from a repo root
|
|
4
4
|
// that keeps its blueprints in `Blueprints/`.
|
|
@@ -62,7 +62,7 @@ export function findRoot(start, explicit) {
|
|
|
62
62
|
if (explicit !== undefined) {
|
|
63
63
|
const resolved = path.resolve(start, explicit);
|
|
64
64
|
if (!isRoot(resolved)) {
|
|
65
|
-
throw new RootError(`${resolved} is not a root: no ${FRAMEWORK_DIR}/Framework.
|
|
65
|
+
throw new RootError(`${resolved} is not a root: no ${FRAMEWORK_DIR}/Framework.yaml inside it`);
|
|
66
66
|
}
|
|
67
67
|
return resolved;
|
|
68
68
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// Scaffolding one blueprint: frontmatter generated from the properties that
|
|
2
|
-
// apply to its collection, body rendered from its
|
|
2
|
+
// apply to its collection, body rendered from its variant's shape, filename in
|
|
3
3
|
// the framework's convention with a permanent kebab-case id inside. Born
|
|
4
4
|
// conforming; the prose is still the owner's.
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { blueprintId } from './blueprint.js';
|
|
8
|
-
import {
|
|
9
|
-
import { convert,
|
|
10
|
-
import {
|
|
8
|
+
import { defaultVariant, findCollection, findVariant, propertiesFor } from './framework.js';
|
|
9
|
+
import { convert, kebab } from './naming.js';
|
|
10
|
+
import { loadTemplate } from './template.js';
|
|
11
11
|
import { blankBlueprint } from './seed.js';
|
|
12
12
|
export class ScaffoldError extends Error {
|
|
13
13
|
constructor(message) {
|
|
@@ -38,24 +38,24 @@ export function scaffoldBlueprint(framework, existing, options) {
|
|
|
38
38
|
const known = framework.collections.map((candidate) => candidate.name).join(', ');
|
|
39
39
|
throw new ScaffoldError(`no collection '${options.collection}' in this framework (declared: ${known})`);
|
|
40
40
|
}
|
|
41
|
-
const
|
|
42
|
-
if (!
|
|
43
|
-
const known = collection.
|
|
44
|
-
throw new ScaffoldError(options.
|
|
45
|
-
? `collection '${collection.name}' declares no
|
|
46
|
-
: `no
|
|
41
|
+
const variant = options.variant === null ? defaultVariant(collection) : findVariant(collection, options.variant);
|
|
42
|
+
if (!variant) {
|
|
43
|
+
const known = collection.variants.map((candidate) => candidate.name).join(', ');
|
|
44
|
+
throw new ScaffoldError(options.variant === null
|
|
45
|
+
? `collection '${collection.name}' declares no variants`
|
|
46
|
+
: `no variant '${options.variant}' in '${collection.name}' (declared: ${known})`);
|
|
47
47
|
}
|
|
48
|
-
const shape =
|
|
48
|
+
const shape = loadTemplate(framework, variant);
|
|
49
49
|
if (!shape) {
|
|
50
|
-
throw new ScaffoldError(`
|
|
50
|
+
throw new ScaffoldError(`variant '${variant.name}' points at a template that does not exist: .eidos/${variant.template}`);
|
|
51
51
|
}
|
|
52
52
|
const title = options.title.trim();
|
|
53
53
|
if (title === '') {
|
|
54
54
|
throw new ScaffoldError('a blueprint needs a title');
|
|
55
55
|
}
|
|
56
56
|
const id = options.id ?? kebab(title);
|
|
57
|
-
if (
|
|
58
|
-
throw new ScaffoldError(`id '${id}' is
|
|
57
|
+
if (/\s/.test(id)) {
|
|
58
|
+
throw new ScaffoldError(`id '${id}' has whitespace in it; an id is one token, in any form`);
|
|
59
59
|
}
|
|
60
60
|
const clash = existing.find((blueprint) => blueprintId(blueprint) === id);
|
|
61
61
|
if (clash) {
|
|
@@ -67,7 +67,7 @@ export function scaffoldBlueprint(framework, existing, options) {
|
|
|
67
67
|
const declared = collection.grouping?.groups.find((candidate) => candidate.name.toLowerCase() === options.group?.toLowerCase() || kebab(candidate.name) === kebab(options.group ?? ''));
|
|
68
68
|
group = declared ? declared.name : convert(options.group, framework.naming);
|
|
69
69
|
if (!declared && collection.grouping && collection.grouping.groups.length > 0) {
|
|
70
|
-
notes.push(`group '${group}' is not declared under ${collection.grouping.label} in
|
|
70
|
+
notes.push(`group '${group}' is not declared under ${collection.grouping.label} in the framework document; add it there`);
|
|
71
71
|
}
|
|
72
72
|
if (!collection.grouping) {
|
|
73
73
|
notes.push(`collection '${collection.name}' declares no grouping; the sub-folder alone groups this blueprint`);
|
|
@@ -83,7 +83,7 @@ export function scaffoldBlueprint(framework, existing, options) {
|
|
|
83
83
|
id,
|
|
84
84
|
title,
|
|
85
85
|
summary: options.summary ?? '',
|
|
86
|
-
|
|
86
|
+
variant: variant.name,
|
|
87
87
|
status: 'Draft',
|
|
88
88
|
date_created: options.date,
|
|
89
89
|
date_modified: options.date,
|
|
@@ -95,11 +95,11 @@ export function scaffoldBlueprint(framework, existing, options) {
|
|
|
95
95
|
for (const [key, raw] of Object.entries(options.set)) {
|
|
96
96
|
const type = types.get(key);
|
|
97
97
|
if (type === undefined) {
|
|
98
|
-
notes.push(`property '${key}' is not in the
|
|
98
|
+
notes.push(`property '${key}' is not in the Properties table for ${collection.name}; written anyway`);
|
|
99
99
|
}
|
|
100
100
|
values[key] = coerce(type ?? 'Text', raw);
|
|
101
101
|
}
|
|
102
|
-
let content = blankBlueprint(framework, collection.name, values, shape.
|
|
102
|
+
let content = blankBlueprint(framework, collection.name, values, shape.text, title);
|
|
103
103
|
const extra = Object.keys(options.set).filter((key) => !types.has(key));
|
|
104
104
|
if (extra.length > 0) {
|
|
105
105
|
const lines = extra.map((key) => `${key}: ${String(values[key])}`).join('\n');
|
|
@@ -110,7 +110,7 @@ export function scaffoldBlueprint(framework, existing, options) {
|
|
|
110
110
|
rel: path.relative(framework.root, file).split(path.sep).join('/'),
|
|
111
111
|
content,
|
|
112
112
|
collection,
|
|
113
|
-
|
|
113
|
+
variant,
|
|
114
114
|
group,
|
|
115
115
|
id,
|
|
116
116
|
notes,
|