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.
- package/dist/commands/init.js +69 -22
- package/dist/commands/sync.js +37 -47
- package/dist/commands/up.js +5 -0
- package/dist/lib/push.js +26 -3
- package/dist/lib/watcher.js +36 -8
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
{
|
|
8
|
-
|
|
9
|
-
{
|
|
10
|
-
|
|
11
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
|
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("
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
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,
|
|
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`);
|
package/dist/commands/sync.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as tar from "tar";
|
|
2
|
-
import { mkdirSync
|
|
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
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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({
|
|
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) {
|
package/dist/commands/up.js
CHANGED
|
@@ -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
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
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
|
});
|
package/dist/lib/watcher.js
CHANGED
|
@@ -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 =
|
|
19
|
-
function buildIgnorePatterns(
|
|
20
|
-
|
|
21
|
-
"**/.git/**", "
|
|
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
|
|
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