cassian-cli 0.3.10 → 0.3.12

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,5 @@
1
1
  export declare function exec(args: string[], opts?: {
2
2
  timeout?: string;
3
3
  noSync?: boolean;
4
+ detach?: boolean;
4
5
  }): Promise<void>;
@@ -7,7 +7,7 @@ export async function exec(args, opts = {}) {
7
7
  fatal("No command specified.", "Usage: cassian exec <command>");
8
8
  }
9
9
  const command = args.join(" ");
10
- const timeout = parseInt(opts.timeout || "600");
10
+ const timeout = parseInt(opts.timeout || "3600");
11
11
  let config;
12
12
  try {
13
13
  config = loadConfig();
@@ -26,13 +26,27 @@ export async function exec(args, opts = {}) {
26
26
  try {
27
27
  await pushWorkspace(client, instance.id, config);
28
28
  }
29
- catch { /* sync best-effort — don't block exec */ }
29
+ catch (e) {
30
+ if (e.message.includes("limit")) {
31
+ fatal(e.message);
32
+ }
33
+ }
34
+ }
35
+ if (opts.detach) {
36
+ const result = await client.post(`/v1/instances/${instance.id}/exec`, {
37
+ command,
38
+ timeout,
39
+ workdir: "/workspace",
40
+ detach: true,
41
+ });
42
+ process.stdout.write(`\x1b[32m✓\x1b[0m Running in background (task: ${result.task_id})\n`);
43
+ process.stdout.write(`\x1b[90m View output: cassian logs\x1b[0m\n`);
44
+ return;
30
45
  }
31
- const mountPath = config.volumes?.[0]?.mount || "/workspace";
32
46
  const result = await client.post(`/v1/instances/${instance.id}/exec`, {
33
47
  command,
34
48
  timeout,
35
- workdir: mountPath,
49
+ workdir: "/workspace",
36
50
  });
37
51
  if (result.stdout)
38
52
  process.stdout.write(result.stdout);
@@ -3,6 +3,7 @@ export interface InitOptions {
3
3
  gpu?: string;
4
4
  gpuCount?: string;
5
5
  disk?: string;
6
+ storage?: boolean;
6
7
  yes?: boolean;
7
8
  }
8
9
  export declare function init(options?: InitOptions): Promise<void>;
@@ -53,7 +53,7 @@ export async function init(options = {}) {
53
53
  gpuType: options.gpu || "rtx3090",
54
54
  gpuCount: parseInt(options.gpuCount || "1"),
55
55
  diskSize: options.disk || "50G",
56
- storage: true,
56
+ storage: options.storage === true,
57
57
  exclude: ["node_modules/", ".venv/", "__pycache__/", "*.pyc", "wandb/"],
58
58
  });
59
59
  writeFileSync("cassian.yaml", yaml);
@@ -127,13 +127,19 @@ export async function init(options = {}) {
127
127
  p.cancel("Setup cancelled.");
128
128
  process.exit(0);
129
129
  }
130
- const storageEnabled = await p.confirm({
131
- message: "Enable cloud storage? \x1b[90m(unlimited, persists at /workspace/storage)\x1b[0m",
132
- initialValue: true,
133
- });
134
- if (p.isCancel(storageEnabled)) {
135
- p.cancel("Setup cancelled.");
136
- process.exit(0);
130
+ let storageEnabled;
131
+ if (options.storage !== undefined) {
132
+ storageEnabled = options.storage;
133
+ }
134
+ else {
135
+ storageEnabled = await p.confirm({
136
+ message: "Enable cloud storage? \x1b[90m(unlimited, persists at /workspace/storage)\x1b[0m",
137
+ initialValue: false,
138
+ });
139
+ if (p.isCancel(storageEnabled)) {
140
+ p.cancel("Setup cancelled.");
141
+ process.exit(0);
142
+ }
137
143
  }
138
144
  const excludeRaw = await p.text({
139
145
  message: "Exclude patterns \x1b[90m(comma-separated, not persisted)\x1b[0m",
@@ -0,0 +1 @@
1
+ export declare function switchGpu(): Promise<void>;
@@ -0,0 +1,64 @@
1
+ import { readFileSync, writeFileSync } from "fs";
2
+ import * as p from "@clack/prompts";
3
+ import { AGENT_URL } from "../lib/constants.js";
4
+ import { loadConfig, findConfigFile } from "../lib/config.js";
5
+ const ALL_GPUS = [
6
+ { value: "h100-sxm", label: "H100 SXM", memory: "80GB", hint: "best for large training" },
7
+ { value: "h100-pcie", label: "H100 PCIe", memory: "80GB" },
8
+ { value: "a100-sxm", label: "A100 SXM", memory: "80GB", hint: "reliable workhorse" },
9
+ { value: "a100-pcie", label: "A100 PCIe", memory: "80GB" },
10
+ { value: "l40s", label: "L40S", memory: "48GB", hint: "great for inference" },
11
+ { value: "l4", label: "L4", memory: "24GB", hint: "efficient inference" },
12
+ { value: "a10g", label: "A10G", memory: "24GB", hint: "good all-rounder" },
13
+ { value: "rtx4090", label: "RTX 4090", memory: "24GB", hint: "fast, affordable" },
14
+ { value: "rtx3090", label: "RTX 3090", memory: "24GB" },
15
+ ];
16
+ async function fetchAvailability() {
17
+ try {
18
+ const resp = await fetch(`${AGENT_URL}/v1/gpu-types`);
19
+ if (!resp.ok)
20
+ return {};
21
+ const { gpu_types } = await resp.json();
22
+ const map = {};
23
+ for (const g of gpu_types)
24
+ map[g.type.toLowerCase()] = g.available;
25
+ return map;
26
+ }
27
+ catch {
28
+ return {};
29
+ }
30
+ }
31
+ export async function switchGpu() {
32
+ const configPath = findConfigFile();
33
+ if (!configPath) {
34
+ p.log.error("No cassian.yaml found. Run 'cassian init' first.");
35
+ process.exit(1);
36
+ }
37
+ const config = loadConfig(configPath);
38
+ const currentType = config.gpu?.type || "unknown";
39
+ p.log.info(`Current GPU: \x1b[32m${currentType.toUpperCase()}\x1b[0m`);
40
+ console.log();
41
+ const availability = await fetchAvailability();
42
+ const gpuType = await p.select({
43
+ message: "Switch to GPU",
44
+ options: ALL_GPUS.map(gpu => {
45
+ const avail = availability[gpu.value] ?? 0;
46
+ const tag = avail > 0 ? `\x1b[32m${avail} available\x1b[0m` : `\x1b[90munavailable\x1b[0m`;
47
+ const hint = gpu.hint ? `${gpu.memory} · ${gpu.hint} · ${tag}` : `${gpu.memory} · ${tag}`;
48
+ return { value: gpu.value, label: gpu.label, hint };
49
+ }),
50
+ });
51
+ if (p.isCancel(gpuType)) {
52
+ p.cancel("Cancelled.");
53
+ process.exit(0);
54
+ }
55
+ if (gpuType === currentType) {
56
+ p.log.info("Already using that GPU type.");
57
+ return;
58
+ }
59
+ const yaml = readFileSync(configPath, "utf-8");
60
+ const updated = yaml.replace(/^(\s*type:\s*).*$/m, `$1${gpuType}`);
61
+ writeFileSync(configPath, updated);
62
+ p.log.success(`Switched to \x1b[32m${gpuType.toUpperCase()}\x1b[0m`);
63
+ p.log.info("Run \x1b[32mcassian up\x1b[0m to start with the new GPU.");
64
+ }
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ import { logs } from "./commands/logs.js";
15
15
  import { volumeCreate, volumeList, volumeDelete } from "./commands/volume.js";
16
16
  import { sync } from "./commands/sync.js";
17
17
  import { exec } from "./commands/exec.js";
18
+ import { switchGpu } from "./commands/switch.js";
18
19
  import { createRequire } from "module";
19
20
  const require = createRequire(import.meta.url);
20
21
  const { version } = require("../package.json");
@@ -46,9 +47,10 @@ program
46
47
  program
47
48
  .command("exec <command...>")
48
49
  .description("Sync files and run a command on the instance")
49
- .option("--timeout <seconds>", "Command timeout in seconds", "600")
50
+ .option("--timeout <seconds>", "Command timeout in seconds", "3600")
50
51
  .option("--no-sync", "Skip file sync before executing")
51
- .action((command, opts) => exec(command, { timeout: opts.timeout, noSync: !opts.sync }));
52
+ .option("-d, --detach", "Run in background, return immediately")
53
+ .action((command, opts) => exec(command, { timeout: opts.timeout, noSync: !opts.sync, detach: opts.detach }));
52
54
  program
53
55
  .command("sync")
54
56
  .description("Sync workspace from instance to local")
@@ -81,6 +83,10 @@ volume
81
83
  .description("Delete a volume (irreversible)")
82
84
  .requiredOption("--name <name>", "Volume name to delete")
83
85
  .action((opts) => volumeDelete(opts.name));
86
+ program
87
+ .command("switch")
88
+ .description("Switch GPU type (shows live availability)")
89
+ .action(switchGpu);
84
90
  program
85
91
  .command("init")
86
92
  .description("Set up a new Cassian project (interactive or via flags)")
@@ -88,12 +94,14 @@ program
88
94
  .option("--gpu <type>", "GPU type (e.g. h100-sxm, a100-sxm, rtx3090)")
89
95
  .option("--gpu-count <n>", "Number of GPUs")
90
96
  .option("--disk <size>", "Disk size (e.g. 50G, 200G)")
97
+ .option("--cloud-storage", "Enable cloud storage at /workspace/storage")
91
98
  .option("-y, --yes", "Skip all prompts, use defaults")
92
99
  .action((opts) => init({
93
100
  name: opts.name,
94
101
  gpu: opts.gpu,
95
102
  gpuCount: opts.gpuCount,
96
103
  disk: opts.disk,
104
+ storage: opts.cloudStorage,
97
105
  yes: opts.yes,
98
106
  }));
99
107
  program.parse();
@@ -2,4 +2,5 @@ import type { CassianConfig, DevOverrides } from "../types.js";
2
2
  export declare function loadConfig(path?: string): CassianConfig;
3
3
  export declare function loadDevOverrides(): DevOverrides;
4
4
  export declare function findSshPublicKey(override?: string): string;
5
+ export declare function findConfigFile(): string | null;
5
6
  export declare function parseSizeToGb(size: string): number;
@@ -35,7 +35,7 @@ export function findSshPublicKey(override) {
35
35
  }
36
36
  throw new Error("No SSH public key found. Run: ssh-keygen -t ed25519");
37
37
  }
38
- function findConfigFile() {
38
+ export function findConfigFile() {
39
39
  for (const name of ["cassian.yaml", "cassian.yml"]) {
40
40
  const p = resolve(process.cwd(), name);
41
41
  if (existsSync(p))
package/dist/lib/push.js CHANGED
@@ -1,3 +1,4 @@
1
+ const MAX_PUSH_SIZE_MB = 500;
1
2
  export async function pushWorkspace(client, instanceId, config) {
2
3
  const tar = await import("tar");
3
4
  const mountPath = config.volumes?.[0]?.mount || "/workspace";
@@ -50,5 +51,14 @@ export async function pushWorkspace(client, instanceId, config) {
50
51
  pack.on("end", res);
51
52
  pack.on("error", rej);
52
53
  });
53
- await client.push(instanceId, Buffer.concat(chunks), mountPath);
54
+ const payload = Buffer.concat(chunks);
55
+ const sizeMb = payload.length / (1024 * 1024);
56
+ if (sizeMb > MAX_PUSH_SIZE_MB) {
57
+ throw new Error(`Workspace is ${Math.round(sizeMb)}MB (limit: ${MAX_PUSH_SIZE_MB}MB). ` +
58
+ `Add large folders to 'workspace.exclude' in cassian.yaml.`);
59
+ }
60
+ if (sizeMb > 100) {
61
+ process.stderr.write(`\x1b[33m⚠ Workspace is ${Math.round(sizeMb)}MB — consider adding excludes to speed up sync.\x1b[0m\n`);
62
+ }
63
+ await client.push(instanceId, payload, mountPath);
54
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cassian-cli",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
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": {