@prestack/thread 0.1.21 → 0.1.23

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 (138) hide show
  1. package/CHANGELOG.md +227 -0
  2. package/dist/core/attach.js +21 -0
  3. package/dist/core/descend.js +6 -3
  4. package/dist/core/generators/commit.js +55 -3
  5. package/dist/core/generators/dev.d.ts +1 -0
  6. package/dist/core/generators/dev.js +69 -0
  7. package/dist/derivation/generators/build.d.ts +3 -1
  8. package/dist/derivation/generators/build.js +104 -22
  9. package/dist/derivation/generators/release.js +74 -25
  10. package/dist/derivation/release.js +3 -3
  11. package/dist/integrations/cli.d.ts +0 -1
  12. package/dist/integrations/cli.js +69 -11
  13. package/dist/integrations/declared.d.ts +2 -0
  14. package/dist/integrations/declared.js +6 -1
  15. package/dist/integrations/pglite.js +13 -3
  16. package/dist/integrations/target.d.ts +3 -0
  17. package/dist/integrity/attach.js +1 -5
  18. package/dist/integrity/slopnators/sl_stale_path.js +10 -0
  19. package/dist/operations/access.js +3 -2
  20. package/dist/operations/capabilities.d.ts +2 -0
  21. package/dist/operations/capabilities.js +2 -0
  22. package/dist/operations/generators/capability.js +237 -24
  23. package/dist/operations/generators/compile.js +150 -50
  24. package/dist/operations/generators/seed.d.ts +3 -0
  25. package/dist/operations/generators/seed.js +25 -3
  26. package/dist/operations/nodes.d.ts +2 -2
  27. package/package.json +1 -1
  28. package/dist/core/actor.ts +0 -21
  29. package/dist/core/attach.ts +0 -613
  30. package/dist/core/chain.ts +0 -145
  31. package/dist/core/condition.ts +0 -227
  32. package/dist/core/descend.ts +0 -1185
  33. package/dist/core/errors/catalogue.ts +0 -314
  34. package/dist/core/errors/classes.ts +0 -51
  35. package/dist/core/errors/error.ts +0 -152
  36. package/dist/core/errors/namespaces.ts +0 -19
  37. package/dist/core/finding.ts +0 -37
  38. package/dist/core/generators/commit.ts +0 -157
  39. package/dist/core/init.ts +0 -222
  40. package/dist/core/invoked.ts +0 -52
  41. package/dist/core/round.ts +0 -74
  42. package/dist/core/rows.ts +0 -506
  43. package/dist/core/scope.ts +0 -20
  44. package/dist/core/thread.ts +0 -29
  45. package/dist/core/tree.ts +0 -90
  46. package/dist/core/types.ts +0 -351
  47. package/dist/derivation/attach.ts +0 -57
  48. package/dist/derivation/generators/build.ts +0 -371
  49. package/dist/derivation/generators/check.ts +0 -118
  50. package/dist/derivation/generators/determinism.ts +0 -145
  51. package/dist/derivation/generators/release.ts +0 -236
  52. package/dist/derivation/release.ts +0 -43
  53. package/dist/integrations/attach.ts +0 -187
  54. package/dist/integrations/cli.ts +0 -606
  55. package/dist/integrations/declared.ts +0 -34
  56. package/dist/integrations/disk.ts +0 -466
  57. package/dist/integrations/generators/read.ts +0 -127
  58. package/dist/integrations/generators/vault.ts +0 -210
  59. package/dist/integrations/https.ts +0 -216
  60. package/dist/integrations/pglite.ts +0 -206
  61. package/dist/integrations/secret.ts +0 -54
  62. package/dist/integrations/target.ts +0 -672
  63. package/dist/integrations/vault.ts +0 -148
  64. package/dist/integrity/attach.ts +0 -150
  65. package/dist/integrity/check-threads.ts +0 -166
  66. package/dist/integrity/constraint.ts +0 -1246
  67. package/dist/integrity/generators/compiler.ts +0 -138
  68. package/dist/integrity/generators/gate.ts +0 -227
  69. package/dist/integrity/generators/issues.ts +0 -160
  70. package/dist/integrity/generators/publish.ts +0 -233
  71. package/dist/integrity/generators/trial.ts +0 -359
  72. package/dist/integrity/lint.ts +0 -214
  73. package/dist/integrity/lsp/bin.ts +0 -44
  74. package/dist/integrity/lsp/diagnostics.ts +0 -92
  75. package/dist/integrity/lsp/findings.ts +0 -63
  76. package/dist/integrity/lsp/lint.ts +0 -124
  77. package/dist/integrity/lsp/protocol.ts +0 -78
  78. package/dist/integrity/lsp/server.ts +0 -224
  79. package/dist/integrity/slopnator.ts +0 -324
  80. package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
  81. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
  82. package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
  83. package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
  84. package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
  85. package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
  86. package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
  87. package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
  88. package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
  89. package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
  90. package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
  91. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
  92. package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
  93. package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
  94. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
  95. package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
  96. package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
  97. package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
  98. package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
  99. package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
  100. package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
  101. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
  102. package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
  103. package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
  104. package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
  105. package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
  106. package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
  107. package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
  108. package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
  109. package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
  110. package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
  111. package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
  112. package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
  113. package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
  114. package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
  115. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
  116. package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
  117. package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
  118. package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
  119. package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
  120. package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
  121. package/dist/integrity/sweep.ts +0 -395
  122. package/dist/knowledge/attach.ts +0 -327
  123. package/dist/knowledge/generators/docsgen.ts +0 -186
  124. package/dist/knowledge/generators/glossary.ts +0 -73
  125. package/dist/knowledge/glossary.ts +0 -6
  126. package/dist/operations/access.ts +0 -131
  127. package/dist/operations/attach.ts +0 -408
  128. package/dist/operations/capabilities.ts +0 -88
  129. package/dist/operations/dispatch.ts +0 -294
  130. package/dist/operations/error-results.ts +0 -31
  131. package/dist/operations/generators/capability.ts +0 -1054
  132. package/dist/operations/generators/compile.ts +0 -701
  133. package/dist/operations/generators/permits.ts +0 -229
  134. package/dist/operations/generators/seed.ts +0 -359
  135. package/dist/operations/generators/trial.ts +0 -666
  136. package/dist/operations/nodes.ts +0 -53
  137. package/dist/operations/operations.ts +0 -91
  138. package/dist/operations/result.ts +0 -28
@@ -12,7 +12,7 @@ import { argv, cwd, stderr, stdout } from 'node:process';
12
12
  import { pathToFileURL } from 'node:url';
13
13
  import { build, initialize, stop } from 'esbuild-wasm/esm/browser.js';
14
14
  import { descend, reached } from '../../core/descend.js';
15
- import { existsSync, readFileSync, realpathSync, statSync, writeFileSync } from '../../integrations/disk.js';
15
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from '../../integrations/disk.js';
16
16
  import { exit, invoked } from '../../core/invoked.js';
17
17
  import { valuesIn } from '../../core/rows.js';
18
18
  import { findTree } from '../../core/tree.js';
@@ -83,35 +83,110 @@ function exported(held, sub, conditions) {
83
83
  }
84
84
  return undefined;
85
85
  }
86
- function packaged(specifier, from, conditions, top) {
87
- const name = specifier.split('/').slice(0, specifier.startsWith('@') ? 2 : 1).join('/');
88
- const sub = `.${specifier.slice(name.length)}`;
86
+ function named(specifier) {
87
+ return specifier.split('/').slice(0, specifier.startsWith('@') ? 2 : 1).join('/');
88
+ }
89
+ function rooted(name, from, top) {
89
90
  for (let at = from; inside(top, at); at = dirname(at)) {
90
91
  const root = join(at, 'node_modules', name);
91
- const manifest = join(root, 'package.json');
92
- if (filed(manifest)) {
93
- const held = JSON.parse(readFileSync(manifest, 'utf8'));
94
- if (held.exports !== undefined) {
95
- const found = exported(held.exports, sub, conditions);
96
- return found === undefined ? undefined : located(join(root, found));
97
- }
98
- const main = typeof held.main === 'string' ? held.main : typeof held.module === 'string' ? held.module : 'index.js';
99
- return located(join(root, sub === '.' ? main : sub));
100
- }
92
+ if (filed(join(root, 'package.json')))
93
+ return root;
101
94
  if (at === top)
102
95
  return undefined;
103
96
  }
104
97
  return undefined;
105
98
  }
106
- const folder = (outside, top) => ({
99
+ function packaged(specifier, from, conditions, top) {
100
+ const name = named(specifier);
101
+ const sub = `.${specifier.slice(name.length)}`;
102
+ const root = rooted(name, from, top);
103
+ if (root === undefined)
104
+ return undefined;
105
+ const held = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
106
+ if (held.exports !== undefined) {
107
+ const found = exported(held.exports, sub, conditions);
108
+ return found === undefined ? undefined : located(join(root, found));
109
+ }
110
+ const main = typeof held.main === 'string' ? held.main : typeof held.module === 'string' ? held.module : 'index.js';
111
+ return located(join(root, sub === '.' ? main : sub));
112
+ }
113
+ function binary(at) {
114
+ const bytes = readFileSync(at, 'latin1');
115
+ if (bytes.length < 4)
116
+ return false;
117
+ const byte = (i) => bytes.charCodeAt(i);
118
+ if (byte(0) === 0x7f && bytes.slice(1, 4) === 'ELF')
119
+ return true;
120
+ if (byte(0) === 0x4d && byte(1) === 0x5a)
121
+ return true;
122
+ if (bytes.slice(0, 4) === '\0asm')
123
+ return true;
124
+ if (byte(1) === 0xfa && byte(2) === 0xed && byte(3) === 0xfe && (byte(0) === 0xce || byte(0) === 0xcf))
125
+ return true;
126
+ if (byte(0) === 0xfe && byte(1) === 0xed && byte(2) === 0xfa && (byte(3) === 0xce || byte(3) === 0xcf))
127
+ return true;
128
+ if (byte(0) === 0xca && byte(1) === 0xfe && byte(2) === 0xba && (byte(3) === 0xbe || byte(3) === 0xbf))
129
+ return true;
130
+ return false;
131
+ }
132
+ function owned(root) {
133
+ const found = [];
134
+ const seen = new Set();
135
+ const walk = (at) => {
136
+ const real = realpathSync(at);
137
+ if (seen.has(real))
138
+ return;
139
+ seen.add(real);
140
+ for (const one of readdirSync(at)) {
141
+ if (one === 'node_modules')
142
+ continue;
143
+ const path = join(at, one);
144
+ const held = statSync(path, { throwIfNoEntry: false });
145
+ if (held?.isDirectory() === true)
146
+ walk(path);
147
+ else if (held?.isFile() === true)
148
+ found.push(path);
149
+ }
150
+ };
151
+ walk(root);
152
+ return found;
153
+ }
154
+ const staying = new Map();
155
+ function installed(root, top, visiting = new Set()) {
156
+ const cached = staying.get(root);
157
+ if (cached !== undefined)
158
+ return cached;
159
+ if (visiting.has(root))
160
+ return false;
161
+ visiting.add(root);
162
+ let decided = owned(root).some(binary);
163
+ if (!decided) {
164
+ const held = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
165
+ const names = Object.keys(held.optionalDependencies ?? {});
166
+ decided = names.some((one) => {
167
+ const found = rooted(one, root, top);
168
+ return found !== undefined && installed(found, top, visiting);
169
+ });
170
+ }
171
+ visiting.delete(root);
172
+ staying.set(root, decided);
173
+ return decided;
174
+ }
175
+ const folder = (outside, top, keeping) => ({
107
176
  name: 'thread-folder',
108
177
  setup(held) {
109
178
  held.onResolve({ filter: /.*/ }, (args) => {
110
179
  const bare = !args.path.startsWith('.') && !isAbsolute(args.path);
111
180
  if (args.path.startsWith('node:') || RUNTIME.has(args.path))
112
181
  return { path: args.path, external: true, sideEffects: false };
113
- if (bare && outside)
114
- return { path: args.path, external: true };
182
+ if (bare && outside) {
183
+ const name = named(args.path);
184
+ const root = rooted(name, args.resolveDir, top);
185
+ if (root !== undefined && installed(root, top)) {
186
+ keeping?.add(name);
187
+ return { path: args.path, external: true };
188
+ }
189
+ }
115
190
  const conditions = args.kind === 'require-call' || args.kind === 'require-resolve' ? REQUIRED : IMPORTED;
116
191
  const path = resolve(args.resolveDir, args.path);
117
192
  const found = bare ? packaged(args.path, args.resolveDir, conditions, top) : inside(top, path) ? located(path) : undefined;
@@ -141,13 +216,13 @@ export async function bundle(options) {
141
216
  hosted.self ??= globalThis;
142
217
  await initialize({ wasmModule: compiled, worker: false });
143
218
  try {
144
- const { packages, plugins, absWorkingDir = cwd(), ...rest } = options;
219
+ const { packages, plugins, keeping, absWorkingDir = cwd(), ...rest } = options;
145
220
  const made = await build({
146
221
  absWorkingDir,
147
222
  ...rest,
148
223
  bundle: true,
149
224
  write: false,
150
- plugins: [...(plugins ?? []), folder(packages === 'external', absWorkingDir)],
225
+ plugins: [...(plugins ?? []), folder(packages === 'external', absWorkingDir, keeping)],
151
226
  });
152
227
  const text = made.outputFiles?.[0]?.text;
153
228
  if (text === undefined)
@@ -323,13 +398,15 @@ async function main() {
323
398
  }
324
399
  const forBrowser = browsing(entry);
325
400
  const refusals = forBrowser ? withheld(pass) : new Map();
401
+ const keeping = forBrowser ? undefined : new Set();
326
402
  let held;
327
403
  try {
328
404
  held = await bundle({
329
405
  entryPoints: [resolve(ROOT, entry)],
330
406
  format: 'esm',
407
+ minify: true,
331
408
  platform: forBrowser ? 'browser' : 'node',
332
- ...(forBrowser ? {} : { packages: 'external' }),
409
+ ...(keeping === undefined ? {} : { packages: 'external', keeping }),
333
410
  outfile: at,
334
411
  plugins: [declared(), ...(refusals.size === 0 ? [] : [withholding(refusals)]), ...compiling],
335
412
  logLevel: 'silent',
@@ -339,16 +416,21 @@ async function main() {
339
416
  stderr.write(`build: ${out} was not written — ${thrown instanceof Error ? thrown.message : String(thrown)}\n`);
340
417
  exit(1);
341
418
  }
419
+ const installs = keeping === undefined
420
+ ? ''
421
+ : keeping.size === 0
422
+ ? ' Every package it imports is compiled in, so npm installs nothing to run it.'
423
+ : ` It still needs npm to install ${String(keeping.size)} package${keeping.size === 1 ? '' : 's'}: ${[...keeping].sort().join(', ')}. Each one loads a native file or starts a program that only exists once npm installs it.`;
342
424
  if (checking) {
343
425
  if (held !== before) {
344
426
  stderr.write(`build: ${out} is not what ${BUILDS} writes. A built artifact has no author — run \`${BUILDS}\` and commit the result.\n`);
345
427
  exit(1);
346
428
  }
347
- stdout.write(`build: ${out} is up to date — ${String(new Set(reached()).size)} declared file(s) reachable.\n`);
429
+ stdout.write(`build: ${out} is up to date — ${String(new Set(reached()).size)} declared file(s) reachable.${installs}\n`);
348
430
  continue;
349
431
  }
350
432
  writeFileSync(at, held, 'utf8');
351
- stdout.write(`build: wrote ${out} from ${entry} — ${String(new Set(reached()).size)} declared file(s) reachable.\n`);
433
+ stdout.write(`build: wrote ${out} from ${entry} — ${String(new Set(reached()).size)} declared file(s) reachable.${installs}\n`);
352
434
  }
353
435
  }
354
436
  if (invoked(import.meta.url))
@@ -1,15 +1,21 @@
1
1
  import { join } from 'node:path';
2
2
  import { argv, stderr, stdout } from 'node:process';
3
- import { existsSync, readFileSync, writeFileSync } from '../../integrations/disk.js';
3
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from '../../integrations/disk.js';
4
+ import { COPY_AT, PROGRAM } from '../../integrations/cli.js';
5
+ import { VAULT_DIR } from '../../integrations/vault.js';
4
6
  import { exit, invoked } from '../../core/invoked.js';
5
- import { marked, valuesIn } from '../../core/rows.js';
7
+ import { markOf, valuesIn } from '../../core/rows.js';
6
8
  import { ours } from '../../core/scope.js';
7
9
  import { findTree, inTree } from '../../core/tree.js';
10
+ import { declaresUi } from '../../integrations/declared.js';
11
+ import { SERVER_OUT } from '../../operations/capabilities.js';
8
12
  import { CHECKED_BY, KINDS, LISTENS, SAID, WRITES, WRITTEN_BY, kindOf } from '../release.js';
9
13
  const TREE = findTree();
10
14
  const MANIFEST = 'package.json';
15
+ const RECEIPT = join(VAULT_DIR, 'gate.json');
16
+ const ROUTE = /^app\/(?:.+\/)?route\.ts$/;
11
17
  const addressed = (rows) => rows.flatMap((one) => [...(one.at === undefined ? [] : [one.at]), ...addressed(one.derives ?? [])]);
12
- const mark = (one) => marked([one.id ?? '', one.at ?? '']);
18
+ const mark = (one) => markOf(one);
13
19
  function releases() {
14
20
  const at = inTree(TREE, 'derivation.ts');
15
21
  if (!existsSync(at))
@@ -32,35 +38,73 @@ function manager() {
32
38
  return 'npm';
33
39
  }
34
40
  }
35
- function scripted() {
36
- const tool = manager();
37
- return { build: `${tool} run build`, start: tool === 'npm' ? 'npm start' : `${tool} start` };
41
+ function served() {
42
+ const found = new Set();
43
+ const walk = (held) => {
44
+ if (held === null || typeof held !== 'object')
45
+ return;
46
+ const row = held;
47
+ const routes = Array.isArray(row.derives) ? row.derives.filter((one) => ROUTE.test(one.at ?? '')) : [];
48
+ if (typeof row.at === 'string' && !row.at.startsWith(`${TREE.name}/`) && routes.length > 0)
49
+ found.add(row.at);
50
+ for (const one of Object.values(held))
51
+ walk(one);
52
+ };
53
+ for (const name of readdirSync(TREE.dir).filter((one) => one.endsWith('.ts')).sort()) {
54
+ walk(valuesIn(readFileSync(join(TREE.dir, name), 'utf8')));
55
+ }
56
+ return [...found].sort();
57
+ }
58
+ function runsThread() {
59
+ const at = inTree(TREE, 'targets.ts');
60
+ if (!existsSync(at))
61
+ return false;
62
+ const held = valuesIn(readFileSync(at, 'utf8'));
63
+ return (held.TARGETS ?? []).some((one) => one.id === PROGRAM);
38
64
  }
39
- function dockerfile(build, start) {
65
+ function dockerfile() {
40
66
  const tool = manager();
41
67
  const corepack = tool === 'npm' ? [] : ['RUN corepack enable'];
42
68
  const lock = tool === 'npm' ? 'package-lock.json*' : `${tool}-lock.yaml*`;
43
- const first = tool === 'npm'
44
- ? 'RUN npm ci --omit=dev --ignore-scripts || npm install --omit=dev --ignore-scripts'
45
- : `RUN ${tool} install --prod --frozen-lockfile --ignore-scripts || ${tool} install --prod --ignore-scripts`;
46
- const all = tool === 'npm' ? 'RUN npm install --ignore-scripts' : `RUN ${tool} install --ignore-scripts`;
69
+ const install = tool === 'npm'
70
+ ? 'RUN npm ci --ignore-scripts || npm install --ignore-scripts'
71
+ : `RUN ${tool} install --frozen-lockfile --ignore-scripts || ${tool} install --ignore-scripts`;
72
+ const thread = runsThread();
73
+ const carried = [RECEIPT, ...served(), ...(thread ? [COPY_AT] : [])];
74
+ const running = ['WORKDIR /app', 'ENV NODE_ENV=production', 'ENV HOSTNAME=0.0.0.0', `ENV PORT=${String(LISTENS)}`];
75
+ const ending = [...(thread ? [`RUN mkdir -p ${TREE.name}`] : []), `EXPOSE ${String(LISTENS)}`];
76
+ if (!declaresUi(TREE)) {
77
+ const production = tool === 'npm'
78
+ ? 'RUN npm ci --omit=dev --ignore-scripts || npm install --omit=dev --ignore-scripts'
79
+ : `RUN ${tool} install --prod --frozen-lockfile --ignore-scripts || ${tool} install --prod --ignore-scripts`;
80
+ return [
81
+ 'FROM node:22-slim',
82
+ ...running,
83
+ ...corepack,
84
+ `COPY package.json ${lock} ./`,
85
+ production,
86
+ ...[SERVER_OUT, ...carried].map((one) => `COPY ${one} ./${one}`),
87
+ ...ending,
88
+ `CMD ${JSON.stringify(['node', SERVER_OUT])}`,
89
+ '',
90
+ ].join('\n');
91
+ }
47
92
  return [
48
93
  'FROM node:22-slim AS build',
49
94
  'WORKDIR /app',
50
95
  ...corepack,
51
96
  `COPY package.json ${lock} ./`,
52
- first,
97
+ install,
53
98
  'COPY . .',
54
- all,
55
- `RUN ${build}`,
99
+ `RUN ${tool} run build`,
56
100
  '',
57
101
  'FROM node:22-slim',
58
- 'WORKDIR /app',
59
- ...corepack,
60
- 'ENV NODE_ENV=production',
61
- 'COPY --from=build /app ./',
62
- `EXPOSE ${String(LISTENS)}`,
63
- `CMD ${JSON.stringify(start.split(' '))}`,
102
+ ...running,
103
+ 'COPY --from=build /app/.next/standalone ./',
104
+ 'COPY --from=build /app/.next/static ./.next/static',
105
+ ...carried.map((one) => `COPY --from=build /app/${one} ./${one}`),
106
+ ...ending,
107
+ `CMD ${JSON.stringify(['node', 'server.js'])}`,
64
108
  '',
65
109
  ].join('\n');
66
110
  }
@@ -76,7 +120,10 @@ function compose(id) {
76
120
  }
77
121
  function withheld() {
78
122
  return [
79
- '.thread',
123
+ VAULT_DIR,
124
+ `!${RECEIPT}`,
125
+ ...(runsThread() ? [`!${COPY_AT}`] : []),
126
+ '.next',
80
127
  'node_modules',
81
128
  '.git',
82
129
  '',
@@ -85,7 +132,6 @@ function withheld() {
85
132
  function made() {
86
133
  const found = new Map();
87
134
  const rows = releases();
88
- const { build, start } = scripted();
89
135
  for (const one of rows) {
90
136
  const id = one.id;
91
137
  if (id === undefined)
@@ -94,7 +140,7 @@ function made() {
94
140
  if (kind === undefined)
95
141
  continue;
96
142
  if (kind === 'docker') {
97
- found.set('Dockerfile', dockerfile(build, start));
143
+ found.set('Dockerfile', dockerfile());
98
144
  found.set('compose.yaml', compose(id));
99
145
  found.set('.dockerignore', withheld());
100
146
  }
@@ -187,8 +233,11 @@ function run() {
187
233
  }
188
234
  for (const [at, text] of files)
189
235
  writeFileSync(join(TREE.at, at), text, 'utf8');
190
- stdout.write('release: wrote ' + [...files.keys()].join(', ') + ' — built and started by what this app\'s own manifest ' +
191
- 'declares, listening on ' + String(LISTENS) + '.\n' +
236
+ stdout.write('release: wrote ' + [...files.keys()].join(', ') + ' — ' +
237
+ (declaresUi(TREE)
238
+ ? 'built by the build this app\'s own manifest declares, and started by `node server.js` from what that build compiled'
239
+ : 'started by `node ' + SERVER_OUT + '`, the server `thread build` compiled into this tree, beside only the packages installed for production') +
240
+ ', listening on ' + String(LISTENS) + '.\n' +
192
241
  'release: no secret is named there. A secret is read from the sealed store at the moment it is used, and ' +
193
242
  'naming it here would be telling whoever deploys this to put a credential in the environment instead.\n');
194
243
  return 0;
@@ -4,7 +4,7 @@ export const WRITES = {
4
4
  docker: [
5
5
  {
6
6
  at: 'Dockerfile',
7
- describes: 'How this app becomes an image and what starts it inside one — the build and start commands its own manifest declares, run with the package manager that manifest names, and the port its server listens on.',
7
+ describes: 'How this app becomes an image, and what starts it inside one. An app that declares `ui` is built inside the image by the build command its own manifest declares, with the package manager that manifest names, and started with `node server.js` from what Next compiled. One that does not builds nothing there: it is started with Node on the server `thread build` compiled into this tree, beside only the packages its manifest installs for production. Either way the image keeps the files its routes read from disk, the receipt of the gate that passed, and the compiled thread command when this app runs one. Nothing of the framework\'s source goes in either way.',
8
8
  },
9
9
  {
10
10
  at: 'compose.yaml',
@@ -12,13 +12,13 @@ export const WRITES = {
12
12
  },
13
13
  {
14
14
  at: '.dockerignore',
15
- describes: 'What a build of this image must never carry out of this directory: the sealed store and the key that opens it, the installed packages, and the history. Without it the build copies everything beside it, and every sealed value goes into the image and into whoever pulls it.',
15
+ describes: 'What a build of this image must never carry out of this directory: the sealed store and the key that opens it, an earlier build, the installed packages, and the history. From the sealed store\'s folder it lets through only the receipt of the gate that passed and, when this app runs the thread command, the compiled copy of that command. Neither holds a secret. Without it the build copies everything beside it, and every sealed value goes into the image and into whoever pulls it.',
16
16
  },
17
17
  ],
18
18
  };
19
19
  export const LISTENS = 3000;
20
20
  export const SAID = {
21
- docker: 'A container image built from this app and the compose file that runs it. It is the one that needs no account and no network to be proven, so it is what an app can be taken end to end with before anybody has chosen where it really lives. What it holds is derived: the build and start commands this app already declares, the port its own server listens on, and the name of every secret it says it must be given — the names only, because a value is typed by a person and a row has never held one.',
21
+ docker: 'A container image built from this app and the compose file that runs it. It needs no account and no network to be proven, so an app can be taken end to end with it before anybody has chosen where it really lives. What it holds is derived: the server, on the port it listens on, with the files its routes read from disk, the receipt of the gate that passed, and the compiled thread command when this app runs one. An app that declares `ui` is served by what Next compiles, started with `node server.js`; one that does not is served by the server `thread build` compiled into this tree, started with Node beside only the packages its manifest installs for production. It names no secret, because a value is typed by a person and read from the sealed store where it is used.',
22
22
  };
23
23
  export const kindOf = (id) => KINDS.find((one) => id.startsWith(PREFIXES[one]));
24
24
  export const WRITTEN_BY = 'thread release';
@@ -26,7 +26,6 @@ export declare const PROGRAM: string;
26
26
  export declare const COPIES: string;
27
27
  export declare const COPY_AT: string;
28
28
  export declare function running(name: string, subcommands: Readonly<Record<string, string>>, at: string): Running;
29
- export { calls, expansions, type Expansion } from '../integrations/declared.ts';
30
29
  export { LOCKFILES };
31
30
  export declare const manager: (into: string) => string;
32
31
  export declare function adding(into: string): readonly [string, string];
@@ -263,6 +263,46 @@ function forgets(one, asked) {
263
263
  one.known.delete(key);
264
264
  }
265
265
  }
266
+ const AHEAD = 32;
267
+ const upOf = (at) => (at.includes('/') ? at.slice(0, at.lastIndexOf('/')) : '');
268
+ const nameOf = (at) => at.slice(at.lastIndexOf('/') + 1);
269
+ const inside = (at, name) => (at === '' ? name : at + '/' + name);
270
+ function listedAt(one, at) {
271
+ const held = one.known.get(told({ order: 'list', at }))?.found;
272
+ return Array.isArray(held) ? held : [];
273
+ }
274
+ function realOf(one, at) {
275
+ const held = one.known.get(told({ order: 'real', at }));
276
+ if (held !== undefined || at === '')
277
+ return held;
278
+ const entry = listedAt(one, upOf(at)).find((each) => each.name === nameOf(at));
279
+ if (entry === undefined || entry.kind === 'other')
280
+ return undefined;
281
+ const above = realOf(one, upOf(at));
282
+ return typeof above?.at === 'string' ? { answered: 'real', at: above.at + '/' + nameOf(at) } : undefined;
283
+ }
284
+ function aheadOf(one, asked) {
285
+ if (asked.order === 'real')
286
+ return asked.at === '' || one.known.has(told({ order: 'real', at: '' })) ? [] : [{ order: 'real', at: '', ahead: true }];
287
+ const name = nameOf(asked.at);
288
+ const dot = name.indexOf('.');
289
+ const reading = asked.order === 'read' && dot > 0 && asked.at.split('/').includes(INSTALLED);
290
+ if (!reading && asked.order !== 'stat')
291
+ return [];
292
+ const suffix = reading ? name.slice(dot) : '';
293
+ const up = upOf(asked.at);
294
+ const found = [];
295
+ for (const each of listedAt(one, up)) {
296
+ if (found.length === AHEAD)
297
+ break;
298
+ if (each.kind !== 'file' || each.name === name || !each.name.endsWith(suffix))
299
+ continue;
300
+ const order = { order: asked.order, at: inside(up, each.name), ahead: true };
301
+ if (!one.known.has(told(order)))
302
+ found.push(order);
303
+ }
304
+ return found;
305
+ }
266
306
  const plainly = (one) => one.order + ' ' + (one.at === '' ? '.' : one.at);
267
307
  const outside = (root, at) => {
268
308
  const step = relative(root, at);
@@ -315,11 +355,22 @@ function watching(one) {
315
355
  reply(one.dir, short, known);
316
356
  continue;
317
357
  }
358
+ const derived = asked.order === 'real' ? realOf(one, asked.at) : undefined;
359
+ if (derived !== undefined) {
360
+ reply(one.dir, short, derived);
361
+ kept(one, asked, derived);
362
+ continue;
363
+ }
318
364
  if (!READS.has(asked.order))
319
365
  forgets(one, asked);
320
366
  const due = one.replaying.shift();
321
367
  if (due !== undefined) {
322
368
  if (told(due.order) === told(asked)) {
369
+ while (one.replaying[0]?.order.ahead === true) {
370
+ const extra = one.replaying.shift();
371
+ if (extra !== undefined)
372
+ kept(one, extra.order, extra.answered);
373
+ }
323
374
  reply(one.dir, short, due.answered);
324
375
  kept(one, asked, due.answered);
325
376
  continue;
@@ -334,7 +385,8 @@ function watching(one) {
334
385
  }
335
386
  one.pending = short;
336
387
  one.asking = asked;
337
- hand(one, still(one, [asked]));
388
+ one.ahead = aheadOf(one, asked);
389
+ hand(one, still(one, [asked, ...one.ahead]));
338
390
  }
339
391
  }, TICK);
340
392
  }
@@ -362,11 +414,11 @@ function next(one, within) {
362
414
  }, Math.max(1_000, within - MARGIN));
363
415
  });
364
416
  }
365
- function begun(from, word, args, root, hears, called, recording = []) {
417
+ function begun(from, word, args, root, hears, called, recording = [], stash) {
366
418
  const of = called ?? randomBytes(6).toString('hex');
367
419
  const dir = join(tmpdir(), SERVED, randomBytes(6).toString('hex'));
368
420
  writeFileSync(join(dir, 'channel'), '', 'utf8');
369
- const one = { of, dir, root, out: '', err: '', why: '', pending: '', waiting: [], parked: [], replaying: [...recording], handed: recording.length, known: new Map() };
421
+ const one = { of, dir, root, out: '', err: '', why: '', pending: '', waiting: [], parked: [], replaying: [...recording], handed: recording.length, known: new Map(), ahead: [], ...(stash === undefined ? {} : { stash }) };
370
422
  const child = spawn(execPath, [...from.words, word, ...args], {
371
423
  cwd: from.own,
372
424
  shell: false,
@@ -397,6 +449,7 @@ function counted(one, held) {
397
449
  one.last = held;
398
450
  return held;
399
451
  }
452
+ const stashing = (one, held) => (one.stash === undefined ? held : { ...held, stashed: one.stash });
400
453
  async function served(word, args, within, how) {
401
454
  const of = how?.of;
402
455
  if (of !== undefined) {
@@ -411,20 +464,26 @@ async function served(word, args, within, how) {
411
464
  const from = startable();
412
465
  if (typeof from === 'string')
413
466
  return nowhere(from);
414
- const again = begun(from, word, args, root, how?.hears, of, recording);
415
- return counted(again, await next(again, within));
467
+ const again = begun(from, word, args, root, how?.hears, of, recording, how?.stash);
468
+ return stashing(again, counted(again, await next(again, within)));
416
469
  }
417
470
  const answering = how?.answering;
418
471
  if (answering !== undefined && answering < one.handed && one.last !== undefined)
419
- return one.last;
420
- const given = (how?.answered ?? [])[0];
472
+ return stashing(one, one.last);
473
+ const [given, ...rest] = how?.answered ?? [];
421
474
  if (one.pending !== '' && given !== undefined) {
475
+ one.ahead.forEach((order, index) => {
476
+ const held = rest[index];
477
+ if (held !== undefined)
478
+ kept(one, order, held);
479
+ });
480
+ one.ahead = [];
422
481
  reply(one.dir, one.pending, given);
423
482
  if (one.asking !== undefined)
424
483
  kept(one, one.asking, given);
425
484
  one.pending = '';
426
485
  }
427
- return counted(one, await next(one, within));
486
+ return stashing(one, counted(one, await next(one, within)));
428
487
  }
429
488
  const root = how?.at ?? '';
430
489
  if (root === '')
@@ -432,8 +491,8 @@ async function served(word, args, within, how) {
432
491
  const from = startable();
433
492
  if (typeof from === 'string')
434
493
  return nowhere(from);
435
- const fresh = begun(from, word, args, root, how?.hears);
436
- return counted(fresh, await next(fresh, within));
494
+ const fresh = begun(from, word, args, root, how?.hears, undefined, [], how?.stash);
495
+ return stashing(fresh, counted(fresh, await next(fresh, within)));
437
496
  }
438
497
  export function running(name, subcommands, at) {
439
498
  const out = {};
@@ -451,7 +510,6 @@ export function running(name, subcommands, at) {
451
510
  }
452
511
  return out;
453
512
  }
454
- export { calls, expansions } from '../integrations/declared.js';
455
513
  export { LOCKFILES };
456
514
  export const manager = (into) => managerOf(into, existsSync);
457
515
  const ADDS = { pnpm: 'add', yarn: 'add', bun: 'add', npm: 'install' };
@@ -1,4 +1,5 @@
1
1
  import { type Call, type Running, type Target } from '../integrations/target.ts';
2
+ import { type Tree } from '../core/tree.ts';
2
3
  export interface Expansion {
3
4
  readonly program: string;
4
5
  readonly target: Target;
@@ -6,3 +7,4 @@ export interface Expansion {
6
7
  }
7
8
  export declare function expansions(at: string): readonly Expansion[];
8
9
  export declare function calls(target: string, at: string): Running;
10
+ export declare function declaresUi(tree: Tree): boolean;
@@ -1,7 +1,7 @@
1
1
  import { running } from '../integrations/cli.js';
2
2
  import { existsSync, readFileSync } from '../integrations/disk.js';
3
3
  import { CALL_PREFIX, REACH } from '../integrations/target.js';
4
- import { valuesIn } from '../core/rows.js';
4
+ import { holdsRow, listBounds, valuesIn } from '../core/rows.js';
5
5
  import { findTree, inTree } from '../core/tree.js';
6
6
  function sheet(at) {
7
7
  const path = inTree(findTree(at), 'targets.ts');
@@ -20,3 +20,8 @@ export function calls(target, at) {
20
20
  named[one.id.replace(CALL_PREFIX, '')] = one.path;
21
21
  return running(target.replace(REACH.cli, ''), named, at);
22
22
  }
23
+ export function declaresUi(tree) {
24
+ const held = readFileSync(tree.root, 'utf8');
25
+ const declares = listBounds(held, 'declares');
26
+ return declares !== undefined && holdsRow(held, declares, 'ui');
27
+ }
@@ -52,14 +52,24 @@ export async function shapeOf(sql) {
52
52
  }
53
53
  }
54
54
  export function standing(held) {
55
- const store = storing({ ...held, at: 'memory://', migrations: [] });
55
+ const store = storing({ ...held, at: 'memory://' });
56
56
  const keeps = Object.keys(held.tables);
57
57
  return {
58
58
  keeps,
59
59
  store,
60
60
  async seed(actor, rows) {
61
- for (const one of rows)
61
+ for (const one of rows) {
62
+ const by = held.address[one.entity];
63
+ const key = by === undefined ? undefined : one.row[by];
64
+ if (by !== undefined && (typeof key === 'string' || typeof key === 'number')) {
65
+ const there = await store.list(actor, one.entity, { where: { [by]: key }, limit: 1 });
66
+ if (there.length > 0) {
67
+ await store.update(actor, one.entity, key, one.row);
68
+ continue;
69
+ }
70
+ }
62
71
  await store.insert(actor, one.entity, one.row);
72
+ }
63
73
  },
64
74
  async taken(actor) {
65
75
  const found = {};
@@ -83,7 +93,7 @@ export function storing(held) {
83
93
  const found = held.address[entity];
84
94
  if (found === undefined) {
85
95
  throw new Error(entity + ' declares nothing that identifies one of its rows, so a row of it cannot be addressed here. ' +
86
- 'Give it a value and say so with `thread create entity edit`.');
96
+ 'Name the value that does: give `thread edit ' + entity + ' --must` its invariants again with one more, unique over that value alone.');
87
97
  }
88
98
  return found;
89
99
  };
@@ -68,6 +68,7 @@ export interface Order {
68
68
  readonly said?: string;
69
69
  readonly mode?: number;
70
70
  readonly words?: readonly string[];
71
+ readonly ahead?: boolean;
71
72
  }
72
73
  export interface Answer {
73
74
  readonly answered: string;
@@ -87,6 +88,7 @@ export interface How {
87
88
  readonly answered?: readonly Answer[];
88
89
  readonly recording?: readonly Recorded[];
89
90
  readonly answering?: number;
91
+ readonly stash?: unknown;
90
92
  }
91
93
  export interface Ran {
92
94
  readonly there: boolean;
@@ -99,6 +101,7 @@ export interface Ran {
99
101
  readonly of?: string;
100
102
  readonly asks?: readonly Order[];
101
103
  readonly lost?: string;
104
+ readonly stashed?: unknown;
102
105
  }
103
106
  export type Running<C extends string = string> = Readonly<Record<C, (args: readonly string[], shows?: boolean | 'errors' | 'tee', within?: number, how?: How) => Promise<Ran>>>;
104
107
  export type Calling<C extends string = string> = {
@@ -116,11 +116,7 @@ export const ATTACH = {
116
116
  { file: 'rules.ts', source: RULES_SOURCE },
117
117
  { file: 'gate.ts', source: GATE_SOURCE },
118
118
  ],
119
- ignores: ['.thread/', '.next/', 'node_modules/', 'tsconfig.tsbuildinfo'],
120
- scripts: {
121
- build: 'next build',
122
- start: 'next start',
123
- },
119
+ ignores: ['.thread/', 'node_modules/', 'tsconfig.tsbuildinfo'],
124
120
  needs: {
125
121
  '@types/node': '^24.0.0',
126
122
  typescript: '^6.0.0',