creek 0.2.2 → 0.3.0-alpha.10
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/claim.d.ts +8 -0
- package/dist/commands/claim.js +84 -0
- package/dist/commands/deploy.d.ts +23 -0
- package/dist/commands/deploy.js +465 -0
- package/dist/commands/env.d.ts +2 -0
- package/dist/commands/env.js +87 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.js +60 -0
- package/dist/commands/login.d.ts +13 -0
- package/dist/commands/login.js +111 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +27 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +25 -0
- package/dist/utils/auth-server.d.ts +22 -0
- package/dist/utils/auth-server.js +91 -0
- package/dist/utils/bundle.d.ts +6 -0
- package/dist/utils/bundle.js +24 -0
- package/dist/utils/config.d.ts +12 -0
- package/dist/utils/config.js +37 -0
- package/dist/utils/sandbox.d.ts +45 -0
- package/dist/utils/sandbox.js +57 -0
- package/dist/utils/ssr-bundle.d.ts +6 -0
- package/dist/utils/ssr-bundle.js +48 -0
- package/package.json +38 -16
- package/README.md +0 -105
- package/bin/creek +0 -10
- package/examples/hello.creek +0 -6
- package/examples/twitter.creek +0 -23
- package/examples/zeromq.creek +0 -16
- package/lib/aggregator.coffee +0 -27
- package/lib/aggregators/count.coffee +0 -15
- package/lib/aggregators/distinct.coffee +0 -20
- package/lib/aggregators/max.coffee +0 -16
- package/lib/aggregators/mean.coffee +0 -27
- package/lib/aggregators/min.coffee +0 -16
- package/lib/aggregators/popular.coffee +0 -27
- package/lib/aggregators/recent.coffee +0 -10
- package/lib/aggregators/sum.coffee +0 -17
- package/lib/bootstrap.js +0 -5
- package/lib/compound-aggregator.coffee +0 -33
- package/lib/creek.coffee +0 -13
- package/lib/interfaces/rest.coffee +0 -30
- package/lib/interfaces/websocket.coffee +0 -15
- package/lib/lazy-bucketed-aggregator.coffee +0 -35
- package/lib/parsers/chunked.coffee +0 -17
- package/lib/parsers/json.coffee +0 -9
- package/lib/parsers/words.coffee +0 -6
- package/lib/parsers/zeromq.coffee +0 -16
- package/lib/runner.coffee +0 -39
- package/lib/timeboxed-aggregator.coffee +0 -64
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import { CreekClient } from "@solcreek/sdk";
|
|
4
|
+
import { getToken, getApiUrl, getSandboxApiUrl } from "../utils/config.js";
|
|
5
|
+
export const claimCommand = defineCommand({
|
|
6
|
+
meta: {
|
|
7
|
+
name: "claim",
|
|
8
|
+
description: "Claim a sandbox deployment as a permanent project",
|
|
9
|
+
},
|
|
10
|
+
args: {
|
|
11
|
+
sandboxId: {
|
|
12
|
+
type: "positional",
|
|
13
|
+
description: "Sandbox ID to claim (shown after sandbox deploy)",
|
|
14
|
+
required: true,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
async run({ args }) {
|
|
18
|
+
const token = getToken();
|
|
19
|
+
if (!token) {
|
|
20
|
+
consola.error("You need to be logged in to claim a sandbox.");
|
|
21
|
+
consola.info("Run `creek login` first, then `creek claim` again.");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const sandboxId = args.sandboxId;
|
|
25
|
+
// 1. Fetch sandbox info
|
|
26
|
+
consola.start("Looking up sandbox...");
|
|
27
|
+
const sandboxApiUrl = getSandboxApiUrl();
|
|
28
|
+
const statusRes = await fetch(`${sandboxApiUrl}/api/sandbox/${sandboxId}/status`);
|
|
29
|
+
if (!statusRes.ok) {
|
|
30
|
+
consola.error("Sandbox not found. It may have expired.");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const sandbox = (await statusRes.json());
|
|
34
|
+
if (!sandbox.claimable) {
|
|
35
|
+
if (sandbox.status === "expired") {
|
|
36
|
+
consola.error("This sandbox has expired and can no longer be claimed.");
|
|
37
|
+
consola.info("Run `creek deploy` to deploy your project permanently.");
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
consola.error(`Sandbox is in '${sandbox.status}' state and cannot be claimed.`);
|
|
41
|
+
}
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
// 2. Create permanent project
|
|
45
|
+
consola.start("Creating permanent project...");
|
|
46
|
+
const client = new CreekClient(getApiUrl(), token);
|
|
47
|
+
const slug = sandbox.templateId ?? sandboxId;
|
|
48
|
+
let project;
|
|
49
|
+
try {
|
|
50
|
+
const res = await client.createProject({
|
|
51
|
+
slug,
|
|
52
|
+
framework: sandbox.framework,
|
|
53
|
+
});
|
|
54
|
+
project = res.project;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Slug might conflict, try with sandbox ID suffix
|
|
58
|
+
const res = await client.createProject({
|
|
59
|
+
slug: `${slug}-${sandboxId}`,
|
|
60
|
+
framework: sandbox.framework,
|
|
61
|
+
});
|
|
62
|
+
project = res.project;
|
|
63
|
+
}
|
|
64
|
+
consola.success(`Created project: ${project.slug}`);
|
|
65
|
+
// 3. Mark sandbox as claimed
|
|
66
|
+
try {
|
|
67
|
+
await fetch(`${sandboxApiUrl}/api/sandbox/${sandboxId}/claim`, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: { "Content-Type": "application/json" },
|
|
70
|
+
body: JSON.stringify({ projectId: project.id }),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Best effort — claim status update is non-critical
|
|
75
|
+
}
|
|
76
|
+
consola.success("Sandbox claimed!");
|
|
77
|
+
consola.info("");
|
|
78
|
+
consola.info("Next steps:");
|
|
79
|
+
consola.info(` cd your-project`);
|
|
80
|
+
consola.info(` creek init`);
|
|
81
|
+
consola.info(` creek deploy # deploy permanently`);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
//# sourceMappingURL=claim.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const deployCommand: import("citty").CommandDef<{
|
|
2
|
+
dir: {
|
|
3
|
+
type: "positional";
|
|
4
|
+
description: string;
|
|
5
|
+
required: false;
|
|
6
|
+
};
|
|
7
|
+
"skip-build": {
|
|
8
|
+
type: "boolean";
|
|
9
|
+
description: string;
|
|
10
|
+
default: false;
|
|
11
|
+
};
|
|
12
|
+
demo: {
|
|
13
|
+
type: "boolean";
|
|
14
|
+
description: string;
|
|
15
|
+
default: false;
|
|
16
|
+
};
|
|
17
|
+
template: {
|
|
18
|
+
type: "string";
|
|
19
|
+
description: string;
|
|
20
|
+
required: false;
|
|
21
|
+
};
|
|
22
|
+
}>;
|
|
23
|
+
//# sourceMappingURL=deploy.d.ts.map
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { execSync, execFileSync } from "node:child_process";
|
|
6
|
+
import { parseConfig, CreekClient, CreekAuthError, isSSRFramework, getSSRServerEntry, getClientAssetsDir, getDefaultBuildOutput, detectFramework, } from "@solcreek/sdk";
|
|
7
|
+
import { getToken, getApiUrl } from "../utils/config.js";
|
|
8
|
+
import { collectAssets } from "../utils/bundle.js";
|
|
9
|
+
import { bundleSSRServer } from "../utils/ssr-bundle.js";
|
|
10
|
+
import { sandboxDeploy, pollSandboxStatus, printSandboxSuccess } from "../utils/sandbox.js";
|
|
11
|
+
export const deployCommand = defineCommand({
|
|
12
|
+
meta: {
|
|
13
|
+
name: "deploy",
|
|
14
|
+
description: "Deploy the current project to Creek",
|
|
15
|
+
},
|
|
16
|
+
args: {
|
|
17
|
+
dir: {
|
|
18
|
+
type: "positional",
|
|
19
|
+
description: "Directory to deploy (default: current directory)",
|
|
20
|
+
required: false,
|
|
21
|
+
},
|
|
22
|
+
"skip-build": {
|
|
23
|
+
type: "boolean",
|
|
24
|
+
description: "Skip the build step",
|
|
25
|
+
default: false,
|
|
26
|
+
},
|
|
27
|
+
demo: {
|
|
28
|
+
type: "boolean",
|
|
29
|
+
description: "Deploy a sample site to see Creek in action",
|
|
30
|
+
default: false,
|
|
31
|
+
},
|
|
32
|
+
template: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "Deploy a template (e.g., react-dashboard, astro-landing)",
|
|
35
|
+
required: false,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
async run({ args }) {
|
|
39
|
+
// --- Demo deploy (zero-friction showcase) ---
|
|
40
|
+
if (args.demo) {
|
|
41
|
+
return await deployDemo();
|
|
42
|
+
}
|
|
43
|
+
// --- Template deploy ---
|
|
44
|
+
if (args.template) {
|
|
45
|
+
return await deployTemplate(args.template);
|
|
46
|
+
}
|
|
47
|
+
// --- Resolve target directory ---
|
|
48
|
+
const cwd = args.dir ? resolve(args.dir) : process.cwd();
|
|
49
|
+
const configPath = join(cwd, "creek.toml");
|
|
50
|
+
const hasConfig = existsSync(configPath);
|
|
51
|
+
const token = getToken();
|
|
52
|
+
// --- Explicit directory (creek deploy ./dist) ---
|
|
53
|
+
if (args.dir) {
|
|
54
|
+
if (!existsSync(cwd)) {
|
|
55
|
+
consola.error(`Directory not found: ${args.dir}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
return await deployDirectory(cwd);
|
|
59
|
+
}
|
|
60
|
+
// --- Authenticated deploy (creek.toml present) ---
|
|
61
|
+
if (hasConfig) {
|
|
62
|
+
if (!token) {
|
|
63
|
+
consola.error("Not authenticated. Run `creek login` first.");
|
|
64
|
+
consola.info("Or deploy without an account: remove creek.toml and run `creek deploy` again.");
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
return await deployAuthenticated(cwd, configPath, token, args["skip-build"]);
|
|
68
|
+
}
|
|
69
|
+
// --- Sandbox deploy (has package.json → auto-detect + build) ---
|
|
70
|
+
if (existsSync(join(cwd, "package.json"))) {
|
|
71
|
+
return await deploySandbox(cwd, args["skip-build"]);
|
|
72
|
+
}
|
|
73
|
+
// --- Auto-detect build output dirs ---
|
|
74
|
+
for (const dir of ["dist", "build", "out", ".output/public"]) {
|
|
75
|
+
const fullPath = resolve(cwd, dir);
|
|
76
|
+
if (existsSync(fullPath)) {
|
|
77
|
+
consola.info(`Found build output: ${dir}/`);
|
|
78
|
+
return await deployDirectory(fullPath);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// --- Nothing found → guide the user ---
|
|
82
|
+
consola.info("No project found in this directory.\n");
|
|
83
|
+
consola.info(" creek deploy ./dist Deploy a build output directory");
|
|
84
|
+
consola.info(" creek deploy --demo Deploy a sample site (see Creek in 5 seconds)");
|
|
85
|
+
consola.info("");
|
|
86
|
+
consola.info("Or create a project first:");
|
|
87
|
+
consola.info(" npm create vite@latest my-app && cd my-app && npx creek deploy");
|
|
88
|
+
process.exit(1);
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// Demo deploy — pre-built page, zero dependencies, instant
|
|
93
|
+
// ============================================================================
|
|
94
|
+
const DEMO_HTML = `<!DOCTYPE html>
|
|
95
|
+
<html lang="en">
|
|
96
|
+
<head>
|
|
97
|
+
<meta charset="utf-8">
|
|
98
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
99
|
+
<title>Creek Deploy Demo</title>
|
|
100
|
+
<style>
|
|
101
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
102
|
+
body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a0a;color:#f5f5f5;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center}
|
|
103
|
+
.card{max-width:480px;text-align:center;padding:3rem 2rem}
|
|
104
|
+
h1{font-size:2rem;font-weight:700;letter-spacing:-0.03em;margin-bottom:0.5rem}
|
|
105
|
+
.accent{background:linear-gradient(135deg,#38bdf8,#818cf8);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
|
|
106
|
+
p{color:#888;line-height:1.6;margin-top:1rem}
|
|
107
|
+
.meta{margin-top:2rem;font-size:0.85rem;color:#555}
|
|
108
|
+
.cta{display:inline-block;margin-top:1.5rem;background:linear-gradient(135deg,#2563eb,#3b82f6);color:#fff;padding:10px 24px;border-radius:8px;text-decoration:none;font-weight:600;font-size:0.9rem;transition:opacity 0.15s}
|
|
109
|
+
.cta:hover{opacity:0.9}
|
|
110
|
+
code{background:#1a1a2e;padding:2px 8px;border-radius:4px;font-size:0.85rem;color:#a5b4fc}
|
|
111
|
+
.pulse{width:12px;height:12px;background:#22c55e;border-radius:50%;display:inline-block;margin-right:8px;animation:pulse 2s infinite}
|
|
112
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}
|
|
113
|
+
</style>
|
|
114
|
+
</head>
|
|
115
|
+
<body>
|
|
116
|
+
<div class="card">
|
|
117
|
+
<h1><span class="accent">Creek</span> is live.</h1>
|
|
118
|
+
<p>This site was deployed to the edge in seconds — no build step, no config, no account.</p>
|
|
119
|
+
<p style="margin-top:1.5rem"><span class="pulse"></span>Running on Cloudflare's global network</p>
|
|
120
|
+
<p class="meta">Now try it with your own project:</p>
|
|
121
|
+
<p style="margin-top:0.5rem"><code>cd your-project && npx creek deploy</code></p>
|
|
122
|
+
<a href="https://creek.dev" class="cta">Learn more about Creek</a>
|
|
123
|
+
</div>
|
|
124
|
+
<script>document.title="Creek Deploy Demo — "+new Date().toLocaleTimeString()</script>
|
|
125
|
+
</body>
|
|
126
|
+
</html>`;
|
|
127
|
+
async function deployDemo() {
|
|
128
|
+
consola.start("Deploying demo site...");
|
|
129
|
+
try {
|
|
130
|
+
const result = await sandboxDeploy({
|
|
131
|
+
assets: { "index.html": Buffer.from(DEMO_HTML).toString("base64") },
|
|
132
|
+
source: "cli-demo",
|
|
133
|
+
});
|
|
134
|
+
consola.start("Waiting for deployment...");
|
|
135
|
+
const status = await pollSandboxStatus(result.statusUrl);
|
|
136
|
+
const duration = status.deployDurationMs
|
|
137
|
+
? `in ${(status.deployDurationMs / 1000).toFixed(1)}s`
|
|
138
|
+
: "in seconds";
|
|
139
|
+
consola.success(`Live ${duration} → ${status.previewUrl}`);
|
|
140
|
+
consola.info("");
|
|
141
|
+
consola.info("That's Creek. Now deploy your own project:");
|
|
142
|
+
consola.info(" cd your-project && npx creek deploy");
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
consola.error(err instanceof Error ? err.message : "Demo deploy failed");
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// ============================================================================
|
|
150
|
+
// Directory deploy — deploy pre-built static files directly
|
|
151
|
+
// ============================================================================
|
|
152
|
+
async function deployDirectory(dir) {
|
|
153
|
+
consola.info("Deploying to sandbox (60 min preview).");
|
|
154
|
+
consola.start("Collecting assets...");
|
|
155
|
+
const { assets, fileList } = collectAssets(dir);
|
|
156
|
+
if (fileList.length === 0) {
|
|
157
|
+
consola.error("No files found in directory.");
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
consola.info(`Found ${fileList.length} files`);
|
|
161
|
+
consola.start("Deploying...");
|
|
162
|
+
try {
|
|
163
|
+
const result = await sandboxDeploy({ assets, source: "cli" });
|
|
164
|
+
consola.start("Waiting for deployment...");
|
|
165
|
+
const status = await pollSandboxStatus(result.statusUrl);
|
|
166
|
+
printSandboxSuccess(status.previewUrl, result.expiresAt, result.sandboxId);
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
consola.error(err instanceof Error ? err.message : "Deploy failed");
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// ============================================================================
|
|
174
|
+
// Sandbox deploy — auto-detect framework, build, deploy
|
|
175
|
+
// ============================================================================
|
|
176
|
+
async function deploySandbox(cwd, skipBuild) {
|
|
177
|
+
consola.info("Deploying to sandbox (60 min preview).");
|
|
178
|
+
consola.info("");
|
|
179
|
+
const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"));
|
|
180
|
+
const framework = detectFramework(pkg);
|
|
181
|
+
if (framework) {
|
|
182
|
+
consola.info(`Detected: ${framework}`);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
consola.info("Framework: auto (static site)");
|
|
186
|
+
}
|
|
187
|
+
// Build
|
|
188
|
+
const buildCommand = pkg.scripts?.build ? "npm run build" : null;
|
|
189
|
+
const outputDir = resolve(cwd, getDefaultBuildOutput(framework));
|
|
190
|
+
if (!skipBuild) {
|
|
191
|
+
if (!buildCommand) {
|
|
192
|
+
consola.error("No build script found in package.json.");
|
|
193
|
+
consola.info("Add a 'build' script or use --skip-build if already built.");
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
consola.start(`Building with: ${buildCommand}`);
|
|
197
|
+
try {
|
|
198
|
+
execSync(buildCommand, { cwd, stdio: "inherit" });
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
consola.error("Build failed");
|
|
202
|
+
consola.info("");
|
|
203
|
+
consola.info("Common fixes:");
|
|
204
|
+
consola.info(" • npm install (missing dependencies?)");
|
|
205
|
+
consola.info(" • Check for TypeScript errors");
|
|
206
|
+
consola.info(` • Verify build works: ${buildCommand}`);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
consola.success("Build complete");
|
|
210
|
+
}
|
|
211
|
+
if (!existsSync(outputDir)) {
|
|
212
|
+
consola.error(`Build output not found: ${outputDir}`);
|
|
213
|
+
if (framework) {
|
|
214
|
+
consola.info(`Expected output for ${framework}: ${getDefaultBuildOutput(framework)}`);
|
|
215
|
+
}
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
// Collect assets
|
|
219
|
+
const isSSR = isSSRFramework(framework);
|
|
220
|
+
const renderMode = isSSR ? "ssr" : "spa";
|
|
221
|
+
let clientAssetsDir = outputDir;
|
|
222
|
+
if (isSSR && framework) {
|
|
223
|
+
const subdir = getClientAssetsDir(framework);
|
|
224
|
+
if (subdir)
|
|
225
|
+
clientAssetsDir = resolve(outputDir, subdir);
|
|
226
|
+
}
|
|
227
|
+
consola.start("Collecting assets...");
|
|
228
|
+
const { assets: clientAssets, fileList } = collectAssets(clientAssetsDir);
|
|
229
|
+
consola.info(`Found ${fileList.length} assets`);
|
|
230
|
+
let serverFiles;
|
|
231
|
+
if (isSSR && framework) {
|
|
232
|
+
const serverEntry = getSSRServerEntry(framework);
|
|
233
|
+
if (serverEntry) {
|
|
234
|
+
const serverEntryPath = resolve(outputDir, serverEntry);
|
|
235
|
+
if (existsSync(serverEntryPath)) {
|
|
236
|
+
consola.start("Bundling SSR server...");
|
|
237
|
+
const bundled = await bundleSSRServer(serverEntryPath);
|
|
238
|
+
serverFiles = { "server.js": Buffer.from(bundled).toString("base64") };
|
|
239
|
+
consola.success(`SSR bundled (${Math.round(bundled.length / 1024)}KB)`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Deploy to sandbox
|
|
244
|
+
consola.start("Deploying...");
|
|
245
|
+
try {
|
|
246
|
+
const result = await sandboxDeploy({
|
|
247
|
+
assets: clientAssets,
|
|
248
|
+
serverFiles,
|
|
249
|
+
framework: framework ?? undefined,
|
|
250
|
+
source: "cli",
|
|
251
|
+
});
|
|
252
|
+
consola.start("Waiting for deployment...");
|
|
253
|
+
const status = await pollSandboxStatus(result.statusUrl);
|
|
254
|
+
printSandboxSuccess(status.previewUrl, result.expiresAt, result.sandboxId);
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
consola.error(err instanceof Error ? err.message : "Sandbox deploy failed");
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// ============================================================================
|
|
262
|
+
// Template deploy — clone + build + deploy to sandbox
|
|
263
|
+
// ============================================================================
|
|
264
|
+
async function deployTemplate(templateId) {
|
|
265
|
+
// Validate template ID — alphanumeric, hyphens, underscores only (no path traversal)
|
|
266
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(templateId)) {
|
|
267
|
+
consola.error("Invalid template name. Use only letters, numbers, hyphens, and underscores.");
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
consola.info(`Deploying template: ${templateId}`);
|
|
271
|
+
// Clone template to temp dir
|
|
272
|
+
const tmpDir = join(process.env.TMPDIR ?? "/tmp", `creek-template-${Date.now()}`);
|
|
273
|
+
const repoUrl = "https://github.com/solcreek/templates";
|
|
274
|
+
consola.start("Cloning template...");
|
|
275
|
+
try {
|
|
276
|
+
execFileSync("git", [
|
|
277
|
+
"clone", "--depth", "1", "--filter=blob:none", "--sparse", repoUrl, tmpDir,
|
|
278
|
+
], { stdio: "pipe" });
|
|
279
|
+
execFileSync("git", [
|
|
280
|
+
"sparse-checkout", "set", templateId,
|
|
281
|
+
], { cwd: tmpDir, stdio: "pipe" });
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
consola.error(`Template '${templateId}' not found.`);
|
|
285
|
+
consola.info("Available templates: creek deploy --template");
|
|
286
|
+
cleanupDir(tmpDir);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
const templateDir = join(tmpDir, templateId);
|
|
290
|
+
// Verify resolved path is still within tmpDir (prevent path traversal)
|
|
291
|
+
if (!resolve(templateDir).startsWith(resolve(tmpDir))) {
|
|
292
|
+
consola.error("Invalid template path.");
|
|
293
|
+
cleanupDir(tmpDir);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
if (!existsSync(templateDir)) {
|
|
297
|
+
consola.error(`Template directory not found: ${templateId}`);
|
|
298
|
+
cleanupDir(tmpDir);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
consola.start("Installing dependencies...");
|
|
302
|
+
try {
|
|
303
|
+
execFileSync("npm", ["install"], { cwd: templateDir, stdio: "pipe" });
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
consola.error("Failed to install dependencies");
|
|
307
|
+
cleanupDir(tmpDir);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
// Deploy as sandbox (reuse sandbox flow)
|
|
311
|
+
await deploySandbox(templateDir, false);
|
|
312
|
+
// Cleanup
|
|
313
|
+
cleanupDir(tmpDir);
|
|
314
|
+
}
|
|
315
|
+
function cleanupDir(dir) {
|
|
316
|
+
try {
|
|
317
|
+
rmSync(dir, { recursive: true, force: true });
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// ignore
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// ============================================================================
|
|
324
|
+
// Authenticated deploy — existing flow
|
|
325
|
+
// ============================================================================
|
|
326
|
+
async function deployAuthenticated(cwd, configPath, token, skipBuild) {
|
|
327
|
+
try {
|
|
328
|
+
const config = parseConfig(readFileSync(configPath, "utf-8"));
|
|
329
|
+
consola.info(`Project: ${config.project.name}`);
|
|
330
|
+
const client = new CreekClient(getApiUrl(), token);
|
|
331
|
+
// Ensure project exists
|
|
332
|
+
let project;
|
|
333
|
+
try {
|
|
334
|
+
project = await client.getProject(config.project.name);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
consola.info("Project not found, creating...");
|
|
338
|
+
const res = await client.createProject({
|
|
339
|
+
slug: config.project.name,
|
|
340
|
+
framework: config.project.framework,
|
|
341
|
+
});
|
|
342
|
+
project = res.project;
|
|
343
|
+
consola.success(`Created project: ${project.slug}`);
|
|
344
|
+
}
|
|
345
|
+
// Build — creek.toml build.command is user-defined, analogous to npm scripts
|
|
346
|
+
if (!skipBuild) {
|
|
347
|
+
const buildCmd = config.build.command;
|
|
348
|
+
if (!buildCmd || typeof buildCmd !== "string" || buildCmd.length > 500) {
|
|
349
|
+
consola.error("Invalid build command in creek.toml");
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
consola.start(`Building with: ${buildCmd}`);
|
|
353
|
+
try {
|
|
354
|
+
execSync(buildCmd, { cwd, stdio: "inherit" });
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
consola.error("Build failed");
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
consola.success("Build complete");
|
|
361
|
+
}
|
|
362
|
+
const outputDir = resolve(cwd, config.build.output);
|
|
363
|
+
if (!existsSync(outputDir)) {
|
|
364
|
+
consola.error(`Build output directory not found: ${config.build.output}`);
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
const framework = config.project.framework ?? null;
|
|
368
|
+
const isSSR = isSSRFramework(framework);
|
|
369
|
+
const renderMode = isSSR ? "ssr" : "spa";
|
|
370
|
+
let clientAssetsDir = outputDir;
|
|
371
|
+
if (isSSR && framework) {
|
|
372
|
+
const clientSubdir = getClientAssetsDir(framework);
|
|
373
|
+
if (clientSubdir) {
|
|
374
|
+
clientAssetsDir = resolve(outputDir, clientSubdir);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
consola.start("Collecting assets...");
|
|
378
|
+
const { assets: clientAssets, fileList } = collectAssets(clientAssetsDir);
|
|
379
|
+
consola.info(`Found ${fileList.length} client assets`);
|
|
380
|
+
let serverFiles;
|
|
381
|
+
if (isSSR && framework) {
|
|
382
|
+
const serverEntry = getSSRServerEntry(framework);
|
|
383
|
+
if (serverEntry) {
|
|
384
|
+
const serverEntryPath = resolve(outputDir, serverEntry);
|
|
385
|
+
if (existsSync(serverEntryPath)) {
|
|
386
|
+
consola.start("Bundling SSR server...");
|
|
387
|
+
const bundled = await bundleSSRServer(serverEntryPath);
|
|
388
|
+
serverFiles = {
|
|
389
|
+
"server.js": Buffer.from(bundled).toString("base64"),
|
|
390
|
+
};
|
|
391
|
+
consola.success(`SSR server bundled (${Math.round(bundled.length / 1024)}KB)`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
consola.start("Creating deployment...");
|
|
396
|
+
const { deployment } = await client.createDeployment(project.id);
|
|
397
|
+
consola.start("Uploading...");
|
|
398
|
+
const bundle = {
|
|
399
|
+
manifest: {
|
|
400
|
+
assets: fileList,
|
|
401
|
+
hasWorker: isSSR,
|
|
402
|
+
entrypoint: null,
|
|
403
|
+
renderMode,
|
|
404
|
+
},
|
|
405
|
+
workerScript: null,
|
|
406
|
+
assets: clientAssets,
|
|
407
|
+
serverFiles,
|
|
408
|
+
resources: config.resources,
|
|
409
|
+
};
|
|
410
|
+
await client.uploadDeploymentBundle(project.id, deployment.id, bundle);
|
|
411
|
+
// Poll for async deploy progress
|
|
412
|
+
const POLL_INTERVAL = 1000;
|
|
413
|
+
const POLL_TIMEOUT = 120_000;
|
|
414
|
+
const TERMINAL = new Set(["active", "failed", "cancelled"]);
|
|
415
|
+
const STEP_LABELS = {
|
|
416
|
+
queued: "Waiting...",
|
|
417
|
+
uploading: "Uploading bundle...",
|
|
418
|
+
provisioning: "Provisioning resources...",
|
|
419
|
+
deploying: "Deploying to edge...",
|
|
420
|
+
};
|
|
421
|
+
let lastStatus = "";
|
|
422
|
+
const start = Date.now();
|
|
423
|
+
while (Date.now() - start < POLL_TIMEOUT) {
|
|
424
|
+
const res = await client.getDeploymentStatus(project.id, deployment.id);
|
|
425
|
+
const { status, failed_step, error_message } = res.deployment;
|
|
426
|
+
if (status !== lastStatus) {
|
|
427
|
+
if (lastStatus && STEP_LABELS[lastStatus]) {
|
|
428
|
+
consola.success(STEP_LABELS[lastStatus].replace("...", ""));
|
|
429
|
+
}
|
|
430
|
+
if (!TERMINAL.has(status) && STEP_LABELS[status]) {
|
|
431
|
+
consola.start(STEP_LABELS[status]);
|
|
432
|
+
}
|
|
433
|
+
lastStatus = status;
|
|
434
|
+
}
|
|
435
|
+
if (status === "active") {
|
|
436
|
+
consola.success(`Deployed! ${res.url ?? res.previewUrl}`);
|
|
437
|
+
if (res.url && res.previewUrl) {
|
|
438
|
+
consola.info(`Preview: ${res.previewUrl}`);
|
|
439
|
+
}
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (status === "failed") {
|
|
443
|
+
const step = failed_step ? ` at ${failed_step}` : "";
|
|
444
|
+
const msg = error_message ?? "Unknown error";
|
|
445
|
+
consola.error(`Deploy failed${step}: ${msg}`);
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
if (status === "cancelled") {
|
|
449
|
+
consola.warn("Deploy was cancelled");
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
453
|
+
}
|
|
454
|
+
consola.error("Deploy timed out after 2 minutes");
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
catch (err) {
|
|
458
|
+
if (err instanceof CreekAuthError) {
|
|
459
|
+
consola.error("Authentication failed. Run `creek login` to re-authenticate.");
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
throw err;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
//# sourceMappingURL=deploy.js.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import { CreekClient } from "@solcreek/sdk";
|
|
4
|
+
import { getToken, getApiUrl } from "../utils/config.js";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { parseConfig } from "@solcreek/sdk";
|
|
8
|
+
function getProjectSlug() {
|
|
9
|
+
const configPath = join(process.cwd(), "creek.toml");
|
|
10
|
+
if (!existsSync(configPath)) {
|
|
11
|
+
consola.error("No creek.toml found. Run `creek init` first.");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
return parseConfig(readFileSync(configPath, "utf-8")).project.name;
|
|
15
|
+
}
|
|
16
|
+
function getClient() {
|
|
17
|
+
const token = getToken();
|
|
18
|
+
if (!token) {
|
|
19
|
+
consola.error("Not authenticated. Run `creek login` first.");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
return new CreekClient(getApiUrl(), token);
|
|
23
|
+
}
|
|
24
|
+
const envSet = defineCommand({
|
|
25
|
+
meta: { name: "set", description: "Set an environment variable" },
|
|
26
|
+
args: {
|
|
27
|
+
key: { type: "positional", description: "Variable name (e.g. DATABASE_URL)", required: true },
|
|
28
|
+
value: { type: "positional", description: "Variable value", required: true },
|
|
29
|
+
},
|
|
30
|
+
async run({ args }) {
|
|
31
|
+
const client = getClient();
|
|
32
|
+
const slug = getProjectSlug();
|
|
33
|
+
await client.setEnvVar(slug, args.key, args.value);
|
|
34
|
+
consola.success(`Set ${args.key}`);
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
function redact(value) {
|
|
38
|
+
if (value.length <= 4)
|
|
39
|
+
return "••••";
|
|
40
|
+
return value.slice(0, 2) + "•".repeat(Math.min(value.length - 4, 20)) + value.slice(-2);
|
|
41
|
+
}
|
|
42
|
+
const envGet = defineCommand({
|
|
43
|
+
meta: { name: "ls", description: "List environment variables" },
|
|
44
|
+
args: {
|
|
45
|
+
show: { type: "boolean", description: "Show values in plaintext (default: redacted)", default: false },
|
|
46
|
+
},
|
|
47
|
+
async run({ args }) {
|
|
48
|
+
const client = getClient();
|
|
49
|
+
const slug = getProjectSlug();
|
|
50
|
+
const vars = await client.listEnvVars(slug);
|
|
51
|
+
if (vars.length === 0) {
|
|
52
|
+
consola.info("No environment variables set.");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
for (const v of vars) {
|
|
56
|
+
const displayed = args.show ? v.value : redact(v.value);
|
|
57
|
+
consola.log(` ${v.key} = ${displayed}`);
|
|
58
|
+
}
|
|
59
|
+
if (!args.show) {
|
|
60
|
+
consola.info(" (use --show to reveal values)");
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
const envRm = defineCommand({
|
|
65
|
+
meta: { name: "rm", description: "Remove an environment variable" },
|
|
66
|
+
args: {
|
|
67
|
+
key: { type: "positional", description: "Variable name to remove", required: true },
|
|
68
|
+
},
|
|
69
|
+
async run({ args }) {
|
|
70
|
+
const client = getClient();
|
|
71
|
+
const slug = getProjectSlug();
|
|
72
|
+
await client.deleteEnvVar(slug, args.key);
|
|
73
|
+
consola.success(`Removed ${args.key}`);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
export const envCommand = defineCommand({
|
|
77
|
+
meta: {
|
|
78
|
+
name: "env",
|
|
79
|
+
description: "Manage environment variables",
|
|
80
|
+
},
|
|
81
|
+
subCommands: {
|
|
82
|
+
set: envSet,
|
|
83
|
+
ls: envGet,
|
|
84
|
+
rm: envRm,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
//# sourceMappingURL=env.js.map
|