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.
Files changed (107) hide show
  1. package/README.md +50 -29
  2. package/browser/dist/assets/index-Cc3cNWHY.css +1 -0
  3. package/browser/dist/assets/index-DQgCQRa5.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 +10 -4
  11. package/dist/src/commands/configure.js +201 -0
  12. package/dist/src/commands/framework.js +37 -7
  13. package/dist/src/commands/index.js +4 -4
  14. package/dist/src/commands/init.js +5 -0
  15. package/dist/src/commands/instructions.js +1 -1
  16. package/dist/src/commands/list.js +8 -8
  17. package/dist/src/commands/migrate.js +32 -0
  18. package/dist/src/commands/new.js +3 -3
  19. package/dist/src/commands/property.js +125 -0
  20. package/dist/src/commands/seeds.js +5 -5
  21. package/dist/src/commands/setup.js +131 -0
  22. package/dist/src/commands/version.js +44 -0
  23. package/dist/src/commands/whoami.js +4 -4
  24. package/dist/src/context.js +5 -5
  25. package/dist/src/core/blueprint.js +16 -11
  26. package/dist/src/core/canvas-schema.js +148 -0
  27. package/dist/src/core/canvas.js +732 -0
  28. package/dist/src/core/check.js +221 -70
  29. package/dist/src/core/convert.js +7 -6
  30. package/dist/src/core/edits.js +1381 -0
  31. package/dist/src/core/framework-markdown.js +119 -34
  32. package/dist/src/core/framework-model.js +62 -10
  33. package/dist/src/core/framework-structured.js +222 -47
  34. package/dist/src/core/framework.js +13 -13
  35. package/dist/src/core/frontmatter.js +61 -1
  36. package/dist/src/core/git.js +84 -0
  37. package/dist/src/core/index-leaf.js +2 -2
  38. package/dist/src/core/links.js +87 -0
  39. package/dist/src/core/markdown.js +16 -9
  40. package/dist/src/core/me.js +16 -8
  41. package/dist/src/core/migrate.js +319 -0
  42. package/dist/src/core/naming.js +1 -1
  43. package/dist/src/core/regions.js +117 -0
  44. package/dist/src/core/root.js +2 -2
  45. package/dist/src/core/scaffold.js +33 -27
  46. package/dist/src/core/seed.js +187 -71
  47. package/dist/src/core/server.js +1410 -53
  48. package/dist/src/core/settings.js +232 -0
  49. package/dist/src/core/store.js +315 -0
  50. package/dist/src/core/template.js +32 -0
  51. package/dist/src/core/versions.js +84 -0
  52. package/dist/src/output.js +4 -1
  53. package/dist/src/program.js +421 -41
  54. package/instructions/authoring.md +15 -12
  55. package/instructions/configuring.md +81 -34
  56. package/instructions/init-required.md +4 -4
  57. package/instructions/overview.md +21 -9
  58. package/instructions/validating.md +9 -6
  59. package/package.json +21 -12
  60. package/standard/EIDOS.md +142 -259
  61. package/standard/seeds/README.md +12 -16
  62. package/standard/seeds/book/Framework.yaml +61 -0
  63. package/standard/seeds/book/README.md +10 -5
  64. package/standard/seeds/book/_gitignore +9 -3
  65. package/standard/seeds/book/me.md +1 -1
  66. package/standard/seeds/book/roles/README.md +3 -3
  67. package/standard/seeds/book/roles/framework-owner.md +2 -2
  68. package/standard/seeds/book/{shapes → templates}/chapter.full.md +0 -8
  69. package/standard/seeds/book/{shapes → templates}/chapter.sketch.md +0 -7
  70. package/standard/seeds/book/{shapes → templates}/frame.market.md +0 -6
  71. package/standard/seeds/book/templates/frame.premise.md +17 -0
  72. package/standard/seeds/book/{shapes → templates}/frame.reader.md +0 -6
  73. package/standard/seeds/book/{shapes → templates}/frame.voice.md +0 -7
  74. package/standard/seeds/research/Framework.yaml +61 -0
  75. package/standard/seeds/research/README.md +10 -5
  76. package/standard/seeds/research/_gitignore +9 -3
  77. package/standard/seeds/research/me.md +1 -1
  78. package/standard/seeds/research/roles/README.md +3 -3
  79. package/standard/seeds/research/roles/framework-owner.md +2 -2
  80. package/standard/seeds/research/{shapes → templates}/frame.ethics.md +0 -6
  81. package/standard/seeds/research/{shapes → templates}/frame.method.md +0 -7
  82. package/standard/seeds/research/{shapes → templates}/frame.prior-work.md +0 -6
  83. package/standard/seeds/research/{shapes → templates}/frame.question.md +0 -7
  84. package/standard/seeds/research/{shapes → templates}/investigation.full.md +0 -8
  85. package/standard/seeds/research/{shapes → templates}/investigation.note.md +0 -7
  86. package/standard/seeds/software/Framework.yaml +62 -0
  87. package/standard/seeds/software/README.md +7 -6
  88. package/standard/seeds/software/_gitignore +9 -3
  89. package/standard/seeds/software/me.md +1 -1
  90. package/standard/seeds/software/roles/README.md +3 -3
  91. package/standard/seeds/software/roles/framework-owner.md +2 -2
  92. package/standard/seeds/software/roles/project-manager.md +2 -2
  93. package/standard/seeds/software/roles/stakeholder.md +1 -1
  94. package/standard/seeds/software/{shapes → templates}/frame.architecture.md +0 -7
  95. package/standard/seeds/software/{shapes → templates}/frame.audience.md +1 -8
  96. package/standard/seeds/software/{shapes → templates}/frame.criteria.md +0 -8
  97. package/standard/seeds/software/{shapes → templates}/frame.market.md +0 -8
  98. package/standard/seeds/software/{shapes → templates}/spec.full.md +0 -8
  99. package/standard/seeds/software/{shapes → templates}/spec.micro.md +0 -9
  100. package/browser/index.html +0 -268
  101. package/dist/src/commands/convert.js +0 -30
  102. package/dist/src/core/shape.js +0 -26
  103. package/standard/seeds/book/Framework.md +0 -87
  104. package/standard/seeds/book/shapes/frame.premise.md +0 -24
  105. package/standard/seeds/research/Framework.md +0 -88
  106. package/standard/seeds/software/Framework.md +0 -88
  107. /package/standard/seeds/software/{shapes → templates}/.gitkeep +0 -0
@@ -0,0 +1,125 @@
1
+ // `eidos property get|set|unset @<blueprint> <property> [<value>]`: one
2
+ // blueprint's frontmatter from the shell, so an agent never opens a file to
3
+ // flip a value. A set is typed by the property's declared type, written in
4
+ // the house style with every other line as it was, and applies the root's
5
+ // on-save rules the way a save from the browser does.
6
+ import { readFileSync, writeFileSync } from 'node:fs';
7
+ import path from 'node:path';
8
+ import { resolveBlueprint } from '../core/blueprint.js';
9
+ import { checkRoot, optionProblem } from '../core/check.js';
10
+ import { blueprintKeySet, blueprintKeyUnset } from '../core/edits.js';
11
+ import { propertiesFor } from '../core/framework.js';
12
+ import { editFrontmatterKey, parseFrontmatter } from '../core/frontmatter.js';
13
+ import { coerce } from '../core/scaffold.js';
14
+ import { readSettings } from '../core/settings.js';
15
+ import { standardVersion } from '../paths.js';
16
+ import { CliError, EXIT_FINDINGS, EXIT_OK, print, printJson, today } from '../output.js';
17
+ import { strictFor } from './check.js';
18
+ function resolve(context, ref) {
19
+ const clean = ref.replace(/^@/, '');
20
+ const blueprint = resolveBlueprint(clean, context.blueprints, context.cwd);
21
+ if (!blueprint)
22
+ throw new CliError(`no blueprint '${clean}' (an id, a path, or a filename)`);
23
+ return blueprint;
24
+ }
25
+ // The local time to the minute, the same stamp an on-save rule writes from the page.
26
+ function localMinute() {
27
+ const now = new Date();
28
+ return `${today()} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
29
+ }
30
+ function withOnSaveRules(root, text) {
31
+ let out = text;
32
+ for (const rule of readSettings(root).shared.on_save)
33
+ out = editFrontmatterKey(out, { kind: 'set', key: rule.property, value: rule.set === 'today' ? today() : localMinute() });
34
+ return out;
35
+ }
36
+ function report(context, blueprint, options, what) {
37
+ const fresh = context.blueprints.map((held) => (held.path === blueprint.path ? reload(blueprint) : held));
38
+ const check = checkRoot(context.framework, fresh, { only: new Set([blueprint.path]), standardVersion: standardVersion() });
39
+ const findings = check.findings.filter((finding) => finding.path === blueprint.rel);
40
+ const strict = strictFor(context.root, undefined);
41
+ const failed = findings.some((finding) => finding.level === 'error') || (strict && findings.some((finding) => finding.level === 'warning'));
42
+ if (options.json) {
43
+ printJson({ ok: !failed, path: blueprint.rel, ...what, findings });
44
+ return failed ? EXIT_FINDINGS : EXIT_OK;
45
+ }
46
+ for (const finding of findings)
47
+ print(` ${finding.level === 'error' ? '✗' : '⚠'} ${finding.code} ${finding.message}`);
48
+ return failed ? EXIT_FINDINGS : EXIT_OK;
49
+ }
50
+ function reload(blueprint) {
51
+ const { properties, body, error } = parseFrontmatter(readFileSync(blueprint.path, 'utf8'));
52
+ return { ...blueprint, properties, frontmatterError: error, body };
53
+ }
54
+ export function runPropertyGet(context, ref, key, options) {
55
+ const blueprint = resolve(context, ref);
56
+ const properties = blueprint.properties ?? {};
57
+ if (key !== undefined) {
58
+ if (!(key in properties))
59
+ throw new CliError(`${blueprint.rel} has no '${key}'`);
60
+ if (options.json)
61
+ printJson({ path: blueprint.rel, [key]: properties[key] });
62
+ else
63
+ print(typeof properties[key] === 'string' ? properties[key] : JSON.stringify(properties[key]));
64
+ return EXIT_OK;
65
+ }
66
+ if (options.json)
67
+ printJson({ path: blueprint.rel, properties });
68
+ else
69
+ for (const [name, value] of Object.entries(properties))
70
+ print(`${name}: ${typeof value === 'string' ? value : JSON.stringify(value)}`);
71
+ return EXIT_OK;
72
+ }
73
+ export function runPropertySet(context, ref, key, raw, options) {
74
+ const blueprint = resolve(context, ref);
75
+ const declared = propertiesFor(context.framework, blueprint.collection.name).find((property) => property.name === key);
76
+ if (!declared) {
77
+ const elsewhere = [...context.framework.schema.custom, ...Object.values(context.framework.schema.tools).flat()].find((property) => property.name === key);
78
+ if (!options.force)
79
+ throw new CliError(elsewhere ? `${key} applies to ${elsewhere.appliesTo === 'all' ? 'all' : elsewhere.appliesTo.join(', ')}, not ${blueprint.collection.name}; check would report it (--force writes it anyway)` : `no block of the Properties table declares '${key}' for ${blueprint.collection.name}; check would report it (--force writes it anyway, configure:property add declares it)`);
80
+ }
81
+ if (declared && declared.owner !== 'eidos' && declared.owner !== 'custom' && !options.force)
82
+ throw new CliError(`${key} is ${declared.owner}'s own property; only that tool writes it (--force writes it anyway)`);
83
+ const value = coerce(declared?.type ?? 'Text', raw);
84
+ if (declared && declared.type.toLowerCase() === 'date' && !/^\d{4}-\d{2}-\d{2}$/.test(String(value)))
85
+ throw new CliError(`${key} is a Date: YYYY-MM-DD`);
86
+ if (declared && declared.type.toLowerCase() === 'number' && typeof value !== 'number')
87
+ throw new CliError(`${key} is a Number`);
88
+ const offList = declared ? optionProblem(declared, value) : null;
89
+ if (offList && !options.force)
90
+ throw new CliError(`${key} is one of ${offList.options.join(', ')}; '${offList.off.join(', ')}' is not (--force writes it anyway, and check reports it)`);
91
+ const text = withOnSaveRules(context.root, blueprintKeySet(blueprint, key, value));
92
+ if (options.dryRun) {
93
+ if (options.json)
94
+ printJson({ path: blueprint.rel, [key]: value, dry_run: true, text });
95
+ else
96
+ print(text.split('\n---')[0] + '\n---');
97
+ return EXIT_OK;
98
+ }
99
+ writeFileSync(blueprint.path, text, 'utf8');
100
+ if (!options.json)
101
+ print(`✓ ${blueprint.rel} ${key}: ${typeof value === 'string' ? value : JSON.stringify(value)}`);
102
+ return report(context, blueprint, options, { [key]: value });
103
+ }
104
+ export function runPropertyUnset(context, ref, key, options) {
105
+ const blueprint = resolve(context, ref);
106
+ const next = blueprintKeyUnset(blueprint, key);
107
+ if (next === null)
108
+ throw new CliError(`${blueprint.rel} has no '${key}'`);
109
+ const declared = propertiesFor(context.framework, blueprint.collection.name).find((property) => property.name === key);
110
+ if (declared?.required && !options.dryRun)
111
+ print(` ! ${key} is required for ${blueprint.collection.name}; check will report it missing`);
112
+ const text = withOnSaveRules(context.root, next);
113
+ if (options.dryRun) {
114
+ if (options.json)
115
+ printJson({ path: blueprint.rel, unset: key, dry_run: true, text });
116
+ else
117
+ print(text.split('\n---')[0] + '\n---');
118
+ return EXIT_OK;
119
+ }
120
+ writeFileSync(blueprint.path, text, 'utf8');
121
+ if (!options.json)
122
+ print(`✓ ${blueprint.rel} ${key} removed`);
123
+ return report(context, blueprint, options, { unset: key });
124
+ }
125
+ export const relativeTo = (root, file) => path.relative(root, file).split(path.sep).join('/');
@@ -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,131 @@
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
+ export 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
+ `strict checks: ${settings.shared.check.strict ? 'on (a warning fails eidos check)' : 'off (only an error fails eidos check)'}`,
36
+ settings.shared.users.length === 0 ? 'users: none' : `users: ${settings.shared.users.map((user) => `${user.name}${user.alias ? ` (@${user.alias})` : ''}${user.role ? ` · ${user.role}` : ''}`).join(', ')}`,
37
+ `acting as: ${settings.local.user ?? (identity ? `the git identity (${identity.name})` : 'the role in me.md')}`,
38
+ `files: ${settings.files.shared} (shared), ${settings.files.local} (yours, gitignored)`,
39
+ ];
40
+ return lines;
41
+ }
42
+ // After `eidos init`: the two settings a framework owner decides once and
43
+ // commits with the root, whether the browser reads git and whether a warning
44
+ // fails the check. A flag answers either without a question; in a terminal
45
+ // the rest is asked; anywhere else the root stays as the flags left it and
46
+ // the browser asks about git instead.
47
+ export async function offerSetup(root, given) {
48
+ if (given.strict !== undefined)
49
+ writeSettings(root, { shared: { check: { strict: given.strict } } });
50
+ if (!interactive())
51
+ return;
52
+ const settings = readSettings(root);
53
+ const inRepo = gitHead(root) !== null;
54
+ const git = settings.configured ? settings.shared.features.git : await askYesNo(`Use git features in the browser (history, authors, versions)?${inRepo ? '' : ' The root is not in a repository yet'}`, true);
55
+ const strict = given.strict ?? (await askYesNo('Should a warning fail `eidos check`, the way an error does? (strict; kept with the root for CI and everyone)', false));
56
+ writeSettings(root, { shared: { features: { git }, check: { strict } } });
57
+ print(` git features ${git ? 'on' : 'off'}; strict checks ${strict ? 'on' : 'off'} (eidos setup changes both, and adds the people who work here)`);
58
+ }
59
+ export async function runSetup(context, options) {
60
+ const root = context.root;
61
+ let settings = readSettings(root);
62
+ const flagged = options.git !== undefined || options.strict !== undefined || options.addUser.length > 0 || options.as !== undefined;
63
+ if (flagged) {
64
+ const git = onOff(options.git);
65
+ if (options.git !== undefined && git === null)
66
+ throw new CliError('--git takes on or off');
67
+ const strict = onOff(options.strict);
68
+ if (options.strict !== undefined && strict === null)
69
+ throw new CliError('--strict takes on or off');
70
+ const users = settings.shared.users.map((user) => ({ ...user }));
71
+ for (const name of options.addUser) {
72
+ if (name.trim() === '')
73
+ throw new CliError('--add-user needs a name');
74
+ const alias = cleanAlias(options.alias) ?? null;
75
+ const holder = alias ? findUser(users, alias) : undefined;
76
+ if (holder && holder.name !== name.trim())
77
+ throw new CliError(`the alias @${alias} is taken`);
78
+ const held = users.find((user) => user.name.toLowerCase() === name.trim().toLowerCase());
79
+ const entry = { name: name.trim(), alias: alias ?? held?.alias ?? null, role: options.role ?? held?.role ?? null, email: options.email ?? held?.email ?? null };
80
+ if (entry.role && !listRoles(root).some((role) => role.name === entry.role))
81
+ throw new CliError(`no role '${entry.role}' under .eidos/roles/ (eidos roles lists them)`);
82
+ if (held)
83
+ Object.assign(held, entry);
84
+ else
85
+ users.push(entry);
86
+ }
87
+ if (options.as !== undefined && options.as !== '' && !findUser(users, options.as))
88
+ throw new CliError(`no user '${options.as}'; add one with --add-user`);
89
+ settings = writeSettings(root, {
90
+ shared: { ...(git === null ? {} : { features: { git } }), ...(strict === null ? {} : { check: { strict } }), ...(options.addUser.length > 0 ? { users } : {}) },
91
+ ...(options.as !== undefined ? { local: { user: options.as === '' ? null : options.as } } : {}),
92
+ });
93
+ }
94
+ else if (interactive()) {
95
+ print(...describe(root, settings), '');
96
+ const inRepo = gitHead(root) !== null;
97
+ 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);
98
+ const strict = await askYesNo('Should a warning fail `eidos check`, the way an error does? (strict; kept with the root)', settings.shared.check.strict);
99
+ const users = settings.shared.users.map((user) => ({ ...user }));
100
+ const identity = git && inRepo ? gitUser(root) : null;
101
+ let acting = settings.local.user;
102
+ if (identity && !users.some((user) => matchesIdentity(user, identity))) {
103
+ if (await askYesNo(`Add ${identity.name}${identity.email ? ` <${identity.email}>` : ''} as a user?`, true)) {
104
+ const alias = cleanAlias(await ask('Alias for @mentions (no spaces)', identity.name.split(/\s+/)[0]?.toLowerCase() ?? ''));
105
+ const roles = listRoles(root).map((role) => role.name);
106
+ const role = roles.length === 0 ? '' : await ask(`Role in this framework (${roles.join(', ')}; blank for none)`, '');
107
+ if (role !== '' && !roles.includes(role))
108
+ throw new CliError(`no role '${role}' under .eidos/roles/`);
109
+ users.push({ name: identity.name, alias, role: role === '' ? null : role, email: identity.email });
110
+ acting = alias ?? identity.name;
111
+ }
112
+ }
113
+ if (users.length > 0) {
114
+ const answer = await ask(`Act as (${users.map((user) => user.alias ?? user.name).join(', ')}; blank for the git identity)`, acting ?? '');
115
+ if (answer !== '' && !findUser(users, answer))
116
+ throw new CliError(`no user '${answer}'`);
117
+ acting = answer === '' ? null : answer;
118
+ }
119
+ settings = writeSettings(root, { shared: { features: { git }, check: { strict }, users }, local: { user: acting } });
120
+ }
121
+ else {
122
+ print(...describe(root, settings), '', 'not a terminal: set values with flags, e.g. eidos setup --git on --strict off --add-user "Ada Lovelace" --alias ada --role developer --as ada');
123
+ return EXIT_OK;
124
+ }
125
+ if (options.json) {
126
+ printJson({ shared: settings.shared, local: settings.local, files: settings.files });
127
+ return EXIT_OK;
128
+ }
129
+ print('✓ settings written', ...describe(root, settings).map((line) => ` ${line}`));
130
+ return EXIT_OK;
131
+ }
@@ -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, tagPrefix, 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 ${tagPrefix(context.root)}${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
  }
@@ -4,10 +4,13 @@
4
4
  import { readdirSync, readFileSync, statSync } from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import { parseFrontmatter } from './frontmatter.js';
7
- import { defaultFlavor, findFlavor } from './framework.js';
7
+ import { defaultVariant, findVariant } from './framework.js';
8
8
  import { headings, sections } from './markdown.js';
9
9
  import { kebab } from './naming.js';
10
- export function blueprintFiles(dir) {
10
+ // Under a markdown framework each collection's `index.md` is the generated
11
+ // leaf, not a blueprint. Under a YAML one the index lives in the document, so
12
+ // `index.md` is an ordinary filename (a blueprint with the id `index`).
13
+ export function blueprintFiles(dir, leafIsIndexMd) {
11
14
  let entries;
12
15
  try {
13
16
  entries = readdirSync(dir);
@@ -16,7 +19,7 @@ export function blueprintFiles(dir) {
16
19
  return [];
17
20
  }
18
21
  return entries
19
- .filter((entry) => /\.md$/i.test(entry) && entry.toLowerCase() !== 'index.md' && !entry.startsWith('.'))
22
+ .filter((entry) => /\.md$/i.test(entry) && !(leafIsIndexMd && entry.toLowerCase() === 'index.md') && !entry.startsWith('.'))
20
23
  .filter((entry) => {
21
24
  try {
22
25
  return statSync(path.join(dir, entry)).isFile();
@@ -69,11 +72,12 @@ export function listBlueprints(framework, collections = framework.collections) {
69
72
  const out = [];
70
73
  for (const collection of collections) {
71
74
  const dir = path.join(framework.root, collection.name);
72
- for (const file of blueprintFiles(dir)) {
75
+ const leafIsIndexMd = framework.format === 'md';
76
+ for (const file of blueprintFiles(dir, leafIsIndexMd)) {
73
77
  out.push(loadBlueprint(framework.root, collection, null, file));
74
78
  }
75
79
  for (const group of groupDirs(dir)) {
76
- for (const file of blueprintFiles(path.join(dir, group))) {
80
+ for (const file of blueprintFiles(path.join(dir, group), leafIsIndexMd)) {
77
81
  out.push(loadBlueprint(framework.root, collection, group, file));
78
82
  }
79
83
  }
@@ -99,14 +103,15 @@ export function blueprintId(blueprint) {
99
103
  export function blueprintTitle(blueprint) {
100
104
  return propertyString(blueprint, 'title') ?? blueprint.stem;
101
105
  }
102
- // The flavor a blueprint follows: the one it names, else its collection's default.
103
- export function resolveFlavor(blueprint) {
104
- const declared = propertyString(blueprint, 'flavor');
106
+ // The variant a blueprint follows: the one it names, else its collection's default.
107
+ export function resolveVariant(blueprint) {
108
+ // `flavor` is the name before 5.0.0; a root not yet migrated still resolves
109
+ const declared = propertyString(blueprint, 'variant') ?? propertyString(blueprint, 'flavor');
105
110
  if (declared === null) {
106
- return { flavor: defaultFlavor(blueprint.collection), declared: null, known: true };
111
+ return { variant: defaultVariant(blueprint.collection), declared: null, known: true };
107
112
  }
108
- const flavor = findFlavor(blueprint.collection, declared);
109
- return { flavor, declared, known: flavor !== null };
113
+ const variant = findVariant(blueprint.collection, declared);
114
+ return { variant, declared, known: variant !== null };
110
115
  }
111
116
  // A blueprint by path, by id, or by filename stem, in that order.
112
117
  export function resolveBlueprint(ref, blueprints, cwd) {
@@ -0,0 +1,148 @@
1
+ // The canvas file, as a JSON Schema (draft 7): what `.eidos/plugins/eidosmd/maps/<id>.yaml`
2
+ // may hold. The file is the whole truth of a canvas; the browser draws it and writes it
3
+ // back, and anything else (a person, an agent) edits it directly.
4
+ import { ANCHORS, EDGE_CAPS } from './canvas.js';
5
+ const hex = { type: 'string', pattern: '^#[0-9a-fA-F]{6}$', description: 'A color as #rrggbb.' };
6
+ const size = { type: 'string', enum: ['small', 'medium', 'large'] };
7
+ const point = { type: 'object', required: ['x', 'y'], additionalProperties: false, properties: { x: { type: 'number' }, y: { type: 'number' } } };
8
+ export const CANVAS_SCHEMA = {
9
+ $schema: 'http://json-schema.org/draft-07/schema#',
10
+ title: 'Eidos canvas map',
11
+ description: 'One canvas of the eidosmd browser: a title and pages, each with frames, nodes, edges, layout, and comment threads. Kept at .eidos/plugins/eidosmd/maps/<id>.yaml; the id is the file name.',
12
+ type: 'object',
13
+ required: ['title', 'pages'],
14
+ additionalProperties: false,
15
+ properties: {
16
+ title: { type: 'string' },
17
+ pages: { type: 'array', minItems: 1, items: { $ref: '#/definitions/page' } },
18
+ },
19
+ definitions: {
20
+ page: {
21
+ type: 'object',
22
+ required: ['id', 'title'],
23
+ additionalProperties: false,
24
+ properties: {
25
+ id: { type: 'string', description: 'Unique within the canvas; the URL names it.' },
26
+ title: { type: 'string' },
27
+ frames: { type: 'array', items: { $ref: '#/definitions/frame' } },
28
+ nodes: { type: 'array', items: { $ref: '#/definitions/node' } },
29
+ edges: { type: 'array', items: { $ref: '#/definitions/edge' } },
30
+ layout: { type: 'object', description: 'Where each node sits, by node id. A node without a place is laid out by the browser and given one when it first moves.', additionalProperties: point },
31
+ comments: { type: 'array', items: { $ref: '#/definitions/comment' } },
32
+ },
33
+ },
34
+ node: {
35
+ type: 'object',
36
+ required: ['id'],
37
+ additionalProperties: false,
38
+ description: 'Exactly one of blueprint, sketch, sticky, text, or annotation says what the node is.',
39
+ oneOf: [{ required: ['blueprint'] }, { required: ['sketch'] }, { required: ['sticky'] }, { required: ['text'] }, { required: ['annotation'] }],
40
+ properties: {
41
+ id: { type: 'string', description: 'Unique within the page; edges name it.' },
42
+ blueprint: { type: 'string', description: "A blueprint's id; its title, summary, and styled properties are read from the file when drawn." },
43
+ sketch: { $ref: '#/definitions/sketch' },
44
+ sticky: { $ref: '#/definitions/sticky' },
45
+ text: { $ref: '#/definitions/text' },
46
+ annotation: { $ref: '#/definitions/annotation' },
47
+ frame: { type: 'string', description: 'The frame this node is attached to; it moves with the frame.' },
48
+ link: { type: 'object', additionalProperties: false, properties: { page: { type: 'string' }, canvas: { type: 'string' } }, description: 'What the card opens: another page, or another canvas.' },
49
+ locked: { type: 'boolean' },
50
+ style: { $ref: '#/definitions/style' },
51
+ styled: { type: 'boolean', description: 'Styled by hand: a change to a styling property asks before the default is applied.' },
52
+ box: { type: 'object', required: ['w', 'h'], additionalProperties: false, properties: { w: { type: 'number', minimum: 40 }, h: { type: 'number', minimum: 24 } }, description: 'An exact size; text wraps and scrolls inside it.' },
53
+ },
54
+ },
55
+ sketch: {
56
+ type: 'object',
57
+ required: ['label'],
58
+ additionalProperties: false,
59
+ description: 'A node that is not a blueprint yet; eidos new promotes it.',
60
+ properties: {
61
+ label: { type: 'string', description: 'The title; may be empty until written.' },
62
+ kind: { type: 'string' },
63
+ status: { type: 'string' },
64
+ note: { type: 'string', description: 'The description.' },
65
+ properties: { type: 'object', additionalProperties: { type: 'string' }, description: 'Values of schema properties that apply to every collection, held until promotion.' },
66
+ },
67
+ },
68
+ sticky: { type: 'object', required: ['text'], additionalProperties: false, properties: { text: { type: 'string' }, color: { type: 'string', enum: ['yellow', 'pink', 'blue', 'green', 'purple', 'orange'] } } },
69
+ text: { type: 'object', required: ['text'], additionalProperties: false, properties: { text: { type: 'string' }, size, font: { type: 'integer', minimum: 8, maximum: 64, description: 'An exact size in pixels; wins over size.' } } },
70
+ annotation: { type: 'object', required: ['text'], additionalProperties: false, description: 'A note that points at a node or a frame through an edge from it; one that points at nothing is removed.', properties: { text: { type: 'string' }, color: hex } },
71
+ style: {
72
+ type: 'object',
73
+ additionalProperties: false,
74
+ description: "A node's own look over the default its properties give it.",
75
+ properties: {
76
+ shape: { type: 'string', enum: ['box', 'pill', 'parallelogram'] },
77
+ color: { ...hex, description: 'The highlight: the bar and the border.' },
78
+ fill: { type: 'string', enum: ['solid', 'tint', 'none'] },
79
+ fill_color: hex,
80
+ size,
81
+ },
82
+ },
83
+ edge: {
84
+ type: 'object',
85
+ required: ['from', 'to'],
86
+ additionalProperties: false,
87
+ description: 'A flow between two nodes, or to a frame border as frame:<id>.',
88
+ properties: {
89
+ from: { type: 'string' },
90
+ to: { type: 'string' },
91
+ label: { type: 'string' },
92
+ label_at: { type: 'number', minimum: 0, maximum: 1, description: 'Where along the line the label sits.' },
93
+ bend: { type: 'number', description: 'Where the middle segment turns, along its axis.' },
94
+ from_anchor: { type: 'string', enum: [...ANCHORS] },
95
+ to_anchor: { type: 'string', enum: [...ANCHORS] },
96
+ color: hex,
97
+ style: { type: 'string', enum: ['solid', 'dashed'] },
98
+ start: { type: 'string', enum: [...EDGE_CAPS] },
99
+ end: { type: 'string', enum: [...EDGE_CAPS], description: 'A solid arrowhead (triangle) when absent.' },
100
+ },
101
+ },
102
+ frame: {
103
+ type: 'object',
104
+ required: ['id', 'title'],
105
+ additionalProperties: false,
106
+ description: 'A region drawn around nodes; what is attached moves with it.',
107
+ properties: {
108
+ id: { type: 'string' },
109
+ title: { type: 'string' },
110
+ x: { type: 'number' },
111
+ y: { type: 'number' },
112
+ w: { type: 'number' },
113
+ h: { type: 'number' },
114
+ color: { type: 'string', enum: ['none', 'sand', 'sky', 'mint', 'rose', 'lilac'] },
115
+ locked: { type: 'boolean' },
116
+ frame: { type: 'string', description: 'The frame this frame is attached to.' },
117
+ },
118
+ },
119
+ comment: {
120
+ type: 'object',
121
+ required: ['id', 'x', 'y', 'messages'],
122
+ additionalProperties: false,
123
+ description: 'A thread pinned to a point, or to a node with x and y as the offset from it.',
124
+ properties: {
125
+ id: { type: 'string' },
126
+ x: { type: 'number' },
127
+ y: { type: 'number' },
128
+ node: { type: 'string' },
129
+ resolved: { type: 'boolean' },
130
+ messages: {
131
+ type: 'array',
132
+ items: {
133
+ type: 'object',
134
+ required: ['author', 'text', 'at'],
135
+ additionalProperties: false,
136
+ properties: {
137
+ author: { type: 'string' },
138
+ role: { type: 'string', description: 'The role the author held in this root when they wrote it.' },
139
+ user: { type: 'string', description: 'The user (alias, else name) from the root\'s settings, when one was known.' },
140
+ text: { type: 'string' },
141
+ at: { type: 'string', description: 'When, as an ISO 8601 time.' },
142
+ },
143
+ },
144
+ },
145
+ },
146
+ },
147
+ },
148
+ };