@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,1109 @@
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 { createHash, randomBytes } from 'node:crypto';
10
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
11
+ import { tmpdir } from 'node:os';
12
+ import { dirname, join, relative } from 'node:path';
13
+ import { argv, env, exit, stderr, stdin, stdout } from 'node:process';
14
+ import { createInterface } from 'node:readline';
15
+ import { pathToFileURL } from 'node:url';
16
+ import { CONSTRAINTS } from "../integrity/constraint.js";
17
+ import { LISTS, descend, listFields, listShape, shapeFields, shapesNamed } from "./descend.js";
18
+ import { quoted } from "./rows.js";
19
+ const RECEIPTS = join(tmpdir(), 'thread-create');
20
+ export const RECEIPT = '--receipt';
21
+ export const ENDS = '.';
22
+ export const FROM = '--from';
23
+ export const CHAINED = '--chained';
24
+ export const BODY = 'body';
25
+ export const WRITES = 'writes';
26
+ export const asFlag = (key) => `--${key}`;
27
+ export function refuse(because) {
28
+ stderr.write(`create: ${because}\n`);
29
+ exit(1);
30
+ }
31
+ export function slug(one) {
32
+ return one
33
+ .trim()
34
+ .toLowerCase()
35
+ .replace(/[^a-z0-9]+/g, '_')
36
+ .replace(/^_+|_+$/g, '');
37
+ }
38
+ export function within(root, path) {
39
+ return relative(root, path).split('\\').join('/');
40
+ }
41
+ const NAMED = /^([a-z][A-Za-z0-9_.-]*):[ \t]?(.*)$/;
42
+ export function parsed(text) {
43
+ const lines = text.split('\n');
44
+ const found = [];
45
+ for (let at = 0; at < lines.length; at += 1) {
46
+ const line = lines[at] ?? '';
47
+ if (line.trim() === '' || line.trimStart().startsWith('#'))
48
+ continue;
49
+ const held = NAMED.exec(line);
50
+ if (held === null) {
51
+ refuse(`line ${String(at + 1)} is neither blank nor an answer: ${JSON.stringify(line.slice(0, 60))}. ` +
52
+ 'Every answer is written `name: value`, and a body is written under `body:` and ended by a line ' +
53
+ `holding only ${ENDS}.`);
54
+ }
55
+ const key = held[1] ?? '';
56
+ if (key !== 'body') {
57
+ found.push({ at: at + 1, key, value: (held[2] ?? '').trim() });
58
+ continue;
59
+ }
60
+ const body = [];
61
+ const beside = held[2] ?? '';
62
+ if (beside.trim() !== '')
63
+ body.push(beside);
64
+ let shut = false;
65
+ for (at += 1; at < lines.length; at += 1) {
66
+ if ((lines[at] ?? '').trim() === ENDS) {
67
+ shut = true;
68
+ break;
69
+ }
70
+ body.push(lines[at] ?? '');
71
+ }
72
+ if (!shut) {
73
+ refuse(`the \`body:\` opened on line ${String(found.length === 0 ? 1 : at)} is never closed — a body ends at a ` +
74
+ `line holding only ${ENDS}, and this one runs to the end of the input.`);
75
+ }
76
+ found.push({ at: at + 1, key, value: body.join('\n').trim() });
77
+ }
78
+ return found;
79
+ }
80
+ export function scoped(given, opens) {
81
+ const own = [];
82
+ const each = [];
83
+ const lists = new Set(LISTS);
84
+ let mine = false;
85
+ for (const one of given) {
86
+ if (one.key === opens) {
87
+ each.push([one]);
88
+ mine = true;
89
+ continue;
90
+ }
91
+ if (lists.has(one.key)) {
92
+ mine = false;
93
+ continue;
94
+ }
95
+ if (mine)
96
+ (each[each.length - 1] ?? []).push(one);
97
+ else if (each.length === 0)
98
+ own.push(one);
99
+ }
100
+ return { own, each };
101
+ }
102
+ const locate = (one) => one.at > 0 ? `line ${String(one.at)}` : `\`--${one.key}\``;
103
+ let meanings;
104
+ function coined(word) {
105
+ if (meanings === undefined) {
106
+ const found = new Map();
107
+ try {
108
+ for (const [id, held] of descend().coined) {
109
+ const first = held.said[0];
110
+ if (first !== undefined && first !== '')
111
+ found.set(id, first);
112
+ }
113
+ }
114
+ catch {
115
+ found.clear();
116
+ }
117
+ meanings = found;
118
+ }
119
+ return meanings.get(word);
120
+ }
121
+ export function asksOf(noun, shape, said, computed) {
122
+ const fields = typeof shape === 'string' ? shapeFields(shape).map((one) => ({ ...one, boolean: false })) : shape;
123
+ if (fields.length === 0) {
124
+ refuse(`nothing installed here states what a ${noun} is made of, so \`thread create ${noun}\` has no questions to ` +
125
+ `ask. It reads that from the shape a ${noun} is declared with rather than from a list kept beside it. ` +
126
+ 'Run `npx thread attach` to see whether the package that states it is taken on.');
127
+ }
128
+ const spoken = { ...said };
129
+ for (const one of fields) {
130
+ if (spoken[one.name] !== undefined || computed.includes(one.name))
131
+ continue;
132
+ const meaning = coined(one.name) ?? (one.guide === '' ? undefined : one.guide);
133
+ if (meaning === undefined)
134
+ continue;
135
+ spoken[one.name] = { guide: meaning };
136
+ }
137
+ const mute = fields
138
+ .filter((one) => one.required && !computed.includes(one.name) && spoken[one.name] === undefined)
139
+ .map((one) => one.name);
140
+ if (mute.length > 0) {
141
+ const one = mute.length === 1;
142
+ refuse(`every ${noun} states ${mute.join(', ')}, and \`thread create ${noun}\` never asks for ${one ? 'it' : 'them'}. ` +
143
+ `It would write a ${noun} with no ${mute.join(' and no ')}, and tell you it worked. ` +
144
+ `Give ${one ? 'it' : 'each'} a sentence where this command lists what it asks, or name ${one ? 'it' : 'them'} ` +
145
+ 'as something the command fills in.');
146
+ }
147
+ return Object.keys(spoken).map((one) => {
148
+ const told = spoken[one];
149
+ const known = fields.find((each) => each.name === one);
150
+ const spare = told.optional ?? (known !== undefined && !known.required);
151
+ return {
152
+ at: one,
153
+ guide: known?.boolean === true ? `${told.guide} — yes or no` : told.guide,
154
+ ...(told.asks === undefined ? {} : { asks: told.asks }),
155
+ ...(spare ? { optional: true } : {}),
156
+ ...(known?.list === true ? { list: true } : {}),
157
+ ...(known?.map === true ? { map: true } : {}),
158
+ ...(known?.boolean === true ? { boolean: true } : {}),
159
+ ...(known?.worded === true ? { worded: true } : {}),
160
+ };
161
+ });
162
+ }
163
+ export const named = (all) => all.map((one) => one.at);
164
+ export const prompted = (one) => one.asks ?? `${one.at} — ${one.guide}${one.optional === true ? ', or blank' : ''}: `;
165
+ export const mapped = (one, said) => one.map === true && !(one.worded === true && !said.includes('='));
166
+ export const CARRIES = ['cites', 'requires', 'takes', 'by', 'runs'];
167
+ export function ownAnswers(all) {
168
+ const opens = new Set(LISTS);
169
+ const at = all.findIndex((one) => opens.has(one.key));
170
+ return at === -1 ? all : all.slice(0, at);
171
+ }
172
+ export async function stating(asks, all, piped, ask, line, settled) {
173
+ const own = ownAnswers(all);
174
+ const skip = new Set([...settled, ...LISTS]);
175
+ const stated = [];
176
+ for (const one of asks) {
177
+ if (skip.has(one.at))
178
+ continue;
179
+ const given = (piped ? (answer(own, one.at) ?? '') : await ask(one.at, prompted(one))).trim();
180
+ if (given === '')
181
+ continue;
182
+ stated.push({ field: one.at, said: given, list: one.list === true, map: one.map === true, boolean: one.boolean === true, worded: one.worded === true });
183
+ }
184
+ if (piped)
185
+ return { stated, kids: childrenIn(all) };
186
+ const kids = new Map();
187
+ for (const list of LISTS) {
188
+ if (!asks.some((one) => one.at === list))
189
+ continue;
190
+ const found = await children(list, all, ask, line);
191
+ if (found.length > 0)
192
+ kids.set(list, found);
193
+ }
194
+ return { stated, kids };
195
+ }
196
+ export function arrayValue(pad, key, said, found) {
197
+ if (found === undefined || found.length === 0)
198
+ return asList(said);
199
+ const lines = rowLines(pad, [], new Map([[key, found]]));
200
+ return ['[', ...lines.slice(1, -1), `${pad}]`].join('\n');
201
+ }
202
+ const BARE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
203
+ const NUMBER = /^-?\d+(?:\.\d+)?$/;
204
+ const nested = (held) => {
205
+ const pairs = Object.entries(held).map(([key, value]) => {
206
+ const name = BARE.test(key) ? key : quoted(key);
207
+ if (typeof value === 'object' && value !== null)
208
+ return `${name}: ${nested(value)}`;
209
+ const said = String(value);
210
+ return `${name}: ${NUMBER.test(said) ? said : quoted(said)}`;
211
+ });
212
+ return pairs.length === 0 ? '{}' : `{ ${pairs.join(', ')} }`;
213
+ };
214
+ export const asMap = (said) => {
215
+ const held = {};
216
+ for (const one of said.split(',').map((each) => each.trim()).filter((each) => each !== '')) {
217
+ const at = one.indexOf('=');
218
+ if (at < 1) {
219
+ refuse(`\`${one}\` is one entry of a name-and-value field, and it states no value. Write each one as ` +
220
+ '`name=value`, separated by commas — `core=*` says that package at any version, and a dot in the name ' +
221
+ 'nests it: `dark.background=colour-surface`. Nothing was written.');
222
+ }
223
+ const path = one.slice(0, at).trim().split('.');
224
+ const last = path[path.length - 1] ?? '';
225
+ let into = held;
226
+ for (const step of path.slice(0, -1)) {
227
+ const next = into[step];
228
+ if (next !== undefined && (typeof next !== 'object' || next === null)) {
229
+ refuse(`\`${one}\` nests under \`${step}\`, and \`${step}\` is already given a value of its own. One or the other. Nothing was written.`);
230
+ }
231
+ if (next === undefined)
232
+ into[step] = {};
233
+ into = into[step];
234
+ }
235
+ if (typeof into[last] === 'object' && into[last] !== null) {
236
+ refuse(`\`${one}\` gives \`${last}\` a value, and entries already nest under it. One or the other. Nothing was written.`);
237
+ }
238
+ into[last] = one.slice(at + 1).trim();
239
+ }
240
+ return nested(held);
241
+ };
242
+ export function gathered(all, fields) {
243
+ const maps = new Set(fields.filter((one) => one.map).map((one) => one.name));
244
+ const entries = new Map();
245
+ for (const one of all) {
246
+ const dot = one.key.indexOf('.');
247
+ if (dot === -1 || !maps.has(one.key.slice(0, dot)))
248
+ continue;
249
+ const head = one.key.slice(0, dot);
250
+ const held = entries.get(head) ?? { at: one.at, parts: [] };
251
+ held.parts.push(`${one.key.slice(dot + 1)}=${one.value}`);
252
+ entries.set(head, held);
253
+ }
254
+ const written = new Set();
255
+ const out = [];
256
+ for (const one of all) {
257
+ const dot = one.key.indexOf('.');
258
+ const head = dot === -1 ? one.key : one.key.slice(0, dot);
259
+ if (dot === -1 || !entries.has(head)) {
260
+ out.push(one);
261
+ continue;
262
+ }
263
+ if (written.has(head))
264
+ continue;
265
+ written.add(head);
266
+ const held = entries.get(head);
267
+ out.push({ at: held?.at ?? one.at, key: head, value: (held?.parts ?? []).join(', ') });
268
+ }
269
+ return out;
270
+ }
271
+ const asList = (said) => `[${said
272
+ .split(',')
273
+ .map((one) => one.trim())
274
+ .filter((one) => one !== '')
275
+ .map(quoted)
276
+ .join(', ')}]`;
277
+ export const asBoolean = (said) => {
278
+ const word = said.trim().toLowerCase();
279
+ if (word === 'yes' || word === 'y' || word === 'true')
280
+ return 'true';
281
+ if (word === 'no' || word === 'n' || word === 'false')
282
+ return 'false';
283
+ refuse(`\`${said}\` is not an answer to a yes-or-no field. Say yes or no. Nothing was written.`);
284
+ };
285
+ export function rowLines(pad, stated, kids) {
286
+ const kid = (one) => [
287
+ `${pad} {`,
288
+ `${pad} id: ${quoted(one.id)},`,
289
+ `${pad} at: ${quoted(one.at)},`,
290
+ `${pad} describes: ${quoted(one.describes)},`,
291
+ ...(one.by === undefined || one.by === '' ? [] : [`${pad} by: ${quoted(one.by)},`]),
292
+ ...(one.runs === undefined || one.runs === '' ? [] : [`${pad} runs: ${quoted(one.runs)},`]),
293
+ `${pad} version: 1,`,
294
+ `${pad} },`,
295
+ ];
296
+ return [
297
+ ...stated.map((one) => `${pad}${one.field}: ${mapped(one, one.said) ? asMap(one.said) : one.list ? asList(one.said) : one.boolean === true ? asBoolean(one.said) : quoted(one.said)},`),
298
+ ...[...kids].flatMap(([list, found]) => [`${pad}${list}: [`, ...found.flatMap(kid), `${pad}],`]),
299
+ ];
300
+ }
301
+ export function withChildren(row, lines) {
302
+ if (lines.length === 0)
303
+ return row;
304
+ const all = row.split('\n');
305
+ let at = all.length - 1;
306
+ while (at >= 0 && (all[at] ?? '').trim() !== '},' && (all[at] ?? '').trim() !== '}')
307
+ at -= 1;
308
+ if (at < 0) {
309
+ refuse('the row just written has no closing brace on a line of its own, so what it declares has nowhere to go. ' +
310
+ 'Nothing more was written, because adding the row and dropping its children quietly is the one outcome ' +
311
+ 'nobody could see.');
312
+ }
313
+ return [...all.slice(0, at), ...lines, ...all.slice(at)].join('\n');
314
+ }
315
+ export function childrenIn(all) {
316
+ const found = new Map();
317
+ for (const list of LISTS) {
318
+ const each = [];
319
+ for (const group of scoped(all, list).each) {
320
+ const id = (answer(group, list) ?? '').trim();
321
+ if (id === '')
322
+ continue;
323
+ const at = (answer(group, 'at') ?? '').trim();
324
+ const describes = (answer(group, 'describes') ?? '').trim();
325
+ if (at === '' || describes === '') {
326
+ refuse(`\`${list} ${id}\` names no ${at === '' ? 'file' : 'meaning'}. Every row says which file it is about and ` +
327
+ 'what that file is, and one missing either is a row the next person cannot act on. Nothing more was written.');
328
+ }
329
+ const body = (answer(group, BODY) ?? '').trim();
330
+ const writes = (answer(group, WRITES) ?? '').trim();
331
+ each.push({
332
+ id,
333
+ at,
334
+ describes,
335
+ ...(writes === '' ? {} : { writes }),
336
+ ...(list === 'declares'
337
+ ? {}
338
+ : { by: (answer(group, 'by') ?? '').trim(), runs: (answer(group, 'runs') ?? '').trim() }),
339
+ ...(body === '' ? {} : { body }),
340
+ });
341
+ }
342
+ if (each.length > 0)
343
+ found.set(list, each);
344
+ }
345
+ return found;
346
+ }
347
+ export const BODIED = ['declares', 'seeds'];
348
+ export const WRITTEN = ['derives', 'seeds'];
349
+ export function seeding(kids, root) {
350
+ const said = [];
351
+ for (const list of BODIED) {
352
+ for (const one of kids.get(list) ?? []) {
353
+ const into = join(root, one.at);
354
+ const there = existsSync(into);
355
+ if (one.body === undefined || one.body === '') {
356
+ said.push(there
357
+ ? `${one.at} was already on disk, and the row now names it.`
358
+ : `${one.at} is declared and nothing is in it yet — write it with ` +
359
+ `\`${TYPED} edit ${one.at} --from <path>\`.`);
360
+ continue;
361
+ }
362
+ if (there) {
363
+ said.push(`${one.at} was already on disk, so the body you wrote was NOT put over it. Nothing was lost — ` +
364
+ `\`${TYPED} read ${one.at}\` prints what is there.`);
365
+ continue;
366
+ }
367
+ mkdirSync(dirname(into), { recursive: true });
368
+ writeFileSync(into, one.body.endsWith('\n') ? one.body : `${one.body}\n`, 'utf8');
369
+ said.push(`wrote ${one.at} with the body you gave it.`);
370
+ }
371
+ }
372
+ return said;
373
+ }
374
+ const ASKED = {
375
+ declares: 'name each file a person writes that this one owns',
376
+ derives: 'name each file a command writes from this one, and the command that writes it',
377
+ seeds: 'name each file a command shapes here',
378
+ };
379
+ export async function children(list, all, ask, line) {
380
+ const written = WRITTEN.includes(list);
381
+ const out = [];
382
+ const owed = (id, at, describes) => {
383
+ if (at !== '' && describes !== '')
384
+ return;
385
+ refuse(`\`${list} ${id}\` names no ${at === '' ? 'file' : 'meaning'}. Every row says which file it is about and what ` +
386
+ 'that file is, and one missing either is a row the next person cannot act on. Nothing more was written.');
387
+ };
388
+ const bodies = [
389
+ ...(written ? [`${asFlag(WRITES)} is the generator's own body — the code that writes the template`] : []),
390
+ ...(BODIED.includes(list) ? [`${asFlag(BODY)} is the body of the file itself`] : []),
391
+ ];
392
+ stdout.write(`\n ${list} — ${ASKED[list] ?? ''}. ` +
393
+ (bodies.length === 0
394
+ ? ''
395
+ : `Each one takes ${bodies.length === 1 ? 'one body' : `${String(bodies.length)} bodies, and they are never the same text`}: ` +
396
+ `${bodies.join('; ')}. `) +
397
+ 'A blank id ends the list.\n');
398
+ for (;;) {
399
+ const id = (await ask(list, ` ${list} — id, or blank to stop: `)).trim();
400
+ if (id === '')
401
+ break;
402
+ const at = (await ask('at', ' at — the file this names: ')).trim();
403
+ const describes = (await ask('describes', ' describes — what that file is, to somebody who has not opened it: ')).trim();
404
+ owed(id, at, describes);
405
+ const by = written ? (await ask('by', ' by — the command that writes it: ')).trim() : undefined;
406
+ const runs = written ? (await ask('runs', ' runs — the command that catches it drifting: ')).trim() : undefined;
407
+ const writes = written
408
+ ? await bodied(`the generator behind \`${by ?? 'that command'}\` — this is ${asFlag(WRITES)}`, '', false, all, line)
409
+ : undefined;
410
+ const body = BODIED.includes(list)
411
+ ? await bodied(`${at} itself — this is ${asFlag(BODY)}`, '', false, all, line)
412
+ : undefined;
413
+ out.push({
414
+ id,
415
+ at,
416
+ describes,
417
+ ...(written ? { by, runs } : {}),
418
+ ...(writes === undefined ? {} : { writes }),
419
+ ...(body === undefined ? {} : { body }),
420
+ });
421
+ }
422
+ return out;
423
+ }
424
+ export async function bodied(what, shows, piped, all, line) {
425
+ const from = argv[argv.indexOf(FROM) + 1];
426
+ if (argv.includes(FROM) && from !== undefined && !from.startsWith('-')) {
427
+ try {
428
+ return refuse(`${FROM} seeds the body out of a file, and a file is not a receipt: nothing here checked it and no descent reached it. Give the body with ${asFlag(BODY)} on the command. Nothing was written.`);
429
+ }
430
+ catch {
431
+ refuse(`\`${FROM} ${from}\` names a file nothing could read, so there is no body to seed and nothing was ` +
432
+ `written. Write it first, then run this again — or drop ${FROM} and type the body when it is asked for.`);
433
+ }
434
+ }
435
+ if (piped) {
436
+ const said = answer(all, BODY);
437
+ return said === undefined || said.trim() === '' ? undefined : said;
438
+ }
439
+ if (shows.trim() === '')
440
+ stdout.write(` body — write what goes in ${what}.\n`);
441
+ else {
442
+ stdout.write(` body — what a ${what} is handed looks like this:\n`);
443
+ for (const one of shows.split('\n'))
444
+ stdout.write(` ${one}\n`);
445
+ }
446
+ stdout.write(` write yours, then a line holding only ${ENDS}. A first line of ${ENDS} leaves it unwritten, and\n` +
447
+ ` \`${TYPED} trials\` reports it unproven until somebody writes it.\n` +
448
+ ` Or give ${FROM} <path> instead and the file at that path is what gets seeded, which is the one\n` +
449
+ ' form that needs no terminal.\n');
450
+ const held = [];
451
+ for (;;) {
452
+ const next = await line();
453
+ if (next === undefined || next.trim() === ENDS)
454
+ break;
455
+ held.push(next);
456
+ }
457
+ const written = held.join('\n').trim();
458
+ return written === '' ? undefined : written;
459
+ }
460
+ export function guidance(all) {
461
+ const width = Math.max(...all.map((one) => one.at.length)) + 3;
462
+ const under = ' '.repeat(width + 2);
463
+ return all.flatMap((one) => {
464
+ const lines = wrapped(one.optional === true ? `${one.guide.replace(/\.$/, '')}. Leave it blank and it is not stated` : one.guide, '');
465
+ return [` ${`${one.at}:`.padEnd(width)}${lines[0] ?? ''}`, ...lines.slice(1).map((each) => `${under}${each}`)];
466
+ });
467
+ }
468
+ export function only(given, allowed, where) {
469
+ for (const one of ownAnswers(given)) {
470
+ if (CARRIES.includes(one.key) || one.key === WRITES)
471
+ continue;
472
+ if (allowed.includes(one.key))
473
+ continue;
474
+ refuse(`${locate(one)} answers \`${one.key}\`, and ${where} takes ${allowed.map((each) => `\`${each}\``).join(', ')}. ` +
475
+ 'Nothing was written.');
476
+ }
477
+ }
478
+ export function answer(given, key) {
479
+ const found = given.filter((one) => one.key === key);
480
+ if (found.length > 1) {
481
+ refuse(`\`${key}\` is answered ${String(found.length)} times: ${found.map((one) => locate(one)).join(', ')}. ` +
482
+ 'Nothing was written.');
483
+ }
484
+ const held = found[0]?.value;
485
+ return held === undefined ? undefined : guarded(key, held);
486
+ }
487
+ export function accepted(given) {
488
+ if (given.length === 0)
489
+ return 'nothing was accepted before it';
490
+ return `accepted so far: ${given.map((one) => `${one.key} (${locate(one)})`).join(', ')}`;
491
+ }
492
+ export function needed(given, key, where) {
493
+ const held = answer(given, key);
494
+ if (held === undefined || held === '') {
495
+ refuse(`${where} states no \`${key}\`, and it is not optional. ${accepted(given)}. Nothing was written.`);
496
+ }
497
+ return held;
498
+ }
499
+ export function wrapped(text, pad) {
500
+ const lines = [];
501
+ let held = '';
502
+ for (const word of text.split(/\s+/).filter((one) => one !== '')) {
503
+ if (held !== '' && `${held} ${word}`.length > 92) {
504
+ lines.push(`${pad}${held}`);
505
+ held = word;
506
+ }
507
+ else
508
+ held = held === '' ? word : `${held} ${word}`;
509
+ }
510
+ if (held !== '')
511
+ lines.push(`${pad}${held}`);
512
+ return lines;
513
+ }
514
+ export function marked(parts) {
515
+ return createHash('sha256').update(parts.join('\n')).digest('hex').slice(0, 6);
516
+ }
517
+ const citesOf = (held) => Array.isArray(held)
518
+ ? held.filter((one) => one !== null && typeof one === 'object' && typeof one.on === 'string' && typeof one.said === 'string')
519
+ : [];
520
+ const HELD = ['holds', 'declares'];
521
+ const NAMED_SHAPE = /^[A-Za-z_][A-Za-z0-9_]*$/;
522
+ const rowLike = (one) => one !== null && typeof one === 'object' && typeof one.id === 'string' && typeof one.describes === 'string';
523
+ const kinded = (shape) => shape
524
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
525
+ .toLowerCase();
526
+ const oneOf = (list) => {
527
+ const word = list.toLowerCase().split('_').join('-');
528
+ return word.endsWith('s') ? word.slice(0, -1) : word;
529
+ };
530
+ export async function declared(tree) {
531
+ const found = new Map();
532
+ const walked = new Set();
533
+ const says = new Map();
534
+ const parents = new Map();
535
+ const sheetsBy = new Map();
536
+ const meant = (shape, sheetAt) => {
537
+ if (shape === undefined)
538
+ return '';
539
+ try {
540
+ const named = shapesNamed();
541
+ for (const at of named.get(shape) ?? []) {
542
+ if (at === sheetAt)
543
+ continue;
544
+ let others = 0;
545
+ for (const [, files] of named)
546
+ if (files.includes(at))
547
+ others += 1;
548
+ if (others !== 1)
549
+ continue;
550
+ const told = says.get(at);
551
+ if (told !== undefined && told !== '')
552
+ return told;
553
+ }
554
+ }
555
+ catch {
556
+ return '';
557
+ }
558
+ return '';
559
+ };
560
+ const sheet = (one) => {
561
+ const at = one?.at;
562
+ return typeof at === 'string' && at.startsWith(`${tree.name}/`) && at.endsWith('.ts') ? at : undefined;
563
+ };
564
+ const walk = async (at, top) => {
565
+ if (walked.has(at))
566
+ return;
567
+ walked.add(at);
568
+ let read;
569
+ try {
570
+ read = (await import(__rewriteRelativeImportExtension(pathToFileURL(join(tree.at, at)).href)));
571
+ }
572
+ catch {
573
+ return;
574
+ }
575
+ for (const [name, value] of Object.entries(read)) {
576
+ const node = value;
577
+ const lists = Array.isArray(value)
578
+ ? [{ list: name, rows: value }]
579
+ : HELD.flatMap((list) => {
580
+ const rows = node?.[list];
581
+ return Array.isArray(rows) ? [{ list, rows }] : [];
582
+ });
583
+ const rowed = lists.filter((one) => one.rows.every(rowLike));
584
+ const first = rowed[0];
585
+ if (first === undefined)
586
+ continue;
587
+ if (Array.isArray(value)) {
588
+ const noun = oneOf(name);
589
+ sheetsBy.set(name, new Set([...(sheetsBy.get(name) ?? []), at]));
590
+ if (!found.has(noun)) {
591
+ found.set(noun, {
592
+ at,
593
+ list: name,
594
+ shape: listShape(at, name),
595
+ fields: listFields(at, name),
596
+ describes: '',
597
+ holds: first.rows.length,
598
+ cites: [],
599
+ top,
600
+ });
601
+ }
602
+ }
603
+ else if (at !== within(tree.at, tree.root)) {
604
+ const files = typeof node?.files === 'string' && node.files !== '' ? node.files : undefined;
605
+ const held = files === undefined ? first : (rowed.find((one) => one.list === 'declares') ?? first);
606
+ const kind = listShape(at, held.list);
607
+ const noun = kind === undefined || (kind === 'Threaded' && files === undefined)
608
+ ? undefined
609
+ : kind !== 'Threaded' && NAMED_SHAPE.test(kind)
610
+ ? kinded(kind)
611
+ : typeof node?.id === 'string' && node.id !== ''
612
+ ? node.id
613
+ : undefined;
614
+ if (typeof noun === 'string' && noun !== '' && !found.has(noun)) {
615
+ found.set(noun, {
616
+ at,
617
+ list: held.list,
618
+ shape: kind,
619
+ fields: listFields(at, held.list),
620
+ describes: typeof node?.describes === 'string' ? node.describes : '',
621
+ holds: held.rows.length,
622
+ cites: citesOf(node?.cites),
623
+ top,
624
+ ...(files === undefined ? {} : { files }),
625
+ });
626
+ }
627
+ }
628
+ for (const one of rowed.flatMap((each) => each.rows)) {
629
+ const where = one.at;
630
+ const told = one.describes;
631
+ if (typeof where === 'string' && typeof told === 'string' && !says.has(where))
632
+ says.set(where, told);
633
+ const next = sheet(one);
634
+ if (next === undefined)
635
+ continue;
636
+ const row = one;
637
+ if (!parents.has(next))
638
+ parents.set(next, row);
639
+ await walk(next, top ?? { id: typeof row.id === 'string' ? row.id : '', describes: typeof row.describes === 'string' ? row.describes : '' });
640
+ }
641
+ }
642
+ };
643
+ await walk(within(tree.at, tree.root), undefined);
644
+ const out = new Map();
645
+ for (const [noun, one] of found) {
646
+ const parent = parents.get(one.at);
647
+ const said = typeof parent?.describes === 'string' ? parent.describes : '';
648
+ const owned = (sheetsBy.get(one.list)?.size ?? 0) <= 1;
649
+ out.set(noun, {
650
+ ...one,
651
+ describes: one.describes !== '' ? one.describes : owned && said !== '' ? said : meant(one.shape, one.at),
652
+ cites: one.cites.length > 0 ? one.cites : citesOf(parent?.cites),
653
+ });
654
+ }
655
+ return out;
656
+ }
657
+ export function nouns(all, verb = 'create') {
658
+ const found = new Map();
659
+ const opens = `thread ${verb} `;
660
+ for (const one of all) {
661
+ const by = one.by;
662
+ if (typeof by !== 'string' || !by.startsWith(opens))
663
+ continue;
664
+ const word = by.slice(opens.length).trim();
665
+ if (word === '' || typeof one.at !== 'string')
666
+ continue;
667
+ const already = found.get(word);
668
+ if (already !== undefined && already !== one.at) {
669
+ refuse(`two rows both state \`by: 'thread ${verb} ${word}'\` — ${already} and ${one.at} — so which one answers a ` +
670
+ `${word} is whichever was written first. One noun, one file.`);
671
+ }
672
+ found.set(word, one.at);
673
+ }
674
+ return found;
675
+ }
676
+ const spends = (root, nonce) => join(RECEIPTS, `${createHash('sha256').update(root).digest('hex').slice(0, 16)}-${nonce}.txt`);
677
+ const stamp = (listing) => createHash('sha256').update(listing).digest('hex').slice(0, 12);
678
+ function beingMade() {
679
+ const words = argv.slice(2).filter((one) => !one.startsWith('-'));
680
+ if (words[0] === 'create' && typeof words[1] === 'string' && words[1] !== '')
681
+ return words[1];
682
+ const file = (argv[1] ?? '').split('\\').join('/').split('/').at(-1) ?? '';
683
+ return file.replace(/\.[^.]+$/, '');
684
+ }
685
+ export const SKILLS_AT = 'thread/knowledge';
686
+ export function skillOf(noun) {
687
+ return `${SKILLS_AT}/${noun}.md`;
688
+ }
689
+ function readTheSkill() {
690
+ const noun = env.THREAD_NOUN ?? beingMade();
691
+ if (noun === '')
692
+ return [];
693
+ return [
694
+ '',
695
+ `READ ${skillOf(noun)} BEFORE YOU WRITE ANYTHING. It is this command's own output, kept as a`,
696
+ 'file, and it lists everything that already exists. Building a second copy of something the tree',
697
+ 'already has is the most expensive mistake available here and it has already been made: thirteen',
698
+ 'elements were declared in one app that every one of them already shipped.',
699
+ '',
700
+ 'If that file is not there, run `npx thread skills` and it will be.',
701
+ ];
702
+ }
703
+ export function issue(root, listing) {
704
+ const nonce = randomBytes(6).toString('hex');
705
+ mkdirSync(RECEIPTS, { recursive: true });
706
+ writeFileSync(spends(root, nonce), `${stamp(listing)}\n`, 'utf8');
707
+ const said = readTheSkill();
708
+ if (said.length > 0)
709
+ stdout.write(`${said.join('\n')}\n`);
710
+ return nonce;
711
+ }
712
+ export function spend(root, listing, offered, sheet) {
713
+ if (offered === undefined || offered.startsWith('-')) {
714
+ refuse(`\`${RECEIPT}\` was handed nothing to check. Run \`thread create\` with no arguments — it lists what is ` +
715
+ 'here and issues a receipt to hand back.');
716
+ }
717
+ const path = spends(root, offered);
718
+ if (!existsSync(path)) {
719
+ refuse(`\`${offered}\` is not a receipt this tree issued, or it has been spent already — one run per receipt. ` +
720
+ 'Run `thread create` again and use the one it prints.');
721
+ }
722
+ let when = '';
723
+ try {
724
+ when = readFileSync(path, 'utf8').trim();
725
+ }
726
+ catch {
727
+ when = '';
728
+ }
729
+ process.on('exit', (code) => {
730
+ if (code === 0)
731
+ rmSync(path, { force: true });
732
+ });
733
+ if (when !== stamp(listing)) {
734
+ refuse(`${sheet} has changed since that receipt was issued, so what you read is not what is here. ` +
735
+ 'Run `thread create` again.');
736
+ }
737
+ }
738
+ export function asking(held) {
739
+ const piped = answered();
740
+ const rl = piped ? undefined : createInterface({ input: stdin, output: stdout });
741
+ const lines = rl?.[Symbol.asyncIterator]();
742
+ return {
743
+ piped,
744
+ ask: async (key, question, from = held) => {
745
+ const already = answer(from, key);
746
+ if (already !== undefined)
747
+ return guarded(key, already);
748
+ if (piped)
749
+ return '';
750
+ stdout.write(question);
751
+ const next = await lines?.next();
752
+ return next === undefined || next.done === true ? '' : guarded(key, next.value.trim());
753
+ },
754
+ line: async () => {
755
+ const next = await lines?.next();
756
+ return next === undefined || next.done === true ? undefined : next.value;
757
+ },
758
+ shut: () => rl?.close(),
759
+ };
760
+ }
761
+ const ANSWERS = '--answers';
762
+ function fromFile() {
763
+ const at = argv.indexOf(ANSWERS);
764
+ if (at === -1)
765
+ return undefined;
766
+ const path = argv[at + 1];
767
+ if (path === undefined || path.startsWith('-')) {
768
+ refuse(`${ANSWERS} needs the path of a file holding the answers, one \`name: value\` to a line.`);
769
+ }
770
+ try {
771
+ return readFileSync(path, 'utf8');
772
+ }
773
+ catch {
774
+ refuse(`${ANSWERS} names ${path}, and there is no file there to read the answers from.`);
775
+ }
776
+ }
777
+ const OWN = '--own';
778
+ const CHECK = '--check';
779
+ function flagged() {
780
+ const found = [];
781
+ for (let at = 2; at < argv.length; at += 1) {
782
+ const one = argv[at] ?? '';
783
+ if (!one.startsWith('--') ||
784
+ one === RECEIPT ||
785
+ one === ANSWERS ||
786
+ one === OWN ||
787
+ one === CHECK ||
788
+ one === FROM ||
789
+ one === CHAINED)
790
+ continue;
791
+ const value = argv[at + 1];
792
+ if (value === undefined || value.startsWith('-')) {
793
+ refuse(`\`${one}\` was given no value — an answer given as a flag is \`${one} value\`, with the value right after it.`);
794
+ }
795
+ found.push({ at: 0, key: one.slice(2), value });
796
+ at += 1;
797
+ }
798
+ return found;
799
+ }
800
+ export function answered() {
801
+ return argv.includes(ANSWERS) || stdin.isTTY !== true;
802
+ }
803
+ export function given() {
804
+ const flags = flagged();
805
+ const fromFlag = new Set(flags.map((one) => one.key));
806
+ const held = fromFile();
807
+ if (held !== undefined)
808
+ return read([...flags, ...parsed(held).filter((one) => !fromFlag.has(one.key))]);
809
+ const text = handed();
810
+ if (text === undefined)
811
+ return read(flags);
812
+ return read([...flags, ...parsed(text).filter((one) => !fromFlag.has(one.key))]);
813
+ }
814
+ const FROM_END = '-from';
815
+ export const FILED = (key) => `${key}${FROM_END}`;
816
+ function read(all) {
817
+ return all.map((one) => {
818
+ if (!one.key.endsWith(FROM_END))
819
+ return one;
820
+ const key = one.key.slice(0, -FROM_END.length);
821
+ if (key === '')
822
+ return one;
823
+ if (all.some((each) => each.key === key)) {
824
+ refuse(`\`${asFlag(key)}\` and \`${asFlag(one.key)}\` both answer ${key}, and they do not have to agree. ` +
825
+ 'Give one of them. Nothing was written.');
826
+ }
827
+ try {
828
+ return refuse(`${asFlag(key)}${FROM_END} takes what this writes out of a file, and a file is not a receipt: nothing here checked it and no descent reached it. Give the text on the command with ${asFlag(key)}. Nothing was written.`);
829
+ }
830
+ catch {
831
+ refuse(`\`${asFlag(one.key)} ${one.value}\` names a file nothing could read, so there is no ${key} to write and ` +
832
+ `nothing was written. Write that file first, or give the text itself with ${asFlag(key)}.`);
833
+ }
834
+ });
835
+ }
836
+ let stood;
837
+ let taken = false;
838
+ export function handed() {
839
+ if (taken)
840
+ return stood;
841
+ taken = true;
842
+ if (stdin.isTTY === true)
843
+ return undefined;
844
+ try {
845
+ stood = readFileSync(0, 'utf8');
846
+ }
847
+ catch {
848
+ stood = undefined;
849
+ }
850
+ return stood;
851
+ }
852
+ export const TYPED = 'npx thread';
853
+ const folded = (said, pad, room = 92) => {
854
+ const out = [];
855
+ let line = '';
856
+ for (const word of said.split(/\s+/)) {
857
+ if (line === '')
858
+ line = word;
859
+ else if (`${line} ${word}`.length <= room)
860
+ line = `${line} ${word}`;
861
+ else {
862
+ out.push(line);
863
+ line = word;
864
+ }
865
+ }
866
+ if (line !== '')
867
+ out.push(line);
868
+ return out.map((one) => `${pad}${one}`);
869
+ };
870
+ const parented = (() => {
871
+ const one = CONSTRAINTS.find((each) => each.id === 'twice-parented');
872
+ if (one === undefined) {
873
+ return [
874
+ '',
875
+ 'THIS PAGE CANNOT STATE THE RULE. It reads what may declare a file out of the `twice-parented`',
876
+ 'rule, and no rule by that name ships any more. Whatever governs this now, it is not said here —',
877
+ `\`${TYPED} create rule\` lists every rule that does exist.`,
878
+ ];
879
+ }
880
+ return [
881
+ '',
882
+ 'which one you pick is a rule rather than a preference, and a check refuses the tree over it:',
883
+ '',
884
+ ` ${one.id}`,
885
+ ...folded(`bans ${one.bans}.`, ' '),
886
+ ...folded(one.instead.replace(/^./, (each) => each.toUpperCase()), ' '),
887
+ ];
888
+ })();
889
+ const NEXT = {
890
+ declares: 'It writes the file from that, and nothing else runs. A file already on disk is left alone and the row simply names it.',
891
+ derives: `It writes NO file at \`at:\`, because the command in \`by:\` writes that one — which is why there is no ${asFlag(BODY)} here to give. If this tree has no command answering \`by:\`, that generator is declared first and ${asFlag(WRITES)} is what goes in it.`,
892
+ seeds: `TWO files come out of this and a different hand writes each, which is the whole reason there are two bodies. If this tree has no command answering \`by:\`, that generator is declared here too, so one run leaves the row, the generator and the file.`,
893
+ };
894
+ export const VERBS = [
895
+ 'nothing is in this tree until a row names it, and which of three lists the row sits in says who',
896
+ 'wrote the file. Pick the one that is true and the rest of this framework follows from it:',
897
+ '',
898
+ ' declares a person wrote it. Exactly one row may ever declare an address, and a second is',
899
+ ' refused: a file with two owners has two answers for where it came from and the tree',
900
+ ' can give neither.',
901
+ ' derives a command wrote it. Delete the file, run the command again, and the same bytes come',
902
+ ' back. Many rows may derive one address, because a derivation owns nothing — it states',
903
+ ' `by:`, the command that writes it, and `runs:`, what catches it drifting.',
904
+ ' seeds a command wrote the shape and a person fills in the part no machine can supply. Many',
905
+ ' rows may seed one address as well, and it is proved by shape rather than by bytes,',
906
+ ' because half of what is on disk is your own writing.',
907
+ '',
908
+ `a file in none of them is not in this app at all — \`${TYPED} descent\` names every one it reached`,
909
+ 'and every one it did not, and nothing undeclared is compiled in or shipped.',
910
+ '',
911
+ 'and the list decides what this command does next, not just what the row says. What it does next is',
912
+ 'ASK FOR BODIES, and there are two of them, which are never the same text and never the same file:',
913
+ '',
914
+ ` ${asFlag(BODY).padEnd(10)} the body of the file the row names. A person wrote it.`,
915
+ ` ${asFlag(WRITES).padEnd(10)} the body of the GENERATOR — the code that writes a template into that file.`,
916
+ '',
917
+ 'give each as text on the command. Neither may be taken out of a file, because a file is not a',
918
+ 'receipt: nothing here checked it and no descent reached it. Single-quote a body that holds quotes.',
919
+ '',
920
+ 'so each list takes a different set of them, and giving one where the other belongs is the mistake',
921
+ 'this section exists to stop:',
922
+ '',
923
+ ...LISTS.flatMap((list) => {
924
+ const takes = [
925
+ ...(WRITTEN.includes(list) ? [asFlag(WRITES)] : []),
926
+ ...(BODIED.includes(list) ? [asFlag(BODY)] : []),
927
+ ];
928
+ const said = folded(`takes ${takes.length === 0 ? 'no body at all' : takes.join(' and ')}. ${NEXT[list] ?? ''}`, ' ');
929
+ return said.map((line, at) => (at === 0 ? ` ${list.padEnd(11)}${line.trim()}` : line));
930
+ }),
931
+ ...parented,
932
+ ];
933
+ export const grows = (sheetAt, noun) => {
934
+ const shaped = noun.split('-').map((one) => `${one.slice(0, 1).toUpperCase()}${one.slice(1)}`).join('');
935
+ return [
936
+ `this is also how the CLI grows. A list of rows in ${sheetAt} becomes a command: declare`,
937
+ `\`export const THINGS: readonly Thing[] = []\` beside what is already there and \`${TYPED} create thing\``,
938
+ 'answers from that moment, asking for whatever a `Thing` states and writing the row into that list.',
939
+ '',
940
+ `The same is true of what you have now: \`${TYPED} create ${noun}\` exists because ${sheetAt} declares`,
941
+ `\`${shaped}\`. Nothing was added to a list of commands anywhere — the CLI reads what your tree says.`,
942
+ ];
943
+ };
944
+ export const VALUED = 'THREAD_VALUED';
945
+ export function answering(noun) {
946
+ return [
947
+ ...(env[VALUED] === '1' ? [] : [...VERBS, '']),
948
+ 'what `new` asks. Give each answer as a flag on one command and close with the receipt:',
949
+ '',
950
+ ` ${TYPED} create ${noun} new --<answer> <value> ... ${RECEIPT} <receipt>`,
951
+ '',
952
+ 'Flags need no terminal, no pipe and no file, so that form works everywhere. The two others',
953
+ 'each need something you may not have: one answer to a line on stdin needs a shell that can',
954
+ 'redirect, and `--answers <path>` needs a way to write the file first. Use them when you have',
955
+ 'them; the line above is the one that never depends on it.',
956
+ '',
957
+ 'Where a question repeats — a list of things, each with answers of its own — there are no',
958
+ 'separate flag names for the inner ones. Give the group\'s own flag again to begin the next,',
959
+ 'then its answers after it, reusing the same words. They are read in the order you write them,',
960
+ 'and everything before the first group flag belongs to the thing itself.',
961
+ '',
962
+ 'the answers, in order:',
963
+ ];
964
+ }
965
+ export const CARRIED = [
966
+ { verb: 'read', typed: `${TYPED} read <row>`, does: 'prints every field a row states' },
967
+ { verb: 'edit', typed: `${TYPED} edit <row> --<field> <value>`, does: 'changes any field it was declared with' },
968
+ { verb: 'remove', typed: `${TYPED} edit <row> --remove`, does: 'takes the row out of the tree' },
969
+ ];
970
+ const elsewhere = CARRIED.map((one) => `\`${one.typed}\` ${one.does}`);
971
+ export const ELSEWHERE = [
972
+ 'reading a row, changing one of its answers and taking it out are done in one place, whatever',
973
+ 'declared it \u2014 name the row by its id or by the mark printed beside it above:',
974
+ '',
975
+ ...CARRIED.map((one) => ` ${one.typed.padEnd(40)}${one.does}`),
976
+ ];
977
+ const opening = (said) => said.split(/(?<=\.)\s/)[0] ?? said;
978
+ const article = (word) => (/^[aeiou]/.test(word) ? 'an' : 'a');
979
+ export function taught(noun, told, commands) {
980
+ const one = told.get(noun);
981
+ const opens = [
982
+ [`${TYPED} read <id>`, 'the row, and the file it names'],
983
+ [`${TYPED} create ${noun}`, 'this list again, with a fresh receipt'],
984
+ ...(commands.has('thread inspect')
985
+ ? [[`${TYPED} inspect <word>`, 'every row whose name or sentence carries the word']]
986
+ : []),
987
+ ];
988
+ const top = one?.top;
989
+ const siblings = top === undefined
990
+ ? []
991
+ : [...told]
992
+ .filter(([, each]) => each.top?.id === top.id && each.describes !== '')
993
+ .sort(([left], [right]) => (left < right ? -1 : 1));
994
+ const pad = Math.max(0, ...siblings.map(([word]) => word.length)) + 3;
995
+ return [
996
+ 'DO NOT CREATE WHAT ALREADY EXISTS. Everything listed below is declared and yours to use. A thing',
997
+ 'you cannot find is usually one of these under a word you have not met \u2014 read the sentences, not',
998
+ 'the names.',
999
+ '',
1000
+ 'IF NOTHING FITS, SAY SO AND STOP. Do not coin a name: a name is a meaning, and a meaning is',
1001
+ 'declared rather than invented. Being told the list is short a thing is a useful answer.',
1002
+ '',
1003
+ 'NOW GO AND OPEN ONE. A sentence is a summary, and every expensive mistake here was made from a',
1004
+ 'summary. These print the real thing, and none of them writes anything:',
1005
+ '',
1006
+ ...opens.map(([said, does]) => ` ${said.padEnd(30)}${does}`),
1007
+ '',
1008
+ ...(one === undefined || one.describes === ''
1009
+ ? []
1010
+ : [
1011
+ `what ${article(noun)} ${noun} is:`,
1012
+ '',
1013
+ ...wrapped(one.describes, ' '),
1014
+ ...one.cites.flatMap((said) => ['', ...wrapped(`said on ${said.on}: "${said.said}"`, ' ')]),
1015
+ '',
1016
+ ]),
1017
+ ...(top === undefined || siblings.length < 2
1018
+ ? []
1019
+ : [
1020
+ `${top.id} \u2014 ${opening(top.describes)}`,
1021
+ 'what it makes, and what each one is. Pick the one that is true before declaring anything:',
1022
+ '',
1023
+ ...siblings.flatMap(([word, each]) => {
1024
+ const lines = wrapped(opening(each.describes), '');
1025
+ return [` ${word.padEnd(pad)}${lines[0] ?? ''}`, ...lines.slice(1).map((line) => `${' '.repeat(pad + 2)}${line}`)];
1026
+ }),
1027
+ '',
1028
+ ]),
1029
+ ];
1030
+ }
1031
+ export function reminded(noun) {
1032
+ const lists = noun === undefined || noun === 'meta'
1033
+ ? `\`${TYPED} create\` lists what this tree makes, and any noun run bare lists what exists, each with its sentence`
1034
+ : `\`${TYPED} create ${noun}\` lists what exists, each with its sentence`;
1035
+ return `thread: before the next thing, ${lists}. Do not create what already exists, and if nothing fits, say so and stop.`;
1036
+ }
1037
+ export function wordsIn(offered) {
1038
+ const values = new Set([argv[argv.indexOf(RECEIPT) + 1], argv[argv.indexOf(ANSWERS) + 1], ...flagged().map((one) => one.value)]);
1039
+ const words = argv.slice(2).filter((one) => !one.startsWith('-') && !values.has(one));
1040
+ const first = words[0];
1041
+ if (offered.length === 0 || first === undefined || offered.includes(first))
1042
+ return words;
1043
+ const carried = CARRIED.find((one) => one.verb === first);
1044
+ if (carried !== undefined) {
1045
+ refuse(`\`${first}\` is not a word this command answers, because every row in this tree has it and it is ` +
1046
+ `answered in one place rather than once per thing. \`${carried.typed}\` ${carried.does}, whatever ` +
1047
+ 'declared it, and it finds the row by its id or by the mark printed beside it. Nothing was written, ' +
1048
+ 'and nothing was read.');
1049
+ }
1050
+ refuse(`\`${first}\` is not one of the things this command does. It does: ${offered.join(', ')}. ` +
1051
+ `Reading, changing and removing a row are done elsewhere, because they reach every row rather than ` +
1052
+ `only this one's: ${elsewhere.join('; ')}. ` +
1053
+ 'Run it with none of those to see what already exists and exactly what each of them asks for. ' +
1054
+ 'Nothing was written, and nothing was read.');
1055
+ }
1056
+ const ANNOUNCED = [
1057
+ { what: 'a Stripe secret key', pattern: /\bsk_(?:live|test)_[A-Za-z0-9]{16,}/ },
1058
+ { what: 'a Stripe restricted key', pattern: /\brk_(?:live|test)_[A-Za-z0-9]{16,}/ },
1059
+ { what: 'a GitHub token', pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{30,}/ },
1060
+ { what: 'a GitHub personal access token', pattern: /\bgithub_pat_[A-Za-z0-9_]{30,}/ },
1061
+ { what: 'an AWS access key id', pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/ },
1062
+ { what: 'a Slack token', pattern: /\bxox[abposr]-[A-Za-z0-9-]{10,}/ },
1063
+ { what: 'an OpenAI key', pattern: /\bsk-[A-Za-z0-9_-]{32,}/ },
1064
+ { what: 'an Anthropic key', pattern: /\bsk-ant-[A-Za-z0-9_-]{20,}/ },
1065
+ { what: 'a Google API key', pattern: /\bAIza[A-Za-z0-9_-]{35}\b/ },
1066
+ { what: 'a JSON web token', pattern: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
1067
+ { what: 'a private key', pattern: /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/ },
1068
+ { what: 'a password inside a connection string', pattern: /\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:[^\s:/@]{6,}@/ },
1069
+ ];
1070
+ const ASSIGNED = /\b(?:secret|password|passwd|api_?key|apikey|access_?token|auth_?token|private_?key|client_?secret|credential)s?\b\s*[:=]\s*(['"`])([^'"`\n]{16,})\1/gi;
1071
+ const MIXED = /^(?=.*[a-z])(?=.*[A-Z0-9])[A-Za-z0-9_+/=.-]{16,}$/;
1072
+ const NOT = [
1073
+ /^(?:sec|pub)_[a-z0-9_]+$/,
1074
+ /^\$\{/,
1075
+ /^(?:x{3,}|X{3,}|\.{3,}|<[^>]+>|change[-_]?me|your[-_]|example|placeholder|redacted|todo|test|dummy|sample|fake|dGhpcyBpcyBub3QgYSBzZWNyZXQ)/i,
1076
+ ];
1077
+ function guarded(key, value) {
1078
+ const carried = credentialIn(value);
1079
+ if (carried !== undefined) {
1080
+ refuse(`\`${key}\` holds ${carried}. A credential is never typed into a declaration — not in a describes, not ` +
1081
+ 'anywhere but a secret. Declare it with `thread create secret`, name it where the thing that needs it ' +
1082
+ 'states what it requires, and it is resolved through the vault at the moment it is used and never ' +
1083
+ 'written down. Nothing was written.');
1084
+ }
1085
+ const address = ADDRESS.exec(value);
1086
+ if (address !== null) {
1087
+ refuse(`\`${key}\` holds the address \`${address[0]}\`. An address is a secret here, not a sentence — the same ` +
1088
+ 'thing behind another host is another environment, and one written into a declaration is one nothing ' +
1089
+ 'can point elsewhere. Declare it with `thread create secret` and name it where the thing that needs it ' +
1090
+ 'states what it requires. Nothing was written.');
1091
+ }
1092
+ return value;
1093
+ }
1094
+ const ADDRESS = /\b[a-z][a-z0-9+.-]*:\/\/[^\s'"`)]+/i;
1095
+ export function credentialIn(text) {
1096
+ for (const one of ANNOUNCED) {
1097
+ if (one.pattern.test(text))
1098
+ return one.what;
1099
+ }
1100
+ for (const found of text.matchAll(ASSIGNED)) {
1101
+ const value = found[2] ?? '';
1102
+ if (NOT.some((each) => each.test(value)))
1103
+ continue;
1104
+ if (!MIXED.test(value))
1105
+ continue;
1106
+ return `a ${(found[0].split(/[:=]/)[0] ?? '').trim()} written down as a literal ${String(value.length)} characters long`;
1107
+ }
1108
+ return undefined;
1109
+ }