@prestack/thread 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/dist/core/actor.ts +20 -0
  2. package/dist/core/asking.d.ts +0 -1
  3. package/dist/core/asking.js +0 -1
  4. package/dist/core/asking.ts +1342 -0
  5. package/dist/core/attach.js +2 -2
  6. package/dist/core/attach.ts +585 -0
  7. package/dist/core/chain.js +8 -1
  8. package/dist/core/chain.ts +148 -0
  9. package/dist/core/cli.js +97 -18
  10. package/dist/core/cli.ts +2004 -0
  11. package/dist/core/condition.ts +227 -0
  12. package/dist/core/descend.d.ts +1 -0
  13. package/dist/core/descend.js +36 -0
  14. package/dist/core/descend.ts +1001 -0
  15. package/dist/core/finding.ts +38 -0
  16. package/dist/core/generators/meta.js +6 -0
  17. package/dist/core/generators/meta.ts +1006 -0
  18. package/dist/core/init.ts +170 -0
  19. package/dist/core/invoked.ts +47 -0
  20. package/dist/core/round.ts +57 -0
  21. package/dist/core/rows.ts +369 -0
  22. package/dist/core/scope.ts +18 -0
  23. package/dist/core/thread.ts +29 -0
  24. package/dist/core/tree.ts +67 -0
  25. package/dist/core/types.ts +350 -0
  26. package/dist/derivation/attach.js +1 -1
  27. package/dist/derivation/attach.ts +68 -0
  28. package/dist/derivation/generators/build.ts +232 -0
  29. package/dist/derivation/generators/check.ts +118 -0
  30. package/dist/derivation/generators/derivation.ts +347 -0
  31. package/dist/derivation/generators/determinism.ts +145 -0
  32. package/dist/derivation/generators/release.js +19 -9
  33. package/dist/derivation/generators/release.ts +213 -0
  34. package/dist/derivation/makers/release.ts +135 -0
  35. package/dist/derivation/release.ts +23 -0
  36. package/dist/integrations/attach.js +2 -2
  37. package/dist/integrations/attach.ts +207 -0
  38. package/dist/integrations/generators/read.ts +127 -0
  39. package/dist/integrations/generators/secret.ts +289 -0
  40. package/dist/integrations/generators/target.js +16 -6
  41. package/dist/integrations/generators/target.ts +944 -0
  42. package/dist/integrations/generators/vault.js +46 -14
  43. package/dist/integrations/generators/vault.ts +212 -0
  44. package/dist/integrations/{sdk.js → module.js} +4 -4
  45. package/dist/integrations/module.ts +189 -0
  46. package/dist/integrations/secret.ts +50 -0
  47. package/dist/integrations/target.d.ts +3 -1
  48. package/dist/integrations/target.js +14 -11
  49. package/dist/integrations/target.ts +340 -0
  50. package/dist/integrations/vault.d.ts +2 -0
  51. package/dist/integrations/vault.js +4 -2
  52. package/dist/integrations/vault.ts +136 -0
  53. package/dist/integrity/attach.ts +234 -0
  54. package/dist/integrity/check-threads.js +2 -26
  55. package/dist/integrity/check-threads.ts +166 -0
  56. package/dist/integrity/constraint.ts +1202 -0
  57. package/dist/integrity/generators/compiler.ts +161 -0
  58. package/dist/integrity/generators/gate.ts +201 -0
  59. package/dist/integrity/generators/issues.ts +160 -0
  60. package/dist/integrity/generators/publish.js +14 -5
  61. package/dist/integrity/generators/publish.ts +247 -0
  62. package/dist/integrity/generators/rule.ts +271 -0
  63. package/dist/integrity/generators/seal.ts +117 -0
  64. package/dist/integrity/generators/trial.ts +320 -0
  65. package/dist/integrity/lint.ts +227 -0
  66. package/dist/integrity/lsp/bin.ts +42 -0
  67. package/dist/integrity/lsp/diagnostics.ts +92 -0
  68. package/dist/integrity/lsp/findings.ts +63 -0
  69. package/dist/integrity/lsp/lint.ts +140 -0
  70. package/dist/integrity/lsp/protocol.ts +78 -0
  71. package/dist/integrity/lsp/server.ts +224 -0
  72. package/dist/integrity/sealed.js +2 -2
  73. package/dist/integrity/sealed.ts +56 -0
  74. package/dist/integrity/slopnator.ts +350 -0
  75. package/dist/integrity/slopnators/sl_broken_lineage.ts +36 -0
  76. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +39 -0
  77. package/dist/integrity/slopnators/sl_derived_drift.ts +98 -0
  78. package/dist/integrity/slopnators/sl_hardcoded_style.ts +55 -0
  79. package/dist/integrity/slopnators/sl_inert_island.ts +91 -0
  80. package/dist/integrity/slopnators/sl_inert_slopnator.ts +93 -0
  81. package/dist/integrity/slopnators/sl_missing_row.ts +182 -0
  82. package/dist/integrity/slopnators/sl_no_barrel.ts +48 -0
  83. package/dist/integrity/slopnators/sl_no_child_process.ts +74 -0
  84. package/dist/integrity/slopnators/sl_no_comment.ts +43 -0
  85. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +328 -0
  86. package/dist/integrity/slopnators/sl_no_import_cycle.ts +50 -0
  87. package/dist/integrity/slopnators/sl_no_such_command.ts +64 -0
  88. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +63 -0
  89. package/dist/integrity/slopnators/sl_secret_literal.ts +100 -0
  90. package/dist/integrity/slopnators/sl_stale_path.ts +94 -0
  91. package/dist/integrity/slopnators/sl_twice_meant.ts +31 -0
  92. package/dist/integrity/slopnators/sl_twice_parented.ts +37 -0
  93. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +87 -0
  94. package/dist/integrity/slopnators/sl_undeclared_destination.ts +65 -0
  95. package/dist/integrity/slopnators/sl_undeclared_element.ts +44 -0
  96. package/dist/integrity/slopnators/sl_undeclared_node.ts +69 -0
  97. package/dist/integrity/slopnators/sl_undeclared_operation.ts +147 -0
  98. package/dist/integrity/slopnators/sl_undeclared_variant.ts +148 -0
  99. package/dist/integrity/slopnators/sl_undescribed.ts +65 -0
  100. package/dist/integrity/slopnators/sl_ungoverned_island.ts +61 -0
  101. package/dist/integrity/slopnators/sl_unkept_rows.ts +111 -0
  102. package/dist/integrity/slopnators/sl_unnamed_island.ts +41 -0
  103. package/dist/integrity/slopnators/sl_unpaired_member.ts +131 -0
  104. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +58 -0
  105. package/dist/integrity/slopnators/sl_unprefixed_member.ts +136 -0
  106. package/dist/integrity/slopnators/sl_unseamed_reach.ts +79 -0
  107. package/dist/integrity/slopnators/sl_unthreaded.ts +419 -0
  108. package/dist/integrity/slopnators/sl_untranslated_key.ts +164 -0
  109. package/dist/integrity/slopnators/sl_unversioned.ts +36 -0
  110. package/dist/integrity/sweep.ts +307 -0
  111. package/dist/knowledge/attach.ts +350 -0
  112. package/dist/knowledge/generators/docsgen.ts +174 -0
  113. package/dist/knowledge/generators/glossary.ts +73 -0
  114. package/dist/knowledge/generators/skills.ts +263 -0
  115. package/dist/knowledge/glossary.ts +6 -0
  116. package/dist/operations/access.ts +88 -0
  117. package/dist/operations/attach.ts +475 -0
  118. package/dist/operations/capabilities.ts +65 -0
  119. package/dist/operations/cost.ts +58 -0
  120. package/dist/operations/dispatch.ts +348 -0
  121. package/dist/operations/error-results.ts +31 -0
  122. package/dist/operations/errors/catalog.ts +168 -0
  123. package/dist/operations/errors/classes.ts +51 -0
  124. package/dist/operations/errors/errors.ts +314 -0
  125. package/dist/operations/generators/capability.js +29 -19
  126. package/dist/operations/generators/capability.ts +704 -0
  127. package/dist/operations/generators/compile.js +20 -1
  128. package/dist/operations/generators/compile.ts +643 -0
  129. package/dist/operations/generators/grant.ts +336 -0
  130. package/dist/operations/generators/operation.ts +972 -0
  131. package/dist/operations/generators/permits.ts +223 -0
  132. package/dist/operations/generators/seed.ts +347 -0
  133. package/dist/operations/generators/trial.js +4 -1
  134. package/dist/operations/generators/trial.ts +529 -0
  135. package/dist/operations/http.ts +164 -0
  136. package/dist/operations/makers/capability.ts +166 -0
  137. package/dist/operations/makers/trial.ts +520 -0
  138. package/dist/operations/memory.ts +470 -0
  139. package/dist/operations/metering.ts +328 -0
  140. package/dist/operations/nodes.ts +54 -0
  141. package/dist/operations/operations.ts +32 -0
  142. package/dist/operations/queue-policy.ts +10 -0
  143. package/dist/operations/result.ts +28 -0
  144. package/dist/operations/seams.d.ts +4 -4
  145. package/dist/operations/seams.ts +364 -0
  146. package/dist/operations/store.ts +29 -0
  147. package/package.json +8 -1
  148. package/dist/integrations/generators/sdk.d.ts +0 -1
  149. package/dist/integrations/generators/sdk.js +0 -148
  150. /package/dist/integrations/{sdk.d.ts → module.d.ts} +0 -0
@@ -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
+ }
@@ -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 };