@botdocs/cli 0.3.1 → 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.
Files changed (90) hide show
  1. package/README.md +123 -37
  2. package/dist/commands/backups.d.ts +4 -0
  3. package/dist/commands/backups.js +291 -0
  4. package/dist/commands/edit.js +16 -8
  5. package/dist/commands/install.d.ts +4 -0
  6. package/dist/commands/install.js +21 -3
  7. package/dist/commands/login.d.ts +7 -0
  8. package/dist/commands/login.js +240 -75
  9. package/dist/commands/publish.js +53 -16
  10. package/dist/commands/sync.d.ts +16 -0
  11. package/dist/commands/sync.js +337 -25
  12. package/dist/commands/team.d.ts +2 -0
  13. package/dist/commands/team.js +251 -0
  14. package/dist/commands/undo.d.ts +19 -0
  15. package/dist/commands/undo.js +88 -0
  16. package/dist/commands/views/conflict-prompt.d.ts +24 -0
  17. package/dist/commands/views/conflict-prompt.js +19 -0
  18. package/dist/commands/views/login-app.d.ts +30 -0
  19. package/dist/commands/views/login-app.js +57 -0
  20. package/dist/commands/views/sync-app.d.ts +27 -0
  21. package/dist/commands/views/sync-app.js +147 -0
  22. package/dist/commands/views/sync-state.d.ts +84 -0
  23. package/dist/commands/views/sync-state.js +93 -0
  24. package/dist/commands/views/theme.d.ts +16 -0
  25. package/dist/commands/views/theme.js +16 -0
  26. package/dist/commands/whoami.js +13 -13
  27. package/dist/index.js +44 -38
  28. package/dist/lib/api.d.ts +2 -3
  29. package/dist/lib/api.js +14 -7
  30. package/dist/lib/auto-detect.js +46 -0
  31. package/dist/lib/backup.d.ts +121 -0
  32. package/dist/lib/backup.js +387 -0
  33. package/dist/lib/canonical.d.ts +1 -1
  34. package/dist/lib/canonical.js +43 -1
  35. package/dist/lib/config.d.ts +8 -1
  36. package/dist/lib/config.js +18 -9
  37. package/dist/lib/lockfile.d.ts +9 -0
  38. package/dist/lib/prompts.d.ts +10 -0
  39. package/dist/lib/prompts.js +36 -12
  40. package/package.json +27 -7
  41. package/templates/agents.md +60 -47
  42. package/templates/ecosystem-prompts/compile-antigravity.md +14 -0
  43. package/templates/ecosystem-prompts/compile-copilot.md +14 -0
  44. package/templates/ecosystem-prompts/compile-gemini.md +14 -0
  45. package/templates/ecosystem-prompts/compile-opencode.md +13 -0
  46. package/templates/ecosystem-prompts/compile-windsurf.md +13 -0
  47. package/dist/commands/check-updates.test.d.ts +0 -1
  48. package/dist/commands/check-updates.test.js +0 -128
  49. package/dist/commands/clone.d.ts +0 -3
  50. package/dist/commands/clone.js +0 -70
  51. package/dist/commands/compile.test.d.ts +0 -1
  52. package/dist/commands/compile.test.js +0 -110
  53. package/dist/commands/diff.d.ts +0 -3
  54. package/dist/commands/diff.js +0 -65
  55. package/dist/commands/edit.test.d.ts +0 -1
  56. package/dist/commands/edit.test.js +0 -102
  57. package/dist/commands/endorse.d.ts +0 -7
  58. package/dist/commands/endorse.js +0 -70
  59. package/dist/commands/ingest.test.d.ts +0 -1
  60. package/dist/commands/ingest.test.js +0 -109
  61. package/dist/commands/install.test.d.ts +0 -1
  62. package/dist/commands/install.test.js +0 -253
  63. package/dist/commands/list.test.d.ts +0 -1
  64. package/dist/commands/list.test.js +0 -51
  65. package/dist/commands/publish.test.d.ts +0 -1
  66. package/dist/commands/publish.test.js +0 -76
  67. package/dist/commands/pull.d.ts +0 -3
  68. package/dist/commands/pull.js +0 -78
  69. package/dist/commands/sync.test.d.ts +0 -1
  70. package/dist/commands/sync.test.js +0 -263
  71. package/dist/commands/uninstall.test.d.ts +0 -1
  72. package/dist/commands/uninstall.test.js +0 -67
  73. package/dist/lib/auto-detect.test.d.ts +0 -1
  74. package/dist/lib/auto-detect.test.js +0 -58
  75. package/dist/lib/canonical.test.d.ts +0 -1
  76. package/dist/lib/canonical.test.js +0 -48
  77. package/dist/lib/diff.test.d.ts +0 -1
  78. package/dist/lib/diff.test.js +0 -28
  79. package/dist/lib/library-sync.test.d.ts +0 -1
  80. package/dist/lib/library-sync.test.js +0 -63
  81. package/dist/lib/llm.test.d.ts +0 -1
  82. package/dist/lib/llm.test.js +0 -72
  83. package/dist/lib/lockfile.test.d.ts +0 -1
  84. package/dist/lib/lockfile.test.js +0 -99
  85. package/dist/lib/manifest.test.d.ts +0 -1
  86. package/dist/lib/manifest.test.js +0 -72
  87. package/dist/lib/shell-hook.test.d.ts +0 -1
  88. package/dist/lib/shell-hook.test.js +0 -68
  89. package/dist/test-utils.d.ts +0 -43
  90. package/dist/test-utils.js +0 -101
@@ -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
- const DEFAULT_GITHUB_CLIENT_ID = 'Ov23lizUYDKJOhumsyee';
3
- const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || process.env.GITHUB_ID || DEFAULT_GITHUB_CLIENT_ID;
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
- // Step 1: Request device code
6
- const deviceResponse = await fetch('https://github.com/login/device/code', {
7
- method: 'POST',
8
- headers: {
9
- Accept: 'application/json',
10
- 'Content-Type': 'application/json',
11
- },
12
- body: JSON.stringify({
13
- client_id: GITHUB_CLIENT_ID,
14
- scope: 'read:user',
15
- }),
16
- });
17
- if (!deviceResponse.ok) {
18
- console.error('Failed to initiate device code flow.');
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 deviceData = (await deviceResponse.json());
22
- // Step 2: Display user code
23
- console.log('\nTo authenticate, visit:');
24
- console.log(`\n ${deviceData.verification_uri}\n`);
25
- console.log(`Enter code: ${deviceData.user_code}\n`);
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
- const tokenData = (await tokenResponse.json());
45
- if (tokenData.access_token) {
46
- // Step 4: Get user info
47
- const userResponse = await fetch('https://api.github.com/user', {
48
- headers: {
49
- Authorization: `Bearer ${tokenData.access_token}`,
50
- Accept: 'application/vnd.github+json',
51
- },
52
- });
53
- if (!userResponse.ok) {
54
- console.error('Failed to get user info from GitHub.');
55
- process.exit(1);
56
- }
57
- const user = (await userResponse.json());
58
- // Step 5: Save credentials
59
- saveAuth({
60
- githubToken: tokenData.access_token,
61
- username: user.login,
62
- displayName: user.name || user.login,
63
- syncLibrary: options?.syncLibrary === true,
64
- });
65
- console.log(`\nAuthenticated as ${user.login}`);
66
- if (options?.syncLibrary) {
67
- console.log(' Library sync enabled. Your installed-refs list will appear at https://botdocs.ai/library after install/sync/uninstall.');
68
- }
69
- else {
70
- console.log(' Library sync is OFF. Re-run `botdocs login --sync-library` to enable the personalized Library page.');
71
- }
72
- return;
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
- if (tokenData.error === 'expired_token') {
75
- console.error('\nDevice code expired. Please try again.');
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
- if (tokenData.error &&
79
- tokenData.error !== 'authorization_pending' &&
80
- tokenData.error !== 'slow_down') {
81
- console.error(`\nAuthentication error: ${tokenData.error_description || tokenData.error}`);
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
- console.error('\nAuthentication timed out. Please try again.');
86
- process.exit(1);
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
  }
@@ -65,6 +65,11 @@ export async function publish(source, options) {
65
65
  console.error('Description is required. Use --description "..."');
66
66
  process.exit(1);
67
67
  }
68
+ // Pull type + sourceEcosystem from botdocs.json so the server stores
69
+ // the row as a SKILL/BUNDLE (instead of defaulting to SPEC).
70
+ const manifest = stat.isDirectory() ? readManifest(resolved) : null;
71
+ const botdocType = manifest?.type ?? 'SPEC';
72
+ const sourceEcosystem = manifest?.sourceEcosystem ?? null;
68
73
  console.log(`Publishing "${title}" (${files.length} file(s))...`);
69
74
  const result = await apiFetch('/api/botdocs', {
70
75
  method: 'POST',
@@ -76,6 +81,8 @@ export async function publish(source, options) {
76
81
  tags,
77
82
  license,
78
83
  files,
84
+ botdocType,
85
+ sourceEcosystem,
79
86
  },
80
87
  });
81
88
  if (options.json) {
@@ -85,6 +92,21 @@ export async function publish(source, options) {
85
92
  console.log(`\nPublished: ${result.url}`);
86
93
  }
87
94
  }
95
+ function readManifest(source) {
96
+ const manifestPath = path.join(source, 'botdocs.json');
97
+ if (!fs.existsSync(manifestPath))
98
+ return null;
99
+ try {
100
+ const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
101
+ return parseManifest(raw);
102
+ }
103
+ catch {
104
+ // Validate is the canonical place to surface manifest errors; here we
105
+ // silently fall back so a malformed file doesn't break a publish that
106
+ // was already going to default to SPEC anyway.
107
+ return null;
108
+ }
109
+ }
88
110
  async function maybeAutoCompile(source, options) {
89
111
  if (options.noCompile)
90
112
  return;
@@ -124,25 +146,38 @@ function collectFromFile(filePath) {
124
146
  return [{ filename, content, sortOrder: 0 }];
125
147
  }
126
148
  function collectFromDirectory(dirPath) {
127
- const entries = fs.readdirSync(dirPath);
128
149
  const files = [];
129
- for (let i = 0; i < entries.length; i++) {
130
- const entry = entries[i];
150
+ walkDirectory(dirPath, dirPath, files);
151
+ files.sort((a, b) => a.sortOrder - b.sortOrder);
152
+ return files;
153
+ }
154
+ function walkDirectory(rootDir, currentDir, out) {
155
+ const entries = fs.readdirSync(currentDir);
156
+ for (const entry of entries) {
131
157
  if (entry.startsWith('.'))
132
158
  continue;
133
- const fullPath = path.join(dirPath, entry);
159
+ const fullPath = path.join(currentDir, entry);
134
160
  const stat = fs.statSync(fullPath);
135
- if (stat.isFile() && (entry.endsWith('.md') || entry.endsWith('.markdown'))) {
136
- files.push({
137
- filename: entry,
138
- content: fs.readFileSync(fullPath, 'utf-8'),
139
- sortOrder: entry === 'index.md' ? 0 : i + 1,
140
- });
161
+ if (stat.isDirectory()) {
162
+ walkDirectory(rootDir, fullPath, out);
163
+ continue;
141
164
  }
165
+ if (!stat.isFile())
166
+ continue;
167
+ if (!entry.endsWith('.md') && !entry.endsWith('.markdown'))
168
+ continue;
169
+ // POSIX-style relative path so the install-time prefix check
170
+ // (e.g. `claude/SKILL.md`) works on every platform.
171
+ const relative = path
172
+ .relative(rootDir, fullPath)
173
+ .split(path.sep)
174
+ .join('/');
175
+ out.push({
176
+ filename: relative,
177
+ content: fs.readFileSync(fullPath, 'utf-8'),
178
+ sortOrder: relative === 'index.md' ? 0 : out.length + 1,
179
+ });
142
180
  }
143
- // Ensure index.md is first
144
- files.sort((a, b) => a.sortOrder - b.sortOrder);
145
- return files;
146
181
  }
147
182
  function collectFromZip(zipPath) {
148
183
  const zip = new AdmZip(zipPath);
@@ -152,11 +187,13 @@ function collectFromZip(zipPath) {
152
187
  const entry = entries[i];
153
188
  if (entry.isDirectory)
154
189
  continue;
155
- // Get the filename (strip any directory prefix)
156
- const filename = path.basename(entry.entryName);
190
+ // POSIX-style path inside the archive preserves directory prefixes
191
+ // (e.g. `claude/SKILL.md`) so install can route the file correctly.
192
+ const filename = entry.entryName.replace(/\\/g, '/');
193
+ const basename = path.basename(filename);
157
194
  if (!filename.endsWith('.md') && !filename.endsWith('.markdown'))
158
195
  continue;
159
- if (filename.startsWith('.'))
196
+ if (basename.startsWith('.'))
160
197
  continue;
161
198
  files.push({
162
199
  filename,
@@ -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 {};