@adrive/cli 0.1.0

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.
@@ -0,0 +1,37 @@
1
+ import kleur from 'kleur';
2
+ import { CliApiClient } from '../lib/api-client.js';
3
+ import { requireLogin } from '../lib/config.js';
4
+ function truncate(s, n) {
5
+ if (!s)
6
+ return '';
7
+ return s.length <= n ? s : `${s.slice(0, n - 1)}…`;
8
+ }
9
+ export async function cmdSearch(query, opts = {}) {
10
+ const cfg = await requireLogin();
11
+ const client = new CliApiClient({
12
+ baseUrl: cfg.api_url,
13
+ apiKey: cfg.api_key,
14
+ agentPlatform: cfg.agent_platform ?? 'cli',
15
+ agentRole: cfg.agent_role,
16
+ });
17
+ const topK = opts.topK ? Number.parseInt(opts.topK, 10) : undefined;
18
+ const filter = {};
19
+ if (opts.platform)
20
+ filter.agent_platform = opts.platform;
21
+ const res = await client.search({
22
+ query,
23
+ workspace_id: opts.workspace ?? cfg.default_workspace,
24
+ top_k: topK,
25
+ filter: Object.keys(filter).length ? filter : undefined,
26
+ });
27
+ if (!res.hits.length) {
28
+ process.stdout.write('No results.\n');
29
+ return;
30
+ }
31
+ for (const h of res.hits) {
32
+ const score = h.score.toFixed(2);
33
+ process.stdout.write(`${kleur.cyan(h.path)} · score=${score} · by=${h.agent_id}@${h.agent_platform}\n`);
34
+ if (h.snippet)
35
+ process.stdout.write(` ${kleur.dim(truncate(h.snippet, 200))}\n`);
36
+ }
37
+ }
@@ -0,0 +1,13 @@
1
+ import { CliApiClient } from '../lib/api-client.js';
2
+ import { requireLogin } from '../lib/config.js';
3
+ export async function cmdWhoami() {
4
+ const cfg = await requireLogin();
5
+ const client = new CliApiClient({
6
+ baseUrl: cfg.api_url,
7
+ apiKey: cfg.api_key,
8
+ agentPlatform: cfg.agent_platform ?? 'cli',
9
+ agentRole: cfg.agent_role,
10
+ });
11
+ const info = await client.whoami();
12
+ process.stdout.write(`${JSON.stringify(info, null, 2)}\n`);
13
+ }
package/dist/index.js ADDED
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import kleur from 'kleur';
4
+ import { cmdInit } from './commands/init.js';
5
+ import { cmdAuthLogin } from './commands/auth-login.js';
6
+ import { cmdAuthLogout } from './commands/auth-logout.js';
7
+ import { cmdAuthStatus } from './commands/auth-status.js';
8
+ import { cmdWhoami } from './commands/whoami.js';
9
+ import { cmdLs } from './commands/ls.js';
10
+ import { cmdPush } from './commands/push.js';
11
+ import { cmdPull } from './commands/pull.js';
12
+ import { cmdSearch } from './commands/search.js';
13
+ import { cmdContext } from './commands/context.js';
14
+ import { cmdRunLog } from './commands/run-log.js';
15
+ import { cmdBilling } from './commands/billing.js';
16
+ import { cmdPlan } from './commands/plan.js';
17
+ const VERSION = '0.1.0';
18
+ function fail(err) {
19
+ const msg = err instanceof Error ? err.message : String(err);
20
+ process.stderr.write(`${kleur.red('[error]')} ${msg}\n`);
21
+ process.exit(1);
22
+ }
23
+ function wrap(fn) {
24
+ return async (...args) => {
25
+ try {
26
+ await fn(...args);
27
+ }
28
+ catch (err) {
29
+ fail(err);
30
+ }
31
+ };
32
+ }
33
+ process.on('SIGINT', () => {
34
+ process.stderr.write('\nInterrupted.\n');
35
+ process.exit(130);
36
+ });
37
+ const program = new Command();
38
+ program
39
+ .name('adrive')
40
+ .description('AgentDrive — the persistent workspace your agents share across Claude Code, Codex, Cursor, and LangGraph.')
41
+ .version(VERSION, '-v, --version');
42
+ program
43
+ .command('init')
44
+ .description('First-run wizard: log in and wire AgentDrive into installed agent tools')
45
+ .option('-y, --yes', 'Skip prompts and accept defaults')
46
+ .option('--api-url <url>', 'Override the AgentDrive API base URL')
47
+ .action(wrap((opts) => cmdInit(opts)));
48
+ const auth = program.command('auth').description('Manage AgentDrive login');
49
+ auth
50
+ .command('login')
51
+ .description('Open browser, log in, store API key')
52
+ .option('--api-url <url>', 'Override the AgentDrive API base URL')
53
+ .action(wrap((opts) => cmdAuthLogin(opts)));
54
+ auth
55
+ .command('logout')
56
+ .description('Wipe local AgentDrive credentials')
57
+ .option('-y, --yes', 'Skip confirmation')
58
+ .action(wrap((opts) => cmdAuthLogout(opts)));
59
+ auth
60
+ .command('status')
61
+ .description('Show current login state')
62
+ .action(wrap(() => cmdAuthStatus()));
63
+ program
64
+ .command('whoami')
65
+ .description('Print principal info from the AgentDrive API')
66
+ .action(wrap(() => cmdWhoami()));
67
+ program
68
+ .command('ls')
69
+ .description('List workspaces')
70
+ .action(wrap(() => cmdLs()));
71
+ program
72
+ .command('push <local-path>')
73
+ .description('Upload a local file to a workspace')
74
+ .option('-w, --workspace <id>', 'Workspace id (default: only workspace, if exactly one)')
75
+ .option('-r, --remote-path <path>', 'Remote path (default: basename of local-path)')
76
+ .option('-m, --message <msg>', 'Commit-style message stored on the version')
77
+ .action(wrap((localPath, opts) => cmdPush(localPath, opts)));
78
+ program
79
+ .command('pull <workspace-id-slash-path>')
80
+ .description('Download a file. Spec is <workspace-id>/<path>')
81
+ .option('-o, --out <file>', 'Write to file (default: stdout)')
82
+ .action(wrap((spec, opts) => cmdPull(spec, opts)));
83
+ program
84
+ .command('search <query>')
85
+ .description('Semantic search across workspace artifacts')
86
+ .option('--top-k <n>', 'Number of hits to return')
87
+ .option('--platform <p>', 'Filter to artifacts written by a specific agent platform')
88
+ .option('-w, --workspace <id>', 'Restrict to one workspace')
89
+ .action(wrap((query, opts) => cmdSearch(query, {
90
+ topK: opts.topK,
91
+ platform: opts.platform,
92
+ workspace: opts.workspace,
93
+ })));
94
+ program
95
+ .command('context <task>')
96
+ .description('Auto-pull relevant artifacts for a task')
97
+ .option('--limit <n>', 'Max artifacts to return')
98
+ .option('-w, --workspace <id>', 'Restrict to one workspace')
99
+ .action(wrap((task, opts) => cmdContext(task, opts)));
100
+ program
101
+ .command('run-log <task>')
102
+ .description('Log an agent run for cross-platform provenance')
103
+ .option('--status <status>', 'success | failure | partial | in_progress', 'success')
104
+ .option('--duration <ms>', 'Duration in milliseconds')
105
+ .option('--ref <path...>', 'Artifacts referenced (repeatable)')
106
+ .option('--produced <path...>', 'Artifacts produced (repeatable)')
107
+ .action(wrap((task, opts) => cmdRunLog(task, {
108
+ status: opts.status,
109
+ duration: opts.duration,
110
+ ref: opts.ref,
111
+ produced: opts.produced,
112
+ })));
113
+ program
114
+ .command('billing')
115
+ .description('Open the Stripe billing portal in your browser')
116
+ .action(wrap(() => cmdBilling()));
117
+ program
118
+ .command('plan')
119
+ .description('Show current plan, usage, and quotas')
120
+ .action(wrap(() => cmdPlan()));
121
+ program.parseAsync(process.argv).catch(fail);
@@ -0,0 +1,122 @@
1
+ export class ApiError extends Error {
2
+ status;
3
+ constructor(message, status) {
4
+ super(message);
5
+ this.status = status;
6
+ this.name = 'ApiError';
7
+ }
8
+ }
9
+ export class CliApiClient {
10
+ baseUrl;
11
+ apiKey;
12
+ agentPlatform;
13
+ agentRole;
14
+ constructor(config) {
15
+ this.baseUrl = config.baseUrl.replace(/\/$/, '');
16
+ this.apiKey = config.apiKey;
17
+ this.agentPlatform = config.agentPlatform ?? 'cli';
18
+ this.agentRole = config.agentRole ?? '';
19
+ }
20
+ headers(authRequired) {
21
+ const h = {
22
+ 'Content-Type': 'application/json',
23
+ 'X-Agent-Platform': this.agentPlatform,
24
+ 'X-Agent-Role': this.agentRole,
25
+ };
26
+ if (authRequired && this.apiKey) {
27
+ h['Authorization'] = `Bearer ${this.apiKey}`;
28
+ }
29
+ return h;
30
+ }
31
+ async request(method, path, body, authRequired = true) {
32
+ const url = `${this.baseUrl}/v1${path}`;
33
+ const res = await fetch(url, {
34
+ method,
35
+ headers: this.headers(authRequired),
36
+ body: body ? JSON.stringify(body) : undefined,
37
+ });
38
+ let json;
39
+ try {
40
+ json = await res.json();
41
+ }
42
+ catch {
43
+ throw new ApiError(`Invalid JSON response (status ${res.status})`, res.status);
44
+ }
45
+ if (!res.ok || !json.success) {
46
+ throw new ApiError(json.error ?? `Request failed (${res.status})`, res.status);
47
+ }
48
+ return json.data;
49
+ }
50
+ async requestPage(method, path, body) {
51
+ const url = `${this.baseUrl}/v1${path}`;
52
+ const res = await fetch(url, {
53
+ method,
54
+ headers: this.headers(true),
55
+ body: body ? JSON.stringify(body) : undefined,
56
+ });
57
+ const json = await res.json();
58
+ if (!res.ok || !json.success) {
59
+ throw new ApiError(json.error ?? `Request failed (${res.status})`, res.status);
60
+ }
61
+ return { items: (json.data ?? []), cursor: json.cursor ?? null };
62
+ }
63
+ async startCliAuth() {
64
+ return this.request('POST', '/auth/cli/start', {}, false);
65
+ }
66
+ async pollCliAuth(id) {
67
+ return this.request('GET', `/auth/cli/poll?id=${encodeURIComponent(id)}`, undefined, false);
68
+ }
69
+ async getAuthProviders() {
70
+ return this.request('GET', '/auth/providers', undefined, false);
71
+ }
72
+ async whoami() {
73
+ // /v1/auth/me is the canonical principal endpoint; fall back to /v1/org if the worker hasn't shipped it.
74
+ try {
75
+ return await this.request('GET', '/auth/me');
76
+ }
77
+ catch (err) {
78
+ if (err instanceof ApiError && err.status === 404) {
79
+ return await this.request('GET', '/org');
80
+ }
81
+ throw err;
82
+ }
83
+ }
84
+ async listWorkspaces() {
85
+ const all = [];
86
+ let cursor = null;
87
+ for (let i = 0; i < 20; i++) {
88
+ const url = cursor ? `/workspaces?cursor=${encodeURIComponent(cursor)}` : '/workspaces';
89
+ const page = await this.requestPage('GET', url);
90
+ all.push(...page.items);
91
+ if (!page.cursor)
92
+ return all;
93
+ cursor = page.cursor;
94
+ }
95
+ return all;
96
+ }
97
+ async writeFile(workspaceId, path, content, message) {
98
+ return this.request('POST', `/workspaces/${workspaceId}/files`, { path, content, message });
99
+ }
100
+ async readFileByPath(workspaceId, path) {
101
+ const files = await this.requestPage('GET', `/workspaces/${workspaceId}/files?prefix=${encodeURIComponent(path)}`);
102
+ const match = files.items.find((f) => f.path === path) ?? files.items[0];
103
+ if (!match)
104
+ throw new ApiError(`File not found: ${path}`, 404);
105
+ return this.request('GET', `/workspaces/${workspaceId}/files/${match.id}?content=true`);
106
+ }
107
+ async search(body) {
108
+ return this.request('POST', '/search', body);
109
+ }
110
+ async context(body) {
111
+ return this.request('POST', '/context', body);
112
+ }
113
+ async logRun(body) {
114
+ return this.request('POST', '/runs', body);
115
+ }
116
+ async billingPortal() {
117
+ return this.request('POST', '/billing/portal', {});
118
+ }
119
+ async billingPlan() {
120
+ return this.request('GET', '/billing/plan');
121
+ }
122
+ }
@@ -0,0 +1,65 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ export const DEFAULT_API_URL = 'https://app.agentdrive.sh';
5
+ export function configDir(env = process.env, home = homedir()) {
6
+ const xdg = env.XDG_CONFIG_HOME;
7
+ const base = xdg && xdg.length > 0 ? xdg : join(home, '.config');
8
+ return join(base, 'agentdrive');
9
+ }
10
+ export function configPath(env = process.env, home = homedir()) {
11
+ return join(configDir(env, home), 'config.json');
12
+ }
13
+ export async function readConfig(path = configPath()) {
14
+ try {
15
+ const raw = await fs.readFile(path, 'utf8');
16
+ const parsed = JSON.parse(raw);
17
+ if (!parsed || typeof parsed !== 'object')
18
+ return null;
19
+ return {
20
+ api_url: parsed.api_url ?? DEFAULT_API_URL,
21
+ api_key: parsed.api_key,
22
+ agent_id: parsed.agent_id,
23
+ org_id: parsed.org_id,
24
+ agent_platform: parsed.agent_platform ?? 'cli',
25
+ agent_role: parsed.agent_role,
26
+ default_workspace: parsed.default_workspace,
27
+ };
28
+ }
29
+ catch (err) {
30
+ const e = err;
31
+ if (e.code === 'ENOENT')
32
+ return null;
33
+ throw err;
34
+ }
35
+ }
36
+ export async function writeConfig(config, path = configPath()) {
37
+ await fs.mkdir(dirname(path), { recursive: true, mode: 0o700 });
38
+ const payload = JSON.stringify(config, null, 2);
39
+ await fs.writeFile(path, payload, { encoding: 'utf8', mode: 0o600 });
40
+ try {
41
+ await fs.chmod(path, 0o600);
42
+ }
43
+ catch {
44
+ // chmod can fail on non-POSIX; permissions already set via writeFile mode.
45
+ }
46
+ }
47
+ export async function deleteConfig(path = configPath()) {
48
+ try {
49
+ await fs.unlink(path);
50
+ return true;
51
+ }
52
+ catch (err) {
53
+ const e = err;
54
+ if (e.code === 'ENOENT')
55
+ return false;
56
+ throw err;
57
+ }
58
+ }
59
+ export async function requireLogin(path = configPath()) {
60
+ const c = await readConfig(path);
61
+ if (!c || !c.api_key) {
62
+ throw new Error('Not logged in. Run `adrive auth login` first.');
63
+ }
64
+ return c;
65
+ }
@@ -0,0 +1,223 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { homedir, platform } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ export function getAgentTargets(opts = {}) {
5
+ const env = opts.env ?? process.env;
6
+ const home = opts.home ?? homedir();
7
+ const os = opts.osPlatform ?? platform();
8
+ const claudeDesktop = os === 'darwin'
9
+ ? join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
10
+ : os === 'win32'
11
+ ? join(env.APPDATA ?? join(home, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json')
12
+ : join(home, '.config', 'Claude', 'claude_desktop_config.json');
13
+ return [
14
+ {
15
+ name: 'claude-code',
16
+ configPath: join(home, '.claude', 'mcp-servers.json'),
17
+ format: 'mcp-json',
18
+ defaultPlatform: 'claude-code',
19
+ },
20
+ {
21
+ name: 'claude-desktop',
22
+ configPath: claudeDesktop,
23
+ format: 'mcp-json',
24
+ defaultPlatform: 'claude-code',
25
+ },
26
+ {
27
+ name: 'codex',
28
+ configPath: join(home, '.codex', 'config.toml'),
29
+ format: 'codex-toml',
30
+ defaultPlatform: 'codex',
31
+ },
32
+ {
33
+ name: 'cursor',
34
+ configPath: join(home, '.cursor', 'mcp.json'),
35
+ format: 'cursor-json',
36
+ defaultPlatform: 'cursor',
37
+ },
38
+ {
39
+ name: 'continue',
40
+ configPath: join(home, '.continue', 'config.json'),
41
+ format: 'continue-json',
42
+ defaultPlatform: 'continue',
43
+ },
44
+ {
45
+ name: 'zed',
46
+ configPath: os === 'darwin'
47
+ ? join(home, '.config', 'zed', 'settings.json')
48
+ : join(home, '.config', 'zed', 'settings.json'),
49
+ format: 'zed-json',
50
+ defaultPlatform: 'zed',
51
+ },
52
+ {
53
+ name: 'cline',
54
+ configPath: os === 'darwin'
55
+ ? join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json')
56
+ : join(home, '.config', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'),
57
+ format: 'mcp-json',
58
+ defaultPlatform: 'cline',
59
+ },
60
+ ];
61
+ }
62
+ async function fileExists(path) {
63
+ try {
64
+ await fs.access(path);
65
+ return true;
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
71
+ async function readJsonSafe(path) {
72
+ try {
73
+ const raw = await fs.readFile(path, 'utf8');
74
+ if (!raw.trim())
75
+ return {};
76
+ return JSON.parse(raw);
77
+ }
78
+ catch (err) {
79
+ const e = err;
80
+ if (e.code === 'ENOENT')
81
+ return null;
82
+ return null;
83
+ }
84
+ }
85
+ function mcpEntry(env, platformOverride) {
86
+ const envBlock = {
87
+ AGENTDRIVE_API_KEY: env.apiKey,
88
+ };
89
+ if (env.apiUrl)
90
+ envBlock.AGENTDRIVE_API_URL = env.apiUrl;
91
+ const plat = env.agentPlatform ?? platformOverride;
92
+ if (plat)
93
+ envBlock.AGENTDRIVE_AGENT_PLATFORM = plat;
94
+ return {
95
+ command: 'npx',
96
+ args: ['-y', '@adrive/mcp-server'],
97
+ env: envBlock,
98
+ };
99
+ }
100
+ export function mergeMcpJson(existing, env, platformOverride) {
101
+ const base = existing && typeof existing === 'object' ? { ...existing } : {};
102
+ const serversRaw = base.mcpServers;
103
+ const servers = serversRaw && typeof serversRaw === 'object' && !Array.isArray(serversRaw)
104
+ ? { ...serversRaw }
105
+ : {};
106
+ servers.agentdrive = mcpEntry(env, platformOverride);
107
+ base.mcpServers = servers;
108
+ return base;
109
+ }
110
+ export function mergeCursorJson(existing, env) {
111
+ return mergeMcpJson(existing, env, 'cursor');
112
+ }
113
+ export function mergeContinueJson(existing, env) {
114
+ const base = existing && typeof existing === 'object' ? { ...existing } : {};
115
+ const rawList = base.mcpServers;
116
+ const list = Array.isArray(rawList) ? [...rawList] : [];
117
+ const filtered = list.filter((s) => {
118
+ if (!s || typeof s !== 'object')
119
+ return true;
120
+ return s.name !== 'agentdrive';
121
+ });
122
+ filtered.push({
123
+ name: 'agentdrive',
124
+ ...mcpEntry(env, 'continue'),
125
+ });
126
+ base.mcpServers = filtered;
127
+ return base;
128
+ }
129
+ export function mergeZedJson(existing, env) {
130
+ const base = existing && typeof existing === 'object' ? { ...existing } : {};
131
+ const ctxRaw = base.context_servers;
132
+ const ctx = ctxRaw && typeof ctxRaw === 'object' && !Array.isArray(ctxRaw)
133
+ ? { ...ctxRaw }
134
+ : {};
135
+ ctx.agentdrive = mcpEntry(env, 'zed');
136
+ base.context_servers = ctx;
137
+ return base;
138
+ }
139
+ export function mergeCodexToml(existing, env) {
140
+ const blockHeader = '[[mcp_servers]]';
141
+ const target = 'name = "agentdrive"';
142
+ const apiKey = env.apiKey.replace(/"/g, '\\"');
143
+ const apiUrlLine = env.apiUrl ? `, AGENTDRIVE_API_URL = "${env.apiUrl}"` : '';
144
+ const newBlock = `${blockHeader}\nname = "agentdrive"\ncommand = "npx"\nargs = ["-y", "@adrive/mcp-server"]\n` +
145
+ `env = { AGENTDRIVE_API_KEY = "${apiKey}", AGENTDRIVE_AGENT_PLATFORM = "codex"${apiUrlLine} }\n`;
146
+ const trimmed = existing.trimEnd();
147
+ if (!trimmed)
148
+ return newBlock;
149
+ if (!trimmed.includes(target)) {
150
+ return `${trimmed}\n\n${newBlock}`;
151
+ }
152
+ // Replace the existing agentdrive block in place: scan blocks, swap the matching one.
153
+ const blocks = trimmed.split(/(?=^\[\[mcp_servers\]\]$)/m);
154
+ const updated = blocks.map((block) => {
155
+ if (block.includes(target))
156
+ return newBlock.trimEnd();
157
+ return block.trimEnd();
158
+ });
159
+ return `${updated.join('\n\n')}\n`;
160
+ }
161
+ export async function wireAgent(target, env, opts = {}) {
162
+ const exists = await fileExists(target.configPath);
163
+ if (!exists && !opts.writeIfMissing) {
164
+ throw new Error(`Config not found: ${target.configPath}`);
165
+ }
166
+ await fs.mkdir(dirname(target.configPath), { recursive: true });
167
+ if (target.format === 'codex-toml') {
168
+ const existing = exists ? await fs.readFile(target.configPath, 'utf8') : '';
169
+ const merged = mergeCodexToml(existing, env);
170
+ await fs.writeFile(target.configPath, merged, 'utf8');
171
+ return;
172
+ }
173
+ const existing = exists ? await readJsonSafe(target.configPath) : {};
174
+ let merged;
175
+ switch (target.format) {
176
+ case 'mcp-json':
177
+ merged = mergeMcpJson(existing, env, target.defaultPlatform);
178
+ break;
179
+ case 'cursor-json':
180
+ merged = mergeCursorJson(existing, env);
181
+ break;
182
+ case 'continue-json':
183
+ merged = mergeContinueJson(existing, env);
184
+ break;
185
+ case 'zed-json':
186
+ merged = mergeZedJson(existing, env);
187
+ break;
188
+ default:
189
+ throw new Error(`Unknown format: ${String(target.format)}`);
190
+ }
191
+ await fs.writeFile(target.configPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8');
192
+ }
193
+ export async function detectInstalledAgents(opts = {}) {
194
+ const targets = getAgentTargets(opts);
195
+ const present = [];
196
+ for (const t of targets) {
197
+ if (await fileExists(t.configPath))
198
+ present.push(t);
199
+ }
200
+ return present;
201
+ }
202
+ export async function wireAllInstalled(env, opts = {}) {
203
+ const all = getAgentTargets(opts);
204
+ const wired = [];
205
+ const skipped = [];
206
+ for (const t of all) {
207
+ const exists = await fileExists(t.configPath);
208
+ const forceWrite = opts.includeMissing?.includes(t.name) ?? false;
209
+ if (!exists && !forceWrite) {
210
+ skipped.push({ name: t.name, reason: 'config not found' });
211
+ continue;
212
+ }
213
+ try {
214
+ await wireAgent(t, env, { writeIfMissing: forceWrite });
215
+ wired.push(t.name);
216
+ }
217
+ catch (err) {
218
+ const msg = err instanceof Error ? err.message : String(err);
219
+ skipped.push({ name: t.name, reason: msg });
220
+ }
221
+ }
222
+ return { wired, skipped };
223
+ }
@@ -0,0 +1,57 @@
1
+ import open from 'open';
2
+ import kleur from 'kleur';
3
+ const SPINNER_FRAMES = ['|', '/', '-', '\\'];
4
+ function defaultSleep(ms) {
5
+ return new Promise((resolve) => setTimeout(resolve, ms));
6
+ }
7
+ export async function pollAuth(client, options = {}) {
8
+ const pollIntervalMs = options.pollIntervalMs ?? 1500;
9
+ const timeoutMs = options.timeoutMs ?? 10 * 60 * 1000;
10
+ const log = options.log ?? ((line) => process.stdout.write(`${line}\n`));
11
+ const sleep = options.sleep ?? defaultSleep;
12
+ const browserOpener = options.openBrowser ?? ((url) => open(url));
13
+ const start = await client.startCliAuth();
14
+ log(kleur.bold('Opening browser to log in...'));
15
+ log(` ${kleur.cyan(start.auth_url)}`);
16
+ log(' If the browser does not open, copy and paste the URL above.');
17
+ try {
18
+ await browserOpener(start.auth_url);
19
+ }
20
+ catch {
21
+ log(kleur.yellow('[warn] Could not auto-open browser. Use the URL above.'));
22
+ }
23
+ const deadline = Date.now() + timeoutMs;
24
+ let frame = 0;
25
+ const showSpinner = options.spinner !== false && process.stdout.isTTY;
26
+ while (Date.now() < deadline) {
27
+ let poll;
28
+ try {
29
+ poll = await client.pollCliAuth(start.id);
30
+ }
31
+ catch (err) {
32
+ if (showSpinner)
33
+ process.stdout.write('\r');
34
+ throw err;
35
+ }
36
+ if (poll.status === 'complete') {
37
+ if (showSpinner)
38
+ process.stdout.write('\r');
39
+ log(kleur.green('[ok] Logged in.'));
40
+ return {
41
+ api_key: poll.api_key,
42
+ agent_id: poll.agent_id,
43
+ org_id: poll.org_id,
44
+ session_token: poll.session_token,
45
+ };
46
+ }
47
+ if (showSpinner) {
48
+ const f = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
49
+ frame += 1;
50
+ process.stdout.write(`\r${kleur.dim(`[wait] ${f} polling for login...`)}`);
51
+ }
52
+ await sleep(pollIntervalMs);
53
+ }
54
+ if (showSpinner)
55
+ process.stdout.write('\r');
56
+ throw new Error('Login timed out. Run `adrive auth login` again.');
57
+ }