cassian-cli 0.1.7 → 0.1.9
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/login.js +30 -163
- package/dist/commands/ssh.js +6 -0
- package/dist/commands/up.js +4 -32
- package/dist/lib/constants.d.ts +1 -1
- package/dist/lib/constants.js +1 -1
- package/package.json +1 -1
package/dist/commands/login.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
18
|
-
console.log(
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
}
|
package/dist/commands/ssh.js
CHANGED
|
@@ -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) {
|
package/dist/commands/up.js
CHANGED
|
@@ -82,14 +82,12 @@ export async function up() {
|
|
|
82
82
|
fatal("Could not connect to Cassian.", "Check your internet connection and try again. If this keeps happening, reach out at trycassian.com.");
|
|
83
83
|
}
|
|
84
84
|
connectSpinner.succeed("Connected");
|
|
85
|
-
// SSH key —
|
|
86
|
-
let sshPubKey;
|
|
85
|
+
// SSH key — optional, used only for direct SSH fallback
|
|
86
|
+
let sshPubKey = "";
|
|
87
87
|
try {
|
|
88
88
|
sshPubKey = readFileSync(findSshPublicKey(dev.ssh_public_key), "utf-8").trim();
|
|
89
89
|
}
|
|
90
|
-
catch {
|
|
91
|
-
fatal("No SSH key found.", "Generate one with: ssh-keygen -t ed25519");
|
|
92
|
-
}
|
|
90
|
+
catch { /* no key is fine — websocket terminal doesn't need it */ }
|
|
93
91
|
const spinner = ora("Starting instance...").start();
|
|
94
92
|
try {
|
|
95
93
|
const client = new ApiClient();
|
|
@@ -132,33 +130,7 @@ export async function up() {
|
|
|
132
130
|
},
|
|
133
131
|
});
|
|
134
132
|
spinner.succeed("Instance ready");
|
|
135
|
-
//
|
|
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
|
-
}
|
|
133
|
+
// Sync happens during ssh/exec, not during up
|
|
162
134
|
// Setup command
|
|
163
135
|
if (config.workspace?.setup) {
|
|
164
136
|
const setupSpinner = ora("Running setup...").start();
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -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
|
|
4
|
+
export declare const PLATFORM_URL: string;
|
package/dist/lib/constants.js
CHANGED
|
@@ -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";
|