@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,27 @@
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
+ /**
5
+ * R4, §15.2 — the config `avclient init` writes: developer-owned, read by
6
+ * `avclient generate`. `file` names it in its own comment line: the
7
+ * `framework.client.mts` it writes, or — to recognize what pilot.0 wrote —
8
+ * `framework.client.ts`.
9
+ */
10
+ export function clientConfigSource(input, file = CLIENT_CONFIG_FILE) {
11
+ return [
12
+ // The specifier is spliced in, not written after `from`, so this module's own
13
+ // shipped JavaScript does not read as importing the package it names.
14
+ input.envVar === undefined
15
+ ? `import { defineClientConfig } from ${JSON.stringify(CLIENT_PACKAGE)};`
16
+ : `import { defineClientConfig, env } from ${JSON.stringify(CLIENT_PACKAGE)};`,
17
+ "",
18
+ `// ${file}: where the Aventara server is, and where its typed client goes.`,
19
+ "export default defineClientConfig({",
20
+ input.envVar === undefined
21
+ ? `\tentrypoint: ${JSON.stringify(input.entrypoint)},`
22
+ : `\tentrypoint: env(${JSON.stringify(input.envVar)}),`,
23
+ `\tgenerateAt: ${JSON.stringify(input.generateAt)},`,
24
+ "});",
25
+ "",
26
+ ].join("\n");
27
+ }
@@ -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,86 @@
1
+ import { mkdirSync, readFileSync, rmSync, 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
+ if (write.content === undefined) {
54
+ rmSync(target, { force: true });
55
+ continue;
56
+ }
57
+ mkdirSync(path.dirname(target), { recursive: true });
58
+ writeFileSync(target, write.content);
59
+ }
60
+ io.stdout(`avclient: initialized ${io.cwd}: ${writes.map((write) => (write.content === undefined ? `removed ${write.path}` : write.path)).join(", ") || "nothing to change"}.\n`);
61
+ const install = `${answers.packageManager} install`;
62
+ if (command.skipInstall) {
63
+ io.stdout(`Next:\n 1. Install @aventara/client ${clientVersion}: ${install}\n 2. With the server running: ${answers.packageManager} run ${GENERATE_SCRIPT}\n`);
64
+ return 0;
65
+ }
66
+ io.stdout(`avclient: running ${install}…\n`);
67
+ const installed = await (io.run ?? runCommand)(answers.packageManager, ["install"], {
68
+ cwd: io.cwd,
69
+ });
70
+ if (installed.code !== 0) {
71
+ 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()}`);
72
+ }
73
+ if (command.skipGenerate) {
74
+ io.stdout(`Next: with the server running, ${answers.packageManager} run ${GENERATE_SCRIPT}\n`);
75
+ return 0;
76
+ }
77
+ try {
78
+ return await runGenerate(io, command.yes);
79
+ }
80
+ catch (error) {
81
+ if (error instanceof ContractTransportError) {
82
+ throw new ClientInitStepError(`the project is set up, but the server did not answer (${error.message.replace(/\.$/, "")}); once it is running, run \`avclient generate\`.`);
83
+ }
84
+ throw error;
85
+ }
86
+ }
@@ -0,0 +1,27 @@
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
+ /** One change to a project file: its new content, or `undefined` to remove it. */
13
+ export type ClientInitWrite = {
14
+ readonly path: string;
15
+ readonly content: string | undefined;
16
+ };
17
+ export type ClientInitPlan = {
18
+ readonly keeping: readonly ClientInitWrite[];
19
+ readonly replacing: readonly ClientInitWrite[];
20
+ readonly conflicts: readonly string[];
21
+ };
22
+ export declare function planClientInit(input: {
23
+ readonly project: ClientProject;
24
+ readonly answers: ClientInitAnswers;
25
+ /** This package's own version: the devDependency is pinned to it exactly (N6). */
26
+ readonly clientVersion: string;
27
+ }): ClientInitPlan;
@@ -0,0 +1,99 @@
1
+ import { CLIENT_CONFIG_FILE, LEGACY_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
+ // pilot.1: a `framework.client.ts` is moved to `framework.client.mts` — two
35
+ // configs would make `avclient generate` refuse. The one pilot.0 wrote for
36
+ // these answers moves without asking; anything else is a conflict.
37
+ const legacyConfig = project.read(LEGACY_CLIENT_CONFIG_FILE);
38
+ if (legacyConfig !== undefined) {
39
+ if (legacyConfig !== clientConfigSource(answers, LEGACY_CLIENT_CONFIG_FILE)) {
40
+ conflicts.push(`${LEGACY_CLIENT_CONFIG_FILE} (replaced by ${CLIENT_CONFIG_FILE})`);
41
+ }
42
+ keeping.push({ path: LEGACY_CLIENT_CONFIG_FILE, content: undefined });
43
+ replacing.push({ path: LEGACY_CLIENT_CONFIG_FILE, content: undefined });
44
+ }
45
+ if (existingConfig !== undefined && existingConfig !== config) {
46
+ conflicts.push(CLIENT_CONFIG_FILE);
47
+ }
48
+ add(CLIENT_CONFIG_FILE, existingConfig, existingConfig ?? config, config);
49
+ if (answers.envVar !== undefined) {
50
+ const before = project.read(".env");
51
+ const lines = before === undefined || before === ""
52
+ ? []
53
+ : before.replace(/\n$/, "").split("\n");
54
+ const at = lines.findIndex((line) => ENV_LINE.exec(line)?.[1] === answers.envVar);
55
+ const line = `${answers.envVar}="${answers.entrypoint}"`;
56
+ const kept = [...lines];
57
+ const replaced = [...lines];
58
+ if (at === -1) {
59
+ kept.push(line);
60
+ replaced.push(line);
61
+ }
62
+ else if (unquoted(ENV_LINE.exec(lines[at])?.[2] ?? "") !==
63
+ answers.entrypoint) {
64
+ conflicts.push(`.env's ${answers.envVar}`);
65
+ replaced[at] = line;
66
+ }
67
+ add(".env", before, `${kept.join("\n")}\n`, `${replaced.join("\n")}\n`);
68
+ }
69
+ const manifest = (replace) => {
70
+ const parsed = JSON.parse(project.manifestText);
71
+ const scripts = {
72
+ ...(parsed.scripts ?? {}),
73
+ };
74
+ const command = "avclient generate";
75
+ if (scripts[GENERATE_SCRIPT] === undefined || replace) {
76
+ scripts[GENERATE_SCRIPT] = command;
77
+ }
78
+ const devDependencies = {
79
+ ...(parsed.devDependencies ?? {}),
80
+ };
81
+ if (devDependencies["@aventara/client"] === undefined || replace) {
82
+ devDependencies["@aventara/client"] = input.clientVersion;
83
+ }
84
+ parsed.scripts = scripts;
85
+ parsed.devDependencies = Object.fromEntries(Object.entries(devDependencies).sort(([left], [right]) => left < right ? -1 : 1));
86
+ return `${JSON.stringify(parsed, null, 2)}\n`;
87
+ };
88
+ const parsed = JSON.parse(project.manifestText);
89
+ const script = parsed.scripts?.[GENERATE_SCRIPT];
90
+ if (script !== undefined && script !== "avclient generate") {
91
+ conflicts.push(`package.json's scripts["${GENERATE_SCRIPT}"]`);
92
+ }
93
+ const pinned = parsed.devDependencies?.["@aventara/client"];
94
+ if (pinned !== undefined && pinned !== input.clientVersion) {
95
+ conflicts.push(`package.json's devDependencies["@aventara/client"] (${pinned})`);
96
+ }
97
+ add("package.json", project.manifestText, manifest(false), manifest(true));
98
+ return { keeping, replacing, conflicts };
99
+ }
@@ -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
+ }