@dokebi/cli 0.4.2 → 0.4.27

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 (200) hide show
  1. package/AI.md +279 -57
  2. package/README.md +35 -10
  3. package/catalog.json +2520 -258
  4. package/describe.schema.json +748 -0
  5. package/dist/api-app.d.ts +2 -0
  6. package/dist/api-app.js +35 -0
  7. package/dist/app-urls.js +3 -2
  8. package/dist/bin.js +638 -37
  9. package/dist/capability-guide.d.ts +30 -0
  10. package/dist/capability-guide.js +122 -0
  11. package/dist/catalog.d.ts +16 -2
  12. package/dist/catalog.js +19 -7
  13. package/dist/check/asset-rules.d.ts +6 -0
  14. package/dist/check/asset-rules.js +163 -0
  15. package/dist/check/browser-sources.d.ts +20 -0
  16. package/dist/check/browser-sources.js +89 -0
  17. package/dist/check/console-rules.d.ts +6 -0
  18. package/dist/check/console-rules.js +40 -0
  19. package/dist/check/error-rules.d.ts +21 -0
  20. package/dist/check/error-rules.js +212 -0
  21. package/dist/check/form-rules.d.ts +12 -0
  22. package/dist/check/form-rules.js +69 -6
  23. package/dist/check/formatter-rules.d.ts +2 -0
  24. package/dist/check/formatter-rules.js +71 -0
  25. package/dist/check/i18n-rules.d.ts +27 -0
  26. package/dist/check/i18n-rules.js +353 -0
  27. package/dist/check/index.d.ts +15 -1
  28. package/dist/check/index.js +89 -11
  29. package/dist/check/mail-rules.d.ts +4 -0
  30. package/dist/check/mail-rules.js +72 -0
  31. package/dist/check/module-rules.d.ts +13 -0
  32. package/dist/check/module-rules.js +104 -0
  33. package/dist/check/packages.d.ts +8 -0
  34. package/dist/check/packages.js +16 -0
  35. package/dist/check/pool-rules.d.ts +8 -0
  36. package/dist/check/pool-rules.js +164 -0
  37. package/dist/check/problems.d.ts +6 -1
  38. package/dist/check/problems.js +100 -2
  39. package/dist/check/project-rules.d.ts +23 -3
  40. package/dist/check/project-rules.js +265 -23
  41. package/dist/check/seal-rules.d.ts +6 -0
  42. package/dist/check/seal-rules.js +68 -0
  43. package/dist/check/security-rules.d.ts +10 -0
  44. package/dist/check/security-rules.js +450 -0
  45. package/dist/check/shared-state-rules.d.ts +15 -0
  46. package/dist/check/shared-state-rules.js +117 -0
  47. package/dist/check/source-rules.d.ts +26 -3
  48. package/dist/check/source-rules.js +249 -28
  49. package/dist/check/structure-rules.d.ts +23 -0
  50. package/dist/check/structure-rules.js +502 -0
  51. package/dist/check/timer-rules.d.ts +7 -0
  52. package/dist/check/timer-rules.js +543 -0
  53. package/dist/check/typescript.d.ts +35 -5
  54. package/dist/check/typescript.js +163 -17
  55. package/dist/check/vite-rules.d.ts +2 -0
  56. package/dist/check/vite-rules.js +136 -0
  57. package/dist/check/wait-rules.d.ts +16 -0
  58. package/dist/check/wait-rules.js +337 -0
  59. package/dist/commands.d.ts +7 -0
  60. package/dist/commands.js +381 -36
  61. package/dist/config-diff.d.ts +42 -0
  62. package/dist/config-diff.js +289 -0
  63. package/dist/config-edit.d.ts +35 -2
  64. package/dist/config-edit.js +145 -13
  65. package/dist/config-init.d.ts +15 -0
  66. package/dist/config-init.js +61 -0
  67. package/dist/config-paths.d.ts +22 -0
  68. package/dist/config-paths.js +82 -0
  69. package/dist/config-secrets.d.ts +46 -0
  70. package/dist/config-secrets.js +187 -0
  71. package/dist/config-show.d.ts +82 -0
  72. package/dist/config-show.js +563 -0
  73. package/dist/config-values.d.ts +8 -0
  74. package/dist/config-values.js +68 -0
  75. package/dist/configuration-scaffold.d.ts +42 -0
  76. package/dist/configuration-scaffold.js +258 -4
  77. package/dist/configuration.d.ts +63 -3
  78. package/dist/configuration.js +178 -4
  79. package/dist/deploy-scripts.d.ts +4 -0
  80. package/dist/deploy-scripts.js +361 -0
  81. package/dist/deploy-templates.d.ts +46 -0
  82. package/dist/deploy-templates.js +379 -0
  83. package/dist/deploy.d.ts +46 -0
  84. package/dist/deploy.js +592 -0
  85. package/dist/deployment.d.ts +2 -0
  86. package/dist/deployment.js +111 -0
  87. package/dist/describe-db.d.ts +22 -0
  88. package/dist/describe-db.js +176 -0
  89. package/dist/describe.d.ts +92 -0
  90. package/dist/describe.js +417 -0
  91. package/dist/dev-compose.d.ts +20 -0
  92. package/dist/dev-compose.js +154 -0
  93. package/dist/dev-guard.js +44 -0
  94. package/dist/dev-host.d.ts +13 -0
  95. package/dist/dev-host.js +55 -0
  96. package/dist/dev-infra.d.ts +72 -4
  97. package/dist/dev-infra.js +237 -17
  98. package/dist/dev-watch.d.ts +34 -0
  99. package/dist/dev-watch.js +192 -0
  100. package/dist/diagnostics.d.ts +10 -7
  101. package/dist/diagnostics.js +22 -4
  102. package/dist/drift.d.ts +50 -0
  103. package/dist/drift.js +218 -0
  104. package/dist/env-file.d.ts +25 -1
  105. package/dist/env-file.js +64 -6
  106. package/dist/errors.d.ts +7 -1
  107. package/dist/example-config.d.ts +2 -0
  108. package/dist/example-config.js +3 -0
  109. package/dist/first-admin.d.ts +43 -0
  110. package/dist/first-admin.js +175 -0
  111. package/dist/formatter-ignore.d.ts +37 -0
  112. package/dist/formatter-ignore.js +344 -0
  113. package/dist/generator.d.ts +45 -3
  114. package/dist/generator.js +205 -15
  115. package/dist/guide.d.ts +1 -1
  116. package/dist/guide.js +42 -21
  117. package/dist/install-module.d.ts +20 -2
  118. package/dist/install-module.js +45 -8
  119. package/dist/install-plan.js +5 -3
  120. package/dist/layout.d.ts +12 -1
  121. package/dist/layout.js +19 -6
  122. package/dist/log-writer-control.d.ts +2 -0
  123. package/dist/log-writer-control.js +17 -2
  124. package/dist/log-writer.js +43 -10
  125. package/dist/logging-config.d.ts +8 -3
  126. package/dist/logging-config.js +19 -5
  127. package/dist/manifest.d.ts +29 -1
  128. package/dist/manifest.js +84 -6
  129. package/dist/merge-install-defaults.d.ts +0 -1
  130. package/dist/merge-install-defaults.js +13 -47
  131. package/dist/npm-client.d.ts +40 -4
  132. package/dist/npm-client.js +152 -15
  133. package/dist/npm-paths.js +18 -4
  134. package/dist/openapi.d.ts +27 -0
  135. package/dist/openapi.js +649 -0
  136. package/dist/option-fixes.d.ts +39 -0
  137. package/dist/option-fixes.js +130 -0
  138. package/dist/output.d.ts +1 -1
  139. package/dist/output.js +5 -5
  140. package/dist/plugin-lifecycle.d.ts +34 -2
  141. package/dist/plugin-lifecycle.js +195 -16
  142. package/dist/plugin-options.d.ts +19 -4
  143. package/dist/plugin-options.js +138 -6
  144. package/dist/plugins-command.js +44 -7
  145. package/dist/process-group.d.ts +39 -0
  146. package/dist/process-group.js +132 -0
  147. package/dist/production-doctor.d.ts +3 -0
  148. package/dist/production-doctor.js +176 -0
  149. package/dist/project-guide.d.ts +19 -1
  150. package/dist/project-guide.js +119 -20
  151. package/dist/project-inspector.d.ts +15 -0
  152. package/dist/project-inspector.js +497 -33
  153. package/dist/project-state.d.ts +5 -0
  154. package/dist/project-state.js +8 -0
  155. package/dist/project.d.ts +203 -19
  156. package/dist/project.js +3006 -333
  157. package/dist/recipe-facts.d.ts +127 -0
  158. package/dist/recipe-facts.js +563 -0
  159. package/dist/recipe-login.d.ts +63 -0
  160. package/dist/recipe-login.js +464 -0
  161. package/dist/recipe-mail.d.ts +21 -0
  162. package/dist/recipe-mail.js +214 -0
  163. package/dist/recipe-members.d.ts +35 -0
  164. package/dist/recipe-members.js +178 -0
  165. package/dist/recipe-record.d.ts +82 -7
  166. package/dist/recipe-record.js +226 -22
  167. package/dist/recipe-reset.d.ts +2 -0
  168. package/dist/recipe-reset.js +117 -0
  169. package/dist/recipe-site.d.ts +48 -0
  170. package/dist/recipe-site.js +396 -0
  171. package/dist/recipe-step.d.ts +42 -0
  172. package/dist/recipe-step.js +89 -0
  173. package/dist/recipe.d.ts +56 -20
  174. package/dist/recipe.js +703 -131
  175. package/dist/registry.d.ts +98 -2
  176. package/dist/registry.js +171 -5
  177. package/dist/reloadable.d.ts +35 -0
  178. package/dist/reloadable.js +257 -0
  179. package/dist/remote-verify.d.ts +31 -0
  180. package/dist/remote-verify.js +204 -0
  181. package/dist/server.d.ts +43 -4
  182. package/dist/server.js +601 -52
  183. package/dist/settings.d.ts +48 -0
  184. package/dist/settings.js +168 -0
  185. package/dist/site-auth.d.ts +39 -0
  186. package/dist/site-auth.js +195 -0
  187. package/dist/template-fixes.d.ts +26 -0
  188. package/dist/template-fixes.js +285 -0
  189. package/dist/test-reporter.d.ts +49 -0
  190. package/dist/test-reporter.js +190 -0
  191. package/dist/transaction.d.ts +47 -0
  192. package/dist/transaction.js +241 -7
  193. package/dist/verify-tests.d.ts +98 -0
  194. package/dist/verify-tests.js +449 -0
  195. package/dist/verify.d.ts +103 -3
  196. package/dist/verify.js +272 -21
  197. package/dist/view-choice.d.ts +58 -0
  198. package/dist/view-choice.js +131 -0
  199. package/dist/vite-watch.js +4 -2
  200. package/package.json +8 -6
@@ -4,6 +4,7 @@ import { hash as hashing } from '@dokebi/core/libraries';
4
4
  import { debug, debugEnabled, describeError, reportError } from './diagnostics.js';
5
5
  import { CliError } from './errors.js';
6
6
  import { exists, fingerprint, hash, json, securePath } from './filesystem.js';
7
+ import { lockDiagnostic, lockHolder } from './project-inspector.js';
7
8
  /**
8
9
  * What is at `path` now, without following a symlink: null when nothing is there, `file:<sha256>`
9
10
  * for a file, `directory:<dev>:<ino>` for a folder (its identity: a folder's content is not
@@ -34,11 +35,156 @@ function differs(installed, now) {
34
35
  return 'symlink';
35
36
  return installed?.startsWith('file:') && now.startsWith('file:') ? 'modified' : 'replaced';
36
37
  }
38
+ /**
39
+ * DUPLICATE_CHANGE (D-0116): two parts of one change wrote the same path (two plugins with the
40
+ * same target, a file where a folder goes). A defect of the packages or the CLI, never of the user.
41
+ */
42
+ function duplicateChange(path) {
43
+ return new CliError('DUPLICATE_CHANGE', `${path} would be written twice by this change; nothing was written`, {
44
+ details: {
45
+ path,
46
+ fix: 'two packages of this change claim the same path: install them one at a time to see which, and report it to their authors',
47
+ },
48
+ phase: 'plan',
49
+ });
50
+ }
51
+ /**
52
+ * INTERRUPTED: a termination signal (Ctrl+C, SIGTERM, SIGHUP) came before the commit, so the
53
+ * change was not applied; the lock and the staging folder are removed as after any refusal.
54
+ */
55
+ export function interruptedError(signal, cause) {
56
+ return new CliError('INTERRUPTED', `${signal} received: the change was cancelled before it was applied`, {
57
+ ...(cause === undefined ? {} : { cause }),
58
+ details: {
59
+ signal,
60
+ fix: 'nothing was written; run the command again when you want the change',
61
+ },
62
+ phase: 'plan',
63
+ });
64
+ }
65
+ const TERMINATION = ['SIGINT', 'SIGTERM', 'SIGHUP'];
66
+ /** The open changes of this process; while there is one, a signal cancels instead of killing. */
67
+ const active = new Set();
68
+ const cancellers = new Map();
69
+ function releaseSignals() {
70
+ for (const [signal, handler] of cancellers)
71
+ process.off(signal, handler);
72
+ cancellers.clear();
73
+ }
74
+ function unwatchSignals(item) {
75
+ active.delete(item);
76
+ if (!active.size)
77
+ releaseSignals();
78
+ }
79
+ /**
80
+ * The first termination signal while a change is open cancels it: a child npm gets the signal
81
+ * from its own forwarder (process-group.ts) and fails, a commit not yet begun throws INTERRUPTED,
82
+ * and the command ends through its usual cleanup, which removes .dokebi/lock and the staging
83
+ * folder. Killed by the default action instead, the process left both behind, and every later
84
+ * command was PROJECT_LOCKED until a person looked, although nothing had been applied. A commit
85
+ * that has begun runs to its end (or its rollback). A second signal ends the process at once.
86
+ */
87
+ function watchSignals(change) {
88
+ active.add(change);
89
+ if (cancellers.size)
90
+ return;
91
+ for (const signal of TERMINATION) {
92
+ const handler = () => {
93
+ const again = [...active].some((item) => item.interrupted !== undefined);
94
+ if (again || !active.size) {
95
+ debug('Second termination signal: ending at once', { signal });
96
+ releaseSignals();
97
+ if (process.listenerCount(signal) === 0)
98
+ process.kill(process.pid, signal);
99
+ return;
100
+ }
101
+ // dokebi-check: allow console-output the user's terminal: what the first Ctrl+C does
102
+ process.stderr.write(`dokebi: ${signal} received, cancelling the change (nothing is applied); send it again to stop at once\n`);
103
+ for (const item of active)
104
+ item.cancel(signal);
105
+ };
106
+ cancellers.set(signal, handler);
107
+ process.on(signal, handler);
108
+ }
109
+ }
110
+ /**
111
+ * A change without a transaction that a termination signal cancels like one (R4CB-3: `dokebi
112
+ * init`, whose staging folder a killed CLI left in the target folder, so the next init was
113
+ * PROJECT_EXISTS). Open it before the work, `check()` before the step that makes the change
114
+ * visible, `failure(error)` for what the work threw, and `close()` in a finally.
115
+ */
116
+ export class Interruption {
117
+ #signal;
118
+ #closed = false;
119
+ constructor() { }
120
+ static open() {
121
+ const interruption = new Interruption();
122
+ watchSignals(interruption);
123
+ return interruption;
124
+ }
125
+ get interrupted() {
126
+ return this.#signal;
127
+ }
128
+ cancel(signal) {
129
+ this.#signal ??= signal;
130
+ debug('Change cancelled by a termination signal', { signal });
131
+ }
132
+ /** Throws INTERRUPTED when a signal came. */
133
+ check() {
134
+ if (this.#signal)
135
+ throw interruptedError(this.#signal);
136
+ }
137
+ /** INTERRUPTED (with `error` as its cause) when a signal came, else `error` itself. */
138
+ failure(error) {
139
+ if (!this.#signal || (error instanceof CliError && error.code === 'INTERRUPTED'))
140
+ return error;
141
+ return interruptedError(this.#signal, error);
142
+ }
143
+ close() {
144
+ if (this.#closed)
145
+ return;
146
+ this.#closed = true;
147
+ unwatchSignals(this);
148
+ }
149
+ }
37
150
  export class Transaction {
38
151
  root;
39
152
  directory;
40
153
  #snapshots = new Map();
154
+ #interrupted;
155
+ #committing = false;
156
+ /** The termination signal that cancelled this change, if one came. */
157
+ get interrupted() {
158
+ return this.#interrupted;
159
+ }
160
+ /** Marks the change cancelled (watchSignals); a commit that has begun is not affected. */
161
+ cancel(signal) {
162
+ this.#interrupted ??= signal;
163
+ debug('Change cancelled by a termination signal', { signal, committing: this.#committing });
164
+ }
165
+ /**
166
+ * What a command that owns this transaction throws for `error` (R4CB-4): INTERRUPTED when a
167
+ * signal cancelled the change before its commit began, whatever part of it then failed (an npm
168
+ * run other than the staging install, an install module the terminal's Ctrl+C reached too), so
169
+ * the result says nothing was applied (phase plan, mutation none). From the commit on, the
170
+ * error stays as it is (COMMIT_FAILED, RECOVERY_REQUIRED, ...).
171
+ */
172
+ failure(error) {
173
+ if (!this.#interrupted || this.#committing)
174
+ return error;
175
+ if (error instanceof CliError && error.code === 'INTERRUPTED')
176
+ return error;
177
+ return interruptedError(this.#interrupted, error);
178
+ }
41
179
  #changes = [];
180
+ /**
181
+ * Paths written with the content they already have: nothing is written there (S-a), but they
182
+ * stay claimed (a second write is DUPLICATE_CHANGE) and watched (an edit before the commit is
183
+ * CONCURRENT_CHANGE), as a real change is (R7).
184
+ */
185
+ #unchanged = new Set();
186
+ /** Staging files are numbered when a write starts, so writes in flight never share one. */
187
+ #staged = 0;
42
188
  #keep = false;
43
189
  constructor(root, directory) {
44
190
  this.root = root;
@@ -53,7 +199,25 @@ export class Transaction {
53
199
  handle = await open(lock, 'wx');
54
200
  }
55
201
  catch (cause) {
56
- throw new CliError('PROJECT_LOCKED', 'Another operation or interrupted transaction owns .dokebi/lock', { cause });
202
+ if (cause.code !== 'EEXIST')
203
+ throw new CliError('LOCK_FAILED', 'Cannot create .dokebi/lock', {
204
+ cause,
205
+ details: {
206
+ error: cause.code ?? describeError(cause),
207
+ fix: 'the project folder (.dokebi) must be writable by this user',
208
+ },
209
+ phase: 'unknown',
210
+ });
211
+ // Who holds it and what to do, as doctor says it (AI-M04 F6): the error had no details.
212
+ const holder = await lockHolder(root);
213
+ const { hint, details } = lockDiagnostic(holder ?? { pid: null, running: null, since: null }, []);
214
+ throw new CliError('PROJECT_LOCKED', 'Another operation or interrupted transaction owns .dokebi/lock', {
215
+ cause,
216
+ details: {
217
+ ...details,
218
+ fix: hint,
219
+ },
220
+ });
57
221
  }
58
222
  try {
59
223
  await handle.writeFile(json({ pid: process.pid }));
@@ -61,7 +225,9 @@ export class Transaction {
61
225
  if ((await readdir(control)).some((name) => name.startsWith('transaction-'))) {
62
226
  throw new CliError('RECOVERY_REQUIRED', 'Interrupted transaction found; preserve .dokebi/transaction-* for recovery', { phase: 'unknown' });
63
227
  }
64
- return new Transaction(root, await mkdtemp(join(control, 'transaction-')));
228
+ const transaction = new Transaction(root, await mkdtemp(join(control, 'transaction-')));
229
+ watchSignals(transaction);
230
+ return transaction;
65
231
  }
66
232
  catch (cause) {
67
233
  await handle.close().catch((error) => {
@@ -83,7 +249,7 @@ export class Transaction {
83
249
  const planned = this.#changes.find((change) => change.path === current);
84
250
  if (planned) {
85
251
  if (planned.kind !== 'directory')
86
- throw new CliError('DUPLICATE_CHANGE', current);
252
+ throw duplicateChange(current);
87
253
  continue;
88
254
  }
89
255
  await this.capture(current);
@@ -91,15 +257,55 @@ export class Transaction {
91
257
  if (expected === null)
92
258
  this.#changes.push({ path: current, kind: 'directory' });
93
259
  else if (!expected?.startsWith('directory:'))
94
- throw new CliError('PATH_CONFLICT', current);
260
+ throw new CliError('PATH_CONFLICT', `${current} is not a folder, but the change needs a folder there for ${path}`, {
261
+ details: {
262
+ path: current,
263
+ needed: path,
264
+ fix: `move ${current} away (a file or link is in the way), then repeat this command`,
265
+ },
266
+ phase: 'plan',
267
+ });
268
+ }
269
+ }
270
+ /** The current text of `path` in the project, or undefined when there is none. */
271
+ async #text(path) {
272
+ try {
273
+ return await readFile(await securePath(this.root, path), 'utf8');
274
+ }
275
+ catch (error) {
276
+ if (error.code === 'ENOENT')
277
+ return undefined;
278
+ throw error;
95
279
  }
96
280
  }
97
281
  async read(path) {
98
282
  await this.capture(path);
99
283
  return readFile(await securePath(this.root, path), 'utf8');
100
284
  }
285
+ /**
286
+ * Rewrites text before it is staged (D-0154: the reloadable marks of dokebi.yaml and
287
+ * dokebi.example.yaml). Set by the operation that owns the transaction.
288
+ */
289
+ transformText;
101
290
  async write(path, content) {
102
- const source = join(this.directory, `new-${this.#changes.length}`);
291
+ if (typeof content === 'string' && this.transformText) {
292
+ const transformed = await this.transformText(path, content);
293
+ // An edit that only dropped marks the file already has (a section rewritten without its
294
+ // schema at hand) is no change: the file stays as it is and is not reported as changed.
295
+ content = transformed;
296
+ }
297
+ // S-a: the content the file already has is no change: never rewritten (a rewrite with the
298
+ // same text gave src/dokebi.generated.ts a new time, and dokebi dev restarted the app).
299
+ if (typeof content === 'string' &&
300
+ !this.#claimed(path) &&
301
+ (await this.#text(path)) === content) {
302
+ await this.capture(path);
303
+ if (this.#claimed(path))
304
+ throw duplicateChange(path);
305
+ this.#unchanged.add(path);
306
+ return;
307
+ }
308
+ const source = join(this.directory, `new-${this.#staged++}`);
103
309
  await writeFile(source, content, { mode: 0o600 });
104
310
  await this.#add(path, source, `file:${hash(content)}`);
105
311
  }
@@ -107,10 +313,17 @@ export class Transaction {
107
313
  async replace(path, source) {
108
314
  await this.#add(path, source);
109
315
  }
316
+ /** A change of this transaction already claims `path` (a write, a removal, an unchanged write). */
317
+ #claimed(path) {
318
+ return this.#unchanged.has(path) || this.#changes.some((change) => change.path === path);
319
+ }
110
320
  async #add(path, source, installs) {
111
- if (this.#changes.some((change) => change.path === path))
112
- throw new CliError('DUPLICATE_CHANGE', path);
321
+ if (this.#claimed(path))
322
+ throw duplicateChange(path);
113
323
  await this.capture(path);
324
+ // Again after the await: a change of the same path may have been added meanwhile.
325
+ if (this.#claimed(path))
326
+ throw duplicateChange(path);
114
327
  this.#changes.push(source === undefined ? { path } : installs ? { path, source, installs } : { path, source });
115
328
  }
116
329
  get paths() {
@@ -126,16 +339,36 @@ export class Transaction {
126
339
  * in the transaction folder, and the commit ends with RECOVERY_REQUIRED naming those paths.
127
340
  */
128
341
  async commit(beforeApply, afterApply) {
342
+ if (this.#interrupted)
343
+ throw interruptedError(this.#interrupted);
344
+ this.#committing = true;
129
345
  for (const [path, expected] of this.#snapshots) {
130
346
  if ((await fingerprint(await securePath(this.root, path))) !== expected) {
131
347
  throw new CliError('CONCURRENT_CHANGE', `File changed while planning: ${path}`);
132
348
  }
133
349
  }
350
+ // A staged folder is recognised by its dev:ino, and a file system hands a freed inode number
351
+ // out again at once (Linux: nearly every time): a node_modules the user removes and creates
352
+ // while afterApply runs would look like ours, and the rollback would remove the user's folder.
353
+ // A handle open on each staged folder until the commit ends keeps its inode in use, so no new
354
+ // folder can get the same number (not on Windows, where a folder cannot be opened).
355
+ const held = [];
356
+ try {
357
+ await this.#apply(held, beforeApply, afterApply);
358
+ }
359
+ finally {
360
+ await Promise.all(held.map((handle) => handle.close()));
361
+ }
362
+ }
363
+ /** The journal, the changes in order and `afterApply`; a failure rolls back (commit). */
364
+ async #apply(held, beforeApply, afterApply) {
134
365
  const journal = [];
135
366
  for (const [index, change] of this.#changes.entries()) {
136
367
  // What this change installs, to recognise it again in a rollback: a written file's hash, a
137
368
  // staged file's hash or a staged folder's identity (node_modules).
138
369
  const installs = change.installs ?? (change.source ? await observe(change.source) : null) ?? undefined;
370
+ if (change.source && installs?.startsWith('directory:') && process.platform !== 'win32')
371
+ held.push(await open(change.source, 'r'));
139
372
  journal.push({
140
373
  ...change,
141
374
  ...(installs ? { installs } : {}),
@@ -317,6 +550,7 @@ export class Transaction {
317
550
  return true;
318
551
  }
319
552
  async close() {
553
+ unwatchSignals(this);
320
554
  if (this.#keep)
321
555
  return;
322
556
  if (await exists(this.directory)) {
@@ -0,0 +1,98 @@
1
+ /** Default time limit of the test step (`--test-timeout-ms`), like typecheck and build. */
2
+ export declare const TEST_TIMEOUT_MS = 300000;
3
+ /** At most this many results are listed; failures always are (`omitted` counts the rest). */
4
+ export declare const MAX_TEST_RESULTS = 1000;
5
+ /** A leading `[ABC-12] ` tag of a test name: the id (D-0077 A; the framework gives it no meaning). */
6
+ export declare const TEST_ID: RegExp;
7
+ /** One test of the project (a leaf test, or a suite or parent that failed by itself). */
8
+ export interface TestCaseResult {
9
+ /** The test's name after the names of its suites and parent tests (` > `). */
10
+ readonly name: string;
11
+ /** The `[ID]` tag at the start of its name (else of the nearest suite or parent with one). */
12
+ readonly id?: string;
13
+ readonly ok: boolean;
14
+ readonly ms: number;
15
+ /** The test file relative to the project (`/` separators). */
16
+ readonly file?: string;
17
+ readonly skipped?: true;
18
+ readonly todo?: true;
19
+ /** First line of the failure message, secrets masked. */
20
+ readonly error?: string;
21
+ }
22
+ /** `tests` of the verify result. */
23
+ export type TestReport = {
24
+ readonly runner: 'node';
25
+ readonly total: number;
26
+ readonly passed: number;
27
+ readonly failed: number;
28
+ readonly skipped: number;
29
+ readonly todo: number;
30
+ readonly ms: number;
31
+ readonly exitCode: number | null;
32
+ readonly results: TestCaseResult[];
33
+ /** Passed or skipped tests left out of `results` beyond MAX_TEST_RESULTS. */
34
+ readonly omitted?: number;
35
+ } | {
36
+ readonly runner: 'other';
37
+ readonly ms: number;
38
+ readonly exitCode: number | null;
39
+ /** Why the tests were not run with the per-test reporter. */
40
+ readonly reason: string;
41
+ };
42
+ /** How the test script is run. */
43
+ export type TestPlan = {
44
+ readonly runner: 'node';
45
+ /** The node arguments of the script (reporter flags removed). */
46
+ readonly args: string[];
47
+ /** `KEY=value` words before `node`. */
48
+ readonly env: Record<string, string>;
49
+ } | {
50
+ readonly runner: 'other';
51
+ readonly reason: string;
52
+ };
53
+ /**
54
+ * The words of a script, or undefined when it uses anything a shell would interpret (operators,
55
+ * substitutions, escapes, globs outside quotes are kept as words: node expands them itself).
56
+ */
57
+ export declare function scriptWords(script: string): string[] | undefined;
58
+ /** Whether verify can run `script` with its reporter, and how. */
59
+ export declare function planTests(script: string, scripts: Readonly<Record<string, unknown>>): TestPlan;
60
+ /** The id of a test: the tag of its own name, else of the nearest parent with one. */
61
+ export declare function testId(name: string, parents?: readonly string[]): string | undefined;
62
+ /**
63
+ * The credentials core's `maskCredentials` hides in a text (`//user:pa/ss@` of a URL, also with a
64
+ * raw `/` in the password; `Password=...` of a connection string): the text itself and the hidden
65
+ * part, read off its masked form, so verify hides exactly what the logs hide (no rule of its own).
66
+ */
67
+ export declare function maskedCredentials(text: string): string[];
68
+ /**
69
+ * Every secret value verify can know for this project: the values of .env, of environment
70
+ * variables named like a secret (core's `redaction.isSecretEnvironmentName`: the field rule of the
71
+ * logs plus `*_KEY`, `*_PASS`; never PWD or OLDPWD, which would cut the project folder out of
72
+ * every error line), and of the options of the settings file (DOKEBI_CONFIG, else dokebi.yaml) `config show` hides. Throws when they cannot be
73
+ * read (the caller then drops the error lines).
74
+ */
75
+ export declare function knownSecrets(root: string): Promise<string[]>;
76
+ /** An error line with the known secret values and secret-looking parts replaced by `***`. */
77
+ export declare function maskLine(line: string, secrets: readonly string[]): string;
78
+ /** What the test step reports: the check fields and the `tests` of the verify result. */
79
+ export interface TestStep {
80
+ readonly ok: boolean;
81
+ readonly skipped?: string;
82
+ readonly code?: string;
83
+ readonly message?: string;
84
+ readonly details?: unknown;
85
+ readonly tests?: TestReport;
86
+ }
87
+ /**
88
+ * The test step of verify for the project at `root` whose package.json scripts are `scripts`.
89
+ * Throws a CliError (ENV_FILE_INVALID, TEST_FAILED) only when the tests cannot be run at all.
90
+ */
91
+ export declare function runTests(root: string, scripts: Readonly<Record<string, unknown>>, options?: {
92
+ readonly timeoutMs?: number;
93
+ }): Promise<TestStep>;
94
+ /**
95
+ * The human lines verify prints under its test check: one per failing test, then a summary
96
+ * (each line starts with a newline and two spaces).
97
+ */
98
+ export declare function testLines(report: TestReport): string;