@levr-one/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 +172 -0
- package/dist/app-0F0Zyyt7.js +350 -0
- package/dist/bash-complete.js +13 -0
- package/dist/cli.js +15 -0
- package/dist/credentials-CfHLkU7k.js +54 -0
- package/dist/currentHandler-oXFeNNgd.js +11 -0
- package/dist/listHandler-BiwIo88i.js +44 -0
- package/dist/loginHandler-C8D6DgjW.js +363 -0
- package/dist/logoutHandler-BBtdpxHX.js +14 -0
- package/dist/pushHandler-DkCrbV7y.js +529 -0
- package/dist/resolve-token-BL8vL_ok.js +33 -0
- package/dist/resolve-workspace-lHEoGPHe.js +89 -0
- package/dist/sdk-client-DunBmYLR.js +30559 -0
- package/dist/selectHandler-C2UDAWOl.js +40 -0
- package/dist/statusHandler-DQDmEOGI.js +51 -0
- package/dist/token-refresh-waF23pyw.js +48 -0
- package/dist/workspace-store-BcyMJAht.js +29 -0
- package/package.json +54 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import { getApiUrl, getAutomationSourceIdOverride, getSourceOverride, getTeamId } from "./credentials-CfHLkU7k.js";
|
|
2
|
+
import { client, configureClient, uploadAutomationIngest, uploadImport } from "./sdk-client-DunBmYLR.js";
|
|
3
|
+
import "./workspace-store-BcyMJAht.js";
|
|
4
|
+
import { resolveWorkspace } from "./resolve-workspace-lHEoGPHe.js";
|
|
5
|
+
import "./token-refresh-waF23pyw.js";
|
|
6
|
+
import { resolveToken } from "./resolve-token-BL8vL_ok.js";
|
|
7
|
+
import { readFileSync, statSync } from "node:fs";
|
|
8
|
+
import { basename } from "node:path";
|
|
9
|
+
import ora from "ora";
|
|
10
|
+
import { execSync } from "node:child_process";
|
|
11
|
+
|
|
12
|
+
//#region ../ci-env/dist/providers/github.js
|
|
13
|
+
const MAX_EVENT_PAYLOAD_BYTES = 5 * 1024 * 1024;
|
|
14
|
+
const PR_EVENT_NAMES = new Set(["pull_request", "pull_request_target"]);
|
|
15
|
+
function readPullRequestEvent(eventPath) {
|
|
16
|
+
if (!eventPath) return void 0;
|
|
17
|
+
try {
|
|
18
|
+
if (statSync(eventPath).size > MAX_EVENT_PAYLOAD_BYTES) return void 0;
|
|
19
|
+
const raw = readFileSync(eventPath, "utf8");
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
} catch {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const github = {
|
|
26
|
+
name: "github_actions",
|
|
27
|
+
detect(env) {
|
|
28
|
+
return env.GITHUB_ACTIONS === "true";
|
|
29
|
+
},
|
|
30
|
+
extract(env) {
|
|
31
|
+
const isPr = !!env.GITHUB_EVENT_NAME && PR_EVENT_NAMES.has(env.GITHUB_EVENT_NAME);
|
|
32
|
+
const ref = env.GITHUB_REF ?? "";
|
|
33
|
+
const prPayload = isPr ? readPullRequestEvent(env.GITHUB_EVENT_PATH) : void 0;
|
|
34
|
+
const refMatch = isPr ? /^refs\/pull\/(\d+)\/merge$/.exec(ref) : null;
|
|
35
|
+
const prNumber = prPayload?.pull_request?.number?.toString() ?? refMatch?.[1];
|
|
36
|
+
const serverUrl = env.GITHUB_SERVER_URL ?? "https://github.com";
|
|
37
|
+
const repo = env.GITHUB_REPOSITORY ?? "";
|
|
38
|
+
const commitSha = env.GITHUB_PULL_REQUEST_HEAD_SHA ?? prPayload?.pull_request?.head?.sha ?? env.GITHUB_SHA;
|
|
39
|
+
return {
|
|
40
|
+
ci_provider: "github_actions",
|
|
41
|
+
ci_build_id: env.GITHUB_RUN_ID,
|
|
42
|
+
ci_build_number: env.GITHUB_RUN_NUMBER,
|
|
43
|
+
ci_build_url: repo ? `${serverUrl}/${repo}/actions/runs/${env.GITHUB_RUN_ID}` : void 0,
|
|
44
|
+
ci_job_name: env.GITHUB_JOB,
|
|
45
|
+
ci_job_url: repo ? `${serverUrl}/${repo}/actions/runs/${env.GITHUB_RUN_ID}` : void 0,
|
|
46
|
+
commit_sha: commitSha,
|
|
47
|
+
commit_author: env.GITHUB_ACTOR,
|
|
48
|
+
branch: isPr ? env.GITHUB_HEAD_REF : ref.startsWith("refs/heads/") ? ref.replace(/^refs\/heads\//, "") : void 0,
|
|
49
|
+
tag: ref.startsWith("refs/tags/") ? ref.replace(/^refs\/tags\//, "") : void 0,
|
|
50
|
+
is_pr: isPr,
|
|
51
|
+
pr_number: prNumber,
|
|
52
|
+
pr_branch: isPr ? env.GITHUB_HEAD_REF : void 0,
|
|
53
|
+
pr_target_branch: isPr ? env.GITHUB_BASE_REF : void 0,
|
|
54
|
+
repository_url: repo ? `${serverUrl}/${repo}` : void 0,
|
|
55
|
+
repository_slug: repo || void 0,
|
|
56
|
+
runner_os: env.RUNNER_OS?.toLowerCase(),
|
|
57
|
+
runner_arch: env.RUNNER_ARCH?.toLowerCase(),
|
|
58
|
+
runner_name: env.RUNNER_NAME
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region ../ci-env/dist/providers/gitlab.js
|
|
65
|
+
const gitlab = {
|
|
66
|
+
name: "gitlab_ci",
|
|
67
|
+
detect(env) {
|
|
68
|
+
return env.GITLAB_CI === "true";
|
|
69
|
+
},
|
|
70
|
+
extract(env) {
|
|
71
|
+
const isPr = !!env.CI_MERGE_REQUEST_IID;
|
|
72
|
+
return {
|
|
73
|
+
ci_provider: "gitlab_ci",
|
|
74
|
+
ci_build_id: env.CI_PIPELINE_ID,
|
|
75
|
+
ci_build_number: env.CI_PIPELINE_IID,
|
|
76
|
+
ci_build_url: env.CI_PIPELINE_URL,
|
|
77
|
+
ci_job_name: env.CI_JOB_NAME,
|
|
78
|
+
ci_job_url: env.CI_JOB_URL,
|
|
79
|
+
commit_sha: env.CI_COMMIT_SHA,
|
|
80
|
+
commit_message: env.CI_COMMIT_MESSAGE,
|
|
81
|
+
commit_author: env.CI_COMMIT_AUTHOR,
|
|
82
|
+
branch: env.CI_COMMIT_BRANCH ?? env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME,
|
|
83
|
+
tag: env.CI_COMMIT_TAG,
|
|
84
|
+
is_pr: isPr,
|
|
85
|
+
pr_number: env.CI_MERGE_REQUEST_IID,
|
|
86
|
+
pr_branch: env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME,
|
|
87
|
+
pr_target_branch: env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME,
|
|
88
|
+
repository_url: env.CI_PROJECT_URL,
|
|
89
|
+
repository_slug: env.CI_PROJECT_PATH,
|
|
90
|
+
runner_name: env.CI_RUNNER_DESCRIPTION
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region ../ci-env/dist/providers/jenkins.js
|
|
97
|
+
const jenkins = {
|
|
98
|
+
name: "jenkins",
|
|
99
|
+
detect(env) {
|
|
100
|
+
return !!env.JENKINS_URL;
|
|
101
|
+
},
|
|
102
|
+
extract(env) {
|
|
103
|
+
const isPr = !!env.CHANGE_ID;
|
|
104
|
+
return {
|
|
105
|
+
ci_provider: "jenkins",
|
|
106
|
+
ci_build_id: env.BUILD_ID,
|
|
107
|
+
ci_build_number: env.BUILD_NUMBER,
|
|
108
|
+
ci_build_url: env.BUILD_URL,
|
|
109
|
+
ci_job_name: env.JOB_NAME,
|
|
110
|
+
ci_job_url: env.JOB_URL,
|
|
111
|
+
commit_sha: env.GIT_COMMIT,
|
|
112
|
+
branch: env.GIT_BRANCH ?? env.BRANCH_NAME,
|
|
113
|
+
is_pr: isPr,
|
|
114
|
+
pr_number: env.CHANGE_ID,
|
|
115
|
+
pr_branch: env.CHANGE_BRANCH,
|
|
116
|
+
pr_target_branch: env.CHANGE_TARGET,
|
|
117
|
+
repository_url: env.GIT_URL
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region ../ci-env/dist/providers/circleci.js
|
|
124
|
+
const circleci = {
|
|
125
|
+
name: "circleci",
|
|
126
|
+
detect(env) {
|
|
127
|
+
return env.CIRCLECI === "true";
|
|
128
|
+
},
|
|
129
|
+
extract(env) {
|
|
130
|
+
const isPr = !!env.CIRCLE_PULL_REQUEST;
|
|
131
|
+
const prNumber = (env.CIRCLE_PULL_REQUEST ?? "").split("/").pop();
|
|
132
|
+
return {
|
|
133
|
+
ci_provider: "circleci",
|
|
134
|
+
ci_build_id: env.CIRCLE_WORKFLOW_ID,
|
|
135
|
+
ci_build_number: env.CIRCLE_BUILD_NUM,
|
|
136
|
+
ci_build_url: env.CIRCLE_BUILD_URL,
|
|
137
|
+
ci_job_name: env.CIRCLE_JOB,
|
|
138
|
+
commit_sha: env.CIRCLE_SHA1,
|
|
139
|
+
branch: env.CIRCLE_BRANCH,
|
|
140
|
+
tag: env.CIRCLE_TAG,
|
|
141
|
+
is_pr: isPr,
|
|
142
|
+
pr_number: isPr ? prNumber : void 0,
|
|
143
|
+
repository_url: env.CIRCLE_REPOSITORY_URL,
|
|
144
|
+
repository_slug: env.CIRCLE_PROJECT_USERNAME && env.CIRCLE_PROJECT_REPONAME ? `${env.CIRCLE_PROJECT_USERNAME}/${env.CIRCLE_PROJECT_REPONAME}` : void 0
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region ../ci-env/dist/providers/azure.js
|
|
151
|
+
const azure = {
|
|
152
|
+
name: "azure_devops",
|
|
153
|
+
detect(env) {
|
|
154
|
+
return env.TF_BUILD === "True";
|
|
155
|
+
},
|
|
156
|
+
extract(env) {
|
|
157
|
+
const isPr = env.BUILD_REASON === "PullRequest" || !!env.SYSTEM_PULLREQUEST_PULLREQUESTID;
|
|
158
|
+
const orgUrl = env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI ?? "";
|
|
159
|
+
const project = env.SYSTEM_TEAMPROJECT ?? "";
|
|
160
|
+
const buildId = env.BUILD_BUILDID ?? "";
|
|
161
|
+
return {
|
|
162
|
+
ci_provider: "azure_devops",
|
|
163
|
+
ci_build_id: buildId,
|
|
164
|
+
ci_build_number: env.BUILD_BUILDNUMBER,
|
|
165
|
+
ci_build_url: orgUrl && project && buildId ? `${orgUrl}${project}/_build/results?buildId=${buildId}` : void 0,
|
|
166
|
+
ci_job_name: env.AGENT_JOBNAME,
|
|
167
|
+
commit_sha: env.BUILD_SOURCEVERSION,
|
|
168
|
+
commit_message: env.BUILD_SOURCEVERSIONMESSAGE,
|
|
169
|
+
branch: env.BUILD_SOURCEBRANCH?.replace(/^refs\/heads\//, ""),
|
|
170
|
+
is_pr: isPr,
|
|
171
|
+
pr_number: env.SYSTEM_PULLREQUEST_PULLREQUESTID,
|
|
172
|
+
pr_branch: env.SYSTEM_PULLREQUEST_SOURCEBRANCH?.replace(/^refs\/heads\//, ""),
|
|
173
|
+
pr_target_branch: env.SYSTEM_PULLREQUEST_TARGETBRANCH?.replace(/^refs\/heads\//, ""),
|
|
174
|
+
repository_url: env.BUILD_REPOSITORY_URI,
|
|
175
|
+
repository_slug: env.BUILD_REPOSITORY_NAME,
|
|
176
|
+
runner_os: env.AGENT_OS?.toLowerCase(),
|
|
177
|
+
runner_name: env.AGENT_NAME
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
//#endregion
|
|
183
|
+
//#region ../ci-env/dist/providers/buildkite.js
|
|
184
|
+
const buildkite = {
|
|
185
|
+
name: "buildkite",
|
|
186
|
+
detect(env) {
|
|
187
|
+
return env.BUILDKITE === "true";
|
|
188
|
+
},
|
|
189
|
+
extract(env) {
|
|
190
|
+
const isPr = env.BUILDKITE_PULL_REQUEST !== "false" && !!env.BUILDKITE_PULL_REQUEST;
|
|
191
|
+
return {
|
|
192
|
+
ci_provider: "buildkite",
|
|
193
|
+
ci_build_id: env.BUILDKITE_BUILD_ID,
|
|
194
|
+
ci_build_number: env.BUILDKITE_BUILD_NUMBER,
|
|
195
|
+
ci_build_url: env.BUILDKITE_BUILD_URL,
|
|
196
|
+
ci_job_name: env.BUILDKITE_LABEL ?? env.BUILDKITE_STEP_KEY,
|
|
197
|
+
commit_sha: env.BUILDKITE_COMMIT,
|
|
198
|
+
commit_message: env.BUILDKITE_MESSAGE,
|
|
199
|
+
commit_author: env.BUILDKITE_BUILD_AUTHOR,
|
|
200
|
+
branch: env.BUILDKITE_BRANCH,
|
|
201
|
+
tag: env.BUILDKITE_TAG,
|
|
202
|
+
is_pr: isPr,
|
|
203
|
+
pr_number: isPr ? env.BUILDKITE_PULL_REQUEST : void 0,
|
|
204
|
+
pr_branch: isPr ? env.BUILDKITE_BRANCH : void 0,
|
|
205
|
+
pr_target_branch: isPr ? env.BUILDKITE_PULL_REQUEST_BASE_BRANCH : void 0,
|
|
206
|
+
repository_url: env.BUILDKITE_REPO,
|
|
207
|
+
repository_slug: env.BUILDKITE_PIPELINE_SLUG ? `${env.BUILDKITE_ORGANIZATION_SLUG}/${env.BUILDKITE_PIPELINE_SLUG}` : void 0,
|
|
208
|
+
runner_name: env.BUILDKITE_AGENT_NAME
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region ../ci-env/dist/providers/bitbucket.js
|
|
215
|
+
const bitbucket = {
|
|
216
|
+
name: "bitbucket",
|
|
217
|
+
detect(env) {
|
|
218
|
+
return !!env.BITBUCKET_BUILD_NUMBER;
|
|
219
|
+
},
|
|
220
|
+
extract(env) {
|
|
221
|
+
const isPr = !!env.BITBUCKET_PR_ID;
|
|
222
|
+
const workspace = env.BITBUCKET_WORKSPACE ?? "";
|
|
223
|
+
const repo = env.BITBUCKET_REPO_SLUG ?? "";
|
|
224
|
+
return {
|
|
225
|
+
ci_provider: "bitbucket",
|
|
226
|
+
ci_build_id: env.BITBUCKET_PIPELINE_UUID,
|
|
227
|
+
ci_build_number: env.BITBUCKET_BUILD_NUMBER,
|
|
228
|
+
ci_build_url: workspace && repo && env.BITBUCKET_BUILD_NUMBER ? `https://bitbucket.org/${workspace}/${repo}/pipelines/results/${env.BITBUCKET_BUILD_NUMBER}` : void 0,
|
|
229
|
+
ci_job_name: env.BITBUCKET_STEP_UUID,
|
|
230
|
+
commit_sha: env.BITBUCKET_COMMIT,
|
|
231
|
+
branch: env.BITBUCKET_BRANCH,
|
|
232
|
+
tag: env.BITBUCKET_TAG,
|
|
233
|
+
is_pr: isPr,
|
|
234
|
+
pr_number: env.BITBUCKET_PR_ID,
|
|
235
|
+
pr_branch: isPr ? env.BITBUCKET_BRANCH : void 0,
|
|
236
|
+
pr_target_branch: env.BITBUCKET_PR_DESTINATION_BRANCH,
|
|
237
|
+
repository_url: workspace && repo ? `https://bitbucket.org/${workspace}/${repo}` : void 0,
|
|
238
|
+
repository_slug: workspace && repo ? `${workspace}/${repo}` : void 0
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
//#endregion
|
|
244
|
+
//#region ../ci-env/dist/providers/generic.js
|
|
245
|
+
/**
|
|
246
|
+
* Generic CI fallback — detects basic CI environment using common env vars.
|
|
247
|
+
* Used when no specific provider is matched.
|
|
248
|
+
*/
|
|
249
|
+
const generic = {
|
|
250
|
+
name: "generic_ci",
|
|
251
|
+
detect(env) {
|
|
252
|
+
return env.CI === "true" || env.CI === "1" || env.CONTINUOUS_INTEGRATION === "true";
|
|
253
|
+
},
|
|
254
|
+
extract(env) {
|
|
255
|
+
return {
|
|
256
|
+
ci_provider: "generic_ci",
|
|
257
|
+
ci_build_id: env.BUILD_ID ?? env.BUILD_NUMBER,
|
|
258
|
+
ci_build_number: env.BUILD_NUMBER,
|
|
259
|
+
ci_build_url: env.BUILD_URL,
|
|
260
|
+
commit_sha: env.GIT_COMMIT ?? env.COMMIT_SHA,
|
|
261
|
+
commit_author: env.GIT_AUTHOR_NAME,
|
|
262
|
+
branch: env.GIT_BRANCH ?? env.BRANCH_NAME ?? env.BRANCH,
|
|
263
|
+
tag: env.GIT_TAG ?? env.TAG_NAME,
|
|
264
|
+
repository_url: env.GIT_URL ?? env.REPOSITORY_URL
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region ../ci-env/dist/workspace-path.js
|
|
271
|
+
/**
|
|
272
|
+
* Detect the workspace path relative to the git repository root.
|
|
273
|
+
* Useful for monorepos to distinguish which sub-project ran tests.
|
|
274
|
+
*
|
|
275
|
+
* Returns undefined if not in a git repository or git is unavailable.
|
|
276
|
+
*/
|
|
277
|
+
function detectWorkspacePath() {
|
|
278
|
+
try {
|
|
279
|
+
return execSync("git rev-parse --show-prefix", {
|
|
280
|
+
encoding: "utf-8",
|
|
281
|
+
timeout: 5e3,
|
|
282
|
+
stdio: [
|
|
283
|
+
"pipe",
|
|
284
|
+
"pipe",
|
|
285
|
+
"pipe"
|
|
286
|
+
]
|
|
287
|
+
}).trim().replace(/\/$/, "") || void 0;
|
|
288
|
+
} catch {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region ../ci-env/dist/detect.js
|
|
295
|
+
/**
|
|
296
|
+
* Ordered list of CI providers to check.
|
|
297
|
+
* More specific providers first, generic fallback last.
|
|
298
|
+
*/
|
|
299
|
+
const PROVIDERS = [
|
|
300
|
+
github,
|
|
301
|
+
gitlab,
|
|
302
|
+
jenkins,
|
|
303
|
+
circleci,
|
|
304
|
+
azure,
|
|
305
|
+
buildkite,
|
|
306
|
+
bitbucket,
|
|
307
|
+
generic
|
|
308
|
+
];
|
|
309
|
+
let cachedResult;
|
|
310
|
+
/**
|
|
311
|
+
* Detect the CI environment from environment variables.
|
|
312
|
+
*
|
|
313
|
+
* @param env - Environment variables to inspect. Defaults to `process.env`.
|
|
314
|
+
* @returns Normalized CI environment metadata, or `null` if not running in CI.
|
|
315
|
+
*/
|
|
316
|
+
function detectCiEnvironment(env) {
|
|
317
|
+
if (env === void 0 && cachedResult !== void 0) return cachedResult;
|
|
318
|
+
const source = env ?? process.env;
|
|
319
|
+
for (const provider of PROVIDERS) if (provider.detect(source)) {
|
|
320
|
+
const result = provider.extract(source);
|
|
321
|
+
if (!result.workspace_path) result.workspace_path = detectWorkspacePath();
|
|
322
|
+
if (!result.runner_os) result.runner_os = process.platform;
|
|
323
|
+
if (!result.runner_arch) result.runner_arch = process.arch;
|
|
324
|
+
const cleaned = Object.fromEntries(Object.entries(result).filter(([, v]) => v !== void 0));
|
|
325
|
+
if (env === void 0) cachedResult = cleaned;
|
|
326
|
+
return cleaned;
|
|
327
|
+
}
|
|
328
|
+
if (env === void 0) cachedResult = null;
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/utils/ci-detect.ts
|
|
334
|
+
/**
|
|
335
|
+
* Auto-detect source name from CI environment.
|
|
336
|
+
* Returns undefined if not running in CI.
|
|
337
|
+
*/
|
|
338
|
+
function detectSource() {
|
|
339
|
+
const ci = detectCiEnvironment();
|
|
340
|
+
if (!ci) return void 0;
|
|
341
|
+
const jobName = process.env["GITHUB_WORKFLOW"] ?? ci.ci_job_name;
|
|
342
|
+
const repo = ci.repository_slug?.split("/").pop();
|
|
343
|
+
if (repo && jobName) return `${repo}/${jobName}`;
|
|
344
|
+
if (jobName) return jobName;
|
|
345
|
+
if (repo) return repo;
|
|
346
|
+
return ci.ci_provider;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Get normalized CI metadata for the import endpoint.
|
|
350
|
+
* Field names match `run_context` columns so the backend can
|
|
351
|
+
* map them directly to structured columns.
|
|
352
|
+
*
|
|
353
|
+
* Returns undefined if not running in CI.
|
|
354
|
+
*/
|
|
355
|
+
function getCiMetadata() {
|
|
356
|
+
return detectCiEnvironment() ?? void 0;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
//#endregion
|
|
360
|
+
//#region src/commands/pushHandler.ts
|
|
361
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
362
|
+
function formatBytes(bytes) {
|
|
363
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
364
|
+
const kb = bytes / 1024;
|
|
365
|
+
if (kb < 1024) return `${kb.toFixed(1)}KB`;
|
|
366
|
+
return `${(kb / 1024).toFixed(1)}MB`;
|
|
367
|
+
}
|
|
368
|
+
async function pushHandler(flags, file) {
|
|
369
|
+
if (flags.verbose) this.logger.setVerbose(true);
|
|
370
|
+
let auth;
|
|
371
|
+
try {
|
|
372
|
+
auth = await resolveToken();
|
|
373
|
+
} catch (err) {
|
|
374
|
+
this.logger.error(err instanceof Error ? err.message : "Authentication failed.");
|
|
375
|
+
this.process.exitCode = 1;
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
configureClient(auth);
|
|
379
|
+
if (flags.verbose) {
|
|
380
|
+
this.logger.debug(`Auth: ${auth.type.toUpperCase()}`);
|
|
381
|
+
this.logger.debug(`API: ${getApiUrl()}`);
|
|
382
|
+
}
|
|
383
|
+
if (auth.type === "jwt") try {
|
|
384
|
+
const ws = await resolveWorkspace(flags["workspace-id"]);
|
|
385
|
+
client.setConfig({
|
|
386
|
+
...client.getConfig(),
|
|
387
|
+
workspaceId: ws.workspaceId
|
|
388
|
+
});
|
|
389
|
+
if (flags.verbose) this.logger.debug(`Workspace: ${ws.workspaceId} (${ws.source})`);
|
|
390
|
+
} catch (err) {
|
|
391
|
+
this.logger.error(err instanceof Error ? err.message : "Workspace resolution failed.");
|
|
392
|
+
this.process.exitCode = 1;
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
let fileStat;
|
|
396
|
+
try {
|
|
397
|
+
fileStat = statSync(file);
|
|
398
|
+
} catch {
|
|
399
|
+
this.logger.error(`File not found: ${file}`);
|
|
400
|
+
this.process.exitCode = 1;
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (fileStat.size > MAX_FILE_SIZE) {
|
|
404
|
+
const sizeMB = (fileStat.size / (1024 * 1024)).toFixed(1);
|
|
405
|
+
this.logger.error(`File too large (${sizeMB}MB). Maximum is 10MB.`);
|
|
406
|
+
this.process.exitCode = 1;
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const teamId = getTeamId(flags["team-id"]);
|
|
410
|
+
const sourceName = flags.source ?? getSourceOverride() ?? detectSource();
|
|
411
|
+
let sourceOrigin;
|
|
412
|
+
if (flags.source) sourceOrigin = "explicit";
|
|
413
|
+
else if (getSourceOverride()) sourceOrigin = "LEVR_SOURCE";
|
|
414
|
+
else if (sourceName) sourceOrigin = "auto-detected";
|
|
415
|
+
if (!sourceName) {
|
|
416
|
+
this.logger.error("Error: --source is required. Provide it explicitly with --source, set the LEVR_SOURCE env var, or run in a supported CI environment for auto-detection.");
|
|
417
|
+
this.logger.error("");
|
|
418
|
+
this.logger.error("Example:");
|
|
419
|
+
this.logger.error(" levr push results.xml --source backend-unit-tests");
|
|
420
|
+
this.process.exitCode = 1;
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const automationSourceId = flags["automation-source"] ?? getAutomationSourceIdOverride();
|
|
424
|
+
const automationSourceOrigin = flags["automation-source"] ? "explicit" : automationSourceId ? "LEVR_AUTOMATION_SOURCE_ID" : void 0;
|
|
425
|
+
const ciMeta = getCiMetadata();
|
|
426
|
+
if (flags.verbose) {
|
|
427
|
+
this.logger.debug(`Team: ${teamId ?? "(server default)"}`);
|
|
428
|
+
this.logger.debug(`File: ${file} (${formatBytes(fileStat.size)})`);
|
|
429
|
+
if (flags.format) this.logger.debug(`Format: ${flags.format}`);
|
|
430
|
+
if (flags["update-mode"]) this.logger.debug(`Update mode: ${flags["update-mode"]}`);
|
|
431
|
+
if (sourceName) this.logger.debug(`Source: ${sourceName} (${sourceOrigin})`);
|
|
432
|
+
if (automationSourceId) this.logger.debug(`Automation source: ${automationSourceId} (${automationSourceOrigin}) → POST /v1/automation-run/ingest`);
|
|
433
|
+
if (ciMeta) {
|
|
434
|
+
this.logger.debug(`CI detected: ${ciMeta.ci_provider?.replace(/_/g, " ") ?? "unknown"}`);
|
|
435
|
+
if (ciMeta.branch) this.logger.debug(`Branch: ${ciMeta.branch}`);
|
|
436
|
+
if (ciMeta.commit_sha) this.logger.debug(`Commit: ${ciMeta.commit_sha.slice(0, 7)}`);
|
|
437
|
+
if (ciMeta.ci_build_id) this.logger.debug(`Build: ${ciMeta.ci_build_id}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const fileName = basename(file);
|
|
441
|
+
this.process.stdout.write(`Pushing ${fileName}...\n`);
|
|
442
|
+
const spinner = ora({
|
|
443
|
+
text: "Uploading...",
|
|
444
|
+
stream: this.process.stdout
|
|
445
|
+
}).start();
|
|
446
|
+
try {
|
|
447
|
+
const fileBuffer = readFileSync(file);
|
|
448
|
+
const fileObj = new File([fileBuffer], fileName);
|
|
449
|
+
if (automationSourceId) {
|
|
450
|
+
const ingestResult = await uploadAutomationIngest({
|
|
451
|
+
file: fileObj,
|
|
452
|
+
fileName,
|
|
453
|
+
automationSourceId,
|
|
454
|
+
runName: flags["run-name"],
|
|
455
|
+
format: flags.format,
|
|
456
|
+
externalRunKey: ciMeta?.ci_build_id,
|
|
457
|
+
importMetadata: ciMeta
|
|
458
|
+
});
|
|
459
|
+
spinner.stop();
|
|
460
|
+
this.process.stdout.write("\nAutomation run ingested!\n\n");
|
|
461
|
+
this.process.stdout.write(` Run ID: ${ingestResult.automation_run_id}\n`);
|
|
462
|
+
this.process.stdout.write(` Source: ${automationSourceId}\n`);
|
|
463
|
+
this.process.stdout.write(` Results: ${ingestResult.passed} passed, ${ingestResult.failed} failed, ${ingestResult.errored} errored, ${ingestResult.skipped} skipped\n`);
|
|
464
|
+
this.process.stdout.write(` Total: ${ingestResult.total_tests}\n`);
|
|
465
|
+
if (ciMeta) {
|
|
466
|
+
const prettyProvider = ciMeta.ci_provider?.replace(/_/g, " ") ?? "CI";
|
|
467
|
+
const ciLabel = ciMeta.ci_build_id ? `${prettyProvider} #${ciMeta.ci_build_id}` : prettyProvider;
|
|
468
|
+
this.process.stdout.write(` CI: ${ciLabel}\n`);
|
|
469
|
+
}
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const result = await uploadImport({
|
|
473
|
+
teamId,
|
|
474
|
+
file: fileObj,
|
|
475
|
+
fileName,
|
|
476
|
+
format: flags.format,
|
|
477
|
+
parentFolderId: flags["parent-folder-id"],
|
|
478
|
+
runName: flags["run-name"],
|
|
479
|
+
updateMode: flags["update-mode"],
|
|
480
|
+
automationSource: sourceName,
|
|
481
|
+
importMetadata: ciMeta
|
|
482
|
+
});
|
|
483
|
+
spinner.stop();
|
|
484
|
+
if (result?.status === "failed") {
|
|
485
|
+
const msg = result.error?.message ?? "Import failed on the server.";
|
|
486
|
+
this.logger.error(msg);
|
|
487
|
+
this.process.exitCode = 1;
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
this.process.stdout.write("\nImport completed!\n\n");
|
|
491
|
+
if (result) {
|
|
492
|
+
if (result.team_id) this.process.stdout.write(` Team: ${result.team_id}\n`);
|
|
493
|
+
if (result.format) this.process.stdout.write(` Format: ${result.format}\n`);
|
|
494
|
+
if (sourceName) this.process.stdout.write(` Source: ${sourceName}${sourceOrigin ? ` (${sourceOrigin})` : ""}\n`);
|
|
495
|
+
if (result.result?.stats) {
|
|
496
|
+
const { tests_created, tests_updated } = result.result.stats;
|
|
497
|
+
this.process.stdout.write(` Tests: ${tests_created} created, ${tests_updated} updated\n`);
|
|
498
|
+
}
|
|
499
|
+
if (result.result?.run_id) this.process.stdout.write(` Run: ${result.result.run_id}\n`);
|
|
500
|
+
if (result.status === "completed_with_warnings" && result.result?.warnings?.length) {
|
|
501
|
+
this.process.stdout.write("\n");
|
|
502
|
+
const warnings = result.result.warnings;
|
|
503
|
+
for (const w of warnings) this.logger.warning(`${w.message} (${w.count})`);
|
|
504
|
+
}
|
|
505
|
+
if (ciMeta) {
|
|
506
|
+
const prettyProvider = ciMeta.ci_provider?.replace(/_/g, " ") ?? "CI";
|
|
507
|
+
const ciLabel = ciMeta.ci_build_id ? `${prettyProvider} #${ciMeta.ci_build_id}` : prettyProvider;
|
|
508
|
+
this.process.stdout.write(` CI: ${ciLabel}\n`);
|
|
509
|
+
}
|
|
510
|
+
if (flags.verbose && result.result?.stats) {
|
|
511
|
+
const s = result.result.stats;
|
|
512
|
+
this.process.stdout.write("\n Details:\n");
|
|
513
|
+
this.process.stdout.write(` Results: ${s.passed} passed, ${s.failed} failed, ${s.errored} errored, ${s.skipped} skipped\n`);
|
|
514
|
+
if (s.pending || s.todo || s.flaky) this.process.stdout.write(` ${s.pending} pending, ${s.todo} todo, ${s.flaky} flaky\n`);
|
|
515
|
+
if (s.suites_created || s.suites_updated) this.process.stdout.write(` Suites: ${s.suites_created} created, ${s.suites_updated} updated\n`);
|
|
516
|
+
if (s.tests_created || s.tests_updated) this.process.stdout.write(` Tests: ${s.tests_created} created, ${s.tests_updated} updated\n`);
|
|
517
|
+
if (s.results_created || s.results_updated) this.process.stdout.write(` Run results: ${s.results_created} created, ${s.results_updated} updated\n`);
|
|
518
|
+
if (s.labels_created || s.label_assignments_created) this.process.stdout.write(` Labels: ${s.labels_created} created, ${s.label_assignments_created} assignments\n`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} catch (err) {
|
|
522
|
+
spinner.stop();
|
|
523
|
+
this.logger.error(err instanceof Error ? err.message : "Upload failed.");
|
|
524
|
+
this.process.exitCode = 1;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
//#endregion
|
|
529
|
+
export { pushHandler };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getPatToken, readCredentials } from "./credentials-CfHLkU7k.js";
|
|
2
|
+
import { isTokenExpired, refreshToken } from "./token-refresh-waF23pyw.js";
|
|
3
|
+
|
|
4
|
+
//#region src/auth/resolve-token.ts
|
|
5
|
+
/**
|
|
6
|
+
* Resolve auth token in priority order:
|
|
7
|
+
* 1. LEVR_TOKEN env var (PAT) — long-lived, no refresh
|
|
8
|
+
* 2. Stored credentials (JWT) — auto-refresh if expired
|
|
9
|
+
* 3. Error — not authenticated
|
|
10
|
+
*/
|
|
11
|
+
async function resolveToken() {
|
|
12
|
+
const pat = getPatToken();
|
|
13
|
+
if (pat) return {
|
|
14
|
+
token: pat,
|
|
15
|
+
type: "pat"
|
|
16
|
+
};
|
|
17
|
+
let creds = readCredentials();
|
|
18
|
+
if (creds) {
|
|
19
|
+
if (isTokenExpired(creds)) {
|
|
20
|
+
const refreshed = await refreshToken(creds);
|
|
21
|
+
if (!refreshed) throw new Error("Token expired and refresh failed. Run 'levr auth login' to re-authenticate.");
|
|
22
|
+
creds = refreshed;
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
token: creds.access_token,
|
|
26
|
+
type: "jwt"
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
throw new Error("Not authenticated. Run 'levr auth login' or set LEVR_TOKEN environment variable.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
export { resolveToken };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { authGetSitesV1 } from "./sdk-client-DunBmYLR.js";
|
|
2
|
+
import { clearWorkspace, loadWorkspace, saveWorkspace } from "./workspace-store-BcyMJAht.js";
|
|
3
|
+
|
|
4
|
+
//#region src/workspace/resolve-workspace.ts
|
|
5
|
+
async function fetchSites() {
|
|
6
|
+
try {
|
|
7
|
+
const result = await authGetSitesV1();
|
|
8
|
+
if (result.error) throw new Error("Failed to list workspaces. Check your connection and run 'levr auth login'.");
|
|
9
|
+
return result.data.sites;
|
|
10
|
+
} catch (err) {
|
|
11
|
+
if (err instanceof Error && err.message.includes("Failed to list")) throw err;
|
|
12
|
+
throw new Error("Failed to list workspaces. Check your connection and run 'levr auth login'.");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Auto-select workspace after login.
|
|
17
|
+
* - Single workspace: persists and returns it.
|
|
18
|
+
* - Multiple: returns count (caller shows hint).
|
|
19
|
+
* - None/error: returns 'none' (non-fatal).
|
|
20
|
+
*/
|
|
21
|
+
async function autoSelectWorkspace() {
|
|
22
|
+
let sites;
|
|
23
|
+
try {
|
|
24
|
+
sites = await fetchSites();
|
|
25
|
+
} catch {
|
|
26
|
+
return { kind: "none" };
|
|
27
|
+
}
|
|
28
|
+
if (sites.length === 1) {
|
|
29
|
+
const ws = sites[0];
|
|
30
|
+
saveWorkspace(ws.workspace_id);
|
|
31
|
+
return {
|
|
32
|
+
kind: "single",
|
|
33
|
+
workspaceId: ws.workspace_id,
|
|
34
|
+
workspaceName: ws.workspace_name
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (sites.length > 1) return {
|
|
38
|
+
kind: "multiple",
|
|
39
|
+
count: sites.length
|
|
40
|
+
};
|
|
41
|
+
return { kind: "none" };
|
|
42
|
+
}
|
|
43
|
+
async function resolveWorkspace(flagValue) {
|
|
44
|
+
let sites = null;
|
|
45
|
+
const getSites = async () => {
|
|
46
|
+
if (!sites) sites = await fetchSites();
|
|
47
|
+
return sites;
|
|
48
|
+
};
|
|
49
|
+
if (flagValue) {
|
|
50
|
+
if (!(await getSites()).some((site) => site.workspace_id === flagValue)) throw new Error(`Workspace ${flagValue} not found. Run 'levr workspace list'.`);
|
|
51
|
+
return {
|
|
52
|
+
workspaceId: flagValue,
|
|
53
|
+
source: "flag"
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const envWs = process.env["LEVR_WORKSPACE_ID"];
|
|
57
|
+
if (envWs) {
|
|
58
|
+
if (!(await getSites()).some((site) => site.workspace_id === envWs)) throw new Error(`LEVR_WORKSPACE_ID ${envWs} not found. Run 'levr workspace list'.`);
|
|
59
|
+
return {
|
|
60
|
+
workspaceId: envWs,
|
|
61
|
+
source: "env"
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const cached = loadWorkspace();
|
|
65
|
+
if (cached) {
|
|
66
|
+
if ((await getSites()).some((site) => site.workspace_id === cached)) return {
|
|
67
|
+
workspaceId: cached,
|
|
68
|
+
source: "cache"
|
|
69
|
+
};
|
|
70
|
+
clearWorkspace();
|
|
71
|
+
}
|
|
72
|
+
const s = await getSites();
|
|
73
|
+
if (s.length === 0) throw new Error("No workspaces available.");
|
|
74
|
+
if (s.length === 1) {
|
|
75
|
+
const single = s[0];
|
|
76
|
+
saveWorkspace(single.workspace_id);
|
|
77
|
+
return {
|
|
78
|
+
workspaceId: single.workspace_id,
|
|
79
|
+
source: "auto"
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const maxList = 10;
|
|
83
|
+
const list = s.slice(0, maxList).map((x) => ` - ${x.workspace_name} (${x.workspace_id})`).join("\n");
|
|
84
|
+
const overflow = s.length > maxList ? `\n ... and ${s.length - maxList} more` : "";
|
|
85
|
+
throw new Error(`Multiple workspaces. Select one:\n${list}${overflow}\n\nRun: levr workspace select <id>`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
//#endregion
|
|
89
|
+
export { autoSelectWorkspace, resolveWorkspace };
|