eidosmd 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +133 -0
- package/NOTICE +15 -0
- package/README.md +146 -0
- package/browser/index.html +268 -0
- package/dist/src/cli.js +5 -0
- package/dist/src/commands/agents.js +47 -0
- package/dist/src/commands/browser.js +40 -0
- package/dist/src/commands/check.js +61 -0
- package/dist/src/commands/convert.js +30 -0
- package/dist/src/commands/framework.js +76 -0
- package/dist/src/commands/index.js +64 -0
- package/dist/src/commands/init.js +66 -0
- package/dist/src/commands/instructions.js +40 -0
- package/dist/src/commands/list.js +73 -0
- package/dist/src/commands/new.js +68 -0
- package/dist/src/commands/seeds.js +40 -0
- package/dist/src/commands/show.js +24 -0
- package/dist/src/commands/whoami.js +84 -0
- package/dist/src/context.js +35 -0
- package/dist/src/core/blueprint.js +129 -0
- package/dist/src/core/check.js +407 -0
- package/dist/src/core/convert.js +37 -0
- package/dist/src/core/framework-markdown.js +233 -0
- package/dist/src/core/framework-model.js +48 -0
- package/dist/src/core/framework-structured.js +334 -0
- package/dist/src/core/framework.js +56 -0
- package/dist/src/core/frontmatter.js +116 -0
- package/dist/src/core/index-leaf.js +83 -0
- package/dist/src/core/markdown.js +100 -0
- package/dist/src/core/me.js +89 -0
- package/dist/src/core/naming.js +46 -0
- package/dist/src/core/root.js +88 -0
- package/dist/src/core/scaffold.js +118 -0
- package/dist/src/core/seed.js +223 -0
- package/dist/src/core/server.js +275 -0
- package/dist/src/core/shape.js +26 -0
- package/dist/src/output.js +38 -0
- package/dist/src/paths.js +28 -0
- package/dist/src/program.js +242 -0
- package/instructions/authoring.md +50 -0
- package/instructions/configuring.md +65 -0
- package/instructions/init-required.md +32 -0
- package/instructions/overview.md +37 -0
- package/instructions/validating.md +33 -0
- package/package.json +53 -0
- package/standard/EIDOS.md +324 -0
- package/standard/seeds/README.md +25 -0
- package/standard/seeds/book/Framework.md +87 -0
- package/standard/seeds/book/README.md +22 -0
- package/standard/seeds/book/_gitignore +4 -0
- package/standard/seeds/book/me.md +18 -0
- package/standard/seeds/book/roles/README.md +12 -0
- package/standard/seeds/book/roles/collaborator.md +16 -0
- package/standard/seeds/book/roles/editor.md +16 -0
- package/standard/seeds/book/roles/framework-owner.md +16 -0
- package/standard/seeds/book/roles/reader.md +16 -0
- package/standard/seeds/book/shapes/chapter.full.md +45 -0
- package/standard/seeds/book/shapes/chapter.sketch.md +26 -0
- package/standard/seeds/book/shapes/frame.market.md +23 -0
- package/standard/seeds/book/shapes/frame.premise.md +24 -0
- package/standard/seeds/book/shapes/frame.reader.md +23 -0
- package/standard/seeds/book/shapes/frame.voice.md +24 -0
- package/standard/seeds/research/Framework.md +88 -0
- package/standard/seeds/research/README.md +23 -0
- package/standard/seeds/research/_gitignore +4 -0
- package/standard/seeds/research/me.md +18 -0
- package/standard/seeds/research/roles/README.md +12 -0
- package/standard/seeds/research/roles/framework-owner.md +16 -0
- package/standard/seeds/research/roles/researcher.md +16 -0
- package/standard/seeds/research/roles/reviewer.md +16 -0
- package/standard/seeds/research/roles/sponsor.md +16 -0
- package/standard/seeds/research/shapes/frame.ethics.md +23 -0
- package/standard/seeds/research/shapes/frame.method.md +24 -0
- package/standard/seeds/research/shapes/frame.prior-work.md +23 -0
- package/standard/seeds/research/shapes/frame.question.md +24 -0
- package/standard/seeds/research/shapes/investigation.full.md +49 -0
- package/standard/seeds/research/shapes/investigation.note.md +26 -0
- package/standard/seeds/software/Framework.md +88 -0
- package/standard/seeds/software/README.md +24 -0
- package/standard/seeds/software/_gitignore +4 -0
- package/standard/seeds/software/me.md +18 -0
- package/standard/seeds/software/roles/.gitkeep +0 -0
- package/standard/seeds/software/roles/README.md +13 -0
- package/standard/seeds/software/roles/designer.md +16 -0
- package/standard/seeds/software/roles/developer.md +16 -0
- package/standard/seeds/software/roles/framework-owner.md +16 -0
- package/standard/seeds/software/roles/project-manager.md +18 -0
- package/standard/seeds/software/roles/stakeholder.md +16 -0
- package/standard/seeds/software/shapes/.gitkeep +0 -0
- package/standard/seeds/software/shapes/frame.architecture.md +26 -0
- package/standard/seeds/software/shapes/frame.audience.md +26 -0
- package/standard/seeds/software/shapes/frame.criteria.md +27 -0
- package/standard/seeds/software/shapes/frame.market.md +31 -0
- package/standard/seeds/software/shapes/spec.full.md +67 -0
- package/standard/seeds/software/shapes/spec.micro.md +32 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// `eidos browser`: the root in a local web page, on this machine only.
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { loadRoot } from '../context.js';
|
|
4
|
+
import { EXIT_OK, print } from '../output.js';
|
|
5
|
+
export const DEFAULT_PORT = 6180;
|
|
6
|
+
function openInBrowser(url) {
|
|
7
|
+
const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
8
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
9
|
+
try {
|
|
10
|
+
spawn(command, args, { detached: true, stdio: 'ignore' }).unref();
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// a machine with no opener still gets the URL on the terminal
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function runBrowser(globals, options, cwd = process.cwd()) {
|
|
17
|
+
// Fail now, on the terminal, if there is no root, rather than on the first request.
|
|
18
|
+
const context = loadRoot(globals, cwd);
|
|
19
|
+
// Loaded here so the markdown renderer never costs the other commands their startup.
|
|
20
|
+
const { createServer, listen } = await import('../core/server.js');
|
|
21
|
+
const server = createServer({ cwd, root: context.root });
|
|
22
|
+
const port = await listen(server, options.port);
|
|
23
|
+
const url = `http://127.0.0.1:${port}/`;
|
|
24
|
+
print(`eidos browser: ${context.root}`, ` ${url} (this machine only; press Ctrl+C to stop)`);
|
|
25
|
+
if (port !== options.port) {
|
|
26
|
+
print(` port ${options.port} was busy`);
|
|
27
|
+
}
|
|
28
|
+
if (options.open) {
|
|
29
|
+
openInBrowser(url);
|
|
30
|
+
}
|
|
31
|
+
await new Promise((resolve) => {
|
|
32
|
+
const stop = () => {
|
|
33
|
+
server.close();
|
|
34
|
+
resolve();
|
|
35
|
+
};
|
|
36
|
+
process.once('SIGINT', stop);
|
|
37
|
+
process.once('SIGTERM', stop);
|
|
38
|
+
});
|
|
39
|
+
return EXIT_OK;
|
|
40
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// `eidos check`: validate the root, or the named blueprints, against the
|
|
2
|
+
// root's own framework. Findings print grouped by file; the exit code says
|
|
3
|
+
// whether anything was an error.
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { checkRoot } from '../core/check.js';
|
|
6
|
+
import { standardVersion } from '../paths.js';
|
|
7
|
+
import { CliError, EXIT_FINDINGS, EXIT_OK, print, printJson } from '../output.js';
|
|
8
|
+
function mark(finding) {
|
|
9
|
+
return finding.level === 'error' ? '✗' : '⚠';
|
|
10
|
+
}
|
|
11
|
+
export function runCheck(context, paths, options) {
|
|
12
|
+
let only;
|
|
13
|
+
if (paths.length > 0) {
|
|
14
|
+
only = new Set();
|
|
15
|
+
for (const given of paths) {
|
|
16
|
+
const resolved = path.resolve(context.cwd, given);
|
|
17
|
+
const match = context.blueprints.find((blueprint) => path.resolve(blueprint.path) === resolved);
|
|
18
|
+
if (!match) {
|
|
19
|
+
throw new CliError(`${given} is not a blueprint in ${context.root} (not in a declared collection, or an index.md)`);
|
|
20
|
+
}
|
|
21
|
+
only.add(match.path);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const report = checkRoot(context.framework, context.blueprints, { only, standardVersion: standardVersion() });
|
|
25
|
+
const failed = report.errors > 0 || (options.strict && report.warnings > 0);
|
|
26
|
+
if (options.json) {
|
|
27
|
+
printJson({
|
|
28
|
+
ok: !failed,
|
|
29
|
+
strict: options.strict,
|
|
30
|
+
root: report.root,
|
|
31
|
+
eidos_version: report.eidosVersion,
|
|
32
|
+
naming: report.naming,
|
|
33
|
+
blueprints: report.blueprints,
|
|
34
|
+
collections: report.collections,
|
|
35
|
+
errors: report.errors,
|
|
36
|
+
warnings: report.warnings,
|
|
37
|
+
findings: report.findings,
|
|
38
|
+
});
|
|
39
|
+
return failed ? EXIT_FINDINGS : EXIT_OK;
|
|
40
|
+
}
|
|
41
|
+
const scope = only ? `${report.blueprints} blueprint${report.blueprints === 1 ? '' : 's'}` : `${report.blueprints} blueprints in ${report.collections} collections`;
|
|
42
|
+
print(`${path.basename(report.root)} (Eidos ${report.eidosVersion ?? 'unversioned'} · ${report.naming}) ${scope}`);
|
|
43
|
+
let current;
|
|
44
|
+
const codeWidth = Math.max(0, ...report.findings.map((finding) => finding.code.length));
|
|
45
|
+
for (const finding of report.findings) {
|
|
46
|
+
if (finding.path !== current) {
|
|
47
|
+
current = finding.path;
|
|
48
|
+
print('', finding.path ?? '(root)');
|
|
49
|
+
}
|
|
50
|
+
print(` ${mark(finding)} ${finding.code.padEnd(codeWidth)} ${finding.message}`);
|
|
51
|
+
}
|
|
52
|
+
print('');
|
|
53
|
+
if (report.findings.length === 0) {
|
|
54
|
+
print('✓ no findings');
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const parts = [`${report.errors} error${report.errors === 1 ? '' : 's'}`, `${report.warnings} warning${report.warnings === 1 ? '' : 's'}`];
|
|
58
|
+
print(`${failed ? '✗' : '⚠'} ${parts.join(', ')}${options.strict && report.errors === 0 && report.warnings > 0 ? ' (--strict)' : ''}`);
|
|
59
|
+
}
|
|
60
|
+
return failed ? EXIT_FINDINGS : EXIT_OK;
|
|
61
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// `eidos convert`: move a markdown root to Framework.yaml, the form the CLI works in.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadRoot } from '../context.js';
|
|
4
|
+
import { convertToYaml } from '../core/convert.js';
|
|
5
|
+
import { EXIT_OK, print, printJson } from '../output.js';
|
|
6
|
+
export function runConvert(globals, options, cwd = process.cwd()) {
|
|
7
|
+
const context = loadRoot(globals, cwd, { allowMarkdown: true });
|
|
8
|
+
const display = (file) => path.relative(cwd, file) || file;
|
|
9
|
+
if (context.framework.format === 'yaml') {
|
|
10
|
+
if (options.json) {
|
|
11
|
+
printJson({ converted: false, framework_file: display(context.framework.file), reason: 'already YAML' });
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
print(`${display(context.framework.file)} is already the YAML form; nothing to convert`);
|
|
15
|
+
}
|
|
16
|
+
return EXIT_OK;
|
|
17
|
+
}
|
|
18
|
+
const result = convertToYaml(context.framework, options.dryRun);
|
|
19
|
+
if (options.json) {
|
|
20
|
+
printJson({ converted: !options.dryRun, dry_run: options.dryRun, framework_file: display(result.written), removed: result.removed.map(display), actions: result.actions.map(display) });
|
|
21
|
+
return EXIT_OK;
|
|
22
|
+
}
|
|
23
|
+
print(...result.actions.map(display));
|
|
24
|
+
print('', `${options.dryRun ? 'would convert' : 'converted'}: ${display(context.framework.file)} → ${display(result.written)}, index inside it`);
|
|
25
|
+
print(" left behind: the markdown form's prose (section intros, comments); each collection's description travelled. Keep anything that matters in the root's README.");
|
|
26
|
+
if (!options.dryRun) {
|
|
27
|
+
print(' next: eidos check');
|
|
28
|
+
}
|
|
29
|
+
return EXIT_OK;
|
|
30
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// `eidos framework`: what the root's framework document declares, as a human
|
|
2
|
+
// summary, as JSON an agent can read instead of parsing the file, or as the
|
|
3
|
+
// YAML document a scriptable root keeps on disk.
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { frameworkToDocument, serializeDocument } from '../core/framework-structured.js';
|
|
6
|
+
import { CliError, EXIT_OK, print, printJson } from '../output.js';
|
|
7
|
+
export function frameworkJson(framework) {
|
|
8
|
+
return {
|
|
9
|
+
root: framework.root,
|
|
10
|
+
file: path.relative(framework.root, framework.file).split(path.sep).join('/'),
|
|
11
|
+
format: framework.format,
|
|
12
|
+
...frameworkToDocument(framework),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function runFramework(context, options) {
|
|
16
|
+
const { framework } = context;
|
|
17
|
+
if (options.as !== undefined) {
|
|
18
|
+
if (options.as !== 'yaml') {
|
|
19
|
+
throw new CliError('--as takes yaml (the markdown form is authored, not generated)');
|
|
20
|
+
}
|
|
21
|
+
process.stdout.write(serializeDocument(frameworkToDocument(framework)));
|
|
22
|
+
return EXIT_OK;
|
|
23
|
+
}
|
|
24
|
+
if (options.json) {
|
|
25
|
+
printJson(frameworkJson(framework));
|
|
26
|
+
return EXIT_OK;
|
|
27
|
+
}
|
|
28
|
+
print(`${path.basename(framework.root)} (Eidos ${framework.eidosVersion ?? 'unversioned'} · ${framework.naming} · ${path.basename(framework.file)})`, framework.root);
|
|
29
|
+
if (framework.problems.length > 0) {
|
|
30
|
+
print('', 'Problems');
|
|
31
|
+
for (const problem of framework.problems) {
|
|
32
|
+
print(` ✗ ${problem}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (framework.topLevel.length > 0) {
|
|
36
|
+
print('', 'Top-level');
|
|
37
|
+
for (const doc of framework.topLevel) {
|
|
38
|
+
print(` ${doc.title} (${doc.path})${doc.description ? ` — ${doc.description}` : ''}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
print('', 'Collections');
|
|
42
|
+
for (const collection of framework.collections) {
|
|
43
|
+
const tags = [];
|
|
44
|
+
if (collection.framing) {
|
|
45
|
+
tags.push('framing');
|
|
46
|
+
}
|
|
47
|
+
if (collection.grouping) {
|
|
48
|
+
tags.push(`grouped by ${collection.grouping.label}${collection.grouping.property ? ` (\`${collection.grouping.property}\`)` : ''}`);
|
|
49
|
+
}
|
|
50
|
+
if (collection.canvas) {
|
|
51
|
+
tags.push(collection.canvas.mode === 'file' ? 'canvas: file' : `canvas: card${collection.canvas.section ? ` from ## ${collection.canvas.section}` : ''}`);
|
|
52
|
+
}
|
|
53
|
+
print(` ${collection.name}${tags.length > 0 ? ` (${tags.join(' · ')})` : ''}`);
|
|
54
|
+
if (collection.description) {
|
|
55
|
+
print(` ${collection.description}`);
|
|
56
|
+
}
|
|
57
|
+
const flavors = collection.flavors.map((flavor) => `${flavor.name}${flavor.isDefault ? ' (default)' : ''}`);
|
|
58
|
+
print(` flavors: ${flavors.length > 0 ? flavors.join(', ') : 'none declared'}`);
|
|
59
|
+
if (collection.grouping && collection.grouping.groups.length > 0) {
|
|
60
|
+
print(` groups: ${collection.grouping.groups.map((group) => group.name).join(', ')}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
print('', 'Schema');
|
|
64
|
+
print(` core: ${framework.schema.core.map((property) => property.name).join(', ')}`);
|
|
65
|
+
if (framework.schema.custom.length === 0) {
|
|
66
|
+
print(' custom: none');
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
print(' custom:');
|
|
70
|
+
for (const property of framework.schema.custom) {
|
|
71
|
+
const scope = property.appliesTo === 'all' ? 'all' : property.appliesTo.join(', ');
|
|
72
|
+
print(` ${property.name} (${property.type}, ${scope}) — ${property.meaning}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return EXIT_OK;
|
|
76
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// `eidos index`: regenerate each collection's index.md, or verify them with --check.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { buildIndexes } from '../core/index-leaf.js';
|
|
4
|
+
import { findCollection } from '../core/framework.js';
|
|
5
|
+
import { CliError, EXIT_FINDINGS, EXIT_OK, print, printError, printJson } from '../output.js';
|
|
6
|
+
export function runIndex(context, options) {
|
|
7
|
+
let collections;
|
|
8
|
+
if (options.collection.length > 0) {
|
|
9
|
+
collections = options.collection.map((name) => {
|
|
10
|
+
const found = findCollection(context.framework, name);
|
|
11
|
+
if (!found) {
|
|
12
|
+
throw new CliError(`collection '${name}' is not declared in Framework.md`);
|
|
13
|
+
}
|
|
14
|
+
return found;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
if (context.framework.collections.length === 0) {
|
|
18
|
+
throw new CliError('no collections declared in _eidos/Framework.md (## Collections)');
|
|
19
|
+
}
|
|
20
|
+
const results = buildIndexes(context.framework, context.blueprints, { collections, check: options.check });
|
|
21
|
+
const stale = results.some((result) => result.status === 'stale');
|
|
22
|
+
const missingSummary = results.flatMap((result) => result.missingSummary);
|
|
23
|
+
if (options.json) {
|
|
24
|
+
printJson({
|
|
25
|
+
ok: !stale,
|
|
26
|
+
check: options.check,
|
|
27
|
+
indexes: results.map((result) => ({
|
|
28
|
+
collection: result.collection.name,
|
|
29
|
+
path: path.relative(context.root, result.file).split(path.sep).join('/'),
|
|
30
|
+
status: result.status,
|
|
31
|
+
blueprints: result.count,
|
|
32
|
+
})),
|
|
33
|
+
missing_summary: missingSummary,
|
|
34
|
+
});
|
|
35
|
+
return stale ? EXIT_FINDINGS : EXIT_OK;
|
|
36
|
+
}
|
|
37
|
+
for (const result of results) {
|
|
38
|
+
const relFile = path.relative(context.root, result.file).split(path.sep).join('/');
|
|
39
|
+
const count = `${result.count} blueprint${result.count === 1 ? '' : 's'}`;
|
|
40
|
+
switch (result.status) {
|
|
41
|
+
case 'folder-missing':
|
|
42
|
+
printError(` ✗ ${result.collection.name}: folder not found (${path.dirname(result.file)})`);
|
|
43
|
+
break;
|
|
44
|
+
case 'stale':
|
|
45
|
+
print(` ✗ stale: ${relFile}`);
|
|
46
|
+
break;
|
|
47
|
+
case 'current':
|
|
48
|
+
print(` ✓ current: ${result.collection.name} (${count})`);
|
|
49
|
+
break;
|
|
50
|
+
case 'written':
|
|
51
|
+
print(` ✓ ${result.collection.name}: ${count} → ${relFile}`);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (missingSummary.length > 0) {
|
|
56
|
+
printError('\nblueprints needing a summary (write one with the owner):');
|
|
57
|
+
printError(missingSummary.map((file) => ` · ${file}`).join('\n'));
|
|
58
|
+
}
|
|
59
|
+
if (stale) {
|
|
60
|
+
printError('\n✗ indexes are stale; run: eidos index');
|
|
61
|
+
return EXIT_FINDINGS;
|
|
62
|
+
}
|
|
63
|
+
return EXIT_OK;
|
|
64
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// `eidos init`: create a root from a bundled seed.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { installSeed, InstallError, listSeeds } from '../core/seed.js';
|
|
4
|
+
import { isNaming, NAMINGS } from '../core/naming.js';
|
|
5
|
+
import { seedsDir } from '../paths.js';
|
|
6
|
+
import { CliError, EXIT_OK, print, printJson } from '../output.js';
|
|
7
|
+
export function runInit(rootArg, options, cwd = process.cwd()) {
|
|
8
|
+
if (!isNaming(options.naming)) {
|
|
9
|
+
throw new CliError(`--naming must be one of: ${NAMINGS.join(', ')}`);
|
|
10
|
+
}
|
|
11
|
+
const naming = options.naming;
|
|
12
|
+
const known = listSeeds(seedsDir).map((seed) => seed.name);
|
|
13
|
+
if (!known.includes(options.seed)) {
|
|
14
|
+
throw new CliError(`no seed '${options.seed}'; bundled seeds: ${known.join(', ')}`);
|
|
15
|
+
}
|
|
16
|
+
const root = path.resolve(cwd, rootArg ?? 'Blueprints');
|
|
17
|
+
let result;
|
|
18
|
+
try {
|
|
19
|
+
result = installSeed({
|
|
20
|
+
seedsDir,
|
|
21
|
+
seed: options.seed,
|
|
22
|
+
root,
|
|
23
|
+
naming,
|
|
24
|
+
groups: options.group,
|
|
25
|
+
product: options.product ?? null,
|
|
26
|
+
date: options.date,
|
|
27
|
+
dryRun: options.dryRun,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (cause) {
|
|
31
|
+
if (cause instanceof InstallError) {
|
|
32
|
+
throw new CliError(cause.message);
|
|
33
|
+
}
|
|
34
|
+
throw cause;
|
|
35
|
+
}
|
|
36
|
+
if (options.json) {
|
|
37
|
+
printJson({
|
|
38
|
+
root: result.root,
|
|
39
|
+
seed: result.seed,
|
|
40
|
+
eidos_version: result.eidosVersion,
|
|
41
|
+
naming: result.naming,
|
|
42
|
+
framework_file: result.frameworkFile,
|
|
43
|
+
collections: result.collections,
|
|
44
|
+
grouped_collection: result.groupedCollection,
|
|
45
|
+
groups: result.groups,
|
|
46
|
+
scaffolded: result.scaffolded,
|
|
47
|
+
actions: result.actions,
|
|
48
|
+
dry_run: options.dryRun,
|
|
49
|
+
});
|
|
50
|
+
return EXIT_OK;
|
|
51
|
+
}
|
|
52
|
+
const display = (text) => text.split(root).join(path.relative(cwd, root) || '.').split(seedsDir).join('<seeds>');
|
|
53
|
+
print(...result.actions.map(display));
|
|
54
|
+
const verb = options.dryRun ? 'would install' : 'installed';
|
|
55
|
+
print('', `${verb}: seed '${result.seed}' (Eidos ${result.eidosVersion ?? '?'}) → ${display(result.root)}, naming ${result.naming}, framework ${display(result.frameworkFile)}`);
|
|
56
|
+
let collections = ` collections: ${result.collections.join(', ')}`;
|
|
57
|
+
if (result.groups.length > 0 && result.groupedCollection) {
|
|
58
|
+
collections += `; groups under ${result.groupedCollection}: ${result.groups.join(', ')}`;
|
|
59
|
+
}
|
|
60
|
+
print(collections);
|
|
61
|
+
if (result.scaffolded.length > 0) {
|
|
62
|
+
print(` blank frames: ${result.scaffolded.join(', ')}`);
|
|
63
|
+
}
|
|
64
|
+
print(` still yours: the README's one-liner, each group's description in ${path.basename(result.frameworkFile)}, and every scaffolded blueprint's summary and body.`, ' next: eidos instructions overview');
|
|
65
|
+
return EXIT_OK;
|
|
66
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// `eidos instructions`: the workflow an agent follows in a root, one guide at
|
|
2
|
+
// a time, and `eidos standard`: the text of the standard this CLI carries.
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { instructionsDir, readStandard, standardVersion } from '../paths.js';
|
|
6
|
+
import { CliError, EXIT_OK, print } from '../output.js';
|
|
7
|
+
export const GUIDES = [
|
|
8
|
+
{ name: 'overview', summary: 'Required first read before answering any request in a root' },
|
|
9
|
+
{ name: 'authoring', summary: 'How to place, scaffold, fill, and check a blueprint with the owner' },
|
|
10
|
+
{ name: 'validating', summary: 'How to run a check, read its findings, and report a review' },
|
|
11
|
+
{ name: 'configuring', summary: 'How to add a collection, a flavor, or a property, and keep the Framework current' },
|
|
12
|
+
{ name: 'init-required', summary: 'How to create a root when there is none here' },
|
|
13
|
+
];
|
|
14
|
+
export function guideText(name) {
|
|
15
|
+
const file = path.join(instructionsDir, `${name}.md`);
|
|
16
|
+
if (!existsSync(file)) {
|
|
17
|
+
throw new CliError(`no guide '${name}'; guides: ${GUIDES.map((guide) => guide.name).join(', ')}`);
|
|
18
|
+
}
|
|
19
|
+
return readFileSync(file, 'utf8');
|
|
20
|
+
}
|
|
21
|
+
export function runInstructions(guide) {
|
|
22
|
+
if (guide !== undefined) {
|
|
23
|
+
process.stdout.write(guideText(guide));
|
|
24
|
+
return EXIT_OK;
|
|
25
|
+
}
|
|
26
|
+
print('Eidos instructions', '', 'Start here:', " 'eidos instructions overview' Required first read before answering any request", " 'eidos <command> --help' Show options, output, and examples", '', 'Guides:');
|
|
27
|
+
for (const entry of GUIDES) {
|
|
28
|
+
print(` ${entry.name}`, ` 'eidos instructions ${entry.name}'`, ` -> ${entry.summary}`);
|
|
29
|
+
}
|
|
30
|
+
print('', `The standard itself: 'eidos standard' (Eidos ${standardVersion()})`);
|
|
31
|
+
return EXIT_OK;
|
|
32
|
+
}
|
|
33
|
+
export function runStandard(options) {
|
|
34
|
+
if (options.version) {
|
|
35
|
+
print(standardVersion());
|
|
36
|
+
return EXIT_OK;
|
|
37
|
+
}
|
|
38
|
+
process.stdout.write(readStandard());
|
|
39
|
+
return EXIT_OK;
|
|
40
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// `eidos list`: the blueprints in the root, filterable by collection, group,
|
|
2
|
+
// flavor, or any property value.
|
|
3
|
+
import { blueprintId, blueprintTitle, propertyString, resolveFlavor } from '../core/blueprint.js';
|
|
4
|
+
import { findCollection } from '../core/framework.js';
|
|
5
|
+
import { CliError, EXIT_OK, print, printJson, table } from '../output.js';
|
|
6
|
+
export function blueprintJson(blueprint) {
|
|
7
|
+
const { flavor, declared } = resolveFlavor(blueprint);
|
|
8
|
+
return {
|
|
9
|
+
id: blueprintId(blueprint),
|
|
10
|
+
title: blueprintTitle(blueprint),
|
|
11
|
+
summary: propertyString(blueprint, 'summary'),
|
|
12
|
+
collection: blueprint.collection.name,
|
|
13
|
+
group: blueprint.group,
|
|
14
|
+
flavor: flavor?.name ?? declared,
|
|
15
|
+
path: blueprint.rel,
|
|
16
|
+
properties: blueprint.properties ?? {},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function matchesWhere(blueprint, clause) {
|
|
20
|
+
const eq = clause.indexOf('=');
|
|
21
|
+
if (eq === -1) {
|
|
22
|
+
throw new CliError(`--where expects key=value, got '${clause}'`);
|
|
23
|
+
}
|
|
24
|
+
const key = clause.slice(0, eq).trim();
|
|
25
|
+
const wanted = clause.slice(eq + 1).trim().toLowerCase();
|
|
26
|
+
const value = blueprint.properties?.[key];
|
|
27
|
+
if (value === undefined || value === null) {
|
|
28
|
+
return wanted === '';
|
|
29
|
+
}
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
return value.some((item) => String(item).toLowerCase() === wanted);
|
|
32
|
+
}
|
|
33
|
+
return String(value).toLowerCase() === wanted;
|
|
34
|
+
}
|
|
35
|
+
export function runList(context, collectionArg, options) {
|
|
36
|
+
let selected = context.blueprints;
|
|
37
|
+
if (collectionArg !== undefined) {
|
|
38
|
+
const collection = findCollection(context.framework, collectionArg);
|
|
39
|
+
if (!collection) {
|
|
40
|
+
const known = context.framework.collections.map((candidate) => candidate.name).join(', ');
|
|
41
|
+
throw new CliError(`no collection '${collectionArg}' (declared: ${known})`);
|
|
42
|
+
}
|
|
43
|
+
selected = selected.filter((blueprint) => blueprint.collection === collection);
|
|
44
|
+
}
|
|
45
|
+
if (options.group !== undefined) {
|
|
46
|
+
const wanted = options.group.toLowerCase();
|
|
47
|
+
selected = selected.filter((blueprint) => blueprint.group?.toLowerCase() === wanted);
|
|
48
|
+
}
|
|
49
|
+
if (options.flavor !== undefined) {
|
|
50
|
+
const wanted = options.flavor.toLowerCase();
|
|
51
|
+
selected = selected.filter((blueprint) => resolveFlavor(blueprint).flavor?.name.toLowerCase() === wanted);
|
|
52
|
+
}
|
|
53
|
+
for (const clause of options.where) {
|
|
54
|
+
selected = selected.filter((blueprint) => matchesWhere(blueprint, clause));
|
|
55
|
+
}
|
|
56
|
+
if (options.json) {
|
|
57
|
+
printJson(selected.map(blueprintJson));
|
|
58
|
+
return EXIT_OK;
|
|
59
|
+
}
|
|
60
|
+
if (selected.length === 0) {
|
|
61
|
+
print('no blueprints match');
|
|
62
|
+
return EXIT_OK;
|
|
63
|
+
}
|
|
64
|
+
const rows = selected.map((blueprint) => [
|
|
65
|
+
blueprintId(blueprint) ?? '(no id)',
|
|
66
|
+
blueprintTitle(blueprint),
|
|
67
|
+
blueprint.group === null ? blueprint.collection.name : `${blueprint.collection.name}/${blueprint.group}`,
|
|
68
|
+
resolveFlavor(blueprint).flavor?.name ?? '?',
|
|
69
|
+
]);
|
|
70
|
+
print(...table([['ID', 'TITLE', 'COLLECTION', 'FLAVOR'], ...rows]));
|
|
71
|
+
print('', `${selected.length} blueprint${selected.length === 1 ? '' : 's'}`);
|
|
72
|
+
return EXIT_OK;
|
|
73
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// `eidos new`: scaffold one blueprint, born conforming.
|
|
2
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { scaffoldBlueprint, ScaffoldError } from '../core/scaffold.js';
|
|
5
|
+
import { CliError, EXIT_OK, print, printJson } from '../output.js';
|
|
6
|
+
function parseSet(clauses) {
|
|
7
|
+
const out = {};
|
|
8
|
+
for (const clause of clauses) {
|
|
9
|
+
const eq = clause.indexOf('=');
|
|
10
|
+
if (eq === -1) {
|
|
11
|
+
throw new CliError(`--set expects key=value, got '${clause}'`);
|
|
12
|
+
}
|
|
13
|
+
out[clause.slice(0, eq).trim()] = clause.slice(eq + 1);
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
export function runNew(context, collection, titleWords, options) {
|
|
18
|
+
let result;
|
|
19
|
+
try {
|
|
20
|
+
result = scaffoldBlueprint(context.framework, context.blueprints, {
|
|
21
|
+
collection,
|
|
22
|
+
title: titleWords.join(' '),
|
|
23
|
+
flavor: options.flavor ?? null,
|
|
24
|
+
group: options.group ?? null,
|
|
25
|
+
id: options.id ?? null,
|
|
26
|
+
summary: options.summary ?? null,
|
|
27
|
+
set: parseSet(options.set),
|
|
28
|
+
date: options.date,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch (cause) {
|
|
32
|
+
if (cause instanceof ScaffoldError) {
|
|
33
|
+
throw new CliError(cause.message);
|
|
34
|
+
}
|
|
35
|
+
throw cause;
|
|
36
|
+
}
|
|
37
|
+
if (!options.dryRun) {
|
|
38
|
+
mkdirSync(path.dirname(result.path), { recursive: true });
|
|
39
|
+
writeFileSync(result.path, result.content, 'utf8');
|
|
40
|
+
}
|
|
41
|
+
if (options.json) {
|
|
42
|
+
printJson({
|
|
43
|
+
path: result.rel,
|
|
44
|
+
absolute_path: result.path,
|
|
45
|
+
id: result.id,
|
|
46
|
+
collection: result.collection.name,
|
|
47
|
+
group: result.group,
|
|
48
|
+
flavor: result.flavor.name,
|
|
49
|
+
dry_run: options.dryRun,
|
|
50
|
+
notes: result.notes,
|
|
51
|
+
content: options.dryRun ? result.content : undefined,
|
|
52
|
+
});
|
|
53
|
+
return EXIT_OK;
|
|
54
|
+
}
|
|
55
|
+
if (options.dryRun) {
|
|
56
|
+
print(`would write ${result.rel}:`, '', result.content);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
print(`✓ ${result.rel} (${result.collection.name}, ${result.flavor.kind}.${result.flavor.name}, id ${result.id})`);
|
|
60
|
+
}
|
|
61
|
+
for (const note of result.notes) {
|
|
62
|
+
print(` ! ${note}`);
|
|
63
|
+
}
|
|
64
|
+
if (!options.dryRun) {
|
|
65
|
+
print(' next: fill the summary and the body with the owner, then `eidos check` and `eidos index`');
|
|
66
|
+
}
|
|
67
|
+
return EXIT_OK;
|
|
68
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// `eidos seeds`: the bundled starting frameworks, read from their own Framework.md.
|
|
2
|
+
import { listSeeds } from '../core/seed.js';
|
|
3
|
+
import { seedsDir } from '../paths.js';
|
|
4
|
+
import { EXIT_OK, print, printJson } from '../output.js';
|
|
5
|
+
export function runSeeds(options) {
|
|
6
|
+
const seeds = listSeeds(seedsDir);
|
|
7
|
+
if (options.json) {
|
|
8
|
+
printJson(seeds.map((seed) => ({
|
|
9
|
+
name: seed.name,
|
|
10
|
+
eidos_version: seed.framework.eidosVersion,
|
|
11
|
+
collections: seed.framework.collections.map((collection) => ({
|
|
12
|
+
name: collection.name,
|
|
13
|
+
framing: collection.framing,
|
|
14
|
+
description: collection.description,
|
|
15
|
+
grouping: collection.grouping?.label ?? null,
|
|
16
|
+
flavors: collection.flavors.map((flavor) => ({ name: flavor.name, description: flavor.description, default: flavor.isDefault })),
|
|
17
|
+
})),
|
|
18
|
+
})));
|
|
19
|
+
return EXIT_OK;
|
|
20
|
+
}
|
|
21
|
+
for (const seed of seeds) {
|
|
22
|
+
print('', `${seed.name} (Eidos ${seed.framework.eidosVersion ?? '?'})`);
|
|
23
|
+
for (const collection of seed.framework.collections) {
|
|
24
|
+
let role = 'flat';
|
|
25
|
+
if (collection.framing) {
|
|
26
|
+
role = 'framing';
|
|
27
|
+
}
|
|
28
|
+
else if (collection.grouping) {
|
|
29
|
+
role = `grouped by ${collection.grouping.label.toLowerCase()}`;
|
|
30
|
+
}
|
|
31
|
+
print(` ${collection.name} — ${role}`, ` ${collection.description}`);
|
|
32
|
+
for (const flavor of collection.flavors) {
|
|
33
|
+
const marker = flavor.isDefault ? ' (default)' : '';
|
|
34
|
+
print(flavor.description ? ` · ${flavor.name} — ${flavor.description}${marker}` : ` · ${flavor.name}${marker}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
print('', 'install one: eidos init [root] --seed <name> [--naming <convention>] [--group <Group>]... [--product <Name>]');
|
|
39
|
+
return EXIT_OK;
|
|
40
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// `eidos show`: one blueprint, by id, filename, or path; the file as written,
|
|
2
|
+
// or with --json its properties and sections parsed out.
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { resolveBlueprint } from '../core/blueprint.js';
|
|
5
|
+
import { CliError, EXIT_OK, printJson } from '../output.js';
|
|
6
|
+
import { blueprintJson } from './list.js';
|
|
7
|
+
export function runShow(context, ref, options) {
|
|
8
|
+
const blueprint = resolveBlueprint(ref, context.blueprints, context.cwd);
|
|
9
|
+
if (!blueprint) {
|
|
10
|
+
throw new CliError(`no blueprint '${ref}' (by id, filename, or path); try: eidos list`);
|
|
11
|
+
}
|
|
12
|
+
if (options.json) {
|
|
13
|
+
printJson({
|
|
14
|
+
...blueprintJson(blueprint),
|
|
15
|
+
absolute_path: blueprint.path,
|
|
16
|
+
frontmatter_error: blueprint.frontmatterError,
|
|
17
|
+
sections: blueprint.sections.map((section) => ({ heading: section.heading.text, body: section.body })),
|
|
18
|
+
body: blueprint.body,
|
|
19
|
+
});
|
|
20
|
+
return EXIT_OK;
|
|
21
|
+
}
|
|
22
|
+
process.stdout.write(readFileSync(blueprint.path, 'utf8'));
|
|
23
|
+
return EXIT_OK;
|
|
24
|
+
}
|