@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/src/aliases.js ADDED
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Aliases — short shortcuts for fully-qualified addresses that don't
5
+ * correspond to a folder on disk. Useful for stable logical addresses an
6
+ * agent uses across cwd's, sessions, and machines: task queues, inboxes,
7
+ * long-running memory areas, frequently-read group namespaces, etc.
8
+ *
9
+ * Stored as a flat key→address map at ~/.bitpub/aliases.json. Used
10
+ * everywhere a command accepts a name, scope, or pattern: a leading `@`
11
+ * triggers expansion.
12
+ *
13
+ * bitpub alias set inbox bitpub://private:agent_xyz/Queues/inbox/
14
+ * bitpub save @inbox/task-001 "review billing parser"
15
+ * bitpub list @inbox
16
+ * bitpub watch --address @inbox/** (after expansion in CLI)
17
+ *
18
+ * Aliases compose with workspaces rather than replacing them: a workspace
19
+ * gives folder-anchored ergonomics for a project; aliases give cross-cwd
20
+ * shortcuts to fixed addresses.
21
+ */
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const os = require('os');
26
+
27
+ const ALIASES_FILE = path.join(os.homedir(), '.bitpub', 'aliases.json');
28
+ const VALID_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
29
+
30
+ function readAliases() {
31
+ try {
32
+ const raw = fs.readFileSync(ALIASES_FILE, 'utf-8');
33
+ const parsed = JSON.parse(raw);
34
+ return parsed && typeof parsed === 'object' ? parsed : {};
35
+ } catch {
36
+ return {};
37
+ }
38
+ }
39
+
40
+ function writeAliases(map) {
41
+ const dir = path.dirname(ALIASES_FILE);
42
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
43
+ fs.writeFileSync(ALIASES_FILE, JSON.stringify(map, null, 2), { mode: 0o600 });
44
+ }
45
+
46
+ function setAlias(name, address) {
47
+ if (!VALID_NAME.test(name)) {
48
+ throw new Error(
49
+ `Invalid alias name "${name}". Use alphanumeric with - or _ (e.g. "inbox", "team-runbooks").`
50
+ );
51
+ }
52
+ if (typeof address !== 'string' || !address.startsWith('bitpub://')) {
53
+ throw new Error('Alias value must be a fully qualified address (bitpub://...).');
54
+ }
55
+ // Normalize to a trailing slash so `@name/x` and `@name` both work cleanly.
56
+ const normalized = address.endsWith('/') || address.endsWith('*') ? address : address + '/';
57
+ const map = readAliases();
58
+ map[name] = normalized;
59
+ writeAliases(map);
60
+ return normalized;
61
+ }
62
+
63
+ function removeAlias(name) {
64
+ const map = readAliases();
65
+ if (!(name in map)) return false;
66
+ delete map[name];
67
+ writeAliases(map);
68
+ return true;
69
+ }
70
+
71
+ function isAliasRef(input) {
72
+ return typeof input === 'string' && input.length > 1 && input[0] === '@';
73
+ }
74
+
75
+ /**
76
+ * Expand `@name` or `@name/<rest>` against the aliases map. Throws if the
77
+ * named alias is not defined. Caller is responsible for using `isAliasRef`
78
+ * first if pass-through behavior is desired.
79
+ */
80
+ function expandAlias(input) {
81
+ if (!isAliasRef(input)) {
82
+ throw new Error(`Not an alias reference: ${input}`);
83
+ }
84
+ const slash = input.indexOf('/');
85
+ const name = slash === -1 ? input.slice(1) : input.slice(1, slash);
86
+ const rest = slash === -1 ? '' : input.slice(slash + 1);
87
+
88
+ const map = readAliases();
89
+ if (!(name in map)) {
90
+ throw new Error(
91
+ `Unknown alias "@${name}". Define it with: bitpub alias set ${name} <bitpub://...> ` +
92
+ '(or list existing aliases: bitpub alias list)'
93
+ );
94
+ }
95
+ return map[name] + rest;
96
+ }
97
+
98
+ /**
99
+ * Convenience: pass-through unless the input is an alias reference.
100
+ * Useful at command entry points where the caller may pass a fully-qualified
101
+ * HCU, a short name, or an `@alias`.
102
+ */
103
+ function maybeExpand(input) {
104
+ return isAliasRef(input) ? expandAlias(input) : input;
105
+ }
106
+
107
+ module.exports = {
108
+ ALIASES_FILE,
109
+ readAliases,
110
+ writeAliases,
111
+ setAlias,
112
+ removeAlias,
113
+ isAliasRef,
114
+ expandAlias,
115
+ maybeExpand,
116
+ };
package/src/api.js ADDED
@@ -0,0 +1,177 @@
1
+ 'use strict';
2
+
3
+ const axios = require('axios');
4
+
5
+ /**
6
+ * Create an API client bound to a specific config.
7
+ * All methods throw on non-2xx responses (axios default behavior).
8
+ */
9
+ /**
10
+ * Pick the right API key for a given HCU.
11
+ * Private namespaces use the personal api_key provisioned by `bitpub init`.
12
+ * Group/public namespaces use group_key set by `bitpub auth login`, falling
13
+ * back to api_key so single-key setups keep working.
14
+ */
15
+ function keyForHcu(config, hcu) {
16
+ if (typeof hcu === 'string' && hcu.startsWith('bitpub://private:')) {
17
+ return config.api_key;
18
+ }
19
+ return config.group_key || config.api_key;
20
+ }
21
+
22
+ function createApiClient(config) {
23
+ const baseURL = (config.api_url || 'http://localhost:8080').replace(/\/$/, '');
24
+
25
+ const http = axios.create({
26
+ baseURL,
27
+ timeout: 30_000,
28
+ });
29
+
30
+ // Surface server-side error messages cleanly, preserving response for callers
31
+ http.interceptors.response.use(
32
+ res => res,
33
+ err => {
34
+ const msg = err.response?.data?.error || err.message;
35
+ const status = err.response?.status;
36
+ const wrapped = new Error(status ? `[${status}] ${msg}` : msg);
37
+ wrapped.status = status;
38
+ wrapped.response = err.response;
39
+ return Promise.reject(wrapped);
40
+ }
41
+ );
42
+
43
+ return {
44
+ /**
45
+ * Pull context slices from the remote ledger.
46
+ * @param {string} hcu
47
+ * @param {number} [limit]
48
+ * @param {object} [opts]
49
+ * @param {boolean} [opts.includeDeleted] surface tombstoned slices
50
+ * @param {boolean} [opts.mine] filter to slices written by this key
51
+ * @returns {Promise<Array>} array of slice DTOs
52
+ */
53
+ async pull(hcu, limit = 50, opts = {}) {
54
+ const params = { hcu, limit };
55
+ if (opts.includeDeleted) params.include_deleted = 'true';
56
+ if (opts.mine) params.mine = 'true';
57
+ const res = await http.get('/v1/context/pull', {
58
+ params,
59
+ headers: { 'x-api-key': keyForHcu(config, hcu) },
60
+ });
61
+ return res.data;
62
+ },
63
+
64
+ /**
65
+ * Push a context slice to the remote ledger.
66
+ * @param {object} body Full ContextSlice payload
67
+ * @param {object} [opts]
68
+ * @param {boolean} [opts.append] append instead of overwrite
69
+ * @param {number} [opts.expectVersion] reject with 409 on version mismatch
70
+ * @param {boolean} [opts.force] un-tombstone a deleted slice with new content
71
+ * @returns {Promise<{success: boolean, slice: object}>}
72
+ */
73
+ async push(body, opts = {}) {
74
+ // Back-compat: older callers passed (body, append, expectVersion) as
75
+ // positional args. Detect that shape and translate to the opts object.
76
+ if (typeof opts === 'boolean' || typeof opts === 'number') {
77
+ opts = { append: !!opts, expectVersion: arguments[2] };
78
+ }
79
+ const { append, expectVersion, force } = opts;
80
+ const params = {};
81
+ if (append) params.append = 'true';
82
+ if (force) params.force = 'true';
83
+ if (expectVersion != null) params.expect_version = String(expectVersion);
84
+ const res = await http.post('/v1/context/push', body, {
85
+ params,
86
+ headers: { 'x-api-key': keyForHcu(config, body?.hcu) },
87
+ });
88
+ return res.data;
89
+ },
90
+
91
+ /**
92
+ * List immediate children of an HCU path.
93
+ * @param {string} hcu
94
+ * @param {object} [opts]
95
+ * @param {boolean} [opts.includeDeleted] return tombstone provenance
96
+ * @returns {Promise<{children: string[] | Array<{hcu: string, deleted_at: string|null, deleted_by: string|null}>}>}
97
+ */
98
+ async list(hcu, opts = {}) {
99
+ const params = { hcu };
100
+ if (opts.includeDeleted) params.include_deleted = 'true';
101
+ const res = await http.get('/v1/context/list', {
102
+ params,
103
+ headers: { 'x-api-key': keyForHcu(config, hcu) },
104
+ });
105
+ return res.data;
106
+ },
107
+
108
+ /**
109
+ * Soft-delete an exact context slice. Bumps version (active(N) →
110
+ * deleted(N+1)). The server preserves the row's payload so a no-content
111
+ * restore can undelete it. Re-dropping an already-tombstoned slice is
112
+ * idempotent.
113
+ */
114
+ async drop(hcu, opts = {}) {
115
+ const params = { hcu };
116
+ if (opts.expectVersion != null) params.expect_version = String(opts.expectVersion);
117
+ const res = await http.delete('/v1/context/drop', {
118
+ params,
119
+ headers: { 'x-api-key': keyForHcu(config, hcu) },
120
+ });
121
+ return res.data;
122
+ },
123
+
124
+ /**
125
+ * Restore a tombstoned slice in place using the server's preserved
126
+ * payload. Bumps version (deleted(N) → active(N+1)). Returns 409 if
127
+ * the slice is already active; 404 if it never existed.
128
+ */
129
+ async restore(hcu, opts = {}) {
130
+ const params = { hcu };
131
+ if (opts.expectVersion != null) params.expect_version = String(opts.expectVersion);
132
+ const res = await http.post('/v1/context/restore', null, {
133
+ params,
134
+ headers: { 'x-api-key': keyForHcu(config, hcu) },
135
+ });
136
+ return res.data;
137
+ },
138
+
139
+ /**
140
+ * Open an SSE heartbeat stream and invoke onSync(evt) for each sync event.
141
+ * Each `evt` is a `{ hcu, deleted? }` object — the optional `deleted: true`
142
+ * hint tells callers the slice was tombstoned (so they can evict from
143
+ * any local cache without a refetch).
144
+ *
145
+ * For backward compatibility with any caller that expected just an HCU
146
+ * string, the `evt` argument is documented as the new shape and callers
147
+ * should detect `evt.deleted` to short-circuit refetch.
148
+ *
149
+ * Returns a cleanup function that closes the connection.
150
+ */
151
+ watch(hcuPattern, onSync, onError) {
152
+ const EventSource = require('eventsource');
153
+ const url = `${baseURL}/v1/context/heartbeat?hcu_pattern=${encodeURIComponent(hcuPattern)}`;
154
+
155
+ const es = new EventSource(url, {
156
+ headers: { 'x-api-key': keyForHcu(config, hcuPattern) },
157
+ });
158
+
159
+ es.addEventListener('sync', (event) => {
160
+ try {
161
+ const data = JSON.parse(event.data);
162
+ onSync(data);
163
+ } catch {
164
+ // Malformed event — ignore
165
+ }
166
+ });
167
+
168
+ es.onerror = (err) => {
169
+ if (onError) onError(err);
170
+ };
171
+
172
+ return () => es.close();
173
+ },
174
+ };
175
+ }
176
+
177
+ module.exports = { createApiClient };
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `bitpub alias` — manage shortcuts for fully-qualified addresses.
5
+ *
6
+ * bitpub alias set inbox bitpub://private:agent_xyz/Queues/inbox/
7
+ * bitpub alias list
8
+ * bitpub alias show inbox
9
+ * bitpub alias rm inbox
10
+ *
11
+ * Aliases are referenced everywhere a name/scope/pattern is accepted by
12
+ * prefixing with `@`:
13
+ *
14
+ * bitpub save @inbox/task-001 "..."
15
+ * bitpub list @inbox
16
+ * bitpub grep "todo" --scope @inbox
17
+ */
18
+
19
+ const { readAliases, setAlias, removeAlias, ALIASES_FILE } = require('../aliases');
20
+
21
+ module.exports = function registerAlias(program) {
22
+ const alias = program
23
+ .command('alias')
24
+ .description('Manage @-aliases for fully-qualified addresses (queues, inboxes, fixed locations)');
25
+
26
+ alias
27
+ .command('set <name> <address>')
28
+ .description('Create or update an alias (e.g. `alias set inbox bitpub://private:.../Queues/inbox/`)')
29
+ .action((name, address) => {
30
+ try {
31
+ const normalized = setAlias(name, address);
32
+ console.log(`✓ @${name} → ${normalized}`);
33
+ } catch (err) {
34
+ console.error(err.message);
35
+ process.exit(1);
36
+ }
37
+ });
38
+
39
+ alias
40
+ .command('list')
41
+ .description('List all defined aliases')
42
+ .action(() => {
43
+ const map = readAliases();
44
+ const keys = Object.keys(map).sort();
45
+ if (keys.length === 0) {
46
+ console.log('No aliases defined.');
47
+ console.log('');
48
+ console.log('Examples:');
49
+ console.log(' bitpub alias set inbox bitpub://private:<owner>/Queues/inbox/');
50
+ console.log(' bitpub alias set memory bitpub://private:<owner>/Memory/');
51
+ return;
52
+ }
53
+ console.log(`File: ${ALIASES_FILE}\n`);
54
+ const width = Math.max(...keys.map(k => k.length));
55
+ for (const k of keys) {
56
+ console.log(` @${k.padEnd(width)} ${map[k]}`);
57
+ }
58
+ });
59
+
60
+ alias
61
+ .command('show <name>')
62
+ .description('Print the fully-qualified address that an alias resolves to')
63
+ .action((name) => {
64
+ const map = readAliases();
65
+ if (!(name in map)) {
66
+ console.error(`Unknown alias: @${name}`);
67
+ process.exit(1);
68
+ }
69
+ console.log(map[name]);
70
+ });
71
+
72
+ alias
73
+ .command('rm <name>')
74
+ .description('Remove an alias')
75
+ .action((name) => {
76
+ const ok = removeAlias(name);
77
+ console.log(ok ? `✓ Removed @${name}` : `(@${name} not found)`);
78
+ });
79
+ };
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `bitpub auth login` — DEPRECATED ALIAS. Use `bitpub setup team` instead.
5
+ *
6
+ * Persists group credentials (key + domain + url) without touching the
7
+ * private identity. Forwards to the same writeConfig path setup uses;
8
+ * hidden from --help.
9
+ */
10
+
11
+ const { readConfig, writeConfig } = require('../config');
12
+ const { initCache } = require('../db/cache');
13
+ const { createApiClient } = require('../api');
14
+
15
+ module.exports = function registerAuth(program) {
16
+ const auth = program
17
+ .command('auth', { hidden: true })
18
+ .description('[deprecated] Use `bitpub setup team` instead');
19
+
20
+ auth
21
+ .command('login')
22
+ .description('[deprecated] Use `bitpub setup team` instead')
23
+ .requiredOption('--key <string>', 'Group API key provided by your team admin')
24
+ .requiredOption('--domain <string>', 'Your organization domain (e.g. acme.com)')
25
+ .option('--url <string>', 'Backend URL', 'http://localhost:8080')
26
+ .option('--verify', 'Ping the backend to verify the key before saving')
27
+ .action(async ({ key, domain, url, verify }) => {
28
+ console.error('warning: `bitpub auth login` is deprecated. Use `bitpub setup team` instead.');
29
+ if (verify) {
30
+ try {
31
+ const api = createApiClient({ group_key: key, api_url: url });
32
+ await api.pull(`bitpub://group:${domain}/**`, 1);
33
+ console.log('✓ Key verified against backend');
34
+ } catch (err) {
35
+ console.error(`Key verification failed: ${err.message}`);
36
+ process.exit(1);
37
+ }
38
+ }
39
+ const existing = readConfig() || {};
40
+ writeConfig({ ...existing, group_key: key, domain, api_url: url });
41
+ initCache();
42
+ console.log(`Authenticated.`);
43
+ console.log(` Domain : ${domain}`);
44
+ console.log(` Backend: ${url}`);
45
+ if (existing.owner) {
46
+ console.log(` Private: agent_${existing.owner} (preserved)`);
47
+ }
48
+ console.log(`\nNext: bitpub sync "bitpub://group:${domain}/**"`);
49
+ });
50
+ };
@@ -0,0 +1,196 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `bitpub browser` — open the visual context explorer in your browser.
5
+ *
6
+ * Formerly `bitpub console`. The old command name is still registered as a
7
+ * hidden alias so existing scripts keep working; new docs and prompts use
8
+ * `browser` because it matches what the user actually sees (a browser tab
9
+ * with a UI).
10
+ *
11
+ * The serve loop is also exported as `startBrowserServer()` so other
12
+ * commands (notably `bitpub welcome --serve`) can launch the same UI without
13
+ * duplicating the http plumbing.
14
+ */
15
+
16
+ const http = require('http');
17
+ const path = require('path');
18
+ const fs = require('fs');
19
+ const os = require('os');
20
+ const { exec } = require('child_process');
21
+ const { readConfig, BITPUB_DIR } = require('../config');
22
+ const { getSyncedNamespaces, initCache } = require('../db/cache');
23
+ const { isPrivateHcu, decrypt, isEncrypted } = require('../crypto');
24
+
25
+ const Database = require('better-sqlite3');
26
+ const DB_PATH = path.join(os.homedir(), '.bitpub', 'cache.db');
27
+
28
+ function getAllSlices() {
29
+ if (!fs.existsSync(DB_PATH)) return [];
30
+ const db = new Database(DB_PATH);
31
+ const rows = db.prepare('SELECT * FROM local_slices ORDER BY last_synced DESC').all();
32
+ db.close();
33
+ return rows;
34
+ }
35
+
36
+ function decryptSlice(slice, apiKey) {
37
+ if (!isPrivateHcu(slice.hcu) || !apiKey) return slice;
38
+ try {
39
+ const payload = typeof slice.payload === 'string' ? JSON.parse(slice.payload) : slice.payload;
40
+ if (isEncrypted(payload.content)) {
41
+ payload.content = decrypt(payload.content, apiKey);
42
+ return { ...slice, payload: typeof slice.payload === 'string' ? JSON.stringify(payload) : payload };
43
+ }
44
+ } catch { /* decryption failure — return as-is */ }
45
+ return slice;
46
+ }
47
+
48
+ function resolveBrowserHtmlPath() {
49
+ // Order matters. We want production installs to always serve the HTML
50
+ // that ships with the *current* CLI version, not whatever got cached in
51
+ // ~/.bitpub/ from a one-time tarball install months ago.
52
+ //
53
+ // 1. cli/static/console.html — populated by `prepack` from the
54
+ // workspace `backend/static/console.html` and shipped inside the
55
+ // CLI tarball. This is the canonical location at runtime.
56
+ // 2. backend/static/console.html — only resolves when running from a
57
+ // checked-out workspace (e.g., `node cli/bin/bitpub.js browser`
58
+ // during development). Convenient but not the real install path.
59
+ // 3. ~/.bitpub/console.html — last-resort cache, mainly for the
60
+ // original install instructions that fetched a single file via curl.
61
+ // We never write here ourselves; it exists only when a user
62
+ // manually populated it.
63
+ //
64
+ // Filename is still console.html on disk — keeping the filename stable
65
+ // avoids churning the prepack step and lets old `~/.bitpub/console.html`
66
+ // caches keep working. The user-facing command is `browser`.
67
+ const candidates = [
68
+ path.join(__dirname, '../../static/console.html'),
69
+ path.join(__dirname, '../../../backend/static/console.html'),
70
+ path.join(BITPUB_DIR, 'console.html'),
71
+ ];
72
+ for (const p of candidates) {
73
+ if (fs.existsSync(p)) return p;
74
+ }
75
+ return null;
76
+ }
77
+
78
+ function openInBrowser(url) {
79
+ const cmd = process.platform === 'darwin' ? 'open' :
80
+ process.platform === 'win32' ? 'start' : 'xdg-open';
81
+ exec(`${cmd} ${url}`);
82
+ }
83
+
84
+ /**
85
+ * Start the browser server and (optionally) open a tab. Returns a Promise
86
+ * that resolves with `{ server, url }` once listening, or rejects on error.
87
+ *
88
+ * @param {object} [opts]
89
+ * @param {number} [opts.port=4141] Port to bind. Falls back to next free port.
90
+ * @param {boolean} [opts.open=true] Whether to launch the OS browser.
91
+ * @param {string} [opts.path='/'] Initial path/query (e.g. '/?welcome=1').
92
+ * @param {boolean} [opts.quiet=false] Suppress the welcome banner on stdout.
93
+ */
94
+ function startBrowserServer(opts = {}) {
95
+ const port = parseInt(opts.port, 10) || 4141;
96
+ const shouldOpen = opts.open !== false;
97
+ const initialPath = opts.path || '/';
98
+ const quiet = !!opts.quiet;
99
+
100
+ return new Promise((resolve, reject) => {
101
+ initCache();
102
+ const cfg = readConfig();
103
+
104
+ const htmlPath = resolveBrowserHtmlPath();
105
+ if (!htmlPath) {
106
+ const err = new Error(
107
+ `Browser UI not found. If you installed from a tarball, run:\n` +
108
+ ` curl -sS "${(cfg && cfg.api_url) || 'https://<your-server>'}/console" -o ~/.bitpub/console.html`
109
+ );
110
+ return reject(err);
111
+ }
112
+
113
+ const htmlTemplate = fs.readFileSync(htmlPath, 'utf-8');
114
+
115
+ const server = http.createServer((req, res) => {
116
+ const url = new URL(req.url, `http://localhost:${port}`);
117
+
118
+ if (url.pathname === '/api/data') {
119
+ res.setHeader('Content-Type', 'application/json');
120
+ const slices = getAllSlices().map(s => {
121
+ if (cfg && cfg.api_key) return decryptSlice(s, cfg.api_key);
122
+ return s;
123
+ });
124
+ res.end(JSON.stringify({
125
+ mode: 'local',
126
+ domain: (cfg && cfg.domain) || '',
127
+ slices,
128
+ synced_namespaces: getSyncedNamespaces(),
129
+ }));
130
+ return;
131
+ }
132
+
133
+ if (url.pathname === '/' || url.pathname === '/index.html') {
134
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
135
+ res.end(htmlTemplate);
136
+ return;
137
+ }
138
+
139
+ res.statusCode = 404;
140
+ res.end('Not found');
141
+ });
142
+
143
+ server.listen(port, () => {
144
+ const baseUrl = `http://localhost:${port}`;
145
+ const fullUrl = baseUrl + (initialPath.startsWith('/') ? initialPath : '/' + initialPath);
146
+ if (!quiet) {
147
+ console.log(`\n BitPub Browser\n`);
148
+ console.log(` Local: ${baseUrl}`);
149
+ if (cfg && cfg.domain) console.log(` Domain: ${cfg.domain}`);
150
+ console.log(`\n Press Ctrl+C to stop.\n`);
151
+ }
152
+ if (shouldOpen) openInBrowser(fullUrl);
153
+ resolve({ server, url: fullUrl });
154
+ });
155
+
156
+ server.on('error', (err) => {
157
+ if (err.code === 'EADDRINUSE') {
158
+ err.message = `Port ${port} is in use. Try: bitpub browser --port ${port + 1}`;
159
+ }
160
+ reject(err);
161
+ });
162
+ });
163
+ }
164
+
165
+ function registerCommand(program, name, opts = {}) {
166
+ const cmd = program
167
+ .command(name)
168
+ .description(opts.description || 'Open the visual context explorer in your browser')
169
+ .option('-p, --port <port>', 'Port to serve on', '4141')
170
+ .option('--no-open', 'Do not auto-open browser')
171
+ .action(async (cliOpts) => {
172
+ if (opts.deprecated) {
173
+ console.error(`(note: 'bitpub ${name}' is deprecated; use 'bitpub browser')`);
174
+ }
175
+ try {
176
+ await startBrowserServer({
177
+ port: cliOpts.port,
178
+ open: cliOpts.open !== false,
179
+ });
180
+ } catch (err) {
181
+ console.error(err.message);
182
+ process.exit(1);
183
+ }
184
+ });
185
+ return cmd;
186
+ }
187
+
188
+ module.exports = function (program) {
189
+ registerCommand(program, 'browser');
190
+ // Backward-compat: the command was previously called `console`. Keep it
191
+ // working so existing scripts and muscle-memory don't break, but log a
192
+ // one-line deprecation hint when used.
193
+ registerCommand(program, 'console', { deprecated: true });
194
+ };
195
+
196
+ module.exports.startBrowserServer = startBrowserServer;