@mod-computer/cli 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -0
- package/dist/cli.bundle.js +24633 -13744
- package/dist/cli.bundle.js.map +4 -4
- package/dist/cli.js +23 -12
- package/dist/commands/add.js +245 -0
- package/dist/commands/auth.js +129 -21
- package/dist/commands/comment.js +568 -0
- package/dist/commands/diff.js +182 -0
- package/dist/commands/index.js +33 -3
- package/dist/commands/init.js +545 -326
- package/dist/commands/ls.js +135 -0
- package/dist/commands/members.js +687 -0
- package/dist/commands/mv.js +282 -0
- package/dist/commands/rm.js +257 -0
- package/dist/commands/status.js +273 -306
- package/dist/commands/sync.js +99 -75
- package/dist/commands/trace.js +1752 -0
- package/dist/commands/workspace.js +354 -330
- package/dist/config/features.js +8 -3
- package/dist/config/release-profiles/development.json +4 -1
- package/dist/config/release-profiles/mvp.json +4 -2
- package/dist/daemon/conflict-resolution.js +172 -0
- package/dist/daemon/content-hash.js +31 -0
- package/dist/daemon/file-sync.js +985 -0
- package/dist/daemon/index.js +203 -0
- package/dist/daemon/mime-types.js +166 -0
- package/dist/daemon/offline-queue.js +211 -0
- package/dist/daemon/path-utils.js +64 -0
- package/dist/daemon/share-policy.js +83 -0
- package/dist/daemon/wasm-errors.js +189 -0
- package/dist/daemon/worker.js +557 -0
- package/dist/daemon-worker.js +3 -2
- package/dist/errors/workspace-errors.js +48 -0
- package/dist/lib/auth-server.js +89 -26
- package/dist/lib/browser.js +1 -1
- package/dist/lib/diff.js +284 -0
- package/dist/lib/formatters.js +204 -0
- package/dist/lib/git.js +137 -0
- package/dist/lib/local-fs.js +201 -0
- package/dist/lib/prompts.js +56 -0
- package/dist/lib/storage.js +11 -1
- package/dist/lib/trace-formatters.js +314 -0
- package/dist/services/add-service.js +554 -0
- package/dist/services/add-validation.js +124 -0
- package/dist/services/mod-config.js +8 -2
- package/dist/services/modignore-service.js +2 -0
- package/dist/stores/use-workspaces-store.js +36 -14
- package/dist/types/add-types.js +99 -0
- package/dist/types/config.js +1 -1
- package/dist/types/workspace-connection.js +53 -2
- package/package.json +7 -5
- package/commands/execute.md +0 -156
- package/commands/overview.md +0 -233
- package/commands/review.md +0 -151
- package/commands/spec.md +0 -169
package/dist/lib/git.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// glassware[type="implementation", id="impl-cli-git-utils--7e4f29ca", requirements="requirement-cli-git-app-1--f5a33b8c,requirement-cli-git-app-2--4f3b552c,requirement-cli-git-qual-1--98d1bacb,requirement-cli-git-qual-2--503addb9,requirement-cli-git-qual-3--5af20852,requirement-cli-git-qual-4--7c40baf1"]
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
/**
|
|
5
|
+
* Detect if a directory is inside a git repository.
|
|
6
|
+
* Walks up the directory tree looking for a .git directory.
|
|
7
|
+
*/
|
|
8
|
+
// glassware[type="implementation", id="impl-cli-detect-git-repo--86f0b85f", specifications="specification-spec-is-git-repo--8d217687"]
|
|
9
|
+
export function detectGitRepo(directory) {
|
|
10
|
+
let currentDir = path.resolve(directory);
|
|
11
|
+
const root = path.parse(currentDir).root;
|
|
12
|
+
while (currentDir !== root) {
|
|
13
|
+
const gitDir = path.join(currentDir, '.git');
|
|
14
|
+
if (fs.existsSync(gitDir)) {
|
|
15
|
+
const headFile = path.join(gitDir, 'HEAD');
|
|
16
|
+
const branchResult = readCurrentBranchWithDetached(headFile);
|
|
17
|
+
return {
|
|
18
|
+
isGitRepo: true,
|
|
19
|
+
branch: branchResult.branch,
|
|
20
|
+
gitDir,
|
|
21
|
+
headFile,
|
|
22
|
+
isDetached: branchResult.isDetached,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
currentDir = path.dirname(currentDir);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
isGitRepo: false,
|
|
29
|
+
branch: null,
|
|
30
|
+
gitDir: null,
|
|
31
|
+
headFile: null,
|
|
32
|
+
isDetached: false,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Read the current branch name from .git/HEAD with detached state info.
|
|
37
|
+
* Returns branch name and whether in detached HEAD state.
|
|
38
|
+
*/
|
|
39
|
+
// glassware[type="implementation", id="impl-cli-read-branch-detached--42ec893d", specifications="specification-spec-detect-branch--76e466f2,specification-spec-detached-head--0fce4d44"]
|
|
40
|
+
export function readCurrentBranchWithDetached(headFile) {
|
|
41
|
+
try {
|
|
42
|
+
if (!fs.existsSync(headFile)) {
|
|
43
|
+
return { branch: null, isDetached: false };
|
|
44
|
+
}
|
|
45
|
+
const content = fs.readFileSync(headFile, 'utf8').trim();
|
|
46
|
+
// Check if it's a symbolic ref (normal branch)
|
|
47
|
+
// Format: "ref: refs/heads/branch-name"
|
|
48
|
+
if (content.startsWith('ref: refs/heads/')) {
|
|
49
|
+
return {
|
|
50
|
+
branch: content.replace('ref: refs/heads/', ''),
|
|
51
|
+
isDetached: false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Detached HEAD (commit hash) - return detached-{hash} format per spec
|
|
55
|
+
if (/^[0-9a-f]{40}$/i.test(content)) {
|
|
56
|
+
return {
|
|
57
|
+
branch: `detached-${content.substring(0, 7)}`,
|
|
58
|
+
isDetached: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return { branch: null, isDetached: false };
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return { branch: null, isDetached: false };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Read the current branch name from .git/HEAD.
|
|
69
|
+
* Returns null if detached HEAD or unable to read.
|
|
70
|
+
*/
|
|
71
|
+
export function readCurrentBranch(headFile) {
|
|
72
|
+
return readCurrentBranchWithDetached(headFile).branch;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get git branch for a directory.
|
|
76
|
+
* Convenience function that returns just the branch name or null.
|
|
77
|
+
*/
|
|
78
|
+
export function getGitBranch(directory) {
|
|
79
|
+
const gitInfo = detectGitRepo(directory);
|
|
80
|
+
return gitInfo.branch;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Check if a path should be ignored based on git patterns.
|
|
84
|
+
* This is a simplified check for common patterns.
|
|
85
|
+
*/
|
|
86
|
+
export function isGitIgnoredPath(relativePath) {
|
|
87
|
+
// Always ignore .git directory contents
|
|
88
|
+
if (relativePath.startsWith('.git/') || relativePath === '.git') {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
// Ignore common patterns
|
|
92
|
+
const ignoredPatterns = [
|
|
93
|
+
/^node_modules\//,
|
|
94
|
+
/^\.git\//,
|
|
95
|
+
/^dist\//,
|
|
96
|
+
/^build\//,
|
|
97
|
+
/^\.next\//,
|
|
98
|
+
/^\.nuxt\//,
|
|
99
|
+
/^coverage\//,
|
|
100
|
+
/^\.cache\//,
|
|
101
|
+
/^\.automerge-data\//,
|
|
102
|
+
/\.log$/,
|
|
103
|
+
/\.lock$/,
|
|
104
|
+
/^\.DS_Store$/,
|
|
105
|
+
/^Thumbs\.db$/,
|
|
106
|
+
];
|
|
107
|
+
return ignoredPatterns.some(pattern => pattern.test(relativePath));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check if a git operation is in progress (rebase, merge, etc).
|
|
111
|
+
* When active git operations are in progress, syncing should be paused.
|
|
112
|
+
*/
|
|
113
|
+
// glassware[type="implementation", id="impl-cli-git-operation-check--c31739c8", specifications="specification-spec-git-operations--addf801f"]
|
|
114
|
+
export function isGitOperationInProgress(directory) {
|
|
115
|
+
const gitInfo = detectGitRepo(directory);
|
|
116
|
+
if (!gitInfo.isGitRepo || !gitInfo.gitDir) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
const gitDir = gitInfo.gitDir;
|
|
120
|
+
// Check for various git operations in progress
|
|
121
|
+
const operationIndicators = [
|
|
122
|
+
path.join(gitDir, 'rebase-merge'), // git rebase in progress
|
|
123
|
+
path.join(gitDir, 'rebase-apply'), // git am or rebase --apply in progress
|
|
124
|
+
path.join(gitDir, 'MERGE_HEAD'), // git merge in progress
|
|
125
|
+
path.join(gitDir, 'CHERRY_PICK_HEAD'), // git cherry-pick in progress
|
|
126
|
+
path.join(gitDir, 'REVERT_HEAD'), // git revert in progress
|
|
127
|
+
path.join(gitDir, 'BISECT_LOG'), // git bisect in progress
|
|
128
|
+
];
|
|
129
|
+
return operationIndicators.some(indicator => fs.existsSync(indicator));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Check if the .git directory exists and is valid.
|
|
133
|
+
*/
|
|
134
|
+
export function isGitRepo(directory) {
|
|
135
|
+
const gitInfo = detectGitRepo(directory);
|
|
136
|
+
return gitInfo.isGitRepo;
|
|
137
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
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
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
// glassware[type=implementation, id=cli-storage
|
|
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
|
|
2
5
|
import fs from 'fs';
|
|
3
6
|
import path from 'path';
|
|
4
7
|
import crypto from 'crypto';
|
|
@@ -71,6 +74,7 @@ export function writeConfig(config) {
|
|
|
71
74
|
const configPath = getConfigPath();
|
|
72
75
|
atomicWrite(configPath, JSON.stringify(config, null, 2), 0o600);
|
|
73
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"]
|
|
74
78
|
/**
|
|
75
79
|
* Get the path to a workspace connection file for a directory.
|
|
76
80
|
* Uses SHA-256 hash of the absolute path (first 16 chars).
|
|
@@ -84,6 +88,7 @@ export function getWorkspaceConnectionPath(directoryPath) {
|
|
|
84
88
|
.slice(0, 16);
|
|
85
89
|
return path.join(getModDir(), 'workspaces', hash);
|
|
86
90
|
}
|
|
91
|
+
// glassware[type="implementation", id="impl-cli-ws-storage-get--bce2619c", requirements="requirement-cli-ws-storage-get--fbbcc532"]
|
|
87
92
|
/**
|
|
88
93
|
* Read the workspace connection for a directory.
|
|
89
94
|
* Returns null if no connection exists.
|
|
@@ -102,6 +107,7 @@ export function readWorkspaceConnection(directoryPath) {
|
|
|
102
107
|
return null;
|
|
103
108
|
}
|
|
104
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"]
|
|
105
111
|
/**
|
|
106
112
|
* Write a workspace connection for a directory atomically.
|
|
107
113
|
*/
|
|
@@ -110,6 +116,7 @@ export function writeWorkspaceConnection(directoryPath, connection) {
|
|
|
110
116
|
const connectionPath = getWorkspaceConnectionPath(directoryPath);
|
|
111
117
|
atomicWrite(connectionPath, JSON.stringify(connection, null, 2), 0o600);
|
|
112
118
|
}
|
|
119
|
+
// glassware[type="implementation", id="impl-cli-ws-storage-remove--7923bc01", requirements="requirement-cli-ws-storage-remove--05267ecd"]
|
|
113
120
|
/**
|
|
114
121
|
* Delete the workspace connection for a directory.
|
|
115
122
|
*/
|
|
@@ -127,9 +134,12 @@ export function deleteWorkspaceConnection(directoryPath) {
|
|
|
127
134
|
return false;
|
|
128
135
|
}
|
|
129
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"]
|
|
130
138
|
/**
|
|
131
139
|
* List all workspace connections.
|
|
132
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).
|
|
133
143
|
*/
|
|
134
144
|
export function listWorkspaceConnections() {
|
|
135
145
|
const workspacesDir = path.join(getModDir(), 'workspaces');
|