@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
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useReducer, useRef } from 'react';
|
|
3
|
+
import { Box, Text, useApp } from 'ink';
|
|
4
|
+
import Spinner from 'ink-spinner';
|
|
5
|
+
import { theme } from './theme.js';
|
|
6
|
+
import { initialState, syncReducer, } from './sync-state.js';
|
|
7
|
+
import { ConflictPrompt } from './conflict-prompt.js';
|
|
8
|
+
/** Map a row state to a one-character status icon. Active states ('checking',
|
|
9
|
+
* 'updating') render a spinner instead and pass `null` through here. */
|
|
10
|
+
function statusIcon(state) {
|
|
11
|
+
switch (state) {
|
|
12
|
+
case 'queued':
|
|
13
|
+
return { glyph: '◌', color: theme.zinc };
|
|
14
|
+
case 'up-to-date':
|
|
15
|
+
case 'updated':
|
|
16
|
+
return { glyph: '✓', color: theme.green };
|
|
17
|
+
case 'skipped':
|
|
18
|
+
return { glyph: '⌀', color: theme.zinc };
|
|
19
|
+
case 'conflict':
|
|
20
|
+
return { glyph: '⚠', color: theme.amber };
|
|
21
|
+
case 'error':
|
|
22
|
+
return { glyph: '✗', color: theme.red };
|
|
23
|
+
// checking/updating handled by the spinner branch in the renderer.
|
|
24
|
+
default:
|
|
25
|
+
return { glyph: ' ', color: theme.zinc };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Right-hand details text for a row. Keeps the rendering consistent across
|
|
29
|
+
* the per-section loops below. */
|
|
30
|
+
function detailsText(row) {
|
|
31
|
+
if (row.details)
|
|
32
|
+
return row.details;
|
|
33
|
+
switch (row.state) {
|
|
34
|
+
case 'queued':
|
|
35
|
+
return 'queued';
|
|
36
|
+
case 'checking':
|
|
37
|
+
return 'checking...';
|
|
38
|
+
case 'up-to-date':
|
|
39
|
+
return 'up to date';
|
|
40
|
+
case 'updating':
|
|
41
|
+
return 'installing...';
|
|
42
|
+
case 'updated':
|
|
43
|
+
return 'updated';
|
|
44
|
+
case 'skipped':
|
|
45
|
+
return 'skipped';
|
|
46
|
+
case 'conflict':
|
|
47
|
+
return 'local edits detected';
|
|
48
|
+
case 'error':
|
|
49
|
+
return 'error';
|
|
50
|
+
default:
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Render one row. Conflict rows still render through this — the inline
|
|
55
|
+
* `<ConflictPrompt />` is rendered separately below the section so the user
|
|
56
|
+
* sees the row + the choice list together. */
|
|
57
|
+
function Row({ row }) {
|
|
58
|
+
const isActive = row.state === 'checking' || row.state === 'updating';
|
|
59
|
+
const { glyph, color } = statusIcon(row.state);
|
|
60
|
+
return (_jsxs(Box, { marginLeft: 2, children: [_jsx(Box, { width: 2, children: isActive ? (_jsx(Text, { color: theme.cyan, children: _jsx(Spinner, { type: "dots" }) })) : (_jsx(Text, { color: color, children: glyph })) }), _jsx(Text, { children: row.ref }), _jsxs(Text, { color: theme.zinc, children: [" \u2014 ", detailsText(row)] })] }));
|
|
61
|
+
}
|
|
62
|
+
/** Group rows by their `team` field. Personal rows (no team) come first;
|
|
63
|
+
* teams are sorted alphabetically by slug to keep the layout stable across
|
|
64
|
+
* runs even when the server returns teams in a different order. */
|
|
65
|
+
function groupByTeam(rows) {
|
|
66
|
+
const personal = [];
|
|
67
|
+
const byTeam = new Map();
|
|
68
|
+
for (const r of rows) {
|
|
69
|
+
if (!r.team) {
|
|
70
|
+
personal.push(r);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const list = byTeam.get(r.team) ?? [];
|
|
74
|
+
list.push(r);
|
|
75
|
+
byTeam.set(r.team, list);
|
|
76
|
+
}
|
|
77
|
+
const sections = [];
|
|
78
|
+
if (personal.length > 0)
|
|
79
|
+
sections.push({ team: null, rows: personal });
|
|
80
|
+
const teamSlugs = Array.from(byTeam.keys()).sort();
|
|
81
|
+
for (const slug of teamSlugs) {
|
|
82
|
+
sections.push({ team: slug, rows: byTeam.get(slug) });
|
|
83
|
+
}
|
|
84
|
+
return sections;
|
|
85
|
+
}
|
|
86
|
+
/** Live Ink view for `botdocs sync`. Owns the reducer state and the conflict
|
|
87
|
+
* resolver Map. On mount it kicks off `runSync` with a dispatch/awaiter pair —
|
|
88
|
+
* the awaiter parks a Promise resolver keyed by `ref|file` in a ref-held Map,
|
|
89
|
+
* and the inline `<ConflictPrompt />` resolves it when the user picks. */
|
|
90
|
+
export function SyncApp({ initialRows, runSync }) {
|
|
91
|
+
const [state, dispatch] = useReducer(syncReducer, initialRows, (rows) => syncReducer(initialState(), { type: 'INIT', rows }));
|
|
92
|
+
const conflictResolvers = useRef(new Map());
|
|
93
|
+
const { exit } = useApp();
|
|
94
|
+
const didRun = useRef(false);
|
|
95
|
+
// Kick off the sync once on mount. Strict-mode-style double-invocation would
|
|
96
|
+
// re-run; we guard with a ref because the work has side effects.
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (didRun.current)
|
|
99
|
+
return;
|
|
100
|
+
didRun.current = true;
|
|
101
|
+
const awaitConflictChoice = (ref, file) => new Promise((resolve) => {
|
|
102
|
+
const key = `${ref}|${file}`;
|
|
103
|
+
conflictResolvers.current.set(key, resolve);
|
|
104
|
+
// Wire up the row-level resolver so the inline <ConflictPrompt /> can
|
|
105
|
+
// call it directly without needing to know which file is active.
|
|
106
|
+
dispatch({
|
|
107
|
+
type: 'CONFLICT',
|
|
108
|
+
ref,
|
|
109
|
+
details: file,
|
|
110
|
+
resolveConflict: (choice) => {
|
|
111
|
+
const r = conflictResolvers.current.get(key);
|
|
112
|
+
if (r) {
|
|
113
|
+
conflictResolvers.current.delete(key);
|
|
114
|
+
r(choice);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
runSync({ dispatch, awaitConflictChoice }).catch((err) => {
|
|
120
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
121
|
+
// Surface fatal errors as a synthetic row + finish so the user still
|
|
122
|
+
// sees something rather than an empty frame.
|
|
123
|
+
dispatch({ type: 'FINISH', elapsedMs: 0 });
|
|
124
|
+
// Re-throw via process.nextTick so the unmount completes first.
|
|
125
|
+
process.nextTick(() => {
|
|
126
|
+
throw err instanceof Error ? err : new Error(message);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
// Intentionally only depend on first mount; the runner is consumed once.
|
|
130
|
+
}, []);
|
|
131
|
+
// Hold the final summary briefly before unmounting so the user can read it.
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (state.finished) {
|
|
134
|
+
const timer = setTimeout(() => exit(), 600);
|
|
135
|
+
return () => clearTimeout(timer);
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
}, [state.finished, exit]);
|
|
139
|
+
const { rows, summary, finished, elapsedMs } = state;
|
|
140
|
+
const sections = groupByTeam(rows);
|
|
141
|
+
const totals = `${rows.length} file${rows.length === 1 ? '' : 's'}`;
|
|
142
|
+
// Find the first unresolved conflict row to render the inline prompt under.
|
|
143
|
+
// Only one conflict prompt is active at a time because the runner awaits
|
|
144
|
+
// each one before moving on.
|
|
145
|
+
const activeConflict = rows.find((r) => r.state === 'conflict' && Boolean(r.resolveConflict));
|
|
146
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Text, { bold: true, color: theme.cyan, children: "BotDocs sync" }), _jsx(Text, { color: theme.zinc, children: totals }), sections.map((section) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: theme.violet, children: section.team ? `TEAM ${section.team}` : 'PERSONAL' }), section.rows.map((r) => (_jsx(Row, { row: r }, r.ref)))] }, section.team ?? '__personal'))), activeConflict ? (_jsx(ConflictPrompt, { row: activeConflict, onChoice: (choice) => activeConflict.resolveConflict?.(choice) })) : null, finished ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.zinc, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Text, { children: [_jsxs(Text, { color: theme.green, children: [summary.updated, " updated"] }), _jsx(Text, { color: theme.zinc, children: " \u00B7 " }), _jsxs(Text, { color: theme.green, children: [summary.upToDate, " up to date"] }), summary.skipped > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.zinc, children: " \u00B7 " }), _jsxs(Text, { color: theme.zinc, children: [summary.skipped, " skipped"] })] })) : null, summary.conflict > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.zinc, children: " \u00B7 " }), _jsxs(Text, { color: theme.amber, children: [summary.conflict, " conflict"] })] })) : null, summary.error > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.zinc, children: " \u00B7 " }), _jsxs(Text, { color: theme.red, children: [summary.error, " error"] })] })) : null, typeof elapsedMs === 'number' ? (_jsxs(Text, { color: theme.zinc, children: [' · ', "finished in ", (elapsedMs / 1000).toFixed(1), "s"] })) : null] })] })) : null] }));
|
|
147
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** Pure reducer + types for the Ink sync view. Kept in a separate file from
|
|
2
|
+
* the React component so we can drive it through unit tests without rendering. */
|
|
3
|
+
/** Per-row state machine. The order roughly tracks the path a row takes during
|
|
4
|
+
* a sync: queued → checking → (up-to-date | updating → updated | conflict →
|
|
5
|
+
* skipped|updated | error). 'skipped' is reachable both from clean-update
|
|
6
|
+
* "skip" and from conflict "skip"; both render identically. */
|
|
7
|
+
export type SyncFileState = 'queued' | 'checking' | 'up-to-date' | 'updating' | 'updated' | 'conflict' | 'skipped' | 'error';
|
|
8
|
+
export interface SyncFileRow {
|
|
9
|
+
/** Display label, typically `@user/slug` or `@user/slug:filename`. */
|
|
10
|
+
ref: string;
|
|
11
|
+
/** Owning section. `undefined` is treated as the personal section. */
|
|
12
|
+
team?: string;
|
|
13
|
+
/** Optional secondary detail (e.g. version delta or error message). */
|
|
14
|
+
details?: string;
|
|
15
|
+
state: SyncFileState;
|
|
16
|
+
/** When `state === 'conflict'`, optional handler the view layer can call
|
|
17
|
+
* with the user's selection. Kept off the wire format and out of summaries.
|
|
18
|
+
*
|
|
19
|
+
* The live Ink view sets this via `CONFLICT` so the inline `<SelectInput />`
|
|
20
|
+
* can resolve the promise the runner is awaiting. When undefined, the row
|
|
21
|
+
* is in a non-interactive "conflict" presentation (e.g. test fixtures). */
|
|
22
|
+
resolveConflict?: (choice: 'overwrite' | 'skip' | 'keep') => void;
|
|
23
|
+
}
|
|
24
|
+
export interface SyncSummary {
|
|
25
|
+
updated: number;
|
|
26
|
+
upToDate: number;
|
|
27
|
+
conflict: number;
|
|
28
|
+
error: number;
|
|
29
|
+
skipped: number;
|
|
30
|
+
}
|
|
31
|
+
export interface SyncState {
|
|
32
|
+
rows: SyncFileRow[];
|
|
33
|
+
summary: SyncSummary;
|
|
34
|
+
/** Wall-clock ms since the sync started. Set on FINISH so the final summary
|
|
35
|
+
* can render "finished in X.Ys" without the view layer needing its own timer. */
|
|
36
|
+
elapsedMs?: number;
|
|
37
|
+
/** True once the parent driver has dispatched FINISH. View uses this to
|
|
38
|
+
* render the divider + summary footer and stop showing active spinners. */
|
|
39
|
+
finished: boolean;
|
|
40
|
+
}
|
|
41
|
+
export type SyncAction = {
|
|
42
|
+
type: 'INIT';
|
|
43
|
+
rows: Array<{
|
|
44
|
+
ref: string;
|
|
45
|
+
team?: string;
|
|
46
|
+
}>;
|
|
47
|
+
} | {
|
|
48
|
+
type: 'ADD_ROW';
|
|
49
|
+
ref: string;
|
|
50
|
+
team?: string;
|
|
51
|
+
} | {
|
|
52
|
+
type: 'CHECKING';
|
|
53
|
+
ref: string;
|
|
54
|
+
} | {
|
|
55
|
+
type: 'UP_TO_DATE';
|
|
56
|
+
ref: string;
|
|
57
|
+
details?: string;
|
|
58
|
+
} | {
|
|
59
|
+
type: 'UPDATING';
|
|
60
|
+
ref: string;
|
|
61
|
+
details?: string;
|
|
62
|
+
} | {
|
|
63
|
+
type: 'UPDATED';
|
|
64
|
+
ref: string;
|
|
65
|
+
details?: string;
|
|
66
|
+
} | {
|
|
67
|
+
type: 'CONFLICT';
|
|
68
|
+
ref: string;
|
|
69
|
+
details?: string;
|
|
70
|
+
resolveConflict?: (choice: 'overwrite' | 'skip' | 'keep') => void;
|
|
71
|
+
} | {
|
|
72
|
+
type: 'SKIPPED';
|
|
73
|
+
ref: string;
|
|
74
|
+
details?: string;
|
|
75
|
+
} | {
|
|
76
|
+
type: 'ERROR';
|
|
77
|
+
ref: string;
|
|
78
|
+
details?: string;
|
|
79
|
+
} | {
|
|
80
|
+
type: 'FINISH';
|
|
81
|
+
elapsedMs: number;
|
|
82
|
+
};
|
|
83
|
+
export declare function initialState(): SyncState;
|
|
84
|
+
export declare function syncReducer(state: SyncState, action: SyncAction): SyncState;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Pure reducer + types for the Ink sync view. Kept in a separate file from
|
|
2
|
+
* the React component so we can drive it through unit tests without rendering. */
|
|
3
|
+
export function initialState() {
|
|
4
|
+
return {
|
|
5
|
+
rows: [],
|
|
6
|
+
summary: { updated: 0, upToDate: 0, conflict: 0, error: 0, skipped: 0 },
|
|
7
|
+
finished: false,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/** Recompute the summary counts from rows in their current state. We keep
|
|
11
|
+
* `summary` in sync this way (rather than incrementing on each transition) so
|
|
12
|
+
* a row moving from CONFLICT → UPDATED doesn't double-count. */
|
|
13
|
+
function recomputeSummary(rows) {
|
|
14
|
+
const s = { updated: 0, upToDate: 0, conflict: 0, error: 0, skipped: 0 };
|
|
15
|
+
for (const r of rows) {
|
|
16
|
+
if (r.state === 'updated')
|
|
17
|
+
s.updated++;
|
|
18
|
+
else if (r.state === 'up-to-date')
|
|
19
|
+
s.upToDate++;
|
|
20
|
+
else if (r.state === 'conflict')
|
|
21
|
+
s.conflict++;
|
|
22
|
+
else if (r.state === 'error')
|
|
23
|
+
s.error++;
|
|
24
|
+
else if (r.state === 'skipped')
|
|
25
|
+
s.skipped++;
|
|
26
|
+
}
|
|
27
|
+
return s;
|
|
28
|
+
}
|
|
29
|
+
function updateRow(state, ref, patch) {
|
|
30
|
+
const rows = state.rows.map((r) => (r.ref === ref ? { ...r, ...patch } : r));
|
|
31
|
+
return { ...state, rows, summary: recomputeSummary(rows) };
|
|
32
|
+
}
|
|
33
|
+
export function syncReducer(state, action) {
|
|
34
|
+
switch (action.type) {
|
|
35
|
+
case 'INIT': {
|
|
36
|
+
const rows = action.rows.map((r) => ({
|
|
37
|
+
ref: r.ref,
|
|
38
|
+
team: r.team,
|
|
39
|
+
state: 'queued',
|
|
40
|
+
}));
|
|
41
|
+
return { rows, summary: recomputeSummary(rows), finished: false };
|
|
42
|
+
}
|
|
43
|
+
case 'ADD_ROW': {
|
|
44
|
+
// Late-added row (e.g. team-pinned skills surfaced after personal sync).
|
|
45
|
+
// No-op if a row with this ref already exists — keep the existing state.
|
|
46
|
+
if (state.rows.some((r) => r.ref === action.ref))
|
|
47
|
+
return state;
|
|
48
|
+
const rows = [
|
|
49
|
+
...state.rows,
|
|
50
|
+
{ ref: action.ref, team: action.team, state: 'queued' },
|
|
51
|
+
];
|
|
52
|
+
return { ...state, rows, summary: recomputeSummary(rows) };
|
|
53
|
+
}
|
|
54
|
+
case 'CHECKING':
|
|
55
|
+
return updateRow(state, action.ref, { state: 'checking' });
|
|
56
|
+
case 'UP_TO_DATE':
|
|
57
|
+
return updateRow(state, action.ref, {
|
|
58
|
+
state: 'up-to-date',
|
|
59
|
+
details: action.details,
|
|
60
|
+
resolveConflict: undefined,
|
|
61
|
+
});
|
|
62
|
+
case 'UPDATING':
|
|
63
|
+
return updateRow(state, action.ref, { state: 'updating', details: action.details });
|
|
64
|
+
case 'UPDATED':
|
|
65
|
+
return updateRow(state, action.ref, {
|
|
66
|
+
state: 'updated',
|
|
67
|
+
details: action.details,
|
|
68
|
+
resolveConflict: undefined,
|
|
69
|
+
});
|
|
70
|
+
case 'CONFLICT':
|
|
71
|
+
return updateRow(state, action.ref, {
|
|
72
|
+
state: 'conflict',
|
|
73
|
+
details: action.details,
|
|
74
|
+
resolveConflict: action.resolveConflict,
|
|
75
|
+
});
|
|
76
|
+
case 'SKIPPED':
|
|
77
|
+
return updateRow(state, action.ref, {
|
|
78
|
+
state: 'skipped',
|
|
79
|
+
details: action.details,
|
|
80
|
+
resolveConflict: undefined,
|
|
81
|
+
});
|
|
82
|
+
case 'ERROR':
|
|
83
|
+
return updateRow(state, action.ref, {
|
|
84
|
+
state: 'error',
|
|
85
|
+
details: action.details,
|
|
86
|
+
resolveConflict: undefined,
|
|
87
|
+
});
|
|
88
|
+
case 'FINISH':
|
|
89
|
+
return { ...state, elapsedMs: action.elapsedMs, finished: true };
|
|
90
|
+
default:
|
|
91
|
+
return state;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Shared BotDocs CLI theme tokens. Mirrors the web app's Tailwind palette so
|
|
2
|
+
* the brand stays consistent across surfaces. Values are hex strings; Ink
|
|
3
|
+
* components accept hex directly for `color` and `backgroundColor` props. */
|
|
4
|
+
export declare const theme: {
|
|
5
|
+
readonly cyan: "#22d3ee";
|
|
6
|
+
readonly violet: "#a78bfa";
|
|
7
|
+
readonly green: "#34d399";
|
|
8
|
+
readonly amber: "#fbbf24";
|
|
9
|
+
readonly red: "#f87171";
|
|
10
|
+
readonly zinc: "#71717a";
|
|
11
|
+
};
|
|
12
|
+
/** Width below which we skip the `ink-big-text` ASCII wordmark and fall back
|
|
13
|
+
* to a plain bold "botdocs" line — narrower terminals render the big-text
|
|
14
|
+
* with awkward wrapping. 60 was chosen empirically: a 50-column wordmark plus
|
|
15
|
+
* margins fits comfortably above that. */
|
|
16
|
+
export declare const BIG_TEXT_MIN_COLS = 60;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Shared BotDocs CLI theme tokens. Mirrors the web app's Tailwind palette so
|
|
2
|
+
* the brand stays consistent across surfaces. Values are hex strings; Ink
|
|
3
|
+
* components accept hex directly for `color` and `backgroundColor` props. */
|
|
4
|
+
export const theme = {
|
|
5
|
+
cyan: '#22d3ee', // Tailwind cyan-400 — primary brand
|
|
6
|
+
violet: '#a78bfa', // Tailwind violet-400 — secondary brand
|
|
7
|
+
green: '#34d399', // Tailwind emerald-400 — success
|
|
8
|
+
amber: '#fbbf24', // Tailwind amber-400 — warning
|
|
9
|
+
red: '#f87171', // Tailwind red-400 — error
|
|
10
|
+
zinc: '#71717a', // Tailwind zinc-500 — subtle / hint
|
|
11
|
+
};
|
|
12
|
+
/** Width below which we skip the `ink-big-text` ASCII wordmark and fall back
|
|
13
|
+
* to a plain bold "botdocs" line — narrower terminals render the big-text
|
|
14
|
+
* with awkward wrapping. 60 was chosen empirically: a 50-column wordmark plus
|
|
15
|
+
* margins fits comfortably above that. */
|
|
16
|
+
export const BIG_TEXT_MIN_COLS = 60;
|
package/dist/commands/whoami.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
import { loadAuth } from '../lib/config.js';
|
|
2
|
+
import { apiFetch, ApiError } from '../lib/api.js';
|
|
2
3
|
export async function whoami(options = {}) {
|
|
3
4
|
const auth = loadAuth();
|
|
4
5
|
if (!auth) {
|
|
5
6
|
console.log('Not logged in. Run `botdocs login` to authenticate.');
|
|
6
7
|
process.exit(1);
|
|
7
8
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
let user;
|
|
10
|
+
try {
|
|
11
|
+
user = await apiFetch('/api/cli/whoami', { auth: true });
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
if (err instanceof ApiError && err.status === 401) {
|
|
15
|
+
console.log('Authentication failed. Run `botdocs login` to sign in again.');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
throw err;
|
|
18
19
|
}
|
|
19
|
-
const user = (await response.json());
|
|
20
20
|
if (options.json) {
|
|
21
|
-
console.log(JSON.stringify({ login: user.
|
|
21
|
+
console.log(JSON.stringify({ login: user.username, name: user.displayName }));
|
|
22
22
|
}
|
|
23
23
|
else {
|
|
24
|
-
console.log(`Logged in as ${user.
|
|
24
|
+
console.log(`Logged in as ${user.username}${user.displayName && user.displayName !== user.username ? ` (${user.displayName})` : ''}`);
|
|
25
25
|
}
|
|
26
26
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import { clone } from './commands/clone.js';
|
|
4
3
|
import { search } from './commands/search.js';
|
|
5
4
|
import { publish } from './commands/publish.js';
|
|
6
|
-
import { pull } from './commands/pull.js';
|
|
7
5
|
import { login } from './commands/login.js';
|
|
8
6
|
import { whoami } from './commands/whoami.js';
|
|
9
7
|
import { init } from './commands/init.js';
|
|
10
8
|
import { validate } from './commands/validate.js';
|
|
11
|
-
import { diff } from './commands/diff.js';
|
|
12
|
-
import { endorse } from './commands/endorse.js';
|
|
13
9
|
import { installInstructions } from './commands/install-instructions.js';
|
|
14
10
|
import { install } from './commands/install.js';
|
|
15
11
|
import { list } from './commands/list.js';
|
|
@@ -19,11 +15,17 @@ import { ingest } from './commands/ingest.js';
|
|
|
19
15
|
import { checkUpdates } from './commands/check-updates.js';
|
|
20
16
|
import { compile } from './commands/compile.js';
|
|
21
17
|
import { edit } from './commands/edit.js';
|
|
18
|
+
import { registerTeamCommands } from './commands/team.js';
|
|
19
|
+
import { registerBackupCommands } from './commands/backups.js';
|
|
20
|
+
import { undo } from './commands/undo.js';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const pkg = require('../package.json');
|
|
22
24
|
const program = new Command();
|
|
23
25
|
program
|
|
24
26
|
.name('botdocs')
|
|
25
|
-
.description('CLI for BotDocs —
|
|
26
|
-
.version(
|
|
27
|
+
.description('CLI for BotDocs — author, publish, install, and sync agent skills')
|
|
28
|
+
.version(pkg.version)
|
|
27
29
|
.option('--json', 'Output results as JSON');
|
|
28
30
|
program
|
|
29
31
|
.command('init [name]')
|
|
@@ -40,12 +42,6 @@ program
|
|
|
40
42
|
.action(async (source) => {
|
|
41
43
|
await validate(source, { json: program.opts().json });
|
|
42
44
|
});
|
|
43
|
-
program
|
|
44
|
-
.command('clone <username/slug>')
|
|
45
|
-
.description('Download all BotDoc files to a local directory')
|
|
46
|
-
.action(async (ref) => {
|
|
47
|
-
await clone(ref, { json: program.opts().json });
|
|
48
|
-
});
|
|
49
45
|
program
|
|
50
46
|
.command('search <query>')
|
|
51
47
|
.description('Search for BotDocs')
|
|
@@ -64,32 +60,17 @@ program
|
|
|
64
60
|
.action(async (source, options) => {
|
|
65
61
|
await publish(source, { ...options, json: program.opts().json });
|
|
66
62
|
});
|
|
67
|
-
program
|
|
68
|
-
.command('diff <username/slug>')
|
|
69
|
-
.description('Preview remote changes before pulling')
|
|
70
|
-
.action(async (ref) => {
|
|
71
|
-
await diff(ref, { json: program.opts().json });
|
|
72
|
-
});
|
|
73
|
-
program
|
|
74
|
-
.command('pull <username/slug>')
|
|
75
|
-
.description('Update previously cloned BotDoc files with the latest version')
|
|
76
|
-
.action(async (ref) => {
|
|
77
|
-
await pull(ref, { json: program.opts().json });
|
|
78
|
-
});
|
|
79
|
-
program
|
|
80
|
-
.command('endorse <username/slug>')
|
|
81
|
-
.description('Endorse a BotDoc after building from it')
|
|
82
|
-
.requiredOption('--rating <rating>', 'Rating: positive, neutral, or negative')
|
|
83
|
-
.option('--comment <comment>', 'Optional feedback about the build experience')
|
|
84
|
-
.action(async (ref, opts) => {
|
|
85
|
-
await endorse(ref, { ...opts, json: program.opts().json });
|
|
86
|
-
});
|
|
87
63
|
program
|
|
88
64
|
.command('login')
|
|
89
|
-
.description('Authenticate
|
|
65
|
+
.description('Authenticate by opening your browser; or pass --token for non-interactive use')
|
|
90
66
|
.option('--sync-library', 'Enable personalized Library page (uploads sanitized lockfile after install/sync/uninstall)')
|
|
67
|
+
.option('--token <token>', 'Authenticate non-interactively with a bd_<...> token (mint one at /settings/tokens)')
|
|
68
|
+
.option('--no-ink', 'Disable the live Ink UI and use plain text output (for screen readers or simple terminals)')
|
|
91
69
|
.action(async (opts) => {
|
|
92
|
-
|
|
70
|
+
// Commander's --no-ink convention sets opts.ink = false; flip to noInk
|
|
71
|
+
// so downstream consumers don't have to handle the inverted boolean.
|
|
72
|
+
const { ink, ...rest } = opts;
|
|
73
|
+
await login({ ...rest, noInk: ink === false });
|
|
93
74
|
});
|
|
94
75
|
program
|
|
95
76
|
.command('whoami')
|
|
@@ -108,12 +89,16 @@ program
|
|
|
108
89
|
});
|
|
109
90
|
program
|
|
110
91
|
.command('install <ref>')
|
|
111
|
-
.description('Install a skill or bundle locally (skills go to ~/.claude/skills/, project files to .cursor/rules/, etc.)')
|
|
92
|
+
.description('Install a skill or bundle locally (skills go to ~/.claude/skills/, project files to .cursor/rules/, .codex/skills/, .github/instructions/, etc.)')
|
|
112
93
|
.option('--project <dir>', 'Override the project root used for project-local files')
|
|
113
94
|
.option('--flat', 'Skip the {scope} subdirectory in install paths (collision-prone, not recommended)')
|
|
114
95
|
.option('--clean', 'Wipe-and-reinstall instead of additive')
|
|
96
|
+
.option('--no-backup', 'Do not back up files that would be overwritten (use in CI where backups are noise)')
|
|
115
97
|
.action(async (ref, opts) => {
|
|
116
|
-
|
|
98
|
+
// Commander's --no-backup convention sets opts.backup = false.
|
|
99
|
+
// Translate to options.noBackup for downstream consumers.
|
|
100
|
+
const { backup, ...rest } = opts;
|
|
101
|
+
await install(ref, { ...rest, noBackup: backup === false, json: program.opts().json });
|
|
117
102
|
});
|
|
118
103
|
program
|
|
119
104
|
.command('list')
|
|
@@ -133,8 +118,16 @@ program
|
|
|
133
118
|
.description('Check installed skills/bundles for updates and apply')
|
|
134
119
|
.option('--yes', 'Auto-accept clean updates; skip files with local edits')
|
|
135
120
|
.option('--dry-run', 'Show what would change without applying')
|
|
121
|
+
.option('--no-backup', 'Do not back up files that would be overwritten (use in CI where backups are noise)')
|
|
122
|
+
.option('--no-ink', 'Disable the live Ink summary view and use plain text output')
|
|
136
123
|
.action(async (ref, opts) => {
|
|
137
|
-
|
|
124
|
+
const { backup, ink, ...rest } = opts;
|
|
125
|
+
await sync(ref, {
|
|
126
|
+
...rest,
|
|
127
|
+
noBackup: backup === false,
|
|
128
|
+
noInk: ink === false,
|
|
129
|
+
json: program.opts().json,
|
|
130
|
+
});
|
|
138
131
|
});
|
|
139
132
|
program
|
|
140
133
|
.command('check-updates')
|
|
@@ -164,9 +157,22 @@ program
|
|
|
164
157
|
program
|
|
165
158
|
.command('edit <ref>')
|
|
166
159
|
.description('LLM-assisted revision of a published skill ecosystem file (BYOK; pushes a draft)')
|
|
167
|
-
.requiredOption('--ecosystem <name>', 'Which ecosystem file to edit (claude, claude-code, cursor, chatgpt, codex)')
|
|
160
|
+
.requiredOption('--ecosystem <name>', 'Which ecosystem file to edit (claude, claude-code, cursor, chatgpt, codex, copilot, antigravity, gemini, opencode, windsurf)')
|
|
168
161
|
.option('--key-env <name>', 'Override the env var used for the API key')
|
|
169
162
|
.action(async (ref, opts) => {
|
|
170
163
|
await edit(ref, { ...opts, json: program.opts().json });
|
|
171
164
|
});
|
|
165
|
+
registerTeamCommands(program);
|
|
166
|
+
registerBackupCommands(program);
|
|
167
|
+
program
|
|
168
|
+
.command('undo')
|
|
169
|
+
.description('Restore files from the most recent backup run (reversible)')
|
|
170
|
+
.option('--dry-run', 'Show what would be restored without writing')
|
|
171
|
+
.option('--yes', 'Skip the confirmation prompt')
|
|
172
|
+
.option('--no-backup', 'Skip backing up the current state before restoring (advanced)')
|
|
173
|
+
.action(async (opts) => {
|
|
174
|
+
// Commander's --no-backup convention sets opts.backup = false.
|
|
175
|
+
const { backup, ...rest } = opts;
|
|
176
|
+
await undo({ ...rest, noBackup: backup === false, json: program.opts().json });
|
|
177
|
+
});
|
|
172
178
|
program.parse();
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
export declare function getApiUrl(): string;
|
|
2
2
|
/** Thrown by apiFetch when the server returns a non-2xx response. Carries the
|
|
3
|
-
* status code and the server-provided message so callers can branch on
|
|
4
|
-
*
|
|
5
|
-
* the clone-required guard). */
|
|
3
|
+
* status code and the server-provided message so callers can branch on
|
|
4
|
+
* different HTTP error codes (404 vs 403 vs 5xx) with friendly hints. */
|
|
6
5
|
export declare class ApiError extends Error {
|
|
7
6
|
readonly status: number;
|
|
8
7
|
constructor(status: number, message: string);
|
package/dist/lib/api.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { loadAuth } from './config.js';
|
|
2
|
-
const DEFAULT_API_URL = 'https://botdocs.ai';
|
|
2
|
+
const DEFAULT_API_URL = 'https://www.botdocs.ai';
|
|
3
3
|
export function getApiUrl() {
|
|
4
4
|
return process.env.BOTDOCS_API_URL || DEFAULT_API_URL;
|
|
5
5
|
}
|
|
6
6
|
/** Thrown by apiFetch when the server returns a non-2xx response. Carries the
|
|
7
|
-
* status code and the server-provided message so callers can branch on
|
|
8
|
-
*
|
|
9
|
-
* the clone-required guard). */
|
|
7
|
+
* status code and the server-provided message so callers can branch on
|
|
8
|
+
* different HTTP error codes (404 vs 403 vs 5xx) with friendly hints. */
|
|
10
9
|
export class ApiError extends Error {
|
|
11
10
|
status;
|
|
12
11
|
constructor(status, message) {
|
|
@@ -27,10 +26,13 @@ export async function apiFetch(path, options = {}) {
|
|
|
27
26
|
}
|
|
28
27
|
if (auth) {
|
|
29
28
|
const config = loadAuth();
|
|
30
|
-
if (!config?.
|
|
31
|
-
|
|
29
|
+
if (!config?.token) {
|
|
30
|
+
// Shape this as an ApiError(401) so callers that already branch on auth
|
|
31
|
+
// failures (e.g. sync's optional team-skill path) handle it uniformly
|
|
32
|
+
// without a separate "no token saved" code path.
|
|
33
|
+
throw new ApiError(401, 'Not authenticated. Run `botdocs login` first.');
|
|
32
34
|
}
|
|
33
|
-
headers['Authorization'] = `Bearer ${config.
|
|
35
|
+
headers['Authorization'] = `Bearer ${config.token}`;
|
|
34
36
|
}
|
|
35
37
|
const response = await fetch(url, {
|
|
36
38
|
method,
|
|
@@ -47,6 +49,11 @@ export async function apiFetch(path, options = {}) {
|
|
|
47
49
|
catch {
|
|
48
50
|
message = text;
|
|
49
51
|
}
|
|
52
|
+
// Friendlier 401 hint when the saved token is stale (e.g. left over from a
|
|
53
|
+
// previous CLI version that stored a GitHub access token).
|
|
54
|
+
if (response.status === 401 && auth) {
|
|
55
|
+
message = 'Authentication failed. Run `botdocs login` to sign in again.';
|
|
56
|
+
}
|
|
50
57
|
throw new ApiError(response.status, message);
|
|
51
58
|
}
|
|
52
59
|
const contentType = response.headers.get('content-type') || '';
|
package/dist/lib/auto-detect.js
CHANGED
|
@@ -27,6 +27,52 @@ export function detectDestination(srcRelative, ctx) {
|
|
|
27
27
|
dest: path.join(ctx.projectDir, '.cursor', 'rules', path.basename(src)),
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
+
if (src.startsWith('codex/')) {
|
|
31
|
+
return {
|
|
32
|
+
kind: 'project',
|
|
33
|
+
dest: path.join(ctx.projectDir, '.codex', 'skills', path.basename(src)),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (src.startsWith('copilot/instructions/')) {
|
|
37
|
+
// GitHub Copilot custom instructions live in .github/instructions/
|
|
38
|
+
// (https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot).
|
|
39
|
+
return {
|
|
40
|
+
kind: 'project',
|
|
41
|
+
dest: path.join(ctx.projectDir, '.github', 'instructions', path.basename(src)),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (src.startsWith('windsurf/rules/')) {
|
|
45
|
+
// Windsurf (Codeium) reads project rules from .codeium/windsurf-rules/
|
|
46
|
+
// (https://docs.codeium.com/windsurf/cascade#windsurfrules).
|
|
47
|
+
return {
|
|
48
|
+
kind: 'project',
|
|
49
|
+
dest: path.join(ctx.projectDir, '.codeium', 'windsurf-rules', path.basename(src)),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (src.startsWith('gemini/instructions/')) {
|
|
53
|
+
// Gemini CLI reads global instructions from ~/.gemini/instructions/
|
|
54
|
+
// (https://github.com/google-gemini/gemini-cli).
|
|
55
|
+
return {
|
|
56
|
+
kind: 'global',
|
|
57
|
+
dest: path.join(ctx.homeDir, '.gemini', 'instructions', path.basename(src)),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (src.startsWith('antigravity/skills/')) {
|
|
61
|
+
// Google Antigravity reads skills from ~/.gemini/antigravity/skills/
|
|
62
|
+
// (shares the gemini config tree).
|
|
63
|
+
return {
|
|
64
|
+
kind: 'global',
|
|
65
|
+
dest: path.join(ctx.homeDir, '.gemini', 'antigravity', 'skills', path.basename(src)),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (src.startsWith('opencode/instructions/')) {
|
|
69
|
+
// OpenCode (SST) reads instructions from ~/.config/opencode/instructions/
|
|
70
|
+
// (https://github.com/sst/opencode).
|
|
71
|
+
return {
|
|
72
|
+
kind: 'global',
|
|
73
|
+
dest: path.join(ctx.homeDir, '.config', 'opencode', 'instructions', path.basename(src)),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
30
76
|
if (src.startsWith('chatgpt/')) {
|
|
31
77
|
return { kind: 'manual', dest: src };
|
|
32
78
|
}
|