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