@prestack/thread 0.0.1 → 0.0.2
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,1006 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
import { argv, env, stdout } from 'node:process'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
|
|
6
|
+
import { exit } from '../invoked.ts'
|
|
7
|
+
import { BODY, RECEIPT, TYPED, answer, arrayValue, asBoolean, asFlag, asMap, asking, bodied, childrenIn, asksOf, declared, gathered, given as pipedIn, grows, guidance, issue, mapped, marked, named as answerNames, needed, nouns, only, ownAnswers, refuse, rowLines, seeding, spend, stating, VERBS, within, wordsIn, type Asked } from '../asking.ts'
|
|
8
|
+
import { generators } from '../chain.ts'
|
|
9
|
+
import { listFields, shapeFields, type Field } from '../descend.ts'
|
|
10
|
+
import {
|
|
11
|
+
STATED,
|
|
12
|
+
fieldIn,
|
|
13
|
+
holdsRow,
|
|
14
|
+
listBounds,
|
|
15
|
+
quoted,
|
|
16
|
+
reconciled,
|
|
17
|
+
rowsIn,
|
|
18
|
+
withField,
|
|
19
|
+
withRow,
|
|
20
|
+
withoutRow,
|
|
21
|
+
} from '../rows.ts'
|
|
22
|
+
import type { Threaded } from '../thread.ts'
|
|
23
|
+
import { findTree } from '../tree.ts'
|
|
24
|
+
|
|
25
|
+
const TREE = findTree()
|
|
26
|
+
|
|
27
|
+
const NOUN = env.THREAD_NOUN ?? 'meta'
|
|
28
|
+
|
|
29
|
+
const MINE = NOUN === 'meta' ? undefined : (await declared(TREE)).get(NOUN)
|
|
30
|
+
|
|
31
|
+
const FILES = MINE?.files
|
|
32
|
+
|
|
33
|
+
const LIST = MINE?.list ?? 'declares'
|
|
34
|
+
|
|
35
|
+
const SHAPE = MINE?.shape ?? 'Threaded'
|
|
36
|
+
|
|
37
|
+
const FIELDS: readonly Field[] = MINE?.fields ?? shapeFields(SHAPE).map((one) => ({ ...one, boolean: false }))
|
|
38
|
+
|
|
39
|
+
const ADDRESSED = FIELDS.some((one) => one.name === 'at')
|
|
40
|
+
|
|
41
|
+
export const ASKS: readonly Asked[] = asksOf(
|
|
42
|
+
NOUN,
|
|
43
|
+
FIELDS,
|
|
44
|
+
{
|
|
45
|
+
name: {
|
|
46
|
+
guide:
|
|
47
|
+
NOUN === 'meta'
|
|
48
|
+
? 'kebab-case, e.g. billing — this becomes its id, and its own sheet is scaffolded empty beside it, ready for whatever this app declares under it next'
|
|
49
|
+
: `kebab-case, e.g. billing — this becomes its id, and no other ${NOUN} in this tree may carry it`,
|
|
50
|
+
asks: 'name: ',
|
|
51
|
+
},
|
|
52
|
+
...(NOUN === 'meta' || !ADDRESSED || FILES !== undefined
|
|
53
|
+
? {}
|
|
54
|
+
: {
|
|
55
|
+
at: {
|
|
56
|
+
guide: 'the path of a file that already exists, relative to this tree',
|
|
57
|
+
asks: 'at — the file this names: ',
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
describes: {
|
|
61
|
+
guide:
|
|
62
|
+
NOUN === 'meta'
|
|
63
|
+
? 'one sentence a reader who has not seen the code could act on — what this governs'
|
|
64
|
+
: `one sentence a reader who has not seen the code could act on — what this ${NOUN} is`,
|
|
65
|
+
asks: 'describes — one sentence saying what this governs: ',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
NOUN === 'meta' || FILES !== undefined ? ['id', 'at', 'version'] : ['id', 'version'],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const said = (one: string): string => ASKS.find((each) => each.at === one)?.asks ?? `${one}: `
|
|
72
|
+
|
|
73
|
+
const WIDTH = Math.max(...ASKS.map((one) => one.at.length)) + 3
|
|
74
|
+
|
|
75
|
+
type Held = Omit<Threaded, 'at'> & { readonly at?: string }
|
|
76
|
+
|
|
77
|
+
const kebab = (one: string): string => one.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
|
|
78
|
+
|
|
79
|
+
const KEBAB = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/
|
|
80
|
+
|
|
81
|
+
const at = MINE?.at ?? within(TREE.at, TREE.root)
|
|
82
|
+
|
|
83
|
+
let sheet: Record<string, unknown> = {}
|
|
84
|
+
try {
|
|
85
|
+
sheet = (await import(pathToFileURL(join(TREE.at, at)).href)) as Record<string, unknown>
|
|
86
|
+
} catch {
|
|
87
|
+
sheet = {}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const shows = sheet['catalogue']
|
|
91
|
+
|
|
92
|
+
const shown: readonly string[] = typeof shows === 'function' ? await (shows as () => Promise<readonly string[]>)() : []
|
|
93
|
+
|
|
94
|
+
const teaches: readonly string[] = Array.isArray(sheet['TEACHES']) ? (sheet['TEACHES'] as readonly string[]) : []
|
|
95
|
+
|
|
96
|
+
const inSheet = ((): readonly Held[] | undefined => {
|
|
97
|
+
const only = sheet[LIST]
|
|
98
|
+
if (Array.isArray(only)) return only as readonly Held[]
|
|
99
|
+
for (const value of Object.values(sheet)) {
|
|
100
|
+
const rows = (value as Readonly<Record<string, unknown>> | undefined)?.[LIST]
|
|
101
|
+
if (Array.isArray(rows)) return rows as readonly Held[]
|
|
102
|
+
}
|
|
103
|
+
return undefined
|
|
104
|
+
})()
|
|
105
|
+
if (inSheet === undefined) {
|
|
106
|
+
refuse(`${at} states no \`${LIST}\` list, so there is nowhere in it for a ${NOUN} to go. \`npx thread read ${at}\` prints what it does state.`)
|
|
107
|
+
}
|
|
108
|
+
const existing: readonly Held[] = inSheet
|
|
109
|
+
|
|
110
|
+
const mark = (one: Held): string => marked([one.id, one.at ?? '', one.describes, String(one.version ?? '')])
|
|
111
|
+
|
|
112
|
+
const listing = [
|
|
113
|
+
existing.length === 0
|
|
114
|
+
? `${at} declares nothing yet.`
|
|
115
|
+
: `${String(existing.length)} thing(s) ${at} already declares:`,
|
|
116
|
+
...existing.map((one) => ` [${mark(one)}] ${one.id} — ${one.describes}`),
|
|
117
|
+
].join('\n')
|
|
118
|
+
|
|
119
|
+
const words = wordsIn(['new', 'read', 'remove', 'add', 'edit'])
|
|
120
|
+
|
|
121
|
+
const mustSay = (fields: readonly Field[] = FIELDS): readonly string[] =>
|
|
122
|
+
fields.filter((one) => one.required).map((one) => one.name)
|
|
123
|
+
|
|
124
|
+
const kept = (): readonly string[] => [...mustSay(), 'version']
|
|
125
|
+
|
|
126
|
+
const SAYS = new Map<string, string>([
|
|
127
|
+
['id', 'the word this row coins — kebab-case, and not already in that sheet'],
|
|
128
|
+
['at', 'the path of a file that already exists, relative to this tree'],
|
|
129
|
+
['describes', 'one sentence a reader who has not seen the file could act on'],
|
|
130
|
+
])
|
|
131
|
+
|
|
132
|
+
function asks(fields: readonly Field[]): readonly string[] {
|
|
133
|
+
const named = mustSay(fields).filter((one) => one !== 'version')
|
|
134
|
+
const mute = named.filter((one) => !SAYS.has(one))
|
|
135
|
+
if (mute.length > 0) {
|
|
136
|
+
refuse(
|
|
137
|
+
`every ${NOUN} states ${mute.join(', ')}, and \`add\` has no sentence to ask for ` +
|
|
138
|
+
`${mute.length === 1 ? 'it' : 'them'} with. The sentence goes beside the others in \`SAYS\`.`,
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
const pad = Math.max(...named.map((one) => one.length)) + 3
|
|
142
|
+
return named.map((one) => ` ${`${one}:`.padEnd(pad)}${SAYS.get(one) ?? ''}`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function unrestorable(row: Readonly<Record<string, unknown>>): readonly string[] {
|
|
146
|
+
const keeps = kept()
|
|
147
|
+
const worn = Object.keys(row).filter((one) => !keeps.includes(one) && row[one] !== undefined)
|
|
148
|
+
if (worn.length === 0) return []
|
|
149
|
+
return [
|
|
150
|
+
'',
|
|
151
|
+
` \`add\` writes ${keeps.join(', ')} and nothing else, so ${worn.length === 1 ? 'this field goes' : 'these fields go'}`,
|
|
152
|
+
' and no command here puts it back. Copy it now if this is a repoint rather than a deletion:',
|
|
153
|
+
'',
|
|
154
|
+
...worn.map((one) => ` ${one}: ${JSON.stringify(row[one])}`),
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const rowLike = (one: unknown): one is Held =>
|
|
159
|
+
one !== null && typeof one === 'object' && typeof (one as Held).id === 'string'
|
|
160
|
+
|
|
161
|
+
interface Below {
|
|
162
|
+
readonly row: Held
|
|
163
|
+
readonly list: string
|
|
164
|
+
readonly sheet: string
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const HELD = ['declares', 'holds'] as const
|
|
168
|
+
|
|
169
|
+
const under = async (row: Held, from: string, seen: Set<string>): Promise<readonly Below[]> => {
|
|
170
|
+
const sheet = row.at
|
|
171
|
+
if (typeof sheet !== 'string' || sheet === '' || sheet === from || seen.has(sheet)) return []
|
|
172
|
+
if (!sheet.startsWith(`${TREE.name}/`)) return []
|
|
173
|
+
seen.add(sheet)
|
|
174
|
+
let read: Record<string, unknown>
|
|
175
|
+
try {
|
|
176
|
+
read = (await import(pathToFileURL(join(TREE.at, sheet)).href)) as Record<string, unknown>
|
|
177
|
+
} catch {
|
|
178
|
+
return []
|
|
179
|
+
}
|
|
180
|
+
const found: Below[] = []
|
|
181
|
+
for (const [name, value] of Object.entries(read)) {
|
|
182
|
+
if (Array.isArray(value)) {
|
|
183
|
+
for (const each of value) if (rowLike(each)) found.push({ row: each, list: name, sheet })
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
for (const list of HELD) {
|
|
187
|
+
const held = (value as Readonly<Record<string, unknown>> | undefined)?.[list]
|
|
188
|
+
if (Array.isArray(held)) for (const each of held) if (rowLike(each)) found.push({ row: each, list, sheet })
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (const each of [...found]) found.push(...(await under(each.row, sheet, seen)))
|
|
192
|
+
return found
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
interface Under {
|
|
196
|
+
readonly meta: Held
|
|
197
|
+
readonly row: Held
|
|
198
|
+
readonly list: string
|
|
199
|
+
readonly sheet: string
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function pointedAt(
|
|
203
|
+
which: string | undefined,
|
|
204
|
+
inNode: string | undefined,
|
|
205
|
+
): Promise<{
|
|
206
|
+
readonly looking: readonly Held[]
|
|
207
|
+
readonly metas: readonly Held[]
|
|
208
|
+
readonly kids: readonly Under[]
|
|
209
|
+
}> {
|
|
210
|
+
const byNode =
|
|
211
|
+
inNode === undefined ? existing : existing.filter((each) => mark(each) === inNode || each.id === inNode)
|
|
212
|
+
|
|
213
|
+
const sheeted = inNode !== undefined && byNode.length === 0 && inNode.endsWith('.ts')
|
|
214
|
+
|
|
215
|
+
const looking = sheeted ? existing : byNode
|
|
216
|
+
const metas = inNode === undefined ? existing.filter((each) => mark(each) === which || each.id === which) : []
|
|
217
|
+
const kids: Under[] = []
|
|
218
|
+
const seen = new Set<string>()
|
|
219
|
+
for (const meta of looking) {
|
|
220
|
+
for (const held of await under(meta, at, seen)) {
|
|
221
|
+
if (mark(held.row) !== which && held.row.id !== which) continue
|
|
222
|
+
if (sheeted && held.sheet !== inNode) continue
|
|
223
|
+
kids.push({ meta, row: held.row, list: held.list, sheet: held.sheet })
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return { looking, metas, kids }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (words[0] === 'read') {
|
|
230
|
+
const which = words[2] ?? words[1]
|
|
231
|
+
const inNode = words[2] === undefined ? undefined : words[1]
|
|
232
|
+
const pointed = await pointedAt(which, inNode)
|
|
233
|
+
const kid = pointed.kids[0]
|
|
234
|
+
const one = pointed.metas[0] ?? kid?.row
|
|
235
|
+
if (one === undefined) {
|
|
236
|
+
refuse(
|
|
237
|
+
`\`${which ?? '(nothing)'}\` is not a row this tree declares. Give its id, or the mark printed in brackets ` +
|
|
238
|
+
`beside it — \`${TYPED} create ${NOUN}\` with nothing after it lists every one.`,
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
const sheet = kid === undefined ? at : kid.sheet
|
|
242
|
+
const said = Object.entries(one as unknown as Record<string, unknown>).filter(
|
|
243
|
+
([name, value]) => name !== 'id' && name !== 'describes' && value !== undefined,
|
|
244
|
+
)
|
|
245
|
+
const pad = Math.max(0, ...said.map(([name]) => name.length)) + 3
|
|
246
|
+
stdout.write(`${one.id} — ${one.describes}\n`)
|
|
247
|
+
stdout.write(` it is a row in ${sheet}\n\n`)
|
|
248
|
+
for (const [name, value] of said) {
|
|
249
|
+
stdout.write(` ${`${name}:`.padEnd(pad)}${typeof value === 'string' ? value : JSON.stringify(value)}\n`)
|
|
250
|
+
}
|
|
251
|
+
if (one.at === undefined) {
|
|
252
|
+
stdout.write(`\n${one.id} is a value ${sheet} holds rather than a file of its own.\n`)
|
|
253
|
+
exit(0)
|
|
254
|
+
}
|
|
255
|
+
if (one.at === sheet) {
|
|
256
|
+
stdout.write(`\n${one.at} is the sheet this row sits in rather than a file of its own.\n`)
|
|
257
|
+
exit(0)
|
|
258
|
+
}
|
|
259
|
+
stdout.write(`\n${one.at}:\n`)
|
|
260
|
+
try {
|
|
261
|
+
stdout.write(readFileSync(join(TREE.at, one.at), 'utf8'))
|
|
262
|
+
} catch {
|
|
263
|
+
stdout.write('(nothing is there)\n')
|
|
264
|
+
}
|
|
265
|
+
exit(0)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (words[0] === 'remove') {
|
|
269
|
+
|
|
270
|
+
const which = words[2] ?? words[1]
|
|
271
|
+
const inNode = words[2] === undefined ? undefined : words[1]
|
|
272
|
+
|
|
273
|
+
const { looking, metas, kids } = await pointedAt(which, inNode)
|
|
274
|
+
if (inNode !== undefined && looking.length === 0) {
|
|
275
|
+
refuse(`\`${inNode}\` is not a governing node ${at} declares, so there is nothing under it to take out.`)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (metas.length + kids.length === 0) {
|
|
279
|
+
refuse(
|
|
280
|
+
`\`${which ?? '(nothing)'}\` is not a row this tree declares. Every governing node in ${at} was ` +
|
|
281
|
+
'searched, and every list under each of them. Give the id, or the mark printed in brackets beside it above.',
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (metas.length + kids.length > 1) {
|
|
286
|
+
const named = [
|
|
287
|
+
...metas.map((each) => `\`${each.id ?? '(unnamed)'}\` — the governing node in ${at}`),
|
|
288
|
+
...kids.map((each) => `\`${each.row.id ?? '(unnamed)'}\` — under \`${each.meta.id ?? '(unnamed)'}\`, in ${each.sheet}`),
|
|
289
|
+
]
|
|
290
|
+
refuse(
|
|
291
|
+
`\`${which}\` names ${String(named.length)} rows: ${named.join('; ')}. Say which node or which sheet to look in — ` +
|
|
292
|
+
`\`thread create ${NOUN} remove <node|sheet> ${String(which)}\` — because two rows carrying the same values carry the ` +
|
|
293
|
+
'same mark AND the same id, so naming the row again cannot separate them. Nothing was written.',
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const kid = kids[0]
|
|
298
|
+
if (kid !== undefined) {
|
|
299
|
+
const sheet = join(TREE.at, kid.sheet)
|
|
300
|
+
const wrote = kid.list === 'derives' && kid.row.at !== undefined && kid.row.at !== kid.sheet
|
|
301
|
+
const flaggedKid = argv.indexOf(RECEIPT)
|
|
302
|
+
if (flaggedKid === -1) {
|
|
303
|
+
const nonce = issue(TREE.at, listing)
|
|
304
|
+
stdout.write(
|
|
305
|
+
[
|
|
306
|
+
'',
|
|
307
|
+
`\`${kid.row.id ?? '(unnamed)'}\` — ${kid.row.describes ?? ''}`,
|
|
308
|
+
kid.row.at === undefined
|
|
309
|
+
? ` it is a value held under \`${kid.meta.id ?? '(unnamed)'}\` in ${kid.sheet}.`
|
|
310
|
+
: ` it names ${kid.row.at}, under \`${kid.meta.id ?? '(unnamed)'}\` in ${kid.sheet}.`,
|
|
311
|
+
'',
|
|
312
|
+
|
|
313
|
+
` thread create ${NOUN} remove ${String(kid.meta.id)} ${String(kid.row.id)} ${RECEIPT} ${nonce}`,
|
|
314
|
+
'',
|
|
315
|
+
...(wrote
|
|
316
|
+
? [
|
|
317
|
+
` this takes the row out of ${kid.sheet} and deletes ${kid.row.at}, because that row derives`,
|
|
318
|
+
' it: a command wrote every byte of it, and with the row gone nothing writes it again. Any',
|
|
319
|
+
` command the row names stops existing at the same moment, because \`${TYPED} create\` reads`,
|
|
320
|
+
' the rows to know what commands there are.',
|
|
321
|
+
]
|
|
322
|
+
: kid.row.at === undefined
|
|
323
|
+
? [` this takes the row out of ${kid.sheet} and deletes no file, because a held value names none.`]
|
|
324
|
+
: [
|
|
325
|
+
` this takes the row out of ${kid.sheet} and LEAVES ${kid.row.at} exactly where it is.`,
|
|
326
|
+
' Nothing here wrote that file, so nothing here deletes it — it goes back to being declared',
|
|
327
|
+
' by nobody, which the descent will say.',
|
|
328
|
+
]),
|
|
329
|
+
...unrestorable(kid.row as unknown as Readonly<Record<string, unknown>>),
|
|
330
|
+
'',
|
|
331
|
+
].join('\n'),
|
|
332
|
+
)
|
|
333
|
+
stdout.write(`create: receipt ${nonce} — spent on use, and stale the moment ${at} changes.\n`)
|
|
334
|
+
exit(0)
|
|
335
|
+
}
|
|
336
|
+
spend(TREE.at, listing, argv[flaggedKid + 1], at)
|
|
337
|
+
|
|
338
|
+
const text = readFileSync(sheet, 'utf8')
|
|
339
|
+
const where = listBounds(text, kid.list)
|
|
340
|
+
if (where === undefined) {
|
|
341
|
+
refuse(`${kid.sheet} states no \`${kid.list}\` list, so there is nothing to take a row out of.`)
|
|
342
|
+
}
|
|
343
|
+
writeFileSync(sheet, withoutRow(text, where, String(kid.row.id)), 'utf8')
|
|
344
|
+
stdout.write(`create: took ${String(kid.row.id)} out of ${kid.sheet}.\n`)
|
|
345
|
+
if (!wrote) {
|
|
346
|
+
if (kid.row.at !== undefined) stdout.write(`create: ${kid.row.at} is still there, and now declared by nobody.\n`)
|
|
347
|
+
exit(0)
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
rmSync(join(TREE.at, String(kid.row.at)))
|
|
351
|
+
stdout.write(`create: deleted ${kid.row.at} — the row derived it, so nothing writes it any more.\n`)
|
|
352
|
+
} catch {
|
|
353
|
+
stdout.write(`create: ${kid.row.at} was not there to delete, so only the row came out.\n`)
|
|
354
|
+
}
|
|
355
|
+
exit(0)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const one = metas[0]
|
|
359
|
+
if (one === undefined) refuse(`\`${which ?? '(nothing)'}\` names nothing this tree declares.`)
|
|
360
|
+
|
|
361
|
+
const owns = typeof one.at === 'string' && one.at !== at
|
|
362
|
+
const inside = await under(one, at, new Set())
|
|
363
|
+
if (inside.length > 0) {
|
|
364
|
+
refuse(
|
|
365
|
+
`\`${one.id}\` governs ${String(inside.length)} thing(s) — ${inside.map((each) => each.row.id).join(', ')}. Removing it would ` +
|
|
366
|
+
'leave every one of them answering to nothing, and this will not cascade on your behalf. Take those out first, ' +
|
|
367
|
+
'and this becomes an empty node you can remove. Nothing was written.',
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const flaggedAt = argv.indexOf(RECEIPT)
|
|
372
|
+
if (flaggedAt === -1) {
|
|
373
|
+
const nonce = issue(TREE.at, listing)
|
|
374
|
+
stdout.write(
|
|
375
|
+
[
|
|
376
|
+
'',
|
|
377
|
+
`\`${one.id}\` — ${one.describes}`,
|
|
378
|
+
owns
|
|
379
|
+
? ` it governs nothing, and its sheet ${one.at ?? ''} declares an empty list.`
|
|
380
|
+
: one.at === undefined
|
|
381
|
+
? ` it is a value ${at} holds, and names no file.`
|
|
382
|
+
: ` it governs nothing, and ${one.at} is the sheet this row sits in rather than a file of its own.`,
|
|
383
|
+
'',
|
|
384
|
+
` thread create ${NOUN} remove ${which ?? one.id} ${RECEIPT} ${nonce}`,
|
|
385
|
+
'',
|
|
386
|
+
owns
|
|
387
|
+
? ` this takes the row out of ${at} and deletes ${one.at ?? ''}. It is the one command here that`
|
|
388
|
+
: ` this takes the row out of ${at} and deletes no file.`,
|
|
389
|
+
owns
|
|
390
|
+
? ' running again will not undo.'
|
|
391
|
+
: one.at === undefined
|
|
392
|
+
? ` ${at} holds every other value in that list and is left exactly as it is.`
|
|
393
|
+
: ` ${one.at} holds every other row in that list and is left exactly as it is.`,
|
|
394
|
+
...unrestorable(one as unknown as Readonly<Record<string, unknown>>),
|
|
395
|
+
'',
|
|
396
|
+
].join('\n'),
|
|
397
|
+
)
|
|
398
|
+
exit(0)
|
|
399
|
+
}
|
|
400
|
+
spend(TREE.at, listing, argv[flaggedAt + 1], at)
|
|
401
|
+
|
|
402
|
+
const text = readFileSync(join(TREE.at, at), 'utf8')
|
|
403
|
+
const where = listBounds(text, LIST)
|
|
404
|
+
if (where === undefined) refuse(`${at} states no \`${LIST}\` list, so there is nothing to take a row out of.`)
|
|
405
|
+
writeFileSync(join(TREE.at, at), withoutRow(text, where, one.id), 'utf8')
|
|
406
|
+
stdout.write(`create: took ${one.id} out of ${at}.\n`)
|
|
407
|
+
if (owns) {
|
|
408
|
+
try {
|
|
409
|
+
rmSync(join(TREE.at, one.at ?? ''))
|
|
410
|
+
stdout.write(`create: deleted ${one.at ?? ''}.\n`)
|
|
411
|
+
} catch {
|
|
412
|
+
stdout.write(`create: ${one.at ?? ''} was not there to delete.\n`)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
exit(0)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const FIELD_KEY = 'field'
|
|
419
|
+
|
|
420
|
+
const FIELD = asFlag(FIELD_KEY)
|
|
421
|
+
|
|
422
|
+
const FIELD_ASKS: readonly Asked[] = [
|
|
423
|
+
{ at: 'describes', guide: 'the sentence the CLI asks with. One a person who has not seen the code could answer' },
|
|
424
|
+
{ at: 'options', guide: 'the values it may take, separated by |, e.g. framework|optional. Leave it off to take any text' },
|
|
425
|
+
{
|
|
426
|
+
at: 'optional',
|
|
427
|
+
guide:
|
|
428
|
+
'answer y and a row may leave the field unstated. Anything else and every row must state it, and each row ' +
|
|
429
|
+
'that does not is named below with the command that fills it in',
|
|
430
|
+
},
|
|
431
|
+
]
|
|
432
|
+
|
|
433
|
+
const FIELD_TAKES: readonly string[] = [FIELD_KEY, ...answerNames(FIELD_ASKS)]
|
|
434
|
+
|
|
435
|
+
if (words[0] === 'edit' && argv.includes(FIELD)) {
|
|
436
|
+
const name = (argv[argv.indexOf(FIELD) + 1] ?? '').trim()
|
|
437
|
+
if (!/^[a-z][A-Za-z0-9]*$/.test(name)) {
|
|
438
|
+
refuse('`' + name + '` cannot be a field. A field is one camelCase word, e.g. folder. Nothing was written.')
|
|
439
|
+
}
|
|
440
|
+
const answers = pipedIn()
|
|
441
|
+
only(answers, FIELD_TAKES, '`edit ' + FIELD + '`')
|
|
442
|
+
const sentence = (answer(answers, 'describes') ?? '').trim()
|
|
443
|
+
const options = (answer(answers, 'options') ?? '').split('|').map((one) => one.trim()).filter((one) => one !== '')
|
|
444
|
+
const optional = (answer(answers, 'optional') ?? '').trim().toLowerCase().startsWith('y')
|
|
445
|
+
|
|
446
|
+
const flaggedField = argv.indexOf(RECEIPT)
|
|
447
|
+
if (flaggedField === -1) {
|
|
448
|
+
const nonce = issue(TREE.at, listing)
|
|
449
|
+
stdout.write(
|
|
450
|
+
[
|
|
451
|
+
'',
|
|
452
|
+
'`edit ' + FIELD + '` gives every ' + NOUN + ' a new field, written into the shape at the top of ' + at + '. From then on',
|
|
453
|
+
'`thread create ' + NOUN + '` asks for it with the sentence you give here, and `thread edit` can set it on any row.',
|
|
454
|
+
'',
|
|
455
|
+
...guidance(FIELD_ASKS),
|
|
456
|
+
'',
|
|
457
|
+
' thread create ' + NOUN + ' edit ' + FIELD + ' <name> --describes <sentence> ' + RECEIPT + ' ' + nonce,
|
|
458
|
+
'',
|
|
459
|
+
].join('\n'),
|
|
460
|
+
)
|
|
461
|
+
stdout.write('create: receipt ' + nonce + ' — spent on use, and stale the moment ' + at + ' changes.\n')
|
|
462
|
+
exit(0)
|
|
463
|
+
}
|
|
464
|
+
spend(TREE.at, listing, argv[flaggedField + 1], at)
|
|
465
|
+
|
|
466
|
+
if (sentence === '') {
|
|
467
|
+
refuse('`' + name + '` was given no sentence, and the sentence is what `thread create ' + NOUN + '` asks with. Give --describes. Nothing was written.')
|
|
468
|
+
}
|
|
469
|
+
const file = join(TREE.at, at)
|
|
470
|
+
const text = readFileSync(file, 'utf8')
|
|
471
|
+
const opens = 'export interface ' + SHAPE + ' extends Threaded {'
|
|
472
|
+
const from = text.indexOf(opens)
|
|
473
|
+
if (from === -1) {
|
|
474
|
+
refuse(at + ' has no shape at its top for this to write into. Every sheet `thread create meta new` writes has one. Run `thread read ' + at + '` to see what is there. Nothing was written.')
|
|
475
|
+
}
|
|
476
|
+
const close = text.indexOf('\n}', from)
|
|
477
|
+
if (close === -1) {
|
|
478
|
+
refuse('the shape at the top of ' + at + ' never closes, so nothing can be added to it. Run `thread read ' + at + '` to see where it stops. Nothing was written.')
|
|
479
|
+
}
|
|
480
|
+
const block = text.slice(from, close)
|
|
481
|
+
if (block.includes('readonly ' + name + ':') || block.includes('readonly ' + name + '?:')) {
|
|
482
|
+
refuse('the shape at the top of ' + at + ' already states `' + name + '`. One field, one statement. Nothing was written.')
|
|
483
|
+
}
|
|
484
|
+
const type = options.length === 0 ? 'string' : options.map((one) => quoted(one)).join(' | ')
|
|
485
|
+
const line = '\n /** ' + sentence + ' */\n readonly ' + name + (optional ? '?' : '') + ': ' + type
|
|
486
|
+
writeFileSync(file, text.slice(0, close) + line + text.slice(close), 'utf8')
|
|
487
|
+
stdout.write('create: every ' + NOUN + ' now states `' + name + '`, and `thread create ' + NOUN + '` asks for it with the sentence you gave.\n')
|
|
488
|
+
|
|
489
|
+
if (!optional) {
|
|
490
|
+
const lacking = existing.filter((one) => (one as unknown as Record<string, unknown>)[name] === undefined)
|
|
491
|
+
if (lacking.length > 0) {
|
|
492
|
+
stdout.write('create: ' + String(lacking.length) + ' ' + NOUN + '(s) already here state no `' + name + '`, and the shape now requires it. Fill each in:\n')
|
|
493
|
+
for (const one of lacking) stdout.write(' thread edit ' + mark(one) + ' --' + name + ' <value>\n')
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
exit(0)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (words[0] === 'edit') {
|
|
500
|
+
const which = words[2] ?? words[1]
|
|
501
|
+
const inNode = words[2] === undefined ? undefined : words[1]
|
|
502
|
+
const { looking, metas, kids } = await pointedAt(which, inNode)
|
|
503
|
+
|
|
504
|
+
if (inNode !== undefined && looking.length === 0) {
|
|
505
|
+
refuse(`\`${inNode}\` is not a governing node ${at} declares, so there is nothing under it to change.`)
|
|
506
|
+
}
|
|
507
|
+
if (metas.length + kids.length === 0) {
|
|
508
|
+
refuse(
|
|
509
|
+
`\`${which ?? '(nothing)'}\` is not a row this tree declares. Every governing node in ${at} was ` +
|
|
510
|
+
'searched, and every list under each of them. Give the id, or the mark printed in brackets beside it above.',
|
|
511
|
+
)
|
|
512
|
+
}
|
|
513
|
+
if (metas.length + kids.length > 1) {
|
|
514
|
+
const named = [
|
|
515
|
+
...metas.map((each) => `\`${each.id ?? '(unnamed)'}\` — the governing node in ${at}`),
|
|
516
|
+
...kids.map(
|
|
517
|
+
(each) => `\`${each.row.id ?? '(unnamed)'}\` — under \`${each.meta.id ?? '(unnamed)'}\`, in ${each.sheet}`,
|
|
518
|
+
),
|
|
519
|
+
]
|
|
520
|
+
refuse(
|
|
521
|
+
`\`${String(which)}\` names ${String(named.length)} rows: ${named.join('; ')}. Say which node to look ` +
|
|
522
|
+
`under — \`thread create ${NOUN} edit <node> ${String(which)}\` — because two rows carrying the same ` +
|
|
523
|
+
'values carry the same mark AND the same id, so naming the row again cannot separate them. Nothing was ' +
|
|
524
|
+
'written.',
|
|
525
|
+
)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const kid = kids[0]
|
|
529
|
+
const held = kid === undefined ? metas[0] : kid.row
|
|
530
|
+
if (held?.id === undefined) {
|
|
531
|
+
refuse(`\`${String(which)}\` names a row with no id, and a row is reached by its id. Nothing was written.`)
|
|
532
|
+
}
|
|
533
|
+
const file = kid === undefined ? join(TREE.at, at) : join(TREE.at, kid.sheet)
|
|
534
|
+
const shown = kid === undefined ? at : kid.sheet
|
|
535
|
+
const inList = kid === undefined ? LIST : kid.list
|
|
536
|
+
|
|
537
|
+
const shaped = listFields(file, inList).filter((one) => one.name !== 'id')
|
|
538
|
+
if (shaped.length === 0) {
|
|
539
|
+
refuse(
|
|
540
|
+
`${shown} states a \`${inList}\` this cannot read the shape of, so which fields a row there may carry is ` +
|
|
541
|
+
'unknown. Editing one blind is how a row stops being the shape its own list declares.',
|
|
542
|
+
)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const editable = shaped.map((one) => one.name)
|
|
546
|
+
|
|
547
|
+
const listed = new Set(shaped.filter((one) => one.list).map((one) => one.name))
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
const flagged = new Set(shaped.filter((one) => one.boolean).map((one) => one.name))
|
|
551
|
+
|
|
552
|
+
const text = readFileSync(file, 'utf8')
|
|
553
|
+
const where = listBounds(text, inList)
|
|
554
|
+
if (where === undefined) refuse(`${shown} states no \`${inList}\` list, so there is no row in it to change.`)
|
|
555
|
+
|
|
556
|
+
const standing = editable.map((one) => ({ field: one, value: fieldIn(text, where, String(held.id), one) }))
|
|
557
|
+
|
|
558
|
+
const flaggedEdit = argv.indexOf(RECEIPT)
|
|
559
|
+
if (flaggedEdit === -1) {
|
|
560
|
+
const nonce = issue(TREE.at, listing)
|
|
561
|
+
stdout.write(
|
|
562
|
+
[
|
|
563
|
+
'',
|
|
564
|
+
`\`${String(held.id)}\` — ${String(held.describes ?? '')}`,
|
|
565
|
+
` it is a row in ${shown}, and \`edit\` changes what one of its fields says. One command, so a refusal`,
|
|
566
|
+
' leaves the row exactly as it stands — which is the thing `remove` then `add` cannot promise.',
|
|
567
|
+
'',
|
|
568
|
+
'what it says now:',
|
|
569
|
+
'',
|
|
570
|
+
...standing.map(
|
|
571
|
+
(one) => ` ${`${one.field}:`.padEnd(12)}${one.value === undefined ? '(not stated)' : one.value}`,
|
|
572
|
+
),
|
|
573
|
+
'',
|
|
574
|
+
'answer only the field you are changing, one per line, in any order:',
|
|
575
|
+
'',
|
|
576
|
+
` thread edit ${String(which)} --<field> <value> ${RECEIPT} ${nonce}`,
|
|
577
|
+
'',
|
|
578
|
+
' a field it is not handed is left alone, and `id` is not among them: changing one retires a word',
|
|
579
|
+
' and coins another, which is a decision rather than a replacement.',
|
|
580
|
+
'',
|
|
581
|
+
].join('\n'),
|
|
582
|
+
)
|
|
583
|
+
stdout.write(`create: receipt ${nonce} — spent on use, and stale the moment ${at} changes.\n`)
|
|
584
|
+
exit(0)
|
|
585
|
+
}
|
|
586
|
+
spend(TREE.at, listing, argv[flaggedEdit + 1], at)
|
|
587
|
+
|
|
588
|
+
const answers = gathered(pipedIn(), shaped)
|
|
589
|
+
const own = ownAnswers(answers)
|
|
590
|
+
const groups = childrenIn(answers)
|
|
591
|
+
only(answers, editable, '`edit`')
|
|
592
|
+
if (answers.length === 0) {
|
|
593
|
+
refuse('`edit` was handed no field to change. Answer one of the fields it listed. Nothing was written.')
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const wasAt = typeof held.at === 'string' ? held.at : ''
|
|
597
|
+
const moving = own.find((one) => one.key === 'at')
|
|
598
|
+
const goesTo = moving === undefined || moving.value === wasAt ? '' : moving.value
|
|
599
|
+
if (goesTo !== '' && existsSync(join(TREE.at, goesTo))) {
|
|
600
|
+
refuse(
|
|
601
|
+
`${goesTo} is already there, and one address holds one thing. Moving \`${String(held.id)}\` onto it would ` +
|
|
602
|
+
'leave two rows pointing at one file and no way to say which owns it. Nothing was written.',
|
|
603
|
+
)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
let next = text
|
|
607
|
+
for (const one of own) {
|
|
608
|
+
const bounds = listBounds(next, inList)
|
|
609
|
+
if (bounds === undefined) refuse(`${shown} states no \`${LIST}\` list.`)
|
|
610
|
+
const now = fieldIn(next, bounds, String(held.id), one.key)
|
|
611
|
+
if (flagged.has(one.key)) {
|
|
612
|
+
const set = withField(next, bounds, String(held.id), one.key, asBoolean(one.value))
|
|
613
|
+
if (set !== undefined) {
|
|
614
|
+
next = set
|
|
615
|
+
continue
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const field = shaped.find((each) => each.name === one.key)
|
|
619
|
+
if (field !== undefined && mapped(field, one.value)) {
|
|
620
|
+
const set = withField(next, bounds, String(held.id), one.key, asMap(one.value))
|
|
621
|
+
if (set !== undefined) {
|
|
622
|
+
next = set
|
|
623
|
+
continue
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (listed.has(one.key)) {
|
|
627
|
+
const put = withField(next, bounds, String(held.id), one.key, arrayValue(' ', one.key, one.value, groups.get(one.key)))
|
|
628
|
+
if (put !== undefined) {
|
|
629
|
+
next = put
|
|
630
|
+
continue
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (now === undefined && listed.has(one.key)) {
|
|
634
|
+
refuse(
|
|
635
|
+
`\`${String(held.id)}\` states no \`${one.key}\`, and that field holds rows rather than a sentence. ` +
|
|
636
|
+
`Write what goes in it with \`${TYPED} create meta add\`, which writes the whole row. Nothing was written.`,
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
if (now !== undefined && !now.startsWith("'")) {
|
|
640
|
+
refuse(
|
|
641
|
+
`\`${one.key}\` on \`${String(held.id)}\` is ${now}, which is not a plain sentence — replacing it with ` +
|
|
642
|
+
'one would change what kind of thing it is. Nothing was written.',
|
|
643
|
+
)
|
|
644
|
+
}
|
|
645
|
+
const written = withField(next, bounds, String(held.id), one.key, quoted(one.value))
|
|
646
|
+
if (written === undefined) {
|
|
647
|
+
refuse(`\`${one.key}\` could not be written on \`${String(held.id)}\`. Nothing was written.`)
|
|
648
|
+
}
|
|
649
|
+
next = written
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
for (const [list, found] of groups) {
|
|
653
|
+
const bounds = listBounds(next, inList)
|
|
654
|
+
if (bounds === undefined) break
|
|
655
|
+
const put = withField(next, bounds, String(held.id), list, arrayValue(' ', list, '', found))
|
|
656
|
+
if (put === undefined) {
|
|
657
|
+
refuse(`${list} could not be written on ${String(held.id)}, so nothing was written. Run npx thread read ${shown} to see what that row states now.`)
|
|
658
|
+
}
|
|
659
|
+
next = put
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
writeFileSync(file, next, 'utf8')
|
|
663
|
+
|
|
664
|
+
if (goesTo !== '') {
|
|
665
|
+
if (existsSync(join(TREE.at, wasAt))) {
|
|
666
|
+
mkdirSync(dirname(join(TREE.at, goesTo)), { recursive: true })
|
|
667
|
+
renameSync(join(TREE.at, wasAt), join(TREE.at, goesTo))
|
|
668
|
+
stdout.write(`create: moved ${wasAt} to ${goesTo}, because that is where ${String(held.id)} now says it is.\n`)
|
|
669
|
+
} else {
|
|
670
|
+
stdout.write(
|
|
671
|
+
`create: nothing was at ${wasAt}, so nothing moved. \`${String(held.id)}\` now says ${goesTo} and that ` +
|
|
672
|
+
'address is empty, which `npx thread descent` will name until something is put there.\n',
|
|
673
|
+
)
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const nowAt = goesTo === '' ? wasAt : goesTo
|
|
678
|
+
const mine = listBounds(next, inList)
|
|
679
|
+
const source = mine === undefined ? undefined : rowsIn(next, mine).find((one) => one.id === held.id)?.source
|
|
680
|
+
if (kid === undefined && nowAt !== '' && source !== undefined) {
|
|
681
|
+
const sheet = join(TREE.at, nowAt)
|
|
682
|
+
if (existsSync(sheet)) {
|
|
683
|
+
const there = readFileSync(sheet, 'utf8')
|
|
684
|
+
const settled = reconciled(there, `const HEAD = ${source.trim().replace(/,$/, '')}\n`, STATED)
|
|
685
|
+
if (settled !== there) writeFileSync(sheet, settled, 'utf8')
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
stdout.write(
|
|
690
|
+
`create: ${String(held.id)} now says ${answers.map((one) => `\`${one.key}\``).join(', ')} in ${shown}.\n`,
|
|
691
|
+
)
|
|
692
|
+
exit(0)
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (words[0] === 'add') {
|
|
696
|
+
const which = words[1]
|
|
697
|
+
const one = existing.find((each) => mark(each) === which || each.id === which)
|
|
698
|
+
if (one === undefined) {
|
|
699
|
+
refuse(
|
|
700
|
+
`\`${which ?? '(nothing)'}\` is not something ${at} declares. Give the id, or the mark printed in brackets ` +
|
|
701
|
+
'beside it above.',
|
|
702
|
+
)
|
|
703
|
+
}
|
|
704
|
+
if (one.at === undefined) {
|
|
705
|
+
refuse(
|
|
706
|
+
`\`${one.id}\` is a value ${at} holds, so it has no sheet of its own for a child to go in. ` +
|
|
707
|
+
`\`thread create ${NOUN} new\` is how one more is declared.`,
|
|
708
|
+
)
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const sheet = join(TREE.at, one.at)
|
|
712
|
+
if (!existsSync(sheet)) {
|
|
713
|
+
refuse(`\`${one.id}\` names ${one.at} and nothing is there, so there is no list to declare a child in.`)
|
|
714
|
+
}
|
|
715
|
+
if (listBounds(readFileSync(sheet, 'utf8'), LIST) === undefined) {
|
|
716
|
+
refuse(`${one.at} states no \`${LIST}\` list, so there is nowhere in it for a child to go.`)
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const fields = listFields(sheet, LIST)
|
|
720
|
+
if (fields.length === 0) {
|
|
721
|
+
refuse(
|
|
722
|
+
`${one.at} states a \`${LIST}\` this cannot read the shape of, so what a row there must say is unknown. ` +
|
|
723
|
+
'Write it as a list with a declared row type — the fields `add` asks for are read off that type, and ' +
|
|
724
|
+
'guessing them is how a row gets written that the tree then refuses.',
|
|
725
|
+
)
|
|
726
|
+
}
|
|
727
|
+
if (!fields.some((each) => each.name === 'at')) {
|
|
728
|
+
refuse(
|
|
729
|
+
`${one.at} holds values rather than files, so there is nothing on disk to point a row at. ` +
|
|
730
|
+
`\`thread create ${NOUN} new\` is how one is declared.`,
|
|
731
|
+
)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const flaggedAt = argv.indexOf(RECEIPT)
|
|
735
|
+
if (flaggedAt === -1) {
|
|
736
|
+
const nonce = issue(TREE.at, listing)
|
|
737
|
+
stdout.write(
|
|
738
|
+
[
|
|
739
|
+
'',
|
|
740
|
+
`\`${one.id}\` — ${one.describes}`,
|
|
741
|
+
` its sheet is ${one.at}, and a row put there names a file that is ALREADY on disk.`,
|
|
742
|
+
'',
|
|
743
|
+
'what `add` takes, one answer per line, in any order:',
|
|
744
|
+
'',
|
|
745
|
+
...asks(fields),
|
|
746
|
+
'',
|
|
747
|
+
` thread create ${NOUN} add ${which ?? one.id} ${RECEIPT} ${nonce}`,
|
|
748
|
+
'',
|
|
749
|
+
' it writes no file. It points this node at one somebody already wrote, and from then on',
|
|
750
|
+
' that file is held to everything this tree refuses.',
|
|
751
|
+
'',
|
|
752
|
+
].join('\n'),
|
|
753
|
+
)
|
|
754
|
+
stdout.write(`create: receipt ${nonce} — spent on use, and stale the moment ${at} changes.\n`)
|
|
755
|
+
exit(0)
|
|
756
|
+
}
|
|
757
|
+
spend(TREE.at, listing, argv[flaggedAt + 1], at)
|
|
758
|
+
|
|
759
|
+
const answers = pipedIn()
|
|
760
|
+
const { piped: fed, ask: asked, shut: close } = asking(answers)
|
|
761
|
+
if (fed) only(answers, mustSay(fields), '`add`')
|
|
762
|
+
|
|
763
|
+
const said = fed ? needed(answers, 'at', '`add`') : await asked('at', 'at — the path of a file that already exists: ')
|
|
764
|
+
const target = join(TREE.at, said)
|
|
765
|
+
const childAt = within(TREE.at, target)
|
|
766
|
+
if (!existsSync(target)) {
|
|
767
|
+
close()
|
|
768
|
+
refuse(`nothing is at ${childAt}. \`add\` points this node at a file that already exists — it writes none.`)
|
|
769
|
+
}
|
|
770
|
+
if (statSync(target).isDirectory()) {
|
|
771
|
+
close()
|
|
772
|
+
refuse(
|
|
773
|
+
`${childAt} is a directory. Name the file — a directory threads nothing beneath it, so everything under it ` +
|
|
774
|
+
'would still read as declared by nobody while the row itself looked answered.',
|
|
775
|
+
)
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const childId = kebab(fed ? needed(answers, 'id', '`add`') : await asked('id', 'id — the word this row coins: '))
|
|
779
|
+
if (!KEBAB.test(childId)) {
|
|
780
|
+
close()
|
|
781
|
+
refuse(`\`${childId}\` cannot be an id — it is kebab-case, e.g. glossary-generator.`)
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const saying = fed
|
|
785
|
+
? needed(answers, 'describes', '`add`')
|
|
786
|
+
: await asked('describes', 'describes — one sentence saying what this file is: ')
|
|
787
|
+
if (saying === '') {
|
|
788
|
+
close()
|
|
789
|
+
refuse(`${childAt} was given no meaning, and a row without one is a path nobody can read.`)
|
|
790
|
+
}
|
|
791
|
+
close()
|
|
792
|
+
|
|
793
|
+
const now = readFileSync(sheet, 'utf8')
|
|
794
|
+
const place = listBounds(now, LIST)
|
|
795
|
+
if (place === undefined) refuse(`${one.at} states no \`${LIST}\` list.`)
|
|
796
|
+
if (holdsRow(now, place, childId)) refuse(`\`${childId}\` is already declared in ${one.at}. One id, one row.`)
|
|
797
|
+
|
|
798
|
+
const row = [
|
|
799
|
+
' {',
|
|
800
|
+
` id: ${quoted(childId)},`,
|
|
801
|
+
` at: ${quoted(childAt)},`,
|
|
802
|
+
` describes: ${quoted(saying)},`,
|
|
803
|
+
' version: 1,',
|
|
804
|
+
' },',
|
|
805
|
+
].join('\n')
|
|
806
|
+
|
|
807
|
+
writeFileSync(sheet, withRow(now, place, row), 'utf8')
|
|
808
|
+
stdout.write(`create: ${one.id} now declares ${childAt}.\n`)
|
|
809
|
+
exit(0)
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
stdout.write(`${listing}\n`)
|
|
813
|
+
for (const line of shown) stdout.write(`${line}\n`)
|
|
814
|
+
|
|
815
|
+
const flagged = argv.indexOf(RECEIPT)
|
|
816
|
+
|
|
817
|
+
if (flagged === -1) {
|
|
818
|
+
const nonce = issue(TREE.at, listing)
|
|
819
|
+
stdout.write(
|
|
820
|
+
[
|
|
821
|
+
'',
|
|
822
|
+
...VERBS,
|
|
823
|
+
'',
|
|
824
|
+
'what `new` takes, one answer per line, in any order:',
|
|
825
|
+
'',
|
|
826
|
+
...guidance(ASKS),
|
|
827
|
+
...(FILES === undefined
|
|
828
|
+
? []
|
|
829
|
+
: [
|
|
830
|
+
` ${'at:'.padEnd(WIDTH)}${FILES.replace('*', '<name>')} — where it lives is never typed`,
|
|
831
|
+
` ${`${asFlag(BODY)}:`.padEnd(WIDTH)}what goes in that file, given on the command. Leave it out and the settle writes the starter`,
|
|
832
|
+
]),
|
|
833
|
+
...(teaches.length === 0 ? [] : ['', ...teaches]),
|
|
834
|
+
'',
|
|
835
|
+
` thread create ${NOUN} new <name> ${RECEIPT} ${nonce}`,
|
|
836
|
+
` thread create ${NOUN} read <mark> no receipt, writes nothing`,
|
|
837
|
+
...(ADDRESSED
|
|
838
|
+
? [
|
|
839
|
+
` thread create ${NOUN} add <mark> ${RECEIPT} ${nonce}`,
|
|
840
|
+
' points a node at a file that already exists — it writes none',
|
|
841
|
+
]
|
|
842
|
+
: []),
|
|
843
|
+
` thread create ${NOUN} remove <id> ${RECEIPT} ${nonce}`,
|
|
844
|
+
' only a node governing nothing — it refuses one with anything under it',
|
|
845
|
+
` thread create ${NOUN} edit <row> ${RECEIPT} ${nonce}`,
|
|
846
|
+
' changes what one field of an existing row says. One command, so a refusal leaves',
|
|
847
|
+
' the row exactly as it stands',
|
|
848
|
+
'',
|
|
849
|
+
`${RECEIPT} is spent by one run, and is refused if ${at} changed after it was issued.`,
|
|
850
|
+
'',
|
|
851
|
+
].join('\n'),
|
|
852
|
+
)
|
|
853
|
+
stdout.write(`create: receipt ${nonce} — spent on use, and stale the moment ${at} changes.\n`)
|
|
854
|
+
exit(0)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
spend(TREE.at, listing, argv[flagged + 1], at)
|
|
858
|
+
|
|
859
|
+
const all = gathered(pipedIn(), FIELDS)
|
|
860
|
+
const { piped, ask, line, shut } = asking(all)
|
|
861
|
+
|
|
862
|
+
const own = ownAnswers(all)
|
|
863
|
+
const TAKES: readonly string[] = FILES === undefined ? answerNames(ASKS) : [...answerNames(ASKS), BODY]
|
|
864
|
+
if (piped) only(own, TAKES, '`new`')
|
|
865
|
+
|
|
866
|
+
const name = kebab(words[1] ?? (piped ? needed(own, 'name', '`new`') : await ask('name', said('name'))))
|
|
867
|
+
if (!KEBAB.test(name)) {
|
|
868
|
+
shut()
|
|
869
|
+
refuse(`\`${name}\` cannot be the id of a governing node — it is kebab-case, e.g. billing.`)
|
|
870
|
+
}
|
|
871
|
+
if (existing.some((one) => one.id === name)) {
|
|
872
|
+
shut()
|
|
873
|
+
refuse(`\`${name}\` is already declared in ${at}. One id, one row.`)
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
let taken: ReadonlyMap<string, string> = new Map()
|
|
877
|
+
try {
|
|
878
|
+
taken = nouns(await generators(TREE), 'create')
|
|
879
|
+
} catch {
|
|
880
|
+
taken = new Map()
|
|
881
|
+
}
|
|
882
|
+
if (taken.has(name)) {
|
|
883
|
+
shut()
|
|
884
|
+
refuse(
|
|
885
|
+
`\`${name}\` is already a noun this tree makes — \`thread create ${name}\` runs ${taken.get(name) ?? 'a command'}. ` +
|
|
886
|
+
'One word, one meaning: a governing node called that would be reachable only by its mark, and the two would ' +
|
|
887
|
+
'be told apart by nothing. Choose another name. Nothing was written.',
|
|
888
|
+
)
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const target = join(TREE.dir, `${name}.ts`)
|
|
892
|
+
const sheetAt = within(TREE.at, target)
|
|
893
|
+
if (NOUN === 'meta' && existsSync(target)) {
|
|
894
|
+
shut()
|
|
895
|
+
refuse(`${sheetAt} is already there and no row names it. Choose another name, or delete it first.`)
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const describes = piped
|
|
899
|
+
? needed(own, 'describes', '`new`')
|
|
900
|
+
: await ask('describes', said('describes'))
|
|
901
|
+
if (describes === '') {
|
|
902
|
+
shut()
|
|
903
|
+
refuse(`\`${name}\` was given no meaning, and a governing node without one is a name nobody can read.`)
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const address =
|
|
907
|
+
NOUN === 'meta'
|
|
908
|
+
? sheetAt
|
|
909
|
+
: FILES !== undefined
|
|
910
|
+
? FILES.replace('*', name)
|
|
911
|
+
: !ADDRESSED
|
|
912
|
+
? undefined
|
|
913
|
+
: piped
|
|
914
|
+
? needed(own, 'at', '`new`')
|
|
915
|
+
: await ask('at', said('at'))
|
|
916
|
+
|
|
917
|
+
const { stated, kids } = await stating(ASKS, all, piped, ask, line, ['name', 'id', 'at', 'describes', 'version'])
|
|
918
|
+
|
|
919
|
+
const body =
|
|
920
|
+
FILES === undefined || address === undefined
|
|
921
|
+
? undefined
|
|
922
|
+
: await bodied(`${address} itself — this is ${asFlag(BODY)}`, '', piped, all, line)
|
|
923
|
+
|
|
924
|
+
shut()
|
|
925
|
+
|
|
926
|
+
if (body !== undefined && address !== undefined && existsSync(join(TREE.at, address))) {
|
|
927
|
+
refuse(
|
|
928
|
+
`${address} is already on disk, so the body you gave has nowhere to go without being put over somebody's file. ` +
|
|
929
|
+
`Leave the body out and the row names what is there, or run \`${TYPED} read ${address}\` and move it first. Nothing was written.`,
|
|
930
|
+
)
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const row = [
|
|
934
|
+
' {',
|
|
935
|
+
` id: ${quoted(name)},`,
|
|
936
|
+
...(address === undefined ? [] : [` at: ${quoted(address)},`]),
|
|
937
|
+
` describes: ${quoted(describes)},`,
|
|
938
|
+
' version: 1,',
|
|
939
|
+
...rowLines(' ', stated, NOUN === 'meta' ? new Map() : kids),
|
|
940
|
+
' },',
|
|
941
|
+
].join('\n')
|
|
942
|
+
|
|
943
|
+
const text = readFileSync(join(TREE.at, at), 'utf8')
|
|
944
|
+
const where = listBounds(text, LIST)
|
|
945
|
+
if (where === undefined) refuse(`${at} states no \`${LIST}\` list to declare a governing node in.`)
|
|
946
|
+
if (holdsRow(text, where, name)) refuse(`\`${name}\` is already declared in ${at}.`)
|
|
947
|
+
|
|
948
|
+
writeFileSync(join(TREE.at, at), withRow(text, where, row), 'utf8')
|
|
949
|
+
stdout.write(`create: declared ${name} in ${at}.\n`)
|
|
950
|
+
|
|
951
|
+
for (const said of seeding(kids, TREE.at)) stdout.write(`create: ${said}\n`)
|
|
952
|
+
|
|
953
|
+
if (FILES !== undefined && address !== undefined) {
|
|
954
|
+
if (body === undefined) {
|
|
955
|
+
stdout.write(`create: ${address} follows — the settle writes its starter, and \`${TYPED} edit ${address} --from <path>\` replaces it.\n`)
|
|
956
|
+
} else {
|
|
957
|
+
mkdirSync(dirname(join(TREE.at, address)), { recursive: true })
|
|
958
|
+
writeFileSync(join(TREE.at, address), body.endsWith('\n') ? body : `${body}\n`, 'utf8')
|
|
959
|
+
stdout.write(`create: wrote ${address} with the body you gave it.\n`)
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (NOUN !== 'meta') {
|
|
964
|
+
stdout.write(`create: \`npx thread read ${at}\` prints what that list says now.\n`)
|
|
965
|
+
exit(0)
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const opens = (readFileSync(join(TREE.at, at), 'utf8').split('\n')[0] ?? '').trim()
|
|
969
|
+
if (!opens.includes('Threaded')) {
|
|
970
|
+
refuse(
|
|
971
|
+
`${at} does not open by importing \`Threaded\`, so a new sheet cannot be given the same import and would be ` +
|
|
972
|
+
'written unable to compile. Nothing was scaffolded.',
|
|
973
|
+
)
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
const CONST = name.toUpperCase().replace(/-/g, '_')
|
|
977
|
+
const SHAPED = name.split('-').map((one) => `${one.slice(0, 1).toUpperCase()}${one.slice(1)}`).join('')
|
|
978
|
+
writeFileSync(
|
|
979
|
+
target,
|
|
980
|
+
[
|
|
981
|
+
opens,
|
|
982
|
+
'',
|
|
983
|
+
`export interface ${SHAPED} extends Threaded {`,
|
|
984
|
+
` readonly declares?: readonly ${SHAPED}[]`,
|
|
985
|
+
'}',
|
|
986
|
+
'',
|
|
987
|
+
`export const ${CONST}: ${SHAPED} = {`,
|
|
988
|
+
` id: ${quoted(name)},`,
|
|
989
|
+
` at: ${quoted(sheetAt)},`,
|
|
990
|
+
` describes: ${quoted(describes)},`,
|
|
991
|
+
' version: 1,',
|
|
992
|
+
...(kids.size > 0 ? rowLines(' ', [], kids) : [' declares: [],']),
|
|
993
|
+
'}',
|
|
994
|
+
'',
|
|
995
|
+
].join('\n'),
|
|
996
|
+
'utf8',
|
|
997
|
+
)
|
|
998
|
+
stdout.write(
|
|
999
|
+
`create: wrote ${sheetAt}, declaring nothing under it yet.\n` +
|
|
1000
|
+
`create: every ${name} carries what any row carries — a name, an address and a sentence. To give ${name}s a ` +
|
|
1001
|
+
`field of their own, change the shape at the top of ${sheetAt}. What you add lands on this node and on ` +
|
|
1002
|
+
`everything declared under it, and \`npx thread create\` starts asking for it.\n`,
|
|
1003
|
+
)
|
|
1004
|
+
stdout.write(['', ...VERBS, '', ...grows(sheetAt, name), ''].join('\n'))
|
|
1005
|
+
|
|
1006
|
+
exit(0)
|