@comment-io/cli 0.1.1-alpha.9 → 0.1.2
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 +16 -10
- package/bin/comment.js +0 -8
- 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/package.json +1 -7
- package/docs/COMMENTFS-SYNC-USAGE.md +0 -97
- package/scripts/commentfs-sync.ts +0 -342
- package/shared/commentfs-sync.ts +0 -1565
package/README.md
CHANGED
|
@@ -60,10 +60,10 @@ The canonical reference for the agent-facing REST API is served at `/llms.txt` (
|
|
|
60
60
|
- `PATCH /docs/:id` — edit via `{ old_string, new_string }` patches
|
|
61
61
|
- `POST /docs/:id/comments` — comment / suggest / reply
|
|
62
62
|
|
|
63
|
-
##
|
|
63
|
+
## Local Sync Files
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
The Go `comment` CLI can mirror read-only markdown projections for the current
|
|
66
|
+
library sync scope, **My Files and Shared With Me**, under `~/Comment Docs`.
|
|
67
67
|
|
|
68
68
|
Install the CLI:
|
|
69
69
|
|
|
@@ -72,14 +72,21 @@ npm install -g '@comment-io/cli@^0.1.1'
|
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
|
-
comment sync login
|
|
76
|
-
comment sync
|
|
75
|
+
comment sync login
|
|
76
|
+
comment sync once
|
|
77
|
+
comment sync enable
|
|
77
78
|
comment sync watch
|
|
78
|
-
comment sync logout
|
|
79
|
+
comment sync logout [--purge-local]
|
|
79
80
|
```
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
Approve the device in Settings when `comment sync login` prints the browser
|
|
83
|
+
code. `comment sync enable` lets the Go bus worker run local sync; install or
|
|
84
|
+
run the bus daemon with `comment bus install` or `comment bus run` for
|
|
85
|
+
persistent background sync. Local files are not an edit path; edit through the
|
|
86
|
+
UI or REST API. Local edits are preserved under `~/.comment-io/sync/recovery`
|
|
87
|
+
and the server version is restored on the next sync. Use
|
|
88
|
+
`comment sync logout --purge-local` only when you also want to remove verified
|
|
89
|
+
clean local projections.
|
|
83
90
|
|
|
84
91
|
## Local Notification Daemon
|
|
85
92
|
|
|
@@ -120,8 +127,7 @@ comment run --runtime claude --profile <handle>
|
|
|
120
127
|
comment run --runtime codex --profile <handle>
|
|
121
128
|
```
|
|
122
129
|
|
|
123
|
-
The npm package ships the Go bus binary for macOS and Linux
|
|
124
|
-
legacy `comment sync ...` commands available through the Node wrapper.
|
|
130
|
+
The npm package ships the Go bus and local sync binary for macOS and Linux.
|
|
125
131
|
|
|
126
132
|
## Docs
|
|
127
133
|
|
package/bin/comment.js
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
|
-
import { createRequire } from 'node:module';
|
|
5
4
|
import { dirname, resolve } from 'node:path';
|
|
6
5
|
import { fileURLToPath } from 'node:url';
|
|
7
6
|
|
|
8
7
|
const binDir = dirname(fileURLToPath(import.meta.url));
|
|
9
8
|
const packageRoot = resolve(binDir, '..');
|
|
10
|
-
const require = createRequire(import.meta.url);
|
|
11
|
-
const tsxCli = require.resolve('tsx/cli');
|
|
12
|
-
const syncCli = resolve(packageRoot, 'scripts', 'commentfs-sync.ts');
|
|
13
9
|
const args = process.argv.slice(2);
|
|
14
10
|
|
|
15
11
|
function run(command, commandArgs, options = {}) {
|
|
@@ -41,10 +37,6 @@ function goTarget() {
|
|
|
41
37
|
return `${platform}-${arch}`;
|
|
42
38
|
}
|
|
43
39
|
|
|
44
|
-
if (args[0] === 'sync') {
|
|
45
|
-
run(process.execPath, [tsxCli, syncCli, ...args]);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
40
|
const target = goTarget();
|
|
49
41
|
const exe = process.platform === 'win32' ? '.exe' : '';
|
|
50
42
|
const bundledBinary = target ? resolve(packageRoot, 'dist', `comment-${target}${exe}`) : '';
|
|
Binary file
|
|
Binary file
|
package/dist/comment-linux-amd64
CHANGED
|
Binary file
|
package/dist/comment-linux-arm64
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@comment-io/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Comment.io CLI and local notification daemon",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -29,18 +29,12 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
"bin/",
|
|
31
31
|
"dist/",
|
|
32
|
-
"scripts/commentfs-sync.ts",
|
|
33
|
-
"shared/commentfs-sync.ts",
|
|
34
|
-
"docs/COMMENTFS-SYNC-USAGE.md",
|
|
35
32
|
"README.md",
|
|
36
33
|
"LICENSE"
|
|
37
34
|
],
|
|
38
35
|
"scripts": {
|
|
39
36
|
"prepack": "node ../../scripts/prepare-cli-package.mjs"
|
|
40
37
|
},
|
|
41
|
-
"dependencies": {
|
|
42
|
-
"tsx": "^4.21.0"
|
|
43
|
-
},
|
|
44
38
|
"engines": {
|
|
45
39
|
"node": ">=20"
|
|
46
40
|
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# CommentFS Read-Only Local Sync
|
|
2
|
-
|
|
3
|
-
CommentFS projects selected Comment.io documents into local markdown files. The
|
|
4
|
-
online Comment.io document is canonical. Local markdown files are read-only
|
|
5
|
-
snapshots for search, context, indexing, and agent inspection.
|
|
6
|
-
|
|
7
|
-
Do not edit synced markdown files directly. Humans should edit through the
|
|
8
|
-
Comment.io UI. Agents should edit through the Comment.io API.
|
|
9
|
-
|
|
10
|
-
## Setup
|
|
11
|
-
|
|
12
|
-
The CommentFS UI is currently gated behind `VITE_ENABLE_COMMENTFS_UI=true` so it
|
|
13
|
-
can be enabled on staging without exposing the flow in production.
|
|
14
|
-
|
|
15
|
-
1. Open Comment.io settings and generate a CommentFS key.
|
|
16
|
-
2. Configure the local CLI:
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
comment sync login --api-key <usk_...>
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
For staging or another deployment, include the base URL:
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
comment sync login --api-key <usk_...> --base-url https://staging.example.com
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
To remove the stored sync key from this computer:
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
comment sync logout
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
3. Open a document's access panel and enable **Sync locally**.
|
|
35
|
-
4. Run one sync:
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
comment sync
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
5. Keep projections updated:
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
comment sync watch
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
By default files are written under `~/Comment Docs`.
|
|
48
|
-
|
|
49
|
-
## Useful Commands
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
comment sync status
|
|
53
|
-
comment sync logout
|
|
54
|
-
comment sync repair
|
|
55
|
-
comment sync explain <path>
|
|
56
|
-
comment sync recover <path>
|
|
57
|
-
comment sync watch --interval 10s --full-interval 5m
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
`comment sync repair` restores read-only permissions on existing projections.
|
|
61
|
-
`comment sync explain` points a markdown or sidecar path back to its source
|
|
62
|
-
document and API docs. `comment sync recover` explains a preserved local edit
|
|
63
|
-
artifact.
|
|
64
|
-
|
|
65
|
-
## Sidecars
|
|
66
|
-
|
|
67
|
-
Each sync root has a `.comment/` folder. Important files include:
|
|
68
|
-
|
|
69
|
-
- `.comment/manifest.json`: local projection manifest.
|
|
70
|
-
- `.comment/docs/<slug>/status.json`: sync health, source URL, revision, sidecar
|
|
71
|
-
paths, and recovery metadata.
|
|
72
|
-
- `.comment/docs/<slug>/edit.md`: short edit instructions and API doc links.
|
|
73
|
-
- `.comment/docs/<slug>/authorship.json`: authorship/provenance metadata.
|
|
74
|
-
- `.comment/docs/<slug>/comments.json`: comment and suggestion metadata.
|
|
75
|
-
- `.comment/docs/<slug>/participants.json`: participant metadata.
|
|
76
|
-
- `.comment/recovery/*.local.md`: preserved local text from unsupported local
|
|
77
|
-
edits.
|
|
78
|
-
|
|
79
|
-
## Local Edits
|
|
80
|
-
|
|
81
|
-
If a local tool changes a synced markdown file, the next sync preserves that
|
|
82
|
-
local text under `.comment/recovery/` and restores the canonical Comment.io
|
|
83
|
-
version. It does not upload local markdown edits.
|
|
84
|
-
|
|
85
|
-
To apply an intended change, open the document in Comment.io or use the API docs
|
|
86
|
-
linked from the sidecar `api_docs_url`.
|
|
87
|
-
|
|
88
|
-
## Auth Notes
|
|
89
|
-
|
|
90
|
-
`COMMENT_IO_USER_API_KEY` and `usk_` keys are only for read-only projection sync.
|
|
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.
|
|
94
|
-
|
|
95
|
-
Agent edits need an edit-capable Comment.io credential such as
|
|
96
|
-
`COMMENT_IO_AGENT_SECRET`, a registered agent credential, or an edit-capable
|
|
97
|
-
per-document token.
|
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { resolve } from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
DEFAULT_COMMENTFS_BASE_URL,
|
|
6
|
-
DEFAULT_SYNC_ROOT,
|
|
7
|
-
deleteCommentFsUserApiKey,
|
|
8
|
-
readCommentFsConfig,
|
|
9
|
-
explainCommentFsPath,
|
|
10
|
-
getCommentFsStatus,
|
|
11
|
-
recoverCommentFsPath,
|
|
12
|
-
repairCommentFsPermissions,
|
|
13
|
-
saveCommentFsUserApiKey,
|
|
14
|
-
syncConfiguredCommentDocs,
|
|
15
|
-
syncConfiguredCommentDocsSettled,
|
|
16
|
-
syncOneCommentDoc,
|
|
17
|
-
syncRemoteSelectedCommentDocsSettled,
|
|
18
|
-
} from '../shared/commentfs-sync.js';
|
|
19
|
-
|
|
20
|
-
interface ParsedArgs {
|
|
21
|
-
command: string[];
|
|
22
|
-
options: Record<string, string | true>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function parseArgs(argv: string[]): ParsedArgs {
|
|
26
|
-
const command: string[] = [];
|
|
27
|
-
const options: Record<string, string | true> = {};
|
|
28
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
29
|
-
const arg = argv[i];
|
|
30
|
-
if (arg.startsWith('--')) {
|
|
31
|
-
const key = arg.slice(2);
|
|
32
|
-
const next = argv[i + 1];
|
|
33
|
-
if (next && !next.startsWith('--')) {
|
|
34
|
-
options[key] = next;
|
|
35
|
-
i += 1;
|
|
36
|
-
} else {
|
|
37
|
-
options[key] = true;
|
|
38
|
-
}
|
|
39
|
-
} else {
|
|
40
|
-
command.push(arg);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return { command, options };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function optionString(options: Record<string, string | true>, key: string): string | undefined {
|
|
47
|
-
const value = options[key];
|
|
48
|
-
return typeof value === 'string' ? value : undefined;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function usage(): string {
|
|
52
|
-
return [
|
|
53
|
-
'Usage:',
|
|
54
|
-
' comment sync add <doc-url-or-slug> [--root <folder>] [--token <token>] [--base-url <url>] [--agent <handle>] [--filename <name.md>]',
|
|
55
|
-
' comment sync login --api-key <user-api-key> [--base-url <url>]',
|
|
56
|
-
' comment sync logout',
|
|
57
|
-
' comment sync [--root <folder>] [--api-key <user-api-key>]',
|
|
58
|
-
' comment sync status [--root <folder>]',
|
|
59
|
-
' comment sync repair [--root <folder>]',
|
|
60
|
-
' comment sync recover <path>',
|
|
61
|
-
' comment sync watch [--root <folder>] [--interval <seconds>] [--full-interval <seconds>] [--max-backoff <seconds>] [--once] [--api-key <user-api-key>] [--base-url <url>]',
|
|
62
|
-
' comment sync explain <path>',
|
|
63
|
-
'',
|
|
64
|
-
'Local markdown files are read-only projections. Edit through Comment.io UI or API.',
|
|
65
|
-
`Default root: ${DEFAULT_SYNC_ROOT}`,
|
|
66
|
-
].join('\n');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function main(): Promise<void> {
|
|
70
|
-
const { command, options } = parseArgs(process.argv.slice(2));
|
|
71
|
-
if (options.help || command[0] === 'help') {
|
|
72
|
-
console.log(usage());
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (command[0] !== 'sync') {
|
|
77
|
-
throw new Error(`Unknown command.\n\n${usage()}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const rootDir = optionString(options, 'root');
|
|
81
|
-
|
|
82
|
-
if (command[1] === 'login') {
|
|
83
|
-
const apiKey = optionString(options, 'api-key');
|
|
84
|
-
if (!apiKey) throw new Error(`Missing --api-key.\n\n${usage()}`);
|
|
85
|
-
const saved = await saveCommentFsUserApiKey({
|
|
86
|
-
userApiKey: apiKey,
|
|
87
|
-
baseUrl: optionString(options, 'base-url'),
|
|
88
|
-
});
|
|
89
|
-
console.log(`saved: ${saved.path}`);
|
|
90
|
-
console.log(`base_url: ${saved.config.baseUrl}`);
|
|
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.');
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (command[1] === 'add') {
|
|
104
|
-
const input = command[2];
|
|
105
|
-
if (!input) throw new Error(`Missing doc URL or slug.\n\n${usage()}`);
|
|
106
|
-
const result = await syncOneCommentDoc({
|
|
107
|
-
rootDir,
|
|
108
|
-
input,
|
|
109
|
-
token: optionString(options, 'token'),
|
|
110
|
-
baseUrl: optionString(options, 'base-url'),
|
|
111
|
-
agentHandle: optionString(options, 'agent'),
|
|
112
|
-
filename: optionString(options, 'filename'),
|
|
113
|
-
});
|
|
114
|
-
printResult(result);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (command[1] === 'explain') {
|
|
119
|
-
const inputPath = command[2];
|
|
120
|
-
if (!inputPath) throw new Error(`Missing path.\n\n${usage()}`);
|
|
121
|
-
const result = await explainCommentFsPath(inputPath);
|
|
122
|
-
console.log(result.explanation);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (command[1] === 'recover') {
|
|
127
|
-
const inputPath = command[2];
|
|
128
|
-
if (!inputPath) throw new Error(`Missing path.\n\n${usage()}`);
|
|
129
|
-
const result = await recoverCommentFsPath(inputPath);
|
|
130
|
-
console.log(result.instructions);
|
|
131
|
-
console.log(`markdown: ${result.markdownPath}`);
|
|
132
|
-
console.log(`status: ${result.statusPath}`);
|
|
133
|
-
console.log(`edit: ${result.editPath}`);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (command[1] === 'watch') {
|
|
138
|
-
await runWatch({
|
|
139
|
-
rootDir,
|
|
140
|
-
baseUrl: optionString(options, 'base-url'),
|
|
141
|
-
apiKey: optionString(options, 'api-key'),
|
|
142
|
-
intervalMs: parseIntervalMs(optionString(options, 'interval') ?? '10s'),
|
|
143
|
-
fullIntervalMs: parseIntervalMs(optionString(options, 'full-interval') ?? '5m'),
|
|
144
|
-
maxBackoffMs: parseIntervalMs(optionString(options, 'max-backoff') ?? '1m'),
|
|
145
|
-
once: options.once === true,
|
|
146
|
-
});
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (command[1] === 'status') {
|
|
151
|
-
const status = await getCommentFsStatus(rootDir);
|
|
152
|
-
console.log(`root: ${status.rootDir}`);
|
|
153
|
-
console.log(`manifest: ${status.manifestPath}`);
|
|
154
|
-
if (status.docs.length === 0) {
|
|
155
|
-
console.log('No Comment.io docs configured.');
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
for (const doc of status.docs) {
|
|
159
|
-
const health = doc.syncHealth?.status ?? 'unknown';
|
|
160
|
-
const revision = doc.revision === null ? 'unknown' : String(doc.revision);
|
|
161
|
-
const mode = doc.readOnly ? 'read-only' : 'writable-or-missing';
|
|
162
|
-
console.log(`${health}: ${doc.title} (${doc.slug}) revision ${revision} ${mode}`);
|
|
163
|
-
console.log(` markdown: ${doc.markdownPath}`);
|
|
164
|
-
console.log(` status: ${doc.statusPath}`);
|
|
165
|
-
if (doc.localChange) console.log(` recovery: ${doc.localChange.recoveryFile}`);
|
|
166
|
-
if (doc.syncHealth?.lastError) console.log(` error: ${doc.syncHealth.lastError}`);
|
|
167
|
-
}
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (command[1] === 'repair') {
|
|
172
|
-
const results = await repairCommentFsPermissions(rootDir);
|
|
173
|
-
if (results.length === 0) {
|
|
174
|
-
console.log('No Comment.io docs configured.');
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
for (const result of results) {
|
|
178
|
-
if (!result.existed) console.log(`missing: ${result.title} (${result.markdownPath})`);
|
|
179
|
-
else if (result.repaired) console.log(`repaired: ${result.title} (${result.markdownPath})`);
|
|
180
|
-
else console.log(`ok: ${result.title} (${result.markdownPath})`);
|
|
181
|
-
}
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (command.length === 1) {
|
|
186
|
-
const apiKey = optionString(options, 'api-key');
|
|
187
|
-
const results = await syncConfiguredCommentDocs({ rootDir, userApiKey: apiKey });
|
|
188
|
-
if (results.length === 0) {
|
|
189
|
-
console.log('No Comment.io docs configured. Run `comment sync add <doc-url-or-slug>` first.');
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
for (const result of results) printResult(result);
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
throw new Error(`Unknown sync subcommand.\n\n${usage()}`);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function printResult(result: {
|
|
200
|
-
ok: boolean;
|
|
201
|
-
title: string;
|
|
202
|
-
revision?: number;
|
|
203
|
-
changed?: boolean;
|
|
204
|
-
localChangeDetected?: boolean;
|
|
205
|
-
recoveryPath?: string;
|
|
206
|
-
disabled?: boolean;
|
|
207
|
-
markdownPath: string;
|
|
208
|
-
statusPath: string;
|
|
209
|
-
error?: string;
|
|
210
|
-
}): void {
|
|
211
|
-
if (!result.ok) {
|
|
212
|
-
console.log(`error: ${result.title}`);
|
|
213
|
-
console.log(`markdown: ${result.markdownPath}`);
|
|
214
|
-
console.log(`status: ${result.statusPath}`);
|
|
215
|
-
console.log(`message: ${result.error ?? 'unknown error'}`);
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
if (result.disabled) {
|
|
219
|
-
console.log(`unselected: ${result.title}`);
|
|
220
|
-
console.log(`removed: ${result.markdownPath}`);
|
|
221
|
-
console.log(`status: ${result.statusPath}`);
|
|
222
|
-
if (result.recoveryPath) console.log(`recovery: ${result.recoveryPath}`);
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
const state = result.localChangeDetected ? 'recovered-local-change' : result.changed ? 'updated' : 'unchanged';
|
|
226
|
-
console.log(`${state}: ${result.title} (revision ${result.revision})`);
|
|
227
|
-
console.log(`markdown: ${result.markdownPath}`);
|
|
228
|
-
console.log(`status: ${result.statusPath}`);
|
|
229
|
-
if (result.recoveryPath) console.log(`recovery: ${result.recoveryPath}`);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async function runWatch(options: {
|
|
233
|
-
rootDir?: string;
|
|
234
|
-
baseUrl?: string;
|
|
235
|
-
apiKey?: string;
|
|
236
|
-
intervalMs: number;
|
|
237
|
-
fullIntervalMs: number;
|
|
238
|
-
maxBackoffMs: number;
|
|
239
|
-
once: boolean;
|
|
240
|
-
}): Promise<void> {
|
|
241
|
-
const configured = await readCommentFsConfig().catch(() => null);
|
|
242
|
-
const apiKey = options.apiKey ?? process.env.COMMENT_IO_USER_API_KEY ?? configured?.userApiKey;
|
|
243
|
-
const baseUrl = (options.baseUrl ?? configured?.baseUrl ?? DEFAULT_COMMENTFS_BASE_URL).replace(/\/$/, '');
|
|
244
|
-
const rootDir = resolve(options.rootDir ?? DEFAULT_SYNC_ROOT);
|
|
245
|
-
const authMode = apiKey ? 'user_api_key' : 'configured_doc_credentials';
|
|
246
|
-
const maxBackoffMs = Math.max(options.maxBackoffMs, options.intervalMs);
|
|
247
|
-
let lastFullSyncAt = 0;
|
|
248
|
-
let consecutiveFailures = 0;
|
|
249
|
-
let emptyPolls = 0;
|
|
250
|
-
|
|
251
|
-
console.log('watch: starting CommentFS read-only projection sync');
|
|
252
|
-
console.log(`root: ${rootDir}`);
|
|
253
|
-
console.log(`base_url: ${apiKey ? baseUrl : 'per-doc manifest URLs'}`);
|
|
254
|
-
console.log(`auth_mode: ${authMode}`);
|
|
255
|
-
console.log(`interval: ${formatDuration(options.intervalMs)}`);
|
|
256
|
-
console.log(`full_reconcile_interval: ${apiKey ? formatDuration(options.fullIntervalMs) : 'not used without a user API key'}`);
|
|
257
|
-
console.log(`max_backoff: ${formatDuration(maxBackoffMs)}`);
|
|
258
|
-
|
|
259
|
-
do {
|
|
260
|
-
try {
|
|
261
|
-
const full = Boolean(apiKey && (lastFullSyncAt === 0 || Date.now() - lastFullSyncAt >= options.fullIntervalMs));
|
|
262
|
-
const results = apiKey
|
|
263
|
-
? await syncRemoteSelectedCommentDocsSettled({
|
|
264
|
-
rootDir,
|
|
265
|
-
baseUrl,
|
|
266
|
-
userApiKey: apiKey,
|
|
267
|
-
full,
|
|
268
|
-
})
|
|
269
|
-
: await syncConfiguredCommentDocsSettled({ rootDir });
|
|
270
|
-
if (full) lastFullSyncAt = Date.now();
|
|
271
|
-
|
|
272
|
-
const failures = results.filter((result) => !result.ok).length;
|
|
273
|
-
if (failures === 0) consecutiveFailures = 0;
|
|
274
|
-
else consecutiveFailures += 1;
|
|
275
|
-
|
|
276
|
-
if (results.length === 0) {
|
|
277
|
-
emptyPolls += 1;
|
|
278
|
-
if (emptyPolls === 1 || emptyPolls % 6 === 0 || options.once) {
|
|
279
|
-
console.log(`${watchStamp()} ${apiKey
|
|
280
|
-
? 'No remote Comment.io sync changes.'
|
|
281
|
-
: 'No Comment.io docs configured. Run `comment sync add <doc-url-or-slug>` first.'}`);
|
|
282
|
-
}
|
|
283
|
-
} else {
|
|
284
|
-
emptyPolls = 0;
|
|
285
|
-
console.log(`${watchStamp()} ${full ? 'full reconcile' : 'poll'} returned ${results.length} result${results.length === 1 ? '' : 's'}.`);
|
|
286
|
-
for (const result of results) printResult(result);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (options.once) {
|
|
290
|
-
if (failures > 0) process.exitCode = 1;
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
await sleep(nextWatchDelay(options.intervalMs, maxBackoffMs, consecutiveFailures));
|
|
294
|
-
} catch (error) {
|
|
295
|
-
consecutiveFailures += 1;
|
|
296
|
-
const delayMs = nextWatchDelay(options.intervalMs, maxBackoffMs, consecutiveFailures);
|
|
297
|
-
console.error(`${watchStamp()} sync poll failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
298
|
-
if (options.once) {
|
|
299
|
-
process.exitCode = 1;
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
console.error(`${watchStamp()} retrying in ${formatDuration(delayMs)}.`);
|
|
303
|
-
await sleep(delayMs);
|
|
304
|
-
}
|
|
305
|
-
} while (true);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function parseIntervalMs(value: string): number {
|
|
309
|
-
const trimmed = value.trim().toLowerCase();
|
|
310
|
-
const match = /^(\d+(?:\.\d+)?)(ms|s|m)?$/.exec(trimmed);
|
|
311
|
-
if (!match) throw new Error(`Invalid interval: ${value}`);
|
|
312
|
-
const amount = Number(match[1]);
|
|
313
|
-
const unit = match[2] ?? 's';
|
|
314
|
-
const ms = unit === 'm' ? amount * 60_000 : unit === 's' ? amount * 1000 : amount;
|
|
315
|
-
if (!Number.isFinite(ms) || ms < 100) throw new Error('Interval must be at least 100ms.');
|
|
316
|
-
return Math.round(ms);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function nextWatchDelay(intervalMs: number, maxBackoffMs: number, consecutiveFailures: number): number {
|
|
320
|
-
if (consecutiveFailures <= 0) return intervalMs;
|
|
321
|
-
const multiplier = 2 ** Math.min(consecutiveFailures - 1, 6);
|
|
322
|
-
return Math.min(maxBackoffMs, Math.max(intervalMs, intervalMs * multiplier));
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function formatDuration(ms: number): string {
|
|
326
|
-
if (ms % 60_000 === 0) return `${ms / 60_000}m`;
|
|
327
|
-
if (ms % 1000 === 0) return `${ms / 1000}s`;
|
|
328
|
-
return `${ms}ms`;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function watchStamp(): string {
|
|
332
|
-
return `[${new Date().toISOString()}]`;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function sleep(ms: number): Promise<void> {
|
|
336
|
-
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
main().catch((error) => {
|
|
340
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
341
|
-
process.exit(1);
|
|
342
|
-
});
|