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.
@@ -0,0 +1,8 @@
1
+ export declare const claimCommand: import("citty").CommandDef<{
2
+ sandboxId: {
3
+ type: "positional";
4
+ description: string;
5
+ required: true;
6
+ };
7
+ }>;
8
+ //# sourceMappingURL=claim.d.ts.map
@@ -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
@@ -4,5 +4,10 @@ export declare const deployCommand: import("citty").CommandDef<{
4
4
  description: string;
5
5
  default: false;
6
6
  };
7
+ template: {
8
+ type: "string";
9
+ description: string;
10
+ required: false;
11
+ };
7
12
  }>;
8
13
  //# sourceMappingURL=deploy.d.ts.map
@@ -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
- if (!existsSync(configPath)) {
26
- consola.error("No creek.toml found. Run `creek init` first.");
27
- process.exit(1);
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 (!args["skip-build"]) {
53
- consola.start(`Building with: ${config.build.command}`);
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(config.build.command, { cwd, stdio: "inherit" });
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
- const result = await client.uploadDeploymentBundle(project.id, deployment.id, bundle);
116
- if (result.url) {
117
- consola.success(`Deployed! ${result.url}`);
118
- if (result.previewUrl) {
119
- consola.info(`Preview: ${result.previewUrl}`);
120
- }
121
- }
122
- else {
123
- let status = deployment.status;
124
- let url = null;
125
- for (let i = 0; i < 30; i++) {
126
- const res = await client.getDeploymentStatus(project.id, deployment.id);
127
- status = res.deployment.status;
128
- url = res.url;
129
- if (status === "active" || status === "failed")
130
- break;
131
- await new Promise((r) => setTimeout(r, 1000));
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" && url) {
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
- else if (status === "failed") {
137
- consola.error("Deployment failed");
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
- else {
141
- consola.warn(`Deployment status: ${status}`);
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,2 @@
1
+ export declare const envCommand: import("citty").CommandDef<import("citty").ArgsDef>;
2
+ //# sourceMappingURL=env.d.ts.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
@@ -4,5 +4,10 @@ export declare const loginCommand: import("citty").CommandDef<{
4
4
  description: string;
5
5
  required: false;
6
6
  };
7
+ headless: {
8
+ type: "boolean";
9
+ description: string;
10
+ default: false;
11
+ };
7
12
  }>;
8
13
  //# sourceMappingURL=login.d.ts.map
@@ -1,32 +1,111 @@
1
1
  import { defineCommand } from "citty";
2
2
  import consola from "consola";
3
- import { writeCliConfig, readCliConfig } from "../utils/config.js";
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 using an API token",
31
+ description: "Authenticate with Creek",
8
32
  },
9
33
  args: {
10
34
  token: {
11
35
  type: "string",
12
- description: "API token",
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
- let token = args.token;
18
- if (!token) {
19
- token = await consola.prompt("Enter your API token:", {
20
- type: "text",
21
- });
46
+ // Mode 1: --token (CI/CD)
47
+ if (args.token) {
48
+ return await saveAndVerify(args.token);
22
49
  }
23
- if (!token || typeof token !== "string") {
24
- consola.error("No token provided");
25
- process.exit(1);
50
+ // Mode 2: --headless (SSH/remote prompt for API key)
51
+ if (args.headless) {
52
+ return await headlessLogin();
26
53
  }
27
- const config = readCliConfig();
28
- writeCliConfig({ ...config, token });
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,2 @@
1
+ export declare const whoamiCommand: import("citty").CommandDef<import("citty").ArgsDef>;
2
+ //# sourceMappingURL=whoami.d.ts.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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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
@@ -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
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "creek",
3
- "version": "0.3.0-alpha.2",
3
+ "version": "0.3.0-alpha.3",
4
4
  "description": "CLI for the Creek deployment platform",
5
5
  "type": "module",
6
6
  "bin": {