@supernovaio/cli
Version:
Supernova.io Command Line Interface
219 lines (214 loc) • 9.45 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]="4f173dbe-1293-53ae-9922-498c47375582")}catch(e){}}();
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import fetch from "node-fetch";
import { Flags } from "@oclif/core";
import { action } from "@oclif/core/ux";
import { SentryTraced } from "@sentry/nestjs";
import { exec as execCallback } from "node:child_process";
import * as fs from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import { z } from "zod";
import fsx from "fs-extra";
import { commonFlags, SentryCommand } from "../types/index.js";
import { sleep } from "../utils/common.js";
import { tmpdir } from "node:os";
const exec = promisify(execCallback);
const TemplateUploadConfig = z.object({});
async function fileExists(p) {
try {
await fs.access(p);
return true;
}
catch {
return false;
}
}
const dockerfile = `
FROM node:22-slim
RUN apt-get update && apt-get install -y curl && apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /home/user
COPY . .
RUN npm i && rm -f .npmrc
RUN node docker-scripts/extract-private-packages.js
`;
export default class TemplateUpload extends SentryCommand {
static args = {};
static description = "Upload custom prototype app sandbox template";
static examples = ["<%= config.bin %> <%= command.id %> TemplateUpload "];
static hidden = true;
static flags = {
...commonFlags,
workspaceId: Flags.string({ char: "w", description: "Workspace ID to upload the template to", required: true }),
designSystemId: Flags.string({
char: "d",
description: "Design system ID to upload the template to",
required: true,
}),
force: Flags.boolean({
char: "f",
description: "Allows overwriting already published version of this template if it exists. This flag has no effect on new versions.",
required: false,
}),
};
get commandId() {
return TemplateUpload.id;
}
get configSchema() {
return TemplateUploadConfig;
}
async run() {
const apiClient = await this.apiClient();
const { flags } = await this.parse();
let pkg;
try {
pkg = await readPackageJson();
}
catch (error) {
if (error instanceof Error)
this.error(`Failed to read or parse package.json: ${error.message}`);
else
throw error;
}
if (pkg.supernova?.privateDependencies) {
this.log(`Following packages will be linked as private dependencies: ${pkg.supernova.privateDependencies}`);
if (!(await fileExists(path.join(process.cwd(), ".npmrc")))) {
this.error(`CLI needs private NPM registry access to be able to bundle private dependencies.\n` +
`Please provide .npmrc file in the root directory and include neccessary access tokens.`);
}
}
else {
this.warn(`package.json doesn't contain 'supernova.privateDependencies' declaration.`);
this.warn(`Dependencies coming from private registries will fail`);
}
const buildData = await apiClient.sandboxes.builds.start({
workspaceId: flags.workspaceId,
designSystemId: flags.designSystemId,
name: pkg.name,
version: pkg.version,
isExistingVersionUpdateAllowed: flags.force ?? false,
});
const url = imageUrl(buildData);
await this.validateDockerDaemon();
const buildDir = await this.createBuildDir();
try {
await this.prepareBuildFolder(buildDir);
await this.buildDockerImage(buildDir, url);
await this.pushDockerImage(buildDir, buildData.dockerRegistryDomain, url);
await this.remoteTemplateBuild(apiClient, buildData.build.id);
this.log(`✅ Template has been successfully uploaded`);
}
finally {
await this.deleteBuildDir(buildDir);
}
}
async validateDockerDaemon() {
await exec("docker info").catch(() => {
this.error(`Docker is not available, please start docker daemon and try again`);
});
}
createBuildDir() {
return fs.mkdtemp(path.join(tmpdir(), "supernova-template-bundle-"));
}
async deleteBuildDir(buildDir) {
await fs.rm(buildDir, { recursive: true, force: true });
}
async prepareBuildFolder(buildDir) {
await fsx.copy(process.cwd(), buildDir, {
filter(src) {
return !src.includes("node_modules/") && !src.includes(".git/") && !src.includes(".out/");
},
});
const cliSrcPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
await fsx.copy(path.join(cliSrcPath, "docker-scripts"), path.join(buildDir, "docker-scripts"));
}
async buildDockerImage(buildDir, imageUrl) {
action.start("🔨 Building docker image");
const process = exec(`docker build --progress=plain -t ${imageUrl} --pull --platform linux/amd64 -f - .`, {
cwd: buildDir,
});
process.child.stdin.write(dockerfile);
process.child.stdin.end();
await process;
action.stop("done");
}
async pushDockerImage(buildDir, dockerHost, imageUrl) {
const response = await fetch(`https://${dockerHost}/v2/`);
if (response.status === 401) {
const { accessToken } = (await this.apiClient()).config;
const loginProcess = exec(`docker login ${dockerHost} -u cli --password-stdin`);
loginProcess.child.stdin.write(accessToken);
loginProcess.child.stdin.end();
await loginProcess;
}
action.start("⬆️ Uploading docker image to Supernova");
await exec(`docker push ${imageUrl}`, {
cwd: buildDir,
});
action.stop("done");
}
async remoteTemplateBuild(client, buildId) {
action.start("📦 Creating template with the image");
await client.sandboxes.builds.finalize(buildId);
const pollIntervalMs = 2000;
const timeoutMs = 5 * 60 * 1000;
const startTime = Date.now();
let build;
do {
await sleep(pollIntervalMs);
build = (await client.sandboxes.builds.get(buildId)).build;
} while (build.state === "Building" && Date.now() - startTime < timeoutMs);
if (build.state !== "Success") {
this.error(`Template creation failed`);
}
action.stop("done");
}
}
__decorate([
SentryTraced(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], TemplateUpload.prototype, "run", null);
async function readPackageJson() {
const pkgPath = path.join(process.cwd(), "package.json");
if (!(await fileExists(pkgPath)))
throw new Error(`package.json file was not found in the current directory`);
const raw = await fs.readFile(pkgPath, "utf8");
const pkg = JSON.parse(raw);
if (typeof pkg !== "object" || pkg === null)
throw new Error(`Error parsing package.json: not a json`);
if (typeof pkg.name !== "string")
throw new Error(`Error parsing package.json: 'name' must be defined`);
if (typeof pkg.version !== "string")
throw new Error(`Error parsing package.json: 'version' must be defined`);
if (typeof pkg.dependencies !== "object" || pkg.dependencies === null)
throw new Error(`Error parsing package.json: 'dependencies' must be defined`);
if (pkg.supernova?.privateDependencies) {
const privateDependencies = pkg.supernova?.privateDependencies;
if (!Array.isArray(privateDependencies))
throw new TypeError(`supernova.privateDependencies must be an array`);
for (const [i, d] of privateDependencies.entries()) {
if (typeof d !== "string") {
throw new TypeError(`supernova.privateDependencies[${i}] must be a string`);
}
if (!pkg.dependencies[d]) {
throw new Error(`Private dependency ${d} is not listed in 'dependencies'`);
}
}
}
return pkg;
}
function imageUrl(build) {
return `${build.dockerRegistryDomain}${build.build.dockerImagePath}`;
}
//# sourceMappingURL=template-upload.js.map
//# debugId=4f173dbe-1293-53ae-9922-498c47375582