eoas 3.0.5 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const core_1 = require("@oclif/core");
5
+ const chalk_1 = tslib_1.__importDefault(require("chalk"));
6
+ const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
7
+ const path_1 = tslib_1.__importDefault(require("path"));
8
+ const log_1 = tslib_1.__importDefault(require("../../lib/log"));
9
+ const envCatalog_1 = require("../../lib/serverConfig/envCatalog");
10
+ const helmValues_1 = require("../../lib/serverConfig/helmValues");
11
+ /** A single document can carry either half of the pair, or both when merged. */
12
+ function classifyInto(docs, doc) {
13
+ if (!docs.values && (0, helmValues_1.looksLikeChartValues)(doc)) {
14
+ docs.values = doc;
15
+ }
16
+ if (!docs.secretEnv) {
17
+ docs.secretEnv = (0, helmValues_1.extractSecretEnv)(doc);
18
+ }
19
+ }
20
+ /** Picks the Helm pair out of a directory, canonical file names first. */
21
+ async function collectHelmDocs(dir, docs, skip) {
22
+ const names = (await fs_extra_1.default.readdir(dir))
23
+ .filter(name => /\.ya?ml$/i.test(name))
24
+ .sort((a, b) => {
25
+ const canonical = (name) => name === 'values.yaml' || name === 'secrets.yaml' ? 0 : 1;
26
+ return canonical(a) - canonical(b) || a.localeCompare(b);
27
+ });
28
+ for (const name of names) {
29
+ const filePath = path_1.default.join(dir, name);
30
+ if (filePath === skip || !(await fs_extra_1.default.stat(filePath)).isFile()) {
31
+ continue;
32
+ }
33
+ try {
34
+ classifyInto(docs, (0, helmValues_1.parseYamlFile)(await fs_extra_1.default.readFile(filePath, 'utf8')));
35
+ }
36
+ catch {
37
+ // Not a single YAML mapping (multi-doc, template output...): not ours.
38
+ }
39
+ }
40
+ }
41
+ class ServerValidate extends core_1.Command {
42
+ static args = {
43
+ file: core_1.Args.string({
44
+ description: 'Path to a server .env file, a Helm values or secrets YAML, or the directory holding the Helm pair',
45
+ required: true,
46
+ }),
47
+ };
48
+ static description = 'Check a server configuration (.env file or Helm values/secrets pair) for missing or inconsistent variables';
49
+ static examples = [
50
+ '<%= config.bin %> <%= command.id %> .env.xprem',
51
+ '<%= config.bin %> <%= command.id %> xprem-helm',
52
+ '<%= config.bin %> <%= command.id %> xprem-helm/values.yaml',
53
+ ];
54
+ static flags = {};
55
+ async run() {
56
+ const { args } = await this.parse(ServerValidate);
57
+ const target = path_1.default.resolve(process.cwd(), args.file);
58
+ if (!(await fs_extra_1.default.pathExists(target))) {
59
+ log_1.default.error(`File not found: ${target}`);
60
+ process.exit(1);
61
+ }
62
+ let issues;
63
+ if ((await fs_extra_1.default.stat(target)).isDirectory()) {
64
+ const docs = {};
65
+ await collectHelmDocs(target, docs);
66
+ if (!docs.values && !docs.secretEnv) {
67
+ log_1.default.error(`No Helm values or secrets YAML found in ${target}`);
68
+ process.exit(1);
69
+ }
70
+ issues = (0, helmValues_1.validateHelmPair)(docs.values, docs.secretEnv);
71
+ }
72
+ else if (/\.ya?ml$/i.test(target)) {
73
+ let doc;
74
+ try {
75
+ doc = (0, helmValues_1.parseYamlFile)(await fs_extra_1.default.readFile(target, 'utf8'));
76
+ }
77
+ catch (e) {
78
+ log_1.default.error(`Could not parse ${args.file} as YAML: ${e instanceof Error ? e.message : e}`);
79
+ process.exit(1);
80
+ return;
81
+ }
82
+ const docs = {};
83
+ classifyInto(docs, doc);
84
+ if (!docs.values && !docs.secretEnv) {
85
+ log_1.default.error(`${args.file} looks like neither a values file for the xprem Helm chart nor a secrets overlay (secretEnv map).`);
86
+ process.exit(1);
87
+ }
88
+ // The other half of the pair is picked up from the same directory.
89
+ await collectHelmDocs(path_1.default.dirname(target), docs, target);
90
+ issues = (0, helmValues_1.validateHelmPair)(docs.values, docs.secretEnv);
91
+ }
92
+ else {
93
+ issues = (0, envCatalog_1.validateEnvMap)((0, envCatalog_1.parseEnvFile)(await fs_extra_1.default.readFile(target, 'utf8')));
94
+ }
95
+ const errors = issues.filter(issue => issue.level === 'error');
96
+ const warnings = issues.filter(issue => issue.level === 'warning');
97
+ for (const issue of errors) {
98
+ log_1.default.log(`${chalk_1.default.red('✖')} ${issue.message}`);
99
+ }
100
+ for (const issue of warnings) {
101
+ log_1.default.log(`${chalk_1.default.yellow('⚠')} ${issue.message}`);
102
+ }
103
+ log_1.default.newLine();
104
+ if (errors.length > 0) {
105
+ log_1.default.fail(`${args.file}: ${errors.length} error(s), ${warnings.length} warning(s). The server would not boot with this configuration.`);
106
+ process.exit(1);
107
+ }
108
+ if (warnings.length > 0) {
109
+ log_1.default.succeed(`${args.file}: no errors, ${warnings.length} warning(s).`);
110
+ return;
111
+ }
112
+ log_1.default.succeed(`${args.file}: everything looks good.`);
113
+ }
114
+ }
115
+ exports.default = ServerValidate;
@@ -2,7 +2,7 @@ import Joi from 'joi';
2
2
  import { Credentials } from './auth';
3
3
  import { RequestedPlatform } from './expoConfig';
4
4
  export declare const MetadataJoi: Joi.ObjectSchema<any>;
5
- interface AssetToUpload {
5
+ export interface AssetToUpload {
6
6
  path: string;
7
7
  name: string;
8
8
  ext: string;
@@ -14,8 +14,34 @@ export interface RequestUploadUrlItem {
14
14
  filePath: string;
15
15
  headers?: Record<string, string>;
16
16
  }
17
+ export interface RequestUploadUrlsResponse {
18
+ uploadRequests: RequestUploadUrlItem[];
19
+ updateId: string;
20
+ rolloutPercentage?: number;
21
+ publishGroup?: string;
22
+ }
23
+ export declare const RequestUploadUrlsResponseJoi: Joi.ObjectSchema<any>;
24
+ export interface ResolvedUploadRequest {
25
+ item: RequestUploadUrlItem;
26
+ absolutePath: string;
27
+ manifestEntry: AssetToUpload;
28
+ }
29
+ /**
30
+ * Maps every upload request of ONE server response onto a file the CLI itself
31
+ * exported, and throws on anything it cannot account for. This runs to
32
+ * completion before the first byte is read: a single bad entry aborts the whole
33
+ * publish rather than uploading the files that happened to be fine.
34
+ *
35
+ * Call it once per response: `eoas publish --platform all` requests upload URLs
36
+ * per runtime version, and every response legitimately names the same files.
37
+ */
38
+ export declare function resolveUploadRequests({ uploadRequests, exportDir, manifest, }: {
39
+ uploadRequests: RequestUploadUrlItem[];
40
+ exportDir: string;
41
+ manifest: AssetToUpload[];
42
+ }): Promise<ResolvedUploadRequest[]>;
17
43
  export declare function activeRolloutConflictMessage(branch: string): string;
18
- export declare function requestUploadUrls({ body, requestUploadUrl, auth, runtimeVersion, platform, commitHash, message, rolloutPercentage, branch, }: {
44
+ export declare function requestUploadUrls({ body, requestUploadUrl, auth, runtimeVersion, platform, commitHash, message, rolloutPercentage, publishGroup, branch, }: {
19
45
  body: {
20
46
  fileNames: string[];
21
47
  };
@@ -26,10 +52,6 @@ export declare function requestUploadUrls({ body, requestUploadUrl, auth, runtim
26
52
  commitHash?: string;
27
53
  message?: string;
28
54
  rolloutPercentage?: number;
55
+ publishGroup?: string;
29
56
  branch: string;
30
- }): Promise<{
31
- uploadRequests: RequestUploadUrlItem[];
32
- updateId: string;
33
- rolloutPercentage?: number;
34
- }>;
35
- export {};
57
+ }): Promise<RequestUploadUrlsResponse>;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.requestUploadUrls = exports.activeRolloutConflictMessage = exports.computeFilesRequests = exports.MetadataJoi = void 0;
3
+ exports.requestUploadUrls = exports.activeRolloutConflictMessage = exports.resolveUploadRequests = exports.RequestUploadUrlsResponseJoi = exports.computeFilesRequests = exports.MetadataJoi = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
6
6
  const joi_1 = tslib_1.__importDefault(require("joi"));
@@ -73,11 +73,155 @@ function computeFilesRequests(projectDir, outputDir, requestedPlatform) {
73
73
  return assets;
74
74
  }
75
75
  exports.computeFilesRequests = computeFilesRequests;
76
+ // The server dictates which local files the CLI opens and where their bytes are
77
+ // sent, so its answer is untrusted input: a hostile or compromised server that
78
+ // gets a forged filePath past us reads any file it wants off the developer or CI
79
+ // machine. Every field is typed here, and the paths are checked against the
80
+ // export manifest in resolveUploadRequests before anything is opened.
81
+ const uploadRequestHeadersJoi = joi_1.default.object()
82
+ .pattern(
83
+ // RFC 7230 header field-name, and a value that cannot smuggle a CRLF.
84
+ /^[A-Za-z0-9!#$%&'*+\-.^_`|~]+$/, joi_1.default.string()
85
+ .allow('')
86
+ .pattern(/^[^\r\n]*$/))
87
+ .optional();
88
+ const uploadRequestJoi = joi_1.default.object({
89
+ requestUploadUrl: joi_1.default.string()
90
+ .uri({ scheme: ['http', 'https'] })
91
+ .required(),
92
+ fileName: joi_1.default.string().required(),
93
+ filePath: joi_1.default.string().required(),
94
+ headers: uploadRequestHeadersJoi,
95
+ // Unknown keys are tolerated so a newer server can add fields without
96
+ // breaking older CLIs; nothing reads them.
97
+ }).unknown(true);
98
+ exports.RequestUploadUrlsResponseJoi = joi_1.default.object({
99
+ // The server marshals updateId from an int64, so it arrives as a JSON number;
100
+ // older or third-party servers may send it as a string. Normalized to a string
101
+ // below, which is what the markUpdateAsUploaded query parameter needs.
102
+ updateId: joi_1.default.alternatives().try(joi_1.default.string(), joi_1.default.number()).required(),
103
+ uploadRequests: joi_1.default.array().items(uploadRequestJoi).required(),
104
+ rolloutPercentage: joi_1.default.number().optional(),
105
+ publishGroup: joi_1.default.string().optional(),
106
+ })
107
+ .required()
108
+ .unknown(true);
109
+ function isLoopbackHost(hostname) {
110
+ const host = hostname.replace(/^\[/, '').replace(/\]$/, '').toLowerCase();
111
+ // Deliberately does not accept *.localhost: resolvers are free to answer it
112
+ // from DNS, which would let a remote host claim the plain-HTTP exemption.
113
+ return host === 'localhost' || host === '::1' || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
114
+ }
115
+ // Escape hatch for the deployments that legitimately serve upload URLs over
116
+ // plain HTTP on a non-loopback host: a MinIO or S3-compatible endpoint set
117
+ // through AWS_BASE_ENDPOINT, or a local bucket whose BASE_URL is an internal
118
+ // hostname. It only lifts the transport requirement, never a path check.
119
+ const INSECURE_UPLOAD_URLS_ENV = 'EOAS_ALLOW_INSECURE_UPLOAD_URLS';
120
+ function insecureUploadUrlsAllowed() {
121
+ const value = process.env[INSECURE_UPLOAD_URLS_ENV];
122
+ return value === '1' || value?.toLowerCase() === 'true';
123
+ }
124
+ function assertSafeUploadUrl(requestUploadUrl) {
125
+ let url;
126
+ try {
127
+ url = new URL(requestUploadUrl);
128
+ }
129
+ catch {
130
+ throw new Error(`The server returned an unusable upload URL: ${requestUploadUrl}`);
131
+ }
132
+ if (url.protocol === 'https:') {
133
+ return;
134
+ }
135
+ if (url.protocol === 'http:' && isLoopbackHost(url.hostname)) {
136
+ return;
137
+ }
138
+ if (url.protocol === 'http:' && insecureUploadUrlsAllowed()) {
139
+ log_1.default.warn(`Uploading to ${url.origin} over plain HTTP because ${INSECURE_UPLOAD_URLS_ENV} is set. Your update artifacts travel unencrypted.`);
140
+ return;
141
+ }
142
+ throw new Error(`Refusing to upload to ${url.origin}: update artifacts may only be sent over HTTPS (plain HTTP is allowed for loopback addresses only). Set ${INSECURE_UPLOAD_URLS_ENV}=1 if your storage endpoint is intentionally served over HTTP.`);
143
+ }
144
+ function assertRelativePathShape(filePath) {
145
+ if (!filePath || filePath.includes('\0')) {
146
+ throw new Error('The server returned an empty or malformed file path.');
147
+ }
148
+ // win32.isAbsolute also covers the posix cases, drive letters and UNC paths.
149
+ if (path_1.default.isAbsolute(filePath) || path_1.default.win32.isAbsolute(filePath)) {
150
+ throw new Error(`Refusing to upload the absolute path "${filePath}" requested by the server.`);
151
+ }
152
+ if (filePath.split(/[/\\]/).some(segment => segment === '..')) {
153
+ throw new Error(`Refusing to upload "${filePath}": the server requested a path outside the export directory.`);
154
+ }
155
+ }
156
+ /**
157
+ * Maps every upload request of ONE server response onto a file the CLI itself
158
+ * exported, and throws on anything it cannot account for. This runs to
159
+ * completion before the first byte is read: a single bad entry aborts the whole
160
+ * publish rather than uploading the files that happened to be fine.
161
+ *
162
+ * Call it once per response: `eoas publish --platform all` requests upload URLs
163
+ * per runtime version, and every response legitimately names the same files.
164
+ */
165
+ async function resolveUploadRequests({ uploadRequests, exportDir, manifest, }) {
166
+ const manifestByPath = new Map(manifest.map(entry => [entry.path, entry]));
167
+ // Canonical root, so a symlinked export directory (or /tmp on macOS) does not
168
+ // make every containment check fail below.
169
+ let exportRoot;
170
+ try {
171
+ exportRoot = await fs_extra_1.default.realpath(exportDir);
172
+ }
173
+ catch {
174
+ throw new Error(`Export directory ${exportDir} could not be resolved.`);
175
+ }
176
+ const seen = new Set();
177
+ const resolved = [];
178
+ for (const item of uploadRequests) {
179
+ assertSafeUploadUrl(item.requestUploadUrl);
180
+ assertRelativePathShape(item.filePath);
181
+ const manifestEntry = manifestByPath.get(item.filePath);
182
+ if (!manifestEntry) {
183
+ throw new Error(`Refusing to upload "${item.filePath}": the server asked for a file that is not part of this export.`);
184
+ }
185
+ if (item.fileName !== path_1.default.basename(item.filePath)) {
186
+ throw new Error(`Refusing to upload "${item.filePath}": the server returned the mismatched name "${item.fileName}".`);
187
+ }
188
+ if (seen.has(item.filePath)) {
189
+ throw new Error(`The server requested "${item.filePath}" more than once.`);
190
+ }
191
+ seen.add(item.filePath);
192
+ const absolutePath = path_1.default.resolve(exportRoot, item.filePath);
193
+ // Unreachable on POSIX: a path with no '..' segment and no leading separator
194
+ // cannot resolve out of the root. Kept for the Windows drive-relative case
195
+ // ("C:file" when the export root sits on another drive) and as a backstop if
196
+ // the checks above are ever relaxed.
197
+ if (absolutePath !== exportRoot && !absolutePath.startsWith(exportRoot + path_1.default.sep)) {
198
+ throw new Error(`Refusing to upload "${item.filePath}": it resolves outside the export directory.`);
199
+ }
200
+ let realPath;
201
+ try {
202
+ realPath = await fs_extra_1.default.realpath(absolutePath);
203
+ }
204
+ catch {
205
+ throw new Error(`File ${item.filePath} not found in the export directory.`);
206
+ }
207
+ // The root is already canonical, so any difference here means a symlink was
208
+ // traversed, either as the file itself or as one of its parent directories.
209
+ if (realPath !== absolutePath) {
210
+ throw new Error(`Refusing to upload "${item.filePath}": it is or goes through a symlink.`);
211
+ }
212
+ if (!(await fs_extra_1.default.lstat(absolutePath)).isFile()) {
213
+ throw new Error(`Refusing to upload "${item.filePath}": it is not a regular file.`);
214
+ }
215
+ resolved.push({ item, absolutePath, manifestEntry });
216
+ }
217
+ return resolved;
218
+ }
219
+ exports.resolveUploadRequests = resolveUploadRequests;
76
220
  function activeRolloutConflictMessage(branch) {
77
221
  return `A progressive rollout is already active for branch "${branch}" on this runtime version. End or revert it from the dashboard before publishing a new update.`;
78
222
  }
79
223
  exports.activeRolloutConflictMessage = activeRolloutConflictMessage;
80
- async function requestUploadUrls({ body, requestUploadUrl, auth, runtimeVersion, platform, commitHash, message, rolloutPercentage, branch, }) {
224
+ async function requestUploadUrls({ body, requestUploadUrl, auth, runtimeVersion, platform, commitHash, message, rolloutPercentage, publishGroup, branch, }) {
81
225
  const uploadUrl = new URL(requestUploadUrl);
82
226
  uploadUrl.searchParams.set('runtimeVersion', runtimeVersion);
83
227
  uploadUrl.searchParams.set('platform', platform);
@@ -85,6 +229,9 @@ async function requestUploadUrls({ body, requestUploadUrl, auth, runtimeVersion,
85
229
  if (rolloutPercentage !== undefined) {
86
230
  uploadUrl.searchParams.set('rolloutPercentage', String(rolloutPercentage));
87
231
  }
232
+ if (publishGroup) {
233
+ uploadUrl.searchParams.set('publishGroup', publishGroup);
234
+ }
88
235
  const requestBody = { ...body };
89
236
  if (message) {
90
237
  requestBody.message = message;
@@ -105,12 +252,19 @@ async function requestUploadUrls({ body, requestUploadUrl, auth, runtimeVersion,
105
252
  throw new Error(`Failed to request upload URL: ${text}`);
106
253
  }
107
254
  const json = await response.json();
255
+ // Joi's sanitized value, not the raw payload: it is the object the schema
256
+ // actually vouched for, with inherited keys such as __proto__ dropped.
257
+ const { error, value } = exports.RequestUploadUrlsResponseJoi.validate(json);
258
+ if (error) {
259
+ throw new Error(`The server returned an invalid upload response: ${error.message}`);
260
+ }
261
+ const validated = { ...value, updateId: String(value.updateId) };
108
262
  // An old server silently ignores unknown query params, so a missing echo means
109
263
  // the rollout was not applied even though the flag was set. Abort before any
110
264
  // file is uploaded: continuing would finalize a full 100% publish.
111
- if (rolloutPercentage !== undefined && json.rolloutPercentage === undefined) {
265
+ if (rolloutPercentage !== undefined && validated.rolloutPercentage === undefined) {
112
266
  throw new Error('The server ignored --rollout-percentage and would publish to 100% of devices. Update the server to a version that supports progressive rollouts, or publish without --rollout-percentage.');
113
267
  }
114
- return json;
268
+ return validated;
115
269
  }
116
270
  exports.requestUploadUrls = requestUploadUrls;
package/dist/lib/auth.js CHANGED
@@ -19,8 +19,9 @@ function retrieveCredentials() {
19
19
  }
20
20
  exports.retrieveCredentials = retrieveCredentials;
21
21
  function validateCredentials(credentials) {
22
- if (!credentials)
22
+ if (!credentials) {
23
23
  return false;
24
+ }
24
25
  return !!(credentials.token || credentials.sessionSecret);
25
26
  }
26
27
  exports.validateCredentials = validateCredentials;
package/dist/lib/log.d.ts CHANGED
@@ -12,10 +12,16 @@ export default class Log {
12
12
  static succeed(message: string): void;
13
13
  static withTick(...args: any[]): void;
14
14
  static withInfo(...args: any[]): void;
15
- private static consoleLog;
15
+ /** Opens a clack session frame; every line after it hangs on the gutter. */
16
+ static intro(title: string): void;
17
+ /** Closes the clack session frame opened by intro. */
18
+ static outro(message: string): void;
19
+ static note(content: string, title?: string): void;
20
+ static cancel(message: string): void;
21
+ private static write;
16
22
  private static withTextColor;
17
23
  private static isLastLineNewLine;
18
- private static updateIsLastLineNewLine;
24
+ private static track;
19
25
  }
20
26
  /**
21
27
  * Prints a link for given URL, using text if provided, otherwise text is just the URL.
package/dist/lib/log.js CHANGED
@@ -2,19 +2,23 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.learnMore = exports.link = void 0;
4
4
  const tslib_1 = require("tslib");
5
- // This file is copied from eas-cli[https://github.com/expo/eas-cli] to ensure consistent user experience across the CLI.
5
+ // Rendering for every command's output, drawn with the @clack/prompts
6
+ // primitives so the whole CLI shares one visual identity (gutter, symbols,
7
+ // notes). The static API is kept from the original eas-cli logger so call
8
+ // sites did not have to change.
9
+ const clack = tslib_1.__importStar(require("@clack/prompts"));
6
10
  const chalk_1 = tslib_1.__importDefault(require("chalk"));
7
- const figures_1 = tslib_1.__importDefault(require("figures"));
8
11
  const getenv_1 = require("getenv");
9
- const log_symbols_1 = tslib_1.__importDefault(require("log-symbols"));
10
12
  const terminal_link_1 = tslib_1.__importDefault(require("terminal-link"));
13
+ const util_1 = require("util");
11
14
  class Log {
12
15
  static isDebug = (0, getenv_1.boolish)('DEBUG', false);
13
16
  static log(...args) {
14
- Log.consoleLog(...args);
17
+ Log.write((0, util_1.format)(...args));
15
18
  }
16
19
  static newLine() {
17
- Log.consoleLog();
20
+ Log.write('');
21
+ Log.isLastLineNewLine = true;
18
22
  }
19
23
  static addNewLineIfNone() {
20
24
  if (!Log.isLastLineNewLine) {
@@ -22,56 +26,68 @@ class Log {
22
26
  }
23
27
  }
24
28
  static error(...args) {
25
- Log.consoleLog(...Log.withTextColor(args, chalk_1.default.red));
29
+ Log.track();
30
+ clack.log.error(Log.withTextColor(args, chalk_1.default.red).join(' '));
26
31
  }
27
32
  static warn(...args) {
28
- Log.consoleLog(...Log.withTextColor(args, chalk_1.default.yellow));
33
+ Log.track();
34
+ clack.log.warn(Log.withTextColor(args, chalk_1.default.yellow).join(' '));
29
35
  }
30
36
  static debug(...args) {
31
37
  if (Log.isDebug) {
32
- Log.consoleLog(...args);
38
+ Log.write((0, util_1.format)(...args));
33
39
  }
34
40
  }
35
41
  static gray(...args) {
36
- Log.consoleLog(...Log.withTextColor(args, chalk_1.default.gray));
42
+ Log.write(Log.withTextColor(args, chalk_1.default.gray).join(' '));
37
43
  }
38
44
  static warnDeprecatedFlag(flag, message) {
39
45
  Log.warn(`› ${chalk_1.default.bold('--' + flag)} flag is deprecated. ${message}`);
40
46
  }
41
47
  static fail(message) {
42
- Log.log(`${chalk_1.default.red(log_symbols_1.default.error)} ${message}`);
48
+ Log.track();
49
+ clack.log.error(message);
43
50
  }
44
51
  static succeed(message) {
45
- Log.log(`${chalk_1.default.green(log_symbols_1.default.success)} ${message}`);
52
+ Log.track();
53
+ clack.log.success(message);
46
54
  }
47
55
  static withTick(...args) {
48
- Log.consoleLog(chalk_1.default.green(figures_1.default.tick), ...args);
56
+ Log.track();
57
+ clack.log.success((0, util_1.format)(...args));
49
58
  }
50
59
  static withInfo(...args) {
51
- Log.consoleLog(chalk_1.default.green(figures_1.default.info), ...args);
60
+ Log.track();
61
+ clack.log.info((0, util_1.format)(...args));
52
62
  }
53
- static consoleLog(...args) {
54
- Log.updateIsLastLineNewLine(args);
55
- // eslint-disable-next-line no-console
56
- console.log(...args);
63
+ /** Opens a clack session frame; every line after it hangs on the gutter. */
64
+ static intro(title) {
65
+ Log.track();
66
+ clack.intro(title);
67
+ }
68
+ /** Closes the clack session frame opened by intro. */
69
+ static outro(message) {
70
+ Log.track();
71
+ clack.outro(message);
72
+ }
73
+ static note(content, title) {
74
+ Log.track();
75
+ clack.note(content, title);
76
+ }
77
+ static cancel(message) {
78
+ Log.track();
79
+ clack.cancel(message);
80
+ }
81
+ static write(text) {
82
+ Log.track(text);
83
+ clack.log.message(text);
57
84
  }
58
85
  static withTextColor(args, chalkColor) {
59
- return args.map(arg => chalkColor(arg));
86
+ return args.map(arg => chalkColor((0, util_1.format)(arg)));
60
87
  }
61
88
  static isLastLineNewLine = false;
62
- static updateIsLastLineNewLine(args) {
63
- if (args.length === 0) {
64
- Log.isLastLineNewLine = true;
65
- }
66
- else {
67
- const lastArg = args[args.length - 1];
68
- if (typeof lastArg === 'string' && (lastArg === '' || lastArg.match(/[\r\n]$/))) {
69
- Log.isLastLineNewLine = true;
70
- }
71
- else {
72
- Log.isLastLineNewLine = false;
73
- }
74
- }
89
+ static track(text) {
90
+ Log.isLastLineNewLine = text !== undefined && text === '';
75
91
  }
76
92
  }
77
93
  exports.default = Log;
package/dist/lib/ora.d.ts CHANGED
@@ -1,9 +1,10 @@
1
- import { Options, Ora } from 'ora';
2
- export { Ora, Options };
3
- /**
4
- * A custom ora spinner that sends the stream to stdout in CI, or non-TTY, instead of stderr (the default).
5
- *
6
- * @param options
7
- * @returns
8
- */
9
- export declare function ora(options?: Options | string): Ora;
1
+ export type Spinner = {
2
+ start(text?: string): Spinner;
3
+ succeed(text?: string): Spinner;
4
+ fail(text?: string): Spinner;
5
+ warn(text?: string): Spinner;
6
+ stop(): Spinner;
7
+ };
8
+ export declare function ora(options?: string | {
9
+ text?: string;
10
+ }): Spinner;