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