cassian-cli 0.1.6 → 0.1.8

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,3 +1,4 @@
1
1
  export declare function exec(args: string[], opts?: {
2
2
  timeout?: string;
3
+ noSync?: boolean;
3
4
  }): Promise<void>;
@@ -1,5 +1,6 @@
1
1
  import { ApiClient } from "../lib/api.js";
2
2
  import { loadConfig } from "../lib/config.js";
3
+ import { pushWorkspace } from "../lib/push.js";
3
4
  import { fatal, handleError } from "../lib/errors.js";
4
5
  export async function exec(args, opts = {}) {
5
6
  if (args.length === 0) {
@@ -21,9 +22,17 @@ export async function exec(args, opts = {}) {
21
22
  if (!instance) {
22
23
  fatal(`${config.name} is not running.`, "Run: cassian up");
23
24
  }
25
+ if (!opts.noSync) {
26
+ try {
27
+ await pushWorkspace(client, instance.id, config);
28
+ }
29
+ catch { /* sync best-effort — don't block exec */ }
30
+ }
31
+ const mountPath = config.volumes?.[0]?.mount || "/workspace";
24
32
  const result = await client.post(`/v1/instances/${instance.id}/exec`, {
25
33
  command,
26
34
  timeout,
35
+ workdir: mountPath,
27
36
  });
28
37
  if (result.stdout)
29
38
  process.stdout.write(result.stdout);
@@ -1,90 +1,44 @@
1
- import { createServer } from "http";
1
+ import { createInterface } from "readline";
2
2
  import open from "open";
3
3
  import { saveCredentials } from "../lib/auth.js";
4
4
  import { success, dim } from "../lib/output.js";
5
5
  import { fatal } from "../lib/errors.js";
6
6
  import { PLATFORM_URL } from "../lib/constants.js";
7
- const CALLBACK_PORT = 9876;
8
7
  export async function login() {
9
- const callbackUrl = `http://localhost:${CALLBACK_PORT}/callback`;
10
- const platformUrl = PLATFORM_URL;
8
+ const authUrl = `${PLATFORM_URL}/cli`;
11
9
  console.log();
12
- const authUrl = `${platformUrl}?cli_redirect=${encodeURIComponent(callbackUrl)}`;
13
- // Derive the allowed CORS origin from platformUrl (e.g. https://trycassian.com)
14
- const allowedOrigin = new URL(platformUrl).origin;
15
- return new Promise((resolve, reject) => {
16
- const server = createServer((req, res) => {
17
- const origin = req.headers["origin"] || "";
18
- // Only accept requests from the platform — block anything else on this port
19
- if (origin && origin !== allowedOrigin) {
20
- res.writeHead(403);
21
- res.end();
22
- return;
23
- }
24
- res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
25
- res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
26
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
27
- if (req.method === "OPTIONS") {
28
- res.writeHead(204);
29
- res.end();
30
- return;
31
- }
32
- const url = new URL(req.url || "/", `http://localhost:${CALLBACK_PORT}`);
33
- // Platform posts tokens directly to /callback?access_token=...&refresh_token=...
34
- if (url.pathname === "/callback" && req.method === "POST") {
35
- const params = url.searchParams;
36
- const accessToken = params.get("access_token");
37
- const refreshToken = params.get("refresh_token");
38
- const expiresIn = parseInt(params.get("expires_in") || "3600");
39
- if (!accessToken || !refreshToken) {
40
- res.writeHead(400);
41
- res.end("Missing tokens");
42
- return;
43
- }
44
- // Decode JWT to get email
45
- const payload = JSON.parse(Buffer.from(accessToken.split(".")[1], "base64url").toString());
46
- const email = payload.email || "unknown";
47
- const creds = {
48
- access_token: accessToken,
49
- refresh_token: refreshToken,
50
- expires_at: Math.floor(Date.now() / 1000) + expiresIn,
51
- user_email: email,
52
- };
53
- saveCredentials(creds);
54
- res.writeHead(200);
55
- res.end("ok");
56
- console.log();
57
- success(`Logged in as ${email}`);
58
- console.log();
59
- server.close();
60
- resolve();
61
- return;
62
- }
63
- res.writeHead(404);
64
- res.end();
65
- });
66
- server.listen(CALLBACK_PORT, "127.0.0.1", () => {
67
- console.log(dim(" Login URL (open in any browser):"));
68
- console.log(` ${authUrl}`);
69
- console.log();
70
- console.log(dim(" Press Enter to open in browser..."));
71
- if (process.stdin.isTTY) {
72
- process.stdin.setRawMode(true);
73
- process.stdin.resume();
74
- process.stdin.once("data", () => {
75
- process.stdin.setRawMode(false);
76
- process.stdin.pause();
77
- open(authUrl).catch(() => { });
78
- });
79
- }
80
- });
81
- server.on("error", () => {
82
- fatal("Could not start login. Try again.");
10
+ open(authUrl).catch(() => { });
11
+ console.log(dim(" Opening browser..."));
12
+ console.log(dim(` If it didn't open, visit: ${authUrl}`));
13
+ console.log();
14
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
15
+ const code = await new Promise((resolve) => {
16
+ rl.question(" Enter code: ", (answer) => {
17
+ rl.close();
18
+ resolve(answer.trim());
83
19
  });
84
- const timeout = setTimeout(() => {
85
- server.close();
86
- fatal("Login timed out.", "Run: cassian login");
87
- }, 120000);
88
- server.on("close", () => clearTimeout(timeout));
89
20
  });
21
+ if (!code || !/^\d{6}$/.test(code)) {
22
+ fatal("Invalid code.", "Enter the 6-digit code shown in your browser.");
23
+ }
24
+ // Exchange code for tokens
25
+ const { AGENT_URL } = await import("../lib/constants.js");
26
+ const resp = await fetch(`${AGENT_URL}/v1/cli/auth?code=${code}`);
27
+ if (!resp.ok) {
28
+ const body = await resp.json().catch(() => ({}));
29
+ const msg = body.error || "Code exchange failed";
30
+ fatal(msg, "Run: cassian login");
31
+ }
32
+ const { access_token, refresh_token } = await resp.json();
33
+ const payload = JSON.parse(Buffer.from(access_token.split(".")[1], "base64url").toString());
34
+ const email = payload.email || "unknown";
35
+ saveCredentials({
36
+ access_token,
37
+ refresh_token,
38
+ expires_at: payload.exp || Math.floor(Date.now() / 1000) + 3600,
39
+ user_email: email,
40
+ });
41
+ console.log();
42
+ success(`Logged in as ${email}`);
43
+ console.log();
90
44
  }
@@ -3,6 +3,7 @@ import { ApiClient } from "../lib/api.js";
3
3
  import { getCredentials, isTokenExpired, refreshToken } from "../lib/auth.js";
4
4
  import { AGENT_URL } from "../lib/constants.js";
5
5
  import { loadConfig } from "../lib/config.js";
6
+ import { pushWorkspace } from "../lib/push.js";
6
7
  import { dim } from "../lib/output.js";
7
8
  import { fatal, handleError } from "../lib/errors.js";
8
9
  import { startBidirectionalSync } from "../lib/watcher.js";
@@ -21,6 +22,11 @@ export async function ssh() {
21
22
  if (!instance) {
22
23
  fatal(`${config.name} is not running.`, "Run: cassian up");
23
24
  }
25
+ // Push local files before connecting
26
+ try {
27
+ await pushWorkspace(client, instance.id, config);
28
+ }
29
+ catch { /* best effort */ }
24
30
  const stopSync = startBidirectionalSync(instance.id, config, AGENT_URL);
25
31
  const connected = await connectWebSocket(instance, stopSync);
26
32
  if (!connected) {
@@ -132,33 +132,7 @@ export async function up() {
132
132
  },
133
133
  });
134
134
  spinner.succeed("Instance ready");
135
- // Push workspace (incremental tar via agent API)
136
- const mountPath = config.volumes?.[0]?.mount || "/workspace";
137
- const wsSpinner = ora("Syncing files...").start();
138
- try {
139
- const tar = await import("tar");
140
- const syncignore = config.workspace?.syncignore ?? [];
141
- const defaultIgnore = ["**/.git/**", "**/node_modules/**", "**/__pycache__/**", "**/*.pyc", "**/.DS_Store", ...syncignore];
142
- const ignoreRegexes = defaultIgnore.map((p) => {
143
- const esc = p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\x00").replace(/\*/g, "[^/]*").replace(/\x00/g, ".*");
144
- return new RegExp(`(^|/)${esc}($|/)`);
145
- });
146
- const chunks = [];
147
- await new Promise((res, rej) => {
148
- const pack = tar.create({ cwd: process.cwd(), filter: (p) => {
149
- const rel = p.replace(/^\.\//, "");
150
- return !ignoreRegexes.some((re) => re.test(rel));
151
- } }, ["."]);
152
- pack.on("data", (c) => chunks.push(c));
153
- pack.on("end", res);
154
- pack.on("error", rej);
155
- });
156
- await client.push(instance.id, Buffer.concat(chunks), mountPath);
157
- wsSpinner.succeed("Files synced");
158
- }
159
- catch {
160
- wsSpinner.warn("File sync skipped — you can sync manually once connected");
161
- }
135
+ // Sync happens during ssh/exec, not during up
162
136
  // Setup command
163
137
  if (config.workspace?.setup) {
164
138
  const setupSpinner = ora("Running setup...").start();
package/dist/index.js CHANGED
@@ -42,9 +42,10 @@ program
42
42
  .action(ssh);
43
43
  program
44
44
  .command("exec <command...>")
45
- .description("Run a command on the instance and return output")
45
+ .description("Sync files and run a command on the instance")
46
46
  .option("--timeout <seconds>", "Command timeout in seconds", "600")
47
- .action((command, opts) => exec(command, opts));
47
+ .option("--no-sync", "Skip file sync before executing")
48
+ .action((command, opts) => exec(command, { timeout: opts.timeout, noSync: !opts.sync }));
48
49
  program
49
50
  .command("sync")
50
51
  .description("Sync workspace from instance to local")
@@ -1,4 +1,4 @@
1
1
  export declare const SUPABASE_URL = "https://ykgwmdvzzburglrkjutc.supabase.co";
2
2
  export declare const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlrZ3dtZHZ6emJ1cmdscmtqdXRjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI0MzI0MzUsImV4cCI6MjA4ODAwODQzNX0.c-MSPTYr9_G77sy_2hAcY7hth8mSKmNQDzfTjIx8rE0";
3
3
  export declare const AGENT_URL = "https://gpu.platops.ai";
4
- export declare const PLATFORM_URL = "https://platform.trycassian.com";
4
+ export declare const PLATFORM_URL: string;
@@ -1,4 +1,4 @@
1
1
  export const SUPABASE_URL = "https://ykgwmdvzzburglrkjutc.supabase.co";
2
2
  export const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlrZ3dtZHZ6emJ1cmdscmtqdXRjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI0MzI0MzUsImV4cCI6MjA4ODAwODQzNX0.c-MSPTYr9_G77sy_2hAcY7hth8mSKmNQDzfTjIx8rE0";
3
3
  export const AGENT_URL = "https://gpu.platops.ai";
4
- export const PLATFORM_URL = "https://platform.trycassian.com";
4
+ export const PLATFORM_URL = process.env.CASSIAN_PLATFORM_URL || "https://platform.trycassian.com";
@@ -0,0 +1,3 @@
1
+ import { ApiClient } from "./api.js";
2
+ import type { CassianConfig } from "../types.js";
3
+ export declare function pushWorkspace(client: ApiClient, instanceId: string, config: CassianConfig): Promise<void>;
@@ -0,0 +1,21 @@
1
+ export async function pushWorkspace(client, instanceId, config) {
2
+ const tar = await import("tar");
3
+ const mountPath = config.volumes?.[0]?.mount || "/workspace";
4
+ const syncignore = config.workspace?.syncignore ?? [];
5
+ const defaultIgnore = ["**/.git/**", "**/node_modules/**", "**/__pycache__/**", "**/*.pyc", "**/.DS_Store", ...syncignore];
6
+ const ignoreRegexes = defaultIgnore.map((p) => {
7
+ const esc = p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\x00").replace(/\*/g, "[^/]*").replace(/\x00/g, ".*");
8
+ return new RegExp(`(^|/)${esc}($|/)`);
9
+ });
10
+ const chunks = [];
11
+ await new Promise((res, rej) => {
12
+ const pack = tar.create({ cwd: process.cwd(), filter: (p) => {
13
+ const rel = p.replace(/^\.\//, "");
14
+ return !ignoreRegexes.some((re) => re.test(rel));
15
+ } }, ["."]);
16
+ pack.on("data", (c) => chunks.push(c));
17
+ pack.on("end", res);
18
+ pack.on("error", rej);
19
+ });
20
+ await client.push(instanceId, Buffer.concat(chunks), mountPath);
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cassian-cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "The Cassian GPU cloud CLI — provision GPUs, sync files, and run workloads from your terminal.",
5
5
  "type": "module",
6
6
  "bin": {