cassian-cli 0.1.2 → 0.1.4

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,24 +1,13 @@
1
1
  import { createServer } from "http";
2
- import { readFileSync, existsSync } from "fs";
3
- import { homedir } from "os";
4
- import { join } from "path";
5
2
  import open from "open";
6
3
  import { saveCredentials } from "../lib/auth.js";
7
4
  import { success, dim } from "../lib/output.js";
8
5
  import { fatal } from "../lib/errors.js";
9
- const CASSIAN_DIR = join(homedir(), ".cassian");
10
- const CONFIG_FILE = join(CASSIAN_DIR, "config.json");
6
+ import { PLATFORM_URL } from "../lib/constants.js";
11
7
  const CALLBACK_PORT = 9876;
12
- function getCliConfig() {
13
- if (!existsSync(CONFIG_FILE)) {
14
- fatal("Cassian CLI is not configured.", "Visit trycassian.com to get started.");
15
- }
16
- return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
17
- }
18
8
  export async function login() {
19
- const config = getCliConfig();
20
9
  const callbackUrl = `http://localhost:${CALLBACK_PORT}/callback`;
21
- const platformUrl = config.platform_url || "https://platform.trycassian.com";
10
+ const platformUrl = PLATFORM_URL;
22
11
  console.log();
23
12
  console.log(" Opening Cassian in your browser...");
24
13
  console.log();
@@ -62,7 +51,6 @@ export async function login() {
62
51
  refresh_token: refreshToken,
63
52
  expires_at: Math.floor(Date.now() / 1000) + expiresIn,
64
53
  user_email: email,
65
- agent_url: config.agent_url,
66
54
  };
67
55
  saveCredentials(creds);
68
56
  res.writeHead(200);
@@ -1,6 +1,7 @@
1
1
  import WebSocket from "ws";
2
2
  import { ApiClient } from "../lib/api.js";
3
3
  import { getCredentials, isTokenExpired, refreshToken } from "../lib/auth.js";
4
+ import { AGENT_URL } from "../lib/constants.js";
4
5
  import { loadConfig } from "../lib/config.js";
5
6
  import { dim } from "../lib/output.js";
6
7
  import { fatal, handleError } from "../lib/errors.js";
@@ -20,8 +21,7 @@ export async function ssh() {
20
21
  if (!instance) {
21
22
  fatal(`${config.name} is not running.`, "Run: cassian up");
22
23
  }
23
- const creds = getCredentials();
24
- const stopSync = startBidirectionalSync(instance.id, config, creds?.agent_url ?? "");
24
+ const stopSync = startBidirectionalSync(instance.id, config, AGENT_URL);
25
25
  const connected = await connectWebSocket(instance, stopSync);
26
26
  if (!connected) {
27
27
  stopSync();
@@ -39,26 +39,14 @@ async function getToken() {
39
39
  if (!creds)
40
40
  throw new Error("Not logged in. Run 'cassian login' first.");
41
41
  if (isTokenExpired(creds)) {
42
- const { readFileSync, existsSync } = await import("fs");
43
- const configPath = `${process.env.HOME}/.cassian/config.json`;
44
- let supabaseUrl = "";
45
- if (existsSync(configPath)) {
46
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
47
- supabaseUrl = config.supabase_url || "";
48
- }
49
- if (!supabaseUrl)
50
- throw new Error("Session expired. Run 'cassian login' again.");
51
- const refreshed = await refreshToken(creds, supabaseUrl);
42
+ const refreshed = await refreshToken(creds);
52
43
  return refreshed.access_token;
53
44
  }
54
45
  return creds.access_token;
55
46
  }
56
47
  async function connectWebSocket(instance, stopSync) {
57
- const creds = getCredentials();
58
- if (!creds)
59
- return false;
60
48
  const token = await getToken();
61
- const agentUrl = creds.agent_url.replace(/^http/, "ws");
49
+ const agentUrl = AGENT_URL.replace(/^http/, "ws");
62
50
  const wsUrl = `${agentUrl}/v1/instances/${instance.id}/terminal?token=${encodeURIComponent(token)}`;
63
51
  return new Promise((resolve) => {
64
52
  const ws = new WebSocket(wsUrl);
@@ -4,6 +4,7 @@ import { execSync } from "child_process";
4
4
  import ora from "ora";
5
5
  import { ApiClient } from "../lib/api.js";
6
6
  import { getCredentials } from "../lib/auth.js";
7
+ import { AGENT_URL } from "../lib/constants.js";
7
8
  import { loadConfig, loadDevOverrides, findSshPublicKey, parseSizeToGb } from "../lib/config.js";
8
9
  import { info, header, green } from "../lib/output.js";
9
10
  import { fatal, handleError } from "../lib/errors.js";
@@ -67,7 +68,7 @@ export async function up() {
67
68
  }
68
69
  // Verify connectivity — no technical details exposed
69
70
  const connectSpinner = ora("Connecting to Cassian...").start();
70
- const agentUrl = creds.agent_url;
71
+ const agentUrl = AGENT_URL;
71
72
  let reachable = false;
72
73
  for (let i = 0; i < 10; i++) {
73
74
  await new Promise((r) => setTimeout(r, 1000));
@@ -100,6 +101,20 @@ export async function up() {
100
101
  await new Promise((r) => setTimeout(r, 1000));
101
102
  spinner.text = "Starting instance...";
102
103
  }
104
+ // Validate GPU type against what the host offers before provisioning
105
+ if (config.gpu.type) {
106
+ const gpus = await client.get("/v1/gpus").catch(() => null);
107
+ if (gpus && gpus.gpus.length > 0) {
108
+ const available = gpus.gpus[0].name.toLowerCase().replace(/[\s_]/g, "-");
109
+ const requested = config.gpu.type.toLowerCase().replace(/[\s_]/g, "-");
110
+ // Check if the requested type is a substring match of what's available
111
+ if (!available.includes(requested) && !requested.includes(available.split("-").pop())) {
112
+ spinner.fail("GPU type not available");
113
+ const names = [...new Set(gpus.gpus.map((g) => g.name))];
114
+ fatal(`GPU type '${config.gpu.type}' is not available on this host.`, `Available: ${names.join(", ")} — update gpu.type in cassian.yaml`);
115
+ }
116
+ }
117
+ }
103
118
  const instance = await client.post("/v1/instances", {
104
119
  name: config.name,
105
120
  gpu_count: config.gpu.count,
package/dist/lib/api.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { getCredentials, isTokenExpired, refreshToken } from "./auth.js";
2
+ import { AGENT_URL } from "./constants.js";
2
3
  export class ApiClient {
3
4
  agentUrl;
4
5
  credentials;
@@ -7,20 +8,11 @@ export class ApiClient {
7
8
  if (!creds)
8
9
  throw new Error("Not logged in. Run 'cassian login' first.");
9
10
  this.credentials = creds;
10
- this.agentUrl = creds.agent_url;
11
+ this.agentUrl = AGENT_URL;
11
12
  }
12
13
  async getToken() {
13
14
  if (isTokenExpired(this.credentials)) {
14
- const configPath = `${process.env.HOME}/.cassian/config.json`;
15
- const { readFileSync, existsSync } = await import("fs");
16
- let supabaseUrl = "";
17
- if (existsSync(configPath)) {
18
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
19
- supabaseUrl = config.supabase_url || "";
20
- }
21
- if (!supabaseUrl)
22
- throw new Error("Session expired. Run 'cassian login' again.");
23
- this.credentials = await refreshToken(this.credentials, supabaseUrl);
15
+ this.credentials = await refreshToken(this.credentials);
24
16
  }
25
17
  return this.credentials.access_token;
26
18
  }
@@ -3,5 +3,5 @@ export declare function getCredentials(): Credentials | null;
3
3
  export declare function saveCredentials(creds: Credentials): void;
4
4
  export declare function clearCredentials(): void;
5
5
  export declare function isTokenExpired(creds: Credentials): boolean;
6
- export declare function refreshToken(creds: Credentials, supabaseUrl: string): Promise<Credentials>;
6
+ export declare function refreshToken(creds: Credentials): Promise<Credentials>;
7
7
  export declare function getAgentUrl(): string;
package/dist/lib/auth.js CHANGED
@@ -1,6 +1,7 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "fs";
2
2
  import { homedir } from "os";
3
3
  import { join } from "path";
4
+ import { SUPABASE_URL, SUPABASE_ANON_KEY, AGENT_URL } from "./constants.js";
4
5
  const CREDENTIALS_DIR = join(homedir(), ".cassian");
5
6
  const CREDENTIALS_FILE = join(CREDENTIALS_DIR, "credentials.json");
6
7
  export function getCredentials() {
@@ -20,18 +21,16 @@ export function saveCredentials(creds) {
20
21
  }
21
22
  export function clearCredentials() {
22
23
  if (existsSync(CREDENTIALS_FILE)) {
23
- writeFileSync(CREDENTIALS_FILE, "");
24
- const { unlinkSync } = require("fs");
25
24
  unlinkSync(CREDENTIALS_FILE);
26
25
  }
27
26
  }
28
27
  export function isTokenExpired(creds) {
29
28
  return Date.now() / 1000 > creds.expires_at - 60; // 60s buffer
30
29
  }
31
- export async function refreshToken(creds, supabaseUrl) {
32
- const resp = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=refresh_token`, {
30
+ export async function refreshToken(creds) {
31
+ const resp = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=refresh_token`, {
33
32
  method: "POST",
34
- headers: { "Content-Type": "application/json", apikey: getAnonKey() },
33
+ headers: { "Content-Type": "application/json", apikey: SUPABASE_ANON_KEY },
35
34
  body: JSON.stringify({ refresh_token: creds.refresh_token }),
36
35
  });
37
36
  if (!resp.ok) {
@@ -48,16 +47,5 @@ export async function refreshToken(creds, supabaseUrl) {
48
47
  return updated;
49
48
  }
50
49
  export function getAgentUrl() {
51
- const creds = getCredentials();
52
- if (!creds)
53
- throw new Error("Not logged in. Run 'cassian login' first.");
54
- return creds.agent_url;
55
- }
56
- function getAnonKey() {
57
- const configPath = join(CREDENTIALS_DIR, "config.json");
58
- if (existsSync(configPath)) {
59
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
60
- return config.supabase_anon_key || "";
61
- }
62
- return "";
50
+ return AGENT_URL;
63
51
  }
@@ -0,0 +1,4 @@
1
+ export declare const SUPABASE_URL = "https://ykgwmdvzzburglrkjutc.supabase.co";
2
+ export declare const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlrZ3dtZHZ6emJ1cmdscmtqdXRjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI0MzI0MzUsImV4cCI6MjA4ODAwODQzNX0.c-MSPTYr9_G77sy_2hAcY7hth8mSKmNQDzfTjIx8rE0";
3
+ export declare const AGENT_URL = "https://gpu.platops.ai";
4
+ export declare const PLATFORM_URL = "https://platform.trycassian.com";
@@ -0,0 +1,4 @@
1
+ export const SUPABASE_URL = "https://ykgwmdvzzburglrkjutc.supabase.co";
2
+ export const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlrZ3dtZHZ6emJ1cmdscmtqdXRjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI0MzI0MzUsImV4cCI6MjA4ODAwODQzNX0.c-MSPTYr9_G77sy_2hAcY7hth8mSKmNQDzfTjIx8rE0";
3
+ export const AGENT_URL = "https://gpu.platops.ai";
4
+ export const PLATFORM_URL = "https://platform.trycassian.com";
@@ -34,14 +34,7 @@ async function getToken() {
34
34
  if (!creds)
35
35
  throw new Error("Not logged in");
36
36
  if (isTokenExpired(creds)) {
37
- const { readFileSync: rf, existsSync: ef } = await import("fs");
38
- const cfgPath = `${process.env.HOME}/.cassian/config.json`;
39
- let supabaseUrl = "";
40
- if (ef(cfgPath))
41
- supabaseUrl = JSON.parse(rf(cfgPath, "utf-8")).supabase_url || "";
42
- if (!supabaseUrl)
43
- throw new Error("Session expired");
44
- const refreshed = await refreshToken(creds, supabaseUrl);
37
+ const refreshed = await refreshToken(creds);
45
38
  return refreshed.access_token;
46
39
  }
47
40
  return creds.access_token;
package/dist/types.d.ts CHANGED
@@ -3,7 +3,6 @@ export interface Credentials {
3
3
  refresh_token: string;
4
4
  expires_at: number;
5
5
  user_email: string;
6
- agent_url: string;
7
6
  }
8
7
  export interface CassianConfig {
9
8
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cassian-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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": {