@vendian/cli 0.0.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/LICENSE +17 -0
- package/README.md +51 -0
- package/bin/vendian.js +8 -0
- package/package.json +26 -0
- package/src/auth.js +239 -0
- package/src/config.js +40 -0
- package/src/constants.js +15 -0
- package/src/doctor.js +23 -0
- package/src/forward.js +47 -0
- package/src/install.js +105 -0
- package/src/main.js +80 -0
- package/src/paths.js +43 -0
- package/src/process.js +50 -0
- package/src/prompt.js +38 -0
- package/src/python.js +77 -0
- package/src/secret-store.js +94 -0
- package/src/setup.js +121 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Copyright (c) 2026 Vendian. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software and its associated source code, package contents, documentation,
|
|
4
|
+
and generated artifacts are proprietary to Vendian.
|
|
5
|
+
|
|
6
|
+
No license is granted to copy, modify, merge, publish, distribute, sublicense,
|
|
7
|
+
sell, reverse engineer, decompile, disassemble, or create derivative works from
|
|
8
|
+
this software, except as expressly permitted in a separate written agreement
|
|
9
|
+
with Vendian.
|
|
10
|
+
|
|
11
|
+
Installation and use of this package are permitted only for accessing Vendian
|
|
12
|
+
services and running Vendian-authorized workflows. Any other use is prohibited.
|
|
13
|
+
|
|
14
|
+
This software is provided "as is" without warranty of any kind, express or
|
|
15
|
+
implied, including but not limited to warranties of merchantability, fitness for
|
|
16
|
+
a particular purpose, and noninfringement. Vendian is not liable for any claim,
|
|
17
|
+
damages, or other liability arising from use of this software.
|
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Vendian CLI
|
|
2
|
+
|
|
3
|
+
Command-line tools for signing in to Vendian and running local agent workflows.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install -g @vendian/cli
|
|
7
|
+
vendian login
|
|
8
|
+
vendian cloud local serve --agents-dir ./agents
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
vendian login
|
|
15
|
+
vendian doctor
|
|
16
|
+
vendian update
|
|
17
|
+
vendian cloud local serve --agents-dir ./agents
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`vendian login` opens a browser sign-in, prepares the local runtime, and stores
|
|
21
|
+
the credentials needed by the CLI. `vendian setup` is kept as an alias for
|
|
22
|
+
existing users.
|
|
23
|
+
|
|
24
|
+
`vendian doctor` checks the local installation. `vendian update` refreshes the
|
|
25
|
+
managed runtime without changing your agents.
|
|
26
|
+
|
|
27
|
+
After login, regular `vendian cloud ...` commands are forwarded to the managed
|
|
28
|
+
runtime installed by the CLI.
|
|
29
|
+
|
|
30
|
+
## Local Data
|
|
31
|
+
|
|
32
|
+
Vendian stores its managed runtime and CLI state under your user profile:
|
|
33
|
+
|
|
34
|
+
- Windows: `%LOCALAPPDATA%\Vendian\cli`
|
|
35
|
+
- macOS/Linux: `~/.vendian/cli`
|
|
36
|
+
|
|
37
|
+
To use a different location:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
VENDIAN_CLI_HOME=/path/to/vendian-cli vendian login
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Agent Dependencies
|
|
44
|
+
|
|
45
|
+
Keep agent-specific packages in each agent's `requirements.txt`. The Vendian
|
|
46
|
+
CLI manages the Vendian runtime separately so agent dependencies stay focused on
|
|
47
|
+
the agent's own code.
|
|
48
|
+
|
|
49
|
+
## License
|
|
50
|
+
|
|
51
|
+
Proprietary. All rights reserved. See `LICENSE`.
|
package/bin/vendian.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { main } from '../src/main.js';
|
|
3
|
+
|
|
4
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
5
|
+
const message = error && typeof error.message === 'string' ? error.message : String(error);
|
|
6
|
+
console.error(`[vendian] ${message}`);
|
|
7
|
+
process.exitCode = 1;
|
|
8
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vendian/cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Public Vendian CLI bootstrapper and launcher",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"private": false,
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public",
|
|
9
|
+
"registry": "https://registry.npmjs.org/"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"bin": {
|
|
13
|
+
"vendian": "bin/vendian.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin/",
|
|
17
|
+
"src/",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node --test"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.18.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { BACKEND_TARGETS } from './constants.js';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_OAUTH_REDIRECT_PORT = 8765;
|
|
9
|
+
|
|
10
|
+
export function resolveApiUrl({ backend, apiUrl, env = process.env } = {}) {
|
|
11
|
+
if (apiUrl) {
|
|
12
|
+
return apiUrl.replace(/\/$/, '');
|
|
13
|
+
}
|
|
14
|
+
if (env.VENDIAN_API_URL) {
|
|
15
|
+
return env.VENDIAN_API_URL.replace(/\/$/, '');
|
|
16
|
+
}
|
|
17
|
+
const target = backend || 'prod';
|
|
18
|
+
const resolved = BACKEND_TARGETS[target];
|
|
19
|
+
if (!resolved) {
|
|
20
|
+
throw new Error(`Unknown Vendian backend '${target}'. Use local, dev, staging, prod, or --api-url.`);
|
|
21
|
+
}
|
|
22
|
+
return resolved;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function loginWithVendianOAuth({ backend, apiUrl, noBrowser = false, env = process.env } = {}) {
|
|
26
|
+
const resolvedApiUrl = resolveApiUrl({ backend, apiUrl, env });
|
|
27
|
+
const callback = await createCallbackServer(env);
|
|
28
|
+
try {
|
|
29
|
+
const config = await getOAuthConfig(resolvedApiUrl, callback.redirectUri);
|
|
30
|
+
const codeVerifier = crypto.randomBytes(48).toString('base64url');
|
|
31
|
+
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
|
32
|
+
const state = crypto.randomBytes(18).toString('base64url');
|
|
33
|
+
const authorizationUrl = authorizationUrlFor(config, { state, codeChallenge });
|
|
34
|
+
|
|
35
|
+
if (noBrowser) {
|
|
36
|
+
console.error(`Open this URL to authenticate the CLI: ${authorizationUrl}`);
|
|
37
|
+
} else {
|
|
38
|
+
openBrowser(authorizationUrl);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const callbackResult = await callback.wait();
|
|
42
|
+
if (callbackResult.state !== state) {
|
|
43
|
+
throw new Error('OAuth state mismatch');
|
|
44
|
+
}
|
|
45
|
+
const clerkToken = await exchangeCodeForClerkToken(config, callbackResult.code, codeVerifier, callback.redirectUri);
|
|
46
|
+
const exchange = await postJson(`${resolvedApiUrl}/api/v1/cli/auth/oauth/exchange`, {
|
|
47
|
+
clerkToken
|
|
48
|
+
});
|
|
49
|
+
return { apiUrl: resolvedApiUrl, ...exchange };
|
|
50
|
+
} finally {
|
|
51
|
+
await callback.close();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function getOAuthConfig(apiUrl, redirectUri) {
|
|
56
|
+
const url = new URL(`${apiUrl}/api/v1/cli/auth/oauth/config`);
|
|
57
|
+
url.searchParams.set('redirectUri', redirectUri);
|
|
58
|
+
return getJson(url);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function authorizationUrlFor(config, { state, codeChallenge }) {
|
|
62
|
+
const url = new URL(String(config.authorizationUrl));
|
|
63
|
+
url.searchParams.set('response_type', 'code');
|
|
64
|
+
url.searchParams.set('client_id', String(config.clientId));
|
|
65
|
+
url.searchParams.set('redirect_uri', String(config.redirectUri));
|
|
66
|
+
url.searchParams.set('scope', String(config.scopes || 'email profile'));
|
|
67
|
+
url.searchParams.set('state', state);
|
|
68
|
+
url.searchParams.set('code_challenge', codeChallenge);
|
|
69
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
70
|
+
return url.toString();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function exchangeCodeForClerkToken(config, code, codeVerifier, redirectUri) {
|
|
74
|
+
const body = new URLSearchParams({
|
|
75
|
+
grant_type: 'authorization_code',
|
|
76
|
+
client_id: String(config.clientId),
|
|
77
|
+
code,
|
|
78
|
+
redirect_uri: redirectUri,
|
|
79
|
+
code_verifier: codeVerifier
|
|
80
|
+
});
|
|
81
|
+
const payload = await postForm(String(config.tokenUrl), body);
|
|
82
|
+
const token = payload.access_token || payload.id_token;
|
|
83
|
+
if (!token) {
|
|
84
|
+
throw new Error('OAuth token exchange did not return a Clerk token');
|
|
85
|
+
}
|
|
86
|
+
return String(token);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function getJson(url) {
|
|
90
|
+
const response = await fetch(url, { headers: { Accept: 'application/json' } });
|
|
91
|
+
return decodeResponse(response);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function postJson(url, body) {
|
|
95
|
+
const response = await fetch(url, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
|
98
|
+
body: JSON.stringify(body)
|
|
99
|
+
});
|
|
100
|
+
return decodeResponse(response);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function postForm(url, body) {
|
|
104
|
+
const response = await fetch(url, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
107
|
+
body
|
|
108
|
+
});
|
|
109
|
+
return decodeResponse(response);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function decodeResponse(response) {
|
|
113
|
+
const text = await response.text();
|
|
114
|
+
let payload = {};
|
|
115
|
+
if (text) {
|
|
116
|
+
try {
|
|
117
|
+
payload = JSON.parse(text);
|
|
118
|
+
} catch {
|
|
119
|
+
payload = { error: text };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
throw new Error(payload.message || payload.error || `HTTP ${response.status}`);
|
|
124
|
+
}
|
|
125
|
+
return payload;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function createCallbackServer(env) {
|
|
129
|
+
const port = Number(env.VENDIAN_CLI_OAUTH_REDIRECT_PORT || DEFAULT_OAUTH_REDIRECT_PORT);
|
|
130
|
+
let server;
|
|
131
|
+
let settled = false;
|
|
132
|
+
let timeout;
|
|
133
|
+
const waitPromise = new Promise((resolve, reject) => {
|
|
134
|
+
timeout = setTimeout(() => {
|
|
135
|
+
settled = true;
|
|
136
|
+
reject(new Error('OAuth login timed out before callback completed'));
|
|
137
|
+
}, 300_000);
|
|
138
|
+
server = http.createServer((req, res) => {
|
|
139
|
+
const url = new URL(req.url || '/', `http://${req.headers.host || '127.0.0.1'}`);
|
|
140
|
+
const code = url.searchParams.get('code');
|
|
141
|
+
const state = url.searchParams.get('state');
|
|
142
|
+
const error = url.searchParams.get('error');
|
|
143
|
+
const body = code
|
|
144
|
+
? 'Vendian CLI authentication complete. You can close this window.'
|
|
145
|
+
: 'Vendian CLI authentication failed. Return to the terminal.';
|
|
146
|
+
res.writeHead(code ? 200 : 400, {
|
|
147
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
148
|
+
'Content-Length': Buffer.byteLength(body)
|
|
149
|
+
});
|
|
150
|
+
res.end(body);
|
|
151
|
+
clearTimeout(timeout);
|
|
152
|
+
if (settled) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
settled = true;
|
|
156
|
+
if (error) {
|
|
157
|
+
reject(new Error(`OAuth login failed: ${error}`));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (!code) {
|
|
161
|
+
reject(new Error('OAuth callback did not include an authorization code'));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
resolve({ code, state });
|
|
165
|
+
});
|
|
166
|
+
server.on('error', reject);
|
|
167
|
+
server.listen(port, '127.0.0.1');
|
|
168
|
+
});
|
|
169
|
+
await new Promise((resolve, reject) => {
|
|
170
|
+
server.once('listening', resolve);
|
|
171
|
+
server.once('error', reject);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
redirectUri: `http://127.0.0.1:${server.address().port}/callback`,
|
|
176
|
+
wait: () => waitPromise,
|
|
177
|
+
close: () => new Promise((resolve) => server.close(resolve))
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function openBrowser(url) {
|
|
182
|
+
const platform = process.platform;
|
|
183
|
+
if (platform === 'win32') {
|
|
184
|
+
spawnSync(...buildWindowsOpenCommand(url), { stdio: 'ignore', shell: false });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (platform === 'darwin') {
|
|
188
|
+
spawnSync('open', [url], { stdio: 'ignore', shell: false });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
spawnSync('xdg-open', [url], { stdio: 'ignore', shell: false });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function buildWindowsOpenCommand(url) {
|
|
195
|
+
return ['rundll32.exe', ['url.dll,FileProtocolHandler', String(url)]];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function cloudConfigPath(env = process.env, platform = process.platform) {
|
|
199
|
+
if (env.VENDIAN_CLOUD_CONFIG) {
|
|
200
|
+
return path.resolve(env.VENDIAN_CLOUD_CONFIG);
|
|
201
|
+
}
|
|
202
|
+
if (platform === 'win32') {
|
|
203
|
+
const root = env.APPDATA || path.join(process.env.USERPROFILE || '', 'AppData', 'Roaming');
|
|
204
|
+
return path.win32.join(root, 'Vendian', 'cloud-auth.json');
|
|
205
|
+
}
|
|
206
|
+
const root = env.XDG_CONFIG_HOME || path.join(process.env.HOME || '', '.config');
|
|
207
|
+
return path.posix.join(root, 'vendian', 'cloud-auth.json');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function saveCloudToken(token, { env = process.env, platform = process.platform } = {}) {
|
|
211
|
+
const file = cloudConfigPath(env, platform);
|
|
212
|
+
let raw = { version: 2, profiles: {}, active_api_url: token.apiUrl };
|
|
213
|
+
try {
|
|
214
|
+
const existing = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
215
|
+
if (existing && typeof existing === 'object' && existing.profiles && typeof existing.profiles === 'object') {
|
|
216
|
+
raw.profiles = existing.profiles;
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
// Missing or invalid config is replaced.
|
|
220
|
+
}
|
|
221
|
+
raw.active_api_url = token.apiUrl;
|
|
222
|
+
raw.profiles[token.apiUrl] = {
|
|
223
|
+
api_url: token.apiUrl,
|
|
224
|
+
access_token: token.accessToken,
|
|
225
|
+
user_id: token.userId,
|
|
226
|
+
email: token.email,
|
|
227
|
+
expires_at: token.expiresAt,
|
|
228
|
+
scopes: token.scopes,
|
|
229
|
+
tooling_eligible: token.toolingEligible
|
|
230
|
+
};
|
|
231
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
232
|
+
fs.writeFileSync(file, `${JSON.stringify(raw, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
233
|
+
try {
|
|
234
|
+
fs.chmodSync(file, 0o600);
|
|
235
|
+
} catch {
|
|
236
|
+
// Best effort on Windows filesystems.
|
|
237
|
+
}
|
|
238
|
+
return file;
|
|
239
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { CONFIG_VERSION } from './constants.js';
|
|
4
|
+
import { configPath } from './paths.js';
|
|
5
|
+
|
|
6
|
+
export function loadConfig(env = process.env, platform = process.platform) {
|
|
7
|
+
const file = configPath(env, platform);
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
10
|
+
return typeof parsed === 'object' && parsed !== null ? parsed : {};
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (error && error.code === 'ENOENT') {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function saveConfig(config, env = process.env, platform = process.platform) {
|
|
20
|
+
const file = configPath(env, platform);
|
|
21
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
22
|
+
const next = { version: CONFIG_VERSION, ...config };
|
|
23
|
+
fs.writeFileSync(file, `${JSON.stringify(next, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
24
|
+
try {
|
|
25
|
+
fs.chmodSync(file, 0o600);
|
|
26
|
+
} catch {
|
|
27
|
+
// Best effort on Windows filesystems.
|
|
28
|
+
}
|
|
29
|
+
return file;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function redact(value) {
|
|
33
|
+
if (!value || typeof value !== 'string') {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
if (value.length <= 8) {
|
|
37
|
+
return '********';
|
|
38
|
+
}
|
|
39
|
+
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
40
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const DEFAULT_GITLAB_HOST = 'gitlab.com';
|
|
2
|
+
export const DEFAULT_SDK_PUBLIC_PROJECT_ID = '77150592';
|
|
3
|
+
export const DEFAULT_SDK_RUNTIME_PROJECT_ID = '79410876';
|
|
4
|
+
export const PUBLIC_PACKAGE = 'vendian-agents[full]';
|
|
5
|
+
export const RUNTIME_PACKAGE = 'vendian-agents-runtime';
|
|
6
|
+
export const CONFIG_VERSION = 1;
|
|
7
|
+
|
|
8
|
+
export const BACKEND_TARGETS = {
|
|
9
|
+
local: 'http://localhost:3000',
|
|
10
|
+
localhost: 'http://localhost:3000',
|
|
11
|
+
dev: 'https://api.dev.vendian.ai',
|
|
12
|
+
staging: 'https://api.staging.vendian.ai',
|
|
13
|
+
prod: 'https://api.vendian.ai',
|
|
14
|
+
production: 'https://api.vendian.ai'
|
|
15
|
+
};
|
package/src/doctor.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { loadConfig, redact } from './config.js';
|
|
3
|
+
import { registryConfig } from './install.js';
|
|
4
|
+
import { managedVenvPath, venvPython, venvVendian } from './paths.js';
|
|
5
|
+
import { findPython, hasUv, pythonVersion, verifyVendianImports } from './python.js';
|
|
6
|
+
|
|
7
|
+
export function doctor({ env = process.env, platform = process.platform } = {}) {
|
|
8
|
+
const config = loadConfig(env, platform);
|
|
9
|
+
const venvPath = managedVenvPath(env, platform);
|
|
10
|
+
const pythonPath = venvPython(venvPath, platform);
|
|
11
|
+
const vendianPath = venvVendian(venvPath, platform);
|
|
12
|
+
const registry = registryConfig(config, env, platform);
|
|
13
|
+
|
|
14
|
+
console.log('Vendian doctor');
|
|
15
|
+
console.log(`Home: ${venvPath}`);
|
|
16
|
+
console.log(`System Python 3.11+: ${findPython(platform) ? 'yes' : 'no'}`);
|
|
17
|
+
console.log(`uv: ${hasUv() ? 'yes' : 'no, will use pip fallback'}`);
|
|
18
|
+
console.log(`Managed Python: ${fs.existsSync(pythonPath) ? pythonVersion(pythonPath) || 'present' : 'missing'}`);
|
|
19
|
+
console.log(`Vendian executable: ${fs.existsSync(vendianPath) ? vendianPath : 'missing'}`);
|
|
20
|
+
console.log(`Vendian imports: ${fs.existsSync(pythonPath) && verifyVendianImports(pythonPath) ? 'ok' : 'missing'}`);
|
|
21
|
+
console.log(`GitLab host: ${registry.gitlabHost}`);
|
|
22
|
+
console.log(`Package token: ${registry.token ? `${redact(registry.token)} (${registry.tokenSource})` : 'missing'}`);
|
|
23
|
+
}
|
package/src/forward.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { loadConfig, saveConfig } from './config.js';
|
|
3
|
+
import { installVendianPackages, registryConfig } from './install.js';
|
|
4
|
+
import { managedVenvPath, venvPython, venvVendian } from './paths.js';
|
|
5
|
+
import { spawnForward } from './process.js';
|
|
6
|
+
|
|
7
|
+
const AUTO_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
export async function forwardToPythonVendian(args, { env = process.env, platform = process.platform } = {}) {
|
|
10
|
+
const venvPath = managedVenvPath(env, platform);
|
|
11
|
+
const vendianPath = venvVendian(venvPath, platform);
|
|
12
|
+
if (!fs.existsSync(vendianPath)) {
|
|
13
|
+
throw new Error('Vendian is not set up yet. Run `vendian login` first.');
|
|
14
|
+
}
|
|
15
|
+
maybeAutoUpdateManagedEnv({ env, platform, venvPath });
|
|
16
|
+
const code = await spawnForward(vendianPath, args, { env });
|
|
17
|
+
process.exitCode = code;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function maybeAutoUpdateManagedEnv({ env = process.env, platform = process.platform, venvPath = managedVenvPath(env, platform) } = {}) {
|
|
21
|
+
if (env.VENDIAN_SKIP_AUTO_UPDATE === '1') {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const config = loadConfig(env, platform);
|
|
25
|
+
const lastUpdate = Date.parse(config.lastManagedUpdateAt || '');
|
|
26
|
+
if (Number.isFinite(lastUpdate) && Date.now() - lastUpdate < AUTO_UPDATE_INTERVAL_MS) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const registry = registryConfig(config, env);
|
|
30
|
+
if (!registry.token) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const pythonPath = venvPython(venvPath, platform);
|
|
34
|
+
if (!fs.existsSync(pythonPath)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
console.error('[vendian] Checking managed CLI/runtime updates...');
|
|
39
|
+
installVendianPackages({ pythonPath, venvPath, config, env });
|
|
40
|
+
saveConfig({ ...config, lastManagedUpdateAt: new Date().toISOString() }, env, platform);
|
|
41
|
+
return true;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const message = error && typeof error.message === 'string' ? error.message : String(error);
|
|
44
|
+
console.error(`[vendian] Update check failed; continuing with installed CLI. ${message}`);
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/install.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_GITLAB_HOST,
|
|
5
|
+
DEFAULT_SDK_PUBLIC_PROJECT_ID,
|
|
6
|
+
DEFAULT_SDK_RUNTIME_PROJECT_ID,
|
|
7
|
+
PUBLIC_PACKAGE,
|
|
8
|
+
RUNTIME_PACKAGE
|
|
9
|
+
} from './constants.js';
|
|
10
|
+
import { runInherit } from './process.js';
|
|
11
|
+
import { hasUv } from './python.js';
|
|
12
|
+
import { loadPackageTokenSecret } from './secret-store.js';
|
|
13
|
+
|
|
14
|
+
export function registryConfig(config = {}, env = process.env, platform = process.platform) {
|
|
15
|
+
const gitlabHost = env.GITLAB_HOST || config.gitlabHost || DEFAULT_GITLAB_HOST;
|
|
16
|
+
const username = env.GITLAB_USERNAME || config.gitlabUsername || '__token__';
|
|
17
|
+
const sdkProjectId = env.SDK_PUBLIC_PROJECT_ID || config.sdkPublicProjectId || DEFAULT_SDK_PUBLIC_PROJECT_ID;
|
|
18
|
+
const runtimeProjectId = env.SDK_RUNTIME_PROJECT_ID || config.sdkRuntimeProjectId || DEFAULT_SDK_RUNTIME_PROJECT_ID;
|
|
19
|
+
const secretToken = loadPackageTokenSecret(config, platform);
|
|
20
|
+
const token = env.GITLAB_TOKEN || config.gitlabToken || secretToken || '';
|
|
21
|
+
const tokenSource = env.GITLAB_TOKEN ? 'env' : config.gitlabToken ? 'config' : secretToken ? 'secret-store' : 'missing';
|
|
22
|
+
const vendianAgentsVersion = env.VENDIAN_AGENTS_VERSION || config.vendianAgentsVersion || '';
|
|
23
|
+
const vendianRuntimeVersion = env.VENDIAN_AGENTS_RUNTIME_VERSION || config.vendianAgentsRuntimeVersion || '';
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
gitlabHost,
|
|
27
|
+
username,
|
|
28
|
+
sdkProjectId,
|
|
29
|
+
runtimeProjectId,
|
|
30
|
+
token,
|
|
31
|
+
tokenSource,
|
|
32
|
+
vendianAgentsVersion,
|
|
33
|
+
vendianRuntimeVersion
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildIndexUrls(registry) {
|
|
38
|
+
if (!registry.token) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const username = encodeURIComponent(registry.username || '__token__');
|
|
42
|
+
const token = encodeURIComponent(registry.token);
|
|
43
|
+
const sdkIndexUrl = `https://${username}:${token}@${registry.gitlabHost}/api/v4/projects/${registry.sdkProjectId}/packages/pypi/simple`;
|
|
44
|
+
const runtimeIndexUrl = `https://${username}:${token}@${registry.gitlabHost}/api/v4/projects/${registry.runtimeProjectId}/packages/pypi/simple`;
|
|
45
|
+
return { sdkIndexUrl, runtimeIndexUrl };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function packageSpecs(registry) {
|
|
49
|
+
const publicSpec = registry.vendianAgentsVersion
|
|
50
|
+
? `${PUBLIC_PACKAGE}==${registry.vendianAgentsVersion}`
|
|
51
|
+
: PUBLIC_PACKAGE;
|
|
52
|
+
const runtimeSpec = registry.vendianRuntimeVersion
|
|
53
|
+
? `${RUNTIME_PACKAGE}==${registry.vendianRuntimeVersion}`
|
|
54
|
+
: RUNTIME_PACKAGE;
|
|
55
|
+
return [publicSpec, runtimeSpec];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function installVendianPackages({ pythonPath, venvPath, config, env = process.env, platform = process.platform }) {
|
|
59
|
+
const registry = registryConfig(config, env, platform);
|
|
60
|
+
const indexes = buildIndexUrls(registry);
|
|
61
|
+
if (!indexes) {
|
|
62
|
+
throw new Error('A GitLab package token is required until backend-issued package tokens are available. Run `vendian setup` or set GITLAB_TOKEN.');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const specs = packageSpecs(registry);
|
|
66
|
+
const installArgs = [
|
|
67
|
+
'install',
|
|
68
|
+
'--upgrade',
|
|
69
|
+
...specs,
|
|
70
|
+
'--index-url',
|
|
71
|
+
indexes.sdkIndexUrl,
|
|
72
|
+
'--extra-index-url',
|
|
73
|
+
indexes.runtimeIndexUrl,
|
|
74
|
+
'--extra-index-url',
|
|
75
|
+
'https://pypi.org/simple'
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
let status;
|
|
79
|
+
if (hasUv()) {
|
|
80
|
+
status = runInherit('uv', ['pip', ...installArgs, '--python', pythonPath]);
|
|
81
|
+
} else {
|
|
82
|
+
status = runInherit(pythonPath, ['-m', 'pip', ...installArgs]);
|
|
83
|
+
}
|
|
84
|
+
if (status !== 0) {
|
|
85
|
+
throw new Error('Could not install Vendian SDK/runtime packages. Check token access and package versions.');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
writeInstallMarker(venvPath, {
|
|
89
|
+
gitlabHost: registry.gitlabHost,
|
|
90
|
+
gitlabUsername: registry.username,
|
|
91
|
+
sdkProjectId: registry.sdkProjectId,
|
|
92
|
+
runtimeProjectId: registry.runtimeProjectId,
|
|
93
|
+
vendianAgentsVersion: registry.vendianAgentsVersion || 'latest',
|
|
94
|
+
vendianAgentsRuntimeVersion: registry.vendianRuntimeVersion || 'latest'
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function writeInstallMarker(venvPath, marker) {
|
|
99
|
+
fs.mkdirSync(venvPath, { recursive: true });
|
|
100
|
+
fs.writeFileSync(
|
|
101
|
+
path.join(venvPath, '.vendian-bootstrap.json'),
|
|
102
|
+
`${JSON.stringify({ version: 1, ...marker }, null, 2)}\n`,
|
|
103
|
+
'utf8'
|
|
104
|
+
);
|
|
105
|
+
}
|
package/src/main.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { doctor } from './doctor.js';
|
|
2
|
+
import { forwardToPythonVendian } from './forward.js';
|
|
3
|
+
import { setup } from './setup.js';
|
|
4
|
+
|
|
5
|
+
function printHelp() {
|
|
6
|
+
console.log(`Vendian CLI bootstrapper
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
vendian login Sign in and install the private Vendian CLI/runtime
|
|
10
|
+
vendian setup Alias for vendian login
|
|
11
|
+
vendian doctor Check local bootstrap health
|
|
12
|
+
vendian update Update the managed Vendian CLI/runtime
|
|
13
|
+
vendian <command> Forward to the managed Python Vendian CLI
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
vendian login
|
|
17
|
+
vendian login --backend prod
|
|
18
|
+
vendian cloud local serve --agents-dir ./agents
|
|
19
|
+
|
|
20
|
+
Environment:
|
|
21
|
+
VENDIAN_CLI_HOME Override managed CLI home
|
|
22
|
+
VENDIAN_API_URL Override Vendian backend API URL
|
|
23
|
+
GITLAB_TOKEN Local-dev package registry token fallback
|
|
24
|
+
GITLAB_USERNAME Local-dev package registry username fallback
|
|
25
|
+
GITLAB_HOST Defaults to gitlab.com
|
|
26
|
+
SDK_PUBLIC_PROJECT_ID Defaults to Vendian SDK project
|
|
27
|
+
SDK_RUNTIME_PROJECT_ID Defaults to Vendian runtime project
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function main(argv) {
|
|
32
|
+
const [command, ...rest] = argv;
|
|
33
|
+
if (!command || command === '--help' || command === '-h') {
|
|
34
|
+
printHelp();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (command === '--version' || command === 'version') {
|
|
38
|
+
console.log('0.1.0');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (isBootstrapCommand(command)) {
|
|
42
|
+
await setup(parseSetupOptions(rest));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (command === 'doctor') {
|
|
46
|
+
doctor();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (command === 'update') {
|
|
50
|
+
await setup({ nonInteractive: true });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
await forwardToPythonVendian(argv);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isBootstrapCommand(command) {
|
|
57
|
+
return command === 'login' || command === 'setup';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseSetupOptions(args) {
|
|
61
|
+
const options = {
|
|
62
|
+
nonInteractive: args.includes('--yes') || args.includes('--non-interactive'),
|
|
63
|
+
noBrowser: args.includes('--no-browser')
|
|
64
|
+
};
|
|
65
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
66
|
+
const arg = args[index];
|
|
67
|
+
if (arg === '--backend' || arg === '-b') {
|
|
68
|
+
options.backend = args[index + 1];
|
|
69
|
+
index += 1;
|
|
70
|
+
} else if (arg.startsWith('--backend=')) {
|
|
71
|
+
options.backend = arg.slice('--backend='.length);
|
|
72
|
+
} else if (arg === '--api-url') {
|
|
73
|
+
options.apiUrl = args[index + 1];
|
|
74
|
+
index += 1;
|
|
75
|
+
} else if (arg.startsWith('--api-url=')) {
|
|
76
|
+
options.apiUrl = arg.slice('--api-url='.length);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return options;
|
|
80
|
+
}
|
package/src/paths.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
function pathApi(platform) {
|
|
5
|
+
return platform === 'win32' ? path.win32 : path.posix;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function vendianHome(env = process.env, platform = process.platform) {
|
|
9
|
+
if (env.VENDIAN_CLI_HOME) {
|
|
10
|
+
return pathApi(platform).resolve(env.VENDIAN_CLI_HOME);
|
|
11
|
+
}
|
|
12
|
+
if (platform === 'win32') {
|
|
13
|
+
const root = env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
14
|
+
return path.win32.join(root, 'Vendian', 'cli');
|
|
15
|
+
}
|
|
16
|
+
return path.posix.join(os.homedir(), '.vendian', 'cli');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function configPath(env = process.env, platform = process.platform) {
|
|
20
|
+
if (env.VENDIAN_CLI_CONFIG) {
|
|
21
|
+
return pathApi(platform).resolve(env.VENDIAN_CLI_CONFIG);
|
|
22
|
+
}
|
|
23
|
+
return pathApi(platform).join(vendianHome(env, platform), 'config.json');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function managedVenvPath(env = process.env, platform = process.platform) {
|
|
27
|
+
if (env.VENDIAN_CLI_VENV) {
|
|
28
|
+
return pathApi(platform).resolve(env.VENDIAN_CLI_VENV);
|
|
29
|
+
}
|
|
30
|
+
return pathApi(platform).join(vendianHome(env, platform), '.venv');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function venvPython(venvPath, platform = process.platform) {
|
|
34
|
+
return platform === 'win32'
|
|
35
|
+
? path.win32.join(venvPath, 'Scripts', 'python.exe')
|
|
36
|
+
: path.posix.join(venvPath, 'bin', 'python');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function venvVendian(venvPath, platform = process.platform) {
|
|
40
|
+
return platform === 'win32'
|
|
41
|
+
? path.win32.join(venvPath, 'Scripts', 'vendian.exe')
|
|
42
|
+
: path.posix.join(venvPath, 'bin', 'vendian');
|
|
43
|
+
}
|
package/src/process.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export function commandExists(command) {
|
|
4
|
+
const result = spawnSync(command, ['--version'], { stdio: 'ignore', shell: false });
|
|
5
|
+
return result.status === 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function runCapture(command, args, options = {}) {
|
|
9
|
+
const result = spawnSync(command, args, {
|
|
10
|
+
encoding: 'utf8',
|
|
11
|
+
shell: false,
|
|
12
|
+
...options
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
ok: result.status === 0,
|
|
16
|
+
status: result.status ?? 1,
|
|
17
|
+
stdout: result.stdout || '',
|
|
18
|
+
stderr: result.stderr || ''
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function runInherit(command, args, options = {}) {
|
|
23
|
+
const result = spawnSync(command, args, {
|
|
24
|
+
stdio: 'inherit',
|
|
25
|
+
shell: false,
|
|
26
|
+
...options
|
|
27
|
+
});
|
|
28
|
+
if (result.error) {
|
|
29
|
+
throw result.error;
|
|
30
|
+
}
|
|
31
|
+
return result.status ?? 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function spawnForward(command, args, options = {}) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const child = spawn(command, args, {
|
|
37
|
+
stdio: 'inherit',
|
|
38
|
+
shell: false,
|
|
39
|
+
...options
|
|
40
|
+
});
|
|
41
|
+
child.on('error', reject);
|
|
42
|
+
child.on('exit', (code, signal) => {
|
|
43
|
+
if (signal) {
|
|
44
|
+
reject(new Error(`command terminated by ${signal}`));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
resolve(code ?? 1);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
package/src/prompt.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
|
|
3
|
+
export function prompt(question, { defaultValue, secret = false } = {}) {
|
|
4
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : '';
|
|
5
|
+
const rl = readline.createInterface({
|
|
6
|
+
input: process.stdin,
|
|
7
|
+
output: process.stdout,
|
|
8
|
+
terminal: true
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
if (!secret) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
14
|
+
rl.close();
|
|
15
|
+
resolve(answer.trim() || defaultValue || '');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const originalWrite = rl._writeToOutput;
|
|
21
|
+
rl._writeToOutput = function writeHidden(stringToWrite) {
|
|
22
|
+
if (rl.stdoutMuted) {
|
|
23
|
+
rl.output.write(stringToWrite.replace(/[^\r\n]/g, '*'));
|
|
24
|
+
} else {
|
|
25
|
+
originalWrite.call(rl, stringToWrite);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
rl.stdoutMuted = true;
|
|
31
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
32
|
+
rl.stdoutMuted = false;
|
|
33
|
+
rl.output.write('\n');
|
|
34
|
+
rl.close();
|
|
35
|
+
resolve(answer.trim() || defaultValue || '');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
package/src/python.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { commandExists, runCapture, runInherit } from './process.js';
|
|
3
|
+
import { venvPython } from './paths.js';
|
|
4
|
+
|
|
5
|
+
export function findPython(platform = process.platform) {
|
|
6
|
+
const candidates = platform === 'win32'
|
|
7
|
+
? [
|
|
8
|
+
{ command: 'py', args: ['-3.11'] },
|
|
9
|
+
{ command: 'python', args: [] },
|
|
10
|
+
{ command: 'python3', args: [] }
|
|
11
|
+
]
|
|
12
|
+
: [
|
|
13
|
+
{ command: 'python3.12', args: [] },
|
|
14
|
+
{ command: 'python3.11', args: [] },
|
|
15
|
+
{ command: 'python3', args: [] },
|
|
16
|
+
{ command: 'python', args: [] }
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
for (const candidate of candidates) {
|
|
20
|
+
const version = runCapture(candidate.command, [
|
|
21
|
+
...candidate.args,
|
|
22
|
+
'-c',
|
|
23
|
+
'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}")'
|
|
24
|
+
]);
|
|
25
|
+
if (!version.ok) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const match = version.stdout.trim().match(/^(\d+)\.(\d+)\./);
|
|
29
|
+
if (!match) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const major = Number(match[1]);
|
|
33
|
+
const minor = Number(match[2]);
|
|
34
|
+
if (major > 3 || (major === 3 && minor >= 11)) {
|
|
35
|
+
return candidate;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function ensureVenv(venvPath, pythonCandidate, platform = process.platform) {
|
|
42
|
+
const pythonPath = venvPython(venvPath, platform);
|
|
43
|
+
if (fs.existsSync(pythonPath)) {
|
|
44
|
+
return pythonPath;
|
|
45
|
+
}
|
|
46
|
+
fs.mkdirSync(venvPath, { recursive: true });
|
|
47
|
+
const status = runInherit(pythonCandidate.command, [
|
|
48
|
+
...pythonCandidate.args,
|
|
49
|
+
'-m',
|
|
50
|
+
'venv',
|
|
51
|
+
venvPath
|
|
52
|
+
]);
|
|
53
|
+
if (status !== 0) {
|
|
54
|
+
throw new Error('Could not create the managed Vendian Python environment.');
|
|
55
|
+
}
|
|
56
|
+
return pythonPath;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function hasUv() {
|
|
60
|
+
return commandExists('uv');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function pythonVersion(pythonPath) {
|
|
64
|
+
const result = runCapture(pythonPath, [
|
|
65
|
+
'-c',
|
|
66
|
+
'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}")'
|
|
67
|
+
]);
|
|
68
|
+
return result.ok ? result.stdout.trim() : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function verifyVendianImports(pythonPath) {
|
|
72
|
+
const result = runCapture(pythonPath, [
|
|
73
|
+
'-c',
|
|
74
|
+
'import vendian_agents, vendian_agents_runtime; print("ok")'
|
|
75
|
+
]);
|
|
76
|
+
return result.ok;
|
|
77
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
const SERVICE = 'Vendian CLI';
|
|
4
|
+
const PACKAGE_TOKEN_ACCOUNT = 'gitlab-package-token';
|
|
5
|
+
|
|
6
|
+
function run(command, args, options = {}) {
|
|
7
|
+
return spawnSync(command, args, {
|
|
8
|
+
encoding: 'utf8',
|
|
9
|
+
shell: false,
|
|
10
|
+
...options
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function packageTokenSecretName() {
|
|
15
|
+
return PACKAGE_TOKEN_ACCOUNT;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function savePackageTokenSecret(token, config = {}, platform = process.platform) {
|
|
19
|
+
if (!token) {
|
|
20
|
+
return { ok: false };
|
|
21
|
+
}
|
|
22
|
+
if (platform === 'darwin') {
|
|
23
|
+
const result = run('security', [
|
|
24
|
+
'add-generic-password',
|
|
25
|
+
'-a',
|
|
26
|
+
PACKAGE_TOKEN_ACCOUNT,
|
|
27
|
+
'-s',
|
|
28
|
+
SERVICE,
|
|
29
|
+
'-w',
|
|
30
|
+
token,
|
|
31
|
+
'-U'
|
|
32
|
+
]);
|
|
33
|
+
return { ok: result.status === 0, provider: 'macos-keychain' };
|
|
34
|
+
}
|
|
35
|
+
if (platform === 'win32') {
|
|
36
|
+
const encrypted = run(
|
|
37
|
+
'powershell.exe',
|
|
38
|
+
[
|
|
39
|
+
'-NoProfile',
|
|
40
|
+
'-NonInteractive',
|
|
41
|
+
'-Command',
|
|
42
|
+
'$secure = ConvertTo-SecureString -String $env:VENDIAN_SECRET_VALUE -AsPlainText -Force; $secure | ConvertFrom-SecureString'
|
|
43
|
+
],
|
|
44
|
+
{ env: { ...process.env, VENDIAN_SECRET_VALUE: token } }
|
|
45
|
+
);
|
|
46
|
+
if (encrypted.status !== 0 || !encrypted.stdout.trim()) {
|
|
47
|
+
return { ok: false, provider: 'windows-dpapi' };
|
|
48
|
+
}
|
|
49
|
+
const nextConfig = {
|
|
50
|
+
...config,
|
|
51
|
+
secretStore: {
|
|
52
|
+
...(config.secretStore || {}),
|
|
53
|
+
[PACKAGE_TOKEN_ACCOUNT]: {
|
|
54
|
+
provider: 'windows-dpapi',
|
|
55
|
+
value: encrypted.stdout.trim()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
return { ok: true, provider: 'windows-dpapi', config: nextConfig };
|
|
60
|
+
}
|
|
61
|
+
return { ok: false };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function loadPackageTokenSecret(config = {}, platform = process.platform) {
|
|
65
|
+
if (platform === 'darwin') {
|
|
66
|
+
const result = run('security', [
|
|
67
|
+
'find-generic-password',
|
|
68
|
+
'-a',
|
|
69
|
+
PACKAGE_TOKEN_ACCOUNT,
|
|
70
|
+
'-s',
|
|
71
|
+
SERVICE,
|
|
72
|
+
'-w'
|
|
73
|
+
]);
|
|
74
|
+
return result.status === 0 ? result.stdout.trim() : '';
|
|
75
|
+
}
|
|
76
|
+
if (platform === 'win32') {
|
|
77
|
+
const entry = config.secretStore?.[PACKAGE_TOKEN_ACCOUNT];
|
|
78
|
+
if (!entry || entry.provider !== 'windows-dpapi' || !entry.value) {
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
const result = run(
|
|
82
|
+
'powershell.exe',
|
|
83
|
+
[
|
|
84
|
+
'-NoProfile',
|
|
85
|
+
'-NonInteractive',
|
|
86
|
+
'-Command',
|
|
87
|
+
'$secure = ConvertTo-SecureString -String $env:VENDIAN_SECRET_BLOB; $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure); try { [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) }'
|
|
88
|
+
],
|
|
89
|
+
{ env: { ...process.env, VENDIAN_SECRET_BLOB: entry.value } }
|
|
90
|
+
);
|
|
91
|
+
return result.status === 0 ? result.stdout.trim() : '';
|
|
92
|
+
}
|
|
93
|
+
return '';
|
|
94
|
+
}
|
package/src/setup.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { loginWithVendianOAuth, saveCloudToken } from './auth.js';
|
|
3
|
+
import { DEFAULT_GITLAB_HOST, DEFAULT_SDK_PUBLIC_PROJECT_ID, DEFAULT_SDK_RUNTIME_PROJECT_ID } from './constants.js';
|
|
4
|
+
import { loadConfig, saveConfig, redact } from './config.js';
|
|
5
|
+
import { installVendianPackages, registryConfig } from './install.js';
|
|
6
|
+
import { managedVenvPath, venvVendian } from './paths.js';
|
|
7
|
+
import { findPython, ensureVenv, pythonVersion, verifyVendianImports } from './python.js';
|
|
8
|
+
import { savePackageTokenSecret } from './secret-store.js';
|
|
9
|
+
|
|
10
|
+
export async function setup({
|
|
11
|
+
nonInteractive = false,
|
|
12
|
+
backend = undefined,
|
|
13
|
+
apiUrl = undefined,
|
|
14
|
+
noBrowser = false,
|
|
15
|
+
env = process.env,
|
|
16
|
+
platform = process.platform
|
|
17
|
+
} = {}) {
|
|
18
|
+
const existing = loadConfig(env, platform);
|
|
19
|
+
const registry = registryConfig(existing, env, platform);
|
|
20
|
+
const next = {
|
|
21
|
+
...existing,
|
|
22
|
+
gitlabHost: registry.gitlabHost || DEFAULT_GITLAB_HOST,
|
|
23
|
+
gitlabUsername: registry.username || '__token__',
|
|
24
|
+
sdkPublicProjectId: registry.sdkProjectId || DEFAULT_SDK_PUBLIC_PROJECT_ID,
|
|
25
|
+
sdkRuntimeProjectId: registry.runtimeProjectId || DEFAULT_SDK_RUNTIME_PROJECT_ID
|
|
26
|
+
};
|
|
27
|
+
let cloudAuthApiUrl;
|
|
28
|
+
|
|
29
|
+
console.log('Vendian setup');
|
|
30
|
+
console.log('This installs the private Vendian Python CLI/runtime into a managed local environment.');
|
|
31
|
+
console.log('Agent requirements.txt files stay reserved for agent-owned dependencies.');
|
|
32
|
+
console.log('');
|
|
33
|
+
|
|
34
|
+
if (!registry.token && !nonInteractive) {
|
|
35
|
+
console.log('Opening Vendian sign-in to get package access...');
|
|
36
|
+
const login = await loginWithVendianOAuth({ backend, apiUrl, noBrowser, env });
|
|
37
|
+
saveCloudToken(login, { env, platform });
|
|
38
|
+
cloudAuthApiUrl = login.apiUrl;
|
|
39
|
+
const packageCredentials = login.packageCredentials;
|
|
40
|
+
if (!packageCredentials?.token) {
|
|
41
|
+
throw new Error('Vendian login succeeded, but the backend did not return package credentials. Configure CLI_PACKAGE_REGISTRY_TOKEN on the backend.');
|
|
42
|
+
}
|
|
43
|
+
next.gitlabHost = packageCredentials.gitlabHost || next.gitlabHost;
|
|
44
|
+
next.gitlabUsername = packageCredentials.username || next.gitlabUsername;
|
|
45
|
+
next.sdkPublicProjectId = packageCredentials.sdkProjectId || next.sdkPublicProjectId;
|
|
46
|
+
next.sdkRuntimeProjectId = packageCredentials.runtimeProjectId || next.sdkRuntimeProjectId;
|
|
47
|
+
next.vendianAgentsVersion = packageCredentials.vendianAgentsVersion || next.vendianAgentsVersion;
|
|
48
|
+
next.vendianAgentsRuntimeVersion = packageCredentials.vendianAgentsRuntimeVersion || next.vendianAgentsRuntimeVersion;
|
|
49
|
+
|
|
50
|
+
const secret = savePackageTokenSecret(packageCredentials.token, next, platform);
|
|
51
|
+
if (secret.ok) {
|
|
52
|
+
Object.assign(next, secret.config || {});
|
|
53
|
+
delete next.gitlabToken;
|
|
54
|
+
console.log(`Package token saved with ${secret.provider}.`);
|
|
55
|
+
} else {
|
|
56
|
+
next.gitlabToken = packageCredentials.token;
|
|
57
|
+
console.log('Package token saved in the local Vendian CLI config file.');
|
|
58
|
+
}
|
|
59
|
+
} else if (registry.token) {
|
|
60
|
+
if (registry.tokenSource !== 'secret-store') {
|
|
61
|
+
next.gitlabToken = registry.token;
|
|
62
|
+
}
|
|
63
|
+
console.log(`Using GitLab package token ${redact(registry.token)}.`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const installRegistry = registryConfig(next, env, platform);
|
|
67
|
+
if (!installRegistry.token) {
|
|
68
|
+
throw new Error('Setup needs package access. Run interactive `vendian login` to sign in, or set GITLAB_TOKEN for local development.');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const python = findPython(platform);
|
|
72
|
+
if (!python) {
|
|
73
|
+
throw new Error('Python 3.11+ was not found. Install Python 3.11 or newer, then rerun `vendian login`.');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const venvPath = managedVenvPath(env, platform);
|
|
77
|
+
console.log(`Managed environment: ${venvPath}`);
|
|
78
|
+
const pythonPath = ensureVenv(venvPath, python, platform);
|
|
79
|
+
console.log(`Python: ${pythonVersion(pythonPath) || pythonPath}`);
|
|
80
|
+
|
|
81
|
+
saveConfig(next, env, platform);
|
|
82
|
+
installVendianPackages({ pythonPath, venvPath, config: next, env, platform });
|
|
83
|
+
saveConfig({ ...next, lastManagedUpdateAt: new Date().toISOString() }, env, platform);
|
|
84
|
+
|
|
85
|
+
if (!verifyVendianImports(pythonPath)) {
|
|
86
|
+
throw new Error('Vendian packages installed, but import verification failed.');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const vendianPath = venvVendian(venvPath, platform);
|
|
90
|
+
if (!fs.existsSync(vendianPath)) {
|
|
91
|
+
throw new Error('Vendian executable was not found after install.');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log('');
|
|
95
|
+
for (const line of setupCompletionLines({ cloudAuthApiUrl, backend, apiUrl })) {
|
|
96
|
+
console.log(line);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function setupCompletionLines({ cloudAuthApiUrl, backend, apiUrl } = {}) {
|
|
101
|
+
const lines = ['Vendian setup complete.'];
|
|
102
|
+
if (cloudAuthApiUrl) {
|
|
103
|
+
lines.push(`Cloud authentication saved for ${cloudAuthApiUrl}.`);
|
|
104
|
+
lines.push('Next: vendian cloud local serve --agents-dir ./agents');
|
|
105
|
+
return lines;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
lines.push(`Next: ${cloudAuthLoginCommand({ backend, apiUrl })}`);
|
|
109
|
+
lines.push('Then: vendian cloud local serve --agents-dir ./agents');
|
|
110
|
+
return lines;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function cloudAuthLoginCommand({ backend, apiUrl } = {}) {
|
|
114
|
+
if (apiUrl) {
|
|
115
|
+
return `vendian cloud auth login --api-url ${apiUrl}`;
|
|
116
|
+
}
|
|
117
|
+
if (backend) {
|
|
118
|
+
return `vendian cloud auth login --backend ${backend}`;
|
|
119
|
+
}
|
|
120
|
+
return 'vendian cloud auth login';
|
|
121
|
+
}
|