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