@prestack/thread 0.0.1

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 (248) hide show
  1. package/CHANGELOG.md +192 -0
  2. package/dist/attach.d.ts +1 -0
  3. package/dist/attach.js +1 -0
  4. package/dist/core/actor.d.ts +12 -0
  5. package/dist/core/actor.js +8 -0
  6. package/dist/core/asking.d.ts +180 -0
  7. package/dist/core/asking.js +1109 -0
  8. package/dist/core/attach.d.ts +54 -0
  9. package/dist/core/attach.js +510 -0
  10. package/dist/core/chain.d.ts +16 -0
  11. package/dist/core/chain.js +114 -0
  12. package/dist/core/cli.d.ts +2 -0
  13. package/dist/core/cli.js +1682 -0
  14. package/dist/core/condition.d.ts +52 -0
  15. package/dist/core/condition.js +198 -0
  16. package/dist/core/descend.d.ts +81 -0
  17. package/dist/core/descend.js +879 -0
  18. package/dist/core/finding.d.ts +12 -0
  19. package/dist/core/finding.js +23 -0
  20. package/dist/core/generators/meta.d.ts +2 -0
  21. package/dist/core/generators/meta.js +823 -0
  22. package/dist/core/init.d.ts +15 -0
  23. package/dist/core/init.js +134 -0
  24. package/dist/core/invoked.d.ts +9 -0
  25. package/dist/core/invoked.js +41 -0
  26. package/dist/core/round.d.ts +1 -0
  27. package/dist/core/round.js +57 -0
  28. package/dist/core/rows.d.ts +26 -0
  29. package/dist/core/rows.js +354 -0
  30. package/dist/core/scope.d.ts +6 -0
  31. package/dist/core/scope.js +12 -0
  32. package/dist/core/thread.d.ts +26 -0
  33. package/dist/core/thread.js +5 -0
  34. package/dist/core/tree.d.ts +12 -0
  35. package/dist/core/tree.js +56 -0
  36. package/dist/core/types.d.ts +49 -0
  37. package/dist/core/types.js +311 -0
  38. package/dist/derivation/attach.d.ts +2 -0
  39. package/dist/derivation/attach.js +61 -0
  40. package/dist/derivation/generators/build.d.ts +16 -0
  41. package/dist/derivation/generators/build.js +217 -0
  42. package/dist/derivation/generators/check.d.ts +18 -0
  43. package/dist/derivation/generators/check.js +78 -0
  44. package/dist/derivation/generators/derivation.d.ts +3 -0
  45. package/dist/derivation/generators/derivation.js +273 -0
  46. package/dist/derivation/generators/determinism.d.ts +20 -0
  47. package/dist/derivation/generators/determinism.js +110 -0
  48. package/dist/derivation/generators/release.d.ts +3 -0
  49. package/dist/derivation/generators/release.js +183 -0
  50. package/dist/derivation/makers/release.d.ts +1 -0
  51. package/dist/derivation/makers/release.js +107 -0
  52. package/dist/derivation/release.d.ts +10 -0
  53. package/dist/derivation/release.js +12 -0
  54. package/dist/integrations/attach.d.ts +6 -0
  55. package/dist/integrations/attach.js +177 -0
  56. package/dist/integrations/generators/read.d.ts +1 -0
  57. package/dist/integrations/generators/read.js +92 -0
  58. package/dist/integrations/generators/sdk.d.ts +1 -0
  59. package/dist/integrations/generators/sdk.js +148 -0
  60. package/dist/integrations/generators/secret.d.ts +1 -0
  61. package/dist/integrations/generators/secret.js +233 -0
  62. package/dist/integrations/generators/target.d.ts +1 -0
  63. package/dist/integrations/generators/target.js +765 -0
  64. package/dist/integrations/generators/vault.d.ts +1 -0
  65. package/dist/integrations/generators/vault.js +164 -0
  66. package/dist/integrations/sdk.d.ts +6 -0
  67. package/dist/integrations/sdk.js +151 -0
  68. package/dist/integrations/secret.d.ts +16 -0
  69. package/dist/integrations/secret.js +30 -0
  70. package/dist/integrations/target.d.ts +49 -0
  71. package/dist/integrations/target.js +225 -0
  72. package/dist/integrations/vault.d.ts +12 -0
  73. package/dist/integrations/vault.js +104 -0
  74. package/dist/integrity/attach.d.ts +2 -0
  75. package/dist/integrity/attach.js +213 -0
  76. package/dist/integrity/check-threads.d.ts +10 -0
  77. package/dist/integrity/check-threads.js +153 -0
  78. package/dist/integrity/constraint.d.ts +16 -0
  79. package/dist/integrity/constraint.js +1105 -0
  80. package/dist/integrity/generators/compiler.d.ts +1 -0
  81. package/dist/integrity/generators/compiler.js +130 -0
  82. package/dist/integrity/generators/gate.d.ts +1 -0
  83. package/dist/integrity/generators/gate.js +172 -0
  84. package/dist/integrity/generators/issues.d.ts +1 -0
  85. package/dist/integrity/generators/issues.js +100 -0
  86. package/dist/integrity/generators/publish.d.ts +1 -0
  87. package/dist/integrity/generators/publish.js +186 -0
  88. package/dist/integrity/generators/rule.d.ts +2 -0
  89. package/dist/integrity/generators/rule.js +213 -0
  90. package/dist/integrity/generators/seal.d.ts +4 -0
  91. package/dist/integrity/generators/seal.js +101 -0
  92. package/dist/integrity/generators/trial.d.ts +39 -0
  93. package/dist/integrity/generators/trial.js +254 -0
  94. package/dist/integrity/lint.d.ts +50 -0
  95. package/dist/integrity/lint.js +161 -0
  96. package/dist/integrity/lsp/bin.d.ts +2 -0
  97. package/dist/integrity/lsp/bin.js +35 -0
  98. package/dist/integrity/lsp/diagnostics.d.ts +25 -0
  99. package/dist/integrity/lsp/diagnostics.js +57 -0
  100. package/dist/integrity/lsp/findings.d.ts +20 -0
  101. package/dist/integrity/lsp/findings.js +39 -0
  102. package/dist/integrity/lsp/lint.d.ts +7 -0
  103. package/dist/integrity/lsp/lint.js +95 -0
  104. package/dist/integrity/lsp/protocol.d.ts +20 -0
  105. package/dist/integrity/lsp/protocol.js +57 -0
  106. package/dist/integrity/lsp/server.d.ts +15 -0
  107. package/dist/integrity/lsp/server.js +171 -0
  108. package/dist/integrity/sealed.d.ts +1 -0
  109. package/dist/integrity/sealed.js +56 -0
  110. package/dist/integrity/slopnator.d.ts +90 -0
  111. package/dist/integrity/slopnator.js +267 -0
  112. package/dist/integrity/slopnators/sl_broken_lineage.d.ts +2 -0
  113. package/dist/integrity/slopnators/sl_broken_lineage.js +35 -0
  114. package/dist/integrity/slopnators/sl_declared_elsewhere.d.ts +2 -0
  115. package/dist/integrity/slopnators/sl_declared_elsewhere.js +33 -0
  116. package/dist/integrity/slopnators/sl_derived_drift.d.ts +8 -0
  117. package/dist/integrity/slopnators/sl_derived_drift.js +91 -0
  118. package/dist/integrity/slopnators/sl_hardcoded_style.d.ts +2 -0
  119. package/dist/integrity/slopnators/sl_hardcoded_style.js +46 -0
  120. package/dist/integrity/slopnators/sl_inert_island.d.ts +2 -0
  121. package/dist/integrity/slopnators/sl_inert_island.js +85 -0
  122. package/dist/integrity/slopnators/sl_inert_slopnator.d.ts +2 -0
  123. package/dist/integrity/slopnators/sl_inert_slopnator.js +76 -0
  124. package/dist/integrity/slopnators/sl_missing_row.d.ts +2 -0
  125. package/dist/integrity/slopnators/sl_missing_row.js +180 -0
  126. package/dist/integrity/slopnators/sl_no_barrel.d.ts +2 -0
  127. package/dist/integrity/slopnators/sl_no_barrel.js +41 -0
  128. package/dist/integrity/slopnators/sl_no_child_process.d.ts +2 -0
  129. package/dist/integrity/slopnators/sl_no_child_process.js +61 -0
  130. package/dist/integrity/slopnators/sl_no_comment.d.ts +2 -0
  131. package/dist/integrity/slopnators/sl_no_comment.js +41 -0
  132. package/dist/integrity/slopnators/sl_no_dispatch_bypass.d.ts +2 -0
  133. package/dist/integrity/slopnators/sl_no_dispatch_bypass.js +301 -0
  134. package/dist/integrity/slopnators/sl_no_import_cycle.d.ts +2 -0
  135. package/dist/integrity/slopnators/sl_no_import_cycle.js +42 -0
  136. package/dist/integrity/slopnators/sl_no_such_command.d.ts +2 -0
  137. package/dist/integrity/slopnators/sl_no_such_command.js +55 -0
  138. package/dist/integrity/slopnators/sl_only_takes_a_literal.d.ts +2 -0
  139. package/dist/integrity/slopnators/sl_only_takes_a_literal.js +59 -0
  140. package/dist/integrity/slopnators/sl_secret_literal.d.ts +2 -0
  141. package/dist/integrity/slopnators/sl_secret_literal.js +89 -0
  142. package/dist/integrity/slopnators/sl_stale_path.d.ts +2 -0
  143. package/dist/integrity/slopnators/sl_stale_path.js +92 -0
  144. package/dist/integrity/slopnators/sl_twice_meant.d.ts +2 -0
  145. package/dist/integrity/slopnators/sl_twice_meant.js +30 -0
  146. package/dist/integrity/slopnators/sl_twice_parented.d.ts +2 -0
  147. package/dist/integrity/slopnators/sl_twice_parented.js +34 -0
  148. package/dist/integrity/slopnators/sl_undeclared_crossing.d.ts +2 -0
  149. package/dist/integrity/slopnators/sl_undeclared_crossing.js +78 -0
  150. package/dist/integrity/slopnators/sl_undeclared_destination.d.ts +2 -0
  151. package/dist/integrity/slopnators/sl_undeclared_destination.js +56 -0
  152. package/dist/integrity/slopnators/sl_undeclared_element.d.ts +2 -0
  153. package/dist/integrity/slopnators/sl_undeclared_element.js +34 -0
  154. package/dist/integrity/slopnators/sl_undeclared_node.d.ts +2 -0
  155. package/dist/integrity/slopnators/sl_undeclared_node.js +59 -0
  156. package/dist/integrity/slopnators/sl_undeclared_operation.d.ts +2 -0
  157. package/dist/integrity/slopnators/sl_undeclared_operation.js +120 -0
  158. package/dist/integrity/slopnators/sl_undeclared_variant.d.ts +2 -0
  159. package/dist/integrity/slopnators/sl_undeclared_variant.js +117 -0
  160. package/dist/integrity/slopnators/sl_undescribed.d.ts +2 -0
  161. package/dist/integrity/slopnators/sl_undescribed.js +54 -0
  162. package/dist/integrity/slopnators/sl_ungoverned_island.d.ts +2 -0
  163. package/dist/integrity/slopnators/sl_ungoverned_island.js +58 -0
  164. package/dist/integrity/slopnators/sl_unkept_rows.d.ts +2 -0
  165. package/dist/integrity/slopnators/sl_unkept_rows.js +104 -0
  166. package/dist/integrity/slopnators/sl_unnamed_island.d.ts +2 -0
  167. package/dist/integrity/slopnators/sl_unnamed_island.js +37 -0
  168. package/dist/integrity/slopnators/sl_unpaired_member.d.ts +2 -0
  169. package/dist/integrity/slopnators/sl_unpaired_member.js +114 -0
  170. package/dist/integrity/slopnators/sl_unpaired_slopnator.d.ts +2 -0
  171. package/dist/integrity/slopnators/sl_unpaired_slopnator.js +60 -0
  172. package/dist/integrity/slopnators/sl_unprefixed_member.d.ts +2 -0
  173. package/dist/integrity/slopnators/sl_unprefixed_member.js +126 -0
  174. package/dist/integrity/slopnators/sl_unseamed_reach.d.ts +2 -0
  175. package/dist/integrity/slopnators/sl_unseamed_reach.js +73 -0
  176. package/dist/integrity/slopnators/sl_unthreaded.d.ts +56 -0
  177. package/dist/integrity/slopnators/sl_unthreaded.js +352 -0
  178. package/dist/integrity/slopnators/sl_untranslated_key.d.ts +2 -0
  179. package/dist/integrity/slopnators/sl_untranslated_key.js +157 -0
  180. package/dist/integrity/slopnators/sl_unversioned.d.ts +2 -0
  181. package/dist/integrity/slopnators/sl_unversioned.js +31 -0
  182. package/dist/integrity/sweep.d.ts +24 -0
  183. package/dist/integrity/sweep.js +264 -0
  184. package/dist/knowledge/attach.d.ts +2 -0
  185. package/dist/knowledge/attach.js +339 -0
  186. package/dist/knowledge/generators/docsgen.d.ts +1 -0
  187. package/dist/knowledge/generators/docsgen.js +145 -0
  188. package/dist/knowledge/generators/glossary.d.ts +1 -0
  189. package/dist/knowledge/generators/glossary.js +56 -0
  190. package/dist/knowledge/generators/skills.d.ts +1 -0
  191. package/dist/knowledge/generators/skills.js +222 -0
  192. package/dist/knowledge/glossary.d.ts +6 -0
  193. package/dist/knowledge/glossary.js +1 -0
  194. package/dist/operations/access.d.ts +27 -0
  195. package/dist/operations/access.js +61 -0
  196. package/dist/operations/attach.d.ts +6 -0
  197. package/dist/operations/attach.js +414 -0
  198. package/dist/operations/capabilities.d.ts +37 -0
  199. package/dist/operations/capabilities.js +35 -0
  200. package/dist/operations/cost.d.ts +14 -0
  201. package/dist/operations/cost.js +37 -0
  202. package/dist/operations/dispatch.d.ts +14 -0
  203. package/dist/operations/dispatch.js +327 -0
  204. package/dist/operations/error-results.d.ts +4 -0
  205. package/dist/operations/error-results.js +25 -0
  206. package/dist/operations/errors/catalog.d.ts +58 -0
  207. package/dist/operations/errors/catalog.js +91 -0
  208. package/dist/operations/errors/classes.d.ts +73 -0
  209. package/dist/operations/errors/classes.js +22 -0
  210. package/dist/operations/errors/errors.d.ts +52 -0
  211. package/dist/operations/errors/errors.js +257 -0
  212. package/dist/operations/generators/capability.d.ts +5 -0
  213. package/dist/operations/generators/capability.js +603 -0
  214. package/dist/operations/generators/compile.d.ts +2 -0
  215. package/dist/operations/generators/compile.js +541 -0
  216. package/dist/operations/generators/grant.d.ts +1 -0
  217. package/dist/operations/generators/grant.js +267 -0
  218. package/dist/operations/generators/operation.d.ts +1 -0
  219. package/dist/operations/generators/operation.js +811 -0
  220. package/dist/operations/generators/permits.d.ts +1 -0
  221. package/dist/operations/generators/permits.js +196 -0
  222. package/dist/operations/generators/seed.d.ts +20 -0
  223. package/dist/operations/generators/seed.js +285 -0
  224. package/dist/operations/generators/trial.d.ts +53 -0
  225. package/dist/operations/generators/trial.js +376 -0
  226. package/dist/operations/http.d.ts +30 -0
  227. package/dist/operations/http.js +121 -0
  228. package/dist/operations/makers/capability.d.ts +1 -0
  229. package/dist/operations/makers/capability.js +117 -0
  230. package/dist/operations/makers/trial.d.ts +1 -0
  231. package/dist/operations/makers/trial.js +413 -0
  232. package/dist/operations/memory.d.ts +104 -0
  233. package/dist/operations/memory.js +365 -0
  234. package/dist/operations/metering.d.ts +55 -0
  235. package/dist/operations/metering.js +247 -0
  236. package/dist/operations/nodes.d.ts +35 -0
  237. package/dist/operations/nodes.js +15 -0
  238. package/dist/operations/operations.d.ts +18 -0
  239. package/dist/operations/operations.js +5 -0
  240. package/dist/operations/queue-policy.d.ts +3 -0
  241. package/dist/operations/queue-policy.js +8 -0
  242. package/dist/operations/result.d.ts +19 -0
  243. package/dist/operations/result.js +10 -0
  244. package/dist/operations/seams.d.ts +239 -0
  245. package/dist/operations/seams.js +45 -0
  246. package/dist/operations/store.d.ts +14 -0
  247. package/dist/operations/store.js +1 -0
  248. package/package.json +16 -0
@@ -0,0 +1,823 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
10
+ import { dirname, join } from 'node:path';
11
+ import { argv, env, stdout } from 'node:process';
12
+ import { pathToFileURL } from 'node:url';
13
+ import { exit } from "../invoked.js";
14
+ import { BODY, RECEIPT, TYPED, answer, arrayValue, asBoolean, asFlag, asMap, asking, bodied, childrenIn, asksOf, declared, gathered, given as pipedIn, grows, guidance, issue, mapped, marked, named as answerNames, needed, nouns, only, ownAnswers, refuse, rowLines, seeding, spend, stating, VERBS, within, wordsIn } from "../asking.js";
15
+ import { generators } from "../chain.js";
16
+ import { listFields, shapeFields } from "../descend.js";
17
+ import { STATED, fieldIn, holdsRow, listBounds, quoted, reconciled, rowsIn, withField, withRow, withoutRow, } from "../rows.js";
18
+ import { findTree } from "../tree.js";
19
+ const TREE = findTree();
20
+ const NOUN = env.THREAD_NOUN ?? 'meta';
21
+ const MINE = NOUN === 'meta' ? undefined : (await declared(TREE)).get(NOUN);
22
+ const FILES = MINE?.files;
23
+ const LIST = MINE?.list ?? 'declares';
24
+ const SHAPE = MINE?.shape ?? 'Threaded';
25
+ const FIELDS = MINE?.fields ?? shapeFields(SHAPE).map((one) => ({ ...one, boolean: false }));
26
+ const ADDRESSED = FIELDS.some((one) => one.name === 'at');
27
+ export const ASKS = asksOf(NOUN, FIELDS, {
28
+ name: {
29
+ guide: NOUN === 'meta'
30
+ ? 'kebab-case, e.g. billing — this becomes its id, and its own sheet is scaffolded empty beside it, ready for whatever this app declares under it next'
31
+ : `kebab-case, e.g. billing — this becomes its id, and no other ${NOUN} in this tree may carry it`,
32
+ asks: 'name: ',
33
+ },
34
+ ...(NOUN === 'meta' || !ADDRESSED || FILES !== undefined
35
+ ? {}
36
+ : {
37
+ at: {
38
+ guide: 'the path of a file that already exists, relative to this tree',
39
+ asks: 'at — the file this names: ',
40
+ },
41
+ }),
42
+ describes: {
43
+ guide: NOUN === 'meta'
44
+ ? 'one sentence a reader who has not seen the code could act on — what this governs'
45
+ : `one sentence a reader who has not seen the code could act on — what this ${NOUN} is`,
46
+ asks: 'describes — one sentence saying what this governs: ',
47
+ },
48
+ }, NOUN === 'meta' || FILES !== undefined ? ['id', 'at', 'version'] : ['id', 'version']);
49
+ const said = (one) => ASKS.find((each) => each.at === one)?.asks ?? `${one}: `;
50
+ const WIDTH = Math.max(...ASKS.map((one) => one.at.length)) + 3;
51
+ const kebab = (one) => one.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
52
+ const KEBAB = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
53
+ const at = MINE?.at ?? within(TREE.at, TREE.root);
54
+ let sheet = {};
55
+ try {
56
+ sheet = (await import(__rewriteRelativeImportExtension(pathToFileURL(join(TREE.at, at)).href)));
57
+ }
58
+ catch {
59
+ sheet = {};
60
+ }
61
+ const inSheet = (() => {
62
+ const only = sheet[LIST];
63
+ if (Array.isArray(only))
64
+ return only;
65
+ for (const value of Object.values(sheet)) {
66
+ const rows = value?.[LIST];
67
+ if (Array.isArray(rows))
68
+ return rows;
69
+ }
70
+ return undefined;
71
+ })();
72
+ if (inSheet === undefined) {
73
+ refuse(`${at} states no \`${LIST}\` list, so there is nowhere in it for a ${NOUN} to go. \`npx thread read ${at}\` prints what it does state.`);
74
+ }
75
+ const existing = inSheet;
76
+ const mark = (one) => marked([one.id, one.at ?? '', one.describes, String(one.version ?? '')]);
77
+ const listing = [
78
+ existing.length === 0
79
+ ? `${at} declares nothing yet.`
80
+ : `${String(existing.length)} thing(s) ${at} already declares:`,
81
+ ...existing.map((one) => ` [${mark(one)}] ${one.id} — ${one.describes}`),
82
+ ].join('\n');
83
+ const words = wordsIn(['new', 'read', 'remove', 'add', 'edit']);
84
+ const mustSay = (fields = FIELDS) => fields.filter((one) => one.required).map((one) => one.name);
85
+ const kept = () => [...mustSay(), 'version'];
86
+ const SAYS = new Map([
87
+ ['id', 'the word this row coins — kebab-case, and not already in that sheet'],
88
+ ['at', 'the path of a file that already exists, relative to this tree'],
89
+ ['describes', 'one sentence a reader who has not seen the file could act on'],
90
+ ]);
91
+ function asks(fields) {
92
+ const named = mustSay(fields).filter((one) => one !== 'version');
93
+ const mute = named.filter((one) => !SAYS.has(one));
94
+ if (mute.length > 0) {
95
+ refuse(`every ${NOUN} states ${mute.join(', ')}, and \`add\` has no sentence to ask for ` +
96
+ `${mute.length === 1 ? 'it' : 'them'} with. The sentence goes beside the others in \`SAYS\`.`);
97
+ }
98
+ const pad = Math.max(...named.map((one) => one.length)) + 3;
99
+ return named.map((one) => ` ${`${one}:`.padEnd(pad)}${SAYS.get(one) ?? ''}`);
100
+ }
101
+ function unrestorable(row) {
102
+ const keeps = kept();
103
+ const worn = Object.keys(row).filter((one) => !keeps.includes(one) && row[one] !== undefined);
104
+ if (worn.length === 0)
105
+ return [];
106
+ return [
107
+ '',
108
+ ` \`add\` writes ${keeps.join(', ')} and nothing else, so ${worn.length === 1 ? 'this field goes' : 'these fields go'}`,
109
+ ' and no command here puts it back. Copy it now if this is a repoint rather than a deletion:',
110
+ '',
111
+ ...worn.map((one) => ` ${one}: ${JSON.stringify(row[one])}`),
112
+ ];
113
+ }
114
+ const rowLike = (one) => one !== null && typeof one === 'object' && typeof one.id === 'string';
115
+ const HELD = ['declares', 'holds'];
116
+ const under = async (row, from, seen) => {
117
+ const sheet = row.at;
118
+ if (typeof sheet !== 'string' || sheet === '' || sheet === from || seen.has(sheet))
119
+ return [];
120
+ if (!sheet.startsWith(`${TREE.name}/`))
121
+ return [];
122
+ seen.add(sheet);
123
+ let read;
124
+ try {
125
+ read = (await import(__rewriteRelativeImportExtension(pathToFileURL(join(TREE.at, sheet)).href)));
126
+ }
127
+ catch {
128
+ return [];
129
+ }
130
+ const found = [];
131
+ for (const [name, value] of Object.entries(read)) {
132
+ if (Array.isArray(value)) {
133
+ for (const each of value)
134
+ if (rowLike(each))
135
+ found.push({ row: each, list: name, sheet });
136
+ continue;
137
+ }
138
+ for (const list of HELD) {
139
+ const held = value?.[list];
140
+ if (Array.isArray(held))
141
+ for (const each of held)
142
+ if (rowLike(each))
143
+ found.push({ row: each, list, sheet });
144
+ }
145
+ }
146
+ for (const each of [...found])
147
+ found.push(...(await under(each.row, sheet, seen)));
148
+ return found;
149
+ };
150
+ async function pointedAt(which, inNode) {
151
+ const byNode = inNode === undefined ? existing : existing.filter((each) => mark(each) === inNode || each.id === inNode);
152
+ const sheeted = inNode !== undefined && byNode.length === 0 && inNode.endsWith('.ts');
153
+ const looking = sheeted ? existing : byNode;
154
+ const metas = inNode === undefined ? existing.filter((each) => mark(each) === which || each.id === which) : [];
155
+ const kids = [];
156
+ const seen = new Set();
157
+ for (const meta of looking) {
158
+ for (const held of await under(meta, at, seen)) {
159
+ if (mark(held.row) !== which && held.row.id !== which)
160
+ continue;
161
+ if (sheeted && held.sheet !== inNode)
162
+ continue;
163
+ kids.push({ meta, row: held.row, list: held.list, sheet: held.sheet });
164
+ }
165
+ }
166
+ return { looking, metas, kids };
167
+ }
168
+ if (words[0] === 'read') {
169
+ const which = words[2] ?? words[1];
170
+ const inNode = words[2] === undefined ? undefined : words[1];
171
+ const pointed = await pointedAt(which, inNode);
172
+ const kid = pointed.kids[0];
173
+ const one = pointed.metas[0] ?? kid?.row;
174
+ if (one === undefined) {
175
+ refuse(`\`${which ?? '(nothing)'}\` is not a row this tree declares. Give its id, or the mark printed in brackets ` +
176
+ `beside it — \`${TYPED} create ${NOUN}\` with nothing after it lists every one.`);
177
+ }
178
+ const sheet = kid === undefined ? at : kid.sheet;
179
+ const said = Object.entries(one).filter(([name, value]) => name !== 'id' && name !== 'describes' && value !== undefined);
180
+ const pad = Math.max(0, ...said.map(([name]) => name.length)) + 3;
181
+ stdout.write(`${one.id} — ${one.describes}\n`);
182
+ stdout.write(` it is a row in ${sheet}\n\n`);
183
+ for (const [name, value] of said) {
184
+ stdout.write(` ${`${name}:`.padEnd(pad)}${typeof value === 'string' ? value : JSON.stringify(value)}\n`);
185
+ }
186
+ if (one.at === undefined) {
187
+ stdout.write(`\n${one.id} is a value ${sheet} holds rather than a file of its own.\n`);
188
+ exit(0);
189
+ }
190
+ if (one.at === sheet) {
191
+ stdout.write(`\n${one.at} is the sheet this row sits in rather than a file of its own.\n`);
192
+ exit(0);
193
+ }
194
+ stdout.write(`\n${one.at}:\n`);
195
+ try {
196
+ stdout.write(readFileSync(join(TREE.at, one.at), 'utf8'));
197
+ }
198
+ catch {
199
+ stdout.write('(nothing is there)\n');
200
+ }
201
+ exit(0);
202
+ }
203
+ if (words[0] === 'remove') {
204
+ const which = words[2] ?? words[1];
205
+ const inNode = words[2] === undefined ? undefined : words[1];
206
+ const { looking, metas, kids } = await pointedAt(which, inNode);
207
+ if (inNode !== undefined && looking.length === 0) {
208
+ refuse(`\`${inNode}\` is not a governing node ${at} declares, so there is nothing under it to take out.`);
209
+ }
210
+ if (metas.length + kids.length === 0) {
211
+ refuse(`\`${which ?? '(nothing)'}\` is not a row this tree declares. Every governing node in ${at} was ` +
212
+ 'searched, and every list under each of them. Give the id, or the mark printed in brackets beside it above.');
213
+ }
214
+ if (metas.length + kids.length > 1) {
215
+ const named = [
216
+ ...metas.map((each) => `\`${each.id ?? '(unnamed)'}\` — the governing node in ${at}`),
217
+ ...kids.map((each) => `\`${each.row.id ?? '(unnamed)'}\` — under \`${each.meta.id ?? '(unnamed)'}\`, in ${each.sheet}`),
218
+ ];
219
+ refuse(`\`${which}\` names ${String(named.length)} rows: ${named.join('; ')}. Say which node or which sheet to look in — ` +
220
+ `\`thread create ${NOUN} remove <node|sheet> ${String(which)}\` — because two rows carrying the same values carry the ` +
221
+ 'same mark AND the same id, so naming the row again cannot separate them. Nothing was written.');
222
+ }
223
+ const kid = kids[0];
224
+ if (kid !== undefined) {
225
+ const sheet = join(TREE.at, kid.sheet);
226
+ const wrote = kid.list === 'derives' && kid.row.at !== undefined && kid.row.at !== kid.sheet;
227
+ const flaggedKid = argv.indexOf(RECEIPT);
228
+ if (flaggedKid === -1) {
229
+ const nonce = issue(TREE.at, listing);
230
+ stdout.write([
231
+ '',
232
+ `\`${kid.row.id ?? '(unnamed)'}\` — ${kid.row.describes ?? ''}`,
233
+ kid.row.at === undefined
234
+ ? ` it is a value held under \`${kid.meta.id ?? '(unnamed)'}\` in ${kid.sheet}.`
235
+ : ` it names ${kid.row.at}, under \`${kid.meta.id ?? '(unnamed)'}\` in ${kid.sheet}.`,
236
+ '',
237
+ ` thread create ${NOUN} remove ${String(kid.meta.id)} ${String(kid.row.id)} ${RECEIPT} ${nonce}`,
238
+ '',
239
+ ...(wrote
240
+ ? [
241
+ ` this takes the row out of ${kid.sheet} and deletes ${kid.row.at}, because that row derives`,
242
+ ' it: a command wrote every byte of it, and with the row gone nothing writes it again. Any',
243
+ ` command the row names stops existing at the same moment, because \`${TYPED} create\` reads`,
244
+ ' the rows to know what commands there are.',
245
+ ]
246
+ : kid.row.at === undefined
247
+ ? [` this takes the row out of ${kid.sheet} and deletes no file, because a held value names none.`]
248
+ : [
249
+ ` this takes the row out of ${kid.sheet} and LEAVES ${kid.row.at} exactly where it is.`,
250
+ ' Nothing here wrote that file, so nothing here deletes it — it goes back to being declared',
251
+ ' by nobody, which the descent will say.',
252
+ ]),
253
+ ...unrestorable(kid.row),
254
+ '',
255
+ ].join('\n'));
256
+ stdout.write(`create: receipt ${nonce} — spent on use, and stale the moment ${at} changes.\n`);
257
+ exit(0);
258
+ }
259
+ spend(TREE.at, listing, argv[flaggedKid + 1], at);
260
+ const text = readFileSync(sheet, 'utf8');
261
+ const where = listBounds(text, kid.list);
262
+ if (where === undefined) {
263
+ refuse(`${kid.sheet} states no \`${kid.list}\` list, so there is nothing to take a row out of.`);
264
+ }
265
+ writeFileSync(sheet, withoutRow(text, where, String(kid.row.id)), 'utf8');
266
+ stdout.write(`create: took ${String(kid.row.id)} out of ${kid.sheet}.\n`);
267
+ if (!wrote) {
268
+ if (kid.row.at !== undefined)
269
+ stdout.write(`create: ${kid.row.at} is still there, and now declared by nobody.\n`);
270
+ exit(0);
271
+ }
272
+ try {
273
+ rmSync(join(TREE.at, String(kid.row.at)));
274
+ stdout.write(`create: deleted ${kid.row.at} — the row derived it, so nothing writes it any more.\n`);
275
+ }
276
+ catch {
277
+ stdout.write(`create: ${kid.row.at} was not there to delete, so only the row came out.\n`);
278
+ }
279
+ exit(0);
280
+ }
281
+ const one = metas[0];
282
+ if (one === undefined)
283
+ refuse(`\`${which ?? '(nothing)'}\` names nothing this tree declares.`);
284
+ const owns = typeof one.at === 'string' && one.at !== at;
285
+ const inside = await under(one, at, new Set());
286
+ if (inside.length > 0) {
287
+ refuse(`\`${one.id}\` governs ${String(inside.length)} thing(s) — ${inside.map((each) => each.row.id).join(', ')}. Removing it would ` +
288
+ 'leave every one of them answering to nothing, and this will not cascade on your behalf. Take those out first, ' +
289
+ 'and this becomes an empty node you can remove. Nothing was written.');
290
+ }
291
+ const flaggedAt = argv.indexOf(RECEIPT);
292
+ if (flaggedAt === -1) {
293
+ const nonce = issue(TREE.at, listing);
294
+ stdout.write([
295
+ '',
296
+ `\`${one.id}\` — ${one.describes}`,
297
+ owns
298
+ ? ` it governs nothing, and its sheet ${one.at ?? ''} declares an empty list.`
299
+ : one.at === undefined
300
+ ? ` it is a value ${at} holds, and names no file.`
301
+ : ` it governs nothing, and ${one.at} is the sheet this row sits in rather than a file of its own.`,
302
+ '',
303
+ ` thread create ${NOUN} remove ${which ?? one.id} ${RECEIPT} ${nonce}`,
304
+ '',
305
+ owns
306
+ ? ` this takes the row out of ${at} and deletes ${one.at ?? ''}. It is the one command here that`
307
+ : ` this takes the row out of ${at} and deletes no file.`,
308
+ owns
309
+ ? ' running again will not undo.'
310
+ : one.at === undefined
311
+ ? ` ${at} holds every other value in that list and is left exactly as it is.`
312
+ : ` ${one.at} holds every other row in that list and is left exactly as it is.`,
313
+ ...unrestorable(one),
314
+ '',
315
+ ].join('\n'));
316
+ exit(0);
317
+ }
318
+ spend(TREE.at, listing, argv[flaggedAt + 1], at);
319
+ const text = readFileSync(join(TREE.at, at), 'utf8');
320
+ const where = listBounds(text, LIST);
321
+ if (where === undefined)
322
+ refuse(`${at} states no \`${LIST}\` list, so there is nothing to take a row out of.`);
323
+ writeFileSync(join(TREE.at, at), withoutRow(text, where, one.id), 'utf8');
324
+ stdout.write(`create: took ${one.id} out of ${at}.\n`);
325
+ if (owns) {
326
+ try {
327
+ rmSync(join(TREE.at, one.at ?? ''));
328
+ stdout.write(`create: deleted ${one.at ?? ''}.\n`);
329
+ }
330
+ catch {
331
+ stdout.write(`create: ${one.at ?? ''} was not there to delete.\n`);
332
+ }
333
+ }
334
+ exit(0);
335
+ }
336
+ const FIELD_KEY = 'field';
337
+ const FIELD = asFlag(FIELD_KEY);
338
+ const FIELD_ASKS = [
339
+ { at: 'describes', guide: 'the sentence the CLI asks with. One a person who has not seen the code could answer' },
340
+ { at: 'options', guide: 'the values it may take, separated by |, e.g. framework|optional. Leave it off to take any text' },
341
+ {
342
+ at: 'optional',
343
+ guide: 'answer y and a row may leave the field unstated. Anything else and every row must state it, and each row ' +
344
+ 'that does not is named below with the command that fills it in',
345
+ },
346
+ ];
347
+ const FIELD_TAKES = [FIELD_KEY, ...answerNames(FIELD_ASKS)];
348
+ if (words[0] === 'edit' && argv.includes(FIELD)) {
349
+ const name = (argv[argv.indexOf(FIELD) + 1] ?? '').trim();
350
+ if (!/^[a-z][A-Za-z0-9]*$/.test(name)) {
351
+ refuse('`' + name + '` cannot be a field. A field is one camelCase word, e.g. folder. Nothing was written.');
352
+ }
353
+ const answers = pipedIn();
354
+ only(answers, FIELD_TAKES, '`edit ' + FIELD + '`');
355
+ const sentence = (answer(answers, 'describes') ?? '').trim();
356
+ const options = (answer(answers, 'options') ?? '').split('|').map((one) => one.trim()).filter((one) => one !== '');
357
+ const optional = (answer(answers, 'optional') ?? '').trim().toLowerCase().startsWith('y');
358
+ const flaggedField = argv.indexOf(RECEIPT);
359
+ if (flaggedField === -1) {
360
+ const nonce = issue(TREE.at, listing);
361
+ stdout.write([
362
+ '',
363
+ '`edit ' + FIELD + '` gives every ' + NOUN + ' a new field, written into the shape at the top of ' + at + '. From then on',
364
+ '`thread create ' + NOUN + '` asks for it with the sentence you give here, and `thread edit` can set it on any row.',
365
+ '',
366
+ ...guidance(FIELD_ASKS),
367
+ '',
368
+ ' thread create ' + NOUN + ' edit ' + FIELD + ' <name> --describes <sentence> ' + RECEIPT + ' ' + nonce,
369
+ '',
370
+ ].join('\n'));
371
+ stdout.write('create: receipt ' + nonce + ' — spent on use, and stale the moment ' + at + ' changes.\n');
372
+ exit(0);
373
+ }
374
+ spend(TREE.at, listing, argv[flaggedField + 1], at);
375
+ if (sentence === '') {
376
+ refuse('`' + name + '` was given no sentence, and the sentence is what `thread create ' + NOUN + '` asks with. Give --describes. Nothing was written.');
377
+ }
378
+ const file = join(TREE.at, at);
379
+ const text = readFileSync(file, 'utf8');
380
+ const opens = 'export interface ' + SHAPE + ' extends Threaded {';
381
+ const from = text.indexOf(opens);
382
+ if (from === -1) {
383
+ refuse(at + ' has no shape at its top for this to write into. Every sheet `thread create meta new` writes has one. Run `thread read ' + at + '` to see what is there. Nothing was written.');
384
+ }
385
+ const close = text.indexOf('\n}', from);
386
+ if (close === -1) {
387
+ refuse('the shape at the top of ' + at + ' never closes, so nothing can be added to it. Run `thread read ' + at + '` to see where it stops. Nothing was written.');
388
+ }
389
+ const block = text.slice(from, close);
390
+ if (block.includes('readonly ' + name + ':') || block.includes('readonly ' + name + '?:')) {
391
+ refuse('the shape at the top of ' + at + ' already states `' + name + '`. One field, one statement. Nothing was written.');
392
+ }
393
+ const type = options.length === 0 ? 'string' : options.map((one) => quoted(one)).join(' | ');
394
+ const line = '\n /** ' + sentence + ' */\n readonly ' + name + (optional ? '?' : '') + ': ' + type;
395
+ writeFileSync(file, text.slice(0, close) + line + text.slice(close), 'utf8');
396
+ stdout.write('create: every ' + NOUN + ' now states `' + name + '`, and `thread create ' + NOUN + '` asks for it with the sentence you gave.\n');
397
+ if (!optional) {
398
+ const lacking = existing.filter((one) => one[name] === undefined);
399
+ if (lacking.length > 0) {
400
+ stdout.write('create: ' + String(lacking.length) + ' ' + NOUN + '(s) already here state no `' + name + '`, and the shape now requires it. Fill each in:\n');
401
+ for (const one of lacking)
402
+ stdout.write(' thread edit ' + mark(one) + ' --' + name + ' <value>\n');
403
+ }
404
+ }
405
+ exit(0);
406
+ }
407
+ if (words[0] === 'edit') {
408
+ const which = words[2] ?? words[1];
409
+ const inNode = words[2] === undefined ? undefined : words[1];
410
+ const { looking, metas, kids } = await pointedAt(which, inNode);
411
+ if (inNode !== undefined && looking.length === 0) {
412
+ refuse(`\`${inNode}\` is not a governing node ${at} declares, so there is nothing under it to change.`);
413
+ }
414
+ if (metas.length + kids.length === 0) {
415
+ refuse(`\`${which ?? '(nothing)'}\` is not a row this tree declares. Every governing node in ${at} was ` +
416
+ 'searched, and every list under each of them. Give the id, or the mark printed in brackets beside it above.');
417
+ }
418
+ if (metas.length + kids.length > 1) {
419
+ const named = [
420
+ ...metas.map((each) => `\`${each.id ?? '(unnamed)'}\` — the governing node in ${at}`),
421
+ ...kids.map((each) => `\`${each.row.id ?? '(unnamed)'}\` — under \`${each.meta.id ?? '(unnamed)'}\`, in ${each.sheet}`),
422
+ ];
423
+ refuse(`\`${String(which)}\` names ${String(named.length)} rows: ${named.join('; ')}. Say which node to look ` +
424
+ `under — \`thread create ${NOUN} edit <node> ${String(which)}\` — because two rows carrying the same ` +
425
+ 'values carry the same mark AND the same id, so naming the row again cannot separate them. Nothing was ' +
426
+ 'written.');
427
+ }
428
+ const kid = kids[0];
429
+ const held = kid === undefined ? metas[0] : kid.row;
430
+ if (held?.id === undefined) {
431
+ refuse(`\`${String(which)}\` names a row with no id, and a row is reached by its id. Nothing was written.`);
432
+ }
433
+ const file = kid === undefined ? join(TREE.at, at) : join(TREE.at, kid.sheet);
434
+ const shown = kid === undefined ? at : kid.sheet;
435
+ const inList = kid === undefined ? LIST : kid.list;
436
+ const shaped = listFields(file, inList).filter((one) => one.name !== 'id');
437
+ if (shaped.length === 0) {
438
+ refuse(`${shown} states a \`${inList}\` this cannot read the shape of, so which fields a row there may carry is ` +
439
+ 'unknown. Editing one blind is how a row stops being the shape its own list declares.');
440
+ }
441
+ const editable = shaped.map((one) => one.name);
442
+ const listed = new Set(shaped.filter((one) => one.list).map((one) => one.name));
443
+ const flagged = new Set(shaped.filter((one) => one.boolean).map((one) => one.name));
444
+ const text = readFileSync(file, 'utf8');
445
+ const where = listBounds(text, inList);
446
+ if (where === undefined)
447
+ refuse(`${shown} states no \`${inList}\` list, so there is no row in it to change.`);
448
+ const standing = editable.map((one) => ({ field: one, value: fieldIn(text, where, String(held.id), one) }));
449
+ const flaggedEdit = argv.indexOf(RECEIPT);
450
+ if (flaggedEdit === -1) {
451
+ const nonce = issue(TREE.at, listing);
452
+ stdout.write([
453
+ '',
454
+ `\`${String(held.id)}\` — ${String(held.describes ?? '')}`,
455
+ ` it is a row in ${shown}, and \`edit\` changes what one of its fields says. One command, so a refusal`,
456
+ ' leaves the row exactly as it stands — which is the thing `remove` then `add` cannot promise.',
457
+ '',
458
+ 'what it says now:',
459
+ '',
460
+ ...standing.map((one) => ` ${`${one.field}:`.padEnd(12)}${one.value === undefined ? '(not stated)' : one.value}`),
461
+ '',
462
+ 'answer only the field you are changing, one per line, in any order:',
463
+ '',
464
+ ` thread edit ${String(which)} --<field> <value> ${RECEIPT} ${nonce}`,
465
+ '',
466
+ ' a field it is not handed is left alone, and `id` is not among them: changing one retires a word',
467
+ ' and coins another, which is a decision rather than a replacement.',
468
+ '',
469
+ ].join('\n'));
470
+ stdout.write(`create: receipt ${nonce} — spent on use, and stale the moment ${at} changes.\n`);
471
+ exit(0);
472
+ }
473
+ spend(TREE.at, listing, argv[flaggedEdit + 1], at);
474
+ const answers = gathered(pipedIn(), shaped);
475
+ const own = ownAnswers(answers);
476
+ const groups = childrenIn(answers);
477
+ only(answers, editable, '`edit`');
478
+ if (answers.length === 0) {
479
+ refuse('`edit` was handed no field to change. Answer one of the fields it listed. Nothing was written.');
480
+ }
481
+ const wasAt = typeof held.at === 'string' ? held.at : '';
482
+ const moving = own.find((one) => one.key === 'at');
483
+ const goesTo = moving === undefined || moving.value === wasAt ? '' : moving.value;
484
+ if (goesTo !== '' && existsSync(join(TREE.at, goesTo))) {
485
+ refuse(`${goesTo} is already there, and one address holds one thing. Moving \`${String(held.id)}\` onto it would ` +
486
+ 'leave two rows pointing at one file and no way to say which owns it. Nothing was written.');
487
+ }
488
+ let next = text;
489
+ for (const one of own) {
490
+ const bounds = listBounds(next, inList);
491
+ if (bounds === undefined)
492
+ refuse(`${shown} states no \`${LIST}\` list.`);
493
+ const now = fieldIn(next, bounds, String(held.id), one.key);
494
+ if (flagged.has(one.key)) {
495
+ const set = withField(next, bounds, String(held.id), one.key, asBoolean(one.value));
496
+ if (set !== undefined) {
497
+ next = set;
498
+ continue;
499
+ }
500
+ }
501
+ const field = shaped.find((each) => each.name === one.key);
502
+ if (field !== undefined && mapped(field, one.value)) {
503
+ const set = withField(next, bounds, String(held.id), one.key, asMap(one.value));
504
+ if (set !== undefined) {
505
+ next = set;
506
+ continue;
507
+ }
508
+ }
509
+ if (listed.has(one.key)) {
510
+ const put = withField(next, bounds, String(held.id), one.key, arrayValue(' ', one.key, one.value, groups.get(one.key)));
511
+ if (put !== undefined) {
512
+ next = put;
513
+ continue;
514
+ }
515
+ }
516
+ if (now === undefined && listed.has(one.key)) {
517
+ refuse(`\`${String(held.id)}\` states no \`${one.key}\`, and that field holds rows rather than a sentence. ` +
518
+ `Write what goes in it with \`${TYPED} create meta add\`, which writes the whole row. Nothing was written.`);
519
+ }
520
+ if (now !== undefined && !now.startsWith("'")) {
521
+ refuse(`\`${one.key}\` on \`${String(held.id)}\` is ${now}, which is not a plain sentence — replacing it with ` +
522
+ 'one would change what kind of thing it is. Nothing was written.');
523
+ }
524
+ const written = withField(next, bounds, String(held.id), one.key, quoted(one.value));
525
+ if (written === undefined) {
526
+ refuse(`\`${one.key}\` could not be written on \`${String(held.id)}\`. Nothing was written.`);
527
+ }
528
+ next = written;
529
+ }
530
+ for (const [list, found] of groups) {
531
+ const bounds = listBounds(next, inList);
532
+ if (bounds === undefined)
533
+ break;
534
+ const put = withField(next, bounds, String(held.id), list, arrayValue(' ', list, '', found));
535
+ if (put === undefined) {
536
+ refuse(`${list} could not be written on ${String(held.id)}, so nothing was written. Run npx thread read ${shown} to see what that row states now.`);
537
+ }
538
+ next = put;
539
+ }
540
+ writeFileSync(file, next, 'utf8');
541
+ if (goesTo !== '') {
542
+ if (existsSync(join(TREE.at, wasAt))) {
543
+ mkdirSync(dirname(join(TREE.at, goesTo)), { recursive: true });
544
+ renameSync(join(TREE.at, wasAt), join(TREE.at, goesTo));
545
+ stdout.write(`create: moved ${wasAt} to ${goesTo}, because that is where ${String(held.id)} now says it is.\n`);
546
+ }
547
+ else {
548
+ stdout.write(`create: nothing was at ${wasAt}, so nothing moved. \`${String(held.id)}\` now says ${goesTo} and that ` +
549
+ 'address is empty, which `npx thread descent` will name until something is put there.\n');
550
+ }
551
+ }
552
+ const nowAt = goesTo === '' ? wasAt : goesTo;
553
+ const mine = listBounds(next, inList);
554
+ const source = mine === undefined ? undefined : rowsIn(next, mine).find((one) => one.id === held.id)?.source;
555
+ if (kid === undefined && nowAt !== '' && source !== undefined) {
556
+ const sheet = join(TREE.at, nowAt);
557
+ if (existsSync(sheet)) {
558
+ const there = readFileSync(sheet, 'utf8');
559
+ const settled = reconciled(there, `const HEAD = ${source.trim().replace(/,$/, '')}\n`, STATED);
560
+ if (settled !== there)
561
+ writeFileSync(sheet, settled, 'utf8');
562
+ }
563
+ }
564
+ stdout.write(`create: ${String(held.id)} now says ${answers.map((one) => `\`${one.key}\``).join(', ')} in ${shown}.\n`);
565
+ exit(0);
566
+ }
567
+ if (words[0] === 'add') {
568
+ const which = words[1];
569
+ const one = existing.find((each) => mark(each) === which || each.id === which);
570
+ if (one === undefined) {
571
+ refuse(`\`${which ?? '(nothing)'}\` is not something ${at} declares. Give the id, or the mark printed in brackets ` +
572
+ 'beside it above.');
573
+ }
574
+ if (one.at === undefined) {
575
+ refuse(`\`${one.id}\` is a value ${at} holds, so it has no sheet of its own for a child to go in. ` +
576
+ `\`thread create ${NOUN} new\` is how one more is declared.`);
577
+ }
578
+ const sheet = join(TREE.at, one.at);
579
+ if (!existsSync(sheet)) {
580
+ refuse(`\`${one.id}\` names ${one.at} and nothing is there, so there is no list to declare a child in.`);
581
+ }
582
+ if (listBounds(readFileSync(sheet, 'utf8'), LIST) === undefined) {
583
+ refuse(`${one.at} states no \`${LIST}\` list, so there is nowhere in it for a child to go.`);
584
+ }
585
+ const fields = listFields(sheet, LIST);
586
+ if (fields.length === 0) {
587
+ refuse(`${one.at} states a \`${LIST}\` this cannot read the shape of, so what a row there must say is unknown. ` +
588
+ 'Write it as a list with a declared row type — the fields `add` asks for are read off that type, and ' +
589
+ 'guessing them is how a row gets written that the tree then refuses.');
590
+ }
591
+ if (!fields.some((each) => each.name === 'at')) {
592
+ refuse(`${one.at} holds values rather than files, so there is nothing on disk to point a row at. ` +
593
+ `\`thread create ${NOUN} new\` is how one is declared.`);
594
+ }
595
+ const flaggedAt = argv.indexOf(RECEIPT);
596
+ if (flaggedAt === -1) {
597
+ const nonce = issue(TREE.at, listing);
598
+ stdout.write([
599
+ '',
600
+ `\`${one.id}\` — ${one.describes}`,
601
+ ` its sheet is ${one.at}, and a row put there names a file that is ALREADY on disk.`,
602
+ '',
603
+ 'what `add` takes, one answer per line, in any order:',
604
+ '',
605
+ ...asks(fields),
606
+ '',
607
+ ` thread create ${NOUN} add ${which ?? one.id} ${RECEIPT} ${nonce}`,
608
+ '',
609
+ ' it writes no file. It points this node at one somebody already wrote, and from then on',
610
+ ' that file is held to everything this tree refuses.',
611
+ '',
612
+ ].join('\n'));
613
+ stdout.write(`create: receipt ${nonce} — spent on use, and stale the moment ${at} changes.\n`);
614
+ exit(0);
615
+ }
616
+ spend(TREE.at, listing, argv[flaggedAt + 1], at);
617
+ const answers = pipedIn();
618
+ const { piped: fed, ask: asked, shut: close } = asking(answers);
619
+ if (fed)
620
+ only(answers, mustSay(fields), '`add`');
621
+ const said = fed ? needed(answers, 'at', '`add`') : await asked('at', 'at — the path of a file that already exists: ');
622
+ const target = join(TREE.at, said);
623
+ const childAt = within(TREE.at, target);
624
+ if (!existsSync(target)) {
625
+ close();
626
+ refuse(`nothing is at ${childAt}. \`add\` points this node at a file that already exists — it writes none.`);
627
+ }
628
+ if (statSync(target).isDirectory()) {
629
+ close();
630
+ refuse(`${childAt} is a directory. Name the file — a directory threads nothing beneath it, so everything under it ` +
631
+ 'would still read as declared by nobody while the row itself looked answered.');
632
+ }
633
+ const childId = kebab(fed ? needed(answers, 'id', '`add`') : await asked('id', 'id — the word this row coins: '));
634
+ if (!KEBAB.test(childId)) {
635
+ close();
636
+ refuse(`\`${childId}\` cannot be an id — it is kebab-case, e.g. glossary-generator.`);
637
+ }
638
+ const saying = fed
639
+ ? needed(answers, 'describes', '`add`')
640
+ : await asked('describes', 'describes — one sentence saying what this file is: ');
641
+ if (saying === '') {
642
+ close();
643
+ refuse(`${childAt} was given no meaning, and a row without one is a path nobody can read.`);
644
+ }
645
+ close();
646
+ const now = readFileSync(sheet, 'utf8');
647
+ const place = listBounds(now, LIST);
648
+ if (place === undefined)
649
+ refuse(`${one.at} states no \`${LIST}\` list.`);
650
+ if (holdsRow(now, place, childId))
651
+ refuse(`\`${childId}\` is already declared in ${one.at}. One id, one row.`);
652
+ const row = [
653
+ ' {',
654
+ ` id: ${quoted(childId)},`,
655
+ ` at: ${quoted(childAt)},`,
656
+ ` describes: ${quoted(saying)},`,
657
+ ' version: 1,',
658
+ ' },',
659
+ ].join('\n');
660
+ writeFileSync(sheet, withRow(now, place, row), 'utf8');
661
+ stdout.write(`create: ${one.id} now declares ${childAt}.\n`);
662
+ exit(0);
663
+ }
664
+ stdout.write(`${listing}\n`);
665
+ const flagged = argv.indexOf(RECEIPT);
666
+ if (flagged === -1) {
667
+ const nonce = issue(TREE.at, listing);
668
+ stdout.write([
669
+ '',
670
+ ...VERBS,
671
+ '',
672
+ 'what `new` takes, one answer per line, in any order:',
673
+ '',
674
+ ...guidance(ASKS),
675
+ ...(FILES === undefined
676
+ ? []
677
+ : [
678
+ ` ${'at:'.padEnd(WIDTH)}${FILES.replace('*', '<name>')} — where it lives is never typed`,
679
+ ` ${`${asFlag(BODY)}:`.padEnd(WIDTH)}what goes in that file, given on the command. Leave it out and the settle writes the starter`,
680
+ ]),
681
+ '',
682
+ ` thread create ${NOUN} new <name> ${RECEIPT} ${nonce}`,
683
+ ` thread create ${NOUN} read <mark> no receipt, writes nothing`,
684
+ ...(ADDRESSED
685
+ ? [
686
+ ` thread create ${NOUN} add <mark> ${RECEIPT} ${nonce}`,
687
+ ' points a node at a file that already exists — it writes none',
688
+ ]
689
+ : []),
690
+ ` thread create ${NOUN} remove <id> ${RECEIPT} ${nonce}`,
691
+ ' only a node governing nothing — it refuses one with anything under it',
692
+ ` thread create ${NOUN} edit <row> ${RECEIPT} ${nonce}`,
693
+ ' changes what one field of an existing row says. One command, so a refusal leaves',
694
+ ' the row exactly as it stands',
695
+ '',
696
+ `${RECEIPT} is spent by one run, and is refused if ${at} changed after it was issued.`,
697
+ '',
698
+ ].join('\n'));
699
+ stdout.write(`create: receipt ${nonce} — spent on use, and stale the moment ${at} changes.\n`);
700
+ exit(0);
701
+ }
702
+ spend(TREE.at, listing, argv[flagged + 1], at);
703
+ const all = gathered(pipedIn(), FIELDS);
704
+ const { piped, ask, line, shut } = asking(all);
705
+ const own = ownAnswers(all);
706
+ const TAKES = FILES === undefined ? answerNames(ASKS) : [...answerNames(ASKS), BODY];
707
+ if (piped)
708
+ only(own, TAKES, '`new`');
709
+ const name = kebab(words[1] ?? (piped ? needed(own, 'name', '`new`') : await ask('name', said('name'))));
710
+ if (!KEBAB.test(name)) {
711
+ shut();
712
+ refuse(`\`${name}\` cannot be the id of a governing node — it is kebab-case, e.g. billing.`);
713
+ }
714
+ if (existing.some((one) => one.id === name)) {
715
+ shut();
716
+ refuse(`\`${name}\` is already declared in ${at}. One id, one row.`);
717
+ }
718
+ let taken = new Map();
719
+ try {
720
+ taken = nouns(await generators(TREE), 'create');
721
+ }
722
+ catch {
723
+ taken = new Map();
724
+ }
725
+ if (taken.has(name)) {
726
+ shut();
727
+ refuse(`\`${name}\` is already a noun this tree makes — \`thread create ${name}\` runs ${taken.get(name) ?? 'a command'}. ` +
728
+ 'One word, one meaning: a governing node called that would be reachable only by its mark, and the two would ' +
729
+ 'be told apart by nothing. Choose another name. Nothing was written.');
730
+ }
731
+ const target = join(TREE.dir, `${name}.ts`);
732
+ const sheetAt = within(TREE.at, target);
733
+ if (NOUN === 'meta' && existsSync(target)) {
734
+ shut();
735
+ refuse(`${sheetAt} is already there and no row names it. Choose another name, or delete it first.`);
736
+ }
737
+ const describes = piped
738
+ ? needed(own, 'describes', '`new`')
739
+ : await ask('describes', said('describes'));
740
+ if (describes === '') {
741
+ shut();
742
+ refuse(`\`${name}\` was given no meaning, and a governing node without one is a name nobody can read.`);
743
+ }
744
+ const address = NOUN === 'meta'
745
+ ? sheetAt
746
+ : FILES !== undefined
747
+ ? FILES.replace('*', name)
748
+ : !ADDRESSED
749
+ ? undefined
750
+ : piped
751
+ ? needed(own, 'at', '`new`')
752
+ : await ask('at', said('at'));
753
+ const { stated, kids } = await stating(ASKS, all, piped, ask, line, ['name', 'id', 'at', 'describes', 'version']);
754
+ const body = FILES === undefined || address === undefined
755
+ ? undefined
756
+ : await bodied(`${address} itself — this is ${asFlag(BODY)}`, '', piped, all, line);
757
+ shut();
758
+ if (body !== undefined && address !== undefined && existsSync(join(TREE.at, address))) {
759
+ refuse(`${address} is already on disk, so the body you gave has nowhere to go without being put over somebody's file. ` +
760
+ `Leave the body out and the row names what is there, or run \`${TYPED} read ${address}\` and move it first. Nothing was written.`);
761
+ }
762
+ const row = [
763
+ ' {',
764
+ ` id: ${quoted(name)},`,
765
+ ...(address === undefined ? [] : [` at: ${quoted(address)},`]),
766
+ ` describes: ${quoted(describes)},`,
767
+ ' version: 1,',
768
+ ...rowLines(' ', stated, NOUN === 'meta' ? new Map() : kids),
769
+ ' },',
770
+ ].join('\n');
771
+ const text = readFileSync(join(TREE.at, at), 'utf8');
772
+ const where = listBounds(text, LIST);
773
+ if (where === undefined)
774
+ refuse(`${at} states no \`${LIST}\` list to declare a governing node in.`);
775
+ if (holdsRow(text, where, name))
776
+ refuse(`\`${name}\` is already declared in ${at}.`);
777
+ writeFileSync(join(TREE.at, at), withRow(text, where, row), 'utf8');
778
+ stdout.write(`create: declared ${name} in ${at}.\n`);
779
+ for (const said of seeding(kids, TREE.at))
780
+ stdout.write(`create: ${said}\n`);
781
+ if (FILES !== undefined && address !== undefined) {
782
+ if (body === undefined) {
783
+ stdout.write(`create: ${address} follows — the settle writes its starter, and \`${TYPED} edit ${address} --from <path>\` replaces it.\n`);
784
+ }
785
+ else {
786
+ mkdirSync(dirname(join(TREE.at, address)), { recursive: true });
787
+ writeFileSync(join(TREE.at, address), body.endsWith('\n') ? body : `${body}\n`, 'utf8');
788
+ stdout.write(`create: wrote ${address} with the body you gave it.\n`);
789
+ }
790
+ }
791
+ if (NOUN !== 'meta') {
792
+ stdout.write(`create: \`npx thread read ${at}\` prints what that list says now.\n`);
793
+ exit(0);
794
+ }
795
+ const opens = (readFileSync(join(TREE.at, at), 'utf8').split('\n')[0] ?? '').trim();
796
+ if (!opens.includes('Threaded')) {
797
+ refuse(`${at} does not open by importing \`Threaded\`, so a new sheet cannot be given the same import and would be ` +
798
+ 'written unable to compile. Nothing was scaffolded.');
799
+ }
800
+ const CONST = name.toUpperCase().replace(/-/g, '_');
801
+ const SHAPED = name.split('-').map((one) => `${one.slice(0, 1).toUpperCase()}${one.slice(1)}`).join('');
802
+ writeFileSync(target, [
803
+ opens,
804
+ '',
805
+ `export interface ${SHAPED} extends Threaded {`,
806
+ ` readonly declares?: readonly ${SHAPED}[]`,
807
+ '}',
808
+ '',
809
+ `export const ${CONST}: ${SHAPED} = {`,
810
+ ` id: ${quoted(name)},`,
811
+ ` at: ${quoted(sheetAt)},`,
812
+ ` describes: ${quoted(describes)},`,
813
+ ' version: 1,',
814
+ ...(kids.size > 0 ? rowLines(' ', [], kids) : [' declares: [],']),
815
+ '}',
816
+ '',
817
+ ].join('\n'), 'utf8');
818
+ stdout.write(`create: wrote ${sheetAt}, declaring nothing under it yet.\n` +
819
+ `create: every ${name} carries what any row carries — a name, an address and a sentence. To give ${name}s a ` +
820
+ `field of their own, change the shape at the top of ${sheetAt}. What you add lands on this node and on ` +
821
+ `everything declared under it, and \`npx thread create\` starts asking for it.\n`);
822
+ stdout.write(['', ...VERBS, '', ...grows(sheetAt, name), ''].join('\n'));
823
+ exit(0);