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.
- package/dist/commands/init.d.ts +12 -1
- package/dist/commands/init.js +87 -87
- package/dist/commands/up.js +14 -0
- package/dist/index.js +21 -2
- package/package.json +1 -1
package/dist/commands/init.d.ts
CHANGED
|
@@ -1 +1,12 @@
|
|
|
1
|
-
export
|
|
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>;
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
37
|
-
|
|
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: "
|
|
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: "
|
|
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 (
|
|
72
|
-
{ label: "4 GB", value: "4G" },
|
|
73
|
-
{ label: "
|
|
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: "
|
|
82
|
-
{ label: "
|
|
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
|
|
91
|
-
|
|
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 (
|
|
101
|
-
|
|
102
|
-
const
|
|
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
|
-
|
|
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`);
|
package/dist/commands/up.js
CHANGED
|
@@ -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
|
|
77
|
-
.
|
|
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();
|