@islamihab/kds 0.10.0 → 0.12.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 +579 -611
  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.12.0",
14615
14627
  private: true,
14616
14628
  type: "module",
14617
14629
  bin: {
@@ -15184,9 +15196,20 @@ var components = componentsGeneric();
15184
15196
 
15185
15197
  // ../../packages/backend/convex/constants.ts
15186
15198
  var KDS_DEVICE_AUTH_CLIENT_ID = "kds-cli";
15187
- var PAGE_MODES = ["themed", "raw"];
15199
+ var PAGE_STYLES = {
15200
+ basic: {
15201
+ description: "a report in the house theme with a light/dark toggle and the credit line, where you write only what goes inside the page"
15202
+ },
15203
+ mockup: {
15204
+ description: "an interactive prototype where the fragment is the screen of a phone, its own <style> and <script> tags do the rest, it fills the display on a real phone, and its manifest and apple-touch-icon install it under its own name and icon"
15205
+ },
15206
+ unthemed: {
15207
+ description: "a blank document where the fragment is served as written with nothing added, so its own <style> and <script> tags decide everything"
15208
+ }
15209
+ };
15188
15210
  var PAGE_VISIBILITIES = ["public", "private"];
15189
15211
  var MAX_PAGE_HTML_BYTES = 4000000;
15212
+ var MAX_PAGE_VERSIONS = 20;
15190
15213
  var ISSUE_STATUSES = [
15191
15214
  "backlog",
15192
15215
  "todo",
@@ -15306,6 +15329,7 @@ var PROJECT_STATUSES = ["planned", "in_progress", "paused", "completed", "cancel
15306
15329
  var PROJECT_HEALTHS = ["on_track", "at_risk", "off_track"];
15307
15330
  var ISSUE_IDENTIFIER_PREFIX = "KAI";
15308
15331
  var ISSUE_LIST_PAGE_SIZE = 50;
15332
+ var MAX_ISSUES_PER_BATCH_READ = 20;
15309
15333
  var MAX_ISSUE_ATTACHMENT_BYTES = 1e7;
15310
15334
  var ISSUE_DUE_DATE_MODES = ["overdue", "due_today", "due_soon", "no_due_date"];
15311
15335
  var BUILT_IN_ISSUE_VIEWS = {
@@ -15352,13 +15376,8 @@ var BUILT_IN_ISSUE_VIEWS = {
15352
15376
  }
15353
15377
  };
15354
15378
  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;
15379
+ var CURRENCIES = ["EGP", "USD", "EUR", "GBP", "AED", "SAR"];
15380
+ var DEFAULT_CURRENCY = "EGP";
15362
15381
  var RECURRING_CHARGE_CADENCES = ["monthly", "yearly"];
15363
15382
  var COST_KINDS = ["one_off", "monthly", "yearly"];
15364
15383
  var MAX_PROJECT_REPO_LENGTH = 200;
@@ -17338,6 +17357,7 @@ var localDateString = (timestamp) => {
17338
17357
  return `${date5.getFullYear()}-${month}-${day}`;
17339
17358
  };
17340
17359
  var localToday = () => localDateString(Date.now());
17360
+ var describeChoices = (choices, separator = "; ") => Object.entries(choices).map(([key, { description }]) => `${key}: ${description}`).join(separator);
17341
17361
 
17342
17362
  // src/lib/zod.ts
17343
17363
  var configSchema = exports_external.object({ sessionToken: exports_external.string().optional(), convexUrl: exports_external.url(), convexSiteUrl: exports_external.url() });
@@ -24112,6 +24132,10 @@ var issueActivityLine = (detail) => {
24112
24132
  case "attachment_added":
24113
24133
  case "attachment_removed":
24114
24134
  return `${label}: ${detail.name}`;
24135
+ case "page_linked":
24136
+ case "page_unlinked":
24137
+ case "page_updated":
24138
+ return `${label}: ${detail.title}`;
24115
24139
  case "child_added":
24116
24140
  case "child_removed":
24117
24141
  return `${label}: ${detail.identifier}`;
@@ -24257,6 +24281,73 @@ var readPageId = (value) => {
24257
24281
  }
24258
24282
  };
24259
24283
 
24284
+ // src/commands/clients/archive.ts
24285
+ var archive = command({
24286
+ name: "archive",
24287
+ description: "Archive a client: it leaves pickers and default lists, and every existing reference stays intact",
24288
+ positionals: {
24289
+ id: exports_external.string().describe("Client id or URL")
24290
+ },
24291
+ run: async ({ positionals: { id } }) => {
24292
+ const backend = await backendClient();
24293
+ const { name } = await backend.mutation(api2.clients.archive, { id: readClientRef(id) });
24294
+ console.log(`Archived ${name}.`);
24295
+ }
24296
+ });
24297
+
24298
+ // src/lib/currency.ts
24299
+ var CURRENCY_CHOICES = CURRENCIES.join(", ");
24300
+ var currencyCode = exports_external.preprocess((value) => typeof value === "string" ? value.toUpperCase() : value, exports_external.enum(CURRENCIES));
24301
+ var parseLineCurrency = (raw) => {
24302
+ const code2 = raw.trim().toUpperCase();
24303
+ if (!code2)
24304
+ return DEFAULT_CURRENCY;
24305
+ const codes = CURRENCIES;
24306
+ if (!codes.includes(code2))
24307
+ throw new Error(`Line currency must be one of ${CURRENCY_CHOICES}; got "${raw.trim()}".`);
24308
+ return code2;
24309
+ };
24310
+
24311
+ // src/commands/clients/charges/add.ts
24312
+ var add = command({
24313
+ name: "add",
24314
+ description: "Add a recurring charge to a client and print its id",
24315
+ positionals: {
24316
+ client: exports_external.string().describe("Client id or URL"),
24317
+ description: exports_external.string().describe('What the charge is for, e.g. "Maintenance retainer"')
24318
+ },
24319
+ options: {
24320
+ amount: exports_external.coerce.number().int().describe("Amount in integer minor units (2500 = 25.00)").meta({ short: "a" }),
24321
+ currency: currencyCode.default(DEFAULT_CURRENCY).describe(`One of ${CURRENCY_CHOICES}`).meta({ short: "c" }),
24322
+ cadence: exports_external.enum(RECURRING_CHARGE_CADENCES).default("monthly").describe("monthly or yearly"),
24323
+ "due-month": exports_external.coerce.number().int().optional().describe("Month (1-12) a yearly charge falls due; defaults to the current month")
24324
+ },
24325
+ run: async ({ positionals: { client: ref, description }, options }) => {
24326
+ const backend = await backendClient();
24327
+ const chargeId = await backend.mutation(api2.recurringCharges.create, {
24328
+ clientId: readClientRef(ref),
24329
+ description,
24330
+ amount: options.amount,
24331
+ currency: options.currency,
24332
+ cadence: options.cadence,
24333
+ dueMonth: options["due-month"]
24334
+ });
24335
+ console.log(chargeId);
24336
+ }
24337
+ });
24338
+
24339
+ // src/lib/output.ts
24340
+ var formatMinorAmount = (amount, currency) => `${(amount / 100).toFixed(2)} ${currency}`;
24341
+ var printTable = (rows) => {
24342
+ if (rows.length === 0)
24343
+ return;
24344
+ const columnCount = Math.max(...rows.map((row) => row.length));
24345
+ const widths = Array.from({ length: columnCount }, (_, column) => Math.max(...rows.map((row) => (row[column] ?? "").length)));
24346
+ for (const row of rows) {
24347
+ console.log(row.map((cell, column) => cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd());
24348
+ }
24349
+ };
24350
+
24260
24351
  // ../../packages/backend/convex/lib/projectRepo.ts
24261
24352
  var DEFAULT_REPO_HOST = "github.com";
24262
24353
  var REPO_FORMAT_ERROR = "Enter a repository like github.com/owner/name.";
@@ -24329,24 +24420,6 @@ var resolveQuotation = async (client3, ref) => {
24329
24420
  throw new Error(`No quotation matches ${ref}.`);
24330
24421
  return quotation;
24331
24422
  };
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
24423
  var repoProject = async (client3) => {
24351
24424
  const repo = await currentRepoKey();
24352
24425
  if (!repo)
@@ -24356,88 +24429,6 @@ var repoProject = async (client3) => {
24356
24429
  throw new Error(`No project is connected to ${repo}. Connect one on the project in the dashboard.`);
24357
24430
  return project;
24358
24431
  };
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
- var resolveMilestone = async (client3, projectId, name) => {
24377
- const milestone = await client3.query(api2.milestones.find, { projectId, name });
24378
- if (!milestone) {
24379
- const milestones = await client3.query(api2.milestones.listByProject, { projectId, today: localToday() });
24380
- const names = milestones.map((candidate) => candidate.name).join(", ");
24381
- throw new Error(names ? `No milestone named "${name}". The project has: ${names}.` : "The project has no milestones.");
24382
- }
24383
- return milestone;
24384
- };
24385
-
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
24432
 
24442
24433
  // src/commands/clients/charges/list.ts
24443
24434
  var monthName = (month) => new Date(Date.UTC(2000, month - 1)).toLocaleString("en-US", { month: "short", timeZone: "UTC" });
@@ -24480,9 +24471,8 @@ var pause = command({
24480
24471
  },
24481
24472
  run: async ({ positionals: { id } }) => {
24482
24473
  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}.`);
24474
+ const { description } = await backend.mutation(api2.recurringCharges.pause, { id: id.trim() });
24475
+ console.log(`Paused ${description}.`);
24486
24476
  }
24487
24477
  });
24488
24478
 
@@ -24495,9 +24485,8 @@ var remove = command({
24495
24485
  },
24496
24486
  run: async ({ positionals: { id } }) => {
24497
24487
  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}.`);
24488
+ const { description } = await backend.mutation(api2.recurringCharges.remove, { id: id.trim() });
24489
+ console.log(`Removed ${description}.`);
24501
24490
  }
24502
24491
  });
24503
24492
 
@@ -24510,9 +24499,8 @@ var resume = command({
24510
24499
  },
24511
24500
  run: async ({ positionals: { id } }) => {
24512
24501
  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}.`);
24502
+ const { description } = await backend.mutation(api2.recurringCharges.resume, { id: id.trim() });
24503
+ console.log(`Resumed ${description}.`);
24516
24504
  }
24517
24505
  });
24518
24506
 
@@ -24526,7 +24514,7 @@ var update = command({
24526
24514
  options: {
24527
24515
  description: exports_external.string().optional().describe("New description").meta({ short: "d" }),
24528
24516
  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" }),
24517
+ currency: currencyCode.optional().describe(`One of ${CURRENCY_CHOICES}`).meta({ short: "c" }),
24530
24518
  cadence: exports_external.enum(RECURRING_CHARGE_CADENCES).optional().describe("monthly or yearly"),
24531
24519
  "due-month": exports_external.coerce.number().int().optional().describe("Month (1-12) a yearly charge falls due")
24532
24520
  },
@@ -24535,16 +24523,15 @@ var update = command({
24535
24523
  throw new Error("Nothing to update. Pass --description, --amount, --currency, --cadence, or --due-month.");
24536
24524
  }
24537
24525
  const backend = await backendClient();
24538
- const charge = await resolveRecurringCharge(backend, id);
24539
- await backend.mutation(api2.recurringCharges.update, {
24540
- id: charge._id,
24526
+ const { description } = await backend.mutation(api2.recurringCharges.update, {
24527
+ id: id.trim(),
24541
24528
  description: options.description,
24542
24529
  amount: options.amount,
24543
24530
  currency: options.currency,
24544
24531
  cadence: options.cadence,
24545
24532
  dueMonth: options["due-month"]
24546
24533
  });
24547
- console.log(`Updated ${options.description ?? charge.description}.`);
24534
+ console.log(`Updated ${description}.`);
24548
24535
  }
24549
24536
  });
24550
24537
 
@@ -24658,65 +24645,6 @@ More clients exist; raise --limit past ${limit}.`);
24658
24645
  }
24659
24646
  });
24660
24647
 
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
24648
  // src/commands/clients/update.ts
24721
24649
  var update2 = command({
24722
24650
  name: "update",
@@ -24740,42 +24668,19 @@ var update2 = command({
24740
24668
  throw new Error("Nothing to update. Pass a profile flag, or a --no form to clear one.");
24741
24669
  }
24742
24670
  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
24671
  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}.`);
24672
+ const { name } = await backend.mutation(api2.clients.edit, {
24673
+ id: readClientRef(id),
24674
+ ...options.name !== undefined ? { name: options.name } : {},
24675
+ ...options["legal-name"] !== undefined ? { legalName: text(options["legal-name"]) } : {},
24676
+ ...options.address !== undefined ? { billingAddress: text(options.address) } : {},
24677
+ ...options.email !== undefined ? { billingEmail: text(options.email) } : {},
24678
+ ...options["tax-id"] !== undefined ? { taxId: text(options["tax-id"]) } : {},
24679
+ ...options["payment-terms"] !== undefined ? { paymentTermsDays: options["payment-terms"] } : {},
24680
+ ...options.currency !== undefined ? { defaultCurrency: options.currency } : {},
24681
+ ...options["billing-day"] !== undefined ? { billingDay: options["billing-day"] } : {}
24682
+ });
24683
+ console.log(`Updated ${name}.`);
24779
24684
  }
24780
24685
  });
24781
24686
 
@@ -24793,15 +24698,16 @@ var create2 = command({
24793
24698
  name: "create",
24794
24699
  description: "Record a cost and print its id",
24795
24700
  longDescription: `One record per real-world bill, never one per project. A cost links to any number
24796
- of projects, each carrying the full amount; no links at all is overhead. Billable
24797
- costs pass through onto a client's invoices, so their projects must all belong to
24798
- the same client.`,
24701
+ of projects, each carrying the full amount; until a client's project carries it, it
24702
+ is overhead. Billable
24703
+ costs pass through onto the invoices of whichever client their projects belong to,
24704
+ so the links cannot span two clients; with no links, billable waits for one.`,
24799
24705
  positionals: {
24800
24706
  name: exports_external.string().describe('What the bill is for, e.g. "Figma seats"')
24801
24707
  },
24802
24708
  options: {
24803
24709
  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" }),
24710
+ currency: currencyCode.default(DEFAULT_CURRENCY).describe(`One of ${CURRENCY_CHOICES}`).meta({ short: "c" }),
24805
24711
  kind: exports_external.enum(COST_KINDS).default("one_off").describe("one_off, monthly, or yearly").meta({ short: "k" }),
24806
24712
  date: exports_external.string().optional().describe("The one-off's date, or a subscription's start (YYYY-MM-DD); default today"),
24807
24713
  end: exports_external.string().optional().describe("Last date a subscription is committed to (YYYY-MM-DD)"),
@@ -24811,7 +24717,6 @@ the same client.`,
24811
24717
  },
24812
24718
  run: async ({ positionals: { name }, options }) => {
24813
24719
  const backend = await backendClient();
24814
- const projects = await Promise.all((options.project ?? []).map((ref) => resolveProject(backend, ref)));
24815
24720
  const costId = await backend.mutation(api2.costs.create, {
24816
24721
  name,
24817
24722
  note: options.note,
@@ -24820,7 +24725,7 @@ the same client.`,
24820
24725
  endDate: options.end,
24821
24726
  amount: options.amount,
24822
24727
  currency: options.currency,
24823
- projectIds: projects.map((project) => project._id),
24728
+ projectIds: options.project?.map(readProjectRef),
24824
24729
  billable: options.billable
24825
24730
  });
24826
24731
  console.log(costId);
@@ -24840,10 +24745,9 @@ var end = command({
24840
24745
  },
24841
24746
  run: async ({ positionals: { id }, options }) => {
24842
24747
  const backend = await backendClient();
24843
- const cost = await resolveCost(backend, id);
24844
24748
  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}.`);
24749
+ const { name } = await backend.mutation(api2.costs.end, { id: id.trim(), endDate: options.date, today });
24750
+ console.log(`Ended ${name} on ${options.date ?? today}.`);
24847
24751
  }
24848
24752
  });
24849
24753
 
@@ -24854,9 +24758,10 @@ var costKindLabel = (cost) => {
24854
24758
  return cost.endDate === undefined ? cost.kind : `${cost.kind} \u2192 ${cost.endDate}`;
24855
24759
  };
24856
24760
  var costProjectsLabel = (cost) => {
24857
- if (cost.overhead)
24858
- return "overhead";
24859
- return cost.projects.length === 0 ? "(deleted project)" : cost.projects.map((project) => project.name).join(", ");
24761
+ const names = cost.projects.map((project) => project.name).join(", ");
24762
+ if (!cost.overhead)
24763
+ return names;
24764
+ return names === "" ? "overhead" : `overhead (${names})`;
24860
24765
  };
24861
24766
 
24862
24767
  // src/commands/costs/get.ts
@@ -24904,7 +24809,7 @@ after an edit. A yearly cost lands whole in its renewal month, never amortized.`
24904
24809
  month: exports_external.string().optional().describe("Derive one month's costs (YYYY-MM)").meta({ short: "m" }),
24905
24810
  kind: exports_external.enum(COST_KINDS).optional().describe("Only one_off, monthly, or yearly").meta({ short: "k" }),
24906
24811
  project: exports_external.string().optional().describe("Only costs linked to this project (id, URL, or repo)").meta({ short: "p" }),
24907
- overhead: exports_external.boolean().default(false).describe("Only costs with no project links"),
24812
+ overhead: exports_external.boolean().default(false).describe("Only costs no client's project carries"),
24908
24813
  billable: exports_external.boolean().optional().describe("Only billable costs, or --no-billable").meta({ negatable: true }),
24909
24814
  limit: exports_external.coerce.number().int().positive().default(50).describe("Most costs to print"),
24910
24815
  json: exports_external.boolean().default(false).describe("Print as JSON")
@@ -24954,9 +24859,8 @@ var remove2 = command({
24954
24859
  },
24955
24860
  run: async ({ positionals: { id } }) => {
24956
24861
  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}.`);
24862
+ const { name } = await backend.mutation(api2.costs.remove, { id: id.trim() });
24863
+ console.log(`Deleted ${name}.`);
24960
24864
  }
24961
24865
  });
24962
24866
 
@@ -24975,7 +24879,7 @@ is fixed at creation \u2014 delete and re-record to change it.`,
24975
24879
  options: {
24976
24880
  name: exports_external.string().optional().describe("New name"),
24977
24881
  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" }),
24882
+ currency: currencyCode.optional().describe(`One of ${CURRENCY_CHOICES}`).meta({ short: "c" }),
24979
24883
  date: exports_external.string().optional().describe("The one-off's date, or a subscription's start (YYYY-MM-DD)"),
24980
24884
  end: exports_external.string().nullable().optional().describe("End date (YYYY-MM-DD), or --no-end to resume").meta({
24981
24885
  negatable: true
@@ -24991,10 +24895,8 @@ is fixed at creation \u2014 delete and re-record to change it.`,
24991
24895
  throw new Error("Nothing to update. Pass --name, --amount, --currency, --date, --end, --project, --billable, or --note.");
24992
24896
  }
24993
24897
  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,
24898
+ const { name } = await backend.mutation(api2.costs.update, {
24899
+ id: id.trim(),
24998
24900
  today: localToday(),
24999
24901
  name: options.name,
25000
24902
  note: options.note === null ? "" : options.note,
@@ -25002,10 +24904,10 @@ is fixed at creation \u2014 delete and re-record to change it.`,
25002
24904
  endDate: options.end === null ? "" : options.end,
25003
24905
  amount: options.amount,
25004
24906
  currency: options.currency,
25005
- projectIds: options.project === undefined ? undefined : projects.map((project) => project._id),
24907
+ projectIds: options.project === null ? [] : options.project?.map(readProjectRef),
25006
24908
  billable: options.billable
25007
24909
  });
25008
- console.log(`Updated ${options.name ?? cost.name}.`);
24910
+ console.log(`Updated ${name}.`);
25009
24911
  }
25010
24912
  });
25011
24913
 
@@ -25015,7 +24917,7 @@ var costs = group({
25015
24917
  description: "Manage costs",
25016
24918
  longDescription: `A cost is money the studio spends, recorded once per real-world bill. It links to any
25017
24919
  number of projects \u2014 each carrying the full amount, so per-project figures never sum
25018
- to the studio total \u2014 and no links at all is overhead. Occurrences are always derived
24920
+ to the studio total. A cost no client's project carries is overhead. Occurrences are always derived
25019
24921
  from the record, so \`costs list --month\` answers for any month, past or future.`,
25020
24922
  commands: [create2, list3, get2, update3, end, remove2]
25021
24923
  });
@@ -25309,9 +25211,10 @@ var seen = command({
25309
25211
  },
25310
25212
  run: async ({ positionals: { id } }) => {
25311
25213
  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.");
25214
+ const { identifier } = await client3.mutation(api2.inbox.markSeen, {
25215
+ targetId: id === undefined ? undefined : readIssueRef(id)
25216
+ });
25217
+ console.log(identifier ? `${identifier} marked seen.` : "Inbox marked seen.");
25315
25218
  }
25316
25219
  });
25317
25220
 
@@ -25325,10 +25228,10 @@ var inbox = group({
25325
25228
  });
25326
25229
 
25327
25230
  // src/lib/line-items.ts
25328
- var LINE_ITEM_SYNTAX = 'description|quantity[ unit]|unit price|currency, e.g. "Development|2.5 hours|10000|EUR"';
25231
+ var LINE_ITEM_SYNTAX = 'description|quantity[ unit]|unit price[|currency], e.g. "Development|2.5 hours|10000|USD"; currency defaults to EGP';
25329
25232
  var parseLineItem = (value) => {
25330
25233
  const parts = value.split("|");
25331
- if (parts.length !== 4)
25234
+ if (parts.length !== 3 && parts.length !== 4)
25332
25235
  throw new Error(`Line items read as ${LINE_ITEM_SYNTAX}; got "${value}".`);
25333
25236
  const [description = "", quantityPart = "", pricePart = "", currency = ""] = parts;
25334
25237
  const [rawQuantity = "", ...unitWords] = quantityPart.trim().split(/\s+/);
@@ -25341,12 +25244,18 @@ var parseLineItem = (value) => {
25341
25244
  throw new Error(`Unit prices must be integer minor units (10000 = 100.00), got "${pricePart.trim()}".`);
25342
25245
  }
25343
25246
  const unit = unitWords.join(" ");
25344
- return { description: description.trim(), quantity, unit: unit || undefined, unitPrice, currency: currency.trim() };
25247
+ return {
25248
+ description: description.trim(),
25249
+ quantity,
25250
+ unit: unit || undefined,
25251
+ unitPrice,
25252
+ currency: parseLineCurrency(currency)
25253
+ };
25345
25254
  };
25346
- var QUOTATION_LINE_ITEM_SYNTAX = 'description|quantity[ unit]|unit price|currency[|section], e.g. "Prototype|1|100000|EUR|Phase 1"';
25255
+ 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
25256
  var parseQuotationLineItem = (value) => {
25348
25257
  const parts = value.split("|");
25349
- if (parts.length !== 4 && parts.length !== 5) {
25258
+ if (parts.length < 3 || parts.length > 5) {
25350
25259
  throw new Error(`Quotation line items read as ${QUOTATION_LINE_ITEM_SYNTAX}; got "${value}".`);
25351
25260
  }
25352
25261
  const section = parts.length === 5 ? parts.pop()?.trim() : undefined;
@@ -25369,11 +25278,9 @@ var create3 = command({
25369
25278
  },
25370
25279
  run: async ({ positionals: { client: ref }, options }) => {
25371
25280
  const backend = await backendClient();
25372
- const client3 = await resolveClient(backend, ref);
25373
- const project = options.project === undefined ? undefined : await resolveProject(backend, options.project);
25374
25281
  const invoiceId = await backend.mutation(api2.invoices.create, {
25375
- clientId: client3._id,
25376
- projectId: project?._id,
25282
+ clientId: readClientRef(ref),
25283
+ projectId: options.project === undefined ? undefined : readProjectRef(options.project),
25377
25284
  lineItems: options.line?.map(parseLineItem),
25378
25285
  notes: options.notes,
25379
25286
  dueDate: options.due
@@ -25391,8 +25298,21 @@ var duplicate = command({
25391
25298
  },
25392
25299
  run: async ({ positionals: { id } }) => {
25393
25300
  const backend = await backendClient();
25394
- const invoice = await resolveInvoice(backend, id);
25395
- console.log(await backend.mutation(api2.invoices.duplicate, { id: invoice._id }));
25301
+ console.log(await backend.mutation(api2.invoices.duplicate, { id: readInvoiceRef(id) }));
25302
+ }
25303
+ });
25304
+
25305
+ // src/commands/invoices/email.ts
25306
+ var email3 = command({
25307
+ name: "email",
25308
+ description: "Email the invoice's link to the client's billing address; repeatable, and never required",
25309
+ positionals: {
25310
+ id: exports_external.string().describe("Invoice id or URL")
25311
+ },
25312
+ run: async ({ positionals: { id } }) => {
25313
+ const backend = await backendClient();
25314
+ const { to, number: number4 } = await backend.mutation(api2.invoices.email, { id: readInvoiceRef(id) });
25315
+ console.log(`Emailed ${number4} to ${to}.`);
25396
25316
  }
25397
25317
  });
25398
25318
 
@@ -25421,6 +25341,9 @@ var get3 = command({
25421
25341
  console.log(`Due: ${invoice.dueDate}`);
25422
25342
  if (invoice.publicUrl)
25423
25343
  console.log(`Link: ${invoice.publicUrl}`);
25344
+ if (invoice.status !== "draft") {
25345
+ console.log(`Emailed: ${invoice.lastEmailedAt === undefined ? "never" : localDateString(invoice.lastEmailedAt)}`);
25346
+ }
25424
25347
  if (invoice.lineItems.length > 0) {
25425
25348
  console.log("");
25426
25349
  printTable([
@@ -25460,9 +25383,7 @@ var issue2 = command({
25460
25383
  },
25461
25384
  run: async ({ positionals: { id } }) => {
25462
25385
  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);
25386
+ const issued = await backend.mutation(api2.invoices.issue, { id: readInvoiceRef(id), today: localToday() });
25466
25387
  console.log(`Issued ${issued.number}, due ${issued.dueDate}.`);
25467
25388
  }
25468
25389
  });
@@ -25477,10 +25398,11 @@ var link = command({
25477
25398
  },
25478
25399
  run: async ({ positionals: { id, project: projectRef } }) => {
25479
25400
  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.");
25401
+ const { projectName } = await backend.mutation(api2.invoices.setProject, {
25402
+ id: readInvoiceRef(id),
25403
+ projectId: projectRef === undefined ? undefined : readProjectRef(projectRef)
25404
+ });
25405
+ console.log(projectName === undefined ? "Cleared the project link." : `Linked to ${projectName}.`);
25484
25406
  }
25485
25407
  });
25486
25408
 
@@ -25508,7 +25430,7 @@ var list4 = command({
25508
25430
  if (result.page.length === 0)
25509
25431
  return console.log("No invoices yet.");
25510
25432
  printTable([
25511
- ["NUMBER", "CLIENT", "PROJECT", "STATUS", "TOTAL", "DUE", "ID"],
25433
+ ["NUMBER", "CLIENT", "PROJECT", "STATUS", "TOTAL", "DUE", "EMAILED", "ID"],
25512
25434
  ...result.page.map((invoice) => [
25513
25435
  invoice.number ?? "draft",
25514
25436
  invoice.clientName,
@@ -25516,6 +25438,7 @@ var list4 = command({
25516
25438
  invoice.derivedStatus,
25517
25439
  formatPerCurrency(invoice.totals.map(({ currency, total }) => ({ currency, amount: total }))),
25518
25440
  invoice.dueDate ?? "-",
25441
+ invoice.lastEmailedAt === undefined ? "-" : localDateString(invoice.lastEmailedAt),
25519
25442
  invoice._id
25520
25443
  ])
25521
25444
  ]);
@@ -25540,18 +25463,16 @@ var add2 = command({
25540
25463
  },
25541
25464
  run: async ({ positionals: { invoice: ref }, options }) => {
25542
25465
  const backend = await backendClient();
25543
- const invoice = await resolveInvoice(backend, ref);
25544
- const paymentId = await backend.mutation(api2.invoices.addPayment, {
25545
- invoiceId: invoice._id,
25466
+ const payment = await backend.mutation(api2.invoices.addPayment, {
25467
+ invoiceId: readInvoiceRef(ref),
25546
25468
  amount: options.amount,
25547
25469
  currency: options.currency,
25548
25470
  paidOn: options.date ?? localToday(),
25549
25471
  note: options.note
25550
25472
  });
25551
- console.log(paymentId);
25552
- if ((await resolveInvoice(backend, invoice._id)).status === "paid") {
25553
- console.log(`${invoice.number} is now fully paid.`);
25554
- }
25473
+ console.log(payment.id);
25474
+ if (payment.paid)
25475
+ console.log(`${payment.number} is now fully paid.`);
25555
25476
  }
25556
25477
  });
25557
25478
 
@@ -25592,8 +25513,7 @@ var remove3 = command({
25592
25513
  },
25593
25514
  run: async ({ positionals: { id } }) => {
25594
25515
  const backend = await backendClient();
25595
- const payment = await resolvePayment(backend, id);
25596
- await backend.mutation(api2.invoices.removePayment, { id: payment._id });
25516
+ await backend.mutation(api2.invoices.removePayment, { id: id.trim() });
25597
25517
  console.log("Removed the payment.");
25598
25518
  }
25599
25519
  });
@@ -25617,9 +25537,8 @@ var rotateLink = command({
25617
25537
  },
25618
25538
  run: async ({ positionals: { id } }) => {
25619
25539
  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.`);
25540
+ const { url: url2, number: number4 } = await backend.mutation(api2.invoices.rotateLink, { id: readInvoiceRef(id) });
25541
+ console.log(`${number4} now reads at ${url2}. Send it again \u2014 the previous link is dead.`);
25623
25542
  }
25624
25543
  });
25625
25544
 
@@ -25641,14 +25560,13 @@ var update4 = command({
25641
25560
  throw new Error("Nothing to update. Pass --line, --notes, or --due (or a --no form to clear).");
25642
25561
  }
25643
25562
  const backend = await backendClient();
25644
- const invoice = await resolveInvoice(backend, id);
25645
- await backend.mutation(api2.invoices.update, {
25646
- id: invoice._id,
25563
+ const { number: number4 } = await backend.mutation(api2.invoices.update, {
25564
+ id: readInvoiceRef(id),
25647
25565
  lineItems: options.line?.map(parseLineItem),
25648
25566
  notes: options.notes === null ? "" : options.notes,
25649
25567
  dueDate: options.due === null ? "" : options.due
25650
25568
  });
25651
- console.log(`Updated ${invoice.number ?? "draft"}.`);
25569
+ console.log(`Updated ${number4 ?? "draft"}.`);
25652
25570
  }
25653
25571
  });
25654
25572
 
@@ -25661,9 +25579,8 @@ var voidInvoice = command({
25661
25579
  },
25662
25580
  run: async ({ positionals: { id } }) => {
25663
25581
  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}.`);
25582
+ const { number: number4 } = await backend.mutation(api2.invoices.voidInvoice, { id: readInvoiceRef(id) });
25583
+ console.log(`Voided ${number4}.`);
25667
25584
  }
25668
25585
  });
25669
25586
 
@@ -25674,7 +25591,7 @@ var invoices = group({
25674
25591
  longDescription: `An invoice is a draft until issued: issuing assigns its gapless number, freezes the
25675
25592
  document, and snapshots the client's billing details. Sent invoices are immutable \u2014
25676
25593
  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]
25594
+ commands: [create3, list4, get3, update4, issue2, email3, voidInvoice, duplicate, link, rotateLink, payments]
25678
25595
  });
25679
25596
 
25680
25597
  // src/lib/attachments.ts
@@ -25743,10 +25660,15 @@ var comment = command({
25743
25660
  },
25744
25661
  run: async ({ positionals: { id, body }, options: { attach } }) => {
25745
25662
  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}.`);
25663
+ if (attach !== undefined && attach.length > 0) {
25664
+ const issue3 = await resolveIssue(client3, id);
25665
+ printAttachments(await attachFiles(client3, issue3._id, attach));
25666
+ }
25667
+ const { identifier } = await client3.mutation(api2.issueComments.create, {
25668
+ issueId: readIssueRef(id),
25669
+ bodyMarkdown: await readTextOption(body)
25670
+ });
25671
+ console.log(`Commented on ${identifier}.`);
25750
25672
  }
25751
25673
  });
25752
25674
 
@@ -25766,7 +25688,6 @@ var agentConfigurationLine = (id) => {
25766
25688
  var create4 = command({
25767
25689
  name: "create",
25768
25690
  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
25691
  positionals: {
25771
25692
  title: exports_external.string().describe("Issue title")
25772
25693
  },
@@ -25781,17 +25702,22 @@ var create4 = command({
25781
25702
  milestone: exports_external.string().optional().describe("Milestone name (needs --project or --here)"),
25782
25703
  parent: exports_external.string().optional().describe("Create as a sub-issue of this issue (identifier, number, or URL)"),
25783
25704
  disposition: exports_external.enum(ISSUE_CREATE_DISPOSITIONS).optional().describe("Route immediately (default needs_triage)"),
25705
+ label: exports_external.array(exports_external.string()).optional().describe("Add a label by name (repeatable)"),
25784
25706
  "agent-config": exports_external.enum(AGENT_CONFIGURATION_IDS).optional().describe(`Agent configuration${agentConfigurationLegacyNote()}`)
25785
25707
  },
25786
25708
  run: async ({ positionals: { title }, options }) => {
25787
25709
  if (options.project && options.here)
25788
25710
  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)
25711
+ let projectRef = options.project === undefined ? undefined : readProjectRef(options.project);
25712
+ if (options.here) {
25713
+ const repo = await currentRepoKey();
25714
+ if (!repo)
25715
+ throw new Error("No repository here: not a git checkout with an origin remote.");
25716
+ projectRef = repo;
25717
+ }
25718
+ if (options.milestone && projectRef === undefined)
25792
25719
  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;
25720
+ const client3 = await backendClient();
25795
25721
  const { identifier } = await client3.mutation(api2.issues.create, {
25796
25722
  title,
25797
25723
  descriptionMarkdown: options.description === undefined ? undefined : await readTextOption(options.description),
@@ -25799,11 +25725,12 @@ var create4 = command({
25799
25725
  priority: options.priority,
25800
25726
  estimate: options.estimate,
25801
25727
  dueDate: options.due,
25802
- projectId: project?._id,
25803
- milestoneId: milestone?._id,
25804
- parentId: parent?._id,
25728
+ projectId: projectRef,
25729
+ milestoneId: options.milestone,
25730
+ parentId: options.parent === undefined ? undefined : readIssueRef(options.parent),
25805
25731
  disposition: options.disposition,
25806
- agentConfigurationId: options["agent-config"]
25732
+ agentConfigurationId: options["agent-config"],
25733
+ labels: options.label
25807
25734
  });
25808
25735
  console.log(identifier);
25809
25736
  }
@@ -25821,86 +25748,107 @@ var printTasks = (tasks) => {
25821
25748
  };
25822
25749
 
25823
25750
  // src/commands/issues/get.ts
25751
+ var printIssue = ({ relations, attachments, pages, tasks, feed, milestone, ...issue3 }) => {
25752
+ const project = issue3.project;
25753
+ console.log(`${issue3.identifier} ${issue3.title}`);
25754
+ console.log(`Status: ${issue3.status} \xB7 Priority: ${issue3.priority} \xB7 Disposition: ${issue3.disposition}`);
25755
+ if (issue3.estimate !== undefined)
25756
+ console.log(`Estimate: ${issue3.estimate}`);
25757
+ if (issue3.dueDate)
25758
+ console.log(`Due: ${issue3.dueDate}`);
25759
+ if (issue3.agentConfigurationId)
25760
+ console.log(`Agent: ${agentConfigurationLine(issue3.agentConfigurationId)}`);
25761
+ if (issue3.branch)
25762
+ console.log(`Branch: ${issue3.branch}`);
25763
+ if (issue3.prUrl)
25764
+ console.log(`PR: ${issue3.prUrl}`);
25765
+ if (project)
25766
+ console.log(`Project: ${project.name}${milestone ? ` \xB7 Milestone: ${milestone.name}` : ""}`);
25767
+ if (issue3.parent)
25768
+ console.log(`Parent: ${issue3.parent.identifier} ${issue3.parent.title}`);
25769
+ if (issue3.labels.length > 0)
25770
+ console.log(`Labels: ${issue3.labels.map((label) => label.name).join(", ")}`);
25771
+ if (issue3.children.total > 0)
25772
+ console.log(`Sub-issues: ${issue3.children.done}/${issue3.children.total} done`);
25773
+ const related = (entries) => entries.map((entry) => `${entry.issue.identifier} (${entry.issue.status})`).join(", ");
25774
+ if (relations.blockedBy.length > 0)
25775
+ console.log(`Blocked by: ${related(relations.blockedBy)}`);
25776
+ if (relations.blocks.length > 0)
25777
+ console.log(`Blocks: ${related(relations.blocks)}`);
25778
+ if (relations.duplicateOf)
25779
+ console.log(`Duplicate of: ${related([relations.duplicateOf])}`);
25780
+ if (relations.duplicates.length > 0)
25781
+ console.log(`Duplicated by: ${related(relations.duplicates)}`);
25782
+ if (relations.relatesTo.length > 0)
25783
+ console.log(`Related: ${related(relations.relatesTo)}`);
25784
+ if (attachments.length > 0) {
25785
+ console.log("Attachments:");
25786
+ for (const attachment of attachments) {
25787
+ console.log(` ${attachment.name} \u2014 ${attachment.url ?? "unavailable"}`);
25788
+ }
25789
+ }
25790
+ if (pages.length > 0) {
25791
+ console.log("Pages:");
25792
+ for (const page of pages) {
25793
+ console.log(` ${page.title} (${page.style}, ${page.visibility}) \u2014 ${page.url}`);
25794
+ }
25795
+ }
25796
+ printTasks(tasks);
25797
+ if (issue3.descriptionMarkdown)
25798
+ console.log(`
25799
+ ${issue3.descriptionMarkdown}`);
25800
+ if (feed.events.length > 0)
25801
+ console.log(`
25802
+ Feed:`);
25803
+ if (feed.truncated)
25804
+ console.log("(older events truncated)");
25805
+ for (const event of feed.events) {
25806
+ const at = new Date(event.at).toISOString().slice(0, 16).replace("T", " ");
25807
+ if (event.type === "activity") {
25808
+ const via = event.via === undefined ? "" : ` \u2014 ${actorLabel(event.via, event.agent)}`;
25809
+ console.log(`${at} ${issueActivityLine(event.detail)}${via}`);
25810
+ } else {
25811
+ console.log(`${at} comment${event.editedAt ? " (edited)" : ""}:`);
25812
+ for (const line of event.bodyMarkdown.split(`
25813
+ `))
25814
+ console.log(` ${line}`);
25815
+ }
25816
+ }
25817
+ };
25824
25818
  var get4 = command({
25825
25819
  name: "get",
25826
- description: "Show an issue: properties, description, and its feed",
25820
+ description: "Show one or more issues: properties, description, and feed",
25827
25821
  positionals: {
25828
- id: exports_external.string().describe("Issue identifier, number, or URL")
25822
+ 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
25823
  },
25830
25824
  options: {
25831
- json: exports_external.boolean().default(false).describe("Print as JSON")
25825
+ json: exports_external.boolean().default(false).describe("Print as JSON (an array when given several issues)")
25832
25826
  },
25833
- run: async ({ positionals: { id }, options: { json: json2 } }) => {
25827
+ run: async ({ positionals: { ids }, options: { json: json2 } }) => {
25834
25828
  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
- }
25829
+ const found = await client3.query(api2.issues.getMany, { identifiers: ids.map(readIssueRef) });
25830
+ const missing = ids.filter((_, index) => !found[index]);
25831
+ if (missing.length === 1)
25832
+ throw new Error(`No issue matches ${missing[0]}.`);
25833
+ if (missing.length > 1)
25834
+ throw new Error(`No issues match ${missing.join(", ")}.`);
25835
+ const details = found.flatMap((detail) => detail ? [detail] : []);
25836
+ if (json2) {
25837
+ const objects = details.map(({ relations, attachments, pages, tasks, feed, milestone: _, ...issue3 }) => ({
25838
+ ...issue3,
25839
+ relations,
25840
+ attachments,
25841
+ pages,
25842
+ tasks,
25843
+ feed
25844
+ }));
25845
+ return console.log(JSON.stringify(details.length === 1 ? objects[0] : objects, null, 2));
25903
25846
  }
25847
+ details.forEach((detail, index) => {
25848
+ if (index > 0)
25849
+ console.log("---");
25850
+ printIssue(detail);
25851
+ });
25904
25852
  }
25905
25853
  });
25906
25854
 
@@ -25947,9 +25895,8 @@ var remove4 = command({
25947
25895
  },
25948
25896
  run: async ({ positionals: { label: ref } }) => {
25949
25897
  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}.`);
25898
+ const removed = await client3.mutation(api2.issueLabels.remove, { id: ref });
25899
+ console.log(`Deleted ${removed.name}.`);
25953
25900
  }
25954
25901
  });
25955
25902
 
@@ -25968,9 +25915,8 @@ var update5 = command({
25968
25915
  if (name === undefined && color === undefined)
25969
25916
  throw new Error("Nothing to update. Pass --name or --color.");
25970
25917
  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}.`);
25918
+ const updated = await client3.mutation(api2.issueLabels.update, { id: ref, name, color });
25919
+ console.log(`Updated ${updated.name}.`);
25974
25920
  }
25975
25921
  });
25976
25922
 
@@ -25981,7 +25927,50 @@ var labels = group({
25981
25927
  commands: [create5, list6, update5, remove4]
25982
25928
  });
25983
25929
 
25930
+ // src/commands/issues/link-pr.ts
25931
+ var linkPr = command({
25932
+ name: "link-pr",
25933
+ description: "Link a pull request to an issue by hand",
25934
+ longDescription: `For a pull request the webhook cannot match on its own: work that started before
25935
+ its issue existed, so the branch never carried the identifier, or a second issue the
25936
+ same pull request lands. From here reviews and the merge move the issue as usual.
25937
+ --claim applies what opening the pull request would have: unstarted work moves to
25938
+ in_progress; any other status is kept.`,
25939
+ positionals: {
25940
+ id: exports_external.string().describe("Issue identifier, number, or URL"),
25941
+ url: exports_external.string().describe("Pull request URL")
25942
+ },
25943
+ options: {
25944
+ claim: exports_external.boolean().default(false).describe("Move backlog or todo work to in_progress along with the link")
25945
+ },
25946
+ run: async ({ positionals: { id, url: url2 }, options: { claim: claim2 } }) => {
25947
+ const client3 = await backendClient();
25948
+ const { identifier, linked, status: status3 } = await client3.mutation(api2.issues.linkPr, {
25949
+ id: readIssueRef(id),
25950
+ prUrl: url2,
25951
+ ...claim2 ? { claim: claim2 } : {}
25952
+ });
25953
+ console.log(`${linked ? "Linked" : "Already linked"} ${url2} to ${identifier}: ${status3}`);
25954
+ }
25955
+ });
25956
+
25984
25957
  // src/commands/issues/list.ts
25958
+ var resolveScope = async (client3, { project, here, milestone }) => {
25959
+ if (milestone) {
25960
+ if (!project && !here)
25961
+ throw new Error("A milestone filter needs --project or --here.");
25962
+ const ref = project ? readProjectRef(project) : await currentRepoKey();
25963
+ if (!ref)
25964
+ throw new Error("No repository here: not a git checkout with an origin remote.");
25965
+ const resolved = await client3.query(api2.milestones.resolve, { project: ref, name: milestone });
25966
+ return { projectId: resolved.projectId, milestoneId: resolved._id };
25967
+ }
25968
+ if (project)
25969
+ return { projectId: (await resolveProject(client3, project))._id };
25970
+ if (here)
25971
+ return { projectId: (await repoProject(client3))._id };
25972
+ return;
25973
+ };
25985
25974
  var list7 = command({
25986
25975
  name: "list",
25987
25976
  description: "List open issues, most recently updated first",
@@ -26002,10 +25991,7 @@ var list7 = command({
26002
25991
  if (options.project && options.here)
26003
25992
  throw new Error("Pass --project or --here, not both.");
26004
25993
  const client3 = await backendClient();
26005
- const project = options.project ? await resolveProject(client3, options.project) : options.here ? await repoProject(client3) : undefined;
26006
- if (options.milestone && !project)
26007
- throw new Error("A milestone filter needs --project or --here.");
26008
- const milestone = project && options.milestone ? await resolveMilestone(client3, project._id, options.milestone) : undefined;
25994
+ const scope = await resolveScope(client3, options);
26009
25995
  const result = await client3.query(api2.issueViews.query, {
26010
25996
  source: {
26011
25997
  type: "custom",
@@ -26019,8 +26005,8 @@ var list7 = command({
26019
26005
  statuses: options.status ? [options.status] : options.all ? undefined : ISSUE_STATUSES.filter(issueIsOpen),
26020
26006
  priorities: options.priority ? [options.priority] : undefined,
26021
26007
  dispositions: options.disposition ? [options.disposition] : undefined,
26022
- projectIds: project ? [project._id] : undefined,
26023
- milestoneIds: milestone ? [milestone._id] : undefined,
26008
+ projectIds: scope ? [scope.projectId] : undefined,
26009
+ milestoneIds: scope?.milestoneId ? [scope.milestoneId] : undefined,
26024
26010
  dueDate: options.due
26025
26011
  }
26026
26012
  }
@@ -26069,9 +26055,8 @@ var markAddressed = command({
26069
26055
  },
26070
26056
  run: async ({ positionals: { id }, options: { commit, finding } }) => {
26071
26057
  const client3 = await backendClient();
26072
- const issue3 = await resolveIssue(client3, id);
26073
26058
  const result = await client3.action(api2.githubReviewer.markAddressed, {
26074
- id: issue3._id,
26059
+ id: readIssueRef(id),
26075
26060
  commitSha: commit,
26076
26061
  findings: finding?.map(parseFindingRef)
26077
26062
  });
@@ -26092,10 +26077,12 @@ var relate = command({
26092
26077
  },
26093
26078
  run: async ({ positionals: { id, kind, other } }) => {
26094
26079
  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}.`);
26080
+ const { identifier, otherIdentifier } = await client3.mutation(api2.issues.addRelation, {
26081
+ id: readIssueRef(id),
26082
+ kind,
26083
+ otherIssueId: readIssueRef(other)
26084
+ });
26085
+ console.log(`${identifier} ${kind.replaceAll("_", " ")} ${otherIdentifier}.`);
26099
26086
  }
26100
26087
  });
26101
26088
 
@@ -26108,9 +26095,8 @@ var remove5 = command({
26108
26095
  },
26109
26096
  run: async ({ positionals: { id } }) => {
26110
26097
  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}.`);
26098
+ const { identifier } = await client3.mutation(api2.issues.remove, { id: readIssueRef(id) });
26099
+ console.log(`Deleted ${identifier}.`);
26114
26100
  }
26115
26101
  });
26116
26102
 
@@ -26118,8 +26104,7 @@ var remove5 = command({
26118
26104
  var route = command({
26119
26105
  name: "route",
26120
26106
  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).`,
26107
+ longDescription: "The disposition says who acts next; needs_triage is the default.",
26123
26108
  positionals: {
26124
26109
  id: exports_external.string().describe("Issue identifier, number, or URL"),
26125
26110
  disposition: exports_external.enum(ISSUE_DISPOSITIONS).describe("Where the issue goes next")
@@ -26129,13 +26114,12 @@ ready_for_agent requires the issue to carry an agent configuration (set --agent-
26129
26114
  },
26130
26115
  run: async ({ positionals: { id, disposition }, options: { comment: comment2 } }) => {
26131
26116
  const client3 = await backendClient();
26132
- const issue3 = await resolveIssue(client3, id);
26133
- await client3.mutation(api2.issues.route, {
26134
- id: issue3._id,
26117
+ const { identifier } = await client3.mutation(api2.issues.route, {
26118
+ id: readIssueRef(id),
26135
26119
  disposition,
26136
26120
  comment: comment2 === undefined ? undefined : await readTextOption(comment2)
26137
26121
  });
26138
- console.log(`Routed ${issue3.identifier} to ${disposition}.`);
26122
+ console.log(`Routed ${identifier} to ${disposition}.`);
26139
26123
  }
26140
26124
  });
26141
26125
 
@@ -26144,8 +26128,7 @@ var set2 = command({
26144
26128
  name: "set",
26145
26129
  description: "Set an issue's status, priority, estimate, due date, project, milestone, parent, agent configuration, or labels",
26146
26130
  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.`,
26131
+ --status is the correction path.`,
26149
26132
  positionals: {
26150
26133
  id: exports_external.string().describe("Issue identifier, number, or URL")
26151
26134
  },
@@ -26177,41 +26160,20 @@ the issue sits in ready_for_agent.`,
26177
26160
  if (Object.values(options).every((value) => value === undefined))
26178
26161
  throw new Error("Nothing to set. Pass --status, --priority, --estimate, --due, --project, --milestone, --parent, --agent-config, or --label.");
26179
26162
  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}.`);
26163
+ const { identifier } = await client3.mutation(api2.issues.edit, {
26164
+ issue: readIssueRef(id),
26165
+ ...parent !== undefined ? { parent: parent === null ? null : readIssueRef(parent) } : {},
26166
+ ...label !== undefined ? { addLabels: label } : {},
26167
+ ...noLabel !== undefined ? { removeLabels: noLabel } : {},
26168
+ ...agentConfig !== undefined ? { agentConfigurationId: agentConfig } : {},
26169
+ ...status3 !== undefined ? { status: status3 } : {},
26170
+ ...priority !== undefined ? { priority } : {},
26171
+ ...estimate !== undefined ? { estimate } : {},
26172
+ ...due !== undefined ? { dueDate: due } : {},
26173
+ ...project !== undefined ? { project: project === null ? null : readProjectRef(project) } : {},
26174
+ ...milestone !== undefined ? { milestone } : {}
26175
+ });
26176
+ console.log(`Updated ${identifier}.`);
26215
26177
  }
26216
26178
  });
26217
26179
 
@@ -26229,29 +26191,18 @@ ready_for_agent is informational, and --ready belongs only to an explicit hand-o
26229
26191
  ready: exports_external.boolean().default(false).describe("Also route the issue to ready_for_agent, assigning the running agent when it is identifiable")
26230
26192
  },
26231
26193
  run: async ({ positionals: { id }, options: { ready } }) => {
26194
+ const actor = await agentClaim();
26195
+ const configurationId = actor && agentConfigurationForActor(actor);
26232
26196
  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}`);
26197
+ const { identifier, branch, disposition, assigned } = await client3.mutation(api2.issues.start, {
26198
+ issue: readIssueRef(id),
26199
+ ...ready ? { ready: { agentConfigurationId: configurationId || undefined } } : {}
26200
+ });
26201
+ console.log(`Started ${identifier}: in_progress, branch ${branch}`);
26251
26202
  if (ready) {
26252
26203
  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.`);
26204
+ } else if (disposition !== "ready_for_agent" && actor) {
26205
+ console.log(`Note: disposition is ${disposition}. Pass --ready if this hand-off was intended.`);
26255
26206
  }
26256
26207
  console.log(`
26257
26208
  git switch -c ${shellArg(branch)}`);
@@ -26267,8 +26218,7 @@ var startReview = command({
26267
26218
  },
26268
26219
  run: async ({ positionals: { id } }) => {
26269
26220
  const client3 = await backendClient();
26270
- const issue3 = await resolveIssue(client3, id);
26271
- const started = await client3.action(api2.githubReviewer.startReview, { id: issue3._id });
26221
+ const started = await client3.action(api2.githubReviewer.startReview, { id: readIssueRef(id) });
26272
26222
  console.log(`Reviewing ${started.identifier}: in_review`);
26273
26223
  console.log(`Announced on ${started.prUrl}; check run open.`);
26274
26224
  }
@@ -26283,9 +26233,8 @@ var submit = command({
26283
26233
  },
26284
26234
  run: async ({ positionals: { id } }) => {
26285
26235
  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`);
26236
+ const { identifier } = await client3.mutation(api2.issues.submit, { id: readIssueRef(id) });
26237
+ console.log(`Submitted ${identifier}: ready_for_review`);
26289
26238
  }
26290
26239
  });
26291
26240
 
@@ -26349,8 +26298,7 @@ Categories: ${REVIEW_FINDING_CATEGORIES.join(", ")}; severities: ${REVIEW_FINDIN
26349
26298
  verdict = { kind: "request_changes", findings: await readFindings(options.findings) };
26350
26299
  }
26351
26300
  const client3 = await backendClient();
26352
- const issue3 = await resolveIssue(client3, id);
26353
- const submitted = await client3.action(api2.githubReviewer.submitReview, { id: issue3._id, verdict });
26301
+ const submitted = await client3.action(api2.githubReviewer.submitReview, { id: readIssueRef(id), verdict });
26354
26302
  console.log(`Submitted review on ${submitted.identifier}: ${submitted.prUrl}`);
26355
26303
  console.log(submitted.summary);
26356
26304
  }
@@ -26377,57 +26325,38 @@ address several rows even as removals renumber it.`,
26377
26325
  },
26378
26326
  run: async ({ positionals: { id }, options: { add: add3, check: check2, uncheck, convert, remove: remove6, json: json2 } }) => {
26379
26327
  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 });
26328
+ const wrote = [add3, check2, uncheck, convert, remove6].some((values) => values !== undefined);
26329
+ if (wrote) {
26330
+ const { identifier, converted, tasks: tasks3 } = await client3.mutation(api2.issueTasks.edit, {
26331
+ issue: readIssueRef(id),
26332
+ ...add3 !== undefined ? { add: add3 } : {},
26333
+ ...check2 !== undefined ? { check: check2 } : {},
26334
+ ...uncheck !== undefined ? { uncheck } : {},
26335
+ ...convert !== undefined ? { convert } : {},
26336
+ ...remove6 !== undefined ? { remove: remove6 } : {}
26337
+ });
26400
26338
  if (!json2) {
26401
- console.log(`${created.identifier} ${created.nested ? "created as a sub-issue" : "created and related"}: ${task2.title}`);
26339
+ for (const created of converted) {
26340
+ console.log(`${created.identifier} ${created.nested ? "created as a sub-issue" : "created and related"}: ${created.title}`);
26341
+ }
26402
26342
  }
26343
+ if (json2)
26344
+ return console.log(JSON.stringify(tasks3, null, 2));
26345
+ if (tasks3.length === 0)
26346
+ return console.log(`${identifier} has no tasks.`);
26347
+ return printTasks(tasks3);
26403
26348
  }
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;
26349
+ const issue3 = await resolveIssue(client3, id);
26350
+ const tasks2 = await client3.query(api2.issueTasks.list, { issueId: issue3._id });
26408
26351
  if (json2)
26409
- return console.log(JSON.stringify(after, null, 2));
26410
- if (after.length === 0)
26352
+ return console.log(JSON.stringify(tasks2, null, 2));
26353
+ if (tasks2.length === 0)
26411
26354
  return console.log(`${issue3.identifier} has no tasks.`);
26412
- printTasks(after);
26355
+ printTasks(tasks2);
26413
26356
  }
26414
26357
  });
26415
26358
 
26416
26359
  // 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
26360
  var unrelate = command({
26432
26361
  name: "unrelate",
26433
26362
  description: "Remove a relation between two issues (a canceled duplicate stays canceled)",
@@ -26438,15 +26367,12 @@ var unrelate = command({
26438
26367
  },
26439
26368
  run: async ({ positionals: { id, kind, other } }) => {
26440
26369
  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}.`);
26370
+ const { identifier, otherIdentifier } = await client3.mutation(api2.issues.removeRelation, {
26371
+ id: readIssueRef(id),
26372
+ kind,
26373
+ otherIssueId: readIssueRef(other)
26374
+ });
26375
+ console.log(`Removed: ${identifier} ${kind.replaceAll("_", " ")} ${otherIdentifier}.`);
26450
26376
  }
26451
26377
  });
26452
26378
 
@@ -26466,16 +26392,18 @@ var update6 = command({
26466
26392
  if (title === undefined && description === undefined && attach === undefined)
26467
26393
  throw new Error("Nothing to update. Pass --title, --description, or --attach.");
26468
26394
  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}.`);
26395
+ let attached;
26396
+ if (attach !== undefined && attach.length > 0) {
26397
+ const issue3 = await resolveIssue(client3, id);
26398
+ printAttachments(await attachFiles(client3, issue3._id, attach));
26399
+ attached = issue3.identifier;
26400
+ }
26401
+ const identifier = title !== undefined || description !== undefined ? (await client3.mutation(api2.issues.update, {
26402
+ id: readIssueRef(id),
26403
+ title,
26404
+ descriptionMarkdown: description === undefined ? undefined : await readTextOption(description)
26405
+ })).identifier : attached;
26406
+ console.log(`Updated ${identifier}.`);
26479
26407
  }
26480
26408
  });
26481
26409
 
@@ -26488,11 +26416,11 @@ todo, in_progress, ready_for_review, in_review, changes_requested, approved, don
26488
26416
  canceled), priority, and disposition (who acts next). The workflow commands \u2014 start,
26489
26417
  submit, start-review, submit-review \u2014 and the linked pull request move the status;
26490
26418
  each refuses an issue outside its own status, and \`set -s\` is for corrections.
26491
- The repo link is never set by hand: work on a branch whose name contains the issue
26492
- identifier and the branch, pull request, and status track automatically \u2014 a change
26493
- request moves the issue to changes_requested, an approval to approved (one
26494
- outstanding change request outweighs any number of approvals), and a merge lands
26495
- it as done.`,
26419
+ Work on a branch whose name contains the issue identifier and the branch, pull
26420
+ request, and status track automatically \u2014 a change request moves the issue to
26421
+ changes_requested, an approval to approved (one outstanding change request outweighs
26422
+ any number of approvals), and a merge lands it as done. link-pr covers the pull
26423
+ request the branch name does not point at; from there the same tracking applies.`,
26496
26424
  commands: [
26497
26425
  create4,
26498
26426
  list7,
@@ -26506,6 +26434,7 @@ it as done.`,
26506
26434
  startReview,
26507
26435
  submitReview,
26508
26436
  markAddressed,
26437
+ linkPr,
26509
26438
  tasks,
26510
26439
  relate,
26511
26440
  unrelate,
@@ -26514,6 +26443,9 @@ it as done.`,
26514
26443
  ]
26515
26444
  });
26516
26445
 
26446
+ // ../../packages/backend/convex/lib/object.ts
26447
+ var keys = (object2) => Object.keys(object2);
26448
+
26517
26449
  // src/lib/group.ts
26518
26450
  import { basename as basename2 } from "path";
26519
26451
  var git = async (...args) => {
@@ -26541,20 +26473,21 @@ var create6 = command({
26541
26473
  options: {
26542
26474
  title: exports_external.string().describe("Page title").meta({ short: "t" }),
26543
26475
  group: exports_external.string().nullable().optional().describe("Group to list the page under (default: the current repo)").meta({ short: "g", negatable: true }),
26544
- raw: exports_external.boolean().default(false).describe("Serve a full document verbatim (may run JS)"),
26545
- private: exports_external.boolean().default(false).describe("Require the publishing account to view the page")
26476
+ style: exports_external.enum(keys(PAGE_STYLES)).default("basic").describe(`Style to render in \u2014 ${describeChoices(PAGE_STYLES)}`),
26477
+ private: exports_external.boolean().default(false).describe("Require the publishing account to view the page"),
26478
+ issue: exports_external.string().optional().describe("Link the page to an issue (KAI-N, number, or URL)")
26546
26479
  },
26547
- run: async ({ positionals: { file: file2 }, options: { title, group: group2, raw, private: isPrivate } }) => {
26480
+ run: async ({ positionals: { file: file2 }, options: { title, group: group2, style, private: isPrivate, issue: issue3 } }) => {
26548
26481
  const newGroup = await groupForCreate(group2);
26549
26482
  const html = await readBody(file2);
26550
- const mode = raw ? "raw" : "themed";
26551
26483
  const visibility = isPrivate ? "private" : "public";
26552
26484
  const { url: url2 } = await (await backendClient()).action(api2.pages.create, {
26553
26485
  title,
26554
26486
  group: newGroup,
26555
26487
  html,
26556
- mode,
26557
- visibility
26488
+ style,
26489
+ visibility,
26490
+ issue: issue3 === undefined ? undefined : readIssueRef(issue3)
26558
26491
  });
26559
26492
  console.log(url2);
26560
26493
  }
@@ -26598,12 +26531,12 @@ var list8 = command({
26598
26531
  if (pages.length === 0)
26599
26532
  return console.log(group2 === undefined ? "No pages yet." : "No pages in that group.");
26600
26533
  printTable([
26601
- ["TITLE", "GROUP", "ACCESS", "MODE", "SIZE", "VERSIONS", "CREATED", "URL"],
26534
+ ["TITLE", "GROUP", "ACCESS", "STYLE", "SIZE", "VERSIONS", "CREATED", "URL"],
26602
26535
  ...pages.map((page) => [
26603
26536
  page.title,
26604
26537
  page.group ?? "-",
26605
26538
  page.visibility,
26606
- page.mode,
26539
+ page.style,
26607
26540
  formatBytes(page.size),
26608
26541
  page.versions === 0 ? "-" : String(page.versions),
26609
26542
  new Date(page.createdAt).toISOString().slice(0, 10),
@@ -26626,6 +26559,14 @@ var remove6 = command({
26626
26559
  }
26627
26560
  });
26628
26561
 
26562
+ // src/lib/pages.ts
26563
+ var warnPruned = (pruned) => {
26564
+ if (pruned.length === 0)
26565
+ return;
26566
+ const versions2 = pruned.length === 1 ? `version ${pruned[0]}` : `versions ${pruned.join(", ")}`;
26567
+ console.error(`Pruned ${versions2}; the history keeps the newest ${MAX_PAGE_VERSIONS}.`);
26568
+ };
26569
+
26629
26570
  // src/commands/pages/revert.ts
26630
26571
  var revert = command({
26631
26572
  name: "revert",
@@ -26635,7 +26576,8 @@ var revert = command({
26635
26576
  version: exports_external.coerce.number().int().positive().describe("Version number from `kds pages versions`")
26636
26577
  },
26637
26578
  run: async ({ positionals: { id, version: version4 } }) => {
26638
- const { url: url2 } = await (await backendClient()).mutation(api2.pages.revert, { id: readPageId(id), version: version4 });
26579
+ const { url: url2, pruned } = await (await backendClient()).mutation(api2.pages.revert, { id: readPageId(id), version: version4 });
26580
+ warnPruned(pruned);
26639
26581
  console.log(url2);
26640
26582
  }
26641
26583
  });
@@ -26643,7 +26585,7 @@ var revert = command({
26643
26585
  // src/commands/pages/update.ts
26644
26586
  var update7 = command({
26645
26587
  name: "update",
26646
- description: "Replace a page's HTML, title, group, mode, or visibility",
26588
+ description: "Replace a page's HTML, title, group, style, or visibility",
26647
26589
  positionals: {
26648
26590
  id: exports_external.string().describe("Page id or URL"),
26649
26591
  file: exports_external.string().optional().describe("HTML file, or - for stdin")
@@ -26651,21 +26593,24 @@ var update7 = command({
26651
26593
  options: {
26652
26594
  title: exports_external.string().optional().describe("New page title").meta({ short: "t" }),
26653
26595
  group: exports_external.string().nullable().optional().describe("Move the page to this group, or --no-group to remove it from one").meta({ short: "g", negatable: true }),
26654
- mode: exports_external.enum(PAGE_MODES).optional().describe("New serving mode"),
26655
- visibility: exports_external.enum(PAGE_VISIBILITIES).optional().describe("Who can view the page")
26596
+ style: exports_external.enum(keys(PAGE_STYLES)).optional().describe(`New style \u2014 ${describeChoices(PAGE_STYLES)}`),
26597
+ visibility: exports_external.enum(PAGE_VISIBILITIES).optional().describe("Who can view the page"),
26598
+ issue: exports_external.string().nullable().optional().describe("Link the page to an issue (KAI-N, number, or URL), or --no-issue to unlink it").meta({ negatable: true })
26656
26599
  },
26657
- run: async ({ positionals: { id, file: file2 }, options: { title, group: group2, mode, visibility } }) => {
26658
- if (!file2 && title === undefined && group2 === undefined && mode === undefined && visibility === undefined)
26659
- throw new Error("Nothing to update. Pass a file, --title, --group, --no-group, --mode, or --visibility.");
26600
+ run: async ({ positionals: { id, file: file2 }, options: { title, group: group2, style, visibility, issue: issue3 } }) => {
26601
+ if (!file2 && title === undefined && group2 === undefined && style === undefined && visibility === undefined && issue3 === undefined)
26602
+ throw new Error("Nothing to update. Pass a file, --title, --group, --no-group, --style, --visibility, --issue, or --no-issue.");
26660
26603
  const html = file2 ? await readBody(file2) : undefined;
26661
- const { url: url2 } = await (await backendClient()).action(api2.pages.update, {
26604
+ const { url: url2, pruned } = await (await backendClient()).action(api2.pages.update, {
26662
26605
  id: readPageId(id),
26663
26606
  html,
26664
26607
  title,
26665
26608
  group: group2,
26666
- mode,
26667
- visibility
26609
+ style,
26610
+ visibility,
26611
+ issue: typeof issue3 === "string" ? readIssueRef(issue3) : issue3
26668
26612
  });
26613
+ warnPruned(pruned);
26669
26614
  console.log(url2);
26670
26615
  }
26671
26616
  });
@@ -26687,10 +26632,10 @@ var versions2 = command({
26687
26632
  if (archived.length === 0)
26688
26633
  return console.log("No previous versions.");
26689
26634
  printTable([
26690
- ["VERSION", "MODE", "SIZE", "ARCHIVED"],
26635
+ ["VERSION", "STYLE", "SIZE", "ARCHIVED"],
26691
26636
  ...archived.map((version4) => [
26692
26637
  String(version4.version),
26693
- version4.mode,
26638
+ version4.style,
26694
26639
  formatBytes(version4.size),
26695
26640
  new Date(version4.archivedAt).toISOString().slice(0, 10)
26696
26641
  ])
@@ -26702,10 +26647,10 @@ var versions2 = command({
26702
26647
  var pages = group({
26703
26648
  name: "pages",
26704
26649
  description: "Publish HTML documents to the web",
26705
- longDescription: `Themed pages (the default) render inside the site chrome under a strict CSP: only
26706
- the site theme's styles apply \u2014 inline style attributes and author scripts are
26707
- dropped. A self-contained document with its own CSS or JS needs raw mode, which
26708
- serves it verbatim. A page keeps its URL across updates and reverts.`,
26650
+ longDescription: `A page is an HTML fragment rendered in one of the site's styles:
26651
+ ${describeChoices(PAGE_STYLES, `
26652
+ `)}
26653
+ Basic is the default. A page keeps its URL across updates and reverts.`,
26709
26654
  commands: [create6, list8, get5, update7, versions2, revert, remove6]
26710
26655
  });
26711
26656
 
@@ -26764,7 +26709,7 @@ var create7 = command({
26764
26709
  health: options.health,
26765
26710
  targetDate: options.target,
26766
26711
  repo: options.repo,
26767
- clientId: options.client === undefined ? undefined : (await resolveClient(backend, options.client))._id
26712
+ clientId: options.client === undefined ? undefined : readClientRef(options.client)
26768
26713
  });
26769
26714
  console.log(projectId);
26770
26715
  }
@@ -26866,9 +26811,8 @@ var remove7 = command({
26866
26811
  },
26867
26812
  run: async ({ positionals: { id } }) => {
26868
26813
  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}.`);
26814
+ const removed = await client3.mutation(api2.projects.remove, { id: readProjectRef(id) });
26815
+ console.log(`Deleted ${removed.name}.`);
26872
26816
  }
26873
26817
  });
26874
26818
 
@@ -26891,21 +26835,15 @@ var set3 = command({
26891
26835
  if (Object.values(options).every((value) => value === undefined))
26892
26836
  throw new Error("Nothing to set. Pass --status, --health, --target, --repo, or --client.");
26893
26837
  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}.`);
26838
+ const { name } = await backend.mutation(api2.projects.edit, {
26839
+ id: readProjectRef(id),
26840
+ ...repo !== undefined ? { repo } : {},
26841
+ ...client3 !== undefined ? { client: client3 === null ? null : readClientRef(client3) } : {},
26842
+ ...status3 !== undefined ? { status: status3 } : {},
26843
+ ...health !== undefined ? { health } : {},
26844
+ ...target !== undefined ? { targetDate: target } : {}
26845
+ });
26846
+ console.log(`Updated ${name}.`);
26909
26847
  }
26910
26848
  });
26911
26849
 
@@ -26925,14 +26863,13 @@ var update8 = command({
26925
26863
  if (name === undefined && summary === undefined && description === undefined)
26926
26864
  throw new Error("Nothing to update. Pass --name, --summary, or --description.");
26927
26865
  const client3 = await backendClient();
26928
- const project2 = await resolveProject(client3, id);
26929
- await client3.mutation(api2.projects.update, {
26930
- id: project2._id,
26866
+ const updated = await client3.mutation(api2.projects.update, {
26867
+ id: readProjectRef(id),
26931
26868
  name,
26932
26869
  summary,
26933
26870
  descriptionMarkdown: description === undefined ? undefined : await readTextOption(description)
26934
26871
  });
26935
- console.log(`Updated ${name ?? project2.name}.`);
26872
+ console.log(`Updated ${updated.name}.`);
26936
26873
  }
26937
26874
  });
26938
26875
 
@@ -26958,13 +26895,12 @@ var accept = command({
26958
26895
  },
26959
26896
  run: async ({ positionals: { id }, options }) => {
26960
26897
  const backend = await backendClient();
26961
- const quotation = await resolveQuotation(backend, id);
26962
- await backend.mutation(api2.quotations.accept, {
26963
- id: quotation._id,
26898
+ const accepted = await backend.mutation(api2.quotations.accept, {
26899
+ id: readQuotationRef(id),
26964
26900
  on: options.date ?? localToday(),
26965
26901
  note: options.note
26966
26902
  });
26967
- console.log(`Accepted ${quotation.number}. Draw invoices from it with: kds quotations convert ${quotation._id}`);
26903
+ console.log(`Accepted ${accepted.number}. Draw invoices from it with: kds quotations convert ${accepted.id}`);
26968
26904
  }
26969
26905
  });
26970
26906
 
@@ -26985,7 +26921,6 @@ Nothing blocks over- or under-invoicing; left to bill is advisory.`,
26985
26921
  },
26986
26922
  run: async ({ positionals: { id }, options }) => {
26987
26923
  const backend = await backendClient();
26988
- const quotation = await resolveQuotation(backend, id);
26989
26924
  const lineIndexes = options.lines?.split(",").map((part) => {
26990
26925
  const lineNumber = Number(part.trim());
26991
26926
  if (!Number.isInteger(lineNumber) || lineNumber < 1) {
@@ -26994,7 +26929,7 @@ Nothing blocks over- or under-invoicing; left to bill is advisory.`,
26994
26929
  return lineNumber - 1;
26995
26930
  });
26996
26931
  const invoiceId = await backend.mutation(api2.quotations.convert, {
26997
- id: quotation._id,
26932
+ id: readQuotationRef(id),
26998
26933
  lineIndexes,
26999
26934
  percentage: options.percentage
27000
26935
  });
@@ -27017,11 +26952,9 @@ var create8 = command({
27017
26952
  },
27018
26953
  run: async ({ positionals: { client: ref }, options }) => {
27019
26954
  const backend = await backendClient();
27020
- const client3 = await resolveClient(backend, ref);
27021
- const project2 = options.project === undefined ? undefined : await resolveProject(backend, options.project);
27022
26955
  const quotationId = await backend.mutation(api2.quotations.create, {
27023
- clientId: client3._id,
27024
- projectId: project2?._id,
26956
+ clientId: readClientRef(ref),
26957
+ projectId: options.project === undefined ? undefined : readProjectRef(options.project),
27025
26958
  lineItems: options.line?.map(parseQuotationLineItem),
27026
26959
  notes: options.notes,
27027
26960
  validUntil: options.validUntil === null ? "" : options.validUntil,
@@ -27040,8 +26973,21 @@ var duplicate2 = command({
27040
26973
  },
27041
26974
  run: async ({ positionals: { id } }) => {
27042
26975
  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() }));
26976
+ console.log(await backend.mutation(api2.quotations.duplicate, { id: readQuotationRef(id), today: localToday() }));
26977
+ }
26978
+ });
26979
+
26980
+ // src/commands/quotations/email.ts
26981
+ var email4 = command({
26982
+ name: "email",
26983
+ description: "Email the quotation's link to the client's billing address; repeatable, and never required",
26984
+ positionals: {
26985
+ id: exports_external.string().describe("Quotation id or URL")
26986
+ },
26987
+ run: async ({ positionals: { id } }) => {
26988
+ const backend = await backendClient();
26989
+ const { to, number: number4 } = await backend.mutation(api2.quotations.email, { id: readQuotationRef(id) });
26990
+ console.log(`Emailed ${number4} to ${to}.`);
27045
26991
  }
27046
26992
  });
27047
26993
 
@@ -27070,6 +27016,9 @@ var get7 = command({
27070
27016
  console.log(`Valid until: ${quotation.validUntil}`);
27071
27017
  if (quotation.publicUrl)
27072
27018
  console.log(`Link: ${quotation.publicUrl}`);
27019
+ if (quotation.status !== "draft") {
27020
+ console.log(`Emailed: ${quotation.lastEmailedAt === undefined ? "never" : localDateString(quotation.lastEmailedAt)}`);
27021
+ }
27073
27022
  if (quotation.outcome) {
27074
27023
  console.log(`${quotation.status === "accepted" ? "Accepted" : "Rejected"}: ${quotation.outcome.on}${quotation.outcome.note ? ` \u2014 ${quotation.outcome.note}` : ""}`);
27075
27024
  }
@@ -27126,9 +27075,7 @@ var issue3 = command({
27126
27075
  },
27127
27076
  run: async ({ positionals: { id } }) => {
27128
27077
  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);
27078
+ const issued = await backend.mutation(api2.quotations.issue, { id: readQuotationRef(id), today: localToday() });
27132
27079
  console.log(`Issued ${issued.number}, ${issued.validUntil ? `valid until ${issued.validUntil}` : "with no expiry"}.`);
27133
27080
  }
27134
27081
  });
@@ -27143,10 +27090,11 @@ var link2 = command({
27143
27090
  },
27144
27091
  run: async ({ positionals: { id, project: projectRef } }) => {
27145
27092
  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.");
27093
+ const { projectName } = await backend.mutation(api2.quotations.setProject, {
27094
+ id: readQuotationRef(id),
27095
+ projectId: projectRef === undefined ? undefined : readProjectRef(projectRef)
27096
+ });
27097
+ console.log(projectName === undefined ? "Cleared the project link." : `Linked to ${projectName}.`);
27150
27098
  }
27151
27099
  });
27152
27100
 
@@ -27174,7 +27122,7 @@ var list10 = command({
27174
27122
  if (result.page.length === 0)
27175
27123
  return console.log("No quotations yet.");
27176
27124
  printTable([
27177
- ["NUMBER", "CLIENT", "PROJECT", "STATUS", "TOTAL", "VALID UNTIL", "ID"],
27125
+ ["NUMBER", "CLIENT", "PROJECT", "STATUS", "TOTAL", "VALID UNTIL", "EMAILED", "ID"],
27178
27126
  ...result.page.map((quotation) => [
27179
27127
  quotation.number ?? "draft",
27180
27128
  quotation.clientName,
@@ -27182,6 +27130,7 @@ var list10 = command({
27182
27130
  quotation.derivedStatus,
27183
27131
  formatPerCurrency(quotation.totals.map(({ currency, total }) => ({ currency, amount: total }))),
27184
27132
  quotation.validUntil ?? "-",
27133
+ quotation.lastEmailedAt === undefined ? "-" : localDateString(quotation.lastEmailedAt),
27185
27134
  quotation._id
27186
27135
  ])
27187
27136
  ]);
@@ -27204,13 +27153,12 @@ var reject2 = command({
27204
27153
  },
27205
27154
  run: async ({ positionals: { id }, options }) => {
27206
27155
  const backend = await backendClient();
27207
- const quotation = await resolveQuotation(backend, id);
27208
- await backend.mutation(api2.quotations.reject, {
27209
- id: quotation._id,
27156
+ const { number: number4 } = await backend.mutation(api2.quotations.reject, {
27157
+ id: readQuotationRef(id),
27210
27158
  on: options.date ?? localToday(),
27211
27159
  note: options.note
27212
27160
  });
27213
- console.log(`Rejected ${quotation.number}.`);
27161
+ console.log(`Rejected ${number4}.`);
27214
27162
  }
27215
27163
  });
27216
27164
 
@@ -27223,9 +27171,8 @@ var rotateLink2 = command({
27223
27171
  },
27224
27172
  run: async ({ positionals: { id } }) => {
27225
27173
  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.`);
27174
+ const { url: url2, number: number4 } = await backend.mutation(api2.quotations.rotateLink, { id: readQuotationRef(id) });
27175
+ console.log(`${number4} now reads at ${url2}. Send it again \u2014 the previous link is dead.`);
27229
27176
  }
27230
27177
  });
27231
27178
 
@@ -27247,14 +27194,13 @@ var update9 = command({
27247
27194
  throw new Error("Nothing to update. Pass --line, --notes, or --valid-until (or a --no form to clear).");
27248
27195
  }
27249
27196
  const backend = await backendClient();
27250
- const quotation = await resolveQuotation(backend, id);
27251
- await backend.mutation(api2.quotations.update, {
27252
- id: quotation._id,
27197
+ const { number: number4 } = await backend.mutation(api2.quotations.update, {
27198
+ id: readQuotationRef(id),
27253
27199
  lineItems: options.line?.map(parseQuotationLineItem),
27254
27200
  notes: options.notes === null ? "" : options.notes,
27255
27201
  validUntil: options.validUntil === null ? "" : options.validUntil
27256
27202
  });
27257
- console.log(`Updated ${quotation.number ?? "draft"}.`);
27203
+ console.log(`Updated ${number4 ?? "draft"}.`);
27258
27204
  }
27259
27205
  });
27260
27206
 
@@ -27267,9 +27213,8 @@ var voidQuotation = command({
27267
27213
  },
27268
27214
  run: async ({ positionals: { id } }) => {
27269
27215
  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}.`);
27216
+ const { number: number4 } = await backend.mutation(api2.quotations.voidQuotation, { id: readQuotationRef(id) });
27217
+ console.log(`Voided ${number4}.`);
27273
27218
  }
27274
27219
  });
27275
27220
 
@@ -27281,7 +27226,21 @@ var quotations = group({
27281
27226
  document awaiting the client's answer. Accept and reject are manual records;
27282
27227
  expiry is derived from valid-until and never blocks accepting. An accepted
27283
27228
  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]
27229
+ commands: [
27230
+ create8,
27231
+ list10,
27232
+ get7,
27233
+ update9,
27234
+ issue3,
27235
+ email4,
27236
+ accept,
27237
+ reject2,
27238
+ voidQuotation,
27239
+ duplicate2,
27240
+ convert,
27241
+ link2,
27242
+ rotateLink2
27243
+ ]
27285
27244
  });
27286
27245
 
27287
27246
  // src/commands/upgrade.ts
@@ -27312,6 +27271,7 @@ var rootCommand = group({
27312
27271
 
27313
27272
  // src/lib/errors.ts
27314
27273
  var errorMessage = (error51) => error51 instanceof ConvexError && typeof error51.data === "string" ? error51.data : error51.message;
27274
+ var isRedactedServerError = (error51) => error51 instanceof Error && !(error51 instanceof ConvexError) && /^\[Request ID: \w+\] Server Error$/.test(error51.message);
27315
27275
 
27316
27276
  // src/lib/update-check.ts
27317
27277
  var latestVersion = async () => {
@@ -27342,6 +27302,12 @@ var maybeNotifyUpdate = async () => {
27342
27302
  if (latest && Bun.semver.order(latest, CLI_VERSION) === 1)
27343
27303
  console.error(`kds v${latest} available (you have v${CLI_VERSION}) \u2014 run 'kds upgrade'`);
27344
27304
  };
27305
+ var staleCliHint = async (current = CLI_VERSION) => {
27306
+ const latest = env.KDS_NO_UPDATE_CHECK ? undefined : await latestVersion().catch(() => {
27307
+ return;
27308
+ });
27309
+ return latest && Bun.semver.order(latest, current) === 1 ? `kds v${current} is behind v${latest}, so the server likely refused a request this version still sends \u2014 run 'kds upgrade' and retry.` : "The server gave no details. If kds was installed a while ago, try 'kds upgrade'; otherwise report the request id.";
27310
+ };
27345
27311
 
27346
27312
  // src/index.ts
27347
27313
  var args = Bun.argv.slice(2);
@@ -27351,6 +27317,8 @@ try {
27351
27317
  await maybeNotifyUpdate();
27352
27318
  } catch (error51) {
27353
27319
  console.error(errorMessage(error51));
27320
+ if (isRedactedServerError(error51))
27321
+ console.error(await staleCliHint());
27354
27322
  process.exitCode = 1;
27355
27323
  } finally {
27356
27324
  closePrompts();