eidosmd 0.1.0 → 0.3.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-Cc3cNWHY.css +1 -0
- package/browser/dist/assets/index-DQgCQRa5.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 +10 -4
- package/dist/src/commands/configure.js +201 -0
- package/dist/src/commands/framework.js +37 -7
- package/dist/src/commands/index.js +4 -4
- package/dist/src/commands/init.js +5 -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/property.js +125 -0
- package/dist/src/commands/seeds.js +5 -5
- package/dist/src/commands/setup.js +131 -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 +732 -0
- package/dist/src/core/check.js +221 -70
- package/dist/src/core/convert.js +7 -6
- package/dist/src/core/edits.js +1381 -0
- package/dist/src/core/framework-markdown.js +119 -34
- package/dist/src/core/framework-model.js +62 -10
- package/dist/src/core/framework-structured.js +222 -47
- package/dist/src/core/framework.js +13 -13
- package/dist/src/core/frontmatter.js +61 -1
- package/dist/src/core/git.js +84 -0
- package/dist/src/core/index-leaf.js +2 -2
- package/dist/src/core/links.js +87 -0
- package/dist/src/core/markdown.js +16 -9
- package/dist/src/core/me.js +16 -8
- package/dist/src/core/migrate.js +319 -0
- package/dist/src/core/naming.js +1 -1
- package/dist/src/core/regions.js +117 -0
- package/dist/src/core/root.js +2 -2
- package/dist/src/core/scaffold.js +33 -27
- package/dist/src/core/seed.js +187 -71
- package/dist/src/core/server.js +1410 -53
- package/dist/src/core/settings.js +232 -0
- package/dist/src/core/store.js +315 -0
- package/dist/src/core/template.js +32 -0
- package/dist/src/core/versions.js +84 -0
- package/dist/src/output.js +4 -1
- package/dist/src/program.js +421 -41
- package/instructions/authoring.md +15 -12
- package/instructions/configuring.md +81 -34
- package/instructions/init-required.md +4 -4
- package/instructions/overview.md +21 -9
- package/instructions/validating.md +9 -6
- package/package.json +21 -12
- package/standard/EIDOS.md +142 -259
- package/standard/seeds/README.md +12 -16
- package/standard/seeds/book/Framework.yaml +61 -0
- package/standard/seeds/book/README.md +10 -5
- package/standard/seeds/book/_gitignore +9 -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 +61 -0
- package/standard/seeds/research/README.md +10 -5
- package/standard/seeds/research/_gitignore +9 -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 +62 -0
- package/standard/seeds/software/README.md +7 -6
- package/standard/seeds/software/_gitignore +9 -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
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
// Scaffolding one blueprint: frontmatter generated from the
|
|
2
|
-
// apply to its collection
|
|
3
|
-
//
|
|
1
|
+
// Scaffolding one blueprint: frontmatter generated from the required
|
|
2
|
+
// properties that apply to its collection plus any optional one given a
|
|
3
|
+
// value, body rendered from its variant's template, filename in the
|
|
4
|
+
// framework's convention with a permanent kebab-case id inside. Born
|
|
4
5
|
// conforming; the prose is still the owner's.
|
|
5
6
|
import { existsSync } from 'node:fs';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
import { blueprintId } from './blueprint.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { optionProblem } from './check.js';
|
|
10
|
+
import { defaultVariant, findCollection, findVariant, propertiesFor } from './framework.js';
|
|
11
|
+
import { convert, kebab } from './naming.js';
|
|
12
|
+
import { loadTemplate } from './template.js';
|
|
11
13
|
import { blankBlueprint } from './seed.js';
|
|
12
14
|
export class ScaffoldError extends Error {
|
|
13
15
|
constructor(message) {
|
|
@@ -15,7 +17,8 @@ export class ScaffoldError extends Error {
|
|
|
15
17
|
this.name = 'ScaffoldError';
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
|
-
|
|
20
|
+
// A value typed on a command line, as the property's declared type holds it.
|
|
21
|
+
export function coerce(type, raw) {
|
|
19
22
|
switch (type.toLowerCase()) {
|
|
20
23
|
case 'list':
|
|
21
24
|
return raw
|
|
@@ -38,24 +41,24 @@ export function scaffoldBlueprint(framework, existing, options) {
|
|
|
38
41
|
const known = framework.collections.map((candidate) => candidate.name).join(', ');
|
|
39
42
|
throw new ScaffoldError(`no collection '${options.collection}' in this framework (declared: ${known})`);
|
|
40
43
|
}
|
|
41
|
-
const
|
|
42
|
-
if (!
|
|
43
|
-
const known = collection.
|
|
44
|
-
throw new ScaffoldError(options.
|
|
45
|
-
? `collection '${collection.name}' declares no
|
|
46
|
-
: `no
|
|
44
|
+
const variant = options.variant === null ? defaultVariant(collection) : findVariant(collection, options.variant);
|
|
45
|
+
if (!variant) {
|
|
46
|
+
const known = collection.variants.map((candidate) => candidate.name).join(', ');
|
|
47
|
+
throw new ScaffoldError(options.variant === null
|
|
48
|
+
? `collection '${collection.name}' declares no variants`
|
|
49
|
+
: `no variant '${options.variant}' in '${collection.name}' (declared: ${known})`);
|
|
47
50
|
}
|
|
48
|
-
const shape =
|
|
51
|
+
const shape = loadTemplate(framework, variant);
|
|
49
52
|
if (!shape) {
|
|
50
|
-
throw new ScaffoldError(`
|
|
53
|
+
throw new ScaffoldError(`variant '${variant.name}' points at a template that does not exist: .eidos/${variant.template}`);
|
|
51
54
|
}
|
|
52
55
|
const title = options.title.trim();
|
|
53
56
|
if (title === '') {
|
|
54
57
|
throw new ScaffoldError('a blueprint needs a title');
|
|
55
58
|
}
|
|
56
59
|
const id = options.id ?? kebab(title);
|
|
57
|
-
if (
|
|
58
|
-
throw new ScaffoldError(`id '${id}' is
|
|
60
|
+
if (/\s/.test(id)) {
|
|
61
|
+
throw new ScaffoldError(`id '${id}' has whitespace in it; an id is one token, in any form`);
|
|
59
62
|
}
|
|
60
63
|
const clash = existing.find((blueprint) => blueprintId(blueprint) === id);
|
|
61
64
|
if (clash) {
|
|
@@ -67,7 +70,7 @@ export function scaffoldBlueprint(framework, existing, options) {
|
|
|
67
70
|
const declared = collection.grouping?.groups.find((candidate) => candidate.name.toLowerCase() === options.group?.toLowerCase() || kebab(candidate.name) === kebab(options.group ?? ''));
|
|
68
71
|
group = declared ? declared.name : convert(options.group, framework.naming);
|
|
69
72
|
if (!declared && collection.grouping && collection.grouping.groups.length > 0) {
|
|
70
|
-
notes.push(`group '${group}' is not declared under ${collection.grouping.label} in
|
|
73
|
+
notes.push(`group '${group}' is not declared under ${collection.grouping.label} in the framework document; add it there`);
|
|
71
74
|
}
|
|
72
75
|
if (!collection.grouping) {
|
|
73
76
|
notes.push(`collection '${collection.name}' declares no grouping; the sub-folder alone groups this blueprint`);
|
|
@@ -83,23 +86,26 @@ export function scaffoldBlueprint(framework, existing, options) {
|
|
|
83
86
|
id,
|
|
84
87
|
title,
|
|
85
88
|
summary: options.summary ?? '',
|
|
86
|
-
|
|
87
|
-
status: 'Draft',
|
|
89
|
+
variant: variant.name,
|
|
88
90
|
date_created: options.date,
|
|
89
91
|
date_modified: options.date,
|
|
90
92
|
};
|
|
91
93
|
if (group !== null && collection.grouping?.property) {
|
|
92
94
|
values[collection.grouping.property] = group;
|
|
93
95
|
}
|
|
94
|
-
const
|
|
96
|
+
const declared = new Map(propertiesFor(framework, collection.name).map((property) => [property.name, property]));
|
|
97
|
+
const types = new Map([...declared].map(([name, property]) => [name, property.type]));
|
|
95
98
|
for (const [key, raw] of Object.entries(options.set)) {
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
98
|
-
notes.push(`property '${key}' is not in the
|
|
99
|
+
const property = declared.get(key);
|
|
100
|
+
if (property === undefined) {
|
|
101
|
+
notes.push(`property '${key}' is not in the Properties table for ${collection.name}; written anyway`);
|
|
99
102
|
}
|
|
100
|
-
values[key] = coerce(type ?? 'Text', raw);
|
|
103
|
+
values[key] = coerce(property?.type ?? 'Text', raw);
|
|
104
|
+
const offList = property ? optionProblem(property, values[key]) : null;
|
|
105
|
+
if (offList)
|
|
106
|
+
notes.push(`'${key}: ${offList.off.join(', ')}' is not one of its options (${offList.options.join(', ')}); written anyway, and check reports it`);
|
|
101
107
|
}
|
|
102
|
-
let content = blankBlueprint(framework, collection.name, values, shape.
|
|
108
|
+
let content = blankBlueprint(framework, collection.name, values, shape.text, title);
|
|
103
109
|
const extra = Object.keys(options.set).filter((key) => !types.has(key));
|
|
104
110
|
if (extra.length > 0) {
|
|
105
111
|
const lines = extra.map((key) => `${key}: ${String(values[key])}`).join('\n');
|
|
@@ -110,7 +116,7 @@ export function scaffoldBlueprint(framework, existing, options) {
|
|
|
110
116
|
rel: path.relative(framework.root, file).split(path.sep).join('/'),
|
|
111
117
|
content,
|
|
112
118
|
collection,
|
|
113
|
-
|
|
119
|
+
variant,
|
|
114
120
|
group,
|
|
115
121
|
id,
|
|
116
122
|
notes,
|
package/dist/src/core/seed.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
// Installing a seed: copy a starting framework into a new root's
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Installing a seed: copy a starting framework into a new root's `.eidos/`,
|
|
2
|
+
// set the naming convention, the starting groups, and the folder names in
|
|
3
|
+
// its Framework.yaml (the seed's own comments kept), scaffold every declared
|
|
4
|
+
// folder (a collection's with its groups, an assets folder empty), and drop
|
|
5
|
+
// one blank blueprint per framing variant.
|
|
5
6
|
// It writes no prose: the README's one-liner, each group's description, and
|
|
6
7
|
// every scaffolded blueprint's summary and body stay the owner's.
|
|
7
8
|
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
8
9
|
import path from 'node:path';
|
|
9
10
|
import { formatFrontmatter } from './frontmatter.js';
|
|
10
11
|
import { parseFramework, propertiesFor } from './framework.js';
|
|
11
|
-
import { frameworkToDocument, serializeDocument } from './framework-structured.js';
|
|
12
12
|
import { buildIndexes } from './index-leaf.js';
|
|
13
13
|
import { listBlueprints } from './blueprint.js';
|
|
14
14
|
import { convert, kebab, titleize } from './naming.js';
|
|
15
|
-
import {
|
|
16
|
-
|
|
15
|
+
import { renderTemplate } from './template.js';
|
|
16
|
+
import { isCollection, TOOL_KEY } from './framework-model.js';
|
|
17
|
+
import { DEFAULT_CANVAS_HINTS } from './canvas.js';
|
|
18
|
+
import { isMap, isScalar, isSeq, parseDocument } from 'yaml';
|
|
17
19
|
const GITIGNORE_SHIPPED = '_gitignore';
|
|
18
20
|
export function listSeeds(seedsDir) {
|
|
19
21
|
if (!existsSync(seedsDir)) {
|
|
@@ -22,12 +24,12 @@ export function listSeeds(seedsDir) {
|
|
|
22
24
|
return readdirSync(seedsDir)
|
|
23
25
|
.sort()
|
|
24
26
|
.map((name) => path.join(seedsDir, name))
|
|
25
|
-
.filter((dir) => existsSync(path.join(dir, 'Framework.
|
|
27
|
+
.filter((dir) => existsSync(path.join(dir, 'Framework.yaml')))
|
|
26
28
|
.map((dir) => ({ name: path.basename(dir), dir, framework: readSeedFramework(dir) }));
|
|
27
29
|
}
|
|
28
30
|
function readSeedFramework(dir) {
|
|
29
|
-
const file = path.join(dir, 'Framework.
|
|
30
|
-
return parseFramework(readFileSync(file, 'utf8'), '
|
|
31
|
+
const file = path.join(dir, 'Framework.yaml');
|
|
32
|
+
return parseFramework(readFileSync(file, 'utf8'), 'yaml', dir, file);
|
|
31
33
|
}
|
|
32
34
|
export class InstallError extends Error {
|
|
33
35
|
constructor(message) {
|
|
@@ -73,105 +75,219 @@ class Writer {
|
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
const mapsIn = (value) => (isSeq(value) ? value.items.filter((item) => isMap(item)) : []);
|
|
79
|
+
const nameOf = (map) => String(map.get('name') ?? '');
|
|
80
|
+
// The `folders` list as the seed wrote it (`collections` in a seed older than 5.3.0).
|
|
81
|
+
const foldersOf = (document) => document.get('folders') ?? document.get('collections');
|
|
82
|
+
// The seed's folders and the properties scoped to its collections, renamed
|
|
83
|
+
// into the root's convention; every other key, and every comment, stays as
|
|
84
|
+
// the seed wrote it.
|
|
85
|
+
function renameFolders(document, renames) {
|
|
86
|
+
for (const folder of mapsIn(foldersOf(document))) {
|
|
87
|
+
const to = renames.get(nameOf(folder));
|
|
88
|
+
if (to !== undefined)
|
|
89
|
+
folder.set('name', to);
|
|
90
|
+
}
|
|
91
|
+
const properties = document.get('properties');
|
|
92
|
+
if (!isMap(properties))
|
|
93
|
+
return;
|
|
94
|
+
const blocks = [properties.get('custom'), ...(isMap(properties.get('tools')) ? properties.get('tools').items.map((pair) => pair.value) : [])];
|
|
95
|
+
for (const block of blocks) {
|
|
96
|
+
for (const property of mapsIn(block)) {
|
|
97
|
+
const applies = property.get('applies_to');
|
|
98
|
+
if (!isSeq(applies))
|
|
99
|
+
continue;
|
|
100
|
+
for (const item of applies.items) {
|
|
101
|
+
if (isScalar(item)) {
|
|
102
|
+
const to = renames.get(String(item.value));
|
|
103
|
+
if (to !== undefined)
|
|
104
|
+
item.value = to;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
79
108
|
}
|
|
80
|
-
return text.replace(/^---\n/, `---\nnaming: ${naming}\n`);
|
|
81
109
|
}
|
|
82
|
-
// The
|
|
83
|
-
//
|
|
84
|
-
function
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
110
|
+
// The canvas hint is the one field the CLI prescribes; a fresh root gets the
|
|
111
|
+
// legend as its default palette on the seed's own status and type.
|
|
112
|
+
function applyDefaultCanvasHints(document) {
|
|
113
|
+
const properties = document.get('properties');
|
|
114
|
+
if (!isMap(properties))
|
|
115
|
+
return;
|
|
116
|
+
for (const property of mapsIn(properties.get('custom'))) {
|
|
117
|
+
const hint = DEFAULT_CANVAS_HINTS[nameOf(property)];
|
|
118
|
+
if (!hint || property.has(TOOL_KEY))
|
|
119
|
+
continue;
|
|
120
|
+
// a hint is a map of maps; a seed writes its rows in flow style, which would put all of it on one line
|
|
121
|
+
property.flow = false;
|
|
122
|
+
property.set(TOOL_KEY, document.createNode({ canvas: hint }));
|
|
123
|
+
}
|
|
89
124
|
}
|
|
90
|
-
|
|
125
|
+
// Comments name the seed's collections too; a word-bounded rename follows them.
|
|
126
|
+
function renameInText(text, from, to) {
|
|
91
127
|
const escaped = from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
92
128
|
return text.replace(new RegExp(`(?<![\\w-])${escaped}(?!\\w)`, 'g'), to);
|
|
93
129
|
}
|
|
94
|
-
// A
|
|
95
|
-
|
|
130
|
+
// A value worth writing: anything but nothing, an empty string, or an empty list.
|
|
131
|
+
function hasValue(value) {
|
|
132
|
+
return value !== undefined && value !== null && value !== '' && !(Array.isArray(value) && value.length === 0);
|
|
133
|
+
}
|
|
134
|
+
// A blank blueprint: every required property that applies to the collection
|
|
135
|
+
// (with the value given, else blank, `[]` for a list), every optional one
|
|
136
|
+
// that was given a value, then the variant's template as the body. An
|
|
137
|
+
// optional property left out is not a gap, so the file carries nothing it
|
|
138
|
+
// doesn't need.
|
|
96
139
|
export function blankBlueprint(framework, collectionName, values, shapeTemplate, title) {
|
|
97
|
-
const entries =
|
|
140
|
+
const entries = [];
|
|
141
|
+
for (const property of propertiesFor(framework, collectionName)) {
|
|
98
142
|
const value = values[property.name];
|
|
99
|
-
if ((value
|
|
100
|
-
|
|
143
|
+
if (hasValue(value)) {
|
|
144
|
+
entries.push([property.name, value]);
|
|
101
145
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
146
|
+
else if (property.required) {
|
|
147
|
+
entries.push([property.name, property.type.toLowerCase() === 'list' ? [] : '']);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return formatFrontmatter(entries) + '\n' + renderTemplate(shapeTemplate, title);
|
|
105
151
|
}
|
|
106
152
|
export function installSeed(options) {
|
|
107
153
|
const seedDir = path.join(options.seedsDir, options.seed);
|
|
108
|
-
const frameworkSrc = path.join(seedDir, 'Framework.
|
|
154
|
+
const frameworkSrc = path.join(seedDir, 'Framework.yaml');
|
|
109
155
|
if (!existsSync(frameworkSrc)) {
|
|
110
|
-
throw new InstallError(`no seed '${options.seed}' under ${options.seedsDir} (needs a Framework.
|
|
156
|
+
throw new InstallError(`no seed '${options.seed}' under ${options.seedsDir} (needs a Framework.yaml)`);
|
|
111
157
|
}
|
|
112
158
|
const root = path.resolve(options.root);
|
|
113
|
-
if (existsSync(path.join(root, '
|
|
114
|
-
throw new InstallError(`${root}
|
|
159
|
+
if (existsSync(path.join(root, '.eidos'))) {
|
|
160
|
+
throw new InstallError(`${root}/.eidos already exists: that is a root; use migrate`);
|
|
115
161
|
}
|
|
116
|
-
|
|
117
|
-
const seedFramework = parseFramework(text, '
|
|
162
|
+
const text = readFileSync(frameworkSrc, 'utf8');
|
|
163
|
+
const seedFramework = parseFramework(text, 'yaml', seedDir, frameworkSrc);
|
|
164
|
+
const document = parseDocument(text);
|
|
118
165
|
if (seedFramework.collections.length === 0) {
|
|
119
166
|
throw new InstallError(`${frameworkSrc} declares no collections`);
|
|
120
167
|
}
|
|
121
168
|
const writer = new Writer(options.dryRun);
|
|
122
169
|
const naming = options.naming;
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
170
|
+
// The framing collection is the recommendation, not the standard's rule: an
|
|
171
|
+
// owner who keeps the framing elsewhere leaves it out, templates and all.
|
|
172
|
+
const leftOut = options.framing === false ? seedFramework.collections.filter((collection) => collection.framing) : [];
|
|
173
|
+
const installing = seedFramework.collections.filter((collection) => !leftOut.includes(collection));
|
|
174
|
+
if (installing.length === 0) {
|
|
175
|
+
throw new InstallError(`'${options.seed}' has only its framing collection; nothing would be left to install`);
|
|
176
|
+
}
|
|
177
|
+
// 1. the framework itself. The seed's README is the root's visible door, and so
|
|
178
|
+
// is any other top-level doc the seed declares (the software seed's
|
|
179
|
+
// Vocabulary), so those move to the root; roles/, templates/, me.md, and
|
|
180
|
+
// .gitignore travel as they are.
|
|
181
|
+
const topLevelDocs = seedFramework.topLevel.filter((doc) => /^\.\.\/[^/]+\.md$/.test(doc.path) && doc.path !== '../README.md').map((doc) => ({ title: doc.title, file: doc.path.slice(3) }));
|
|
182
|
+
writer.copyTree(seedDir, path.join(root, '.eidos'));
|
|
183
|
+
writer.remove(path.join(root, '.eidos', 'README.md'));
|
|
184
|
+
for (const doc of topLevelDocs) {
|
|
185
|
+
writer.remove(path.join(root, '.eidos', doc.file));
|
|
186
|
+
}
|
|
127
187
|
// npm never packs a file named .gitignore, so a seed ships it as _gitignore.
|
|
128
188
|
if (existsSync(path.join(seedDir, GITIGNORE_SHIPPED))) {
|
|
129
|
-
writer.rename(path.join(root, '
|
|
189
|
+
writer.rename(path.join(root, '.eidos', GITIGNORE_SHIPPED), path.join(root, '.eidos', '.gitignore'));
|
|
190
|
+
}
|
|
191
|
+
// 2. the framework document: naming, starting groups, and each collection in
|
|
192
|
+
// convention, edited in place so the seed's comments travel with it.
|
|
193
|
+
document.set('naming', naming);
|
|
194
|
+
for (const collection of leftOut) {
|
|
195
|
+
const folders = foldersOf(document);
|
|
196
|
+
if (isSeq(folders))
|
|
197
|
+
folders.items = folders.items.filter((item) => !(isMap(item) && nameOf(item) === collection.name));
|
|
198
|
+
for (const variant of collection.variants)
|
|
199
|
+
writer.remove(path.join(root, '.eidos', variant.template));
|
|
130
200
|
}
|
|
131
|
-
// 2. the Framework: naming, starting groups, and each collection in convention.
|
|
132
|
-
text = setNaming(text, naming);
|
|
133
201
|
const grouped = seedFramework.collections.find((collection) => collection.grouping && !collection.framing) ?? null;
|
|
134
202
|
if (options.groups.length > 0) {
|
|
135
203
|
if (!grouped || !grouped.grouping) {
|
|
136
204
|
throw new InstallError(`no collection in '${options.seed}' declares a grouping`);
|
|
137
205
|
}
|
|
138
|
-
|
|
206
|
+
const groupedNode = mapsIn(foldersOf(document)).find((collection) => nameOf(collection) === grouped.name);
|
|
207
|
+
const grouping = groupedNode?.get('grouping');
|
|
208
|
+
if (!isMap(grouping)) {
|
|
209
|
+
throw new InstallError(`${frameworkSrc}: ${grouped.name} declares a grouping the document does not carry`);
|
|
210
|
+
}
|
|
211
|
+
grouping.set('groups', document.createNode(options.groups.map((group) => ({ name: convert(group, naming), description: '' }))));
|
|
139
212
|
}
|
|
140
213
|
const readmeSrc = path.join(seedDir, 'README.md');
|
|
141
214
|
let readme = existsSync(readmeSrc) ? readFileSync(readmeSrc, 'utf8') : null;
|
|
142
215
|
if (readme !== null && options.product !== null) {
|
|
143
216
|
readme = readme.replace(/\{\{Product\}\}/g, options.product);
|
|
144
217
|
}
|
|
145
|
-
|
|
146
|
-
|
|
218
|
+
// A declared top-level doc is a file a person reads, so it takes the naming
|
|
219
|
+
// convention (README.md alone keeps its name), and the links to it in the
|
|
220
|
+
// framework and the README move with it.
|
|
221
|
+
const docs = [];
|
|
222
|
+
for (const doc of topLevelDocs) {
|
|
223
|
+
const source = path.join(seedDir, doc.file);
|
|
224
|
+
if (!existsSync(source)) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
let body = readFileSync(source, 'utf8');
|
|
228
|
+
if (options.product !== null) {
|
|
229
|
+
body = body.replace(/\{\{Product\}\}/g, options.product);
|
|
230
|
+
}
|
|
231
|
+
const target = `${convert(doc.title, naming)}.md`;
|
|
232
|
+
if (target !== doc.file) {
|
|
233
|
+
for (const entry of mapsIn(document.get('top_level'))) {
|
|
234
|
+
if (entry.get('path') === `../${doc.file}`)
|
|
235
|
+
entry.set('path', `../${target}`);
|
|
236
|
+
}
|
|
237
|
+
if (readme !== null) {
|
|
238
|
+
readme = readme.split(`(${doc.file})`).join(`(${target})`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
docs.push({ target, body });
|
|
147
242
|
}
|
|
243
|
+
// Every declared folder takes the naming convention, a collection and an
|
|
244
|
+
// assets folder alike; the files inside an assets folder keep their names.
|
|
148
245
|
const folders = new Map();
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
246
|
+
const renames = new Map();
|
|
247
|
+
if (readme !== null && leftOut.length > 0) {
|
|
248
|
+
readme = readme
|
|
249
|
+
.split('\n')
|
|
250
|
+
.filter((line) => !leftOut.some((collection) => line.includes(`[${collection.name}]`)))
|
|
251
|
+
.join('\n');
|
|
252
|
+
}
|
|
253
|
+
const plain = seedFramework.folders.filter((folder) => !isCollection(folder));
|
|
254
|
+
for (const declared of [...installing, ...plain]) {
|
|
255
|
+
// a seed writes a collection's name in Title Case and its assets folder in
|
|
256
|
+
// lowercase; the convention is applied to the title-cased word either way
|
|
257
|
+
const folder = convert(titleize(declared.name), naming);
|
|
258
|
+
folders.set(declared.name, folder);
|
|
259
|
+
if (folder !== declared.name) {
|
|
260
|
+
renames.set(declared.name, folder);
|
|
154
261
|
if (readme !== null) {
|
|
155
|
-
readme =
|
|
262
|
+
readme = renameInText(readme, declared.name, folder);
|
|
156
263
|
}
|
|
157
264
|
}
|
|
158
265
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
266
|
+
renameFolders(document, renames);
|
|
267
|
+
applyDefaultCanvasHints(document);
|
|
268
|
+
let serialized = document.toString({ lineWidth: 0 });
|
|
269
|
+
// comments name the collections too, so the word follows; an assets folder's
|
|
270
|
+
// name is also a type (`type: assets`), so only its entry is renamed
|
|
271
|
+
for (const [from, to] of renames) {
|
|
272
|
+
if (installing.some((collection) => collection.name === from))
|
|
273
|
+
serialized = renameInText(serialized, from, to);
|
|
274
|
+
}
|
|
275
|
+
const frameworkFile = path.join(root, '.eidos', 'Framework.yaml');
|
|
166
276
|
writer.write(frameworkFile, serialized);
|
|
167
277
|
const installed = parseFramework(serialized, 'yaml', root, frameworkFile);
|
|
168
278
|
if (readme !== null) {
|
|
169
279
|
writer.write(path.join(root, 'README.md'), readme);
|
|
170
280
|
}
|
|
171
|
-
|
|
281
|
+
for (const doc of docs) {
|
|
282
|
+
writer.write(path.join(root, doc.target), doc.body);
|
|
283
|
+
}
|
|
284
|
+
// 3. a folder per declared folder; blank blueprints for the framing collection.
|
|
172
285
|
const scaffolded = [];
|
|
173
286
|
const groupsInConvention = options.groups.map((group) => convert(group, naming));
|
|
174
|
-
for (const
|
|
287
|
+
for (const folder of plain) {
|
|
288
|
+
writer.mkdir(path.join(root, folders.get(folder.name) ?? folder.name));
|
|
289
|
+
}
|
|
290
|
+
for (const collection of installing) {
|
|
175
291
|
const folderName = folders.get(collection.name) ?? collection.name;
|
|
176
292
|
const folder = path.join(root, folderName);
|
|
177
293
|
writer.mkdir(folder);
|
|
@@ -183,22 +299,21 @@ export function installSeed(options) {
|
|
|
183
299
|
if (!collection.framing) {
|
|
184
300
|
continue;
|
|
185
301
|
}
|
|
186
|
-
for (const
|
|
187
|
-
const
|
|
188
|
-
if (!existsSync(
|
|
189
|
-
writer.actions.push(`skip ${collection.name}/${
|
|
302
|
+
for (const variant of collection.variants) {
|
|
303
|
+
const templateFile = path.join(seedDir, variant.template);
|
|
304
|
+
if (!existsSync(templateFile)) {
|
|
305
|
+
writer.actions.push(`skip ${collection.name}/${variant.name}: no template at ${variant.template}`);
|
|
190
306
|
continue;
|
|
191
307
|
}
|
|
192
|
-
const title = titleize(
|
|
308
|
+
const title = titleize(variant.name);
|
|
193
309
|
const values = {
|
|
194
|
-
id: kebab(
|
|
310
|
+
id: kebab(variant.name),
|
|
195
311
|
title,
|
|
196
|
-
|
|
197
|
-
status: 'Draft',
|
|
312
|
+
variant: variant.name,
|
|
198
313
|
date_created: options.date,
|
|
199
314
|
date_modified: options.date,
|
|
200
315
|
};
|
|
201
|
-
const body = blankBlueprint(installed, folderName, values, readFileSync(
|
|
316
|
+
const body = blankBlueprint(installed, folderName, values, readFileSync(templateFile, 'utf8'), title);
|
|
202
317
|
const file = path.join(folder, `${convert(title, naming)}.md`);
|
|
203
318
|
writer.write(file, body);
|
|
204
319
|
scaffolded.push(`${folderName}/${convert(title, naming)}.md`);
|
|
@@ -214,7 +329,8 @@ export function installSeed(options) {
|
|
|
214
329
|
eidosVersion: installed.eidosVersion,
|
|
215
330
|
naming,
|
|
216
331
|
frameworkFile,
|
|
217
|
-
collections:
|
|
332
|
+
collections: installing.map((collection) => folders.get(collection.name) ?? collection.name),
|
|
333
|
+
folders: plain.map((folder) => folders.get(folder.name) ?? folder.name),
|
|
218
334
|
groupedCollection: grouped ? (folders.get(grouped.name) ?? grouped.name) : null,
|
|
219
335
|
groups: groupsInConvention,
|
|
220
336
|
scaffolded,
|