From 38203a8fb0e64b19d16737e323cc3db90bc18885 Mon Sep 17 00:00:00 2001 From: Peter Schilling Date: Fri, 18 Sep 2026 14:23:24 -0700 Subject: [PATCH] Accept a pasted Linear URL wherever an identifier goes Copying a link out of Linear and pasting it into the CLI is the obvious way to refer to something, and it did not work. `issue view ` failed outright with "Could not determine issue ID". Project and document URLs appeared to work, but only because Linear's API quietly extracts a slug when a whole URL is handed to a `slugId` filter -- undocumented behavior the CLI should not lean on, and one that produces a bare "not found: https://..." the moment the URL points at anything else. Parse the URL locally instead. A new module classifies a reference as ordinary input, a Linear URL naming something the CLI can look up, or a Linear URL it cannot use. The first falls through untouched, so every existing ID, slug, name and UUID lookup is unchanged -- and so is `issue link`, where a lone URL argument still means the thing being linked rather than the issue to link it to. The third is reported rather than retried as a name, which is what turns `/settings` from a mysterious missing project into a sentence. Wiring it into the shared resolvers covers most of the surface at once, since positional arguments and flags like --project, --parent and --team already funnel through them. The rest needed finding: ten commands define their own local resolver shadowing the shared one, and five document commands pass an id straight to `document(id:)`. Each gets the same extraction. A URL for the wrong kind of thing now names what it actually points at, and one from another workspace names both. The workspace comparison trusts the configured workspace even under a raw LINEAR_API_KEY, where the key's organization is not knowable locally: refusing to guess there would disable the check for the most common setup, which is exactly when a URL from the wrong workspace gets pasted. Two things are deliberately refused rather than guessed. Cycle URLs, because Cycle has no url field in the schema and a wrong guess at the shape resolves to the wrong cycle. And comment IDs, because the #comment- anchor keeps only the first eight characters of the UUID; the issue is in the path, so an issue-scoped prefix search looked feasible, but the issue query fetches comments(first: 50) with no pagination, and a lookup that can silently miss a match on page two has no business backing `issue comment delete`. A comment URL still names its issue, so `issue view` takes one. --- CHANGELOG.md | 1 + README.md | 1 + docs/usage.md | 12 + src/commands/document/document-comment-add.ts | 6 +- .../document/document-comment-list.ts | 6 +- src/commands/document/document-delete.ts | 13 +- src/commands/document/document-update.ts | 4 +- src/commands/document/document-view.ts | 8 +- .../initiative-update-create.ts | 10 + .../initiative-update-list.ts | 10 + .../initiative/initiative-add-project.ts | 17 + src/commands/initiative/initiative-archive.ts | 10 + src/commands/initiative/initiative-delete.ts | 10 + .../initiative/initiative-remove-project.ts | 17 + .../initiative/initiative-unarchive.ts | 10 + src/commands/initiative/initiative-update.ts | 10 + src/commands/initiative/initiative-view.ts | 10 + src/commands/issue/issue-comment-delete.ts | 3 + src/commands/issue/issue-comment-update.ts | 3 + src/commands/label/label-delete.ts | 2 + src/commands/project/project-create.ts | 10 + src/utils/linear-url.ts | 368 ++++++++++++++++++ src/utils/linear.ts | 60 +++ src/utils/templates.ts | 2 + .../issue/issue-url-reference.test.ts | 101 +++++ test/utils/linear-url.test.ts | 309 +++++++++++++++ test/utils/linear.test.ts | 63 +++ 27 files changed, 1066 insertions(+), 10 deletions(-) create mode 100644 src/utils/linear-url.ts create mode 100644 test/commands/issue/issue-url-reference.test.ts create mode 100644 test/utils/linear-url.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d1faf871..b54d1f6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Added +- every command that takes an issue, project, document, initiative, or team now also takes the URL you copied out of Linear — `linear issue view https://linear.app/acme/issue/ENG-123/some-title`, `linear project view `, `--project`, `--parent`, `--team` and the rest, since they all resolve through the same lookups. `issue view ` did not work at all before; project and document URLs happened to work through an undocumented server-side behavior in Linear's API, which the CLI no longer relies on. A URL pointing at the wrong kind of thing now says so ("that is an issue URL, not a project URL") instead of reporting the whole URL as a missing name, as does one from another workspace, a page that names nothing (`/settings`), or a cycle URL, whose shape Linear does not publish. Commands whose identifiers have no URL at all — milestones, labels, templates, releases — say that plainly. A comment link carries only the first eight characters of the comment's ID, so it names its issue but cannot be used as a comment ID. `issue link ` is unchanged: a lone URL there is still the thing being linked - `project view` now shows what Linear's project page shows: the long-form overview body (`content`), milestones with their status and progress, resources (`externalLinks`), documents, attachments, related projects with their dependency direction, labels, members, initiatives, and Linear's own progress percentage. Only `description` — the 255-character summary — was rendered before, so a project whose body was written with `project create --content-file` displayed nothing of it. A project reference can now be a UUID, slug ID, or exact name everywhere, including with `--web`/`--app`, and long output pages like `issue view` does (`--no-pager` to disable). `--json` keeps the GraphQL field names and the `{ nodes, pageInfo }` shape of every connection - `project view` with no argument opens a searchable list of projects to pick from, scoped like `project list` — the configured team, or the whole workspace when no team is set. It only prompts when stdin and stdout are both terminals; piped, redirected, in CI, or with `--json` it says a project is required instead of hanging on a prompt nobody can answer - `issue archive ` archives an issue through Linear's `issueArchive` mutation, distinct from `issue delete`, which trashes it. It resolves identifiers like the other issue commands, prompts with the identifier and title unless `--confirm`/`-y` is passed, reports an already-archived issue instead of silently succeeding, and takes `--bulk`, `--bulk-file`, and `--bulk-stdin` like `issue delete` ([#285](https://github.com/schpet/linear-cli/pull/285); thanks @martin-piliar for the command and the report in [#284](https://github.com/schpet/linear-cli/issues/284)) diff --git a/README.md b/README.md index ca3aeef8..8f9e7e3c 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,7 @@ linear user list --json # machine-readable output ```bash linear project list # list projects +linear project view https://linear.app/acme/project/mobile-launch-272f50ef9250 # paste a URL from Linear linear project view # pick from a searchable list of projects linear project view # overview, milestones, resources, documents, related projects linear project view "Mobile launch" # a UUID, slug ID, or exact name all work diff --git a/docs/usage.md b/docs/usage.md index 8d2fdfc5..7932b25e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -363,6 +363,18 @@ linear project update PROJECT-ID --initiative "Q4 Bets" --initiative "Platform" linear project list ``` +#### referring to things by URL + +Anywhere the CLI takes an issue, project, document, initiative, or team, you can paste the URL from Linear instead of its ID, slug, or name. + +```bash +linear issue view https://linear.app/acme/issue/ENG-123/some-title +linear project view https://linear.app/acme/project/mobile-launch-272f50ef9250 +linear issue query --project https://linear.app/acme/project/mobile-launch-272f50ef9250 +``` + +The scheme is optional, and query strings and title slugs are ignored. A URL for the wrong kind of thing, or from another workspace, is reported as such. + #### view project details Shows the project's overview body, milestones, resources, documents, attachments, related projects, latest status update, issue counts, and details. A project is a UUID, slug ID, or exact name. diff --git a/src/commands/document/document-comment-add.ts b/src/commands/document/document-comment-add.ts index de1cd8f2..dd6b588b 100644 --- a/src/commands/document/document-comment-add.ts +++ b/src/commands/document/document-comment-add.ts @@ -16,6 +16,7 @@ import { REPLY_TO_DESCRIPTION, resolveCommentBody, } from "../../utils/comments.ts" +import { resolveDocumentReference } from "../../utils/linear.ts" // A document comment attaches to the document's content record, not to the // document itself, so look that id up first. `document(id:)` accepts a UUID or @@ -39,10 +40,13 @@ export const commentAddCommand = new Command() .option("-b, --body ", COMMENT_BODY_DESCRIPTION) .option("--body-file ", COMMENT_BODY_FILE_DESCRIPTION) .option("-p, --parent, --reply-to ", REPLY_TO_DESCRIPTION) - .action(async (options, document) => { + .action(async (options, rawDocument) => { const { body, bodyFile, parent } = options try { + // Inside the try: resolution rejects a wrong-kind or cross-workspace URL, + // and those errors have to reach handleError like every other failure. + const document = resolveDocumentReference(rawDocument) const textBody = await resolveCommentBody({ body, bodyFile }) const client = getGraphQLClient() diff --git a/src/commands/document/document-comment-list.ts b/src/commands/document/document-comment-list.ts index 56928182..89f71ad8 100644 --- a/src/commands/document/document-comment-list.ts +++ b/src/commands/document/document-comment-list.ts @@ -10,6 +10,7 @@ import { collectCommentPages, renderCommentThreads, } from "../../utils/comments.ts" +import { resolveDocumentReference } from "../../utils/linear.ts" // `document(id:)` accepts a UUID or a slug ID, so no resolver is needed. const GetDocumentComments = gql(` @@ -34,10 +35,13 @@ export const commentListCommand = new Command() .description("List comments on a document (by ID or slug)") .arguments("") .option("-j, --json", "Output as JSON") - .action(async (options, document) => { + .action(async (options, rawDocument) => { const { json } = options try { + // Inside the try: resolution rejects a wrong-kind or cross-workspace URL, + // and those errors have to reach handleError like every other failure. + const document = resolveDocumentReference(rawDocument) const client = getGraphQLClient() const comments = await collectCommentPages(async (after) => { const data = await translateNotFound( diff --git a/src/commands/document/document-delete.ts b/src/commands/document/document-delete.ts index e28b8e75..7bb1fadf 100644 --- a/src/commands/document/document-delete.ts +++ b/src/commands/document/document-delete.ts @@ -15,6 +15,7 @@ import { NotFoundError, ValidationError, } from "../../utils/errors.ts" +import { resolveDocumentReference } from "../../utils/linear.ts" interface DocumentDeleteResult extends BulkOperationResult { title?: string @@ -71,7 +72,7 @@ export const deleteCommand = new Command() async function handleSingleDelete( // deno-lint-ignore no-explicit-any client: any, - documentId: string, + rawDocumentId: string, options: { yes?: boolean }, ): Promise { const { yes } = options @@ -87,10 +88,11 @@ async function handleSingleDelete( } `) + const documentId = resolveDocumentReference(rawDocumentId) const documentDetails = await client.request(detailsQuery, { id: documentId }) if (!documentDetails?.document) { - throw new NotFoundError("Document", documentId) + throw new NotFoundError("Document", rawDocumentId) } const document = documentDetails.document @@ -189,11 +191,14 @@ async function handleBulkDelete( } `) - let documentUuid = docId + const resolvedDocId = resolveDocumentReference(docId) + let documentUuid = resolvedDocId let title = docId try { - const details = await client.request(detailsQuery, { id: docId }) + const details = await client.request(detailsQuery, { + id: resolvedDocId, + }) if (details?.document) { documentUuid = details.document.id title = details.document.title diff --git a/src/commands/document/document-update.ts b/src/commands/document/document-update.ts index 4d8a8d86..b175556c 100644 --- a/src/commands/document/document-update.ts +++ b/src/commands/document/document-update.ts @@ -20,6 +20,7 @@ import { toDocumentTargetInput, } from "./attachment-target.ts" import { withMarkdownHint } from "../../utils/markdown-help.ts" +import { resolveDocumentReference } from "../../utils/linear.ts" const GetDocumentForEdit = gql(` query GetDocumentForEdit($id: String!) { @@ -233,9 +234,10 @@ export const updateCommand = new Command() edit, force, }, - documentId, + rawDocumentId, ) => { try { + const documentId = resolveDocumentReference(rawDocumentId) const targetOptions: DocumentTargetOptions = { project, issue, diff --git a/src/commands/document/document-view.ts b/src/commands/document/document-view.ts index b5ab4551..be1210f9 100644 --- a/src/commands/document/document-view.ts +++ b/src/commands/document/document-view.ts @@ -16,6 +16,7 @@ import { isNotFoundError, NotFoundError, } from "../../utils/errors.ts" +import { resolveDocumentReference } from "../../utils/linear.ts" const GetDocument = gql(` query GetDocument($id: String!) { @@ -174,13 +175,14 @@ export const viewCommand = new Command() .option("-w, --web", "Open document in browser") .option("--json", "Output full document as JSON") .option("--no-download", "Keep remote URLs instead of downloading files") - .action(async ({ raw, web, json, download }, id) => { + .action(async ({ raw, web, json, download }, rawId) => { const { Spinner } = await import("@std/cli/unstable-spinner") const showSpinner = shouldShowSpinner() && !raw && !json const spinner = showSpinner ? new Spinner() : null - spinner?.start() try { + const id = resolveDocumentReference(rawId) + spinner?.start() const client = getGraphQLClient() const result = json ? { document: await getDocumentWithAllComments(client, id) } @@ -293,7 +295,7 @@ export const viewCommand = new Command() // Report through handleError like every other failure; throwing from // here would escape the action and print a stack trace instead. const reported = isClientError(error) && isNotFoundError(error) - ? new NotFoundError("Document", id) + ? new NotFoundError("Document", rawId) : error handleError(reported, "Failed to view document") } diff --git a/src/commands/initiative-update/initiative-update-create.ts b/src/commands/initiative-update/initiative-update-create.ts index 00a2c493..6c20f138 100644 --- a/src/commands/initiative-update/initiative-update-create.ts +++ b/src/commands/initiative-update/initiative-update-create.ts @@ -12,6 +12,7 @@ import { import { getGraphQLClient } from "../../utils/graphql.ts" import { shouldShowSpinner } from "../../utils/hyperlink.ts" import { withMarkdownHint } from "../../utils/markdown-help.ts" +import { expectLinearUrlKind } from "../../utils/linear-url.ts" const HEALTH_VALUES = ["onTrack", "atRisk", "offTrack"] as const type HealthValue = (typeof HEALTH_VALUES)[number] @@ -43,6 +44,15 @@ async function resolveInitiativeId( client: any, idOrSlugOrName: string, ): Promise { + const urlRef = expectLinearUrlKind( + idOrSlugOrName, + "initiative", + "an initiative URL, UUID, slug ID, or exact name", + ) + if (urlRef != null) { + idOrSlugOrName = urlRef.slugId + } + // Try as UUID first if ( /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( diff --git a/src/commands/initiative-update/initiative-update-list.ts b/src/commands/initiative-update/initiative-update-list.ts index 9233a9e3..5601b0de 100644 --- a/src/commands/initiative-update/initiative-update-list.ts +++ b/src/commands/initiative-update/initiative-update-list.ts @@ -8,6 +8,7 @@ import { import { handleError, NotFoundError } from "../../utils/errors.ts" import { getGraphQLClient } from "../../utils/graphql.ts" import { shouldShowSpinner } from "../../utils/hyperlink.ts" +import { expectLinearUrlKind } from "../../utils/linear-url.ts" /** * Resolve initiative ID from UUID, slug, or name @@ -17,6 +18,15 @@ async function resolveInitiativeId( client: any, idOrSlugOrName: string, ): Promise { + const urlRef = expectLinearUrlKind( + idOrSlugOrName, + "initiative", + "an initiative URL, UUID, slug ID, or exact name", + ) + if (urlRef != null) { + idOrSlugOrName = urlRef.slugId + } + // Try as UUID first if ( /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( diff --git a/src/commands/initiative/initiative-add-project.ts b/src/commands/initiative/initiative-add-project.ts index d72031ce..c46921fb 100644 --- a/src/commands/initiative/initiative-add-project.ts +++ b/src/commands/initiative/initiative-add-project.ts @@ -3,6 +3,7 @@ import { gql } from "../../__codegen__/gql.ts" import { getGraphQLClient } from "../../utils/graphql.ts" import { shouldShowSpinner } from "../../utils/hyperlink.ts" import { CliError, handleError, NotFoundError } from "../../utils/errors.ts" +import { expectLinearUrlKind } from "../../utils/linear-url.ts" const AddProjectToInitiative = gql(` mutation AddProjectToInitiative($input: InitiativeToProjectCreateInput!) { @@ -20,6 +21,14 @@ async function resolveInitiativeId( client: any, idOrSlugOrName: string, ): Promise<{ id: string; name: string } | undefined> { + const urlRef = expectLinearUrlKind( + idOrSlugOrName, + "initiative", + "an initiative URL, UUID, slug ID, or exact name", + ) + if (urlRef != null) { + idOrSlugOrName = urlRef.slugId + } // Try as UUID first if ( /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( @@ -99,6 +108,14 @@ async function resolveProjectId( client: any, idOrSlugOrName: string, ): Promise<{ id: string; name: string } | undefined> { + const urlRef = expectLinearUrlKind( + idOrSlugOrName, + "project", + "a project URL, UUID, slug ID, or exact name", + ) + if (urlRef != null) { + idOrSlugOrName = urlRef.slugId + } // Try as UUID first if ( /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( diff --git a/src/commands/initiative/initiative-archive.ts b/src/commands/initiative/initiative-archive.ts index 3a893750..6d2b75c6 100644 --- a/src/commands/initiative/initiative-archive.ts +++ b/src/commands/initiative/initiative-archive.ts @@ -16,6 +16,7 @@ import { NotFoundError, ValidationError, } from "../../utils/errors.ts" +import { expectLinearUrlKind } from "../../utils/linear-url.ts" interface InitiativeArchiveResult extends BulkOperationResult { name: string @@ -299,6 +300,15 @@ async function resolveInitiativeId( client: any, idOrSlugOrName: string, ): Promise { + const urlRef = expectLinearUrlKind( + idOrSlugOrName, + "initiative", + "an initiative URL, UUID, slug ID, or exact name", + ) + if (urlRef != null) { + idOrSlugOrName = urlRef.slugId + } + // Try as UUID first if ( /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( diff --git a/src/commands/initiative/initiative-delete.ts b/src/commands/initiative/initiative-delete.ts index 9c3e24d8..58fb870f 100644 --- a/src/commands/initiative/initiative-delete.ts +++ b/src/commands/initiative/initiative-delete.ts @@ -16,6 +16,7 @@ import { NotFoundError, ValidationError, } from "../../utils/errors.ts" +import { expectLinearUrlKind } from "../../utils/linear-url.ts" interface InitiativeDeleteResult extends BulkOperationResult { name: string @@ -307,6 +308,15 @@ async function resolveInitiativeId( client: any, idOrSlugOrName: string, ): Promise { + const urlRef = expectLinearUrlKind( + idOrSlugOrName, + "initiative", + "an initiative URL, UUID, slug ID, or exact name", + ) + if (urlRef != null) { + idOrSlugOrName = urlRef.slugId + } + // Try as UUID first if ( /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( diff --git a/src/commands/initiative/initiative-remove-project.ts b/src/commands/initiative/initiative-remove-project.ts index 72081c4b..360c0600 100644 --- a/src/commands/initiative/initiative-remove-project.ts +++ b/src/commands/initiative/initiative-remove-project.ts @@ -9,6 +9,7 @@ import { NotFoundError, ValidationError, } from "../../utils/errors.ts" +import { expectLinearUrlKind } from "../../utils/linear-url.ts" const GetInitiativeToProjects = gql(` query GetInitiativeToProjects($first: Int) { @@ -39,6 +40,14 @@ async function resolveInitiativeId( client: any, idOrSlugOrName: string, ): Promise<{ id: string; name: string } | undefined> { + const urlRef = expectLinearUrlKind( + idOrSlugOrName, + "initiative", + "an initiative URL, UUID, slug ID, or exact name", + ) + if (urlRef != null) { + idOrSlugOrName = urlRef.slugId + } // Try as UUID first if ( /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( @@ -118,6 +127,14 @@ async function resolveProjectId( client: any, idOrSlugOrName: string, ): Promise<{ id: string; name: string } | undefined> { + const urlRef = expectLinearUrlKind( + idOrSlugOrName, + "project", + "a project URL, UUID, slug ID, or exact name", + ) + if (urlRef != null) { + idOrSlugOrName = urlRef.slugId + } // Try as UUID first if ( /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( diff --git a/src/commands/initiative/initiative-unarchive.ts b/src/commands/initiative/initiative-unarchive.ts index 3250e92b..d91115dd 100644 --- a/src/commands/initiative/initiative-unarchive.ts +++ b/src/commands/initiative/initiative-unarchive.ts @@ -9,6 +9,7 @@ import { NotFoundError, ValidationError, } from "../../utils/errors.ts" +import { expectLinearUrlKind } from "../../utils/linear-url.ts" export const unarchiveCommand = new Command() .name("unarchive") @@ -124,6 +125,15 @@ async function resolveInitiativeId( client: any, idOrSlugOrName: string, ): Promise { + const urlRef = expectLinearUrlKind( + idOrSlugOrName, + "initiative", + "an initiative URL, UUID, slug ID, or exact name", + ) + if (urlRef != null) { + idOrSlugOrName = urlRef.slugId + } + // Try as UUID first if ( /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( diff --git a/src/commands/initiative/initiative-update.ts b/src/commands/initiative/initiative-update.ts index dd156c7e..1b616ce9 100644 --- a/src/commands/initiative/initiative-update.ts +++ b/src/commands/initiative/initiative-update.ts @@ -5,6 +5,7 @@ import { getGraphQLClient } from "../../utils/graphql.ts" import { lookupUserId } from "../../utils/linear.ts" import { shouldShowSpinner } from "../../utils/hyperlink.ts" import { CliError, handleError, NotFoundError } from "../../utils/errors.ts" +import { expectLinearUrlKind } from "../../utils/linear-url.ts" // Initiative status options from Linear API const INITIATIVE_STATUSES = [ @@ -229,6 +230,15 @@ async function resolveInitiativeId( client: any, idOrSlugOrName: string, ): Promise { + const urlRef = expectLinearUrlKind( + idOrSlugOrName, + "initiative", + "an initiative URL, UUID, slug ID, or exact name", + ) + if (urlRef != null) { + idOrSlugOrName = urlRef.slugId + } + // Try as UUID first if ( /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( diff --git a/src/commands/initiative/initiative-view.ts b/src/commands/initiative/initiative-view.ts index 9d44b0e8..1dddd4c7 100644 --- a/src/commands/initiative/initiative-view.ts +++ b/src/commands/initiative/initiative-view.ts @@ -6,6 +6,7 @@ import { getGraphQLClient } from "../../utils/graphql.ts" import { formatRelativeTime } from "../../utils/display.ts" import { shouldShowSpinner } from "../../utils/hyperlink.ts" import { handleError, NotFoundError } from "../../utils/errors.ts" +import { expectLinearUrlKind } from "../../utils/linear-url.ts" const GetInitiativeDetails = gql(` query GetInitiativeDetails($id: String!) { @@ -254,6 +255,15 @@ async function resolveInitiativeId( client: any, idOrSlugOrName: string, ): Promise { + const urlRef = expectLinearUrlKind( + idOrSlugOrName, + "initiative", + "an initiative URL, UUID, slug ID, or exact name", + ) + if (urlRef != null) { + idOrSlugOrName = urlRef.slugId + } + // Try as UUID first if ( /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( diff --git a/src/commands/issue/issue-comment-delete.ts b/src/commands/issue/issue-comment-delete.ts index 4932fb68..1711c664 100644 --- a/src/commands/issue/issue-comment-delete.ts +++ b/src/commands/issue/issue-comment-delete.ts @@ -2,6 +2,7 @@ import { Command } from "@cliffy/command" import { gql } from "../../__codegen__/gql.ts" import { getGraphQLClient } from "../../utils/graphql.ts" import { CliError, handleError } from "../../utils/errors.ts" +import { rejectCommentUrl, rejectLinearUrl } from "../../utils/linear-url.ts" export const commentDeleteCommand = new Command() .name("delete") @@ -9,6 +10,8 @@ export const commentDeleteCommand = new Command() .arguments("") .action(async (_options, commentId) => { try { + rejectCommentUrl(commentId) + rejectLinearUrl(commentId, "a comment UUID") const mutation = gql(` mutation DeleteComment($id: String!) { commentDelete(id: $id) { diff --git a/src/commands/issue/issue-comment-update.ts b/src/commands/issue/issue-comment-update.ts index d4779a5d..334a4ed6 100644 --- a/src/commands/issue/issue-comment-update.ts +++ b/src/commands/issue/issue-comment-update.ts @@ -4,6 +4,7 @@ import { gql } from "../../__codegen__/gql.ts" import { getGraphQLClient } from "../../utils/graphql.ts" import { CliError, handleError, ValidationError } from "../../utils/errors.ts" import { withMarkdownHint } from "../../utils/markdown-help.ts" +import { rejectCommentUrl, rejectLinearUrl } from "../../utils/linear-url.ts" export const commentUpdateCommand = new Command() .name("update") @@ -18,6 +19,8 @@ export const commentUpdateCommand = new Command() const { body, bodyFile } = options try { + rejectCommentUrl(commentId) + rejectLinearUrl(commentId, "a comment UUID") // Validate that body and bodyFile are not both provided if (body && bodyFile) { throw new ValidationError( diff --git a/src/commands/label/label-delete.ts b/src/commands/label/label-delete.ts index 25a9174b..c2c6abc7 100644 --- a/src/commands/label/label-delete.ts +++ b/src/commands/label/label-delete.ts @@ -4,6 +4,7 @@ import { gql } from "../../__codegen__/gql.ts" import { getGraphQLClient } from "../../utils/graphql.ts" import { getTeamKey, resolveTeam } from "../../utils/linear.ts" import { shouldShowSpinner } from "../../utils/hyperlink.ts" +import { rejectLinearUrl } from "../../utils/linear-url.ts" import { CliError, handleError, @@ -66,6 +67,7 @@ async function resolveLabelId( nameOrId: string, teamKey?: string, ): Promise