@nahisaho/katashiro-workspace 0.4.0
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 +182 -0
- package/package.json +47 -0
- package/src/docker-workspace.ts +416 -0
- package/src/index.ts +41 -0
- package/src/local-workspace.ts +311 -0
- package/src/types.ts +242 -0
- package/src/workspace-factory.ts +145 -0
- package/tests/local-workspace.test.ts +283 -0
- package/tests/workspace-factory.test.ts +120 -0
- package/tsconfig.json +12 -0
package/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# @nahisaho/katashiro-workspace
|
|
2
|
+
|
|
3
|
+
KATASHIRO Workspace Management - Unified workspace interface for file operations across local, remote, and Docker environments.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Unified Interface** (REQ-011-01): Single interface for file operations regardless of workspace type
|
|
8
|
+
- **LocalWorkspace** (REQ-011-02): Operations on local file system
|
|
9
|
+
- **DockerWorkspace** (REQ-011-04): Operations within Docker containers
|
|
10
|
+
- **Identical API** (REQ-011-05): Tools work the same regardless of workspace type
|
|
11
|
+
- **Fast Search** (REQ-011-06): Glob search within 1 second for up to 10,000 files
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @nahisaho/katashiro-workspace
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### LocalWorkspace
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { LocalWorkspace, WorkspaceFactory } from '@nahisaho/katashiro-workspace';
|
|
25
|
+
|
|
26
|
+
// Direct instantiation
|
|
27
|
+
const workspace = new LocalWorkspace({
|
|
28
|
+
type: 'local',
|
|
29
|
+
workingDir: '/path/to/project',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Or using factory
|
|
33
|
+
const workspace2 = WorkspaceFactory.createLocal('/path/to/project');
|
|
34
|
+
|
|
35
|
+
// File operations
|
|
36
|
+
const content = await workspace.read('README.md');
|
|
37
|
+
await workspace.write('output.txt', 'Hello World');
|
|
38
|
+
const files = await workspace.search('**/*.ts');
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### DockerWorkspace
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { DockerWorkspace, WorkspaceFactory } from '@nahisaho/katashiro-workspace';
|
|
45
|
+
|
|
46
|
+
const workspace = new DockerWorkspace({
|
|
47
|
+
type: 'docker',
|
|
48
|
+
containerId: 'container123',
|
|
49
|
+
workingDir: '/app',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Initialize connection
|
|
53
|
+
await workspace.initialize();
|
|
54
|
+
|
|
55
|
+
// Same API as LocalWorkspace
|
|
56
|
+
const content = await workspace.read('config.json');
|
|
57
|
+
await workspace.write('output.txt', 'Hello Docker');
|
|
58
|
+
|
|
59
|
+
// Cleanup
|
|
60
|
+
await workspace.cleanup();
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Factory Pattern
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { WorkspaceFactory, createWorkspace } from '@nahisaho/katashiro-workspace';
|
|
67
|
+
|
|
68
|
+
// Create workspace from config
|
|
69
|
+
const workspace = WorkspaceFactory.create({
|
|
70
|
+
type: 'local', // or 'docker'
|
|
71
|
+
workingDir: '/path/to/project',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Create and initialize in one step
|
|
75
|
+
const workspace2 = await createWorkspace({
|
|
76
|
+
type: 'docker',
|
|
77
|
+
containerId: 'container123',
|
|
78
|
+
workingDir: '/app',
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Unified Tool Development (REQ-011-05)
|
|
83
|
+
|
|
84
|
+
Tools can be written once and work with any workspace type:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import type { Workspace } from '@nahisaho/katashiro-workspace';
|
|
88
|
+
|
|
89
|
+
// Tool that works with any workspace type
|
|
90
|
+
async function analyzeProject(workspace: Workspace): Promise<ProjectAnalysis> {
|
|
91
|
+
// Read package.json
|
|
92
|
+
const packageJson = await workspace.read('package.json');
|
|
93
|
+
|
|
94
|
+
// Search for TypeScript files
|
|
95
|
+
const tsFiles = await workspace.search('**/*.ts');
|
|
96
|
+
|
|
97
|
+
// List src directory
|
|
98
|
+
const srcFiles = await workspace.list('src');
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
dependencies: JSON.parse(packageJson).dependencies,
|
|
102
|
+
fileCount: tsFiles.length,
|
|
103
|
+
srcEntries: srcFiles.map(f => f.name),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Use with LocalWorkspace
|
|
108
|
+
const local = WorkspaceFactory.createLocal('/local/project');
|
|
109
|
+
const localResult = await analyzeProject(local);
|
|
110
|
+
|
|
111
|
+
// Use with DockerWorkspace
|
|
112
|
+
const docker = WorkspaceFactory.createDocker('container123', '/app');
|
|
113
|
+
await docker.initialize();
|
|
114
|
+
const dockerResult = await analyzeProject(docker);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## API Reference
|
|
118
|
+
|
|
119
|
+
### Workspace Interface
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
interface Workspace {
|
|
123
|
+
readonly type: 'local' | 'remote' | 'docker';
|
|
124
|
+
readonly workingDir: string;
|
|
125
|
+
readonly readOnly: boolean;
|
|
126
|
+
|
|
127
|
+
read(path: string, encoding?: BufferEncoding): Promise<string>;
|
|
128
|
+
readBuffer(path: string): Promise<Buffer>;
|
|
129
|
+
write(path: string, content: string): Promise<void>;
|
|
130
|
+
writeBuffer(path: string, buffer: Buffer): Promise<void>;
|
|
131
|
+
list(path: string): Promise<FileInfo[]>;
|
|
132
|
+
listEntries(path: string): Promise<DirectoryEntry[]>;
|
|
133
|
+
search(pattern: string): Promise<string[]>;
|
|
134
|
+
exists(path: string): Promise<boolean>;
|
|
135
|
+
delete(path: string): Promise<void>;
|
|
136
|
+
mkdir(path: string, recursive?: boolean): Promise<void>;
|
|
137
|
+
stat(path: string): Promise<FileInfo>;
|
|
138
|
+
copy(src: string, dest: string): Promise<void>;
|
|
139
|
+
move(src: string, dest: string): Promise<void>;
|
|
140
|
+
initialize?(): Promise<void>;
|
|
141
|
+
cleanup?(): Promise<void>;
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Error Handling
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import { WorkspaceError } from '@nahisaho/katashiro-workspace';
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
await workspace.read('nonexistent.txt');
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error instanceof WorkspaceError) {
|
|
154
|
+
switch (error.code) {
|
|
155
|
+
case 'NOT_FOUND':
|
|
156
|
+
console.log('File not found:', error.path);
|
|
157
|
+
break;
|
|
158
|
+
case 'PERMISSION_DENIED':
|
|
159
|
+
console.log('Permission denied');
|
|
160
|
+
break;
|
|
161
|
+
case 'READ_ONLY':
|
|
162
|
+
console.log('Workspace is read-only');
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Requirements Coverage
|
|
170
|
+
|
|
171
|
+
| Requirement | Status | Description |
|
|
172
|
+
|------------|--------|-------------|
|
|
173
|
+
| REQ-011-01 | ✅ | Unified interface for file operations |
|
|
174
|
+
| REQ-011-02 | ✅ | LocalWorkspace for local file system |
|
|
175
|
+
| REQ-011-03 | ⏳ | RemoteWorkspace (planned) |
|
|
176
|
+
| REQ-011-04 | ✅ | DockerWorkspace for Docker containers |
|
|
177
|
+
| REQ-011-05 | ✅ | Tools work identically regardless of type |
|
|
178
|
+
| REQ-011-06 | ✅ | Fast glob search |
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nahisaho/katashiro-workspace",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "KATASHIRO Workspace Management - Unified workspace interface for file operations",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"test:watch": "vitest",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"clean": "rm -rf dist"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@nahisaho/katashiro-core": "workspace:*",
|
|
23
|
+
"fast-glob": "^3.3.2",
|
|
24
|
+
"dockerode": "^4.0.2"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/dockerode": "^3.3.31",
|
|
28
|
+
"@types/node": "^22.10.7",
|
|
29
|
+
"typescript": "^5.7.3",
|
|
30
|
+
"vitest": "^2.1.9"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"katashiro",
|
|
34
|
+
"workspace",
|
|
35
|
+
"file-operations",
|
|
36
|
+
"local-workspace",
|
|
37
|
+
"remote-workspace",
|
|
38
|
+
"docker-workspace"
|
|
39
|
+
],
|
|
40
|
+
"author": "nahisaho",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/nahisaho/KATASHIRO.git",
|
|
45
|
+
"directory": "katashiro/packages/workspace"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DockerWorkspace - Dockerコンテナ内ファイルシステムワークスペース
|
|
3
|
+
*
|
|
4
|
+
* @requirement REQ-011-04
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Dockerode from 'dockerode';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import type {
|
|
10
|
+
Workspace,
|
|
11
|
+
DockerWorkspaceConfig,
|
|
12
|
+
FileInfo,
|
|
13
|
+
DirectoryEntry,
|
|
14
|
+
} from './types.js';
|
|
15
|
+
import { WorkspaceError, DEFAULT_DOCKER_SOCKET } from './types.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* DockerWorkspace - Dockerコンテナ内でのファイル操作
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const workspace = new DockerWorkspace({
|
|
23
|
+
* type: 'docker',
|
|
24
|
+
* containerId: 'container123',
|
|
25
|
+
* workingDir: '/app',
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* await workspace.initialize();
|
|
29
|
+
* const content = await workspace.read('config.json');
|
|
30
|
+
* await workspace.write('output.txt', 'Hello Docker');
|
|
31
|
+
* await workspace.cleanup();
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export class DockerWorkspace implements Workspace {
|
|
35
|
+
readonly type = 'docker' as const;
|
|
36
|
+
readonly workingDir: string;
|
|
37
|
+
readonly readOnly: boolean;
|
|
38
|
+
|
|
39
|
+
private docker: Dockerode;
|
|
40
|
+
private container: Dockerode.Container;
|
|
41
|
+
private containerId: string;
|
|
42
|
+
|
|
43
|
+
constructor(config: DockerWorkspaceConfig) {
|
|
44
|
+
this.containerId = config.containerId;
|
|
45
|
+
this.workingDir = config.workingDir;
|
|
46
|
+
this.readOnly = config.readOnly ?? false;
|
|
47
|
+
|
|
48
|
+
this.docker = new Dockerode({
|
|
49
|
+
socketPath: config.socketPath || DEFAULT_DOCKER_SOCKET,
|
|
50
|
+
});
|
|
51
|
+
this.container = this.docker.getContainer(this.containerId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* パスを解決
|
|
56
|
+
*/
|
|
57
|
+
private resolvePath(filePath: string): string {
|
|
58
|
+
if (path.isAbsolute(filePath)) {
|
|
59
|
+
return filePath;
|
|
60
|
+
}
|
|
61
|
+
return path.posix.join(this.workingDir, filePath);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 書き込み可能か検証
|
|
66
|
+
*/
|
|
67
|
+
private validateWritable(): void {
|
|
68
|
+
if (this.readOnly) {
|
|
69
|
+
throw new WorkspaceError('READ_ONLY', 'Workspace is read-only');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* コンテナ内でコマンドを実行
|
|
75
|
+
*/
|
|
76
|
+
private async exec(cmd: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
77
|
+
try {
|
|
78
|
+
const exec = await this.container.exec({
|
|
79
|
+
Cmd: cmd,
|
|
80
|
+
AttachStdout: true,
|
|
81
|
+
AttachStderr: true,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
exec.start({ hijack: true, stdin: false }, (err, stream) => {
|
|
86
|
+
if (err) {
|
|
87
|
+
reject(new WorkspaceError('OPERATION_FAILED', `Exec failed: ${err.message}`));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!stream) {
|
|
92
|
+
reject(new WorkspaceError('OPERATION_FAILED', 'No stream returned'));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let stdout = '';
|
|
97
|
+
let stderr = '';
|
|
98
|
+
|
|
99
|
+
// Docker multiplexed stream handling
|
|
100
|
+
stream.on('data', (chunk: Buffer) => {
|
|
101
|
+
// Docker stream format: 8-byte header + data
|
|
102
|
+
// First byte: 1=stdout, 2=stderr
|
|
103
|
+
if (chunk.length > 8) {
|
|
104
|
+
const streamType = chunk[0];
|
|
105
|
+
const data = chunk.slice(8).toString('utf-8');
|
|
106
|
+
if (streamType === 1) {
|
|
107
|
+
stdout += data;
|
|
108
|
+
} else if (streamType === 2) {
|
|
109
|
+
stderr += data;
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
stdout += chunk.toString('utf-8');
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
stream.on('end', async () => {
|
|
117
|
+
try {
|
|
118
|
+
const inspectResult = await exec.inspect();
|
|
119
|
+
resolve({
|
|
120
|
+
stdout: stdout.trim(),
|
|
121
|
+
stderr: stderr.trim(),
|
|
122
|
+
exitCode: inspectResult.ExitCode ?? 0,
|
|
123
|
+
});
|
|
124
|
+
} catch {
|
|
125
|
+
resolve({ stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
stream.on('error', (error: Error) => {
|
|
130
|
+
reject(new WorkspaceError('OPERATION_FAILED', `Stream error: ${error.message}`));
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
throw new WorkspaceError(
|
|
136
|
+
'CONNECTION_FAILED',
|
|
137
|
+
`Failed to execute command: ${error instanceof Error ? error.message : String(error)}`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async initialize(): Promise<void> {
|
|
143
|
+
try {
|
|
144
|
+
const info = await this.container.inspect();
|
|
145
|
+
if (!info.State.Running) {
|
|
146
|
+
throw new WorkspaceError(
|
|
147
|
+
'CONNECTION_FAILED',
|
|
148
|
+
`Container ${this.containerId} is not running`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (error instanceof WorkspaceError) throw error;
|
|
153
|
+
throw new WorkspaceError(
|
|
154
|
+
'CONNECTION_FAILED',
|
|
155
|
+
`Failed to connect to container: ${error instanceof Error ? error.message : String(error)}`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async cleanup(): Promise<void> {
|
|
161
|
+
// DockerWorkspaceはステートレスなので特にクリーンアップは不要
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async read(filePath: string, _encoding: BufferEncoding = 'utf-8'): Promise<string> {
|
|
165
|
+
const resolved = this.resolvePath(filePath);
|
|
166
|
+
|
|
167
|
+
const result = await this.exec(['cat', resolved]);
|
|
168
|
+
if (result.exitCode !== 0) {
|
|
169
|
+
throw this.parseError(result.stderr, filePath);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return result.stdout;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async readBuffer(filePath: string): Promise<Buffer> {
|
|
176
|
+
const resolved = this.resolvePath(filePath);
|
|
177
|
+
|
|
178
|
+
const result = await this.exec(['cat', resolved]);
|
|
179
|
+
if (result.exitCode !== 0) {
|
|
180
|
+
throw this.parseError(result.stderr, filePath);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return Buffer.from(result.stdout, 'utf-8');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async write(filePath: string, content: string): Promise<void> {
|
|
187
|
+
this.validateWritable();
|
|
188
|
+
const resolved = this.resolvePath(filePath);
|
|
189
|
+
|
|
190
|
+
// ディレクトリを作成
|
|
191
|
+
const dir = path.posix.dirname(resolved);
|
|
192
|
+
await this.exec(['mkdir', '-p', dir]);
|
|
193
|
+
|
|
194
|
+
// base64エンコードして書き込み(特殊文字対策)
|
|
195
|
+
const encoded = Buffer.from(content, 'utf-8').toString('base64');
|
|
196
|
+
const result = await this.exec(['sh', '-c', `echo '${encoded}' | base64 -d > '${resolved}'`]);
|
|
197
|
+
|
|
198
|
+
if (result.exitCode !== 0) {
|
|
199
|
+
throw this.parseError(result.stderr, filePath);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async writeBuffer(filePath: string, buffer: Buffer): Promise<void> {
|
|
204
|
+
this.validateWritable();
|
|
205
|
+
const resolved = this.resolvePath(filePath);
|
|
206
|
+
|
|
207
|
+
const dir = path.posix.dirname(resolved);
|
|
208
|
+
await this.exec(['mkdir', '-p', dir]);
|
|
209
|
+
|
|
210
|
+
const encoded = buffer.toString('base64');
|
|
211
|
+
const result = await this.exec(['sh', '-c', `echo '${encoded}' | base64 -d > '${resolved}'`]);
|
|
212
|
+
|
|
213
|
+
if (result.exitCode !== 0) {
|
|
214
|
+
throw this.parseError(result.stderr, filePath);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async list(dirPath: string): Promise<FileInfo[]> {
|
|
219
|
+
const resolved = this.resolvePath(dirPath);
|
|
220
|
+
|
|
221
|
+
const result = await this.exec([
|
|
222
|
+
'sh',
|
|
223
|
+
'-c',
|
|
224
|
+
`ls -la --time-style=+%s '${resolved}' | tail -n +2`,
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
if (result.exitCode !== 0) {
|
|
228
|
+
throw this.parseError(result.stderr, dirPath);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return this.parseLsOutput(result.stdout, resolved);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async listEntries(dirPath: string): Promise<DirectoryEntry[]> {
|
|
235
|
+
const resolved = this.resolvePath(dirPath);
|
|
236
|
+
|
|
237
|
+
const result = await this.exec(['sh', '-c', `ls -la '${resolved}' | tail -n +2`]);
|
|
238
|
+
|
|
239
|
+
if (result.exitCode !== 0) {
|
|
240
|
+
throw this.parseError(result.stderr, dirPath);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return this.parseLsEntries(result.stdout);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async search(pattern: string): Promise<string[]> {
|
|
247
|
+
const result = await this.exec([
|
|
248
|
+
'sh',
|
|
249
|
+
'-c',
|
|
250
|
+
`cd '${this.workingDir}' && find . -type f -name '${pattern}' 2>/dev/null | head -10000`,
|
|
251
|
+
]);
|
|
252
|
+
|
|
253
|
+
if (result.exitCode !== 0 && result.stderr) {
|
|
254
|
+
throw new WorkspaceError('OPERATION_FAILED', `Search failed: ${result.stderr}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return result.stdout
|
|
258
|
+
.split('\n')
|
|
259
|
+
.filter(line => line.trim())
|
|
260
|
+
.map(line => line.replace(/^\.\//, ''));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async exists(filePath: string): Promise<boolean> {
|
|
264
|
+
const resolved = this.resolvePath(filePath);
|
|
265
|
+
|
|
266
|
+
const result = await this.exec(['test', '-e', resolved]);
|
|
267
|
+
return result.exitCode === 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async delete(filePath: string): Promise<void> {
|
|
271
|
+
this.validateWritable();
|
|
272
|
+
const resolved = this.resolvePath(filePath);
|
|
273
|
+
|
|
274
|
+
const result = await this.exec(['rm', '-rf', resolved]);
|
|
275
|
+
if (result.exitCode !== 0) {
|
|
276
|
+
throw this.parseError(result.stderr, filePath);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async mkdir(dirPath: string, recursive = true): Promise<void> {
|
|
281
|
+
this.validateWritable();
|
|
282
|
+
const resolved = this.resolvePath(dirPath);
|
|
283
|
+
|
|
284
|
+
const cmd = recursive ? ['mkdir', '-p', resolved] : ['mkdir', resolved];
|
|
285
|
+
const result = await this.exec(cmd);
|
|
286
|
+
|
|
287
|
+
if (result.exitCode !== 0) {
|
|
288
|
+
throw this.parseError(result.stderr, dirPath);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async stat(filePath: string): Promise<FileInfo> {
|
|
293
|
+
const resolved = this.resolvePath(filePath);
|
|
294
|
+
|
|
295
|
+
const result = await this.exec([
|
|
296
|
+
'stat',
|
|
297
|
+
'-c',
|
|
298
|
+
'%n|%s|%F|%Y|%W',
|
|
299
|
+
resolved,
|
|
300
|
+
]);
|
|
301
|
+
|
|
302
|
+
if (result.exitCode !== 0) {
|
|
303
|
+
throw this.parseError(result.stderr, filePath);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const [name, size, type, mtime, ctime] = result.stdout.split('|');
|
|
307
|
+
return {
|
|
308
|
+
name: path.posix.basename(name),
|
|
309
|
+
path: resolved,
|
|
310
|
+
size: parseInt(size, 10),
|
|
311
|
+
isDirectory: type === 'directory',
|
|
312
|
+
modifiedAt: new Date(parseInt(mtime, 10) * 1000),
|
|
313
|
+
createdAt: new Date(parseInt(ctime, 10) * 1000),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async copy(src: string, dest: string): Promise<void> {
|
|
318
|
+
this.validateWritable();
|
|
319
|
+
const srcResolved = this.resolvePath(src);
|
|
320
|
+
const destResolved = this.resolvePath(dest);
|
|
321
|
+
|
|
322
|
+
const dir = path.posix.dirname(destResolved);
|
|
323
|
+
await this.exec(['mkdir', '-p', dir]);
|
|
324
|
+
|
|
325
|
+
const result = await this.exec(['cp', '-r', srcResolved, destResolved]);
|
|
326
|
+
if (result.exitCode !== 0) {
|
|
327
|
+
throw this.parseError(result.stderr, src);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async move(src: string, dest: string): Promise<void> {
|
|
332
|
+
this.validateWritable();
|
|
333
|
+
const srcResolved = this.resolvePath(src);
|
|
334
|
+
const destResolved = this.resolvePath(dest);
|
|
335
|
+
|
|
336
|
+
const dir = path.posix.dirname(destResolved);
|
|
337
|
+
await this.exec(['mkdir', '-p', dir]);
|
|
338
|
+
|
|
339
|
+
const result = await this.exec(['mv', srcResolved, destResolved]);
|
|
340
|
+
if (result.exitCode !== 0) {
|
|
341
|
+
throw this.parseError(result.stderr, src);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* lsの出力をパース
|
|
347
|
+
*/
|
|
348
|
+
private parseLsOutput(output: string, basePath: string): FileInfo[] {
|
|
349
|
+
return output
|
|
350
|
+
.split('\n')
|
|
351
|
+
.filter(line => line.trim())
|
|
352
|
+
.map(line => {
|
|
353
|
+
const parts = line.split(/\s+/);
|
|
354
|
+
if (parts.length < 7) return null;
|
|
355
|
+
|
|
356
|
+
const permissions = parts[0];
|
|
357
|
+
const size = parseInt(parts[4], 10);
|
|
358
|
+
const timestamp = parseInt(parts[5], 10);
|
|
359
|
+
const name = parts.slice(6).join(' ');
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
name,
|
|
363
|
+
path: path.posix.join(basePath, name),
|
|
364
|
+
size: isNaN(size) ? 0 : size,
|
|
365
|
+
isDirectory: permissions.startsWith('d'),
|
|
366
|
+
modifiedAt: new Date(timestamp * 1000),
|
|
367
|
+
createdAt: new Date(timestamp * 1000),
|
|
368
|
+
};
|
|
369
|
+
})
|
|
370
|
+
.filter((info): info is FileInfo => info !== null);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* lsエントリをパース
|
|
375
|
+
*/
|
|
376
|
+
private parseLsEntries(output: string): DirectoryEntry[] {
|
|
377
|
+
return output
|
|
378
|
+
.split('\n')
|
|
379
|
+
.filter(line => line.trim())
|
|
380
|
+
.map(line => {
|
|
381
|
+
const parts = line.split(/\s+/);
|
|
382
|
+
if (parts.length < 7) return null;
|
|
383
|
+
|
|
384
|
+
const permissions = parts[0];
|
|
385
|
+
const name = parts.slice(8).join(' ');
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
name,
|
|
389
|
+
isDirectory: permissions.startsWith('d'),
|
|
390
|
+
};
|
|
391
|
+
})
|
|
392
|
+
.filter((entry): entry is DirectoryEntry => entry !== null);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* エラーメッセージをパース
|
|
397
|
+
*/
|
|
398
|
+
private parseError(stderr: string, filePath: string): WorkspaceError {
|
|
399
|
+
const msg = stderr.toLowerCase();
|
|
400
|
+
|
|
401
|
+
if (msg.includes('no such file') || msg.includes('not found')) {
|
|
402
|
+
return new WorkspaceError('NOT_FOUND', `Not found: ${filePath}`, filePath);
|
|
403
|
+
}
|
|
404
|
+
if (msg.includes('permission denied')) {
|
|
405
|
+
return new WorkspaceError('PERMISSION_DENIED', `Permission denied: ${filePath}`, filePath);
|
|
406
|
+
}
|
|
407
|
+
if (msg.includes('is a directory')) {
|
|
408
|
+
return new WorkspaceError('IS_DIRECTORY', `Is a directory: ${filePath}`, filePath);
|
|
409
|
+
}
|
|
410
|
+
if (msg.includes('not a directory')) {
|
|
411
|
+
return new WorkspaceError('NOT_DIRECTORY', `Not a directory: ${filePath}`, filePath);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return new WorkspaceError('OPERATION_FAILED', stderr || `Operation failed: ${filePath}`, filePath);
|
|
415
|
+
}
|
|
416
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nahisaho/katashiro-workspace
|
|
3
|
+
*
|
|
4
|
+
* Workspace management module for KATASHIRO.
|
|
5
|
+
* Provides unified interface for file operations across local, remote, and Docker environments.
|
|
6
|
+
*
|
|
7
|
+
* @module @nahisaho/katashiro-workspace
|
|
8
|
+
* @requirement REQ-011
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Types
|
|
12
|
+
export type {
|
|
13
|
+
WorkspaceType,
|
|
14
|
+
FileInfo,
|
|
15
|
+
DirectoryEntry,
|
|
16
|
+
WorkspaceConfigBase,
|
|
17
|
+
LocalWorkspaceConfig,
|
|
18
|
+
RemoteWorkspaceConfig,
|
|
19
|
+
DockerWorkspaceConfig,
|
|
20
|
+
WorkspaceConfig,
|
|
21
|
+
Workspace,
|
|
22
|
+
WorkspaceErrorCode,
|
|
23
|
+
} from './types.js';
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
WorkspaceError,
|
|
27
|
+
DEFAULT_REMOTE_TIMEOUT,
|
|
28
|
+
DEFAULT_DOCKER_SOCKET,
|
|
29
|
+
} from './types.js';
|
|
30
|
+
|
|
31
|
+
// Workspace implementations
|
|
32
|
+
export { LocalWorkspace } from './local-workspace.js';
|
|
33
|
+
export { DockerWorkspace } from './docker-workspace.js';
|
|
34
|
+
|
|
35
|
+
// Factory and utilities
|
|
36
|
+
export {
|
|
37
|
+
WorkspaceFactory,
|
|
38
|
+
createWorkspace,
|
|
39
|
+
readFile,
|
|
40
|
+
writeFile,
|
|
41
|
+
} from './workspace-factory.js';
|