@comment-io/cli 0.1.0 → 0.1.1-alpha.5

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 CHANGED
@@ -6,15 +6,31 @@ Collaborative markdown editor with provenance tracking, comments, suggestions, a
6
6
 
7
7
  - **Backend** (`cf/`) — Cloudflare Workers + Durable Objects + Yjs
8
8
  - **Frontend** (`src/`) — React 19, Zustand, Milkdown, Tailwind CSS v4, Vite
9
+ - **Shared packages** (`packages/`) — document client/core/surface and shared protocol utilities
10
+ - **Plugins** (`plugins/`) — public Claude Code/OpenClaw plugin artifacts synced to standalone repos
9
11
 
10
12
  ## Local Development
11
13
 
12
14
  ```bash
13
- make dev # CF Worker (:8787) + Vite editor (:3100) — background
15
+ make setup # Fresh-machine bootstrap (idempotent safe to re-run)
16
+ make dev # Per-worktree Caddy host + Worker + Vite — background
14
17
  make dev-stop # Stop dev servers
18
+ make dev-status # Show this worktree's dev URL and ports
15
19
  make logs # Tail structured logs (jq)
16
20
  ```
17
21
 
22
+ `make setup` checks the toolchain, installs `simple-worktree` globally,
23
+ initializes its git hooks, creates `.env.local` from `.env.example`, stamps
24
+ `cf/.dev.vars` with dev secrets, and runs `npm install`.
25
+
26
+ For browser/OAuth/local-server work, create a worktree with `swt create <name>`
27
+ and run `make dev` there. The app is served at
28
+ `https://<worktree-name>.toofs.us` behind Caddy with isolated cookies,
29
+ localStorage, Vite HMR, Worker ports, and local Durable Object persistence.
30
+ Google login uses the singleton `https://auth.toofs.us` broker, so every
31
+ worktree can log in without adding a new Google redirect URI. See
32
+ [`docs/LOCAL-WORKTREE-DEV.md`](docs/LOCAL-WORKTREE-DEV.md).
33
+
18
34
  ## Tests
19
35
 
20
36
  ```bash
@@ -22,6 +38,19 @@ cd cf && npx vitest run # Backend test suite
22
38
  npx vite build # Verify frontend builds
23
39
  ```
24
40
 
41
+ ## Monorepo and DocumentSurface
42
+
43
+ This repo is an npm-workspaces monorepo. The root install owns `cf` and
44
+ `packages/*`; do not run separate nested installs for routine development.
45
+
46
+ Comment.io and Botspring share the same document experience through
47
+ `@comment-io/document-surface`. Product shells create a Comment.io client,
48
+ provide a slug/token and storage namespace, and render the shared editor,
49
+ comments, suggestions, provenance, and sync UI. The current Botspring staging
50
+ smoke host lives at `https://botspring.dev` and renders one configurable
51
+ DocumentSurface against the staging Worker. See
52
+ [`docs/DOCUMENT-SURFACE.md`](docs/DOCUMENT-SURFACE.md).
53
+
25
54
  ## Agent API
26
55
 
27
56
  The canonical reference for the agent-facing REST API is served at `/llms.txt` (and `/docs/api` for the interactive spec). Starting points:
@@ -39,13 +68,14 @@ files under `~/Comment Docs`.
39
68
  Install the CLI:
40
69
 
41
70
  ```bash
42
- npm install -g @comment-io/cli
71
+ npm install -g '@comment-io/cli@^0.1.1'
43
72
  ```
44
73
 
45
74
  ```bash
46
75
  comment sync login --api-key <usk_...>
47
76
  comment sync
48
77
  comment sync watch
78
+ comment sync logout
49
79
  ```
50
80
 
51
81
  Enable **Sync locally** on a document first. Local files are not an edit path;
@@ -56,21 +86,46 @@ edit through the UI or API. See `docs/COMMENTFS-SYNC-USAGE.md`.
56
86
  Registered agents can receive @mention notifications through the local daemon:
57
87
 
58
88
  ```bash
59
- comment daemon install
60
- comment daemon health
61
- comment notifications wait --profile <handle> --timeout 30m
89
+ comment bus install # macOS launchd or Linux systemd --user
90
+ comment bus status
91
+ comment run --runtime claude --profile <handle>
92
+ ```
93
+
94
+ On macOS, `comment bus install` installs the Go bus daemon as a launchd user
95
+ service. On Linux, it installs a `systemd --user` service when systemd is
96
+ available. In both cases notifications keep working after restart.
97
+ If persistent service install is unavailable, run `comment bus run` under your
98
+ own user service manager.
99
+
100
+ For a one-shot manual check outside `comment run`, use
101
+ `comment messages wait --profile <handle> --timeout 10s`, then receive and
102
+ ack/release the returned local `message_id`.
103
+
104
+ The message wait command returns a local `message_id`. Receive it before
105
+ handling the work, renew it during long work, then ack it after handling or
106
+ release it so another worker can retry:
107
+
108
+ ```bash
109
+ comment messages receive --profile <handle> <message-id>
110
+ comment messages renew --profile <handle> <message-id>
111
+ comment messages ack --profile <handle> <message-id>
112
+ comment messages release --profile <handle> <message-id>
62
113
  ```
63
114
 
64
- The notification wait command leases work locally. Ack the delivered claim after
65
- handling it, or release it so another worker can retry:
115
+ For a live local runtime, launch the CLI through the daemon bridge so
116
+ notifications can wake that tmux session:
66
117
 
67
118
  ```bash
68
- comment notifications ack <claim-id>
69
- comment notifications release <claim-id>
119
+ comment run --runtime claude --profile <handle>
120
+ comment run --runtime codex --profile <handle>
70
121
  ```
71
122
 
123
+ The npm package ships the Go bus binary for macOS and Linux while keeping the
124
+ legacy `comment sync ...` commands available through the Node wrapper.
125
+
72
126
  ## Docs
73
127
 
74
128
  - `CLAUDE.md` — repo-level agent/developer instructions
129
+ - `docs/DOCUMENT-SURFACE.md` — shared Comment.io/Botspring document surface contract
75
130
  - `docs/LOGGING.md` — structured logging guide
76
131
  - `docs/ARCHITECTURE.md` — architecture notes
package/bin/comment.js CHANGED
@@ -1,24 +1,65 @@
1
1
  #!/usr/bin/env node
2
+ import { existsSync } from 'node:fs';
2
3
  import { spawnSync } from 'node:child_process';
4
+ import { createRequire } from 'node:module';
3
5
  import { dirname, resolve } from 'node:path';
4
6
  import { fileURLToPath } from 'node:url';
5
7
 
6
8
  const binDir = dirname(fileURLToPath(import.meta.url));
7
9
  const packageRoot = resolve(binDir, '..');
8
- const tsxCli = resolve(packageRoot, 'node_modules', 'tsx', 'dist', 'cli.mjs');
10
+ const require = createRequire(import.meta.url);
11
+ const tsxCli = require.resolve('tsx/cli');
9
12
  const syncCli = resolve(packageRoot, 'scripts', 'commentfs-sync.ts');
13
+ const args = process.argv.slice(2);
10
14
 
11
- const child = spawnSync(process.execPath, [tsxCli, syncCli, ...process.argv.slice(2)], {
12
- stdio: 'inherit',
13
- });
15
+ function run(command, commandArgs, options = {}) {
16
+ const child = spawnSync(command, commandArgs, {
17
+ stdio: 'inherit',
18
+ ...options,
19
+ });
20
+ if (child.error) {
21
+ console.error(child.error.message);
22
+ process.exit(1);
23
+ }
24
+ if (child.signal) {
25
+ process.kill(process.pid, child.signal);
26
+ } else {
27
+ process.exit(child.status ?? 0);
28
+ }
29
+ }
30
+
31
+ function goTarget() {
32
+ const platform = {
33
+ darwin: 'darwin',
34
+ linux: 'linux',
35
+ }[process.platform];
36
+ const arch = {
37
+ arm64: 'arm64',
38
+ x64: 'amd64',
39
+ }[process.arch];
40
+ if (!platform || !arch) return null;
41
+ return `${platform}-${arch}`;
42
+ }
14
43
 
15
- if (child.error) {
16
- console.error(child.error.message);
17
- process.exit(1);
44
+ if (args[0] === 'sync') {
45
+ run(process.execPath, [tsxCli, syncCli, ...args]);
18
46
  }
19
47
 
20
- if (child.signal) {
21
- process.kill(process.pid, child.signal);
22
- } else {
23
- process.exit(child.status ?? 0);
48
+ const target = goTarget();
49
+ const exe = process.platform === 'win32' ? '.exe' : '';
50
+ const bundledBinary = target ? resolve(packageRoot, 'dist', `comment-${target}${exe}`) : '';
51
+
52
+ if (bundledBinary && existsSync(bundledBinary)) {
53
+ run(bundledBinary, args);
24
54
  }
55
+
56
+ const repoGoMain = resolve(packageRoot, 'packages', 'comment-go', 'cmd', 'comment');
57
+ if (target && existsSync(repoGoMain)) {
58
+ run('go', ['run', './cmd/comment', ...args], {
59
+ cwd: resolve(packageRoot, 'packages', 'comment-go'),
60
+ });
61
+ }
62
+
63
+ console.error(`Unsupported platform or missing bundled Comment.io binary: ${process.platform}/${process.arch}`);
64
+ console.error('Install a release of @comment-io/cli that includes your platform, or build the Go CLI from source.');
65
+ process.exit(1);
Binary file
Binary file
Binary file
Binary file
@@ -25,6 +25,12 @@ For staging or another deployment, include the base URL:
25
25
  comment sync login --api-key <usk_...> --base-url https://staging.example.com
26
26
  ```
27
27
 
28
+ To remove the stored sync key from this computer:
29
+
30
+ ```bash
31
+ comment sync logout
32
+ ```
33
+
28
34
  3. Open a document's access panel and enable **Sync locally**.
29
35
  4. Run one sync:
30
36
 
@@ -44,6 +50,7 @@ By default files are written under `~/Comment Docs`.
44
50
 
45
51
  ```bash
46
52
  comment sync status
53
+ comment sync logout
47
54
  comment sync repair
48
55
  comment sync explain <path>
49
56
  comment sync recover <path>
@@ -82,6 +89,8 @@ linked from the sidecar `api_docs_url`.
82
89
 
83
90
  `COMMENT_IO_USER_API_KEY` and `usk_` keys are only for read-only projection sync.
84
91
  They can poll selected docs and fetch projections. They cannot write documents.
92
+ `comment sync logout` removes the stored `usk_` key from this computer; revoke
93
+ the key in Comment.io to invalidate it everywhere.
85
94
 
86
95
  Agent edits need an edit-capable Comment.io credential such as
87
96
  `COMMENT_IO_AGENT_SECRET`, a registered agent credential, or an edit-capable
package/package.json CHANGED
@@ -1,29 +1,36 @@
1
1
  {
2
2
  "name": "@comment-io/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1-alpha.5",
4
4
  "description": "Comment.io CLI and local notification daemon",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "homepage": "https://comment.io",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "git+ssh://git@github.com/comment-io/comment.git"
10
+ "url": "git+https://github.com/botspring-ai/monorepo.git"
11
11
  },
12
12
  "bugs": {
13
- "url": "https://github.com/comment-io/comment/issues"
13
+ "url": "https://github.com/botspring-ai/monorepo/issues"
14
14
  },
15
15
  "publishConfig": {
16
16
  "access": "public"
17
17
  },
18
+ "os": [
19
+ "darwin",
20
+ "linux"
21
+ ],
22
+ "cpu": [
23
+ "arm64",
24
+ "x64"
25
+ ],
18
26
  "bin": {
19
27
  "comment": "bin/comment.js"
20
28
  },
21
29
  "files": [
22
30
  "bin/",
31
+ "dist/",
23
32
  "scripts/commentfs-sync.ts",
24
- "scripts/commentd.ts",
25
33
  "shared/commentfs-sync.ts",
26
- "shared/comment-notifications.ts",
27
34
  "docs/COMMENTFS-SYNC-USAGE.md",
28
35
  "README.md",
29
36
  "LICENSE"
@@ -1,13 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { spawnSync } from 'node:child_process';
3
- import { mkdir, rm, writeFile } from 'node:fs/promises';
4
- import { homedir, platform } from 'node:os';
2
+ import { homedir } from 'node:os';
5
3
  import { resolve } from 'node:path';
6
- import { dirname } from 'node:path';
7
- import { fileURLToPath } from 'node:url';
8
4
  import {
9
5
  DEFAULT_COMMENTFS_BASE_URL,
10
6
  DEFAULT_SYNC_ROOT,
7
+ deleteCommentFsUserApiKey,
11
8
  readCommentFsConfig,
12
9
  explainCommentFsPath,
13
10
  getCommentFsStatus,
@@ -19,15 +16,6 @@ import {
19
16
  syncOneCommentDoc,
20
17
  syncRemoteSelectedCommentDocsSettled,
21
18
  } from '../shared/commentfs-sync.js';
22
- import {
23
- daemonPidPath,
24
- daemonRequest,
25
- isProcessAlive,
26
- loadAgentProfiles,
27
- readPid,
28
- waitForLocalNotification,
29
- } from '../shared/comment-notifications.js';
30
- import { runCommentDaemon, startDetachedDaemon } from './commentd.js';
31
19
 
32
20
  interface ParsedArgs {
33
21
  command: string[];
@@ -65,16 +53,13 @@ function usage(): string {
65
53
  'Usage:',
66
54
  ' comment sync add <doc-url-or-slug> [--root <folder>] [--token <token>] [--base-url <url>] [--agent <handle>] [--filename <name.md>]',
67
55
  ' comment sync login --api-key <user-api-key> [--base-url <url>]',
56
+ ' comment sync logout',
68
57
  ' comment sync [--root <folder>] [--api-key <user-api-key>]',
69
58
  ' comment sync status [--root <folder>]',
70
59
  ' comment sync repair [--root <folder>]',
71
60
  ' comment sync recover <path>',
72
61
  ' comment sync watch [--root <folder>] [--interval <seconds>] [--full-interval <seconds>] [--max-backoff <seconds>] [--once] [--api-key <user-api-key>] [--base-url <url>]',
73
62
  ' comment sync explain <path>',
74
- ' comment daemon run|start|stop|status|health|install|uninstall [--root <folder>]',
75
- ' comment notifications wait --profile <handle> [--timeout 30m] [--poll 250ms]',
76
- ' comment notifications ack <claim-id>',
77
- ' comment notifications release <claim-id>',
78
63
  '',
79
64
  'Local markdown files are read-only projections. Edit through Comment.io UI or API.',
80
65
  `Default root: ${DEFAULT_SYNC_ROOT}`,
@@ -88,16 +73,6 @@ async function main(): Promise<void> {
88
73
  return;
89
74
  }
90
75
 
91
- if (command[0] === 'daemon') {
92
- await runDaemonCommand(command.slice(1), options);
93
- return;
94
- }
95
-
96
- if (command[0] === 'notifications') {
97
- await runNotificationsCommand(command.slice(1), options);
98
- return;
99
- }
100
-
101
76
  if (command[0] !== 'sync') {
102
77
  throw new Error(`Unknown command.\n\n${usage()}`);
103
78
  }
@@ -113,9 +88,15 @@ async function main(): Promise<void> {
113
88
  });
114
89
  console.log(`saved: ${saved.path}`);
115
90
  console.log(`base_url: ${saved.config.baseUrl}`);
116
- if (options['no-daemon'] !== true) {
117
- await maybeOfferDaemonInstall();
118
- }
91
+ return;
92
+ }
93
+
94
+ if (command[1] === 'logout') {
95
+ const result = await deleteCommentFsUserApiKey();
96
+ console.log(`${result.removed ? 'removed' : 'not logged in'}: ${result.path}`);
97
+ console.log(result.removed
98
+ ? 'CommentFS user API key removed from this computer.'
99
+ : 'No CommentFS user API key was stored on this computer.');
119
100
  return;
120
101
  }
121
102
 
@@ -215,231 +196,6 @@ async function main(): Promise<void> {
215
196
  throw new Error(`Unknown sync subcommand.\n\n${usage()}`);
216
197
  }
217
198
 
218
- async function runDaemonCommand(command: string[], options: Record<string, string | true>): Promise<void> {
219
- const sub = command[0] ?? 'status';
220
- if (sub === 'run') {
221
- await runCommentDaemon({
222
- rootDir: optionString(options, 'root'),
223
- intervalMs: parseIntervalMs(optionString(options, 'interval') ?? '10s'),
224
- fullIntervalMs: parseIntervalMs(optionString(options, 'full-interval') ?? '5m'),
225
- maxBackoffMs: parseIntervalMs(optionString(options, 'max-backoff') ?? '1m'),
226
- });
227
- return;
228
- }
229
-
230
- if (sub === 'start') {
231
- const health = await daemonRequest({ op: 'health' });
232
- if (health.ok) {
233
- console.log('daemon: already running');
234
- return;
235
- }
236
- const started = await startDetachedDaemon([]);
237
- console.log(`daemon: starting${started.pid ? ` pid ${started.pid}` : ''}`);
238
- for (let i = 0; i < 20; i++) {
239
- await sleep(250);
240
- const resp = await daemonRequest({ op: 'health' }, homedir(), 500);
241
- if (resp.ok) {
242
- console.log('daemon: running');
243
- return;
244
- }
245
- }
246
- throw new Error('Daemon did not become healthy within 5s.');
247
- }
248
-
249
- if (sub === 'stop') {
250
- const resp = await daemonRequest({ op: 'stop' });
251
- if (resp.ok) {
252
- console.log('daemon: stopping');
253
- return;
254
- }
255
- const pid = await readPid(daemonPidPath());
256
- if (pid && await isProcessAlive(pid)) {
257
- process.kill(pid, 'SIGTERM');
258
- console.log(`daemon: sent SIGTERM to ${pid}`);
259
- return;
260
- }
261
- console.log('daemon: not running');
262
- return;
263
- }
264
-
265
- if (sub === 'health') {
266
- const resp = await daemonRequest({ op: 'health' });
267
- console.log(JSON.stringify(resp, null, 2));
268
- if (!resp.ok) process.exitCode = 1;
269
- return;
270
- }
271
-
272
- if (sub === 'status') {
273
- const pid = await readPid(daemonPidPath());
274
- const alive = pid ? await isProcessAlive(pid) : false;
275
- const resp = await daemonRequest({ op: 'health' }, homedir(), 500);
276
- console.log(`pid: ${pid ?? 'none'}`);
277
- console.log(`process: ${alive ? 'running' : 'not-running'}`);
278
- console.log(`socket: ${resp.ok ? 'healthy' : 'unavailable'}`);
279
- if (resp.ok) console.log(JSON.stringify((resp as { health?: unknown }).health, null, 2));
280
- return;
281
- }
282
-
283
- if (sub === 'install') {
284
- await installDaemonService();
285
- console.log('daemon: installed');
286
- return;
287
- }
288
-
289
- if (sub === 'uninstall') {
290
- await uninstallDaemonService();
291
- console.log('daemon: uninstalled');
292
- return;
293
- }
294
-
295
- throw new Error(`Unknown daemon subcommand.\n\n${usage()}`);
296
- }
297
-
298
- async function runNotificationsCommand(command: string[], options: Record<string, string | true>): Promise<void> {
299
- const sub = command[0];
300
- if (sub === 'wait') {
301
- let profile = optionString(options, 'profile');
302
- if (!profile) {
303
- const profiles = await loadAgentProfiles();
304
- profile = profiles[0]?.handle;
305
- }
306
- if (!profile) throw new Error('Missing --profile and no configured Comment.io agent profile found.');
307
-
308
- const health = await daemonRequest({ op: 'health' }, homedir(), 500);
309
- if (!health.ok) {
310
- console.log(JSON.stringify({ timeout: true, error: 'comment daemon is not running', code: 'DAEMON_UNAVAILABLE' }));
311
- return;
312
- }
313
-
314
- const timeoutMs = parseIntervalMs(optionString(options, 'timeout') ?? '30m');
315
- const pollMs = parseIntervalMs(optionString(options, 'poll') ?? '250ms');
316
- const envelope = await waitForLocalNotification({ profile, timeoutMs, pollMs });
317
- if (!envelope) {
318
- console.log(JSON.stringify({ timeout: true, profile }));
319
- return;
320
- }
321
- console.log(JSON.stringify(envelope, null, 2));
322
- return;
323
- }
324
-
325
- if (sub === 'ack' || sub === 'release') {
326
- const claimId = command[1];
327
- if (!claimId) throw new Error(`Missing claim id.\n\n${usage()}`);
328
- const resp = await daemonRequest({ op: sub, claim_id: claimId });
329
- console.log(JSON.stringify(resp, null, 2));
330
- if (!resp.ok) process.exitCode = 1;
331
- return;
332
- }
333
-
334
- throw new Error(`Unknown notifications subcommand.\n\n${usage()}`);
335
- }
336
-
337
- async function maybeOfferDaemonInstall(): Promise<void> {
338
- const health = await daemonRequest({ op: 'health' }, homedir(), 500);
339
- if (health.ok) return;
340
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
341
- console.log('daemon: not running; run `comment daemon install` or `comment daemon start` to enable background sync and notifications.');
342
- return;
343
- }
344
- process.stdout.write('Install and start the Comment.io daemon for background sync and notifications? [y/N] ');
345
- const answer = await new Promise<string>((resolvePromise) => {
346
- process.stdin.once('data', (chunk) => resolvePromise(chunk.toString('utf-8').trim().toLowerCase()));
347
- });
348
- if (answer === 'y' || answer === 'yes') {
349
- await installDaemonService();
350
- } else {
351
- console.log('daemon: skipped');
352
- }
353
- }
354
-
355
- function commandPaths(): { tsxCli: string; cliScript: string } {
356
- const script = fileURLToPath(import.meta.url);
357
- const packageRoot = resolve(dirname(script), '..');
358
- return {
359
- tsxCli: resolve(packageRoot, 'node_modules', 'tsx', 'dist', 'cli.mjs'),
360
- cliScript: script,
361
- };
362
- }
363
-
364
- async function installDaemonService(): Promise<void> {
365
- const { tsxCli, cliScript } = commandPaths();
366
- if (platform() === 'darwin') {
367
- const plistPath = resolve(homedir(), 'Library', 'LaunchAgents', 'io.comment.daemon.plist');
368
- await mkdir(dirname(plistPath), { recursive: true, mode: 0o755 });
369
- const plist = [
370
- '<?xml version="1.0" encoding="UTF-8"?>',
371
- '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
372
- '<plist version="1.0">',
373
- '<dict>',
374
- ' <key>Label</key><string>io.comment.daemon</string>',
375
- ' <key>ProgramArguments</key>',
376
- ' <array>',
377
- ` <string>${escapeXml(process.execPath)}</string>`,
378
- ` <string>${escapeXml(tsxCli)}</string>`,
379
- ` <string>${escapeXml(cliScript)}</string>`,
380
- ' <string>daemon</string>',
381
- ' <string>run</string>',
382
- ' </array>',
383
- ' <key>RunAtLoad</key><true/>',
384
- ' <key>KeepAlive</key><true/>',
385
- '</dict>',
386
- '</plist>',
387
- '',
388
- ].join('\n');
389
- await writeFile(plistPath, plist, { mode: 0o644 });
390
- spawnSync('launchctl', ['unload', plistPath], { stdio: 'ignore' });
391
- spawnSync('launchctl', ['load', '-w', plistPath], { stdio: 'inherit' });
392
- return;
393
- }
394
-
395
- if (platform() === 'linux') {
396
- const unitDir = resolve(homedir(), '.config', 'systemd', 'user');
397
- const unitPath = resolve(unitDir, 'comment-io.service');
398
- await mkdir(unitDir, { recursive: true, mode: 0o755 });
399
- const unit = [
400
- '[Unit]',
401
- 'Description=Comment.io local daemon',
402
- '',
403
- '[Service]',
404
- `ExecStart=${process.execPath} ${tsxCli} ${cliScript} daemon run`,
405
- 'Restart=always',
406
- 'RestartSec=5',
407
- '',
408
- '[Install]',
409
- 'WantedBy=default.target',
410
- '',
411
- ].join('\n');
412
- await writeFile(unitPath, unit, { mode: 0o644 });
413
- spawnSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'inherit' });
414
- spawnSync('systemctl', ['--user', 'enable', '--now', 'comment-io.service'], { stdio: 'inherit' });
415
- return;
416
- }
417
-
418
- await runDaemonCommand(['start'], {});
419
- }
420
-
421
- async function uninstallDaemonService(): Promise<void> {
422
- if (platform() === 'darwin') {
423
- const plistPath = resolve(homedir(), 'Library', 'LaunchAgents', 'io.comment.daemon.plist');
424
- spawnSync('launchctl', ['unload', plistPath], { stdio: 'ignore' });
425
- await rm(plistPath, { force: true });
426
- return;
427
- }
428
- if (platform() === 'linux') {
429
- spawnSync('systemctl', ['--user', 'disable', '--now', 'comment-io.service'], { stdio: 'ignore' });
430
- await rm(resolve(homedir(), '.config', 'systemd', 'user', 'comment-io.service'), { force: true });
431
- spawnSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'ignore' });
432
- }
433
- }
434
-
435
- function escapeXml(value: string): string {
436
- return value
437
- .replace(/&/g, '&amp;')
438
- .replace(/</g, '&lt;')
439
- .replace(/>/g, '&gt;')
440
- .replace(/"/g, '&quot;');
441
- }
442
-
443
199
  function printResult(result: {
444
200
  ok: boolean;
445
201
  title: string;
@@ -422,6 +422,13 @@ export async function saveCommentFsUserApiKey(options: {
422
422
  return { path, config };
423
423
  }
424
424
 
425
+ export async function deleteCommentFsUserApiKey(homeDir = homedir()): Promise<{ path: string; removed: boolean }> {
426
+ const path = commentFsConfigPath(homeDir);
427
+ const removed = existsSync(path);
428
+ await rm(path, { force: true });
429
+ return { path, removed };
430
+ }
431
+
425
432
  export async function syncOneCommentDoc(options: SyncDocOptions): Promise<SyncDocResult> {
426
433
  const rootDir = resolve(options.rootDir ?? DEFAULT_SYNC_ROOT);
427
434
  const now = options.now ?? (() => new Date());