@islamihab/kds 0.1.4 → 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.
- package/index.js +865 -76
- 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
|
-
|
|
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
|
|
14500
|
-
|
|
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.1
|
|
14597
|
+
version: "0.2.1",
|
|
14591
14598
|
private: true,
|
|
14592
14599
|
type: "module",
|
|
14593
14600
|
bin: {
|
|
@@ -15143,6 +15150,30 @@ var KDS_DEVICE_AUTH_CLIENT_ID = "kds-cli";
|
|
|
15143
15150
|
var PAGE_MODES = ["themed", "raw"];
|
|
15144
15151
|
var PAGE_VISIBILITIES = ["public", "private"];
|
|
15145
15152
|
var MAX_PAGE_HTML_BYTES = 4000000;
|
|
15153
|
+
var ISSUE_STATUSES = ["backlog", "todo", "in_progress", "done", "canceled"];
|
|
15154
|
+
var ISSUE_PRIORITIES = ["urgent", "high", "medium", "low", "none"];
|
|
15155
|
+
var ISSUE_DISPOSITIONS = [
|
|
15156
|
+
"needs_triage",
|
|
15157
|
+
"needs_info",
|
|
15158
|
+
"ready_for_agent",
|
|
15159
|
+
"ready_for_human",
|
|
15160
|
+
"wontfix"
|
|
15161
|
+
];
|
|
15162
|
+
var ISSUE_CREATE_DISPOSITIONS = [
|
|
15163
|
+
"needs_triage",
|
|
15164
|
+
"ready_for_agent",
|
|
15165
|
+
"ready_for_human"
|
|
15166
|
+
];
|
|
15167
|
+
var ISSUE_ESTIMATES = [1, 2, 3, 5, 8];
|
|
15168
|
+
var ISSUE_TERMINAL_STATUSES = ["done", "canceled"];
|
|
15169
|
+
var ISSUE_RELATION_KINDS = ["blocks", "blocked_by", "duplicate_of", "duplicated_by", "relates_to"];
|
|
15170
|
+
var PROJECT_STATUSES = ["planned", "in_progress", "paused", "completed", "canceled"];
|
|
15171
|
+
var PROJECT_HEALTHS = ["on_track", "at_risk", "off_track"];
|
|
15172
|
+
var ISSUE_IDENTIFIER_PREFIX = "KAI";
|
|
15173
|
+
var ISSUE_LIST_PAGE_SIZE = 50;
|
|
15174
|
+
var MAX_ISSUE_ATTACHMENT_BYTES = 1e7;
|
|
15175
|
+
var ISSUE_DUE_DATE_MODES = ["overdue", "due_today", "due_soon", "no_due_date"];
|
|
15176
|
+
var issueIsOpen = (status) => !ISSUE_TERMINAL_STATUSES.some((terminal) => terminal === status);
|
|
15146
15177
|
var MAX_PROJECT_REPO_LENGTH = 200;
|
|
15147
15178
|
|
|
15148
15179
|
// src/commands/auth/login.ts
|
|
@@ -17103,6 +17134,12 @@ var normalizeUrl = (url2, errorMessage) => {
|
|
|
17103
17134
|
throw new Error(errorMessage ?? "URL must be a valid URL");
|
|
17104
17135
|
return new URL(parsed.data).origin;
|
|
17105
17136
|
};
|
|
17137
|
+
var localToday = () => {
|
|
17138
|
+
const now3 = new Date;
|
|
17139
|
+
const month = String(now3.getMonth() + 1).padStart(2, "0");
|
|
17140
|
+
const day = String(now3.getDate()).padStart(2, "0");
|
|
17141
|
+
return `${now3.getFullYear()}-${month}-${day}`;
|
|
17142
|
+
};
|
|
17106
17143
|
|
|
17107
17144
|
// src/lib/zod.ts
|
|
17108
17145
|
var configSchema = exports_external.object({ sessionToken: exports_external.string().optional(), convexUrl: exports_external.url(), convexSiteUrl: exports_external.url() });
|
|
@@ -17786,22 +17823,58 @@ var auth = group({
|
|
|
17786
17823
|
commands: [login, logout, status]
|
|
17787
17824
|
});
|
|
17788
17825
|
|
|
17789
|
-
// src/lib/
|
|
17826
|
+
// src/lib/attachments.ts
|
|
17790
17827
|
import { basename } from "path";
|
|
17791
|
-
var
|
|
17792
|
-
|
|
17793
|
-
|
|
17794
|
-
|
|
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));
|
|
17795
17872
|
};
|
|
17796
|
-
var
|
|
17797
|
-
|
|
17798
|
-
|
|
17799
|
-
|
|
17800
|
-
return repoNameFromRemote(remote);
|
|
17801
|
-
const root = await git("rev-parse", "--show-toplevel");
|
|
17802
|
-
return root ? basename(root) : undefined;
|
|
17873
|
+
var printAttachments = (attachments) => {
|
|
17874
|
+
for (const attachment of attachments) {
|
|
17875
|
+
console.log(`Attached ${attachment.name}${attachment.url ? ` \u2014 ${attachment.url}` : ""}`);
|
|
17876
|
+
}
|
|
17803
17877
|
};
|
|
17804
|
-
var groupForCreate = async (group2) => group2 === null ? undefined : group2 ?? await detectRepoGroup();
|
|
17805
17878
|
|
|
17806
17879
|
// src/lib/input.ts
|
|
17807
17880
|
var assertSize = (size) => {
|
|
@@ -17818,6 +17891,27 @@ var readBody = async (path) => {
|
|
|
17818
17891
|
assertSize(file2.size);
|
|
17819
17892
|
return await file2.text();
|
|
17820
17893
|
};
|
|
17894
|
+
var readTextOption = async (value) => value === "-" ? await Bun.stdin.text() : value;
|
|
17895
|
+
var readIssueRef = (value) => {
|
|
17896
|
+
const trimmed = value.trim();
|
|
17897
|
+
if (/^\d+$/.test(trimmed))
|
|
17898
|
+
return `${ISSUE_IDENTIFIER_PREFIX}-${trimmed}`;
|
|
17899
|
+
try {
|
|
17900
|
+
const url2 = new URL(trimmed);
|
|
17901
|
+
return url2.pathname.match(/\/issues\/([^/]+)\/?$/)?.[1] ?? trimmed;
|
|
17902
|
+
} catch {
|
|
17903
|
+
return trimmed;
|
|
17904
|
+
}
|
|
17905
|
+
};
|
|
17906
|
+
var readProjectRef = (value) => {
|
|
17907
|
+
const trimmed = value.trim();
|
|
17908
|
+
try {
|
|
17909
|
+
const url2 = new URL(trimmed);
|
|
17910
|
+
return url2.pathname.match(/\/projects\/([^/]+)\/?$/)?.[1] ?? trimmed;
|
|
17911
|
+
} catch {
|
|
17912
|
+
return trimmed;
|
|
17913
|
+
}
|
|
17914
|
+
};
|
|
17821
17915
|
var readPageId = (value) => {
|
|
17822
17916
|
const trimmed = value.trim();
|
|
17823
17917
|
try {
|
|
@@ -17829,8 +17923,569 @@ var readPageId = (value) => {
|
|
|
17829
17923
|
}
|
|
17830
17924
|
};
|
|
17831
17925
|
|
|
17832
|
-
//
|
|
17926
|
+
// ../../packages/backend/convex/lib/projectRepo.ts
|
|
17927
|
+
var DEFAULT_REPO_HOST = "github.com";
|
|
17928
|
+
var REPO_FORMAT_ERROR = "Enter a repository like github.com/owner/name.";
|
|
17929
|
+
var HOST_PATTERN = /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/;
|
|
17930
|
+
var SEGMENT_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
|
|
17931
|
+
var normalizeProjectRepo = (value) => {
|
|
17932
|
+
const trimmed = value?.trim();
|
|
17933
|
+
if (!trimmed)
|
|
17934
|
+
return;
|
|
17935
|
+
if (trimmed.length > MAX_PROJECT_REPO_LENGTH) {
|
|
17936
|
+
throw new ConvexError(`Repository must be ${MAX_PROJECT_REPO_LENGTH} characters or fewer.`);
|
|
17937
|
+
}
|
|
17938
|
+
const path = trimmed.toLowerCase().replace(/^(https?|ssh|git):\/\//, "").replace(/^[^/@:]+@/, "").replace(":", "/").replace(/\/+$/, "").replace(/\.git$/, "");
|
|
17939
|
+
const segments = path.split("/");
|
|
17940
|
+
const [host, owner, name] = segments.length === 2 ? [DEFAULT_REPO_HOST, ...segments] : segments.length === 3 ? segments : [];
|
|
17941
|
+
if (!host || !owner || !name)
|
|
17942
|
+
throw new ConvexError(REPO_FORMAT_ERROR);
|
|
17943
|
+
if (!HOST_PATTERN.test(host) || !SEGMENT_PATTERN.test(owner) || !SEGMENT_PATTERN.test(name)) {
|
|
17944
|
+
throw new ConvexError(REPO_FORMAT_ERROR);
|
|
17945
|
+
}
|
|
17946
|
+
const key = `${host}/${owner}/${name}`;
|
|
17947
|
+
if (key.length > MAX_PROJECT_REPO_LENGTH) {
|
|
17948
|
+
throw new ConvexError(`Repository must be ${MAX_PROJECT_REPO_LENGTH} characters or fewer.`);
|
|
17949
|
+
}
|
|
17950
|
+
return key;
|
|
17951
|
+
};
|
|
17952
|
+
var tryNormalizeProjectRepo = (value) => {
|
|
17953
|
+
try {
|
|
17954
|
+
return normalizeProjectRepo(value);
|
|
17955
|
+
} catch {
|
|
17956
|
+
return;
|
|
17957
|
+
}
|
|
17958
|
+
};
|
|
17959
|
+
|
|
17960
|
+
// src/lib/repo.ts
|
|
17961
|
+
var currentRepoKey = async () => {
|
|
17962
|
+
const proc = Bun.spawn(["git", "remote", "get-url", "origin"], { stdout: "pipe", stderr: "ignore" });
|
|
17963
|
+
const url2 = await new Response(proc.stdout).text();
|
|
17964
|
+
return await proc.exited === 0 ? tryNormalizeProjectRepo(url2) : undefined;
|
|
17965
|
+
};
|
|
17966
|
+
|
|
17967
|
+
// src/lib/resolve.ts
|
|
17968
|
+
var resolveIssue = async (client3, ref) => {
|
|
17969
|
+
const issue2 = await client3.query(api2.issues.get, { identifier: readIssueRef(ref) });
|
|
17970
|
+
if (!issue2)
|
|
17971
|
+
throw new Error(`No issue matches ${ref}.`);
|
|
17972
|
+
return issue2;
|
|
17973
|
+
};
|
|
17974
|
+
var resolveProject = async (client3, ref) => {
|
|
17975
|
+
const project = await client3.query(api2.projects.get, { id: readProjectRef(ref), today: localToday() });
|
|
17976
|
+
if (!project)
|
|
17977
|
+
throw new Error(`No project matches ${ref}.`);
|
|
17978
|
+
return project;
|
|
17979
|
+
};
|
|
17980
|
+
var repoProject = async (client3) => {
|
|
17981
|
+
const repo = await currentRepoKey();
|
|
17982
|
+
if (!repo)
|
|
17983
|
+
throw new Error("No repository here: not a git checkout with an origin remote.");
|
|
17984
|
+
const project = await client3.query(api2.projects.findByRepo, { repo });
|
|
17985
|
+
if (!project)
|
|
17986
|
+
throw new Error(`No project is connected to ${repo}. Connect one on the project in the dashboard.`);
|
|
17987
|
+
return project;
|
|
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
|
+
};
|
|
18003
|
+
var resolveMilestone = async (client3, projectId, name) => {
|
|
18004
|
+
const milestones = await client3.query(api2.milestones.listByProject, { projectId, today: localToday() });
|
|
18005
|
+
const wanted = name.trim().toLowerCase();
|
|
18006
|
+
const milestone = milestones.find((candidate) => candidate.name.toLowerCase() === wanted);
|
|
18007
|
+
if (!milestone) {
|
|
18008
|
+
const names = milestones.map((candidate) => candidate.name).join(", ");
|
|
18009
|
+
throw new Error(names ? `No milestone named "${name}". The project has: ${names}.` : "The project has no milestones.");
|
|
18010
|
+
}
|
|
18011
|
+
return milestone;
|
|
18012
|
+
};
|
|
18013
|
+
|
|
18014
|
+
// src/commands/issues/comment.ts
|
|
18015
|
+
var comment = command({
|
|
18016
|
+
name: "comment",
|
|
18017
|
+
description: "Comment on an issue",
|
|
18018
|
+
positionals: {
|
|
18019
|
+
id: exports_external.string().describe("Issue identifier, number, or URL"),
|
|
18020
|
+
body: exports_external.string().describe("Comment markdown, or - for stdin")
|
|
18021
|
+
},
|
|
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 } }) => {
|
|
18026
|
+
const client3 = await backendClient();
|
|
18027
|
+
const issue2 = await resolveIssue(client3, id);
|
|
18028
|
+
printAttachments(await attachFiles(client3, issue2._id, attach ?? []));
|
|
18029
|
+
await client3.mutation(api2.issueComments.create, { issueId: issue2._id, bodyMarkdown: await readTextOption(body) });
|
|
18030
|
+
console.log(`Commented on ${issue2.identifier}.`);
|
|
18031
|
+
}
|
|
18032
|
+
});
|
|
18033
|
+
|
|
18034
|
+
// src/commands/issues/create.ts
|
|
17833
18035
|
var create = command({
|
|
18036
|
+
name: "create",
|
|
18037
|
+
description: "Create an issue and print its identifier",
|
|
18038
|
+
positionals: {
|
|
18039
|
+
title: exports_external.string().describe("Issue title")
|
|
18040
|
+
},
|
|
18041
|
+
options: {
|
|
18042
|
+
description: exports_external.string().optional().describe("Markdown description, or - for stdin").meta({ short: "d" }),
|
|
18043
|
+
status: exports_external.enum(ISSUE_STATUSES).optional().describe("Starting status (default backlog)").meta({ short: "s" }),
|
|
18044
|
+
priority: exports_external.enum(ISSUE_PRIORITIES).optional().describe("Priority (default none)").meta({ short: "p" }),
|
|
18045
|
+
estimate: exports_external.coerce.number().pipe(exports_external.literal([...ISSUE_ESTIMATES])).optional().describe(`Points (${ISSUE_ESTIMATES.join("|")})`),
|
|
18046
|
+
due: exports_external.iso.date().optional().describe("Due date (YYYY-MM-DD)"),
|
|
18047
|
+
project: exports_external.string().optional().describe("Project to create the issue in (id or URL)"),
|
|
18048
|
+
here: exports_external.boolean().default(false).describe("Create in the checkout's connected project"),
|
|
18049
|
+
milestone: exports_external.string().optional().describe("Milestone name (needs --project or --here)"),
|
|
18050
|
+
disposition: exports_external.enum(ISSUE_CREATE_DISPOSITIONS).optional().describe("Route immediately (default needs_triage)")
|
|
18051
|
+
},
|
|
18052
|
+
run: async ({ positionals: { title }, options }) => {
|
|
18053
|
+
if (options.project && options.here)
|
|
18054
|
+
throw new Error("Pass --project or --here, not both.");
|
|
18055
|
+
const client3 = await backendClient();
|
|
18056
|
+
const project = options.project ? await resolveProject(client3, options.project) : options.here ? await repoProject(client3) : undefined;
|
|
18057
|
+
if (options.milestone && !project)
|
|
18058
|
+
throw new Error("A milestone needs --project or --here.");
|
|
18059
|
+
const milestone = project && options.milestone ? await resolveMilestone(client3, project._id, options.milestone) : undefined;
|
|
18060
|
+
const { identifier } = await client3.mutation(api2.issues.create, {
|
|
18061
|
+
title,
|
|
18062
|
+
descriptionMarkdown: options.description === undefined ? undefined : await readTextOption(options.description),
|
|
18063
|
+
status: options.status,
|
|
18064
|
+
priority: options.priority,
|
|
18065
|
+
estimate: options.estimate,
|
|
18066
|
+
dueDate: options.due,
|
|
18067
|
+
projectId: project?._id,
|
|
18068
|
+
milestoneId: milestone?._id,
|
|
18069
|
+
disposition: options.disposition
|
|
18070
|
+
});
|
|
18071
|
+
console.log(identifier);
|
|
18072
|
+
}
|
|
18073
|
+
});
|
|
18074
|
+
|
|
18075
|
+
// src/commands/issues/get.ts
|
|
18076
|
+
var SOURCE_LABELS = { github: "GitHub" };
|
|
18077
|
+
var activityLine = (detail) => {
|
|
18078
|
+
const label = detail.kind.replaceAll("_", " ");
|
|
18079
|
+
switch (detail.kind) {
|
|
18080
|
+
case "created":
|
|
18081
|
+
case "description_changed":
|
|
18082
|
+
return label;
|
|
18083
|
+
case "title_changed":
|
|
18084
|
+
case "status_changed":
|
|
18085
|
+
case "priority_changed":
|
|
18086
|
+
case "disposition_changed":
|
|
18087
|
+
case "estimate_changed":
|
|
18088
|
+
case "due_date_changed":
|
|
18089
|
+
case "branch_changed":
|
|
18090
|
+
case "pr_changed":
|
|
18091
|
+
case "project_changed":
|
|
18092
|
+
case "milestone_changed":
|
|
18093
|
+
case "parent_changed":
|
|
18094
|
+
return `${label}: ${detail.from ?? "none"} \u2192 ${detail.to ?? "none"}`;
|
|
18095
|
+
case "label_added":
|
|
18096
|
+
case "label_removed":
|
|
18097
|
+
case "attachment_added":
|
|
18098
|
+
case "attachment_removed":
|
|
18099
|
+
return `${label}: ${detail.name}`;
|
|
18100
|
+
case "child_added":
|
|
18101
|
+
case "child_removed":
|
|
18102
|
+
return `${label}: ${detail.identifier}`;
|
|
18103
|
+
case "relation_added":
|
|
18104
|
+
case "relation_removed":
|
|
18105
|
+
return `${label}: ${detail.relation.replaceAll("_", " ")} ${detail.identifier}`;
|
|
18106
|
+
}
|
|
18107
|
+
};
|
|
18108
|
+
var get = command({
|
|
18109
|
+
name: "get",
|
|
18110
|
+
description: "Show an issue: properties, description, and its feed",
|
|
18111
|
+
positionals: {
|
|
18112
|
+
id: exports_external.string().describe("Issue identifier, number, or URL")
|
|
18113
|
+
},
|
|
18114
|
+
options: {
|
|
18115
|
+
json: exports_external.boolean().default(false).describe("Print as JSON")
|
|
18116
|
+
},
|
|
18117
|
+
run: async ({ positionals: { id }, options: { json: json2 } }) => {
|
|
18118
|
+
const client3 = await backendClient();
|
|
18119
|
+
const issue2 = await resolveIssue(client3, 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
|
+
]);
|
|
18125
|
+
if (json2)
|
|
18126
|
+
return console.log(JSON.stringify({ ...issue2, relations, attachments, feed }, null, 2));
|
|
18127
|
+
const project = issue2.projectId ? await client3.query(api2.projects.get, { id: issue2.projectId, today: localToday() }) : null;
|
|
18128
|
+
const milestone = issue2.milestoneId ? await client3.query(api2.milestones.get, { id: issue2.milestoneId, today: localToday() }) : null;
|
|
18129
|
+
console.log(`${issue2.identifier} ${issue2.title}`);
|
|
18130
|
+
console.log(`Status: ${issue2.status} \xB7 Priority: ${issue2.priority} \xB7 Disposition: ${issue2.disposition}`);
|
|
18131
|
+
if (issue2.estimate !== undefined)
|
|
18132
|
+
console.log(`Estimate: ${issue2.estimate}`);
|
|
18133
|
+
if (issue2.dueDate)
|
|
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}`);
|
|
18139
|
+
if (project)
|
|
18140
|
+
console.log(`Project: ${project.name}${milestone ? ` \xB7 Milestone: ${milestone.name}` : ""}`);
|
|
18141
|
+
if (issue2.parent)
|
|
18142
|
+
console.log(`Parent: ${issue2.parent.identifier} ${issue2.parent.title}`);
|
|
18143
|
+
if (issue2.labels.length > 0)
|
|
18144
|
+
console.log(`Labels: ${issue2.labels.map((label) => label.name).join(", ")}`);
|
|
18145
|
+
if (issue2.children.total > 0)
|
|
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
|
+
}
|
|
18164
|
+
if (issue2.descriptionMarkdown)
|
|
18165
|
+
console.log(`
|
|
18166
|
+
${issue2.descriptionMarkdown}`);
|
|
18167
|
+
if (feed.events.length > 0)
|
|
18168
|
+
console.log(`
|
|
18169
|
+
Feed:`);
|
|
18170
|
+
if (feed.truncated)
|
|
18171
|
+
console.log("(older events truncated)");
|
|
18172
|
+
for (const event of feed.events) {
|
|
18173
|
+
const at = new Date(event.at).toISOString().slice(0, 16).replace("T", " ");
|
|
18174
|
+
if (event.type === "activity") {
|
|
18175
|
+
const via = event.via === undefined ? "" : ` (via ${SOURCE_LABELS[event.via]})`;
|
|
18176
|
+
console.log(`${at} ${activityLine(event.detail)}${via}`);
|
|
18177
|
+
} else {
|
|
18178
|
+
console.log(`${at} comment${event.editedAt ? " (edited)" : ""}:`);
|
|
18179
|
+
for (const line of event.bodyMarkdown.split(`
|
|
18180
|
+
`))
|
|
18181
|
+
console.log(` ${line}`);
|
|
18182
|
+
}
|
|
18183
|
+
}
|
|
18184
|
+
}
|
|
18185
|
+
});
|
|
18186
|
+
|
|
18187
|
+
// src/lib/output.ts
|
|
18188
|
+
var printTable = (rows) => {
|
|
18189
|
+
if (rows.length === 0)
|
|
18190
|
+
return;
|
|
18191
|
+
const columnCount = Math.max(...rows.map((row) => row.length));
|
|
18192
|
+
const widths = Array.from({ length: columnCount }, (_, column) => Math.max(...rows.map((row) => (row[column] ?? "").length)));
|
|
18193
|
+
for (const row of rows) {
|
|
18194
|
+
console.log(row.map((cell, column) => cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd());
|
|
18195
|
+
}
|
|
18196
|
+
};
|
|
18197
|
+
|
|
18198
|
+
// src/commands/issues/list.ts
|
|
18199
|
+
var list = command({
|
|
18200
|
+
name: "list",
|
|
18201
|
+
description: "List open issues, most recently updated first",
|
|
18202
|
+
options: {
|
|
18203
|
+
all: exports_external.boolean().default(false).describe("Include done and canceled issues"),
|
|
18204
|
+
status: exports_external.enum(ISSUE_STATUSES).optional().describe("Only this status").meta({ short: "s" }),
|
|
18205
|
+
priority: exports_external.enum(ISSUE_PRIORITIES).optional().describe("Only this priority").meta({ short: "p" }),
|
|
18206
|
+
disposition: exports_external.enum(ISSUE_DISPOSITIONS).optional().describe("Only this disposition").meta({ short: "d" }),
|
|
18207
|
+
project: exports_external.string().optional().describe("Only this project (id or URL)"),
|
|
18208
|
+
here: exports_external.boolean().default(false).describe("Only the checkout's connected project"),
|
|
18209
|
+
milestone: exports_external.string().optional().describe("Only this milestone (name; needs --project or --here)"),
|
|
18210
|
+
due: exports_external.enum(ISSUE_DUE_DATE_MODES).optional().describe("Only this due-date state"),
|
|
18211
|
+
search: exports_external.string().optional().describe("Free-text search (results come in relevance order)"),
|
|
18212
|
+
limit: exports_external.coerce.number().int().positive().default(ISSUE_LIST_PAGE_SIZE).describe("Most issues to print"),
|
|
18213
|
+
json: exports_external.boolean().default(false).describe("Print as JSON")
|
|
18214
|
+
},
|
|
18215
|
+
run: async ({ options }) => {
|
|
18216
|
+
if (options.project && options.here)
|
|
18217
|
+
throw new Error("Pass --project or --here, not both.");
|
|
18218
|
+
const client3 = await backendClient();
|
|
18219
|
+
const project = options.project ? await resolveProject(client3, options.project) : options.here ? await repoProject(client3) : undefined;
|
|
18220
|
+
if (options.milestone && !project)
|
|
18221
|
+
throw new Error("A milestone filter needs --project or --here.");
|
|
18222
|
+
const milestone = project && options.milestone ? await resolveMilestone(client3, project._id, options.milestone) : undefined;
|
|
18223
|
+
const result = await client3.query(api2.issueViews.query, {
|
|
18224
|
+
source: {
|
|
18225
|
+
type: "custom",
|
|
18226
|
+
query: {
|
|
18227
|
+
layout: "list",
|
|
18228
|
+
groupBy: "none",
|
|
18229
|
+
orderBy: "updated_at",
|
|
18230
|
+
orderDirection: "desc",
|
|
18231
|
+
filters: {
|
|
18232
|
+
query: options.search,
|
|
18233
|
+
statuses: options.status ? [options.status] : options.all ? undefined : ISSUE_STATUSES.filter(issueIsOpen),
|
|
18234
|
+
priorities: options.priority ? [options.priority] : undefined,
|
|
18235
|
+
dispositions: options.disposition ? [options.disposition] : undefined,
|
|
18236
|
+
projectIds: project ? [project._id] : undefined,
|
|
18237
|
+
milestoneIds: milestone ? [milestone._id] : undefined,
|
|
18238
|
+
dueDate: options.due
|
|
18239
|
+
}
|
|
18240
|
+
}
|
|
18241
|
+
},
|
|
18242
|
+
paginationOpts: { numItems: options.limit, cursor: null },
|
|
18243
|
+
today: localToday()
|
|
18244
|
+
});
|
|
18245
|
+
if (options.json)
|
|
18246
|
+
return console.log(JSON.stringify(result.page, null, 2));
|
|
18247
|
+
if (result.page.length === 0)
|
|
18248
|
+
return console.log("No issues match.");
|
|
18249
|
+
printTable([
|
|
18250
|
+
["ID", "TITLE", "STATUS", "PRIORITY", "DISPOSITION", "DUE", "UPDATED"],
|
|
18251
|
+
...result.page.map((issue2) => [
|
|
18252
|
+
issue2.identifier,
|
|
18253
|
+
issue2.title,
|
|
18254
|
+
issue2.status,
|
|
18255
|
+
issue2.priority,
|
|
18256
|
+
issue2.disposition,
|
|
18257
|
+
issue2.dueDate ?? "-",
|
|
18258
|
+
new Date(issue2.updatedAt).toISOString().slice(0, 10)
|
|
18259
|
+
])
|
|
18260
|
+
]);
|
|
18261
|
+
if (!result.isDone)
|
|
18262
|
+
console.log(`
|
|
18263
|
+
More issues match; raise --limit past ${options.limit}.`);
|
|
18264
|
+
}
|
|
18265
|
+
});
|
|
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
|
+
|
|
18285
|
+
// src/commands/issues/remove.ts
|
|
18286
|
+
var remove = command({
|
|
18287
|
+
name: "delete",
|
|
18288
|
+
description: "Delete an issue permanently (sub-issues survive as top-level issues)",
|
|
18289
|
+
positionals: {
|
|
18290
|
+
id: exports_external.string().describe("Issue identifier, number, or URL")
|
|
18291
|
+
},
|
|
18292
|
+
run: async ({ positionals: { id } }) => {
|
|
18293
|
+
const client3 = await backendClient();
|
|
18294
|
+
const issue2 = await resolveIssue(client3, id);
|
|
18295
|
+
await client3.mutation(api2.issues.remove, { id: issue2._id });
|
|
18296
|
+
console.log(`Deleted ${issue2.identifier}.`);
|
|
18297
|
+
}
|
|
18298
|
+
});
|
|
18299
|
+
|
|
18300
|
+
// src/commands/issues/route.ts
|
|
18301
|
+
var route = command({
|
|
18302
|
+
name: "route",
|
|
18303
|
+
description: "Route an issue to a disposition (wontfix also cancels it)",
|
|
18304
|
+
positionals: {
|
|
18305
|
+
id: exports_external.string().describe("Issue identifier, number, or URL"),
|
|
18306
|
+
disposition: exports_external.enum(ISSUE_DISPOSITIONS).describe("Where the issue goes next")
|
|
18307
|
+
},
|
|
18308
|
+
options: {
|
|
18309
|
+
comment: exports_external.string().optional().describe("Why, as a comment (required for needs_info and wontfix), or - for stdin").meta({ short: "c" })
|
|
18310
|
+
},
|
|
18311
|
+
run: async ({ positionals: { id, disposition }, options: { comment: comment2 } }) => {
|
|
18312
|
+
const client3 = await backendClient();
|
|
18313
|
+
const issue2 = await resolveIssue(client3, id);
|
|
18314
|
+
await client3.mutation(api2.issues.route, {
|
|
18315
|
+
id: issue2._id,
|
|
18316
|
+
disposition,
|
|
18317
|
+
comment: comment2 === undefined ? undefined : await readTextOption(comment2)
|
|
18318
|
+
});
|
|
18319
|
+
console.log(`Routed ${issue2.identifier} to ${disposition}.`);
|
|
18320
|
+
}
|
|
18321
|
+
});
|
|
18322
|
+
|
|
18323
|
+
// src/commands/issues/set.ts
|
|
18324
|
+
var set2 = command({
|
|
18325
|
+
name: "set",
|
|
18326
|
+
description: "Set an issue's status, priority, estimate, due date, project, milestone, parent, or labels",
|
|
18327
|
+
positionals: {
|
|
18328
|
+
id: exports_external.string().describe("Issue identifier, number, or URL")
|
|
18329
|
+
},
|
|
18330
|
+
options: {
|
|
18331
|
+
status: exports_external.enum(ISSUE_STATUSES).optional().describe("New status").meta({ short: "s" }),
|
|
18332
|
+
priority: exports_external.enum(ISSUE_PRIORITIES).optional().describe("New priority").meta({ short: "p" }),
|
|
18333
|
+
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 }),
|
|
18334
|
+
due: exports_external.iso.date().nullable().optional().describe("Due date (YYYY-MM-DD), or --no-due to clear").meta({ negatable: true }),
|
|
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 }),
|
|
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 }),
|
|
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)")
|
|
18340
|
+
},
|
|
18341
|
+
run: async ({ positionals: { id }, options }) => {
|
|
18342
|
+
const { status: status2, priority, estimate, due, project, milestone, parent, label, "no-label": noLabel } = options;
|
|
18343
|
+
if (Object.values(options).every((value) => value === undefined))
|
|
18344
|
+
throw new Error("Nothing to set. Pass --status, --priority, --estimate, --due, --project, --milestone, --parent, or --label.");
|
|
18345
|
+
const client3 = await backendClient();
|
|
18346
|
+
const issue2 = await resolveIssue(client3, id);
|
|
18347
|
+
const projectId = project === undefined ? issue2.projectId : project === null ? undefined : (await resolveProject(client3, project))._id;
|
|
18348
|
+
let milestoneId;
|
|
18349
|
+
if (milestone === undefined) {
|
|
18350
|
+
milestoneId = projectId === issue2.projectId ? issue2.milestoneId : undefined;
|
|
18351
|
+
} else if (milestone !== null) {
|
|
18352
|
+
if (projectId === undefined)
|
|
18353
|
+
throw new Error("A milestone needs a project. Pass --project too.");
|
|
18354
|
+
milestoneId = (await resolveMilestone(client3, projectId, milestone))._id;
|
|
18355
|
+
}
|
|
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 ?? []);
|
|
18359
|
+
if (parent !== undefined)
|
|
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 });
|
|
18365
|
+
if (status2 !== undefined)
|
|
18366
|
+
await client3.mutation(api2.issues.setStatus, { id: issue2._id, status: status2 });
|
|
18367
|
+
if (priority !== undefined)
|
|
18368
|
+
await client3.mutation(api2.issues.setPriority, { id: issue2._id, priority });
|
|
18369
|
+
if (estimate !== undefined)
|
|
18370
|
+
await client3.mutation(api2.issues.setEstimate, { id: issue2._id, estimate: estimate ?? undefined });
|
|
18371
|
+
if (due !== undefined)
|
|
18372
|
+
await client3.mutation(api2.issues.setDueDate, { id: issue2._id, dueDate: due ?? undefined });
|
|
18373
|
+
if (project !== undefined || milestone !== undefined)
|
|
18374
|
+
await client3.mutation(api2.issues.setMembership, { id: issue2._id, projectId, milestoneId });
|
|
18375
|
+
console.log(`Updated ${issue2.identifier}.`);
|
|
18376
|
+
}
|
|
18377
|
+
});
|
|
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
|
+
|
|
18434
|
+
// src/commands/issues/update.ts
|
|
18435
|
+
var update = command({
|
|
18436
|
+
name: "update",
|
|
18437
|
+
description: "Rewrite an issue's title or description, or attach files",
|
|
18438
|
+
positionals: {
|
|
18439
|
+
id: exports_external.string().describe("Issue identifier, number, or URL")
|
|
18440
|
+
},
|
|
18441
|
+
options: {
|
|
18442
|
+
title: exports_external.string().optional().describe("New title").meta({ short: "t" }),
|
|
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" })
|
|
18445
|
+
},
|
|
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.");
|
|
18449
|
+
const client3 = await backendClient();
|
|
18450
|
+
const issue2 = await resolveIssue(client3, id);
|
|
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
|
+
}
|
|
18459
|
+
console.log(`Updated ${issue2.identifier}.`);
|
|
18460
|
+
}
|
|
18461
|
+
});
|
|
18462
|
+
|
|
18463
|
+
// src/commands/issues/index.ts
|
|
18464
|
+
var issues = group({
|
|
18465
|
+
name: "issues",
|
|
18466
|
+
description: "Track and triage issues",
|
|
18467
|
+
commands: [create, list, get, update, set2, route, start, relate, unrelate, comment, remove]
|
|
18468
|
+
});
|
|
18469
|
+
|
|
18470
|
+
// src/lib/group.ts
|
|
18471
|
+
import { basename as basename2 } from "path";
|
|
18472
|
+
var git = async (...args) => {
|
|
18473
|
+
const proc = Bun.spawn(["git", ...args], { stdout: "pipe", stderr: "ignore" });
|
|
18474
|
+
const [output, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
18475
|
+
return exitCode === 0 ? output.trim() || null : null;
|
|
18476
|
+
};
|
|
18477
|
+
var repoNameFromRemote = (remote) => remote.replace(/\/+$/, "").replace(/\.git$/, "").split(/[/:]/).pop() || undefined;
|
|
18478
|
+
var detectRepoGroup = async () => {
|
|
18479
|
+
const remote = await git("remote", "get-url", "origin");
|
|
18480
|
+
if (remote)
|
|
18481
|
+
return repoNameFromRemote(remote);
|
|
18482
|
+
const root = await git("rev-parse", "--show-toplevel");
|
|
18483
|
+
return root ? basename2(root) : undefined;
|
|
18484
|
+
};
|
|
18485
|
+
var groupForCreate = async (group2) => group2 === null ? undefined : group2 ?? await detectRepoGroup();
|
|
18486
|
+
|
|
18487
|
+
// src/commands/pages/create.ts
|
|
18488
|
+
var create2 = command({
|
|
17834
18489
|
name: "create",
|
|
17835
18490
|
description: "Publish a page and print its URL",
|
|
17836
18491
|
positionals: {
|
|
@@ -17859,7 +18514,7 @@ var create = command({
|
|
|
17859
18514
|
});
|
|
17860
18515
|
|
|
17861
18516
|
// src/commands/pages/get.ts
|
|
17862
|
-
var
|
|
18517
|
+
var get2 = command({
|
|
17863
18518
|
name: "get",
|
|
17864
18519
|
description: "Print a page's HTML",
|
|
17865
18520
|
positionals: {
|
|
@@ -17881,19 +18536,8 @@ var get = command({
|
|
|
17881
18536
|
// ../../packages/backend/convex/lib/format.ts
|
|
17882
18537
|
var formatBytes = (bytes) => bytes < 1024 ? `${bytes} B` : `${Math.round(bytes / 1024)} KB`;
|
|
17883
18538
|
|
|
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
18539
|
// src/commands/pages/list.ts
|
|
17896
|
-
var
|
|
18540
|
+
var list2 = command({
|
|
17897
18541
|
name: "list",
|
|
17898
18542
|
description: "List your published pages",
|
|
17899
18543
|
options: {
|
|
@@ -17923,7 +18567,7 @@ var list = command({
|
|
|
17923
18567
|
});
|
|
17924
18568
|
|
|
17925
18569
|
// src/commands/pages/remove.ts
|
|
17926
|
-
var
|
|
18570
|
+
var remove2 = command({
|
|
17927
18571
|
name: "delete",
|
|
17928
18572
|
description: "Delete a page",
|
|
17929
18573
|
positionals: {
|
|
@@ -17950,7 +18594,7 @@ var revert = command({
|
|
|
17950
18594
|
});
|
|
17951
18595
|
|
|
17952
18596
|
// src/commands/pages/update.ts
|
|
17953
|
-
var
|
|
18597
|
+
var update2 = command({
|
|
17954
18598
|
name: "update",
|
|
17955
18599
|
description: "Replace a page's HTML, title, group, mode, or visibility",
|
|
17956
18600
|
positionals: {
|
|
@@ -18011,50 +18655,9 @@ var versions2 = command({
|
|
|
18011
18655
|
var pages = group({
|
|
18012
18656
|
name: "pages",
|
|
18013
18657
|
description: "Publish HTML documents to the web",
|
|
18014
|
-
commands: [
|
|
18658
|
+
commands: [create2, list2, get2, update2, versions2, revert, remove2]
|
|
18015
18659
|
});
|
|
18016
18660
|
|
|
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
18661
|
// src/commands/project.ts
|
|
18059
18662
|
var project = command({
|
|
18060
18663
|
name: "project",
|
|
@@ -18084,6 +18687,192 @@ var project = command({
|
|
|
18084
18687
|
}
|
|
18085
18688
|
});
|
|
18086
18689
|
|
|
18690
|
+
// src/commands/projects/create.ts
|
|
18691
|
+
var create3 = command({
|
|
18692
|
+
name: "create",
|
|
18693
|
+
description: "Create a project and print its id",
|
|
18694
|
+
positionals: {
|
|
18695
|
+
name: exports_external.string().describe("Project name")
|
|
18696
|
+
},
|
|
18697
|
+
options: {
|
|
18698
|
+
summary: exports_external.string().optional().describe("One-line summary").meta({ short: "s" }),
|
|
18699
|
+
description: exports_external.string().optional().describe("Markdown description, or - for stdin").meta({ short: "d" }),
|
|
18700
|
+
status: exports_external.enum(PROJECT_STATUSES).optional().describe("Starting status (default planned)"),
|
|
18701
|
+
health: exports_external.enum(PROJECT_HEALTHS).optional().describe("Health"),
|
|
18702
|
+
target: exports_external.iso.date().optional().describe("Target date (YYYY-MM-DD)"),
|
|
18703
|
+
repo: exports_external.string().optional().describe("Repository to connect (owner/name or any remote URL)")
|
|
18704
|
+
},
|
|
18705
|
+
run: async ({ positionals: { name }, options }) => {
|
|
18706
|
+
const projectId = await (await backendClient()).mutation(api2.projects.create, {
|
|
18707
|
+
name,
|
|
18708
|
+
summary: options.summary,
|
|
18709
|
+
descriptionMarkdown: options.description === undefined ? undefined : await readTextOption(options.description),
|
|
18710
|
+
status: options.status,
|
|
18711
|
+
health: options.health,
|
|
18712
|
+
targetDate: options.target,
|
|
18713
|
+
repo: options.repo
|
|
18714
|
+
});
|
|
18715
|
+
console.log(projectId);
|
|
18716
|
+
}
|
|
18717
|
+
});
|
|
18718
|
+
|
|
18719
|
+
// src/commands/projects/get.ts
|
|
18720
|
+
var get3 = command({
|
|
18721
|
+
name: "get",
|
|
18722
|
+
description: "Show a project: properties, description, and milestones",
|
|
18723
|
+
positionals: {
|
|
18724
|
+
id: exports_external.string().describe("Project id or URL")
|
|
18725
|
+
},
|
|
18726
|
+
options: {
|
|
18727
|
+
json: exports_external.boolean().default(false).describe("Print as JSON")
|
|
18728
|
+
},
|
|
18729
|
+
run: async ({ positionals: { id }, options: { json: json2 } }) => {
|
|
18730
|
+
const client3 = await backendClient();
|
|
18731
|
+
const project2 = await resolveProject(client3, id);
|
|
18732
|
+
const milestones = await client3.query(api2.milestones.listByProject, {
|
|
18733
|
+
projectId: project2._id,
|
|
18734
|
+
today: localToday()
|
|
18735
|
+
});
|
|
18736
|
+
if (json2)
|
|
18737
|
+
return console.log(JSON.stringify({ ...project2, milestones }, null, 2));
|
|
18738
|
+
console.log(project2.name);
|
|
18739
|
+
if (project2.summary)
|
|
18740
|
+
console.log(project2.summary);
|
|
18741
|
+
if (project2.repo)
|
|
18742
|
+
console.log(`Repo: ${project2.repo}`);
|
|
18743
|
+
console.log(`Status: ${project2.status}${project2.health ? ` \xB7 Health: ${project2.health}` : ""}`);
|
|
18744
|
+
console.log(`Progress: ${project2.progress.done}/${project2.progress.total} issues done`);
|
|
18745
|
+
if (project2.targetDate)
|
|
18746
|
+
console.log(`Target date: ${project2.targetDate}${project2.isOverdue ? " (overdue)" : ""}`);
|
|
18747
|
+
if (project2.descriptionMarkdown)
|
|
18748
|
+
console.log(`
|
|
18749
|
+
${project2.descriptionMarkdown}`);
|
|
18750
|
+
if (milestones.length > 0) {
|
|
18751
|
+
console.log(`
|
|
18752
|
+
Milestones:`);
|
|
18753
|
+
for (const milestone of milestones) {
|
|
18754
|
+
const target = milestone.targetDate ? ` \xB7 target ${milestone.targetDate}${milestone.isOverdue ? " (overdue)" : ""}` : "";
|
|
18755
|
+
console.log(` ${milestone.name} \u2014 ${milestone.progress.done}/${milestone.progress.total} done${target}`);
|
|
18756
|
+
}
|
|
18757
|
+
}
|
|
18758
|
+
}
|
|
18759
|
+
});
|
|
18760
|
+
|
|
18761
|
+
// src/commands/projects/list.ts
|
|
18762
|
+
var list3 = command({
|
|
18763
|
+
name: "list",
|
|
18764
|
+
description: "List your projects, most recently updated first",
|
|
18765
|
+
options: {
|
|
18766
|
+
limit: exports_external.coerce.number().int().positive().default(50).describe("Most projects to print"),
|
|
18767
|
+
json: exports_external.boolean().default(false).describe("Print as JSON")
|
|
18768
|
+
},
|
|
18769
|
+
run: async ({ options: { limit, json: json2 } }) => {
|
|
18770
|
+
const result = await (await backendClient()).query(api2.projects.listPaginated, {
|
|
18771
|
+
paginationOpts: { numItems: limit, cursor: null },
|
|
18772
|
+
today: localToday()
|
|
18773
|
+
});
|
|
18774
|
+
if (json2)
|
|
18775
|
+
return console.log(JSON.stringify(result.page, null, 2));
|
|
18776
|
+
if (result.page.length === 0)
|
|
18777
|
+
return console.log("No projects yet.");
|
|
18778
|
+
printTable([
|
|
18779
|
+
["NAME", "STATUS", "HEALTH", "PROGRESS", "TARGET", "REPO", "ID"],
|
|
18780
|
+
...result.page.map((project2) => [
|
|
18781
|
+
project2.name,
|
|
18782
|
+
project2.status,
|
|
18783
|
+
project2.health ?? "-",
|
|
18784
|
+
`${project2.progress.done}/${project2.progress.total}`,
|
|
18785
|
+
project2.targetDate ? `${project2.targetDate}${project2.isOverdue ? " (overdue)" : ""}` : "-",
|
|
18786
|
+
project2.repo ?? "-",
|
|
18787
|
+
project2._id
|
|
18788
|
+
])
|
|
18789
|
+
]);
|
|
18790
|
+
if (!result.isDone)
|
|
18791
|
+
console.log(`
|
|
18792
|
+
More projects exist; raise --limit past ${limit}.`);
|
|
18793
|
+
}
|
|
18794
|
+
});
|
|
18795
|
+
|
|
18796
|
+
// src/commands/projects/remove.ts
|
|
18797
|
+
var remove3 = command({
|
|
18798
|
+
name: "delete",
|
|
18799
|
+
description: "Delete a project and its milestones (its issues survive, projectless)",
|
|
18800
|
+
positionals: {
|
|
18801
|
+
id: exports_external.string().describe("Project id or URL")
|
|
18802
|
+
},
|
|
18803
|
+
run: async ({ positionals: { id } }) => {
|
|
18804
|
+
const client3 = await backendClient();
|
|
18805
|
+
const project2 = await resolveProject(client3, id);
|
|
18806
|
+
await client3.mutation(api2.projects.remove, { id: project2._id });
|
|
18807
|
+
console.log(`Deleted ${project2.name}.`);
|
|
18808
|
+
}
|
|
18809
|
+
});
|
|
18810
|
+
|
|
18811
|
+
// src/commands/projects/set.ts
|
|
18812
|
+
var set3 = command({
|
|
18813
|
+
name: "set",
|
|
18814
|
+
description: "Set a project's status, health, target date, or repository",
|
|
18815
|
+
positionals: {
|
|
18816
|
+
id: exports_external.string().describe("Project id or URL")
|
|
18817
|
+
},
|
|
18818
|
+
options: {
|
|
18819
|
+
status: exports_external.enum(PROJECT_STATUSES).optional().describe("New status").meta({ short: "s" }),
|
|
18820
|
+
health: exports_external.enum(PROJECT_HEALTHS).nullable().optional().describe("New health, or --no-health to clear").meta({ negatable: true }),
|
|
18821
|
+
target: exports_external.iso.date().nullable().optional().describe("Target date (YYYY-MM-DD), or --no-target to clear").meta({ negatable: true }),
|
|
18822
|
+
repo: exports_external.string().nullable().optional().describe("Repository to connect (owner/name or any remote URL), or --no-repo to disconnect").meta({ negatable: true })
|
|
18823
|
+
},
|
|
18824
|
+
run: async ({ positionals: { id }, options }) => {
|
|
18825
|
+
const { status: status2, health, target, repo } = options;
|
|
18826
|
+
if (Object.values(options).every((value) => value === undefined))
|
|
18827
|
+
throw new Error("Nothing to set. Pass --status, --health, --target, or --repo.");
|
|
18828
|
+
const client3 = await backendClient();
|
|
18829
|
+
const project2 = await resolveProject(client3, id);
|
|
18830
|
+
if (repo !== undefined)
|
|
18831
|
+
await client3.mutation(api2.projects.setRepo, { id: project2._id, repo: repo ?? undefined });
|
|
18832
|
+
if (status2 !== undefined)
|
|
18833
|
+
await client3.mutation(api2.projects.setStatus, { id: project2._id, status: status2 });
|
|
18834
|
+
if (health !== undefined)
|
|
18835
|
+
await client3.mutation(api2.projects.setHealth, { id: project2._id, health: health ?? undefined });
|
|
18836
|
+
if (target !== undefined)
|
|
18837
|
+
await client3.mutation(api2.projects.setTargetDate, { id: project2._id, targetDate: target ?? undefined });
|
|
18838
|
+
console.log(`Updated ${project2.name}.`);
|
|
18839
|
+
}
|
|
18840
|
+
});
|
|
18841
|
+
|
|
18842
|
+
// src/commands/projects/update.ts
|
|
18843
|
+
var update3 = command({
|
|
18844
|
+
name: "update",
|
|
18845
|
+
description: "Rewrite a project's name, summary, or description",
|
|
18846
|
+
positionals: {
|
|
18847
|
+
id: exports_external.string().describe("Project id or URL")
|
|
18848
|
+
},
|
|
18849
|
+
options: {
|
|
18850
|
+
name: exports_external.string().optional().describe("New name").meta({ short: "n" }),
|
|
18851
|
+
summary: exports_external.string().optional().describe("New one-line summary").meta({ short: "s" }),
|
|
18852
|
+
description: exports_external.string().optional().describe("New markdown description, or - for stdin").meta({ short: "d" })
|
|
18853
|
+
},
|
|
18854
|
+
run: async ({ positionals: { id }, options: { name, summary, description } }) => {
|
|
18855
|
+
if (name === undefined && summary === undefined && description === undefined)
|
|
18856
|
+
throw new Error("Nothing to update. Pass --name, --summary, or --description.");
|
|
18857
|
+
const client3 = await backendClient();
|
|
18858
|
+
const project2 = await resolveProject(client3, id);
|
|
18859
|
+
await client3.mutation(api2.projects.update, {
|
|
18860
|
+
id: project2._id,
|
|
18861
|
+
name,
|
|
18862
|
+
summary,
|
|
18863
|
+
descriptionMarkdown: description === undefined ? undefined : await readTextOption(description)
|
|
18864
|
+
});
|
|
18865
|
+
console.log(`Updated ${name ?? project2.name}.`);
|
|
18866
|
+
}
|
|
18867
|
+
});
|
|
18868
|
+
|
|
18869
|
+
// src/commands/projects/index.ts
|
|
18870
|
+
var projects = group({
|
|
18871
|
+
name: "projects",
|
|
18872
|
+
description: "Manage projects",
|
|
18873
|
+
commands: [create3, list3, get3, update3, set3, remove3]
|
|
18874
|
+
});
|
|
18875
|
+
|
|
18087
18876
|
// src/commands/upgrade.ts
|
|
18088
18877
|
var upgrade = command({
|
|
18089
18878
|
name: "upgrade",
|
|
@@ -18099,7 +18888,7 @@ var upgrade = command({
|
|
|
18099
18888
|
var rootCommand = group({
|
|
18100
18889
|
name: "kds",
|
|
18101
18890
|
description: "KDS CLI",
|
|
18102
|
-
commands: [auth, pages, project, upgrade],
|
|
18891
|
+
commands: [auth, issues, pages, project, projects, upgrade],
|
|
18103
18892
|
options: {
|
|
18104
18893
|
version: exports_external.boolean().default(false).describe("Show the version").meta({ short: "v" })
|
|
18105
18894
|
},
|