eidosmd 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/browser/dist/assets/index-Cc3cNWHY.css +1 -0
- package/browser/dist/assets/index-DQgCQRa5.js +46 -0
- package/browser/dist/index.html +2 -2
- package/dist/src/commands/check.js +9 -3
- package/dist/src/commands/configure.js +201 -0
- package/dist/src/commands/framework.js +15 -4
- package/dist/src/commands/init.js +4 -0
- package/dist/src/commands/property.js +125 -0
- package/dist/src/commands/setup.js +24 -12
- package/dist/src/commands/version.js +2 -2
- package/dist/src/core/canvas.js +59 -49
- package/dist/src/core/check.js +101 -26
- package/dist/src/core/edits.js +1381 -0
- package/dist/src/core/framework-markdown.js +9 -3
- package/dist/src/core/framework-model.js +43 -8
- package/dist/src/core/framework-structured.js +104 -28
- package/dist/src/core/frontmatter.js +61 -1
- package/dist/src/core/git.js +28 -3
- package/dist/src/core/links.js +87 -0
- package/dist/src/core/markdown.js +16 -9
- package/dist/src/core/migrate.js +91 -15
- package/dist/src/core/regions.js +117 -0
- package/dist/src/core/scaffold.js +15 -9
- package/dist/src/core/seed.js +56 -31
- package/dist/src/core/server.js +204 -31
- package/dist/src/core/settings.js +63 -12
- package/dist/src/core/store.js +73 -17
- package/dist/src/core/versions.js +10 -5
- package/dist/src/program.js +296 -11
- package/instructions/authoring.md +4 -2
- package/instructions/configuring.md +27 -11
- package/instructions/overview.md +6 -4
- package/instructions/validating.md +6 -4
- package/package.json +1 -1
- package/standard/EIDOS.md +135 -193
- package/standard/seeds/README.md +12 -16
- package/standard/seeds/book/Framework.yaml +30 -50
- package/standard/seeds/book/README.md +2 -1
- package/standard/seeds/book/_gitignore +7 -1
- package/standard/seeds/research/Framework.yaml +30 -50
- package/standard/seeds/research/README.md +2 -1
- package/standard/seeds/research/_gitignore +7 -1
- package/standard/seeds/software/Framework.yaml +31 -51
- package/standard/seeds/software/README.md +2 -1
- package/standard/seeds/software/_gitignore +7 -1
- package/browser/dist/assets/index-C2NMN_D4.css +0 -1
- package/browser/dist/assets/index-C65k1ihb.js +0 -46
package/browser/dist/index.html
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<meta name="color-scheme" content="dark light" />
|
|
7
7
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
|
8
8
|
<title>Eidos</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-DQgCQRa5.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Cc3cNWHY.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="app"></div>
|
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
// whether anything was an error.
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { checkRoot } from '../core/check.js';
|
|
6
|
+
import { readSettings } from '../core/settings.js';
|
|
6
7
|
import { standardVersion } from '../paths.js';
|
|
7
8
|
import { CliError, EXIT_FINDINGS, EXIT_OK, print, printJson } from '../output.js';
|
|
9
|
+
// Whether a warning fails this run: the flag when given, else the root's setting.
|
|
10
|
+
export function strictFor(root, flag) {
|
|
11
|
+
return flag ?? readSettings(root).shared.check.strict;
|
|
12
|
+
}
|
|
8
13
|
function mark(finding) {
|
|
9
14
|
return finding.level === 'error' ? '✗' : '⚠';
|
|
10
15
|
}
|
|
@@ -22,11 +27,12 @@ export function runCheck(context, paths, options) {
|
|
|
22
27
|
}
|
|
23
28
|
}
|
|
24
29
|
const report = checkRoot(context.framework, context.blueprints, { only, standardVersion: standardVersion() });
|
|
25
|
-
const
|
|
30
|
+
const strict = strictFor(context.root, options.strict);
|
|
31
|
+
const failed = report.errors > 0 || (strict && report.warnings > 0);
|
|
26
32
|
if (options.json) {
|
|
27
33
|
printJson({
|
|
28
34
|
ok: !failed,
|
|
29
|
-
strict
|
|
35
|
+
strict,
|
|
30
36
|
root: report.root,
|
|
31
37
|
eidos_version: report.eidosVersion,
|
|
32
38
|
naming: report.naming,
|
|
@@ -55,7 +61,7 @@ export function runCheck(context, paths, options) {
|
|
|
55
61
|
}
|
|
56
62
|
else {
|
|
57
63
|
const parts = [`${report.errors} error${report.errors === 1 ? '' : 's'}`, `${report.warnings} warning${report.warnings === 1 ? '' : 's'}`];
|
|
58
|
-
print(`${failed ? '✗' : '⚠'} ${parts.join(', ')}${
|
|
64
|
+
print(`${failed ? '✗' : '⚠'} ${parts.join(', ')}${strict && report.errors === 0 && report.warnings > 0 ? (options.strict === undefined ? ' (strict: this root fails on warnings)' : ' (--strict)') : ''}`);
|
|
59
65
|
}
|
|
60
66
|
return failed ? EXIT_FINDINGS : EXIT_OK;
|
|
61
67
|
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// `eidos configure:<noun> <operation>`: the framework edited from the shell.
|
|
2
|
+
// Every mutating command builds a plan (src/core/edits.ts), prints it whole,
|
|
3
|
+
// and asks before writing; --dry-run prints and stops, --yes proceeds
|
|
4
|
+
// without the question, --force proceeds through what needs forcing. With
|
|
5
|
+
// no terminal and no switch the command refuses and names the switch, so an
|
|
6
|
+
// agent learns it from the error instead of hanging on a prompt.
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { applyPlan, editContext, EditError, planJson } from '../core/edits.js';
|
|
9
|
+
import { listRoles } from '../core/me.js';
|
|
10
|
+
import { readSettings } from '../core/settings.js';
|
|
11
|
+
import { standardVersion } from '../paths.js';
|
|
12
|
+
import { askYesNo, interactive } from './setup.js';
|
|
13
|
+
import { CliError, EXIT_FINDINGS, EXIT_OK, EXIT_USAGE, print, printJson } from '../output.js';
|
|
14
|
+
const describe = (step) => {
|
|
15
|
+
switch (step.kind) {
|
|
16
|
+
case 'document':
|
|
17
|
+
return `~ ${step.path} ${step.what}`;
|
|
18
|
+
case 'key':
|
|
19
|
+
if (step.op === 'rename')
|
|
20
|
+
return `~ ${step.path} ${step.key} → ${step.to ?? ''}`;
|
|
21
|
+
if (step.op === 'delete')
|
|
22
|
+
return `~ ${step.path} drop ${step.key}`;
|
|
23
|
+
return `~ ${step.path} ${step.key}: ${JSON.stringify(step.before ?? null)} → ${JSON.stringify(step.after ?? '')}`;
|
|
24
|
+
case 'links':
|
|
25
|
+
return `~ ${step.path} ${step.count} link${step.count === 1 ? '' : 's'} rewritten`;
|
|
26
|
+
case 'write':
|
|
27
|
+
return `+ ${step.path} ${step.what}`;
|
|
28
|
+
case 'move':
|
|
29
|
+
return `> ${step.from} → ${step.to}`;
|
|
30
|
+
case 'remove':
|
|
31
|
+
return `- ${step.path} ${step.what}`;
|
|
32
|
+
case 'settings':
|
|
33
|
+
return `~ ${step.path} ${step.what}`;
|
|
34
|
+
default:
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
function printPlan(plan) {
|
|
39
|
+
print(`plan: ${plan.command}`);
|
|
40
|
+
if (plan.steps.length === 0)
|
|
41
|
+
print(' nothing to change');
|
|
42
|
+
for (const step of plan.steps)
|
|
43
|
+
print(` ${describe(step)}`);
|
|
44
|
+
if (plan.lost.length > 0) {
|
|
45
|
+
print('', 'values that will be gone:');
|
|
46
|
+
for (const lost of plan.lost)
|
|
47
|
+
print(` ${lost.path} ${lost.key}: ${JSON.stringify(lost.value)}`);
|
|
48
|
+
}
|
|
49
|
+
if (plan.notes.length > 0) {
|
|
50
|
+
print('', 'note:');
|
|
51
|
+
for (const note of plan.notes)
|
|
52
|
+
print(` ${note}`);
|
|
53
|
+
}
|
|
54
|
+
if (plan.conflicts.length > 0) {
|
|
55
|
+
print('', 'conflicts:');
|
|
56
|
+
for (const conflict of plan.conflicts)
|
|
57
|
+
print(` ✗ ${conflict.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Build, show, confirm, apply, then index and check: the one path every
|
|
61
|
+
// mutating configure command takes.
|
|
62
|
+
export async function runPlan(context, build, options) {
|
|
63
|
+
let plan;
|
|
64
|
+
try {
|
|
65
|
+
plan = build(editContext(context.root));
|
|
66
|
+
}
|
|
67
|
+
catch (cause) {
|
|
68
|
+
if (cause instanceof EditError)
|
|
69
|
+
throw new CliError(cause.message);
|
|
70
|
+
throw cause;
|
|
71
|
+
}
|
|
72
|
+
if (!options.json)
|
|
73
|
+
printPlan(plan);
|
|
74
|
+
const blocking = plan.conflicts.filter((conflict) => !conflict.force || !options.force);
|
|
75
|
+
if (blocking.length > 0) {
|
|
76
|
+
if (options.json)
|
|
77
|
+
printJson({ ok: false, applied: false, ...planJson(plan) });
|
|
78
|
+
const forcible = blocking.every((conflict) => conflict.force);
|
|
79
|
+
throw new CliError(forcible ? 'the plan needs --force to proceed (see conflicts)' : `the plan cannot proceed: ${blocking.map((conflict) => conflict.message).join('; ')}`);
|
|
80
|
+
}
|
|
81
|
+
if (options.dryRun) {
|
|
82
|
+
if (options.json)
|
|
83
|
+
printJson({ ok: true, applied: false, dry_run: true, ...planJson(plan) });
|
|
84
|
+
else
|
|
85
|
+
print('', 'dry run: nothing written');
|
|
86
|
+
return EXIT_OK;
|
|
87
|
+
}
|
|
88
|
+
if (plan.steps.length === 0) {
|
|
89
|
+
if (options.json)
|
|
90
|
+
printJson({ ok: true, applied: false, ...planJson(plan) });
|
|
91
|
+
return EXIT_OK;
|
|
92
|
+
}
|
|
93
|
+
if (!options.yes && !options.force) {
|
|
94
|
+
if (!interactive()) {
|
|
95
|
+
if (options.json)
|
|
96
|
+
printJson({ ok: false, applied: false, ...planJson(plan) });
|
|
97
|
+
throw new CliError(plan.destructive ? 'not a terminal: values or files would be gone; run with --force to proceed, or --dry-run to see the plan' : 'not a terminal: run with --yes to proceed, or --dry-run to see the plan', EXIT_USAGE);
|
|
98
|
+
}
|
|
99
|
+
const go = await askYesNo(plan.destructive ? 'Proceed? Values or files listed above will be gone' : 'Proceed?', !plan.destructive);
|
|
100
|
+
if (!go) {
|
|
101
|
+
print('nothing written');
|
|
102
|
+
return EXIT_OK;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else if (plan.destructive && !options.force && !interactive()) {
|
|
106
|
+
if (options.json)
|
|
107
|
+
printJson({ ok: false, applied: false, ...planJson(plan) });
|
|
108
|
+
throw new CliError('not a terminal: values or files would be gone; --yes is not enough, run with --force', EXIT_USAGE);
|
|
109
|
+
}
|
|
110
|
+
const applied = applyPlan(plan, standardVersion());
|
|
111
|
+
const strict = readSettings(context.root).shared.check.strict;
|
|
112
|
+
const failed = applied.errors > 0 || (strict && applied.warnings > 0);
|
|
113
|
+
if (options.json) {
|
|
114
|
+
printJson({ ok: !failed, applied: true, ...planJson(plan), new_findings: applied.newFindings, errors: applied.errors, warnings: applied.warnings, strict });
|
|
115
|
+
return failed ? EXIT_FINDINGS : EXIT_OK;
|
|
116
|
+
}
|
|
117
|
+
print('', `✓ ${plan.steps.length} step${plan.steps.length === 1 ? '' : 's'} applied; index rewritten`);
|
|
118
|
+
if (applied.newFindings.length > 0) {
|
|
119
|
+
print(' check now also reports:');
|
|
120
|
+
for (const finding of applied.newFindings)
|
|
121
|
+
print(` ${finding.level === 'error' ? '✗' : '⚠'} ${finding.path ?? '(root)'} ${finding.code} ${finding.message}`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
print(' check: nothing new');
|
|
125
|
+
}
|
|
126
|
+
return failed ? EXIT_FINDINGS : EXIT_OK;
|
|
127
|
+
}
|
|
128
|
+
// ---- show -------------------------------------------------------------------
|
|
129
|
+
export function showProperty(context, name, json) {
|
|
130
|
+
const all = [...context.framework.schema.core, ...context.framework.schema.custom, ...Object.values(context.framework.schema.tools).flat()];
|
|
131
|
+
const chosen = name === undefined ? all : all.filter((property) => property.name === name);
|
|
132
|
+
if (name !== undefined && chosen.length === 0)
|
|
133
|
+
throw new CliError(`no property '${name}' (eidos framework lists them)`);
|
|
134
|
+
const view = chosen.map((property) => ({ name: property.name, type: property.type, applies_to: property.appliesTo, required: property.required, ...(property.options ? { options: property.options } : {}), meaning: property.meaning, owner: property.owner, ...(property.tools ? { tools: property.tools } : {}) }));
|
|
135
|
+
if (json) {
|
|
136
|
+
printJson(name === undefined ? view : view[0]);
|
|
137
|
+
return EXIT_OK;
|
|
138
|
+
}
|
|
139
|
+
for (const property of view) {
|
|
140
|
+
print(`${property.name} (${property.type}, ${property.applies_to === 'all' ? 'all' : property.applies_to.join(', ')}${property.required ? ', required' : ''}; ${property.owner === 'eidos' ? "the standard's" : property.owner === 'custom' ? 'yours' : `${property.owner}'s`})`);
|
|
141
|
+
if (property.meaning)
|
|
142
|
+
print(` ${property.meaning}`);
|
|
143
|
+
if (property.options)
|
|
144
|
+
print(` options: ${property.options.join(', ')}`);
|
|
145
|
+
}
|
|
146
|
+
return EXIT_OK;
|
|
147
|
+
}
|
|
148
|
+
export function showCollection(context, name, json) {
|
|
149
|
+
const chosen = name === undefined ? context.framework.collections : context.framework.collections.filter((collection) => collection.name.toLowerCase() === name.toLowerCase());
|
|
150
|
+
if (name !== undefined && chosen.length === 0)
|
|
151
|
+
throw new CliError(`no collection '${name}'`);
|
|
152
|
+
const view = chosen.map((collection) => ({ name: collection.name, description: collection.description, unit: collection.variants.find((variant) => variant.isDefault)?.unit ?? null, variants: collection.variants.map((variant) => ({ name: variant.name, template: variant.template, default: variant.isDefault, description: variant.description })), grouping: collection.grouping }));
|
|
153
|
+
if (json) {
|
|
154
|
+
printJson(name === undefined ? view : view[0]);
|
|
155
|
+
return EXIT_OK;
|
|
156
|
+
}
|
|
157
|
+
for (const collection of view) {
|
|
158
|
+
print(`${collection.name}${collection.unit ? ` (unit ${collection.unit})` : ''}`);
|
|
159
|
+
if (collection.description)
|
|
160
|
+
print(` ${collection.description}`);
|
|
161
|
+
print(` variants: ${collection.variants.map((variant) => `${variant.name}${variant.default ? ' (default)' : ''}`).join(', ')}`);
|
|
162
|
+
if (collection.grouping)
|
|
163
|
+
print(` ${collection.grouping.label}${collection.grouping.property ? ` (${collection.grouping.property})` : ''}: ${collection.grouping.groups.map((group) => group.name).join(', ') || 'none yet'}`);
|
|
164
|
+
}
|
|
165
|
+
return EXIT_OK;
|
|
166
|
+
}
|
|
167
|
+
// Every folder at the root as the framework declares it, with its type; a
|
|
168
|
+
// collection's variants and grouping are `configure:collection show`'s.
|
|
169
|
+
export function showFolder(context, name, json) {
|
|
170
|
+
const chosen = name === undefined ? context.framework.folders : context.framework.folders.filter((folder) => folder.name.toLowerCase() === name.toLowerCase());
|
|
171
|
+
if (name !== undefined && chosen.length === 0)
|
|
172
|
+
throw new CliError(`no folder '${name}' (eidos framework lists them)`);
|
|
173
|
+
const view = chosen.map((folder) => ({ name: folder.name, type: folder.type, description: folder.description }));
|
|
174
|
+
if (json) {
|
|
175
|
+
printJson(name === undefined ? view : view[0]);
|
|
176
|
+
return EXIT_OK;
|
|
177
|
+
}
|
|
178
|
+
for (const folder of view) {
|
|
179
|
+
print(`${folder.name} (${folder.type})`);
|
|
180
|
+
if (folder.description)
|
|
181
|
+
print(` ${folder.description}`);
|
|
182
|
+
}
|
|
183
|
+
return EXIT_OK;
|
|
184
|
+
}
|
|
185
|
+
export function showRole(context, name, json) {
|
|
186
|
+
const roles = listRoles(context.root);
|
|
187
|
+
const chosen = name === undefined ? roles : roles.filter((role) => role.name === name);
|
|
188
|
+
if (name !== undefined && chosen.length === 0)
|
|
189
|
+
throw new CliError(`no role '${name}' (eidos roles lists them)`);
|
|
190
|
+
if (json) {
|
|
191
|
+
printJson(name === undefined ? chosen.map((role) => ({ name: role.name, title: role.title, summary: role.summary })) : { name: chosen[0]?.name, title: chosen[0]?.title, summary: chosen[0]?.summary, text: readFileSync(chosen[0]?.file ?? '', 'utf8') });
|
|
192
|
+
return EXIT_OK;
|
|
193
|
+
}
|
|
194
|
+
if (name === undefined) {
|
|
195
|
+
for (const role of chosen)
|
|
196
|
+
print(`${role.name} ${role.title}${role.summary ? `: ${role.summary}` : ''}`);
|
|
197
|
+
return EXIT_OK;
|
|
198
|
+
}
|
|
199
|
+
process.stdout.write(readFileSync(chosen[0]?.file ?? '', 'utf8'));
|
|
200
|
+
return EXIT_OK;
|
|
201
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// summary, as JSON an agent can read instead of parsing the file, or as the
|
|
3
3
|
// document itself, normalized.
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { unitOf } from '../core/framework-model.js';
|
|
5
|
+
import { isCollection, unitOf } from '../core/framework-model.js';
|
|
6
6
|
import { frameworkToDocument, serializeDocument } from '../core/framework-structured.js';
|
|
7
7
|
import { CliError, EXIT_OK, print, printJson } from '../output.js';
|
|
8
8
|
export function frameworkJson(framework) {
|
|
@@ -65,8 +65,15 @@ export function runFramework(context, options) {
|
|
|
65
65
|
print(` groups: ${collection.grouping.groups.map((group) => group.name).join(', ')}`);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
const plain = framework.folders.filter((folder) => !isCollection(folder));
|
|
69
|
+
if (plain.length > 0) {
|
|
70
|
+
print('', 'Folders');
|
|
71
|
+
for (const folder of plain) {
|
|
72
|
+
print(` ${folder.name} (${folder.type})${folder.description ? ` — ${folder.description}` : ''}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
68
75
|
print('', 'Properties');
|
|
69
|
-
print(` core: ${framework.schema.core.map((property) => property.name).join(', ')}`);
|
|
76
|
+
print(` core: ${framework.schema.core.map((property) => `${property.name}${property.required ? ' (required)' : ''}`).join(', ')}`);
|
|
70
77
|
if (framework.schema.custom.length === 0) {
|
|
71
78
|
print(' custom: none');
|
|
72
79
|
}
|
|
@@ -75,14 +82,18 @@ export function runFramework(context, options) {
|
|
|
75
82
|
for (const property of framework.schema.custom) {
|
|
76
83
|
const scope = property.appliesTo === 'all' ? 'all' : property.appliesTo.join(', ');
|
|
77
84
|
const tools = property.tools ? ` [${Object.keys(property.tools).join(', ')}]` : '';
|
|
78
|
-
print(` ${property.name} (${property.type}, ${scope}) — ${property.meaning}${tools}`);
|
|
85
|
+
print(` ${property.name} (${property.type}, ${scope}${property.required ? ', required' : ''}) — ${property.meaning}${tools}`);
|
|
86
|
+
if (property.options)
|
|
87
|
+
print(` options: ${property.options.join(', ')}`);
|
|
79
88
|
}
|
|
80
89
|
}
|
|
81
90
|
for (const [tool, block] of Object.entries(framework.schema.tools)) {
|
|
82
91
|
print(` ${tool} (the tool's own block):`);
|
|
83
92
|
for (const property of block) {
|
|
84
93
|
const scope = property.appliesTo === 'all' ? 'all' : property.appliesTo.join(', ');
|
|
85
|
-
print(` ${property.name} (${property.type}, ${scope}) — ${property.meaning}`);
|
|
94
|
+
print(` ${property.name} (${property.type}, ${scope}${property.required ? ', required' : ''}) — ${property.meaning}`);
|
|
95
|
+
if (property.options)
|
|
96
|
+
print(` options: ${property.options.join(', ')}`);
|
|
86
97
|
}
|
|
87
98
|
}
|
|
88
99
|
if (framework.vocabulary.length > 0) {
|
|
@@ -42,6 +42,7 @@ export function runInit(rootArg, options, cwd = process.cwd()) {
|
|
|
42
42
|
naming: result.naming,
|
|
43
43
|
framework_file: result.frameworkFile,
|
|
44
44
|
collections: result.collections,
|
|
45
|
+
folders: result.folders,
|
|
45
46
|
grouped_collection: result.groupedCollection,
|
|
46
47
|
groups: result.groups,
|
|
47
48
|
scaffolded: result.scaffolded,
|
|
@@ -59,6 +60,9 @@ export function runInit(rootArg, options, cwd = process.cwd()) {
|
|
|
59
60
|
collections += `; groups under ${result.groupedCollection}: ${result.groups.join(', ')}`;
|
|
60
61
|
}
|
|
61
62
|
print(collections);
|
|
63
|
+
if (result.folders.length > 0) {
|
|
64
|
+
print(` folders: ${result.folders.join(', ')}`);
|
|
65
|
+
}
|
|
62
66
|
if (result.scaffolded.length > 0) {
|
|
63
67
|
print(` blank frames: ${result.scaffolded.join(', ')}`);
|
|
64
68
|
}
|
|
@@ -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('/');
|
|
@@ -8,7 +8,7 @@ import { gitHead, gitUser } from '../core/git.js';
|
|
|
8
8
|
import { listRoles } from '../core/me.js';
|
|
9
9
|
import { cleanAlias, findUser, matchesIdentity, readSettings, writeSettings } from '../core/settings.js';
|
|
10
10
|
import { CliError, EXIT_OK, print, printJson } from '../output.js';
|
|
11
|
-
const interactive = () => process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
11
|
+
export const interactive = () => process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
12
12
|
// One line, with a default shown in brackets; an empty answer takes it.
|
|
13
13
|
async function ask(question, fallback) {
|
|
14
14
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -32,30 +32,41 @@ function describe(root, settings) {
|
|
|
32
32
|
const identity = inRepo ? gitUser(root) : null;
|
|
33
33
|
const lines = [
|
|
34
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)'}`,
|
|
35
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(', ')}`,
|
|
36
37
|
`acting as: ${settings.local.user ?? (identity ? `the git identity (${identity.name})` : 'the role in me.md')}`,
|
|
37
38
|
`files: ${settings.files.shared} (shared), ${settings.files.local} (yours, gitignored)`,
|
|
38
39
|
];
|
|
39
40
|
return lines;
|
|
40
41
|
}
|
|
41
|
-
// After `eidos init
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
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())
|
|
45
51
|
return;
|
|
52
|
+
const settings = readSettings(root);
|
|
46
53
|
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
|
-
|
|
49
|
-
|
|
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)`);
|
|
50
58
|
}
|
|
51
59
|
export async function runSetup(context, options) {
|
|
52
60
|
const root = context.root;
|
|
53
61
|
let settings = readSettings(root);
|
|
54
|
-
const flagged = options.git !== undefined || options.addUser.length > 0 || options.as !== undefined;
|
|
62
|
+
const flagged = options.git !== undefined || options.strict !== undefined || options.addUser.length > 0 || options.as !== undefined;
|
|
55
63
|
if (flagged) {
|
|
56
64
|
const git = onOff(options.git);
|
|
57
65
|
if (options.git !== undefined && git === null)
|
|
58
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');
|
|
59
70
|
const users = settings.shared.users.map((user) => ({ ...user }));
|
|
60
71
|
for (const name of options.addUser) {
|
|
61
72
|
if (name.trim() === '')
|
|
@@ -76,7 +87,7 @@ export async function runSetup(context, options) {
|
|
|
76
87
|
if (options.as !== undefined && options.as !== '' && !findUser(users, options.as))
|
|
77
88
|
throw new CliError(`no user '${options.as}'; add one with --add-user`);
|
|
78
89
|
settings = writeSettings(root, {
|
|
79
|
-
shared: { ...(git === null ? {} : { features: { git } }), ...(options.addUser.length > 0 ? { users } : {}) },
|
|
90
|
+
shared: { ...(git === null ? {} : { features: { git } }), ...(strict === null ? {} : { check: { strict } }), ...(options.addUser.length > 0 ? { users } : {}) },
|
|
80
91
|
...(options.as !== undefined ? { local: { user: options.as === '' ? null : options.as } } : {}),
|
|
81
92
|
});
|
|
82
93
|
}
|
|
@@ -84,6 +95,7 @@ export async function runSetup(context, options) {
|
|
|
84
95
|
print(...describe(root, settings), '');
|
|
85
96
|
const inRepo = gitHead(root) !== null;
|
|
86
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);
|
|
87
99
|
const users = settings.shared.users.map((user) => ({ ...user }));
|
|
88
100
|
const identity = git && inRepo ? gitUser(root) : null;
|
|
89
101
|
let acting = settings.local.user;
|
|
@@ -104,10 +116,10 @@ export async function runSetup(context, options) {
|
|
|
104
116
|
throw new CliError(`no user '${answer}'`);
|
|
105
117
|
acting = answer === '' ? null : answer;
|
|
106
118
|
}
|
|
107
|
-
settings = writeSettings(root, { shared: { features: { git }, users }, local: { user: acting } });
|
|
119
|
+
settings = writeSettings(root, { shared: { features: { git }, check: { strict }, users }, local: { user: acting } });
|
|
108
120
|
}
|
|
109
121
|
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');
|
|
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');
|
|
111
123
|
return EXIT_OK;
|
|
112
124
|
}
|
|
113
125
|
if (options.json) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// name, a commit that exists, and a tag only on --tag. Never proposed; an
|
|
4
4
|
// empty list is the normal state.
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { readVersions, recordVersion,
|
|
6
|
+
import { readVersions, recordVersion, tagPrefix, VersionError, versionsFile } from '../core/versions.js';
|
|
7
7
|
import { CliError, EXIT_OK, print, printJson } from '../output.js';
|
|
8
8
|
export function runVersionList(context, options) {
|
|
9
9
|
const versions = readVersions(context.root);
|
|
@@ -37,7 +37,7 @@ export function runVersionRecord(context, version, options) {
|
|
|
37
37
|
}
|
|
38
38
|
print(`✓ ${recorded.version.version} → ${recorded.version.commit}${recorded.version.tag ? ` tagged ${recorded.version.tag}` : ''}`);
|
|
39
39
|
if (!recorded.version.tag) {
|
|
40
|
-
print(` (no tag; --tag would have created ${
|
|
40
|
+
print(` (no tag; --tag would have created ${tagPrefix(context.root)}${recorded.version.version})`);
|
|
41
41
|
}
|
|
42
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
43
|
return EXIT_OK;
|