apisteady 0.0.0-stage → 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Miguel Gasang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,55 @@
1
- # Temporary Holding Version
1
+ # apisteady
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Generate typed TypeScript API clients from OpenAPI, and keep them maintainable as the API evolves.**
4
+
5
+ apisteady turns an OpenAPI 3.0 / 3.1 or Swagger 2.0 spec into a dependency-free, fully typed TypeScript
6
+ client. Then it keeps that client maintainable: regenerating never overwrites your code, names stay stable when
7
+ the spec shifts, and every API change is listed, with breaking ones marked.
8
+
9
+ ```bash
10
+ npm install --save-dev apisteady
11
+ npx apisteady init # writes .apisteadyrc.yml, detecting your openapi.yaml / .json
12
+ npx apisteady generate # writes ./sdk
13
+ ```
14
+
15
+ ```ts
16
+ import { PetstoreClient } from './sdk/index.js';
17
+
18
+ const client = new PetstoreClient({ auth: { bearerAuth: process.env.TOKEN } });
19
+ const pets = await client.pets.listPets({ limit: 20, status: ['available'] });
20
+ ```
21
+
22
+ ## What it does
23
+
24
+ - **Safe regeneration.** `generated/` is rewritten on every run. Your own code lives in `client.ts` (created once,
25
+ then yours) or in `// <apisteady:custom>` regions inside generated files, and is carried over. Hand edits to
26
+ generated code are detected instead of silently overwritten.
27
+ - **Stable names.** Every assigned name is recorded in a lock file, so a renamed `operationId` or a new
28
+ colliding endpoint never renames an existing method.
29
+ - **API changelog.** `apisteady diff` and `apisteady changelog` list what changed between two versions of the
30
+ spec, and mark breaking changes, judged by whether the data is sent or received.
31
+ - **CI gating.** `apisteady generate --check` fails when the committed SDK is stale, and
32
+ `apisteady diff --base origin/main --require-bump` fails on breaking changes without a new major version.
33
+ A [GitHub Action](https://github.com/euruuuuu/steadysdk#github-action) runs both.
34
+ - **Watch mode.** `apisteady watch` regenerates on every save of the spec, or of any file it references.
35
+ - **Configurable naming, ignore lists, your own HTTP runtime** (`apisteady eject runtime`), and webhook payload types.
36
+
37
+ ## Commands
38
+
39
+ | Command | |
40
+ | --- | --- |
41
+ | `apisteady init` | Create `.apisteadyrc.yml` |
42
+ | `apisteady generate` | Generate the SDK (`--dry-run`, `--check`, `--relock`, `--force`) |
43
+ | `apisteady watch` | Regenerate on every change |
44
+ | `apisteady diff [old] [new]` | List API changes (`--base <git-rev>`, `--json`, `--fail-on-breaking`, `--require-bump`) |
45
+ | `apisteady changelog [old] [new]` | Write the changes as Markdown or JSON (`--out <file>`) |
46
+ | `apisteady eject runtime` | Copy the HTTP runtime into your project to customize it |
47
+
48
+ The CLI needs Node.js 22.19 or newer. The generated SDK has no dependencies and runs anywhere `fetch` exists:
49
+ Node.js 18+, browsers, Deno and Bun.
50
+
51
+ **Documentation, examples and the full option reference:** <https://github.com/euruuuuu/steadysdk>
52
+
53
+ ## License
54
+
55
+ MIT
@@ -0,0 +1,13 @@
1
+ import { type ApiDiff } from '@apisteady/core';
2
+ import type { Ui } from './ui.js';
3
+ export interface PrintChangesOptions {
4
+ /** Shown before the counts, e.g. `API changes since the last generation`. */
5
+ heading: string;
6
+ /** Maximum number of changes listed (breaking ones first); the rest are counted. */
7
+ limit?: number;
8
+ /** Shown when `limit` cuts the list short. */
9
+ moreHint?: string;
10
+ }
11
+ /** Terminal view of a diff: a summary line, then one line per change, breaking changes first. */
12
+ export declare function printChanges(ui: Ui, diff: ApiDiff, options: PrintChangesOptions): void;
13
+ //# sourceMappingURL=changes.d.ts.map
@@ -0,0 +1,32 @@
1
+ import { changeSubject } from '@apisteady/core';
2
+ /** Terminal view of a diff: a summary line, then one line per change, breaking changes first. */
3
+ export function printChanges(ui, diff, options) {
4
+ const { breaking, nonBreaking } = diff.summary;
5
+ const counts = [
6
+ breaking > 0 ? ui.c.red(ui.c.bold(`${breaking} breaking`)) : undefined,
7
+ nonBreaking > 0 ? `${nonBreaking} non-breaking` : undefined,
8
+ ].filter(Boolean);
9
+ ui.info(`${options.heading}: ${counts.join(', ')}`);
10
+ const ordered = [...diff.changes.filter((change) => change.breaking), ...diff.changes.filter((change) => !change.breaking)];
11
+ const limit = options.limit ?? Number.POSITIVE_INFINITY;
12
+ for (const change of ordered.slice(0, limit)) {
13
+ ui.line(` ${symbol(ui, change)} ${ui.c.bold(changeSubject(change))}: ${inlineCode(ui, change.message)}`);
14
+ }
15
+ if (ordered.length > limit) {
16
+ ui.line(ui.c.dim(` … ${ordered.length - limit} more${options.moreHint ? ` (${options.moreHint})` : ''}`));
17
+ }
18
+ }
19
+ function symbol(ui, change) {
20
+ if (change.breaking)
21
+ return ui.c.red('!');
22
+ if (change.kind.endsWith('-added'))
23
+ return ui.c.green('+');
24
+ if (change.kind.endsWith('-removed'))
25
+ return ui.c.yellow('-');
26
+ return ui.c.yellow('~');
27
+ }
28
+ /** `code` spans become colored text on a color terminal and stay as backticks otherwise. */
29
+ function inlineCode(ui, text) {
30
+ return ui.io.color ? text.replace(/`([^`]+)`/g, (_match, inner) => ui.c.cyan(inner)) : text;
31
+ }
32
+ //# sourceMappingURL=changes.js.map
package/dist/cli.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { type Io } from './ui.js';
2
+ /** Runs the CLI in-process and resolves to the exit code. */
3
+ export declare function run(argv: readonly string[], io?: Io): Promise<number>;
4
+ //# sourceMappingURL=cli.d.ts.map
package/dist/cli.js ADDED
@@ -0,0 +1,107 @@
1
+ import path from 'node:path';
2
+ import { Argument, Command, CommanderError } from 'commander';
3
+ import { APISTEADY_VERSION, TEMPLATE_NAMES } from '@apisteady/core';
4
+ import { changelogCommand } from './commands/changelog.js';
5
+ import { diffCommand } from './commands/diff.js';
6
+ import { ejectCommand } from './commands/eject.js';
7
+ import { generateCommand } from './commands/generate.js';
8
+ import { watchCommand } from './commands/watch.js';
9
+ import { initCommand } from './commands/init.js';
10
+ import { defaultIo, Ui } from './ui.js';
11
+ /** Runs the CLI in-process and resolves to the exit code. */
12
+ export async function run(argv, io = defaultIo()) {
13
+ let exitCode = 0;
14
+ const program = new Command('apisteady')
15
+ .description('Generate and maintain typed API client SDKs from OpenAPI specs')
16
+ .version(APISTEADY_VERSION, '-v, --version')
17
+ .option('-C, --cwd <dir>', 'run as if started in <dir>')
18
+ .option('--no-color', 'disable colored output')
19
+ .showHelpAfterError()
20
+ .exitOverride()
21
+ .configureOutput({ writeOut: io.stdout, writeErr: io.stderr });
22
+ const uiFor = (command) => {
23
+ const globals = command.optsWithGlobals();
24
+ const cwd = globals.cwd === undefined ? io.cwd : path.resolve(io.cwd, globals.cwd);
25
+ return new Ui({ ...io, cwd, color: io.color && globals.color !== false });
26
+ };
27
+ // Commander calls actions with the positional arguments, then the options, then the command.
28
+ const action = (handler) => async (...received) => {
29
+ const command = received.at(-1);
30
+ const ui = uiFor(command);
31
+ try {
32
+ exitCode = await handler(ui, received.at(-2), command.processedArgs);
33
+ }
34
+ catch (error) {
35
+ ui.error(error);
36
+ exitCode = 1;
37
+ }
38
+ };
39
+ program
40
+ .command('init')
41
+ .description('create a .apisteadyrc.yml config in the current directory')
42
+ .option('-i, --input <spec>', 'path or URL of the OpenAPI document (detected when omitted)')
43
+ .option('-o, --output <dir>', 'output directory for the SDK', './sdk')
44
+ .option('-f, --force', 'overwrite an existing config')
45
+ .action(action(initCommand));
46
+ program
47
+ .command('generate')
48
+ .alias('gen')
49
+ .description('generate the SDK, keeping your own files and existing names intact')
50
+ .option('-c, --config <file>', 'config file (default: nearest .apisteadyrc.yml)')
51
+ .option('-i, --input <spec>', 'OpenAPI document, overriding the config')
52
+ .option('-o, --output <dir>', 'output directory, overriding the config')
53
+ .option('--no-validate', 'skip OpenAPI validation')
54
+ .option('--dry-run', 'show what would change without writing')
55
+ .option('--check', 'exit 1 if the SDK on disk is out of date (for CI)')
56
+ .option('--relock', 'ignore the name lock and re-derive names from the rules')
57
+ .option('-f, --force', 'overwrite generated files even if they were edited by hand')
58
+ .option('--verbose', 'show notes and unchanged files')
59
+ .action(action(generateCommand));
60
+ program
61
+ .command('watch')
62
+ .description('generate, then regenerate whenever the spec, a file it references or the config changes')
63
+ .option('-c, --config <file>', 'config file (default: nearest .apisteadyrc.yml)')
64
+ .option('-i, --input <spec>', 'OpenAPI document, overriding the config')
65
+ .option('-o, --output <dir>', 'output directory, overriding the config')
66
+ .option('--no-validate', 'skip OpenAPI validation')
67
+ .option('--verbose', 'show notes and unchanged files')
68
+ .action(action(watchCommand));
69
+ const specArguments = (command) => command
70
+ .argument('[old]', 'spec to compare from (default: the spec the SDK was last generated from); <git-rev>:<path> reads it from git')
71
+ .argument('[new]', 'spec to compare to (default: the configured input)')
72
+ .option('-c, --config <file>', 'config file (default: nearest .apisteadyrc.yml)')
73
+ .option('-i, --input <spec>', 'use this spec instead of the configured input as [new]')
74
+ .option('-b, --base <git-rev>', 'compare against the configured spec as it is at this git revision, e.g. origin/main')
75
+ .option('--no-validate', 'skip OpenAPI validation')
76
+ .option('--json', 'print the changes as JSON');
77
+ specArguments(program.command('diff'))
78
+ .description('list API changes, marking breaking ones (default: since the last generation)')
79
+ .option('--fail-on-breaking', 'exit 1 if any change is breaking (for CI)')
80
+ .option('--require-bump', 'exit 1 if there are breaking changes but info.version is not a new major version (for CI)')
81
+ .action(action(diffCommand));
82
+ specArguments(program.command('changelog'))
83
+ .description('write the API changes as a Markdown changelog entry')
84
+ .option('--out <file>', 'write to a file instead of stdout')
85
+ .action(action(changelogCommand));
86
+ program
87
+ .command('eject')
88
+ .description('copy a built-in template into your project to customize it')
89
+ .addArgument(new Argument('<template>', 'template to copy').choices(TEMPLATE_NAMES))
90
+ .option('-c, --config <file>', 'config file (default: nearest .apisteadyrc.yml)')
91
+ .option('--out <file>', 'where to write it (default: templates.<template> from the config, or ./apisteady/<template>.ts)')
92
+ .option('--stdout', 'print it instead, e.g. to compare with your copy')
93
+ .option('-f, --force', 'replace an existing file')
94
+ .action(action(ejectCommand));
95
+ try {
96
+ await program.parseAsync([...argv], { from: 'user' });
97
+ }
98
+ catch (error) {
99
+ if (error instanceof CommanderError) {
100
+ // --help and --version also arrive here, with exit code 0.
101
+ return error.exitCode;
102
+ }
103
+ throw error;
104
+ }
105
+ return exitCode;
106
+ }
107
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,8 @@
1
+ import type { Ui } from '../ui.js';
2
+ import { type DiffCommandOptions } from './diff.js';
3
+ export interface ChangelogCommandOptions extends Pick<DiffCommandOptions, 'config' | 'input' | 'base' | 'validate' | 'json'> {
4
+ out?: string;
5
+ }
6
+ /** Markdown (or JSON) release notes for the API changes, on stdout or into a file. */
7
+ export declare function changelogCommand(ui: Ui, options: ChangelogCommandOptions, args: ReadonlyArray<string | undefined>): Promise<number>;
8
+ //# sourceMappingURL=changelog.d.ts.map
@@ -0,0 +1,20 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { renderChangelog, serializeDiff } from '@apisteady/core';
4
+ import { compareSpecs } from './diff.js';
5
+ /** Markdown (or JSON) release notes for the API changes, on stdout or into a file. */
6
+ export async function changelogCommand(ui, options, args) {
7
+ const { diff } = await compareSpecs(ui, args, options);
8
+ const content = options.json ? serializeDiff(diff) : renderChangelog(diff);
9
+ if (options.out === undefined) {
10
+ ui.io.stdout(content);
11
+ return 0;
12
+ }
13
+ const target = path.resolve(ui.io.cwd, options.out);
14
+ await mkdir(path.dirname(target), { recursive: true });
15
+ await writeFile(target, content, 'utf8');
16
+ const { breaking, nonBreaking } = diff.summary;
17
+ ui.success(`Wrote ${ui.c.bold(ui.rel(target))} (${breaking} breaking, ${nonBreaking} non-breaking)`);
18
+ return 0;
19
+ }
20
+ //# sourceMappingURL=changelog.js.map
@@ -0,0 +1,24 @@
1
+ import { type ApiDiff } from '@apisteady/core';
2
+ import type { Ui } from '../ui.js';
3
+ export interface DiffCommandOptions {
4
+ config?: string;
5
+ input?: string;
6
+ base?: string;
7
+ validate?: boolean;
8
+ json?: boolean;
9
+ failOnBreaking?: boolean;
10
+ requireBump?: boolean;
11
+ }
12
+ export interface ComparedSpecs {
13
+ diff: ApiDiff;
14
+ /** Where each side came from (file path or URL). */
15
+ from: string;
16
+ to: string;
17
+ }
18
+ /**
19
+ * Resolves the two sides of a comparison. `old` defaults to the spec the SDK was last generated from
20
+ * (`<output>/.apisteady/spec.json`), `new` to the configured input; a config is only needed for those defaults.
21
+ */
22
+ export declare function compareSpecs(ui: Ui, [oldSpec, newSpec]: ReadonlyArray<string | undefined>, options: Pick<DiffCommandOptions, 'config' | 'input' | 'base' | 'validate'>): Promise<ComparedSpecs>;
23
+ export declare function diffCommand(ui: Ui, options: DiffCommandOptions, args: ReadonlyArray<string | undefined>): Promise<number>;
24
+ //# sourceMappingURL=diff.d.ts.map
@@ -0,0 +1,98 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { applyIgnore, buildModel, checkVersionBump, isApiSteadyError, diffModels, diffTitle, loadConfig, loadSpec, loadSpecSnapshot, parseRevisionArgument, ApiSteadyError, serializeDiff, } from '@apisteady/core';
4
+ import { printChanges } from '../changes.js';
5
+ /**
6
+ * Resolves the two sides of a comparison. `old` defaults to the spec the SDK was last generated from
7
+ * (`<output>/.apisteady/spec.json`), `new` to the configured input; a config is only needed for those defaults.
8
+ */
9
+ export async function compareSpecs(ui, [oldSpec, newSpec], options) {
10
+ if (oldSpec !== undefined && options.base !== undefined) {
11
+ throw new ApiSteadyError('ARGUMENTS_INVALID', 'Pass either an [old] spec or --base, not both', {
12
+ hint: '`--base main` compares the configured spec as it is on main; `main:openapi.yaml` names a file at a revision.',
13
+ });
14
+ }
15
+ const validate = options.validate !== false;
16
+ let pending;
17
+ const config = () => (pending ??= loadConfig({
18
+ cwd: ui.io.cwd,
19
+ configPath: options.config,
20
+ overrides: {
21
+ ...(options.input !== undefined && { input: options.input }),
22
+ ...(!validate && { validate: false }),
23
+ },
24
+ }));
25
+ const from = oldSpec !== undefined
26
+ ? await loadArgument(ui, oldSpec, validate)
27
+ : options.base !== undefined
28
+ ? await atBase(ui, await config(), options.base)
29
+ : await lastGenerated(ui, await config());
30
+ const to = newSpec !== undefined
31
+ ? await loadArgument(ui, newSpec, validate)
32
+ : await config().then((resolved) => loadSpec(resolved.inputPath, { validate: resolved.validate }));
33
+ // When the comparison involves the SDK's config, compare what the SDK covers: apply its ignore list.
34
+ const ignore = pending && (await pending).ignore;
35
+ const model = (spec) => (ignore ? applyIgnore(buildModel(spec.document), ignore) : buildModel(spec.document));
36
+ const head = model(to);
37
+ const base = from === 'missing' ? { ...head, version: 'none', operations: [], webhooks: [], schemas: [], securitySchemes: [], servers: [] } : model(from);
38
+ return { diff: diffModels(base, head), from: from === 'missing' ? `${options.base} (no spec yet)` : from.source, to: to.source };
39
+ }
40
+ /** A spec argument: a file, a URL, or `<revision>:<path>` to read it from git (`main:openapi.yaml`). */
41
+ async function loadArgument(ui, argument, validate) {
42
+ const onDisk = await stat(path.resolve(ui.io.cwd, argument)).catch(() => undefined);
43
+ const atRevision = onDisk ? undefined : parseRevisionArgument(argument);
44
+ return atRevision
45
+ ? loadSpec(atRevision.file, { cwd: ui.io.cwd, validate, revision: atRevision.revision })
46
+ : loadSpec(argument, { cwd: ui.io.cwd, validate });
47
+ }
48
+ /** The configured spec at a git revision; 'missing' when it did not exist yet, so everything in it is new. */
49
+ async function atBase(ui, config, revision) {
50
+ try {
51
+ return await loadSpec(config.inputPath, { validate: config.validate, revision });
52
+ }
53
+ catch (error) {
54
+ if (!isApiSteadyError(error) || error.code !== 'SPEC_NOT_FOUND')
55
+ throw error;
56
+ ui.io.stderr(`${ui.c.dim(`${ui.rel(config.inputPath)} does not exist at ${revision}; every endpoint counts as new.`)}\n`);
57
+ return 'missing';
58
+ }
59
+ }
60
+ async function lastGenerated(ui, config) {
61
+ const snapshot = await loadSpecSnapshot(config.outputDir);
62
+ if (snapshot)
63
+ return snapshot;
64
+ throw new ApiSteadyError('BASELINE_NOT_FOUND', `No generated SDK found in ${ui.rel(config.outputDir)} to compare against`, {
65
+ hint: 'Run `apisteady generate` first, or compare two files: `apisteady diff old.yaml new.yaml`.',
66
+ });
67
+ }
68
+ export async function diffCommand(ui, options, args) {
69
+ const { diff, from, to } = await compareSpecs(ui, args, options);
70
+ if (options.json) {
71
+ ui.io.stdout(serializeDiff(diff));
72
+ }
73
+ else {
74
+ ui.line(ui.c.dim(`Comparing ${ui.rel(from)} → ${ui.rel(to)}`));
75
+ if (diff.changes.length === 0)
76
+ ui.success(`No API changes (${diffTitle(diff)})`);
77
+ else
78
+ printChanges(ui, diff, { heading: `API changes in ${diffTitle(diff)}` });
79
+ }
80
+ let exitCode = 0;
81
+ if (options.requireBump) {
82
+ const check = checkVersionBump(diff);
83
+ if (!check.ok) {
84
+ ui.fail(`${check.message} (--require-bump)`);
85
+ exitCode = 1;
86
+ }
87
+ else if (diff.summary.breaking > 0 && !options.json) {
88
+ ui.success(check.message);
89
+ }
90
+ }
91
+ if (options.failOnBreaking && diff.summary.breaking > 0) {
92
+ const { breaking } = diff.summary;
93
+ ui.fail(`${breaking} breaking change${breaking === 1 ? '' : 's'} (--fail-on-breaking)`);
94
+ exitCode = 1;
95
+ }
96
+ return exitCode;
97
+ }
98
+ //# sourceMappingURL=diff.js.map
@@ -0,0 +1,10 @@
1
+ import type { Ui } from '../ui.js';
2
+ export interface EjectCommandOptions {
3
+ config?: string;
4
+ out?: string;
5
+ stdout?: boolean;
6
+ force?: boolean;
7
+ }
8
+ /** Copies a built-in template into the project so it can be customized (`templates.<name>` in the config). */
9
+ export declare function ejectCommand(ui: Ui, options: EjectCommandOptions, [name]: ReadonlyArray<string | undefined>): Promise<number>;
10
+ //# sourceMappingURL=eject.d.ts.map
@@ -0,0 +1,43 @@
1
+ import { mkdir, stat, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { ejectTemplate, isApiSteadyError, loadConfig, ApiSteadyError } from '@apisteady/core';
4
+ /** Copies a built-in template into the project so it can be customized (`templates.<name>` in the config). */
5
+ export async function ejectCommand(ui, options, [name]) {
6
+ const template = name;
7
+ const content = await ejectTemplate(template);
8
+ if (options.stdout) {
9
+ ui.io.stdout(content);
10
+ return 0;
11
+ }
12
+ const config = await findConfig(ui, options.config);
13
+ const base = config?.rootDir ?? ui.io.cwd;
14
+ const target = options.out !== undefined ? path.resolve(ui.io.cwd, options.out) : (config?.runtimeTemplatePath ?? path.join(base, 'apisteady', `${template}.ts`));
15
+ if (!options.force && (await stat(target).catch(() => undefined))) {
16
+ throw new ApiSteadyError('FILE_EXISTS', `${ui.rel(target)} already exists`, {
17
+ hint: 'Pass --force to replace it (your changes to it are lost), or --stdout to compare it with the built-in version.',
18
+ });
19
+ }
20
+ await mkdir(path.dirname(target), { recursive: true });
21
+ await writeFile(target, content, 'utf8');
22
+ ui.success(`Copied the built-in ${template} template to ${ui.c.bold(ui.rel(target))}`);
23
+ if (config?.runtimeTemplatePath === target) {
24
+ ui.line(`apisteady uses it for generated/${template}.ts from the next run on.`);
25
+ }
26
+ else {
27
+ const relative = path.relative(base, target).split(path.sep).join('/');
28
+ ui.line(`To use it, add this to ${config?.configPath ? ui.rel(config.configPath) : 'your apisteady config'}:`);
29
+ ui.line(ui.c.cyan(` templates:\n ${template}: ${relative.startsWith('.') ? relative : `./${relative}`}`));
30
+ }
31
+ return 0;
32
+ }
33
+ async function findConfig(ui, configPath) {
34
+ try {
35
+ return await loadConfig({ cwd: ui.io.cwd, configPath });
36
+ }
37
+ catch (error) {
38
+ if (isApiSteadyError(error) && error.code === 'CONFIG_NOT_FOUND' && configPath === undefined)
39
+ return undefined;
40
+ throw error;
41
+ }
42
+ }
43
+ //# sourceMappingURL=eject.js.map
@@ -0,0 +1,23 @@
1
+ import { type GenerateResult, type ResolvedConfig } from '@apisteady/core';
2
+ import type { Ui } from '../ui.js';
3
+ export interface GenerateCommandOptions {
4
+ config?: string;
5
+ input?: string;
6
+ output?: string;
7
+ validate?: boolean;
8
+ dryRun?: boolean;
9
+ check?: boolean;
10
+ relock?: boolean;
11
+ force?: boolean;
12
+ verbose?: boolean;
13
+ }
14
+ export declare function generateCommand(ui: Ui, options: GenerateCommandOptions): Promise<number>;
15
+ /** The config with `--config`, `--input`, `--output` and `--no-validate` applied. */
16
+ export declare function loadGenerateConfig(ui: Ui, options: Pick<GenerateCommandOptions, 'config' | 'input' | 'output' | 'validate'>): Promise<ResolvedConfig>;
17
+ /** What was loaded, diagnostics, API changes, the files touched, and the outcome. */
18
+ export declare function printGeneration(ui: Ui, result: GenerateResult, options: {
19
+ dryRun: boolean;
20
+ force?: boolean | undefined;
21
+ verbose?: boolean | undefined;
22
+ }, started: number): void;
23
+ //# sourceMappingURL=generate.d.ts.map
@@ -0,0 +1,104 @@
1
+ import { describeConflict, generate, hasChanges, loadConfig, MANIFEST_FILE, } from '@apisteady/core';
2
+ import { printChanges } from '../changes.js';
3
+ const SYMBOLS = { create: '+', update: '~', delete: '-', unchanged: ' ', keep: '=' };
4
+ export async function generateCommand(ui, options) {
5
+ const started = performance.now();
6
+ const config = await loadGenerateConfig(ui, options);
7
+ if (config.configPath)
8
+ ui.line(ui.c.dim(`Using ${ui.rel(config.configPath)}`));
9
+ const dryRun = options.dryRun === true || options.check === true;
10
+ const result = await generate({ config, dryRun, relock: options.relock === true, force: options.force === true });
11
+ if (options.check) {
12
+ printSummary(ui, result, options.verbose === true);
13
+ return reportCheck(ui, result.plan);
14
+ }
15
+ printGeneration(ui, result, { ...options, dryRun }, started);
16
+ return 0;
17
+ }
18
+ /** The config with `--config`, `--input`, `--output` and `--no-validate` applied. */
19
+ export function loadGenerateConfig(ui, options) {
20
+ return loadConfig({
21
+ cwd: ui.io.cwd,
22
+ configPath: options.config,
23
+ overrides: {
24
+ ...(options.input !== undefined && { input: options.input }),
25
+ ...(options.output !== undefined && { output: options.output }),
26
+ ...(options.validate === false && { validate: false }),
27
+ },
28
+ });
29
+ }
30
+ /** What was loaded, diagnostics, API changes, the files touched, and the outcome. */
31
+ export function printGeneration(ui, result, options, started) {
32
+ const { plan } = result;
33
+ printSummary(ui, result, options.verbose === true);
34
+ printPlan(ui, plan, options.verbose === true);
35
+ const elapsed = Math.round(performance.now() - started);
36
+ if (options.dryRun) {
37
+ ui.info(`Dry run: nothing was written ${ui.c.dim(`(${elapsed} ms)`)}`);
38
+ if (plan.conflicts.length > 0 && !options.force) {
39
+ ui.warn('A real run would stop here: these files were changed outside apisteady (use --force to overwrite):');
40
+ for (const conflict of plan.conflicts)
41
+ ui.line(` ${describeConflict(conflict)}`);
42
+ }
43
+ }
44
+ else {
45
+ ui.success(`SDK written to ${ui.c.bold(ui.rel(plan.outputDir))} ${ui.c.dim(`(${elapsed} ms)`)}`);
46
+ }
47
+ }
48
+ function printSummary(ui, result, verbose) {
49
+ const { model, spec } = result;
50
+ ui.success(`Loaded ${ui.c.bold(`${model.title} ${model.version}`)} (${spec.convertedFrom ? `Swagger ${spec.convertedFrom}, read as OpenAPI 3.0` : `OpenAPI ${spec.openapiVersion}`}): ` +
51
+ `${count(model.operations.length, 'operation')}, ${count(model.schemas.length, 'schema')}`);
52
+ ui.diagnostics(result.diagnostics, verbose);
53
+ if (result.changes && result.changes.changes.length > 0) {
54
+ const { from, to } = result.changes;
55
+ printChanges(ui, result.changes, {
56
+ heading: `API changes since the last generation${from.version === to.version ? '' : ` (${from.version} → ${to.version})`}`,
57
+ limit: verbose ? undefined : 10,
58
+ moreHint: 'run with --verbose to list them all',
59
+ });
60
+ }
61
+ }
62
+ function printPlan(ui, plan, verbose) {
63
+ const tally = { create: 0, update: 0, delete: 0, unchanged: 0, keep: 0 };
64
+ for (const change of plan.changes) {
65
+ if (change.ownership === 'meta')
66
+ continue;
67
+ tally[change.action]++;
68
+ const quiet = change.action === 'unchanged' || (change.action === 'keep' && change.ownership === 'scaffold');
69
+ if (quiet && !verbose)
70
+ continue;
71
+ const color = { create: ui.c.green, update: ui.c.yellow, delete: ui.c.red, unchanged: ui.c.dim, keep: ui.c.dim }[change.action];
72
+ const note = change.action === 'keep' ? ui.c.dim(change.ownership === 'scaffold' ? ' (yours, left as is)' : ' (edited, left in place)') : '';
73
+ ui.line(` ${color(`${SYMBOLS[change.action]} ${change.path}`)}${note}`);
74
+ }
75
+ const parts = [
76
+ tally.create && `${tally.create} created`,
77
+ tally.update && `${tally.update} updated`,
78
+ tally.delete && `${tally.delete} deleted`,
79
+ tally.unchanged && `${tally.unchanged} unchanged`,
80
+ ].filter(Boolean);
81
+ ui.line(ui.c.dim(` ${parts.join(', ') || 'no files'}`));
82
+ }
83
+ /** `--check`: exit 1 when the SDK on disk is not what the current spec and config would produce. */
84
+ function reportCheck(ui, plan) {
85
+ // The manifest also records the apisteady version; upgrading apisteady alone should not fail CI.
86
+ const outdated = { ...plan, changes: plan.changes.filter((change) => change.path !== MANIFEST_FILE) };
87
+ if (!hasChanges(outdated) && plan.conflicts.length === 0) {
88
+ ui.success('SDK is up to date');
89
+ return 0;
90
+ }
91
+ ui.fail('SDK is out of date with the spec or config. Run `apisteady generate` and commit the result.');
92
+ for (const change of outdated.changes) {
93
+ if (change.action === 'create' || change.action === 'update' || change.action === 'delete') {
94
+ ui.io.stderr(` ${SYMBOLS[change.action]} ${change.path}\n`);
95
+ }
96
+ }
97
+ for (const conflict of plan.conflicts)
98
+ ui.io.stderr(` ! ${describeConflict(conflict)}\n`);
99
+ return 1;
100
+ }
101
+ function count(n, noun) {
102
+ return `${n} ${noun}${n === 1 ? '' : 's'}`;
103
+ }
104
+ //# sourceMappingURL=generate.js.map
@@ -0,0 +1,9 @@
1
+ import type { Ui } from '../ui.js';
2
+ export interface InitCommandOptions {
3
+ input?: string;
4
+ output?: string;
5
+ force?: boolean;
6
+ }
7
+ export declare const CONFIG_FILE = ".apisteadyrc.yml";
8
+ export declare function initCommand(ui: Ui, options: InitCommandOptions): Promise<number>;
9
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1,111 @@
1
+ import { readdir, stat, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { buildModel, loadSpec, parseConfig, ApiSteadyError } from '@apisteady/core';
4
+ import { parse as parseYaml } from 'yaml';
5
+ export const CONFIG_FILE = '.apisteadyrc.yml';
6
+ const SPEC_CANDIDATES = /^(openapi|swagger|api|spec)([.-][\w.-]+)?\.(ya?ml|json)$/i;
7
+ /** Where specs usually live, besides the project root. */
8
+ const SPEC_DIRECTORIES = ['api', 'apis', 'spec', 'specs', 'openapi', 'docs', 'schema'];
9
+ const EXAMPLE_CONFIG = 'https://github.com/euruuuuu/steadysdk/blob/main/.apisteadyrc.example.yml';
10
+ export async function initCommand(ui, options) {
11
+ const target = path.join(ui.io.cwd, CONFIG_FILE);
12
+ if (!options.force && (await stat(target).catch(() => undefined))) {
13
+ throw new ApiSteadyError('CONFIG_EXISTS', `${CONFIG_FILE} already exists`, { hint: 'Pass --force to overwrite it.' });
14
+ }
15
+ const candidates = options.input === undefined ? await detectSpecs(ui.io.cwd) : [];
16
+ const input = options.input ?? candidates[0];
17
+ const output = options.output ?? './sdk';
18
+ const content = configTemplate(input ?? './openapi.yaml', output);
19
+ parseConfig(parseYaml(content), 'init template'); // never write a config apisteady itself would reject
20
+ await writeFile(target, content, 'utf8');
21
+ ui.success(`Created ${ui.c.bold(CONFIG_FILE)}`);
22
+ if (input === undefined) {
23
+ ui.warn('No OpenAPI file found here; set `input` in the config to your spec.');
24
+ }
25
+ else {
26
+ const what = await describeSpec(ui, input);
27
+ if (options.input === undefined)
28
+ ui.info(`Detected spec ${ui.c.bold(input)}${what ? ` ${ui.c.dim(`(${what})`)}` : ''}`);
29
+ else if (what)
30
+ ui.info(`Spec ${ui.c.bold(input)} ${ui.c.dim(`(${what})`)}`);
31
+ if (candidates.length > 1)
32
+ ui.line(ui.c.dim(` Also found: ${candidates.slice(1).join(', ')}. Change \`input\` to use one of those.`));
33
+ }
34
+ // When run through npx, suggest commands that work the same way.
35
+ const cli = process.env['npm_command'] === 'exec' ? 'npx apisteady' : 'apisteady';
36
+ ui.line();
37
+ ui.line('Next steps:');
38
+ ui.line(` 1. Review ${CONFIG_FILE}: naming rules, grouping, parts of the API to leave out`);
39
+ ui.line(` ${ui.c.dim(`Every option: ${EXAMPLE_CONFIG}`)}`);
40
+ ui.line(` 2. Generate the SDK: ${ui.c.cyan(`${cli} generate`)}`);
41
+ ui.line(` 3. Commit ${output.replace(/^\.\//, '')}/, including its .apisteady/ folder (it keeps names stable)`);
42
+ ui.line(` 4. Keep it in sync in CI: ${ui.c.cyan(`${cli} generate --check`)}`);
43
+ ui.line();
44
+ ui.line(ui.c.dim(`While you edit the spec, ${cli} watch regenerates on every save.`));
45
+ return 0;
46
+ }
47
+ /** Spec files in the project root, then in the usual spec directories; `openapi.*` first. */
48
+ async function detectSpecs(cwd) {
49
+ const found = [];
50
+ for (const directory of ['', ...SPEC_DIRECTORIES]) {
51
+ const entries = await readdir(path.join(cwd, directory)).catch(() => []);
52
+ const matches = entries.filter((entry) => SPEC_CANDIDATES.test(entry)).sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
53
+ found.push(...matches.map((entry) => `./${directory === '' ? entry : `${directory}/${entry}`}`));
54
+ }
55
+ return found;
56
+ }
57
+ function rank(file) {
58
+ return file.toLowerCase().startsWith('openapi') ? 0 : 1;
59
+ }
60
+ /** `Petstore 1.0.0, OpenAPI 3.0.3, 11 operations`, or a warning when the file cannot be read. */
61
+ async function describeSpec(ui, input) {
62
+ try {
63
+ const spec = await loadSpec(input, { cwd: ui.io.cwd, validate: false });
64
+ const model = buildModel(spec.document);
65
+ const format = spec.convertedFrom ? `Swagger ${spec.convertedFrom}` : `OpenAPI ${spec.openapiVersion}`;
66
+ const count = model.operations.length;
67
+ return `${model.title} ${model.version}, ${format}, ${count} operation${count === 1 ? '' : 's'}`;
68
+ }
69
+ catch (error) {
70
+ ui.warn(`Could not read ${input} as an OpenAPI spec yet: ${error instanceof Error ? error.message : String(error)}`);
71
+ return undefined;
72
+ }
73
+ }
74
+ function configTemplate(input, output) {
75
+ return `# apisteady configuration. Paths are relative to this file.
76
+ # Every option is documented in .apisteadyrc.example.yml in the apisteady repository.
77
+
78
+ # OpenAPI 3.0 / 3.1 document: YAML or JSON, file path or URL.
79
+ input: ${yamlString(input)}
80
+
81
+ # Where the SDK goes. apisteady owns <output>/generated/ and .apisteady/; everything else there is yours.
82
+ output: ${yamlString(output)}
83
+
84
+ client:
85
+ # name: MyApiClient # root client class; defaults to the spec title + "Client"
86
+ groupBy: auto # auto | tag | path | none (auto: by tag if the spec uses tags, else by path)
87
+ # baseUrl: https://api.example.com
88
+
89
+ naming:
90
+ # Words kept in a fixed form, e.g. [ID, API, URL] turns userId into userID.
91
+ acronyms: []
92
+ operations:
93
+ case: camel # camel | pascal | snake | constant | preserve
94
+ source: operationId # operationId (fall back to path) | path
95
+ # stripPrefixes: ['^\\w+Controller_'] # NestJS-style operationIds
96
+ types:
97
+ case: pascal
98
+ # suffix: Dto
99
+ parameters:
100
+ case: camel
101
+ # Keep names stable across regenerations (.apisteady/names.lock.json).
102
+ lock: true
103
+ overrides:
104
+ operations: {} # "GET /users/{id}": fetchUser or listPets: getAllPets
105
+ schemas: {} # UserDTO: User
106
+ `;
107
+ }
108
+ function yamlString(value) {
109
+ return /^[\w./-]+$/.test(value) ? value : JSON.stringify(value);
110
+ }
111
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1,9 @@
1
+ import type { Ui } from '../ui.js';
2
+ import { type GenerateCommandOptions } from './generate.js';
3
+ export type WatchCommandOptions = Pick<GenerateCommandOptions, 'config' | 'input' | 'output' | 'validate' | 'verbose'>;
4
+ /**
5
+ * Generates, then regenerates whenever the spec (including files it `$ref`s), the config or the runtime
6
+ * template changes. Errors (an invalid spec mid-edit, a conflict) are reported and watching continues.
7
+ */
8
+ export declare function watchCommand(ui: Ui, options: WatchCommandOptions): Promise<number>;
9
+ //# sourceMappingURL=watch.d.ts.map
@@ -0,0 +1,98 @@
1
+ import { generate, ApiSteadyError } from '@apisteady/core';
2
+ import { FileWatcher } from '../watcher.js';
3
+ import { loadGenerateConfig, printGeneration } from './generate.js';
4
+ /** Saves arrive as bursts of file events; wait for the burst to end before regenerating. */
5
+ const SETTLE_MS = 100;
6
+ /**
7
+ * Generates, then regenerates whenever the spec (including files it `$ref`s), the config or the runtime
8
+ * template changes. Errors (an invalid spec mid-edit, a conflict) are reported and watching continues.
9
+ */
10
+ export async function watchCommand(ui, options) {
11
+ // Without a usable config there is nothing to watch, so that one is fatal.
12
+ let config = await loadGenerateConfig(ui, options);
13
+ assertWatchable(config);
14
+ if (config.configPath)
15
+ ui.line(ui.c.dim(`Using ${ui.rel(config.configPath)}`));
16
+ const interrupt = ui.io.signal ? { signal: ui.io.signal, dispose: () => { } } : onInterrupt();
17
+ let timer;
18
+ let running;
19
+ let queued;
20
+ const watcher = new FileWatcher((file) => {
21
+ queued ??= file;
22
+ clearTimeout(timer);
23
+ timer = setTimeout(() => void cycle(), SETTLE_MS);
24
+ });
25
+ const regenerate = async () => {
26
+ const started = performance.now();
27
+ const files = new Set([config.inputPath, config.configPath, config.runtimeTemplatePath].filter((file) => file !== undefined));
28
+ try {
29
+ const result = await generate({ config });
30
+ for (const file of result.spec.files)
31
+ files.add(file);
32
+ printGeneration(ui, result, { dryRun: false, verbose: options.verbose }, started);
33
+ }
34
+ catch (error) {
35
+ ui.error(error);
36
+ }
37
+ watcher.set(files);
38
+ };
39
+ const cycle = async () => {
40
+ if (running)
41
+ return; // the running cycle picks up `queued` when it finishes
42
+ const file = queued;
43
+ queued = undefined;
44
+ if (file === undefined || interrupt.signal.aborted)
45
+ return;
46
+ running = (async () => {
47
+ ui.line();
48
+ ui.line(ui.c.dim(`[${new Date().toTimeString().slice(0, 8)}] ${ui.rel(file)} changed`));
49
+ try {
50
+ config = await loadGenerateConfig(ui, options);
51
+ assertWatchable(config);
52
+ await regenerate();
53
+ }
54
+ catch (error) {
55
+ ui.error(error); // e.g. the config is mid-edit: keep watching what we had
56
+ }
57
+ })();
58
+ await running;
59
+ running = undefined;
60
+ if (queued !== undefined)
61
+ await cycle();
62
+ };
63
+ await regenerate();
64
+ ui.info(`Watching ${watcher.size} file${watcher.size === 1 ? '' : 's'} for changes. Press Ctrl+C to stop.`);
65
+ await new Promise((resolve) => {
66
+ if (interrupt.signal.aborted)
67
+ resolve();
68
+ else
69
+ interrupt.signal.addEventListener('abort', () => resolve(), { once: true });
70
+ });
71
+ clearTimeout(timer);
72
+ watcher.close();
73
+ interrupt.dispose();
74
+ await running;
75
+ ui.line(ui.c.dim('Stopped watching.'));
76
+ return 0;
77
+ }
78
+ function assertWatchable(config) {
79
+ if (/^https?:\/\//i.test(config.inputPath)) {
80
+ throw new ApiSteadyError('WATCH_UNSUPPORTED', `Cannot watch ${config.inputPath}: it is a URL`, {
81
+ hint: 'Watch mode needs the spec on disk. Download it, or run `apisteady generate` when it changes.',
82
+ });
83
+ }
84
+ }
85
+ function onInterrupt() {
86
+ const controller = new AbortController();
87
+ const stop = () => controller.abort();
88
+ process.once('SIGINT', stop);
89
+ process.once('SIGTERM', stop);
90
+ return {
91
+ signal: controller.signal,
92
+ dispose: () => {
93
+ process.off('SIGINT', stop);
94
+ process.off('SIGTERM', stop);
95
+ },
96
+ };
97
+ }
98
+ //# sourceMappingURL=watch.js.map
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { run } from './cli.js';
3
+ process.exitCode = await run(process.argv.slice(2));
4
+ //# sourceMappingURL=index.js.map
package/dist/ui.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import pc from 'picocolors';
2
+ import { type Diagnostic } from '@apisteady/core';
3
+ export interface Io {
4
+ stdout: (text: string) => void;
5
+ stderr: (text: string) => void;
6
+ cwd: string;
7
+ /** Colors are on for TTYs unless NO_COLOR is set; tests pass false. */
8
+ color: boolean;
9
+ /** Stops long-running commands (`watch`). Without it they stop on Ctrl+C. */
10
+ signal?: AbortSignal | undefined;
11
+ }
12
+ export declare function defaultIo(): Io;
13
+ /** Small presentation layer so commands read as a sequence of steps. */
14
+ export declare class Ui {
15
+ readonly io: Io;
16
+ private readonly colors;
17
+ constructor(io: Io);
18
+ get c(): ReturnType<typeof pc.createColors>;
19
+ line(text?: string): void;
20
+ success(text: string): void;
21
+ warn(text: string): void;
22
+ info(text: string): void;
23
+ fail(text: string): void;
24
+ /** A path relative to the working directory, for display. */
25
+ rel(target: string): string;
26
+ diagnostics(items: readonly Diagnostic[], verbose: boolean): void;
27
+ error(error: unknown): void;
28
+ private formatDiagnostic;
29
+ }
30
+ //# sourceMappingURL=ui.d.ts.map
package/dist/ui.js ADDED
@@ -0,0 +1,75 @@
1
+ import path from 'node:path';
2
+ import pc from 'picocolors';
3
+ import { isApiSteadyError } from '@apisteady/core';
4
+ export function defaultIo() {
5
+ return {
6
+ stdout: (text) => process.stdout.write(text),
7
+ stderr: (text) => process.stderr.write(text),
8
+ cwd: process.cwd(),
9
+ color: pc.isColorSupported,
10
+ };
11
+ }
12
+ /** Small presentation layer so commands read as a sequence of steps. */
13
+ export class Ui {
14
+ io;
15
+ colors;
16
+ constructor(io) {
17
+ this.io = io;
18
+ this.colors = pc.createColors(io.color);
19
+ }
20
+ get c() {
21
+ return this.colors;
22
+ }
23
+ line(text = '') {
24
+ this.io.stdout(`${text}\n`);
25
+ }
26
+ success(text) {
27
+ this.line(`${this.colors.green('✔')} ${text}`);
28
+ }
29
+ warn(text) {
30
+ this.line(`${this.colors.yellow('⚠')} ${text}`);
31
+ }
32
+ info(text) {
33
+ this.line(`${this.colors.cyan('ℹ')} ${text}`);
34
+ }
35
+ fail(text) {
36
+ this.io.stderr(`${this.colors.red('✖')} ${text}\n`);
37
+ }
38
+ /** A path relative to the working directory, for display. */
39
+ rel(target) {
40
+ if (/^https?:\/\//i.test(target))
41
+ return target;
42
+ const relative = path.relative(this.io.cwd, target);
43
+ return relative === '' ? '.' : relative.startsWith('..') ? target : relative;
44
+ }
45
+ diagnostics(items, verbose) {
46
+ const warnings = items.filter((item) => item.level === 'warning');
47
+ const notes = items.filter((item) => item.level === 'info');
48
+ for (const item of warnings)
49
+ this.warn(this.formatDiagnostic(item));
50
+ if (verbose) {
51
+ for (const item of notes)
52
+ this.info(this.formatDiagnostic(item));
53
+ }
54
+ else if (notes.length > 0) {
55
+ this.line(this.colors.dim(` ${notes.length} note${notes.length === 1 ? '' : 's'} hidden (run with --verbose to see them)`));
56
+ }
57
+ }
58
+ error(error) {
59
+ if (isApiSteadyError(error)) {
60
+ this.fail(error.message);
61
+ for (const detail of error.details)
62
+ this.io.stderr(` ${this.colors.dim('•')} ${detail}\n`);
63
+ if (error.hint)
64
+ this.io.stderr(` ${this.colors.cyan('Hint:')} ${error.hint}\n`);
65
+ return;
66
+ }
67
+ this.fail('Unexpected error — this is a bug in apisteady, please report it.');
68
+ this.io.stderr(`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
69
+ }
70
+ formatDiagnostic(item) {
71
+ const where = item.location ? this.colors.dim(` (${item.location})`) : '';
72
+ return `${item.message}${where}`;
73
+ }
74
+ }
75
+ //# sourceMappingURL=ui.js.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Watches a changing set of files. It watches their directories rather than the files themselves:
3
+ * many editors save by writing a new file and renaming it over the old one, which a file watch misses.
4
+ */
5
+ export declare class FileWatcher {
6
+ private readonly onChange;
7
+ private readonly watchers;
8
+ private files;
9
+ constructor(onChange: (file: string) => void);
10
+ get size(): number;
11
+ /** Replaces the watched files. Directories that cannot be watched (yet) are retried on the next call. */
12
+ set(files: Iterable<string>): void;
13
+ close(): void;
14
+ }
15
+ //# sourceMappingURL=watcher.d.ts.map
@@ -0,0 +1,51 @@
1
+ import { watch } from 'node:fs';
2
+ import path from 'node:path';
3
+ /**
4
+ * Watches a changing set of files. It watches their directories rather than the files themselves:
5
+ * many editors save by writing a new file and renaming it over the old one, which a file watch misses.
6
+ */
7
+ export class FileWatcher {
8
+ onChange;
9
+ watchers = new Map();
10
+ files = new Set();
11
+ constructor(onChange) {
12
+ this.onChange = onChange;
13
+ }
14
+ get size() {
15
+ return this.files.size;
16
+ }
17
+ /** Replaces the watched files. Directories that cannot be watched (yet) are retried on the next call. */
18
+ set(files) {
19
+ this.files = new Set([...files].map((file) => path.resolve(file)));
20
+ const directories = new Set([...this.files].map((file) => path.dirname(file)));
21
+ for (const [directory, watcher] of this.watchers) {
22
+ if (directories.has(directory))
23
+ continue;
24
+ watcher.close();
25
+ this.watchers.delete(directory);
26
+ }
27
+ for (const directory of directories) {
28
+ if (this.watchers.has(directory))
29
+ continue;
30
+ try {
31
+ const watcher = watch(directory, (_event, name) => {
32
+ // Some platforms omit the name; then any change in the directory counts.
33
+ const file = name === null ? undefined : path.join(directory, name.toString());
34
+ if (file === undefined || this.files.has(file))
35
+ this.onChange(file ?? directory);
36
+ });
37
+ watcher.on('error', () => this.watchers.delete(directory));
38
+ this.watchers.set(directory, watcher);
39
+ }
40
+ catch {
41
+ // The directory does not exist right now.
42
+ }
43
+ }
44
+ }
45
+ close() {
46
+ for (const watcher of this.watchers.values())
47
+ watcher.close();
48
+ this.watchers.clear();
49
+ }
50
+ }
51
+ //# sourceMappingURL=watcher.js.map
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "apisteady",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Generate and maintain typed API client SDKs from OpenAPI specs",
5
+ "keywords": [
6
+ "openapi",
7
+ "swagger",
8
+ "sdk",
9
+ "sdk-generator",
10
+ "codegen",
11
+ "typescript",
12
+ "api-client",
13
+ "cli",
14
+ "changelog",
15
+ "breaking-changes"
16
+ ],
17
+ "homepage": "https://github.com/euruuuuu/steadysdk#readme",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/euruuuuu/steadysdk.git",
21
+ "directory": "packages/cli"
22
+ },
23
+ "bugs": "https://github.com/euruuuuu/steadysdk/issues",
24
+ "license": "MIT",
25
+ "type": "module",
26
+ "bin": {
27
+ "apisteady": "dist/index.js"
28
+ },
29
+ "exports": {
30
+ "./package.json": "./package.json"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "!dist/**/*.map"
35
+ ],
36
+ "engines": {
37
+ "node": ">=22.19.0"
38
+ },
39
+ "scripts": {
40
+ "build": "tsc -b",
41
+ "prepack": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -b --force"
42
+ },
43
+ "dependencies": {
44
+ "@apisteady/core": "^0.1.0",
45
+ "commander": "^15.0.0",
46
+ "picocolors": "^1.1.1",
47
+ "yaml": "^2.9.1"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ }
52
+ }