@lynxship/cli 0.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.
- package/README.md +122 -0
- package/dist/android-build.d.ts +14 -0
- package/dist/android-build.d.ts.map +1 -0
- package/dist/android-build.js +201 -0
- package/dist/artifact-name.d.ts +3 -0
- package/dist/artifact-name.d.ts.map +1 -0
- package/dist/artifact-name.js +4 -0
- package/dist/autolink.d.ts +14 -0
- package/dist/autolink.d.ts.map +1 -0
- package/dist/autolink.js +144 -0
- package/dist/config.d.ts +51 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +62 -0
- package/dist/configure.d.ts +11 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +172 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1082 -0
- package/dist/ios-build.d.ts +13 -0
- package/dist/ios-build.d.ts.map +1 -0
- package/dist/ios-build.js +174 -0
- package/dist/ota-assets.d.ts +3 -0
- package/dist/ota-assets.d.ts.map +1 -0
- package/dist/ota-assets.js +48 -0
- package/dist/ota-doctor.d.ts +10 -0
- package/dist/ota-doctor.d.ts.map +1 -0
- package/dist/ota-doctor.js +53 -0
- package/dist/paths.d.ts +2 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +12 -0
- package/dist/process-runner.d.ts +18 -0
- package/dist/process-runner.d.ts.map +1 -0
- package/dist/process-runner.js +97 -0
- package/dist/prompt.d.ts +3 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +58 -0
- package/dist/r2.d.ts +32 -0
- package/dist/r2.d.ts.map +1 -0
- package/dist/r2.js +135 -0
- package/dist/remote.d.ts +33 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +116 -0
- package/dist/runtime-fingerprint.d.ts +10 -0
- package/dist/runtime-fingerprint.d.ts.map +1 -0
- package/dist/runtime-fingerprint.js +238 -0
- package/dist/secure-store.d.ts +32 -0
- package/dist/secure-store.d.ts.map +1 -0
- package/dist/secure-store.js +263 -0
- package/dist/ui/colors.d.ts +24 -0
- package/dist/ui/colors.d.ts.map +1 -0
- package/dist/ui/colors.js +64 -0
- package/dist/ui/components.d.ts +36 -0
- package/dist/ui/components.d.ts.map +1 -0
- package/dist/ui/components.js +268 -0
- package/dist/ui/index.d.ts +24 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +68 -0
- package/dist/ui/logo.d.ts +3 -0
- package/dist/ui/logo.d.ts.map +1 -0
- package/dist/ui/logo.js +32 -0
- package/dist/ui/state.d.ts +5 -0
- package/dist/ui/state.d.ts.map +1 -0
- package/dist/ui/state.js +7 -0
- package/dist/ui/terminal.d.ts +12 -0
- package/dist/ui/terminal.d.ts.map +1 -0
- package/dist/ui/terminal.js +23 -0
- package/package.json +75 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { assert } from "@lynxship/contracts";
|
|
5
|
+
import { globalLynxShipDirectory } from "./paths.js";
|
|
6
|
+
export function credentialStorageDescription() {
|
|
7
|
+
if (process.platform === "win32")
|
|
8
|
+
return "Windows DPAPI encrypted";
|
|
9
|
+
if (process.platform === "darwin")
|
|
10
|
+
return "macOS Keychain";
|
|
11
|
+
return "owner-only credential file";
|
|
12
|
+
}
|
|
13
|
+
const fileName = ".credentials.dpapi.json";
|
|
14
|
+
const keychainService = "com.lynxship.cli.credentials";
|
|
15
|
+
function credentialFile(root, global = false) {
|
|
16
|
+
return join(global ? globalLynxShipDirectory() : join(root, ".lynxship"), fileName);
|
|
17
|
+
}
|
|
18
|
+
function runPowerShell(command, input) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const child = spawn(`${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`, ["-NoProfile", "-NonInteractive", "-Command", command], {
|
|
21
|
+
windowsHide: true,
|
|
22
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
23
|
+
env: {
|
|
24
|
+
...process.env,
|
|
25
|
+
PSModulePath: [
|
|
26
|
+
`${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\WindowsPowerShell\\v1.0\\Modules`,
|
|
27
|
+
`${process.env.ProgramFiles ?? "C:\\Program Files"}\\WindowsPowerShell\\Modules`,
|
|
28
|
+
].join(";"),
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
let output = "";
|
|
32
|
+
let error = "";
|
|
33
|
+
child.stdout.on("data", (chunk) => (output += chunk.toString()));
|
|
34
|
+
child.stderr.on("data", (chunk) => (error += chunk.toString()));
|
|
35
|
+
child.once("error", reject);
|
|
36
|
+
child.once("close", (code) => {
|
|
37
|
+
if (code !== 0)
|
|
38
|
+
reject(new Error(error.trim() || "Windows secure storage failed"));
|
|
39
|
+
else
|
|
40
|
+
resolve(output.trim());
|
|
41
|
+
});
|
|
42
|
+
child.stdin.end(input);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async function protect(value) {
|
|
46
|
+
if (process.platform !== "win32")
|
|
47
|
+
return value;
|
|
48
|
+
return runPowerShell("Import-Module Microsoft.PowerShell.Security; $input | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString", value);
|
|
49
|
+
}
|
|
50
|
+
async function unprotect(value) {
|
|
51
|
+
if (process.platform !== "win32")
|
|
52
|
+
return value;
|
|
53
|
+
return runPowerShell("Import-Module Microsoft.PowerShell.Security; $cipher = [Console]::In.ReadToEnd(); $secure = ConvertTo-SecureString $cipher; $ptr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure); try { [Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr) }", value);
|
|
54
|
+
}
|
|
55
|
+
function runSecurity(args) {
|
|
56
|
+
return new Promise((resolveOutput, reject) => {
|
|
57
|
+
const child = spawn("security", args, {
|
|
58
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
59
|
+
});
|
|
60
|
+
let output = "";
|
|
61
|
+
let error = "";
|
|
62
|
+
child.stdout.on("data", (chunk) => (output += chunk.toString()));
|
|
63
|
+
child.stderr.on("data", (chunk) => (error += chunk.toString()));
|
|
64
|
+
child.once("error", reject);
|
|
65
|
+
child.once("close", (code) => {
|
|
66
|
+
if (code !== 0)
|
|
67
|
+
reject(new Error(error.trim() || "macOS Keychain operation failed"));
|
|
68
|
+
else
|
|
69
|
+
resolveOutput(output.trim());
|
|
70
|
+
});
|
|
71
|
+
child.stdin.end();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function keychainAccount(root, global) {
|
|
75
|
+
return global ? "global" : `project:${resolve(root)}`;
|
|
76
|
+
}
|
|
77
|
+
function credentialValues(credentials) {
|
|
78
|
+
const values = {};
|
|
79
|
+
if (credentials.r2) {
|
|
80
|
+
values.r2AccessKeyId = credentials.r2.accessKeyId;
|
|
81
|
+
values.r2SecretAccessKey = credentials.r2.secretAccessKey;
|
|
82
|
+
}
|
|
83
|
+
if (credentials.android) {
|
|
84
|
+
values.androidKeystorePath = credentials.android.keystorePath;
|
|
85
|
+
values.androidKeyAlias = credentials.android.keyAlias;
|
|
86
|
+
values.androidKeystorePassword = credentials.android.keystorePassword;
|
|
87
|
+
values.androidKeyPassword = credentials.android.keyPassword;
|
|
88
|
+
}
|
|
89
|
+
if (credentials.googlePlay) {
|
|
90
|
+
values.googlePlayServiceAccountJson =
|
|
91
|
+
credentials.googlePlay.serviceAccountJson;
|
|
92
|
+
values.googlePlayApplicationId = credentials.googlePlay.applicationId;
|
|
93
|
+
values.googlePlayTrack = credentials.googlePlay.track;
|
|
94
|
+
values.googlePlayReleaseStatus = credentials.googlePlay.releaseStatus;
|
|
95
|
+
}
|
|
96
|
+
if (credentials.appStoreConnect) {
|
|
97
|
+
values.ascApiKeyId = credentials.appStoreConnect.apiKeyId;
|
|
98
|
+
values.ascIssuerId = credentials.appStoreConnect.issuerId;
|
|
99
|
+
values.ascPrivateKey = credentials.appStoreConnect.privateKey;
|
|
100
|
+
values.ascBundleIdentifier = credentials.appStoreConnect.bundleIdentifier;
|
|
101
|
+
values.ascAppId = credentials.appStoreConnect.ascAppId ?? "";
|
|
102
|
+
values.ascTransporterPath =
|
|
103
|
+
credentials.appStoreConnect.transporterPath ?? "";
|
|
104
|
+
}
|
|
105
|
+
return values;
|
|
106
|
+
}
|
|
107
|
+
function credentialsFromValues(values) {
|
|
108
|
+
return {
|
|
109
|
+
r2: values.r2AccessKeyId && values.r2SecretAccessKey
|
|
110
|
+
? {
|
|
111
|
+
accessKeyId: values.r2AccessKeyId,
|
|
112
|
+
secretAccessKey: values.r2SecretAccessKey,
|
|
113
|
+
}
|
|
114
|
+
: undefined,
|
|
115
|
+
android: values.androidKeystorePath &&
|
|
116
|
+
values.androidKeyAlias &&
|
|
117
|
+
values.androidKeystorePassword &&
|
|
118
|
+
values.androidKeyPassword
|
|
119
|
+
? {
|
|
120
|
+
keystorePath: values.androidKeystorePath,
|
|
121
|
+
keyAlias: values.androidKeyAlias,
|
|
122
|
+
keystorePassword: values.androidKeystorePassword,
|
|
123
|
+
keyPassword: values.androidKeyPassword,
|
|
124
|
+
}
|
|
125
|
+
: undefined,
|
|
126
|
+
googlePlay: values.googlePlayServiceAccountJson &&
|
|
127
|
+
values.googlePlayApplicationId &&
|
|
128
|
+
values.googlePlayTrack &&
|
|
129
|
+
values.googlePlayReleaseStatus
|
|
130
|
+
? {
|
|
131
|
+
serviceAccountJson: values.googlePlayServiceAccountJson,
|
|
132
|
+
applicationId: values.googlePlayApplicationId,
|
|
133
|
+
track: values.googlePlayTrack,
|
|
134
|
+
releaseStatus: values.googlePlayReleaseStatus,
|
|
135
|
+
}
|
|
136
|
+
: undefined,
|
|
137
|
+
appStoreConnect: values.ascApiKeyId &&
|
|
138
|
+
values.ascIssuerId &&
|
|
139
|
+
values.ascPrivateKey &&
|
|
140
|
+
values.ascBundleIdentifier
|
|
141
|
+
? {
|
|
142
|
+
apiKeyId: values.ascApiKeyId,
|
|
143
|
+
issuerId: values.ascIssuerId,
|
|
144
|
+
privateKey: values.ascPrivateKey,
|
|
145
|
+
bundleIdentifier: values.ascBundleIdentifier,
|
|
146
|
+
ascAppId: values.ascAppId || undefined,
|
|
147
|
+
transporterPath: values.ascTransporterPath || undefined,
|
|
148
|
+
}
|
|
149
|
+
: undefined,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async function readKeychain(root, global) {
|
|
153
|
+
try {
|
|
154
|
+
const value = await runSecurity([
|
|
155
|
+
"find-generic-password",
|
|
156
|
+
"-a",
|
|
157
|
+
keychainAccount(root, global),
|
|
158
|
+
"-s",
|
|
159
|
+
keychainService,
|
|
160
|
+
"-w",
|
|
161
|
+
]);
|
|
162
|
+
const stored = JSON.parse(value);
|
|
163
|
+
assert(stored.version === 1 && stored.platform === "macos-keychain", "CLI_CREDENTIALS_INVALID", "Unsupported LynxShip Keychain record");
|
|
164
|
+
return credentialsFromValues(stored.values);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function saveKeychain(root, credentials, global) {
|
|
171
|
+
const record = {
|
|
172
|
+
version: 1,
|
|
173
|
+
platform: "macos-keychain",
|
|
174
|
+
values: credentialValues(credentials),
|
|
175
|
+
};
|
|
176
|
+
await runSecurity([
|
|
177
|
+
"add-generic-password",
|
|
178
|
+
"-U",
|
|
179
|
+
"-a",
|
|
180
|
+
keychainAccount(root, global),
|
|
181
|
+
"-s",
|
|
182
|
+
keychainService,
|
|
183
|
+
"-w",
|
|
184
|
+
JSON.stringify(record),
|
|
185
|
+
]);
|
|
186
|
+
await unlink(credentialFile(root, global)).catch(() => undefined);
|
|
187
|
+
}
|
|
188
|
+
function mergeCredentials(global, project) {
|
|
189
|
+
return {
|
|
190
|
+
r2: project.r2 ?? global.r2,
|
|
191
|
+
android: project.android ?? global.android,
|
|
192
|
+
googlePlay: project.googlePlay ?? global.googlePlay,
|
|
193
|
+
appStoreConnect: project.appStoreConnect ?? global.appStoreConnect,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
async function readCredentialFile(file) {
|
|
197
|
+
try {
|
|
198
|
+
const encrypted = JSON.parse(await readFile(file, "utf8"));
|
|
199
|
+
assert(encrypted.version === 1 &&
|
|
200
|
+
(encrypted.platform === "windows-dpapi" ||
|
|
201
|
+
encrypted.platform === "macos-keychain" ||
|
|
202
|
+
encrypted.platform === "file-mode-600"), "CLI_CREDENTIALS_INVALID", "Unsupported LynxShip credential store");
|
|
203
|
+
const values = Object.fromEntries(await Promise.all(Object.entries(encrypted.values).map(async ([key, value]) => [
|
|
204
|
+
key,
|
|
205
|
+
encrypted.platform === "windows-dpapi"
|
|
206
|
+
? await unprotect(value)
|
|
207
|
+
: value,
|
|
208
|
+
])));
|
|
209
|
+
return credentialsFromValues(values);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (error.code === "ENOENT")
|
|
213
|
+
return {};
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function readCredentialStore(root, global) {
|
|
218
|
+
if (process.platform === "darwin") {
|
|
219
|
+
const keychain = await readKeychain(root, global);
|
|
220
|
+
if (keychain)
|
|
221
|
+
return keychain;
|
|
222
|
+
}
|
|
223
|
+
const legacy = await readCredentialFile(credentialFile(root, global));
|
|
224
|
+
if (process.platform === "darwin" &&
|
|
225
|
+
(legacy.r2 !== undefined || legacy.android !== undefined)) {
|
|
226
|
+
await saveKeychain(root, legacy, global).catch(() => undefined);
|
|
227
|
+
}
|
|
228
|
+
return legacy;
|
|
229
|
+
}
|
|
230
|
+
export async function loadCredentials(root) {
|
|
231
|
+
const [global, project] = await Promise.all([
|
|
232
|
+
readCredentialStore(root, true),
|
|
233
|
+
readCredentialStore(root, false),
|
|
234
|
+
]);
|
|
235
|
+
return mergeCredentials(global, project);
|
|
236
|
+
}
|
|
237
|
+
export async function saveCredentials(root, credentials, options = {}) {
|
|
238
|
+
const global = options.global ?? false;
|
|
239
|
+
if (process.platform === "darwin") {
|
|
240
|
+
await saveKeychain(root, credentials, global);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const values = credentialValues(credentials);
|
|
244
|
+
const encrypted = {
|
|
245
|
+
version: 1,
|
|
246
|
+
platform: process.platform === "win32" ? "windows-dpapi" : "file-mode-600",
|
|
247
|
+
values: Object.fromEntries(await Promise.all(Object.entries(values).map(async ([key, value]) => [
|
|
248
|
+
key,
|
|
249
|
+
await protect(value),
|
|
250
|
+
]))),
|
|
251
|
+
};
|
|
252
|
+
const directory = global
|
|
253
|
+
? globalLynxShipDirectory()
|
|
254
|
+
: join(root, ".lynxship");
|
|
255
|
+
await mkdir(directory, { recursive: true });
|
|
256
|
+
const file = credentialFile(root, global);
|
|
257
|
+
await writeFile(file, `${JSON.stringify(encrypted, null, 2)}\n`, {
|
|
258
|
+
encoding: "utf8",
|
|
259
|
+
mode: 0o600,
|
|
260
|
+
});
|
|
261
|
+
if (process.platform !== "win32")
|
|
262
|
+
await chmod(file, 0o600);
|
|
263
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ChalkInstance } from "chalk";
|
|
2
|
+
export declare const LYNX_BRAND: {
|
|
3
|
+
readonly pink: "#ff6b9d";
|
|
4
|
+
readonly cyan: "#45b7d1";
|
|
5
|
+
};
|
|
6
|
+
export type ColorName = "teal" | "tealDim" | "blue" | "orange" | "yellow" | "red" | "purple" | "green" | "text" | "muted" | "dim";
|
|
7
|
+
export interface CliColors {
|
|
8
|
+
brand: (value: string) => string;
|
|
9
|
+
brandBold: (value: string) => string;
|
|
10
|
+
teal: ChalkInstance;
|
|
11
|
+
tealDim: ChalkInstance;
|
|
12
|
+
blue: ChalkInstance;
|
|
13
|
+
orange: ChalkInstance;
|
|
14
|
+
yellow: ChalkInstance;
|
|
15
|
+
red: ChalkInstance;
|
|
16
|
+
purple: ChalkInstance;
|
|
17
|
+
green: ChalkInstance;
|
|
18
|
+
text: ChalkInstance;
|
|
19
|
+
muted: ChalkInstance;
|
|
20
|
+
dim: ChalkInstance;
|
|
21
|
+
bold: (value: string) => string;
|
|
22
|
+
}
|
|
23
|
+
export declare function createColors(enabled: boolean): CliColors;
|
|
24
|
+
//# sourceMappingURL=colors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"colors.d.ts","sourceRoot":"","sources":["../../src/ui/colors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAElD,eAAO,MAAM,UAAU;;;CAGb,CAAC;AAEX,MAAM,MAAM,SAAS,GACjB,MAAM,GACN,SAAS,GACT,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,KAAK,GACL,QAAQ,GACR,OAAO,GACP,MAAM,GACN,OAAO,GACP,KAAK,CAAC;AAEV,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACjC,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACrC,IAAI,EAAE,aAAa,CAAC;IACpB,OAAO,EAAE,aAAa,CAAC;IACvB,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,EAAE,aAAa,CAAC;IACtB,GAAG,EAAE,aAAa,CAAC;IACnB,MAAM,EAAE,aAAa,CAAC;IACtB,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,EAAE,aAAa,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,GAAG,EAAE,aAAa,CAAC;IACnB,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;CACjC;AAiDD,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,CAmBxD"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Chalk } from "chalk";
|
|
2
|
+
export const LYNX_BRAND = {
|
|
3
|
+
pink: "#ff6b9d",
|
|
4
|
+
cyan: "#45b7d1",
|
|
5
|
+
};
|
|
6
|
+
function createColor(hex, enabled) {
|
|
7
|
+
return new Chalk({ level: enabled ? 3 : 0 }).hex(hex);
|
|
8
|
+
}
|
|
9
|
+
function hexToRgb(hex) {
|
|
10
|
+
const value = hex.replace("#", "");
|
|
11
|
+
return [
|
|
12
|
+
Number.parseInt(value.slice(0, 2), 16),
|
|
13
|
+
Number.parseInt(value.slice(2, 4), 16),
|
|
14
|
+
Number.parseInt(value.slice(4, 6), 16),
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
function rgbToHex(red, green, blue) {
|
|
18
|
+
return `#${[red, green, blue]
|
|
19
|
+
.map((channel) => channel.toString(16).padStart(2, "0"))
|
|
20
|
+
.join("")}`;
|
|
21
|
+
}
|
|
22
|
+
function createBrand(enabled) {
|
|
23
|
+
const brandChalk = new Chalk({ level: enabled ? 3 : 0 });
|
|
24
|
+
const [startRed, startGreen, startBlue] = hexToRgb(LYNX_BRAND.pink);
|
|
25
|
+
const [endRed, endGreen, endBlue] = hexToRgb(LYNX_BRAND.cyan);
|
|
26
|
+
return (value) => {
|
|
27
|
+
if (!enabled || value.length === 0)
|
|
28
|
+
return value;
|
|
29
|
+
const characters = [...value];
|
|
30
|
+
const visibleCount = characters.filter((character) => !/\s/u.test(character)).length;
|
|
31
|
+
let visibleIndex = 0;
|
|
32
|
+
return characters
|
|
33
|
+
.map((character) => {
|
|
34
|
+
if (/\s/u.test(character))
|
|
35
|
+
return character;
|
|
36
|
+
const ratio = visibleCount <= 1 ? 0 : visibleIndex / (visibleCount - 1);
|
|
37
|
+
visibleIndex += 1;
|
|
38
|
+
const red = Math.round(startRed + (endRed - startRed) * ratio);
|
|
39
|
+
const green = Math.round(startGreen + (endGreen - startGreen) * ratio);
|
|
40
|
+
const blue = Math.round(startBlue + (endBlue - startBlue) * ratio);
|
|
41
|
+
return brandChalk.hex(rgbToHex(red, green, blue))(character);
|
|
42
|
+
})
|
|
43
|
+
.join("");
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function createColors(enabled) {
|
|
47
|
+
const brand = createBrand(enabled);
|
|
48
|
+
return {
|
|
49
|
+
brand,
|
|
50
|
+
brandBold: (value) => new Chalk({ level: enabled ? 3 : 0 }).bold(brand(value)),
|
|
51
|
+
teal: createColor(LYNX_BRAND.cyan, enabled),
|
|
52
|
+
tealDim: createColor("#2f91ab", enabled),
|
|
53
|
+
blue: createColor("#72c7df", enabled),
|
|
54
|
+
orange: createColor("#ff9a8b", enabled),
|
|
55
|
+
yellow: createColor("#f6c15d", enabled),
|
|
56
|
+
red: createColor("#ff6b7a", enabled),
|
|
57
|
+
purple: createColor("#c5a0ff", enabled),
|
|
58
|
+
green: createColor("#58d6b4", enabled),
|
|
59
|
+
text: createColor("#f4f8fb", enabled),
|
|
60
|
+
muted: createColor("#91a8b8", enabled),
|
|
61
|
+
dim: createColor("#526b7c", enabled),
|
|
62
|
+
bold: new Chalk({ level: enabled ? 3 : 0 }).bold,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ColorName } from "./colors.js";
|
|
2
|
+
export interface BoxRow {
|
|
3
|
+
label: string;
|
|
4
|
+
value: string;
|
|
5
|
+
valueColor?: ColorName;
|
|
6
|
+
}
|
|
7
|
+
export declare function sectionHeader(title: string): void;
|
|
8
|
+
export declare const log: {
|
|
9
|
+
success(message: string): void;
|
|
10
|
+
warn(message: string): void;
|
|
11
|
+
error(message: string): void;
|
|
12
|
+
info(message: string): void;
|
|
13
|
+
debug(message: string): void;
|
|
14
|
+
};
|
|
15
|
+
export declare function summaryBox(title: string, rows: BoxRow[]): void;
|
|
16
|
+
export interface ProgressHandle {
|
|
17
|
+
event(message: string): void;
|
|
18
|
+
update(value?: number, label?: string): void;
|
|
19
|
+
stop(): void;
|
|
20
|
+
}
|
|
21
|
+
export declare function createProgress(label: string, enabled: boolean): ProgressHandle;
|
|
22
|
+
export interface SpinnerHandle {
|
|
23
|
+
succeed(message: string): void;
|
|
24
|
+
fail(message: string): void;
|
|
25
|
+
stop(): void;
|
|
26
|
+
}
|
|
27
|
+
export declare function spin(text: string, enabled: boolean): SpinnerHandle;
|
|
28
|
+
export declare const pill: {
|
|
29
|
+
success: (text: string) => string;
|
|
30
|
+
warn: (text: string) => string;
|
|
31
|
+
info: (text: string) => string;
|
|
32
|
+
error: (text: string) => string;
|
|
33
|
+
};
|
|
34
|
+
export declare function finalLine(message: string, success?: boolean): void;
|
|
35
|
+
export declare function downloadArtifact(url: string, expiresAt?: string, enabled?: boolean): void;
|
|
36
|
+
//# sourceMappingURL=components.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../src/ui/components.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAI7C,MAAM,WAAW,MAAM;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,SAAS,CAAC;CACxB;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAEjD;AAED,eAAO,MAAM,GAAG;qBACG,MAAM,GAAG,IAAI;kBAGhB,MAAM,GAAG,IAAI;mBAGZ,MAAM,GAAG,IAAI;kBAGd,MAAM,GAAG,IAAI;mBAGZ,MAAM,GAAG,IAAI;CAG7B,CAAC;AAgBF,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CA6C9D;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,IAAI,IAAI,IAAI,CAAC;CACd;AA2FD,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,OAAO,GACf,cAAc,CA8DhB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,IAAI,IAAI,CAAC;CACd;AAED,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,aAAa,CA2BlE;AAYD,eAAO,MAAM,IAAI;oBACC,MAAM;iBACT,MAAM;iBACN,MAAM;kBACL,MAAM;CACrB,CAAC;AAEF,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,UAAO,GAAG,IAAI,CAE/D;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,UAAO,GACb,IAAI,CAQN"}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import boxen from "boxen";
|
|
2
|
+
import { Chalk } from "chalk";
|
|
3
|
+
import cliProgress from "cli-progress";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import qrcode from "qrcode-terminal";
|
|
6
|
+
import { c, colorsEnabled } from "./state.js";
|
|
7
|
+
import { supportsUnicode, terminalWidth } from "./terminal.js";
|
|
8
|
+
export function sectionHeader(title) {
|
|
9
|
+
console.log(`\n${c.brandBold(`◆ LynxShip — ${title}`)}`);
|
|
10
|
+
}
|
|
11
|
+
export const log = {
|
|
12
|
+
success(message) {
|
|
13
|
+
console.log(` ${c.teal("[OK] ")} ${c.text(message)}`);
|
|
14
|
+
},
|
|
15
|
+
warn(message) {
|
|
16
|
+
console.log(` ${c.yellow("[WARN]")} ${c.yellow(message)}`);
|
|
17
|
+
},
|
|
18
|
+
error(message) {
|
|
19
|
+
console.error(` ${c.red("[ERROR]")} ${c.red(message)}`);
|
|
20
|
+
},
|
|
21
|
+
info(message) {
|
|
22
|
+
console.log(` ${c.muted(message)}`);
|
|
23
|
+
},
|
|
24
|
+
debug(message) {
|
|
25
|
+
console.log(` ${c.dim(`debug: ${message}`)}`);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const valueColors = {
|
|
29
|
+
teal: (value) => c.teal(value),
|
|
30
|
+
tealDim: (value) => c.tealDim(value),
|
|
31
|
+
blue: (value) => c.blue(value),
|
|
32
|
+
orange: (value) => c.orange(value),
|
|
33
|
+
yellow: (value) => c.yellow(value),
|
|
34
|
+
red: (value) => c.red(value),
|
|
35
|
+
purple: (value) => c.purple(value),
|
|
36
|
+
green: (value) => c.green(value),
|
|
37
|
+
text: (value) => c.text(value),
|
|
38
|
+
muted: (value) => c.muted(value),
|
|
39
|
+
dim: (value) => c.dim(value),
|
|
40
|
+
};
|
|
41
|
+
export function summaryBox(title, rows) {
|
|
42
|
+
const width = terminalWidth();
|
|
43
|
+
const labelWidth = Math.min(16, Math.max(...rows.map((row) => row.label.length), 0));
|
|
44
|
+
const plain = rows
|
|
45
|
+
.map((row) => `${row.label.padEnd(labelWidth)} ${row.value}`)
|
|
46
|
+
.join("\n");
|
|
47
|
+
if (width < 58) {
|
|
48
|
+
console.log(`\n${plain}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const content = rows
|
|
52
|
+
.map((row) => {
|
|
53
|
+
const label = c.muted(row.label.padEnd(labelWidth));
|
|
54
|
+
const value = row.valueColor
|
|
55
|
+
? valueColors[row.valueColor](row.value)
|
|
56
|
+
: c.text(row.value);
|
|
57
|
+
return `${label} ${value}`;
|
|
58
|
+
})
|
|
59
|
+
.join("\n");
|
|
60
|
+
// Measure the plain title first. Coloring the title before boxen measures it
|
|
61
|
+
// would make ANSI escape sequences affect the visible width of the border.
|
|
62
|
+
const rendered = boxen(content, {
|
|
63
|
+
width: Math.min(120, Math.max(56, width - 2)),
|
|
64
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
65
|
+
borderStyle: "round",
|
|
66
|
+
borderColor: "#29495b",
|
|
67
|
+
title,
|
|
68
|
+
titleAlignment: "left",
|
|
69
|
+
});
|
|
70
|
+
const firstLineEnd = rendered.indexOf("\n");
|
|
71
|
+
const firstLine = firstLineEnd === -1 ? rendered : rendered.slice(0, firstLineEnd);
|
|
72
|
+
const titleStart = firstLine.indexOf(title);
|
|
73
|
+
const coloredTitle = titleStart === -1
|
|
74
|
+
? firstLine
|
|
75
|
+
: `${firstLine.slice(0, titleStart)}${c.brand(title)}${firstLine.slice(titleStart + title.length)}`;
|
|
76
|
+
const coloredBox = firstLineEnd === -1
|
|
77
|
+
? coloredTitle
|
|
78
|
+
: `${coloredTitle}${rendered.slice(firstLineEnd)}`;
|
|
79
|
+
console.log(`\n${coloredBox}`);
|
|
80
|
+
}
|
|
81
|
+
function eventTone(message) {
|
|
82
|
+
const normalized = message.trim().toLowerCase();
|
|
83
|
+
if (normalized.startsWith("$ "))
|
|
84
|
+
return "command";
|
|
85
|
+
if (normalized.includes("error") ||
|
|
86
|
+
normalized.includes("failed") ||
|
|
87
|
+
normalized.includes("exception"))
|
|
88
|
+
return "error";
|
|
89
|
+
if (normalized.includes("warning") ||
|
|
90
|
+
normalized.includes("no-source") ||
|
|
91
|
+
normalized.includes("deprecated"))
|
|
92
|
+
return "warning";
|
|
93
|
+
if (normalized.includes("successful") ||
|
|
94
|
+
normalized.includes("ready") ||
|
|
95
|
+
normalized.startsWith("artifact ready") ||
|
|
96
|
+
normalized.startsWith("done"))
|
|
97
|
+
return "success";
|
|
98
|
+
if (normalized.startsWith("building ") ||
|
|
99
|
+
normalized.startsWith("checking ") ||
|
|
100
|
+
normalized.startsWith("syncing ") ||
|
|
101
|
+
normalized.startsWith("running ") ||
|
|
102
|
+
normalized.startsWith("uploading ") ||
|
|
103
|
+
normalized.startsWith("verifying ") ||
|
|
104
|
+
normalized.startsWith("build queued"))
|
|
105
|
+
return "step";
|
|
106
|
+
return "output";
|
|
107
|
+
}
|
|
108
|
+
function formatEvent(message) {
|
|
109
|
+
const colors = {
|
|
110
|
+
command: c.blue,
|
|
111
|
+
step: c.teal,
|
|
112
|
+
success: c.green,
|
|
113
|
+
warning: c.yellow,
|
|
114
|
+
error: c.red,
|
|
115
|
+
output: c.text,
|
|
116
|
+
};
|
|
117
|
+
const tone = eventTone(message);
|
|
118
|
+
const tag = {
|
|
119
|
+
command: "[CMD]",
|
|
120
|
+
step: "[STEP]",
|
|
121
|
+
success: "[OK]",
|
|
122
|
+
warning: "[WARN]",
|
|
123
|
+
error: "[ERROR]",
|
|
124
|
+
output: "[LOG]",
|
|
125
|
+
}[tone];
|
|
126
|
+
const normalized = message.trim().replace(/^\$\s*/, "");
|
|
127
|
+
const rail = supportsUnicode() ? "│" : "|";
|
|
128
|
+
return ` ${colors[tone](rail)} ${colors[tone](tag.padEnd(7))} ${colors[tone](normalized)}`;
|
|
129
|
+
}
|
|
130
|
+
const activeAnimations = new Set();
|
|
131
|
+
let cleanupHooksInstalled = false;
|
|
132
|
+
function installCleanupHooks() {
|
|
133
|
+
if (cleanupHooksInstalled)
|
|
134
|
+
return;
|
|
135
|
+
cleanupHooksInstalled = true;
|
|
136
|
+
const cleanup = () => {
|
|
137
|
+
for (const stop of activeAnimations)
|
|
138
|
+
stop();
|
|
139
|
+
activeAnimations.clear();
|
|
140
|
+
};
|
|
141
|
+
process.once("exit", cleanup);
|
|
142
|
+
process.once("SIGINT", () => {
|
|
143
|
+
cleanup();
|
|
144
|
+
process.exitCode = 130;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function trackAnimation(stop) {
|
|
148
|
+
installCleanupHooks();
|
|
149
|
+
activeAnimations.add(stop);
|
|
150
|
+
return () => activeAnimations.delete(stop);
|
|
151
|
+
}
|
|
152
|
+
export function createProgress(label, enabled) {
|
|
153
|
+
if (!enabled)
|
|
154
|
+
return {
|
|
155
|
+
event: () => undefined,
|
|
156
|
+
update: () => undefined,
|
|
157
|
+
stop: () => undefined,
|
|
158
|
+
};
|
|
159
|
+
const spinnerFrames = supportsUnicode()
|
|
160
|
+
? ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
161
|
+
: ["|", "/", "-", "\\"];
|
|
162
|
+
let spinnerIndex = 0;
|
|
163
|
+
let currentValue = 0;
|
|
164
|
+
let hasMeasuredValue = false;
|
|
165
|
+
const formatPercent = (value) => `${value.toFixed(2).replace(/\.?0+$/, "")}%`;
|
|
166
|
+
const bar = new cliProgress.SingleBar({
|
|
167
|
+
format: ` {spinner} {stage} ${c.teal("{bar}")} ${c.muted("{percent}")}`,
|
|
168
|
+
barCompleteChar: "█",
|
|
169
|
+
barIncompleteChar: "░",
|
|
170
|
+
barsize: 20,
|
|
171
|
+
hideCursor: true,
|
|
172
|
+
clearOnComplete: true,
|
|
173
|
+
});
|
|
174
|
+
const stage = (value) => c.muted(value.padEnd(38));
|
|
175
|
+
const payload = (value) => ({
|
|
176
|
+
stage: stage(value),
|
|
177
|
+
spinner: c.brand(spinnerFrames[spinnerIndex] ?? ""),
|
|
178
|
+
percent: hasMeasuredValue ? formatPercent(currentValue) : "",
|
|
179
|
+
});
|
|
180
|
+
let currentLabel = label;
|
|
181
|
+
bar.start(100, currentValue, payload(currentLabel));
|
|
182
|
+
const timer = setInterval(() => {
|
|
183
|
+
spinnerIndex = (spinnerIndex + 1) % spinnerFrames.length;
|
|
184
|
+
bar.update(currentValue, payload(currentLabel));
|
|
185
|
+
}, 120);
|
|
186
|
+
timer.unref?.();
|
|
187
|
+
const untrack = trackAnimation(() => {
|
|
188
|
+
clearInterval(timer);
|
|
189
|
+
bar.stop();
|
|
190
|
+
});
|
|
191
|
+
const logEvent = (message) => {
|
|
192
|
+
if (!bar.isActive)
|
|
193
|
+
return;
|
|
194
|
+
bar.stop();
|
|
195
|
+
console.log(formatEvent(message));
|
|
196
|
+
bar.start(100, currentValue, payload(currentLabel));
|
|
197
|
+
};
|
|
198
|
+
return {
|
|
199
|
+
event: logEvent,
|
|
200
|
+
update: (value, nextLabel) => {
|
|
201
|
+
if (value !== undefined) {
|
|
202
|
+
currentValue = Math.min(100, Math.max(0, value));
|
|
203
|
+
hasMeasuredValue = true;
|
|
204
|
+
}
|
|
205
|
+
if (nextLabel)
|
|
206
|
+
currentLabel = nextLabel;
|
|
207
|
+
bar.update(currentValue, payload(currentLabel));
|
|
208
|
+
},
|
|
209
|
+
stop: () => {
|
|
210
|
+
clearInterval(timer);
|
|
211
|
+
bar.stop();
|
|
212
|
+
untrack();
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
export function spin(text, enabled) {
|
|
217
|
+
if (!enabled)
|
|
218
|
+
return {
|
|
219
|
+
succeed: () => undefined,
|
|
220
|
+
fail: () => undefined,
|
|
221
|
+
stop: () => undefined,
|
|
222
|
+
};
|
|
223
|
+
const spinner = ora({
|
|
224
|
+
text: c.brand(text),
|
|
225
|
+
spinner: "dots",
|
|
226
|
+
color: "cyan",
|
|
227
|
+
}).start();
|
|
228
|
+
const untrack = trackAnimation(() => spinner.stop());
|
|
229
|
+
return {
|
|
230
|
+
succeed: (message) => {
|
|
231
|
+
spinner.succeed(`${c.teal("[OK]")} ${c.text(message)}`);
|
|
232
|
+
untrack();
|
|
233
|
+
},
|
|
234
|
+
fail: (message) => {
|
|
235
|
+
spinner.fail(`${c.red("[ERROR]")} ${c.red(message)}`);
|
|
236
|
+
untrack();
|
|
237
|
+
},
|
|
238
|
+
stop: () => {
|
|
239
|
+
spinner.stop();
|
|
240
|
+
untrack();
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function pillStyle(background, foreground, text) {
|
|
245
|
+
return new Chalk({ level: colorsEnabled ? 3 : 0 })
|
|
246
|
+
.bgHex(background)
|
|
247
|
+
.hex(foreground)(` ${text} `);
|
|
248
|
+
}
|
|
249
|
+
export const pill = {
|
|
250
|
+
success: (text) => pillStyle("#0d2e22", "#10B981", text),
|
|
251
|
+
warn: (text) => pillStyle("#2e2208", "#FFD166", text),
|
|
252
|
+
info: (text) => pillStyle("#0a1f30", "#4F9EFF", text),
|
|
253
|
+
error: (text) => pillStyle("#2e0d10", "#FF4757", text),
|
|
254
|
+
};
|
|
255
|
+
export function finalLine(message, success = true) {
|
|
256
|
+
console.log(`\n${success ? c.brand("◆") : c.red("◆")} ${c.text(message)}`);
|
|
257
|
+
}
|
|
258
|
+
export function downloadArtifact(url, expiresAt, enabled = true) {
|
|
259
|
+
if (!enabled)
|
|
260
|
+
return;
|
|
261
|
+
console.log(`\n${c.brandBold("Download artifact")}`);
|
|
262
|
+
// qrcode-terminal's small mode is the safest compact terminal renderer.
|
|
263
|
+
// A shorter QR requires a shorter download URL, not fewer QR modules.
|
|
264
|
+
qrcode.generate(url, { small: true }, (code) => console.log(code));
|
|
265
|
+
console.log(` ${c.teal("URL")} ${url}`);
|
|
266
|
+
if (expiresAt)
|
|
267
|
+
console.log(` ${c.muted(`Link expires at ${expiresAt}`)}`);
|
|
268
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type BoxRow, type ProgressHandle, type SpinnerHandle } from "./components.js";
|
|
2
|
+
import { terminalOptions, type TerminalOptions } from "./terminal.js";
|
|
3
|
+
export declare class CliUi {
|
|
4
|
+
readonly options: TerminalOptions;
|
|
5
|
+
readonly interactive: boolean;
|
|
6
|
+
constructor(args: string[]);
|
|
7
|
+
banner(): void;
|
|
8
|
+
header(title: string): void;
|
|
9
|
+
success(message: string): void;
|
|
10
|
+
warn(message: string): void;
|
|
11
|
+
info(message: string): void;
|
|
12
|
+
debug(message: string): void;
|
|
13
|
+
error(message: string): void;
|
|
14
|
+
summary(title: string, rows: BoxRow[]): void;
|
|
15
|
+
configurationStatus(rows: BoxRow[]): void;
|
|
16
|
+
progress(label: string): ProgressHandle;
|
|
17
|
+
spinner(text: string): SpinnerHandle;
|
|
18
|
+
done(message: string, success?: boolean): void;
|
|
19
|
+
downloadArtifact(url: string, expiresAt?: string): void;
|
|
20
|
+
}
|
|
21
|
+
export declare function createCliUi(args: string[]): CliUi;
|
|
22
|
+
export { terminalOptions };
|
|
23
|
+
export type { BoxRow, TerminalOptions };
|
|
24
|
+
//# sourceMappingURL=index.d.ts.map
|