@aventara/client 0.0.0-stage → 0.1.0-pilot.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +91 -0
  2. package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
  3. package/README.md +234 -2
  4. package/dist/avclient.bin.d.ts +2 -0
  5. package/dist/avclient.bin.js +15 -0
  6. package/dist/cli/command.parser.d.ts +37 -0
  7. package/dist/cli/command.parser.js +177 -0
  8. package/dist/cli/generate.command.d.ts +24 -0
  9. package/dist/cli/generate.command.js +41 -0
  10. package/dist/cli/generation-failure.renderer.d.ts +6 -0
  11. package/dist/cli/generation-failure.renderer.js +52 -0
  12. package/dist/cli/generation-success.renderer.d.ts +32 -0
  13. package/dist/cli/generation-success.renderer.js +47 -0
  14. package/dist/cli/terminal.prompter.d.ts +13 -0
  15. package/dist/cli/terminal.prompter.js +53 -0
  16. package/dist/cli/warning.renderer.d.ts +10 -0
  17. package/dist/cli/warning.renderer.js +14 -0
  18. package/dist/cli.d.ts +29 -0
  19. package/dist/cli.js +75 -0
  20. package/dist/config/client-config.interface.d.ts +62 -0
  21. package/dist/config/client-config.interface.js +14 -0
  22. package/dist/config/config.loader.d.ts +41 -0
  23. package/dist/config/config.loader.js +95 -0
  24. package/dist/config/config.resolver.d.ts +50 -0
  25. package/dist/config/config.resolver.js +126 -0
  26. package/dist/config/env.cascade.d.ts +84 -0
  27. package/dist/config/env.cascade.js +126 -0
  28. package/dist/contract/contract.acceptance.d.ts +77 -0
  29. package/dist/contract/contract.acceptance.js +124 -0
  30. package/dist/contract/contract.fetcher.d.ts +64 -0
  31. package/dist/contract/contract.fetcher.js +85 -0
  32. package/dist/contract/contract.loader.d.ts +32 -0
  33. package/dist/contract/contract.loader.js +32 -0
  34. package/dist/emit/banner.emitter.d.ts +31 -0
  35. package/dist/emit/banner.emitter.js +42 -0
  36. package/dist/emit/client-surface.emitter.d.ts +32 -0
  37. package/dist/emit/client-surface.emitter.js +236 -0
  38. package/dist/emit/client-tree.emitter.d.ts +37 -0
  39. package/dist/emit/client-tree.emitter.js +103 -0
  40. package/dist/emit/contract-carrier.emitter.d.ts +13 -0
  41. package/dist/emit/contract-carrier.emitter.js +60 -0
  42. package/dist/emit/derivation.emitter.d.ts +45 -0
  43. package/dist/emit/derivation.emitter.js +233 -0
  44. package/dist/emit/descriptor.emitter.d.ts +4 -0
  45. package/dist/emit/descriptor.emitter.js +97 -0
  46. package/dist/emit/emitted-tree.interface.d.ts +61 -0
  47. package/dist/emit/emitted-tree.interface.js +18 -0
  48. package/dist/emit/enum.emitter.d.ts +24 -0
  49. package/dist/emit/enum.emitter.js +42 -0
  50. package/dist/emit/name.deriver.d.ts +153 -0
  51. package/dist/emit/name.deriver.js +411 -0
  52. package/dist/emit/named-type.emitter.d.ts +32 -0
  53. package/dist/emit/named-type.emitter.js +50 -0
  54. package/dist/emit/runtime.emitter.d.ts +87 -0
  55. package/dist/emit/runtime.emitter.js +707 -0
  56. package/dist/emit/scalar.codec.d.ts +63 -0
  57. package/dist/emit/scalar.codec.js +498 -0
  58. package/dist/emit/transaction.emitter.d.ts +17 -0
  59. package/dist/emit/transaction.emitter.js +438 -0
  60. package/dist/generate.d.ts +123 -0
  61. package/dist/generate.js +98 -0
  62. package/dist/index.d.ts +8 -0
  63. package/dist/index.js +8 -0
  64. package/dist/init/client-config.template.d.ts +11 -0
  65. package/dist/init/client-config.template.js +27 -0
  66. package/dist/init/client-init.errors.d.ts +9 -0
  67. package/dist/init/client-init.errors.js +9 -0
  68. package/dist/init/client-init.orchestrator.d.ts +3 -0
  69. package/dist/init/client-init.orchestrator.js +86 -0
  70. package/dist/init/client-init.planner.d.ts +27 -0
  71. package/dist/init/client-init.planner.js +99 -0
  72. package/dist/init/client-init.questions.d.ts +52 -0
  73. package/dist/init/client-init.questions.js +124 -0
  74. package/dist/init/client-project.inspector.d.ts +15 -0
  75. package/dist/init/client-project.inspector.js +32 -0
  76. package/dist/init/command.runner.d.ts +8 -0
  77. package/dist/init/command.runner.js +17 -0
  78. package/dist/node-version.guard.d.ts +8 -0
  79. package/dist/node-version.guard.js +59 -0
  80. package/dist/output/output.validator.d.ts +75 -0
  81. package/dist/output/output.validator.js +262 -0
  82. package/dist/output/output.writer.d.ts +162 -0
  83. package/dist/output/output.writer.js +499 -0
  84. package/package.json +47 -3
@@ -0,0 +1,499 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import * as fsPromises from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { carriesGeneratedOwnership, GENERATED_OWNERSHIP_HEAD_BYTES, } from "../emit/banner.emitter.js";
5
+ import { CLIENT_ENTRY_FILE, GENERATED_DIRECTORY, } from "../emit/emitted-tree.interface.js";
6
+ import { resolveInstalledTypeScript, validateOutputTree, } from "./output.validator.js";
7
+ /** A write that stopped; the previous output is intact unless it says otherwise. */
8
+ export class OutputWriteError extends Error {
9
+ stage;
10
+ name = "OutputWriteError";
11
+ /**
12
+ * What the run said before it stopped, in the order a success returns its
13
+ * warnings: the emission's, the validator's, the writer's. Printed before the
14
+ * refusal's sentence.
15
+ */
16
+ warnings;
17
+ constructor(stage, message, options) {
18
+ super(message, options);
19
+ this.stage = stage;
20
+ this.warnings = options?.warnings ?? [];
21
+ }
22
+ /** The same refusal, with `earlier` said before the warnings it already carries. */
23
+ precededBy(earlier) {
24
+ return new OutputWriteError(this.stage, this.message, {
25
+ cause: this.cause,
26
+ warnings: [...earlier, ...this.warnings],
27
+ });
28
+ }
29
+ }
30
+ /**
31
+ * What a run said before a defect stopped it, kept beside the defect rather than
32
+ * on a wrapper: a defect propagates as the very error thrown, with its own stack,
33
+ * so a caller matching it by identity or type still can. Keyed weakly, so it
34
+ * lives exactly as long as the error does. A thrown primitive cannot be a key;
35
+ * what was said before one is not kept.
36
+ */
37
+ const WARNINGS_BEFORE_DEFECT = new WeakMap();
38
+ /**
39
+ * Records `earlier` as said before `defect`, ahead of anything already recorded,
40
+ * and returns `defect` unchanged so the caller rethrows the same error.
41
+ */
42
+ export function precedeDefect(defect, earlier) {
43
+ if (earlier.length > 0 && typeof defect === "object" && defect !== null) {
44
+ WARNINGS_BEFORE_DEFECT.set(defect, [
45
+ ...earlier,
46
+ ...warningsRaisedBeforeDefect(defect),
47
+ ]);
48
+ }
49
+ return defect;
50
+ }
51
+ /**
52
+ * What the run said before `defect` stopped it, in the order a success returns
53
+ * its warnings; empty when nothing was, or for an error no run threw.
54
+ */
55
+ export function warningsRaisedBeforeDefect(defect) {
56
+ return typeof defect === "object" && defect !== null
57
+ ? (WARNINGS_BEFORE_DEFECT.get(defect) ?? [])
58
+ : [];
59
+ }
60
+ const NEXT_PREFIX = ".aventara-next-";
61
+ const READY_PREFIX = ".aventara-ready-";
62
+ const PREVIOUS_GENERATED = `.${GENERATED_DIRECTORY}.aventara-previous`;
63
+ /**
64
+ * Writes `emission.tree` as `<generateAt>/AvClient.ts` and `<generateAt>/generated/`.
65
+ *
66
+ * @throws OutputWriteError when the write stops; the previous output is intact.
67
+ */
68
+ export async function writeClientOutput(input) {
69
+ const fs = input.fileSystem ?? fsPromises;
70
+ const generateAt = path.resolve(input.generateAt);
71
+ const paths = {
72
+ generateAt,
73
+ generated: path.join(generateAt, GENERATED_DIRECTORY),
74
+ entry: path.join(generateAt, CLIENT_ENTRY_FILE),
75
+ previous: path.join(generateAt, PREVIOUS_GENERATED),
76
+ };
77
+ const writerWarnings = [];
78
+ let validationWarnings = [];
79
+ /** The outermost directory this run created on the way to `generateAt`, if any. */
80
+ let created;
81
+ try {
82
+ return await writeStagedOutput();
83
+ }
84
+ catch (error) {
85
+ if (created !== undefined) {
86
+ writerWarnings.push(...(await removeCreatedDirectories(fs, generateAt, created)));
87
+ }
88
+ const said = [
89
+ ...input.emission.warnings,
90
+ ...validationWarnings,
91
+ ...writerWarnings,
92
+ ];
93
+ throw error instanceof OutputWriteError
94
+ ? error.precededBy(said)
95
+ : precedeDefect(error, said);
96
+ }
97
+ async function writeStagedOutput() {
98
+ // prepare — recover a killed run, then decide whether the two entries are ours.
99
+ const state = await stage("prepare", paths, async () => {
100
+ created = await ensureDirectory(fs, generateAt);
101
+ writerWarnings.push(...(await recoverInterruptedRun(fs, paths)));
102
+ const inspection = await inspectOwnedEntries(fs, paths);
103
+ const confirmed = new Set(input.overrideForeign ?? []);
104
+ const unconfirmed = inspection.foreign.filter((entry) => !confirmed.has(entry));
105
+ if (unconfirmed.length > 0) {
106
+ throw foreignContentRefusal(paths, unconfirmed);
107
+ }
108
+ return inspection;
109
+ });
110
+ // temp — the whole tree, AvClient.ts and generated/ together, staged in generateAt.
111
+ const suffix = randomBytes(6).toString("hex");
112
+ let staging = path.join(generateAt, `${NEXT_PREFIX}${suffix}`);
113
+ let stagingExists = false;
114
+ try {
115
+ await stage("temp", paths, async () => {
116
+ // `mkdir`, not `mkdtemp`: generated/ is moved out of this directory and
117
+ // takes the mode any directory the user creates takes (`mkdtemp` makes 0700).
118
+ await fs.mkdir(staging);
119
+ stagingExists = true;
120
+ for (const file of input.emission.tree) {
121
+ const target = path.join(staging, file.path);
122
+ await fs.mkdir(path.dirname(target), { recursive: true });
123
+ await fs.writeFile(target, file.bytes);
124
+ }
125
+ });
126
+ // validate — Q6, over AvClient.ts and generated/ as one program.
127
+ const validation = await stage("validate", paths, () => validateOutputTree(staging, input.emission.tree, input.resolveTypeScript ?? resolveInstalledTypeScript));
128
+ if (validation.accepted) {
129
+ validationWarnings = validation.warnings;
130
+ }
131
+ else {
132
+ throw new OutputWriteError("validate", `the generated output failed its ${validation.checked} check, so ${LEFT_AS_THEY_WERE(paths)} ` +
133
+ "This is a defect in @aventara/client; please report it with these findings:\n" +
134
+ validation.findings.map((finding) => ` ${finding}`).join("\n"));
135
+ }
136
+ // replace — the order and its rollbacks are the module doc's.
137
+ await stage("replace", paths, async () => {
138
+ const ready = path.join(generateAt, `${READY_PREFIX}${suffix}`);
139
+ await fs.rename(staging, ready);
140
+ staging = ready;
141
+ const movedAside = state.generated === "present";
142
+ if (movedAside) {
143
+ await fs.rename(paths.generated, paths.previous);
144
+ }
145
+ try {
146
+ await fs.rename(path.join(staging, GENERATED_DIRECTORY), paths.generated);
147
+ }
148
+ catch (error) {
149
+ if (movedAside) {
150
+ await rollBack(paths, error, () => fs.rename(paths.previous, paths.generated));
151
+ }
152
+ throw error;
153
+ }
154
+ try {
155
+ await fs.rename(path.join(staging, CLIENT_ENTRY_FILE), paths.entry);
156
+ }
157
+ catch (error) {
158
+ await rollBack(paths, error, async () => {
159
+ await fs.rename(paths.generated, path.join(staging, GENERATED_DIRECTORY));
160
+ if (movedAside) {
161
+ await fs.rename(paths.previous, paths.generated);
162
+ }
163
+ });
164
+ throw error;
165
+ }
166
+ });
167
+ if (state.generated === "present") {
168
+ try {
169
+ await fs.rm(paths.previous, { recursive: true, force: true });
170
+ }
171
+ catch (error) {
172
+ writerWarnings.push(`the previous generated/ was replaced, but its copy at ${paths.previous} could not be removed ` +
173
+ `(${describe(error)}); the next run removes it.`);
174
+ }
175
+ }
176
+ return {
177
+ generateAt: generateAt,
178
+ checked: validation.checked,
179
+ warnings: [
180
+ ...input.emission.warnings,
181
+ ...validationWarnings,
182
+ ...writerWarnings,
183
+ ],
184
+ };
185
+ }
186
+ finally {
187
+ if (stagingExists) {
188
+ await fs.rm(staging, { recursive: true, force: true }).catch(() => {
189
+ // The next run removes it by name; the error being thrown matters more.
190
+ });
191
+ }
192
+ }
193
+ }
194
+ }
195
+ /** The sentence's ending when the previous output was not changed. */
196
+ function LEFT_AS_THEY_WERE(paths) {
197
+ return `${CLIENT_ENTRY_FILE} and ${GENERATED_DIRECTORY}/ in ${paths.generateAt} were left exactly as they were.`;
198
+ }
199
+ /**
200
+ * Creates `generateAt` when missing; refuses when something other than a
201
+ * directory is there.
202
+ *
203
+ * @returns the outermost directory it created — `generateAt` or an ancestor —
204
+ * or `undefined` when `generateAt` already existed.
205
+ */
206
+ async function ensureDirectory(fs, generateAt) {
207
+ const stats = await lstatOrUndefined(fs, generateAt);
208
+ if (stats === undefined) {
209
+ return fs.mkdir(generateAt, { recursive: true });
210
+ }
211
+ if (!stats.isDirectory()) {
212
+ throw notADirectory(generateAt);
213
+ }
214
+ return undefined;
215
+ }
216
+ /**
217
+ * Removes, after a failed run, the directories it created: `generateAt` and each
218
+ * parent up to `outermost`, innermost first. `rmdir` removes only an empty
219
+ * directory, so nothing anyone else put there meanwhile is lost; the first one
220
+ * that cannot be removed ends the walk, and is named in the warning returned.
221
+ */
222
+ async function removeCreatedDirectories(fs, generateAt, outermost) {
223
+ for (let directory = generateAt;; directory = path.dirname(directory)) {
224
+ try {
225
+ await fs.rmdir(directory);
226
+ }
227
+ catch (error) {
228
+ return [
229
+ `${directory}, which this run created, could not be removed (${describe(error)}); ` +
230
+ "remove it if you do not want it.",
231
+ ];
232
+ }
233
+ if (directory === outermost || path.dirname(directory) === directory) {
234
+ return [];
235
+ }
236
+ }
237
+ }
238
+ /**
239
+ * Leftovers of a run that was killed, recognised only by the names this writer
240
+ * gives them:
241
+ *
242
+ * - a staging directory that never became ready was not validated: removed;
243
+ * - a ready one whose `generated/` was already moved in but whose `AvClient.ts`
244
+ * was not: its `AvClient.ts` is moved in, completing the new pair;
245
+ * - any other ready one: removed;
246
+ * - a previous `generated/` moved aside: put back when nothing replaced it,
247
+ * removed when something did.
248
+ */
249
+ async function recoverInterruptedRun(fs, paths) {
250
+ const warnings = [];
251
+ for (const entry of (await fs.readdir(paths.generateAt)).sort()) {
252
+ const leftover = path.join(paths.generateAt, entry);
253
+ if (entry.startsWith(READY_PREFIX)) {
254
+ const stagedEntry = path.join(leftover, CLIENT_ENTRY_FILE);
255
+ const generatedMovedIn = (await lstatOrUndefined(fs, path.join(leftover, GENERATED_DIRECTORY))) === undefined;
256
+ if (generatedMovedIn &&
257
+ (await lstatOrUndefined(fs, stagedEntry)) !== undefined) {
258
+ await fs.rename(stagedEntry, paths.entry);
259
+ warnings.push(`an interrupted run had moved its generated/ in but not its AvClient.ts; ` +
260
+ `the validated AvClient.ts was moved in at ${paths.entry} before generating.`);
261
+ }
262
+ await fs.rm(leftover, { recursive: true, force: true });
263
+ }
264
+ else if (entry.startsWith(NEXT_PREFIX)) {
265
+ await fs.rm(leftover, { recursive: true, force: true });
266
+ }
267
+ }
268
+ if ((await lstatOrUndefined(fs, paths.previous)) === undefined) {
269
+ return warnings;
270
+ }
271
+ if ((await generatedAfterRecovery(fs, paths)) === paths.previous) {
272
+ await fs.rename(paths.previous, paths.generated);
273
+ warnings.push(`an interrupted run had moved the previous generated/ aside; it was put back at ${paths.generated} before generating.`);
274
+ }
275
+ else {
276
+ await fs.rm(paths.previous, { recursive: true, force: true });
277
+ }
278
+ return warnings;
279
+ }
280
+ /**
281
+ * Where the `generated/` a run works with comes from once a killed run is
282
+ * recovered: the previous one moved aside when nothing replaced it — recovery
283
+ * puts it back — and `generated/` otherwise. One answer for the recovery and for
284
+ * the read-only look ahead of it, so the two cannot disagree.
285
+ */
286
+ async function generatedAfterRecovery(fs, paths) {
287
+ return (await lstatOrUndefined(fs, paths.generated)) === undefined &&
288
+ (await lstatOrUndefined(fs, paths.previous)) !== undefined
289
+ ? paths.previous
290
+ : paths.generated;
291
+ }
292
+ /**
293
+ * Lists what in `<generateAt>/AvClient.ts` and `<generateAt>/generated/` the
294
+ * generator did not produce — a file without the ownership line, a symbolic link,
295
+ * anything that is not a regular file — so the person running it can be asked
296
+ * before a generation overwrites or removes it (architect, 2026-10-04). Reads
297
+ * only; looks at nothing else in `generateAt`. Empty when `generateAt` does not
298
+ * exist yet.
299
+ *
300
+ * It sees what the write will see once it has recovered a killed run, without
301
+ * recovering it: a `generated/` the killed run moved aside with nothing in its
302
+ * place is read where it lies and its files named where recovery puts them back
303
+ * (`generated/…`). So the question asked, and `--yes`, cover them too — and a
304
+ * run that stops here has still touched nothing.
305
+ *
306
+ * @throws OutputWriteError when `generateAt` is not a directory, `generated` is
307
+ * not a directory or `AvClient.ts` is not a file: no confirmation repairs that.
308
+ */
309
+ export async function findForeignOutputContent(generateAt, fileSystem = fsPromises) {
310
+ const root = path.resolve(generateAt);
311
+ const stats = await lstatOrUndefined(fileSystem, root);
312
+ if (stats === undefined) {
313
+ return [];
314
+ }
315
+ if (!stats.isDirectory()) {
316
+ throw notADirectory(root);
317
+ }
318
+ const paths = {
319
+ generateAt: root,
320
+ generated: path.join(root, GENERATED_DIRECTORY),
321
+ entry: path.join(root, CLIENT_ENTRY_FILE),
322
+ previous: path.join(root, PREVIOUS_GENERATED),
323
+ };
324
+ const inspection = await inspectOwnedEntries(fileSystem, paths, await generatedAfterRecovery(fileSystem, paths));
325
+ return inspection.foreign;
326
+ }
327
+ /**
328
+ * What stands at the two owned entries. `generatedSource` is the directory read
329
+ * as `generated/` — `generated/` itself, or the previous one a killed run moved
330
+ * aside, which recovery will put back there — and its files are named as
331
+ * `generated/…` either way.
332
+ */
333
+ async function inspectOwnedEntries(fs, paths, generatedSource = paths.generated) {
334
+ const foreign = [];
335
+ const relative = (file) => path.relative(paths.generateAt, file).split(path.sep).join("/");
336
+ const generatedStats = await lstatOrUndefined(fs, generatedSource);
337
+ if (generatedStats !== undefined) {
338
+ if (!generatedStats.isDirectory()) {
339
+ throw new OutputWriteError("prepare", `refusing to replace ${generatedSource}: it is not a directory, and the generator owns ` +
340
+ `${GENERATED_DIRECTORY}/ as one; move it, or choose another \`generateAt\`. Nothing was written.`);
341
+ }
342
+ const entries = await fs.readdir(generatedSource, {
343
+ recursive: true,
344
+ withFileTypes: true,
345
+ });
346
+ for (const entry of entries) {
347
+ if (entry.isDirectory()) {
348
+ continue;
349
+ }
350
+ const file = path.join(entry.parentPath, entry.name);
351
+ if (!entry.isFile() || !(await carriesOwnershipLine(fs, file))) {
352
+ foreign.push(relative(path.join(paths.generated, path.relative(generatedSource, file))));
353
+ }
354
+ }
355
+ }
356
+ const entryStats = await lstatOrUndefined(fs, paths.entry);
357
+ if (entryStats !== undefined) {
358
+ if (!entryStats.isFile()) {
359
+ throw new OutputWriteError("prepare", `refusing to replace ${paths.entry}: it is not a file, and the generator owns ${CLIENT_ENTRY_FILE} ` +
360
+ "as one; move it, or choose another `generateAt`. Nothing was written.");
361
+ }
362
+ if (!(await carriesOwnershipLine(fs, paths.entry))) {
363
+ foreign.push(relative(paths.entry));
364
+ }
365
+ }
366
+ return {
367
+ generated: generatedStats === undefined ? "absent" : "present",
368
+ entry: entryStats === undefined ? "absent" : "present",
369
+ foreign: foreign.sort(),
370
+ };
371
+ }
372
+ /** Foreign content nobody confirmed may be overwritten. */
373
+ function foreignContentRefusal(paths, foreign) {
374
+ const them = foreign.length === 1 ? "it" : "them";
375
+ return new OutputWriteError("prepare", `refusing to generate into ${paths.generateAt}: ${foreign.join(", ")} ${foreign.length === 1 ? "was" : "were"} ` +
376
+ `not generated by @aventara/client, and generating would overwrite or remove ${them}; confirm the ` +
377
+ `override, move ${them} out, or choose another \`generateAt\`. Nothing was written.`);
378
+ }
379
+ function notADirectory(generateAt) {
380
+ return new OutputWriteError("prepare", `refusing to generate into ${generateAt}: it is not a directory. ` +
381
+ "`generateAt` must name a directory; choose another `generateAt`. Nothing was written.");
382
+ }
383
+ async function carriesOwnershipLine(fs, file) {
384
+ const handle = await fs.open(file, "r");
385
+ try {
386
+ const head = Buffer.alloc(GENERATED_OWNERSHIP_HEAD_BYTES);
387
+ const { bytesRead } = await handle.read(head, 0, GENERATED_OWNERSHIP_HEAD_BYTES, 0);
388
+ return carriesGeneratedOwnership(head.subarray(0, bytesRead).toString("utf8"));
389
+ }
390
+ finally {
391
+ await handle.close();
392
+ }
393
+ }
394
+ /**
395
+ * Undoes a replace that stopped part-way. When the undo itself fails, the
396
+ * sentence says where the previous `generated/` is: the next run puts it back.
397
+ */
398
+ async function rollBack(paths, cause, undo) {
399
+ try {
400
+ await undo();
401
+ }
402
+ catch (rollbackError) {
403
+ throw new OutputWriteError("replace", `the generated output could not be moved into ${paths.generateAt} (${describe(cause)}), and the ` +
404
+ `previous output could not be restored (${describe(rollbackError)}). The previous ` +
405
+ `${GENERATED_DIRECTORY}/ is intact at ${paths.previous}; the next run puts it back.`, { cause });
406
+ }
407
+ }
408
+ /**
409
+ * Runs one stage, turning a filesystem error into that stage's sentence. An
410
+ * `OutputWriteError` passes through; anything without a `code` is a defect and
411
+ * keeps its stack.
412
+ */
413
+ async function stage(name, paths, run) {
414
+ try {
415
+ return await run();
416
+ }
417
+ catch (error) {
418
+ if (error instanceof OutputWriteError || !isSystemError(error)) {
419
+ throw error;
420
+ }
421
+ throw new OutputWriteError(name, `could not ${STAGE_ACTIONS[name]} (${describe(error)}); ${LEFT_AS_THEY_WERE(paths)}`, { cause: error });
422
+ }
423
+ }
424
+ const STAGE_ACTIONS = {
425
+ prepare: "inspect the generated output",
426
+ temp: "write the generated output to a staging directory",
427
+ validate: "validate the generated output",
428
+ replace: "replace the generated output",
429
+ };
430
+ async function lstatOrUndefined(fs, file) {
431
+ try {
432
+ return await fs.lstat(file);
433
+ }
434
+ catch (error) {
435
+ if (isSystemError(error) && error.code === "ENOENT") {
436
+ return undefined;
437
+ }
438
+ throw error;
439
+ }
440
+ }
441
+ function isSystemError(error) {
442
+ return (error instanceof Error &&
443
+ typeof error.code === "string");
444
+ }
445
+ function describe(error) {
446
+ return isSystemError(error)
447
+ ? `${error.code}: ${error.message}`
448
+ : String(error);
449
+ }
450
+ /**
451
+ * Whether `AvClient.ts` and `generated/` in `generateAt` hold exactly `tree` —
452
+ * every file byte for byte, no other file, and nothing a killed run moved aside —
453
+ * so that writing `tree` would change nothing (Phase 12-rest Q6: "up to date").
454
+ */
455
+ export async function ownedOutputMatches(generateAt, tree) {
456
+ const root = path.resolve(generateAt);
457
+ if ((await lstatOrUndefined(fsPromises, path.join(root, PREVIOUS_GENERATED))) !== undefined) {
458
+ return false;
459
+ }
460
+ let present;
461
+ try {
462
+ present = (await fsPromises.readdir(path.join(root, GENERATED_DIRECTORY), {
463
+ recursive: true,
464
+ withFileTypes: true,
465
+ }))
466
+ .filter((entry) => !entry.isDirectory())
467
+ .map((entry) => path
468
+ .relative(root, path.join(entry.parentPath, entry.name))
469
+ .split(path.sep)
470
+ .join("/"));
471
+ }
472
+ catch (error) {
473
+ if (isSystemError(error) && error.code === "ENOENT") {
474
+ return false;
475
+ }
476
+ throw error;
477
+ }
478
+ const expected = new Set(tree.map((file) => file.path));
479
+ if (present.length + 1 !== expected.size ||
480
+ present.some((file) => !expected.has(file))) {
481
+ return false;
482
+ }
483
+ for (const file of tree) {
484
+ let bytes;
485
+ try {
486
+ bytes = await fsPromises.readFile(path.join(root, file.path));
487
+ }
488
+ catch (error) {
489
+ if (isSystemError(error) && error.code === "ENOENT") {
490
+ return false;
491
+ }
492
+ throw error;
493
+ }
494
+ if (!bytes.equals(Buffer.from(file.bytes))) {
495
+ return false;
496
+ }
497
+ }
498
+ return true;
499
+ }
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "@aventara/client",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0-pilot.1",
4
+ "license": "SEE LICENSE IN LICENSE",
5
+ "description": "Development-time generator for Aventara typed remote clients.",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": "^22.18.0 || >=24.2.0"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "bin": {
13
+ "avclient": "./dist/avclient.bin.js"
14
+ },
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "LICENSE-ADDITIONAL-PERMISSION.md"
24
+ ],
25
+ "dependencies": {
26
+ "@aventara/core": "0.1.0-pilot.1"
27
+ },
28
+ "peerDependencies": {
29
+ "typescript": ">=5.5.0"
30
+ },
31
+ "peerDependenciesMeta": {
32
+ "typescript": {
33
+ "optional": true
34
+ }
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "devDependencies": {
40
+ "@aventara/testing": "0.1.0-pilot.1",
41
+ "@types/node": "24.10.1",
42
+ "typescript": "^5.9.2"
43
+ },
44
+ "scripts": {
45
+ "build": "tsc -p tsconfig.json",
46
+ "typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
47
+ "test": "vitest run --typecheck --config ../../vitest.config.mts --root .",
48
+ "lint": "biome lint src"
49
+ }
6
50
  }