@mod-computer/cli 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.bundle.js +216 -36371
- package/package.json +3 -3
- package/dist/app.js +0 -227
- package/dist/cli.bundle.js.map +0 -7
- package/dist/cli.js +0 -132
- package/dist/commands/add.js +0 -245
- package/dist/commands/agents-run.js +0 -71
- package/dist/commands/auth.js +0 -259
- package/dist/commands/branch.js +0 -1411
- package/dist/commands/claude-sync.js +0 -772
- package/dist/commands/comment.js +0 -568
- package/dist/commands/diff.js +0 -182
- package/dist/commands/index.js +0 -73
- package/dist/commands/init.js +0 -597
- package/dist/commands/ls.js +0 -135
- package/dist/commands/members.js +0 -687
- package/dist/commands/mv.js +0 -282
- package/dist/commands/recover.js +0 -207
- package/dist/commands/rm.js +0 -257
- package/dist/commands/spec.js +0 -386
- package/dist/commands/status.js +0 -296
- package/dist/commands/sync.js +0 -119
- package/dist/commands/trace.js +0 -1752
- package/dist/commands/workspace.js +0 -447
- package/dist/components/conflict-resolution-ui.js +0 -120
- package/dist/components/messages.js +0 -5
- package/dist/components/thread.js +0 -8
- package/dist/config/features.js +0 -83
- package/dist/containers/branches-container.js +0 -140
- package/dist/containers/directory-container.js +0 -92
- package/dist/containers/thread-container.js +0 -214
- package/dist/containers/threads-container.js +0 -27
- package/dist/containers/workspaces-container.js +0 -27
- package/dist/daemon/conflict-resolution.js +0 -172
- package/dist/daemon/content-hash.js +0 -31
- package/dist/daemon/file-sync.js +0 -985
- package/dist/daemon/index.js +0 -203
- package/dist/daemon/mime-types.js +0 -166
- package/dist/daemon/offline-queue.js +0 -211
- package/dist/daemon/path-utils.js +0 -64
- package/dist/daemon/share-policy.js +0 -83
- package/dist/daemon/wasm-errors.js +0 -189
- package/dist/daemon/worker.js +0 -557
- package/dist/daemon-worker.js +0 -258
- package/dist/errors/workspace-errors.js +0 -48
- package/dist/lib/auth-server.js +0 -216
- package/dist/lib/browser.js +0 -35
- package/dist/lib/diff.js +0 -284
- package/dist/lib/formatters.js +0 -204
- package/dist/lib/git.js +0 -137
- package/dist/lib/local-fs.js +0 -201
- package/dist/lib/prompts.js +0 -56
- package/dist/lib/storage.js +0 -213
- package/dist/lib/trace-formatters.js +0 -314
- package/dist/services/add-service.js +0 -554
- package/dist/services/add-validation.js +0 -124
- package/dist/services/automatic-file-tracker.js +0 -303
- package/dist/services/cli-orchestrator.js +0 -227
- package/dist/services/feature-flags.js +0 -187
- package/dist/services/file-import-service.js +0 -283
- package/dist/services/file-transformation-service.js +0 -218
- package/dist/services/logger.js +0 -44
- package/dist/services/mod-config.js +0 -67
- package/dist/services/modignore-service.js +0 -328
- package/dist/services/sync-daemon.js +0 -244
- package/dist/services/thread-notification-service.js +0 -50
- package/dist/services/thread-service.js +0 -147
- package/dist/stores/use-directory-store.js +0 -96
- package/dist/stores/use-threads-store.js +0 -46
- package/dist/stores/use-workspaces-store.js +0 -54
- package/dist/types/add-types.js +0 -99
- package/dist/types/config.js +0 -16
- package/dist/types/index.js +0 -2
- package/dist/types/workspace-connection.js +0 -53
- package/dist/types.js +0 -1
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
const FEATURE_DIR = '.mod';
|
|
4
|
-
const FEATURE_FILE = 'feature-flags.json';
|
|
5
|
-
const FLAG_LIST_ENV = 'MOD_FLAGS';
|
|
6
|
-
const FEATURE_FILE_ENV = 'MOD_FEATURE_FLAGS_PATH';
|
|
7
|
-
const FEATURE_DEFINITIONS = {
|
|
8
|
-
'tasks-panel': {
|
|
9
|
-
description: 'Enable the Tasks view in the interactive TUI (/tasks).',
|
|
10
|
-
defaultValue: true,
|
|
11
|
-
},
|
|
12
|
-
'directory-view': {
|
|
13
|
-
description: 'Enable the project file browser (/files).',
|
|
14
|
-
defaultValue: true,
|
|
15
|
-
},
|
|
16
|
-
'background-watch': {
|
|
17
|
-
description: 'Allow automatic workspace file watching & background sync.',
|
|
18
|
-
defaultValue: true,
|
|
19
|
-
},
|
|
20
|
-
'agents-run-command': {
|
|
21
|
-
description: 'Expose the experimental agents-run command.',
|
|
22
|
-
defaultValue: true,
|
|
23
|
-
},
|
|
24
|
-
'full-ui': {
|
|
25
|
-
description: 'Enable full CLI functionality beyond alpha-safe commands.',
|
|
26
|
-
defaultValue: false,
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
const FLAG_KEYS = Object.keys(FEATURE_DEFINITIONS);
|
|
30
|
-
let cachedFlags = null;
|
|
31
|
-
const TRUE_VALUES = new Set(['1', 'true', 'yes', 'on', 'enable', 'enabled']);
|
|
32
|
-
const FALSE_VALUES = new Set(['0', 'false', 'no', 'off', 'disable', 'disabled']);
|
|
33
|
-
function canonicalizeFlagName(name) {
|
|
34
|
-
if (!name)
|
|
35
|
-
return null;
|
|
36
|
-
const normalized = name
|
|
37
|
-
.trim()
|
|
38
|
-
.toLowerCase()
|
|
39
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
40
|
-
.replace(/^-+/, '')
|
|
41
|
-
.replace(/-+$/, '');
|
|
42
|
-
return (FLAG_KEYS.find(flag => flag === normalized) ?? null);
|
|
43
|
-
}
|
|
44
|
-
function parseBoolean(value) {
|
|
45
|
-
if (typeof value === 'boolean')
|
|
46
|
-
return value;
|
|
47
|
-
if (typeof value === 'number')
|
|
48
|
-
return value !== 0;
|
|
49
|
-
if (typeof value === 'string') {
|
|
50
|
-
const normalized = value.trim().toLowerCase();
|
|
51
|
-
if (TRUE_VALUES.has(normalized))
|
|
52
|
-
return true;
|
|
53
|
-
if (FALSE_VALUES.has(normalized))
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
return undefined;
|
|
57
|
-
}
|
|
58
|
-
function getFeatureFlagsFilePath() {
|
|
59
|
-
const overridePath = process.env[FEATURE_FILE_ENV];
|
|
60
|
-
if (overridePath && overridePath.trim().length > 0) {
|
|
61
|
-
return path.resolve(overridePath.trim());
|
|
62
|
-
}
|
|
63
|
-
return path.join(process.cwd(), FEATURE_DIR, FEATURE_FILE);
|
|
64
|
-
}
|
|
65
|
-
function getPackageLocalConfigPath() {
|
|
66
|
-
try {
|
|
67
|
-
const __filename = new URL(import.meta.url).pathname;
|
|
68
|
-
const __dirname = path.dirname(__filename);
|
|
69
|
-
// From dist/services -> ../config/feature-flags.json
|
|
70
|
-
return path.join(__dirname, '../../config/feature-flags.json');
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
return '';
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
function parseConfigFile(filePath) {
|
|
77
|
-
if (!fs.existsSync(filePath))
|
|
78
|
-
return {};
|
|
79
|
-
try {
|
|
80
|
-
const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
81
|
-
const overrides = {};
|
|
82
|
-
for (const [name, value] of Object.entries(raw)) {
|
|
83
|
-
const key = canonicalizeFlagName(name);
|
|
84
|
-
if (!key)
|
|
85
|
-
continue;
|
|
86
|
-
if (typeof value === 'boolean') {
|
|
87
|
-
overrides[key] = value;
|
|
88
|
-
}
|
|
89
|
-
else if (typeof value === 'string') {
|
|
90
|
-
const parsed = parseBoolean(value);
|
|
91
|
-
if (typeof parsed === 'boolean')
|
|
92
|
-
overrides[key] = parsed;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return overrides;
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
return {};
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
function readJsonOverrides() {
|
|
102
|
-
const globalOverrides = parseConfigFile(getFeatureFlagsFilePath());
|
|
103
|
-
const packageOverrides = parseConfigFile(getPackageLocalConfigPath());
|
|
104
|
-
// Package-local overrides take precedence over global ones
|
|
105
|
-
return { ...globalOverrides, ...packageOverrides };
|
|
106
|
-
}
|
|
107
|
-
function parseListEnvOverrides() {
|
|
108
|
-
const value = process.env[FLAG_LIST_ENV];
|
|
109
|
-
if (!value)
|
|
110
|
-
return {};
|
|
111
|
-
const overrides = {};
|
|
112
|
-
const tokens = value.split(/[, ]+/g).map(token => token.trim()).filter(Boolean);
|
|
113
|
-
for (const token of tokens) {
|
|
114
|
-
let enabled = true;
|
|
115
|
-
let name = token;
|
|
116
|
-
if (name.startsWith('!') || name.startsWith('-')) {
|
|
117
|
-
enabled = false;
|
|
118
|
-
name = name.slice(1);
|
|
119
|
-
}
|
|
120
|
-
else if (name.startsWith('+')) {
|
|
121
|
-
name = name.slice(1);
|
|
122
|
-
}
|
|
123
|
-
const key = canonicalizeFlagName(name);
|
|
124
|
-
if (key)
|
|
125
|
-
overrides[key] = enabled;
|
|
126
|
-
}
|
|
127
|
-
return overrides;
|
|
128
|
-
}
|
|
129
|
-
function parsePerFlagEnvOverrides() {
|
|
130
|
-
const overrides = {};
|
|
131
|
-
for (const key of FLAG_KEYS) {
|
|
132
|
-
const envName = `MOD_FLAG_${key.replace(/[^A-Z0-9]/gi, '_').toUpperCase()}`;
|
|
133
|
-
const raw = process.env[envName];
|
|
134
|
-
const parsed = parseBoolean(raw);
|
|
135
|
-
if (typeof parsed === 'boolean') {
|
|
136
|
-
overrides[key] = parsed;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
const fullUiEnv = process.env['MOD_CLI_FULL_UI'];
|
|
140
|
-
const fullUiParsed = parseBoolean(fullUiEnv);
|
|
141
|
-
if (typeof fullUiParsed === 'boolean') {
|
|
142
|
-
overrides['full-ui'] = fullUiParsed;
|
|
143
|
-
}
|
|
144
|
-
return overrides;
|
|
145
|
-
}
|
|
146
|
-
function buildFlags() {
|
|
147
|
-
const overrides = {
|
|
148
|
-
...readJsonOverrides(),
|
|
149
|
-
...parseListEnvOverrides(),
|
|
150
|
-
...parsePerFlagEnvOverrides(),
|
|
151
|
-
};
|
|
152
|
-
const resolved = {};
|
|
153
|
-
for (const key of FLAG_KEYS) {
|
|
154
|
-
resolved[key] = overrides[key] ?? FEATURE_DEFINITIONS[key].defaultValue;
|
|
155
|
-
}
|
|
156
|
-
return resolved;
|
|
157
|
-
}
|
|
158
|
-
export function getFeatureFlags(options) {
|
|
159
|
-
if (!cachedFlags || options?.refresh) {
|
|
160
|
-
cachedFlags = buildFlags();
|
|
161
|
-
}
|
|
162
|
-
return cachedFlags;
|
|
163
|
-
}
|
|
164
|
-
export function isFeatureEnabled(flag) {
|
|
165
|
-
return getFeatureFlags()[flag];
|
|
166
|
-
}
|
|
167
|
-
export function describeFeatureFlags() {
|
|
168
|
-
const flags = getFeatureFlags();
|
|
169
|
-
return FLAG_KEYS.map(key => ({
|
|
170
|
-
key,
|
|
171
|
-
description: FEATURE_DEFINITIONS[key].description,
|
|
172
|
-
defaultValue: FEATURE_DEFINITIONS[key].defaultValue,
|
|
173
|
-
enabled: flags[key],
|
|
174
|
-
}));
|
|
175
|
-
}
|
|
176
|
-
export function shouldEnableBackgroundWatch() {
|
|
177
|
-
const env = process.env.MOD_CLI_AUTO_WATCH;
|
|
178
|
-
if (env === '1')
|
|
179
|
-
return true;
|
|
180
|
-
if (env === '0')
|
|
181
|
-
return false;
|
|
182
|
-
return isFeatureEnabled('background-watch');
|
|
183
|
-
}
|
|
184
|
-
export function shouldExposeFullUi() {
|
|
185
|
-
return isFeatureEnabled('full-ui');
|
|
186
|
-
}
|
|
187
|
-
export const FEATURE_FLAGS_FILE_NAME = FEATURE_FILE;
|
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import crypto from 'crypto';
|
|
4
|
-
import { createModWorkspace } from '@mod/mod-core/mod-workspace';
|
|
5
|
-
import { detectMimeType, mimeTypeToCanvasType } from '@mod/mod-core';
|
|
6
|
-
import { ModIgnoreService } from './modignore-service.js';
|
|
7
|
-
import { readModConfig } from './mod-config.js';
|
|
8
|
-
export class FileImportService {
|
|
9
|
-
constructor(repo) {
|
|
10
|
-
this.folderCache = new Map(); // path -> folderId
|
|
11
|
-
this.repo = repo;
|
|
12
|
-
}
|
|
13
|
-
async previewImport(options = {}) {
|
|
14
|
-
console.log('🔍 Scanning for importable files...');
|
|
15
|
-
const workingDir = options.workingDirectory || process.cwd();
|
|
16
|
-
const preview = await this.scanFiles(workingDir, options);
|
|
17
|
-
console.log('\n📋 Import Preview:');
|
|
18
|
-
console.log(` Total files scanned: ${preview.totalFilesScanned}`);
|
|
19
|
-
console.log(` Files to import: ${preview.filteredFiles.length}`);
|
|
20
|
-
console.log(` Files excluded: ${preview.excludedFiles.length}`);
|
|
21
|
-
console.log(` Estimated size: ${this.formatBytes(preview.estimatedSize)}`);
|
|
22
|
-
console.log(` Estimated time: ${preview.estimatedDuration}ms`);
|
|
23
|
-
if (options.verbose) {
|
|
24
|
-
console.log('\n📁 Files to import:');
|
|
25
|
-
preview.filteredFiles.slice(0, 20).forEach(file => {
|
|
26
|
-
console.log(` ${file}`);
|
|
27
|
-
});
|
|
28
|
-
if (preview.filteredFiles.length > 20) {
|
|
29
|
-
console.log(` ... and ${preview.filteredFiles.length - 20} more files`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return preview;
|
|
33
|
-
}
|
|
34
|
-
async promptImport(options = {}) {
|
|
35
|
-
const preview = await this.previewImport(options);
|
|
36
|
-
if (preview.filteredFiles.length === 0) {
|
|
37
|
-
console.log('❌ No files found to import');
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
// Simple confirmation for now - could enhance with readline interface
|
|
41
|
-
console.log('\n❓ Proceed with import? (This will create documents in your workspace)');
|
|
42
|
-
console.log(' Type "y" to continue or any other key to cancel...');
|
|
43
|
-
// For CLI implementation, we'll return true for now
|
|
44
|
-
// In a full implementation, this would use readline to get user input
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
async executeImport(options = {}) {
|
|
48
|
-
const startTime = Date.now();
|
|
49
|
-
const workingDir = options.workingDirectory || process.cwd();
|
|
50
|
-
const batchSize = options.batchSize || 50;
|
|
51
|
-
console.log('🚀 Starting file import...');
|
|
52
|
-
// Clear folder cache for fresh import
|
|
53
|
-
this.folderCache.clear();
|
|
54
|
-
const config = readModConfig();
|
|
55
|
-
if (!config || !config.workspaceId) {
|
|
56
|
-
throw new Error('No active workspace configured. Run `mod workspace create <name>` first.');
|
|
57
|
-
}
|
|
58
|
-
const modWorkspace = createModWorkspace(this.repo);
|
|
59
|
-
const workspaceHandle = await modWorkspace.openWorkspace(config.workspaceId);
|
|
60
|
-
const preview = await this.scanFiles(workingDir, options);
|
|
61
|
-
const result = {
|
|
62
|
-
importedFiles: [],
|
|
63
|
-
skippedFiles: [],
|
|
64
|
-
errors: [],
|
|
65
|
-
totalDuration: 0
|
|
66
|
-
};
|
|
67
|
-
const batches = this.chunkArray(preview.filteredFiles, batchSize);
|
|
68
|
-
for (let i = 0; i < batches.length; i++) {
|
|
69
|
-
const batch = batches[i];
|
|
70
|
-
console.log(`📦 Processing batch ${i + 1}/${batches.length} (${batch.length} files)...`);
|
|
71
|
-
await Promise.all(batch.map(async (filePath) => {
|
|
72
|
-
try {
|
|
73
|
-
await this.importFile(filePath, workspaceHandle, workingDir);
|
|
74
|
-
result.importedFiles.push(filePath);
|
|
75
|
-
if (options.verbose) {
|
|
76
|
-
console.log(` ✓ ${filePath}`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
result.errors.push({
|
|
81
|
-
file: filePath,
|
|
82
|
-
error: error instanceof Error ? error.message : String(error)
|
|
83
|
-
});
|
|
84
|
-
if (options.verbose) {
|
|
85
|
-
console.log(` ❌ ${filePath}: ${error}`);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}));
|
|
89
|
-
// Small delay between batches to prevent overwhelming the system
|
|
90
|
-
if (i < batches.length - 1) {
|
|
91
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
result.totalDuration = Date.now() - startTime;
|
|
95
|
-
console.log('\n✅ Import complete!');
|
|
96
|
-
console.log(` Imported: ${result.importedFiles.length} files`);
|
|
97
|
-
console.log(` Skipped: ${result.skippedFiles.length} files`);
|
|
98
|
-
console.log(` Errors: ${result.errors.length} files`);
|
|
99
|
-
console.log(` Duration: ${result.totalDuration}ms`);
|
|
100
|
-
if (result.errors.length > 0) {
|
|
101
|
-
console.log('\n❌ Errors occurred:');
|
|
102
|
-
result.errors.slice(0, 5).forEach(error => {
|
|
103
|
-
console.log(` ${error.file}: ${error.error}`);
|
|
104
|
-
});
|
|
105
|
-
if (result.errors.length > 5) {
|
|
106
|
-
console.log(` ... and ${result.errors.length - 5} more errors`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return result;
|
|
110
|
-
}
|
|
111
|
-
async scanFiles(workingDir, options) {
|
|
112
|
-
this.modIgnoreService = new ModIgnoreService(workingDir);
|
|
113
|
-
const scannedFiles = [];
|
|
114
|
-
const filteredFiles = [];
|
|
115
|
-
const excludedFiles = [];
|
|
116
|
-
let totalSize = 0;
|
|
117
|
-
await this.scanDirectory(workingDir, workingDir, scannedFiles, filteredFiles, excludedFiles, options);
|
|
118
|
-
// Calculate size estimates
|
|
119
|
-
for (const file of filteredFiles) {
|
|
120
|
-
try {
|
|
121
|
-
const stats = await fs.promises.stat(file);
|
|
122
|
-
totalSize += stats.size;
|
|
123
|
-
}
|
|
124
|
-
catch (error) {
|
|
125
|
-
// File might have been deleted during scan, skip
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return {
|
|
129
|
-
totalFilesScanned: scannedFiles.length,
|
|
130
|
-
filteredFiles,
|
|
131
|
-
excludedFiles,
|
|
132
|
-
estimatedSize: totalSize,
|
|
133
|
-
estimatedDuration: filteredFiles.length * 10 // Rough estimate: 10ms per file
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
async scanDirectory(rootDir, currentDir, scannedFiles, filteredFiles, excludedFiles, options) {
|
|
137
|
-
try {
|
|
138
|
-
const entries = await fs.promises.readdir(currentDir, { withFileTypes: true });
|
|
139
|
-
for (const entry of entries) {
|
|
140
|
-
const fullPath = path.join(currentDir, entry.name);
|
|
141
|
-
const relativePath = path.relative(rootDir, fullPath);
|
|
142
|
-
if (entry.isDirectory()) {
|
|
143
|
-
// Check if directory should be excluded
|
|
144
|
-
if (this.modIgnoreService?.shouldIgnore(fullPath, rootDir)) {
|
|
145
|
-
excludedFiles.push(relativePath);
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
// Recurse into directory
|
|
149
|
-
await this.scanDirectory(rootDir, fullPath, scannedFiles, filteredFiles, excludedFiles, options);
|
|
150
|
-
}
|
|
151
|
-
else if (entry.isFile()) {
|
|
152
|
-
scannedFiles.push(relativePath);
|
|
153
|
-
// Check if file should be excluded
|
|
154
|
-
if (this.modIgnoreService?.shouldIgnore(fullPath, rootDir)) {
|
|
155
|
-
excludedFiles.push(relativePath);
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
// Check file extension filter
|
|
159
|
-
if (this.isTrackableFile(fullPath)) {
|
|
160
|
-
// Apply pattern filters if specified
|
|
161
|
-
if (options.patterns && options.patterns.length > 0) {
|
|
162
|
-
const matches = options.patterns.some(pattern => relativePath.includes(pattern) || fullPath.includes(pattern));
|
|
163
|
-
if (!matches) {
|
|
164
|
-
excludedFiles.push(relativePath);
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
filteredFiles.push(fullPath);
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
excludedFiles.push(relativePath);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
catch (error) {
|
|
177
|
-
// Directory might not be accessible, skip
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
isTrackableFile(filePath) {
|
|
181
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
182
|
-
const trackableExtensions = [
|
|
183
|
-
'.md', '.txt', '.js', '.ts', '.jsx', '.tsx', '.json', '.yaml', '.yml',
|
|
184
|
-
'.py', '.java', '.cpp', '.c', '.h', '.css', '.scss', '.html', '.xml',
|
|
185
|
-
'.sql', '.go', '.rs', '.php', '.rb', '.swift', '.kt', '.scala', '.sh',
|
|
186
|
-
'.ps1', '.dockerfile', '.tf', '.hcl', '.vue', '.svelte', '.astro'
|
|
187
|
-
];
|
|
188
|
-
return trackableExtensions.includes(ext);
|
|
189
|
-
}
|
|
190
|
-
async importFile(filePath, workspaceHandle, workingDir) {
|
|
191
|
-
try {
|
|
192
|
-
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
193
|
-
const relativePath = path.relative(workingDir, filePath);
|
|
194
|
-
const contentHash = crypto.createHash('sha256').update(content).digest('hex');
|
|
195
|
-
// Parse directory structure to create hierarchical folders
|
|
196
|
-
const pathParts = relativePath.split(path.sep);
|
|
197
|
-
const fileName = pathParts.pop();
|
|
198
|
-
const dirPath = pathParts.join('/');
|
|
199
|
-
let folderId = null;
|
|
200
|
-
// Create folder hierarchy if the file is in a subdirectory
|
|
201
|
-
if (dirPath) {
|
|
202
|
-
folderId = await this.ensureFolderHierarchy(dirPath, workspaceHandle);
|
|
203
|
-
}
|
|
204
|
-
// Create document via workspace handle with proper folder association
|
|
205
|
-
const ext = path.extname(fileName).toLowerCase();
|
|
206
|
-
const mimeType = this.getMimeTypeForExtension(ext);
|
|
207
|
-
const canvasType = mimeTypeToCanvasType(mimeType);
|
|
208
|
-
const documentData = {
|
|
209
|
-
text: content,
|
|
210
|
-
metadata: {
|
|
211
|
-
type: canvasType, // Required for workspace container routing
|
|
212
|
-
originalFilename: fileName,
|
|
213
|
-
tags: [`imported:${new Date().toISOString()}`, `hash:${contentHash}`],
|
|
214
|
-
typeData: {
|
|
215
|
-
originalPath: filePath,
|
|
216
|
-
relativePath,
|
|
217
|
-
contentHash
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
const fileDoc = await workspaceHandle.file.create(documentData, {
|
|
222
|
-
name: fileName, // Use just the filename, not the full path
|
|
223
|
-
mimeType,
|
|
224
|
-
folderId: folderId, // Associate with the appropriate folder (cast to handle DocumentId type)
|
|
225
|
-
});
|
|
226
|
-
// This should be handled automatically by the workspace handle
|
|
227
|
-
}
|
|
228
|
-
catch (error) {
|
|
229
|
-
throw new Error(`Failed to import ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
async ensureFolderHierarchy(dirPath, workspaceHandle) {
|
|
233
|
-
// Check cache first to avoid creating duplicate folders
|
|
234
|
-
if (this.folderCache.has(dirPath)) {
|
|
235
|
-
return this.folderCache.get(dirPath);
|
|
236
|
-
}
|
|
237
|
-
const pathParts = dirPath.split('/').filter(Boolean);
|
|
238
|
-
let currentPath = '';
|
|
239
|
-
let parentFolderId = null;
|
|
240
|
-
// Create folders hierarchically from root to deepest level
|
|
241
|
-
for (const folderName of pathParts) {
|
|
242
|
-
currentPath = currentPath ? `${currentPath}/${folderName}` : folderName;
|
|
243
|
-
if (this.folderCache.has(currentPath)) {
|
|
244
|
-
parentFolderId = this.folderCache.get(currentPath);
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
|
-
// Create folder using workspace handle - we'll use the folder path approach
|
|
248
|
-
// since the workspace handle doesn't expose the direct service createFolder method
|
|
249
|
-
const folderRef = await workspaceHandle.folder.create(currentPath, {
|
|
250
|
-
name: folderName,
|
|
251
|
-
metadata: {
|
|
252
|
-
type: 'folder',
|
|
253
|
-
path: currentPath,
|
|
254
|
-
createdAt: new Date().toISOString()
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
// Cache the folder ID for reuse
|
|
258
|
-
this.folderCache.set(currentPath, folderRef.id);
|
|
259
|
-
parentFolderId = folderRef.id;
|
|
260
|
-
}
|
|
261
|
-
return parentFolderId;
|
|
262
|
-
}
|
|
263
|
-
chunkArray(array, chunkSize) {
|
|
264
|
-
const chunks = [];
|
|
265
|
-
for (let i = 0; i < array.length; i += chunkSize) {
|
|
266
|
-
chunks.push(array.slice(i, i + chunkSize));
|
|
267
|
-
}
|
|
268
|
-
return chunks;
|
|
269
|
-
}
|
|
270
|
-
getMimeTypeForExtension(ext) {
|
|
271
|
-
// Use centralized MIME type detection from mod-core
|
|
272
|
-
const fileName = `file${ext}`;
|
|
273
|
-
return detectMimeType(fileName);
|
|
274
|
-
}
|
|
275
|
-
formatBytes(bytes) {
|
|
276
|
-
if (bytes === 0)
|
|
277
|
-
return '0 Bytes';
|
|
278
|
-
const k = 1024;
|
|
279
|
-
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
280
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
281
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
282
|
-
}
|
|
283
|
-
}
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
export class FileTransformationService {
|
|
3
|
-
constructor() {
|
|
4
|
-
this.languageExtensionMap = {
|
|
5
|
-
// JavaScript/TypeScript
|
|
6
|
-
javascript: '.js',
|
|
7
|
-
typescript: '.ts',
|
|
8
|
-
tsx: '.tsx',
|
|
9
|
-
jsx: '.jsx',
|
|
10
|
-
// Python
|
|
11
|
-
python: '.py',
|
|
12
|
-
// Java/Kotlin/Scala
|
|
13
|
-
java: '.java',
|
|
14
|
-
kotlin: '.kt',
|
|
15
|
-
scala: '.scala',
|
|
16
|
-
// C/C++
|
|
17
|
-
c: '.c',
|
|
18
|
-
cpp: '.cpp',
|
|
19
|
-
'c++': '.cpp',
|
|
20
|
-
cc: '.cc',
|
|
21
|
-
cxx: '.cxx',
|
|
22
|
-
// C#
|
|
23
|
-
csharp: '.cs',
|
|
24
|
-
'c#': '.cs',
|
|
25
|
-
// Go
|
|
26
|
-
go: '.go',
|
|
27
|
-
golang: '.go',
|
|
28
|
-
// Rust
|
|
29
|
-
rust: '.rs',
|
|
30
|
-
// PHP
|
|
31
|
-
php: '.php',
|
|
32
|
-
// Ruby
|
|
33
|
-
ruby: '.rb',
|
|
34
|
-
// Swift
|
|
35
|
-
swift: '.swift',
|
|
36
|
-
// Dart
|
|
37
|
-
dart: '.dart',
|
|
38
|
-
// Shell/Bash
|
|
39
|
-
shell: '.sh',
|
|
40
|
-
bash: '.sh',
|
|
41
|
-
zsh: '.zsh',
|
|
42
|
-
fish: '.fish',
|
|
43
|
-
// Web technologies
|
|
44
|
-
html: '.html',
|
|
45
|
-
css: '.css',
|
|
46
|
-
scss: '.scss',
|
|
47
|
-
sass: '.sass',
|
|
48
|
-
less: '.less',
|
|
49
|
-
// Config/Data
|
|
50
|
-
json: '.json',
|
|
51
|
-
yaml: '.yml',
|
|
52
|
-
yml: '.yml',
|
|
53
|
-
toml: '.toml',
|
|
54
|
-
xml: '.xml',
|
|
55
|
-
// SQL
|
|
56
|
-
sql: '.sql',
|
|
57
|
-
mysql: '.sql',
|
|
58
|
-
postgresql: '.sql',
|
|
59
|
-
sqlite: '.sql',
|
|
60
|
-
// Other
|
|
61
|
-
dockerfile: 'Dockerfile',
|
|
62
|
-
makefile: 'Makefile',
|
|
63
|
-
r: '.r',
|
|
64
|
-
matlab: '.m',
|
|
65
|
-
lua: '.lua',
|
|
66
|
-
perl: '.pl',
|
|
67
|
-
// Assembly
|
|
68
|
-
assembly: '.asm',
|
|
69
|
-
asm: '.asm',
|
|
70
|
-
// Functional languages
|
|
71
|
-
haskell: '.hs',
|
|
72
|
-
clojure: '.clj',
|
|
73
|
-
erlang: '.erl',
|
|
74
|
-
elixir: '.ex',
|
|
75
|
-
ocaml: '.ml',
|
|
76
|
-
fsharp: '.fs',
|
|
77
|
-
// Other popular languages
|
|
78
|
-
groovy: '.groovy',
|
|
79
|
-
powershell: '.ps1',
|
|
80
|
-
vim: '.vim',
|
|
81
|
-
// Markup
|
|
82
|
-
markdown: '.md',
|
|
83
|
-
tex: '.tex',
|
|
84
|
-
latex: '.tex',
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Transform a file based on its metadata
|
|
89
|
-
*/
|
|
90
|
-
transformFile(originalFileName, content, metadata) {
|
|
91
|
-
const result = {
|
|
92
|
-
fileName: originalFileName,
|
|
93
|
-
content,
|
|
94
|
-
transformed: false,
|
|
95
|
-
originalType: metadata.type,
|
|
96
|
-
};
|
|
97
|
-
if (metadata.type === 'text') {
|
|
98
|
-
// Transform text files to markdown
|
|
99
|
-
result.fileName = this.ensureExtension(originalFileName, '.md');
|
|
100
|
-
result.transformed = result.fileName !== originalFileName;
|
|
101
|
-
result.targetType = 'markdown';
|
|
102
|
-
}
|
|
103
|
-
else if (metadata.type === 'code') {
|
|
104
|
-
// Transform code files based on language
|
|
105
|
-
const language = this.detectLanguage(metadata, originalFileName);
|
|
106
|
-
if (language) {
|
|
107
|
-
const extension = this.languageExtensionMap[language.toLowerCase()];
|
|
108
|
-
if (extension) {
|
|
109
|
-
result.fileName = this.replaceExtension(originalFileName, extension);
|
|
110
|
-
result.transformed = result.fileName !== originalFileName;
|
|
111
|
-
result.targetType = `code (${language})`;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
// If no language detected or mapped, default to .txt
|
|
115
|
-
if (!result.transformed) {
|
|
116
|
-
result.fileName = this.ensureExtension(originalFileName, '.txt');
|
|
117
|
-
result.transformed = result.fileName !== originalFileName;
|
|
118
|
-
result.targetType = 'text (unknown language)';
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return result;
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Detect the programming language from metadata or filename
|
|
125
|
-
*/
|
|
126
|
-
detectLanguage(metadata, fileName) {
|
|
127
|
-
// First, check metadata for explicit language
|
|
128
|
-
if (metadata.language) {
|
|
129
|
-
return metadata.language;
|
|
130
|
-
}
|
|
131
|
-
// Try to infer from filename extension
|
|
132
|
-
const ext = path.extname(fileName).toLowerCase();
|
|
133
|
-
const extToLanguage = {
|
|
134
|
-
'.js': 'javascript',
|
|
135
|
-
'.ts': 'typescript',
|
|
136
|
-
'.tsx': 'tsx',
|
|
137
|
-
'.jsx': 'jsx',
|
|
138
|
-
'.py': 'python',
|
|
139
|
-
'.java': 'java',
|
|
140
|
-
'.kt': 'kotlin',
|
|
141
|
-
'.scala': 'scala',
|
|
142
|
-
'.c': 'c',
|
|
143
|
-
'.cpp': 'cpp',
|
|
144
|
-
'.cc': 'cpp',
|
|
145
|
-
'.cxx': 'cpp',
|
|
146
|
-
'.cs': 'csharp',
|
|
147
|
-
'.go': 'go',
|
|
148
|
-
'.rs': 'rust',
|
|
149
|
-
'.php': 'php',
|
|
150
|
-
'.rb': 'ruby',
|
|
151
|
-
'.swift': 'swift',
|
|
152
|
-
'.dart': 'dart',
|
|
153
|
-
'.sh': 'shell',
|
|
154
|
-
'.bash': 'bash',
|
|
155
|
-
'.zsh': 'zsh',
|
|
156
|
-
'.fish': 'fish',
|
|
157
|
-
'.html': 'html',
|
|
158
|
-
'.css': 'css',
|
|
159
|
-
'.scss': 'scss',
|
|
160
|
-
'.sass': 'sass',
|
|
161
|
-
'.less': 'less',
|
|
162
|
-
'.json': 'json',
|
|
163
|
-
'.yml': 'yaml',
|
|
164
|
-
'.yaml': 'yaml',
|
|
165
|
-
'.toml': 'toml',
|
|
166
|
-
'.xml': 'xml',
|
|
167
|
-
'.sql': 'sql',
|
|
168
|
-
'.r': 'r',
|
|
169
|
-
'.m': 'matlab',
|
|
170
|
-
'.lua': 'lua',
|
|
171
|
-
'.pl': 'perl',
|
|
172
|
-
'.asm': 'assembly',
|
|
173
|
-
'.hs': 'haskell',
|
|
174
|
-
'.clj': 'clojure',
|
|
175
|
-
'.erl': 'erlang',
|
|
176
|
-
'.ex': 'elixir',
|
|
177
|
-
'.ml': 'ocaml',
|
|
178
|
-
'.fs': 'fsharp',
|
|
179
|
-
'.groovy': 'groovy',
|
|
180
|
-
'.ps1': 'powershell',
|
|
181
|
-
'.vim': 'vim',
|
|
182
|
-
'.md': 'markdown',
|
|
183
|
-
'.tex': 'latex',
|
|
184
|
-
};
|
|
185
|
-
return extToLanguage[ext] || null;
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Ensure a filename has a specific extension
|
|
189
|
-
*/
|
|
190
|
-
ensureExtension(fileName, extension) {
|
|
191
|
-
const currentExt = path.extname(fileName);
|
|
192
|
-
if (currentExt === extension) {
|
|
193
|
-
return fileName;
|
|
194
|
-
}
|
|
195
|
-
const baseName = currentExt ? fileName.slice(0, -currentExt.length) : fileName;
|
|
196
|
-
return baseName + extension;
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Replace the extension of a filename
|
|
200
|
-
*/
|
|
201
|
-
replaceExtension(fileName, newExtension) {
|
|
202
|
-
const currentExt = path.extname(fileName);
|
|
203
|
-
const baseName = currentExt ? fileName.slice(0, -currentExt.length) : fileName;
|
|
204
|
-
return baseName + newExtension;
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Get the mapping of languages to extensions (for documentation/debugging)
|
|
208
|
-
*/
|
|
209
|
-
getLanguageExtensionMap() {
|
|
210
|
-
return { ...this.languageExtensionMap };
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* Add or update a language extension mapping
|
|
214
|
-
*/
|
|
215
|
-
setLanguageExtension(language, extension) {
|
|
216
|
-
this.languageExtensionMap[language.toLowerCase()] = extension;
|
|
217
|
-
}
|
|
218
|
-
}
|