cassian-cli 0.1.6 → 0.1.7
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/exec.d.ts +1 -0
- package/dist/commands/exec.js +9 -0
- package/dist/commands/login.js +149 -62
- package/dist/index.js +3 -2
- package/dist/lib/push.d.ts +3 -0
- package/dist/lib/push.js +21 -0
- package/package.json +1 -1
package/dist/commands/exec.d.ts
CHANGED
package/dist/commands/exec.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ApiClient } from "../lib/api.js";
|
|
2
2
|
import { loadConfig } from "../lib/config.js";
|
|
3
|
+
import { pushWorkspace } from "../lib/push.js";
|
|
3
4
|
import { fatal, handleError } from "../lib/errors.js";
|
|
4
5
|
export async function exec(args, opts = {}) {
|
|
5
6
|
if (args.length === 0) {
|
|
@@ -21,9 +22,17 @@ export async function exec(args, opts = {}) {
|
|
|
21
22
|
if (!instance) {
|
|
22
23
|
fatal(`${config.name} is not running.`, "Run: cassian up");
|
|
23
24
|
}
|
|
25
|
+
if (!opts.noSync) {
|
|
26
|
+
try {
|
|
27
|
+
await pushWorkspace(client, instance.id, config);
|
|
28
|
+
}
|
|
29
|
+
catch { /* sync best-effort — don't block exec */ }
|
|
30
|
+
}
|
|
31
|
+
const mountPath = config.volumes?.[0]?.mount || "/workspace";
|
|
24
32
|
const result = await client.post(`/v1/instances/${instance.id}/exec`, {
|
|
25
33
|
command,
|
|
26
34
|
timeout,
|
|
35
|
+
workdir: mountPath,
|
|
27
36
|
});
|
|
28
37
|
if (result.stdout)
|
|
29
38
|
process.stdout.write(result.stdout);
|
package/dist/commands/login.js
CHANGED
|
@@ -1,90 +1,177 @@
|
|
|
1
1
|
import { createServer } from "http";
|
|
2
|
+
import { createInterface } from "readline";
|
|
2
3
|
import open from "open";
|
|
3
4
|
import { saveCredentials } from "../lib/auth.js";
|
|
4
5
|
import { success, dim } from "../lib/output.js";
|
|
5
6
|
import { fatal } from "../lib/errors.js";
|
|
6
7
|
import { PLATFORM_URL } from "../lib/constants.js";
|
|
7
|
-
const
|
|
8
|
+
const PORT_RANGE = [9876, 9877, 9878, 9879, 9880];
|
|
8
9
|
export async function login() {
|
|
9
|
-
const
|
|
10
|
-
const
|
|
10
|
+
const port = await findOpenPort();
|
|
11
|
+
const callbackUrl = port ? `http://localhost:${port}/callback` : null;
|
|
12
|
+
const authUrl = callbackUrl
|
|
13
|
+
? `${PLATFORM_URL}?cli_redirect=${encodeURIComponent(callbackUrl)}`
|
|
14
|
+
: `${PLATFORM_URL}?cli_mode=token`;
|
|
15
|
+
const allowedOrigin = new URL(PLATFORM_URL).origin;
|
|
11
16
|
console.log();
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
res.
|
|
17
|
+
console.log(dim(" Login URL:"));
|
|
18
|
+
console.log(` ${authUrl}`);
|
|
19
|
+
console.log();
|
|
20
|
+
// If we got a port, try the automatic callback flow
|
|
21
|
+
if (port && callbackUrl) {
|
|
22
|
+
console.log(dim(" Press Enter to open browser. Or paste the token from the page below."));
|
|
23
|
+
console.log();
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
let done = false;
|
|
26
|
+
const server = createServer((req, res) => {
|
|
27
|
+
const origin = req.headers["origin"] || "";
|
|
28
|
+
if (origin && origin !== allowedOrigin) {
|
|
29
|
+
res.writeHead(403);
|
|
30
|
+
res.end();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
34
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
35
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
36
|
+
if (req.method === "OPTIONS") {
|
|
37
|
+
res.writeHead(204);
|
|
38
|
+
res.end();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
42
|
+
if (url.pathname === "/callback" && req.method === "POST") {
|
|
43
|
+
const accessToken = url.searchParams.get("access_token");
|
|
44
|
+
const refreshToken = url.searchParams.get("refresh_token");
|
|
45
|
+
const expiresIn = parseInt(url.searchParams.get("expires_in") || "3600");
|
|
46
|
+
if (!accessToken || !refreshToken) {
|
|
47
|
+
res.writeHead(400);
|
|
48
|
+
res.end();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
finishLogin(accessToken, refreshToken, expiresIn);
|
|
52
|
+
res.writeHead(200);
|
|
53
|
+
res.end("ok");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
res.writeHead(404);
|
|
29
57
|
res.end();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const expiresIn = parseInt(params.get("expires_in") || "3600");
|
|
39
|
-
if (!accessToken || !refreshToken) {
|
|
40
|
-
res.writeHead(400);
|
|
41
|
-
res.end("Missing tokens");
|
|
58
|
+
});
|
|
59
|
+
// Also accept pasted token from stdin
|
|
60
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
61
|
+
rl.on("line", (line) => {
|
|
62
|
+
const trimmed = line.trim();
|
|
63
|
+
if (!trimmed) {
|
|
64
|
+
// Enter with no input = open browser
|
|
65
|
+
open(authUrl).catch(() => { });
|
|
42
66
|
return;
|
|
43
67
|
}
|
|
44
|
-
//
|
|
68
|
+
// User pasted a token
|
|
69
|
+
if (handlePastedToken(trimmed)) {
|
|
70
|
+
rl.close();
|
|
71
|
+
server.close();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
server.listen(port, "127.0.0.1");
|
|
75
|
+
server.on("error", () => {
|
|
76
|
+
// Port taken — fall back to paste-only
|
|
77
|
+
console.log(dim(" Could not bind port. Paste your token below:"));
|
|
78
|
+
});
|
|
79
|
+
const timeout = setTimeout(() => {
|
|
80
|
+
if (!done) {
|
|
81
|
+
server.close();
|
|
82
|
+
rl.close();
|
|
83
|
+
fatal("Login timed out.", "Run: cassian login");
|
|
84
|
+
}
|
|
85
|
+
}, 300000);
|
|
86
|
+
server.on("close", () => { if (!done)
|
|
87
|
+
clearTimeout(timeout); });
|
|
88
|
+
function finishLogin(accessToken, refreshToken, expiresIn) {
|
|
89
|
+
if (done)
|
|
90
|
+
return;
|
|
91
|
+
done = true;
|
|
92
|
+
clearTimeout(timeout);
|
|
45
93
|
const payload = JSON.parse(Buffer.from(accessToken.split(".")[1], "base64url").toString());
|
|
46
94
|
const email = payload.email || "unknown";
|
|
47
|
-
|
|
95
|
+
saveCredentials({
|
|
48
96
|
access_token: accessToken,
|
|
49
97
|
refresh_token: refreshToken,
|
|
50
98
|
expires_at: Math.floor(Date.now() / 1000) + expiresIn,
|
|
51
99
|
user_email: email,
|
|
52
|
-
};
|
|
53
|
-
saveCredentials(creds);
|
|
54
|
-
res.writeHead(200);
|
|
55
|
-
res.end("ok");
|
|
100
|
+
});
|
|
56
101
|
console.log();
|
|
57
102
|
success(`Logged in as ${email}`);
|
|
58
103
|
console.log();
|
|
104
|
+
rl.close();
|
|
59
105
|
server.close();
|
|
60
106
|
resolve();
|
|
61
|
-
return;
|
|
62
107
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
108
|
+
function handlePastedToken(token) {
|
|
109
|
+
if (done)
|
|
110
|
+
return false;
|
|
111
|
+
try {
|
|
112
|
+
// Format: access_token:::refresh_token
|
|
113
|
+
let accessToken;
|
|
114
|
+
let refreshToken;
|
|
115
|
+
if (token.includes(":::")) {
|
|
116
|
+
[accessToken, refreshToken] = token.split(":::");
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
fatal("Invalid token format.", "Copy the full token from the login page.");
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const parts = accessToken.split(".");
|
|
123
|
+
if (parts.length !== 3)
|
|
124
|
+
throw new Error("bad jwt");
|
|
125
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
|
|
126
|
+
const expiresIn = (payload.exp || Math.floor(Date.now() / 1000) + 3600) - Math.floor(Date.now() / 1000);
|
|
127
|
+
finishLogin(accessToken, refreshToken, expiresIn);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
console.log(dim(" Invalid token. Try again or wait for browser callback."));
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
79
134
|
}
|
|
80
135
|
});
|
|
81
|
-
|
|
82
|
-
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// No port available — paste only
|
|
139
|
+
console.log(dim(" After logging in, copy the token from the page and paste it here:"));
|
|
140
|
+
console.log();
|
|
141
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
142
|
+
const token = await new Promise((res) => {
|
|
143
|
+
rl.question(" Token: ", (answer) => { rl.close(); res(answer.trim()); });
|
|
144
|
+
});
|
|
145
|
+
if (!token || !token.includes(":::")) {
|
|
146
|
+
fatal("Invalid token.", "Copy the full token from the login page.");
|
|
147
|
+
}
|
|
148
|
+
const [accessToken, refreshToken] = token.split(":::");
|
|
149
|
+
const payload = JSON.parse(Buffer.from(accessToken.split(".")[1], "base64url").toString());
|
|
150
|
+
const email = payload.email || "unknown";
|
|
151
|
+
saveCredentials({
|
|
152
|
+
access_token: accessToken,
|
|
153
|
+
refresh_token: refreshToken,
|
|
154
|
+
expires_at: payload.exp || Math.floor(Date.now() / 1000) + 3600,
|
|
155
|
+
user_email: email,
|
|
83
156
|
});
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
157
|
+
console.log();
|
|
158
|
+
success(`Logged in as ${email}`);
|
|
159
|
+
console.log();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function findOpenPort() {
|
|
163
|
+
return new Promise((resolve) => {
|
|
164
|
+
let idx = 0;
|
|
165
|
+
function tryNext() {
|
|
166
|
+
if (idx >= PORT_RANGE.length) {
|
|
167
|
+
resolve(null);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const port = PORT_RANGE[idx++];
|
|
171
|
+
const srv = createServer();
|
|
172
|
+
srv.listen(port, "127.0.0.1", () => { srv.close(() => resolve(port)); });
|
|
173
|
+
srv.on("error", () => tryNext());
|
|
174
|
+
}
|
|
175
|
+
tryNext();
|
|
89
176
|
});
|
|
90
177
|
}
|
package/dist/index.js
CHANGED
|
@@ -42,9 +42,10 @@ program
|
|
|
42
42
|
.action(ssh);
|
|
43
43
|
program
|
|
44
44
|
.command("exec <command...>")
|
|
45
|
-
.description("
|
|
45
|
+
.description("Sync files and run a command on the instance")
|
|
46
46
|
.option("--timeout <seconds>", "Command timeout in seconds", "600")
|
|
47
|
-
.
|
|
47
|
+
.option("--no-sync", "Skip file sync before executing")
|
|
48
|
+
.action((command, opts) => exec(command, { timeout: opts.timeout, noSync: !opts.sync }));
|
|
48
49
|
program
|
|
49
50
|
.command("sync")
|
|
50
51
|
.description("Sync workspace from instance to local")
|
package/dist/lib/push.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export async function pushWorkspace(client, instanceId, config) {
|
|
2
|
+
const tar = await import("tar");
|
|
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) => {
|
|
7
|
+
const esc = p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\x00").replace(/\*/g, "[^/]*").replace(/\x00/g, ".*");
|
|
8
|
+
return new RegExp(`(^|/)${esc}($|/)`);
|
|
9
|
+
});
|
|
10
|
+
const chunks = [];
|
|
11
|
+
await new Promise((res, rej) => {
|
|
12
|
+
const pack = tar.create({ cwd: process.cwd(), filter: (p) => {
|
|
13
|
+
const rel = p.replace(/^\.\//, "");
|
|
14
|
+
return !ignoreRegexes.some((re) => re.test(rel));
|
|
15
|
+
} }, ["."]);
|
|
16
|
+
pack.on("data", (c) => chunks.push(c));
|
|
17
|
+
pack.on("end", res);
|
|
18
|
+
pack.on("error", rej);
|
|
19
|
+
});
|
|
20
|
+
await client.push(instanceId, Buffer.concat(chunks), mountPath);
|
|
21
|
+
}
|