qraft
Version:
A powerful CLI tool to qraft structured project setups from GitHub template repositories
326 lines • 14.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.RepositoryManager = void 0;
const rest_1 = require("@octokit/rest");
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const boxRegistryManager_1 = require("./boxRegistryManager");
const manifestManager_1 = require("./manifestManager");
const permissionChecker_1 = require("./permissionChecker");
const pullRequestCreator_1 = require("./pullRequestCreator");
const repositoryForker_1 = require("./repositoryForker");
class RepositoryManager {
constructor(githubToken) {
this.githubToken = githubToken;
this.permissionChecker = new permissionChecker_1.PermissionChecker(githubToken);
this.repositoryForker = new repositoryForker_1.RepositoryForker(githubToken);
this.pullRequestCreator = new pullRequestCreator_1.PullRequestCreator(githubToken);
this.manifestManager = new manifestManager_1.ManifestManager();
this.boxRegistryManager = new boxRegistryManager_1.BoxRegistryManager();
}
/**
* Create a new box in a GitHub repository
*/
async createBox(owner, repo, boxName, localPath, manifest, remotePath, options = {}) {
try {
if (!this.githubToken) {
throw new Error('GitHub token required for creating boxes');
}
// Check permissions
const permissions = await this.permissionChecker.checkRepositoryPermissions(owner, repo);
if (!permissions.permissions.canWrite) {
// User doesn't have write access, fork the repository
const forkResult = await this.repositoryForker.forkRepository(owner, repo);
if (!forkResult.success) {
return {
success: false,
message: `Cannot create box: ${forkResult.message}`,
boxPath: '',
nextSteps: forkResult.nextSteps
};
}
// Use the fork for creating the box
return this.createBoxInRepository(forkResult.forkOwner, forkResult.forkName, boxName, localPath, manifest, { ...options, createPR: true }, remotePath);
}
// User has write access, create directly
return this.createBoxInRepository(owner, repo, boxName, localPath, manifest, options, remotePath);
}
catch (error) {
return {
success: false,
message: `Failed to create box: ${error instanceof Error ? error.message : 'Unknown error'}`,
boxPath: '',
nextSteps: [
'Check your GitHub token permissions',
'Verify the repository exists and you have access',
'Try again or contact the repository owner'
]
};
}
}
/**
* Create box in a specific repository (with write access)
*/
async createBoxInRepository(owner, repo, boxName, localPath, manifest, options = {}, remotePath) {
const octokit = new rest_1.Octokit({
auth: this.githubToken,
userAgent: 'qraft-cli'
});
// Use remotePath if provided, otherwise fall back to boxName
const boxPath = remotePath || boxName;
// Update manifest with remotePath if provided
if (remotePath) {
manifest.remotePath = remotePath;
}
try {
// Get the default branch if no branch specified
let branch = options.branch;
if (!branch) {
const { data: repoData } = await octokit.rest.repos.get({
owner,
repo
});
branch = repoData.default_branch;
}
// Get the current commit SHA of the branch
const { data: refData } = await octokit.rest.git.getRef({
owner,
repo,
ref: `heads/${branch}`
});
const baseSha = refData.object.sha;
// Get the base tree
const { data: baseCommit } = await octokit.rest.git.getCommit({
owner,
repo,
commit_sha: baseSha
});
// Collect all files to upload
const filesToUpload = await this.collectFiles(localPath, boxPath);
// Add manifest.json
filesToUpload.push({
path: `${boxPath}/manifest.json`,
content: JSON.stringify(manifest, null, 2),
encoding: 'utf-8'
});
// Create tree with all files
const tree = filesToUpload.map(file => ({
path: file.path,
mode: '100644',
type: 'blob',
content: typeof file.content === 'string' ? file.content : file.content.toString('base64')
}));
const { data: newTree } = await octokit.rest.git.createTree({
owner,
repo,
tree,
base_tree: baseCommit.tree.sha
});
// Create commit
const commitMessage = options.commitMessage || `feat: add ${boxName} box`;
const { data: newCommit } = await octokit.rest.git.createCommit({
owner,
repo,
message: commitMessage,
tree: newTree.sha,
parents: [baseSha]
});
// Update the branch reference
await octokit.rest.git.updateRef({
owner,
repo,
ref: `heads/${branch}`,
sha: newCommit.sha
});
// Store local manifest copy after successful creation
try {
await this.storeLocalManifestCopy(localPath, manifest, `${owner}/${repo}`, boxName);
}
catch (manifestError) {
// Log manifest storage error but don't fail the entire operation
console.warn(`Warning: Failed to store local manifest copy: ${manifestError instanceof Error ? manifestError.message : 'Unknown error'}`);
}
// Update box registry with name → remote path mapping
try {
// For now, we'll use a local registry path. In a real implementation,
// this would be the cloned registry directory
const registryPath = process.cwd(); // Temporary - should be actual registry path
await this.boxRegistryManager.registerBox(registryPath, `${owner}/${repo}`, boxName, boxPath, manifest);
}
catch (registryError) {
// Log registry update error but don't fail the entire operation
console.warn(`Warning: Failed to update box registry: ${registryError instanceof Error ? registryError.message : 'Unknown error'}`);
}
const result = {
success: true,
message: `Box '${boxName}' created successfully`,
boxPath,
commitSha: newCommit.sha,
nextSteps: [
`Box '${boxName}' is now available in the registry`,
`You can download it using: qraft copy ${boxName}`,
'Share the box with others by sharing the registry'
]
};
// Create PR if requested (for forks)
if (options.createPR) {
const boxMetadata = {
name: manifest.name,
description: manifest.description,
tags: manifest.tags || [],
fileCount: filesToUpload.length - 1, // Exclude manifest.json
size: this.calculateTotalSize(filesToUpload)
};
const prOptions = {
headBranch: branch
};
if (options.prTitle) {
prOptions.title = options.prTitle;
}
if (options.prDescription) {
prOptions.description = options.prDescription;
}
const prResult = await this.pullRequestCreator.createPullRequest(owner, repo, boxMetadata, prOptions);
if (prResult.success) {
result.prUrl = prResult.prUrl;
result.prNumber = prResult.prNumber;
result.nextSteps = [
`Pull request created: ${prResult.prUrl}`,
'Wait for review and approval',
'Box will be available after PR is merged'
];
}
}
return result;
}
catch (error) {
return {
success: false,
message: `Failed to create box in repository: ${error instanceof Error ? error.message : 'Unknown error'}`,
boxPath,
nextSteps: [
'Check your GitHub token permissions',
'Verify the repository exists and you have write access',
'Ensure the branch exists',
'Try again or contact the repository owner'
]
};
}
}
/**
* Collect all files from local directory
*/
async collectFiles(localPath, boxPath) {
const files = [];
const scanDirectory = async (dirPath, relativePath = '') => {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
const relativeFilePath = path.join(relativePath, entry.name).replace(/\\/g, '/');
if (entry.isDirectory()) {
await scanDirectory(fullPath, relativeFilePath);
}
else if (entry.isFile()) {
const content = await fs.readFile(fullPath);
const isText = this.isTextFile(fullPath);
files.push({
path: `${boxPath}/${relativeFilePath}`,
content: isText ? content.toString('utf-8') : content,
encoding: isText ? 'utf-8' : 'base64'
});
}
}
};
await scanDirectory(localPath);
return files;
}
/**
* Check if a file is a text file
*/
isTextFile(filePath) {
const textExtensions = [
'.txt', '.md', '.json', '.js', '.ts', '.jsx', '.tsx', '.css', '.scss', '.sass',
'.html', '.htm', '.xml', '.yaml', '.yml', '.toml', '.ini', '.cfg', '.conf',
'.py', '.rb', '.php', '.java', '.c', '.cpp', '.h', '.hpp', '.cs', '.go',
'.rs', '.sh', '.bash', '.zsh', '.fish', '.ps1', '.bat', '.cmd', '.dockerfile',
'.gitignore', '.gitattributes', '.editorconfig', '.eslintrc', '.prettierrc'
];
const ext = path.extname(filePath).toLowerCase();
return textExtensions.includes(ext) || path.basename(filePath).startsWith('.');
}
/**
* Calculate total size of files
*/
calculateTotalSize(files) {
const totalBytes = files.reduce((sum, file) => {
const size = typeof file.content === 'string'
? Buffer.byteLength(file.content, 'utf-8')
: file.content.length;
return sum + size;
}, 0);
if (totalBytes < 1024)
return `${totalBytes}B`;
if (totalBytes < 1024 * 1024)
return `${(totalBytes / 1024).toFixed(1)}KB`;
return `${(totalBytes / (1024 * 1024)).toFixed(1)}MB`;
}
/**
* Store local manifest copy after box creation
* @param localPath Local path where the box was created from
* @param manifest Box manifest
* @param registry Registry identifier (owner/repo)
* @param boxName Box name
* @returns Promise<void>
*/
async storeLocalManifestCopy(localPath, manifest, registry, boxName) {
try {
// Store the manifest with source information
await this.manifestManager.storeLocalManifest(localPath, manifest, registry, `${registry}/${boxName}`);
}
catch (error) {
throw new Error(`Failed to store local manifest copy: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Get manifest manager instance
* @returns ManifestManager Manifest manager instance
*/
getManifestManager() {
return this.manifestManager;
}
}
exports.RepositoryManager = RepositoryManager;
//# sourceMappingURL=repositoryManager.js.map