@apso/cli 0.13.0 → 0.14.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/commands/doctor.d.ts +10 -0
- package/dist/commands/doctor.js +127 -0
- package/dist/commands/mcp/serve.js +131 -2
- package/dist/lib/doctor/checks.d.ts +35 -0
- package/dist/lib/doctor/checks.js +222 -0
- package/dist/lib/doctor/index.d.ts +4 -0
- package/dist/lib/doctor/index.js +14 -0
- package/dist/lib/doctor/runner.d.ts +29 -0
- package/dist/lib/doctor/runner.js +175 -0
- package/dist/lib/doctor/types.d.ts +35 -0
- package/dist/lib/doctor/types.js +2 -0
- package/npm-shrinkwrap.json +2 -2
- package/oclif.manifest.json +30 -1
- package/package.json +1 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import BaseCommand from "../lib/base-command";
|
|
2
|
+
export default class Doctor extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
report: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
};
|
|
9
|
+
run(): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const path = tslib_1.__importStar(require("path"));
|
|
5
|
+
const core_1 = require("@oclif/core");
|
|
6
|
+
const inquirer = tslib_1.__importStar(require("inquirer"));
|
|
7
|
+
const base_command_1 = tslib_1.__importDefault(require("../lib/base-command"));
|
|
8
|
+
const apsorc_parser_1 = require("../lib/apsorc-parser");
|
|
9
|
+
const runner_1 = require("../lib/doctor/runner");
|
|
10
|
+
class Doctor extends base_command_1.default {
|
|
11
|
+
async run() {
|
|
12
|
+
const { flags } = await this.parse(Doctor);
|
|
13
|
+
// Find and parse .apsorc
|
|
14
|
+
const configPath = (0, apsorc_parser_1.findConfigPath)();
|
|
15
|
+
if (!configPath) {
|
|
16
|
+
this.error("No .apsorc file found in the current directory or parent directories.");
|
|
17
|
+
}
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = (0, apsorc_parser_1.parseApsorc)();
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
24
|
+
this.error(`Failed to parse .apsorc: ${msg}`);
|
|
25
|
+
}
|
|
26
|
+
const projectRoot = path.dirname(configPath);
|
|
27
|
+
const language = parsed.language || "typescript";
|
|
28
|
+
const ctx = {
|
|
29
|
+
entities: parsed.entities,
|
|
30
|
+
relationshipMap: parsed.relationshipMap,
|
|
31
|
+
rootFolder: parsed.rootFolder || "src",
|
|
32
|
+
language,
|
|
33
|
+
projectRoot,
|
|
34
|
+
};
|
|
35
|
+
if (!flags.json) {
|
|
36
|
+
this.log("Running diagnostics...");
|
|
37
|
+
}
|
|
38
|
+
const findings = await (0, runner_1.runDiagnostics)(ctx);
|
|
39
|
+
// Output
|
|
40
|
+
if (flags.json) {
|
|
41
|
+
this.log(JSON.stringify(findings, null, 2));
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
this.log("");
|
|
45
|
+
this.log((0, runner_1.formatFindings)(findings));
|
|
46
|
+
const errors = findings.filter((f) => f.severity === "error").length;
|
|
47
|
+
const warnings = findings.filter((f) => f.severity === "warning").length;
|
|
48
|
+
this.log(`${findings.length} finding(s): ${errors} error(s), ${warnings} warning(s)`);
|
|
49
|
+
}
|
|
50
|
+
// Report flow
|
|
51
|
+
if (flags.report) {
|
|
52
|
+
const actionable = findings.filter((f) => f.severity === "error" || f.severity === "warning");
|
|
53
|
+
if (actionable.length === 0) {
|
|
54
|
+
this.log("No errors or warnings to report.");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (!(0, runner_1.isGhAvailable)()) {
|
|
58
|
+
this.error("GitHub CLI (gh) is not installed or not authenticated.\n" +
|
|
59
|
+
"Install: https://cli.github.com\n" +
|
|
60
|
+
"Authenticate: gh auth login");
|
|
61
|
+
}
|
|
62
|
+
// Dedupe check
|
|
63
|
+
const firstError = actionable.find((f) => f.severity === "error") || actionable[0];
|
|
64
|
+
const searchTitle = firstError.message.slice(0, 100);
|
|
65
|
+
const existing = (0, runner_1.searchExistingIssues)(searchTitle);
|
|
66
|
+
if (existing.length > 0) {
|
|
67
|
+
this.log("");
|
|
68
|
+
this.log("Potentially related open issues:");
|
|
69
|
+
for (const issue of existing) {
|
|
70
|
+
this.log(` #${issue.number}: ${issue.title}`);
|
|
71
|
+
this.log(` ${issue.url}`);
|
|
72
|
+
}
|
|
73
|
+
this.log("");
|
|
74
|
+
const { proceed } = await inquirer.prompt([
|
|
75
|
+
{
|
|
76
|
+
type: "confirm",
|
|
77
|
+
name: "proceed",
|
|
78
|
+
message: "File a new issue anyway?",
|
|
79
|
+
default: false,
|
|
80
|
+
},
|
|
81
|
+
]);
|
|
82
|
+
if (!proceed) {
|
|
83
|
+
this.log("Aborted.");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Confirm before filing
|
|
88
|
+
const { confirm } = await inquirer.prompt([
|
|
89
|
+
{
|
|
90
|
+
type: "confirm",
|
|
91
|
+
name: "confirm",
|
|
92
|
+
message: `File a GitHub issue with ${actionable.length} finding(s) against apsoai/cli?`,
|
|
93
|
+
default: true,
|
|
94
|
+
},
|
|
95
|
+
]);
|
|
96
|
+
if (!confirm) {
|
|
97
|
+
this.log("Aborted.");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const url = (0, runner_1.fileGitHubIssue)(findings, ctx);
|
|
102
|
+
this.log(`Issue filed: ${url}`);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
106
|
+
this.error(`Failed to file issue: ${msg}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
Doctor.description = "Run diagnostic checks on generated code and optionally file a GitHub issue with findings";
|
|
112
|
+
Doctor.examples = [
|
|
113
|
+
`$ apso doctor`,
|
|
114
|
+
`$ apso doctor --json`,
|
|
115
|
+
`$ apso doctor --report`,
|
|
116
|
+
];
|
|
117
|
+
Doctor.flags = {
|
|
118
|
+
report: core_1.Flags.boolean({
|
|
119
|
+
description: "File a GitHub issue with diagnostic findings (requires gh CLI)",
|
|
120
|
+
default: false,
|
|
121
|
+
}),
|
|
122
|
+
json: core_1.Flags.boolean({
|
|
123
|
+
description: "Output findings as JSON instead of human-readable text",
|
|
124
|
+
default: false,
|
|
125
|
+
}),
|
|
126
|
+
};
|
|
127
|
+
exports.default = Doctor;
|
|
@@ -10,6 +10,7 @@ const zod_1 = require("zod");
|
|
|
10
10
|
const base_command_1 = tslib_1.__importDefault(require("../../lib/base-command"));
|
|
11
11
|
const apsorc_parser_1 = require("../../lib/apsorc-parser");
|
|
12
12
|
const lib_1 = require("../../lib");
|
|
13
|
+
const runner_1 = require("../../lib/doctor/runner");
|
|
13
14
|
const file_system_1 = require("../../lib/utils/file-system");
|
|
14
15
|
const schema_convert_1 = require("../../lib/utils/schema-convert");
|
|
15
16
|
const perf_hooks_1 = require("perf_hooks");
|
|
@@ -626,6 +627,133 @@ class McpServe extends base_command_1.default {
|
|
|
626
627
|
],
|
|
627
628
|
};
|
|
628
629
|
});
|
|
630
|
+
// ── diagnose ──────────────────────────────────────────────────
|
|
631
|
+
server.tool("diagnose", "Run diagnostic checks on the current project's generated code. Detects issues like missing PK generation strategies, unused imports, unrecognized field types, broken relationships, and migration failures.", {}, async () => {
|
|
632
|
+
try {
|
|
633
|
+
const configPath = (0, apsorc_parser_1.findConfigPath)();
|
|
634
|
+
if (!configPath) {
|
|
635
|
+
return {
|
|
636
|
+
content: [
|
|
637
|
+
{
|
|
638
|
+
type: "text",
|
|
639
|
+
text: "No .apsorc file found in the current directory or parent directories.",
|
|
640
|
+
},
|
|
641
|
+
],
|
|
642
|
+
isError: true,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
const parsed = (0, apsorc_parser_1.parseApsorc)();
|
|
646
|
+
const projectRoot = path.dirname(configPath);
|
|
647
|
+
const language = parsed.language || "typescript";
|
|
648
|
+
const ctx = {
|
|
649
|
+
entities: parsed.entities,
|
|
650
|
+
relationshipMap: parsed.relationshipMap,
|
|
651
|
+
rootFolder: parsed.rootFolder || "src",
|
|
652
|
+
language,
|
|
653
|
+
projectRoot,
|
|
654
|
+
};
|
|
655
|
+
const findings = await (0, runner_1.runDiagnostics)(ctx);
|
|
656
|
+
if (findings.length === 0) {
|
|
657
|
+
return {
|
|
658
|
+
content: [
|
|
659
|
+
{
|
|
660
|
+
type: "text",
|
|
661
|
+
text: "All diagnostic checks passed. No issues found.",
|
|
662
|
+
},
|
|
663
|
+
],
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
const text = (0, runner_1.formatFindings)(findings);
|
|
667
|
+
const errors = findings.filter((f) => f.severity === "error").length;
|
|
668
|
+
const warnings = findings.filter((f) => f.severity === "warning").length;
|
|
669
|
+
return {
|
|
670
|
+
content: [
|
|
671
|
+
{
|
|
672
|
+
type: "text",
|
|
673
|
+
text: `${text}\n${findings.length} finding(s): ${errors} error(s), ${warnings} warning(s)\n\nUse the report_issue tool to file a GitHub issue with these findings.`,
|
|
674
|
+
},
|
|
675
|
+
],
|
|
676
|
+
isError: errors > 0,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
catch (error) {
|
|
680
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
681
|
+
return {
|
|
682
|
+
content: [
|
|
683
|
+
{
|
|
684
|
+
type: "text",
|
|
685
|
+
text: `Diagnostic error: ${msg}`,
|
|
686
|
+
},
|
|
687
|
+
],
|
|
688
|
+
isError: true,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
// ── report_issue ──────────────────────────────────────────────
|
|
693
|
+
server.tool("report_issue", "File a GitHub issue against apsoai/cli with diagnostic findings or a custom description. Deduplicates against open issues. Requires the gh CLI to be installed and authenticated.", {
|
|
694
|
+
title: zod_1.z.string().describe("Issue title"),
|
|
695
|
+
body: zod_1.z.string().describe("Issue body (markdown)"),
|
|
696
|
+
confirm: zod_1.z
|
|
697
|
+
.boolean()
|
|
698
|
+
.default(false)
|
|
699
|
+
.describe("Set to true to actually create the issue. When false, returns a preview and any duplicate issues found."),
|
|
700
|
+
}, async ({ title, body, confirm }) => {
|
|
701
|
+
try {
|
|
702
|
+
if (!(0, runner_1.isGhAvailable)()) {
|
|
703
|
+
return {
|
|
704
|
+
content: [
|
|
705
|
+
{
|
|
706
|
+
type: "text",
|
|
707
|
+
text: "GitHub CLI (gh) is not installed or not authenticated.\nInstall: https://cli.github.com\nAuthenticate: gh auth login",
|
|
708
|
+
},
|
|
709
|
+
],
|
|
710
|
+
isError: true,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
// Check for duplicates
|
|
714
|
+
const existing = (0, runner_1.searchExistingIssues)(title);
|
|
715
|
+
const dupeInfo = existing.length > 0
|
|
716
|
+
? `\n\nPotentially related open issues:\n${existing.map((i) => ` #${i.number}: ${i.title} (${i.url})`).join("\n")}`
|
|
717
|
+
: "";
|
|
718
|
+
if (!confirm) {
|
|
719
|
+
return {
|
|
720
|
+
content: [
|
|
721
|
+
{
|
|
722
|
+
type: "text",
|
|
723
|
+
text: `Issue preview:\n\nTitle: ${title}\n\n${body}${dupeInfo}\n\nCall report_issue again with confirm: true to file this issue.`,
|
|
724
|
+
},
|
|
725
|
+
],
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
// File the issue
|
|
729
|
+
const { execSync } = await Promise.resolve().then(() => tslib_1.__importStar(require("child_process")));
|
|
730
|
+
const url = execSync(`gh issue create --repo apsoai/cli --title "${title.replace(/"/g, '\\"')}" --body-file -`, {
|
|
731
|
+
input: body,
|
|
732
|
+
encoding: "utf-8",
|
|
733
|
+
timeout: 30000,
|
|
734
|
+
}).trim();
|
|
735
|
+
return {
|
|
736
|
+
content: [
|
|
737
|
+
{
|
|
738
|
+
type: "text",
|
|
739
|
+
text: `Issue filed: ${url}${dupeInfo}`,
|
|
740
|
+
},
|
|
741
|
+
],
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
catch (error) {
|
|
745
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
746
|
+
return {
|
|
747
|
+
content: [
|
|
748
|
+
{
|
|
749
|
+
type: "text",
|
|
750
|
+
text: `Failed to file issue: ${msg}`,
|
|
751
|
+
},
|
|
752
|
+
],
|
|
753
|
+
isError: true,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
});
|
|
629
757
|
}
|
|
630
758
|
registerResources(server) {
|
|
631
759
|
// ── Schema guide resource ──────────────────────────────────────
|
|
@@ -717,8 +845,9 @@ function validateSchemaObject(schema) {
|
|
|
717
845
|
return errors;
|
|
718
846
|
}
|
|
719
847
|
const validFieldTypes = [
|
|
720
|
-
"text", "integer", "float", "decimal", "numeric",
|
|
721
|
-
"boolean", "date", "
|
|
848
|
+
"text", "string", "varchar", "integer", "float", "decimal", "numeric",
|
|
849
|
+
"boolean", "date", "timestamp", "datetime", "uuid", "enum", "json",
|
|
850
|
+
"json-plain", "array",
|
|
722
851
|
];
|
|
723
852
|
const validRelTypes = ["OneToMany", "ManyToOne", "ManyToMany", "OneToOne"];
|
|
724
853
|
const entityNames = new Set();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DiagnosticFinding, DiagnosticContext } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Check that UUID primary keys use @PrimaryGeneratedColumn('uuid')
|
|
4
|
+
* instead of @PrimaryColumn() in generated entity files.
|
|
5
|
+
*
|
|
6
|
+
* Issue #59: uuid PK generated with no generation strategy causes
|
|
7
|
+
* runtime errors because the database never generates a value.
|
|
8
|
+
*/
|
|
9
|
+
export declare function checkPkStrategy(ctx: DiagnosticContext): DiagnosticFinding[];
|
|
10
|
+
/**
|
|
11
|
+
* Check for unused imports in generated entity files.
|
|
12
|
+
*
|
|
13
|
+
* The code generator sometimes imports symbols (like Generated, Index)
|
|
14
|
+
* that aren't used in the entity body.
|
|
15
|
+
*/
|
|
16
|
+
export declare function checkUnusedImports(ctx: DiagnosticContext): DiagnosticFinding[];
|
|
17
|
+
/**
|
|
18
|
+
* Check that each field type in the schema is recognized by the generator.
|
|
19
|
+
*
|
|
20
|
+
* Uses the canonical fieldTypeToColumnType map as the source of truth.
|
|
21
|
+
*/
|
|
22
|
+
export declare function checkFieldTypeMismatch(ctx: DiagnosticContext): DiagnosticFinding[];
|
|
23
|
+
/**
|
|
24
|
+
* Check that relationship targets reference existing entities.
|
|
25
|
+
*
|
|
26
|
+
* This overlaps with validate_schema but is included for completeness
|
|
27
|
+
* in the diagnostic suite.
|
|
28
|
+
*/
|
|
29
|
+
export declare function checkRelationshipTarget(ctx: DiagnosticContext): DiagnosticFinding[];
|
|
30
|
+
/**
|
|
31
|
+
* Check that generated schema can sync in PGlite without errors.
|
|
32
|
+
*
|
|
33
|
+
* This is an async check that imports and runs the migration sandbox.
|
|
34
|
+
*/
|
|
35
|
+
export declare function checkMigrationSandbox(ctx: DiagnosticContext): Promise<DiagnosticFinding[]>;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkMigrationSandbox = exports.checkRelationshipTarget = exports.checkFieldTypeMismatch = exports.checkUnusedImports = exports.checkPkStrategy = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
6
|
+
const path = tslib_1.__importStar(require("path"));
|
|
7
|
+
const field_1 = require("../utils/field");
|
|
8
|
+
const casing_1 = require("../utils/casing");
|
|
9
|
+
/**
|
|
10
|
+
* Check that UUID primary keys use @PrimaryGeneratedColumn('uuid')
|
|
11
|
+
* instead of @PrimaryColumn() in generated entity files.
|
|
12
|
+
*
|
|
13
|
+
* Issue #59: uuid PK generated with no generation strategy causes
|
|
14
|
+
* runtime errors because the database never generates a value.
|
|
15
|
+
*/
|
|
16
|
+
function checkPkStrategy(ctx) {
|
|
17
|
+
var _a, _b;
|
|
18
|
+
const findings = [];
|
|
19
|
+
for (const entity of ctx.entities) {
|
|
20
|
+
if (entity.primaryKeyType !== "uuid")
|
|
21
|
+
continue;
|
|
22
|
+
// Check if any field is explicitly marked as primary (custom PK)
|
|
23
|
+
const hasCustomPk = (_b = (_a = entity.fields) === null || _a === void 0 ? void 0 : _a.some((f) => f.primary)) !== null && _b !== void 0 ? _b : false;
|
|
24
|
+
if (hasCustomPk)
|
|
25
|
+
continue;
|
|
26
|
+
// Look for the generated entity file
|
|
27
|
+
const entityFileName = `${entity.name}.entity.ts`;
|
|
28
|
+
const entityDir = (0, casing_1.snakeCase)(entity.name);
|
|
29
|
+
const entityPath = path.join(ctx.projectRoot, ctx.rootFolder, "autogen", entityDir, entityFileName);
|
|
30
|
+
if (!fs.existsSync(entityPath)) {
|
|
31
|
+
findings.push({
|
|
32
|
+
check: "pk-strategy",
|
|
33
|
+
severity: "warning",
|
|
34
|
+
message: `Entity "${entity.name}" has primaryKeyType "uuid" but generated file not found`,
|
|
35
|
+
entity: entity.name,
|
|
36
|
+
file: path.relative(ctx.projectRoot, entityPath),
|
|
37
|
+
suggestion: "Run `apso generate` to create entity files",
|
|
38
|
+
});
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const content = fs.readFileSync(entityPath, "utf-8");
|
|
42
|
+
// The correct pattern is @PrimaryGeneratedColumn('uuid')
|
|
43
|
+
const hasPrimaryGenerated = /PrimaryGeneratedColumn\s*\(\s*['"]uuid['"]\s*\)/.test(content);
|
|
44
|
+
// Wrong patterns: @PrimaryColumn() or @PrimaryColumn({ type: 'uuid' }) without generation
|
|
45
|
+
const hasPrimaryColumnOnly = /PrimaryColumn\s*\(/.test(content) && !hasPrimaryGenerated;
|
|
46
|
+
if (hasPrimaryColumnOnly) {
|
|
47
|
+
findings.push({
|
|
48
|
+
check: "pk-strategy",
|
|
49
|
+
severity: "error",
|
|
50
|
+
message: `Entity "${entity.name}" uses @PrimaryColumn() for UUID PK instead of @PrimaryGeneratedColumn('uuid'). The database will not auto-generate IDs.`,
|
|
51
|
+
entity: entity.name,
|
|
52
|
+
file: path.relative(ctx.projectRoot, entityPath),
|
|
53
|
+
suggestion: `Replace @PrimaryColumn() with @PrimaryGeneratedColumn('uuid') in the entity file, or regenerate with \`apso generate\``,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
else if (!hasPrimaryGenerated) {
|
|
57
|
+
findings.push({
|
|
58
|
+
check: "pk-strategy",
|
|
59
|
+
severity: "warning",
|
|
60
|
+
message: `Entity "${entity.name}" has primaryKeyType "uuid" but no @PrimaryGeneratedColumn('uuid') found in generated code`,
|
|
61
|
+
entity: entity.name,
|
|
62
|
+
file: path.relative(ctx.projectRoot, entityPath),
|
|
63
|
+
suggestion: "Regenerate with `apso generate`",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return findings;
|
|
68
|
+
}
|
|
69
|
+
exports.checkPkStrategy = checkPkStrategy;
|
|
70
|
+
/**
|
|
71
|
+
* Check for unused imports in generated entity files.
|
|
72
|
+
*
|
|
73
|
+
* The code generator sometimes imports symbols (like Generated, Index)
|
|
74
|
+
* that aren't used in the entity body.
|
|
75
|
+
*/
|
|
76
|
+
function checkUnusedImports(ctx) {
|
|
77
|
+
const findings = [];
|
|
78
|
+
if (ctx.language !== "typescript")
|
|
79
|
+
return findings;
|
|
80
|
+
for (const entity of ctx.entities) {
|
|
81
|
+
const entityFileName = `${entity.name}.entity.ts`;
|
|
82
|
+
const entityDir = (0, casing_1.snakeCase)(entity.name);
|
|
83
|
+
const entityPath = path.join(ctx.projectRoot, ctx.rootFolder, "autogen", entityDir, entityFileName);
|
|
84
|
+
if (!fs.existsSync(entityPath))
|
|
85
|
+
continue;
|
|
86
|
+
const content = fs.readFileSync(entityPath, "utf-8");
|
|
87
|
+
const lines = content.split("\n");
|
|
88
|
+
// Find import lines from typeorm
|
|
89
|
+
for (const line of lines) {
|
|
90
|
+
const importMatch = line.match(/import\s*\{([^}]+)\}\s*from\s*['"]typeorm['"]/);
|
|
91
|
+
if (!importMatch)
|
|
92
|
+
continue;
|
|
93
|
+
const imports = importMatch[1]
|
|
94
|
+
.split(",")
|
|
95
|
+
.map((s) => s.trim())
|
|
96
|
+
.filter((s) => s.length > 0);
|
|
97
|
+
// Check each imported symbol against the rest of the file
|
|
98
|
+
// (excluding the import line itself)
|
|
99
|
+
const restOfFile = content.replace(line, "");
|
|
100
|
+
for (const sym of imports) {
|
|
101
|
+
// Count usages: decorators appear as @Symbol or Symbol( or Symbol<
|
|
102
|
+
const usagePattern = new RegExp(`(?:@${sym}\\b|${sym}\\s*[(<])`, "g");
|
|
103
|
+
const usages = restOfFile.match(usagePattern);
|
|
104
|
+
if (!usages || usages.length === 0) {
|
|
105
|
+
findings.push({
|
|
106
|
+
check: "unused-imports",
|
|
107
|
+
severity: "warning",
|
|
108
|
+
message: `Entity "${entity.name}" imports "${sym}" from typeorm but never uses it`,
|
|
109
|
+
entity: entity.name,
|
|
110
|
+
file: path.relative(ctx.projectRoot, entityPath),
|
|
111
|
+
suggestion: `Remove "${sym}" from the import statement`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return findings;
|
|
118
|
+
}
|
|
119
|
+
exports.checkUnusedImports = checkUnusedImports;
|
|
120
|
+
/**
|
|
121
|
+
* Check that each field type in the schema is recognized by the generator.
|
|
122
|
+
*
|
|
123
|
+
* Uses the canonical fieldTypeToColumnType map as the source of truth.
|
|
124
|
+
*/
|
|
125
|
+
function checkFieldTypeMismatch(ctx) {
|
|
126
|
+
const findings = [];
|
|
127
|
+
const validTypes = new Set(Object.keys(field_1.fieldTypeToColumnType));
|
|
128
|
+
for (const entity of ctx.entities) {
|
|
129
|
+
for (const field of entity.fields || []) {
|
|
130
|
+
if (!validTypes.has(field.type)) {
|
|
131
|
+
findings.push({
|
|
132
|
+
check: "field-type-mismatch",
|
|
133
|
+
severity: "error",
|
|
134
|
+
message: `Entity "${entity.name}", field "${field.name}": type "${field.type}" is not recognized by the generator`,
|
|
135
|
+
entity: entity.name,
|
|
136
|
+
suggestion: `Valid types: text, integer, float, decimal, numeric, boolean, date, enum, json, json-plain, array, uuid`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
// Enum fields must have values
|
|
140
|
+
if (field.type === "enum" && (!field.values || field.values.length === 0)) {
|
|
141
|
+
findings.push({
|
|
142
|
+
check: "field-type-mismatch",
|
|
143
|
+
severity: "error",
|
|
144
|
+
message: `Entity "${entity.name}", field "${field.name}": enum type requires a "values" array`,
|
|
145
|
+
entity: entity.name,
|
|
146
|
+
suggestion: `Add "values": ["option1", "option2"] to the field definition`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return findings;
|
|
152
|
+
}
|
|
153
|
+
exports.checkFieldTypeMismatch = checkFieldTypeMismatch;
|
|
154
|
+
/**
|
|
155
|
+
* Check that relationship targets reference existing entities.
|
|
156
|
+
*
|
|
157
|
+
* This overlaps with validate_schema but is included for completeness
|
|
158
|
+
* in the diagnostic suite.
|
|
159
|
+
*/
|
|
160
|
+
function checkRelationshipTarget(ctx) {
|
|
161
|
+
const findings = [];
|
|
162
|
+
const entityNames = new Set(ctx.entities.map((e) => e.name));
|
|
163
|
+
for (const [entityName, relationships] of Object.entries(ctx.relationshipMap)) {
|
|
164
|
+
if (!entityNames.has(entityName)) {
|
|
165
|
+
findings.push({
|
|
166
|
+
check: "relationship-target",
|
|
167
|
+
severity: "error",
|
|
168
|
+
message: `Relationship source "${entityName}" does not match any entity`,
|
|
169
|
+
entity: entityName,
|
|
170
|
+
suggestion: `Check spelling or add an entity named "${entityName}"`,
|
|
171
|
+
});
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
for (const rel of relationships) {
|
|
175
|
+
if (!entityNames.has(rel.name)) {
|
|
176
|
+
findings.push({
|
|
177
|
+
check: "relationship-target",
|
|
178
|
+
severity: "error",
|
|
179
|
+
message: `Entity "${entityName}": relationship target "${rel.name}" does not exist`,
|
|
180
|
+
entity: entityName,
|
|
181
|
+
suggestion: `Check spelling or add an entity named "${rel.name}"`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return findings;
|
|
187
|
+
}
|
|
188
|
+
exports.checkRelationshipTarget = checkRelationshipTarget;
|
|
189
|
+
/**
|
|
190
|
+
* Check that generated schema can sync in PGlite without errors.
|
|
191
|
+
*
|
|
192
|
+
* This is an async check that imports and runs the migration sandbox.
|
|
193
|
+
*/
|
|
194
|
+
async function checkMigrationSandbox(ctx) {
|
|
195
|
+
const findings = [];
|
|
196
|
+
try {
|
|
197
|
+
const { runMigrationSandbox } = await Promise.resolve().then(() => tslib_1.__importStar(require("../migrate/sandbox")));
|
|
198
|
+
const result = await runMigrationSandbox({
|
|
199
|
+
entities: ctx.entities,
|
|
200
|
+
relationshipMap: ctx.relationshipMap,
|
|
201
|
+
});
|
|
202
|
+
if (!result.success) {
|
|
203
|
+
findings.push({
|
|
204
|
+
check: "migration-sandbox",
|
|
205
|
+
severity: "error",
|
|
206
|
+
message: `Migration sandbox failed: ${result.error || "unknown error"}`,
|
|
207
|
+
suggestion: "Fix schema issues and run `apso migrate` to debug",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
213
|
+
findings.push({
|
|
214
|
+
check: "migration-sandbox",
|
|
215
|
+
severity: "warning",
|
|
216
|
+
message: `Could not run migration sandbox: ${msg}`,
|
|
217
|
+
suggestion: "Ensure @electric-sql/pglite and typeorm-pglite are installed",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return findings;
|
|
221
|
+
}
|
|
222
|
+
exports.checkMigrationSandbox = checkMigrationSandbox;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { DiagnosticFinding, DiagnosticContext } from "./types";
|
|
2
|
+
export { runDiagnostics } from "./runner";
|
|
3
|
+
export { checkPkStrategy, checkUnusedImports, checkFieldTypeMismatch, checkRelationshipTarget, } from "./checks";
|
|
4
|
+
export { isGhAvailable, searchExistingIssues, fileGitHubIssue, } from "./runner";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fileGitHubIssue = exports.searchExistingIssues = exports.isGhAvailable = exports.checkRelationshipTarget = exports.checkFieldTypeMismatch = exports.checkUnusedImports = exports.checkPkStrategy = exports.runDiagnostics = void 0;
|
|
4
|
+
var runner_1 = require("./runner");
|
|
5
|
+
Object.defineProperty(exports, "runDiagnostics", { enumerable: true, get: function () { return runner_1.runDiagnostics; } });
|
|
6
|
+
var checks_1 = require("./checks");
|
|
7
|
+
Object.defineProperty(exports, "checkPkStrategy", { enumerable: true, get: function () { return checks_1.checkPkStrategy; } });
|
|
8
|
+
Object.defineProperty(exports, "checkUnusedImports", { enumerable: true, get: function () { return checks_1.checkUnusedImports; } });
|
|
9
|
+
Object.defineProperty(exports, "checkFieldTypeMismatch", { enumerable: true, get: function () { return checks_1.checkFieldTypeMismatch; } });
|
|
10
|
+
Object.defineProperty(exports, "checkRelationshipTarget", { enumerable: true, get: function () { return checks_1.checkRelationshipTarget; } });
|
|
11
|
+
var runner_2 = require("./runner");
|
|
12
|
+
Object.defineProperty(exports, "isGhAvailable", { enumerable: true, get: function () { return runner_2.isGhAvailable; } });
|
|
13
|
+
Object.defineProperty(exports, "searchExistingIssues", { enumerable: true, get: function () { return runner_2.searchExistingIssues; } });
|
|
14
|
+
Object.defineProperty(exports, "fileGitHubIssue", { enumerable: true, get: function () { return runner_2.fileGitHubIssue; } });
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { DiagnosticFinding, DiagnosticContext } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Run all diagnostic checks and return aggregated findings.
|
|
4
|
+
*
|
|
5
|
+
* Sync checks run first (schema validation, code scanning).
|
|
6
|
+
* The migration sandbox check is async and runs last.
|
|
7
|
+
*/
|
|
8
|
+
export declare function runDiagnostics(ctx: DiagnosticContext): Promise<DiagnosticFinding[]>;
|
|
9
|
+
/**
|
|
10
|
+
* Format findings as a human-readable string.
|
|
11
|
+
*/
|
|
12
|
+
export declare function formatFindings(findings: DiagnosticFinding[]): string;
|
|
13
|
+
/**
|
|
14
|
+
* Check if the GitHub CLI is installed and authenticated.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isGhAvailable(): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Search for existing issues that might duplicate what we're about to file.
|
|
19
|
+
*/
|
|
20
|
+
export declare function searchExistingIssues(title: string): {
|
|
21
|
+
number: number;
|
|
22
|
+
title: string;
|
|
23
|
+
url: string;
|
|
24
|
+
}[];
|
|
25
|
+
/**
|
|
26
|
+
* Build and file a GitHub issue from diagnostic findings.
|
|
27
|
+
* Returns the URL of the created issue.
|
|
28
|
+
*/
|
|
29
|
+
export declare function fileGitHubIssue(findings: DiagnosticFinding[], ctx: DiagnosticContext): string;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fileGitHubIssue = exports.searchExistingIssues = exports.isGhAvailable = exports.formatFindings = exports.runDiagnostics = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
const os = tslib_1.__importStar(require("os"));
|
|
7
|
+
const checks_1 = require("./checks");
|
|
8
|
+
/**
|
|
9
|
+
* Run all diagnostic checks and return aggregated findings.
|
|
10
|
+
*
|
|
11
|
+
* Sync checks run first (schema validation, code scanning).
|
|
12
|
+
* The migration sandbox check is async and runs last.
|
|
13
|
+
*/
|
|
14
|
+
async function runDiagnostics(ctx) {
|
|
15
|
+
const findings = [];
|
|
16
|
+
// Sync checks
|
|
17
|
+
findings.push(...(0, checks_1.checkFieldTypeMismatch)(ctx));
|
|
18
|
+
findings.push(...(0, checks_1.checkRelationshipTarget)(ctx));
|
|
19
|
+
findings.push(...(0, checks_1.checkPkStrategy)(ctx));
|
|
20
|
+
findings.push(...(0, checks_1.checkUnusedImports)(ctx));
|
|
21
|
+
// Async checks
|
|
22
|
+
const sandboxFindings = await (0, checks_1.checkMigrationSandbox)(ctx);
|
|
23
|
+
findings.push(...sandboxFindings);
|
|
24
|
+
return findings;
|
|
25
|
+
}
|
|
26
|
+
exports.runDiagnostics = runDiagnostics;
|
|
27
|
+
/**
|
|
28
|
+
* Format findings as a human-readable string.
|
|
29
|
+
*/
|
|
30
|
+
function formatFindings(findings) {
|
|
31
|
+
if (findings.length === 0) {
|
|
32
|
+
return "No issues found.";
|
|
33
|
+
}
|
|
34
|
+
const lines = [];
|
|
35
|
+
const errors = findings.filter((f) => f.severity === "error");
|
|
36
|
+
const warnings = findings.filter((f) => f.severity === "warning");
|
|
37
|
+
const infos = findings.filter((f) => f.severity === "info");
|
|
38
|
+
if (errors.length > 0) {
|
|
39
|
+
lines.push(`${errors.length} error(s):`);
|
|
40
|
+
for (const f of errors) {
|
|
41
|
+
lines.push(formatFinding(f));
|
|
42
|
+
}
|
|
43
|
+
lines.push("");
|
|
44
|
+
}
|
|
45
|
+
if (warnings.length > 0) {
|
|
46
|
+
lines.push(`${warnings.length} warning(s):`);
|
|
47
|
+
for (const f of warnings) {
|
|
48
|
+
lines.push(formatFinding(f));
|
|
49
|
+
}
|
|
50
|
+
lines.push("");
|
|
51
|
+
}
|
|
52
|
+
if (infos.length > 0) {
|
|
53
|
+
lines.push(`${infos.length} info(s):`);
|
|
54
|
+
for (const f of infos) {
|
|
55
|
+
lines.push(formatFinding(f));
|
|
56
|
+
}
|
|
57
|
+
lines.push("");
|
|
58
|
+
}
|
|
59
|
+
return lines.join("\n");
|
|
60
|
+
}
|
|
61
|
+
exports.formatFindings = formatFindings;
|
|
62
|
+
function formatFinding(f) {
|
|
63
|
+
const parts = [` [${f.severity.toUpperCase()}] ${f.message}`];
|
|
64
|
+
if (f.file) {
|
|
65
|
+
parts.push(` File: ${f.file}`);
|
|
66
|
+
}
|
|
67
|
+
if (f.suggestion) {
|
|
68
|
+
parts.push(` Fix: ${f.suggestion}`);
|
|
69
|
+
}
|
|
70
|
+
return parts.join("\n");
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Check if the GitHub CLI is installed and authenticated.
|
|
74
|
+
*/
|
|
75
|
+
function isGhAvailable() {
|
|
76
|
+
try {
|
|
77
|
+
(0, child_process_1.execSync)("gh auth status", { stdio: "ignore" });
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
exports.isGhAvailable = isGhAvailable;
|
|
85
|
+
/**
|
|
86
|
+
* Search for existing issues that might duplicate what we're about to file.
|
|
87
|
+
*/
|
|
88
|
+
function searchExistingIssues(title) {
|
|
89
|
+
try {
|
|
90
|
+
// Extract key terms from the title for search
|
|
91
|
+
const searchQuery = title.slice(0, 100);
|
|
92
|
+
const result = (0, child_process_1.execSync)(`gh issue list --repo apsoai/cli --search "${searchQuery.replace(/"/g, '\\"')}" --state open --json number,title,url --limit 5`, { encoding: "utf-8", timeout: 15000 });
|
|
93
|
+
return JSON.parse(result);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
exports.searchExistingIssues = searchExistingIssues;
|
|
100
|
+
/**
|
|
101
|
+
* Build and file a GitHub issue from diagnostic findings.
|
|
102
|
+
* Returns the URL of the created issue.
|
|
103
|
+
*/
|
|
104
|
+
function fileGitHubIssue(findings, ctx) {
|
|
105
|
+
const pkg = loadCliVersion();
|
|
106
|
+
const title = buildIssueTitle(findings);
|
|
107
|
+
const body = buildIssueBody(findings, ctx, pkg);
|
|
108
|
+
const result = (0, child_process_1.execSync)(`gh issue create --repo apsoai/cli --title "${title.replace(/"/g, '\\"')}" --body-file -`, {
|
|
109
|
+
input: body,
|
|
110
|
+
encoding: "utf-8",
|
|
111
|
+
timeout: 30000,
|
|
112
|
+
});
|
|
113
|
+
return result.trim();
|
|
114
|
+
}
|
|
115
|
+
exports.fileGitHubIssue = fileGitHubIssue;
|
|
116
|
+
function loadCliVersion() {
|
|
117
|
+
try {
|
|
118
|
+
const pkg = require("../../../package.json");
|
|
119
|
+
return pkg.version || "unknown";
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return "unknown";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function buildIssueTitle(findings) {
|
|
126
|
+
const firstError = findings.find((f) => f.severity === "error");
|
|
127
|
+
if (firstError) {
|
|
128
|
+
const prefix = firstError.entity ? `[${firstError.entity}] ` : "";
|
|
129
|
+
return `[doctor] ${prefix}${firstError.message}`.slice(0, 120);
|
|
130
|
+
}
|
|
131
|
+
return `[doctor] ${findings.length} diagnostic finding(s)`;
|
|
132
|
+
}
|
|
133
|
+
function buildIssueBody(findings, ctx, cliVersion) {
|
|
134
|
+
const sections = [];
|
|
135
|
+
// Environment
|
|
136
|
+
sections.push("## Environment");
|
|
137
|
+
sections.push(`- \`@apso/cli\` ${cliVersion} (${os.platform()}, node ${process.version})`);
|
|
138
|
+
sections.push(`- Language: ${ctx.language}`);
|
|
139
|
+
sections.push("");
|
|
140
|
+
// Findings table
|
|
141
|
+
sections.push("## Diagnostic findings");
|
|
142
|
+
sections.push("");
|
|
143
|
+
sections.push("| Severity | Check | Entity | Message |");
|
|
144
|
+
sections.push("|----------|-------|--------|---------|");
|
|
145
|
+
for (const f of findings) {
|
|
146
|
+
sections.push(`| ${f.severity} | ${f.check} | ${f.entity || "-"} | ${f.message} |`);
|
|
147
|
+
}
|
|
148
|
+
sections.push("");
|
|
149
|
+
// Schema excerpt (entity names, field types, PK types -- no field values or data)
|
|
150
|
+
sections.push("## Schema excerpt");
|
|
151
|
+
sections.push("```json");
|
|
152
|
+
const excerpt = ctx.entities.map((e) => ({
|
|
153
|
+
name: e.name,
|
|
154
|
+
primaryKeyType: e.primaryKeyType || "serial",
|
|
155
|
+
fields: (e.fields || []).map((f) => ({
|
|
156
|
+
name: f.name,
|
|
157
|
+
type: f.type,
|
|
158
|
+
})),
|
|
159
|
+
}));
|
|
160
|
+
sections.push(JSON.stringify(excerpt, null, 2));
|
|
161
|
+
sections.push("```");
|
|
162
|
+
sections.push("");
|
|
163
|
+
// Suggestions
|
|
164
|
+
const suggestions = findings
|
|
165
|
+
.filter((f) => f.suggestion)
|
|
166
|
+
.map((f) => `- **${f.check}**: ${f.suggestion}`);
|
|
167
|
+
if (suggestions.length > 0) {
|
|
168
|
+
sections.push("## Suggested fixes");
|
|
169
|
+
sections.push(suggestions.join("\n"));
|
|
170
|
+
sections.push("");
|
|
171
|
+
}
|
|
172
|
+
sections.push("---");
|
|
173
|
+
sections.push("Filed automatically by `apso doctor --report`");
|
|
174
|
+
return sections.join("\n");
|
|
175
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Entity } from "../types/entity";
|
|
2
|
+
import { RelationshipMap } from "../types/relationship";
|
|
3
|
+
import { TargetLanguage } from "../types/generator";
|
|
4
|
+
/**
|
|
5
|
+
* A single finding from a diagnostic check.
|
|
6
|
+
*/
|
|
7
|
+
export interface DiagnosticFinding {
|
|
8
|
+
/** Check identifier (e.g. "pk-strategy") */
|
|
9
|
+
check: string;
|
|
10
|
+
/** Severity level */
|
|
11
|
+
severity: "error" | "warning" | "info";
|
|
12
|
+
/** Human-readable description */
|
|
13
|
+
message: string;
|
|
14
|
+
/** Affected entity name */
|
|
15
|
+
entity?: string;
|
|
16
|
+
/** Affected file path (relative to project root) */
|
|
17
|
+
file?: string;
|
|
18
|
+
/** Suggested fix */
|
|
19
|
+
suggestion?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Context passed to each diagnostic check function.
|
|
23
|
+
*/
|
|
24
|
+
export interface DiagnosticContext {
|
|
25
|
+
/** Parsed entities from .apsorc */
|
|
26
|
+
entities: Entity[];
|
|
27
|
+
/** Relationship map from .apsorc */
|
|
28
|
+
relationshipMap: RelationshipMap;
|
|
29
|
+
/** Root folder from .apsorc (e.g. "src") */
|
|
30
|
+
rootFolder: string;
|
|
31
|
+
/** Target language */
|
|
32
|
+
language: TargetLanguage;
|
|
33
|
+
/** Absolute path to the project root (where .apsorc lives) */
|
|
34
|
+
projectRoot: string;
|
|
35
|
+
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apso/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@apso/cli",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.14.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@electric-sql/pglite": "^0.2.17",
|
package/oclif.manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.14.0",
|
|
3
3
|
"commands": {
|
|
4
4
|
"config": {
|
|
5
5
|
"id": "config",
|
|
@@ -102,6 +102,35 @@
|
|
|
102
102
|
},
|
|
103
103
|
"args": {}
|
|
104
104
|
},
|
|
105
|
+
"doctor": {
|
|
106
|
+
"id": "doctor",
|
|
107
|
+
"description": "Run diagnostic checks on generated code and optionally file a GitHub issue with findings",
|
|
108
|
+
"strict": true,
|
|
109
|
+
"pluginName": "@apso/cli",
|
|
110
|
+
"pluginAlias": "@apso/cli",
|
|
111
|
+
"pluginType": "core",
|
|
112
|
+
"aliases": [],
|
|
113
|
+
"examples": [
|
|
114
|
+
"$ apso doctor",
|
|
115
|
+
"$ apso doctor --json",
|
|
116
|
+
"$ apso doctor --report"
|
|
117
|
+
],
|
|
118
|
+
"flags": {
|
|
119
|
+
"report": {
|
|
120
|
+
"name": "report",
|
|
121
|
+
"type": "boolean",
|
|
122
|
+
"description": "File a GitHub issue with diagnostic findings (requires gh CLI)",
|
|
123
|
+
"allowNo": false
|
|
124
|
+
},
|
|
125
|
+
"json": {
|
|
126
|
+
"name": "json",
|
|
127
|
+
"type": "boolean",
|
|
128
|
+
"description": "Output findings as JSON instead of human-readable text",
|
|
129
|
+
"allowNo": false
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"args": {}
|
|
133
|
+
},
|
|
105
134
|
"generate": {
|
|
106
135
|
"id": "generate",
|
|
107
136
|
"description": "Generate code from .apsorc schema",
|