@islamihab/kds 0.2.0 → 0.2.2

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 +352 -66
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -14450,7 +14450,12 @@ var isHidden = (field) => field.meta()?.hidden === true;
14450
14450
  var isNegatable = (field) => field.meta()?.negatable === true;
14451
14451
  var isHelpFlag = (arg) => arg === "-h" || arg === "--help";
14452
14452
  var valuePlaceholder = (field) => {
14453
- const base = baseType(field);
14453
+ let base = baseType(field);
14454
+ if (base instanceof exports_external.ZodArray) {
14455
+ const element = base.element;
14456
+ if (element instanceof exports_external.ZodType)
14457
+ base = element;
14458
+ }
14454
14459
  if (base instanceof exports_external.ZodBoolean)
14455
14460
  return "";
14456
14461
  if (base instanceof exports_external.ZodEnum)
@@ -14496,8 +14501,10 @@ var parseArgs = ({
14496
14501
  }) => {
14497
14502
  const options = {};
14498
14503
  for (const { key, field, short, negatedKey } of optionsEntries) {
14499
- const type = baseType(field) instanceof exports_external.ZodBoolean ? "boolean" : "string";
14500
- options[key] = short ? { short, type } : { type };
14504
+ const base = baseType(field);
14505
+ const type = base instanceof exports_external.ZodBoolean ? "boolean" : "string";
14506
+ const multiple = base instanceof exports_external.ZodArray ? true : undefined;
14507
+ options[key] = short ? { short, type, multiple } : { type, multiple };
14501
14508
  if (negatedKey)
14502
14509
  options[negatedKey] = { type: "boolean" };
14503
14510
  }
@@ -14587,7 +14594,7 @@ import { join } from "path";
14587
14594
  // package.json
14588
14595
  var package_default = {
14589
14596
  name: "cli",
14590
- version: "0.2.0",
14597
+ version: "0.2.2",
14591
14598
  private: true,
14592
14599
  type: "module",
14593
14600
  bin: {
@@ -15159,10 +15166,12 @@ var ISSUE_CREATE_DISPOSITIONS = [
15159
15166
  ];
15160
15167
  var ISSUE_ESTIMATES = [1, 2, 3, 5, 8];
15161
15168
  var ISSUE_TERMINAL_STATUSES = ["done", "canceled"];
15169
+ var ISSUE_RELATION_KINDS = ["blocks", "blocked_by", "duplicate_of", "duplicated_by", "relates_to"];
15162
15170
  var PROJECT_STATUSES = ["planned", "in_progress", "paused", "completed", "canceled"];
15163
15171
  var PROJECT_HEALTHS = ["on_track", "at_risk", "off_track"];
15164
15172
  var ISSUE_IDENTIFIER_PREFIX = "KAI";
15165
15173
  var ISSUE_LIST_PAGE_SIZE = 50;
15174
+ var MAX_ISSUE_ATTACHMENT_BYTES = 1e7;
15166
15175
  var ISSUE_DUE_DATE_MODES = ["overdue", "due_today", "due_soon", "no_due_date"];
15167
15176
  var issueIsOpen = (status) => !ISSUE_TERMINAL_STATUSES.some((terminal) => terminal === status);
15168
15177
  var MAX_PROJECT_REPO_LENGTH = 200;
@@ -17814,6 +17823,117 @@ var auth = group({
17814
17823
  commands: [login, logout, status]
17815
17824
  });
17816
17825
 
17826
+ // src/lib/activity.ts
17827
+ var SOURCE_LABELS = { github: "GitHub" };
17828
+ var issueActivityLine = (detail) => {
17829
+ const label = detail.kind.replaceAll("_", " ");
17830
+ switch (detail.kind) {
17831
+ case "created":
17832
+ case "description_changed":
17833
+ return label;
17834
+ case "title_changed":
17835
+ case "status_changed":
17836
+ case "priority_changed":
17837
+ case "disposition_changed":
17838
+ case "estimate_changed":
17839
+ case "due_date_changed":
17840
+ case "branch_changed":
17841
+ case "pr_changed":
17842
+ case "project_changed":
17843
+ case "milestone_changed":
17844
+ case "parent_changed":
17845
+ return `${label}: ${detail.from ?? "none"} \u2192 ${detail.to ?? "none"}`;
17846
+ case "label_added":
17847
+ case "label_removed":
17848
+ case "attachment_added":
17849
+ case "attachment_removed":
17850
+ return `${label}: ${detail.name}`;
17851
+ case "child_added":
17852
+ case "child_removed":
17853
+ return `${label}: ${detail.identifier}`;
17854
+ case "relation_added":
17855
+ case "relation_removed":
17856
+ return `${label}: ${detail.relation.replaceAll("_", " ")} ${detail.identifier}`;
17857
+ }
17858
+ };
17859
+ var projectActivityLine = (detail) => {
17860
+ const label = detail.kind.replaceAll("_", " ");
17861
+ switch (detail.kind) {
17862
+ case "created":
17863
+ case "summary_changed":
17864
+ case "description_changed":
17865
+ case "milestones_reordered":
17866
+ return label;
17867
+ case "name_changed":
17868
+ case "status_changed":
17869
+ case "health_changed":
17870
+ case "target_date_changed":
17871
+ case "repo_changed":
17872
+ case "milestone_renamed":
17873
+ return `${label}: ${detail.from ?? "none"} \u2192 ${detail.to ?? "none"}`;
17874
+ case "milestone_added":
17875
+ case "milestone_removed":
17876
+ case "milestone_description_changed":
17877
+ return `${label}: ${detail.name}`;
17878
+ case "milestone_target_date_changed":
17879
+ return `${label}: ${detail.name} ${detail.from ?? "none"} \u2192 ${detail.to ?? "none"}`;
17880
+ case "issue_added":
17881
+ case "issue_removed":
17882
+ return `${label}: ${detail.identifier}`;
17883
+ }
17884
+ };
17885
+
17886
+ // src/lib/output.ts
17887
+ var printTable = (rows) => {
17888
+ if (rows.length === 0)
17889
+ return;
17890
+ const columnCount = Math.max(...rows.map((row) => row.length));
17891
+ const widths = Array.from({ length: columnCount }, (_, column) => Math.max(...rows.map((row) => (row[column] ?? "").length)));
17892
+ for (const row of rows) {
17893
+ console.log(row.map((cell, column) => cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd());
17894
+ }
17895
+ };
17896
+
17897
+ // src/commands/inbox/list.ts
17898
+ var inboxOptions = {
17899
+ all: exports_external.boolean().default(false).describe("Include events already marked seen"),
17900
+ json: exports_external.boolean().default(false).describe("Print as JSON")
17901
+ };
17902
+ var runInbox = async (options) => {
17903
+ const client3 = await backendClient();
17904
+ const inbox = await client3.query(api2.inbox.list, {});
17905
+ const targetLastSeen = new Map(inbox.targetLastSeen.map((state) => [state.targetId, state.lastSeenAt]));
17906
+ const events = options.all ? inbox.events : inbox.events.filter((event) => {
17907
+ const targetId = event.type === "issue" ? event.issue.id : event.project.id;
17908
+ return event.at > Math.max(inbox.lastSeenAt, targetLastSeen.get(targetId) ?? 0);
17909
+ });
17910
+ if (options.json)
17911
+ return console.log(JSON.stringify({ ...inbox, events }, null, 2));
17912
+ if (events.length === 0) {
17913
+ if (options.all)
17914
+ console.log("Nothing in the inbox.");
17915
+ else
17916
+ console.log(inbox.truncated ? "Nothing unseen in the newest events shown." : "Inbox zero.");
17917
+ if (!options.all && inbox.events.length > 0)
17918
+ console.log("Pass --all to reprint seen events.");
17919
+ return;
17920
+ }
17921
+ printTable([
17922
+ ["WHEN", "ACTOR", "WHERE", "EVENT"],
17923
+ ...events.map((event) => [
17924
+ new Date(event.at).toISOString().slice(0, 16).replace("T", " "),
17925
+ SOURCE_LABELS[event.via],
17926
+ event.type === "issue" ? event.issue.identifier : event.project.name,
17927
+ event.type === "issue" ? issueActivityLine(event.detail) : projectActivityLine(event.detail)
17928
+ ])
17929
+ ]);
17930
+ if (inbox.truncated)
17931
+ console.log(`
17932
+ (older events truncated)`);
17933
+ if (!options.all)
17934
+ console.log("\nRun `kds inbox seen` once these are handled.");
17935
+ };
17936
+
17817
17937
  // src/lib/input.ts
17818
17938
  var assertSize = (size) => {
17819
17939
  if (size > MAX_PAGE_HTML_BYTES)
@@ -17924,6 +18044,20 @@ var repoProject = async (client3) => {
17924
18044
  throw new Error(`No project is connected to ${repo}. Connect one on the project in the dashboard.`);
17925
18045
  return project;
17926
18046
  };
18047
+ var resolveIssueLabels = async (client3, names) => {
18048
+ if (names.length === 0)
18049
+ return [];
18050
+ const labels = await client3.query(api2.issueLabels.list, {});
18051
+ return names.map((name) => {
18052
+ const wanted = name.trim().toLowerCase();
18053
+ const label = labels.find((candidate) => candidate.name.toLowerCase() === wanted);
18054
+ if (!label) {
18055
+ const available = labels.map((candidate) => candidate.name).join(", ");
18056
+ throw new Error(available ? `No label named "${name}". The labels are: ${available}.` : "There are no labels yet.");
18057
+ }
18058
+ return label;
18059
+ });
18060
+ };
17927
18061
  var resolveMilestone = async (client3, projectId, name) => {
17928
18062
  const milestones = await client3.query(api2.milestones.listByProject, { projectId, today: localToday() });
17929
18063
  const wanted = name.trim().toLowerCase();
@@ -17935,6 +18069,83 @@ var resolveMilestone = async (client3, projectId, name) => {
17935
18069
  return milestone;
17936
18070
  };
17937
18071
 
18072
+ // src/commands/inbox/seen.ts
18073
+ var seen = command({
18074
+ name: "seen",
18075
+ description: "Mark one issue, or the whole inbox, as seen",
18076
+ positionals: {
18077
+ id: exports_external.string().optional().describe("Issue identifier, number, or URL")
18078
+ },
18079
+ run: async ({ positionals: { id } }) => {
18080
+ const client3 = await backendClient();
18081
+ const issue2 = id === undefined ? undefined : await resolveIssue(client3, id);
18082
+ await client3.mutation(api2.inbox.markSeen, { targetId: issue2?._id });
18083
+ console.log(issue2 ? `${issue2.identifier} marked seen.` : "Inbox marked seen.");
18084
+ }
18085
+ });
18086
+
18087
+ // src/commands/inbox/index.ts
18088
+ var inbox = group({
18089
+ name: "inbox",
18090
+ description: "Automated activity across issues and projects, unseen first",
18091
+ options: inboxOptions,
18092
+ run: async ({ options }) => await runInbox(options),
18093
+ commands: [seen]
18094
+ });
18095
+
18096
+ // src/lib/attachments.ts
18097
+ import { basename } from "path";
18098
+ var attachFiles = async (client3, issueId, paths) => {
18099
+ if (paths.length === 0)
18100
+ return [];
18101
+ const files = paths.map((path) => ({ path, file: Bun.file(path) }));
18102
+ for (const { path, file: file2 } of files) {
18103
+ if (!await file2.exists())
18104
+ throw new Error(`No file at ${path}.`);
18105
+ if (file2.size <= 0)
18106
+ throw new Error(`${path} is empty; an attachment cannot be.`);
18107
+ if (file2.size > MAX_ISSUE_ATTACHMENT_BYTES) {
18108
+ throw new Error(`${path} is too large (${file2.size} bytes; max ${MAX_ISSUE_ATTACHMENT_BYTES}).`);
18109
+ }
18110
+ }
18111
+ const created = new Set;
18112
+ try {
18113
+ for (const { path, file: file2 } of files) {
18114
+ const uploadUrl = await client3.mutation(api2.issueAttachments.generateUploadUrl, { issueId });
18115
+ const response = await fetch(uploadUrl, {
18116
+ method: "POST",
18117
+ headers: { "Content-Type": file2.type },
18118
+ body: file2
18119
+ });
18120
+ if (!response.ok)
18121
+ throw new Error(`Uploading ${path} failed (${response.status}).`);
18122
+ const storageId = exports_external.custom((value) => typeof value === "string");
18123
+ const body = exports_external.object({ storageId }).safeParse(await response.json());
18124
+ if (!body.success)
18125
+ throw new Error("The upload returned no file reference.");
18126
+ created.add(await client3.action(api2.issueAttachments.create, {
18127
+ issueId,
18128
+ storageId: body.data.storageId,
18129
+ name: basename(path)
18130
+ }));
18131
+ }
18132
+ } catch (error51) {
18133
+ for (const id of created) {
18134
+ await client3.mutation(api2.issueAttachments.remove, { id }).catch(() => {
18135
+ console.error("An attachment from this failed batch could not be removed; check the dashboard.");
18136
+ });
18137
+ }
18138
+ throw error51;
18139
+ }
18140
+ const attachments = await client3.query(api2.issueAttachments.list, { issueId });
18141
+ return attachments.filter((attachment) => created.has(attachment._id));
18142
+ };
18143
+ var printAttachments = (attachments) => {
18144
+ for (const attachment of attachments) {
18145
+ console.log(`Attached ${attachment.name}${attachment.url ? ` \u2014 ${attachment.url}` : ""}`);
18146
+ }
18147
+ };
18148
+
17938
18149
  // src/commands/issues/comment.ts
17939
18150
  var comment = command({
17940
18151
  name: "comment",
@@ -17943,9 +18154,13 @@ var comment = command({
17943
18154
  id: exports_external.string().describe("Issue identifier, number, or URL"),
17944
18155
  body: exports_external.string().describe("Comment markdown, or - for stdin")
17945
18156
  },
17946
- run: async ({ positionals: { id, body } }) => {
18157
+ options: {
18158
+ attach: exports_external.array(exports_external.string()).optional().describe("Attach a file to the issue (repeatable)").meta({ short: "a" })
18159
+ },
18160
+ run: async ({ positionals: { id, body }, options: { attach } }) => {
17947
18161
  const client3 = await backendClient();
17948
18162
  const issue2 = await resolveIssue(client3, id);
18163
+ printAttachments(await attachFiles(client3, issue2._id, attach ?? []));
17949
18164
  await client3.mutation(api2.issueComments.create, { issueId: issue2._id, bodyMarkdown: await readTextOption(body) });
17950
18165
  console.log(`Commented on ${issue2.identifier}.`);
17951
18166
  }
@@ -17993,35 +18208,6 @@ var create = command({
17993
18208
  });
17994
18209
 
17995
18210
  // src/commands/issues/get.ts
17996
- var activityLine = (detail) => {
17997
- const label = detail.kind.replaceAll("_", " ");
17998
- switch (detail.kind) {
17999
- case "created":
18000
- case "description_changed":
18001
- return label;
18002
- case "title_changed":
18003
- case "status_changed":
18004
- case "priority_changed":
18005
- case "disposition_changed":
18006
- case "estimate_changed":
18007
- case "due_date_changed":
18008
- case "project_changed":
18009
- case "milestone_changed":
18010
- case "parent_changed":
18011
- return `${label}: ${detail.from ?? "none"} \u2192 ${detail.to ?? "none"}`;
18012
- case "label_added":
18013
- case "label_removed":
18014
- case "attachment_added":
18015
- case "attachment_removed":
18016
- return `${label}: ${detail.name}`;
18017
- case "child_added":
18018
- case "child_removed":
18019
- return `${label}: ${detail.identifier}`;
18020
- case "relation_added":
18021
- case "relation_removed":
18022
- return `${label}: ${detail.relation.replaceAll("_", " ")} ${detail.identifier}`;
18023
- }
18024
- };
18025
18211
  var get = command({
18026
18212
  name: "get",
18027
18213
  description: "Show an issue: properties, description, and its feed",
@@ -18034,9 +18220,13 @@ var get = command({
18034
18220
  run: async ({ positionals: { id }, options: { json: json2 } }) => {
18035
18221
  const client3 = await backendClient();
18036
18222
  const issue2 = await resolveIssue(client3, id);
18037
- const feed = await client3.query(api2.issues.feed, { id: issue2._id });
18223
+ const [feed, relations, attachments] = await Promise.all([
18224
+ client3.query(api2.issues.feed, { id: issue2._id }),
18225
+ client3.query(api2.issues.relations, { id: issue2._id }),
18226
+ client3.query(api2.issueAttachments.list, { issueId: issue2._id })
18227
+ ]);
18038
18228
  if (json2)
18039
- return console.log(JSON.stringify({ ...issue2, feed }, null, 2));
18229
+ return console.log(JSON.stringify({ ...issue2, relations, attachments, feed }, null, 2));
18040
18230
  const project = issue2.projectId ? await client3.query(api2.projects.get, { id: issue2.projectId, today: localToday() }) : null;
18041
18231
  const milestone = issue2.milestoneId ? await client3.query(api2.milestones.get, { id: issue2.milestoneId, today: localToday() }) : null;
18042
18232
  console.log(`${issue2.identifier} ${issue2.title}`);
@@ -18045,6 +18235,10 @@ var get = command({
18045
18235
  console.log(`Estimate: ${issue2.estimate}`);
18046
18236
  if (issue2.dueDate)
18047
18237
  console.log(`Due: ${issue2.dueDate}`);
18238
+ if (issue2.branch)
18239
+ console.log(`Branch: ${issue2.branch}`);
18240
+ if (issue2.prUrl)
18241
+ console.log(`PR: ${issue2.prUrl}`);
18048
18242
  if (project)
18049
18243
  console.log(`Project: ${project.name}${milestone ? ` \xB7 Milestone: ${milestone.name}` : ""}`);
18050
18244
  if (issue2.parent)
@@ -18053,6 +18247,23 @@ var get = command({
18053
18247
  console.log(`Labels: ${issue2.labels.map((label) => label.name).join(", ")}`);
18054
18248
  if (issue2.children.total > 0)
18055
18249
  console.log(`Sub-issues: ${issue2.children.done}/${issue2.children.total} done`);
18250
+ const related = (entries) => entries.map((entry) => `${entry.issue.identifier} (${entry.issue.status})`).join(", ");
18251
+ if (relations.blockedBy.length > 0)
18252
+ console.log(`Blocked by: ${related(relations.blockedBy)}`);
18253
+ if (relations.blocks.length > 0)
18254
+ console.log(`Blocks: ${related(relations.blocks)}`);
18255
+ if (relations.duplicateOf)
18256
+ console.log(`Duplicate of: ${related([relations.duplicateOf])}`);
18257
+ if (relations.duplicates.length > 0)
18258
+ console.log(`Duplicated by: ${related(relations.duplicates)}`);
18259
+ if (relations.relatesTo.length > 0)
18260
+ console.log(`Related: ${related(relations.relatesTo)}`);
18261
+ if (attachments.length > 0) {
18262
+ console.log("Attachments:");
18263
+ for (const attachment of attachments) {
18264
+ console.log(` ${attachment.name} \u2014 ${attachment.url ?? "unavailable"}`);
18265
+ }
18266
+ }
18056
18267
  if (issue2.descriptionMarkdown)
18057
18268
  console.log(`
18058
18269
  ${issue2.descriptionMarkdown}`);
@@ -18064,7 +18275,8 @@ Feed:`);
18064
18275
  for (const event of feed.events) {
18065
18276
  const at = new Date(event.at).toISOString().slice(0, 16).replace("T", " ");
18066
18277
  if (event.type === "activity") {
18067
- console.log(`${at} ${activityLine(event.detail)}`);
18278
+ const via = event.via === undefined ? "" : ` (via ${SOURCE_LABELS[event.via]})`;
18279
+ console.log(`${at} ${issueActivityLine(event.detail)}${via}`);
18068
18280
  } else {
18069
18281
  console.log(`${at} comment${event.editedAt ? " (edited)" : ""}:`);
18070
18282
  for (const line of event.bodyMarkdown.split(`
@@ -18075,17 +18287,6 @@ Feed:`);
18075
18287
  }
18076
18288
  });
18077
18289
 
18078
- // src/lib/output.ts
18079
- var printTable = (rows) => {
18080
- if (rows.length === 0)
18081
- return;
18082
- const columnCount = Math.max(...rows.map((row) => row.length));
18083
- const widths = Array.from({ length: columnCount }, (_, column) => Math.max(...rows.map((row) => (row[column] ?? "").length)));
18084
- for (const row of rows) {
18085
- console.log(row.map((cell, column) => cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd());
18086
- }
18087
- };
18088
-
18089
18290
  // src/commands/issues/list.ts
18090
18291
  var list = command({
18091
18292
  name: "list",
@@ -18155,6 +18356,24 @@ More issues match; raise --limit past ${options.limit}.`);
18155
18356
  }
18156
18357
  });
18157
18358
 
18359
+ // src/commands/issues/relate.ts
18360
+ var relate = command({
18361
+ name: "relate",
18362
+ description: "Relate two issues, stated from the first one's side (duplicate_of also cancels it)",
18363
+ positionals: {
18364
+ id: exports_external.string().describe("Issue identifier, number, or URL"),
18365
+ kind: exports_external.enum(ISSUE_RELATION_KINDS).describe(`How the first issue relates to the second (${ISSUE_RELATION_KINDS.join("|")})`),
18366
+ other: exports_external.string().describe("The other issue's identifier, number, or URL")
18367
+ },
18368
+ run: async ({ positionals: { id, kind, other } }) => {
18369
+ const client3 = await backendClient();
18370
+ const issue2 = await resolveIssue(client3, id);
18371
+ const otherIssue = await resolveIssue(client3, other);
18372
+ await client3.mutation(api2.issues.addRelation, { id: issue2._id, kind, otherIssueId: otherIssue._id });
18373
+ console.log(`${issue2.identifier} ${kind.replaceAll("_", " ")} ${otherIssue.identifier}.`);
18374
+ }
18375
+ });
18376
+
18158
18377
  // src/commands/issues/remove.ts
18159
18378
  var remove = command({
18160
18379
  name: "delete",
@@ -18196,7 +18415,7 @@ var route = command({
18196
18415
  // src/commands/issues/set.ts
18197
18416
  var set2 = command({
18198
18417
  name: "set",
18199
- description: "Set an issue's status, priority, estimate, due date, project, milestone, or parent",
18418
+ description: "Set an issue's status, priority, estimate, due date, project, milestone, parent, or labels",
18200
18419
  positionals: {
18201
18420
  id: exports_external.string().describe("Issue identifier, number, or URL")
18202
18421
  },
@@ -18207,12 +18426,14 @@ var set2 = command({
18207
18426
  due: exports_external.iso.date().nullable().optional().describe("Due date (YYYY-MM-DD), or --no-due to clear").meta({ negatable: true }),
18208
18427
  project: exports_external.string().nullable().optional().describe("Move to this project (id or URL), or --no-project to make the issue projectless").meta({ negatable: true }),
18209
18428
  milestone: exports_external.string().nullable().optional().describe("Move to this milestone of the issue's project (name), or --no-milestone to clear").meta({ negatable: true }),
18210
- parent: exports_external.string().nullable().optional().describe("Make this a sub-issue of another issue, or --no-parent to promote it").meta({ negatable: true })
18429
+ parent: exports_external.string().nullable().optional().describe("Make this a sub-issue of another issue, or --no-parent to promote it").meta({ negatable: true }),
18430
+ label: exports_external.array(exports_external.string()).optional().describe("Add a label by name (repeatable)"),
18431
+ "no-label": exports_external.array(exports_external.string()).optional().describe("Remove a label by name (repeatable)")
18211
18432
  },
18212
18433
  run: async ({ positionals: { id }, options }) => {
18213
- const { status: status2, priority, estimate, due, project, milestone, parent } = options;
18434
+ const { status: status2, priority, estimate, due, project, milestone, parent, label, "no-label": noLabel } = options;
18214
18435
  if (Object.values(options).every((value) => value === undefined))
18215
- throw new Error("Nothing to set. Pass --status, --priority, --estimate, --due, --project, --milestone, or --parent.");
18436
+ throw new Error("Nothing to set. Pass --status, --priority, --estimate, --due, --project, --milestone, --parent, or --label.");
18216
18437
  const client3 = await backendClient();
18217
18438
  const issue2 = await resolveIssue(client3, id);
18218
18439
  const projectId = project === undefined ? issue2.projectId : project === null ? undefined : (await resolveProject(client3, project))._id;
@@ -18225,8 +18446,14 @@ var set2 = command({
18225
18446
  milestoneId = (await resolveMilestone(client3, projectId, milestone))._id;
18226
18447
  }
18227
18448
  const parentId = parent == null ? undefined : (await resolveIssue(client3, parent))._id;
18449
+ const addLabels = await resolveIssueLabels(client3, label ?? []);
18450
+ const removeLabels = await resolveIssueLabels(client3, noLabel ?? []);
18228
18451
  if (parent !== undefined)
18229
18452
  await client3.mutation(api2.issues.setParent, { id: issue2._id, parentId });
18453
+ for (const { _id } of addLabels)
18454
+ await client3.mutation(api2.issues.addLabel, { issueId: issue2._id, labelId: _id });
18455
+ for (const { _id } of removeLabels)
18456
+ await client3.mutation(api2.issues.removeLabel, { issueId: issue2._id, labelId: _id });
18230
18457
  if (status2 !== undefined)
18231
18458
  await client3.mutation(api2.issues.setStatus, { id: issue2._id, status: status2 });
18232
18459
  if (priority !== undefined)
@@ -18241,27 +18468,86 @@ var set2 = command({
18241
18468
  }
18242
18469
  });
18243
18470
 
18471
+ // src/commands/issues/start.ts
18472
+ var shellArg = (value) => /^[\w./-]+$/.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
18473
+ var start = command({
18474
+ name: "start",
18475
+ description: "Claim an issue: move it to in_progress and record the branch to work under",
18476
+ positionals: {
18477
+ id: exports_external.string().describe("Issue identifier, number, or URL")
18478
+ },
18479
+ run: async ({ positionals: { id } }) => {
18480
+ const client3 = await backendClient();
18481
+ const issue2 = await resolveIssue(client3, id);
18482
+ const { branch } = await client3.mutation(api2.issues.start, { id: issue2._id });
18483
+ console.log(`Started ${issue2.identifier}: in_progress, branch ${branch}`);
18484
+ console.log(`
18485
+ git switch -c ${shellArg(branch)}`);
18486
+ }
18487
+ });
18488
+
18489
+ // src/commands/issues/unrelate.ts
18490
+ var relationEntries = (relations, kind) => {
18491
+ switch (kind) {
18492
+ case "blocks":
18493
+ return relations.blocks;
18494
+ case "blocked_by":
18495
+ return relations.blockedBy;
18496
+ case "duplicate_of":
18497
+ return relations.duplicateOf ? [relations.duplicateOf] : [];
18498
+ case "duplicated_by":
18499
+ return relations.duplicates;
18500
+ case "relates_to":
18501
+ return relations.relatesTo;
18502
+ }
18503
+ };
18504
+ var unrelate = command({
18505
+ name: "unrelate",
18506
+ description: "Remove a relation between two issues (a canceled duplicate stays canceled)",
18507
+ positionals: {
18508
+ id: exports_external.string().describe("Issue identifier, number, or URL"),
18509
+ kind: exports_external.enum(ISSUE_RELATION_KINDS).describe(`How the first issue relates to the second (${ISSUE_RELATION_KINDS.join("|")})`),
18510
+ other: exports_external.string().describe("The other issue's identifier, number, or URL")
18511
+ },
18512
+ run: async ({ positionals: { id, kind, other } }) => {
18513
+ const client3 = await backendClient();
18514
+ const issue2 = await resolveIssue(client3, id);
18515
+ const otherIssue = await resolveIssue(client3, other);
18516
+ const relations = await client3.query(api2.issues.relations, { id: issue2._id });
18517
+ const relation = relationEntries(relations, kind).find((entry) => entry.issue._id === otherIssue._id);
18518
+ const label = kind.replaceAll("_", " ");
18519
+ if (!relation)
18520
+ throw new Error(`${issue2.identifier} has no "${label}" relation to ${otherIssue.identifier}.`);
18521
+ await client3.mutation(api2.issues.removeRelation, { id: relation._id });
18522
+ console.log(`Removed: ${issue2.identifier} ${label} ${otherIssue.identifier}.`);
18523
+ }
18524
+ });
18525
+
18244
18526
  // src/commands/issues/update.ts
18245
18527
  var update = command({
18246
18528
  name: "update",
18247
- description: "Rewrite an issue's title or description",
18529
+ description: "Rewrite an issue's title or description, or attach files",
18248
18530
  positionals: {
18249
18531
  id: exports_external.string().describe("Issue identifier, number, or URL")
18250
18532
  },
18251
18533
  options: {
18252
18534
  title: exports_external.string().optional().describe("New title").meta({ short: "t" }),
18253
- description: exports_external.string().optional().describe("New markdown description, or - for stdin").meta({ short: "d" })
18535
+ description: exports_external.string().optional().describe("New markdown description, or - for stdin").meta({ short: "d" }),
18536
+ attach: exports_external.array(exports_external.string()).optional().describe("Attach a file to the issue (repeatable)").meta({ short: "a" })
18254
18537
  },
18255
- run: async ({ positionals: { id }, options: { title, description } }) => {
18256
- if (title === undefined && description === undefined)
18257
- throw new Error("Nothing to update. Pass --title or --description.");
18538
+ run: async ({ positionals: { id }, options: { title, description, attach } }) => {
18539
+ if (title === undefined && description === undefined && attach === undefined)
18540
+ throw new Error("Nothing to update. Pass --title, --description, or --attach.");
18258
18541
  const client3 = await backendClient();
18259
18542
  const issue2 = await resolveIssue(client3, id);
18260
- await client3.mutation(api2.issues.update, {
18261
- id: issue2._id,
18262
- title,
18263
- descriptionMarkdown: description === undefined ? undefined : await readTextOption(description)
18264
- });
18543
+ printAttachments(await attachFiles(client3, issue2._id, attach ?? []));
18544
+ if (title !== undefined || description !== undefined) {
18545
+ await client3.mutation(api2.issues.update, {
18546
+ id: issue2._id,
18547
+ title,
18548
+ descriptionMarkdown: description === undefined ? undefined : await readTextOption(description)
18549
+ });
18550
+ }
18265
18551
  console.log(`Updated ${issue2.identifier}.`);
18266
18552
  }
18267
18553
  });
@@ -18270,11 +18556,11 @@ var update = command({
18270
18556
  var issues = group({
18271
18557
  name: "issues",
18272
18558
  description: "Track and triage issues",
18273
- commands: [create, list, get, update, set2, route, comment, remove]
18559
+ commands: [create, list, get, update, set2, route, start, relate, unrelate, comment, remove]
18274
18560
  });
18275
18561
 
18276
18562
  // src/lib/group.ts
18277
- import { basename } from "path";
18563
+ import { basename as basename2 } from "path";
18278
18564
  var git = async (...args) => {
18279
18565
  const proc = Bun.spawn(["git", ...args], { stdout: "pipe", stderr: "ignore" });
18280
18566
  const [output, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
@@ -18286,7 +18572,7 @@ var detectRepoGroup = async () => {
18286
18572
  if (remote)
18287
18573
  return repoNameFromRemote(remote);
18288
18574
  const root = await git("rev-parse", "--show-toplevel");
18289
- return root ? basename(root) : undefined;
18575
+ return root ? basename2(root) : undefined;
18290
18576
  };
18291
18577
  var groupForCreate = async (group2) => group2 === null ? undefined : group2 ?? await detectRepoGroup();
18292
18578
 
@@ -18694,7 +18980,7 @@ var upgrade = command({
18694
18980
  var rootCommand = group({
18695
18981
  name: "kds",
18696
18982
  description: "KDS CLI",
18697
- commands: [auth, issues, pages, project, projects, upgrade],
18983
+ commands: [auth, inbox, issues, pages, project, projects, upgrade],
18698
18984
  options: {
18699
18985
  version: exports_external.boolean().default(false).describe("Show the version").meta({ short: "v" })
18700
18986
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@islamihab/kds",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Command-line client for Kai Dev Studio",
5
5
  "license": "MIT",
6
6
  "publishConfig": {