@vendian/cli 0.0.1 → 0.0.2
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 +7 -2
- package/package.json +1 -1
- package/src/auth.js +26 -0
- package/src/doctor.js +2 -3
- package/src/install.js +2 -2
- package/src/main.js +18 -12
- package/src/setup.js +32 -26
- package/src/tui.js +119 -0
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@ Command-line tools for signing in to Vendian and running local agent workflows.
|
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install -g @vendian/cli
|
|
7
|
+
vendian
|
|
7
8
|
vendian login
|
|
8
9
|
vendian cloud local serve --agents-dir ./agents
|
|
9
10
|
```
|
|
@@ -11,15 +12,19 @@ vendian cloud local serve --agents-dir ./agents
|
|
|
11
12
|
## Commands
|
|
12
13
|
|
|
13
14
|
```bash
|
|
15
|
+
vendian
|
|
14
16
|
vendian login
|
|
15
17
|
vendian doctor
|
|
16
18
|
vendian update
|
|
17
19
|
vendian cloud local serve --agents-dir ./agents
|
|
18
20
|
```
|
|
19
21
|
|
|
22
|
+
Run `vendian` with no arguments to open the interactive menu. It shows your
|
|
23
|
+
current endpoint connections and guides common local workflows.
|
|
24
|
+
|
|
20
25
|
`vendian login` opens a browser sign-in, prepares the local runtime, and stores
|
|
21
|
-
the credentials needed by the CLI. `vendian setup` is
|
|
22
|
-
existing users.
|
|
26
|
+
the credentials needed by the CLI for the selected endpoint. `vendian setup` is
|
|
27
|
+
kept as an alias for existing users.
|
|
23
28
|
|
|
24
29
|
`vendian doctor` checks the local installation. `vendian update` refreshes the
|
|
25
30
|
managed runtime without changing your agents.
|
package/package.json
CHANGED
package/src/auth.js
CHANGED
|
@@ -52,6 +52,32 @@ export async function loginWithVendianOAuth({ backend, apiUrl, noBrowser = false
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
export function loadCloudConfig(env = process.env, platform = process.platform) {
|
|
56
|
+
const file = cloudConfigPath(env, platform);
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
59
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
60
|
+
} catch {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function cloudAuthStatus({ backend, apiUrl, env = process.env, platform = process.platform } = {}) {
|
|
66
|
+
const targetApiUrl = resolveApiUrl({ backend, apiUrl, env });
|
|
67
|
+
const config = loadCloudConfig(env, platform);
|
|
68
|
+
const profiles = config.profiles && typeof config.profiles === 'object' ? config.profiles : {};
|
|
69
|
+
const profile = profiles[targetApiUrl];
|
|
70
|
+
return {
|
|
71
|
+
apiUrl: targetApiUrl,
|
|
72
|
+
activeApiUrl: typeof config.active_api_url === 'string' ? config.active_api_url : undefined,
|
|
73
|
+
profile: profile && typeof profile === 'object' ? profile : undefined,
|
|
74
|
+
authenticated: Boolean(profile?.access_token),
|
|
75
|
+
email: typeof profile?.email === 'string' ? profile.email : undefined,
|
|
76
|
+
expiresAt: typeof profile?.expires_at === 'string' ? profile.expires_at : undefined,
|
|
77
|
+
profiles
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
55
81
|
async function getOAuthConfig(apiUrl, redirectUri) {
|
|
56
82
|
const url = new URL(`${apiUrl}/api/v1/cli/auth/oauth/config`);
|
|
57
83
|
url.searchParams.set('redirectUri', redirectUri);
|
package/src/doctor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import { loadConfig
|
|
2
|
+
import { loadConfig } from './config.js';
|
|
3
3
|
import { registryConfig } from './install.js';
|
|
4
4
|
import { managedVenvPath, venvPython, venvVendian } from './paths.js';
|
|
5
5
|
import { findPython, hasUv, pythonVersion, verifyVendianImports } from './python.js';
|
|
@@ -18,6 +18,5 @@ export function doctor({ env = process.env, platform = process.platform } = {})
|
|
|
18
18
|
console.log(`Managed Python: ${fs.existsSync(pythonPath) ? pythonVersion(pythonPath) || 'present' : 'missing'}`);
|
|
19
19
|
console.log(`Vendian executable: ${fs.existsSync(vendianPath) ? vendianPath : 'missing'}`);
|
|
20
20
|
console.log(`Vendian imports: ${fs.existsSync(pythonPath) && verifyVendianImports(pythonPath) ? 'ok' : 'missing'}`);
|
|
21
|
-
console.log(`
|
|
22
|
-
console.log(`Package token: ${registry.token ? `${redact(registry.token)} (${registry.tokenSource})` : 'missing'}`);
|
|
21
|
+
console.log(`Package access: ${registry.token ? `configured (${registry.tokenSource})` : 'missing'}`);
|
|
23
22
|
}
|
package/src/install.js
CHANGED
|
@@ -59,7 +59,7 @@ export function installVendianPackages({ pythonPath, venvPath, config, env = pro
|
|
|
59
59
|
const registry = registryConfig(config, env, platform);
|
|
60
60
|
const indexes = buildIndexUrls(registry);
|
|
61
61
|
if (!indexes) {
|
|
62
|
-
throw new Error('
|
|
62
|
+
throw new Error('Package access is missing. Run `vendian login`.');
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
const specs = packageSpecs(registry);
|
|
@@ -82,7 +82,7 @@ export function installVendianPackages({ pythonPath, venvPath, config, env = pro
|
|
|
82
82
|
status = runInherit(pythonPath, ['-m', 'pip', ...installArgs]);
|
|
83
83
|
}
|
|
84
84
|
if (status !== 0) {
|
|
85
|
-
throw new Error('Could not install Vendian
|
|
85
|
+
throw new Error('Could not install Vendian runtime packages. Check package access and try again.');
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
writeInstallMarker(venvPath, {
|
package/src/main.js
CHANGED
|
@@ -1,41 +1,46 @@
|
|
|
1
1
|
import { doctor } from './doctor.js';
|
|
2
2
|
import { forwardToPythonVendian } from './forward.js';
|
|
3
3
|
import { setup } from './setup.js';
|
|
4
|
+
import { runTui } from './tui.js';
|
|
4
5
|
|
|
5
6
|
function printHelp() {
|
|
6
|
-
console.log(`Vendian CLI
|
|
7
|
+
console.log(`Vendian CLI
|
|
7
8
|
|
|
8
9
|
Usage:
|
|
9
|
-
vendian
|
|
10
|
+
vendian Open the interactive menu
|
|
11
|
+
vendian login Sign in and prepare the local runtime
|
|
10
12
|
vendian setup Alias for vendian login
|
|
11
13
|
vendian doctor Check local bootstrap health
|
|
12
14
|
vendian update Update the managed Vendian CLI/runtime
|
|
13
|
-
vendian <command>
|
|
15
|
+
vendian <command> Run a Vendian cloud command
|
|
14
16
|
|
|
15
17
|
Examples:
|
|
16
18
|
vendian login
|
|
17
|
-
vendian login --backend
|
|
19
|
+
vendian login --backend staging
|
|
18
20
|
vendian cloud local serve --agents-dir ./agents
|
|
19
21
|
|
|
20
22
|
Environment:
|
|
21
23
|
VENDIAN_CLI_HOME Override managed CLI home
|
|
22
24
|
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
25
|
`);
|
|
29
26
|
}
|
|
30
27
|
|
|
31
28
|
export async function main(argv) {
|
|
32
29
|
const [command, ...rest] = argv;
|
|
33
|
-
if (!command
|
|
30
|
+
if (!command) {
|
|
31
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
32
|
+
await runTui();
|
|
33
|
+
} else {
|
|
34
|
+
printHelp();
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (command === '--help' || command === '-h') {
|
|
34
39
|
printHelp();
|
|
35
40
|
return;
|
|
36
41
|
}
|
|
37
42
|
if (command === '--version' || command === 'version') {
|
|
38
|
-
console.log('0.1
|
|
43
|
+
console.log('0.0.1');
|
|
39
44
|
return;
|
|
40
45
|
}
|
|
41
46
|
if (isBootstrapCommand(command)) {
|
|
@@ -60,7 +65,8 @@ export function isBootstrapCommand(command) {
|
|
|
60
65
|
function parseSetupOptions(args) {
|
|
61
66
|
const options = {
|
|
62
67
|
nonInteractive: args.includes('--yes') || args.includes('--non-interactive'),
|
|
63
|
-
noBrowser: args.includes('--no-browser')
|
|
68
|
+
noBrowser: args.includes('--no-browser'),
|
|
69
|
+
forceAuth: args.includes('--force') || args.includes('--reauth')
|
|
64
70
|
};
|
|
65
71
|
for (let index = 0; index < args.length; index += 1) {
|
|
66
72
|
const arg = args[index];
|
package/src/setup.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import { loginWithVendianOAuth, saveCloudToken } from './auth.js';
|
|
2
|
+
import { cloudAuthStatus, loginWithVendianOAuth, saveCloudToken } from './auth.js';
|
|
3
3
|
import { DEFAULT_GITLAB_HOST, DEFAULT_SDK_PUBLIC_PROJECT_ID, DEFAULT_SDK_RUNTIME_PROJECT_ID } from './constants.js';
|
|
4
|
-
import { loadConfig, saveConfig
|
|
4
|
+
import { loadConfig, saveConfig } from './config.js';
|
|
5
5
|
import { installVendianPackages, registryConfig } from './install.js';
|
|
6
6
|
import { managedVenvPath, venvVendian } from './paths.js';
|
|
7
7
|
import { findPython, ensureVenv, pythonVersion, verifyVendianImports } from './python.js';
|
|
@@ -12,11 +12,13 @@ export async function setup({
|
|
|
12
12
|
backend = undefined,
|
|
13
13
|
apiUrl = undefined,
|
|
14
14
|
noBrowser = false,
|
|
15
|
+
forceAuth = false,
|
|
15
16
|
env = process.env,
|
|
16
17
|
platform = process.platform
|
|
17
18
|
} = {}) {
|
|
18
19
|
const existing = loadConfig(env, platform);
|
|
19
20
|
const registry = registryConfig(existing, env, platform);
|
|
21
|
+
const auth = cloudAuthStatus({ backend, apiUrl, env, platform });
|
|
20
22
|
const next = {
|
|
21
23
|
...existing,
|
|
22
24
|
gitlabHost: registry.gitlabHost || DEFAULT_GITLAB_HOST,
|
|
@@ -24,48 +26,52 @@ export async function setup({
|
|
|
24
26
|
sdkPublicProjectId: registry.sdkProjectId || DEFAULT_SDK_PUBLIC_PROJECT_ID,
|
|
25
27
|
sdkRuntimeProjectId: registry.runtimeProjectId || DEFAULT_SDK_RUNTIME_PROJECT_ID
|
|
26
28
|
};
|
|
27
|
-
let cloudAuthApiUrl;
|
|
29
|
+
let cloudAuthApiUrl = auth.authenticated ? auth.apiUrl : undefined;
|
|
28
30
|
|
|
29
|
-
console.log('Vendian
|
|
30
|
-
console.log('This
|
|
31
|
+
console.log('Vendian login');
|
|
32
|
+
console.log('This signs in to Vendian and prepares the local runtime.');
|
|
31
33
|
console.log('Agent requirements.txt files stay reserved for agent-owned dependencies.');
|
|
32
34
|
console.log('');
|
|
33
35
|
|
|
34
|
-
if (!
|
|
35
|
-
console.log(
|
|
36
|
+
if ((!auth.authenticated || forceAuth) && !nonInteractive) {
|
|
37
|
+
console.log(`Opening Vendian sign-in for ${auth.apiUrl}...`);
|
|
36
38
|
const login = await loginWithVendianOAuth({ backend, apiUrl, noBrowser, env });
|
|
37
39
|
saveCloudToken(login, { env, platform });
|
|
38
40
|
cloudAuthApiUrl = login.apiUrl;
|
|
39
41
|
const packageCredentials = login.packageCredentials;
|
|
40
|
-
if (!packageCredentials?.token) {
|
|
42
|
+
if (!registry.token && !packageCredentials?.token) {
|
|
41
43
|
throw new Error('Vendian login succeeded, but the backend did not return package credentials. Configure CLI_PACKAGE_REGISTRY_TOKEN on the backend.');
|
|
42
44
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
if (packageCredentials) {
|
|
46
|
+
next.gitlabHost = packageCredentials.gitlabHost || next.gitlabHost;
|
|
47
|
+
next.gitlabUsername = packageCredentials.username || next.gitlabUsername;
|
|
48
|
+
next.sdkPublicProjectId = packageCredentials.sdkProjectId || next.sdkPublicProjectId;
|
|
49
|
+
next.sdkRuntimeProjectId = packageCredentials.runtimeProjectId || next.sdkRuntimeProjectId;
|
|
50
|
+
next.vendianAgentsVersion = packageCredentials.vendianAgentsVersion || next.vendianAgentsVersion;
|
|
51
|
+
next.vendianAgentsRuntimeVersion = packageCredentials.vendianAgentsRuntimeVersion || next.vendianAgentsRuntimeVersion;
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
const secret = savePackageTokenSecret(packageCredentials.token, next, platform);
|
|
54
|
+
if (secret.ok) {
|
|
55
|
+
Object.assign(next, secret.config || {});
|
|
56
|
+
delete next.gitlabToken;
|
|
57
|
+
console.log('Package access saved.');
|
|
58
|
+
} else {
|
|
59
|
+
next.gitlabToken = packageCredentials.token;
|
|
60
|
+
console.log('Package access saved in the local Vendian CLI config file.');
|
|
61
|
+
}
|
|
58
62
|
}
|
|
63
|
+
} else if (auth.authenticated) {
|
|
64
|
+
console.log(`Cloud authentication already saved for ${auth.apiUrl}${auth.email ? ` (${auth.email})` : ''}.`);
|
|
59
65
|
} else if (registry.token) {
|
|
60
66
|
if (registry.tokenSource !== 'secret-store') {
|
|
61
67
|
next.gitlabToken = registry.token;
|
|
62
68
|
}
|
|
63
|
-
console.log(
|
|
69
|
+
console.log('Using saved package access.');
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
const installRegistry = registryConfig(next, env, platform);
|
|
67
73
|
if (!installRegistry.token) {
|
|
68
|
-
throw new Error('
|
|
74
|
+
throw new Error('Package access is missing. Run interactive `vendian login` to sign in.');
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
const python = findPython(platform);
|
|
@@ -98,9 +104,9 @@ export async function setup({
|
|
|
98
104
|
}
|
|
99
105
|
|
|
100
106
|
export function setupCompletionLines({ cloudAuthApiUrl, backend, apiUrl } = {}) {
|
|
101
|
-
const lines = ['Vendian
|
|
107
|
+
const lines = ['Vendian login complete.'];
|
|
102
108
|
if (cloudAuthApiUrl) {
|
|
103
|
-
lines.push(`
|
|
109
|
+
lines.push(`Connected to ${cloudAuthApiUrl}.`);
|
|
104
110
|
lines.push('Next: vendian cloud local serve --agents-dir ./agents');
|
|
105
111
|
return lines;
|
|
106
112
|
}
|
package/src/tui.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import readline from 'node:readline/promises';
|
|
2
|
+
import { BACKEND_TARGETS } from './constants.js';
|
|
3
|
+
import { cloudAuthStatus } from './auth.js';
|
|
4
|
+
import { doctor } from './doctor.js';
|
|
5
|
+
import { forwardToPythonVendian } from './forward.js';
|
|
6
|
+
import { setup } from './setup.js';
|
|
7
|
+
|
|
8
|
+
const ENDPOINTS = [
|
|
9
|
+
{ key: 'local', label: 'Local' },
|
|
10
|
+
{ key: 'dev', label: 'Dev' },
|
|
11
|
+
{ key: 'staging', label: 'Staging' },
|
|
12
|
+
{ key: 'prod', label: 'Production' }
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export async function runTui({ env = process.env, platform = process.platform, input = process.stdin, output = process.stdout } = {}) {
|
|
16
|
+
const rl = readline.createInterface({ input, output });
|
|
17
|
+
try {
|
|
18
|
+
while (true) {
|
|
19
|
+
output.write(`${renderHome({ env, platform })}\n`);
|
|
20
|
+
const choice = await ask(rl, 'Choose an option');
|
|
21
|
+
if (choice === '1') {
|
|
22
|
+
await connectEndpoint(rl, output, { env, platform });
|
|
23
|
+
} else if (choice === '2') {
|
|
24
|
+
const agentsDir = (await ask(rl, 'Agents directory', './agents')).trim() || './agents';
|
|
25
|
+
await forwardToPythonVendian(['cloud', 'local', 'serve', '--agents-dir', agentsDir], { env, platform });
|
|
26
|
+
return;
|
|
27
|
+
} else if (choice === '3') {
|
|
28
|
+
doctor({ env, platform });
|
|
29
|
+
} else if (choice === '4') {
|
|
30
|
+
await setup({ nonInteractive: true, env, platform });
|
|
31
|
+
} else if (choice === '5') {
|
|
32
|
+
output.write(`${helpText()}\n`);
|
|
33
|
+
} else if (choice === '6' || choice.toLowerCase() === 'q') {
|
|
34
|
+
return;
|
|
35
|
+
} else {
|
|
36
|
+
output.write('Unknown option.\n');
|
|
37
|
+
}
|
|
38
|
+
output.write('\n');
|
|
39
|
+
}
|
|
40
|
+
} finally {
|
|
41
|
+
rl.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function renderHome({ env = process.env, platform = process.platform } = {}) {
|
|
46
|
+
const rows = endpointRows({ env, platform });
|
|
47
|
+
return [
|
|
48
|
+
'',
|
|
49
|
+
'Vendian CLI',
|
|
50
|
+
'-----------',
|
|
51
|
+
'Connections:',
|
|
52
|
+
...rows.map((row) => ` ${row.label.padEnd(10)} ${row.status}${row.detail ? ` - ${row.detail}` : ''}`),
|
|
53
|
+
'',
|
|
54
|
+
'1. Connect or switch endpoint',
|
|
55
|
+
'2. Start local agent server',
|
|
56
|
+
'3. Run doctor',
|
|
57
|
+
'4. Update managed runtime',
|
|
58
|
+
'5. Help',
|
|
59
|
+
'6. Exit'
|
|
60
|
+
].join('\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function endpointRows({ env = process.env, platform = process.platform } = {}) {
|
|
64
|
+
return ENDPOINTS.map((endpoint) => {
|
|
65
|
+
const status = cloudAuthStatus({ backend: endpoint.key, env, platform });
|
|
66
|
+
const active = status.activeApiUrl === status.apiUrl;
|
|
67
|
+
return {
|
|
68
|
+
key: endpoint.key,
|
|
69
|
+
label: endpoint.label,
|
|
70
|
+
apiUrl: status.apiUrl,
|
|
71
|
+
status: status.authenticated ? (active ? 'connected' : 'signed in') : 'not signed in',
|
|
72
|
+
detail: status.authenticated ? status.email || status.apiUrl : status.apiUrl
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function connectEndpoint(rl, output, { env, platform }) {
|
|
78
|
+
output.write('\nConnect endpoint:\n');
|
|
79
|
+
ENDPOINTS.forEach((endpoint, index) => {
|
|
80
|
+
output.write(`${index + 1}. ${endpoint.label} (${BACKEND_TARGETS[endpoint.key]})\n`);
|
|
81
|
+
});
|
|
82
|
+
output.write('5. Custom API URL\n');
|
|
83
|
+
const choice = await ask(rl, 'Endpoint');
|
|
84
|
+
if (choice === '5') {
|
|
85
|
+
const apiUrl = (await ask(rl, 'API URL')).trim();
|
|
86
|
+
if (!apiUrl) {
|
|
87
|
+
output.write('API URL is required.\n');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
await setup({ apiUrl, forceAuth: true, env, platform });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const endpoint = ENDPOINTS[Number(choice) - 1];
|
|
95
|
+
if (!endpoint) {
|
|
96
|
+
output.write('Unknown endpoint.\n');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await setup({ backend: endpoint.key, forceAuth: true, env, platform });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function helpText() {
|
|
103
|
+
return [
|
|
104
|
+
'',
|
|
105
|
+
'Common commands:',
|
|
106
|
+
' vendian login',
|
|
107
|
+
' vendian login --backend staging',
|
|
108
|
+
' vendian login --api-url http://localhost:3000',
|
|
109
|
+
' vendian cloud local serve --agents-dir ./agents',
|
|
110
|
+
' vendian doctor',
|
|
111
|
+
' vendian update'
|
|
112
|
+
].join('\n');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function ask(rl, question, defaultValue) {
|
|
116
|
+
const suffix = defaultValue ? ` (${defaultValue})` : '';
|
|
117
|
+
const answer = await rl.question(`${question}${suffix}: `);
|
|
118
|
+
return answer.trim() || defaultValue || '';
|
|
119
|
+
}
|