@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.
Files changed (183) hide show
  1. package/CHANGELOG.md +310 -0
  2. package/dist/core/{asking.d.ts → answers.d.ts} +6 -14
  3. package/dist/core/{asking.js → answers.js} +13 -332
  4. package/dist/core/{asking.ts → answers.ts} +14 -357
  5. package/dist/core/attach.js +2 -2
  6. package/dist/core/attach.ts +2 -3
  7. package/dist/core/chain.js +4 -11
  8. package/dist/core/chain.ts +4 -3
  9. package/dist/core/client.d.ts +57 -0
  10. package/dist/core/client.js +243 -0
  11. package/dist/core/client.ts +279 -0
  12. package/dist/core/descend.d.ts +2 -0
  13. package/dist/core/descend.js +109 -16
  14. package/dist/core/descend.ts +120 -16
  15. package/dist/core/files.d.ts +45 -0
  16. package/dist/core/files.js +145 -0
  17. package/dist/core/files.ts +193 -0
  18. package/dist/core/init.d.ts +1 -0
  19. package/dist/core/init.js +10 -2
  20. package/dist/core/init.ts +8 -2
  21. package/dist/core/invoked.js +1 -1
  22. package/dist/core/invoked.ts +1 -1
  23. package/dist/core/rows.d.ts +2 -0
  24. package/dist/core/rows.js +65 -0
  25. package/dist/core/rows.ts +58 -0
  26. package/dist/core/tree.js +1 -1
  27. package/dist/core/tree.ts +1 -1
  28. package/dist/derivation/attach.js +0 -12
  29. package/dist/derivation/attach.ts +0 -12
  30. package/dist/derivation/generators/build.js +8 -7
  31. package/dist/derivation/generators/build.ts +8 -7
  32. package/dist/derivation/generators/release.d.ts +1 -1
  33. package/dist/derivation/generators/release.js +15 -23
  34. package/dist/derivation/generators/release.ts +15 -15
  35. package/dist/integrations/attach.js +1 -19
  36. package/dist/integrations/attach.ts +1 -19
  37. package/dist/integrations/generators/read.js +2 -2
  38. package/dist/integrations/generators/read.ts +4 -4
  39. package/dist/integrations/generators/vault.js +4 -12
  40. package/dist/integrations/generators/vault.ts +4 -4
  41. package/dist/integrations/module.js +5 -3
  42. package/dist/integrations/module.ts +5 -3
  43. package/dist/integrations/vault.js +5 -5
  44. package/dist/integrations/vault.ts +5 -5
  45. package/dist/integrity/attach.js +0 -12
  46. package/dist/integrity/attach.ts +0 -12
  47. package/dist/integrity/check-threads.js +1 -1
  48. package/dist/integrity/check-threads.ts +1 -1
  49. package/dist/integrity/constraint.d.ts +11 -2
  50. package/dist/integrity/constraint.js +163 -20
  51. package/dist/integrity/constraint.ts +174 -22
  52. package/dist/integrity/generators/compiler.js +7 -1
  53. package/dist/integrity/generators/compiler.ts +10 -1
  54. package/dist/integrity/generators/gate.js +23 -18
  55. package/dist/integrity/generators/gate.ts +29 -9
  56. package/dist/integrity/generators/publish.js +14 -26
  57. package/dist/integrity/generators/publish.ts +14 -17
  58. package/dist/integrity/generators/trial.d.ts +2 -2
  59. package/dist/integrity/generators/trial.js +108 -50
  60. package/dist/integrity/generators/trial.ts +107 -48
  61. package/dist/integrity/lint.js +3 -3
  62. package/dist/integrity/lint.ts +3 -3
  63. package/dist/integrity/lsp/diagnostics.js +1 -1
  64. package/dist/integrity/lsp/diagnostics.ts +1 -1
  65. package/dist/integrity/lsp/lint.js +1 -1
  66. package/dist/integrity/lsp/lint.ts +1 -1
  67. package/dist/integrity/slopnator.d.ts +1 -14
  68. package/dist/integrity/slopnator.js +8 -40
  69. package/dist/integrity/slopnator.ts +9 -51
  70. package/dist/integrity/slopnators/sl_broken_lineage.js +4 -2
  71. package/dist/integrity/slopnators/sl_broken_lineage.ts +4 -2
  72. package/dist/integrity/slopnators/sl_declared_elsewhere.js +24 -3
  73. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +24 -3
  74. package/dist/integrity/slopnators/sl_derived_drift.js +10 -4
  75. package/dist/integrity/slopnators/sl_derived_drift.ts +10 -4
  76. package/dist/integrity/slopnators/sl_inert_island.js +7 -13
  77. package/dist/integrity/slopnators/sl_inert_island.ts +7 -16
  78. package/dist/integrity/slopnators/sl_inert_slopnator.js +8 -8
  79. package/dist/integrity/slopnators/sl_inert_slopnator.ts +8 -8
  80. package/dist/integrity/slopnators/sl_missing_row.js +14 -9
  81. package/dist/integrity/slopnators/sl_missing_row.ts +14 -10
  82. package/dist/integrity/slopnators/sl_no_child_process.js +8 -8
  83. package/dist/integrity/slopnators/sl_no_child_process.ts +8 -8
  84. package/dist/integrity/slopnators/sl_no_import_cycle.js +1 -1
  85. package/dist/integrity/slopnators/sl_no_import_cycle.ts +1 -1
  86. package/dist/integrity/slopnators/sl_no_such_command.js +20 -5
  87. package/dist/integrity/slopnators/sl_no_such_command.ts +22 -5
  88. package/dist/integrity/slopnators/sl_only_takes_a_literal.js +8 -8
  89. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +8 -8
  90. package/dist/integrity/slopnators/sl_stale_path.js +91 -18
  91. package/dist/integrity/slopnators/sl_stale_path.ts +97 -18
  92. package/dist/integrity/slopnators/sl_twice_meant.js +25 -2
  93. package/dist/integrity/slopnators/sl_twice_meant.ts +28 -2
  94. package/dist/integrity/slopnators/sl_twice_parented.js +26 -5
  95. package/dist/integrity/slopnators/sl_twice_parented.ts +26 -5
  96. package/dist/integrity/slopnators/sl_undeclared_node.js +18 -2
  97. package/dist/integrity/slopnators/sl_undeclared_node.ts +18 -2
  98. package/dist/integrity/slopnators/sl_undeclared_operation.js +3 -1
  99. package/dist/integrity/slopnators/sl_undeclared_operation.ts +3 -1
  100. package/dist/integrity/slopnators/sl_undeclared_variant.js +6 -2
  101. package/dist/integrity/slopnators/sl_undeclared_variant.ts +6 -2
  102. package/dist/integrity/slopnators/sl_ungoverned_island.js +3 -4
  103. package/dist/integrity/slopnators/sl_ungoverned_island.ts +3 -4
  104. package/dist/integrity/slopnators/sl_unkept_rows.js +1 -1
  105. package/dist/integrity/slopnators/sl_unkept_rows.ts +1 -1
  106. package/dist/integrity/slopnators/sl_unpaired_member.js +13 -8
  107. package/dist/integrity/slopnators/sl_unpaired_member.ts +16 -8
  108. package/dist/integrity/slopnators/sl_unpaired_slopnator.js +16 -3
  109. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +16 -3
  110. package/dist/integrity/slopnators/sl_unthreaded.d.ts +1 -0
  111. package/dist/integrity/slopnators/sl_unthreaded.js +18 -10
  112. package/dist/integrity/slopnators/sl_unthreaded.ts +19 -11
  113. package/dist/integrity/slopnators/sl_untranslated_key.js +1 -1
  114. package/dist/integrity/slopnators/sl_untranslated_key.ts +1 -1
  115. package/dist/integrity/slopnators/sl_unversioned.js +16 -2
  116. package/dist/integrity/slopnators/sl_unversioned.ts +16 -2
  117. package/dist/integrity/sweep.js +17 -3
  118. package/dist/integrity/sweep.ts +20 -3
  119. package/dist/knowledge/attach.js +0 -6
  120. package/dist/knowledge/attach.ts +0 -6
  121. package/dist/knowledge/generators/docsgen.js +12 -3
  122. package/dist/knowledge/generators/docsgen.ts +12 -2
  123. package/dist/knowledge/generators/glossary.js +1 -1
  124. package/dist/knowledge/generators/glossary.ts +1 -1
  125. package/dist/operations/attach.js +2 -26
  126. package/dist/operations/attach.ts +2 -27
  127. package/dist/operations/dispatch.js +2 -3
  128. package/dist/operations/dispatch.ts +2 -3
  129. package/dist/operations/generators/capability.js +4 -7
  130. package/dist/operations/generators/capability.ts +3 -6
  131. package/dist/operations/generators/compile.js +8 -13
  132. package/dist/operations/generators/compile.ts +8 -4
  133. package/dist/operations/generators/permits.js +4 -13
  134. package/dist/operations/generators/permits.ts +3 -3
  135. package/dist/operations/generators/seed.js +5 -2
  136. package/dist/operations/generators/seed.ts +6 -2
  137. package/dist/operations/generators/trial.js +10 -10
  138. package/dist/operations/generators/trial.ts +10 -10
  139. package/dist/operations/http.js +4 -1
  140. package/dist/operations/http.ts +6 -1
  141. package/package.json +2 -2
  142. package/dist/core/cli.d.ts +0 -2
  143. package/dist/core/cli.js +0 -1761
  144. package/dist/core/cli.ts +0 -2004
  145. package/dist/core/generators/meta.d.ts +0 -2
  146. package/dist/core/generators/meta.js +0 -829
  147. package/dist/core/generators/meta.ts +0 -1006
  148. package/dist/derivation/generators/derivation.d.ts +0 -3
  149. package/dist/derivation/generators/derivation.js +0 -273
  150. package/dist/derivation/generators/derivation.ts +0 -347
  151. package/dist/derivation/makers/release.d.ts +0 -1
  152. package/dist/derivation/makers/release.js +0 -107
  153. package/dist/derivation/makers/release.ts +0 -135
  154. package/dist/integrations/generators/secret.d.ts +0 -1
  155. package/dist/integrations/generators/secret.js +0 -233
  156. package/dist/integrations/generators/secret.ts +0 -289
  157. package/dist/integrations/generators/target.d.ts +0 -1
  158. package/dist/integrations/generators/target.js +0 -775
  159. package/dist/integrations/generators/target.ts +0 -944
  160. package/dist/integrity/generators/rule.d.ts +0 -2
  161. package/dist/integrity/generators/rule.js +0 -213
  162. package/dist/integrity/generators/rule.ts +0 -271
  163. package/dist/integrity/generators/seal.d.ts +0 -4
  164. package/dist/integrity/generators/seal.js +0 -101
  165. package/dist/integrity/generators/seal.ts +0 -117
  166. package/dist/integrity/sealed.d.ts +0 -1
  167. package/dist/integrity/sealed.js +0 -56
  168. package/dist/integrity/sealed.ts +0 -56
  169. package/dist/knowledge/generators/skills.d.ts +0 -1
  170. package/dist/knowledge/generators/skills.js +0 -222
  171. package/dist/knowledge/generators/skills.ts +0 -263
  172. package/dist/operations/generators/grant.d.ts +0 -1
  173. package/dist/operations/generators/grant.js +0 -267
  174. package/dist/operations/generators/grant.ts +0 -336
  175. package/dist/operations/generators/operation.d.ts +0 -1
  176. package/dist/operations/generators/operation.js +0 -811
  177. package/dist/operations/generators/operation.ts +0 -972
  178. package/dist/operations/makers/capability.d.ts +0 -1
  179. package/dist/operations/makers/capability.js +0 -117
  180. package/dist/operations/makers/capability.ts +0 -166
  181. package/dist/operations/makers/trial.d.ts +0 -1
  182. package/dist/operations/makers/trial.js +0 -413
  183. 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)))
@@ -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[]>;