@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.
@@ -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
+ };