@prestack/thread 0.0.1 → 0.0.2

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 (150) hide show
  1. package/dist/core/actor.ts +20 -0
  2. package/dist/core/asking.d.ts +0 -1
  3. package/dist/core/asking.js +0 -1
  4. package/dist/core/asking.ts +1342 -0
  5. package/dist/core/attach.js +2 -2
  6. package/dist/core/attach.ts +585 -0
  7. package/dist/core/chain.js +8 -1
  8. package/dist/core/chain.ts +148 -0
  9. package/dist/core/cli.js +97 -18
  10. package/dist/core/cli.ts +2004 -0
  11. package/dist/core/condition.ts +227 -0
  12. package/dist/core/descend.d.ts +1 -0
  13. package/dist/core/descend.js +36 -0
  14. package/dist/core/descend.ts +1001 -0
  15. package/dist/core/finding.ts +38 -0
  16. package/dist/core/generators/meta.js +6 -0
  17. package/dist/core/generators/meta.ts +1006 -0
  18. package/dist/core/init.ts +170 -0
  19. package/dist/core/invoked.ts +47 -0
  20. package/dist/core/round.ts +57 -0
  21. package/dist/core/rows.ts +369 -0
  22. package/dist/core/scope.ts +18 -0
  23. package/dist/core/thread.ts +29 -0
  24. package/dist/core/tree.ts +67 -0
  25. package/dist/core/types.ts +350 -0
  26. package/dist/derivation/attach.js +1 -1
  27. package/dist/derivation/attach.ts +68 -0
  28. package/dist/derivation/generators/build.ts +232 -0
  29. package/dist/derivation/generators/check.ts +118 -0
  30. package/dist/derivation/generators/derivation.ts +347 -0
  31. package/dist/derivation/generators/determinism.ts +145 -0
  32. package/dist/derivation/generators/release.js +19 -9
  33. package/dist/derivation/generators/release.ts +213 -0
  34. package/dist/derivation/makers/release.ts +135 -0
  35. package/dist/derivation/release.ts +23 -0
  36. package/dist/integrations/attach.js +2 -2
  37. package/dist/integrations/attach.ts +207 -0
  38. package/dist/integrations/generators/read.ts +127 -0
  39. package/dist/integrations/generators/secret.ts +289 -0
  40. package/dist/integrations/generators/target.js +16 -6
  41. package/dist/integrations/generators/target.ts +944 -0
  42. package/dist/integrations/generators/vault.js +46 -14
  43. package/dist/integrations/generators/vault.ts +212 -0
  44. package/dist/integrations/{sdk.js → module.js} +4 -4
  45. package/dist/integrations/module.ts +189 -0
  46. package/dist/integrations/secret.ts +50 -0
  47. package/dist/integrations/target.d.ts +3 -1
  48. package/dist/integrations/target.js +14 -11
  49. package/dist/integrations/target.ts +340 -0
  50. package/dist/integrations/vault.d.ts +2 -0
  51. package/dist/integrations/vault.js +4 -2
  52. package/dist/integrations/vault.ts +136 -0
  53. package/dist/integrity/attach.ts +234 -0
  54. package/dist/integrity/check-threads.js +2 -26
  55. package/dist/integrity/check-threads.ts +166 -0
  56. package/dist/integrity/constraint.ts +1202 -0
  57. package/dist/integrity/generators/compiler.ts +161 -0
  58. package/dist/integrity/generators/gate.ts +201 -0
  59. package/dist/integrity/generators/issues.ts +160 -0
  60. package/dist/integrity/generators/publish.js +14 -5
  61. package/dist/integrity/generators/publish.ts +247 -0
  62. package/dist/integrity/generators/rule.ts +271 -0
  63. package/dist/integrity/generators/seal.ts +117 -0
  64. package/dist/integrity/generators/trial.ts +320 -0
  65. package/dist/integrity/lint.ts +227 -0
  66. package/dist/integrity/lsp/bin.ts +42 -0
  67. package/dist/integrity/lsp/diagnostics.ts +92 -0
  68. package/dist/integrity/lsp/findings.ts +63 -0
  69. package/dist/integrity/lsp/lint.ts +140 -0
  70. package/dist/integrity/lsp/protocol.ts +78 -0
  71. package/dist/integrity/lsp/server.ts +224 -0
  72. package/dist/integrity/sealed.js +2 -2
  73. package/dist/integrity/sealed.ts +56 -0
  74. package/dist/integrity/slopnator.ts +350 -0
  75. package/dist/integrity/slopnators/sl_broken_lineage.ts +36 -0
  76. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +39 -0
  77. package/dist/integrity/slopnators/sl_derived_drift.ts +98 -0
  78. package/dist/integrity/slopnators/sl_hardcoded_style.ts +55 -0
  79. package/dist/integrity/slopnators/sl_inert_island.ts +91 -0
  80. package/dist/integrity/slopnators/sl_inert_slopnator.ts +93 -0
  81. package/dist/integrity/slopnators/sl_missing_row.ts +182 -0
  82. package/dist/integrity/slopnators/sl_no_barrel.ts +48 -0
  83. package/dist/integrity/slopnators/sl_no_child_process.ts +74 -0
  84. package/dist/integrity/slopnators/sl_no_comment.ts +43 -0
  85. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +328 -0
  86. package/dist/integrity/slopnators/sl_no_import_cycle.ts +50 -0
  87. package/dist/integrity/slopnators/sl_no_such_command.ts +64 -0
  88. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +63 -0
  89. package/dist/integrity/slopnators/sl_secret_literal.ts +100 -0
  90. package/dist/integrity/slopnators/sl_stale_path.ts +94 -0
  91. package/dist/integrity/slopnators/sl_twice_meant.ts +31 -0
  92. package/dist/integrity/slopnators/sl_twice_parented.ts +37 -0
  93. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +87 -0
  94. package/dist/integrity/slopnators/sl_undeclared_destination.ts +65 -0
  95. package/dist/integrity/slopnators/sl_undeclared_element.ts +44 -0
  96. package/dist/integrity/slopnators/sl_undeclared_node.ts +69 -0
  97. package/dist/integrity/slopnators/sl_undeclared_operation.ts +147 -0
  98. package/dist/integrity/slopnators/sl_undeclared_variant.ts +148 -0
  99. package/dist/integrity/slopnators/sl_undescribed.ts +65 -0
  100. package/dist/integrity/slopnators/sl_ungoverned_island.ts +61 -0
  101. package/dist/integrity/slopnators/sl_unkept_rows.ts +111 -0
  102. package/dist/integrity/slopnators/sl_unnamed_island.ts +41 -0
  103. package/dist/integrity/slopnators/sl_unpaired_member.ts +131 -0
  104. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +58 -0
  105. package/dist/integrity/slopnators/sl_unprefixed_member.ts +136 -0
  106. package/dist/integrity/slopnators/sl_unseamed_reach.ts +79 -0
  107. package/dist/integrity/slopnators/sl_unthreaded.ts +419 -0
  108. package/dist/integrity/slopnators/sl_untranslated_key.ts +164 -0
  109. package/dist/integrity/slopnators/sl_unversioned.ts +36 -0
  110. package/dist/integrity/sweep.ts +307 -0
  111. package/dist/knowledge/attach.ts +350 -0
  112. package/dist/knowledge/generators/docsgen.ts +174 -0
  113. package/dist/knowledge/generators/glossary.ts +73 -0
  114. package/dist/knowledge/generators/skills.ts +263 -0
  115. package/dist/knowledge/glossary.ts +6 -0
  116. package/dist/operations/access.ts +88 -0
  117. package/dist/operations/attach.ts +475 -0
  118. package/dist/operations/capabilities.ts +65 -0
  119. package/dist/operations/cost.ts +58 -0
  120. package/dist/operations/dispatch.ts +348 -0
  121. package/dist/operations/error-results.ts +31 -0
  122. package/dist/operations/errors/catalog.ts +168 -0
  123. package/dist/operations/errors/classes.ts +51 -0
  124. package/dist/operations/errors/errors.ts +314 -0
  125. package/dist/operations/generators/capability.js +29 -19
  126. package/dist/operations/generators/capability.ts +704 -0
  127. package/dist/operations/generators/compile.js +20 -1
  128. package/dist/operations/generators/compile.ts +643 -0
  129. package/dist/operations/generators/grant.ts +336 -0
  130. package/dist/operations/generators/operation.ts +972 -0
  131. package/dist/operations/generators/permits.ts +223 -0
  132. package/dist/operations/generators/seed.ts +347 -0
  133. package/dist/operations/generators/trial.js +4 -1
  134. package/dist/operations/generators/trial.ts +529 -0
  135. package/dist/operations/http.ts +164 -0
  136. package/dist/operations/makers/capability.ts +166 -0
  137. package/dist/operations/makers/trial.ts +520 -0
  138. package/dist/operations/memory.ts +470 -0
  139. package/dist/operations/metering.ts +328 -0
  140. package/dist/operations/nodes.ts +54 -0
  141. package/dist/operations/operations.ts +32 -0
  142. package/dist/operations/queue-policy.ts +10 -0
  143. package/dist/operations/result.ts +28 -0
  144. package/dist/operations/seams.d.ts +4 -4
  145. package/dist/operations/seams.ts +364 -0
  146. package/dist/operations/store.ts +29 -0
  147. package/package.json +8 -1
  148. package/dist/integrations/generators/sdk.d.ts +0 -1
  149. package/dist/integrations/generators/sdk.js +0 -148
  150. /package/dist/integrations/{sdk.d.ts → module.d.ts} +0 -0
@@ -0,0 +1,944 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import { readFileSync, rmSync, writeFileSync } from 'node:fs'
3
+ import { join, relative } from 'node:path'
4
+ import { argv, stdout } from 'node:process'
5
+
6
+ import ts from 'typescript'
7
+
8
+ import type { Env } from '../../core/actor.ts'
9
+ import { exit } from '../../core/invoked.ts'
10
+ import { ELSEWHERE, RECEIPT, answer, asking, asksOf, given, issue, needed, only, ownAnswers, refuse, rowLines, seeding, slug, spend, stating, wordsIn } from '../../core/asking.ts'
11
+ import { holdsRow, listBounds, rowsIn, showing, withRow, withoutRow } from '../../core/rows.ts'
12
+ import { named as packageOf } from '../../core/scope.ts'
13
+ import { findTree, inTree } from '../../core/tree.ts'
14
+
15
+ import type { Secret } from '../secret.ts'
16
+ import { secrecyOf } from '../secret.ts'
17
+ import {
18
+ CALL_PREFIX,
19
+ HOLDS,
20
+ REACH,
21
+ SAID,
22
+ addressOf,
23
+ credentialsOf,
24
+ fills,
25
+ holdsOf,
26
+ misnamed,
27
+ reachOf,
28
+ refusals,
29
+ type Call,
30
+ type Target,
31
+ } from '../target.ts'
32
+ import { opened } from '../vault.ts'
33
+
34
+ const TREE = findTree()
35
+
36
+ const FILE = 'targets.ts'
37
+
38
+ const TARGET_LIST = 'TARGETS'
39
+
40
+ const CALL_LIST = 'CALLS'
41
+
42
+ const NAME = /^[a-z][a-z0-9_]*$/
43
+
44
+ const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const
45
+
46
+ const WORDS = Object.keys(REACH) as (keyof typeof REACH)[]
47
+
48
+ const ASKS: readonly { readonly key: string; readonly says: readonly string[]; readonly edits?: true }[] = [
49
+ { key: 'name', says: ['lower_snake_case, and the prefix is added for you'] },
50
+ { key: 'reach', says: [`how it is reached — ${WORDS.join(', ')}`] },
51
+ {
52
+ key: 'describes',
53
+ edits: true,
54
+ says: [
55
+ 'what it is for, in one sentence, and it has to be yours — blank is',
56
+ 'refused, because copying the reach paragraph above would give two',
57
+ 'targets the same meaning',
58
+ ],
59
+ },
60
+ {
61
+ key: 'uses',
62
+ edits: true,
63
+ says: [
64
+ 'ONLY for module and cli — the npm package to install, or the',
65
+ 'program to run. Not asked for the other four, and refused on them',
66
+ ],
67
+ },
68
+ {
69
+ key: 'address',
70
+ edits: true,
71
+ says: ['the id of a secret with no `sends`. Blank only for a reach', 'that is addressed by nothing'],
72
+ },
73
+ { key: 'credentials', edits: true, says: ['comma-separated ids of secrets that DO have a `sends`'] },
74
+ {
75
+ key: 'accepts',
76
+ edits: true,
77
+ says: [
78
+ 'comma-separated — entity ids for a place that keeps things, call',
79
+ 'ids for one that is called. For module and cli it is what the thing',
80
+ 'can do, read off its published types and offered to you. Usually',
81
+ 'left blank here and filled by `call`, by `thread entity`, or by `edit`',
82
+ ],
83
+ },
84
+ ]
85
+
86
+ const TAKES: readonly string[] = ASKS.map((one) => one.key)
87
+
88
+ const EDITS: readonly string[] = ASKS.filter((one) => one.edits === true).map((one) => one.key)
89
+
90
+ const ASKED: readonly string[] = ASKS.flatMap((one) =>
91
+ one.says.map((line, at) => ` ${at === 0 ? `${one.key}:`.padEnd(32) : ' '.repeat(32)}${line}`),
92
+ )
93
+
94
+ const CALLS: readonly { readonly key: string; readonly says: readonly string[] }[] = [
95
+ {
96
+ key: 'of',
97
+ says: ['which target the call belongs to — not needed when it is', 'named as <target> on the command line'],
98
+ },
99
+ { key: 'name', says: ['lower_snake_case; the id becomes cl_<name>'] },
100
+ { key: 'describes', says: ['what it does, in one sentence'] },
101
+ { key: 'method', says: [`one of ${METHODS.join(', ')}`] },
102
+ {
103
+ key: 'path',
104
+ says: [
105
+ 'under the target address, leading slash and all. Anything that',
106
+ 'varies is written {like_this}',
107
+ ],
108
+ },
109
+ ]
110
+
111
+ const MAKES: readonly string[] = CALLS.map((one) => one.key)
112
+
113
+ const CALLED: readonly string[] = CALLS.flatMap((one) =>
114
+ one.says.map((line, at) => ` ${at === 0 ? `${one.key}:`.padEnd(32) : ' '.repeat(32)}${line}`),
115
+ )
116
+
117
+ const PRODUCTION = '--production'
118
+
119
+ const here = (one: string): string => relative(TREE.at, one).split('\\').join('/')
120
+
121
+ const path = inTree(TREE, FILE)
122
+
123
+ const at = here(path)
124
+
125
+ import { quoted } from '../../core/rows.ts'
126
+
127
+ let targets: readonly Target[] = []
128
+ let calls: readonly Call[] = []
129
+ try {
130
+ const taken = (await import(new URL(`file://${path}`).href)) as {
131
+ TARGETS?: readonly Target[]
132
+ CALLS?: readonly Call[]
133
+ }
134
+ targets = taken.TARGETS ?? []
135
+ calls = taken.CALLS ?? []
136
+ } catch {
137
+ refuse(`${at} could not be read. Attach \`${packageOf('integrations')}\` first — an app states where it reaches there.`)
138
+ }
139
+
140
+ let secrets: readonly Secret[] = []
141
+ try {
142
+ const taken = (await import(new URL(`file://${inTree(TREE, 'secrets.ts')}`).href)) as { SECRETS?: readonly Secret[] }
143
+ secrets = taken.SECRETS ?? []
144
+ } catch {
145
+ secrets = []
146
+ }
147
+
148
+ let entities: readonly string[] = []
149
+ try {
150
+ const taken = (await import(new URL(`file://${inTree(TREE, 'entities.ts')}`).href)) as {
151
+ ENTITIES?: readonly { readonly id: string }[]
152
+ }
153
+ entities = (taken.ENTITIES ?? []).map((one) => one.id)
154
+ } catch {
155
+ entities = []
156
+ }
157
+
158
+ const holdingOf = (id: string): string => {
159
+ const holds = holdsOf(id)
160
+ if (holds === undefined) return 'nothing — its id wears no prefix a target is reached by'
161
+ const yes = [
162
+ ...(holds.migrations ? ['migrations'] : []),
163
+ ...(holds.references ? ['references'] : []),
164
+ ]
165
+ return `accepts ${holds.takes}, reached from ${holds.from}, ${yes.length === 0 ? 'holds no enforcement of its own' : `holds ${yes.join(', ')}`}`
166
+ }
167
+
168
+ const requiredBy = (one: Target): string => {
169
+ if (one.requires.length === 0) return 'nothing, so nothing says where it is'
170
+ const address = addressOf(one, secrets)
171
+ return one.requires
172
+ .map((each) => `${each}${each === address?.id ? ' (its address)' : ''}`)
173
+ .join(', ')
174
+ }
175
+
176
+ const listing = [
177
+ targets.length === 0
178
+ ? 'no targets yet.'
179
+ : [
180
+ `${String(targets.length)} target(s):`,
181
+ ...targets.flatMap((one) => [
182
+ ` ${one.id} — ${holdingOf(one.id)}`,
183
+
184
+ ` accepts ${[...one.takes, ...(one.offers ?? [])].join(', ') || 'nothing'}`,
185
+ ` requires ${requiredBy(one)}`,
186
+ ]),
187
+ ].join('\n'),
188
+ calls.length === 0
189
+ ? 'no calls yet.'
190
+ : [
191
+ `${String(calls.length)} call(s):`,
192
+ ...calls.map((one) => ` ${one.id} — ${one.method} ${one.path} on ${one.of}`),
193
+ ].join('\n'),
194
+ secrets.length === 0
195
+ ? 'no secrets yet — declare one with `thread create secret` before a target can require it.'
196
+ : [
197
+ `${String(secrets.length)} secret(s) a target may require:`,
198
+ ...secrets.map(
199
+ (one) =>
200
+ ` ${one.id} — ${secrecyOf(one.id) ?? '?'}, ${one.sends === undefined ? 'no `sends`, so it can be an address' : `sent as ${one.sends.header}`}`,
201
+ ),
202
+ ].join('\n'),
203
+ ].join('\n\n')
204
+
205
+ const wrong = refusals(targets, secrets, calls, entities)
206
+
207
+ const words = wordsIn(['new', 'call'])
208
+
209
+ const verb = words[0]
210
+
211
+ const asCall = calls.find((one) => one.id === verb)
212
+ if (asCall !== undefined) {
213
+ const of = targets.find((one) => one.id === asCall.of)
214
+
215
+ const targetWrong = wrong.filter((each) => each.includes(asCall.id) || (of !== undefined && each.includes(of.id)))
216
+ if (targetWrong.length > 0) refuse(targetWrong.join('\n'))
217
+ if (of === undefined) refuse(`\`${asCall.of}\` is not a target this app declares`)
218
+
219
+ const address = addressOf(of, secrets)
220
+ if (address === undefined) refuse(`\`${of.id}\` names no address, and the check above should already have said so`)
221
+ const required = credentialsOf(of, secrets)
222
+
223
+ const names = [...new Set(fills(asCall))]
224
+ const writes = asCall.method === 'POST' || asCall.method === 'PUT' || asCall.method === 'PATCH'
225
+ const allowed = writes ? [...names, 'body'] : names
226
+
227
+ const piped = given()
228
+ only(piped, allowed, `\`${asCall.id}\``)
229
+ const { ask, shut } = asking(piped)
230
+
231
+ const values = new Map<string, string>()
232
+ for (const name of names) {
233
+ const value = piped.length > 0 ? needed(piped, name, `\`${asCall.id}\``) : await ask(name, `${name} (fills the path): `)
234
+ values.set(name, value.trim())
235
+ }
236
+
237
+ let body: string | undefined
238
+ if (writes) {
239
+ const typed = (piped.length > 0 ? (answer(piped, 'body') ?? '') : await ask('body', 'body, as JSON (blank for none): ')).trim()
240
+ if (typed !== '') {
241
+ try {
242
+ JSON.parse(typed)
243
+ } catch (thrown) {
244
+ shut()
245
+ refuse(`that is not JSON: ${thrown instanceof Error ? thrown.message : String(thrown)}. Nothing was called.`)
246
+ }
247
+ body = typed
248
+ }
249
+ }
250
+ shut()
251
+
252
+ let route = asCall.path
253
+ for (const name of fills(asCall)) route = route.replace(`{${name}}`, encodeURIComponent(values.get(name) ?? ''))
254
+
255
+ const env: Env = argv.includes(PRODUCTION) ? 'production' : 'sandbox'
256
+
257
+ const opening = opened(TREE.at, secrets.map((one) => one.id), env)
258
+ let resolved: Readonly<Record<string, string>>
259
+ try {
260
+ resolved = await opening(of.requires)
261
+ } catch (thrown) {
262
+ refuse(thrown instanceof Error ? thrown.message : String(thrown))
263
+ }
264
+
265
+ const host = resolved[address.id] ?? ''
266
+ const based = /^https?:\/\//.test(host) ? host : `https://${host}`
267
+ const url = `${based.replace(/\/+$/, '')}${route.startsWith('/') ? route : `/${route}`}`
268
+
269
+ const headers: Record<string, string> = {}
270
+ for (const credential of required) {
271
+ const value = resolved[credential.id]
272
+ if (value === undefined || credential.sends === undefined) continue
273
+ headers[credential.sends.header] = `${credential.sends.prefix ?? ''}${value}`
274
+ }
275
+ if (body !== undefined) headers['content-type'] = 'application/json'
276
+
277
+ let response: Response
278
+ try {
279
+ response = await fetch(url, { method: asCall.method, headers, ...(body === undefined ? {} : { body }) })
280
+ } catch (thrown) {
281
+ refuse(
282
+ `\`${asCall.id}\` — ${asCall.method} ${route} on \`${of.id}\` — threw before a response came back: ` +
283
+ `${thrown instanceof Error ? thrown.message : String(thrown)}`,
284
+ )
285
+ }
286
+
287
+ const text = await response.text()
288
+ stdout.write(`call: ${asCall.method} ${route} on \`${of.id}\` (${env}) — ${String(response.status)} ${response.statusText}\n`)
289
+ if (text.trim() !== '') {
290
+ try {
291
+ stdout.write(`${JSON.stringify(JSON.parse(text), null, 2)}\n`)
292
+ } catch {
293
+ stdout.write(`${text}\n`)
294
+ }
295
+ }
296
+ exit(response.ok ? 0 : 1)
297
+ }
298
+
299
+ if (verb !== undefined && verb.startsWith(CALL_PREFIX)) {
300
+ refuse(
301
+ `\`${verb}\` is not a call this app declares. What is here is ${calls.map((one) => one.id).join(', ') || '(nothing yet)'} — ` +
302
+ 'declare one with `thread create target call <target> <name>`.',
303
+ )
304
+ }
305
+
306
+ stdout.write(`${listing}\n`)
307
+ if (wrong.length > 0) {
308
+ stdout.write(`\n${String(wrong.length)} thing(s) this app already states that will not hold:\n`)
309
+ for (const one of wrong) stdout.write(` ${one}\n`)
310
+ }
311
+
312
+ const method = (nonce: string): string =>
313
+ [
314
+ '',
315
+ 'what this makes:',
316
+ '',
317
+ ` thread create target new <name> ${RECEIPT} ${nonce}`,
318
+ ` thread create target call <target> <name> ${RECEIPT} ${nonce}`,
319
+ ' thread run call <id> no receipt — makes an already-declared call, through',
320
+ ' the vault. Add --production to call it there; the',
321
+ ' default is sandbox.',
322
+ '',
323
+ ...ELSEWHERE,
324
+ '',
325
+ ` ${RECEIPT} is spent by one run and refused if ${at} moved after it was issued.`,
326
+ '',
327
+ 'how a target may be reached. The word is what you send; the prefix is what its id wears, and the',
328
+ 'prefix IS how it is reached — no field says so:',
329
+ '',
330
+ ...WORDS.flatMap((one) => [
331
+ ` ${one.padEnd(9)} → ${REACH[one]}<name>`,
332
+ ` accepts ${HOLDS[one].takes}, from ${HOLDS[one].from}, ${HOLDS[one].addressed ? 'addressed' : 'addressed by nothing'}`,
333
+ ` ${SAID[one]}`,
334
+ '',
335
+ ]),
336
+ '`new` asks, in this order:',
337
+ '',
338
+ ...ASKED,
339
+ ' and then, at a terminal only, for a module whose package holds resources: naming one above asks',
340
+ ' which of its methods you want, and what is written is the whole path — `customers.create`.',
341
+ ' Blank there takes every method it just listed. There is no key for it, because a piped answer',
342
+ ' names whole paths in `accepts` and never needs asking twice.',
343
+ '',
344
+ `\`edit\` takes: ${EDITS.join(', ')} — and for a module it reads the package again, so a version`,
345
+ 'that gained methods offers them without the row being retyped.',
346
+ '',
347
+ '`call` asks, in this order:',
348
+ '',
349
+ ...CALLED,
350
+ '',
351
+ 'A node then names the call. It never writes a url, a method or a secret: what a target is, where',
352
+ 'it is and what opens it are all read off these rows when the operation is compiled.',
353
+ ].join('\n')
354
+
355
+ const flagged = argv.indexOf(RECEIPT)
356
+
357
+ if (verb === undefined && flagged === -1) {
358
+ const nonce = issue(TREE.at, listing)
359
+ stdout.write(`${method(nonce)}\n`)
360
+ stdout.write(`\ntarget: receipt ${nonce} — spent on use, and stale the moment ${at} changes.\n`)
361
+ exit(0)
362
+ }
363
+
364
+ spend(TREE.at, listing, argv[flagged + 1], at)
365
+
366
+ if (verb !== 'new' && verb !== 'call') {
367
+ refuse(`there is nothing called "${verb ?? ''}" to do here — say new, or call`)
368
+ }
369
+
370
+ const text = readFileSync(path, 'utf8')
371
+
372
+ const listed = (one: string | undefined): readonly string[] =>
373
+ (one ?? '')
374
+ .split(',')
375
+ .map((each) => each.trim())
376
+ .filter((each) => each !== '')
377
+
378
+ if (verb === 'call') {
379
+ const piped = given()
380
+ only(piped, MAKES, 'a call')
381
+ const { ask, shut } = asking(piped)
382
+
383
+ const of = (words[1] ?? (await ask('of', `which target — ${targets.map((one) => one.id).join(', ') || '(none yet)'}: `))).trim()
384
+ const target = targets.find((one) => one.id === of)
385
+ if (target === undefined) {
386
+ shut()
387
+ refuse(`this app declares no target \`${of}\`. A call belongs to one place, and that place is declared first.`)
388
+ }
389
+ if (holdsOf(target.id)?.takes !== 'calls') {
390
+ shut()
391
+ refuse(
392
+ `\`${target.id}\` is reached over ${String(reachOf(target.id))} and keeps entities. A place you keep rows in is ` +
393
+ 'not a place you issue verbs at.',
394
+ )
395
+ }
396
+
397
+ const name = slug(words[2] ?? (await ask('name', 'name: ')))
398
+ if (name === '' || !NAME.test(name)) {
399
+ shut()
400
+ refuse(`"${name}" cannot be the name of a call — it is written in lower_snake_case`)
401
+ }
402
+ const id = `${CALL_PREFIX}${name}`
403
+ if (calls.some((one) => one.id === id)) {
404
+ shut()
405
+ refuse(`\`${id}\` is already declared in ${at}`)
406
+ }
407
+
408
+ const describes = (piped.length > 0 ? needed(piped, 'describes', 'a call') : await ask('describes', 'what does it do? ')).trim()
409
+ if (describes === '') {
410
+ shut()
411
+ refuse('a call says what it does — the sentence is what somebody reads before making it')
412
+ }
413
+
414
+ const verbWord = (piped.length > 0 ? needed(piped, 'method', 'a call') : await ask('method', `method — ${METHODS.join(', ')}: `))
415
+ .trim()
416
+ .toUpperCase()
417
+ if (!(METHODS as readonly string[]).includes(verbWord)) {
418
+ shut()
419
+ refuse(`there is no such method as "${verbWord}" — it is one of ${METHODS.join(', ')}`)
420
+ }
421
+
422
+ const route = (piped.length > 0 ? needed(piped, 'path', 'a call') : await ask('path', 'path, leading slash and all: ')).trim()
423
+ shut()
424
+ if (!route.startsWith('/')) refuse(`"${route}" is not a path under an address — it opens with a slash`)
425
+
426
+ const callWhere = listBounds(text, CALL_LIST)
427
+ if (callWhere === undefined) refuse(`${at} states no \`${CALL_LIST}\` list to declare a call in`)
428
+
429
+ const row = [
430
+ ' {',
431
+ ` id: ${quoted(id)},`,
432
+ ` at: '_threads/${FILE}',`,
433
+ ` describes: ${quoted(describes)},`,
434
+ ' version: 1,',
435
+ ` of: ${quoted(target.id)},`,
436
+ ` method: ${quoted(verbWord)},`,
437
+ ` path: ${quoted(route)},`,
438
+ ' },',
439
+ ].join('\n')
440
+
441
+ const withCall = withRow(text, callWhere, row)
442
+
443
+ const targetWhere = listBounds(withCall, TARGET_LIST)
444
+ if (targetWhere === undefined) refuse(`${at} states no \`${TARGET_LIST}\` list`)
445
+ const taking = [...target.takes, id]
446
+ const replaced = withCall.replace(
447
+ new RegExp(`(id: '${target.id}',[\\s\\S]*?takes: )\\[[^\\]]*\\]`),
448
+ `$1[${taking.map((one) => quoted(one)).join(', ')}]`,
449
+ )
450
+ if (replaced === withCall) {
451
+ refuse(`\`${target.id}\` has no \`takes\` this could add \`${id}\` to. Nothing was written.`)
452
+ }
453
+ writeFileSync(path, replaced, 'utf8')
454
+
455
+ stdout.write(`target: declared ${id} — ${verbWord} ${route} on ${target.id}, and ${target.id} now accepts it.\n`)
456
+ exit(0)
457
+ }
458
+
459
+ const piped = given()
460
+ const own = ownAnswers(piped)
461
+ only(own, TAKES, 'a target')
462
+ const { ask, line, shut } = asking(piped)
463
+
464
+ const name = slug(words[1] ?? (await ask('name', 'name: ')))
465
+ if (name === '') {
466
+ shut()
467
+ refuse('a target has to be called something')
468
+ }
469
+ if (!NAME.test(name)) {
470
+ shut()
471
+ refuse(`"${name}" cannot be the name of a target — it is written in lower_snake_case`)
472
+ }
473
+
474
+ const reach = (piped.length > 0 ? needed(own, 'reach', 'a target') : await ask('reach', `how is it reached — ${WORDS.join(', ')}: `)).trim()
475
+ if (!WORDS.includes(reach as keyof typeof REACH)) {
476
+ shut()
477
+ refuse(`there is no such reach as "${reach}". A target is reached ${WORDS.join(', ')}.`)
478
+ }
479
+ const word = reach as keyof typeof REACH
480
+ const id = `${REACH[word]}${name}`
481
+
482
+ if (targets.some((one) => one.id === id)) {
483
+ shut()
484
+ refuse(`\`${id}\` is already declared in ${at}`)
485
+ }
486
+
487
+ const answered = (piped.length > 0 ? (answer(own, 'describes') ?? '') : await ask('describes', 'what is it for? ')).trim()
488
+
489
+ if (answered === '') {
490
+ shut()
491
+ refuse(
492
+ `a target says what IT is, and blank would have copied the ${word} paragraph above onto this row — so ` +
493
+ `\`${REACH[word]}${name}\` would carry a sentence about ${word} reaches in general and nothing about this one. ` +
494
+ 'Say what it is to somebody who has not seen it: what this app reaches through it, and what for.',
495
+ )
496
+ }
497
+ const describes = answered
498
+
499
+ const PACKAGE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/
500
+ const PROGRAM = /^[a-z][a-z0-9._-]*$/
501
+
502
+ const arrives = HOLDS[word].arrives
503
+ const uses = (
504
+ arrives === null
505
+ ? ''
506
+ : piped.length > 0
507
+ ? needed(own, 'uses', `a target reached as ${word}`)
508
+ : await ask(
509
+ 'uses',
510
+ arrives === 'npm'
511
+ ? 'npm package to install — the name `npm install` is given: '
512
+ : 'program to run — the name you would type at a shell, e.g. `git`: ',
513
+ )
514
+ ).trim()
515
+
516
+ if (arrives !== null) {
517
+ if (uses === '') {
518
+ shut()
519
+ refuse(
520
+ arrives === 'npm'
521
+ ? 'a module installs something — the npm package is what `npm install` is given'
522
+ : 'a command runs something — the program is what you would type at a shell',
523
+ )
524
+ }
525
+ if (!(arrives === 'npm' ? PACKAGE : PROGRAM).test(uses)) {
526
+ shut()
527
+ refuse(`"${uses}" does not read as ${arrives === 'npm' ? 'an npm package name' : 'a program name'}`)
528
+ }
529
+ }
530
+
531
+ if (arrives === 'npm') {
532
+ stdout.write(`target: installing ${uses} …\n`)
533
+ const got = spawnSync('npm', ['install', uses], { cwd: TREE.at, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
534
+ if (got.status !== 0) {
535
+ shut()
536
+ refuse(`\`npm install ${uses}\` failed, so nothing was declared:\n${`${got.stdout ?? ''}${got.stderr ?? ''}`.trim()}`)
537
+ }
538
+ stdout.write(`target: installed ${uses}.\n`)
539
+ } else if (arrives === 'machine') {
540
+ const found = spawnSync(uses, ['--version'], { cwd: TREE.at, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
541
+ if (found.error !== undefined || found.status === null) {
542
+ shut()
543
+ refuse(
544
+ `\`${uses}\` is not on this machine, so there is nothing to declare. It is a program rather than a dependency ` +
545
+ '— install it the way this machine installs programs and run this again. Nothing was written.',
546
+ )
547
+ }
548
+ }
549
+
550
+ const driver = HOLDS[word].driver
551
+
552
+ if (driver !== null) {
553
+ stdout.write(`target: installing ${driver} …\n`)
554
+ const opened = spawnSync('npm', ['install', driver], { cwd: TREE.at, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
555
+ if (opened.status !== 0) {
556
+ shut()
557
+ refuse(`npm install ${driver} failed, so nothing was declared. A ${word} target is opened with ${driver}, and the store written for it imports that package:\n${`${opened.stdout ?? ''}${opened.stderr ?? ''}`.trim()}`)
558
+ }
559
+ stdout.write(`target: installed ${driver} — what a ${word} target is opened with.\n`)
560
+ }
561
+
562
+ let offered: readonly string[] = []
563
+ let deeper: ReadonlyMap<string, readonly string[]> = new Map()
564
+ if (HOLDS[word].takes === 'names' && arrives === 'npm') {
565
+ const read = surfaceOf(uses, id)
566
+ offered = read.offered
567
+ deeper = read.deeper
568
+ stdout.write(
569
+ offered.length === 0
570
+ ? `target: ${uses} publishes no types this could read, so name what it can do yourself.\n`
571
+ : `target: read ${String(offered.length)} name(s) off \`${uses}\`, ${String(deeper.size)} of which hold methods.\n`,
572
+ )
573
+ }
574
+
575
+ const PROBES: readonly (readonly string[])[] = [
576
+ ['help', '-a'],
577
+ ['--help'],
578
+ ['help'],
579
+ ]
580
+
581
+ const COLUMN = /^\s{2,}([a-z][a-z0-9-]*):?\s{2,}\S/
582
+
583
+ const LISTED = /^\s{2,}[a-z][a-z0-9-]+(?:,\s*[a-z][a-z0-9-]+)*,?\s*$/
584
+
585
+ const readOff = (text: string): readonly string[] => {
586
+ const found = new Set<string>()
587
+ for (const line of text.split('\n')) {
588
+ const column = COLUMN.exec(line)
589
+ if (column?.[1] !== undefined) {
590
+ found.add(column[1])
591
+ continue
592
+ }
593
+ if (!LISTED.test(line)) continue
594
+ for (const each of line.split(',')) {
595
+ const one = each.trim()
596
+ if (/^[a-z][a-z0-9-]*$/.test(one)) found.add(one)
597
+ }
598
+ }
599
+ return [...found]
600
+ }
601
+
602
+ if (HOLDS[word].takes === 'names' && arrives === 'machine') {
603
+ for (const probe of PROBES) {
604
+ const asked = spawnSync(uses, [...probe], { cwd: TREE.at, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
605
+
606
+ const said = readOff(`${asked.stdout ?? ''}\n${asked.stderr ?? ''}`)
607
+ if (said.length < 3) continue
608
+ offered = said.filter((one) => misnamed(id, one) === undefined).sort()
609
+ stdout.write(`target: read ${String(offered.length)} command(s) off \`${uses} ${probe.join(' ')}\`.\n`)
610
+ break
611
+ }
612
+ }
613
+
614
+ const addresses = secrets.filter((one) => one.sends === undefined)
615
+ const credentials = secrets.filter((one) => one.sends !== undefined)
616
+
617
+ const address = HOLDS[word].addressed
618
+ ? (piped.length > 0
619
+ ? needed(own, 'address', 'a target that is reached somewhere')
620
+ : await ask('address', `address — one of ${addresses.map((one) => one.id).join(', ') || '(declare one with `thread create secret`)'}: `)
621
+ ).trim()
622
+ : ''
623
+
624
+ if (HOLDS[word].addressed) {
625
+ const found = secrets.find((one) => one.id === address)
626
+ if (found === undefined) {
627
+ shut()
628
+ refuse(
629
+ `\`${address}\` is not a secret this app declares. A target reached over ${word} needs one saying where it is — ` +
630
+ 'declare it with `thread create secret`, leaving the header blank.',
631
+ )
632
+ }
633
+ if (found.sends !== undefined) {
634
+ shut()
635
+ refuse(
636
+ `\`${address}\` has a \`sends\`, so it is a credential and not an address — it goes into the ` +
637
+ `\`${found.sends.header}\` header on every call. An address is the one secret with nowhere to be sent.`,
638
+ )
639
+ }
640
+ } else if (address !== '') {
641
+ shut()
642
+ refuse(`a target reached over ${word} is addressed by nothing, and \`${address}\` was offered as its address`)
643
+ }
644
+
645
+ const carrying = listed(piped.length > 0 ? answer(own, 'credentials') : await ask('credentials', `credentials, comma-separated — ${credentials.map((one) => one.id).join(', ') || '(none declared)'}: `))
646
+ for (const one of carrying) {
647
+ const found = secrets.find((each) => each.id === one)
648
+ if (found === undefined) {
649
+ shut()
650
+ refuse(`\`${one}\` is not a secret this app declares`)
651
+ }
652
+
653
+ if (HOLDS[word].addressed && found.sends === undefined) {
654
+ shut()
655
+ refuse(`\`${one}\` has no \`sends\`, so it is an address rather than something to send. One target, one address.`)
656
+ }
657
+ }
658
+
659
+ const asked =
660
+ HOLDS[word].takes === 'names'
661
+ ? offered.length === 0
662
+ ? 'what it can do, comma-separated — nothing was readable off it, so name them: '
663
+ : deeper.size === 0
664
+ ? `what it can do, comma-separated (blank takes all ${String(offered.length)}):\n ${offered.join(', ')}\n > `
665
+ : `what it can do, comma-separated (blank takes none). ${String(deeper.size)} of these hold methods, and naming ` +
666
+ `one asks which of them you want; the rest are called directly:\n ${offered.join(', ')}\n > `
667
+ : `accepts, comma-separated ${HOLDS[word].takes} (blank for none yet): `
668
+
669
+ const named = listed(piped.length > 0 ? answer(own, 'accepts') : await ask('accepts', asked))
670
+ const wide = named.length === 0 && HOLDS[word].takes === 'names' && deeper.size === 0 ? offered : named
671
+
672
+ const accepts: string[] = []
673
+ for (const one of wide) {
674
+ const beneath = deeper.get(one)
675
+ if (beneath === undefined) {
676
+ accepts.push(one)
677
+ continue
678
+ }
679
+ const picked = listed(
680
+ await ask(
681
+ `accepts_${one}`,
682
+ `what may run on \`${one}\` (blank takes all ${String(beneath.length)}):\n ${beneath.join(', ')}\n > `,
683
+ ),
684
+ )
685
+ for (const each of picked.length === 0 ? beneath : picked) accepts.push(`${one}.${each}`)
686
+ }
687
+
688
+ for (const one of accepts) {
689
+ const wrong = misnamed(id, one)
690
+ if (wrong !== undefined) {
691
+ shut()
692
+ refuse(wrong)
693
+ }
694
+ }
695
+
696
+ if (accepts.length > 0 && HOLDS[word].takes === 'entities') {
697
+ shut()
698
+ refuse(
699
+ `a target keeps entities, and which entities it keeps is written by \`thread create entity\` — it is the ` +
700
+ `command that also writes what this target then derives: its schema, its migrations, its reference. Naming ` +
701
+ `${accepts.map((one) => `\`${one}\``).join(', ')} here would put ${accepts.length === 1 ? 'it' : 'them'} in ` +
702
+ 'the target and leave those rows unwritten, so `thread schema` would refuse and ask you to edit a sheet by ' +
703
+ 'hand. Declare the target with `accepts` blank, then run `thread create entity` — a new one asks where it is ' +
704
+ 'kept, and one that already exists is offered a home by `thread create entity edit`. Nothing was written.',
705
+ )
706
+ }
707
+
708
+
709
+ const { stated, kids } = await stating(asksOf('target', 'Target', {}, ['id', 'at', 'describes', 'version', 'uses', 'offers', 'takes', 'requires', 'accepts']), piped, piped.length > 0, ask, line, [])
710
+
711
+ shut()
712
+
713
+ const requires = [...(address === '' ? [] : [address]), ...carrying]
714
+
715
+ const where = listBounds(text, TARGET_LIST)
716
+ if (where === undefined) refuse(`${at} states no \`${TARGET_LIST}\` list to declare a target in`)
717
+ if (holdsRow(text, where, id)) refuse(`\`${id}\` is already declared in ${at}`)
718
+
719
+ const row = [
720
+ ' {',
721
+ ` id: ${quoted(id)},`,
722
+ ` at: '_threads/${FILE}',`,
723
+ ` describes: ${quoted(describes)},`,
724
+ ' version: 1,',
725
+
726
+ ...(uses === '' ? [] : [` uses: ${quoted(uses)},`]),
727
+
728
+ ...(HOLDS[word].takes === 'names'
729
+ ? [` offers: [${accepts.map((one) => quoted(one)).join(', ')}],`, ' takes: [],']
730
+ : [` takes: [${accepts.map((one) => quoted(one)).join(', ')}],`]),
731
+ ` requires: [${requires.map((one) => quoted(one)).join(', ')}],`,
732
+ ...rowLines(' ', stated, kids),
733
+ ' },',
734
+ ].join('\n')
735
+
736
+ writeFileSync(path, withRow(text, where, row), 'utf8')
737
+
738
+ for (const told of seeding(kids, TREE.at)) stdout.write(`target: ${told}\n`)
739
+
740
+ if (HOLDS[word].takes === 'names') grows(id, name, uses, accepts)
741
+
742
+ const after = refusals(
743
+ [
744
+ ...targets,
745
+ {
746
+ id,
747
+ at: `_threads/${FILE}`,
748
+ describes,
749
+ version: 1,
750
+ ...(HOLDS[word].takes === 'names' ? { takes: [], offers: accepts } : { takes: accepts }),
751
+ requires,
752
+ ...(uses === '' ? {} : { uses }),
753
+ },
754
+ ],
755
+ secrets,
756
+ calls,
757
+ entities,
758
+ )
759
+ const mine = after.filter((one) => one.includes(id))
760
+
761
+ stdout.write(`target: declared ${id} in ${at} — ${holdingOf(id)}.\n`)
762
+ if (mine.length > 0) {
763
+ for (const one of mine) stdout.write(`target: ${one}\n`)
764
+ }
765
+
766
+ const kept = spawnSync('npx', ['thread', 'vault'], { cwd: TREE.at, encoding: 'utf8' })
767
+ if (kept.status !== 0) stdout.write(`${`${kept.stdout ?? ''}${kept.stderr ?? ''}`.trim()}\n`)
768
+ exit(0)
769
+
770
+ function surfaceOf(
771
+ uses: string,
772
+ target: string,
773
+ ): {
774
+ readonly offered: readonly string[]
775
+ readonly deeper: ReadonlyMap<string, readonly string[]>
776
+ } {
777
+ const nothing = { offered: [] as readonly string[], deeper: new Map<string, readonly string[]>() }
778
+
779
+ const options: ts.CompilerOptions = {
780
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
781
+ module: ts.ModuleKind.ESNext,
782
+ target: ts.ScriptTarget.ESNext,
783
+ skipLibCheck: true,
784
+ allowJs: true,
785
+ noEmit: true,
786
+ }
787
+ const found = ts.resolveModuleName(uses, join(TREE.at, 'package.json'), options, ts.sys)
788
+ const declaration = found.resolvedModule?.resolvedFileName
789
+ if (declaration === undefined) return nothing
790
+
791
+ const program = ts.createProgram([declaration], options)
792
+ const checker = program.getTypeChecker()
793
+ const file = program.getSourceFile(declaration)
794
+ if (file === undefined) return nothing
795
+ const held = checker.getSymbolAtLocation(file)
796
+ if (held === undefined) return nothing
797
+
798
+ const unaliased = (one: ts.Symbol): ts.Symbol =>
799
+ (one.flags & ts.SymbolFlags.Alias) === 0 ? one : checker.getAliasedSymbol(one)
800
+ const exported = checker.getExportsOfModule(held)
801
+ const named = exported.find((one) => one.getName() === 'default' || one.getName() === 'export=')
802
+ const entry = named === undefined ? undefined : unaliased(named)
803
+
804
+ const typeOf = (one: ts.Symbol): ts.Type => checker.getTypeOfSymbolAtLocation(one, one.declarations?.[0] ?? file)
805
+ const calls = (one: ts.Type): readonly ts.Signature[] => checker.getSignaturesOfType(one, ts.SignatureKind.Call)
806
+
807
+ let top: ts.Type | undefined
808
+ if (entry !== undefined) {
809
+ const declared = checker.getDeclaredTypeOfSymbol(entry)
810
+ if (checker.getPropertiesOfType(declared).length > 0) top = declared
811
+ else {
812
+ const signature = calls(typeOf(entry))[0]
813
+ if (signature !== undefined) top = checker.getReturnTypeOfSignature(signature)
814
+ }
815
+ }
816
+
817
+ const HIDDEN = /^_/
818
+ const inherits = (one: ts.Type): ReadonlySet<string> => {
819
+ const names = new Set<string>()
820
+ for (const base of one.getBaseTypes() ?? []) for (const each of checker.getPropertiesOfType(base)) names.add(each.getName())
821
+ return names
822
+ }
823
+ const own = (one: ts.Type, entry = false): readonly ts.Symbol[] => {
824
+ const skip = entry ? new Set<string>() : inherits(one)
825
+ return checker.getPropertiesOfType(one).filter((each) => !HIDDEN.test(each.getName()) && !skip.has(each.getName()))
826
+ }
827
+
828
+ const DEEP = 3
829
+ const under = (one: ts.Type, depth: number, seen: ReadonlySet<string>): readonly string[] => {
830
+ if (depth > DEEP) return []
831
+ const out: string[] = []
832
+ for (const each of own(one)) {
833
+ const mine = typeOf(each)
834
+ if (calls(mine).length > 0) {
835
+ out.push(each.getName())
836
+ continue
837
+ }
838
+ const shape = checker.typeToString(mine)
839
+ if (seen.has(shape)) continue
840
+ for (const deep of under(mine, depth + 1, new Set([...seen, shape]))) out.push(`${each.getName()}.${deep}`)
841
+ }
842
+ return out.sort()
843
+ }
844
+
845
+ const walked = top !== undefined && checker.getPropertiesOfType(top).length > 0 ? own(top, true) : exported.map(unaliased)
846
+ const offered: string[] = []
847
+ const deeper = new Map<string, readonly string[]>()
848
+ for (const one of walked) {
849
+ const name = one.getName()
850
+ if (HIDDEN.test(name) || name === 'default' || name === 'export=') continue
851
+ if (misnamed(target, name) !== undefined) continue
852
+ const mine = typeOf(one)
853
+ if (calls(mine).length > 0) {
854
+ offered.push(name)
855
+ continue
856
+ }
857
+ const beneath = under(mine, 1, new Set()).filter((each) => misnamed(target, `${name}.${each}`) === undefined)
858
+ if (beneath.length > 0) {
859
+ offered.push(name)
860
+ deeper.set(name, beneath)
861
+ }
862
+ }
863
+ return { offered: offered.sort(), deeper }
864
+ }
865
+
866
+ function grows(id: string, name: string, uses: string, held: readonly string[]): void {
867
+
868
+ const said =
869
+ `The command this app gained by declaring \`${id}\`. \`thread run ${name} <command>\` runs one of the ` +
870
+ `command(s) read off \`${uses}\` when it was declared, with every argument and flag reaching it untouched; ` +
871
+ 'asked for nothing it prints that set. A command outside the set was never declared here and does not run. ' +
872
+ `The row states \`_threads/run_${name}.ts\`, one line naming this integration so the shared runner knows ` +
873
+ 'which it is — there is nothing in that file to change that declaring this again would not write the same ' +
874
+ 'way. This verb exists only because the integration is declared: take the target away and it goes.'
875
+
876
+ writeFileSync(
877
+ inTree(TREE, `run_${name}.ts`),
878
+ `import { ran } from '../module.ts'\n\nawait ran(${quoted(name)})\n`,
879
+ 'utf8',
880
+ )
881
+
882
+ const owner = inTree(TREE, 'integrations.ts')
883
+ try {
884
+ const owned = readFileSync(owner, 'utf8')
885
+ const under = listBounds(owned, 'declares')
886
+ if (under !== undefined && !holdsRow(owned, under, `run_${name}`)) {
887
+ writeFileSync(
888
+ owner,
889
+ withRow(
890
+ owned,
891
+ under,
892
+ [
893
+ ' {',
894
+ ` id: ${quoted(`run_${name}`)},`,
895
+ ` at: ${quoted(`_threads/run_${name}.ts`)},`,
896
+ ` describes: ${quoted(said)},`,
897
+ ' version: 1,',
898
+ ' },',
899
+ ].join('\n'),
900
+ ),
901
+ 'utf8',
902
+ )
903
+ }
904
+ } catch {
905
+ stdout.write(`target: _threads/integrations.ts could not be read, so \`run_${name}\` is registered but not declared.\n`)
906
+ }
907
+
908
+ const seat = inTree(TREE, 'derivation.ts')
909
+ let listed = ''
910
+ try {
911
+ listed = readFileSync(seat, 'utf8')
912
+ } catch {
913
+ listed = ''
914
+ }
915
+ const into = listed === '' ? undefined : listBounds(listed, 'GENERATORS')
916
+ if (into === undefined) {
917
+ stdout.write(
918
+ `target: ${id} is declared, but _threads/derivation.ts states no \`GENERATORS\` list, so \`thread run ${name}\` ` +
919
+ `does not exist yet. Attach \`${packageOf('derivation')}\` and declare this again.\n`,
920
+ )
921
+ } else if (holdsRow(listed, into, `run_${name}`)) {
922
+ stdout.write(`target: \`thread run ${name}\` already resolves — left as it was.\n`)
923
+ } else {
924
+ writeFileSync(
925
+ seat,
926
+ withRow(
927
+ listed,
928
+ into,
929
+ [
930
+ ' {',
931
+ ` id: ${quoted(`run_${name}`)},`,
932
+ ` at: ${quoted(`_threads/run_${name}.ts`)},`,
933
+ ` describes: ${quoted(said)},`,
934
+ ' version: 1,',
935
+ ` by: ${quoted(`thread run ${name}`)},`,
936
+ ' },',
937
+ ].join('\n'),
938
+ ),
939
+ 'utf8',
940
+ )
941
+ stdout.write(`target: \`thread run ${name}\` is now a command on this tree — ${String(held.length)} of them under it.\n`)
942
+ }
943
+ }
944
+