@botdocs/cli 0.3.2 → 0.4.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.
- package/README.md +123 -37
- package/dist/commands/backups.d.ts +4 -0
- package/dist/commands/backups.js +291 -0
- package/dist/commands/edit.js +16 -8
- package/dist/commands/install.d.ts +4 -0
- package/dist/commands/install.js +21 -3
- package/dist/commands/login.d.ts +7 -0
- package/dist/commands/login.js +240 -75
- package/dist/commands/sync.d.ts +16 -0
- package/dist/commands/sync.js +337 -25
- package/dist/commands/team.d.ts +2 -0
- package/dist/commands/team.js +251 -0
- package/dist/commands/undo.d.ts +19 -0
- package/dist/commands/undo.js +88 -0
- package/dist/commands/views/conflict-prompt.d.ts +24 -0
- package/dist/commands/views/conflict-prompt.js +19 -0
- package/dist/commands/views/login-app.d.ts +30 -0
- package/dist/commands/views/login-app.js +57 -0
- package/dist/commands/views/sync-app.d.ts +27 -0
- package/dist/commands/views/sync-app.js +147 -0
- package/dist/commands/views/sync-state.d.ts +84 -0
- package/dist/commands/views/sync-state.js +93 -0
- package/dist/commands/views/theme.d.ts +16 -0
- package/dist/commands/views/theme.js +16 -0
- package/dist/commands/whoami.js +13 -13
- package/dist/index.js +44 -38
- package/dist/lib/api.d.ts +2 -3
- package/dist/lib/api.js +14 -7
- package/dist/lib/auto-detect.js +46 -0
- package/dist/lib/backup.d.ts +121 -0
- package/dist/lib/backup.js +387 -0
- package/dist/lib/canonical.d.ts +1 -1
- package/dist/lib/canonical.js +43 -1
- package/dist/lib/config.d.ts +8 -1
- package/dist/lib/config.js +18 -9
- package/dist/lib/lockfile.d.ts +9 -0
- package/dist/lib/prompts.d.ts +10 -0
- package/dist/lib/prompts.js +36 -12
- package/package.json +27 -7
- package/templates/agents.md +60 -47
- package/templates/ecosystem-prompts/compile-antigravity.md +14 -0
- package/templates/ecosystem-prompts/compile-copilot.md +14 -0
- package/templates/ecosystem-prompts/compile-gemini.md +14 -0
- package/templates/ecosystem-prompts/compile-opencode.md +13 -0
- package/templates/ecosystem-prompts/compile-windsurf.md +13 -0
- package/dist/commands/check-updates.test.d.ts +0 -1
- package/dist/commands/check-updates.test.js +0 -128
- package/dist/commands/clone.d.ts +0 -3
- package/dist/commands/clone.js +0 -70
- package/dist/commands/compile.test.d.ts +0 -1
- package/dist/commands/compile.test.js +0 -110
- package/dist/commands/diff.d.ts +0 -3
- package/dist/commands/diff.js +0 -65
- package/dist/commands/edit.test.d.ts +0 -1
- package/dist/commands/edit.test.js +0 -102
- package/dist/commands/endorse.d.ts +0 -7
- package/dist/commands/endorse.js +0 -70
- package/dist/commands/ingest.test.d.ts +0 -1
- package/dist/commands/ingest.test.js +0 -109
- package/dist/commands/install.test.d.ts +0 -1
- package/dist/commands/install.test.js +0 -253
- package/dist/commands/list.test.d.ts +0 -1
- package/dist/commands/list.test.js +0 -51
- package/dist/commands/publish.test.d.ts +0 -1
- package/dist/commands/publish.test.js +0 -138
- package/dist/commands/pull.d.ts +0 -3
- package/dist/commands/pull.js +0 -78
- package/dist/commands/sync.test.d.ts +0 -1
- package/dist/commands/sync.test.js +0 -263
- package/dist/commands/uninstall.test.d.ts +0 -1
- package/dist/commands/uninstall.test.js +0 -67
- package/dist/lib/auto-detect.test.d.ts +0 -1
- package/dist/lib/auto-detect.test.js +0 -58
- package/dist/lib/canonical.test.d.ts +0 -1
- package/dist/lib/canonical.test.js +0 -48
- package/dist/lib/diff.test.d.ts +0 -1
- package/dist/lib/diff.test.js +0 -28
- package/dist/lib/library-sync.test.d.ts +0 -1
- package/dist/lib/library-sync.test.js +0 -63
- package/dist/lib/llm.test.d.ts +0 -1
- package/dist/lib/llm.test.js +0 -72
- package/dist/lib/lockfile.test.d.ts +0 -1
- package/dist/lib/lockfile.test.js +0 -99
- package/dist/lib/manifest.test.d.ts +0 -1
- package/dist/lib/manifest.test.js +0 -72
- package/dist/lib/shell-hook.test.d.ts +0 -1
- package/dist/lib/shell-hook.test.js +0 -68
- package/dist/test-utils.d.ts +0 -43
- package/dist/test-utils.js +0 -101
package/dist/commands/login.js
CHANGED
|
@@ -1,87 +1,252 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import open from 'open';
|
|
4
|
+
import { render } from 'ink';
|
|
5
|
+
import * as p from '@clack/prompts';
|
|
1
6
|
import { saveAuth } from '../lib/config.js';
|
|
2
|
-
|
|
3
|
-
|
|
7
|
+
import { getApiUrl } from '../lib/api.js';
|
|
8
|
+
import { LoginApp } from './views/login-app.js';
|
|
9
|
+
/** Total wall-clock budget for the polling loop. After this we tell the user
|
|
10
|
+
* the request expired and exit 1. Mirrors the server-side state TTL. */
|
|
11
|
+
const POLL_TIMEOUT_MS = 10 * 60 * 1000;
|
|
12
|
+
const POLL_INITIAL_DELAY_MS = 1500;
|
|
13
|
+
const POLL_MAX_DELAY_MS = 5000;
|
|
14
|
+
const POLL_BACKOFF_FACTOR = 1.3;
|
|
4
15
|
export async function login(options) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
if (options?.token) {
|
|
17
|
+
await loginWithToken(options.token, options.syncLibrary === true);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
// Pick a render path: Ink for a real TTY (unless --no-ink), plain otherwise.
|
|
21
|
+
// The plain path covers piped output, CI, screen readers, and explicit opt-out.
|
|
22
|
+
const useInk = !options?.noInk && Boolean(process.stdout.isTTY);
|
|
23
|
+
if (useInk) {
|
|
24
|
+
await loginInkInteractive(options?.syncLibrary === true);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
await loginPlainInteractive(options?.syncLibrary === true);
|
|
28
|
+
}
|
|
29
|
+
/** --token flag flow: validate the supplied token by hitting the API and, on
|
|
30
|
+
* success, persist it to ~/.botdocs/auth.json so subsequent commands can reuse
|
|
31
|
+
* the bearer. Stays plain — this is paste-then-save with no perceivable
|
|
32
|
+
* latency, so a TUI adds friction with no upside. */
|
|
33
|
+
async function loginWithToken(token, syncLibrary) {
|
|
34
|
+
if (!token.startsWith('bd_')) {
|
|
35
|
+
console.error('Invalid token. Tokens start with `bd_` and are minted at /settings/tokens.');
|
|
19
36
|
process.exit(1);
|
|
20
37
|
}
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
console.log('Waiting for authorization...');
|
|
27
|
-
// Step 3: Poll for token
|
|
28
|
-
const interval = (deviceData.interval || 5) * 1000;
|
|
29
|
-
const expiresAt = Date.now() + deviceData.expires_in * 1000;
|
|
30
|
-
while (Date.now() < expiresAt) {
|
|
31
|
-
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
32
|
-
const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
|
|
33
|
-
method: 'POST',
|
|
34
|
-
headers: {
|
|
35
|
-
Accept: 'application/json',
|
|
36
|
-
'Content-Type': 'application/json',
|
|
37
|
-
},
|
|
38
|
-
body: JSON.stringify({
|
|
39
|
-
client_id: GITHUB_CLIENT_ID,
|
|
40
|
-
device_code: deviceData.device_code,
|
|
41
|
-
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
42
|
-
}),
|
|
38
|
+
const baseUrl = getApiUrl();
|
|
39
|
+
let res;
|
|
40
|
+
try {
|
|
41
|
+
res = await fetch(`${baseUrl}/api/cli/whoami`, {
|
|
42
|
+
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
|
|
43
43
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
console.error(`Failed to contact ${baseUrl}: ${err instanceof Error ? err.message : String(err)}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
if (res.status === 401) {
|
|
50
|
+
console.error('Authentication failed. The token is invalid or expired.');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
console.error(`Token validation failed (${res.status}). Try again later.`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
const user = (await res.json());
|
|
58
|
+
saveAuth({
|
|
59
|
+
token,
|
|
60
|
+
username: user.username,
|
|
61
|
+
displayName: user.displayName,
|
|
62
|
+
syncLibrary,
|
|
63
|
+
});
|
|
64
|
+
printSignedIn(user.username, syncLibrary);
|
|
65
|
+
}
|
|
66
|
+
/** Helper: register a new state with the server and return the public auth
|
|
67
|
+
* URL plus the last-6-chars suffix the user can verify in their browser. */
|
|
68
|
+
async function initLoginState(baseUrl) {
|
|
69
|
+
const state = randomBytes(32).toString('hex');
|
|
70
|
+
const initRes = await fetch(`${baseUrl}/api/cli/auth/init`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
73
|
+
body: JSON.stringify({ state }),
|
|
74
|
+
});
|
|
75
|
+
if (!initRes.ok) {
|
|
76
|
+
throw new Error(`Failed to start login (HTTP ${initRes.status}).`);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
state,
|
|
80
|
+
authUrl: `${baseUrl}/cli-auth?state=${state}`,
|
|
81
|
+
tail: state.slice(-6),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Browser-mediated flow rendered via Ink. The component is a pure function
|
|
85
|
+
* of `status`; we drive that state via React updates from the actual auth
|
|
86
|
+
* work (init → open → poll → save) happening here. On any terminal state
|
|
87
|
+
* (success/expired/error) we re-render once with the final values and let
|
|
88
|
+
* the component's effect call `useApp().exit()` to unmount. */
|
|
89
|
+
async function loginInkInteractive(syncLibrary) {
|
|
90
|
+
const baseUrl = getApiUrl();
|
|
91
|
+
let status = 'initializing';
|
|
92
|
+
let authUrl;
|
|
93
|
+
let stateTail;
|
|
94
|
+
let resolvedUsername;
|
|
95
|
+
let errorMessage;
|
|
96
|
+
const instance = render(React.createElement(LoginApp, {
|
|
97
|
+
status,
|
|
98
|
+
syncLibrary,
|
|
99
|
+
}));
|
|
100
|
+
// Re-render the LoginApp with current state. Wrapped in a helper because
|
|
101
|
+
// we update the screen at multiple points across the async flow.
|
|
102
|
+
const rerender = () => {
|
|
103
|
+
instance.rerender(React.createElement(LoginApp, {
|
|
104
|
+
status,
|
|
105
|
+
authUrl,
|
|
106
|
+
stateTail,
|
|
107
|
+
username: resolvedUsername,
|
|
108
|
+
errorMessage,
|
|
109
|
+
syncLibrary,
|
|
110
|
+
}));
|
|
111
|
+
};
|
|
112
|
+
// Allow Ctrl-C to cleanly cancel mid-poll. We let Ink unmount the screen
|
|
113
|
+
// before exiting so the terminal isn't left in raw mode.
|
|
114
|
+
const onSigInt = () => {
|
|
115
|
+
instance.unmount();
|
|
116
|
+
console.log('\nLogin cancelled. Re-run `botdocs login` to try again.');
|
|
117
|
+
process.exit(130);
|
|
118
|
+
};
|
|
119
|
+
process.once('SIGINT', onSigInt);
|
|
120
|
+
try {
|
|
121
|
+
// Phase 1: register state with the server.
|
|
122
|
+
let init;
|
|
123
|
+
try {
|
|
124
|
+
init = await initLoginState(baseUrl);
|
|
73
125
|
}
|
|
74
|
-
|
|
75
|
-
|
|
126
|
+
catch (err) {
|
|
127
|
+
status = 'error';
|
|
128
|
+
errorMessage = err instanceof Error ? err.message : String(err);
|
|
129
|
+
rerender();
|
|
130
|
+
await instance.waitUntilExit();
|
|
76
131
|
process.exit(1);
|
|
77
132
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
133
|
+
authUrl = init.authUrl;
|
|
134
|
+
stateTail = init.tail;
|
|
135
|
+
status = 'browser-opening';
|
|
136
|
+
rerender();
|
|
137
|
+
// Phase 2: best-effort browser open. We don't await long here — the
|
|
138
|
+
// printed URL in the polling state acts as the fallback if the OS hook
|
|
139
|
+
// is unreliable (CI, restricted shells, etc.).
|
|
140
|
+
try {
|
|
141
|
+
await open(init.authUrl);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Swallowed — URL is still visible on screen.
|
|
145
|
+
}
|
|
146
|
+
// Phase 3: poll until the server hands us a token.
|
|
147
|
+
status = 'polling';
|
|
148
|
+
rerender();
|
|
149
|
+
const granted = await pollUntilGranted(baseUrl, init.state);
|
|
150
|
+
if (!granted) {
|
|
151
|
+
status = 'expired';
|
|
152
|
+
rerender();
|
|
153
|
+
await instance.waitUntilExit();
|
|
82
154
|
process.exit(1);
|
|
83
155
|
}
|
|
156
|
+
saveAuth({
|
|
157
|
+
token: granted.token,
|
|
158
|
+
username: granted.username,
|
|
159
|
+
displayName: granted.displayName,
|
|
160
|
+
syncLibrary,
|
|
161
|
+
});
|
|
162
|
+
resolvedUsername = granted.username;
|
|
163
|
+
status = 'success';
|
|
164
|
+
rerender();
|
|
165
|
+
await instance.waitUntilExit();
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
process.off('SIGINT', onSigInt);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** Plain-text fallback for non-TTY environments (CI, piped output, screen
|
|
172
|
+
* readers) and `--no-ink`. Uses a clack spinner for a tasteful single-line
|
|
173
|
+
* polling indicator; the spinner degrades gracefully when stdout isn't a TTY. */
|
|
174
|
+
async function loginPlainInteractive(syncLibrary) {
|
|
175
|
+
const baseUrl = getApiUrl();
|
|
176
|
+
let init;
|
|
177
|
+
try {
|
|
178
|
+
init = await initLoginState(baseUrl);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
console.log('\nOpening your browser to authorize this terminal session.');
|
|
185
|
+
console.log(`If it doesn't open, visit:\n\n ${init.authUrl}\n`);
|
|
186
|
+
console.log(`Confirm the suffix matches: …${init.tail}\n`);
|
|
187
|
+
try {
|
|
188
|
+
await open(init.authUrl);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// Suppressed — the printed URL is the fallback.
|
|
192
|
+
}
|
|
193
|
+
const sp = p.spinner();
|
|
194
|
+
sp.start('Waiting for authorization (up to 10 minutes)…');
|
|
195
|
+
const granted = await pollUntilGranted(baseUrl, init.state);
|
|
196
|
+
if (!granted) {
|
|
197
|
+
sp.stop('Login expired.');
|
|
198
|
+
// Tests assert on stderr — keep the human-readable failure there so
|
|
199
|
+
// automation can pipe stderr separately without losing the signal.
|
|
200
|
+
console.error('\nLogin expired — re-run `botdocs login`.');
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
sp.stop(`Signed in as @${granted.username}`);
|
|
204
|
+
saveAuth({
|
|
205
|
+
token: granted.token,
|
|
206
|
+
username: granted.username,
|
|
207
|
+
displayName: granted.displayName,
|
|
208
|
+
syncLibrary,
|
|
209
|
+
});
|
|
210
|
+
printSyncLibraryHint(syncLibrary);
|
|
211
|
+
}
|
|
212
|
+
/** Polls the server with exponential backoff. Returns the granted payload on
|
|
213
|
+
* success or null if the loop ran out of budget (matching the server's TTL). */
|
|
214
|
+
async function pollUntilGranted(baseUrl, state) {
|
|
215
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
216
|
+
let delay = POLL_INITIAL_DELAY_MS;
|
|
217
|
+
while (Date.now() < deadline) {
|
|
218
|
+
await sleep(delay);
|
|
219
|
+
delay = Math.min(delay * POLL_BACKOFF_FACTOR, POLL_MAX_DELAY_MS);
|
|
220
|
+
const res = await fetch(`${baseUrl}/api/cli/auth/poll?state=${encodeURIComponent(state)}`, { headers: { Accept: 'application/json' } });
|
|
221
|
+
if (res.status === 410) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
if (!res.ok) {
|
|
225
|
+
// Transient server errors — keep polling instead of exiting.
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const body = (await res.json());
|
|
229
|
+
if ('pending' in body && body.pending) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if ('token' in body) {
|
|
233
|
+
return body;
|
|
234
|
+
}
|
|
84
235
|
}
|
|
85
|
-
|
|
86
|
-
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
function printSignedIn(username, syncLibrary) {
|
|
239
|
+
console.log(`\n✓ Signed in as @${username}`);
|
|
240
|
+
printSyncLibraryHint(syncLibrary);
|
|
241
|
+
}
|
|
242
|
+
function printSyncLibraryHint(syncLibrary) {
|
|
243
|
+
if (syncLibrary) {
|
|
244
|
+
console.log(' Library sync enabled. Your installed-refs list will appear at https://botdocs.ai/library after install/sync/uninstall.');
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
console.log(' Library sync is OFF. Re-run `botdocs login --sync-library` to enable the personalized Library page.');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function sleep(ms) {
|
|
251
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
87
252
|
}
|
package/dist/commands/sync.d.ts
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
|
+
import type { SyncAction } from './views/sync-state.js';
|
|
1
2
|
interface SyncOptions {
|
|
2
3
|
yes?: boolean;
|
|
3
4
|
dryRun?: boolean;
|
|
4
5
|
json?: boolean;
|
|
6
|
+
/** When true, skip backups before overwriting. Default behavior backs up
|
|
7
|
+
* any locally-edited or untracked file at a sync destination before the
|
|
8
|
+
* overwrite. --dry-run prints "would back up" lines without writing. */
|
|
9
|
+
noBackup?: boolean;
|
|
10
|
+
/** Force the plain-text rendering path even on a real TTY. Mirrors the
|
|
11
|
+
* same flag on `login` — useful for screen readers and anyone who finds
|
|
12
|
+
* the live redraw distracting. */
|
|
13
|
+
noInk?: boolean;
|
|
5
14
|
}
|
|
15
|
+
/** The three choices a user can make on a per-skill conflict. `keep` and
|
|
16
|
+
* `skip` are semantically identical today (both leave the local file alone)
|
|
17
|
+
* but kept separate so we can distinguish "I explicitly want my version" from
|
|
18
|
+
* "let me decide later" in future UX. */
|
|
19
|
+
export type ConflictChoice = 'keep' | 'overwrite' | 'skip';
|
|
20
|
+
export type AwaitConflictChoice = (ref: string, file: string) => Promise<ConflictChoice>;
|
|
21
|
+
export type SyncDispatch = (action: SyncAction) => void;
|
|
6
22
|
export declare function sync(rawRef: string | undefined, options: SyncOptions): Promise<void>;
|
|
7
23
|
export {};
|