@prestack/thread 0.0.2 → 0.1.0

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 +98 -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 +186 -0
  11. package/dist/core/client.ts +227 -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 +95 -0
  17. package/dist/core/files.ts +137 -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 +1 -1
  53. package/dist/integrity/generators/compiler.ts +1 -1
  54. package/dist/integrity/generators/gate.js +8 -16
  55. package/dist/integrity/generators/gate.ts +8 -7
  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,186 @@
1
+ #!/usr/bin/env node
2
+ import { dirname, isAbsolute, relative, resolve } from 'node:path';
3
+ import { argv, cwd, env, stderr, stdout } from 'node:process';
4
+ import { kind, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "./files.js";
5
+ import { exit, invoked } from "./invoked.js";
6
+ import { THING } from "./scope.js";
7
+ const HERE = cwd();
8
+ const REASONS = {
9
+ ENOENT: 'is not there',
10
+ EACCES: 'is there, and this machine does not allow it',
11
+ EPERM: 'is there, and this machine does not allow it',
12
+ EISDIR: 'is a folder, and that order is for a file',
13
+ ENOTDIR: 'has a file part-way along its path where a folder was expected',
14
+ ENOTEMPTY: 'still holds something',
15
+ };
16
+ const HOME = (() => {
17
+ try {
18
+ return realpathSync(HERE);
19
+ }
20
+ catch {
21
+ return HERE;
22
+ }
23
+ })();
24
+ function deepest(at) {
25
+ let found = at;
26
+ while (kind(found) === 'none') {
27
+ const up = dirname(found);
28
+ if (up === found)
29
+ return found;
30
+ found = up;
31
+ }
32
+ return found;
33
+ }
34
+ function inside(at) {
35
+ let real;
36
+ try {
37
+ real = realpathSync(deepest(resolve(HERE, at)));
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ const step = relative(HOME, real);
43
+ return step === '' || (!step.startsWith('..') && !isAbsolute(step));
44
+ }
45
+ function rootly(at) {
46
+ const asked = resolve(HERE, at);
47
+ if (kind(asked) === 'none')
48
+ return false;
49
+ try {
50
+ return realpathSync(asked) === HOME;
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);
57
+ function local(at) {
58
+ try {
59
+ return LOOPBACK.has(new URL(at).hostname);
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ }
65
+ function why(at, thrown) {
66
+ const held = thrown instanceof Error ? Reflect.get(thrown, 'code') : undefined;
67
+ const said = typeof held === 'string' ? REASONS[held] : undefined;
68
+ if (said !== undefined)
69
+ return `${at} ${said}.`;
70
+ return `${at} could not be reached — ${thrown instanceof Error ? thrown.message : String(thrown)}.`;
71
+ }
72
+ export function carry(one) {
73
+ if (!inside(one.at)) {
74
+ return {
75
+ answered: 'refused',
76
+ 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.`,
77
+ };
78
+ }
79
+ if (one.order === 'remove' && rootly(one.at)) {
80
+ return {
81
+ answered: 'refused',
82
+ because: `${one.at} is the repository this was run in, and this carries orders inside it rather than taking it away.`,
83
+ };
84
+ }
85
+ try {
86
+ if (one.order === 'kind')
87
+ return { answered: 'kind', held: kind(one.at) };
88
+ if (one.order === 'read')
89
+ return { answered: 'read', said: readFileSync(one.at) };
90
+ if (one.order === 'list') {
91
+ const held = readdirSync(one.at, { withFileTypes: true }).map((each) => ({
92
+ name: each.name,
93
+ kind: each.isFile() ? 'file' : each.isDirectory() ? 'folder' : 'other',
94
+ }));
95
+ return { answered: 'list', held };
96
+ }
97
+ if (one.order === 'stat') {
98
+ const held = statSync(one.at);
99
+ return { answered: 'stat', held: { size: held.size, modified: held.mtimeMs } };
100
+ }
101
+ if (one.order === 'remove') {
102
+ rmSync(one.at, { recursive: true, force: true });
103
+ return { answered: 'removed' };
104
+ }
105
+ if (one.order === 'real')
106
+ return { answered: 'real', at: realpathSync(one.at) };
107
+ writeFileSync(one.at, one.said);
108
+ return { answered: 'written' };
109
+ }
110
+ catch (thrown) {
111
+ return { answered: 'refused', because: why(one.at, thrown) };
112
+ }
113
+ }
114
+ const AT = 'THREAD_SERVER';
115
+ const KEY = 'THREAD_KEY';
116
+ const ROUNDS = 100;
117
+ async function sending(at, key, body) {
118
+ const said = await fetch(at, {
119
+ method: 'POST',
120
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${key}` },
121
+ body: JSON.stringify(body),
122
+ });
123
+ if (said.status === 401 || said.status === 403)
124
+ throw new Error(`the key in ${KEY} is not one ${at} accepts`);
125
+ if (!said.ok)
126
+ throw new Error(`${at} answered ${String(said.status)}`);
127
+ return (await said.json());
128
+ }
129
+ export async function talking(at, key, message) {
130
+ let turn = await sending(at, key, { said: message });
131
+ for (let spun = 0; spun < ROUNDS; spun += 1) {
132
+ if (turn.say !== undefined && turn.say !== '')
133
+ stdout.write(`${turn.say}\n`);
134
+ if (turn.done === true)
135
+ return 0;
136
+ turn = await sending(at, key, { of: turn.of, answered: (turn.orders ?? []).map(carry) });
137
+ }
138
+ stderr.write(`${THING}: this went ${String(ROUNDS)} rounds without finishing, so it was stopped there. What is already\n` +
139
+ `in this repository stands, and nothing half-written was left behind — every change is one order, carried\n` +
140
+ `and answered before the next one is asked for.\n`);
141
+ return 1;
142
+ }
143
+ export async function run(said) {
144
+ if (said.length === 0) {
145
+ stdout.write(`${THING}: say what you want built, in one sentence, in quotes.\n\n ${THING} "take payments with stripe"\n\n` +
146
+ `That sentence is the whole of what this takes — there is no command here to look up. Nothing on this\n` +
147
+ `machine declares a row, writes a file or runs a check. What comes back is written into this repository\n` +
148
+ `for you, and every check this framework runs has to pass before it stays.\n`);
149
+ return 0;
150
+ }
151
+ const message = said.length === 1 ? (said[0] ?? '') : '';
152
+ if (!message.includes(' ')) {
153
+ stderr.write(`${THING}: there is no \`${said.join(' ')}\` here, and no other command to find instead. This takes one\n` +
154
+ `sentence, in quotes, and nothing else — ${THING} "take payments with stripe". Say what you want and it\n` +
155
+ `is answered where the work happens, never on this machine.\n`);
156
+ return 1;
157
+ }
158
+ const at = env[AT];
159
+ if (at === undefined || at === '') {
160
+ stderr.write(`${THING}: there is nowhere to send that yet. This copy has no server to answer it — ${AT} names one, and\n` +
161
+ `nothing here is set to one.\n`);
162
+ return 1;
163
+ }
164
+ if (!local(at)) {
165
+ stderr.write(`${THING}: ${AT} names ${at}, which is not on this machine, and this only ever talks to a server running\n` +
166
+ `here. Set ${AT} to a http://localhost address and say it again. Nothing was sent.\n`);
167
+ return 1;
168
+ }
169
+ const key = env[KEY];
170
+ if (key === undefined || key === '') {
171
+ stderr.write(`${THING}: nothing here says who is asking, and ${at} answers no one it cannot name. ${KEY} carries your\n` +
172
+ `key, and nothing on this machine is set to one. Set it in this terminal and say it again — it is read\n` +
173
+ `from there, never written into this repository and never sent anywhere but ${at}.\n`);
174
+ return 1;
175
+ }
176
+ try {
177
+ return await talking(at, key, message);
178
+ }
179
+ catch (thrown) {
180
+ stderr.write(`${THING}: ${at} could not answer — ${thrown instanceof Error ? thrown.message : String(thrown)}. Nothing was\n` +
181
+ `written, so this repository is as it was.\n`);
182
+ return 1;
183
+ }
184
+ }
185
+ if (invoked(import.meta.url))
186
+ exit(await run(argv.slice(2)));
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ import { dirname, isAbsolute, relative, resolve } from 'node:path'
3
+ import { argv, cwd, env, stderr, stdout } from 'node:process'
4
+ import { kind, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync, type Kind, type Listed, type Measured } from './files.ts'
5
+ import { exit, invoked } from './invoked.ts'
6
+ import { THING } from './scope.ts'
7
+
8
+ export type Order =
9
+ | { readonly order: 'kind'; readonly at: string }
10
+ | { readonly order: 'read'; readonly at: string }
11
+ | { readonly order: 'list'; readonly at: string }
12
+ | { readonly order: 'stat'; readonly at: string }
13
+ | { readonly order: 'remove'; readonly at: string }
14
+ | { readonly order: 'real'; readonly at: string }
15
+ | { readonly order: 'write'; readonly at: string; readonly said: string }
16
+
17
+ export type Answered =
18
+ | { readonly answered: 'kind'; readonly held: Kind }
19
+ | { readonly answered: 'read'; readonly said: string }
20
+ | { readonly answered: 'list'; readonly held: readonly Listed[] }
21
+ | { readonly answered: 'stat'; readonly held: Measured }
22
+ | { readonly answered: 'written' }
23
+ | { readonly answered: 'removed' }
24
+ | { readonly answered: 'real'; readonly at: string }
25
+ | { readonly answered: 'refused'; readonly because: string }
26
+
27
+ const HERE = cwd()
28
+
29
+ const REASONS: Readonly<Record<string, string>> = {
30
+ ENOENT: 'is not there',
31
+ EACCES: 'is there, and this machine does not allow it',
32
+ EPERM: 'is there, and this machine does not allow it',
33
+ EISDIR: 'is a folder, and that order is for a file',
34
+ ENOTDIR: 'has a file part-way along its path where a folder was expected',
35
+ ENOTEMPTY: 'still holds something',
36
+ }
37
+
38
+ const HOME = ((): string => {
39
+ try {
40
+ return realpathSync(HERE)
41
+ } catch {
42
+ return HERE
43
+ }
44
+ })()
45
+
46
+ function deepest(at: string): string {
47
+ let found = at
48
+ while (kind(found) === 'none') {
49
+ const up = dirname(found)
50
+ if (up === found) return found
51
+ found = up
52
+ }
53
+ return found
54
+ }
55
+
56
+ function inside(at: string): boolean {
57
+ let real: string
58
+ try {
59
+ real = realpathSync(deepest(resolve(HERE, at)))
60
+ } catch {
61
+ return false
62
+ }
63
+ const step = relative(HOME, real)
64
+ return step === '' || (!step.startsWith('..') && !isAbsolute(step))
65
+ }
66
+
67
+ function rootly(at: string): boolean {
68
+ const asked = resolve(HERE, at)
69
+ if (kind(asked) === 'none') return false
70
+ try {
71
+ return realpathSync(asked) === HOME
72
+ } catch {
73
+ return false
74
+ }
75
+ }
76
+
77
+ const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]', '::1'])
78
+
79
+ function local(at: string): boolean {
80
+ try {
81
+ return LOOPBACK.has(new URL(at).hostname)
82
+ } catch {
83
+ return false
84
+ }
85
+ }
86
+
87
+ function why(at: string, thrown: unknown): string {
88
+ const held: unknown = thrown instanceof Error ? Reflect.get(thrown, 'code') : undefined
89
+ const said = typeof held === 'string' ? REASONS[held] : undefined
90
+ if (said !== undefined) return `${at} ${said}.`
91
+ return `${at} could not be reached — ${thrown instanceof Error ? thrown.message : String(thrown)}.`
92
+ }
93
+
94
+ export function carry(one: Order): Answered {
95
+ if (!inside(one.at)) {
96
+ return {
97
+ answered: 'refused',
98
+ 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.`,
99
+ }
100
+ }
101
+ if (one.order === 'remove' && rootly(one.at)) {
102
+ return {
103
+ answered: 'refused',
104
+ because: `${one.at} is the repository this was run in, and this carries orders inside it rather than taking it away.`,
105
+ }
106
+ }
107
+ try {
108
+ if (one.order === 'kind') return { answered: 'kind', held: kind(one.at) }
109
+ if (one.order === 'read') return { answered: 'read', said: readFileSync(one.at) }
110
+ if (one.order === 'list') {
111
+ const held = readdirSync(one.at, { withFileTypes: true }).map((each) => ({
112
+ name: each.name,
113
+ kind: each.isFile() ? ('file' as const) : each.isDirectory() ? ('folder' as const) : ('other' as const),
114
+ }))
115
+ return { answered: 'list', held }
116
+ }
117
+ if (one.order === 'stat') {
118
+ const held = statSync(one.at)
119
+ return { answered: 'stat', held: { size: held.size, modified: held.mtimeMs } }
120
+ }
121
+ if (one.order === 'remove') {
122
+ rmSync(one.at, { recursive: true, force: true })
123
+ return { answered: 'removed' }
124
+ }
125
+ if (one.order === 'real') return { answered: 'real', at: realpathSync(one.at) }
126
+ writeFileSync(one.at, one.said)
127
+ return { answered: 'written' }
128
+ } catch (thrown) {
129
+ return { answered: 'refused', because: why(one.at, thrown) }
130
+ }
131
+ }
132
+
133
+ export interface Turn {
134
+ readonly of: string
135
+ readonly orders?: readonly Order[]
136
+ readonly say?: string
137
+ readonly done?: boolean
138
+ }
139
+
140
+ const AT = 'THREAD_SERVER'
141
+
142
+ const KEY = 'THREAD_KEY'
143
+
144
+ const ROUNDS = 100
145
+
146
+ async function sending(at: string, key: string, body: unknown): Promise<Turn> {
147
+ const said = await fetch(at, {
148
+ method: 'POST',
149
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${key}` },
150
+ body: JSON.stringify(body),
151
+ })
152
+ if (said.status === 401 || said.status === 403) throw new Error(`the key in ${KEY} is not one ${at} accepts`)
153
+ if (!said.ok) throw new Error(`${at} answered ${String(said.status)}`)
154
+ return (await said.json()) as Turn
155
+ }
156
+
157
+ export async function talking(at: string, key: string, message: string): Promise<number> {
158
+ let turn = await sending(at, key, { said: message })
159
+ for (let spun = 0; spun < ROUNDS; spun += 1) {
160
+ if (turn.say !== undefined && turn.say !== '') stdout.write(`${turn.say}\n`)
161
+ if (turn.done === true) return 0
162
+ turn = await sending(at, key, { of: turn.of, answered: (turn.orders ?? []).map(carry) })
163
+ }
164
+ stderr.write(
165
+ `${THING}: this went ${String(ROUNDS)} rounds without finishing, so it was stopped there. What is already\n` +
166
+ `in this repository stands, and nothing half-written was left behind — every change is one order, carried\n` +
167
+ `and answered before the next one is asked for.\n`,
168
+ )
169
+ return 1
170
+ }
171
+
172
+ export async function run(said: readonly string[]): Promise<number> {
173
+ if (said.length === 0) {
174
+ stdout.write(
175
+ `${THING}: say what you want built, in one sentence, in quotes.\n\n ${THING} "take payments with stripe"\n\n` +
176
+ `That sentence is the whole of what this takes — there is no command here to look up. Nothing on this\n` +
177
+ `machine declares a row, writes a file or runs a check. What comes back is written into this repository\n` +
178
+ `for you, and every check this framework runs has to pass before it stays.\n`,
179
+ )
180
+ return 0
181
+ }
182
+ const message = said.length === 1 ? (said[0] ?? '') : ''
183
+ if (!message.includes(' ')) {
184
+ stderr.write(
185
+ `${THING}: there is no \`${said.join(' ')}\` here, and no other command to find instead. This takes one\n` +
186
+ `sentence, in quotes, and nothing else — ${THING} "take payments with stripe". Say what you want and it\n` +
187
+ `is answered where the work happens, never on this machine.\n`,
188
+ )
189
+ return 1
190
+ }
191
+ const at = env[AT]
192
+ if (at === undefined || at === '') {
193
+ stderr.write(
194
+ `${THING}: there is nowhere to send that yet. This copy has no server to answer it — ${AT} names one, and\n` +
195
+ `nothing here is set to one.\n`,
196
+ )
197
+ return 1
198
+ }
199
+ if (!local(at)) {
200
+ stderr.write(
201
+ `${THING}: ${AT} names ${at}, which is not on this machine, and this only ever talks to a server running\n` +
202
+ `here. Set ${AT} to a http://localhost address and say it again. Nothing was sent.\n`,
203
+ )
204
+ return 1
205
+ }
206
+
207
+ const key = env[KEY]
208
+ if (key === undefined || key === '') {
209
+ stderr.write(
210
+ `${THING}: nothing here says who is asking, and ${at} answers no one it cannot name. ${KEY} carries your\n` +
211
+ `key, and nothing on this machine is set to one. Set it in this terminal and say it again — it is read\n` +
212
+ `from there, never written into this repository and never sent anywhere but ${at}.\n`,
213
+ )
214
+ return 1
215
+ }
216
+ try {
217
+ return await talking(at, key, message)
218
+ } catch (thrown) {
219
+ stderr.write(
220
+ `${THING}: ${at} could not answer — ${thrown instanceof Error ? thrown.message : String(thrown)}. Nothing was\n` +
221
+ `written, so this repository is as it was.\n`,
222
+ )
223
+ return 1
224
+ }
225
+ }
226
+
227
+ 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[]>;
@@ -1,5 +1,5 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
2
+ import { attached, existsSync, kind, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "./files.js";
3
3
  import { dirname, join, relative, resolve } from 'node:path';
4
4
  import ts from 'typescript';
5
5
  import { ignored } from "./init.js";
@@ -26,12 +26,17 @@ function rooted() {
26
26
  export const ROOT = TREE_AT?.at ?? NOWHERE;
27
27
  export const TREE = `${TREE_AT?.name ?? NOWHERE}/`;
28
28
  export const THREAD_AT = `${TREE_AT?.name ?? NOWHERE}/thread.ts`;
29
- const OWN_IGNORES = (() => {
30
- if (spawnSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: ROOT, stdio: 'ignore' }).status === 0) {
31
- return undefined;
29
+ let ignores;
30
+ let readIgnores = false;
31
+ function ownIgnores() {
32
+ if (readIgnores)
33
+ return ignores;
34
+ readIgnores = true;
35
+ if (!attached() && spawnSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: ROOT, stdio: 'ignore' }).status === 0) {
36
+ return ignores;
32
37
  }
33
38
  try {
34
- return readFileSync(join(ROOT, '.gitignore'), 'utf8')
39
+ ignores = readFileSync(join(ROOT, '.gitignore'), 'utf8')
35
40
  .split('\n')
36
41
  .map((one) => one.trim())
37
42
  .filter((one) => one !== '' && !one.startsWith('#') && !one.startsWith('!'))
@@ -39,21 +44,24 @@ const OWN_IGNORES = (() => {
39
44
  .filter((one) => one !== '' && !one.includes('*'));
40
45
  }
41
46
  catch {
42
- return [];
47
+ ignores = [];
43
48
  }
44
- })();
49
+ return ignores;
50
+ }
45
51
  const asked = new Map();
46
52
  export function unkept(at) {
47
53
  const held = asked.get(at);
48
54
  if (held !== undefined)
49
55
  return held;
50
- const found = OWN_IGNORES === undefined
56
+ const own = ownIgnores();
57
+ const found = own === undefined
51
58
  ? spawnSync('git', ['check-ignore', '-q', '--', at], { cwd: ROOT, stdio: 'ignore' }).status === 0
52
- : OWN_IGNORES.some((one) => at === one || at.startsWith(`${one}/`) || at.split('/').includes(one));
59
+ : own.some((one) => at === one || at.startsWith(`${one}/`) || at.split('/').includes(one));
53
60
  asked.set(at, found);
54
61
  return found;
55
62
  }
56
63
  export const LISTS = ['declares', 'derives', 'seeds'];
64
+ export const ROW_LISTS = [...LISTS, 'holds'];
57
65
  const SOURCE = /\.(ts|tsx|mts|cts)$/;
58
66
  const RUNNER = /^(?:pnpm|npm|yarn) (?:run )?([\w:.-]+)/;
59
67
  export function under(pass, id, root, seen = new Set()) {
@@ -148,12 +156,67 @@ const INVARIANT = {
148
156
  allowJs: true,
149
157
  jsx: ts.JsxEmit.ReactJSX,
150
158
  };
159
+ const NEVER = ['node_modules', 'bower_components', 'jspm_packages'];
160
+ function bounds(from, one) {
161
+ const star = one.indexOf('*');
162
+ if (star === -1)
163
+ return { under: resolve(from, one), ending: '' };
164
+ const head = one.slice(0, star);
165
+ const cut = head.lastIndexOf('/');
166
+ const tail = one.slice(star);
167
+ const dot = tail.lastIndexOf('.');
168
+ const end = dot === -1 ? '' : tail.slice(dot);
169
+ return {
170
+ under: resolve(from, cut === -1 ? '' : head.slice(0, cut)),
171
+ ending: end.includes('/') || end.includes('*') ? '' : end,
172
+ };
173
+ }
174
+ function wanted(at, only) {
175
+ if (only.length === 0)
176
+ return true;
177
+ return only.some((one) => (at === one.under || at.startsWith(`${one.under}/`)) && at.endsWith(one.ending));
178
+ }
179
+ function listing(at, take, skip, only, deep) {
180
+ const found = [];
181
+ if (deep < 0 || !existsSync(at))
182
+ return found;
183
+ for (const one of readdirSync(at, { withFileTypes: true })) {
184
+ const full = join(at, one.name);
185
+ if (skip.some((each) => full === each || full.startsWith(`${each}/`)))
186
+ continue;
187
+ if (one.isDirectory()) {
188
+ if (NEVER.includes(one.name))
189
+ continue;
190
+ found.push(...listing(full, take, skip, only, deep - 1));
191
+ }
192
+ else if (take.some((each) => one.name.endsWith(each)) && wanted(full, only)) {
193
+ found.push(full);
194
+ }
195
+ }
196
+ return found;
197
+ }
198
+ function parsing(from) {
199
+ return {
200
+ useCaseSensitiveFileNames: true,
201
+ fileExists: (at) => existsSync(at),
202
+ readFile: (at) => {
203
+ try {
204
+ return readFileSync(at);
205
+ }
206
+ catch {
207
+ return undefined;
208
+ }
209
+ },
210
+ readDirectory: (at, take, skip, only, deep) => listing(at, take, (skip ?? []).map((one) => resolve(from, one.endsWith('/**') ? one.slice(0, -3) : one)), only.map((one) => bounds(from, one)), deep ?? Number.MAX_SAFE_INTEGER),
211
+ };
212
+ }
151
213
  export function configured() {
152
214
  for (const name of CONFIGS) {
153
215
  const at = join(ROOT, name);
154
- if (!ts.sys.fileExists(at))
216
+ if (!existsSync(at))
155
217
  continue;
156
- const held = ts.readConfigFile(at, ts.sys.readFile.bind(ts.sys));
218
+ const host = parsing(ROOT);
219
+ const held = ts.readConfigFile(at, host.readFile);
157
220
  if (held.error !== undefined) {
158
221
  return {
159
222
  options: { ...FALLBACK, ...INVARIANT },
@@ -161,7 +224,7 @@ export function configured() {
161
224
  trouble: `${name} is there and cannot be read: ${ts.flattenDiagnosticMessageText(held.error.messageText, ' ')}`,
162
225
  };
163
226
  }
164
- const parsed = ts.parseJsonConfigFileContent(held.config, ts.sys, dirname(at));
227
+ const parsed = ts.parseJsonConfigFileContent(held.config, host, dirname(at));
165
228
  const complained = parsed.errors[0];
166
229
  if (complained !== undefined) {
167
230
  return {
@@ -174,13 +237,37 @@ export function configured() {
174
237
  }
175
238
  return { options: { ...FALLBACK, ...INVARIANT }, from: '' };
176
239
  }
240
+ export function hosting(options) {
241
+ const host = ts.createCompilerHost(options, true);
242
+ const lib = host.getDefaultLibLocation?.();
243
+ const toolchain = (at) => lib !== undefined && (at === lib || at.startsWith(`${lib}/`));
244
+ return {
245
+ ...host,
246
+ fileExists: (at) => (toolchain(at) ? host.fileExists(at) : kind(at) === 'file'),
247
+ directoryExists: (at) => (toolchain(at) ? (host.directoryExists?.(at) ?? false) : kind(at) === 'folder'),
248
+ readFile: (at) => (toolchain(at) ? host.readFile(at) : kind(at) === 'file' ? readFileSync(at) : undefined),
249
+ getSourceFile: (at, version) => {
250
+ if (toolchain(at))
251
+ return host.getSourceFile(at, version);
252
+ if (kind(at) !== 'file')
253
+ return undefined;
254
+ return ts.createSourceFile(at, readFileSync(at), version, true);
255
+ },
256
+ getDirectories: (at) => kind(at) === 'folder' ? readdirSync(at, { withFileTypes: true }).filter((one) => one.isDirectory()).map((one) => one.name) : [],
257
+ realpath: (at) => (toolchain(at) ? (host.realpath?.(at) ?? at) : realpathSync(at)),
258
+ writeFile: (at, said) => {
259
+ writeFileSync(at, said);
260
+ },
261
+ };
262
+ }
177
263
  let built;
178
264
  function compiled() {
179
265
  rooted();
180
266
  if (built !== undefined)
181
267
  return built;
182
268
  const roots = [THREAD_AT, ...sources(), ...shipped()].filter((one) => SOURCE.test(one)).map((one) => resolve(ROOT, one));
183
- const program = ts.createProgram([...new Set(roots)], configured().options);
269
+ const options = configured().options;
270
+ const program = ts.createProgram([...new Set(roots)], options, hosting(options));
184
271
  built = { program, checker: program.getTypeChecker() };
185
272
  return built;
186
273
  }
@@ -585,6 +672,9 @@ export function forget() {
585
672
  scanned = undefined;
586
673
  commands = undefined;
587
674
  source.clear();
675
+ asked.clear();
676
+ ignores = undefined;
677
+ readIgnores = false;
588
678
  }
589
679
  export function descend() {
590
680
  rooted();
@@ -762,7 +852,10 @@ export function descend() {
762
852
  continue;
763
853
  }
764
854
  for (const one of value.elements) {
765
- if (!ts.isObjectLiteralExpression(one)) {
855
+ const declaration = ts.isIdentifier(one) ? checker.getSymbolAtLocation(one)?.valueDeclaration : undefined;
856
+ const initial = declaration !== undefined && ts.isVariableDeclaration(declaration) ? unwrap(declaration.initializer) : undefined;
857
+ const object = ts.isObjectLiteralExpression(one) ? one : initial !== undefined && ts.isObjectLiteralExpression(initial) ? initial : undefined;
858
+ if (object === undefined) {
766
859
  findings.push({
767
860
  file: at,
768
861
  detail: `\`${by}\` holds something in \`${list}\` that is not a row written out here`,
@@ -773,7 +866,7 @@ export function descend() {
773
866
  rows += 1;
774
867
  const wanted = checker.getContextualType(one);
775
868
  const kind = wanted === undefined ? undefined : elementOf(checker, wanted);
776
- const row = rowOf(one, kind === undefined ? type : checker.typeToString(kind), at, kind);
869
+ const row = rowOf(object, kind === undefined ? type : checker.typeToString(kind), at, kind);
777
870
  coin(row.id, row.describes, under, by);
778
871
  judge(row, by, list, false);
779
872
  if (row.at !== undefined) {
@@ -782,7 +875,7 @@ export function descend() {
782
875
  derived.set(row.id, row);
783
876
  }
784
877
  if (LISTS.some((name) => row.fields.has(name)))
785
- into(one, row.type, at, under, by, true);
878
+ into(object, row.type, at, under, by, true);
786
879
  if (row.at !== undefined)
787
880
  open(row.at, wanted === undefined ? undefined : elementOf(checker, wanted), under, row.id ?? by, by);
788
881
  }