@prestack/thread 0.1.21 → 0.1.23
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.
- package/CHANGELOG.md +227 -0
- package/dist/core/attach.js +21 -0
- package/dist/core/descend.js +6 -3
- package/dist/core/generators/commit.js +55 -3
- package/dist/core/generators/dev.d.ts +1 -0
- package/dist/core/generators/dev.js +69 -0
- package/dist/derivation/generators/build.d.ts +3 -1
- package/dist/derivation/generators/build.js +104 -22
- package/dist/derivation/generators/release.js +74 -25
- package/dist/derivation/release.js +3 -3
- package/dist/integrations/cli.d.ts +0 -1
- package/dist/integrations/cli.js +69 -11
- package/dist/integrations/declared.d.ts +2 -0
- package/dist/integrations/declared.js +6 -1
- package/dist/integrations/pglite.js +13 -3
- package/dist/integrations/target.d.ts +3 -0
- package/dist/integrity/attach.js +1 -5
- package/dist/integrity/slopnators/sl_stale_path.js +10 -0
- package/dist/operations/access.js +3 -2
- package/dist/operations/capabilities.d.ts +2 -0
- package/dist/operations/capabilities.js +2 -0
- package/dist/operations/generators/capability.js +237 -24
- package/dist/operations/generators/compile.js +150 -50
- package/dist/operations/generators/seed.d.ts +3 -0
- package/dist/operations/generators/seed.js +25 -3
- package/dist/operations/nodes.d.ts +2 -2
- package/package.json +1 -1
- package/dist/core/actor.ts +0 -21
- package/dist/core/attach.ts +0 -613
- package/dist/core/chain.ts +0 -145
- package/dist/core/condition.ts +0 -227
- package/dist/core/descend.ts +0 -1185
- package/dist/core/errors/catalogue.ts +0 -314
- package/dist/core/errors/classes.ts +0 -51
- package/dist/core/errors/error.ts +0 -152
- package/dist/core/errors/namespaces.ts +0 -19
- package/dist/core/finding.ts +0 -37
- package/dist/core/generators/commit.ts +0 -157
- package/dist/core/init.ts +0 -222
- package/dist/core/invoked.ts +0 -52
- package/dist/core/round.ts +0 -74
- package/dist/core/rows.ts +0 -506
- package/dist/core/scope.ts +0 -20
- package/dist/core/thread.ts +0 -29
- package/dist/core/tree.ts +0 -90
- package/dist/core/types.ts +0 -351
- package/dist/derivation/attach.ts +0 -57
- package/dist/derivation/generators/build.ts +0 -371
- package/dist/derivation/generators/check.ts +0 -118
- package/dist/derivation/generators/determinism.ts +0 -145
- package/dist/derivation/generators/release.ts +0 -236
- package/dist/derivation/release.ts +0 -43
- package/dist/integrations/attach.ts +0 -187
- package/dist/integrations/cli.ts +0 -606
- package/dist/integrations/declared.ts +0 -34
- package/dist/integrations/disk.ts +0 -466
- package/dist/integrations/generators/read.ts +0 -127
- package/dist/integrations/generators/vault.ts +0 -210
- package/dist/integrations/https.ts +0 -216
- package/dist/integrations/pglite.ts +0 -206
- package/dist/integrations/secret.ts +0 -54
- package/dist/integrations/target.ts +0 -672
- package/dist/integrations/vault.ts +0 -148
- package/dist/integrity/attach.ts +0 -150
- package/dist/integrity/check-threads.ts +0 -166
- package/dist/integrity/constraint.ts +0 -1246
- package/dist/integrity/generators/compiler.ts +0 -138
- package/dist/integrity/generators/gate.ts +0 -227
- package/dist/integrity/generators/issues.ts +0 -160
- package/dist/integrity/generators/publish.ts +0 -233
- package/dist/integrity/generators/trial.ts +0 -359
- package/dist/integrity/lint.ts +0 -214
- package/dist/integrity/lsp/bin.ts +0 -44
- package/dist/integrity/lsp/diagnostics.ts +0 -92
- package/dist/integrity/lsp/findings.ts +0 -63
- package/dist/integrity/lsp/lint.ts +0 -124
- package/dist/integrity/lsp/protocol.ts +0 -78
- package/dist/integrity/lsp/server.ts +0 -224
- package/dist/integrity/slopnator.ts +0 -324
- package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
- package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
- package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
- package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
- package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
- package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
- package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
- package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
- package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
- package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
- package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
- package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
- package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
- package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
- package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
- package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
- package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
- package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
- package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
- package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
- package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
- package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
- package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
- package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
- package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
- package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
- package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
- package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
- package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
- package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
- package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
- package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
- package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
- package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
- package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
- package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
- package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
- package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
- package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
- package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
- package/dist/integrity/sweep.ts +0 -395
- package/dist/knowledge/attach.ts +0 -327
- package/dist/knowledge/generators/docsgen.ts +0 -186
- package/dist/knowledge/generators/glossary.ts +0 -73
- package/dist/knowledge/glossary.ts +0 -6
- package/dist/operations/access.ts +0 -131
- package/dist/operations/attach.ts +0 -408
- package/dist/operations/capabilities.ts +0 -88
- package/dist/operations/dispatch.ts +0 -294
- package/dist/operations/error-results.ts +0 -31
- package/dist/operations/generators/capability.ts +0 -1054
- package/dist/operations/generators/compile.ts +0 -701
- package/dist/operations/generators/permits.ts +0 -229
- package/dist/operations/generators/seed.ts +0 -359
- package/dist/operations/generators/trial.ts +0 -666
- package/dist/operations/nodes.ts +0 -53
- package/dist/operations/operations.ts +0 -91
- package/dist/operations/result.ts +0 -28
package/dist/integrations/cli.ts
DELETED
|
@@ -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
|
-
}
|