@insforge/cli 0.1.30 → 0.1.32
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.js +622 -30
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -106,6 +106,10 @@ var ProjectNotLinkedError = class extends CLIError {
|
|
|
106
106
|
super("No project linked. Run `insforge link` first.", 3, "PROJECT_NOT_LINKED");
|
|
107
107
|
}
|
|
108
108
|
};
|
|
109
|
+
function getDeploymentError(metadata) {
|
|
110
|
+
if (!metadata || typeof metadata.error !== "object" || !metadata.error) return null;
|
|
111
|
+
return metadata.error.errorMessage ?? null;
|
|
112
|
+
}
|
|
109
113
|
function handleError(err, json) {
|
|
110
114
|
if (err instanceof CLIError) {
|
|
111
115
|
if (json) {
|
|
@@ -708,21 +712,6 @@ ${missing.join("\n")}
|
|
|
708
712
|
`;
|
|
709
713
|
appendFileSync(gitignorePath, block);
|
|
710
714
|
}
|
|
711
|
-
async function installCliGlobally(json) {
|
|
712
|
-
try {
|
|
713
|
-
const { stdout } = await execAsync("npm ls -g @insforge/cli --json", { timeout: 1e4 });
|
|
714
|
-
const parsed = JSON.parse(stdout);
|
|
715
|
-
if (parsed?.dependencies?.["@insforge/cli"]) return;
|
|
716
|
-
} catch {
|
|
717
|
-
}
|
|
718
|
-
try {
|
|
719
|
-
if (!json) clack5.log.info("Installing InsForge CLI globally...");
|
|
720
|
-
await execAsync("npm install -g @insforge/cli", { timeout: 6e4 });
|
|
721
|
-
if (!json) clack5.log.success("InsForge CLI installed. You can now run `insforge` directly.");
|
|
722
|
-
} catch {
|
|
723
|
-
if (!json) clack5.log.warn("Failed to install CLI globally. You can run manually: npm install -g @insforge/cli");
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
715
|
async function installSkills(json) {
|
|
727
716
|
try {
|
|
728
717
|
if (!json) clack5.log.info("Installing InsForge agent skills...");
|
|
@@ -822,7 +811,6 @@ function registerProjectLinkCommand(program2) {
|
|
|
822
811
|
} else {
|
|
823
812
|
outputSuccess(`Linked to direct project at ${projectConfig2.oss_host}`);
|
|
824
813
|
}
|
|
825
|
-
await installCliGlobally(json);
|
|
826
814
|
await installSkills(json);
|
|
827
815
|
await reportCliUsage("cli.link_direct", true, 6);
|
|
828
816
|
return;
|
|
@@ -831,7 +819,7 @@ function registerProjectLinkCommand(program2) {
|
|
|
831
819
|
handleError(err, json);
|
|
832
820
|
}
|
|
833
821
|
}
|
|
834
|
-
await requireAuth(apiUrl, false);
|
|
822
|
+
const creds = await requireAuth(apiUrl, false);
|
|
835
823
|
let orgId = opts.orgId;
|
|
836
824
|
let projectId = opts.projectId;
|
|
837
825
|
if (!orgId && !projectId) {
|
|
@@ -873,10 +861,24 @@ function registerProjectLinkCommand(program2) {
|
|
|
873
861
|
if (clack6.isCancel(selected)) process.exit(0);
|
|
874
862
|
projectId = selected;
|
|
875
863
|
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
864
|
+
let project;
|
|
865
|
+
let apiKey;
|
|
866
|
+
try {
|
|
867
|
+
[project, apiKey] = await Promise.all([
|
|
868
|
+
getProject(projectId, apiUrl),
|
|
869
|
+
getProjectApiKey(projectId, apiUrl)
|
|
870
|
+
]);
|
|
871
|
+
} catch (err) {
|
|
872
|
+
if (err instanceof CLIError && (err.exitCode === 5 || err.exitCode === 4 || err.message.includes("not found"))) {
|
|
873
|
+
const identity = creds.user?.email ?? creds.user?.name ?? "unknown user";
|
|
874
|
+
throw new CLIError(
|
|
875
|
+
`You're logged in as ${identity}, and you don't have access to project ${projectId}. Check that the project ID is correct and belongs to one of your organizations.`,
|
|
876
|
+
5,
|
|
877
|
+
"PERMISSION_DENIED"
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
throw err;
|
|
881
|
+
}
|
|
880
882
|
const projectConfig = {
|
|
881
883
|
project_id: project.id,
|
|
882
884
|
project_name: project.name,
|
|
@@ -892,7 +894,6 @@ function registerProjectLinkCommand(program2) {
|
|
|
892
894
|
} else {
|
|
893
895
|
outputSuccess(`Linked to project "${project.name}" (${project.appkey}.${project.region})`);
|
|
894
896
|
}
|
|
895
|
-
await installCliGlobally(json);
|
|
896
897
|
await installSkills(json);
|
|
897
898
|
await reportCliUsage("cli.link", true, 6);
|
|
898
899
|
} catch (err) {
|
|
@@ -1942,12 +1943,13 @@ async function deployProject(opts) {
|
|
|
1942
1943
|
try {
|
|
1943
1944
|
const statusRes = await ossFetch(`/api/deployments/${deploymentId}`);
|
|
1944
1945
|
deployment = await statusRes.json();
|
|
1945
|
-
|
|
1946
|
+
const status = deployment.status.toUpperCase();
|
|
1947
|
+
if (status === "READY") {
|
|
1946
1948
|
break;
|
|
1947
1949
|
}
|
|
1948
|
-
if (
|
|
1950
|
+
if (status === "ERROR" || status === "CANCELED") {
|
|
1949
1951
|
s?.stop("Deployment failed");
|
|
1950
|
-
throw new CLIError(deployment.
|
|
1952
|
+
throw new CLIError(getDeploymentError(deployment.metadata) ?? `Deployment failed with status: ${deployment.status}`);
|
|
1951
1953
|
}
|
|
1952
1954
|
const elapsed = Math.round((Date.now() - startTime) / 1e3);
|
|
1953
1955
|
s?.message(`Building and deploying... (${elapsed}s, status: ${deployment.status})`);
|
|
@@ -1955,8 +1957,8 @@ async function deployProject(opts) {
|
|
|
1955
1957
|
if (err instanceof CLIError) throw err;
|
|
1956
1958
|
}
|
|
1957
1959
|
}
|
|
1958
|
-
const isReady = deployment?.status === "
|
|
1959
|
-
const liveUrl = isReady ? deployment?.
|
|
1960
|
+
const isReady = deployment?.status.toUpperCase() === "READY";
|
|
1961
|
+
const liveUrl = isReady ? deployment?.url ?? null : null;
|
|
1960
1962
|
return { deploymentId, deployment, isReady, liveUrl };
|
|
1961
1963
|
}
|
|
1962
1964
|
function registerDeploymentsDeployCommand(deploymentsCmd2) {
|
|
@@ -2136,7 +2138,6 @@ function registerCreateCommand(program2) {
|
|
|
2136
2138
|
} else if (hasTemplate) {
|
|
2137
2139
|
await downloadTemplate(template, projectConfig, projectName, json, apiUrl);
|
|
2138
2140
|
}
|
|
2139
|
-
await installCliGlobally(json);
|
|
2140
2141
|
await installSkills(json);
|
|
2141
2142
|
await reportCliUsage("cli.create", true, 6);
|
|
2142
2143
|
if (hasTemplate) {
|
|
@@ -2469,6 +2470,7 @@ function registerDeploymentsStatusCommand(deploymentsCmd2) {
|
|
|
2469
2470
|
if (json) {
|
|
2470
2471
|
outputJson(d);
|
|
2471
2472
|
} else {
|
|
2473
|
+
const errorMessage = getDeploymentError(d.metadata);
|
|
2472
2474
|
outputTable(
|
|
2473
2475
|
["Field", "Value"],
|
|
2474
2476
|
[
|
|
@@ -2476,10 +2478,10 @@ function registerDeploymentsStatusCommand(deploymentsCmd2) {
|
|
|
2476
2478
|
["Status", d.status],
|
|
2477
2479
|
["Provider", d.provider ?? "-"],
|
|
2478
2480
|
["Provider ID", d.providerDeploymentId ?? "-"],
|
|
2479
|
-
["URL", d.
|
|
2481
|
+
["URL", d.url ?? "-"],
|
|
2480
2482
|
["Created", new Date(d.createdAt).toLocaleString()],
|
|
2481
2483
|
["Updated", new Date(d.updatedAt).toLocaleString()],
|
|
2482
|
-
...
|
|
2484
|
+
...errorMessage ? [["Error", errorMessage]] : []
|
|
2483
2485
|
]
|
|
2484
2486
|
);
|
|
2485
2487
|
}
|
|
@@ -3082,6 +3084,594 @@ function formatSize2(gb) {
|
|
|
3082
3084
|
return `${gb.toFixed(2)} GB`;
|
|
3083
3085
|
}
|
|
3084
3086
|
|
|
3087
|
+
// src/commands/diagnose/metrics.ts
|
|
3088
|
+
var METRIC_LABELS = {
|
|
3089
|
+
cpu_usage: "CPU Usage",
|
|
3090
|
+
memory_usage: "Memory Usage",
|
|
3091
|
+
disk_usage: "Disk Usage",
|
|
3092
|
+
network_in: "Network In",
|
|
3093
|
+
network_out: "Network Out"
|
|
3094
|
+
};
|
|
3095
|
+
var NETWORK_METRICS = /* @__PURE__ */ new Set(["network_in", "network_out"]);
|
|
3096
|
+
function formatValue(metric, value) {
|
|
3097
|
+
if (NETWORK_METRICS.has(metric)) {
|
|
3098
|
+
return formatBytes(value) + "/s";
|
|
3099
|
+
}
|
|
3100
|
+
return `${value.toFixed(1)}%`;
|
|
3101
|
+
}
|
|
3102
|
+
function formatBytes(bytes) {
|
|
3103
|
+
if (bytes < 1024) return `${bytes.toFixed(1)} B`;
|
|
3104
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
3105
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
3106
|
+
}
|
|
3107
|
+
function computeStats(data) {
|
|
3108
|
+
if (data.length === 0) return { latest: 0, avg: 0, max: 0 };
|
|
3109
|
+
const latest = data[data.length - 1].value;
|
|
3110
|
+
let sum = 0;
|
|
3111
|
+
let max = -Infinity;
|
|
3112
|
+
for (const d of data) {
|
|
3113
|
+
sum += d.value;
|
|
3114
|
+
if (d.value > max) max = d.value;
|
|
3115
|
+
}
|
|
3116
|
+
return { latest, avg: sum / data.length, max };
|
|
3117
|
+
}
|
|
3118
|
+
function aggregateByMetric(series) {
|
|
3119
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3120
|
+
for (const s of series) {
|
|
3121
|
+
const existing = grouped.get(s.metric);
|
|
3122
|
+
if (existing) existing.push(s);
|
|
3123
|
+
else grouped.set(s.metric, [s]);
|
|
3124
|
+
}
|
|
3125
|
+
const result = [];
|
|
3126
|
+
for (const [metric, group] of grouped) {
|
|
3127
|
+
if (group.length === 1 || !NETWORK_METRICS.has(metric)) {
|
|
3128
|
+
result.push(group[0]);
|
|
3129
|
+
continue;
|
|
3130
|
+
}
|
|
3131
|
+
const tsMap = /* @__PURE__ */ new Map();
|
|
3132
|
+
for (const s of group) {
|
|
3133
|
+
for (const d of s.data) {
|
|
3134
|
+
tsMap.set(d.timestamp, (tsMap.get(d.timestamp) ?? 0) + d.value);
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
const merged = [...tsMap.entries()].sort((a, b) => a[0] - b[0]).map(([timestamp, value]) => ({ timestamp, value }));
|
|
3138
|
+
result.push({ metric, instance_id: "aggregate", data: merged });
|
|
3139
|
+
}
|
|
3140
|
+
return result;
|
|
3141
|
+
}
|
|
3142
|
+
async function fetchMetricsSummary(projectId, apiUrl) {
|
|
3143
|
+
const res = await platformFetch(`/projects/v1/${projectId}/metrics?range=1h`, {}, apiUrl);
|
|
3144
|
+
return await res.json();
|
|
3145
|
+
}
|
|
3146
|
+
function registerDiagnoseMetricsCommand(diagnoseCmd2) {
|
|
3147
|
+
diagnoseCmd2.command("metrics").description("Display EC2 instance metrics (CPU, memory, disk, network)").option("--range <range>", "Time range: 1h, 6h, 24h, 7d", "1h").option("--metrics <list>", "Comma-separated metrics to query").action(async (opts, cmd) => {
|
|
3148
|
+
const { json, apiUrl } = getRootOpts(cmd);
|
|
3149
|
+
try {
|
|
3150
|
+
await requireAuth(apiUrl);
|
|
3151
|
+
const config = getProjectConfig();
|
|
3152
|
+
if (!config) throw new ProjectNotLinkedError();
|
|
3153
|
+
if (config.project_id === "oss-project") {
|
|
3154
|
+
throw new CLIError(
|
|
3155
|
+
"Metrics requires InsForge Platform login. Not available when linked via --api-key."
|
|
3156
|
+
);
|
|
3157
|
+
}
|
|
3158
|
+
const params = new URLSearchParams({ range: opts.range });
|
|
3159
|
+
if (opts.metrics) params.set("metrics", opts.metrics);
|
|
3160
|
+
const res = await platformFetch(
|
|
3161
|
+
`/projects/v1/${config.project_id}/metrics?${params.toString()}`,
|
|
3162
|
+
{},
|
|
3163
|
+
apiUrl
|
|
3164
|
+
);
|
|
3165
|
+
const data = await res.json();
|
|
3166
|
+
const aggregated = aggregateByMetric(data.metrics);
|
|
3167
|
+
if (json) {
|
|
3168
|
+
const enriched = {
|
|
3169
|
+
...data,
|
|
3170
|
+
metrics: aggregated.map((m) => {
|
|
3171
|
+
const stats = computeStats(m.data);
|
|
3172
|
+
return { ...m, latest: stats.latest, avg: stats.avg, max: stats.max };
|
|
3173
|
+
})
|
|
3174
|
+
};
|
|
3175
|
+
outputJson(enriched);
|
|
3176
|
+
} else {
|
|
3177
|
+
if (!aggregated.length) {
|
|
3178
|
+
console.log("No metrics data available.");
|
|
3179
|
+
return;
|
|
3180
|
+
}
|
|
3181
|
+
const headers = ["Metric", "Latest", "Avg", "Max", "Range"];
|
|
3182
|
+
const rows = aggregated.map((m) => {
|
|
3183
|
+
const stats = computeStats(m.data);
|
|
3184
|
+
return [
|
|
3185
|
+
METRIC_LABELS[m.metric] ?? m.metric,
|
|
3186
|
+
formatValue(m.metric, stats.latest),
|
|
3187
|
+
formatValue(m.metric, stats.avg),
|
|
3188
|
+
formatValue(m.metric, stats.max),
|
|
3189
|
+
data.range
|
|
3190
|
+
];
|
|
3191
|
+
});
|
|
3192
|
+
outputTable(headers, rows);
|
|
3193
|
+
}
|
|
3194
|
+
await reportCliUsage("cli.diagnose.metrics", true);
|
|
3195
|
+
} catch (err) {
|
|
3196
|
+
await reportCliUsage("cli.diagnose.metrics", false);
|
|
3197
|
+
handleError(err, json);
|
|
3198
|
+
}
|
|
3199
|
+
});
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
// src/commands/diagnose/advisor.ts
|
|
3203
|
+
async function fetchAdvisorSummary(projectId, apiUrl) {
|
|
3204
|
+
const res = await platformFetch(`/projects/v1/${projectId}/advisor/latest`, {}, apiUrl);
|
|
3205
|
+
return await res.json();
|
|
3206
|
+
}
|
|
3207
|
+
function registerDiagnoseAdvisorCommand(diagnoseCmd2) {
|
|
3208
|
+
diagnoseCmd2.command("advisor").description("Display latest advisor scan results and issues").option("--severity <level>", "Filter by severity: critical, warning, info").option("--category <cat>", "Filter by category: security, performance, health").option("--limit <n>", "Maximum number of issues to return", "50").action(async (opts, cmd) => {
|
|
3209
|
+
const { json, apiUrl } = getRootOpts(cmd);
|
|
3210
|
+
try {
|
|
3211
|
+
await requireAuth(apiUrl);
|
|
3212
|
+
const config = getProjectConfig();
|
|
3213
|
+
if (!config) throw new ProjectNotLinkedError();
|
|
3214
|
+
if (config.project_id === "oss-project") {
|
|
3215
|
+
throw new CLIError(
|
|
3216
|
+
"Advisor requires InsForge Platform login. Not available when linked via --api-key."
|
|
3217
|
+
);
|
|
3218
|
+
}
|
|
3219
|
+
const projectId = config.project_id;
|
|
3220
|
+
const scanRes = await platformFetch(
|
|
3221
|
+
`/projects/v1/${projectId}/advisor/latest`,
|
|
3222
|
+
{},
|
|
3223
|
+
apiUrl
|
|
3224
|
+
);
|
|
3225
|
+
const scan = await scanRes.json();
|
|
3226
|
+
const issueParams = new URLSearchParams();
|
|
3227
|
+
if (opts.severity) issueParams.set("severity", opts.severity);
|
|
3228
|
+
if (opts.category) issueParams.set("category", opts.category);
|
|
3229
|
+
issueParams.set("limit", opts.limit);
|
|
3230
|
+
const issuesRes = await platformFetch(
|
|
3231
|
+
`/projects/v1/${projectId}/advisor/latest/issues?${issueParams.toString()}`,
|
|
3232
|
+
{},
|
|
3233
|
+
apiUrl
|
|
3234
|
+
);
|
|
3235
|
+
const issuesData = await issuesRes.json();
|
|
3236
|
+
if (json) {
|
|
3237
|
+
outputJson({ scan, issues: issuesData.issues });
|
|
3238
|
+
} else {
|
|
3239
|
+
const date = new Date(scan.scannedAt).toLocaleDateString();
|
|
3240
|
+
const s = scan.summary;
|
|
3241
|
+
console.log(
|
|
3242
|
+
`Scan: ${date} (${scan.status}) \u2014 ${s.critical} critical, ${s.warning} warning, ${s.info} info
|
|
3243
|
+
`
|
|
3244
|
+
);
|
|
3245
|
+
if (!issuesData.issues || issuesData.issues.length === 0) {
|
|
3246
|
+
console.log("No issues found.");
|
|
3247
|
+
return;
|
|
3248
|
+
}
|
|
3249
|
+
const headers = ["Severity", "Category", "Affected Object", "Title"];
|
|
3250
|
+
const rows = issuesData.issues.map((issue) => [
|
|
3251
|
+
issue.severity,
|
|
3252
|
+
issue.category,
|
|
3253
|
+
issue.affectedObject,
|
|
3254
|
+
issue.title
|
|
3255
|
+
]);
|
|
3256
|
+
outputTable(headers, rows);
|
|
3257
|
+
}
|
|
3258
|
+
await reportCliUsage("cli.diagnose.advisor", true);
|
|
3259
|
+
} catch (err) {
|
|
3260
|
+
await reportCliUsage("cli.diagnose.advisor", false);
|
|
3261
|
+
handleError(err, json);
|
|
3262
|
+
}
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
// src/commands/diagnose/db.ts
|
|
3267
|
+
var DB_CHECKS = {
|
|
3268
|
+
connections: {
|
|
3269
|
+
label: "Connections",
|
|
3270
|
+
sql: `SELECT
|
|
3271
|
+
(SELECT count(*) FROM pg_stat_activity WHERE state IS NOT NULL) AS active,
|
|
3272
|
+
(SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS max`,
|
|
3273
|
+
format(rows) {
|
|
3274
|
+
const r = rows[0] ?? {};
|
|
3275
|
+
console.log(` Active: ${r.active} / ${r.max}`);
|
|
3276
|
+
}
|
|
3277
|
+
},
|
|
3278
|
+
"slow-queries": {
|
|
3279
|
+
label: "Slow Queries (>5s)",
|
|
3280
|
+
sql: `SELECT pid, now() - query_start AS duration, substring(query for 80) AS query
|
|
3281
|
+
FROM pg_stat_activity
|
|
3282
|
+
WHERE state = 'active' AND now() - query_start > interval '5 seconds'
|
|
3283
|
+
ORDER BY query_start ASC`,
|
|
3284
|
+
format(rows) {
|
|
3285
|
+
if (rows.length === 0) {
|
|
3286
|
+
console.log(" None");
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
const headers = ["PID", "Duration", "Query"];
|
|
3290
|
+
const tableRows = rows.map((r) => [
|
|
3291
|
+
String(r.pid ?? ""),
|
|
3292
|
+
String(r.duration ?? ""),
|
|
3293
|
+
String(r.query ?? "")
|
|
3294
|
+
]);
|
|
3295
|
+
outputTable(headers, tableRows);
|
|
3296
|
+
}
|
|
3297
|
+
},
|
|
3298
|
+
bloat: {
|
|
3299
|
+
label: "Table Bloat (top 10)",
|
|
3300
|
+
sql: `SELECT schemaname || '.' || relname AS table, n_dead_tup AS dead_tuples
|
|
3301
|
+
FROM pg_stat_user_tables
|
|
3302
|
+
ORDER BY n_dead_tup DESC
|
|
3303
|
+
LIMIT 10`,
|
|
3304
|
+
format(rows) {
|
|
3305
|
+
if (rows.length === 0) {
|
|
3306
|
+
console.log(" No user tables found.");
|
|
3307
|
+
return;
|
|
3308
|
+
}
|
|
3309
|
+
const headers = ["Table", "Dead Tuples"];
|
|
3310
|
+
const tableRows = rows.map((r) => [
|
|
3311
|
+
String(r.table ?? ""),
|
|
3312
|
+
String(r.dead_tuples ?? 0)
|
|
3313
|
+
]);
|
|
3314
|
+
outputTable(headers, tableRows);
|
|
3315
|
+
}
|
|
3316
|
+
},
|
|
3317
|
+
size: {
|
|
3318
|
+
label: "Table Sizes (top 10)",
|
|
3319
|
+
sql: `SELECT schemaname || '.' || relname AS table,
|
|
3320
|
+
pg_size_pretty(pg_total_relation_size(relid)) AS size
|
|
3321
|
+
FROM pg_stat_user_tables
|
|
3322
|
+
ORDER BY pg_total_relation_size(relid) DESC
|
|
3323
|
+
LIMIT 10`,
|
|
3324
|
+
format(rows) {
|
|
3325
|
+
if (rows.length === 0) {
|
|
3326
|
+
console.log(" No user tables found.");
|
|
3327
|
+
return;
|
|
3328
|
+
}
|
|
3329
|
+
const headers = ["Table", "Size"];
|
|
3330
|
+
const tableRows = rows.map((r) => [
|
|
3331
|
+
String(r.table ?? ""),
|
|
3332
|
+
String(r.size ?? "")
|
|
3333
|
+
]);
|
|
3334
|
+
outputTable(headers, tableRows);
|
|
3335
|
+
}
|
|
3336
|
+
},
|
|
3337
|
+
"index-usage": {
|
|
3338
|
+
label: "Index Usage (worst 10)",
|
|
3339
|
+
sql: `SELECT schemaname || '.' || relname AS table, idx_scan, seq_scan,
|
|
3340
|
+
CASE WHEN (idx_scan + seq_scan) > 0
|
|
3341
|
+
THEN round(100.0 * idx_scan / (idx_scan + seq_scan), 1)
|
|
3342
|
+
ELSE 0 END AS idx_ratio
|
|
3343
|
+
FROM pg_stat_user_tables
|
|
3344
|
+
WHERE (idx_scan + seq_scan) > 0
|
|
3345
|
+
ORDER BY idx_ratio ASC
|
|
3346
|
+
LIMIT 10`,
|
|
3347
|
+
format(rows) {
|
|
3348
|
+
if (rows.length === 0) {
|
|
3349
|
+
console.log(" No scan data available.");
|
|
3350
|
+
return;
|
|
3351
|
+
}
|
|
3352
|
+
const headers = ["Table", "Index Scans", "Seq Scans", "Index Ratio"];
|
|
3353
|
+
const tableRows = rows.map((r) => [
|
|
3354
|
+
String(r.table ?? ""),
|
|
3355
|
+
String(r.idx_scan ?? 0),
|
|
3356
|
+
String(r.seq_scan ?? 0),
|
|
3357
|
+
`${r.idx_ratio ?? 0}%`
|
|
3358
|
+
]);
|
|
3359
|
+
outputTable(headers, tableRows);
|
|
3360
|
+
}
|
|
3361
|
+
},
|
|
3362
|
+
locks: {
|
|
3363
|
+
label: "Waiting Locks",
|
|
3364
|
+
sql: `SELECT pid, mode, relation::regclass AS relation, granted
|
|
3365
|
+
FROM pg_locks
|
|
3366
|
+
WHERE NOT granted`,
|
|
3367
|
+
format(rows) {
|
|
3368
|
+
if (rows.length === 0) {
|
|
3369
|
+
console.log(" None");
|
|
3370
|
+
return;
|
|
3371
|
+
}
|
|
3372
|
+
const headers = ["PID", "Mode", "Relation", "Granted"];
|
|
3373
|
+
const tableRows = rows.map((r) => [
|
|
3374
|
+
String(r.pid ?? ""),
|
|
3375
|
+
String(r.mode ?? ""),
|
|
3376
|
+
String(r.relation ?? ""),
|
|
3377
|
+
String(r.granted ?? "")
|
|
3378
|
+
]);
|
|
3379
|
+
outputTable(headers, tableRows);
|
|
3380
|
+
}
|
|
3381
|
+
},
|
|
3382
|
+
"cache-hit": {
|
|
3383
|
+
label: "Cache Hit Ratio",
|
|
3384
|
+
sql: `SELECT CASE WHEN sum(heap_blks_hit + heap_blks_read) > 0
|
|
3385
|
+
THEN round(100.0 * sum(heap_blks_hit) / sum(heap_blks_hit + heap_blks_read), 1)
|
|
3386
|
+
ELSE 0 END AS ratio
|
|
3387
|
+
FROM pg_statio_user_tables`,
|
|
3388
|
+
format(rows) {
|
|
3389
|
+
const ratio = rows[0]?.ratio ?? 0;
|
|
3390
|
+
console.log(` ${ratio}%`);
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
};
|
|
3394
|
+
var ALL_CHECKS = Object.keys(DB_CHECKS);
|
|
3395
|
+
async function runDbChecks() {
|
|
3396
|
+
const results = {};
|
|
3397
|
+
for (const key of ALL_CHECKS) {
|
|
3398
|
+
try {
|
|
3399
|
+
const { rows } = await runRawSql(DB_CHECKS[key].sql, true);
|
|
3400
|
+
results[key] = rows;
|
|
3401
|
+
} catch {
|
|
3402
|
+
results[key] = [];
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
return results;
|
|
3406
|
+
}
|
|
3407
|
+
function registerDiagnoseDbCommand(diagnoseCmd2) {
|
|
3408
|
+
diagnoseCmd2.command("db").description("Run database health checks (connections, bloat, index usage, etc.)").option("--check <checks>", "Comma-separated checks: " + ALL_CHECKS.join(", "), "all").action(async (opts, cmd) => {
|
|
3409
|
+
const { json } = getRootOpts(cmd);
|
|
3410
|
+
try {
|
|
3411
|
+
await requireAuth();
|
|
3412
|
+
if (!getProjectConfig()) throw new ProjectNotLinkedError();
|
|
3413
|
+
const checkNames = opts.check === "all" ? ALL_CHECKS : opts.check.split(",").map((s) => s.trim());
|
|
3414
|
+
const results = {};
|
|
3415
|
+
for (const name of checkNames) {
|
|
3416
|
+
const check = DB_CHECKS[name];
|
|
3417
|
+
if (!check) {
|
|
3418
|
+
console.error(`Unknown check: ${name}. Available: ${ALL_CHECKS.join(", ")}`);
|
|
3419
|
+
continue;
|
|
3420
|
+
}
|
|
3421
|
+
try {
|
|
3422
|
+
const { rows } = await runRawSql(check.sql, true);
|
|
3423
|
+
results[name] = rows;
|
|
3424
|
+
} catch (err) {
|
|
3425
|
+
results[name] = [];
|
|
3426
|
+
if (!json) {
|
|
3427
|
+
console.error(` Failed to run ${name}: ${err instanceof Error ? err.message : err}`);
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
if (json) {
|
|
3432
|
+
outputJson(results);
|
|
3433
|
+
} else {
|
|
3434
|
+
for (const name of checkNames) {
|
|
3435
|
+
const check = DB_CHECKS[name];
|
|
3436
|
+
if (!check) continue;
|
|
3437
|
+
console.log(`
|
|
3438
|
+
\u2500\u2500 ${check.label} ${"\u2500".repeat(Math.max(0, 40 - check.label.length))}`);
|
|
3439
|
+
check.format(results[name] ?? []);
|
|
3440
|
+
}
|
|
3441
|
+
console.log("");
|
|
3442
|
+
}
|
|
3443
|
+
await reportCliUsage("cli.diagnose.db", true);
|
|
3444
|
+
} catch (err) {
|
|
3445
|
+
await reportCliUsage("cli.diagnose.db", false);
|
|
3446
|
+
handleError(err, json);
|
|
3447
|
+
}
|
|
3448
|
+
});
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
// src/commands/diagnose/logs.ts
|
|
3452
|
+
var LOG_SOURCES = ["insforge.logs", "postgREST.logs", "postgres.logs", "function.logs"];
|
|
3453
|
+
var ERROR_PATTERN = /\b(error|fatal|panic)\b/i;
|
|
3454
|
+
function parseLogEntry(entry) {
|
|
3455
|
+
if (typeof entry === "string") {
|
|
3456
|
+
return { ts: "", msg: entry };
|
|
3457
|
+
}
|
|
3458
|
+
const e = entry;
|
|
3459
|
+
const ts = String(e.timestamp ?? e.time ?? "");
|
|
3460
|
+
const msg = String(e.message ?? e.msg ?? e.log ?? JSON.stringify(e));
|
|
3461
|
+
return { ts, msg };
|
|
3462
|
+
}
|
|
3463
|
+
async function fetchSourceLogs(source, limit) {
|
|
3464
|
+
const res = await ossFetch(`/api/logs/${encodeURIComponent(source)}?limit=${limit}`);
|
|
3465
|
+
const data = await res.json();
|
|
3466
|
+
const logs = Array.isArray(data) ? data : data.logs ?? [];
|
|
3467
|
+
const errors = [];
|
|
3468
|
+
for (const entry of logs) {
|
|
3469
|
+
const { ts, msg } = parseLogEntry(entry);
|
|
3470
|
+
if (ERROR_PATTERN.test(msg)) {
|
|
3471
|
+
errors.push({ timestamp: ts, message: msg, source });
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
return { source, total: logs.length, errors };
|
|
3475
|
+
}
|
|
3476
|
+
async function fetchLogsSummary(limit = 100) {
|
|
3477
|
+
const results = [];
|
|
3478
|
+
for (const source of LOG_SOURCES) {
|
|
3479
|
+
try {
|
|
3480
|
+
results.push(await fetchSourceLogs(source, limit));
|
|
3481
|
+
} catch {
|
|
3482
|
+
results.push({ source, total: 0, errors: [] });
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
return results;
|
|
3486
|
+
}
|
|
3487
|
+
function registerDiagnoseLogsCommand(diagnoseCmd2) {
|
|
3488
|
+
diagnoseCmd2.command("logs").description("Aggregate error-level logs from all backend sources").option("--source <name>", "Specific log source to check").option("--limit <n>", "Number of log entries per source", "100").action(async (opts, cmd) => {
|
|
3489
|
+
const { json } = getRootOpts(cmd);
|
|
3490
|
+
try {
|
|
3491
|
+
await requireAuth();
|
|
3492
|
+
if (!getProjectConfig()) throw new ProjectNotLinkedError();
|
|
3493
|
+
const limit = parseInt(opts.limit, 10) || 100;
|
|
3494
|
+
const sources = opts.source ? [opts.source] : [...LOG_SOURCES];
|
|
3495
|
+
const summaries = [];
|
|
3496
|
+
for (const source of sources) {
|
|
3497
|
+
try {
|
|
3498
|
+
summaries.push(await fetchSourceLogs(source, limit));
|
|
3499
|
+
} catch {
|
|
3500
|
+
summaries.push({ source, total: 0, errors: [] });
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
if (json) {
|
|
3504
|
+
outputJson({ sources: summaries });
|
|
3505
|
+
} else {
|
|
3506
|
+
const headers = ["Source", "Total", "Errors"];
|
|
3507
|
+
const rows = summaries.map((s) => [s.source, String(s.total), String(s.errors.length)]);
|
|
3508
|
+
outputTable(headers, rows);
|
|
3509
|
+
const allErrors = summaries.flatMap((s) => s.errors);
|
|
3510
|
+
if (allErrors.length > 0) {
|
|
3511
|
+
console.log("\n\u2500\u2500 Error Details " + "\u2500".repeat(30));
|
|
3512
|
+
for (const err of allErrors) {
|
|
3513
|
+
const prefix = err.timestamp ? `[${err.source}] ${err.timestamp}` : `[${err.source}]`;
|
|
3514
|
+
console.log(`
|
|
3515
|
+
${prefix}`);
|
|
3516
|
+
console.log(` ${err.message}`);
|
|
3517
|
+
}
|
|
3518
|
+
console.log("");
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
await reportCliUsage("cli.diagnose.logs", true);
|
|
3522
|
+
} catch (err) {
|
|
3523
|
+
await reportCliUsage("cli.diagnose.logs", false);
|
|
3524
|
+
handleError(err, json);
|
|
3525
|
+
}
|
|
3526
|
+
});
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
// src/commands/diagnose/index.ts
|
|
3530
|
+
function sectionHeader(title) {
|
|
3531
|
+
return `\u2500\u2500 ${title} ${"\u2500".repeat(Math.max(0, 44 - title.length))}`;
|
|
3532
|
+
}
|
|
3533
|
+
function registerDiagnoseCommands(diagnoseCmd2) {
|
|
3534
|
+
diagnoseCmd2.description("Backend diagnostics \u2014 run with no subcommand for a full health report").action(async (_opts, cmd) => {
|
|
3535
|
+
const { json, apiUrl } = getRootOpts(cmd);
|
|
3536
|
+
try {
|
|
3537
|
+
await requireAuth(apiUrl);
|
|
3538
|
+
const config = getProjectConfig();
|
|
3539
|
+
if (!config) throw new ProjectNotLinkedError();
|
|
3540
|
+
const projectId = config.project_id;
|
|
3541
|
+
const projectName = config.project_name;
|
|
3542
|
+
const ossMode = config.project_id === "oss-project";
|
|
3543
|
+
const metricsPromise = ossMode ? Promise.reject(new Error("Platform login required (linked via --api-key)")) : fetchMetricsSummary(projectId, apiUrl);
|
|
3544
|
+
const advisorPromise = ossMode ? Promise.reject(new Error("Platform login required (linked via --api-key)")) : fetchAdvisorSummary(projectId, apiUrl);
|
|
3545
|
+
const [metricsResult, advisorResult, dbResult, logsResult] = await Promise.allSettled([
|
|
3546
|
+
metricsPromise,
|
|
3547
|
+
advisorPromise,
|
|
3548
|
+
runDbChecks(),
|
|
3549
|
+
fetchLogsSummary(100)
|
|
3550
|
+
]);
|
|
3551
|
+
if (json) {
|
|
3552
|
+
const report = { project: projectName, errors: [] };
|
|
3553
|
+
const errors = [];
|
|
3554
|
+
if (metricsResult.status === "fulfilled") {
|
|
3555
|
+
const data = metricsResult.value;
|
|
3556
|
+
report.metrics = data.metrics.map((m) => {
|
|
3557
|
+
if (m.data.length === 0) return { metric: m.metric, latest: null, avg: null, max: null };
|
|
3558
|
+
let sum = 0;
|
|
3559
|
+
let max = -Infinity;
|
|
3560
|
+
for (const d of m.data) {
|
|
3561
|
+
sum += d.value;
|
|
3562
|
+
if (d.value > max) max = d.value;
|
|
3563
|
+
}
|
|
3564
|
+
return {
|
|
3565
|
+
metric: m.metric,
|
|
3566
|
+
latest: m.data[m.data.length - 1].value,
|
|
3567
|
+
avg: sum / m.data.length,
|
|
3568
|
+
max
|
|
3569
|
+
};
|
|
3570
|
+
});
|
|
3571
|
+
} else {
|
|
3572
|
+
report.metrics = null;
|
|
3573
|
+
errors.push(metricsResult.reason?.message ?? "Metrics unavailable");
|
|
3574
|
+
}
|
|
3575
|
+
if (advisorResult.status === "fulfilled") {
|
|
3576
|
+
report.advisor = advisorResult.value;
|
|
3577
|
+
} else {
|
|
3578
|
+
report.advisor = null;
|
|
3579
|
+
errors.push(advisorResult.reason?.message ?? "Advisor unavailable");
|
|
3580
|
+
}
|
|
3581
|
+
if (dbResult.status === "fulfilled") {
|
|
3582
|
+
report.db = dbResult.value;
|
|
3583
|
+
} else {
|
|
3584
|
+
report.db = null;
|
|
3585
|
+
errors.push(dbResult.reason?.message ?? "DB checks unavailable");
|
|
3586
|
+
}
|
|
3587
|
+
if (logsResult.status === "fulfilled") {
|
|
3588
|
+
report.logs = logsResult.value;
|
|
3589
|
+
} else {
|
|
3590
|
+
report.logs = null;
|
|
3591
|
+
errors.push(logsResult.reason?.message ?? "Logs unavailable");
|
|
3592
|
+
}
|
|
3593
|
+
report.errors = errors;
|
|
3594
|
+
outputJson(report);
|
|
3595
|
+
} else {
|
|
3596
|
+
console.log(`
|
|
3597
|
+
InsForge Health Report \u2014 ${projectName}
|
|
3598
|
+
`);
|
|
3599
|
+
console.log(sectionHeader("System Metrics (last 1h)"));
|
|
3600
|
+
if (metricsResult.status === "fulfilled") {
|
|
3601
|
+
const metrics = metricsResult.value.metrics;
|
|
3602
|
+
if (metrics.length === 0) {
|
|
3603
|
+
console.log(" No metrics data available.");
|
|
3604
|
+
} else {
|
|
3605
|
+
const vals = {};
|
|
3606
|
+
for (const m of metrics) {
|
|
3607
|
+
if (m.data.length > 0) vals[m.metric] = m.data[m.data.length - 1].value;
|
|
3608
|
+
}
|
|
3609
|
+
const cpu = vals.cpu_usage !== void 0 ? `${vals.cpu_usage.toFixed(1)}%` : "N/A";
|
|
3610
|
+
const mem = vals.memory_usage !== void 0 ? `${vals.memory_usage.toFixed(1)}%` : "N/A";
|
|
3611
|
+
const disk = vals.disk_usage !== void 0 ? `${vals.disk_usage.toFixed(1)}%` : "N/A";
|
|
3612
|
+
const netIn = vals.network_in !== void 0 ? formatBytesCompact(vals.network_in) + "/s" : "N/A";
|
|
3613
|
+
const netOut = vals.network_out !== void 0 ? formatBytesCompact(vals.network_out) + "/s" : "N/A";
|
|
3614
|
+
console.log(` CPU: ${cpu} Memory: ${mem}`);
|
|
3615
|
+
console.log(` Disk: ${disk} Network: \u2193${netIn} \u2191${netOut}`);
|
|
3616
|
+
}
|
|
3617
|
+
} else {
|
|
3618
|
+
console.log(` N/A \u2014 ${metricsResult.reason?.message ?? "unavailable"}`);
|
|
3619
|
+
}
|
|
3620
|
+
console.log("\n" + sectionHeader("Advisor Scan"));
|
|
3621
|
+
if (advisorResult.status === "fulfilled") {
|
|
3622
|
+
const scan = advisorResult.value;
|
|
3623
|
+
const s = scan.summary;
|
|
3624
|
+
const date = new Date(scan.scannedAt).toLocaleDateString();
|
|
3625
|
+
console.log(` ${date} (${scan.status}) \u2014 ${s.critical} critical \xB7 ${s.warning} warning \xB7 ${s.info} info`);
|
|
3626
|
+
} else {
|
|
3627
|
+
console.log(` N/A \u2014 ${advisorResult.reason?.message ?? "unavailable"}`);
|
|
3628
|
+
}
|
|
3629
|
+
console.log("\n" + sectionHeader("Database"));
|
|
3630
|
+
if (dbResult.status === "fulfilled") {
|
|
3631
|
+
const db = dbResult.value;
|
|
3632
|
+
const conn = db.connections?.[0];
|
|
3633
|
+
const cache = db["cache-hit"]?.[0];
|
|
3634
|
+
const deadTuples = (db.bloat ?? []).reduce(
|
|
3635
|
+
(sum, r) => sum + (Number(r.dead_tuples) || 0),
|
|
3636
|
+
0
|
|
3637
|
+
);
|
|
3638
|
+
const lockCount = (db.locks ?? []).length;
|
|
3639
|
+
console.log(
|
|
3640
|
+
` Connections: ${conn?.active ?? "?"}/${conn?.max ?? "?"} Cache Hit: ${cache?.ratio ?? "?"}%`
|
|
3641
|
+
);
|
|
3642
|
+
console.log(
|
|
3643
|
+
` Dead tuples: ${deadTuples.toLocaleString()} Locks waiting: ${lockCount}`
|
|
3644
|
+
);
|
|
3645
|
+
} else {
|
|
3646
|
+
console.log(` N/A \u2014 ${dbResult.reason?.message ?? "unavailable"}`);
|
|
3647
|
+
}
|
|
3648
|
+
console.log("\n" + sectionHeader("Recent Errors (last 100 logs/source)"));
|
|
3649
|
+
if (logsResult.status === "fulfilled") {
|
|
3650
|
+
const summaries = logsResult.value;
|
|
3651
|
+
const parts = summaries.map((s) => `${s.source}: ${s.errors.length}`);
|
|
3652
|
+
console.log(` ${parts.join(" ")}`);
|
|
3653
|
+
} else {
|
|
3654
|
+
console.log(` N/A \u2014 ${logsResult.reason?.message ?? "unavailable"}`);
|
|
3655
|
+
}
|
|
3656
|
+
console.log("");
|
|
3657
|
+
}
|
|
3658
|
+
await reportCliUsage("cli.diagnose", true);
|
|
3659
|
+
} catch (err) {
|
|
3660
|
+
await reportCliUsage("cli.diagnose", false);
|
|
3661
|
+
handleError(err, json);
|
|
3662
|
+
}
|
|
3663
|
+
});
|
|
3664
|
+
registerDiagnoseMetricsCommand(diagnoseCmd2);
|
|
3665
|
+
registerDiagnoseAdvisorCommand(diagnoseCmd2);
|
|
3666
|
+
registerDiagnoseDbCommand(diagnoseCmd2);
|
|
3667
|
+
registerDiagnoseLogsCommand(diagnoseCmd2);
|
|
3668
|
+
}
|
|
3669
|
+
function formatBytesCompact(bytes) {
|
|
3670
|
+
if (bytes < 1024) return `${bytes.toFixed(0)}B`;
|
|
3671
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
3672
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3085
3675
|
// src/index.ts
|
|
3086
3676
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
3087
3677
|
var pkg = JSON.parse(readFileSync6(join7(__dirname, "../package.json"), "utf-8"));
|
|
@@ -3149,6 +3739,8 @@ registerSecretsUpdateCommand(secretsCmd);
|
|
|
3149
3739
|
registerSecretsDeleteCommand(secretsCmd);
|
|
3150
3740
|
registerLogsCommand(program);
|
|
3151
3741
|
registerMetadataCommand(program);
|
|
3742
|
+
var diagnoseCmd = program.command("diagnose");
|
|
3743
|
+
registerDiagnoseCommands(diagnoseCmd);
|
|
3152
3744
|
var schedulesCmd = program.command("schedules").description("Manage scheduled tasks (cron jobs)");
|
|
3153
3745
|
registerSchedulesListCommand(schedulesCmd);
|
|
3154
3746
|
registerSchedulesGetCommand(schedulesCmd);
|