@botdocs/cli 0.3.2 → 0.5.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 +145 -36
- 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/ingest.d.ts +2 -0
- package/dist/commands/ingest.js +162 -28
- package/dist/commands/install.d.ts +4 -0
- package/dist/commands/install.js +40 -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 +46 -39
- 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/README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# botdocs
|
|
2
2
|
|
|
3
|
-
The official CLI for [BotDocs](https://botdocs.ai) —
|
|
4
|
-
|
|
3
|
+
The official CLI for [BotDocs](https://botdocs.ai) — author, publish,
|
|
4
|
+
install, and sync agent skills.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
BotDocs is the registry teams use to share agent skills across the
|
|
7
|
+
agents their developers run (Claude Code, Cursor, Codex, ChatGPT). This
|
|
8
|
+
CLI is the fastest way to install a team's skills, stay in sync, and
|
|
9
|
+
publish your own.
|
|
9
10
|
|
|
10
11
|
## Install
|
|
11
12
|
|
|
@@ -30,54 +31,66 @@ Requires Node.js 20 or newer.
|
|
|
30
31
|
## Quick start
|
|
31
32
|
|
|
32
33
|
```bash
|
|
33
|
-
#
|
|
34
|
-
botdocs init my-spec
|
|
35
|
-
|
|
36
|
-
# validate before publishing
|
|
37
|
-
botdocs validate my-spec/
|
|
38
|
-
|
|
39
|
-
# log in (GitHub device code, one time)
|
|
34
|
+
# log in (opens your browser, sign in with any provider, one time)
|
|
40
35
|
botdocs login
|
|
41
36
|
|
|
42
|
-
#
|
|
43
|
-
botdocs
|
|
37
|
+
# install a team's shared skills
|
|
38
|
+
botdocs install @teamco/eng-skills
|
|
44
39
|
|
|
45
|
-
#
|
|
46
|
-
botdocs
|
|
40
|
+
# stay in sync with your team
|
|
41
|
+
botdocs sync
|
|
47
42
|
|
|
48
|
-
#
|
|
49
|
-
botdocs
|
|
50
|
-
|
|
43
|
+
# scaffold and publish your own skill
|
|
44
|
+
botdocs init my-skill
|
|
45
|
+
botdocs validate my-skill/
|
|
46
|
+
botdocs publish my-skill/
|
|
51
47
|
```
|
|
52
48
|
|
|
53
49
|
## Commands
|
|
54
50
|
|
|
55
51
|
| Command | Purpose |
|
|
56
52
|
|---|---|
|
|
57
|
-
| `init [name]` | Scaffold a new
|
|
53
|
+
| `init [name]` | Scaffold a new skill directory (`--canonical` for a multi-ecosystem skill). |
|
|
58
54
|
| `compile <path>` | Generate per-ecosystem skill drafts from a canonical source (BYOK). |
|
|
59
55
|
| `edit <ref>` | LLM-assisted revision of a published skill ecosystem file (BYOK). |
|
|
60
56
|
| `validate <source>` | Pre-publish structural check on a directory or file. |
|
|
61
|
-
| `clone <user/slug>` | Download every file in a BotDoc to a local directory. |
|
|
62
57
|
| `search <query>` | Search the public registry. |
|
|
63
58
|
| `publish <source>` | Publish from a file, directory, or zip archive. |
|
|
64
|
-
| `diff <user/slug>` | Preview remote changes before pulling. |
|
|
65
|
-
| `pull <user/slug>` | Update a previously-cloned BotDoc. |
|
|
66
59
|
| `install <ref>` | Install a skill or bundle (auto-detects destinations). |
|
|
67
60
|
| `sync [ref]` | Check installed skills/bundles for updates and apply. |
|
|
68
61
|
| `uninstall <ref>` | Remove an installed skill or bundle. |
|
|
69
62
|
| `list` | Show installed skills and bundles. |
|
|
70
63
|
| `ingest <path>` | Walk a directory, detect existing skills, upload as drafts. |
|
|
71
|
-
| `
|
|
64
|
+
| `team list` / `show` / `create` / `add` / `remove` / `push` / `unpush` | Manage teams: shared skill libraries for your org. |
|
|
65
|
+
| `undo` | Restore the most recent backup run (reversible). |
|
|
66
|
+
| `backups list` / `restore` / `diff` / `clear` | Browse, restore, diff, and prune backup runs. |
|
|
72
67
|
| `check-updates` | Check installed refs for available updates (1h cached). |
|
|
73
68
|
| `install-instructions [target]` | Write/refresh `AGENTS.md` (or install a shell hook with `--shell-hook`). |
|
|
74
|
-
| `login` | Authenticate
|
|
69
|
+
| `login` | Authenticate by opening your browser; pass `--token bd_xxx` for headless/CI (`--sync-library` enables `/library`). |
|
|
75
70
|
| `whoami` | Show the currently authenticated user. |
|
|
76
71
|
|
|
77
72
|
Every command accepts `--json` for machine-readable output.
|
|
78
73
|
|
|
79
74
|
Run `botdocs <command> --help` for full flags on any command.
|
|
80
75
|
|
|
76
|
+
## Look & feel
|
|
77
|
+
|
|
78
|
+
`botdocs login` and `botdocs sync` are interactive and live by default — a
|
|
79
|
+
cyan-to-violet gradient brand mark, an animated polling spinner, and
|
|
80
|
+
per-file status rows that update in place. The CLI auto-detects when it's
|
|
81
|
+
running in a real terminal vs. a piped/CI environment and renders the
|
|
82
|
+
right thing without any flags. If you prefer plain output even on a TTY
|
|
83
|
+
(screen-reader friendly, or just calmer), pass `--no-ink` to either
|
|
84
|
+
command:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
botdocs login --no-ink
|
|
88
|
+
botdocs sync --no-ink
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`botdocs sync --json` keeps emitting the same machine-readable JSON it
|
|
92
|
+
always has — it never touches the Ink renderer.
|
|
93
|
+
|
|
81
94
|
## Configuration
|
|
82
95
|
|
|
83
96
|
| Variable | Default | Purpose |
|
|
@@ -85,7 +98,11 @@ Run `botdocs <command> --help` for full flags on any command.
|
|
|
85
98
|
| `BOTDOCS_API_URL` | `https://botdocs.ai` | Override the registry API endpoint (useful for local development). |
|
|
86
99
|
|
|
87
100
|
Auth is stored at `~/.botdocs/auth.json` after `botdocs login`. Delete it
|
|
88
|
-
to log out.
|
|
101
|
+
to log out. The default `botdocs login` opens your browser at
|
|
102
|
+
`/cli-auth`, where you sign in with whichever provider you prefer
|
|
103
|
+
(GitHub, Google, or email-OTP) and confirm the terminal session. For
|
|
104
|
+
non-interactive environments, mint a token at `/settings/tokens` and
|
|
105
|
+
run `botdocs login --token bd_xxx` instead.
|
|
89
106
|
|
|
90
107
|
## Teaching agents to use this CLI
|
|
91
108
|
|
|
@@ -150,7 +167,13 @@ botdocs init my-skill --canonical # scaffolds claude-code source
|
|
|
150
167
|
# edit claude-code/commands/my-skill.md
|
|
151
168
|
|
|
152
169
|
botdocs compile my-skill/ # generates claude/SKILL.md,
|
|
153
|
-
# cursor/rules/my-skill.mdc,
|
|
170
|
+
# cursor/rules/my-skill.mdc,
|
|
171
|
+
# codex/my-skill.md,
|
|
172
|
+
# copilot/instructions/my-skill.instructions.md,
|
|
173
|
+
# windsurf/rules/my-skill.md,
|
|
174
|
+
# gemini/instructions/my-skill.md,
|
|
175
|
+
# antigravity/skills/my-skill.md,
|
|
176
|
+
# opencode/instructions/my-skill.md, etc.
|
|
154
177
|
|
|
155
178
|
botdocs publish my-skill/ # auto-compiles if stale; --no-compile to skip
|
|
156
179
|
```
|
|
@@ -168,6 +191,27 @@ botdocs edit @you/my-skill --ecosystem cursor
|
|
|
168
191
|
Haiku) when set, otherwise fall back to `BOTDOCS_OPENAI_KEY`
|
|
169
192
|
(GPT-4o mini). Use `--key-env <NAME>` to point at a different env var.
|
|
170
193
|
|
|
194
|
+
## Teams
|
|
195
|
+
|
|
196
|
+
A team is a curated library of skills shared across its members. Skills
|
|
197
|
+
stay user-owned; teams *pin* skills to indicate "these are the ones we
|
|
198
|
+
use." Members install/sync the pinned set automatically.
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
botdocs team create teamco --name "Team Co" # admin-only side: create
|
|
202
|
+
botdocs team add teamco @bob --role write # add a member
|
|
203
|
+
botdocs team push teamco @alice/eng-review-skill # pin a skill (WRITE+)
|
|
204
|
+
botdocs team push teamco @alice/eng --version 3 # pin to a specific version
|
|
205
|
+
|
|
206
|
+
botdocs team list # what teams am I in?
|
|
207
|
+
botdocs team show teamco # members + pinned skills
|
|
208
|
+
botdocs sync # pulls personal + team pins
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`botdocs sync` walks the team's pinned skills and installs/updates them
|
|
212
|
+
locally, marking the lockfile entry with the team they came from. Pinning
|
|
213
|
+
is curation, not access control — skills are public in v1.
|
|
214
|
+
|
|
171
215
|
## Skills + bundles
|
|
172
216
|
|
|
173
217
|
Skills are bundles of files that ship to specific destinations on disk
|
|
@@ -190,22 +234,87 @@ botdocs sync
|
|
|
190
234
|
`botdocs list` shows what you have installed; `botdocs uninstall <ref>`
|
|
191
235
|
removes it.
|
|
192
236
|
|
|
237
|
+
### Safety: backups before overwrite
|
|
238
|
+
|
|
239
|
+
`install` and `sync` will never silently clobber a hand-written file at
|
|
240
|
+
a colliding destination. Before any overwrite of a file the lockfile
|
|
241
|
+
doesn't claim as "ours and unchanged," the original is copied to a
|
|
242
|
+
timestamped backup directory:
|
|
243
|
+
|
|
244
|
+
- Project-scoped files → `<cwd>/.botdocs-backup/<ISO-ts>/<relative-path>`
|
|
245
|
+
- Global-scoped files (e.g. `~/.claude/skills/...`) → `~/.botdocs/backup/<ISO-ts>/<flattened-path>`
|
|
246
|
+
|
|
247
|
+
A single CLI invocation reuses the same `<ts>` folder, so all backups
|
|
248
|
+
from one run live together. A one-line warning is printed to stdout
|
|
249
|
+
for each backup taken. If the existing file matches a fingerprint we
|
|
250
|
+
recorded — i.e. we wrote it ourselves and the user hasn't edited it
|
|
251
|
+
since — no backup is taken (there's nothing to save). Backup failures
|
|
252
|
+
(e.g. permission errors) print a warning but don't block the install.
|
|
253
|
+
|
|
254
|
+
Pass `--no-backup` on `install` or `sync` to opt out — useful in CI
|
|
255
|
+
where backups are noise.
|
|
256
|
+
|
|
257
|
+
#### Undo and the `backups` surface
|
|
258
|
+
|
|
259
|
+
Backups are reversible. If you realize an `install` or `sync` clobbered
|
|
260
|
+
something you wanted to keep:
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
botdocs undo # restore the most recent backup run
|
|
264
|
+
botdocs undo --dry-run # preview without writing
|
|
265
|
+
botdocs undo --yes # skip the confirm prompt (scripts)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Before writing the restored content, the CURRENT state at each path is
|
|
269
|
+
itself backed up under a new timestamp — so `botdocs undo` is reversible.
|
|
270
|
+
Running it twice in a row swaps the state back. Pass `--no-backup` to
|
|
271
|
+
skip the pre-backup (loses reversibility).
|
|
272
|
+
|
|
273
|
+
For more granular control, the `backups` group browses, partial-restores,
|
|
274
|
+
diffs, and prunes:
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
botdocs backups list # every run, newest first
|
|
278
|
+
botdocs backups list <ts> # files in a specific run
|
|
279
|
+
botdocs backups restore <ts> # restore the full run
|
|
280
|
+
botdocs backups restore <ts> --files a.mdc,b.mdc # restore a subset
|
|
281
|
+
botdocs backups diff <ts> <relpath> # diff backup vs current
|
|
282
|
+
botdocs backups clear # delete all runs (confirm)
|
|
283
|
+
botdocs backups clear --older-than 30d # delete runs older than N days
|
|
284
|
+
botdocs backups clear --dry-run # preview without deleting
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Each backup run records a `manifest.json` sidecar mapping original→backup,
|
|
288
|
+
so restoration is unambiguous even when global-scope filenames flatten
|
|
289
|
+
ambiguously (e.g. paths containing `_`).
|
|
290
|
+
|
|
193
291
|
Authors who want to share their existing collection of skills run
|
|
194
292
|
`botdocs ingest <path>` — the CLI walks the directory, detects each
|
|
195
|
-
skill
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
293
|
+
skill across all 10 supported ecosystems (claude, claude-code, cursor,
|
|
294
|
+
chatgpt, codex, copilot, windsurf, gemini, antigravity, opencode), and
|
|
295
|
+
uploads them as drafts in your BotDocs account for review before
|
|
296
|
+
publishing.
|
|
199
297
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
298
|
+
By default `ingest` expects the canonical BotDocs layout (e.g.
|
|
299
|
+
`claude-code/commands/<slug>.md`, `cursor/rules/<slug>.mdc`). If your
|
|
300
|
+
files live in real on-disk locations instead, point `--from-tool=<ecosystem>`
|
|
301
|
+
at them and every matching file becomes a draft for that ecosystem:
|
|
203
302
|
|
|
204
303
|
```bash
|
|
205
|
-
|
|
304
|
+
# Ingest all your Claude Code commands directly:
|
|
305
|
+
botdocs ingest --from-tool=claude-code ~/.claude/commands/
|
|
306
|
+
|
|
307
|
+
# Ingest a project's Cursor rules:
|
|
308
|
+
botdocs ingest --from-tool=cursor .cursor/rules/
|
|
309
|
+
|
|
310
|
+
# Ingest GitHub Copilot instructions (the .instructions.md extension is
|
|
311
|
+
# stripped from the slug automatically):
|
|
312
|
+
botdocs ingest --from-tool=copilot .github/instructions/
|
|
206
313
|
```
|
|
207
314
|
|
|
208
|
-
|
|
315
|
+
The upload always uses the canonical BotDocs filename, so when someone
|
|
316
|
+
else `botdocs install`s the resulting skill it lands in the right
|
|
317
|
+
on-disk location.
|
|
209
318
|
|
|
210
319
|
## Development
|
|
211
320
|
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import * as p from '@clack/prompts';
|
|
4
|
+
import { clearBackups, listBackupFiles, listBackupRuns, restoreBackup, } from '../lib/backup.js';
|
|
5
|
+
import { hasChanges, renderDiff } from '../lib/diff.js';
|
|
6
|
+
/** Show every backup run, newest first. */
|
|
7
|
+
async function backupsList(timestamp, options) {
|
|
8
|
+
const projectRoot = process.cwd();
|
|
9
|
+
if (timestamp) {
|
|
10
|
+
// List files in a specific run.
|
|
11
|
+
const entries = listBackupFiles(timestamp, projectRoot);
|
|
12
|
+
if (options.json) {
|
|
13
|
+
console.log(JSON.stringify({ runTimestamp: timestamp, files: entries }));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (entries.length === 0) {
|
|
17
|
+
console.log(`\n No backup run found with timestamp: ${timestamp}\n`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
console.log(`\n Backup run: ${timestamp}`);
|
|
21
|
+
console.log(` Files: ${entries.length}\n`);
|
|
22
|
+
for (const e of entries) {
|
|
23
|
+
const rel = path.relative(projectRoot, e.originalPath) || e.originalPath;
|
|
24
|
+
console.log(` [${e.scope}] ${rel}`);
|
|
25
|
+
}
|
|
26
|
+
console.log('');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const runs = listBackupRuns(projectRoot);
|
|
30
|
+
if (options.json) {
|
|
31
|
+
console.log(JSON.stringify({ runs }));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (runs.length === 0) {
|
|
35
|
+
console.log('\n No backup runs.\n');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
console.log('');
|
|
39
|
+
for (const r of runs) {
|
|
40
|
+
console.log(` ${r.runTimestamp} (${r.scope}, ${r.fileCount} file${r.fileCount === 1 ? '' : 's'})`);
|
|
41
|
+
}
|
|
42
|
+
console.log('');
|
|
43
|
+
}
|
|
44
|
+
/** Restore a specific backup run, optionally a subset via --files. */
|
|
45
|
+
async function backupsRestore(timestamp, options) {
|
|
46
|
+
const projectRoot = process.cwd();
|
|
47
|
+
const entries = listBackupFiles(timestamp, projectRoot);
|
|
48
|
+
if (entries.length === 0) {
|
|
49
|
+
if (options.json) {
|
|
50
|
+
console.log(JSON.stringify({ restored: [], failed: [], preBackedUp: [], message: 'No such run.' }));
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.log(`\n No backup run found with timestamp: ${timestamp}\n`);
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const filters = options.files
|
|
58
|
+
? options.files.split(',').map((f) => f.trim()).filter((f) => f.length > 0)
|
|
59
|
+
: undefined;
|
|
60
|
+
const targetCount = filters
|
|
61
|
+
? entries.filter((e) => filters.some((f) => e.originalPath.endsWith(f))).length
|
|
62
|
+
: entries.length;
|
|
63
|
+
if (!options.json) {
|
|
64
|
+
const verb = options.dryRun ? 'Would restore' : 'Restore';
|
|
65
|
+
console.log(`\n ${verb} ${targetCount} file(s) from ${timestamp}`);
|
|
66
|
+
if (!options.noBackup && !options.dryRun) {
|
|
67
|
+
console.log(' (current state will be backed up first — reversible)');
|
|
68
|
+
}
|
|
69
|
+
console.log('');
|
|
70
|
+
}
|
|
71
|
+
if (!options.dryRun && !options.yes && !options.json) {
|
|
72
|
+
const confirmed = await p.confirm({
|
|
73
|
+
message: `Restore ${targetCount} file(s) from ${timestamp}?`,
|
|
74
|
+
initialValue: false,
|
|
75
|
+
});
|
|
76
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
77
|
+
console.log(' Cancelled.\n');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const result = restoreBackup(timestamp, projectRoot, {
|
|
82
|
+
files: filters,
|
|
83
|
+
dryRun: options.dryRun,
|
|
84
|
+
noBackup: options.noBackup,
|
|
85
|
+
});
|
|
86
|
+
if (options.json) {
|
|
87
|
+
console.log(JSON.stringify({
|
|
88
|
+
runTimestamp: timestamp,
|
|
89
|
+
dryRun: options.dryRun ?? false,
|
|
90
|
+
restored: result.restored.map((e) => e.originalPath),
|
|
91
|
+
failed: result.failed.map((f) => ({ originalPath: f.entry.originalPath, error: f.error })),
|
|
92
|
+
preBackedUp: result.preBackedUp,
|
|
93
|
+
}));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
reportRestoreResult(result, projectRoot, options.dryRun ?? false);
|
|
97
|
+
}
|
|
98
|
+
function reportRestoreResult(result, projectRoot, dryRun) {
|
|
99
|
+
const verb = dryRun ? 'Would restore' : 'Restored';
|
|
100
|
+
console.log(` ✓ ${verb} ${result.restored.length} file(s)`);
|
|
101
|
+
for (const e of result.restored) {
|
|
102
|
+
const rel = path.relative(projectRoot, e.originalPath) || e.originalPath;
|
|
103
|
+
console.log(` ${rel}`);
|
|
104
|
+
}
|
|
105
|
+
if (result.failed.length > 0) {
|
|
106
|
+
console.log(`\n ⚠ ${result.failed.length} file(s) failed:`);
|
|
107
|
+
for (const f of result.failed) {
|
|
108
|
+
const rel = path.relative(projectRoot, f.entry.originalPath) || f.entry.originalPath;
|
|
109
|
+
console.log(` ${rel} (${f.error})`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
console.log('');
|
|
113
|
+
}
|
|
114
|
+
/** Heuristic: read the first 8 KB of a buffer and treat the presence of a NUL
|
|
115
|
+
* byte as "binary." Good enough for telling backup text files apart from
|
|
116
|
+
* compiled binaries. */
|
|
117
|
+
function isBinary(buf) {
|
|
118
|
+
const limit = Math.min(buf.length, 8192);
|
|
119
|
+
for (let i = 0; i < limit; i++) {
|
|
120
|
+
if (buf[i] === 0)
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
/** Show a unified diff between the backup and the current on-disk file. */
|
|
126
|
+
async function backupsDiff(timestamp, relpath, options) {
|
|
127
|
+
const projectRoot = process.cwd();
|
|
128
|
+
const entries = listBackupFiles(timestamp, projectRoot);
|
|
129
|
+
const entry = entries.find((e) => e.originalPath.endsWith(relpath));
|
|
130
|
+
if (!entry) {
|
|
131
|
+
if (options.json) {
|
|
132
|
+
console.log(JSON.stringify({ error: 'no matching file in backup run' }));
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
console.log(`\n No file matching "${relpath}" in run ${timestamp}\n`);
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (!fs.existsSync(entry.backupPath)) {
|
|
140
|
+
if (options.json) {
|
|
141
|
+
console.log(JSON.stringify({ error: 'backup file missing on disk', backupPath: entry.backupPath }));
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
console.log(`\n Backup file missing on disk: ${entry.backupPath}\n`);
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const backupBuf = fs.readFileSync(entry.backupPath);
|
|
149
|
+
const currentBuf = fs.existsSync(entry.originalPath)
|
|
150
|
+
? fs.readFileSync(entry.originalPath)
|
|
151
|
+
: Buffer.alloc(0);
|
|
152
|
+
if (isBinary(backupBuf) || isBinary(currentBuf)) {
|
|
153
|
+
if (options.json) {
|
|
154
|
+
console.log(JSON.stringify({
|
|
155
|
+
originalPath: entry.originalPath,
|
|
156
|
+
backupPath: entry.backupPath,
|
|
157
|
+
binary: true,
|
|
158
|
+
differs: !backupBuf.equals(currentBuf),
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
console.log(`\n ${entry.originalPath}: differs (binary)\n`);
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const backupStr = backupBuf.toString('utf-8');
|
|
167
|
+
const currentStr = currentBuf.toString('utf-8');
|
|
168
|
+
if (options.json) {
|
|
169
|
+
console.log(JSON.stringify({
|
|
170
|
+
originalPath: entry.originalPath,
|
|
171
|
+
backupPath: entry.backupPath,
|
|
172
|
+
binary: false,
|
|
173
|
+
differs: hasChanges(currentStr, backupStr),
|
|
174
|
+
}));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// Diff direction: show current → backup (i.e. what restoring would do).
|
|
178
|
+
console.log(`\n ${entry.originalPath}\n`);
|
|
179
|
+
console.log(renderDiff(currentStr, backupStr));
|
|
180
|
+
}
|
|
181
|
+
/** Parse `--older-than 30d` → 30. Returns undefined when the flag isn't set. */
|
|
182
|
+
function parseOlderThan(raw) {
|
|
183
|
+
if (!raw)
|
|
184
|
+
return undefined;
|
|
185
|
+
const m = raw.match(/^(\d+)\s*d?$/);
|
|
186
|
+
if (!m) {
|
|
187
|
+
throw new Error(`Invalid --older-than value: ${raw} (expected e.g. "30d" or "30")`);
|
|
188
|
+
}
|
|
189
|
+
return parseInt(m[1], 10);
|
|
190
|
+
}
|
|
191
|
+
async function backupsClear(options) {
|
|
192
|
+
const projectRoot = process.cwd();
|
|
193
|
+
let olderThanDays;
|
|
194
|
+
try {
|
|
195
|
+
olderThanDays = parseOlderThan(options.olderThan);
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
if (options.json) {
|
|
199
|
+
console.log(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
console.error(`\n ✗ ${err instanceof Error ? err.message : String(err)}\n`);
|
|
203
|
+
}
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
// First do a dry-run to see what's affected.
|
|
207
|
+
const preview = clearBackups(projectRoot, { olderThanDays, dryRun: true });
|
|
208
|
+
if (preview.cleared.length === 0) {
|
|
209
|
+
if (options.json) {
|
|
210
|
+
console.log(JSON.stringify({ cleared: [], kept: preview.kept }));
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
console.log('\n Nothing to clear.\n');
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (!options.json) {
|
|
218
|
+
const verb = options.dryRun ? 'Would clear' : 'Clear';
|
|
219
|
+
console.log(`\n ${verb} ${preview.cleared.length} backup run(s):`);
|
|
220
|
+
for (const r of preview.cleared) {
|
|
221
|
+
console.log(` ${r.runTimestamp} (${r.scope}, ${r.fileCount} file${r.fileCount === 1 ? '' : 's'})`);
|
|
222
|
+
}
|
|
223
|
+
console.log('');
|
|
224
|
+
}
|
|
225
|
+
if (options.dryRun) {
|
|
226
|
+
if (options.json) {
|
|
227
|
+
console.log(JSON.stringify({ dryRun: true, cleared: preview.cleared, kept: preview.kept }));
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// Double-confirm for destructive clears unless --yes. The "double" here is
|
|
232
|
+
// showing the list first AND requiring a y/N — printing the list makes the
|
|
233
|
+
// single prompt informed enough that a second prompt would be noise.
|
|
234
|
+
if (!options.yes && !options.json) {
|
|
235
|
+
const confirmed = await p.confirm({
|
|
236
|
+
message: `Permanently delete ${preview.cleared.length} backup run(s)?`,
|
|
237
|
+
initialValue: false,
|
|
238
|
+
});
|
|
239
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
240
|
+
console.log(' Cancelled.\n');
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const result = clearBackups(projectRoot, { olderThanDays });
|
|
245
|
+
if (options.json) {
|
|
246
|
+
console.log(JSON.stringify({ cleared: result.cleared, kept: result.kept }));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
console.log(` ✓ Cleared ${result.cleared.length} backup run(s).\n`);
|
|
250
|
+
}
|
|
251
|
+
export function registerBackupCommands(program) {
|
|
252
|
+
const backups = program
|
|
253
|
+
.command('backups')
|
|
254
|
+
.description('Browse, restore, diff, and clear backup runs from install/sync overwrites');
|
|
255
|
+
backups
|
|
256
|
+
.command('list [timestamp]')
|
|
257
|
+
.description('List backup runs newest first; with a timestamp, list files in that run')
|
|
258
|
+
.action(async (timestamp) => {
|
|
259
|
+
await backupsList(timestamp, { json: program.opts().json });
|
|
260
|
+
});
|
|
261
|
+
backups
|
|
262
|
+
.command('restore <timestamp>')
|
|
263
|
+
.description('Restore a backup run (or a subset with --files)')
|
|
264
|
+
.option('--files <list>', 'Comma-separated relpath suffixes to restore (default: all files in the run)')
|
|
265
|
+
.option('--dry-run', 'Show what would be restored without writing')
|
|
266
|
+
.option('--yes', 'Skip the confirmation prompt')
|
|
267
|
+
.option('--no-backup', 'Skip backing up the current state before restoring (advanced)')
|
|
268
|
+
.action(async (timestamp, opts) => {
|
|
269
|
+
const { backup, ...rest } = opts;
|
|
270
|
+
await backupsRestore(timestamp, {
|
|
271
|
+
...rest,
|
|
272
|
+
noBackup: backup === false,
|
|
273
|
+
json: program.opts().json,
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
backups
|
|
277
|
+
.command('diff <timestamp> <relpath>')
|
|
278
|
+
.description('Show a diff between the backup and the current file')
|
|
279
|
+
.action(async (timestamp, relpath) => {
|
|
280
|
+
await backupsDiff(timestamp, relpath, { json: program.opts().json });
|
|
281
|
+
});
|
|
282
|
+
backups
|
|
283
|
+
.command('clear')
|
|
284
|
+
.description('Delete backup runs (all, or filtered by age)')
|
|
285
|
+
.option('--older-than <duration>', 'Only clear runs older than this (e.g. "30d")')
|
|
286
|
+
.option('--dry-run', 'Show what would be cleared without deleting')
|
|
287
|
+
.option('--yes', 'Skip the confirmation prompt')
|
|
288
|
+
.action(async (opts) => {
|
|
289
|
+
await backupsClear({ ...opts, json: program.opts().json });
|
|
290
|
+
});
|
|
291
|
+
}
|
package/dist/commands/edit.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import
|
|
4
|
+
import * as p from '@clack/prompts';
|
|
5
5
|
import { ApiError, apiFetch, fetchRawContent } from '../lib/api.js';
|
|
6
6
|
import { complete, detectProvider, LlmError } from '../lib/llm.js';
|
|
7
7
|
import { renderDiff } from '../lib/diff.js';
|
|
@@ -63,7 +63,15 @@ export async function edit(rawRef, options) {
|
|
|
63
63
|
}
|
|
64
64
|
console.log(` ✓ Pulled ${target.filename}`);
|
|
65
65
|
const currentContent = await fetchRawContent(target.rawUrl);
|
|
66
|
-
const
|
|
66
|
+
const userRequestRaw = await p.text({
|
|
67
|
+
message: 'What change would you like to make?',
|
|
68
|
+
placeholder: 'e.g. "Add a section about testing"',
|
|
69
|
+
});
|
|
70
|
+
if (p.isCancel(userRequestRaw)) {
|
|
71
|
+
p.cancel('Edit cancelled.');
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
const userRequest = userRequestRaw;
|
|
67
75
|
if (!userRequest.trim()) {
|
|
68
76
|
console.error('\n ✗ No request provided. Aborting.\n');
|
|
69
77
|
process.exit(1);
|
|
@@ -79,15 +87,15 @@ export async function edit(rawRef, options) {
|
|
|
79
87
|
}
|
|
80
88
|
revised = resp.text;
|
|
81
89
|
console.log(renderDiff(currentContent, revised));
|
|
82
|
-
const choice = await select({
|
|
90
|
+
const choice = await p.select({
|
|
83
91
|
message: 'Apply revision?',
|
|
84
|
-
|
|
85
|
-
{
|
|
86
|
-
{
|
|
87
|
-
{
|
|
92
|
+
options: [
|
|
93
|
+
{ value: 'accept', label: 'Accept (push as draft to BotDocs)' },
|
|
94
|
+
{ value: 'regenerate', label: 'Regenerate' },
|
|
95
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
88
96
|
],
|
|
89
97
|
});
|
|
90
|
-
if (choice === 'cancel') {
|
|
98
|
+
if (p.isCancel(choice) || choice === 'cancel') {
|
|
91
99
|
console.log('\n Cancelled. No changes pushed.\n');
|
|
92
100
|
return;
|
|
93
101
|
}
|
|
@@ -2,6 +2,8 @@ interface IngestOptions {
|
|
|
2
2
|
bundle?: string;
|
|
3
3
|
dryRun?: boolean;
|
|
4
4
|
json?: boolean;
|
|
5
|
+
fromTool?: string;
|
|
5
6
|
}
|
|
7
|
+
export declare const SUPPORTED_TOOLS: readonly string[];
|
|
6
8
|
export declare function ingest(rootPath: string, options: IngestOptions): Promise<void>;
|
|
7
9
|
export {};
|