@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,160 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `bitpub sync [path]` — pull remote slices into the local cache.
|
|
5
|
+
*
|
|
6
|
+
* Two modes:
|
|
7
|
+
* - One-shot (default): walk the addresses for the given path, pull
|
|
8
|
+
* them, write to cache, exit. Replaces the old `bitpub fetch`.
|
|
9
|
+
* - Live (`--watch`): open the SSE heartbeat stream and keep the
|
|
10
|
+
* cache up to date in the background until interrupted. Replaces
|
|
11
|
+
* the old `bitpub watch`.
|
|
12
|
+
*
|
|
13
|
+
* The `path` argument is optional and accepts the same forms as
|
|
14
|
+
* everything else: a project short name (rare for sync — it'd be a
|
|
15
|
+
* single slice), an `@alias`, or a full `bitpub://` address with or
|
|
16
|
+
* without wildcards. With no argument, syncs the active project.
|
|
17
|
+
*
|
|
18
|
+
* Power flags (`--limit`, `--include-deleted`) are kept here for the
|
|
19
|
+
* minority of users who care; they default to safe values.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const { requireConfig } = require('../config');
|
|
23
|
+
const { createApiClient } = require('../api');
|
|
24
|
+
const {
|
|
25
|
+
upsertSlice,
|
|
26
|
+
evictSlice,
|
|
27
|
+
recordNamespaceSync,
|
|
28
|
+
} = require('../db/cache');
|
|
29
|
+
const { decryptSlices } = require('../crypto');
|
|
30
|
+
const { activeNamespace } = require('../workspace');
|
|
31
|
+
const { maybeExpand } = require('../aliases');
|
|
32
|
+
|
|
33
|
+
function resolveSyncPattern(input, config) {
|
|
34
|
+
// No arg → active project's pattern
|
|
35
|
+
if (!input) {
|
|
36
|
+
const active = activeNamespace(config);
|
|
37
|
+
if (!active) {
|
|
38
|
+
throw new Error('No project anchored here and no path given. Run `bitpub setup` first or pass a path / address.');
|
|
39
|
+
}
|
|
40
|
+
return { pattern: active.namespace + '**', label: active.workspace?.marker.label || '(today\'s session bucket)' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Alias or fully-qualified URL: normalize to a pattern
|
|
44
|
+
let resolved;
|
|
45
|
+
try { resolved = maybeExpand(input); }
|
|
46
|
+
catch (err) { throw err; }
|
|
47
|
+
|
|
48
|
+
if (resolved.startsWith('bitpub://')) {
|
|
49
|
+
const pattern = resolved.endsWith('**') || resolved.endsWith('*')
|
|
50
|
+
? resolved
|
|
51
|
+
: resolved.replace(/\/?$/, '/**');
|
|
52
|
+
return { pattern, label: input };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Bare short name: treat as a project-relative single slice address
|
|
56
|
+
const active = activeNamespace(config);
|
|
57
|
+
if (!active) {
|
|
58
|
+
throw new Error(`Cannot resolve "${input}": no project anchored here. Pass a full bitpub:// URL or an alias.`);
|
|
59
|
+
}
|
|
60
|
+
return { pattern: active.namespace + resolved, label: input };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function runOneShot({ pattern, label, limit, includeDeleted }, config) {
|
|
64
|
+
const api = createApiClient(config);
|
|
65
|
+
const limitNum = parseInt(limit, 10) || 500;
|
|
66
|
+
|
|
67
|
+
const slices = await api.pull(pattern, limitNum, { includeDeleted: !!includeDeleted });
|
|
68
|
+
decryptSlices(slices, config.api_key);
|
|
69
|
+
|
|
70
|
+
for (const slice of slices) upsertSlice(slice);
|
|
71
|
+
recordNamespaceSync(pattern, slices.length);
|
|
72
|
+
|
|
73
|
+
console.log(`Synced ${slices.length} slice(s) from ${label}`);
|
|
74
|
+
|
|
75
|
+
// Surface page-cap warning so the user knows to narrow their pattern.
|
|
76
|
+
if (slices.length >= Math.min(limitNum, 500)) {
|
|
77
|
+
console.log(
|
|
78
|
+
`\n Hit the page limit (${slices.length} rows). Server caps single sync at 500.` +
|
|
79
|
+
` If you expect more, sync sub-namespaces individually:`
|
|
80
|
+
);
|
|
81
|
+
const base = pattern.replace(/\/\*+$/, '');
|
|
82
|
+
console.log(` bitpub sync '${base}/Projects/**'`);
|
|
83
|
+
console.log(` bitpub sync '${base}/Memory/**'`);
|
|
84
|
+
console.log(` bitpub sync '${base}/Sessions/**'`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function runWatch({ pattern, label }, config) {
|
|
89
|
+
const api = createApiClient(config);
|
|
90
|
+
|
|
91
|
+
console.log(`Watching ${label} (Ctrl+C to stop)`);
|
|
92
|
+
|
|
93
|
+
const close = api.watch(
|
|
94
|
+
pattern,
|
|
95
|
+
async (evt) => {
|
|
96
|
+
const updatedAddress = typeof evt === 'string' ? evt : evt.hcu;
|
|
97
|
+
const isDeleted = typeof evt === 'object' && evt.deleted === true;
|
|
98
|
+
|
|
99
|
+
if (isDeleted) {
|
|
100
|
+
evictSlice(updatedAddress);
|
|
101
|
+
console.log(`[sync] ${updatedAddress} (deleted, evicted from cache)`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
process.stdout.write(`[sync] ${updatedAddress} … `);
|
|
106
|
+
try {
|
|
107
|
+
const slices = await api.pull(updatedAddress);
|
|
108
|
+
decryptSlices(slices, config.api_key);
|
|
109
|
+
for (const slice of slices) upsertSlice(slice);
|
|
110
|
+
recordNamespaceSync(pattern, slices.length);
|
|
111
|
+
console.log(`${slices.length} slice(s) updated`);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.log(`fetch failed: ${err.message}`);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
(err) => {
|
|
117
|
+
console.error(`[sync] Connection error: ${err?.message ?? 'unknown'}. Retrying…`);
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
process.on('SIGINT', () => {
|
|
122
|
+
close();
|
|
123
|
+
console.log('\nSync watcher stopped.');
|
|
124
|
+
process.exit(0);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = function registerSync(program) {
|
|
129
|
+
program
|
|
130
|
+
.command('sync [path]')
|
|
131
|
+
.description('Pull the latest slices from the cloud into your local cache (--watch for live)')
|
|
132
|
+
.option('--watch', 'Stay open and keep the cache in sync via SSE')
|
|
133
|
+
.option('--limit <number>', 'Max slices per request (server caps at 500)', '500')
|
|
134
|
+
.option('--include-deleted', 'Include tombstoned slices in the sync')
|
|
135
|
+
.action(async (path, opts) => {
|
|
136
|
+
const config = requireConfig();
|
|
137
|
+
|
|
138
|
+
let resolved;
|
|
139
|
+
try { resolved = resolveSyncPattern(path, config); }
|
|
140
|
+
catch (err) { console.error(err.message); process.exit(1); }
|
|
141
|
+
|
|
142
|
+
if (opts.watch) {
|
|
143
|
+
return runWatch(resolved, config);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
await runOneShot(
|
|
148
|
+
{ ...resolved, limit: opts.limit, includeDeleted: opts.includeDeleted },
|
|
149
|
+
config
|
|
150
|
+
);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.error(`Sync failed: ${err.message}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
module.exports.runOneShot = runOneShot;
|
|
159
|
+
module.exports.runWatch = runWatch;
|
|
160
|
+
module.exports.resolveSyncPattern = resolveSyncPattern;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `bitpub trash …` — DEPRECATED ALIAS. Use `bitpub delete --list` /
|
|
5
|
+
* `bitpub delete <name> --undo` / `bitpub delete --empty-trash` instead.
|
|
6
|
+
*
|
|
7
|
+
* Subcommands kept for backwards compatibility:
|
|
8
|
+
*
|
|
9
|
+
* trash list → bitpub delete --list
|
|
10
|
+
* trash restore <name> → bitpub delete <name> --undo
|
|
11
|
+
* trash empty → bitpub delete --empty-trash
|
|
12
|
+
*
|
|
13
|
+
* Each prints a one-line stderr deprecation note then forwards to the
|
|
14
|
+
* new implementation. Hidden from --help.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { requireConfig } = require('../config');
|
|
18
|
+
const { runUndo } = require('./delete');
|
|
19
|
+
const {
|
|
20
|
+
listTrash,
|
|
21
|
+
emptyTrash: emptyTrashRows,
|
|
22
|
+
purgeExpiredTrash,
|
|
23
|
+
TRASH_TTL_DAYS,
|
|
24
|
+
} = require('../db/cache');
|
|
25
|
+
const { activeNamespace, resolveHcu } = require('../workspace');
|
|
26
|
+
|
|
27
|
+
function safeParse(s) {
|
|
28
|
+
try { return JSON.parse(s); } catch { return null; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = function registerTrash(program) {
|
|
32
|
+
const trash = program
|
|
33
|
+
.command('trash', { hidden: true })
|
|
34
|
+
.description('[deprecated] Use `bitpub delete --list` / `--undo` / `--empty-trash` instead');
|
|
35
|
+
|
|
36
|
+
trash
|
|
37
|
+
.command('list')
|
|
38
|
+
.option('--all', 'Show every trash entry across all namespaces')
|
|
39
|
+
.action(({ all }) => {
|
|
40
|
+
console.error('warning: `bitpub trash list` is deprecated. Use `bitpub delete --list` instead.');
|
|
41
|
+
const config = requireConfig();
|
|
42
|
+
const purged = purgeExpiredTrash();
|
|
43
|
+
if (purged > 0) {
|
|
44
|
+
console.log(`(purged ${purged} entry(ies) past the ${TRASH_TTL_DAYS}-day TTL)`);
|
|
45
|
+
}
|
|
46
|
+
let prefix = null;
|
|
47
|
+
if (!all) {
|
|
48
|
+
const active = activeNamespace(config);
|
|
49
|
+
if (active) prefix = active.namespace;
|
|
50
|
+
}
|
|
51
|
+
const rows = listTrash(prefix);
|
|
52
|
+
if (rows.length === 0) {
|
|
53
|
+
console.log(prefix ? `(no trashed slices in ${prefix})` : '(trash is empty)');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
console.log(`Trash (${rows.length} entry(ies), TTL ${TRASH_TTL_DAYS}d):`);
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
const meta = safeParse(row.metadata) || {};
|
|
59
|
+
const shortName = prefix ? row.hcu.replace(prefix, '') : row.hcu;
|
|
60
|
+
const ver = meta.version != null ? `v${meta.version}` : ' ';
|
|
61
|
+
console.log(` ${ver.padEnd(5)} ${row.deleted_at} ${shortName}`);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
trash
|
|
66
|
+
.command('restore <name>')
|
|
67
|
+
.action(async (name) => {
|
|
68
|
+
console.error('warning: `bitpub trash restore` is deprecated. Use `bitpub delete <name> --undo` instead.');
|
|
69
|
+
const config = requireConfig();
|
|
70
|
+
let address;
|
|
71
|
+
try { ({ hcu: address } = resolveHcu(name, config)); }
|
|
72
|
+
catch (err) { console.error(err.message); process.exit(1); }
|
|
73
|
+
await runUndo(address, config);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
trash
|
|
77
|
+
.command('empty')
|
|
78
|
+
.option('--namespace <prefix>', 'Restrict to entries with this prefix')
|
|
79
|
+
.action(({ namespace }) => {
|
|
80
|
+
console.error('warning: `bitpub trash empty` is deprecated. Use `bitpub delete --empty-trash` instead.');
|
|
81
|
+
const removed = emptyTrashRows(namespace || null);
|
|
82
|
+
if (removed === 0) {
|
|
83
|
+
console.log('(nothing to empty)');
|
|
84
|
+
} else {
|
|
85
|
+
console.log(`Emptied ${removed} trash entry(ies).`);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `bitpub update` — pull the latest CLI tarball from your tenant and
|
|
5
|
+
* reinstall globally.
|
|
6
|
+
*
|
|
7
|
+
* Resolution order for the tenant URL (first match wins):
|
|
8
|
+
* 1. --url flag
|
|
9
|
+
* 2. BITPUB_CLOUD_URL env var
|
|
10
|
+
* 3. api_url from ~/.bitpub/config.json (set by `bitpub init` / `auth login`)
|
|
11
|
+
* 4. https://bitpub.io
|
|
12
|
+
*
|
|
13
|
+
* The tarball lives at `${url}/cli/latest.tgz` — same path the one-liner
|
|
14
|
+
* installer uses. We download to a temp file, peek at its package.json
|
|
15
|
+
* for a version diff, then run `npm install -g <tarball>`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const { spawnSync } = require('child_process');
|
|
22
|
+
const axios = require('axios');
|
|
23
|
+
const { readConfig } = require('../config');
|
|
24
|
+
const { refreshExistingSkills } = require('./skills');
|
|
25
|
+
|
|
26
|
+
module.exports = function registerUpdate(program) {
|
|
27
|
+
program
|
|
28
|
+
.command('update')
|
|
29
|
+
.description('Update the BitPub CLI (and any installed skill files) to the latest tarball published by your tenant')
|
|
30
|
+
.option('--url <string>', 'Override the tenant URL to fetch the tarball from')
|
|
31
|
+
.option('--check', 'Only check the remote version; do not install')
|
|
32
|
+
.option('--skip-skills', 'Do not refresh installed skill files after the update')
|
|
33
|
+
.action(async (opts) => {
|
|
34
|
+
const config = readConfig();
|
|
35
|
+
const cloudUrl = (
|
|
36
|
+
opts.url ||
|
|
37
|
+
process.env.BITPUB_CLOUD_URL ||
|
|
38
|
+
config?.api_url ||
|
|
39
|
+
'https://bitpub.io'
|
|
40
|
+
).replace(/\/$/, '');
|
|
41
|
+
|
|
42
|
+
const tarballUrl = `${cloudUrl}/cli/latest.tgz`;
|
|
43
|
+
const localVersion = require('../../package.json').version;
|
|
44
|
+
|
|
45
|
+
console.log(`Current : v${localVersion}`);
|
|
46
|
+
console.log(`Tarball : ${tarballUrl}`);
|
|
47
|
+
|
|
48
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bitpub-update-'));
|
|
49
|
+
const tarballPath = path.join(tmpDir, 'bitpub-cli.tgz');
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const resp = await axios.get(tarballUrl, {
|
|
53
|
+
responseType: 'arraybuffer',
|
|
54
|
+
timeout: 30_000,
|
|
55
|
+
maxRedirects: 5,
|
|
56
|
+
validateStatus: (s) => s === 200,
|
|
57
|
+
});
|
|
58
|
+
fs.writeFileSync(tarballPath, Buffer.from(resp.data));
|
|
59
|
+
} catch (err) {
|
|
60
|
+
cleanup(tmpDir);
|
|
61
|
+
const status = err.response?.status;
|
|
62
|
+
if (status) {
|
|
63
|
+
console.error(`\n✗ Failed to download tarball: HTTP ${status} from ${tarballUrl}`);
|
|
64
|
+
if (status === 404) {
|
|
65
|
+
console.error(' This tenant does not appear to publish a CLI tarball.');
|
|
66
|
+
console.error(' Try --url https://bitpub.io for the public tenant.');
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
console.error(`\n✗ Failed to download tarball: ${err.message}`);
|
|
70
|
+
}
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const remoteVersion = readVersionFromTarball(tarballPath) || 'unknown';
|
|
75
|
+
console.log(`Remote : v${remoteVersion}`);
|
|
76
|
+
|
|
77
|
+
if (opts.check) {
|
|
78
|
+
cleanup(tmpDir);
|
|
79
|
+
if (remoteVersion === localVersion) {
|
|
80
|
+
console.log('\n✓ Already up to date.');
|
|
81
|
+
} else {
|
|
82
|
+
console.log('\nRun `bitpub update` to install v' + remoteVersion + '.');
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (remoteVersion === localVersion) {
|
|
88
|
+
console.log('\nAlready on v' + localVersion + ' — reinstalling anyway to refresh files.');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log('\nInstalling globally with npm...');
|
|
92
|
+
const result = spawnSync('npm', ['install', '-g', tarballPath], {
|
|
93
|
+
stdio: 'inherit',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
cleanup(tmpDir);
|
|
97
|
+
|
|
98
|
+
if (result.status !== 0) {
|
|
99
|
+
console.error('\n✗ npm install -g failed.');
|
|
100
|
+
console.error(' Try running the command with sudo, or set npm prefix to a writable dir.');
|
|
101
|
+
process.exit(result.status || 1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log(`\n✓ BitPub CLI updated to v${remoteVersion}.`);
|
|
105
|
+
|
|
106
|
+
if (opts.skipSkills) return;
|
|
107
|
+
|
|
108
|
+
console.log('\nRefreshing installed skill files...');
|
|
109
|
+
const { updates, error } = await refreshExistingSkills();
|
|
110
|
+
if (error) {
|
|
111
|
+
console.log(` ⊘ skipped: ${error}`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (updates.length === 0) {
|
|
115
|
+
console.log(' (no skill installs found — run `bitpub skills install` to set them up)');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
for (const u of updates) {
|
|
119
|
+
const arrow = u.status === 'updated' ? '↻' : '·';
|
|
120
|
+
let suffix = '';
|
|
121
|
+
if (u.oldVersion && u.newVersion && u.oldVersion !== u.newVersion) {
|
|
122
|
+
suffix = ` v${u.oldVersion} → v${u.newVersion}`;
|
|
123
|
+
} else if (u.newVersion) {
|
|
124
|
+
suffix = ` v${u.newVersion}`;
|
|
125
|
+
}
|
|
126
|
+
console.log(` ${arrow} ${u.label.padEnd(28)} ${u.path}${suffix}`);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
function cleanup(tmpDir) {
|
|
132
|
+
try {
|
|
133
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
134
|
+
} catch {
|
|
135
|
+
// best-effort
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Peek at `package/package.json` inside an npm tarball using the system
|
|
141
|
+
* `tar`. Both BSD tar (macOS default) and GNU tar (Linux) support
|
|
142
|
+
* `-xzOf <file> <member>` to extract a single entry to stdout. Returns
|
|
143
|
+
* null on any error; callers treat that as "version unknown" and proceed.
|
|
144
|
+
*/
|
|
145
|
+
function readVersionFromTarball(tarballPath) {
|
|
146
|
+
const out = spawnSync('tar', ['-xzOf', tarballPath, 'package/package.json'], {
|
|
147
|
+
encoding: 'utf-8',
|
|
148
|
+
});
|
|
149
|
+
if (out.status !== 0 || !out.stdout) return null;
|
|
150
|
+
try {
|
|
151
|
+
return JSON.parse(out.stdout).version || null;
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `bitpub watch` — DEPRECATED ALIAS. Use `bitpub sync --watch` instead.
|
|
5
|
+
*
|
|
6
|
+
* Forwards to sync's runWatch implementation directly. Hidden from --help;
|
|
7
|
+
* kept for backwards compatibility with shell scripts and existing
|
|
8
|
+
* automation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { requireConfig } = require('../config');
|
|
12
|
+
const { runWatch } = require('./sync');
|
|
13
|
+
|
|
14
|
+
module.exports = function registerWatch(program) {
|
|
15
|
+
program
|
|
16
|
+
.command('watch', { hidden: true })
|
|
17
|
+
.description('[deprecated] Use `bitpub sync --watch` instead')
|
|
18
|
+
.requiredOption('--address <string>', 'Address pattern to watch (supports ** wildcards)')
|
|
19
|
+
.action(({ address }) => {
|
|
20
|
+
console.error('warning: `bitpub watch` is deprecated. Use `bitpub sync --watch` instead.');
|
|
21
|
+
const config = requireConfig();
|
|
22
|
+
runWatch({ pattern: address, label: address }, config);
|
|
23
|
+
});
|
|
24
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `bitpub welcome` — first-run UX for non-technical users.
|
|
5
|
+
*
|
|
6
|
+
* Two responsibilities:
|
|
7
|
+
* 1. Save a "Welcome" slice into the user's private namespace at a stable
|
|
8
|
+
* address (`bitpub://private:<owner>/Welcome`). This is proof to a
|
|
9
|
+
* non-technical user that the round trip works: an identity got
|
|
10
|
+
* provisioned, encryption is set up, and reads/writes hit the cloud.
|
|
11
|
+
*
|
|
12
|
+
* 2. With `--serve`, start the local browser UI and open a tab to the
|
|
13
|
+
* welcome slice. This is what install.sh calls so the very first thing
|
|
14
|
+
* a new user sees after `curl ... | bash` is a working browser tab,
|
|
15
|
+
* not just a blinking shell prompt.
|
|
16
|
+
*
|
|
17
|
+
* Idempotent by default. The welcome slice is saved exactly once per
|
|
18
|
+
* machine — re-running `bitpub welcome` is a no-op unless `--force` is
|
|
19
|
+
* passed. This is gated by `welcome_saved_at` in ~/.bitpub/config.json so
|
|
20
|
+
* users (or install.sh re-runs) don't keep re-stamping the same slice.
|
|
21
|
+
*
|
|
22
|
+
* Note: tied to install, not to init. `bitpub init` is idempotent and may
|
|
23
|
+
* run many times across many folders; the welcome flow should fire exactly
|
|
24
|
+
* once, the first time the user sets up BitPub on this machine.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const { requireConfig, readConfig, writeConfig, authorIdFor } = require('../config');
|
|
28
|
+
const { createApiClient } = require('../api');
|
|
29
|
+
const { upsertSlice } = require('../db/cache');
|
|
30
|
+
const { encrypt, decryptSlices } = require('../crypto');
|
|
31
|
+
const { startBrowserServer } = require('./browser');
|
|
32
|
+
|
|
33
|
+
const WELCOME_SLICE_NAME = 'Welcome';
|
|
34
|
+
|
|
35
|
+
function welcomeAddressFor(owner) {
|
|
36
|
+
return `bitpub://private:${owner}/${WELCOME_SLICE_NAME}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildWelcomeContent(config) {
|
|
40
|
+
const owner = config.owner || '<your-owner>';
|
|
41
|
+
const address = welcomeAddressFor(owner);
|
|
42
|
+
// Plain markdown — the browser UI renders the slice content directly.
|
|
43
|
+
// Keep it short, friendly, and oriented toward the three workflows we
|
|
44
|
+
// want non-technical users to discover next: skills, sharing, building.
|
|
45
|
+
return [
|
|
46
|
+
'# Welcome to BitPub',
|
|
47
|
+
'',
|
|
48
|
+
'Your install worked. **This page is your first saved memory** — a slice',
|
|
49
|
+
`at \`${address}\` that only you can read (it\'s end-to-end encrypted on`,
|
|
50
|
+
'your machine before it leaves your laptop).',
|
|
51
|
+
'',
|
|
52
|
+
'---',
|
|
53
|
+
'',
|
|
54
|
+
'## What just happened',
|
|
55
|
+
'',
|
|
56
|
+
'- A **private namespace** was provisioned in the cloud for you',
|
|
57
|
+
' (`bitpub://private:' + owner + '/...`).',
|
|
58
|
+
'- A **local cache** was set up at `~/.bitpub/cache.db` so reads are',
|
|
59
|
+
' instant and work offline.',
|
|
60
|
+
'- This memory was **saved and encrypted, then synced** — proof the round',
|
|
61
|
+
' trip works. You\'re looking at it right now in the BitPub Browser.',
|
|
62
|
+
'',
|
|
63
|
+
'---',
|
|
64
|
+
'',
|
|
65
|
+
'## What to try next',
|
|
66
|
+
'',
|
|
67
|
+
'Pick one. Each one takes under a minute.',
|
|
68
|
+
'',
|
|
69
|
+
'### 1. Install a skill from the catalog',
|
|
70
|
+
'',
|
|
71
|
+
'Skills are reusable instructions for AI agents — like cloning a GitHub',
|
|
72
|
+
'repo, but one click and no folder structure to figure out. Your agent',
|
|
73
|
+
'picks up new skills automatically.',
|
|
74
|
+
'',
|
|
75
|
+
'> *Catalog UI coming soon. For now, see the README\'s "Packaging agents',
|
|
76
|
+
'> for your team" section for the manual flow.*',
|
|
77
|
+
'',
|
|
78
|
+
'### 2. Share with your team',
|
|
79
|
+
'',
|
|
80
|
+
'Generate a link teammates can click to join your group namespace and',
|
|
81
|
+
'read shared context. No GitHub invites, no repo access, no PRs.',
|
|
82
|
+
'',
|
|
83
|
+
'> *One-click sharing coming soon. Today: `bitpub auth login --domain',
|
|
84
|
+
'> yourcompany.com` joins an existing team namespace.*',
|
|
85
|
+
'',
|
|
86
|
+
'### 3. See what you can build',
|
|
87
|
+
'',
|
|
88
|
+
'BitPub is a file system for agents. Anything you\'d put in a folder you',
|
|
89
|
+
'can put here — research notes, prompts, drafts, agent configs, job',
|
|
90
|
+
'queues. Multiple agents can read and write the same slices.',
|
|
91
|
+
'',
|
|
92
|
+
'> Read the [Cookbook](https://github.com/tollbit/shared-memory-protocol/blob/main/COOKBOOK.md)',
|
|
93
|
+
'> for real patterns teams have built on top of these primitives.',
|
|
94
|
+
'',
|
|
95
|
+
'---',
|
|
96
|
+
'',
|
|
97
|
+
'## You don\'t need to interact with this UI',
|
|
98
|
+
'',
|
|
99
|
+
'The Browser is here so you can *see* what your agents are doing. The',
|
|
100
|
+
'real interface is your agent itself — Claude Code, Cursor, Codex, or',
|
|
101
|
+
'any MCP-compatible client. They\'ll read and write to BitPub on their',
|
|
102
|
+
'own. Close this tab whenever you\'re ready.',
|
|
103
|
+
'',
|
|
104
|
+
].join('\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function saveWelcomeSlice(config, { force } = {}) {
|
|
108
|
+
const cfg = readConfig() || {};
|
|
109
|
+
if (cfg.welcome_saved_at && !force) {
|
|
110
|
+
return { saved: false, address: welcomeAddressFor(config.owner), reason: 'already-saved' };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const address = welcomeAddressFor(config.owner);
|
|
114
|
+
const content = buildWelcomeContent(config);
|
|
115
|
+
const api = createApiClient(config);
|
|
116
|
+
|
|
117
|
+
const body = {
|
|
118
|
+
hcu: address,
|
|
119
|
+
metadata: {
|
|
120
|
+
author_id: authorIdFor(config),
|
|
121
|
+
timestamp: new Date().toISOString(),
|
|
122
|
+
tags: ['welcome', 'onboarding'],
|
|
123
|
+
},
|
|
124
|
+
payload: {
|
|
125
|
+
format: 'text/markdown',
|
|
126
|
+
content: encrypt(content, config.api_key),
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const result = await api.push(body, {});
|
|
131
|
+
decryptSlices([result.slice], config.api_key);
|
|
132
|
+
upsertSlice(result.slice);
|
|
133
|
+
|
|
134
|
+
writeConfig({ ...cfg, welcome_saved_at: new Date().toISOString() });
|
|
135
|
+
|
|
136
|
+
return { saved: true, address, version: result.slice.metadata.version };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = function registerWelcome(program) {
|
|
140
|
+
program
|
|
141
|
+
.command('welcome')
|
|
142
|
+
.description('Save a first welcome memory and (with --serve) open the browser to view it')
|
|
143
|
+
.option('--serve', 'Start the local browser UI and open a tab to the welcome slice')
|
|
144
|
+
.option('--force', 'Re-save the welcome slice even if one was already saved on this machine')
|
|
145
|
+
.option('-p, --port <port>', 'Port to serve on when --serve is passed', '4141')
|
|
146
|
+
.option('--no-open', 'Do not auto-open the browser tab when --serve is passed')
|
|
147
|
+
.action(async (opts) => {
|
|
148
|
+
const config = requireConfig();
|
|
149
|
+
|
|
150
|
+
let saveResult;
|
|
151
|
+
try {
|
|
152
|
+
saveResult = await saveWelcomeSlice(config, { force: !!opts.force });
|
|
153
|
+
} catch (err) {
|
|
154
|
+
// Don't make a save failure fatal to the install — the user still
|
|
155
|
+
// has a working CLI. Surface it but keep going to --serve so they
|
|
156
|
+
// at least see the browser is up.
|
|
157
|
+
console.error(`(welcome: could not save first memory — ${err.message})`);
|
|
158
|
+
saveResult = { saved: false, address: welcomeAddressFor(config.owner), reason: 'error' };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (saveResult.saved) {
|
|
162
|
+
console.log(`✓ Saved your first memory → ${saveResult.address} (v${saveResult.version})`);
|
|
163
|
+
} else if (saveResult.reason === 'already-saved') {
|
|
164
|
+
console.log(`✓ Welcome memory already exists → ${saveResult.address}`);
|
|
165
|
+
console.log(' (Pass --force to overwrite it.)');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!opts.serve) {
|
|
169
|
+
console.log('\n Next: open the browser to see it.');
|
|
170
|
+
console.log(' bitpub browser');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
await startBrowserServer({
|
|
176
|
+
port: opts.port,
|
|
177
|
+
open: opts.open !== false,
|
|
178
|
+
path: '/?welcome=1',
|
|
179
|
+
});
|
|
180
|
+
} catch (err) {
|
|
181
|
+
console.error(err.message);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
module.exports.saveWelcomeSlice = saveWelcomeSlice;
|
|
188
|
+
module.exports.welcomeAddressFor = welcomeAddressFor;
|
|
189
|
+
module.exports.buildWelcomeContent = buildWelcomeContent;
|