cassian-cli 0.1.7 → 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,177 +1,44 @@
1
- import { createServer } from "http";
2
1
  import { createInterface } from "readline";
3
2
  import open from "open";
4
3
  import { saveCredentials } from "../lib/auth.js";
5
4
  import { success, dim } from "../lib/output.js";
6
5
  import { fatal } from "../lib/errors.js";
7
6
  import { PLATFORM_URL } from "../lib/constants.js";
8
- const PORT_RANGE = [9876, 9877, 9878, 9879, 9880];
9
7
  export async function login() {
10
- const port = await findOpenPort();
11
- const callbackUrl = port ? `http://localhost:${port}/callback` : null;
12
- const authUrl = callbackUrl
13
- ? `${PLATFORM_URL}?cli_redirect=${encodeURIComponent(callbackUrl)}`
14
- : `${PLATFORM_URL}?cli_mode=token`;
15
- const allowedOrigin = new URL(PLATFORM_URL).origin;
8
+ const authUrl = `${PLATFORM_URL}/cli`;
16
9
  console.log();
17
- console.log(dim(" Login URL:"));
18
- console.log(` ${authUrl}`);
10
+ open(authUrl).catch(() => { });
11
+ console.log(dim(" Opening browser..."));
12
+ console.log(dim(` If it didn't open, visit: ${authUrl}`));
19
13
  console.log();
20
- // If we got a port, try the automatic callback flow
21
- if (port && callbackUrl) {
22
- console.log(dim(" Press Enter to open browser. Or paste the token from the page below."));
23
- console.log();
24
- return new Promise((resolve) => {
25
- let done = false;
26
- const server = createServer((req, res) => {
27
- const origin = req.headers["origin"] || "";
28
- if (origin && origin !== allowedOrigin) {
29
- res.writeHead(403);
30
- res.end();
31
- return;
32
- }
33
- res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
34
- res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
35
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
36
- if (req.method === "OPTIONS") {
37
- res.writeHead(204);
38
- res.end();
39
- return;
40
- }
41
- const url = new URL(req.url || "/", `http://localhost:${port}`);
42
- if (url.pathname === "/callback" && req.method === "POST") {
43
- const accessToken = url.searchParams.get("access_token");
44
- const refreshToken = url.searchParams.get("refresh_token");
45
- const expiresIn = parseInt(url.searchParams.get("expires_in") || "3600");
46
- if (!accessToken || !refreshToken) {
47
- res.writeHead(400);
48
- res.end();
49
- return;
50
- }
51
- finishLogin(accessToken, refreshToken, expiresIn);
52
- res.writeHead(200);
53
- res.end("ok");
54
- return;
55
- }
56
- res.writeHead(404);
57
- res.end();
58
- });
59
- // Also accept pasted token from stdin
60
- const rl = createInterface({ input: process.stdin, output: process.stdout });
61
- rl.on("line", (line) => {
62
- const trimmed = line.trim();
63
- if (!trimmed) {
64
- // Enter with no input = open browser
65
- open(authUrl).catch(() => { });
66
- return;
67
- }
68
- // User pasted a token
69
- if (handlePastedToken(trimmed)) {
70
- rl.close();
71
- server.close();
72
- }
73
- });
74
- server.listen(port, "127.0.0.1");
75
- server.on("error", () => {
76
- // Port taken — fall back to paste-only
77
- console.log(dim(" Could not bind port. Paste your token below:"));
78
- });
79
- const timeout = setTimeout(() => {
80
- if (!done) {
81
- server.close();
82
- rl.close();
83
- fatal("Login timed out.", "Run: cassian login");
84
- }
85
- }, 300000);
86
- server.on("close", () => { if (!done)
87
- clearTimeout(timeout); });
88
- function finishLogin(accessToken, refreshToken, expiresIn) {
89
- if (done)
90
- return;
91
- done = true;
92
- clearTimeout(timeout);
93
- const payload = JSON.parse(Buffer.from(accessToken.split(".")[1], "base64url").toString());
94
- const email = payload.email || "unknown";
95
- saveCredentials({
96
- access_token: accessToken,
97
- refresh_token: refreshToken,
98
- expires_at: Math.floor(Date.now() / 1000) + expiresIn,
99
- user_email: email,
100
- });
101
- console.log();
102
- success(`Logged in as ${email}`);
103
- console.log();
104
- rl.close();
105
- server.close();
106
- resolve();
107
- }
108
- function handlePastedToken(token) {
109
- if (done)
110
- return false;
111
- try {
112
- // Format: access_token:::refresh_token
113
- let accessToken;
114
- let refreshToken;
115
- if (token.includes(":::")) {
116
- [accessToken, refreshToken] = token.split(":::");
117
- }
118
- else {
119
- fatal("Invalid token format.", "Copy the full token from the login page.");
120
- return false;
121
- }
122
- const parts = accessToken.split(".");
123
- if (parts.length !== 3)
124
- throw new Error("bad jwt");
125
- const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
126
- const expiresIn = (payload.exp || Math.floor(Date.now() / 1000) + 3600) - Math.floor(Date.now() / 1000);
127
- finishLogin(accessToken, refreshToken, expiresIn);
128
- return true;
129
- }
130
- catch {
131
- console.log(dim(" Invalid token. Try again or wait for browser callback."));
132
- return false;
133
- }
134
- }
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());
135
19
  });
20
+ });
21
+ if (!code || !/^\d{6}$/.test(code)) {
22
+ fatal("Invalid code.", "Enter the 6-digit code shown in your browser.");
136
23
  }
137
- else {
138
- // No port available paste only
139
- console.log(dim(" After logging in, copy the token from the page and paste it here:"));
140
- console.log();
141
- const rl = createInterface({ input: process.stdin, output: process.stdout });
142
- const token = await new Promise((res) => {
143
- rl.question(" Token: ", (answer) => { rl.close(); res(answer.trim()); });
144
- });
145
- if (!token || !token.includes(":::")) {
146
- fatal("Invalid token.", "Copy the full token from the login page.");
147
- }
148
- const [accessToken, refreshToken] = token.split(":::");
149
- const payload = JSON.parse(Buffer.from(accessToken.split(".")[1], "base64url").toString());
150
- const email = payload.email || "unknown";
151
- saveCredentials({
152
- access_token: accessToken,
153
- refresh_token: refreshToken,
154
- expires_at: payload.exp || Math.floor(Date.now() / 1000) + 3600,
155
- user_email: email,
156
- });
157
- console.log();
158
- success(`Logged in as ${email}`);
159
- console.log();
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");
160
31
  }
161
- }
162
- function findOpenPort() {
163
- return new Promise((resolve) => {
164
- let idx = 0;
165
- function tryNext() {
166
- if (idx >= PORT_RANGE.length) {
167
- resolve(null);
168
- return;
169
- }
170
- const port = PORT_RANGE[idx++];
171
- const srv = createServer();
172
- srv.listen(port, "127.0.0.1", () => { srv.close(() => resolve(port)); });
173
- srv.on("error", () => tryNext());
174
- }
175
- tryNext();
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,
176
40
  });
41
+ console.log();
42
+ success(`Logged in as ${email}`);
43
+ console.log();
177
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();
@@ -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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cassian-cli",
3
- "version": "0.1.7",
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": {