stage-ai 0.1.0 → 0.2.0

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.
@@ -1,4 +1,4 @@
1
1
  import type { OutputOptions } from "../utils/output.js";
2
- export declare function exec(command: string, options: OutputOptions & {
2
+ export declare function exec(_command: string, _options: OutputOptions & {
3
3
  session?: string;
4
4
  }): Promise<void>;
@@ -1,34 +1,7 @@
1
- import { stagePost } from "../client.js";
2
- import { error, output } from "../utils/output.js";
1
+ import { error } from "../utils/output.js";
3
2
  import { EXIT_ERROR } from "../utils/exit-codes.js";
4
- export async function exec(command, options) {
5
- const sessionId = options.session;
6
- try {
7
- const result = await stagePost("/api/stage/exec", { command }, sessionId);
8
- output(options, {
9
- json: () => ({
10
- stdout: result.stdout,
11
- stderr: result.stderr,
12
- exitCode: result.exitCode,
13
- session: sessionId,
14
- }),
15
- quiet: () => {
16
- if (result.stdout)
17
- process.stdout.write(result.stdout);
18
- process.exit(result.exitCode);
19
- },
20
- human: () => {
21
- if (result.stdout)
22
- process.stdout.write(result.stdout);
23
- if (result.stderr)
24
- process.stderr.write(result.stderr);
25
- if (result.exitCode !== 0)
26
- process.exit(result.exitCode);
27
- },
28
- });
29
- }
30
- catch (e) {
31
- error(e.message);
32
- process.exit(EXIT_ERROR);
33
- }
3
+ export async function exec(_command, _options) {
4
+ error("exec command is not supported with Convex backend");
5
+ error("Use 'stage ls' to list files, or write/read files directly");
6
+ process.exit(EXIT_ERROR);
34
7
  }
@@ -1,25 +1,28 @@
1
- import { stagePost } from "../client.js";
1
+ import { getAllFiles } from "../convex-client.js";
2
2
  import { error, output } from "../utils/output.js";
3
3
  import { EXIT_ERROR } from "../utils/exit-codes.js";
4
4
  export async function ls(remotePath, options) {
5
5
  const sessionId = options.session;
6
6
  const dir = remotePath || "/app";
7
7
  try {
8
- const result = await stagePost("/api/stage/exec", { command: `find ${dir} -type f 2>/dev/null | sort` }, sessionId);
9
- const files = (result.stdout || "").trim();
10
- const list = files ? files.split("\n") : [];
8
+ const files = await getAllFiles(sessionId);
9
+ // Filter by directory if specified
10
+ const filtered = files
11
+ .filter(f => f.path.startsWith(dir))
12
+ .map(f => f.path)
13
+ .sort();
11
14
  output(options, {
12
- json: () => ({ path: dir, files: list, count: list.length, session: sessionId }),
15
+ json: () => ({ path: dir, files: filtered, count: filtered.length, session: sessionId }),
13
16
  quiet: () => {
14
- if (files)
15
- process.stdout.write(files + "\n");
17
+ if (filtered.length)
18
+ process.stdout.write(filtered.join("\n") + "\n");
16
19
  },
17
20
  human: () => {
18
- if (!list.length) {
21
+ if (!filtered.length) {
19
22
  console.log("(empty)");
20
23
  return;
21
24
  }
22
- for (const f of list) {
25
+ for (const f of filtered) {
23
26
  console.log(f);
24
27
  }
25
28
  },
@@ -1,15 +1,34 @@
1
- import { getBaseUrl, stagePost } from "../client.js";
1
+ import { createSession, getConvexConfig } from "../convex-client.js";
2
2
  import { error, output, success, hint } from "../utils/output.js";
3
3
  import { EXIT_ERROR } from "../utils/exit-codes.js";
4
4
  export async function newSession(options) {
5
5
  try {
6
- const result = await stagePost("/api/stage/sessions", {});
7
- const url = `${getBaseUrl()}/s/${result.id}`;
6
+ const sessionId = await createSession();
7
+ const config = getConvexConfig();
8
+ // Determine Stage UI URL
9
+ // For cloud: derive from Convex URL or use STAGE_URL
10
+ // For self-hosted: use STAGE_URL or default localhost
11
+ let stageUrl = process.env.STAGE_URL;
12
+ if (!stageUrl) {
13
+ if (config.isSelfHosted) {
14
+ stageUrl = "http://localhost:3000";
15
+ }
16
+ else {
17
+ // For cloud, try to derive from Convex URL or use a default
18
+ stageUrl = process.env.NEXT_PUBLIC_STAGE_URL || "http://localhost:3000";
19
+ }
20
+ }
21
+ const url = `${stageUrl}/s/${sessionId}`;
8
22
  output(options, {
9
- json: () => ({ id: result.id, url }),
10
- quiet: () => process.stdout.write(result.id),
23
+ json: () => ({
24
+ id: sessionId,
25
+ url,
26
+ convexUrl: config.url,
27
+ mode: config.isSelfHosted ? "self-hosted" : "cloud",
28
+ }),
29
+ quiet: () => process.stdout.write(sessionId),
11
30
  human: () => {
12
- success(`Session created: ${result.id}`);
31
+ success(`Session created: ${sessionId}`);
13
32
  hint(`URL: ${url}`);
14
33
  },
15
34
  });
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, readdirSync, statSync } from "node:fs";
2
2
  import { join, relative, posix } from "node:path";
3
- import { stagePost } from "../client.js";
3
+ import { writeFile, triggerRender } from "../convex-client.js";
4
4
  import { error, output, success, dim, bullet } from "../utils/output.js";
5
5
  import { EXIT_ERROR, EXIT_USER_ERROR } from "../utils/exit-codes.js";
6
6
  function walkDir(dir) {
@@ -47,14 +47,17 @@ export async function push(localDir, remoteDir, options) {
47
47
  process.exit(EXIT_USER_ERROR);
48
48
  }
49
49
  try {
50
- await stagePost("/api/stage/files", { files }, sessionId);
50
+ // Write all files to Convex
51
+ const paths = Object.keys(files);
52
+ for (const remotePath of paths) {
53
+ await writeFile(sessionId, remotePath, files[remotePath]);
54
+ }
51
55
  let version;
52
56
  if (options.render !== false) {
53
57
  const entry = options.entry || `${targetDir}/App.tsx`;
54
- const renderResult = await stagePost("/api/stage/render", { entry }, sessionId);
58
+ const renderResult = await triggerRender(sessionId, entry);
55
59
  version = renderResult.version;
56
60
  }
57
- const paths = Object.keys(files);
58
61
  output(options, {
59
62
  json: () => ({
60
63
  success: true,
@@ -1,16 +1,16 @@
1
- import { stageGet } from "../client.js";
1
+ import { readFile } from "../convex-client.js";
2
2
  import { error, output } from "../utils/output.js";
3
3
  import { EXIT_ERROR, EXIT_NOT_FOUND } from "../utils/exit-codes.js";
4
4
  export async function read(remotePath, options) {
5
5
  const sessionId = options.session;
6
6
  try {
7
- const result = await stageGet(`/api/stage/files?path=${encodeURIComponent(remotePath)}`, sessionId);
8
- if (result.error) {
9
- error(result.error);
7
+ const result = await readFile(sessionId, remotePath);
8
+ if (!result) {
9
+ error(`File not found: ${remotePath}`);
10
10
  process.exit(EXIT_NOT_FOUND);
11
11
  }
12
12
  output(options, {
13
- json: () => ({ path: remotePath, content: result.content, session: sessionId }),
13
+ json: () => ({ path: remotePath, content: result.content, version: result.version, session: sessionId }),
14
14
  quiet: () => process.stdout.write(result.content),
15
15
  human: () => process.stdout.write(result.content),
16
16
  });
@@ -1,20 +1,20 @@
1
- import { stagePost } from "../client.js";
1
+ import { triggerRender } from "../convex-client.js";
2
2
  import { error, output, success, dim } from "../utils/output.js";
3
3
  import { EXIT_ERROR } from "../utils/exit-codes.js";
4
4
  export async function render(entry, options) {
5
5
  const sessionId = options.session;
6
6
  const entryPoint = entry || "/app/App.tsx";
7
7
  try {
8
- const result = await stagePost("/api/stage/render", { entry: entryPoint }, sessionId);
8
+ const result = await triggerRender(sessionId, entryPoint);
9
9
  output(options, {
10
10
  json: () => ({
11
11
  success: true,
12
- entry: entryPoint,
12
+ entry: result.entry,
13
13
  version: result.version,
14
14
  session: sessionId,
15
15
  }),
16
16
  quiet: () => { },
17
- human: () => success(`Rendered ${entryPoint} ${dim(`(v${result.version})`)}`),
17
+ human: () => success(`Rendered ${result.entry} ${dim(`(v${result.version})`)}`),
18
18
  });
19
19
  }
20
20
  catch (e) {
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { stagePost } from "../client.js";
2
+ import { writeFile } from "../convex-client.js";
3
3
  import { error, output, success } from "../utils/output.js";
4
4
  import { EXIT_ERROR, EXIT_USER_ERROR } from "../utils/exit-codes.js";
5
5
  export async function write(remotePath, localPath, options) {
@@ -26,11 +26,11 @@ export async function write(remotePath, localPath, options) {
26
26
  process.exit(EXIT_USER_ERROR);
27
27
  }
28
28
  try {
29
- await stagePost("/api/stage/files", { files: { [remotePath]: content } }, sessionId);
29
+ const result = await writeFile(sessionId, remotePath, content);
30
30
  output(options, {
31
- json: () => ({ success: true, path: remotePath, bytes: content.length, session: sessionId }),
31
+ json: () => ({ success: true, path: remotePath, bytes: content.length, version: result.version, session: sessionId }),
32
32
  quiet: () => { },
33
- human: () => success(`${remotePath} (${content.length} bytes)`),
33
+ human: () => success(`${remotePath} (${content.length} bytes, v${result.version})`),
34
34
  });
35
35
  }
36
36
  catch (e) {
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Convex client for Stage CLI.
3
+ * Supports both Convex Cloud and self-hosted deployments.
4
+ *
5
+ * Environment variables:
6
+ * Convex Cloud:
7
+ * CONVEX_URL - Deployment URL (e.g., https://xxx.convex.cloud)
8
+ *
9
+ * Self-hosted:
10
+ * CONVEX_SELF_HOSTED_URL - Backend URL (e.g., http://127.0.0.1:3210)
11
+ * CONVEX_SELF_HOSTED_ADMIN_KEY - Admin key for auth
12
+ */
13
+ interface ConvexConfig {
14
+ url: string;
15
+ adminKey?: string;
16
+ isSelfHosted: boolean;
17
+ }
18
+ export declare function setConvexConfig(config: Partial<ConvexConfig>): void;
19
+ export declare function getConvexConfig(): ConvexConfig;
20
+ export declare function getConvexUrl(): string;
21
+ export declare function setConvexUrl(url: string): void;
22
+ export declare function createSession(): Promise<string>;
23
+ export declare function writeFile(sessionId: string, path: string, content: string): Promise<{
24
+ path: string;
25
+ version: number;
26
+ size: number;
27
+ }>;
28
+ export declare function readFile(sessionId: string, path: string): Promise<{
29
+ path: string;
30
+ content: string;
31
+ version?: number;
32
+ } | null>;
33
+ export declare function getAllFiles(sessionId: string): Promise<Array<{
34
+ path: string;
35
+ content: string;
36
+ version?: number;
37
+ }>>;
38
+ export declare function triggerRender(sessionId: string, entry?: string): Promise<{
39
+ entry: string;
40
+ version: number;
41
+ }>;
42
+ export declare function getRenderState(sessionId: string): Promise<{
43
+ entry: string;
44
+ version: number;
45
+ } | null>;
46
+ export declare function createSnapshot(sessionId: string, name?: string): Promise<string>;
47
+ export declare function getSnapshots(sessionId: string): Promise<Array<{
48
+ name?: string;
49
+ createdAt: number;
50
+ }>>;
51
+ export declare function getFileHistory(sessionId: string, path: string): Promise<Array<{
52
+ version?: number;
53
+ createdAt?: number;
54
+ }>>;
55
+ export declare function printConfig(): void;
56
+ export {};
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Convex client for Stage CLI.
3
+ * Supports both Convex Cloud and self-hosted deployments.
4
+ *
5
+ * Environment variables:
6
+ * Convex Cloud:
7
+ * CONVEX_URL - Deployment URL (e.g., https://xxx.convex.cloud)
8
+ *
9
+ * Self-hosted:
10
+ * CONVEX_SELF_HOSTED_URL - Backend URL (e.g., http://127.0.0.1:3210)
11
+ * CONVEX_SELF_HOSTED_ADMIN_KEY - Admin key for auth
12
+ */
13
+ let _configOverride = null;
14
+ export function setConvexConfig(config) {
15
+ _configOverride = config;
16
+ }
17
+ export function getConvexConfig() {
18
+ // Check for overrides first
19
+ if (_configOverride?.url) {
20
+ return {
21
+ url: _configOverride.url,
22
+ adminKey: _configOverride.adminKey,
23
+ isSelfHosted: _configOverride.isSelfHosted ?? true,
24
+ };
25
+ }
26
+ // Check for Convex Cloud
27
+ const cloudUrl = process.env.CONVEX_URL;
28
+ if (cloudUrl) {
29
+ return {
30
+ url: cloudUrl,
31
+ isSelfHosted: false,
32
+ };
33
+ }
34
+ // Check for self-hosted
35
+ const selfHostedUrl = process.env.CONVEX_SELF_HOSTED_URL;
36
+ const selfHostedKey = process.env.CONVEX_SELF_HOSTED_ADMIN_KEY;
37
+ if (selfHostedUrl) {
38
+ return {
39
+ url: selfHostedUrl,
40
+ adminKey: selfHostedKey,
41
+ isSelfHosted: true,
42
+ };
43
+ }
44
+ // Default to local self-hosted
45
+ return {
46
+ url: "http://127.0.0.1:3210",
47
+ isSelfHosted: true,
48
+ };
49
+ }
50
+ export function getConvexUrl() {
51
+ return getConvexConfig().url;
52
+ }
53
+ // For backwards compatibility
54
+ export function setConvexUrl(url) {
55
+ setConvexConfig({ url, isSelfHosted: true });
56
+ }
57
+ // Generic mutation/query helpers
58
+ async function mutation(path, args) {
59
+ const config = getConvexConfig();
60
+ const url = `${config.url}/api/mutation`;
61
+ const headers = {
62
+ "Content-Type": "application/json"
63
+ };
64
+ // Add admin key for self-hosted
65
+ if (config.isSelfHosted && config.adminKey) {
66
+ headers["Authorization"] = `Convex ${config.adminKey}`;
67
+ }
68
+ const res = await fetch(url, {
69
+ method: "POST",
70
+ headers,
71
+ body: JSON.stringify({ path: `stage:${path}`, args }),
72
+ });
73
+ if (!res.ok) {
74
+ const text = await res.text();
75
+ throw new Error(`Convex error ${res.status}: ${text}`);
76
+ }
77
+ const data = await res.json();
78
+ if (data.status === "error") {
79
+ throw new Error(data.errorMessage || "Unknown error");
80
+ }
81
+ return data.value;
82
+ }
83
+ async function query(path, args) {
84
+ const config = getConvexConfig();
85
+ const url = `${config.url}/api/query`;
86
+ const headers = {
87
+ "Content-Type": "application/json"
88
+ };
89
+ // Add admin key for self-hosted
90
+ if (config.isSelfHosted && config.adminKey) {
91
+ headers["Authorization"] = `Convex ${config.adminKey}`;
92
+ }
93
+ const res = await fetch(url, {
94
+ method: "POST",
95
+ headers,
96
+ body: JSON.stringify({ path: `stage:${path}`, args }),
97
+ });
98
+ if (!res.ok) {
99
+ const text = await res.text();
100
+ throw new Error(`Convex error ${res.status}: ${text}`);
101
+ }
102
+ const data = await res.json();
103
+ if (data.status === "error") {
104
+ throw new Error(data.errorMessage || "Unknown error");
105
+ }
106
+ return data.value;
107
+ }
108
+ // Stage API
109
+ export async function createSession() {
110
+ return await mutation("createSession", {});
111
+ }
112
+ export async function writeFile(sessionId, path, content) {
113
+ return await mutation("writeFile", { sessionId, path, content });
114
+ }
115
+ export async function readFile(sessionId, path) {
116
+ return await query("readFile", { sessionId, path });
117
+ }
118
+ export async function getAllFiles(sessionId) {
119
+ return await query("getAllFiles", { sessionId });
120
+ }
121
+ export async function triggerRender(sessionId, entry) {
122
+ return await mutation("triggerRender", { sessionId, entry });
123
+ }
124
+ export async function getRenderState(sessionId) {
125
+ return await query("getRenderState", { sessionId });
126
+ }
127
+ export async function createSnapshot(sessionId, name) {
128
+ return await mutation("createSnapshot", { sessionId, name });
129
+ }
130
+ export async function getSnapshots(sessionId) {
131
+ return await query("getSnapshots", { sessionId });
132
+ }
133
+ export async function getFileHistory(sessionId, path) {
134
+ return await query("getFileHistory", { sessionId, path });
135
+ }
136
+ // Helper to show current config
137
+ export function printConfig() {
138
+ const config = getConvexConfig();
139
+ console.log(`Mode: ${config.isSelfHosted ? "self-hosted" : "cloud"}`);
140
+ console.log(`URL: ${config.url}`);
141
+ if (config.isSelfHosted && config.adminKey) {
142
+ console.log(`Admin key: ${config.adminKey.slice(0, 20)}...`);
143
+ }
144
+ }
package/dist/main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
3
  import { Command } from "commander";
4
- import { setBaseUrl } from "./client.js";
4
+ import { setConvexConfig, printConfig } from "./convex-client.js";
5
5
  import { newSession } from "./commands/new.js";
6
6
  import { write } from "./commands/write.js";
7
7
  import { read } from "./commands/read.js";
@@ -15,11 +15,25 @@ const { version } = require("../package.json");
15
15
  const program = new Command();
16
16
  program
17
17
  .name("stage")
18
- .description("CLI client for Stage — a sandboxed React runtime for AI agents")
18
+ .description(`CLI client for Stage — a sandboxed React runtime for AI agents
19
+
20
+ Environment variables:
21
+ CONVEX_URL Convex Cloud deployment URL
22
+ CONVEX_SELF_HOSTED_URL Self-hosted Convex URL
23
+ CONVEX_SELF_HOSTED_ADMIN_KEY Self-hosted admin key
24
+ STAGE_URL Stage frontend URL (for session links)`)
19
25
  .version(`stage ${version}`, "-v, --version")
20
26
  .option("--json", "Output as JSON")
21
27
  .option("-q, --quiet", "Suppress output")
22
- .option("-u, --url <url>", "Stage server URL (default: $STAGE_URL or http://localhost:3000)");
28
+ .option("-u, --url <url>", "Convex URL (overrides env vars)")
29
+ .option("--self-hosted", "Force self-hosted mode")
30
+ .option("--cloud", "Force cloud mode");
31
+ program
32
+ .command("config")
33
+ .description("Show current Convex configuration")
34
+ .action(() => {
35
+ printConfig();
36
+ });
23
37
  program
24
38
  .command("new")
25
39
  .description("Create a new session")
@@ -45,7 +59,7 @@ program
45
59
  });
46
60
  program
47
61
  .command("exec <command>")
48
- .description("Run a bash command in Stage's virtual FS")
62
+ .description("[DEPRECATED] Not supported with Convex backend")
49
63
  .requiredOption("-s, --session <id>", "Session ID")
50
64
  .action(async (command, opts, cmd) => {
51
65
  const root = cmd.optsWithGlobals();
@@ -61,7 +75,7 @@ program
61
75
  });
62
76
  program
63
77
  .command("ls [path]")
64
- .description("List files in Stage's virtual FS")
78
+ .description("List files in session")
65
79
  .requiredOption("-s, --session <id>", "Session ID")
66
80
  .action(async (path, opts, cmd) => {
67
81
  const root = cmd.optsWithGlobals();
@@ -92,8 +106,21 @@ program
92
106
  });
93
107
  program.hook("preAction", (_thisCommand, actionCommand) => {
94
108
  const root = actionCommand.optsWithGlobals();
95
- if (root.url)
96
- setBaseUrl(root.url);
109
+ // Configure Convex client based on flags
110
+ if (root.url) {
111
+ const isSelfHosted = root.selfHosted || (!root.cloud && !root.url.includes("convex.cloud"));
112
+ setConvexConfig({
113
+ url: root.url,
114
+ isSelfHosted,
115
+ adminKey: process.env.CONVEX_SELF_HOSTED_ADMIN_KEY,
116
+ });
117
+ }
118
+ else if (root.cloud) {
119
+ setConvexConfig({ isSelfHosted: false });
120
+ }
121
+ else if (root.selfHosted) {
122
+ setConvexConfig({ isSelfHosted: true });
123
+ }
97
124
  });
98
125
  program.parseAsync(process.argv).catch((err) => {
99
126
  console.error("Fatal error:", err.message);
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "stage-ai",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "CLI client for Stage — a sandboxed React runtime for AI-generated applications",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
7
7
  "bin": {
8
8
  "stage": "./dist/main.js"
9
9
  },
10
- "files": ["dist"],
10
+ "files": [
11
+ "dist"
12
+ ],
11
13
  "scripts": {
12
14
  "build": "tsc",
13
15
  "prepublishOnly": "npm run build",
@@ -29,14 +31,24 @@
29
31
  "bugs": {
30
32
  "url": "https://github.com/the-shift-dev/stage-cli/issues"
31
33
  },
32
- "keywords": ["ai", "agent", "sandbox", "react", "runtime", "cli", "stage"],
34
+ "keywords": [
35
+ "ai",
36
+ "agent",
37
+ "sandbox",
38
+ "react",
39
+ "runtime",
40
+ "cli",
41
+ "stage"
42
+ ],
33
43
  "license": "MIT",
34
44
  "devDependencies": {
35
45
  "@biomejs/biome": "^2.3.14",
46
+ "@types/node": "^25.3.3",
36
47
  "typescript": "^5.8.0"
37
48
  },
38
49
  "dependencies": {
39
50
  "chalk": "^5.6.2",
40
- "commander": "^14.0.3"
51
+ "commander": "^14.0.3",
52
+ "convex": "^1.32.0"
41
53
  }
42
54
  }