@vecteur/cli 0.3.1 → 0.4.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/README.md +49 -0
- package/bin/vecteur.js +311 -70
- package/package.json +6 -6
- package/src/client.js +313 -48
- package/src/contract.js +45 -10
- package/src/credentials.js +118 -31
- package/src/download.js +42 -0
- package/src/format.js +152 -8
- package/src/mcp.js +46 -9
- package/src/projects.js +42 -0
- package/bin/vecteur-mcp.js +0 -29
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Vecteur CLI
|
|
2
|
+
|
|
3
|
+
One command line for Vecteur's public API. Sign in through your browser, then use the same
|
|
4
|
+
Projects and run records as the web app. The client stores your credential locally; it never
|
|
5
|
+
calls the OPE, engine or database directly.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @vecteur/cli
|
|
9
|
+
vecteur login
|
|
10
|
+
vecteur whoami
|
|
11
|
+
vecteur project list
|
|
12
|
+
vecteur project create 'Transfer study'
|
|
13
|
+
vecteur project use 'Transfer study'
|
|
14
|
+
vecteur project get PROJECT_ID --json
|
|
15
|
+
vecteur run submit --ask 'GTO to GEO transfer'
|
|
16
|
+
vecteur run get RUN_ID --json
|
|
17
|
+
vecteur run events RUN_ID --json
|
|
18
|
+
vecteur artifact get sha256:ARTIFACT_DIGEST --output report.docx
|
|
19
|
+
vecteur logout
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Use `vecteur login --origin URL` for another deployment. For automation, supply a Project PAT
|
|
23
|
+
through `VECTEUR_TOKEN` and its origin through `VECTEUR_BASE_URL`; never pass a token on argv.
|
|
24
|
+
`--help` and `--version` work without signing in.
|
|
25
|
+
|
|
26
|
+
`project list` shows authorized Projects and the server's Organization-wide active Project
|
|
27
|
+
allowance. `project create NAME --workspace NAME_OR_ID` chooses among multiple Workspaces; one
|
|
28
|
+
Workspace is implicit. Names must match exactly and uniquely, or use an id. Quota refusals show
|
|
29
|
+
the server's reason and recovery links.
|
|
30
|
+
|
|
31
|
+
`project use NAME_OR_ID` saves the selected id beside the login for that origin. `run submit`
|
|
32
|
+
checks this selection against the live authorized list each time; `--project ID` overrides it.
|
|
33
|
+
With `VECTEUR_TOKEN`, runs require `--project ID` and `project use` requires a persistent login.
|
|
34
|
+
Project PATs retain their server-defined scope; listing and creating Projects require an account
|
|
35
|
+
login. `project list --json` preserves `{projects, active_projects, selected_project_id}`.
|
|
36
|
+
|
|
37
|
+
`run events` validates dense SSE sequence and returns the canonical direct record only when its
|
|
38
|
+
embedded events match the stream. Redirects, secret-shaped responses, malformed responses and
|
|
39
|
+
timeouts fail nonzero. `--json` sorts object keys for deterministic machine output.
|
|
40
|
+
|
|
41
|
+
Human run output shows the sealed synthesis, input origins, artifact download commands, the typed
|
|
42
|
+
terminal cause/recovery, and—after `run submit`—the selected Project's UTC-month customer cost using
|
|
43
|
+
the server's display strings. Machine run output remains the unchanged canonical record. Artifact
|
|
44
|
+
downloads use the Project-scoped admitted CAS route, verify the declared length, media type and
|
|
45
|
+
SHA-256, and refuse to overwrite a local path. Use `--project ID` when no Project is selected.
|
|
46
|
+
|
|
47
|
+
`vecteur mcp` serves MCP over stdio using the same credentials and public client. It exposes
|
|
48
|
+
`vecteur_project_get`, `vecteur_run_submit`, `vecteur_run_get` and `vecteur_run_events`.
|
|
49
|
+
An MCP host supplies tool arguments; stdout carries only MCP protocol.
|
package/bin/vecteur.js
CHANGED
|
@@ -1,106 +1,347 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { canonicalJson, human, progress } from "../src/format.js";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return
|
|
5
|
+
import {
|
|
6
|
+
CliError, VecteurClient, configuration, isTokenShaped, redact, requestTimeout, resolveOrigin,
|
|
7
|
+
shouldOpenBrowser, waitForDeviceAuthorization,
|
|
8
|
+
} from "../src/client.js";
|
|
9
|
+
import {
|
|
10
|
+
clearCredentials, credentialsPath, readCredentials, setCurrentOrigin, writeCredentials, selectProject,
|
|
11
|
+
} from "../src/credentials.js";
|
|
12
|
+
import { downloadArtifact } from "../src/download.js";
|
|
13
|
+
import { humanProjects, resolveNamed, resolveRunProject, resolveWorkspace } from "../src/projects.js";
|
|
14
|
+
import { canonicalJson, human, humanCost, progress } from "../src/format.js";
|
|
15
|
+
|
|
16
|
+
function manifest() {
|
|
17
|
+
return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function versionParts(raw) {
|
|
21
|
+
const match = String(raw).match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
|
|
22
|
+
return match ? [Number(match[1]), Number(match[2] ?? 0), Number(match[3] ?? 0)] : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function refuseOldNode() {
|
|
26
|
+
const declared = manifest().engines?.node;
|
|
27
|
+
if (typeof declared !== "string" || !declared.startsWith(">=")) {
|
|
28
|
+
throw new CliError("engines_invalid", "package engines.node must be a >= floor");
|
|
29
|
+
}
|
|
30
|
+
const floor = versionParts(declared.slice(2));
|
|
31
|
+
const current = versionParts(process.versions.node);
|
|
32
|
+
if (!floor || !current) {
|
|
33
|
+
throw new CliError("engines_invalid", "package engines.node must be a >= floor");
|
|
34
|
+
}
|
|
35
|
+
const below = current[0] < floor[0]
|
|
36
|
+
|| (current[0] === floor[0] && current[1] < floor[1])
|
|
37
|
+
|| (current[0] === floor[0] && current[1] === floor[1] && current[2] < floor[2]);
|
|
38
|
+
if (below) {
|
|
39
|
+
throw new CliError("node_version_unsupported",
|
|
40
|
+
`Node ${process.versions.node} is below engines.node ${declared}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function helpText(version) {
|
|
45
|
+
return `vecteur ${version}
|
|
46
|
+
login | logout | whoami | project list | project create <name> [--workspace <name-or-id>] | project use <name-or-id> | project get <id> | run submit [-p <id>] -a <ask> | run get <id> | run events <id> | artifact get <id> [--output <path>] [-p <id>] | mcp
|
|
47
|
+
--help --version --json --origin <url> --no-browser --local -p/--project -a/--ask --output <path>
|
|
48
|
+
`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function writeStatus(env = process.env) {
|
|
52
|
+
const saved = readCredentials(env);
|
|
53
|
+
if (saved?.current) {
|
|
54
|
+
process.stdout.write(`${saved.current}\nnext: vecteur whoami\n`);
|
|
55
|
+
} else {
|
|
56
|
+
process.stdout.write("not logged in\nnext: vecteur login\n");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function openBrowser(url) {
|
|
61
|
+
const command = process.platform === "darwin" ? "open"
|
|
62
|
+
: process.platform === "win32" ? "cmd" : "xdg-open";
|
|
63
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
64
|
+
try {
|
|
65
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
66
|
+
child.on("error", () => {});
|
|
67
|
+
child.unref();
|
|
68
|
+
} catch {
|
|
69
|
+
// The address is already on screen; failing to spawn a browser is not a failed login.
|
|
70
|
+
}
|
|
24
71
|
}
|
|
25
72
|
|
|
26
73
|
function parseCommand(argv) {
|
|
27
|
-
|
|
28
|
-
const args = json ? argv.slice(0, -1) : [...argv];
|
|
74
|
+
let args = [...argv];
|
|
29
75
|
const value = (candidate) => typeof candidate === "string" && candidate.length > 0 &&
|
|
30
76
|
!candidate.startsWith("-");
|
|
31
|
-
|
|
32
|
-
|
|
77
|
+
const takeFlag = (...names) => {
|
|
78
|
+
const indexes = args.flatMap((arg, i) => names.includes(arg) ? [i] : []);
|
|
79
|
+
if (indexes.length > 1) throw new CliError("usage_invalid", "Invalid command grammar");
|
|
80
|
+
if (indexes.length === 0) return false;
|
|
81
|
+
args = args.filter((_, i) => i !== indexes[0]);
|
|
82
|
+
return true;
|
|
83
|
+
};
|
|
84
|
+
const takeOption = (...names) => {
|
|
85
|
+
const indexes = args.flatMap((arg, i) => names.includes(arg) ? [i] : []);
|
|
86
|
+
if (indexes.length > 1) throw new CliError("usage_invalid", "Invalid command grammar");
|
|
87
|
+
if (indexes.length === 0) return undefined;
|
|
88
|
+
const index = indexes[0];
|
|
89
|
+
if (!value(args[index + 1])) throw new CliError("usage_invalid", "Invalid command grammar");
|
|
90
|
+
const option = args[index + 1];
|
|
91
|
+
args = args.filter((_, i) => i !== index && i !== index + 1);
|
|
92
|
+
return option;
|
|
93
|
+
};
|
|
94
|
+
const version = takeFlag("--version");
|
|
95
|
+
const help = takeFlag("--help");
|
|
96
|
+
const json = takeFlag("--json");
|
|
97
|
+
const origin = takeOption("--origin");
|
|
98
|
+
const noBrowser = takeFlag("--no-browser");
|
|
99
|
+
const local = takeFlag("--local");
|
|
100
|
+
const projectId = takeOption("--project", "-p");
|
|
101
|
+
const ask = takeOption("--ask", "-a");
|
|
102
|
+
const workspace = takeOption("--workspace");
|
|
103
|
+
const output = takeOption("--output");
|
|
104
|
+
if (args.some((arg) => typeof arg === "string" && arg.startsWith("-"))) {
|
|
105
|
+
throw new CliError("usage_invalid", "Invalid command grammar");
|
|
106
|
+
}
|
|
107
|
+
if (version) return { kind: "version" };
|
|
108
|
+
if (help) return { kind: "help" };
|
|
109
|
+
const extra = json || origin !== undefined || noBrowser || local
|
|
110
|
+
|| projectId !== undefined || ask !== undefined || workspace !== undefined || output !== undefined;
|
|
111
|
+
if (args.length === 0 && !extra) return { kind: "status" };
|
|
112
|
+
if (args.length === 1 && args[0] === "mcp" && !extra) return { kind: "mcp" };
|
|
113
|
+
if (args[0] === "project" && projectId === undefined && ask === undefined && output === undefined
|
|
114
|
+
&& !noBrowser && !local) {
|
|
115
|
+
if (args.length === 3 && args[1] === "create" && value(args[2])) {
|
|
116
|
+
return { kind: "project_create", name: args[2], workspace, origin, json };
|
|
117
|
+
}
|
|
118
|
+
if (workspace === undefined && args.length === 2 && args[1] === "list") {
|
|
119
|
+
return { kind: "project_list", origin, json };
|
|
120
|
+
}
|
|
121
|
+
if (workspace === undefined && args.length === 3 && args[1] === "use" && value(args[2])) {
|
|
122
|
+
return { kind: "project_use", name: args[2], origin, json };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (workspace !== undefined) throw new CliError("usage_invalid", "Invalid command grammar");
|
|
126
|
+
if (args.length === 1 && args[0] === "login" && !local
|
|
127
|
+
&& projectId === undefined && ask === undefined && output === undefined) {
|
|
128
|
+
return { kind: "login", origin, noBrowser, json };
|
|
129
|
+
}
|
|
130
|
+
if (args.length === 1 && args[0] === "logout" && !noBrowser
|
|
131
|
+
&& projectId === undefined && ask === undefined && output === undefined) {
|
|
132
|
+
return { kind: "logout", origin, local, json };
|
|
33
133
|
}
|
|
34
|
-
if (args.length === 1 && args[0] === "
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return { kind: "project_get", id: args[2], json };
|
|
134
|
+
if (args.length === 1 && args[0] === "whoami" && !noBrowser && !local
|
|
135
|
+
&& projectId === undefined && ask === undefined && output === undefined) {
|
|
136
|
+
return { kind: "whoami", origin, json };
|
|
38
137
|
}
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
138
|
+
if (noBrowser || local) throw new CliError("usage_invalid", "Invalid command grammar");
|
|
139
|
+
if (args.length === 3 && args[0] === "project" && args[1] === "get" && value(args[2])
|
|
140
|
+
&& projectId === undefined && ask === undefined && output === undefined) {
|
|
141
|
+
return { kind: "project_get", id: args[2], origin, json };
|
|
42
142
|
}
|
|
43
|
-
if (args.length ===
|
|
44
|
-
|
|
143
|
+
if (args.length === 2 && args[0] === "run" && args[1] === "submit"
|
|
144
|
+
&& (projectId === undefined || value(projectId)) && value(ask) && output === undefined) {
|
|
145
|
+
return { kind: "run_submit", projectId, ask, origin, json };
|
|
45
146
|
}
|
|
46
|
-
if (args.length === 3 && args[0] === "run" && args[1] === "
|
|
47
|
-
|
|
147
|
+
if (args.length === 3 && args[0] === "run" && args[1] === "get" && value(args[2])
|
|
148
|
+
&& projectId === undefined && ask === undefined && output === undefined) {
|
|
149
|
+
return { kind: "run_get", id: args[2], origin, json };
|
|
150
|
+
}
|
|
151
|
+
if (args.length === 3 && args[0] === "run" && args[1] === "events" && value(args[2])
|
|
152
|
+
&& projectId === undefined && ask === undefined && output === undefined) {
|
|
153
|
+
return { kind: "run_events", id: args[2], origin, json };
|
|
154
|
+
}
|
|
155
|
+
if (args.length === 3 && args[0] === "artifact" && args[1] === "get" && value(args[2])
|
|
156
|
+
&& ask === undefined) {
|
|
157
|
+
return { kind: "artifact_get", id: args[2], projectId, output, origin, json };
|
|
48
158
|
}
|
|
49
159
|
throw new CliError("usage_invalid", "Invalid command grammar");
|
|
50
160
|
}
|
|
51
161
|
|
|
162
|
+
/**
|
|
163
|
+
* `--origin` moves the STORE's current origin, and only the store path may do it.
|
|
164
|
+
*
|
|
165
|
+
* When the credential came from the environment there is no stored login for the named origin
|
|
166
|
+
* to become current, writing to the store is the one thing the CI path must not do, and reading
|
|
167
|
+
* it is how an unreadable file broke a run that never needed one. Both callers ask here rather
|
|
168
|
+
* than each remembering the rule, because one of them already forgot it.
|
|
169
|
+
*/
|
|
170
|
+
function rememberOrigin(flagOrigin, origin, env) {
|
|
171
|
+
if (flagOrigin && !env.VECTEUR_TOKEN) setCurrentOrigin(origin, env);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function writeLoggedOut(json, path) {
|
|
175
|
+
process.stdout.write(json
|
|
176
|
+
? canonicalJson({ logged_in: false, credentials: path })
|
|
177
|
+
: `logged out\ncredentials removed: ${path}\n`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function login(command, env = process.env) {
|
|
181
|
+
const origin = resolveOrigin(env, undefined, command.origin);
|
|
182
|
+
const timeout = requestTimeout(env);
|
|
183
|
+
const client = new VecteurClient({ origin, token: "", timeout });
|
|
184
|
+
const grant = await client.deviceMint();
|
|
185
|
+
const prompt = `Visit ${grant.verification_uri_complete}\nUser code: ${grant.user_code}\n`;
|
|
186
|
+
(command.json ? process.stderr : process.stdout).write(prompt);
|
|
187
|
+
if (shouldOpenBrowser(command.noBrowser, env)) openBrowser(grant.verification_uri_complete);
|
|
188
|
+
const created = await waitForDeviceAuthorization(client, grant);
|
|
189
|
+
if (!isTokenShaped(created.token)) {
|
|
190
|
+
throw new CliError("pat_created_invalid", "Server returned malformed owner data");
|
|
191
|
+
}
|
|
192
|
+
const previous = readCredentials(env)?.logins?.[origin]?.token;
|
|
193
|
+
if (previous) {
|
|
194
|
+
try {
|
|
195
|
+
await new VecteurClient({ origin, token: previous, timeout }).credentialSelfRevoke();
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (!(error instanceof CliError && error.code === "pat_revoked")) throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const path = writeCredentials({ origin, token: created.token }, env);
|
|
201
|
+
const account = await new VecteurClient({ origin, token: created.token, timeout }).accountMe();
|
|
202
|
+
process.stdout.write(command.json
|
|
203
|
+
? canonicalJson({ logged_in: true, origin, credentials: path, account })
|
|
204
|
+
: `logged in to ${origin}\n${renderAccount(account, origin)}credentials: ${path}\n`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function logout(command, env = process.env) {
|
|
208
|
+
const path = credentialsPath(env);
|
|
209
|
+
let saved;
|
|
210
|
+
try {
|
|
211
|
+
saved = readCredentials(env);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (!command.local) throw error;
|
|
214
|
+
clearCredentials(env);
|
|
215
|
+
writeLoggedOut(command.json, path);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!saved) {
|
|
219
|
+
writeLoggedOut(command.json, path);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const origin = resolveOrigin(env, saved, command.origin);
|
|
223
|
+
const token = saved.logins[origin]?.token;
|
|
224
|
+
if (!token) {
|
|
225
|
+
throw new CliError("origin_mismatch",
|
|
226
|
+
`the stored login is for ${saved.current} and ${command.origin !== undefined ? "--origin" : "VECTEUR_BASE_URL"} names `
|
|
227
|
+
+ `${origin} — set VECTEUR_TOKEN for that origin, or log in to it`);
|
|
228
|
+
}
|
|
229
|
+
if (!command.local) {
|
|
230
|
+
try {
|
|
231
|
+
await new VecteurClient({ origin, token, timeout: requestTimeout(env) }).credentialSelfRevoke();
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (error instanceof CliError && (error.code === "request_failed" || error.code === "request_timeout")) {
|
|
234
|
+
throw new CliError("logout_offline",
|
|
235
|
+
"logout requires the origin; pass --local to delete only this machine's store");
|
|
236
|
+
}
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
writeLoggedOut(command.json, clearCredentials(env, origin));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function renderAccount(account, origin) {
|
|
244
|
+
return `${account.display_name} <${account.email}> (${account.id}) at ${origin}\n`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function whoami(command, env = process.env) {
|
|
248
|
+
const config = configuration(env, undefined, command.origin);
|
|
249
|
+
rememberOrigin(command.origin, config.origin, env);
|
|
250
|
+
const account = await new VecteurClient(config).accountMe();
|
|
251
|
+
process.stdout.write(command.json
|
|
252
|
+
? canonicalJson(account)
|
|
253
|
+
: renderAccount(account, config.origin));
|
|
254
|
+
}
|
|
255
|
+
|
|
52
256
|
async function main() {
|
|
257
|
+
refuseOldNode();
|
|
53
258
|
const command = parseCommand(process.argv.slice(2));
|
|
259
|
+
if (command.kind === "version") {
|
|
260
|
+
process.stdout.write(`vecteur ${manifest().version}\n`);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (command.kind === "help") {
|
|
264
|
+
process.stdout.write(helpText(manifest().version));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (command.kind === "status") {
|
|
268
|
+
writeStatus();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (command.kind === "mcp") {
|
|
272
|
+
const { serveVecteurMcp } = await import("../src/mcp.js");
|
|
273
|
+
serveVecteurMcp();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
54
276
|
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// makes a first run impossible.
|
|
277
|
+
// Login and logout run BEFORE a client is constructed from configuration(): requiring a
|
|
278
|
+
// valid login in order to log in (or to delete a corrupt store locally) is the shape of
|
|
279
|
+
// bug that makes a first run or a recovery impossible. VECTEUR_TOKEN is the CI path and
|
|
280
|
+
// is never written or revoked here.
|
|
58
281
|
if (command.kind === "login") {
|
|
59
|
-
|
|
60
|
-
// Shape-check HERE so the refusal names what is wrong with what was just piped in. Falling
|
|
61
|
-
// straight through to `configuration` reported "no login found: run `vecteur login`" to a
|
|
62
|
-
// user in the middle of running exactly that.
|
|
63
|
-
if (!/^vct_[A-Za-z0-9_-]{43}$/.test(token)) {
|
|
64
|
-
throw new CliError("token_invalid",
|
|
65
|
-
`stdin held ${token.length} characters; a personal access token is \`vct_\` and 43 more`);
|
|
66
|
-
}
|
|
67
|
-
// Then through the SAME function every command resolves through, so an origin this CLI
|
|
68
|
-
// would later refuse is refused now, while the user is still holding the token — not on
|
|
69
|
-
// their next command with the credential already on disk.
|
|
70
|
-
const checked = configuration({ VECTEUR_TOKEN: token, VECTEUR_BASE_URL: command.origin }, null);
|
|
71
|
-
const path = writeCredentials({ origin: checked.origin, token }, process.env);
|
|
72
|
-
process.stdout.write(command.json
|
|
73
|
-
? canonicalJson({ logged_in: true, origin: checked.origin, credentials: path })
|
|
74
|
-
: `logged in to ${checked.origin}\ncredentials: ${path}\n`);
|
|
282
|
+
await login(command);
|
|
75
283
|
return;
|
|
76
284
|
}
|
|
77
285
|
if (command.kind === "logout") {
|
|
78
|
-
|
|
79
|
-
process.stdout.write(command.json
|
|
80
|
-
? canonicalJson({ logged_in: false, credentials: path })
|
|
81
|
-
: `logged out\ncredentials removed: ${path}\n`);
|
|
286
|
+
await logout(command);
|
|
82
287
|
return;
|
|
83
288
|
}
|
|
84
289
|
if (command.kind === "whoami") {
|
|
85
|
-
|
|
86
|
-
// NEVER the token, not even a prefix. A door that prints part of a credential teaches the
|
|
87
|
-
// habit of pasting it somewhere, and a prefix is enough to confirm a guess.
|
|
88
|
-
process.stdout.write(command.json
|
|
89
|
-
? canonicalJson({ logged_in: Boolean(saved), origin: saved?.origin ?? null })
|
|
90
|
-
: saved ? `logged in to ${saved.origin}\n` : "not logged in\n");
|
|
91
|
-
process.exitCode = saved ? 0 : 1;
|
|
290
|
+
await whoami(command);
|
|
92
291
|
return;
|
|
93
292
|
}
|
|
94
293
|
|
|
95
|
-
const
|
|
294
|
+
const config = configuration(process.env, undefined, command.origin);
|
|
295
|
+
rememberOrigin(command.origin, config.origin, process.env);
|
|
296
|
+
const client = new VecteurClient(config);
|
|
96
297
|
const onEvent = command.json ? null : (event) => process.stderr.write(progress(event));
|
|
97
298
|
let value;
|
|
98
|
-
|
|
299
|
+
let renderOptions = {};
|
|
300
|
+
if (command.kind === "project_list") {
|
|
301
|
+
const [account, projects] = await Promise.all([client.accountMe(), client.projectList()]);
|
|
302
|
+
value = { projects, active_projects: account.entitlements.active_projects,
|
|
303
|
+
selected_project_id: config.projectId ?? null };
|
|
304
|
+
process.stdout.write(command.json ? canonicalJson(value) : humanProjects(value));
|
|
305
|
+
return;
|
|
306
|
+
} else if (command.kind === "project_create") {
|
|
307
|
+
const account = await client.accountMe();
|
|
308
|
+
value = await client.projectCreate(command.name, resolveWorkspace(account, command.workspace));
|
|
309
|
+
} else if (command.kind === "project_use") {
|
|
310
|
+
if (process.env.VECTEUR_TOKEN) throw new CliError("persistent_login_required",
|
|
311
|
+
"project use requires a persistent login; unset VECTEUR_TOKEN and run `vecteur login`");
|
|
312
|
+
value = resolveNamed(await client.projectList(), command.name, "project");
|
|
313
|
+
selectProject(config.origin, value.id);
|
|
314
|
+
} else if (command.kind === "project_get") value = await client.projectGet(command.id);
|
|
315
|
+
else if (command.kind === "artifact_get") {
|
|
316
|
+
const projectId = await resolveRunProject(client, command.projectId);
|
|
317
|
+
value = await downloadArtifact(client, command.id, projectId, command.output);
|
|
318
|
+
process.stdout.write(command.json ? canonicalJson(value)
|
|
319
|
+
: `Downloaded ${value.bytes} bytes to ${value.path} (${value.content_type})\n`);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
99
322
|
else if (command.kind === "run_submit") {
|
|
100
|
-
|
|
323
|
+
const projectId = await resolveRunProject(client, command.projectId);
|
|
324
|
+
value = await client.runSubmit(projectId, command.ask, onEvent);
|
|
325
|
+
if (!command.json) {
|
|
326
|
+
process.stdout.write(human(value, { projectId }));
|
|
327
|
+
try {
|
|
328
|
+
const cost = await client.customerCost(projectId);
|
|
329
|
+
process.stdout.write(`\n${humanCost(cost)}`);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if (error instanceof CliError && error.code === "pat_expired") {
|
|
332
|
+
throw new CliError("pat_expired",
|
|
333
|
+
`credential expired after Run ${value.run_id} was admitted; next: vecteur login; then: vecteur run get ${value.run_id}`);
|
|
334
|
+
}
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
101
339
|
} else if (command.kind === "run_get") value = await client.runGet(command.id);
|
|
102
340
|
else value = await client.runEvents(command.id, onEvent);
|
|
103
|
-
|
|
341
|
+
if (!command.json && !renderOptions.projectId && config.projectId) {
|
|
342
|
+
renderOptions = { projectId: config.projectId };
|
|
343
|
+
}
|
|
344
|
+
process.stdout.write(command.json ? canonicalJson(value) : human(value, renderOptions));
|
|
104
345
|
}
|
|
105
346
|
|
|
106
347
|
main().catch((error) => {
|
package/package.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vecteur/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
|
-
"
|
|
7
|
-
"vecteur": "bin/vecteur.js",
|
|
8
|
-
"vecteur-mcp": "bin/vecteur-mcp.js"
|
|
6
|
+
"vecteur": "bin/vecteur.js"
|
|
9
7
|
},
|
|
10
8
|
"files": [
|
|
11
|
-
"
|
|
9
|
+
"README.md",
|
|
12
10
|
"bin/vecteur.js",
|
|
13
11
|
"src/client.js",
|
|
14
12
|
"src/contract.js",
|
|
15
13
|
"src/credentials.js",
|
|
14
|
+
"src/download.js",
|
|
16
15
|
"src/errors.js",
|
|
17
16
|
"src/format.js",
|
|
18
|
-
"src/mcp.js"
|
|
17
|
+
"src/mcp.js",
|
|
18
|
+
"src/projects.js"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
21
|
"test": "node --test tests/*.test.js",
|