@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,241 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `bitpub list [path]` — see what's saved here, where you are, and how
|
|
5
|
+
* fresh your local cache is.
|
|
6
|
+
*
|
|
7
|
+
* Replaces the previous `list` (alphabetical), `recent` (newest-first),
|
|
8
|
+
* `status` (cache health), and `catch-up` (session bootstrap) — all four
|
|
9
|
+
* answered slightly different versions of "what do I have." Folding them
|
|
10
|
+
* into one verb means the agent doesn't have to choose between them and
|
|
11
|
+
* the user has one place to look.
|
|
12
|
+
*
|
|
13
|
+
* Behavior:
|
|
14
|
+
* bitpub list → active project, newest first, with
|
|
15
|
+
* the project label, where it lives,
|
|
16
|
+
* cache freshness, and any group_link.
|
|
17
|
+
* bitpub list <pattern> → an arbitrary scope (alias or full URL)
|
|
18
|
+
* bitpub list --all → alphabetical (the old `list` view)
|
|
19
|
+
* bitpub list --sync → refresh from the cloud first
|
|
20
|
+
* bitpub list --json → parseable output (agents)
|
|
21
|
+
*
|
|
22
|
+
* The unified output is what an agent should run at session start —
|
|
23
|
+
* `catch-up` is now an alias for `list --sync`. The 80% case (no flags)
|
|
24
|
+
* is a one-screen answer to "what's here, where is it, when did I last
|
|
25
|
+
* touch it."
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const { requireConfig } = require('../config');
|
|
29
|
+
const { createApiClient } = require('../api');
|
|
30
|
+
const {
|
|
31
|
+
querySlices,
|
|
32
|
+
upsertSlice,
|
|
33
|
+
getCacheStats,
|
|
34
|
+
getSyncedNamespaces,
|
|
35
|
+
} = require('../db/cache');
|
|
36
|
+
const { decryptSlices } = require('../crypto');
|
|
37
|
+
const { activeNamespace } = require('../workspace');
|
|
38
|
+
const { maybeExpand } = require('../aliases');
|
|
39
|
+
|
|
40
|
+
function safeParse(s) {
|
|
41
|
+
try { return JSON.parse(s); } catch { return null; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function relativeTime(iso) {
|
|
45
|
+
if (!iso) return 'never';
|
|
46
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
47
|
+
if (ms < 60_000) return 'just now';
|
|
48
|
+
const m = Math.floor(ms / 60_000);
|
|
49
|
+
if (m < 60) return `${m} min${m === 1 ? '' : 's'} ago`;
|
|
50
|
+
const h = Math.floor(m / 60);
|
|
51
|
+
if (h < 24) return `${h} hour${h === 1 ? '' : 's'} ago`;
|
|
52
|
+
const d = Math.floor(h / 24);
|
|
53
|
+
return `${d} day${d === 1 ? '' : 's'} ago`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function safeExpand(input) {
|
|
57
|
+
try { return maybeExpand(input); }
|
|
58
|
+
catch (err) { console.error(err.message); process.exit(1); }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = function registerList(program) {
|
|
62
|
+
program
|
|
63
|
+
.command('list [path]')
|
|
64
|
+
.description('See what\'s saved here, when it last changed, and where it lives')
|
|
65
|
+
.option('--sync', 'Refresh from the cloud before listing')
|
|
66
|
+
.option('--all', 'Sort alphabetically instead of newest-first')
|
|
67
|
+
.option('--json', 'Output parseable JSON (for agents and scripts)')
|
|
68
|
+
.option('-n, --limit <number>', 'Max slices to show', '20')
|
|
69
|
+
.option('--include-deleted', 'Show recently deleted slices alongside active ones')
|
|
70
|
+
.action(async (path, opts) => {
|
|
71
|
+
const config = requireConfig();
|
|
72
|
+
|
|
73
|
+
let queryPattern;
|
|
74
|
+
let label;
|
|
75
|
+
let workspaceObj = null;
|
|
76
|
+
let groupLink = null;
|
|
77
|
+
|
|
78
|
+
const expandedPattern = path ? safeExpand(path) : null;
|
|
79
|
+
if (expandedPattern && expandedPattern.startsWith('bitpub://')) {
|
|
80
|
+
queryPattern = expandedPattern.endsWith('**') || expandedPattern.endsWith('*')
|
|
81
|
+
? expandedPattern
|
|
82
|
+
: expandedPattern.replace(/\/?$/, '/**');
|
|
83
|
+
label = path;
|
|
84
|
+
} else {
|
|
85
|
+
const active = activeNamespace(config);
|
|
86
|
+
if (!active) {
|
|
87
|
+
console.error('No saves here yet. Run `bitpub save <name> "..."` to create your first slice.');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
queryPattern = active.namespace + '**';
|
|
91
|
+
label = active.workspace?.marker.label || '(no project anchored here — showing today\'s session bucket)';
|
|
92
|
+
workspaceObj = active.workspace;
|
|
93
|
+
groupLink = active.workspace?.marker.group_link || null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Audit-mode query (always hits the network; bypasses cache) ───
|
|
97
|
+
if (opts.includeDeleted) {
|
|
98
|
+
const api = createApiClient(config);
|
|
99
|
+
let slices;
|
|
100
|
+
try {
|
|
101
|
+
slices = await api.pull(queryPattern, 500, { includeDeleted: true });
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.error(`Audit fetch failed: ${err.message}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
decryptSlices(slices, config.api_key);
|
|
107
|
+
renderAlphabetical(label, queryPattern, slices, { auditMode: true, json: !!opts.json });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Optional sync first (catch-up parity) ────────────────────────
|
|
112
|
+
if (opts.sync) {
|
|
113
|
+
const api = createApiClient(config);
|
|
114
|
+
try {
|
|
115
|
+
const slices = await api.pull(queryPattern, 200);
|
|
116
|
+
decryptSlices(slices, config.api_key);
|
|
117
|
+
slices.forEach(upsertSlice);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error(`(sync failed: ${err.message} — using local cache)`);
|
|
120
|
+
}
|
|
121
|
+
// If a group_link is set on the project, also pull team context
|
|
122
|
+
// so the user sees it alongside their private work.
|
|
123
|
+
if (groupLink) {
|
|
124
|
+
try {
|
|
125
|
+
const groupPattern = groupLink.endsWith('**') || groupLink.endsWith('*')
|
|
126
|
+
? groupLink
|
|
127
|
+
: groupLink.replace(/\/?$/, '/**');
|
|
128
|
+
const groupSlices = await api.pull(groupPattern, 200);
|
|
129
|
+
decryptSlices(groupSlices, config.api_key);
|
|
130
|
+
groupSlices.forEach(upsertSlice);
|
|
131
|
+
} catch {
|
|
132
|
+
// Non-fatal
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const slices = querySlices(queryPattern);
|
|
138
|
+
|
|
139
|
+
if (opts.json) {
|
|
140
|
+
const out = {
|
|
141
|
+
path: label,
|
|
142
|
+
pattern: queryPattern,
|
|
143
|
+
project: workspaceObj?.marker || null,
|
|
144
|
+
group_link: groupLink,
|
|
145
|
+
slices: slices.map(s => ({
|
|
146
|
+
address: s.hcu,
|
|
147
|
+
metadata: safeParse(s.metadata),
|
|
148
|
+
payload: safeParse(s.payload),
|
|
149
|
+
last_synced: s.last_synced,
|
|
150
|
+
})),
|
|
151
|
+
};
|
|
152
|
+
console.log(JSON.stringify(out, null, 2));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Header: where am I, where does this live, how fresh is it ─
|
|
157
|
+
const stats = getCacheStats();
|
|
158
|
+
console.log(`Project: ${label}`);
|
|
159
|
+
if (workspaceObj) {
|
|
160
|
+
console.log(`Folder: ${workspaceObj.root}`);
|
|
161
|
+
}
|
|
162
|
+
console.log(`Address: ${queryPattern.replace(/\/\*\*?$/, '/')}`);
|
|
163
|
+
if (groupLink) console.log(`Team link: ${groupLink}`);
|
|
164
|
+
console.log(`Cache: ${stats?.total_slices ?? 0} slices total · last refreshed ${relativeTime(stats?.latest_sync)}`);
|
|
165
|
+
console.log('');
|
|
166
|
+
|
|
167
|
+
if (slices.length === 0) {
|
|
168
|
+
console.log('(nothing saved in this project yet)');
|
|
169
|
+
console.log('');
|
|
170
|
+
console.log('Try: bitpub save <name> "your first note"');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const limit = parseInt(opts.limit, 10) || 20;
|
|
175
|
+
|
|
176
|
+
if (opts.all) {
|
|
177
|
+
renderAlphabetical(label, queryPattern, slices, { json: false, limit });
|
|
178
|
+
} else {
|
|
179
|
+
renderRecent(queryPattern, slices, limit);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
function renderRecent(pattern, slices, limit) {
|
|
185
|
+
const base = pattern.replace(/\*\*?$/, '').replace(/\/?$/, '/');
|
|
186
|
+
const sorted = slices.slice().sort((a, b) =>
|
|
187
|
+
String(b.last_synced).localeCompare(String(a.last_synced))
|
|
188
|
+
);
|
|
189
|
+
const top = sorted.slice(0, limit);
|
|
190
|
+
|
|
191
|
+
for (const s of top) {
|
|
192
|
+
const meta = safeParse(s.metadata) || {};
|
|
193
|
+
const payload = safeParse(s.payload) || {};
|
|
194
|
+
const shortName = s.hcu.startsWith(base) ? s.hcu.slice(base.length) : s.hcu;
|
|
195
|
+
const preview = String(payload.content || '').replace(/\s+/g, ' ').slice(0, 60);
|
|
196
|
+
const ver = meta.version != null ? `v${meta.version}` : ' ';
|
|
197
|
+
console.log(` ${shortName.padEnd(28)} ${ver.padEnd(5)} ${preview}`);
|
|
198
|
+
}
|
|
199
|
+
if (sorted.length > limit) {
|
|
200
|
+
console.log(` … ${sorted.length - limit} more (--limit ${sorted.length} to show all)`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function renderAlphabetical(label, pattern, slices, opts = {}) {
|
|
205
|
+
const base = pattern.replace(/\*\*?$/, '').replace(/\/?$/, '/');
|
|
206
|
+
if (opts.json) {
|
|
207
|
+
console.log(JSON.stringify(slices.map(s => ({
|
|
208
|
+
address: s.hcu,
|
|
209
|
+
metadata: safeParse(s.metadata),
|
|
210
|
+
payload: safeParse(s.payload),
|
|
211
|
+
deleted_at: s.deleted_at || null,
|
|
212
|
+
})), null, 2));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (opts.auditMode) {
|
|
217
|
+
console.log(`Scope : ${label} (audit; includes deleted)`);
|
|
218
|
+
console.log(`Total : ${slices.length} slice(s)`);
|
|
219
|
+
console.log('');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (slices.length === 0) {
|
|
223
|
+
console.log(' (none)');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const sorted = slices.slice().sort((a, b) => a.hcu.localeCompare(b.hcu));
|
|
228
|
+
const limit = opts.limit || sorted.length;
|
|
229
|
+
for (const s of sorted.slice(0, limit)) {
|
|
230
|
+
const meta = s.metadata && typeof s.metadata === 'object' ? s.metadata : safeParse(s.metadata) || {};
|
|
231
|
+
const payload = s.payload && typeof s.payload === 'object' ? s.payload : safeParse(s.payload) || {};
|
|
232
|
+
const shortName = s.hcu.startsWith(base) ? s.hcu.slice(base.length) : s.hcu;
|
|
233
|
+
const ver = meta.version != null ? `v${meta.version}` : ' ';
|
|
234
|
+
const sizeStr = `${String(payload.content || '').length}b`;
|
|
235
|
+
if (s.deleted_at) {
|
|
236
|
+
console.log(` [deleted ${s.deleted_at}] ${ver.padEnd(5)} ${shortName}`);
|
|
237
|
+
} else {
|
|
238
|
+
console.log(` ${shortName.padEnd(36)} ${ver.padEnd(5)} ${sizeStr}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `bitpub load <name>` — read a slice by name or full address.
|
|
5
|
+
*
|
|
6
|
+
* Resolution order:
|
|
7
|
+
* 1. If the input looks like a `bitpub://` URL or `@alias`, treat it as
|
|
8
|
+
* explicit and look up the exact address.
|
|
9
|
+
* 2. Otherwise, resolve through the active project ("notes" →
|
|
10
|
+
* `<project>/notes`) and try the cache; if missing, fetch once.
|
|
11
|
+
* 3. If still empty AND the input was a short name, fall through to a
|
|
12
|
+
* "did you mean…" search across the user's full private namespace.
|
|
13
|
+
* With exactly one match, auto-load and print a transparency note to
|
|
14
|
+
* stderr saying where it actually came from. With multiple, list
|
|
15
|
+
* them and exit. With zero, print an actionable not-found error.
|
|
16
|
+
*
|
|
17
|
+
* The DWIM step exists because agents holding a fully-qualified URL will
|
|
18
|
+
* sometimes (incorrectly) strip it down to a short name before calling
|
|
19
|
+
* `load`. Rather than make that case fail mysteriously, we recover when
|
|
20
|
+
* we can and surface the recovery to the user.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const { requireConfig } = require('../config');
|
|
24
|
+
const { createApiClient } = require('../api');
|
|
25
|
+
const { querySlices, upsertSlice, findSlicesByName } = require('../db/cache');
|
|
26
|
+
const { decryptSlices } = require('../crypto');
|
|
27
|
+
const { resolveHcu } = require('../workspace');
|
|
28
|
+
const { isAliasRef } = require('../aliases');
|
|
29
|
+
|
|
30
|
+
module.exports = function registerLoad(program) {
|
|
31
|
+
program
|
|
32
|
+
.command('load <name>')
|
|
33
|
+
.description('Load a slice by short name (this project) or full bitpub:// address')
|
|
34
|
+
.option('--no-fetch', 'Do not fetch from cloud if missing locally')
|
|
35
|
+
.option('--format <raw|json>', 'Output format', 'raw')
|
|
36
|
+
.action(async (name, opts) => {
|
|
37
|
+
const config = requireConfig();
|
|
38
|
+
const wasShortName = !name.startsWith('bitpub://') && !isAliasRef(name);
|
|
39
|
+
|
|
40
|
+
let hcu;
|
|
41
|
+
try {
|
|
42
|
+
({ hcu } = resolveHcu(name, config));
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error(err.message);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let slices = querySlices(hcu);
|
|
49
|
+
|
|
50
|
+
if (slices.length === 0 && opts.fetch !== false) {
|
|
51
|
+
const api = createApiClient(config);
|
|
52
|
+
try {
|
|
53
|
+
const remote = await api.pull(hcu, 1);
|
|
54
|
+
decryptSlices(remote, config.api_key);
|
|
55
|
+
remote.forEach(upsertSlice);
|
|
56
|
+
slices = querySlices(hcu);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error(`(remote fetch failed: ${err.message})`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// DWIM recovery: lookup against the active project missed, but
|
|
63
|
+
// the user gave us a short name. The slice they meant might live
|
|
64
|
+
// in a sibling namespace under their private scope (/Memory/,
|
|
65
|
+
// /Inbox/, a different project, /Transcripts/, etc.). Search by
|
|
66
|
+
// path-tail and recover when the answer is unambiguous.
|
|
67
|
+
if (slices.length === 0 && wasShortName && config.owner) {
|
|
68
|
+
const scope = `bitpub://private:${config.owner}/**`;
|
|
69
|
+
const candidates = findSlicesByName(name, scope);
|
|
70
|
+
|
|
71
|
+
if (candidates.length === 1) {
|
|
72
|
+
const found = candidates[0];
|
|
73
|
+
// Transparency note → stderr so stdout stays clean for pipes.
|
|
74
|
+
console.error(`(loaded from ${found.hcu} — not in this project)`);
|
|
75
|
+
slices = [found];
|
|
76
|
+
} else if (candidates.length > 1) {
|
|
77
|
+
console.error(`Found ${candidates.length} slices named "${name}" in your private memory:`);
|
|
78
|
+
console.error('');
|
|
79
|
+
for (const c of candidates) {
|
|
80
|
+
console.error(` ${c.hcu}`);
|
|
81
|
+
}
|
|
82
|
+
console.error('');
|
|
83
|
+
console.error('Load by full address:');
|
|
84
|
+
console.error(` bitpub load <one-of-the-addresses-above>`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
// Zero candidates: fall through to the not-found block below.
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (slices.length === 0) {
|
|
91
|
+
if (wasShortName) {
|
|
92
|
+
console.error(`Not found: "${name}" (not in this project, not elsewhere in your private memory)`);
|
|
93
|
+
console.error('');
|
|
94
|
+
console.error('Try:');
|
|
95
|
+
console.error(` bitpub find "${name}" # search by content across everything you've saved`);
|
|
96
|
+
console.error(` bitpub list # see what's in this project`);
|
|
97
|
+
console.error(` bitpub sync # pull the latest from the cloud, then retry`);
|
|
98
|
+
} else {
|
|
99
|
+
console.error(`Not found: ${hcu}`);
|
|
100
|
+
console.error(` Run: bitpub sync ${hcu} to refresh the cache and retry.`);
|
|
101
|
+
}
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (opts.format === 'json') {
|
|
106
|
+
const out = slices.map(s => ({
|
|
107
|
+
hcu: s.hcu,
|
|
108
|
+
metadata: JSON.parse(s.metadata),
|
|
109
|
+
payload: JSON.parse(s.payload),
|
|
110
|
+
last_synced: s.last_synced,
|
|
111
|
+
}));
|
|
112
|
+
console.log(JSON.stringify(out, null, 2));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const parts = slices.map(s => {
|
|
117
|
+
const payload = JSON.parse(s.payload);
|
|
118
|
+
return payload.content ?? '';
|
|
119
|
+
});
|
|
120
|
+
console.log(parts.join('\n---\n'));
|
|
121
|
+
});
|
|
122
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `bitpub push` — DEPRECATED ALIAS. Use `bitpub save` instead.
|
|
5
|
+
*
|
|
6
|
+
* `bitpub save` accepts a fully-qualified `bitpub://` URL, so all the
|
|
7
|
+
* power that lived in `push` is reachable through `save` (with the same
|
|
8
|
+
* `--append`, `--expect-version`, `--force`, `--file`, and `--tags`
|
|
9
|
+
* flags). Hidden from --help.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const { requireConfig, authorIdFor } = require('../config');
|
|
14
|
+
const { createApiClient } = require('../api');
|
|
15
|
+
const { upsertSlice } = require('../db/cache');
|
|
16
|
+
const { encrypt, decryptSlices, isPrivateHcu } = require('../crypto');
|
|
17
|
+
|
|
18
|
+
module.exports = function registerPush(program) {
|
|
19
|
+
program
|
|
20
|
+
.command('push', { hidden: true })
|
|
21
|
+
.description('[deprecated] Use `bitpub save <address> ...` instead')
|
|
22
|
+
.requiredOption('--address <string>', 'Target address (no wildcards)')
|
|
23
|
+
.option('--content <string>', 'Content string to push')
|
|
24
|
+
.option('--file <path>', 'Path to a file whose contents will be pushed')
|
|
25
|
+
.option('--format <string>', 'Content format', 'text/markdown')
|
|
26
|
+
.option('--tags <string>', 'Comma-separated tags')
|
|
27
|
+
.option('--append', 'Append to existing slice instead of overwriting')
|
|
28
|
+
.option('--expect-version <number>', 'Reject push if current version differs', parseInt)
|
|
29
|
+
.option('--force', 'Override the default-strict refusal to overwrite a tombstoned slice')
|
|
30
|
+
.action(async (opts) => {
|
|
31
|
+
console.error('warning: `bitpub push` is deprecated. Use `bitpub save <address> ...` instead.');
|
|
32
|
+
const { address, content, file, format, tags, append, expectVersion, force } = opts;
|
|
33
|
+
|
|
34
|
+
if (!content && !file) {
|
|
35
|
+
console.error('Provide --content <string> or --file <path>');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const finalContent = file ? fs.readFileSync(file, 'utf-8') : content;
|
|
40
|
+
const tagList = tags ? tags.split(',').map(t => t.trim()).filter(Boolean) : [];
|
|
41
|
+
|
|
42
|
+
const config = requireConfig();
|
|
43
|
+
const api = createApiClient(config);
|
|
44
|
+
|
|
45
|
+
const payloadContent = isPrivateHcu(address)
|
|
46
|
+
? encrypt(finalContent, config.api_key)
|
|
47
|
+
: finalContent;
|
|
48
|
+
|
|
49
|
+
const body = {
|
|
50
|
+
hcu: address,
|
|
51
|
+
metadata: {
|
|
52
|
+
author_id: authorIdFor(config),
|
|
53
|
+
timestamp: new Date().toISOString(),
|
|
54
|
+
tags: tagList,
|
|
55
|
+
},
|
|
56
|
+
payload: { format, content: payloadContent },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const result = await api.push(body, { append: !!append, expectVersion, force: !!force });
|
|
61
|
+
decryptSlices([result.slice], config.api_key);
|
|
62
|
+
upsertSlice(result.slice);
|
|
63
|
+
console.log(`Pushed → ${address} (v${result.slice.metadata.version})`);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (err.response?.status === 409) {
|
|
66
|
+
const data = err.response.data;
|
|
67
|
+
if (data?.deleted_at) {
|
|
68
|
+
console.error(`Push blocked: ${data.error}`);
|
|
69
|
+
console.error(` Tombstoned at: ${data.deleted_at}`);
|
|
70
|
+
console.error(` To undelete with the prior content: bitpub delete "${address}" --undo`);
|
|
71
|
+
console.error(` To overwrite with new content, retry with --force.`);
|
|
72
|
+
} else {
|
|
73
|
+
console.error(
|
|
74
|
+
`Version conflict: expected v${data.expected_version} but server has v${data.actual_version}. ` +
|
|
75
|
+
`Fetch the latest version and retry.`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
console.error(`Push failed: ${err.message}`);
|
|
80
|
+
}
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `bitpub read` — DEPRECATED ALIAS. Use `bitpub load` instead.
|
|
5
|
+
*
|
|
6
|
+
* `bitpub load` accepts a fully-qualified `bitpub://` URL plus the same
|
|
7
|
+
* `--format` flag, so this is a thin pass-through. Hidden from --help.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { querySlices } = require('../db/cache');
|
|
11
|
+
|
|
12
|
+
module.exports = function registerRead(program) {
|
|
13
|
+
program
|
|
14
|
+
.command('read', { hidden: true })
|
|
15
|
+
.description('[deprecated] Use `bitpub load <address>` instead')
|
|
16
|
+
.requiredOption('--address <string>', 'Address or pattern to read (supports * and ** wildcards)')
|
|
17
|
+
.option('--format <raw|json>', 'Output format: raw content or full JSON DTOs', 'raw')
|
|
18
|
+
.action(({ address, format }) => {
|
|
19
|
+
console.error('warning: `bitpub read` is deprecated. Use `bitpub load <address>` instead.');
|
|
20
|
+
const slices = querySlices(address);
|
|
21
|
+
if (slices.length === 0) {
|
|
22
|
+
console.error(`No slices found for: ${address}`);
|
|
23
|
+
console.error(`Run: bitpub sync ${address} to refresh from the cloud, then retry.`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
if (format === 'json') {
|
|
27
|
+
const output = slices.map(s => ({
|
|
28
|
+
hcu: s.hcu,
|
|
29
|
+
metadata: JSON.parse(s.metadata),
|
|
30
|
+
payload: JSON.parse(s.payload),
|
|
31
|
+
last_synced: s.last_synced,
|
|
32
|
+
}));
|
|
33
|
+
console.log(JSON.stringify(output, null, 2));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const parts = slices.map(s => {
|
|
37
|
+
const payload = JSON.parse(s.payload);
|
|
38
|
+
return payload.content ?? '';
|
|
39
|
+
});
|
|
40
|
+
console.log(parts.join('\n---\n'));
|
|
41
|
+
});
|
|
42
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `bitpub recent` — DEPRECATED ALIAS. Use `bitpub list` instead.
|
|
5
|
+
*
|
|
6
|
+
* `bitpub list` is now newest-first by default and shows the same
|
|
7
|
+
* preview info `recent` did. This thin alias prints a one-line warning
|
|
8
|
+
* and delegates. Hidden from --help.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { requireConfig } = require('../config');
|
|
12
|
+
const { createApiClient } = require('../api');
|
|
13
|
+
const { querySlices, upsertSlice, getCacheStats } = 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
|
+
module.exports = function registerRecent(program) {
|
|
22
|
+
program
|
|
23
|
+
.command('recent', { hidden: true })
|
|
24
|
+
.description('[deprecated] Use `bitpub list` instead')
|
|
25
|
+
.option('-n, --limit <number>', 'Max slices to show', '10')
|
|
26
|
+
.option('--sync', 'Refresh from cloud before listing')
|
|
27
|
+
.action(async (opts) => {
|
|
28
|
+
console.error('warning: `bitpub recent` is deprecated. Use `bitpub list` instead.');
|
|
29
|
+
const config = requireConfig();
|
|
30
|
+
const active = activeNamespace(config);
|
|
31
|
+
if (!active) {
|
|
32
|
+
console.error('No workspace found and no owner configured. Run `bitpub setup` first.');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const pattern = active.namespace + '**';
|
|
36
|
+
if (opts.sync) {
|
|
37
|
+
const api = createApiClient(config);
|
|
38
|
+
try {
|
|
39
|
+
const slices = await api.pull(pattern, 200);
|
|
40
|
+
decryptSlices(slices, config.api_key);
|
|
41
|
+
slices.forEach(upsertSlice);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error(`(sync failed: ${err.message})`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const slices = querySlices(pattern);
|
|
47
|
+
slices.sort((a, b) => String(b.last_synced).localeCompare(String(a.last_synced)));
|
|
48
|
+
const limit = parseInt(opts.limit, 10) || 10;
|
|
49
|
+
const top = slices.slice(0, limit);
|
|
50
|
+
const label = active.workspace?.marker.label || '(session default)';
|
|
51
|
+
console.log(`Workspace : ${label}`);
|
|
52
|
+
console.log(`Namespace : ${active.namespace}`);
|
|
53
|
+
console.log('');
|
|
54
|
+
if (top.length === 0) {
|
|
55
|
+
console.log(`No slices yet. Save one: bitpub save <name> "..."`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
for (const s of top) {
|
|
59
|
+
const meta = safeParse(s.metadata) || {};
|
|
60
|
+
const payload = safeParse(s.payload) || {};
|
|
61
|
+
const shortName = s.hcu.replace(active.namespace, '') || '(root)';
|
|
62
|
+
const preview = String(payload.content || '').replace(/\s+/g, ' ').slice(0, 60);
|
|
63
|
+
const ver = meta.version != null ? `v${meta.version}` : ' ';
|
|
64
|
+
console.log(` ${shortName.padEnd(28)} ${ver.padEnd(5)} ${preview}`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `bitpub restore` — DEPRECATED ALIAS. Use `bitpub delete <name> --undo` instead.
|
|
5
|
+
*
|
|
6
|
+
* Forwards to delete's runUndo. Hidden from --help.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { requireConfig } = require('../config');
|
|
10
|
+
const { runUndo } = require('./delete');
|
|
11
|
+
|
|
12
|
+
module.exports = function registerRestore(program) {
|
|
13
|
+
program
|
|
14
|
+
.command('restore', { hidden: true })
|
|
15
|
+
.description('[deprecated] Use `bitpub delete <name> --undo` instead')
|
|
16
|
+
.requiredOption('--address <string>', 'Exact address to restore')
|
|
17
|
+
.option('--expect-version <number>', 'Reject if current version differs', parseInt)
|
|
18
|
+
.action(async ({ address }) => {
|
|
19
|
+
console.error('warning: `bitpub restore` is deprecated. Use `bitpub delete <name> --undo` instead.');
|
|
20
|
+
const config = requireConfig();
|
|
21
|
+
await runUndo(address, config);
|
|
22
|
+
});
|
|
23
|
+
};
|