@prestack/thread 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/actor.ts +20 -0
- package/dist/core/asking.d.ts +0 -1
- package/dist/core/asking.js +0 -1
- package/dist/core/asking.ts +1342 -0
- package/dist/core/attach.js +2 -2
- package/dist/core/attach.ts +585 -0
- package/dist/core/chain.js +8 -1
- package/dist/core/chain.ts +148 -0
- package/dist/core/cli.js +97 -18
- package/dist/core/cli.ts +2004 -0
- package/dist/core/condition.ts +227 -0
- package/dist/core/descend.d.ts +1 -0
- package/dist/core/descend.js +36 -0
- package/dist/core/descend.ts +1001 -0
- package/dist/core/finding.ts +38 -0
- package/dist/core/generators/meta.js +6 -0
- package/dist/core/generators/meta.ts +1006 -0
- package/dist/core/init.ts +170 -0
- package/dist/core/invoked.ts +47 -0
- package/dist/core/round.ts +57 -0
- package/dist/core/rows.ts +369 -0
- package/dist/core/scope.ts +18 -0
- package/dist/core/thread.ts +29 -0
- package/dist/core/tree.ts +67 -0
- package/dist/core/types.ts +350 -0
- package/dist/derivation/attach.js +1 -1
- package/dist/derivation/attach.ts +68 -0
- package/dist/derivation/generators/build.ts +232 -0
- package/dist/derivation/generators/check.ts +118 -0
- package/dist/derivation/generators/derivation.ts +347 -0
- package/dist/derivation/generators/determinism.ts +145 -0
- package/dist/derivation/generators/release.js +19 -9
- package/dist/derivation/generators/release.ts +213 -0
- package/dist/derivation/makers/release.ts +135 -0
- package/dist/derivation/release.ts +23 -0
- package/dist/integrations/attach.js +2 -2
- package/dist/integrations/attach.ts +207 -0
- package/dist/integrations/generators/read.ts +127 -0
- package/dist/integrations/generators/secret.ts +289 -0
- package/dist/integrations/generators/target.js +16 -6
- package/dist/integrations/generators/target.ts +944 -0
- package/dist/integrations/generators/vault.js +46 -14
- package/dist/integrations/generators/vault.ts +212 -0
- package/dist/integrations/{sdk.js → module.js} +4 -4
- package/dist/integrations/module.ts +189 -0
- package/dist/integrations/secret.ts +50 -0
- package/dist/integrations/target.d.ts +3 -1
- package/dist/integrations/target.js +14 -11
- package/dist/integrations/target.ts +340 -0
- package/dist/integrations/vault.d.ts +2 -0
- package/dist/integrations/vault.js +4 -2
- package/dist/integrations/vault.ts +136 -0
- package/dist/integrity/attach.ts +234 -0
- package/dist/integrity/check-threads.js +2 -26
- package/dist/integrity/check-threads.ts +166 -0
- package/dist/integrity/constraint.ts +1202 -0
- package/dist/integrity/generators/compiler.ts +161 -0
- package/dist/integrity/generators/gate.ts +201 -0
- package/dist/integrity/generators/issues.ts +160 -0
- package/dist/integrity/generators/publish.js +14 -5
- package/dist/integrity/generators/publish.ts +247 -0
- package/dist/integrity/generators/rule.ts +271 -0
- package/dist/integrity/generators/seal.ts +117 -0
- package/dist/integrity/generators/trial.ts +320 -0
- package/dist/integrity/lint.ts +227 -0
- package/dist/integrity/lsp/bin.ts +42 -0
- package/dist/integrity/lsp/diagnostics.ts +92 -0
- package/dist/integrity/lsp/findings.ts +63 -0
- package/dist/integrity/lsp/lint.ts +140 -0
- package/dist/integrity/lsp/protocol.ts +78 -0
- package/dist/integrity/lsp/server.ts +224 -0
- package/dist/integrity/sealed.js +2 -2
- package/dist/integrity/sealed.ts +56 -0
- package/dist/integrity/slopnator.ts +350 -0
- package/dist/integrity/slopnators/sl_broken_lineage.ts +36 -0
- package/dist/integrity/slopnators/sl_declared_elsewhere.ts +39 -0
- package/dist/integrity/slopnators/sl_derived_drift.ts +98 -0
- package/dist/integrity/slopnators/sl_hardcoded_style.ts +55 -0
- package/dist/integrity/slopnators/sl_inert_island.ts +91 -0
- package/dist/integrity/slopnators/sl_inert_slopnator.ts +93 -0
- package/dist/integrity/slopnators/sl_missing_row.ts +182 -0
- package/dist/integrity/slopnators/sl_no_barrel.ts +48 -0
- package/dist/integrity/slopnators/sl_no_child_process.ts +74 -0
- package/dist/integrity/slopnators/sl_no_comment.ts +43 -0
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +328 -0
- package/dist/integrity/slopnators/sl_no_import_cycle.ts +50 -0
- package/dist/integrity/slopnators/sl_no_such_command.ts +64 -0
- package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +63 -0
- package/dist/integrity/slopnators/sl_secret_literal.ts +100 -0
- package/dist/integrity/slopnators/sl_stale_path.ts +94 -0
- package/dist/integrity/slopnators/sl_twice_meant.ts +31 -0
- package/dist/integrity/slopnators/sl_twice_parented.ts +37 -0
- package/dist/integrity/slopnators/sl_undeclared_crossing.ts +87 -0
- package/dist/integrity/slopnators/sl_undeclared_destination.ts +65 -0
- package/dist/integrity/slopnators/sl_undeclared_element.ts +44 -0
- package/dist/integrity/slopnators/sl_undeclared_node.ts +69 -0
- package/dist/integrity/slopnators/sl_undeclared_operation.ts +147 -0
- package/dist/integrity/slopnators/sl_undeclared_variant.ts +148 -0
- package/dist/integrity/slopnators/sl_undescribed.ts +65 -0
- package/dist/integrity/slopnators/sl_ungoverned_island.ts +61 -0
- package/dist/integrity/slopnators/sl_unkept_rows.ts +111 -0
- package/dist/integrity/slopnators/sl_unnamed_island.ts +41 -0
- package/dist/integrity/slopnators/sl_unpaired_member.ts +131 -0
- package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +58 -0
- package/dist/integrity/slopnators/sl_unprefixed_member.ts +136 -0
- package/dist/integrity/slopnators/sl_unseamed_reach.ts +79 -0
- package/dist/integrity/slopnators/sl_unthreaded.ts +419 -0
- package/dist/integrity/slopnators/sl_untranslated_key.ts +164 -0
- package/dist/integrity/slopnators/sl_unversioned.ts +36 -0
- package/dist/integrity/sweep.ts +307 -0
- package/dist/knowledge/attach.ts +350 -0
- package/dist/knowledge/generators/docsgen.ts +174 -0
- package/dist/knowledge/generators/glossary.ts +73 -0
- package/dist/knowledge/generators/skills.ts +263 -0
- package/dist/knowledge/glossary.ts +6 -0
- package/dist/operations/access.ts +88 -0
- package/dist/operations/attach.ts +475 -0
- package/dist/operations/capabilities.ts +65 -0
- package/dist/operations/cost.ts +58 -0
- package/dist/operations/dispatch.ts +348 -0
- package/dist/operations/error-results.ts +31 -0
- package/dist/operations/errors/catalog.ts +168 -0
- package/dist/operations/errors/classes.ts +51 -0
- package/dist/operations/errors/errors.ts +314 -0
- package/dist/operations/generators/capability.js +29 -19
- package/dist/operations/generators/capability.ts +704 -0
- package/dist/operations/generators/compile.js +20 -1
- package/dist/operations/generators/compile.ts +643 -0
- package/dist/operations/generators/grant.ts +336 -0
- package/dist/operations/generators/operation.ts +972 -0
- package/dist/operations/generators/permits.ts +223 -0
- package/dist/operations/generators/seed.ts +347 -0
- package/dist/operations/generators/trial.js +4 -1
- package/dist/operations/generators/trial.ts +529 -0
- package/dist/operations/http.ts +164 -0
- package/dist/operations/makers/capability.ts +166 -0
- package/dist/operations/makers/trial.ts +520 -0
- package/dist/operations/memory.ts +470 -0
- package/dist/operations/metering.ts +328 -0
- package/dist/operations/nodes.ts +54 -0
- package/dist/operations/operations.ts +32 -0
- package/dist/operations/queue-policy.ts +10 -0
- package/dist/operations/result.ts +28 -0
- package/dist/operations/seams.d.ts +4 -4
- package/dist/operations/seams.ts +364 -0
- package/dist/operations/store.ts +29 -0
- package/package.json +8 -1
- package/dist/integrations/generators/sdk.d.ts +0 -1
- package/dist/integrations/generators/sdk.js +0 -148
- /package/dist/integrations/{sdk.d.ts → module.d.ts} +0 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join, relative, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { quoted } from './rows.ts'
|
|
5
|
+
|
|
6
|
+
export const TREE = '_threads'
|
|
7
|
+
export const ROOT = 'thread.ts'
|
|
8
|
+
|
|
9
|
+
export class AlreadyThreadedError extends Error {
|
|
10
|
+
readonly at: string
|
|
11
|
+
constructor(at: string) {
|
|
12
|
+
super(
|
|
13
|
+
|
|
14
|
+
`${at} already exists, so this repository already has a root. A tree has one root and it is written once. ` +
|
|
15
|
+
'Declare something under it with `thread create` — run it bare to see what this tree makes — or take on ' +
|
|
16
|
+
'a package with `thread attach`, which brings its rows with it. Neither is written by hand.',
|
|
17
|
+
)
|
|
18
|
+
this.name = 'AlreadyThreadedError'
|
|
19
|
+
this.at = at
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const COMPILER = 'tsconfig.json'
|
|
24
|
+
|
|
25
|
+
export const LOCKFILES = ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb'] as const
|
|
26
|
+
|
|
27
|
+
export const MANIFEST = 'package.json'
|
|
28
|
+
|
|
29
|
+
const MODULE = 'module'
|
|
30
|
+
|
|
31
|
+
export const REGISTRY = '.npm' + 'rc'
|
|
32
|
+
|
|
33
|
+
export const IGNORED = '.git' + 'ignore'
|
|
34
|
+
|
|
35
|
+
export function ignored(): string {
|
|
36
|
+
return [
|
|
37
|
+
'node_' + 'modules/',
|
|
38
|
+
'.next/',
|
|
39
|
+
'dist/',
|
|
40
|
+
|
|
41
|
+
'*.tsbuildinfo',
|
|
42
|
+
|
|
43
|
+
'next-env.d.ts',
|
|
44
|
+
'.DS_Store',
|
|
45
|
+
'',
|
|
46
|
+
].join('\n')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function compiler(): string {
|
|
50
|
+
return `${JSON.stringify(
|
|
51
|
+
{
|
|
52
|
+
compilerOptions: {
|
|
53
|
+
target: 'ES2022',
|
|
54
|
+
module: 'ESNext',
|
|
55
|
+
moduleResolution: 'bundler',
|
|
56
|
+
strict: true,
|
|
57
|
+
|
|
58
|
+
allowImportingTsExtensions: true,
|
|
59
|
+
|
|
60
|
+
skipLibCheck: true,
|
|
61
|
+
|
|
62
|
+
types: ['node'],
|
|
63
|
+
jsx: 'react-jsx',
|
|
64
|
+
noEmit: true,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
null,
|
|
68
|
+
2,
|
|
69
|
+
)}\n`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function owned(into: string): readonly { readonly id: string; readonly at: string; readonly describes: string; readonly runs?: string }[] {
|
|
73
|
+
const held = LOCKFILES.filter((one) => existsSync(join(into, one)))
|
|
74
|
+
return [
|
|
75
|
+
{
|
|
76
|
+
id: 'manifest',
|
|
77
|
+
at: 'package.json',
|
|
78
|
+
describes:
|
|
79
|
+
'What this app is made of and what may be run against it — the packages it installs and the commands that build it, serve it and refuse it.',
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
...(existsSync(join(into, REGISTRY))
|
|
83
|
+
? [
|
|
84
|
+
{
|
|
85
|
+
id: 'registry',
|
|
86
|
+
at: REGISTRY,
|
|
87
|
+
describes:
|
|
88
|
+
'Which registry this app’s packages come from, and any credential scoped to it. Declared beside the lockfile because an install is not reproducible without it: the lockfile says which versions answered, and this says who answered.',
|
|
89
|
+
},
|
|
90
|
+
]
|
|
91
|
+
: []),
|
|
92
|
+
{
|
|
93
|
+
id: 'ignored',
|
|
94
|
+
at: IGNORED,
|
|
95
|
+
describes:
|
|
96
|
+
'What is built rather than written, so the checks that read this repository can tell the two apart. Every surface here asks which files are tracked, and without it an app that has been served once answers with its whole build cache.',
|
|
97
|
+
},
|
|
98
|
+
...held.map((one) => ({
|
|
99
|
+
id: 'lockfile',
|
|
100
|
+
at: one,
|
|
101
|
+
describes:
|
|
102
|
+
'The exact version of every package this app resolved to, so an install here and an install anywhere else are the same install.',
|
|
103
|
+
})),
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function root(into: string): string {
|
|
108
|
+
const rows = owned(into).flatMap((one) => [
|
|
109
|
+
' {',
|
|
110
|
+
` id: ${quoted(one.id)},`,
|
|
111
|
+
` at: ${quoted(one.at)},`,
|
|
112
|
+
` describes: ${quoted(one.describes)},`,
|
|
113
|
+
' version: 1,',
|
|
114
|
+
...(one.runs === undefined ? [] : [` runs: ${quoted(one.runs)},`]),
|
|
115
|
+
' },',
|
|
116
|
+
])
|
|
117
|
+
return [
|
|
118
|
+
"import type { Threaded } from './thread.ts'",
|
|
119
|
+
'',
|
|
120
|
+
'export const THREAD: Threaded = {',
|
|
121
|
+
" id: 'thread',",
|
|
122
|
+
` at: '${TREE}/${ROOT}',`,
|
|
123
|
+
" describes: 'The framework that declares what must exist, forbids what is not declared, and derives what can be derived.',",
|
|
124
|
+
' version: 1,',
|
|
125
|
+
' declares: [',
|
|
126
|
+
...rows,
|
|
127
|
+
' ],',
|
|
128
|
+
'}',
|
|
129
|
+
'',
|
|
130
|
+
].join('\n')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function init(into: string): readonly string[] {
|
|
134
|
+
const dir = resolve(into, TREE)
|
|
135
|
+
const at = join(dir, ROOT)
|
|
136
|
+
if (existsSync(at)) throw new AlreadyThreadedError(relative(into, at))
|
|
137
|
+
mkdirSync(dir, { recursive: true })
|
|
138
|
+
const written: string[] = []
|
|
139
|
+
|
|
140
|
+
const held = join(into, COMPILER)
|
|
141
|
+
if (!existsSync(held)) {
|
|
142
|
+
writeFileSync(held, compiler(), 'utf8')
|
|
143
|
+
written.push(COMPILER)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const manifest = join(into, MANIFEST)
|
|
147
|
+
if (!existsSync(manifest)) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`there is no ${MANIFEST} here, and a thread tree is source this package has to be able to load. ` +
|
|
150
|
+
'Run `npm init` first, then `thread init` — the tree is written beside a manifest, not instead of one.',
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
const stated = JSON.parse(readFileSync(manifest, 'utf8')) as { type?: string }
|
|
154
|
+
if (stated.type !== MODULE) {
|
|
155
|
+
writeFileSync(manifest, `${JSON.stringify({ ...stated, type: MODULE }, null, 2)}\n`, 'utf8')
|
|
156
|
+
written.push(MANIFEST)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const ignore = join(into, IGNORED)
|
|
160
|
+
if (!existsSync(ignore)) {
|
|
161
|
+
writeFileSync(ignore, ignored(), 'utf8')
|
|
162
|
+
written.push(IGNORED)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const [name, text] of [[ROOT, root(into)]] as const) {
|
|
166
|
+
writeFileSync(join(dir, name), text, 'utf8')
|
|
167
|
+
written.push(relative(into, join(dir, name)))
|
|
168
|
+
}
|
|
169
|
+
return written
|
|
170
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
import { argv, exit as ending } from 'node:process'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
export function invoked(url: string): boolean {
|
|
7
|
+
const named = argv[1]
|
|
8
|
+
if (named === undefined) return false
|
|
9
|
+
try {
|
|
10
|
+
const held = new URL(url)
|
|
11
|
+
held.search = ''
|
|
12
|
+
held.hash = ''
|
|
13
|
+
return realpathSync(named) === realpathSync(fileURLToPath(held))
|
|
14
|
+
} catch {
|
|
15
|
+
return false
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const SOURCE = import.meta.url.endsWith('.ts')
|
|
20
|
+
|
|
21
|
+
export const BESIDE = (name: string): string => `${name}${SOURCE ? '.ts' : '.js'}`
|
|
22
|
+
|
|
23
|
+
export const LOADER: readonly string[] = SOURCE ? ['--import', 'tsx'] : []
|
|
24
|
+
|
|
25
|
+
export const CLI: readonly string[] = [
|
|
26
|
+
...LOADER,
|
|
27
|
+
join(dirname(fileURLToPath(import.meta.url)), BESIDE('cli')),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
export const REFUSED = Symbol.for('thread.refused')
|
|
31
|
+
|
|
32
|
+
let sharing = false
|
|
33
|
+
|
|
34
|
+
export const shared = (on: boolean): void => {
|
|
35
|
+
sharing = on
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function exit(code?: number): never {
|
|
39
|
+
if (!sharing) ending(code)
|
|
40
|
+
throw Object.assign(new Error(`refused with ${String(code ?? 0)}`), { [REFUSED]: code ?? 0 })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const refusal = (thrown: unknown): number | undefined => {
|
|
44
|
+
if (typeof thrown !== 'object' || thrown === null) return undefined
|
|
45
|
+
const held = (thrown as Record<symbol, unknown>)[REFUSED]
|
|
46
|
+
return typeof held === 'number' ? held : undefined
|
|
47
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import { argv, exitCode, stdout } from 'node:process'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import { generators, RAN, REFUSED_BY } from './chain.ts'
|
|
6
|
+
import { forget } from './descend.ts'
|
|
7
|
+
import { refusal, shared } from './invoked.ts'
|
|
8
|
+
import { findTree } from './tree.ts'
|
|
9
|
+
|
|
10
|
+
let spun = 0
|
|
11
|
+
|
|
12
|
+
const words = (command: string): readonly string[] => command.trim().split(/\s+/).slice(1)
|
|
13
|
+
|
|
14
|
+
shared(true)
|
|
15
|
+
|
|
16
|
+
const tree = findTree()
|
|
17
|
+
const rows = await generators(tree)
|
|
18
|
+
|
|
19
|
+
for (const command of argv.slice(2).filter((one) => one !== '')) {
|
|
20
|
+
const said = words(command)
|
|
21
|
+
const name = said[0]
|
|
22
|
+
if (name === undefined) continue
|
|
23
|
+
|
|
24
|
+
const held = rows.find((one) => one.by === `thread ${name}`)
|
|
25
|
+
if (held?.at === undefined) {
|
|
26
|
+
stdout.write(`${REFUSED_BY} ${command}\n`)
|
|
27
|
+
stdout.write(`round: no command named "${name}", so \`${command}\` was not run\n`)
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const found = resolve(tree.at, held.at)
|
|
32
|
+
|
|
33
|
+
forget()
|
|
34
|
+
|
|
35
|
+
argv.length = 1
|
|
36
|
+
argv.push(found, ...said.slice(1))
|
|
37
|
+
|
|
38
|
+
spun += 1
|
|
39
|
+
|
|
40
|
+
process.exitCode = 0
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await import(`${pathToFileURL(found).href}?round=${String(spun)}`)
|
|
44
|
+
if (exitCode !== undefined && exitCode !== 0) stdout.write(`${REFUSED_BY} ${command}\n`)
|
|
45
|
+
else stdout.write(`${RAN} ${command}\n`)
|
|
46
|
+
} catch (thrown) {
|
|
47
|
+
const code = refusal(thrown)
|
|
48
|
+
if (code === undefined) {
|
|
49
|
+
|
|
50
|
+
stdout.write(`${REFUSED_BY} ${command}\n`)
|
|
51
|
+
stdout.write(`round: \`${command}\` threw: ${thrown instanceof Error ? thrown.message : String(thrown)}\n`)
|
|
52
|
+
} else if (code === 0) stdout.write(`${RAN} ${command}\n`)
|
|
53
|
+
else stdout.write(`${REFUSED_BY} ${command}\n`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
process.exitCode = 0
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
|
|
3
|
+
export interface Bounds {
|
|
4
|
+
readonly open: number
|
|
5
|
+
readonly close: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const PARSED = new Map<string, ts.SourceFile>()
|
|
9
|
+
|
|
10
|
+
function tree(text: string): ts.SourceFile {
|
|
11
|
+
const held = PARSED.get(text)
|
|
12
|
+
if (held !== undefined) return held
|
|
13
|
+
const made = ts.createSourceFile('rows.ts', text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
|
14
|
+
if (PARSED.size > 8) PARSED.clear()
|
|
15
|
+
PARSED.set(text, made)
|
|
16
|
+
return made
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function walk(node: ts.Node, seen: (one: ts.Node) => void): void {
|
|
20
|
+
seen(node)
|
|
21
|
+
ts.forEachChild(node, (one) => {
|
|
22
|
+
walk(one, seen)
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const keyed = (one: ts.PropertyAssignment): string | undefined =>
|
|
27
|
+
ts.isIdentifier(one.name) || ts.isStringLiteral(one.name) ? one.name.text : undefined
|
|
28
|
+
|
|
29
|
+
function stringy(node: ts.ObjectLiteralExpression, name: string): string | undefined {
|
|
30
|
+
for (const one of node.properties) {
|
|
31
|
+
if (!ts.isPropertyAssignment(one) || keyed(one) !== name) continue
|
|
32
|
+
return ts.isStringLiteral(one.initializer) ? one.initializer.text : undefined
|
|
33
|
+
}
|
|
34
|
+
return undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function listy(node: ts.ObjectLiteralExpression, name: string): ts.ArrayLiteralExpression | undefined {
|
|
38
|
+
for (const one of node.properties) {
|
|
39
|
+
if (!ts.isPropertyAssignment(one) || keyed(one) !== name) continue
|
|
40
|
+
return ts.isArrayLiteralExpression(one.initializer) ? one.initializer : undefined
|
|
41
|
+
}
|
|
42
|
+
return undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const spanned = (node: ts.Node): { readonly open: number; readonly close: number } => ({
|
|
46
|
+
open: node.getStart(),
|
|
47
|
+
close: node.getEnd() - 1,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
export function inCode(text: string): readonly boolean[] {
|
|
51
|
+
const held = new Array<boolean>(text.length).fill(true)
|
|
52
|
+
let cursor = 0
|
|
53
|
+
while (cursor < text.length) {
|
|
54
|
+
const one = text[cursor]
|
|
55
|
+
const next = text[cursor + 1]
|
|
56
|
+
if (one === '/' && next === '/') {
|
|
57
|
+
while (cursor < text.length && text[cursor] !== '\n') held[cursor++] = false
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
if (one === '/' && next === '*') {
|
|
61
|
+
held[cursor++] = false
|
|
62
|
+
held[cursor++] = false
|
|
63
|
+
while (cursor < text.length && !(text[cursor] === '*' && text[cursor + 1] === '/')) held[cursor++] = false
|
|
64
|
+
held[cursor++] = false
|
|
65
|
+
held[cursor++] = false
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
if (one === "'" || one === '"' || one === '`') {
|
|
69
|
+
held[cursor++] = false
|
|
70
|
+
while (cursor < text.length && text[cursor] !== one) {
|
|
71
|
+
if (text[cursor] === '\\') held[cursor++] = false
|
|
72
|
+
held[cursor++] = false
|
|
73
|
+
}
|
|
74
|
+
held[cursor++] = false
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
cursor += 1
|
|
78
|
+
}
|
|
79
|
+
return held
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function matched(text: string, code: readonly boolean[], from: number, open: string, close: string): number {
|
|
83
|
+
let depth = 0
|
|
84
|
+
for (let cursor = from; cursor < text.length; cursor += 1) {
|
|
85
|
+
if (code[cursor] !== true) continue
|
|
86
|
+
if (text[cursor] === open) depth += 1
|
|
87
|
+
if (text[cursor] === close) {
|
|
88
|
+
depth -= 1
|
|
89
|
+
if (depth === 0) return cursor
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return -1
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function listBounds(text: string, list: string): Bounds | undefined {
|
|
96
|
+
const file = tree(text)
|
|
97
|
+
|
|
98
|
+
let found: ts.ArrayLiteralExpression | undefined
|
|
99
|
+
let held: ts.ObjectLiteralExpression | undefined
|
|
100
|
+
|
|
101
|
+
walk(file, (node) => {
|
|
102
|
+
if (found !== undefined || !ts.isVariableDeclaration(node) || node.initializer === undefined) return
|
|
103
|
+
|
|
104
|
+
const unwrapped = (node2: ts.Expression): ts.Expression =>
|
|
105
|
+
ts.isAsExpression(node2) || ts.isSatisfiesExpression(node2) ? unwrapped(node2.expression) : node2
|
|
106
|
+
const one = unwrapped(node.initializer)
|
|
107
|
+
if (ts.isIdentifier(node.name) && node.name.text === list && ts.isArrayLiteralExpression(one)) {
|
|
108
|
+
found = one
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
if (held === undefined && ts.isObjectLiteralExpression(one)) held = one
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
if (found !== undefined) return spanned(found)
|
|
115
|
+
if (held === undefined) return undefined
|
|
116
|
+
const inside = listy(held, list)
|
|
117
|
+
return inside === undefined ? undefined : spanned(inside)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function elements(text: string, where: Bounds): readonly ts.ObjectLiteralExpression[] {
|
|
121
|
+
let found: ts.ArrayLiteralExpression | undefined
|
|
122
|
+
walk(tree(text), (node) => {
|
|
123
|
+
if (found !== undefined) return
|
|
124
|
+
if (ts.isArrayLiteralExpression(node) && node.getStart() === where.open && node.getEnd() - 1 === where.close) {
|
|
125
|
+
found = node
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
if (found === undefined) return []
|
|
129
|
+
return found.elements.filter((one): one is ts.ObjectLiteralExpression => ts.isObjectLiteralExpression(one))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function rowAt(text: string, where: Bounds, id: string): ts.ObjectLiteralExpression | undefined {
|
|
133
|
+
return elements(text, where).find((one) => stringy(one, 'id') === id)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function holdsRow(text: string, where: Bounds, id: string): boolean {
|
|
137
|
+
return rowAt(text, where, id) !== undefined
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function seededAt(text: string, where: Bounds, id: string): string | undefined {
|
|
141
|
+
const held = rowAt(text, where, id)
|
|
142
|
+
if (held === undefined) return undefined
|
|
143
|
+
const seeds = listy(held, 'seeds')
|
|
144
|
+
const derives = listy(held, 'derives')
|
|
145
|
+
if (seeds !== undefined && derives !== undefined) return undefined
|
|
146
|
+
const first = (seeds ?? derives)?.elements[0]
|
|
147
|
+
if (first === undefined || !ts.isObjectLiteralExpression(first)) return undefined
|
|
148
|
+
return stringy(first, 'at')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function rowsIn(
|
|
152
|
+
text: string,
|
|
153
|
+
where: Bounds,
|
|
154
|
+
): readonly { readonly id: string; readonly at: string | undefined; readonly source: string }[] {
|
|
155
|
+
const found: { id: string; at: string | undefined; source: string }[] = []
|
|
156
|
+
for (const one of elements(text, where)) {
|
|
157
|
+
const id = stringy(one, 'id')
|
|
158
|
+
if (id === undefined) continue
|
|
159
|
+
|
|
160
|
+
const at = stringy(one, 'at')
|
|
161
|
+
const from = one.getStart()
|
|
162
|
+
const line = text.lastIndexOf('\n', from) + 1
|
|
163
|
+
const pad = text.slice(line, from)
|
|
164
|
+
found.push({ id, at, source: `${/^[ \t]*$/.test(pad) ? pad : ''}${text.slice(from, one.getEnd())},` })
|
|
165
|
+
}
|
|
166
|
+
return found
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function withRow(text: string, where: Bounds, row: string): string {
|
|
170
|
+
const held = /[ \t]*$/.exec(text.slice(0, where.close))?.[0] ?? ''
|
|
171
|
+
const before = text.slice(0, where.close - held.length)
|
|
172
|
+
const empty = before.trimEnd().endsWith('[')
|
|
173
|
+
const line = text.lastIndexOf('\n', where.open)
|
|
174
|
+
const pad = empty ? (/^[ \t]*/.exec(text.slice(line + 1))?.[0] ?? '') : held
|
|
175
|
+
return `${before}${empty ? '\n' : ''}${row}\n${pad}${text.slice(where.close)}`
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function withSeed(text: string, where: Bounds, id: string, seed: string): string {
|
|
179
|
+
const held = rowAt(text, where, id)
|
|
180
|
+
if (held === undefined) return text
|
|
181
|
+
if (listy(held, 'seeds') !== undefined) return text
|
|
182
|
+
|
|
183
|
+
const to = held.getEnd() - 1
|
|
184
|
+
const line = text.lastIndexOf('\n', held.getStart())
|
|
185
|
+
const pad = /^[ \t]*/.exec(text.slice(line + 1))?.[0] ?? ' '
|
|
186
|
+
const before = text.slice(0, to).trimEnd()
|
|
187
|
+
const comma = before.endsWith(',') ? '' : ','
|
|
188
|
+
return `${before}${comma}\n${pad} seeds: [\n${seed}\n${pad} ],\n${pad}${text.slice(to)}`
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function withRequires(text: string, where: Bounds, id: string, requires: readonly string[]): string | undefined {
|
|
192
|
+
const held = rowAt(text, where, id)
|
|
193
|
+
if (held === undefined) return undefined
|
|
194
|
+
|
|
195
|
+
const line = text.lastIndexOf('\n', held.getStart()) + 1
|
|
196
|
+
const pad = `${/^[ \t]*/.exec(text.slice(line))?.[0] ?? ' '} `
|
|
197
|
+
const written = requires.length === 0 ? '' : `${pad}requires: [${requires.map((one) => `'${one}'`).join(', ')}],\n`
|
|
198
|
+
|
|
199
|
+
const propertyNamed = (name: string): ts.PropertyAssignment | undefined => {
|
|
200
|
+
for (const one of held.properties) {
|
|
201
|
+
if (ts.isPropertyAssignment(one) && keyed(one) === name) return one
|
|
202
|
+
}
|
|
203
|
+
return undefined
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const already = propertyNamed('requires')
|
|
207
|
+
if (already !== undefined) {
|
|
208
|
+
const opens = text.lastIndexOf('\n', already.getStart()) + 1
|
|
209
|
+
const shut = text.indexOf('\n', already.getEnd())
|
|
210
|
+
return `${text.slice(0, opens)}${written}${text.slice(shut === -1 ? text.length : shut + 1)}`
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const version = propertyNamed('version')
|
|
214
|
+
if (version === undefined) return undefined
|
|
215
|
+
const after = text.indexOf('\n', version.getEnd())
|
|
216
|
+
const at = after === -1 ? text.length : after + 1
|
|
217
|
+
return `${text.slice(0, at)}${written}${text.slice(at)}`
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function seedsIn(text: string, where: Bounds, id: string): Bounds | undefined {
|
|
221
|
+
const held = rowAt(text, where, id)
|
|
222
|
+
if (held === undefined) return undefined
|
|
223
|
+
const list = listy(held, 'seeds')
|
|
224
|
+
return list === undefined ? undefined : spanned(list)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function afterRow(text: string, where: Bounds, follows: string, row: string): string | undefined {
|
|
228
|
+
const held = rowAt(text, where, follows)
|
|
229
|
+
if (held === undefined) return undefined
|
|
230
|
+
let to = held.getEnd()
|
|
231
|
+
if (text[to] === ',') to += 1
|
|
232
|
+
return `${text.slice(0, to)}\n${row}${text.slice(to)}`
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function headed(text: string): ts.ObjectLiteralExpression | undefined {
|
|
236
|
+
let found: ts.ObjectLiteralExpression | undefined
|
|
237
|
+
walk(tree(text), (node) => {
|
|
238
|
+
if (found !== undefined || !ts.isVariableDeclaration(node) || node.initializer === undefined) return
|
|
239
|
+
const one = ts.isAsExpression(node.initializer) ? node.initializer.expression : node.initializer
|
|
240
|
+
if (ts.isObjectLiteralExpression(one)) found = one
|
|
241
|
+
})
|
|
242
|
+
return found
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function stated(node: ts.ObjectLiteralExpression, field: string): Bounds | undefined {
|
|
246
|
+
for (const one of node.properties) {
|
|
247
|
+
if (!ts.isPropertyAssignment(one) || keyed(one) !== field) continue
|
|
248
|
+
return { open: one.initializer.getStart(), close: one.initializer.getEnd() }
|
|
249
|
+
}
|
|
250
|
+
return undefined
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export const quoted = (one: string): string => `'${one.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n')}'`
|
|
254
|
+
|
|
255
|
+
export const STATED = ['at', 'describes', 'version']
|
|
256
|
+
|
|
257
|
+
export function reconciled(held: string, source: string, fields: readonly string[]): string {
|
|
258
|
+
const ships = headed(source)
|
|
259
|
+
if (ships === undefined) return held
|
|
260
|
+
const named = stringy(ships, 'id')
|
|
261
|
+
|
|
262
|
+
let next = held
|
|
263
|
+
for (const field of fields) {
|
|
264
|
+
const want = stated(ships, field)
|
|
265
|
+
if (want === undefined) continue
|
|
266
|
+
|
|
267
|
+
const mine = headed(next)
|
|
268
|
+
if (mine === undefined || stringy(mine, 'id') !== named) return held
|
|
269
|
+
const have = stated(mine, field)
|
|
270
|
+
if (have === undefined) continue
|
|
271
|
+
const said = source.slice(want.open, want.close)
|
|
272
|
+
if (next.slice(have.open, have.close) === said) continue
|
|
273
|
+
next = next.slice(0, have.open) + said + next.slice(have.close)
|
|
274
|
+
}
|
|
275
|
+
return next
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function fieldIn(text: string, where: Bounds, id: string, field: string): string | undefined {
|
|
279
|
+
const held = rowAt(text, where, id)
|
|
280
|
+
if (held === undefined) return undefined
|
|
281
|
+
for (const one of held.properties) {
|
|
282
|
+
if (!ts.isPropertyAssignment(one) || keyed(one) !== field) continue
|
|
283
|
+
return text.slice(one.initializer.getStart(), one.initializer.getEnd())
|
|
284
|
+
}
|
|
285
|
+
return undefined
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function withField(
|
|
289
|
+
text: string,
|
|
290
|
+
where: Bounds,
|
|
291
|
+
id: string,
|
|
292
|
+
field: string,
|
|
293
|
+
value: string,
|
|
294
|
+
): string | undefined {
|
|
295
|
+
const held = rowAt(text, where, id)
|
|
296
|
+
if (held === undefined) return undefined
|
|
297
|
+
for (const one of held.properties) {
|
|
298
|
+
if (!ts.isPropertyAssignment(one) || keyed(one) !== field) continue
|
|
299
|
+
return `${text.slice(0, one.initializer.getStart())}${value}${text.slice(one.initializer.getEnd())}`
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const last = held.properties.at(-1)
|
|
303
|
+
if (last === undefined) return undefined
|
|
304
|
+
|
|
305
|
+
let ends = last.getEnd()
|
|
306
|
+
if (text[ends] === ',') ends += 1
|
|
307
|
+
const already = text[ends - 1] === ',' ? '' : ','
|
|
308
|
+
|
|
309
|
+
if (!text.slice(held.getStart(), held.getEnd()).includes('\n')) {
|
|
310
|
+
return `${text.slice(0, ends)}${already} ${field}: ${value}${text.slice(ends)}`
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const line = text.lastIndexOf('\n', last.getStart()) + 1
|
|
314
|
+
const pad = /^[ \t]*/.exec(text.slice(line))?.[0] ?? ' '
|
|
315
|
+
return `${text.slice(0, ends)}${already}\n${pad}${field}: ${value},${text.slice(ends)}`
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function withoutRow(text: string, where: Bounds, id: string): string {
|
|
319
|
+
const held = rowAt(text, where, id)
|
|
320
|
+
if (held === undefined) return text
|
|
321
|
+
let ends = held.getEnd()
|
|
322
|
+
if (text[ends] === ',') ends += 1
|
|
323
|
+
const pad = /[ \t]*$/.exec(text.slice(0, held.getStart()))?.[0] ?? ''
|
|
324
|
+
const from = held.getStart() - pad.length
|
|
325
|
+
if (text[ends] === '\n') ends += 1
|
|
326
|
+
const left = text.slice(0, from)
|
|
327
|
+
const right = text.slice(ends)
|
|
328
|
+
return left.trimEnd().endsWith('[') && right.trimStart().startsWith(']')
|
|
329
|
+
? `${left.replace(/\s*$/, '')}${right.replace(/^\s*/, '')}`
|
|
330
|
+
: left + right
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const lines = (held: unknown, pad: string): readonly string[] => {
|
|
334
|
+
if (held === null || held === undefined) return [String(held)]
|
|
335
|
+
if (Array.isArray(held)) {
|
|
336
|
+
if (held.length === 0) return ['[]']
|
|
337
|
+
if (held.every((one) => typeof one !== 'object' || one === null)) return [held.map((one) => String(one)).join(', ')]
|
|
338
|
+
return held.flatMap((one) => {
|
|
339
|
+
const [first = '', ...rest] = lines(one, `${pad} `)
|
|
340
|
+
return [`${pad}- ${first.trimStart()}`, ...rest]
|
|
341
|
+
})
|
|
342
|
+
}
|
|
343
|
+
if (typeof held === 'object') {
|
|
344
|
+
const parts = Object.entries(held as Record<string, unknown>)
|
|
345
|
+
if (parts.length === 0) return ['{}']
|
|
346
|
+
return parts.flatMap(([key, one]) => {
|
|
347
|
+
const [first = '', ...rest] = lines(one, `${pad} `)
|
|
348
|
+
return rest.length === 0 && !first.includes('\n') ? [`${pad}${key}: ${first}`] : [`${pad}${key}:`, first, ...rest]
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
return [String(held)]
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function showing(row: object, only: readonly string[] = []): string {
|
|
355
|
+
const held = row as Record<string, unknown>
|
|
356
|
+
const keys = Object.keys(held)
|
|
357
|
+
|
|
358
|
+
const wanted = only.filter((one) => one !== '')
|
|
359
|
+
const stray = wanted.filter((one) => !keys.includes(one))
|
|
360
|
+
if (stray.length > 0) {
|
|
361
|
+
return `${stray.map((one) => `\`${one}\``).join(', ')} ${stray.length === 1 ? 'is not a field' : 'are not fields'} of this row. It states ${keys.join(', ')}\n`
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const showing = wanted.length === 0 ? keys : wanted
|
|
365
|
+
return `${showing.flatMap((key) => {
|
|
366
|
+
const [first = '', ...rest] = lines(held[key], ' ')
|
|
367
|
+
return rest.length === 0 ? [`${key}: ${first.trimStart()}`] : [`${key}:`, first, ...rest]
|
|
368
|
+
}).join('\n')}\n`
|
|
369
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const ORGANISATION = '@prestack'
|
|
2
|
+
|
|
3
|
+
export const THING = 'thread'
|
|
4
|
+
|
|
5
|
+
export const NAMED = `${ORGANISATION}/${THING}`
|
|
6
|
+
|
|
7
|
+
const CORE = 'core'
|
|
8
|
+
|
|
9
|
+
const HEAD = `${NAMED}-`
|
|
10
|
+
|
|
11
|
+
export const named = (id: string): string => (id === CORE ? NAMED : `${HEAD}${id}`)
|
|
12
|
+
|
|
13
|
+
export const idOf = (name: string): string | undefined => {
|
|
14
|
+
if (name === NAMED) return CORE
|
|
15
|
+
return name.startsWith(HEAD) ? name.slice(HEAD.length) : undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const ours = (name: string): boolean => idOf(name) !== undefined
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface Quote {
|
|
2
|
+
readonly on: string
|
|
3
|
+
readonly said: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface Threaded {
|
|
7
|
+
readonly id: string
|
|
8
|
+
readonly at: string
|
|
9
|
+
readonly describes: string
|
|
10
|
+
readonly version?: number
|
|
11
|
+
readonly cites?: readonly Quote[]
|
|
12
|
+
readonly requires?: readonly string[]
|
|
13
|
+
readonly takes?: readonly string[]
|
|
14
|
+
readonly by?: string
|
|
15
|
+
readonly runs?: string
|
|
16
|
+
readonly declares?: readonly Threaded[]
|
|
17
|
+
readonly derives?: readonly Threaded[]
|
|
18
|
+
readonly seeds?: readonly Threaded[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Member extends Threaded {
|
|
22
|
+
readonly version: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const PREFIXES = {
|
|
26
|
+
workflow: 'wf_',
|
|
27
|
+
view: 'vw_',
|
|
28
|
+
agent: 'agt_',
|
|
29
|
+
} as const satisfies Readonly<Record<string, string>>
|