@controlplane/cli
Version:
Control Plane Corporation CLI
245 lines • 9.68 kB
JavaScript
"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