@solcreek/cli 0.4.18 → 0.4.20
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/deploy.js +3 -1
- package/dist/commands/metrics.d.ts +36 -0
- package/dist/commands/metrics.js +167 -0
- package/dist/index.js +2 -0
- package/dist/utils/sandbox.d.ts +10 -0
- package/dist/utils/sandbox.js +20 -1
- package/package.json +2 -2
package/dist/commands/deploy.js
CHANGED
|
@@ -8,7 +8,7 @@ import { CreekClient, CreekAuthError, detectFramework, resolveConfig, formatDete
|
|
|
8
8
|
import { buildDoctorContext } from "../utils/doctor-context.js";
|
|
9
9
|
import { getToken, getApiUrl } from "../utils/config.js";
|
|
10
10
|
import { collectAssets } from "../utils/bundle.js";
|
|
11
|
-
import { sandboxDeploy, pollSandboxStatus, printSandboxSuccess } from "../utils/sandbox.js";
|
|
11
|
+
import { sandboxDeploy, pollSandboxStatus, printSandboxSuccess, expiresInMinutes } from "../utils/sandbox.js";
|
|
12
12
|
import { prepareDeployBundle } from "../utils/prepare-bundle.js";
|
|
13
13
|
import { BuildLogEmitter } from "../utils/build-log.js";
|
|
14
14
|
import { isTTY, jsonOutput, resolveJsonMode, globalArgs, shouldAutoConfirm, AUTH_BREADCRUMBS, NO_PROJECT_BREADCRUMBS } from "../utils/output.js";
|
|
@@ -648,6 +648,7 @@ async function deployDirectory(dir, jsonMode, tos) {
|
|
|
648
648
|
url: status.previewUrl,
|
|
649
649
|
deployDurationMs: status.deployDurationMs,
|
|
650
650
|
expiresAt: result.expiresAt,
|
|
651
|
+
expiresInMinutes: expiresInMinutes(result.expiresAt),
|
|
651
652
|
assetCount: fileList.length,
|
|
652
653
|
mode: "sandbox",
|
|
653
654
|
}, 0, [
|
|
@@ -777,6 +778,7 @@ async function deploySandbox(cwd, skipBuild, jsonMode = false, resolved, tos) {
|
|
|
777
778
|
url: status.previewUrl,
|
|
778
779
|
deployDurationMs: status.deployDurationMs,
|
|
779
780
|
expiresAt: result.expiresAt,
|
|
781
|
+
expiresInMinutes: expiresInMinutes(result.expiresAt),
|
|
780
782
|
framework: framework ?? null,
|
|
781
783
|
assetCount: fileList.length,
|
|
782
784
|
mode: "sandbox",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `creek metrics` — read project traffic + error aggregates from the
|
|
3
|
+
* control-plane metrics endpoint (Analytics Engine + zone GraphQL).
|
|
4
|
+
*
|
|
5
|
+
* Auth: requires `creek login`. Server enforces tenant isolation from
|
|
6
|
+
* the session — the CLI passes only the project slug.
|
|
7
|
+
*
|
|
8
|
+
* Output shape matches the Dashboard Analytics tab: total requests
|
|
9
|
+
* (including edge-cache hits), worker invocations, error count, and
|
|
10
|
+
* three breakdowns (HTTP method, scriptType, statusBucket). Live p50/
|
|
11
|
+
* p99 CPU times aren't exposed here yet; they live in the Workers
|
|
12
|
+
* GraphQL subset the Dashboard reads separately.
|
|
13
|
+
*
|
|
14
|
+
* Pair with `--json` to pipe into reports / dashboards.
|
|
15
|
+
*/
|
|
16
|
+
export declare const metricsCommand: import("citty").CommandDef<{
|
|
17
|
+
json: {
|
|
18
|
+
type: "boolean";
|
|
19
|
+
description: string;
|
|
20
|
+
default: boolean;
|
|
21
|
+
};
|
|
22
|
+
yes: {
|
|
23
|
+
type: "boolean";
|
|
24
|
+
description: string;
|
|
25
|
+
default: boolean;
|
|
26
|
+
};
|
|
27
|
+
project: {
|
|
28
|
+
type: "string";
|
|
29
|
+
description: string;
|
|
30
|
+
};
|
|
31
|
+
period: {
|
|
32
|
+
type: "string";
|
|
33
|
+
description: string;
|
|
34
|
+
};
|
|
35
|
+
}>;
|
|
36
|
+
//# sourceMappingURL=metrics.d.ts.map
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import { CreekClient, resolveConfig, ConfigNotFoundError, } from "@solcreek/sdk";
|
|
4
|
+
import { getToken, getApiUrl } from "../utils/config.js";
|
|
5
|
+
import { globalArgs, resolveJsonMode, jsonOutput, AUTH_BREADCRUMBS, NO_PROJECT_BREADCRUMBS, } from "../utils/output.js";
|
|
6
|
+
const VALID_PERIODS = ["1h", "6h", "24h", "7d", "30d"];
|
|
7
|
+
/**
|
|
8
|
+
* `creek metrics` — read project traffic + error aggregates from the
|
|
9
|
+
* control-plane metrics endpoint (Analytics Engine + zone GraphQL).
|
|
10
|
+
*
|
|
11
|
+
* Auth: requires `creek login`. Server enforces tenant isolation from
|
|
12
|
+
* the session — the CLI passes only the project slug.
|
|
13
|
+
*
|
|
14
|
+
* Output shape matches the Dashboard Analytics tab: total requests
|
|
15
|
+
* (including edge-cache hits), worker invocations, error count, and
|
|
16
|
+
* three breakdowns (HTTP method, scriptType, statusBucket). Live p50/
|
|
17
|
+
* p99 CPU times aren't exposed here yet; they live in the Workers
|
|
18
|
+
* GraphQL subset the Dashboard reads separately.
|
|
19
|
+
*
|
|
20
|
+
* Pair with `--json` to pipe into reports / dashboards.
|
|
21
|
+
*/
|
|
22
|
+
export const metricsCommand = defineCommand({
|
|
23
|
+
meta: {
|
|
24
|
+
name: "metrics",
|
|
25
|
+
description: "Read request + error metrics for a project. One-shot query — pair with --json for agent use or `| jq` piping.",
|
|
26
|
+
},
|
|
27
|
+
args: {
|
|
28
|
+
project: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "Project slug. Defaults to creek.toml in cwd.",
|
|
31
|
+
},
|
|
32
|
+
period: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: `Time window. One of: ${VALID_PERIODS.join(", ")}. Default: 24h`,
|
|
35
|
+
},
|
|
36
|
+
...globalArgs,
|
|
37
|
+
},
|
|
38
|
+
async run({ args }) {
|
|
39
|
+
const jsonMode = resolveJsonMode(args);
|
|
40
|
+
const token = getToken();
|
|
41
|
+
if (!token) {
|
|
42
|
+
if (jsonMode)
|
|
43
|
+
jsonOutput({ ok: false, error: "not_authenticated" }, 1, AUTH_BREADCRUMBS);
|
|
44
|
+
consola.error("Not authenticated. Run `creek login` first.");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const projectSlug = await resolveProjectSlug(args.project, jsonMode);
|
|
48
|
+
const period = validatePeriod(args.period, jsonMode);
|
|
49
|
+
const client = new CreekClient(getApiUrl(), token);
|
|
50
|
+
let response;
|
|
51
|
+
try {
|
|
52
|
+
response = await client.getMetrics(projectSlug, period);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
56
|
+
if (jsonMode)
|
|
57
|
+
jsonOutput({ ok: false, error: "metrics_failed", message: msg }, 1, []);
|
|
58
|
+
consola.error(`Failed to read metrics: ${msg}`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
if (jsonMode) {
|
|
62
|
+
jsonOutput({ ok: true, project: projectSlug, ...response }, 0, [
|
|
63
|
+
{
|
|
64
|
+
command: `creek logs --project ${projectSlug} --since ${period}`,
|
|
65
|
+
description: "Tail logs for the same window",
|
|
66
|
+
},
|
|
67
|
+
]);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
printHuman(projectSlug, response);
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
function validatePeriod(raw, jsonMode) {
|
|
74
|
+
if (!raw)
|
|
75
|
+
return "24h";
|
|
76
|
+
if (VALID_PERIODS.includes(raw))
|
|
77
|
+
return raw;
|
|
78
|
+
const message = `Invalid --period: ${raw}. Valid: ${VALID_PERIODS.join(", ")}`;
|
|
79
|
+
if (jsonMode)
|
|
80
|
+
jsonOutput({ ok: false, error: "invalid_period", message }, 1, []);
|
|
81
|
+
consola.error(message);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
async function resolveProjectSlug(override, jsonMode) {
|
|
85
|
+
if (override)
|
|
86
|
+
return override;
|
|
87
|
+
let resolved;
|
|
88
|
+
try {
|
|
89
|
+
resolved = resolveConfig(process.cwd());
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
if (err instanceof ConfigNotFoundError) {
|
|
93
|
+
if (jsonMode)
|
|
94
|
+
jsonOutput({ ok: false, error: "no_project", message: "No project config in cwd" }, 1, NO_PROJECT_BREADCRUMBS);
|
|
95
|
+
consola.error("No project config in cwd. Pass --project <slug>.");
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
return resolved.projectName;
|
|
101
|
+
}
|
|
102
|
+
// ─── Human output ────────────────────────────────────────────────────────
|
|
103
|
+
const COLOR = {
|
|
104
|
+
reset: "\x1b[0m",
|
|
105
|
+
dim: "\x1b[2m",
|
|
106
|
+
bold: "\x1b[1m",
|
|
107
|
+
red: "\x1b[31m",
|
|
108
|
+
yellow: "\x1b[33m",
|
|
109
|
+
green: "\x1b[32m",
|
|
110
|
+
cyan: "\x1b[36m",
|
|
111
|
+
gray: "\x1b[90m",
|
|
112
|
+
};
|
|
113
|
+
function tty() {
|
|
114
|
+
return process.stdout.isTTY ?? false;
|
|
115
|
+
}
|
|
116
|
+
function c(s, color) {
|
|
117
|
+
return tty() ? `${COLOR[color]}${s}${COLOR.reset}` : s;
|
|
118
|
+
}
|
|
119
|
+
function fmtNumber(n) {
|
|
120
|
+
if (n === 0)
|
|
121
|
+
return "0";
|
|
122
|
+
if (n < 1000)
|
|
123
|
+
return String(n);
|
|
124
|
+
if (n < 1_000_000)
|
|
125
|
+
return `${(n / 1000).toFixed(1)}k`;
|
|
126
|
+
return `${(n / 1_000_000).toFixed(2)}M`;
|
|
127
|
+
}
|
|
128
|
+
function fmtPct(n, total) {
|
|
129
|
+
if (total === 0)
|
|
130
|
+
return "0%";
|
|
131
|
+
return `${((n / total) * 100).toFixed(1)}%`;
|
|
132
|
+
}
|
|
133
|
+
function printHuman(slug, r) {
|
|
134
|
+
const { totals } = r;
|
|
135
|
+
const cachePct = fmtPct(totals.cachedReqs, totals.reqs);
|
|
136
|
+
const errPct = fmtPct(totals.errs, totals.invocations);
|
|
137
|
+
consola.log("");
|
|
138
|
+
consola.log(` ${c("⬡ creek metrics", "bold")} ${c(`${slug} · ${r.period}`, "dim")}`);
|
|
139
|
+
consola.log("");
|
|
140
|
+
consola.log(` Requests: ${c(fmtNumber(totals.reqs), "bold")}`);
|
|
141
|
+
consola.log(` Cache hits: ${fmtNumber(totals.cachedReqs)} ${c(`(${cachePct})`, "dim")}`);
|
|
142
|
+
consola.log(` Invocations: ${fmtNumber(totals.invocations)} ${c("worker ran", "dim")}`);
|
|
143
|
+
const errColor = totals.errs > 0 ? "red" : "green";
|
|
144
|
+
consola.log(` Errors: ${c(fmtNumber(totals.errs), errColor)} ${c(`(${errPct} of invocations)`, "dim")}`);
|
|
145
|
+
consola.log("");
|
|
146
|
+
printBreakdown("Method", r.breakdowns.method);
|
|
147
|
+
printBreakdown("Script type", r.breakdowns.scriptType);
|
|
148
|
+
printBreakdown("Status bucket", r.breakdowns.statusBucket);
|
|
149
|
+
}
|
|
150
|
+
function printBreakdown(title, rows) {
|
|
151
|
+
if (rows.length === 0)
|
|
152
|
+
return;
|
|
153
|
+
consola.log(` ${c(title, "dim")}`);
|
|
154
|
+
const total = rows.reduce((sum, r) => sum + r.reqs, 0);
|
|
155
|
+
const top = rows.slice(0, 5);
|
|
156
|
+
for (const row of top) {
|
|
157
|
+
const pct = fmtPct(row.reqs, total);
|
|
158
|
+
const label = row.label || c("(empty)", "gray");
|
|
159
|
+
const errBadge = row.errs > 0 ? c(` ${row.errs} err`, "red") : "";
|
|
160
|
+
consola.log(` ${label.padEnd(12)} ${fmtNumber(row.reqs).padStart(8)} ${c(pct.padStart(6), "dim")}${errBadge}`);
|
|
161
|
+
}
|
|
162
|
+
if (rows.length > top.length) {
|
|
163
|
+
consola.log(` ${c(`+${rows.length - top.length} more`, "dim")}`);
|
|
164
|
+
}
|
|
165
|
+
consola.log("");
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=metrics.js.map
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { rollbackCommand } from "./commands/rollback.js";
|
|
|
19
19
|
import { opsCommand } from "./commands/ops.js";
|
|
20
20
|
import { queueCommand } from "./commands/queue.js";
|
|
21
21
|
import { logsCommand } from "./commands/logs.js";
|
|
22
|
+
import { metricsCommand } from "./commands/metrics.js";
|
|
22
23
|
import { doctorCommand } from "./commands/doctor.js";
|
|
23
24
|
import { dbCommand } from "./commands/db.js";
|
|
24
25
|
import { storageCommand } from "./commands/storage.js";
|
|
@@ -49,6 +50,7 @@ const main = defineCommand({
|
|
|
49
50
|
projects: projectsCommand,
|
|
50
51
|
deployments: deploymentsCommand,
|
|
51
52
|
logs: logsCommand,
|
|
53
|
+
metrics: metricsCommand,
|
|
52
54
|
doctor: doctorCommand,
|
|
53
55
|
login: loginCommand,
|
|
54
56
|
whoami: whoamiCommand,
|
package/dist/utils/sandbox.d.ts
CHANGED
|
@@ -60,6 +60,16 @@ export declare function sandboxDeploy(bundle: {
|
|
|
60
60
|
* Poll sandbox status until terminal state.
|
|
61
61
|
*/
|
|
62
62
|
export declare function pollSandboxStatus(statusUrl: string): Promise<SandboxStatusResponse>;
|
|
63
|
+
/**
|
|
64
|
+
* Compute minutes-remaining until expiresAt (ISO string). Clamps at 0.
|
|
65
|
+
* Returned integer is ceiling, so a sandbox with 60:01 left reports 61.
|
|
66
|
+
*/
|
|
67
|
+
export declare function expiresInMinutes(expiresAt: string): number;
|
|
68
|
+
/**
|
|
69
|
+
* Format expiresAt as a local-clock wall time ("HH:MM") for users who
|
|
70
|
+
* don't want to mentally parse ISO timestamps in UTC.
|
|
71
|
+
*/
|
|
72
|
+
export declare function expiresAtLocal(expiresAt: string): string;
|
|
63
73
|
/**
|
|
64
74
|
* Print sandbox success message with claim instructions.
|
|
65
75
|
*/
|
package/dist/utils/sandbox.js
CHANGED
|
@@ -57,13 +57,32 @@ export async function pollSandboxStatus(statusUrl) {
|
|
|
57
57
|
}
|
|
58
58
|
throw new Error("Sandbox deploy timed out");
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Compute minutes-remaining until expiresAt (ISO string). Clamps at 0.
|
|
62
|
+
* Returned integer is ceiling, so a sandbox with 60:01 left reports 61.
|
|
63
|
+
*/
|
|
64
|
+
export function expiresInMinutes(expiresAt) {
|
|
65
|
+
const ms = new Date(expiresAt).getTime() - Date.now();
|
|
66
|
+
return Math.max(0, Math.ceil(ms / 60_000));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Format expiresAt as a local-clock wall time ("HH:MM") for users who
|
|
70
|
+
* don't want to mentally parse ISO timestamps in UTC.
|
|
71
|
+
*/
|
|
72
|
+
export function expiresAtLocal(expiresAt) {
|
|
73
|
+
const d = new Date(expiresAt);
|
|
74
|
+
const hh = d.getHours().toString().padStart(2, "0");
|
|
75
|
+
const mm = d.getMinutes().toString().padStart(2, "0");
|
|
76
|
+
return `${hh}:${mm}`;
|
|
77
|
+
}
|
|
60
78
|
/**
|
|
61
79
|
* Print sandbox success message with claim instructions.
|
|
62
80
|
*/
|
|
63
81
|
export function printSandboxSuccess(previewUrl, expiresAt, sandboxId) {
|
|
82
|
+
const mins = expiresInMinutes(expiresAt);
|
|
64
83
|
consola.success(` Live → ${previewUrl}`);
|
|
65
84
|
consola.info("");
|
|
66
|
-
consola.info(
|
|
85
|
+
consola.info(` Expires in ${mins} minutes (local ${expiresAtLocal(expiresAt)}).`);
|
|
67
86
|
consola.info(" Make it permanent: creek login && creek claim " + sandboxId);
|
|
68
87
|
}
|
|
69
88
|
//# sourceMappingURL=sandbox.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solcreek/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.20",
|
|
4
4
|
"description": "CLI for the Creek deployment platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"esbuild": "^0.25.0",
|
|
34
34
|
"smol-toml": "^1.3.1",
|
|
35
35
|
"ws": "^8.20.0",
|
|
36
|
-
"@solcreek/sdk": "0.4.
|
|
36
|
+
"@solcreek/sdk": "0.4.9"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@testing-library/dom": "^10.4.1",
|