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.
Files changed (58) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +162 -0
  3. package/bin/dev.js +13 -0
  4. package/bin/run.js +10 -0
  5. package/dist/commands/diff.d.ts +31 -0
  6. package/dist/commands/diff.d.ts.map +1 -0
  7. package/dist/commands/diff.js +215 -0
  8. package/dist/commands/diff.js.map +1 -0
  9. package/dist/commands/fetch.d.ts +28 -0
  10. package/dist/commands/fetch.d.ts.map +1 -0
  11. package/dist/commands/fetch.js +223 -0
  12. package/dist/commands/fetch.js.map +1 -0
  13. package/dist/commands/generate.d.ts +26 -0
  14. package/dist/commands/generate.d.ts.map +1 -0
  15. package/dist/commands/generate.js +187 -0
  16. package/dist/commands/generate.js.map +1 -0
  17. package/dist/commands/init.d.ts +92 -0
  18. package/dist/commands/init.d.ts.map +1 -0
  19. package/dist/commands/init.js +738 -0
  20. package/dist/commands/init.js.map +1 -0
  21. package/dist/commands/status.d.ts +38 -0
  22. package/dist/commands/status.d.ts.map +1 -0
  23. package/dist/commands/status.js +233 -0
  24. package/dist/commands/status.js.map +1 -0
  25. package/dist/commands/validate.d.ts +27 -0
  26. package/dist/commands/validate.d.ts.map +1 -0
  27. package/dist/commands/validate.js +209 -0
  28. package/dist/commands/validate.js.map +1 -0
  29. package/dist/commands/watch.d.ts +34 -0
  30. package/dist/commands/watch.d.ts.map +1 -0
  31. package/dist/commands/watch.js +202 -0
  32. package/dist/commands/watch.js.map +1 -0
  33. package/dist/index.d.ts +6 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +6 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/lib/config.d.ts +151 -0
  38. package/dist/lib/config.d.ts.map +1 -0
  39. package/dist/lib/config.js +336 -0
  40. package/dist/lib/config.js.map +1 -0
  41. package/dist/lib/errors.d.ts +77 -0
  42. package/dist/lib/errors.d.ts.map +1 -0
  43. package/dist/lib/errors.js +144 -0
  44. package/dist/lib/errors.js.map +1 -0
  45. package/dist/lib/fetcher.d.ts +115 -0
  46. package/dist/lib/fetcher.d.ts.map +1 -0
  47. package/dist/lib/fetcher.js +237 -0
  48. package/dist/lib/fetcher.js.map +1 -0
  49. package/dist/lib/generator.d.ts +96 -0
  50. package/dist/lib/generator.d.ts.map +1 -0
  51. package/dist/lib/generator.js +1575 -0
  52. package/dist/lib/generator.js.map +1 -0
  53. package/dist/lib/logger.d.ts +63 -0
  54. package/dist/lib/logger.d.ts.map +1 -0
  55. package/dist/lib/logger.js +183 -0
  56. package/dist/lib/logger.js.map +1 -0
  57. package/oclif.manifest.json +556 -0
  58. 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