@islamihab/kds 0.10.0 → 0.11.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.
Files changed (2) hide show
  1. package/index.js +444 -558
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -14406,7 +14406,10 @@ var printHelp = (help) => {
14406
14406
  label: key,
14407
14407
  description: `${field.description ?? ""}${isRequired(field) ? " (required)" : ""}`
14408
14408
  }));
14409
- const usagePositionals = help.positionals?.map(([key, field]) => isRequired(field) ? ` <${key}>` : ` [${key}]`).join("") ?? "";
14409
+ const usagePositionals = help.positionals?.map(([key, field]) => {
14410
+ const name = isRest(field) ? `${key}...` : key;
14411
+ return isRequired(field) ? ` <${name}>` : ` [${name}]`;
14412
+ }).join("") ?? "";
14410
14413
  const options = help.options?.filter(({ field }) => !isHidden(field)).concat([
14411
14414
  {
14412
14415
  key: "help",
@@ -14451,6 +14454,7 @@ var baseType = (field) => {
14451
14454
  return t;
14452
14455
  };
14453
14456
  var isRequired = (field) => !(field instanceof exports_external.ZodOptional || field instanceof exports_external.ZodDefault);
14457
+ var isRest = (field) => baseType(field) instanceof exports_external.ZodArray;
14454
14458
  var isHidden = (field) => field.meta()?.hidden === true;
14455
14459
  var isNegatable = (field) => field.meta()?.negatable === true;
14456
14460
  var isHelpFlag = (arg) => arg === "-h" || arg === "--help";
@@ -14519,7 +14523,8 @@ var parseArgs = ({
14519
14523
  options,
14520
14524
  allowPositionals: (positionalEntries?.length ?? 0) > 0
14521
14525
  });
14522
- if (positionalEntries && positionals.length > positionalEntries.length) {
14526
+ const hasRest = positionalEntries?.some(([, field]) => isRest(field)) ?? false;
14527
+ if (positionalEntries && !hasRest && positionals.length > positionalEntries.length) {
14523
14528
  help();
14524
14529
  throw new Error(`Unexpected argument '${positionals[positionalEntries.length]}'`);
14525
14530
  }
@@ -14552,6 +14557,10 @@ var parseArgs = ({
14552
14557
  var command = (def) => {
14553
14558
  const inputShape = exports_external.object({ positionals: exports_external.object(def.positionals), options: exports_external.object(def.options) });
14554
14559
  const positionalEntries = Object.entries(inputShape.shape.positionals.shape);
14560
+ const restIndex = positionalEntries.findIndex(([, field]) => isRest(field));
14561
+ if (restIndex !== -1 && restIndex !== positionalEntries.length - 1) {
14562
+ throw new Error(`Positional '${positionalEntries[restIndex]?.[0]}' collects the rest, so it must come last`);
14563
+ }
14555
14564
  const optionsEntries = optionEntries(inputShape.shape.options.shape);
14556
14565
  const run = async (args, path) => {
14557
14566
  const help = () => printHelp({
@@ -14567,7 +14576,10 @@ var command = (def) => {
14567
14576
  await def.run({
14568
14577
  ...inputShape.parse({
14569
14578
  options: values,
14570
- positionals: Object.fromEntries(positionalEntries.map(([key], index) => [key, positionals[index]]))
14579
+ positionals: Object.fromEntries(positionalEntries.map(([key], index) => [
14580
+ key,
14581
+ index === restIndex && positionals.length > index ? positionals.slice(index) : positionals[index]
14582
+ ]))
14571
14583
  }),
14572
14584
  help
14573
14585
  });
@@ -14611,7 +14623,7 @@ import { join } from "path";
14611
14623
  // package.json
14612
14624
  var package_default = {
14613
14625
  name: "cli",
14614
- version: "0.10.0",
14626
+ version: "0.11.0",
14615
14627
  private: true,
14616
14628
  type: "module",
14617
14629
  bin: {
@@ -15306,6 +15318,7 @@ var PROJECT_STATUSES = ["planned", "in_progress", "paused", "completed", "cancel
15306
15318
  var PROJECT_HEALTHS = ["on_track", "at_risk", "off_track"];
15307
15319
  var ISSUE_IDENTIFIER_PREFIX = "KAI";
15308
15320
  var ISSUE_LIST_PAGE_SIZE = 50;
15321
+ var MAX_ISSUES_PER_BATCH_READ = 20;
15309
15322
  var MAX_ISSUE_ATTACHMENT_BYTES = 1e7;
15310
15323
  var ISSUE_DUE_DATE_MODES = ["overdue", "due_today", "due_soon", "no_due_date"];
15311
15324
  var BUILT_IN_ISSUE_VIEWS = {
@@ -15352,13 +15365,8 @@ var BUILT_IN_ISSUE_VIEWS = {
15352
15365
  }
15353
15366
  };
15354
15367
  var issueIsOpen = (status) => !ISSUE_TERMINAL_STATUSES.some((terminal) => terminal === status);
15355
- var MAX_CLIENT_NAME_LENGTH = 200;
15356
- var MAX_CLIENT_LEGAL_NAME_LENGTH = 200;
15357
- var MAX_CLIENT_BILLING_ADDRESS_LENGTH = 2000;
15358
- var MAX_CLIENT_BILLING_EMAIL_LENGTH = 320;
15359
- var MAX_CLIENT_TAX_ID_LENGTH = 100;
15360
- var MAX_CLIENT_PAYMENT_TERMS_DAYS = 365;
15361
- var MAX_BILLING_DAY = 31;
15368
+ var CURRENCIES = ["EGP", "USD", "EUR", "GBP", "AED", "SAR"];
15369
+ var DEFAULT_CURRENCY = "EGP";
15362
15370
  var RECURRING_CHARGE_CADENCES = ["monthly", "yearly"];
15363
15371
  var COST_KINDS = ["one_off", "monthly", "yearly"];
15364
15372
  var MAX_PROJECT_REPO_LENGTH = 200;
@@ -24257,6 +24265,73 @@ var readPageId = (value) => {
24257
24265
  }
24258
24266
  };
24259
24267
 
24268
+ // src/commands/clients/archive.ts
24269
+ var archive = command({
24270
+ name: "archive",
24271
+ description: "Archive a client: it leaves pickers and default lists, and every existing reference stays intact",
24272
+ positionals: {
24273
+ id: exports_external.string().describe("Client id or URL")
24274
+ },
24275
+ run: async ({ positionals: { id } }) => {
24276
+ const backend = await backendClient();
24277
+ const { name } = await backend.mutation(api2.clients.archive, { id: readClientRef(id) });
24278
+ console.log(`Archived ${name}.`);
24279
+ }
24280
+ });
24281
+
24282
+ // src/lib/currency.ts
24283
+ var CURRENCY_CHOICES = CURRENCIES.join(", ");
24284
+ var currencyCode = exports_external.preprocess((value) => typeof value === "string" ? value.toUpperCase() : value, exports_external.enum(CURRENCIES));
24285
+ var parseLineCurrency = (raw) => {
24286
+ const code2 = raw.trim().toUpperCase();
24287
+ if (!code2)
24288
+ return DEFAULT_CURRENCY;
24289
+ const codes = CURRENCIES;
24290
+ if (!codes.includes(code2))
24291
+ throw new Error(`Line currency must be one of ${CURRENCY_CHOICES}; got "${raw.trim()}".`);
24292
+ return code2;
24293
+ };
24294
+
24295
+ // src/commands/clients/charges/add.ts
24296
+ var add = command({
24297
+ name: "add",
24298
+ description: "Add a recurring charge to a client and print its id",
24299
+ positionals: {
24300
+ client: exports_external.string().describe("Client id or URL"),
24301
+ description: exports_external.string().describe('What the charge is for, e.g. "Maintenance retainer"')
24302
+ },
24303
+ options: {
24304
+ amount: exports_external.coerce.number().int().describe("Amount in integer minor units (2500 = 25.00)").meta({ short: "a" }),
24305
+ currency: currencyCode.default(DEFAULT_CURRENCY).describe(`One of ${CURRENCY_CHOICES}`).meta({ short: "c" }),
24306
+ cadence: exports_external.enum(RECURRING_CHARGE_CADENCES).default("monthly").describe("monthly or yearly"),
24307
+ "due-month": exports_external.coerce.number().int().optional().describe("Month (1-12) a yearly charge falls due; defaults to the current month")
24308
+ },
24309
+ run: async ({ positionals: { client: ref, description }, options }) => {
24310
+ const backend = await backendClient();
24311
+ const chargeId = await backend.mutation(api2.recurringCharges.create, {
24312
+ clientId: readClientRef(ref),
24313
+ description,
24314
+ amount: options.amount,
24315
+ currency: options.currency,
24316
+ cadence: options.cadence,
24317
+ dueMonth: options["due-month"]
24318
+ });
24319
+ console.log(chargeId);
24320
+ }
24321
+ });
24322
+
24323
+ // src/lib/output.ts
24324
+ var formatMinorAmount = (amount, currency) => `${(amount / 100).toFixed(2)} ${currency}`;
24325
+ var printTable = (rows) => {
24326
+ if (rows.length === 0)
24327
+ return;
24328
+ const columnCount = Math.max(...rows.map((row) => row.length));
24329
+ const widths = Array.from({ length: columnCount }, (_, column) => Math.max(...rows.map((row) => (row[column] ?? "").length)));
24330
+ for (const row of rows) {
24331
+ console.log(row.map((cell, column) => cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd());
24332
+ }
24333
+ };
24334
+
24260
24335
  // ../../packages/backend/convex/lib/projectRepo.ts
24261
24336
  var DEFAULT_REPO_HOST = "github.com";
24262
24337
  var REPO_FORMAT_ERROR = "Enter a repository like github.com/owner/name.";
@@ -24329,24 +24404,6 @@ var resolveQuotation = async (client3, ref) => {
24329
24404
  throw new Error(`No quotation matches ${ref}.`);
24330
24405
  return quotation;
24331
24406
  };
24332
- var resolvePayment = async (client3, ref) => {
24333
- const payment = await client3.query(api2.invoices.getPayment, { id: ref.trim() });
24334
- if (!payment)
24335
- throw new Error(`No payment matches ${ref}.`);
24336
- return payment;
24337
- };
24338
- var resolveRecurringCharge = async (client3, ref) => {
24339
- const charge = await client3.query(api2.recurringCharges.get, { id: ref.trim() });
24340
- if (!charge)
24341
- throw new Error(`No recurring charge matches ${ref}.`);
24342
- return charge;
24343
- };
24344
- var resolveCost = async (client3, ref) => {
24345
- const cost = await client3.query(api2.costs.get, { id: ref.trim(), today: localToday() });
24346
- if (!cost)
24347
- throw new Error(`No cost matches ${ref}.`);
24348
- return cost;
24349
- };
24350
24407
  var repoProject = async (client3) => {
24351
24408
  const repo = await currentRepoKey();
24352
24409
  if (!repo)
@@ -24356,23 +24413,6 @@ var repoProject = async (client3) => {
24356
24413
  throw new Error(`No project is connected to ${repo}. Connect one on the project in the dashboard.`);
24357
24414
  return project;
24358
24415
  };
24359
- var missingLabelError = async (client3, lead) => {
24360
- const labels = await client3.query(api2.issueLabels.list, {});
24361
- const available = labels.map((candidate) => candidate.name).join(", ");
24362
- return new Error(available ? `${lead} The labels are: ${available}.` : "There are no labels yet.");
24363
- };
24364
- var resolveIssueLabels = async (client3, names) => await Promise.all(names.map(async (name) => {
24365
- const label = await client3.query(api2.issueLabels.find, { ref: name });
24366
- if (!label)
24367
- throw await missingLabelError(client3, `No label named "${name}".`);
24368
- return label;
24369
- }));
24370
- var resolveIssueLabel = async (client3, ref) => {
24371
- const label = await client3.query(api2.issueLabels.find, { ref });
24372
- if (!label)
24373
- throw await missingLabelError(client3, `No label matches "${ref}".`);
24374
- return label;
24375
- };
24376
24416
  var resolveMilestone = async (client3, projectId, name) => {
24377
24417
  const milestone = await client3.query(api2.milestones.find, { projectId, name });
24378
24418
  if (!milestone) {
@@ -24383,62 +24423,6 @@ var resolveMilestone = async (client3, projectId, name) => {
24383
24423
  return milestone;
24384
24424
  };
24385
24425
 
24386
- // src/commands/clients/archive.ts
24387
- var archive = command({
24388
- name: "archive",
24389
- description: "Archive a client: it leaves pickers and default lists, and every existing reference stays intact",
24390
- positionals: {
24391
- id: exports_external.string().describe("Client id or URL")
24392
- },
24393
- run: async ({ positionals: { id } }) => {
24394
- const backend = await backendClient();
24395
- const client3 = await resolveClient(backend, id);
24396
- await backend.mutation(api2.clients.archive, { id: client3._id });
24397
- console.log(`Archived ${client3.name}.`);
24398
- }
24399
- });
24400
-
24401
- // src/commands/clients/charges/add.ts
24402
- var add = command({
24403
- name: "add",
24404
- description: "Add a recurring charge to a client and print its id",
24405
- positionals: {
24406
- client: exports_external.string().describe("Client id or URL"),
24407
- description: exports_external.string().describe('What the charge is for, e.g. "Maintenance retainer"')
24408
- },
24409
- options: {
24410
- amount: exports_external.coerce.number().int().describe("Amount in integer minor units (2500 = 25.00)").meta({ short: "a" }),
24411
- currency: exports_external.string().describe("Three-letter code like USD or EUR").meta({ short: "c" }),
24412
- cadence: exports_external.enum(RECURRING_CHARGE_CADENCES).default("monthly").describe("monthly or yearly"),
24413
- "due-month": exports_external.coerce.number().int().optional().describe("Month (1-12) a yearly charge falls due; defaults to the current month")
24414
- },
24415
- run: async ({ positionals: { client: ref, description }, options }) => {
24416
- const backend = await backendClient();
24417
- const client3 = await resolveClient(backend, ref);
24418
- const chargeId = await backend.mutation(api2.recurringCharges.create, {
24419
- clientId: client3._id,
24420
- description,
24421
- amount: options.amount,
24422
- currency: options.currency,
24423
- cadence: options.cadence,
24424
- dueMonth: options["due-month"]
24425
- });
24426
- console.log(chargeId);
24427
- }
24428
- });
24429
-
24430
- // src/lib/output.ts
24431
- var formatMinorAmount = (amount, currency) => `${(amount / 100).toFixed(2)} ${currency}`;
24432
- var printTable = (rows) => {
24433
- if (rows.length === 0)
24434
- return;
24435
- const columnCount = Math.max(...rows.map((row) => row.length));
24436
- const widths = Array.from({ length: columnCount }, (_, column) => Math.max(...rows.map((row) => (row[column] ?? "").length)));
24437
- for (const row of rows) {
24438
- console.log(row.map((cell, column) => cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd());
24439
- }
24440
- };
24441
-
24442
24426
  // src/commands/clients/charges/list.ts
24443
24427
  var monthName = (month) => new Date(Date.UTC(2000, month - 1)).toLocaleString("en-US", { month: "short", timeZone: "UTC" });
24444
24428
  var list = command({
@@ -24480,9 +24464,8 @@ var pause = command({
24480
24464
  },
24481
24465
  run: async ({ positionals: { id } }) => {
24482
24466
  const backend = await backendClient();
24483
- const charge = await resolveRecurringCharge(backend, id);
24484
- await backend.mutation(api2.recurringCharges.pause, { id: charge._id });
24485
- console.log(`Paused ${charge.description}.`);
24467
+ const { description } = await backend.mutation(api2.recurringCharges.pause, { id: id.trim() });
24468
+ console.log(`Paused ${description}.`);
24486
24469
  }
24487
24470
  });
24488
24471
 
@@ -24495,9 +24478,8 @@ var remove = command({
24495
24478
  },
24496
24479
  run: async ({ positionals: { id } }) => {
24497
24480
  const backend = await backendClient();
24498
- const charge = await resolveRecurringCharge(backend, id);
24499
- await backend.mutation(api2.recurringCharges.remove, { id: charge._id });
24500
- console.log(`Removed ${charge.description}.`);
24481
+ const { description } = await backend.mutation(api2.recurringCharges.remove, { id: id.trim() });
24482
+ console.log(`Removed ${description}.`);
24501
24483
  }
24502
24484
  });
24503
24485
 
@@ -24510,9 +24492,8 @@ var resume = command({
24510
24492
  },
24511
24493
  run: async ({ positionals: { id } }) => {
24512
24494
  const backend = await backendClient();
24513
- const charge = await resolveRecurringCharge(backend, id);
24514
- await backend.mutation(api2.recurringCharges.resume, { id: charge._id });
24515
- console.log(`Resumed ${charge.description}.`);
24495
+ const { description } = await backend.mutation(api2.recurringCharges.resume, { id: id.trim() });
24496
+ console.log(`Resumed ${description}.`);
24516
24497
  }
24517
24498
  });
24518
24499
 
@@ -24526,7 +24507,7 @@ var update = command({
24526
24507
  options: {
24527
24508
  description: exports_external.string().optional().describe("New description").meta({ short: "d" }),
24528
24509
  amount: exports_external.coerce.number().int().optional().describe("Amount in integer minor units").meta({ short: "a" }),
24529
- currency: exports_external.string().optional().describe("Three-letter code like USD or EUR").meta({ short: "c" }),
24510
+ currency: currencyCode.optional().describe(`One of ${CURRENCY_CHOICES}`).meta({ short: "c" }),
24530
24511
  cadence: exports_external.enum(RECURRING_CHARGE_CADENCES).optional().describe("monthly or yearly"),
24531
24512
  "due-month": exports_external.coerce.number().int().optional().describe("Month (1-12) a yearly charge falls due")
24532
24513
  },
@@ -24535,16 +24516,15 @@ var update = command({
24535
24516
  throw new Error("Nothing to update. Pass --description, --amount, --currency, --cadence, or --due-month.");
24536
24517
  }
24537
24518
  const backend = await backendClient();
24538
- const charge = await resolveRecurringCharge(backend, id);
24539
- await backend.mutation(api2.recurringCharges.update, {
24540
- id: charge._id,
24519
+ const { description } = await backend.mutation(api2.recurringCharges.update, {
24520
+ id: id.trim(),
24541
24521
  description: options.description,
24542
24522
  amount: options.amount,
24543
24523
  currency: options.currency,
24544
24524
  cadence: options.cadence,
24545
24525
  dueMonth: options["due-month"]
24546
24526
  });
24547
- console.log(`Updated ${options.description ?? charge.description}.`);
24527
+ console.log(`Updated ${description}.`);
24548
24528
  }
24549
24529
  });
24550
24530
 
@@ -24658,65 +24638,6 @@ More clients exist; raise --limit past ${limit}.`);
24658
24638
  }
24659
24639
  });
24660
24640
 
24661
- // ../../packages/backend/convex/lib/clientProfile.ts
24662
- var normalizeClientName = (value) => {
24663
- const name = value.trim();
24664
- if (!name)
24665
- throw new ConvexError("Client name is required.");
24666
- if (name.length > MAX_CLIENT_NAME_LENGTH) {
24667
- throw new ConvexError(`Client name must be ${MAX_CLIENT_NAME_LENGTH} characters or fewer.`);
24668
- }
24669
- return name;
24670
- };
24671
- var normalizeOptionalText = (value, label, max) => {
24672
- const text = value?.trim();
24673
- if (!text)
24674
- return;
24675
- if (text.length > max)
24676
- throw new ConvexError(`${label} must be ${max} characters or fewer.`);
24677
- return text;
24678
- };
24679
- var normalizeLegalName = (value) => normalizeOptionalText(value, "Legal name", MAX_CLIENT_LEGAL_NAME_LENGTH);
24680
- var normalizeBillingAddress = (value) => normalizeOptionalText(value, "Billing address", MAX_CLIENT_BILLING_ADDRESS_LENGTH);
24681
- var normalizeTaxId = (value) => normalizeOptionalText(value, "Tax ID", MAX_CLIENT_TAX_ID_LENGTH);
24682
- var normalizeBillingEmail = (value) => {
24683
- const email3 = normalizeOptionalText(value, "Billing email", MAX_CLIENT_BILLING_EMAIL_LENGTH);
24684
- if (email3 === undefined)
24685
- return;
24686
- if (!/^[^\s@]+@[^\s@]+$/.test(email3))
24687
- throw new ConvexError("Billing email must be a single valid address.");
24688
- return email3;
24689
- };
24690
- var assertPaymentTermsDays = (value) => {
24691
- if (value === undefined)
24692
- return;
24693
- if (!Number.isInteger(value) || value < 0 || value > MAX_CLIENT_PAYMENT_TERMS_DAYS) {
24694
- throw new ConvexError(`Payment terms must be a whole number of days between 0 and ${MAX_CLIENT_PAYMENT_TERMS_DAYS}.`);
24695
- }
24696
- return value;
24697
- };
24698
- var normalizeCurrencyCode = (value) => {
24699
- const code2 = value?.trim().toUpperCase();
24700
- if (!code2)
24701
- return;
24702
- if (!/^[A-Z]{3}$/.test(code2))
24703
- throw new ConvexError("Currency must be a three-letter code like USD or EUR.");
24704
- return code2;
24705
- };
24706
-
24707
- // ../../node_modules/.bun/convex-helpers@0.1.120+1dd148a8b8056605/node_modules/convex-helpers/index.js
24708
- var _error = Symbol();
24709
-
24710
- // ../../packages/backend/convex/lib/billingRun.ts
24711
- var assertBillingDay = (value) => {
24712
- if (value === undefined)
24713
- return;
24714
- if (!Number.isInteger(value) || value < 1 || value > MAX_BILLING_DAY) {
24715
- throw new ConvexError(`Billing day must be a whole number from 1 to ${MAX_BILLING_DAY}.`);
24716
- }
24717
- return value;
24718
- };
24719
-
24720
24641
  // src/commands/clients/update.ts
24721
24642
  var update2 = command({
24722
24643
  name: "update",
@@ -24740,42 +24661,19 @@ var update2 = command({
24740
24661
  throw new Error("Nothing to update. Pass a profile flag, or a --no form to clear one.");
24741
24662
  }
24742
24663
  const text = (value) => value === null ? "" : value;
24743
- const profile = {
24744
- name: options.name,
24745
- legalName: text(options["legal-name"]),
24746
- billingAddress: text(options.address),
24747
- billingEmail: text(options.email),
24748
- taxId: text(options["tax-id"])
24749
- };
24750
- const terms = options["payment-terms"];
24751
- const billingDay = options["billing-day"];
24752
- if (profile.name !== undefined)
24753
- normalizeClientName(profile.name);
24754
- normalizeLegalName(profile.legalName);
24755
- normalizeBillingAddress(profile.billingAddress);
24756
- normalizeBillingEmail(profile.billingEmail);
24757
- normalizeTaxId(profile.taxId);
24758
- assertPaymentTermsDays(terms ?? undefined);
24759
- normalizeCurrencyCode(options.currency ?? undefined);
24760
- assertBillingDay(billingDay ?? undefined);
24761
24664
  const backend = await backendClient();
24762
- const client3 = await resolveClient(backend, id);
24763
- if (Object.values(profile).some((value) => value !== undefined)) {
24764
- await backend.mutation(api2.clients.update, { id: client3._id, ...profile });
24765
- }
24766
- if (terms !== undefined) {
24767
- await backend.mutation(api2.clients.setPaymentTerms, { id: client3._id, paymentTermsDays: terms ?? undefined });
24768
- }
24769
- if (options.currency !== undefined) {
24770
- await backend.mutation(api2.clients.setDefaultCurrency, {
24771
- id: client3._id,
24772
- defaultCurrency: options.currency ?? undefined
24773
- });
24774
- }
24775
- if (billingDay !== undefined) {
24776
- await backend.mutation(api2.clients.setBillingDay, { id: client3._id, billingDay: billingDay ?? undefined });
24777
- }
24778
- console.log(`Updated ${options.name?.trim() ?? client3.name}.`);
24665
+ const { name } = await backend.mutation(api2.clients.edit, {
24666
+ id: readClientRef(id),
24667
+ ...options.name !== undefined ? { name: options.name } : {},
24668
+ ...options["legal-name"] !== undefined ? { legalName: text(options["legal-name"]) } : {},
24669
+ ...options.address !== undefined ? { billingAddress: text(options.address) } : {},
24670
+ ...options.email !== undefined ? { billingEmail: text(options.email) } : {},
24671
+ ...options["tax-id"] !== undefined ? { taxId: text(options["tax-id"]) } : {},
24672
+ ...options["payment-terms"] !== undefined ? { paymentTermsDays: options["payment-terms"] } : {},
24673
+ ...options.currency !== undefined ? { defaultCurrency: options.currency } : {},
24674
+ ...options["billing-day"] !== undefined ? { billingDay: options["billing-day"] } : {}
24675
+ });
24676
+ console.log(`Updated ${name}.`);
24779
24677
  }
24780
24678
  });
24781
24679
 
@@ -24801,7 +24699,7 @@ the same client.`,
24801
24699
  },
24802
24700
  options: {
24803
24701
  amount: exports_external.coerce.number().int().describe("Amount in integer minor units (2500 = 25.00)").meta({ short: "a" }),
24804
- currency: exports_external.string().describe("Three-letter code like USD or EUR").meta({ short: "c" }),
24702
+ currency: currencyCode.default(DEFAULT_CURRENCY).describe(`One of ${CURRENCY_CHOICES}`).meta({ short: "c" }),
24805
24703
  kind: exports_external.enum(COST_KINDS).default("one_off").describe("one_off, monthly, or yearly").meta({ short: "k" }),
24806
24704
  date: exports_external.string().optional().describe("The one-off's date, or a subscription's start (YYYY-MM-DD); default today"),
24807
24705
  end: exports_external.string().optional().describe("Last date a subscription is committed to (YYYY-MM-DD)"),
@@ -24811,7 +24709,6 @@ the same client.`,
24811
24709
  },
24812
24710
  run: async ({ positionals: { name }, options }) => {
24813
24711
  const backend = await backendClient();
24814
- const projects = await Promise.all((options.project ?? []).map((ref) => resolveProject(backend, ref)));
24815
24712
  const costId = await backend.mutation(api2.costs.create, {
24816
24713
  name,
24817
24714
  note: options.note,
@@ -24820,7 +24717,7 @@ the same client.`,
24820
24717
  endDate: options.end,
24821
24718
  amount: options.amount,
24822
24719
  currency: options.currency,
24823
- projectIds: projects.map((project) => project._id),
24720
+ projectIds: options.project?.map(readProjectRef),
24824
24721
  billable: options.billable
24825
24722
  });
24826
24723
  console.log(costId);
@@ -24840,10 +24737,9 @@ var end = command({
24840
24737
  },
24841
24738
  run: async ({ positionals: { id }, options }) => {
24842
24739
  const backend = await backendClient();
24843
- const cost = await resolveCost(backend, id);
24844
24740
  const today = localToday();
24845
- await backend.mutation(api2.costs.end, { id: cost._id, endDate: options.date, today });
24846
- console.log(`Ended ${cost.name} on ${options.date ?? today}.`);
24741
+ const { name } = await backend.mutation(api2.costs.end, { id: id.trim(), endDate: options.date, today });
24742
+ console.log(`Ended ${name} on ${options.date ?? today}.`);
24847
24743
  }
24848
24744
  });
24849
24745
 
@@ -24954,9 +24850,8 @@ var remove2 = command({
24954
24850
  },
24955
24851
  run: async ({ positionals: { id } }) => {
24956
24852
  const backend = await backendClient();
24957
- const cost = await resolveCost(backend, id);
24958
- await backend.mutation(api2.costs.remove, { id: cost._id });
24959
- console.log(`Deleted ${cost.name}.`);
24853
+ const { name } = await backend.mutation(api2.costs.remove, { id: id.trim() });
24854
+ console.log(`Deleted ${name}.`);
24960
24855
  }
24961
24856
  });
24962
24857
 
@@ -24975,7 +24870,7 @@ is fixed at creation \u2014 delete and re-record to change it.`,
24975
24870
  options: {
24976
24871
  name: exports_external.string().optional().describe("New name"),
24977
24872
  amount: exports_external.coerce.number().int().optional().describe("Amount in integer minor units").meta({ short: "a" }),
24978
- currency: exports_external.string().optional().describe("Three-letter code like USD or EUR").meta({ short: "c" }),
24873
+ currency: currencyCode.optional().describe(`One of ${CURRENCY_CHOICES}`).meta({ short: "c" }),
24979
24874
  date: exports_external.string().optional().describe("The one-off's date, or a subscription's start (YYYY-MM-DD)"),
24980
24875
  end: exports_external.string().nullable().optional().describe("End date (YYYY-MM-DD), or --no-end to resume").meta({
24981
24876
  negatable: true
@@ -24991,10 +24886,8 @@ is fixed at creation \u2014 delete and re-record to change it.`,
24991
24886
  throw new Error("Nothing to update. Pass --name, --amount, --currency, --date, --end, --project, --billable, or --note.");
24992
24887
  }
24993
24888
  const backend = await backendClient();
24994
- const cost = await resolveCost(backend, id);
24995
- const projects = options.project ? await Promise.all(options.project.map((ref) => resolveProject(backend, ref))) : [];
24996
- await backend.mutation(api2.costs.update, {
24997
- id: cost._id,
24889
+ const { name } = await backend.mutation(api2.costs.update, {
24890
+ id: id.trim(),
24998
24891
  today: localToday(),
24999
24892
  name: options.name,
25000
24893
  note: options.note === null ? "" : options.note,
@@ -25002,10 +24895,10 @@ is fixed at creation \u2014 delete and re-record to change it.`,
25002
24895
  endDate: options.end === null ? "" : options.end,
25003
24896
  amount: options.amount,
25004
24897
  currency: options.currency,
25005
- projectIds: options.project === undefined ? undefined : projects.map((project) => project._id),
24898
+ projectIds: options.project === null ? [] : options.project?.map(readProjectRef),
25006
24899
  billable: options.billable
25007
24900
  });
25008
- console.log(`Updated ${options.name ?? cost.name}.`);
24901
+ console.log(`Updated ${name}.`);
25009
24902
  }
25010
24903
  });
25011
24904
 
@@ -25309,9 +25202,10 @@ var seen = command({
25309
25202
  },
25310
25203
  run: async ({ positionals: { id } }) => {
25311
25204
  const client3 = await backendClient();
25312
- const issue2 = id === undefined ? undefined : await resolveIssue(client3, id);
25313
- await client3.mutation(api2.inbox.markSeen, { targetId: issue2?._id });
25314
- console.log(issue2 ? `${issue2.identifier} marked seen.` : "Inbox marked seen.");
25205
+ const { identifier } = await client3.mutation(api2.inbox.markSeen, {
25206
+ targetId: id === undefined ? undefined : readIssueRef(id)
25207
+ });
25208
+ console.log(identifier ? `${identifier} marked seen.` : "Inbox marked seen.");
25315
25209
  }
25316
25210
  });
25317
25211
 
@@ -25325,10 +25219,10 @@ var inbox = group({
25325
25219
  });
25326
25220
 
25327
25221
  // src/lib/line-items.ts
25328
- var LINE_ITEM_SYNTAX = 'description|quantity[ unit]|unit price|currency, e.g. "Development|2.5 hours|10000|EUR"';
25222
+ var LINE_ITEM_SYNTAX = 'description|quantity[ unit]|unit price[|currency], e.g. "Development|2.5 hours|10000|USD"; currency defaults to EGP';
25329
25223
  var parseLineItem = (value) => {
25330
25224
  const parts = value.split("|");
25331
- if (parts.length !== 4)
25225
+ if (parts.length !== 3 && parts.length !== 4)
25332
25226
  throw new Error(`Line items read as ${LINE_ITEM_SYNTAX}; got "${value}".`);
25333
25227
  const [description = "", quantityPart = "", pricePart = "", currency = ""] = parts;
25334
25228
  const [rawQuantity = "", ...unitWords] = quantityPart.trim().split(/\s+/);
@@ -25341,12 +25235,18 @@ var parseLineItem = (value) => {
25341
25235
  throw new Error(`Unit prices must be integer minor units (10000 = 100.00), got "${pricePart.trim()}".`);
25342
25236
  }
25343
25237
  const unit = unitWords.join(" ");
25344
- return { description: description.trim(), quantity, unit: unit || undefined, unitPrice, currency: currency.trim() };
25238
+ return {
25239
+ description: description.trim(),
25240
+ quantity,
25241
+ unit: unit || undefined,
25242
+ unitPrice,
25243
+ currency: parseLineCurrency(currency)
25244
+ };
25345
25245
  };
25346
- var QUOTATION_LINE_ITEM_SYNTAX = 'description|quantity[ unit]|unit price|currency[|section], e.g. "Prototype|1|100000|EUR|Phase 1"';
25246
+ var QUOTATION_LINE_ITEM_SYNTAX = 'description|quantity[ unit]|unit price[|currency[|section]], e.g. "Prototype|1|100000|USD|Phase 1"; an empty currency part defaults to EGP';
25347
25247
  var parseQuotationLineItem = (value) => {
25348
25248
  const parts = value.split("|");
25349
- if (parts.length !== 4 && parts.length !== 5) {
25249
+ if (parts.length < 3 || parts.length > 5) {
25350
25250
  throw new Error(`Quotation line items read as ${QUOTATION_LINE_ITEM_SYNTAX}; got "${value}".`);
25351
25251
  }
25352
25252
  const section = parts.length === 5 ? parts.pop()?.trim() : undefined;
@@ -25369,11 +25269,9 @@ var create3 = command({
25369
25269
  },
25370
25270
  run: async ({ positionals: { client: ref }, options }) => {
25371
25271
  const backend = await backendClient();
25372
- const client3 = await resolveClient(backend, ref);
25373
- const project = options.project === undefined ? undefined : await resolveProject(backend, options.project);
25374
25272
  const invoiceId = await backend.mutation(api2.invoices.create, {
25375
- clientId: client3._id,
25376
- projectId: project?._id,
25273
+ clientId: readClientRef(ref),
25274
+ projectId: options.project === undefined ? undefined : readProjectRef(options.project),
25377
25275
  lineItems: options.line?.map(parseLineItem),
25378
25276
  notes: options.notes,
25379
25277
  dueDate: options.due
@@ -25391,8 +25289,21 @@ var duplicate = command({
25391
25289
  },
25392
25290
  run: async ({ positionals: { id } }) => {
25393
25291
  const backend = await backendClient();
25394
- const invoice = await resolveInvoice(backend, id);
25395
- console.log(await backend.mutation(api2.invoices.duplicate, { id: invoice._id }));
25292
+ console.log(await backend.mutation(api2.invoices.duplicate, { id: readInvoiceRef(id) }));
25293
+ }
25294
+ });
25295
+
25296
+ // src/commands/invoices/email.ts
25297
+ var email3 = command({
25298
+ name: "email",
25299
+ description: "Email the invoice's link to the client's billing address; repeatable, and never required",
25300
+ positionals: {
25301
+ id: exports_external.string().describe("Invoice id or URL")
25302
+ },
25303
+ run: async ({ positionals: { id } }) => {
25304
+ const backend = await backendClient();
25305
+ const { to, number: number4 } = await backend.mutation(api2.invoices.email, { id: readInvoiceRef(id) });
25306
+ console.log(`Emailed ${number4} to ${to}.`);
25396
25307
  }
25397
25308
  });
25398
25309
 
@@ -25421,6 +25332,9 @@ var get3 = command({
25421
25332
  console.log(`Due: ${invoice.dueDate}`);
25422
25333
  if (invoice.publicUrl)
25423
25334
  console.log(`Link: ${invoice.publicUrl}`);
25335
+ if (invoice.status !== "draft") {
25336
+ console.log(`Emailed: ${invoice.lastEmailedAt === undefined ? "never" : localDateString(invoice.lastEmailedAt)}`);
25337
+ }
25424
25338
  if (invoice.lineItems.length > 0) {
25425
25339
  console.log("");
25426
25340
  printTable([
@@ -25460,9 +25374,7 @@ var issue2 = command({
25460
25374
  },
25461
25375
  run: async ({ positionals: { id } }) => {
25462
25376
  const backend = await backendClient();
25463
- const invoice = await resolveInvoice(backend, id);
25464
- await backend.mutation(api2.invoices.issue, { id: invoice._id, today: localToday() });
25465
- const issued = await resolveInvoice(backend, invoice._id);
25377
+ const issued = await backend.mutation(api2.invoices.issue, { id: readInvoiceRef(id), today: localToday() });
25466
25378
  console.log(`Issued ${issued.number}, due ${issued.dueDate}.`);
25467
25379
  }
25468
25380
  });
@@ -25477,10 +25389,11 @@ var link = command({
25477
25389
  },
25478
25390
  run: async ({ positionals: { id, project: projectRef } }) => {
25479
25391
  const backend = await backendClient();
25480
- const invoice = await resolveInvoice(backend, id);
25481
- const project = projectRef === undefined ? undefined : await resolveProject(backend, projectRef);
25482
- await backend.mutation(api2.invoices.setProject, { id: invoice._id, projectId: project?._id });
25483
- console.log(project ? `Linked to ${project.name}.` : "Cleared the project link.");
25392
+ const { projectName } = await backend.mutation(api2.invoices.setProject, {
25393
+ id: readInvoiceRef(id),
25394
+ projectId: projectRef === undefined ? undefined : readProjectRef(projectRef)
25395
+ });
25396
+ console.log(projectName === undefined ? "Cleared the project link." : `Linked to ${projectName}.`);
25484
25397
  }
25485
25398
  });
25486
25399
 
@@ -25508,7 +25421,7 @@ var list4 = command({
25508
25421
  if (result.page.length === 0)
25509
25422
  return console.log("No invoices yet.");
25510
25423
  printTable([
25511
- ["NUMBER", "CLIENT", "PROJECT", "STATUS", "TOTAL", "DUE", "ID"],
25424
+ ["NUMBER", "CLIENT", "PROJECT", "STATUS", "TOTAL", "DUE", "EMAILED", "ID"],
25512
25425
  ...result.page.map((invoice) => [
25513
25426
  invoice.number ?? "draft",
25514
25427
  invoice.clientName,
@@ -25516,6 +25429,7 @@ var list4 = command({
25516
25429
  invoice.derivedStatus,
25517
25430
  formatPerCurrency(invoice.totals.map(({ currency, total }) => ({ currency, amount: total }))),
25518
25431
  invoice.dueDate ?? "-",
25432
+ invoice.lastEmailedAt === undefined ? "-" : localDateString(invoice.lastEmailedAt),
25519
25433
  invoice._id
25520
25434
  ])
25521
25435
  ]);
@@ -25540,18 +25454,16 @@ var add2 = command({
25540
25454
  },
25541
25455
  run: async ({ positionals: { invoice: ref }, options }) => {
25542
25456
  const backend = await backendClient();
25543
- const invoice = await resolveInvoice(backend, ref);
25544
- const paymentId = await backend.mutation(api2.invoices.addPayment, {
25545
- invoiceId: invoice._id,
25457
+ const payment = await backend.mutation(api2.invoices.addPayment, {
25458
+ invoiceId: readInvoiceRef(ref),
25546
25459
  amount: options.amount,
25547
25460
  currency: options.currency,
25548
25461
  paidOn: options.date ?? localToday(),
25549
25462
  note: options.note
25550
25463
  });
25551
- console.log(paymentId);
25552
- if ((await resolveInvoice(backend, invoice._id)).status === "paid") {
25553
- console.log(`${invoice.number} is now fully paid.`);
25554
- }
25464
+ console.log(payment.id);
25465
+ if (payment.paid)
25466
+ console.log(`${payment.number} is now fully paid.`);
25555
25467
  }
25556
25468
  });
25557
25469
 
@@ -25592,8 +25504,7 @@ var remove3 = command({
25592
25504
  },
25593
25505
  run: async ({ positionals: { id } }) => {
25594
25506
  const backend = await backendClient();
25595
- const payment = await resolvePayment(backend, id);
25596
- await backend.mutation(api2.invoices.removePayment, { id: payment._id });
25507
+ await backend.mutation(api2.invoices.removePayment, { id: id.trim() });
25597
25508
  console.log("Removed the payment.");
25598
25509
  }
25599
25510
  });
@@ -25617,9 +25528,8 @@ var rotateLink = command({
25617
25528
  },
25618
25529
  run: async ({ positionals: { id } }) => {
25619
25530
  const backend = await backendClient();
25620
- const invoice = await resolveInvoice(backend, id);
25621
- const url2 = await backend.mutation(api2.invoices.rotateLink, { id: invoice._id });
25622
- console.log(`${invoice.number} now reads at ${url2}. Send it again \u2014 the previous link is dead.`);
25531
+ const { url: url2, number: number4 } = await backend.mutation(api2.invoices.rotateLink, { id: readInvoiceRef(id) });
25532
+ console.log(`${number4} now reads at ${url2}. Send it again \u2014 the previous link is dead.`);
25623
25533
  }
25624
25534
  });
25625
25535
 
@@ -25641,14 +25551,13 @@ var update4 = command({
25641
25551
  throw new Error("Nothing to update. Pass --line, --notes, or --due (or a --no form to clear).");
25642
25552
  }
25643
25553
  const backend = await backendClient();
25644
- const invoice = await resolveInvoice(backend, id);
25645
- await backend.mutation(api2.invoices.update, {
25646
- id: invoice._id,
25554
+ const { number: number4 } = await backend.mutation(api2.invoices.update, {
25555
+ id: readInvoiceRef(id),
25647
25556
  lineItems: options.line?.map(parseLineItem),
25648
25557
  notes: options.notes === null ? "" : options.notes,
25649
25558
  dueDate: options.due === null ? "" : options.due
25650
25559
  });
25651
- console.log(`Updated ${invoice.number ?? "draft"}.`);
25560
+ console.log(`Updated ${number4 ?? "draft"}.`);
25652
25561
  }
25653
25562
  });
25654
25563
 
@@ -25661,9 +25570,8 @@ var voidInvoice = command({
25661
25570
  },
25662
25571
  run: async ({ positionals: { id } }) => {
25663
25572
  const backend = await backendClient();
25664
- const invoice = await resolveInvoice(backend, id);
25665
- await backend.mutation(api2.invoices.voidInvoice, { id: invoice._id });
25666
- console.log(`Voided ${invoice.number}.`);
25573
+ const { number: number4 } = await backend.mutation(api2.invoices.voidInvoice, { id: readInvoiceRef(id) });
25574
+ console.log(`Voided ${number4}.`);
25667
25575
  }
25668
25576
  });
25669
25577
 
@@ -25674,7 +25582,7 @@ var invoices = group({
25674
25582
  longDescription: `An invoice is a draft until issued: issuing assigns its gapless number, freezes the
25675
25583
  document, and snapshots the client's billing details. Sent invoices are immutable \u2014
25676
25584
  the fix path is void + duplicate into a fresh draft. Drafts delete outright.`,
25677
- commands: [create3, list4, get3, update4, issue2, voidInvoice, duplicate, link, rotateLink, payments]
25585
+ commands: [create3, list4, get3, update4, issue2, email3, voidInvoice, duplicate, link, rotateLink, payments]
25678
25586
  });
25679
25587
 
25680
25588
  // src/lib/attachments.ts
@@ -25743,10 +25651,15 @@ var comment = command({
25743
25651
  },
25744
25652
  run: async ({ positionals: { id, body }, options: { attach } }) => {
25745
25653
  const client3 = await backendClient();
25746
- const issue3 = await resolveIssue(client3, id);
25747
- printAttachments(await attachFiles(client3, issue3._id, attach ?? []));
25748
- await client3.mutation(api2.issueComments.create, { issueId: issue3._id, bodyMarkdown: await readTextOption(body) });
25749
- console.log(`Commented on ${issue3.identifier}.`);
25654
+ if (attach !== undefined && attach.length > 0) {
25655
+ const issue3 = await resolveIssue(client3, id);
25656
+ printAttachments(await attachFiles(client3, issue3._id, attach));
25657
+ }
25658
+ const { identifier } = await client3.mutation(api2.issueComments.create, {
25659
+ issueId: readIssueRef(id),
25660
+ bodyMarkdown: await readTextOption(body)
25661
+ });
25662
+ console.log(`Commented on ${identifier}.`);
25750
25663
  }
25751
25664
  });
25752
25665
 
@@ -25766,7 +25679,6 @@ var agentConfigurationLine = (id) => {
25766
25679
  var create4 = command({
25767
25680
  name: "create",
25768
25681
  description: "Create an issue and print its identifier",
25769
- longDescription: "--disposition ready_for_agent requires --agent-config; the agent queue refuses an issue without one.",
25770
25682
  positionals: {
25771
25683
  title: exports_external.string().describe("Issue title")
25772
25684
  },
@@ -25781,17 +25693,22 @@ var create4 = command({
25781
25693
  milestone: exports_external.string().optional().describe("Milestone name (needs --project or --here)"),
25782
25694
  parent: exports_external.string().optional().describe("Create as a sub-issue of this issue (identifier, number, or URL)"),
25783
25695
  disposition: exports_external.enum(ISSUE_CREATE_DISPOSITIONS).optional().describe("Route immediately (default needs_triage)"),
25696
+ label: exports_external.array(exports_external.string()).optional().describe("Add a label by name (repeatable)"),
25784
25697
  "agent-config": exports_external.enum(AGENT_CONFIGURATION_IDS).optional().describe(`Agent configuration${agentConfigurationLegacyNote()}`)
25785
25698
  },
25786
25699
  run: async ({ positionals: { title }, options }) => {
25787
25700
  if (options.project && options.here)
25788
25701
  throw new Error("Pass --project or --here, not both.");
25789
- const client3 = await backendClient();
25790
- const project = options.project ? await resolveProject(client3, options.project) : options.here ? await repoProject(client3) : undefined;
25791
- if (options.milestone && !project)
25702
+ let projectRef = options.project === undefined ? undefined : readProjectRef(options.project);
25703
+ if (options.here) {
25704
+ const repo = await currentRepoKey();
25705
+ if (!repo)
25706
+ throw new Error("No repository here: not a git checkout with an origin remote.");
25707
+ projectRef = repo;
25708
+ }
25709
+ if (options.milestone && projectRef === undefined)
25792
25710
  throw new Error("A milestone needs --project or --here.");
25793
- const milestone = project && options.milestone ? await resolveMilestone(client3, project._id, options.milestone) : undefined;
25794
- const parent = options.parent ? await resolveIssue(client3, options.parent) : undefined;
25711
+ const client3 = await backendClient();
25795
25712
  const { identifier } = await client3.mutation(api2.issues.create, {
25796
25713
  title,
25797
25714
  descriptionMarkdown: options.description === undefined ? undefined : await readTextOption(options.description),
@@ -25799,11 +25716,12 @@ var create4 = command({
25799
25716
  priority: options.priority,
25800
25717
  estimate: options.estimate,
25801
25718
  dueDate: options.due,
25802
- projectId: project?._id,
25803
- milestoneId: milestone?._id,
25804
- parentId: parent?._id,
25719
+ projectId: projectRef,
25720
+ milestoneId: options.milestone,
25721
+ parentId: options.parent === undefined ? undefined : readIssueRef(options.parent),
25805
25722
  disposition: options.disposition,
25806
- agentConfigurationId: options["agent-config"]
25723
+ agentConfigurationId: options["agent-config"],
25724
+ labels: options.label
25807
25725
  });
25808
25726
  console.log(identifier);
25809
25727
  }
@@ -25821,86 +25739,100 @@ var printTasks = (tasks) => {
25821
25739
  };
25822
25740
 
25823
25741
  // src/commands/issues/get.ts
25742
+ var printIssue = ({ relations, attachments, tasks, feed, milestone, ...issue3 }) => {
25743
+ const project = issue3.project;
25744
+ console.log(`${issue3.identifier} ${issue3.title}`);
25745
+ console.log(`Status: ${issue3.status} \xB7 Priority: ${issue3.priority} \xB7 Disposition: ${issue3.disposition}`);
25746
+ if (issue3.estimate !== undefined)
25747
+ console.log(`Estimate: ${issue3.estimate}`);
25748
+ if (issue3.dueDate)
25749
+ console.log(`Due: ${issue3.dueDate}`);
25750
+ if (issue3.agentConfigurationId)
25751
+ console.log(`Agent: ${agentConfigurationLine(issue3.agentConfigurationId)}`);
25752
+ if (issue3.branch)
25753
+ console.log(`Branch: ${issue3.branch}`);
25754
+ if (issue3.prUrl)
25755
+ console.log(`PR: ${issue3.prUrl}`);
25756
+ if (project)
25757
+ console.log(`Project: ${project.name}${milestone ? ` \xB7 Milestone: ${milestone.name}` : ""}`);
25758
+ if (issue3.parent)
25759
+ console.log(`Parent: ${issue3.parent.identifier} ${issue3.parent.title}`);
25760
+ if (issue3.labels.length > 0)
25761
+ console.log(`Labels: ${issue3.labels.map((label) => label.name).join(", ")}`);
25762
+ if (issue3.children.total > 0)
25763
+ console.log(`Sub-issues: ${issue3.children.done}/${issue3.children.total} done`);
25764
+ const related = (entries) => entries.map((entry) => `${entry.issue.identifier} (${entry.issue.status})`).join(", ");
25765
+ if (relations.blockedBy.length > 0)
25766
+ console.log(`Blocked by: ${related(relations.blockedBy)}`);
25767
+ if (relations.blocks.length > 0)
25768
+ console.log(`Blocks: ${related(relations.blocks)}`);
25769
+ if (relations.duplicateOf)
25770
+ console.log(`Duplicate of: ${related([relations.duplicateOf])}`);
25771
+ if (relations.duplicates.length > 0)
25772
+ console.log(`Duplicated by: ${related(relations.duplicates)}`);
25773
+ if (relations.relatesTo.length > 0)
25774
+ console.log(`Related: ${related(relations.relatesTo)}`);
25775
+ if (attachments.length > 0) {
25776
+ console.log("Attachments:");
25777
+ for (const attachment of attachments) {
25778
+ console.log(` ${attachment.name} \u2014 ${attachment.url ?? "unavailable"}`);
25779
+ }
25780
+ }
25781
+ printTasks(tasks);
25782
+ if (issue3.descriptionMarkdown)
25783
+ console.log(`
25784
+ ${issue3.descriptionMarkdown}`);
25785
+ if (feed.events.length > 0)
25786
+ console.log(`
25787
+ Feed:`);
25788
+ if (feed.truncated)
25789
+ console.log("(older events truncated)");
25790
+ for (const event of feed.events) {
25791
+ const at = new Date(event.at).toISOString().slice(0, 16).replace("T", " ");
25792
+ if (event.type === "activity") {
25793
+ const via = event.via === undefined ? "" : ` \u2014 ${actorLabel(event.via, event.agent)}`;
25794
+ console.log(`${at} ${issueActivityLine(event.detail)}${via}`);
25795
+ } else {
25796
+ console.log(`${at} comment${event.editedAt ? " (edited)" : ""}:`);
25797
+ for (const line of event.bodyMarkdown.split(`
25798
+ `))
25799
+ console.log(` ${line}`);
25800
+ }
25801
+ }
25802
+ };
25824
25803
  var get4 = command({
25825
25804
  name: "get",
25826
- description: "Show an issue: properties, description, and its feed",
25805
+ description: "Show one or more issues: properties, description, and feed",
25827
25806
  positionals: {
25828
- id: exports_external.string().describe("Issue identifier, number, or URL")
25807
+ ids: exports_external.array(exports_external.string()).min(1).describe(`Issue identifiers, numbers, or URLs (up to ${MAX_ISSUES_PER_BATCH_READ} per call)`)
25829
25808
  },
25830
25809
  options: {
25831
- json: exports_external.boolean().default(false).describe("Print as JSON")
25810
+ json: exports_external.boolean().default(false).describe("Print as JSON (an array when given several issues)")
25832
25811
  },
25833
- run: async ({ positionals: { id }, options: { json: json2 } }) => {
25812
+ run: async ({ positionals: { ids }, options: { json: json2 } }) => {
25834
25813
  const client3 = await backendClient();
25835
- const issue3 = await resolveIssue(client3, id);
25836
- const [feed, relations, attachments, tasks] = await Promise.all([
25837
- client3.query(api2.issues.feed, { id: issue3._id }),
25838
- client3.query(api2.issues.relations, { id: issue3._id }),
25839
- client3.query(api2.issueAttachments.list, { issueId: issue3._id }),
25840
- client3.query(api2.issueTasks.list, { issueId: issue3._id })
25841
- ]);
25842
- if (json2)
25843
- return console.log(JSON.stringify({ ...issue3, relations, attachments, tasks, feed }, null, 2));
25844
- const project = issue3.project;
25845
- const milestone = issue3.milestoneId ? await client3.query(api2.milestones.get, { id: issue3.milestoneId, today: localToday() }) : null;
25846
- console.log(`${issue3.identifier} ${issue3.title}`);
25847
- console.log(`Status: ${issue3.status} \xB7 Priority: ${issue3.priority} \xB7 Disposition: ${issue3.disposition}`);
25848
- if (issue3.estimate !== undefined)
25849
- console.log(`Estimate: ${issue3.estimate}`);
25850
- if (issue3.dueDate)
25851
- console.log(`Due: ${issue3.dueDate}`);
25852
- if (issue3.agentConfigurationId)
25853
- console.log(`Agent: ${agentConfigurationLine(issue3.agentConfigurationId)}`);
25854
- if (issue3.branch)
25855
- console.log(`Branch: ${issue3.branch}`);
25856
- if (issue3.prUrl)
25857
- console.log(`PR: ${issue3.prUrl}`);
25858
- if (project)
25859
- console.log(`Project: ${project.name}${milestone ? ` \xB7 Milestone: ${milestone.name}` : ""}`);
25860
- if (issue3.parent)
25861
- console.log(`Parent: ${issue3.parent.identifier} ${issue3.parent.title}`);
25862
- if (issue3.labels.length > 0)
25863
- console.log(`Labels: ${issue3.labels.map((label) => label.name).join(", ")}`);
25864
- if (issue3.children.total > 0)
25865
- console.log(`Sub-issues: ${issue3.children.done}/${issue3.children.total} done`);
25866
- const related = (entries) => entries.map((entry) => `${entry.issue.identifier} (${entry.issue.status})`).join(", ");
25867
- if (relations.blockedBy.length > 0)
25868
- console.log(`Blocked by: ${related(relations.blockedBy)}`);
25869
- if (relations.blocks.length > 0)
25870
- console.log(`Blocks: ${related(relations.blocks)}`);
25871
- if (relations.duplicateOf)
25872
- console.log(`Duplicate of: ${related([relations.duplicateOf])}`);
25873
- if (relations.duplicates.length > 0)
25874
- console.log(`Duplicated by: ${related(relations.duplicates)}`);
25875
- if (relations.relatesTo.length > 0)
25876
- console.log(`Related: ${related(relations.relatesTo)}`);
25877
- if (attachments.length > 0) {
25878
- console.log("Attachments:");
25879
- for (const attachment of attachments) {
25880
- console.log(` ${attachment.name} \u2014 ${attachment.url ?? "unavailable"}`);
25881
- }
25882
- }
25883
- printTasks(tasks);
25884
- if (issue3.descriptionMarkdown)
25885
- console.log(`
25886
- ${issue3.descriptionMarkdown}`);
25887
- if (feed.events.length > 0)
25888
- console.log(`
25889
- Feed:`);
25890
- if (feed.truncated)
25891
- console.log("(older events truncated)");
25892
- for (const event of feed.events) {
25893
- const at = new Date(event.at).toISOString().slice(0, 16).replace("T", " ");
25894
- if (event.type === "activity") {
25895
- const via = event.via === undefined ? "" : ` \u2014 ${actorLabel(event.via, event.agent)}`;
25896
- console.log(`${at} ${issueActivityLine(event.detail)}${via}`);
25897
- } else {
25898
- console.log(`${at} comment${event.editedAt ? " (edited)" : ""}:`);
25899
- for (const line of event.bodyMarkdown.split(`
25900
- `))
25901
- console.log(` ${line}`);
25902
- }
25814
+ const found = await client3.query(api2.issues.getMany, { identifiers: ids.map(readIssueRef) });
25815
+ const missing = ids.filter((_, index) => !found[index]);
25816
+ if (missing.length === 1)
25817
+ throw new Error(`No issue matches ${missing[0]}.`);
25818
+ if (missing.length > 1)
25819
+ throw new Error(`No issues match ${missing.join(", ")}.`);
25820
+ const details = found.flatMap((detail) => detail ? [detail] : []);
25821
+ if (json2) {
25822
+ const objects = details.map(({ relations, attachments, tasks, feed, milestone: _, ...issue3 }) => ({
25823
+ ...issue3,
25824
+ relations,
25825
+ attachments,
25826
+ tasks,
25827
+ feed
25828
+ }));
25829
+ return console.log(JSON.stringify(details.length === 1 ? objects[0] : objects, null, 2));
25903
25830
  }
25831
+ details.forEach((detail, index) => {
25832
+ if (index > 0)
25833
+ console.log("---");
25834
+ printIssue(detail);
25835
+ });
25904
25836
  }
25905
25837
  });
25906
25838
 
@@ -25947,9 +25879,8 @@ var remove4 = command({
25947
25879
  },
25948
25880
  run: async ({ positionals: { label: ref } }) => {
25949
25881
  const client3 = await backendClient();
25950
- const label = await resolveIssueLabel(client3, ref);
25951
- await client3.mutation(api2.issueLabels.remove, { id: label._id });
25952
- console.log(`Deleted ${label.name}.`);
25882
+ const removed = await client3.mutation(api2.issueLabels.remove, { id: ref });
25883
+ console.log(`Deleted ${removed.name}.`);
25953
25884
  }
25954
25885
  });
25955
25886
 
@@ -25968,9 +25899,8 @@ var update5 = command({
25968
25899
  if (name === undefined && color === undefined)
25969
25900
  throw new Error("Nothing to update. Pass --name or --color.");
25970
25901
  const client3 = await backendClient();
25971
- const label = await resolveIssueLabel(client3, ref);
25972
- await client3.mutation(api2.issueLabels.update, { id: label._id, name, color });
25973
- console.log(`Updated ${name ?? label.name}.`);
25902
+ const updated = await client3.mutation(api2.issueLabels.update, { id: ref, name, color });
25903
+ console.log(`Updated ${updated.name}.`);
25974
25904
  }
25975
25905
  });
25976
25906
 
@@ -26069,9 +25999,8 @@ var markAddressed = command({
26069
25999
  },
26070
26000
  run: async ({ positionals: { id }, options: { commit, finding } }) => {
26071
26001
  const client3 = await backendClient();
26072
- const issue3 = await resolveIssue(client3, id);
26073
26002
  const result = await client3.action(api2.githubReviewer.markAddressed, {
26074
- id: issue3._id,
26003
+ id: readIssueRef(id),
26075
26004
  commitSha: commit,
26076
26005
  findings: finding?.map(parseFindingRef)
26077
26006
  });
@@ -26092,10 +26021,12 @@ var relate = command({
26092
26021
  },
26093
26022
  run: async ({ positionals: { id, kind, other } }) => {
26094
26023
  const client3 = await backendClient();
26095
- const issue3 = await resolveIssue(client3, id);
26096
- const otherIssue = await resolveIssue(client3, other);
26097
- await client3.mutation(api2.issues.addRelation, { id: issue3._id, kind, otherIssueId: otherIssue._id });
26098
- console.log(`${issue3.identifier} ${kind.replaceAll("_", " ")} ${otherIssue.identifier}.`);
26024
+ const { identifier, otherIdentifier } = await client3.mutation(api2.issues.addRelation, {
26025
+ id: readIssueRef(id),
26026
+ kind,
26027
+ otherIssueId: readIssueRef(other)
26028
+ });
26029
+ console.log(`${identifier} ${kind.replaceAll("_", " ")} ${otherIdentifier}.`);
26099
26030
  }
26100
26031
  });
26101
26032
 
@@ -26108,9 +26039,8 @@ var remove5 = command({
26108
26039
  },
26109
26040
  run: async ({ positionals: { id } }) => {
26110
26041
  const client3 = await backendClient();
26111
- const issue3 = await resolveIssue(client3, id);
26112
- await client3.mutation(api2.issues.remove, { id: issue3._id });
26113
- console.log(`Deleted ${issue3.identifier}.`);
26042
+ const { identifier } = await client3.mutation(api2.issues.remove, { id: readIssueRef(id) });
26043
+ console.log(`Deleted ${identifier}.`);
26114
26044
  }
26115
26045
  });
26116
26046
 
@@ -26118,8 +26048,7 @@ var remove5 = command({
26118
26048
  var route = command({
26119
26049
  name: "route",
26120
26050
  description: "Route an issue to a disposition (wontfix also cancels it)",
26121
- longDescription: `The disposition says who acts next; needs_triage is the default. Routing to
26122
- ready_for_agent requires the issue to carry an agent configuration (set --agent-config).`,
26051
+ longDescription: "The disposition says who acts next; needs_triage is the default.",
26123
26052
  positionals: {
26124
26053
  id: exports_external.string().describe("Issue identifier, number, or URL"),
26125
26054
  disposition: exports_external.enum(ISSUE_DISPOSITIONS).describe("Where the issue goes next")
@@ -26129,13 +26058,12 @@ ready_for_agent requires the issue to carry an agent configuration (set --agent-
26129
26058
  },
26130
26059
  run: async ({ positionals: { id, disposition }, options: { comment: comment2 } }) => {
26131
26060
  const client3 = await backendClient();
26132
- const issue3 = await resolveIssue(client3, id);
26133
- await client3.mutation(api2.issues.route, {
26134
- id: issue3._id,
26061
+ const { identifier } = await client3.mutation(api2.issues.route, {
26062
+ id: readIssueRef(id),
26135
26063
  disposition,
26136
26064
  comment: comment2 === undefined ? undefined : await readTextOption(comment2)
26137
26065
  });
26138
- console.log(`Routed ${issue3.identifier} to ${disposition}.`);
26066
+ console.log(`Routed ${identifier} to ${disposition}.`);
26139
26067
  }
26140
26068
  });
26141
26069
 
@@ -26144,8 +26072,7 @@ var set2 = command({
26144
26072
  name: "set",
26145
26073
  description: "Set an issue's status, priority, estimate, due date, project, milestone, parent, agent configuration, or labels",
26146
26074
  longDescription: `Status normally moves through the workflow commands and the linked pull request;
26147
- --status is the correction path. Clearing the agent configuration is refused while
26148
- the issue sits in ready_for_agent.`,
26075
+ --status is the correction path.`,
26149
26076
  positionals: {
26150
26077
  id: exports_external.string().describe("Issue identifier, number, or URL")
26151
26078
  },
@@ -26177,41 +26104,20 @@ the issue sits in ready_for_agent.`,
26177
26104
  if (Object.values(options).every((value) => value === undefined))
26178
26105
  throw new Error("Nothing to set. Pass --status, --priority, --estimate, --due, --project, --milestone, --parent, --agent-config, or --label.");
26179
26106
  const client3 = await backendClient();
26180
- const issue3 = await resolveIssue(client3, id);
26181
- const projectId = project === undefined ? issue3.project?._id : project === null ? undefined : (await resolveProject(client3, project))._id;
26182
- let milestoneId;
26183
- if (milestone === undefined) {
26184
- milestoneId = projectId === issue3.project?._id ? issue3.milestoneId : undefined;
26185
- } else if (milestone !== null) {
26186
- if (projectId === undefined)
26187
- throw new Error("A milestone needs a project. Pass --project too.");
26188
- milestoneId = (await resolveMilestone(client3, projectId, milestone))._id;
26189
- }
26190
- const parentId = parent == null ? undefined : (await resolveIssue(client3, parent))._id;
26191
- const addLabels = await resolveIssueLabels(client3, label ?? []);
26192
- const removeLabels = await resolveIssueLabels(client3, noLabel ?? []);
26193
- if (parent !== undefined)
26194
- await client3.mutation(api2.issues.setParent, { id: issue3._id, parentId });
26195
- for (const { _id } of addLabels)
26196
- await client3.mutation(api2.issues.addLabel, { issueId: issue3._id, labelId: _id });
26197
- for (const { _id } of removeLabels)
26198
- await client3.mutation(api2.issues.removeLabel, { issueId: issue3._id, labelId: _id });
26199
- if (agentConfig !== undefined)
26200
- await client3.mutation(api2.issues.setAgentConfiguration, {
26201
- id: issue3._id,
26202
- agentConfigurationId: agentConfig ?? undefined
26203
- });
26204
- if (status3 !== undefined)
26205
- await client3.mutation(api2.issues.setStatus, { id: issue3._id, status: status3 });
26206
- if (priority !== undefined)
26207
- await client3.mutation(api2.issues.setPriority, { id: issue3._id, priority });
26208
- if (estimate !== undefined)
26209
- await client3.mutation(api2.issues.setEstimate, { id: issue3._id, estimate: estimate ?? undefined });
26210
- if (due !== undefined)
26211
- await client3.mutation(api2.issues.setDueDate, { id: issue3._id, dueDate: due ?? undefined });
26212
- if (project !== undefined || milestone !== undefined)
26213
- await client3.mutation(api2.issues.setMembership, { id: issue3._id, projectId, milestoneId });
26214
- console.log(`Updated ${issue3.identifier}.`);
26107
+ const { identifier } = await client3.mutation(api2.issues.edit, {
26108
+ issue: readIssueRef(id),
26109
+ ...parent !== undefined ? { parent: parent === null ? null : readIssueRef(parent) } : {},
26110
+ ...label !== undefined ? { addLabels: label } : {},
26111
+ ...noLabel !== undefined ? { removeLabels: noLabel } : {},
26112
+ ...agentConfig !== undefined ? { agentConfigurationId: agentConfig } : {},
26113
+ ...status3 !== undefined ? { status: status3 } : {},
26114
+ ...priority !== undefined ? { priority } : {},
26115
+ ...estimate !== undefined ? { estimate } : {},
26116
+ ...due !== undefined ? { dueDate: due } : {},
26117
+ ...project !== undefined ? { project: project === null ? null : readProjectRef(project) } : {},
26118
+ ...milestone !== undefined ? { milestone } : {}
26119
+ });
26120
+ console.log(`Updated ${identifier}.`);
26215
26121
  }
26216
26122
  });
26217
26123
 
@@ -26229,29 +26135,18 @@ ready_for_agent is informational, and --ready belongs only to an explicit hand-o
26229
26135
  ready: exports_external.boolean().default(false).describe("Also route the issue to ready_for_agent, assigning the running agent when it is identifiable")
26230
26136
  },
26231
26137
  run: async ({ positionals: { id }, options: { ready } }) => {
26138
+ const actor = await agentClaim();
26139
+ const configurationId = actor && agentConfigurationForActor(actor);
26232
26140
  const client3 = await backendClient();
26233
- const issue3 = await resolveIssue(client3, id);
26234
- const { branch } = await client3.mutation(api2.issues.start, { id: issue3._id });
26235
- let assigned;
26236
- if (ready) {
26237
- const actor = await agentClaim();
26238
- const configurationId = actor && agentConfigurationForActor(actor);
26239
- if (configurationId && configurationId !== issue3.agentConfigurationId) {
26240
- await client3.mutation(api2.issues.setAgentConfiguration, {
26241
- id: issue3._id,
26242
- agentConfigurationId: configurationId
26243
- });
26244
- assigned = configurationId;
26245
- }
26246
- }
26247
- if (ready && issue3.disposition !== "ready_for_agent") {
26248
- await client3.mutation(api2.issues.route, { id: issue3._id, disposition: "ready_for_agent" });
26249
- }
26250
- console.log(`Started ${issue3.identifier}: in_progress, branch ${branch}`);
26141
+ const { identifier, branch, disposition, assigned } = await client3.mutation(api2.issues.start, {
26142
+ issue: readIssueRef(id),
26143
+ ...ready ? { ready: { agentConfigurationId: configurationId || undefined } } : {}
26144
+ });
26145
+ console.log(`Started ${identifier}: in_progress, branch ${branch}`);
26251
26146
  if (ready) {
26252
26147
  console.log(`Routed to ready_for_agent${assigned ? `, assigned ${agentConfiguration(assigned).label}` : ""}.`);
26253
- } else if (issue3.disposition !== "ready_for_agent" && await agentClaim()) {
26254
- console.log(`Note: disposition is ${issue3.disposition}. Pass --ready if this hand-off was intended.`);
26148
+ } else if (disposition !== "ready_for_agent" && actor) {
26149
+ console.log(`Note: disposition is ${disposition}. Pass --ready if this hand-off was intended.`);
26255
26150
  }
26256
26151
  console.log(`
26257
26152
  git switch -c ${shellArg(branch)}`);
@@ -26267,8 +26162,7 @@ var startReview = command({
26267
26162
  },
26268
26163
  run: async ({ positionals: { id } }) => {
26269
26164
  const client3 = await backendClient();
26270
- const issue3 = await resolveIssue(client3, id);
26271
- const started = await client3.action(api2.githubReviewer.startReview, { id: issue3._id });
26165
+ const started = await client3.action(api2.githubReviewer.startReview, { id: readIssueRef(id) });
26272
26166
  console.log(`Reviewing ${started.identifier}: in_review`);
26273
26167
  console.log(`Announced on ${started.prUrl}; check run open.`);
26274
26168
  }
@@ -26283,9 +26177,8 @@ var submit = command({
26283
26177
  },
26284
26178
  run: async ({ positionals: { id } }) => {
26285
26179
  const client3 = await backendClient();
26286
- const issue3 = await resolveIssue(client3, id);
26287
- await client3.mutation(api2.issues.submit, { id: issue3._id });
26288
- console.log(`Submitted ${issue3.identifier}: ready_for_review`);
26180
+ const { identifier } = await client3.mutation(api2.issues.submit, { id: readIssueRef(id) });
26181
+ console.log(`Submitted ${identifier}: ready_for_review`);
26289
26182
  }
26290
26183
  });
26291
26184
 
@@ -26349,8 +26242,7 @@ Categories: ${REVIEW_FINDING_CATEGORIES.join(", ")}; severities: ${REVIEW_FINDIN
26349
26242
  verdict = { kind: "request_changes", findings: await readFindings(options.findings) };
26350
26243
  }
26351
26244
  const client3 = await backendClient();
26352
- const issue3 = await resolveIssue(client3, id);
26353
- const submitted = await client3.action(api2.githubReviewer.submitReview, { id: issue3._id, verdict });
26245
+ const submitted = await client3.action(api2.githubReviewer.submitReview, { id: readIssueRef(id), verdict });
26354
26246
  console.log(`Submitted review on ${submitted.identifier}: ${submitted.prUrl}`);
26355
26247
  console.log(submitted.summary);
26356
26248
  }
@@ -26377,57 +26269,38 @@ address several rows even as removals renumber it.`,
26377
26269
  },
26378
26270
  run: async ({ positionals: { id }, options: { add: add3, check: check2, uncheck, convert, remove: remove6, json: json2 } }) => {
26379
26271
  const client3 = await backendClient();
26380
- const issue3 = await resolveIssue(client3, id);
26381
- const before = await client3.query(api2.issueTasks.list, { issueId: issue3._id });
26382
- const at = (numbered) => (numbered ?? []).map((number4) => {
26383
- const task2 = before[number4 - 1];
26384
- if (!task2)
26385
- throw new Error(`${issue3.identifier} has no task ${number4}.`);
26386
- return task2;
26387
- });
26388
- const ticked = at(check2);
26389
- const unticked = at(uncheck);
26390
- const promoted = at(convert);
26391
- const dropped = at(remove6);
26392
- for (const title of add3 ?? [])
26393
- await client3.mutation(api2.issueTasks.create, { issueId: issue3._id, title });
26394
- for (const task2 of ticked)
26395
- await client3.mutation(api2.issueTasks.update, { id: task2._id, isDone: true });
26396
- for (const task2 of unticked)
26397
- await client3.mutation(api2.issueTasks.update, { id: task2._id, isDone: false });
26398
- for (const task2 of promoted) {
26399
- const created = await client3.mutation(api2.issueTasks.convert, { id: task2._id });
26272
+ const wrote = [add3, check2, uncheck, convert, remove6].some((values) => values !== undefined);
26273
+ if (wrote) {
26274
+ const { identifier, converted, tasks: tasks3 } = await client3.mutation(api2.issueTasks.edit, {
26275
+ issue: readIssueRef(id),
26276
+ ...add3 !== undefined ? { add: add3 } : {},
26277
+ ...check2 !== undefined ? { check: check2 } : {},
26278
+ ...uncheck !== undefined ? { uncheck } : {},
26279
+ ...convert !== undefined ? { convert } : {},
26280
+ ...remove6 !== undefined ? { remove: remove6 } : {}
26281
+ });
26400
26282
  if (!json2) {
26401
- console.log(`${created.identifier} ${created.nested ? "created as a sub-issue" : "created and related"}: ${task2.title}`);
26283
+ for (const created of converted) {
26284
+ console.log(`${created.identifier} ${created.nested ? "created as a sub-issue" : "created and related"}: ${created.title}`);
26285
+ }
26402
26286
  }
26287
+ if (json2)
26288
+ return console.log(JSON.stringify(tasks3, null, 2));
26289
+ if (tasks3.length === 0)
26290
+ return console.log(`${identifier} has no tasks.`);
26291
+ return printTasks(tasks3);
26403
26292
  }
26404
- for (const task2 of dropped)
26405
- await client3.mutation(api2.issueTasks.remove, { id: task2._id });
26406
- const wrote = [add3, check2, uncheck, convert, remove6].some((values) => values !== undefined);
26407
- const after = wrote ? await client3.query(api2.issueTasks.list, { issueId: issue3._id }) : before;
26293
+ const issue3 = await resolveIssue(client3, id);
26294
+ const tasks2 = await client3.query(api2.issueTasks.list, { issueId: issue3._id });
26408
26295
  if (json2)
26409
- return console.log(JSON.stringify(after, null, 2));
26410
- if (after.length === 0)
26296
+ return console.log(JSON.stringify(tasks2, null, 2));
26297
+ if (tasks2.length === 0)
26411
26298
  return console.log(`${issue3.identifier} has no tasks.`);
26412
- printTasks(after);
26299
+ printTasks(tasks2);
26413
26300
  }
26414
26301
  });
26415
26302
 
26416
26303
  // src/commands/issues/unrelate.ts
26417
- var relationEntries = (relations, kind) => {
26418
- switch (kind) {
26419
- case "blocks":
26420
- return relations.blocks;
26421
- case "blocked_by":
26422
- return relations.blockedBy;
26423
- case "duplicate_of":
26424
- return relations.duplicateOf ? [relations.duplicateOf] : [];
26425
- case "duplicated_by":
26426
- return relations.duplicates;
26427
- case "relates_to":
26428
- return relations.relatesTo;
26429
- }
26430
- };
26431
26304
  var unrelate = command({
26432
26305
  name: "unrelate",
26433
26306
  description: "Remove a relation between two issues (a canceled duplicate stays canceled)",
@@ -26438,15 +26311,12 @@ var unrelate = command({
26438
26311
  },
26439
26312
  run: async ({ positionals: { id, kind, other } }) => {
26440
26313
  const client3 = await backendClient();
26441
- const issue3 = await resolveIssue(client3, id);
26442
- const otherIssue = await resolveIssue(client3, other);
26443
- const relations = await client3.query(api2.issues.relations, { id: issue3._id });
26444
- const relation = relationEntries(relations, kind).find((entry) => entry.issue._id === otherIssue._id);
26445
- const label = kind.replaceAll("_", " ");
26446
- if (!relation)
26447
- throw new Error(`${issue3.identifier} has no "${label}" relation to ${otherIssue.identifier}.`);
26448
- await client3.mutation(api2.issues.removeRelation, { id: relation._id });
26449
- console.log(`Removed: ${issue3.identifier} ${label} ${otherIssue.identifier}.`);
26314
+ const { identifier, otherIdentifier } = await client3.mutation(api2.issues.removeRelation, {
26315
+ id: readIssueRef(id),
26316
+ kind,
26317
+ otherIssueId: readIssueRef(other)
26318
+ });
26319
+ console.log(`Removed: ${identifier} ${kind.replaceAll("_", " ")} ${otherIdentifier}.`);
26450
26320
  }
26451
26321
  });
26452
26322
 
@@ -26466,16 +26336,18 @@ var update6 = command({
26466
26336
  if (title === undefined && description === undefined && attach === undefined)
26467
26337
  throw new Error("Nothing to update. Pass --title, --description, or --attach.");
26468
26338
  const client3 = await backendClient();
26469
- const issue3 = await resolveIssue(client3, id);
26470
- printAttachments(await attachFiles(client3, issue3._id, attach ?? []));
26471
- if (title !== undefined || description !== undefined) {
26472
- await client3.mutation(api2.issues.update, {
26473
- id: issue3._id,
26474
- title,
26475
- descriptionMarkdown: description === undefined ? undefined : await readTextOption(description)
26476
- });
26477
- }
26478
- console.log(`Updated ${issue3.identifier}.`);
26339
+ let attached;
26340
+ if (attach !== undefined && attach.length > 0) {
26341
+ const issue3 = await resolveIssue(client3, id);
26342
+ printAttachments(await attachFiles(client3, issue3._id, attach));
26343
+ attached = issue3.identifier;
26344
+ }
26345
+ const identifier = title !== undefined || description !== undefined ? (await client3.mutation(api2.issues.update, {
26346
+ id: readIssueRef(id),
26347
+ title,
26348
+ descriptionMarkdown: description === undefined ? undefined : await readTextOption(description)
26349
+ })).identifier : attached;
26350
+ console.log(`Updated ${identifier}.`);
26479
26351
  }
26480
26352
  });
26481
26353
 
@@ -26764,7 +26636,7 @@ var create7 = command({
26764
26636
  health: options.health,
26765
26637
  targetDate: options.target,
26766
26638
  repo: options.repo,
26767
- clientId: options.client === undefined ? undefined : (await resolveClient(backend, options.client))._id
26639
+ clientId: options.client === undefined ? undefined : readClientRef(options.client)
26768
26640
  });
26769
26641
  console.log(projectId);
26770
26642
  }
@@ -26866,9 +26738,8 @@ var remove7 = command({
26866
26738
  },
26867
26739
  run: async ({ positionals: { id } }) => {
26868
26740
  const client3 = await backendClient();
26869
- const project2 = await resolveProject(client3, id);
26870
- await client3.mutation(api2.projects.remove, { id: project2._id });
26871
- console.log(`Deleted ${project2.name}.`);
26741
+ const removed = await client3.mutation(api2.projects.remove, { id: readProjectRef(id) });
26742
+ console.log(`Deleted ${removed.name}.`);
26872
26743
  }
26873
26744
  });
26874
26745
 
@@ -26891,21 +26762,15 @@ var set3 = command({
26891
26762
  if (Object.values(options).every((value) => value === undefined))
26892
26763
  throw new Error("Nothing to set. Pass --status, --health, --target, --repo, or --client.");
26893
26764
  const backend = await backendClient();
26894
- const project2 = await resolveProject(backend, id);
26895
- const linkedClient = typeof client3 === "string" ? await resolveClient(backend, client3) : undefined;
26896
- if (linkedClient?.archivedAt !== undefined)
26897
- throw new Error(`${linkedClient.name} is archived and takes no new work.`);
26898
- if (repo !== undefined)
26899
- await backend.mutation(api2.projects.setRepo, { id: project2._id, repo: repo ?? undefined });
26900
- if (client3 !== undefined)
26901
- await backend.mutation(api2.projects.setClient, { id: project2._id, clientId: linkedClient?._id });
26902
- if (status3 !== undefined)
26903
- await backend.mutation(api2.projects.setStatus, { id: project2._id, status: status3 });
26904
- if (health !== undefined)
26905
- await backend.mutation(api2.projects.setHealth, { id: project2._id, health: health ?? undefined });
26906
- if (target !== undefined)
26907
- await backend.mutation(api2.projects.setTargetDate, { id: project2._id, targetDate: target ?? undefined });
26908
- console.log(`Updated ${project2.name}.`);
26765
+ const { name } = await backend.mutation(api2.projects.edit, {
26766
+ id: readProjectRef(id),
26767
+ ...repo !== undefined ? { repo } : {},
26768
+ ...client3 !== undefined ? { client: client3 === null ? null : readClientRef(client3) } : {},
26769
+ ...status3 !== undefined ? { status: status3 } : {},
26770
+ ...health !== undefined ? { health } : {},
26771
+ ...target !== undefined ? { targetDate: target } : {}
26772
+ });
26773
+ console.log(`Updated ${name}.`);
26909
26774
  }
26910
26775
  });
26911
26776
 
@@ -26925,14 +26790,13 @@ var update8 = command({
26925
26790
  if (name === undefined && summary === undefined && description === undefined)
26926
26791
  throw new Error("Nothing to update. Pass --name, --summary, or --description.");
26927
26792
  const client3 = await backendClient();
26928
- const project2 = await resolveProject(client3, id);
26929
- await client3.mutation(api2.projects.update, {
26930
- id: project2._id,
26793
+ const updated = await client3.mutation(api2.projects.update, {
26794
+ id: readProjectRef(id),
26931
26795
  name,
26932
26796
  summary,
26933
26797
  descriptionMarkdown: description === undefined ? undefined : await readTextOption(description)
26934
26798
  });
26935
- console.log(`Updated ${name ?? project2.name}.`);
26799
+ console.log(`Updated ${updated.name}.`);
26936
26800
  }
26937
26801
  });
26938
26802
 
@@ -26958,13 +26822,12 @@ var accept = command({
26958
26822
  },
26959
26823
  run: async ({ positionals: { id }, options }) => {
26960
26824
  const backend = await backendClient();
26961
- const quotation = await resolveQuotation(backend, id);
26962
- await backend.mutation(api2.quotations.accept, {
26963
- id: quotation._id,
26825
+ const accepted = await backend.mutation(api2.quotations.accept, {
26826
+ id: readQuotationRef(id),
26964
26827
  on: options.date ?? localToday(),
26965
26828
  note: options.note
26966
26829
  });
26967
- console.log(`Accepted ${quotation.number}. Draw invoices from it with: kds quotations convert ${quotation._id}`);
26830
+ console.log(`Accepted ${accepted.number}. Draw invoices from it with: kds quotations convert ${accepted.id}`);
26968
26831
  }
26969
26832
  });
26970
26833
 
@@ -26985,7 +26848,6 @@ Nothing blocks over- or under-invoicing; left to bill is advisory.`,
26985
26848
  },
26986
26849
  run: async ({ positionals: { id }, options }) => {
26987
26850
  const backend = await backendClient();
26988
- const quotation = await resolveQuotation(backend, id);
26989
26851
  const lineIndexes = options.lines?.split(",").map((part) => {
26990
26852
  const lineNumber = Number(part.trim());
26991
26853
  if (!Number.isInteger(lineNumber) || lineNumber < 1) {
@@ -26994,7 +26856,7 @@ Nothing blocks over- or under-invoicing; left to bill is advisory.`,
26994
26856
  return lineNumber - 1;
26995
26857
  });
26996
26858
  const invoiceId = await backend.mutation(api2.quotations.convert, {
26997
- id: quotation._id,
26859
+ id: readQuotationRef(id),
26998
26860
  lineIndexes,
26999
26861
  percentage: options.percentage
27000
26862
  });
@@ -27017,11 +26879,9 @@ var create8 = command({
27017
26879
  },
27018
26880
  run: async ({ positionals: { client: ref }, options }) => {
27019
26881
  const backend = await backendClient();
27020
- const client3 = await resolveClient(backend, ref);
27021
- const project2 = options.project === undefined ? undefined : await resolveProject(backend, options.project);
27022
26882
  const quotationId = await backend.mutation(api2.quotations.create, {
27023
- clientId: client3._id,
27024
- projectId: project2?._id,
26883
+ clientId: readClientRef(ref),
26884
+ projectId: options.project === undefined ? undefined : readProjectRef(options.project),
27025
26885
  lineItems: options.line?.map(parseQuotationLineItem),
27026
26886
  notes: options.notes,
27027
26887
  validUntil: options.validUntil === null ? "" : options.validUntil,
@@ -27040,8 +26900,21 @@ var duplicate2 = command({
27040
26900
  },
27041
26901
  run: async ({ positionals: { id } }) => {
27042
26902
  const backend = await backendClient();
27043
- const quotation = await resolveQuotation(backend, id);
27044
- console.log(await backend.mutation(api2.quotations.duplicate, { id: quotation._id, today: localToday() }));
26903
+ console.log(await backend.mutation(api2.quotations.duplicate, { id: readQuotationRef(id), today: localToday() }));
26904
+ }
26905
+ });
26906
+
26907
+ // src/commands/quotations/email.ts
26908
+ var email4 = command({
26909
+ name: "email",
26910
+ description: "Email the quotation's link to the client's billing address; repeatable, and never required",
26911
+ positionals: {
26912
+ id: exports_external.string().describe("Quotation id or URL")
26913
+ },
26914
+ run: async ({ positionals: { id } }) => {
26915
+ const backend = await backendClient();
26916
+ const { to, number: number4 } = await backend.mutation(api2.quotations.email, { id: readQuotationRef(id) });
26917
+ console.log(`Emailed ${number4} to ${to}.`);
27045
26918
  }
27046
26919
  });
27047
26920
 
@@ -27070,6 +26943,9 @@ var get7 = command({
27070
26943
  console.log(`Valid until: ${quotation.validUntil}`);
27071
26944
  if (quotation.publicUrl)
27072
26945
  console.log(`Link: ${quotation.publicUrl}`);
26946
+ if (quotation.status !== "draft") {
26947
+ console.log(`Emailed: ${quotation.lastEmailedAt === undefined ? "never" : localDateString(quotation.lastEmailedAt)}`);
26948
+ }
27073
26949
  if (quotation.outcome) {
27074
26950
  console.log(`${quotation.status === "accepted" ? "Accepted" : "Rejected"}: ${quotation.outcome.on}${quotation.outcome.note ? ` \u2014 ${quotation.outcome.note}` : ""}`);
27075
26951
  }
@@ -27126,9 +27002,7 @@ var issue3 = command({
27126
27002
  },
27127
27003
  run: async ({ positionals: { id } }) => {
27128
27004
  const backend = await backendClient();
27129
- const quotation = await resolveQuotation(backend, id);
27130
- await backend.mutation(api2.quotations.issue, { id: quotation._id, today: localToday() });
27131
- const issued = await resolveQuotation(backend, quotation._id);
27005
+ const issued = await backend.mutation(api2.quotations.issue, { id: readQuotationRef(id), today: localToday() });
27132
27006
  console.log(`Issued ${issued.number}, ${issued.validUntil ? `valid until ${issued.validUntil}` : "with no expiry"}.`);
27133
27007
  }
27134
27008
  });
@@ -27143,10 +27017,11 @@ var link2 = command({
27143
27017
  },
27144
27018
  run: async ({ positionals: { id, project: projectRef } }) => {
27145
27019
  const backend = await backendClient();
27146
- const quotation = await resolveQuotation(backend, id);
27147
- const project2 = projectRef === undefined ? undefined : await resolveProject(backend, projectRef);
27148
- await backend.mutation(api2.quotations.setProject, { id: quotation._id, projectId: project2?._id });
27149
- console.log(project2 ? `Linked to ${project2.name}.` : "Cleared the project link.");
27020
+ const { projectName } = await backend.mutation(api2.quotations.setProject, {
27021
+ id: readQuotationRef(id),
27022
+ projectId: projectRef === undefined ? undefined : readProjectRef(projectRef)
27023
+ });
27024
+ console.log(projectName === undefined ? "Cleared the project link." : `Linked to ${projectName}.`);
27150
27025
  }
27151
27026
  });
27152
27027
 
@@ -27174,7 +27049,7 @@ var list10 = command({
27174
27049
  if (result.page.length === 0)
27175
27050
  return console.log("No quotations yet.");
27176
27051
  printTable([
27177
- ["NUMBER", "CLIENT", "PROJECT", "STATUS", "TOTAL", "VALID UNTIL", "ID"],
27052
+ ["NUMBER", "CLIENT", "PROJECT", "STATUS", "TOTAL", "VALID UNTIL", "EMAILED", "ID"],
27178
27053
  ...result.page.map((quotation) => [
27179
27054
  quotation.number ?? "draft",
27180
27055
  quotation.clientName,
@@ -27182,6 +27057,7 @@ var list10 = command({
27182
27057
  quotation.derivedStatus,
27183
27058
  formatPerCurrency(quotation.totals.map(({ currency, total }) => ({ currency, amount: total }))),
27184
27059
  quotation.validUntil ?? "-",
27060
+ quotation.lastEmailedAt === undefined ? "-" : localDateString(quotation.lastEmailedAt),
27185
27061
  quotation._id
27186
27062
  ])
27187
27063
  ]);
@@ -27204,13 +27080,12 @@ var reject2 = command({
27204
27080
  },
27205
27081
  run: async ({ positionals: { id }, options }) => {
27206
27082
  const backend = await backendClient();
27207
- const quotation = await resolveQuotation(backend, id);
27208
- await backend.mutation(api2.quotations.reject, {
27209
- id: quotation._id,
27083
+ const { number: number4 } = await backend.mutation(api2.quotations.reject, {
27084
+ id: readQuotationRef(id),
27210
27085
  on: options.date ?? localToday(),
27211
27086
  note: options.note
27212
27087
  });
27213
- console.log(`Rejected ${quotation.number}.`);
27088
+ console.log(`Rejected ${number4}.`);
27214
27089
  }
27215
27090
  });
27216
27091
 
@@ -27223,9 +27098,8 @@ var rotateLink2 = command({
27223
27098
  },
27224
27099
  run: async ({ positionals: { id } }) => {
27225
27100
  const backend = await backendClient();
27226
- const quotation = await resolveQuotation(backend, id);
27227
- const url2 = await backend.mutation(api2.quotations.rotateLink, { id: quotation._id });
27228
- console.log(`${quotation.number} now reads at ${url2}. Send it again \u2014 the previous link is dead.`);
27101
+ const { url: url2, number: number4 } = await backend.mutation(api2.quotations.rotateLink, { id: readQuotationRef(id) });
27102
+ console.log(`${number4} now reads at ${url2}. Send it again \u2014 the previous link is dead.`);
27229
27103
  }
27230
27104
  });
27231
27105
 
@@ -27247,14 +27121,13 @@ var update9 = command({
27247
27121
  throw new Error("Nothing to update. Pass --line, --notes, or --valid-until (or a --no form to clear).");
27248
27122
  }
27249
27123
  const backend = await backendClient();
27250
- const quotation = await resolveQuotation(backend, id);
27251
- await backend.mutation(api2.quotations.update, {
27252
- id: quotation._id,
27124
+ const { number: number4 } = await backend.mutation(api2.quotations.update, {
27125
+ id: readQuotationRef(id),
27253
27126
  lineItems: options.line?.map(parseQuotationLineItem),
27254
27127
  notes: options.notes === null ? "" : options.notes,
27255
27128
  validUntil: options.validUntil === null ? "" : options.validUntil
27256
27129
  });
27257
- console.log(`Updated ${quotation.number ?? "draft"}.`);
27130
+ console.log(`Updated ${number4 ?? "draft"}.`);
27258
27131
  }
27259
27132
  });
27260
27133
 
@@ -27267,9 +27140,8 @@ var voidQuotation = command({
27267
27140
  },
27268
27141
  run: async ({ positionals: { id } }) => {
27269
27142
  const backend = await backendClient();
27270
- const quotation = await resolveQuotation(backend, id);
27271
- await backend.mutation(api2.quotations.voidQuotation, { id: quotation._id });
27272
- console.log(`Voided ${quotation.number}.`);
27143
+ const { number: number4 } = await backend.mutation(api2.quotations.voidQuotation, { id: readQuotationRef(id) });
27144
+ console.log(`Voided ${number4}.`);
27273
27145
  }
27274
27146
  });
27275
27147
 
@@ -27281,7 +27153,21 @@ var quotations = group({
27281
27153
  document awaiting the client's answer. Accept and reject are manual records;
27282
27154
  expiry is derived from valid-until and never blocks accepting. An accepted
27283
27155
  quotation converts into any number of draft invoices, each tracing back to it.`,
27284
- commands: [create8, list10, get7, update9, issue3, accept, reject2, voidQuotation, duplicate2, convert, link2, rotateLink2]
27156
+ commands: [
27157
+ create8,
27158
+ list10,
27159
+ get7,
27160
+ update9,
27161
+ issue3,
27162
+ email4,
27163
+ accept,
27164
+ reject2,
27165
+ voidQuotation,
27166
+ duplicate2,
27167
+ convert,
27168
+ link2,
27169
+ rotateLink2
27170
+ ]
27285
27171
  });
27286
27172
 
27287
27173
  // src/commands/upgrade.ts