@supernovaio/cli
Version:
Supernova.io Command Line Interface
277 lines (275 loc) • 14 kB
JavaScript
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="0de7f429-2e03-5634-bb1f-8a633e7a9c38")}catch(e){}}();
import { execFile as execFileCallback } from "node:child_process";
import fs from "node:fs";
import fsPromises from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import inquirer from "inquirer";
import { getApiClient as getDefaultApiClient } from "../../api-client.js";
import { fileExists } from "../../validate-templates.js";
import { TEMP_NPMRC_MARKER } from "../constants.js";
import { containerRepositoryUrlForFramework, findNextAvailablePatchVersion } from "../helpers.js";
import { promptConfirmation } from "../prompts.js";
import { colorize, muted, success, warning } from "../ui.js";
const execFile = promisify(execFileCallback);
export function createTemplateService(input) {
const getApiClient = input.getApiClient ?? getDefaultApiClient;
return {
async detectDockerAvailabilityIssue() {
try {
await execFile("docker", ["info"]);
return undefined;
}
catch (caughtError) {
if (caughtError?.code === "ENOENT") {
return {
kind: "missing",
message: "Docker CLI was not found on this system. Install Docker and make sure `docker` is available in PATH before uploading.",
};
}
return {
kind: "not_running",
message: "Docker is installed, but does not appear to be running. Start Docker Desktop or your Docker daemon.",
};
}
},
async ensureTemplateRepository(framework, destinationPath) {
const repositoryUrl = containerRepositoryUrlForFramework(framework);
const resolvedDestination = path.resolve(destinationPath);
const destinationExists = fs.existsSync(resolvedDestination);
if (!destinationExists) {
input.log(`${muted("Cloning")} ${repositoryUrl} ${muted("->")} ${resolvedDestination}`);
await cloneRepository(repositoryUrl, resolvedDestination, input.error);
return;
}
const entries = fs.readdirSync(resolvedDestination);
if (entries.length === 0) {
input.log(`${muted("Cloning")} ${repositoryUrl} ${muted("into")} ${resolvedDestination}`);
await cloneRepositoryIntoCurrentDirectory(repositoryUrl, resolvedDestination, input.error);
return;
}
const replaceExisting = await promptConfirmation(`Destination ${resolvedDestination} is non-empty. Replace its contents with the ${framework} container starter?`, {
defaultValue: false,
});
if (!replaceExisting) {
input.log(`${warning("Keeping existing files.")} Clone manually if needed: ${repositoryUrl}`);
return;
}
input.log(`${muted("Removing existing contents from")} ${resolvedDestination}`);
for (const entry of entries) {
await fsPromises.rm(path.join(resolvedDestination, entry), { force: true, recursive: true });
}
input.log(`${muted("Cloning")} ${repositoryUrl} ${muted("into")} ${resolvedDestination}`);
await cloneRepositoryIntoCurrentDirectory(repositoryUrl, resolvedDestination, input.error);
},
async resolveTemplateUploadPath(defaultPath) {
const candidateFromContext = path.resolve(defaultPath);
const normalizedFromContext = normalizeTemplatePath(candidateFromContext);
if (normalizedFromContext && hasTemplatePackageJson(normalizedFromContext)) {
return normalizedFromContext;
}
input.log(warning(`package.json not found in ${candidateFromContext}`));
input.log(muted("Checked:"));
input.log(muted(`- ${path.join(candidateFromContext, "package.json")}`));
input.log(muted(`- ${path.join(candidateFromContext, "*/package.json")} (single nested auto-detect)`));
input.log(muted("Select the folder that contains your prepared container."));
while (true) {
const response = await inquirer.prompt([
{
default: candidateFromContext,
message: colorize("Path to container root (folder with package.json):", "blue"),
name: "templateUploadPath",
type: "input",
},
]);
const candidate = path.resolve(response.templateUploadPath.trim());
if (!fs.existsSync(candidate)) {
input.log(warning(`Path does not exist: ${candidate}`));
if (!(await promptRetryTemplatePath()))
input.error("Upload cancelled.");
continue;
}
const normalizedCandidate = normalizeTemplatePath(candidate);
if (!normalizedCandidate) {
input.log(warning(`Path is neither a directory nor a package.json file: ${candidate}`));
if (!(await promptRetryTemplatePath()))
input.error("Upload cancelled.");
continue;
}
if (hasTemplatePackageJson(normalizedCandidate)) {
return normalizedCandidate;
}
const discoveredNestedPath = findSingleNestedPackageJsonDirectory(normalizedCandidate);
if (discoveredNestedPath) {
input.log(success(`Found nested container package.json: ${discoveredNestedPath}`));
return discoveredNestedPath;
}
input.log(warning(`package.json not found in ${normalizedCandidate}`));
if (!(await promptRetryTemplatePath()))
input.error("Upload cancelled.");
}
},
async ensureTemplateVersionIsUnique(versionInput) {
const pkg = await readTemplatePackageMeta(versionInput.templateDestinationPath);
if (!pkg)
return;
const templates = await listSandboxTemplates(getApiClient, input.env, versionInput.workspaceId, versionInput.designSystemId);
const conflictingTemplate = templates.find(template => template.name === pkg.name);
const existingVersions = new Set((conflictingTemplate?.versions ?? []).map(version => version.name));
if (!existingVersions.has(pkg.version))
return;
input.log(warning(`Template version conflict detected: ${pkg.name}@${pkg.version} already exists in this design system.`));
const response = await inquirer.prompt([
{
choices: [
{ name: "Bump patch version in package.json and continue", value: "bump_patch" },
{ name: "Continue anyway", value: "continue" },
{ name: "Cancel upload", value: "cancel" },
],
default: "bump_patch",
message: colorize("Choose how to resolve the version conflict:", "yellow"),
name: "action",
type: "list",
},
]);
if (response.action === "cancel") {
input.error("Upload cancelled due to template version conflict.");
}
if (response.action === "continue") {
return;
}
const nextVersion = findNextAvailablePatchVersion(pkg.version, existingVersions);
if (!nextVersion) {
input.error(`Cannot auto-bump non-semver version "${pkg.version}". Update package.json manually and retry.`);
}
await updateTemplatePackageVersion(versionInput.templateDestinationPath, nextVersion);
input.log(success(`Updated package.json version: ${pkg.version} -> ${nextVersion}`));
},
async runTemplateUploadWithCleanup(uploadInput) {
const markerPath = path.join(uploadInput.templateDestinationPath, TEMP_NPMRC_MARKER);
const npmrcPath = path.join(uploadInput.templateDestinationPath, ".npmrc");
const packageMeta = await readTemplatePackageMeta(uploadInput.templateDestinationPath);
if (!packageMeta) {
input.error("Unable to resolve uploaded container template: package.json name or version is missing.");
}
try {
await runTemplateUpload(input.runTemplateUploadCommand, {
designSystemId: uploadInput.designSystemId,
npmToken: uploadInput.npmToken,
templateDestinationPath: uploadInput.templateDestinationPath,
workspaceId: uploadInput.workspaceId,
});
const uploadedTemplate = await resolveUploadedSandboxTemplate(getApiClient, input.env, {
designSystemId: uploadInput.designSystemId,
packageName: packageMeta.name,
packageVersion: packageMeta.version,
workspaceId: uploadInput.workspaceId,
});
if (!uploadedTemplate) {
input.error(`Unable to resolve uploaded container template ${packageMeta.name}@${packageMeta.version} in Supernova.`);
}
return uploadedTemplate;
}
finally {
if (await fileExists(markerPath)) {
await fsPromises.rm(markerPath, { force: true });
await fsPromises.rm(npmrcPath, { force: true });
}
}
},
};
}
async function runTemplateUpload(runTemplateUploadCommand, input) {
const args = ["--workspaceId", input.workspaceId, "--designSystemId", input.designSystemId];
if (input.npmToken) {
args.push("--npmToken", input.npmToken);
}
await runTemplateUploadCommand(input.templateDestinationPath, args);
}
async function cloneRepository(repositoryUrl, destinationPath, error) {
try {
await execFile("git", ["clone", repositoryUrl, destinationPath]);
}
catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
error(`Container clone failed: ${message}`);
}
}
async function cloneRepositoryIntoCurrentDirectory(repositoryUrl, cwd, error) {
try {
await execFile("git", ["clone", repositoryUrl, "."], { cwd });
}
catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
error(`Container clone failed: ${message}`);
}
}
function hasTemplatePackageJson(templatePath) {
return fs.existsSync(path.join(templatePath, "package.json"));
}
function normalizeTemplatePath(inputPath) {
if (!fs.existsSync(inputPath))
return null;
const stat = fs.statSync(inputPath);
if (stat.isDirectory())
return inputPath;
if (stat.isFile() && path.basename(inputPath) === "package.json")
return path.dirname(inputPath);
return null;
}
function findSingleNestedPackageJsonDirectory(rootPath) {
const childEntries = fs.readdirSync(rootPath, { withFileTypes: true });
const dirsWithPackageJson = childEntries
.filter(entry => entry.isDirectory())
.map(entry => path.join(rootPath, entry.name))
.filter(dirPath => hasTemplatePackageJson(dirPath));
return dirsWithPackageJson.length === 1 ? dirsWithPackageJson[0] : null;
}
async function promptRetryTemplatePath() {
return promptConfirmation("Try a different container path?");
}
async function readTemplatePackageMeta(templateDestinationPath) {
const packageJsonPath = path.join(templateDestinationPath, "package.json");
if (!fs.existsSync(packageJsonPath))
return null;
try {
const raw = await fsPromises.readFile(packageJsonPath, "utf8");
const parsed = JSON.parse(raw);
if (!parsed.name || !parsed.version)
return null;
return { name: parsed.name, version: parsed.version };
}
catch {
return null;
}
}
async function updateTemplatePackageVersion(templateDestinationPath, nextVersion) {
const packageJsonPath = path.join(templateDestinationPath, "package.json");
const raw = await fsPromises.readFile(packageJsonPath, "utf8");
const parsed = JSON.parse(raw);
parsed.version = nextVersion;
await fsPromises.writeFile(packageJsonPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
}
async function listSandboxTemplates(getApiClient, env, workspaceId, designSystemId) {
const client = await getApiClient(env);
const { templates } = await client.sandboxes.templates.list({
designSystemId,
workspaceId,
});
return templates;
}
async function resolveUploadedSandboxTemplate(getApiClient, env, input) {
const templates = await listSandboxTemplates(getApiClient, env, input.workspaceId, input.designSystemId);
const uploadedTemplate = templates.find(template => template.name === input.packageName);
if (!uploadedTemplate?.id)
return undefined;
const uploadedVersion = uploadedTemplate.versions?.find(version => version.name === input.packageVersion);
if (!uploadedVersion?.name)
return undefined;
return {
id: uploadedTemplate.id,
version: uploadedVersion.name,
};
}
//# sourceMappingURL=template-service.js.map
//# debugId=0de7f429-2e03-5634-bb1f-8a633e7a9c38