@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 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
- const [project, apiKey] = await Promise.all([
877
- getProject(projectId, apiUrl),
878
- getProjectApiKey(projectId, apiUrl)
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
- if (deployment.status === "ready" || deployment.status === "READY") {
1946
+ const status = deployment.status.toUpperCase();
1947
+ if (status === "READY") {
1946
1948
  break;
1947
1949
  }
1948
- if (deployment.status === "error" || deployment.status === "ERROR" || deployment.status === "canceled") {
1950
+ if (status === "ERROR" || status === "CANCELED") {
1949
1951
  s?.stop("Deployment failed");
1950
- throw new CLIError(deployment.error ?? `Deployment failed with status: ${deployment.status}`);
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 === "ready" || deployment?.status === "READY";
1959
- const liveUrl = isReady ? deployment?.deploymentUrl ?? deployment?.url ?? null : null;
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.deploymentUrl ?? d.url ?? "-"],
2481
+ ["URL", d.url ?? "-"],
2480
2482
  ["Created", new Date(d.createdAt).toLocaleString()],
2481
2483
  ["Updated", new Date(d.updatedAt).toLocaleString()],
2482
- ...d.error ? [["Error", d.error]] : []
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);