kontexted 0.1.12 → 0.1.14
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 +175 -26
- package/dist/commands/sync/conflicts/list.d.ts +16 -0
- package/dist/commands/sync/conflicts/list.js +131 -0
- package/dist/commands/sync/conflicts/resolve.d.ts +17 -0
- package/dist/commands/sync/conflicts/resolve.js +186 -0
- package/dist/commands/sync/conflicts/show.d.ts +16 -0
- package/dist/commands/sync/conflicts/show.js +128 -0
- package/dist/commands/sync/conflicts.d.ts +11 -0
- package/dist/commands/sync/conflicts.js +20 -0
- package/dist/commands/sync/force-pull.d.ts +21 -0
- package/dist/commands/sync/force-pull.js +173 -0
- package/dist/commands/sync/force-push.d.ts +21 -0
- package/dist/commands/sync/force-push.js +269 -0
- package/dist/commands/sync/index.d.ts +10 -0
- package/dist/commands/sync/index.js +187 -0
- package/dist/commands/sync/init.d.ts +13 -0
- package/dist/commands/sync/init.js +260 -0
- package/dist/commands/sync/reset.d.ts +21 -0
- package/dist/commands/sync/reset.js +134 -0
- package/dist/commands/sync/start.d.ts +23 -0
- package/dist/commands/sync/start.js +232 -0
- package/dist/commands/sync/status.d.ts +19 -0
- package/dist/commands/sync/status.js +205 -0
- package/dist/commands/sync/stop.d.ts +14 -0
- package/dist/commands/sync/stop.js +153 -0
- package/dist/index.js +2 -0
- package/dist/lib/api-client.d.ts +13 -0
- package/dist/lib/api-client.js +28 -2
- package/dist/lib/sync/command-utils.d.ts +77 -0
- package/dist/lib/sync/command-utils.js +280 -0
- package/dist/lib/sync/crypto.d.ts +8 -0
- package/dist/lib/sync/crypto.js +11 -0
- package/dist/lib/sync/file-watcher.d.ts +30 -0
- package/dist/lib/sync/file-watcher.js +117 -0
- package/dist/lib/sync/queue.d.ts +44 -0
- package/dist/lib/sync/queue.js +87 -0
- package/dist/lib/sync/remote-listener.d.ts +52 -0
- package/dist/lib/sync/remote-listener.js +228 -0
- package/dist/lib/sync/sync-engine.d.ts +175 -0
- package/dist/lib/sync/sync-engine.js +995 -0
- package/dist/lib/sync/types.d.ts +351 -0
- package/dist/lib/sync/types.js +5 -0
- package/dist/lib/sync/utils.d.ts +51 -0
- package/dist/lib/sync/utils.js +126 -0
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Kontexted CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The official CLI for Kontexted—provides **Disk Sync**, MCP proxy, server management, and workspace operations for AI-assisted development.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,68 +8,217 @@ A command-line tool for Kontexted that provides MCP proxy functionality and work
|
|
|
8
8
|
npm install -g kontexted
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Overview
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Kontexted provides three ways for AI assistants to access your notes:
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
| Method | Best For | Setup Difficulty |
|
|
16
|
+
|--------|----------|------------------|
|
|
17
|
+
| **Disk Sync** | Direct file access, all AI tools | ★☆☆ Easy |
|
|
18
|
+
| **MCP Server** | Claude Desktop, MCP-compatible tools | ★★☆ Medium |
|
|
19
|
+
| **CLI Skills** | Scripted operations, custom workflows | ★★★ Advanced |
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
kontexted login --url https://app.example.com --workspace my-workspace --alias prod
|
|
19
|
-
```
|
|
21
|
+
---
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
## Disk Sync (Recommended)
|
|
24
|
+
|
|
25
|
+
Sync your Kontexted workspace to disk as markdown files. This is the **recommended method** for AI coding agents to access your notes.
|
|
26
|
+
|
|
27
|
+
### Quick Start
|
|
22
28
|
|
|
23
29
|
```bash
|
|
24
|
-
|
|
30
|
+
# In your project directory
|
|
31
|
+
kontexted sync init --alias my-workspace --dir .
|
|
32
|
+
kontexted sync start --daemon
|
|
25
33
|
```
|
|
26
34
|
|
|
27
|
-
|
|
35
|
+
Your notes are now available at `.kontexted/folder/note.md`
|
|
36
|
+
|
|
37
|
+
### How It Works
|
|
38
|
+
|
|
39
|
+
- Notes sync to `.kontexted/` as markdown files
|
|
40
|
+
- Real-time bidirectional sync with file watching
|
|
41
|
+
- Directory is gitignored; `.ignore` file allows AI tools to reference files
|
|
42
|
+
- Works with opencode, Claude Code, Cursor, Windsurf, and any AI that reads files
|
|
43
|
+
|
|
44
|
+
### Commands
|
|
45
|
+
|
|
46
|
+
| Command | Description |
|
|
47
|
+
|---------|-------------|
|
|
48
|
+
| `sync init --alias <name> --dir .` | Initialize sync in current directory |
|
|
49
|
+
| `sync start --daemon` | Start background sync with file watching |
|
|
50
|
+
| `sync start --foreground` | Start sync in foreground |
|
|
51
|
+
| `sync stop` | Stop sync daemon |
|
|
52
|
+
| `sync status` | Check sync status |
|
|
53
|
+
| `sync force-pull` | Pull all notes from server |
|
|
54
|
+
| `sync force-push` | Push all local changes to server |
|
|
55
|
+
| `sync conflicts list` | List sync conflicts |
|
|
56
|
+
| `sync conflicts show <id>` | Show conflict details |
|
|
57
|
+
| `sync conflicts resolve <id> --strategy <local\|remote>` | Resolve conflict |
|
|
58
|
+
| `sync reset` | Reset sync state |
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## MCP Proxy
|
|
28
63
|
|
|
29
|
-
Start the MCP proxy server
|
|
64
|
+
Start the MCP proxy server for Claude Desktop or other MCP clients:
|
|
30
65
|
|
|
31
66
|
```bash
|
|
32
|
-
#
|
|
33
|
-
kontexted --alias
|
|
67
|
+
# Read-only mode
|
|
68
|
+
kontexted mcp --alias <name>
|
|
34
69
|
|
|
35
|
-
#
|
|
36
|
-
kontexted --
|
|
70
|
+
# With write access
|
|
71
|
+
kontexted mcp --alias <name> --write
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Configuring Claude Desktop
|
|
37
75
|
|
|
38
|
-
|
|
39
|
-
kontexted --alias prod --write
|
|
76
|
+
**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
40
77
|
|
|
41
|
-
|
|
42
|
-
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"mcpServers": {
|
|
81
|
+
"kontexted": {
|
|
82
|
+
"command": "kontexted",
|
|
83
|
+
"args": ["mcp", "--alias", "my-workspace", "--write"]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
43
87
|
```
|
|
44
88
|
|
|
45
|
-
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Server Management
|
|
46
92
|
|
|
47
|
-
|
|
93
|
+
Run a local Kontexted server:
|
|
48
94
|
|
|
49
95
|
```bash
|
|
50
|
-
|
|
96
|
+
# Initialize local server (first time)
|
|
97
|
+
kontexted server init
|
|
98
|
+
|
|
99
|
+
# Start server in background
|
|
100
|
+
kontexted server start
|
|
101
|
+
|
|
102
|
+
# Start server in foreground
|
|
103
|
+
kontexted server start --foreground
|
|
104
|
+
|
|
105
|
+
# Stop server
|
|
106
|
+
kontexted server stop
|
|
107
|
+
|
|
108
|
+
# Check status
|
|
109
|
+
kontexted server status
|
|
110
|
+
|
|
111
|
+
# View logs
|
|
112
|
+
kontexted server logs -f
|
|
113
|
+
|
|
114
|
+
# Display signup invite code
|
|
115
|
+
kontexted server show-invite
|
|
116
|
+
|
|
117
|
+
# Diagnose issues
|
|
118
|
+
kontexted server doctor
|
|
51
119
|
```
|
|
52
120
|
|
|
53
|
-
|
|
121
|
+
### Default Configuration
|
|
122
|
+
|
|
123
|
+
| Setting | Default |
|
|
124
|
+
|---------|---------|
|
|
125
|
+
| Database | SQLite (`~/.kontexted/data/kontexted.db`) |
|
|
126
|
+
| Port | `4729` |
|
|
127
|
+
| Config | `~/.kontexted/config.json` |
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## CLI Skills
|
|
132
|
+
|
|
133
|
+
Direct CLI access for workspace operations:
|
|
54
134
|
|
|
55
135
|
```bash
|
|
56
|
-
|
|
136
|
+
# Query workspace tree
|
|
137
|
+
kontexted skill workspace-tree --alias <name>
|
|
138
|
+
|
|
139
|
+
# Search notes
|
|
140
|
+
kontexted skill search-notes --alias <name> --query "search text" --limit 10
|
|
141
|
+
|
|
142
|
+
# Get note by ID
|
|
143
|
+
kontexted skill note-by-id --alias <name> --note-id <id>
|
|
144
|
+
|
|
145
|
+
# Create folder (requires --write login)
|
|
146
|
+
kontexted skill create-folder --alias <name> --name <slug> --display-name "Name"
|
|
147
|
+
|
|
148
|
+
# Create note (requires --write login)
|
|
149
|
+
kontexted skill create-note --alias <name> --name <slug> --title "Title"
|
|
150
|
+
|
|
151
|
+
# Update note content (requires --write login)
|
|
152
|
+
kontexted skill update-note-content --alias <name> --note-id <id> --content "Content"
|
|
57
153
|
```
|
|
58
154
|
|
|
59
|
-
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Authentication
|
|
60
158
|
|
|
61
159
|
```bash
|
|
160
|
+
# Login to a server
|
|
161
|
+
kontexted login --url https://app.example.com --workspace my-workspace --alias prod
|
|
162
|
+
|
|
163
|
+
# Login with write permissions
|
|
164
|
+
kontexted login --url https://app.example.com --workspace my-workspace --alias prod --write
|
|
165
|
+
|
|
166
|
+
# Show stored profiles
|
|
167
|
+
kontexted show-config
|
|
168
|
+
|
|
169
|
+
# Remove a profile
|
|
170
|
+
kontexted logout --alias prod
|
|
171
|
+
|
|
172
|
+
# Remove all profiles
|
|
62
173
|
kontexted logout
|
|
63
174
|
```
|
|
64
175
|
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Command Reference
|
|
179
|
+
|
|
180
|
+
| Category | Command | Description |
|
|
181
|
+
|----------|---------|-------------|
|
|
182
|
+
| **Sync** | `sync init` | Initialize disk sync |
|
|
183
|
+
| | `sync start` | Start sync daemon |
|
|
184
|
+
| | `sync stop` | Stop sync daemon |
|
|
185
|
+
| | `sync status` | Check status |
|
|
186
|
+
| | `sync force-pull` | Force pull from server |
|
|
187
|
+
| | `sync force-push` | Force push to server |
|
|
188
|
+
| | `sync conflicts list` | List conflicts |
|
|
189
|
+
| | `sync reset` | Reset sync state |
|
|
190
|
+
| **Server** | `server init` | Initialize local server |
|
|
191
|
+
| | `server start` | Start server |
|
|
192
|
+
| | `server stop` | Stop server |
|
|
193
|
+
| | `server status` | Check server status |
|
|
194
|
+
| | `server logs` | View logs |
|
|
195
|
+
| | `server show-invite` | Display invite code |
|
|
196
|
+
| | `server doctor` | Diagnose issues |
|
|
197
|
+
| **Auth** | `login` | Authenticate to server |
|
|
198
|
+
| | `logout` | Remove stored profile |
|
|
199
|
+
| | `show-config` | Display configuration |
|
|
200
|
+
| **MCP** | `mcp` | Start MCP proxy |
|
|
201
|
+
| **Skills** | `skill workspace-tree` | Get folder structure |
|
|
202
|
+
| | `skill search-notes` | Search notes |
|
|
203
|
+
| | `skill note-by-id` | Get note by ID |
|
|
204
|
+
| | `skill create-folder` | Create folder |
|
|
205
|
+
| | `skill create-note` | Create note |
|
|
206
|
+
| | `skill update-note-content` | Update note |
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
65
210
|
## Configuration
|
|
66
211
|
|
|
67
|
-
Profiles are stored in `~/.kontexted/
|
|
212
|
+
Profiles are stored in `~/.kontexted/config.json` with OAuth tokens.
|
|
213
|
+
|
|
214
|
+
---
|
|
68
215
|
|
|
69
216
|
## Requirements
|
|
70
217
|
|
|
71
218
|
- Node.js 18 or higher
|
|
72
219
|
|
|
220
|
+
---
|
|
221
|
+
|
|
73
222
|
## License
|
|
74
223
|
|
|
75
224
|
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* Handler for the sync conflicts list command
|
|
4
|
+
*/
|
|
5
|
+
export declare function handler(argv: {
|
|
6
|
+
json?: boolean;
|
|
7
|
+
dir?: string;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
export declare const command = "list";
|
|
10
|
+
export declare const desc = "List unresolved conflicts";
|
|
11
|
+
export declare const builder: () => void;
|
|
12
|
+
export declare function handlerYargs(argv: any): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Register the sync conflicts list command.
|
|
15
|
+
*/
|
|
16
|
+
export declare function registerConflictsListCommand(conflictsCommand: Command): void;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Default sync directory name
|
|
5
|
+
*/
|
|
6
|
+
const DEFAULT_SYNC_DIR = ".kontexted";
|
|
7
|
+
/**
|
|
8
|
+
* Find the sync directory by looking for .kontexted/ or using --dir option
|
|
9
|
+
*/
|
|
10
|
+
async function findSyncDir(cwd, dirArg) {
|
|
11
|
+
// If --dir was provided, use it
|
|
12
|
+
if (dirArg) {
|
|
13
|
+
const syncDir = path.resolve(cwd, dirArg);
|
|
14
|
+
try {
|
|
15
|
+
await fs.access(syncDir);
|
|
16
|
+
return syncDir;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
console.error(`Error: Directory not found: ${syncDir}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Otherwise, look for .kontexted/ in current directory
|
|
24
|
+
const defaultSyncDir = path.join(cwd, DEFAULT_SYNC_DIR);
|
|
25
|
+
try {
|
|
26
|
+
await fs.access(defaultSyncDir);
|
|
27
|
+
return defaultSyncDir;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
console.error(`Error: Sync directory not found.`);
|
|
31
|
+
console.error(`Expected to find '${DEFAULT_SYNC_DIR}/' in current directory or specify --dir option.`);
|
|
32
|
+
console.error(`Run 'kontexted sync init' first to initialize sync.`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Read and parse conflicts.log to get all conflict entries
|
|
38
|
+
*/
|
|
39
|
+
async function getConflicts(syncDir) {
|
|
40
|
+
const logPath = path.join(syncDir, ".sync", "conflicts.log");
|
|
41
|
+
try {
|
|
42
|
+
const content = await fs.readFile(logPath, "utf-8");
|
|
43
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
44
|
+
return lines.map((line) => JSON.parse(line));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Format timestamp for display
|
|
52
|
+
*/
|
|
53
|
+
function formatTimestamp(isoString) {
|
|
54
|
+
const date = new Date(isoString);
|
|
55
|
+
return date.toISOString().replace("T", " ").slice(0, 19) + " UTC";
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Display conflicts in human-readable format
|
|
59
|
+
*/
|
|
60
|
+
function displayConflicts(conflicts) {
|
|
61
|
+
if (conflicts.length === 0) {
|
|
62
|
+
console.log("No conflicts found.");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
console.log("Conflicts:");
|
|
66
|
+
for (let i = 0; i < conflicts.length; i++) {
|
|
67
|
+
const conflict = conflicts[i];
|
|
68
|
+
const winnerText = conflict.winner === "local" ? "local wins" : "remote wins";
|
|
69
|
+
const timestamp = formatTimestamp(conflict.timestamp);
|
|
70
|
+
console.log(` ${i + 1}. ${conflict.filePath} (${winnerText}, ${timestamp})`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Display conflicts in JSON format
|
|
75
|
+
*/
|
|
76
|
+
function displayConflictsJson(conflicts) {
|
|
77
|
+
const output = conflicts.map((conflict, index) => ({
|
|
78
|
+
id: index + 1,
|
|
79
|
+
filePath: conflict.filePath,
|
|
80
|
+
winner: conflict.winner,
|
|
81
|
+
timestamp: conflict.timestamp,
|
|
82
|
+
localMtime: conflict.localMtime,
|
|
83
|
+
remoteMtime: conflict.remoteMtime,
|
|
84
|
+
}));
|
|
85
|
+
console.log(JSON.stringify(output, null, 2));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Handler for the sync conflicts list command
|
|
89
|
+
*/
|
|
90
|
+
export async function handler(argv) {
|
|
91
|
+
const cwd = process.cwd();
|
|
92
|
+
// Find the sync directory
|
|
93
|
+
const syncDir = await findSyncDir(cwd, argv.dir);
|
|
94
|
+
// Get conflicts
|
|
95
|
+
const conflicts = await getConflicts(syncDir);
|
|
96
|
+
// Display output
|
|
97
|
+
if (argv.json) {
|
|
98
|
+
displayConflictsJson(conflicts);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
displayConflicts(conflicts);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// ============ Yargs Command Module ============
|
|
105
|
+
export const command = "list";
|
|
106
|
+
export const desc = "List unresolved conflicts";
|
|
107
|
+
export const builder = () => { };
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
109
|
+
export async function handlerYargs(argv) {
|
|
110
|
+
await handler({
|
|
111
|
+
json: argv.json,
|
|
112
|
+
dir: argv.dir,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// ============ Register with Commander ============
|
|
116
|
+
/**
|
|
117
|
+
* Register the sync conflicts list command.
|
|
118
|
+
*/
|
|
119
|
+
export function registerConflictsListCommand(conflictsCommand) {
|
|
120
|
+
conflictsCommand
|
|
121
|
+
.command("list")
|
|
122
|
+
.description(desc)
|
|
123
|
+
.option("--dir <directory>", "Sync directory")
|
|
124
|
+
.option("--json", "Output as JSON")
|
|
125
|
+
.action(async (opts) => {
|
|
126
|
+
await handlerYargs({
|
|
127
|
+
json: opts.json,
|
|
128
|
+
dir: opts.dir,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* Handler for the sync conflicts resolve command
|
|
4
|
+
*/
|
|
5
|
+
export declare function handler(argv: {
|
|
6
|
+
id: string;
|
|
7
|
+
keep: "local" | "remote";
|
|
8
|
+
dir?: string;
|
|
9
|
+
}): Promise<void>;
|
|
10
|
+
export declare const command = "resolve";
|
|
11
|
+
export declare const desc = "Manually resolve conflict";
|
|
12
|
+
export declare const builder: () => void;
|
|
13
|
+
export declare function handlerYargs(argv: any): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Register the sync conflicts resolve command.
|
|
16
|
+
*/
|
|
17
|
+
export declare function registerConflictsResolveCommand(conflictsCommand: Command): void;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Default sync directory name
|
|
5
|
+
*/
|
|
6
|
+
const DEFAULT_SYNC_DIR = ".kontexted";
|
|
7
|
+
/**
|
|
8
|
+
* Find the sync directory by looking for .kontexted/ or using --dir option
|
|
9
|
+
*/
|
|
10
|
+
async function findSyncDir(cwd, dirArg) {
|
|
11
|
+
// If --dir was provided, use it
|
|
12
|
+
if (dirArg) {
|
|
13
|
+
const syncDir = path.resolve(cwd, dirArg);
|
|
14
|
+
try {
|
|
15
|
+
await fs.access(syncDir);
|
|
16
|
+
return syncDir;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
console.error(`Error: Directory not found: ${syncDir}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Otherwise, look for .kontexted/ in current directory
|
|
24
|
+
const defaultSyncDir = path.join(cwd, DEFAULT_SYNC_DIR);
|
|
25
|
+
try {
|
|
26
|
+
await fs.access(defaultSyncDir);
|
|
27
|
+
return defaultSyncDir;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
console.error(`Error: Sync directory not found.`);
|
|
31
|
+
console.error(`Expected to find '${DEFAULT_SYNC_DIR}/' in current directory or specify --dir option.`);
|
|
32
|
+
console.error(`Run 'kontexted sync init' first to initialize sync.`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Read and parse conflicts.log to get all conflict entries
|
|
38
|
+
*/
|
|
39
|
+
async function getConflicts(syncDir) {
|
|
40
|
+
const logPath = path.join(syncDir, ".sync", "conflicts.log");
|
|
41
|
+
try {
|
|
42
|
+
const content = await fs.readFile(logPath, "utf-8");
|
|
43
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
44
|
+
return lines.map((line) => JSON.parse(line));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Write conflicts back to the log file
|
|
52
|
+
*/
|
|
53
|
+
async function writeConflicts(syncDir, conflicts) {
|
|
54
|
+
const logPath = path.join(syncDir, ".sync", "conflicts.log");
|
|
55
|
+
const content = conflicts
|
|
56
|
+
.map((entry) => JSON.stringify(entry))
|
|
57
|
+
.join("\n");
|
|
58
|
+
await fs.writeFile(logPath, content, "utf-8");
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Load sync state from .sync/state.json
|
|
62
|
+
*/
|
|
63
|
+
async function loadSyncState(syncDir) {
|
|
64
|
+
const statePath = path.join(syncDir, ".sync", "state.json");
|
|
65
|
+
try {
|
|
66
|
+
const stateRaw = await fs.readFile(statePath, "utf-8");
|
|
67
|
+
return JSON.parse(stateRaw);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Save sync state to .sync/state.json
|
|
78
|
+
*/
|
|
79
|
+
async function saveSyncState(syncDir, state) {
|
|
80
|
+
const statePath = path.join(syncDir, ".sync", "state.json");
|
|
81
|
+
await fs.writeFile(statePath, JSON.stringify(state, null, 2), "utf-8");
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Handler for the sync conflicts resolve command
|
|
85
|
+
*/
|
|
86
|
+
export async function handler(argv) {
|
|
87
|
+
const cwd = process.cwd();
|
|
88
|
+
// Find the sync directory
|
|
89
|
+
const syncDir = await findSyncDir(cwd, argv.dir);
|
|
90
|
+
// Parse the conflict ID (1-based index)
|
|
91
|
+
const conflictId = parseInt(argv.id, 10);
|
|
92
|
+
if (isNaN(conflictId) || conflictId < 1) {
|
|
93
|
+
console.error(`Error: Invalid conflict ID: ${argv.id}`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
// Get conflicts
|
|
97
|
+
const conflicts = await getConflicts(syncDir);
|
|
98
|
+
// Find the conflict by ID (convert to 0-based index)
|
|
99
|
+
const conflictIndex = conflictId - 1;
|
|
100
|
+
if (conflictIndex < 0 || conflictIndex >= conflicts.length) {
|
|
101
|
+
console.error(`Error: Conflict not found: ${argv.id}`);
|
|
102
|
+
console.error(`Total conflicts: ${conflicts.length}`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
const conflict = conflicts[conflictIndex];
|
|
106
|
+
// Resolve the conflict
|
|
107
|
+
const localFilePath = path.join(syncDir, conflict.filePath);
|
|
108
|
+
if (argv.keep === "local") {
|
|
109
|
+
// Keep local version - already in place, just remove conflict entry
|
|
110
|
+
console.log(`Resolved conflict #${conflictId}: keeping local version`);
|
|
111
|
+
console.log(` File: ${conflict.filePath}`);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// Keep remote version - copy loser (shadow) to main file
|
|
115
|
+
const loserPath = path.join(syncDir, conflict.loserPath);
|
|
116
|
+
const loserContent = await fs.readFile(loserPath, "utf-8");
|
|
117
|
+
await fs.writeFile(localFilePath, loserContent, "utf-8");
|
|
118
|
+
console.log(`Resolved conflict #${conflictId}: keeping remote version`);
|
|
119
|
+
console.log(` File: ${conflict.filePath}`);
|
|
120
|
+
console.log(` Copied from: ${conflict.loserPath}`);
|
|
121
|
+
}
|
|
122
|
+
// Update sync state - mark file as synced with the new hash
|
|
123
|
+
let state = await loadSyncState(syncDir);
|
|
124
|
+
if (state) {
|
|
125
|
+
// Get the new content hash
|
|
126
|
+
const newContent = await fs.readFile(localFilePath, "utf-8");
|
|
127
|
+
const newHash = await hashContent(newContent);
|
|
128
|
+
// Update the file state
|
|
129
|
+
if (state.files[conflict.filePath]) {
|
|
130
|
+
state.files[conflict.filePath].localHash = newHash;
|
|
131
|
+
state.files[conflict.filePath].remoteHash = newHash;
|
|
132
|
+
await saveSyncState(syncDir, state);
|
|
133
|
+
console.log(` Updated sync state for ${conflict.filePath}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Remove the conflict from the log
|
|
137
|
+
conflicts.splice(conflictIndex, 1);
|
|
138
|
+
await writeConflicts(syncDir, conflicts);
|
|
139
|
+
console.log(` Conflict removed from conflicts.log`);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Simple hash function for content (SHA-256 via crypto)
|
|
143
|
+
*/
|
|
144
|
+
async function hashContent(content) {
|
|
145
|
+
const crypto = await import("node:crypto");
|
|
146
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
147
|
+
}
|
|
148
|
+
// ============ Yargs Command Module ============
|
|
149
|
+
export const command = "resolve";
|
|
150
|
+
export const desc = "Manually resolve conflict";
|
|
151
|
+
export const builder = () => { };
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
153
|
+
export async function handlerYargs(argv) {
|
|
154
|
+
await handler({
|
|
155
|
+
id: argv.id,
|
|
156
|
+
keep: argv.keep,
|
|
157
|
+
dir: argv.dir,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
// ============ Register with Commander ============
|
|
161
|
+
/**
|
|
162
|
+
* Register the sync conflicts resolve command.
|
|
163
|
+
*/
|
|
164
|
+
export function registerConflictsResolveCommand(conflictsCommand) {
|
|
165
|
+
conflictsCommand
|
|
166
|
+
.command("resolve <id>")
|
|
167
|
+
.description(desc)
|
|
168
|
+
.requiredOption("--keep <local|remote>", "Which version to keep: 'local' or 'remote'")
|
|
169
|
+
.option("--dir <directory>", "Sync directory")
|
|
170
|
+
.action(async (id, opts) => {
|
|
171
|
+
if (!opts.keep) {
|
|
172
|
+
console.error("Error: --keep option is required");
|
|
173
|
+
console.error("Usage: kontexted sync conflicts resolve <id> --keep <local|remote>");
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
if (opts.keep !== "local" && opts.keep !== "remote") {
|
|
177
|
+
console.error("Error: --keep must be 'local' or 'remote'");
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
await handlerYargs({
|
|
181
|
+
id,
|
|
182
|
+
keep: opts.keep,
|
|
183
|
+
dir: opts.dir,
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* Handler for the sync conflicts show command
|
|
4
|
+
*/
|
|
5
|
+
export declare function handler(argv: {
|
|
6
|
+
id: string;
|
|
7
|
+
dir?: string;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
export declare const command = "show";
|
|
10
|
+
export declare const desc = "Show details of a conflict";
|
|
11
|
+
export declare const builder: () => void;
|
|
12
|
+
export declare function handlerYargs(argv: any): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Register the sync conflicts show command.
|
|
15
|
+
*/
|
|
16
|
+
export declare function registerConflictsShowCommand(conflictsCommand: Command): void;
|