@prestack/thread 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/actor.ts +20 -0
- package/dist/core/asking.d.ts +0 -1
- package/dist/core/asking.js +0 -1
- package/dist/core/asking.ts +1342 -0
- package/dist/core/attach.js +2 -2
- package/dist/core/attach.ts +585 -0
- package/dist/core/chain.js +8 -1
- package/dist/core/chain.ts +148 -0
- package/dist/core/cli.js +97 -18
- package/dist/core/cli.ts +2004 -0
- package/dist/core/condition.ts +227 -0
- package/dist/core/descend.d.ts +1 -0
- package/dist/core/descend.js +36 -0
- package/dist/core/descend.ts +1001 -0
- package/dist/core/finding.ts +38 -0
- package/dist/core/generators/meta.js +6 -0
- package/dist/core/generators/meta.ts +1006 -0
- package/dist/core/init.ts +170 -0
- package/dist/core/invoked.ts +47 -0
- package/dist/core/round.ts +57 -0
- package/dist/core/rows.ts +369 -0
- package/dist/core/scope.ts +18 -0
- package/dist/core/thread.ts +29 -0
- package/dist/core/tree.ts +67 -0
- package/dist/core/types.ts +350 -0
- package/dist/derivation/attach.js +1 -1
- package/dist/derivation/attach.ts +68 -0
- package/dist/derivation/generators/build.ts +232 -0
- package/dist/derivation/generators/check.ts +118 -0
- package/dist/derivation/generators/derivation.ts +347 -0
- package/dist/derivation/generators/determinism.ts +145 -0
- package/dist/derivation/generators/release.js +19 -9
- package/dist/derivation/generators/release.ts +213 -0
- package/dist/derivation/makers/release.ts +135 -0
- package/dist/derivation/release.ts +23 -0
- package/dist/integrations/attach.js +2 -2
- package/dist/integrations/attach.ts +207 -0
- package/dist/integrations/generators/read.ts +127 -0
- package/dist/integrations/generators/secret.ts +289 -0
- package/dist/integrations/generators/target.js +16 -6
- package/dist/integrations/generators/target.ts +944 -0
- package/dist/integrations/generators/vault.js +46 -14
- package/dist/integrations/generators/vault.ts +212 -0
- package/dist/integrations/{sdk.js → module.js} +4 -4
- package/dist/integrations/module.ts +189 -0
- package/dist/integrations/secret.ts +50 -0
- package/dist/integrations/target.d.ts +3 -1
- package/dist/integrations/target.js +14 -11
- package/dist/integrations/target.ts +340 -0
- package/dist/integrations/vault.d.ts +2 -0
- package/dist/integrations/vault.js +4 -2
- package/dist/integrations/vault.ts +136 -0
- package/dist/integrity/attach.ts +234 -0
- package/dist/integrity/check-threads.js +2 -26
- package/dist/integrity/check-threads.ts +166 -0
- package/dist/integrity/constraint.ts +1202 -0
- package/dist/integrity/generators/compiler.ts +161 -0
- package/dist/integrity/generators/gate.ts +201 -0
- package/dist/integrity/generators/issues.ts +160 -0
- package/dist/integrity/generators/publish.js +14 -5
- package/dist/integrity/generators/publish.ts +247 -0
- package/dist/integrity/generators/rule.ts +271 -0
- package/dist/integrity/generators/seal.ts +117 -0
- package/dist/integrity/generators/trial.ts +320 -0
- package/dist/integrity/lint.ts +227 -0
- package/dist/integrity/lsp/bin.ts +42 -0
- package/dist/integrity/lsp/diagnostics.ts +92 -0
- package/dist/integrity/lsp/findings.ts +63 -0
- package/dist/integrity/lsp/lint.ts +140 -0
- package/dist/integrity/lsp/protocol.ts +78 -0
- package/dist/integrity/lsp/server.ts +224 -0
- package/dist/integrity/sealed.js +2 -2
- package/dist/integrity/sealed.ts +56 -0
- package/dist/integrity/slopnator.ts +350 -0
- package/dist/integrity/slopnators/sl_broken_lineage.ts +36 -0
- package/dist/integrity/slopnators/sl_declared_elsewhere.ts +39 -0
- package/dist/integrity/slopnators/sl_derived_drift.ts +98 -0
- package/dist/integrity/slopnators/sl_hardcoded_style.ts +55 -0
- package/dist/integrity/slopnators/sl_inert_island.ts +91 -0
- package/dist/integrity/slopnators/sl_inert_slopnator.ts +93 -0
- package/dist/integrity/slopnators/sl_missing_row.ts +182 -0
- package/dist/integrity/slopnators/sl_no_barrel.ts +48 -0
- package/dist/integrity/slopnators/sl_no_child_process.ts +74 -0
- package/dist/integrity/slopnators/sl_no_comment.ts +43 -0
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +328 -0
- package/dist/integrity/slopnators/sl_no_import_cycle.ts +50 -0
- package/dist/integrity/slopnators/sl_no_such_command.ts +64 -0
- package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +63 -0
- package/dist/integrity/slopnators/sl_secret_literal.ts +100 -0
- package/dist/integrity/slopnators/sl_stale_path.ts +94 -0
- package/dist/integrity/slopnators/sl_twice_meant.ts +31 -0
- package/dist/integrity/slopnators/sl_twice_parented.ts +37 -0
- package/dist/integrity/slopnators/sl_undeclared_crossing.ts +87 -0
- package/dist/integrity/slopnators/sl_undeclared_destination.ts +65 -0
- package/dist/integrity/slopnators/sl_undeclared_element.ts +44 -0
- package/dist/integrity/slopnators/sl_undeclared_node.ts +69 -0
- package/dist/integrity/slopnators/sl_undeclared_operation.ts +147 -0
- package/dist/integrity/slopnators/sl_undeclared_variant.ts +148 -0
- package/dist/integrity/slopnators/sl_undescribed.ts +65 -0
- package/dist/integrity/slopnators/sl_ungoverned_island.ts +61 -0
- package/dist/integrity/slopnators/sl_unkept_rows.ts +111 -0
- package/dist/integrity/slopnators/sl_unnamed_island.ts +41 -0
- package/dist/integrity/slopnators/sl_unpaired_member.ts +131 -0
- package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +58 -0
- package/dist/integrity/slopnators/sl_unprefixed_member.ts +136 -0
- package/dist/integrity/slopnators/sl_unseamed_reach.ts +79 -0
- package/dist/integrity/slopnators/sl_unthreaded.ts +419 -0
- package/dist/integrity/slopnators/sl_untranslated_key.ts +164 -0
- package/dist/integrity/slopnators/sl_unversioned.ts +36 -0
- package/dist/integrity/sweep.ts +307 -0
- package/dist/knowledge/attach.ts +350 -0
- package/dist/knowledge/generators/docsgen.ts +174 -0
- package/dist/knowledge/generators/glossary.ts +73 -0
- package/dist/knowledge/generators/skills.ts +263 -0
- package/dist/knowledge/glossary.ts +6 -0
- package/dist/operations/access.ts +88 -0
- package/dist/operations/attach.ts +475 -0
- package/dist/operations/capabilities.ts +65 -0
- package/dist/operations/cost.ts +58 -0
- package/dist/operations/dispatch.ts +348 -0
- package/dist/operations/error-results.ts +31 -0
- package/dist/operations/errors/catalog.ts +168 -0
- package/dist/operations/errors/classes.ts +51 -0
- package/dist/operations/errors/errors.ts +314 -0
- package/dist/operations/generators/capability.js +29 -19
- package/dist/operations/generators/capability.ts +704 -0
- package/dist/operations/generators/compile.js +20 -1
- package/dist/operations/generators/compile.ts +643 -0
- package/dist/operations/generators/grant.ts +336 -0
- package/dist/operations/generators/operation.ts +972 -0
- package/dist/operations/generators/permits.ts +223 -0
- package/dist/operations/generators/seed.ts +347 -0
- package/dist/operations/generators/trial.js +4 -1
- package/dist/operations/generators/trial.ts +529 -0
- package/dist/operations/http.ts +164 -0
- package/dist/operations/makers/capability.ts +166 -0
- package/dist/operations/makers/trial.ts +520 -0
- package/dist/operations/memory.ts +470 -0
- package/dist/operations/metering.ts +328 -0
- package/dist/operations/nodes.ts +54 -0
- package/dist/operations/operations.ts +32 -0
- package/dist/operations/queue-policy.ts +10 -0
- package/dist/operations/result.ts +28 -0
- package/dist/operations/seams.d.ts +4 -4
- package/dist/operations/seams.ts +364 -0
- package/dist/operations/store.ts +29 -0
- package/package.json +8 -1
- package/dist/integrations/generators/sdk.d.ts +0 -1
- package/dist/integrations/generators/sdk.js +0 -148
- /package/dist/integrations/{sdk.d.ts → module.d.ts} +0 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
3
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join, relative } from 'node:path'
|
|
6
|
+
import { argv, stderr, stdin, stdout } from 'node:process'
|
|
7
|
+
import { createInterface } from 'node:readline'
|
|
8
|
+
|
|
9
|
+
import { exit } from '../../core/invoked.ts'
|
|
10
|
+
import { ELSEWHERE, wordsIn } from '../../core/asking.ts'
|
|
11
|
+
import { generators, settling } from '../../core/chain.ts'
|
|
12
|
+
import { holdsRow, listBounds, showing, withRow } from '../../core/rows.ts'
|
|
13
|
+
import { findTree, inTree } from '../../core/tree.ts'
|
|
14
|
+
|
|
15
|
+
import { PUBLIC_ROLE as PUBLIC } from '../../core/actor.ts'
|
|
16
|
+
import { ACTOR_FACTS, AGGREGATIONS, COMPARE_OPS, conditioned, said } from '../../core/condition.ts'
|
|
17
|
+
|
|
18
|
+
import type { Grant } from '../access.ts'
|
|
19
|
+
import { declaredWords, undeclared } from '../access.ts'
|
|
20
|
+
import { descend } from '../../core/descend.ts'
|
|
21
|
+
|
|
22
|
+
import { operations as woven, types } from './seed.ts'
|
|
23
|
+
|
|
24
|
+
interface Kept {
|
|
25
|
+
readonly id: string
|
|
26
|
+
readonly describes: string
|
|
27
|
+
|
|
28
|
+
readonly holds: readonly { readonly id: string; readonly options?: readonly string[] }[]
|
|
29
|
+
readonly links?: readonly { readonly id: string; readonly to: string }[]
|
|
30
|
+
readonly actor?: { readonly roles: string; readonly entitlements?: string }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const TREE = findTree()
|
|
34
|
+
|
|
35
|
+
const FILE = 'access.ts'
|
|
36
|
+
|
|
37
|
+
const LIST = 'GRANTS'
|
|
38
|
+
|
|
39
|
+
const RECEIPTS = join(tmpdir(), 'thread-grant')
|
|
40
|
+
|
|
41
|
+
const RECEIPT = '--receipt'
|
|
42
|
+
|
|
43
|
+
const KEBAB = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/
|
|
44
|
+
|
|
45
|
+
const here = (path: string): string => relative(TREE.at, path).split('\\').join('/')
|
|
46
|
+
|
|
47
|
+
const at = here(inTree(TREE, FILE))
|
|
48
|
+
|
|
49
|
+
function refuse(because: string): never {
|
|
50
|
+
stderr.write(`grant: ${because}\n`)
|
|
51
|
+
exit(1)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const kebab = (one: string): string => one.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
|
|
55
|
+
|
|
56
|
+
import { quoted } from '../../core/rows.ts'
|
|
57
|
+
|
|
58
|
+
async function taken<T>(file: string, name: string): Promise<readonly T[]> {
|
|
59
|
+
try {
|
|
60
|
+
const held = (await import(new URL(`file://${inTree(TREE, file)}`).href)) as Record<string, unknown>
|
|
61
|
+
const list = held[name]
|
|
62
|
+
return Array.isArray(list) ? (list as readonly T[]) : []
|
|
63
|
+
} catch {
|
|
64
|
+
return []
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const entities = await taken<Kept>('entities.ts', 'ENTITIES')
|
|
69
|
+
|
|
70
|
+
const declares = (which: 'roles' | 'entitlements'): readonly string[] => declaredWords(entities, which, PUBLIC)
|
|
71
|
+
|
|
72
|
+
const pass = await descend()
|
|
73
|
+
const operations = [...woven(pass, types(pass))].map(([id]) => ({
|
|
74
|
+
id,
|
|
75
|
+
describes: pass.stated.get(id)?.describes ?? '',
|
|
76
|
+
}))
|
|
77
|
+
const grants = await taken<Grant>(FILE, LIST)
|
|
78
|
+
|
|
79
|
+
if (operations.length === 0) {
|
|
80
|
+
refuse('this app declares no operations, and a grant permits one. Declare what it can be asked to do first — `thread create operation`.')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const mark = (one: Grant): string =>
|
|
84
|
+
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)
|
|
85
|
+
|
|
86
|
+
const listing = [
|
|
87
|
+
entities.length === 0
|
|
88
|
+
? 'this app keeps nothing, so a grant here names operations and callers and no entity.'
|
|
89
|
+
: `${String(entities.length)} entity(s) a grant may be written on:`,
|
|
90
|
+
...entities.map(
|
|
91
|
+
(one) =>
|
|
92
|
+
` ${one.id} — ${[...one.holds.map((each) => each.id), ...(one.links ?? []).map((each) => each.id)].join(', ')}`,
|
|
93
|
+
),
|
|
94
|
+
'',
|
|
95
|
+
grants.length === 0
|
|
96
|
+
? 'no grants yet, so nothing permits any operation and every caller is refused.'
|
|
97
|
+
: `${String(grants.length)} grant(s):`,
|
|
98
|
+
|
|
99
|
+
...grants.map(
|
|
100
|
+
(one) =>
|
|
101
|
+
` [${mark(one)}] ${one.id} — may ${one.may.join(', ')}${one.on === undefined ? '' : ` on ${one.on}`} — ${one.describes}`,
|
|
102
|
+
),
|
|
103
|
+
].join('\n')
|
|
104
|
+
|
|
105
|
+
const words = wordsIn(['new'])
|
|
106
|
+
|
|
107
|
+
stdout.write(`${listing}\n`)
|
|
108
|
+
|
|
109
|
+
const tree = createHash('sha256').update(TREE.at).digest('hex').slice(0, 16)
|
|
110
|
+
const against = createHash('sha256').update(listing).digest('hex').slice(0, 12)
|
|
111
|
+
const spends = (nonce: string): string => join(RECEIPTS, `${tree}-${nonce}.txt`)
|
|
112
|
+
|
|
113
|
+
const OPERANDS: readonly { readonly word: string; readonly means: string }[] = [
|
|
114
|
+
{ word: 'field', means: 'a value or link on the row being judged, by its id' },
|
|
115
|
+
{ word: 'value', means: 'a literal — text, a number, true, false, or null' },
|
|
116
|
+
{ word: 'actor', means: `something about who is asking: ${ACTOR_FACTS.join(', ')}` },
|
|
117
|
+
{ word: 'now', means: 'the moment the row is being touched' },
|
|
118
|
+
{ word: 'ago', means: 'a span before now, written like 30d, 12h, 15m, 45s' },
|
|
119
|
+
{ word: 'agg', means: `a count or total over another entity: ${AGGREGATIONS.join(', ')}` },
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
function method(nonce: string): string {
|
|
123
|
+
return [
|
|
124
|
+
'what this makes:',
|
|
125
|
+
'',
|
|
126
|
+
` thread grant new <name> ${RECEIPT} ${nonce}`,
|
|
127
|
+
'',
|
|
128
|
+
...ELSEWHERE,
|
|
129
|
+
'',
|
|
130
|
+
`${RECEIPT} is spent by one run and refused if ${at} moved after it was issued.`,
|
|
131
|
+
'',
|
|
132
|
+
'what `new` asks:',
|
|
133
|
+
'',
|
|
134
|
+
' name: kebab-case, and it names the permission rather than the row',
|
|
135
|
+
' what does it permit? one sentence',
|
|
136
|
+
...(entities.length === 0
|
|
137
|
+
? []
|
|
138
|
+
: [` which entity (blank for none): ${entities.map((one) => one.id).join(', ')}`]),
|
|
139
|
+
` which operations (comma separated): ${operations.map((one) => one.id).join(', ')}`,
|
|
140
|
+
` which roles: ${declares('roles').join(', ')}`,
|
|
141
|
+
` ${PUBLIC} is everybody — the role an unauthenticated caller`,
|
|
142
|
+
' already wears. The rest are the options of the value an',
|
|
143
|
+
' entity names as saying what a caller may do, so a role',
|
|
144
|
+
' nothing declares is refused here rather than written',
|
|
145
|
+
` which entitlements (blank none): ${declares('entitlements').join(', ') || 'none are declared'}`,
|
|
146
|
+
'',
|
|
147
|
+
'then the condition, one step at a time. Nothing is pasted — each answer is offered from what',
|
|
148
|
+
'the step before it allows, so a condition that cannot be written cannot be typed:',
|
|
149
|
+
'',
|
|
150
|
+
' is: pick a side, below',
|
|
151
|
+
` op: ${COMPARE_OPS.join(', ')}`,
|
|
152
|
+
' than: pick the other side',
|
|
153
|
+
' and / or / done: join another comparison, or stop',
|
|
154
|
+
'',
|
|
155
|
+
'what a side may be:',
|
|
156
|
+
'',
|
|
157
|
+
...OPERANDS.map((one) => ` ${one.word.padEnd(8)} ${one.means}`),
|
|
158
|
+
'',
|
|
159
|
+
'A grant naming no roles, no entitlements and no condition reading the actor is refused when the',
|
|
160
|
+
'gate is written — it would admit every caller including one nobody authenticated. So one of',
|
|
161
|
+
'roles, entitlements, or an `actor` condition is required here.',
|
|
162
|
+
].join('\n')
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const flagged = argv.indexOf(RECEIPT)
|
|
166
|
+
|
|
167
|
+
if (flagged === -1) {
|
|
168
|
+
const nonce = randomBytes(6).toString('hex')
|
|
169
|
+
mkdirSync(RECEIPTS, { recursive: true })
|
|
170
|
+
writeFileSync(spends(nonce), `${against}\n`, 'utf8')
|
|
171
|
+
stdout.write(`\n${method(nonce)}\n`)
|
|
172
|
+
stdout.write(`\ngrant: receipt ${nonce} — spent on use, and stale the moment ${at} changes.\n`)
|
|
173
|
+
stdout.write(`grant: thread grant new <name> ${RECEIPT} ${nonce}\n`)
|
|
174
|
+
exit(0)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const offered = argv[flagged + 1]
|
|
178
|
+
if (offered === undefined || offered.startsWith('-')) {
|
|
179
|
+
refuse(`\`${RECEIPT}\` was handed nothing to check. Run \`thread grant\` with no arguments.`)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let when = ''
|
|
183
|
+
try {
|
|
184
|
+
when = readFileSync(spends(offered), 'utf8').trim()
|
|
185
|
+
} catch {
|
|
186
|
+
refuse(`\`${offered}\` is not a receipt this tree issued, or it has been spent — one run per receipt.`)
|
|
187
|
+
}
|
|
188
|
+
rmSync(spends(offered), { force: true })
|
|
189
|
+
if (when !== against) refuse(`${at} has changed since that receipt was issued. Run \`thread grant\` again.`)
|
|
190
|
+
|
|
191
|
+
const rl = createInterface({ input: stdin, output: stdout })
|
|
192
|
+
const lines = rl[Symbol.asyncIterator]()
|
|
193
|
+
|
|
194
|
+
const ask = async (question: string): Promise<string> => {
|
|
195
|
+
stdout.write(question)
|
|
196
|
+
const next = await lines.next()
|
|
197
|
+
if (next.done === true) {
|
|
198
|
+
refuse(
|
|
199
|
+
`the input ended while this was still asking "${question.trim()}". A grant is not written from ` +
|
|
200
|
+
'half an answer, so nothing was written. Answer every line the listing names, or run it ' +
|
|
201
|
+
'without a pipe and answer as you go.',
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
return next.value.trim()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const withLinks = (one: Kept): { readonly id: string; readonly fields: readonly string[] } => ({
|
|
208
|
+
id: one.id,
|
|
209
|
+
fields: [...one.holds.map((each) => each.id), ...(one.links ?? []).map((each) => each.id)],
|
|
210
|
+
})
|
|
211
|
+
const holdsOnly = (one: Kept): { readonly id: string; readonly fields: readonly string[] } => ({
|
|
212
|
+
id: one.id,
|
|
213
|
+
fields: one.holds.map((each) => each.id),
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
const say = (line: string): void => {
|
|
217
|
+
stdout.write(line)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const name = kebab(words[1] ?? (await ask('name: ')))
|
|
221
|
+
if (name === '' || !KEBAB.test(name)) {
|
|
222
|
+
rl.close()
|
|
223
|
+
refuse(`"${name}" cannot be the id of a grant — it is kebab-case, e.g. owner-reads-own-invoice`)
|
|
224
|
+
}
|
|
225
|
+
if (grants.some((one) => one.id === name)) {
|
|
226
|
+
rl.close()
|
|
227
|
+
refuse(`\`${name}\` is already granted in ${at}. One id, one policy — two would silently keep whichever was written second.`)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const describes = await ask('what does it permit? ')
|
|
231
|
+
if (describes === '') {
|
|
232
|
+
rl.close()
|
|
233
|
+
refuse(`\`${name}\` was given no meaning, and a row without one is a name nobody can read`)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const on = entities.length === 0 ? '' : await ask(`which entity (blank if it acts on none) — ${entities.map((one) => one.id).join(', ')}: `)
|
|
237
|
+
const entity = on === '' ? undefined : entities.find((one) => one.id === on)
|
|
238
|
+
if (on !== '' && entity === undefined) {
|
|
239
|
+
rl.close()
|
|
240
|
+
refuse(`nothing declares \`${on}\`, and a grant on an entity that does not exist enforces nothing`)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const may = (await ask(`which operations (comma separated) — ${operations.map((one) => one.id).join(', ')}: `))
|
|
244
|
+
.split(',')
|
|
245
|
+
.map((one) => one.trim())
|
|
246
|
+
.filter((one) => one !== '')
|
|
247
|
+
const stray = may.filter((one) => !operations.some((each) => each.id === one))
|
|
248
|
+
if (may.length === 0 || stray.length > 0) {
|
|
249
|
+
rl.close()
|
|
250
|
+
refuse(
|
|
251
|
+
stray.length > 0
|
|
252
|
+
? `this app declares no operation called ${stray.join(', ')} — it declares ${operations.map((one) => one.id).join(', ')}`
|
|
253
|
+
: 'a grant permits at least one operation, or it permits nothing and may as well not be written',
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const named = (which: 'roles' | 'entitlements', answered: string): readonly string[] => {
|
|
258
|
+
const wrong = undeclared(answered, declares(which), which)
|
|
259
|
+
if (wrong !== undefined) refuse(wrong)
|
|
260
|
+
return answered.split(',').map((one) => one.trim()).filter((one) => one !== '')
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const listed = declares('roles')
|
|
264
|
+
const granted = declares('entitlements')
|
|
265
|
+
|
|
266
|
+
const roles = named('roles', await ask(`which roles (comma separated) — ${listed.join(', ')}: `))
|
|
267
|
+
const entitlements = named(
|
|
268
|
+
'entitlements',
|
|
269
|
+
await ask(`which entitlements (blank for none)${granted.length === 0 ? '' : ` — ${granted.join(', ')}`}: `),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
const condition =
|
|
273
|
+
entity === undefined
|
|
274
|
+
? undefined
|
|
275
|
+
: await conditioned((_key, question) => ask(question), say, withLinks(entity), entities.map(holdsOnly))
|
|
276
|
+
rl.close()
|
|
277
|
+
|
|
278
|
+
const to =
|
|
279
|
+
roles.length === 0 && entitlements.length === 0
|
|
280
|
+
? undefined
|
|
281
|
+
: { ...(roles.length === 0 ? {} : { roles }), ...(entitlements.length === 0 ? {} : { entitlements }) }
|
|
282
|
+
|
|
283
|
+
const row = [
|
|
284
|
+
' {',
|
|
285
|
+
` id: ${quoted(name)},`,
|
|
286
|
+
` at: ${quoted(at)},`,
|
|
287
|
+
` describes: ${quoted(describes)},`,
|
|
288
|
+
' version: 1,',
|
|
289
|
+
` may: [${may.map((one) => quoted(one)).join(', ')}],`,
|
|
290
|
+
...(on === '' ? [] : [` on: ${quoted(on)},`]),
|
|
291
|
+
...(to === undefined ? [] : [` to: ${said(to)},`]),
|
|
292
|
+
...(condition === undefined ? [] : [` when: ${said(condition)},`]),
|
|
293
|
+
' },',
|
|
294
|
+
].join('\n')
|
|
295
|
+
|
|
296
|
+
const path = inTree(TREE, FILE)
|
|
297
|
+
const text = readFileSync(path, 'utf8')
|
|
298
|
+
const where = listBounds(text, LIST)
|
|
299
|
+
if (where === undefined) refuse(`${at} states no \`${LIST}\` list to grant in`)
|
|
300
|
+
if (holdsRow(text, where, name)) refuse(`\`${name}\` is already granted in ${at}`)
|
|
301
|
+
|
|
302
|
+
writeFileSync(path, withRow(text, where, row), 'utf8')
|
|
303
|
+
stdout.write(`grant: ${name} may ${may.join(', ')}${on === '' ? '' : ` on ${on}`}, written into ${at}.\n`)
|
|
304
|
+
|
|
305
|
+
const settled = settling(TREE.at, await generators(TREE), ['thread grant'])
|
|
306
|
+
for (const one of settled.ran) stdout.write(`${one}\n`)
|
|
307
|
+
|
|
308
|
+
const refused = settled.unsettled.filter((one) => one.includes('permits'))
|
|
309
|
+
|
|
310
|
+
if (refused.length > 0) {
|
|
311
|
+
writeFileSync(path, text, 'utf8')
|
|
312
|
+
stderr.write(
|
|
313
|
+
`grant: \`${name}\` was taken back out of ${at}, because ${refused.join(', ')} would not settle ` +
|
|
314
|
+
`after ${String(settled.rounds)} round(s) — the refusal above says what it wanted instead. Nothing else moved.\n`,
|
|
315
|
+
)
|
|
316
|
+
exit(1)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (settled.unsettled.length > 0) {
|
|
320
|
+
stderr.write(
|
|
321
|
+
`grant: \`${name}\` is written and stands — the gate generator accepted it. What did not settle is ` +
|
|
322
|
+
`${settled.unsettled.join(', ')}, which is about the rest of this tree rather than about this row. ` +
|
|
323
|
+
'Run it yourself and read what it says.\n',
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const records = spawnSync('npx', ['thread'], { cwd: TREE.at, encoding: 'utf8' })
|
|
328
|
+
if (`${records.stdout ?? ''}`.includes('written')) {
|
|
329
|
+
const kept = spawnSync('npx', ['thread', 'written'], { cwd: TREE.at, encoding: 'utf8' })
|
|
330
|
+
if (kept.status !== 0) {
|
|
331
|
+
stdout.write(`${`${kept.stdout ?? ''}${kept.stderr ?? ''}`.trim()}\n`)
|
|
332
|
+
stderr.write("grant: the row is written and stands; recording it as the method's was refused above.\n")
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
exit(0)
|