Makelore 2.0 initial clean snapshot

This commit is contained in:
inman
2026-07-29 17:22:35 +08:00
commit b8ca3f8eea
694 changed files with 139782 additions and 0 deletions

View File

@@ -0,0 +1,262 @@
import { constants, type Stats } from 'node:fs';
import {
copyFile,
lstat,
mkdir,
mkdtemp,
readdir,
rm,
rmdir,
stat,
} from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, sep } from 'node:path';
import type { ProjectConfig, ProjectTemplateId } from '../../shared/project-config';
import { createInitialProjectConfig, readProjectConfig } from './project-config';
import { initializeWorksPackageTemplate } from './project-package-template';
type StagedProjectEntries = {
directories: string[];
files: string[];
};
export type ProjectDirectoryInitializationResult = {
config: ProjectConfig;
reusedExistingConfig: boolean;
};
export type ProjectDirectoryInitializationInput = {
projectPath: string;
templateId: ProjectTemplateId;
defaultModel?: string | null;
allowExistingDirectory: boolean;
};
export type ProjectDirectoryInitializationDependencies = {
copyFile: typeof copyFile;
};
const defaultDependencies: ProjectDirectoryInitializationDependencies = {
copyFile,
};
function isErrnoCode(error: unknown, code: string): boolean {
return (error as NodeJS.ErrnoException | undefined)?.code === code;
}
function displayPath(relativePath: string): string {
return relativePath.split(sep).join('/');
}
function compareByDepth(left: string, right: string): number {
const depthDifference = left.split(sep).length - right.split(sep).length;
return depthDifference || left.localeCompare(right);
}
async function collectStagedEntries(
stagingPath: string,
relativeDirectory = '',
): Promise<StagedProjectEntries> {
const directories: string[] = [];
const files: string[] = [];
const entries = await readdir(join(stagingPath, relativeDirectory), { withFileTypes: true });
for (const entry of entries) {
const relativePath = join(relativeDirectory, entry.name);
if (entry.isDirectory()) {
directories.push(relativePath);
const nested = await collectStagedEntries(stagingPath, relativePath);
directories.push(...nested.directories);
files.push(...nested.files);
continue;
}
if (entry.isFile()) {
files.push(relativePath);
continue;
}
throw new Error(`Unsupported staged project entry: ${displayPath(relativePath)}`);
}
return {
directories: directories.sort(compareByDepth),
files: files.sort((left, right) => left.localeCompare(right)),
};
}
async function readEntry(path: string) {
try {
return await lstat(path);
} catch (error) {
if (isErrnoCode(error, 'ENOENT')) return null;
throw error;
}
}
function conflictError(relativePath: string, detail = 'already exists'): Error {
return new Error(
`Project initialization conflict: ${displayPath(relativePath)} ${detail}. The selected folder was not changed.`,
);
}
async function rollbackCreatedEntries(
createdFiles: string[],
createdDirectories: string[],
): Promise<string[]> {
const rollbackErrors: string[] = [];
for (const filePath of [...createdFiles].reverse()) {
try {
await rm(filePath, { force: true });
} catch (error) {
rollbackErrors.push(`${filePath}: ${error instanceof Error ? error.message : String(error)}`);
}
}
for (const directoryPath of [...createdDirectories].reverse()) {
try {
await rmdir(directoryPath);
} catch (error) {
if (isErrnoCode(error, 'ENOENT') || isErrnoCode(error, 'ENOTEMPTY') || isErrnoCode(error, 'EEXIST')) {
continue;
}
rollbackErrors.push(`${directoryPath}: ${error instanceof Error ? error.message : String(error)}`);
}
}
return rollbackErrors;
}
async function mergeStagedProject(
stagingPath: string,
projectPath: string,
dependencies: ProjectDirectoryInitializationDependencies,
): Promise<void> {
const staged = await collectStagedEntries(stagingPath);
const missingDirectories: string[] = [];
for (const relativePath of staged.directories) {
const destination = join(projectPath, relativePath);
const existing = await readEntry(destination);
if (!existing) {
missingDirectories.push(relativePath);
continue;
}
if (!existing.isDirectory()) {
throw conflictError(relativePath, 'already exists and is not a directory');
}
}
for (const relativePath of staged.files) {
if (await readEntry(join(projectPath, relativePath))) {
throw conflictError(relativePath);
}
}
const createdDirectories: string[] = [];
const createdFiles: string[] = [];
try {
for (const relativePath of missingDirectories) {
const destination = join(projectPath, relativePath);
try {
await mkdir(destination);
createdDirectories.push(destination);
} catch (error) {
if (isErrnoCode(error, 'EEXIST') && (await readEntry(destination))?.isDirectory()) {
continue;
}
throw error;
}
}
for (const relativePath of staged.files) {
const destination = join(projectPath, relativePath);
try {
await dependencies.copyFile(join(stagingPath, relativePath), destination, constants.COPYFILE_EXCL);
} catch (error) {
if (isErrnoCode(error, 'EEXIST')) throw conflictError(relativePath);
throw error;
}
createdFiles.push(destination);
}
} catch (error) {
const rollbackErrors = await rollbackCreatedEntries(createdFiles, createdDirectories);
if (rollbackErrors.length > 0) {
throw new Error(
`Project initialization failed and rollback was incomplete: ${rollbackErrors.join('; ')}`,
{ cause: error },
);
}
throw error;
}
}
async function requireExistingDirectory(projectPath: string): Promise<void> {
let projectStats: Stats;
try {
projectStats = await stat(projectPath);
} catch (error) {
if (isErrnoCode(error, 'ENOENT')) {
throw new Error('The selected project folder no longer exists', { cause: error });
}
throw error;
}
if (!projectStats.isDirectory()) {
throw new Error('The selected project path is not a directory');
}
}
export async function initializeProjectDirectory(
input: ProjectDirectoryInitializationInput,
dependencies: ProjectDirectoryInitializationDependencies = defaultDependencies,
): Promise<ProjectDirectoryInitializationResult> {
let createdProjectDirectory = false;
if (input.allowExistingDirectory) {
await requireExistingDirectory(input.projectPath);
const existing = await readProjectConfig(input.projectPath);
if (existing.status === 'valid') {
return { config: existing.config, reusedExistingConfig: true };
}
if (existing.status === 'invalid') {
throw new Error(`Existing project configuration is invalid: ${existing.error}`);
}
} else {
await mkdir(input.projectPath);
createdProjectDirectory = true;
}
let stagingPath: string | null = null;
try {
stagingPath = await mkdtemp(join(tmpdir(), 'niancode-project-init-'));
try {
await initializeWorksPackageTemplate(stagingPath, input.templateId);
} catch (error) {
throw new Error(
`Project package template initialization failed: ${error instanceof Error ? error.message : String(error)}`,
{ cause: error },
);
}
let config: ProjectConfig;
try {
config = await createInitialProjectConfig(stagingPath, input.templateId, {
defaultModel: input.defaultModel,
});
} catch (error) {
throw new Error(
`Project configuration initialization failed: ${error instanceof Error ? error.message : String(error)}`,
{ cause: error },
);
}
await mergeStagedProject(stagingPath, input.projectPath, dependencies);
return { config, reusedExistingConfig: false };
} catch (error) {
if (createdProjectDirectory) {
await rm(input.projectPath, { recursive: true, force: true });
}
throw error;
} finally {
if (stagingPath) {
await rm(stagingPath, { recursive: true, force: true });
}
}
}