@prestack/thread 0.1.21 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/dist/core/attach.js +21 -0
  3. package/dist/core/descend.js +6 -3
  4. package/dist/core/generators/commit.js +55 -3
  5. package/dist/core/generators/dev.d.ts +1 -0
  6. package/dist/core/generators/dev.js +68 -0
  7. package/dist/core/tree.d.ts +1 -0
  8. package/dist/core/tree.js +7 -1
  9. package/dist/derivation/generators/build.d.ts +3 -1
  10. package/dist/derivation/generators/build.js +103 -22
  11. package/dist/derivation/generators/release.js +74 -26
  12. package/dist/derivation/release.js +3 -3
  13. package/dist/integrations/cli.js +69 -10
  14. package/dist/integrations/pglite.js +13 -3
  15. package/dist/integrations/target.d.ts +3 -0
  16. package/dist/integrity/attach.js +1 -5
  17. package/dist/integrity/slopnators/sl_stale_path.js +10 -0
  18. package/dist/operations/access.js +3 -2
  19. package/dist/operations/capabilities.d.ts +2 -0
  20. package/dist/operations/capabilities.js +2 -0
  21. package/dist/operations/generators/capability.js +234 -24
  22. package/dist/operations/generators/compile.js +150 -50
  23. package/dist/operations/generators/seed.d.ts +3 -0
  24. package/dist/operations/generators/seed.js +25 -3
  25. package/dist/operations/nodes.d.ts +2 -2
  26. package/package.json +1 -1
  27. package/dist/core/actor.ts +0 -21
  28. package/dist/core/attach.ts +0 -613
  29. package/dist/core/chain.ts +0 -145
  30. package/dist/core/condition.ts +0 -227
  31. package/dist/core/descend.ts +0 -1185
  32. package/dist/core/errors/catalogue.ts +0 -314
  33. package/dist/core/errors/classes.ts +0 -51
  34. package/dist/core/errors/error.ts +0 -152
  35. package/dist/core/errors/namespaces.ts +0 -19
  36. package/dist/core/finding.ts +0 -37
  37. package/dist/core/generators/commit.ts +0 -157
  38. package/dist/core/init.ts +0 -222
  39. package/dist/core/invoked.ts +0 -52
  40. package/dist/core/round.ts +0 -74
  41. package/dist/core/rows.ts +0 -506
  42. package/dist/core/scope.ts +0 -20
  43. package/dist/core/thread.ts +0 -29
  44. package/dist/core/tree.ts +0 -90
  45. package/dist/core/types.ts +0 -351
  46. package/dist/derivation/attach.ts +0 -57
  47. package/dist/derivation/generators/build.ts +0 -371
  48. package/dist/derivation/generators/check.ts +0 -118
  49. package/dist/derivation/generators/determinism.ts +0 -145
  50. package/dist/derivation/generators/release.ts +0 -236
  51. package/dist/derivation/release.ts +0 -43
  52. package/dist/integrations/attach.ts +0 -187
  53. package/dist/integrations/cli.ts +0 -606
  54. package/dist/integrations/declared.ts +0 -34
  55. package/dist/integrations/disk.ts +0 -466
  56. package/dist/integrations/generators/read.ts +0 -127
  57. package/dist/integrations/generators/vault.ts +0 -210
  58. package/dist/integrations/https.ts +0 -216
  59. package/dist/integrations/pglite.ts +0 -206
  60. package/dist/integrations/secret.ts +0 -54
  61. package/dist/integrations/target.ts +0 -672
  62. package/dist/integrations/vault.ts +0 -148
  63. package/dist/integrity/attach.ts +0 -150
  64. package/dist/integrity/check-threads.ts +0 -166
  65. package/dist/integrity/constraint.ts +0 -1246
  66. package/dist/integrity/generators/compiler.ts +0 -138
  67. package/dist/integrity/generators/gate.ts +0 -227
  68. package/dist/integrity/generators/issues.ts +0 -160
  69. package/dist/integrity/generators/publish.ts +0 -233
  70. package/dist/integrity/generators/trial.ts +0 -359
  71. package/dist/integrity/lint.ts +0 -214
  72. package/dist/integrity/lsp/bin.ts +0 -44
  73. package/dist/integrity/lsp/diagnostics.ts +0 -92
  74. package/dist/integrity/lsp/findings.ts +0 -63
  75. package/dist/integrity/lsp/lint.ts +0 -124
  76. package/dist/integrity/lsp/protocol.ts +0 -78
  77. package/dist/integrity/lsp/server.ts +0 -224
  78. package/dist/integrity/slopnator.ts +0 -324
  79. package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
  80. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
  81. package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
  82. package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
  83. package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
  84. package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
  85. package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
  86. package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
  87. package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
  88. package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
  89. package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
  90. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
  91. package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
  92. package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
  93. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
  94. package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
  95. package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
  96. package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
  97. package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
  98. package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
  99. package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
  100. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
  101. package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
  102. package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
  103. package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
  104. package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
  105. package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
  106. package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
  107. package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
  108. package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
  109. package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
  110. package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
  111. package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
  112. package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
  113. package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
  114. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
  115. package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
  116. package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
  117. package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
  118. package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
  119. package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
  120. package/dist/integrity/sweep.ts +0 -395
  121. package/dist/knowledge/attach.ts +0 -327
  122. package/dist/knowledge/generators/docsgen.ts +0 -186
  123. package/dist/knowledge/generators/glossary.ts +0 -73
  124. package/dist/knowledge/glossary.ts +0 -6
  125. package/dist/operations/access.ts +0 -131
  126. package/dist/operations/attach.ts +0 -408
  127. package/dist/operations/capabilities.ts +0 -88
  128. package/dist/operations/dispatch.ts +0 -294
  129. package/dist/operations/error-results.ts +0 -31
  130. package/dist/operations/generators/capability.ts +0 -1054
  131. package/dist/operations/generators/compile.ts +0 -701
  132. package/dist/operations/generators/permits.ts +0 -229
  133. package/dist/operations/generators/seed.ts +0 -359
  134. package/dist/operations/generators/trial.ts +0 -666
  135. package/dist/operations/nodes.ts +0 -53
  136. package/dist/operations/operations.ts +0 -91
  137. package/dist/operations/result.ts +0 -28
@@ -14,7 +14,7 @@ export declare const nd_read: <I, R extends string = never>(block: (opened: Read
14
14
  export declare const nd_action: <I, R extends string = never>(block: (opened: Opened<R>, held: Held<I>) => Promise<unknown>) => {
15
15
  type: 'nd_action';
16
16
  };
17
- export declare const nd_branch: <I, R extends string = never>(choose: (opened: Reads<Opened<R>>, held: Held<I>) => Promise<string>, ways: Readonly<Record<string, (opened: Opened<R>, held: Held<I>) => Promise<unknown>>>) => {
17
+ export declare const nd_branch: <I, R extends string = never>(choose: (opened: Reads<Opened<R>>, held: Held<I>) => Promise<string>, ways?: Readonly<Record<string, (opened: Opened<R>, held: Held<I>) => Promise<unknown>>>) => {
18
18
  type: 'nd_branch';
19
19
  };
20
20
  export declare const nd_each: <I, R extends string = never>(over: (held: Held<I>) => readonly unknown[], block: (item: unknown, opened: Opened<R>, held: Held<I>) => Promise<unknown>) => {
@@ -23,7 +23,7 @@ export declare const nd_each: <I, R extends string = never>(over: (held: Held<I>
23
23
  export declare const nd_parallel: <I, R extends string = never>(blocks: readonly ((opened: Opened<R>, held: Held<I>) => Promise<unknown>)[]) => {
24
24
  type: 'nd_parallel';
25
25
  };
26
- export declare const nd_repeat: <I, R extends string = never>(ceiling: number, block: (pass: number, carried: unknown, opened: Opened<R>, held: Held<I>) => Promise<{
26
+ export declare const nd_repeat: <I, R extends string = never>(ceiling: number, block?: (pass: number, carried: unknown, opened: Opened<R>, held: Held<I>) => Promise<{
27
27
  readonly done: boolean;
28
28
  readonly value: unknown;
29
29
  }>) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prestack/thread",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -1,21 +0,0 @@
1
- export type Env = 'sandbox' | 'production'
2
-
3
- export const ENVIRONMENT = [
4
- ['run', 'x-request-id', 'crypto.randomUUID()'],
5
- ['address', 'x-forwarded-for', null],
6
- ['device', 'user-agent', null],
7
- ['replay', 'idempotency-key', null],
8
- ['moment', null, 'new Date().toISOString()'],
9
- ] as const
10
-
11
- export type Environment = { readonly [K in (typeof ENVIRONMENT)[number][0]]?: string }
12
-
13
- export type Actor = Environment & {
14
- readonly id: string | null
15
- readonly roles: readonly string[]
16
- readonly entitlements: readonly string[]
17
- }
18
-
19
- export const PUBLIC_ROLE = 'role_public'
20
-
21
- export const ANONYMOUS: Actor = { id: null, roles: [PUBLIC_ROLE], entitlements: [] }
@@ -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
- }