@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.
Files changed (75) hide show
  1. package/dist/cli.bundle.js +216 -36371
  2. package/package.json +3 -3
  3. package/dist/app.js +0 -227
  4. package/dist/cli.bundle.js.map +0 -7
  5. package/dist/cli.js +0 -132
  6. package/dist/commands/add.js +0 -245
  7. package/dist/commands/agents-run.js +0 -71
  8. package/dist/commands/auth.js +0 -259
  9. package/dist/commands/branch.js +0 -1411
  10. package/dist/commands/claude-sync.js +0 -772
  11. package/dist/commands/comment.js +0 -568
  12. package/dist/commands/diff.js +0 -182
  13. package/dist/commands/index.js +0 -73
  14. package/dist/commands/init.js +0 -597
  15. package/dist/commands/ls.js +0 -135
  16. package/dist/commands/members.js +0 -687
  17. package/dist/commands/mv.js +0 -282
  18. package/dist/commands/recover.js +0 -207
  19. package/dist/commands/rm.js +0 -257
  20. package/dist/commands/spec.js +0 -386
  21. package/dist/commands/status.js +0 -296
  22. package/dist/commands/sync.js +0 -119
  23. package/dist/commands/trace.js +0 -1752
  24. package/dist/commands/workspace.js +0 -447
  25. package/dist/components/conflict-resolution-ui.js +0 -120
  26. package/dist/components/messages.js +0 -5
  27. package/dist/components/thread.js +0 -8
  28. package/dist/config/features.js +0 -83
  29. package/dist/containers/branches-container.js +0 -140
  30. package/dist/containers/directory-container.js +0 -92
  31. package/dist/containers/thread-container.js +0 -214
  32. package/dist/containers/threads-container.js +0 -27
  33. package/dist/containers/workspaces-container.js +0 -27
  34. package/dist/daemon/conflict-resolution.js +0 -172
  35. package/dist/daemon/content-hash.js +0 -31
  36. package/dist/daemon/file-sync.js +0 -985
  37. package/dist/daemon/index.js +0 -203
  38. package/dist/daemon/mime-types.js +0 -166
  39. package/dist/daemon/offline-queue.js +0 -211
  40. package/dist/daemon/path-utils.js +0 -64
  41. package/dist/daemon/share-policy.js +0 -83
  42. package/dist/daemon/wasm-errors.js +0 -189
  43. package/dist/daemon/worker.js +0 -557
  44. package/dist/daemon-worker.js +0 -258
  45. package/dist/errors/workspace-errors.js +0 -48
  46. package/dist/lib/auth-server.js +0 -216
  47. package/dist/lib/browser.js +0 -35
  48. package/dist/lib/diff.js +0 -284
  49. package/dist/lib/formatters.js +0 -204
  50. package/dist/lib/git.js +0 -137
  51. package/dist/lib/local-fs.js +0 -201
  52. package/dist/lib/prompts.js +0 -56
  53. package/dist/lib/storage.js +0 -213
  54. package/dist/lib/trace-formatters.js +0 -314
  55. package/dist/services/add-service.js +0 -554
  56. package/dist/services/add-validation.js +0 -124
  57. package/dist/services/automatic-file-tracker.js +0 -303
  58. package/dist/services/cli-orchestrator.js +0 -227
  59. package/dist/services/feature-flags.js +0 -187
  60. package/dist/services/file-import-service.js +0 -283
  61. package/dist/services/file-transformation-service.js +0 -218
  62. package/dist/services/logger.js +0 -44
  63. package/dist/services/mod-config.js +0 -67
  64. package/dist/services/modignore-service.js +0 -328
  65. package/dist/services/sync-daemon.js +0 -244
  66. package/dist/services/thread-notification-service.js +0 -50
  67. package/dist/services/thread-service.js +0 -147
  68. package/dist/stores/use-directory-store.js +0 -96
  69. package/dist/stores/use-threads-store.js +0 -46
  70. package/dist/stores/use-workspaces-store.js +0 -54
  71. package/dist/types/add-types.js +0 -99
  72. package/dist/types/config.js +0 -16
  73. package/dist/types/index.js +0 -2
  74. package/dist/types/workspace-connection.js +0 -53
  75. package/dist/types.js +0 -1
@@ -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
- }