@islamihab/kds 0.2.0 → 0.2.1

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 +219 -25
  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.1",
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,59 @@ var auth = group({
17814
17823
  commands: [login, logout, status]
17815
17824
  });
17816
17825
 
17826
+ // src/lib/attachments.ts
17827
+ import { basename } from "path";
17828
+ var attachFiles = async (client3, issueId, paths) => {
17829
+ if (paths.length === 0)
17830
+ return [];
17831
+ const files = paths.map((path) => ({ path, file: Bun.file(path) }));
17832
+ for (const { path, file: file2 } of files) {
17833
+ if (!await file2.exists())
17834
+ throw new Error(`No file at ${path}.`);
17835
+ if (file2.size <= 0)
17836
+ throw new Error(`${path} is empty; an attachment cannot be.`);
17837
+ if (file2.size > MAX_ISSUE_ATTACHMENT_BYTES) {
17838
+ throw new Error(`${path} is too large (${file2.size} bytes; max ${MAX_ISSUE_ATTACHMENT_BYTES}).`);
17839
+ }
17840
+ }
17841
+ const created = new Set;
17842
+ try {
17843
+ for (const { path, file: file2 } of files) {
17844
+ const uploadUrl = await client3.mutation(api2.issueAttachments.generateUploadUrl, { issueId });
17845
+ const response = await fetch(uploadUrl, {
17846
+ method: "POST",
17847
+ headers: { "Content-Type": file2.type },
17848
+ body: file2
17849
+ });
17850
+ if (!response.ok)
17851
+ throw new Error(`Uploading ${path} failed (${response.status}).`);
17852
+ const storageId = exports_external.custom((value) => typeof value === "string");
17853
+ const body = exports_external.object({ storageId }).safeParse(await response.json());
17854
+ if (!body.success)
17855
+ throw new Error("The upload returned no file reference.");
17856
+ created.add(await client3.action(api2.issueAttachments.create, {
17857
+ issueId,
17858
+ storageId: body.data.storageId,
17859
+ name: basename(path)
17860
+ }));
17861
+ }
17862
+ } catch (error51) {
17863
+ for (const id of created) {
17864
+ await client3.mutation(api2.issueAttachments.remove, { id }).catch(() => {
17865
+ console.error("An attachment from this failed batch could not be removed; check the dashboard.");
17866
+ });
17867
+ }
17868
+ throw error51;
17869
+ }
17870
+ const attachments = await client3.query(api2.issueAttachments.list, { issueId });
17871
+ return attachments.filter((attachment) => created.has(attachment._id));
17872
+ };
17873
+ var printAttachments = (attachments) => {
17874
+ for (const attachment of attachments) {
17875
+ console.log(`Attached ${attachment.name}${attachment.url ? ` \u2014 ${attachment.url}` : ""}`);
17876
+ }
17877
+ };
17878
+
17817
17879
  // src/lib/input.ts
17818
17880
  var assertSize = (size) => {
17819
17881
  if (size > MAX_PAGE_HTML_BYTES)
@@ -17924,6 +17986,20 @@ var repoProject = async (client3) => {
17924
17986
  throw new Error(`No project is connected to ${repo}. Connect one on the project in the dashboard.`);
17925
17987
  return project;
17926
17988
  };
17989
+ var resolveIssueLabels = async (client3, names) => {
17990
+ if (names.length === 0)
17991
+ return [];
17992
+ const labels = await client3.query(api2.issueLabels.list, {});
17993
+ return names.map((name) => {
17994
+ const wanted = name.trim().toLowerCase();
17995
+ const label = labels.find((candidate) => candidate.name.toLowerCase() === wanted);
17996
+ if (!label) {
17997
+ const available = labels.map((candidate) => candidate.name).join(", ");
17998
+ throw new Error(available ? `No label named "${name}". The labels are: ${available}.` : "There are no labels yet.");
17999
+ }
18000
+ return label;
18001
+ });
18002
+ };
17927
18003
  var resolveMilestone = async (client3, projectId, name) => {
17928
18004
  const milestones = await client3.query(api2.milestones.listByProject, { projectId, today: localToday() });
17929
18005
  const wanted = name.trim().toLowerCase();
@@ -17943,9 +18019,13 @@ var comment = command({
17943
18019
  id: exports_external.string().describe("Issue identifier, number, or URL"),
17944
18020
  body: exports_external.string().describe("Comment markdown, or - for stdin")
17945
18021
  },
17946
- run: async ({ positionals: { id, body } }) => {
18022
+ options: {
18023
+ attach: exports_external.array(exports_external.string()).optional().describe("Attach a file to the issue (repeatable)").meta({ short: "a" })
18024
+ },
18025
+ run: async ({ positionals: { id, body }, options: { attach } }) => {
17947
18026
  const client3 = await backendClient();
17948
18027
  const issue2 = await resolveIssue(client3, id);
18028
+ printAttachments(await attachFiles(client3, issue2._id, attach ?? []));
17949
18029
  await client3.mutation(api2.issueComments.create, { issueId: issue2._id, bodyMarkdown: await readTextOption(body) });
17950
18030
  console.log(`Commented on ${issue2.identifier}.`);
17951
18031
  }
@@ -17993,6 +18073,7 @@ var create = command({
17993
18073
  });
17994
18074
 
17995
18075
  // src/commands/issues/get.ts
18076
+ var SOURCE_LABELS = { github: "GitHub" };
17996
18077
  var activityLine = (detail) => {
17997
18078
  const label = detail.kind.replaceAll("_", " ");
17998
18079
  switch (detail.kind) {
@@ -18005,6 +18086,8 @@ var activityLine = (detail) => {
18005
18086
  case "disposition_changed":
18006
18087
  case "estimate_changed":
18007
18088
  case "due_date_changed":
18089
+ case "branch_changed":
18090
+ case "pr_changed":
18008
18091
  case "project_changed":
18009
18092
  case "milestone_changed":
18010
18093
  case "parent_changed":
@@ -18034,9 +18117,13 @@ var get = command({
18034
18117
  run: async ({ positionals: { id }, options: { json: json2 } }) => {
18035
18118
  const client3 = await backendClient();
18036
18119
  const issue2 = await resolveIssue(client3, id);
18037
- const feed = await client3.query(api2.issues.feed, { id: issue2._id });
18120
+ const [feed, relations, attachments] = await Promise.all([
18121
+ client3.query(api2.issues.feed, { id: issue2._id }),
18122
+ client3.query(api2.issues.relations, { id: issue2._id }),
18123
+ client3.query(api2.issueAttachments.list, { issueId: issue2._id })
18124
+ ]);
18038
18125
  if (json2)
18039
- return console.log(JSON.stringify({ ...issue2, feed }, null, 2));
18126
+ return console.log(JSON.stringify({ ...issue2, relations, attachments, feed }, null, 2));
18040
18127
  const project = issue2.projectId ? await client3.query(api2.projects.get, { id: issue2.projectId, today: localToday() }) : null;
18041
18128
  const milestone = issue2.milestoneId ? await client3.query(api2.milestones.get, { id: issue2.milestoneId, today: localToday() }) : null;
18042
18129
  console.log(`${issue2.identifier} ${issue2.title}`);
@@ -18045,6 +18132,10 @@ var get = command({
18045
18132
  console.log(`Estimate: ${issue2.estimate}`);
18046
18133
  if (issue2.dueDate)
18047
18134
  console.log(`Due: ${issue2.dueDate}`);
18135
+ if (issue2.branch)
18136
+ console.log(`Branch: ${issue2.branch}`);
18137
+ if (issue2.prUrl)
18138
+ console.log(`PR: ${issue2.prUrl}`);
18048
18139
  if (project)
18049
18140
  console.log(`Project: ${project.name}${milestone ? ` \xB7 Milestone: ${milestone.name}` : ""}`);
18050
18141
  if (issue2.parent)
@@ -18053,6 +18144,23 @@ var get = command({
18053
18144
  console.log(`Labels: ${issue2.labels.map((label) => label.name).join(", ")}`);
18054
18145
  if (issue2.children.total > 0)
18055
18146
  console.log(`Sub-issues: ${issue2.children.done}/${issue2.children.total} done`);
18147
+ const related = (entries) => entries.map((entry) => `${entry.issue.identifier} (${entry.issue.status})`).join(", ");
18148
+ if (relations.blockedBy.length > 0)
18149
+ console.log(`Blocked by: ${related(relations.blockedBy)}`);
18150
+ if (relations.blocks.length > 0)
18151
+ console.log(`Blocks: ${related(relations.blocks)}`);
18152
+ if (relations.duplicateOf)
18153
+ console.log(`Duplicate of: ${related([relations.duplicateOf])}`);
18154
+ if (relations.duplicates.length > 0)
18155
+ console.log(`Duplicated by: ${related(relations.duplicates)}`);
18156
+ if (relations.relatesTo.length > 0)
18157
+ console.log(`Related: ${related(relations.relatesTo)}`);
18158
+ if (attachments.length > 0) {
18159
+ console.log("Attachments:");
18160
+ for (const attachment of attachments) {
18161
+ console.log(` ${attachment.name} \u2014 ${attachment.url ?? "unavailable"}`);
18162
+ }
18163
+ }
18056
18164
  if (issue2.descriptionMarkdown)
18057
18165
  console.log(`
18058
18166
  ${issue2.descriptionMarkdown}`);
@@ -18064,7 +18172,8 @@ Feed:`);
18064
18172
  for (const event of feed.events) {
18065
18173
  const at = new Date(event.at).toISOString().slice(0, 16).replace("T", " ");
18066
18174
  if (event.type === "activity") {
18067
- console.log(`${at} ${activityLine(event.detail)}`);
18175
+ const via = event.via === undefined ? "" : ` (via ${SOURCE_LABELS[event.via]})`;
18176
+ console.log(`${at} ${activityLine(event.detail)}${via}`);
18068
18177
  } else {
18069
18178
  console.log(`${at} comment${event.editedAt ? " (edited)" : ""}:`);
18070
18179
  for (const line of event.bodyMarkdown.split(`
@@ -18155,6 +18264,24 @@ More issues match; raise --limit past ${options.limit}.`);
18155
18264
  }
18156
18265
  });
18157
18266
 
18267
+ // src/commands/issues/relate.ts
18268
+ var relate = command({
18269
+ name: "relate",
18270
+ description: "Relate two issues, stated from the first one's side (duplicate_of also cancels it)",
18271
+ positionals: {
18272
+ id: exports_external.string().describe("Issue identifier, number, or URL"),
18273
+ kind: exports_external.enum(ISSUE_RELATION_KINDS).describe(`How the first issue relates to the second (${ISSUE_RELATION_KINDS.join("|")})`),
18274
+ other: exports_external.string().describe("The other issue's identifier, number, or URL")
18275
+ },
18276
+ run: async ({ positionals: { id, kind, other } }) => {
18277
+ const client3 = await backendClient();
18278
+ const issue2 = await resolveIssue(client3, id);
18279
+ const otherIssue = await resolveIssue(client3, other);
18280
+ await client3.mutation(api2.issues.addRelation, { id: issue2._id, kind, otherIssueId: otherIssue._id });
18281
+ console.log(`${issue2.identifier} ${kind.replaceAll("_", " ")} ${otherIssue.identifier}.`);
18282
+ }
18283
+ });
18284
+
18158
18285
  // src/commands/issues/remove.ts
18159
18286
  var remove = command({
18160
18287
  name: "delete",
@@ -18196,7 +18323,7 @@ var route = command({
18196
18323
  // src/commands/issues/set.ts
18197
18324
  var set2 = command({
18198
18325
  name: "set",
18199
- description: "Set an issue's status, priority, estimate, due date, project, milestone, or parent",
18326
+ description: "Set an issue's status, priority, estimate, due date, project, milestone, parent, or labels",
18200
18327
  positionals: {
18201
18328
  id: exports_external.string().describe("Issue identifier, number, or URL")
18202
18329
  },
@@ -18207,12 +18334,14 @@ var set2 = command({
18207
18334
  due: exports_external.iso.date().nullable().optional().describe("Due date (YYYY-MM-DD), or --no-due to clear").meta({ negatable: true }),
18208
18335
  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
18336
  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 })
18337
+ parent: exports_external.string().nullable().optional().describe("Make this a sub-issue of another issue, or --no-parent to promote it").meta({ negatable: true }),
18338
+ label: exports_external.array(exports_external.string()).optional().describe("Add a label by name (repeatable)"),
18339
+ "no-label": exports_external.array(exports_external.string()).optional().describe("Remove a label by name (repeatable)")
18211
18340
  },
18212
18341
  run: async ({ positionals: { id }, options }) => {
18213
- const { status: status2, priority, estimate, due, project, milestone, parent } = options;
18342
+ const { status: status2, priority, estimate, due, project, milestone, parent, label, "no-label": noLabel } = options;
18214
18343
  if (Object.values(options).every((value) => value === undefined))
18215
- throw new Error("Nothing to set. Pass --status, --priority, --estimate, --due, --project, --milestone, or --parent.");
18344
+ throw new Error("Nothing to set. Pass --status, --priority, --estimate, --due, --project, --milestone, --parent, or --label.");
18216
18345
  const client3 = await backendClient();
18217
18346
  const issue2 = await resolveIssue(client3, id);
18218
18347
  const projectId = project === undefined ? issue2.projectId : project === null ? undefined : (await resolveProject(client3, project))._id;
@@ -18225,8 +18354,14 @@ var set2 = command({
18225
18354
  milestoneId = (await resolveMilestone(client3, projectId, milestone))._id;
18226
18355
  }
18227
18356
  const parentId = parent == null ? undefined : (await resolveIssue(client3, parent))._id;
18357
+ const addLabels = await resolveIssueLabels(client3, label ?? []);
18358
+ const removeLabels = await resolveIssueLabels(client3, noLabel ?? []);
18228
18359
  if (parent !== undefined)
18229
18360
  await client3.mutation(api2.issues.setParent, { id: issue2._id, parentId });
18361
+ for (const { _id } of addLabels)
18362
+ await client3.mutation(api2.issues.addLabel, { issueId: issue2._id, labelId: _id });
18363
+ for (const { _id } of removeLabels)
18364
+ await client3.mutation(api2.issues.removeLabel, { issueId: issue2._id, labelId: _id });
18230
18365
  if (status2 !== undefined)
18231
18366
  await client3.mutation(api2.issues.setStatus, { id: issue2._id, status: status2 });
18232
18367
  if (priority !== undefined)
@@ -18241,27 +18376,86 @@ var set2 = command({
18241
18376
  }
18242
18377
  });
18243
18378
 
18379
+ // src/commands/issues/start.ts
18380
+ var shellArg = (value) => /^[\w./-]+$/.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
18381
+ var start = command({
18382
+ name: "start",
18383
+ description: "Claim an issue: move it to in_progress and record the branch to work under",
18384
+ positionals: {
18385
+ id: exports_external.string().describe("Issue identifier, number, or URL")
18386
+ },
18387
+ run: async ({ positionals: { id } }) => {
18388
+ const client3 = await backendClient();
18389
+ const issue2 = await resolveIssue(client3, id);
18390
+ const { branch } = await client3.mutation(api2.issues.start, { id: issue2._id });
18391
+ console.log(`Started ${issue2.identifier}: in_progress, branch ${branch}`);
18392
+ console.log(`
18393
+ git switch -c ${shellArg(branch)}`);
18394
+ }
18395
+ });
18396
+
18397
+ // src/commands/issues/unrelate.ts
18398
+ var relationEntries = (relations, kind) => {
18399
+ switch (kind) {
18400
+ case "blocks":
18401
+ return relations.blocks;
18402
+ case "blocked_by":
18403
+ return relations.blockedBy;
18404
+ case "duplicate_of":
18405
+ return relations.duplicateOf ? [relations.duplicateOf] : [];
18406
+ case "duplicated_by":
18407
+ return relations.duplicates;
18408
+ case "relates_to":
18409
+ return relations.relatesTo;
18410
+ }
18411
+ };
18412
+ var unrelate = command({
18413
+ name: "unrelate",
18414
+ description: "Remove a relation between two issues (a canceled duplicate stays canceled)",
18415
+ positionals: {
18416
+ id: exports_external.string().describe("Issue identifier, number, or URL"),
18417
+ kind: exports_external.enum(ISSUE_RELATION_KINDS).describe(`How the first issue relates to the second (${ISSUE_RELATION_KINDS.join("|")})`),
18418
+ other: exports_external.string().describe("The other issue's identifier, number, or URL")
18419
+ },
18420
+ run: async ({ positionals: { id, kind, other } }) => {
18421
+ const client3 = await backendClient();
18422
+ const issue2 = await resolveIssue(client3, id);
18423
+ const otherIssue = await resolveIssue(client3, other);
18424
+ const relations = await client3.query(api2.issues.relations, { id: issue2._id });
18425
+ const relation = relationEntries(relations, kind).find((entry) => entry.issue._id === otherIssue._id);
18426
+ const label = kind.replaceAll("_", " ");
18427
+ if (!relation)
18428
+ throw new Error(`${issue2.identifier} has no "${label}" relation to ${otherIssue.identifier}.`);
18429
+ await client3.mutation(api2.issues.removeRelation, { id: relation._id });
18430
+ console.log(`Removed: ${issue2.identifier} ${label} ${otherIssue.identifier}.`);
18431
+ }
18432
+ });
18433
+
18244
18434
  // src/commands/issues/update.ts
18245
18435
  var update = command({
18246
18436
  name: "update",
18247
- description: "Rewrite an issue's title or description",
18437
+ description: "Rewrite an issue's title or description, or attach files",
18248
18438
  positionals: {
18249
18439
  id: exports_external.string().describe("Issue identifier, number, or URL")
18250
18440
  },
18251
18441
  options: {
18252
18442
  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" })
18443
+ description: exports_external.string().optional().describe("New markdown description, or - for stdin").meta({ short: "d" }),
18444
+ attach: exports_external.array(exports_external.string()).optional().describe("Attach a file to the issue (repeatable)").meta({ short: "a" })
18254
18445
  },
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.");
18446
+ run: async ({ positionals: { id }, options: { title, description, attach } }) => {
18447
+ if (title === undefined && description === undefined && attach === undefined)
18448
+ throw new Error("Nothing to update. Pass --title, --description, or --attach.");
18258
18449
  const client3 = await backendClient();
18259
18450
  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
- });
18451
+ printAttachments(await attachFiles(client3, issue2._id, attach ?? []));
18452
+ if (title !== undefined || description !== undefined) {
18453
+ await client3.mutation(api2.issues.update, {
18454
+ id: issue2._id,
18455
+ title,
18456
+ descriptionMarkdown: description === undefined ? undefined : await readTextOption(description)
18457
+ });
18458
+ }
18265
18459
  console.log(`Updated ${issue2.identifier}.`);
18266
18460
  }
18267
18461
  });
@@ -18270,11 +18464,11 @@ var update = command({
18270
18464
  var issues = group({
18271
18465
  name: "issues",
18272
18466
  description: "Track and triage issues",
18273
- commands: [create, list, get, update, set2, route, comment, remove]
18467
+ commands: [create, list, get, update, set2, route, start, relate, unrelate, comment, remove]
18274
18468
  });
18275
18469
 
18276
18470
  // src/lib/group.ts
18277
- import { basename } from "path";
18471
+ import { basename as basename2 } from "path";
18278
18472
  var git = async (...args) => {
18279
18473
  const proc = Bun.spawn(["git", ...args], { stdout: "pipe", stderr: "ignore" });
18280
18474
  const [output, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
@@ -18286,7 +18480,7 @@ var detectRepoGroup = async () => {
18286
18480
  if (remote)
18287
18481
  return repoNameFromRemote(remote);
18288
18482
  const root = await git("rev-parse", "--show-toplevel");
18289
- return root ? basename(root) : undefined;
18483
+ return root ? basename2(root) : undefined;
18290
18484
  };
18291
18485
  var groupForCreate = async (group2) => group2 === null ? undefined : group2 ?? await detectRepoGroup();
18292
18486
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@islamihab/kds",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Command-line client for Kai Dev Studio",
5
5
  "license": "MIT",
6
6
  "publishConfig": {