@todesktop/cli 1.6.3 → 1.7.0-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/dist/cli.js CHANGED
@@ -20,8 +20,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
20
20
 
21
21
  // src/index.ts
22
22
  var import_commander = require("commander");
23
- var import_ink33 = require("ink");
24
- var import_react21 = require("react");
23
+ var import_ink38 = require("ink");
24
+ var import_react24 = require("react");
25
25
  var import_register = require("source-map-support/register");
26
26
 
27
27
  // src/components/Build.tsx
@@ -2551,13 +2551,16 @@ var Login_default = Login;
2551
2551
  // src/components/LoadingText.tsx
2552
2552
  var import_ink15 = require("ink");
2553
2553
  var import_jsx_runtime16 = require("react/jsx-runtime");
2554
- var LoadingText = () => {
2554
+ var LoadingText = ({ text = "Loading" }) => {
2555
2555
  const { stdout } = (0, import_ink15.useStdout)();
2556
2556
  const stdOutRedirected = !stdout.isTTY;
2557
2557
  if (stdOutRedirected) {
2558
2558
  return null;
2559
2559
  }
2560
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_ink15.Text, { children: "Loading..." });
2560
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_ink15.Text, { children: [
2561
+ text,
2562
+ "..."
2563
+ ] });
2561
2564
  };
2562
2565
  var LoadingText_default = LoadingText;
2563
2566
 
@@ -3306,13 +3309,13 @@ var capitalize_default = (input) => {
3306
3309
  };
3307
3310
 
3308
3311
  // src/utilities/getBuilds.ts
3309
- var getBuilds_default = async ({
3310
- addWhereClauses = (query) => query,
3312
+ async function getBuilds({
3313
+ addWhereClauses = (collection) => collection,
3311
3314
  userId,
3312
3315
  appId,
3313
3316
  limit = 5,
3314
3317
  startAfter = null
3315
- }) => {
3318
+ }) {
3316
3319
  logger_default.debug({ appId, limit, startAfter }, "getBuilds");
3317
3320
  const appRef = firestore_default.doc(`users/${userId}/applications/${appId}`);
3318
3321
  const appSnapshot = await appRef.get();
@@ -3331,7 +3334,7 @@ var getBuilds_default = async ({
3331
3334
  return [];
3332
3335
  }
3333
3336
  return buildsResult.docs.map((doc) => doc.data());
3334
- };
3337
+ }
3335
3338
 
3336
3339
  // src/utilities/getRelativeDateFromDateString.ts
3337
3340
  var dateFns = __toESM(require("date-fns"));
@@ -3430,7 +3433,7 @@ var ViewBuilds = ({
3430
3433
  }
3431
3434
  const pageSize = count || 5;
3432
3435
  const user2 = await findAppUserId_default(config2.id);
3433
- getBuilds_default({
3436
+ getBuilds({
3434
3437
  appId: config2.id,
3435
3438
  limit: pageSize + 1,
3436
3439
  startAfter,
@@ -3639,14 +3642,18 @@ var import_prop_types22 = __toESM(require("prop-types"));
3639
3642
  var import_react18 = require("react");
3640
3643
 
3641
3644
  // src/utilities/getBuildById.ts
3642
- var getBuildById_default = async ({ appId, buildId, userId }) => {
3645
+ async function getBuildById({
3646
+ appId,
3647
+ buildId,
3648
+ userId
3649
+ }) {
3643
3650
  logger_default.debug({ appId, buildId }, "getBuildById");
3644
3651
  const snapshot = await firestore_default.doc(`users/${userId}/applications/${appId}/builds/${buildId}`).get();
3645
3652
  if (!snapshot.exists) {
3646
3653
  return null;
3647
3654
  }
3648
3655
  return snapshot.data();
3649
- };
3656
+ }
3650
3657
 
3651
3658
  // src/components/ReleaseBuild.tsx
3652
3659
  var import_jsx_runtime32 = require("react/jsx-runtime");
@@ -3704,7 +3711,7 @@ var ReleaseBuild = ({ commandUsed, id, shouldConfirm, configPath }) => {
3704
3711
  const appId2 = config2.id;
3705
3712
  const { id: userId } = await findAppUserId_default(appId2);
3706
3713
  const loadBuild = (buildId) => {
3707
- getBuildById_default({ appId: appId2, buildId, userId }).then((buildResult) => {
3714
+ getBuildById({ appId: appId2, buildId, userId }).then((buildResult) => {
3708
3715
  if (!buildResult) {
3709
3716
  throw new Error(
3710
3717
  `No such build ${buildId} for application ${appId2}`
@@ -4000,7 +4007,7 @@ var PickBuildForRelease = ({ commandUsed, shouldConfirm, configPath }) => {
4000
4007
  return;
4001
4008
  }
4002
4009
  const user2 = await findAppUserId_default(config2.id);
4003
- getBuilds_default({
4010
+ getBuilds({
4004
4011
  addWhereClauses: (query) => query.where("status", "==", "succeeded"),
4005
4012
  appId: config2.id,
4006
4013
  limit: 50,
@@ -4141,40 +4148,627 @@ var ReleaseCommand = ({
4141
4148
  };
4142
4149
  var ReleaseCommand_default = ReleaseCommand;
4143
4150
 
4144
- // src/commands/WhoamiCommand.tsx
4145
- var import_react20 = require("react");
4151
+ // src/commands/smoke-test/SmokeTestCommand.tsx
4152
+ var import_react22 = require("react");
4153
+
4154
+ // src/commands/smoke-test/components/BuildPicker.tsx
4146
4155
  var import_ink32 = require("ink");
4156
+ var import_react20 = require("react");
4147
4157
  var import_jsx_runtime36 = require("react/jsx-runtime");
4158
+ function BuildPicker({
4159
+ buildFilter,
4160
+ buildId,
4161
+ children,
4162
+ commandUsed,
4163
+ configPath,
4164
+ question = "Which build would you like to test?"
4165
+ }) {
4166
+ const exit = useExit_default();
4167
+ const onInput = useInput_default();
4168
+ const isRealIdPassed = buildId && buildId !== "latest";
4169
+ const [state, setState] = (0, import_react20.useState)({
4170
+ selectedBuildId: isRealIdPassed ? buildId : null,
4171
+ state: isRealIdPassed ? "selected" : "loading"
4172
+ });
4173
+ (0, import_react20.useEffect)(() => {
4174
+ loadPickerData({
4175
+ buildFilter,
4176
+ buildId,
4177
+ configPath,
4178
+ state: state.state,
4179
+ updateState
4180
+ });
4181
+ }, [buildId, configPath, state.state]);
4182
+ onInput((input, key) => {
4183
+ if (key.escape && ["show-builds", "loading"].includes(state.state)) {
4184
+ exit();
4185
+ }
4186
+ });
4187
+ function updateState(changes) {
4188
+ setState((previousState) => ({ ...previousState, ...changes }));
4189
+ }
4190
+ function onSelect(item) {
4191
+ updateState({ selectedBuildId: item.value.ID, state: "selected" });
4192
+ }
4193
+ switch (state.state) {
4194
+ case "error":
4195
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ErrorDisplay_default, { commandUsed, error: state.error });
4196
+ case "loading":
4197
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(LoadingText_default, {});
4198
+ case "no-builds":
4199
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_ink32.Box, { children: [
4200
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ink32.Text, { children: "No eligible builds found " }),
4201
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ink32.Text, { dimColor: true, children: "(i.e. unreleased and successful)" })
4202
+ ] });
4203
+ case "selected":
4204
+ return children(state.selectedBuildId);
4205
+ case "show-builds":
4206
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_jsx_runtime36.Fragment, { children: [
4207
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ink32.Box, { children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ink32.Text, { children: question }) }),
4208
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(SelectTable_default, { data: state.builds, onSelect }),
4209
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ink32.Box, { children: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_ink32.Text, { dimColor: true, children: [
4210
+ "Showing the latest ",
4211
+ state.builds.length,
4212
+ " unreleased successful builds"
4213
+ ] }) })
4214
+ ] });
4215
+ default:
4216
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
4217
+ ErrorDisplay_default,
4218
+ {
4219
+ commandUsed,
4220
+ error: new Error(`Unknown state ${state.state}`)
4221
+ }
4222
+ );
4223
+ }
4224
+ }
4225
+ async function loadPickerData({
4226
+ buildFilter,
4227
+ buildId,
4228
+ configPath,
4229
+ state,
4230
+ updateState
4231
+ }) {
4232
+ if (state === "selected") {
4233
+ return;
4234
+ }
4235
+ try {
4236
+ if (buildId === "latest") {
4237
+ const latestBuildId = await getLatestBuild({ configPath });
4238
+ if (latestBuildId) {
4239
+ updateState({ state: "selected", selectedBuildId: latestBuildId });
4240
+ } else {
4241
+ updateState({ state: "no-builds" });
4242
+ }
4243
+ } else {
4244
+ const builds = await getBuildItems({ buildFilter, configPath });
4245
+ updateState({
4246
+ builds,
4247
+ state: builds.length > 0 ? "show-builds" : "no-builds"
4248
+ });
4249
+ }
4250
+ } catch (error) {
4251
+ updateState({ error, state: "error" });
4252
+ }
4253
+ }
4254
+ async function getLatestBuild({
4255
+ configPath
4256
+ }) {
4257
+ const { id: appId } = getProjectConfig(configPath).config;
4258
+ const { id: userId } = await findAppUserId_default(appId);
4259
+ return getLatestBuildId_default({ appId, userId });
4260
+ }
4261
+ async function getBuildItems({
4262
+ buildFilter = () => true,
4263
+ configPath
4264
+ }) {
4265
+ const { id: appId } = getProjectConfig(configPath).config;
4266
+ const { id: userId, label: userName } = await findAppUserId_default(appId);
4267
+ const rawBuilds = await getBuilds({
4268
+ addWhereClauses: (query) => query.where("status", "==", "succeeded"),
4269
+ appId,
4270
+ limit: 50,
4271
+ userId
4272
+ });
4273
+ return rawBuilds.filter(buildFilter).slice(0, 5).map((build) => ({
4274
+ ID: build.id,
4275
+ Version: build.appVersion || "unknown",
4276
+ "Creation date": getRelativeDateFromDateString_default(build.createdAt),
4277
+ Owner: userName || "unknown"
4278
+ }));
4279
+ }
4280
+
4281
+ // src/commands/smoke-test/components/Cancellation.tsx
4282
+ var import_ink33 = require("ink");
4283
+ var import_react21 = require("react");
4284
+ var import_jsx_runtime37 = require("react/jsx-runtime");
4285
+ function Cancellation({
4286
+ children,
4287
+ disabled = false,
4288
+ exitWhenCanceled = true,
4289
+ onCancel,
4290
+ renderCanceling = () => /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(LoadingText_default, { text: `Canceling ${subject}` }),
4291
+ renderCanceled = () => /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_ink33.Text, { children: [
4292
+ upperCaseFirstChar(subject),
4293
+ " canceled."
4294
+ ] }),
4295
+ renderCancelText = () => /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_ink33.Text, { color: "gray", children: [
4296
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ink33.Text, { bold: true, children: "[esc]:" }),
4297
+ " cancel ",
4298
+ subject
4299
+ ] }),
4300
+ renderError = (e) => /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(ErrorDisplay_default, { error: e }),
4301
+ subject
4302
+ }) {
4303
+ const [state, setState] = (0, import_react21.useState)("normal");
4304
+ const [error, setError] = (0, import_react21.useState)();
4305
+ const onInput = useInput_default();
4306
+ const exit = useExit_default();
4307
+ onInput(async (input, key) => {
4308
+ if (key.escape) {
4309
+ cancel();
4310
+ }
4311
+ });
4312
+ function cancel() {
4313
+ if (disabled) {
4314
+ return;
4315
+ }
4316
+ logger_default.debug(`esc pressed, cancelling ${subject}`);
4317
+ setState("cancelling");
4318
+ onCancel().then(() => {
4319
+ setState("cancelled");
4320
+ if (exitWhenCanceled) {
4321
+ setTimeout(() => exit(), 10);
4322
+ }
4323
+ }).catch((e) => {
4324
+ setError(e);
4325
+ setState("error");
4326
+ });
4327
+ }
4328
+ switch (state) {
4329
+ case "cancelled":
4330
+ return renderCanceled();
4331
+ case "cancelling":
4332
+ return renderCanceling();
4333
+ case "error":
4334
+ return renderError(error);
4335
+ default:
4336
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_jsx_runtime37.Fragment, { children: [
4337
+ children,
4338
+ !disabled && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_ink33.Box, { children: renderCancelText() })
4339
+ ] });
4340
+ }
4341
+ }
4342
+ function upperCaseFirstChar(str) {
4343
+ return str[0].toUpperCase() + str.slice(1);
4344
+ }
4345
+
4346
+ // src/commands/smoke-test/components/SmokeTestView.tsx
4347
+ var import_ink36 = require("ink");
4348
+
4349
+ // src/commands/smoke-test/components/BuildError.tsx
4350
+ var import_ink34 = require("ink");
4351
+ var import_ink_link5 = __toESM(require("ink-link"));
4352
+ var import_jsx_runtime38 = require("react/jsx-runtime");
4353
+ function BuildError({
4354
+ build,
4355
+ message: message2
4356
+ }) {
4357
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_ink34.Box, { flexDirection: "column", children: [
4358
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_ink34.Text, { bold: true, color: "red", children: [
4359
+ "Can't finish smoke test ",
4360
+ build.appName,
4361
+ " v",
4362
+ build.appVersion
4363
+ ] }),
4364
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ink34.Box, { marginBottom: 1, marginTop: 1, children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ink34.Text, { children: message2 }) }),
4365
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ink34.Text, { bold: true, children: "See web UI for more information: " }),
4366
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ink34.Text, { children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_ink_link5.default, { fallback: false, url: build.url, children: build.url }) })
4367
+ ] });
4368
+ }
4369
+
4370
+ // src/commands/smoke-test/components/ProgressBar.tsx
4371
+ var import_ink35 = require("ink");
4372
+ var import_jsx_runtime39 = require("react/jsx-runtime");
4373
+ function ProgressBar2({
4374
+ color = "white",
4375
+ label,
4376
+ labelWidth = 9,
4377
+ marginBottom = 1,
4378
+ progress,
4379
+ text = ""
4380
+ }) {
4381
+ const percentage = progress > 0 ? Math.round(progress).toString().padStart(2, "0") + "%" : "";
4382
+ const displayedText = (text || "").replace(/(?:\r\n|\r|\n)\s*/g, "\u21B5 ").replace(/(.{63}).+/, "$1\u2026");
4383
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_ink35.Box, { marginBottom, children: [
4384
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ink35.Box, { width: labelWidth, children: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_ink35.Text, { children: [
4385
+ label,
4386
+ ":"
4387
+ ] }) }),
4388
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ink35.Box, { width: 6, justifyContent: "flex-end", marginRight: 1, children: Boolean(percentage) && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ink35.Text, { backgroundColor: color, color: "black", children: ` ${percentage} ` }) }),
4389
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ink35.Box, { children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_ink35.Text, { children: displayedText }) })
4390
+ ] });
4391
+ }
4392
+
4393
+ // src/commands/smoke-test/components/TestProgress.tsx
4394
+ var import_jsx_runtime40 = require("react/jsx-runtime");
4395
+ function TestProgress({
4396
+ progress
4397
+ }) {
4398
+ if (!progress) {
4399
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(LoadingText_default, { text: "Loading test stats" });
4400
+ }
4401
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
4402
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(OsProgress, { os: "Windows", progress: progress.windows }),
4403
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(OsProgress, { os: "macOS", progress: progress.mac }),
4404
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(OsProgress, { os: "Linux", progress: progress.linux })
4405
+ ] });
4406
+ }
4407
+ function OsProgress({
4408
+ os: os4,
4409
+ progress
4410
+ }) {
4411
+ const colors = {
4412
+ progress: "white",
4413
+ error: "red",
4414
+ done: "green"
4415
+ };
4416
+ const text = progress.message + (progress.state === "progress" ? "..." : "");
4417
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
4418
+ ProgressBar2,
4419
+ {
4420
+ color: colors[progress.state],
4421
+ label: os4,
4422
+ progress: progress.progress,
4423
+ text
4424
+ }
4425
+ );
4426
+ }
4427
+
4428
+ // src/commands/smoke-test/components/SmokeTestView.tsx
4429
+ var import_jsx_runtime41 = require("react/jsx-runtime");
4430
+ function SmokeTestView({
4431
+ commandUsed = "",
4432
+ state
4433
+ }) {
4434
+ var _a, _b, _c, _d;
4435
+ switch (state.state) {
4436
+ case "build-error": {
4437
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(BuildError, { build: state.build, message: (_a = state.error) == null ? void 0 : _a.message });
4438
+ }
4439
+ case "complete":
4440
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_ink36.Text, { bold: true, color: "greenBright", children: "Smoke test passed." });
4441
+ case "error":
4442
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
4443
+ ErrorDisplay_default,
4444
+ {
4445
+ commandUsed,
4446
+ error: { stack: (_b = state.error) == null ? void 0 : _b.stack }
4447
+ }
4448
+ );
4449
+ case "loading":
4450
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(LoadingText_default, { text: "Preparing smoke test" });
4451
+ case "progress":
4452
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(TestProgress, { progress: state.progress });
4453
+ case "progress-error":
4454
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(import_jsx_runtime41.Fragment, { children: [
4455
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(TestProgress, { progress: state.progress }),
4456
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
4457
+ BuildError,
4458
+ {
4459
+ build: state.build,
4460
+ message: (_d = (_c = state.progress) == null ? void 0 : _c.total) == null ? void 0 : _d.message
4461
+ }
4462
+ )
4463
+ ] });
4464
+ default:
4465
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
4466
+ ErrorDisplay_default,
4467
+ {
4468
+ commandUsed,
4469
+ error: new Error(`Unknown state ${state}`)
4470
+ }
4471
+ );
4472
+ }
4473
+ }
4474
+
4475
+ // src/commands/smoke-test/utilities/build.ts
4476
+ var import_semver = __toESM(require("semver"));
4477
+
4478
+ // src/commands/smoke-test/utilities/SmokeError.ts
4479
+ var SmokeError = class extends Error {
4480
+ constructor(message2, { cause, type = "error" } = {}) {
4481
+ super(message2, { cause });
4482
+ this.type = type;
4483
+ }
4484
+ };
4485
+
4486
+ // src/commands/smoke-test/utilities/build.ts
4487
+ var MIN_RUNTIME_VERSION = "1.2.1-1";
4488
+ function isBuildTestable(build) {
4489
+ try {
4490
+ validateBuild(build);
4491
+ return true;
4492
+ } catch (e) {
4493
+ return false;
4494
+ }
4495
+ }
4496
+ function validateBuild(build) {
4497
+ if (build.status !== "succeeded") {
4498
+ throw new SmokeError(
4499
+ `The build must be completed successfully. Actual build status: ${build.status}`
4500
+ );
4501
+ }
4502
+ if (!build.todesktopRuntimeVersionSpecified) {
4503
+ throw new SmokeError(
4504
+ "The build has no todesktopRuntimeVersionSpecified set. Please make sure @todesktop/runtime is installed as a dependency and make a new build"
4505
+ );
4506
+ }
4507
+ if (!isRuntimeVerRangeAllowed(build.todesktopRuntimeVersionSpecified)) {
4508
+ throw new SmokeError(
4509
+ `The build should have @todesktop/shared@${MIN_RUNTIME_VERSION} installed at least`
4510
+ );
4511
+ }
4512
+ }
4513
+ function isRuntimeVerRangeAllowed(runtimeVerRange) {
4514
+ const minRuntimeVersionForApp = import_semver.default.minVersion(runtimeVerRange, {
4515
+ loose: true
4516
+ });
4517
+ return import_semver.default.gte(minRuntimeVersionForApp, MIN_RUNTIME_VERSION, {
4518
+ loose: true
4519
+ });
4520
+ }
4521
+
4522
+ // src/commands/smoke-test/utilities/cancelSmokeTest.ts
4523
+ async function cancelSmokeTest({
4524
+ appId,
4525
+ buildId,
4526
+ userId
4527
+ }) {
4528
+ await postToFirebaseFunction_default("cancelSmokeTest", {
4529
+ appId,
4530
+ buildId,
4531
+ userId
4532
+ });
4533
+ }
4534
+
4535
+ // src/commands/smoke-test/utilities/fetchBuild.ts
4536
+ async function fetchBuild({
4537
+ appId,
4538
+ buildId,
4539
+ userId
4540
+ }) {
4541
+ const build = await getBuildById({ appId, buildId, userId });
4542
+ if (!build) {
4543
+ throw new SmokeError(`No such build ${buildId} for application ${appId}`);
4544
+ }
4545
+ return build;
4546
+ }
4547
+
4548
+ // src/commands/smoke-test/utilities/fetchUserIdByAppId.ts
4549
+ async function fetchUserIdByAppId(appId) {
4550
+ try {
4551
+ const { id: userId } = await findAppUserId_default(appId);
4552
+ return userId;
4553
+ } catch (e) {
4554
+ throw new Error(`Can't fetch user for app ${appId}, ${e.message}`);
4555
+ }
4556
+ }
4557
+
4558
+ // src/commands/smoke-test/utilities/queueSmokeTest.ts
4559
+ async function queueSmokeTest({
4560
+ appId,
4561
+ buildId,
4562
+ nodeVersion,
4563
+ userId
4564
+ }) {
4565
+ try {
4566
+ await postToFirebaseFunction_default("queueSmokeTest", {
4567
+ appId,
4568
+ buildId,
4569
+ nodeVersion,
4570
+ userId
4571
+ });
4572
+ } catch (e) {
4573
+ if (["failed-precondition", "not-found"].includes(e.code)) {
4574
+ throw new SmokeError(e.mesage, { type: "build-error" });
4575
+ } else {
4576
+ throw new SmokeError("Unexpected internal error while testing build");
4577
+ }
4578
+ }
4579
+ }
4580
+
4581
+ // src/commands/smoke-test/utilities/waitUntilFinished.ts
4582
+ async function waitUntilFinished({
4583
+ appId,
4584
+ buildId,
4585
+ onProgress,
4586
+ userId
4587
+ }) {
4588
+ const docPath = `users/${userId}/applications/${appId}/builds/${buildId}`;
4589
+ return new Promise((resolve4, reject) => {
4590
+ const unsubscribe = firestore_default.doc(docPath).onSnapshot(
4591
+ (snapshot) => {
4592
+ const data = snapshot.exists ? snapshot.data() : void 0;
4593
+ const progress = makeProgress(data);
4594
+ onProgress(progress);
4595
+ if (progress.isFinished) {
4596
+ unsubscribe();
4597
+ resolve4(progress);
4598
+ }
4599
+ },
4600
+ (err) => {
4601
+ unsubscribe();
4602
+ reject(err);
4603
+ }
4604
+ );
4605
+ });
4606
+ }
4607
+ function makeProgress(build) {
4608
+ const def = { progress: 0, state: "progress" };
4609
+ const { linux = def, mac = def, windows = def } = build.smokeTest;
4610
+ const platforms = [linux, mac, windows];
4611
+ const isFinished = platforms.every((p) => p.state !== "progress");
4612
+ let totalState = "progress";
4613
+ if (platforms.every((p) => p.state === "done")) {
4614
+ totalState = "done";
4615
+ } else if (isFinished && platforms.some((p) => p.state === "error")) {
4616
+ totalState = "error";
4617
+ }
4618
+ return {
4619
+ isFinished,
4620
+ linux,
4621
+ mac,
4622
+ windows,
4623
+ total: {
4624
+ message: platforms.filter((p) => p.state === "error").map((p) => p.message).join("\n\n"),
4625
+ progress: Math.round(
4626
+ (linux.progress + mac.progress + windows.progress) / 3
4627
+ ),
4628
+ state: totalState
4629
+ }
4630
+ };
4631
+ }
4632
+
4633
+ // src/commands/smoke-test/SmokeTestCommand.tsx
4634
+ var import_jsx_runtime42 = require("react/jsx-runtime");
4635
+ function SmokeTestCommand({
4636
+ buildId,
4637
+ configPath
4638
+ }) {
4639
+ checkIfReactIsUsable_default();
4640
+ useAnalyticsCommand("smoke-test", { buildId, configPath });
4641
+ const command = `todesktop release ${buildId === "latest" ? "--latest" : "<id>"}`;
4642
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(ErrorBoundary_default, { children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(LoginHOC_default, { children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
4643
+ BuildPicker,
4644
+ {
4645
+ buildFilter: isBuildTestable,
4646
+ buildId,
4647
+ commandUsed: command,
4648
+ configPath,
4649
+ children: (id) => /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
4650
+ SmokeTestContainer,
4651
+ {
4652
+ commandUsed: command,
4653
+ buildId: id,
4654
+ configPath
4655
+ }
4656
+ )
4657
+ }
4658
+ ) }) });
4659
+ }
4660
+ function SmokeTestContainer({
4661
+ buildId,
4662
+ commandUsed,
4663
+ configPath
4664
+ }) {
4665
+ const exit = useExit_default();
4666
+ const [state, setState] = (0, import_react22.useState)({ buildId, state: "loading" });
4667
+ const [abortController] = (0, import_react22.useState)(new AbortController());
4668
+ const abortSignal = abortController.signal;
4669
+ (0, import_react22.useEffect)(() => {
4670
+ smokeTestWorkflow({ abortSignal, buildId, configPath, updateState });
4671
+ }, [buildId, configPath]);
4672
+ function updateState(changes) {
4673
+ setState((previousState) => ({ ...previousState, ...changes }));
4674
+ if (["build-error", "complete", "progress-error"].includes(changes.state)) {
4675
+ setTimeout(() => exit(), 10);
4676
+ }
4677
+ }
4678
+ async function onCancel() {
4679
+ abortController.abort();
4680
+ if (state.state === "progress" && buildCanBeCanceled(state.build)) {
4681
+ await cancelSmokeTest(state);
4682
+ }
4683
+ }
4684
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
4685
+ Cancellation,
4686
+ {
4687
+ disabled: !isCancelable(state),
4688
+ onCancel,
4689
+ subject: "smoke test",
4690
+ children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(SmokeTestView, { commandUsed, state })
4691
+ }
4692
+ );
4693
+ }
4694
+ async function smokeTestWorkflow({
4695
+ abortSignal,
4696
+ buildId,
4697
+ configPath,
4698
+ updateState
4699
+ }) {
4700
+ try {
4701
+ const { id: appId, nodeVersion } = getProjectConfig(configPath).config;
4702
+ const userId = await fetchUserIdByAppId(appId);
4703
+ const build = await fetchBuild({ appId, buildId, userId });
4704
+ updateState({ appId, build, userId });
4705
+ validateBuild(build);
4706
+ if (!abortSignal.aborted) {
4707
+ await queueSmokeTest({ appId, buildId, nodeVersion, userId });
4708
+ }
4709
+ updateState({ state: "progress" });
4710
+ const { total } = await waitUntilFinished({
4711
+ appId,
4712
+ buildId,
4713
+ userId,
4714
+ onProgress: (progress) => updateState({ progress })
4715
+ });
4716
+ updateState({
4717
+ state: total.state == "error" ? "progress-error" : "complete"
4718
+ });
4719
+ } catch (error) {
4720
+ updateState({ error, state: error.type || "error" });
4721
+ }
4722
+ }
4723
+ function buildCanBeCanceled(build) {
4724
+ var _a, _b;
4725
+ if ((_a = build == null ? void 0 : build.smokeTest) == null ? void 0 : _a.isCanceled) {
4726
+ return false;
4727
+ }
4728
+ if (!((_b = build == null ? void 0 : build.smokeTest) == null ? void 0 : _b.buildServerExecutionId)) {
4729
+ return false;
4730
+ }
4731
+ return true;
4732
+ }
4733
+ function isCancelable(state) {
4734
+ const cancelableStates = ["loading", "progress"];
4735
+ return cancelableStates.includes(state.state);
4736
+ }
4737
+
4738
+ // src/commands/WhoamiCommand.tsx
4739
+ var import_react23 = require("react");
4740
+ var import_ink37 = require("ink");
4741
+ var import_jsx_runtime43 = require("react/jsx-runtime");
4148
4742
  var WhoAmI = () => {
4149
4743
  const exit = useExit_default();
4150
4744
  checkIfReactIsUsable_default();
4151
4745
  const auth = getAuthConfig();
4152
4746
  const { hasAttemptedTracking } = useAnalyticsCommand("whoami", {}, {});
4153
- (0, import_react20.useEffect)(() => {
4747
+ (0, import_react23.useEffect)(() => {
4154
4748
  if (hasAttemptedTracking) {
4155
4749
  exit();
4156
4750
  }
4157
4751
  }, [exit, hasAttemptedTracking]);
4158
4752
  if (!auth || !auth.email) {
4159
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ink32.Text, { children: "You're not signed in" });
4753
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ink37.Text, { children: "You're not signed in" });
4160
4754
  }
4161
- return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_ink32.Text, { children: auth.email });
4755
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_ink37.Text, { children: auth.email });
4162
4756
  };
4163
- var WhoAmIWrapper = () => /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(ErrorBoundary_default, { children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(LoginHOC_default, { isInteractive: false, children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(WhoAmI, {}) }) });
4757
+ var WhoAmIWrapper = () => /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(ErrorBoundary_default, { children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(LoginHOC_default, { isInteractive: false, children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(WhoAmI, {}) }) });
4164
4758
  var WhoamiCommand_default = WhoAmIWrapper;
4165
4759
 
4166
4760
  // src/utilities/exitIfCLIOutOfDate.ts
4167
4761
  var import_chalk3 = __toESM(require("chalk"));
4168
4762
  var import_is_installed_globally = __toESM(require("is-installed-globally"));
4169
4763
  var import_latest_version = __toESM(require("latest-version"));
4170
- var import_semver = __toESM(require("semver"));
4764
+ var import_semver2 = __toESM(require("semver"));
4171
4765
  var exitIfCLIOutOfDate_default = () => {
4172
4766
  if (process.env.AVA_PATH) {
4173
4767
  return;
4174
4768
  }
4175
4769
  const pkg = getToDesktopPackageJson();
4176
4770
  (0, import_latest_version.default)(pkg.name).then((latest) => {
4177
- if (import_semver.default.gt(latest, pkg.version)) {
4771
+ if (import_semver2.default.gt(latest, pkg.version)) {
4178
4772
  const commandToUpdate = import_chalk3.default.greenBright(
4179
4773
  `npm install ${import_is_installed_globally.default ? "--location=global" : "--save-dev"} @todesktop/cli`
4180
4774
  );
@@ -4182,7 +4776,7 @@ var exitIfCLIOutOfDate_default = () => {
4182
4776
  `Your version of @todesktop/cli is out of date.
4183
4777
  Run ${commandToUpdate} to update to v${latest}.`
4184
4778
  );
4185
- if (!import_semver.default.satisfies(latest, `^${pkg.version}`)) {
4779
+ if (!import_semver2.default.satisfies(latest, `^${pkg.version}`)) {
4186
4780
  console.log(`CLI is exiting because it is out out of date.`);
4187
4781
  process.exit(1);
4188
4782
  }
@@ -4201,7 +4795,7 @@ var package_default = {
4201
4795
  access: "public"
4202
4796
  },
4203
4797
  name: "@todesktop/cli",
4204
- version: "1.6.2",
4798
+ version: "1.6.3",
4205
4799
  license: "MIT",
4206
4800
  author: "Dave Jeffery <dave@todesktop.com> (http://www.todesktop.com/)",
4207
4801
  homepage: "https://todesktop.com/cli",
@@ -4281,7 +4875,7 @@ var package_default = {
4281
4875
  "xdg-basedir": "^4.0.0"
4282
4876
  },
4283
4877
  devDependencies: {
4284
- "@todesktop/shared": "^7.152.0",
4878
+ "@todesktop/shared": "^7.160.0",
4285
4879
  "@types/bunyan": "^1.8.6",
4286
4880
  "@types/react": "^18.0.26",
4287
4881
  "@typescript-eslint/eslint-plugin": "^5.46.1",
@@ -4303,6 +4897,7 @@ var package_default = {
4303
4897
  "extract-zip": "^2.0.1",
4304
4898
  "fs-extra": "^9.0.1",
4305
4899
  husky: "^4.3.0",
4900
+ "ink-testing-library": "^2.1.0",
4306
4901
  "lint-staged": "^10.2.11",
4307
4902
  prettier: "^2.8.1",
4308
4903
  proxyquire: "^2.1.3",
@@ -4311,15 +4906,19 @@ var package_default = {
4311
4906
  },
4312
4907
  ava: {
4313
4908
  extensions: [
4314
- "ts"
4909
+ "ts",
4910
+ "tsx"
4315
4911
  ],
4316
4912
  files: [
4317
4913
  "test/**/*.ts",
4914
+ "**/*.test.ts",
4915
+ "**/*.test.tsx",
4318
4916
  "!test/fixtures/**/*",
4319
4917
  "!test/utilities/**/*"
4320
4918
  ],
4321
4919
  require: [
4322
- "esbuild-register"
4920
+ "esbuild-register",
4921
+ "./test/unit-test-init"
4323
4922
  ],
4324
4923
  timeout: "10m"
4325
4924
  },
@@ -4417,27 +5016,28 @@ var parseCount = (value) => {
4417
5016
  }
4418
5017
  throw new import_commander.InvalidArgumentError("Should be a positive number");
4419
5018
  };
5019
+ var configOption = new import_commander.Option(
5020
+ "--config [string]",
5021
+ "Path to a different configuration file. If not specified, `todesktop.json` in the project root will be used"
5022
+ );
4420
5023
  import_commander.program.name("todekstop").version(getCliVersion_default());
4421
5024
  import_commander.program.command("build").description(
4422
5025
  "Build an Electron app with native installers, code signing baked-in, etc. but without releasing it (existing users won't get an auto-update). For quicker builds, you can append `--code-sign=false` to disablecode-signing and notarization."
4423
5026
  ).option(
4424
5027
  "--code-sign [bool]",
4425
5028
  "Whether or not code-signing and notarization should be performed. Disable this for quicker builds"
4426
- ).option(
4427
- "--config [string]",
4428
- "Path to a different configuration file. If not specified, `todesktop.json` in the project root will be used"
4429
- ).action(({ codeSign, config: config2 }) => {
5029
+ ).addOption(configOption).action(({ codeSign, config: config2 }) => {
4430
5030
  runCommand(BuildCommand_default, {
4431
5031
  shouldCodeSign: codeSign,
4432
5032
  configPath: config2
4433
5033
  });
4434
5034
  });
4435
- import_commander.program.command("builds").description("View your builds").argument("[id]", "View a specific build by ID").option("--latest", "View the latest build").option(
4436
- "--config [string]",
4437
- "Path to a different configuration file. If not specified, `todesktop.json` in the project root will be used"
4438
- ).option("--count [number]", "Number of builds to show per page", parseCount).addOption(
5035
+ import_commander.program.command("builds").description("View your builds").argument("[id]", "View a specific build by ID").option("--latest", "View the latest build").addOption(configOption).option("--count [number]", "Number of builds to show per page", parseCount).addOption(
4439
5036
  new import_commander.Option("--format <type>", "Format to output the build details in").choices(["json", "table"]).default("table")
4440
- ).option("--exit", "View the latest build").action((id, { config: config2, count, exit, format, latest }) => {
5037
+ ).option(
5038
+ "--exit",
5039
+ "Disable dynamic pagination and exit the process once the build data has been displayed"
5040
+ ).action((id, { config: config2, count, exit, format, latest }) => {
4441
5041
  runCommand(BuildsCommand_default, {
4442
5042
  configPath: config2,
4443
5043
  count,
@@ -4450,10 +5050,7 @@ import_commander.program.command("builds").description("View your builds").argum
4450
5050
  import_commander.program.command("logout").description("Logs you out").action(() => {
4451
5051
  runCommand(LogoutCommand_default, null, { exitIfOutOfDate: false });
4452
5052
  });
4453
- import_commander.program.command("release").description("Release a build").argument("[id]", "A specific build ID to release").option("--force", "Skips interactive confirmation step").option("--latest", "Release the latest build").option(
4454
- "--config [string]",
4455
- "Path to a different configuration file. If not specified, `todesktop.json` in the project root will be used"
4456
- ).action((id, { config: config2, force, latest }) => {
5053
+ import_commander.program.command("release").description("Release a build").argument("[id]", "A specific build ID to release").option("--force", "Skips interactive confirmation step").option("--latest", "Release the latest build").addOption(configOption).action((id, { config: config2, force, latest }) => {
4457
5054
  runCommand(ReleaseCommand_default, {
4458
5055
  configPath: config2,
4459
5056
  force,
@@ -4461,12 +5058,18 @@ import_commander.program.command("release").description("Release a build").argum
4461
5058
  shouldReleaseLatest: latest
4462
5059
  });
4463
5060
  });
5061
+ import_commander.program.command("smoke-test").description("Check whether the build works and can be successfully updated").argument("[id]", "A specific build ID to test").option("--latest", "Release the latest build").addOption(configOption).action((id, { config: config2, latest }) => {
5062
+ runCommand(SmokeTestCommand, {
5063
+ configPath: config2,
5064
+ buildId: latest ? "latest" : id
5065
+ });
5066
+ });
4464
5067
  import_commander.program.command("whoami").description("Prints the email of the account you're signed into").action(() => {
4465
5068
  runCommand(WhoamiCommand_default);
4466
5069
  });
4467
5070
  var runCommand = (component, props = null, { exitIfOutOfDate = true } = {}) => {
4468
5071
  onCommand_default({ exitIfOutOfDate });
4469
- const { waitUntilExit } = (0, import_ink33.render)((0, import_react21.createElement)(component, props));
5072
+ const { waitUntilExit } = (0, import_ink38.render)((0, import_react24.createElement)(component, props));
4470
5073
  waitUntilExit().catch((error) => {
4471
5074
  console.error(error.stack);
4472
5075
  });