UNPKG

@supernovaio/cli

Version:

Supernova.io Command Line Interface

219 lines (214 loc) 9.45 kB
!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