@prestack/thread 0.1.20 → 0.1.22

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 (148) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/dist/core/attach.js +21 -0
  3. package/dist/core/descend.d.ts +2 -0
  4. package/dist/core/descend.js +18 -3
  5. package/dist/core/generators/commit.js +55 -3
  6. package/dist/core/generators/dev.d.ts +1 -0
  7. package/dist/core/generators/dev.js +68 -0
  8. package/dist/core/rows.d.ts +5 -0
  9. package/dist/core/rows.js +45 -0
  10. package/dist/core/tree.d.ts +1 -0
  11. package/dist/core/tree.js +7 -1
  12. package/dist/derivation/generators/build.d.ts +3 -1
  13. package/dist/derivation/generators/build.js +103 -22
  14. package/dist/derivation/generators/release.js +74 -26
  15. package/dist/derivation/release.js +3 -3
  16. package/dist/integrations/cli.d.ts +3 -9
  17. package/dist/integrations/cli.js +96 -36
  18. package/dist/integrations/declared.d.ts +8 -0
  19. package/dist/integrations/declared.js +22 -0
  20. package/dist/integrations/pglite.js +13 -3
  21. package/dist/integrations/target.d.ts +4 -1
  22. package/dist/integrations/target.js +20 -6
  23. package/dist/integrity/attach.js +1 -5
  24. package/dist/integrity/generators/publish.js +2 -1
  25. package/dist/integrity/slopnators/sl_stale_path.js +10 -0
  26. package/dist/integrity/slopnators/sl_unlisted_program.js +2 -2
  27. package/dist/operations/access.js +3 -2
  28. package/dist/operations/capabilities.d.ts +2 -0
  29. package/dist/operations/capabilities.js +2 -0
  30. package/dist/operations/generators/capability.js +234 -24
  31. package/dist/operations/generators/compile.d.ts +21 -0
  32. package/dist/operations/generators/compile.js +151 -51
  33. package/dist/operations/generators/permits.d.ts +2 -1
  34. package/dist/operations/generators/permits.js +52 -47
  35. package/dist/operations/generators/seed.d.ts +3 -0
  36. package/dist/operations/generators/seed.js +25 -3
  37. package/dist/operations/nodes.d.ts +2 -2
  38. package/package.json +1 -1
  39. package/dist/core/actor.ts +0 -21
  40. package/dist/core/attach.ts +0 -613
  41. package/dist/core/chain.ts +0 -145
  42. package/dist/core/condition.ts +0 -227
  43. package/dist/core/descend.ts +0 -1173
  44. package/dist/core/errors/catalogue.ts +0 -314
  45. package/dist/core/errors/classes.ts +0 -51
  46. package/dist/core/errors/error.ts +0 -152
  47. package/dist/core/errors/namespaces.ts +0 -19
  48. package/dist/core/finding.ts +0 -37
  49. package/dist/core/generators/commit.ts +0 -157
  50. package/dist/core/init.ts +0 -222
  51. package/dist/core/invoked.ts +0 -52
  52. package/dist/core/round.ts +0 -74
  53. package/dist/core/rows.ts +0 -459
  54. package/dist/core/scope.ts +0 -20
  55. package/dist/core/thread.ts +0 -29
  56. package/dist/core/tree.ts +0 -90
  57. package/dist/core/types.ts +0 -351
  58. package/dist/derivation/attach.ts +0 -57
  59. package/dist/derivation/generators/build.ts +0 -371
  60. package/dist/derivation/generators/check.ts +0 -118
  61. package/dist/derivation/generators/determinism.ts +0 -145
  62. package/dist/derivation/generators/release.ts +0 -236
  63. package/dist/derivation/release.ts +0 -43
  64. package/dist/integrations/attach.ts +0 -187
  65. package/dist/integrations/cli.ts +0 -615
  66. package/dist/integrations/disk.ts +0 -466
  67. package/dist/integrations/generators/read.ts +0 -127
  68. package/dist/integrations/generators/vault.ts +0 -210
  69. package/dist/integrations/https.ts +0 -216
  70. package/dist/integrations/pglite.ts +0 -206
  71. package/dist/integrations/secret.ts +0 -54
  72. package/dist/integrations/target.ts +0 -656
  73. package/dist/integrations/vault.ts +0 -148
  74. package/dist/integrity/attach.ts +0 -150
  75. package/dist/integrity/check-threads.ts +0 -166
  76. package/dist/integrity/constraint.ts +0 -1246
  77. package/dist/integrity/generators/compiler.ts +0 -138
  78. package/dist/integrity/generators/gate.ts +0 -227
  79. package/dist/integrity/generators/issues.ts +0 -160
  80. package/dist/integrity/generators/publish.ts +0 -232
  81. package/dist/integrity/generators/trial.ts +0 -359
  82. package/dist/integrity/lint.ts +0 -214
  83. package/dist/integrity/lsp/bin.ts +0 -44
  84. package/dist/integrity/lsp/diagnostics.ts +0 -92
  85. package/dist/integrity/lsp/findings.ts +0 -63
  86. package/dist/integrity/lsp/lint.ts +0 -124
  87. package/dist/integrity/lsp/protocol.ts +0 -78
  88. package/dist/integrity/lsp/server.ts +0 -224
  89. package/dist/integrity/slopnator.ts +0 -324
  90. package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
  91. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
  92. package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
  93. package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
  94. package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
  95. package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
  96. package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
  97. package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
  98. package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
  99. package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
  100. package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
  101. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
  102. package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
  103. package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
  104. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
  105. package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
  106. package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
  107. package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
  108. package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
  109. package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
  110. package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
  111. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
  112. package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
  113. package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
  114. package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
  115. package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
  116. package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
  117. package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
  118. package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
  119. package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
  120. package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
  121. package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
  122. package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
  123. package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
  124. package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
  125. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
  126. package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
  127. package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
  128. package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
  129. package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
  130. package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
  131. package/dist/integrity/sweep.ts +0 -395
  132. package/dist/knowledge/attach.ts +0 -327
  133. package/dist/knowledge/generators/docsgen.ts +0 -186
  134. package/dist/knowledge/generators/glossary.ts +0 -73
  135. package/dist/knowledge/glossary.ts +0 -6
  136. package/dist/operations/access.ts +0 -131
  137. package/dist/operations/attach.ts +0 -408
  138. package/dist/operations/capabilities.ts +0 -88
  139. package/dist/operations/dispatch.ts +0 -294
  140. package/dist/operations/error-results.ts +0 -31
  141. package/dist/operations/generators/capability.ts +0 -1054
  142. package/dist/operations/generators/compile.ts +0 -701
  143. package/dist/operations/generators/permits.ts +0 -224
  144. package/dist/operations/generators/seed.ts +0 -359
  145. package/dist/operations/generators/trial.ts +0 -666
  146. package/dist/operations/nodes.ts +0 -53
  147. package/dist/operations/operations.ts +0 -91
  148. package/dist/operations/result.ts +0 -28
@@ -1,615 +0,0 @@
1
- import { spawn, spawnSync, type ChildProcessWithoutNullStreams, type SpawnSyncReturns } from 'node:child_process'
2
- import { createHash, randomBytes } from 'node:crypto'
3
- import { tmpdir, uptime } from 'node:os'
4
- import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
5
- import { allowedNodeEnvironmentFlags, env, execPath, kill, pid, stderr, stdout } from 'node:process'
6
- import { fileURLToPath } from 'node:url'
7
-
8
- import { CHANNEL, FOLDER, across, attached, createFileSync, existsSync, here, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from '../integrations/disk.ts'
9
- import { VAULT_DIR } from '../integrations/vault.ts'
10
-
11
- import { CALL_PREFIX, LOCKFILES, ORDERS, PACKAGER, REACH, managerOf, programOf, unlisted, type Answer, type Call, type How, type Order, type Ran, type Recorded, type Running, type Target } from '../integrations/target.ts'
12
- import { BESIDE, CLI } from '../core/invoked.ts'
13
- import { valuesIn } from '../core/rows.ts'
14
- import { THING } from '../core/scope.ts'
15
- import { findTree, inTree } from '../core/tree.ts'
16
-
17
- export interface Starting {
18
-
19
- readonly at?: string
20
-
21
- readonly shows?: boolean | 'errors' | 'tee'
22
-
23
- readonly hears?: string
24
-
25
- readonly within?: number
26
-
27
- readonly holds?: NodeJS.ProcessEnv
28
- }
29
-
30
- const answered = (went: SpawnSyncReturns<string>): Ran => ({
31
- there: went.error === undefined,
32
- ok: went.status === 0,
33
- code: went.status ?? 1,
34
- out: String(went.stdout ?? ''),
35
- err: String(went.stderr ?? ''),
36
- killed: went.signal ?? null,
37
- why: went.error === undefined ? '' : went.error.message,
38
- })
39
-
40
- const standing = (at: string | undefined): string | undefined => (at === undefined || across(at) ? undefined : at)
41
-
42
- export function starts(program: string, words: readonly string[], how: Starting = {}): Ran {
43
- let went: SpawnSyncReturns<string>
44
- try {
45
- went = spawnSync(program, [...words], {
46
- cwd: standing(how.at),
47
- encoding: 'utf8',
48
- shell: false,
49
- stdio:
50
- how.shows === true
51
- ? [how.hears === undefined ? 'inherit' : 'pipe', 'inherit', 'inherit']
52
- : how.shows === 'errors'
53
- ? [how.hears === undefined ? 'ignore' : 'pipe', 'pipe', 'inherit']
54
- : 'pipe',
55
- input: how.hears,
56
- env: how.holds,
57
- ...(how.within === undefined ? {} : { timeout: how.within, killSignal: 'SIGKILL' as const }),
58
- })
59
- } catch (thrown) {
60
- return nowhere(thrown instanceof Error ? thrown.message : String(thrown))
61
- }
62
- const ran = answered(went)
63
- if (how.shows === 'tee') {
64
- stdout.write(ran.out)
65
- stderr.write(ran.err)
66
- }
67
- return ran
68
- }
69
-
70
- export function started(program: string, words: readonly string[], how: Starting = {}): Promise<Ran> {
71
- return new Promise((keep) => {
72
- let child: ChildProcessWithoutNullStreams
73
- try {
74
- child = spawn(program, [...words], { cwd: standing(how.at), shell: false, env: how.holds })
75
- } catch (thrown) {
76
- keep(nowhere(thrown instanceof Error ? thrown.message : String(thrown)))
77
- return
78
- }
79
- let out = ''
80
- let err = ''
81
- let why = ''
82
- const clock = how.within === undefined ? undefined : setTimeout(() => child.kill('SIGKILL'), how.within)
83
- child.stdout.on('data', (chunk: Buffer) => (out += chunk.toString()))
84
- child.stderr.on('data', (chunk: Buffer) => (err += chunk.toString()))
85
- child.on('error', (error) => (why = error.message))
86
- child.on('close', (code, killed) => {
87
- if (clock !== undefined) clearTimeout(clock)
88
- keep({ there: why === '', ok: why === '' && code === 0, code: code ?? 1, out, err, killed, why })
89
- })
90
- child.stdin.end(how.hears)
91
- })
92
- }
93
-
94
- export function handed(
95
- at: string,
96
- program: string,
97
- passed: readonly string[],
98
- shows: boolean | 'errors' | 'tee' = true,
99
- within?: number,
100
- hears?: string,
101
- ): Ran {
102
- return starts(program, passed, { at, shows, ...(within === undefined ? {} : { within }), ...(hears === undefined ? {} : { hears }) })
103
- }
104
-
105
- const HOLDING = 'THREAD_RUNNING'
106
-
107
- const CLAIMED = 'running.'
108
-
109
- const CLAIM = '.json'
110
-
111
- const STARTING = 10_000
112
-
113
- export interface Holding {
114
- readonly pid: number
115
- readonly runs: string
116
- readonly since: number
117
- }
118
-
119
- export interface Held {
120
- readonly by?: Holding
121
- }
122
-
123
- let claimed = ''
124
-
125
- const claimAt = (dir: string, one: number): string => join(dir, CLAIMED + String(one) + CLAIM)
126
-
127
- const generations = (dir: string): readonly number[] =>
128
- existsSync(dir)
129
- ? readdirSync(dir)
130
- .filter((one) => one.startsWith(CLAIMED) && one.endsWith(CLAIM))
131
- .map((one) => Number(one.slice(CLAIMED.length, one.length - CLAIM.length)))
132
- .filter((one) => Number.isInteger(one) && one >= 0)
133
- : []
134
-
135
- const living = (one: number): boolean => {
136
- try {
137
- kill(one, 0)
138
- return true
139
- } catch (thrown) {
140
- return (thrown as NodeJS.ErrnoException).code === 'EPERM'
141
- }
142
- }
143
-
144
- function holding(at: string): Held {
145
- try {
146
- const by = JSON.parse(readFileSync(at, 'utf8')) as Holding
147
- return by.since > Date.now() - uptime() * 1000 && living(by.pid) ? { by } : {}
148
- } catch {
149
- try {
150
- return Date.now() - statSync(at).mtimeMs < STARTING ? { by: { pid: 0, runs: '', since: Date.now() } } : {}
151
- } catch {
152
- return {}
153
- }
154
- }
155
- }
156
-
157
- export function releases(): void {
158
- const at = claimed === '' ? env[HOLDING] : claimed
159
- if (at !== undefined) rmSync(at, { force: true })
160
- claimed = ''
161
- Reflect.deleteProperty(env, HOLDING)
162
- }
163
-
164
- export function claims(root: string, runs: string): Held {
165
- if (attached()) return {}
166
- const dir = join(root, VAULT_DIR)
167
- for (let tries = 0; tries < 5; tries += 1) {
168
- const taken = generations(dir)
169
- const top = Math.max(-1, ...taken)
170
- const held: Held = top < 0 ? {} : holding(claimAt(dir, top))
171
- if (held.by !== undefined) return env[HOLDING] === claimAt(dir, top) ? {} : held
172
- const mine = claimAt(dir, top + 1)
173
- if (!createFileSync(mine, JSON.stringify({ pid, runs, since: Date.now() }))) continue
174
- claimed = mine
175
- env[HOLDING] = mine
176
- for (const one of taken) rmSync(claimAt(dir, one), { force: true })
177
- process.on('exit', releases)
178
- return {}
179
- }
180
- return { by: { pid: 0, runs: '', since: Date.now() } }
181
- }
182
-
183
- export function programFor(name: string, at: string): string {
184
- const found = programOf(name, at, existsSync)
185
- if (found === undefined) throw new Error(unlisted(name))
186
- return found
187
- }
188
-
189
- export const SIGNING = 'THREAD_RECEIPTS_KEY'
190
-
191
- const SIGNED_AT = 'thread-receipts.key'
192
-
193
- let signed = ''
194
-
195
- export function signing(): string {
196
- if (signed !== '') return signed
197
- const given = env[SIGNING]
198
- if (given !== undefined && given !== '') {
199
- signed = given
200
- return signed
201
- }
202
- if (attached()) {
203
- throw new Error(
204
- 'this answers for a folder on another machine and was handed no key to seal receipts with, so it issues none. ' +
205
- 'The server that started it hands one over in ' + SIGNING + '.',
206
- )
207
- }
208
- const at = join(tmpdir(), SIGNED_AT)
209
- if (!existsSync(at)) createFileSync(at, randomBytes(32).toString('hex'))
210
- signed = readFileSync(at, 'utf8').trim()
211
- return signed
212
- }
213
-
214
- const OWN = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..')
215
-
216
- export const PROGRAM = REACH.cli + THING
217
-
218
- export const COPIES = THING + ' create copy'
219
-
220
- export const COPY_AT = join(VAULT_DIR, THING + '.json')
221
-
222
- interface Started {
223
- readonly words: readonly string[]
224
- readonly own: string
225
- readonly reads: readonly string[]
226
- }
227
-
228
- const UNCOPIED =
229
- 'the thread command sits beside none of the framework code this server runs, because no package ships it, and the ' +
230
- 'folder this server was started from keeps no compiled copy of it. Run `' + COPIES + '` in that folder from a server ' +
231
- 'that runs the framework itself, and the next command here starts that copy.'
232
-
233
- const UNWRITTEN =
234
- 'the compiled copy of the thread command this folder keeps could not be written out beside its installed packages, so ' +
235
- 'no command could start: '
236
-
237
- const INSTALLED = 'node_modules'
238
-
239
- const CACHED = join(INSTALLED, '.cache', THING)
240
-
241
- function startable(): Started | string {
242
- if (existsSync(CLI.at(-1) ?? '')) return { words: CLI, own: OWN, reads: [OWN] }
243
- let root: string
244
- try {
245
- root = findTree().at
246
- } catch {
247
- return UNCOPIED
248
- }
249
- const copied = resolve(root, COPY_AT)
250
- if (!existsSync(copied)) return UNCOPIED
251
- const said = readFileSync(copied, 'utf8')
252
- const own = join(root, CACHED, createHash('sha256').update(said).digest('hex').slice(0, 16))
253
- const entry = join(own, 'cli', BESIDE('cli'))
254
- if (!existsSync(entry)) {
255
- const part = own + '-' + String(pid)
256
- try {
257
- for (const [at, text] of Object.entries(JSON.parse(said) as Readonly<Record<string, string>>)) writeFileSync(join(part, at), text, 'utf8')
258
- renameSync(part, own)
259
- } catch (thrown) {
260
- rmSync(part, { recursive: true, force: true })
261
- if (!existsSync(entry)) return UNWRITTEN + (thrown instanceof Error ? thrown.message : String(thrown))
262
- }
263
- }
264
- return { words: [entry], own, reads: [join(root, INSTALLED)] }
265
- }
266
-
267
- const GUARDS = ['--permission', '--experimental-permission']
268
-
269
- const HUSHES = '--disable-warning'
270
-
271
- const swapped = (path: string): string =>
272
- [...path].map((one) => (one === one.toUpperCase() ? one.toLowerCase() : one.toUpperCase())).join('')
273
-
274
- let unguarded = false
275
-
276
- function guarded(reads: readonly string[]): string {
277
- const before = env.NODE_OPTIONS ?? ''
278
- const on = GUARDS.find((one) => allowedNodeEnvironmentFlags.has(one))
279
- if (on === undefined) {
280
- if (!unguarded) {
281
- stderr.write(
282
- 'thread: this node has no permission model, so a thread command started for a folder runs unguarded: nothing ' +
283
- 'here stops it reading that folder straight off this disk, and on one machine that would go unnoticed.\n',
284
- )
285
- unguarded = true
286
- }
287
- return before
288
- }
289
- const kept = [...new Set([tmpdir(), realpathSync(tmpdir())])]
290
- const allowed = (flag: string, paths: readonly string[]): readonly string[] => paths.map((one) => flag + '="' + one + '"')
291
- const quiet = allowedNodeEnvironmentFlags.has(HUSHES) ? [HUSHES + '=SecurityWarning'] : []
292
- return [before, on, ...allowed('--allow-fs-read', [...reads.flatMap((one) => [one, swapped(one)]), ...kept]), ...allowed('--allow-fs-write', kept), '--allow-child-process', '--allow-worker', ...quiet]
293
- .filter((one) => one !== '')
294
- .join(' ')
295
- }
296
-
297
- const SERVED = 'thread-serve'
298
-
299
- const TICK = 2
300
-
301
- const MARGIN = 5_000
302
-
303
- const SAYING = '-saying'
304
-
305
- const ORDER = '-order'
306
-
307
- const ANSWER = '-answer'
308
-
309
- interface Serving {
310
- readonly of: string
311
- readonly dir: string
312
- readonly root: string
313
- out: string
314
- err: string
315
- why: string
316
- pending: string
317
- waiting: ((held: Ran) => void)[]
318
- parked: Ran[]
319
- replaying: Recorded[]
320
- handed: number
321
- readonly known: Map<string, Answer>
322
- asking?: Order
323
- last?: Ran
324
- stop?: () => void
325
- watch?: ReturnType<typeof setInterval>
326
- }
327
-
328
- const serving = new Map<string, Serving>()
329
-
330
- const still = (one: Serving, asks: readonly Order[]): Ran => ({ there: true, ok: false, code: -1, out: '', err: '', killed: null, why: '', of: one.of, asks })
331
-
332
- const nowhere = (why: string): Ran => ({ there: false, ok: false, code: 1, out: '', err: '', killed: null, why })
333
-
334
- const lost = (of: string): Ran => ({ there: true, ok: false, code: -1, out: '', err: '', killed: null, why: '', of, lost: of })
335
-
336
- const told = (one: Order): string => JSON.stringify([one.order, one.at, one.said ?? null, one.mode ?? null, one.words ?? null])
337
-
338
- const READS: ReadonlySet<string> = new Set(Object.entries(ORDERS).filter(([order, answer]) => order === answer).map(([order]) => order))
339
-
340
- function kept(one: Serving, asked: Order, held: Answer): void {
341
- if (READS.has(asked.order) && held.answered !== 'refused') one.known.set(told(asked), held)
342
- }
343
-
344
- const plainly = (one: Order): string => one.order + ' ' + (one.at === '' ? '.' : one.at)
345
-
346
- const outside = (root: string, at: string): boolean => {
347
- const step = relative(root, at)
348
- return step.startsWith('..') ? true : isAbsolute(step)
349
- }
350
-
351
- function hand(one: Serving, held: Ran): void {
352
- const settle = one.waiting.shift()
353
- if (settle === undefined) one.parked.push(held)
354
- else settle(held)
355
- }
356
-
357
- function reply(dir: string, name: string, held: Answer): void {
358
- writeFileSync(join(dir, name + SAYING), JSON.stringify(held), 'utf8')
359
- renameSync(join(dir, name + SAYING), join(dir, name + ANSWER))
360
- }
361
-
362
- function watching(one: Serving): void {
363
- one.watch = setInterval(() => {
364
- let names: readonly string[] = []
365
- try {
366
- names = readdirSync(one.dir)
367
- } catch {
368
- return
369
- }
370
- for (const name of names) {
371
- if (!name.endsWith(ORDER)) continue
372
- const file = join(one.dir, name)
373
- let said = ''
374
- try {
375
- said = readFileSync(file, 'utf8')
376
- } catch {
377
- continue
378
- }
379
- rmSync(file, { force: true })
380
- const order = JSON.parse(said) as Order
381
- const short = name.slice(0, name.length - ORDER.length)
382
- if (outside(one.root, order.at)) {
383
- reply(one.dir, short, {
384
- answered: 'refused',
385
- because: order.at + ' is outside the folder this command was pointed at, and nothing outside it is ever asked of the machine answering.',
386
- })
387
- continue
388
- }
389
- const asked: Order = { ...order, at: relative(one.root, order.at) }
390
- const known = one.known.get(told(asked))
391
- if (known !== undefined) {
392
- reply(one.dir, short, known)
393
- continue
394
- }
395
- if (!READS.has(asked.order)) one.known.clear()
396
- const due = one.replaying.shift()
397
- if (due !== undefined) {
398
- if (told(due.order) === told(asked)) {
399
- reply(one.dir, short, due.answered)
400
- kept(one, asked, due.answered)
401
- continue
402
- }
403
- one.replaying = []
404
- one.err +=
405
- 'thread: this command was carried on here by running it again and handing it the answers it was given before, ' +
406
- 'and this time it asked for ' + plainly(asked) + ' where before it asked for ' + plainly(due.order) + '. A command ' +
407
- 'that does not ask the same things twice cannot be carried on from its answers, so it was stopped. Run it again.\n'
408
- one.stop?.()
409
- continue
410
- }
411
- one.pending = short
412
- one.asking = asked
413
- hand(one, still(one, [asked]))
414
- }
415
- }, TICK)
416
- }
417
-
418
- function next(one: Serving, within?: number): Promise<Ran> {
419
- const held = one.parked.shift()
420
- if (held !== undefined) return Promise.resolve(held)
421
- return new Promise((settle) => {
422
- const done = { is: false }
423
- const mine = (ran: Ran): void => {
424
- if (done.is) return
425
- done.is = true
426
- settle(ran)
427
- }
428
- one.waiting.push(mine)
429
- if (within === undefined) return
430
- setTimeout(() => {
431
- if (done.is) return
432
- done.is = true
433
- one.waiting = one.waiting.filter((each) => each !== mine)
434
- settle(still(one, []))
435
- }, Math.max(1_000, within - MARGIN))
436
- })
437
- }
438
-
439
- function begun(from: Started, word: string, args: readonly string[], root: string, hears?: string, called?: string, recording: readonly Recorded[] = []): Serving {
440
- const of = called ?? randomBytes(6).toString('hex')
441
- const dir = join(tmpdir(), SERVED, randomBytes(6).toString('hex'))
442
- writeFileSync(join(dir, 'channel'), '', 'utf8')
443
- const one: Serving = { of, dir, root, out: '', err: '', why: '', pending: '', waiting: [], parked: [], replaying: [...recording], handed: recording.length, known: new Map() }
444
- const child = spawn(execPath, [...from.words, word, ...args], {
445
- cwd: from.own,
446
- shell: false,
447
- env: { ...env, [CHANNEL]: dir, [FOLDER]: root, [SIGNING]: signing(), NODE_OPTIONS: guarded(from.reads) },
448
- })
449
- one.stop = () => child.kill()
450
- child.stdout.on('data', (chunk: Buffer) => (one.out += chunk.toString()))
451
- child.stderr.on('data', (chunk: Buffer) => (one.err += chunk.toString()))
452
- child.on('error', (thrown) => (one.why = thrown.message))
453
- child.on('close', (code, killed) => {
454
- if (one.watch !== undefined) clearInterval(one.watch)
455
- serving.delete(of)
456
- rmSync(dir, { recursive: true, force: true })
457
- hand(one, { there: one.why === '', ok: one.why === '' && code === 0, code: code ?? 1, out: one.out, err: one.err, killed, why: one.why })
458
- })
459
- child.stdin.end(hears ?? '')
460
- watching(one)
461
- serving.set(of, one)
462
- return one
463
- }
464
-
465
- const UNPLACED = ' against, so it did not start. A command on this server runs against the folder the person gave and nowhere else.'
466
-
467
- function counted(one: Serving, held: Ran): Ran {
468
- const asks = held.asks ?? []
469
- if (asks.length === 0) return held
470
- one.handed += asks.length
471
- one.last = held
472
- return held
473
- }
474
-
475
- async function served(word: string, args: readonly string[], within?: number, how?: How): Promise<Ran> {
476
- const of = how?.of
477
- if (of !== undefined) {
478
- const one = serving.get(of)
479
- if (one === undefined) {
480
- const recording = how?.recording
481
- if (recording === undefined) return lost(of)
482
- const root = how?.at ?? ''
483
- if (root === '') return nowhere('nothing said which folder to run thread ' + word + UNPLACED)
484
- const from = startable()
485
- if (typeof from === 'string') return nowhere(from)
486
- const again = begun(from, word, args, root, how?.hears, of, recording)
487
- return counted(again, await next(again, within))
488
- }
489
- const answering = how?.answering
490
- if (answering !== undefined && answering < one.handed && one.last !== undefined) return one.last
491
- const given = (how?.answered ?? [])[0]
492
- if (one.pending !== '' && given !== undefined) {
493
- reply(one.dir, one.pending, given)
494
- if (one.asking !== undefined) kept(one, one.asking, given)
495
- one.pending = ''
496
- }
497
- return counted(one, await next(one, within))
498
- }
499
- const root = how?.at ?? ''
500
- if (root === '') return nowhere('nothing said which folder to run thread ' + word + UNPLACED)
501
- const from = startable()
502
- if (typeof from === 'string') return nowhere(from)
503
- const fresh = begun(from, word, args, root, how?.hears)
504
- return counted(fresh, await next(fresh, within))
505
- }
506
-
507
- export function running(name: string, subcommands: Readonly<Record<string, string>>, at: string): Running {
508
- const out: Record<
509
- string,
510
- (args: readonly string[], shows?: Starting['shows'], within?: number, how?: How) => Promise<Ran>
511
- > = {}
512
- for (const [key, word] of Object.entries(subcommands)) {
513
- out[key] =
514
- name === THING
515
- ? async (args, shows, within, how) => served(word, args, within, how)
516
- : async (args, shows, within, how) => {
517
- const where = how?.at ?? at
518
- const program = programFor(name, where)
519
- if ((shows ?? false) !== false) return handed(where, program, [word, ...args], shows ?? false, within, how?.hears)
520
- return await started(program, [word, ...args], { at: where, ...(within === undefined ? {} : { within }), ...(how?.hears === undefined ? {} : { hears: how.hears }) })
521
- }
522
- }
523
- return out
524
- }
525
-
526
- interface Sheet {
527
- readonly TARGETS?: readonly Target[]
528
- readonly CALLS?: readonly Call[]
529
- }
530
-
531
- function sheet(at: string): Sheet {
532
- const path = inTree(findTree(at), 'targets.ts')
533
- return existsSync(path) ? (valuesIn(readFileSync(path, 'utf8')) as Sheet) : {}
534
- }
535
-
536
- export interface Expansion {
537
- readonly program: string
538
- readonly target: Target
539
- readonly calls: readonly Call[]
540
- }
541
-
542
- export function expansions(at: string): readonly Expansion[] {
543
- const held = sheet(at)
544
- return (held.TARGETS ?? [])
545
- .filter((one) => one.id.startsWith(REACH.cli))
546
- .map((one) => ({ program: one.id.slice(REACH.cli.length), target: one, calls: (held.CALLS ?? []).filter((each) => each.of === one.id) }))
547
- }
548
-
549
- export function calls(target: string, at: string): Running {
550
- const named: Record<string, string> = {}
551
- for (const one of sheet(at).CALLS ?? []) if (one.of === target) named[one.id.replace(CALL_PREFIX, '')] = one.path
552
- return running(target.replace(REACH.cli, ''), named, at)
553
- }
554
-
555
- export { LOCKFILES }
556
-
557
- export const manager = (into: string): string => managerOf(into, existsSync)
558
-
559
- const ADDS: Readonly<Record<string, string>> = { pnpm: 'add', yarn: 'add', bun: 'add', npm: 'install' }
560
-
561
- export function adding(into: string): readonly [string, string] {
562
- const tool = manager(into)
563
- const verb = ADDS[tool]
564
- if (verb === undefined) {
565
- throw new Error(
566
- tool + ' is what the lockfile in ' + into + ' picks, and nothing here knows the word it adds a package with.',
567
- )
568
- }
569
- return [tool, verb]
570
- }
571
-
572
- export function needing(into: string, needs: Readonly<Record<string, string>>): readonly string[] {
573
- const manifest = join(into, 'package.json')
574
- const was = readFileSync(manifest, 'utf8')
575
- const parsed = JSON.parse(was) as Record<string, unknown> & {
576
- readonly dependencies?: Readonly<Record<string, string>>
577
- readonly devDependencies?: Readonly<Record<string, string>>
578
- }
579
- const named = { ...parsed.devDependencies, ...parsed.dependencies }
580
- const added = Object.keys(needs).filter((one) => named[one] === undefined).sort()
581
- if (added.length === 0) return []
582
- const dependencies: Record<string, string> = { ...parsed.dependencies }
583
- for (const one of added) dependencies[one] = needs[one] ?? 'latest'
584
- writeFileSync(manifest, `${JSON.stringify({ ...parsed, dependencies }, null, 2)}\n`, 'utf8')
585
- try {
586
- installing(into)
587
- } catch (thrown) {
588
- writeFileSync(manifest, was, 'utf8')
589
- throw new Error(
590
- `${added.join(', ')} could not be fetched — ${thrown instanceof Error ? thrown.message : String(thrown)} ` +
591
- 'package.json is as it was, so nothing here names a package that is not on disk.',
592
- )
593
- }
594
- return added
595
- }
596
-
597
- export function installing(at: string): string {
598
- if (attached()) return here().fetch(at)
599
- const tool = manager(at)
600
- const ran = starts(programFor(PACKAGER, at), ['install'], { at })
601
- const said = (ran.out + ran.err).trim()
602
- if (!ran.there) {
603
- throw new Error(
604
- tool + ' is what the lockfile in ' + at + ' picks, and it is not on this machine, so nothing here can fetch ' +
605
- 'what that tree depends on until that changes.',
606
- )
607
- }
608
- if (!ran.ok) {
609
- throw new Error(
610
- tool + ' could not install what ' + at + ' depends on. It said: ' +
611
- (said === '' ? 'nothing, and exited ' + String(ran.code) : said),
612
- )
613
- }
614
- return said
615
- }