@mod-computer/cli 0.2.4 → 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/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/services/logger.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
// Evaluate debug flag at call time to allow dotenv to load later
|
|
4
|
-
function isDebugEnabled() {
|
|
5
|
-
return Boolean(process.env.MOD_CLI_DEBUG) || Boolean(process.env.DEBUG);
|
|
6
|
-
}
|
|
7
|
-
const LOG_DIR = path.resolve(process.cwd(), '.mod');
|
|
8
|
-
const LOG_FILE = path.join(LOG_DIR, 'cli-debug.log');
|
|
9
|
-
function ensureLogDir() {
|
|
10
|
-
try {
|
|
11
|
-
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
12
|
-
}
|
|
13
|
-
catch { }
|
|
14
|
-
}
|
|
15
|
-
export function log(...args) {
|
|
16
|
-
if (!isDebugEnabled())
|
|
17
|
-
return;
|
|
18
|
-
try {
|
|
19
|
-
ensureLogDir();
|
|
20
|
-
const line = `[${new Date().toISOString()}] ${args.map(a => safeStringify(a)).join(' ')}\n`;
|
|
21
|
-
fs.appendFileSync(LOG_FILE, line, 'utf8');
|
|
22
|
-
// Also mirror to stderr so it doesn't interfere with Ink UI
|
|
23
|
-
try {
|
|
24
|
-
console.error(line.trimEnd());
|
|
25
|
-
}
|
|
26
|
-
catch { }
|
|
27
|
-
}
|
|
28
|
-
catch { }
|
|
29
|
-
}
|
|
30
|
-
function safeStringify(v) {
|
|
31
|
-
try {
|
|
32
|
-
if (typeof v === 'string')
|
|
33
|
-
return v;
|
|
34
|
-
return JSON.stringify(v);
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
try {
|
|
38
|
-
return String(v);
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
return '[unprintable]';
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
const MOD_DIR = '.mod';
|
|
4
|
-
const CONFIG_FILE = 'config.json';
|
|
5
|
-
function getConfigPath() {
|
|
6
|
-
const override = process.env.MOD_CONFIG_PATH;
|
|
7
|
-
if (override && typeof override === 'string' && override.trim() !== '') {
|
|
8
|
-
return override;
|
|
9
|
-
}
|
|
10
|
-
return path.join(process.cwd(), MOD_DIR, CONFIG_FILE);
|
|
11
|
-
}
|
|
12
|
-
function ensureModDir() {
|
|
13
|
-
const modDirPath = path.join(process.cwd(), MOD_DIR);
|
|
14
|
-
if (!fs.existsSync(modDirPath)) {
|
|
15
|
-
fs.mkdirSync(modDirPath, { recursive: true });
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
export function readModConfig() {
|
|
19
|
-
// Preferred: .mod/config.json
|
|
20
|
-
const cfgPath = getConfigPath();
|
|
21
|
-
if (fs.existsSync(cfgPath)) {
|
|
22
|
-
try {
|
|
23
|
-
const config = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
24
|
-
return config || {};
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
// Legacy migration: root .mod file containing JSON
|
|
31
|
-
const legacyPath = path.join(process.cwd(), '.mod');
|
|
32
|
-
if (fs.existsSync(legacyPath) && fs.statSync(legacyPath).isFile()) {
|
|
33
|
-
try {
|
|
34
|
-
const legacy = JSON.parse(fs.readFileSync(legacyPath, 'utf8'));
|
|
35
|
-
const migrated = {};
|
|
36
|
-
if (legacy && typeof legacy.workspaceId === 'string')
|
|
37
|
-
migrated.workspaceId = legacy.workspaceId;
|
|
38
|
-
if (legacy && typeof legacy.activeBranchId === 'string')
|
|
39
|
-
migrated.activeBranchId = legacy.activeBranchId;
|
|
40
|
-
return migrated;
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
export function writeModConfig(update) {
|
|
49
|
-
try {
|
|
50
|
-
// NOTE: .mod directory creation disabled - config moved to ~/.mod/config
|
|
51
|
-
// ensureModDir();
|
|
52
|
-
const cfgPath = getConfigPath();
|
|
53
|
-
// Only create directory if config path is not in cwd (e.g., using MOD_CONFIG_PATH override)
|
|
54
|
-
const configDir = path.dirname(cfgPath);
|
|
55
|
-
if (!configDir.includes(process.cwd()) && !fs.existsSync(configDir)) {
|
|
56
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
57
|
-
}
|
|
58
|
-
const current = fs.existsSync(cfgPath)
|
|
59
|
-
? JSON.parse(fs.readFileSync(cfgPath, 'utf8'))
|
|
60
|
-
: {};
|
|
61
|
-
const next = { ...current, ...update };
|
|
62
|
-
fs.writeFileSync(cfgPath, JSON.stringify(next, null, 2), 'utf8');
|
|
63
|
-
}
|
|
64
|
-
catch (error) {
|
|
65
|
-
console.error(`Failed to write mod config:`, error);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
// glassware[type="implementation", id="impl-cli-add-modignore-service--ae506406", requirements="requirement-cli-add-ignore-modignore--92ba9b60,requirement-cli-add-ignore-gitignore--054c4b53,requirement-cli-add-ignore-default--facbedfe,requirement-cli-add-ignore-negation--79b9bf48"]
|
|
2
|
-
// spec: packages/mod-cli/specs/add.md
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
export class ModIgnoreService {
|
|
6
|
-
constructor(workingDirectory) {
|
|
7
|
-
this.patterns = [];
|
|
8
|
-
this.defaultPatterns = [
|
|
9
|
-
'node_modules/',
|
|
10
|
-
'*/node_modules/*',
|
|
11
|
-
'.git/',
|
|
12
|
-
'.DS_Store',
|
|
13
|
-
'Thumbs.db',
|
|
14
|
-
'*.log',
|
|
15
|
-
'npm-debug.log*',
|
|
16
|
-
'yarn-debug.log*',
|
|
17
|
-
'.env',
|
|
18
|
-
'.env.*',
|
|
19
|
-
'dist/',
|
|
20
|
-
'build/',
|
|
21
|
-
'.next/',
|
|
22
|
-
'.nuxt/',
|
|
23
|
-
'coverage/',
|
|
24
|
-
'.nyc_output/',
|
|
25
|
-
'*.tmp',
|
|
26
|
-
'*.temp',
|
|
27
|
-
'*.swp',
|
|
28
|
-
'*.swo',
|
|
29
|
-
'.cache/',
|
|
30
|
-
'.vscode/',
|
|
31
|
-
'.idea/',
|
|
32
|
-
'.automerge-data/',
|
|
33
|
-
'package-lock.json',
|
|
34
|
-
'yarn.lock',
|
|
35
|
-
'pnpm-lock.yaml',
|
|
36
|
-
'.mod/.cache',
|
|
37
|
-
];
|
|
38
|
-
this.trackableExtensions = [
|
|
39
|
-
'.md', '.txt', '.js', '.ts', '.jsx', '.tsx', '.json', '.yaml', '.yml',
|
|
40
|
-
'.py', '.java', '.cpp', '.c', '.h', '.css', '.scss', '.html', '.xml',
|
|
41
|
-
'.sql', '.go', '.rs', '.php', '.rb', '.swift', '.kt', '.scala'
|
|
42
|
-
];
|
|
43
|
-
this.loadIgnoreFile(workingDirectory);
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Load .modignore file from the working directory
|
|
47
|
-
*/
|
|
48
|
-
loadIgnoreFile(workingDirectory) {
|
|
49
|
-
// Start with default patterns
|
|
50
|
-
this.patterns = this.defaultPatterns.map(pattern => this.parsePattern(pattern));
|
|
51
|
-
const ignoreFilePath = path.join(workingDirectory, '.modignore');
|
|
52
|
-
try {
|
|
53
|
-
if (fs.existsSync(ignoreFilePath)) {
|
|
54
|
-
const content = fs.readFileSync(ignoreFilePath, 'utf8');
|
|
55
|
-
const lines = content.split('\n')
|
|
56
|
-
.map(line => line.trim())
|
|
57
|
-
.filter(line => line && !line.startsWith('#')); // Remove comments and empty lines
|
|
58
|
-
for (const line of lines) {
|
|
59
|
-
this.patterns.push(this.parsePattern(line));
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
catch (err) {
|
|
64
|
-
console.warn('Warning: Could not read .modignore file:', err);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Parse a single ignore pattern
|
|
69
|
-
*/
|
|
70
|
-
parsePattern(pattern) {
|
|
71
|
-
let cleanPattern = pattern.trim();
|
|
72
|
-
const isNegation = cleanPattern.startsWith('!');
|
|
73
|
-
if (isNegation) {
|
|
74
|
-
cleanPattern = cleanPattern.slice(1);
|
|
75
|
-
}
|
|
76
|
-
const isDirectory = cleanPattern.endsWith('/');
|
|
77
|
-
if (isDirectory) {
|
|
78
|
-
cleanPattern = cleanPattern.slice(0, -1);
|
|
79
|
-
}
|
|
80
|
-
return {
|
|
81
|
-
pattern: cleanPattern,
|
|
82
|
-
isNegation,
|
|
83
|
-
isDirectory,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Check if a file path should be ignored
|
|
88
|
-
*/
|
|
89
|
-
shouldIgnore(filePath, workingDirectory) {
|
|
90
|
-
// Always ignore .mod and .modignore files
|
|
91
|
-
const relativePath = path.relative(workingDirectory, filePath);
|
|
92
|
-
const fileName = path.basename(filePath);
|
|
93
|
-
if (fileName === '.mod' || fileName === '.modignore') {
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
|
-
// Check if it's a directory
|
|
97
|
-
const isDirectory = fs.existsSync(filePath) && fs.statSync(filePath).isDirectory();
|
|
98
|
-
let shouldIgnore = false;
|
|
99
|
-
for (const { pattern, isNegation, isDirectory: patternIsDirectory } of this.patterns) {
|
|
100
|
-
const matches = this.matchesPattern(relativePath, pattern, isDirectory, patternIsDirectory);
|
|
101
|
-
if (matches) {
|
|
102
|
-
shouldIgnore = !isNegation;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return shouldIgnore;
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Check if a path matches a pattern
|
|
109
|
-
*/
|
|
110
|
-
matchesPattern(relativePath, pattern, isDirectory, patternIsDirectory) {
|
|
111
|
-
// If pattern is for directories only, skip non-directories
|
|
112
|
-
if (patternIsDirectory && !isDirectory) {
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
// Convert pattern to regex
|
|
116
|
-
const regexPattern = this.patternToRegex(pattern);
|
|
117
|
-
const regex = new RegExp(regexPattern);
|
|
118
|
-
// Check direct match
|
|
119
|
-
if (regex.test(relativePath)) {
|
|
120
|
-
return true;
|
|
121
|
-
}
|
|
122
|
-
// Check if any parent directory matches (for directory patterns)
|
|
123
|
-
const pathParts = relativePath.split(path.sep);
|
|
124
|
-
for (let i = 0; i < pathParts.length; i++) {
|
|
125
|
-
const partialPath = pathParts.slice(0, i + 1).join(path.sep);
|
|
126
|
-
if (regex.test(partialPath)) {
|
|
127
|
-
return true;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Convert a glob pattern to a regex pattern
|
|
134
|
-
*/
|
|
135
|
-
patternToRegex(pattern) {
|
|
136
|
-
// Escape special regex characters except * and ?
|
|
137
|
-
let regex = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
138
|
-
// Handle ** globstar patterns first (before single *)
|
|
139
|
-
regex = regex.replace(/\*\*/g, '(?:.*[\\\\/])?.*'); // ** matches zero or more directories
|
|
140
|
-
// Convert remaining glob patterns to regex
|
|
141
|
-
regex = regex.replace(/\*/g, '[^\\\\/]*'); // * matches any characters except path separators
|
|
142
|
-
regex = regex.replace(/\?/g, '.'); // ? matches any single character
|
|
143
|
-
// Handle directory separators
|
|
144
|
-
regex = regex.replace(/\\\//g, '[\\\\/]'); // Handle both / and \ path separators
|
|
145
|
-
// For patterns ending with /, match anything under that directory
|
|
146
|
-
if (pattern.endsWith('/')) {
|
|
147
|
-
regex = regex.replace(/\$/, '(?:[\\\\/].*)?$');
|
|
148
|
-
}
|
|
149
|
-
// Anchor the pattern
|
|
150
|
-
if (!regex.startsWith('(?:.*[\\\\/])?.*') && !regex.startsWith('^')) {
|
|
151
|
-
regex = '^' + regex;
|
|
152
|
-
}
|
|
153
|
-
if (!regex.endsWith('.*') && !regex.endsWith('$')) {
|
|
154
|
-
regex = regex + '$';
|
|
155
|
-
}
|
|
156
|
-
return regex;
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Get all loaded patterns (for debugging)
|
|
160
|
-
*/
|
|
161
|
-
getPatterns() {
|
|
162
|
-
return [...this.patterns];
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Filter a list of file paths, removing ignored ones
|
|
166
|
-
*/
|
|
167
|
-
filterIgnored(filePaths, workingDirectory) {
|
|
168
|
-
return filePaths.filter(filePath => !this.shouldIgnore(filePath, workingDirectory));
|
|
169
|
-
}
|
|
170
|
-
async preFilterDirectory(workingDirectory) {
|
|
171
|
-
const result = {
|
|
172
|
-
totalFiles: 0,
|
|
173
|
-
filteredFiles: [],
|
|
174
|
-
excludedCount: 0,
|
|
175
|
-
trackableFiles: []
|
|
176
|
-
};
|
|
177
|
-
await this.scanDirectoryRecursive(workingDirectory, workingDirectory, result);
|
|
178
|
-
result.trackableFiles = result.filteredFiles.filter(filePath => this.isTrackableFile(filePath));
|
|
179
|
-
return result;
|
|
180
|
-
}
|
|
181
|
-
async scanDirectoryRecursive(dirPath, rootPath, result) {
|
|
182
|
-
try {
|
|
183
|
-
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
184
|
-
for (const entry of entries) {
|
|
185
|
-
const fullPath = path.join(dirPath, entry.name);
|
|
186
|
-
if (entry.isDirectory()) {
|
|
187
|
-
// Check if directory should be ignored before recursing
|
|
188
|
-
if (!this.shouldIgnore(fullPath, rootPath)) {
|
|
189
|
-
await this.scanDirectoryRecursive(fullPath, rootPath, result);
|
|
190
|
-
}
|
|
191
|
-
else {
|
|
192
|
-
try {
|
|
193
|
-
const excludedCount = await this.countFilesInDirectory(fullPath);
|
|
194
|
-
result.excludedCount += excludedCount;
|
|
195
|
-
}
|
|
196
|
-
catch (error) {
|
|
197
|
-
console.warn(`Failed to count files in excluded directory ${fullPath}:`, error);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
else if (entry.isFile()) {
|
|
202
|
-
result.totalFiles++;
|
|
203
|
-
if (!this.shouldIgnore(fullPath, rootPath)) {
|
|
204
|
-
result.filteredFiles.push(fullPath);
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
result.excludedCount++;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
catch (error) {
|
|
213
|
-
console.warn(`Failed to scan directory ${dirPath}:`, error);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
async countFilesInDirectory(dirPath) {
|
|
217
|
-
try {
|
|
218
|
-
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
219
|
-
let count = 0;
|
|
220
|
-
for (const entry of entries) {
|
|
221
|
-
if (entry.isFile()) {
|
|
222
|
-
count++;
|
|
223
|
-
}
|
|
224
|
-
else if (entry.isDirectory()) {
|
|
225
|
-
count += await this.countFilesInDirectory(path.join(dirPath, entry.name));
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return count;
|
|
229
|
-
}
|
|
230
|
-
catch (error) {
|
|
231
|
-
return 0;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
isTrackableFile(filePath) {
|
|
235
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
236
|
-
return this.trackableExtensions.includes(ext);
|
|
237
|
-
}
|
|
238
|
-
getPatternStats() {
|
|
239
|
-
return {
|
|
240
|
-
totalPatterns: this.patterns.length,
|
|
241
|
-
defaultPatterns: this.defaultPatterns.length,
|
|
242
|
-
customPatterns: Math.max(0, this.patterns.length - this.defaultPatterns.length)
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Create a default .modignore file in the specified directory
|
|
247
|
-
*/
|
|
248
|
-
static createDefaultIgnoreFile(workingDirectory) {
|
|
249
|
-
const ignoreFilePath = path.join(workingDirectory, '.modignore');
|
|
250
|
-
if (fs.existsSync(ignoreFilePath)) {
|
|
251
|
-
return; // Don't overwrite existing file
|
|
252
|
-
}
|
|
253
|
-
const defaultContent = `# Mod Ignore File
|
|
254
|
-
# Patterns to exclude from mod sync operations
|
|
255
|
-
|
|
256
|
-
# Dependencies
|
|
257
|
-
node_modules/
|
|
258
|
-
vendor/
|
|
259
|
-
|
|
260
|
-
# Build outputs
|
|
261
|
-
dist/
|
|
262
|
-
build/
|
|
263
|
-
out/
|
|
264
|
-
target/
|
|
265
|
-
|
|
266
|
-
# Environment files
|
|
267
|
-
.env
|
|
268
|
-
.env.local
|
|
269
|
-
.env.development.local
|
|
270
|
-
.env.test.local
|
|
271
|
-
.env.production.local
|
|
272
|
-
|
|
273
|
-
# Logs
|
|
274
|
-
*.log
|
|
275
|
-
logs/
|
|
276
|
-
|
|
277
|
-
# Runtime data
|
|
278
|
-
pids/
|
|
279
|
-
*.pid
|
|
280
|
-
*.seed
|
|
281
|
-
*.pid.lock
|
|
282
|
-
|
|
283
|
-
# Coverage directory used by tools like istanbul
|
|
284
|
-
coverage/
|
|
285
|
-
.nyc_output/
|
|
286
|
-
|
|
287
|
-
# Cache directories
|
|
288
|
-
.cache/
|
|
289
|
-
.npm/
|
|
290
|
-
.yarn/
|
|
291
|
-
|
|
292
|
-
# OS generated files
|
|
293
|
-
.DS_Store
|
|
294
|
-
.DS_Store?
|
|
295
|
-
._*
|
|
296
|
-
.Spotlight-V100
|
|
297
|
-
.Trashes
|
|
298
|
-
ehthumbs.db
|
|
299
|
-
Thumbs.db
|
|
300
|
-
|
|
301
|
-
# Temporary files
|
|
302
|
-
*.tmp
|
|
303
|
-
*.temp
|
|
304
|
-
*.swp
|
|
305
|
-
*.swo
|
|
306
|
-
*~
|
|
307
|
-
|
|
308
|
-
# IDE files
|
|
309
|
-
.vscode/
|
|
310
|
-
.idea/
|
|
311
|
-
*.suo
|
|
312
|
-
*.ntvs*
|
|
313
|
-
*.njsproj
|
|
314
|
-
*.sln
|
|
315
|
-
*.sw?
|
|
316
|
-
|
|
317
|
-
# Automerge data
|
|
318
|
-
.automerge-data/
|
|
319
|
-
`;
|
|
320
|
-
try {
|
|
321
|
-
fs.writeFileSync(ignoreFilePath, defaultContent, 'utf8');
|
|
322
|
-
console.log('Created default .modignore file');
|
|
323
|
-
}
|
|
324
|
-
catch (err) {
|
|
325
|
-
console.warn('Warning: Could not create .modignore file:', err);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs/promises';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import { spawn } from 'child_process';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import { readModConfig } from './mod-config.js';
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = path.dirname(__filename);
|
|
8
|
-
export class SyncDaemon {
|
|
9
|
-
constructor() {
|
|
10
|
-
this.syncQueue = [];
|
|
11
|
-
this.cacheDir = path.join(process.cwd(), '.mod', '.cache');
|
|
12
|
-
this.stateFilePath = path.join(this.cacheDir, 'sync-daemon.json');
|
|
13
|
-
this.pidFilePath = path.join(this.cacheDir, 'sync-daemon.pid');
|
|
14
|
-
}
|
|
15
|
-
async start(options = {}) {
|
|
16
|
-
try {
|
|
17
|
-
const currentState = await this.getState();
|
|
18
|
-
if (currentState && currentState.status === 'running' && !options.force) {
|
|
19
|
-
if (await this.isProcessRunning(currentState.pid)) {
|
|
20
|
-
throw new Error(`Daemon is already running (PID: ${currentState.pid})`);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
await fs.mkdir(this.cacheDir, { recursive: true });
|
|
24
|
-
const cfg = readModConfig();
|
|
25
|
-
if (!cfg?.workspaceId) {
|
|
26
|
-
throw new Error('No active workspace found in .mod/config.json');
|
|
27
|
-
}
|
|
28
|
-
const daemonProcess = spawn(process.execPath, [
|
|
29
|
-
path.join(__dirname, '../daemon-worker.js'),
|
|
30
|
-
'--workspace-id', cfg.workspaceId,
|
|
31
|
-
'--active-branch', cfg.activeBranchId || 'main',
|
|
32
|
-
'--working-dir', process.cwd()
|
|
33
|
-
], {
|
|
34
|
-
detached: true,
|
|
35
|
-
stdio: options.verbose ? 'inherit' : 'ignore',
|
|
36
|
-
cwd: process.cwd(),
|
|
37
|
-
env: { ...process.env, MOD_DAEMON_MODE: 'true' }
|
|
38
|
-
});
|
|
39
|
-
daemonProcess.unref();
|
|
40
|
-
const initialState = {
|
|
41
|
-
pid: daemonProcess.pid,
|
|
42
|
-
status: 'starting',
|
|
43
|
-
startedAt: new Date().toISOString(),
|
|
44
|
-
lastActivity: new Date().toISOString(),
|
|
45
|
-
workspaceId: cfg.workspaceId,
|
|
46
|
-
activeBranchId: cfg.activeBranchId,
|
|
47
|
-
watchedFiles: 0,
|
|
48
|
-
crashCount: 0,
|
|
49
|
-
uptime: 0
|
|
50
|
-
};
|
|
51
|
-
await this.saveState(initialState);
|
|
52
|
-
await fs.writeFile(this.pidFilePath, daemonProcess.pid.toString(), 'utf8');
|
|
53
|
-
console.log(`✅ Sync daemon started (PID: ${daemonProcess.pid})`);
|
|
54
|
-
console.log(`📁 Working directory: ${process.cwd()}`);
|
|
55
|
-
console.log(`🔗 Workspace: ${cfg.workspaceId}`);
|
|
56
|
-
await this.waitForDaemonReady(daemonProcess.pid, 5000);
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
console.error('Failed to start sync daemon:', error);
|
|
60
|
-
throw error;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
async stop(options = {}) {
|
|
64
|
-
try {
|
|
65
|
-
const currentState = await this.getState();
|
|
66
|
-
if (!currentState || currentState.status === 'stopped') {
|
|
67
|
-
console.log('Daemon is not running');
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
if (!(await this.isProcessRunning(currentState.pid))) {
|
|
71
|
-
console.log('Daemon process not found (cleaning up stale state)');
|
|
72
|
-
await this.cleanup();
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
console.log(`🛑 Stopping daemon (PID: ${currentState.pid})...`);
|
|
76
|
-
await this.updateState({ status: 'stopping' });
|
|
77
|
-
// Send SIGTERM for graceful shutdown
|
|
78
|
-
process.kill(currentState.pid, 'SIGTERM');
|
|
79
|
-
const shutdownTimeout = 10000; // 10 seconds
|
|
80
|
-
let gracefulShutdown = false;
|
|
81
|
-
for (let i = 0; i < shutdownTimeout / 100; i++) {
|
|
82
|
-
if (!(await this.isProcessRunning(currentState.pid))) {
|
|
83
|
-
gracefulShutdown = true;
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
87
|
-
}
|
|
88
|
-
if (!gracefulShutdown) {
|
|
89
|
-
if (options.force) {
|
|
90
|
-
console.log('⚠️ Forcing daemon shutdown...');
|
|
91
|
-
process.kill(currentState.pid, 'SIGKILL');
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
throw new Error('Daemon did not shut down gracefully. Use --force to kill the process.');
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
await this.cleanup();
|
|
98
|
-
console.log('✅ Sync daemon stopped');
|
|
99
|
-
}
|
|
100
|
-
catch (error) {
|
|
101
|
-
console.error('Failed to stop sync daemon:', error);
|
|
102
|
-
throw error;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
async restart(options = {}) {
|
|
106
|
-
console.log('🔄 Restarting sync daemon...');
|
|
107
|
-
await this.stop(options);
|
|
108
|
-
await new Promise(resolve => setTimeout(resolve, 1000)); // Brief pause
|
|
109
|
-
await this.start(options);
|
|
110
|
-
}
|
|
111
|
-
async status() {
|
|
112
|
-
try {
|
|
113
|
-
const state = await this.getState();
|
|
114
|
-
if (!state) {
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
const isRunning = await this.isProcessRunning(state.pid);
|
|
118
|
-
if (!isRunning && state.status !== 'stopped') {
|
|
119
|
-
// Process died unexpectedly
|
|
120
|
-
await this.updateState({
|
|
121
|
-
status: 'crashed',
|
|
122
|
-
lastError: 'Process terminated unexpectedly'
|
|
123
|
-
});
|
|
124
|
-
state.status = 'crashed';
|
|
125
|
-
}
|
|
126
|
-
// Calculate uptime if running
|
|
127
|
-
if (state.status === 'running') {
|
|
128
|
-
state.uptime = Date.now() - new Date(state.startedAt).getTime();
|
|
129
|
-
}
|
|
130
|
-
return state;
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
console.error('Failed to get daemon status:', error);
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
async handleCrash(pid, error) {
|
|
138
|
-
const state = await this.getState();
|
|
139
|
-
if (!state)
|
|
140
|
-
return;
|
|
141
|
-
const crashCount = (state.crashCount || 0) + 1;
|
|
142
|
-
await this.updateState({
|
|
143
|
-
status: 'crashed',
|
|
144
|
-
crashCount,
|
|
145
|
-
lastError: error || 'Process crashed unexpectedly',
|
|
146
|
-
lastActivity: new Date().toISOString()
|
|
147
|
-
});
|
|
148
|
-
const backoffDelay = Math.min(1000 * Math.pow(2, crashCount - 1), 30000); // Max 30 seconds
|
|
149
|
-
console.error(`💥 Daemon crashed (attempt ${crashCount}). Restarting in ${backoffDelay}ms...`);
|
|
150
|
-
if (crashCount <= 5) { // Max 5 restart attempts
|
|
151
|
-
setTimeout(async () => {
|
|
152
|
-
try {
|
|
153
|
-
await this.start({ autoRestart: true });
|
|
154
|
-
}
|
|
155
|
-
catch (restartError) {
|
|
156
|
-
console.error('Failed to restart daemon after crash:', restartError);
|
|
157
|
-
}
|
|
158
|
-
}, backoffDelay);
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
console.error('❌ Maximum crash restart attempts exceeded. Manual restart required.');
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
async addToSyncQueue(item) {
|
|
165
|
-
const queueItem = {
|
|
166
|
-
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
167
|
-
timestamp: new Date().toISOString(),
|
|
168
|
-
retryCount: 0,
|
|
169
|
-
...item
|
|
170
|
-
};
|
|
171
|
-
if (item.priority === 'high') {
|
|
172
|
-
this.syncQueue.unshift(queueItem);
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
this.syncQueue.push(queueItem);
|
|
176
|
-
}
|
|
177
|
-
await this.processSyncQueue();
|
|
178
|
-
}
|
|
179
|
-
async processSyncQueue() {
|
|
180
|
-
while (this.syncQueue.length > 0) {
|
|
181
|
-
const item = this.syncQueue.shift();
|
|
182
|
-
try {
|
|
183
|
-
// Process sync operation here
|
|
184
|
-
await this.updateState({ lastActivity: new Date().toISOString() });
|
|
185
|
-
}
|
|
186
|
-
catch (error) {
|
|
187
|
-
console.error(`Failed to process sync queue item ${item.id}:`, error);
|
|
188
|
-
if (item.retryCount < 3) {
|
|
189
|
-
item.retryCount++;
|
|
190
|
-
setTimeout(() => this.syncQueue.push(item), 1000 * item.retryCount);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
async getState() {
|
|
196
|
-
try {
|
|
197
|
-
const data = await fs.readFile(this.stateFilePath, 'utf8');
|
|
198
|
-
return JSON.parse(data);
|
|
199
|
-
}
|
|
200
|
-
catch {
|
|
201
|
-
return null;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
async saveState(state) {
|
|
205
|
-
await fs.writeFile(this.stateFilePath, JSON.stringify(state, null, 2), 'utf8');
|
|
206
|
-
}
|
|
207
|
-
async updateState(updates) {
|
|
208
|
-
const currentState = await this.getState();
|
|
209
|
-
if (currentState) {
|
|
210
|
-
const newState = { ...currentState, ...updates };
|
|
211
|
-
await this.saveState(newState);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
async isProcessRunning(pid) {
|
|
215
|
-
try {
|
|
216
|
-
process.kill(pid, 0); // Signal 0 checks if process exists without killing
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
|
-
catch {
|
|
220
|
-
return false;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
async waitForDaemonReady(pid, timeoutMs) {
|
|
224
|
-
const startTime = Date.now();
|
|
225
|
-
while (Date.now() - startTime < timeoutMs) {
|
|
226
|
-
const state = await this.getState();
|
|
227
|
-
if (state && state.pid === pid && state.status === 'running') {
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
231
|
-
}
|
|
232
|
-
throw new Error('Daemon failed to start within timeout period');
|
|
233
|
-
}
|
|
234
|
-
async cleanup() {
|
|
235
|
-
try {
|
|
236
|
-
await fs.unlink(this.pidFilePath);
|
|
237
|
-
}
|
|
238
|
-
catch { /* ignore */ }
|
|
239
|
-
try {
|
|
240
|
-
await fs.unlink(this.stateFilePath);
|
|
241
|
-
}
|
|
242
|
-
catch { /* ignore */ }
|
|
243
|
-
}
|
|
244
|
-
}
|