@solcreek/cli 0.4.20 → 0.4.22
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/CHANGELOG.md +21 -0
- package/dist/commands/dashboard.d.ts +21 -0
- package/dist/commands/dashboard.js +72 -0
- package/dist/commands/deploy.d.ts +10 -0
- package/dist/commands/deploy.js +252 -0
- package/dist/commands/dev.d.ts +13 -0
- package/dist/commands/dev.js +77 -2
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.js +158 -2
- package/dist/commands/login.js +1 -1
- package/dist/commands/logs.d.ts +12 -0
- package/dist/commands/logs.js +69 -1
- package/dist/commands/restart.d.ts +26 -0
- package/dist/commands/restart.js +55 -0
- package/dist/commands/rollback.d.ts +13 -0
- package/dist/commands/rollback.js +188 -1
- package/dist/commands/stop.d.ts +26 -0
- package/dist/commands/stop.js +65 -0
- package/dist/commands/top.d.ts +28 -0
- package/dist/commands/top.js +171 -0
- package/dist/dev/creekd-runner.d.ts +22 -0
- package/dist/dev/creekd-runner.js +188 -0
- package/dist/index.js +8 -0
- package/dist/utils/auth-server.d.ts +2 -2
- package/dist/utils/auth-server.js +16 -3
- package/dist/utils/creekd-client.d.ts +152 -0
- package/dist/utils/creekd-client.js +144 -0
- package/dist/utils/gitignore.d.ts +2 -0
- package/dist/utils/gitignore.js +32 -0
- package/dist/utils/hostkey.d.ts +39 -0
- package/dist/utils/hostkey.js +84 -0
- package/dist/utils/hosts.d.ts +70 -0
- package/dist/utils/hosts.js +90 -0
- package/dist/utils/local-cache.d.ts +69 -0
- package/dist/utils/local-cache.js +100 -0
- package/dist/utils/nextjs.d.ts +4 -2
- package/dist/utils/nextjs.js +107 -38
- package/dist/utils/prepare-bundle.js +1 -1
- package/dist/utils/top-format.d.ts +4 -0
- package/dist/utils/top-format.js +32 -0
- package/dist/utils/watch.d.ts +81 -0
- package/dist/utils/watch.js +87 -0
- package/package.json +2 -2
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import { globalArgs, resolveJsonMode, jsonOutput, shouldAutoConfirm } from "../utils/output.js";
|
|
4
|
+
import { CreekdClient, CreekdApiError, getCreekdUrl } from "../utils/creekd-client.js";
|
|
5
|
+
export const stopCommand = defineCommand({
|
|
6
|
+
meta: {
|
|
7
|
+
name: "stop",
|
|
8
|
+
description: "Stop an app on a creekd instance",
|
|
9
|
+
},
|
|
10
|
+
args: {
|
|
11
|
+
id: {
|
|
12
|
+
type: "positional",
|
|
13
|
+
description: "App ID to stop",
|
|
14
|
+
required: true,
|
|
15
|
+
},
|
|
16
|
+
server: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "creekd admin API URL (or $CREEKD_URL)",
|
|
19
|
+
},
|
|
20
|
+
token: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Bearer token (or $CREEKD_TOKEN)",
|
|
23
|
+
},
|
|
24
|
+
...globalArgs,
|
|
25
|
+
},
|
|
26
|
+
async run({ args }) {
|
|
27
|
+
const jsonMode = resolveJsonMode(args);
|
|
28
|
+
const client = new CreekdClient(args.server || getCreekdUrl(), args.token || process.env.CREEKD_TOKEN || process.env.CREEKCTL_TOKEN || "");
|
|
29
|
+
const id = args.id;
|
|
30
|
+
if (!shouldAutoConfirm(args) && !jsonMode) {
|
|
31
|
+
const readline = await import("node:readline/promises");
|
|
32
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
33
|
+
const answer = await rl.question(`Stop app "${id}"? [y/N] `);
|
|
34
|
+
rl.close();
|
|
35
|
+
if (answer.toLowerCase() !== "y") {
|
|
36
|
+
consola.info("Aborted.");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
await client.stopApp(id);
|
|
42
|
+
if (jsonMode) {
|
|
43
|
+
jsonOutput({ ok: true, stopped: id }, 0, [
|
|
44
|
+
{ command: `creek top`, description: "Live process overview" },
|
|
45
|
+
]);
|
|
46
|
+
}
|
|
47
|
+
consola.success(`Stopped ${id}`);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
if (err instanceof CreekdApiError) {
|
|
51
|
+
if (jsonMode)
|
|
52
|
+
jsonOutput({ ok: false, error: err.code, message: err.message }, 1);
|
|
53
|
+
if (err.status === 404) {
|
|
54
|
+
consola.error(`App "${id}" not found.`);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
consola.error(`Stop failed: ${err.message}`);
|
|
58
|
+
}
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
//# sourceMappingURL=stop.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare const topCommand: import("citty").CommandDef<{
|
|
2
|
+
server: {
|
|
3
|
+
type: "string";
|
|
4
|
+
description: string;
|
|
5
|
+
required: false;
|
|
6
|
+
};
|
|
7
|
+
token: {
|
|
8
|
+
type: "string";
|
|
9
|
+
description: string;
|
|
10
|
+
required: false;
|
|
11
|
+
};
|
|
12
|
+
interval: {
|
|
13
|
+
type: "string";
|
|
14
|
+
description: string;
|
|
15
|
+
default: string;
|
|
16
|
+
};
|
|
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
|
+
}>;
|
|
28
|
+
//# sourceMappingURL=top.d.ts.map
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import { globalArgs, resolveJsonMode, jsonOutput, isTTY } from "../utils/output.js";
|
|
4
|
+
import { CreekdClient, CreekdApiError, getCreekdUrl, } from "../utils/creekd-client.js";
|
|
5
|
+
import { fmtBytes, fmtDuration, calcCpuPercent } from "../utils/top-format.js";
|
|
6
|
+
export const topCommand = defineCommand({
|
|
7
|
+
meta: {
|
|
8
|
+
name: "top",
|
|
9
|
+
description: "Live view of apps on a creekd instance",
|
|
10
|
+
},
|
|
11
|
+
args: {
|
|
12
|
+
...globalArgs,
|
|
13
|
+
server: {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "creekd admin API URL (or $CREEKD_URL)",
|
|
16
|
+
required: false,
|
|
17
|
+
},
|
|
18
|
+
token: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Bearer token (or $CREEKD_TOKEN)",
|
|
21
|
+
required: false,
|
|
22
|
+
},
|
|
23
|
+
interval: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "Refresh interval in seconds (default 2)",
|
|
26
|
+
default: "2",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
async run({ args }) {
|
|
30
|
+
const jsonMode = resolveJsonMode(args);
|
|
31
|
+
const client = new CreekdClient(args.server || getCreekdUrl(), args.token || process.env.CREEKD_TOKEN || process.env.CREEKCTL_TOKEN || "");
|
|
32
|
+
const intervalMs = Math.max(500, parseFloat(args.interval || "2") * 1000);
|
|
33
|
+
if (jsonMode) {
|
|
34
|
+
const snapshot = await collectSnapshot(client);
|
|
35
|
+
jsonOutput({ ok: true, ...snapshot }, 0, [
|
|
36
|
+
{ command: "creek top --json", description: "Refresh snapshot" },
|
|
37
|
+
{ command: "creek logs <app-id>", description: "Stream app logs" },
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
await liveTop(client, intervalMs);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
let prevCpu = new Map();
|
|
44
|
+
async function collectSnapshot(client) {
|
|
45
|
+
const apps = await client.listApps();
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
const rows = [];
|
|
48
|
+
const statsResults = await Promise.allSettled(apps.map((app) => client.getStats(app.id)));
|
|
49
|
+
for (let i = 0; i < apps.length; i++) {
|
|
50
|
+
const app = apps[i];
|
|
51
|
+
const stats = statsResults[i].status === "fulfilled"
|
|
52
|
+
? statsResults[i].value
|
|
53
|
+
: null;
|
|
54
|
+
let cpuStr = "—";
|
|
55
|
+
if (stats?.cgroup_enabled && stats.cpu_usage_usec != null) {
|
|
56
|
+
const prev = prevCpu.get(app.id);
|
|
57
|
+
if (prev) {
|
|
58
|
+
const pct = calcCpuPercent(prev.usec, prev.ts, stats.cpu_usage_usec, now);
|
|
59
|
+
if (pct !== null)
|
|
60
|
+
cpuStr = pct.toFixed(1) + "%";
|
|
61
|
+
}
|
|
62
|
+
prevCpu.set(app.id, { usec: stats.cpu_usage_usec, ts: now });
|
|
63
|
+
}
|
|
64
|
+
rows.push({
|
|
65
|
+
id: app.id,
|
|
66
|
+
status: app.status,
|
|
67
|
+
cpu: cpuStr,
|
|
68
|
+
mem: stats?.memory_current_bytes != null ? fmtBytes(stats.memory_current_bytes) : "—",
|
|
69
|
+
memLimit: stats?.memory_max_bytes != null && stats.memory_max_bytes > 0
|
|
70
|
+
? fmtBytes(stats.memory_max_bytes)
|
|
71
|
+
: "—",
|
|
72
|
+
pids: stats?.pids_current != null ? String(stats.pids_current) : "—",
|
|
73
|
+
restarts: app.restart_count,
|
|
74
|
+
uptime: fmtDuration(app.uptime_ms),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const running = apps.filter((a) => a.status === "running").length;
|
|
78
|
+
const crashed = apps.filter((a) => a.status === "crash_loop").length;
|
|
79
|
+
return {
|
|
80
|
+
apps: rows,
|
|
81
|
+
summary: { total: apps.length, running, crashed },
|
|
82
|
+
timestamp: new Date().toISOString(),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async function liveTop(client, intervalMs) {
|
|
86
|
+
const url = client instanceof CreekdClient
|
|
87
|
+
? getCreekdUrl()
|
|
88
|
+
: "creekd";
|
|
89
|
+
let first = true;
|
|
90
|
+
// eslint-disable-next-line no-constant-condition
|
|
91
|
+
while (true) {
|
|
92
|
+
try {
|
|
93
|
+
const snap = await collectSnapshot(client);
|
|
94
|
+
if (isTTY)
|
|
95
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
96
|
+
render(snap, url);
|
|
97
|
+
first = false;
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
if (first) {
|
|
101
|
+
if (err instanceof CreekdApiError && err.status === 401) {
|
|
102
|
+
consola.error("Authentication failed. Set CREEKD_TOKEN or use --token.");
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
consola.error(`Cannot reach creekd at ${url}`);
|
|
106
|
+
consola.info("Is creekd running? Check with: systemctl status creekd");
|
|
107
|
+
}
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
if (isTTY)
|
|
111
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
112
|
+
consola.warn(`Refresh failed: ${err.message}`);
|
|
113
|
+
}
|
|
114
|
+
await sleep(intervalMs);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function render(snap, url) {
|
|
118
|
+
const { apps, summary } = snap;
|
|
119
|
+
const dim = "\x1b[2m";
|
|
120
|
+
const reset = "\x1b[0m";
|
|
121
|
+
const bold = "\x1b[1m";
|
|
122
|
+
const green = "\x1b[32m";
|
|
123
|
+
const red = "\x1b[31m";
|
|
124
|
+
const yellow = "\x1b[33m";
|
|
125
|
+
const header = `${bold}creek top${reset}${dim} — ${url}${reset} ` +
|
|
126
|
+
`${summary.total} apps, ${green}${summary.running} running${reset}` +
|
|
127
|
+
(summary.crashed > 0 ? `, ${red}${summary.crashed} crashed${reset}` : "");
|
|
128
|
+
process.stdout.write(header + "\n\n");
|
|
129
|
+
if (apps.length === 0) {
|
|
130
|
+
process.stdout.write(`${dim} No apps running.${reset}\n`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const cols = ["APP", "STATUS", "CPU", "MEM", "LIMIT", "PIDS", "RESTARTS", "UPTIME"];
|
|
134
|
+
const widths = cols.map((c, i) => {
|
|
135
|
+
const dataMax = Math.max(...apps.map((r) => String(cellValue(r, i)).length), 0);
|
|
136
|
+
return Math.max(c.length, dataMax);
|
|
137
|
+
});
|
|
138
|
+
const headerLine = cols.map((c, i) => c.padEnd(widths[i])).join(" ");
|
|
139
|
+
process.stdout.write(`${dim} ${headerLine}${reset}\n`);
|
|
140
|
+
for (const row of apps) {
|
|
141
|
+
const statusColor = row.status === "running" ? green
|
|
142
|
+
: row.status === "crash_loop" ? red
|
|
143
|
+
: row.status === "starting" ? yellow
|
|
144
|
+
: dim;
|
|
145
|
+
const cells = cols.map((_, i) => {
|
|
146
|
+
const val = String(cellValue(row, i));
|
|
147
|
+
if (i === 1)
|
|
148
|
+
return `${statusColor}${val.padEnd(widths[i])}${reset}`;
|
|
149
|
+
return val.padEnd(widths[i]);
|
|
150
|
+
});
|
|
151
|
+
process.stdout.write(" " + cells.join(" ") + "\n");
|
|
152
|
+
}
|
|
153
|
+
process.stdout.write(`\n${dim} Refreshing every ${snap._intervalS || 2}s — Ctrl+C to quit${reset}\n`);
|
|
154
|
+
}
|
|
155
|
+
function cellValue(row, col) {
|
|
156
|
+
switch (col) {
|
|
157
|
+
case 0: return row.id;
|
|
158
|
+
case 1: return row.status;
|
|
159
|
+
case 2: return row.cpu;
|
|
160
|
+
case 3: return row.mem;
|
|
161
|
+
case 4: return row.memLimit;
|
|
162
|
+
case 5: return row.pids;
|
|
163
|
+
case 6: return row.restarts;
|
|
164
|
+
case 7: return row.uptime;
|
|
165
|
+
default: return "";
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function sleep(ms) {
|
|
169
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=top.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ResolvedConfig } from "@solcreek/sdk";
|
|
2
|
+
export interface CreekdDevServerOptions {
|
|
3
|
+
cwd: string;
|
|
4
|
+
port: number;
|
|
5
|
+
config: ResolvedConfig;
|
|
6
|
+
reset: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare class CreekdDevServer {
|
|
9
|
+
private options;
|
|
10
|
+
private sandboxProcess;
|
|
11
|
+
constructor(options: CreekdDevServerOptions);
|
|
12
|
+
start(): Promise<void>;
|
|
13
|
+
stop(): Promise<void>;
|
|
14
|
+
private requireCreekd;
|
|
15
|
+
private ensureSandbox;
|
|
16
|
+
private buildEnvVars;
|
|
17
|
+
private detectDevCommand;
|
|
18
|
+
/** For compatibility with DevServer interface */
|
|
19
|
+
triggerScheduled(): Promise<void>;
|
|
20
|
+
sendQueueMessage(_payload: unknown): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=creekd-runner.d.ts.map
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// CreekdDevServer for `creek dev --target creekd`.
|
|
2
|
+
//
|
|
3
|
+
// Orchestrates `creekd sandbox` (Go binary) instead of Miniflare.
|
|
4
|
+
// Real Postgres, Redis, SeaweedFS run inside a Lima VM.
|
|
5
|
+
// The app process runs inside the VM with env vars injected.
|
|
6
|
+
//
|
|
7
|
+
// Requirements:
|
|
8
|
+
// - `creekd` binary in PATH
|
|
9
|
+
// - Lima (`limactl`) for macOS/Linux sandbox VM
|
|
10
|
+
import { execSync, spawn } from "node:child_process";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { consola } from "consola";
|
|
14
|
+
export class CreekdDevServer {
|
|
15
|
+
options;
|
|
16
|
+
sandboxProcess = null;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.options = options;
|
|
19
|
+
}
|
|
20
|
+
async start() {
|
|
21
|
+
const { cwd, port, config, reset } = this.options;
|
|
22
|
+
const startTime = Date.now();
|
|
23
|
+
// 1. Check creekd is installed
|
|
24
|
+
this.requireCreekd();
|
|
25
|
+
// 2. Start sandbox (provisions Lima VM + primitives from creek.toml)
|
|
26
|
+
consola.info("Starting creekd sandbox...");
|
|
27
|
+
const status = await this.ensureSandbox(cwd);
|
|
28
|
+
// 3. Build env var map from sandbox primitives
|
|
29
|
+
const env = this.buildEnvVars(config, status, port);
|
|
30
|
+
// 4. Load .env.local if present
|
|
31
|
+
const envLocalPath = join(cwd, ".env.local");
|
|
32
|
+
if (existsSync(envLocalPath)) {
|
|
33
|
+
const { readFileSync } = await import("node:fs");
|
|
34
|
+
const lines = readFileSync(envLocalPath, "utf-8").split("\n");
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
const trimmed = line.trim();
|
|
37
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
38
|
+
continue;
|
|
39
|
+
const eqIdx = trimmed.indexOf("=");
|
|
40
|
+
if (eqIdx > 0) {
|
|
41
|
+
const key = trimmed.slice(0, eqIdx);
|
|
42
|
+
const val = trimmed.slice(eqIdx + 1).replace(/^["']|["']$/g, "");
|
|
43
|
+
if (!env[key])
|
|
44
|
+
env[key] = val;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
consola.info(`.env.local loaded`);
|
|
48
|
+
}
|
|
49
|
+
// 5. Detect runtime and dev command
|
|
50
|
+
const devCmd = this.detectDevCommand(cwd, config);
|
|
51
|
+
// 6. Print status
|
|
52
|
+
const elapsed = Date.now() - startTime;
|
|
53
|
+
console.log("");
|
|
54
|
+
consola.success("⬡ creek dev (creekd sandbox)\n");
|
|
55
|
+
consola.info(`App: http://localhost:${port}`);
|
|
56
|
+
for (const p of status.ports) {
|
|
57
|
+
if (p.name !== "app") {
|
|
58
|
+
consola.info(`${p.name.padEnd(12)}localhost:${p.host}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
console.log("");
|
|
62
|
+
for (const [k, v] of Object.entries(env)) {
|
|
63
|
+
if (k.startsWith("DATABASE") || k.startsWith("REDIS") || k.startsWith("S3_") || k.startsWith("SMTP")) {
|
|
64
|
+
const masked = v.replace(/:[^:@]+@/, ":***@");
|
|
65
|
+
consola.info(` ${k}=${masked}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
console.log("");
|
|
69
|
+
consola.info(`Running: ${devCmd.join(" ")}`);
|
|
70
|
+
consola.info(`Ready in ${elapsed}ms`);
|
|
71
|
+
console.log("");
|
|
72
|
+
// 7. Start the dev server process with env vars
|
|
73
|
+
this.sandboxProcess = spawn(devCmd[0], devCmd.slice(1), {
|
|
74
|
+
cwd,
|
|
75
|
+
env: { ...process.env, ...env },
|
|
76
|
+
stdio: "inherit",
|
|
77
|
+
});
|
|
78
|
+
this.sandboxProcess.on("exit", (code) => {
|
|
79
|
+
if (code !== null && code !== 0) {
|
|
80
|
+
consola.error(`Dev server exited with code ${code}`);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async stop() {
|
|
85
|
+
if (this.sandboxProcess) {
|
|
86
|
+
this.sandboxProcess.kill("SIGTERM");
|
|
87
|
+
this.sandboxProcess = null;
|
|
88
|
+
}
|
|
89
|
+
// Don't stop the sandbox VM — it persists for fast restarts
|
|
90
|
+
}
|
|
91
|
+
// --- Internals ---
|
|
92
|
+
requireCreekd() {
|
|
93
|
+
try {
|
|
94
|
+
execSync("creekd --version", { stdio: "pipe" });
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
throw new Error([
|
|
98
|
+
"creekd is not installed.",
|
|
99
|
+
"",
|
|
100
|
+
" Install with: curl -fsSL https://install.creek.dev | sh",
|
|
101
|
+
"",
|
|
102
|
+
" Or use --target cf to develop with Miniflare (CF Workers local).",
|
|
103
|
+
].join("\n"));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async ensureSandbox(cwd) {
|
|
107
|
+
try {
|
|
108
|
+
const output = execSync(`creekd sandbox --non-interactive --json "${cwd}"`, { encoding: "utf-8", timeout: 300_000 });
|
|
109
|
+
// Find the JSON line in output (creekd may print logs before JSON)
|
|
110
|
+
const lines = output.trim().split("\n");
|
|
111
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(lines[i]);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
throw new Error("No JSON status from creekd sandbox");
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
if (e.message?.includes("not installed"))
|
|
123
|
+
throw e;
|
|
124
|
+
throw new Error(`creekd sandbox failed: ${e.message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
buildEnvVars(config, status, port) {
|
|
128
|
+
const env = {
|
|
129
|
+
PORT: String(port),
|
|
130
|
+
};
|
|
131
|
+
// Map sandbox ports to standard env vars
|
|
132
|
+
for (const p of status.ports) {
|
|
133
|
+
switch (p.name) {
|
|
134
|
+
case "postgres":
|
|
135
|
+
env.DATABASE_URL = `postgresql://creek:creek_sandbox@127.0.0.1:${p.host}/app`;
|
|
136
|
+
break;
|
|
137
|
+
case "mysql":
|
|
138
|
+
env.DATABASE_URL = `mysql://creek:creek_sandbox@127.0.0.1:${p.host}/app`;
|
|
139
|
+
break;
|
|
140
|
+
case "redis":
|
|
141
|
+
env.REDIS_URL = `redis://127.0.0.1:${p.host}/0`;
|
|
142
|
+
break;
|
|
143
|
+
case "s3":
|
|
144
|
+
env.S3_ENDPOINT = `http://127.0.0.1:${p.host}`;
|
|
145
|
+
env.S3_BUCKET = config.projectName;
|
|
146
|
+
env.AWS_ACCESS_KEY_ID = "creek";
|
|
147
|
+
env.AWS_SECRET_ACCESS_KEY = "creek_sandbox";
|
|
148
|
+
break;
|
|
149
|
+
case "smtp":
|
|
150
|
+
env.SMTP_URL = `smtp://127.0.0.1:${p.host}`;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// SQLite fallback for database if no postgres/mysql port
|
|
155
|
+
if (!env.DATABASE_URL) {
|
|
156
|
+
const dbDir = join(this.options.cwd, ".creek", "dev");
|
|
157
|
+
env.DATABASE_URL = `sqlite://${dbDir}/dev.db`;
|
|
158
|
+
}
|
|
159
|
+
return env;
|
|
160
|
+
}
|
|
161
|
+
detectDevCommand(cwd, config) {
|
|
162
|
+
// Check package.json for dev script
|
|
163
|
+
const pkgPath = join(cwd, "package.json");
|
|
164
|
+
if (existsSync(pkgPath)) {
|
|
165
|
+
const { readFileSync } = require("node:fs");
|
|
166
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
167
|
+
if (pkg.scripts?.dev) {
|
|
168
|
+
return ["npm", "run", "dev"];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Fallback to bun --watch or node --watch
|
|
172
|
+
const entryFiles = ["src/index.ts", "src/index.mjs", "src/index.js", "index.ts", "index.mjs", "index.js"];
|
|
173
|
+
for (const entry of entryFiles) {
|
|
174
|
+
if (existsSync(join(cwd, entry))) {
|
|
175
|
+
return ["bun", "--watch", entry];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return ["bun", "--watch", "."];
|
|
179
|
+
}
|
|
180
|
+
/** For compatibility with DevServer interface */
|
|
181
|
+
async triggerScheduled() {
|
|
182
|
+
consola.warn("Scheduled triggers not yet supported in creekd dev mode");
|
|
183
|
+
}
|
|
184
|
+
async sendQueueMessage(_payload) {
|
|
185
|
+
consola.warn("Queue messages not yet supported in creekd dev mode");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=creekd-runner.js.map
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,10 @@ import { doctorCommand } from "./commands/doctor.js";
|
|
|
24
24
|
import { dbCommand } from "./commands/db.js";
|
|
25
25
|
import { storageCommand } from "./commands/storage.js";
|
|
26
26
|
import { cacheCommand } from "./commands/cache.js";
|
|
27
|
+
import { topCommand } from "./commands/top.js";
|
|
28
|
+
import { restartCommand } from "./commands/restart.js";
|
|
29
|
+
import { stopCommand } from "./commands/stop.js";
|
|
30
|
+
import { dashboardCommand } from "./commands/dashboard.js";
|
|
27
31
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
32
|
const cliPkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
29
33
|
// Read version from the "creek" facade package (what users install),
|
|
@@ -63,6 +67,10 @@ const main = defineCommand({
|
|
|
63
67
|
cache: cacheCommand,
|
|
64
68
|
domains: domainsCommand,
|
|
65
69
|
rollback: rollbackCommand,
|
|
70
|
+
top: topCommand,
|
|
71
|
+
dashboard: dashboardCommand,
|
|
72
|
+
restart: restartCommand,
|
|
73
|
+
stop: stopCommand,
|
|
66
74
|
ops: opsCommand,
|
|
67
75
|
},
|
|
68
76
|
});
|
|
@@ -13,10 +13,10 @@ export interface AuthCallbackResult {
|
|
|
13
13
|
* 4. This server receives the callback, validates state, resolves the promise
|
|
14
14
|
* 5. Server auto-closes
|
|
15
15
|
*/
|
|
16
|
-
export declare function startAuthServer(): {
|
|
16
|
+
export declare function startAuthServer(): Promise<{
|
|
17
17
|
port: number;
|
|
18
18
|
state: string;
|
|
19
19
|
waitForCallback: () => Promise<string>;
|
|
20
20
|
close: () => void;
|
|
21
|
-
}
|
|
21
|
+
}>;
|
|
22
22
|
//# sourceMappingURL=auth-server.d.ts.map
|
|
@@ -11,7 +11,7 @@ import { randomBytes } from "node:crypto";
|
|
|
11
11
|
* 4. This server receives the callback, validates state, resolves the promise
|
|
12
12
|
* 5. Server auto-closes
|
|
13
13
|
*/
|
|
14
|
-
export function startAuthServer() {
|
|
14
|
+
export async function startAuthServer() {
|
|
15
15
|
const state = randomBytes(16).toString("hex");
|
|
16
16
|
let resolveCallback;
|
|
17
17
|
let rejectCallback;
|
|
@@ -45,10 +45,23 @@ export function startAuthServer() {
|
|
|
45
45
|
res.writeHead(404);
|
|
46
46
|
res.end("Not found");
|
|
47
47
|
});
|
|
48
|
-
// Listen on port 0 = OS picks a random available port
|
|
49
|
-
server.listen(
|
|
48
|
+
// Listen on port 0 = OS picks a random available port.
|
|
49
|
+
// server.listen() is async — we MUST await the 'listening' event before
|
|
50
|
+
// reading server.address(), otherwise address() returns null and the
|
|
51
|
+
// callback URL ships ?port=0 to the dashboard (which correctly rejects
|
|
52
|
+
// with "Missing port or state parameter"). Regression seen in the wild
|
|
53
|
+
// on macOS — Node's behavior here is timing-dependent.
|
|
54
|
+
await new Promise((resolve, reject) => {
|
|
55
|
+
server.once("listening", () => resolve());
|
|
56
|
+
server.once("error", (err) => reject(err));
|
|
57
|
+
server.listen(0, "localhost");
|
|
58
|
+
});
|
|
50
59
|
const address = server.address();
|
|
51
60
|
const port = typeof address === "object" && address ? address.port : 0;
|
|
61
|
+
if (port === 0) {
|
|
62
|
+
server.close();
|
|
63
|
+
throw new Error("Could not determine local port for auth callback — server.address() returned null after listening.");
|
|
64
|
+
}
|
|
52
65
|
// Timeout after 2 minutes
|
|
53
66
|
const timeout = setTimeout(() => {
|
|
54
67
|
rejectCallback(new Error("Login timed out after 2 minutes"));
|