@prestack/thread 0.0.3 → 0.1.1
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 +310 -0
- package/dist/core/{asking.d.ts → answers.d.ts} +6 -14
- package/dist/core/{asking.js → answers.js} +13 -332
- package/dist/core/{asking.ts → answers.ts} +14 -357
- package/dist/core/attach.js +2 -2
- package/dist/core/attach.ts +2 -3
- package/dist/core/chain.js +4 -11
- package/dist/core/chain.ts +4 -3
- package/dist/core/client.d.ts +57 -0
- package/dist/core/client.js +243 -0
- package/dist/core/client.ts +279 -0
- package/dist/core/descend.d.ts +2 -0
- package/dist/core/descend.js +109 -16
- package/dist/core/descend.ts +120 -16
- package/dist/core/files.d.ts +45 -0
- package/dist/core/files.js +145 -0
- package/dist/core/files.ts +193 -0
- package/dist/core/init.d.ts +1 -0
- package/dist/core/init.js +10 -2
- package/dist/core/init.ts +8 -2
- package/dist/core/invoked.js +1 -1
- package/dist/core/invoked.ts +1 -1
- package/dist/core/rows.d.ts +2 -0
- package/dist/core/rows.js +65 -0
- package/dist/core/rows.ts +58 -0
- package/dist/core/tree.js +1 -1
- package/dist/core/tree.ts +1 -1
- package/dist/derivation/attach.js +0 -12
- package/dist/derivation/attach.ts +0 -12
- package/dist/derivation/generators/build.js +8 -7
- package/dist/derivation/generators/build.ts +8 -7
- package/dist/derivation/generators/release.d.ts +1 -1
- package/dist/derivation/generators/release.js +15 -23
- package/dist/derivation/generators/release.ts +15 -15
- package/dist/integrations/attach.js +1 -19
- package/dist/integrations/attach.ts +1 -19
- package/dist/integrations/generators/read.js +2 -2
- package/dist/integrations/generators/read.ts +4 -4
- package/dist/integrations/generators/vault.js +4 -12
- package/dist/integrations/generators/vault.ts +4 -4
- package/dist/integrations/module.js +5 -3
- package/dist/integrations/module.ts +5 -3
- package/dist/integrations/vault.js +5 -5
- package/dist/integrations/vault.ts +5 -5
- package/dist/integrity/attach.js +0 -12
- package/dist/integrity/attach.ts +0 -12
- package/dist/integrity/check-threads.js +1 -1
- package/dist/integrity/check-threads.ts +1 -1
- package/dist/integrity/constraint.d.ts +11 -2
- package/dist/integrity/constraint.js +163 -20
- package/dist/integrity/constraint.ts +174 -22
- package/dist/integrity/generators/compiler.js +7 -1
- package/dist/integrity/generators/compiler.ts +10 -1
- package/dist/integrity/generators/gate.js +23 -18
- package/dist/integrity/generators/gate.ts +29 -9
- package/dist/integrity/generators/publish.js +14 -26
- package/dist/integrity/generators/publish.ts +14 -17
- package/dist/integrity/generators/trial.d.ts +2 -2
- package/dist/integrity/generators/trial.js +108 -50
- package/dist/integrity/generators/trial.ts +107 -48
- package/dist/integrity/lint.js +3 -3
- package/dist/integrity/lint.ts +3 -3
- package/dist/integrity/lsp/diagnostics.js +1 -1
- package/dist/integrity/lsp/diagnostics.ts +1 -1
- package/dist/integrity/lsp/lint.js +1 -1
- package/dist/integrity/lsp/lint.ts +1 -1
- package/dist/integrity/slopnator.d.ts +1 -14
- package/dist/integrity/slopnator.js +8 -40
- package/dist/integrity/slopnator.ts +9 -51
- package/dist/integrity/slopnators/sl_broken_lineage.js +4 -2
- package/dist/integrity/slopnators/sl_broken_lineage.ts +4 -2
- package/dist/integrity/slopnators/sl_declared_elsewhere.js +24 -3
- package/dist/integrity/slopnators/sl_declared_elsewhere.ts +24 -3
- package/dist/integrity/slopnators/sl_derived_drift.js +10 -4
- package/dist/integrity/slopnators/sl_derived_drift.ts +10 -4
- package/dist/integrity/slopnators/sl_inert_island.js +7 -13
- package/dist/integrity/slopnators/sl_inert_island.ts +7 -16
- package/dist/integrity/slopnators/sl_inert_slopnator.js +8 -8
- package/dist/integrity/slopnators/sl_inert_slopnator.ts +8 -8
- package/dist/integrity/slopnators/sl_missing_row.js +14 -9
- package/dist/integrity/slopnators/sl_missing_row.ts +14 -10
- package/dist/integrity/slopnators/sl_no_child_process.js +8 -8
- package/dist/integrity/slopnators/sl_no_child_process.ts +8 -8
- package/dist/integrity/slopnators/sl_no_import_cycle.js +1 -1
- package/dist/integrity/slopnators/sl_no_import_cycle.ts +1 -1
- package/dist/integrity/slopnators/sl_no_such_command.js +20 -5
- package/dist/integrity/slopnators/sl_no_such_command.ts +22 -5
- package/dist/integrity/slopnators/sl_only_takes_a_literal.js +8 -8
- package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +8 -8
- package/dist/integrity/slopnators/sl_stale_path.js +91 -18
- package/dist/integrity/slopnators/sl_stale_path.ts +97 -18
- package/dist/integrity/slopnators/sl_twice_meant.js +25 -2
- package/dist/integrity/slopnators/sl_twice_meant.ts +28 -2
- package/dist/integrity/slopnators/sl_twice_parented.js +26 -5
- package/dist/integrity/slopnators/sl_twice_parented.ts +26 -5
- package/dist/integrity/slopnators/sl_undeclared_node.js +18 -2
- package/dist/integrity/slopnators/sl_undeclared_node.ts +18 -2
- package/dist/integrity/slopnators/sl_undeclared_operation.js +3 -1
- package/dist/integrity/slopnators/sl_undeclared_operation.ts +3 -1
- package/dist/integrity/slopnators/sl_undeclared_variant.js +6 -2
- package/dist/integrity/slopnators/sl_undeclared_variant.ts +6 -2
- package/dist/integrity/slopnators/sl_ungoverned_island.js +3 -4
- package/dist/integrity/slopnators/sl_ungoverned_island.ts +3 -4
- package/dist/integrity/slopnators/sl_unkept_rows.js +1 -1
- package/dist/integrity/slopnators/sl_unkept_rows.ts +1 -1
- package/dist/integrity/slopnators/sl_unpaired_member.js +13 -8
- package/dist/integrity/slopnators/sl_unpaired_member.ts +16 -8
- package/dist/integrity/slopnators/sl_unpaired_slopnator.js +16 -3
- package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +16 -3
- package/dist/integrity/slopnators/sl_unthreaded.d.ts +1 -0
- package/dist/integrity/slopnators/sl_unthreaded.js +18 -10
- package/dist/integrity/slopnators/sl_unthreaded.ts +19 -11
- package/dist/integrity/slopnators/sl_untranslated_key.js +1 -1
- package/dist/integrity/slopnators/sl_untranslated_key.ts +1 -1
- package/dist/integrity/slopnators/sl_unversioned.js +16 -2
- package/dist/integrity/slopnators/sl_unversioned.ts +16 -2
- package/dist/integrity/sweep.js +17 -3
- package/dist/integrity/sweep.ts +20 -3
- package/dist/knowledge/attach.js +0 -6
- package/dist/knowledge/attach.ts +0 -6
- package/dist/knowledge/generators/docsgen.js +12 -3
- package/dist/knowledge/generators/docsgen.ts +12 -2
- package/dist/knowledge/generators/glossary.js +1 -1
- package/dist/knowledge/generators/glossary.ts +1 -1
- package/dist/operations/attach.js +2 -26
- package/dist/operations/attach.ts +2 -27
- package/dist/operations/dispatch.js +2 -3
- package/dist/operations/dispatch.ts +2 -3
- package/dist/operations/generators/capability.js +4 -7
- package/dist/operations/generators/capability.ts +3 -6
- package/dist/operations/generators/compile.js +8 -13
- package/dist/operations/generators/compile.ts +8 -4
- package/dist/operations/generators/permits.js +4 -13
- package/dist/operations/generators/permits.ts +3 -3
- package/dist/operations/generators/seed.js +5 -2
- package/dist/operations/generators/seed.ts +6 -2
- package/dist/operations/generators/trial.js +10 -10
- package/dist/operations/generators/trial.ts +10 -10
- package/dist/operations/http.js +4 -1
- package/dist/operations/http.ts +6 -1
- package/package.json +2 -2
- package/dist/core/cli.d.ts +0 -2
- package/dist/core/cli.js +0 -1761
- package/dist/core/cli.ts +0 -2004
- package/dist/core/generators/meta.d.ts +0 -2
- package/dist/core/generators/meta.js +0 -829
- package/dist/core/generators/meta.ts +0 -1006
- package/dist/derivation/generators/derivation.d.ts +0 -3
- package/dist/derivation/generators/derivation.js +0 -273
- package/dist/derivation/generators/derivation.ts +0 -347
- package/dist/derivation/makers/release.d.ts +0 -1
- package/dist/derivation/makers/release.js +0 -107
- package/dist/derivation/makers/release.ts +0 -135
- package/dist/integrations/generators/secret.d.ts +0 -1
- package/dist/integrations/generators/secret.js +0 -233
- package/dist/integrations/generators/secret.ts +0 -289
- package/dist/integrations/generators/target.d.ts +0 -1
- package/dist/integrations/generators/target.js +0 -775
- package/dist/integrations/generators/target.ts +0 -944
- package/dist/integrity/generators/rule.d.ts +0 -2
- package/dist/integrity/generators/rule.js +0 -213
- package/dist/integrity/generators/rule.ts +0 -271
- package/dist/integrity/generators/seal.d.ts +0 -4
- package/dist/integrity/generators/seal.js +0 -101
- package/dist/integrity/generators/seal.ts +0 -117
- package/dist/integrity/sealed.d.ts +0 -1
- package/dist/integrity/sealed.js +0 -56
- package/dist/integrity/sealed.ts +0 -56
- package/dist/knowledge/generators/skills.d.ts +0 -1
- package/dist/knowledge/generators/skills.js +0 -222
- package/dist/knowledge/generators/skills.ts +0 -263
- package/dist/operations/generators/grant.d.ts +0 -1
- package/dist/operations/generators/grant.js +0 -267
- package/dist/operations/generators/grant.ts +0 -336
- package/dist/operations/generators/operation.d.ts +0 -1
- package/dist/operations/generators/operation.js +0 -811
- package/dist/operations/generators/operation.ts +0 -972
- package/dist/operations/makers/capability.d.ts +0 -1
- package/dist/operations/makers/capability.js +0 -117
- package/dist/operations/makers/capability.ts +0 -166
- package/dist/operations/makers/trial.d.ts +0 -1
- package/dist/operations/makers/trial.js +0 -413
- package/dist/operations/makers/trial.ts +0 -520
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process'
|
|
2
|
-
import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
-
import { dirname, join, relative } from 'node:path'
|
|
4
|
-
import { argv, cwd, env, execPath, stdout } from 'node:process'
|
|
5
|
-
import { fileURLToPath } from 'node:url'
|
|
6
|
-
|
|
7
|
-
import { SKILLS_AT, declared, nouns, skillOf } from '../../core/asking.ts'
|
|
8
|
-
import { generators } from '../../core/chain.ts'
|
|
9
|
-
import { TREE } from '../../core/descend.ts'
|
|
10
|
-
import { LOADER, exit, invoked } from '../../core/invoked.ts'
|
|
11
|
-
import { ORGANISATION, named } from '../../core/scope.ts'
|
|
12
|
-
import type { Tree } from '../../core/tree.ts'
|
|
13
|
-
import { findTree } from '../../core/tree.ts'
|
|
14
|
-
|
|
15
|
-
const WHERE = SKILLS_AT
|
|
16
|
-
|
|
17
|
-
const RECEIPT_LINE = /^[a-z]+: receipt .*$/gm
|
|
18
|
-
|
|
19
|
-
const NONCE = /--receipt [0-9a-f]{6,}/g
|
|
20
|
-
|
|
21
|
-
const CRLF = /\r\n/g
|
|
22
|
-
|
|
23
|
-
const META = fileURLToPath(new URL('../../core/generators/meta.ts', import.meta.url))
|
|
24
|
-
|
|
25
|
-
const fileFor = (noun: string): string => skillOf(noun)
|
|
26
|
-
|
|
27
|
-
async function offering(tree: Tree): Promise<ReadonlyMap<string, string>> {
|
|
28
|
-
const answers = relative(tree.at, META).split('\\').join('/')
|
|
29
|
-
const found = new Map(nouns(await generators(tree)))
|
|
30
|
-
found.set('meta', answers)
|
|
31
|
-
for (const noun of (await declared(tree)).keys()) if (!found.has(noun)) found.set(noun, answers)
|
|
32
|
-
return found
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function printed(root: string, at: string, noun: string): { readonly refused: string; readonly body: string } {
|
|
36
|
-
if (at === '') return { refused: `nothing in this tree is written by \`thread create ${noun}\``, body: '' }
|
|
37
|
-
|
|
38
|
-
const ran = spawnSync(execPath, [...LOADER, join(root, at)], {
|
|
39
|
-
cwd: root,
|
|
40
|
-
encoding: 'utf8',
|
|
41
|
-
env: { ...env, THREAD_NOUN: noun },
|
|
42
|
-
|
|
43
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
44
|
-
})
|
|
45
|
-
const body = String(ran.stdout ?? '').replace(CRLF, '\n').replace(RECEIPT_LINE, '').trimEnd()
|
|
46
|
-
const said = String(ran.stderr ?? '').replace(CRLF, '\n').trim().split('\n')[0] ?? ''
|
|
47
|
-
return { refused: body === '' ? said || 'it printed nothing' : '', body }
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function skill(noun: string, body: string): string {
|
|
51
|
-
return [
|
|
52
|
-
`# ${noun}`,
|
|
53
|
-
'',
|
|
54
|
-
`Everything \`npx thread create ${noun}\` knows, written out by \`npx thread skills\`.`,
|
|
55
|
-
'',
|
|
56
|
-
'**Do not edit this file.** It is derived from the tree, so an edit here is reverted the next',
|
|
57
|
-
'time the command runs, and `npx thread skills --check` fails until it is. If something below is',
|
|
58
|
-
'wrong or missing, the command that printed it is what to change.',
|
|
59
|
-
'',
|
|
60
|
-
`**Read this before you declare a ${noun}.** What follows is what already exists. The reason it`,
|
|
61
|
-
'exists is that things get built twice by people who did not look — thirteen elements were once',
|
|
62
|
-
'declared in one app that every one of them already shipped, and the cost was two days of fixes',
|
|
63
|
-
'landing on a page nobody was looking at. Open what it names before you add to it.',
|
|
64
|
-
'',
|
|
65
|
-
'When you are ready to write, run the command yourself for a live receipt. The one printed here',
|
|
66
|
-
'has been removed on purpose: a receipt is spent by one run and goes stale the moment its sheet',
|
|
67
|
-
'changes, so a copy kept in a file is always the wrong one.',
|
|
68
|
-
'',
|
|
69
|
-
'```',
|
|
70
|
-
body.replace(NONCE, '--receipt <receipt>'),
|
|
71
|
-
'```',
|
|
72
|
-
'',
|
|
73
|
-
].join('\n')
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const SHEET = `${TREE}_generated/skills.ts`
|
|
77
|
-
|
|
78
|
-
function sheet(files: readonly string[]): string {
|
|
79
|
-
const rows = files.map((at) => {
|
|
80
|
-
const noun = at.slice(WHERE.length + 1, -'.md'.length)
|
|
81
|
-
return [
|
|
82
|
-
' {',
|
|
83
|
-
` id: 'skill-${noun}',`,
|
|
84
|
-
` at: '${at}',`,
|
|
85
|
-
` describes: 'What \`thread create ${noun}\` teaches: the ${noun}s this tree already has, with the sentence each one states, and the rules for declaring another. Written from what that command prints about itself, so the skill and the command cannot disagree.',`,
|
|
86
|
-
' version: 1,',
|
|
87
|
-
" by: 'thread skills',",
|
|
88
|
-
" runs: 'thread skills --check',",
|
|
89
|
-
' },',
|
|
90
|
-
].join('\n')
|
|
91
|
-
})
|
|
92
|
-
return [
|
|
93
|
-
"import type { Threaded } from '../../thread/core/thread.ts'",
|
|
94
|
-
'',
|
|
95
|
-
'export const SKILLS: Threaded = {',
|
|
96
|
-
" id: 'skills',",
|
|
97
|
-
` at: '${SHEET}',`,
|
|
98
|
-
" describes: 'One row per skill `thread skills` writes, so every `skills/<noun>.md` is declared by the command that wrote it instead of being refused as a file nothing named. It sits beside the files it names because the nouns are not known until the tree is read, and a list somebody keeps by hand would go stale the first time a package brings a new maker.',",
|
|
99
|
-
' version: 1,',
|
|
100
|
-
' derives: [',
|
|
101
|
-
...rows,
|
|
102
|
-
' ],',
|
|
103
|
-
'}',
|
|
104
|
-
'',
|
|
105
|
-
].join('\n')
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const HERE = (): string => cwd()
|
|
109
|
-
|
|
110
|
-
const COINS = /by:\s*'thread create ([a-z][a-z0-9-]*)'/g
|
|
111
|
-
|
|
112
|
-
const ANSWERS_AT = new RegExp(`at:\\s*'node_modules/${ORGANISATION}/[^/]+/(dist/[^']+)'`, 'g')
|
|
113
|
-
|
|
114
|
-
function ownMakers(root: string): ReadonlyMap<string, string> {
|
|
115
|
-
const found = new Map<string, string>()
|
|
116
|
-
|
|
117
|
-
let names: readonly string[]
|
|
118
|
-
try {
|
|
119
|
-
names = readdirSync(join(root, 'src'), { recursive: true }) as readonly string[]
|
|
120
|
-
} catch {
|
|
121
|
-
return found
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
for (const one of names) {
|
|
125
|
-
const path = String(one).split('\\').join('/')
|
|
126
|
-
if (!path.endsWith('attach.ts')) continue
|
|
127
|
-
|
|
128
|
-
let text: string
|
|
129
|
-
try {
|
|
130
|
-
text = readFileSync(join(root, 'src', String(one)), 'utf8')
|
|
131
|
-
} catch {
|
|
132
|
-
continue
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
for (const row of text.split(/\{\s*$/m)) {
|
|
136
|
-
const noun = [...row.matchAll(COINS)].at(0)?.[1]
|
|
137
|
-
const at = [...row.matchAll(ANSWERS_AT)].at(-1)?.[1]
|
|
138
|
-
if (noun === undefined || at === undefined || found.has(noun)) continue
|
|
139
|
-
found.set(noun, join(root, at))
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return found
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function printedOwn(at: string): { readonly refused: string; readonly body: string } {
|
|
147
|
-
const ran = spawnSync(execPath, ['--experimental-strip-types', at, '--own'], {
|
|
148
|
-
encoding: 'utf8',
|
|
149
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
150
|
-
})
|
|
151
|
-
const body = String(ran.stdout ?? '').replace(CRLF, '\n').replace(RECEIPT_LINE, '').trimEnd()
|
|
152
|
-
const said = String(ran.stderr ?? '').replace(CRLF, '\n').trim().split('\n')[0] ?? ''
|
|
153
|
-
return { refused: ran.status === 0 && body !== '' ? '' : said || 'it printed nothing', body }
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async function run(): Promise<number> {
|
|
157
|
-
const own = argv.includes('--own')
|
|
158
|
-
const checking = argv.includes('--check')
|
|
159
|
-
|
|
160
|
-
const root = own ? HERE() : (findTree() as Tree).at
|
|
161
|
-
|
|
162
|
-
const mine = own ? ownMakers(root) : new Map<string, string>()
|
|
163
|
-
const theirs = own ? new Map<string, string>() : await offering(findTree() as Tree)
|
|
164
|
-
const known = [...(own ? mine : theirs).keys()].sort()
|
|
165
|
-
|
|
166
|
-
if (known.length === 0) {
|
|
167
|
-
stdout.write(
|
|
168
|
-
own
|
|
169
|
-
? 'skills: this package ships no maker, so it coins no noun and has no skill to write.\n'
|
|
170
|
-
: 'skills: this tree declares no `create` command yet, so there is no method to write down. ' +
|
|
171
|
-
`Attach a package that brings one — \`npx thread attach ${named('ui')}\` — and run this again.\n`,
|
|
172
|
-
)
|
|
173
|
-
return 0
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const wanted = new Map<string, string>()
|
|
177
|
-
const skipped: string[] = []
|
|
178
|
-
for (const noun of known) {
|
|
179
|
-
const { refused, body } = own ? printedOwn(mine.get(noun) ?? '') : printed(root, theirs.get(noun) ?? '', noun)
|
|
180
|
-
if (refused !== '') {
|
|
181
|
-
skipped.push(` ${noun} — ${refused}`)
|
|
182
|
-
continue
|
|
183
|
-
}
|
|
184
|
-
wanted.set(fileFor(noun), skill(noun, body))
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (wanted.size === 0) {
|
|
188
|
-
stdout.write(
|
|
189
|
-
`skills: every \`create\` this tree declares refused, so there is no method to write down yet:\n${skipped.join('\n')}\n`,
|
|
190
|
-
)
|
|
191
|
-
return 1
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const made = wanted.size
|
|
195
|
-
const sheetAt = own ? '' : SHEET
|
|
196
|
-
if (sheetAt !== '') wanted.set(sheetAt, sheet([...wanted.keys()].sort()))
|
|
197
|
-
|
|
198
|
-
const held = new Map<string, string>()
|
|
199
|
-
try {
|
|
200
|
-
for (const name of readdirSync(join(root,WHERE))) {
|
|
201
|
-
if (!name.endsWith('.md')) continue
|
|
202
|
-
held.set(join(WHERE, name), readFileSync(join(root,WHERE, name), 'utf8').replace(CRLF, '\n'))
|
|
203
|
-
}
|
|
204
|
-
} catch {
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (sheetAt !== '') {
|
|
209
|
-
try {
|
|
210
|
-
held.set(sheetAt, readFileSync(join(root, sheetAt), 'utf8').replace(CRLF, '\n'))
|
|
211
|
-
} catch {
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const stale = [...held.keys()].filter((one) => !wanted.has(one)).sort()
|
|
217
|
-
const differs = [...wanted.entries()].filter(([one, text]) => held.get(one) !== text).map(([one]) => one).sort()
|
|
218
|
-
|
|
219
|
-
if (checking) {
|
|
220
|
-
if (stale.length === 0 && differs.length === 0) {
|
|
221
|
-
stdout.write(
|
|
222
|
-
`skills: ${String(made)} skill(s), each matching what its own command prints` +
|
|
223
|
-
(sheetAt === '' ? '' : `, and each declared by a row in ${sheetAt}`) +
|
|
224
|
-
'.\n',
|
|
225
|
-
)
|
|
226
|
-
return 0
|
|
227
|
-
}
|
|
228
|
-
for (const one of differs) {
|
|
229
|
-
stdout.write(
|
|
230
|
-
`skills: ${one} is not what \`npx thread skills\` writes. A skill is derived from the command ` +
|
|
231
|
-
'it describes, so an edit here is a second statement of the method and will drift from the ' +
|
|
232
|
-
'first. Change the command, then run `npx thread skills`.\n',
|
|
233
|
-
)
|
|
234
|
-
}
|
|
235
|
-
for (const one of stale) {
|
|
236
|
-
stdout.write(
|
|
237
|
-
`skills: ${one} names a \`create\` this tree no longer declares, so it teaches a command that ` +
|
|
238
|
-
'will refuse. Run `npx thread skills`.\n',
|
|
239
|
-
)
|
|
240
|
-
}
|
|
241
|
-
return 1
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
for (const [one, text] of wanted) {
|
|
245
|
-
mkdirSync(dirname(join(root,one)), { recursive: true })
|
|
246
|
-
writeFileSync(join(root,one), text, 'utf8')
|
|
247
|
-
}
|
|
248
|
-
for (const one of stale) rmSync(join(root,one), { force: true })
|
|
249
|
-
|
|
250
|
-
stdout.write(
|
|
251
|
-
`skills: wrote ${String(made)} skill(s) into ${WHERE}/` +
|
|
252
|
-
(sheetAt === '' ? '' : `, and the ${String(made)} row(s) declaring them into ${sheetAt}`) +
|
|
253
|
-
(stale.length === 0
|
|
254
|
-
? ''
|
|
255
|
-
: `, and removed ${String(stale.length)} naming a command this tree no longer has`) +
|
|
256
|
-
'.\n' +
|
|
257
|
-
|
|
258
|
-
(skipped.length === 0 ? '' : `skills: no method to keep yet for:\n${skipped.join('\n')}\n`),
|
|
259
|
-
)
|
|
260
|
-
return 0
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (invoked(import.meta.url)) exit(await run())
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
-
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
return path;
|
|
8
|
-
};
|
|
9
|
-
import { spawnSync } from 'node:child_process';
|
|
10
|
-
import { createHash, randomBytes } from 'node:crypto';
|
|
11
|
-
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
|
-
import { tmpdir } from 'node:os';
|
|
13
|
-
import { join, relative } from 'node:path';
|
|
14
|
-
import { argv, stderr, stdin, stdout } from 'node:process';
|
|
15
|
-
import { createInterface } from 'node:readline';
|
|
16
|
-
import { exit } from "../../core/invoked.js";
|
|
17
|
-
import { ELSEWHERE, wordsIn } from "../../core/asking.js";
|
|
18
|
-
import { generators, settling } from "../../core/chain.js";
|
|
19
|
-
import { holdsRow, listBounds, withRow } from "../../core/rows.js";
|
|
20
|
-
import { findTree, inTree } from "../../core/tree.js";
|
|
21
|
-
import { PUBLIC_ROLE as PUBLIC } from "../../core/actor.js";
|
|
22
|
-
import { ACTOR_FACTS, AGGREGATIONS, COMPARE_OPS, conditioned, said } from "../../core/condition.js";
|
|
23
|
-
import { declaredWords, undeclared } from "../access.js";
|
|
24
|
-
import { descend } from "../../core/descend.js";
|
|
25
|
-
import { operations as woven, types } from "./seed.js";
|
|
26
|
-
const TREE = findTree();
|
|
27
|
-
const FILE = 'access.ts';
|
|
28
|
-
const LIST = 'GRANTS';
|
|
29
|
-
const RECEIPTS = join(tmpdir(), 'thread-grant');
|
|
30
|
-
const RECEIPT = '--receipt';
|
|
31
|
-
const KEBAB = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
32
|
-
const here = (path) => relative(TREE.at, path).split('\\').join('/');
|
|
33
|
-
const at = here(inTree(TREE, FILE));
|
|
34
|
-
function refuse(because) {
|
|
35
|
-
stderr.write(`grant: ${because}\n`);
|
|
36
|
-
exit(1);
|
|
37
|
-
}
|
|
38
|
-
const kebab = (one) => one.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
39
|
-
import { quoted } from "../../core/rows.js";
|
|
40
|
-
async function taken(file, name) {
|
|
41
|
-
try {
|
|
42
|
-
const held = (await import(__rewriteRelativeImportExtension(new URL(`file://${inTree(TREE, file)}`).href)));
|
|
43
|
-
const list = held[name];
|
|
44
|
-
return Array.isArray(list) ? list : [];
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
return [];
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
const entities = await taken('entities.ts', 'ENTITIES');
|
|
51
|
-
const declares = (which) => declaredWords(entities, which, PUBLIC);
|
|
52
|
-
const pass = await descend();
|
|
53
|
-
const operations = [...woven(pass, types(pass))].map(([id]) => ({
|
|
54
|
-
id,
|
|
55
|
-
describes: pass.stated.get(id)?.describes ?? '',
|
|
56
|
-
}));
|
|
57
|
-
const grants = await taken(FILE, LIST);
|
|
58
|
-
if (operations.length === 0) {
|
|
59
|
-
refuse('this app declares no operations, and a grant permits one. Declare what it can be asked to do first — `thread create operation`.');
|
|
60
|
-
}
|
|
61
|
-
const mark = (one) => createHash('sha256').update([one.id, one.on ?? '', one.may.join(','), JSON.stringify(one.to ?? {}), JSON.stringify(one.when ?? {})].join('\n')).digest('hex').slice(0, 6);
|
|
62
|
-
const listing = [
|
|
63
|
-
entities.length === 0
|
|
64
|
-
? 'this app keeps nothing, so a grant here names operations and callers and no entity.'
|
|
65
|
-
: `${String(entities.length)} entity(s) a grant may be written on:`,
|
|
66
|
-
...entities.map((one) => ` ${one.id} — ${[...one.holds.map((each) => each.id), ...(one.links ?? []).map((each) => each.id)].join(', ')}`),
|
|
67
|
-
'',
|
|
68
|
-
grants.length === 0
|
|
69
|
-
? 'no grants yet, so nothing permits any operation and every caller is refused.'
|
|
70
|
-
: `${String(grants.length)} grant(s):`,
|
|
71
|
-
...grants.map((one) => ` [${mark(one)}] ${one.id} — may ${one.may.join(', ')}${one.on === undefined ? '' : ` on ${one.on}`} — ${one.describes}`),
|
|
72
|
-
].join('\n');
|
|
73
|
-
const words = wordsIn(['new']);
|
|
74
|
-
stdout.write(`${listing}\n`);
|
|
75
|
-
const tree = createHash('sha256').update(TREE.at).digest('hex').slice(0, 16);
|
|
76
|
-
const against = createHash('sha256').update(listing).digest('hex').slice(0, 12);
|
|
77
|
-
const spends = (nonce) => join(RECEIPTS, `${tree}-${nonce}.txt`);
|
|
78
|
-
const OPERANDS = [
|
|
79
|
-
{ word: 'field', means: 'a value or link on the row being judged, by its id' },
|
|
80
|
-
{ word: 'value', means: 'a literal — text, a number, true, false, or null' },
|
|
81
|
-
{ word: 'actor', means: `something about who is asking: ${ACTOR_FACTS.join(', ')}` },
|
|
82
|
-
{ word: 'now', means: 'the moment the row is being touched' },
|
|
83
|
-
{ word: 'ago', means: 'a span before now, written like 30d, 12h, 15m, 45s' },
|
|
84
|
-
{ word: 'agg', means: `a count or total over another entity: ${AGGREGATIONS.join(', ')}` },
|
|
85
|
-
];
|
|
86
|
-
function method(nonce) {
|
|
87
|
-
return [
|
|
88
|
-
'what this makes:',
|
|
89
|
-
'',
|
|
90
|
-
` thread grant new <name> ${RECEIPT} ${nonce}`,
|
|
91
|
-
'',
|
|
92
|
-
...ELSEWHERE,
|
|
93
|
-
'',
|
|
94
|
-
`${RECEIPT} is spent by one run and refused if ${at} moved after it was issued.`,
|
|
95
|
-
'',
|
|
96
|
-
'what `new` asks:',
|
|
97
|
-
'',
|
|
98
|
-
' name: kebab-case, and it names the permission rather than the row',
|
|
99
|
-
' what does it permit? one sentence',
|
|
100
|
-
...(entities.length === 0
|
|
101
|
-
? []
|
|
102
|
-
: [` which entity (blank for none): ${entities.map((one) => one.id).join(', ')}`]),
|
|
103
|
-
` which operations (comma separated): ${operations.map((one) => one.id).join(', ')}`,
|
|
104
|
-
` which roles: ${declares('roles').join(', ')}`,
|
|
105
|
-
` ${PUBLIC} is everybody — the role an unauthenticated caller`,
|
|
106
|
-
' already wears. The rest are the options of the value an',
|
|
107
|
-
' entity names as saying what a caller may do, so a role',
|
|
108
|
-
' nothing declares is refused here rather than written',
|
|
109
|
-
` which entitlements (blank none): ${declares('entitlements').join(', ') || 'none are declared'}`,
|
|
110
|
-
'',
|
|
111
|
-
'then the condition, one step at a time. Nothing is pasted — each answer is offered from what',
|
|
112
|
-
'the step before it allows, so a condition that cannot be written cannot be typed:',
|
|
113
|
-
'',
|
|
114
|
-
' is: pick a side, below',
|
|
115
|
-
` op: ${COMPARE_OPS.join(', ')}`,
|
|
116
|
-
' than: pick the other side',
|
|
117
|
-
' and / or / done: join another comparison, or stop',
|
|
118
|
-
'',
|
|
119
|
-
'what a side may be:',
|
|
120
|
-
'',
|
|
121
|
-
...OPERANDS.map((one) => ` ${one.word.padEnd(8)} ${one.means}`),
|
|
122
|
-
'',
|
|
123
|
-
'A grant naming no roles, no entitlements and no condition reading the actor is refused when the',
|
|
124
|
-
'gate is written — it would admit every caller including one nobody authenticated. So one of',
|
|
125
|
-
'roles, entitlements, or an `actor` condition is required here.',
|
|
126
|
-
].join('\n');
|
|
127
|
-
}
|
|
128
|
-
const flagged = argv.indexOf(RECEIPT);
|
|
129
|
-
if (flagged === -1) {
|
|
130
|
-
const nonce = randomBytes(6).toString('hex');
|
|
131
|
-
mkdirSync(RECEIPTS, { recursive: true });
|
|
132
|
-
writeFileSync(spends(nonce), `${against}\n`, 'utf8');
|
|
133
|
-
stdout.write(`\n${method(nonce)}\n`);
|
|
134
|
-
stdout.write(`\ngrant: receipt ${nonce} — spent on use, and stale the moment ${at} changes.\n`);
|
|
135
|
-
stdout.write(`grant: thread grant new <name> ${RECEIPT} ${nonce}\n`);
|
|
136
|
-
exit(0);
|
|
137
|
-
}
|
|
138
|
-
const offered = argv[flagged + 1];
|
|
139
|
-
if (offered === undefined || offered.startsWith('-')) {
|
|
140
|
-
refuse(`\`${RECEIPT}\` was handed nothing to check. Run \`thread grant\` with no arguments.`);
|
|
141
|
-
}
|
|
142
|
-
let when = '';
|
|
143
|
-
try {
|
|
144
|
-
when = readFileSync(spends(offered), 'utf8').trim();
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
refuse(`\`${offered}\` is not a receipt this tree issued, or it has been spent — one run per receipt.`);
|
|
148
|
-
}
|
|
149
|
-
rmSync(spends(offered), { force: true });
|
|
150
|
-
if (when !== against)
|
|
151
|
-
refuse(`${at} has changed since that receipt was issued. Run \`thread grant\` again.`);
|
|
152
|
-
const rl = createInterface({ input: stdin, output: stdout });
|
|
153
|
-
const lines = rl[Symbol.asyncIterator]();
|
|
154
|
-
const ask = async (question) => {
|
|
155
|
-
stdout.write(question);
|
|
156
|
-
const next = await lines.next();
|
|
157
|
-
if (next.done === true) {
|
|
158
|
-
refuse(`the input ended while this was still asking "${question.trim()}". A grant is not written from ` +
|
|
159
|
-
'half an answer, so nothing was written. Answer every line the listing names, or run it ' +
|
|
160
|
-
'without a pipe and answer as you go.');
|
|
161
|
-
}
|
|
162
|
-
return next.value.trim();
|
|
163
|
-
};
|
|
164
|
-
const withLinks = (one) => ({
|
|
165
|
-
id: one.id,
|
|
166
|
-
fields: [...one.holds.map((each) => each.id), ...(one.links ?? []).map((each) => each.id)],
|
|
167
|
-
});
|
|
168
|
-
const holdsOnly = (one) => ({
|
|
169
|
-
id: one.id,
|
|
170
|
-
fields: one.holds.map((each) => each.id),
|
|
171
|
-
});
|
|
172
|
-
const say = (line) => {
|
|
173
|
-
stdout.write(line);
|
|
174
|
-
};
|
|
175
|
-
const name = kebab(words[1] ?? (await ask('name: ')));
|
|
176
|
-
if (name === '' || !KEBAB.test(name)) {
|
|
177
|
-
rl.close();
|
|
178
|
-
refuse(`"${name}" cannot be the id of a grant — it is kebab-case, e.g. owner-reads-own-invoice`);
|
|
179
|
-
}
|
|
180
|
-
if (grants.some((one) => one.id === name)) {
|
|
181
|
-
rl.close();
|
|
182
|
-
refuse(`\`${name}\` is already granted in ${at}. One id, one policy — two would silently keep whichever was written second.`);
|
|
183
|
-
}
|
|
184
|
-
const describes = await ask('what does it permit? ');
|
|
185
|
-
if (describes === '') {
|
|
186
|
-
rl.close();
|
|
187
|
-
refuse(`\`${name}\` was given no meaning, and a row without one is a name nobody can read`);
|
|
188
|
-
}
|
|
189
|
-
const on = entities.length === 0 ? '' : await ask(`which entity (blank if it acts on none) — ${entities.map((one) => one.id).join(', ')}: `);
|
|
190
|
-
const entity = on === '' ? undefined : entities.find((one) => one.id === on);
|
|
191
|
-
if (on !== '' && entity === undefined) {
|
|
192
|
-
rl.close();
|
|
193
|
-
refuse(`nothing declares \`${on}\`, and a grant on an entity that does not exist enforces nothing`);
|
|
194
|
-
}
|
|
195
|
-
const may = (await ask(`which operations (comma separated) — ${operations.map((one) => one.id).join(', ')}: `))
|
|
196
|
-
.split(',')
|
|
197
|
-
.map((one) => one.trim())
|
|
198
|
-
.filter((one) => one !== '');
|
|
199
|
-
const stray = may.filter((one) => !operations.some((each) => each.id === one));
|
|
200
|
-
if (may.length === 0 || stray.length > 0) {
|
|
201
|
-
rl.close();
|
|
202
|
-
refuse(stray.length > 0
|
|
203
|
-
? `this app declares no operation called ${stray.join(', ')} — it declares ${operations.map((one) => one.id).join(', ')}`
|
|
204
|
-
: 'a grant permits at least one operation, or it permits nothing and may as well not be written');
|
|
205
|
-
}
|
|
206
|
-
const named = (which, answered) => {
|
|
207
|
-
const wrong = undeclared(answered, declares(which), which);
|
|
208
|
-
if (wrong !== undefined)
|
|
209
|
-
refuse(wrong);
|
|
210
|
-
return answered.split(',').map((one) => one.trim()).filter((one) => one !== '');
|
|
211
|
-
};
|
|
212
|
-
const listed = declares('roles');
|
|
213
|
-
const granted = declares('entitlements');
|
|
214
|
-
const roles = named('roles', await ask(`which roles (comma separated) — ${listed.join(', ')}: `));
|
|
215
|
-
const entitlements = named('entitlements', await ask(`which entitlements (blank for none)${granted.length === 0 ? '' : ` — ${granted.join(', ')}`}: `));
|
|
216
|
-
const condition = entity === undefined
|
|
217
|
-
? undefined
|
|
218
|
-
: await conditioned((_key, question) => ask(question), say, withLinks(entity), entities.map(holdsOnly));
|
|
219
|
-
rl.close();
|
|
220
|
-
const to = roles.length === 0 && entitlements.length === 0
|
|
221
|
-
? undefined
|
|
222
|
-
: { ...(roles.length === 0 ? {} : { roles }), ...(entitlements.length === 0 ? {} : { entitlements }) };
|
|
223
|
-
const row = [
|
|
224
|
-
' {',
|
|
225
|
-
` id: ${quoted(name)},`,
|
|
226
|
-
` at: ${quoted(at)},`,
|
|
227
|
-
` describes: ${quoted(describes)},`,
|
|
228
|
-
' version: 1,',
|
|
229
|
-
` may: [${may.map((one) => quoted(one)).join(', ')}],`,
|
|
230
|
-
...(on === '' ? [] : [` on: ${quoted(on)},`]),
|
|
231
|
-
...(to === undefined ? [] : [` to: ${said(to)},`]),
|
|
232
|
-
...(condition === undefined ? [] : [` when: ${said(condition)},`]),
|
|
233
|
-
' },',
|
|
234
|
-
].join('\n');
|
|
235
|
-
const path = inTree(TREE, FILE);
|
|
236
|
-
const text = readFileSync(path, 'utf8');
|
|
237
|
-
const where = listBounds(text, LIST);
|
|
238
|
-
if (where === undefined)
|
|
239
|
-
refuse(`${at} states no \`${LIST}\` list to grant in`);
|
|
240
|
-
if (holdsRow(text, where, name))
|
|
241
|
-
refuse(`\`${name}\` is already granted in ${at}`);
|
|
242
|
-
writeFileSync(path, withRow(text, where, row), 'utf8');
|
|
243
|
-
stdout.write(`grant: ${name} may ${may.join(', ')}${on === '' ? '' : ` on ${on}`}, written into ${at}.\n`);
|
|
244
|
-
const settled = settling(TREE.at, await generators(TREE), ['thread grant']);
|
|
245
|
-
for (const one of settled.ran)
|
|
246
|
-
stdout.write(`${one}\n`);
|
|
247
|
-
const refused = settled.unsettled.filter((one) => one.includes('permits'));
|
|
248
|
-
if (refused.length > 0) {
|
|
249
|
-
writeFileSync(path, text, 'utf8');
|
|
250
|
-
stderr.write(`grant: \`${name}\` was taken back out of ${at}, because ${refused.join(', ')} would not settle ` +
|
|
251
|
-
`after ${String(settled.rounds)} round(s) — the refusal above says what it wanted instead. Nothing else moved.\n`);
|
|
252
|
-
exit(1);
|
|
253
|
-
}
|
|
254
|
-
if (settled.unsettled.length > 0) {
|
|
255
|
-
stderr.write(`grant: \`${name}\` is written and stands — the gate generator accepted it. What did not settle is ` +
|
|
256
|
-
`${settled.unsettled.join(', ')}, which is about the rest of this tree rather than about this row. ` +
|
|
257
|
-
'Run it yourself and read what it says.\n');
|
|
258
|
-
}
|
|
259
|
-
const records = spawnSync('npx', ['thread'], { cwd: TREE.at, encoding: 'utf8' });
|
|
260
|
-
if (`${records.stdout ?? ''}`.includes('written')) {
|
|
261
|
-
const kept = spawnSync('npx', ['thread', 'written'], { cwd: TREE.at, encoding: 'utf8' });
|
|
262
|
-
if (kept.status !== 0) {
|
|
263
|
-
stdout.write(`${`${kept.stdout ?? ''}${kept.stderr ?? ''}`.trim()}\n`);
|
|
264
|
-
stderr.write("grant: the row is written and stands; recording it as the method's was refused above.\n");
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
exit(0);
|