@fgv/ts-res-browser 1.0.0 → 5.0.0-2

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.
@@ -0,0 +1,302 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import {
3
+ ArchiveBoxIcon,
4
+ FolderOpenIcon,
5
+ ExclamationTriangleIcon,
6
+ CheckCircleIcon,
7
+ ArrowRightIcon
8
+ } from '@heroicons/react/24/outline';
9
+ import { Message } from '../../types/app';
10
+ import { UseResourceManagerReturn } from '../../hooks/useResourceManager';
11
+ import { BrowserZipFileTreeAccessors } from '../../utils/zip/browserZipFileTreeAccessors';
12
+ import { FileTree } from '@fgv/ts-utils';
13
+
14
+ interface ZipLoaderProps {
15
+ onMessage: (type: Message['type'], message: string) => void;
16
+ resourceManager: UseResourceManagerReturn;
17
+ zipFile?: string;
18
+ zipPath?: string;
19
+ onLoadComplete?: () => void;
20
+ }
21
+
22
+ interface ZipManifest {
23
+ timestamp: string;
24
+ input?: {
25
+ type: 'file' | 'directory';
26
+ originalPath: string;
27
+ archivePath: string;
28
+ };
29
+ config?: {
30
+ type: 'file';
31
+ originalPath: string;
32
+ archivePath: string;
33
+ };
34
+ }
35
+
36
+ const ZipLoader: React.FC<ZipLoaderProps> = ({
37
+ onMessage,
38
+ resourceManager,
39
+ zipFile,
40
+ zipPath,
41
+ onLoadComplete
42
+ }) => {
43
+ const [isLoading, setIsLoading] = useState(false);
44
+ const [loadingStep, setLoadingStep] = useState<string>('');
45
+ const [zipFound, setZipFound] = useState<boolean | null>(null);
46
+ const [manifest, setManifest] = useState<ZipManifest | null>(null);
47
+ const [autoloadAttempted, setAutoloadAttempted] = useState(false);
48
+
49
+ // Auto-load ZIP if we have a specific file reference
50
+ useEffect(() => {
51
+ if ((zipFile || zipPath) && !autoloadAttempted) {
52
+ setAutoloadAttempted(true);
53
+ handleLoadZip();
54
+ }
55
+ }, [zipFile, zipPath, autoloadAttempted]);
56
+
57
+ const handleLoadZip = async () => {
58
+ if (!zipFile && !zipPath) {
59
+ onMessage('error', 'No ZIP file specified');
60
+ return;
61
+ }
62
+
63
+ setIsLoading(true);
64
+ setLoadingStep('Opening file picker...');
65
+
66
+ try {
67
+ // Try to load the ZIP file using File System Access API
68
+ let fileHandle: FileSystemFileHandle | null = null;
69
+
70
+ if ('showOpenFilePicker' in window) {
71
+ try {
72
+ const [handle] = await (window as any).showOpenFilePicker({
73
+ types: [
74
+ {
75
+ description: 'ZIP Archives',
76
+ accept: { 'application/zip': ['.zip'] }
77
+ }
78
+ ],
79
+ startIn: 'downloads',
80
+ multiple: false
81
+ });
82
+ fileHandle = handle;
83
+ setZipFound(true);
84
+ } catch (error) {
85
+ if ((error as DOMException).name !== 'AbortError') {
86
+ onMessage('warning', 'Could not open file picker. Please select the ZIP file manually.');
87
+ }
88
+ setZipFound(false);
89
+ return;
90
+ }
91
+ } else {
92
+ onMessage('error', 'File System Access API not supported. Please use a modern browser.');
93
+ setZipFound(false);
94
+ return;
95
+ }
96
+
97
+ if (!fileHandle) {
98
+ setZipFound(false);
99
+ return;
100
+ }
101
+
102
+ setLoadingStep('Reading ZIP archive...');
103
+ const file = await fileHandle.getFile();
104
+
105
+ // Create ZIP FileTree
106
+ const zipAccessorsResult = await BrowserZipFileTreeAccessors.fromFile(file);
107
+ if (zipAccessorsResult.isFailure()) {
108
+ onMessage('error', `Failed to read ZIP: ${zipAccessorsResult.message}`);
109
+ return;
110
+ }
111
+
112
+ const zipAccessors = zipAccessorsResult.value;
113
+ const fileTree = FileTree.FileTree.create(zipAccessors);
114
+ if (fileTree.isFailure()) {
115
+ onMessage('error', `Failed to create file tree: ${fileTree.message}`);
116
+ return;
117
+ }
118
+
119
+ setLoadingStep('Reading manifest...');
120
+
121
+ // Try to read manifest
122
+ const manifestResult = fileTree.value.getFile('/manifest.json');
123
+ if (manifestResult.isSuccess()) {
124
+ const manifestContents = manifestResult.value.getContents();
125
+ if (manifestContents.isSuccess()) {
126
+ const parsedManifest = manifestContents.value as ZipManifest;
127
+ setManifest(parsedManifest);
128
+ onMessage('info', `ZIP created: ${new Date(parsedManifest.timestamp).toLocaleString()}`);
129
+ }
130
+ }
131
+
132
+ // Process resources from the ZIP
133
+ setLoadingStep('Processing resources...');
134
+
135
+ // Look for input directory/files
136
+ let inputProcessed = false;
137
+ if (manifest?.input) {
138
+ const inputPath = `/${manifest.input.archivePath}`;
139
+ const inputResult = fileTree.value.getItem(inputPath);
140
+
141
+ if (inputResult.isSuccess()) {
142
+ const inputItem = inputResult.value;
143
+
144
+ if (inputItem.type === 'directory') {
145
+ // Process directory
146
+ await resourceManager.actions.processFileTree(fileTree.value, inputPath);
147
+ inputProcessed = true;
148
+ } else if (inputItem.type === 'file') {
149
+ // Process single file
150
+ await resourceManager.actions.processFileTree(fileTree.value, inputPath);
151
+ inputProcessed = true;
152
+ }
153
+ }
154
+ }
155
+
156
+ // Fallback: look for common input patterns
157
+ if (!inputProcessed) {
158
+ const commonInputPaths = ['/input', '/resources', '/data'];
159
+ for (const inputPath of commonInputPaths) {
160
+ const inputResult = fileTree.value.getItem(inputPath);
161
+ if (inputResult.isSuccess() && inputResult.value.type === 'directory') {
162
+ await resourceManager.actions.processFileTree(fileTree.value, inputPath);
163
+ inputProcessed = true;
164
+ break;
165
+ }
166
+ }
167
+ }
168
+
169
+ // Load configuration if present
170
+ if (manifest?.config) {
171
+ const configPath = `/${manifest.config.archivePath}`;
172
+ const configResult = fileTree.value.getFile(configPath);
173
+
174
+ if (configResult.isSuccess()) {
175
+ const configContents = configResult.value.getContents();
176
+ if (configContents.isSuccess()) {
177
+ resourceManager.actions.applyConfiguration(configContents.value as any);
178
+ onMessage('success', 'Configuration loaded from ZIP');
179
+ }
180
+ }
181
+ }
182
+
183
+ if (inputProcessed) {
184
+ onMessage('success', 'Resources loaded successfully from ZIP!');
185
+ onLoadComplete?.();
186
+ } else {
187
+ onMessage('warning', 'ZIP loaded but no resources found. Check ZIP contents.');
188
+ }
189
+ } catch (error) {
190
+ onMessage('error', `Failed to load ZIP: ${error instanceof Error ? error.message : String(error)}`);
191
+ setZipFound(false);
192
+ } finally {
193
+ setIsLoading(false);
194
+ setLoadingStep('');
195
+ }
196
+ };
197
+
198
+ const expectedFileName = zipFile || 'ts-res-bundle-*.zip';
199
+
200
+ return (
201
+ <div className="p-6">
202
+ <div className="flex items-center space-x-3 mb-6">
203
+ <ArchiveBoxIcon className="h-8 w-8 text-purple-600" />
204
+ <h2 className="text-2xl font-bold text-gray-900">Load ZIP Archive</h2>
205
+ </div>
206
+
207
+ <div className="max-w-2xl">
208
+ {/* ZIP File Info */}
209
+ <div className="bg-blue-50 rounded-lg p-4 mb-6">
210
+ <div className="flex items-start space-x-3">
211
+ <ArchiveBoxIcon className="h-5 w-5 text-blue-600 mt-0.5" />
212
+ <div className="text-sm text-blue-800">
213
+ <p className="font-medium">ZIP Archive Ready</p>
214
+ {zipPath && (
215
+ <p className="mt-1">
216
+ <span className="font-medium">Location:</span> {zipPath}
217
+ </p>
218
+ )}
219
+ {zipFile && (
220
+ <p className="mt-1">
221
+ <span className="font-medium">File:</span> {zipFile}
222
+ </p>
223
+ )}
224
+ <p className="mt-2">
225
+ The CLI has created a ZIP archive containing your resources and configuration. Click below to
226
+ load it directly into the browser.
227
+ </p>
228
+ </div>
229
+ </div>
230
+ </div>
231
+
232
+ {/* Load Button */}
233
+ <div className="space-y-4">
234
+ <button
235
+ onClick={handleLoadZip}
236
+ disabled={isLoading}
237
+ className="w-full flex items-center justify-center space-x-2 px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
238
+ >
239
+ {isLoading ? (
240
+ <>
241
+ <div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
242
+ <span>{loadingStep || 'Loading...'}</span>
243
+ </>
244
+ ) : (
245
+ <>
246
+ <FolderOpenIcon className="w-5 h-5" />
247
+ <span>Load ZIP Archive</span>
248
+ <ArrowRightIcon className="w-5 h-5" />
249
+ </>
250
+ )}
251
+ </button>
252
+
253
+ {/* Instructions */}
254
+ <div className="bg-gray-50 rounded-lg p-4">
255
+ <h3 className="text-sm font-medium text-gray-900 mb-2">Instructions:</h3>
256
+ <ol className="text-sm text-gray-600 space-y-1 list-decimal list-inside">
257
+ <li>Click "Load ZIP Archive" above</li>
258
+ <li>
259
+ Select the ZIP file: <code className="bg-gray-200 px-1 rounded">{expectedFileName}</code>
260
+ </li>
261
+ <li>The browser will automatically process your resources and configuration</li>
262
+ </ol>
263
+ </div>
264
+
265
+ {/* Manifest Info */}
266
+ {manifest && (
267
+ <div className="bg-green-50 rounded-lg p-4">
268
+ <div className="flex items-start space-x-3">
269
+ <CheckCircleIcon className="h-5 w-5 text-green-600 mt-0.5" />
270
+ <div className="text-sm text-green-800">
271
+ <p className="font-medium">ZIP Archive Contents</p>
272
+ <div className="mt-2 space-y-1">
273
+ {manifest.input && <p>📁 Resources: {manifest.input.originalPath}</p>}
274
+ {manifest.config && <p>⚙️ Configuration: {manifest.config.originalPath}</p>}
275
+ </div>
276
+ </div>
277
+ </div>
278
+ </div>
279
+ )}
280
+
281
+ {/* Error State */}
282
+ {zipFound === false && (
283
+ <div className="bg-yellow-50 rounded-lg p-4">
284
+ <div className="flex items-start space-x-3">
285
+ <ExclamationTriangleIcon className="h-5 w-5 text-yellow-600 mt-0.5" />
286
+ <div className="text-sm text-yellow-800">
287
+ <p className="font-medium">ZIP File Not Found</p>
288
+ <p className="mt-1">
289
+ Could not locate or open the ZIP file. Please make sure it exists in your Downloads folder
290
+ and try again.
291
+ </p>
292
+ </div>
293
+ </div>
294
+ </div>
295
+ )}
296
+ </div>
297
+ </div>
298
+ </div>
299
+ );
300
+ };
301
+
302
+ export default ZipLoader;
@@ -18,6 +18,9 @@ export const useAppState = (): { state: AppState; actions: AppActions } => {
18
18
  setState((prev) => ({ ...prev, selectedTool: tool }));
19
19
  }, []);
20
20
 
21
+ // Alias for setSelectedTool to match the interface
22
+ const setActiveTool = setSelectedTool;
23
+
21
24
  const addMessage = useCallback((type: Message['type'], message: string) => {
22
25
  const newMessage: Message = {
23
26
  id: Date.now().toString(),
@@ -94,6 +97,7 @@ export const useAppState = (): { state: AppState; actions: AppActions } => {
94
97
 
95
98
  const actions: AppActions = {
96
99
  setSelectedTool,
100
+ setActiveTool,
97
101
  addMessage,
98
102
  clearMessages,
99
103
  updateFilterEnabled,
@@ -1,5 +1,5 @@
1
1
  import { useState, useCallback } from 'react';
2
- import { Result, succeed, fail } from '@fgv/ts-utils';
2
+ import { Result, succeed, fail, FileTree } from '@fgv/ts-utils';
3
3
  import {
4
4
  ProcessedResources,
5
5
  processImportedDirectory,
@@ -7,6 +7,7 @@ import {
7
7
  createSimpleContext
8
8
  } from '../utils/tsResIntegration';
9
9
  import { ImportedDirectory, ImportedFile } from '../utils/fileImport';
10
+ import { FileTreeConverter } from '../utils/fileTreeConverter';
10
11
  import { Config } from '@fgv/ts-res';
11
12
 
12
13
  export interface ResourceManagerState {
@@ -22,6 +23,7 @@ export interface UseResourceManagerReturn {
22
23
  actions: {
23
24
  processDirectory: (directory: ImportedDirectory) => Promise<void>;
24
25
  processFiles: (files: ImportedFile[]) => Promise<void>;
26
+ processFileTree: (fileTree: FileTree.FileTree, rootPath?: string) => Promise<void>;
25
27
  clearError: () => void;
26
28
  reset: () => void;
27
29
  resolveResource: (resourceId: string, context?: Record<string, string>) => Promise<Result<any>>;
@@ -216,6 +218,54 @@ export const useResourceManager = (): UseResourceManagerReturn => {
216
218
  [state.processedResources]
217
219
  );
218
220
 
221
+ const processFileTree = useCallback(
222
+ async (fileTree: FileTree.FileTree, rootPath: string = '/') => {
223
+ console.log('=== STARTING FILETREE PROCESSING ===');
224
+ console.log('Root path:', rootPath);
225
+
226
+ setState((prev) => ({ ...prev, isProcessing: true, error: null }));
227
+
228
+ try {
229
+ console.log('1. Converting FileTree to ImportedDirectory/Files...');
230
+
231
+ const convertResult = FileTreeConverter.convertPath(fileTree, rootPath);
232
+ if (convertResult.isFailure()) {
233
+ setState((prev) => ({
234
+ ...prev,
235
+ isProcessing: false,
236
+ error: `Failed to convert FileTree: ${convertResult.message}`
237
+ }));
238
+ return;
239
+ }
240
+
241
+ const converted = convertResult.value;
242
+ console.log('2. FileTree converted successfully');
243
+
244
+ // Process based on whether we got a directory or files
245
+ if (Array.isArray(converted)) {
246
+ // Got files
247
+ console.log('3. Processing as files, count:', converted.length);
248
+ await processFiles(converted);
249
+ } else {
250
+ // Got directory
251
+ console.log('3. Processing as directory:', converted.name);
252
+ await processDirectory(converted);
253
+ }
254
+
255
+ console.log('4. processFileTree completed successfully');
256
+ } catch (error) {
257
+ console.error('=== FILETREE PROCESSING ERROR ===');
258
+ console.error('Error:', error);
259
+ setState((prev) => ({
260
+ ...prev,
261
+ isProcessing: false,
262
+ error: `Unexpected error: ${error instanceof Error ? error.message : String(error)}`
263
+ }));
264
+ }
265
+ },
266
+ [processFiles, processDirectory]
267
+ );
268
+
219
269
  const applyConfiguration = useCallback((config: Config.Model.ISystemConfiguration) => {
220
270
  console.log('Applying configuration:', config.name || 'Unnamed configuration');
221
271
  setState((prev) => ({
@@ -233,6 +283,7 @@ export const useResourceManager = (): UseResourceManagerReturn => {
233
283
  actions: {
234
284
  processDirectory,
235
285
  processFiles,
286
+ processFileTree,
236
287
  clearError,
237
288
  reset,
238
289
  resolveResource,
@@ -0,0 +1,56 @@
1
+ /*
2
+ * Copyright (c) 2025 Erik Fortune
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ * of this software and associated documentation files (the "Software"), to deal
6
+ * in the Software without restriction, including without limitation the rights
7
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ * copies of the Software, and to permit persons to whom the Software is
9
+ * furnished to do so, subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ * SOFTWARE.
21
+ */
22
+
23
+ import { useEffect, useState } from 'react';
24
+ import { parseUrlParameters, IUrlConfigOptions } from '../utils/urlParams';
25
+
26
+ /**
27
+ * Hook to parse and provide URL parameters for initial app configuration
28
+ */
29
+ export function useUrlParams() {
30
+ const [urlParams, setUrlParams] = useState<IUrlConfigOptions>({});
31
+ const [hasUrlParams, setHasUrlParams] = useState(false);
32
+
33
+ useEffect(() => {
34
+ const params = parseUrlParameters();
35
+ setUrlParams(params);
36
+
37
+ // Check if any meaningful parameters were provided
38
+ const hasParams = !!(
39
+ params.input ||
40
+ params.config ||
41
+ params.contextFilter ||
42
+ params.qualifierDefaults ||
43
+ params.resourceTypes ||
44
+ params.maxDistance !== undefined ||
45
+ params.reduceQualifiers ||
46
+ params.interactive
47
+ );
48
+
49
+ setHasUrlParams(hasParams);
50
+ }, []);
51
+
52
+ return {
53
+ urlParams,
54
+ hasUrlParams
55
+ };
56
+ }
package/src/types/app.ts CHANGED
@@ -6,7 +6,14 @@ export interface Message {
6
6
  timestamp: Date;
7
7
  }
8
8
 
9
- export type Tool = 'import' | 'source' | 'filter' | 'compiled' | 'resolution' | 'configuration';
9
+ export type Tool =
10
+ | 'import'
11
+ | 'source'
12
+ | 'filter'
13
+ | 'compiled'
14
+ | 'resolution'
15
+ | 'configuration'
16
+ | 'zip-loader';
10
17
 
11
18
  export interface FilterState {
12
19
  enabled: boolean;
@@ -24,6 +31,7 @@ export interface AppState {
24
31
 
25
32
  export interface AppActions {
26
33
  setSelectedTool: (tool: Tool, force?: boolean) => void;
34
+ setActiveTool: (tool: Tool, force?: boolean) => void; // Alias for setSelectedTool
27
35
  addMessage: (type: Message['type'], message: string) => void;
28
36
  clearMessages: () => void;
29
37
  updateFilterEnabled: (enabled: boolean) => void;
@@ -18,6 +18,7 @@ export interface FileImportOptions {
18
18
  acceptedTypes?: string[];
19
19
  multiple?: boolean;
20
20
  includeDirectories?: boolean;
21
+ startIn?: string | FileSystemHandle;
21
22
  }
22
23
 
23
24
  /**
@@ -34,7 +35,7 @@ export class ModernFileImporter {
34
35
  /**
35
36
  * Pick a directory using File System Access API
36
37
  */
37
- async pickDirectory(): Promise<Result<ImportedDirectory>> {
38
+ async pickDirectory(options: FileImportOptions = {}): Promise<Result<ImportedDirectory>> {
38
39
  try {
39
40
  if (!isFileSystemAccessSupported()) {
40
41
  return fail('File System Access API not supported');
@@ -42,7 +43,7 @@ export class ModernFileImporter {
42
43
 
43
44
  const directoryHandle = await window.showDirectoryPicker({
44
45
  mode: 'read',
45
- startIn: 'documents'
46
+ startIn: options.startIn || 'documents'
46
47
  });
47
48
 
48
49
  const importedDirectory = await this.processDirectoryHandle(directoryHandle);
@@ -76,7 +77,8 @@ export class ModernFileImporter {
76
77
  }
77
78
  ]
78
79
  : undefined,
79
- excludeAcceptAllOption: false
80
+ excludeAcceptAllOption: false,
81
+ startIn: options.startIn || 'documents'
80
82
  });
81
83
 
82
84
  const importedFiles: ImportedFile[] = [];
@@ -303,9 +305,9 @@ export class FileImporter {
303
305
  /**
304
306
  * Pick a directory using the best available method
305
307
  */
306
- async pickDirectory(): Promise<Result<ImportedDirectory>> {
308
+ async pickDirectory(options: FileImportOptions = {}): Promise<Result<ImportedDirectory>> {
307
309
  if (isFileSystemAccessSupported()) {
308
- return this.modernImporter.pickDirectory();
310
+ return this.modernImporter.pickDirectory(options);
309
311
  } else {
310
312
  return this.fallbackImporter.pickDirectory();
311
313
  }
@@ -0,0 +1,160 @@
1
+ /*
2
+ * Copyright (c) 2025 Erik Fortune
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ * of this software and associated documentation files (the "Software"), to deal
6
+ * in the Software without restriction, including without limitation the rights
7
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ * copies of the Software, and to permit persons to whom the Software is
9
+ * furnished to do so, subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ * SOFTWARE.
21
+ */
22
+
23
+ import { FileTree, Result, succeed, fail } from '@fgv/ts-utils';
24
+ import { ImportedDirectory, ImportedFile } from './fileImport';
25
+
26
+ /**
27
+ * Converts a FileTree item (directory or file) to ImportedDirectory/ImportedFile format
28
+ * for compatibility with existing resource processing pipeline.
29
+ */
30
+ export class FileTreeConverter {
31
+ /**
32
+ * Converts a FileTree directory to ImportedDirectory format.
33
+ */
34
+ public static convertDirectory(
35
+ fileTree: FileTree.FileTree,
36
+ directoryPath: string
37
+ ): Result<ImportedDirectory> {
38
+ try {
39
+ const itemResult = fileTree.getItem(directoryPath);
40
+ if (itemResult.isFailure()) {
41
+ return fail(`Directory not found: ${directoryPath}`);
42
+ }
43
+
44
+ const item = itemResult.value;
45
+ if (item.type !== 'directory') {
46
+ return fail(`Path is not a directory: ${directoryPath}`);
47
+ }
48
+
49
+ const childrenResult = item.getChildren();
50
+ if (childrenResult.isFailure()) {
51
+ return fail(`Failed to get directory children: ${childrenResult.message}`);
52
+ }
53
+
54
+ const files: ImportedFile[] = [];
55
+ const directories: ImportedDirectory[] = [];
56
+
57
+ for (const child of childrenResult.value) {
58
+ if (child.type === 'file') {
59
+ const fileResult = this.convertFile(child);
60
+ if (fileResult.isSuccess()) {
61
+ files.push(fileResult.value);
62
+ }
63
+ } else if (child.type === 'directory') {
64
+ const dirResult = this.convertDirectory(fileTree, child.absolutePath);
65
+ if (dirResult.isSuccess()) {
66
+ directories.push(dirResult.value);
67
+ }
68
+ }
69
+ }
70
+
71
+ const importedDirectory: ImportedDirectory = {
72
+ name: item.name,
73
+ path: item.absolutePath,
74
+ files,
75
+ directories
76
+ };
77
+
78
+ return succeed(importedDirectory);
79
+ } catch (error) {
80
+ return fail(`Failed to convert directory: ${error instanceof Error ? error.message : String(error)}`);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Converts a FileTree file item to ImportedFile format.
86
+ */
87
+ public static convertFile(fileItem: FileTree.IFileTreeFileItem): Result<ImportedFile> {
88
+ try {
89
+ const contentsResult = fileItem.getRawContents();
90
+ if (contentsResult.isFailure()) {
91
+ return fail(`Failed to read file contents: ${contentsResult.message}`);
92
+ }
93
+
94
+ const importedFile: ImportedFile = {
95
+ name: fileItem.name,
96
+ path: fileItem.absolutePath,
97
+ content: contentsResult.value
98
+ };
99
+
100
+ return succeed(importedFile);
101
+ } catch (error) {
102
+ return fail(`Failed to convert file: ${error instanceof Error ? error.message : String(error)}`);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Converts multiple FileTree files to ImportedFile format.
108
+ */
109
+ public static convertFiles(fileItems: FileTree.IFileTreeFileItem[]): Result<ImportedFile[]> {
110
+ try {
111
+ const importedFiles: ImportedFile[] = [];
112
+
113
+ for (const fileItem of fileItems) {
114
+ const fileResult = this.convertFile(fileItem);
115
+ if (fileResult.isSuccess()) {
116
+ importedFiles.push(fileResult.value);
117
+ } else {
118
+ return fail(`Failed to convert file ${fileItem.name}: ${fileResult.message}`);
119
+ }
120
+ }
121
+
122
+ return succeed(importedFiles);
123
+ } catch (error) {
124
+ return fail(`Failed to convert files: ${error instanceof Error ? error.message : String(error)}`);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Converts a FileTree path to ImportedDirectory or ImportedFile format,
130
+ * automatically detecting the type.
131
+ */
132
+ public static convertPath(
133
+ fileTree: FileTree.FileTree,
134
+ path: string
135
+ ): Result<ImportedDirectory | ImportedFile[]> {
136
+ try {
137
+ const itemResult = fileTree.getItem(path);
138
+ if (itemResult.isFailure()) {
139
+ return fail(`Path not found: ${path}`);
140
+ }
141
+
142
+ const item = itemResult.value;
143
+
144
+ if (item.type === 'directory') {
145
+ return this.convertDirectory(fileTree, path);
146
+ } else if (item.type === 'file') {
147
+ const fileResult = this.convertFile(item);
148
+ if (fileResult.isSuccess()) {
149
+ return succeed([fileResult.value]);
150
+ } else {
151
+ return fileResult;
152
+ }
153
+ } else {
154
+ return fail(`Unknown item type: ${path}`);
155
+ }
156
+ } catch (error) {
157
+ return fail(`Failed to convert path: ${error instanceof Error ? error.message : String(error)}`);
158
+ }
159
+ }
160
+ }