@mod-computer/cli 0.1.1 → 0.2.1
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 +72 -0
- package/dist/cli.bundle.js +23743 -12931
- package/dist/cli.bundle.js.map +4 -4
- package/dist/cli.js +23 -12
- package/dist/commands/add.js +245 -0
- package/dist/commands/auth.js +129 -21
- package/dist/commands/comment.js +568 -0
- package/dist/commands/diff.js +182 -0
- package/dist/commands/index.js +33 -3
- package/dist/commands/init.js +475 -221
- package/dist/commands/ls.js +135 -0
- package/dist/commands/members.js +687 -0
- package/dist/commands/mv.js +282 -0
- package/dist/commands/rm.js +257 -0
- package/dist/commands/status.js +273 -306
- package/dist/commands/sync.js +99 -75
- package/dist/commands/trace.js +1752 -0
- package/dist/commands/workspace.js +354 -330
- package/dist/config/features.js +8 -3
- package/dist/config/release-profiles/development.json +4 -1
- package/dist/config/release-profiles/mvp.json +4 -2
- package/dist/daemon/conflict-resolution.js +172 -0
- package/dist/daemon/content-hash.js +31 -0
- package/dist/daemon/file-sync.js +985 -0
- package/dist/daemon/index.js +203 -0
- package/dist/daemon/mime-types.js +166 -0
- package/dist/daemon/offline-queue.js +211 -0
- package/dist/daemon/path-utils.js +64 -0
- package/dist/daemon/share-policy.js +83 -0
- package/dist/daemon/wasm-errors.js +189 -0
- package/dist/daemon/worker.js +557 -0
- package/dist/daemon-worker.js +3 -2
- package/dist/errors/workspace-errors.js +48 -0
- package/dist/lib/auth-server.js +89 -26
- package/dist/lib/browser.js +1 -1
- package/dist/lib/diff.js +284 -0
- package/dist/lib/formatters.js +204 -0
- package/dist/lib/git.js +137 -0
- package/dist/lib/local-fs.js +201 -0
- package/dist/lib/prompts.js +23 -83
- package/dist/lib/storage.js +11 -1
- package/dist/lib/trace-formatters.js +314 -0
- package/dist/services/add-service.js +554 -0
- package/dist/services/add-validation.js +124 -0
- package/dist/services/mod-config.js +8 -2
- package/dist/services/modignore-service.js +2 -0
- package/dist/stores/use-workspaces-store.js +36 -14
- package/dist/types/add-types.js +99 -0
- package/dist/types/config.js +1 -1
- package/dist/types/workspace-connection.js +53 -2
- package/package.json +7 -5
- package/commands/execute.md +0 -156
- package/commands/overview.md +0 -233
- package/commands/review.md +0 -151
- package/commands/spec.md +0 -169
package/dist/config/features.js
CHANGED
|
@@ -11,7 +11,9 @@ export const FEATURES = {
|
|
|
11
11
|
SYNC_OPERATIONS: 'sync-operations',
|
|
12
12
|
WATCH_OPERATIONS: 'watch-operations',
|
|
13
13
|
CONNECTOR_INTEGRATIONS: 'connector-integrations',
|
|
14
|
-
AUTH: 'auth'
|
|
14
|
+
AUTH: 'auth',
|
|
15
|
+
MEMBER_MANAGEMENT: 'member-management',
|
|
16
|
+
TRACING: 'tracing'
|
|
15
17
|
};
|
|
16
18
|
let releaseProfile = null;
|
|
17
19
|
export function isFeatureEnabled(feature) {
|
|
@@ -28,7 +30,7 @@ export function isFeatureEnabled(feature) {
|
|
|
28
30
|
return releaseProfile[feature] ?? false;
|
|
29
31
|
}
|
|
30
32
|
function loadReleaseProfile() {
|
|
31
|
-
const profileName = process.env.MOD_RELEASE_PROFILE || '
|
|
33
|
+
const profileName = process.env.MOD_RELEASE_PROFILE || 'mvp';
|
|
32
34
|
try {
|
|
33
35
|
const __filename = fileURLToPath(import.meta.url);
|
|
34
36
|
const __dirname = path.dirname(__filename);
|
|
@@ -50,7 +52,10 @@ function loadReleaseProfile() {
|
|
|
50
52
|
[FEATURES.AGENT_INTEGRATIONS]: true,
|
|
51
53
|
[FEATURES.SYNC_OPERATIONS]: true,
|
|
52
54
|
[FEATURES.WATCH_OPERATIONS]: true,
|
|
53
|
-
[FEATURES.CONNECTOR_INTEGRATIONS]: true
|
|
55
|
+
[FEATURES.CONNECTOR_INTEGRATIONS]: true,
|
|
56
|
+
[FEATURES.AUTH]: true,
|
|
57
|
+
[FEATURES.MEMBER_MANAGEMENT]: true,
|
|
58
|
+
[FEATURES.TRACING]: true
|
|
54
59
|
};
|
|
55
60
|
}
|
|
56
61
|
// Fallback to minimal profile for unknown profiles
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
"workspace-management": true,
|
|
4
4
|
"workspace-branching": false,
|
|
5
5
|
"task-management": false,
|
|
6
|
-
"file-operations":
|
|
6
|
+
"file-operations": true,
|
|
7
7
|
"agent-integrations": false,
|
|
8
8
|
"sync-operations": true,
|
|
9
9
|
"watch-operations": true,
|
|
10
10
|
"connector-integrations": false,
|
|
11
|
-
"auth": true
|
|
11
|
+
"auth": true,
|
|
12
|
+
"member-management": true,
|
|
13
|
+
"tracing": true
|
|
12
14
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// glassware[type="implementation", id="impl-conflict-resolution--323bddc9", requirements="requirement-cli-sync-conflict-lww--626f73f4,requirement-cli-sync-conflict-log--7ac92a94"]
|
|
2
|
+
/**
|
|
3
|
+
* Conflict resolution for bidirectional sync.
|
|
4
|
+
* Implements last-write-wins strategy with conflict logging.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { getLogsDir } from '../lib/storage.js';
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a conflict between local and remote file states.
|
|
11
|
+
* Uses last-write-wins strategy with local preference for concurrent edits.
|
|
12
|
+
*
|
|
13
|
+
* @param local - Local file state
|
|
14
|
+
* @param remote - Remote file state (from Automerge doc)
|
|
15
|
+
* @returns Resolution decision
|
|
16
|
+
*/
|
|
17
|
+
export function resolveConflict(local, remote) {
|
|
18
|
+
// If content hashes match, no conflict (same content)
|
|
19
|
+
if (local.contentHash && remote.contentHash && local.contentHash === remote.contentHash) {
|
|
20
|
+
return {
|
|
21
|
+
resolution: 'use-local', // Doesn't matter, they're the same
|
|
22
|
+
reason: 'Content identical (same hash)',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
// If local file hasn't been modified since last sync, use remote
|
|
26
|
+
// Check both timestamp AND content hash difference to handle cases where
|
|
27
|
+
// remote metadata.updatedAt isn't updated (e.g., web editor edits)
|
|
28
|
+
if (!local.modified) {
|
|
29
|
+
// Local hasn't changed, so any remote difference should be applied
|
|
30
|
+
if (remote.updatedAt > local.lastSyncedAt) {
|
|
31
|
+
return {
|
|
32
|
+
resolution: 'use-remote',
|
|
33
|
+
reason: 'Local unchanged, remote updated (by timestamp)',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// Even if timestamp isn't newer, if content is different, use remote
|
|
37
|
+
// This handles cases where the web editor doesn't update metadata.updatedAt
|
|
38
|
+
if (local.contentHash && remote.contentHash && local.contentHash !== remote.contentHash) {
|
|
39
|
+
return {
|
|
40
|
+
resolution: 'use-remote',
|
|
41
|
+
reason: 'Local unchanged, remote content differs',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// If local was modified and remote hasn't changed since last sync, use local
|
|
46
|
+
if (local.modified && remote.updatedAt <= local.lastSyncedAt) {
|
|
47
|
+
return {
|
|
48
|
+
resolution: 'use-local',
|
|
49
|
+
reason: 'Local modified, remote unchanged since last sync',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// Both modified concurrently - local wins (user's active work takes precedence)
|
|
53
|
+
// Future enhancement: implement actual merge for text files
|
|
54
|
+
return {
|
|
55
|
+
resolution: 'use-local',
|
|
56
|
+
reason: 'Both modified concurrently, local wins (user active work)',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get the path to the conflict log file.
|
|
61
|
+
*/
|
|
62
|
+
export function getConflictLogPath() {
|
|
63
|
+
return path.join(getLogsDir(), 'conflicts.log');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Log a conflict record to the conflicts log file.
|
|
67
|
+
*/
|
|
68
|
+
export function logConflict(conflict) {
|
|
69
|
+
const logPath = getConflictLogPath();
|
|
70
|
+
const logDir = path.dirname(logPath);
|
|
71
|
+
// Ensure log directory exists
|
|
72
|
+
if (!fs.existsSync(logDir)) {
|
|
73
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
const line = `${conflict.timestamp} [${conflict.resolution.toUpperCase()}] ${conflict.path} ` +
|
|
76
|
+
`(local: ${conflict.localTimestamp}, remote: ${conflict.remoteTimestamp}) - ${conflict.reason}\n`;
|
|
77
|
+
fs.appendFileSync(logPath, line);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Create a conflict record and log it.
|
|
81
|
+
*/
|
|
82
|
+
export function recordConflict(local, remote, resolution, reason) {
|
|
83
|
+
const record = {
|
|
84
|
+
path: local.path,
|
|
85
|
+
localTimestamp: local.localMtime,
|
|
86
|
+
remoteTimestamp: remote.updatedAt,
|
|
87
|
+
resolution,
|
|
88
|
+
timestamp: new Date().toISOString(),
|
|
89
|
+
reason,
|
|
90
|
+
};
|
|
91
|
+
logConflict(record);
|
|
92
|
+
return record;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Read recent conflicts from the log file.
|
|
96
|
+
* @param limit - Maximum number of conflicts to return
|
|
97
|
+
* @returns Array of conflict records (most recent first)
|
|
98
|
+
*/
|
|
99
|
+
export function readRecentConflicts(limit = 100) {
|
|
100
|
+
const logPath = getConflictLogPath();
|
|
101
|
+
if (!fs.existsSync(logPath)) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const content = fs.readFileSync(logPath, 'utf-8');
|
|
106
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
107
|
+
// Parse lines and return most recent first
|
|
108
|
+
return lines
|
|
109
|
+
.slice(-limit)
|
|
110
|
+
.reverse()
|
|
111
|
+
.map(parseLine)
|
|
112
|
+
.filter((record) => record !== null);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.warn('[conflict-resolution] Could not read conflict log');
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Parse a conflict log line back into a record.
|
|
121
|
+
* Format: TIMESTAMP [RESOLUTION] PATH (local: TIMESTAMP, remote: TIMESTAMP) - REASON
|
|
122
|
+
*/
|
|
123
|
+
function parseLine(line) {
|
|
124
|
+
const match = line.match(/^(\S+) \[(\w+-\w+)\] (.+?) \(local: ([^,]+), remote: ([^)]+)\) - (.+)$/);
|
|
125
|
+
if (!match) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
timestamp: match[1],
|
|
130
|
+
resolution: match[2],
|
|
131
|
+
path: match[3],
|
|
132
|
+
localTimestamp: match[4],
|
|
133
|
+
remoteTimestamp: match[5],
|
|
134
|
+
reason: match[6],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* ConflictResolver - manages conflict resolution for a sync session.
|
|
139
|
+
*/
|
|
140
|
+
export class ConflictResolver {
|
|
141
|
+
constructor(logger = console.log) {
|
|
142
|
+
this.conflictCount = 0;
|
|
143
|
+
this.log = logger;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Check and resolve a potential conflict between local and remote states.
|
|
147
|
+
* @returns The resolution decision and whether a conflict was detected
|
|
148
|
+
*/
|
|
149
|
+
checkAndResolve(local, remote) {
|
|
150
|
+
const { resolution, reason } = resolveConflict(local, remote);
|
|
151
|
+
// It's only a "conflict" if both sides were modified
|
|
152
|
+
const isConflict = local.modified && remote.updatedAt > local.lastSyncedAt;
|
|
153
|
+
if (isConflict) {
|
|
154
|
+
this.conflictCount++;
|
|
155
|
+
recordConflict(local, remote, resolution, reason);
|
|
156
|
+
this.log(`[conflict] ${local.path}: ${reason} → ${resolution}`);
|
|
157
|
+
}
|
|
158
|
+
return { resolution, reason, isConflict };
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Get the number of conflicts detected in this session.
|
|
162
|
+
*/
|
|
163
|
+
getConflictCount() {
|
|
164
|
+
return this.conflictCount;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Reset the conflict count (e.g., at start of new sync session).
|
|
168
|
+
*/
|
|
169
|
+
resetCount() {
|
|
170
|
+
this.conflictCount = 0;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// glassware[type="implementation", id="impl-content-hash--dba4e6e0", requirements="requirement-cli-sync-content-hash--cc8aff1f"]
|
|
2
|
+
/**
|
|
3
|
+
* Content hash utilities for change detection.
|
|
4
|
+
* Uses SHA-256 to compute content hashes for efficient change detection.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from 'crypto';
|
|
7
|
+
/**
|
|
8
|
+
* Compute SHA-256 hash of content for change detection.
|
|
9
|
+
* Returns hex-encoded hash string.
|
|
10
|
+
*/
|
|
11
|
+
export function computeContentHash(content) {
|
|
12
|
+
const hash = createHash('sha256');
|
|
13
|
+
if (typeof content === 'string') {
|
|
14
|
+
hash.update(content, 'utf-8');
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
hash.update(content);
|
|
18
|
+
}
|
|
19
|
+
return hash.digest('hex');
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Check if content has changed by comparing hashes.
|
|
23
|
+
* @returns true if content has changed, false if unchanged
|
|
24
|
+
*/
|
|
25
|
+
export function hasContentChanged(newContent, existingHash) {
|
|
26
|
+
if (!existingHash) {
|
|
27
|
+
return true; // No existing hash means it's new
|
|
28
|
+
}
|
|
29
|
+
const newHash = computeContentHash(newContent);
|
|
30
|
+
return newHash !== existingHash;
|
|
31
|
+
}
|