linear-sdk-cli 0.8.3 → 0.9.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.
@@ -0,0 +1,138 @@
1
+ /** Parsing shared by the release command groups; API behavior stays in services. */
2
+ import { Command, Option } from "commander";
3
+ import { resolveBody } from "./body.js";
4
+ import { usageError } from "./errors.js";
5
+ import type { LifecycleInput, ReleaseInput, NoteInput, LinkInput } from "../services/release.js";
6
+
7
+ export const PIPELINE_TYPES = ["scheduled", "continuous"];
8
+ export const STAGE_TYPES = ["planned", "started", "completed", "canceled"];
9
+ export const repeat = (value: string, previous: string[] = []): string[] => [...previous, value];
10
+
11
+ export function booleanValue(value: string): boolean {
12
+ if (value === "true") return true;
13
+ if (value === "false") return false;
14
+ throw usageError("Expected true or false.");
15
+ }
16
+ export function finiteNumber(value: string): number {
17
+ if (!value.trim() || !Number.isFinite(Number(value)))
18
+ throw usageError("Expected a finite number.");
19
+ return Number(value);
20
+ }
21
+
22
+ export function descriptionOptions(command: Command): Command {
23
+ return command
24
+ .addOption(
25
+ new Option("--description <text>", "release description (Markdown)").conflicts([
26
+ "descriptionFile",
27
+ "clearDescription",
28
+ ]),
29
+ )
30
+ .addOption(
31
+ new Option(
32
+ "--description-file <path>",
33
+ "read description from file ('-' for stdin)",
34
+ ).conflicts(["description", "clearDescription"]),
35
+ )
36
+ .addOption(
37
+ new Option("--clear-description", "clear the description").conflicts([
38
+ "description",
39
+ "descriptionFile",
40
+ ]),
41
+ );
42
+ }
43
+ export function releaseInput(opts: Record<string, any>): ReleaseInput {
44
+ return {
45
+ name: opts.name,
46
+ version: opts.releaseVersion,
47
+ commitSha: opts.commitSha,
48
+ stageId: opts.stage,
49
+ startDate: opts.startDate,
50
+ targetDate: opts.targetDate,
51
+ description: description(opts),
52
+ };
53
+ }
54
+ function description(opts: Record<string, any>): string | null | undefined {
55
+ return opts.clearDescription
56
+ ? null
57
+ : resolveBody({ arg: opts.description, file: opts.descriptionFile, interactive: false });
58
+ }
59
+ export function lifecycleOptions(command: Command): Command {
60
+ return descriptionOptions(command)
61
+ .requiredOption("--pipeline <id|slug>", "target pipeline UUID or slug")
62
+ .requiredOption(
63
+ "--release-version <version>",
64
+ "explicit release version within the pipeline (no latest fallback)",
65
+ )
66
+ .option("--name <name>", "release name")
67
+ .option("--link <label=url|url>", "attach an external link (repeatable)", repeat);
68
+ }
69
+ export function lifecycleInput(opts: Record<string, any>): LifecycleInput {
70
+ return {
71
+ name: opts.name,
72
+ description: description(opts),
73
+ links: opts.link?.map(parseReleaseLink),
74
+ };
75
+ }
76
+ export function parseReleaseLink(value: string): LinkInput {
77
+ // Bare URLs may contain '=' in their query string; do not split those.
78
+ const bare = /^https?:\/\//i.test(value);
79
+ const index = value.indexOf("=");
80
+ const url = bare || index < 0 ? value : value.slice(index + 1);
81
+ const label = bare || index < 0 ? undefined : value.slice(0, index);
82
+ try {
83
+ const parsed = new URL(url);
84
+ if (
85
+ !["https:", "http:"].includes(parsed.protocol) ||
86
+ parsed.username ||
87
+ parsed.password ||
88
+ label === ""
89
+ )
90
+ throw new Error();
91
+ } catch {
92
+ throw usageError("--link requires an HTTP(S) URL or Label=URL, without embedded credentials.");
93
+ }
94
+ return { url, ...(label === undefined ? {} : { label }) };
95
+ }
96
+ export function issueReferences(values: string[] | undefined, commitSha: string) {
97
+ return values?.map((value) => {
98
+ const [identifier, sha = commitSha, ...rest] = value.split("=");
99
+ if (
100
+ !/^[a-z][a-z\d]*-\d+$/i.test(identifier ?? "") ||
101
+ rest.length ||
102
+ !/^(?:[a-f\d]{40}|[a-f\d]{64})$/i.test(sha)
103
+ ) {
104
+ throw usageError("--issue/--reverted-issue requires KEY-123 or KEY-123=FULL_SHA.");
105
+ }
106
+ return { identifier: identifier!.toUpperCase(), commitSha: sha };
107
+ });
108
+ }
109
+ export function noteOptions(command: Command): Command {
110
+ return command
111
+ .option("--title <title>", "release note title")
112
+ .addOption(new Option("--body <text>", "release note body (Markdown)").conflicts("bodyFile"))
113
+ .addOption(
114
+ new Option("--body-file <path>", "read Markdown from file ('-' for stdin)").conflicts("body"),
115
+ )
116
+ .addOption(
117
+ new Option("--release <id>", "covered release UUID or slug (repeatable)")
118
+ .argParser(repeat)
119
+ .conflicts(["fromRelease", "toRelease"]),
120
+ )
121
+ .addOption(
122
+ new Option("--from-release <id>", "oldest release in an inclusive createdAt range").conflicts(
123
+ "release",
124
+ ),
125
+ )
126
+ .addOption(
127
+ new Option("--to-release <id>", "newest release in the inclusive range").conflicts("release"),
128
+ );
129
+ }
130
+ export function noteInput(opts: Record<string, any>): NoteInput {
131
+ return {
132
+ title: opts.title,
133
+ content: resolveBody({ arg: opts.body, file: opts.bodyFile, interactive: false }),
134
+ releaseIds: opts.release,
135
+ rangeFromReleaseId: opts.fromRelease,
136
+ rangeToReleaseId: opts.toRelease,
137
+ };
138
+ }
@@ -922,7 +922,10 @@ export interface CreateOptions {
922
922
  state?: string;
923
923
  priority?: number;
924
924
  label?: string[];
925
- project?: string;
925
+ /** Explicit project; null creates outside any project, including template/parent defaults. */
926
+ project?: string | null;
927
+ /** Configured fallback, applied only when neither an explicit nor a parent project wins. */
928
+ defaultProject?: string;
926
929
  milestone?: string;
927
930
  cycle?: string;
928
931
  estimate?: number;
@@ -1305,6 +1308,7 @@ export async function prepareIssueCreate(
1305
1308
  if (opts.clearDelegate) input.delegateId = null;
1306
1309
  if (opts.state) input.stateId = await resolveStateId(client, team.id, opts.state);
1307
1310
  if (opts.label?.length) input.labelIds = await resolveLabelIds(client, opts.label, team.id);
1311
+ if (opts.project === null) input.projectId = null;
1308
1312
  if (opts.project) input.projectId = await resolveProjectId(client, opts.project);
1309
1313
  if (opts.parent) {
1310
1314
  const parent = await resolveIssue(client, opts.parent);
@@ -1312,15 +1316,20 @@ export async function prepareIssueCreate(
1312
1316
  // A sub-issue lands in its parent's project unless told otherwise — what
1313
1317
  // Linear's UI does, and what the reference CLI does. Without this, every
1314
1318
  // `--parent` child sat outside the project its parent belongs to.
1315
- if (!input.projectId) {
1319
+ if (input.projectId === undefined) {
1316
1320
  const parentProject = await parent.project;
1317
1321
  if (parentProject) input.projectId = parentProject.id;
1318
1322
  }
1319
1323
  }
1324
+ if (input.projectId === undefined && opts.defaultProject) {
1325
+ input.projectId = await resolveProjectId(client, opts.defaultProject);
1326
+ }
1320
1327
  if (opts.milestone) {
1321
1328
  // Resolved after the parent so a milestone can name one in the inherited project.
1322
1329
  if (!input.projectId)
1323
- throw usageError("A milestone requires --project (or a --parent in a project).");
1330
+ throw usageError(
1331
+ "A milestone requires --project, a configured project, or a --parent in a project.",
1332
+ );
1324
1333
  input.projectMilestoneId = await resolveMilestoneId(client, input.projectId, opts.milestone);
1325
1334
  }
1326
1335
  if (opts.cycle) input.cycleId = await resolveCycleId(client, team.id, opts.cycle);