chowbea-axios 1.0.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 +22 -0
- package/README.md +162 -0
- package/bin/dev.js +13 -0
- package/bin/run.js +10 -0
- package/dist/commands/diff.d.ts +31 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +215 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/fetch.d.ts +28 -0
- package/dist/commands/fetch.d.ts.map +1 -0
- package/dist/commands/fetch.js +223 -0
- package/dist/commands/fetch.js.map +1 -0
- package/dist/commands/generate.d.ts +26 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +187 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/init.d.ts +92 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +738 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/status.d.ts +38 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +233 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/validate.d.ts +27 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +209 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/commands/watch.d.ts +34 -0
- package/dist/commands/watch.d.ts.map +1 -0
- package/dist/commands/watch.js +202 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/config.d.ts +151 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +336 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/errors.d.ts +77 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +144 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/fetcher.d.ts +115 -0
- package/dist/lib/fetcher.d.ts.map +1 -0
- package/dist/lib/fetcher.js +237 -0
- package/dist/lib/fetcher.js.map +1 -0
- package/dist/lib/generator.d.ts +96 -0
- package/dist/lib/generator.d.ts.map +1 -0
- package/dist/lib/generator.js +1575 -0
- package/dist/lib/generator.js.map +1 -0
- package/dist/lib/logger.d.ts +63 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +183 -0
- package/dist/lib/logger.js.map +1 -0
- package/oclif.manifest.json +556 -0
- package/package.json +68 -0
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init command - full project setup for chowbea-axios.
|
|
3
|
+
* Creates config, adds workspace entry, adds npm scripts, generates client files,
|
|
4
|
+
* installs dependencies, builds CLI, and runs initial fetch.
|
|
5
|
+
*/
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { access, readFile, writeFile } from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { checkbox, confirm, input } from "@inquirer/prompts";
|
|
10
|
+
import { Command, Flags } from "@oclif/core";
|
|
11
|
+
import { configExists, DEFAULT_INSTANCE_CONFIG, ensureOutputFolders, findProjectRoot, getConfigPath, getOutputPaths, loadConfig, } from "../lib/config.js";
|
|
12
|
+
import { formatError } from "../lib/errors.js";
|
|
13
|
+
import { generateClientFiles } from "../lib/generator.js";
|
|
14
|
+
import { createLogger, getLogLevel, logSeparator } from "../lib/logger.js";
|
|
15
|
+
/**
|
|
16
|
+
* Default npm scripts to add to package.json
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_SCRIPTS = {
|
|
19
|
+
"api:generate": "node cli/chowbea-axios/bin/run.js generate",
|
|
20
|
+
"api:fetch": "node cli/chowbea-axios/bin/run.js fetch",
|
|
21
|
+
"api:watch": "node cli/chowbea-axios/bin/run.js watch",
|
|
22
|
+
"api:init": "node cli/chowbea-axios/bin/run.js init",
|
|
23
|
+
"api:status": "node cli/chowbea-axios/bin/run.js status",
|
|
24
|
+
"api:validate": "node cli/chowbea-axios/bin/run.js validate",
|
|
25
|
+
"api:diff": "node cli/chowbea-axios/bin/run.js diff",
|
|
26
|
+
"api:help": "node cli/chowbea-axios/bin/run.js --help",
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Default pnpm-workspace.yaml content
|
|
30
|
+
*/
|
|
31
|
+
const DEFAULT_WORKSPACE_YAML = `# pnpm workspace configuration
|
|
32
|
+
# Includes the CLI tools as workspace packages
|
|
33
|
+
packages:
|
|
34
|
+
- "cli/*"
|
|
35
|
+
`;
|
|
36
|
+
/**
|
|
37
|
+
* Initialize chowbea-axios in a project.
|
|
38
|
+
* Creates config, workspace file, and npm scripts.
|
|
39
|
+
*/
|
|
40
|
+
export default class Init extends Command {
|
|
41
|
+
static description = `Full project setup - one command to get started.
|
|
42
|
+
|
|
43
|
+
Prompts for your API endpoint, then automatically:
|
|
44
|
+
- Creates api.config.toml with your settings
|
|
45
|
+
- Sets up pnpm workspace for the CLI
|
|
46
|
+
- Adds npm scripts (api:fetch, api:generate, etc.)
|
|
47
|
+
- Installs openapi-typescript dependency
|
|
48
|
+
- Builds the CLI
|
|
49
|
+
- Generates client files (api.instance.ts, api.client.ts, etc.)
|
|
50
|
+
- Fetches spec and generates types (if not localhost)
|
|
51
|
+
|
|
52
|
+
Detects existing setup and asks before overwriting.`;
|
|
53
|
+
static examples = [
|
|
54
|
+
{
|
|
55
|
+
command: "<%= config.bin %> init",
|
|
56
|
+
description: "Interactive setup - prompts for endpoint, does everything",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
command: "<%= config.bin %> init --force",
|
|
60
|
+
description: "Skip confirmations, overwrite existing files",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
command: "<%= config.bin %> init --skip-client",
|
|
64
|
+
description: "Setup without generating client files",
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
static flags = {
|
|
68
|
+
force: Flags.boolean({
|
|
69
|
+
char: "f",
|
|
70
|
+
description: "Skip all confirmations and overwrite everything",
|
|
71
|
+
default: false,
|
|
72
|
+
}),
|
|
73
|
+
"skip-scripts": Flags.boolean({
|
|
74
|
+
description: "Skip adding npm scripts to package.json",
|
|
75
|
+
default: false,
|
|
76
|
+
}),
|
|
77
|
+
"skip-workspace": Flags.boolean({
|
|
78
|
+
description: "Skip creating/updating pnpm-workspace.yaml",
|
|
79
|
+
default: false,
|
|
80
|
+
}),
|
|
81
|
+
"skip-client": Flags.boolean({
|
|
82
|
+
description: "Skip generating client files (api.instance.ts, api.error.ts, api.client.ts)",
|
|
83
|
+
default: false,
|
|
84
|
+
}),
|
|
85
|
+
"skip-concurrent": Flags.boolean({
|
|
86
|
+
description: "Skip setting up concurrent dev script",
|
|
87
|
+
default: false,
|
|
88
|
+
}),
|
|
89
|
+
"base-url-env": Flags.string({
|
|
90
|
+
description: "Environment variable name for base URL",
|
|
91
|
+
default: DEFAULT_INSTANCE_CONFIG.base_url_env,
|
|
92
|
+
}),
|
|
93
|
+
"token-key": Flags.string({
|
|
94
|
+
description: "localStorage key for auth token",
|
|
95
|
+
default: DEFAULT_INSTANCE_CONFIG.token_key,
|
|
96
|
+
}),
|
|
97
|
+
"with-credentials": Flags.boolean({
|
|
98
|
+
description: "Include credentials (cookies) in requests",
|
|
99
|
+
default: DEFAULT_INSTANCE_CONFIG.with_credentials,
|
|
100
|
+
allowNo: true,
|
|
101
|
+
}),
|
|
102
|
+
timeout: Flags.integer({
|
|
103
|
+
description: "Request timeout in milliseconds",
|
|
104
|
+
default: DEFAULT_INSTANCE_CONFIG.timeout,
|
|
105
|
+
}),
|
|
106
|
+
quiet: Flags.boolean({
|
|
107
|
+
char: "q",
|
|
108
|
+
description: "Suppress non-error output",
|
|
109
|
+
default: false,
|
|
110
|
+
}),
|
|
111
|
+
verbose: Flags.boolean({
|
|
112
|
+
char: "v",
|
|
113
|
+
description: "Show detailed output",
|
|
114
|
+
default: false,
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
async run() {
|
|
118
|
+
const { flags } = await this.parse(Init);
|
|
119
|
+
// Create logger with appropriate level
|
|
120
|
+
const logger = createLogger({
|
|
121
|
+
level: getLogLevel(flags),
|
|
122
|
+
});
|
|
123
|
+
logSeparator(logger, "chowbea-axios init");
|
|
124
|
+
try {
|
|
125
|
+
// Find project root
|
|
126
|
+
const projectRoot = await findProjectRoot();
|
|
127
|
+
logger.info({ projectRoot }, "Found project root");
|
|
128
|
+
// Check for existing setup and notify user
|
|
129
|
+
const existingSetup = await this.detectExistingSetup(projectRoot);
|
|
130
|
+
if (existingSetup.length > 0 && !flags.force) {
|
|
131
|
+
logger.warn("Existing setup detected:");
|
|
132
|
+
for (const item of existingSetup) {
|
|
133
|
+
logger.warn(` - ${item}`);
|
|
134
|
+
}
|
|
135
|
+
const shouldContinue = await confirm({
|
|
136
|
+
message: "Continue with setup? (existing files may be modified)",
|
|
137
|
+
default: true,
|
|
138
|
+
});
|
|
139
|
+
if (!shouldContinue) {
|
|
140
|
+
logger.info("Setup cancelled");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Prompt for API endpoint URL
|
|
145
|
+
const apiEndpoint = await input({
|
|
146
|
+
message: "Enter your OpenAPI spec endpoint URL:",
|
|
147
|
+
default: "http://localhost:3000/docs/swagger/json",
|
|
148
|
+
});
|
|
149
|
+
// Build instance config from flags
|
|
150
|
+
const instanceConfig = {
|
|
151
|
+
base_url_env: flags["base-url-env"],
|
|
152
|
+
token_key: flags["token-key"],
|
|
153
|
+
with_credentials: flags["with-credentials"],
|
|
154
|
+
timeout: flags.timeout,
|
|
155
|
+
};
|
|
156
|
+
// Step 1: Create api.config.toml
|
|
157
|
+
await this.setupConfig(projectRoot, flags, instanceConfig, apiEndpoint, logger);
|
|
158
|
+
// Step 2: Create/update pnpm-workspace.yaml
|
|
159
|
+
if (!flags["skip-workspace"]) {
|
|
160
|
+
await this.setupWorkspace(projectRoot, flags, logger);
|
|
161
|
+
}
|
|
162
|
+
// Step 3: Add npm scripts to package.json
|
|
163
|
+
if (!flags["skip-scripts"]) {
|
|
164
|
+
await this.setupScripts(projectRoot, flags, logger);
|
|
165
|
+
}
|
|
166
|
+
// Step 3.5: Setup concurrent dev script (optional)
|
|
167
|
+
if (!flags["skip-concurrent"]) {
|
|
168
|
+
await this.setupConcurrentlyScript(projectRoot, logger);
|
|
169
|
+
}
|
|
170
|
+
// Step 4: Check and install openapi-typescript
|
|
171
|
+
await this.ensureOpenApiTypescript(projectRoot, logger);
|
|
172
|
+
// Step 5: Run pnpm install
|
|
173
|
+
await this.runPnpmInstall(projectRoot, logger);
|
|
174
|
+
// Step 6: Build the CLI
|
|
175
|
+
await this.buildCli(projectRoot, logger);
|
|
176
|
+
// Step 7: Generate client files
|
|
177
|
+
if (!flags["skip-client"]) {
|
|
178
|
+
await this.setupClientFiles(projectRoot, instanceConfig, flags, logger);
|
|
179
|
+
}
|
|
180
|
+
// Step 8: Run initial fetch (if not localhost default)
|
|
181
|
+
const isLocalhost = apiEndpoint.includes("localhost") || apiEndpoint.includes("127.0.0.1");
|
|
182
|
+
if (!isLocalhost) {
|
|
183
|
+
await this.runInitialFetch(projectRoot, logger);
|
|
184
|
+
}
|
|
185
|
+
// Summary
|
|
186
|
+
logSeparator(logger, "Setup Complete");
|
|
187
|
+
logger.info("");
|
|
188
|
+
if (isLocalhost) {
|
|
189
|
+
logger.info("Setup complete! When your API server is running:");
|
|
190
|
+
logger.info(" pnpm api:fetch # Fetch spec and generate types");
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
logger.info("Setup complete! Your API client is ready to use.");
|
|
194
|
+
logger.info("");
|
|
195
|
+
logger.info("Useful commands:");
|
|
196
|
+
logger.info(" pnpm api:status # Check current status");
|
|
197
|
+
logger.info(" pnpm api:fetch # Re-fetch spec and regenerate");
|
|
198
|
+
logger.info(" pnpm api:watch # Watch for spec changes");
|
|
199
|
+
}
|
|
200
|
+
logger.info("");
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
logger.error(formatError(error));
|
|
204
|
+
this.exit(1);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Detects existing setup files to notify user.
|
|
209
|
+
* Does NOT auto-create any files - only checks what exists.
|
|
210
|
+
*/
|
|
211
|
+
async detectExistingSetup(projectRoot) {
|
|
212
|
+
const found = [];
|
|
213
|
+
// Check for config
|
|
214
|
+
const configPath = getConfigPath(projectRoot);
|
|
215
|
+
const hasConfig = await configExists(configPath);
|
|
216
|
+
if (hasConfig) {
|
|
217
|
+
found.push("api.config.toml");
|
|
218
|
+
}
|
|
219
|
+
// Check for workspace
|
|
220
|
+
try {
|
|
221
|
+
await access(path.join(projectRoot, "pnpm-workspace.yaml"));
|
|
222
|
+
found.push("pnpm-workspace.yaml");
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Not found
|
|
226
|
+
}
|
|
227
|
+
// Only check for client files if config exists (to avoid auto-creating config)
|
|
228
|
+
if (hasConfig) {
|
|
229
|
+
try {
|
|
230
|
+
const { config } = await loadConfig();
|
|
231
|
+
const outputPaths = getOutputPaths(config, projectRoot);
|
|
232
|
+
try {
|
|
233
|
+
await access(outputPaths.instance);
|
|
234
|
+
found.push("api.instance.ts");
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
/* Not found */
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
await access(outputPaths.client);
|
|
241
|
+
found.push("api.client.ts");
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
/* Not found */
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// Config parsing failed
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return found;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Ensures openapi-typescript is installed as a dev dependency.
|
|
255
|
+
*/
|
|
256
|
+
async ensureOpenApiTypescript(projectRoot, logger) {
|
|
257
|
+
logger.info("Checking for openapi-typescript...");
|
|
258
|
+
// Check if already installed
|
|
259
|
+
const packageJsonPath = path.join(projectRoot, "package.json");
|
|
260
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
261
|
+
const deps = packageJson.dependencies || {};
|
|
262
|
+
const devDeps = packageJson.devDependencies || {};
|
|
263
|
+
if (deps["openapi-typescript"] || devDeps["openapi-typescript"]) {
|
|
264
|
+
logger.info("openapi-typescript already installed");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
// Install it
|
|
268
|
+
logger.info("Installing openapi-typescript...");
|
|
269
|
+
const result = spawnSync("pnpm", ["add", "-D", "openapi-typescript"], {
|
|
270
|
+
cwd: projectRoot,
|
|
271
|
+
stdio: "inherit",
|
|
272
|
+
});
|
|
273
|
+
if (result.status !== 0) {
|
|
274
|
+
throw new Error("Failed to install openapi-typescript");
|
|
275
|
+
}
|
|
276
|
+
logger.info("openapi-typescript installed");
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Runs pnpm install to link workspaces.
|
|
280
|
+
*/
|
|
281
|
+
async runPnpmInstall(projectRoot, logger) {
|
|
282
|
+
logger.info("Running pnpm install...");
|
|
283
|
+
const result = spawnSync("pnpm", ["install"], {
|
|
284
|
+
cwd: projectRoot,
|
|
285
|
+
stdio: "inherit",
|
|
286
|
+
});
|
|
287
|
+
if (result.status !== 0) {
|
|
288
|
+
throw new Error("pnpm install failed");
|
|
289
|
+
}
|
|
290
|
+
logger.info("Dependencies installed");
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Builds the CLI.
|
|
294
|
+
*/
|
|
295
|
+
async buildCli(projectRoot, logger) {
|
|
296
|
+
logger.info("Building CLI...");
|
|
297
|
+
const result = spawnSync("pnpm", ["--filter", "chowbea-axios", "build"], {
|
|
298
|
+
cwd: projectRoot,
|
|
299
|
+
stdio: "inherit",
|
|
300
|
+
});
|
|
301
|
+
if (result.status !== 0) {
|
|
302
|
+
throw new Error("CLI build failed");
|
|
303
|
+
}
|
|
304
|
+
logger.info("CLI built successfully");
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Runs initial fetch to download spec and generate types.
|
|
308
|
+
*/
|
|
309
|
+
async runInitialFetch(projectRoot, logger) {
|
|
310
|
+
logger.info("Fetching OpenAPI spec and generating types...");
|
|
311
|
+
const result = spawnSync("node", ["cli/chowbea-axios/bin/run.js", "fetch"], {
|
|
312
|
+
cwd: projectRoot,
|
|
313
|
+
stdio: "inherit",
|
|
314
|
+
});
|
|
315
|
+
if (result.status !== 0) {
|
|
316
|
+
logger.warn("Initial fetch failed - you can run 'pnpm api:fetch' later");
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
logger.info("Types generated successfully");
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Creates api.config.toml if it doesn't exist or if user confirms overwrite.
|
|
324
|
+
*/
|
|
325
|
+
async setupConfig(projectRoot, flags, instanceConfig, apiEndpoint, logger) {
|
|
326
|
+
const configPath = getConfigPath(projectRoot);
|
|
327
|
+
const exists = await configExists(configPath);
|
|
328
|
+
logger.info("Setting up api.config.toml...");
|
|
329
|
+
if (exists) {
|
|
330
|
+
if (flags.force) {
|
|
331
|
+
logger.info("Overwriting existing config (--force)");
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
const shouldOverwrite = await confirm({
|
|
335
|
+
message: "api.config.toml already exists. Overwrite?",
|
|
336
|
+
default: false,
|
|
337
|
+
});
|
|
338
|
+
if (!shouldOverwrite) {
|
|
339
|
+
logger.info("Skipping config creation");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// Generate config with instance settings and endpoint
|
|
345
|
+
const configContent = this.generateConfigContent(instanceConfig, apiEndpoint);
|
|
346
|
+
await writeFile(configPath, configContent, "utf8");
|
|
347
|
+
logger.info({ path: configPath }, "Created api.config.toml");
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Generates api.config.toml content with instance settings.
|
|
351
|
+
*/
|
|
352
|
+
generateConfigContent(instanceConfig, apiEndpoint) {
|
|
353
|
+
return `# Chowbea Axios API Configuration
|
|
354
|
+
# Run 'chowbea-axios init' to regenerate with prompts
|
|
355
|
+
|
|
356
|
+
# Remote OpenAPI specification endpoint
|
|
357
|
+
api_endpoint = "${apiEndpoint}"
|
|
358
|
+
|
|
359
|
+
# Polling interval for watch mode (milliseconds)
|
|
360
|
+
poll_interval_ms = 10000
|
|
361
|
+
|
|
362
|
+
[output]
|
|
363
|
+
# Folder where all generated files are written
|
|
364
|
+
# Structure:
|
|
365
|
+
# _internal/ - cache files (openapi.json, .api-cache.json)
|
|
366
|
+
# _generated/ - generated code (api.types.ts, api.operations.ts)
|
|
367
|
+
# api.instance.ts - axios instance (generated once, editable)
|
|
368
|
+
# api.error.ts - error handling (generated once, editable)
|
|
369
|
+
# api.client.ts - typed API facade (generated once, editable)
|
|
370
|
+
folder = "app/services/api"
|
|
371
|
+
|
|
372
|
+
[instance]
|
|
373
|
+
# Environment variable name for base URL
|
|
374
|
+
base_url_env = "${instanceConfig.base_url_env}"
|
|
375
|
+
|
|
376
|
+
# localStorage key for auth token
|
|
377
|
+
token_key = "${instanceConfig.token_key}"
|
|
378
|
+
|
|
379
|
+
# Include credentials (cookies) in requests
|
|
380
|
+
with_credentials = ${instanceConfig.with_credentials}
|
|
381
|
+
|
|
382
|
+
# Request timeout in milliseconds
|
|
383
|
+
timeout = ${instanceConfig.timeout}
|
|
384
|
+
`;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Creates or updates pnpm-workspace.yaml to include cli/*.
|
|
388
|
+
*/
|
|
389
|
+
async setupWorkspace(projectRoot, flags, logger) {
|
|
390
|
+
const workspacePath = path.join(projectRoot, "pnpm-workspace.yaml");
|
|
391
|
+
logger.info("Setting up pnpm-workspace.yaml...");
|
|
392
|
+
// Check if workspace file exists
|
|
393
|
+
let existingContent = null;
|
|
394
|
+
try {
|
|
395
|
+
await access(workspacePath);
|
|
396
|
+
existingContent = await readFile(workspacePath, "utf8");
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
// File doesn't exist
|
|
400
|
+
}
|
|
401
|
+
// Check if cli/* is already included
|
|
402
|
+
if (existingContent && existingContent.includes("cli/*")) {
|
|
403
|
+
logger.info("pnpm-workspace.yaml already includes cli/*");
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (existingContent) {
|
|
407
|
+
// File exists but doesn't have cli/*
|
|
408
|
+
if (!flags.force) {
|
|
409
|
+
const shouldModify = await confirm({
|
|
410
|
+
message: "pnpm-workspace.yaml exists but doesn't include cli/*. Add it?",
|
|
411
|
+
default: true,
|
|
412
|
+
});
|
|
413
|
+
if (!shouldModify) {
|
|
414
|
+
logger.warn("Skipping workspace setup - you may need to add cli/* manually");
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// Append cli/* to existing packages
|
|
419
|
+
const updatedContent = this.addCliToWorkspace(existingContent);
|
|
420
|
+
await writeFile(workspacePath, updatedContent, "utf8");
|
|
421
|
+
logger.info("Updated pnpm-workspace.yaml to include cli/*");
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
// Create new workspace file
|
|
425
|
+
await writeFile(workspacePath, DEFAULT_WORKSPACE_YAML, "utf8");
|
|
426
|
+
logger.info({ path: workspacePath }, "Created pnpm-workspace.yaml");
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Adds cli/* to an existing pnpm-workspace.yaml content.
|
|
431
|
+
* Handles various YAML formats including comments and different indentation.
|
|
432
|
+
*/
|
|
433
|
+
addCliToWorkspace(content) {
|
|
434
|
+
const lines = content.split("\n");
|
|
435
|
+
const result = [];
|
|
436
|
+
let packagesLineIndex = -1;
|
|
437
|
+
let firstPackageEntryIndex = -1;
|
|
438
|
+
let detectedIndent = " "; // Default indentation
|
|
439
|
+
// First pass: find the packages section and first entry
|
|
440
|
+
for (let i = 0; i < lines.length; i++) {
|
|
441
|
+
const line = lines[i];
|
|
442
|
+
const trimmed = line.trim();
|
|
443
|
+
// Find packages: line (ignoring comments)
|
|
444
|
+
if (trimmed === "packages:" || trimmed.startsWith("packages:")) {
|
|
445
|
+
packagesLineIndex = i;
|
|
446
|
+
}
|
|
447
|
+
// Find first package entry after packages: line
|
|
448
|
+
if (packagesLineIndex !== -1 &&
|
|
449
|
+
firstPackageEntryIndex === -1 &&
|
|
450
|
+
trimmed.startsWith("-") &&
|
|
451
|
+
!trimmed.startsWith("#")) {
|
|
452
|
+
firstPackageEntryIndex = i;
|
|
453
|
+
// Detect indentation from first entry
|
|
454
|
+
const leadingSpaces = line.match(/^(\s*)/);
|
|
455
|
+
if (leadingSpaces && leadingSpaces[1]) {
|
|
456
|
+
detectedIndent = leadingSpaces[1];
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// Build result with cli/* added in the right place
|
|
461
|
+
for (let i = 0; i < lines.length; i++) {
|
|
462
|
+
result.push(lines[i]);
|
|
463
|
+
// Add cli/* after the first package entry
|
|
464
|
+
if (i === firstPackageEntryIndex) {
|
|
465
|
+
result.push(`${detectedIndent}- "cli/*"`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// If no packages section found, create one
|
|
469
|
+
if (packagesLineIndex === -1) {
|
|
470
|
+
result.push("");
|
|
471
|
+
result.push("packages:");
|
|
472
|
+
result.push(' - "cli/*"');
|
|
473
|
+
}
|
|
474
|
+
else if (firstPackageEntryIndex === -1) {
|
|
475
|
+
// packages: exists but no entries - add cli/* right after it
|
|
476
|
+
const insertIndex = result.findIndex((line) => line.trim() === "packages:" || line.trim().startsWith("packages:"));
|
|
477
|
+
if (insertIndex !== -1) {
|
|
478
|
+
result.splice(insertIndex + 1, 0, ' - "cli/*"');
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return result.join("\n");
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Adds npm scripts to package.json.
|
|
485
|
+
*/
|
|
486
|
+
async setupScripts(projectRoot, flags, logger) {
|
|
487
|
+
const packageJsonPath = path.join(projectRoot, "package.json");
|
|
488
|
+
logger.info("Setting up npm scripts...");
|
|
489
|
+
// Read existing package.json
|
|
490
|
+
let packageJson;
|
|
491
|
+
try {
|
|
492
|
+
const content = await readFile(packageJsonPath, "utf8");
|
|
493
|
+
packageJson = JSON.parse(content);
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
logger.error("Could not read package.json");
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
499
|
+
// Ensure scripts object exists
|
|
500
|
+
if (!packageJson.scripts || typeof packageJson.scripts !== "object") {
|
|
501
|
+
packageJson.scripts = {};
|
|
502
|
+
}
|
|
503
|
+
const scripts = packageJson.scripts;
|
|
504
|
+
// Find which scripts need to be added/updated
|
|
505
|
+
const toAdd = [];
|
|
506
|
+
const toUpdate = [];
|
|
507
|
+
for (const [name, command] of Object.entries(DEFAULT_SCRIPTS)) {
|
|
508
|
+
if (!(name in scripts)) {
|
|
509
|
+
toAdd.push(name);
|
|
510
|
+
}
|
|
511
|
+
else if (scripts[name] !== command) {
|
|
512
|
+
toUpdate.push(name);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (toAdd.length === 0 && toUpdate.length === 0) {
|
|
516
|
+
logger.info("All npm scripts already configured");
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
// Report what will be done
|
|
520
|
+
if (toAdd.length > 0) {
|
|
521
|
+
logger.info({ scripts: toAdd }, "Scripts to add");
|
|
522
|
+
}
|
|
523
|
+
if (toUpdate.length > 0) {
|
|
524
|
+
logger.info({ scripts: toUpdate }, "Scripts that differ from defaults");
|
|
525
|
+
if (!flags.force) {
|
|
526
|
+
const shouldUpdate = await confirm({
|
|
527
|
+
message: `Update ${toUpdate.length} existing script(s) to chowbea-axios defaults?`,
|
|
528
|
+
default: false,
|
|
529
|
+
});
|
|
530
|
+
if (!shouldUpdate) {
|
|
531
|
+
// Only add new scripts, don't update existing
|
|
532
|
+
toUpdate.length = 0;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// Apply changes
|
|
537
|
+
for (const name of toAdd) {
|
|
538
|
+
scripts[name] = DEFAULT_SCRIPTS[name];
|
|
539
|
+
}
|
|
540
|
+
for (const name of toUpdate) {
|
|
541
|
+
scripts[name] = DEFAULT_SCRIPTS[name];
|
|
542
|
+
}
|
|
543
|
+
// Write updated package.json
|
|
544
|
+
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", "utf8");
|
|
545
|
+
logger.info({ added: toAdd.length, updated: toUpdate.length }, "Updated package.json scripts");
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Generates client files (api.instance.ts, api.error.ts, api.client.ts).
|
|
549
|
+
*/
|
|
550
|
+
async setupClientFiles(projectRoot, instanceConfig, flags, logger) {
|
|
551
|
+
logger.info("Setting up client files...");
|
|
552
|
+
// Load config to get output paths
|
|
553
|
+
const { config } = await loadConfig();
|
|
554
|
+
const outputPaths = getOutputPaths(config, projectRoot);
|
|
555
|
+
// Ensure output folders exist
|
|
556
|
+
await ensureOutputFolders(outputPaths);
|
|
557
|
+
// Generate client files
|
|
558
|
+
const result = await generateClientFiles({
|
|
559
|
+
paths: outputPaths,
|
|
560
|
+
instanceConfig,
|
|
561
|
+
logger,
|
|
562
|
+
force: flags.force,
|
|
563
|
+
});
|
|
564
|
+
if (result.helpers || result.instance || result.error || result.client) {
|
|
565
|
+
logger.info("Client files created:");
|
|
566
|
+
if (result.helpers)
|
|
567
|
+
logger.info(` - ${outputPaths.helpers}`);
|
|
568
|
+
if (result.instance)
|
|
569
|
+
logger.info(` - ${outputPaths.instance}`);
|
|
570
|
+
if (result.error)
|
|
571
|
+
logger.info(` - ${outputPaths.error}`);
|
|
572
|
+
if (result.client)
|
|
573
|
+
logger.info(` - ${outputPaths.client}`);
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
logger.info("Client files already exist (use --force to regenerate)");
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Detects the package manager based on lockfile presence.
|
|
581
|
+
* Returns 'pnpm', 'yarn', 'bun', or 'npm'.
|
|
582
|
+
*/
|
|
583
|
+
async detectPackageManager(projectRoot) {
|
|
584
|
+
// Check for lockfiles in order of preference
|
|
585
|
+
try {
|
|
586
|
+
await access(path.join(projectRoot, "pnpm-lock.yaml"));
|
|
587
|
+
return "pnpm";
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
/* Not found */
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
await access(path.join(projectRoot, "yarn.lock"));
|
|
594
|
+
return "yarn";
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
/* Not found */
|
|
598
|
+
}
|
|
599
|
+
try {
|
|
600
|
+
await access(path.join(projectRoot, "bun.lockb"));
|
|
601
|
+
return "bun";
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
/* Not found */
|
|
605
|
+
}
|
|
606
|
+
try {
|
|
607
|
+
await access(path.join(projectRoot, "package-lock.json"));
|
|
608
|
+
return "npm";
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
/* Not found */
|
|
612
|
+
}
|
|
613
|
+
// Default to pnpm if no lockfile found
|
|
614
|
+
return "pnpm";
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Sets up a concurrent watch script combining api:watch with user-selected dev scripts.
|
|
618
|
+
* Uses concurrently to run multiple scripts in parallel with labeled output.
|
|
619
|
+
*/
|
|
620
|
+
async setupConcurrentlyScript(projectRoot, logger) {
|
|
621
|
+
// Ask if user wants concurrent dev mode
|
|
622
|
+
const wantsConcurrent = await confirm({
|
|
623
|
+
message: "Would you like to create a script that runs api:watch alongside your dev server?",
|
|
624
|
+
default: true,
|
|
625
|
+
});
|
|
626
|
+
if (!wantsConcurrent) {
|
|
627
|
+
logger.info("Skipping concurrent script setup");
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
// Prompt for script alias
|
|
631
|
+
const scriptAlias = await input({
|
|
632
|
+
message: "Enter a name for the concurrent script:",
|
|
633
|
+
default: "dev:all",
|
|
634
|
+
validate: (value) => {
|
|
635
|
+
if (!value.trim())
|
|
636
|
+
return "Script name is required";
|
|
637
|
+
if (!/^[a-z][a-z0-9:_-]*$/i.test(value))
|
|
638
|
+
return "Invalid script name";
|
|
639
|
+
return true;
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
// Read package.json to get existing scripts
|
|
643
|
+
const packageJsonPath = path.join(projectRoot, "package.json");
|
|
644
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
645
|
+
const scripts = (packageJson.scripts || {});
|
|
646
|
+
// Filter out api:* scripts and the new alias itself
|
|
647
|
+
const userScripts = Object.keys(scripts).filter((name) => !name.startsWith("api:") && name !== scriptAlias);
|
|
648
|
+
if (userScripts.length === 0) {
|
|
649
|
+
logger.warn("No other scripts found to run concurrently");
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
// Let user select which scripts to include
|
|
653
|
+
const selectedScripts = await checkbox({
|
|
654
|
+
message: "Select scripts to run alongside api:watch:",
|
|
655
|
+
choices: userScripts.map((name) => ({
|
|
656
|
+
name: `${name}: ${scripts[name].slice(0, 50)}${scripts[name].length > 50 ? "..." : ""}`,
|
|
657
|
+
value: name,
|
|
658
|
+
})),
|
|
659
|
+
});
|
|
660
|
+
if (selectedScripts.length === 0) {
|
|
661
|
+
logger.info("No scripts selected, skipping concurrent setup");
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
// Collect short labels for each selected script
|
|
665
|
+
const labels = { "api:watch": "api" };
|
|
666
|
+
for (const scriptName of selectedScripts) {
|
|
667
|
+
const defaultLabel = scriptName
|
|
668
|
+
.replace(/[^a-z0-9]/gi, "")
|
|
669
|
+
.slice(0, 6)
|
|
670
|
+
.toLowerCase();
|
|
671
|
+
const label = await input({
|
|
672
|
+
message: `Short label for "${scriptName}" (shown in terminal output):`,
|
|
673
|
+
default: defaultLabel || "cmd",
|
|
674
|
+
validate: (value) => value.trim().length > 0 || "Label is required",
|
|
675
|
+
});
|
|
676
|
+
labels[scriptName] = label.trim();
|
|
677
|
+
}
|
|
678
|
+
// Detect package manager for the run command
|
|
679
|
+
const pm = await this.detectPackageManager(projectRoot);
|
|
680
|
+
const runCmd = pm === "npm" ? "npm run" : pm;
|
|
681
|
+
// Build the concurrently command
|
|
682
|
+
const allScripts = ["api:watch", ...selectedScripts];
|
|
683
|
+
const names = allScripts.map((s) => labels[s]).join(",");
|
|
684
|
+
const commands = allScripts.map((s) => `"${runCmd} ${s}"`).join(" ");
|
|
685
|
+
const concurrentlyCmd = `concurrently --names '${names}' ${commands}`;
|
|
686
|
+
// Add the script to package.json
|
|
687
|
+
scripts[scriptAlias] = concurrentlyCmd;
|
|
688
|
+
packageJson.scripts = scripts;
|
|
689
|
+
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", "utf8");
|
|
690
|
+
logger.info({ script: scriptAlias, pm }, "Created concurrent script");
|
|
691
|
+
// Ensure concurrently is installed
|
|
692
|
+
await this.ensureConcurrently(projectRoot, pm, logger);
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Ensures concurrently is installed as a dev dependency.
|
|
696
|
+
*/
|
|
697
|
+
async ensureConcurrently(projectRoot, pm, logger) {
|
|
698
|
+
const packageJsonPath = path.join(projectRoot, "package.json");
|
|
699
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
700
|
+
const devDeps = packageJson.devDependencies || {};
|
|
701
|
+
if (devDeps.concurrently) {
|
|
702
|
+
logger.debug("concurrently already installed");
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
logger.info("Installing concurrently...");
|
|
706
|
+
// Build install command based on package manager
|
|
707
|
+
let cmd;
|
|
708
|
+
let args;
|
|
709
|
+
switch (pm) {
|
|
710
|
+
case "yarn":
|
|
711
|
+
cmd = "yarn";
|
|
712
|
+
args = ["add", "-D", "concurrently"];
|
|
713
|
+
break;
|
|
714
|
+
case "bun":
|
|
715
|
+
cmd = "bun";
|
|
716
|
+
args = ["add", "-d", "concurrently"];
|
|
717
|
+
break;
|
|
718
|
+
case "npm":
|
|
719
|
+
cmd = "npm";
|
|
720
|
+
args = ["install", "-D", "concurrently"];
|
|
721
|
+
break;
|
|
722
|
+
default:
|
|
723
|
+
cmd = "pnpm";
|
|
724
|
+
args = ["add", "-D", "concurrently"];
|
|
725
|
+
}
|
|
726
|
+
const result = spawnSync(cmd, args, {
|
|
727
|
+
cwd: projectRoot,
|
|
728
|
+
stdio: "inherit",
|
|
729
|
+
});
|
|
730
|
+
if (result.status !== 0) {
|
|
731
|
+
logger.warn("Failed to install concurrently - you may need to install it manually");
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
logger.info("concurrently installed");
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
//# sourceMappingURL=init.js.map
|