bookmarks-but-better 1.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/lib/status.mjs ADDED
@@ -0,0 +1,254 @@
1
+ // The status report: what is true of this machine, read into one shape by
2
+ // `daemon.mjs`, and here turned into problems that each name the one command
3
+ // that fixes them. No I/O, so every situation is a fixture in
4
+ // `test/status.test.mjs`.
5
+
6
+ import { contractHome } from "./layout.mjs";
7
+
8
+ const DEFAULT_PORT = 52222;
9
+ const DEFAULT_BIND = "127.0.0.1";
10
+
11
+ const INSTALL = "npx bookmarks-but-better install";
12
+
13
+ /** `http://host:port`, with an IPv6 host in brackets. */
14
+ export function formatOrigin(bind = DEFAULT_BIND, port = DEFAULT_PORT) {
15
+ const host = bind.includes(":") ? `[${bind}]` : bind;
16
+ return `http://${host}:${port}`;
17
+ }
18
+
19
+ /** The version out of `bookmarks-but-better --version`'s one line. */
20
+ export function parseVersionOutput(text) {
21
+ const match = String(text)
22
+ .trim()
23
+ .match(/(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/);
24
+ return match ? match[1] : null;
25
+ }
26
+
27
+ /**
28
+ * `major.minor.patch` of a version, ignoring any prerelease suffix: a
29
+ * `4.1.0-beta.2` daemon is the 4.1.0 line as far as compatibility goes.
30
+ */
31
+ export function baseVersion(version) {
32
+ const match = String(version ?? "").match(/^(\d+)\.(\d+)\.(\d+)/);
33
+ return match ? match.slice(1, 4).map(Number) : null;
34
+ }
35
+
36
+ /** Negative, zero or positive as `a` is older than, the same line as, or newer than `b`. */
37
+ export function compareBase(a, b) {
38
+ const left = baseVersion(a);
39
+ const right = baseVersion(b);
40
+ if (!left || !right) return 0;
41
+ for (let index = 0; index < 3; index += 1) {
42
+ if (left[index] !== right[index]) return left[index] - right[index];
43
+ }
44
+ return 0;
45
+ }
46
+
47
+ /** An empty report: the shape `daemon.mjs` fills in. */
48
+ export function emptyReport({ toolVersion, daemonVersion, binaryPath }) {
49
+ return {
50
+ tool: { version: toolVersion, daemonVersion },
51
+ binary: { installed: false, path: binaryPath, version: null },
52
+ registry: null,
53
+ registryError: null,
54
+ service: null,
55
+ serviceError: null,
56
+ origin: null,
57
+ health: null,
58
+ healthError: null,
59
+ };
60
+ }
61
+
62
+ const ids = (list) => new Set((list ?? []).map((entry) => entry.id));
63
+ const sameSet = (a, b) => a.size === b.size && [...a].every((id) => b.has(id));
64
+
65
+ /**
66
+ * Every problem in `report`, each with the command that fixes it — `fix` is
67
+ * that command as text, `action` the same thing the interactive menu can
68
+ * perform: `{ kind: "install" }`, `{ kind: "vault-remove", id }`, or
69
+ * `{ kind: "manual" }` for something only a person can do. Order is the order
70
+ * a person should read them in: nothing later is worth fixing while something
71
+ * earlier is wrong.
72
+ */
73
+ export function assess(report) {
74
+ const problems = [];
75
+ const add = (summary, fix, action = { kind: "install" }) => problems.push({ summary, fix, action });
76
+
77
+ if (!report.binary.installed) {
78
+ add("the daemon is not installed", INSTALL);
79
+ return { ok: false, problems };
80
+ }
81
+ // The tool knows one daemon line. An older daemon is an update; a newer one
82
+ // means the tool is what is out of date, and reinstalling would downgrade.
83
+ const line = compareBase(report.binary.version, report.tool.daemonVersion);
84
+ if (report.binary.version && line < 0) {
85
+ add(
86
+ `the installed daemon is ${report.binary.version}; this tool installs ${report.tool.daemonVersion}`,
87
+ INSTALL,
88
+ );
89
+ } else if (report.binary.version && line > 0) {
90
+ add(
91
+ `the installed daemon is ${report.binary.version}, newer than the ${report.tool.daemonVersion} this tool knows`,
92
+ "npx bookmarks-but-better@latest",
93
+ { kind: "manual" },
94
+ );
95
+ }
96
+
97
+ const registry = report.registry;
98
+ if (!registry) {
99
+ add(
100
+ `the configuration could not be read${report.registryError ? `: ${report.registryError}` : ""}`,
101
+ INSTALL,
102
+ );
103
+ } else if (registry.vaults.length === 0) {
104
+ add("no vault is configured", INSTALL);
105
+ } else {
106
+ for (const vault of registry.vaults) {
107
+ if (vault.state === "directory missing") {
108
+ add(
109
+ `vault \`${vault.id}\`: ${vault.path} does not exist`,
110
+ `restore that folder, or: npx bookmarks-but-better vault remove ${vault.id}`,
111
+ { kind: "vault-remove", id: vault.id },
112
+ );
113
+ } else if (vault.state === "not a directory") {
114
+ add(
115
+ `vault \`${vault.id}\`: ${vault.path} is not a directory`,
116
+ `npx bookmarks-but-better vault remove ${vault.id}`,
117
+ { kind: "vault-remove", id: vault.id },
118
+ );
119
+ } else if (vault.state === "not initialized") {
120
+ add(
121
+ `vault \`${vault.id}\`: ${vault.path} is not a vault yet`,
122
+ `npx bookmarks-but-better vault remove ${vault.id}, then vault add ${vault.id} ${vault.path}`,
123
+ { kind: "vault-remove", id: vault.id },
124
+ );
125
+ }
126
+ }
127
+ }
128
+
129
+ const service = report.service;
130
+ if (!service) {
131
+ add(
132
+ `the background service could not be checked${report.serviceError ? `: ${report.serviceError}` : ""}`,
133
+ INSTALL,
134
+ );
135
+ } else if (service.state === "not-installed") {
136
+ add("the background service is not installed", INSTALL);
137
+ } else if (service.state === "stopped") {
138
+ add("the background service is installed but not running", INSTALL);
139
+ } else if (registry && registry.vaults.length > 0 && !sameSet(ids(registry.vaults), ids(service.vaults))) {
140
+ add("the service was installed for a different set of vaults than is configured", INSTALL);
141
+ }
142
+
143
+ const health = report.health;
144
+ const serviceInstalled = service && service.state !== "not-installed";
145
+ if (!health) {
146
+ if (serviceInstalled && service.state !== "stopped") {
147
+ add(
148
+ `the daemon is not answering at ${report.origin}${report.healthError ? ` (${report.healthError})` : ""}`,
149
+ `${INSTALL} (which restarts it)`,
150
+ );
151
+ }
152
+ } else {
153
+ if (report.binary.version && health.version !== report.binary.version) {
154
+ add(
155
+ `the running daemon is ${health.version}, the installed one ${report.binary.version}`,
156
+ `${INSTALL} (which restarts it)`,
157
+ );
158
+ }
159
+ if (registry) {
160
+ const hosted = ids(health.vaults);
161
+ for (const vault of registry.vaults) {
162
+ if (!hosted.has(vault.id)) {
163
+ add(
164
+ `\`${vault.id}\` is configured but the running daemon does not host it`,
165
+ `${INSTALL} (which restarts it)`,
166
+ );
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ return { ok: problems.length === 0, problems };
173
+ }
174
+
175
+ /** The report as lines for a terminal. */
176
+ export function render(report, { homedir = "" } = {}) {
177
+ const home = (value) => contractHome(value, homedir);
178
+ const lines = [];
179
+ const row = (label, value) => lines.push(`${label.padEnd(10)} ${value}`);
180
+
181
+ row("tool", `${report.tool.version} (installs daemon ${report.tool.daemonVersion})`);
182
+
183
+ if (!report.binary.installed) {
184
+ row("daemon", "not installed");
185
+ } else {
186
+ row("daemon", `${report.binary.version ?? "unknown version"} ${home(report.binary.path)}`);
187
+ }
188
+
189
+ if (report.binary.installed) {
190
+ const service = report.service;
191
+ if (!service) {
192
+ row("service", `could not be checked${report.serviceError ? `: ${report.serviceError}` : ""}`);
193
+ } else if (service.state === "not-installed") {
194
+ row("service", "not installed");
195
+ } else {
196
+ const state =
197
+ service.state === "installed-unsupervised"
198
+ ? "installed; starts at your next login"
199
+ : service.state;
200
+ row("service", `${state} (${service.kind})${service.port ? `, port ${service.port}` : ""}`);
201
+ }
202
+
203
+ if (report.health) {
204
+ const clients = report.health.clients ?? 0;
205
+ const who =
206
+ clients === 0
207
+ ? "no browser connected"
208
+ : `${clients} browser connection${clients === 1 ? "" : "s"}`;
209
+ row("running", `yes, ${report.health.version} at ${report.origin}; ${who}`);
210
+ } else {
211
+ row("running", `no${report.healthError ? ` (${report.origin}: ${report.healthError})` : ""}`);
212
+ }
213
+
214
+ const registry = report.registry;
215
+ if (!registry) {
216
+ row("vaults", `could not be read${report.registryError ? `: ${report.registryError}` : ""}`);
217
+ } else if (registry.vaults.length === 0) {
218
+ row("vaults", "none configured");
219
+ } else {
220
+ const hosted = ids(report.health?.vaults);
221
+ const width = Math.max(...registry.vaults.map((vault) => vault.id.length));
222
+ lines.push("vaults");
223
+ for (const vault of registry.vaults) {
224
+ const state = hosted.has(vault.id)
225
+ ? "hosted"
226
+ : vault.state === "ok" || vault.state === "served now"
227
+ ? "configured, not hosted"
228
+ : vault.state;
229
+ lines.push(` ${vault.id.padEnd(width)} ${home(vault.path)} (${state})`);
230
+ }
231
+ }
232
+ }
233
+
234
+ const { ok, problems } = assess(report);
235
+ lines.push("");
236
+ if (ok) {
237
+ lines.push("everything is in place");
238
+ if (report.health && (report.health.clients ?? 0) === 0) {
239
+ lines.push(`connect a browser: Settings → Sources → Connect, address ${report.origin}`);
240
+ }
241
+ } else {
242
+ lines.push(problems.length === 1 ? "one thing to fix:" : `${problems.length} things to fix:`);
243
+ for (const problem of problems) {
244
+ lines.push(` - ${problem.summary}`);
245
+ lines.push(` ${problem.fix}`);
246
+ }
247
+ }
248
+ return lines.join("\n");
249
+ }
250
+
251
+ /** The report plus its assessment, for `--json`. */
252
+ export function toJson(report) {
253
+ return { ...report, assessment: assess(report) };
254
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "bookmarks-but-better",
3
+ "version": "1.0.0",
4
+ "daemon": {
5
+ "version": "4.1.0"
6
+ },
7
+ "description": "Installs and looks after the Bookmarks But Better daemon: status, install, uninstall, and the vaults it serves.",
8
+ "keywords": [
9
+ "bookmarks",
10
+ "markdown",
11
+ "daemon",
12
+ "installer"
13
+ ],
14
+ "homepage": "https://bookmarks.farhadeidi.com",
15
+ "bugs": "https://github.com/farhadeidi/bookmarks-but-better/issues",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/farhadeidi/bookmarks-but-better.git",
19
+ "directory": "packages/bookmarks-but-better"
20
+ },
21
+ "license": "MIT",
22
+ "author": "Bookmarks But Better contributors",
23
+ "type": "module",
24
+ "bin": {
25
+ "bookmarks-but-better": "bin/bookmarks-but-better.mjs"
26
+ },
27
+ "files": [
28
+ "bin",
29
+ "lib",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
33
+ "engines": {
34
+ "node": ">=20.12"
35
+ },
36
+ "scripts": {
37
+ "test": "node --test"
38
+ },
39
+ "dependencies": {
40
+ "@clack/prompts": "^1.8.0"
41
+ }
42
+ }