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/dist/src/core/server.js
CHANGED
|
@@ -12,10 +12,11 @@ import path from 'node:path';
|
|
|
12
12
|
import { marked } from 'marked';
|
|
13
13
|
import { checkRoot } from './check.js';
|
|
14
14
|
import { buildIndexes } from './index-leaf.js';
|
|
15
|
+
import { applyPlan, editContext, EditError, moveWithLinks, planDocAdd, planDocRemove, renameRoleFiles } from './edits.js';
|
|
15
16
|
import { embeddedIndexOf, frameworkToDocument, parseFrameworkStructured, serializeDocument, withEmbeddedIndex } from './framework-structured.js';
|
|
16
17
|
import { FRAMEWORK_DIR, FrameworkError, propertiesFor } from './framework.js';
|
|
17
|
-
import { readVersions, recordVersion, VersionError } from './versions.js';
|
|
18
|
-
import { gitHead, gitLog, gitShowFile, gitUser } from './git.js';
|
|
18
|
+
import { readVersions, recordVersion, tagPrefix, VersionError } from './versions.js';
|
|
19
|
+
import { moveFile, gitHead, gitLog, gitShowFile, gitUser } from './git.js';
|
|
19
20
|
import { actingUser, matchesIdentity, readSettings, writeSettings } from './settings.js';
|
|
20
21
|
import { formatFrontmatter } from './frontmatter.js';
|
|
21
22
|
import { findRole, isRoleName, listRoles, meFile, readActor, renderActor, roleFile, writeActor } from './me.js';
|
|
@@ -23,6 +24,7 @@ import { scaffoldBlueprint, ScaffoldError } from './scaffold.js';
|
|
|
23
24
|
import { blueprintId, blueprintTitle, propertyString, resolveVariant } from './blueprint.js';
|
|
24
25
|
import { loadTemplate, stripFrontmatter } from './template.js';
|
|
25
26
|
import { unitOf } from './framework-model.js';
|
|
27
|
+
import { regionsAsHtml } from './regions.js';
|
|
26
28
|
import { Store } from './store.js';
|
|
27
29
|
import { CliError, today } from '../output.js';
|
|
28
30
|
import { browserDir, packageVersion, readStandard, standardVersion } from '../paths.js';
|
|
@@ -91,29 +93,50 @@ function findingsFor(context, blueprint) {
|
|
|
91
93
|
return checkRoot(context.framework, context.blueprints, { only: new Set([blueprint.path]), standardVersion: standardVersion() }).findings.filter((finding) => finding.path === blueprint.rel);
|
|
92
94
|
}
|
|
93
95
|
function propertyView(property) {
|
|
94
|
-
return { name: property.name, type: property.type, core: property.core, owner: property.owner, meaning: property.meaning, applies_to: property.appliesTo, canvas: property.canvas ?? null, tools: property.tools ?? {} };
|
|
96
|
+
return { name: property.name, type: property.type, core: property.core, owner: property.owner, required: property.required, options: property.options ?? null, meaning: property.meaning, applies_to: property.appliesTo, canvas: property.canvas ?? null, tools: property.tools ?? {} };
|
|
95
97
|
}
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
+
// Markdown a person reads in the root, rendered: a tool's region is shown as
|
|
99
|
+
// it is, never rendered, since its contents are the tool's to draw.
|
|
100
|
+
function renderMarkdown(text) {
|
|
101
|
+
return marked.parse(regionsAsHtml(text), { async: false });
|
|
102
|
+
}
|
|
103
|
+
// The values a property already takes across a collection, for the editor
|
|
104
|
+
// to offer; a canvas hint's keys count as values too. A property that
|
|
105
|
+
// declares options offers them first, in their declared order, then any
|
|
106
|
+
// value in use that the list does not hold, so a filter still finds it.
|
|
98
107
|
function knownValues(context, collectionName) {
|
|
99
108
|
const out = {};
|
|
100
109
|
for (const property of propertiesFor(context.framework, collectionName)) {
|
|
101
|
-
if (property.type
|
|
110
|
+
if (!['Text', 'List'].includes(property.type) || ['id', 'title', 'summary'].includes(property.name))
|
|
111
|
+
continue;
|
|
112
|
+
if (property.type === 'List' && !property.options)
|
|
102
113
|
continue;
|
|
103
114
|
const values = new Set();
|
|
104
|
-
for (const key of
|
|
115
|
+
for (const key of Object.keys(property.canvas?.styles ?? {}))
|
|
105
116
|
values.add(key);
|
|
106
117
|
for (const blueprint of context.blueprints) {
|
|
107
118
|
if (blueprint.collection.name !== collectionName)
|
|
108
119
|
continue;
|
|
109
|
-
const value
|
|
110
|
-
if (value)
|
|
120
|
+
for (const value of heldValues(blueprint, property.name))
|
|
111
121
|
values.add(value);
|
|
112
122
|
}
|
|
113
|
-
|
|
114
|
-
|
|
123
|
+
const listed = withOptionsFirst(property, values);
|
|
124
|
+
if (listed.length > 0)
|
|
125
|
+
out[property.name] = listed;
|
|
115
126
|
}
|
|
116
|
-
return
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
// The strings a blueprint holds under a property: the Text value, or a List's elements.
|
|
130
|
+
function heldValues(blueprint, name) {
|
|
131
|
+
const held = blueprint.properties?.[name];
|
|
132
|
+
if (Array.isArray(held))
|
|
133
|
+
return held.filter((item) => typeof item === 'string' && item.trim() !== '');
|
|
134
|
+
const value = propertyString(blueprint, name);
|
|
135
|
+
return value === null ? [] : [value];
|
|
136
|
+
}
|
|
137
|
+
function withOptionsFirst(property, values) {
|
|
138
|
+
const rest = [...values].filter((value) => !property.options?.includes(value)).sort((a, b) => a.localeCompare(b));
|
|
139
|
+
return [...(property.options ?? []), ...rest];
|
|
117
140
|
}
|
|
118
141
|
function blueprintDetail(context, blueprint) {
|
|
119
142
|
const raw = readFileSync(blueprint.path, 'utf8');
|
|
@@ -128,7 +151,7 @@ function blueprintDetail(context, blueprint) {
|
|
|
128
151
|
sections: blueprint.sections.map((section) => section.heading.text),
|
|
129
152
|
template_sections: template?.sections ?? [],
|
|
130
153
|
body: blueprint.body,
|
|
131
|
-
html:
|
|
154
|
+
html: renderMarkdown(blueprint.body),
|
|
132
155
|
raw,
|
|
133
156
|
findings: findingsFor(context, blueprint),
|
|
134
157
|
};
|
|
@@ -150,14 +173,15 @@ function topLevelFile(context, relPath) {
|
|
|
150
173
|
throw new HttpError(404, `no top-level document at ${wanted}`);
|
|
151
174
|
}
|
|
152
175
|
const doc = context.framework.topLevel.find((entry) => path.resolve(path.dirname(context.framework.file), entry.path) === file);
|
|
153
|
-
return { file, rel: rel(context.root, file), title: doc?.title ?? path.basename(file, '.md') };
|
|
176
|
+
return { file, rel: rel(context.root, file), title: doc?.title ?? path.basename(file, '.md'), description: doc?.description ?? '', declared: doc !== undefined };
|
|
154
177
|
}
|
|
155
178
|
function docView(context, relPath) {
|
|
156
|
-
const { file, rel: relative, title } = topLevelFile(context, relPath);
|
|
179
|
+
const { file, rel: relative, title, description, declared } = topLevelFile(context, relPath);
|
|
157
180
|
if (!existsSync(file))
|
|
158
181
|
throw new HttpError(404, `${relative} does not exist`);
|
|
159
182
|
const raw = readFileSync(file, 'utf8');
|
|
160
|
-
|
|
183
|
+
const git = gitInfo(context.root);
|
|
184
|
+
return { path: relative, title, description, declared, html: renderMarkdown(raw), raw, git: git.available, history: git.available ? gitLog(context.root, relative, 20) : [] };
|
|
161
185
|
}
|
|
162
186
|
function rootSnapshot(context, store) {
|
|
163
187
|
const actor = readActor(context.root);
|
|
@@ -169,16 +193,19 @@ function rootSnapshot(context, store) {
|
|
|
169
193
|
cli_version: packageVersion(),
|
|
170
194
|
standard_version: standardVersion(),
|
|
171
195
|
search_engine: store.engine,
|
|
196
|
+
// the machine the server runs on, for the page to name its file manager
|
|
197
|
+
platform: process.platform,
|
|
172
198
|
git_head: git.head,
|
|
173
199
|
git,
|
|
174
200
|
settings,
|
|
175
201
|
framework: { file: rel(context.root, context.framework.file), format: context.framework.format, problems: context.framework.problems, ...frameworkToDocument(context.framework) },
|
|
176
202
|
blueprints: context.blueprints.map(blueprintSummary),
|
|
177
203
|
roles: listRoles(context.root).map((role) => ({ name: role.name, title: role.title, summary: role.summary })),
|
|
178
|
-
// the health check's totals, for the sidebar's badge
|
|
204
|
+
// the health check's totals, for the sidebar's badge, and whether this
|
|
205
|
+
// root counts a warning as a failure
|
|
179
206
|
check: (() => {
|
|
180
207
|
const report = checkRoot(context.framework, context.blueprints, { standardVersion: standardVersion() });
|
|
181
|
-
return { errors: report.errors, warnings: report.warnings };
|
|
208
|
+
return { errors: report.errors, warnings: report.warnings, strict: settings.shared.check.strict };
|
|
182
209
|
})(),
|
|
183
210
|
canvases: listCanvases(context.framework).map((canvas) => ({ id: canvas.id, title: canvas.title, pages: canvas.pages.length })),
|
|
184
211
|
actor: { exists: actor.exists, role: actor.actor.role, ownership: actor.actor.ownership, experience: actor.actor.experience, capacity: actor.actor.capacity, path: rel(context.root, meFile(context.root)) },
|
|
@@ -218,6 +245,8 @@ function collectionTable(context, name) {
|
|
|
218
245
|
description: collection.description,
|
|
219
246
|
unit: unitOf(collection),
|
|
220
247
|
framing: collection.framing,
|
|
248
|
+
// variants whose template file does not exist: nothing to scaffold a new blueprint from
|
|
249
|
+
missing_templates: collection.variants.filter((variant) => !existsSync(path.join(context.framework.root, FRAMEWORK_DIR, variant.template))).map((variant) => variant.name),
|
|
221
250
|
variants: collection.variants.map((variant) => ({ name: variant.name, default: variant.isDefault, description: variant.description })),
|
|
222
251
|
grouping: collection.grouping ? { label: collection.grouping.label, property: collection.grouping.property, groups: collection.grouping.groups } : null,
|
|
223
252
|
},
|
|
@@ -226,22 +255,24 @@ function collectionTable(context, name) {
|
|
|
226
255
|
rows,
|
|
227
256
|
};
|
|
228
257
|
}
|
|
229
|
-
// The values a
|
|
258
|
+
// The values a property takes anywhere in the root, for a sketch's fields;
|
|
259
|
+
// declared options first, in order.
|
|
230
260
|
function knownValuesAll(context, name) {
|
|
231
261
|
const values = new Set();
|
|
232
262
|
const property = context.framework.schema.custom.find((candidate) => candidate.name === name);
|
|
233
|
-
for (const key of
|
|
263
|
+
for (const key of Object.keys(property?.canvas?.styles ?? {}))
|
|
234
264
|
values.add(key);
|
|
235
265
|
for (const blueprint of context.blueprints) {
|
|
236
|
-
const value
|
|
237
|
-
if (value !== null && value !== '')
|
|
266
|
+
for (const value of heldValues(blueprint, name))
|
|
238
267
|
values.add(value);
|
|
239
268
|
}
|
|
240
|
-
return [...values];
|
|
269
|
+
return property ? withOptionsFirst(property, values) : [...values];
|
|
241
270
|
}
|
|
242
271
|
function checkSnapshot(context) {
|
|
243
272
|
const report = checkRoot(context.framework, context.blueprints, { standardVersion: standardVersion() });
|
|
244
273
|
return {
|
|
274
|
+
// whether this root counts a warning as a failure: the owner's setting
|
|
275
|
+
strict: readSettings(context.root).shared.check.strict,
|
|
245
276
|
root: report.root,
|
|
246
277
|
eidos_version: report.eidosVersion,
|
|
247
278
|
naming: report.naming,
|
|
@@ -283,10 +314,62 @@ function frameworkView(context) {
|
|
|
283
314
|
// The whole document replaced, the generated index carried over untouched.
|
|
284
315
|
// A document that cannot be read at all is refused; one with problems is
|
|
285
316
|
// written and the problems returned, as `check` would report them.
|
|
317
|
+
// Settings renames a collection or a template through its document; the files
|
|
318
|
+
// follow: a collection's folder, and a template's file, moved when the old one
|
|
319
|
+
// exists and the new one does not. Nothing is overwritten.
|
|
320
|
+
function applyRenames(context, given) {
|
|
321
|
+
if (!given || typeof given !== 'object')
|
|
322
|
+
return;
|
|
323
|
+
const renames = given;
|
|
324
|
+
const inside = (base, target) => target.startsWith(base + path.sep) && !path.relative(base, target).includes('..');
|
|
325
|
+
// a top-level doc's file follows its title, and every link to it follows the file
|
|
326
|
+
const docMoves = [];
|
|
327
|
+
for (const [from, to] of Object.entries(renames.docs ?? {})) {
|
|
328
|
+
const source = path.resolve(context.root, from);
|
|
329
|
+
const target = path.resolve(context.root, to);
|
|
330
|
+
if (!inside(context.root, source) || !inside(context.root, target) || source === target || path.dirname(source) !== context.root || path.dirname(target) !== context.root || !/\.md$/i.test(target))
|
|
331
|
+
continue;
|
|
332
|
+
if (existsSync(source) && !existsSync(target))
|
|
333
|
+
docMoves.push({ from: source, to: target });
|
|
334
|
+
}
|
|
335
|
+
if (docMoves.length > 0)
|
|
336
|
+
moveWithLinks(context.root, docMoves);
|
|
337
|
+
const templatesDir = path.join(context.root, FRAMEWORK_DIR, 'templates');
|
|
338
|
+
for (const [from, to] of Object.entries(renames.templates ?? {})) {
|
|
339
|
+
const source = path.resolve(context.root, FRAMEWORK_DIR, from);
|
|
340
|
+
const target = path.resolve(context.root, FRAMEWORK_DIR, to);
|
|
341
|
+
if (!inside(templatesDir, source) || !inside(templatesDir, target) || source === target)
|
|
342
|
+
continue;
|
|
343
|
+
if (existsSync(source) && !existsSync(target))
|
|
344
|
+
moveFile(context.root, source, target);
|
|
345
|
+
}
|
|
346
|
+
for (const [from, to] of Object.entries(renames.folders ?? {})) {
|
|
347
|
+
const source = path.resolve(context.root, from);
|
|
348
|
+
const target = path.resolve(context.root, to);
|
|
349
|
+
if (!inside(context.root, source) || !inside(context.root, target) || source === target || path.dirname(source) !== context.root || path.dirname(target) !== context.root)
|
|
350
|
+
continue;
|
|
351
|
+
if (existsSync(source) && !existsSync(target))
|
|
352
|
+
moveFile(context.root, source, target);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Settings removes a top-level doc's file when the entry is removed on
|
|
356
|
+
// purpose: one <name>.md straight under the root, never anything else.
|
|
357
|
+
function applyDeletes(context, given) {
|
|
358
|
+
if (!given || typeof given !== 'object')
|
|
359
|
+
return;
|
|
360
|
+
const deletes = given;
|
|
361
|
+
for (const name of Array.isArray(deletes.docs) ? deletes.docs : []) {
|
|
362
|
+
if (typeof name !== 'string' || !/^[^/\\]+\.md$/i.test(name) || name.startsWith('.'))
|
|
363
|
+
continue;
|
|
364
|
+
const file = path.join(context.root, name);
|
|
365
|
+
if (existsSync(file))
|
|
366
|
+
unlinkSync(file);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
286
369
|
function saveFramework(context, body) {
|
|
287
370
|
const given = (typeof body['document'] === 'object' && body['document'] !== null ? body['document'] : body);
|
|
288
|
-
if (!Array.isArray(given['
|
|
289
|
-
throw new HttpError(400, 'expected { document } with a
|
|
371
|
+
if (!Array.isArray(given['folders'])) {
|
|
372
|
+
throw new HttpError(400, 'expected { document } with a folders list');
|
|
290
373
|
}
|
|
291
374
|
const document = { ...given };
|
|
292
375
|
delete document['index'];
|
|
@@ -435,7 +518,12 @@ function pageView(context, canvas, page) {
|
|
|
435
518
|
nodes,
|
|
436
519
|
comments: renderComments(page, nodes),
|
|
437
520
|
author: commentAuthor(context),
|
|
438
|
-
|
|
521
|
+
// each styling property with its value styles, and the color per value alone for the legend
|
|
522
|
+
palette: canvasProperties(context.framework).map((property) => {
|
|
523
|
+
const styles = property.canvas.styles ?? {};
|
|
524
|
+
const color = Object.fromEntries(Object.entries(styles).flatMap(([value, style]) => (style.color ? [[value, style.color]] : [])));
|
|
525
|
+
return { property: property.name, type: property.type, applies_to: property.appliesTo, styles, color: Object.keys(color).length > 0 ? color : null, show: property.canvas.show === true };
|
|
526
|
+
}),
|
|
439
527
|
// schema properties a sketch may hold: those applying to every collection, with the values the root uses
|
|
440
528
|
sketch_properties: propertiesFor(context.framework, '')
|
|
441
529
|
.filter((property) => !property.core && property.appliesTo === 'all')
|
|
@@ -1094,7 +1182,7 @@ export function createServer(options) {
|
|
|
1094
1182
|
if (resource === 'events' && method === 'GET') {
|
|
1095
1183
|
response.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' });
|
|
1096
1184
|
response.write(': connected\n\n');
|
|
1097
|
-
const off = store.onChange(() => response.write(
|
|
1185
|
+
const off = store.onChange((paths) => response.write(`data: ${JSON.stringify({ type: 'change', paths })}\n\n`));
|
|
1098
1186
|
request.on('close', off);
|
|
1099
1187
|
return;
|
|
1100
1188
|
}
|
|
@@ -1215,11 +1303,33 @@ export function createServer(options) {
|
|
|
1215
1303
|
json(response, 200, docView(store.context(), rest));
|
|
1216
1304
|
return;
|
|
1217
1305
|
}
|
|
1218
|
-
// POST /api/docs {
|
|
1306
|
+
// POST /api/docs { title, description? }: a top-level doc declared under
|
|
1307
|
+
// top_level and written at the root, named for its title, the same plan as
|
|
1308
|
+
// `eidos configure:doc add`. { path, title } writes the file alone, for a
|
|
1309
|
+
// framework draft that declares it itself.
|
|
1219
1310
|
if (resource === 'docs' && rest === '' && method === 'POST') {
|
|
1220
1311
|
const current = store.context();
|
|
1221
1312
|
const body = await readBody(request);
|
|
1222
|
-
|
|
1313
|
+
if (typeof body['path'] !== 'string') {
|
|
1314
|
+
const title = str(body['title']);
|
|
1315
|
+
if (!title)
|
|
1316
|
+
throw new HttpError(400, 'expected { title }');
|
|
1317
|
+
let plan;
|
|
1318
|
+
try {
|
|
1319
|
+
plan = planDocAdd(editContext(current.root), title, str(body['description']) ?? '');
|
|
1320
|
+
}
|
|
1321
|
+
catch (cause) {
|
|
1322
|
+
if (cause instanceof EditError)
|
|
1323
|
+
throw new HttpError(400, cause.message);
|
|
1324
|
+
throw cause;
|
|
1325
|
+
}
|
|
1326
|
+
applyPlan(plan, standardVersion());
|
|
1327
|
+
store.invalidate();
|
|
1328
|
+
const made = store.context().framework.topLevel.find((doc) => doc.title === title);
|
|
1329
|
+
json(response, 201, { path: made ? path.basename(made.path) : '', title });
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
const name = body['path'].trim().replace(/\\/g, '/');
|
|
1223
1333
|
if (!/^[^/]+\.md$/i.test(name) || name.startsWith('.'))
|
|
1224
1334
|
throw new HttpError(400, 'a top-level document is one <name>.md straight under the root');
|
|
1225
1335
|
const file = path.join(current.root, name);
|
|
@@ -1231,6 +1341,28 @@ export function createServer(options) {
|
|
|
1231
1341
|
json(response, 201, { path: name, title });
|
|
1232
1342
|
return;
|
|
1233
1343
|
}
|
|
1344
|
+
// DELETE /api/docs/<path>: the entry under top_level and the file, the same plan as `eidos configure:doc remove`.
|
|
1345
|
+
if (resource === 'docs' && rest !== '' && method === 'DELETE') {
|
|
1346
|
+
const current = store.context();
|
|
1347
|
+
const { file, rel: relative, declared } = topLevelFile(current, rest);
|
|
1348
|
+
if (declared) {
|
|
1349
|
+
let plan;
|
|
1350
|
+
try {
|
|
1351
|
+
plan = planDocRemove(editContext(current.root), path.basename(file), { preserve: false });
|
|
1352
|
+
}
|
|
1353
|
+
catch (cause) {
|
|
1354
|
+
if (cause instanceof EditError)
|
|
1355
|
+
throw new HttpError(400, cause.message);
|
|
1356
|
+
throw cause;
|
|
1357
|
+
}
|
|
1358
|
+
applyPlan(plan, standardVersion());
|
|
1359
|
+
}
|
|
1360
|
+
else if (existsSync(file))
|
|
1361
|
+
rmSync(file);
|
|
1362
|
+
store.invalidate();
|
|
1363
|
+
json(response, 200, { deleted: relative });
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1234
1366
|
if (resource === 'docs' && rest !== '' && method === 'PUT') {
|
|
1235
1367
|
const current = store.context();
|
|
1236
1368
|
const { file, rel: relative } = topLevelFile(current, rest);
|
|
@@ -1243,7 +1375,7 @@ export function createServer(options) {
|
|
|
1243
1375
|
return;
|
|
1244
1376
|
}
|
|
1245
1377
|
if (resource === 'versions' && method === 'GET') {
|
|
1246
|
-
json(response, 200, { versions: readVersions(store.context().root), git_head: gitInfo(store.context().root).head });
|
|
1378
|
+
json(response, 200, { versions: readVersions(store.context().root), git_head: gitInfo(store.context().root).head, tag_prefix: tagPrefix(store.context().root) });
|
|
1247
1379
|
return;
|
|
1248
1380
|
}
|
|
1249
1381
|
// POST /api/versions { version, commit?, tag? }: a snapshot of the root, only because someone asked.
|
|
@@ -1268,7 +1400,10 @@ export function createServer(options) {
|
|
|
1268
1400
|
return;
|
|
1269
1401
|
}
|
|
1270
1402
|
if (resource === 'framework' && method === 'PUT') {
|
|
1271
|
-
const
|
|
1403
|
+
const body = await readBody(request);
|
|
1404
|
+
applyRenames(store.context(), body['renames']);
|
|
1405
|
+
applyDeletes(store.context(), body['deletes']);
|
|
1406
|
+
const saved = saveFramework(store.context(), body);
|
|
1272
1407
|
store.invalidate();
|
|
1273
1408
|
json(response, 200, { ...saved, ...frameworkView(store.context()) });
|
|
1274
1409
|
return;
|
|
@@ -1311,6 +1446,21 @@ export function createServer(options) {
|
|
|
1311
1446
|
}
|
|
1312
1447
|
if (method === 'PUT') {
|
|
1313
1448
|
const body = await readBody(request);
|
|
1449
|
+
// { rename } moves the file to the new name, me.md and the users following
|
|
1450
|
+
if (typeof body['rename'] === 'string') {
|
|
1451
|
+
const to = body['rename'].trim();
|
|
1452
|
+
if (!isRoleName(to))
|
|
1453
|
+
throw new HttpError(400, 'a role name is kebab-case: letters, digits, and dashes');
|
|
1454
|
+
if (!existsSync(file))
|
|
1455
|
+
throw new HttpError(404, `no role '${name}'`);
|
|
1456
|
+
if (to !== name && existsSync(roleFile(current.root, to)))
|
|
1457
|
+
throw new HttpError(409, `a role '${to}' already exists`);
|
|
1458
|
+
if (to !== name)
|
|
1459
|
+
renameRoleFiles(current.root, name, to);
|
|
1460
|
+
store.invalidate();
|
|
1461
|
+
json(response, 200, { name: to, path: rel(current.root, roleFile(current.root, to)) });
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1314
1464
|
if (typeof body['text'] !== 'string')
|
|
1315
1465
|
throw new HttpError(400, 'expected { text }');
|
|
1316
1466
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -1390,6 +1540,29 @@ export function createServer(options) {
|
|
|
1390
1540
|
json(response, 200, { launched: command });
|
|
1391
1541
|
return;
|
|
1392
1542
|
}
|
|
1543
|
+
// POST /api/reveal { path }: the file shown in the machine's file manager, selected where the platform can (Finder, Explorer, the FileManager1 desktop service on Linux, else the folder opened).
|
|
1544
|
+
if (resource === 'reveal' && method === 'POST') {
|
|
1545
|
+
const current = store.context();
|
|
1546
|
+
const body = await readBody(request);
|
|
1547
|
+
const target = typeof body['path'] === 'string' ? body['path'] : '';
|
|
1548
|
+
const file = path.resolve(current.root, target);
|
|
1549
|
+
if (target === '' || !file.startsWith(current.root + path.sep) || !existsSync(file))
|
|
1550
|
+
throw new HttpError(400, 'not a file of this root');
|
|
1551
|
+
const launch = (command, args, onError) => {
|
|
1552
|
+
const child = spawn(command, args, { cwd: current.root, detached: true, stdio: 'ignore' });
|
|
1553
|
+
child.on('error', () => onError?.());
|
|
1554
|
+
child.on('exit', (code) => code !== 0 && onError?.());
|
|
1555
|
+
child.unref();
|
|
1556
|
+
};
|
|
1557
|
+
if (process.platform === 'darwin')
|
|
1558
|
+
launch('open', ['-R', file]);
|
|
1559
|
+
else if (process.platform === 'win32')
|
|
1560
|
+
launch('explorer', [`/select,${file}`]);
|
|
1561
|
+
else
|
|
1562
|
+
launch('dbus-send', ['--session', '--dest=org.freedesktop.FileManager1', '--type=method_call', '/org/freedesktop/FileManager1', 'org.freedesktop.FileManager1.ShowItems', `array:string:file://${file}`, 'string:'], () => launch('xdg-open', [path.dirname(file)]));
|
|
1563
|
+
json(response, 200, { revealed: rel(current.root, file) });
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1393
1566
|
if (resource === 'canvas') {
|
|
1394
1567
|
await handleCanvas(store, method, segments.slice(2), request, response);
|
|
1395
1568
|
return;
|
|
@@ -3,21 +3,37 @@
|
|
|
3
3
|
// root's and travels with it (whether git is used, the people who work in
|
|
4
4
|
// the root, what every save does to a blueprint); `local.yaml` is one
|
|
5
5
|
// person's on one machine (who they are acting as, how their canvas
|
|
6
|
-
// behaves, which columns they hide, the diff viewer installed here)
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// behaves, which columns they hide, the diff viewer installed here), the
|
|
7
|
+
// personal file the standard names since 5.2.0, kept out of git by the one
|
|
8
|
+
// pattern the root's `.eidos/.gitignore` carries for every tool. Nothing
|
|
9
|
+
// the browser remembers lives anywhere else.
|
|
9
10
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
11
|
import path from 'node:path';
|
|
11
12
|
import { parse as parseYaml, stringify } from 'yaml';
|
|
12
13
|
import { FRAMEWORK_DIR, PLUGINS_DIR, TOOL_KEY } from './framework-model.js';
|
|
13
14
|
export const SETTINGS_DIR = `${PLUGINS_DIR}/${TOOL_KEY}`;
|
|
14
|
-
|
|
15
|
+
// The one line in the root's `.eidos/.gitignore` that keeps every tool's
|
|
16
|
+
// personal file out of the repository (Eidos 5.2.0), beside `me.md`.
|
|
17
|
+
export const LOCAL_IGNORE_PATTERN = 'plugins/*/local.yaml';
|
|
18
|
+
// Whether the root's `.eidos/.gitignore` already carries that line.
|
|
19
|
+
export function rootIgnoresLocal(root) {
|
|
20
|
+
const file = path.join(root, FRAMEWORK_DIR, '.gitignore');
|
|
21
|
+
if (!existsSync(file))
|
|
22
|
+
return false;
|
|
23
|
+
return readFileSync(file, 'utf8')
|
|
24
|
+
.split(/\r?\n/)
|
|
25
|
+
.some((line) => line.trim() === LOCAL_IGNORE_PATTERN);
|
|
26
|
+
}
|
|
27
|
+
export const DEFAULT_TAG_PREFIX = 'blueprints/';
|
|
28
|
+
export const DEFAULT_SHARED = { features: { git: true }, users: [], on_save: [], versions: { tag_prefix: DEFAULT_TAG_PREFIX }, check: { strict: false } };
|
|
15
29
|
export const DEFAULT_LOCAL = {
|
|
16
30
|
user: null,
|
|
17
31
|
theme: 'system',
|
|
18
32
|
canvas: { wheel_zoom: true, right_drag_pan: true, ctrl_click_menu: false, enter_commits: true, context_menu: true, resize_all: false },
|
|
19
33
|
tables: { page_size: 50, columns: {} },
|
|
20
34
|
source_mode: false,
|
|
35
|
+
hide_overview: false,
|
|
36
|
+
reader_width: 'fixed',
|
|
21
37
|
diff_viewer: null,
|
|
22
38
|
editor: null,
|
|
23
39
|
recent_searches: [],
|
|
@@ -25,10 +41,12 @@ export const DEFAULT_LOCAL = {
|
|
|
25
41
|
const SHARED_HEADER = `eidosmd settings shared by everyone in this root. Edit by hand, with eidos setup, or from the browser.
|
|
26
42
|
features.git: whether the browser reads git (history, authors, versions) when the root is in a repository.
|
|
27
43
|
users: the people who work here: { name, alias (for @mentions, no spaces), role (from .eidos/roles/), email (ties to git) }.
|
|
28
|
-
on_save: rules applied to a blueprint's frontmatter on every save from the browser: { property, set: today | now }
|
|
44
|
+
on_save: rules applied to a blueprint's frontmatter on every save from the browser: { property, set: today | now }.
|
|
45
|
+
versions.tag_prefix: what a recorded version's git tag starts with (eidos version record --tag), blueprints/ by default.
|
|
46
|
+
check.strict: whether a warning fails eidos check as an error does; the framework owner's decision for this root (eidos check --strict / --no-strict override one run).`;
|
|
29
47
|
const LOCAL_HEADER = `eidosmd settings for this machine, ignored by git. Edit by hand, with eidos setup, or from the browser's Preferences.
|
|
30
48
|
user: who this machine acts as, by alias or name from users; absent = the git identity, else the role in me.md.
|
|
31
|
-
theme: system | light | dark. canvas: scroll-wheel zoom, right-drag pan, ctrl-click menus, whether Enter finishes an in-place edit, whether a right click opens the menu or the details, whether every node resizes. tables: page size and the columns hidden per collection. source_mode: the reader shows files as text.
|
|
49
|
+
theme: system | light | dark. canvas: scroll-wheel zoom, right-drag pan, ctrl-click menus, whether Enter finishes an in-place edit, whether a right click opens the menu or the details, whether every node resizes. tables: page size and the columns hidden per collection. source_mode: the reader shows files as text. hide_overview: the sidebar leaves out Eidos Overview. reader_width: fixed (every page but the canvas held to about 1200px, for a wide screen) | fluid (the whole window).
|
|
32
50
|
editor: a command with {file} that opens a file in an external editor; blank = the system's default app.
|
|
33
51
|
diff_viewer: the git plugin's viewer, a command with {a} and {b} for the two files, e.g. "code --diff {a} {b}".`;
|
|
34
52
|
const isPlain = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -71,7 +89,17 @@ function readShared(value, base = DEFAULT_SHARED) {
|
|
|
71
89
|
rules.push({ property: entry['property'].trim(), set: entry['set'] });
|
|
72
90
|
}
|
|
73
91
|
}
|
|
74
|
-
|
|
92
|
+
const versions = isPlain(value['versions']) ? value['versions'] : {};
|
|
93
|
+
const prefix = typeof versions['tag_prefix'] === 'string' ? versions['tag_prefix'].trim() : base.versions.tag_prefix;
|
|
94
|
+
const check = isPlain(value['check']) ? value['check'] : {};
|
|
95
|
+
const strict = typeof check['strict'] === 'boolean' ? check['strict'] : base.check.strict;
|
|
96
|
+
return {
|
|
97
|
+
features: { git },
|
|
98
|
+
users: 'users' in value ? readUsers(value['users']) : base.users.map((user) => ({ ...user })),
|
|
99
|
+
on_save: 'on_save' in value ? rules : base.on_save.map((rule) => ({ ...rule })),
|
|
100
|
+
versions: { tag_prefix: prefix },
|
|
101
|
+
check: { strict },
|
|
102
|
+
};
|
|
75
103
|
}
|
|
76
104
|
// A user by alias (case-insensitive) or by name.
|
|
77
105
|
export function findUser(users, key) {
|
|
@@ -103,6 +131,9 @@ function readLocal(value, base = DEFAULT_LOCAL) {
|
|
|
103
131
|
if (typeof tables['page_size'] === 'number' && tables['page_size'] >= 10)
|
|
104
132
|
out.tables.page_size = Math.round(tables['page_size']);
|
|
105
133
|
if (isPlain(tables['columns'])) {
|
|
134
|
+
// a columns map given is the whole map (a write merges per collection
|
|
135
|
+
// before it gets here), so an entry it lacks is gone
|
|
136
|
+
out.tables.columns = {};
|
|
106
137
|
for (const [collection, hidden] of Object.entries(tables['columns'])) {
|
|
107
138
|
if (Array.isArray(hidden))
|
|
108
139
|
out.tables.columns[collection] = hidden.filter((name) => typeof name === 'string');
|
|
@@ -110,6 +141,10 @@ function readLocal(value, base = DEFAULT_LOCAL) {
|
|
|
110
141
|
}
|
|
111
142
|
if (typeof value['source_mode'] === 'boolean')
|
|
112
143
|
out.source_mode = value['source_mode'];
|
|
144
|
+
if (typeof value['hide_overview'] === 'boolean')
|
|
145
|
+
out.hide_overview = value['hide_overview'];
|
|
146
|
+
if (value['reader_width'] === 'fixed' || value['reader_width'] === 'fluid')
|
|
147
|
+
out.reader_width = value['reader_width'];
|
|
113
148
|
if (value['diff_viewer'] === null)
|
|
114
149
|
out.diff_viewer = null;
|
|
115
150
|
else if (typeof value['diff_viewer'] === 'string')
|
|
@@ -142,8 +177,11 @@ function writeYaml(file, header, value) {
|
|
|
142
177
|
writeFileSync(file, `${comment}\n${stringify(value)}`, 'utf8');
|
|
143
178
|
}
|
|
144
179
|
// Each file is written whole from the merged value: a partial patch keeps
|
|
145
|
-
// what it does not name.
|
|
146
|
-
// the
|
|
180
|
+
// what it does not name. On a root whose `.eidos/.gitignore` does not yet
|
|
181
|
+
// carry the standard's pattern (one written before 5.2.0 and not migrated),
|
|
182
|
+
// a .gitignore of this tool's own is written beside the local file, so a
|
|
183
|
+
// personal setting never reaches the repository; `eidos migrate` adds the
|
|
184
|
+
// pattern and removes that file.
|
|
147
185
|
export function writeSettings(root, patch) {
|
|
148
186
|
const current = readSettings(root);
|
|
149
187
|
const dir = settingsDir(root);
|
|
@@ -153,6 +191,10 @@ export function writeSettings(root, patch) {
|
|
|
153
191
|
const merged = { ...base, ...given };
|
|
154
192
|
if (isPlain(given['features']))
|
|
155
193
|
merged['features'] = { ...base['features'], ...given['features'] };
|
|
194
|
+
if (isPlain(given['versions']))
|
|
195
|
+
merged['versions'] = { ...base['versions'], ...given['versions'] };
|
|
196
|
+
if (isPlain(given['check']))
|
|
197
|
+
merged['check'] = { ...base['check'], ...given['check'] };
|
|
156
198
|
writeYaml(path.join(dir, 'settings.yaml'), SHARED_HEADER, readShared(merged, current.shared));
|
|
157
199
|
}
|
|
158
200
|
if (isPlain(patch.local)) {
|
|
@@ -164,12 +206,21 @@ export function writeSettings(root, patch) {
|
|
|
164
206
|
if (isPlain(given['tables'])) {
|
|
165
207
|
const tables = base['tables'];
|
|
166
208
|
merged['tables'] = { ...tables, ...given['tables'] };
|
|
167
|
-
if (isPlain(given['tables']['columns']))
|
|
168
|
-
|
|
209
|
+
if (isPlain(given['tables']['columns'])) {
|
|
210
|
+
// a collection given as null drops its entry, for a collection renamed away
|
|
211
|
+
const columns = { ...tables['columns'] };
|
|
212
|
+
for (const [collection, hidden] of Object.entries(given['tables']['columns'])) {
|
|
213
|
+
if (hidden === null)
|
|
214
|
+
delete columns[collection];
|
|
215
|
+
else
|
|
216
|
+
columns[collection] = hidden;
|
|
217
|
+
}
|
|
218
|
+
merged['tables']['columns'] = columns;
|
|
219
|
+
}
|
|
169
220
|
}
|
|
170
221
|
writeYaml(path.join(dir, 'local.yaml'), LOCAL_HEADER, readLocal(merged, current.local));
|
|
171
222
|
const ignore = path.join(dir, '.gitignore');
|
|
172
|
-
if (!existsSync(ignore))
|
|
223
|
+
if (!rootIgnoresLocal(root) && !existsSync(ignore))
|
|
173
224
|
writeFileSync(ignore, 'local.yaml\n', 'utf8');
|
|
174
225
|
}
|
|
175
226
|
return readSettings(root);
|