@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 +878 -248
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +102 -1
- package/dist/index.d.cts.map +1 -1
- package/package.json +4 -4
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
338
|
-
|
|
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
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const
|
|
403
|
-
|
|
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 ${
|
|
938
|
+
return `All ${data.total} file(s) are properly formatted`;
|
|
409
939
|
}
|
|
410
940
|
const parts = [];
|
|
411
|
-
if (
|
|
412
|
-
if (
|
|
413
|
-
if (
|
|
414
|
-
return `${
|
|
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
|
|
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
|
-
|
|
502
|
-
|
|
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
|
|
1022
|
+
const result = formatter.needsFormat({
|
|
512
1023
|
sourceCode,
|
|
513
1024
|
filePath
|
|
514
1025
|
});
|
|
515
|
-
if (result
|
|
1026
|
+
if (result.isErr()) {
|
|
516
1027
|
errors++;
|
|
517
1028
|
continue;
|
|
518
1029
|
}
|
|
519
|
-
if (result
|
|
1030
|
+
if (result.value) {
|
|
520
1031
|
unformatted.push(filePath);
|
|
521
1032
|
modified++;
|
|
522
1033
|
} else unchanged++;
|
|
523
1034
|
} else {
|
|
524
|
-
const result
|
|
1035
|
+
const result = formatter.format({
|
|
525
1036
|
sourceCode,
|
|
526
1037
|
filePath
|
|
527
1038
|
});
|
|
528
|
-
if (result
|
|
1039
|
+
if (result.isErr()) {
|
|
529
1040
|
errors++;
|
|
530
1041
|
continue;
|
|
531
1042
|
}
|
|
532
|
-
if (result
|
|
533
|
-
await (0, node_fs_promises.writeFile)(filePath, result
|
|
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
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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")
|
|
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
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
|