@islamihab/kds 0.11.0 → 0.13.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 +474 -148
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -14623,7 +14623,7 @@ import { join } from "path";
14623
14623
  // package.json
14624
14624
  var package_default = {
14625
14625
  name: "cli",
14626
- version: "0.11.0",
14626
+ version: "0.13.0",
14627
14627
  private: true,
14628
14628
  type: "module",
14629
14629
  bin: {
@@ -15196,9 +15196,22 @@ var components = componentsGeneric();
15196
15196
 
15197
15197
  // ../../packages/backend/convex/constants.ts
15198
15198
  var KDS_DEVICE_AUTH_CLIENT_ID = "kds-cli";
15199
- 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
+ };
15200
15210
  var PAGE_VISIBILITIES = ["public", "private"];
15201
15211
  var MAX_PAGE_HTML_BYTES = 4000000;
15212
+ var MAX_PAGE_VERSIONS = 20;
15213
+ var FILE_VISIBILITIES = ["public", "private"];
15214
+ var MAX_FILE_BYTES = 20000000;
15202
15215
  var ISSUE_STATUSES = [
15203
15216
  "backlog",
15204
15217
  "todo",
@@ -15368,7 +15381,16 @@ var issueIsOpen = (status) => !ISSUE_TERMINAL_STATUSES.some((terminal) => termin
15368
15381
  var CURRENCIES = ["EGP", "USD", "EUR", "GBP", "AED", "SAR"];
15369
15382
  var DEFAULT_CURRENCY = "EGP";
15370
15383
  var RECURRING_CHARGE_CADENCES = ["monthly", "yearly"];
15384
+ var RECURRING_CHARGE_CADENCE_LABELS = {
15385
+ monthly: "Monthly",
15386
+ yearly: "Yearly"
15387
+ };
15371
15388
  var COST_KINDS = ["one_off", "monthly", "yearly"];
15389
+ var COST_KIND_LABELS = {
15390
+ one_off: "One-off",
15391
+ monthly: "Monthly",
15392
+ yearly: "Yearly"
15393
+ };
15372
15394
  var MAX_PROJECT_REPO_LENGTH = 200;
15373
15395
  var INVOICE_STATUSES = ["draft", "sent", "paid", "void"];
15374
15396
  var invoiceLineTotal = ({ quantity, unitPrice }) => {
@@ -17346,6 +17368,7 @@ var localDateString = (timestamp) => {
17346
17368
  return `${date5.getFullYear()}-${month}-${day}`;
17347
17369
  };
17348
17370
  var localToday = () => localDateString(Date.now());
17371
+ var describeChoices = (choices, separator = "; ") => Object.entries(choices).map(([key, { description }]) => `${key}: ${description}`).join(separator);
17349
17372
 
17350
17373
  // src/lib/zod.ts
17351
17374
  var configSchema = exports_external.object({ sessionToken: exports_external.string().optional(), convexUrl: exports_external.url(), convexSiteUrl: exports_external.url() });
@@ -24120,6 +24143,10 @@ var issueActivityLine = (detail) => {
24120
24143
  case "attachment_added":
24121
24144
  case "attachment_removed":
24122
24145
  return `${label}: ${detail.name}`;
24146
+ case "page_linked":
24147
+ case "page_unlinked":
24148
+ case "page_updated":
24149
+ return `${label}: ${detail.title}`;
24123
24150
  case "child_added":
24124
24151
  case "child_removed":
24125
24152
  return `${label}: ${detail.identifier}`;
@@ -24264,6 +24291,15 @@ var readPageId = (value) => {
24264
24291
  return trimmed;
24265
24292
  }
24266
24293
  };
24294
+ var readFileRef = (value) => {
24295
+ const trimmed = value.trim();
24296
+ try {
24297
+ const url2 = new URL(trimmed);
24298
+ return url2.pathname.match(/\/f\/([^/]+)\/?$/)?.[1] ?? trimmed;
24299
+ } catch {
24300
+ return trimmed;
24301
+ }
24302
+ };
24267
24303
 
24268
24304
  // src/commands/clients/archive.ts
24269
24305
  var archive = command({
@@ -24413,15 +24449,6 @@ var repoProject = async (client3) => {
24413
24449
  throw new Error(`No project is connected to ${repo}. Connect one on the project in the dashboard.`);
24414
24450
  return project;
24415
24451
  };
24416
- var resolveMilestone = async (client3, projectId, name) => {
24417
- const milestone = await client3.query(api2.milestones.find, { projectId, name });
24418
- if (!milestone) {
24419
- const milestones = await client3.query(api2.milestones.listByProject, { projectId, today: localToday() });
24420
- const names = milestones.map((candidate) => candidate.name).join(", ");
24421
- throw new Error(names ? `No milestone named "${name}". The project has: ${names}.` : "The project has no milestones.");
24422
- }
24423
- return milestone;
24424
- };
24425
24452
 
24426
24453
  // src/commands/clients/charges/list.ts
24427
24454
  var monthName = (month) => new Date(Date.UTC(2000, month - 1)).toLocaleString("en-US", { month: "short", timeZone: "UTC" });
@@ -24691,9 +24718,10 @@ var create2 = command({
24691
24718
  name: "create",
24692
24719
  description: "Record a cost and print its id",
24693
24720
  longDescription: `One record per real-world bill, never one per project. A cost links to any number
24694
- of projects, each carrying the full amount; no links at all is overhead. Billable
24695
- costs pass through onto a client's invoices, so their projects must all belong to
24696
- the same client.`,
24721
+ of projects, each carrying the full amount; until a client's project carries it, it
24722
+ is overhead. Billable
24723
+ costs pass through onto the invoices of whichever client their projects belong to,
24724
+ so the links cannot span two clients; with no links, billable waits for one.`,
24697
24725
  positionals: {
24698
24726
  name: exports_external.string().describe('What the bill is for, e.g. "Figma seats"')
24699
24727
  },
@@ -24750,9 +24778,10 @@ var costKindLabel = (cost) => {
24750
24778
  return cost.endDate === undefined ? cost.kind : `${cost.kind} \u2192 ${cost.endDate}`;
24751
24779
  };
24752
24780
  var costProjectsLabel = (cost) => {
24753
- if (cost.overhead)
24754
- return "overhead";
24755
- return cost.projects.length === 0 ? "(deleted project)" : cost.projects.map((project) => project.name).join(", ");
24781
+ const names = cost.projects.map((project) => project.name).join(", ");
24782
+ if (!cost.overhead)
24783
+ return names;
24784
+ return names === "" ? "overhead" : `overhead (${names})`;
24756
24785
  };
24757
24786
 
24758
24787
  // src/commands/costs/get.ts
@@ -24800,7 +24829,7 @@ after an edit. A yearly cost lands whole in its renewal month, never amortized.`
24800
24829
  month: exports_external.string().optional().describe("Derive one month's costs (YYYY-MM)").meta({ short: "m" }),
24801
24830
  kind: exports_external.enum(COST_KINDS).optional().describe("Only one_off, monthly, or yearly").meta({ short: "k" }),
24802
24831
  project: exports_external.string().optional().describe("Only costs linked to this project (id, URL, or repo)").meta({ short: "p" }),
24803
- overhead: exports_external.boolean().default(false).describe("Only costs with no project links"),
24832
+ overhead: exports_external.boolean().default(false).describe("Only costs no client's project carries"),
24804
24833
  billable: exports_external.boolean().optional().describe("Only billable costs, or --no-billable").meta({ negatable: true }),
24805
24834
  limit: exports_external.coerce.number().int().positive().default(50).describe("Most costs to print"),
24806
24835
  json: exports_external.boolean().default(false).describe("Print as JSON")
@@ -24908,7 +24937,7 @@ var costs = group({
24908
24937
  description: "Manage costs",
24909
24938
  longDescription: `A cost is money the studio spends, recorded once per real-world bill. It links to any
24910
24939
  number of projects \u2014 each carrying the full amount, so per-project figures never sum
24911
- to the studio total \u2014 and no links at all is overhead. Occurrences are always derived
24940
+ to the studio total. A cost no client's project carries is overhead. Occurrences are always derived
24912
24941
  from the record, so \`costs list --month\` answers for any month, past or future.`,
24913
24942
  commands: [create2, list3, get2, update3, end, remove2]
24914
24943
  });
@@ -25153,6 +25182,186 @@ var daemon = group({
25153
25182
  commands: [start, stop, status2, logs]
25154
25183
  });
25155
25184
 
25185
+ // src/commands/files/create.ts
25186
+ import { basename } from "path";
25187
+
25188
+ // src/lib/upload.ts
25189
+ var localFile = async (path, maxBytes) => {
25190
+ const file2 = Bun.file(path);
25191
+ if (!await file2.exists())
25192
+ throw new Error(`No file at ${path}.`);
25193
+ if (file2.size <= 0)
25194
+ throw new Error(`${path} is empty.`);
25195
+ if (file2.size > maxBytes)
25196
+ throw new Error(`${path} is too large (${file2.size} bytes; max ${maxBytes}).`);
25197
+ return file2;
25198
+ };
25199
+ var uploadFile = async (path, file2, uploadUrl) => {
25200
+ const response = await fetch(uploadUrl, { method: "POST", headers: { "Content-Type": file2.type }, body: file2 });
25201
+ if (!response.ok)
25202
+ throw new Error(`Uploading ${path} failed (${response.status}).`);
25203
+ const storageId = exports_external.custom((value) => typeof value === "string");
25204
+ const body = exports_external.object({ storageId }).safeParse(await response.json());
25205
+ if (!body.success)
25206
+ throw new Error("The upload returned no file reference.");
25207
+ return body.data.storageId;
25208
+ };
25209
+
25210
+ // src/commands/files/create.ts
25211
+ var create3 = command({
25212
+ name: "create",
25213
+ description: "Upload one or more files and print their URLs",
25214
+ positionals: {
25215
+ files: exports_external.array(exports_external.string()).min(1).describe("Files to upload")
25216
+ },
25217
+ options: {
25218
+ folder: exports_external.string().optional().describe("Folder to file them under, such as screenshots/2026-09").meta({ short: "f" }),
25219
+ private: exports_external.boolean().default(false).describe("Keep the file to this account: no share URL is issued")
25220
+ },
25221
+ run: async ({ positionals: { files: paths }, options: { folder, private: isPrivate } }) => {
25222
+ const files = await Promise.all(paths.map(async (path) => ({ path, file: await localFile(path, MAX_FILE_BYTES) })));
25223
+ const client3 = await backendClient();
25224
+ const results = await Promise.allSettled(files.map(async ({ path, file: file2 }) => {
25225
+ const uploadUrl = await client3.mutation(api2.files.generateUploadUrl, {});
25226
+ const storageId = await uploadFile(path, file2, uploadUrl);
25227
+ return await client3.action(api2.files.create, {
25228
+ storageId,
25229
+ name: basename(path),
25230
+ folder,
25231
+ visibility: isPrivate ? "private" : "public"
25232
+ });
25233
+ }));
25234
+ for (const result of results) {
25235
+ if (result.status === "fulfilled")
25236
+ console.log(result.value.url ?? result.value.downloadUrl ?? result.value.id);
25237
+ }
25238
+ const failed = results.find((result) => result.status === "rejected");
25239
+ if (failed)
25240
+ throw failed.reason;
25241
+ }
25242
+ });
25243
+
25244
+ // ../../packages/backend/convex/lib/format.ts
25245
+ var formatBytes = (bytes) => bytes < 1024 ? `${bytes} B` : `${Math.round(bytes / 1024)} KB`;
25246
+
25247
+ // src/commands/files/get.ts
25248
+ var get3 = command({
25249
+ name: "get",
25250
+ description: "Show a file's details and links",
25251
+ positionals: {
25252
+ id: exports_external.string().describe("File id or URL")
25253
+ },
25254
+ options: {
25255
+ json: exports_external.boolean().default(false).describe("Print as JSON")
25256
+ },
25257
+ run: async ({ positionals: { id }, options: { json: json2 } }) => {
25258
+ const file2 = await (await backendClient()).query(api2.files.get, { ref: readFileRef(id) });
25259
+ if (json2)
25260
+ return console.log(JSON.stringify(file2, null, 2));
25261
+ console.log(`Name: ${file2.name}`);
25262
+ console.log(`Folder: ${file2.folder ?? "-"}`);
25263
+ console.log(`Access: ${file2.visibility}`);
25264
+ console.log(`Type: ${file2.mimeType}`);
25265
+ console.log(`Size: ${formatBytes(file2.size)}`);
25266
+ console.log(`Created: ${new Date(file2.createdAt).toISOString()}`);
25267
+ console.log(`Id: ${file2.id}`);
25268
+ console.log(`URL: ${file2.url ?? "- (private)"}`);
25269
+ console.log(`Download: ${file2.downloadUrl ?? "-"}`);
25270
+ }
25271
+ });
25272
+
25273
+ // src/commands/files/list.ts
25274
+ var list4 = command({
25275
+ name: "list",
25276
+ description: "List your files, or one folder and everything beneath it",
25277
+ positionals: {
25278
+ folder: exports_external.string().optional().describe("Folder path to list")
25279
+ },
25280
+ options: {
25281
+ json: exports_external.boolean().default(false).describe("Print as JSON")
25282
+ },
25283
+ run: async ({ positionals: { folder }, options: { json: json2 } }) => {
25284
+ const files = await (await backendClient()).query(api2.files.list, { folder });
25285
+ if (json2)
25286
+ return console.log(JSON.stringify(files, null, 2));
25287
+ if (files.length === 0)
25288
+ return console.log(folder === undefined ? "No files yet." : "No files in that folder.");
25289
+ printTable([
25290
+ ["NAME", "FOLDER", "ACCESS", "TYPE", "SIZE", "CREATED", "URL"],
25291
+ ...files.map((file2) => [
25292
+ file2.name,
25293
+ file2.folder ?? "-",
25294
+ file2.visibility,
25295
+ file2.mimeType,
25296
+ formatBytes(file2.size),
25297
+ new Date(file2.createdAt).toISOString().slice(0, 10),
25298
+ file2.url ?? file2.id
25299
+ ])
25300
+ ]);
25301
+ }
25302
+ });
25303
+
25304
+ // src/commands/files/remove.ts
25305
+ var remove3 = command({
25306
+ name: "delete",
25307
+ description: "Delete a file",
25308
+ positionals: {
25309
+ id: exports_external.string().describe("File id or URL")
25310
+ },
25311
+ run: async ({ positionals: { id } }) => {
25312
+ await (await backendClient()).mutation(api2.files.remove, { ref: readFileRef(id) });
25313
+ console.log("Deleted.");
25314
+ }
25315
+ });
25316
+
25317
+ // src/commands/files/rotate-link.ts
25318
+ var rotateLink = command({
25319
+ name: "rotate-link",
25320
+ description: "Reissue a file's share URL; every link handed out so far stops working",
25321
+ positionals: {
25322
+ id: exports_external.string().describe("File id or URL")
25323
+ },
25324
+ run: async ({ positionals: { id } }) => {
25325
+ const file2 = await (await backendClient()).mutation(api2.files.rotateLink, { ref: readFileRef(id) });
25326
+ console.log(file2.url ?? "Rotated; the file is private, so no share URL is issued.");
25327
+ }
25328
+ });
25329
+
25330
+ // src/commands/files/update.ts
25331
+ var update4 = command({
25332
+ name: "update",
25333
+ description: "Rename a file, move it to a folder, or change who can reach it",
25334
+ positionals: {
25335
+ id: exports_external.string().describe("File id or URL")
25336
+ },
25337
+ options: {
25338
+ name: exports_external.string().optional().describe("New file name").meta({ short: "n" }),
25339
+ folder: exports_external.string().nullable().optional().describe("Move the file to this folder, or --no-folder to move it to the root").meta({ short: "f", negatable: true }),
25340
+ visibility: exports_external.enum(FILE_VISIBILITIES).optional().describe("Who can reach the file")
25341
+ },
25342
+ run: async ({ positionals: { id }, options: { name, folder, visibility } }) => {
25343
+ if (name === undefined && folder === undefined && visibility === undefined)
25344
+ throw new Error("Nothing to update. Pass --name, --folder, --no-folder, or --visibility.");
25345
+ const file2 = await (await backendClient()).mutation(api2.files.update, {
25346
+ ref: readFileRef(id),
25347
+ name,
25348
+ folder,
25349
+ visibility
25350
+ });
25351
+ console.log(file2.url ?? "Updated.");
25352
+ }
25353
+ });
25354
+
25355
+ // src/commands/files/index.ts
25356
+ var files = group({
25357
+ name: "files",
25358
+ description: "Share files at a URL",
25359
+ longDescription: `A file is uploaded as it is and reached at a share URL that renders images, video, audio,
25360
+ PDF and plain text in the browser and downloads everything else. Folders are paths such as
25361
+ screenshots/2026-09; a private file has no share URL and is reached from this account only.`,
25362
+ commands: [create3, list4, get3, update4, rotateLink, remove3]
25363
+ });
25364
+
25156
25365
  // src/commands/inbox/list.ts
25157
25366
  var inboxOptions = {
25158
25367
  all: exports_external.boolean().default(false).describe("Include events already marked seen"),
@@ -25252,10 +25461,34 @@ var parseQuotationLineItem = (value) => {
25252
25461
  const section = parts.length === 5 ? parts.pop()?.trim() : undefined;
25253
25462
  return { ...parseLineItem(parts.join("|")), section: section || undefined };
25254
25463
  };
25464
+ var EXPECTED_COST_SYNTAX = 'name|amount|kind[|currency], kind one of one_off, monthly, yearly, e.g. "Hosting|2500|monthly|USD"; currency defaults to EGP';
25465
+ var RECURRING_FEE_SYNTAX = 'description|amount|cadence[|currency], cadence monthly or yearly, e.g. "Maintenance|50000|monthly|USD"; currency defaults to EGP';
25466
+ var parseCommitment = (value, syntax, kinds) => {
25467
+ const parts = value.split("|");
25468
+ if (parts.length !== 3 && parts.length !== 4)
25469
+ throw new Error(`Entries read as ${syntax}; got "${value}".`);
25470
+ const [label = "", amountPart = "", kindPart = "", currency = ""] = parts;
25471
+ const amount = Number(amountPart.trim());
25472
+ if (!amountPart.trim() || !Number.isSafeInteger(amount)) {
25473
+ throw new Error(`Amounts must be integer minor units (10000 = 100.00), got "${amountPart.trim()}".`);
25474
+ }
25475
+ const kind = kinds.find((candidate) => candidate === kindPart.trim());
25476
+ if (kind === undefined)
25477
+ throw new Error(`Expected one of ${kinds.join(", ")}, got "${kindPart.trim()}".`);
25478
+ return { label: label.trim(), amount, kind, currency: parseLineCurrency(currency) };
25479
+ };
25480
+ var parseExpectedCost = (value) => {
25481
+ const { label, kind, ...rest } = parseCommitment(value, EXPECTED_COST_SYNTAX, COST_KINDS);
25482
+ return { name: label, kind, ...rest };
25483
+ };
25484
+ var parseRecurringFee = (value) => {
25485
+ const { label, kind, ...rest } = parseCommitment(value, RECURRING_FEE_SYNTAX, RECURRING_CHARGE_CADENCES);
25486
+ return { description: label, cadence: kind, ...rest };
25487
+ };
25255
25488
  var formatPerCurrency = (entries) => entries.length === 0 ? "-" : entries.map(({ amount, currency }) => formatMinorAmount(amount, currency)).join(" + ");
25256
25489
 
25257
25490
  // src/commands/invoices/create.ts
25258
- var create3 = command({
25491
+ var create4 = command({
25259
25492
  name: "create",
25260
25493
  description: "Create a draft invoice for a client and print its id",
25261
25494
  positionals: {
@@ -25308,7 +25541,7 @@ var email3 = command({
25308
25541
  });
25309
25542
 
25310
25543
  // src/commands/invoices/get.ts
25311
- var get3 = command({
25544
+ var get4 = command({
25312
25545
  name: "get",
25313
25546
  description: "Show an invoice: document, totals, and payments",
25314
25547
  positionals: {
@@ -25398,7 +25631,7 @@ var link = command({
25398
25631
  });
25399
25632
 
25400
25633
  // src/commands/invoices/list.ts
25401
- var list4 = command({
25634
+ var list5 = command({
25402
25635
  name: "list",
25403
25636
  description: "List your invoices, newest first",
25404
25637
  options: {
@@ -25468,7 +25701,7 @@ var add2 = command({
25468
25701
  });
25469
25702
 
25470
25703
  // src/commands/invoices/payments/list.ts
25471
- var list5 = command({
25704
+ var list6 = command({
25472
25705
  name: "list",
25473
25706
  description: "List the payments recorded on an invoice, oldest first",
25474
25707
  positionals: {
@@ -25496,7 +25729,7 @@ var list5 = command({
25496
25729
  });
25497
25730
 
25498
25731
  // src/commands/invoices/payments/remove.ts
25499
- var remove3 = command({
25732
+ var remove4 = command({
25500
25733
  name: "remove",
25501
25734
  description: "Remove a recorded payment (unflips paid when the invoice is no longer covered)",
25502
25735
  positionals: {
@@ -25516,11 +25749,11 @@ var payments = group({
25516
25749
  longDescription: `A payment is money received against a sent invoice, in one currency. Paid flips
25517
25750
  automatically once every currency's total is covered; overpayment is rejected,
25518
25751
  and forgiving a shortfall is a payment with a note.`,
25519
- commands: [add2, list5, remove3]
25752
+ commands: [add2, list6, remove4]
25520
25753
  });
25521
25754
 
25522
25755
  // src/commands/invoices/rotate-link.ts
25523
- var rotateLink = command({
25756
+ var rotateLink2 = command({
25524
25757
  name: "rotate-link",
25525
25758
  description: "Reissue the invoice's public link, which stops every link already shared or emailed working",
25526
25759
  positionals: {
@@ -25534,7 +25767,7 @@ var rotateLink = command({
25534
25767
  });
25535
25768
 
25536
25769
  // src/commands/invoices/update.ts
25537
- var update4 = command({
25770
+ var update5 = command({
25538
25771
  name: "update",
25539
25772
  description: "Update a draft invoice (sent invoices are frozen)",
25540
25773
  longDescription: "An omitted flag leaves its field unchanged. Passed --line flags replace the whole line list; --no-notes and --no-due clear their fields.",
@@ -25582,52 +25815,29 @@ var invoices = group({
25582
25815
  longDescription: `An invoice is a draft until issued: issuing assigns its gapless number, freezes the
25583
25816
  document, and snapshots the client's billing details. Sent invoices are immutable \u2014
25584
25817
  the fix path is void + duplicate into a fresh draft. Drafts delete outright.`,
25585
- commands: [create3, list4, get3, update4, issue2, email3, voidInvoice, duplicate, link, rotateLink, payments]
25818
+ commands: [create4, list5, get4, update5, issue2, email3, voidInvoice, duplicate, link, rotateLink2, payments]
25586
25819
  });
25587
25820
 
25588
25821
  // src/lib/attachments.ts
25589
- import { basename } from "path";
25822
+ import { basename as basename2 } from "path";
25590
25823
  var attachFiles = async (client3, issueId, paths) => {
25591
25824
  if (paths.length === 0)
25592
25825
  return [];
25593
- const files = paths.map((path) => ({ path, file: Bun.file(path) }));
25594
- for (const { path, file: file2 } of files) {
25595
- if (!await file2.exists())
25596
- throw new Error(`No file at ${path}.`);
25597
- if (file2.size <= 0)
25598
- throw new Error(`${path} is empty; an attachment cannot be.`);
25599
- if (file2.size > MAX_ISSUE_ATTACHMENT_BYTES) {
25600
- throw new Error(`${path} is too large (${file2.size} bytes; max ${MAX_ISSUE_ATTACHMENT_BYTES}).`);
25601
- }
25602
- }
25603
- const created = new Set;
25604
- try {
25605
- for (const { path, file: file2 } of files) {
25606
- const uploadUrl = await client3.mutation(api2.issueAttachments.generateUploadUrl, { issueId });
25607
- const response = await fetch(uploadUrl, {
25608
- method: "POST",
25609
- headers: { "Content-Type": file2.type },
25610
- body: file2
25611
- });
25612
- if (!response.ok)
25613
- throw new Error(`Uploading ${path} failed (${response.status}).`);
25614
- const storageId = exports_external.custom((value) => typeof value === "string");
25615
- const body = exports_external.object({ storageId }).safeParse(await response.json());
25616
- if (!body.success)
25617
- throw new Error("The upload returned no file reference.");
25618
- created.add(await client3.action(api2.issueAttachments.create, {
25619
- issueId,
25620
- storageId: body.data.storageId,
25621
- name: basename(path)
25622
- }));
25623
- }
25624
- } catch (error51) {
25826
+ const files2 = await Promise.all(paths.map(async (path) => ({ path, file: await localFile(path, MAX_ISSUE_ATTACHMENT_BYTES) })));
25827
+ const results = await Promise.allSettled(files2.map(async ({ path, file: file2 }) => {
25828
+ const uploadUrl = await client3.mutation(api2.issueAttachments.generateUploadUrl, { issueId });
25829
+ const storageId = await uploadFile(path, file2, uploadUrl);
25830
+ return await client3.action(api2.issueAttachments.create, { issueId, storageId, name: basename2(path) });
25831
+ }));
25832
+ const created = new Set(results.flatMap((result) => result.status === "fulfilled" ? [result.value] : []));
25833
+ const failed = results.find((result) => result.status === "rejected");
25834
+ if (failed) {
25625
25835
  for (const id of created) {
25626
25836
  await client3.mutation(api2.issueAttachments.remove, { id }).catch(() => {
25627
25837
  console.error("An attachment from this failed batch could not be removed; check the dashboard.");
25628
25838
  });
25629
25839
  }
25630
- throw error51;
25840
+ throw failed.reason;
25631
25841
  }
25632
25842
  const attachments = await client3.query(api2.issueAttachments.list, { issueId });
25633
25843
  return attachments.filter((attachment) => created.has(attachment._id));
@@ -25676,7 +25886,7 @@ var agentConfigurationLine = (id) => {
25676
25886
  };
25677
25887
 
25678
25888
  // src/commands/issues/create.ts
25679
- var create4 = command({
25889
+ var create5 = command({
25680
25890
  name: "create",
25681
25891
  description: "Create an issue and print its identifier",
25682
25892
  positionals: {
@@ -25739,7 +25949,7 @@ var printTasks = (tasks) => {
25739
25949
  };
25740
25950
 
25741
25951
  // src/commands/issues/get.ts
25742
- var printIssue = ({ relations, attachments, tasks, feed, milestone, ...issue3 }) => {
25952
+ var printIssue = ({ relations, attachments, pages, tasks, feed, milestone, ...issue3 }) => {
25743
25953
  const project = issue3.project;
25744
25954
  console.log(`${issue3.identifier} ${issue3.title}`);
25745
25955
  console.log(`Status: ${issue3.status} \xB7 Priority: ${issue3.priority} \xB7 Disposition: ${issue3.disposition}`);
@@ -25778,6 +25988,12 @@ var printIssue = ({ relations, attachments, tasks, feed, milestone, ...issue3 })
25778
25988
  console.log(` ${attachment.name} \u2014 ${attachment.url ?? "unavailable"}`);
25779
25989
  }
25780
25990
  }
25991
+ if (pages.length > 0) {
25992
+ console.log("Pages:");
25993
+ for (const page of pages) {
25994
+ console.log(` ${page.title} (${page.style}, ${page.visibility}) \u2014 ${page.url}`);
25995
+ }
25996
+ }
25781
25997
  printTasks(tasks);
25782
25998
  if (issue3.descriptionMarkdown)
25783
25999
  console.log(`
@@ -25800,7 +26016,7 @@ Feed:`);
25800
26016
  }
25801
26017
  }
25802
26018
  };
25803
- var get4 = command({
26019
+ var get5 = command({
25804
26020
  name: "get",
25805
26021
  description: "Show one or more issues: properties, description, and feed",
25806
26022
  positionals: {
@@ -25819,10 +26035,11 @@ var get4 = command({
25819
26035
  throw new Error(`No issues match ${missing.join(", ")}.`);
25820
26036
  const details = found.flatMap((detail) => detail ? [detail] : []);
25821
26037
  if (json2) {
25822
- const objects = details.map(({ relations, attachments, tasks, feed, milestone: _, ...issue3 }) => ({
26038
+ const objects = details.map(({ relations, attachments, pages, tasks, feed, milestone: _, ...issue3 }) => ({
25823
26039
  ...issue3,
25824
26040
  relations,
25825
26041
  attachments,
26042
+ pages,
25826
26043
  tasks,
25827
26044
  feed
25828
26045
  }));
@@ -25837,7 +26054,7 @@ var get4 = command({
25837
26054
  });
25838
26055
 
25839
26056
  // src/commands/issues/labels/create.ts
25840
- var create5 = command({
26057
+ var create6 = command({
25841
26058
  name: "create",
25842
26059
  description: "Create an issue label and print its id",
25843
26060
  positionals: {
@@ -25854,7 +26071,7 @@ var create5 = command({
25854
26071
  });
25855
26072
 
25856
26073
  // src/commands/issues/labels/list.ts
25857
- var list6 = command({
26074
+ var list7 = command({
25858
26075
  name: "list",
25859
26076
  description: "List issue labels alphabetically",
25860
26077
  options: {
@@ -25871,7 +26088,7 @@ var list6 = command({
25871
26088
  });
25872
26089
 
25873
26090
  // src/commands/issues/labels/remove.ts
25874
- var remove4 = command({
26091
+ var remove5 = command({
25875
26092
  name: "delete",
25876
26093
  description: "Delete an issue label and remove it from every issue",
25877
26094
  positionals: {
@@ -25885,7 +26102,7 @@ var remove4 = command({
25885
26102
  });
25886
26103
 
25887
26104
  // src/commands/issues/labels/update.ts
25888
- var update5 = command({
26105
+ var update6 = command({
25889
26106
  name: "update",
25890
26107
  description: "Rename or recolor an issue label",
25891
26108
  positionals: {
@@ -25908,11 +26125,54 @@ var update5 = command({
25908
26125
  var labels = group({
25909
26126
  name: "labels",
25910
26127
  description: "Manage issue labels",
25911
- commands: [create5, list6, update5, remove4]
26128
+ commands: [create6, list7, update6, remove5]
26129
+ });
26130
+
26131
+ // src/commands/issues/link-pr.ts
26132
+ var linkPr = command({
26133
+ name: "link-pr",
26134
+ description: "Link a pull request to an issue by hand",
26135
+ longDescription: `For a pull request the webhook cannot match on its own: work that started before
26136
+ its issue existed, so the branch never carried the identifier, or a second issue the
26137
+ same pull request lands. From here reviews and the merge move the issue as usual.
26138
+ --claim applies what opening the pull request would have: unstarted work moves to
26139
+ in_progress; any other status is kept.`,
26140
+ positionals: {
26141
+ id: exports_external.string().describe("Issue identifier, number, or URL"),
26142
+ url: exports_external.string().describe("Pull request URL")
26143
+ },
26144
+ options: {
26145
+ claim: exports_external.boolean().default(false).describe("Move backlog or todo work to in_progress along with the link")
26146
+ },
26147
+ run: async ({ positionals: { id, url: url2 }, options: { claim: claim2 } }) => {
26148
+ const client3 = await backendClient();
26149
+ const { identifier, linked, status: status3 } = await client3.mutation(api2.issues.linkPr, {
26150
+ id: readIssueRef(id),
26151
+ prUrl: url2,
26152
+ ...claim2 ? { claim: claim2 } : {}
26153
+ });
26154
+ console.log(`${linked ? "Linked" : "Already linked"} ${url2} to ${identifier}: ${status3}`);
26155
+ }
25912
26156
  });
25913
26157
 
25914
26158
  // src/commands/issues/list.ts
25915
- var list7 = command({
26159
+ var resolveScope = async (client3, { project, here, milestone }) => {
26160
+ if (milestone) {
26161
+ if (!project && !here)
26162
+ throw new Error("A milestone filter needs --project or --here.");
26163
+ const ref = project ? readProjectRef(project) : await currentRepoKey();
26164
+ if (!ref)
26165
+ throw new Error("No repository here: not a git checkout with an origin remote.");
26166
+ const resolved = await client3.query(api2.milestones.resolve, { project: ref, name: milestone });
26167
+ return { projectId: resolved.projectId, milestoneId: resolved._id };
26168
+ }
26169
+ if (project)
26170
+ return { projectId: (await resolveProject(client3, project))._id };
26171
+ if (here)
26172
+ return { projectId: (await repoProject(client3))._id };
26173
+ return;
26174
+ };
26175
+ var list8 = command({
25916
26176
  name: "list",
25917
26177
  description: "List open issues, most recently updated first",
25918
26178
  options: {
@@ -25932,10 +26192,7 @@ var list7 = command({
25932
26192
  if (options.project && options.here)
25933
26193
  throw new Error("Pass --project or --here, not both.");
25934
26194
  const client3 = await backendClient();
25935
- const project = options.project ? await resolveProject(client3, options.project) : options.here ? await repoProject(client3) : undefined;
25936
- if (options.milestone && !project)
25937
- throw new Error("A milestone filter needs --project or --here.");
25938
- const milestone = project && options.milestone ? await resolveMilestone(client3, project._id, options.milestone) : undefined;
26195
+ const scope = await resolveScope(client3, options);
25939
26196
  const result = await client3.query(api2.issueViews.query, {
25940
26197
  source: {
25941
26198
  type: "custom",
@@ -25949,8 +26206,8 @@ var list7 = command({
25949
26206
  statuses: options.status ? [options.status] : options.all ? undefined : ISSUE_STATUSES.filter(issueIsOpen),
25950
26207
  priorities: options.priority ? [options.priority] : undefined,
25951
26208
  dispositions: options.disposition ? [options.disposition] : undefined,
25952
- projectIds: project ? [project._id] : undefined,
25953
- milestoneIds: milestone ? [milestone._id] : undefined,
26209
+ projectIds: scope ? [scope.projectId] : undefined,
26210
+ milestoneIds: scope?.milestoneId ? [scope.milestoneId] : undefined,
25954
26211
  dueDate: options.due
25955
26212
  }
25956
26213
  }
@@ -26031,7 +26288,7 @@ var relate = command({
26031
26288
  });
26032
26289
 
26033
26290
  // src/commands/issues/remove.ts
26034
- var remove5 = command({
26291
+ var remove6 = command({
26035
26292
  name: "delete",
26036
26293
  description: "Delete an issue permanently (sub-issues survive as top-level issues)",
26037
26294
  positionals: {
@@ -26267,9 +26524,9 @@ address several rows even as removals renumber it.`,
26267
26524
  remove: position.optional().describe("Drop a task by its number (repeatable)"),
26268
26525
  json: exports_external.boolean().default(false).describe("Print as JSON")
26269
26526
  },
26270
- run: async ({ positionals: { id }, options: { add: add3, check: check2, uncheck, convert, remove: remove6, json: json2 } }) => {
26527
+ run: async ({ positionals: { id }, options: { add: add3, check: check2, uncheck, convert, remove: remove7, json: json2 } }) => {
26271
26528
  const client3 = await backendClient();
26272
- const wrote = [add3, check2, uncheck, convert, remove6].some((values) => values !== undefined);
26529
+ const wrote = [add3, check2, uncheck, convert, remove7].some((values) => values !== undefined);
26273
26530
  if (wrote) {
26274
26531
  const { identifier, converted, tasks: tasks3 } = await client3.mutation(api2.issueTasks.edit, {
26275
26532
  issue: readIssueRef(id),
@@ -26277,7 +26534,7 @@ address several rows even as removals renumber it.`,
26277
26534
  ...check2 !== undefined ? { check: check2 } : {},
26278
26535
  ...uncheck !== undefined ? { uncheck } : {},
26279
26536
  ...convert !== undefined ? { convert } : {},
26280
- ...remove6 !== undefined ? { remove: remove6 } : {}
26537
+ ...remove7 !== undefined ? { remove: remove7 } : {}
26281
26538
  });
26282
26539
  if (!json2) {
26283
26540
  for (const created of converted) {
@@ -26321,7 +26578,7 @@ var unrelate = command({
26321
26578
  });
26322
26579
 
26323
26580
  // src/commands/issues/update.ts
26324
- var update6 = command({
26581
+ var update7 = command({
26325
26582
  name: "update",
26326
26583
  description: "Rewrite an issue's title or description, or attach files",
26327
26584
  positionals: {
@@ -26360,16 +26617,16 @@ todo, in_progress, ready_for_review, in_review, changes_requested, approved, don
26360
26617
  canceled), priority, and disposition (who acts next). The workflow commands \u2014 start,
26361
26618
  submit, start-review, submit-review \u2014 and the linked pull request move the status;
26362
26619
  each refuses an issue outside its own status, and \`set -s\` is for corrections.
26363
- The repo link is never set by hand: work on a branch whose name contains the issue
26364
- identifier and the branch, pull request, and status track automatically \u2014 a change
26365
- request moves the issue to changes_requested, an approval to approved (one
26366
- outstanding change request outweighs any number of approvals), and a merge lands
26367
- it as done.`,
26620
+ Work on a branch whose name contains the issue identifier and the branch, pull
26621
+ request, and status track automatically \u2014 a change request moves the issue to
26622
+ changes_requested, an approval to approved (one outstanding change request outweighs
26623
+ any number of approvals), and a merge lands it as done. link-pr covers the pull
26624
+ request the branch name does not point at; from there the same tracking applies.`,
26368
26625
  commands: [
26369
- create4,
26370
- list7,
26371
- get4,
26372
- update6,
26626
+ create5,
26627
+ list8,
26628
+ get5,
26629
+ update7,
26373
26630
  set2,
26374
26631
  labels,
26375
26632
  route,
@@ -26378,16 +26635,20 @@ it as done.`,
26378
26635
  startReview,
26379
26636
  submitReview,
26380
26637
  markAddressed,
26638
+ linkPr,
26381
26639
  tasks,
26382
26640
  relate,
26383
26641
  unrelate,
26384
26642
  comment,
26385
- remove5
26643
+ remove6
26386
26644
  ]
26387
26645
  });
26388
26646
 
26647
+ // ../../packages/backend/convex/lib/object.ts
26648
+ var keys = (object2) => Object.keys(object2);
26649
+
26389
26650
  // src/lib/group.ts
26390
- import { basename as basename2 } from "path";
26651
+ import { basename as basename3 } from "path";
26391
26652
  var git = async (...args) => {
26392
26653
  const proc = Bun.spawn(["git", ...args], { stdout: "pipe", stderr: "ignore" });
26393
26654
  const [output, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
@@ -26399,12 +26660,12 @@ var detectRepoGroup = async () => {
26399
26660
  if (remote)
26400
26661
  return repoNameFromRemote(remote);
26401
26662
  const root = await git("rev-parse", "--show-toplevel");
26402
- return root ? basename2(root) : undefined;
26663
+ return root ? basename3(root) : undefined;
26403
26664
  };
26404
26665
  var groupForCreate = async (group2) => group2 === null ? undefined : group2 ?? await detectRepoGroup();
26405
26666
 
26406
26667
  // src/commands/pages/create.ts
26407
- var create6 = command({
26668
+ var create7 = command({
26408
26669
  name: "create",
26409
26670
  description: "Publish a page and print its URL",
26410
26671
  positionals: {
@@ -26413,27 +26674,28 @@ var create6 = command({
26413
26674
  options: {
26414
26675
  title: exports_external.string().describe("Page title").meta({ short: "t" }),
26415
26676
  group: exports_external.string().nullable().optional().describe("Group to list the page under (default: the current repo)").meta({ short: "g", negatable: true }),
26416
- raw: exports_external.boolean().default(false).describe("Serve a full document verbatim (may run JS)"),
26417
- private: exports_external.boolean().default(false).describe("Require the publishing account to view the page")
26677
+ style: exports_external.enum(keys(PAGE_STYLES)).default("basic").describe(`Style to render in \u2014 ${describeChoices(PAGE_STYLES)}`),
26678
+ private: exports_external.boolean().default(false).describe("Require the publishing account to view the page"),
26679
+ issue: exports_external.string().optional().describe("Link the page to an issue (KAI-N, number, or URL)")
26418
26680
  },
26419
- run: async ({ positionals: { file: file2 }, options: { title, group: group2, raw, private: isPrivate } }) => {
26681
+ run: async ({ positionals: { file: file2 }, options: { title, group: group2, style, private: isPrivate, issue: issue3 } }) => {
26420
26682
  const newGroup = await groupForCreate(group2);
26421
26683
  const html = await readBody(file2);
26422
- const mode = raw ? "raw" : "themed";
26423
26684
  const visibility = isPrivate ? "private" : "public";
26424
26685
  const { url: url2 } = await (await backendClient()).action(api2.pages.create, {
26425
26686
  title,
26426
26687
  group: newGroup,
26427
26688
  html,
26428
- mode,
26429
- visibility
26689
+ style,
26690
+ visibility,
26691
+ issue: issue3 === undefined ? undefined : readIssueRef(issue3)
26430
26692
  });
26431
26693
  console.log(url2);
26432
26694
  }
26433
26695
  });
26434
26696
 
26435
26697
  // src/commands/pages/get.ts
26436
- var get5 = command({
26698
+ var get6 = command({
26437
26699
  name: "get",
26438
26700
  description: "Print a page's HTML",
26439
26701
  positionals: {
@@ -26452,11 +26714,8 @@ var get5 = command({
26452
26714
  }
26453
26715
  });
26454
26716
 
26455
- // ../../packages/backend/convex/lib/format.ts
26456
- var formatBytes = (bytes) => bytes < 1024 ? `${bytes} B` : `${Math.round(bytes / 1024)} KB`;
26457
-
26458
26717
  // src/commands/pages/list.ts
26459
- var list8 = command({
26718
+ var list9 = command({
26460
26719
  name: "list",
26461
26720
  description: "List your published pages",
26462
26721
  options: {
@@ -26470,12 +26729,12 @@ var list8 = command({
26470
26729
  if (pages.length === 0)
26471
26730
  return console.log(group2 === undefined ? "No pages yet." : "No pages in that group.");
26472
26731
  printTable([
26473
- ["TITLE", "GROUP", "ACCESS", "MODE", "SIZE", "VERSIONS", "CREATED", "URL"],
26732
+ ["TITLE", "GROUP", "ACCESS", "STYLE", "SIZE", "VERSIONS", "CREATED", "URL"],
26474
26733
  ...pages.map((page) => [
26475
26734
  page.title,
26476
26735
  page.group ?? "-",
26477
26736
  page.visibility,
26478
- page.mode,
26737
+ page.style,
26479
26738
  formatBytes(page.size),
26480
26739
  page.versions === 0 ? "-" : String(page.versions),
26481
26740
  new Date(page.createdAt).toISOString().slice(0, 10),
@@ -26486,7 +26745,7 @@ var list8 = command({
26486
26745
  });
26487
26746
 
26488
26747
  // src/commands/pages/remove.ts
26489
- var remove6 = command({
26748
+ var remove7 = command({
26490
26749
  name: "delete",
26491
26750
  description: "Delete a page",
26492
26751
  positionals: {
@@ -26498,6 +26757,14 @@ var remove6 = command({
26498
26757
  }
26499
26758
  });
26500
26759
 
26760
+ // src/lib/pages.ts
26761
+ var warnPruned = (pruned) => {
26762
+ if (pruned.length === 0)
26763
+ return;
26764
+ const versions2 = pruned.length === 1 ? `version ${pruned[0]}` : `versions ${pruned.join(", ")}`;
26765
+ console.error(`Pruned ${versions2}; the history keeps the newest ${MAX_PAGE_VERSIONS}.`);
26766
+ };
26767
+
26501
26768
  // src/commands/pages/revert.ts
26502
26769
  var revert = command({
26503
26770
  name: "revert",
@@ -26507,15 +26774,16 @@ var revert = command({
26507
26774
  version: exports_external.coerce.number().int().positive().describe("Version number from `kds pages versions`")
26508
26775
  },
26509
26776
  run: async ({ positionals: { id, version: version4 } }) => {
26510
- const { url: url2 } = await (await backendClient()).mutation(api2.pages.revert, { id: readPageId(id), version: version4 });
26777
+ const { url: url2, pruned } = await (await backendClient()).mutation(api2.pages.revert, { id: readPageId(id), version: version4 });
26778
+ warnPruned(pruned);
26511
26779
  console.log(url2);
26512
26780
  }
26513
26781
  });
26514
26782
 
26515
26783
  // src/commands/pages/update.ts
26516
- var update7 = command({
26784
+ var update8 = command({
26517
26785
  name: "update",
26518
- description: "Replace a page's HTML, title, group, mode, or visibility",
26786
+ description: "Replace a page's HTML, title, group, style, or visibility",
26519
26787
  positionals: {
26520
26788
  id: exports_external.string().describe("Page id or URL"),
26521
26789
  file: exports_external.string().optional().describe("HTML file, or - for stdin")
@@ -26523,21 +26791,24 @@ var update7 = command({
26523
26791
  options: {
26524
26792
  title: exports_external.string().optional().describe("New page title").meta({ short: "t" }),
26525
26793
  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 }),
26526
- mode: exports_external.enum(PAGE_MODES).optional().describe("New serving mode"),
26527
- visibility: exports_external.enum(PAGE_VISIBILITIES).optional().describe("Who can view the page")
26794
+ style: exports_external.enum(keys(PAGE_STYLES)).optional().describe(`New style \u2014 ${describeChoices(PAGE_STYLES)}`),
26795
+ visibility: exports_external.enum(PAGE_VISIBILITIES).optional().describe("Who can view the page"),
26796
+ 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 })
26528
26797
  },
26529
- run: async ({ positionals: { id, file: file2 }, options: { title, group: group2, mode, visibility } }) => {
26530
- if (!file2 && title === undefined && group2 === undefined && mode === undefined && visibility === undefined)
26531
- throw new Error("Nothing to update. Pass a file, --title, --group, --no-group, --mode, or --visibility.");
26798
+ run: async ({ positionals: { id, file: file2 }, options: { title, group: group2, style, visibility, issue: issue3 } }) => {
26799
+ if (!file2 && title === undefined && group2 === undefined && style === undefined && visibility === undefined && issue3 === undefined)
26800
+ throw new Error("Nothing to update. Pass a file, --title, --group, --no-group, --style, --visibility, --issue, or --no-issue.");
26532
26801
  const html = file2 ? await readBody(file2) : undefined;
26533
- const { url: url2 } = await (await backendClient()).action(api2.pages.update, {
26802
+ const { url: url2, pruned } = await (await backendClient()).action(api2.pages.update, {
26534
26803
  id: readPageId(id),
26535
26804
  html,
26536
26805
  title,
26537
26806
  group: group2,
26538
- mode,
26539
- visibility
26807
+ style,
26808
+ visibility,
26809
+ issue: typeof issue3 === "string" ? readIssueRef(issue3) : issue3
26540
26810
  });
26811
+ warnPruned(pruned);
26541
26812
  console.log(url2);
26542
26813
  }
26543
26814
  });
@@ -26559,10 +26830,10 @@ var versions2 = command({
26559
26830
  if (archived.length === 0)
26560
26831
  return console.log("No previous versions.");
26561
26832
  printTable([
26562
- ["VERSION", "MODE", "SIZE", "ARCHIVED"],
26833
+ ["VERSION", "STYLE", "SIZE", "ARCHIVED"],
26563
26834
  ...archived.map((version4) => [
26564
26835
  String(version4.version),
26565
- version4.mode,
26836
+ version4.style,
26566
26837
  formatBytes(version4.size),
26567
26838
  new Date(version4.archivedAt).toISOString().slice(0, 10)
26568
26839
  ])
@@ -26574,11 +26845,11 @@ var versions2 = command({
26574
26845
  var pages = group({
26575
26846
  name: "pages",
26576
26847
  description: "Publish HTML documents to the web",
26577
- longDescription: `Themed pages (the default) render inside the site chrome under a strict CSP: only
26578
- the site theme's styles apply \u2014 inline style attributes and author scripts are
26579
- dropped. A self-contained document with its own CSS or JS needs raw mode, which
26580
- serves it verbatim. A page keeps its URL across updates and reverts.`,
26581
- commands: [create6, list8, get5, update7, versions2, revert, remove6]
26848
+ longDescription: `A page is an HTML fragment rendered in one of the site's styles:
26849
+ ${describeChoices(PAGE_STYLES, `
26850
+ `)}
26851
+ Basic is the default. A page keeps its URL across updates and reverts.`,
26852
+ commands: [create7, list9, get6, update8, versions2, revert, remove7]
26582
26853
  });
26583
26854
 
26584
26855
  // src/commands/project.ts
@@ -26611,7 +26882,7 @@ var project = command({
26611
26882
  });
26612
26883
 
26613
26884
  // src/commands/projects/create.ts
26614
- var create7 = command({
26885
+ var create8 = command({
26615
26886
  name: "create",
26616
26887
  description: "Create a project and print its id",
26617
26888
  positionals: {
@@ -26643,7 +26914,7 @@ var create7 = command({
26643
26914
  });
26644
26915
 
26645
26916
  // src/commands/projects/get.ts
26646
- var get6 = command({
26917
+ var get7 = command({
26647
26918
  name: "get",
26648
26919
  description: "Show a project: properties, description, and milestones",
26649
26920
  positionals: {
@@ -26687,7 +26958,7 @@ Milestones:`);
26687
26958
  });
26688
26959
 
26689
26960
  // src/commands/projects/list.ts
26690
- var list9 = command({
26961
+ var list10 = command({
26691
26962
  name: "list",
26692
26963
  description: "List your projects, most recently updated first",
26693
26964
  options: {
@@ -26730,7 +27001,7 @@ More projects exist; raise --limit past ${limit}.`);
26730
27001
  });
26731
27002
 
26732
27003
  // src/commands/projects/remove.ts
26733
- var remove7 = command({
27004
+ var remove8 = command({
26734
27005
  name: "delete",
26735
27006
  description: "Delete a project and its milestones (its issues survive, projectless)",
26736
27007
  positionals: {
@@ -26775,7 +27046,7 @@ var set3 = command({
26775
27046
  });
26776
27047
 
26777
27048
  // src/commands/projects/update.ts
26778
- var update8 = command({
27049
+ var update9 = command({
26779
27050
  name: "update",
26780
27051
  description: "Rewrite a project's name, summary, or description",
26781
27052
  positionals: {
@@ -26806,7 +27077,7 @@ var projects = group({
26806
27077
  description: "Manage projects",
26807
27078
  longDescription: `Connect a repository (set --repo) to make --here resolve in that checkout and to
26808
27079
  let issue branches and pull requests link automatically.`,
26809
- commands: [create7, list9, get6, update8, set3, remove7]
27080
+ commands: [create8, list10, get7, update9, set3, remove8]
26810
27081
  });
26811
27082
 
26812
27083
  // src/commands/quotations/accept.ts
@@ -26865,7 +27136,7 @@ Nothing blocks over- or under-invoicing; left to bill is advisory.`,
26865
27136
  });
26866
27137
 
26867
27138
  // src/commands/quotations/create.ts
26868
- var create8 = command({
27139
+ var create9 = command({
26869
27140
  name: "create",
26870
27141
  description: "Create a draft quotation for a client and print its id",
26871
27142
  positionals: {
@@ -26873,6 +27144,8 @@ var create8 = command({
26873
27144
  },
26874
27145
  options: {
26875
27146
  line: exports_external.array(exports_external.string()).optional().describe(`Line item as ${QUOTATION_LINE_ITEM_SYNTAX}; repeatable`).meta({ short: "l" }),
27147
+ cost: exports_external.array(exports_external.string()).optional().describe(`Expected cost as ${EXPECTED_COST_SYNTAX}; repeatable`),
27148
+ fee: exports_external.array(exports_external.string()).optional().describe(`Recurring fee as ${RECURRING_FEE_SYNTAX}; repeatable`),
26876
27149
  project: exports_external.string().optional().describe("Project id or URL to link (must belong to the same client)"),
26877
27150
  notes: exports_external.string().optional().describe("Free text rendered at the document foot"),
26878
27151
  validUntil: exports_external.string().nullable().optional().describe("Last valid day (YYYY-MM-DD); defaults from the profile's validity days, --no-valid-until never expires").meta({ negatable: true })
@@ -26883,6 +27156,8 @@ var create8 = command({
26883
27156
  clientId: readClientRef(ref),
26884
27157
  projectId: options.project === undefined ? undefined : readProjectRef(options.project),
26885
27158
  lineItems: options.line?.map(parseQuotationLineItem),
27159
+ expectedCosts: options.cost?.map(parseExpectedCost),
27160
+ recurringFees: options.fee?.map(parseRecurringFee),
26886
27161
  notes: options.notes,
26887
27162
  validUntil: options.validUntil === null ? "" : options.validUntil,
26888
27163
  today: localToday()
@@ -26919,7 +27194,7 @@ var email4 = command({
26919
27194
  });
26920
27195
 
26921
27196
  // src/commands/quotations/get.ts
26922
- var get7 = command({
27197
+ var get8 = command({
26923
27198
  name: "get",
26924
27199
  description: "Show a quotation: document, totals, and \u2014 once accepted \u2014 its billing progress",
26925
27200
  positionals: {
@@ -26965,6 +27240,30 @@ var get7 = command({
26965
27240
  }
26966
27241
  console.log(`
26967
27242
  Total: ${formatPerCurrency(quotation.totals.map(({ currency, total }) => ({ currency, amount: total })))}`);
27243
+ if (quotation.expectedCosts.length > 0) {
27244
+ console.log(`
27245
+ Expected costs (estimates, billed at cost):`);
27246
+ printTable([
27247
+ ["NAME", "KIND", "AMOUNT"],
27248
+ ...quotation.expectedCosts.map((cost) => [
27249
+ cost.name,
27250
+ COST_KIND_LABELS[cost.kind],
27251
+ formatMinorAmount(cost.amount, cost.currency)
27252
+ ])
27253
+ ]);
27254
+ }
27255
+ if (quotation.recurringFees.length > 0) {
27256
+ console.log(`
27257
+ Recurring fees:`);
27258
+ printTable([
27259
+ ["DESCRIPTION", "CADENCE", "AMOUNT"],
27260
+ ...quotation.recurringFees.map((fee) => [
27261
+ fee.description,
27262
+ RECURRING_CHARGE_CADENCE_LABELS[fee.cadence],
27263
+ formatMinorAmount(fee.amount, fee.currency)
27264
+ ])
27265
+ ]);
27266
+ }
26968
27267
  if (quotation.billing) {
26969
27268
  console.log(`Invoiced: ${formatPerCurrency(quotation.billing.progress.map(({ currency, invoiced }) => ({ currency, amount: invoiced })))}`);
26970
27269
  const leftToBill = quotation.billing.progress.filter(({ remaining }) => remaining > 0);
@@ -27026,7 +27325,7 @@ var link2 = command({
27026
27325
  });
27027
27326
 
27028
27327
  // src/commands/quotations/list.ts
27029
- var list10 = command({
27328
+ var list11 = command({
27030
27329
  name: "list",
27031
27330
  description: "List your quotations, newest first (expired is derived, never stored)",
27032
27331
  options: {
@@ -27090,7 +27389,7 @@ var reject2 = command({
27090
27389
  });
27091
27390
 
27092
27391
  // src/commands/quotations/rotate-link.ts
27093
- var rotateLink2 = command({
27392
+ var rotateLink3 = command({
27094
27393
  name: "rotate-link",
27095
27394
  description: "Reissue the quotation's public link, which stops every link already shared or emailed working",
27096
27395
  positionals: {
@@ -27104,26 +27403,30 @@ var rotateLink2 = command({
27104
27403
  });
27105
27404
 
27106
27405
  // src/commands/quotations/update.ts
27107
- var update9 = command({
27406
+ var update10 = command({
27108
27407
  name: "update",
27109
27408
  description: "Update a draft quotation (sent quotations are frozen)",
27110
- longDescription: "An omitted flag leaves its field unchanged. Passed --line flags replace the whole line list; --no-notes clears the notes and --no-valid-until makes the quotation never expire.",
27409
+ longDescription: "An omitted flag leaves its field unchanged. Passed --line, --cost, or --fee flags replace that whole list (--no-cost or --no-fee empties it); --no-notes clears the notes and --no-valid-until makes the quotation never expire.",
27111
27410
  positionals: {
27112
27411
  id: exports_external.string().describe("Quotation id or URL")
27113
27412
  },
27114
27413
  options: {
27115
27414
  line: exports_external.array(exports_external.string()).optional().describe(`Line item as ${QUOTATION_LINE_ITEM_SYNTAX}; repeatable`).meta({ short: "l" }),
27415
+ cost: exports_external.array(exports_external.string()).nullable().optional().describe(`Expected cost as ${EXPECTED_COST_SYNTAX}; repeatable, or --no-cost to clear`).meta({ negatable: true }),
27416
+ fee: exports_external.array(exports_external.string()).nullable().optional().describe(`Recurring fee as ${RECURRING_FEE_SYNTAX}; repeatable, or --no-fee to clear`).meta({ negatable: true }),
27116
27417
  notes: exports_external.string().nullable().optional().describe("Document-foot notes, or --no-notes").meta({ negatable: true }),
27117
27418
  validUntil: exports_external.string().nullable().optional().describe("Last valid day (YYYY-MM-DD), or --no-valid-until to never expire").meta({ negatable: true })
27118
27419
  },
27119
27420
  run: async ({ positionals: { id }, options }) => {
27120
- if (options.line === undefined && options.notes === undefined && options.validUntil === undefined) {
27121
- throw new Error("Nothing to update. Pass --line, --notes, or --valid-until (or a --no form to clear).");
27421
+ if (options.line === undefined && options.cost === undefined && options.fee === undefined && options.notes === undefined && options.validUntil === undefined) {
27422
+ throw new Error("Nothing to update. Pass --line, --cost, --fee, --notes, or --valid-until (or a --no form to clear).");
27122
27423
  }
27123
27424
  const backend = await backendClient();
27124
27425
  const { number: number4 } = await backend.mutation(api2.quotations.update, {
27125
27426
  id: readQuotationRef(id),
27126
27427
  lineItems: options.line?.map(parseQuotationLineItem),
27428
+ expectedCosts: options.cost === null ? [] : options.cost?.map(parseExpectedCost),
27429
+ recurringFees: options.fee === null ? [] : options.fee?.map(parseRecurringFee),
27127
27430
  notes: options.notes === null ? "" : options.notes,
27128
27431
  validUntil: options.validUntil === null ? "" : options.validUntil
27129
27432
  });
@@ -27154,10 +27457,10 @@ document awaiting the client's answer. Accept and reject are manual records;
27154
27457
  expiry is derived from valid-until and never blocks accepting. An accepted
27155
27458
  quotation converts into any number of draft invoices, each tracing back to it.`,
27156
27459
  commands: [
27157
- create8,
27158
- list10,
27159
- get7,
27160
- update9,
27460
+ create9,
27461
+ list11,
27462
+ get8,
27463
+ update10,
27161
27464
  issue3,
27162
27465
  email4,
27163
27466
  accept,
@@ -27166,7 +27469,7 @@ quotation converts into any number of draft invoices, each tracing back to it.`,
27166
27469
  duplicate2,
27167
27470
  convert,
27168
27471
  link2,
27169
- rotateLink2
27472
+ rotateLink3
27170
27473
  ]
27171
27474
  });
27172
27475
 
@@ -27185,7 +27488,21 @@ var upgrade = command({
27185
27488
  var rootCommand = group({
27186
27489
  name: "kds",
27187
27490
  description: "KDS CLI",
27188
- commands: [auth, clients, costs, daemon, inbox, invoices, issues, pages, project, projects, quotations, upgrade],
27491
+ commands: [
27492
+ auth,
27493
+ clients,
27494
+ costs,
27495
+ daemon,
27496
+ files,
27497
+ inbox,
27498
+ invoices,
27499
+ issues,
27500
+ pages,
27501
+ project,
27502
+ projects,
27503
+ quotations,
27504
+ upgrade
27505
+ ],
27189
27506
  options: {
27190
27507
  version: exports_external.boolean().default(false).describe("Show the version").meta({ short: "v" })
27191
27508
  },
@@ -27198,6 +27515,7 @@ var rootCommand = group({
27198
27515
 
27199
27516
  // src/lib/errors.ts
27200
27517
  var errorMessage = (error51) => error51 instanceof ConvexError && typeof error51.data === "string" ? error51.data : error51.message;
27518
+ var isRedactedServerError = (error51) => error51 instanceof Error && !(error51 instanceof ConvexError) && /^\[Request ID: \w+\] Server Error$/.test(error51.message);
27201
27519
 
27202
27520
  // src/lib/update-check.ts
27203
27521
  var latestVersion = async () => {
@@ -27228,6 +27546,12 @@ var maybeNotifyUpdate = async () => {
27228
27546
  if (latest && Bun.semver.order(latest, CLI_VERSION) === 1)
27229
27547
  console.error(`kds v${latest} available (you have v${CLI_VERSION}) \u2014 run 'kds upgrade'`);
27230
27548
  };
27549
+ var staleCliHint = async (current = CLI_VERSION) => {
27550
+ const latest = env.KDS_NO_UPDATE_CHECK ? undefined : await latestVersion().catch(() => {
27551
+ return;
27552
+ });
27553
+ 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.";
27554
+ };
27231
27555
 
27232
27556
  // src/index.ts
27233
27557
  var args = Bun.argv.slice(2);
@@ -27237,6 +27561,8 @@ try {
27237
27561
  await maybeNotifyUpdate();
27238
27562
  } catch (error51) {
27239
27563
  console.error(errorMessage(error51));
27564
+ if (isRedactedServerError(error51))
27565
+ console.error(await staleCliHint());
27240
27566
  process.exitCode = 1;
27241
27567
  } finally {
27242
27568
  closePrompts();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@islamihab/kds",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Command-line client for Kai Dev Studio",
5
5
  "license": "MIT",
6
6
  "publishConfig": {