@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
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
3
|
+
import { argv, cwd, env, stderr, stdin, stdout } from 'node:process';
|
|
4
|
+
import { createInterface, emitKeypressEvents } from 'node:readline';
|
|
5
|
+
import { kind, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "./files.js";
|
|
6
|
+
import { exit, invoked } from "./invoked.js";
|
|
7
|
+
import { THING } from "./scope.js";
|
|
8
|
+
const HERE = cwd();
|
|
9
|
+
const REASONS = {
|
|
10
|
+
ENOENT: 'is not there',
|
|
11
|
+
EACCES: 'is there, and this machine does not allow it',
|
|
12
|
+
EPERM: 'is there, and this machine does not allow it',
|
|
13
|
+
EISDIR: 'is a folder, and that order is for a file',
|
|
14
|
+
ENOTDIR: 'has a file part-way along its path where a folder was expected',
|
|
15
|
+
ENOTEMPTY: 'still holds something',
|
|
16
|
+
};
|
|
17
|
+
const HOME = (() => {
|
|
18
|
+
try {
|
|
19
|
+
return realpathSync(HERE);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return HERE;
|
|
23
|
+
}
|
|
24
|
+
})();
|
|
25
|
+
function deepest(at) {
|
|
26
|
+
let found = at;
|
|
27
|
+
while (kind(found) === 'none') {
|
|
28
|
+
const up = dirname(found);
|
|
29
|
+
if (up === found)
|
|
30
|
+
return found;
|
|
31
|
+
found = up;
|
|
32
|
+
}
|
|
33
|
+
return found;
|
|
34
|
+
}
|
|
35
|
+
function inside(at) {
|
|
36
|
+
let real;
|
|
37
|
+
try {
|
|
38
|
+
real = realpathSync(deepest(resolve(HERE, at)));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const step = relative(HOME, real);
|
|
44
|
+
return step === '' || (!step.startsWith('..') && !isAbsolute(step));
|
|
45
|
+
}
|
|
46
|
+
function rootly(at) {
|
|
47
|
+
const asked = resolve(HERE, at);
|
|
48
|
+
if (kind(asked) === 'none')
|
|
49
|
+
return false;
|
|
50
|
+
try {
|
|
51
|
+
return realpathSync(asked) === HOME;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);
|
|
58
|
+
function local(at) {
|
|
59
|
+
try {
|
|
60
|
+
return LOOPBACK.has(new URL(at).hostname);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function why(at, thrown) {
|
|
67
|
+
const held = thrown instanceof Error ? Reflect.get(thrown, 'code') : undefined;
|
|
68
|
+
const said = typeof held === 'string' ? REASONS[held] : undefined;
|
|
69
|
+
if (said !== undefined)
|
|
70
|
+
return `${at} ${said}.`;
|
|
71
|
+
return `${at} could not be reached — ${thrown instanceof Error ? thrown.message : String(thrown)}.`;
|
|
72
|
+
}
|
|
73
|
+
export function carry(one) {
|
|
74
|
+
if (!inside(one.at)) {
|
|
75
|
+
return {
|
|
76
|
+
answered: 'refused',
|
|
77
|
+
because: `${one.at} is outside ${HERE} once its links are followed, and this answers only for what is really inside the repository it was run in.`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (one.order === 'remove' && rootly(one.at)) {
|
|
81
|
+
return {
|
|
82
|
+
answered: 'refused',
|
|
83
|
+
because: `${one.at} is the repository this was run in, and this carries orders inside it rather than taking it away.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
if (one.order === 'kind')
|
|
88
|
+
return { answered: 'kind', held: kind(one.at) };
|
|
89
|
+
if (one.order === 'read')
|
|
90
|
+
return { answered: 'read', said: readFileSync(one.at) };
|
|
91
|
+
if (one.order === 'list') {
|
|
92
|
+
const held = readdirSync(one.at, { withFileTypes: true }).map((each) => ({
|
|
93
|
+
name: each.name,
|
|
94
|
+
kind: each.isFile() ? 'file' : each.isDirectory() ? 'folder' : 'other',
|
|
95
|
+
}));
|
|
96
|
+
return { answered: 'list', held };
|
|
97
|
+
}
|
|
98
|
+
if (one.order === 'stat') {
|
|
99
|
+
const held = statSync(one.at);
|
|
100
|
+
return { answered: 'stat', held: { size: held.size, modified: held.mtimeMs } };
|
|
101
|
+
}
|
|
102
|
+
if (one.order === 'remove') {
|
|
103
|
+
rmSync(one.at, { recursive: true, force: true });
|
|
104
|
+
return { answered: 'removed' };
|
|
105
|
+
}
|
|
106
|
+
if (one.order === 'real')
|
|
107
|
+
return { answered: 'real', at: realpathSync(one.at) };
|
|
108
|
+
writeFileSync(one.at, one.said);
|
|
109
|
+
return { answered: 'written' };
|
|
110
|
+
}
|
|
111
|
+
catch (thrown) {
|
|
112
|
+
return { answered: 'refused', because: why(one.at, thrown) };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const AT = 'THREAD_SERVER';
|
|
116
|
+
const KEY = 'THREAD_KEY';
|
|
117
|
+
const QUIET = 100;
|
|
118
|
+
async function sending(at, key, body) {
|
|
119
|
+
const said = await fetch(at, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${key}` },
|
|
122
|
+
body: JSON.stringify(body),
|
|
123
|
+
});
|
|
124
|
+
if (said.status === 401 || said.status === 403)
|
|
125
|
+
throw new Error(`the key in ${KEY} is not one ${at} accepts`);
|
|
126
|
+
if (!said.ok)
|
|
127
|
+
throw new Error(`${at} answered ${String(said.status)}`);
|
|
128
|
+
return (await said.json());
|
|
129
|
+
}
|
|
130
|
+
export async function talking(at, key, message) {
|
|
131
|
+
let turn = await sending(at, key, { said: message });
|
|
132
|
+
let silent = 0;
|
|
133
|
+
for (;;) {
|
|
134
|
+
if (turn.say !== undefined && turn.say !== '')
|
|
135
|
+
stdout.write(`${turn.say}\n`);
|
|
136
|
+
if (turn.done === true)
|
|
137
|
+
return 0;
|
|
138
|
+
const orders = turn.orders ?? [];
|
|
139
|
+
silent = orders.length === 0 && (turn.say === undefined || turn.say === '') ? silent + 1 : 0;
|
|
140
|
+
if (silent >= QUIET)
|
|
141
|
+
break;
|
|
142
|
+
turn = await sending(at, key, { of: turn.of, answered: orders.map(carry) });
|
|
143
|
+
}
|
|
144
|
+
stderr.write(`${THING}: nothing has come back from ${at} for ${String(QUIET)} exchanges, so this stopped waiting. What is already\n` +
|
|
145
|
+
`in this repository stands, and nothing half-written was left behind — every change is one order, carried\n` +
|
|
146
|
+
`and answered before the next one is asked for.\n` +
|
|
147
|
+
`This only ever gives up on silence: a server that keeps asking for files never reaches it, so a run that is\n` +
|
|
148
|
+
`going nowhere and still talking is one you stop with ctrl-c.\n`);
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
const PROMPT = '> ';
|
|
152
|
+
async function chatting(at, key) {
|
|
153
|
+
stdout.write(THING +
|
|
154
|
+
': talking to ' +
|
|
155
|
+
at +
|
|
156
|
+
'. Say what you want built, in a sentence — there is no command here to look up.\n' +
|
|
157
|
+
'Press Esc to stop a turn. Type while one is running and what you type goes next, when it finishes.\n' +
|
|
158
|
+
'Ctrl-C to leave.\n\n');
|
|
159
|
+
const queued = [];
|
|
160
|
+
let busy = false;
|
|
161
|
+
const held = createInterface({ input: stdin, output: stdout, prompt: PROMPT });
|
|
162
|
+
emitKeypressEvents(stdin);
|
|
163
|
+
stdin.on('keypress', (...said) => {
|
|
164
|
+
const one = said[1];
|
|
165
|
+
if (busy && one !== undefined && one.name === 'escape')
|
|
166
|
+
void sending(at, key, { stop: true }).catch(() => undefined);
|
|
167
|
+
});
|
|
168
|
+
const next = async () => {
|
|
169
|
+
const one = queued.shift();
|
|
170
|
+
if (one === undefined) {
|
|
171
|
+
busy = false;
|
|
172
|
+
held.prompt();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
busy = true;
|
|
176
|
+
try {
|
|
177
|
+
await talking(at, key, one);
|
|
178
|
+
}
|
|
179
|
+
catch (thrown) {
|
|
180
|
+
stderr.write(THING + ': ' + at + ' could not answer — ' + (thrown instanceof Error ? thrown.message : String(thrown)) + '. Nothing was written.\n');
|
|
181
|
+
}
|
|
182
|
+
await next();
|
|
183
|
+
};
|
|
184
|
+
held.on('line', (line) => {
|
|
185
|
+
const one = line.trim();
|
|
186
|
+
if (one.length === 0) {
|
|
187
|
+
if (!busy)
|
|
188
|
+
held.prompt();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
queued.push(one);
|
|
192
|
+
if (busy) {
|
|
193
|
+
stdout.write(THING + ': queued — it goes next.\n');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
void next();
|
|
197
|
+
});
|
|
198
|
+
held.prompt();
|
|
199
|
+
return await new Promise((settle) => {
|
|
200
|
+
held.on('close', () => {
|
|
201
|
+
settle(0);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
function pointed() {
|
|
206
|
+
const at = env[AT];
|
|
207
|
+
if (at === undefined || at.length === 0) {
|
|
208
|
+
stderr.write(`${THING}: there is nowhere to send that yet. This copy has no server to answer it — ${AT} names one, and\n` +
|
|
209
|
+
`nothing here is set to one.\n`);
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
if (!local(at)) {
|
|
213
|
+
stderr.write(`${THING}: ${AT} names ${at}, which is not on this machine, and this only ever talks to a server running\n` +
|
|
214
|
+
`here. Set ${AT} to a http://localhost address and say it again. Nothing was sent.\n`);
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
return at;
|
|
218
|
+
}
|
|
219
|
+
export async function run(said) {
|
|
220
|
+
const at = pointed();
|
|
221
|
+
if (at === undefined)
|
|
222
|
+
return 1;
|
|
223
|
+
const key = env[KEY] ?? '';
|
|
224
|
+
if (said.length === 0)
|
|
225
|
+
return await chatting(at, key);
|
|
226
|
+
const message = said.length === 1 ? (said[0] ?? '') : '';
|
|
227
|
+
if (!message.includes(' ')) {
|
|
228
|
+
stderr.write(`${THING}: there is no \`${said.join(' ')}\` here, and no other command to find instead. This takes one\n` +
|
|
229
|
+
`sentence, in quotes, and nothing else — ${THING} "take payments with stripe". Say what you want and it\n` +
|
|
230
|
+
`is answered where the work happens, never on this machine.\n`);
|
|
231
|
+
return 1;
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
return await talking(at, key, message);
|
|
235
|
+
}
|
|
236
|
+
catch (thrown) {
|
|
237
|
+
stderr.write(`${THING}: ${at} could not answer — ${thrown instanceof Error ? thrown.message : String(thrown)}. Nothing was\n` +
|
|
238
|
+
`written, so this repository is as it was.\n`);
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (invoked(import.meta.url))
|
|
243
|
+
exit(await run(argv.slice(2)));
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve } from 'node:path'
|
|
3
|
+
import { argv, cwd, env, stderr, stdin, stdout } from 'node:process'
|
|
4
|
+
import { createInterface, emitKeypressEvents } from 'node:readline'
|
|
5
|
+
import { kind, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync, type Kind, type Listed, type Measured } from './files.ts'
|
|
6
|
+
import { exit, invoked } from './invoked.ts'
|
|
7
|
+
import { THING } from './scope.ts'
|
|
8
|
+
|
|
9
|
+
export type Order =
|
|
10
|
+
| { readonly order: 'kind'; readonly at: string }
|
|
11
|
+
| { readonly order: 'read'; readonly at: string }
|
|
12
|
+
| { readonly order: 'list'; readonly at: string }
|
|
13
|
+
| { readonly order: 'stat'; readonly at: string }
|
|
14
|
+
| { readonly order: 'remove'; readonly at: string }
|
|
15
|
+
| { readonly order: 'real'; readonly at: string }
|
|
16
|
+
| { readonly order: 'write'; readonly at: string; readonly said: string }
|
|
17
|
+
|
|
18
|
+
export type Answered =
|
|
19
|
+
| { readonly answered: 'kind'; readonly held: Kind }
|
|
20
|
+
| { readonly answered: 'read'; readonly said: string }
|
|
21
|
+
| { readonly answered: 'list'; readonly held: readonly Listed[] }
|
|
22
|
+
| { readonly answered: 'stat'; readonly held: Measured }
|
|
23
|
+
| { readonly answered: 'written' }
|
|
24
|
+
| { readonly answered: 'removed' }
|
|
25
|
+
| { readonly answered: 'real'; readonly at: string }
|
|
26
|
+
| { readonly answered: 'refused'; readonly because: string }
|
|
27
|
+
|
|
28
|
+
const HERE = cwd()
|
|
29
|
+
|
|
30
|
+
const REASONS: Readonly<Record<string, string>> = {
|
|
31
|
+
ENOENT: 'is not there',
|
|
32
|
+
EACCES: 'is there, and this machine does not allow it',
|
|
33
|
+
EPERM: 'is there, and this machine does not allow it',
|
|
34
|
+
EISDIR: 'is a folder, and that order is for a file',
|
|
35
|
+
ENOTDIR: 'has a file part-way along its path where a folder was expected',
|
|
36
|
+
ENOTEMPTY: 'still holds something',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const HOME = ((): string => {
|
|
40
|
+
try {
|
|
41
|
+
return realpathSync(HERE)
|
|
42
|
+
} catch {
|
|
43
|
+
return HERE
|
|
44
|
+
}
|
|
45
|
+
})()
|
|
46
|
+
|
|
47
|
+
function deepest(at: string): string {
|
|
48
|
+
let found = at
|
|
49
|
+
while (kind(found) === 'none') {
|
|
50
|
+
const up = dirname(found)
|
|
51
|
+
if (up === found) return found
|
|
52
|
+
found = up
|
|
53
|
+
}
|
|
54
|
+
return found
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function inside(at: string): boolean {
|
|
58
|
+
let real: string
|
|
59
|
+
try {
|
|
60
|
+
real = realpathSync(deepest(resolve(HERE, at)))
|
|
61
|
+
} catch {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
const step = relative(HOME, real)
|
|
65
|
+
return step === '' || (!step.startsWith('..') && !isAbsolute(step))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function rootly(at: string): boolean {
|
|
69
|
+
const asked = resolve(HERE, at)
|
|
70
|
+
if (kind(asked) === 'none') return false
|
|
71
|
+
try {
|
|
72
|
+
return realpathSync(asked) === HOME
|
|
73
|
+
} catch {
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]', '::1'])
|
|
79
|
+
|
|
80
|
+
function local(at: string): boolean {
|
|
81
|
+
try {
|
|
82
|
+
return LOOPBACK.has(new URL(at).hostname)
|
|
83
|
+
} catch {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function why(at: string, thrown: unknown): string {
|
|
89
|
+
const held: unknown = thrown instanceof Error ? Reflect.get(thrown, 'code') : undefined
|
|
90
|
+
const said = typeof held === 'string' ? REASONS[held] : undefined
|
|
91
|
+
if (said !== undefined) return `${at} ${said}.`
|
|
92
|
+
return `${at} could not be reached — ${thrown instanceof Error ? thrown.message : String(thrown)}.`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function carry(one: Order): Answered {
|
|
96
|
+
if (!inside(one.at)) {
|
|
97
|
+
return {
|
|
98
|
+
answered: 'refused',
|
|
99
|
+
because: `${one.at} is outside ${HERE} once its links are followed, and this answers only for what is really inside the repository it was run in.`,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (one.order === 'remove' && rootly(one.at)) {
|
|
103
|
+
return {
|
|
104
|
+
answered: 'refused',
|
|
105
|
+
because: `${one.at} is the repository this was run in, and this carries orders inside it rather than taking it away.`,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
if (one.order === 'kind') return { answered: 'kind', held: kind(one.at) }
|
|
110
|
+
if (one.order === 'read') return { answered: 'read', said: readFileSync(one.at) }
|
|
111
|
+
if (one.order === 'list') {
|
|
112
|
+
const held = readdirSync(one.at, { withFileTypes: true }).map((each) => ({
|
|
113
|
+
name: each.name,
|
|
114
|
+
kind: each.isFile() ? ('file' as const) : each.isDirectory() ? ('folder' as const) : ('other' as const),
|
|
115
|
+
}))
|
|
116
|
+
return { answered: 'list', held }
|
|
117
|
+
}
|
|
118
|
+
if (one.order === 'stat') {
|
|
119
|
+
const held = statSync(one.at)
|
|
120
|
+
return { answered: 'stat', held: { size: held.size, modified: held.mtimeMs } }
|
|
121
|
+
}
|
|
122
|
+
if (one.order === 'remove') {
|
|
123
|
+
rmSync(one.at, { recursive: true, force: true })
|
|
124
|
+
return { answered: 'removed' }
|
|
125
|
+
}
|
|
126
|
+
if (one.order === 'real') return { answered: 'real', at: realpathSync(one.at) }
|
|
127
|
+
writeFileSync(one.at, one.said)
|
|
128
|
+
return { answered: 'written' }
|
|
129
|
+
} catch (thrown) {
|
|
130
|
+
return { answered: 'refused', because: why(one.at, thrown) }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface Turn {
|
|
135
|
+
readonly of: string
|
|
136
|
+
readonly orders?: readonly Order[]
|
|
137
|
+
readonly say?: string
|
|
138
|
+
readonly done?: boolean
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const AT = 'THREAD_SERVER'
|
|
142
|
+
|
|
143
|
+
const KEY = 'THREAD_KEY'
|
|
144
|
+
|
|
145
|
+
const QUIET = 100
|
|
146
|
+
|
|
147
|
+
async function sending(at: string, key: string, body: unknown): Promise<Turn> {
|
|
148
|
+
const said = await fetch(at, {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${key}` },
|
|
151
|
+
body: JSON.stringify(body),
|
|
152
|
+
})
|
|
153
|
+
if (said.status === 401 || said.status === 403) throw new Error(`the key in ${KEY} is not one ${at} accepts`)
|
|
154
|
+
if (!said.ok) throw new Error(`${at} answered ${String(said.status)}`)
|
|
155
|
+
return (await said.json()) as Turn
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function talking(at: string, key: string, message: string): Promise<number> {
|
|
159
|
+
let turn = await sending(at, key, { said: message })
|
|
160
|
+
let silent = 0
|
|
161
|
+
for (;;) {
|
|
162
|
+
if (turn.say !== undefined && turn.say !== '') stdout.write(`${turn.say}\n`)
|
|
163
|
+
if (turn.done === true) return 0
|
|
164
|
+
const orders = turn.orders ?? []
|
|
165
|
+
silent = orders.length === 0 && (turn.say === undefined || turn.say === '') ? silent + 1 : 0
|
|
166
|
+
if (silent >= QUIET) break
|
|
167
|
+
turn = await sending(at, key, { of: turn.of, answered: orders.map(carry) })
|
|
168
|
+
}
|
|
169
|
+
stderr.write(
|
|
170
|
+
`${THING}: nothing has come back from ${at} for ${String(QUIET)} exchanges, so this stopped waiting. What is already\n` +
|
|
171
|
+
`in this repository stands, and nothing half-written was left behind — every change is one order, carried\n` +
|
|
172
|
+
`and answered before the next one is asked for.\n` +
|
|
173
|
+
`This only ever gives up on silence: a server that keeps asking for files never reaches it, so a run that is\n` +
|
|
174
|
+
`going nowhere and still talking is one you stop with ctrl-c.\n`,
|
|
175
|
+
)
|
|
176
|
+
return 1
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const PROMPT = '> '
|
|
180
|
+
|
|
181
|
+
async function chatting(at: string, key: string): Promise<number> {
|
|
182
|
+
stdout.write(
|
|
183
|
+
THING +
|
|
184
|
+
': talking to ' +
|
|
185
|
+
at +
|
|
186
|
+
'. Say what you want built, in a sentence — there is no command here to look up.\n' +
|
|
187
|
+
'Press Esc to stop a turn. Type while one is running and what you type goes next, when it finishes.\n' +
|
|
188
|
+
'Ctrl-C to leave.\n\n',
|
|
189
|
+
)
|
|
190
|
+
const queued: string[] = []
|
|
191
|
+
let busy = false
|
|
192
|
+
const held = createInterface({ input: stdin, output: stdout, prompt: PROMPT })
|
|
193
|
+
emitKeypressEvents(stdin)
|
|
194
|
+
stdin.on('keypress', (...said: readonly unknown[]) => {
|
|
195
|
+
const one = said[1] as { readonly name?: string } | undefined
|
|
196
|
+
if (busy && one !== undefined && one.name === 'escape') void sending(at, key, { stop: true }).catch(() => undefined)
|
|
197
|
+
})
|
|
198
|
+
const next = async (): Promise<void> => {
|
|
199
|
+
const one = queued.shift()
|
|
200
|
+
if (one === undefined) {
|
|
201
|
+
busy = false
|
|
202
|
+
held.prompt()
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
busy = true
|
|
206
|
+
try {
|
|
207
|
+
await talking(at, key, one)
|
|
208
|
+
} catch (thrown) {
|
|
209
|
+
stderr.write(THING + ': ' + at + ' could not answer — ' + (thrown instanceof Error ? thrown.message : String(thrown)) + '. Nothing was written.\n')
|
|
210
|
+
}
|
|
211
|
+
await next()
|
|
212
|
+
}
|
|
213
|
+
held.on('line', (line: string) => {
|
|
214
|
+
const one = line.trim()
|
|
215
|
+
if (one.length === 0) {
|
|
216
|
+
if (!busy) held.prompt()
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
queued.push(one)
|
|
220
|
+
if (busy) {
|
|
221
|
+
stdout.write(THING + ': queued — it goes next.\n')
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
void next()
|
|
225
|
+
})
|
|
226
|
+
held.prompt()
|
|
227
|
+
return await new Promise<number>((settle) => {
|
|
228
|
+
held.on('close', () => {
|
|
229
|
+
settle(0)
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function pointed(): string | undefined {
|
|
235
|
+
const at = env[AT]
|
|
236
|
+
if (at === undefined || at.length === 0) {
|
|
237
|
+
stderr.write(
|
|
238
|
+
`${THING}: there is nowhere to send that yet. This copy has no server to answer it — ${AT} names one, and\n` +
|
|
239
|
+
`nothing here is set to one.\n`,
|
|
240
|
+
)
|
|
241
|
+
return undefined
|
|
242
|
+
}
|
|
243
|
+
if (!local(at)) {
|
|
244
|
+
stderr.write(
|
|
245
|
+
`${THING}: ${AT} names ${at}, which is not on this machine, and this only ever talks to a server running\n` +
|
|
246
|
+
`here. Set ${AT} to a http://localhost address and say it again. Nothing was sent.\n`,
|
|
247
|
+
)
|
|
248
|
+
return undefined
|
|
249
|
+
}
|
|
250
|
+
return at
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function run(said: readonly string[]): Promise<number> {
|
|
254
|
+
const at = pointed()
|
|
255
|
+
if (at === undefined) return 1
|
|
256
|
+
const key = env[KEY] ?? ''
|
|
257
|
+
if (said.length === 0) return await chatting(at, key)
|
|
258
|
+
const message = said.length === 1 ? (said[0] ?? '') : ''
|
|
259
|
+
if (!message.includes(' ')) {
|
|
260
|
+
stderr.write(
|
|
261
|
+
`${THING}: there is no \`${said.join(' ')}\` here, and no other command to find instead. This takes one\n` +
|
|
262
|
+
`sentence, in quotes, and nothing else — ${THING} "take payments with stripe". Say what you want and it\n` +
|
|
263
|
+
`is answered where the work happens, never on this machine.\n`,
|
|
264
|
+
)
|
|
265
|
+
return 1
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
return await talking(at, key, message)
|
|
270
|
+
} catch (thrown) {
|
|
271
|
+
stderr.write(
|
|
272
|
+
`${THING}: ${at} could not answer — ${thrown instanceof Error ? thrown.message : String(thrown)}. Nothing was\n` +
|
|
273
|
+
`written, so this repository is as it was.\n`,
|
|
274
|
+
)
|
|
275
|
+
return 1
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (invoked(import.meta.url)) exit(await run(argv.slice(2)))
|
package/dist/core/descend.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export declare const TREE: string;
|
|
|
5
5
|
export declare const THREAD_AT: string;
|
|
6
6
|
export declare function unkept(at: string): boolean;
|
|
7
7
|
export declare const LISTS: readonly ["declares", "derives", "seeds"];
|
|
8
|
+
export declare const ROW_LISTS: readonly ["declares", "derives", "seeds", "holds"];
|
|
8
9
|
export interface Row {
|
|
9
10
|
readonly at?: string;
|
|
10
11
|
readonly id?: string;
|
|
@@ -46,6 +47,7 @@ export declare function configured(): {
|
|
|
46
47
|
readonly from: string;
|
|
47
48
|
readonly trouble?: string;
|
|
48
49
|
};
|
|
50
|
+
export declare function hosting(options: ts.CompilerOptions): ts.CompilerHost;
|
|
49
51
|
export declare function uncompiled(): readonly Finding[];
|
|
50
52
|
export declare function read(at: string): string | undefined;
|
|
51
53
|
export declare function shapesNamed(): ReadonlyMap<string, readonly string[]>;
|