@islamihab/kds 0.1.4 → 0.2.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 +672 -77
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -14587,7 +14587,7 @@ import { join } from "path";
14587
14587
  // package.json
14588
14588
  var package_default = {
14589
14589
  name: "cli",
14590
- version: "0.1.4",
14590
+ version: "0.2.0",
14591
14591
  private: true,
14592
14592
  type: "module",
14593
14593
  bin: {
@@ -15143,6 +15143,28 @@ var KDS_DEVICE_AUTH_CLIENT_ID = "kds-cli";
15143
15143
  var PAGE_MODES = ["themed", "raw"];
15144
15144
  var PAGE_VISIBILITIES = ["public", "private"];
15145
15145
  var MAX_PAGE_HTML_BYTES = 4000000;
15146
+ var ISSUE_STATUSES = ["backlog", "todo", "in_progress", "done", "canceled"];
15147
+ var ISSUE_PRIORITIES = ["urgent", "high", "medium", "low", "none"];
15148
+ var ISSUE_DISPOSITIONS = [
15149
+ "needs_triage",
15150
+ "needs_info",
15151
+ "ready_for_agent",
15152
+ "ready_for_human",
15153
+ "wontfix"
15154
+ ];
15155
+ var ISSUE_CREATE_DISPOSITIONS = [
15156
+ "needs_triage",
15157
+ "ready_for_agent",
15158
+ "ready_for_human"
15159
+ ];
15160
+ var ISSUE_ESTIMATES = [1, 2, 3, 5, 8];
15161
+ var ISSUE_TERMINAL_STATUSES = ["done", "canceled"];
15162
+ var PROJECT_STATUSES = ["planned", "in_progress", "paused", "completed", "canceled"];
15163
+ var PROJECT_HEALTHS = ["on_track", "at_risk", "off_track"];
15164
+ var ISSUE_IDENTIFIER_PREFIX = "KAI";
15165
+ var ISSUE_LIST_PAGE_SIZE = 50;
15166
+ var ISSUE_DUE_DATE_MODES = ["overdue", "due_today", "due_soon", "no_due_date"];
15167
+ var issueIsOpen = (status) => !ISSUE_TERMINAL_STATUSES.some((terminal) => terminal === status);
15146
15168
  var MAX_PROJECT_REPO_LENGTH = 200;
15147
15169
 
15148
15170
  // src/commands/auth/login.ts
@@ -17103,6 +17125,12 @@ var normalizeUrl = (url2, errorMessage) => {
17103
17125
  throw new Error(errorMessage ?? "URL must be a valid URL");
17104
17126
  return new URL(parsed.data).origin;
17105
17127
  };
17128
+ var localToday = () => {
17129
+ const now3 = new Date;
17130
+ const month = String(now3.getMonth() + 1).padStart(2, "0");
17131
+ const day = String(now3.getDate()).padStart(2, "0");
17132
+ return `${now3.getFullYear()}-${month}-${day}`;
17133
+ };
17106
17134
 
17107
17135
  // src/lib/zod.ts
17108
17136
  var configSchema = exports_external.object({ sessionToken: exports_external.string().optional(), convexUrl: exports_external.url(), convexSiteUrl: exports_external.url() });
@@ -17786,23 +17814,6 @@ var auth = group({
17786
17814
  commands: [login, logout, status]
17787
17815
  });
17788
17816
 
17789
- // src/lib/group.ts
17790
- import { basename } from "path";
17791
- var git = async (...args) => {
17792
- const proc = Bun.spawn(["git", ...args], { stdout: "pipe", stderr: "ignore" });
17793
- const [output, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
17794
- return exitCode === 0 ? output.trim() || null : null;
17795
- };
17796
- var repoNameFromRemote = (remote) => remote.replace(/\/+$/, "").replace(/\.git$/, "").split(/[/:]/).pop() || undefined;
17797
- var detectRepoGroup = async () => {
17798
- const remote = await git("remote", "get-url", "origin");
17799
- if (remote)
17800
- return repoNameFromRemote(remote);
17801
- const root = await git("rev-parse", "--show-toplevel");
17802
- return root ? basename(root) : undefined;
17803
- };
17804
- var groupForCreate = async (group2) => group2 === null ? undefined : group2 ?? await detectRepoGroup();
17805
-
17806
17817
  // src/lib/input.ts
17807
17818
  var assertSize = (size) => {
17808
17819
  if (size > MAX_PAGE_HTML_BYTES)
@@ -17818,6 +17829,27 @@ var readBody = async (path) => {
17818
17829
  assertSize(file2.size);
17819
17830
  return await file2.text();
17820
17831
  };
17832
+ var readTextOption = async (value) => value === "-" ? await Bun.stdin.text() : value;
17833
+ var readIssueRef = (value) => {
17834
+ const trimmed = value.trim();
17835
+ if (/^\d+$/.test(trimmed))
17836
+ return `${ISSUE_IDENTIFIER_PREFIX}-${trimmed}`;
17837
+ try {
17838
+ const url2 = new URL(trimmed);
17839
+ return url2.pathname.match(/\/issues\/([^/]+)\/?$/)?.[1] ?? trimmed;
17840
+ } catch {
17841
+ return trimmed;
17842
+ }
17843
+ };
17844
+ var readProjectRef = (value) => {
17845
+ const trimmed = value.trim();
17846
+ try {
17847
+ const url2 = new URL(trimmed);
17848
+ return url2.pathname.match(/\/projects\/([^/]+)\/?$/)?.[1] ?? trimmed;
17849
+ } catch {
17850
+ return trimmed;
17851
+ }
17852
+ };
17821
17853
  var readPageId = (value) => {
17822
17854
  const trimmed = value.trim();
17823
17855
  try {
@@ -17829,8 +17861,437 @@ var readPageId = (value) => {
17829
17861
  }
17830
17862
  };
17831
17863
 
17832
- // src/commands/pages/create.ts
17864
+ // ../../packages/backend/convex/lib/projectRepo.ts
17865
+ var DEFAULT_REPO_HOST = "github.com";
17866
+ var REPO_FORMAT_ERROR = "Enter a repository like github.com/owner/name.";
17867
+ var HOST_PATTERN = /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/;
17868
+ var SEGMENT_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
17869
+ var normalizeProjectRepo = (value) => {
17870
+ const trimmed = value?.trim();
17871
+ if (!trimmed)
17872
+ return;
17873
+ if (trimmed.length > MAX_PROJECT_REPO_LENGTH) {
17874
+ throw new ConvexError(`Repository must be ${MAX_PROJECT_REPO_LENGTH} characters or fewer.`);
17875
+ }
17876
+ const path = trimmed.toLowerCase().replace(/^(https?|ssh|git):\/\//, "").replace(/^[^/@:]+@/, "").replace(":", "/").replace(/\/+$/, "").replace(/\.git$/, "");
17877
+ const segments = path.split("/");
17878
+ const [host, owner, name] = segments.length === 2 ? [DEFAULT_REPO_HOST, ...segments] : segments.length === 3 ? segments : [];
17879
+ if (!host || !owner || !name)
17880
+ throw new ConvexError(REPO_FORMAT_ERROR);
17881
+ if (!HOST_PATTERN.test(host) || !SEGMENT_PATTERN.test(owner) || !SEGMENT_PATTERN.test(name)) {
17882
+ throw new ConvexError(REPO_FORMAT_ERROR);
17883
+ }
17884
+ const key = `${host}/${owner}/${name}`;
17885
+ if (key.length > MAX_PROJECT_REPO_LENGTH) {
17886
+ throw new ConvexError(`Repository must be ${MAX_PROJECT_REPO_LENGTH} characters or fewer.`);
17887
+ }
17888
+ return key;
17889
+ };
17890
+ var tryNormalizeProjectRepo = (value) => {
17891
+ try {
17892
+ return normalizeProjectRepo(value);
17893
+ } catch {
17894
+ return;
17895
+ }
17896
+ };
17897
+
17898
+ // src/lib/repo.ts
17899
+ var currentRepoKey = async () => {
17900
+ const proc = Bun.spawn(["git", "remote", "get-url", "origin"], { stdout: "pipe", stderr: "ignore" });
17901
+ const url2 = await new Response(proc.stdout).text();
17902
+ return await proc.exited === 0 ? tryNormalizeProjectRepo(url2) : undefined;
17903
+ };
17904
+
17905
+ // src/lib/resolve.ts
17906
+ var resolveIssue = async (client3, ref) => {
17907
+ const issue2 = await client3.query(api2.issues.get, { identifier: readIssueRef(ref) });
17908
+ if (!issue2)
17909
+ throw new Error(`No issue matches ${ref}.`);
17910
+ return issue2;
17911
+ };
17912
+ var resolveProject = async (client3, ref) => {
17913
+ const project = await client3.query(api2.projects.get, { id: readProjectRef(ref), today: localToday() });
17914
+ if (!project)
17915
+ throw new Error(`No project matches ${ref}.`);
17916
+ return project;
17917
+ };
17918
+ var repoProject = async (client3) => {
17919
+ const repo = await currentRepoKey();
17920
+ if (!repo)
17921
+ throw new Error("No repository here: not a git checkout with an origin remote.");
17922
+ const project = await client3.query(api2.projects.findByRepo, { repo });
17923
+ if (!project)
17924
+ throw new Error(`No project is connected to ${repo}. Connect one on the project in the dashboard.`);
17925
+ return project;
17926
+ };
17927
+ var resolveMilestone = async (client3, projectId, name) => {
17928
+ const milestones = await client3.query(api2.milestones.listByProject, { projectId, today: localToday() });
17929
+ const wanted = name.trim().toLowerCase();
17930
+ const milestone = milestones.find((candidate) => candidate.name.toLowerCase() === wanted);
17931
+ if (!milestone) {
17932
+ const names = milestones.map((candidate) => candidate.name).join(", ");
17933
+ throw new Error(names ? `No milestone named "${name}". The project has: ${names}.` : "The project has no milestones.");
17934
+ }
17935
+ return milestone;
17936
+ };
17937
+
17938
+ // src/commands/issues/comment.ts
17939
+ var comment = command({
17940
+ name: "comment",
17941
+ description: "Comment on an issue",
17942
+ positionals: {
17943
+ id: exports_external.string().describe("Issue identifier, number, or URL"),
17944
+ body: exports_external.string().describe("Comment markdown, or - for stdin")
17945
+ },
17946
+ run: async ({ positionals: { id, body } }) => {
17947
+ const client3 = await backendClient();
17948
+ const issue2 = await resolveIssue(client3, id);
17949
+ await client3.mutation(api2.issueComments.create, { issueId: issue2._id, bodyMarkdown: await readTextOption(body) });
17950
+ console.log(`Commented on ${issue2.identifier}.`);
17951
+ }
17952
+ });
17953
+
17954
+ // src/commands/issues/create.ts
17833
17955
  var create = command({
17956
+ name: "create",
17957
+ description: "Create an issue and print its identifier",
17958
+ positionals: {
17959
+ title: exports_external.string().describe("Issue title")
17960
+ },
17961
+ options: {
17962
+ description: exports_external.string().optional().describe("Markdown description, or - for stdin").meta({ short: "d" }),
17963
+ status: exports_external.enum(ISSUE_STATUSES).optional().describe("Starting status (default backlog)").meta({ short: "s" }),
17964
+ priority: exports_external.enum(ISSUE_PRIORITIES).optional().describe("Priority (default none)").meta({ short: "p" }),
17965
+ estimate: exports_external.coerce.number().pipe(exports_external.literal([...ISSUE_ESTIMATES])).optional().describe(`Points (${ISSUE_ESTIMATES.join("|")})`),
17966
+ due: exports_external.iso.date().optional().describe("Due date (YYYY-MM-DD)"),
17967
+ project: exports_external.string().optional().describe("Project to create the issue in (id or URL)"),
17968
+ here: exports_external.boolean().default(false).describe("Create in the checkout's connected project"),
17969
+ milestone: exports_external.string().optional().describe("Milestone name (needs --project or --here)"),
17970
+ disposition: exports_external.enum(ISSUE_CREATE_DISPOSITIONS).optional().describe("Route immediately (default needs_triage)")
17971
+ },
17972
+ run: async ({ positionals: { title }, options }) => {
17973
+ if (options.project && options.here)
17974
+ throw new Error("Pass --project or --here, not both.");
17975
+ const client3 = await backendClient();
17976
+ const project = options.project ? await resolveProject(client3, options.project) : options.here ? await repoProject(client3) : undefined;
17977
+ if (options.milestone && !project)
17978
+ throw new Error("A milestone needs --project or --here.");
17979
+ const milestone = project && options.milestone ? await resolveMilestone(client3, project._id, options.milestone) : undefined;
17980
+ const { identifier } = await client3.mutation(api2.issues.create, {
17981
+ title,
17982
+ descriptionMarkdown: options.description === undefined ? undefined : await readTextOption(options.description),
17983
+ status: options.status,
17984
+ priority: options.priority,
17985
+ estimate: options.estimate,
17986
+ dueDate: options.due,
17987
+ projectId: project?._id,
17988
+ milestoneId: milestone?._id,
17989
+ disposition: options.disposition
17990
+ });
17991
+ console.log(identifier);
17992
+ }
17993
+ });
17994
+
17995
+ // 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
+ var get = command({
18026
+ name: "get",
18027
+ description: "Show an issue: properties, description, and its feed",
18028
+ positionals: {
18029
+ id: exports_external.string().describe("Issue identifier, number, or URL")
18030
+ },
18031
+ options: {
18032
+ json: exports_external.boolean().default(false).describe("Print as JSON")
18033
+ },
18034
+ run: async ({ positionals: { id }, options: { json: json2 } }) => {
18035
+ const client3 = await backendClient();
18036
+ const issue2 = await resolveIssue(client3, id);
18037
+ const feed = await client3.query(api2.issues.feed, { id: issue2._id });
18038
+ if (json2)
18039
+ return console.log(JSON.stringify({ ...issue2, feed }, null, 2));
18040
+ const project = issue2.projectId ? await client3.query(api2.projects.get, { id: issue2.projectId, today: localToday() }) : null;
18041
+ const milestone = issue2.milestoneId ? await client3.query(api2.milestones.get, { id: issue2.milestoneId, today: localToday() }) : null;
18042
+ console.log(`${issue2.identifier} ${issue2.title}`);
18043
+ console.log(`Status: ${issue2.status} \xB7 Priority: ${issue2.priority} \xB7 Disposition: ${issue2.disposition}`);
18044
+ if (issue2.estimate !== undefined)
18045
+ console.log(`Estimate: ${issue2.estimate}`);
18046
+ if (issue2.dueDate)
18047
+ console.log(`Due: ${issue2.dueDate}`);
18048
+ if (project)
18049
+ console.log(`Project: ${project.name}${milestone ? ` \xB7 Milestone: ${milestone.name}` : ""}`);
18050
+ if (issue2.parent)
18051
+ console.log(`Parent: ${issue2.parent.identifier} ${issue2.parent.title}`);
18052
+ if (issue2.labels.length > 0)
18053
+ console.log(`Labels: ${issue2.labels.map((label) => label.name).join(", ")}`);
18054
+ if (issue2.children.total > 0)
18055
+ console.log(`Sub-issues: ${issue2.children.done}/${issue2.children.total} done`);
18056
+ if (issue2.descriptionMarkdown)
18057
+ console.log(`
18058
+ ${issue2.descriptionMarkdown}`);
18059
+ if (feed.events.length > 0)
18060
+ console.log(`
18061
+ Feed:`);
18062
+ if (feed.truncated)
18063
+ console.log("(older events truncated)");
18064
+ for (const event of feed.events) {
18065
+ const at = new Date(event.at).toISOString().slice(0, 16).replace("T", " ");
18066
+ if (event.type === "activity") {
18067
+ console.log(`${at} ${activityLine(event.detail)}`);
18068
+ } else {
18069
+ console.log(`${at} comment${event.editedAt ? " (edited)" : ""}:`);
18070
+ for (const line of event.bodyMarkdown.split(`
18071
+ `))
18072
+ console.log(` ${line}`);
18073
+ }
18074
+ }
18075
+ }
18076
+ });
18077
+
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
+ // src/commands/issues/list.ts
18090
+ var list = command({
18091
+ name: "list",
18092
+ description: "List open issues, most recently updated first",
18093
+ options: {
18094
+ all: exports_external.boolean().default(false).describe("Include done and canceled issues"),
18095
+ status: exports_external.enum(ISSUE_STATUSES).optional().describe("Only this status").meta({ short: "s" }),
18096
+ priority: exports_external.enum(ISSUE_PRIORITIES).optional().describe("Only this priority").meta({ short: "p" }),
18097
+ disposition: exports_external.enum(ISSUE_DISPOSITIONS).optional().describe("Only this disposition").meta({ short: "d" }),
18098
+ project: exports_external.string().optional().describe("Only this project (id or URL)"),
18099
+ here: exports_external.boolean().default(false).describe("Only the checkout's connected project"),
18100
+ milestone: exports_external.string().optional().describe("Only this milestone (name; needs --project or --here)"),
18101
+ due: exports_external.enum(ISSUE_DUE_DATE_MODES).optional().describe("Only this due-date state"),
18102
+ search: exports_external.string().optional().describe("Free-text search (results come in relevance order)"),
18103
+ limit: exports_external.coerce.number().int().positive().default(ISSUE_LIST_PAGE_SIZE).describe("Most issues to print"),
18104
+ json: exports_external.boolean().default(false).describe("Print as JSON")
18105
+ },
18106
+ run: async ({ options }) => {
18107
+ if (options.project && options.here)
18108
+ throw new Error("Pass --project or --here, not both.");
18109
+ const client3 = await backendClient();
18110
+ const project = options.project ? await resolveProject(client3, options.project) : options.here ? await repoProject(client3) : undefined;
18111
+ if (options.milestone && !project)
18112
+ throw new Error("A milestone filter needs --project or --here.");
18113
+ const milestone = project && options.milestone ? await resolveMilestone(client3, project._id, options.milestone) : undefined;
18114
+ const result = await client3.query(api2.issueViews.query, {
18115
+ source: {
18116
+ type: "custom",
18117
+ query: {
18118
+ layout: "list",
18119
+ groupBy: "none",
18120
+ orderBy: "updated_at",
18121
+ orderDirection: "desc",
18122
+ filters: {
18123
+ query: options.search,
18124
+ statuses: options.status ? [options.status] : options.all ? undefined : ISSUE_STATUSES.filter(issueIsOpen),
18125
+ priorities: options.priority ? [options.priority] : undefined,
18126
+ dispositions: options.disposition ? [options.disposition] : undefined,
18127
+ projectIds: project ? [project._id] : undefined,
18128
+ milestoneIds: milestone ? [milestone._id] : undefined,
18129
+ dueDate: options.due
18130
+ }
18131
+ }
18132
+ },
18133
+ paginationOpts: { numItems: options.limit, cursor: null },
18134
+ today: localToday()
18135
+ });
18136
+ if (options.json)
18137
+ return console.log(JSON.stringify(result.page, null, 2));
18138
+ if (result.page.length === 0)
18139
+ return console.log("No issues match.");
18140
+ printTable([
18141
+ ["ID", "TITLE", "STATUS", "PRIORITY", "DISPOSITION", "DUE", "UPDATED"],
18142
+ ...result.page.map((issue2) => [
18143
+ issue2.identifier,
18144
+ issue2.title,
18145
+ issue2.status,
18146
+ issue2.priority,
18147
+ issue2.disposition,
18148
+ issue2.dueDate ?? "-",
18149
+ new Date(issue2.updatedAt).toISOString().slice(0, 10)
18150
+ ])
18151
+ ]);
18152
+ if (!result.isDone)
18153
+ console.log(`
18154
+ More issues match; raise --limit past ${options.limit}.`);
18155
+ }
18156
+ });
18157
+
18158
+ // src/commands/issues/remove.ts
18159
+ var remove = command({
18160
+ name: "delete",
18161
+ description: "Delete an issue permanently (sub-issues survive as top-level issues)",
18162
+ positionals: {
18163
+ id: exports_external.string().describe("Issue identifier, number, or URL")
18164
+ },
18165
+ run: async ({ positionals: { id } }) => {
18166
+ const client3 = await backendClient();
18167
+ const issue2 = await resolveIssue(client3, id);
18168
+ await client3.mutation(api2.issues.remove, { id: issue2._id });
18169
+ console.log(`Deleted ${issue2.identifier}.`);
18170
+ }
18171
+ });
18172
+
18173
+ // src/commands/issues/route.ts
18174
+ var route = command({
18175
+ name: "route",
18176
+ description: "Route an issue to a disposition (wontfix also cancels it)",
18177
+ positionals: {
18178
+ id: exports_external.string().describe("Issue identifier, number, or URL"),
18179
+ disposition: exports_external.enum(ISSUE_DISPOSITIONS).describe("Where the issue goes next")
18180
+ },
18181
+ options: {
18182
+ comment: exports_external.string().optional().describe("Why, as a comment (required for needs_info and wontfix), or - for stdin").meta({ short: "c" })
18183
+ },
18184
+ run: async ({ positionals: { id, disposition }, options: { comment: comment2 } }) => {
18185
+ const client3 = await backendClient();
18186
+ const issue2 = await resolveIssue(client3, id);
18187
+ await client3.mutation(api2.issues.route, {
18188
+ id: issue2._id,
18189
+ disposition,
18190
+ comment: comment2 === undefined ? undefined : await readTextOption(comment2)
18191
+ });
18192
+ console.log(`Routed ${issue2.identifier} to ${disposition}.`);
18193
+ }
18194
+ });
18195
+
18196
+ // src/commands/issues/set.ts
18197
+ var set2 = command({
18198
+ name: "set",
18199
+ description: "Set an issue's status, priority, estimate, due date, project, milestone, or parent",
18200
+ positionals: {
18201
+ id: exports_external.string().describe("Issue identifier, number, or URL")
18202
+ },
18203
+ options: {
18204
+ status: exports_external.enum(ISSUE_STATUSES).optional().describe("New status").meta({ short: "s" }),
18205
+ priority: exports_external.enum(ISSUE_PRIORITIES).optional().describe("New priority").meta({ short: "p" }),
18206
+ estimate: exports_external.coerce.number().pipe(exports_external.literal([...ISSUE_ESTIMATES])).nullable().optional().describe(`Points (${ISSUE_ESTIMATES.join("|")}), or --no-estimate to clear`).meta({ negatable: true }),
18207
+ due: exports_external.iso.date().nullable().optional().describe("Due date (YYYY-MM-DD), or --no-due to clear").meta({ negatable: true }),
18208
+ 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
+ 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 })
18211
+ },
18212
+ run: async ({ positionals: { id }, options }) => {
18213
+ const { status: status2, priority, estimate, due, project, milestone, parent } = options;
18214
+ if (Object.values(options).every((value) => value === undefined))
18215
+ throw new Error("Nothing to set. Pass --status, --priority, --estimate, --due, --project, --milestone, or --parent.");
18216
+ const client3 = await backendClient();
18217
+ const issue2 = await resolveIssue(client3, id);
18218
+ const projectId = project === undefined ? issue2.projectId : project === null ? undefined : (await resolveProject(client3, project))._id;
18219
+ let milestoneId;
18220
+ if (milestone === undefined) {
18221
+ milestoneId = projectId === issue2.projectId ? issue2.milestoneId : undefined;
18222
+ } else if (milestone !== null) {
18223
+ if (projectId === undefined)
18224
+ throw new Error("A milestone needs a project. Pass --project too.");
18225
+ milestoneId = (await resolveMilestone(client3, projectId, milestone))._id;
18226
+ }
18227
+ const parentId = parent == null ? undefined : (await resolveIssue(client3, parent))._id;
18228
+ if (parent !== undefined)
18229
+ await client3.mutation(api2.issues.setParent, { id: issue2._id, parentId });
18230
+ if (status2 !== undefined)
18231
+ await client3.mutation(api2.issues.setStatus, { id: issue2._id, status: status2 });
18232
+ if (priority !== undefined)
18233
+ await client3.mutation(api2.issues.setPriority, { id: issue2._id, priority });
18234
+ if (estimate !== undefined)
18235
+ await client3.mutation(api2.issues.setEstimate, { id: issue2._id, estimate: estimate ?? undefined });
18236
+ if (due !== undefined)
18237
+ await client3.mutation(api2.issues.setDueDate, { id: issue2._id, dueDate: due ?? undefined });
18238
+ if (project !== undefined || milestone !== undefined)
18239
+ await client3.mutation(api2.issues.setMembership, { id: issue2._id, projectId, milestoneId });
18240
+ console.log(`Updated ${issue2.identifier}.`);
18241
+ }
18242
+ });
18243
+
18244
+ // src/commands/issues/update.ts
18245
+ var update = command({
18246
+ name: "update",
18247
+ description: "Rewrite an issue's title or description",
18248
+ positionals: {
18249
+ id: exports_external.string().describe("Issue identifier, number, or URL")
18250
+ },
18251
+ options: {
18252
+ 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" })
18254
+ },
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.");
18258
+ const client3 = await backendClient();
18259
+ 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
+ });
18265
+ console.log(`Updated ${issue2.identifier}.`);
18266
+ }
18267
+ });
18268
+
18269
+ // src/commands/issues/index.ts
18270
+ var issues = group({
18271
+ name: "issues",
18272
+ description: "Track and triage issues",
18273
+ commands: [create, list, get, update, set2, route, comment, remove]
18274
+ });
18275
+
18276
+ // src/lib/group.ts
18277
+ import { basename } from "path";
18278
+ var git = async (...args) => {
18279
+ const proc = Bun.spawn(["git", ...args], { stdout: "pipe", stderr: "ignore" });
18280
+ const [output, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
18281
+ return exitCode === 0 ? output.trim() || null : null;
18282
+ };
18283
+ var repoNameFromRemote = (remote) => remote.replace(/\/+$/, "").replace(/\.git$/, "").split(/[/:]/).pop() || undefined;
18284
+ var detectRepoGroup = async () => {
18285
+ const remote = await git("remote", "get-url", "origin");
18286
+ if (remote)
18287
+ return repoNameFromRemote(remote);
18288
+ const root = await git("rev-parse", "--show-toplevel");
18289
+ return root ? basename(root) : undefined;
18290
+ };
18291
+ var groupForCreate = async (group2) => group2 === null ? undefined : group2 ?? await detectRepoGroup();
18292
+
18293
+ // src/commands/pages/create.ts
18294
+ var create2 = command({
17834
18295
  name: "create",
17835
18296
  description: "Publish a page and print its URL",
17836
18297
  positionals: {
@@ -17859,7 +18320,7 @@ var create = command({
17859
18320
  });
17860
18321
 
17861
18322
  // src/commands/pages/get.ts
17862
- var get = command({
18323
+ var get2 = command({
17863
18324
  name: "get",
17864
18325
  description: "Print a page's HTML",
17865
18326
  positionals: {
@@ -17881,19 +18342,8 @@ var get = command({
17881
18342
  // ../../packages/backend/convex/lib/format.ts
17882
18343
  var formatBytes = (bytes) => bytes < 1024 ? `${bytes} B` : `${Math.round(bytes / 1024)} KB`;
17883
18344
 
17884
- // src/lib/output.ts
17885
- var printTable = (rows) => {
17886
- if (rows.length === 0)
17887
- return;
17888
- const columnCount = Math.max(...rows.map((row) => row.length));
17889
- const widths = Array.from({ length: columnCount }, (_, column) => Math.max(...rows.map((row) => (row[column] ?? "").length)));
17890
- for (const row of rows) {
17891
- console.log(row.map((cell, column) => cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd());
17892
- }
17893
- };
17894
-
17895
18345
  // src/commands/pages/list.ts
17896
- var list = command({
18346
+ var list2 = command({
17897
18347
  name: "list",
17898
18348
  description: "List your published pages",
17899
18349
  options: {
@@ -17923,7 +18373,7 @@ var list = command({
17923
18373
  });
17924
18374
 
17925
18375
  // src/commands/pages/remove.ts
17926
- var remove = command({
18376
+ var remove2 = command({
17927
18377
  name: "delete",
17928
18378
  description: "Delete a page",
17929
18379
  positionals: {
@@ -17950,7 +18400,7 @@ var revert = command({
17950
18400
  });
17951
18401
 
17952
18402
  // src/commands/pages/update.ts
17953
- var update = command({
18403
+ var update2 = command({
17954
18404
  name: "update",
17955
18405
  description: "Replace a page's HTML, title, group, mode, or visibility",
17956
18406
  positionals: {
@@ -18011,50 +18461,9 @@ var versions2 = command({
18011
18461
  var pages = group({
18012
18462
  name: "pages",
18013
18463
  description: "Publish HTML documents to the web",
18014
- commands: [create, list, get, update, versions2, revert, remove]
18464
+ commands: [create2, list2, get2, update2, versions2, revert, remove2]
18015
18465
  });
18016
18466
 
18017
- // ../../packages/backend/convex/lib/projectRepo.ts
18018
- var DEFAULT_REPO_HOST = "github.com";
18019
- var REPO_FORMAT_ERROR = "Enter a repository like github.com/owner/name.";
18020
- var HOST_PATTERN = /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/;
18021
- var SEGMENT_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
18022
- var normalizeProjectRepo = (value) => {
18023
- const trimmed = value?.trim();
18024
- if (!trimmed)
18025
- return;
18026
- if (trimmed.length > MAX_PROJECT_REPO_LENGTH) {
18027
- throw new ConvexError(`Repository must be ${MAX_PROJECT_REPO_LENGTH} characters or fewer.`);
18028
- }
18029
- const path = trimmed.toLowerCase().replace(/^(https?|ssh|git):\/\//, "").replace(/^[^/@:]+@/, "").replace(":", "/").replace(/\/+$/, "").replace(/\.git$/, "");
18030
- const segments = path.split("/");
18031
- const [host, owner, name] = segments.length === 2 ? [DEFAULT_REPO_HOST, ...segments] : segments.length === 3 ? segments : [];
18032
- if (!host || !owner || !name)
18033
- throw new ConvexError(REPO_FORMAT_ERROR);
18034
- if (!HOST_PATTERN.test(host) || !SEGMENT_PATTERN.test(owner) || !SEGMENT_PATTERN.test(name)) {
18035
- throw new ConvexError(REPO_FORMAT_ERROR);
18036
- }
18037
- const key = `${host}/${owner}/${name}`;
18038
- if (key.length > MAX_PROJECT_REPO_LENGTH) {
18039
- throw new ConvexError(`Repository must be ${MAX_PROJECT_REPO_LENGTH} characters or fewer.`);
18040
- }
18041
- return key;
18042
- };
18043
- var tryNormalizeProjectRepo = (value) => {
18044
- try {
18045
- return normalizeProjectRepo(value);
18046
- } catch {
18047
- return;
18048
- }
18049
- };
18050
-
18051
- // src/lib/repo.ts
18052
- var currentRepoKey = async () => {
18053
- const proc = Bun.spawn(["git", "remote", "get-url", "origin"], { stdout: "pipe", stderr: "ignore" });
18054
- const url2 = await new Response(proc.stdout).text();
18055
- return await proc.exited === 0 ? tryNormalizeProjectRepo(url2) : undefined;
18056
- };
18057
-
18058
18467
  // src/commands/project.ts
18059
18468
  var project = command({
18060
18469
  name: "project",
@@ -18084,6 +18493,192 @@ var project = command({
18084
18493
  }
18085
18494
  });
18086
18495
 
18496
+ // src/commands/projects/create.ts
18497
+ var create3 = command({
18498
+ name: "create",
18499
+ description: "Create a project and print its id",
18500
+ positionals: {
18501
+ name: exports_external.string().describe("Project name")
18502
+ },
18503
+ options: {
18504
+ summary: exports_external.string().optional().describe("One-line summary").meta({ short: "s" }),
18505
+ description: exports_external.string().optional().describe("Markdown description, or - for stdin").meta({ short: "d" }),
18506
+ status: exports_external.enum(PROJECT_STATUSES).optional().describe("Starting status (default planned)"),
18507
+ health: exports_external.enum(PROJECT_HEALTHS).optional().describe("Health"),
18508
+ target: exports_external.iso.date().optional().describe("Target date (YYYY-MM-DD)"),
18509
+ repo: exports_external.string().optional().describe("Repository to connect (owner/name or any remote URL)")
18510
+ },
18511
+ run: async ({ positionals: { name }, options }) => {
18512
+ const projectId = await (await backendClient()).mutation(api2.projects.create, {
18513
+ name,
18514
+ summary: options.summary,
18515
+ descriptionMarkdown: options.description === undefined ? undefined : await readTextOption(options.description),
18516
+ status: options.status,
18517
+ health: options.health,
18518
+ targetDate: options.target,
18519
+ repo: options.repo
18520
+ });
18521
+ console.log(projectId);
18522
+ }
18523
+ });
18524
+
18525
+ // src/commands/projects/get.ts
18526
+ var get3 = command({
18527
+ name: "get",
18528
+ description: "Show a project: properties, description, and milestones",
18529
+ positionals: {
18530
+ id: exports_external.string().describe("Project id or URL")
18531
+ },
18532
+ options: {
18533
+ json: exports_external.boolean().default(false).describe("Print as JSON")
18534
+ },
18535
+ run: async ({ positionals: { id }, options: { json: json2 } }) => {
18536
+ const client3 = await backendClient();
18537
+ const project2 = await resolveProject(client3, id);
18538
+ const milestones = await client3.query(api2.milestones.listByProject, {
18539
+ projectId: project2._id,
18540
+ today: localToday()
18541
+ });
18542
+ if (json2)
18543
+ return console.log(JSON.stringify({ ...project2, milestones }, null, 2));
18544
+ console.log(project2.name);
18545
+ if (project2.summary)
18546
+ console.log(project2.summary);
18547
+ if (project2.repo)
18548
+ console.log(`Repo: ${project2.repo}`);
18549
+ console.log(`Status: ${project2.status}${project2.health ? ` \xB7 Health: ${project2.health}` : ""}`);
18550
+ console.log(`Progress: ${project2.progress.done}/${project2.progress.total} issues done`);
18551
+ if (project2.targetDate)
18552
+ console.log(`Target date: ${project2.targetDate}${project2.isOverdue ? " (overdue)" : ""}`);
18553
+ if (project2.descriptionMarkdown)
18554
+ console.log(`
18555
+ ${project2.descriptionMarkdown}`);
18556
+ if (milestones.length > 0) {
18557
+ console.log(`
18558
+ Milestones:`);
18559
+ for (const milestone of milestones) {
18560
+ const target = milestone.targetDate ? ` \xB7 target ${milestone.targetDate}${milestone.isOverdue ? " (overdue)" : ""}` : "";
18561
+ console.log(` ${milestone.name} \u2014 ${milestone.progress.done}/${milestone.progress.total} done${target}`);
18562
+ }
18563
+ }
18564
+ }
18565
+ });
18566
+
18567
+ // src/commands/projects/list.ts
18568
+ var list3 = command({
18569
+ name: "list",
18570
+ description: "List your projects, most recently updated first",
18571
+ options: {
18572
+ limit: exports_external.coerce.number().int().positive().default(50).describe("Most projects to print"),
18573
+ json: exports_external.boolean().default(false).describe("Print as JSON")
18574
+ },
18575
+ run: async ({ options: { limit, json: json2 } }) => {
18576
+ const result = await (await backendClient()).query(api2.projects.listPaginated, {
18577
+ paginationOpts: { numItems: limit, cursor: null },
18578
+ today: localToday()
18579
+ });
18580
+ if (json2)
18581
+ return console.log(JSON.stringify(result.page, null, 2));
18582
+ if (result.page.length === 0)
18583
+ return console.log("No projects yet.");
18584
+ printTable([
18585
+ ["NAME", "STATUS", "HEALTH", "PROGRESS", "TARGET", "REPO", "ID"],
18586
+ ...result.page.map((project2) => [
18587
+ project2.name,
18588
+ project2.status,
18589
+ project2.health ?? "-",
18590
+ `${project2.progress.done}/${project2.progress.total}`,
18591
+ project2.targetDate ? `${project2.targetDate}${project2.isOverdue ? " (overdue)" : ""}` : "-",
18592
+ project2.repo ?? "-",
18593
+ project2._id
18594
+ ])
18595
+ ]);
18596
+ if (!result.isDone)
18597
+ console.log(`
18598
+ More projects exist; raise --limit past ${limit}.`);
18599
+ }
18600
+ });
18601
+
18602
+ // src/commands/projects/remove.ts
18603
+ var remove3 = command({
18604
+ name: "delete",
18605
+ description: "Delete a project and its milestones (its issues survive, projectless)",
18606
+ positionals: {
18607
+ id: exports_external.string().describe("Project id or URL")
18608
+ },
18609
+ run: async ({ positionals: { id } }) => {
18610
+ const client3 = await backendClient();
18611
+ const project2 = await resolveProject(client3, id);
18612
+ await client3.mutation(api2.projects.remove, { id: project2._id });
18613
+ console.log(`Deleted ${project2.name}.`);
18614
+ }
18615
+ });
18616
+
18617
+ // src/commands/projects/set.ts
18618
+ var set3 = command({
18619
+ name: "set",
18620
+ description: "Set a project's status, health, target date, or repository",
18621
+ positionals: {
18622
+ id: exports_external.string().describe("Project id or URL")
18623
+ },
18624
+ options: {
18625
+ status: exports_external.enum(PROJECT_STATUSES).optional().describe("New status").meta({ short: "s" }),
18626
+ health: exports_external.enum(PROJECT_HEALTHS).nullable().optional().describe("New health, or --no-health to clear").meta({ negatable: true }),
18627
+ target: exports_external.iso.date().nullable().optional().describe("Target date (YYYY-MM-DD), or --no-target to clear").meta({ negatable: true }),
18628
+ repo: exports_external.string().nullable().optional().describe("Repository to connect (owner/name or any remote URL), or --no-repo to disconnect").meta({ negatable: true })
18629
+ },
18630
+ run: async ({ positionals: { id }, options }) => {
18631
+ const { status: status2, health, target, repo } = options;
18632
+ if (Object.values(options).every((value) => value === undefined))
18633
+ throw new Error("Nothing to set. Pass --status, --health, --target, or --repo.");
18634
+ const client3 = await backendClient();
18635
+ const project2 = await resolveProject(client3, id);
18636
+ if (repo !== undefined)
18637
+ await client3.mutation(api2.projects.setRepo, { id: project2._id, repo: repo ?? undefined });
18638
+ if (status2 !== undefined)
18639
+ await client3.mutation(api2.projects.setStatus, { id: project2._id, status: status2 });
18640
+ if (health !== undefined)
18641
+ await client3.mutation(api2.projects.setHealth, { id: project2._id, health: health ?? undefined });
18642
+ if (target !== undefined)
18643
+ await client3.mutation(api2.projects.setTargetDate, { id: project2._id, targetDate: target ?? undefined });
18644
+ console.log(`Updated ${project2.name}.`);
18645
+ }
18646
+ });
18647
+
18648
+ // src/commands/projects/update.ts
18649
+ var update3 = command({
18650
+ name: "update",
18651
+ description: "Rewrite a project's name, summary, or description",
18652
+ positionals: {
18653
+ id: exports_external.string().describe("Project id or URL")
18654
+ },
18655
+ options: {
18656
+ name: exports_external.string().optional().describe("New name").meta({ short: "n" }),
18657
+ summary: exports_external.string().optional().describe("New one-line summary").meta({ short: "s" }),
18658
+ description: exports_external.string().optional().describe("New markdown description, or - for stdin").meta({ short: "d" })
18659
+ },
18660
+ run: async ({ positionals: { id }, options: { name, summary, description } }) => {
18661
+ if (name === undefined && summary === undefined && description === undefined)
18662
+ throw new Error("Nothing to update. Pass --name, --summary, or --description.");
18663
+ const client3 = await backendClient();
18664
+ const project2 = await resolveProject(client3, id);
18665
+ await client3.mutation(api2.projects.update, {
18666
+ id: project2._id,
18667
+ name,
18668
+ summary,
18669
+ descriptionMarkdown: description === undefined ? undefined : await readTextOption(description)
18670
+ });
18671
+ console.log(`Updated ${name ?? project2.name}.`);
18672
+ }
18673
+ });
18674
+
18675
+ // src/commands/projects/index.ts
18676
+ var projects = group({
18677
+ name: "projects",
18678
+ description: "Manage projects",
18679
+ commands: [create3, list3, get3, update3, set3, remove3]
18680
+ });
18681
+
18087
18682
  // src/commands/upgrade.ts
18088
18683
  var upgrade = command({
18089
18684
  name: "upgrade",
@@ -18099,7 +18694,7 @@ var upgrade = command({
18099
18694
  var rootCommand = group({
18100
18695
  name: "kds",
18101
18696
  description: "KDS CLI",
18102
- commands: [auth, pages, project, upgrade],
18697
+ commands: [auth, issues, pages, project, projects, upgrade],
18103
18698
  options: {
18104
18699
  version: exports_external.boolean().default(false).describe("Show the version").meta({ short: "v" })
18105
18700
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@islamihab/kds",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Command-line client for Kai Dev Studio",
5
5
  "license": "MIT",
6
6
  "publishConfig": {