cassian-cli 0.1.1 → 0.1.3

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 +1,12 @@
1
- export declare function init(): Promise<void>;
1
+ export interface InitOptions {
2
+ name?: string;
3
+ gpu?: string;
4
+ gpuCount?: string;
5
+ memory?: string;
6
+ shm?: string;
7
+ disk?: string;
8
+ setup?: string;
9
+ syncignore?: string;
10
+ yes?: boolean;
11
+ }
12
+ export declare function init(options?: InitOptions): Promise<void>;
@@ -2,11 +2,18 @@ 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" },
12
+ { label: "RTX 3090", value: "rtx3090", hint: "24GB" },
13
+ ];
5
14
  function prompt(rl, question, fallback) {
6
15
  return new Promise((resolve) => {
7
- rl.question(question, (answer) => {
8
- resolve(answer.trim() || fallback);
9
- });
16
+ rl.question(question, (answer) => resolve(answer.trim() || fallback));
10
17
  });
11
18
  }
12
19
  function promptChoice(rl, question, choices, fallback) {
@@ -19,116 +26,109 @@ function promptChoice(rl, question, choices, fallback) {
19
26
  });
20
27
  rl.question(dim(` [1-${choices.length}, default ${choices.findIndex(c => c.value === fallback) + 1}]: `), (answer) => {
21
28
  const idx = parseInt(answer.trim()) - 1;
22
- if (idx >= 0 && idx < choices.length)
23
- resolve(choices[idx].value);
24
- else
25
- resolve(fallback);
29
+ resolve(idx >= 0 && idx < choices.length ? choices[idx].value : fallback);
26
30
  });
27
31
  });
28
32
  }
29
- export async function init() {
33
+ function buildYaml(opts) {
34
+ const volName = opts.name.replace(/[^a-z0-9-]/g, "-") + "-data";
35
+ const syncignoreLines = opts.syncignore.map(p => ` - "${p}"`).join("\n");
36
+ const setupLine = opts.setup ? ` setup: ${opts.setup}\n` : "";
37
+ return `name: ${opts.name}
38
+
39
+ gpu:
40
+ count: ${opts.gpuCount}
41
+ type: ${opts.gpuType}
42
+
43
+ image: default
44
+
45
+ volumes:
46
+ - name: ${volName}
47
+ size: ${opts.diskSize}
48
+ mount: /workspace
49
+
50
+ resources:
51
+ memory: ${opts.memory}
52
+ shm_size: ${opts.shmSize}
53
+
54
+ workspace:
55
+ ${setupLine} syncignore:
56
+ ${syncignoreLines}
57
+ `;
58
+ }
59
+ export async function init(options = {}) {
30
60
  if (existsSync("cassian.yaml")) {
31
61
  fatal("cassian.yaml already exists in this directory.");
32
62
  }
63
+ const dirName = process.cwd().split("/").pop().toLowerCase().replace(/[^a-z0-9-]/g, "-");
64
+ const skipPrompts = options.yes ?? false;
65
+ // If all required flags are provided, skip the wizard entirely
66
+ const allFlagsProvided = !!(options.name && options.gpu && options.gpuCount && options.memory && options.disk);
67
+ if (allFlagsProvided || skipPrompts) {
68
+ const syncignore = options.syncignore
69
+ ? options.syncignore.split(",").map(s => s.trim()).filter(Boolean)
70
+ : ["outputs/**", "*.ckpt", "*.safetensors", "**/.cache/**"];
71
+ const yaml = buildYaml({
72
+ name: options.name || dirName,
73
+ gpuType: options.gpu || "rtx3090",
74
+ gpuCount: parseInt(options.gpuCount || "1"),
75
+ memory: options.memory || "32G",
76
+ shmSize: options.shm || "16G",
77
+ diskSize: options.disk || "50G",
78
+ setup: options.setup || (existsSync("requirements.txt") ? "pip install -r /workspace/requirements.txt" : ""),
79
+ syncignore,
80
+ });
81
+ writeFileSync("cassian.yaml", yaml);
82
+ console.log();
83
+ console.log(` ${green("✓")} Created cassian.yaml`);
84
+ console.log();
85
+ console.log(` Run ${green("cassian up")} to start your instance.`);
86
+ console.log();
87
+ return;
88
+ }
89
+ // Interactive wizard
33
90
  const rl = createInterface({ input: process.stdin, output: process.stdout });
34
91
  console.log();
35
92
  console.log(bold(" Let's set up your Cassian project."));
36
- console.log(dim(" Press enter to accept defaults.\n"));
37
- // Instance name
38
- const dirName = process.cwd().split("/").pop().toLowerCase().replace(/[^a-z0-9-]/g, "-");
39
- const name = await prompt(rl, ` Instance name ${dim(`[${dirName}]`)}: `, dirName);
40
- // GPU type
93
+ console.log(dim(" Press enter to accept defaults. Or pass flags to skip: cassian init --help\n"));
94
+ const name = options.name || await prompt(rl, ` Instance name ${dim(`[${dirName}]`)}: `, dirName);
41
95
  console.log();
42
- const gpuType = await promptChoice(rl, " GPU type:", [
43
- { label: "H100 SXM", value: "h100-sxm", hint: "80GB — best for large training runs" },
44
- { label: "H100 PCIe", value: "h100-pcie", hint: "80GB" },
45
- { label: "A100 SXM", value: "a100-sxm", hint: "80GB — reliable workhorse" },
46
- { label: "A100 PCIe", value: "a100-pcie", hint: "80GB" },
47
- { label: "L40S", value: "l40s", hint: "48GB — great for inference" },
48
- { label: "RTX 4090", value: "rtx4090", hint: "24GB — fast, affordable" },
49
- { label: "RTX 3090", value: "rtx3090", hint: "24GB" },
50
- ], "rtx3090");
51
- // GPU count
96
+ const gpuType = options.gpu || await promptChoice(rl, " GPU type:", GPU_OPTIONS, "rtx3090");
52
97
  console.log();
53
- const gpuCountStr = await promptChoice(rl, " Number of GPUs:", [
54
- { label: "1", value: "1" },
55
- { label: "2", value: "2" },
56
- { label: "4", value: "4" },
57
- { label: "8", value: "8" },
98
+ const gpuCountStr = options.gpuCount || await promptChoice(rl, " Number of GPUs:", [
99
+ { label: "1", value: "1" }, { label: "2", value: "2" },
100
+ { label: "4", value: "4" }, { label: "8", value: "8" },
58
101
  ], "1");
59
102
  const gpuCount = parseInt(gpuCountStr);
60
- // RAM
61
103
  console.log();
62
- const memory = await promptChoice(rl, " RAM:", [
63
- { label: "16 GB", value: "16G" },
64
- { label: "32 GB", value: "32G", hint: "recommended" },
65
- { label: "64 GB", value: "64G" },
66
- { label: "128 GB", value: "128G" },
67
- { label: "256 GB", value: "256G" },
104
+ const memory = options.memory || await promptChoice(rl, " RAM:", [
105
+ { label: "16 GB", value: "16G" }, { label: "32 GB", value: "32G", hint: "recommended" },
106
+ { label: "64 GB", value: "64G" }, { label: "128 GB", value: "128G" }, { label: "256 GB", value: "256G" },
68
107
  ], "32G");
69
- // Shared memory (for PyTorch DataLoader)
70
108
  console.log();
71
- const shmSize = await promptChoice(rl, " Shared memory (for PyTorch DataLoader):", [
72
- { label: "4 GB", value: "4G" },
73
- { label: "8 GB", value: "8G" },
74
- { label: "16 GB", value: "16G", hint: "recommended" },
75
- { label: "32 GB", value: "32G" },
109
+ const shmSize = options.shm || await promptChoice(rl, " Shared memory (PyTorch DataLoader):", [
110
+ { label: "4 GB", value: "4G" }, { label: "8 GB", value: "8G" },
111
+ { label: "16 GB", value: "16G", hint: "recommended" }, { label: "32 GB", value: "32G" },
76
112
  ], "16G");
77
- // Disk size
78
113
  console.log();
79
- const diskSize = await promptChoice(rl, " Workspace disk size:", [
80
- { label: "20 GB", value: "20G" },
81
- { label: "50 GB", value: "50G", hint: "recommended" },
82
- { label: "100 GB", value: "100G" },
83
- { label: "200 GB", value: "200G" },
84
- { label: "500 GB", value: "500G", hint: "for large datasets / checkpoints" },
85
- { label: "1 TB", value: "1T" },
114
+ const diskSize = options.disk || await promptChoice(rl, " Workspace disk size:", [
115
+ { label: "20 GB", value: "20G" }, { label: "50 GB", value: "50G", hint: "recommended" },
116
+ { label: "100 GB", value: "100G" }, { label: "200 GB", value: "200G" },
117
+ { label: "500 GB", value: "500G", hint: "for large datasets / checkpoints" }, { label: "1 TB", value: "1T" },
86
118
  ], "50G");
87
- // Setup command
88
119
  console.log();
89
120
  const hasReqs = existsSync("requirements.txt");
90
- const hasPyproject = existsSync("pyproject.toml");
91
- let defaultSetup = "";
92
- if (hasReqs)
93
- defaultSetup = "pip install -r /workspace/requirements.txt";
94
- else if (hasPyproject)
95
- defaultSetup = "pip install -e /workspace";
96
- const setupRaw = await prompt(rl, ` Setup command ${dim(`[${defaultSetup || "skip"}]`)}: `, defaultSetup);
121
+ const defaultSetup = hasReqs ? "pip install -r /workspace/requirements.txt" : existsSync("pyproject.toml") ? "pip install -e /workspace" : "";
122
+ const setupRaw = options.setup ?? await prompt(rl, ` Setup command ${dim(`[${defaultSetup || "skip"}]`)}: `, defaultSetup);
97
123
  const setup = setupRaw === "skip" ? "" : setupRaw;
98
- // Syncignore — what not to sync
99
124
  console.log();
100
- console.log(dim(" Patterns to exclude from sync (like .gitignore)."));
101
- console.log(dim(" Separate with commas, or press enter for defaults."));
102
- const syncignoreRaw = await prompt(rl, ` Syncignore ${dim("[outputs/**, *.ckpt, *.safetensors, **/.cache/**]")}: `, "");
103
- const customIgnore = syncignoreRaw
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
104
128
  ? syncignoreRaw.split(",").map(s => s.trim()).filter(Boolean)
105
129
  : ["outputs/**", "*.ckpt", "*.safetensors", "**/.cache/**"];
106
130
  rl.close();
107
- // Build yaml
108
- const volName = name.replace(/[^a-z0-9-]/g, "-") + "-data";
109
- const syncignoreLines = customIgnore.map(p => ` - "${p}"`).join("\n");
110
- const setupLine = setup ? ` setup: ${setup}\n` : "";
111
- const yaml = `name: ${name}
112
-
113
- gpu:
114
- count: ${gpuCount}
115
- type: ${gpuType}
116
-
117
- image: default
118
-
119
- volumes:
120
- - name: ${volName}
121
- size: ${diskSize}
122
- mount: /workspace
123
-
124
- resources:
125
- memory: ${memory}
126
- shm_size: ${shmSize}
127
-
128
- workspace:
129
- ${setupLine} syncignore:
130
- ${syncignoreLines}
131
- `;
131
+ const yaml = buildYaml({ name, gpuType, gpuCount, memory, shmSize, diskSize, setup, syncignore });
132
132
  writeFileSync("cassian.yaml", yaml);
133
133
  console.log();
134
134
  console.log(` ${green("✓")} Created cassian.yaml`);
@@ -100,6 +100,20 @@ export async function up() {
100
100
  await new Promise((r) => setTimeout(r, 1000));
101
101
  spinner.text = "Starting instance...";
102
102
  }
103
+ // Validate GPU type against what the host offers before provisioning
104
+ if (config.gpu.type) {
105
+ const gpus = await client.get("/v1/gpus").catch(() => null);
106
+ if (gpus && gpus.gpus.length > 0) {
107
+ const available = gpus.gpus[0].name.toLowerCase().replace(/[\s_]/g, "-");
108
+ const requested = config.gpu.type.toLowerCase().replace(/[\s_]/g, "-");
109
+ // Check if the requested type is a substring match of what's available
110
+ if (!available.includes(requested) && !requested.includes(available.split("-").pop())) {
111
+ spinner.fail("GPU type not available");
112
+ const names = [...new Set(gpus.gpus.map((g) => g.name))];
113
+ fatal(`GPU type '${config.gpu.type}' is not available on this host.`, `Available: ${names.join(", ")} — update gpu.type in cassian.yaml`);
114
+ }
115
+ }
116
+ }
103
117
  const instance = await client.post("/v1/instances", {
104
118
  name: config.name,
105
119
  gpu_count: config.gpu.count,
package/dist/index.js CHANGED
@@ -73,6 +73,25 @@ volume
73
73
  .action((opts) => volumeDelete(opts.name));
74
74
  program
75
75
  .command("init")
76
- .description("Set up a new Cassian project interactively")
77
- .action(init);
76
+ .description("Set up a new Cassian project (interactive or via flags)")
77
+ .option("--name <name>", "Instance name")
78
+ .option("--gpu <type>", "GPU type (e.g. h100-sxm, a100-sxm, rtx3090)")
79
+ .option("--gpu-count <n>", "Number of GPUs")
80
+ .option("--memory <size>", "RAM (e.g. 32G, 128G)")
81
+ .option("--shm <size>", "Shared memory for PyTorch (e.g. 16G)")
82
+ .option("--disk <size>", "Workspace disk size (e.g. 50G, 1T)")
83
+ .option("--setup <cmd>", "Setup command to run after sync")
84
+ .option("--syncignore <list>", "Comma-separated patterns to exclude from sync")
85
+ .option("-y, --yes", "Skip all prompts, use defaults or provided flags")
86
+ .action((opts) => init({
87
+ name: opts.name,
88
+ gpu: opts.gpu,
89
+ gpuCount: opts.gpuCount,
90
+ memory: opts.memory,
91
+ shm: opts.shm,
92
+ disk: opts.disk,
93
+ setup: opts.setup,
94
+ syncignore: opts.syncignore,
95
+ yes: opts.yes,
96
+ }));
78
97
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cassian-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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": {