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
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Rewriting the markdown links a move breaks. When a folder or a file moves,
|
|
2
|
+
// every `[text](path)` anywhere in the root whose target lands inside the
|
|
3
|
+
// moved path is rewritten to point at the new place, relative to where the
|
|
4
|
+
// file holding it ends up. It is a path substitution and nothing else: a
|
|
5
|
+
// link's text is untouched, a link in a fenced code block, in inline code,
|
|
6
|
+
// or inside a tool's region is left as it is, and a filename typed in prose
|
|
7
|
+
// without link syntax is not a link.
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { regionLines } from './regions.js';
|
|
10
|
+
export function movePath(absolute, moves) {
|
|
11
|
+
for (const move of moves) {
|
|
12
|
+
if (absolute === move.from)
|
|
13
|
+
return move.to;
|
|
14
|
+
if (absolute.startsWith(move.from + path.sep))
|
|
15
|
+
return path.join(move.to, path.relative(move.from, absolute));
|
|
16
|
+
}
|
|
17
|
+
return absolute;
|
|
18
|
+
}
|
|
19
|
+
const LINK = /\[([^\]]*)\]\(\s*(<[^>]*>|[^)\s]+)((?:\s+"[^"]*")?)\s*\)/g;
|
|
20
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
21
|
+
function isFileTarget(target) {
|
|
22
|
+
return target !== '' && !/^[a-z][a-z0-9+.-]*:/i.test(target) && !target.startsWith('#') && !target.startsWith('//');
|
|
23
|
+
}
|
|
24
|
+
// The spans of inline code on a line, so a link quoted in code is left alone.
|
|
25
|
+
function codeSpans(line) {
|
|
26
|
+
const out = [];
|
|
27
|
+
const pattern = /`+[^`]*`+/g;
|
|
28
|
+
let match = pattern.exec(line);
|
|
29
|
+
while (match !== null) {
|
|
30
|
+
out.push([match.index, match.index + match[0].length]);
|
|
31
|
+
match = pattern.exec(line);
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
// The text of a file with every link a move breaks rewritten. `before` is
|
|
36
|
+
// where the file is now and `after` where it will be once the moves are
|
|
37
|
+
// made, so a file inside a moved folder gets its outward links right too.
|
|
38
|
+
export function rewriteLinks(text, before, after, moves) {
|
|
39
|
+
const lines = text.replace(/\r\n?/g, '\n').split('\n');
|
|
40
|
+
const owned = regionLines(text);
|
|
41
|
+
let count = 0;
|
|
42
|
+
let fence = null;
|
|
43
|
+
const out = lines.map((line, index) => {
|
|
44
|
+
if (owned.has(index))
|
|
45
|
+
return line;
|
|
46
|
+
const fenced = FENCE.exec(line);
|
|
47
|
+
if (fence) {
|
|
48
|
+
if (fenced && fenced[1] && fenced[1][0] === fence.char && fenced[1].length >= fence.length && line.trim() === fenced[1])
|
|
49
|
+
fence = null;
|
|
50
|
+
return line;
|
|
51
|
+
}
|
|
52
|
+
if (fenced && fenced[1]) {
|
|
53
|
+
fence = { char: fenced[1][0] ?? '`', length: fenced[1].length };
|
|
54
|
+
return line;
|
|
55
|
+
}
|
|
56
|
+
const code = codeSpans(line);
|
|
57
|
+
return line.replace(LINK, (whole, label, rawTarget, title, offset) => {
|
|
58
|
+
if (code.some(([start, end]) => offset >= start && offset < end))
|
|
59
|
+
return whole;
|
|
60
|
+
const wrapped = rawTarget.startsWith('<') && rawTarget.endsWith('>');
|
|
61
|
+
const target = wrapped ? rawTarget.slice(1, -1) : rawTarget;
|
|
62
|
+
const hash = target.indexOf('#');
|
|
63
|
+
const file = hash === -1 ? target : target.slice(0, hash);
|
|
64
|
+
const anchor = hash === -1 ? '' : target.slice(hash);
|
|
65
|
+
if (!isFileTarget(file))
|
|
66
|
+
return whole;
|
|
67
|
+
let decoded = file;
|
|
68
|
+
try {
|
|
69
|
+
decoded = decodeURIComponent(file);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return whole;
|
|
73
|
+
}
|
|
74
|
+
const absolute = path.resolve(path.dirname(before), decoded);
|
|
75
|
+
const moved = movePath(absolute, moves);
|
|
76
|
+
let relative = path.relative(path.dirname(after), moved).split(path.sep).join('/');
|
|
77
|
+
if (relative === decoded.replace(/\\/g, '/'))
|
|
78
|
+
return whole;
|
|
79
|
+
if (relative === '')
|
|
80
|
+
relative = path.basename(moved);
|
|
81
|
+
count += 1;
|
|
82
|
+
const encoded = wrapped ? `<${relative}>` : /%20/.test(file) ? relative.replace(/ /g, '%20') : /\s/.test(relative) ? `<${relative}>` : relative;
|
|
83
|
+
return `[${label}](${encoded}${anchor}${title})`;
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
return { text: out.join('\n'), count };
|
|
87
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Small markdown readers shared by the framework, shape, and blueprint parsers:
|
|
2
2
|
// splitting frontmatter from body, and finding headings and sections outside
|
|
3
|
-
// code fences and
|
|
3
|
+
// code fences, HTML comments, and the regions a tool owns.
|
|
4
|
+
import { regionLines } from './regions.js';
|
|
4
5
|
export function normalizeNewlines(text) {
|
|
5
6
|
return text.replace(/\r\n?/g, '\n');
|
|
6
7
|
}
|
|
@@ -21,37 +22,43 @@ export function splitFrontmatter(text) {
|
|
|
21
22
|
}
|
|
22
23
|
return { frontmatter: null, body: lines.join('\n') };
|
|
23
24
|
}
|
|
24
|
-
// Every line with a flag saying whether it sits inside a fenced code block
|
|
25
|
-
//
|
|
25
|
+
// Every line with a flag saying whether it sits inside a fenced code block, an
|
|
26
|
+
// HTML comment, or a tool's region, so a `## heading` quoted in guidance, or
|
|
27
|
+
// one a tool wrote inside its region, is not read as one of the person's.
|
|
26
28
|
function visibleLines(text) {
|
|
27
29
|
const out = [];
|
|
30
|
+
const owned = regionLines(text);
|
|
28
31
|
let inFence = false;
|
|
29
32
|
let inComment = false;
|
|
30
|
-
|
|
33
|
+
normalizeNewlines(text).split('\n').forEach((line, index) => {
|
|
31
34
|
const trimmed = line.trim();
|
|
35
|
+
if (owned.has(index)) {
|
|
36
|
+
out.push({ line, visible: false });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
32
39
|
if (!inComment && /^(```|~~~)/.test(trimmed)) {
|
|
33
40
|
inFence = !inFence;
|
|
34
41
|
out.push({ line, visible: false });
|
|
35
|
-
|
|
42
|
+
return;
|
|
36
43
|
}
|
|
37
44
|
if (inFence) {
|
|
38
45
|
out.push({ line, visible: false });
|
|
39
|
-
|
|
46
|
+
return;
|
|
40
47
|
}
|
|
41
48
|
if (!inComment && trimmed.startsWith('<!--')) {
|
|
42
49
|
inComment = !trimmed.includes('-->');
|
|
43
50
|
out.push({ line, visible: false });
|
|
44
|
-
|
|
51
|
+
return;
|
|
45
52
|
}
|
|
46
53
|
if (inComment) {
|
|
47
54
|
if (trimmed.includes('-->')) {
|
|
48
55
|
inComment = false;
|
|
49
56
|
}
|
|
50
57
|
out.push({ line, visible: false });
|
|
51
|
-
|
|
58
|
+
return;
|
|
52
59
|
}
|
|
53
60
|
out.push({ line, visible: true });
|
|
54
|
-
}
|
|
61
|
+
});
|
|
55
62
|
return out;
|
|
56
63
|
}
|
|
57
64
|
export function headings(text) {
|
package/dist/src/core/me.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
// The actor:
|
|
2
|
-
//
|
|
1
|
+
// The actor: `.eidos/me.md`, personal and gitignored, naming a role from
|
|
2
|
+
// `.eidos/roles/` and calibrating it on three axes.
|
|
3
3
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { headings, section } from './markdown.js';
|
|
6
6
|
import { kebab } from './naming.js';
|
|
7
7
|
export function rolesDir(root) {
|
|
8
|
-
return path.join(root, '
|
|
8
|
+
return path.join(root, '.eidos', 'roles');
|
|
9
9
|
}
|
|
10
10
|
export function meFile(root) {
|
|
11
|
-
return path.join(root, '
|
|
11
|
+
return path.join(root, '.eidos', 'me.md');
|
|
12
12
|
}
|
|
13
13
|
export function listRoles(root) {
|
|
14
14
|
const dir = rolesDir(root);
|
|
@@ -27,6 +27,11 @@ export function listRoles(root) {
|
|
|
27
27
|
return { name: entry.replace(/\.md$/i, ''), file, title, summary };
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
|
+
// A role's file name is its kebab-case name, one level under roles/.
|
|
31
|
+
export const isRoleName = (name) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
|
|
32
|
+
export function roleFile(root, name) {
|
|
33
|
+
return path.join(rolesDir(root), `${name}.md`);
|
|
34
|
+
}
|
|
30
35
|
export function findRole(root, name) {
|
|
31
36
|
const wanted = kebab(name);
|
|
32
37
|
return listRoles(root).find((role) => role.name === wanted || kebab(role.title) === wanted) ?? null;
|
|
@@ -65,13 +70,16 @@ export function parseActor(text) {
|
|
|
65
70
|
actor.capacity = axis('Technical capacity');
|
|
66
71
|
return actor;
|
|
67
72
|
}
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
// `note` is anything the person adds under their role, in their own words.
|
|
74
|
+
export function renderActor(role, custom, axes, note = null) {
|
|
75
|
+
const lines = ['# Me', '', 'Personal and per-actor, gitignored, never shared. The agent reads it before acting.', ''];
|
|
70
76
|
if (role) {
|
|
71
|
-
lines.push(`## You are: ${role.title}`, '', `Role: [
|
|
77
|
+
lines.push(`## You are: ${role.title}`, '', `Role: [.eidos/roles/${role.name}.md](roles/${role.name}.md).`, '');
|
|
78
|
+
if (note)
|
|
79
|
+
lines.push(note, '');
|
|
72
80
|
}
|
|
73
81
|
else if (custom) {
|
|
74
|
-
lines.push(`## You are: ${custom}`, '', 'A custom role: describe how you want to be helped here.', '');
|
|
82
|
+
lines.push(`## You are: ${custom}`, '', note ?? 'A custom role: describe how you want to be helped here.', '');
|
|
75
83
|
}
|
|
76
84
|
else {
|
|
77
85
|
lines.push('## You are:', '', '<!-- Name your role, or pick one with `eidos whoami --role <role>`. -->', '');
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// `eidos migrate`: the mechanical hop from an older root to the standard this
|
|
2
|
+
// CLI carries. From 4.x, five names move on disk and nothing is dropped:
|
|
3
|
+
// `_eidos/` becomes `.eidos/`, `shapes/` becomes `templates/`, the framework
|
|
4
|
+
// document takes the 5.0.0 keys, every blueprint's `flavor` becomes
|
|
5
|
+
// `variant`, and `eidos_version` is set. A root that kept the markdown
|
|
6
|
+
// Framework.md gets Framework.yaml in its place, the index inside it. A
|
|
7
|
+
// template that opens with frontmatter loses the block; one named off its
|
|
8
|
+
// unit is noted, since only the owner can say which name is right. From
|
|
9
|
+
// 5.0.0 nothing on disk moves: the version is bumped and the standard's core
|
|
10
|
+
// block is rewritten with its required rows, every region carried as found.
|
|
11
|
+
// 5.2.0 named the one personal file a tool keeps, `local.yaml`, and the one
|
|
12
|
+
// line in `.eidos/.gitignore` that covers every tool's: the hop adds that
|
|
13
|
+
// line, and this CLI's own `.gitignore` beside its local file, which did the
|
|
14
|
+
// same job before, goes. 5.3.0 declares every folder at the root with a
|
|
15
|
+
// type: `collections` becomes `folders` and every entry gains
|
|
16
|
+
// `type: collection`; anything at the root the document does not declare is
|
|
17
|
+
// the owner's to declare or move, so it is reported, never touched.
|
|
18
|
+
import { existsSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { isMap, isScalar, isSeq, parseDocument } from 'yaml';
|
|
21
|
+
import { FRAMEWORK_DIR, FRAMEWORK_FILES, isCollection, STANDARD_CORE, unitOf } from './framework-model.js';
|
|
22
|
+
import { convertToYaml } from './convert.js';
|
|
23
|
+
import { annotateYaml, coreEntries, YAML_GUIDANCE } from './framework-structured.js';
|
|
24
|
+
import { undeclaredAtRoot } from './check.js';
|
|
25
|
+
import { parseFramework } from './framework.js';
|
|
26
|
+
import { parseVersions } from './framework-markdown.js';
|
|
27
|
+
import { stripFrontmatter } from './template.js';
|
|
28
|
+
import { LOCAL_IGNORE_PATTERN, rootIgnoresLocal, settingsDir } from './settings.js';
|
|
29
|
+
import { readVersions, writeVersions, versionsFile } from './versions.js';
|
|
30
|
+
import { standardVersion } from '../paths.js';
|
|
31
|
+
export const LEGACY_DIR = '_eidos';
|
|
32
|
+
// The comment the seeds write above the pattern, so a migrated root reads as a fresh one does.
|
|
33
|
+
const LOCAL_IGNORE_NOTE = "# Eidos: a tool's local.yaml is personal and never shared.\n# A tool that keeps a folder under plugins/ may keep its personal settings there, one machine's (a\n# viewer command, an editor, a key path); everything else in its folder is the root's. This one\n# line covers every tool, so no tool writes a .gitignore of its own.\n";
|
|
34
|
+
// The guidance this CLI wrote above `collections` before 5.3.0. A document
|
|
35
|
+
// carrying it word for word gets the `folders` guidance in its place; any
|
|
36
|
+
// other comment there is the owner's or the seed's and stays.
|
|
37
|
+
const COLLECTIONS_GUIDANCE = " The collections. Each declares its variants (one default; each a template file named <unit>.<variant>.md under templates/, the unit being the word for one of its blueprints) and its grouping.\n Add a group under `grouping.groups` with a one-line description; add a variant with a template file in templates/.";
|
|
38
|
+
export class MigrationError extends Error {
|
|
39
|
+
constructor(message) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = 'MigrationError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// A root for this command is a folder with `.eidos/` or the older `_eidos/`,
|
|
45
|
+
// found upward from `start` the way every other command finds one.
|
|
46
|
+
export function findMigratableRoot(start, explicit) {
|
|
47
|
+
const has = (dir) => existsSync(path.join(dir, FRAMEWORK_DIR)) || existsSync(path.join(dir, LEGACY_DIR));
|
|
48
|
+
if (explicit !== undefined) {
|
|
49
|
+
const resolved = path.resolve(start, explicit);
|
|
50
|
+
if (!has(resolved))
|
|
51
|
+
throw new MigrationError(`${resolved} is not a root: no ${FRAMEWORK_DIR}/ (or ${LEGACY_DIR}/) inside it`);
|
|
52
|
+
return resolved;
|
|
53
|
+
}
|
|
54
|
+
let current = path.resolve(start);
|
|
55
|
+
for (;;) {
|
|
56
|
+
if (has(current))
|
|
57
|
+
return current;
|
|
58
|
+
const parent = path.dirname(current);
|
|
59
|
+
if (parent === current)
|
|
60
|
+
throw new MigrationError(`no root found from ${start}: no ${FRAMEWORK_DIR}/ (or ${LEGACY_DIR}/) here or above`);
|
|
61
|
+
current = parent;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const rel = (root, file) => path.relative(root, file).split(path.sep).join('/');
|
|
65
|
+
function markdownFiles(dir) {
|
|
66
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory())
|
|
67
|
+
return [];
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const entry of readdirSync(dir).sort()) {
|
|
70
|
+
if (entry.startsWith('.'))
|
|
71
|
+
continue;
|
|
72
|
+
const full = path.join(dir, entry);
|
|
73
|
+
if (statSync(full).isDirectory())
|
|
74
|
+
out.push(...markdownFiles(full));
|
|
75
|
+
else if (/\.md$/i.test(entry))
|
|
76
|
+
out.push(full);
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
const keyOf = (pair) => (isScalar(pair.key) ? String(pair.key.value) : null);
|
|
81
|
+
const renameKey = (pair, to) => {
|
|
82
|
+
if (isScalar(pair.key))
|
|
83
|
+
pair.key.value = to;
|
|
84
|
+
};
|
|
85
|
+
// The YAML form, key by key, comments kept: `schema` → `properties`,
|
|
86
|
+
// `flavors` → `variants`, each variant's `shape` → `template` (with its path),
|
|
87
|
+
// `unit` gone, `collections` → `folders` with `type: collection` on every
|
|
88
|
+
// entry, the core block rewritten, the version set. A document that still
|
|
89
|
+
// had the 4.x keys gets this CLI's guidance comments in their place; one
|
|
90
|
+
// already on 5.x keeps every comment it had, the seed's included.
|
|
91
|
+
export function migrateYamlDocument(text, version) {
|
|
92
|
+
const document = parseDocument(text);
|
|
93
|
+
const top = document.contents;
|
|
94
|
+
if (!isMap(top))
|
|
95
|
+
return text;
|
|
96
|
+
// the root's versions left the framework document in 5.0.0; migrateRoot moves them
|
|
97
|
+
const legacy = top.items.some((pair) => keyOf(pair) === 'schema' || keyOf(pair) === 'versions');
|
|
98
|
+
top.items = top.items.filter((pair) => keyOf(pair) !== 'versions');
|
|
99
|
+
for (const pair of top.items) {
|
|
100
|
+
const key = keyOf(pair);
|
|
101
|
+
if (key === 'schema')
|
|
102
|
+
renameKey(pair, 'properties');
|
|
103
|
+
if (key === 'collections') {
|
|
104
|
+
renameKey(pair, 'folders');
|
|
105
|
+
if (isScalar(pair.key) && pair.key.commentBefore === COLLECTIONS_GUIDANCE)
|
|
106
|
+
pair.key.commentBefore = YAML_GUIDANCE['folders'] ?? null;
|
|
107
|
+
if (isSeq(pair.value)) {
|
|
108
|
+
for (const folder of pair.value.items) {
|
|
109
|
+
if (!isMap(folder) || folder.has('type'))
|
|
110
|
+
continue;
|
|
111
|
+
const map = folder;
|
|
112
|
+
// the type sits beside the name, the way the seeds write it
|
|
113
|
+
const at = map.items.findIndex((entry) => keyOf(entry) === 'name');
|
|
114
|
+
map.items.splice(at === -1 ? 0 : at + 1, 0, document.createPair('type', 'collection'));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// the standard's own block is migrate's to rewrite: its rows are this version's core
|
|
119
|
+
if ((key === 'schema' || key === 'properties') && isMap(pair.value)) {
|
|
120
|
+
for (const entry of pair.value.items) {
|
|
121
|
+
if (keyOf(entry) === 'core' && isSeq(entry.value) && entry.value.items.length > 0) {
|
|
122
|
+
// one row per line, the flow style the seeds write the block in
|
|
123
|
+
const rows = document.createNode(coreEntries(STANDARD_CORE));
|
|
124
|
+
if (isSeq(rows)) {
|
|
125
|
+
for (const row of rows.items)
|
|
126
|
+
if (isMap(row))
|
|
127
|
+
row.flow = true;
|
|
128
|
+
}
|
|
129
|
+
entry.value = rows;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (key === 'eidos_version' && isScalar(pair.value))
|
|
134
|
+
pair.value.value = version;
|
|
135
|
+
if (key === 'collections' && isSeq(pair.value)) {
|
|
136
|
+
for (const collection of pair.value.items) {
|
|
137
|
+
if (!isMap(collection))
|
|
138
|
+
continue;
|
|
139
|
+
const map = collection;
|
|
140
|
+
map.items = map.items.filter((entry) => keyOf(entry) !== 'unit');
|
|
141
|
+
for (const entry of map.items) {
|
|
142
|
+
if (keyOf(entry) === 'flavors')
|
|
143
|
+
renameKey(entry, 'variants');
|
|
144
|
+
if ((keyOf(entry) === 'variants' || keyOf(entry) === 'flavors') && isSeq(entry.value)) {
|
|
145
|
+
for (const variant of entry.value.items) {
|
|
146
|
+
if (!isMap(variant))
|
|
147
|
+
continue;
|
|
148
|
+
for (const field of variant.items) {
|
|
149
|
+
if (keyOf(field) === 'shape') {
|
|
150
|
+
renameKey(field, 'template');
|
|
151
|
+
if (isScalar(field.value))
|
|
152
|
+
field.value.value = String(field.value.value).replace(/^shapes\//, 'templates/');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (legacy)
|
|
162
|
+
annotateYaml(document);
|
|
163
|
+
return document.toString({ lineWidth: 0 });
|
|
164
|
+
}
|
|
165
|
+
// What a 4.x document recorded under `versions`, in either form, for the move
|
|
166
|
+
// to this CLI's own file.
|
|
167
|
+
export function legacyVersions(text, format) {
|
|
168
|
+
if (format === 'md')
|
|
169
|
+
return parseVersions(text);
|
|
170
|
+
const top = parseDocument(text).contents;
|
|
171
|
+
if (!isMap(top))
|
|
172
|
+
return [];
|
|
173
|
+
const out = [];
|
|
174
|
+
for (const pair of top.items) {
|
|
175
|
+
if (keyOf(pair) !== 'versions' || !isSeq(pair.value))
|
|
176
|
+
continue;
|
|
177
|
+
for (const row of pair.value.items) {
|
|
178
|
+
if (!isMap(row))
|
|
179
|
+
continue;
|
|
180
|
+
const version = String(row.get('version') ?? '').trim();
|
|
181
|
+
const commit = String(row.get('commit') ?? '').trim();
|
|
182
|
+
if (version === '' || commit === '')
|
|
183
|
+
continue;
|
|
184
|
+
const tag = String(row.get('tag') ?? '').trim();
|
|
185
|
+
out.push({ version, commit, tag: tag === '' ? null : tag });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
// A document already on this standard is left alone, except for a `versions`
|
|
191
|
+
// key it may still carry.
|
|
192
|
+
function withoutVersions(text) {
|
|
193
|
+
const document = parseDocument(text);
|
|
194
|
+
if (!isMap(document.contents) || !document.has('versions'))
|
|
195
|
+
return text;
|
|
196
|
+
document.delete('versions');
|
|
197
|
+
return document.toString({ lineWidth: 0 });
|
|
198
|
+
}
|
|
199
|
+
// The markdown form, line by line: the headings, the bullets, the links, the core row, the version.
|
|
200
|
+
export function migrateMarkdownDocument(text, version) {
|
|
201
|
+
return text
|
|
202
|
+
.replace(/^eidos_version:\s*\S+/m, `eidos_version: ${version}`)
|
|
203
|
+
.replace(/^## Schema\s*$/m, '## Properties')
|
|
204
|
+
.replace(/^### Eidos Core\s*$/m, '### Eidos')
|
|
205
|
+
.replace(/^### Custom Properties\s*$/m, '### Custom')
|
|
206
|
+
.replace(/^### (.+?) Properties\s*$/gm, '### $1')
|
|
207
|
+
.replace(/^(\s*)- \*\*Flavors:\*\*/gm, '$1- **Variants:**')
|
|
208
|
+
.replace(/^\s*- \*\*Unit:\*\*[^\n]*\n/gm, '')
|
|
209
|
+
.replace(/\]\(shapes\//g, '](templates/')
|
|
210
|
+
.replace(/^(\|\s*)flavor(\s*\|)/gm, '$1variant$2')
|
|
211
|
+
.replace(/_eidos\//g, '.eidos/');
|
|
212
|
+
}
|
|
213
|
+
export function migrateRoot(root, options) {
|
|
214
|
+
const actions = [];
|
|
215
|
+
const notes = [];
|
|
216
|
+
const version = standardVersion();
|
|
217
|
+
const act = (what, run) => {
|
|
218
|
+
actions.push(what);
|
|
219
|
+
if (!options.dryRun)
|
|
220
|
+
run();
|
|
221
|
+
};
|
|
222
|
+
const legacy = path.join(root, LEGACY_DIR);
|
|
223
|
+
const dir = path.join(root, FRAMEWORK_DIR);
|
|
224
|
+
if (existsSync(legacy) && !existsSync(dir))
|
|
225
|
+
act(`rename ${LEGACY_DIR}/ to ${FRAMEWORK_DIR}/`, () => renameSync(legacy, dir));
|
|
226
|
+
else if (!existsSync(dir))
|
|
227
|
+
throw new MigrationError(`${root} is not a root: no ${FRAMEWORK_DIR}/ (or ${LEGACY_DIR}/) inside it`);
|
|
228
|
+
// on a dry run the folder may still be the old one
|
|
229
|
+
const live = existsSync(dir) ? dir : legacy;
|
|
230
|
+
const shapes = path.join(live, 'shapes');
|
|
231
|
+
const templates = path.join(live, 'templates');
|
|
232
|
+
if (existsSync(shapes) && !existsSync(templates))
|
|
233
|
+
act(`rename ${FRAMEWORK_DIR}/shapes/ to ${FRAMEWORK_DIR}/templates/`, () => renameSync(shapes, templates));
|
|
234
|
+
const liveTemplates = existsSync(templates) ? templates : shapes;
|
|
235
|
+
const found = FRAMEWORK_FILES.map((entry) => ({ ...entry, file: path.join(live, entry.name) })).find((entry) => existsSync(entry.file));
|
|
236
|
+
if (!found)
|
|
237
|
+
throw new MigrationError(`no Framework.yaml or Framework.md under ${rel(root, live)}/`);
|
|
238
|
+
const text = readFileSync(found.file, 'utf8');
|
|
239
|
+
const from = /^eidos_version:\s*([^\s#]+)/m.exec(text)?.[1] ?? null;
|
|
240
|
+
// A YAML root already on this standard is left as it is, its comments included.
|
|
241
|
+
const next = found.format === 'yaml' ? (from === version ? withoutVersions(text) : migrateYamlDocument(text, version)) : migrateMarkdownDocument(text, version);
|
|
242
|
+
if (next !== text)
|
|
243
|
+
act(`rewrite ${FRAMEWORK_DIR}/${found.name} in the ${version} form`, () => writeFileSync(found.file, next, 'utf8'));
|
|
244
|
+
const recorded = legacyVersions(text, found.format);
|
|
245
|
+
if (recorded.length > 0) {
|
|
246
|
+
act(`move ${recorded.length} recorded version${recorded.length === 1 ? '' : 's'} to ${rel(root, versionsFile(root))}, this CLI's own record`, () => {
|
|
247
|
+
const kept = readVersions(root);
|
|
248
|
+
writeVersions(root, [...recorded.filter((entry) => !kept.some((held) => held.version === entry.version)), ...kept]);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
const ignore = path.join(root, '.gitignore');
|
|
252
|
+
if (existsSync(ignore)) {
|
|
253
|
+
const held = readFileSync(ignore, 'utf8');
|
|
254
|
+
if (held.includes(`${LEGACY_DIR}/`))
|
|
255
|
+
act(`point .gitignore at ${FRAMEWORK_DIR}/`, () => writeFileSync(ignore, held.replace(new RegExp(`${LEGACY_DIR}/`, 'g'), `${FRAMEWORK_DIR}/`), 'utf8'));
|
|
256
|
+
}
|
|
257
|
+
// 5.2.0: one line keeps every tool's personal file out of the repository,
|
|
258
|
+
// and this CLI's private .gitignore, which covered only its own, is retired.
|
|
259
|
+
const frameworkIgnore = path.join(live, '.gitignore');
|
|
260
|
+
if (!rootIgnoresLocal(root)) {
|
|
261
|
+
act(`add ${LOCAL_IGNORE_PATTERN} to ${FRAMEWORK_DIR}/.gitignore, the one line every tool's personal file needs`, () => {
|
|
262
|
+
const held = existsSync(frameworkIgnore) ? readFileSync(frameworkIgnore, 'utf8') : '# Eidos: me.md is personal and never shared.\nme.md\n';
|
|
263
|
+
writeFileSync(frameworkIgnore, `${held.replace(/\s*$/, '')}\n\n${LOCAL_IGNORE_NOTE}${LOCAL_IGNORE_PATTERN}\n`, 'utf8');
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
const ownIgnore = path.join(settingsDir(root), '.gitignore');
|
|
267
|
+
if (existsSync(ownIgnore) && readFileSync(ownIgnore, 'utf8').trim() === 'local.yaml') {
|
|
268
|
+
act(`remove ${rel(root, ownIgnore)}; the root's .gitignore covers it now`, () => rmSync(ownIgnore, { force: true }));
|
|
269
|
+
}
|
|
270
|
+
const framework = parseFramework(next, found.format, root, path.join(dir, found.name));
|
|
271
|
+
// The markdown form left the standard in 5.0.0: the same framework is written
|
|
272
|
+
// as data, and the markdown document and each collection's index.md go.
|
|
273
|
+
if (found.format === 'md') {
|
|
274
|
+
act(`write ${FRAMEWORK_DIR}/Framework.yaml from ${found.name}, the index inside it; remove ${found.name} and each collection's index.md`, () => {
|
|
275
|
+
convertToYaml(framework, false);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
// 5.3.0: everything at the root is declared, and only the owner can say
|
|
279
|
+
// what an undeclared entry is or whether a declared one with nothing behind
|
|
280
|
+
// it should stay; each is a note, never a move.
|
|
281
|
+
const undeclared = undeclaredAtRoot(framework);
|
|
282
|
+
// on a dry run the framework folder may still be the old one
|
|
283
|
+
for (const entry of undeclared.folders.filter((name) => name !== LEGACY_DIR))
|
|
284
|
+
notes.push(`${entry}/ is at the root and no folder declares it; declare it (eidos configure:folder add ${entry} --type assets|other, or configure:collection add) or move it`);
|
|
285
|
+
for (const entry of undeclared.files)
|
|
286
|
+
notes.push(`${entry} is at the root and top_level does not list it; declare it (eidos configure:doc add) or move it`);
|
|
287
|
+
for (const folder of framework.folders) {
|
|
288
|
+
if (!existsSync(path.join(root, folder.name)))
|
|
289
|
+
notes.push(`${folder.name}/ is declared as ${isCollection(folder) ? 'a collection' : `an ${folder.type} folder`} but is not there; create it or remove the entry`);
|
|
290
|
+
}
|
|
291
|
+
for (const doc of framework.topLevel) {
|
|
292
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(doc.path))
|
|
293
|
+
continue;
|
|
294
|
+
if (!existsSync(path.resolve(dir, doc.path.split('#')[0] ?? '')))
|
|
295
|
+
notes.push(`top_level lists ${doc.title} at ${doc.path}, which is not there; write it or remove the entry`);
|
|
296
|
+
}
|
|
297
|
+
for (const collection of framework.collections) {
|
|
298
|
+
for (const file of markdownFiles(path.join(root, collection.name))) {
|
|
299
|
+
const content = readFileSync(file, 'utf8');
|
|
300
|
+
const block = /^---\r?\n[\s\S]*?\r?\n---/.exec(content);
|
|
301
|
+
if (!block || !/^flavor:/m.test(block[0]))
|
|
302
|
+
continue;
|
|
303
|
+
act(`rename flavor to variant in ${rel(root, file)}`, () => writeFileSync(file, content.replace(block[0], block[0].replace(/^flavor:/m, 'variant:')), 'utf8'));
|
|
304
|
+
}
|
|
305
|
+
const unit = unitOf(collection);
|
|
306
|
+
for (const variant of collection.variants) {
|
|
307
|
+
const file = path.join(liveTemplates, path.basename(variant.template));
|
|
308
|
+
if (existsSync(file)) {
|
|
309
|
+
const body = readFileSync(file, 'utf8');
|
|
310
|
+
if (/^---[ \t]*\r?\n/.test(body))
|
|
311
|
+
act(`strip the frontmatter block from ${FRAMEWORK_DIR}/templates/${path.basename(file)}`, () => writeFileSync(file, stripFrontmatter(body), 'utf8'));
|
|
312
|
+
}
|
|
313
|
+
const expected = unit ? `${unit}.${variant.name}.md` : null;
|
|
314
|
+
if (expected && path.basename(variant.template) !== expected)
|
|
315
|
+
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`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return { root, from, to: version, actions, notes, dryRun: options.dryRun };
|
|
319
|
+
}
|
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(' ')
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Regions: the span inside a markdown file that a tool owns, fenced by two
|
|
2
|
+
// HTML comments carrying its name (Eidos 5.1.0). `<!-- <tool>:<region> <args> -->`
|
|
3
|
+
// opens one and `<!-- /<tool>:<region> -->` closes it, each alone on its line;
|
|
4
|
+
// what sits between is the tool's, rewritten wholesale by it and read by
|
|
5
|
+
// nothing else. The standard reads none of the contents: a check faults only
|
|
6
|
+
// an opener with no closer, a heading or a link inside is not the person's,
|
|
7
|
+
// and a renderer that does not know the tool shows the contents as they are.
|
|
8
|
+
// A tool's identifier and a region's name: lowercase letters, digits, and hyphens.
|
|
9
|
+
const NAME = '[a-z0-9][a-z0-9-]*';
|
|
10
|
+
// Both closers HTML allows, `-->` and `--!>`, end a marker.
|
|
11
|
+
const OPENER = new RegExp(`^<!--\\s*(${NAME}):(${NAME})(?:\\s+(.*?))?\\s*--!?>$`);
|
|
12
|
+
const CLOSER = new RegExp(`^<!--\\s*/(${NAME}):(${NAME})\\s*--!?>$`);
|
|
13
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
14
|
+
function parseArgs(text) {
|
|
15
|
+
const args = {};
|
|
16
|
+
for (const token of (text ?? '').split(/\s+/)) {
|
|
17
|
+
const eq = token.indexOf('=');
|
|
18
|
+
if (eq > 0)
|
|
19
|
+
args[token.slice(0, eq)] = token.slice(eq + 1);
|
|
20
|
+
}
|
|
21
|
+
return args;
|
|
22
|
+
}
|
|
23
|
+
// Every region in the text, in order. A marker inside a fenced code block
|
|
24
|
+
// (three or more backticks or tildes, closed by the same) is text; a region
|
|
25
|
+
// closes at the first closer naming its tool and region, and the lines
|
|
26
|
+
// between are not read for markers, so another tool's marker there is content.
|
|
27
|
+
export function parseRegions(text) {
|
|
28
|
+
const lines = text.replace(/\r\n?/g, '\n').split('\n');
|
|
29
|
+
const out = [];
|
|
30
|
+
let fence = null;
|
|
31
|
+
let index = 0;
|
|
32
|
+
while (index < lines.length) {
|
|
33
|
+
const line = (lines[index] ?? '').trim();
|
|
34
|
+
if (fence) {
|
|
35
|
+
const closing = FENCE.exec(lines[index] ?? '');
|
|
36
|
+
if (closing && closing[1] && closing[1][0] === fence.char && closing[1].length >= fence.length && (lines[index] ?? '').trim() === closing[1])
|
|
37
|
+
fence = null;
|
|
38
|
+
index += 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const opening = FENCE.exec(lines[index] ?? '');
|
|
42
|
+
if (opening && opening[1]) {
|
|
43
|
+
fence = { char: opening[1][0] ?? '`', length: opening[1].length };
|
|
44
|
+
index += 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const opener = OPENER.exec(line);
|
|
48
|
+
if (!opener || !opener[1] || !opener[2]) {
|
|
49
|
+
index += 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const region = { tool: opener[1], region: opener[2], args: parseArgs(opener[3]), open: index, close: null, contents: '' };
|
|
53
|
+
let end = index + 1;
|
|
54
|
+
while (end < lines.length) {
|
|
55
|
+
const closer = CLOSER.exec((lines[end] ?? '').trim());
|
|
56
|
+
if (closer && closer[1] === region.tool && closer[2] === region.region) {
|
|
57
|
+
region.close = end;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
end += 1;
|
|
61
|
+
}
|
|
62
|
+
region.contents = lines.slice(index + 1, region.close ?? lines.length).join('\n');
|
|
63
|
+
out.push(region);
|
|
64
|
+
index = (region.close ?? lines.length) + 1;
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
// The zero-based lines every region occupies, markers included; an unclosed
|
|
69
|
+
// one runs to the end of the text.
|
|
70
|
+
export function regionLines(text) {
|
|
71
|
+
const out = new Set();
|
|
72
|
+
const total = text.replace(/\r\n?/g, '\n').split('\n').length;
|
|
73
|
+
for (const region of parseRegions(text)) {
|
|
74
|
+
for (let line = region.open; line <= (region.close ?? total - 1); line += 1)
|
|
75
|
+
out.add(line);
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
// The text with every region's lines blanked, line count kept, so what the
|
|
80
|
+
// standard does read (headings, links, prose) is only the person's.
|
|
81
|
+
export function stripRegions(text) {
|
|
82
|
+
const inside = regionLines(text);
|
|
83
|
+
if (inside.size === 0)
|
|
84
|
+
return text;
|
|
85
|
+
return text
|
|
86
|
+
.replace(/\r\n?/g, '\n')
|
|
87
|
+
.split('\n')
|
|
88
|
+
.map((line, index) => (inside.has(index) ? '' : line))
|
|
89
|
+
.join('\n');
|
|
90
|
+
}
|
|
91
|
+
function escapeHtml(text) {
|
|
92
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
93
|
+
}
|
|
94
|
+
// The text with each region replaced by one HTML block that shows its
|
|
95
|
+
// contents as they are: a `<pre>` carrying the tool and region name, which a
|
|
96
|
+
// markdown renderer passes through whole (an HTML block that opens with
|
|
97
|
+
// `<pre` runs to `</pre>`, blank lines included), so nothing inside is
|
|
98
|
+
// rendered as markdown. A renderer that knows the tool draws its own view.
|
|
99
|
+
export function regionsAsHtml(text) {
|
|
100
|
+
const regions = parseRegions(text);
|
|
101
|
+
if (regions.length === 0)
|
|
102
|
+
return text;
|
|
103
|
+
const lines = text.replace(/\r\n?/g, '\n').split('\n');
|
|
104
|
+
const out = [];
|
|
105
|
+
let index = 0;
|
|
106
|
+
for (const region of regions) {
|
|
107
|
+
out.push(...lines.slice(index, region.open));
|
|
108
|
+
const args = Object.entries(region.args)
|
|
109
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
110
|
+
.join(' ');
|
|
111
|
+
const attributes = [`data-tool="${escapeHtml(region.tool)}"`, `data-region="${escapeHtml(region.region)}"`, args !== '' ? `data-args="${escapeHtml(args)}"` : '', region.close === null ? 'data-unclosed=""' : ''].filter((attribute) => attribute !== '');
|
|
112
|
+
out.push('', `<pre class="region" ${attributes.join(' ')}>${escapeHtml(region.contents)}</pre>`, '');
|
|
113
|
+
index = (region.close ?? lines.length) + 1;
|
|
114
|
+
}
|
|
115
|
+
out.push(...lines.slice(index));
|
|
116
|
+
return out.join('\n');
|
|
117
|
+
}
|
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
|
}
|