@soda-gql/cli 0.8.0 → 0.9.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/dist/index.cjs CHANGED
@@ -26,17 +26,115 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  }) : target, mod));
27
27
 
28
28
  //#endregion
29
+ let neverthrow = require("neverthrow");
29
30
  let node_fs_promises = require("node:fs/promises");
30
31
  let node_path = require("node:path");
31
32
  let __soda_gql_builder = require("@soda-gql/builder");
32
33
  let __soda_gql_config = require("@soda-gql/config");
33
34
  let __soda_gql_codegen = require("@soda-gql/codegen");
34
- let neverthrow = require("neverthrow");
35
35
  let zod = require("zod");
36
+ let node_fs = require("node:fs");
36
37
  let fast_glob = require("fast-glob");
37
38
  fast_glob = __toESM(fast_glob);
38
- let node_fs = require("node:fs");
39
39
 
40
+ //#region packages/cli/src/errors.ts
41
+ /**
42
+ * Error constructor helpers for concise error creation.
43
+ * Each function returns a specific error type for better type inference.
44
+ */
45
+ const cliErrors = {
46
+ argsInvalid: (command, message) => ({
47
+ category: "cli",
48
+ code: "CLI_ARGS_INVALID",
49
+ message,
50
+ command
51
+ }),
52
+ unknownCommand: (command) => ({
53
+ category: "cli",
54
+ code: "CLI_UNKNOWN_COMMAND",
55
+ message: `Unknown command: ${command}`,
56
+ command
57
+ }),
58
+ unknownSubcommand: (parent, subcommand) => ({
59
+ category: "cli",
60
+ code: "CLI_UNKNOWN_SUBCOMMAND",
61
+ message: `Unknown subcommand: ${subcommand}`,
62
+ parent,
63
+ subcommand
64
+ }),
65
+ fileExists: (filePath, message) => ({
66
+ category: "cli",
67
+ code: "CLI_FILE_EXISTS",
68
+ message: message ?? `File already exists: ${filePath}. Use --force to overwrite.`,
69
+ filePath
70
+ }),
71
+ fileNotFound: (filePath, message) => ({
72
+ category: "cli",
73
+ code: "CLI_FILE_NOT_FOUND",
74
+ message: message ?? `File not found: ${filePath}`,
75
+ filePath
76
+ }),
77
+ writeFailed: (filePath, message, cause) => ({
78
+ category: "cli",
79
+ code: "CLI_WRITE_FAILED",
80
+ message: message ?? `Failed to write file: ${filePath}`,
81
+ filePath,
82
+ cause
83
+ }),
84
+ readFailed: (filePath, message, cause) => ({
85
+ category: "cli",
86
+ code: "CLI_READ_FAILED",
87
+ message: message ?? `Failed to read file: ${filePath}`,
88
+ filePath,
89
+ cause
90
+ }),
91
+ noPatterns: (message) => ({
92
+ category: "cli",
93
+ code: "CLI_NO_PATTERNS",
94
+ message: message ?? "No patterns provided and config not found. Usage: soda-gql format [patterns...] [--check]"
95
+ }),
96
+ formatterNotInstalled: (message) => ({
97
+ category: "cli",
98
+ code: "CLI_FORMATTER_NOT_INSTALLED",
99
+ message: message ?? "@soda-gql/formatter is not installed. Run: bun add @soda-gql/formatter"
100
+ }),
101
+ parseError: (message, filePath) => ({
102
+ category: "cli",
103
+ code: "CLI_PARSE_ERROR",
104
+ message,
105
+ filePath
106
+ }),
107
+ formatError: (message, filePath) => ({
108
+ category: "cli",
109
+ code: "CLI_FORMAT_ERROR",
110
+ message,
111
+ filePath
112
+ }),
113
+ unexpected: (message, cause) => ({
114
+ category: "cli",
115
+ code: "CLI_UNEXPECTED",
116
+ message,
117
+ cause
118
+ }),
119
+ fromCodegen: (error) => ({
120
+ category: "codegen",
121
+ error
122
+ }),
123
+ fromBuilder: (error) => ({
124
+ category: "builder",
125
+ error
126
+ }),
127
+ fromArtifact: (error) => ({
128
+ category: "artifact",
129
+ error
130
+ }),
131
+ fromConfig: (error) => ({
132
+ category: "config",
133
+ error
134
+ })
135
+ };
136
+
137
+ //#endregion
40
138
  //#region packages/cli/src/commands/artifact/build.ts
41
139
  const BUILD_HELP = `Usage: soda-gql artifact build [options]
42
140
 
@@ -78,33 +176,29 @@ const parseBuildArgs = (argv) => {
78
176
  }
79
177
  return args;
80
178
  };
179
+ const formatSuccess$3 = (data) => {
180
+ const { artifact, outputPath, dryRun } = data;
181
+ const fragmentCount = Object.values(artifact.elements).filter((e) => e.type === "fragment").length;
182
+ const operationCount = Object.values(artifact.elements).filter((e) => e.type === "operation").length;
183
+ const lines = [];
184
+ if (dryRun) lines.push(`Validation passed: ${fragmentCount} fragments, ${operationCount} operations`);
185
+ else lines.push(`Build complete: ${fragmentCount} fragments, ${operationCount} operations`);
186
+ if (artifact.meta?.version) lines.push(` Version: ${artifact.meta.version}`);
187
+ if (outputPath && !dryRun) lines.push(`Artifact written to: ${outputPath}`);
188
+ return lines.join("\n");
189
+ };
81
190
  /**
82
191
  * Build command - builds and validates soda-gql artifacts.
83
192
  */
84
193
  const buildCommand = async (argv) => {
85
194
  const args = parseBuildArgs(argv);
86
- if (args.help) {
87
- process.stdout.write(BUILD_HELP);
88
- return 0;
89
- }
195
+ if (args.help) return (0, neverthrow.ok)({ message: BUILD_HELP });
90
196
  const configResult = (0, __soda_gql_config.loadConfig)(args.configPath);
91
- if (configResult.isErr()) {
92
- const error = configResult.error;
93
- process.stderr.write(`Error: Failed to load config\n`);
94
- process.stderr.write(` at ${error.filePath}\n`);
95
- process.stderr.write(` ${error.message}\n`);
96
- return 1;
97
- }
197
+ if (configResult.isErr()) return (0, neverthrow.err)(cliErrors.fromConfig(configResult.error));
98
198
  const config = configResult.value;
99
199
  const buildResult = await (0, __soda_gql_builder.createBuilderService)({ config }).buildAsync();
100
- if (buildResult.isErr()) {
101
- const formattedError = (0, __soda_gql_builder.formatBuilderErrorForCLI)(buildResult.error);
102
- process.stderr.write(`${formattedError}\n`);
103
- return 1;
104
- }
200
+ if (buildResult.isErr()) return (0, neverthrow.err)(cliErrors.fromBuilder(buildResult.error));
105
201
  const artifact = buildResult.value;
106
- const fragmentCount = Object.values(artifact.elements).filter((e) => e.type === "fragment").length;
107
- const operationCount = Object.values(artifact.elements).filter((e) => e.type === "operation").length;
108
202
  const meta = args.version ? {
109
203
  version: args.version,
110
204
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -114,17 +208,33 @@ const buildCommand = async (argv) => {
114
208
  ...artifact
115
209
  };
116
210
  if (args.dryRun) {
117
- process.stdout.write(`Validation passed: ${fragmentCount} fragments, ${operationCount} operations\n`);
118
- if (args.version) process.stdout.write(` Version: ${args.version}\n`);
119
- } else {
120
- const outputPath = (0, node_path.resolve)(process.cwd(), args.outputPath);
121
- await (0, node_fs_promises.mkdir)((0, node_path.dirname)(outputPath), { recursive: true });
211
+ const data$1 = {
212
+ artifact: artifactWithMeta,
213
+ dryRun: true
214
+ };
215
+ return (0, neverthrow.ok)({
216
+ message: formatSuccess$3(data$1),
217
+ data: data$1
218
+ });
219
+ }
220
+ const outputPath = (0, node_path.resolve)(process.cwd(), args.outputPath);
221
+ const outputDir = (0, node_path.dirname)(outputPath);
222
+ try {
223
+ await (0, node_fs_promises.mkdir)(outputDir, { recursive: true });
122
224
  await (0, node_fs_promises.writeFile)(outputPath, JSON.stringify(artifactWithMeta, null, 2));
123
- process.stdout.write(`Build complete: ${fragmentCount} fragments, ${operationCount} operations\n`);
124
- if (args.version) process.stdout.write(` Version: ${args.version}\n`);
125
- process.stdout.write(`Artifact written to: ${outputPath}\n`);
225
+ } catch (error) {
226
+ const message = error instanceof Error ? error.message : String(error);
227
+ return (0, neverthrow.err)(cliErrors.writeFailed(outputPath, `Failed to write artifact: ${message}`, error));
126
228
  }
127
- return 0;
229
+ const data = {
230
+ artifact: artifactWithMeta,
231
+ outputPath,
232
+ dryRun: false
233
+ };
234
+ return (0, neverthrow.ok)({
235
+ message: formatSuccess$3(data),
236
+ data
237
+ });
128
238
  };
129
239
 
130
240
  //#endregion
@@ -155,36 +265,27 @@ const parseValidateArgs = (argv) => {
155
265
  else if (!arg.startsWith("-")) args.artifactPath = arg;
156
266
  return args;
157
267
  };
268
+ const formatSuccess$2 = (artifact) => {
269
+ const lines = [`Artifact valid: ${Object.values(artifact.elements).filter((e) => e.type === "fragment").length} fragments, ${Object.values(artifact.elements).filter((e) => e.type === "operation").length} operations`];
270
+ if (artifact.meta) {
271
+ lines.push(` Version: ${artifact.meta.version}`);
272
+ lines.push(` Created: ${artifact.meta.createdAt}`);
273
+ } else lines.push(` (No metadata - legacy artifact format)`);
274
+ return lines.join("\n");
275
+ };
158
276
  /**
159
277
  * Validate command - validates a pre-built artifact file.
160
278
  */
161
279
  const validateCommand = async (argv) => {
162
280
  const args = parseValidateArgs(argv);
163
- if (args.help) {
164
- process.stdout.write(VALIDATE_HELP);
165
- return 0;
166
- }
167
- if (!args.artifactPath) {
168
- process.stderr.write("Error: Missing artifact path argument\n\n");
169
- process.stdout.write(VALIDATE_HELP);
170
- return 1;
171
- }
281
+ if (args.help) return (0, neverthrow.ok)({ message: VALIDATE_HELP });
282
+ if (!args.artifactPath) return (0, neverthrow.err)(cliErrors.argsInvalid("artifact validate", "Missing artifact path argument"));
172
283
  const result = await (0, __soda_gql_builder.loadArtifact)((0, node_path.resolve)(process.cwd(), args.artifactPath));
173
- if (result.isErr()) {
174
- const error = result.error;
175
- process.stderr.write(`Validation failed: ${error.message}\n`);
176
- if (error.filePath) process.stderr.write(` File: ${error.filePath}\n`);
177
- return 1;
178
- }
179
- const artifact = result.value;
180
- const fragmentCount = Object.values(artifact.elements).filter((e) => e.type === "fragment").length;
181
- const operationCount = Object.values(artifact.elements).filter((e) => e.type === "operation").length;
182
- process.stdout.write(`Artifact valid: ${fragmentCount} fragments, ${operationCount} operations\n`);
183
- if (artifact.meta) {
184
- process.stdout.write(` Version: ${artifact.meta.version}\n`);
185
- process.stdout.write(` Created: ${artifact.meta.createdAt}\n`);
186
- } else process.stdout.write(` (No metadata - legacy artifact format)\n`);
187
- return 0;
284
+ if (result.isErr()) return (0, neverthrow.err)(cliErrors.fromArtifact(result.error));
285
+ return (0, neverthrow.ok)({
286
+ message: formatSuccess$2(result.value),
287
+ data: result.value
288
+ });
188
289
  };
189
290
 
190
291
  //#endregion
@@ -204,15 +305,10 @@ Run 'soda-gql artifact <subcommand> --help' for more information.
204
305
  */
205
306
  const artifactCommand = async (argv) => {
206
307
  const [subcommand, ...rest] = argv;
207
- if (!subcommand || subcommand === "--help" || subcommand === "-h") {
208
- process.stdout.write(ARTIFACT_HELP);
209
- return 0;
210
- }
308
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") return (0, neverthrow.ok)({ message: ARTIFACT_HELP });
211
309
  if (subcommand === "build") return buildCommand(rest);
212
310
  if (subcommand === "validate") return validateCommand(rest);
213
- process.stderr.write(`Unknown subcommand: ${subcommand}\n`);
214
- process.stderr.write(`Run 'soda-gql artifact --help' for available subcommands.\n`);
215
- return 1;
311
+ return (0, neverthrow.err)(cliErrors.unknownSubcommand("artifact", subcommand));
216
312
  };
217
313
 
218
314
  //#endregion
@@ -262,28 +358,16 @@ const parseArgs = (args, schema) => {
262
358
  //#region packages/cli/src/commands/codegen.ts
263
359
  const parseCodegenArgs = (argv) => {
264
360
  const parsed = parseArgs([...argv], CodegenArgsSchema);
265
- if (!parsed.isOk()) return (0, neverthrow.err)({
266
- code: "EMIT_FAILED",
267
- message: parsed.error,
268
- outPath: ""
269
- });
361
+ if (!parsed.isOk()) return (0, neverthrow.err)(cliErrors.argsInvalid("codegen", parsed.error));
270
362
  const args = parsed.value;
271
363
  if (args["emit-inject-template"]) return (0, neverthrow.ok)({
272
364
  kind: "emitInjectTemplate",
273
365
  outPath: args["emit-inject-template"]
274
366
  });
275
367
  const configResult = (0, __soda_gql_config.loadConfig)(args.config);
276
- if (configResult.isErr()) return (0, neverthrow.err)({
277
- code: "EMIT_FAILED",
278
- message: `Failed to load config: ${configResult.error.message}`,
279
- outPath: ""
280
- });
368
+ if (configResult.isErr()) return (0, neverthrow.err)(cliErrors.fromConfig(configResult.error));
281
369
  const config = configResult.value;
282
- if (!config.schemas || Object.keys(config.schemas).length === 0) return (0, neverthrow.err)({
283
- code: "EMIT_FAILED",
284
- message: "schemas configuration is required in soda-gql.config.ts",
285
- outPath: ""
286
- });
370
+ if (!config.schemas || Object.keys(config.schemas).length === 0) return (0, neverthrow.err)(cliErrors.argsInvalid("codegen", "schemas configuration is required in soda-gql.config.ts"));
287
371
  const schemas = {};
288
372
  for (const [name, schemaConfig] of Object.entries(config.schemas)) schemas[name] = {
289
373
  schema: schemaConfig.schema,
@@ -305,20 +389,6 @@ const formatSuccess$1 = (success) => {
305
389
  const formatTemplateSuccess = (outPath) => {
306
390
  return `Created inject template → ${outPath}`;
307
391
  };
308
- const errorHints = {
309
- SCHEMA_NOT_FOUND: "Verify the schema path in soda-gql.config.ts",
310
- SCHEMA_INVALID: "Check your GraphQL schema for syntax errors",
311
- INJECT_MODULE_NOT_FOUND: "Run: soda-gql codegen --emit-inject-template <path>",
312
- INJECT_MODULE_REQUIRED: "Add inject configuration to your schema in soda-gql.config.ts",
313
- INJECT_TEMPLATE_EXISTS: "Delete the existing file to regenerate, or use a different path",
314
- EMIT_FAILED: "Check write permissions and that the output directory exists"
315
- };
316
- const formatCodegenError = (error) => {
317
- const message = "message" in error ? error.message : "Unknown error";
318
- const hint = errorHints[error.code];
319
- const hintLine = hint ? `\n Hint: ${hint}` : "";
320
- return `${error.code}: ${message}${hintLine}`;
321
- };
322
392
  const CODEGEN_HELP = `Usage: soda-gql codegen [options]
323
393
 
324
394
  Generate graphql-system runtime module from GraphQL schema.
@@ -333,58 +403,521 @@ Examples:
333
403
  soda-gql codegen --emit-inject-template ./src/graphql/scalars.ts
334
404
  `;
335
405
  const codegenCommand = async (argv) => {
336
- if (argv.includes("--help") || argv.includes("-h")) {
337
- process.stdout.write(CODEGEN_HELP);
338
- return 0;
406
+ if (argv.includes("--help") || argv.includes("-h")) return (0, neverthrow.ok)({ message: CODEGEN_HELP });
407
+ const parsed = parseCodegenArgs(argv);
408
+ if (parsed.isErr()) return (0, neverthrow.err)(parsed.error);
409
+ const command = parsed.value;
410
+ if (command.kind === "emitInjectTemplate") {
411
+ const outPath = (0, node_path.resolve)(command.outPath);
412
+ const result$1 = (0, __soda_gql_codegen.writeInjectTemplate)(outPath);
413
+ if (result$1.isErr()) return (0, neverthrow.err)(cliErrors.fromCodegen(result$1.error));
414
+ return (0, neverthrow.ok)({ message: formatTemplateSuccess(outPath) });
339
415
  }
416
+ const resolvedSchemas = {};
417
+ for (const [name, schemaConfig] of Object.entries(command.schemas)) resolvedSchemas[name] = {
418
+ schema: (0, node_path.resolve)(schemaConfig.schema),
419
+ inject: {
420
+ scalars: (0, node_path.resolve)(schemaConfig.inject.scalars),
421
+ ...schemaConfig.inject.adapter ? { adapter: (0, node_path.resolve)(schemaConfig.inject.adapter) } : {}
422
+ },
423
+ defaultInputDepth: schemaConfig.defaultInputDepth,
424
+ inputDepthOverrides: schemaConfig.inputDepthOverrides
425
+ };
426
+ const result = await (0, __soda_gql_codegen.runCodegen)({
427
+ schemas: resolvedSchemas,
428
+ outPath: (0, node_path.resolve)(command.outPath),
429
+ format: "human",
430
+ importExtension: command.importExtension
431
+ });
432
+ if (result.isErr()) return (0, neverthrow.err)(cliErrors.fromCodegen(result.error));
433
+ return (0, neverthrow.ok)({
434
+ message: formatSuccess$1(result.value),
435
+ data: result.value
436
+ });
437
+ };
438
+
439
+ //#endregion
440
+ //#region packages/cli/src/commands/doctor/checks/codegen-freshness.ts
441
+ /**
442
+ * Codegen freshness check.
443
+ * @module
444
+ */
445
+ /**
446
+ * Check if generated code is newer than schema files.
447
+ */
448
+ const checkCodegenFreshness = () => {
449
+ const configPath = (0, __soda_gql_config.findConfigFile)();
450
+ if (!configPath) return {
451
+ name: "Codegen Freshness",
452
+ status: "skip",
453
+ message: "No soda-gql.config.ts found",
454
+ data: { schemas: [] }
455
+ };
456
+ const configResult = (0, __soda_gql_config.loadConfig)(configPath);
457
+ if (configResult.isErr()) return {
458
+ name: "Codegen Freshness",
459
+ status: "skip",
460
+ message: "Could not load config",
461
+ data: { schemas: [] }
462
+ };
463
+ const config = configResult.value;
464
+ const generatedPath = (0, node_path.join)(config.outdir, "index.ts");
465
+ if (!(0, node_fs.existsSync)(generatedPath)) return {
466
+ name: "Codegen Freshness",
467
+ status: "warn",
468
+ message: "Generated code not found - run codegen",
469
+ data: { schemas: [] },
470
+ fix: "Run: soda-gql codegen"
471
+ };
472
+ const generatedMtime = (0, node_fs.statSync)(generatedPath).mtimeMs;
473
+ const schemaResults = [];
474
+ let hasStale = false;
475
+ for (const [name, schemaConfig] of Object.entries(config.schemas)) {
476
+ if (!(0, node_fs.existsSync)(schemaConfig.schema)) continue;
477
+ const schemaMtime = (0, node_fs.statSync)(schemaConfig.schema).mtimeMs;
478
+ const isStale = schemaMtime > generatedMtime;
479
+ if (isStale) hasStale = true;
480
+ schemaResults.push({
481
+ name,
482
+ schemaPath: schemaConfig.schema,
483
+ generatedPath,
484
+ schemaMtime,
485
+ generatedMtime,
486
+ isStale
487
+ });
488
+ }
489
+ if (hasStale) return {
490
+ name: "Codegen Freshness",
491
+ status: "warn",
492
+ message: `Schema modified after codegen: ${schemaResults.filter((s) => s.isStale).map((s) => s.name).join(", ")}`,
493
+ data: { schemas: schemaResults },
494
+ fix: "Run: soda-gql codegen"
495
+ };
496
+ return {
497
+ name: "Codegen Freshness",
498
+ status: "pass",
499
+ message: "Generated code is up to date",
500
+ data: { schemas: schemaResults }
501
+ };
502
+ };
503
+
504
+ //#endregion
505
+ //#region packages/cli/src/commands/doctor/checks/config-validation.ts
506
+ /**
507
+ * Config validation check.
508
+ * @module
509
+ */
510
+ /**
511
+ * Check that config file is valid and referenced files exist.
512
+ */
513
+ const checkConfigValidation = () => {
514
+ const configPath = (0, __soda_gql_config.findConfigFile)();
515
+ if (!configPath) return {
516
+ name: "Config Validation",
517
+ status: "skip",
518
+ message: "No soda-gql.config.ts found",
519
+ data: {
520
+ configPath: null,
521
+ missingFiles: []
522
+ }
523
+ };
524
+ const configResult = (0, __soda_gql_config.loadConfig)(configPath);
525
+ if (configResult.isErr()) return {
526
+ name: "Config Validation",
527
+ status: "fail",
528
+ message: `Config error: ${configResult.error.message}`,
529
+ data: {
530
+ configPath,
531
+ missingFiles: []
532
+ },
533
+ fix: "Check your soda-gql.config.ts for syntax errors"
534
+ };
535
+ const config = configResult.value;
536
+ const missingFiles = [];
537
+ for (const [name, schemaConfig] of Object.entries(config.schemas)) {
538
+ if (!(0, node_fs.existsSync)(schemaConfig.schema)) missingFiles.push(`Schema '${name}': ${schemaConfig.schema}`);
539
+ if (!(0, node_fs.existsSync)(schemaConfig.inject.scalars)) missingFiles.push(`Scalars '${name}': ${schemaConfig.inject.scalars}`);
540
+ if (schemaConfig.inject.adapter && !(0, node_fs.existsSync)(schemaConfig.inject.adapter)) missingFiles.push(`Adapter '${name}': ${schemaConfig.inject.adapter}`);
541
+ }
542
+ if (missingFiles.length > 0) return {
543
+ name: "Config Validation",
544
+ status: "fail",
545
+ message: `${missingFiles.length} referenced file(s) not found`,
546
+ data: {
547
+ configPath,
548
+ missingFiles
549
+ },
550
+ fix: "Create the missing files or update paths in config"
551
+ };
552
+ return {
553
+ name: "Config Validation",
554
+ status: "pass",
555
+ message: "Config loaded successfully",
556
+ data: {
557
+ configPath,
558
+ missingFiles: []
559
+ }
560
+ };
561
+ };
562
+
563
+ //#endregion
564
+ //#region packages/cli/src/commands/doctor/discovery.ts
565
+ /**
566
+ * Package discovery utilities for doctor command.
567
+ * @module
568
+ */
569
+ const SODA_GQL_SCOPE = "@soda-gql";
570
+ /**
571
+ * Find the nearest node_modules directory.
572
+ */
573
+ const findNodeModules = (startDir = process.cwd()) => {
574
+ let currentDir = startDir;
575
+ while (currentDir !== (0, node_path.dirname)(currentDir)) {
576
+ const nodeModulesPath = (0, node_path.join)(currentDir, "node_modules");
577
+ if ((0, node_fs.existsSync)(nodeModulesPath) && (0, node_fs.statSync)(nodeModulesPath).isDirectory()) return nodeModulesPath;
578
+ currentDir = (0, node_path.dirname)(currentDir);
579
+ }
580
+ return null;
581
+ };
582
+ /**
583
+ * Read package.json from a directory.
584
+ */
585
+ const readPackageJson = (dir) => {
586
+ const packageJsonPath = (0, node_path.join)(dir, "package.json");
340
587
  try {
341
- const parsed = parseCodegenArgs(argv);
342
- if (parsed.isErr()) {
343
- process.stderr.write(`${formatCodegenError(parsed.error)}\n`);
344
- return 1;
588
+ const content = (0, node_fs.readFileSync)(packageJsonPath, "utf-8");
589
+ const pkg = JSON.parse(content);
590
+ if (!pkg.name || !pkg.version) return (0, neverthrow.err)(`Invalid package.json at ${packageJsonPath}`);
591
+ return (0, neverthrow.ok)({
592
+ name: pkg.name,
593
+ version: pkg.version
594
+ });
595
+ } catch {
596
+ return (0, neverthrow.err)(`Failed to read package.json at ${packageJsonPath}`);
597
+ }
598
+ };
599
+ /**
600
+ * Discover @soda-gql packages at a specific node_modules path.
601
+ */
602
+ const discoverAtPath = (nodeModulesPath) => {
603
+ const scopePath = (0, node_path.join)(nodeModulesPath, SODA_GQL_SCOPE);
604
+ if (!(0, node_fs.existsSync)(scopePath)) return [];
605
+ const packages = [];
606
+ try {
607
+ const entries = (0, node_fs.readdirSync)(scopePath, { withFileTypes: true });
608
+ for (const entry of entries) {
609
+ if (!entry.isDirectory()) continue;
610
+ const packageDir = (0, node_path.join)(scopePath, entry.name);
611
+ const result = readPackageJson(packageDir);
612
+ if (result.isOk()) packages.push({
613
+ name: result.value.name,
614
+ version: result.value.version,
615
+ path: packageDir
616
+ });
617
+ }
618
+ } catch {}
619
+ return packages;
620
+ };
621
+ /**
622
+ * Discover all @soda-gql packages including nested node_modules.
623
+ * Uses breadth-first search to avoid deep recursion.
624
+ */
625
+ const discoverAllSodaGqlPackages = (startDir = process.cwd()) => {
626
+ const rootNodeModules = findNodeModules(startDir);
627
+ if (!rootNodeModules) return (0, neverthrow.err)("No node_modules directory found");
628
+ const allPackages = [];
629
+ const visitedPaths = /* @__PURE__ */ new Set();
630
+ const queue = [rootNodeModules];
631
+ while (queue.length > 0) {
632
+ const nodeModulesPath = queue.shift();
633
+ if (!nodeModulesPath) continue;
634
+ let realPath;
635
+ try {
636
+ realPath = (0, node_path.resolve)(nodeModulesPath);
637
+ } catch {
638
+ continue;
639
+ }
640
+ if (visitedPaths.has(realPath)) continue;
641
+ visitedPaths.add(realPath);
642
+ const packages = discoverAtPath(nodeModulesPath);
643
+ allPackages.push(...packages);
644
+ try {
645
+ const entries = (0, node_fs.readdirSync)(nodeModulesPath, { withFileTypes: true });
646
+ for (const entry of entries) {
647
+ if (!entry.isDirectory()) continue;
648
+ if (entry.name.startsWith("@")) {
649
+ const scopeDir = (0, node_path.join)(nodeModulesPath, entry.name);
650
+ try {
651
+ const scopeEntries = (0, node_fs.readdirSync)(scopeDir, { withFileTypes: true });
652
+ for (const scopeEntry of scopeEntries) {
653
+ if (!scopeEntry.isDirectory()) continue;
654
+ const nestedNodeModules = (0, node_path.join)(scopeDir, scopeEntry.name, "node_modules");
655
+ if ((0, node_fs.existsSync)(nestedNodeModules)) queue.push(nestedNodeModules);
656
+ }
657
+ } catch {}
658
+ } else {
659
+ const nestedNodeModules = (0, node_path.join)(nodeModulesPath, entry.name, "node_modules");
660
+ if ((0, node_fs.existsSync)(nestedNodeModules)) queue.push(nestedNodeModules);
661
+ }
662
+ }
663
+ } catch {}
664
+ }
665
+ return (0, neverthrow.ok)(allPackages);
666
+ };
667
+ /**
668
+ * Get the soda-gql CLI version.
669
+ */
670
+ const getCliVersion = () => {
671
+ try {
672
+ const content = (0, node_fs.readFileSync)((0, node_path.join)(__dirname, "..", "..", "..", "package.json"), "utf-8");
673
+ return JSON.parse(content).version ?? "unknown";
674
+ } catch {
675
+ return "unknown";
676
+ }
677
+ };
678
+ /**
679
+ * Get TypeScript version from node_modules.
680
+ */
681
+ const getTypescriptVersion = (startDir = process.cwd()) => {
682
+ const nodeModulesPath = findNodeModules(startDir);
683
+ if (!nodeModulesPath) return null;
684
+ const tsPackageJson = (0, node_path.join)(nodeModulesPath, "typescript", "package.json");
685
+ try {
686
+ const content = (0, node_fs.readFileSync)(tsPackageJson, "utf-8");
687
+ return JSON.parse(content).version ?? null;
688
+ } catch {
689
+ return null;
690
+ }
691
+ };
692
+
693
+ //#endregion
694
+ //#region packages/cli/src/commands/doctor/checks/duplicate-packages.ts
695
+ /**
696
+ * Duplicate packages check.
697
+ * @module
698
+ */
699
+ /**
700
+ * Check for duplicate @soda-gql packages installed at different paths.
701
+ */
702
+ const checkDuplicatePackages = () => {
703
+ const packagesResult = discoverAllSodaGqlPackages();
704
+ if (packagesResult.isErr()) return {
705
+ name: "Duplicate Packages",
706
+ status: "skip",
707
+ message: packagesResult.error,
708
+ data: { duplicates: [] }
709
+ };
710
+ const packages = packagesResult.value;
711
+ const byName = /* @__PURE__ */ new Map();
712
+ for (const pkg of packages) {
713
+ const existing = byName.get(pkg.name) ?? [];
714
+ existing.push(pkg);
715
+ byName.set(pkg.name, existing);
716
+ }
717
+ const duplicates = [];
718
+ for (const [name, instances] of byName) if (instances.length > 1) duplicates.push({
719
+ name,
720
+ instances: instances.map((i) => ({
721
+ path: i.path,
722
+ version: i.version
723
+ }))
724
+ });
725
+ if (duplicates.length === 0) return {
726
+ name: "Duplicate Packages",
727
+ status: "pass",
728
+ message: "No duplicate packages detected",
729
+ data: { duplicates: [] }
730
+ };
731
+ return {
732
+ name: "Duplicate Packages",
733
+ status: "warn",
734
+ message: `Duplicate packages found: ${duplicates.map((d) => d.name).join(", ")}`,
735
+ data: { duplicates },
736
+ fix: "Run: rm -rf node_modules && bun install"
737
+ };
738
+ };
739
+
740
+ //#endregion
741
+ //#region packages/cli/src/commands/doctor/checks/version-consistency.ts
742
+ /**
743
+ * Version consistency check.
744
+ * @module
745
+ */
746
+ /**
747
+ * Check that all @soda-gql packages have consistent versions.
748
+ */
749
+ const checkVersionConsistency = () => {
750
+ const packagesResult = discoverAllSodaGqlPackages();
751
+ if (packagesResult.isErr()) return {
752
+ name: "Version Consistency",
753
+ status: "skip",
754
+ message: packagesResult.error,
755
+ data: {
756
+ packages: [],
757
+ expectedVersion: null
758
+ }
759
+ };
760
+ const packages = packagesResult.value;
761
+ if (packages.length === 0) return {
762
+ name: "Version Consistency",
763
+ status: "skip",
764
+ message: "No @soda-gql packages found",
765
+ data: {
766
+ packages: [],
767
+ expectedVersion: null
768
+ }
769
+ };
770
+ const byName = /* @__PURE__ */ new Map();
771
+ for (const pkg of packages) {
772
+ const existing = byName.get(pkg.name) ?? [];
773
+ existing.push(pkg);
774
+ byName.set(pkg.name, existing);
775
+ }
776
+ const uniquePackages = Array.from(byName.values()).map((instances) => instances[0]).filter((pkg) => pkg !== void 0);
777
+ const versionCounts = /* @__PURE__ */ new Map();
778
+ for (const pkg of uniquePackages) versionCounts.set(pkg.version, (versionCounts.get(pkg.version) ?? 0) + 1);
779
+ let expectedVersion = uniquePackages[0]?.version ?? null;
780
+ let maxCount = 0;
781
+ for (const [version, count] of versionCounts) if (count > maxCount) {
782
+ maxCount = count;
783
+ expectedVersion = version;
784
+ }
785
+ const packageResults = uniquePackages.map((pkg) => ({
786
+ name: pkg.name,
787
+ version: pkg.version,
788
+ path: pkg.path,
789
+ isMismatch: pkg.version !== expectedVersion
790
+ }));
791
+ const mismatches = packageResults.filter((p) => p.isMismatch);
792
+ if (mismatches.length === 0) return {
793
+ name: "Version Consistency",
794
+ status: "pass",
795
+ message: `All ${uniquePackages.length} packages at version ${expectedVersion}`,
796
+ data: {
797
+ packages: packageResults,
798
+ expectedVersion
799
+ }
800
+ };
801
+ return {
802
+ name: "Version Consistency",
803
+ status: "fail",
804
+ message: `Version mismatch: ${mismatches.map((p) => p.name).join(", ")}`,
805
+ data: {
806
+ packages: packageResults,
807
+ expectedVersion
808
+ },
809
+ fix: `Run: bun update ${mismatches.map((p) => p.name).join(" ")}`
810
+ };
811
+ };
812
+
813
+ //#endregion
814
+ //#region packages/cli/src/commands/doctor/output.ts
815
+ const STATUS_SYMBOLS = {
816
+ pass: "✓",
817
+ warn: "!",
818
+ fail: "✗",
819
+ skip: "-"
820
+ };
821
+ /**
822
+ * Type guard to check if data is an object (not null/primitive).
823
+ */
824
+ const isObject = (data) => {
825
+ return typeof data === "object" && data !== null;
826
+ };
827
+ /**
828
+ * Format a single check result for human output.
829
+ */
830
+ const formatCheckResult = (result) => {
831
+ const lines = [];
832
+ const symbol = STATUS_SYMBOLS[result.status];
833
+ lines.push(`${symbol} ${result.message}`);
834
+ if (result.status === "fail" || result.status === "warn") {
835
+ const data = result.data;
836
+ if (isObject(data) && "packages" in data && "expectedVersion" in data) {
837
+ const versionData = data;
838
+ const mismatched = versionData.packages.filter((p) => p.isMismatch);
839
+ for (const pkg of mismatched) lines.push(` ${pkg.name}: ${pkg.version} <- mismatch`);
840
+ if (versionData.expectedVersion && mismatched.length > 0) lines.push(` Expected: ${versionData.expectedVersion}`);
345
841
  }
346
- const command = parsed.value;
347
- if (command.kind === "emitInjectTemplate") {
348
- const outPath = (0, node_path.resolve)(command.outPath);
349
- const result$1 = (0, __soda_gql_codegen.writeInjectTemplate)(outPath);
350
- if (result$1.isErr()) {
351
- process.stderr.write(`${formatCodegenError(result$1.error)}\n`);
352
- return 1;
842
+ if (isObject(data) && "duplicates" in data) {
843
+ const dupData = data;
844
+ for (const dup of dupData.duplicates) {
845
+ lines.push(` ${dup.name}:`);
846
+ for (const instance of dup.instances) lines.push(` ${instance.version} at ${instance.path}`);
353
847
  }
354
- process.stdout.write(`${formatTemplateSuccess(outPath)}\n`);
355
- return 0;
356
848
  }
357
- const resolvedSchemas = {};
358
- for (const [name, schemaConfig] of Object.entries(command.schemas)) resolvedSchemas[name] = {
359
- schema: (0, node_path.resolve)(schemaConfig.schema),
360
- inject: {
361
- scalars: (0, node_path.resolve)(schemaConfig.inject.scalars),
362
- ...schemaConfig.inject.adapter ? { adapter: (0, node_path.resolve)(schemaConfig.inject.adapter) } : {}
363
- },
364
- defaultInputDepth: schemaConfig.defaultInputDepth,
365
- inputDepthOverrides: schemaConfig.inputDepthOverrides
366
- };
367
- const result = await (0, __soda_gql_codegen.runCodegen)({
368
- schemas: resolvedSchemas,
369
- outPath: (0, node_path.resolve)(command.outPath),
370
- format: "human",
371
- importExtension: command.importExtension
372
- });
373
- if (result.isErr()) {
374
- process.stderr.write(`${formatCodegenError(result.error)}\n`);
375
- return 1;
849
+ if (result.fix) {
850
+ lines.push("");
851
+ lines.push(` Fix: ${result.fix}`);
376
852
  }
377
- process.stdout.write(`${formatSuccess$1(result.value)}\n`);
378
- return 0;
379
- } catch (error) {
380
- const unexpectedError = {
381
- code: "EMIT_FAILED",
382
- message: error instanceof Error ? error.message : String(error),
383
- outPath: ""
384
- };
385
- process.stderr.write(`${formatCodegenError(unexpectedError)}\n`);
386
- return 1;
387
853
  }
854
+ return lines;
855
+ };
856
+ /**
857
+ * Format the complete doctor result for human output.
858
+ */
859
+ const formatDoctorResult = (result) => {
860
+ const lines = [];
861
+ lines.push(`soda-gql doctor v${result.version}`);
862
+ lines.push("");
863
+ for (const check of result.checks) {
864
+ lines.push(...formatCheckResult(check));
865
+ lines.push("");
866
+ }
867
+ const passed = result.checks.filter((c) => c.status === "pass").length;
868
+ if (result.issueCount === 0 && result.warningCount === 0) lines.push(`Summary: All ${passed} checks passed`);
869
+ else {
870
+ const parts = [];
871
+ if (result.issueCount > 0) parts.push(`${result.issueCount} issue${result.issueCount > 1 ? "s" : ""}`);
872
+ if (result.warningCount > 0) parts.push(`${result.warningCount} warning${result.warningCount > 1 ? "s" : ""}`);
873
+ lines.push(`Summary: ${parts.join(", ")} found`);
874
+ }
875
+ return lines.join("\n");
876
+ };
877
+
878
+ //#endregion
879
+ //#region packages/cli/src/commands/doctor/index.ts
880
+ /**
881
+ * Doctor command entry point.
882
+ * @module
883
+ */
884
+ const DOCTOR_HELP = `Usage: soda-gql doctor
885
+
886
+ Run diagnostic checks on your soda-gql installation.
887
+
888
+ Checks performed:
889
+ - Version consistency across @soda-gql packages
890
+ - Duplicate package detection
891
+ - Config file validation
892
+ - Codegen freshness (schema vs generated code)
893
+
894
+ Options:
895
+ --help, -h Show this help message
896
+ `;
897
+ const doctorCommand = (argv) => {
898
+ if (argv.includes("--help") || argv.includes("-h")) return (0, neverthrow.ok)({ message: DOCTOR_HELP });
899
+ const version = getCliVersion();
900
+ const tsVersion = getTypescriptVersion();
901
+ const checks = [];
902
+ if (tsVersion) checks.push({
903
+ name: "TypeScript Version",
904
+ status: "pass",
905
+ message: `TypeScript version: ${tsVersion}`
906
+ });
907
+ checks.push(checkVersionConsistency());
908
+ checks.push(checkDuplicatePackages());
909
+ checks.push(checkConfigValidation());
910
+ checks.push(checkCodegenFreshness());
911
+ const result = {
912
+ version,
913
+ checks,
914
+ issueCount: checks.filter((c) => c.status === "fail").length,
915
+ warningCount: checks.filter((c) => c.status === "warn").length
916
+ };
917
+ return (0, neverthrow.ok)({
918
+ message: formatDoctorResult(result),
919
+ data: result
920
+ });
388
921
  };
389
922
 
390
923
  //#endregion
@@ -396,22 +929,19 @@ const loadFormatter = async () => {
396
929
  return null;
397
930
  }
398
931
  };
399
- const formatFormatError = (error) => {
400
- return `${error.code}: ${error.message}`;
401
- };
402
- const formatResult = (result) => {
403
- if (result.mode === "check") {
404
- if (result.unformatted.length > 0) {
405
- const files = result.unformatted.map((f) => ` ${f}`).join("\n");
406
- return `${result.unformatted.length} file(s) need formatting:\n${files}`;
932
+ const formatResultMessage = (data) => {
933
+ if (data.mode === "check") {
934
+ if (data.unformatted.length > 0) {
935
+ const files = data.unformatted.map((f) => ` ${f}`).join("\n");
936
+ return `${data.unformatted.length} file(s) need formatting:\n${files}`;
407
937
  }
408
- return `All ${result.total} file(s) are properly formatted`;
938
+ return `All ${data.total} file(s) are properly formatted`;
409
939
  }
410
940
  const parts = [];
411
- if (result.modified > 0) parts.push(`${result.modified} formatted`);
412
- if (result.unchanged > 0) parts.push(`${result.unchanged} unchanged`);
413
- if (result.errors > 0) parts.push(`${result.errors} errors`);
414
- return `${result.total} file(s) checked: ${parts.join(", ")}`;
941
+ if (data.modified > 0) parts.push(`${data.modified} formatted`);
942
+ if (data.unchanged > 0) parts.push(`${data.unchanged} unchanged`);
943
+ if (data.errors > 0) parts.push(`${data.errors} errors`);
944
+ return `${data.total} file(s) checked: ${parts.join(", ")}`;
415
945
  };
416
946
  const isGlobPattern = (pattern) => {
417
947
  return /[*?[\]{}]/.test(pattern);
@@ -449,19 +979,9 @@ Examples:
449
979
  soda-gql format --check # Check mode with config
450
980
  `;
451
981
  const formatCommand = async (argv) => {
452
- if (argv.includes("--help") || argv.includes("-h")) {
453
- process.stdout.write(FORMAT_HELP);
454
- return 0;
455
- }
982
+ if (argv.includes("--help") || argv.includes("-h")) return (0, neverthrow.ok)({ message: FORMAT_HELP });
456
983
  const parsed = parseArgs([...argv], FormatArgsSchema);
457
- if (!parsed.isOk()) {
458
- const error = {
459
- code: "PARSE_ERROR",
460
- message: parsed.error
461
- };
462
- process.stderr.write(`${formatFormatError(error)}\n`);
463
- return 1;
464
- }
984
+ if (!parsed.isOk()) return (0, neverthrow.err)(cliErrors.argsInvalid("format", parsed.error));
465
985
  const args = parsed.value;
466
986
  const isCheckMode = args.check === true;
467
987
  const explicitPatterns = args._ ?? [];
@@ -470,36 +990,27 @@ const formatCommand = async (argv) => {
470
990
  if (explicitPatterns.length > 0) targetPatterns = explicitPatterns;
471
991
  else {
472
992
  const configResult = (0, __soda_gql_config.loadConfig)(args.config);
473
- if (configResult.isErr()) {
474
- process.stderr.write(`${formatFormatError({
475
- code: "NO_PATTERNS",
476
- message: "No patterns provided and config not found. Usage: soda-gql format [patterns...] [--check]"
477
- })}\n`);
478
- return 1;
479
- }
993
+ if (configResult.isErr()) return (0, neverthrow.err)(cliErrors.noPatterns());
480
994
  targetPatterns = configResult.value.include;
481
995
  excludePatterns = configResult.value.exclude;
482
996
  }
483
997
  const formatter = await loadFormatter();
484
- if (!formatter) {
485
- process.stderr.write(`${formatFormatError({
486
- code: "FORMATTER_NOT_INSTALLED",
487
- message: "@soda-gql/formatter is not installed. Run: npm install @soda-gql/formatter"
488
- })}\n`);
489
- return 1;
490
- }
998
+ if (!formatter) return (0, neverthrow.err)(cliErrors.formatterNotInstalled());
491
999
  const files = await expandGlobPatterns(targetPatterns, excludePatterns);
492
1000
  if (files.length === 0) {
493
- const result$1 = {
1001
+ const data$1 = {
494
1002
  mode: isCheckMode ? "check" : "format",
495
1003
  total: 0,
496
1004
  modified: 0,
497
1005
  unchanged: 0,
498
1006
  errors: 0,
499
- unformatted: []
1007
+ unformatted: [],
1008
+ hasFormattingIssues: false
500
1009
  };
501
- process.stdout.write(`${formatResult(result$1)}\n`);
502
- return 0;
1010
+ return (0, neverthrow.ok)({
1011
+ message: formatResultMessage(data$1),
1012
+ data: data$1
1013
+ });
503
1014
  }
504
1015
  let modified = 0;
505
1016
  let unchanged = 0;
@@ -508,44 +1019,46 @@ const formatCommand = async (argv) => {
508
1019
  for (const filePath of files) {
509
1020
  const sourceCode = await (0, node_fs_promises.readFile)(filePath, "utf-8");
510
1021
  if (isCheckMode) {
511
- const result$1 = formatter.needsFormat({
1022
+ const result = formatter.needsFormat({
512
1023
  sourceCode,
513
1024
  filePath
514
1025
  });
515
- if (result$1.isErr()) {
1026
+ if (result.isErr()) {
516
1027
  errors++;
517
1028
  continue;
518
1029
  }
519
- if (result$1.value) {
1030
+ if (result.value) {
520
1031
  unformatted.push(filePath);
521
1032
  modified++;
522
1033
  } else unchanged++;
523
1034
  } else {
524
- const result$1 = formatter.format({
1035
+ const result = formatter.format({
525
1036
  sourceCode,
526
1037
  filePath
527
1038
  });
528
- if (result$1.isErr()) {
1039
+ if (result.isErr()) {
529
1040
  errors++;
530
1041
  continue;
531
1042
  }
532
- if (result$1.value.modified) {
533
- await (0, node_fs_promises.writeFile)(filePath, result$1.value.sourceCode, "utf-8");
1043
+ if (result.value.modified) {
1044
+ await (0, node_fs_promises.writeFile)(filePath, result.value.sourceCode, "utf-8");
534
1045
  modified++;
535
1046
  } else unchanged++;
536
1047
  }
537
1048
  }
538
- const result = {
1049
+ const data = {
539
1050
  mode: isCheckMode ? "check" : "format",
540
1051
  total: files.length,
541
1052
  modified,
542
1053
  unchanged,
543
1054
  errors,
544
- unformatted
1055
+ unformatted,
1056
+ hasFormattingIssues: isCheckMode && unformatted.length > 0 || errors > 0
545
1057
  };
546
- process.stdout.write(`${formatResult(result)}\n`);
547
- if (isCheckMode && unformatted.length > 0) return 1;
548
- return errors > 0 ? 1 : 0;
1058
+ return (0, neverthrow.ok)({
1059
+ message: formatResultMessage(data),
1060
+ data
1061
+ });
549
1062
  };
550
1063
 
551
1064
  //#endregion
@@ -619,11 +1132,7 @@ Generated files:
619
1132
  `;
620
1133
  const checkFilesExist = (files, force) => {
621
1134
  if (force) return (0, neverthrow.ok)(void 0);
622
- for (const file of files) if ((0, node_fs.existsSync)(file.path)) return (0, neverthrow.err)({
623
- code: "FILE_EXISTS",
624
- message: `File already exists: ${file.path}. Use --force to overwrite.`,
625
- filePath: file.path
626
- });
1135
+ for (const file of files) if ((0, node_fs.existsSync)(file.path)) return (0, neverthrow.err)(cliErrors.fileExists(file.path));
627
1136
  return (0, neverthrow.ok)(void 0);
628
1137
  };
629
1138
  const writeFiles = (files) => {
@@ -634,11 +1143,7 @@ const writeFiles = (files) => {
634
1143
  createdPaths.push(file.path);
635
1144
  } catch (error) {
636
1145
  const message = error instanceof Error ? error.message : String(error);
637
- return (0, neverthrow.err)({
638
- code: "WRITE_FAILED",
639
- message: `Failed to write ${file.description}: ${message}`,
640
- filePath: file.path
641
- });
1146
+ return (0, neverthrow.err)(cliErrors.writeFailed(file.path, `Failed to write ${file.description}: ${message}`, error));
642
1147
  }
643
1148
  return (0, neverthrow.ok)({ filesCreated: createdPaths });
644
1149
  };
@@ -655,23 +1160,10 @@ const formatSuccess = (result) => {
655
1160
  lines.push(" 3. Import gql from ./graphql-system");
656
1161
  return lines.join("\n");
657
1162
  };
658
- const formatInitError = (error) => {
659
- return `${error.code}: ${error.message}`;
660
- };
661
1163
  const initCommand = async (argv) => {
662
- if (argv.includes("--help") || argv.includes("-h")) {
663
- process.stdout.write(INIT_HELP);
664
- return 0;
665
- }
1164
+ if (argv.includes("--help") || argv.includes("-h")) return (0, neverthrow.ok)({ message: INIT_HELP });
666
1165
  const parsed = parseArgs([...argv], InitArgsSchema);
667
- if (!parsed.isOk()) {
668
- const error = {
669
- code: "PARSE_ERROR",
670
- message: parsed.error
671
- };
672
- process.stderr.write(`${formatInitError(error)}\n`);
673
- return 1;
674
- }
1166
+ if (!parsed.isOk()) return (0, neverthrow.err)(cliErrors.argsInvalid("init", parsed.error));
675
1167
  const force = parsed.value.force === true;
676
1168
  const cwd = process.cwd();
677
1169
  const files = [
@@ -697,54 +1189,192 @@ const initCommand = async (argv) => {
697
1189
  }
698
1190
  ];
699
1191
  const existsCheck = checkFilesExist(files, force);
700
- if (existsCheck.isErr()) {
701
- process.stderr.write(`${formatInitError(existsCheck.error)}\n`);
702
- return 1;
703
- }
1192
+ if (existsCheck.isErr()) return (0, neverthrow.err)(existsCheck.error);
704
1193
  const writeResult = writeFiles(files);
705
- if (writeResult.isErr()) {
706
- process.stderr.write(`${formatInitError(writeResult.error)}\n`);
707
- return 1;
708
- }
709
- process.stdout.write(`${formatSuccess(writeResult.value)}\n`);
710
- return 0;
1194
+ if (writeResult.isErr()) return (0, neverthrow.err)(writeResult.error);
1195
+ return (0, neverthrow.ok)({
1196
+ message: formatSuccess(writeResult.value),
1197
+ data: writeResult.value
1198
+ });
711
1199
  };
712
1200
 
713
1201
  //#endregion
714
1202
  //#region packages/cli/src/utils/format.ts
715
- const formatError = (error, format = "human") => {
716
- if (format === "json") return JSON.stringify({ error }, null, 2);
717
- return error instanceof Error ? error.message : String(error);
1203
+ /**
1204
+ * CLI-specific error hints to help users fix issues.
1205
+ */
1206
+ const cliErrorHints = {
1207
+ CLI_ARGS_INVALID: "Check command usage with --help",
1208
+ CLI_UNKNOWN_COMMAND: "Run 'soda-gql --help' for available commands",
1209
+ CLI_UNKNOWN_SUBCOMMAND: "Run the parent command with --help for available subcommands",
1210
+ CLI_FILE_EXISTS: "Use --force flag to overwrite existing files",
1211
+ CLI_FILE_NOT_FOUND: "Verify the file path exists",
1212
+ CLI_WRITE_FAILED: "Check write permissions and disk space",
1213
+ CLI_READ_FAILED: "Check file permissions and verify the file is not locked",
1214
+ CLI_NO_PATTERNS: "Provide file patterns or create soda-gql.config.ts",
1215
+ CLI_FORMATTER_NOT_INSTALLED: "Install with: bun add @soda-gql/formatter",
1216
+ CLI_PARSE_ERROR: "Check the file for syntax errors",
1217
+ CLI_FORMAT_ERROR: "Verify the file contains valid soda-gql code",
1218
+ CLI_UNEXPECTED: "This is an unexpected error. Please report at https://github.com/soda-gql/soda-gql/issues"
1219
+ };
1220
+ /**
1221
+ * Codegen-specific error hints.
1222
+ */
1223
+ const codegenErrorHints = {
1224
+ SCHEMA_NOT_FOUND: "Verify the schema path in soda-gql.config.ts",
1225
+ SCHEMA_INVALID: "Check your GraphQL schema for syntax errors",
1226
+ INJECT_MODULE_NOT_FOUND: "Run: soda-gql codegen --emit-inject-template <path>",
1227
+ INJECT_MODULE_REQUIRED: "Add inject configuration to your schema in soda-gql.config.ts",
1228
+ INJECT_TEMPLATE_EXISTS: "Delete the existing file to regenerate, or use a different path",
1229
+ EMIT_FAILED: "Check write permissions and that the output directory exists",
1230
+ INJECT_TEMPLATE_FAILED: "Check write permissions for the output path"
1231
+ };
1232
+ /**
1233
+ * Config-specific error hints.
1234
+ */
1235
+ const configErrorHints = {
1236
+ CONFIG_NOT_FOUND: "Create a soda-gql.config.ts file in your project root",
1237
+ CONFIG_LOAD_FAILED: "Check your configuration file for syntax errors",
1238
+ CONFIG_VALIDATION_FAILED: "Verify your configuration matches the expected schema",
1239
+ CONFIG_INVALID_PATH: "Verify the path in your configuration exists"
1240
+ };
1241
+ /**
1242
+ * Artifact-specific error hints.
1243
+ */
1244
+ const artifactErrorHints = {
1245
+ ARTIFACT_NOT_FOUND: "Verify the artifact file path exists",
1246
+ ARTIFACT_PARSE_ERROR: "Check that the artifact file is valid JSON",
1247
+ ARTIFACT_VALIDATION_ERROR: "Verify the artifact was built with a compatible version of soda-gql"
1248
+ };
1249
+ /**
1250
+ * Get hint for any error type.
1251
+ */
1252
+ const getErrorHint = (error) => {
1253
+ if (error.category === "cli") return cliErrorHints[error.code];
1254
+ if (error.category === "codegen") return codegenErrorHints[error.error.code];
1255
+ if (error.category === "config") return configErrorHints[error.error.code];
1256
+ if (error.category === "artifact") return artifactErrorHints[error.error.code];
1257
+ };
1258
+ /**
1259
+ * Format CliError to human-readable string with hints.
1260
+ */
1261
+ const formatCliErrorHuman = (error) => {
1262
+ if (error.category === "builder") return (0, __soda_gql_builder.formatBuilderErrorForCLI)(error.error);
1263
+ const lines = [];
1264
+ if (error.category === "codegen") {
1265
+ const codegenError = error.error;
1266
+ lines.push(`Error [${codegenError.code}]: ${codegenError.message}`);
1267
+ if ("schemaPath" in codegenError) lines.push(` Schema: ${codegenError.schemaPath}`);
1268
+ if ("outPath" in codegenError && codegenError.outPath) lines.push(` Output: ${codegenError.outPath}`);
1269
+ if ("injectPath" in codegenError) lines.push(` Inject: ${codegenError.injectPath}`);
1270
+ } else if (error.category === "config") {
1271
+ const configError = error.error;
1272
+ lines.push(`Error [${configError.code}]: ${configError.message}`);
1273
+ if (configError.filePath) lines.push(` Config: ${configError.filePath}`);
1274
+ } else if (error.category === "artifact") {
1275
+ const artifactError = error.error;
1276
+ lines.push(`Error [${artifactError.code}]: ${artifactError.message}`);
1277
+ if (artifactError.filePath) lines.push(` Artifact: ${artifactError.filePath}`);
1278
+ } else {
1279
+ lines.push(`Error [${error.code}]: ${error.message}`);
1280
+ if ("filePath" in error && error.filePath) lines.push(` File: ${error.filePath}`);
1281
+ if ("command" in error && error.code !== "CLI_UNKNOWN_COMMAND") lines.push(` Command: ${error.command}`);
1282
+ if ("parent" in error) lines.push(` Parent: ${error.parent}`);
1283
+ }
1284
+ const hint = getErrorHint(error);
1285
+ if (hint) {
1286
+ lines.push("");
1287
+ lines.push(` Hint: ${hint}`);
1288
+ }
1289
+ return lines.join("\n");
1290
+ };
1291
+ /**
1292
+ * Format CliError to JSON string.
1293
+ */
1294
+ const formatCliErrorJson = (error) => {
1295
+ if (error.category === "cli") {
1296
+ const { category: _category, ...rest } = error;
1297
+ return JSON.stringify({ error: rest }, null, 2);
1298
+ }
1299
+ return JSON.stringify({ error: error.error }, null, 2);
1300
+ };
1301
+ /**
1302
+ * Format CliError with output format preference.
1303
+ */
1304
+ const formatCliError = (error, format = "human") => {
1305
+ return format === "json" ? formatCliErrorJson(error) : formatCliErrorHuman(error);
718
1306
  };
719
1307
 
720
1308
  //#endregion
721
1309
  //#region packages/cli/src/index.ts
1310
+ const MAIN_HELP = `Usage: soda-gql <command> [options]
1311
+
1312
+ Commands:
1313
+ init Initialize a new soda-gql project
1314
+ codegen Generate graphql-system runtime module
1315
+ format Format soda-gql field selections
1316
+ artifact Manage soda-gql artifacts
1317
+ doctor Run diagnostic checks
1318
+
1319
+ Run 'soda-gql <command> --help' for more information on a specific command.
1320
+ `;
1321
+ /**
1322
+ * Parse output format from argv.
1323
+ * Returns "json" if --format=json or --json flag is present, otherwise "human".
1324
+ */
1325
+ const getOutputFormat = (argv) => {
1326
+ for (const arg of argv) {
1327
+ if (arg === "--format=json" || arg === "--json") return "json";
1328
+ if (arg === "--format=human") return "human";
1329
+ }
1330
+ return "human";
1331
+ };
722
1332
  const dispatch = async (argv) => {
723
1333
  const [command, ...rest] = argv;
724
- if (!command || command === "--help" || command === "-h") {
725
- process.stdout.write(`Usage: soda-gql <command> [options]\n`);
726
- process.stdout.write(`\nCommands:\n`);
727
- process.stdout.write(` init Initialize a new soda-gql project\n`);
728
- process.stdout.write(` codegen Generate graphql-system runtime module\n`);
729
- process.stdout.write(` format Format soda-gql field selections\n`);
730
- process.stdout.write(` artifact Manage soda-gql artifacts\n`);
731
- return 0;
732
- }
1334
+ if (!command || command === "--help" || command === "-h") return (0, neverthrow.ok)({ message: MAIN_HELP });
733
1335
  if (command === "init") return initCommand(rest);
734
1336
  if (command === "codegen") return codegenCommand(rest);
735
- if (command === "format") return formatCommand(rest);
1337
+ if (command === "format") {
1338
+ const result = await formatCommand(rest);
1339
+ if (result.isOk()) {
1340
+ const exitCode = result.value.data?.hasFormattingIssues ? 1 : 0;
1341
+ return (0, neverthrow.ok)({
1342
+ ...result.value,
1343
+ exitCode
1344
+ });
1345
+ }
1346
+ return (0, neverthrow.err)(result.error);
1347
+ }
736
1348
  if (command === "artifact") return artifactCommand(rest);
737
- process.stderr.write(`Unknown command: ${command}\n`);
738
- return 1;
739
- };
740
- dispatch(process.argv.slice(2)).then((exitCode) => {
741
- process.exitCode = exitCode;
742
- }).catch((error) => {
743
- const unexpectedError = {
744
- code: "UNEXPECTED_ERROR",
745
- message: error instanceof Error ? error.message : String(error)
746
- };
747
- process.stderr.write(`${formatError(unexpectedError, "json")}\n`);
1349
+ if (command === "doctor") {
1350
+ const result = doctorCommand(rest);
1351
+ if (result.isOk()) {
1352
+ const exitCode = result.value.data?.issueCount ? 1 : 0;
1353
+ return (0, neverthrow.ok)({
1354
+ ...result.value,
1355
+ exitCode
1356
+ });
1357
+ }
1358
+ return result;
1359
+ }
1360
+ return (0, neverthrow.err)(cliErrors.unknownCommand(command));
1361
+ };
1362
+ const main = async () => {
1363
+ const argv = process.argv.slice(2);
1364
+ const format = getOutputFormat(argv);
1365
+ const result = await dispatch(argv);
1366
+ if (result.isOk()) {
1367
+ process.stdout.write(`${result.value.message}\n`);
1368
+ process.exitCode = result.value.exitCode ?? 0;
1369
+ } else {
1370
+ process.stderr.write(`${formatCliError(result.error, format)}\n`);
1371
+ process.exitCode = 1;
1372
+ }
1373
+ };
1374
+ main().catch((error) => {
1375
+ const unexpectedError = cliErrors.unexpected(error instanceof Error ? error.message : String(error), error);
1376
+ const format = getOutputFormat(process.argv.slice(2));
1377
+ process.stderr.write(`${formatCliError(unexpectedError, format)}\n`);
748
1378
  process.exitCode = 1;
749
1379
  });
750
1380