@mod-computer/cli 0.2.3 → 0.2.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/dist/cli.bundle.js +216 -36371
- package/package.json +3 -3
- package/dist/app.js +0 -227
- package/dist/cli.bundle.js.map +0 -7
- package/dist/cli.js +0 -132
- package/dist/commands/add.js +0 -245
- package/dist/commands/agents-run.js +0 -71
- package/dist/commands/auth.js +0 -259
- package/dist/commands/branch.js +0 -1411
- package/dist/commands/claude-sync.js +0 -772
- package/dist/commands/comment.js +0 -568
- package/dist/commands/diff.js +0 -182
- package/dist/commands/index.js +0 -73
- package/dist/commands/init.js +0 -597
- package/dist/commands/ls.js +0 -135
- package/dist/commands/members.js +0 -687
- package/dist/commands/mv.js +0 -282
- package/dist/commands/recover.js +0 -207
- package/dist/commands/rm.js +0 -257
- package/dist/commands/spec.js +0 -386
- package/dist/commands/status.js +0 -296
- package/dist/commands/sync.js +0 -119
- package/dist/commands/trace.js +0 -1752
- package/dist/commands/workspace.js +0 -447
- package/dist/components/conflict-resolution-ui.js +0 -120
- package/dist/components/messages.js +0 -5
- package/dist/components/thread.js +0 -8
- package/dist/config/features.js +0 -83
- package/dist/containers/branches-container.js +0 -140
- package/dist/containers/directory-container.js +0 -92
- package/dist/containers/thread-container.js +0 -214
- package/dist/containers/threads-container.js +0 -27
- package/dist/containers/workspaces-container.js +0 -27
- package/dist/daemon/conflict-resolution.js +0 -172
- package/dist/daemon/content-hash.js +0 -31
- package/dist/daemon/file-sync.js +0 -985
- package/dist/daemon/index.js +0 -203
- package/dist/daemon/mime-types.js +0 -166
- package/dist/daemon/offline-queue.js +0 -211
- package/dist/daemon/path-utils.js +0 -64
- package/dist/daemon/share-policy.js +0 -83
- package/dist/daemon/wasm-errors.js +0 -189
- package/dist/daemon/worker.js +0 -557
- package/dist/daemon-worker.js +0 -258
- package/dist/errors/workspace-errors.js +0 -48
- package/dist/lib/auth-server.js +0 -216
- package/dist/lib/browser.js +0 -35
- package/dist/lib/diff.js +0 -284
- package/dist/lib/formatters.js +0 -204
- package/dist/lib/git.js +0 -137
- package/dist/lib/local-fs.js +0 -201
- package/dist/lib/prompts.js +0 -56
- package/dist/lib/storage.js +0 -213
- package/dist/lib/trace-formatters.js +0 -314
- package/dist/services/add-service.js +0 -554
- package/dist/services/add-validation.js +0 -124
- package/dist/services/automatic-file-tracker.js +0 -303
- package/dist/services/cli-orchestrator.js +0 -227
- package/dist/services/feature-flags.js +0 -187
- package/dist/services/file-import-service.js +0 -283
- package/dist/services/file-transformation-service.js +0 -218
- package/dist/services/logger.js +0 -44
- package/dist/services/mod-config.js +0 -67
- package/dist/services/modignore-service.js +0 -328
- package/dist/services/sync-daemon.js +0 -244
- package/dist/services/thread-notification-service.js +0 -50
- package/dist/services/thread-service.js +0 -147
- package/dist/stores/use-directory-store.js +0 -96
- package/dist/stores/use-threads-store.js +0 -46
- package/dist/stores/use-workspaces-store.js +0 -54
- package/dist/types/add-types.js +0 -99
- package/dist/types/config.js +0 -16
- package/dist/types/index.js +0 -2
- package/dist/types/workspace-connection.js +0 -53
- package/dist/types.js +0 -1
package/dist/daemon/index.js
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
// glassware[type="implementation", id="cli-daemon-manager--4c5e461e", requirements="requirement-cli-sync-app-1--6a95c316,requirement-cli-sync-app-2--849a3392,requirement-cli-sync-infra-3--e8b62ac3,requirement-cli-storage-pid-1--ab7339a0,requirement-cli-storage-pid-2--a78c87bb,requirement-cli-storage-pid-3--48975e4c,requirement-cli-storage-pid-4--35d0f765"]
|
|
2
|
-
// glassware[type="implementation", id="impl-daemon-manager-distributed--f5e77099", specifications="specification-spec-sync-start--cd4dcfee,specification-spec-sync-stop--ea84706e,specification-spec-status-command--0fb19514,specification-spec-status-states--3f110d43,specification-spec-status-timestamp--120c109a"]
|
|
3
|
-
/**
|
|
4
|
-
* Sync daemon management - handles start/stop/status for background sync process.
|
|
5
|
-
* Uses device-level state in ~/.mod/
|
|
6
|
-
*/
|
|
7
|
-
import fs from 'fs';
|
|
8
|
-
import path from 'path';
|
|
9
|
-
import { spawn } from 'child_process';
|
|
10
|
-
import { fileURLToPath } from 'url';
|
|
11
|
-
import { getPidFilePath, getLogsDir, getSyncLogPath, listWorkspaceConnections, readConfig, ensureModDir, } from '../lib/storage.js';
|
|
12
|
-
import { getGitBranch } from '../lib/git.js';
|
|
13
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
-
const __dirname = path.dirname(__filename);
|
|
15
|
-
/**
|
|
16
|
-
* Check if daemon is currently running.
|
|
17
|
-
*/
|
|
18
|
-
export function isDaemonRunning() {
|
|
19
|
-
const pidPath = getPidFilePath();
|
|
20
|
-
if (!fs.existsSync(pidPath)) {
|
|
21
|
-
return { running: false };
|
|
22
|
-
}
|
|
23
|
-
try {
|
|
24
|
-
const pidStr = fs.readFileSync(pidPath, 'utf-8').trim();
|
|
25
|
-
const pid = parseInt(pidStr, 10);
|
|
26
|
-
if (isNaN(pid)) {
|
|
27
|
-
// Corrupted PID file, clean up
|
|
28
|
-
fs.unlinkSync(pidPath);
|
|
29
|
-
return { running: false };
|
|
30
|
-
}
|
|
31
|
-
// Check if process is running
|
|
32
|
-
try {
|
|
33
|
-
process.kill(pid, 0);
|
|
34
|
-
return { running: true, pid };
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
// Process not running, clean up stale PID file
|
|
38
|
-
fs.unlinkSync(pidPath);
|
|
39
|
-
return { running: false };
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
return { running: false };
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Start the sync daemon.
|
|
48
|
-
*/
|
|
49
|
-
export async function startDaemon(options) {
|
|
50
|
-
ensureModDir();
|
|
51
|
-
// Check if already running
|
|
52
|
-
const { running, pid: existingPid } = isDaemonRunning();
|
|
53
|
-
if (running) {
|
|
54
|
-
return {
|
|
55
|
-
success: true,
|
|
56
|
-
pid: existingPid,
|
|
57
|
-
message: `Sync daemon already running (pid ${existingPid})`,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
// Check for workspace connections
|
|
61
|
-
const connections = listWorkspaceConnections();
|
|
62
|
-
if (connections.length === 0) {
|
|
63
|
-
return {
|
|
64
|
-
success: false,
|
|
65
|
-
message: 'No workspace connections found. Run `mod init` first.',
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
// Spawn daemon process
|
|
69
|
-
const daemonScript = path.join(__dirname, 'worker.js');
|
|
70
|
-
// Check if worker script exists
|
|
71
|
-
if (!fs.existsSync(daemonScript)) {
|
|
72
|
-
return {
|
|
73
|
-
success: false,
|
|
74
|
-
message: 'Daemon worker script not found. Build may be incomplete.',
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
const logPath = getSyncLogPath();
|
|
78
|
-
const logDir = getLogsDir();
|
|
79
|
-
// Ensure logs directory exists
|
|
80
|
-
if (!fs.existsSync(logDir)) {
|
|
81
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
82
|
-
}
|
|
83
|
-
// Open log file for daemon output
|
|
84
|
-
const logFd = fs.openSync(logPath, 'a');
|
|
85
|
-
const daemon = spawn(process.execPath, [daemonScript], {
|
|
86
|
-
detached: true,
|
|
87
|
-
stdio: options?.verbose ? 'inherit' : ['ignore', logFd, logFd],
|
|
88
|
-
env: {
|
|
89
|
-
...process.env,
|
|
90
|
-
MOD_DAEMON_MODE: 'true',
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
daemon.unref();
|
|
94
|
-
fs.closeSync(logFd);
|
|
95
|
-
if (!daemon.pid) {
|
|
96
|
-
return {
|
|
97
|
-
success: false,
|
|
98
|
-
message: 'Failed to spawn daemon process',
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
// Write PID file
|
|
102
|
-
fs.writeFileSync(getPidFilePath(), daemon.pid.toString());
|
|
103
|
-
// Check auth state for messaging
|
|
104
|
-
const config = readConfig();
|
|
105
|
-
const isAuthenticated = !!config.auth;
|
|
106
|
-
const directories = connections.map((c) => c.path).join(', ');
|
|
107
|
-
if (isAuthenticated) {
|
|
108
|
-
return {
|
|
109
|
-
success: true,
|
|
110
|
-
pid: daemon.pid,
|
|
111
|
-
message: `Sync daemon started (pid ${daemon.pid})\nWatching: ${directories}\nSyncing with collaborators`,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
return {
|
|
116
|
-
success: true,
|
|
117
|
-
pid: daemon.pid,
|
|
118
|
-
message: `Sync daemon started (pid ${daemon.pid})\nWatching: ${directories}\nTracking changes locally\n\nSign in to sync with collaborators: mod auth login`,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Stop the sync daemon.
|
|
124
|
-
*/
|
|
125
|
-
export async function stopDaemon(options) {
|
|
126
|
-
const { running, pid } = isDaemonRunning();
|
|
127
|
-
if (!running || !pid) {
|
|
128
|
-
return {
|
|
129
|
-
success: true,
|
|
130
|
-
message: 'Sync daemon not running',
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
try {
|
|
134
|
-
// Send SIGTERM for graceful shutdown
|
|
135
|
-
process.kill(pid, 'SIGTERM');
|
|
136
|
-
// Wait for process to exit (up to 5 seconds)
|
|
137
|
-
let attempts = 50;
|
|
138
|
-
while (attempts > 0) {
|
|
139
|
-
try {
|
|
140
|
-
process.kill(pid, 0);
|
|
141
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
142
|
-
attempts--;
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
// Process exited
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
// Force kill if still running
|
|
150
|
-
if (attempts === 0 && options?.force) {
|
|
151
|
-
try {
|
|
152
|
-
process.kill(pid, 'SIGKILL');
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
// Process already gone
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
// Clean up PID file
|
|
159
|
-
const pidPath = getPidFilePath();
|
|
160
|
-
if (fs.existsSync(pidPath)) {
|
|
161
|
-
fs.unlinkSync(pidPath);
|
|
162
|
-
}
|
|
163
|
-
return {
|
|
164
|
-
success: true,
|
|
165
|
-
message: 'Sync daemon stopped',
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
catch (error) {
|
|
169
|
-
return {
|
|
170
|
-
success: false,
|
|
171
|
-
message: `Failed to stop daemon: ${error.message}`,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
// glassware[type="implementation", id="impl-daemon-status-git-branch--80bdc2eb", requirements="requirement-cli-git-ux-1--01756c1e,requirement-cli-git-ux-3--6137e6b6"]
|
|
176
|
-
/**
|
|
177
|
-
* Get daemon status.
|
|
178
|
-
*/
|
|
179
|
-
export function getDaemonStatus() {
|
|
180
|
-
const { running, pid } = isDaemonRunning();
|
|
181
|
-
const connections = listWorkspaceConnections();
|
|
182
|
-
const config = readConfig();
|
|
183
|
-
let cloudSync;
|
|
184
|
-
if (!config.auth) {
|
|
185
|
-
cloudSync = 'not_signed_in';
|
|
186
|
-
}
|
|
187
|
-
else {
|
|
188
|
-
// TODO: Check actual WebSocket connection status
|
|
189
|
-
cloudSync = 'connected';
|
|
190
|
-
}
|
|
191
|
-
return {
|
|
192
|
-
running,
|
|
193
|
-
pid,
|
|
194
|
-
connections: connections.map((c) => ({
|
|
195
|
-
path: c.path,
|
|
196
|
-
workspaceName: c.workspaceName,
|
|
197
|
-
gitBranch: getGitBranch(c.path),
|
|
198
|
-
})),
|
|
199
|
-
cloudSync,
|
|
200
|
-
lastSync: connections[0]?.lastSyncedAt,
|
|
201
|
-
pendingChanges: 0, // TODO: Track pending changes
|
|
202
|
-
};
|
|
203
|
-
}
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
// glassware[type="implementation", id="impl-mime-types--c09c1b7f", requirements="requirement-cli-sync-mime-detect--a84702ed"]
|
|
2
|
-
/**
|
|
3
|
-
* MIME type detection for file sync.
|
|
4
|
-
* Maps file extensions to MIME types.
|
|
5
|
-
*/
|
|
6
|
-
import path from 'path';
|
|
7
|
-
/**
|
|
8
|
-
* MIME type mappings by file extension.
|
|
9
|
-
*/
|
|
10
|
-
export const MIME_TYPES = {
|
|
11
|
-
// Text
|
|
12
|
-
'.txt': 'text/plain',
|
|
13
|
-
'.md': 'text/markdown',
|
|
14
|
-
'.markdown': 'text/markdown',
|
|
15
|
-
// TypeScript/JavaScript
|
|
16
|
-
'.ts': 'text/typescript',
|
|
17
|
-
'.tsx': 'text/typescript',
|
|
18
|
-
'.js': 'text/javascript',
|
|
19
|
-
'.jsx': 'text/javascript',
|
|
20
|
-
'.mjs': 'text/javascript',
|
|
21
|
-
'.cjs': 'text/javascript',
|
|
22
|
-
// Web
|
|
23
|
-
'.html': 'text/html',
|
|
24
|
-
'.htm': 'text/html',
|
|
25
|
-
'.css': 'text/css',
|
|
26
|
-
'.scss': 'text/scss',
|
|
27
|
-
'.sass': 'text/sass',
|
|
28
|
-
'.less': 'text/less',
|
|
29
|
-
// Data/Config
|
|
30
|
-
'.json': 'application/json',
|
|
31
|
-
'.yaml': 'text/yaml',
|
|
32
|
-
'.yml': 'text/yaml',
|
|
33
|
-
'.toml': 'text/toml',
|
|
34
|
-
'.xml': 'application/xml',
|
|
35
|
-
'.svg': 'image/svg+xml',
|
|
36
|
-
// Programming languages
|
|
37
|
-
'.py': 'text/x-python',
|
|
38
|
-
'.rb': 'text/x-ruby',
|
|
39
|
-
'.go': 'text/x-go',
|
|
40
|
-
'.rs': 'text/x-rust',
|
|
41
|
-
'.java': 'text/x-java',
|
|
42
|
-
'.c': 'text/x-c',
|
|
43
|
-
'.cpp': 'text/x-c++',
|
|
44
|
-
'.cc': 'text/x-c++',
|
|
45
|
-
'.h': 'text/x-c',
|
|
46
|
-
'.hpp': 'text/x-c++',
|
|
47
|
-
'.cs': 'text/x-csharp',
|
|
48
|
-
'.swift': 'text/x-swift',
|
|
49
|
-
'.kt': 'text/x-kotlin',
|
|
50
|
-
'.scala': 'text/x-scala',
|
|
51
|
-
'.php': 'text/x-php',
|
|
52
|
-
// Shell/Scripts
|
|
53
|
-
'.sh': 'text/x-shellscript',
|
|
54
|
-
'.bash': 'text/x-shellscript',
|
|
55
|
-
'.zsh': 'text/x-shellscript',
|
|
56
|
-
'.fish': 'text/x-shellscript',
|
|
57
|
-
'.ps1': 'text/x-powershell',
|
|
58
|
-
'.bat': 'text/x-batch',
|
|
59
|
-
// Database/Query
|
|
60
|
-
'.sql': 'text/x-sql',
|
|
61
|
-
'.graphql': 'text/x-graphql',
|
|
62
|
-
'.gql': 'text/x-graphql',
|
|
63
|
-
// Documentation
|
|
64
|
-
'.rst': 'text/x-rst',
|
|
65
|
-
'.tex': 'text/x-latex',
|
|
66
|
-
'.adoc': 'text/asciidoc',
|
|
67
|
-
// Config files
|
|
68
|
-
'.env': 'text/plain',
|
|
69
|
-
'.ini': 'text/plain',
|
|
70
|
-
'.cfg': 'text/plain',
|
|
71
|
-
'.conf': 'text/plain',
|
|
72
|
-
'.gitignore': 'text/plain',
|
|
73
|
-
'.dockerignore': 'text/plain',
|
|
74
|
-
'.editorconfig': 'text/plain',
|
|
75
|
-
// Images
|
|
76
|
-
'.png': 'image/png',
|
|
77
|
-
'.jpg': 'image/jpeg',
|
|
78
|
-
'.jpeg': 'image/jpeg',
|
|
79
|
-
'.gif': 'image/gif',
|
|
80
|
-
'.webp': 'image/webp',
|
|
81
|
-
'.ico': 'image/x-icon',
|
|
82
|
-
'.bmp': 'image/bmp',
|
|
83
|
-
// Audio
|
|
84
|
-
'.mp3': 'audio/mpeg',
|
|
85
|
-
'.wav': 'audio/wav',
|
|
86
|
-
'.ogg': 'audio/ogg',
|
|
87
|
-
// Video
|
|
88
|
-
'.mp4': 'video/mp4',
|
|
89
|
-
'.webm': 'video/webm',
|
|
90
|
-
'.avi': 'video/x-msvideo',
|
|
91
|
-
// Documents
|
|
92
|
-
'.pdf': 'application/pdf',
|
|
93
|
-
// Archives
|
|
94
|
-
'.zip': 'application/zip',
|
|
95
|
-
'.tar': 'application/x-tar',
|
|
96
|
-
'.gz': 'application/gzip',
|
|
97
|
-
// Fonts
|
|
98
|
-
'.woff': 'font/woff',
|
|
99
|
-
'.woff2': 'font/woff2',
|
|
100
|
-
'.ttf': 'font/ttf',
|
|
101
|
-
'.otf': 'font/otf',
|
|
102
|
-
'.eot': 'application/vnd.ms-fontobject',
|
|
103
|
-
};
|
|
104
|
-
/**
|
|
105
|
-
* Text MIME type prefixes for determining if a file is text-based.
|
|
106
|
-
*/
|
|
107
|
-
export const TEXT_MIME_PREFIXES = [
|
|
108
|
-
'text/',
|
|
109
|
-
'application/json',
|
|
110
|
-
'application/javascript',
|
|
111
|
-
'application/typescript',
|
|
112
|
-
'application/xml',
|
|
113
|
-
'application/yaml',
|
|
114
|
-
'application/x-yaml',
|
|
115
|
-
'image/svg+xml',
|
|
116
|
-
];
|
|
117
|
-
/**
|
|
118
|
-
* Detect MIME type from filename.
|
|
119
|
-
* @param filename - Name of the file (with extension)
|
|
120
|
-
* @returns MIME type string
|
|
121
|
-
*/
|
|
122
|
-
export function detectMimeType(filename) {
|
|
123
|
-
const ext = path.extname(filename).toLowerCase();
|
|
124
|
-
return MIME_TYPES[ext] || 'application/octet-stream';
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Check if a MIME type represents a text-based file.
|
|
128
|
-
* @param mimeType - MIME type string
|
|
129
|
-
* @returns true if the file is text-based
|
|
130
|
-
*/
|
|
131
|
-
export function isTextMimeType(mimeType) {
|
|
132
|
-
return TEXT_MIME_PREFIXES.some(prefix => mimeType.startsWith(prefix));
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Get language identifier from MIME type (for code highlighting).
|
|
136
|
-
* @param mimeType - MIME type string
|
|
137
|
-
* @returns Language identifier or undefined
|
|
138
|
-
*/
|
|
139
|
-
export function getLanguageFromMimeType(mimeType) {
|
|
140
|
-
const languageMap = {
|
|
141
|
-
'text/typescript': 'typescript',
|
|
142
|
-
'text/javascript': 'javascript',
|
|
143
|
-
'text/x-python': 'python',
|
|
144
|
-
'text/x-ruby': 'ruby',
|
|
145
|
-
'text/x-go': 'go',
|
|
146
|
-
'text/x-rust': 'rust',
|
|
147
|
-
'text/x-java': 'java',
|
|
148
|
-
'text/x-c': 'c',
|
|
149
|
-
'text/x-c++': 'cpp',
|
|
150
|
-
'text/x-csharp': 'csharp',
|
|
151
|
-
'text/x-swift': 'swift',
|
|
152
|
-
'text/x-kotlin': 'kotlin',
|
|
153
|
-
'text/x-scala': 'scala',
|
|
154
|
-
'text/x-php': 'php',
|
|
155
|
-
'text/x-shellscript': 'shell',
|
|
156
|
-
'text/x-sql': 'sql',
|
|
157
|
-
'text/html': 'html',
|
|
158
|
-
'text/css': 'css',
|
|
159
|
-
'text/scss': 'scss',
|
|
160
|
-
'text/markdown': 'markdown',
|
|
161
|
-
'application/json': 'json',
|
|
162
|
-
'text/yaml': 'yaml',
|
|
163
|
-
'application/xml': 'xml',
|
|
164
|
-
};
|
|
165
|
-
return languageMap[mimeType];
|
|
166
|
-
}
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
// glassware[type="implementation", id="impl-offline-queue--3955f31a", requirements="requirement-cli-sync-offline-queue--08f04bd5,requirement-cli-sync-queue-persist--e35cadaa,requirement-cli-sync-queue-replay--a7954975"]
|
|
2
|
-
/**
|
|
3
|
-
* Offline queue management for sync daemon.
|
|
4
|
-
* Queues changes during offline periods and replays them on reconnect.
|
|
5
|
-
* Persists queue to disk across daemon restarts.
|
|
6
|
-
*/
|
|
7
|
-
import fs from 'fs';
|
|
8
|
-
import path from 'path';
|
|
9
|
-
import { getModDir } from '../lib/storage.js';
|
|
10
|
-
/**
|
|
11
|
-
* Get the path to the offline queue file.
|
|
12
|
-
*/
|
|
13
|
-
export function getQueuePath() {
|
|
14
|
-
return path.join(getModDir(), 'sync-queue.json');
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Load the offline queue from disk.
|
|
18
|
-
* Returns empty array if file doesn't exist or is corrupted.
|
|
19
|
-
*/
|
|
20
|
-
export function loadQueue() {
|
|
21
|
-
const queuePath = getQueuePath();
|
|
22
|
-
if (!fs.existsSync(queuePath)) {
|
|
23
|
-
return [];
|
|
24
|
-
}
|
|
25
|
-
try {
|
|
26
|
-
const content = fs.readFileSync(queuePath, 'utf-8');
|
|
27
|
-
const queue = JSON.parse(content);
|
|
28
|
-
if (!Array.isArray(queue)) {
|
|
29
|
-
console.warn('[offline-queue] Queue file is not an array, resetting');
|
|
30
|
-
return [];
|
|
31
|
-
}
|
|
32
|
-
// Validate queue entries
|
|
33
|
-
return queue.filter((entry) => entry &&
|
|
34
|
-
typeof entry.type === 'string' &&
|
|
35
|
-
typeof entry.relativePath === 'string' &&
|
|
36
|
-
typeof entry.timestamp === 'string' &&
|
|
37
|
-
typeof entry.workspaceId === 'string');
|
|
38
|
-
}
|
|
39
|
-
catch (error) {
|
|
40
|
-
console.warn('[offline-queue] Could not read queue file, starting fresh');
|
|
41
|
-
return [];
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Persist the queue to disk atomically.
|
|
46
|
-
*/
|
|
47
|
-
export function persistQueue(queue) {
|
|
48
|
-
const queuePath = getQueuePath();
|
|
49
|
-
const tempPath = `${queuePath}.tmp.${process.pid}`;
|
|
50
|
-
const content = JSON.stringify(queue, null, 2);
|
|
51
|
-
fs.writeFileSync(tempPath, content, { mode: 0o600 });
|
|
52
|
-
fs.renameSync(tempPath, queuePath);
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Add a change to the offline queue.
|
|
56
|
-
* Deduplicates by keeping only the latest change per path/workspace combination.
|
|
57
|
-
*/
|
|
58
|
-
export function queueChange(queue, change) {
|
|
59
|
-
// Find existing entry for same path and workspace
|
|
60
|
-
const existingIdx = queue.findIndex(c => c.relativePath === change.relativePath && c.workspaceId === change.workspaceId);
|
|
61
|
-
let newQueue;
|
|
62
|
-
if (existingIdx >= 0) {
|
|
63
|
-
// Replace existing entry with new one
|
|
64
|
-
newQueue = [...queue];
|
|
65
|
-
newQueue[existingIdx] = change;
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
// Add new entry
|
|
69
|
-
newQueue = [...queue, change];
|
|
70
|
-
}
|
|
71
|
-
// Persist to disk
|
|
72
|
-
persistQueue(newQueue);
|
|
73
|
-
return newQueue;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Remove a change from the queue after successful sync.
|
|
77
|
-
*/
|
|
78
|
-
export function removeFromQueue(queue, change) {
|
|
79
|
-
const newQueue = queue.filter(c => !(c.relativePath === change.relativePath &&
|
|
80
|
-
c.workspaceId === change.workspaceId &&
|
|
81
|
-
c.timestamp === change.timestamp));
|
|
82
|
-
// Persist to disk
|
|
83
|
-
persistQueue(newQueue);
|
|
84
|
-
return newQueue;
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Sort queue by timestamp for ordered replay.
|
|
88
|
-
*/
|
|
89
|
-
export function sortQueueByTimestamp(queue) {
|
|
90
|
-
return [...queue].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Clear the entire queue.
|
|
94
|
-
*/
|
|
95
|
-
export function clearQueue() {
|
|
96
|
-
const queuePath = getQueuePath();
|
|
97
|
-
if (fs.existsSync(queuePath)) {
|
|
98
|
-
fs.unlinkSync(queuePath);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Get queue statistics.
|
|
103
|
-
*/
|
|
104
|
-
export function getQueueStats(queue) {
|
|
105
|
-
const workspaceIds = new Set(queue.map(c => c.workspaceId));
|
|
106
|
-
return {
|
|
107
|
-
total: queue.length,
|
|
108
|
-
creates: queue.filter(c => c.type === 'create').length,
|
|
109
|
-
updates: queue.filter(c => c.type === 'update').length,
|
|
110
|
-
deletes: queue.filter(c => c.type === 'delete').length,
|
|
111
|
-
workspaces: workspaceIds.size,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
// glassware[type="implementation", id="impl-offline-queue-manager--311f689d", requirements="requirement-cli-sync-offline-queue--08f04bd5,requirement-cli-sync-queue-replay--a7954975"]
|
|
115
|
-
/**
|
|
116
|
-
* OfflineQueueManager - manages the offline queue as a class for daemon use.
|
|
117
|
-
*/
|
|
118
|
-
export class OfflineQueueManager {
|
|
119
|
-
constructor(logger = console.log) {
|
|
120
|
-
this.queue = [];
|
|
121
|
-
this.log = logger;
|
|
122
|
-
this.queue = loadQueue();
|
|
123
|
-
this.log(`[offline-queue] Loaded ${this.queue.length} queued changes`);
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Add a change to the queue.
|
|
127
|
-
*/
|
|
128
|
-
add(change) {
|
|
129
|
-
const timestampedChange = {
|
|
130
|
-
...change,
|
|
131
|
-
timestamp: new Date().toISOString(),
|
|
132
|
-
};
|
|
133
|
-
this.queue = queueChange(this.queue, timestampedChange);
|
|
134
|
-
this.log(`[offline-queue] Queued ${change.type}: ${change.relativePath}`);
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Get all queued changes, sorted by timestamp.
|
|
138
|
-
*/
|
|
139
|
-
getAll() {
|
|
140
|
-
return sortQueueByTimestamp(this.queue);
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Get changes for a specific workspace.
|
|
144
|
-
*/
|
|
145
|
-
getForWorkspace(workspaceId) {
|
|
146
|
-
return sortQueueByTimestamp(this.queue.filter(c => c.workspaceId === workspaceId));
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Mark a change as successfully synced (removes from queue).
|
|
150
|
-
*/
|
|
151
|
-
markSynced(change) {
|
|
152
|
-
this.queue = removeFromQueue(this.queue, change);
|
|
153
|
-
this.log(`[offline-queue] Synced: ${change.relativePath}`);
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Replay all queued changes using the provided sync function.
|
|
157
|
-
* @param syncFn - Function to sync each change
|
|
158
|
-
* @returns Number of successfully synced changes
|
|
159
|
-
*/
|
|
160
|
-
async replay(syncFn) {
|
|
161
|
-
const sorted = this.getAll();
|
|
162
|
-
let synced = 0;
|
|
163
|
-
let failed = 0;
|
|
164
|
-
this.log(`[offline-queue] Replaying ${sorted.length} queued changes...`);
|
|
165
|
-
for (const change of sorted) {
|
|
166
|
-
try {
|
|
167
|
-
const success = await syncFn(change);
|
|
168
|
-
if (success) {
|
|
169
|
-
this.markSynced(change);
|
|
170
|
-
synced++;
|
|
171
|
-
}
|
|
172
|
-
else {
|
|
173
|
-
failed++;
|
|
174
|
-
this.log(`[offline-queue] Failed to sync: ${change.relativePath}`);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
catch (error) {
|
|
178
|
-
failed++;
|
|
179
|
-
this.log(`[offline-queue] Error syncing ${change.relativePath}: ${error?.message || error}`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
this.log(`[offline-queue] Replay complete: ${synced} synced, ${failed} failed`);
|
|
183
|
-
return { synced, failed };
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Get queue statistics.
|
|
187
|
-
*/
|
|
188
|
-
getStats() {
|
|
189
|
-
return getQueueStats(this.queue);
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Clear all queued changes.
|
|
193
|
-
*/
|
|
194
|
-
clear() {
|
|
195
|
-
clearQueue();
|
|
196
|
-
this.queue = [];
|
|
197
|
-
this.log('[offline-queue] Queue cleared');
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* Check if queue has pending changes.
|
|
201
|
-
*/
|
|
202
|
-
hasPending() {
|
|
203
|
-
return this.queue.length > 0;
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Get count of pending changes.
|
|
207
|
-
*/
|
|
208
|
-
getPendingCount() {
|
|
209
|
-
return this.queue.length;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
// glassware[type="implementation", id="impl-path-utils--4ad8bb3f", requirements="requirement-cli-sync-path-normalize--5087578b"]
|
|
2
|
-
/**
|
|
3
|
-
* Path normalization utilities for cross-platform consistency.
|
|
4
|
-
* Ensures all paths use forward slashes and are relative to workspace root.
|
|
5
|
-
*/
|
|
6
|
-
import path from 'path';
|
|
7
|
-
/**
|
|
8
|
-
* Normalize a path to forward-slash format relative to workspace root.
|
|
9
|
-
* @param absolutePath - Absolute path to the file
|
|
10
|
-
* @param workspaceRoot - Absolute path to the workspace root directory
|
|
11
|
-
* @returns Forward-slash separated relative path
|
|
12
|
-
*/
|
|
13
|
-
export function normalizePath(absolutePath, workspaceRoot) {
|
|
14
|
-
const relative = path.relative(workspaceRoot, absolutePath);
|
|
15
|
-
return relative.split(path.sep).join('/');
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Get the parent directory path from a file path.
|
|
19
|
-
* @param relativePath - Forward-slash separated relative path
|
|
20
|
-
* @returns Parent directory path, or null if file is at root
|
|
21
|
-
*/
|
|
22
|
-
export function getParentPath(relativePath) {
|
|
23
|
-
const parts = relativePath.split('/');
|
|
24
|
-
parts.pop(); // Remove filename
|
|
25
|
-
return parts.length > 0 ? parts.join('/') : null;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Get the filename from a path.
|
|
29
|
-
* @param filePath - Forward-slash separated path
|
|
30
|
-
* @returns Filename
|
|
31
|
-
*/
|
|
32
|
-
export function getFileName(filePath) {
|
|
33
|
-
return filePath.split('/').pop() || filePath;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Get directory parts from a file path (excluding filename).
|
|
37
|
-
* @param relativePath - Forward-slash separated relative path
|
|
38
|
-
* @returns Array of directory names
|
|
39
|
-
*/
|
|
40
|
-
export function getDirectoryParts(relativePath) {
|
|
41
|
-
const parts = relativePath.split('/');
|
|
42
|
-
parts.pop(); // Remove filename
|
|
43
|
-
return parts;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Join path parts with forward slashes.
|
|
47
|
-
* @param parts - Path parts to join
|
|
48
|
-
* @returns Forward-slash separated path
|
|
49
|
-
*/
|
|
50
|
-
export function joinPath(...parts) {
|
|
51
|
-
return parts.filter(Boolean).join('/');
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Check if a path is within the workspace root.
|
|
55
|
-
* Prevents path traversal attacks.
|
|
56
|
-
* @param absolutePath - Absolute path to check
|
|
57
|
-
* @param workspaceRoot - Workspace root directory
|
|
58
|
-
* @returns true if path is within workspace
|
|
59
|
-
*/
|
|
60
|
-
export function isWithinWorkspace(absolutePath, workspaceRoot) {
|
|
61
|
-
const resolvedPath = path.resolve(absolutePath);
|
|
62
|
-
const resolvedRoot = path.resolve(workspaceRoot);
|
|
63
|
-
return resolvedPath.startsWith(resolvedRoot + path.sep) || resolvedPath === resolvedRoot;
|
|
64
|
-
}
|