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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +91 -0
  2. package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
  3. package/README.md +268 -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 +30 -0
  7. package/dist/cli/command.parser.js +132 -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 +54 -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 +71 -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 +33 -0
  23. package/dist/config/config.loader.js +80 -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 +6 -0
  65. package/dist/init/client-config.template.js +22 -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 +82 -0
  70. package/dist/init/client-init.planner.d.ts +26 -0
  71. package/dist/init/client-init.planner.js +88 -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 +76 -0
  81. package/dist/output/output.validator.js +254 -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,22 @@
1
+ import { CLIENT_CONFIG_FILE } from "../config/config.loader.js";
2
+ /** The package the written config imports its two functions from. */
3
+ const CLIENT_PACKAGE = "@aventara/client";
4
+ /** R4, §15.2 — the `framework.client.ts` `avclient init` writes: developer-owned, read by `avclient generate`. */
5
+ export function clientConfigSource(input) {
6
+ return [
7
+ // The specifier is spliced in, not written after `from`, so this module's own
8
+ // shipped JavaScript does not read as importing the package it names.
9
+ input.envVar === undefined
10
+ ? `import { defineClientConfig } from ${JSON.stringify(CLIENT_PACKAGE)};`
11
+ : `import { defineClientConfig, env } from ${JSON.stringify(CLIENT_PACKAGE)};`,
12
+ "",
13
+ `// ${CLIENT_CONFIG_FILE}: where the Aventara server is, and where its typed client goes.`,
14
+ "export default defineClientConfig({",
15
+ input.envVar === undefined
16
+ ? `\tentrypoint: ${JSON.stringify(input.entrypoint)},`
17
+ : `\tentrypoint: env(${JSON.stringify(input.envVar)}),`,
18
+ `\tgenerateAt: ${JSON.stringify(input.generateAt)},`,
19
+ "});",
20
+ "",
21
+ ].join("\n");
22
+ }
@@ -0,0 +1,9 @@
1
+ /** `avclient init`'s refusals after the questions: each one sentence, exit 1. */
2
+ /** Nobody confirmed replacing existing content. A refusal: nothing was touched. */
3
+ export declare class ClientInitNotConfirmedError extends Error {
4
+ readonly name = "ClientInitNotConfirmedError";
5
+ }
6
+ /** A step after the write failed. Reported with what to run; the files are kept. */
7
+ export declare class ClientInitStepError extends Error {
8
+ readonly name = "ClientInitStepError";
9
+ }
@@ -0,0 +1,9 @@
1
+ /** `avclient init`'s refusals after the questions: each one sentence, exit 1. */
2
+ /** Nobody confirmed replacing existing content. A refusal: nothing was touched. */
3
+ export class ClientInitNotConfirmedError extends Error {
4
+ name = "ClientInitNotConfirmedError";
5
+ }
6
+ /** A step after the write failed. Reported with what to run; the files are kept. */
7
+ export class ClientInitStepError extends Error {
8
+ name = "ClientInitStepError";
9
+ }
@@ -0,0 +1,3 @@
1
+ import type { ClientInitCommand } from "../cli/command.parser.js";
2
+ import { type CliIo } from "../cli/generate.command.js";
3
+ export declare function runClientInit(command: ClientInitCommand, io: CliIo): Promise<number>;
@@ -0,0 +1,82 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { runGenerate } from "../cli/generate.command.js";
4
+ import { CLIENT_CONFIG_FILE } from "../config/config.loader.js";
5
+ import { ContractTransportError } from "../contract/contract.fetcher.js";
6
+ import { ClientInitNotConfirmedError, ClientInitStepError, } from "./client-init.errors.js";
7
+ import { GENERATE_SCRIPT, planClientInit } from "./client-init.planner.js";
8
+ import { resolveClientInitAnswers } from "./client-init.questions.js";
9
+ import { inspectClientProject } from "./client-project.inspector.js";
10
+ import { runCommand } from "./command.runner.js";
11
+ /**
12
+ * R4 — `avclient init`, the whole frontend setup, in the order every wizard of
13
+ * this item keeps: inspect, ask, plan, confirm, and only then write; then the
14
+ * install (N6) and, unless skipped, the first generation through the very path
15
+ * `avclient generate` takes. A refusal before the write leaves the project as
16
+ * it was; a failure after it says what to run next, and keeps the files.
17
+ */
18
+ const MANIFEST = new URL("../../package.json", import.meta.url);
19
+ function ownVersion() {
20
+ return JSON.parse(readFileSync(MANIFEST, "utf8"))
21
+ .version;
22
+ }
23
+ export async function runClientInit(command, io) {
24
+ const project = inspectClientProject(io.cwd);
25
+ const userAgent = io.env.npm_config_user_agent;
26
+ const answers = await resolveClientInitAnswers({
27
+ given: command.given,
28
+ noEnvVar: command.noEnvVar,
29
+ yes: command.yes,
30
+ interactive: io.interactive,
31
+ ask: io.ask ?? (async () => ""),
32
+ say: (line) => io.stderr(`${line}\n`),
33
+ detectedPackageManager: project.lockfile ??
34
+ (userAgent?.split("/")[0] === "pnpm" ? "pnpm" : "npm"),
35
+ });
36
+ const clientVersion = ownVersion();
37
+ const plan = planClientInit({ project, answers, clientVersion });
38
+ // The generateAt rule (architect, 2026-10-04; R5).
39
+ if (plan.conflicts.length > 0 && !command.yes) {
40
+ const them = plan.conflicts.length === 1 ? "it" : "them";
41
+ const has = plan.conflicts.length === 1 ? "has" : "have";
42
+ if (!io.interactive) {
43
+ throw new ClientInitNotConfirmedError(`not initializing, and nothing was touched: ${plan.conflicts.join(", ")} already ${has} other content and nobody can be asked (stdin is not a terminal); run \`avclient init --yes\` to replace ${them}.`);
44
+ }
45
+ io.stderr(`avclient: warning: ${plan.conflicts.join(", ")} already ${has} other content, and initializing will replace ${them}.\n`);
46
+ if (!(await io.confirm("avclient: replace them and initialize? [y/N] "))) {
47
+ throw new ClientInitNotConfirmedError(`cancelled, and nothing was touched; run again and answer yes to replace ${them}.`);
48
+ }
49
+ }
50
+ const writes = plan.conflicts.length === 0 ? plan.keeping : plan.replacing;
51
+ for (const write of writes) {
52
+ const target = path.join(io.cwd, write.path);
53
+ mkdirSync(path.dirname(target), { recursive: true });
54
+ writeFileSync(target, write.content);
55
+ }
56
+ io.stdout(`avclient: initialized ${io.cwd}: ${writes.map((write) => write.path).join(", ") || "nothing to change"}.\n`);
57
+ const install = `${answers.packageManager} install`;
58
+ if (command.skipInstall) {
59
+ io.stdout(`Next:\n 1. Install @aventara/client ${clientVersion}: ${install}\n 2. With the server running: ${answers.packageManager} run ${GENERATE_SCRIPT}\n`);
60
+ return 0;
61
+ }
62
+ io.stdout(`avclient: running ${install}…\n`);
63
+ const installed = await (io.run ?? runCommand)(answers.packageManager, ["install"], {
64
+ cwd: io.cwd,
65
+ });
66
+ if (installed.code !== 0) {
67
+ throw new ClientInitStepError(`\`${install}\` exited with code ${installed.code} after ${CLIENT_CONFIG_FILE} was written; fix what it reports and run \`${install}\` again:\n${installed.stderr.trimEnd()}`);
68
+ }
69
+ if (command.skipGenerate) {
70
+ io.stdout(`Next: with the server running, ${answers.packageManager} run ${GENERATE_SCRIPT}\n`);
71
+ return 0;
72
+ }
73
+ try {
74
+ return await runGenerate(io, command.yes);
75
+ }
76
+ catch (error) {
77
+ if (error instanceof ContractTransportError) {
78
+ throw new ClientInitStepError(`the project is set up, but the server did not answer (${error.message.replace(/\.$/, "")}); once it is running, run \`avclient generate\`.`);
79
+ }
80
+ throw error;
81
+ }
82
+ }
@@ -0,0 +1,26 @@
1
+ import type { ClientInitAnswers } from "./client-init.questions.js";
2
+ import type { ClientProject } from "./client-project.inspector.js";
3
+ /**
4
+ * §4.5 — what `avclient init` writes, decided before anything is: the config
5
+ * file, the `.env` entry (only with a variable), the `avclient:generate` script
6
+ * and the exact devDependency (N6) — each computed with existing content kept
7
+ * and replaced, and every **conflict** (content it did not produce) named for
8
+ * the generateAt rule.
9
+ */
10
+ /** R4: the script a frontend regenerates its client with. */
11
+ export declare const GENERATE_SCRIPT = "avclient:generate";
12
+ export type ClientInitWrite = {
13
+ readonly path: string;
14
+ readonly content: string;
15
+ };
16
+ export type ClientInitPlan = {
17
+ readonly keeping: readonly ClientInitWrite[];
18
+ readonly replacing: readonly ClientInitWrite[];
19
+ readonly conflicts: readonly string[];
20
+ };
21
+ export declare function planClientInit(input: {
22
+ readonly project: ClientProject;
23
+ readonly answers: ClientInitAnswers;
24
+ /** This package's own version: the devDependency is pinned to it exactly (N6). */
25
+ readonly clientVersion: string;
26
+ }): ClientInitPlan;
@@ -0,0 +1,88 @@
1
+ import { CLIENT_CONFIG_FILE } from "../config/config.loader.js";
2
+ import { clientConfigSource } from "./client-config.template.js";
3
+ /**
4
+ * §4.5 — what `avclient init` writes, decided before anything is: the config
5
+ * file, the `.env` entry (only with a variable), the `avclient:generate` script
6
+ * and the exact devDependency (N6) — each computed with existing content kept
7
+ * and replaced, and every **conflict** (content it did not produce) named for
8
+ * the generateAt rule.
9
+ */
10
+ /** R4: the script a frontend regenerates its client with. */
11
+ export const GENERATE_SCRIPT = "avclient:generate";
12
+ const ENV_LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/;
13
+ function unquoted(value) {
14
+ const quote = value[0];
15
+ return (quote === '"' || quote === "'") && value.endsWith(quote)
16
+ ? value.slice(1, -1)
17
+ : value;
18
+ }
19
+ export function planClientInit(input) {
20
+ const { project, answers } = input;
21
+ const conflicts = [];
22
+ const keeping = [];
23
+ const replacing = [];
24
+ const add = (path, before, kept, replaced) => {
25
+ if (kept !== before) {
26
+ keeping.push({ path, content: kept });
27
+ }
28
+ if (replaced !== before) {
29
+ replacing.push({ path, content: replaced });
30
+ }
31
+ };
32
+ const config = clientConfigSource(answers);
33
+ const existingConfig = project.read(CLIENT_CONFIG_FILE);
34
+ if (existingConfig !== undefined && existingConfig !== config) {
35
+ conflicts.push(CLIENT_CONFIG_FILE);
36
+ }
37
+ add(CLIENT_CONFIG_FILE, existingConfig, existingConfig ?? config, config);
38
+ if (answers.envVar !== undefined) {
39
+ const before = project.read(".env");
40
+ const lines = before === undefined || before === ""
41
+ ? []
42
+ : before.replace(/\n$/, "").split("\n");
43
+ const at = lines.findIndex((line) => ENV_LINE.exec(line)?.[1] === answers.envVar);
44
+ const line = `${answers.envVar}="${answers.entrypoint}"`;
45
+ const kept = [...lines];
46
+ const replaced = [...lines];
47
+ if (at === -1) {
48
+ kept.push(line);
49
+ replaced.push(line);
50
+ }
51
+ else if (unquoted(ENV_LINE.exec(lines[at])?.[2] ?? "") !==
52
+ answers.entrypoint) {
53
+ conflicts.push(`.env's ${answers.envVar}`);
54
+ replaced[at] = line;
55
+ }
56
+ add(".env", before, `${kept.join("\n")}\n`, `${replaced.join("\n")}\n`);
57
+ }
58
+ const manifest = (replace) => {
59
+ const parsed = JSON.parse(project.manifestText);
60
+ const scripts = {
61
+ ...(parsed.scripts ?? {}),
62
+ };
63
+ const command = "avclient generate";
64
+ if (scripts[GENERATE_SCRIPT] === undefined || replace) {
65
+ scripts[GENERATE_SCRIPT] = command;
66
+ }
67
+ const devDependencies = {
68
+ ...(parsed.devDependencies ?? {}),
69
+ };
70
+ if (devDependencies["@aventara/client"] === undefined || replace) {
71
+ devDependencies["@aventara/client"] = input.clientVersion;
72
+ }
73
+ parsed.scripts = scripts;
74
+ parsed.devDependencies = Object.fromEntries(Object.entries(devDependencies).sort(([left], [right]) => left < right ? -1 : 1));
75
+ return `${JSON.stringify(parsed, null, 2)}\n`;
76
+ };
77
+ const parsed = JSON.parse(project.manifestText);
78
+ const script = parsed.scripts?.[GENERATE_SCRIPT];
79
+ if (script !== undefined && script !== "avclient generate") {
80
+ conflicts.push(`package.json's scripts["${GENERATE_SCRIPT}"]`);
81
+ }
82
+ const pinned = parsed.devDependencies?.["@aventara/client"];
83
+ if (pinned !== undefined && pinned !== input.clientVersion) {
84
+ conflicts.push(`package.json's devDependencies["@aventara/client"] (${pinned})`);
85
+ }
86
+ add("package.json", project.manifestText, manifest(false), manifest(true));
87
+ return { keeping, replacing, conflicts };
88
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * R4, R5, Q16 rows 12–15 — `avclient init`'s questions in one table: each
3
+ * question's flag, label and default, from which the command line, the prompt,
4
+ * `--yes`'s defaults and the non-interactive refusal are derived — the rule
5
+ * `@aventara/cli`'s wizard follows, implemented here because the two packages
6
+ * never import each other (P7).
7
+ */
8
+ export declare const PACKAGE_MANAGERS: readonly ["npm", "pnpm"];
9
+ export type PackageManager = (typeof PACKAGE_MANAGERS)[number];
10
+ export type ClientInitAnswers = {
11
+ /** The deployment's entrypoint (Q16-12: `--entrypoint`, `defineClientConfig`'s key). */
12
+ readonly entrypoint: string;
13
+ /** The variable `framework.client.ts` reads it from, or `undefined` for a literal (Q16-13). */
14
+ readonly envVar: string | undefined;
15
+ /** Where the client is generated (Q16-14: `--generate-at`, the config key). */
16
+ readonly generateAt: string;
17
+ readonly packageManager: PackageManager;
18
+ };
19
+ type QuestionId = "entrypoint" | "envVar" | "generateAt" | "packageManager";
20
+ /** An answer the question cannot take. A refusal: one sentence. */
21
+ export declare class ClientInitAnswerError extends Error {
22
+ readonly name = "ClientInitAnswerError";
23
+ }
24
+ /** Nobody can answer and some questions are unanswered. A refusal: one sentence. */
25
+ export declare class ClientInitUnansweredError extends Error {
26
+ readonly name = "ClientInitUnansweredError";
27
+ }
28
+ export type ClientInitQuestion = {
29
+ readonly id: QuestionId;
30
+ readonly flag: string;
31
+ readonly placeholder: string;
32
+ readonly label: string;
33
+ readonly defaultFor: (detectedPackageManager: PackageManager) => string;
34
+ /** @throws ClientInitAnswerError */
35
+ readonly parse: (raw: string) => string;
36
+ };
37
+ /** The project convention for the variable (spec §15.2). */
38
+ export declare const DEFAULT_ENV_VAR = "AVENTARA_API_URL";
39
+ export declare const CLIENT_INIT_QUESTIONS: readonly ClientInitQuestion[];
40
+ export type ClientInitSources = {
41
+ readonly given: Readonly<Partial<Record<QuestionId, string>>>;
42
+ /** `--no-env-var`: the entrypoint is written as a literal, and no variable is asked. */
43
+ readonly noEnvVar: boolean;
44
+ readonly yes: boolean;
45
+ readonly interactive: boolean;
46
+ readonly ask: (prompt: string) => Promise<string>;
47
+ readonly say: (line: string) => void;
48
+ readonly detectedPackageManager: PackageManager;
49
+ };
50
+ /** Flag → `--yes`'s default → the prompt → the refusal naming exactly the unanswered flags (R5). */
51
+ export declare function resolveClientInitAnswers(sources: ClientInitSources): Promise<ClientInitAnswers>;
52
+ export {};
@@ -0,0 +1,124 @@
1
+ import { resolveEntrypoint } from "../config/config.resolver.js";
2
+ /**
3
+ * R4, R5, Q16 rows 12–15 — `avclient init`'s questions in one table: each
4
+ * question's flag, label and default, from which the command line, the prompt,
5
+ * `--yes`'s defaults and the non-interactive refusal are derived — the rule
6
+ * `@aventara/cli`'s wizard follows, implemented here because the two packages
7
+ * never import each other (P7).
8
+ */
9
+ export const PACKAGE_MANAGERS = ["npm", "pnpm"];
10
+ /** An answer the question cannot take. A refusal: one sentence. */
11
+ export class ClientInitAnswerError extends Error {
12
+ name = "ClientInitAnswerError";
13
+ }
14
+ /** Nobody can answer and some questions are unanswered. A refusal: one sentence. */
15
+ export class ClientInitUnansweredError extends Error {
16
+ name = "ClientInitUnansweredError";
17
+ }
18
+ /** The project convention for the variable (spec §15.2). */
19
+ export const DEFAULT_ENV_VAR = "AVENTARA_API_URL";
20
+ export const CLIENT_INIT_QUESTIONS = [
21
+ {
22
+ id: "entrypoint",
23
+ flag: "--entrypoint",
24
+ placeholder: "<url>",
25
+ label: "Server entrypoint",
26
+ defaultFor: () => "http://localhost:3000/api",
27
+ parse: (raw) => {
28
+ try {
29
+ resolveEntrypoint(raw.trim());
30
+ }
31
+ catch (error) {
32
+ throw new ClientInitAnswerError(error.message.replace(/\.$/, ""));
33
+ }
34
+ return raw.trim();
35
+ },
36
+ },
37
+ {
38
+ id: "envVar",
39
+ flag: "--env-var",
40
+ placeholder: "<NAME>",
41
+ label: "Environment variable holding it",
42
+ defaultFor: () => DEFAULT_ENV_VAR,
43
+ parse: (raw) => {
44
+ const name = raw.trim();
45
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
46
+ throw new ClientInitAnswerError(`${JSON.stringify(raw)} is not an environment variable name.`);
47
+ }
48
+ return name;
49
+ },
50
+ },
51
+ {
52
+ id: "generateAt",
53
+ flag: "--generate-at",
54
+ placeholder: "<dir>",
55
+ label: "Generate the client into",
56
+ defaultFor: () => "./src/api",
57
+ parse: (raw) => {
58
+ const directory = raw.trim();
59
+ if (directory === "") {
60
+ throw new ClientInitAnswerError("--generate-at takes a directory.");
61
+ }
62
+ return directory;
63
+ },
64
+ },
65
+ {
66
+ id: "packageManager",
67
+ flag: "--package-manager",
68
+ placeholder: `<${PACKAGE_MANAGERS.join("|")}>`,
69
+ label: `Package manager (${PACKAGE_MANAGERS.join(", ")})`,
70
+ defaultFor: (detected) => detected,
71
+ parse: (raw) => {
72
+ const manager = PACKAGE_MANAGERS.find((candidate) => candidate === raw.trim());
73
+ if (manager === undefined) {
74
+ throw new ClientInitAnswerError(`${JSON.stringify(raw)} is not a supported package manager; choose ${PACKAGE_MANAGERS.join(" or ")}.`);
75
+ }
76
+ return manager;
77
+ },
78
+ },
79
+ ];
80
+ /** Flag → `--yes`'s default → the prompt → the refusal naming exactly the unanswered flags (R5). */
81
+ export async function resolveClientInitAnswers(sources) {
82
+ const answered = {};
83
+ const missing = [];
84
+ for (const question of CLIENT_INIT_QUESTIONS) {
85
+ if (question.id === "envVar" && sources.noEnvVar) {
86
+ continue;
87
+ }
88
+ const given = sources.given[question.id];
89
+ const fallback = question.defaultFor(sources.detectedPackageManager);
90
+ if (given !== undefined) {
91
+ answered[question.id] = question.parse(given);
92
+ }
93
+ else if (sources.yes) {
94
+ answered[question.id] = question.parse(fallback);
95
+ }
96
+ else if (sources.interactive) {
97
+ for (;;) {
98
+ const typed = (await sources.ask(`${question.label} [${fallback}]: `)).trim();
99
+ try {
100
+ answered[question.id] = question.parse(typed === "" ? fallback : typed);
101
+ break;
102
+ }
103
+ catch (error) {
104
+ if (!(error instanceof ClientInitAnswerError)) {
105
+ throw error;
106
+ }
107
+ sources.say(error.message);
108
+ }
109
+ }
110
+ }
111
+ else {
112
+ missing.push(`${question.flag} ${question.placeholder}`);
113
+ }
114
+ }
115
+ if (missing.length > 0) {
116
+ throw new ClientInitUnansweredError(`stdin is not a terminal, so nothing can be asked: pass ${missing.join(", ")}, or pass --yes to accept their defaults.`);
117
+ }
118
+ return {
119
+ entrypoint: answered.entrypoint,
120
+ envVar: sources.noEnvVar ? undefined : answered.envVar,
121
+ generateAt: answered.generateAt,
122
+ packageManager: answered.packageManager,
123
+ };
124
+ }
@@ -0,0 +1,15 @@
1
+ import type { PackageManager } from "./client-init.questions.js";
2
+ /** The frontend `avclient init` serves, read before anything is asked or written. */
3
+ /** The directory is not a project `avclient init` can set up. A refusal: nothing written. */
4
+ export declare class ClientProjectRefusedError extends Error {
5
+ readonly name = "ClientProjectRefusedError";
6
+ }
7
+ export type ClientProject = {
8
+ readonly directory: string;
9
+ readonly manifestText: string;
10
+ /** The package manager its lockfile names, if exactly one does. */
11
+ readonly lockfile: PackageManager | undefined;
12
+ /** The current text of a project file, or `undefined`. */
13
+ readonly read: (file: string) => string | undefined;
14
+ };
15
+ export declare function inspectClientProject(directory: string): ClientProject;
@@ -0,0 +1,32 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ /** The frontend `avclient init` serves, read before anything is asked or written. */
4
+ /** The directory is not a project `avclient init` can set up. A refusal: nothing written. */
5
+ export class ClientProjectRefusedError extends Error {
6
+ name = "ClientProjectRefusedError";
7
+ }
8
+ export function inspectClientProject(directory) {
9
+ const manifest = path.join(directory, "package.json");
10
+ if (!existsSync(manifest)) {
11
+ throw new ClientProjectRefusedError("avclient init runs in a frontend project: no package.json here; create one first.");
12
+ }
13
+ const lockfiles = [
14
+ ["package-lock.json", "npm"],
15
+ ["pnpm-lock.yaml", "pnpm"],
16
+ ]
17
+ .filter(([file]) => existsSync(path.join(directory, file)))
18
+ .map(([, manager]) => manager);
19
+ return {
20
+ directory,
21
+ manifestText: readFileSync(manifest, "utf8"),
22
+ lockfile: lockfiles.length === 1 ? lockfiles[0] : undefined,
23
+ read: (file) => {
24
+ try {
25
+ return readFileSync(path.join(directory, file), "utf8");
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ },
31
+ };
32
+ }
@@ -0,0 +1,8 @@
1
+ /** One child process, stdout and stderr piped and collected: `init`'s install. */
2
+ export type CommandRunner = (command: string, args: readonly string[], options: {
3
+ readonly cwd: string;
4
+ }) => Promise<{
5
+ readonly code: number;
6
+ readonly stderr: string;
7
+ }>;
8
+ export declare const runCommand: CommandRunner;
@@ -0,0 +1,17 @@
1
+ import { spawn } from "node:child_process";
2
+ export const runCommand = (command, args, options) => new Promise((resolve, reject) => {
3
+ const child = spawn(command, args, {
4
+ cwd: options.cwd,
5
+ stdio: ["ignore", "pipe", "pipe"],
6
+ env: process.env,
7
+ });
8
+ let stderr = "";
9
+ child.stdout.resume();
10
+ child.stderr.setEncoding("utf8").on("data", (chunk) => {
11
+ stderr += chunk;
12
+ });
13
+ child.once("error", reject);
14
+ child.once("close", (code) => {
15
+ resolve({ code: code ?? 1, stderr });
16
+ });
17
+ });
@@ -0,0 +1,8 @@
1
+ /** The sentence a bin answers on `version`, or `undefined` when `range` admits it. */
2
+ export declare function nodeVersionRefusal(bin: string, packageName: string, range: string, version: string): string | undefined;
3
+ /**
4
+ * Reads the manifest at `manifestUrl`; on a Node its `engines.node` does not
5
+ * admit, writes the sentence to stderr, sets exit code 1 and answers `true` —
6
+ * the bin then loads nothing else.
7
+ */
8
+ export declare function refuseUnsupportedNode(bin: string, manifestUrl: URL): boolean;
@@ -0,0 +1,59 @@
1
+ // biome-ignore lint/style/useNodejsImportProtocol: Node 14.0–14.13.0 resolves no "node:" specifier in an ES module, and this module runs on the Nodes the packages do not support.
2
+ import { readFileSync } from "fs";
3
+ function versionOf(text) {
4
+ const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(text);
5
+ if (match === null) {
6
+ throw new Error(`"${text}" is not a Node version`);
7
+ }
8
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
9
+ }
10
+ function compareVersions(left, right) {
11
+ for (let index = 0; index < 3; index += 1) {
12
+ const difference = left[index] - right[index];
13
+ if (difference !== 0) {
14
+ return difference;
15
+ }
16
+ }
17
+ return 0;
18
+ }
19
+ /**
20
+ * Whether `range` admits `version`. Reads `^x.y.z` (the same major, from the
21
+ * floor; majors ≥ 1) and `>=x.y.z`, joined by `||`; anything else throws rather
22
+ * than admit or refuse by guess.
23
+ */
24
+ function admits(range, version) {
25
+ return range.split("||").some((part) => {
26
+ const comparator = /^\s*(\^|>=)(\d+\.\d+\.\d+)\s*$/.exec(part);
27
+ if (comparator === null) {
28
+ throw new Error(`engines.node "${range}": the bin's Node guard reads only "^x.y.z" and ">=x.y.z" joined by "||"`);
29
+ }
30
+ const floor = versionOf(comparator[2]);
31
+ return (compareVersions(version, floor) >= 0 &&
32
+ (comparator[1] === ">=" || version[0] === floor[0]));
33
+ });
34
+ }
35
+ /** The sentence a bin answers on `version`, or `undefined` when `range` admits it. */
36
+ export function nodeVersionRefusal(bin, packageName, range, version) {
37
+ return admits(range, versionOf(version))
38
+ ? undefined
39
+ : `${bin}: Node ${version} is not supported; ${packageName} needs Node ${range}.`;
40
+ }
41
+ /**
42
+ * Reads the manifest at `manifestUrl`; on a Node its `engines.node` does not
43
+ * admit, writes the sentence to stderr, sets exit code 1 and answers `true` —
44
+ * the bin then loads nothing else.
45
+ */
46
+ export function refuseUnsupportedNode(bin, manifestUrl) {
47
+ const manifest = JSON.parse(readFileSync(manifestUrl, "utf8"));
48
+ const range = manifest.engines === undefined ? undefined : manifest.engines.node;
49
+ if (range === undefined) {
50
+ throw new Error(`${manifest.name} declares no engines.node for its bin's Node guard`);
51
+ }
52
+ const refusal = nodeVersionRefusal(bin, manifest.name, range, process.version);
53
+ if (refusal === undefined) {
54
+ return false;
55
+ }
56
+ process.stderr.write(`${refusal}\n`);
57
+ process.exitCode = 1;
58
+ return true;
59
+ }
@@ -0,0 +1,76 @@
1
+ import type * as ts from "typescript";
2
+ import type { EmittedTree } from "../emit/emitted-tree.interface.js";
3
+ /**
4
+ * §15.3's "type/sanity validation" step, between emit-to-temp and the replace:
5
+ * the gate that makes the atomicity worth having (plan §4 Q6). It judges a
6
+ * directory the writer has just filled with `tree`, and answers with a value — the
7
+ * writer decides what a rejection means for the previous output.
8
+ *
9
+ * # Three checks, the strongest available first
10
+ *
11
+ * - **Shape**, always: the directory holds exactly `tree` — no file missing, none
12
+ * extra, every one byte-for-byte what was emitted — and every file carries the
13
+ * ownership line. That last is not decoration: the NEXT run reads it to decide
14
+ * that this directory is a previous generation it may replace whole, so a file
15
+ * without it would make the next run refuse.
16
+ * - **Types**, when the `typescript` optional peer resolves (Q5, Q6): a real
17
+ * program over the tree under a consumer's strictest plausible settings. The
18
+ * tree is judged ALONE — the compiler host serves nothing outside the directory
19
+ * but TypeScript's own `lib` files, so an import the tree cannot satisfy itself
20
+ * fails here even when a `node_modules` beside the output could satisfy it
21
+ * (§15.5).
22
+ * - **Syntax**, when it does not: each file through Node's own TypeScript parser
23
+ * (`node:module`'s `stripTypeScriptTypes`), with a loud warning that the output
24
+ * was NOT type-checked. Degraded, never skipped (S7).
25
+ */
26
+ /** The `typescript` module, as the validator uses it. */
27
+ export type TypeScriptCompiler = typeof ts;
28
+ /**
29
+ * Finds the `typescript` optional peer: the module, or `undefined` when it is not
30
+ * installed. Anything else — an installation that resolves but fails to load — is
31
+ * not "absent" and is thrown.
32
+ */
33
+ export type TypeScriptResolver = () => Promise<TypeScriptCompiler | undefined>;
34
+ /**
35
+ * A resolver for `specifier`, looked up from this package the way Node would look
36
+ * it up: an optional peer is linked beside the package that declares it.
37
+ */
38
+ export declare function typeScriptResolverFor(specifier: string): TypeScriptResolver;
39
+ /** The `typescript` this package's optional peer names (Q5). */
40
+ export declare const resolveInstalledTypeScript: TypeScriptResolver;
41
+ /**
42
+ * D4 — the resolved `typescript` has no classic compiler API. TypeScript 7 is
43
+ * such a package (plan B3: it exports its version and nothing else), and before
44
+ * this refusal the type check died on it with a `TypeError` stack. A refusal,
45
+ * not a degraded check: a compiler the developer installed is not "absent", and
46
+ * passing it over silently would weaken the check they chose.
47
+ */
48
+ export declare class UnsupportedTypeScriptError extends Error {
49
+ readonly name = "UnsupportedTypeScriptError";
50
+ }
51
+ /** How deeply the tree was judged. */
52
+ export type OutputCheck = "types" | "syntax" | "shape";
53
+ export interface OutputAccepted {
54
+ readonly accepted: true;
55
+ readonly checked: OutputCheck;
56
+ /** Said once to the person running the generator; empty when nothing degraded. */
57
+ readonly warnings: readonly string[];
58
+ }
59
+ export interface OutputRejected {
60
+ readonly accepted: false;
61
+ readonly checked: OutputCheck;
62
+ /** What is wrong, one line per finding, paths relative to the tree. */
63
+ readonly findings: readonly string[];
64
+ }
65
+ export type OutputValidation = OutputAccepted | OutputRejected;
66
+ /**
67
+ * The warning a run without `typescript` carries. Loud on purpose, in its words:
68
+ * the prefix is the CLI's one `warning:`, so the text carries none of its own.
69
+ */
70
+ export declare const TYPESCRIPT_UNRESOLVED_WARNING: string;
71
+ /** The warning a run with neither `typescript` nor Node's parser carries. */
72
+ export declare const SYNTAX_CHECK_UNAVAILABLE_WARNING: string;
73
+ /**
74
+ * Judges `directory`, which the caller has just filled with `tree`.
75
+ */
76
+ export declare function validateOutputTree(directory: string, tree: EmittedTree, resolveTypeScript?: TypeScriptResolver): Promise<OutputValidation>;