@myna-sh/cli 0.15.0 → 0.16.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/main.js CHANGED
@@ -701,7 +701,7 @@ var ManagementClient = class {
701
701
  */
702
702
  reports = {
703
703
  list: (project, opts = {}) => {
704
- const { board, status, type, priority, assignee, labels, q, since, ...rest } = opts;
704
+ const { board, status, type, priority, assignee, labels, q, since, reporter, property, ...rest } = opts;
705
705
  return this.page(`/projects/${enc(project)}/reports`, rest, {
706
706
  board,
707
707
  status: Array.isArray(status) ? status.join(",") : status,
@@ -710,7 +710,9 @@ var ManagementClient = class {
710
710
  assignee,
711
711
  labels: labels?.join(","),
712
712
  q,
713
- since
713
+ since,
714
+ reporter,
715
+ property
714
716
  });
715
717
  },
716
718
  get: (project, report, signal) => this.get(
@@ -766,13 +768,7 @@ var ManagementClient = class {
766
768
  void 0,
767
769
  signal
768
770
  ),
769
- /** What publishing this report to a board would expose. Read before moving. */
770
- exposure: (project, report, board, signal) => this.get(
771
- `/projects/${enc(project)}/reports/${enc(String(report))}/exposure`,
772
- { board },
773
- signal
774
- ),
775
- /** Move between boards. A public destination needs `confirm: true`. */
771
+ /** Move a report to another board. Ordinary triage; every board is private. */
776
772
  move: (project, report, body) => this.mutate(
777
773
  "POST",
778
774
  `/projects/${enc(project)}/reports/${enc(String(report))}/move`,
@@ -806,14 +802,58 @@ var ManagementClient = class {
806
802
  *
807
803
  * Unlike every other credential here, one of these is *meant* to be readable
808
804
  * by every visitor to a customer's page. What makes that safe lives on the
809
- * server: the key may only file on one board, only from an origin the project
810
- * registered, and only within its own rate and quota budget.
805
+ * server: the key can only file a report and read back what the presenting
806
+ * reporter wrote, only from an origin the project registered, and only within
807
+ * its own rate and quota budget.
808
+ *
809
+ * Both `rotate` calls return the new secret and leave the old one working for
810
+ * 24 hours, so a frontend can be rebuilt and redeployed in between.
811
811
  */
812
812
  ingestKeys = {
813
813
  list: (project, signal) => this.get(`/projects/${enc(project)}/ingest-keys`, void 0, signal),
814
814
  create: (project, body) => this.mutate("POST", `/projects/${enc(project)}/ingest-keys`, body),
815
+ rotate: (project, key) => this.mutate(
816
+ "POST",
817
+ `/projects/${enc(project)}/ingest-keys/${enc(key)}/rotate`
818
+ ),
819
+ rotateIdentitySecret: (project, key) => this.mutate(
820
+ "POST",
821
+ `/projects/${enc(project)}/ingest-keys/${enc(key)}/rotate-identity-secret`
822
+ ),
815
823
  revoke: (project, key) => this.mutate("DELETE", `/projects/${enc(project)}/ingest-keys/${enc(key)}`)
816
824
  };
825
+ // --- Feedback: reporters --------------------------------------------------
826
+ /**
827
+ * The people who filed the reports.
828
+ *
829
+ * `verified` is the only field Myna stands behind: a signature the project's
830
+ * own backend produced. `properties` is what that backend said about its own
831
+ * user — true of their system, not checked here.
832
+ */
833
+ reporters = {
834
+ list: (project, opts = {}) => {
835
+ const { q, blocked, ...rest } = opts;
836
+ return this.page(`/projects/${enc(project)}/reporters`, rest, {
837
+ q,
838
+ blocked: blocked === void 0 ? void 0 : String(blocked)
839
+ });
840
+ },
841
+ /** By id (`rpr_…`), by the customer's own user id, or by email. */
842
+ get: (project, reporter, signal) => this.get(
843
+ `/projects/${enc(project)}/reporters/${enc(reporter)}`,
844
+ void 0,
845
+ signal
846
+ ),
847
+ /**
848
+ * Block or unblock. Blocking stops notifications and closes read-back; it
849
+ * never deletes what they already filed.
850
+ */
851
+ setBlocked: (project, reporter, body) => this.mutate(
852
+ "PATCH",
853
+ `/projects/${enc(project)}/reporters/${enc(reporter)}`,
854
+ body
855
+ )
856
+ };
817
857
  // --- Declared external checks ---------------------------------------------
818
858
  checks = {
819
859
  list: (project, signal) => this.get(`/projects/${enc(project)}/checks`, void 0, signal),
@@ -3141,7 +3181,7 @@ function registerFeedback(program) {
3141
3181
  );
3142
3182
  })
3143
3183
  );
3144
- boards.command("create").description("Create a board").requiredOption("--key <key>", "lowercase key, e.g. qa").requiredOption("--name <name>", "display name").option("--type <type>", "bug | qa | feedback | feature_request", "bug").option("--description <text>", "what this board is for").option("--guidance <text>", "how reports here should be triaged and answered").action(
3184
+ boards.command("create").description("Create a board").requiredOption("--key <key>", "lowercase key, e.g. qa").requiredOption("--name <name>", "display name").option("--type <type>", "bug | qa | feedback | feature_request", "bug").option("--description <text>", "what this board is for").option("--guidance <text>", "how reports here should be triaged and answered").option("--app-url <url>", "where your product shows these reports; linked from notification emails").option("--require-identity", "refuse submissions that carry no server-signed identity").action(
3145
3185
  handle(async (ctx, _args, opts) => {
3146
3186
  const project = ctx.requireProject();
3147
3187
  const board = await ctx.management().boards.create(project, {
@@ -3149,30 +3189,31 @@ function registerFeedback(program) {
3149
3189
  name: opts.name,
3150
3190
  type: opts.type,
3151
3191
  description: opts.description,
3152
- guidance: opts.guidance
3192
+ guidance: opts.guidance,
3193
+ appUrl: opts.appUrl,
3194
+ requireIdentity: opts.requireIdentity
3153
3195
  });
3154
3196
  emit(board, () => diag(`Created board ${board.key} (${board.id}).`));
3155
3197
  })
3156
3198
  );
3157
- boards.command("update").description("Change a board's name, visibility, or voting").argument("<board>", "board key or id").option("--name <name>", "display name").option("--description <text>", "what this board is for").option("--guidance <text>", "how reports here should be triaged and answered").option("--visibility <visibility>", "private | public").option("--voting", "let readers of a public board vote").option("--no-voting", "turn voting off").action(
3199
+ boards.command("update").description("Change a board's name, app URL, or identity requirement").argument("<board>", "board key or id").option("--name <name>", "display name").option("--description <text>", "what this board is for").option("--guidance <text>", "how reports here should be triaged and answered").option("--app-url <url>", "where your product shows these reports; 'none' to clear it").option("--require-identity", "refuse submissions that carry no server-signed identity").option("--no-require-identity", "accept unsigned submissions").action(
3158
3200
  handle(async (ctx, args, opts) => {
3159
3201
  const project = ctx.requireProject();
3160
- const visibility = opts.visibility;
3161
- if (visibility && visibility !== "private" && visibility !== "public") {
3162
- throw new Error(`--visibility must be private or public (got "${visibility}").`);
3163
- }
3202
+ const appUrl = opts.appUrl;
3164
3203
  const board = await ctx.management().boards.update(project, args[0], {
3165
3204
  name: opts.name,
3166
3205
  description: opts.description,
3167
3206
  guidance: opts.guidance,
3168
- visibility,
3169
- votingEnabled: opts.voting
3207
+ // `none` clears it. An empty string would fail URL validation, and
3208
+ // omitting the flag has to keep meaning "leave it alone".
3209
+ appUrl: appUrl === void 0 ? void 0 : appUrl === "none" ? null : appUrl,
3210
+ requireIdentity: opts.requireIdentity
3170
3211
  });
3171
3212
  emit(board, () => {
3172
3213
  diag(`Updated board ${board.key}.`);
3173
- if (visibility === "public") {
3214
+ if (board.requireIdentity && !board.appUrl) {
3174
3215
  diag(
3175
- `It is now public: every report on it is readable by anyone, and that cannot be undone for reports people have already seen.`
3216
+ `This board requires a signed identity but names no --app-url, so notification emails carry the update with no link back.`
3176
3217
  );
3177
3218
  }
3178
3219
  });
@@ -3186,7 +3227,7 @@ function registerFeedback(program) {
3186
3227
  })
3187
3228
  );
3188
3229
  const reports = feedback.command("reports").description("Read and act on reports");
3189
- reports.command("list").description("List reports, most recently active first").option("--board <key>", "only this board").option("--status <list>", "comma-separated statuses").option("--type <type>", "bug | qa | feedback | feature_request").option("--priority <priority>", "low | normal | high | urgent").option("--assignee <userId>", "only reports assigned to this user").option("--labels <list>", "comma-separated labels; matches any").option("-q, --query <text>", "full-text search over title and body").option("--since <iso>", "only reports active at or after this time").option("--limit <n>", "page size", "25").action(
3230
+ reports.command("list").description("List reports, most recently active first").option("--board <key>", "only this board").option("--status <list>", "comma-separated statuses").option("--type <type>", "bug | qa | feedback | feature_request").option("--priority <priority>", "low | normal | high | urgent").option("--assignee <userId>", "only reports assigned to this user").option("--labels <list>", "comma-separated labels; matches any").option("-q, --query <text>", "full-text search over title and body").option("--since <iso>", "only reports active at or after this time").option("--reporter <ref>", "a reporter's external id or email address").option("--property <key:value>", "match a reporter property, e.g. plan:enterprise").option("--limit <n>", "page size", "25").action(
3190
3231
  handle(async (ctx, _args, opts) => {
3191
3232
  const project = ctx.requireProject();
3192
3233
  const page = await ctx.management().reports.list(project, {
@@ -3198,6 +3239,8 @@ function registerFeedback(program) {
3198
3239
  labels: opts.labels?.split(","),
3199
3240
  q: opts.query,
3200
3241
  since: opts.since,
3242
+ reporter: opts.reporter,
3243
+ property: opts.property,
3201
3244
  limit: Number(opts.limit)
3202
3245
  });
3203
3246
  emit(
@@ -3359,50 +3402,13 @@ Timeline:`);
3359
3402
  );
3360
3403
  })
3361
3404
  );
3362
- reports.command("exposure").description("Show exactly what publishing a report to a board would expose").argument("<report>", "report id, number, or #number").requiredOption("--board <key>", "destination board").action(
3363
- handle(async (ctx, args, opts) => {
3364
- const project = ctx.requireProject();
3365
- const e = await ctx.management().reports.exposure(project, args[0], opts.board);
3366
- emit(e, () => {
3367
- diag(`Moving to "${e.board.key}" (${e.board.visibility}) would make this readable by anyone:`);
3368
- diag(`
3369
- ${e.willBePublic.title}`);
3370
- if (e.willBePublic.body) diag(`
3371
- ${e.willBePublic.body}`);
3372
- if (e.willBePublic.comments.length > 0) {
3373
- diag(`
3374
- Public replies (${e.willBePublic.comments.length}):`);
3375
- for (const c of e.willBePublic.comments) diag(` ${c.author}: ${c.body}`);
3376
- }
3377
- if (e.willBePublic.attachments.length > 0) {
3378
- diag(`
3379
- Attachments:`);
3380
- for (const a of e.willBePublic.attachments) diag(` ${a.filename} (${a.kind}, ${a.byteSize} bytes)`);
3381
- }
3382
- if (e.willBePublic.context) diag(`
3383
- Environment: ${JSON.stringify(e.willBePublic.context)}`);
3384
- diag(`
3385
- Stays private: ${e.willStayPrivate.internalComments} internal note(s)` + (e.willStayPrivate.reporterEmail ? `, the reporter's email` : ""));
3386
- for (const w of e.warnings) diag(`
3387
- ! ${w}`);
3388
- diag(`
3389
- Publish with: myna feedback reports move ${args[0]} --board ${opts.board} --confirm`);
3390
- });
3391
- })
3392
- );
3393
- reports.command("move").description("Move a report to another board").argument("<report>", "report id, number, or #number").requiredOption("--board <key>", "destination board").option("--confirm", "required when the destination is public; run `exposure` first").action(
3405
+ reports.command("move").description("Move a report to another board").argument("<report>", "report id, number, or #number").requiredOption("--board <key>", "destination board").action(
3394
3406
  handle(async (ctx, args, opts) => {
3395
3407
  const project = ctx.requireProject();
3396
- const result = await ctx.management().reports.move(project, args[0], {
3397
- board: opts.board,
3398
- confirm: Boolean(opts.confirm)
3408
+ const report = await ctx.management().reports.move(project, args[0], {
3409
+ board: opts.board
3399
3410
  });
3400
- emit(
3401
- result,
3402
- () => diag(
3403
- result.isPublic ? `#${result.number} is now public on ${result.board}.` : `#${result.number} moved to ${result.board}.`
3404
- )
3405
- );
3411
+ emit(report, () => diag(`#${report.number} moved to ${report.boardKey}.`));
3406
3412
  })
3407
3413
  );
3408
3414
  reports.command("link").description("Point a report at a commit, pull request, entry, or release").argument("<report>", "report id, number, or #number").requiredOption("--kind <kind>", "commit | pull_request | entry | release | url").requiredOption("--value <value>", "the sha, URL, id, or release number").option("--title <text>", "label for the link").action(
@@ -3457,15 +3463,18 @@ Publish with: myna feedback reports move ${args[0]} --board ${opts.board} --conf
3457
3463
  () => table(rows, [
3458
3464
  { header: "ID", value: (k) => k.id },
3459
3465
  { header: "NAME", value: (k) => k.name },
3460
- { header: "BOARD", value: (k) => k.boardKey },
3466
+ { header: "BOARD", value: (k) => k.boardKey ?? "any" },
3461
3467
  { header: "SIGNED ID", value: (k) => k.hasIdentitySecret ? "yes" : "no" },
3462
3468
  { header: "LAST USED", value: (k) => k.lastUsedAt ?? "never" },
3463
- { header: "STATE", value: (k) => k.revokedAt ? "revoked" : "live" }
3469
+ {
3470
+ header: "STATE",
3471
+ value: (k) => k.revokedAt ? "revoked" : k.previousSecretExpiresAt ? "rotating" : "live"
3472
+ }
3464
3473
  ])
3465
3474
  );
3466
3475
  })
3467
3476
  );
3468
- keys.command("create").description("Create a publishable ingest key").requiredOption("--name <name>", "what this key is for").requiredOption("--board <key>", "board it may file on").option("--signed-identity", "also mint a server-side secret for signed identify").action(
3477
+ keys.command("create").description("Create a publishable ingest key").requiredOption("--name <name>", "what this key is for").option("--board <key>", "restrict it to one board; omit to accept any board on the project").option("--signed-identity", "also mint a server-side secret for signed identify").action(
3469
3478
  handle(async (ctx, _args, opts) => {
3470
3479
  const project = ctx.requireProject();
3471
3480
  const created = await ctx.management().ingestKeys.create(project, {
@@ -3474,13 +3483,18 @@ Publish with: myna feedback reports move ${args[0]} --board ${opts.board} --conf
3474
3483
  withIdentitySecret: Boolean(opts.signedIdentity)
3475
3484
  });
3476
3485
  emit(created, () => {
3477
- diag(`Created ingest key ${created.id} for board ${created.boardKey}.`);
3486
+ diag(
3487
+ created.boardKey ? `Created ingest key ${created.id} for board ${created.boardKey}.` : `Created ingest key ${created.id} for any board on this project.`
3488
+ );
3478
3489
  diag(``);
3479
3490
  diag(` ${created.key}`);
3480
3491
  diag(``);
3481
3492
  diag(`This key is publishable \u2014 it is designed to sit in your browser bundle.`);
3482
- diag(`It can only file a report on ${created.boardKey}, and only from an origin`);
3483
- diag(`registered on this project. Add yours with: myna projects update --origins`);
3493
+ diag(
3494
+ created.boardKey ? `It can only file on ${created.boardKey} and read back what the presenting reporter wrote.` : `It can file on any board on this project and read back what the presenting reporter wrote.`
3495
+ );
3496
+ diag(`Requests are refused from any origin the project has not registered.`);
3497
+ diag(`Add yours with: myna projects update --origins`);
3484
3498
  if (created.identitySecret) {
3485
3499
  diag(``);
3486
3500
  diag(`Identity signing secret (shown once \u2014 keep it on your server):`);
@@ -3488,15 +3502,115 @@ Publish with: myna feedback reports move ${args[0]} --board ${opts.board} --conf
3488
3502
  diag(` ${created.identitySecret}`);
3489
3503
  diag(``);
3490
3504
  diag(`Sign a user id with HMAC-SHA256 and pass it as identify.signature.`);
3505
+ diag(`signIdentity in @myna-sh/sdk/feedback/server does it for you.`);
3491
3506
  }
3492
3507
  });
3493
3508
  })
3494
3509
  );
3510
+ keys.command("rotate").description("Mint a new secret, leaving the old one live for 24 hours").argument("<key>", "ingest key id").option("--identity-secret", "rotate the identity signing secret instead of the key").action(
3511
+ handle(async (ctx, args, opts) => {
3512
+ const project = ctx.requireProject();
3513
+ const keyId = args[0];
3514
+ if (opts.identitySecret) {
3515
+ const rotated2 = await ctx.management().ingestKeys.rotateIdentitySecret(project, keyId);
3516
+ emit(rotated2, () => {
3517
+ diag(`Rotated the identity secret on ${rotated2.id} (shown once):`);
3518
+ diag(``);
3519
+ diag(` ${rotated2.identitySecret}`);
3520
+ diag(``);
3521
+ diag(`The old secret keeps verifying until ${rotated2.previousIdentitySecretExpiresAt}.`);
3522
+ });
3523
+ return;
3524
+ }
3525
+ const rotated = await ctx.management().ingestKeys.rotate(project, keyId);
3526
+ emit(rotated, () => {
3527
+ diag(`Rotated ${rotated.id} (shown once):`);
3528
+ diag(``);
3529
+ diag(` ${rotated.key}`);
3530
+ diag(``);
3531
+ diag(`The old key keeps working until ${rotated.previousSecretExpiresAt}. Deploy before then.`);
3532
+ });
3533
+ })
3534
+ );
3495
3535
  keys.command("revoke").description("Revoke an ingest key").argument("<key>", "ingest key id").action(
3496
3536
  handle(async (ctx, args) => {
3497
3537
  const project = ctx.requireProject();
3498
3538
  const result = await ctx.management().ingestKeys.revoke(project, args[0]);
3499
- emit(result, () => diag(`Revoked ${args[0]}. Sites using it can no longer submit.`));
3539
+ emit(
3540
+ result,
3541
+ () => diag(
3542
+ `Revoked ${args[0]}. Sites using it can no longer submit, including a secret mid-rotation.`
3543
+ )
3544
+ );
3545
+ })
3546
+ );
3547
+ const people = feedback.command("people").description("The reporters who filed the reports");
3548
+ people.command("list").description("List reporters, most recent first").option("-q, --query <text>", "match an email, name, or external id").option("--blocked", "only blocked reporters").option("--limit <n>", "page size", "25").action(
3549
+ handle(async (ctx, _args, opts) => {
3550
+ const project = ctx.requireProject();
3551
+ const page = await ctx.management().reporters.list(project, {
3552
+ q: opts.query,
3553
+ blocked: opts.blocked ? true : void 0,
3554
+ limit: Number(opts.limit)
3555
+ });
3556
+ emit(
3557
+ { data: page.data, nextCursor: page.nextCursor },
3558
+ () => table(page.data, [
3559
+ { header: "ID", value: (r) => r.id },
3560
+ { header: "WHO", value: (r) => r.displayName ?? r.email ?? r.externalId ?? "\u2014" },
3561
+ { header: "EXTERNAL", value: (r) => r.externalId ?? "\u2014" },
3562
+ { header: "VERIFIED", value: (r) => r.verified ? "yes" : "no" },
3563
+ { header: "REPORTS", value: (r) => String(r.reportCount) },
3564
+ { header: "LAST SEEN", value: (r) => r.lastSeenAt ?? "never" },
3565
+ { header: "STATE", value: (r) => r.blocked ? "blocked" : "active" }
3566
+ ])
3567
+ );
3568
+ })
3569
+ );
3570
+ people.command("show").description("One reporter and everything they have filed").argument("<reporter>", "reporter id, your own user id, or an email address").action(
3571
+ handle(async (ctx, args) => {
3572
+ const project = ctx.requireProject();
3573
+ const r = await ctx.management().reporters.get(project, args[0]);
3574
+ emit(r, () => {
3575
+ diag(`${r.displayName ?? r.email ?? r.externalId ?? r.id}`);
3576
+ diag(
3577
+ `${r.verified ? "verified" : "unverified"} \xB7 ${r.reportCount} report(s)` + (r.blocked ? " \xB7 blocked" : "")
3578
+ );
3579
+ if (r.email) diag(`email: ${r.email}`);
3580
+ if (r.externalId) diag(`your id: ${r.externalId}`);
3581
+ if (r.properties && Object.keys(r.properties).length > 0) {
3582
+ diag(`
3583
+ Properties (from your application):`);
3584
+ for (const [k, v] of Object.entries(r.properties)) diag(` ${k}: ${JSON.stringify(v)}`);
3585
+ }
3586
+ if (r.reports.length > 0) {
3587
+ diag(``);
3588
+ table(r.reports, [
3589
+ { header: "#", value: (x) => String(x.number) },
3590
+ { header: "TITLE", value: (x) => x.title },
3591
+ { header: "BOARD", value: (x) => x.boardKey },
3592
+ { header: "STATUS", value: (x) => x.status },
3593
+ { header: "ACTIVITY", value: (x) => x.lastActivityAt }
3594
+ ]);
3595
+ }
3596
+ });
3597
+ })
3598
+ );
3599
+ people.command("block").description("Stop a reporter filing, reading back, or being notified").argument("<reporter>", "reporter id, your own user id, or an email address").action(
3600
+ handle(async (ctx, args) => {
3601
+ const project = ctx.requireProject();
3602
+ const r = await ctx.management().reporters.setBlocked(project, args[0], { blocked: true });
3603
+ emit(
3604
+ r,
3605
+ () => diag(`Blocked ${r.displayName ?? r.email ?? r.id}. What they already filed is untouched.`)
3606
+ );
3607
+ })
3608
+ );
3609
+ people.command("unblock").description("Let a blocked reporter back in").argument("<reporter>", "reporter id, your own user id, or an email address").action(
3610
+ handle(async (ctx, args) => {
3611
+ const project = ctx.requireProject();
3612
+ const r = await ctx.management().reporters.setBlocked(project, args[0], { blocked: false });
3613
+ emit(r, () => diag(`Unblocked ${r.displayName ?? r.email ?? r.id}.`));
3500
3614
  })
3501
3615
  );
3502
3616
  const attachments = feedback.command("attachments").description("Evidence attached to a report");
@@ -3683,9 +3797,7 @@ var ID_PREFIXES = {
3683
3797
  reportAttachment: "rat",
3684
3798
  reportAttachmentUpload: "rau",
3685
3799
  reportLink: "rlk",
3686
- ingestKey: "ing",
3687
- reporterToken: "rtk",
3688
- reportVote: "rvt"
3800
+ ingestKey: "ing"
3689
3801
  };
3690
3802
  var PREFIX_SET = new Set(Object.values(ID_PREFIXES));
3691
3803
 
@@ -3782,7 +3894,18 @@ var REPORT_LIMITS = {
3782
3894
  labelsPerReport: 20,
3783
3895
  labelChars: 40,
3784
3896
  /** Serialized `context`, which a developer supplies and we do not police. */
3785
- contextBytes: 16 * 1024
3897
+ contextBytes: 16 * 1024,
3898
+ /**
3899
+ * Serialized reporter `properties`, and how many keys they may have.
3900
+ *
3901
+ * Properties are the customer's own facts about their own user — plan, seat
3902
+ * count, the tenant they belong to — sent with a signed identity so a triager
3903
+ * can tell an enterprise regression from a hobbyist's. Bounded because they
3904
+ * are replaced wholesale on every submission and read on every inbox filter,
3905
+ * not because 33 keys would be wrong to want.
3906
+ */
3907
+ reporterPropertyBytes: 4 * 1024,
3908
+ reporterPropertyKeys: 32
3786
3909
  };
3787
3910
 
3788
3911
  // ../shared/dist/rank.js
@@ -5452,7 +5575,7 @@ function installCommand(manager, version) {
5452
5575
  }
5453
5576
 
5454
5577
  // src/version.ts
5455
- var VERSION = true ? "0.15.0" : "0.0.0-dev";
5578
+ var VERSION = true ? "0.16.0" : "0.0.0-dev";
5456
5579
  var IS_RELEASE_BUILD = true;
5457
5580
 
5458
5581
  // src/commands/doctor.ts