@islamihab/kds 0.12.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 +337 -93
  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.12.0",
14626
+ version: "0.13.0",
14627
14627
  private: true,
14628
14628
  type: "module",
14629
14629
  bin: {
@@ -15210,6 +15210,8 @@ var PAGE_STYLES = {
15210
15210
  var PAGE_VISIBILITIES = ["public", "private"];
15211
15211
  var MAX_PAGE_HTML_BYTES = 4000000;
15212
15212
  var MAX_PAGE_VERSIONS = 20;
15213
+ var FILE_VISIBILITIES = ["public", "private"];
15214
+ var MAX_FILE_BYTES = 20000000;
15213
15215
  var ISSUE_STATUSES = [
15214
15216
  "backlog",
15215
15217
  "todo",
@@ -15379,7 +15381,16 @@ var issueIsOpen = (status) => !ISSUE_TERMINAL_STATUSES.some((terminal) => termin
15379
15381
  var CURRENCIES = ["EGP", "USD", "EUR", "GBP", "AED", "SAR"];
15380
15382
  var DEFAULT_CURRENCY = "EGP";
15381
15383
  var RECURRING_CHARGE_CADENCES = ["monthly", "yearly"];
15384
+ var RECURRING_CHARGE_CADENCE_LABELS = {
15385
+ monthly: "Monthly",
15386
+ yearly: "Yearly"
15387
+ };
15382
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
+ };
15383
15394
  var MAX_PROJECT_REPO_LENGTH = 200;
15384
15395
  var INVOICE_STATUSES = ["draft", "sent", "paid", "void"];
15385
15396
  var invoiceLineTotal = ({ quantity, unitPrice }) => {
@@ -24280,6 +24291,15 @@ var readPageId = (value) => {
24280
24291
  return trimmed;
24281
24292
  }
24282
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
+ };
24283
24303
 
24284
24304
  // src/commands/clients/archive.ts
24285
24305
  var archive = command({
@@ -25162,6 +25182,186 @@ var daemon = group({
25162
25182
  commands: [start, stop, status2, logs]
25163
25183
  });
25164
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
+
25165
25365
  // src/commands/inbox/list.ts
25166
25366
  var inboxOptions = {
25167
25367
  all: exports_external.boolean().default(false).describe("Include events already marked seen"),
@@ -25261,10 +25461,34 @@ var parseQuotationLineItem = (value) => {
25261
25461
  const section = parts.length === 5 ? parts.pop()?.trim() : undefined;
25262
25462
  return { ...parseLineItem(parts.join("|")), section: section || undefined };
25263
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
+ };
25264
25488
  var formatPerCurrency = (entries) => entries.length === 0 ? "-" : entries.map(({ amount, currency }) => formatMinorAmount(amount, currency)).join(" + ");
25265
25489
 
25266
25490
  // src/commands/invoices/create.ts
25267
- var create3 = command({
25491
+ var create4 = command({
25268
25492
  name: "create",
25269
25493
  description: "Create a draft invoice for a client and print its id",
25270
25494
  positionals: {
@@ -25317,7 +25541,7 @@ var email3 = command({
25317
25541
  });
25318
25542
 
25319
25543
  // src/commands/invoices/get.ts
25320
- var get3 = command({
25544
+ var get4 = command({
25321
25545
  name: "get",
25322
25546
  description: "Show an invoice: document, totals, and payments",
25323
25547
  positionals: {
@@ -25407,7 +25631,7 @@ var link = command({
25407
25631
  });
25408
25632
 
25409
25633
  // src/commands/invoices/list.ts
25410
- var list4 = command({
25634
+ var list5 = command({
25411
25635
  name: "list",
25412
25636
  description: "List your invoices, newest first",
25413
25637
  options: {
@@ -25477,7 +25701,7 @@ var add2 = command({
25477
25701
  });
25478
25702
 
25479
25703
  // src/commands/invoices/payments/list.ts
25480
- var list5 = command({
25704
+ var list6 = command({
25481
25705
  name: "list",
25482
25706
  description: "List the payments recorded on an invoice, oldest first",
25483
25707
  positionals: {
@@ -25505,7 +25729,7 @@ var list5 = command({
25505
25729
  });
25506
25730
 
25507
25731
  // src/commands/invoices/payments/remove.ts
25508
- var remove3 = command({
25732
+ var remove4 = command({
25509
25733
  name: "remove",
25510
25734
  description: "Remove a recorded payment (unflips paid when the invoice is no longer covered)",
25511
25735
  positionals: {
@@ -25525,11 +25749,11 @@ var payments = group({
25525
25749
  longDescription: `A payment is money received against a sent invoice, in one currency. Paid flips
25526
25750
  automatically once every currency's total is covered; overpayment is rejected,
25527
25751
  and forgiving a shortfall is a payment with a note.`,
25528
- commands: [add2, list5, remove3]
25752
+ commands: [add2, list6, remove4]
25529
25753
  });
25530
25754
 
25531
25755
  // src/commands/invoices/rotate-link.ts
25532
- var rotateLink = command({
25756
+ var rotateLink2 = command({
25533
25757
  name: "rotate-link",
25534
25758
  description: "Reissue the invoice's public link, which stops every link already shared or emailed working",
25535
25759
  positionals: {
@@ -25543,7 +25767,7 @@ var rotateLink = command({
25543
25767
  });
25544
25768
 
25545
25769
  // src/commands/invoices/update.ts
25546
- var update4 = command({
25770
+ var update5 = command({
25547
25771
  name: "update",
25548
25772
  description: "Update a draft invoice (sent invoices are frozen)",
25549
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.",
@@ -25591,52 +25815,29 @@ var invoices = group({
25591
25815
  longDescription: `An invoice is a draft until issued: issuing assigns its gapless number, freezes the
25592
25816
  document, and snapshots the client's billing details. Sent invoices are immutable \u2014
25593
25817
  the fix path is void + duplicate into a fresh draft. Drafts delete outright.`,
25594
- 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]
25595
25819
  });
25596
25820
 
25597
25821
  // src/lib/attachments.ts
25598
- import { basename } from "path";
25822
+ import { basename as basename2 } from "path";
25599
25823
  var attachFiles = async (client3, issueId, paths) => {
25600
25824
  if (paths.length === 0)
25601
25825
  return [];
25602
- const files = paths.map((path) => ({ path, file: Bun.file(path) }));
25603
- for (const { path, file: file2 } of files) {
25604
- if (!await file2.exists())
25605
- throw new Error(`No file at ${path}.`);
25606
- if (file2.size <= 0)
25607
- throw new Error(`${path} is empty; an attachment cannot be.`);
25608
- if (file2.size > MAX_ISSUE_ATTACHMENT_BYTES) {
25609
- throw new Error(`${path} is too large (${file2.size} bytes; max ${MAX_ISSUE_ATTACHMENT_BYTES}).`);
25610
- }
25611
- }
25612
- const created = new Set;
25613
- try {
25614
- for (const { path, file: file2 } of files) {
25615
- const uploadUrl = await client3.mutation(api2.issueAttachments.generateUploadUrl, { issueId });
25616
- const response = await fetch(uploadUrl, {
25617
- method: "POST",
25618
- headers: { "Content-Type": file2.type },
25619
- body: file2
25620
- });
25621
- if (!response.ok)
25622
- throw new Error(`Uploading ${path} failed (${response.status}).`);
25623
- const storageId = exports_external.custom((value) => typeof value === "string");
25624
- const body = exports_external.object({ storageId }).safeParse(await response.json());
25625
- if (!body.success)
25626
- throw new Error("The upload returned no file reference.");
25627
- created.add(await client3.action(api2.issueAttachments.create, {
25628
- issueId,
25629
- storageId: body.data.storageId,
25630
- name: basename(path)
25631
- }));
25632
- }
25633
- } 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) {
25634
25835
  for (const id of created) {
25635
25836
  await client3.mutation(api2.issueAttachments.remove, { id }).catch(() => {
25636
25837
  console.error("An attachment from this failed batch could not be removed; check the dashboard.");
25637
25838
  });
25638
25839
  }
25639
- throw error51;
25840
+ throw failed.reason;
25640
25841
  }
25641
25842
  const attachments = await client3.query(api2.issueAttachments.list, { issueId });
25642
25843
  return attachments.filter((attachment) => created.has(attachment._id));
@@ -25685,7 +25886,7 @@ var agentConfigurationLine = (id) => {
25685
25886
  };
25686
25887
 
25687
25888
  // src/commands/issues/create.ts
25688
- var create4 = command({
25889
+ var create5 = command({
25689
25890
  name: "create",
25690
25891
  description: "Create an issue and print its identifier",
25691
25892
  positionals: {
@@ -25815,7 +26016,7 @@ Feed:`);
25815
26016
  }
25816
26017
  }
25817
26018
  };
25818
- var get4 = command({
26019
+ var get5 = command({
25819
26020
  name: "get",
25820
26021
  description: "Show one or more issues: properties, description, and feed",
25821
26022
  positionals: {
@@ -25853,7 +26054,7 @@ var get4 = command({
25853
26054
  });
25854
26055
 
25855
26056
  // src/commands/issues/labels/create.ts
25856
- var create5 = command({
26057
+ var create6 = command({
25857
26058
  name: "create",
25858
26059
  description: "Create an issue label and print its id",
25859
26060
  positionals: {
@@ -25870,7 +26071,7 @@ var create5 = command({
25870
26071
  });
25871
26072
 
25872
26073
  // src/commands/issues/labels/list.ts
25873
- var list6 = command({
26074
+ var list7 = command({
25874
26075
  name: "list",
25875
26076
  description: "List issue labels alphabetically",
25876
26077
  options: {
@@ -25887,7 +26088,7 @@ var list6 = command({
25887
26088
  });
25888
26089
 
25889
26090
  // src/commands/issues/labels/remove.ts
25890
- var remove4 = command({
26091
+ var remove5 = command({
25891
26092
  name: "delete",
25892
26093
  description: "Delete an issue label and remove it from every issue",
25893
26094
  positionals: {
@@ -25901,7 +26102,7 @@ var remove4 = command({
25901
26102
  });
25902
26103
 
25903
26104
  // src/commands/issues/labels/update.ts
25904
- var update5 = command({
26105
+ var update6 = command({
25905
26106
  name: "update",
25906
26107
  description: "Rename or recolor an issue label",
25907
26108
  positionals: {
@@ -25924,7 +26125,7 @@ var update5 = command({
25924
26125
  var labels = group({
25925
26126
  name: "labels",
25926
26127
  description: "Manage issue labels",
25927
- commands: [create5, list6, update5, remove4]
26128
+ commands: [create6, list7, update6, remove5]
25928
26129
  });
25929
26130
 
25930
26131
  // src/commands/issues/link-pr.ts
@@ -25971,7 +26172,7 @@ var resolveScope = async (client3, { project, here, milestone }) => {
25971
26172
  return { projectId: (await repoProject(client3))._id };
25972
26173
  return;
25973
26174
  };
25974
- var list7 = command({
26175
+ var list8 = command({
25975
26176
  name: "list",
25976
26177
  description: "List open issues, most recently updated first",
25977
26178
  options: {
@@ -26087,7 +26288,7 @@ var relate = command({
26087
26288
  });
26088
26289
 
26089
26290
  // src/commands/issues/remove.ts
26090
- var remove5 = command({
26291
+ var remove6 = command({
26091
26292
  name: "delete",
26092
26293
  description: "Delete an issue permanently (sub-issues survive as top-level issues)",
26093
26294
  positionals: {
@@ -26323,9 +26524,9 @@ address several rows even as removals renumber it.`,
26323
26524
  remove: position.optional().describe("Drop a task by its number (repeatable)"),
26324
26525
  json: exports_external.boolean().default(false).describe("Print as JSON")
26325
26526
  },
26326
- 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 } }) => {
26327
26528
  const client3 = await backendClient();
26328
- const wrote = [add3, check2, uncheck, convert, remove6].some((values) => values !== undefined);
26529
+ const wrote = [add3, check2, uncheck, convert, remove7].some((values) => values !== undefined);
26329
26530
  if (wrote) {
26330
26531
  const { identifier, converted, tasks: tasks3 } = await client3.mutation(api2.issueTasks.edit, {
26331
26532
  issue: readIssueRef(id),
@@ -26333,7 +26534,7 @@ address several rows even as removals renumber it.`,
26333
26534
  ...check2 !== undefined ? { check: check2 } : {},
26334
26535
  ...uncheck !== undefined ? { uncheck } : {},
26335
26536
  ...convert !== undefined ? { convert } : {},
26336
- ...remove6 !== undefined ? { remove: remove6 } : {}
26537
+ ...remove7 !== undefined ? { remove: remove7 } : {}
26337
26538
  });
26338
26539
  if (!json2) {
26339
26540
  for (const created of converted) {
@@ -26377,7 +26578,7 @@ var unrelate = command({
26377
26578
  });
26378
26579
 
26379
26580
  // src/commands/issues/update.ts
26380
- var update6 = command({
26581
+ var update7 = command({
26381
26582
  name: "update",
26382
26583
  description: "Rewrite an issue's title or description, or attach files",
26383
26584
  positionals: {
@@ -26422,10 +26623,10 @@ changes_requested, an approval to approved (one outstanding change request outwe
26422
26623
  any number of approvals), and a merge lands it as done. link-pr covers the pull
26423
26624
  request the branch name does not point at; from there the same tracking applies.`,
26424
26625
  commands: [
26425
- create4,
26426
- list7,
26427
- get4,
26428
- update6,
26626
+ create5,
26627
+ list8,
26628
+ get5,
26629
+ update7,
26429
26630
  set2,
26430
26631
  labels,
26431
26632
  route,
@@ -26439,7 +26640,7 @@ request the branch name does not point at; from there the same tracking applies.
26439
26640
  relate,
26440
26641
  unrelate,
26441
26642
  comment,
26442
- remove5
26643
+ remove6
26443
26644
  ]
26444
26645
  });
26445
26646
 
@@ -26447,7 +26648,7 @@ request the branch name does not point at; from there the same tracking applies.
26447
26648
  var keys = (object2) => Object.keys(object2);
26448
26649
 
26449
26650
  // src/lib/group.ts
26450
- import { basename as basename2 } from "path";
26651
+ import { basename as basename3 } from "path";
26451
26652
  var git = async (...args) => {
26452
26653
  const proc = Bun.spawn(["git", ...args], { stdout: "pipe", stderr: "ignore" });
26453
26654
  const [output, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
@@ -26459,12 +26660,12 @@ var detectRepoGroup = async () => {
26459
26660
  if (remote)
26460
26661
  return repoNameFromRemote(remote);
26461
26662
  const root = await git("rev-parse", "--show-toplevel");
26462
- return root ? basename2(root) : undefined;
26663
+ return root ? basename3(root) : undefined;
26463
26664
  };
26464
26665
  var groupForCreate = async (group2) => group2 === null ? undefined : group2 ?? await detectRepoGroup();
26465
26666
 
26466
26667
  // src/commands/pages/create.ts
26467
- var create6 = command({
26668
+ var create7 = command({
26468
26669
  name: "create",
26469
26670
  description: "Publish a page and print its URL",
26470
26671
  positionals: {
@@ -26494,7 +26695,7 @@ var create6 = command({
26494
26695
  });
26495
26696
 
26496
26697
  // src/commands/pages/get.ts
26497
- var get5 = command({
26698
+ var get6 = command({
26498
26699
  name: "get",
26499
26700
  description: "Print a page's HTML",
26500
26701
  positionals: {
@@ -26513,11 +26714,8 @@ var get5 = command({
26513
26714
  }
26514
26715
  });
26515
26716
 
26516
- // ../../packages/backend/convex/lib/format.ts
26517
- var formatBytes = (bytes) => bytes < 1024 ? `${bytes} B` : `${Math.round(bytes / 1024)} KB`;
26518
-
26519
26717
  // src/commands/pages/list.ts
26520
- var list8 = command({
26718
+ var list9 = command({
26521
26719
  name: "list",
26522
26720
  description: "List your published pages",
26523
26721
  options: {
@@ -26547,7 +26745,7 @@ var list8 = command({
26547
26745
  });
26548
26746
 
26549
26747
  // src/commands/pages/remove.ts
26550
- var remove6 = command({
26748
+ var remove7 = command({
26551
26749
  name: "delete",
26552
26750
  description: "Delete a page",
26553
26751
  positionals: {
@@ -26583,7 +26781,7 @@ var revert = command({
26583
26781
  });
26584
26782
 
26585
26783
  // src/commands/pages/update.ts
26586
- var update7 = command({
26784
+ var update8 = command({
26587
26785
  name: "update",
26588
26786
  description: "Replace a page's HTML, title, group, style, or visibility",
26589
26787
  positionals: {
@@ -26651,7 +26849,7 @@ var pages = group({
26651
26849
  ${describeChoices(PAGE_STYLES, `
26652
26850
  `)}
26653
26851
  Basic is the default. A page keeps its URL across updates and reverts.`,
26654
- commands: [create6, list8, get5, update7, versions2, revert, remove6]
26852
+ commands: [create7, list9, get6, update8, versions2, revert, remove7]
26655
26853
  });
26656
26854
 
26657
26855
  // src/commands/project.ts
@@ -26684,7 +26882,7 @@ var project = command({
26684
26882
  });
26685
26883
 
26686
26884
  // src/commands/projects/create.ts
26687
- var create7 = command({
26885
+ var create8 = command({
26688
26886
  name: "create",
26689
26887
  description: "Create a project and print its id",
26690
26888
  positionals: {
@@ -26716,7 +26914,7 @@ var create7 = command({
26716
26914
  });
26717
26915
 
26718
26916
  // src/commands/projects/get.ts
26719
- var get6 = command({
26917
+ var get7 = command({
26720
26918
  name: "get",
26721
26919
  description: "Show a project: properties, description, and milestones",
26722
26920
  positionals: {
@@ -26760,7 +26958,7 @@ Milestones:`);
26760
26958
  });
26761
26959
 
26762
26960
  // src/commands/projects/list.ts
26763
- var list9 = command({
26961
+ var list10 = command({
26764
26962
  name: "list",
26765
26963
  description: "List your projects, most recently updated first",
26766
26964
  options: {
@@ -26803,7 +27001,7 @@ More projects exist; raise --limit past ${limit}.`);
26803
27001
  });
26804
27002
 
26805
27003
  // src/commands/projects/remove.ts
26806
- var remove7 = command({
27004
+ var remove8 = command({
26807
27005
  name: "delete",
26808
27006
  description: "Delete a project and its milestones (its issues survive, projectless)",
26809
27007
  positionals: {
@@ -26848,7 +27046,7 @@ var set3 = command({
26848
27046
  });
26849
27047
 
26850
27048
  // src/commands/projects/update.ts
26851
- var update8 = command({
27049
+ var update9 = command({
26852
27050
  name: "update",
26853
27051
  description: "Rewrite a project's name, summary, or description",
26854
27052
  positionals: {
@@ -26879,7 +27077,7 @@ var projects = group({
26879
27077
  description: "Manage projects",
26880
27078
  longDescription: `Connect a repository (set --repo) to make --here resolve in that checkout and to
26881
27079
  let issue branches and pull requests link automatically.`,
26882
- commands: [create7, list9, get6, update8, set3, remove7]
27080
+ commands: [create8, list10, get7, update9, set3, remove8]
26883
27081
  });
26884
27082
 
26885
27083
  // src/commands/quotations/accept.ts
@@ -26938,7 +27136,7 @@ Nothing blocks over- or under-invoicing; left to bill is advisory.`,
26938
27136
  });
26939
27137
 
26940
27138
  // src/commands/quotations/create.ts
26941
- var create8 = command({
27139
+ var create9 = command({
26942
27140
  name: "create",
26943
27141
  description: "Create a draft quotation for a client and print its id",
26944
27142
  positionals: {
@@ -26946,6 +27144,8 @@ var create8 = command({
26946
27144
  },
26947
27145
  options: {
26948
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`),
26949
27149
  project: exports_external.string().optional().describe("Project id or URL to link (must belong to the same client)"),
26950
27150
  notes: exports_external.string().optional().describe("Free text rendered at the document foot"),
26951
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 })
@@ -26956,6 +27156,8 @@ var create8 = command({
26956
27156
  clientId: readClientRef(ref),
26957
27157
  projectId: options.project === undefined ? undefined : readProjectRef(options.project),
26958
27158
  lineItems: options.line?.map(parseQuotationLineItem),
27159
+ expectedCosts: options.cost?.map(parseExpectedCost),
27160
+ recurringFees: options.fee?.map(parseRecurringFee),
26959
27161
  notes: options.notes,
26960
27162
  validUntil: options.validUntil === null ? "" : options.validUntil,
26961
27163
  today: localToday()
@@ -26992,7 +27194,7 @@ var email4 = command({
26992
27194
  });
26993
27195
 
26994
27196
  // src/commands/quotations/get.ts
26995
- var get7 = command({
27197
+ var get8 = command({
26996
27198
  name: "get",
26997
27199
  description: "Show a quotation: document, totals, and \u2014 once accepted \u2014 its billing progress",
26998
27200
  positionals: {
@@ -27038,6 +27240,30 @@ var get7 = command({
27038
27240
  }
27039
27241
  console.log(`
27040
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
+ }
27041
27267
  if (quotation.billing) {
27042
27268
  console.log(`Invoiced: ${formatPerCurrency(quotation.billing.progress.map(({ currency, invoiced }) => ({ currency, amount: invoiced })))}`);
27043
27269
  const leftToBill = quotation.billing.progress.filter(({ remaining }) => remaining > 0);
@@ -27099,7 +27325,7 @@ var link2 = command({
27099
27325
  });
27100
27326
 
27101
27327
  // src/commands/quotations/list.ts
27102
- var list10 = command({
27328
+ var list11 = command({
27103
27329
  name: "list",
27104
27330
  description: "List your quotations, newest first (expired is derived, never stored)",
27105
27331
  options: {
@@ -27163,7 +27389,7 @@ var reject2 = command({
27163
27389
  });
27164
27390
 
27165
27391
  // src/commands/quotations/rotate-link.ts
27166
- var rotateLink2 = command({
27392
+ var rotateLink3 = command({
27167
27393
  name: "rotate-link",
27168
27394
  description: "Reissue the quotation's public link, which stops every link already shared or emailed working",
27169
27395
  positionals: {
@@ -27177,26 +27403,30 @@ var rotateLink2 = command({
27177
27403
  });
27178
27404
 
27179
27405
  // src/commands/quotations/update.ts
27180
- var update9 = command({
27406
+ var update10 = command({
27181
27407
  name: "update",
27182
27408
  description: "Update a draft quotation (sent quotations are frozen)",
27183
- 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.",
27184
27410
  positionals: {
27185
27411
  id: exports_external.string().describe("Quotation id or URL")
27186
27412
  },
27187
27413
  options: {
27188
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 }),
27189
27417
  notes: exports_external.string().nullable().optional().describe("Document-foot notes, or --no-notes").meta({ negatable: true }),
27190
27418
  validUntil: exports_external.string().nullable().optional().describe("Last valid day (YYYY-MM-DD), or --no-valid-until to never expire").meta({ negatable: true })
27191
27419
  },
27192
27420
  run: async ({ positionals: { id }, options }) => {
27193
- if (options.line === undefined && options.notes === undefined && options.validUntil === undefined) {
27194
- 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).");
27195
27423
  }
27196
27424
  const backend = await backendClient();
27197
27425
  const { number: number4 } = await backend.mutation(api2.quotations.update, {
27198
27426
  id: readQuotationRef(id),
27199
27427
  lineItems: options.line?.map(parseQuotationLineItem),
27428
+ expectedCosts: options.cost === null ? [] : options.cost?.map(parseExpectedCost),
27429
+ recurringFees: options.fee === null ? [] : options.fee?.map(parseRecurringFee),
27200
27430
  notes: options.notes === null ? "" : options.notes,
27201
27431
  validUntil: options.validUntil === null ? "" : options.validUntil
27202
27432
  });
@@ -27227,10 +27457,10 @@ document awaiting the client's answer. Accept and reject are manual records;
27227
27457
  expiry is derived from valid-until and never blocks accepting. An accepted
27228
27458
  quotation converts into any number of draft invoices, each tracing back to it.`,
27229
27459
  commands: [
27230
- create8,
27231
- list10,
27232
- get7,
27233
- update9,
27460
+ create9,
27461
+ list11,
27462
+ get8,
27463
+ update10,
27234
27464
  issue3,
27235
27465
  email4,
27236
27466
  accept,
@@ -27239,7 +27469,7 @@ quotation converts into any number of draft invoices, each tracing back to it.`,
27239
27469
  duplicate2,
27240
27470
  convert,
27241
27471
  link2,
27242
- rotateLink2
27472
+ rotateLink3
27243
27473
  ]
27244
27474
  });
27245
27475
 
@@ -27258,7 +27488,21 @@ var upgrade = command({
27258
27488
  var rootCommand = group({
27259
27489
  name: "kds",
27260
27490
  description: "KDS CLI",
27261
- 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
+ ],
27262
27506
  options: {
27263
27507
  version: exports_external.boolean().default(false).describe("Show the version").meta({ short: "v" })
27264
27508
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@islamihab/kds",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Command-line client for Kai Dev Studio",
5
5
  "license": "MIT",
6
6
  "publishConfig": {