@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
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalWorkspace - ローカルファイルシステムワークスペース
|
|
3
|
+
*
|
|
4
|
+
* @requirement REQ-011-02
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'node:fs/promises';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import { glob } from 'fast-glob';
|
|
10
|
+
import type {
|
|
11
|
+
Workspace,
|
|
12
|
+
LocalWorkspaceConfig,
|
|
13
|
+
FileInfo,
|
|
14
|
+
DirectoryEntry,
|
|
15
|
+
} from './types.js';
|
|
16
|
+
import { WorkspaceError } from './types.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* LocalWorkspace - ローカルファイルシステム操作
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const workspace = new LocalWorkspace({
|
|
24
|
+
* type: 'local',
|
|
25
|
+
* workingDir: '/path/to/project',
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* const content = await workspace.read('README.md');
|
|
29
|
+
* await workspace.write('output.txt', 'Hello World');
|
|
30
|
+
* const files = await workspace.search('**\/*.ts');
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export class LocalWorkspace implements Workspace {
|
|
34
|
+
readonly type = 'local' as const;
|
|
35
|
+
readonly workingDir: string;
|
|
36
|
+
readonly readOnly: boolean;
|
|
37
|
+
|
|
38
|
+
constructor(config: LocalWorkspaceConfig) {
|
|
39
|
+
this.workingDir = path.resolve(config.workingDir);
|
|
40
|
+
this.readOnly = config.readOnly ?? false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 絶対パスを解決
|
|
45
|
+
*/
|
|
46
|
+
private resolvePath(filePath: string): string {
|
|
47
|
+
if (path.isAbsolute(filePath)) {
|
|
48
|
+
return filePath;
|
|
49
|
+
}
|
|
50
|
+
return path.join(this.workingDir, filePath);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* ワークスペース内のパスか検証
|
|
55
|
+
*/
|
|
56
|
+
private validatePath(filePath: string): void {
|
|
57
|
+
const resolved = this.resolvePath(filePath);
|
|
58
|
+
const normalized = path.normalize(resolved);
|
|
59
|
+
|
|
60
|
+
// パストラバーサル防止
|
|
61
|
+
if (!normalized.startsWith(this.workingDir)) {
|
|
62
|
+
throw new WorkspaceError(
|
|
63
|
+
'PERMISSION_DENIED',
|
|
64
|
+
`Path traversal detected: ${filePath}`,
|
|
65
|
+
filePath
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 書き込み可能か検証
|
|
72
|
+
*/
|
|
73
|
+
private validateWritable(): void {
|
|
74
|
+
if (this.readOnly) {
|
|
75
|
+
throw new WorkspaceError(
|
|
76
|
+
'READ_ONLY',
|
|
77
|
+
'Workspace is read-only'
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async read(filePath: string, encoding: BufferEncoding = 'utf-8'): Promise<string> {
|
|
83
|
+
const resolved = this.resolvePath(filePath);
|
|
84
|
+
this.validatePath(filePath);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
return await fs.readFile(resolved, { encoding });
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw this.handleFsError(error, filePath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async readBuffer(filePath: string): Promise<Buffer> {
|
|
94
|
+
const resolved = this.resolvePath(filePath);
|
|
95
|
+
this.validatePath(filePath);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
return await fs.readFile(resolved);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
throw this.handleFsError(error, filePath);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async write(filePath: string, content: string): Promise<void> {
|
|
105
|
+
this.validateWritable();
|
|
106
|
+
const resolved = this.resolvePath(filePath);
|
|
107
|
+
this.validatePath(filePath);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// ディレクトリを作成
|
|
111
|
+
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
112
|
+
await fs.writeFile(resolved, content, 'utf-8');
|
|
113
|
+
} catch (error) {
|
|
114
|
+
throw this.handleFsError(error, filePath);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async writeBuffer(filePath: string, buffer: Buffer): Promise<void> {
|
|
119
|
+
this.validateWritable();
|
|
120
|
+
const resolved = this.resolvePath(filePath);
|
|
121
|
+
this.validatePath(filePath);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
125
|
+
await fs.writeFile(resolved, buffer);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
throw this.handleFsError(error, filePath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async list(dirPath: string): Promise<FileInfo[]> {
|
|
132
|
+
const resolved = this.resolvePath(dirPath);
|
|
133
|
+
this.validatePath(dirPath);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const entries = await fs.readdir(resolved, { withFileTypes: true });
|
|
137
|
+
const fileInfos: FileInfo[] = [];
|
|
138
|
+
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
const entryPath = path.join(resolved, entry.name);
|
|
141
|
+
const stats = await fs.stat(entryPath);
|
|
142
|
+
|
|
143
|
+
fileInfos.push({
|
|
144
|
+
name: entry.name,
|
|
145
|
+
path: entryPath,
|
|
146
|
+
size: stats.size,
|
|
147
|
+
isDirectory: entry.isDirectory(),
|
|
148
|
+
modifiedAt: stats.mtime,
|
|
149
|
+
createdAt: stats.birthtime,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return fileInfos;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw this.handleFsError(error, dirPath);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async listEntries(dirPath: string): Promise<DirectoryEntry[]> {
|
|
160
|
+
const resolved = this.resolvePath(dirPath);
|
|
161
|
+
this.validatePath(dirPath);
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const entries = await fs.readdir(resolved, { withFileTypes: true });
|
|
165
|
+
return entries.map(entry => ({
|
|
166
|
+
name: entry.name,
|
|
167
|
+
isDirectory: entry.isDirectory(),
|
|
168
|
+
}));
|
|
169
|
+
} catch (error) {
|
|
170
|
+
throw this.handleFsError(error, dirPath);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async search(pattern: string): Promise<string[]> {
|
|
175
|
+
try {
|
|
176
|
+
const files = await glob(pattern, {
|
|
177
|
+
cwd: this.workingDir,
|
|
178
|
+
absolute: false,
|
|
179
|
+
onlyFiles: true,
|
|
180
|
+
ignore: ['**/node_modules/**', '**/.git/**'],
|
|
181
|
+
});
|
|
182
|
+
return files;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
throw new WorkspaceError(
|
|
185
|
+
'OPERATION_FAILED',
|
|
186
|
+
`Search failed: ${error instanceof Error ? error.message : String(error)}`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async exists(filePath: string): Promise<boolean> {
|
|
192
|
+
const resolved = this.resolvePath(filePath);
|
|
193
|
+
this.validatePath(filePath);
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
await fs.access(resolved);
|
|
197
|
+
return true;
|
|
198
|
+
} catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async delete(filePath: string): Promise<void> {
|
|
204
|
+
this.validateWritable();
|
|
205
|
+
const resolved = this.resolvePath(filePath);
|
|
206
|
+
this.validatePath(filePath);
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const stats = await fs.stat(resolved);
|
|
210
|
+
if (stats.isDirectory()) {
|
|
211
|
+
await fs.rm(resolved, { recursive: true });
|
|
212
|
+
} else {
|
|
213
|
+
await fs.unlink(resolved);
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
throw this.handleFsError(error, filePath);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async mkdir(dirPath: string, recursive = true): Promise<void> {
|
|
221
|
+
this.validateWritable();
|
|
222
|
+
const resolved = this.resolvePath(dirPath);
|
|
223
|
+
this.validatePath(dirPath);
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
await fs.mkdir(resolved, { recursive });
|
|
227
|
+
} catch (error) {
|
|
228
|
+
throw this.handleFsError(error, dirPath);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async stat(filePath: string): Promise<FileInfo> {
|
|
233
|
+
const resolved = this.resolvePath(filePath);
|
|
234
|
+
this.validatePath(filePath);
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const stats = await fs.stat(resolved);
|
|
238
|
+
return {
|
|
239
|
+
name: path.basename(resolved),
|
|
240
|
+
path: resolved,
|
|
241
|
+
size: stats.size,
|
|
242
|
+
isDirectory: stats.isDirectory(),
|
|
243
|
+
modifiedAt: stats.mtime,
|
|
244
|
+
createdAt: stats.birthtime,
|
|
245
|
+
};
|
|
246
|
+
} catch (error) {
|
|
247
|
+
throw this.handleFsError(error, filePath);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async copy(src: string, dest: string): Promise<void> {
|
|
252
|
+
this.validateWritable();
|
|
253
|
+
const srcResolved = this.resolvePath(src);
|
|
254
|
+
const destResolved = this.resolvePath(dest);
|
|
255
|
+
this.validatePath(src);
|
|
256
|
+
this.validatePath(dest);
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
await fs.mkdir(path.dirname(destResolved), { recursive: true });
|
|
260
|
+
await fs.cp(srcResolved, destResolved, { recursive: true });
|
|
261
|
+
} catch (error) {
|
|
262
|
+
throw this.handleFsError(error, src);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async move(src: string, dest: string): Promise<void> {
|
|
267
|
+
this.validateWritable();
|
|
268
|
+
const srcResolved = this.resolvePath(src);
|
|
269
|
+
const destResolved = this.resolvePath(dest);
|
|
270
|
+
this.validatePath(src);
|
|
271
|
+
this.validatePath(dest);
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
await fs.mkdir(path.dirname(destResolved), { recursive: true });
|
|
275
|
+
await fs.rename(srcResolved, destResolved);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
throw this.handleFsError(error, src);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* FSエラーをWorkspaceErrorに変換
|
|
283
|
+
*/
|
|
284
|
+
private handleFsError(error: unknown, filePath: string): WorkspaceError {
|
|
285
|
+
if (error instanceof WorkspaceError) {
|
|
286
|
+
return error;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const fsError = error as NodeJS.ErrnoException;
|
|
290
|
+
switch (fsError.code) {
|
|
291
|
+
case 'ENOENT':
|
|
292
|
+
return new WorkspaceError('NOT_FOUND', `Not found: ${filePath}`, filePath, fsError);
|
|
293
|
+
case 'EACCES':
|
|
294
|
+
case 'EPERM':
|
|
295
|
+
return new WorkspaceError('PERMISSION_DENIED', `Permission denied: ${filePath}`, filePath, fsError);
|
|
296
|
+
case 'EEXIST':
|
|
297
|
+
return new WorkspaceError('ALREADY_EXISTS', `Already exists: ${filePath}`, filePath, fsError);
|
|
298
|
+
case 'EISDIR':
|
|
299
|
+
return new WorkspaceError('IS_DIRECTORY', `Is a directory: ${filePath}`, filePath, fsError);
|
|
300
|
+
case 'ENOTDIR':
|
|
301
|
+
return new WorkspaceError('NOT_DIRECTORY', `Not a directory: ${filePath}`, filePath, fsError);
|
|
302
|
+
default:
|
|
303
|
+
return new WorkspaceError(
|
|
304
|
+
'OPERATION_FAILED',
|
|
305
|
+
`Operation failed: ${fsError.message}`,
|
|
306
|
+
filePath,
|
|
307
|
+
fsError
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace 型定義
|
|
3
|
+
*
|
|
4
|
+
* @requirement REQ-011
|
|
5
|
+
* @design REQ-011-01〜REQ-011-06
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// ワークスペースタイプ
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ワークスペースの種類
|
|
14
|
+
*/
|
|
15
|
+
export type WorkspaceType = 'local' | 'remote' | 'docker';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* ファイル情報
|
|
19
|
+
*/
|
|
20
|
+
export interface FileInfo {
|
|
21
|
+
/** ファイル名 */
|
|
22
|
+
name: string;
|
|
23
|
+
/** フルパス */
|
|
24
|
+
path: string;
|
|
25
|
+
/** ファイルサイズ(バイト) */
|
|
26
|
+
size: number;
|
|
27
|
+
/** ディレクトリかどうか */
|
|
28
|
+
isDirectory: boolean;
|
|
29
|
+
/** 最終更新日時 */
|
|
30
|
+
modifiedAt: Date;
|
|
31
|
+
/** 作成日時 */
|
|
32
|
+
createdAt: Date;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* ディレクトリエントリ
|
|
37
|
+
*/
|
|
38
|
+
export interface DirectoryEntry {
|
|
39
|
+
/** ファイル名 */
|
|
40
|
+
name: string;
|
|
41
|
+
/** ディレクトリかどうか */
|
|
42
|
+
isDirectory: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* ワークスペース設定ベース
|
|
47
|
+
*/
|
|
48
|
+
export interface WorkspaceConfigBase {
|
|
49
|
+
/** 作業ディレクトリ */
|
|
50
|
+
workingDir: string;
|
|
51
|
+
/** 読み取り専用モード */
|
|
52
|
+
readOnly?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* ローカルワークスペース設定
|
|
57
|
+
*/
|
|
58
|
+
export interface LocalWorkspaceConfig extends WorkspaceConfigBase {
|
|
59
|
+
type: 'local';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* リモートワークスペース設定
|
|
64
|
+
*/
|
|
65
|
+
export interface RemoteWorkspaceConfig extends WorkspaceConfigBase {
|
|
66
|
+
type: 'remote';
|
|
67
|
+
/** APIベースURL */
|
|
68
|
+
apiUrl: string;
|
|
69
|
+
/** 認証トークン */
|
|
70
|
+
authToken?: string;
|
|
71
|
+
/** タイムアウト(ミリ秒) */
|
|
72
|
+
timeout?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Dockerワークスペース設定
|
|
77
|
+
*/
|
|
78
|
+
export interface DockerWorkspaceConfig extends WorkspaceConfigBase {
|
|
79
|
+
type: 'docker';
|
|
80
|
+
/** コンテナID */
|
|
81
|
+
containerId: string;
|
|
82
|
+
/** Docker ソケット */
|
|
83
|
+
socketPath?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* ワークスペース設定
|
|
88
|
+
*/
|
|
89
|
+
export type WorkspaceConfig =
|
|
90
|
+
| LocalWorkspaceConfig
|
|
91
|
+
| RemoteWorkspaceConfig
|
|
92
|
+
| DockerWorkspaceConfig;
|
|
93
|
+
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// ワークスペースインターフェース
|
|
96
|
+
// ============================================================================
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* ワークスペースインターフェース(REQ-011-01)
|
|
100
|
+
*/
|
|
101
|
+
export interface Workspace {
|
|
102
|
+
/** ワークスペースタイプ */
|
|
103
|
+
readonly type: WorkspaceType;
|
|
104
|
+
/** 作業ディレクトリ */
|
|
105
|
+
readonly workingDir: string;
|
|
106
|
+
/** 読み取り専用かどうか */
|
|
107
|
+
readonly readOnly: boolean;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* ファイルを読み取る
|
|
111
|
+
* @param path ファイルパス
|
|
112
|
+
* @param encoding エンコーディング(デフォルト: utf-8)
|
|
113
|
+
*/
|
|
114
|
+
read(path: string, encoding?: BufferEncoding): Promise<string>;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* ファイルをバイナリとして読み取る
|
|
118
|
+
* @param path ファイルパス
|
|
119
|
+
*/
|
|
120
|
+
readBuffer(path: string): Promise<Buffer>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* ファイルに書き込む
|
|
124
|
+
* @param path ファイルパス
|
|
125
|
+
* @param content コンテンツ
|
|
126
|
+
*/
|
|
127
|
+
write(path: string, content: string): Promise<void>;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* バイナリを書き込む
|
|
131
|
+
* @param path ファイルパス
|
|
132
|
+
* @param buffer バッファ
|
|
133
|
+
*/
|
|
134
|
+
writeBuffer(path: string, buffer: Buffer): Promise<void>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* ディレクトリの内容をリスト
|
|
138
|
+
* @param path ディレクトリパス
|
|
139
|
+
*/
|
|
140
|
+
list(path: string): Promise<FileInfo[]>;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 簡易ディレクトリリスト(名前のみ)
|
|
144
|
+
* @param path ディレクトリパス
|
|
145
|
+
*/
|
|
146
|
+
listEntries(path: string): Promise<DirectoryEntry[]>;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* globパターンでファイルを検索(REQ-011-06)
|
|
150
|
+
* @param pattern globパターン
|
|
151
|
+
*/
|
|
152
|
+
search(pattern: string): Promise<string[]>;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* ファイル/ディレクトリの存在確認
|
|
156
|
+
* @param path パス
|
|
157
|
+
*/
|
|
158
|
+
exists(path: string): Promise<boolean>;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* ファイル/ディレクトリを削除
|
|
162
|
+
* @param path パス
|
|
163
|
+
*/
|
|
164
|
+
delete(path: string): Promise<void>;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* ディレクトリを作成
|
|
168
|
+
* @param path ディレクトリパス
|
|
169
|
+
* @param recursive 再帰的に作成するか
|
|
170
|
+
*/
|
|
171
|
+
mkdir(path: string, recursive?: boolean): Promise<void>;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* ファイル情報を取得
|
|
175
|
+
* @param path パス
|
|
176
|
+
*/
|
|
177
|
+
stat(path: string): Promise<FileInfo>;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* ファイル/ディレクトリをコピー
|
|
181
|
+
* @param src ソースパス
|
|
182
|
+
* @param dest 宛先パス
|
|
183
|
+
*/
|
|
184
|
+
copy(src: string, dest: string): Promise<void>;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* ファイル/ディレクトリを移動
|
|
188
|
+
* @param src ソースパス
|
|
189
|
+
* @param dest 宛先パス
|
|
190
|
+
*/
|
|
191
|
+
move(src: string, dest: string): Promise<void>;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* ワークスペースを初期化
|
|
195
|
+
*/
|
|
196
|
+
initialize?(): Promise<void>;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* ワークスペースをクリーンアップ
|
|
200
|
+
*/
|
|
201
|
+
cleanup?(): Promise<void>;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ============================================================================
|
|
205
|
+
// エラー
|
|
206
|
+
// ============================================================================
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* ワークスペースエラーコード
|
|
210
|
+
*/
|
|
211
|
+
export type WorkspaceErrorCode =
|
|
212
|
+
| 'NOT_FOUND'
|
|
213
|
+
| 'PERMISSION_DENIED'
|
|
214
|
+
| 'ALREADY_EXISTS'
|
|
215
|
+
| 'IS_DIRECTORY'
|
|
216
|
+
| 'NOT_DIRECTORY'
|
|
217
|
+
| 'READ_ONLY'
|
|
218
|
+
| 'OPERATION_FAILED'
|
|
219
|
+
| 'CONNECTION_FAILED'
|
|
220
|
+
| 'TIMEOUT';
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* ワークスペースエラー
|
|
224
|
+
*/
|
|
225
|
+
export class WorkspaceError extends Error {
|
|
226
|
+
constructor(
|
|
227
|
+
public readonly code: WorkspaceErrorCode,
|
|
228
|
+
message: string,
|
|
229
|
+
public readonly path?: string,
|
|
230
|
+
public readonly cause?: Error
|
|
231
|
+
) {
|
|
232
|
+
super(message);
|
|
233
|
+
this.name = 'WorkspaceError';
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ============================================================================
|
|
238
|
+
// デフォルト設定
|
|
239
|
+
// ============================================================================
|
|
240
|
+
|
|
241
|
+
export const DEFAULT_REMOTE_TIMEOUT = 30000;
|
|
242
|
+
export const DEFAULT_DOCKER_SOCKET = '/var/run/docker.sock';
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkspaceFactory - ワークスペースファクトリー
|
|
3
|
+
*
|
|
4
|
+
* @requirement REQ-011-05
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LocalWorkspace } from './local-workspace.js';
|
|
8
|
+
import { DockerWorkspace } from './docker-workspace.js';
|
|
9
|
+
import type { Workspace, WorkspaceConfig } from './types.js';
|
|
10
|
+
import { WorkspaceError } from './types.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* WorkspaceFactory - ワークスペースの生成ユーティリティ
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* // ローカルワークスペース
|
|
18
|
+
* const local = WorkspaceFactory.create({
|
|
19
|
+
* type: 'local',
|
|
20
|
+
* workingDir: '/path/to/project',
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* // Dockerワークスペース
|
|
24
|
+
* const docker = WorkspaceFactory.create({
|
|
25
|
+
* type: 'docker',
|
|
26
|
+
* containerId: 'container123',
|
|
27
|
+
* workingDir: '/app',
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* // 型に依存しない統一操作
|
|
31
|
+
* async function readConfig(workspace: Workspace): Promise<string> {
|
|
32
|
+
* return workspace.read('config.json');
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export class WorkspaceFactory {
|
|
37
|
+
/**
|
|
38
|
+
* ワークスペースを生成(REQ-011-05)
|
|
39
|
+
*/
|
|
40
|
+
static create(config: WorkspaceConfig): Workspace {
|
|
41
|
+
switch (config.type) {
|
|
42
|
+
case 'local':
|
|
43
|
+
return new LocalWorkspace(config);
|
|
44
|
+
|
|
45
|
+
case 'docker':
|
|
46
|
+
return new DockerWorkspace(config);
|
|
47
|
+
|
|
48
|
+
case 'remote':
|
|
49
|
+
throw new WorkspaceError(
|
|
50
|
+
'OPERATION_FAILED',
|
|
51
|
+
'RemoteWorkspace is not yet implemented'
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
default:
|
|
55
|
+
throw new WorkspaceError(
|
|
56
|
+
'OPERATION_FAILED',
|
|
57
|
+
`Unknown workspace type: ${(config as WorkspaceConfig).type}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* ローカルワークスペースを生成
|
|
64
|
+
*/
|
|
65
|
+
static createLocal(workingDir: string, options?: { readOnly?: boolean }): LocalWorkspace {
|
|
66
|
+
return new LocalWorkspace({
|
|
67
|
+
type: 'local',
|
|
68
|
+
workingDir,
|
|
69
|
+
readOnly: options?.readOnly,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Dockerワークスペースを生成
|
|
75
|
+
*/
|
|
76
|
+
static createDocker(
|
|
77
|
+
containerId: string,
|
|
78
|
+
workingDir: string,
|
|
79
|
+
options?: { readOnly?: boolean; socketPath?: string }
|
|
80
|
+
): DockerWorkspace {
|
|
81
|
+
return new DockerWorkspace({
|
|
82
|
+
type: 'docker',
|
|
83
|
+
containerId,
|
|
84
|
+
workingDir,
|
|
85
|
+
readOnly: options?.readOnly,
|
|
86
|
+
socketPath: options?.socketPath,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// ユーティリティ関数
|
|
93
|
+
// ============================================================================
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* ワークスペースを作成して初期化
|
|
97
|
+
*/
|
|
98
|
+
export async function createWorkspace(config: WorkspaceConfig): Promise<Workspace> {
|
|
99
|
+
const workspace = WorkspaceFactory.create(config);
|
|
100
|
+
if (workspace.initialize) {
|
|
101
|
+
await workspace.initialize();
|
|
102
|
+
}
|
|
103
|
+
return workspace;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* ワークスペースでファイルを読み取る簡易関数
|
|
108
|
+
*/
|
|
109
|
+
export async function readFile(
|
|
110
|
+
config: WorkspaceConfig,
|
|
111
|
+
filePath: string
|
|
112
|
+
): Promise<string> {
|
|
113
|
+
const workspace = WorkspaceFactory.create(config);
|
|
114
|
+
if (workspace.initialize) {
|
|
115
|
+
await workspace.initialize();
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
return await workspace.read(filePath);
|
|
119
|
+
} finally {
|
|
120
|
+
if (workspace.cleanup) {
|
|
121
|
+
await workspace.cleanup();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* ワークスペースでファイルを書き込む簡易関数
|
|
128
|
+
*/
|
|
129
|
+
export async function writeFile(
|
|
130
|
+
config: WorkspaceConfig,
|
|
131
|
+
filePath: string,
|
|
132
|
+
content: string
|
|
133
|
+
): Promise<void> {
|
|
134
|
+
const workspace = WorkspaceFactory.create(config);
|
|
135
|
+
if (workspace.initialize) {
|
|
136
|
+
await workspace.initialize();
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
await workspace.write(filePath, content);
|
|
140
|
+
} finally {
|
|
141
|
+
if (workspace.cleanup) {
|
|
142
|
+
await workspace.cleanup();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|