creek 0.3.0-alpha.2 → 0.3.0-alpha.3
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 +5 -0
- package/dist/commands/deploy.js +249 -44
- package/dist/commands/env.d.ts +2 -0
- package/dist/commands/env.js +87 -0
- package/dist/commands/login.d.ts +5 -0
- package/dist/commands/login.js +93 -14
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +27 -0
- package/dist/index.js +6 -0
- package/dist/utils/auth-server.d.ts +22 -0
- package/dist/utils/auth-server.js +91 -0
- package/dist/utils/config.d.ts +1 -0
- package/dist/utils/config.js +6 -2
- package/dist/utils/sandbox.d.ts +43 -0
- package/dist/utils/sandbox.js +56 -0
- package/package.json +1 -1
|
@@ -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
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
import consola from "consola";
|
|
3
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
5
|
-
import { execSync } from "node:child_process";
|
|
6
|
-
import { parseConfig, CreekClient, isSSRFramework, getSSRServerEntry, getClientAssetsDir, } from "@solcreek/sdk";
|
|
3
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { join, resolve, basename } 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
7
|
import { getToken, getApiUrl } from "../utils/config.js";
|
|
8
8
|
import { collectAssets } from "../utils/bundle.js";
|
|
9
9
|
import { bundleSSRServer } from "../utils/ssr-bundle.js";
|
|
10
|
+
import { sandboxDeploy, pollSandboxStatus, printSandboxSuccess } from "../utils/sandbox.js";
|
|
10
11
|
export const deployCommand = defineCommand({
|
|
11
12
|
meta: {
|
|
12
13
|
name: "deploy",
|
|
@@ -18,21 +19,200 @@ export const deployCommand = defineCommand({
|
|
|
18
19
|
description: "Skip the build step",
|
|
19
20
|
default: false,
|
|
20
21
|
},
|
|
22
|
+
template: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "Deploy a template (e.g., react-dashboard, astro-landing)",
|
|
25
|
+
required: false,
|
|
26
|
+
},
|
|
21
27
|
},
|
|
22
28
|
async run({ args }) {
|
|
29
|
+
// --- Template deploy ---
|
|
30
|
+
if (args.template) {
|
|
31
|
+
return await deployTemplate(args.template);
|
|
32
|
+
}
|
|
33
|
+
// --- Check if user has an account ---
|
|
34
|
+
const token = getToken();
|
|
23
35
|
const cwd = process.cwd();
|
|
24
36
|
const configPath = join(cwd, "creek.toml");
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
37
|
+
const hasConfig = existsSync(configPath);
|
|
38
|
+
if (!hasConfig) {
|
|
39
|
+
// No creek.toml → sandbox mode (works with or without account)
|
|
40
|
+
return await deploySandbox(cwd, args["skip-build"]);
|
|
28
41
|
}
|
|
29
|
-
const config = parseConfig(readFileSync(configPath, "utf-8"));
|
|
30
|
-
consola.info(`Project: ${config.project.name}`);
|
|
31
|
-
const token = getToken();
|
|
32
42
|
if (!token) {
|
|
33
43
|
consola.error("Not authenticated. Run `creek login` first.");
|
|
44
|
+
consola.info("Or deploy without an account: remove creek.toml and run `creek deploy` again.");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
// --- Regular deploy (authenticated, creek.toml present) ---
|
|
48
|
+
return await deployAuthenticated(cwd, configPath, token, args["skip-build"]);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Sandbox deploy — no account, auto-detect framework
|
|
53
|
+
// ============================================================================
|
|
54
|
+
async function deploySandbox(cwd, skipBuild) {
|
|
55
|
+
consola.info("No account found. Deploying to sandbox (60 min preview).");
|
|
56
|
+
consola.info("");
|
|
57
|
+
// Auto-detect framework
|
|
58
|
+
const pkgPath = join(cwd, "package.json");
|
|
59
|
+
if (!existsSync(pkgPath)) {
|
|
60
|
+
consola.error("No package.json found. Make sure you're in a project directory.");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
64
|
+
const framework = detectFramework(pkg);
|
|
65
|
+
const projectName = pkg.name ?? basename(cwd);
|
|
66
|
+
if (framework) {
|
|
67
|
+
consola.info(`Detected: ${framework}`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
consola.info("Framework: auto (static site)");
|
|
71
|
+
}
|
|
72
|
+
// Build
|
|
73
|
+
const buildCommand = pkg.scripts?.build ? "npm run build" : null;
|
|
74
|
+
const outputDir = resolve(cwd, getDefaultBuildOutput(framework));
|
|
75
|
+
if (!skipBuild) {
|
|
76
|
+
if (!buildCommand) {
|
|
77
|
+
consola.error("No build script found in package.json.");
|
|
78
|
+
consola.info("Add a 'build' script or use --skip-build if already built.");
|
|
34
79
|
process.exit(1);
|
|
35
80
|
}
|
|
81
|
+
consola.start(`Building with: ${buildCommand}`);
|
|
82
|
+
try {
|
|
83
|
+
execSync(buildCommand, { cwd, stdio: "inherit" });
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
consola.error("Build failed");
|
|
87
|
+
consola.info("");
|
|
88
|
+
consola.info("Common fixes:");
|
|
89
|
+
consola.info(" • npm install (missing dependencies?)");
|
|
90
|
+
consola.info(" • Check for TypeScript errors");
|
|
91
|
+
consola.info(` • Verify build works: ${buildCommand}`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
consola.success("Build complete");
|
|
95
|
+
}
|
|
96
|
+
if (!existsSync(outputDir)) {
|
|
97
|
+
consola.error(`Build output not found: ${outputDir}`);
|
|
98
|
+
if (framework) {
|
|
99
|
+
consola.info(`Expected output for ${framework}: ${getDefaultBuildOutput(framework)}`);
|
|
100
|
+
}
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
// Collect assets
|
|
104
|
+
const isSSR = isSSRFramework(framework);
|
|
105
|
+
const renderMode = isSSR ? "ssr" : "spa";
|
|
106
|
+
let clientAssetsDir = outputDir;
|
|
107
|
+
if (isSSR && framework) {
|
|
108
|
+
const subdir = getClientAssetsDir(framework);
|
|
109
|
+
if (subdir)
|
|
110
|
+
clientAssetsDir = resolve(outputDir, subdir);
|
|
111
|
+
}
|
|
112
|
+
consola.start("Collecting assets...");
|
|
113
|
+
const { assets: clientAssets, fileList } = collectAssets(clientAssetsDir);
|
|
114
|
+
consola.info(`Found ${fileList.length} assets`);
|
|
115
|
+
let serverFiles;
|
|
116
|
+
if (isSSR && framework) {
|
|
117
|
+
const serverEntry = getSSRServerEntry(framework);
|
|
118
|
+
if (serverEntry) {
|
|
119
|
+
const serverEntryPath = resolve(outputDir, serverEntry);
|
|
120
|
+
if (existsSync(serverEntryPath)) {
|
|
121
|
+
consola.start("Bundling SSR server...");
|
|
122
|
+
const bundled = await bundleSSRServer(serverEntryPath);
|
|
123
|
+
serverFiles = { "server.js": Buffer.from(bundled).toString("base64") };
|
|
124
|
+
consola.success(`SSR bundled (${Math.round(bundled.length / 1024)}KB)`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Deploy to sandbox
|
|
129
|
+
consola.start("Deploying to sandbox...");
|
|
130
|
+
try {
|
|
131
|
+
const result = await sandboxDeploy({
|
|
132
|
+
manifest: { assets: fileList, hasWorker: isSSR, entrypoint: null, renderMode },
|
|
133
|
+
assets: clientAssets,
|
|
134
|
+
serverFiles,
|
|
135
|
+
framework: framework ?? undefined,
|
|
136
|
+
source: "cli",
|
|
137
|
+
});
|
|
138
|
+
consola.start("Waiting for deployment...");
|
|
139
|
+
const status = await pollSandboxStatus(result.statusUrl);
|
|
140
|
+
printSandboxSuccess(status.previewUrl, result.expiresAt, result.sandboxId);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
consola.error(err instanceof Error ? err.message : "Sandbox deploy failed");
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// ============================================================================
|
|
148
|
+
// Template deploy — clone + build + deploy to sandbox
|
|
149
|
+
// ============================================================================
|
|
150
|
+
async function deployTemplate(templateId) {
|
|
151
|
+
// Validate template ID — alphanumeric, hyphens, underscores only (no path traversal)
|
|
152
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(templateId)) {
|
|
153
|
+
consola.error("Invalid template name. Use only letters, numbers, hyphens, and underscores.");
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
consola.info(`Deploying template: ${templateId}`);
|
|
157
|
+
// Clone template to temp dir
|
|
158
|
+
const tmpDir = join(process.env.TMPDIR ?? "/tmp", `creek-template-${Date.now()}`);
|
|
159
|
+
const repoUrl = "https://github.com/solcreek/templates";
|
|
160
|
+
consola.start("Cloning template...");
|
|
161
|
+
try {
|
|
162
|
+
execFileSync("git", [
|
|
163
|
+
"clone", "--depth", "1", "--filter=blob:none", "--sparse", repoUrl, tmpDir,
|
|
164
|
+
], { stdio: "pipe" });
|
|
165
|
+
execFileSync("git", [
|
|
166
|
+
"sparse-checkout", "set", templateId,
|
|
167
|
+
], { cwd: tmpDir, stdio: "pipe" });
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
consola.error(`Template '${templateId}' not found.`);
|
|
171
|
+
consola.info("Available templates: creek deploy --template");
|
|
172
|
+
cleanupDir(tmpDir);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
const templateDir = join(tmpDir, templateId);
|
|
176
|
+
// Verify resolved path is still within tmpDir (prevent path traversal)
|
|
177
|
+
if (!resolve(templateDir).startsWith(resolve(tmpDir))) {
|
|
178
|
+
consola.error("Invalid template path.");
|
|
179
|
+
cleanupDir(tmpDir);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
if (!existsSync(templateDir)) {
|
|
183
|
+
consola.error(`Template directory not found: ${templateId}`);
|
|
184
|
+
cleanupDir(tmpDir);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
consola.start("Installing dependencies...");
|
|
188
|
+
try {
|
|
189
|
+
execFileSync("npm", ["install"], { cwd: templateDir, stdio: "pipe" });
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
consola.error("Failed to install dependencies");
|
|
193
|
+
cleanupDir(tmpDir);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
// Deploy as sandbox (reuse sandbox flow)
|
|
197
|
+
await deploySandbox(templateDir, false);
|
|
198
|
+
// Cleanup
|
|
199
|
+
cleanupDir(tmpDir);
|
|
200
|
+
}
|
|
201
|
+
function cleanupDir(dir) {
|
|
202
|
+
try {
|
|
203
|
+
rmSync(dir, { recursive: true, force: true });
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// ignore
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// ============================================================================
|
|
210
|
+
// Authenticated deploy — existing flow
|
|
211
|
+
// ============================================================================
|
|
212
|
+
async function deployAuthenticated(cwd, configPath, token, skipBuild) {
|
|
213
|
+
try {
|
|
214
|
+
const config = parseConfig(readFileSync(configPath, "utf-8"));
|
|
215
|
+
consola.info(`Project: ${config.project.name}`);
|
|
36
216
|
const client = new CreekClient(getApiUrl(), token);
|
|
37
217
|
// Ensure project exists
|
|
38
218
|
let project;
|
|
@@ -48,11 +228,16 @@ export const deployCommand = defineCommand({
|
|
|
48
228
|
project = res.project;
|
|
49
229
|
consola.success(`Created project: ${project.slug}`);
|
|
50
230
|
}
|
|
51
|
-
// Build
|
|
52
|
-
if (!
|
|
53
|
-
|
|
231
|
+
// Build — creek.toml build.command is user-defined, analogous to npm scripts
|
|
232
|
+
if (!skipBuild) {
|
|
233
|
+
const buildCmd = config.build.command;
|
|
234
|
+
if (!buildCmd || typeof buildCmd !== "string" || buildCmd.length > 500) {
|
|
235
|
+
consola.error("Invalid build command in creek.toml");
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
consola.start(`Building with: ${buildCmd}`);
|
|
54
239
|
try {
|
|
55
|
-
execSync(
|
|
240
|
+
execSync(buildCmd, { cwd, stdio: "inherit" });
|
|
56
241
|
}
|
|
57
242
|
catch {
|
|
58
243
|
consola.error("Build failed");
|
|
@@ -68,7 +253,6 @@ export const deployCommand = defineCommand({
|
|
|
68
253
|
const framework = config.project.framework ?? null;
|
|
69
254
|
const isSSR = isSSRFramework(framework);
|
|
70
255
|
const renderMode = isSSR ? "ssr" : "spa";
|
|
71
|
-
// Collect client assets
|
|
72
256
|
let clientAssetsDir = outputDir;
|
|
73
257
|
if (isSSR && framework) {
|
|
74
258
|
const clientSubdir = getClientAssetsDir(framework);
|
|
@@ -79,7 +263,6 @@ export const deployCommand = defineCommand({
|
|
|
79
263
|
consola.start("Collecting assets...");
|
|
80
264
|
const { assets: clientAssets, fileList } = collectAssets(clientAssetsDir);
|
|
81
265
|
consola.info(`Found ${fileList.length} client assets`);
|
|
82
|
-
// Bundle server files for SSR
|
|
83
266
|
let serverFiles;
|
|
84
267
|
if (isSSR && framework) {
|
|
85
268
|
const serverEntry = getSSRServerEntry(framework);
|
|
@@ -88,7 +271,6 @@ export const deployCommand = defineCommand({
|
|
|
88
271
|
if (existsSync(serverEntryPath)) {
|
|
89
272
|
consola.start("Bundling SSR server...");
|
|
90
273
|
const bundled = await bundleSSRServer(serverEntryPath);
|
|
91
|
-
// Send as base64-encoded single file
|
|
92
274
|
serverFiles = {
|
|
93
275
|
"server.js": Buffer.from(bundled).toString("base64"),
|
|
94
276
|
};
|
|
@@ -96,10 +278,8 @@ export const deployCommand = defineCommand({
|
|
|
96
278
|
}
|
|
97
279
|
}
|
|
98
280
|
}
|
|
99
|
-
// Create deployment
|
|
100
281
|
consola.start("Creating deployment...");
|
|
101
282
|
const { deployment } = await client.createDeployment(project.id);
|
|
102
|
-
// Upload bundle
|
|
103
283
|
consola.start("Uploading...");
|
|
104
284
|
const bundle = {
|
|
105
285
|
manifest: {
|
|
@@ -111,36 +291,61 @@ export const deployCommand = defineCommand({
|
|
|
111
291
|
workerScript: null,
|
|
112
292
|
assets: clientAssets,
|
|
113
293
|
serverFiles,
|
|
294
|
+
resources: config.resources,
|
|
114
295
|
};
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
296
|
+
await client.uploadDeploymentBundle(project.id, deployment.id, bundle);
|
|
297
|
+
// Poll for async deploy progress
|
|
298
|
+
const POLL_INTERVAL = 1000;
|
|
299
|
+
const POLL_TIMEOUT = 120_000;
|
|
300
|
+
const TERMINAL = new Set(["active", "failed", "cancelled"]);
|
|
301
|
+
const STEP_LABELS = {
|
|
302
|
+
queued: "Waiting...",
|
|
303
|
+
uploading: "Uploading bundle...",
|
|
304
|
+
provisioning: "Provisioning resources...",
|
|
305
|
+
deploying: "Deploying to edge...",
|
|
306
|
+
};
|
|
307
|
+
let lastStatus = "";
|
|
308
|
+
const start = Date.now();
|
|
309
|
+
while (Date.now() - start < POLL_TIMEOUT) {
|
|
310
|
+
const res = await client.getDeploymentStatus(project.id, deployment.id);
|
|
311
|
+
const { status, failed_step, error_message } = res.deployment;
|
|
312
|
+
if (status !== lastStatus) {
|
|
313
|
+
if (lastStatus && STEP_LABELS[lastStatus]) {
|
|
314
|
+
consola.success(STEP_LABELS[lastStatus].replace("...", ""));
|
|
315
|
+
}
|
|
316
|
+
if (!TERMINAL.has(status) && STEP_LABELS[status]) {
|
|
317
|
+
consola.start(STEP_LABELS[status]);
|
|
318
|
+
}
|
|
319
|
+
lastStatus = status;
|
|
132
320
|
}
|
|
133
|
-
if (status === "active"
|
|
134
|
-
consola.success(`Deployed! ${url}`);
|
|
321
|
+
if (status === "active") {
|
|
322
|
+
consola.success(`Deployed! ${res.url ?? res.previewUrl}`);
|
|
323
|
+
if (res.url && res.previewUrl) {
|
|
324
|
+
consola.info(`Preview: ${res.previewUrl}`);
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
135
327
|
}
|
|
136
|
-
|
|
137
|
-
|
|
328
|
+
if (status === "failed") {
|
|
329
|
+
const step = failed_step ? ` at ${failed_step}` : "";
|
|
330
|
+
const msg = error_message ?? "Unknown error";
|
|
331
|
+
consola.error(`Deploy failed${step}: ${msg}`);
|
|
138
332
|
process.exit(1);
|
|
139
333
|
}
|
|
140
|
-
|
|
141
|
-
consola.warn(
|
|
334
|
+
if (status === "cancelled") {
|
|
335
|
+
consola.warn("Deploy was cancelled");
|
|
336
|
+
process.exit(1);
|
|
142
337
|
}
|
|
338
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
143
339
|
}
|
|
144
|
-
|
|
145
|
-
|
|
340
|
+
consola.error("Deploy timed out after 2 minutes");
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
if (err instanceof CreekAuthError) {
|
|
345
|
+
consola.error("Authentication failed. Run `creek login` to re-authenticate.");
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
throw err;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
146
351
|
//# 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
|
package/dist/commands/login.d.ts
CHANGED
package/dist/commands/login.js
CHANGED
|
@@ -1,32 +1,111 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
import consola from "consola";
|
|
3
|
-
import {
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { CreekClient } from "@solcreek/sdk";
|
|
5
|
+
import { writeCliConfig, readCliConfig, getApiUrl } from "../utils/config.js";
|
|
6
|
+
import { startAuthServer } from "../utils/auth-server.js";
|
|
7
|
+
function getDashboardUrl() {
|
|
8
|
+
const apiUrl = getApiUrl();
|
|
9
|
+
// http://localhost:8787 → http://localhost:3000
|
|
10
|
+
// https://api.creek.dev → https://app.creek.dev
|
|
11
|
+
return apiUrl
|
|
12
|
+
.replace("api.", "app.")
|
|
13
|
+
.replace(":8787", ":3000");
|
|
14
|
+
}
|
|
15
|
+
function openBrowser(url) {
|
|
16
|
+
try {
|
|
17
|
+
const cmd = process.platform === "darwin"
|
|
18
|
+
? "open"
|
|
19
|
+
: process.platform === "win32"
|
|
20
|
+
? "start"
|
|
21
|
+
: "xdg-open";
|
|
22
|
+
execFileSync(cmd, [url], { stdio: "ignore" });
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Browser open failed — user will need to copy the URL manually
|
|
26
|
+
}
|
|
27
|
+
}
|
|
4
28
|
export const loginCommand = defineCommand({
|
|
5
29
|
meta: {
|
|
6
30
|
name: "login",
|
|
7
|
-
description: "Authenticate with Creek
|
|
31
|
+
description: "Authenticate with Creek",
|
|
8
32
|
},
|
|
9
33
|
args: {
|
|
10
34
|
token: {
|
|
11
35
|
type: "string",
|
|
12
|
-
description: "API
|
|
36
|
+
description: "API key (for CI/CD, skips interactive prompt)",
|
|
13
37
|
required: false,
|
|
14
38
|
},
|
|
39
|
+
headless: {
|
|
40
|
+
type: "boolean",
|
|
41
|
+
description: "Use headless mode (paste API key manually, for SSH/remote)",
|
|
42
|
+
default: false,
|
|
43
|
+
},
|
|
15
44
|
},
|
|
16
45
|
async run({ args }) {
|
|
17
|
-
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
type: "text",
|
|
21
|
-
});
|
|
46
|
+
// Mode 1: --token (CI/CD)
|
|
47
|
+
if (args.token) {
|
|
48
|
+
return await saveAndVerify(args.token);
|
|
22
49
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
50
|
+
// Mode 2: --headless (SSH/remote — prompt for API key)
|
|
51
|
+
if (args.headless) {
|
|
52
|
+
return await headlessLogin();
|
|
26
53
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
consola.success("Token saved successfully");
|
|
54
|
+
// Mode 3: Default — localhost redirect (best UX)
|
|
55
|
+
return await browserLogin();
|
|
30
56
|
},
|
|
31
57
|
});
|
|
58
|
+
/**
|
|
59
|
+
* Default login: open browser → dashboard creates API key → redirect to localhost callback.
|
|
60
|
+
*/
|
|
61
|
+
async function browserLogin() {
|
|
62
|
+
const { port, state, waitForCallback, close } = startAuthServer();
|
|
63
|
+
const dashboardUrl = getDashboardUrl();
|
|
64
|
+
const authUrl = `${dashboardUrl}/cli-auth?port=${port}&state=${state}`;
|
|
65
|
+
consola.info("Opening browser to authenticate...");
|
|
66
|
+
consola.info(`If the browser doesn't open, visit: ${authUrl}`);
|
|
67
|
+
consola.info("");
|
|
68
|
+
openBrowser(authUrl);
|
|
69
|
+
consola.start("Waiting for authentication...");
|
|
70
|
+
try {
|
|
71
|
+
const key = await waitForCallback();
|
|
72
|
+
await saveAndVerify(key);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
close();
|
|
76
|
+
consola.error(err instanceof Error ? err.message : "Authentication failed");
|
|
77
|
+
consola.info("Try `creek login --headless` if browser login isn't working.");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Headless login: prompt user to paste API key from dashboard.
|
|
83
|
+
*/
|
|
84
|
+
async function headlessLogin() {
|
|
85
|
+
const dashboardUrl = getDashboardUrl();
|
|
86
|
+
consola.info("Create an API key in the Creek dashboard:");
|
|
87
|
+
consola.info(` ${dashboardUrl}/api-keys`);
|
|
88
|
+
consola.info("");
|
|
89
|
+
const apiKey = await consola.prompt("Paste your API key:", { type: "text" });
|
|
90
|
+
if (!apiKey || typeof apiKey !== "string") {
|
|
91
|
+
consola.error("No API key provided");
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
await saveAndVerify(apiKey.trim());
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Validate key against API, save to config, print success.
|
|
98
|
+
*/
|
|
99
|
+
async function saveAndVerify(apiKey) {
|
|
100
|
+
consola.start("Verifying...");
|
|
101
|
+
const client = new CreekClient(getApiUrl(), apiKey);
|
|
102
|
+
const session = await client.getSession();
|
|
103
|
+
if (!session?.user) {
|
|
104
|
+
consola.error("Invalid API key. Please check and try again.");
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
const config = readCliConfig();
|
|
108
|
+
writeCliConfig({ ...config, token: apiKey });
|
|
109
|
+
consola.success(`Logged in as ${session.user.name} (${session.user.email})`);
|
|
110
|
+
}
|
|
32
111
|
//# sourceMappingURL=login.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
export const whoamiCommand = defineCommand({
|
|
6
|
+
meta: {
|
|
7
|
+
name: "whoami",
|
|
8
|
+
description: "Show the currently authenticated user",
|
|
9
|
+
},
|
|
10
|
+
async run() {
|
|
11
|
+
const token = getToken();
|
|
12
|
+
if (!token) {
|
|
13
|
+
consola.error("Not authenticated. Run `creek login` first.");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const client = new CreekClient(getApiUrl(), token);
|
|
17
|
+
const session = await client.getSession();
|
|
18
|
+
if (!session?.user) {
|
|
19
|
+
consola.error("Session expired or invalid. Run `creek login` to re-authenticate.");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
consola.log(` User: ${session.user.name}`);
|
|
23
|
+
consola.log(` Email: ${session.user.email}`);
|
|
24
|
+
consola.log(` API: ${getApiUrl()}`);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
//# sourceMappingURL=whoami.js.map
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { defineCommand, runMain } from "citty";
|
|
3
3
|
import { loginCommand } from "./commands/login.js";
|
|
4
|
+
import { whoamiCommand } from "./commands/whoami.js";
|
|
4
5
|
import { initCommand } from "./commands/init.js";
|
|
5
6
|
import { deployCommand } from "./commands/deploy.js";
|
|
7
|
+
import { claimCommand } from "./commands/claim.js";
|
|
8
|
+
import { envCommand } from "./commands/env.js";
|
|
6
9
|
const main = defineCommand({
|
|
7
10
|
meta: {
|
|
8
11
|
name: "creek",
|
|
@@ -11,8 +14,11 @@ const main = defineCommand({
|
|
|
11
14
|
},
|
|
12
15
|
subCommands: {
|
|
13
16
|
login: loginCommand,
|
|
17
|
+
whoami: whoamiCommand,
|
|
14
18
|
init: initCommand,
|
|
15
19
|
deploy: deployCommand,
|
|
20
|
+
claim: claimCommand,
|
|
21
|
+
env: envCommand,
|
|
16
22
|
},
|
|
17
23
|
});
|
|
18
24
|
runMain(main);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface AuthCallbackResult {
|
|
2
|
+
key: string;
|
|
3
|
+
state: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Start a temporary local HTTP server to receive the OAuth-style callback
|
|
7
|
+
* from the dashboard after CLI auth.
|
|
8
|
+
*
|
|
9
|
+
* Flow:
|
|
10
|
+
* 1. Server starts on a random available port
|
|
11
|
+
* 2. CLI opens browser to dashboard /cli-auth?port=X&state=Y
|
|
12
|
+
* 3. Dashboard creates API key, redirects to http://localhost:X/callback?key=...&state=...
|
|
13
|
+
* 4. This server receives the callback, validates state, resolves the promise
|
|
14
|
+
* 5. Server auto-closes
|
|
15
|
+
*/
|
|
16
|
+
export declare function startAuthServer(): {
|
|
17
|
+
port: number;
|
|
18
|
+
state: string;
|
|
19
|
+
waitForCallback: () => Promise<string>;
|
|
20
|
+
close: () => void;
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=auth-server.d.ts.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
/**
|
|
4
|
+
* Start a temporary local HTTP server to receive the OAuth-style callback
|
|
5
|
+
* from the dashboard after CLI auth.
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. Server starts on a random available port
|
|
9
|
+
* 2. CLI opens browser to dashboard /cli-auth?port=X&state=Y
|
|
10
|
+
* 3. Dashboard creates API key, redirects to http://localhost:X/callback?key=...&state=...
|
|
11
|
+
* 4. This server receives the callback, validates state, resolves the promise
|
|
12
|
+
* 5. Server auto-closes
|
|
13
|
+
*/
|
|
14
|
+
export function startAuthServer() {
|
|
15
|
+
const state = randomBytes(16).toString("hex");
|
|
16
|
+
let resolveCallback;
|
|
17
|
+
let rejectCallback;
|
|
18
|
+
const callbackPromise = new Promise((resolve, reject) => {
|
|
19
|
+
resolveCallback = resolve;
|
|
20
|
+
rejectCallback = reject;
|
|
21
|
+
});
|
|
22
|
+
const server = createServer((req, res) => {
|
|
23
|
+
const url = new URL(req.url ?? "/", `http://localhost`);
|
|
24
|
+
if (url.pathname === "/callback") {
|
|
25
|
+
const key = url.searchParams.get("key");
|
|
26
|
+
const returnedState = url.searchParams.get("state");
|
|
27
|
+
if (returnedState !== state) {
|
|
28
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
29
|
+
res.end(htmlPage("Authentication Failed", "Invalid state parameter. Please try again."));
|
|
30
|
+
rejectCallback(new Error("State mismatch — possible CSRF attack"));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!key) {
|
|
34
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
35
|
+
res.end(htmlPage("Authentication Failed", "No API key received. Please try again."));
|
|
36
|
+
rejectCallback(new Error("No API key in callback"));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
40
|
+
res.end(htmlPage("Authenticated!", "You can close this window and return to the terminal."));
|
|
41
|
+
resolveCallback(key);
|
|
42
|
+
setTimeout(() => server.close(), 500);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
res.writeHead(404);
|
|
46
|
+
res.end("Not found");
|
|
47
|
+
});
|
|
48
|
+
// Listen on port 0 = OS picks a random available port
|
|
49
|
+
server.listen(0, "localhost");
|
|
50
|
+
const address = server.address();
|
|
51
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
52
|
+
// Timeout after 2 minutes
|
|
53
|
+
const timeout = setTimeout(() => {
|
|
54
|
+
rejectCallback(new Error("Login timed out after 2 minutes"));
|
|
55
|
+
server.close();
|
|
56
|
+
}, 120_000);
|
|
57
|
+
return {
|
|
58
|
+
port,
|
|
59
|
+
state,
|
|
60
|
+
waitForCallback: () => callbackPromise.finally(() => clearTimeout(timeout)),
|
|
61
|
+
close: () => {
|
|
62
|
+
clearTimeout(timeout);
|
|
63
|
+
server.close();
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function escapeHtml(s) {
|
|
68
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
69
|
+
}
|
|
70
|
+
function htmlPage(title, message) {
|
|
71
|
+
return `<!DOCTYPE html>
|
|
72
|
+
<html>
|
|
73
|
+
<head>
|
|
74
|
+
<meta charset="utf-8">
|
|
75
|
+
<title>Creek CLI - ${escapeHtml(title)}</title>
|
|
76
|
+
<style>
|
|
77
|
+
body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #0a0a0a; color: #eee; }
|
|
78
|
+
.card { text-align: center; padding: 2rem; }
|
|
79
|
+
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
|
|
80
|
+
p { color: #888; }
|
|
81
|
+
</style>
|
|
82
|
+
</head>
|
|
83
|
+
<body>
|
|
84
|
+
<div class="card">
|
|
85
|
+
<h1>${escapeHtml(title)}</h1>
|
|
86
|
+
<p>${escapeHtml(message)}</p>
|
|
87
|
+
</div>
|
|
88
|
+
</body>
|
|
89
|
+
</html>`;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=auth-server.js.map
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -7,5 +7,6 @@ export declare function readCliConfig(): CliConfig;
|
|
|
7
7
|
export declare function writeCliConfig(config: CliConfig): void;
|
|
8
8
|
export declare function getToken(): string | undefined;
|
|
9
9
|
export declare function getApiUrl(): string;
|
|
10
|
+
export declare function getSandboxApiUrl(): string;
|
|
10
11
|
export {};
|
|
11
12
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/utils/config.js
CHANGED
|
@@ -18,9 +18,9 @@ export function readCliConfig() {
|
|
|
18
18
|
}
|
|
19
19
|
export function writeCliConfig(config) {
|
|
20
20
|
if (!existsSync(CONFIG_DIR)) {
|
|
21
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
21
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
22
22
|
}
|
|
23
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
23
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
24
24
|
}
|
|
25
25
|
export function getToken() {
|
|
26
26
|
return process.env.CREEK_TOKEN ?? readCliConfig().token;
|
|
@@ -30,4 +30,8 @@ export function getApiUrl() {
|
|
|
30
30
|
readCliConfig().apiUrl ??
|
|
31
31
|
"https://api.creek.dev");
|
|
32
32
|
}
|
|
33
|
+
export function getSandboxApiUrl() {
|
|
34
|
+
return (process.env.CREEK_SANDBOX_API_URL ??
|
|
35
|
+
"https://sandbox-api.creek.dev");
|
|
36
|
+
}
|
|
33
37
|
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
interface SandboxDeployResponse {
|
|
2
|
+
sandboxId: string;
|
|
3
|
+
status: string;
|
|
4
|
+
statusUrl: string;
|
|
5
|
+
previewUrl: string;
|
|
6
|
+
expiresAt: string;
|
|
7
|
+
}
|
|
8
|
+
interface SandboxStatusResponse {
|
|
9
|
+
sandboxId: string;
|
|
10
|
+
status: string;
|
|
11
|
+
previewUrl: string;
|
|
12
|
+
expiresAt: string;
|
|
13
|
+
expiresInSeconds: number;
|
|
14
|
+
claimable: boolean;
|
|
15
|
+
failedStep?: string;
|
|
16
|
+
errorMessage?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Deploy a bundle to the sandbox API (no auth required).
|
|
20
|
+
*/
|
|
21
|
+
export declare function sandboxDeploy(bundle: {
|
|
22
|
+
manifest: {
|
|
23
|
+
assets: string[];
|
|
24
|
+
hasWorker: boolean;
|
|
25
|
+
entrypoint: string | null;
|
|
26
|
+
renderMode: string;
|
|
27
|
+
};
|
|
28
|
+
assets: Record<string, string>;
|
|
29
|
+
serverFiles?: Record<string, string>;
|
|
30
|
+
framework?: string;
|
|
31
|
+
templateId?: string;
|
|
32
|
+
source: string;
|
|
33
|
+
}): Promise<SandboxDeployResponse>;
|
|
34
|
+
/**
|
|
35
|
+
* Poll sandbox status until terminal state.
|
|
36
|
+
*/
|
|
37
|
+
export declare function pollSandboxStatus(statusUrl: string): Promise<SandboxStatusResponse>;
|
|
38
|
+
/**
|
|
39
|
+
* Print sandbox success message with claim instructions.
|
|
40
|
+
*/
|
|
41
|
+
export declare function printSandboxSuccess(previewUrl: string, expiresAt: string, sandboxId: string): void;
|
|
42
|
+
export {};
|
|
43
|
+
//# sourceMappingURL=sandbox.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import consola from "consola";
|
|
2
|
+
import { getSandboxApiUrl } from "./config.js";
|
|
3
|
+
/**
|
|
4
|
+
* Deploy a bundle to the sandbox API (no auth required).
|
|
5
|
+
*/
|
|
6
|
+
export async function sandboxDeploy(bundle) {
|
|
7
|
+
const apiUrl = getSandboxApiUrl();
|
|
8
|
+
const res = await fetch(`${apiUrl}/api/sandbox/deploy`, {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: { "Content-Type": "application/json" },
|
|
11
|
+
body: JSON.stringify(bundle),
|
|
12
|
+
});
|
|
13
|
+
if (!res.ok) {
|
|
14
|
+
const err = await res.json().catch(() => ({ message: res.statusText }));
|
|
15
|
+
throw new Error(err.message ?? `Sandbox deploy failed (${res.status})`);
|
|
16
|
+
}
|
|
17
|
+
return res.json();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Poll sandbox status until terminal state.
|
|
21
|
+
*/
|
|
22
|
+
export async function pollSandboxStatus(statusUrl) {
|
|
23
|
+
const POLL_INTERVAL = 1000;
|
|
24
|
+
const POLL_TIMEOUT = 60_000;
|
|
25
|
+
const start = Date.now();
|
|
26
|
+
while (Date.now() - start < POLL_TIMEOUT) {
|
|
27
|
+
const res = await fetch(statusUrl);
|
|
28
|
+
if (!res.ok)
|
|
29
|
+
throw new Error(`Status check failed (${res.status})`);
|
|
30
|
+
const status = (await res.json());
|
|
31
|
+
if (status.status === "active")
|
|
32
|
+
return status;
|
|
33
|
+
if (status.status === "failed") {
|
|
34
|
+
const step = status.failedStep ? ` at ${status.failedStep}` : "";
|
|
35
|
+
throw new Error(`Sandbox deploy failed${step}: ${status.errorMessage ?? "Unknown error"}`);
|
|
36
|
+
}
|
|
37
|
+
if (status.status === "expired") {
|
|
38
|
+
throw new Error("Sandbox expired before activation");
|
|
39
|
+
}
|
|
40
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
41
|
+
}
|
|
42
|
+
throw new Error("Sandbox deploy timed out");
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Print sandbox success message with claim instructions.
|
|
46
|
+
*/
|
|
47
|
+
export function printSandboxSuccess(previewUrl, expiresAt, sandboxId) {
|
|
48
|
+
consola.success(`Deployed! ${previewUrl}`);
|
|
49
|
+
consola.info("");
|
|
50
|
+
consola.info("This is a free sandbox preview — it will be available for 60 minutes.");
|
|
51
|
+
consola.info("");
|
|
52
|
+
consola.info("Want to keep it? Make it permanent:");
|
|
53
|
+
consola.info(` creek login`);
|
|
54
|
+
consola.info(` creek claim ${sandboxId}`);
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=sandbox.js.map
|