@apso/cli 0.13.0 → 0.15.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/dev.d.ts +54 -2
- package/dist/commands/dev.js +219 -32
- package/dist/commands/doctor.d.ts +10 -0
- package/dist/commands/doctor.js +127 -0
- package/dist/commands/init.js +2 -2
- package/dist/commands/link.js +1 -1
- package/dist/commands/mcp/serve.js +131 -2
- package/dist/commands/projects.js +1 -1
- package/dist/lib/config/types.js +2 -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/dist/lib/migrate/sandbox.js +126 -4
- package/dist/lib/migrate/snapshot.d.ts +1 -0
- package/dist/lib/migrate/snapshot.js +3 -0
- package/dist/lib/templates/go/utils/query.eta +37 -9
- package/dist/lib/templates/python/utils/query.eta +38 -10
- package/dist/lib/utils/template.js +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/oclif.manifest.json +41 -7
- package/package.json +1 -1
package/dist/commands/dev.d.ts
CHANGED
|
@@ -3,13 +3,65 @@ export default class Dev extends BaseCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
docker: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
6
7
|
detach: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
7
8
|
build: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
8
9
|
};
|
|
10
|
+
private children;
|
|
9
11
|
run(): Promise<void>;
|
|
10
12
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
+
* TypeScript: run `npm run dev` which reads DATABASE_TYPE from .env.
|
|
14
|
+
* PGlite is handled natively by the NestJS app -- no sidecar needed.
|
|
15
|
+
*/
|
|
16
|
+
private runTypeScript;
|
|
17
|
+
/**
|
|
18
|
+
* Python: if pglite, start the pglite-server sidecar first, then uvicorn.
|
|
19
|
+
*/
|
|
20
|
+
private runPython;
|
|
21
|
+
/**
|
|
22
|
+
* Go: if pglite, start the pglite-server sidecar first, then go run.
|
|
23
|
+
*/
|
|
24
|
+
private runGo;
|
|
25
|
+
/**
|
|
26
|
+
* Fallback: run docker compose up (the old behavior).
|
|
27
|
+
*/
|
|
28
|
+
private runDocker;
|
|
29
|
+
/**
|
|
30
|
+
* Detect the project language from .apsorc.
|
|
31
|
+
* Falls back to typescript if not specified.
|
|
32
|
+
*/
|
|
33
|
+
private detectLanguage;
|
|
34
|
+
/**
|
|
35
|
+
* Read DATABASE_TYPE from .env. Defaults to pglite.
|
|
36
|
+
*/
|
|
37
|
+
private readDatabaseType;
|
|
38
|
+
/**
|
|
39
|
+
* Read a single value from .env.
|
|
40
|
+
*/
|
|
41
|
+
private readEnvValue;
|
|
42
|
+
/**
|
|
43
|
+
* Find .apsorc by walking up from cwd.
|
|
44
|
+
*/
|
|
45
|
+
private findApsorc;
|
|
46
|
+
/**
|
|
47
|
+
* Run `npm install` if node_modules doesn't exist.
|
|
48
|
+
* Needed for the pglite-server sidecar in Python/Go projects.
|
|
49
|
+
*/
|
|
50
|
+
private ensureNodeModules;
|
|
51
|
+
/**
|
|
52
|
+
* Wait for a TCP port to become reachable.
|
|
53
|
+
*/
|
|
54
|
+
private waitForPort;
|
|
55
|
+
/**
|
|
56
|
+
* Spawn a process in the background and track it for cleanup.
|
|
57
|
+
*/
|
|
58
|
+
private spawnBackground;
|
|
59
|
+
/**
|
|
60
|
+
* Spawn a process and wait for it to exit.
|
|
61
|
+
*/
|
|
62
|
+
private spawnAndWait;
|
|
63
|
+
/**
|
|
64
|
+
* Detect whether to use `docker compose` (v2) or `docker-compose` (v1).
|
|
13
65
|
*/
|
|
14
66
|
private detectComposeVariant;
|
|
15
67
|
}
|
package/dist/commands/dev.js
CHANGED
|
@@ -4,61 +4,246 @@ const tslib_1 = require("tslib");
|
|
|
4
4
|
const core_1 = require("@oclif/core");
|
|
5
5
|
const child_process_1 = require("child_process");
|
|
6
6
|
const fs = tslib_1.__importStar(require("fs"));
|
|
7
|
+
const path = tslib_1.__importStar(require("path"));
|
|
7
8
|
const shelljs_1 = tslib_1.__importDefault(require("shelljs"));
|
|
8
9
|
const base_command_1 = tslib_1.__importDefault(require("../lib/base-command"));
|
|
9
10
|
class Dev extends base_command_1.default {
|
|
11
|
+
constructor() {
|
|
12
|
+
super(...arguments);
|
|
13
|
+
this.children = [];
|
|
14
|
+
}
|
|
10
15
|
async run() {
|
|
11
16
|
const { flags } = await this.parse(Dev);
|
|
12
|
-
|
|
17
|
+
if (flags.docker) {
|
|
18
|
+
return this.runDocker(flags);
|
|
19
|
+
}
|
|
20
|
+
const language = this.detectLanguage();
|
|
21
|
+
const databaseType = this.readDatabaseType();
|
|
22
|
+
this.log(`Detected language: ${language}`);
|
|
23
|
+
this.log(`Database type: ${databaseType}`);
|
|
24
|
+
// Set up signal forwarding for clean shutdown
|
|
25
|
+
const cleanup = () => {
|
|
26
|
+
for (const child of this.children) {
|
|
27
|
+
child.kill("SIGTERM");
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
process.on("SIGINT", cleanup);
|
|
31
|
+
process.on("SIGTERM", cleanup);
|
|
32
|
+
try {
|
|
33
|
+
switch (language) {
|
|
34
|
+
case "typescript":
|
|
35
|
+
await this.runTypeScript();
|
|
36
|
+
break;
|
|
37
|
+
case "python":
|
|
38
|
+
await this.runPython(databaseType);
|
|
39
|
+
break;
|
|
40
|
+
case "go":
|
|
41
|
+
await this.runGo(databaseType);
|
|
42
|
+
break;
|
|
43
|
+
default:
|
|
44
|
+
this.error(`Unsupported language: ${language}. Supported: typescript, python, go.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
process.removeListener("SIGINT", cleanup);
|
|
49
|
+
process.removeListener("SIGTERM", cleanup);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* TypeScript: run `npm run dev` which reads DATABASE_TYPE from .env.
|
|
54
|
+
* PGlite is handled natively by the NestJS app -- no sidecar needed.
|
|
55
|
+
*/
|
|
56
|
+
async runTypeScript() {
|
|
57
|
+
this.log("Starting TypeScript dev server...");
|
|
58
|
+
await this.spawnAndWait("npm", ["run", "dev"]);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Python: if pglite, start the pglite-server sidecar first, then uvicorn.
|
|
62
|
+
*/
|
|
63
|
+
async runPython(databaseType) {
|
|
64
|
+
if (databaseType === "pglite") {
|
|
65
|
+
this.log("Starting PGlite sidecar...");
|
|
66
|
+
await this.ensureNodeModules();
|
|
67
|
+
this.spawnBackground("npm", ["run", "db"]);
|
|
68
|
+
// Give the sidecar a moment to bind the port
|
|
69
|
+
await this.waitForPort(5432, 10000);
|
|
70
|
+
}
|
|
71
|
+
this.log("Starting Python dev server...");
|
|
72
|
+
await this.spawnAndWait("uvicorn", [
|
|
73
|
+
"app.main:app",
|
|
74
|
+
"--reload",
|
|
75
|
+
"--host",
|
|
76
|
+
"0.0.0.0",
|
|
77
|
+
"--port",
|
|
78
|
+
this.readEnvValue("PORT") || "3000",
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Go: if pglite, start the pglite-server sidecar first, then go run.
|
|
83
|
+
*/
|
|
84
|
+
async runGo(databaseType) {
|
|
85
|
+
if (databaseType === "pglite") {
|
|
86
|
+
this.log("Starting PGlite sidecar...");
|
|
87
|
+
await this.ensureNodeModules();
|
|
88
|
+
this.spawnBackground("npm", ["run", "db"]);
|
|
89
|
+
await this.waitForPort(5432, 10000);
|
|
90
|
+
}
|
|
91
|
+
this.log("Starting Go dev server...");
|
|
92
|
+
await this.spawnAndWait("go", ["run", "cmd/main.go"]);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Fallback: run docker compose up (the old behavior).
|
|
96
|
+
*/
|
|
97
|
+
async runDocker(flags) {
|
|
13
98
|
if (!shelljs_1.default.which("docker")) {
|
|
14
99
|
this.error("Docker is required but not found. Please install Docker and try again.\nhttps://docs.docker.com/get-docker/");
|
|
15
100
|
}
|
|
16
|
-
// Detect compose variant (v2 plugin vs v1 standalone)
|
|
17
101
|
const composeCmd = this.detectComposeVariant();
|
|
18
|
-
// Verify docker-compose.yml exists
|
|
19
102
|
if (!fs.existsSync("docker-compose.yml")) {
|
|
20
103
|
this.error("No docker-compose.yml found in the current directory.\nMake sure you are in an Apso project root.");
|
|
21
104
|
}
|
|
22
|
-
// Build the command args
|
|
23
105
|
const args = [...composeCmd, "up"];
|
|
24
|
-
if (flags.build)
|
|
106
|
+
if (flags.build)
|
|
25
107
|
args.push("--build");
|
|
26
|
-
|
|
27
|
-
if (flags.detach) {
|
|
108
|
+
if (flags.detach)
|
|
28
109
|
args.push("-d");
|
|
29
|
-
}
|
|
30
110
|
this.log(`Running: ${args.join(" ")}`);
|
|
31
|
-
|
|
111
|
+
await this.spawnAndWait(args[0], args.slice(1), {
|
|
112
|
+
AUTH_DISABLED: "true",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// --- Helpers ---
|
|
116
|
+
/**
|
|
117
|
+
* Detect the project language from .apsorc.
|
|
118
|
+
* Falls back to typescript if not specified.
|
|
119
|
+
*/
|
|
120
|
+
detectLanguage() {
|
|
121
|
+
const apsorcPath = this.findApsorc();
|
|
122
|
+
if (!apsorcPath) {
|
|
123
|
+
this.log("No .apsorc found, defaulting to typescript.");
|
|
124
|
+
return "typescript";
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const content = fs.readFileSync(apsorcPath, "utf-8");
|
|
128
|
+
const config = JSON.parse(content);
|
|
129
|
+
return config.language || "typescript";
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
this.log("Could not parse .apsorc, defaulting to typescript.");
|
|
133
|
+
return "typescript";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Read DATABASE_TYPE from .env. Defaults to pglite.
|
|
138
|
+
*/
|
|
139
|
+
readDatabaseType() {
|
|
140
|
+
return this.readEnvValue("DATABASE_TYPE") || "pglite";
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Read a single value from .env.
|
|
144
|
+
*/
|
|
145
|
+
readEnvValue(key) {
|
|
146
|
+
const envPath = path.resolve(".env");
|
|
147
|
+
if (!fs.existsSync(envPath))
|
|
148
|
+
return undefined;
|
|
149
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
150
|
+
for (const line of content.split("\n")) {
|
|
151
|
+
const trimmed = line.trim();
|
|
152
|
+
if (trimmed.startsWith("#") || !trimmed.includes("="))
|
|
153
|
+
continue;
|
|
154
|
+
const [k, ...rest] = trimmed.split("=");
|
|
155
|
+
if (k.trim() === key)
|
|
156
|
+
return rest.join("=").trim();
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Find .apsorc by walking up from cwd.
|
|
162
|
+
*/
|
|
163
|
+
findApsorc() {
|
|
164
|
+
let dir = process.cwd();
|
|
165
|
+
while (dir !== path.parse(dir).root) {
|
|
166
|
+
const candidate = path.join(dir, ".apsorc");
|
|
167
|
+
if (fs.existsSync(candidate))
|
|
168
|
+
return candidate;
|
|
169
|
+
dir = path.dirname(dir);
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Run `npm install` if node_modules doesn't exist.
|
|
175
|
+
* Needed for the pglite-server sidecar in Python/Go projects.
|
|
176
|
+
*/
|
|
177
|
+
async ensureNodeModules() {
|
|
178
|
+
if (!fs.existsSync("node_modules")) {
|
|
179
|
+
this.log("Installing pglite-server dependencies...");
|
|
180
|
+
await this.spawnAndWait("npm", ["install"]);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Wait for a TCP port to become reachable.
|
|
185
|
+
*/
|
|
186
|
+
waitForPort(port, timeoutMs) {
|
|
187
|
+
return new Promise((resolve, reject) => {
|
|
188
|
+
const start = Date.now();
|
|
189
|
+
const check = () => {
|
|
190
|
+
const net = require("net");
|
|
191
|
+
const sock = net.createConnection({ port, host: "127.0.0.1" }, () => {
|
|
192
|
+
sock.destroy();
|
|
193
|
+
resolve();
|
|
194
|
+
});
|
|
195
|
+
sock.on("error", () => {
|
|
196
|
+
sock.destroy();
|
|
197
|
+
if (Date.now() - start > timeoutMs) {
|
|
198
|
+
reject(new Error(`Port ${port} did not become available within ${timeoutMs}ms`));
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
setTimeout(check, 250);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
check();
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Spawn a process in the background and track it for cleanup.
|
|
210
|
+
*/
|
|
211
|
+
spawnBackground(cmd, args) {
|
|
212
|
+
const child = (0, child_process_1.spawn)(cmd, args, {
|
|
32
213
|
stdio: "inherit",
|
|
33
|
-
env: {
|
|
34
|
-
...process.env,
|
|
35
|
-
AUTH_DISABLED: "true",
|
|
36
|
-
},
|
|
214
|
+
env: { ...process.env },
|
|
37
215
|
});
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
|
|
216
|
+
this.children.push(child);
|
|
217
|
+
child.on("error", (err) => {
|
|
218
|
+
this.warn(`Background process ${cmd} failed: ${err.message}`);
|
|
219
|
+
});
|
|
220
|
+
return child;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Spawn a process and wait for it to exit.
|
|
224
|
+
*/
|
|
225
|
+
spawnAndWait(cmd, args, extraEnv) {
|
|
44
226
|
return new Promise((resolve, reject) => {
|
|
227
|
+
const child = (0, child_process_1.spawn)(cmd, args, {
|
|
228
|
+
stdio: "inherit",
|
|
229
|
+
env: { ...process.env, ...extraEnv },
|
|
230
|
+
});
|
|
231
|
+
this.children.push(child);
|
|
45
232
|
child.on("close", (code) => {
|
|
46
|
-
|
|
47
|
-
process.removeListener("SIGTERM", forwardSignal);
|
|
233
|
+
this.children = this.children.filter((c) => c !== child);
|
|
48
234
|
if (code !== 0 && code !== null) {
|
|
49
|
-
reject(new Error(
|
|
235
|
+
reject(new Error(`${cmd} exited with code ${code}`));
|
|
50
236
|
return;
|
|
51
237
|
}
|
|
52
238
|
resolve();
|
|
53
239
|
});
|
|
54
240
|
child.on("error", (err) => {
|
|
55
|
-
this.error(`Failed to start
|
|
241
|
+
this.error(`Failed to start ${cmd}: ${err.message}`);
|
|
56
242
|
});
|
|
57
243
|
});
|
|
58
244
|
}
|
|
59
245
|
/**
|
|
60
|
-
* Detect whether to use `docker compose` (v2
|
|
61
|
-
* Returns the command parts as an array.
|
|
246
|
+
* Detect whether to use `docker compose` (v2) or `docker-compose` (v1).
|
|
62
247
|
*/
|
|
63
248
|
detectComposeVariant() {
|
|
64
249
|
try {
|
|
@@ -66,7 +251,6 @@ class Dev extends base_command_1.default {
|
|
|
66
251
|
return ["docker", "compose"];
|
|
67
252
|
}
|
|
68
253
|
catch {
|
|
69
|
-
// Fall back to v1
|
|
70
254
|
if (shelljs_1.default.which("docker-compose")) {
|
|
71
255
|
return ["docker-compose"];
|
|
72
256
|
}
|
|
@@ -74,21 +258,24 @@ class Dev extends base_command_1.default {
|
|
|
74
258
|
}
|
|
75
259
|
}
|
|
76
260
|
}
|
|
77
|
-
Dev.description = "Start the local development server
|
|
261
|
+
Dev.description = "Start the local development server. Detects project language and database type from .apsorc and .env.";
|
|
78
262
|
Dev.examples = [
|
|
79
263
|
`$ apso dev`,
|
|
80
|
-
`$ apso dev --
|
|
81
|
-
`$ apso dev --
|
|
82
|
-
`$ apso dev --build --detach`,
|
|
264
|
+
`$ apso dev --docker`,
|
|
265
|
+
`$ apso dev --docker --build`,
|
|
83
266
|
];
|
|
84
267
|
Dev.flags = {
|
|
268
|
+
docker: core_1.Flags.boolean({
|
|
269
|
+
description: "Use Docker Compose instead of the native dev stack",
|
|
270
|
+
default: false,
|
|
271
|
+
}),
|
|
85
272
|
detach: core_1.Flags.boolean({
|
|
86
273
|
char: "d",
|
|
87
|
-
description: "Run containers in the background",
|
|
274
|
+
description: "Run containers in the background (Docker mode only)",
|
|
88
275
|
default: false,
|
|
89
276
|
}),
|
|
90
277
|
build: core_1.Flags.boolean({
|
|
91
|
-
description: "Rebuild images before starting",
|
|
278
|
+
description: "Rebuild images before starting (Docker mode only)",
|
|
92
279
|
default: false,
|
|
93
280
|
}),
|
|
94
281
|
};
|
|
@@ -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;
|
package/dist/commands/init.js
CHANGED
|
@@ -55,7 +55,7 @@ class Init extends base_command_1.default {
|
|
|
55
55
|
// Select workspace
|
|
56
56
|
const workspaces = await services_1.workspacesApi.list();
|
|
57
57
|
if (workspaces.length === 0) {
|
|
58
|
-
this.error("No workspaces found. Create a workspace at https://app.apso.
|
|
58
|
+
this.error("No workspaces found. Create a workspace at https://app.apso.cloud first.");
|
|
59
59
|
}
|
|
60
60
|
const { workspace } = await inquirer_1.default.prompt([
|
|
61
61
|
{
|
|
@@ -170,7 +170,7 @@ class Init extends base_command_1.default {
|
|
|
170
170
|
// Select workspace
|
|
171
171
|
const workspaces = await services_1.workspacesApi.list();
|
|
172
172
|
if (workspaces.length === 0) {
|
|
173
|
-
this.error("No workspaces found. Create a workspace at https://app.apso.
|
|
173
|
+
this.error("No workspaces found. Create a workspace at https://app.apso.cloud first.");
|
|
174
174
|
}
|
|
175
175
|
const { workspace } = await inquirer_1.default.prompt([
|
|
176
176
|
{
|
package/dist/commands/link.js
CHANGED
|
@@ -44,7 +44,7 @@ class Link extends base_command_1.default {
|
|
|
44
44
|
else {
|
|
45
45
|
const workspaces = await services_1.workspacesApi.list();
|
|
46
46
|
if (workspaces.length === 0) {
|
|
47
|
-
this.error("No workspaces found. Create a workspace at https://app.apso.
|
|
47
|
+
this.error("No workspaces found. Create a workspace at https://app.apso.cloud first.");
|
|
48
48
|
}
|
|
49
49
|
const response = await inquirer_1.default.prompt([
|
|
50
50
|
{
|