eidosmd 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +50 -29
  2. package/browser/dist/assets/index-C2NMN_D4.css +1 -0
  3. package/browser/dist/assets/index-C65k1ihb.js +46 -0
  4. package/browser/dist/favicon.svg +5 -0
  5. package/browser/dist/index.html +15 -0
  6. package/browser/dist/mark.svg +4 -0
  7. package/dist/src/cli.js +7 -0
  8. package/dist/src/commands/agents.js +1 -1
  9. package/dist/src/commands/canvas.js +77 -0
  10. package/dist/src/commands/check.js +1 -1
  11. package/dist/src/commands/framework.js +24 -5
  12. package/dist/src/commands/index.js +4 -4
  13. package/dist/src/commands/init.js +1 -0
  14. package/dist/src/commands/instructions.js +1 -1
  15. package/dist/src/commands/list.js +8 -8
  16. package/dist/src/commands/migrate.js +32 -0
  17. package/dist/src/commands/new.js +3 -3
  18. package/dist/src/commands/seeds.js +5 -5
  19. package/dist/src/commands/setup.js +119 -0
  20. package/dist/src/commands/version.js +44 -0
  21. package/dist/src/commands/whoami.js +4 -4
  22. package/dist/src/context.js +5 -5
  23. package/dist/src/core/blueprint.js +16 -11
  24. package/dist/src/core/canvas-schema.js +148 -0
  25. package/dist/src/core/canvas.js +722 -0
  26. package/dist/src/core/check.js +127 -51
  27. package/dist/src/core/convert.js +7 -6
  28. package/dist/src/core/framework-markdown.js +113 -34
  29. package/dist/src/core/framework-model.js +27 -10
  30. package/dist/src/core/framework-structured.js +132 -33
  31. package/dist/src/core/framework.js +13 -13
  32. package/dist/src/core/git.js +59 -0
  33. package/dist/src/core/index-leaf.js +2 -2
  34. package/dist/src/core/me.js +16 -8
  35. package/dist/src/core/migrate.js +243 -0
  36. package/dist/src/core/naming.js +1 -1
  37. package/dist/src/core/root.js +2 -2
  38. package/dist/src/core/scaffold.js +19 -19
  39. package/dist/src/core/seed.js +151 -60
  40. package/dist/src/core/server.js +1236 -52
  41. package/dist/src/core/settings.js +181 -0
  42. package/dist/src/core/store.js +259 -0
  43. package/dist/src/core/template.js +32 -0
  44. package/dist/src/core/versions.js +79 -0
  45. package/dist/src/output.js +4 -1
  46. package/dist/src/program.js +134 -39
  47. package/instructions/authoring.md +13 -12
  48. package/instructions/configuring.md +65 -34
  49. package/instructions/init-required.md +4 -4
  50. package/instructions/overview.md +17 -7
  51. package/instructions/validating.md +5 -4
  52. package/package.json +21 -12
  53. package/standard/EIDOS.md +129 -188
  54. package/standard/seeds/README.md +6 -6
  55. package/standard/seeds/book/Framework.yaml +81 -0
  56. package/standard/seeds/book/README.md +9 -5
  57. package/standard/seeds/book/_gitignore +3 -3
  58. package/standard/seeds/book/me.md +1 -1
  59. package/standard/seeds/book/roles/README.md +3 -3
  60. package/standard/seeds/book/roles/framework-owner.md +2 -2
  61. package/standard/seeds/book/{shapes → templates}/chapter.full.md +0 -8
  62. package/standard/seeds/book/{shapes → templates}/chapter.sketch.md +0 -7
  63. package/standard/seeds/book/{shapes → templates}/frame.market.md +0 -6
  64. package/standard/seeds/book/templates/frame.premise.md +17 -0
  65. package/standard/seeds/book/{shapes → templates}/frame.reader.md +0 -6
  66. package/standard/seeds/book/{shapes → templates}/frame.voice.md +0 -7
  67. package/standard/seeds/research/Framework.yaml +81 -0
  68. package/standard/seeds/research/README.md +9 -5
  69. package/standard/seeds/research/_gitignore +3 -3
  70. package/standard/seeds/research/me.md +1 -1
  71. package/standard/seeds/research/roles/README.md +3 -3
  72. package/standard/seeds/research/roles/framework-owner.md +2 -2
  73. package/standard/seeds/research/{shapes → templates}/frame.ethics.md +0 -6
  74. package/standard/seeds/research/{shapes → templates}/frame.method.md +0 -7
  75. package/standard/seeds/research/{shapes → templates}/frame.prior-work.md +0 -6
  76. package/standard/seeds/research/{shapes → templates}/frame.question.md +0 -7
  77. package/standard/seeds/research/{shapes → templates}/investigation.full.md +0 -8
  78. package/standard/seeds/research/{shapes → templates}/investigation.note.md +0 -7
  79. package/standard/seeds/software/Framework.yaml +82 -0
  80. package/standard/seeds/software/README.md +5 -5
  81. package/standard/seeds/software/_gitignore +3 -3
  82. package/standard/seeds/software/me.md +1 -1
  83. package/standard/seeds/software/roles/README.md +3 -3
  84. package/standard/seeds/software/roles/framework-owner.md +2 -2
  85. package/standard/seeds/software/roles/project-manager.md +2 -2
  86. package/standard/seeds/software/roles/stakeholder.md +1 -1
  87. package/standard/seeds/software/{shapes → templates}/frame.architecture.md +0 -7
  88. package/standard/seeds/software/{shapes → templates}/frame.audience.md +1 -8
  89. package/standard/seeds/software/{shapes → templates}/frame.criteria.md +0 -8
  90. package/standard/seeds/software/{shapes → templates}/frame.market.md +0 -8
  91. package/standard/seeds/software/{shapes → templates}/spec.full.md +0 -8
  92. package/standard/seeds/software/{shapes → templates}/spec.micro.md +0 -9
  93. package/browser/index.html +0 -268
  94. package/dist/src/commands/convert.js +0 -30
  95. package/dist/src/core/shape.js +0 -26
  96. package/standard/seeds/book/Framework.md +0 -87
  97. package/standard/seeds/book/shapes/frame.premise.md +0 -24
  98. package/standard/seeds/research/Framework.md +0 -88
  99. package/standard/seeds/software/Framework.md +0 -88
  100. /package/standard/seeds/software/{shapes → templates}/.gitkeep +0 -0
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Eidos">
2
+ <rect width="32" height="32" rx="7" fill="#000000"/>
3
+ <rect x="5.1" y="5.1" width="21.8" height="21.8" rx="5" fill="none" stroke="#e08a63" stroke-width="1.8" opacity="0.5"/>
4
+ <path d="M16 9.4 21.9 16 16 22.6 10.1 16 16 9.4Z" fill="#e08a63"/>
5
+ </svg>
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="color-scheme" content="dark light" />
7
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
8
+ <title>Eidos</title>
9
+ <script type="module" crossorigin src="/assets/index-C65k1ihb.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-C2NMN_D4.css">
11
+ </head>
12
+ <body>
13
+ <div id="app"></div>
14
+ </body>
15
+ </html>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none" role="img" aria-label="Eidos">
2
+ <rect x="2.75" y="2.75" width="26.5" height="26.5" rx="6.25" stroke="#e08a63" stroke-width="1.9" opacity="0.42"/>
3
+ <path d="M16 8.6 22.9 16 16 23.4 9.1 16 16 8.6Z" fill="#e08a63"/>
4
+ </svg>
package/dist/src/cli.js CHANGED
@@ -2,4 +2,11 @@
2
2
  // The executable entry point: one root of Eidos blueprints, scaffolded,
3
3
  // validated, and indexed from the shell.
4
4
  import { buildProgram } from './program.js';
5
+ // A reader that closes the pipe early (`eidos framework | head`) is not an
6
+ // error worth a stack trace: stop writing and leave quietly.
7
+ process.stdout.on('error', (error) => {
8
+ if (error.code === 'EPIPE')
9
+ process.exit(0);
10
+ throw error;
11
+ });
5
12
  buildProgram().parse(process.argv);
@@ -10,7 +10,7 @@ export function nudge() {
10
10
  NUDGE_START,
11
11
  '## Eidos',
12
12
  '',
13
- 'This repository keeps its blueprints in an Eidos root. Before creating, editing, reviewing, or discussing a blueprint or the framework, run `eidos instructions overview` and follow it; it names the guide to read for each kind of work. Use the `eidos` CLI for structure (`eidos new`, `eidos check`, `eidos index`); never hand-assemble frontmatter or edit an `index.md`.',
13
+ 'This repository keeps its blueprints in an Eidos root. Before creating, editing, reviewing, or discussing a blueprint or the framework, run `eidos instructions overview` and follow it; it names the guide to read for each kind of work. Use the `eidos` CLI for structure (`eidos new`, `eidos check`, `eidos index`); never hand-assemble frontmatter or edit the generated index in the framework document.',
14
14
  NUDGE_END,
15
15
  ].join('\n');
16
16
  }
@@ -0,0 +1,77 @@
1
+ // `eidos canvas`: the canvas maps a root keeps under .eidos/plugins/eidosmd/maps/, listed or
2
+ // created from the shell; the browser is where they are drawn.
3
+ import { readFileSync } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { stringify } from 'yaml';
6
+ import { createCanvas, findPage, listCanvases, loadCanvas, CanvasError } from '../core/canvas.js';
7
+ import { CANVAS_SCHEMA } from '../core/canvas-schema.js';
8
+ import { CliError, EXIT_OK, print, printJson } from '../output.js';
9
+ export function runCanvasList(context, options) {
10
+ const canvases = listCanvases(context.framework);
11
+ if (options.json) {
12
+ printJson(canvases.map((canvas) => ({ id: canvas.id, title: canvas.title, path: path.relative(context.root, canvas.file).split(path.sep).join('/'), pages: canvas.pages.map((page) => ({ id: page.id, title: page.title, nodes: page.nodes.length, edges: page.edges.length })), problems: canvas.problems })));
13
+ return EXIT_OK;
14
+ }
15
+ if (canvases.length === 0) {
16
+ print('no canvases under .eidos/plugins/eidosmd/maps/; create one: eidos canvas new "<title>", or open eidos browser');
17
+ return EXIT_OK;
18
+ }
19
+ for (const canvas of canvases) {
20
+ print(`${canvas.id} — ${canvas.title}`);
21
+ for (const page of canvas.pages) {
22
+ print(` ${page.id}: ${page.title} (${page.nodes.length} node${page.nodes.length === 1 ? '' : 's'}, ${page.edges.length} edge${page.edges.length === 1 ? '' : 's'})`);
23
+ }
24
+ }
25
+ return EXIT_OK;
26
+ }
27
+ export function runCanvasNew(context, title, options) {
28
+ let canvas;
29
+ try {
30
+ canvas = createCanvas(context.framework, title);
31
+ }
32
+ catch (cause) {
33
+ if (cause instanceof CanvasError) {
34
+ throw new CliError(cause.message);
35
+ }
36
+ throw cause;
37
+ }
38
+ const rel = path.relative(context.root, canvas.file).split(path.sep).join('/');
39
+ if (options.json) {
40
+ printJson({ id: canvas.id, title: canvas.title, path: rel });
41
+ return EXIT_OK;
42
+ }
43
+ print(`✓ ${rel} (canvas ${canvas.id}, one page: overview)`, ' next: eidos browser, and draw');
44
+ return EXIT_OK;
45
+ }
46
+ // The file as it is, or one page of it, for a person or an agent about to edit it.
47
+ export function runCanvasShow(context, id, options) {
48
+ let canvas;
49
+ try {
50
+ canvas = loadCanvas(context.framework, id);
51
+ if (options.page !== undefined) {
52
+ const page = findPage(canvas, options.page);
53
+ if (options.json)
54
+ printJson(page);
55
+ else
56
+ print(stringify(page).trimEnd());
57
+ return EXIT_OK;
58
+ }
59
+ }
60
+ catch (cause) {
61
+ if (cause instanceof CanvasError) {
62
+ throw new CliError(cause.message);
63
+ }
64
+ throw cause;
65
+ }
66
+ if (options.json) {
67
+ printJson({ id: canvas.id, title: canvas.title, path: path.relative(context.root, canvas.file).split(path.sep).join('/'), pages: canvas.pages, problems: canvas.problems });
68
+ return EXIT_OK;
69
+ }
70
+ print(readFileSync(canvas.file, 'utf8').trimEnd());
71
+ return EXIT_OK;
72
+ }
73
+ // The JSON Schema of the file, for whoever edits one without the browser.
74
+ export function runCanvasSchema() {
75
+ printJson(CANVAS_SCHEMA);
76
+ return EXIT_OK;
77
+ }
@@ -16,7 +16,7 @@ export function runCheck(context, paths, options) {
16
16
  const resolved = path.resolve(context.cwd, given);
17
17
  const match = context.blueprints.find((blueprint) => path.resolve(blueprint.path) === resolved);
18
18
  if (!match) {
19
- throw new CliError(`${given} is not a blueprint in ${context.root} (not in a declared collection, or an index.md)`);
19
+ throw new CliError(`${given} is not a blueprint in ${context.root} (not in a declared collection)`);
20
20
  }
21
21
  only.add(match.path);
22
22
  }
@@ -1,7 +1,8 @@
1
1
  // `eidos framework`: what the root's framework document declares, as a human
2
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.
3
+ // document itself, normalized.
4
4
  import path from 'node:path';
5
+ import { unitOf } from '../core/framework-model.js';
5
6
  import { frameworkToDocument, serializeDocument } from '../core/framework-structured.js';
6
7
  import { CliError, EXIT_OK, print, printJson } from '../output.js';
7
8
  export function frameworkJson(framework) {
@@ -16,7 +17,7 @@ export function runFramework(context, options) {
16
17
  const { framework } = context;
17
18
  if (options.as !== undefined) {
18
19
  if (options.as !== 'yaml') {
19
- throw new CliError('--as takes yaml (the markdown form is authored, not generated)');
20
+ throw new CliError('--as takes yaml');
20
21
  }
21
22
  process.stdout.write(serializeDocument(frameworkToDocument(framework)));
22
23
  return EXIT_OK;
@@ -44,6 +45,10 @@ export function runFramework(context, options) {
44
45
  if (collection.framing) {
45
46
  tags.push('framing');
46
47
  }
48
+ const unit = unitOf(collection);
49
+ if (unit) {
50
+ tags.push(`unit: ${unit}`);
51
+ }
47
52
  if (collection.grouping) {
48
53
  tags.push(`grouped by ${collection.grouping.label}${collection.grouping.property ? ` (\`${collection.grouping.property}\`)` : ''}`);
49
54
  }
@@ -54,13 +59,13 @@ export function runFramework(context, options) {
54
59
  if (collection.description) {
55
60
  print(` ${collection.description}`);
56
61
  }
57
- const flavors = collection.flavors.map((flavor) => `${flavor.name}${flavor.isDefault ? ' (default)' : ''}`);
58
- print(` flavors: ${flavors.length > 0 ? flavors.join(', ') : 'none declared'}`);
62
+ const variants = collection.variants.map((variant) => `${variant.name}${variant.isDefault ? ' (default)' : ''}`);
63
+ print(` variants: ${variants.length > 0 ? variants.join(', ') : 'none declared'}`);
59
64
  if (collection.grouping && collection.grouping.groups.length > 0) {
60
65
  print(` groups: ${collection.grouping.groups.map((group) => group.name).join(', ')}`);
61
66
  }
62
67
  }
63
- print('', 'Schema');
68
+ print('', 'Properties');
64
69
  print(` core: ${framework.schema.core.map((property) => property.name).join(', ')}`);
65
70
  if (framework.schema.custom.length === 0) {
66
71
  print(' custom: none');
@@ -68,9 +73,23 @@ export function runFramework(context, options) {
68
73
  else {
69
74
  print(' custom:');
70
75
  for (const property of framework.schema.custom) {
76
+ const scope = property.appliesTo === 'all' ? 'all' : property.appliesTo.join(', ');
77
+ const tools = property.tools ? ` [${Object.keys(property.tools).join(', ')}]` : '';
78
+ print(` ${property.name} (${property.type}, ${scope}) — ${property.meaning}${tools}`);
79
+ }
80
+ }
81
+ for (const [tool, block] of Object.entries(framework.schema.tools)) {
82
+ print(` ${tool} (the tool's own block):`);
83
+ for (const property of block) {
71
84
  const scope = property.appliesTo === 'all' ? 'all' : property.appliesTo.join(', ');
72
85
  print(` ${property.name} (${property.type}, ${scope}) — ${property.meaning}`);
73
86
  }
74
87
  }
88
+ if (framework.vocabulary.length > 0) {
89
+ print('', 'Vocabulary');
90
+ for (const term of framework.vocabulary) {
91
+ print(` ${term.term}: ${term.means}${term.not.length > 0 ? ` (not: ${term.not.join('; ')})` : ''}`);
92
+ }
93
+ }
75
94
  return EXIT_OK;
76
95
  }
@@ -1,4 +1,4 @@
1
- // `eidos index`: regenerate each collection's index.md, or verify them with --check.
1
+ // `eidos index`: regenerate each collection's index in the framework document, or verify them with --check.
2
2
  import path from 'node:path';
3
3
  import { buildIndexes } from '../core/index-leaf.js';
4
4
  import { findCollection } from '../core/framework.js';
@@ -9,13 +9,13 @@ export function runIndex(context, options) {
9
9
  collections = options.collection.map((name) => {
10
10
  const found = findCollection(context.framework, name);
11
11
  if (!found) {
12
- throw new CliError(`collection '${name}' is not declared in Framework.md`);
12
+ throw new CliError(`collection '${name}' is not declared in the framework document`);
13
13
  }
14
14
  return found;
15
15
  });
16
16
  }
17
17
  if (context.framework.collections.length === 0) {
18
- throw new CliError('no collections declared in _eidos/Framework.md (## Collections)');
18
+ throw new CliError('no collections declared in the framework document');
19
19
  }
20
20
  const results = buildIndexes(context.framework, context.blueprints, { collections, check: options.check });
21
21
  const stale = results.some((result) => result.status === 'stale');
@@ -39,7 +39,7 @@ export function runIndex(context, options) {
39
39
  const count = `${result.count} blueprint${result.count === 1 ? '' : 's'}`;
40
40
  switch (result.status) {
41
41
  case 'folder-missing':
42
- printError(` ✗ ${result.collection.name}: folder not found (${path.dirname(result.file)})`);
42
+ printError(` ✗ ${result.collection.name}: folder not found (${path.join(context.root, result.collection.name)})`);
43
43
  break;
44
44
  case 'stale':
45
45
  print(` ✗ stale: ${relFile}`);
@@ -23,6 +23,7 @@ export function runInit(rootArg, options, cwd = process.cwd()) {
23
23
  naming,
24
24
  groups: options.group,
25
25
  product: options.product ?? null,
26
+ framing: options.framing,
26
27
  date: options.date,
27
28
  dryRun: options.dryRun,
28
29
  });
@@ -8,7 +8,7 @@ export const GUIDES = [
8
8
  { name: 'overview', summary: 'Required first read before answering any request in a root' },
9
9
  { name: 'authoring', summary: 'How to place, scaffold, fill, and check a blueprint with the owner' },
10
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' },
11
+ { name: 'configuring', summary: 'How to add a collection, a variant, or a property, and keep the Framework current' },
12
12
  { name: 'init-required', summary: 'How to create a root when there is none here' },
13
13
  ];
14
14
  export function guideText(name) {
@@ -1,17 +1,17 @@
1
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';
2
+ // variant, or any property value.
3
+ import { blueprintId, blueprintTitle, propertyString, resolveVariant } from '../core/blueprint.js';
4
4
  import { findCollection } from '../core/framework.js';
5
5
  import { CliError, EXIT_OK, print, printJson, table } from '../output.js';
6
6
  export function blueprintJson(blueprint) {
7
- const { flavor, declared } = resolveFlavor(blueprint);
7
+ const { variant, declared } = resolveVariant(blueprint);
8
8
  return {
9
9
  id: blueprintId(blueprint),
10
10
  title: blueprintTitle(blueprint),
11
11
  summary: propertyString(blueprint, 'summary'),
12
12
  collection: blueprint.collection.name,
13
13
  group: blueprint.group,
14
- flavor: flavor?.name ?? declared,
14
+ variant: variant?.name ?? declared,
15
15
  path: blueprint.rel,
16
16
  properties: blueprint.properties ?? {},
17
17
  };
@@ -46,9 +46,9 @@ export function runList(context, collectionArg, options) {
46
46
  const wanted = options.group.toLowerCase();
47
47
  selected = selected.filter((blueprint) => blueprint.group?.toLowerCase() === wanted);
48
48
  }
49
- if (options.flavor !== undefined) {
50
- const wanted = options.flavor.toLowerCase();
51
- selected = selected.filter((blueprint) => resolveFlavor(blueprint).flavor?.name.toLowerCase() === wanted);
49
+ if (options.variant !== undefined) {
50
+ const wanted = options.variant.toLowerCase();
51
+ selected = selected.filter((blueprint) => resolveVariant(blueprint).variant?.name.toLowerCase() === wanted);
52
52
  }
53
53
  for (const clause of options.where) {
54
54
  selected = selected.filter((blueprint) => matchesWhere(blueprint, clause));
@@ -65,7 +65,7 @@ export function runList(context, collectionArg, options) {
65
65
  blueprintId(blueprint) ?? '(no id)',
66
66
  blueprintTitle(blueprint),
67
67
  blueprint.group === null ? blueprint.collection.name : `${blueprint.collection.name}/${blueprint.group}`,
68
- resolveFlavor(blueprint).flavor?.name ?? '?',
68
+ resolveVariant(blueprint).variant?.name ?? '?',
69
69
  ]);
70
70
  print(...table([['ID', 'TITLE', 'COLLECTION', 'FLAVOR'], ...rows]));
71
71
  print('', `${selected.length} blueprint${selected.length === 1 ? '' : 's'}`);
@@ -0,0 +1,32 @@
1
+ // `eidos migrate`: move a root to the standard this CLI carries, mechanically,
2
+ // and say what moved.
3
+ import { findMigratableRoot, migrateRoot, MigrationError } from '../core/migrate.js';
4
+ import { CliError, EXIT_OK, print, printJson } from '../output.js';
5
+ export function runMigrate(options, cwd = process.cwd()) {
6
+ let report;
7
+ try {
8
+ report = migrateRoot(findMigratableRoot(cwd, options.root), { dryRun: options.dryRun });
9
+ }
10
+ catch (cause) {
11
+ if (cause instanceof MigrationError)
12
+ throw new CliError(cause.message);
13
+ throw cause;
14
+ }
15
+ if (options.json) {
16
+ printJson(report);
17
+ return EXIT_OK;
18
+ }
19
+ if (report.actions.length === 0) {
20
+ print(`${report.root} already targets Eidos ${report.to}; nothing to move`);
21
+ }
22
+ else {
23
+ print(`${options.dryRun ? 'would move' : 'moved'} ${report.root} from Eidos ${report.from ?? '?'} to ${report.to}:`);
24
+ for (const action of report.actions)
25
+ print(` ${options.dryRun ? '·' : '✓'} ${action}`);
26
+ }
27
+ for (const note of report.notes)
28
+ print(` ! ${note}`);
29
+ if (!options.dryRun && report.actions.length > 0)
30
+ print('', 'next: eidos check');
31
+ return EXIT_OK;
32
+ }
@@ -20,7 +20,7 @@ export function runNew(context, collection, titleWords, options) {
20
20
  result = scaffoldBlueprint(context.framework, context.blueprints, {
21
21
  collection,
22
22
  title: titleWords.join(' '),
23
- flavor: options.flavor ?? null,
23
+ variant: options.variant ?? null,
24
24
  group: options.group ?? null,
25
25
  id: options.id ?? null,
26
26
  summary: options.summary ?? null,
@@ -45,7 +45,7 @@ export function runNew(context, collection, titleWords, options) {
45
45
  id: result.id,
46
46
  collection: result.collection.name,
47
47
  group: result.group,
48
- flavor: result.flavor.name,
48
+ variant: result.variant.name,
49
49
  dry_run: options.dryRun,
50
50
  notes: result.notes,
51
51
  content: options.dryRun ? result.content : undefined,
@@ -56,7 +56,7 @@ export function runNew(context, collection, titleWords, options) {
56
56
  print(`would write ${result.rel}:`, '', result.content);
57
57
  }
58
58
  else {
59
- print(`✓ ${result.rel} (${result.collection.name}, ${result.flavor.kind}.${result.flavor.name}, id ${result.id})`);
59
+ print(`✓ ${result.rel} (${result.collection.name}, ${result.variant.unit}.${result.variant.name}, id ${result.id})`);
60
60
  }
61
61
  for (const note of result.notes) {
62
62
  print(` ! ${note}`);
@@ -1,4 +1,4 @@
1
- // `eidos seeds`: the bundled starting frameworks, read from their own Framework.md.
1
+ // `eidos seeds`: the bundled starting frameworks, read from their own Framework.yaml.
2
2
  import { listSeeds } from '../core/seed.js';
3
3
  import { seedsDir } from '../paths.js';
4
4
  import { EXIT_OK, print, printJson } from '../output.js';
@@ -13,7 +13,7 @@ export function runSeeds(options) {
13
13
  framing: collection.framing,
14
14
  description: collection.description,
15
15
  grouping: collection.grouping?.label ?? null,
16
- flavors: collection.flavors.map((flavor) => ({ name: flavor.name, description: flavor.description, default: flavor.isDefault })),
16
+ variants: collection.variants.map((variant) => ({ name: variant.name, description: variant.description, default: variant.isDefault })),
17
17
  })),
18
18
  })));
19
19
  return EXIT_OK;
@@ -29,9 +29,9 @@ export function runSeeds(options) {
29
29
  role = `grouped by ${collection.grouping.label.toLowerCase()}`;
30
30
  }
31
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}`);
32
+ for (const variant of collection.variants) {
33
+ const marker = variant.isDefault ? ' (default)' : '';
34
+ print(variant.description ? ` · ${variant.name} — ${variant.description}${marker}` : ` · ${variant.name}${marker}`);
35
35
  }
36
36
  }
37
37
  }
@@ -0,0 +1,119 @@
1
+ // `eidos setup`: the CLI's own settings for a root, asked in the terminal or
2
+ // given as flags: whether git is read, the people who work here (name, alias
3
+ // for @mentions, role from .eidos/roles/, email), and who this machine acts
4
+ // as. Writes .eidos/plugins/eidosmd/settings.yaml (shared) and local.yaml
5
+ // (personal); nothing of the standard's changes.
6
+ import { createInterface } from 'node:readline/promises';
7
+ import { gitHead, gitUser } from '../core/git.js';
8
+ import { listRoles } from '../core/me.js';
9
+ import { cleanAlias, findUser, matchesIdentity, readSettings, writeSettings } from '../core/settings.js';
10
+ import { CliError, EXIT_OK, print, printJson } from '../output.js';
11
+ const interactive = () => process.stdin.isTTY === true && process.stdout.isTTY === true;
12
+ // One line, with a default shown in brackets; an empty answer takes it.
13
+ async function ask(question, fallback) {
14
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
15
+ try {
16
+ const answer = (await rl.question(`${question}${fallback === '' ? '' : ` [${fallback}]`}: `)).trim();
17
+ return answer === '' ? fallback : answer;
18
+ }
19
+ finally {
20
+ rl.close();
21
+ }
22
+ }
23
+ export async function askYesNo(question, fallback) {
24
+ const answer = (await ask(`${question} (${fallback ? 'Y/n' : 'y/N'})`, '')).toLowerCase();
25
+ if (answer === '')
26
+ return fallback;
27
+ return answer.startsWith('y');
28
+ }
29
+ const onOff = (value) => (value === undefined ? null : /^(on|true|yes|1)$/i.test(value) ? true : /^(off|false|no|0)$/i.test(value) ? false : null);
30
+ function describe(root, settings) {
31
+ const inRepo = gitHead(root) !== null;
32
+ const identity = inRepo ? gitUser(root) : null;
33
+ const lines = [
34
+ `git features: ${settings.shared.features.git ? 'on' : 'off'}${inRepo ? '' : ' (this root is not in a git repository)'}${identity ? `; git identity ${identity.name}${identity.email ? ` <${identity.email}>` : ''}` : ''}`,
35
+ settings.shared.users.length === 0 ? 'users: none' : `users: ${settings.shared.users.map((user) => `${user.name}${user.alias ? ` (@${user.alias})` : ''}${user.role ? ` · ${user.role}` : ''}`).join(', ')}`,
36
+ `acting as: ${settings.local.user ?? (identity ? `the git identity (${identity.name})` : 'the role in me.md')}`,
37
+ `files: ${settings.files.shared} (shared), ${settings.files.local} (yours, gitignored)`,
38
+ ];
39
+ return lines;
40
+ }
41
+ // After `eidos init` in a terminal: the one question worth asking at once.
42
+ // Anywhere else the root stays unconfigured and the browser asks instead.
43
+ export async function offerGit(root) {
44
+ if (!interactive() || readSettings(root).configured)
45
+ return;
46
+ const inRepo = gitHead(root) !== null;
47
+ const git = await askYesNo(`Use git features in the browser (history, authors, versions)?${inRepo ? '' : ' The root is not in a repository yet'}`, true);
48
+ writeSettings(root, { shared: { features: { git } } });
49
+ print(` git features ${git ? 'on' : 'off'} (eidos setup changes it, and adds the people who work here)`);
50
+ }
51
+ export async function runSetup(context, options) {
52
+ const root = context.root;
53
+ let settings = readSettings(root);
54
+ const flagged = options.git !== undefined || options.addUser.length > 0 || options.as !== undefined;
55
+ if (flagged) {
56
+ const git = onOff(options.git);
57
+ if (options.git !== undefined && git === null)
58
+ throw new CliError('--git takes on or off');
59
+ const users = settings.shared.users.map((user) => ({ ...user }));
60
+ for (const name of options.addUser) {
61
+ if (name.trim() === '')
62
+ throw new CliError('--add-user needs a name');
63
+ const alias = cleanAlias(options.alias) ?? null;
64
+ const holder = alias ? findUser(users, alias) : undefined;
65
+ if (holder && holder.name !== name.trim())
66
+ throw new CliError(`the alias @${alias} is taken`);
67
+ const held = users.find((user) => user.name.toLowerCase() === name.trim().toLowerCase());
68
+ const entry = { name: name.trim(), alias: alias ?? held?.alias ?? null, role: options.role ?? held?.role ?? null, email: options.email ?? held?.email ?? null };
69
+ if (entry.role && !listRoles(root).some((role) => role.name === entry.role))
70
+ throw new CliError(`no role '${entry.role}' under .eidos/roles/ (eidos roles lists them)`);
71
+ if (held)
72
+ Object.assign(held, entry);
73
+ else
74
+ users.push(entry);
75
+ }
76
+ if (options.as !== undefined && options.as !== '' && !findUser(users, options.as))
77
+ throw new CliError(`no user '${options.as}'; add one with --add-user`);
78
+ settings = writeSettings(root, {
79
+ shared: { ...(git === null ? {} : { features: { git } }), ...(options.addUser.length > 0 ? { users } : {}) },
80
+ ...(options.as !== undefined ? { local: { user: options.as === '' ? null : options.as } } : {}),
81
+ });
82
+ }
83
+ else if (interactive()) {
84
+ print(...describe(root, settings), '');
85
+ const inRepo = gitHead(root) !== null;
86
+ const git = await askYesNo(`Use git features in the browser (history, authors, versions)?${inRepo ? '' : ' The root is not in a repository yet'}`, settings.shared.features.git);
87
+ const users = settings.shared.users.map((user) => ({ ...user }));
88
+ const identity = git && inRepo ? gitUser(root) : null;
89
+ let acting = settings.local.user;
90
+ if (identity && !users.some((user) => matchesIdentity(user, identity))) {
91
+ if (await askYesNo(`Add ${identity.name}${identity.email ? ` <${identity.email}>` : ''} as a user?`, true)) {
92
+ const alias = cleanAlias(await ask('Alias for @mentions (no spaces)', identity.name.split(/\s+/)[0]?.toLowerCase() ?? ''));
93
+ const roles = listRoles(root).map((role) => role.name);
94
+ const role = roles.length === 0 ? '' : await ask(`Role in this framework (${roles.join(', ')}; blank for none)`, '');
95
+ if (role !== '' && !roles.includes(role))
96
+ throw new CliError(`no role '${role}' under .eidos/roles/`);
97
+ users.push({ name: identity.name, alias, role: role === '' ? null : role, email: identity.email });
98
+ acting = alias ?? identity.name;
99
+ }
100
+ }
101
+ if (users.length > 0) {
102
+ const answer = await ask(`Act as (${users.map((user) => user.alias ?? user.name).join(', ')}; blank for the git identity)`, acting ?? '');
103
+ if (answer !== '' && !findUser(users, answer))
104
+ throw new CliError(`no user '${answer}'`);
105
+ acting = answer === '' ? null : answer;
106
+ }
107
+ settings = writeSettings(root, { shared: { features: { git }, users }, local: { user: acting } });
108
+ }
109
+ else {
110
+ print(...describe(root, settings), '', 'not a terminal: set values with flags, e.g. eidos setup --git on --add-user "Ada Lovelace" --alias ada --role developer --as ada');
111
+ return EXIT_OK;
112
+ }
113
+ if (options.json) {
114
+ printJson({ shared: settings.shared, local: settings.local, files: settings.files });
115
+ return EXIT_OK;
116
+ }
117
+ print('✓ settings written', ...describe(root, settings).map((line) => ` ${line}`));
118
+ return EXIT_OK;
119
+ }
@@ -0,0 +1,44 @@
1
+ // `eidos version`: the root's own versions, this CLI's own record under
2
+ // .eidos/plugins/eidosmd/, listed, or one recorded because the owner asked: a
3
+ // name, a commit that exists, and a tag only on --tag. Never proposed; an
4
+ // empty list is the normal state.
5
+ import path from 'node:path';
6
+ import { readVersions, recordVersion, TAG_PREFIX, VersionError, versionsFile } from '../core/versions.js';
7
+ import { CliError, EXIT_OK, print, printJson } from '../output.js';
8
+ export function runVersionList(context, options) {
9
+ const versions = readVersions(context.root);
10
+ if (options.json) {
11
+ printJson(versions);
12
+ return EXIT_OK;
13
+ }
14
+ if (versions.length === 0) {
15
+ print('no versions recorded (the normal state); record one when a team needs a fixed point: eidos version record <version> [--commit <sha>] [--tag]');
16
+ return EXIT_OK;
17
+ }
18
+ for (const version of versions) {
19
+ print(`${version.version} ${version.commit}${version.tag ? ` ${version.tag}` : ''}`);
20
+ }
21
+ return EXIT_OK;
22
+ }
23
+ export function runVersionRecord(context, version, options) {
24
+ let recorded;
25
+ try {
26
+ recorded = recordVersion(context.root, { version, commit: options.commit, tag: options.tag });
27
+ }
28
+ catch (cause) {
29
+ if (cause instanceof VersionError) {
30
+ throw new CliError(cause.message);
31
+ }
32
+ throw cause;
33
+ }
34
+ if (options.json) {
35
+ printJson({ ...recorded.version, tagged: recorded.tagged });
36
+ return EXIT_OK;
37
+ }
38
+ print(`✓ ${recorded.version.version} → ${recorded.version.commit}${recorded.version.tag ? ` tagged ${recorded.version.tag}` : ''}`);
39
+ if (!recorded.version.tag) {
40
+ print(` (no tag; --tag would have created ${TAG_PREFIX}${recorded.version.version})`);
41
+ }
42
+ print(` recorded in ${path.relative(context.root, versionsFile(context.root))}; the commit that adds it is not the one it names, the way a tag follows the commit it marks`);
43
+ return EXIT_OK;
44
+ }
@@ -11,7 +11,7 @@ export function runRoles(context, options) {
11
11
  return EXIT_OK;
12
12
  }
13
13
  if (roles.length === 0) {
14
- print('no roles installed under _eidos/roles/; an older framework, or one that declares none');
14
+ print('no roles installed under .eidos/roles/; an older framework, or one that declares none');
15
15
  return EXIT_OK;
16
16
  }
17
17
  for (const role of roles) {
@@ -43,7 +43,7 @@ export function runWhoami(context, options) {
43
43
  if (!role) {
44
44
  if (options.role !== undefined && listRoles(context.root).length > 0) {
45
45
  const known = listRoles(context.root).map((candidate) => candidate.name).join(', ');
46
- throw new CliError(`no role '${options.role}' under _eidos/roles/ (installed: ${known}); a custom role is set by editing _eidos/me.md`);
46
+ throw new CliError(`no role '${options.role}' under .eidos/roles/ (installed: ${known}); a custom role is set by editing .eidos/me.md`);
47
47
  }
48
48
  custom = roleName;
49
49
  }
@@ -67,7 +67,7 @@ export function runWhoami(context, options) {
67
67
  return EXIT_OK;
68
68
  }
69
69
  if (!exists) {
70
- print('no _eidos/me.md: the actor is unset, so the agent defaults to full facilitation', 'set it: eidos whoami --role <name>');
70
+ print('no .eidos/me.md: the actor is unset, so the agent defaults to full facilitation', 'set it: eidos whoami --role <name>');
71
71
  return EXIT_OK;
72
72
  }
73
73
  if (actor.role === null) {
@@ -75,7 +75,7 @@ export function runWhoami(context, options) {
75
75
  return runRoles(context, { json: false });
76
76
  }
77
77
  print(`you are: ${actor.role}`);
78
- const roleFile = actor.roleFile ? path.resolve(context.root, '_eidos', actor.roleFile) : null;
78
+ const roleFile = actor.roleFile ? path.resolve(context.root, '.eidos', actor.roleFile) : null;
79
79
  if (roleFile && existsSync(roleFile)) {
80
80
  print(`role file: ${path.relative(context.root, roleFile).split(path.sep).join('/')}`, '', readFileSync(roleFile, 'utf8').trimEnd(), '');
81
81
  }
@@ -4,9 +4,9 @@ import { listBlueprints } from './core/blueprint.js';
4
4
  import { FrameworkError, loadFramework } from './core/framework.js';
5
5
  import { findRoot, RootError } from './core/root.js';
6
6
  import { CliError, EXIT_USAGE } from './output.js';
7
- // The CLI works on Framework.yaml. A root that keeps Framework.md is read only
8
- // to convert it (or to print it), so every other command points at `convert`.
9
- export function loadRoot(globals, cwd = process.cwd(), options = {}) {
7
+ // The framework document is Framework.yaml. A root that still keeps the
8
+ // 4.x Framework.md is read only by `migrate`, so every command points there.
9
+ export function loadRoot(globals, cwd = process.cwd()) {
10
10
  let root;
11
11
  try {
12
12
  root = findRoot(cwd, globals.root);
@@ -28,8 +28,8 @@ export function loadRoot(globals, cwd = process.cwd(), options = {}) {
28
28
  }
29
29
  throw cause;
30
30
  }
31
- if (framework.format === 'md' && !options.allowMarkdown) {
32
- throw new CliError(`${root} keeps its framework as _eidos/Framework.md. The eidos CLI works on Framework.yaml: run \`eidos convert\` to move it (Framework.md and each collection's index.md are removed; the markdown form's prose has no field to land in). \`eidos convert --dry-run\` shows the writes, and \`eidos framework --as yaml\` prints the result without touching anything.`, EXIT_USAGE);
31
+ if (framework.format === 'md') {
32
+ throw new CliError(`${root} keeps its framework as .eidos/Framework.md, the form before Eidos 5.0.0. Run \`eidos migrate\` to move it: Framework.yaml takes its place with the index inside, and Framework.md and each collection's index.md are removed (the markdown form's prose has no field to land in). \`eidos migrate --dry-run\` lists the moves without touching anything.`, EXIT_USAGE);
33
33
  }
34
34
  return { cwd, root, framework, blueprints: listBlueprints(framework) };
35
35
  }