import { BaseTool } from './base.js';
import { GASClient } from '../api/gasClient.js';
import { parsePath } from '../api/pathParser.js';
import { ValidationError, FileOperationError } from '../errors/mcpErrors.js';
import { SessionAuthManager } from '../auth/sessionManager.js';
import { SchemaFragments } from '../utils/schemaFragments.js';
import { ProjectResolver } from '../utils/projectResolver.js';
/**
* Reorder files in a Google Apps Script project
*/
export class ReorderTool extends BaseTool {
public name = 'reorder';
public description = 'Change the execution order of files in a Google Apps Script project';
public inputSchema = {
type: 'object',
properties: {
...SchemaFragments.scriptId,
fileName: {
type: 'string',
description: 'Name of the file to reorder'
},
newPosition: {
type: 'number',
description: 'New position in the execution order (0-based)'
},
...SchemaFragments.accessToken
},
required: ['scriptId', 'fileName', 'newPosition']
};
private gasClient: GASClient;
constructor(sessionAuthManager?: SessionAuthManager) {
super(sessionAuthManager);
this.gasClient = new GASClient();
}
async execute(params: any): Promise<any> {
const accessToken = await this.getAuthToken(params);
const scriptId = this.validate.scriptId(params.scriptId, 'project operation');
const fileName = this.validate.string(params.fileName, 'fileName', 'project operation');
const newPosition = this.validate.number(params.newPosition, 'newPosition', 'project operation', 0);
// Get current files
const files = await this.gasClient.getProjectContent(scriptId, accessToken);
// Find the target file
const targetFile = files.find((f: any) => f.name === fileName);
if (!targetFile) {
throw new FileOperationError('reorder', fileName, 'file not found');
}
// Validate new position
if (newPosition >= files.length) {
throw new ValidationError('newPosition', newPosition, `position between 0 and ${files.length - 1}`);
}
// Get current position of the file being moved
const currentIndex = files.findIndex((f: any) => f.name === fileName);
// CRITICAL: Prevent moving critical infrastructure files from their required positions
const criticalFiles: Record<string, number> = {
'common-js/require': 0,
'common-js/ConfigManager': 1,
'common-js/__mcp_exec': 2
};
if (fileName in criticalFiles) {
const requiredPosition = criticalFiles[fileName];
if (currentIndex === requiredPosition && newPosition !== requiredPosition) {
throw new ValidationError(
'newPosition',
newPosition,
`${fileName} must always remain at position ${requiredPosition}. This is a critical infrastructure file that cannot be moved.`
);
}
// Allow moving it back to required position if it's currently out of place
if (currentIndex !== requiredPosition && newPosition !== requiredPosition) {
throw new ValidationError(
'newPosition',
newPosition,
`${fileName} can only be moved to position ${requiredPosition}. To fix file order, set newPosition to ${requiredPosition}.`
);
}
}
// Create new file order
let reorderedFiles = [...files];
// Remove file from current position
const [movedFile] = reorderedFiles.splice(currentIndex, 1);
// Insert at new position
reorderedFiles.splice(newPosition, 0, movedFile);
// Enforce critical file ordering using extract-and-insert pattern
// Position 0: common-js/require (module system)
// Position 1: common-js/ConfigManager (configuration)
// Position 2: common-js/__mcp_exec (execution infrastructure)
// ✅ File names in GAS API don't have extensions
const criticalFileNames = [
'common-js/require', // Position 0
'common-js/ConfigManager', // Position 1
'common-js/__mcp_exec' // Position 2
];
// Extract critical files in order
const extractedCriticalFiles: any[] = [];
criticalFileNames.forEach(name => {
const file = reorderedFiles.find((f: any) => f.name === name);
if (file) extractedCriticalFiles.push(file);
});
// Remove critical files from array
const nonCriticalFiles = reorderedFiles.filter(
(f: any) => !criticalFileNames.includes(f.name)
);
// Rebuild: critical files first, then others
reorderedFiles = [...extractedCriticalFiles, ...nonCriticalFiles];
// Update the project with new file order
const updatedFiles = await this.gasClient.updateProjectContent(scriptId, reorderedFiles, accessToken);
// ✅ Sync local cache with updated remote mtimes
try {
const { LocalFileManager } = await import('../utils/localFileManager.js');
const { setFileMtimeToRemote } = await import('../utils/fileHelpers.js');
const { join } = await import('path');
const localRoot = await LocalFileManager.getProjectDirectory(scriptId);
if (localRoot) {
// Update mtimes for all files since reordering changes updateTime for all files
for (const file of updatedFiles) {
if (file.updateTime) {
const fileExtension = LocalFileManager.getFileExtensionFromName(file.name);
const localPath = join(localRoot, file.name + fileExtension);
try {
await setFileMtimeToRemote(localPath, file.updateTime, file.type);
} catch (mtimeError) {
// File might not exist locally - that's okay
}
}
}
console.error(`⏰ [SYNC] Updated local mtimes after reorder operation`);
}
} catch (syncError) {
// Don't fail the operation if local sync fails - remote update succeeded
console.error(`⚠️ [SYNC] Failed to update local mtimes after reorder: ${syncError}`);
}
return {
status: 'reordered',
scriptId,
fileName,
oldPosition: currentIndex,
newPosition,
totalFiles: files.length,
message: `Moved ${fileName} from position ${currentIndex} to ${newPosition}. Critical files enforced: require(0), ConfigManager(1), __mcp_exec(2).`
};
}
}
/**
* List all configured projects
*/
export class ProjectListTool extends BaseTool {
public name = 'project_list';
public description = 'List all configured projects';
public inputSchema = {
type: 'object',
properties: {
...SchemaFragments.workingDir
}
};
constructor(sessionAuthManager?: SessionAuthManager) {
super(sessionAuthManager);
}
async execute(params: any): Promise<any> {
const { LocalFileManager } = await import('../utils/localFileManager.js');
const workingDir = params.workingDir || LocalFileManager.getResolvedWorkingDirectory();
// Get projects using utility
const projects = await ProjectResolver.listProjects(workingDir);
// Get current project if set
let currentProject;
try {
currentProject = await ProjectResolver.getCurrentProject(workingDir);
} catch (error) {
currentProject = null;
}
return {
projects,
currentProject,
totalProjects: projects.length,
configPath: workingDir
};
}
}