@prestack/thread 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/actor.ts +20 -0
- package/dist/core/asking.d.ts +0 -1
- package/dist/core/asking.js +0 -1
- package/dist/core/asking.ts +1342 -0
- package/dist/core/attach.js +2 -2
- package/dist/core/attach.ts +585 -0
- package/dist/core/chain.js +8 -1
- package/dist/core/chain.ts +148 -0
- package/dist/core/cli.js +97 -18
- package/dist/core/cli.ts +2004 -0
- package/dist/core/condition.ts +227 -0
- package/dist/core/descend.d.ts +1 -0
- package/dist/core/descend.js +36 -0
- package/dist/core/descend.ts +1001 -0
- package/dist/core/finding.ts +38 -0
- package/dist/core/generators/meta.js +6 -0
- package/dist/core/generators/meta.ts +1006 -0
- package/dist/core/init.ts +170 -0
- package/dist/core/invoked.ts +47 -0
- package/dist/core/round.ts +57 -0
- package/dist/core/rows.ts +369 -0
- package/dist/core/scope.ts +18 -0
- package/dist/core/thread.ts +29 -0
- package/dist/core/tree.ts +67 -0
- package/dist/core/types.ts +350 -0
- package/dist/derivation/attach.js +1 -1
- package/dist/derivation/attach.ts +68 -0
- package/dist/derivation/generators/build.ts +232 -0
- package/dist/derivation/generators/check.ts +118 -0
- package/dist/derivation/generators/derivation.ts +347 -0
- package/dist/derivation/generators/determinism.ts +145 -0
- package/dist/derivation/generators/release.js +19 -9
- package/dist/derivation/generators/release.ts +213 -0
- package/dist/derivation/makers/release.ts +135 -0
- package/dist/derivation/release.ts +23 -0
- package/dist/integrations/attach.js +2 -2
- package/dist/integrations/attach.ts +207 -0
- package/dist/integrations/generators/read.ts +127 -0
- package/dist/integrations/generators/secret.ts +289 -0
- package/dist/integrations/generators/target.js +16 -6
- package/dist/integrations/generators/target.ts +944 -0
- package/dist/integrations/generators/vault.js +46 -14
- package/dist/integrations/generators/vault.ts +212 -0
- package/dist/integrations/{sdk.js → module.js} +4 -4
- package/dist/integrations/module.ts +189 -0
- package/dist/integrations/secret.ts +50 -0
- package/dist/integrations/target.d.ts +3 -1
- package/dist/integrations/target.js +14 -11
- package/dist/integrations/target.ts +340 -0
- package/dist/integrations/vault.d.ts +2 -0
- package/dist/integrations/vault.js +4 -2
- package/dist/integrations/vault.ts +136 -0
- package/dist/integrity/attach.ts +234 -0
- package/dist/integrity/check-threads.js +2 -26
- package/dist/integrity/check-threads.ts +166 -0
- package/dist/integrity/constraint.ts +1202 -0
- package/dist/integrity/generators/compiler.ts +161 -0
- package/dist/integrity/generators/gate.ts +201 -0
- package/dist/integrity/generators/issues.ts +160 -0
- package/dist/integrity/generators/publish.js +14 -5
- package/dist/integrity/generators/publish.ts +247 -0
- package/dist/integrity/generators/rule.ts +271 -0
- package/dist/integrity/generators/seal.ts +117 -0
- package/dist/integrity/generators/trial.ts +320 -0
- package/dist/integrity/lint.ts +227 -0
- package/dist/integrity/lsp/bin.ts +42 -0
- package/dist/integrity/lsp/diagnostics.ts +92 -0
- package/dist/integrity/lsp/findings.ts +63 -0
- package/dist/integrity/lsp/lint.ts +140 -0
- package/dist/integrity/lsp/protocol.ts +78 -0
- package/dist/integrity/lsp/server.ts +224 -0
- package/dist/integrity/sealed.js +2 -2
- package/dist/integrity/sealed.ts +56 -0
- package/dist/integrity/slopnator.ts +350 -0
- package/dist/integrity/slopnators/sl_broken_lineage.ts +36 -0
- package/dist/integrity/slopnators/sl_declared_elsewhere.ts +39 -0
- package/dist/integrity/slopnators/sl_derived_drift.ts +98 -0
- package/dist/integrity/slopnators/sl_hardcoded_style.ts +55 -0
- package/dist/integrity/slopnators/sl_inert_island.ts +91 -0
- package/dist/integrity/slopnators/sl_inert_slopnator.ts +93 -0
- package/dist/integrity/slopnators/sl_missing_row.ts +182 -0
- package/dist/integrity/slopnators/sl_no_barrel.ts +48 -0
- package/dist/integrity/slopnators/sl_no_child_process.ts +74 -0
- package/dist/integrity/slopnators/sl_no_comment.ts +43 -0
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +328 -0
- package/dist/integrity/slopnators/sl_no_import_cycle.ts +50 -0
- package/dist/integrity/slopnators/sl_no_such_command.ts +64 -0
- package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +63 -0
- package/dist/integrity/slopnators/sl_secret_literal.ts +100 -0
- package/dist/integrity/slopnators/sl_stale_path.ts +94 -0
- package/dist/integrity/slopnators/sl_twice_meant.ts +31 -0
- package/dist/integrity/slopnators/sl_twice_parented.ts +37 -0
- package/dist/integrity/slopnators/sl_undeclared_crossing.ts +87 -0
- package/dist/integrity/slopnators/sl_undeclared_destination.ts +65 -0
- package/dist/integrity/slopnators/sl_undeclared_element.ts +44 -0
- package/dist/integrity/slopnators/sl_undeclared_node.ts +69 -0
- package/dist/integrity/slopnators/sl_undeclared_operation.ts +147 -0
- package/dist/integrity/slopnators/sl_undeclared_variant.ts +148 -0
- package/dist/integrity/slopnators/sl_undescribed.ts +65 -0
- package/dist/integrity/slopnators/sl_ungoverned_island.ts +61 -0
- package/dist/integrity/slopnators/sl_unkept_rows.ts +111 -0
- package/dist/integrity/slopnators/sl_unnamed_island.ts +41 -0
- package/dist/integrity/slopnators/sl_unpaired_member.ts +131 -0
- package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +58 -0
- package/dist/integrity/slopnators/sl_unprefixed_member.ts +136 -0
- package/dist/integrity/slopnators/sl_unseamed_reach.ts +79 -0
- package/dist/integrity/slopnators/sl_unthreaded.ts +419 -0
- package/dist/integrity/slopnators/sl_untranslated_key.ts +164 -0
- package/dist/integrity/slopnators/sl_unversioned.ts +36 -0
- package/dist/integrity/sweep.ts +307 -0
- package/dist/knowledge/attach.ts +350 -0
- package/dist/knowledge/generators/docsgen.ts +174 -0
- package/dist/knowledge/generators/glossary.ts +73 -0
- package/dist/knowledge/generators/skills.ts +263 -0
- package/dist/knowledge/glossary.ts +6 -0
- package/dist/operations/access.ts +88 -0
- package/dist/operations/attach.ts +475 -0
- package/dist/operations/capabilities.ts +65 -0
- package/dist/operations/cost.ts +58 -0
- package/dist/operations/dispatch.ts +348 -0
- package/dist/operations/error-results.ts +31 -0
- package/dist/operations/errors/catalog.ts +168 -0
- package/dist/operations/errors/classes.ts +51 -0
- package/dist/operations/errors/errors.ts +314 -0
- package/dist/operations/generators/capability.js +29 -19
- package/dist/operations/generators/capability.ts +704 -0
- package/dist/operations/generators/compile.js +20 -1
- package/dist/operations/generators/compile.ts +643 -0
- package/dist/operations/generators/grant.ts +336 -0
- package/dist/operations/generators/operation.ts +972 -0
- package/dist/operations/generators/permits.ts +223 -0
- package/dist/operations/generators/seed.ts +347 -0
- package/dist/operations/generators/trial.js +4 -1
- package/dist/operations/generators/trial.ts +529 -0
- package/dist/operations/http.ts +164 -0
- package/dist/operations/makers/capability.ts +166 -0
- package/dist/operations/makers/trial.ts +520 -0
- package/dist/operations/memory.ts +470 -0
- package/dist/operations/metering.ts +328 -0
- package/dist/operations/nodes.ts +54 -0
- package/dist/operations/operations.ts +32 -0
- package/dist/operations/queue-policy.ts +10 -0
- package/dist/operations/result.ts +28 -0
- package/dist/operations/seams.d.ts +4 -4
- package/dist/operations/seams.ts +364 -0
- package/dist/operations/store.ts +29 -0
- package/package.json +8 -1
- package/dist/integrations/generators/sdk.d.ts +0 -1
- package/dist/integrations/generators/sdk.js +0 -148
- /package/dist/integrations/{sdk.d.ts → module.d.ts} +0 -0
package/dist/core/attach.js
CHANGED
|
@@ -73,7 +73,7 @@ async function attaching(into, name) {
|
|
|
73
73
|
}
|
|
74
74
|
catch {
|
|
75
75
|
if (!existsSync(join(into, 'node_modules', owner, 'package.json'))) {
|
|
76
|
-
throw new UnattachableError(owner, `is not installed in this app — run \`npm i ${owner}\` first, because \`thread attach\` takes on a ` +
|
|
76
|
+
throw new UnattachableError(owner, `is not installed in this app — run \`npm i -D ${owner}\` first, because \`thread attach\` takes on a ` +
|
|
77
77
|
'package that is already here rather than fetching one');
|
|
78
78
|
}
|
|
79
79
|
throw new UnattachableError(name, name.endsWith(DOOR) ? `is named in \`attaches\` and ${owner} as installed here ships no such module` : 'exports no `attach`');
|
|
@@ -126,7 +126,7 @@ export async function attach(into, name, touched = new Set(), under = []) {
|
|
|
126
126
|
const one = await attaching(into, name);
|
|
127
127
|
for (const on of one.attaches ?? []) {
|
|
128
128
|
if (!(await attaches(into, on))) {
|
|
129
|
-
throw new UnattachableError(name, `must have ${on} attached before it, and ${on} is not installed here — run \`npm install ${packageOf(on)}\` and attach ${name} again. ` +
|
|
129
|
+
throw new UnattachableError(name, `must have ${on} attached before it, and ${on} is not installed here — run \`npm install -D ${packageOf(on)}\` and attach ${name} again. ` +
|
|
130
130
|
`${on} is named in \`attaches\` and not in dependencies, because a package that only appends a row to a file ` +
|
|
131
131
|
'another package owns would otherwise carry an npm edge it never imports');
|
|
132
132
|
}
|
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import { join, relative, resolve } from 'node:path'
|
|
4
|
+
import { stdout } from 'node:process'
|
|
5
|
+
|
|
6
|
+
import { ROOT, TREE, root as rooted } from './init.ts'
|
|
7
|
+
import { STATED, holdsRow, listBounds, quoted, reconciled, rowsIn, withRow, withoutRow } from './rows.ts'
|
|
8
|
+
import { ours as isOurs } from './scope.ts'
|
|
9
|
+
|
|
10
|
+
const LISTS = ['declares', 'derives', 'seeds']
|
|
11
|
+
|
|
12
|
+
const SHAPE = /export interface [A-Za-z0-9_]+ extends Threaded \{[\s\S]*?\n\}\n/
|
|
13
|
+
|
|
14
|
+
const MEMBERS = 'MEMBERS'
|
|
15
|
+
|
|
16
|
+
const DOOR = '/attach'
|
|
17
|
+
|
|
18
|
+
export const packageOf = (name: string): string =>
|
|
19
|
+
name.startsWith('@') ? name.split('/').slice(0, 2).join('/') : (name.split('/')[0] ?? name)
|
|
20
|
+
|
|
21
|
+
export interface Appending {
|
|
22
|
+
readonly into: string
|
|
23
|
+
readonly list: string
|
|
24
|
+
readonly id: string
|
|
25
|
+
readonly source: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Writing {
|
|
29
|
+
readonly file: string
|
|
30
|
+
readonly source: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Attaching {
|
|
34
|
+
readonly id: string
|
|
35
|
+
readonly writes: readonly Writing[]
|
|
36
|
+
readonly declares: string
|
|
37
|
+
readonly rows?: readonly Appending[]
|
|
38
|
+
|
|
39
|
+
readonly retires?: readonly {
|
|
40
|
+
readonly into: string
|
|
41
|
+
readonly list: string
|
|
42
|
+
readonly id: string
|
|
43
|
+
|
|
44
|
+
readonly made?: string
|
|
45
|
+
|
|
46
|
+
readonly why: string
|
|
47
|
+
}[]
|
|
48
|
+
|
|
49
|
+
readonly attaches?: readonly string[]
|
|
50
|
+
|
|
51
|
+
readonly scripts?: Readonly<Record<string, string>>
|
|
52
|
+
|
|
53
|
+
readonly needs?: Readonly<Record<string, string>>
|
|
54
|
+
|
|
55
|
+
readonly ignores?: readonly string[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class UnattachableError extends Error {
|
|
59
|
+
constructor(name: string, why: string) {
|
|
60
|
+
super(`${name} ${why}. A package that attaches exports an \`ATTACH\` from \`${name.endsWith(DOOR) ? name : `${name}${DOOR}`}\`.`)
|
|
61
|
+
this.name = 'UnattachableError'
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class StillADependencyError extends Error {
|
|
66
|
+
constructor(name: string, of: readonly string[]) {
|
|
67
|
+
super(`${name} is still a dependency of ${of.join(', ')} — detach ${of.length === 1 ? 'it' : 'those'} first, or their rows are left pointing at nothing.`)
|
|
68
|
+
this.name = 'StillADependencyError'
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class StillDeclaredError extends Error {
|
|
73
|
+
constructor(name: string, theirs: readonly string[]) {
|
|
74
|
+
super(
|
|
75
|
+
`${name} writes the files this tree declares ${theirs.slice(0, 6).join(', ')}${theirs.length > 6 ? ` and ${String(theirs.length - 6)} more` : ''} in, so detaching it would leave ${theirs.length === 1 ? 'that' : 'those'} declared by nothing. ` +
|
|
76
|
+
'Remove what this app declared of its own first, or keep the package. Detaching is for a package nothing of yours is built on.',
|
|
77
|
+
)
|
|
78
|
+
this.name = 'StillDeclaredError'
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class DependencyRingError extends Error {
|
|
83
|
+
constructor(ring: readonly string[]) {
|
|
84
|
+
super(`${ring.join(' depends on ')} — a ring has no first package, so there is no order to attach them in.`)
|
|
85
|
+
this.name = 'DependencyRingError'
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class TakenFileError extends Error {
|
|
90
|
+
constructor(name: string, file: string) {
|
|
91
|
+
super(`${file} is already there and the tree does not declare \`${name}\`'s id, so another package holds that name. One file, one owner.`)
|
|
92
|
+
this.name = 'TakenFileError'
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class UnthreadedError extends Error {
|
|
97
|
+
constructor() {
|
|
98
|
+
super(`there is no ${TREE}/${ROOT} here — run \`thread init\` first.`)
|
|
99
|
+
this.name = 'UnthreadedError'
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function attaches(into: string, name: string): Promise<boolean> {
|
|
104
|
+
try {
|
|
105
|
+
await attaching(into, name)
|
|
106
|
+
return true
|
|
107
|
+
} catch {
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function attaching(into: string, name: string): Promise<Attaching> {
|
|
113
|
+
const spec = name.endsWith(DOOR) ? name : `${name}${DOOR}`
|
|
114
|
+
const owner = packageOf(name)
|
|
115
|
+
let held: { ATTACH?: Attaching }
|
|
116
|
+
try {
|
|
117
|
+
held = (await import(createRequire(join(into, 'package.json')).resolve(spec))) as { ATTACH?: Attaching }
|
|
118
|
+
} catch {
|
|
119
|
+
if (!existsSync(join(into, 'node_modules', owner, 'package.json'))) {
|
|
120
|
+
throw new UnattachableError(
|
|
121
|
+
owner,
|
|
122
|
+
`is not installed in this app — run \`npm i -D ${owner}\` first, because \`thread attach\` takes on a ` +
|
|
123
|
+
'package that is already here rather than fetching one',
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
throw new UnattachableError(
|
|
127
|
+
name,
|
|
128
|
+
name.endsWith(DOOR) ? `is named in \`attaches\` and ${owner} as installed here ships no such module` : 'exports no `attach`',
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
if (held.ATTACH === undefined) throw new UnattachableError(name, 'exports no `ATTACH`')
|
|
132
|
+
return held.ATTACH
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function behind(into: string, name: string): Promise<readonly { readonly id: string; readonly says?: string }[]> {
|
|
136
|
+
let one: Attaching
|
|
137
|
+
try {
|
|
138
|
+
one = await attaching(into, name)
|
|
139
|
+
} catch {
|
|
140
|
+
return []
|
|
141
|
+
}
|
|
142
|
+
const dir = resolve(into, TREE)
|
|
143
|
+
const stale: { id: string; says?: string }[] = []
|
|
144
|
+
for (const row of one.rows ?? []) {
|
|
145
|
+
const at = join(dir, row.into)
|
|
146
|
+
if (!existsSync(at)) continue
|
|
147
|
+
const text = readFileSync(at, 'utf8')
|
|
148
|
+
const where = listBounds(text, row.list)
|
|
149
|
+
if (where === undefined) continue
|
|
150
|
+
if (text.slice(where.open, where.close).includes(row.source.trim().replace(/,$/, ''))) continue
|
|
151
|
+
|
|
152
|
+
const says = /by:\s*'(thread [a-z0-9_ -]+)'/.exec(row.source)?.[1]
|
|
153
|
+
const known = says !== undefined && text.slice(where.open, where.close).includes(`by: '${says}'`)
|
|
154
|
+
stale.push(says === undefined || known ? { id: row.id } : { id: row.id, says })
|
|
155
|
+
}
|
|
156
|
+
return stale
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function edit(at: string, change: (text: string) => string, touched: Set<string>, into: string): void {
|
|
160
|
+
const text = readFileSync(at, 'utf8')
|
|
161
|
+
const next = change(text)
|
|
162
|
+
if (next === text) return
|
|
163
|
+
writeFileSync(at, next, 'utf8')
|
|
164
|
+
touched.add(relative(into, at))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function attach(
|
|
168
|
+
into: string,
|
|
169
|
+
name: string,
|
|
170
|
+
touched = new Set<string>(),
|
|
171
|
+
under: readonly string[] = [],
|
|
172
|
+
): Promise<readonly string[]> {
|
|
173
|
+
if (under.includes(name)) throw new DependencyRingError([...under.slice(under.indexOf(name)), name])
|
|
174
|
+
const dir = resolve(into, TREE)
|
|
175
|
+
const root = join(dir, ROOT)
|
|
176
|
+
if (!existsSync(root)) throw new UnthreadedError()
|
|
177
|
+
|
|
178
|
+
const one = await attaching(into, name)
|
|
179
|
+
|
|
180
|
+
for (const on of one.attaches ?? []) {
|
|
181
|
+
if (!(await attaches(into, on))) {
|
|
182
|
+
throw new UnattachableError(
|
|
183
|
+
name,
|
|
184
|
+
`must have ${on} attached before it, and ${on} is not installed here — run \`npm install -D ${packageOf(on)}\` and attach ${name} again. ` +
|
|
185
|
+
`${on} is named in \`attaches\` and not in dependencies, because a package that only appends a row to a file ` +
|
|
186
|
+
'another package owns would otherwise carry an npm edge it never imports',
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
await attach(into, on, touched, [...under, name])
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const plan = new Map<string, string>()
|
|
193
|
+
|
|
194
|
+
const retired: string[] = []
|
|
195
|
+
|
|
196
|
+
const reshaped: string[] = []
|
|
197
|
+
|
|
198
|
+
const noticed: string[] = []
|
|
199
|
+
|
|
200
|
+
const held = readFileSync(root, 'utf8')
|
|
201
|
+
const declares = listBounds(held, 'declares')
|
|
202
|
+
|
|
203
|
+
const ours = [...one.declares.matchAll(/\bid: '([^']+)'/g)].map((found) => found[1])
|
|
204
|
+
const mine =
|
|
205
|
+
declares !== undefined &&
|
|
206
|
+
(holdsRow(held, declares, one.id) || ours.some((id) => id !== undefined && holdsRow(held, declares, id)))
|
|
207
|
+
|
|
208
|
+
const installed = (at: string): boolean => at.startsWith('node_modules/') && isOurs(at.split('/').slice(1, 3).join('/'))
|
|
209
|
+
|
|
210
|
+
const pathIn = (source: string): string | undefined => {
|
|
211
|
+
const listed = `const HELD = {\n declares: [\n${source}\n ],\n}\n`
|
|
212
|
+
const span = listBounds(listed, 'declares')
|
|
213
|
+
return span === undefined ? undefined : rowsIn(listed, span)[0]?.at
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const clashes: { readonly id: string; readonly yours: string; readonly theirs: string }[] = []
|
|
217
|
+
|
|
218
|
+
for (const write of one.writes) {
|
|
219
|
+
const at = join(dir, write.file)
|
|
220
|
+
if (!existsSync(at)) continue
|
|
221
|
+
const there = readFileSync(at, 'utf8')
|
|
222
|
+
for (const list of LISTS) {
|
|
223
|
+
const ships = listBounds(write.source, list)
|
|
224
|
+
const where = listBounds(there, list)
|
|
225
|
+
if (ships === undefined || where === undefined) continue
|
|
226
|
+
const standing = rowsIn(there, where)
|
|
227
|
+
for (const row of rowsIn(write.source, ships)) {
|
|
228
|
+
const held = standing.find((each) => each.id === row.id)
|
|
229
|
+
if (held?.at === undefined || row.at === undefined || held.at === row.at) continue
|
|
230
|
+
if (installed(held.at)) continue
|
|
231
|
+
clashes.push({ id: row.id, yours: held.at, theirs: row.at })
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const row of one.rows ?? []) {
|
|
237
|
+
const target = join(dir, row.into)
|
|
238
|
+
if (!existsSync(target)) continue
|
|
239
|
+
const text = readFileSync(target, 'utf8')
|
|
240
|
+
const where = listBounds(text, row.list)
|
|
241
|
+
if (where === undefined) continue
|
|
242
|
+
const held = rowsIn(text, where).find((each) => each.id === row.id)
|
|
243
|
+
const theirs = pathIn(row.source)
|
|
244
|
+
if (held?.at === undefined || theirs === undefined || held.at === theirs) continue
|
|
245
|
+
if (installed(held.at)) continue
|
|
246
|
+
clashes.push({ id: row.id, yours: held.at, theirs })
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (clashes.length > 0) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`${name} ships rows this tree already declares under the same names, pointing at different files:\n` +
|
|
252
|
+
clashes.map((one) => ` ${one.id} — you declare ${one.yours}, this package ships ${one.theirs}`).join('\n') +
|
|
253
|
+
'\nA name means one thing in a tree, so these cannot both stand. Give yours another name: declare it ' +
|
|
254
|
+
'under the new name with `thread create meta add` FIRST, and only then take the old row out with ' +
|
|
255
|
+
'`thread create meta remove`. In that order a refusal costs nothing. The other way round, a remove ' +
|
|
256
|
+
'that works followed by an add that refuses leaves your file declared by nobody. Nothing has been ' +
|
|
257
|
+
'written here.',
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (const write of one.writes) {
|
|
262
|
+
const at = join(dir, write.file)
|
|
263
|
+
if (existsSync(at) && !mine) throw new TakenFileError(name, `${TREE}/${write.file}`)
|
|
264
|
+
if (!existsSync(at)) {
|
|
265
|
+
plan.set(at, write.source)
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
const there = readFileSync(at, 'utf8')
|
|
269
|
+
let next = reconciled(there, write.source, STATED)
|
|
270
|
+
|
|
271
|
+
const shipped = SHAPE.exec(write.source)?.[0]
|
|
272
|
+
const standing = SHAPE.exec(next)?.[0]
|
|
273
|
+
if (shipped !== undefined && standing !== undefined && shipped !== standing) {
|
|
274
|
+
next = next.replace(standing, shipped)
|
|
275
|
+
reshaped.push(write.file)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
for (const list of LISTS) {
|
|
279
|
+
const ships = listBounds(write.source, list)
|
|
280
|
+
for (const row of ships === undefined ? [] : rowsIn(write.source, ships)) {
|
|
281
|
+
const body = row.source.trim().replace(/,$/, '')
|
|
282
|
+
const where = listBounds(next, list)
|
|
283
|
+
if (where === undefined) break
|
|
284
|
+
if (next.slice(where.open, where.close).includes(body)) continue
|
|
285
|
+
const without = holdsRow(next, where, row.id) ? withoutRow(next, where, row.id) : next
|
|
286
|
+
const bounds = listBounds(without, list)
|
|
287
|
+
if (bounds === undefined) break
|
|
288
|
+
next = withRow(without, bounds, row.source)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (next !== there) plan.set(at, next)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (declares === undefined) {
|
|
296
|
+
const ends = held.lastIndexOf('\n}')
|
|
297
|
+
plan.set(root, `${held.slice(0, ends)}\n declares: [\n${one.declares}\n ],${held.slice(ends)}`)
|
|
298
|
+
} else {
|
|
299
|
+
|
|
300
|
+
const listed = `const HELD = {\n declares: [\n${one.declares}\n ],\n}\n`
|
|
301
|
+
const span = listBounds(listed, 'declares')
|
|
302
|
+
for (const meta of span === undefined ? [] : rowsIn(listed, span)) {
|
|
303
|
+
const text = plan.get(root) ?? held
|
|
304
|
+
const where = listBounds(text, 'declares')
|
|
305
|
+
if (where === undefined) break
|
|
306
|
+
const already = rowsIn(text, where).find((row) => row.id === meta.id)
|
|
307
|
+
if (already !== undefined && already.source.trim() === meta.source.trim()) continue
|
|
308
|
+
const without = already === undefined ? text : withoutRow(text, where, meta.id)
|
|
309
|
+
const bounds = listBounds(without, 'declares')
|
|
310
|
+
if (bounds !== undefined) plan.set(root, withRow(without, bounds, meta.source))
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const today = rooted(into)
|
|
315
|
+
const owns = listBounds(today, 'declares')
|
|
316
|
+
for (const grown of owns === undefined ? [] : rowsIn(today, owns)) {
|
|
317
|
+
const text = plan.get(root) ?? readFileSync(root, 'utf8')
|
|
318
|
+
const where = listBounds(text, 'declares')
|
|
319
|
+
if (where === undefined) break
|
|
320
|
+
if (holdsRow(text, where, grown.id)) continue
|
|
321
|
+
plan.set(root, withRow(text, where, grown.source))
|
|
322
|
+
noticed.push(grown.id)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const atRoot = plan.get(root) ?? readFileSync(root, 'utf8')
|
|
326
|
+
const rootList = listBounds(atRoot, 'declares')
|
|
327
|
+
const holds = new Set(rootList === undefined ? [] : rowsIn(atRoot, rootList).map((row) => row.id))
|
|
328
|
+
for (const row of one.rows ?? []) {
|
|
329
|
+
const coined = /by:\s*'thread create ([a-z0-9_-]+)'/.exec(row.source)?.[1]
|
|
330
|
+
if (coined !== undefined && holds.has(coined)) {
|
|
331
|
+
|
|
332
|
+
const waiting = (one.rows ?? []).filter((each) => {
|
|
333
|
+
const at = join(dir, each.into)
|
|
334
|
+
if (!existsSync(at)) return false
|
|
335
|
+
const text = plan.get(at) ?? readFileSync(at, 'utf8')
|
|
336
|
+
const where = listBounds(text, each.list)
|
|
337
|
+
if (where === undefined) return false
|
|
338
|
+
return !text.slice(where.open, where.close).includes(each.source.trim().replace(/,$/, ''))
|
|
339
|
+
}).length
|
|
340
|
+
const withdrawing = (one.retires ?? []).filter((gone) => {
|
|
341
|
+
const at = join(dir, gone.into)
|
|
342
|
+
if (!existsSync(at)) return false
|
|
343
|
+
const text = plan.get(at) ?? readFileSync(at, 'utf8')
|
|
344
|
+
const where = listBounds(text, gone.list)
|
|
345
|
+
return where !== undefined && holdsRow(text, where, gone.id)
|
|
346
|
+
}).length
|
|
347
|
+
|
|
348
|
+
throw new Error(
|
|
349
|
+
`${name} brings a noun called \`${coined}\`, and this tree already declares something by that name. One ` +
|
|
350
|
+
`word, one meaning: if this attached, \`thread create ${coined}\` would run the package's command and the ` +
|
|
351
|
+
`thing you declared would be reachable only by its mark.\n\n` +
|
|
352
|
+
`Take yours out with \`thread create meta remove ${coined}\` — it refuses if anything is declared under it, ` +
|
|
353
|
+
`so read it first with \`thread create meta\` and move whatever it governs. Then attach again. ` +
|
|
354
|
+
`Nothing was written.\n\n` +
|
|
355
|
+
|
|
356
|
+
`A refusal here names a command you can actually run. If the one above does not fit what you are trying ` +
|
|
357
|
+
`to do, that is a gap in this framework rather than something to work around — say so.` +
|
|
358
|
+
(waiting + withdrawing === 0
|
|
359
|
+
? ''
|
|
360
|
+
: `\n\nAn attach is all or nothing, so this is also holding back everything else ${name} would have ` +
|
|
361
|
+
`delivered: ${String(waiting)} row(s) this tree does not hold at the version installed` +
|
|
362
|
+
`${withdrawing === 0 ? '' : ` and ${String(withdrawing)} it has withdrawn`}. None of that arrives until the ` +
|
|
363
|
+
`collision above is cleared, and nothing else will tell you — \`npm install\` will go on reporting this ` +
|
|
364
|
+
`tree current.`),
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
for (const row of one.rows ?? []) {
|
|
370
|
+
const target = join(dir, row.into)
|
|
371
|
+
|
|
372
|
+
if (!existsSync(target) && !plan.has(target)) {
|
|
373
|
+
throw new UnattachableError(name, `wants a row in ${row.into}, which is not here`)
|
|
374
|
+
}
|
|
375
|
+
const text = plan.get(target) ?? readFileSync(target, 'utf8')
|
|
376
|
+
const where = listBounds(text, row.list)
|
|
377
|
+
if (where === undefined) throw new UnattachableError(name, `wants a row in \`${row.list}\`, which ${row.into} does not export`)
|
|
378
|
+
|
|
379
|
+
if (text.slice(where.open, where.close).includes(row.source)) continue
|
|
380
|
+
|
|
381
|
+
const held = holdsRow(text, where, row.id) ? withoutRow(text, where, row.id) : text
|
|
382
|
+
const bounds = listBounds(held, row.list)
|
|
383
|
+
if (bounds === undefined) throw new UnattachableError(name, `wants a row in \`${row.list}\`, which ${row.into} does not export`)
|
|
384
|
+
plan.set(target, withRow(held, bounds, row.source))
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
for (const gone of one.retires ?? []) {
|
|
388
|
+
const shipped = (one.rows ?? []).find(
|
|
389
|
+
(row) => row.into === gone.into && row.list === gone.list && row.id === gone.id,
|
|
390
|
+
)
|
|
391
|
+
if (shipped !== undefined) {
|
|
392
|
+
throw new UnattachableError(
|
|
393
|
+
name,
|
|
394
|
+
`both ships and retires \`${gone.id}\` in ${gone.into}, so attaching it would add and remove one row`,
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (gone.made !== undefined) {
|
|
399
|
+
const byproduct = join(into, gone.made)
|
|
400
|
+
if (existsSync(byproduct)) {
|
|
401
|
+
rmSync(byproduct)
|
|
402
|
+
retired.push(`${gone.made} — the file \`${gone.id}\` derived, which nothing writes now`)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const target = join(dir, gone.into)
|
|
407
|
+
if (!existsSync(target) && !plan.has(target)) continue
|
|
408
|
+
const text = plan.get(target) ?? readFileSync(target, 'utf8')
|
|
409
|
+
const where = listBounds(text, gone.list)
|
|
410
|
+
if (where === undefined || !holdsRow(text, where, gone.id)) continue
|
|
411
|
+
plan.set(target, withoutRow(text, where, gone.id))
|
|
412
|
+
retired.push(`${gone.into}: ${gone.id} — ${gone.why}`)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (one.scripts !== undefined) {
|
|
416
|
+
const manifest = join(into, 'package.json')
|
|
417
|
+
const held = readFileSync(manifest, 'utf8')
|
|
418
|
+
const parsed = JSON.parse(held) as Record<string, unknown> & { scripts?: Record<string, string> }
|
|
419
|
+
const scripts = { ...parsed.scripts }
|
|
420
|
+
let moved = false
|
|
421
|
+
for (const [name, runs] of Object.entries(one.scripts)) {
|
|
422
|
+
if (scripts[name] === runs) continue
|
|
423
|
+
scripts[name] = runs
|
|
424
|
+
moved = true
|
|
425
|
+
}
|
|
426
|
+
if (moved) plan.set(manifest, `${JSON.stringify({ ...parsed, scripts }, null, 2)}\n`)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (one.needs !== undefined) {
|
|
430
|
+
const manifest = join(into, 'package.json')
|
|
431
|
+
|
|
432
|
+
const held = plan.get(manifest) ?? readFileSync(manifest, 'utf8')
|
|
433
|
+
const parsed = JSON.parse(held) as Record<string, unknown> & { dependencies?: Record<string, string> }
|
|
434
|
+
const needs = { ...parsed.dependencies }
|
|
435
|
+
const added: string[] = []
|
|
436
|
+
for (const [name, at] of Object.entries(one.needs)) {
|
|
437
|
+
if (needs[name] !== undefined) continue
|
|
438
|
+
needs[name] = at
|
|
439
|
+
added.push(name)
|
|
440
|
+
}
|
|
441
|
+
if (added.length > 0) {
|
|
442
|
+
plan.set(manifest, `${JSON.stringify({ ...parsed, dependencies: needs }, null, 2)}\n`)
|
|
443
|
+
stdout.write(`thread: added ${added.join(', ')} to dependencies — run \`npm install\` to fetch ${added.length === 1 ? 'it' : 'them'}\n`)
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (one.ignores !== undefined && one.ignores.length > 0) {
|
|
448
|
+
const at = join(into, '.gitignore')
|
|
449
|
+
const had = existsSync(at) ? readFileSync(at, 'utf8') : ''
|
|
450
|
+
const lines = had.split('\n').map((line) => line.trim())
|
|
451
|
+
const adding = one.ignores.filter((path) => !lines.includes(path))
|
|
452
|
+
if (adding.length > 0) {
|
|
453
|
+
plan.set(at, `${had === '' ? '' : had.replace(/\n*$/, '\n')}${adding.join('\n')}\n`)
|
|
454
|
+
stdout.write(`thread: ignoring ${adding.join(', ')} — written by this framework, not declared by your tree\n`)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
for (const [path, text] of plan) {
|
|
459
|
+
writeFileSync(path, text, 'utf8')
|
|
460
|
+
touched.add(relative(into, path))
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
for (const line of retired) stdout.write(`thread: retired ${line}\n`)
|
|
464
|
+
for (const file of reshaped) stdout.write(`thread: reshaped ${file} — what a row may state has changed\n`)
|
|
465
|
+
for (const id of noticed) stdout.write(`thread: declared ${id} — a file this app grew after \`thread init\` ran\n`)
|
|
466
|
+
|
|
467
|
+
return [...touched].sort()
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function dependents(into: string, name: string): Promise<readonly string[]> {
|
|
471
|
+
const root = readFileSync(join(resolve(into, TREE), ROOT), 'utf8')
|
|
472
|
+
const manifest = JSON.parse(readFileSync(join(into, 'package.json'), 'utf8')) as {
|
|
473
|
+
dependencies?: Record<string, string>
|
|
474
|
+
devDependencies?: Record<string, string>
|
|
475
|
+
}
|
|
476
|
+
const bounds = listBounds(root, 'declares')
|
|
477
|
+
if (bounds === undefined) return []
|
|
478
|
+
const found: string[] = []
|
|
479
|
+
for (const other of [...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.devDependencies ?? {})]) {
|
|
480
|
+
if (other === name) continue
|
|
481
|
+
let held: Attaching
|
|
482
|
+
try {
|
|
483
|
+
held = await attaching(into, other)
|
|
484
|
+
} catch {
|
|
485
|
+
continue
|
|
486
|
+
}
|
|
487
|
+
if (holdsRow(root, bounds, held.id) && (held.attaches ?? []).includes(name)) found.push(other)
|
|
488
|
+
}
|
|
489
|
+
return found.sort()
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export async function detach(into: string, name: string): Promise<readonly string[]> {
|
|
493
|
+
const dir = resolve(into, TREE)
|
|
494
|
+
const root = join(dir, ROOT)
|
|
495
|
+
if (!existsSync(root)) throw new UnthreadedError()
|
|
496
|
+
|
|
497
|
+
const of = await dependents(into, name)
|
|
498
|
+
if (of.length > 0) throw new StillADependencyError(name, of)
|
|
499
|
+
|
|
500
|
+
const one = await attaching(into, name)
|
|
501
|
+
|
|
502
|
+
const owned = new Set((one.rows ?? []).map((row) => row.id))
|
|
503
|
+
const theirs: string[] = []
|
|
504
|
+
for (const write of one.writes) {
|
|
505
|
+
const at = join(dir, write.file)
|
|
506
|
+
if (!existsSync(at)) continue
|
|
507
|
+
const text = readFileSync(at, 'utf8')
|
|
508
|
+
for (const list of [...LISTS, MEMBERS]) {
|
|
509
|
+
const where = listBounds(text, list)
|
|
510
|
+
if (where === undefined) continue
|
|
511
|
+
for (const row of rowsIn(text, where)) if (!owned.has(row.id)) theirs.push(row.id)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (theirs.length > 0) throw new StillDeclaredError(name, theirs)
|
|
515
|
+
|
|
516
|
+
const touched = new Set<string>()
|
|
517
|
+
|
|
518
|
+
for (const row of one.rows ?? []) {
|
|
519
|
+
const target = join(dir, row.into)
|
|
520
|
+
if (!existsSync(target)) continue
|
|
521
|
+
edit(target, (text) => {
|
|
522
|
+
const where = listBounds(text, row.list)
|
|
523
|
+
return where === undefined ? text : withoutRow(text, where, row.id)
|
|
524
|
+
}, touched, into)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
edit(root, (text) => {
|
|
528
|
+
const where = listBounds(text, 'declares')
|
|
529
|
+
return where === undefined ? text : withoutRow(text, where, one.id)
|
|
530
|
+
}, touched, into)
|
|
531
|
+
|
|
532
|
+
for (const write of one.writes) {
|
|
533
|
+
const at = join(dir, write.file)
|
|
534
|
+
if (!existsSync(at)) continue
|
|
535
|
+
const text = readFileSync(at, 'utf8')
|
|
536
|
+
const kept = [...LISTS, MEMBERS].some((list) => {
|
|
537
|
+
const where = listBounds(text, list)
|
|
538
|
+
return where !== undefined && rowsIn(text, where).length > 0
|
|
539
|
+
})
|
|
540
|
+
if (kept) continue
|
|
541
|
+
rmSync(at)
|
|
542
|
+
touched.add(relative(into, at))
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return [...touched].sort()
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const DESCRIBES =
|
|
549
|
+
'The package every tree installs first: the descent, the CLI, and the makers and generators that used to be separate packages.'
|
|
550
|
+
|
|
551
|
+
const CORE_SOURCE = `import type { Threaded } from '../core/thread.ts'
|
|
552
|
+
|
|
553
|
+
export const CORE: Threaded = {
|
|
554
|
+
id: 'core',
|
|
555
|
+
at: '_threads/core.ts',
|
|
556
|
+
describes: ${quoted(DESCRIBES)},
|
|
557
|
+
version: 1,
|
|
558
|
+
declares: [],
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export const GENERATORS: readonly Threaded[] = []
|
|
562
|
+
`
|
|
563
|
+
|
|
564
|
+
const SEEDED = {
|
|
565
|
+
attaches: ['@prestack/thread/derivation/attach', '@prestack/thread/integrations/attach', '@prestack/thread/integrity/attach', '@prestack/thread/knowledge/attach', '@prestack/thread/operations/attach'] as const,
|
|
566
|
+
id: 'core',
|
|
567
|
+
declares: [
|
|
568
|
+
' {',
|
|
569
|
+
' id: \'core\',',
|
|
570
|
+
' at: \'_threads/core.ts\',',
|
|
571
|
+
' describes: \'The package every tree installs first: the descent, the CLI, and the makers and generators that used to be separate packages.\',',
|
|
572
|
+
' version: 1,',
|
|
573
|
+
' },',
|
|
574
|
+
].join('\n'),
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const SHIPPED: readonly { readonly into: string; readonly list: string; readonly id: string; readonly source: string }[] = [
|
|
578
|
+
]
|
|
579
|
+
export const ATTACH: Attaching = {
|
|
580
|
+
attaches: SEEDED.attaches,
|
|
581
|
+
id: SEEDED.id,
|
|
582
|
+
declares: SEEDED.declares,
|
|
583
|
+
writes: [{ file: 'core.ts', source: CORE_SOURCE }],
|
|
584
|
+
rows: SHIPPED,
|
|
585
|
+
}
|
package/dist/core/chain.js
CHANGED
|
@@ -65,12 +65,19 @@ export function batched(at, commands, within) {
|
|
|
65
65
|
held = [];
|
|
66
66
|
}
|
|
67
67
|
const orphaned = held.join('\n').trim();
|
|
68
|
+
const died = out.error !== undefined
|
|
69
|
+
? ` The runner itself failed to start or was stopped: ${out.error.message}.`
|
|
70
|
+
: out.signal !== null && out.signal !== undefined
|
|
71
|
+
? ` The runner was killed by ${out.signal}, so everything after the command it was running is unreported rather than refused.`
|
|
72
|
+
: found.size === 0 && out.status !== 0
|
|
73
|
+
? ` The runner exited ${String(out.status)} having reported nothing, so it died before the first command finished.`
|
|
74
|
+
: '';
|
|
68
75
|
let firstUnreached = true;
|
|
69
76
|
return commands.map((one) => {
|
|
70
77
|
const has = found.get(one);
|
|
71
78
|
if (has !== undefined)
|
|
72
79
|
return has;
|
|
73
|
-
const why = `settle: the runner never reported on \`${one}\`, so it was not run and is not settled
|
|
80
|
+
const why = `settle: the runner never reported on \`${one}\`, so it was not run and is not settled.${died}`;
|
|
74
81
|
const said = firstUnreached && orphaned !== '' ? `${orphaned}\n${why}` : why;
|
|
75
82
|
firstUnreached = false;
|
|
76
83
|
return { command: one, ok: false, said };
|