@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
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;
|