@prestack/thread 0.0.3 → 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
@@ -1,775 +0,0 @@
1
- var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
- if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
- return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
- return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
- });
6
- }
7
- return path;
8
- };
9
- import { spawnSync } from 'node:child_process';
10
- import { readFileSync, writeFileSync } from 'node:fs';
11
- import { join, relative } from 'node:path';
12
- import { argv, stdout } from 'node:process';
13
- import ts from 'typescript';
14
- import { exit } from "../../core/invoked.js";
15
- import { ELSEWHERE, RECEIPT, answer, asking, asksOf, given, issue, needed, only, ownAnswers, refuse, rowLines, seeding, slug, spend, stating, wordsIn } from "../../core/asking.js";
16
- import { holdsRow, listBounds, withRow } from "../../core/rows.js";
17
- import { named as packageOf } from "../../core/scope.js";
18
- import { findTree, inTree } from "../../core/tree.js";
19
- import { secrecyOf } from "../secret.js";
20
- import { CALL_PREFIX, HOLDS, REACH, SAID, addressOf, credentialsOf, fills, holdsOf, misnamed, reachOf, refusals, } from "../target.js";
21
- import { opened } from "../vault.js";
22
- const TREE = findTree();
23
- const FILE = 'targets.ts';
24
- const TARGET_LIST = 'TARGETS';
25
- const CALL_LIST = 'CALLS';
26
- const NAME = /^[a-z][a-z0-9_]*$/;
27
- const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
28
- const WORDS = Object.keys(REACH);
29
- const ASKS = [
30
- { key: 'name', says: ['lower_snake_case, and the prefix is added for you'] },
31
- { key: 'reach', says: [`how it is reached — ${WORDS.join(', ')}`] },
32
- {
33
- key: 'describes',
34
- edits: true,
35
- says: [
36
- 'what it is for, in one sentence, and it has to be yours — blank is',
37
- 'refused, because copying the reach paragraph above would give two',
38
- 'targets the same meaning',
39
- ],
40
- },
41
- {
42
- key: 'uses',
43
- edits: true,
44
- says: [
45
- 'ONLY for module and cli — the npm package to install, or the',
46
- 'program to run. Not asked for the other four, and refused on them',
47
- ],
48
- },
49
- {
50
- key: 'address',
51
- edits: true,
52
- says: ['the id of a secret with no `sends`. Blank only for a reach', 'that is addressed by nothing'],
53
- },
54
- { key: 'credentials', edits: true, says: ['comma-separated ids of secrets that DO have a `sends`'] },
55
- {
56
- key: 'accepts',
57
- edits: true,
58
- says: [
59
- 'comma-separated — entity ids for a place that keeps things, call',
60
- 'ids for one that is called. For module and cli it is what the thing',
61
- 'can do, read off its published types and offered to you. Usually',
62
- 'left blank here and filled by `call`, by `thread entity`, or by `edit`',
63
- ],
64
- },
65
- ];
66
- const TAKES = ASKS.map((one) => one.key);
67
- const EDITS = ASKS.filter((one) => one.edits === true).map((one) => one.key);
68
- const ASKED = ASKS.flatMap((one) => one.says.map((line, at) => ` ${at === 0 ? `${one.key}:`.padEnd(32) : ' '.repeat(32)}${line}`));
69
- const CALLS = [
70
- {
71
- key: 'of',
72
- says: ['which target the call belongs to — not needed when it is', 'named as <target> on the command line'],
73
- },
74
- { key: 'name', says: ['lower_snake_case; the id becomes cl_<name>'] },
75
- { key: 'describes', says: ['what it does, in one sentence'] },
76
- { key: 'method', says: [`one of ${METHODS.join(', ')}`] },
77
- {
78
- key: 'path',
79
- says: [
80
- 'under the target address, leading slash and all. Anything that',
81
- 'varies is written {like_this}',
82
- ],
83
- },
84
- ];
85
- const MAKES = CALLS.map((one) => one.key);
86
- const CALLED = CALLS.flatMap((one) => one.says.map((line, at) => ` ${at === 0 ? `${one.key}:`.padEnd(32) : ' '.repeat(32)}${line}`));
87
- const PRODUCTION = '--production';
88
- const here = (one) => relative(TREE.at, one).split('\\').join('/');
89
- const path = inTree(TREE, FILE);
90
- const at = here(path);
91
- import { quoted } from "../../core/rows.js";
92
- let targets = [];
93
- let calls = [];
94
- try {
95
- const taken = (await import(__rewriteRelativeImportExtension(new URL(`file://${path}`).href)));
96
- targets = taken.TARGETS ?? [];
97
- calls = taken.CALLS ?? [];
98
- }
99
- catch {
100
- refuse(`${at} could not be read. Attach \`${packageOf('integrations')}\` first — an app states where it reaches there.`);
101
- }
102
- let secrets = [];
103
- try {
104
- const taken = (await import(__rewriteRelativeImportExtension(new URL(`file://${inTree(TREE, 'secrets.ts')}`).href)));
105
- secrets = taken.SECRETS ?? [];
106
- }
107
- catch {
108
- secrets = [];
109
- }
110
- let entities = [];
111
- try {
112
- const taken = (await import(__rewriteRelativeImportExtension(new URL(`file://${inTree(TREE, 'entities.ts')}`).href)));
113
- entities = (taken.ENTITIES ?? []).map((one) => one.id);
114
- }
115
- catch {
116
- entities = [];
117
- }
118
- const holdingOf = (id) => {
119
- const holds = holdsOf(id);
120
- if (holds === undefined)
121
- return 'nothing — its id wears no prefix a target is reached by';
122
- const yes = [
123
- ...(holds.migrations ? ['migrations'] : []),
124
- ...(holds.references ? ['references'] : []),
125
- ];
126
- return `accepts ${holds.takes}, reached from ${holds.from}, ${yes.length === 0 ? 'holds no enforcement of its own' : `holds ${yes.join(', ')}`}`;
127
- };
128
- const requiredBy = (one) => {
129
- if (one.requires.length === 0)
130
- return 'nothing, so nothing says where it is';
131
- const address = addressOf(one, secrets);
132
- return one.requires
133
- .map((each) => `${each}${each === address?.id ? ' (its address)' : ''}`)
134
- .join(', ');
135
- };
136
- const listing = [
137
- targets.length === 0
138
- ? 'no targets yet.'
139
- : [
140
- `${String(targets.length)} target(s):`,
141
- ...targets.flatMap((one) => [
142
- ` ${one.id} — ${holdingOf(one.id)}`,
143
- ` accepts ${[...one.takes, ...(one.offers ?? [])].join(', ') || 'nothing'}`,
144
- ` requires ${requiredBy(one)}`,
145
- ]),
146
- ].join('\n'),
147
- calls.length === 0
148
- ? 'no calls yet.'
149
- : [
150
- `${String(calls.length)} call(s):`,
151
- ...calls.map((one) => ` ${one.id} — ${one.method} ${one.path} on ${one.of}`),
152
- ].join('\n'),
153
- secrets.length === 0
154
- ? 'no secrets yet — declare one with `thread create secret` before a target can require it.'
155
- : [
156
- `${String(secrets.length)} secret(s) a target may require:`,
157
- ...secrets.map((one) => ` ${one.id} — ${secrecyOf(one.id) ?? '?'}, ${one.sends === undefined ? 'no `sends`, so it can be an address' : `sent as ${one.sends.header}`}`),
158
- ].join('\n'),
159
- ].join('\n\n');
160
- const wrong = refusals(targets, secrets, calls, entities);
161
- const words = wordsIn(['new', 'call']);
162
- const verb = words[0];
163
- const asCall = calls.find((one) => one.id === verb);
164
- if (asCall !== undefined) {
165
- const of = targets.find((one) => one.id === asCall.of);
166
- const targetWrong = wrong.filter((each) => each.includes(asCall.id) || (of !== undefined && each.includes(of.id)));
167
- if (targetWrong.length > 0)
168
- refuse(targetWrong.join('\n'));
169
- if (of === undefined)
170
- refuse(`\`${asCall.of}\` is not a target this app declares`);
171
- const address = addressOf(of, secrets);
172
- if (address === undefined)
173
- refuse(`\`${of.id}\` names no address, and the check above should already have said so`);
174
- const required = credentialsOf(of, secrets);
175
- const names = [...new Set(fills(asCall))];
176
- const writes = asCall.method === 'POST' || asCall.method === 'PUT' || asCall.method === 'PATCH';
177
- const allowed = writes ? [...names, 'body'] : names;
178
- const piped = given();
179
- only(piped, allowed, `\`${asCall.id}\``);
180
- const { ask, shut } = asking(piped);
181
- const values = new Map();
182
- for (const name of names) {
183
- const value = piped.length > 0 ? needed(piped, name, `\`${asCall.id}\``) : await ask(name, `${name} (fills the path): `);
184
- values.set(name, value.trim());
185
- }
186
- let body;
187
- if (writes) {
188
- const typed = (piped.length > 0 ? (answer(piped, 'body') ?? '') : await ask('body', 'body, as JSON (blank for none): ')).trim();
189
- if (typed !== '') {
190
- try {
191
- JSON.parse(typed);
192
- }
193
- catch (thrown) {
194
- shut();
195
- refuse(`that is not JSON: ${thrown instanceof Error ? thrown.message : String(thrown)}. Nothing was called.`);
196
- }
197
- body = typed;
198
- }
199
- }
200
- shut();
201
- let route = asCall.path;
202
- for (const name of fills(asCall))
203
- route = route.replace(`{${name}}`, encodeURIComponent(values.get(name) ?? ''));
204
- const env = argv.includes(PRODUCTION) ? 'production' : 'sandbox';
205
- const opening = opened(TREE.at, secrets.map((one) => one.id), env);
206
- let resolved;
207
- try {
208
- resolved = await opening(of.requires);
209
- }
210
- catch (thrown) {
211
- refuse(thrown instanceof Error ? thrown.message : String(thrown));
212
- }
213
- const host = resolved[address.id] ?? '';
214
- const based = /^https?:\/\//.test(host) ? host : `https://${host}`;
215
- const url = `${based.replace(/\/+$/, '')}${route.startsWith('/') ? route : `/${route}`}`;
216
- const headers = {};
217
- for (const credential of required) {
218
- const value = resolved[credential.id];
219
- if (value === undefined || credential.sends === undefined)
220
- continue;
221
- headers[credential.sends.header] = `${credential.sends.prefix ?? ''}${value}`;
222
- }
223
- if (body !== undefined)
224
- headers['content-type'] = 'application/json';
225
- let response;
226
- try {
227
- response = await fetch(url, { method: asCall.method, headers, ...(body === undefined ? {} : { body }) });
228
- }
229
- catch (thrown) {
230
- refuse(`\`${asCall.id}\` — ${asCall.method} ${route} on \`${of.id}\` — threw before a response came back: ` +
231
- `${thrown instanceof Error ? thrown.message : String(thrown)}`);
232
- }
233
- const text = await response.text();
234
- stdout.write(`call: ${asCall.method} ${route} on \`${of.id}\` (${env}) — ${String(response.status)} ${response.statusText}\n`);
235
- if (text.trim() !== '') {
236
- try {
237
- stdout.write(`${JSON.stringify(JSON.parse(text), null, 2)}\n`);
238
- }
239
- catch {
240
- stdout.write(`${text}\n`);
241
- }
242
- }
243
- exit(response.ok ? 0 : 1);
244
- }
245
- if (verb !== undefined && verb.startsWith(CALL_PREFIX)) {
246
- refuse(`\`${verb}\` is not a call this app declares. What is here is ${calls.map((one) => one.id).join(', ') || '(nothing yet)'} — ` +
247
- 'declare one with `thread create target call <target> <name>`.');
248
- }
249
- stdout.write(`${listing}\n`);
250
- if (wrong.length > 0) {
251
- stdout.write(`\n${String(wrong.length)} thing(s) this app already states that will not hold:\n`);
252
- for (const one of wrong)
253
- stdout.write(` ${one}\n`);
254
- }
255
- const method = (nonce) => [
256
- '',
257
- 'what this makes:',
258
- '',
259
- ` thread create target new <name> ${RECEIPT} ${nonce}`,
260
- ` thread create target call <target> <name> ${RECEIPT} ${nonce}`,
261
- ' thread run call <id> no receipt — makes an already-declared call, through',
262
- ' the vault. Add --production to call it there; the',
263
- ' default is sandbox.',
264
- '',
265
- ...ELSEWHERE,
266
- '',
267
- ` ${RECEIPT} is spent by one run and refused if ${at} moved after it was issued.`,
268
- '',
269
- 'how a target may be reached. The word is what you send; the prefix is what its id wears, and the',
270
- 'prefix IS how it is reached — no field says so:',
271
- '',
272
- ...WORDS.flatMap((one) => [
273
- ` ${one.padEnd(9)} → ${REACH[one]}<name>`,
274
- ` accepts ${HOLDS[one].takes}, from ${HOLDS[one].from}, ${HOLDS[one].addressed ? 'addressed' : 'addressed by nothing'}`,
275
- ` ${SAID[one]}`,
276
- '',
277
- ]),
278
- '`new` asks, in this order:',
279
- '',
280
- ...ASKED,
281
- ' and then, at a terminal only, for a module whose package holds resources: naming one above asks',
282
- ' which of its methods you want, and what is written is the whole path — `customers.create`.',
283
- ' Blank there takes every method it just listed. There is no key for it, because a piped answer',
284
- ' names whole paths in `accepts` and never needs asking twice.',
285
- '',
286
- `\`edit\` takes: ${EDITS.join(', ')} — and for a module it reads the package again, so a version`,
287
- 'that gained methods offers them without the row being retyped.',
288
- '',
289
- '`call` asks, in this order:',
290
- '',
291
- ...CALLED,
292
- '',
293
- 'A node then names the call. It never writes a url, a method or a secret: what a target is, where',
294
- 'it is and what opens it are all read off these rows when the operation is compiled.',
295
- ].join('\n');
296
- const flagged = argv.indexOf(RECEIPT);
297
- if (verb === undefined && flagged === -1) {
298
- const nonce = issue(TREE.at, listing);
299
- stdout.write(`${method(nonce)}\n`);
300
- stdout.write(`\ntarget: receipt ${nonce} — spent on use, and stale the moment ${at} changes.\n`);
301
- exit(0);
302
- }
303
- spend(TREE.at, listing, argv[flagged + 1], at);
304
- if (verb !== 'new' && verb !== 'call') {
305
- refuse(`there is nothing called "${verb ?? ''}" to do here — say new, or call`);
306
- }
307
- const text = readFileSync(path, 'utf8');
308
- const listed = (one) => (one ?? '')
309
- .split(',')
310
- .map((each) => each.trim())
311
- .filter((each) => each !== '');
312
- if (verb === 'call') {
313
- const piped = given();
314
- only(piped, MAKES, 'a call');
315
- const { ask, shut } = asking(piped);
316
- const of = (words[1] ?? (await ask('of', `which target — ${targets.map((one) => one.id).join(', ') || '(none yet)'}: `))).trim();
317
- const target = targets.find((one) => one.id === of);
318
- if (target === undefined) {
319
- shut();
320
- refuse(`this app declares no target \`${of}\`. A call belongs to one place, and that place is declared first.`);
321
- }
322
- if (holdsOf(target.id)?.takes !== 'calls') {
323
- shut();
324
- refuse(`\`${target.id}\` is reached over ${String(reachOf(target.id))} and keeps entities. A place you keep rows in is ` +
325
- 'not a place you issue verbs at.');
326
- }
327
- const name = slug(words[2] ?? (await ask('name', 'name: ')));
328
- if (name === '' || !NAME.test(name)) {
329
- shut();
330
- refuse(`"${name}" cannot be the name of a call — it is written in lower_snake_case`);
331
- }
332
- const id = `${CALL_PREFIX}${name}`;
333
- if (calls.some((one) => one.id === id)) {
334
- shut();
335
- refuse(`\`${id}\` is already declared in ${at}`);
336
- }
337
- const describes = (piped.length > 0 ? needed(piped, 'describes', 'a call') : await ask('describes', 'what does it do? ')).trim();
338
- if (describes === '') {
339
- shut();
340
- refuse('a call says what it does — the sentence is what somebody reads before making it');
341
- }
342
- const verbWord = (piped.length > 0 ? needed(piped, 'method', 'a call') : await ask('method', `method — ${METHODS.join(', ')}: `))
343
- .trim()
344
- .toUpperCase();
345
- if (!METHODS.includes(verbWord)) {
346
- shut();
347
- refuse(`there is no such method as "${verbWord}" — it is one of ${METHODS.join(', ')}`);
348
- }
349
- const route = (piped.length > 0 ? needed(piped, 'path', 'a call') : await ask('path', 'path, leading slash and all: ')).trim();
350
- shut();
351
- if (!route.startsWith('/'))
352
- refuse(`"${route}" is not a path under an address — it opens with a slash`);
353
- const callWhere = listBounds(text, CALL_LIST);
354
- if (callWhere === undefined)
355
- refuse(`${at} states no \`${CALL_LIST}\` list to declare a call in`);
356
- const row = [
357
- ' {',
358
- ` id: ${quoted(id)},`,
359
- ` at: '_threads/${FILE}',`,
360
- ` describes: ${quoted(describes)},`,
361
- ' version: 1,',
362
- ` of: ${quoted(target.id)},`,
363
- ` method: ${quoted(verbWord)},`,
364
- ` path: ${quoted(route)},`,
365
- ' },',
366
- ].join('\n');
367
- const withCall = withRow(text, callWhere, row);
368
- const targetWhere = listBounds(withCall, TARGET_LIST);
369
- if (targetWhere === undefined)
370
- refuse(`${at} states no \`${TARGET_LIST}\` list`);
371
- const taking = [...target.takes, id];
372
- const replaced = withCall.replace(new RegExp(`(id: '${target.id}',[\\s\\S]*?takes: )\\[[^\\]]*\\]`), `$1[${taking.map((one) => quoted(one)).join(', ')}]`);
373
- if (replaced === withCall) {
374
- refuse(`\`${target.id}\` has no \`takes\` this could add \`${id}\` to. Nothing was written.`);
375
- }
376
- writeFileSync(path, replaced, 'utf8');
377
- stdout.write(`target: declared ${id} — ${verbWord} ${route} on ${target.id}, and ${target.id} now accepts it.\n`);
378
- exit(0);
379
- }
380
- const piped = given();
381
- const own = ownAnswers(piped);
382
- only(own, TAKES, 'a target');
383
- const { ask, line, shut } = asking(piped);
384
- const name = slug(words[1] ?? (await ask('name', 'name: ')));
385
- if (name === '') {
386
- shut();
387
- refuse('a target has to be called something');
388
- }
389
- if (!NAME.test(name)) {
390
- shut();
391
- refuse(`"${name}" cannot be the name of a target — it is written in lower_snake_case`);
392
- }
393
- const reach = (piped.length > 0 ? needed(own, 'reach', 'a target') : await ask('reach', `how is it reached — ${WORDS.join(', ')}: `)).trim();
394
- if (!WORDS.includes(reach)) {
395
- shut();
396
- refuse(`there is no such reach as "${reach}". A target is reached ${WORDS.join(', ')}.`);
397
- }
398
- const word = reach;
399
- const id = `${REACH[word]}${name}`;
400
- if (targets.some((one) => one.id === id)) {
401
- shut();
402
- refuse(`\`${id}\` is already declared in ${at}`);
403
- }
404
- const answered = (piped.length > 0 ? (answer(own, 'describes') ?? '') : await ask('describes', 'what is it for? ')).trim();
405
- if (answered === '') {
406
- shut();
407
- refuse(`a target says what IT is, and blank would have copied the ${word} paragraph above onto this row — so ` +
408
- `\`${REACH[word]}${name}\` would carry a sentence about ${word} reaches in general and nothing about this one. ` +
409
- 'Say what it is to somebody who has not seen it: what this app reaches through it, and what for.');
410
- }
411
- const describes = answered;
412
- const PACKAGE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
413
- const PROGRAM = /^[a-z][a-z0-9._-]*$/;
414
- const arrives = HOLDS[word].arrives;
415
- const uses = (arrives === null
416
- ? ''
417
- : piped.length > 0
418
- ? needed(own, 'uses', `a target reached as ${word}`)
419
- : await ask('uses', arrives === 'npm'
420
- ? 'npm package to install — the name `npm install` is given: '
421
- : 'program to run — the name you would type at a shell, e.g. `git`: ')).trim();
422
- if (arrives !== null) {
423
- if (uses === '') {
424
- shut();
425
- refuse(arrives === 'npm'
426
- ? 'a module installs something — the npm package is what `npm install` is given'
427
- : 'a command runs something — the program is what you would type at a shell');
428
- }
429
- if (!(arrives === 'npm' ? PACKAGE : PROGRAM).test(uses)) {
430
- shut();
431
- refuse(`"${uses}" does not read as ${arrives === 'npm' ? 'an npm package name' : 'a program name'}`);
432
- }
433
- }
434
- if (arrives === 'npm') {
435
- stdout.write(`target: installing ${uses} …\n`);
436
- const got = spawnSync('npm', ['install', uses], { cwd: TREE.at, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
437
- if (got.status !== 0) {
438
- shut();
439
- refuse(`\`npm install ${uses}\` failed, so nothing was declared:\n${`${got.stdout ?? ''}${got.stderr ?? ''}`.trim()}`);
440
- }
441
- stdout.write(`target: installed ${uses}.\n`);
442
- }
443
- else if (arrives === 'machine') {
444
- const found = spawnSync(uses, ['--version'], { cwd: TREE.at, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
445
- if (found.error !== undefined || found.status === null) {
446
- shut();
447
- refuse(`\`${uses}\` is not on this machine, so there is nothing to declare. It is a program rather than a dependency ` +
448
- '— install it the way this machine installs programs and run this again. Nothing was written.');
449
- }
450
- }
451
- const driver = HOLDS[word].driver;
452
- if (driver !== null) {
453
- stdout.write(`target: installing ${driver} …\n`);
454
- const opened = spawnSync('npm', ['install', driver], { cwd: TREE.at, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
455
- if (opened.status !== 0) {
456
- shut();
457
- refuse(`npm install ${driver} failed, so nothing was declared. A ${word} target is opened with ${driver}, and the store written for it imports that package:\n${`${opened.stdout ?? ''}${opened.stderr ?? ''}`.trim()}`);
458
- }
459
- stdout.write(`target: installed ${driver} — what a ${word} target is opened with.\n`);
460
- }
461
- let offered = [];
462
- let deeper = new Map();
463
- if (HOLDS[word].takes === 'names' && arrives === 'npm') {
464
- const read = surfaceOf(uses, id);
465
- offered = read.offered;
466
- deeper = read.deeper;
467
- stdout.write(offered.length === 0
468
- ? `target: ${uses} publishes no types this could read, so name what it can do yourself.\n`
469
- : `target: read ${String(offered.length)} name(s) off \`${uses}\`, ${String(deeper.size)} of which hold methods.\n`);
470
- }
471
- const PROBES = [
472
- ['help', '-a'],
473
- ['--help'],
474
- ['help'],
475
- ];
476
- const COLUMN = /^\s{2,}([a-z][a-z0-9-]*):?\s{2,}\S/;
477
- const LISTED = /^\s{2,}[a-z][a-z0-9-]+(?:,\s*[a-z][a-z0-9-]+)*,?\s*$/;
478
- const readOff = (text) => {
479
- const found = new Set();
480
- for (const line of text.split('\n')) {
481
- const column = COLUMN.exec(line);
482
- if (column?.[1] !== undefined) {
483
- found.add(column[1]);
484
- continue;
485
- }
486
- if (!LISTED.test(line))
487
- continue;
488
- for (const each of line.split(',')) {
489
- const one = each.trim();
490
- if (/^[a-z][a-z0-9-]*$/.test(one))
491
- found.add(one);
492
- }
493
- }
494
- return [...found];
495
- };
496
- if (HOLDS[word].takes === 'names' && arrives === 'machine') {
497
- for (const probe of PROBES) {
498
- const asked = spawnSync(uses, [...probe], { cwd: TREE.at, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
499
- const said = readOff(`${asked.stdout ?? ''}\n${asked.stderr ?? ''}`);
500
- if (said.length < 3)
501
- continue;
502
- offered = said.filter((one) => misnamed(id, one) === undefined).sort();
503
- stdout.write(`target: read ${String(offered.length)} command(s) off \`${uses} ${probe.join(' ')}\`.\n`);
504
- break;
505
- }
506
- }
507
- const addresses = secrets.filter((one) => one.sends === undefined);
508
- const credentials = secrets.filter((one) => one.sends !== undefined);
509
- const address = HOLDS[word].addressed
510
- ? (piped.length > 0
511
- ? needed(own, 'address', 'a target that is reached somewhere')
512
- : await ask('address', `address — one of ${addresses.map((one) => one.id).join(', ') || '(declare one with `thread create secret`)'}: `)).trim()
513
- : '';
514
- if (HOLDS[word].addressed) {
515
- const found = secrets.find((one) => one.id === address);
516
- if (found === undefined) {
517
- shut();
518
- refuse(`\`${address}\` is not a secret this app declares. A target reached over ${word} needs one saying where it is — ` +
519
- 'declare it with `thread create secret`, leaving the header blank.');
520
- }
521
- if (found.sends !== undefined) {
522
- shut();
523
- refuse(`\`${address}\` has a \`sends\`, so it is a credential and not an address — it goes into the ` +
524
- `\`${found.sends.header}\` header on every call. An address is the one secret with nowhere to be sent.`);
525
- }
526
- }
527
- else if (address !== '') {
528
- shut();
529
- refuse(`a target reached over ${word} is addressed by nothing, and \`${address}\` was offered as its address`);
530
- }
531
- const carrying = listed(piped.length > 0 ? answer(own, 'credentials') : await ask('credentials', `credentials, comma-separated — ${credentials.map((one) => one.id).join(', ') || '(none declared)'}: `));
532
- for (const one of carrying) {
533
- const found = secrets.find((each) => each.id === one);
534
- if (found === undefined) {
535
- shut();
536
- refuse(`\`${one}\` is not a secret this app declares`);
537
- }
538
- if (HOLDS[word].addressed && found.sends === undefined) {
539
- shut();
540
- refuse(`\`${one}\` has no \`sends\`, so it is an address rather than something to send. One target, one address.`);
541
- }
542
- }
543
- const asked = HOLDS[word].takes === 'names'
544
- ? offered.length === 0
545
- ? 'what it can do, comma-separated — nothing was readable off it, so name them: '
546
- : deeper.size === 0
547
- ? `what it can do, comma-separated (blank takes all ${String(offered.length)}):\n ${offered.join(', ')}\n > `
548
- : `what it can do, comma-separated (blank takes none). ${String(deeper.size)} of these hold methods, and naming ` +
549
- `one asks which of them you want; the rest are called directly:\n ${offered.join(', ')}\n > `
550
- : `accepts, comma-separated ${HOLDS[word].takes} (blank for none yet): `;
551
- const named = listed(piped.length > 0 ? answer(own, 'accepts') : await ask('accepts', asked));
552
- const wide = named.length === 0 && HOLDS[word].takes === 'names' && deeper.size === 0 ? offered : named;
553
- const accepts = [];
554
- for (const one of wide) {
555
- const beneath = deeper.get(one);
556
- if (beneath === undefined) {
557
- accepts.push(one);
558
- continue;
559
- }
560
- const picked = listed(await ask(`accepts_${one}`, `what may run on \`${one}\` (blank takes all ${String(beneath.length)}):\n ${beneath.join(', ')}\n > `));
561
- for (const each of picked.length === 0 ? beneath : picked)
562
- accepts.push(`${one}.${each}`);
563
- }
564
- for (const one of accepts) {
565
- const wrong = misnamed(id, one);
566
- if (wrong !== undefined) {
567
- shut();
568
- refuse(wrong);
569
- }
570
- }
571
- if (accepts.length > 0 && HOLDS[word].takes === 'entities') {
572
- shut();
573
- refuse(`a target keeps entities, and which entities it keeps is written by \`thread create entity\` — it is the ` +
574
- `command that also writes what this target then derives: its schema, its migrations, its reference. Naming ` +
575
- `${accepts.map((one) => `\`${one}\``).join(', ')} here would put ${accepts.length === 1 ? 'it' : 'them'} in ` +
576
- 'the target and leave those rows unwritten, so `thread schema` would refuse and ask you to edit a sheet by ' +
577
- 'hand. Declare the target with `accepts` blank, then run `thread create entity` — a new one asks where it is ' +
578
- 'kept, and one that already exists is offered a home by `thread create entity edit`. Nothing was written.');
579
- }
580
- const { stated, kids } = await stating(asksOf('target', 'Target', {}, ['id', 'at', 'describes', 'version', 'uses', 'offers', 'takes', 'requires', 'accepts']), piped, piped.length > 0, ask, line, []);
581
- shut();
582
- const requires = [...(address === '' ? [] : [address]), ...carrying];
583
- const where = listBounds(text, TARGET_LIST);
584
- if (where === undefined)
585
- refuse(`${at} states no \`${TARGET_LIST}\` list to declare a target in`);
586
- if (holdsRow(text, where, id))
587
- refuse(`\`${id}\` is already declared in ${at}`);
588
- const row = [
589
- ' {',
590
- ` id: ${quoted(id)},`,
591
- ` at: '_threads/${FILE}',`,
592
- ` describes: ${quoted(describes)},`,
593
- ' version: 1,',
594
- ...(uses === '' ? [] : [` uses: ${quoted(uses)},`]),
595
- ...(HOLDS[word].takes === 'names'
596
- ? [` offers: [${accepts.map((one) => quoted(one)).join(', ')}],`, ' takes: [],']
597
- : [` takes: [${accepts.map((one) => quoted(one)).join(', ')}],`]),
598
- ` requires: [${requires.map((one) => quoted(one)).join(', ')}],`,
599
- ...rowLines(' ', stated, kids),
600
- ' },',
601
- ].join('\n');
602
- writeFileSync(path, withRow(text, where, row), 'utf8');
603
- for (const told of seeding(kids, TREE.at))
604
- stdout.write(`target: ${told}\n`);
605
- if (HOLDS[word].takes === 'names')
606
- grows(id, name, uses, accepts);
607
- const after = refusals([
608
- ...targets,
609
- {
610
- id,
611
- at: `_threads/${FILE}`,
612
- describes,
613
- version: 1,
614
- ...(HOLDS[word].takes === 'names' ? { takes: [], offers: accepts } : { takes: accepts }),
615
- requires,
616
- ...(uses === '' ? {} : { uses }),
617
- },
618
- ], secrets, calls, entities);
619
- const mine = after.filter((one) => one.includes(id));
620
- stdout.write(`target: declared ${id} in ${at} — ${holdingOf(id)}.\n`);
621
- if (mine.length > 0) {
622
- for (const one of mine)
623
- stdout.write(`target: ${one}\n`);
624
- }
625
- const kept = spawnSync('npx', ['thread', 'vault'], { cwd: TREE.at, encoding: 'utf8' });
626
- if (kept.status !== 0)
627
- stdout.write(`${`${kept.stdout ?? ''}${kept.stderr ?? ''}`.trim()}\n`);
628
- exit(0);
629
- function surfaceOf(uses, target) {
630
- const nothing = { offered: [], deeper: new Map() };
631
- const options = {
632
- moduleResolution: ts.ModuleResolutionKind.Bundler,
633
- module: ts.ModuleKind.ESNext,
634
- target: ts.ScriptTarget.ESNext,
635
- skipLibCheck: true,
636
- allowJs: true,
637
- noEmit: true,
638
- };
639
- const found = ts.resolveModuleName(uses, join(TREE.at, 'package.json'), options, ts.sys);
640
- const declaration = found.resolvedModule?.resolvedFileName;
641
- if (declaration === undefined)
642
- return nothing;
643
- const program = ts.createProgram([declaration], options);
644
- const checker = program.getTypeChecker();
645
- const file = program.getSourceFile(declaration);
646
- if (file === undefined)
647
- return nothing;
648
- const held = checker.getSymbolAtLocation(file);
649
- if (held === undefined)
650
- return nothing;
651
- const unaliased = (one) => (one.flags & ts.SymbolFlags.Alias) === 0 ? one : checker.getAliasedSymbol(one);
652
- const exported = checker.getExportsOfModule(held);
653
- const named = exported.find((one) => one.getName() === 'default' || one.getName() === 'export=');
654
- const entry = named === undefined ? undefined : unaliased(named);
655
- const typeOf = (one) => checker.getTypeOfSymbolAtLocation(one, one.declarations?.[0] ?? file);
656
- const calls = (one) => checker.getSignaturesOfType(one, ts.SignatureKind.Call);
657
- let top;
658
- if (entry !== undefined) {
659
- const declared = checker.getDeclaredTypeOfSymbol(entry);
660
- if (checker.getPropertiesOfType(declared).length > 0)
661
- top = declared;
662
- else {
663
- const signature = calls(typeOf(entry))[0];
664
- if (signature !== undefined)
665
- top = checker.getReturnTypeOfSignature(signature);
666
- }
667
- }
668
- const HIDDEN = /^_/;
669
- const inherits = (one) => {
670
- const names = new Set();
671
- for (const base of one.getBaseTypes() ?? [])
672
- for (const each of checker.getPropertiesOfType(base))
673
- names.add(each.getName());
674
- return names;
675
- };
676
- const own = (one, entry = false) => {
677
- const skip = entry ? new Set() : inherits(one);
678
- return checker.getPropertiesOfType(one).filter((each) => !HIDDEN.test(each.getName()) && !skip.has(each.getName()));
679
- };
680
- const DEEP = 3;
681
- const under = (one, depth, seen) => {
682
- if (depth > DEEP)
683
- return [];
684
- const out = [];
685
- for (const each of own(one)) {
686
- const mine = typeOf(each);
687
- if (calls(mine).length > 0) {
688
- out.push(each.getName());
689
- continue;
690
- }
691
- const shape = checker.typeToString(mine);
692
- if (seen.has(shape))
693
- continue;
694
- for (const deep of under(mine, depth + 1, new Set([...seen, shape])))
695
- out.push(`${each.getName()}.${deep}`);
696
- }
697
- return out.sort();
698
- };
699
- const walked = top !== undefined && checker.getPropertiesOfType(top).length > 0 ? own(top, true) : exported.map(unaliased);
700
- const offered = [];
701
- const deeper = new Map();
702
- for (const one of walked) {
703
- const name = one.getName();
704
- if (HIDDEN.test(name) || name === 'default' || name === 'export=')
705
- continue;
706
- if (misnamed(target, name) !== undefined)
707
- continue;
708
- const mine = typeOf(one);
709
- if (calls(mine).length > 0) {
710
- offered.push(name);
711
- continue;
712
- }
713
- const beneath = under(mine, 1, new Set()).filter((each) => misnamed(target, `${name}.${each}`) === undefined);
714
- if (beneath.length > 0) {
715
- offered.push(name);
716
- deeper.set(name, beneath);
717
- }
718
- }
719
- return { offered: offered.sort(), deeper };
720
- }
721
- function grows(id, name, uses, held) {
722
- const said = `The command this app gained by declaring \`${id}\`. \`thread run ${name} <command>\` runs one of the ` +
723
- `command(s) read off \`${uses}\` when it was declared, with every argument and flag reaching it untouched; ` +
724
- 'asked for nothing it prints that set. A command outside the set was never declared here and does not run. ' +
725
- `The row states \`_threads/run_${name}.ts\`, one line naming this integration so the shared runner knows ` +
726
- 'which it is — there is nothing in that file to change that declaring this again would not write the same ' +
727
- 'way. This verb exists only because the integration is declared: take the target away and it goes.';
728
- writeFileSync(inTree(TREE, `run_${name}.ts`), `import { ran } from '../module.ts'\n\nawait ran(${quoted(name)})\n`, 'utf8');
729
- const owner = inTree(TREE, 'integrations.ts');
730
- try {
731
- const owned = readFileSync(owner, 'utf8');
732
- const under = listBounds(owned, 'declares');
733
- if (under !== undefined && !holdsRow(owned, under, `run_${name}`)) {
734
- writeFileSync(owner, withRow(owned, under, [
735
- ' {',
736
- ` id: ${quoted(`run_${name}`)},`,
737
- ` at: ${quoted(`_threads/run_${name}.ts`)},`,
738
- ` describes: ${quoted(said)},`,
739
- ' version: 1,',
740
- ' },',
741
- ].join('\n')), 'utf8');
742
- }
743
- }
744
- catch {
745
- stdout.write(`target: _threads/integrations.ts could not be read, so \`run_${name}\` is registered but not declared.\n`);
746
- }
747
- const seat = inTree(TREE, 'derivation.ts');
748
- let listed = '';
749
- try {
750
- listed = readFileSync(seat, 'utf8');
751
- }
752
- catch {
753
- listed = '';
754
- }
755
- const into = listed === '' ? undefined : listBounds(listed, 'GENERATORS');
756
- if (into === undefined) {
757
- stdout.write(`target: ${id} is declared, but _threads/derivation.ts states no \`GENERATORS\` list, so \`thread run ${name}\` ` +
758
- `does not exist yet. Attach \`${packageOf('derivation')}\` and declare this again.\n`);
759
- }
760
- else if (holdsRow(listed, into, `run_${name}`)) {
761
- stdout.write(`target: \`thread run ${name}\` already resolves — left as it was.\n`);
762
- }
763
- else {
764
- writeFileSync(seat, withRow(listed, into, [
765
- ' {',
766
- ` id: ${quoted(`run_${name}`)},`,
767
- ` at: ${quoted(`_threads/run_${name}.ts`)},`,
768
- ` describes: ${quoted(said)},`,
769
- ' version: 1,',
770
- ` by: ${quoted(`thread run ${name}`)},`,
771
- ' },',
772
- ].join('\n')), 'utf8');
773
- stdout.write(`target: \`thread run ${name}\` is now a command on this tree — ${String(held.length)} of them under it.\n`);
774
- }
775
- }