@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
|
@@ -1,1054 +0,0 @@
|
|
|
1
|
-
import ts from 'typescript'
|
|
2
|
-
|
|
3
|
-
import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from '../../integrations/disk.ts'
|
|
4
|
-
import { dirname, resolve } from 'node:path'
|
|
5
|
-
import { argv, stdout } from 'node:process'
|
|
6
|
-
import { pathToFileURL } from 'node:url'
|
|
7
|
-
|
|
8
|
-
import { ROOT, TREE, descend, forget } from '../../core/descend.ts'
|
|
9
|
-
import { exit, invoked } from '../../core/invoked.ts'
|
|
10
|
-
import { named as packageOf } from '../../core/scope.ts'
|
|
11
|
-
import { PREFIXES } from '../../core/thread.ts'
|
|
12
|
-
|
|
13
|
-
import { secrecyOf, type Secret } from '../../integrations/secret.ts'
|
|
14
|
-
import { reachOf, servesBrowser, targetOf, type Call, type Target, type Webhook } from '../../integrations/target.ts'
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
CAPABILITIES_AT,
|
|
18
|
-
CLIENT_AT,
|
|
19
|
-
MCP_AT,
|
|
20
|
-
OPENAPI_AT,
|
|
21
|
-
REACHING_AT,
|
|
22
|
-
ACTOR_AT,
|
|
23
|
-
DEFAULTS_AT,
|
|
24
|
-
DISPATCHING_AT,
|
|
25
|
-
CHECKED_AT,
|
|
26
|
-
inboundAt,
|
|
27
|
-
isSurface,
|
|
28
|
-
ROUTE,
|
|
29
|
-
routeAt,
|
|
30
|
-
SURFACES,
|
|
31
|
-
switched,
|
|
32
|
-
UNSTATED,
|
|
33
|
-
type Capability,
|
|
34
|
-
type Surface,
|
|
35
|
-
} from '../capabilities.ts'
|
|
36
|
-
import { ANONYMOUS, ENVIRONMENT } from '../../core/actor.ts'
|
|
37
|
-
import { DISPATCH_AT } from '../dispatch.ts'
|
|
38
|
-
import { STREAMED, TOLD } from '../../integrations/https.ts'
|
|
39
|
-
import { holdsRow, listBounds, marked, nestedBounds, rowsIn, unread, valuesIn, withField, withRow, withTopField, withoutRow } from '../../core/rows.ts'
|
|
40
|
-
import { said } from '../attach.ts'
|
|
41
|
-
import { MEMBERS_LIST } from '../operations.ts'
|
|
42
|
-
import { beside, operations, quoted, runtime, types } from './seed.ts'
|
|
43
|
-
|
|
44
|
-
interface Written {
|
|
45
|
-
readonly at: string
|
|
46
|
-
readonly text: string
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const compare = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0)
|
|
50
|
-
|
|
51
|
-
const listedIn = (held: ts.Expression | undefined): readonly string[] =>
|
|
52
|
-
held !== undefined && ts.isArrayLiteralExpression(held)
|
|
53
|
-
? held.elements.filter((one) => ts.isStringLiteral(one)).map((one) => (one as ts.StringLiteral).text)
|
|
54
|
-
: []
|
|
55
|
-
|
|
56
|
-
const trueIn = (held: ts.Expression | undefined): boolean => held !== undefined && held.kind === ts.SyntaxKind.TrueKeyword
|
|
57
|
-
|
|
58
|
-
function stable(value: unknown): unknown {
|
|
59
|
-
if (Array.isArray(value)) return value.map(stable)
|
|
60
|
-
if (typeof value === 'object' && value !== null) {
|
|
61
|
-
const found: Record<string, unknown> = {}
|
|
62
|
-
for (const key of Object.keys(value as Record<string, unknown>).sort(compare)) {
|
|
63
|
-
found[key] = stable((value as Record<string, unknown>)[key])
|
|
64
|
-
}
|
|
65
|
-
return found
|
|
66
|
-
}
|
|
67
|
-
return value === undefined ? null : value
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const printed = (value: unknown): string => `${JSON.stringify(stable(value), null, 2)}\n`
|
|
71
|
-
|
|
72
|
-
function verbOf(id: string): 'GET' | 'POST' | undefined {
|
|
73
|
-
if (id.startsWith(PREFIXES.view)) return 'GET'
|
|
74
|
-
if (id.startsWith(PREFIXES.workflow) || id.startsWith(PREFIXES.agent)) return 'POST'
|
|
75
|
-
return undefined
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function schemaOf(at: string): Promise<unknown | string> {
|
|
79
|
-
const from = resolve(ROOT, at)
|
|
80
|
-
if (!existsSync(from)) return `${at} is not there, so nothing states what this operation takes`
|
|
81
|
-
|
|
82
|
-
const held = (await import(pathToFileURL(from).href)) as { readonly input?: unknown }
|
|
83
|
-
const input = held.input
|
|
84
|
-
if (input === undefined) return `${at} exports no \`input\`, so nothing states what this operation takes`
|
|
85
|
-
|
|
86
|
-
const zod = (await import('zod')) as { readonly toJSONSchema?: (one: unknown) => unknown }
|
|
87
|
-
if (zod.toJSONSchema === undefined) {
|
|
88
|
-
return 'this app\'s zod states no `toJSONSchema`, so an input schema cannot be written from it'
|
|
89
|
-
}
|
|
90
|
-
try {
|
|
91
|
-
return zod.toJSONSchema(input)
|
|
92
|
-
} catch (thrown) {
|
|
93
|
-
return `${at} states an input that cannot be written as a schema: ${thrown instanceof Error ? thrown.message : String(thrown)}`
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function sheet<T>(file: string, list: string): readonly T[] {
|
|
98
|
-
const at = resolve(ROOT, `_threads/${file}`)
|
|
99
|
-
if (!existsSync(at)) return []
|
|
100
|
-
const text = readFileSync(at, 'utf8')
|
|
101
|
-
if (unread(text).includes(list)) {
|
|
102
|
-
throw new Error(
|
|
103
|
-
`_threads/${file} works out \`${list}\` when the file runs instead of writing it down, and this command reads sheets ` +
|
|
104
|
-
'without running them. Write each row of it out in full',
|
|
105
|
-
)
|
|
106
|
-
}
|
|
107
|
-
return (valuesIn(text) as Record<string, readonly T[] | undefined>)[list] ?? []
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function browsing(requires: readonly string[], targets: readonly Target[], secrets: readonly Secret[]): boolean {
|
|
111
|
-
for (const word of requires) {
|
|
112
|
-
const target = targets.find((one) => one.id === word)
|
|
113
|
-
if (target === undefined) continue
|
|
114
|
-
if (servesBrowser(target, secrets) !== true) return false
|
|
115
|
-
}
|
|
116
|
-
return true
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export interface Surfaced {
|
|
120
|
-
readonly live: readonly Surface[]
|
|
121
|
-
readonly stated: boolean
|
|
122
|
-
readonly wrong: readonly string[]
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function defaulted(): Surfaced | undefined {
|
|
126
|
-
const at = resolve(ROOT, DEFAULTS_AT)
|
|
127
|
-
if (!existsSync(at)) return undefined
|
|
128
|
-
const row = Object.values(valuesIn(readFileSync(at, 'utf8'))).find(
|
|
129
|
-
(one): one is { readonly live?: unknown } =>
|
|
130
|
-
typeof one === 'object' && one !== null && (one as { readonly at?: unknown }).at === DEFAULTS_AT,
|
|
131
|
-
)
|
|
132
|
-
if (row === undefined || !Array.isArray(row.live)) return { live: UNSTATED, stated: false, wrong: [] }
|
|
133
|
-
const given = row.live.map(String)
|
|
134
|
-
return { live: SURFACES.filter((each) => given.includes(each)), stated: true, wrong: given.filter((each) => !isSurface(each)) }
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export async function derive({ schemas = true }: { readonly schemas?: boolean } = {}): Promise<{
|
|
138
|
-
readonly found: readonly Capability[]
|
|
139
|
-
readonly refusals: readonly string[]
|
|
140
|
-
readonly live: readonly Surface[]
|
|
141
|
-
}> {
|
|
142
|
-
const pass = descend()
|
|
143
|
-
const known = types(pass)
|
|
144
|
-
const refusals: string[] = []
|
|
145
|
-
const found: Capability[] = []
|
|
146
|
-
|
|
147
|
-
const targets = sheet<Target>('targets.ts', 'TARGETS')
|
|
148
|
-
const secrets = sheet<Secret>('secrets.ts', 'SECRETS')
|
|
149
|
-
|
|
150
|
-
for (const one of targets) {
|
|
151
|
-
if (reachOf(one.id) !== 'sdk' || (one.declares ?? []).length > 0) continue
|
|
152
|
-
refusals.push(
|
|
153
|
-
`\`${one.id}\` calls a package inside this app, and its row declares no file that does the calling. That file is the one ` +
|
|
154
|
-
'place the package is imported, and it exports `calls`, holding the function each call names as its path. Give the row ' +
|
|
155
|
-
`one, and the file with it, with \`thread edit ${one.id} --declares <id> --at <path> --describes <sentence> --body <text>\``,
|
|
156
|
-
)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const tree = defaulted()
|
|
160
|
-
if (tree === undefined) {
|
|
161
|
-
refusals.push(
|
|
162
|
-
`${DEFAULTS_AT} is not here, so nothing says which surfaces an operation answers on, and working it out anyway would ` +
|
|
163
|
-
`take away every route this app has. \`thread attach ${packageOf('operations')}\` writes it`,
|
|
164
|
-
)
|
|
165
|
-
}
|
|
166
|
-
for (const one of tree?.wrong ?? []) {
|
|
167
|
-
refusals.push(
|
|
168
|
-
`${DEFAULTS_AT} says \`${one}\` is live, and there is no such surface. There are ${String(SURFACES.length)}: ` +
|
|
169
|
-
`${SURFACES.join(', ')}. \`thread create capability edit defaults\` rewrites it`,
|
|
170
|
-
)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
for (const [id, one] of [...operations(pass, known)].sort((a, b) => compare(a[0], b[0]))) {
|
|
174
|
-
const stated = pass.stated.get(id)
|
|
175
|
-
const declared = {
|
|
176
|
-
webhooks: listedIn(stated?.fields.get('webhooks')),
|
|
177
|
-
signed: trueIn(stated?.fields.get('signed')),
|
|
178
|
-
}
|
|
179
|
-
const describes = stated?.describes
|
|
180
|
-
if (describes === undefined) {
|
|
181
|
-
refusals.push(`\`${id}\` states no \`describes\`, and a caller told nothing about an operation cannot choose it`)
|
|
182
|
-
continue
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const method = verbOf(id)
|
|
186
|
-
if (method === undefined) {
|
|
187
|
-
refusals.push(
|
|
188
|
-
`\`${id}\` wears none of the prefixes an operation may wear (${Object.values(PREFIXES).join(', ')}), so there is no verb to give it`,
|
|
189
|
-
)
|
|
190
|
-
continue
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const takes = one.takes[0]?.at
|
|
194
|
-
if (takes === undefined) {
|
|
195
|
-
refusals.push(`\`${id}\` names nothing it takes, so nothing states its input`)
|
|
196
|
-
continue
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const schema = schemas ? await schemaOf(takes) : undefined
|
|
200
|
-
if (typeof schema === 'string') {
|
|
201
|
-
refusals.push(`\`${id}\`: ${schema}`)
|
|
202
|
-
continue
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const off = listedIn(stated?.fields.get('off'))
|
|
206
|
-
const stray = off.filter((each) => !isSurface(each))
|
|
207
|
-
if (stray.length > 0) {
|
|
208
|
-
refusals.push(
|
|
209
|
-
`\`${id}\` turns off ${stray.map((each) => `\`${each}\``).join(', ')}, and there is no such surface. There are ` +
|
|
210
|
-
`${String(SURFACES.length)}: ${SURFACES.join(', ')}. \`thread create capability edit ${id}\` rewrites it`,
|
|
211
|
-
)
|
|
212
|
-
continue
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const live = (tree?.live ?? UNSTATED).filter((each) => !off.includes(each))
|
|
216
|
-
|
|
217
|
-
found.push({
|
|
218
|
-
operation: id,
|
|
219
|
-
describes,
|
|
220
|
-
requires: [...one.requires].sort(compare),
|
|
221
|
-
nodes: one.nodes.map((each) => each.id ?? '').filter((each) => each !== ''),
|
|
222
|
-
input: schema,
|
|
223
|
-
live: [...live].sort(compare) as readonly Surface[],
|
|
224
|
-
webhooks: [...declared.webhooks].sort(compare),
|
|
225
|
-
signed: declared.signed,
|
|
226
|
-
browsable: browsing(one.requires, targets, secrets),
|
|
227
|
-
mcp: { name: id, description: describes, inputSchema: schema },
|
|
228
|
-
cli: { invocation: `${id} --input '<json>'`, describes },
|
|
229
|
-
api: { method, path: `/operations/${id}` },
|
|
230
|
-
})
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
for (const one of sheet<Webhook>('targets.ts', 'WEBHOOKS')) {
|
|
234
|
-
const runs = found.find((each) => each.operation === one.invokes)
|
|
235
|
-
if (runs === undefined) {
|
|
236
|
-
refusals.push(
|
|
237
|
-
`\`${one.id}\` invokes \`${one.invokes}\`, and this app declares no operation by that name. A webhook is a way in ` +
|
|
238
|
-
`to something already declared, so declare that operation or point this webhook at one that exists`,
|
|
239
|
-
)
|
|
240
|
-
continue
|
|
241
|
-
}
|
|
242
|
-
const route = (one.derives ?? []).some((each) => each.at === inboundAt(one.id) && each.by !== undefined && each.runs !== undefined)
|
|
243
|
-
if (!route) {
|
|
244
|
-
refusals.push(
|
|
245
|
-
`\`${one.id}\` derives no route. A webhook is an address anybody may post to, so the file that answers it has to be a row ` +
|
|
246
|
-
`this tree declares: state a \`derives\` on this webhook naming ${inboundAt(one.id)}, with \`by\` and \`runs\` set to ` +
|
|
247
|
-
'`thread capabilities` and `thread capabilities --check`, so what is written there is checked against what writes it',
|
|
248
|
-
)
|
|
249
|
-
}
|
|
250
|
-
if (!runs.webhooks.includes(one.id)) {
|
|
251
|
-
refusals.push(
|
|
252
|
-
`\`${one.id}\` invokes \`${one.invokes}\`, and that operation does not name it back. An operation is run from outside ` +
|
|
253
|
-
`only by a webhook it named, so add ${one.id} to \`webhooks\` on that operation's row in ${TREE}operations.ts, or point this webhook elsewhere`,
|
|
254
|
-
)
|
|
255
|
-
}
|
|
256
|
-
if (runs.signed && (one.arrives === undefined || (one.requires ?? []).length === 0)) {
|
|
257
|
-
refusals.push(
|
|
258
|
-
`\`${one.invokes}\` says it will only be run by a signed webhook, and \`${one.id}\` carries no signature to check. ` +
|
|
259
|
-
`Give this webhook the header its signature arrives on and the secret it is signed with`,
|
|
260
|
-
)
|
|
261
|
-
}
|
|
262
|
-
if (!runs.live.includes('webhook')) {
|
|
263
|
-
refusals.push(
|
|
264
|
-
`\`${one.id}\` invokes \`${one.invokes}\`, and that operation does not answer on the webhook surface. Both halves have to ` +
|
|
265
|
-
`agree before anything outside can run it: webhook has to be live in ${DEFAULTS_AT}, which \`thread create capability edit defaults\` changes, and not in the \`off\` on its row, which \`thread create capability edit ${one.invokes}\` changes`,
|
|
266
|
-
)
|
|
267
|
-
}
|
|
268
|
-
const holds = (one.requires ?? []).filter((each) => secrecyOf(each) !== undefined)
|
|
269
|
-
if (one.arrives !== undefined && holds.length === 0) {
|
|
270
|
-
refusals.push(
|
|
271
|
-
`\`${one.id}\` says a signature arrives on \`${one.arrives}\` and requires no secret to check it against, so every ` +
|
|
272
|
-
`request would be taken on trust. Name the secret it is signed with, or take \`arrives\` off the row`,
|
|
273
|
-
)
|
|
274
|
-
}
|
|
275
|
-
if (one.arrives === undefined && holds.length > 0) {
|
|
276
|
-
refusals.push(
|
|
277
|
-
`\`${one.id}\` requires ${holds.join(', ')} and says no header a signature arrives on, so nothing would ever be checked ` +
|
|
278
|
-
`against it. Say which header carries the signature, or take the secret off the row`,
|
|
279
|
-
)
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return { found, refusals, live: tree?.live ?? UNSTATED }
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function named(): { readonly title: string; readonly version: string } {
|
|
287
|
-
try {
|
|
288
|
-
const held = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf8')) as {
|
|
289
|
-
readonly name?: unknown
|
|
290
|
-
readonly version?: unknown
|
|
291
|
-
}
|
|
292
|
-
return {
|
|
293
|
-
title: typeof held.name === 'string' ? held.name : 'this app',
|
|
294
|
-
version: typeof held.version === 'string' ? held.version : '0.0.0',
|
|
295
|
-
}
|
|
296
|
-
} catch {
|
|
297
|
-
return { title: 'this app', version: '0.0.0' }
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function openapi(found: readonly Capability[]): unknown {
|
|
302
|
-
const paths: Record<string, unknown> = {}
|
|
303
|
-
for (const one of found) {
|
|
304
|
-
if (!one.live.includes('api')) continue
|
|
305
|
-
const method = one.api.method.toLowerCase()
|
|
306
|
-
const responses = {
|
|
307
|
-
'200': {
|
|
308
|
-
description: 'the operation ran',
|
|
309
|
-
content: { 'application/json': { schema: { type: 'object', properties: { ok: { const: true }, value: {} }, required: ['ok'] } } },
|
|
310
|
-
},
|
|
311
|
-
'400': {
|
|
312
|
-
description: 'dispatch refused it — the code says which gate',
|
|
313
|
-
content: {
|
|
314
|
-
'application/json': {
|
|
315
|
-
schema: {
|
|
316
|
-
type: 'object',
|
|
317
|
-
properties: {
|
|
318
|
-
ok: { const: false },
|
|
319
|
-
code: { enum: ['denied', 'invalid', 'rate_limited', 'feature_denied', 'quota_exceeded', 'error'] },
|
|
320
|
-
error: { type: 'string' },
|
|
321
|
-
because: { type: 'string' },
|
|
322
|
-
},
|
|
323
|
-
required: ['ok', 'code', 'error'],
|
|
324
|
-
},
|
|
325
|
-
},
|
|
326
|
-
},
|
|
327
|
-
},
|
|
328
|
-
}
|
|
329
|
-
paths[one.api.path] = {
|
|
330
|
-
[method]: {
|
|
331
|
-
operationId: one.operation,
|
|
332
|
-
summary: one.describes,
|
|
333
|
-
...(one.api.method === 'GET'
|
|
334
|
-
? { parameters: [{ name: 'input', in: 'query', required: true, content: { 'application/json': { schema: one.input } } }] }
|
|
335
|
-
: { requestBody: { required: true, content: { 'application/json': { schema: one.input } } } }),
|
|
336
|
-
responses,
|
|
337
|
-
},
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
const who = named()
|
|
341
|
-
return {
|
|
342
|
-
openapi: '3.1.0',
|
|
343
|
-
info: { title: who.title, version: who.version, description: 'Every operation this app declares that is live on its API surface.' },
|
|
344
|
-
paths,
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const mcp = (found: readonly Capability[]): unknown => ({
|
|
349
|
-
tools: found.filter((one) => one.live.includes('mcp')).map((one) => one.mcp),
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
const compiled = (operation: string): boolean => existsSync(resolve(ROOT, `operations/${operation}.ts`))
|
|
353
|
-
|
|
354
|
-
function client(found: readonly Capability[]): string {
|
|
355
|
-
const rows = found
|
|
356
|
-
.filter(held)
|
|
357
|
-
.map((one) =>
|
|
358
|
-
[
|
|
359
|
-
' {',
|
|
360
|
-
` operation: ${JSON.stringify(one.operation)},`,
|
|
361
|
-
` describes: ${JSON.stringify(one.describes)},`,
|
|
362
|
-
` method: ${JSON.stringify(one.api.method)},`,
|
|
363
|
-
` path: ${JSON.stringify(one.api.path)},`,
|
|
364
|
-
' },',
|
|
365
|
-
].join('\n'),
|
|
366
|
-
)
|
|
367
|
-
|
|
368
|
-
const near = found.filter((one) => one.browsable && compiled(one.operation))
|
|
369
|
-
|
|
370
|
-
const bound = near.flatMap((one) => [
|
|
371
|
-
'export async function ' + one.operation + 'Locally(input: unknown, actor: Actor): Promise<Result<unknown>> {',
|
|
372
|
-
` const held = await dispatch(${one.operation}, input, actor, DECLARED, opening, counting)`,
|
|
373
|
-
' return held',
|
|
374
|
-
'}',
|
|
375
|
-
'',
|
|
376
|
-
])
|
|
377
|
-
|
|
378
|
-
return [
|
|
379
|
-
...(near.length === 0
|
|
380
|
-
? []
|
|
381
|
-
: [
|
|
382
|
-
`import type { Actor } from '${runtime(CLIENT_AT, '../../core/actor.ts')}'`,
|
|
383
|
-
`import type { Result } from '${runtime(CLIENT_AT, '../result.ts')}'`,
|
|
384
|
-
'',
|
|
385
|
-
"import { dispatch } from './_dispatch.ts'",
|
|
386
|
-
`import { counting, DECLARED, opening } from '${beside(CLIENT_AT, REACHING_AT)}'`,
|
|
387
|
-
...near.map((one) => `import { operation as ${one.operation} } from './${one.operation}.ts'`),
|
|
388
|
-
'',
|
|
389
|
-
]),
|
|
390
|
-
'export interface Reachable {',
|
|
391
|
-
' readonly operation: string',
|
|
392
|
-
' readonly describes: string',
|
|
393
|
-
" readonly method: 'GET' | 'POST'",
|
|
394
|
-
' readonly path: string',
|
|
395
|
-
'}',
|
|
396
|
-
'',
|
|
397
|
-
`export const CLIENT: readonly Reachable[] = [${rows.length === 0 ? '' : `\n${rows.join('\n')}\n`}]`,
|
|
398
|
-
'',
|
|
399
|
-
'export const reachable = (operation: string): Reachable | undefined =>',
|
|
400
|
-
' CLIENT.find((one) => one.operation === operation)',
|
|
401
|
-
'',
|
|
402
|
-
...bound,
|
|
403
|
-
'',
|
|
404
|
-
].join('\n')
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function route(one: Capability): string {
|
|
408
|
-
const at = routeAt(one.operation)
|
|
409
|
-
return [
|
|
410
|
-
`import { existsSync } from ${quoted(runtime(at, '../../integrations/disk.ts'))}`,
|
|
411
|
-
"import { join } from 'node:path'",
|
|
412
|
-
'',
|
|
413
|
-
'import { counting, DECLARED, opening } from ' + quoted(beside(at, REACHING_AT)),
|
|
414
|
-
'import { actor } from ' + quoted(beside(at, ACTOR_AT)),
|
|
415
|
-
'',
|
|
416
|
-
"import { dispatch } from '../../../operations/_dispatch.ts'",
|
|
417
|
-
`import { operation } from '../../../operations/${one.operation}.ts'`,
|
|
418
|
-
'',
|
|
419
|
-
'const STATUS: Record<string, number> = {',
|
|
420
|
-
' denied: 403,',
|
|
421
|
-
' invalid: 400,',
|
|
422
|
-
' rate_limited: 429,',
|
|
423
|
-
' feature_denied: 403,',
|
|
424
|
-
' quota_exceeded: 429,',
|
|
425
|
-
' error: 500,',
|
|
426
|
-
'}',
|
|
427
|
-
'',
|
|
428
|
-
"const PASSED = existsSync(join(process.cwd(), '.thread', 'gate.json'))",
|
|
429
|
-
'',
|
|
430
|
-
`export async function ${one.api.method}(request: Request): Promise<Response> {`,
|
|
431
|
-
' if (!PASSED) {',
|
|
432
|
-
' return Response.json(',
|
|
433
|
-
" { ok: false, code: 'error', error: 'failed', because: 'this tree has no passing gate, so nothing in it may answer a request. Run `thread gate` — a gate that has never run is not a gate that passed' },",
|
|
434
|
-
' { status: 500 },',
|
|
435
|
-
' )',
|
|
436
|
-
' }',
|
|
437
|
-
...(one.api.method === 'GET'
|
|
438
|
-
? [' const input = Object.fromEntries(new URL(request.url).searchParams)']
|
|
439
|
-
: [
|
|
440
|
-
' const input = await request.json().catch(() => undefined)',
|
|
441
|
-
" if ((request.headers.get('accept') ?? '').includes(" + quoted(STREAMED) + ')) {',
|
|
442
|
-
' const lines = new TransformStream<string, string>()',
|
|
443
|
-
' const writing = lines.writable.getWriter()',
|
|
444
|
-
" const sent = (said: unknown): void => { writing.write(JSON.stringify(said) + '\\n').catch(() => undefined) }",
|
|
445
|
-
' void dispatch(operation, input, await actor(request), DECLARED, opening, counting, (said) => sent({ ' + TOLD + ': said })).then((held) => {',
|
|
446
|
-
' sent(held)',
|
|
447
|
-
' return writing.close()',
|
|
448
|
-
' }).catch(() => undefined)',
|
|
449
|
-
' return new Response(lines.readable.pipeThrough(new TextEncoderStream()), { headers: { ' + quoted('content-type') + ': ' + quoted(STREAMED) + ' } })',
|
|
450
|
-
' }',
|
|
451
|
-
]),
|
|
452
|
-
' const held = await dispatch(operation, input, await actor(request), DECLARED, opening, counting)',
|
|
453
|
-
' return Response.json(held, { status: held.ok ? 200 : (STATUS[held.code] ?? 500) })',
|
|
454
|
-
'}',
|
|
455
|
-
'',
|
|
456
|
-
].join('\n')
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function inbound(one: Webhook): string {
|
|
460
|
-
const at = inboundAt(one.id)
|
|
461
|
-
const secret = (one.requires ?? [])[0]
|
|
462
|
-
const signed = one.arrives !== undefined && secret !== undefined
|
|
463
|
-
const refuse = (why: string): string =>
|
|
464
|
-
" return Response.json({ ok: false, code: " + quoted("denied") + ", error: " + quoted("not allowed") +
|
|
465
|
-
", because: " + quoted(why) + " }, { status: 403 })"
|
|
466
|
-
return [
|
|
467
|
-
...(signed ? ["import { createHmac, timingSafeEqual } from " + quoted("node:crypto")] : []),
|
|
468
|
-
"import { existsSync } from " + quoted(runtime(at, "../../integrations/disk.ts")),
|
|
469
|
-
"import { join } from " + quoted("node:path"),
|
|
470
|
-
"",
|
|
471
|
-
"import { counting, DECLARED, opening } from " + quoted(beside(at, REACHING_AT)),
|
|
472
|
-
"",
|
|
473
|
-
"import { dispatch } from " + quoted(beside(at, DISPATCH_AT)),
|
|
474
|
-
"import { operation } from " + quoted(beside(at, "operations/" + one.invokes + ".ts")),
|
|
475
|
-
"",
|
|
476
|
-
"const ARRIVING = " + JSON.stringify(ANONYMOUS),
|
|
477
|
-
"",
|
|
478
|
-
"const PASSED = existsSync(join(process.cwd(), " + quoted(".thread") + ", " + quoted("gate.json") + "))",
|
|
479
|
-
"",
|
|
480
|
-
"export async function POST(request: Request): Promise<Response> {",
|
|
481
|
-
" if (!PASSED) {",
|
|
482
|
-
refuse("this tree has no passing gate, so nothing in it may answer a request. Run thread gate"),
|
|
483
|
-
" }",
|
|
484
|
-
" const raw = await request.text()",
|
|
485
|
-
...(signed
|
|
486
|
-
? [
|
|
487
|
-
" const carried = request.headers.get(" + quoted(one.arrives as string) + ")",
|
|
488
|
-
" if (carried === null) {",
|
|
489
|
-
refuse("this webhook is signed and the request carried nothing on " + String(one.arrives)),
|
|
490
|
-
" }",
|
|
491
|
-
" const held = await opening.resolve([" + quoted(secret as string) + "])",
|
|
492
|
-
" const mine = Buffer.from(createHmac(" + quoted("sha256") + ", held[" + quoted(secret as string) +
|
|
493
|
-
"] ?? " + quoted("") + ").update(raw).digest(" + quoted("hex") + "))",
|
|
494
|
-
" const said = Buffer.from(carried)",
|
|
495
|
-
" if (said.length !== mine.length || !timingSafeEqual(said, mine)) {",
|
|
496
|
-
refuse("the signature on this request is not what " + String(secret) + " signs a body with"),
|
|
497
|
-
" }",
|
|
498
|
-
]
|
|
499
|
-
: []),
|
|
500
|
-
" const input: unknown = JSON.parse(raw)",
|
|
501
|
-
" const answer = await dispatch(operation, input, ARRIVING, DECLARED, opening, counting)",
|
|
502
|
-
" return Response.json(answer, { status: answer.ok ? 200 : 500 })",
|
|
503
|
-
"}",
|
|
504
|
-
"",
|
|
505
|
-
].join("\n")
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
function declaring(webhooks: readonly Webhook[]): readonly string[] {
|
|
509
|
-
const sheet = resolve(ROOT, TREE + 'targets.ts')
|
|
510
|
-
if (!existsSync(sheet)) return []
|
|
511
|
-
let text = readFileSync(sheet, 'utf8')
|
|
512
|
-
const wrote: string[] = []
|
|
513
|
-
for (const one of webhooks) {
|
|
514
|
-
const id = one.id + '_route'
|
|
515
|
-
const where = listBounds(text, 'WEBHOOKS')
|
|
516
|
-
if (where === undefined) break
|
|
517
|
-
const inside = nestedBounds(text, where, one.id, 'derives')
|
|
518
|
-
if (inside !== undefined && holdsRow(text, inside, id)) continue
|
|
519
|
-
const row = [
|
|
520
|
-
' {',
|
|
521
|
-
" id: " + quoted(id) + ",",
|
|
522
|
-
" at: " + quoted(inboundAt(one.id)) + ",",
|
|
523
|
-
" describes: " + quoted(
|
|
524
|
-
'The file that answers what ' + one.id + ' posts. It checks the signature before anything runs and then hands the body ' +
|
|
525
|
-
'to the same dispatch every other caller reaches, so an inbound event is refused or dispatched and never anything else.',
|
|
526
|
-
) + ",",
|
|
527
|
-
' version: 1,',
|
|
528
|
-
" by: 'thread capabilities',",
|
|
529
|
-
" runs: 'thread capabilities --check',",
|
|
530
|
-
' },',
|
|
531
|
-
].join('\n')
|
|
532
|
-
const next = inside === undefined
|
|
533
|
-
? withField(text, where, one.id, 'derives', '[\n' + row + '\n ]')
|
|
534
|
-
: withRow(text, inside, row)
|
|
535
|
-
if (next === undefined || next === text) continue
|
|
536
|
-
text = next
|
|
537
|
-
wrote.push(id)
|
|
538
|
-
}
|
|
539
|
-
if (wrote.length > 0) writeFileSync(sheet, text, 'utf8')
|
|
540
|
-
return wrote
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
const STORED = /export\s+const\s+store\b/
|
|
544
|
-
|
|
545
|
-
const STORES_AT = 'src/stores'
|
|
546
|
-
|
|
547
|
-
function kept(): readonly string[] {
|
|
548
|
-
const at = resolve(ROOT, STORES_AT)
|
|
549
|
-
if (!existsSync(at)) return []
|
|
550
|
-
return readdirSync(at)
|
|
551
|
-
.filter((one) => one.endsWith('.ts'))
|
|
552
|
-
.filter((one) => {
|
|
553
|
-
try {
|
|
554
|
-
return STORED.test(readFileSync(resolve(at, one), 'utf8'))
|
|
555
|
-
} catch {
|
|
556
|
-
return false
|
|
557
|
-
}
|
|
558
|
-
})
|
|
559
|
-
.sort()
|
|
560
|
-
.map((one) => `${STORES_AT}/${one}`)
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
const ENTITIES_AT = `${TREE}entities.ts`
|
|
564
|
-
|
|
565
|
-
const OPERATIONS_AT = `${TREE}operations.ts`
|
|
566
|
-
|
|
567
|
-
const VAULT_AT = `src/vault.${'ts'}`
|
|
568
|
-
|
|
569
|
-
const served = (one: Capability): boolean => one.live.includes('api') && compiled(one.operation)
|
|
570
|
-
|
|
571
|
-
const held = (one: Capability): boolean => one.browsable && one.live.includes('api')
|
|
572
|
-
|
|
573
|
-
function dispatching(found: readonly Capability[]): string {
|
|
574
|
-
|
|
575
|
-
const reachable = found.filter(served)
|
|
576
|
-
const checked = quoted(beside(DISPATCHING_AT, CHECKED_AT))
|
|
577
|
-
return [
|
|
578
|
-
'import type {} from ' + checked,
|
|
579
|
-
'import type { Calling } from ' + quoted(runtime(DISPATCHING_AT, '../../integrations/target.ts')),
|
|
580
|
-
'',
|
|
581
|
-
'declare module ' + checked + ' {',
|
|
582
|
-
...(reachable.length === 0
|
|
583
|
-
? [' interface Dispatching {}']
|
|
584
|
-
: [' interface Dispatching {', ...reachable.map((one) => ` readonly ${one.operation}: true`), ' }']),
|
|
585
|
-
'}',
|
|
586
|
-
'',
|
|
587
|
-
...(reachable.length === 0
|
|
588
|
-
? []
|
|
589
|
-
: [
|
|
590
|
-
'interface Reaches {',
|
|
591
|
-
' readonly operation: string',
|
|
592
|
-
" readonly method: 'GET' | 'POST'",
|
|
593
|
-
' readonly path: string',
|
|
594
|
-
'}',
|
|
595
|
-
'',
|
|
596
|
-
'async function sent(one: Reaches, input?: unknown): Promise<unknown> {',
|
|
597
|
-
' const answered = await fetch(one.path, {',
|
|
598
|
-
' method: one.method,',
|
|
599
|
-
" headers: { 'content-type': 'application/json' },",
|
|
600
|
-
" ...(one.method === 'GET' ? {} : { body: JSON.stringify(input ?? {}) }),",
|
|
601
|
-
' })',
|
|
602
|
-
' return await answered.json()',
|
|
603
|
-
'}',
|
|
604
|
-
'',
|
|
605
|
-
]),
|
|
606
|
-
`export const calls: Calling<${reachable.length === 0 ? 'never' : reachable.map((one) => quoted(one.operation)).join(' | ')}> = {${
|
|
607
|
-
reachable.length === 0
|
|
608
|
-
? ''
|
|
609
|
-
: `\n${reachable
|
|
610
|
-
.map(
|
|
611
|
-
(one) =>
|
|
612
|
-
` ${one.operation}: (given) => sent({ operation: ${JSON.stringify(one.operation)}, method: ${JSON.stringify(one.api?.method)}, path: ${JSON.stringify(one.api?.path)} }, given),`,
|
|
613
|
-
)
|
|
614
|
-
.join('\n')}\n`
|
|
615
|
-
}}`,
|
|
616
|
-
'',
|
|
617
|
-
'const CALLS: Readonly<Record<string, ((given?: unknown) => Promise<unknown>) | undefined>> = calls',
|
|
618
|
-
'',
|
|
619
|
-
'export async function dispatchTo(operation: string, input?: Readonly<Record<string, unknown>>): Promise<unknown> {',
|
|
620
|
-
' const one = CALLS[operation]',
|
|
621
|
-
' if (one === undefined) {',
|
|
622
|
-
' throw new Error(',
|
|
623
|
-
" `thread: nothing declares an operation called ${operation}, so there is nowhere to send this. ` +",
|
|
624
|
-
" `This app reaches: ${Object.keys(CALLS).join(', ') || '(nothing)'}`,",
|
|
625
|
-
' )',
|
|
626
|
-
' }',
|
|
627
|
-
' return await one(input)',
|
|
628
|
-
'}',
|
|
629
|
-
'',
|
|
630
|
-
].join('\n')
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
export function reaching(
|
|
634
|
-
targets: readonly Target[],
|
|
635
|
-
calls: readonly Call[],
|
|
636
|
-
stores: readonly string[],
|
|
637
|
-
vaulted: boolean,
|
|
638
|
-
carried: boolean,
|
|
639
|
-
): string {
|
|
640
|
-
const taken = (one: Target): readonly string[] =>
|
|
641
|
-
calls.filter((each) => each.of === one.id && one.takes.includes(each.id)).map((each) => each.id.replace(/^cl_/, ''))
|
|
642
|
-
|
|
643
|
-
const rows = targets.map((one) => {
|
|
644
|
-
const mine = taken(one).map((each) => quoted(each))
|
|
645
|
-
return ' readonly ' + one.id + ': ' + (mine.length === 0 ? 'never' : mine.join(' | '))
|
|
646
|
-
})
|
|
647
|
-
|
|
648
|
-
const named = stores.map((one, at) => ({
|
|
649
|
-
from: beside(REACHING_AT, one),
|
|
650
|
-
as: 's' + String(at),
|
|
651
|
-
of: (one.split('/').pop() ?? '').replace(/\.ts$/, ''),
|
|
652
|
-
}))
|
|
653
|
-
|
|
654
|
-
const adapted = targets
|
|
655
|
-
.filter((one) => reachOf(one.id) === 'sdk')
|
|
656
|
-
.flatMap((one, at) => {
|
|
657
|
-
const file = (one.declares ?? [])[0]?.at
|
|
658
|
-
return file === undefined ? [] : [{ from: beside(REACHING_AT, file), as: 'c' + String(at), of: one.id }]
|
|
659
|
-
})
|
|
660
|
-
|
|
661
|
-
const used = new Set<string>()
|
|
662
|
-
const opened = targets
|
|
663
|
-
.map((one) => {
|
|
664
|
-
const reach = reachOf(one.id)
|
|
665
|
-
if (reach === 'sdk') {
|
|
666
|
-
const held = adapted.find((each) => each.of === one.id)
|
|
667
|
-
if (held === undefined) return ''
|
|
668
|
-
const each = calls
|
|
669
|
-
.filter((call) => call.of === one.id && one.takes.includes(call.id))
|
|
670
|
-
.map((call) => quoted(call.id.replace(/^cl_/, '')) + ': ' + held.as + '[' + quoted(call.path) + ']')
|
|
671
|
-
if (each.length > 0) used.add(held.as)
|
|
672
|
-
return ' ' + one.id + ': { ' + each.join(', ') + ' },'
|
|
673
|
-
}
|
|
674
|
-
if (reach === 'disk') {
|
|
675
|
-
const through = one.requires.find((each) => reachOf(each) !== undefined)
|
|
676
|
-
if (through !== undefined) return ''
|
|
677
|
-
used.add('HERE')
|
|
678
|
-
return ' ' + one.id + ': HERE,'
|
|
679
|
-
}
|
|
680
|
-
if (reach === 'cli') {
|
|
681
|
-
const each = calls
|
|
682
|
-
.filter((call) => call.of === one.id && one.takes.includes(call.id))
|
|
683
|
-
.map((call) => quoted(call.id.replace(/^cl_/, '')) + ': ' + quoted(call.path))
|
|
684
|
-
used.add('running')
|
|
685
|
-
return ' ' + one.id + ': running(' + quoted(one.id.replace(/^cli_/, '')) + ', { ' + each.join(', ') + ' }, process.cwd()),'
|
|
686
|
-
}
|
|
687
|
-
if (reach === 'https') {
|
|
688
|
-
used.add('sendingHttp')
|
|
689
|
-
return ' ' + one.id + ': sendingHttp(),'
|
|
690
|
-
}
|
|
691
|
-
const held = named.find((each) => each.of === one.id)
|
|
692
|
-
if (held === undefined) return ''
|
|
693
|
-
used.add(held.as)
|
|
694
|
-
return ' ' + one.id + ': ' + held.as + ','
|
|
695
|
-
})
|
|
696
|
-
.filter((each) => each !== '')
|
|
697
|
-
|
|
698
|
-
return [
|
|
699
|
-
...(used.has('HERE') ? ['import { HERE } from ' + quoted(runtime(REACHING_AT, '../../integrations/disk.ts'))] : []),
|
|
700
|
-
...(used.has('running') ? ['import { running } from ' + quoted(runtime(REACHING_AT, '../../integrations/cli.ts'))] : []),
|
|
701
|
-
...(used.has('sendingHttp') ? ['import { sendingHttp } from ' + quoted(runtime(REACHING_AT, '../../integrations/https.ts'))] : []),
|
|
702
|
-
'import type { Counting } from ' +
|
|
703
|
-
quoted(carried ? packageOf('operations') + '/operations/operations' : runtime(REACHING_AT, '../operations.ts')),
|
|
704
|
-
...named.filter((each) => used.has(each.as)).map((each) => 'import { store as ' + each.as + ' } from ' + quoted(each.from)),
|
|
705
|
-
...adapted.filter((each) => used.has(each.as)).map((each) => 'import { calls as ' + each.as + ' } from ' + quoted(each.from)),
|
|
706
|
-
...(vaulted ? ['import { vault } from ' + quoted(beside(REACHING_AT, VAULT_AT))] : []),
|
|
707
|
-
'',
|
|
708
|
-
'declare module ' +
|
|
709
|
-
quoted(carried ? packageOf('operations') + '/operations/operations' : runtime(REACHING_AT, '../operations.ts')) +
|
|
710
|
-
' {',
|
|
711
|
-
...(rows.length === 0 ? [' interface Reaching {}'] : [' interface Reaching {', ...rows, ' }']),
|
|
712
|
-
'}',
|
|
713
|
-
'',
|
|
714
|
-
'export const DECLARED: Readonly<Record<string, unknown>> = {',
|
|
715
|
-
...opened,
|
|
716
|
-
'}',
|
|
717
|
-
'',
|
|
718
|
-
vaulted ? 'export const opening = { resolve: vault }' : 'export const opening = undefined',
|
|
719
|
-
'',
|
|
720
|
-
'export const counting: Counting | undefined = undefined',
|
|
721
|
-
'',
|
|
722
|
-
].join('\n')
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
const reading = (header: string): string => 'request.headers.get(' + quoted(header) + ')'
|
|
726
|
-
|
|
727
|
-
const CARRIED = ENVIRONMENT.flatMap(([fact, header, fills]) => {
|
|
728
|
-
const taken = header === null ? fills : fills === null ? reading(header) : reading(header) + ' ?? ' + fills
|
|
729
|
-
return taken === null ? [] : [{ fact, taken, always: fills !== null }]
|
|
730
|
-
})
|
|
731
|
-
|
|
732
|
-
function caller(keeping: boolean, kept: string | undefined): string {
|
|
733
|
-
const looking = keeping && kept !== undefined
|
|
734
|
-
return [
|
|
735
|
-
'import type { Actor, Environment } from ' + quoted(runtime(ACTOR_AT, '../../core/actor.ts')),
|
|
736
|
-
...(looking ? ['import { ENTITIES } from ' + quoted(beside(ACTOR_AT, ENTITIES_AT))] : []),
|
|
737
|
-
...(looking ? ['import { DECLARED } from ' + quoted(beside(ACTOR_AT, REACHING_AT))] : []),
|
|
738
|
-
...(looking ? ['import type { Store } from ' + quoted(runtime(ACTOR_AT, '../../integrations/target.ts'))] : []),
|
|
739
|
-
'',
|
|
740
|
-
'const ANONYMOUS: Actor = ' + JSON.stringify(ANONYMOUS),
|
|
741
|
-
'',
|
|
742
|
-
'function environment(request: Request): Environment {',
|
|
743
|
-
...CARRIED.map((one) => ' const ' + one.fact + ' = ' + one.taken),
|
|
744
|
-
' return {',
|
|
745
|
-
...CARRIED.map((one) =>
|
|
746
|
-
one.always ? ' ' + one.fact + ',' : ' ...(' + one.fact + ' === null ? {} : { ' + one.fact + ' }),',
|
|
747
|
-
),
|
|
748
|
-
' }',
|
|
749
|
-
'}',
|
|
750
|
-
'',
|
|
751
|
-
...(looking
|
|
752
|
-
? [
|
|
753
|
-
'const worn = (held: unknown): readonly string[] =>',
|
|
754
|
-
' Array.isArray(held) ? held.filter((one) => typeof one === ' + quoted('string') + ') : typeof held === ' + quoted('string') + ' ? [held] : []',
|
|
755
|
-
'',
|
|
756
|
-
'function addressed(request: Request, from: string): string | null {',
|
|
757
|
-
' if (from.startsWith(' + quoted('cookie:') + ')) {',
|
|
758
|
-
' const jar = request.headers.get(' + quoted('cookie') + ') ?? ' + quoted(''),
|
|
759
|
-
' const want = from.slice(' + quoted('cookie:') + '.length).trim() + ' + quoted('='),
|
|
760
|
-
' const found = jar.split(' + quoted(';') + ').map((one) => one.trim()).find((one) => one.startsWith(want))',
|
|
761
|
-
' return found === undefined ? null : decodeURIComponent(found.slice(want.length))',
|
|
762
|
-
' }',
|
|
763
|
-
' const held = request.headers.get(from)',
|
|
764
|
-
' if (held === null) return null',
|
|
765
|
-
' const bare = held.startsWith(' + quoted('Bearer ') + ') ? held.slice(' + quoted('Bearer ') + '.length) : held',
|
|
766
|
-
' return bare.trim() === ' + quoted('') + ' ? null : bare.trim()',
|
|
767
|
-
'}',
|
|
768
|
-
'',
|
|
769
|
-
]
|
|
770
|
-
: []),
|
|
771
|
-
'export async function actor(request: Request): Promise<Actor> {',
|
|
772
|
-
' const anyone: Actor = { ...ANONYMOUS, ...environment(request) }',
|
|
773
|
-
...(looking
|
|
774
|
-
? [
|
|
775
|
-
' const who = ENTITIES.find((one) => one.actor !== undefined)',
|
|
776
|
-
' if (who?.actor === undefined) return anyone',
|
|
777
|
-
' const address = (who.must ?? []).flatMap((one) => (' + quoted('unique') + ' in one ? one.unique : []))[0]',
|
|
778
|
-
' if (address === undefined) return anyone',
|
|
779
|
-
' const held = addressed(request, who.actor.from)',
|
|
780
|
-
' if (held === null) return anyone',
|
|
781
|
-
' const keeper = DECLARED[' + quoted(kept ?? '') + '] as Store | undefined',
|
|
782
|
-
' if (keeper === undefined) return anyone',
|
|
783
|
-
' const token = who.actor.verifies',
|
|
784
|
-
' const row =',
|
|
785
|
-
' token === undefined',
|
|
786
|
-
' ? await keeper.get(anyone, who.id, held, [',
|
|
787
|
-
' address,',
|
|
788
|
-
' who.actor.roles,',
|
|
789
|
-
' ...(who.actor.entitlements === undefined ? [] : [who.actor.entitlements]),',
|
|
790
|
-
' ])',
|
|
791
|
-
' : ((await keeper.list(anyone, who.id, { where: { [token]: held }, limit: 1 }))[0] ?? null)',
|
|
792
|
-
' if (row === null) return anyone',
|
|
793
|
-
' return {',
|
|
794
|
-
' ...anyone,',
|
|
795
|
-
' id: String(row[address] ?? held),',
|
|
796
|
-
' roles: worn(row[who.actor.roles]),',
|
|
797
|
-
' entitlements: who.actor.entitlements === undefined ? [] : worn(row[who.actor.entitlements]),',
|
|
798
|
-
' }',
|
|
799
|
-
]
|
|
800
|
-
: [' return anyone']),
|
|
801
|
-
'}',
|
|
802
|
-
'',
|
|
803
|
-
].join('\n')
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
const carries = (): boolean => existsSync(resolve(ROOT, 'node_modules', packageOf('operations')))
|
|
807
|
-
|
|
808
|
-
const emitted = (): boolean => existsSync(resolve(ROOT, CHECKED_AT))
|
|
809
|
-
|
|
810
|
-
const files = (
|
|
811
|
-
found: readonly Capability[],
|
|
812
|
-
targets: readonly Target[],
|
|
813
|
-
calls: readonly Call[],
|
|
814
|
-
keeps: string | undefined,
|
|
815
|
-
webhooks: readonly Webhook[] = [],
|
|
816
|
-
): readonly Written[] => [
|
|
817
|
-
{ at: CAPABILITIES_AT, text: printed(found) },
|
|
818
|
-
{ at: OPENAPI_AT, text: printed(openapi(found)) },
|
|
819
|
-
{ at: MCP_AT, text: printed(mcp(found)) },
|
|
820
|
-
{ at: CLIENT_AT, text: client(found) },
|
|
821
|
-
|
|
822
|
-
{ at: REACHING_AT, text: reaching(targets, calls, kept(), existsSync(resolve(ROOT, VAULT_AT)), carries()) },
|
|
823
|
-
{ at: ACTOR_AT, text: caller(existsSync(resolve(ROOT, ENTITIES_AT)), keeps) },
|
|
824
|
-
...(emitted() ? [{ at: DISPATCHING_AT, text: dispatching(found) }] : []),
|
|
825
|
-
...found.filter(served).map((one) => ({ at: routeAt(one.operation), text: route(one) })),
|
|
826
|
-
...webhooks
|
|
827
|
-
.filter((one) => found.some((each) => each.operation === one.invokes && each.live.includes('webhook') && each.webhooks.includes(one.id) && compiled(one.invokes)))
|
|
828
|
-
.flatMap((one) => {
|
|
829
|
-
const at = (one.derives ?? []).map((each) => each.at).find((each) => each === inboundAt(one.id))
|
|
830
|
-
return at === undefined ? [] : [{ at, text: inbound(one) }]
|
|
831
|
-
}),
|
|
832
|
-
]
|
|
833
|
-
|
|
834
|
-
interface Carried {
|
|
835
|
-
readonly live: readonly Surface[]
|
|
836
|
-
readonly off: ReadonlyMap<string, readonly Surface[]>
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
function unmoved(): Carried | undefined {
|
|
840
|
-
const tree = defaulted()
|
|
841
|
-
if (tree === undefined || tree.stated) return undefined
|
|
842
|
-
const at = resolve(ROOT, CAPABILITIES_AT)
|
|
843
|
-
const switches = switched(existsSync(at) ? readFileSync(at, 'utf8') : undefined)
|
|
844
|
-
if (switches.size === 0) return undefined
|
|
845
|
-
const live = SURFACES.filter((each) => [...switches.values()].some((one) => one.includes(each)))
|
|
846
|
-
const off = new Map(
|
|
847
|
-
[...switches]
|
|
848
|
-
.map(([id, one]) => [id, live.filter((each) => !one.includes(each))] as const)
|
|
849
|
-
.filter(([, one]) => one.length > 0),
|
|
850
|
-
)
|
|
851
|
-
const same = off.size === 0 && live.length === UNSTATED.length && live.every((each) => UNSTATED.includes(each))
|
|
852
|
-
return same ? undefined : { live, off }
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
export const asArray = (surfaces: readonly Surface[]): string => `[${surfaces.map((one) => quoted(one)).join(', ')}]`
|
|
856
|
-
|
|
857
|
-
function carry(plan: Carried): readonly string[] {
|
|
858
|
-
const at = resolve(ROOT, DEFAULTS_AT)
|
|
859
|
-
const put = withTopField(readFileSync(at, 'utf8'), 'live', asArray(plan.live))
|
|
860
|
-
if (put === undefined) return []
|
|
861
|
-
writeFileSync(at, put, 'utf8')
|
|
862
|
-
const sheet = resolve(ROOT, OPERATIONS_AT)
|
|
863
|
-
let text = readFileSync(sheet, 'utf8')
|
|
864
|
-
const told = [`${DEFAULTS_AT} is live on ${plan.live.join(', ') || 'nothing'}`]
|
|
865
|
-
for (const [id, off] of plan.off) {
|
|
866
|
-
const where = listBounds(text, MEMBERS_LIST)
|
|
867
|
-
const next = where === undefined ? undefined : withField(text, where, id, 'off', asArray(off))
|
|
868
|
-
if (next === undefined) continue
|
|
869
|
-
text = next
|
|
870
|
-
told.push(`${id} turns off ${off.join(', ')}`)
|
|
871
|
-
}
|
|
872
|
-
writeFileSync(sheet, text, 'utf8')
|
|
873
|
-
return told
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
function routed(found: readonly Capability[], writing: boolean): readonly string[] {
|
|
877
|
-
const sheet = resolve(ROOT, OPERATIONS_AT)
|
|
878
|
-
if (!existsSync(sheet)) return []
|
|
879
|
-
let text = readFileSync(sheet, 'utf8')
|
|
880
|
-
const wrong: string[] = []
|
|
881
|
-
for (const one of found) {
|
|
882
|
-
const id = `${one.operation}_route`
|
|
883
|
-
const row = said(
|
|
884
|
-
{ id, at: routeAt(one.operation), describes: ROUTE, by: 'thread capabilities', runs: 'thread capabilities --check' },
|
|
885
|
-
' ',
|
|
886
|
-
)
|
|
887
|
-
const members = listBounds(text, MEMBERS_LIST)
|
|
888
|
-
const inside = members === undefined ? undefined : nestedBounds(text, members, one.operation, 'derives')
|
|
889
|
-
const standing = inside === undefined ? undefined : rowsIn(text, inside).find((each) => each.id === id)
|
|
890
|
-
const wants = one.live.includes('api')
|
|
891
|
-
if (wants ? standing?.source.trim() === row.trim() : standing === undefined) continue
|
|
892
|
-
wrong.push(
|
|
893
|
-
wants
|
|
894
|
-
? `${one.operation} answers on api and its row ${standing === undefined ? 'declares no route' : 'says something else about its route'}`
|
|
895
|
-
: `${one.operation} does not answer on api and its row still declares ${routeAt(one.operation)}`,
|
|
896
|
-
)
|
|
897
|
-
if (!writing) continue
|
|
898
|
-
let next = inside !== undefined && standing !== undefined ? withoutRow(text, inside, id) : text
|
|
899
|
-
if (wants) {
|
|
900
|
-
const list = listBounds(next, MEMBERS_LIST)
|
|
901
|
-
const within = list === undefined ? undefined : nestedBounds(next, list, one.operation, 'derives')
|
|
902
|
-
const put =
|
|
903
|
-
within !== undefined
|
|
904
|
-
? withRow(next, within, row)
|
|
905
|
-
: list === undefined
|
|
906
|
-
? undefined
|
|
907
|
-
: withField(next, list, one.operation, 'derives', `[\n${row}\n ]`)
|
|
908
|
-
if (put !== undefined) next = put
|
|
909
|
-
}
|
|
910
|
-
text = next
|
|
911
|
-
}
|
|
912
|
-
if (writing && wrong.length > 0) writeFileSync(sheet, text, 'utf8')
|
|
913
|
-
return wrong
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
const unserved = (found: readonly Capability[]): readonly string[] =>
|
|
917
|
-
found
|
|
918
|
-
.filter((one) => !one.live.includes('api') && existsSync(resolve(ROOT, routeAt(one.operation))))
|
|
919
|
-
.map((one) => routeAt(one.operation))
|
|
920
|
-
|
|
921
|
-
if (invoked(import.meta.url)) {
|
|
922
|
-
const words = argv.slice(2).filter((one) => !one.startsWith('-'))
|
|
923
|
-
if (words[0] !== undefined && words[0] !== 'read') {
|
|
924
|
-
console.error(
|
|
925
|
-
'capabilities: `' + words[0] + '` is not something this command does. `thread capabilities` writes what can be reached, and ' +
|
|
926
|
-
'`thread capabilities read <mark>` prints one. Nothing was written.',
|
|
927
|
-
)
|
|
928
|
-
exit(1)
|
|
929
|
-
}
|
|
930
|
-
if (!argv.includes('--check')) {
|
|
931
|
-
const declared = declaring(sheet<Webhook>('targets.ts', 'WEBHOOKS'))
|
|
932
|
-
for (const one of declared) console.log(`capabilities: declared ${one} in ${TREE}targets.ts, so the route it writes has a row that says the address was agreed to`)
|
|
933
|
-
if (declared.length > 0) forget()
|
|
934
|
-
const pending = unmoved()
|
|
935
|
-
if (pending !== undefined) {
|
|
936
|
-
console.log(
|
|
937
|
-
`capabilities: carried what ${CAPABILITIES_AT} had switched onto the rows it is worked out from now — ` +
|
|
938
|
-
`${carry(pending).join('; ')}. Nothing in it is set by hand any more.`,
|
|
939
|
-
)
|
|
940
|
-
forget()
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
const { found, refusals, live } = await derive()
|
|
944
|
-
for (const one of refusals) console.error(`capabilities: ${one}`)
|
|
945
|
-
|
|
946
|
-
const mark = (one: Capability): string =>
|
|
947
|
-
marked([one.operation, one.describes, one.requires.join(','), one.nodes.join(','), one.live.join(',')])
|
|
948
|
-
|
|
949
|
-
const verb = words[0]
|
|
950
|
-
|
|
951
|
-
if (verb === 'read') {
|
|
952
|
-
const which = words[1]
|
|
953
|
-
const one = found.find((each) => mark(each) === which)
|
|
954
|
-
if (one === undefined) {
|
|
955
|
-
console.error(
|
|
956
|
-
`capabilities: \`${which ?? '(nothing)'}\` is not a mark this tree issued. Run \`thread capabilities\` to see them — each is printed in brackets beside the capability it belongs to.`,
|
|
957
|
-
)
|
|
958
|
-
exit(1)
|
|
959
|
-
}
|
|
960
|
-
stdout.write(`${one.operation} — ${one.describes}\n`)
|
|
961
|
-
stdout.write(`requires ${one.requires.length === 0 ? 'nothing' : one.requires.join(', ')}\n`)
|
|
962
|
-
stdout.write(`nodes: ${one.nodes.length === 0 ? '(none)' : one.nodes.join(', ')}\n`)
|
|
963
|
-
stdout.write(
|
|
964
|
-
`live: ${SURFACES.map((each) => `${one.live.includes(each) ? '[on] ' : '[off]'} ${each}`).join(' ')}\n`,
|
|
965
|
-
)
|
|
966
|
-
stdout.write(
|
|
967
|
-
`${one.browsable ? 'may be called from a page' : 'server only — it reaches a target that needs a server secret'}\n`,
|
|
968
|
-
)
|
|
969
|
-
stdout.write(`api: ${one.api.method} ${one.api.path}\n`)
|
|
970
|
-
stdout.write(`cli: ${one.cli.invocation}\n`)
|
|
971
|
-
stdout.write(`mcp: ${one.mcp.name} — ${one.mcp.description}\n`)
|
|
972
|
-
stdout.write('input:\n')
|
|
973
|
-
for (const line of printed(one.input).trimEnd().split('\n')) stdout.write(` ${line}\n`)
|
|
974
|
-
exit(0)
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
const checking = argv.includes('--check')
|
|
978
|
-
let adrift = 0
|
|
979
|
-
|
|
980
|
-
if (found.length === 0 && refusals.length === 0) {
|
|
981
|
-
console.log('capabilities: this app declares no operation, so nothing is reachable through one yet.')
|
|
982
|
-
exit(adrift > 0 ? 1 : 0)
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
const all = sheet<Target>('targets.ts', 'TARGETS')
|
|
986
|
-
const people = sheet<{ readonly id: string; readonly actor?: unknown }>('entities.ts', 'ENTITIES')
|
|
987
|
-
const one = people.find((each) => each.actor !== undefined)
|
|
988
|
-
const where = one === undefined ? undefined : targetOf(one.id, all)
|
|
989
|
-
const written = files(
|
|
990
|
-
found,
|
|
991
|
-
all,
|
|
992
|
-
sheet<Call>('targets.ts', 'CALLS'),
|
|
993
|
-
where === undefined || typeof where === 'string' ? undefined : where.id,
|
|
994
|
-
sheet<Webhook>('targets.ts', 'WEBHOOKS'),
|
|
995
|
-
)
|
|
996
|
-
|
|
997
|
-
if (checking) {
|
|
998
|
-
const drifted = written.filter((one) => {
|
|
999
|
-
const path = resolve(ROOT, one.at)
|
|
1000
|
-
return (existsSync(path) ? readFileSync(path, 'utf8') : '') !== one.text
|
|
1001
|
-
})
|
|
1002
|
-
for (const one of drifted) {
|
|
1003
|
-
console.error(
|
|
1004
|
-
`capabilities: ${one.at} is not what the operations derive. Run \`thread capabilities\` and commit the result.`,
|
|
1005
|
-
)
|
|
1006
|
-
}
|
|
1007
|
-
const unsettled = [
|
|
1008
|
-
...(unmoved() === undefined ? [] : [`${CAPABILITIES_AT} switches surfaces that no row states yet`]),
|
|
1009
|
-
...routed(found, false),
|
|
1010
|
-
...unserved(found).map((at) => `${at} is still on disk to be served, and its operation does not answer on api`),
|
|
1011
|
-
]
|
|
1012
|
-
for (const one of unsettled) console.error(`capabilities: ${one}. Run \`thread capabilities\` and commit the result.`)
|
|
1013
|
-
adrift += unsettled.length
|
|
1014
|
-
const off = found.filter((one) => one.live.length < live.length)
|
|
1015
|
-
console.log(
|
|
1016
|
-
`capabilities: ${String(found.length)} operation(s) reachable, ${String(off.length)} with a surface switched off, ` +
|
|
1017
|
-
`${String(drifted.length + adrift)} finding(s).`,
|
|
1018
|
-
)
|
|
1019
|
-
exit(drifted.length + adrift > 0 || refusals.length > 0 ? 1 : 0)
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
if (refusals.length > 0) {
|
|
1023
|
-
console.error(
|
|
1024
|
-
`capabilities: ${String(refusals.length)} refusal(s) above, so nothing was written`,
|
|
1025
|
-
)
|
|
1026
|
-
exit(1)
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
if (!emitted()) {
|
|
1030
|
-
const gone = resolve(ROOT, DISPATCHING_AT)
|
|
1031
|
-
if (existsSync(gone)) rmSync(gone)
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
for (const one of routed(found, true)) console.log(`capabilities: ${one}, so ${OPERATIONS_AT} was changed to match`)
|
|
1035
|
-
for (const at of unserved(found)) {
|
|
1036
|
-
rmSync(resolve(ROOT, at))
|
|
1037
|
-
const folder = dirname(resolve(ROOT, at))
|
|
1038
|
-
if (readdirSync(folder).length === 0) rmSync(folder, { recursive: true, force: true })
|
|
1039
|
-
console.log(`capabilities: deleted ${at}, because its operation does not answer on api and nothing may serve it`)
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
for (const one of written) {
|
|
1043
|
-
const path = resolve(ROOT, one.at)
|
|
1044
|
-
writeFileSync(path, one.text)
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
const shipped = found.filter(held).length
|
|
1048
|
-
stdout.write(
|
|
1049
|
-
`capabilities: wrote ${written.map((one) => one.at).join(', ')} — ${String(found.length)} operation(s), ` +
|
|
1050
|
-
`${String(shipped)} of them in the client bundle.\n`,
|
|
1051
|
-
)
|
|
1052
|
-
exit(0)
|
|
1053
|
-
}
|
|
1054
|
-
|