@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/lib/local-fs.js
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
// glassware[type="implementation", id="impl-cli-fd-local-fs--3a1efb54", requirements="requirement-cli-fd-local-read--bbd1aae6,requirement-cli-fd-local-delete--df1be435,requirement-cli-fd-local-move--022534b1"]
|
|
2
|
-
// spec: packages/mod-cli/specs/file-directory.md
|
|
3
|
-
import fs from 'fs/promises';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
/**
|
|
6
|
-
* Read local file content as string
|
|
7
|
-
*/
|
|
8
|
-
// glassware[type="implementation", id="impl-cli-fd-local-read--99aa05fa", requirements="requirement-cli-fd-local-read--bbd1aae6"]
|
|
9
|
-
export async function readLocalFile(filePath) {
|
|
10
|
-
try {
|
|
11
|
-
const absolutePath = path.resolve(filePath);
|
|
12
|
-
const content = await fs.readFile(absolutePath, 'utf-8');
|
|
13
|
-
return content;
|
|
14
|
-
}
|
|
15
|
-
catch (error) {
|
|
16
|
-
if (error.code === 'ENOENT') {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
throw error;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Check if local file exists
|
|
24
|
-
*/
|
|
25
|
-
export async function localFileExists(filePath) {
|
|
26
|
-
try {
|
|
27
|
-
const absolutePath = path.resolve(filePath);
|
|
28
|
-
await fs.access(absolutePath);
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Get local file stats
|
|
37
|
-
*/
|
|
38
|
-
export async function getLocalFileStats(filePath) {
|
|
39
|
-
try {
|
|
40
|
-
const absolutePath = path.resolve(filePath);
|
|
41
|
-
const stats = await fs.stat(absolutePath);
|
|
42
|
-
return {
|
|
43
|
-
size: stats.size,
|
|
44
|
-
mtime: stats.mtime,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Delete local file with proper error handling
|
|
53
|
-
*/
|
|
54
|
-
// glassware[type="implementation", id="impl-cli-fd-local-delete--74bf0b5b", requirements="requirement-cli-fd-local-delete--df1be435"]
|
|
55
|
-
export async function deleteLocalFile(filePath) {
|
|
56
|
-
try {
|
|
57
|
-
const absolutePath = path.resolve(filePath);
|
|
58
|
-
await fs.unlink(absolutePath);
|
|
59
|
-
return { success: true };
|
|
60
|
-
}
|
|
61
|
-
catch (error) {
|
|
62
|
-
if (error.code === 'ENOENT') {
|
|
63
|
-
return { success: true }; // Already deleted
|
|
64
|
-
}
|
|
65
|
-
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
|
66
|
-
return { success: false, error: `Permission denied: ${filePath}` };
|
|
67
|
-
}
|
|
68
|
-
return { success: false, error: error.message };
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Delete local directory recursively
|
|
73
|
-
*/
|
|
74
|
-
export async function deleteLocalDirectory(dirPath) {
|
|
75
|
-
try {
|
|
76
|
-
const absolutePath = path.resolve(dirPath);
|
|
77
|
-
await fs.rm(absolutePath, { recursive: true, force: true });
|
|
78
|
-
return { success: true };
|
|
79
|
-
}
|
|
80
|
-
catch (error) {
|
|
81
|
-
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
|
82
|
-
return { success: false, error: `Permission denied: ${dirPath}` };
|
|
83
|
-
}
|
|
84
|
-
return { success: false, error: error.message };
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Move local file with cross-device fallback
|
|
89
|
-
*/
|
|
90
|
-
// glassware[type="implementation", id="impl-cli-fd-local-move--692fd328", requirements="requirement-cli-fd-local-move--022534b1"]
|
|
91
|
-
export async function moveLocalFile(src, dest) {
|
|
92
|
-
try {
|
|
93
|
-
const absoluteSrc = path.resolve(src);
|
|
94
|
-
const absoluteDest = path.resolve(dest);
|
|
95
|
-
// Ensure destination directory exists
|
|
96
|
-
const destDir = path.dirname(absoluteDest);
|
|
97
|
-
await fs.mkdir(destDir, { recursive: true });
|
|
98
|
-
try {
|
|
99
|
-
await fs.rename(absoluteSrc, absoluteDest);
|
|
100
|
-
}
|
|
101
|
-
catch (err) {
|
|
102
|
-
if (err.code === 'EXDEV') {
|
|
103
|
-
// Cross-device: copy then delete
|
|
104
|
-
await fs.copyFile(absoluteSrc, absoluteDest);
|
|
105
|
-
await fs.unlink(absoluteSrc);
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
throw err;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return { success: true };
|
|
112
|
-
}
|
|
113
|
-
catch (error) {
|
|
114
|
-
if (error.code === 'ENOENT') {
|
|
115
|
-
return { success: false, error: `File not found: ${src}` };
|
|
116
|
-
}
|
|
117
|
-
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
|
118
|
-
return { success: false, error: `Permission denied: ${src}` };
|
|
119
|
-
}
|
|
120
|
-
return { success: false, error: error.message };
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* List files in a local directory recursively
|
|
125
|
-
*/
|
|
126
|
-
export async function listLocalFiles(dirPath, pattern) {
|
|
127
|
-
const files = [];
|
|
128
|
-
const absolutePath = path.resolve(dirPath);
|
|
129
|
-
async function walk(dir) {
|
|
130
|
-
try {
|
|
131
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
132
|
-
for (const entry of entries) {
|
|
133
|
-
const fullPath = path.join(dir, entry.name);
|
|
134
|
-
const relativePath = path.relative(absolutePath, fullPath);
|
|
135
|
-
if (entry.isDirectory()) {
|
|
136
|
-
await walk(fullPath);
|
|
137
|
-
}
|
|
138
|
-
else if (entry.isFile()) {
|
|
139
|
-
if (!pattern || matchGlob(relativePath, pattern)) {
|
|
140
|
-
files.push(relativePath);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
// Skip directories we can't read
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
await walk(absolutePath);
|
|
150
|
-
return files;
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Simple glob matching (supports * and **)
|
|
154
|
-
*/
|
|
155
|
-
export function matchGlob(filePath, pattern) {
|
|
156
|
-
// Normalize path separators
|
|
157
|
-
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
158
|
-
const normalizedPattern = pattern.replace(/\\/g, '/');
|
|
159
|
-
// Handle patterns starting with **/ (should match any path depth including root)
|
|
160
|
-
if (normalizedPattern.startsWith('**/')) {
|
|
161
|
-
// **/ means "anywhere in path", so we match either:
|
|
162
|
-
// 1. The file directly at root (just matches the part after **/)
|
|
163
|
-
// 2. The file nested in directories (matches .* followed by / then the part after **/)
|
|
164
|
-
const rest = normalizedPattern.slice(3); // Remove **/
|
|
165
|
-
const restRegex = rest
|
|
166
|
-
.replace(/\./g, '\\.')
|
|
167
|
-
.replace(/\*\*/g, '.*')
|
|
168
|
-
.replace(/\*/g, '[^/]*');
|
|
169
|
-
const regex = new RegExp(`^(.*\\/)?${restRegex}$`);
|
|
170
|
-
return regex.test(normalizedPath);
|
|
171
|
-
}
|
|
172
|
-
// Convert glob to regex for other patterns
|
|
173
|
-
const regexPattern = normalizedPattern
|
|
174
|
-
.replace(/\./g, '\\.') // Escape dots
|
|
175
|
-
.replace(/\*\*/g, '.*') // ** matches anything
|
|
176
|
-
.replace(/\*/g, '[^/]*'); // * matches anything except /
|
|
177
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
178
|
-
return regex.test(normalizedPath);
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Check if a file appears to be binary
|
|
182
|
-
*/
|
|
183
|
-
export async function isBinaryFile(filePath) {
|
|
184
|
-
try {
|
|
185
|
-
const absolutePath = path.resolve(filePath);
|
|
186
|
-
const buffer = Buffer.alloc(512);
|
|
187
|
-
const fd = await fs.open(absolutePath, 'r');
|
|
188
|
-
const { bytesRead } = await fd.read(buffer, 0, 512, 0);
|
|
189
|
-
await fd.close();
|
|
190
|
-
// Check for null bytes (common in binary files)
|
|
191
|
-
for (let i = 0; i < bytesRead; i++) {
|
|
192
|
-
if (buffer[i] === 0) {
|
|
193
|
-
return true;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
catch {
|
|
199
|
-
return false;
|
|
200
|
-
}
|
|
201
|
-
}
|
package/dist/lib/prompts.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
// glassware[type="implementation", id="cli-prompts--bbf3cbc1", requirements="requirement-cli-init-ux-2--b69b045f,requirement-cli-init-ux-3--3dde4846,requirement-cli-init-ux-4--140d9249"]
|
|
2
|
-
import { select as inquirerSelect, input as inquirerInput, confirm as inquirerConfirm } from '@inquirer/prompts';
|
|
3
|
-
/**
|
|
4
|
-
* Display a selection prompt with arrow key navigation.
|
|
5
|
-
*/
|
|
6
|
-
export async function select(question, options) {
|
|
7
|
-
const result = await inquirerSelect({
|
|
8
|
-
message: question,
|
|
9
|
-
choices: options.map(opt => ({
|
|
10
|
-
name: opt.label,
|
|
11
|
-
value: opt.value,
|
|
12
|
-
description: opt.description,
|
|
13
|
-
})),
|
|
14
|
-
});
|
|
15
|
-
return result;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Display a text input prompt and return the value.
|
|
19
|
-
*/
|
|
20
|
-
export async function input(question, options) {
|
|
21
|
-
const result = await inquirerInput({
|
|
22
|
-
message: question,
|
|
23
|
-
default: options?.default,
|
|
24
|
-
validate: options?.validate
|
|
25
|
-
? (value) => options.validate(value) ?? true
|
|
26
|
-
: undefined,
|
|
27
|
-
});
|
|
28
|
-
return result;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Display a yes/no confirmation prompt.
|
|
32
|
-
*/
|
|
33
|
-
export async function confirm(question, options) {
|
|
34
|
-
const result = await inquirerConfirm({
|
|
35
|
-
message: question,
|
|
36
|
-
default: options?.default ?? true,
|
|
37
|
-
});
|
|
38
|
-
return result;
|
|
39
|
-
}
|
|
40
|
-
// glassware[type="implementation", id="impl-validate-workspace-name--6732de6e", requirements="requirement-cli-init-qual-2--4ba7ca9c"]
|
|
41
|
-
/**
|
|
42
|
-
* Validate a workspace name.
|
|
43
|
-
* Returns error message if invalid, null if valid.
|
|
44
|
-
*/
|
|
45
|
-
export function validateWorkspaceName(name) {
|
|
46
|
-
if (!name || name.trim().length === 0) {
|
|
47
|
-
return 'Workspace name cannot be empty';
|
|
48
|
-
}
|
|
49
|
-
if (name.length > 100) {
|
|
50
|
-
return 'Workspace name must be 100 characters or less';
|
|
51
|
-
}
|
|
52
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9-_ ]*[a-zA-Z0-9]$|^[a-zA-Z0-9]$/.test(name.trim())) {
|
|
53
|
-
return 'Workspace name must start and end with alphanumeric characters';
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
}
|
package/dist/lib/storage.js
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
// glassware[type="implementation", id="impl-cli-storage--44a11f2b", specifications="specification-spec-token-storage--26199fe8,specification-spec-config-permissions--18b20291,specification-spec-token-reuse--bfc74a74"]
|
|
2
|
-
// glassware[type="implementation", id="impl-cli-storage-distributed--54f8febe", specifications="specification-spec-storage-dir--54adedf5,specification-spec-create-storage-dir--29d5746d,specification-spec-nodefs-adapter--55a00910,specification-spec-storage-path-config--26888df2"]
|
|
3
|
-
// glassware[type="implementation", id="impl-cli-ws-storage--bb831726", requirements="requirement-cli-ws-storage-location--5691bb5c,requirement-cli-ws-storage-hash--940b982f,requirement-cli-ws-storage-save--e11dd16d,requirement-cli-ws-storage-get--fbbcc532,requirement-cli-ws-storage-remove--05267ecd,requirement-cli-ws-storage-list--f900c191,requirement-cli-ws-storage-atomic--732eb2e0,requirement-cli-ws-storage-permissions--68c455f2"]
|
|
4
|
-
// spec: packages/mod-cli/specs/workspaces.md
|
|
5
|
-
import fs from 'fs';
|
|
6
|
-
import path from 'path';
|
|
7
|
-
import crypto from 'crypto';
|
|
8
|
-
import os from 'os';
|
|
9
|
-
import { DEFAULT_CONFIG, DEFAULT_SETTINGS } from '../types/config.js';
|
|
10
|
-
/**
|
|
11
|
-
* Get the path to the ~/.mod/ directory.
|
|
12
|
-
*/
|
|
13
|
-
export function getModDir() {
|
|
14
|
-
return path.join(os.homedir(), '.mod');
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Ensure the ~/.mod/ directory structure exists.
|
|
18
|
-
* Creates: ~/.mod/, ~/.mod/workspaces/, ~/.mod/automerge/, ~/.mod/logs/
|
|
19
|
-
*/
|
|
20
|
-
export function ensureModDir() {
|
|
21
|
-
const modDir = getModDir();
|
|
22
|
-
const dirs = [
|
|
23
|
-
modDir,
|
|
24
|
-
path.join(modDir, 'workspaces'),
|
|
25
|
-
path.join(modDir, 'automerge'),
|
|
26
|
-
path.join(modDir, 'logs'),
|
|
27
|
-
];
|
|
28
|
-
for (const dir of dirs) {
|
|
29
|
-
if (!fs.existsSync(dir)) {
|
|
30
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Get the path to the config file.
|
|
36
|
-
*/
|
|
37
|
-
export function getConfigPath() {
|
|
38
|
-
return path.join(getModDir(), 'config');
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Read the global config from ~/.mod/config.
|
|
42
|
-
* Returns default config if file doesn't exist or is invalid.
|
|
43
|
-
*/
|
|
44
|
-
export function readConfig() {
|
|
45
|
-
const configPath = getConfigPath();
|
|
46
|
-
if (!fs.existsSync(configPath)) {
|
|
47
|
-
return { ...DEFAULT_CONFIG };
|
|
48
|
-
}
|
|
49
|
-
try {
|
|
50
|
-
const content = fs.readFileSync(configPath, 'utf-8');
|
|
51
|
-
const stored = JSON.parse(content);
|
|
52
|
-
// Merge with defaults for missing fields
|
|
53
|
-
return {
|
|
54
|
-
version: stored.version ?? DEFAULT_CONFIG.version,
|
|
55
|
-
auth: stored.auth ?? null,
|
|
56
|
-
settings: {
|
|
57
|
-
...DEFAULT_SETTINGS,
|
|
58
|
-
...stored.settings,
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
catch (error) {
|
|
63
|
-
// Return defaults if file is corrupted
|
|
64
|
-
console.warn('Warning: Could not read ~/.mod/config, using defaults');
|
|
65
|
-
return { ...DEFAULT_CONFIG };
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Write config to ~/.mod/config atomically.
|
|
70
|
-
* Creates the file with 0600 permissions (user read/write only).
|
|
71
|
-
*/
|
|
72
|
-
export function writeConfig(config) {
|
|
73
|
-
ensureModDir();
|
|
74
|
-
const configPath = getConfigPath();
|
|
75
|
-
atomicWrite(configPath, JSON.stringify(config, null, 2), 0o600);
|
|
76
|
-
}
|
|
77
|
-
// glassware[type="implementation", id="impl-cli-ws-storage-hash--7647b959", requirements="requirement-cli-ws-storage-hash--940b982f,requirement-cli-init-app-6--aa3c1ace"]
|
|
78
|
-
/**
|
|
79
|
-
* Get the path to a workspace connection file for a directory.
|
|
80
|
-
* Uses SHA-256 hash of the absolute path (first 16 chars).
|
|
81
|
-
*/
|
|
82
|
-
export function getWorkspaceConnectionPath(directoryPath) {
|
|
83
|
-
const absolutePath = path.resolve(directoryPath);
|
|
84
|
-
const hash = crypto
|
|
85
|
-
.createHash('sha256')
|
|
86
|
-
.update(absolutePath)
|
|
87
|
-
.digest('hex')
|
|
88
|
-
.slice(0, 16);
|
|
89
|
-
return path.join(getModDir(), 'workspaces', hash);
|
|
90
|
-
}
|
|
91
|
-
// glassware[type="implementation", id="impl-cli-ws-storage-get--bce2619c", requirements="requirement-cli-ws-storage-get--fbbcc532"]
|
|
92
|
-
/**
|
|
93
|
-
* Read the workspace connection for a directory.
|
|
94
|
-
* Returns null if no connection exists.
|
|
95
|
-
*/
|
|
96
|
-
export function readWorkspaceConnection(directoryPath) {
|
|
97
|
-
const connectionPath = getWorkspaceConnectionPath(directoryPath);
|
|
98
|
-
if (!fs.existsSync(connectionPath)) {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
try {
|
|
102
|
-
const content = fs.readFileSync(connectionPath, 'utf-8');
|
|
103
|
-
return JSON.parse(content);
|
|
104
|
-
}
|
|
105
|
-
catch (error) {
|
|
106
|
-
console.warn(`Warning: Could not read workspace connection for ${directoryPath}`);
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
// glassware[type="implementation", id="impl-cli-ws-storage-save--352ee3cc", requirements="requirement-cli-ws-storage-save--e11dd16d,requirement-cli-ws-storage-atomic--732eb2e0,requirement-cli-ws-storage-permissions--68c455f2,requirement-cli-init-qual-3--fc1e8a03"]
|
|
111
|
-
/**
|
|
112
|
-
* Write a workspace connection for a directory atomically.
|
|
113
|
-
*/
|
|
114
|
-
export function writeWorkspaceConnection(directoryPath, connection) {
|
|
115
|
-
ensureModDir();
|
|
116
|
-
const connectionPath = getWorkspaceConnectionPath(directoryPath);
|
|
117
|
-
atomicWrite(connectionPath, JSON.stringify(connection, null, 2), 0o600);
|
|
118
|
-
}
|
|
119
|
-
// glassware[type="implementation", id="impl-cli-ws-storage-remove--7923bc01", requirements="requirement-cli-ws-storage-remove--05267ecd"]
|
|
120
|
-
/**
|
|
121
|
-
* Delete the workspace connection for a directory.
|
|
122
|
-
*/
|
|
123
|
-
export function deleteWorkspaceConnection(directoryPath) {
|
|
124
|
-
const connectionPath = getWorkspaceConnectionPath(directoryPath);
|
|
125
|
-
if (!fs.existsSync(connectionPath)) {
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
try {
|
|
129
|
-
fs.unlinkSync(connectionPath);
|
|
130
|
-
return true;
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
console.warn(`Warning: Could not delete workspace connection for ${directoryPath}`);
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
// glassware[type="implementation", id="impl-cli-ws-storage-list--00734c67", requirements="requirement-cli-ws-storage-list--f900c191,requirement-cli-init-data-0--d659ea61,requirement-cli-init-int-3--ada97918"]
|
|
138
|
-
/**
|
|
139
|
-
* List all workspace connections.
|
|
140
|
-
* Returns array of connections, filtering out corrupted files.
|
|
141
|
-
* Workspace connections map local directories to workspaces (data-0).
|
|
142
|
-
* Connection visible to sync daemon for directory lookup (int-3).
|
|
143
|
-
*/
|
|
144
|
-
export function listWorkspaceConnections() {
|
|
145
|
-
const workspacesDir = path.join(getModDir(), 'workspaces');
|
|
146
|
-
if (!fs.existsSync(workspacesDir)) {
|
|
147
|
-
return [];
|
|
148
|
-
}
|
|
149
|
-
const files = fs.readdirSync(workspacesDir);
|
|
150
|
-
const connections = [];
|
|
151
|
-
for (const file of files) {
|
|
152
|
-
const filePath = path.join(workspacesDir, file);
|
|
153
|
-
try {
|
|
154
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
155
|
-
const connection = JSON.parse(content);
|
|
156
|
-
connections.push(connection);
|
|
157
|
-
}
|
|
158
|
-
catch (error) {
|
|
159
|
-
// Skip corrupted files
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return connections;
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Find workspace connection for the current directory or any parent.
|
|
167
|
-
* Walks up the directory tree looking for a connection.
|
|
168
|
-
*/
|
|
169
|
-
export function findWorkspaceConnection(startPath) {
|
|
170
|
-
let currentPath = path.resolve(startPath);
|
|
171
|
-
const root = path.parse(currentPath).root;
|
|
172
|
-
while (currentPath !== root) {
|
|
173
|
-
const connection = readWorkspaceConnection(currentPath);
|
|
174
|
-
if (connection) {
|
|
175
|
-
return connection;
|
|
176
|
-
}
|
|
177
|
-
currentPath = path.dirname(currentPath);
|
|
178
|
-
}
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Atomic write: write to temp file then rename.
|
|
183
|
-
* Ensures file is never partially written.
|
|
184
|
-
*/
|
|
185
|
-
function atomicWrite(filePath, content, mode) {
|
|
186
|
-
const tempPath = `${filePath}.tmp.${process.pid}`;
|
|
187
|
-
fs.writeFileSync(tempPath, content, { mode });
|
|
188
|
-
fs.renameSync(tempPath, filePath);
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Get the path to the sync daemon PID file.
|
|
192
|
-
*/
|
|
193
|
-
export function getPidFilePath() {
|
|
194
|
-
return path.join(getModDir(), 'sync.pid');
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Get the path to the automerge storage directory.
|
|
198
|
-
*/
|
|
199
|
-
export function getAutomergeStoragePath() {
|
|
200
|
-
return path.join(getModDir(), 'automerge');
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Get the path to the logs directory.
|
|
204
|
-
*/
|
|
205
|
-
export function getLogsDir() {
|
|
206
|
-
return path.join(getModDir(), 'logs');
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Get the path to the sync daemon log file.
|
|
210
|
-
*/
|
|
211
|
-
export function getSyncLogPath() {
|
|
212
|
-
return path.join(getLogsDir(), 'sync.log');
|
|
213
|
-
}
|