UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

245 lines 9.68 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PlainProgress = void 0; const chalk = require("chalk"); const path = require("path"); const format_1 = require("../../../util/format"); const config_1 = require("../config"); const log_filter_1 = require("../log-filter"); // ANCHOR - PlainProgress /** * Renders build progress as append-only lines: milestones once, upload progress * throttled, service events by level, and the BuildKit log streamed line by line. * Once the log streams it becomes the live output — the service's routine narrative * events fall silent so they never splice into a BuildKit vertex, while warnings and * errors still surface. Safe for CI logs — nothing rewrites the screen. */ class PlainProgress { constructor(env) { var _a; this.logStreaming = false; this.lastUploadPrint = Number.NEGATIVE_INFINITY; // Negative infinity guarantees the first upload snapshot always prints. this.logRemainder = ''; this.now = (_a = env.now) !== null && _a !== void 0 ? _a : Date.now; this.sink = env.err; this.logFilter = new log_filter_1.RemoteBuildLogFilter(); this.palette = new chalk.Instance({ level: env.color ? 1 : 0 }); } // Public Methods // /** * Append-only output shows nothing until the first real line, so the start of the * run is silent here; a terminal renderer overrides this to begin its spinner. * * @inheritdoc */ onStart() { } /** @inheritdoc */ onScanStarted(dir) { this.err(this.scanLabel(dir)); } /** @inheritdoc */ onScanned(index) { const fileCount = Object.keys(index.manifest).length; const links = index.symlinks.length > 0 ? `, ${counted(index.symlinks.length, 'symlink')}` : ''; // Git rules shaping a docker context surprise people, so that mode is named. const rules = index.ignoreMode === 'git' ? ', using .gitignore rules' : ''; this.err(`Scanned ${counted(fileCount, 'file')}${links} (${(0, format_1.humanSize)(index.totalBytes)}${rules})`); } /** @inheritdoc */ onSensitiveFiles(paths) { const shown = paths.slice(0, 3).join(', '); const more = paths.length > 3 ? ` and ${paths.length - 3} more` : ''; this.err(`Note: ${shown}${more} will upload with the build context. Add credential files to .dockerignore to keep them out; build credentials can be passed with --secret.`); } /** @inheritdoc */ onPlanned(plan) { if (plan.mode === 'seed') { // Only a returning build's full send is worth announcing; a first build's progress shows it. if (plan.staged) { this.err(`Uploading all ${counted(plan.totalFiles, 'file')} as one archive`); } return; } if (plan.uploads.length === 0) { this.err(`Nothing to upload: ${counted(plan.unchanged, 'file')} already uploaded from a previous build.`); return; } // A full upload is shown by the progress that follows; only a partial upload needs a line. if (plan.unchanged === 0) { return; } this.err(`Uploading ${counted(plan.uploads.length, 'changed file')} (${plan.unchanged} unchanged)`); } /** @inheritdoc */ onUploadProgress(snapshot) { const complete = snapshot.completedFiles >= snapshot.totalFiles && snapshot.sentBytes >= snapshot.totalBytes; if (!complete && this.now() - this.lastUploadPrint < config_1.UPLOAD_PRINT_EVERY_MS) { return; } this.lastUploadPrint = this.now(); if (snapshot.isArchive) { this.err(complete ? ` uploaded the archive (${(0, format_1.humanSize)(snapshot.totalBytes)})` : ` uploading the archive: ${(0, format_1.humanSize)(snapshot.sentBytes)}/${(0, format_1.humanSize)(snapshot.totalBytes)}`); return; } if (complete) { this.err(` uploaded ${counted(snapshot.totalFiles, 'file')} (${(0, format_1.humanSize)(snapshot.totalBytes)})`); return; } this.err(` uploading ${snapshot.completedFiles}/${snapshot.totalFiles} files (${(0, format_1.humanSize)(snapshot.sentBytes)}/${(0, format_1.humanSize)(snapshot.totalBytes)})`); } /** @inheritdoc */ onConnectStarted(provider, org, connectUrl) { this.err(`${provider} is not connected for org "${org}". Opening your browser to connect it.`); this.err(` If the browser does not open, visit: ${connectUrl}`); } /** @inheritdoc */ onConnected() { this.err(this.paint('green', 'Connected. Resuming the build')); } /** @inheritdoc */ onSubmitted(build) { this.err(`Building ${build.imageRef} (build ${this.paint('gray', build.id)})`); } /** @inheritdoc */ onDetached(build) { this.err(`Build ${build.id} continues in the background. It appears in "cpln image get ${displayRefOf(build)}" once pushed.`); } /** @inheritdoc */ onInterrupted(build) { if (build) { this.err(this.paint('yellow', `Interrupted. Build ${build.id} continues remotely. Check it with "cpln image get ${displayRefOf(build)}".`)); return; } this.err(this.paint('yellow', 'Interrupted before the build was submitted. Nothing is running remotely.')); } /** * The service's coarse status transitions carry no detail the events and the log * do not, so they are not rendered. * * @inheritdoc */ onBuildStatus() { } /** @inheritdoc */ onBuildEvent(event) { if (event.level === 'error') { this.err(` ${this.paint('red', '[error]')} ${event.message}`); return; } if (event.level === 'warn' || event.level === 'warning') { this.err(` ${this.paint('yellow', '[warn]')} ${event.message}`); return; } // Once the BuildKit log streams, the routine narrative would only splice into it. if (!this.logStreaming) { this.err(` ${event.message}`); } } /** @inheritdoc */ onBuildLog(chunk) { var _a; let shown = false; const combined = this.logRemainder + chunk; const lines = combined.split('\n'); this.logRemainder = (_a = lines.pop()) !== null && _a !== void 0 ? _a : ''; for (const line of lines) { this.logStreaming = true; shown = this.printFiltered(this.logFilter.accept(line)) || shown; } return shown; } /** @inheritdoc */ onLogTruncated() { this.err(' (earlier build log truncated)'); } /** @inheritdoc */ onUnknownStatus(status) { this.err(` the service reported an unrecognized build status "${status}", continuing to wait`); } /** @inheritdoc */ finish() { if (this.logRemainder !== '') { this.printFiltered(this.logFilter.accept(this.logRemainder)); this.logRemainder = ''; } this.printFiltered(this.logFilter.finish()); } // Private Methods // /** * Writes one finished line to the output. Subclasses override this to interleave * lines with a live in-place indicator. * * @param {string} line - The line to write. * @returns {void} */ err(line) { this.sink(line); } /** * The label announcing the folder scan, shared by the plain line and the spinner. * * @param {string} dir - The folder being built. * @returns {string} The scan label. */ scanLabel(dir) { return `Scanning ${describeDir(dir)}`; } /** * Prints the lines a filter pass produced. * * @param {string[]} lines - The lines to print. * @returns {boolean} True when at least one line reached the output. */ printFiltered(lines) { for (const line of lines) { this.err(line); } return lines.length > 0; } /** * Applies a color to text when color output is enabled, otherwise returns it plain. * * @param {'green' | 'red' | 'yellow' | 'gray'} color - The color to apply. * @param {string} text - The text to color. * @returns {string} The colored or plain text. */ paint(color, text) { return this.palette[color](text); } } exports.PlainProgress = PlainProgress; // SECTION - Functions /** * Describes the build folder for display: the current working directory reads as * "the current folder", any other path shows as given. * * @param {string} dir - The folder being built. * @returns {string} The display label. */ function describeDir(dir) { return dir === '.' || path.resolve(dir) === process.cwd() ? 'the current folder' : dir; } /** * Extracts the name:tag form from a full registry reference. * * @param {BuildRecord} build - The build record. * @returns {string} The name:tag display reference. */ function displayRefOf(build) { var _a; return (_a = build.imageRef.split('/').pop()) !== null && _a !== void 0 ? _a : build.imageRef; } /** * Pluralizes a counted noun. * * @param {number} count - The count. * @param {string} singular - The singular noun. * @param {string} plural - (Optional) The plural form when it is not singular + "s". * @returns {string} The counted, pluralized phrase. */ function counted(count, singular, plural) { return `${count} ${count === 1 ? singular : (plural !== null && plural !== void 0 ? plural : `${singular}s`)}`; } // !SECTION //# sourceMappingURL=plain.js.map