@bitpub/cli 2.0.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 +98 -0
- package/bin/bitpub.js +67 -0
- package/package.json +58 -0
- package/skills/bitpub/SKILL.md +325 -0
- package/src/agents-md.js +100 -0
- package/src/aliases.js +116 -0
- package/src/api.js +177 -0
- package/src/commands/alias.js +79 -0
- package/src/commands/auth.js +50 -0
- package/src/commands/browser.js +196 -0
- package/src/commands/catchup.js +109 -0
- package/src/commands/delete.js +189 -0
- package/src/commands/drop.js +22 -0
- package/src/commands/fetch.js +29 -0
- package/src/commands/find.js +175 -0
- package/src/commands/grep.js +26 -0
- package/src/commands/init.js +49 -0
- package/src/commands/list.js +241 -0
- package/src/commands/load.js +122 -0
- package/src/commands/push.js +84 -0
- package/src/commands/read.js +42 -0
- package/src/commands/recent.js +67 -0
- package/src/commands/restore.js +23 -0
- package/src/commands/save.js +255 -0
- package/src/commands/seed.js +152 -0
- package/src/commands/setup.js +312 -0
- package/src/commands/skills.js +304 -0
- package/src/commands/status.js +62 -0
- package/src/commands/sync.js +160 -0
- package/src/commands/trash.js +88 -0
- package/src/commands/update.js +155 -0
- package/src/commands/watch.js +24 -0
- package/src/commands/welcome.js +189 -0
- package/src/config.js +85 -0
- package/src/crypto.js +61 -0
- package/src/db/cache.js +373 -0
- package/src/workspace.js +377 -0
- package/static/console.html +2263 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `bitpub setup` — first-time install + identity provision + skill registration.
|
|
5
|
+
*
|
|
6
|
+
* One verb, three jobs. Idempotent — re-running is always safe and skips
|
|
7
|
+
* any step that's already done. Most agents won't call this directly:
|
|
8
|
+
* the first `bitpub save` from a non-$HOME folder runs setup lazily for
|
|
9
|
+
* any user that doesn't have an identity yet (see save.js for the
|
|
10
|
+
* autosetup hook).
|
|
11
|
+
*
|
|
12
|
+
* Subcommands for the rare cases where explicit control matters:
|
|
13
|
+
*
|
|
14
|
+
* bitpub setup → identity + project anchor + skill install
|
|
15
|
+
* bitpub setup team → join a team (key + domain, today; invite token, later)
|
|
16
|
+
* bitpub setup skill install → register the BitPub skill into Claude/Cursor/Codex
|
|
17
|
+
* bitpub setup skill list → show where the skill is installed
|
|
18
|
+
*
|
|
19
|
+
* The original `init`, `auth login`, and `skills install` verbs are kept
|
|
20
|
+
* as hidden, deprecation-warning aliases so existing scripts keep working.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const os = require('os');
|
|
26
|
+
const axios = require('axios');
|
|
27
|
+
const {
|
|
28
|
+
readConfig,
|
|
29
|
+
writeConfig,
|
|
30
|
+
isConfigured,
|
|
31
|
+
DEFAULT_CLOUD_URL,
|
|
32
|
+
} = require('../config');
|
|
33
|
+
const { initCache } = require('../db/cache');
|
|
34
|
+
const { createWorkspace, findWorkspace } = require('../workspace');
|
|
35
|
+
const { injectAgentsMd } = require('../agents-md');
|
|
36
|
+
const { installSkills, listSkillStatus } = require('./skills');
|
|
37
|
+
const { createApiClient } = require('../api');
|
|
38
|
+
|
|
39
|
+
const LEGACY_CONFIG_DIR = path.join(os.homedir(), '.tollbit');
|
|
40
|
+
const LEGACY_CONFIG_FILE = path.join(LEGACY_CONFIG_DIR, 'config.json');
|
|
41
|
+
const LEGACY_ALIASES_FILE = path.join(LEGACY_CONFIG_DIR, 'aliases.json');
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Ensure the local store + a private cloud identity exist. Used both by
|
|
45
|
+
* `bitpub setup` directly and by save.js as a lazy bootstrap. Pass
|
|
46
|
+
* `quiet: true` from inside another command to suppress the ascii output
|
|
47
|
+
* (callers will report success themselves).
|
|
48
|
+
*
|
|
49
|
+
* Returns the resulting config (after any provisioning), or null if it
|
|
50
|
+
* couldn't provision and `localOnly` was passed.
|
|
51
|
+
*/
|
|
52
|
+
async function ensureIdentity({ url = DEFAULT_CLOUD_URL, force = false, localOnly = false, quiet = false } = {}) {
|
|
53
|
+
url = url.replace(/\/$/, '');
|
|
54
|
+
initCache();
|
|
55
|
+
|
|
56
|
+
// Legacy ~/.tollbit/ import — preserves continuity for anyone who
|
|
57
|
+
// installed before the BitPub rename. Silent on success unless quiet=false.
|
|
58
|
+
const importedFromLegacy = maybeImportLegacyConfig({ quiet });
|
|
59
|
+
|
|
60
|
+
const existing = readConfig();
|
|
61
|
+
const alreadyHasIdentity = isConfigured(existing) && !force;
|
|
62
|
+
|
|
63
|
+
if (alreadyHasIdentity) return existing;
|
|
64
|
+
|
|
65
|
+
if (localOnly) {
|
|
66
|
+
if (!quiet) console.log('(--local-only: skipping cloud provisioning.)');
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!quiet) console.log(`Provisioning identity against ${url}…`);
|
|
71
|
+
|
|
72
|
+
let resp;
|
|
73
|
+
try {
|
|
74
|
+
resp = await axios.post(
|
|
75
|
+
`${url}/v1/identity/provision`,
|
|
76
|
+
{},
|
|
77
|
+
{ timeout: 15_000, headers: { 'content-type': 'application/json' } }
|
|
78
|
+
);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const status = err.response?.status;
|
|
81
|
+
const detail = err.response?.data?.error || err.message || err.code || 'unknown error';
|
|
82
|
+
if (status === 503) {
|
|
83
|
+
throw new Error(`${url} doesn't support self-service identity. Use: bitpub setup team --key <K> --domain <D>`);
|
|
84
|
+
} else if (status === 429) {
|
|
85
|
+
throw new Error(`Rate limit at ${url}. Try again later.`);
|
|
86
|
+
} else if (!err.response) {
|
|
87
|
+
throw new Error(`Could not reach ${url} (${err.code || detail}).`);
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`Provisioning failed: ${detail}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const { api_key, owner_id, namespace, tier, storage_cap_bytes, writes_per_day } = resp.data;
|
|
93
|
+
writeConfig({
|
|
94
|
+
api_key,
|
|
95
|
+
owner: owner_id,
|
|
96
|
+
api_url: url,
|
|
97
|
+
tier,
|
|
98
|
+
provisioned_at: new Date().toISOString(),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (!quiet) {
|
|
102
|
+
const capMb = Math.round((storage_cap_bytes || 0) / (1024 * 1024));
|
|
103
|
+
console.log('✓ Cloud identity provisioned\n');
|
|
104
|
+
console.log(` Owner : ${owner_id}`);
|
|
105
|
+
console.log(` Namespace : ${namespace}`);
|
|
106
|
+
console.log(` Tier : ${tier}`);
|
|
107
|
+
console.log(` Storage : 0 / ${capMb} MB`);
|
|
108
|
+
if (writes_per_day) console.log(` Writes : 0 / ${writes_per_day} per day`);
|
|
109
|
+
console.log(` Backend : ${url}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return readConfig();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Make sure this folder has a project anchor. Returns the anchor (new
|
|
117
|
+
* or existing), or null if we're in $HOME (where we refuse to anchor)
|
|
118
|
+
* or `owner` is missing.
|
|
119
|
+
*
|
|
120
|
+
* `quiet: true` is the common case — `bitpub save` calls this with
|
|
121
|
+
* `quiet: true` so the first save in a new folder is indistinguishable
|
|
122
|
+
* from the hundredth. `bitpub setup` calls it with `quiet: false` to
|
|
123
|
+
* report what just happened in human terms.
|
|
124
|
+
*/
|
|
125
|
+
function ensureWorkspace({ owner, cwd = process.cwd(), label, quiet = false } = {}) {
|
|
126
|
+
if (!owner) return null;
|
|
127
|
+
const home = path.resolve(os.homedir());
|
|
128
|
+
if (path.resolve(cwd) === home) {
|
|
129
|
+
if (!quiet) {
|
|
130
|
+
console.log('(Skipping project anchor — you\'re in $HOME. Run setup inside a project folder to anchor here.)');
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const existing = findWorkspace(cwd);
|
|
136
|
+
if (existing) {
|
|
137
|
+
if (!quiet) {
|
|
138
|
+
console.log(`✓ This folder is already a project: ${existing.marker.label || existing.marker.id}`);
|
|
139
|
+
console.log(` Address : ${existing.marker.namespace}`);
|
|
140
|
+
}
|
|
141
|
+
injectAgentsMd(cwd, { quiet: true });
|
|
142
|
+
return existing;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const ws = createWorkspace(cwd, owner, { label });
|
|
147
|
+
if (!quiet) {
|
|
148
|
+
console.log('✓ Anchored this folder as a project');
|
|
149
|
+
console.log(` Label : ${ws.marker.label}`);
|
|
150
|
+
console.log(` Address : ${ws.marker.namespace}`);
|
|
151
|
+
}
|
|
152
|
+
injectAgentsMd(cwd, { quiet });
|
|
153
|
+
return ws;
|
|
154
|
+
} catch (err) {
|
|
155
|
+
if (!quiet) console.error(`(Could not anchor folder: ${err.message})`);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function ensureSkill({ quiet = false } = {}) {
|
|
161
|
+
try {
|
|
162
|
+
const { installed } = await installSkills({});
|
|
163
|
+
if (installed > 0 && !quiet) {
|
|
164
|
+
console.log('\n✓ BitPub skill installed into agent directories');
|
|
165
|
+
console.log(' Quit Claude Code and reopen it to pick up the new skill.');
|
|
166
|
+
}
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (!quiet) console.log(`\n(Skill install skipped: ${err.message} — run 'bitpub setup skill install' to retry)`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = function registerSetup(program) {
|
|
173
|
+
const setup = program
|
|
174
|
+
.command('setup')
|
|
175
|
+
.description('First-time install + identity provision + skill registration. Idempotent.')
|
|
176
|
+
.option('--url <string>', 'Cloud tenant URL', DEFAULT_CLOUD_URL)
|
|
177
|
+
.option('--local-only', 'Set up the local store only; skip cloud provisioning')
|
|
178
|
+
.option('--no-anchor', 'Skip anchoring this folder as a project')
|
|
179
|
+
.option('--label <string>', 'Override the project label (defaults to folder name)')
|
|
180
|
+
.option('--force', 'Overwrite an existing identity (the old key is unrecoverable after this)')
|
|
181
|
+
.option('--import-from <path>', 'Import an existing TollBit config (defaults to ~/.tollbit/config.json if present)')
|
|
182
|
+
.option('--no-import', 'Skip auto-import even if a legacy ~/.tollbit/ config is present')
|
|
183
|
+
.action(async (opts) => {
|
|
184
|
+
try {
|
|
185
|
+
const config = await ensureIdentity({
|
|
186
|
+
url: opts.url,
|
|
187
|
+
force: !!opts.force,
|
|
188
|
+
localOnly: !!opts.localOnly,
|
|
189
|
+
});
|
|
190
|
+
if (config && opts.anchor !== false) {
|
|
191
|
+
ensureWorkspace({ owner: config.owner, label: opts.label });
|
|
192
|
+
}
|
|
193
|
+
await ensureSkill({});
|
|
194
|
+
console.log('\nTry it:');
|
|
195
|
+
console.log(' bitpub save notes "first slice"');
|
|
196
|
+
console.log(' bitpub list # see what\'s in this project');
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.error(`\n✗ ${err.message}`);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ── setup team … ────────────────────────────────────────────────────
|
|
204
|
+
// Joins (or rejoins) a team namespace. Today: requires --key + --domain
|
|
205
|
+
// from a team admin. Future: --invite <token> in lieu of those, once
|
|
206
|
+
// server-side invite generation lands.
|
|
207
|
+
setup
|
|
208
|
+
.command('team')
|
|
209
|
+
.description('Join a team namespace (key + domain from your team admin)')
|
|
210
|
+
.requiredOption('--key <string>', 'Team API key provided by your team admin')
|
|
211
|
+
.requiredOption('--domain <string>', 'Your organization domain (e.g. acme.com)')
|
|
212
|
+
.option('--url <string>', 'Backend URL (defaults to current api_url or https://bitpub.io)')
|
|
213
|
+
.option('--verify', 'Ping the backend to verify the key before saving')
|
|
214
|
+
.action(async ({ key, domain, url, verify }) => {
|
|
215
|
+
const existing = readConfig() || {};
|
|
216
|
+
const apiUrl = url || existing.api_url || DEFAULT_CLOUD_URL;
|
|
217
|
+
|
|
218
|
+
if (verify) {
|
|
219
|
+
try {
|
|
220
|
+
const api = createApiClient({ group_key: key, api_url: apiUrl });
|
|
221
|
+
await api.pull(`bitpub://group:${domain}/**`, 1);
|
|
222
|
+
console.log('✓ Key verified against backend');
|
|
223
|
+
} catch (err) {
|
|
224
|
+
console.error(`Key verification failed: ${err.message}`);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
writeConfig({ ...existing, group_key: key, domain, api_url: apiUrl });
|
|
230
|
+
initCache();
|
|
231
|
+
|
|
232
|
+
console.log(`✓ Joined team ${domain}`);
|
|
233
|
+
console.log(` Backend : ${apiUrl}`);
|
|
234
|
+
if (existing.owner) {
|
|
235
|
+
console.log(` Private : agent_${existing.owner} (preserved)`);
|
|
236
|
+
}
|
|
237
|
+
console.log('\nNext: bitpub sync "bitpub://group:' + domain + '/**"');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ── setup skill {install|list} ──────────────────────────────────────
|
|
241
|
+
const skill = setup.command('skill').description('Manage agent skill installation');
|
|
242
|
+
|
|
243
|
+
skill
|
|
244
|
+
.command('install')
|
|
245
|
+
.description('Install the BitPub skill into every detected agent directory (Claude, Cursor, Codex)')
|
|
246
|
+
.option('--target <name>', 'Install only into a single named target (claude|cursor|codex|project|agents-md)')
|
|
247
|
+
.option('--all', 'Install everywhere, creating directories that do not yet exist')
|
|
248
|
+
.option('--dry-run', 'Print what would be installed without writing files')
|
|
249
|
+
.option('--no-agents-md', 'Skip AGENTS.md injection in the current project')
|
|
250
|
+
.action(async (opts) => {
|
|
251
|
+
const { installed, skipped } = await installSkills(opts);
|
|
252
|
+
console.log('');
|
|
253
|
+
console.log(`Done. ${installed} installed/updated, ${skipped} skipped.`);
|
|
254
|
+
if (installed > 0) {
|
|
255
|
+
console.log('Quit Claude Code and reopen it to pick up the new skill — starting a new chat is not enough.');
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
skill
|
|
260
|
+
.command('list')
|
|
261
|
+
.description('Show where the BitPub skill is currently installed')
|
|
262
|
+
.action(listSkillStatus);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// ── Legacy ~/.tollbit/ import (kept verbatim from original init.js) ────
|
|
266
|
+
function maybeImportLegacyConfig({ quiet = false } = {}) {
|
|
267
|
+
const sourcePath = LEGACY_CONFIG_FILE;
|
|
268
|
+
if (!fs.existsSync(sourcePath)) return false;
|
|
269
|
+
if (isConfigured(readConfig())) return false;
|
|
270
|
+
|
|
271
|
+
let legacy;
|
|
272
|
+
try {
|
|
273
|
+
legacy = JSON.parse(fs.readFileSync(sourcePath, 'utf-8'));
|
|
274
|
+
} catch (err) {
|
|
275
|
+
if (!quiet) console.error(`\n(Could not parse ${sourcePath}: ${err.message} — skipping import)`);
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const migrated = { ...legacy };
|
|
280
|
+
if (typeof migrated.api_url === 'string') {
|
|
281
|
+
migrated.api_url = migrated.api_url
|
|
282
|
+
.replace(/^https?:\/\/tollbit\.org/, 'https://bitpub.io')
|
|
283
|
+
.replace(/^https?:\/\/tollbit\.com/, 'https://bitpub.io');
|
|
284
|
+
}
|
|
285
|
+
migrated.imported_from = sourcePath;
|
|
286
|
+
migrated.imported_at = new Date().toISOString();
|
|
287
|
+
writeConfig(migrated);
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
if (fs.existsSync(LEGACY_ALIASES_FILE)) {
|
|
291
|
+
const newAliasesPath = path.join(os.homedir(), '.bitpub', 'aliases.json');
|
|
292
|
+
if (!fs.existsSync(newAliasesPath)) {
|
|
293
|
+
const raw = fs.readFileSync(LEGACY_ALIASES_FILE, 'utf-8');
|
|
294
|
+
const rewritten = raw.replace(/tollbit:\/\//g, 'bitpub://');
|
|
295
|
+
fs.writeFileSync(newAliasesPath, rewritten);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
// Non-fatal
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (!quiet) {
|
|
303
|
+
console.log(`✓ Imported existing identity from ${sourcePath}`);
|
|
304
|
+
if (migrated.owner) console.log(` Owner : ${migrated.owner}`);
|
|
305
|
+
if (migrated.domain) console.log(` Domain : ${migrated.domain}`);
|
|
306
|
+
}
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
module.exports.ensureIdentity = ensureIdentity;
|
|
311
|
+
module.exports.ensureWorkspace = ensureWorkspace;
|
|
312
|
+
module.exports.ensureSkill = ensureSkill;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `bitpub skills install` — distribute the BitPub skill into every
|
|
5
|
+
* place the agent on this machine knows to look.
|
|
6
|
+
*
|
|
7
|
+
* Distribution beats hope: most agents won't go discover a skill from a URL
|
|
8
|
+
* unless someone tells them about it. By dropping the skill into the
|
|
9
|
+
* well-known agent directories (Claude Code, Cursor, Codex, etc.) plus
|
|
10
|
+
* AGENTS.md in the current project, we guarantee the agent's first
|
|
11
|
+
* filesystem reconnaissance finds it.
|
|
12
|
+
*
|
|
13
|
+
* Targets (each detected and offered independently):
|
|
14
|
+
* 1. ~/.claude/skills/bitpub/SKILL.md (Claude Code, user-level)
|
|
15
|
+
* 2. ~/.cursor/skills-cursor/bitpub/SKILL.md (Cursor, user-level)
|
|
16
|
+
* 3. ~/.codex/skills/bitpub/SKILL.md (Codex, user-level)
|
|
17
|
+
* 4. <cwd>/.claude/skills/bitpub/SKILL.md (Claude Code, project-level — only if the dir already exists)
|
|
18
|
+
* 5. <cwd>/AGENTS.md (cross-agent convention; section appended/updated)
|
|
19
|
+
*
|
|
20
|
+
* Skill content is fetched from <api_url>/skill.md when an identity is
|
|
21
|
+
* configured, falling back to a bundled copy at cli/skills/bitpub/SKILL.md.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const os = require('os');
|
|
27
|
+
const axios = require('axios');
|
|
28
|
+
const { readConfig, DEFAULT_CLOUD_URL } = require('../config');
|
|
29
|
+
const { findWorkspace } = require('../workspace');
|
|
30
|
+
const { injectAgentsMd } = require('../agents-md');
|
|
31
|
+
|
|
32
|
+
const SKILL_NAME = 'bitpub';
|
|
33
|
+
const SKILL_FILENAME = 'SKILL.md';
|
|
34
|
+
|
|
35
|
+
module.exports = function registerSkills(program) {
|
|
36
|
+
// The top-level `bitpub skills …` command tree is now a deprecated
|
|
37
|
+
// alias — the canonical entry point is `bitpub setup skill …`.
|
|
38
|
+
// We keep both wired so existing scripts and shell aliases keep
|
|
39
|
+
// working; the deprecation hint prints when the legacy path is used.
|
|
40
|
+
const skills = program
|
|
41
|
+
.command('skills', { hidden: true })
|
|
42
|
+
.description('[deprecated] Use `bitpub setup skill` instead');
|
|
43
|
+
|
|
44
|
+
skills
|
|
45
|
+
.command('install')
|
|
46
|
+
.option('--target <name>', 'Install only into a single named target (claude|cursor|codex|project|agents-md)')
|
|
47
|
+
.option('--all', 'Install everywhere, creating directories that do not yet exist')
|
|
48
|
+
.option('--dry-run', 'Print what would be installed without writing files')
|
|
49
|
+
.option('--no-agents-md', 'Skip AGENTS.md injection in the current project')
|
|
50
|
+
.action(async (opts) => {
|
|
51
|
+
console.error('warning: `bitpub skills install` is deprecated. Use `bitpub setup skill install` instead.');
|
|
52
|
+
const { installed, skipped } = await installSkills(opts);
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log(`Done. ${installed} installed/updated, ${skipped} skipped.`);
|
|
55
|
+
if (installed > 0) {
|
|
56
|
+
console.log('Quit Claude Code and reopen it to pick up the new skill — starting a new chat is not enough.');
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
skills
|
|
61
|
+
.command('list')
|
|
62
|
+
.action(() => {
|
|
63
|
+
console.error('warning: `bitpub skills list` is deprecated. Use `bitpub setup skill list` instead.');
|
|
64
|
+
listSkillStatus();
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Print the install status for each known agent skill target. Used by
|
|
70
|
+
* the legacy `bitpub skills list` command and the new `bitpub setup
|
|
71
|
+
* skill list`. No arguments — always considers all targets.
|
|
72
|
+
*/
|
|
73
|
+
function listSkillStatus() {
|
|
74
|
+
const targets = enumerateTargets({ all: true });
|
|
75
|
+
console.log('BitPub skill installation status:\n');
|
|
76
|
+
for (const t of targets) {
|
|
77
|
+
const exists = fs.existsSync(t.targetPath);
|
|
78
|
+
const flag = exists ? '✓' : ' ';
|
|
79
|
+
console.log(` [${flag}] ${t.label.padEnd(28)} ${t.targetPath}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Target enumeration ──────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
function enumerateTargets(opts) {
|
|
86
|
+
const home = os.homedir();
|
|
87
|
+
const cwd = process.cwd();
|
|
88
|
+
|
|
89
|
+
const candidates = [
|
|
90
|
+
{
|
|
91
|
+
name: 'claude',
|
|
92
|
+
label: 'Claude Code (user)',
|
|
93
|
+
parentDir: path.join(home, '.claude', 'skills'),
|
|
94
|
+
targetPath: path.join(home, '.claude', 'skills', SKILL_NAME, SKILL_FILENAME),
|
|
95
|
+
kind: 'skill-file',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'cursor',
|
|
99
|
+
label: 'Cursor (user)',
|
|
100
|
+
parentDir: path.join(home, '.cursor', 'skills-cursor'),
|
|
101
|
+
targetPath: path.join(home, '.cursor', 'skills-cursor', SKILL_NAME, SKILL_FILENAME),
|
|
102
|
+
kind: 'skill-file',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'codex',
|
|
106
|
+
label: 'Codex (user)',
|
|
107
|
+
parentDir: path.join(home, '.codex', 'skills'),
|
|
108
|
+
targetPath: path.join(home, '.codex', 'skills', SKILL_NAME, SKILL_FILENAME),
|
|
109
|
+
kind: 'skill-file',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'project',
|
|
113
|
+
label: 'Project .claude/skills',
|
|
114
|
+
parentDir: path.join(cwd, '.claude', 'skills'),
|
|
115
|
+
targetPath: path.join(cwd, '.claude', 'skills', SKILL_NAME, SKILL_FILENAME),
|
|
116
|
+
kind: 'skill-file',
|
|
117
|
+
// Only auto-install at project level if the dir already exists.
|
|
118
|
+
requiresExistingParent: true,
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
// AGENTS.md injection — only when we're inside a workspace (or in the cwd
|
|
123
|
+
// anyway with --all). It's an append-not-overwrite operation, so safer.
|
|
124
|
+
const ws = findWorkspace(cwd);
|
|
125
|
+
if (opts.agentsMd !== false && (ws || opts.all)) {
|
|
126
|
+
candidates.push({
|
|
127
|
+
name: 'agents-md',
|
|
128
|
+
label: 'AGENTS.md (project)',
|
|
129
|
+
parentDir: cwd,
|
|
130
|
+
targetPath: path.join(cwd, 'AGENTS.md'),
|
|
131
|
+
kind: 'agents-md-section',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return candidates;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Per-target handling ─────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
function handleTarget(target, skillBody, opts) {
|
|
141
|
+
if (target.kind === 'skill-file') {
|
|
142
|
+
return handleSkillFile(target, skillBody, opts);
|
|
143
|
+
}
|
|
144
|
+
if (target.kind === 'agents-md-section') {
|
|
145
|
+
return handleAgentsMd(target, opts);
|
|
146
|
+
}
|
|
147
|
+
return 'skipped';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function handleSkillFile(target, skillBody, opts) {
|
|
151
|
+
const parentExists = fs.existsSync(target.parentDir);
|
|
152
|
+
const shouldCreateParent = opts.all || !target.requiresExistingParent;
|
|
153
|
+
|
|
154
|
+
if (!parentExists && !shouldCreateParent) {
|
|
155
|
+
console.log(` ⊘ ${target.label} — directory not present (${target.parentDir})`);
|
|
156
|
+
return 'skipped';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const targetDir = path.dirname(target.targetPath);
|
|
160
|
+
|
|
161
|
+
if (opts.dryRun) {
|
|
162
|
+
console.log(` → would write ${target.targetPath}`);
|
|
163
|
+
return 'installed';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
167
|
+
const existed = fs.existsSync(target.targetPath);
|
|
168
|
+
fs.writeFileSync(target.targetPath, skillBody);
|
|
169
|
+
console.log(` ${existed ? '↻' : '✓'} ${target.label.padEnd(28)} ${target.targetPath}`);
|
|
170
|
+
return existed ? 'updated' : 'installed';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function handleAgentsMd(target, opts) {
|
|
174
|
+
return injectAgentsMd(target.parentDir, { dryRun: opts.dryRun });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Skill body sourcing ─────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
async function loadSkillBody() {
|
|
180
|
+
const config = readConfig();
|
|
181
|
+
const apiUrl = (config?.api_url || DEFAULT_CLOUD_URL).replace(/\/$/, '');
|
|
182
|
+
|
|
183
|
+
// Try the live canonical copy first so installs always reflect the latest version.
|
|
184
|
+
try {
|
|
185
|
+
const res = await axios.get(`${apiUrl}/skill.md`, { timeout: 10_000 });
|
|
186
|
+
if (typeof res.data === 'string' && res.data.length > 0) {
|
|
187
|
+
return res.data;
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
// fall through to bundled fallback
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const bundled = path.join(__dirname, '..', '..', 'skills', SKILL_NAME, SKILL_FILENAME);
|
|
194
|
+
if (fs.existsSync(bundled)) {
|
|
195
|
+
const raw = fs.readFileSync(bundled, 'utf-8');
|
|
196
|
+
// The bundled copy is the canonical template. Render the API_BASE_URL
|
|
197
|
+
// placeholder against the configured URL so offline installs still produce
|
|
198
|
+
// self-referential URLs.
|
|
199
|
+
return raw.replace(/\{\{API_BASE_URL\}\}/g, apiUrl);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
throw new Error(
|
|
203
|
+
'Could not load skill body from network or bundled copy. ' +
|
|
204
|
+
`Tried ${apiUrl}/skill.md and ${bundled}.`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Pull `version: X.Y.Z` out of the skill's YAML front-matter. Returns null if
|
|
210
|
+
* the body has no front-matter or no version key (older skills, edited files).
|
|
211
|
+
*/
|
|
212
|
+
function parseSkillVersion(body) {
|
|
213
|
+
if (typeof body !== 'string') return null;
|
|
214
|
+
const m = body.match(/^---\n([\s\S]*?)\n---/);
|
|
215
|
+
if (!m) return null;
|
|
216
|
+
const v = m[1].match(/^version:\s*(.+?)\s*$/m);
|
|
217
|
+
return v ? v[1].trim() : null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Refresh skill targets that *already exist*, leaving uninstalled targets
|
|
222
|
+
* alone. Used by `bitpub update` so updating the CLI also updates the skill
|
|
223
|
+
* copies the user has actually opted into. Creating new installs remains the
|
|
224
|
+
* explicit job of `bitpub skills install`.
|
|
225
|
+
*
|
|
226
|
+
* @param {{ dryRun?: boolean }} opts
|
|
227
|
+
* @returns {Promise<{ updates: Array<{label,path,status,oldVersion?,newVersion?}>, error: string|null }>}
|
|
228
|
+
*/
|
|
229
|
+
async function refreshExistingSkills(opts = {}) {
|
|
230
|
+
let skillBody;
|
|
231
|
+
try {
|
|
232
|
+
skillBody = await loadSkillBody();
|
|
233
|
+
} catch (err) {
|
|
234
|
+
return { updates: [], error: err.message };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const newVersion = parseSkillVersion(skillBody);
|
|
238
|
+
const targets = enumerateTargets({ all: true });
|
|
239
|
+
const updates = [];
|
|
240
|
+
|
|
241
|
+
for (const target of targets) {
|
|
242
|
+
if (!fs.existsSync(target.targetPath)) continue;
|
|
243
|
+
|
|
244
|
+
if (target.kind === 'skill-file') {
|
|
245
|
+
const oldBody = fs.readFileSync(target.targetPath, 'utf-8');
|
|
246
|
+
if (oldBody === skillBody) {
|
|
247
|
+
updates.push({
|
|
248
|
+
label: target.label,
|
|
249
|
+
path: target.targetPath,
|
|
250
|
+
status: 'unchanged',
|
|
251
|
+
oldVersion: parseSkillVersion(oldBody),
|
|
252
|
+
newVersion,
|
|
253
|
+
});
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (!opts.dryRun) {
|
|
257
|
+
fs.writeFileSync(target.targetPath, skillBody);
|
|
258
|
+
}
|
|
259
|
+
updates.push({
|
|
260
|
+
label: target.label,
|
|
261
|
+
path: target.targetPath,
|
|
262
|
+
status: 'updated',
|
|
263
|
+
oldVersion: parseSkillVersion(oldBody),
|
|
264
|
+
newVersion,
|
|
265
|
+
});
|
|
266
|
+
} else if (target.kind === 'agents-md-section') {
|
|
267
|
+
if (!opts.dryRun) {
|
|
268
|
+
injectAgentsMd(target.parentDir, { quiet: true });
|
|
269
|
+
}
|
|
270
|
+
updates.push({
|
|
271
|
+
label: target.label,
|
|
272
|
+
path: target.targetPath,
|
|
273
|
+
status: 'updated',
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return { updates, error: null };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function installSkills(opts = {}) {
|
|
282
|
+
const skillBody = await loadSkillBody();
|
|
283
|
+
const targets = enumerateTargets(opts);
|
|
284
|
+
const filtered = opts.target
|
|
285
|
+
? targets.filter(t => t.name === opts.target)
|
|
286
|
+
: targets;
|
|
287
|
+
|
|
288
|
+
let installed = 0;
|
|
289
|
+
let skipped = 0;
|
|
290
|
+
for (const target of filtered) {
|
|
291
|
+
const result = handleTarget(target, skillBody, opts);
|
|
292
|
+
if (result === 'installed' || result === 'updated') installed += 1;
|
|
293
|
+
else skipped += 1;
|
|
294
|
+
}
|
|
295
|
+
return { installed, skipped };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
module.exports.installSkills = installSkills;
|
|
299
|
+
module.exports.loadSkillBody = loadSkillBody;
|
|
300
|
+
module.exports.enumerateTargets = enumerateTargets;
|
|
301
|
+
module.exports.refreshExistingSkills = refreshExistingSkills;
|
|
302
|
+
module.exports.parseSkillVersion = parseSkillVersion;
|
|
303
|
+
module.exports.listSkillStatus = listSkillStatus;
|
|
304
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `bitpub status` — DEPRECATED ALIAS. Use `bitpub list` instead.
|
|
5
|
+
*
|
|
6
|
+
* The new `bitpub list` (with no args) shows the same workspace anchor,
|
|
7
|
+
* cache health, and synced-namespace info, plus the slices saved here.
|
|
8
|
+
* Hidden from --help; kept for shell scripts.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { getSyncedNamespaces, getCacheStats } = require('../db/cache');
|
|
12
|
+
const { readConfig } = require('../config');
|
|
13
|
+
|
|
14
|
+
function relativeTime(isoString) {
|
|
15
|
+
const diffMs = Date.now() - new Date(isoString).getTime();
|
|
16
|
+
const secs = Math.floor(diffMs / 1000);
|
|
17
|
+
if (secs < 60) return 'just now';
|
|
18
|
+
const mins = Math.floor(secs / 60);
|
|
19
|
+
if (mins < 60) return `${mins} min${mins !== 1 ? 's' : ''} ago`;
|
|
20
|
+
const hours = Math.floor(mins / 60);
|
|
21
|
+
if (hours < 24) return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
|
|
22
|
+
const days = Math.floor(hours / 24);
|
|
23
|
+
return `${days} day${days !== 1 ? 's' : ''} ago`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = function registerStatus(program) {
|
|
27
|
+
program
|
|
28
|
+
.command('status', { hidden: true })
|
|
29
|
+
.description('[deprecated] Use `bitpub list` instead')
|
|
30
|
+
.action(() => {
|
|
31
|
+
console.error('warning: `bitpub status` is deprecated. Use `bitpub list` instead.');
|
|
32
|
+
const config = readConfig();
|
|
33
|
+
const stats = getCacheStats();
|
|
34
|
+
const namespaces = getSyncedNamespaces();
|
|
35
|
+
console.log('BitPub — Local Cache Status');
|
|
36
|
+
console.log('─'.repeat(40));
|
|
37
|
+
if (config) {
|
|
38
|
+
if (config.owner) console.log(`Owner : ${config.owner}`);
|
|
39
|
+
if (config.domain) console.log(`Domain : ${config.domain}`);
|
|
40
|
+
if (config.tier) console.log(`Tier : ${config.tier}`);
|
|
41
|
+
console.log(`Backend: ${config.api_url || 'http://localhost:8080'}`);
|
|
42
|
+
} else {
|
|
43
|
+
console.log('Not configured. Run: bitpub setup');
|
|
44
|
+
}
|
|
45
|
+
console.log(`Slices : ${stats?.total_slices ?? 0} cached`);
|
|
46
|
+
if (stats?.latest_sync) {
|
|
47
|
+
console.log(`Updated: ${relativeTime(stats.latest_sync)}`);
|
|
48
|
+
}
|
|
49
|
+
if (namespaces.length === 0) {
|
|
50
|
+
console.log('\nNo namespaces synced yet.');
|
|
51
|
+
console.log('Run: bitpub sync "bitpub://group:YOUR_DOMAIN/**"');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
console.log('\nSynced Namespaces:');
|
|
55
|
+
for (const ns of namespaces) {
|
|
56
|
+
const ago = relativeTime(ns.last_synced);
|
|
57
|
+
const count = `${ns.slice_count} slice${ns.slice_count !== 1 ? 's' : ''}`;
|
|
58
|
+
console.log(` ${ns.pattern}`);
|
|
59
|
+
console.log(` Last synced: ${ago} | ${count}`);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
};
|