@prestack/thread 0.1.3 → 0.1.5

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 (270) hide show
  1. package/CHANGELOG.md +563 -0
  2. package/dist/core/actor.d.ts +1 -1
  3. package/dist/core/actor.js +5 -4
  4. package/dist/core/actor.ts +5 -4
  5. package/dist/core/attach.d.ts +0 -7
  6. package/dist/core/attach.js +38 -67
  7. package/dist/core/attach.ts +34 -80
  8. package/dist/core/chain.js +13 -17
  9. package/dist/core/chain.ts +11 -15
  10. package/dist/core/condition.d.ts +4 -4
  11. package/dist/core/condition.js +2 -2
  12. package/dist/core/descend.d.ts +4 -2
  13. package/dist/core/descend.js +91 -23
  14. package/dist/core/descend.ts +90 -21
  15. package/dist/core/errors/catalogue.d.ts +52 -0
  16. package/dist/{operations/errors/errors.js → core/errors/catalogue.js} +1 -1
  17. package/dist/{operations/errors/errors.ts → core/errors/catalogue.ts} +1 -1
  18. package/dist/{operations/errors/catalog.d.ts → core/errors/error.d.ts} +2 -3
  19. package/dist/{operations/errors/catalog.js → core/errors/error.js} +1 -16
  20. package/dist/{operations/errors/catalog.ts → core/errors/error.ts} +3 -19
  21. package/dist/core/errors/namespaces.d.ts +3 -0
  22. package/dist/core/errors/namespaces.js +15 -0
  23. package/dist/core/errors/namespaces.ts +19 -0
  24. package/dist/core/finding.js +2 -2
  25. package/dist/core/finding.ts +1 -2
  26. package/dist/core/init.d.ts +1 -1
  27. package/dist/core/init.js +51 -5
  28. package/dist/core/init.ts +58 -5
  29. package/dist/core/invoked.d.ts +1 -1
  30. package/dist/core/invoked.js +3 -1
  31. package/dist/core/invoked.ts +5 -2
  32. package/dist/core/round.js +36 -22
  33. package/dist/core/round.ts +26 -9
  34. package/dist/core/rows.d.ts +4 -0
  35. package/dist/core/rows.js +31 -2
  36. package/dist/core/rows.ts +33 -1
  37. package/dist/core/scope.d.ts +1 -0
  38. package/dist/core/scope.js +1 -0
  39. package/dist/core/scope.ts +2 -0
  40. package/dist/core/thread.d.ts +3 -3
  41. package/dist/core/tree.d.ts +4 -1
  42. package/dist/core/tree.js +26 -6
  43. package/dist/core/tree.ts +29 -6
  44. package/dist/core/types.d.ts +3 -3
  45. package/dist/core/types.js +3 -1
  46. package/dist/core/types.ts +2 -1
  47. package/dist/derivation/attach.js +3 -2
  48. package/dist/derivation/attach.ts +2 -1
  49. package/dist/derivation/generators/build.js +6 -6
  50. package/dist/derivation/generators/build.ts +1 -1
  51. package/dist/derivation/generators/check.js +1 -1
  52. package/dist/derivation/generators/determinism.js +1 -1
  53. package/dist/derivation/generators/release.js +60 -47
  54. package/dist/derivation/generators/release.ts +66 -43
  55. package/dist/derivation/release.d.ts +7 -3
  56. package/dist/derivation/release.js +14 -1
  57. package/dist/derivation/release.ts +22 -2
  58. package/dist/integrations/attach.d.ts +4 -0
  59. package/dist/integrations/attach.js +37 -56
  60. package/dist/integrations/attach.ts +54 -60
  61. package/dist/integrations/cli.d.ts +37 -0
  62. package/dist/integrations/cli.js +440 -0
  63. package/dist/integrations/cli.ts +543 -0
  64. package/dist/{core/files.d.ts → integrations/disk.d.ts} +16 -24
  65. package/dist/integrations/disk.js +435 -0
  66. package/dist/integrations/disk.ts +465 -0
  67. package/dist/integrations/generators/read.js +6 -6
  68. package/dist/integrations/generators/read.ts +4 -4
  69. package/dist/integrations/generators/vault.js +10 -11
  70. package/dist/integrations/generators/vault.ts +4 -5
  71. package/dist/{operations/http.d.ts → integrations/https.d.ts} +10 -6
  72. package/dist/integrations/https.js +170 -0
  73. package/dist/integrations/https.ts +216 -0
  74. package/dist/integrations/pglite.d.ts +26 -0
  75. package/dist/integrations/pglite.js +143 -0
  76. package/dist/integrations/pglite.ts +206 -0
  77. package/dist/integrations/secret.d.ts +8 -2
  78. package/dist/integrations/secret.js +3 -5
  79. package/dist/integrations/secret.ts +10 -6
  80. package/dist/integrations/target.d.ts +155 -14
  81. package/dist/integrations/target.js +171 -50
  82. package/dist/integrations/target.ts +377 -61
  83. package/dist/integrations/vault.d.ts +1 -0
  84. package/dist/integrations/vault.js +20 -5
  85. package/dist/integrations/vault.ts +17 -5
  86. package/dist/integrity/attach.js +8 -77
  87. package/dist/integrity/attach.ts +7 -76
  88. package/dist/integrity/check-threads.js +5 -5
  89. package/dist/integrity/check-threads.ts +2 -2
  90. package/dist/integrity/constraint.d.ts +0 -1
  91. package/dist/integrity/constraint.js +267 -366
  92. package/dist/integrity/constraint.ts +267 -375
  93. package/dist/integrity/generators/compiler.js +14 -17
  94. package/dist/integrity/generators/compiler.ts +10 -15
  95. package/dist/integrity/generators/gate.js +15 -15
  96. package/dist/integrity/generators/gate.ts +10 -10
  97. package/dist/integrity/generators/issues.js +7 -7
  98. package/dist/integrity/generators/publish.js +39 -57
  99. package/dist/integrity/generators/publish.ts +44 -55
  100. package/dist/integrity/generators/trial.d.ts +3 -3
  101. package/dist/integrity/generators/trial.js +133 -165
  102. package/dist/integrity/generators/trial.ts +145 -169
  103. package/dist/integrity/lint.js +9 -13
  104. package/dist/integrity/lint.ts +7 -10
  105. package/dist/integrity/lsp/bin.js +5 -4
  106. package/dist/integrity/lsp/bin.ts +3 -1
  107. package/dist/integrity/lsp/diagnostics.js +3 -3
  108. package/dist/integrity/lsp/diagnostics.ts +1 -1
  109. package/dist/integrity/lsp/lint.js +19 -30
  110. package/dist/integrity/lsp/lint.ts +16 -32
  111. package/dist/integrity/lsp/server.js +3 -3
  112. package/dist/integrity/slopnator.d.ts +3 -13
  113. package/dist/integrity/slopnator.js +59 -38
  114. package/dist/integrity/slopnator.ts +54 -38
  115. package/dist/integrity/slopnators/sl_broken_lineage.js +2 -2
  116. package/dist/integrity/slopnators/sl_declared_elsewhere.js +1 -1
  117. package/dist/integrity/slopnators/sl_derived_drift.js +4 -4
  118. package/dist/integrity/slopnators/sl_derived_drift.ts +1 -1
  119. package/dist/integrity/slopnators/sl_door_in_a_node.js +220 -0
  120. package/dist/integrity/slopnators/sl_door_in_a_node.ts +250 -0
  121. package/dist/integrity/slopnators/sl_inert_island.js +10 -8
  122. package/dist/integrity/slopnators/sl_inert_island.ts +8 -6
  123. package/dist/integrity/slopnators/sl_missing_row.js +6 -51
  124. package/dist/integrity/slopnators/sl_missing_row.ts +4 -50
  125. package/dist/integrity/slopnators/sl_no_barrel.js +1 -1
  126. package/dist/integrity/slopnators/sl_no_child_process.js +36 -18
  127. package/dist/integrity/slopnators/sl_no_child_process.ts +44 -18
  128. package/dist/integrity/slopnators/sl_no_dispatch_bypass.js +80 -149
  129. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +95 -147
  130. package/dist/integrity/slopnators/sl_no_import_cycle.js +15 -4
  131. package/dist/integrity/slopnators/sl_no_import_cycle.ts +20 -4
  132. package/dist/integrity/slopnators/sl_no_such_command.js +2 -2
  133. package/dist/integrity/slopnators/sl_no_such_command.ts +1 -1
  134. package/dist/integrity/slopnators/sl_outside_an_operation.js +20 -0
  135. package/dist/integrity/slopnators/sl_outside_an_operation.ts +25 -0
  136. package/dist/integrity/slopnators/sl_screen_holds_only_declared.d.ts +2 -0
  137. package/dist/integrity/slopnators/sl_screen_holds_only_declared.js +61 -0
  138. package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +70 -0
  139. package/dist/integrity/slopnators/sl_secret_literal.js +1 -33
  140. package/dist/integrity/slopnators/sl_secret_literal.ts +1 -33
  141. package/dist/integrity/slopnators/sl_stale_path.js +5 -22
  142. package/dist/integrity/slopnators/sl_stale_path.ts +4 -20
  143. package/dist/integrity/slopnators/sl_twice_meant.js +1 -1
  144. package/dist/integrity/slopnators/sl_twice_parented.js +29 -5
  145. package/dist/integrity/slopnators/sl_twice_parented.ts +28 -4
  146. package/dist/integrity/slopnators/sl_undeclared_crossing.d.ts +4 -1
  147. package/dist/integrity/slopnators/sl_undeclared_crossing.js +426 -62
  148. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +454 -70
  149. package/dist/integrity/slopnators/sl_undeclared_destination.js +59 -25
  150. package/dist/integrity/slopnators/sl_undeclared_destination.ts +69 -21
  151. package/dist/integrity/slopnators/sl_undeclared_list.d.ts +2 -0
  152. package/dist/integrity/slopnators/sl_undeclared_list.js +363 -0
  153. package/dist/integrity/slopnators/sl_undeclared_list.ts +358 -0
  154. package/dist/integrity/slopnators/sl_undeclared_node.js +1 -11
  155. package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -10
  156. package/dist/integrity/slopnators/sl_undeclared_operation.js +111 -29
  157. package/dist/integrity/slopnators/sl_undeclared_operation.ts +134 -26
  158. package/dist/integrity/slopnators/sl_undeclared_variant.js +2 -2
  159. package/dist/integrity/slopnators/sl_undeclared_webhook.d.ts +2 -0
  160. package/dist/integrity/slopnators/sl_undeclared_webhook.js +90 -0
  161. package/dist/integrity/slopnators/sl_undeclared_webhook.ts +108 -0
  162. package/dist/integrity/slopnators/sl_underived_route.d.ts +2 -0
  163. package/dist/integrity/slopnators/sl_underived_route.js +61 -0
  164. package/dist/integrity/slopnators/sl_underived_route.ts +72 -0
  165. package/dist/integrity/slopnators/sl_ungoverned_island.js +3 -3
  166. package/dist/integrity/slopnators/sl_ungoverned_island.ts +1 -1
  167. package/dist/integrity/slopnators/sl_unlisted_program.d.ts +2 -0
  168. package/dist/integrity/slopnators/sl_unlisted_program.js +39 -0
  169. package/dist/integrity/slopnators/sl_unlisted_program.ts +40 -0
  170. package/dist/integrity/slopnators/sl_unnamed_island.js +1 -1
  171. package/dist/integrity/slopnators/sl_unpaired_member.js +3 -3
  172. package/dist/integrity/slopnators/sl_unpaired_member.ts +1 -1
  173. package/dist/integrity/slopnators/sl_unpaired_slopnator.js +4 -4
  174. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +1 -1
  175. package/dist/integrity/slopnators/sl_unprefixed_member.js +5 -31
  176. package/dist/integrity/slopnators/sl_unprefixed_member.ts +2 -28
  177. package/dist/integrity/slopnators/sl_unthreaded.js +11 -7
  178. package/dist/integrity/slopnators/sl_unthreaded.ts +6 -7
  179. package/dist/integrity/slopnators/sl_untranslated_key.js +2 -25
  180. package/dist/integrity/slopnators/sl_untranslated_key.ts +1 -25
  181. package/dist/integrity/slopnators/sl_unvaulted_secret.d.ts +2 -0
  182. package/dist/integrity/slopnators/sl_unvaulted_secret.js +107 -0
  183. package/dist/integrity/slopnators/sl_unvaulted_secret.ts +132 -0
  184. package/dist/integrity/slopnators/sl_unversioned.js +6 -4
  185. package/dist/integrity/slopnators/sl_unversioned.ts +5 -3
  186. package/dist/integrity/sweep.d.ts +1 -0
  187. package/dist/integrity/sweep.js +105 -42
  188. package/dist/integrity/sweep.ts +115 -42
  189. package/dist/knowledge/attach.js +4 -3
  190. package/dist/knowledge/attach.ts +3 -2
  191. package/dist/knowledge/generators/docsgen.js +8 -6
  192. package/dist/knowledge/generators/docsgen.ts +4 -2
  193. package/dist/knowledge/generators/glossary.js +4 -4
  194. package/dist/knowledge/generators/glossary.ts +1 -1
  195. package/dist/operations/access.d.ts +23 -0
  196. package/dist/operations/access.js +28 -1
  197. package/dist/operations/access.ts +43 -0
  198. package/dist/operations/attach.js +33 -113
  199. package/dist/operations/attach.ts +51 -117
  200. package/dist/operations/capabilities.d.ts +14 -5
  201. package/dist/operations/capabilities.js +13 -4
  202. package/dist/operations/capabilities.ts +27 -8
  203. package/dist/operations/dispatch.d.ts +8 -2
  204. package/dist/operations/dispatch.js +128 -185
  205. package/dist/operations/dispatch.ts +138 -191
  206. package/dist/operations/error-results.d.ts +1 -1
  207. package/dist/operations/error-results.js +2 -2
  208. package/dist/operations/error-results.ts +2 -2
  209. package/dist/operations/generators/capability.d.ts +11 -1
  210. package/dist/operations/generators/capability.js +481 -224
  211. package/dist/operations/generators/capability.ts +562 -233
  212. package/dist/operations/generators/compile.js +66 -39
  213. package/dist/operations/generators/compile.ts +78 -39
  214. package/dist/operations/generators/permits.js +11 -12
  215. package/dist/operations/generators/permits.ts +3 -4
  216. package/dist/operations/generators/seed.d.ts +1 -0
  217. package/dist/operations/generators/seed.js +19 -17
  218. package/dist/operations/generators/seed.ts +22 -14
  219. package/dist/operations/generators/trial.d.ts +6 -8
  220. package/dist/operations/generators/trial.js +200 -70
  221. package/dist/operations/generators/trial.ts +230 -93
  222. package/dist/operations/nodes.d.ts +15 -16
  223. package/dist/operations/nodes.ts +16 -17
  224. package/dist/operations/operations.d.ts +40 -4
  225. package/dist/operations/operations.js +1 -0
  226. package/dist/operations/operations.ts +61 -4
  227. package/dist/operations/result.d.ts +2 -2
  228. package/dist/operations/result.ts +2 -2
  229. package/package.json +1 -4
  230. package/dist/core/answers.d.ts +0 -171
  231. package/dist/core/answers.js +0 -789
  232. package/dist/core/answers.ts +0 -999
  233. package/dist/core/client.d.ts +0 -63
  234. package/dist/core/client.js +0 -249
  235. package/dist/core/client.ts +0 -289
  236. package/dist/core/files.js +0 -171
  237. package/dist/core/files.ts +0 -222
  238. package/dist/integrations/module.d.ts +0 -6
  239. package/dist/integrations/module.js +0 -153
  240. package/dist/integrations/module.ts +0 -191
  241. package/dist/integrity/slopnators/sl_unkept_rows.js +0 -104
  242. package/dist/integrity/slopnators/sl_unkept_rows.ts +0 -111
  243. package/dist/integrity/slopnators/sl_unseamed_reach.js +0 -73
  244. package/dist/integrity/slopnators/sl_unseamed_reach.ts +0 -79
  245. package/dist/operations/cost.d.ts +0 -14
  246. package/dist/operations/cost.js +0 -37
  247. package/dist/operations/cost.ts +0 -58
  248. package/dist/operations/errors/errors.d.ts +0 -52
  249. package/dist/operations/http.js +0 -124
  250. package/dist/operations/http.ts +0 -169
  251. package/dist/operations/memory.d.ts +0 -104
  252. package/dist/operations/memory.js +0 -365
  253. package/dist/operations/memory.ts +0 -470
  254. package/dist/operations/metering.d.ts +0 -55
  255. package/dist/operations/metering.js +0 -247
  256. package/dist/operations/metering.ts +0 -328
  257. package/dist/operations/queue-policy.d.ts +0 -3
  258. package/dist/operations/queue-policy.js +0 -8
  259. package/dist/operations/queue-policy.ts +0 -10
  260. package/dist/operations/seams.d.ts +0 -239
  261. package/dist/operations/seams.js +0 -45
  262. package/dist/operations/seams.ts +0 -364
  263. package/dist/operations/store.d.ts +0 -14
  264. package/dist/operations/store.js +0 -1
  265. package/dist/operations/store.ts +0 -29
  266. /package/dist/{operations → core}/errors/classes.d.ts +0 -0
  267. /package/dist/{operations → core}/errors/classes.js +0 -0
  268. /package/dist/{operations → core}/errors/classes.ts +0 -0
  269. /package/dist/integrity/slopnators/{sl_unkept_rows.d.ts → sl_door_in_a_node.d.ts} +0 -0
  270. /package/dist/integrity/slopnators/{sl_unseamed_reach.d.ts → sl_outside_an_operation.d.ts} +0 -0
@@ -1,999 +0,0 @@
1
- import { createHash, randomBytes } from 'node:crypto'
2
- import { existsSync, readFileSync, rmSync, writeFileSync } from './files.ts'
3
- import { tmpdir } from 'node:os'
4
- import { dirname, join, relative } from 'node:path'
5
- import { argv, env, exit, stderr, stdin, stdout } from 'node:process'
6
- import { createInterface } from 'node:readline'
7
- import { pathToFileURL } from 'node:url'
8
- import { CONSTRAINTS } from '../integrity/constraint.ts'
9
- import type { Row } from './descend.ts'
10
- import { LISTS, descend, listFields, listShape, shapeFields, shapesNamed, type Field } from './descend.ts'
11
- import { quoted, valuesIn } from './rows.ts'
12
- import type { Tree } from './tree.ts'
13
-
14
- export const RECEIPT = '--receipt'
15
-
16
- export const ENDS = '.'
17
-
18
- export const FROM = '--from'
19
-
20
- export const CHAINED = '--chained'
21
-
22
- export const BODY = 'body'
23
-
24
- export const WRITES = 'writes'
25
-
26
- export const asFlag = (key: string): string => `--${key}`
27
-
28
- export function refuse(because: string): never {
29
- stderr.write(`create: ${because}\n`)
30
- exit(1)
31
- }
32
-
33
- export function slug(one: string): string {
34
- return one
35
- .trim()
36
- .toLowerCase()
37
- .replace(/[^a-z0-9]+/g, '_')
38
- .replace(/^_+|_+$/g, '')
39
- }
40
-
41
- export function within(root: string, path: string): string {
42
- return relative(root, path).split('\\').join('/')
43
- }
44
-
45
- const NAMED = /^([a-z][A-Za-z0-9_.-]*):[ \t]?(.*)$/
46
-
47
- export function parsed(text: string): readonly { readonly at: number; readonly key: string; readonly value: string }[] {
48
- const lines = text.split('\n')
49
- const found: { at: number; key: string; value: string }[] = []
50
- for (let at = 0; at < lines.length; at += 1) {
51
- const line = lines[at] ?? ''
52
- if (line.trim() === '' || line.trimStart().startsWith('#')) continue
53
- const held = NAMED.exec(line)
54
- if (held === null) {
55
- refuse(
56
- `line ${String(at + 1)} is neither blank nor an answer: ${JSON.stringify(line.slice(0, 60))}. ` +
57
- 'Every answer is written `name: value`, and a body is written under `body:` and ended by a line ' +
58
- `holding only ${ENDS}.`,
59
- )
60
- }
61
- const key = held[1] ?? ''
62
- if (key !== 'body') {
63
- found.push({ at: at + 1, key, value: (held[2] ?? '').trim() })
64
- continue
65
- }
66
-
67
- const body: string[] = []
68
- const beside = held[2] ?? ''
69
- if (beside.trim() !== '') body.push(beside)
70
- let shut = false
71
- for (at += 1; at < lines.length; at += 1) {
72
- if ((lines[at] ?? '').trim() === ENDS) {
73
- shut = true
74
- break
75
- }
76
- body.push(lines[at] ?? '')
77
- }
78
- if (!shut) {
79
- refuse(
80
- `the \`body:\` opened on line ${String(found.length === 0 ? 1 : at)} is never closed — a body ends at a ` +
81
- `line holding only ${ENDS}, and this one runs to the end of the input.`,
82
- )
83
- }
84
- found.push({ at: at + 1, key, value: body.join('\n').trim() })
85
- }
86
- return found
87
- }
88
-
89
- export function scoped(
90
- given: readonly { readonly at: number; readonly key: string; readonly value: string }[],
91
- opens: string,
92
- ): {
93
- readonly own: readonly { readonly at: number; readonly key: string; readonly value: string }[]
94
- readonly each: readonly (readonly { readonly at: number; readonly key: string; readonly value: string }[])[]
95
- } {
96
- const own: { at: number; key: string; value: string }[] = []
97
- const each: { at: number; key: string; value: string }[][] = []
98
- const lists = new Set<string>(LISTS)
99
- let mine = false
100
- for (const one of given) {
101
- if (one.key === opens) {
102
- each.push([one])
103
- mine = true
104
- continue
105
- }
106
- if (lists.has(one.key)) {
107
- mine = false
108
- continue
109
- }
110
- if (mine) (each[each.length - 1] ?? []).push(one)
111
- else if (each.length === 0) own.push(one)
112
- }
113
- return { own, each }
114
- }
115
-
116
- const locate = (one: { readonly at: number; readonly key: string }): string =>
117
- one.at > 0 ? `line ${String(one.at)}` : `\`--${one.key}\``
118
-
119
- export interface Asked {
120
- readonly at: string
121
- readonly guide: string
122
- readonly asks?: string
123
- readonly optional?: boolean
124
- readonly list?: boolean
125
- readonly map?: boolean
126
- readonly boolean?: boolean
127
- readonly worded?: boolean
128
- }
129
-
130
- export interface Said {
131
- readonly guide: string
132
- readonly asks?: string
133
- readonly optional?: boolean
134
- }
135
-
136
- export const named = (all: readonly Asked[]): readonly string[] => all.map((one) => one.at)
137
-
138
- export const prompted = (one: Asked): string =>
139
- one.asks ?? `${one.at} — ${one.guide}${one.optional === true ? ', or blank' : ''}: `
140
-
141
- export type Given = { readonly at: number; readonly key: string; readonly value: string }
142
-
143
- export interface Child {
144
- readonly id: string
145
- readonly at: string
146
- readonly describes: string
147
- readonly by?: string
148
- readonly runs?: string
149
- readonly body?: string
150
- readonly writes?: string
151
- }
152
-
153
- export interface Stated {
154
- readonly field: string
155
- readonly said: string
156
- readonly list: boolean
157
- readonly map?: boolean
158
- readonly boolean?: boolean
159
- readonly worded?: boolean
160
- }
161
-
162
- export const mapped = (one: { readonly map?: boolean; readonly worded?: boolean }, said: string): boolean =>
163
- one.map === true && !(one.worded === true && !said.includes('='))
164
-
165
- export const CARRIES: readonly string[] = ['cites', 'requires', 'takes', 'by', 'runs']
166
-
167
- export function ownAnswers(all: readonly Given[]): readonly Given[] {
168
- const opens = new Set<string>(LISTS)
169
- const at = all.findIndex((one) => opens.has(one.key))
170
- return at === -1 ? all : all.slice(0, at)
171
- }
172
-
173
- export async function stating(
174
- asks: readonly Asked[],
175
- all: readonly Given[],
176
- piped: boolean,
177
- ask: (name: string, asks: string) => Promise<string>,
178
- line: () => Promise<string | undefined>,
179
- settled: readonly string[],
180
- ): Promise<{ readonly stated: readonly Stated[]; readonly kids: ReadonlyMap<string, readonly Child[]> }> {
181
- const own = ownAnswers(all)
182
- const skip = new Set<string>([...settled, ...LISTS])
183
- const stated: Stated[] = []
184
- for (const one of asks) {
185
- if (skip.has(one.at)) continue
186
- const given = (piped ? (answer(own, one.at) ?? '') : await ask(one.at, prompted(one))).trim()
187
- if (given === '') continue
188
- stated.push({ field: one.at, said: given, list: one.list === true, map: one.map === true, boolean: one.boolean === true, worded: one.worded === true })
189
- }
190
-
191
- if (piped) return { stated, kids: childrenIn(all) }
192
-
193
- const kids = new Map<string, readonly Child[]>()
194
- for (const list of LISTS) {
195
- if (!asks.some((one) => one.at === list)) continue
196
- const found = await children(list, all, ask, line)
197
- if (found.length > 0) kids.set(list, found)
198
- }
199
- return { stated, kids }
200
- }
201
-
202
- export function arrayValue(pad: string, key: string, said: string, found: readonly Child[] | undefined): string {
203
- if (found === undefined || found.length === 0) return asList(said)
204
- const lines = rowLines(pad, [], new Map([[key, found]]))
205
- return ['[', ...lines.slice(1, -1), `${pad}]`].join('\n')
206
- }
207
-
208
- const BARE = /^[A-Za-z_$][A-Za-z0-9_$]*$/
209
-
210
- const NUMBER = /^-?\d+(?:\.\d+)?$/
211
-
212
- const nested = (held: Readonly<Record<string, unknown>>): string => {
213
- const pairs = Object.entries(held).map(([key, value]) => {
214
- const name = BARE.test(key) ? key : quoted(key)
215
- if (typeof value === 'object' && value !== null) return `${name}: ${nested(value as Readonly<Record<string, unknown>>)}`
216
- const said = String(value)
217
- return `${name}: ${NUMBER.test(said) ? said : quoted(said)}`
218
- })
219
- return pairs.length === 0 ? '{}' : `{ ${pairs.join(', ')} }`
220
- }
221
-
222
- export const asMap = (said: string): string => {
223
- const held: Record<string, unknown> = {}
224
- for (const one of said.split(',').map((each) => each.trim()).filter((each) => each !== '')) {
225
- const at = one.indexOf('=')
226
- if (at < 1) {
227
- refuse(
228
- `\`${one}\` is one entry of a name-and-value field, and it states no value. Write each one as ` +
229
- '`name=value`, separated by commas — `core=*` says that package at any version, and a dot in the name ' +
230
- 'nests it: `dark.background=colour-surface`. Nothing was written.',
231
- )
232
- }
233
- const path = one.slice(0, at).trim().split('.')
234
- const last = path[path.length - 1] ?? ''
235
- let into = held
236
- for (const step of path.slice(0, -1)) {
237
- const next = into[step]
238
- if (next !== undefined && (typeof next !== 'object' || next === null)) {
239
- refuse(`\`${one}\` nests under \`${step}\`, and \`${step}\` is already given a value of its own. One or the other. Nothing was written.`)
240
- }
241
- if (next === undefined) into[step] = {}
242
- into = into[step] as Record<string, unknown>
243
- }
244
- if (typeof into[last] === 'object' && into[last] !== null) {
245
- refuse(`\`${one}\` gives \`${last}\` a value, and entries already nest under it. One or the other. Nothing was written.`)
246
- }
247
- into[last] = one.slice(at + 1).trim()
248
- }
249
- return nested(held)
250
- }
251
-
252
- export function gathered(all: readonly Given[], fields: readonly Field[]): readonly Given[] {
253
- const maps = new Set(fields.filter((one) => one.map).map((one) => one.name))
254
- const entries = new Map<string, { readonly at: number; readonly parts: string[] }>()
255
- for (const one of all) {
256
- const dot = one.key.indexOf('.')
257
- if (dot === -1 || !maps.has(one.key.slice(0, dot))) continue
258
- const head = one.key.slice(0, dot)
259
- const held = entries.get(head) ?? { at: one.at, parts: [] }
260
- held.parts.push(`${one.key.slice(dot + 1)}=${one.value}`)
261
- entries.set(head, held)
262
- }
263
- const written = new Set<string>()
264
- const out: Given[] = []
265
- for (const one of all) {
266
- const dot = one.key.indexOf('.')
267
- const head = dot === -1 ? one.key : one.key.slice(0, dot)
268
- if (dot === -1 || !entries.has(head)) {
269
- out.push(one)
270
- continue
271
- }
272
- if (written.has(head)) continue
273
- written.add(head)
274
- const held = entries.get(head)
275
- out.push({ at: held?.at ?? one.at, key: head, value: (held?.parts ?? []).join(', ') })
276
- }
277
- return out
278
- }
279
-
280
- const asList = (said: string): string =>
281
- `[${said
282
- .split(',')
283
- .map((one) => one.trim())
284
- .filter((one) => one !== '')
285
- .map(quoted)
286
- .join(', ')}]`
287
-
288
- export const asBoolean = (said: string): string => {
289
- const word = said.trim().toLowerCase()
290
- if (word === 'yes' || word === 'y' || word === 'true') return 'true'
291
- if (word === 'no' || word === 'n' || word === 'false') return 'false'
292
- refuse(`\`${said}\` is not an answer to a yes-or-no field. Say yes or no. Nothing was written.`)
293
- }
294
-
295
- export function rowLines(
296
- pad: string,
297
- stated: readonly Stated[],
298
- kids: ReadonlyMap<string, readonly Child[]>,
299
- ): readonly string[] {
300
- const kid = (one: Child): readonly string[] => [
301
- `${pad} {`,
302
- `${pad} id: ${quoted(one.id)},`,
303
- `${pad} at: ${quoted(one.at)},`,
304
- `${pad} describes: ${quoted(one.describes)},`,
305
- ...(one.by === undefined || one.by === '' ? [] : [`${pad} by: ${quoted(one.by)},`]),
306
- ...(one.runs === undefined || one.runs === '' ? [] : [`${pad} runs: ${quoted(one.runs)},`]),
307
- `${pad} version: 1,`,
308
- `${pad} },`,
309
- ]
310
- return [
311
- ...stated.map(
312
- (one) =>
313
- `${pad}${one.field}: ${
314
- mapped(one, one.said) ? asMap(one.said) : one.list ? asList(one.said) : one.boolean === true ? asBoolean(one.said) : quoted(one.said)
315
- },`,
316
- ),
317
- ...[...kids].flatMap(([list, found]) => [`${pad}${list}: [`, ...found.flatMap(kid), `${pad}],`]),
318
- ]
319
- }
320
-
321
- export function withChildren(row: string, lines: readonly string[]): string {
322
- if (lines.length === 0) return row
323
- const all = row.split('\n')
324
- let at = all.length - 1
325
- while (at >= 0 && (all[at] ?? '').trim() !== '},' && (all[at] ?? '').trim() !== '}') at -= 1
326
- if (at < 0) {
327
- refuse(
328
- 'the row just written has no closing brace on a line of its own, so what it declares has nowhere to go. ' +
329
- 'Nothing more was written, because adding the row and dropping its children quietly is the one outcome ' +
330
- 'nobody could see.',
331
- )
332
- }
333
- return [...all.slice(0, at), ...lines, ...all.slice(at)].join('\n')
334
- }
335
-
336
- export function childrenIn(all: readonly Given[]): ReadonlyMap<string, readonly Child[]> {
337
- const found = new Map<string, readonly Child[]>()
338
- for (const list of LISTS) {
339
- const each: Child[] = []
340
- for (const group of scoped(all, list).each) {
341
- const id = (answer(group, list) ?? '').trim()
342
- if (id === '') continue
343
- const at = (answer(group, 'at') ?? '').trim()
344
- const describes = (answer(group, 'describes') ?? '').trim()
345
- if (at === '' || describes === '') {
346
- refuse(
347
- `\`${list} ${id}\` names no ${at === '' ? 'file' : 'meaning'}. Every row says which file it is about and ` +
348
- 'what that file is, and one missing either is a row the next person cannot act on. Nothing more was written.',
349
- )
350
- }
351
- const body = (answer(group, BODY) ?? '').trim()
352
- const writes = (answer(group, WRITES) ?? '').trim()
353
- each.push({
354
- id,
355
- at,
356
- describes,
357
- ...(writes === '' ? {} : { writes }),
358
- ...(list === 'declares'
359
- ? {}
360
- : { by: (answer(group, 'by') ?? '').trim(), runs: (answer(group, 'runs') ?? '').trim() }),
361
- ...(body === '' ? {} : { body }),
362
- })
363
- }
364
- if (each.length > 0) found.set(list, each)
365
- }
366
- return found
367
- }
368
-
369
- export const BODIED: readonly string[] = ['declares', 'seeds']
370
-
371
- export const WRITTEN: readonly string[] = ['derives', 'seeds']
372
-
373
- export function seeding(kids: ReadonlyMap<string, readonly Child[]>, root: string): readonly string[] {
374
- const said: string[] = []
375
- for (const list of BODIED) {
376
- for (const one of kids.get(list) ?? []) {
377
- const into = join(root, one.at)
378
- const there = existsSync(into)
379
-
380
- if (one.body === undefined || one.body === '') {
381
- said.push(
382
- there
383
- ? `${one.at} was already on disk, and the row now names it.`
384
- : `${one.at} is declared and nothing is in it yet — write it with ` +
385
- `\`${TYPED} edit ${one.at} --from <path>\`.`,
386
- )
387
- continue
388
- }
389
-
390
- if (there) {
391
- said.push(
392
- `${one.at} was already on disk, so the body you wrote was NOT put over it. Nothing was lost — ` +
393
- `\`${TYPED} read ${one.at}\` prints what is there.`,
394
- )
395
- continue
396
- }
397
-
398
- writeFileSync(into, one.body.endsWith('\n') ? one.body : `${one.body}\n`, 'utf8')
399
- said.push(`wrote ${one.at} with the body you gave it.`)
400
- }
401
- }
402
- return said
403
- }
404
-
405
- const ASKED: Readonly<Record<string, string>> = {
406
- declares: 'name each file a person writes that this one owns',
407
- derives: 'name each file a command writes from this one, and the command that writes it',
408
- seeds: 'name each file a command shapes here',
409
- }
410
-
411
- export async function children(
412
- list: string,
413
- all: readonly Given[],
414
- ask: (name: string, asks: string) => Promise<string>,
415
- line: () => Promise<string | undefined>,
416
- ): Promise<readonly Child[]> {
417
- const written = WRITTEN.includes(list)
418
- const out: Child[] = []
419
-
420
- const owed = (id: string, at: string, describes: string): void => {
421
- if (at !== '' && describes !== '') return
422
- refuse(
423
- `\`${list} ${id}\` names no ${at === '' ? 'file' : 'meaning'}. Every row says which file it is about and what ` +
424
- 'that file is, and one missing either is a row the next person cannot act on. Nothing more was written.',
425
- )
426
- }
427
-
428
- const bodies = [
429
- ...(written ? [`${asFlag(WRITES)} is the generator's own body — the code that writes the template`] : []),
430
- ...(BODIED.includes(list) ? [`${asFlag(BODY)} is the body of the file itself`] : []),
431
- ]
432
- stdout.write(
433
- `\n ${list} — ${ASKED[list] ?? ''}. ` +
434
- (bodies.length === 0
435
- ? ''
436
- : `Each one takes ${bodies.length === 1 ? 'one body' : `${String(bodies.length)} bodies, and they are never the same text`}: ` +
437
- `${bodies.join('; ')}. `) +
438
- 'A blank id ends the list.\n',
439
- )
440
- for (;;) {
441
- const id = (await ask(list, ` ${list} — id, or blank to stop: `)).trim()
442
- if (id === '') break
443
- const at = (await ask('at', ' at — the file this names: ')).trim()
444
- const describes = (await ask('describes', ' describes — what that file is, to somebody who has not opened it: ')).trim()
445
- owed(id, at, describes)
446
- const by = written ? (await ask('by', ' by — the command that writes it: ')).trim() : undefined
447
- const runs = written ? (await ask('runs', ' runs — the command that catches it drifting: ')).trim() : undefined
448
- const writes = written
449
- ? await bodied(`the generator behind \`${by ?? 'that command'}\` — this is ${asFlag(WRITES)}`, '', false, all, line)
450
- : undefined
451
- const body = BODIED.includes(list)
452
- ? await bodied(`${at} itself — this is ${asFlag(BODY)}`, '', false, all, line)
453
- : undefined
454
- out.push({
455
- id,
456
- at,
457
- describes,
458
- ...(written ? { by, runs } : {}),
459
- ...(writes === undefined ? {} : { writes }),
460
- ...(body === undefined ? {} : { body }),
461
- })
462
- }
463
- return out
464
- }
465
-
466
- export async function bodied(
467
- what: string,
468
- shows: string,
469
- piped: boolean,
470
- all: readonly Given[],
471
- line: () => Promise<string | undefined>,
472
- ): Promise<string | undefined> {
473
- const from = argv[argv.indexOf(FROM) + 1]
474
- if (argv.includes(FROM) && from !== undefined && !from.startsWith('-')) {
475
- try {
476
- 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.`)
477
- } catch {
478
- refuse(
479
- `\`${FROM} ${from}\` names a file nothing could read, so there is no body to seed and nothing was ` +
480
- `written. Write it first, then run this again — or drop ${FROM} and type the body when it is asked for.`,
481
- )
482
- }
483
- }
484
- if (piped) {
485
- const said = answer(all, BODY)
486
- return said === undefined || said.trim() === '' ? undefined : said
487
- }
488
- if (shows.trim() === '') stdout.write(` body — write what goes in ${what}.\n`)
489
- else {
490
- stdout.write(` body — what a ${what} is handed looks like this:\n`)
491
- for (const one of shows.split('\n')) stdout.write(` ${one}\n`)
492
- }
493
- stdout.write(
494
- ` write yours, then a line holding only ${ENDS}. A first line of ${ENDS} leaves it unwritten, and\n` +
495
- ` \`${TYPED} trials\` reports it unproven until somebody writes it.\n` +
496
- ` Or give ${FROM} <path> instead and the file at that path is what gets seeded, which is the one\n` +
497
- ' form that needs no terminal.\n',
498
- )
499
- const held: string[] = []
500
- for (;;) {
501
- const next = await line()
502
- if (next === undefined || next.trim() === ENDS) break
503
- held.push(next)
504
- }
505
- const written = held.join('\n').trim()
506
- return written === '' ? undefined : written
507
- }
508
-
509
- export function only(
510
- given: readonly { readonly at: number; readonly key: string; readonly value: string }[],
511
- allowed: readonly string[],
512
- where: string,
513
- ): void {
514
- unasked(ownAnswers(given), allowed, where)
515
- }
516
-
517
- export function unasked(
518
- given: readonly { readonly at: number; readonly key: string; readonly value: string }[],
519
- allowed: readonly string[],
520
- where: string,
521
- ): void {
522
- for (const one of given) {
523
- if (CARRIES.includes(one.key) || one.key === WRITES) continue
524
- if (allowed.includes(one.key)) continue
525
- refuse(
526
- `${locate(one)} answers \`${one.key}\`, and ${where} takes ${allowed.map((each) => `\`${each}\``).join(', ')}. ` +
527
- 'Nothing was written.',
528
- )
529
- }
530
- }
531
-
532
- export function answer(
533
- given: readonly { readonly at: number; readonly key: string; readonly value: string }[],
534
- key: string,
535
- ): string | undefined {
536
- const found = given.filter((one) => one.key === key)
537
- if (found.length > 1) {
538
- refuse(
539
- `\`${key}\` is answered ${String(found.length)} times: ${found.map((one) => locate(one)).join(', ')}. ` +
540
- 'Nothing was written.',
541
- )
542
- }
543
- const held = found[0]?.value
544
- return held === undefined ? undefined : guarded(key, held)
545
- }
546
-
547
- export function accepted(
548
- given: readonly { readonly at: number; readonly key: string; readonly value: string }[],
549
- ): string {
550
- if (given.length === 0) return 'nothing was accepted before it'
551
- return `accepted so far: ${given.map((one) => `${one.key} (${locate(one)})`).join(', ')}`
552
- }
553
-
554
- export function needed(
555
- given: readonly { readonly at: number; readonly key: string; readonly value: string }[],
556
- key: string,
557
- where: string,
558
- ): string {
559
- const held = answer(given, key)
560
- if (held === undefined || held === '') {
561
- refuse(`${where} states no \`${key}\`, and it is not optional. ${accepted(given)}. Nothing was written.`)
562
- }
563
- return held
564
- }
565
-
566
- export function wrapped(text: string, pad: string): readonly string[] {
567
- const lines: string[] = []
568
- let held = ''
569
- for (const word of text.split(/\s+/).filter((one) => one !== '')) {
570
- if (held !== '' && `${held} ${word}`.length > 92) {
571
- lines.push(`${pad}${held}`)
572
- held = word
573
- } else held = held === '' ? word : `${held} ${word}`
574
- }
575
- if (held !== '') lines.push(`${pad}${held}`)
576
- return lines
577
- }
578
-
579
- export function marked(parts: readonly string[]): string {
580
- return createHash('sha256').update(parts.join('\n')).digest('hex').slice(0, 6)
581
- }
582
-
583
- export interface Told {
584
- readonly on: string
585
- readonly said: string
586
- }
587
-
588
- export interface Declared {
589
- readonly at: string
590
- readonly list: string
591
- readonly shape: string | undefined
592
- readonly fields: readonly Field[]
593
- readonly describes: string
594
- readonly holds: number
595
- readonly cites: readonly Told[]
596
- readonly top: { readonly id: string; readonly describes: string } | undefined
597
- readonly files?: string
598
- }
599
-
600
- const citesOf = (held: unknown): readonly Told[] =>
601
- Array.isArray(held)
602
- ? held.filter(
603
- (one): one is Told =>
604
- one !== null && typeof one === 'object' && typeof (one as Told).on === 'string' && typeof (one as Told).said === 'string',
605
- )
606
- : []
607
-
608
- const NAMED_SHAPE = /^[A-Za-z_][A-Za-z0-9_]*$/
609
-
610
- const rowLike = (one: unknown): one is Row =>
611
- one !== null && typeof one === 'object' && typeof (one as Row).id === 'string' && typeof (one as Row).describes === 'string'
612
-
613
- const kinded = (shape: string): string =>
614
- shape
615
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
616
- .toLowerCase()
617
-
618
- const oneOf = (list: string): string => {
619
- const word = list.toLowerCase().split('_').join('-')
620
- return word.endsWith('s') ? word.slice(0, -1) : word
621
- }
622
-
623
- export async function declared(tree: Tree): Promise<ReadonlyMap<string, Declared>> {
624
- const found = new Map<string, Declared>()
625
- const walked = new Set<string>()
626
-
627
- const says = new Map<string, string>()
628
-
629
- const parents = new Map<string, Row & { readonly cites?: unknown }>()
630
-
631
- const sheetsBy = new Map<string, Set<string>>()
632
-
633
- const meant = (shape: string | undefined, sheetAt: string): string => {
634
- if (shape === undefined) return ''
635
- try {
636
- const named = shapesNamed()
637
- for (const at of named.get(shape) ?? []) {
638
- if (at === sheetAt) continue
639
- let others = 0
640
- for (const [, files] of named) if (files.includes(at)) others += 1
641
- if (others !== 1) continue
642
- const told = says.get(at)
643
- if (told !== undefined && told !== '') return told
644
- }
645
- } catch {
646
- return ''
647
- }
648
- return ''
649
- }
650
-
651
- const sheet = (one: unknown): string | undefined => {
652
- const at = (one as Row | undefined)?.at
653
- return typeof at === 'string' && at.startsWith(`${tree.name}/`) && at.endsWith('.ts') ? at : undefined
654
- }
655
-
656
- const walk = async (at: string, top: Declared['top']): Promise<void> => {
657
- if (walked.has(at)) return
658
- walked.add(at)
659
-
660
- let read: Record<string, unknown>
661
- try {
662
- read = valuesIn(readFileSync(join(tree.at, at))) as Record<string, unknown>
663
- } catch {
664
- return
665
- }
666
-
667
- for (const [name, value] of Object.entries(read)) {
668
- const node = value as
669
- | { readonly id?: unknown; readonly describes?: unknown; readonly cites?: unknown; readonly declares?: unknown; readonly holds?: unknown; readonly files?: unknown }
670
- | undefined
671
- const lists: readonly { readonly list: string; readonly rows: readonly unknown[] }[] = Array.isArray(value)
672
- ? [{ list: name, rows: value }]
673
- : (['holds', 'declares'] as const).flatMap((list) => {
674
- const rows = node?.[list]
675
- return Array.isArray(rows) ? [{ list, rows }] : []
676
- })
677
- const rowed = lists.filter((one) => one.rows.every(rowLike))
678
- const first = rowed[0]
679
- if (first === undefined) continue
680
-
681
- if (Array.isArray(value)) {
682
- const noun = oneOf(name)
683
- sheetsBy.set(name, new Set([...(sheetsBy.get(name) ?? []), at]))
684
- if (!found.has(noun)) {
685
- found.set(noun, {
686
- at,
687
- list: name,
688
- shape: listShape(at, name),
689
- fields: listFields(at, name),
690
- describes: '',
691
- holds: first.rows.length,
692
- cites: [],
693
- top,
694
- })
695
- }
696
- } else if (at !== within(tree.at, tree.root)) {
697
- const files = typeof node?.files === 'string' && node.files !== '' ? node.files : undefined
698
- const held = files === undefined ? first : (rowed.find((one) => one.list === 'declares') ?? first)
699
- const kind = listShape(at, held.list)
700
- const noun =
701
- kind === undefined || (kind === 'Threaded' && files === undefined)
702
- ? undefined
703
- : kind !== 'Threaded' && NAMED_SHAPE.test(kind)
704
- ? kinded(kind)
705
- : typeof node?.id === 'string' && node.id !== ''
706
- ? node.id
707
- : undefined
708
- if (typeof noun === 'string' && noun !== '' && !found.has(noun)) {
709
- found.set(noun, {
710
- at,
711
- list: held.list,
712
- shape: kind,
713
- fields: listFields(at, held.list),
714
- describes: typeof node?.describes === 'string' ? node.describes : '',
715
- holds: held.rows.length,
716
- cites: citesOf(node?.cites),
717
- top,
718
- ...(files === undefined ? {} : { files }),
719
- })
720
- }
721
- }
722
- for (const one of rowed.flatMap((each) => each.rows)) {
723
- const where = (one as Row).at
724
- const told = (one as Row).describes
725
- if (typeof where === 'string' && typeof told === 'string' && !says.has(where)) says.set(where, told)
726
- const next = sheet(one)
727
- if (next === undefined) continue
728
- const row = one as Row & { readonly cites?: unknown }
729
- if (!parents.has(next)) parents.set(next, row)
730
- await walk(next, top ?? { id: typeof row.id === 'string' ? row.id : '', describes: typeof row.describes === 'string' ? row.describes : '' })
731
- }
732
- }
733
- }
734
-
735
- await walk(within(tree.at, tree.root), undefined)
736
-
737
- const out = new Map<string, Declared>()
738
- for (const [noun, one] of found) {
739
- const parent = parents.get(one.at)
740
- const said = typeof parent?.describes === 'string' ? parent.describes : ''
741
- const owned = (sheetsBy.get(one.list)?.size ?? 0) <= 1
742
- out.set(noun, {
743
- ...one,
744
- describes: one.describes !== '' ? one.describes : owned && said !== '' ? said : meant(one.shape, one.at),
745
- cites: one.cites.length > 0 ? one.cites : citesOf(parent?.cites),
746
- })
747
- }
748
- return out
749
- }
750
-
751
- export function nouns(all: readonly Row[], verb = 'create'): ReadonlyMap<string, string> {
752
- const found = new Map<string, string>()
753
- const opens = `thread ${verb} `
754
- for (const one of all) {
755
- const by = one.by
756
- if (typeof by !== 'string' || !by.startsWith(opens)) continue
757
- const word = by.slice(opens.length).trim()
758
- if (word === '' || typeof one.at !== 'string') continue
759
- const already = found.get(word)
760
- if (already !== undefined && already !== one.at) {
761
- refuse(
762
- `two rows both state \`by: 'thread ${verb} ${word}'\` — ${already} and ${one.at} — so which one answers a ` +
763
- `${word} is whichever was written first. One noun, one file.`,
764
- )
765
- }
766
- found.set(word, one.at)
767
- }
768
- return found
769
- }
770
-
771
- export function asking(
772
- held: readonly { readonly at: number; readonly key: string; readonly value: string }[],
773
- ): {
774
- readonly piped: boolean
775
- readonly ask: (
776
- key: string,
777
- question: string,
778
- from?: readonly { readonly at: number; readonly key: string; readonly value: string }[],
779
- ) => Promise<string>
780
-
781
- readonly line: () => Promise<string | undefined>
782
- readonly shut: () => void
783
- } {
784
- const piped = answered()
785
- const rl = piped ? undefined : createInterface({ input: stdin, output: stdout })
786
- const lines = rl?.[Symbol.asyncIterator]()
787
- return {
788
- piped,
789
- ask: async (key, question, from = held) => {
790
- const already = answer(from, key)
791
- if (already !== undefined) return guarded(key, already)
792
- if (piped) return ''
793
- stdout.write(question)
794
- const next = await lines?.next()
795
- return next === undefined || next.done === true ? '' : guarded(key, next.value.trim())
796
- },
797
- line: async () => {
798
- const next = await lines?.next()
799
- return next === undefined || next.done === true ? undefined : next.value
800
- },
801
- shut: () => rl?.close(),
802
- }
803
- }
804
-
805
- const ANSWERS = '--answers'
806
-
807
- function fromFile(): string | undefined {
808
- const at = argv.indexOf(ANSWERS)
809
- if (at === -1) return undefined
810
- const path = argv[at + 1]
811
- if (path === undefined || path.startsWith('-')) {
812
- refuse(`${ANSWERS} needs the path of a file holding the answers, one \`name: value\` to a line.`)
813
- }
814
- try {
815
- return readFileSync(path, 'utf8')
816
- } catch {
817
- refuse(`${ANSWERS} names ${path}, and there is no file there to read the answers from.`)
818
- }
819
- }
820
-
821
- const OWN = '--own'
822
- const CHECK = '--check'
823
-
824
- function flagged(): readonly { readonly at: number; readonly key: string; readonly value: string }[] {
825
- const found: { at: number; key: string; value: string }[] = []
826
- for (let at = 2; at < argv.length; at += 1) {
827
- const one = argv[at] ?? ''
828
- if (
829
- !one.startsWith('--') ||
830
- one === RECEIPT ||
831
- one === ANSWERS ||
832
- one === OWN ||
833
- one === CHECK ||
834
- one === FROM ||
835
- one === CHAINED
836
- )
837
- continue
838
- const value = argv[at + 1]
839
- if (value === undefined || value.startsWith('-')) {
840
- refuse(`\`${one}\` was given no value — an answer given as a flag is \`${one} value\`, with the value right after it.`)
841
- }
842
- found.push({ at: 0, key: one.slice(2), value })
843
- at += 1
844
- }
845
- return found
846
- }
847
-
848
- export function answered(): boolean {
849
- return argv.includes(ANSWERS) || stdin.isTTY !== true
850
- }
851
-
852
- export function given(): readonly { readonly at: number; readonly key: string; readonly value: string }[] {
853
- const flags = flagged()
854
- const fromFlag = new Set(flags.map((one) => one.key))
855
-
856
- const held = fromFile()
857
- if (held !== undefined) return read([...flags, ...parsed(held).filter((one) => !fromFlag.has(one.key))])
858
-
859
- const text = handed()
860
- if (text === undefined) return read(flags)
861
-
862
- return read([...flags, ...parsed(text).filter((one) => !fromFlag.has(one.key))])
863
- }
864
-
865
- const FROM_END = '-from'
866
-
867
- function read(
868
- all: readonly { readonly at: number; readonly key: string; readonly value: string }[],
869
- ): readonly { readonly at: number; readonly key: string; readonly value: string }[] {
870
- return all.map((one) => {
871
- if (!one.key.endsWith(FROM_END)) return one
872
- const key = one.key.slice(0, -FROM_END.length)
873
- if (key === '') return one
874
- if (all.some((each) => each.key === key)) {
875
- refuse(
876
- `\`${asFlag(key)}\` and \`${asFlag(one.key)}\` both answer ${key}, and they do not have to agree. ` +
877
- 'Give one of them. Nothing was written.',
878
- )
879
- }
880
- try {
881
- 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.`)
882
- } catch {
883
- refuse(
884
- `\`${asFlag(one.key)} ${one.value}\` names a file nothing could read, so there is no ${key} to write and ` +
885
- `nothing was written. Write that file first, or give the text itself with ${asFlag(key)}.`,
886
- )
887
- }
888
- })
889
- }
890
-
891
- let stood: string | undefined
892
- let taken = false
893
-
894
- export function handed(): string | undefined {
895
- if (taken) return stood
896
- taken = true
897
- if (stdin.isTTY === true) return undefined
898
- try {
899
- stood = readFileSync(0, 'utf8')
900
- } catch {
901
- stood = undefined
902
- }
903
- return stood
904
- }
905
-
906
- export const TYPED = 'thread'
907
-
908
- export const CARRIED: readonly { readonly verb: string; readonly typed: string; readonly does: string }[] = [
909
- { verb: 'read', typed: `${TYPED} read <row>`, does: 'prints every field a row states' },
910
- { verb: 'edit', typed: `${TYPED} edit <row> --<field> <value>`, does: 'changes any field it was declared with' },
911
- { verb: 'remove', typed: `${TYPED} edit <row> --remove`, does: 'takes the row out of the tree' },
912
- ]
913
-
914
- const elsewhere = CARRIED.map((one) => `\`${one.typed}\` ${one.does}`)
915
-
916
- export function wordsIn(offered: readonly string[]): readonly string[] {
917
- const values = new Set([argv[argv.indexOf(RECEIPT) + 1], argv[argv.indexOf(ANSWERS) + 1], ...flagged().map((one) => one.value)])
918
- const words = argv.slice(2).filter((one) => !one.startsWith('-') && !values.has(one))
919
- const first = words[0]
920
- if (offered.length === 0 || first === undefined || offered.includes(first)) return words
921
- const carried = CARRIED.find((one) => one.verb === first)
922
- if (carried !== undefined) {
923
- refuse(
924
- `\`${first}\` is not a word this command answers, because every row in this tree has it and it is ` +
925
- `answered in one place rather than once per thing. \`${carried.typed}\` ${carried.does}, whatever ` +
926
- 'declared it, and it finds the row by its id or by the mark printed beside it. Nothing was written, ' +
927
- 'and nothing was read.',
928
- )
929
- }
930
- refuse(
931
- `\`${first}\` is not one of the things this command does. It does: ${offered.join(', ')}. ` +
932
- `Reading, changing and removing a row are done elsewhere, because they reach every row rather than ` +
933
- `only this one's: ${elsewhere.join('; ')}. ` +
934
- 'Run it with none of those to see what already exists and exactly what each of them asks for. ' +
935
- 'Nothing was written, and nothing was read.',
936
- )
937
- }
938
-
939
- const ANNOUNCED: readonly { readonly what: string; readonly pattern: RegExp }[] = [
940
- { what: 'a Stripe secret key', pattern: /\bsk_(?:live|test)_[A-Za-z0-9]{16,}/ },
941
- { what: 'a Stripe restricted key', pattern: /\brk_(?:live|test)_[A-Za-z0-9]{16,}/ },
942
- { what: 'a GitHub token', pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{30,}/ },
943
- { what: 'a GitHub personal access token', pattern: /\bgithub_pat_[A-Za-z0-9_]{30,}/ },
944
- { what: 'an AWS access key id', pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/ },
945
- { what: 'a Slack token', pattern: /\bxox[abposr]-[A-Za-z0-9-]{10,}/ },
946
- { what: 'an OpenAI key', pattern: /\bsk-[A-Za-z0-9_-]{32,}/ },
947
- { what: 'an Anthropic key', pattern: /\bsk-ant-[A-Za-z0-9_-]{20,}/ },
948
- { what: 'a Google API key', pattern: /\bAIza[A-Za-z0-9_-]{35}\b/ },
949
- { what: 'a JSON web token', pattern: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
950
- { what: 'a private key', pattern: /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/ },
951
- { what: 'a password inside a connection string', pattern: /\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:[^\s:/@]{6,}@/ },
952
- ]
953
-
954
- 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
955
-
956
- const MIXED = /^(?=.*[a-z])(?=.*[A-Z0-9])[A-Za-z0-9_+/=.-]{16,}$/
957
-
958
- const NOT = [
959
- /^(?:sec|pub)_[a-z0-9_]+$/,
960
- /^\$\{/,
961
- /^(?:x{3,}|X{3,}|\.{3,}|<[^>]+>|change[-_]?me|your[-_]|example|placeholder|redacted|todo|test|dummy|sample|fake|dGhpcyBpcyBub3QgYSBzZWNyZXQ)/i,
962
- ]
963
-
964
- function guarded(key: string, value: string): string {
965
- const carried = credentialIn(value)
966
- if (carried !== undefined) {
967
- refuse(
968
- `\`${key}\` holds ${carried}. A credential is never typed into a declaration — not in a describes, not ` +
969
- 'anywhere but a secret. Declare it with `thread create secret`, name it where the thing that needs it ' +
970
- 'states what it requires, and it is resolved through the vault at the moment it is used and never ' +
971
- 'written down. Nothing was written.',
972
- )
973
- }
974
- const address = ADDRESS.exec(value)
975
- if (address !== null) {
976
- refuse(
977
- `\`${key}\` holds the address \`${address[0]}\`. An address is a secret here, not a sentence — the same ` +
978
- 'thing behind another host is another environment, and one written into a declaration is one nothing ' +
979
- 'can point elsewhere. Declare it with `thread create secret` and name it where the thing that needs it ' +
980
- 'states what it requires. Nothing was written.',
981
- )
982
- }
983
- return value
984
- }
985
-
986
- const ADDRESS = /\b[a-z][a-z0-9+.-]*:\/\/[^\s'"`)]+/i
987
-
988
- export function credentialIn(text: string): string | undefined {
989
- for (const one of ANNOUNCED) {
990
- if (one.pattern.test(text)) return one.what
991
- }
992
- for (const found of text.matchAll(ASSIGNED)) {
993
- const value = found[2] ?? ''
994
- if (NOT.some((each) => each.test(value))) continue
995
- if (!MIXED.test(value)) continue
996
- return `a ${(found[0].split(/[:=]/)[0] ?? '').trim()} written down as a literal ${String(value.length)} characters long`
997
- }
998
- return undefined
999
- }