eidosmd 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +133 -0
  2. package/NOTICE +15 -0
  3. package/README.md +146 -0
  4. package/browser/index.html +268 -0
  5. package/dist/src/cli.js +5 -0
  6. package/dist/src/commands/agents.js +47 -0
  7. package/dist/src/commands/browser.js +40 -0
  8. package/dist/src/commands/check.js +61 -0
  9. package/dist/src/commands/convert.js +30 -0
  10. package/dist/src/commands/framework.js +76 -0
  11. package/dist/src/commands/index.js +64 -0
  12. package/dist/src/commands/init.js +66 -0
  13. package/dist/src/commands/instructions.js +40 -0
  14. package/dist/src/commands/list.js +73 -0
  15. package/dist/src/commands/new.js +68 -0
  16. package/dist/src/commands/seeds.js +40 -0
  17. package/dist/src/commands/show.js +24 -0
  18. package/dist/src/commands/whoami.js +84 -0
  19. package/dist/src/context.js +35 -0
  20. package/dist/src/core/blueprint.js +129 -0
  21. package/dist/src/core/check.js +407 -0
  22. package/dist/src/core/convert.js +37 -0
  23. package/dist/src/core/framework-markdown.js +233 -0
  24. package/dist/src/core/framework-model.js +48 -0
  25. package/dist/src/core/framework-structured.js +334 -0
  26. package/dist/src/core/framework.js +56 -0
  27. package/dist/src/core/frontmatter.js +116 -0
  28. package/dist/src/core/index-leaf.js +83 -0
  29. package/dist/src/core/markdown.js +100 -0
  30. package/dist/src/core/me.js +89 -0
  31. package/dist/src/core/naming.js +46 -0
  32. package/dist/src/core/root.js +88 -0
  33. package/dist/src/core/scaffold.js +118 -0
  34. package/dist/src/core/seed.js +223 -0
  35. package/dist/src/core/server.js +275 -0
  36. package/dist/src/core/shape.js +26 -0
  37. package/dist/src/output.js +38 -0
  38. package/dist/src/paths.js +28 -0
  39. package/dist/src/program.js +242 -0
  40. package/instructions/authoring.md +50 -0
  41. package/instructions/configuring.md +65 -0
  42. package/instructions/init-required.md +32 -0
  43. package/instructions/overview.md +37 -0
  44. package/instructions/validating.md +33 -0
  45. package/package.json +53 -0
  46. package/standard/EIDOS.md +324 -0
  47. package/standard/seeds/README.md +25 -0
  48. package/standard/seeds/book/Framework.md +87 -0
  49. package/standard/seeds/book/README.md +22 -0
  50. package/standard/seeds/book/_gitignore +4 -0
  51. package/standard/seeds/book/me.md +18 -0
  52. package/standard/seeds/book/roles/README.md +12 -0
  53. package/standard/seeds/book/roles/collaborator.md +16 -0
  54. package/standard/seeds/book/roles/editor.md +16 -0
  55. package/standard/seeds/book/roles/framework-owner.md +16 -0
  56. package/standard/seeds/book/roles/reader.md +16 -0
  57. package/standard/seeds/book/shapes/chapter.full.md +45 -0
  58. package/standard/seeds/book/shapes/chapter.sketch.md +26 -0
  59. package/standard/seeds/book/shapes/frame.market.md +23 -0
  60. package/standard/seeds/book/shapes/frame.premise.md +24 -0
  61. package/standard/seeds/book/shapes/frame.reader.md +23 -0
  62. package/standard/seeds/book/shapes/frame.voice.md +24 -0
  63. package/standard/seeds/research/Framework.md +88 -0
  64. package/standard/seeds/research/README.md +23 -0
  65. package/standard/seeds/research/_gitignore +4 -0
  66. package/standard/seeds/research/me.md +18 -0
  67. package/standard/seeds/research/roles/README.md +12 -0
  68. package/standard/seeds/research/roles/framework-owner.md +16 -0
  69. package/standard/seeds/research/roles/researcher.md +16 -0
  70. package/standard/seeds/research/roles/reviewer.md +16 -0
  71. package/standard/seeds/research/roles/sponsor.md +16 -0
  72. package/standard/seeds/research/shapes/frame.ethics.md +23 -0
  73. package/standard/seeds/research/shapes/frame.method.md +24 -0
  74. package/standard/seeds/research/shapes/frame.prior-work.md +23 -0
  75. package/standard/seeds/research/shapes/frame.question.md +24 -0
  76. package/standard/seeds/research/shapes/investigation.full.md +49 -0
  77. package/standard/seeds/research/shapes/investigation.note.md +26 -0
  78. package/standard/seeds/software/Framework.md +88 -0
  79. package/standard/seeds/software/README.md +24 -0
  80. package/standard/seeds/software/_gitignore +4 -0
  81. package/standard/seeds/software/me.md +18 -0
  82. package/standard/seeds/software/roles/.gitkeep +0 -0
  83. package/standard/seeds/software/roles/README.md +13 -0
  84. package/standard/seeds/software/roles/designer.md +16 -0
  85. package/standard/seeds/software/roles/developer.md +16 -0
  86. package/standard/seeds/software/roles/framework-owner.md +16 -0
  87. package/standard/seeds/software/roles/project-manager.md +18 -0
  88. package/standard/seeds/software/roles/stakeholder.md +16 -0
  89. package/standard/seeds/software/shapes/.gitkeep +0 -0
  90. package/standard/seeds/software/shapes/frame.architecture.md +26 -0
  91. package/standard/seeds/software/shapes/frame.audience.md +26 -0
  92. package/standard/seeds/software/shapes/frame.criteria.md +27 -0
  93. package/standard/seeds/software/shapes/frame.market.md +31 -0
  94. package/standard/seeds/software/shapes/spec.full.md +67 -0
  95. package/standard/seeds/software/shapes/spec.micro.md +32 -0
package/LICENSE ADDED
@@ -0,0 +1,133 @@
1
+ Required Notice: Copyright 2026 The Virtual Panda (https://eidosmd.com)
2
+
3
+ # PolyForm Noncommercial License 1.0.0
4
+
5
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
6
+
7
+ ## Acceptance
8
+
9
+ In order to get any license under these terms, you must agree
10
+ to them as both strict obligations and conditions to all
11
+ your licenses.
12
+
13
+ ## Copyright License
14
+
15
+ The licensor grants you a copyright license for the
16
+ software to do everything you might do with the software
17
+ that would otherwise infringe the licensor's copyright
18
+ in it for any permitted purpose. However, you may
19
+ only distribute the software according to [Distribution
20
+ License](#distribution-license) and make changes or new works
21
+ based on the software according to [Changes and New Works
22
+ License](#changes-and-new-works-license).
23
+
24
+ ## Distribution License
25
+
26
+ The licensor grants you an additional copyright license
27
+ to distribute copies of the software. Your license
28
+ to distribute covers distributing the software with
29
+ changes and new works permitted by [Changes and New Works
30
+ License](#changes-and-new-works-license).
31
+
32
+ ## Notices
33
+
34
+ You must ensure that anyone who gets a copy of any part of
35
+ the software from you also gets a copy of these terms or the
36
+ URL for them above, as well as copies of any plain-text lines
37
+ beginning with `Required Notice:` that the licensor provided
38
+ with the software. For example:
39
+
40
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
41
+
42
+ ## Changes and New Works License
43
+
44
+ The licensor grants you an additional copyright license to
45
+ make changes and new works based on the software for any
46
+ permitted purpose.
47
+
48
+ ## Patent License
49
+
50
+ The licensor grants you a patent license for the software that
51
+ covers patent claims the licensor can license, or becomes able
52
+ to license, that you would infringe by using the software.
53
+
54
+ ## Noncommercial Purposes
55
+
56
+ Any noncommercial purpose is a permitted purpose.
57
+
58
+ ## Personal Uses
59
+
60
+ Personal use for research, experiment, and testing for
61
+ the benefit of public knowledge, personal study, private
62
+ entertainment, hobby projects, amateur pursuits, or religious
63
+ observance, without any anticipated commercial application,
64
+ is use for a permitted purpose.
65
+
66
+ ## Noncommercial Organizations
67
+
68
+ Use by any charitable organization, educational institution,
69
+ public research organization, public safety or health
70
+ organization, environmental protection organization,
71
+ or government institution is use for a permitted purpose
72
+ regardless of the source of funding or obligations resulting
73
+ from the funding.
74
+
75
+ ## Fair Use
76
+
77
+ You may have "fair use" rights for the software under the
78
+ law. These terms do not limit them.
79
+
80
+ ## No Other Rights
81
+
82
+ These terms do not allow you to sublicense or transfer any of
83
+ your licenses to anyone else, or prevent the licensor from
84
+ granting licenses to anyone else. These terms do not imply
85
+ any other licenses.
86
+
87
+ ## Patent Defense
88
+
89
+ If you make any written claim that the software infringes or
90
+ contributes to infringement of any patent, your patent license
91
+ for the software granted under these terms ends immediately. If
92
+ your company makes such a claim, your patent license ends
93
+ immediately for work on behalf of your company.
94
+
95
+ ## Violations
96
+
97
+ The first time you are notified in writing that you have
98
+ violated any of these terms, or done anything with the software
99
+ not covered by your licenses, your licenses can nonetheless
100
+ continue if you come into full compliance with these terms,
101
+ and take practical steps to correct past violations, within
102
+ 32 days of receiving notice. Otherwise, all your licenses
103
+ end immediately.
104
+
105
+ ## No Liability
106
+
107
+ ***As far as the law allows, the software comes as is, without
108
+ any warranty or condition, and the licensor will not be liable
109
+ to you for any damages arising out of these terms or the use
110
+ or nature of the software, under any kind of legal claim.***
111
+
112
+ ## Definitions
113
+
114
+ The **licensor** is the individual or entity offering these
115
+ terms, and the **software** is the software the licensor makes
116
+ available under these terms.
117
+
118
+ **You** refers to the individual or entity agreeing to these
119
+ terms.
120
+
121
+ **Your company** is any legal entity, sole proprietorship,
122
+ or other kind of organization that you work for, plus all
123
+ organizations that have control over, are under the control of,
124
+ or are under common control with that organization. **Control**
125
+ means ownership of substantially all the assets of an entity,
126
+ or the power to direct its management and policies by vote,
127
+ contract, or otherwise. Control can be direct or indirect.
128
+
129
+ **Your licenses** are all the licenses granted to you for the
130
+ software under these terms.
131
+
132
+ **Use** means anything you do with the software requiring one
133
+ of your licenses.
package/NOTICE ADDED
@@ -0,0 +1,15 @@
1
+ eidosmd
2
+ Copyright 2026 The Virtual Panda
3
+
4
+ The eidos CLI is licensed under the PolyForm Noncommercial License 1.0.0
5
+ (see LICENSE): free to use, read, and change for any noncommercial purpose;
6
+ commercial use needs a license from The Virtual Panda.
7
+
8
+ This product includes material from the Eidos standard
9
+ (https://github.com/BuildableWorks/Eidos), Copyright 2026 Buildable,
10
+ licensed under the Apache License, Version 2.0
11
+ (https://www.apache.org/licenses/LICENSE-2.0). The files under standard/
12
+ (EIDOS.md and seeds/) are copied from the pinned checkout of that repository
13
+ at the monorepo's standard/ submodule by scripts/sync-standard.mjs,
14
+ unmodified except that each seed's .gitignore travels as _gitignore, and are
15
+ installed into a root by `eidos init`. That material keeps its Apache license.
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # eidosmd
2
+
3
+ The command line for [Eidos](https://github.com/BuildableWorks/Eidos/blob/main/README.md): a markdown standard in which one file is the complete source of truth for one unit of a thing, true whether or not it has been built.
4
+
5
+ `eidos` scaffolds a root, generates blueprints that are born conforming, validates them against the root's own framework, regenerates the indexes, and hands any coding agent the workflow it needs to work in one. Plain files, no service, no hidden state. It carries **Eidos 4.4.3**.
6
+
7
+ ```bash
8
+ npm install -g eidosmd
9
+ eidos --help
10
+ ```
11
+
12
+ Node 20 or newer. The package installs one binary, `eidos`, and three small pure-JavaScript dependencies with no dependencies of their own, so the install takes seconds and every command starts in well under a tenth of one.
13
+
14
+ ## Quick start
15
+
16
+ ```bash
17
+ eidos seeds # the bundled starting frameworks
18
+ eidos init --group Identity --product "Care Connect"
19
+ cd Blueprints
20
+ eidos framework # what this root declares
21
+ eidos new specs "Session Management" --group identity --summary "Keeps a signed-in user signed in."
22
+ eidos check # a review, not a gate
23
+ eidos index # each collection's index.md
24
+ ```
25
+
26
+ `init` copies a seed into `Blueprints/_eidos/` (the framework: shapes, roles, `Framework.yaml`), creates a folder per collection, and scaffolds one blank frame per framing flavor. It writes no prose. Everything a seed ships is reshapeable afterwards.
27
+
28
+ ### The framework document
29
+
30
+ The CLI works on `_eidos/Framework.yaml`: the framework as data, the same fields the standard's markdown form carries, comments where you want them, and every collection's index inside it under `index` (which `eidos index` rewrites and nothing else). The standard also allows `Framework.md`, the form for people, with a per-collection `index.md`; a root that keeps it is converted first, and every other command says so:
31
+
32
+ ```bash
33
+ eidos convert --dry-run # the writes it would make
34
+ eidos convert # Framework.yaml with the index inside; Framework.md and each index.md removed
35
+ ```
36
+
37
+ The markdown form's prose has no field to land in and is left behind; each collection's description travels.
38
+
39
+ ## Commands
40
+
41
+ | Command | What it does |
42
+ | --- | --- |
43
+ | `eidos init [root]` | Create a root from a seed: `--seed`, `--naming`, `--group` (repeatable), `--product`, `--dry-run` |
44
+ | `eidos convert` | Move a markdown root to `Framework.yaml`, the form the CLI works in; `--dry-run` |
45
+ | `eidos seeds` | List the bundled seeds (software, book, research) with their collections and flavors |
46
+ | `eidos framework` | Show the root's framework: version, naming, top-level docs, collections, flavors, groups, Schema; `--as yaml` prints it as the document; the one command that also reads a `Framework.md` root |
47
+ | `eidos list [collection]` | List blueprints; filter with `--group`, `--flavor`, `--where key=value` |
48
+ | `eidos show <id\|file\|path>` | Print one blueprint, or its properties and sections with `--json` |
49
+ | `eidos new <collection> <title>` | Scaffold a blueprint: frontmatter from the Schema, body from the flavor's shape, filename in convention |
50
+ | `eidos check [paths...]` | Validate the root (or the named blueprints) against its own framework; `--strict`, `--json` |
51
+ | `eidos index` | Regenerate the indexes, the `index` key inside `Framework.yaml`; `--check` verifies without writing |
52
+ | `eidos browser` | Open the root in a local web page on 127.0.0.1: browse, read, create, edit, check, index |
53
+ | `eidos roles` | The roles the framework installed under `_eidos/roles/` |
54
+ | `eidos whoami` | Show or set the actor in `_eidos/me.md` (role and calibration) |
55
+ | `eidos instructions [guide]` | The workflow an agent follows: overview, authoring, validating, configuring, init-required |
56
+ | `eidos standard` | Print the text of the standard this CLI carries; `--version` for just the number |
57
+ | `eidos agents` | Print the nudge that points agents at `eidos instructions`; `--write` puts it in `AGENTS.md` |
58
+
59
+ Every command takes `--root <path>`; without it the root is found from the working directory by its `_eidos/` marker, looking up the tree and then two levels down, so the commands work from a repository root that keeps its blueprints in `Blueprints/`.
60
+
61
+ `framework`, `list`, `show`, `check`, `index`, `new`, and `whoami` take `--json` for stable output. Exit codes: `0` done, `1` the check found errors (or an index is stale under `--check`), `2` the command could not run.
62
+
63
+ ## The browser
64
+
65
+ ```bash
66
+ eidos browser # http://127.0.0.1:6180, this machine only; Ctrl+C to stop
67
+ eidos browser --no-open --port 7000
68
+ ```
69
+
70
+ The root in a local web page: browse collections and groups, read a blueprint rendered with its properties and its findings, follow links between blueprints, create one from the same scaffold `new` uses, edit and save one (the check runs on save), run the check, rebuild the index, and read the framework, the standard, and the agent instructions. There is nothing in the page that is not a command: every write runs the same core function as `new`, `index`, or `check`, and the page re-reads the root on every request, so it stays honest while files change on disk.
71
+
72
+ ## What `check` reports
73
+
74
+ Validation is framework-defined: every rule reads `_eidos/Framework.yaml` and the shapes in `_eidos/shapes/`, never a contract hardcoded in the tool.
75
+
76
+ - **Errors** are wrong on any reading: frontmatter that does not parse, a missing `id` or `title`, an `id` that is not kebab-case or is used twice, a `flavor` the collection does not declare, a link to a file that does not exist, a shape the framework points at but lacks.
77
+ - **Warnings** are gaps the standard says to surface, never refuse: a property the Schema applies here but the blueprint lacks or leaves empty, a property the Schema does not declare, a work-tracking field, a section the flavor's shape declares but the body lacks, a section the shape does not know, sections out of order, a grouping value that does not match its folder, a declared frame nobody has written, a stale `index.md`, a version gap between the framework and the standard the CLI carries.
78
+
79
+ Frontmatter that is not valid YAML (an unquoted summary with a colon in it) is read leniently and reported as a warning, because Obsidian will refuse it even though a person can read it. `--strict` fails on warnings too, for CI.
80
+
81
+ ## For agents
82
+
83
+ Eidos is human-first: the owner holds the intent, the scope, and the decisions, and an agent facilitates. The CLI owns structure (`new`, `check`, `index`) so an agent never hand-assembles frontmatter or edits a generated index; the words in a blueprint are written with the owner.
84
+
85
+ ```bash
86
+ eidos instructions overview # required first read
87
+ eidos agents --write # a short pointer in AGENTS.md (or --file CLAUDE.md)
88
+ ```
89
+
90
+ The guides are the operating contract, the same one the standard's [Claude skills](https://github.com/BuildableWorks/Eidos/tree/main/skills) carry, restated around the commands. An agent with a shell needs nothing else.
91
+
92
+ ## The standard and this tool
93
+
94
+ Two things version separately.
95
+
96
+ - **The standard** is [`EIDOS.md`](https://github.com/BuildableWorks/Eidos/blob/main/EIDOS.md), maintained by Buildable in its own repository. A root records the version it targets as `eidos_version` in its framework document.
97
+ - **This CLI** follows its own semver, in [`package.json`](package.json) and [`CHANGELOG.md`](CHANGELOG.md). It ships one release of the standard under [`standard/`](standard) (`EIDOS.md` and the seeds), which is what `eidos init` installs and what `eidos check` compares a framework's version against. `eidos --version` names both.
98
+
99
+ `standard/` is a committed copy of that repository's `EIDOS.md` and `seeds/`, refreshed from the monorepo's `standard/` submodule by `pnpm sync-standard` (`--check` exits 1 if it has drifted from the pin); `npm pack` ships only what sits inside this package, so the copy has to be here. Each seed's `.gitignore` travels as `_gitignore`, because npm never packs a file by that name, and `eidos init` renames it back. Never edit `standard/` by hand: sync it.
100
+
101
+ A root on a newer or older standard still works: the framework in front of the tool is the operative contract, and the gap is reported once by `check`. Moving a root between versions is a judgment call this CLI does not automate; the `migrate` skill and [`versions/MIGRATIONS.md`](https://github.com/BuildableWorks/Eidos/blob/main/versions/MIGRATIONS.md) cover it.
102
+
103
+ ## Development
104
+
105
+ From the monorepo root or from `cli/`:
106
+
107
+ ```bash
108
+ pnpm install
109
+ pnpm build # tsc → dist/
110
+ pnpm test # builds, then runs the suite with node --test
111
+ pnpm check # type-check
112
+ node dist/src/cli.js --help
113
+ ```
114
+
115
+ ```txt
116
+ src/
117
+ cli.ts the executable entry
118
+ program.ts every command, its options, and its help (commander)
119
+ context.ts find the root, load its framework and blueprints
120
+ commands/ one module per command: what each prints and returns
121
+ browser/ the one HTML page `eidos browser` serves
122
+ core/ the testable model, no printing:
123
+ root.ts finding a root by its _eidos/ marker
124
+ framework.ts finding and loading the framework document
125
+ framework-model.ts the model: collections, flavors, groups, Schema
126
+ framework-markdown.ts parsing Framework.md, for convert and framework --as yaml
127
+ framework-structured.ts parsing and writing Framework.yaml, and the index inside it
128
+ shape.ts reading a shape file's sections and template
129
+ blueprint.ts loading blueprints from a collection's folder
130
+ frontmatter.ts YAML in, house-style frontmatter out
131
+ check.ts every validation rule
132
+ index-leaf.ts the index inside Framework.yaml
133
+ convert.ts moving a markdown root to Framework.yaml
134
+ seed.ts installing a seed into a new root
135
+ server.ts the browser's http server and JSON API, over the same core
136
+ scaffold.ts generating one conforming blueprint
137
+ me.ts roles and the actor
138
+ instructions/ the guides `eidos instructions` prints
139
+ standard/ the committed copy of EIDOS.md and seeds/ (synced from Buildable's repo, never hand-edited)
140
+ scripts/ sync-standard.mjs
141
+ test/ node:test suites; temp roots are installed from the seeds
142
+ ```
143
+
144
+ ## License
145
+
146
+ [PolyForm Noncommercial 1.0.0](LICENSE). Copyright The Virtual Panda. The source is here to read, run, and change for any noncommercial purpose; using it at work needs a commercial license, the way Obsidian does it. `standard/` is material from the [Eidos](https://github.com/BuildableWorks/Eidos) repository, copyright Buildable, under Apache-2.0; see [NOTICE](NOTICE).
@@ -0,0 +1,268 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Eidos</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light dark;
10
+ --bg: #fbfaf7; --panel: #f3f1ec; --line: #ddd8cf; --text: #1f1d1a; --muted: #6b665e;
11
+ --accent: #7a4a1f; --accent-soft: #f1e4d6; --error: #b23a2a; --warn: #9a6a12; --ok: #2f6b3a;
12
+ --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
13
+ --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
14
+ }
15
+ @media (prefers-color-scheme: dark) {
16
+ :root { --bg: #1b1916; --panel: #232019; --line: #3a352c; --text: #ebe6dc; --muted: #a39d92; --accent: #d9a56b; --accent-soft: #3a2d1f; --error: #e2705f; --warn: #d8a437; --ok: #7bc48a; }
17
+ }
18
+ * { box-sizing: border-box; }
19
+ body { margin: 0; font: 15px/1.5 var(--sans); color: var(--text); background: var(--bg); }
20
+ a { color: var(--accent); }
21
+ button, select, input, textarea { font: inherit; color: inherit; }
22
+ button { background: var(--panel); border: 1px solid var(--line); border-radius: 6px; padding: 4px 10px; cursor: pointer; }
23
+ button:hover { border-color: var(--accent); }
24
+ button.primary { background: var(--accent); color: var(--bg); border-color: var(--accent); }
25
+ input, select, textarea { background: var(--bg); border: 1px solid var(--line); border-radius: 6px; padding: 5px 8px; width: 100%; }
26
+ code, pre { font-family: var(--mono); font-size: 0.92em; }
27
+ pre { background: var(--panel); padding: 10px 12px; border-radius: 6px; overflow-x: auto; }
28
+ .layout { display: grid; grid-template-columns: 290px 1fr; min-height: 100vh; }
29
+ aside { background: var(--panel); border-right: 1px solid var(--line); padding: 14px; overflow-y: auto; position: sticky; top: 0; height: 100vh; }
30
+ main { padding: 20px 32px 60px; max-width: 980px; }
31
+ .brand { display: flex; align-items: baseline; gap: 8px; margin-bottom: 10px; }
32
+ .brand strong { font-size: 18px; }
33
+ .brand small { color: var(--muted); }
34
+ .nav a { display: block; padding: 3px 6px; border-radius: 4px; color: inherit; text-decoration: none; }
35
+ .nav a.active, .nav a:hover { background: var(--accent-soft); }
36
+ .nav h3 { margin: 14px 0 4px; font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); display: flex; justify-content: space-between; align-items: center; }
37
+ .nav h3 button { font-size: 12px; padding: 1px 7px; }
38
+ .nav h4 { margin: 8px 0 2px 6px; font-size: 12px; color: var(--muted); font-weight: 600; }
39
+ .nav .bp { font-size: 14px; padding-left: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
40
+ .nav .bp.flagged::after { content: " •"; color: var(--warn); }
41
+ .search { margin: 6px 0 4px; }
42
+ .toolbar { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 18px; align-items: center; }
43
+ .toolbar .spacer { flex: 1; }
44
+ .meta { color: var(--muted); font-size: 13px; margin: 0 0 6px; }
45
+ .meta code { color: var(--text); }
46
+ h1.title { margin: 0 0 6px; font-size: 26px; }
47
+ table.props { border-collapse: collapse; margin: 14px 0 22px; font-size: 14px; }
48
+ table.props td { padding: 3px 12px 3px 0; vertical-align: top; }
49
+ table.props td:first-child { color: var(--muted); font-family: var(--mono); font-size: 13px; white-space: nowrap; }
50
+ .body { border-top: 1px solid var(--line); padding-top: 12px; }
51
+ .body h1 { font-size: 24px; } .body h2 { font-size: 20px; margin-top: 28px; border-bottom: 1px solid var(--line); padding-bottom: 4px; } .body h3 { font-size: 16px; }
52
+ .body table { border-collapse: collapse; } .body th, .body td { border: 1px solid var(--line); padding: 4px 8px; }
53
+ .body em:only-child { color: var(--muted); }
54
+ .findings { margin: 10px 0 16px; padding: 0; list-style: none; font-size: 14px; }
55
+ .findings li { padding: 4px 8px; border-left: 3px solid var(--warn); background: var(--panel); margin-bottom: 4px; border-radius: 0 4px 4px 0; }
56
+ .findings li.error { border-left-color: var(--error); }
57
+ .findings code { color: var(--muted); }
58
+ .badge { display: inline-block; font-size: 12px; padding: 1px 8px; border-radius: 10px; background: var(--panel); border: 1px solid var(--line); color: var(--muted); margin-left: 6px; }
59
+ .badge.error { color: var(--error); border-color: var(--error); } .badge.warn { color: var(--warn); border-color: var(--warn); } .badge.ok { color: var(--ok); border-color: var(--ok); }
60
+ textarea.editor { min-height: 60vh; font-family: var(--mono); font-size: 13px; line-height: 1.45; white-space: pre; }
61
+ .form { display: grid; grid-template-columns: 140px 1fr; gap: 10px 14px; max-width: 640px; align-items: center; margin: 16px 0; }
62
+ .form label { color: var(--muted); font-size: 14px; }
63
+ .toast { position: fixed; right: 20px; bottom: 20px; background: var(--text); color: var(--bg); padding: 10px 16px; border-radius: 8px; max-width: 420px; font-size: 14px; white-space: pre-wrap; box-shadow: 0 6px 24px rgba(0,0,0,.25); }
64
+ .file-group { margin: 18px 0 6px; font-family: var(--mono); font-size: 14px; }
65
+ .file-group a { text-decoration: none; }
66
+ .hint { color: var(--muted); font-size: 13px; }
67
+ .empty { color: var(--muted); padding: 40px 0; }
68
+ .fw h2 { margin-top: 26px; }
69
+ .fw dl { display: grid; grid-template-columns: max-content 1fr; gap: 4px 14px; }
70
+ .fw dt { color: var(--muted); }
71
+ @media (max-width: 760px) { .layout { grid-template-columns: 1fr; } aside { position: static; height: auto; } main { padding: 16px; } }
72
+ </style>
73
+ </head>
74
+ <body>
75
+ <div class="layout">
76
+ <aside>
77
+ <div class="brand"><strong id="rootName">Eidos</strong><small id="rootVersion"></small></div>
78
+ <input class="search" id="search" type="search" placeholder="Find a blueprint…" autocomplete="off">
79
+ <nav class="nav" id="nav"></nav>
80
+ </aside>
81
+ <main id="main"><div class="empty">Loading…</div></main>
82
+ </div>
83
+ <script>
84
+ (() => {
85
+ const $ = (sel, el = document) => el.querySelector(sel);
86
+ const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
87
+ const api = async (method, url, body) => {
88
+ const res = await fetch(url, { method, headers: body ? { 'content-type': 'application/json' } : {}, body: body ? JSON.stringify(body) : undefined });
89
+ const data = await res.json().catch(() => ({}));
90
+ if (!res.ok) throw new Error(data.error || res.statusText);
91
+ return data;
92
+ };
93
+ let state = { root: null, filter: '', flagged: new Set() };
94
+ let toastTimer;
95
+ const toast = (msg, ms = 4200) => {
96
+ let el = $('.toast'); if (!el) { el = document.createElement('div'); el.className = 'toast'; document.body.appendChild(el); }
97
+ el.textContent = msg; clearTimeout(toastTimer); toastTimer = setTimeout(() => el.remove(), ms);
98
+ };
99
+ const route = () => { const h = location.hash.replace(/^#\/?/, ''); const [view, ...rest] = h.split('/'); return { view: view || 'framework', arg: rest.join('/') }; };
100
+ const go = (path) => { location.hash = '#/' + path; };
101
+ const bpHref = (p) => '#/blueprint/' + p.split('/').map(encodeURIComponent).join('/');
102
+
103
+ async function loadRoot() {
104
+ state.root = await api('GET', '/api/root');
105
+ $('#rootName').textContent = state.root.name;
106
+ $('#rootVersion').textContent = `Eidos ${state.root.framework.eidos_version ?? '?'} · ${state.root.framework.file}`;
107
+ document.title = `${state.root.name} · Eidos`;
108
+ renderNav();
109
+ }
110
+
111
+ function renderNav() {
112
+ const { framework, blueprints } = state.root;
113
+ const q = state.filter.toLowerCase();
114
+ const cur = route();
115
+ const active = (h) => location.hash === h ? ' active' : '';
116
+ let html = `<a href="#/framework" class="${active('#/framework') || (cur.view === 'framework' ? 'active' : '')}">Framework</a>
117
+ <a href="#/check" class="${cur.view === 'check' ? 'active' : ''}">Check</a>
118
+ <a href="#/standard" class="${cur.view === 'standard' ? 'active' : ''}">The standard</a>
119
+ <a href="#/instructions/overview" class="${cur.view === 'instructions' ? 'active' : ''}">Agent instructions</a>`;
120
+ for (const c of framework.collections) {
121
+ const own = blueprints.filter((b) => b.collection === c.name && (!q || [b.title, b.id, b.summary, b.path].some((v) => (v || '').toLowerCase().includes(q))));
122
+ html += `<h3><span>${esc(c.name)}</span><button data-new="${esc(c.name)}" title="New blueprint in ${esc(c.name)}">+ new</button></h3>`;
123
+ const groups = [...new Set(own.map((b) => b.group).filter(Boolean))].sort();
124
+ const item = (b) => `<a class="bp${state.flagged.has(b.path) ? ' flagged' : ''}${cur.view === 'blueprint' && decodeURIComponent(cur.arg) === b.path ? ' active' : ''}" href="${bpHref(b.path)}" title="${esc(b.summary || b.path)}">${esc(b.title)}</a>`;
125
+ html += own.filter((b) => !b.group).map(item).join('');
126
+ for (const g of groups) html += `<h4>${esc(g)}</h4>` + own.filter((b) => b.group === g).map(item).join('');
127
+ if (own.length === 0) html += `<div class="hint" style="padding-left:12px">${q ? 'no match' : 'empty'}</div>`;
128
+ }
129
+ $('#nav').innerHTML = html;
130
+ }
131
+
132
+ const findingsHtml = (findings) => findings.length ? `<ul class="findings">${findings.map((f) => `<li class="${f.level}"><code>${esc(f.code)}</code> ${esc(f.message)}</li>`).join('')}</ul>` : '';
133
+
134
+ async function viewBlueprint(pathArg) {
135
+ const p = decodeURIComponent(pathArg);
136
+ const main = $('#main');
137
+ let b;
138
+ try { b = await api('GET', '/api/blueprints/' + p.split('/').map(encodeURIComponent).join('/')); }
139
+ catch (e) { main.innerHTML = `<div class="empty">${esc(e.message)}</div>`; return; }
140
+ const errors = b.findings.filter((f) => f.level === 'error').length, warns = b.findings.length - errors;
141
+ const badge = errors ? `<span class="badge error">${errors} error${errors > 1 ? 's' : ''}</span>` : '';
142
+ const wbadge = warns ? `<span class="badge warn">${warns} warning${warns > 1 ? 's' : ''}</span>` : '';
143
+ // a property value that is a markdown link opens the blueprint it points at
144
+ const val = (x) => { const m = typeof x === 'string' && /^\s*\[([^\]]*)\]\(([^)]+)\)\s*$/.exec(x); if (!m) return `<code>${esc(x)}</code>`; const hit = state.root.blueprints.find((y) => y.path === resolveRel(p, m[2].split('#')[0])); return hit ? `<a href="${bpHref(hit.path)}">${esc(m[1])}</a>` : `<span title="not a blueprint in this root: ${esc(m[2])}">${esc(m[1])}</span> <span class="badge error">broken</span>`; };
145
+ const props = Object.entries(b.properties).map(([k, v]) => `<tr><td>${esc(k)}</td><td>${Array.isArray(v) ? (v.length ? v.map(val).join(', ') : '<span class="hint">[]</span>') : (v === null || v === '' ? '<span class="hint">—</span>' : esc(v))}</td></tr>`).join('');
146
+ const missing = b.shape_sections.filter((s) => !b.sections.map((x) => x.toLowerCase()).includes(s.toLowerCase()));
147
+ main.innerHTML = `
148
+ <div class="toolbar"><span class="meta">${esc(b.collection)}${b.group ? ' / ' + esc(b.group) : ''} · <code>${esc(b.path)}</code> · ${esc(b.flavor || '')}</span><span class="spacer"></span>
149
+ <button id="edit">Edit</button></div>
150
+ <h1 class="title">${esc(b.title)}${badge}${wbadge}</h1>
151
+ <p class="meta">id <code>${esc(b.id || '(none)')}</code>${b.summary ? ' · ' + esc(b.summary) : ' · <span class="hint">no summary yet</span>'}</p>
152
+ ${findingsHtml(b.findings)}
153
+ ${missing.length ? `<p class="hint">Sections the ${esc(b.flavor)} shape declares and this blueprint lacks: ${missing.map((s) => `<code>${esc(s)}</code>`).join(', ')}</p>` : ''}
154
+ <table class="props">${props}</table>
155
+ <div class="body" id="body">${b.html}</div>
156
+ <div id="editor" hidden><textarea class="editor" id="text"></textarea>
157
+ <div class="toolbar" style="margin-top:10px"><button class="primary" id="save">Save</button><button id="cancel">Cancel</button><span class="hint">Saving runs the check on this file; the result shows above.</span></div></div>`;
158
+ if (b.findings.length) state.flagged.add(p); else state.flagged.delete(p);
159
+ renderNav();
160
+ // relative .md links open the blueprint they point at
161
+ for (const a of main.querySelectorAll('#body a[href]')) {
162
+ const href = a.getAttribute('href');
163
+ if (/^[a-z]+:/i.test(href) || href.startsWith('#')) continue;
164
+ const target = resolveRel(p, href.split('#')[0]);
165
+ const hit = state.root.blueprints.find((x) => x.path === target);
166
+ if (hit) { a.href = bpHref(hit.path); } else { a.title = 'not a blueprint in this root: ' + target; a.addEventListener('click', (e) => { e.preventDefault(); toast('Not a blueprint in this root: ' + target); }); }
167
+ }
168
+ $('#edit').onclick = () => { $('#body').hidden = true; $('#editor').hidden = false; $('#text').value = b.raw; $('#edit').hidden = true; $('#text').focus(); };
169
+ $('#cancel').onclick = () => viewBlueprint(pathArg);
170
+ $('#save').onclick = async () => {
171
+ try { const r = await api('PUT', '/api/blueprints/' + p.split('/').map(encodeURIComponent).join('/'), { text: $('#text').value }); toast(r.findings.length ? `Saved. ${r.findings.length} finding${r.findings.length > 1 ? 's' : ''} on this file.` : 'Saved. No findings.'); await loadRoot(); viewBlueprint(pathArg); }
172
+ catch (e) { toast('Save failed: ' + e.message); }
173
+ };
174
+ }
175
+ function resolveRel(from, href) {
176
+ const parts = from.split('/').slice(0, -1);
177
+ for (const seg of decodeURIComponent(href).split('/')) { if (seg === '..') parts.pop(); else if (seg !== '.' && seg !== '') parts.push(seg); }
178
+ return parts.join('/');
179
+ }
180
+
181
+ function viewFramework() {
182
+ const f = state.root.framework, a = state.root.actor;
183
+ const flav = (c) => c.flavors.map((x) => `<code>${esc(x.name)}</code>${x.default ? ' (default)' : ''}${x.description ? ' — ' + esc(x.description) : ''}`).join('<br>');
184
+ const canvas = (c) => !c.canvas ? '' : typeof c.canvas === 'string' ? c.canvas : `card from ## ${esc(c.canvas.section)}`;
185
+ $('#main').innerHTML = `<div class="fw">
186
+ <h1 class="title">${esc(state.root.name)}</h1>
187
+ <p class="meta">Eidos ${esc(f.eidos_version ?? 'unversioned')} · naming <code>${esc(f.naming)}</code> · <code>${esc(f.file)}</code> · CLI ${esc(state.root.cli_version)} (standard ${esc(state.root.standard_version)})</p>
188
+ ${f.problems.length ? findingsHtml(f.problems.map((m) => ({ level: 'error', code: 'framework-invalid', message: m }))) : ''}
189
+ <div class="toolbar"><button id="reindex">Rebuild index</button><button id="runcheck">Run check</button><span class="hint">Both run exactly what <code>eidos index</code> and <code>eidos check</code> run.</span></div>
190
+ <h2>Actor</h2><p>${a.exists && a.role ? `<strong>${esc(a.role)}</strong> · ownership: ${esc(a.ownership || '—')} · experience: ${esc(a.experience || '—')} · capacity: ${esc(a.capacity || '—')}` : `<span class="hint">unset (full facilitation) — set it with <code>eidos whoami --role &lt;name&gt;</code></span>`}</p>
191
+ <p class="hint">Roles installed: ${state.root.roles.map((r) => `<code>${esc(r.name)}</code>`).join(', ') || 'none'}</p>
192
+ <h2>Collections</h2>
193
+ ${f.collections.map((c, i) => `<h3>${esc(c.name)}${i === 0 ? ' <span class="badge">framing</span>' : ''}${c.grouping ? ` <span class="badge">grouped by ${esc(c.grouping.label)}${c.grouping.property ? ' (' + esc(c.grouping.property) + ')' : ''}</span>` : ''}</h3>
194
+ ${c.description ? `<p>${esc(c.description)}</p>` : ''}
195
+ <dl><dt>flavors</dt><dd>${flav(c)}</dd>${c.canvas ? `<dt>canvas</dt><dd>${canvas(c)}</dd>` : ''}${c.grouping && c.grouping.groups ? `<dt>groups</dt><dd>${c.grouping.groups.map((g) => `<code>${esc(g.name)}</code>${g.description ? ' — ' + esc(g.description) : ''}`).join('<br>')}</dd>` : ''}</dl>`).join('')}
196
+ <h2>Schema</h2>
197
+ <table class="props"><tr><td>core</td><td>${f.schema.core.map((p) => `<code>${esc(p.name)}</code>`).join(' ')}</td></tr>
198
+ ${f.schema.custom.map((p) => `<tr><td>${esc(p.name)}</td><td><span class="badge">${esc(p.type)}</span> <span class="badge">${Array.isArray(p.applies_to) ? esc(p.applies_to.join(', ')) : 'all'}</span> ${esc(p.meaning)}</td></tr>`).join('')}</table>
199
+ ${f.top_level && f.top_level.length ? `<h2>Top-level docs</h2><ul>${f.top_level.map((d) => `<li><strong>${esc(d.title)}</strong> <code>${esc(d.path)}</code>${d.description ? ' — ' + esc(d.description) : ''}</li>`).join('')}</ul>` : ''}
200
+ </div>`;
201
+ $('#reindex').onclick = async () => { try { const r = await api('POST', '/api/index'); toast(r.indexes.map((i) => `${i.collection}: ${i.status} (${i.blueprints})`).join('\n') + (r.missing_summary.length ? `\n${r.missing_summary.length} blueprint(s) still need a summary` : '')); } catch (e) { toast(e.message); } };
202
+ $('#runcheck').onclick = () => go('check');
203
+ }
204
+
205
+ async function viewCheck() {
206
+ const main = $('#main'); main.innerHTML = '<div class="empty">Checking…</div>';
207
+ const r = await api('GET', '/api/check');
208
+ state.flagged = new Set(r.findings.map((f) => f.path).filter(Boolean));
209
+ renderNav();
210
+ const byFile = new Map();
211
+ for (const f of r.findings) { const k = f.path || '(root)'; if (!byFile.has(k)) byFile.set(k, []); byFile.get(k).push(f); }
212
+ const isBp = (k) => state.root.blueprints.some((b) => b.path === k);
213
+ main.innerHTML = `<h1 class="title">Check <span class="badge ${r.errors ? 'error' : r.warnings ? 'warn' : 'ok'}">${r.errors} error${r.errors === 1 ? '' : 's'}, ${r.warnings} warning${r.warnings === 1 ? '' : 's'}</span></h1>
214
+ <p class="meta">${r.blueprints} blueprints in ${r.collections} collections · Eidos ${esc(r.eidos_version ?? '?')} · ${esc(r.naming)} · the same report as <code>eidos check</code></p>
215
+ ${r.findings.length === 0 ? '<p class="empty">No findings.</p>' : ''}
216
+ ${[...byFile.entries()].map(([k, fs]) => `<div class="file-group">${isBp(k) ? `<a href="${bpHref(k)}">${esc(k)}</a>` : esc(k)}</div>${findingsHtml(fs)}`).join('')}`;
217
+ }
218
+
219
+ async function viewDoc(url, title) {
220
+ const main = $('#main'); main.innerHTML = '<div class="empty">Loading…</div>';
221
+ const r = await api('GET', url);
222
+ main.innerHTML = `<div class="body" style="border:0">${r.html}</div>`;
223
+ }
224
+
225
+ function viewNew(collectionName) {
226
+ const f = state.root.framework;
227
+ const c = f.collections.find((x) => x.name === collectionName) || f.collections[0];
228
+ const groups = (c.grouping && c.grouping.groups) ? c.grouping.groups.map((g) => g.name) : [];
229
+ $('#main').innerHTML = `<h1 class="title">New blueprint</h1>
230
+ <p class="hint">Runs <code>eidos new</code>: frontmatter from the Schema, body from the flavor's shape, filename in the naming convention. You fill the prose afterwards.</p>
231
+ <div class="form">
232
+ <label>Collection</label><select id="n-col">${f.collections.map((x) => `<option ${x.name === c.name ? 'selected' : ''}>${esc(x.name)}</option>`).join('')}</select>
233
+ <label>Title</label><input id="n-title" placeholder="Session Management" autofocus>
234
+ <label>Flavor</label><select id="n-flavor">${c.flavors.map((x) => `<option ${x.default ? 'selected' : ''}>${esc(x.name)}</option>`).join('')}</select>
235
+ <label>Group</label><input id="n-group" list="n-groups" placeholder="${groups.length ? 'one of the declared groups, or a new one' : c.grouping ? c.grouping.label : 'optional sub-folder'}"><datalist id="n-groups">${groups.map((g) => `<option value="${esc(g)}">`).join('')}</datalist>
236
+ <label>Summary</label><input id="n-summary" placeholder="One plain line: what this blueprint is.">
237
+ <span></span><div class="toolbar"><button class="primary" id="n-create">Create</button><button id="n-cancel">Cancel</button></div>
238
+ </div>`;
239
+ $('#n-col').onchange = () => viewNew($('#n-col').value);
240
+ $('#n-cancel').onclick = () => history.back();
241
+ $('#n-create').onclick = async () => {
242
+ try {
243
+ const r = await api('POST', '/api/blueprints', { collection: $('#n-col').value, title: $('#n-title').value, flavor: $('#n-flavor').value, group: $('#n-group').value, summary: $('#n-summary').value });
244
+ toast(`Created ${r.path}` + (r.notes.length ? '\n' + r.notes.join('\n') : ''), 6000);
245
+ await loadRoot(); go('blueprint/' + r.path.split('/').map(encodeURIComponent).join('/'));
246
+ } catch (e) { toast(e.message, 6000); }
247
+ };
248
+ }
249
+
250
+ async function render() {
251
+ const { view, arg } = route();
252
+ renderNav();
253
+ if (view === 'blueprint') return viewBlueprint(arg);
254
+ if (view === 'check') return viewCheck();
255
+ if (view === 'standard') return viewDoc('/api/standard');
256
+ if (view === 'instructions') return viewDoc('/api/instructions/' + (arg || 'overview'));
257
+ if (view === 'new') return viewNew(decodeURIComponent(arg));
258
+ return viewFramework();
259
+ }
260
+
261
+ $('#search').addEventListener('input', (e) => { state.filter = e.target.value; renderNav(); });
262
+ $('#nav').addEventListener('click', (e) => { const b = e.target.closest('button[data-new]'); if (b) go('new/' + encodeURIComponent(b.dataset.new)); });
263
+ window.addEventListener('hashchange', render);
264
+ loadRoot().then(render).catch((e) => { $('#main').innerHTML = `<div class="empty">${esc(e.message)}</div>`; });
265
+ })();
266
+ </script>
267
+ </body>
268
+ </html>
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ // The executable entry point: one root of Eidos blueprints, scaffolded,
3
+ // validated, and indexed from the shell.
4
+ import { buildProgram } from './program.js';
5
+ buildProgram().parse(process.argv);
@@ -0,0 +1,47 @@
1
+ // `eidos agents`: the short nudge that points an agent at `eidos instructions`,
2
+ // printed, or written into an instruction file such as AGENTS.md or CLAUDE.md.
3
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { EXIT_OK, print } from '../output.js';
6
+ export const NUDGE_START = '<!-- eidos:instructions:start -->';
7
+ export const NUDGE_END = '<!-- eidos:instructions:end -->';
8
+ export function nudge() {
9
+ return [
10
+ NUDGE_START,
11
+ '## Eidos',
12
+ '',
13
+ 'This repository keeps its blueprints in an Eidos root. Before creating, editing, reviewing, or discussing a blueprint or the framework, run `eidos instructions overview` and follow it; it names the guide to read for each kind of work. Use the `eidos` CLI for structure (`eidos new`, `eidos check`, `eidos index`); never hand-assemble frontmatter or edit an `index.md`.',
14
+ NUDGE_END,
15
+ ].join('\n');
16
+ }
17
+ export function runAgents(options, cwd = process.cwd()) {
18
+ const block = nudge();
19
+ if (!options.write) {
20
+ print(block, '', `write it: eidos agents --write [--file ${options.file}]`);
21
+ return EXIT_OK;
22
+ }
23
+ const file = path.resolve(cwd, options.file);
24
+ const rel = path.relative(cwd, file) || options.file;
25
+ if (!existsSync(file)) {
26
+ writeFileSync(file, block + '\n', 'utf8');
27
+ print(`✓ created ${rel} with the Eidos nudge`);
28
+ return EXIT_OK;
29
+ }
30
+ const current = readFileSync(file, 'utf8');
31
+ const start = current.indexOf(NUDGE_START);
32
+ const end = current.indexOf(NUDGE_END);
33
+ if (start !== -1 && end !== -1 && end > start) {
34
+ const updated = current.slice(0, start) + block + current.slice(end + NUDGE_END.length);
35
+ if (updated === current) {
36
+ print(`✓ ${rel} already carries the current nudge`);
37
+ return EXIT_OK;
38
+ }
39
+ writeFileSync(file, updated, 'utf8');
40
+ print(`✓ refreshed the Eidos nudge in ${rel}`);
41
+ return EXIT_OK;
42
+ }
43
+ const separator = current.endsWith('\n') ? (current.endsWith('\n\n') ? '' : '\n') : '\n\n';
44
+ writeFileSync(file, current + separator + block + '\n', 'utf8');
45
+ print(`✓ appended the Eidos nudge to ${rel}`);
46
+ return EXIT_OK;
47
+ }