feat: initial commit - Jhonny Editor
- Adicionado estrutura completa do projeto - Configurado MCP server para Premiere Pro - Adicionado documentação e skills - Configurado Gitignore para o projeto
This commit is contained in:
Executable
+291
@@ -0,0 +1,291 @@
|
||||
import {
|
||||
copyFileSync,
|
||||
constants,
|
||||
existsSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
statSync,
|
||||
} from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { basename, dirname, extname, join, resolve } from "node:path";
|
||||
import { buildToolScript } from "../bridge/script-builder.js";
|
||||
import {
|
||||
getTempDir,
|
||||
getBridgeLiveness,
|
||||
sendCommand,
|
||||
type BridgeOptions,
|
||||
type CommandResult,
|
||||
} from "../bridge/file-bridge.js";
|
||||
|
||||
const MAX_CANDIDATES = 50;
|
||||
const AUTOSAVE_DIR_NAMES = [
|
||||
"Adobe Premiere Pro Auto-Save",
|
||||
"Premiere Pro Auto-Save",
|
||||
];
|
||||
|
||||
interface ProjectSnapshot {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface RecoveryCandidate {
|
||||
path: string;
|
||||
fileName: string;
|
||||
modifiedAt: string;
|
||||
modifiedMs: number;
|
||||
sizeBytes: number;
|
||||
newerThanProjectFile: boolean | null;
|
||||
}
|
||||
|
||||
export interface ProjectBackupReceipt {
|
||||
sourcePath: string;
|
||||
backupPath: string;
|
||||
sizeBytes: number;
|
||||
checksumSha256: string;
|
||||
sourceUnchanged: true;
|
||||
byteIdentical: true;
|
||||
}
|
||||
|
||||
function sha256File(path: string): string {
|
||||
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
||||
}
|
||||
|
||||
export function createProjectBackup(projectPath: string, now = new Date()): ProjectBackupReceipt {
|
||||
const sourcePath = resolve(projectPath);
|
||||
if (extname(sourcePath).toLowerCase() !== ".prproj") {
|
||||
throw new Error("project_path must point to an Adobe Premiere .prproj file");
|
||||
}
|
||||
if (!existsSync(sourcePath)) throw new Error(`Project file does not exist: ${sourcePath}`);
|
||||
const before = statSync(sourcePath);
|
||||
if (!before.isFile()) throw new Error(`Project path is not a regular file: ${sourcePath}`);
|
||||
|
||||
const stamp = now.toISOString().replace(/[:.]/g, "-");
|
||||
const backupPath = `${sourcePath}.backup-${stamp}`;
|
||||
copyFileSync(sourcePath, backupPath, constants.COPYFILE_EXCL);
|
||||
|
||||
const after = statSync(sourcePath);
|
||||
const backup = statSync(backupPath);
|
||||
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
|
||||
throw new Error("Project file changed while its backup was being created; do not rely on this copy");
|
||||
}
|
||||
const sourceChecksum = sha256File(sourcePath);
|
||||
const backupChecksum = sha256File(backupPath);
|
||||
if (backup.size !== before.size || backupChecksum !== sourceChecksum) {
|
||||
throw new Error("Project backup verification failed: copied bytes do not match the source");
|
||||
}
|
||||
return {
|
||||
sourcePath,
|
||||
backupPath,
|
||||
sizeBytes: backup.size,
|
||||
checksumSha256: backupChecksum,
|
||||
sourceUnchanged: true,
|
||||
byteIdentical: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function discoverAdjacentRecoveryCandidates(
|
||||
projectPath: string,
|
||||
): RecoveryCandidate[] {
|
||||
if (!projectPath || extname(projectPath).toLowerCase() !== ".prproj") return [];
|
||||
const absoluteProjectPath = resolve(projectPath);
|
||||
const projectDirectory = dirname(absoluteProjectPath);
|
||||
const projectStem = basename(projectPath, extname(projectPath)).toLowerCase();
|
||||
let projectModifiedMs: number | null = null;
|
||||
try {
|
||||
projectModifiedMs = statSync(absoluteProjectPath).mtimeMs;
|
||||
} catch {
|
||||
// An unsaved/new or inaccessible project may not exist on disk yet.
|
||||
}
|
||||
|
||||
const roots = [
|
||||
...AUTOSAVE_DIR_NAMES.map((name) => join(projectDirectory, name)),
|
||||
projectDirectory,
|
||||
];
|
||||
const seen = new Set<string>();
|
||||
const candidates: RecoveryCandidate[] = [];
|
||||
for (const root of roots) {
|
||||
if (!existsSync(root)) continue;
|
||||
let names: string[] = [];
|
||||
try {
|
||||
names = readdirSync(root);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const name of names) {
|
||||
if (candidates.length >= MAX_CANDIDATES) break;
|
||||
if (extname(name).toLowerCase() !== ".prproj") continue;
|
||||
if (!name.toLowerCase().includes(projectStem)) continue;
|
||||
const candidatePath = resolve(root, name);
|
||||
if (candidatePath === absoluteProjectPath || seen.has(candidatePath)) continue;
|
||||
seen.add(candidatePath);
|
||||
try {
|
||||
const stat = statSync(candidatePath);
|
||||
if (!stat.isFile()) continue;
|
||||
candidates.push({
|
||||
path: candidatePath,
|
||||
fileName: basename(candidatePath),
|
||||
modifiedAt: stat.mtime.toISOString(),
|
||||
modifiedMs: stat.mtimeMs,
|
||||
sizeBytes: stat.size,
|
||||
newerThanProjectFile:
|
||||
projectModifiedMs === null ? null : stat.mtimeMs > projectModifiedMs,
|
||||
});
|
||||
} catch {
|
||||
// A candidate can disappear during an autosave; omit unstable entries.
|
||||
}
|
||||
}
|
||||
}
|
||||
return candidates.sort((left, right) => right.modifiedMs - left.modifiedMs);
|
||||
}
|
||||
|
||||
export function collectBridgeTelemetry(
|
||||
bridgeOptions: BridgeOptions,
|
||||
nowMs = Date.now(),
|
||||
) {
|
||||
const directory = getTempDir(bridgeOptions);
|
||||
const heartbeat = getBridgeLiveness(bridgeOptions, nowMs);
|
||||
const counts = { pendingCommands: 0, pendingResponses: 0, busyOperations: 0 };
|
||||
let oldestPendingAgeMs: number | null = null;
|
||||
let directoryAccessible = false;
|
||||
try {
|
||||
if (existsSync(directory)) {
|
||||
directoryAccessible = true;
|
||||
for (const name of readdirSync(directory)) {
|
||||
let bucket: keyof typeof counts | null = null;
|
||||
if (/^cmd_.+\.jsx$/.test(name)) bucket = "pendingCommands";
|
||||
else if (/^res_.+\.json$/.test(name)) bucket = "pendingResponses";
|
||||
else if (/^busy_.+\.json$/.test(name)) bucket = "busyOperations";
|
||||
if (!bucket) continue;
|
||||
counts[bucket]++;
|
||||
try {
|
||||
const age = Math.max(0, nowMs - statSync(join(directory, name)).mtimeMs);
|
||||
oldestPendingAgeMs =
|
||||
oldestPendingAgeMs === null ? age : Math.max(oldestPendingAgeMs, age);
|
||||
} catch {
|
||||
// Aggregate telemetry remains useful if one transient file disappears.
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
directoryAccessible = false;
|
||||
}
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
directoryAccessible,
|
||||
...counts,
|
||||
oldestPendingAgeMs,
|
||||
heartbeat,
|
||||
healthy:
|
||||
directoryAccessible &&
|
||||
heartbeat.state === "running" &&
|
||||
counts.busyOperations === 0 &&
|
||||
(oldestPendingAgeMs === null || oldestPendingAgeMs < 30_000),
|
||||
privacy:
|
||||
"Aggregate local bridge state only; no paths, filenames, script contents, project names, or media names are returned.",
|
||||
};
|
||||
}
|
||||
|
||||
function projectSnapshot(result: CommandResult): ProjectSnapshot | null {
|
||||
if (!result.success || !result.data || typeof result.data !== "object") return null;
|
||||
const value = result.data as Record<string, unknown>;
|
||||
if (typeof value.name !== "string" || typeof value.path !== "string") return null;
|
||||
return { name: value.name, path: value.path };
|
||||
}
|
||||
|
||||
export function getRecoveryTools(bridgeOptions: BridgeOptions) {
|
||||
return {
|
||||
create_project_backup: {
|
||||
description:
|
||||
"Create a collision-safe, byte-verified backup beside an existing .prproj file without opening or modifying the source project.",
|
||||
parameters: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
project_path: {
|
||||
type: "string",
|
||||
description: "Absolute or working-directory-relative path to an existing .prproj file",
|
||||
},
|
||||
},
|
||||
required: ["project_path"],
|
||||
},
|
||||
handler: async (args: { project_path: string }) => {
|
||||
try {
|
||||
return { success: true, data: createProjectBackup(args.project_path) };
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
inspect_project_recovery: {
|
||||
description:
|
||||
"Read-only recovery inspection: diagnose the active project path and list adjacent Premiere Auto-Save project candidates without opening, copying, or restoring anything.",
|
||||
parameters: {},
|
||||
handler: async () => {
|
||||
const result = await sendCommand(
|
||||
buildToolScript(`
|
||||
if (!app.project) return __error("No project is open");
|
||||
return __result({
|
||||
name: app.project.name || "",
|
||||
path: app.project.path || ""
|
||||
});
|
||||
`),
|
||||
bridgeOptions,
|
||||
);
|
||||
if (!result.success) return result;
|
||||
const project = projectSnapshot(result);
|
||||
if (!project) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Premiere returned an invalid project snapshot",
|
||||
};
|
||||
}
|
||||
const hasSavedPath =
|
||||
project.path.length > 0 &&
|
||||
extname(project.path).toLowerCase() === ".prproj";
|
||||
const candidates = hasSavedPath
|
||||
? discoverAdjacentRecoveryCandidates(project.path)
|
||||
: [];
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
project: {
|
||||
name: project.name,
|
||||
path: project.path || null,
|
||||
hasSavedPath,
|
||||
fileExists: hasSavedPath ? existsSync(project.path) : false,
|
||||
},
|
||||
unsavedChanges: {
|
||||
status: "not_exposed",
|
||||
dirty: null,
|
||||
reason:
|
||||
"Premiere's documented scripting APIs do not expose the current dirty/unsaved flag.",
|
||||
},
|
||||
recovery: {
|
||||
mode: "read_only_discovery",
|
||||
candidateCount: candidates.length,
|
||||
truncated: candidates.length >= MAX_CANDIDATES,
|
||||
candidates,
|
||||
automaticRestoreSupported: false,
|
||||
guidance:
|
||||
candidates.length > 0
|
||||
? "Review modification times and file sizes, then use Premiere Pro's File > Open to inspect a chosen copy. Keep the current project open until you have confirmed the recovery."
|
||||
: hasSavedPath
|
||||
? "No adjacent .prproj recovery candidates were found. Check Premiere Pro's configured Auto Save location and Creative Cloud recovery UI manually."
|
||||
: "Save the project to establish a project directory; then inspect Premiere's Auto Save location manually.",
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
get_bridge_telemetry: {
|
||||
description:
|
||||
"Inspect privacy-preserving aggregate bridge health: pending command/response counts, busy operations, queue age, and CEP heartbeat state without returning project or personal data.",
|
||||
parameters: {},
|
||||
handler: async () => ({
|
||||
success: true,
|
||||
data: collectBridgeTelemetry(bridgeOptions),
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user