@prestack/thread 0.0.3 → 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 (183) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/dist/core/{asking.d.ts → answers.d.ts} +6 -14
  3. package/dist/core/{asking.js → answers.js} +13 -332
  4. package/dist/core/{asking.ts → answers.ts} +14 -357
  5. package/dist/core/attach.js +2 -2
  6. package/dist/core/attach.ts +2 -3
  7. package/dist/core/chain.js +4 -11
  8. package/dist/core/chain.ts +4 -3
  9. package/dist/core/client.d.ts +57 -0
  10. package/dist/core/client.js +186 -0
  11. package/dist/core/client.ts +227 -0
  12. package/dist/core/descend.d.ts +2 -0
  13. package/dist/core/descend.js +109 -16
  14. package/dist/core/descend.ts +120 -16
  15. package/dist/core/files.d.ts +45 -0
  16. package/dist/core/files.js +95 -0
  17. package/dist/core/files.ts +137 -0
  18. package/dist/core/init.d.ts +1 -0
  19. package/dist/core/init.js +10 -2
  20. package/dist/core/init.ts +8 -2
  21. package/dist/core/invoked.js +1 -1
  22. package/dist/core/invoked.ts +1 -1
  23. package/dist/core/rows.d.ts +2 -0
  24. package/dist/core/rows.js +65 -0
  25. package/dist/core/rows.ts +58 -0
  26. package/dist/core/tree.js +1 -1
  27. package/dist/core/tree.ts +1 -1
  28. package/dist/derivation/attach.js +0 -12
  29. package/dist/derivation/attach.ts +0 -12
  30. package/dist/derivation/generators/build.js +8 -7
  31. package/dist/derivation/generators/build.ts +8 -7
  32. package/dist/derivation/generators/release.d.ts +1 -1
  33. package/dist/derivation/generators/release.js +15 -23
  34. package/dist/derivation/generators/release.ts +15 -15
  35. package/dist/integrations/attach.js +1 -19
  36. package/dist/integrations/attach.ts +1 -19
  37. package/dist/integrations/generators/read.js +2 -2
  38. package/dist/integrations/generators/read.ts +4 -4
  39. package/dist/integrations/generators/vault.js +4 -12
  40. package/dist/integrations/generators/vault.ts +4 -4
  41. package/dist/integrations/module.js +5 -3
  42. package/dist/integrations/module.ts +5 -3
  43. package/dist/integrations/vault.js +5 -5
  44. package/dist/integrations/vault.ts +5 -5
  45. package/dist/integrity/attach.js +0 -12
  46. package/dist/integrity/attach.ts +0 -12
  47. package/dist/integrity/check-threads.js +1 -1
  48. package/dist/integrity/check-threads.ts +1 -1
  49. package/dist/integrity/constraint.d.ts +11 -2
  50. package/dist/integrity/constraint.js +163 -20
  51. package/dist/integrity/constraint.ts +174 -22
  52. package/dist/integrity/generators/compiler.js +1 -1
  53. package/dist/integrity/generators/compiler.ts +1 -1
  54. package/dist/integrity/generators/gate.js +8 -16
  55. package/dist/integrity/generators/gate.ts +8 -7
  56. package/dist/integrity/generators/publish.js +14 -26
  57. package/dist/integrity/generators/publish.ts +14 -17
  58. package/dist/integrity/generators/trial.d.ts +2 -2
  59. package/dist/integrity/generators/trial.js +108 -50
  60. package/dist/integrity/generators/trial.ts +107 -48
  61. package/dist/integrity/lint.js +3 -3
  62. package/dist/integrity/lint.ts +3 -3
  63. package/dist/integrity/lsp/diagnostics.js +1 -1
  64. package/dist/integrity/lsp/diagnostics.ts +1 -1
  65. package/dist/integrity/lsp/lint.js +1 -1
  66. package/dist/integrity/lsp/lint.ts +1 -1
  67. package/dist/integrity/slopnator.d.ts +1 -14
  68. package/dist/integrity/slopnator.js +8 -40
  69. package/dist/integrity/slopnator.ts +9 -51
  70. package/dist/integrity/slopnators/sl_broken_lineage.js +4 -2
  71. package/dist/integrity/slopnators/sl_broken_lineage.ts +4 -2
  72. package/dist/integrity/slopnators/sl_declared_elsewhere.js +24 -3
  73. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +24 -3
  74. package/dist/integrity/slopnators/sl_derived_drift.js +10 -4
  75. package/dist/integrity/slopnators/sl_derived_drift.ts +10 -4
  76. package/dist/integrity/slopnators/sl_inert_island.js +7 -13
  77. package/dist/integrity/slopnators/sl_inert_island.ts +7 -16
  78. package/dist/integrity/slopnators/sl_inert_slopnator.js +8 -8
  79. package/dist/integrity/slopnators/sl_inert_slopnator.ts +8 -8
  80. package/dist/integrity/slopnators/sl_missing_row.js +14 -9
  81. package/dist/integrity/slopnators/sl_missing_row.ts +14 -10
  82. package/dist/integrity/slopnators/sl_no_child_process.js +8 -8
  83. package/dist/integrity/slopnators/sl_no_child_process.ts +8 -8
  84. package/dist/integrity/slopnators/sl_no_import_cycle.js +1 -1
  85. package/dist/integrity/slopnators/sl_no_import_cycle.ts +1 -1
  86. package/dist/integrity/slopnators/sl_no_such_command.js +20 -5
  87. package/dist/integrity/slopnators/sl_no_such_command.ts +22 -5
  88. package/dist/integrity/slopnators/sl_only_takes_a_literal.js +8 -8
  89. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +8 -8
  90. package/dist/integrity/slopnators/sl_stale_path.js +91 -18
  91. package/dist/integrity/slopnators/sl_stale_path.ts +97 -18
  92. package/dist/integrity/slopnators/sl_twice_meant.js +25 -2
  93. package/dist/integrity/slopnators/sl_twice_meant.ts +28 -2
  94. package/dist/integrity/slopnators/sl_twice_parented.js +26 -5
  95. package/dist/integrity/slopnators/sl_twice_parented.ts +26 -5
  96. package/dist/integrity/slopnators/sl_undeclared_node.js +18 -2
  97. package/dist/integrity/slopnators/sl_undeclared_node.ts +18 -2
  98. package/dist/integrity/slopnators/sl_undeclared_operation.js +3 -1
  99. package/dist/integrity/slopnators/sl_undeclared_operation.ts +3 -1
  100. package/dist/integrity/slopnators/sl_undeclared_variant.js +6 -2
  101. package/dist/integrity/slopnators/sl_undeclared_variant.ts +6 -2
  102. package/dist/integrity/slopnators/sl_ungoverned_island.js +3 -4
  103. package/dist/integrity/slopnators/sl_ungoverned_island.ts +3 -4
  104. package/dist/integrity/slopnators/sl_unkept_rows.js +1 -1
  105. package/dist/integrity/slopnators/sl_unkept_rows.ts +1 -1
  106. package/dist/integrity/slopnators/sl_unpaired_member.js +13 -8
  107. package/dist/integrity/slopnators/sl_unpaired_member.ts +16 -8
  108. package/dist/integrity/slopnators/sl_unpaired_slopnator.js +16 -3
  109. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +16 -3
  110. package/dist/integrity/slopnators/sl_unthreaded.d.ts +1 -0
  111. package/dist/integrity/slopnators/sl_unthreaded.js +18 -10
  112. package/dist/integrity/slopnators/sl_unthreaded.ts +19 -11
  113. package/dist/integrity/slopnators/sl_untranslated_key.js +1 -1
  114. package/dist/integrity/slopnators/sl_untranslated_key.ts +1 -1
  115. package/dist/integrity/slopnators/sl_unversioned.js +16 -2
  116. package/dist/integrity/slopnators/sl_unversioned.ts +16 -2
  117. package/dist/integrity/sweep.js +17 -3
  118. package/dist/integrity/sweep.ts +20 -3
  119. package/dist/knowledge/attach.js +0 -6
  120. package/dist/knowledge/attach.ts +0 -6
  121. package/dist/knowledge/generators/docsgen.js +12 -3
  122. package/dist/knowledge/generators/docsgen.ts +12 -2
  123. package/dist/knowledge/generators/glossary.js +1 -1
  124. package/dist/knowledge/generators/glossary.ts +1 -1
  125. package/dist/operations/attach.js +2 -26
  126. package/dist/operations/attach.ts +2 -27
  127. package/dist/operations/dispatch.js +2 -3
  128. package/dist/operations/dispatch.ts +2 -3
  129. package/dist/operations/generators/capability.js +4 -7
  130. package/dist/operations/generators/capability.ts +3 -6
  131. package/dist/operations/generators/compile.js +8 -13
  132. package/dist/operations/generators/compile.ts +8 -4
  133. package/dist/operations/generators/permits.js +4 -13
  134. package/dist/operations/generators/permits.ts +3 -3
  135. package/dist/operations/generators/seed.js +5 -2
  136. package/dist/operations/generators/seed.ts +6 -2
  137. package/dist/operations/generators/trial.js +10 -10
  138. package/dist/operations/generators/trial.ts +10 -10
  139. package/dist/operations/http.js +4 -1
  140. package/dist/operations/http.ts +6 -1
  141. package/package.json +2 -2
  142. package/dist/core/cli.d.ts +0 -2
  143. package/dist/core/cli.js +0 -1761
  144. package/dist/core/cli.ts +0 -2004
  145. package/dist/core/generators/meta.d.ts +0 -2
  146. package/dist/core/generators/meta.js +0 -829
  147. package/dist/core/generators/meta.ts +0 -1006
  148. package/dist/derivation/generators/derivation.d.ts +0 -3
  149. package/dist/derivation/generators/derivation.js +0 -273
  150. package/dist/derivation/generators/derivation.ts +0 -347
  151. package/dist/derivation/makers/release.d.ts +0 -1
  152. package/dist/derivation/makers/release.js +0 -107
  153. package/dist/derivation/makers/release.ts +0 -135
  154. package/dist/integrations/generators/secret.d.ts +0 -1
  155. package/dist/integrations/generators/secret.js +0 -233
  156. package/dist/integrations/generators/secret.ts +0 -289
  157. package/dist/integrations/generators/target.d.ts +0 -1
  158. package/dist/integrations/generators/target.js +0 -775
  159. package/dist/integrations/generators/target.ts +0 -944
  160. package/dist/integrity/generators/rule.d.ts +0 -2
  161. package/dist/integrity/generators/rule.js +0 -213
  162. package/dist/integrity/generators/rule.ts +0 -271
  163. package/dist/integrity/generators/seal.d.ts +0 -4
  164. package/dist/integrity/generators/seal.js +0 -101
  165. package/dist/integrity/generators/seal.ts +0 -117
  166. package/dist/integrity/sealed.d.ts +0 -1
  167. package/dist/integrity/sealed.js +0 -56
  168. package/dist/integrity/sealed.ts +0 -56
  169. package/dist/knowledge/generators/skills.d.ts +0 -1
  170. package/dist/knowledge/generators/skills.js +0 -222
  171. package/dist/knowledge/generators/skills.ts +0 -263
  172. package/dist/operations/generators/grant.d.ts +0 -1
  173. package/dist/operations/generators/grant.js +0 -267
  174. package/dist/operations/generators/grant.ts +0 -336
  175. package/dist/operations/generators/operation.d.ts +0 -1
  176. package/dist/operations/generators/operation.js +0 -811
  177. package/dist/operations/generators/operation.ts +0 -972
  178. package/dist/operations/makers/capability.d.ts +0 -1
  179. package/dist/operations/makers/capability.js +0 -117
  180. package/dist/operations/makers/capability.ts +0 -166
  181. package/dist/operations/makers/trial.d.ts +0 -1
  182. package/dist/operations/makers/trial.js +0 -413
  183. package/dist/operations/makers/trial.ts +0 -520
package/dist/core/cli.js DELETED
@@ -1,1761 +0,0 @@
1
- #!/usr/bin/env node
2
- var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
3
- if (typeof path === "string" && /^\.\.?\//.test(path)) {
4
- return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
5
- return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
6
- });
7
- }
8
- return path;
9
- };
10
- import { argv, cwd, exit, stderr, stdout } from 'node:process';
11
- import { spawnSync } from 'node:child_process';
12
- import { existsSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
13
- import { createRequire } from 'node:module';
14
- import { join, relative, resolve } from 'node:path';
15
- import { fileURLToPath, pathToFileURL } from 'node:url';
16
- import { CARRIES, CHAINED, RECEIPT, TYPED, VALUED, VERBS, WRITES, answer, answered, asFlag, asking, children, childrenIn, declared, given, handed, nouns, ownAnswers, reminded, rowLines, seeding, taught, withChildren } from "./asking.js";
17
- import { LISTS, descend } from "./descend.js";
18
- import { BESIDE, LOADER, refusal } from "./invoked.js";
19
- import { NAMED, ORGANISATION, idOf, named, ours } from "./scope.js";
20
- import { findTree } from "./tree.js";
21
- const chain = () => import("./chain.js");
22
- const attaching = () => import("./attach.js");
23
- const starting = () => import("./init.js");
24
- const COMMANDS = ['init', 'attach', 'detach', 'create', 'run', 'read', 'edit', 'list', 'help', 'repair'];
25
- const SKIPS = new Set(['node_modules', '.git', 'dist', '.next', '.thread']);
26
- const FINDS = '--find';
27
- const REPLACES = '--replace';
28
- const FROM = '--from';
29
- const REMOVES = '--remove';
30
- const first = (told) => {
31
- const one = told.split(/(?<=\.)\s/)[0] ?? told;
32
- return one.length > 88 ? `${one.slice(0, 87)}…` : one;
33
- };
34
- const opening = (told) => told.split(/(?<=\.)\s/)[0] ?? told;
35
- const over = (said, pad, room) => {
36
- const out = [];
37
- let line = '';
38
- for (const word of said.split(/\s+/)) {
39
- if (line === '')
40
- line = word;
41
- else if (`${line} ${word}`.length <= room)
42
- line = `${line} ${word}`;
43
- else {
44
- out.push(line);
45
- line = word;
46
- }
47
- }
48
- if (line !== '')
49
- out.push(line);
50
- return out.map((one, at) => (at === 0 ? one : `${pad}${one}`));
51
- };
52
- const columned = (said, says, room = 22) => {
53
- const pad = ' '.repeat(room + 3);
54
- const [head, ...rest] = over(says, pad, 74);
55
- return [` ${said.padEnd(room)} ${head ?? ''}`, ...rest];
56
- };
57
- const commanded = (word, one) => {
58
- const meaning = one.describes.trim().replace(/\.$/, '');
59
- const opens = `Declares ${/^[aeiou]/.test(word) ? 'an' : 'a'} ${word}`;
60
- return meaning === '' ? `${opens}.` : `${opens} — ${meaning.slice(0, 1).toLowerCase()}${meaning.slice(1)}.`;
61
- };
62
- const fielded = (word, one) => {
63
- if (one === undefined || one.fields.length === 0)
64
- return [];
65
- const room = Math.max(...one.fields.map((each) => each.name.length));
66
- const kind = (each) => each.list ? 'a list' : each.map ? 'a table' : each.boolean ? 'yes or no' : each.worded ? 'a word' : 'a value';
67
- const base = ['id', 'at', 'describes', 'version'];
68
- const order = (each) => {
69
- const held = base.indexOf(each.name);
70
- return held === -1 ? (each.required ? 100 : 200) : held;
71
- };
72
- return [
73
- '',
74
- `every ${word} row states these:`,
75
- '',
76
- ...[...one.fields].sort((a, b) => order(a) - order(b) || (a.name < b.name ? -1 : 1)).flatMap((each) => columned(each.name, `${each.required ? '' : 'optional, '}${kind(each)}${each.guide === '' ? '' : `. ${each.guide}`}`, room + 2)),
77
- ];
78
- };
79
- const doing = (word, holds, answers, at) => {
80
- const held = [
81
- ...(holds === undefined
82
- ? []
83
- : [
84
- ...columned(`${TYPED} create ${word}`, 'What it asks for, and everything already declared.', 34),
85
- ...columned(`${TYPED} create ${word} read <mark>`, 'One of them in full. It writes nothing.', 34),
86
- ]),
87
- ...(answers?.by === undefined
88
- ? []
89
- : [
90
- ...columned(answers.by, first(answers.describes ?? ''), 34),
91
- ...(answers.runs === undefined ? [] : columned(answers.runs, 'Proves it is still what its command writes.', 34)),
92
- ]),
93
- ...(at === undefined ? [] : columned(`${TYPED} read ${at}`, 'Prints the file itself.', 34)),
94
- ];
95
- return held.length === 0 ? [] : ['', 'to do something with it:', ...held];
96
- };
97
- const OWN = [
98
- {
99
- said: 'read <path>',
100
- describes: 'Print any file with line numbers, or list a directory. `--find <text>` searches every file in this tree. Reach for this first; `inspect <word>` reads rows instead, which is a different question.',
101
- },
102
- { said: 'init' },
103
- { said: 'attach <package>' },
104
- { said: 'detach <package>', describes: 'Give a package back, withdrawing every row it put here.' },
105
- { said: 'create <noun>', describes: 'Declare a thing. Run it with no noun to see what this tree makes.' },
106
- { said: 'run <noun>', describes: 'Run a thing that was declared.' },
107
- {
108
- said: 'edit <path|row>',
109
- describes: 'Change one stretch of a file this tree declares, or one field of a row. Taking a thing out is one of the changes rather than a command of its own.',
110
- },
111
- {
112
- said: 'list',
113
- describes: 'Every command this tree answers, what each one is for, and what each thing you can declare holds.',
114
- },
115
- {
116
- said: 'help <word>',
117
- describes: 'What one word in this tree means, what declares it, what it declares, and what to run next.',
118
- },
119
- {
120
- said: 'repair',
121
- describes: 'Writes every derived file again from the rows, running each generator until a full pass changes nothing, and touches nothing you wrote. Run it when the package manifests fell out of step with the rows and pnpm refuses to start: it needs no pnpm in front of it, so `npx thread repair` in an app and `node --import tsx thread/core/cli.ts repair` in the framework repository both reach it.',
122
- },
123
- ];
124
- let COINED;
125
- const coining = () => {
126
- const found = new Map();
127
- try {
128
- const pass = descend();
129
- for (const [, threadings] of pass.threaded) {
130
- for (const one of threadings) {
131
- const id = one.row.id;
132
- const says = one.row.describes;
133
- if (typeof id === 'string' && typeof says === 'string' && says !== '' && !found.has(id))
134
- found.set(id, says);
135
- }
136
- }
137
- for (const [id, row] of pass.stated) {
138
- const says = row.describes;
139
- if (typeof says === 'string' && says !== '' && !found.has(id))
140
- found.set(id, says);
141
- }
142
- }
143
- catch {
144
- return found;
145
- }
146
- return found;
147
- };
148
- const coined = () => (COINED ??= coining());
149
- const verb = (one) => one.said.split(' ')[0] ?? one.said;
150
- const framework = () => OWN.map((one) => ({ said: one.said, describes: coined().get(verb(one)) ?? one.describes ?? '' }));
151
- const WORKS = [
152
- 'Everything this app is, it DECLARES — in `_threads/`, one row per thing. You never write those rows',
153
- 'by hand: `npx thread create <noun>` asks what it needs and writes one. Everything else is DERIVED',
154
- 'from them — the schema, the migrations, the routes, the docs — by generators that re-run themselves',
155
- 'whenever a row changes, so nothing has to be kept in step. `npx thread gate` refuses the tree if any',
156
- 'of it has drifted, and a build reads that refusal.',
157
- ];
158
- const heading = (short) => [
159
- ...framework().flatMap((one) => columned(one.said, short ? opening(one.describes) : one.describes)),
160
- ...untold(),
161
- ];
162
- const untold = () => {
163
- const held = OWN.map(verb).filter((one) => !coined().has(one));
164
- if (held.length === 0)
165
- return [];
166
- return [
167
- '',
168
- ...over(`${held.join(', ')} — no row coins these, so the sentence beside each is typed into this command rather ` +
169
- `than read from the tree. \`${TYPED} create concept\` declares one, and this page reads it from there.`, '', 88),
170
- ];
171
- };
172
- const WITHIN = '--in';
173
- function within(root, path) {
174
- const held = resolve(root, path);
175
- return held === root || held.startsWith(`${root}/`);
176
- }
177
- function every(at, root, held = []) {
178
- let entries = [];
179
- try {
180
- entries = readdirSync(at, { withFileTypes: true });
181
- }
182
- catch {
183
- return held;
184
- }
185
- for (const entry of entries) {
186
- if (SKIPS.has(entry.name))
187
- continue;
188
- const path = join(at, entry.name);
189
- if (entry.isDirectory())
190
- every(path, root, held);
191
- else
192
- held.push(path);
193
- }
194
- return held;
195
- }
196
- function reading(root) {
197
- const scoped = argv.indexOf(WITHIN);
198
- let where = root;
199
- if (scoped !== -1) {
200
- const named = argv[scoped + 1];
201
- if (named === undefined || named.startsWith('-')) {
202
- stderr.write(`npx thread read ${WITHIN} <path> — which directory to look in. It is a place in this app, e.g. ${WITHIN} _threads.\n`);
203
- return 1;
204
- }
205
- where = resolve(root, named);
206
- }
207
- const found = argv.indexOf(FINDS);
208
- if (found !== -1) {
209
- const looked = argv[found + 1];
210
- if (looked === undefined || looked.startsWith('-')) {
211
- stderr.write(`npx thread read ${FINDS} <text> — what to look for, in every file this app owns.\n`);
212
- return 1;
213
- }
214
- let hits = 0;
215
- for (const path of every(where, root)) {
216
- let text = '';
217
- try {
218
- text = readFileSync(path, 'utf8');
219
- }
220
- catch {
221
- continue;
222
- }
223
- if (!text.includes(looked))
224
- continue;
225
- const lines = text.split('\n');
226
- for (let at = 0; at < lines.length; at += 1) {
227
- if (!(lines[at] ?? '').includes(looked))
228
- continue;
229
- hits += 1;
230
- stdout.write(`${relative(root, path)}:${String(at + 1)}: ${(lines[at] ?? '').trim()}\n`);
231
- }
232
- }
233
- stdout.write(`read: ${String(hits)} match(es) for ${JSON.stringify(looked)}${scoped === -1 ? ' in this whole app' : ` under ${relative(root, where) || '.'}`}` +
234
- `${scoped === -1 && hits > 200 ? ` — narrow it with \`${WITHIN} <path>\`` : ''}.\n`);
235
- return hits === 0 ? 1 : 0;
236
- }
237
- const named = argv[3] === undefined || argv[3].startsWith('-') ? (scoped === -1 ? undefined : relative(root, where)) : argv[3];
238
- if (named === undefined) {
239
- stderr.write([
240
- `${TYPED} read <path> print a file this app owns, with line numbers`,
241
- `${TYPED} read <directory> list what is in it, so you can name the next thing to read`,
242
- `${TYPED} read ${FINDS} <text> every line in this app that carries it, with where it is`,
243
- `${TYPED} read ${FINDS} <text> ${WITHIN} <path> the same, looking only under that directory`,
244
- '',
245
- 'it reads and never writes, and it refuses nothing — any path on this machine, inside this app',
246
- 'or outside it. `npx thread inspect <word>` reads the rows instead — every row whose name or',
247
- 'sentence carries that word, which is a different question from every line that does.',
248
- '',
249
- ].join('\n'));
250
- return 1;
251
- }
252
- const at = resolve(root, named);
253
- let held;
254
- try {
255
- held = readdirSync(at, { withFileTypes: true });
256
- }
257
- catch {
258
- held = undefined;
259
- }
260
- if (held !== undefined) {
261
- const kept = held.filter((one) => !SKIPS.has(one.name)).sort((one, two) => (one.name < two.name ? -1 : 1));
262
- for (const one of kept)
263
- stdout.write(`${relative(root, join(at, one.name))}${one.isDirectory() ? '/' : ''}\n`);
264
- stdout.write(`read: ${String(kept.length)} thing(s) in ${relative(root, at) || '.'}${held.length === kept.length ? '' : `, and ${String(held.length - kept.length)} not this app's to read`}. Name one to read it.\n`);
265
- return 0;
266
- }
267
- let text = '';
268
- try {
269
- text = readFileSync(at, 'utf8');
270
- }
271
- catch (thrown) {
272
- stderr.write(`thread: ${named} could not be read — ${thrown instanceof Error ? thrown.message : String(thrown)}\n`);
273
- return 1;
274
- }
275
- const lines = text.split('\n');
276
- const width = String(lines.length).length;
277
- for (let at = 0; at < lines.length; at += 1) {
278
- stdout.write(`${String(at + 1).padStart(width)} ${lines[at] ?? ''}\n`);
279
- }
280
- return 0;
281
- }
282
- function editable(root) {
283
- const rows = [];
284
- for (const [at, threadings] of descend().threaded) {
285
- if (threadings.some((one) => one.list === 'derives'))
286
- continue;
287
- if (!existsSync(join(root, at)))
288
- continue;
289
- const said = threadings.map((one) => one.row.describes).find((one) => typeof one === 'string' && one !== '');
290
- rows.push({ at, says: typeof said === 'string' ? said : '' });
291
- }
292
- return rows.sort((one, two) => (one.at < two.at ? -1 : 1));
293
- }
294
- function editing(root) {
295
- const named = argv[3];
296
- if (named === undefined || named.startsWith('-')) {
297
- const scoped = argv.indexOf(WITHIN);
298
- const under = scoped === -1 ? '' : (argv[scoped + 1] ?? '');
299
- let held = [];
300
- let broke = '';
301
- try {
302
- held = editable(root);
303
- }
304
- catch (thrown) {
305
- broke = thrown instanceof Error ? thrown.message : String(thrown);
306
- }
307
- const shown = under === '' ? held : held.filter((one) => one.at === under || one.at.startsWith(`${under.replace(/\/$/, '')}/`));
308
- stderr.write([
309
- `${TYPED} edit <path> ${FINDS} <text> ${REPLACES} <text>`,
310
- ' change one exact stretch of a file this tree declares.',
311
- '',
312
- 'It refuses unless that text appears exactly once, because an edit that could have landed in two',
313
- 'places is an edit nobody can check. Widen what you look for — take in the line above or below —',
314
- 'until it is unique.',
315
- '',
316
- ` ${TYPED} read <path> print it first, with line numbers`,
317
- ` ${TYPED} read ${FINDS} <text> find where a thing is before you change it`,
318
- ` ${TYPED} edit ${WITHIN} <path> the list below, narrowed to one directory`,
319
- ` ${TYPED} edit <row> --<field> <value> change what a ROW says, by its id or its mark`,
320
- ` ${TYPED} edit <row> ${REMOVES} take that row out of the tree`,
321
- '',
322
- 'Naming something that is not a file here reads it as a row, and the fields you may change come',
323
- 'from the shape that row was declared with. So whatever `create` asked for, `edit` can change,',
324
- 'and taking a thing out is one of the changes rather than a command of its own.',
325
- '',
326
- ...VERBS,
327
- '',
328
- 'that is why the list below is shorter than what is on disk. A file a command writes is not yours',
329
- 'to edit — change what it is written FROM and run that command, and the same bytes come back.',
330
- '',
331
- broke !== ''
332
- ? `The tree could not be read, so this cannot say what you may edit — ${broke}. Run \`${TYPED} descent\`.`
333
- : shown.length === 0
334
- ? under === ''
335
- ? 'This tree declares nothing you can edit yet. Everything it has is written by a command, or the ' +
336
- `descent reaches nothing on disk. \`${TYPED} descent\` says which.`
337
- : `Nothing under ${under} is yours to edit. Run \`${TYPED} edit\` with nothing after it for the whole list.`
338
- : `what this tree lets you edit${under === '' ? '' : `, under ${under}`}:`,
339
- ...shown.map((one) => ` ${one.at}${one.says === '' ? '' : `\n ${first(one.says)}`}`),
340
- '',
341
- 'A file a command writes is not on that list. Naming one here refuses and tells you which command',
342
- 'writes it, so you change what it is written FROM and run that.',
343
- '',
344
- ].join('\n'));
345
- return 1;
346
- }
347
- if (!within(root, named)) {
348
- stderr.write(`thread: ${named} is outside this app, and this edits what the app owns.\n`);
349
- return 1;
350
- }
351
- const found = argv.indexOf(FINDS);
352
- const swapped = argv.indexOf(REPLACES);
353
- const filed = argv.indexOf(`${REPLACES}-from`);
354
- const wholesale = argv.indexOf(FROM);
355
- const sought = argv.indexOf(`${FINDS}-from`);
356
- let looked = found === -1 ? undefined : argv[found + 1];
357
- if (sought !== -1) {
358
- const at = argv[sought + 1];
359
- if (looked !== undefined) {
360
- stderr.write(`${FINDS} and ${FINDS}-from both say what to look for, and they do not have to agree. Give one. Nothing was written.\n`);
361
- return 1;
362
- }
363
- if (at === undefined || at.startsWith('-') || !existsSync(at)) {
364
- stderr.write(`${FINDS}-from names a file nothing could read, so there is nothing to look for. Nothing was written.\n`);
365
- return 1;
366
- }
367
- looked = readFileSync(at, 'utf8').replace(/\n$/, '');
368
- }
369
- let put = swapped === -1 ? undefined : argv[swapped + 1];
370
- if (filed !== -1) {
371
- const at = argv[filed + 1];
372
- if (put !== undefined) {
373
- stderr.write(`${REPLACES} and ${REPLACES}-from both say what goes in, and they do not have to agree. Give one. Nothing was written.\n`);
374
- return 1;
375
- }
376
- if (at === undefined || at.startsWith('-') || !existsSync(at)) {
377
- stderr.write(`${REPLACES}-from names a file nothing could read, so there is no replacement to make. Nothing was written.\n`);
378
- return 1;
379
- }
380
- put = readFileSync(at, 'utf8').replace(/\n$/, '');
381
- }
382
- const source = wholesale === -1 ? undefined : argv[wholesale + 1];
383
- if (source !== undefined) {
384
- stderr.write(`${FROM} replaces a whole file with what another file holds, and a file is not a receipt: nothing here ` +
385
- 'checked it and no descent reached it, so what it carried in was never seen. Every byte this writes ' +
386
- `arrives on the command — ${FINDS} <text> ${REPLACES} <text>. Nothing was written.\n`);
387
- return 1;
388
- }
389
- if (source === undefined && (looked === undefined || looked === '' || put === undefined)) {
390
- stderr.write(`npx thread edit <path> ${FINDS} <text> ${REPLACES} <text> changes one stretch, and both are needed.\n` +
391
- `npx thread edit <path> ${FROM} <file> is refused: a file is not a receipt, so every byte this writes arrives on the\n` +
392
- 'command. Single-quote a body that holds quotes.\n');
393
- return 1;
394
- }
395
- const at = resolve(root, named);
396
- const path = relative(root, at);
397
- let threadings;
398
- let outright;
399
- try {
400
- const pass = descend();
401
- threadings = pass.threaded.get(path);
402
- if (threadings === undefined) {
403
- for (const one of pass.stated.values()) {
404
- if (one.at === path) {
405
- outright = one;
406
- break;
407
- }
408
- }
409
- }
410
- }
411
- catch (thrown) {
412
- stderr.write('thread: the descent could not run, so nothing here knows what this tree declares and an edit would ' +
413
- `be a guess — ${thrown instanceof Error ? thrown.message : String(thrown)}\n`);
414
- return 1;
415
- }
416
- if (threadings === undefined && outright === undefined) {
417
- stderr.write(`thread: nothing declares ${path}, so it is not this tree's to change. A file the descent never reaches ` +
418
- 'does not exist as far as this app is concerned, and editing one would put work somewhere nothing ' +
419
- 'reads. Declare it first — `npx thread create` lists what this tree makes — then edit it.\n');
420
- return 1;
421
- }
422
- const derived = (threadings ?? []).find((one) => one.list === 'derives');
423
- if (derived !== undefined) {
424
- const by = derived.row.by;
425
- stderr.write(`thread: ${path} is derived rather than written — ${by === undefined ? 'a generator writes it' : `\`${by}\` writes it`}. ` +
426
- 'An edit here is reverted the next time that runs, and its `--check` fails until it is, so the change ' +
427
- 'you want belongs in what it is derived FROM. Change that, then run the command above.\n');
428
- return 1;
429
- }
430
- if (source !== undefined) {
431
- let body;
432
- try {
433
- body = readFileSync(resolve(root, source), 'utf8');
434
- }
435
- catch (thrown) {
436
- stderr.write(`thread: ${source} could not be read, so there is nothing to put in ${path} and nothing was written — ` +
437
- `${thrown instanceof Error ? thrown.message : String(thrown)}\n`);
438
- return 1;
439
- }
440
- try {
441
- writeFileSync(at, body, 'utf8');
442
- }
443
- catch (thrown) {
444
- stderr.write(`thread: ${path} could not be written — ${thrown instanceof Error ? thrown.message : String(thrown)}\n`);
445
- return 1;
446
- }
447
- stdout.write(`thread: wrote ${path} from ${source} — ${String(body.split('\n').length)} line(s). ` +
448
- `\`npx thread read ${path}\` prints what it says now.\n`);
449
- stdout.write(`${reminded(undefined)}\n`);
450
- return 0;
451
- }
452
- if (looked === undefined || put === undefined) {
453
- stderr.write(`npx thread edit <path> ${FINDS} <text> ${REPLACES} <text> — both are needed.\n`);
454
- return 1;
455
- }
456
- let text;
457
- try {
458
- text = readFileSync(at, 'utf8');
459
- }
460
- catch (thrown) {
461
- stderr.write(`thread: ${path} could not be read — ${thrown instanceof Error ? thrown.message : String(thrown)}\n`);
462
- return 1;
463
- }
464
- const hits = text.split(looked).length - 1;
465
- if (hits === 0) {
466
- stderr.write(`thread: ${path} does not carry that text, so there is nothing to change and nothing was written. ` +
467
- `\`npx thread read ${path}\` prints it, and \`npx thread read ${FINDS} <text>\` says where a thing is.\n`);
468
- return 1;
469
- }
470
- if (hits > 1) {
471
- stderr.write(`thread: that text appears ${String(hits)} times in ${path}, so this would change ${String(hits)} places ` +
472
- 'and you named one. Nothing was written. Widen what you look for until it appears once.\n');
473
- return 1;
474
- }
475
- try {
476
- writeFileSync(at, text.split(looked).join(put), 'utf8');
477
- }
478
- catch (thrown) {
479
- stderr.write(`thread: ${path} could not be written — ${thrown instanceof Error ? thrown.message : String(thrown)}\n`);
480
- return 1;
481
- }
482
- stdout.write(`thread: changed ${path} — one stretch of ${String(looked.length)} character(s) became ${String(put.length)}. ` +
483
- `\`npx thread read ${path}\` prints what it says now.\n`);
484
- stdout.write(`${reminded(undefined)}\n`);
485
- return 0;
486
- }
487
- async function surface() {
488
- let rows = [];
489
- let told = new Map();
490
- let broke = '';
491
- try {
492
- const tree = findTree();
493
- try {
494
- rows = await (await chain()).generators(tree);
495
- }
496
- catch (thrown) {
497
- broke = thrown instanceof Error ? thrown.message : String(thrown);
498
- }
499
- try {
500
- told = await declared(tree);
501
- }
502
- catch (thrown) {
503
- if (broke === '')
504
- broke = thrown instanceof Error ? thrown.message : String(thrown);
505
- }
506
- }
507
- catch (thrown) {
508
- broke = thrown instanceof Error ? thrown.message : String(thrown);
509
- }
510
- return { rows, told, broke };
511
- }
512
- const spoken = (rows, told) => [
513
- ...rows
514
- .filter((one) => (one.by ?? '').startsWith('thread create '))
515
- .map((one) => {
516
- const word = (one.by ?? '').slice('thread create '.length);
517
- const meant = told.get(word)?.describes ?? '';
518
- return { said: `create ${word}`, describes: meant !== '' ? meant : (one.describes ?? '') };
519
- }),
520
- ...[...told]
521
- .filter(([word]) => !rows.some((one) => one.by === `thread create ${word}`))
522
- .map(([word, one]) => ({ said: `create ${word}`, describes: commanded(word, one) })),
523
- ].sort((one, two) => (one.said < two.said ? -1 : 1));
524
- async function listing() {
525
- const { rows, told, broke } = await surface();
526
- const kept = (keep) => rows
527
- .filter(keep)
528
- .map((one) => ({ said: (one.by ?? '').slice('thread '.length), describes: one.describes ?? '' }))
529
- .sort((one, two) => (one.said < two.said ? -1 : 1))
530
- .flatMap((one) => columned(one.said, opening(one.describes)));
531
- const makes = spoken(rows, told).flatMap((one) => {
532
- const word = one.said.slice('create '.length);
533
- const held = told.get(word);
534
- return [
535
- ...columned(one.said, opening(one.describes)),
536
- ...(held === undefined
537
- ? []
538
- : [
539
- `${' '.repeat(25)}${held.holds === 0
540
- ? `writes into ${held.at}, where none is declared yet`
541
- : `writes into ${held.at}, where ${String(held.holds)} already ${held.holds === 1 ? 'is' : 'are'}`}`,
542
- ]),
543
- ];
544
- });
545
- const runs = kept((one) => (one.by ?? '').startsWith('thread run '));
546
- const derives = kept((one) => (one.by ?? '').startsWith('thread ') && !(one.by ?? '').startsWith('thread create ') && !(one.by ?? '').startsWith('thread run '));
547
- stdout.write([
548
- `${TYPED} list`,
549
- '',
550
- ...WORKS,
551
- '',
552
- `every command this tree answers. These ${String(OWN.length)} are this framework's own and are here in every tree:`,
553
- '',
554
- ...heading(false),
555
- ...(broke === ''
556
- ? []
557
- : [
558
- '',
559
- 'THIS LIST IS INCOMPLETE. The tree could not be read, so everything it makes, runs and derives is',
560
- `missing below — only the commands above are certain. The read failed with: ${broke}`,
561
- ]),
562
- ...(makes.length === 0
563
- ? []
564
- : ['', 'what this tree makes. Each one asks what it needs and writes the row for you:', '', ...makes]),
565
- ...(runs.length === 0 ? [] : ['', 'what this tree runs:', '', ...runs]),
566
- ...(derives.length === 0 ? [] : ['', 'what this tree derives from what is declared:', '', ...derives]),
567
- '',
568
- `${TYPED} help <word> says what any one of these words means and what to run next.`,
569
- '',
570
- ].join('\n'));
571
- return 0;
572
- }
573
- async function repairing() {
574
- let root;
575
- try {
576
- root = findTree().at;
577
- }
578
- catch {
579
- stderr.write('thread: there is no tree here, so nothing declares anything and there is nothing to repair. Run `npx thread init` first.\n');
580
- return 1;
581
- }
582
- stdout.write('thread: repairing — every generator this tree declares runs, then runs again, until a full pass changes nothing. ' +
583
- 'Every derived file comes back to what its rows state, and nothing you wrote is touched.\n');
584
- let rows;
585
- try {
586
- rows = await (await chain()).generators(findTree());
587
- }
588
- catch (thrown) {
589
- stderr.write(`thread: the tree could not be read, so nothing was repaired — ${thrown instanceof Error ? thrown.message : String(thrown)}. ` +
590
- `Run \`${TYPED} issues\` and fix what it names first.\n`);
591
- return 1;
592
- }
593
- if (rows.length === 0) {
594
- stdout.write('thread: this tree declares no generator, so it derives nothing and there is nothing to repair.\n');
595
- return 0;
596
- }
597
- const settled = (await chain()).settling(root, rows);
598
- for (const said of settled.ran)
599
- stdout.write(`${said}\n`);
600
- if (settled.unsettled.length > 0) {
601
- const writers = settled.unsettled.map((one) => one.replace(/\s*--check\s*$/, ''));
602
- stderr.write(`thread: ${settled.unsettled.join(', ')} did not settle, so part of what the rows state is not on disk. ` +
603
- `Run ${writers.join(', ')} alone to see what each one refuses.\n`);
604
- return 1;
605
- }
606
- stdout.write('thread: repaired — every derived file is what its rows state. pnpm reads the package manifests before it runs anything, ' +
607
- 'so a workspace it refused a moment ago runs again from here.\n');
608
- return 0;
609
- }
610
- async function helping(word) {
611
- const { rows, told, broke } = await surface();
612
- let every = [];
613
- let outright;
614
- let unread = '';
615
- try {
616
- const pass = descend();
617
- for (const [at, threadings] of pass.threaded) {
618
- for (const one of threadings)
619
- every.push({ row: one.row, at, list: one.list, by: one.by });
620
- }
621
- outright = pass.stated.get(word);
622
- }
623
- catch (thrown) {
624
- every = [];
625
- outright = undefined;
626
- unread = thrown instanceof Error ? thrown.message : String(thrown);
627
- }
628
- const own = framework().find((one) => verb(one) === word);
629
- const holds = told.get(word);
630
- const named = every.filter((one) => one.row.id === word);
631
- const answers = rows.find((one) => one.by === `thread ${word}` || one.by === `thread create ${word}` || one.by === `thread run ${word}`);
632
- if (own === undefined && holds === undefined && named.length === 0 && answers === undefined && outright === undefined) {
633
- stderr.write((unread === ''
634
- ? `thread: nothing in this tree is called "${word}" — no command, nothing you can declare, and no row.\n`
635
- : `thread: this tree could not be read, so whether anything is called "${word}" is unknown — it was not ` +
636
- `searched. Run \`${TYPED} descent\`, fix what it names, and ask again. What stopped it: ${unread}\n`) +
637
- `\`${TYPED} list\` prints every command that does exist, and \`${TYPED} inspect ${word}\` searches every ` +
638
- 'row whose name or sentence carries it, which sometimes finds what this could not.\n' +
639
- (broke === '' ? '' : `The tree could not be fully read, so this answer may be short: ${broke}\n`));
640
- return 1;
641
- }
642
- const says = own?.describes ?? holds?.describes ?? named[0]?.row.describes ?? outright?.describes ?? answers?.describes ?? '';
643
- stdout.write([
644
- `${word} — ${says}`,
645
- '',
646
- ...(own === undefined ? [] : [` it is one of this framework's own commands, in every tree: ${TYPED} ${own.said}`]),
647
- ...named.flatMap((one) => [
648
- ` it is at ${one.at}`,
649
- one.list === 'declares'
650
- ? ` ${one.by} declares it — a person wrote it`
651
- : ` ${one.by} ${one.list} it — a command writes it`,
652
- ]),
653
- ...(outright === undefined || named.length > 0
654
- ? []
655
- : [
656
- ` it is a row in ${outright.where}, in a list of its own rather than under a parent`,
657
- ` ${TYPED} read ${word} prints every field it states`,
658
- ]),
659
- ...(holds === undefined
660
- ? []
661
- : [
662
- holds.holds === 0
663
- ? ` nothing is declared as one yet. They are listed in ${holds.at}`
664
- : ` ${String(holds.holds)} ${holds.holds === 1 ? 'is' : 'are'} declared, listed in ${holds.at}`,
665
- ]),
666
- ...fielded(word, holds),
667
- ...doing(word, holds, answers, named[0]?.at),
668
- '',
669
- ].join('\n'));
670
- return 0;
671
- }
672
- function tutorial(took, core, waiting, blocked) {
673
- return [
674
- '',
675
- '─────────────────────────────────────────────────────────────────────────',
676
- '',
677
- core.length === 0
678
- ? 'No framework packages are installed here yet, so there was nothing to take on.'
679
- : `Threaded. ${String(core.length)} package(s) on this tree: ${core.join(', ')}.${took.length === 0 ? ' All of them were already attached.' : ''}`,
680
- '',
681
- 'HOW IT WORKS, in four sentences.',
682
- '',
683
- ' Everything this app is, it DECLARES — in `_threads/`, one row per thing. You never write',
684
- ' those rows by hand: the `npx thread create <noun>` command asks what it needs and writes one.',
685
- ' Everything else is DERIVED from them — the schema, the migrations, the routes, the docs — by',
686
- ' generators that re-run themselves whenever a row changes, so nothing has to be kept in step.',
687
- ' `npx thread gate` refuses the tree if any of it has drifted, and a build reads that refusal.',
688
- '',
689
- ...(existsSync(join(cwd(), PAGE))
690
- ? [
691
- `READ THIS FIRST — \`${PAGE}\`, which this command just wrote.`,
692
- '',
693
- ' Every node this tree declares, every command it has, and every word it coins, derived',
694
- ' from the rows. It is written again whenever a row changes, so it cannot go stale, and',
695
- ' it is the whole of what this app is, on one page.',
696
- '',
697
- ` ${TYPED} docs --show`,
698
- '',
699
- ]
700
- : []),
701
- 'WHAT TO DO NEXT.',
702
- '',
703
- ` ${TYPED} every command this tree has, and what each one is for`,
704
- ` ${TYPED} create every thing you can declare, and what each one is`,
705
- ` ${TYPED} create <noun> the whole method for one of them — it asks nothing until you`,
706
- ' hand back the receipt it prints, so reading it costs nothing',
707
- ` ${TYPED} inspect <word> every row whose name or sentence carries that word, and what`,
708
- ' each one means — the widest read this tree has',
709
- ` ${TYPED} gate every check, run in order`,
710
- '',
711
- ...(waiting.length === 0
712
- ? []
713
- : [
714
- `NOT ATTACHED, BECAUSE THEY ARE YOURS TO CHOOSE: ${waiting.join(', ')}.`,
715
- '',
716
- ` An app that keeps nothing needs no database, and one with no interface needs no screens.`,
717
- ` Take one on when you want it — \`npx thread attach ${waiting[0]}\` — and everything`,
718
- ' it declares, and everything that derives from it, arrives in the same run.',
719
- '',
720
- ]),
721
- ...(blocked.length === 0
722
- ? []
723
- : [
724
- `NOT TAKEN ON YET — ${String(blocked.length)} package(s) want something that is not installed:`,
725
- '',
726
- ...blocked.flatMap((one) => [` ${one.name}`, ` ${one.why.replace(/\s+/g, ' ').trim()}`, '']),
727
- ' Install what each names and run `npx thread init` again. Everything that could be threaded',
728
- ' already has been, so nothing is lost by doing it in two goes.',
729
- '',
730
- ]),
731
- 'Nothing here is written by hand, including this app\'s own manifest and tsconfig. If you find',
732
- 'yourself editing a file under `_threads/` or anything a command wrote, that is the framework',
733
- 'missing a question — say so rather than working around it.',
734
- '',
735
- ].join('\n');
736
- }
737
- const META = 'The command that declares a governing node — one of the files every other row in this tree is read through, stating what it holds and what may be declared beneath it.';
738
- const PAGE = 'docs.md';
739
- const FLOOR = NAMED;
740
- function here(name) {
741
- try {
742
- createRequire(join(cwd(), 'package.json')).resolve(`${name}/attach`);
743
- return true;
744
- }
745
- catch {
746
- return false;
747
- }
748
- }
749
- async function offered() {
750
- try {
751
- return await (await chain()).generators(findTree());
752
- }
753
- catch (thrown) {
754
- stderr.write(`thread: ${thrown instanceof Error ? thrown.message : String(thrown)}\n`);
755
- exit(1);
756
- }
757
- }
758
- const MINE = 'thread';
759
- const UNDER = 'generators';
760
- const MAKING = 'makers';
761
- function own(name, under = UNDER) {
762
- let found;
763
- const look = (at) => {
764
- for (const entry of readdirSync(at, { withFileTypes: true })) {
765
- if (!entry.isDirectory() || entry.name === 'node_modules')
766
- continue;
767
- const made = join(at, entry.name, under, `${name}.ts`);
768
- try {
769
- readFileSync(made);
770
- found = made;
771
- return;
772
- }
773
- catch {
774
- look(join(at, entry.name));
775
- }
776
- if (found !== undefined)
777
- return;
778
- }
779
- };
780
- try {
781
- look(join(cwd(), MINE));
782
- }
783
- catch {
784
- return undefined;
785
- }
786
- return found;
787
- }
788
- async function derived(name) {
789
- if (argv.includes('--own')) {
790
- const mine = own(name);
791
- if (mine === undefined) {
792
- stderr.write(`thread: \`--own\` means this repository's own generator, and there is none named ` +
793
- `"${name}" under any package here. Drop the flag to use an app's tree instead.\n`);
794
- return 1;
795
- }
796
- argv[1] = mine;
797
- argv.splice(2, 1);
798
- try {
799
- await import(__rewriteRelativeImportExtension(pathToFileURL(mine).href));
800
- return 0;
801
- }
802
- catch (thrown) {
803
- stderr.write(`thread: \`npx thread ${name} --own\` is at ${mine}, and running it threw: ` +
804
- `${thrown instanceof Error ? thrown.message : String(thrown)}\n`);
805
- return 1;
806
- }
807
- }
808
- const all = await offered();
809
- const offers = all.filter((one) => (one.by ?? '').startsWith('thread '));
810
- const held = offers.find((one) => one.by === `thread ${name}`);
811
- if (held?.at === undefined) {
812
- let makes = [];
813
- try {
814
- makes = [...(await declared(findTree())).keys()].map((one) => `create ${one}`);
815
- }
816
- catch {
817
- makes = [];
818
- }
819
- const named = [
820
- ...new Set([...COMMANDS, 'create meta', ...makes, ...offers.map((one) => (one.by ?? '').slice('thread '.length))]),
821
- ].sort();
822
- stderr.write(`thread: no command named "${name}". There is: ${named.join(', ')}\n`);
823
- return 1;
824
- }
825
- const at = resolve(findTree().at, held.at);
826
- argv[1] = at;
827
- argv.splice(2, 1);
828
- try {
829
- await import(__rewriteRelativeImportExtension(pathToFileURL(at).href));
830
- return 0;
831
- }
832
- catch (thrown) {
833
- stderr.write(`thread: \`npx thread ${name}\` is declared by \`${held.id ?? ''}\` at ${held.at}, and running it threw: ${thrown instanceof Error ? thrown.message : String(thrown)}\n`);
834
- return 1;
835
- }
836
- }
837
- function stated(root) {
838
- const under = join(root, '_threads');
839
- try {
840
- return readdirSync(under)
841
- .filter((one) => one.endsWith('.ts') || one.endsWith('.json'))
842
- .sort()
843
- .map((one) => {
844
- try {
845
- return `${one}:${readFileSync(join(under, one), 'utf8')}`;
846
- }
847
- catch {
848
- return `${one}:`;
849
- }
850
- })
851
- .join('\n');
852
- }
853
- catch {
854
- return '';
855
- }
856
- }
857
- function declaring(was) {
858
- const dir = findTree().dir;
859
- for (const one of readdirSync(dir)) {
860
- if (!one.endsWith('.ts'))
861
- continue;
862
- const now = readFileSync(join(dir, one), 'utf8');
863
- const older = was.get(one) ?? '';
864
- if (now === older)
865
- continue;
866
- const before = ids(older);
867
- const after = ids(now);
868
- const count = (list, id) => list.filter((each) => each === id).length;
869
- const fresh = [...new Set(after)].filter((each) => count(after, each) > count(before, each));
870
- if (fresh.length === 0)
871
- continue;
872
- const outer = fresh
873
- .flatMap((each) => rowsFor(now, each).map((source) => ({ id: each, source })))
874
- .filter((each) => !older.includes(each.source))
875
- .sort((a, b) => b.source.length - a.source.length);
876
- const held = outer[0];
877
- if (held === undefined)
878
- return undefined;
879
- return { sheet: one, now, id: held.id, source: held.source };
880
- }
881
- return undefined;
882
- }
883
- const states = (row, field) => new RegExp(`\\n\\s*${field}:`).test(row);
884
- let heard = new Map();
885
- function adopted(was) {
886
- if (argv.includes('edit'))
887
- return;
888
- let kids;
889
- let own;
890
- try {
891
- kids = new Map([...childrenIn(given()), ...heard]);
892
- own = ownAnswers(given());
893
- }
894
- catch {
895
- return;
896
- }
897
- const plain = CARRIES.map((one) => ({ field: one, said: (answer(own, one) ?? '').trim() })).filter((one) => one.said !== '');
898
- if (kids.size === 0 && plain.length === 0)
899
- return;
900
- const held = declaring(was);
901
- if (held === undefined)
902
- return;
903
- const owed = new Map([...kids].filter(([list]) => !states(held.source, list)));
904
- const rest = plain
905
- .filter((each) => !states(held.source, each.field))
906
- .map((each) => ({ field: each.field, said: each.said, list: each.field !== 'by' && each.field !== 'runs' }));
907
- if (owed.size === 0 && rest.length === 0)
908
- return;
909
- const pad = (/\n(\s*)id:/.exec(held.source) ?? [, ' '])[1] ?? ' ';
910
- const grown = withChildren(held.source, rowLines(pad, rest, owed));
911
- writeFileSync(join(findTree().dir, held.sheet), held.now.replace(held.source, grown), 'utf8');
912
- const named = [...rest.map((each) => each.field), ...[...owed.keys()]];
913
- stdout.write(`thread: wrote ${named.join(', ')} onto ${held.id} in ${findTree().name}/${held.sheet}.\n`);
914
- for (const said of seeding(owed, findTree().at))
915
- stdout.write(`thread: ${said}\n`);
916
- }
917
- function rowsFor(text, id) {
918
- const mark = `id: '${id}'`;
919
- const found = [];
920
- for (let at = text.indexOf(mark); at !== -1; at = text.indexOf(mark, at + 1)) {
921
- const row = rowAt(text, at);
922
- if (row !== undefined)
923
- found.push(row);
924
- }
925
- return found;
926
- }
927
- function rowAt(text, at) {
928
- let open = text.lastIndexOf('{', at);
929
- if (open === -1)
930
- return undefined;
931
- let depth = 0;
932
- for (let each = open; each < text.length; each += 1) {
933
- if (text[each] === '{')
934
- depth += 1;
935
- if (text[each] === '}') {
936
- depth -= 1;
937
- if (depth === 0) {
938
- const end = text[each + 1] === ',' ? each + 2 : each + 1;
939
- while (open > 0 && text[open - 1] !== '\n')
940
- open -= 1;
941
- return text.slice(open, end);
942
- }
943
- }
944
- }
945
- return undefined;
946
- }
947
- const ids = (text) => [...text.matchAll(/\n\s*id: '([^']+)'/g)].map((one) => one[1]);
948
- const SELF = fileURLToPath(import.meta.url);
949
- const FOLDER = 'src/generators';
950
- const kebab = (one) => one.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
951
- const shared = (one, two) => {
952
- const here = one.split('/');
953
- const there = two.split('/');
954
- let same = 0;
955
- while (same < here.length && same < there.length && here[same] === there[same])
956
- same += 1;
957
- return same;
958
- };
959
- function folderFor(rows, at) {
960
- const held = new Map();
961
- for (const row of rows) {
962
- const path = (row.at ?? '').trim();
963
- if (path === '' || !path.includes('/'))
964
- continue;
965
- const folder = path.slice(0, path.lastIndexOf('/'));
966
- held.set(folder, (held.get(folder) ?? 0) + 1);
967
- }
968
- if (held.size === 0)
969
- return FOLDER;
970
- const ranked = [...held].sort((one, two) => {
971
- const near = shared(two[0], at) - shared(one[0], at);
972
- return near === 0 ? two[1] - one[1] : near;
973
- });
974
- return ranked[0]?.[0] ?? FOLDER;
975
- }
976
- const BY_IN = /\bby:\s*'([^']+)'/g;
977
- const AT_IN = /\bat:\s*'([^']+)'/;
978
- function sheetsNow() {
979
- const found = new Map();
980
- try {
981
- for (const one of readdirSync(findTree().dir)) {
982
- if (!one.endsWith('.ts'))
983
- continue;
984
- found.set(one, readFileSync(join(findTree().dir, one), 'utf8'));
985
- }
986
- }
987
- catch {
988
- return found;
989
- }
990
- return found;
991
- }
992
- function writtenOwed(was, commands) {
993
- const out = [];
994
- const seen = new Set();
995
- for (const [name, text] of sheetsNow()) {
996
- const before = was.get(name);
997
- if (before === text)
998
- continue;
999
- const had = new Set((before ?? '').split('\n'));
1000
- for (const line of text.split('\n')) {
1001
- if (had.has(line))
1002
- continue;
1003
- for (const found of line.matchAll(BY_IN)) {
1004
- const by = (found[1] ?? '').trim();
1005
- if (by === '' || commands.has(by) || seen.has(by))
1006
- continue;
1007
- seen.add(by);
1008
- out.push({ at: AT_IN.exec(line)?.[1] ?? '', by, runs: '' });
1009
- }
1010
- }
1011
- }
1012
- return out;
1013
- }
1014
- function generated(rows, was) {
1015
- if (argv.includes(CHAINED))
1016
- return [];
1017
- let kids;
1018
- try {
1019
- kids = new Map([...childrenIn(given()), ...heard]);
1020
- }
1021
- catch {
1022
- kids = new Map();
1023
- }
1024
- const commands = new Set(rows.map((one) => (one.by ?? '').trim()).filter((one) => one !== ''));
1025
- const asked = [...(kids.get('derives') ?? []), ...(kids.get('seeds') ?? [])]
1026
- .filter((one) => {
1027
- const by = (one.by ?? '').trim();
1028
- return by !== '' && !commands.has(by);
1029
- })
1030
- .map((one) => ({ at: one.at, by: (one.by ?? '').trim(), runs: (one.runs ?? '').trim(), writes: one.writes }));
1031
- const named = new Set(asked.map((one) => one.by));
1032
- const found = writtenOwed(was, commands).filter((one) => !named.has(one.by));
1033
- let top = '';
1034
- try {
1035
- top = (answer(given(), WRITES) ?? '').trim();
1036
- }
1037
- catch {
1038
- top = '';
1039
- }
1040
- const owed = [
1041
- ...asked,
1042
- ...found.map((one) => (found.length === 1 && top !== '' && one.writes === undefined ? { ...one, writes: top } : one)),
1043
- ];
1044
- if (owed.length === 0)
1045
- return [];
1046
- const short = [];
1047
- for (const one of owed) {
1048
- const by = one.by;
1049
- const name = kebab(by.replace(/^thread\s+/, ''));
1050
- const file = one.at === '' ? 'the file that row names' : one.at;
1051
- if (name === '') {
1052
- short.push(`the row states \`by: '${by}'\`, which is no command name, so nothing can be declared to write ${file}.`);
1053
- continue;
1054
- }
1055
- if (one.writes === undefined || one.writes.trim() === '') {
1056
- short.push(`${file} is written by \`${by}\`, and this tree has no such command. A generator is what would answer it, and ` +
1057
- `none was declared, because the body one would be made from was not given — ${asFlag(WRITES)} is that body. ` +
1058
- 'Give it on this command and the generator is declared in the same run.');
1059
- continue;
1060
- }
1061
- stdout.write(`thread: nothing writes ${file} yet — \`${by}\` is a command this tree does not have. Declaring it.\n`);
1062
- const asked = spawnSync(process.execPath, [...LOADER, SELF, 'create', 'generator', CHAINED], {
1063
- encoding: 'utf8',
1064
- input: '',
1065
- });
1066
- const receipt = /--receipt ([0-9a-f]+)/.exec(asked.stdout ?? '')?.[1];
1067
- if (receipt === undefined) {
1068
- short.push(`\`${TYPED} create generator\` issued no receipt, so the generator behind \`${by}\` was not declared and ` +
1069
- `nothing writes ${file}.`);
1070
- continue;
1071
- }
1072
- const runs = one.runs;
1073
- const ran = spawnSync(process.execPath, [
1074
- ...LOADER, SELF, 'create', 'generator', 'new', CHAINED,
1075
- '--name', name,
1076
- '--describes',
1077
- `Writes ${file}. It runs as \`${by}\`${runs === '' ? '' : `, and \`${runs}\` says whether what it wrote still matches`}.`,
1078
- '--folder', folderFor(rows, one.at),
1079
- '--by', by,
1080
- ...(runs === '' ? [] : ['--runs', runs]),
1081
- ...(one.writes === undefined || one.writes === '' ? [] : ['--body', one.writes]),
1082
- '--receipt', receipt,
1083
- ], { stdio: 'inherit' });
1084
- if (ran.status !== 0) {
1085
- short.push(`the generator behind \`${by}\` was refused, so nothing writes ${file}.`);
1086
- }
1087
- }
1088
- return short;
1089
- }
1090
- async function deleting(root, targets) {
1091
- const named = targets.map((one) => relative(root, resolve(root, one)).split('\\').join('/'));
1092
- const missing = named.filter((one) => !existsSync(resolve(root, one)));
1093
- if (missing.length > 0) {
1094
- stderr.write(`thread: there is nothing at ${missing.join(', ')}, so nothing was taken out.\n`);
1095
- return 1;
1096
- }
1097
- let declared;
1098
- let derived;
1099
- try {
1100
- const pass = await descend();
1101
- declared = new Set([...pass.threaded.keys(), ...pass.mentioned.keys()]);
1102
- derived = new Set([...pass.threaded].filter(([, threadings]) => threadings.every((one) => one.list === 'derives')).map(([at]) => at));
1103
- }
1104
- catch {
1105
- stderr.write('thread: the descent will not run, so whether a row names these has no answer — and deleting a file a row ' +
1106
- 'still names leaves that row pointing at nothing. Run `npx thread issues` and fix what it reports first.\n');
1107
- return 1;
1108
- }
1109
- const under = (one) => {
1110
- if (!statSync(resolve(root, one)).isDirectory())
1111
- return [one];
1112
- return readdirSync(resolve(root, one), { withFileTypes: true }).flatMap((each) => under(`${one}/${each.name}`));
1113
- };
1114
- const asked = named.filter((one) => !statSync(resolve(root, one)).isDirectory() && declared.has(one) && !derived.has(one));
1115
- if (asked.length > 0) {
1116
- stderr.write(`thread: a row names ${asked.join(', ')}, and a file taken out from under its row leaves the row naming ` +
1117
- 'nothing. Take the row out first — `npx thread edit <row> --remove` prints the row it found and what to ' +
1118
- 'run — then take out the file. Nothing was taken out.\n');
1119
- return 1;
1120
- }
1121
- const every = named.flatMap(under);
1122
- const kept = every.filter((one) => declared.has(one) && !derived.has(one));
1123
- const going = every.filter((one) => !declared.has(one) || derived.has(one));
1124
- const back = going.filter((one) => derived.has(one));
1125
- if (going.length === 0) {
1126
- stdout.write('thread: every file there is declared, so there is nothing to take out.\n');
1127
- return 0;
1128
- }
1129
- for (const one of going)
1130
- rmSync(resolve(root, one));
1131
- stdout.write(`thread: took out ${String(going.length)} file(s)` +
1132
- (back.length === 0
1133
- ? ' nothing declared'
1134
- : ` — ${String(back.length)} of them derived, so the command each one's row names in \`by\` writes it again`) +
1135
- `${kept.length > 0 ? `, and left the ${String(kept.length)} that rows name` : ''}.\n`);
1136
- stdout.write(`${reminded(undefined)}\n`);
1137
- return 0;
1138
- }
1139
- const either = (all) => all.length < 2 ? (all[0] ?? '') : `${all.slice(0, -1).join(', ')} or ${all[all.length - 1] ?? ''}`;
1140
- async function wizard(was) {
1141
- const held = declaring(was);
1142
- if (held === undefined)
1143
- return 0;
1144
- let coming;
1145
- try {
1146
- coming = new Set(childrenIn(given()).keys());
1147
- }
1148
- catch {
1149
- coming = new Set();
1150
- }
1151
- const owed = LISTS.filter((list) => !coming.has(list) && !states(held.source, list));
1152
- if (owed.length === 0)
1153
- return 0;
1154
- if (answered()) {
1155
- if (owed.length < LISTS.length)
1156
- return 0;
1157
- stdout.write(`thread: ${held.id} names no file it ${either(owed)}. Name them on the create itself: ` +
1158
- '`--declares <id> --at <path> --describes <sentence>` for a file a person wrote, `--derives` with ' +
1159
- '`--by <command>` and `--runs <command>` for one a command writes, and `--seeds` with `--body <text>` ' +
1160
- 'or `--from <path>` for one a command shapes and you fill in.\n');
1161
- return 0;
1162
- }
1163
- stdout.write(`\nthread: every file ${held.id} owns is named here, or nothing in this tree knows it exists.\n` +
1164
- ' If it exists, declare it. If a command can write it, derive it. Otherwise seed it and write it now.\n');
1165
- const { ask, line, shut } = asking([]);
1166
- const found = new Map();
1167
- try {
1168
- for (const list of owed) {
1169
- const kids = await children(list, [], ask, line);
1170
- if (kids.length > 0)
1171
- found.set(list, kids);
1172
- }
1173
- }
1174
- catch (thrown) {
1175
- shut();
1176
- heard = found;
1177
- if (refusal(thrown) === undefined)
1178
- throw thrown;
1179
- return 1;
1180
- }
1181
- shut();
1182
- heard = found;
1183
- return 0;
1184
- }
1185
- async function dispatching(verb, makes, brings) {
1186
- if (argv.includes('--own')) {
1187
- const noun = argv[3];
1188
- if (noun === undefined || noun.startsWith('-')) {
1189
- stderr.write(`npx thread ${verb} <noun> --own\n`);
1190
- return 1;
1191
- }
1192
- const mine = own(noun, MAKING);
1193
- if (mine === undefined) {
1194
- stderr.write(`thread: \`--own\` means this repository's own maker, and there is none named "${noun}" ` +
1195
- 'under any package here. Drop the flag to use an app\'s tree instead.\n');
1196
- return 1;
1197
- }
1198
- argv[1] = mine;
1199
- argv.splice(2, 2);
1200
- try {
1201
- await import(__rewriteRelativeImportExtension(pathToFileURL(mine).href));
1202
- return 0;
1203
- }
1204
- catch (thrown) {
1205
- stderr.write(`thread: \`npx thread ${verb} ${noun} --own\` is at ${mine}, and running it threw: ` +
1206
- `${thrown instanceof Error ? thrown.message : String(thrown)}\n`);
1207
- return 1;
1208
- }
1209
- }
1210
- let offering;
1211
- let rows = [];
1212
- try {
1213
- rows = await offered();
1214
- offering = nouns(rows, verb);
1215
- }
1216
- catch {
1217
- offering = new Map();
1218
- }
1219
- let told = new Map();
1220
- if (verb === 'create') {
1221
- const mine = fileURLToPath(new URL(BESIDE('./generators/meta'), import.meta.url));
1222
- const already = offering.get('meta');
1223
- if (already !== undefined && already !== mine) {
1224
- stderr.write(`thread: a row states \`by: 'thread create meta'\` — ${already} — and this package answers meta ` +
1225
- 'itself, so which one answers a meta is whichever was read first. One noun, one file.\n');
1226
- return 1;
1227
- }
1228
- const grown = new Map(offering).set('meta', mine);
1229
- try {
1230
- told = await declared(findTree());
1231
- for (const noun of told.keys())
1232
- if (!grown.has(noun))
1233
- grown.set(noun, mine);
1234
- }
1235
- catch {
1236
- told = new Map();
1237
- }
1238
- offering = grown;
1239
- }
1240
- const said = [...offering.keys()].sort();
1241
- const noun = argv[3];
1242
- const how = `thread: a package brings a noun into being by declaring a row that states \`by: 'thread ${verb} <noun>'\` and the file that answers it.\n`;
1243
- if (noun === undefined || noun.startsWith('-')) {
1244
- const sentence = (word) => {
1245
- const meant = told.get(word)?.describes ?? '';
1246
- if (meant !== '')
1247
- return first(meant);
1248
- return first(rows.find((one) => one.by === `thread ${verb} ${word}`)?.describes ??
1249
- (word === 'meta' ? META : ((() => {
1250
- const held = told.get(word);
1251
- return held === undefined ? '' : commanded(word, held);
1252
- })())));
1253
- };
1254
- stderr.write(said.length === 0
1255
- ? `npx thread ${verb} <noun>\n\nthis tree ${brings} — ${how.replace('thread: ', '')}`
1256
- : [
1257
- `npx thread ${verb} <noun>`,
1258
- '',
1259
- `this tree ${makes}:`,
1260
- '',
1261
- ...said.map((word) => ` ${word.padEnd(12)} ${sentence(word)}`),
1262
- '',
1263
- 'where a noun comes from, and what answers it:',
1264
- '',
1265
- ' a list of rows names it. `export const PACKAGES: readonly Package[]` in a sheet makes',
1266
- ` \`npx thread ${verb} package\` exist from that moment, asking for whatever a \`Package\` states.`,
1267
- ' Nothing is added to a list of commands anywhere — this reads what your tree declares.',
1268
- '',
1269
- ` a row may answer it instead. A row stating \`by: 'thread ${verb} package'\` names the file that`,
1270
- ' runs, and that file wins over this one. The list still names the noun; the row names what',
1271
- ' answers. Declare no such row and this command answers it itself, which is usually enough.',
1272
- '',
1273
- ' a field nobody has a sentence for stops it. Every question is read from the word the field',
1274
- ' is named after, so a field whose word this tree coins is asked for with no work at all, and',
1275
- ' one it does not coin refuses by name rather than writing a row with a hole in it.',
1276
- ` \`npx thread ${verb} concept\` is how a word gets coined.`,
1277
- '',
1278
- `run \`npx thread ${verb} <noun>\` with nothing after it and it lists what is already declared,`,
1279
- 'with a mark beside each, and states every question it will ask before it asks one.',
1280
- `\`npx thread read <row>\` prints every field one of them states, and writes nothing — read`,
1281
- 'what is there before adding to it.',
1282
- '',
1283
- 'every noun asks for a `describes`, and it is the one answer nothing can derive. It is what',
1284
- 'the glossary shows, what a form labels the thing with, and what the next person reads.',
1285
- '',
1286
- ' say what the thing IS to somebody who has not seen it, not what it is called.',
1287
- ' one sentence, ending in a full stop. Say why it exists if that is not obvious.',
1288
- '',
1289
- ' no: "the user email" — the id again, with spaces',
1290
- ' no: "email field" — its type, which the type already says',
1291
- ' yes: "Where we reach somebody who has asked us to. Not how we identify them: an',
1292
- ' address changes hands, and a row it addressed would follow it."',
1293
- '',
1294
- ].join('\n'));
1295
- return 1;
1296
- }
1297
- const held = offering.get(noun);
1298
- if (held === undefined) {
1299
- stderr.write(`thread: there is nothing this can ${verb} called "${noun}". This tree ${makes}: ${said.length === 0 ? '(nothing)' : said.join(', ')}.\n${how}`);
1300
- return 1;
1301
- }
1302
- const at = resolve(findTree().at, held);
1303
- argv[1] = at;
1304
- argv.splice(2, 2);
1305
- process.env.THREAD_NOUN = noun;
1306
- const addressed = told.get(noun)?.fields.some((one) => one.name === 'at') ?? false;
1307
- process.env[VALUED] = addressed ? '' : '1';
1308
- if (verb === 'create') {
1309
- const opens = argv[2];
1310
- const bare = !argv.includes(RECEIPT) && (opens === undefined || opens.startsWith('-'));
1311
- if (bare) {
1312
- const commands = new Set(rows.map((one) => (one.by ?? '').trim()));
1313
- stdout.write(`${taught(noun, told, commands).join('\n')}\n`);
1314
- }
1315
- }
1316
- const sheets = () => {
1317
- const found = new Map();
1318
- try {
1319
- for (const one of readdirSync(findTree().dir)) {
1320
- if (!one.endsWith('.ts'))
1321
- continue;
1322
- found.set(one, readFileSync(join(findTree().dir, one), 'utf8'));
1323
- }
1324
- }
1325
- catch {
1326
- return found;
1327
- }
1328
- return found;
1329
- };
1330
- const was = sheets();
1331
- const before = stated(findTree().at);
1332
- let answering = rows;
1333
- const { settling } = await chain();
1334
- process.on('exit', () => {
1335
- if (stated(findTree().at) === before)
1336
- return;
1337
- adopted(was);
1338
- const missing = generated(answering, was);
1339
- if (missing.length > 0) {
1340
- for (const said of missing)
1341
- stderr.write(`thread: ${said}\n`);
1342
- stderr.write('thread: the row is written and what it names is not. A `derives` or `seeds` row states the command that ' +
1343
- 'writes its file, and this tree has no such command — so the row names a file nothing will ever write. ' +
1344
- `Declare the generator with \`${TYPED} create generator\`, giving it ${asFlag(WRITES)}, or take the row ` +
1345
- `out with \`${TYPED} edit <row> --remove\`.\n`);
1346
- process.exitCode = 1;
1347
- }
1348
- let settled;
1349
- stdout.write('thread: the row is written. Settling the tree now — every generator this tree declares runs, then every check runs, ' +
1350
- 'and nothing prints until a round ends, so a large tree sits quiet here for minutes.\n');
1351
- try {
1352
- settled = settling(findTree().at, rows);
1353
- }
1354
- catch (thrown) {
1355
- stdout.write('thread: the row is written, and what it derives was not built — reading what this tree offers threw: ' +
1356
- `${thrown instanceof Error ? thrown.message : String(thrown)}\n`);
1357
- return;
1358
- }
1359
- for (const said of settled.ran)
1360
- stdout.write(`${said}\n`);
1361
- if (settled.unsettled.length === 0)
1362
- return;
1363
- const writers = settled.unsettled.map((one) => one.replace(/\s*--check\s*$/, ''));
1364
- stdout.write(`thread: ${settled.unsettled.join(', ')} did not settle, so what this row derives is not built yet. ` +
1365
- `Run: ${writers.join(', ')}\n`);
1366
- });
1367
- if (verb === 'create')
1368
- process.on('exit', () => stdout.write(`${reminded(noun)}\n`));
1369
- const piped = handed();
1370
- const ran = spawnSync(process.execPath, [...LOADER, at, ...argv.slice(2)], {
1371
- ...(piped === undefined ? {} : { input: piped }),
1372
- stdio: [piped === undefined ? 'inherit' : 'pipe', 'inherit', 'inherit'],
1373
- env: { ...process.env, THREAD_NOUN: noun },
1374
- });
1375
- if (ran.error !== undefined) {
1376
- stderr.write(`thread: \`${noun}\` is declared as answered by ${held}, and running it threw: ${ran.error.message}\n`);
1377
- return 1;
1378
- }
1379
- if (ran.status !== 0)
1380
- return ran.status ?? 1;
1381
- try {
1382
- answering = await (await chain()).generators(findTree());
1383
- }
1384
- catch {
1385
- answering = rows;
1386
- }
1387
- if (verb !== 'create')
1388
- return 0;
1389
- return await wizard(was);
1390
- }
1391
- const HELP = new Set(['--help', '-h', 'help']);
1392
- async function attachable(verb) {
1393
- let installed = [];
1394
- try {
1395
- installed = readdirSync(join(cwd(), 'node_modules', ORGANISATION))
1396
- .map((one) => idOf(`${ORGANISATION}/${one}`))
1397
- .filter((one) => one !== undefined);
1398
- }
1399
- catch {
1400
- installed = [];
1401
- }
1402
- let already = [];
1403
- try {
1404
- already = readdirSync(findTree().dir).map((one) => one.replace(/\.ts$/, ''));
1405
- }
1406
- catch {
1407
- already = [];
1408
- }
1409
- const version = (one) => {
1410
- try {
1411
- const said = createRequire(join(cwd(), 'package.json')).resolve(`${named(one)}/package.json`);
1412
- const held = JSON.parse(readFileSync(said, 'utf8'));
1413
- return held.version ?? '';
1414
- }
1415
- catch {
1416
- return '';
1417
- }
1418
- };
1419
- const here = installed.filter((one) => one !== 'core').sort();
1420
- const offered = verb === 'attach' ? here : here.filter((one) => already.includes(one));
1421
- if (offered.length === 0) {
1422
- return verb === 'attach'
1423
- ? [
1424
- `nothing to attach — no \`${named('*')}\` package is installed here beyond the CLI itself.`,
1425
- `Install one with \`npm install -D ${named('<name>')}\` — this framework is scaffolding, so it goes in \`devDependencies\` and a host installs none of it — then run this again to see it listed.`,
1426
- ]
1427
- : [
1428
- 'nothing to detach — this tree has taken on no package yet.',
1429
- 'What is installed is not what is attached: run `npx thread attach` to see what may be taken on.',
1430
- ];
1431
- }
1432
- const stale = new Map();
1433
- for (const one of offered) {
1434
- if (!already.includes(one))
1435
- continue;
1436
- const held = await (await attaching()).behind(cwd(), named(one));
1437
- if (held.length > 0)
1438
- stale.set(one, held.length);
1439
- }
1440
- const numbers = new Set([...installed, 'core'].map(version).filter((one) => one !== ''));
1441
- const width = Math.max(...offered.map((one) => named(one).length)) + 2;
1442
- return [
1443
- `${NAMED} ${version('core')} — the CLI you are running.`,
1444
- '',
1445
- `installed here, and what may be ${verb === 'attach' ? 'attached' : 'detached'}:`,
1446
- '',
1447
- ...offered.map((one) => ` ${named(one)}${' '.repeat(width - named(one).length)}${version(one)}` +
1448
- `${already.includes(one) ? ` (attached${stale.has(one) ? `, ${String(stale.get(one) ?? 0)} row(s) behind` : ''})` : ''}`),
1449
- '',
1450
- ...(stale.size === 0
1451
- ? []
1452
- : [
1453
- `${[...stale.keys()].map(named).join(', ')} ${stale.size === 1 ? 'ships rows' : 'ship rows'} this tree has not taken on.`,
1454
- 'Installing a package does not write its rows — the rows live in your sheets and `npx thread attach` is what puts',
1455
- `them there. Run \`npx thread attach ${[...stale.keys()].map(named).join('` and `npx thread attach `')}\` to take them.`,
1456
- '',
1457
- ]),
1458
- ...(numbers.size <= 1
1459
- ? []
1460
- : [
1461
- `these are not all the same release — ${[...numbers].sort().join(', ')} are installed together. They ship as one,`,
1462
- `so \`npm install ${named('<name>')}@latest\` for whichever is behind, and attach again to take its rows.`,
1463
- '',
1464
- ]),
1465
- ...(verb === 'attach'
1466
- ? []
1467
- : [
1468
- 'what detaching does, before you do it:',
1469
- '',
1470
- ' every row that package put here is withdrawn — its sheet in `_threads/`, the rows it',
1471
- ' appended to other sheets, the nouns it brought, and the scripts it wrote into the manifest.',
1472
- ' What YOU declared with `npx thread create` into a sheet that package owns goes with the sheet,',
1473
- ' because the sheet is the package\'s. A package another one is attached to refuses to go',
1474
- ' until that one has, and it names which.',
1475
- '',
1476
- ' Nothing is installed or uninstalled by this. It is about what this tree declares, and the',
1477
- ' package stays on disk ready to attach again.',
1478
- '',
1479
- ]),
1480
- ...(verb === 'attach'
1481
- ? [
1482
- 'a package brings its own commands, its own checks and its own sheet. Attach one and',
1483
- '`npx thread create` grows the nouns it declares — run it after to see what changed.',
1484
- ]
1485
- : [
1486
- 'run `npx thread issues` first. A package is rarely the thing that is wrong, and detaching one',
1487
- 'to reach a single bad row withdraws everything else it brought along with it.',
1488
- ]),
1489
- ];
1490
- }
1491
- async function run(name) {
1492
- const asked = argv[3];
1493
- if (name === 'help' && asked !== undefined && !asked.startsWith('-'))
1494
- return await helping(asked);
1495
- if (name !== undefined && HELP.has(name)) {
1496
- await run(undefined);
1497
- return 0;
1498
- }
1499
- if (name === undefined) {
1500
- let rows = [];
1501
- let unread = '';
1502
- try {
1503
- const tree = findTree();
1504
- try {
1505
- rows = await (await chain()).generators(tree);
1506
- }
1507
- catch (thrown) {
1508
- unread = thrown instanceof Error ? thrown.message : String(thrown);
1509
- }
1510
- }
1511
- catch {
1512
- rows = [];
1513
- }
1514
- const under = (kept, keep, also = []) => [
1515
- ...kept.filter(keep).map((one) => ({ said: (one.by ?? '').slice('thread '.length), describes: one.describes ?? '' })),
1516
- ...also,
1517
- ]
1518
- .sort((one, two) => (one.said < two.said ? -1 : 1))
1519
- .flatMap((one) => columned(one.said, opening(one.describes)));
1520
- let told = new Map();
1521
- try {
1522
- told = await declared(findTree());
1523
- }
1524
- catch {
1525
- told = new Map();
1526
- }
1527
- const makes = under(rows, () => false, spoken(rows, told));
1528
- const runs = under(rows, (one) => (one.by ?? '').startsWith('thread run '));
1529
- const derives = under(rows, (one) => (one.by ?? '').startsWith('thread ') && !(one.by ?? '').startsWith('thread create ') && !(one.by ?? '').startsWith('thread run '));
1530
- stderr.write([
1531
- `${TYPED} <command>`,
1532
- '',
1533
- ...WORKS,
1534
- '',
1535
- `a tree is made, attached to, and then declared into. These ${String(OWN.length)} are this framework's own:`,
1536
- '',
1537
- ...heading(true),
1538
- ...(unread === ''
1539
- ? []
1540
- : [
1541
- '',
1542
- 'THIS PAGE IS INCOMPLETE. This tree is here but could not be read, so everything it makes,',
1543
- 'runs and derives is missing below — only the six commands above are certain.',
1544
- ` the read failed with: ${unread}`,
1545
- 'Until that is fixed, a command missing from this page means nothing at all.',
1546
- ]),
1547
- ...(makes.length === 0
1548
- ? []
1549
- : ['', 'what this tree makes — each states what it wants and writes the row for you:', '', ...makes]),
1550
- ...(runs.length === 0 ? [] : ['', 'what this tree runs:', '', ...runs]),
1551
- ...(derives.length === 0
1552
- ? []
1553
- : [
1554
- '',
1555
- 'what this tree derives from what is declared. You rarely run these yourself — a command',
1556
- 'that writes a row runs whatever its change made stale, and `npx thread gate` runs every check:',
1557
- '',
1558
- ...derives,
1559
- ]),
1560
- '',
1561
- ...(await attachable('attach')),
1562
- '',
1563
- `read before you write: \`${TYPED} read <id>\` prints exactly what one row declares and the file it`,
1564
- 'names, needs no receipt, and writes nothing. Run any noun above with no arguments to see what',
1565
- 'already exists, each with its sentence, and the whole method it asks. That is a ROW — `read <path>`',
1566
- 'at the top of this page is a FILE, and when one of the two comes back empty the other usually does not.',
1567
- '',
1568
- 'this page is what THIS tree answers, not everything thread can do. A command that is not listed',
1569
- 'may be one no attached package brings rather than one that does not exist, so `attach` first and',
1570
- 'look again. And try it anyway: an unknown command prints every command that does exist.',
1571
- '',
1572
- ].join('\n'));
1573
- return 1;
1574
- }
1575
- if (name === 'create') {
1576
- return await dispatching('create', 'makes', 'makes nothing yet');
1577
- }
1578
- async function settled() {
1579
- let rows;
1580
- try {
1581
- rows = await (await chain()).generators(findTree());
1582
- }
1583
- catch (thrown) {
1584
- stderr.write(`thread: the packages are attached and their rows are written; what they derive was not built, because ` +
1585
- `reading what this tree offers threw: ${thrown instanceof Error ? thrown.message : String(thrown)}\n`);
1586
- return;
1587
- }
1588
- if (rows.length === 0)
1589
- return;
1590
- const settled = (await chain()).settling(findTree().at, rows);
1591
- for (const said of settled.ran)
1592
- stdout.write(`${said}\n`);
1593
- if (settled.unsettled.length > 0) {
1594
- const writers = settled.unsettled.map((one) => one.replace(/\s*--check\s*$/, ''));
1595
- stdout.write(`thread: ${settled.unsettled.join(', ')} did not settle. The rows are written; what they derive is not built ` +
1596
- 'yet, which is usually because this attach named dependencies your app does not have yet — a generator ' +
1597
- `cannot read a tree whose compiler cannot resolve it. Install them, then run: ${writers.join(', ')}\n`);
1598
- }
1599
- }
1600
- if (name === 'run') {
1601
- return await dispatching('run', 'runs', 'runs nothing yet');
1602
- }
1603
- if (name === 'read') {
1604
- let root = cwd();
1605
- let threaded = true;
1606
- try {
1607
- root = findTree().at;
1608
- }
1609
- catch {
1610
- root = cwd();
1611
- threaded = false;
1612
- }
1613
- const target = argv[3];
1614
- if (threaded && target !== undefined && !target.startsWith('-') && !existsSync(resolve(root, target))) {
1615
- argv.splice(2, 1, 'create', 'meta', 'read');
1616
- return await dispatching('create', 'makes', 'makes nothing yet');
1617
- }
1618
- return reading(root);
1619
- }
1620
- if (name === 'list')
1621
- return await listing();
1622
- if (name === 'repair')
1623
- return await repairing();
1624
- if (name === 'edit') {
1625
- let root = cwd();
1626
- try {
1627
- root = findTree().at;
1628
- }
1629
- catch {
1630
- stderr.write('thread: there is no tree here, so nothing declares anything and there is no body this owns. ' +
1631
- 'Run `npx thread init` first.\n');
1632
- return 1;
1633
- }
1634
- const target = argv[3];
1635
- if (target !== undefined && !target.startsWith('-') && !existsSync(resolve(root, target)) && !argv.includes(FROM)) {
1636
- const dropping = argv.indexOf(REMOVES);
1637
- if (dropping !== -1)
1638
- argv.splice(dropping, 1);
1639
- argv.splice(2, 1, 'create', 'meta', dropping === -1 ? 'edit' : 'remove');
1640
- return await dispatching('create', 'makes', 'makes nothing yet');
1641
- }
1642
- if (target !== undefined && !target.startsWith('-') && argv.includes(REMOVES)) {
1643
- return await deleting(root, argv.slice(3).filter((one) => !one.startsWith('-')));
1644
- }
1645
- return editing(root);
1646
- }
1647
- if (name === 'attach' || name === 'detach') {
1648
- const which = argv[3];
1649
- if (which === undefined) {
1650
- stderr.write([`npx thread ${name} <package>`, '', ...(await attachable(name)), ''].join('\n'));
1651
- return 1;
1652
- }
1653
- try {
1654
- const { attach, behind, detach } = await attaching();
1655
- const brought = name === 'attach' ? await behind(cwd(), which) : [];
1656
- const touched = name === 'attach' ? await attach(cwd(), which) : await detach(cwd(), which);
1657
- if (touched.length === 0) {
1658
- stdout.write(`thread: no changes — every row ${which} ships is already in this tree, at the version installed.\n`);
1659
- }
1660
- if (touched.length > 0 && name === 'attach') {
1661
- const gained = [...new Set(brought.map((one) => one.says).filter((one) => one !== undefined))].sort();
1662
- if (gained.length > 0) {
1663
- stdout.write(`thread: this tree now answers ${gained.map((one) => `\`${one}\``).join(', ')} — run any of them bare to see what it does.\n`);
1664
- }
1665
- }
1666
- for (const one of touched)
1667
- stdout.write(`thread: ${name === 'attach' ? 'wrote' : 'changed'} ${one}\n`);
1668
- if (touched.length > 0)
1669
- await settled();
1670
- return 0;
1671
- }
1672
- catch (thrown) {
1673
- stderr.write(`thread: ${thrown instanceof Error ? thrown.message : String(thrown)}\n`);
1674
- return 1;
1675
- }
1676
- }
1677
- if (name !== 'init')
1678
- return await derived(name);
1679
- try {
1680
- const { attach, attaches } = await attaching();
1681
- for (const one of (await starting()).init(cwd()))
1682
- stdout.write(`thread: wrote ${one}\n`);
1683
- const installed = () => {
1684
- try {
1685
- const manifest = JSON.parse(readFileSync(join(cwd(), 'package.json'), 'utf8'));
1686
- return [...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.devDependencies ?? {})]
1687
- .filter(ours)
1688
- .sort();
1689
- }
1690
- catch {
1691
- return [];
1692
- }
1693
- };
1694
- const core = [];
1695
- const skipped = [];
1696
- for (const one of installed()) {
1697
- if (!(await attaches(cwd(), one)))
1698
- continue;
1699
- if (one === NAMED)
1700
- core.push(one);
1701
- else
1702
- skipped.push(one);
1703
- }
1704
- const took = [];
1705
- const blocked = [];
1706
- for (const one of core) {
1707
- try {
1708
- const touched = await attach(cwd(), one);
1709
- if (touched.length > 0)
1710
- took.push(one);
1711
- for (const each of touched)
1712
- stdout.write(`thread: wrote ${each}\n`);
1713
- }
1714
- catch (thrown) {
1715
- blocked.push({ name: one, why: thrown instanceof Error ? thrown.message : String(thrown) });
1716
- }
1717
- }
1718
- for (const one of blocked)
1719
- core.splice(core.indexOf(one.name), 1);
1720
- if (!here(FLOOR)) {
1721
- stdout.write(`thread: ${FLOOR} is not installed, so this tree has no gate and nothing yet refuses anything. ` +
1722
- `Install it with \`npm install ${FLOOR}\`, then run \`npx thread attach ${FLOOR}\`.\n`);
1723
- }
1724
- const onDisk = (one) => existsSync(join(cwd(), 'node_modules', one, 'package.json'));
1725
- const declared = (() => {
1726
- try {
1727
- const manifest = JSON.parse(readFileSync(join(cwd(), 'package.json'), 'utf8'));
1728
- return [
1729
- ...new Set([...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.devDependencies ?? {})]),
1730
- ].sort();
1731
- }
1732
- catch {
1733
- return [];
1734
- }
1735
- })();
1736
- const absent = declared.filter((one) => !onDisk(one));
1737
- if (absent.length > 0) {
1738
- stdout.write(`thread: ${absent.join(', ')} ${absent.length === 1 ? 'is' : 'are'} declared here and not on disk. ` +
1739
- 'Installing, because an attach that adds a package to this manifest and then leaves it unfetched has ' +
1740
- 'handed you a tree its own generators cannot read.\n');
1741
- const ran = spawnSync('npm', ['install'], { cwd: cwd(), stdio: 'inherit' });
1742
- const still = absent.filter((one) => !onDisk(one));
1743
- if (ran.status !== 0 || still.length > 0) {
1744
- stderr.write(`thread: \`npm install\` did not put ${still.length === 0 ? 'them' : still.join(', ')} on disk` +
1745
- `${ran.status === null || ran.status === 0 ? '' : ` — it exited ${String(ran.status)}`}. The rows are ` +
1746
- 'written and what they derive is not built, and it stays that way until the install succeeds. ' +
1747
- 'Nothing after this ran.\n');
1748
- return 1;
1749
- }
1750
- stdout.write(`thread: installed ${absent.join(', ')}.\n`);
1751
- }
1752
- await settled();
1753
- stdout.write(tutorial(took, core, skipped, blocked));
1754
- return 0;
1755
- }
1756
- catch (thrown) {
1757
- stderr.write(`thread: ${thrown instanceof Error ? thrown.message : String(thrown)}\n`);
1758
- return 1;
1759
- }
1760
- }
1761
- exit(await run(argv[2]));