cassian-cli 0.2.1 → 0.3.1

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.
@@ -2,14 +2,42 @@ import { createInterface } from "readline";
2
2
  import { writeFileSync, existsSync } from "fs";
3
3
  import { green, dim, bold } from "../lib/output.js";
4
4
  import { fatal } from "../lib/errors.js";
5
- const GPU_OPTIONS = [
6
- { label: "H100 SXM", value: "h100-sxm", hint: "80GB — best for large training" },
7
- { label: "H100 PCIe", value: "h100-pcie", hint: "80GB" },
8
- { label: "A100 SXM", value: "a100-sxm", hint: "80GB — reliable workhorse" },
9
- { label: "A100 PCIe", value: "a100-pcie", hint: "80GB" },
10
- { label: "L40S", value: "l40s", hint: "48GB — great for inference" },
11
- { label: "RTX 4090", value: "rtx4090", hint: "24GB — fast, affordable" },
5
+ import { AGENT_URL } from "../lib/constants.js";
6
+ const GPU_METADATA = {
7
+ "h100": { memory: "80GB", hint: "best for large training" },
8
+ "a100": { memory: "80GB", hint: "reliable workhorse" },
9
+ "l40s": { memory: "48GB", hint: "great for inference" },
10
+ "l4": { memory: "24GB", hint: "efficient inference" },
11
+ "a10g": { memory: "24GB", hint: "good all-rounder" },
12
+ "rtx4090": { memory: "24GB", hint: "fast, affordable" },
13
+ "rtx3090": { memory: "24GB" },
14
+ };
15
+ async function fetchAvailableGpus() {
16
+ try {
17
+ const resp = await fetch(`${AGENT_URL}/v1/gpu-types`);
18
+ if (!resp.ok)
19
+ return [];
20
+ const { gpu_types } = await resp.json();
21
+ const options = [];
22
+ for (const gpu of gpu_types) {
23
+ const type = gpu.type.toLowerCase();
24
+ const meta = GPU_METADATA[type];
25
+ const label = type.toUpperCase().replace("RTX", "RTX ");
26
+ const hint = meta
27
+ ? `${meta.memory}${meta.hint ? ` — ${meta.hint}` : ""} — ${gpu.available} available`
28
+ : `${gpu.available} available`;
29
+ options.push({ label, value: type, hint });
30
+ }
31
+ return options;
32
+ }
33
+ catch {
34
+ return [];
35
+ }
36
+ }
37
+ const FALLBACK_GPU_OPTIONS = [
12
38
  { label: "RTX 3090", value: "rtx3090", hint: "24GB" },
39
+ { label: "L4", value: "l4", hint: "24GB — efficient inference" },
40
+ { label: "A10G", value: "a10g", hint: "24GB — good all-rounder" },
13
41
  ];
14
42
  function prompt(rl, question, fallback) {
15
43
  return new Promise((resolve) => {
@@ -32,8 +60,13 @@ function promptChoice(rl, question, choices, fallback) {
32
60
  }
33
61
  function buildYaml(opts) {
34
62
  const volName = opts.name.replace(/[^a-z0-9-]/g, "-") + "-data";
35
- const syncignoreLines = opts.syncignore.map(p => ` - "${p}"`).join("\n");
36
63
  const setupLine = opts.setup ? ` setup: ${opts.setup}\n` : "";
64
+ const noSyncLines = opts.noSync.length
65
+ ? ` no_sync:\n${opts.noSync.map(p => ` - "${p}"`).join("\n")}\n` : "";
66
+ const storageLines = opts.storage.length
67
+ ? ` storage:\n${opts.storage.map(p => ` - "${p}"`).join("\n")}\n` : "";
68
+ const excludeLines = opts.exclude.length
69
+ ? ` exclude:\n${opts.exclude.map(p => ` - "${p}"`).join("\n")}\n` : "";
37
70
  return `name: ${opts.name}
38
71
 
39
72
  gpu:
@@ -52,9 +85,7 @@ resources:
52
85
  shm_size: ${opts.shmSize}
53
86
 
54
87
  workspace:
55
- ${setupLine} syncignore:
56
- ${syncignoreLines}
57
- `;
88
+ ${setupLine}${noSyncLines}${storageLines}${excludeLines}`;
58
89
  }
59
90
  export async function init(options = {}) {
60
91
  if (existsSync("cassian.yaml")) {
@@ -65,9 +96,6 @@ export async function init(options = {}) {
65
96
  // If all required flags are provided, skip the wizard entirely
66
97
  const allFlagsProvided = !!(options.name && options.gpu && options.gpuCount && options.memory && options.disk);
67
98
  if (allFlagsProvided || skipPrompts) {
68
- const syncignore = options.syncignore
69
- ? options.syncignore.split(",").map(s => s.trim()).filter(Boolean)
70
- : ["outputs/**", "*.ckpt", "*.safetensors", "**/.cache/**"];
71
99
  const yaml = buildYaml({
72
100
  name: options.name || dirName,
73
101
  gpuType: options.gpu || "rtx3090",
@@ -76,7 +104,9 @@ export async function init(options = {}) {
76
104
  shmSize: options.shm || "16G",
77
105
  diskSize: options.disk || "50G",
78
106
  setup: options.setup || (existsSync("requirements.txt") ? "pip install -r /workspace/requirements.txt" : ""),
79
- syncignore,
107
+ noSync: ["checkpoints/", "outputs/"],
108
+ storage: [],
109
+ exclude: ["node_modules/", ".venv/", "__pycache__/", "*.pyc", "wandb/"],
80
110
  });
81
111
  writeFileSync("cassian.yaml", yaml);
82
112
  console.log();
@@ -93,7 +123,10 @@ export async function init(options = {}) {
93
123
  console.log(dim(" Press enter to accept defaults. Or pass flags to skip: cassian init --help\n"));
94
124
  const name = options.name || await prompt(rl, ` Instance name ${dim(`[${dirName}]`)}: `, dirName);
95
125
  console.log();
96
- const gpuType = options.gpu || await promptChoice(rl, " GPU type:", GPU_OPTIONS, "rtx3090");
126
+ const liveGpus = await fetchAvailableGpus();
127
+ const gpuOptions = liveGpus.length > 0 ? liveGpus : FALLBACK_GPU_OPTIONS;
128
+ const defaultGpu = gpuOptions[0].value;
129
+ const gpuType = options.gpu || await promptChoice(rl, " GPU type:", gpuOptions, defaultGpu);
97
130
  console.log();
98
131
  const gpuCountStr = options.gpuCount || await promptChoice(rl, " Number of GPUs:", [
99
132
  { label: "1", value: "1" }, { label: "2", value: "2" },
@@ -122,13 +155,27 @@ export async function init(options = {}) {
122
155
  const setupRaw = options.setup ?? await prompt(rl, ` Setup command ${dim(`[${defaultSetup || "skip"}]`)}: `, defaultSetup);
123
156
  const setup = setupRaw === "skip" ? "" : setupRaw;
124
157
  console.log();
125
- console.log(dim(" Patterns to exclude from sync (comma-separated, or enter for defaults)."));
126
- const syncignoreRaw = options.syncignore ?? await prompt(rl, ` Syncignore ${dim("[outputs/**, *.ckpt, *.safetensors, **/.cache/**]")}: `, "");
127
- const syncignore = syncignoreRaw
128
- ? syncignoreRaw.split(",").map(s => s.trim()).filter(Boolean)
129
- : ["outputs/**", "*.ckpt", "*.safetensors", "**/.cache/**"];
158
+ console.log(dim(" Folders that stay on the GPU instance but don't sync to your machine"));
159
+ console.log(dim(" (large files like checkpoints, training outputs still persists across sessions)"));
160
+ const noSyncRaw = options.syncignore ?? await prompt(rl, ` No-sync folders ${dim("[checkpoints/, outputs/]")}: `, "");
161
+ const noSync = noSyncRaw
162
+ ? noSyncRaw.split(",").map(s => s.trim()).filter(Boolean)
163
+ : ["checkpoints/", "outputs/"];
164
+ console.log();
165
+ console.log(dim(" Folders streamed from cloud storage (huge datasets, pretrained models)"));
166
+ console.log(dim(" (doesn't use instance disk — reads directly from cloud)"));
167
+ const storageRaw = await prompt(rl, ` Cloud storage folders ${dim("[none]")}: `, "");
168
+ const storage = storageRaw
169
+ ? storageRaw.split(",").map(s => s.trim()).filter(Boolean)
170
+ : [];
171
+ console.log();
172
+ console.log(dim(" Folders to exclude entirely (build artifacts, can be recreated)"));
173
+ const excludeRaw = await prompt(rl, ` Exclude ${dim("[node_modules/, .venv/, __pycache__/, *.pyc, wandb/]")}: `, "");
174
+ const exclude = excludeRaw
175
+ ? excludeRaw.split(",").map(s => s.trim()).filter(Boolean)
176
+ : ["node_modules/", ".venv/", "__pycache__/", "*.pyc", "wandb/"];
130
177
  rl.close();
131
- const yaml = buildYaml({ name, gpuType, gpuCount, memory, shmSize, diskSize, setup, syncignore });
178
+ const yaml = buildYaml({ name, gpuType, gpuCount, memory, shmSize, diskSize, setup, noSync, storage, exclude });
132
179
  writeFileSync("cassian.yaml", yaml);
133
180
  console.log();
134
181
  console.log(` ${green("✓")} Created cassian.yaml`);
@@ -1,6 +1,5 @@
1
1
  import * as tar from "tar";
2
- import { mkdirSync, existsSync, readdirSync, unlinkSync, statSync } from "fs";
3
- import { join, normalize } from "path";
2
+ import { mkdirSync } from "fs";
4
3
  import { Readable } from "stream";
5
4
  import ora from "ora";
6
5
  import { ApiClient } from "../lib/api.js";
@@ -26,55 +25,46 @@ export async function sync() {
26
25
  const tarBuf = await client.pull(instance.id, mountPath);
27
26
  const cwd = process.cwd();
28
27
  mkdirSync(cwd, { recursive: true });
29
- // Build ignored set (same as push only reconcile within sync scope)
30
- const syncignore = config.workspace?.syncignore ?? [];
31
- const defaultIgnore = ["**/.git/**", "**/node_modules/**", "**/__pycache__/**", "**/*.pyc", "**/.DS_Store", ...syncignore];
32
- const ignoreRegexes = defaultIgnore.map((p) => {
28
+ // Build skip patterns from all workspace config fields
29
+ const patterns = ["**/.git/**", "**/.DS_Store"];
30
+ if (config.workspace?.syncignore)
31
+ patterns.push(...config.workspace.syncignore);
32
+ if (config.workspace?.no_sync) {
33
+ for (const p of config.workspace.no_sync) {
34
+ patterns.push(p.endsWith("/") ? `${p}**` : `**/${p}/**`);
35
+ }
36
+ }
37
+ if (config.workspace?.storage) {
38
+ for (const p of config.workspace.storage) {
39
+ patterns.push(p.endsWith("/") ? `${p}**` : `**/${p}/**`);
40
+ }
41
+ }
42
+ if (config.workspace?.exclude) {
43
+ for (const p of config.workspace.exclude) {
44
+ if (p.endsWith("/"))
45
+ patterns.push(`${p}**`);
46
+ else if (p.includes("*"))
47
+ patterns.push(`**/${p}`);
48
+ else
49
+ patterns.push(`**/${p}/**`);
50
+ }
51
+ }
52
+ const ignoreRegexes = patterns.map((p) => {
33
53
  const esc = p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\x00").replace(/\*/g, "[^/]*").replace(/\x00/g, ".*");
34
54
  return new RegExp(`(^|/)${esc}($|/)`);
35
55
  });
36
- // Collect remote file set from tar
37
- const remoteFiles = new Set();
38
- await new Promise((res) => {
39
- const parser = new tar.Parser({
40
- onentry: (entry) => {
41
- const stripped = entry.path.replace(/^[^/]+\//, "");
42
- const norm = normalize(stripped);
43
- if (stripped && !norm.startsWith("..") && !norm.startsWith("/"))
44
- remoteFiles.add(norm);
45
- entry.resume();
46
- },
47
- });
48
- Readable.from(tarBuf).pipe(parser)
49
- .on("finish", res).on("error", res);
50
- });
51
- // Delete local synced files not in remote
52
- function walk(dir) {
53
- if (!existsSync(dir))
54
- return [];
55
- return readdirSync(dir).flatMap((e) => {
56
- const full = join(dir, e);
57
- const rel = full.slice(cwd.length + 1);
58
- if (ignoreRegexes.some((re) => re.test(rel)))
59
- return [];
60
- try {
61
- return statSync(full).isDirectory() ? walk(full) : [rel];
62
- }
63
- catch {
64
- return [];
65
- }
66
- });
67
- }
68
- for (const f of walk(cwd)) {
69
- if (!remoteFiles.has(normalize(f))) {
70
- try {
71
- unlinkSync(join(cwd, f));
72
- }
73
- catch { /* skip */ }
74
- }
56
+ function shouldSkip(rel) {
57
+ return ignoreRegexes.some((re) => re.test(rel));
75
58
  }
76
- // Extract
77
- await new Promise((res, rej) => Readable.from(tarBuf).pipe(tar.extract({ cwd, strip: 1 })).on("finish", res).on("error", rej));
59
+ // Extract with filtering
60
+ await new Promise((res, rej) => Readable.from(tarBuf).pipe(tar.extract({
61
+ cwd,
62
+ strip: 1,
63
+ filter: (path) => {
64
+ const stripped = path.replace(/^\.\//, "");
65
+ return !shouldSkip(stripped);
66
+ },
67
+ })).on("finish", res).on("error", rej));
78
68
  spinner.succeed("Files synced");
79
69
  }
80
70
  catch (err) {
@@ -128,6 +128,11 @@ export async function up() {
128
128
  shm_size: config.resources?.shm_size || null,
129
129
  cpus: config.resources?.cpus || null,
130
130
  },
131
+ workspace: {
132
+ no_sync: config.workspace?.no_sync || [],
133
+ storage: config.workspace?.storage || [],
134
+ exclude: config.workspace?.exclude || config.workspace?.syncignore || [],
135
+ },
131
136
  });
132
137
  spinner.succeed("Instance ready");
133
138
  // Sync happens during ssh/exec, not during up
package/dist/lib/push.js CHANGED
@@ -1,9 +1,32 @@
1
1
  export async function pushWorkspace(client, instanceId, config) {
2
2
  const tar = await import("tar");
3
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) => {
4
+ // Build ignore list from all workspace fields
5
+ const patterns = ["**/.git/**", "**/.DS_Store"];
6
+ if (config.workspace?.syncignore)
7
+ patterns.push(...config.workspace.syncignore);
8
+ if (config.workspace?.no_sync) {
9
+ for (const p of config.workspace.no_sync) {
10
+ patterns.push(p.endsWith("/") ? `${p}**` : `**/${p}/**`);
11
+ }
12
+ }
13
+ if (config.workspace?.storage) {
14
+ for (const p of config.workspace.storage) {
15
+ const name = p.replace(/\/$/, "");
16
+ patterns.push(`${name}`, `${name}/**`);
17
+ }
18
+ }
19
+ if (config.workspace?.exclude) {
20
+ for (const p of config.workspace.exclude) {
21
+ if (p.endsWith("/"))
22
+ patterns.push(`${p}**`);
23
+ else if (p.includes("*"))
24
+ patterns.push(`**/${p}`);
25
+ else
26
+ patterns.push(`**/${p}/**`);
27
+ }
28
+ }
29
+ const ignoreRegexes = patterns.map((p) => {
7
30
  const esc = p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\x00").replace(/\*/g, "[^/]*").replace(/\x00/g, ".*");
8
31
  return new RegExp(`(^|/)${esc}($|/)`);
9
32
  });
@@ -15,13 +15,42 @@ import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, statSyn
15
15
  import { resolve, dirname, normalize } from "path";
16
16
  import { getCredentials, isTokenExpired, refreshToken } from "./auth.js";
17
17
  const POLL_INTERVAL_MS = 2000;
18
- const MAX_FILE_SIZE = 512 * 1024; // 512KB — skip large files (weights, checkpoints)
19
- function buildIgnorePatterns(syncignore) {
20
- return [
21
- "**/.git/**", "**/node_modules/**", "**/__pycache__/**",
22
- "**/*.pyc", "**/.DS_Store",
23
- ...syncignore,
18
+ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
19
+ function buildIgnorePatterns(config) {
20
+ const patterns = [
21
+ "**/.git/**", "**/.DS_Store",
24
22
  ];
23
+ // Legacy syncignore
24
+ if (config.workspace?.syncignore)
25
+ patterns.push(...config.workspace.syncignore);
26
+ // no_sync: persists to cloud but doesn't sync to laptop
27
+ if (config.workspace?.no_sync) {
28
+ for (const p of config.workspace.no_sync) {
29
+ patterns.push(p.endsWith("/") ? `${p}**` : `**/${p}/**`);
30
+ }
31
+ }
32
+ // storage: cold paths, don't sync to laptop (exclude the name itself + contents)
33
+ if (config.workspace?.storage) {
34
+ for (const p of config.workspace.storage) {
35
+ const name = p.replace(/\/$/, "");
36
+ patterns.push(name, `${name}/**`);
37
+ }
38
+ }
39
+ // exclude: throwaway, don't sync to laptop
40
+ if (config.workspace?.exclude) {
41
+ for (const p of config.workspace.exclude) {
42
+ if (p.endsWith("/")) {
43
+ patterns.push(`${p}**`);
44
+ }
45
+ else if (p.includes("*")) {
46
+ patterns.push(`**/${p}`);
47
+ }
48
+ else {
49
+ patterns.push(`**/${p}/**`);
50
+ }
51
+ }
52
+ }
53
+ return patterns;
25
54
  }
26
55
  function globToRegex(pattern) {
27
56
  const esc = pattern
@@ -41,8 +70,7 @@ async function getToken() {
41
70
  }
42
71
  export function startBidirectionalSync(instanceId, config, agentUrl) {
43
72
  const mountPath = config.volumes?.[0]?.mount ?? "/workspace";
44
- const syncignore = config.workspace?.syncignore ?? [];
45
- const ignoreRegexes = buildIgnorePatterns(syncignore).map(globToRegex);
73
+ const ignoreRegexes = buildIgnorePatterns(config).map(globToRegex);
46
74
  const cwd = process.cwd();
47
75
  let stopped = false;
48
76
  // Track files recently pushed by us so we don't pull-echo them back
package/dist/types.d.ts CHANGED
@@ -24,6 +24,9 @@ export interface CassianConfig {
24
24
  workspace?: {
25
25
  setup?: string;
26
26
  syncignore?: string[];
27
+ no_sync?: string[];
28
+ storage?: string[];
29
+ exclude?: string[];
27
30
  };
28
31
  }
29
32
  export interface DevOverrides {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cassian-cli",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
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": {