@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 +64 -9
- package/bin/comment.js +52 -11
- package/dist/comment-darwin-amd64 +0 -0
- package/dist/comment-darwin-arm64 +0 -0
- package/dist/comment-linux-amd64 +0 -0
- package/dist/comment-linux-arm64 +0 -0
- package/docs/COMMENTFS-SYNC-USAGE.md +9 -0
- package/package.json +12 -5
- package/scripts/commentfs-sync.ts +12 -256
- package/shared/commentfs-sync.ts +7 -0
- package/scripts/commentd.ts +0 -398
- package/shared/comment-notifications.ts +0 -388
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
|
|
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
|
|
60
|
-
comment
|
|
61
|
-
comment
|
|
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
|
-
|
|
65
|
-
|
|
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
|
|
69
|
-
comment
|
|
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
|
|
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
|
-
|
|
12
|
-
|
|
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 (
|
|
16
|
-
|
|
17
|
-
process.exit(1);
|
|
44
|
+
if (args[0] === 'sync') {
|
|
45
|
+
run(process.execPath, [tsxCli, syncCli, ...args]);
|
|
18
46
|
}
|
|
19
47
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
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.
|
|
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+
|
|
10
|
+
"url": "git+https://github.com/botspring-ai/monorepo.git"
|
|
11
11
|
},
|
|
12
12
|
"bugs": {
|
|
13
|
-
"url": "https://github.com/
|
|
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 {
|
|
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
|
-
|
|
117
|
-
|
|
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, '&')
|
|
438
|
-
.replace(/</g, '<')
|
|
439
|
-
.replace(/>/g, '>')
|
|
440
|
-
.replace(/"/g, '"');
|
|
441
|
-
}
|
|
442
|
-
|
|
443
199
|
function printResult(result: {
|
|
444
200
|
ok: boolean;
|
|
445
201
|
title: string;
|
package/shared/commentfs-sync.ts
CHANGED
|
@@ -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());
|