@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,109 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `bitpub catch-up` — DEPRECATED ALIAS. Use `bitpub list --sync` instead.
5
+ *
6
+ * The new `list --sync` does what catch-up did: pull the active workspace
7
+ * (and any group_link) from the cloud, then surface recent slices with
8
+ * the workspace anchor, cache freshness, and previews. Hidden from --help.
9
+ */
10
+
11
+ const { requireConfig } = require('../config');
12
+ const { createApiClient } = require('../api');
13
+ const { querySlices, upsertSlice } = require('../db/cache');
14
+ const { decryptSlices } = require('../crypto');
15
+ const { activeNamespace } = require('../workspace');
16
+
17
+ function safeParse(s) {
18
+ try { return JSON.parse(s); } catch { return null; }
19
+ }
20
+
21
+ function normalizePattern(hcu) {
22
+ if (hcu.endsWith('**') || hcu.endsWith('*')) return hcu;
23
+ return hcu.replace(/\/?$/, '/**');
24
+ }
25
+
26
+ function normalizeBase(hcu) {
27
+ return hcu.replace(/\/?$/, '/');
28
+ }
29
+
30
+ function sortByRecency(rows) {
31
+ return rows.slice().sort((a, b) => String(b.last_synced).localeCompare(String(a.last_synced)));
32
+ }
33
+
34
+ async function sync(api, pattern, config) {
35
+ try {
36
+ const slices = await api.pull(pattern, 200);
37
+ decryptSlices(slices, config.api_key);
38
+ slices.forEach(upsertSlice);
39
+ } catch (err) {
40
+ console.error(`(sync ${pattern} failed: ${err.message})`);
41
+ }
42
+ }
43
+
44
+ function printSection(title, rows, base, limit) {
45
+ console.log(title);
46
+ if (rows.length === 0) {
47
+ console.log(' (empty)');
48
+ console.log('');
49
+ return;
50
+ }
51
+ for (const s of rows.slice(0, limit)) {
52
+ const meta = safeParse(s.metadata) || {};
53
+ const payload = safeParse(s.payload) || {};
54
+ const shortName = s.hcu.startsWith(base) ? s.hcu.slice(base.length) : s.hcu;
55
+ const preview = String(payload.content || '').replace(/\s+/g, ' ').slice(0, 80);
56
+ const when = String(meta.timestamp || s.last_synced || '').slice(0, 16).replace('T', ' ');
57
+ console.log(` [${when}] ${shortName || '(root)'}`);
58
+ if (preview) console.log(` ${preview}`);
59
+ }
60
+ if (rows.length > limit) {
61
+ console.log(` … ${rows.length - limit} more`);
62
+ }
63
+ console.log('');
64
+ }
65
+
66
+ module.exports = function registerCatchup(program) {
67
+ program
68
+ .command('catch-up', { hidden: true })
69
+ .description('[deprecated] Use `bitpub list --sync` instead')
70
+ .option('-n, --limit <number>', 'Slices to surface per scope', '10')
71
+ .option('--no-sync', 'Skip the network sync (cache only)')
72
+ .action(async (opts) => {
73
+ console.error('warning: `bitpub catch-up` is deprecated. Use `bitpub list --sync` instead.');
74
+ const config = requireConfig();
75
+ const active = activeNamespace(config);
76
+ if (!active) {
77
+ console.error('No workspace and no owner configured. Run `bitpub setup` first.');
78
+ process.exit(1);
79
+ }
80
+ const limit = parseInt(opts.limit, 10) || 10;
81
+ const api = createApiClient(config);
82
+ const groupLink = active.workspace?.marker.group_link || null;
83
+ if (opts.sync !== false) {
84
+ await sync(api, active.namespace + '**', config);
85
+ if (groupLink) await sync(api, normalizePattern(groupLink), config);
86
+ }
87
+ console.log('═══ Catch-up ═══');
88
+ if (active.workspace) {
89
+ console.log(`Workspace : ${active.workspace.marker.label || active.workspace.marker.id}`);
90
+ console.log(`Root : ${active.workspace.root}`);
91
+ } else {
92
+ console.log('Workspace : (none — using per-day session default)');
93
+ }
94
+ console.log(`Namespace : ${active.namespace}`);
95
+ if (groupLink) console.log(`Group link : ${groupLink}`);
96
+ console.log('');
97
+ const privateSlices = sortByRecency(querySlices(active.namespace + '**'));
98
+ printSection('Your recent work (private)', privateSlices, active.namespace, limit);
99
+ if (groupLink) {
100
+ const groupSlices = sortByRecency(querySlices(normalizePattern(groupLink)));
101
+ const groupBase = normalizeBase(groupLink);
102
+ printSection('Team context (group, read-mostly)', groupSlices, groupBase, limit);
103
+ }
104
+ if (privateSlices.length === 0 && !groupLink) {
105
+ console.log('No prior work here. Save your first slice:');
106
+ console.log(' bitpub save <name> "..."');
107
+ }
108
+ });
109
+ };
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `bitpub delete <name-or-address>` — remove a slice.
5
+ *
6
+ * Subsumes the previous `drop`, `trash`, and `restore` verbs into one
7
+ * verb that does the right thing by default, with friendlier flags for
8
+ * the recovery cases:
9
+ *
10
+ * bitpub delete notes soft-delete (recoverable)
11
+ * bitpub delete notes --undo recover from trash / restore on server
12
+ * bitpub delete --list show what's in the local trash
13
+ * bitpub delete --empty-trash clear the local undo buffer
14
+ *
15
+ * Scope-aware cache behavior preserved from `drop`:
16
+ * - private: row leaves local_slices, lands in local_trash (offline undo)
17
+ * - group: row is evicted; recovery goes through the server (--undo)
18
+ *
19
+ * Wildcards are rejected — a delete is an exact-address operation. The
20
+ * input grammar is the same as load/save: a project short name, an
21
+ * `@alias`, or a full `bitpub://` URL.
22
+ */
23
+
24
+ const { requireConfig } = require('../config');
25
+ const { createApiClient } = require('../api');
26
+ const {
27
+ upsertSlice,
28
+ evictSlice,
29
+ trashSlice,
30
+ getTrashEntry,
31
+ removeFromTrash,
32
+ listTrash,
33
+ emptyTrash: emptyTrashRows,
34
+ purgeExpiredTrash,
35
+ TRASH_TTL_DAYS,
36
+ } = require('../db/cache');
37
+ const { decryptSlices, isPrivateHcu } = require('../crypto');
38
+ const { activeNamespace, resolveHcu } = require('../workspace');
39
+
40
+ function safeParse(s) {
41
+ try { return JSON.parse(s); } catch { return null; }
42
+ }
43
+
44
+ async function runDelete(name, opts) {
45
+ if (!name) {
46
+ console.error('Provide an address or short name to delete.');
47
+ process.exit(1);
48
+ }
49
+ if (name.includes('*')) {
50
+ console.error('Wildcards are not permitted. Delete one slice at a time.');
51
+ process.exit(1);
52
+ }
53
+
54
+ const config = requireConfig();
55
+ let address;
56
+ try { ({ hcu: address } = resolveHcu(name, config)); }
57
+ catch (err) { console.error(err.message); process.exit(1); }
58
+
59
+ if (opts.undo) return runUndo(address, config);
60
+
61
+ const api = createApiClient(config);
62
+ try {
63
+ const result = await api.drop(address, { expectVersion: opts.expectVersion });
64
+ const slice = result.slice;
65
+ if (slice) decryptSlices([slice], config.api_key);
66
+
67
+ if (isPrivateHcu(address) && slice) {
68
+ trashSlice(slice, slice.deleted_at);
69
+ console.log(`✓ Deleted → ${address} (v${slice.metadata.version})`);
70
+ console.log(` Undo with: bitpub delete "${name}" --undo (within ${TRASH_TTL_DAYS} days)`);
71
+ } else {
72
+ evictSlice(address);
73
+ console.log(`✓ Deleted → ${address} (v${slice?.metadata?.version ?? '?'})`);
74
+ console.log(` Undo with: bitpub delete "${name}" --undo`);
75
+ }
76
+ } catch (err) {
77
+ if (err.response?.status === 409) {
78
+ const data = err.response.data;
79
+ console.error(
80
+ `Version conflict: expected v${data.expected_version} but server has v${data.actual_version}.`
81
+ );
82
+ } else if (err.response?.status === 404) {
83
+ console.error(`Not found: ${address}`);
84
+ } else {
85
+ console.error(`Delete failed: ${err.message}`);
86
+ }
87
+ process.exit(1);
88
+ }
89
+ }
90
+
91
+ async function runUndo(address, config) {
92
+ const api = createApiClient(config);
93
+
94
+ // Private undos use the recorded trash version as a CAS guard so we
95
+ // don't silently clobber a concurrent restore. Group undos go straight
96
+ // through the server with no version check (no local trash entry).
97
+ const entry = getTrashEntry(address);
98
+
99
+ try {
100
+ const result = await api.restore(
101
+ address,
102
+ entry ? { expectVersion: entry.version } : {}
103
+ );
104
+ const slice = result.slice;
105
+ if (slice) decryptSlices([slice], config.api_key);
106
+ if (slice) upsertSlice(slice);
107
+ if (entry) removeFromTrash(address);
108
+
109
+ console.log(`✓ Undone → ${address} (v${slice?.metadata?.version ?? '?'})`);
110
+ } catch (err) {
111
+ const status = err.response?.status;
112
+ const data = err.response?.data;
113
+ if (status === 409 && data?.error?.match(/not in deleted state/i)) {
114
+ console.error(`${address} is already active. Nothing to undo.`);
115
+ if (entry) removeFromTrash(address);
116
+ } else if (status === 409 && entry) {
117
+ console.error(
118
+ `Version drift: trash entry has v${entry.version} but server is at v${data?.actual_version}.\n` +
119
+ `Run: bitpub sync ${address} to see the current state, then decide.`
120
+ );
121
+ } else if (status === 404) {
122
+ console.error(`${address} doesn't exist on the server.`);
123
+ if (entry) removeFromTrash(address);
124
+ } else {
125
+ console.error(`Undo failed: ${err.message}`);
126
+ }
127
+ process.exit(1);
128
+ }
129
+ }
130
+
131
+ function runListTrash(config, opts) {
132
+ const purged = purgeExpiredTrash();
133
+ if (purged > 0) {
134
+ console.log(`(purged ${purged} entry(ies) past the ${TRASH_TTL_DAYS}-day TTL)`);
135
+ }
136
+
137
+ let prefix = null;
138
+ if (!opts.all) {
139
+ const active = activeNamespace(config);
140
+ if (active) prefix = active.namespace;
141
+ }
142
+
143
+ const rows = listTrash(prefix);
144
+ if (rows.length === 0) {
145
+ console.log(prefix ? `(no deleted slices in ${prefix})` : '(nothing in trash)');
146
+ return;
147
+ }
148
+
149
+ console.log(`Recently deleted (${rows.length}, kept for ${TRASH_TTL_DAYS} days):`);
150
+ for (const row of rows) {
151
+ const meta = safeParse(row.metadata) || {};
152
+ const shortName = prefix ? row.hcu.replace(prefix, '') : row.hcu;
153
+ const ver = meta.version != null ? `v${meta.version}` : ' ';
154
+ console.log(` ${ver.padEnd(5)} ${row.deleted_at} ${shortName}`);
155
+ }
156
+ console.log('');
157
+ console.log('Undo with: bitpub delete <name> --undo');
158
+ }
159
+
160
+ function runEmptyTrash(opts) {
161
+ const removed = emptyTrashRows(opts.namespace || null);
162
+ if (removed === 0) {
163
+ console.log('(nothing to empty)');
164
+ } else {
165
+ console.log(`Emptied ${removed} entry${removed === 1 ? '' : 'ies'} from local trash.`);
166
+ console.log(' (server-side recovery still possible via `bitpub delete <name> --undo`)');
167
+ }
168
+ }
169
+
170
+ module.exports = function registerDelete(program) {
171
+ program
172
+ .command('delete [name]')
173
+ .description('Delete a slice (recoverable for 30 days). --undo to recover; --list to see deleted.')
174
+ .option('--undo', 'Restore a deleted slice from the local trash or the server')
175
+ .option('--list', 'Show recently deleted slices instead of deleting')
176
+ .option('--empty-trash', 'Clear the local undo buffer (server-side recovery still possible)')
177
+ .option('--all', 'With --list: show deleted slices across every namespace')
178
+ .option('--namespace <prefix>', 'With --empty-trash: restrict to a namespace prefix')
179
+ .option('--expect-version <number>', 'Reject if current version differs (optimistic concurrency)', parseInt)
180
+ .action(async (name, opts) => {
181
+ const config = requireConfig();
182
+ if (opts.list) return runListTrash(config, opts);
183
+ if (opts.emptyTrash) return runEmptyTrash(opts);
184
+ return runDelete(name, opts);
185
+ });
186
+ };
187
+
188
+ module.exports.runDelete = runDelete;
189
+ module.exports.runUndo = runUndo;
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `bitpub drop` — DEPRECATED ALIAS. Use `bitpub delete` instead.
5
+ *
6
+ * Forwards to delete's runDelete with the same semantics (soft-delete,
7
+ * recoverable). Hidden from --help.
8
+ */
9
+
10
+ const { runDelete } = require('./delete');
11
+
12
+ module.exports = function registerDrop(program) {
13
+ program
14
+ .command('drop', { hidden: true })
15
+ .description('[deprecated] Use `bitpub delete` instead')
16
+ .requiredOption('--address <string>', 'Exact address to drop')
17
+ .option('--expect-version <number>', 'Reject if current version differs', parseInt)
18
+ .action(async ({ address, expectVersion }) => {
19
+ console.error('warning: `bitpub drop` is deprecated. Use `bitpub delete <name>` instead.');
20
+ await runDelete(address, { expectVersion });
21
+ });
22
+ };
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `bitpub fetch` — DEPRECATED ALIAS. Use `bitpub sync` instead.
5
+ *
6
+ * Forwards to the new sync implementation with the same semantics
7
+ * (one-shot pull into the local cache). Hidden from --help.
8
+ */
9
+
10
+ const { requireConfig } = require('../config');
11
+ const { runOneShot } = require('./sync');
12
+
13
+ module.exports = function registerFetch(program) {
14
+ program
15
+ .command('fetch', { hidden: true })
16
+ .description('[deprecated] Use `bitpub sync` instead')
17
+ .requiredOption('--address <string>', 'Address pattern to sync (supports * and ** wildcards)')
18
+ .option('--limit <number>', 'Max slices to fetch (server caps at 500 per request)', '500')
19
+ .action(async ({ address, limit }) => {
20
+ console.error('warning: `bitpub fetch` is deprecated. Use `bitpub sync` instead.');
21
+ const config = requireConfig();
22
+ try {
23
+ await runOneShot({ pattern: address, label: address, limit, includeDeleted: false }, config);
24
+ } catch (err) {
25
+ console.error(`Sync failed: ${err.message}`);
26
+ process.exit(1);
27
+ }
28
+ });
29
+ };
@@ -0,0 +1,175 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `bitpub find <term>` — search for slices by name (URL tail) and by
5
+ * content, in a single command.
6
+ *
7
+ * Why both modes in one verb?
8
+ *
9
+ * - Agents calling `find` after a `load` miss don't always know whether
10
+ * the user gave them a partial slice name or a fragment of the
11
+ * content they want. Showing both kinds of matches makes the
12
+ * recovery work without a flag.
13
+ *
14
+ * - For human users, a unified "what's in my memory that mentions X"
15
+ * mental model is easier than choosing between two flavors of
16
+ * search up front.
17
+ *
18
+ * Output is always:
19
+ * 1. Name matches first (more specific — exact path-tail equals term).
20
+ * 2. Content matches second (with a few snippet lines per match).
21
+ *
22
+ * Defaults to scope = the active project; pass `--scope` for a wider
23
+ * search across `bitpub://private:<owner>/**` or a group namespace.
24
+ */
25
+
26
+ const { requireConfig } = require('../config');
27
+ const { createApiClient } = require('../api');
28
+ const { querySlices, upsertSlice, findSlicesByName } = require('../db/cache');
29
+ const { decryptSlices } = require('../crypto');
30
+ const { activeNamespace } = require('../workspace');
31
+ const { maybeExpand } = require('../aliases');
32
+
33
+ function escapeRegex(s) {
34
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
35
+ }
36
+
37
+ function safeParse(s) {
38
+ try { return JSON.parse(s); } catch { return null; }
39
+ }
40
+
41
+ async function runFind(term, opts) {
42
+ const config = requireConfig();
43
+
44
+ // Resolve the search scope. Default = active project; --scope overrides
45
+ // (and accepts aliases like @memory). --all widens to the user's full
46
+ // private namespace, which is the right move for "where did I put X".
47
+ let scope;
48
+ let scopeLabel;
49
+
50
+ if (opts.all) {
51
+ if (!config.owner) {
52
+ console.error('--all requires a private identity. Run `bitpub setup` first.');
53
+ process.exit(1);
54
+ }
55
+ scope = `bitpub://private:${config.owner}/**`;
56
+ scopeLabel = '(everything in your private memory)';
57
+ } else if (opts.scope) {
58
+ let expanded;
59
+ try { expanded = maybeExpand(opts.scope); }
60
+ catch (err) { console.error(err.message); process.exit(1); }
61
+ scope = expanded.endsWith('**') || expanded.endsWith('*')
62
+ ? expanded
63
+ : expanded.replace(/\/?$/, '/**');
64
+ scopeLabel = opts.scope;
65
+ } else {
66
+ const active = activeNamespace(config);
67
+ if (!active) {
68
+ console.error('No project anchored here and no identity. Run `bitpub setup` or pass --scope <pattern>.');
69
+ process.exit(1);
70
+ }
71
+ scope = active.namespace + '**';
72
+ scopeLabel = active.workspace?.marker.label || '(today\'s session bucket)';
73
+ }
74
+
75
+ if (opts.sync) {
76
+ const api = createApiClient(config);
77
+ try {
78
+ const slices = await api.pull(scope, 500);
79
+ decryptSlices(slices, config.api_key);
80
+ slices.forEach(upsertSlice);
81
+ } catch (err) {
82
+ console.error(`(sync failed: ${err.message} — using local cache)`);
83
+ }
84
+ }
85
+
86
+ const wantName = !opts.contentOnly;
87
+ const wantContent = !opts.nameOnly;
88
+
89
+ // ── Name matches (path-tail equals or matches the term) ────────────
90
+ let nameHits = [];
91
+ if (wantName) {
92
+ nameHits = findSlicesByName(term, scope);
93
+ }
94
+ const nameHitSet = new Set(nameHits.map(s => s.hcu));
95
+
96
+ // ── Content matches (regex over slice payload contents) ────────────
97
+ let contentHits = [];
98
+ if (wantContent) {
99
+ const flags = opts.ignoreCase ? 'gi' : 'g';
100
+ const re = new RegExp(escapeRegex(term), flags);
101
+ const maxPerSlice = parseInt(opts.maxPerSlice, 10) || 3;
102
+
103
+ const allSlices = querySlices(scope);
104
+ for (const s of allSlices) {
105
+ // Don't double-count: if a slice already matched by name, skip the
106
+ // content scan to keep the output tidy. The user will see the
107
+ // address in the name-matches section regardless.
108
+ if (nameHitSet.has(s.hcu)) continue;
109
+
110
+ const payload = safeParse(s.payload) || {};
111
+ const content = String(payload.content || '');
112
+ if (!content) continue;
113
+ const lines = content.split('\n');
114
+ const matches = lines.filter(l => re.test(l));
115
+ if (matches.length === 0) continue;
116
+ contentHits.push({ slice: s, matches, maxPerSlice });
117
+ }
118
+ }
119
+
120
+ const totalHits = nameHits.length + contentHits.length;
121
+
122
+ console.log(`Search : "${term}"`);
123
+ console.log(`Scope : ${scopeLabel}`);
124
+ console.log(`Found : ${nameHits.length} by name, ${contentHits.length} by content`);
125
+ console.log('');
126
+
127
+ if (totalHits === 0) {
128
+ console.log(` (no matches)`);
129
+ if (!opts.all) {
130
+ console.log('');
131
+ console.log(' Try widening the search:');
132
+ console.log(` bitpub find "${term}" --all`);
133
+ }
134
+ return;
135
+ }
136
+
137
+ if (nameHits.length > 0) {
138
+ console.log('Matched by name:');
139
+ for (const s of nameHits) {
140
+ const meta = safeParse(s.metadata) || {};
141
+ const ver = meta.version != null ? `v${meta.version}` : ' ';
142
+ console.log(` ${s.hcu} ${ver}`);
143
+ }
144
+ console.log('');
145
+ }
146
+
147
+ if (contentHits.length > 0) {
148
+ console.log('Matched by content:');
149
+ for (const { slice: s, matches, maxPerSlice } of contentHits) {
150
+ console.log(` ${s.hcu}`);
151
+ for (const line of matches.slice(0, maxPerSlice)) {
152
+ console.log(` ${line.trim().slice(0, 120)}`);
153
+ }
154
+ if (matches.length > maxPerSlice) {
155
+ console.log(` … ${matches.length - maxPerSlice} more match(es)`);
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ module.exports = function registerFind(program) {
162
+ program
163
+ .command('find <term>')
164
+ .description('Search slices by name and content (use --all for everything you\'ve saved)')
165
+ .option('--scope <pattern>', 'Override search scope (address pattern, supports ** wildcards)')
166
+ .option('--all', 'Search across your full private memory (shorthand for --scope bitpub://private:<owner>/**)')
167
+ .option('-i, --ignore-case', 'Case-insensitive content search')
168
+ .option('--name-only', 'Skip content search')
169
+ .option('--content-only', 'Skip name search')
170
+ .option('--sync', 'Refresh from cloud before searching')
171
+ .option('-n, --max-per-slice <number>', 'Max matching content lines per slice', '3')
172
+ .action(runFind);
173
+ };
174
+
175
+ module.exports.runFind = runFind;
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `bitpub grep` — DEPRECATED ALIAS. Use `bitpub find` instead.
5
+ *
6
+ * Kept working so existing scripts and shell aliases don't break. The
7
+ * deprecation warning prints once per invocation; the call is forwarded
8
+ * to `find`'s implementation with `--content-only` so behavior matches
9
+ * the old grep semantics exactly (content search only, no name search).
10
+ */
11
+
12
+ const { runFind } = require('./find');
13
+
14
+ module.exports = function registerGrep(program) {
15
+ program
16
+ .command('grep <term>', { hidden: true })
17
+ .description('[deprecated] Use `bitpub find` instead — content-only search in the active workspace')
18
+ .option('--scope <pattern>', 'Override search scope (address pattern, supports ** wildcards)')
19
+ .option('-i, --ignore-case', 'Case-insensitive')
20
+ .option('--sync', 'Refresh from cloud before searching')
21
+ .option('-n, --max-per-slice <number>', 'Max matching lines per slice', '3')
22
+ .action(async (term, opts) => {
23
+ console.error('warning: `bitpub grep` is deprecated. Use `bitpub find` instead.');
24
+ await runFind(term, { ...opts, contentOnly: true });
25
+ });
26
+ };
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `bitpub init` — DEPRECATED ALIAS. Use `bitpub setup` instead.
5
+ *
6
+ * Forwards to setup's ensure* helpers so the behavior is identical to
7
+ * `bitpub setup` (idempotent identity provision + folder anchor + skill
8
+ * install). Hidden from --help; prints a one-line stderr warning.
9
+ *
10
+ * Save.js auto-runs setup lazily before the first save anyway, so most
11
+ * users will never need to call this directly under either name.
12
+ */
13
+
14
+ const {
15
+ ensureIdentity,
16
+ ensureWorkspace,
17
+ ensureSkill,
18
+ } = require('./setup');
19
+ const { DEFAULT_CLOUD_URL } = require('../config');
20
+
21
+ module.exports = function registerInit(program) {
22
+ program
23
+ .command('init', { hidden: true })
24
+ .description('[deprecated] Use `bitpub setup` instead')
25
+ .option('--url <string>', 'Cloud tenant URL', DEFAULT_CLOUD_URL)
26
+ .option('--local-only', 'Set up the local store only; skip cloud provisioning')
27
+ .option('--no-workspace', 'Skip creating a .bitpub/workspace.json in the current folder')
28
+ .option('--workspace-label <string>', 'Override the workspace label (defaults to folder name)')
29
+ .option('--force', 'Overwrite an existing identity (the old key is unrecoverable)')
30
+ .option('--import-from <path>', 'Import an existing TollBit config')
31
+ .option('--no-import', 'Skip auto-import')
32
+ .action(async (opts) => {
33
+ console.error('warning: `bitpub init` is deprecated. Use `bitpub setup` instead.');
34
+ try {
35
+ const config = await ensureIdentity({
36
+ url: opts.url,
37
+ force: !!opts.force,
38
+ localOnly: !!opts.localOnly,
39
+ });
40
+ if (config && opts.workspace !== false) {
41
+ ensureWorkspace({ owner: config.owner, label: opts.workspaceLabel });
42
+ }
43
+ await ensureSkill({});
44
+ } catch (err) {
45
+ console.error(`\n✗ ${err.message}`);
46
+ process.exit(1);
47
+ }
48
+ });
49
+ };