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.
package/src/config.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Configuration resolution.
3
3
  *
4
4
  * Two tiers with different trust boundaries:
5
- * - Non-secret settings (team, workspace, sort): flag > env > project
5
+ * - Non-secret settings (team, workspace, project, sort): flag > env > project
6
6
  * config > selected workspace profile (team only) > user config
7
7
  * (`~/.config/linear/config.toml`) > the reference CLI's global config
8
8
  * (`~/.config/linear/linear.toml`, read-only).
@@ -68,6 +68,7 @@ import {
68
68
  export interface RawSettings {
69
69
  team?: string;
70
70
  workspace?: string;
71
+ project?: string;
71
72
  sort?: string;
72
73
  apiKey?: string;
73
74
  accessToken?: string;
@@ -102,7 +103,7 @@ export interface SettingOrigin {
102
103
  }
103
104
 
104
105
  /** The non-secret settings, each with its provenance. */
105
- export type SettingOrigins = Record<"team" | "workspace" | "sort", SettingOrigin>;
106
+ export type SettingOrigins = Record<"team" | "workspace" | "project" | "sort", SettingOrigin>;
106
107
 
107
108
  export interface ResolvedConfig {
108
109
  apiKey?: string;
@@ -130,6 +131,8 @@ export interface ResolvedConfig {
130
131
  team?: string;
131
132
  /** Non-secret display workspace setting (separate from credentialWorkspace). */
132
133
  workspace?: string;
134
+ /** Default Linear project name or id for issue creation only. */
135
+ project?: string;
133
136
  sort: string;
134
137
  /** Where `sort` came from, so an invalid value can be blamed precisely. */
135
138
  sortSource: ConfigSource;
@@ -215,7 +218,7 @@ interface UserConfig {
215
218
  workspaces: Record<string, WorkspaceEntry>;
216
219
  }
217
220
 
218
- type NonSecretSetting = "team" | "workspace" | "sort";
221
+ type NonSecretSetting = keyof SettingOrigins;
219
222
  type SettingKeys = Partial<Record<NonSecretSetting, string>>;
220
223
 
221
224
  interface FileSettings {
@@ -299,6 +302,7 @@ function settingsFromParsed(parsed: Record<string, unknown>): FileSettings {
299
302
  const found = [
300
303
  read("team", ["team", "team_id"]),
301
304
  read("workspace", ["workspace"]),
305
+ read("project", ["project"]),
302
306
  read("sort", ["sort", "issue_sort"]),
303
307
  ];
304
308
  const settings: RawSettings = {};
@@ -468,6 +472,7 @@ function fromEnv(env: NodeJS.ProcessEnv): RawSettings {
468
472
  accessToken: env.LINEAR_ACCESS_TOKEN,
469
473
  team: env.LINEAR_TEAM || env.LINEAR_TEAM_ID,
470
474
  workspace: env.LINEAR_WORKSPACE,
475
+ project: env.LINEAR_PROJECT,
471
476
  sort: env.LINEAR_ISSUE_SORT,
472
477
  };
473
478
  }
@@ -645,7 +650,7 @@ export function resolveConfig(inputs: ConfigInputs = {}): ResolvedConfig {
645
650
  ["user", user.settings, userPath, user.settingKeys],
646
651
  ["global", global.settings, globalPath, global.keys],
647
652
  ];
648
- const pick = <K extends "team" | "workspace" | "sort">(key: K): string | undefined =>
653
+ const pick = (key: NonSecretSetting): string | undefined =>
649
654
  tiers.find(([, settings]) => settings[key] !== undefined)?.[1][key];
650
655
 
651
656
  const profileTeam = workspaceProfile ? user.workspaces[workspaceProfile]?.team : undefined;
@@ -664,7 +669,7 @@ export function resolveConfig(inputs: ConfigInputs = {}): ResolvedConfig {
664
669
  ];
665
670
 
666
671
  /** Which tier `pick` took the value from, and which file if it was one. */
667
- const originOf = (key: "team" | "workspace" | "sort"): SettingOrigin => {
672
+ const originOf = (key: NonSecretSetting): SettingOrigin => {
668
673
  const candidates = key === "team" ? teamTiers : tiers;
669
674
  const hit = candidates.find(([, settings]) => settings[key] !== undefined);
670
675
  if (!hit) return { source: "none" };
@@ -678,6 +683,7 @@ export function resolveConfig(inputs: ConfigInputs = {}): ResolvedConfig {
678
683
  const origins: SettingOrigins = {
679
684
  team: originOf("team"),
680
685
  workspace: originOf("workspace"),
686
+ project: originOf("project"),
681
687
  sort: originOf("sort"),
682
688
  };
683
689
 
@@ -696,6 +702,7 @@ export function resolveConfig(inputs: ConfigInputs = {}): ResolvedConfig {
696
702
  // The display setting walks every tier. Credential selection above uses
697
703
  // the flag, env, and project tiers, then the auth-specific user default.
698
704
  workspace: pick("workspace"),
705
+ project: pick("project"),
699
706
  sort: pick("sort") ?? "priority",
700
707
  sortSource: origins.sort.source,
701
708
  origins,
@@ -958,6 +965,246 @@ function workspacesTable(obj: Record<string, unknown>): Record<string, Workspace
958
965
  return obj.workspaces as Record<string, WorkspaceTable>;
959
966
  }
960
967
 
968
+ interface ProfileRename {
969
+ from: string;
970
+ to: string;
971
+ phase: "prepared" | "committed";
972
+ keyring: boolean;
973
+ }
974
+
975
+ export interface RenameProfileResult {
976
+ path: string;
977
+ storage: CredentialStorage;
978
+ team: string | null;
979
+ defaultWorkspace: string | null;
980
+ referenceCredentialsPath: string | null;
981
+ }
982
+
983
+ /**
984
+ * Move an API-key profile without login/logout or revocation. The private config
985
+ * contains a non-secret recovery marker until all stores agree. Copy, verify,
986
+ * commit metadata, update the shared reference list, then delete the old secret.
987
+ * Retrying the same command resumes an interrupted operation; never overwrite a
988
+ * destination belonging to another operation, even when its key happens to match.
989
+ * OAuth lifecycles are deliberately excluded (including dormant fallback records).
990
+ */
991
+ export async function renameApiKeyProfile(
992
+ from: string,
993
+ to: string,
994
+ validate: (apiKey: string) => Promise<void>,
995
+ ): Promise<RenameProfileResult> {
996
+ const slugPattern = /^[a-z0-9](?:[a-z0-9-]{0,98}[a-z0-9])?$/;
997
+ if (!slugPattern.test(from) || !slugPattern.test(to) || from === to)
998
+ throw new CliError("Pass two different lowercase Linear workspace URL slugs.", "usage");
999
+ const path = userConfigPath();
1000
+ const referencePath = referenceCredentialsPath();
1001
+ const lock = acquireUserConfigLock(path);
1002
+ const retry = `linear auth rename ${from} ${to}`;
1003
+ let started = false;
1004
+ let step = "save the recovery marker in the private user config";
1005
+ try {
1006
+ const obj = readUserObject(path);
1007
+ const ws = workspacesTable(obj);
1008
+ const pending = obj.api_key_profile_rename as ProfileRename | undefined;
1009
+ if (
1010
+ pending &&
1011
+ (pending.from !== from ||
1012
+ pending.to !== to ||
1013
+ !["prepared", "committed"].includes(pending.phase) ||
1014
+ typeof pending.keyring !== "boolean")
1015
+ )
1016
+ throw new CliError(
1017
+ "Another profile rename is pending. Complete the original `auth rename <old-slug> <new-slug>` command first (see api_key_profile_rename in the private user config).",
1018
+ "usage",
1019
+ );
1020
+ const user = readUserConfig(path);
1021
+ if (
1022
+ (pending?.phase === "prepared" && Object.hasOwn(ws, to)) ||
1023
+ (pending?.phase === "committed" && Object.hasOwn(ws, from))
1024
+ )
1025
+ throw new CliError(
1026
+ "Profile metadata conflicts with the pending rename. No credentials were removed.",
1027
+ "runtime",
1028
+ );
1029
+ const selected = pending?.phase === "committed" ? to : from;
1030
+ const entry = user.workspaces[selected];
1031
+ if (!entry || !Object.hasOwn(user.workspaces, selected))
1032
+ throw new CliError(`Workspace '${selected}' is not configured.`, "not_found");
1033
+ if (entry.oauth || entry.app || entry.appFileCredential !== undefined)
1034
+ throw new CliError(
1035
+ "`auth rename` supports API-key profiles only. OAuth user sessions rotate refresh tokens; OAuth app profiles bind organization identity and acquire scoped tokens. Neither lifecycle is migrated by this command. Credentials and grants were left unchanged.",
1036
+ "usage",
1037
+ );
1038
+ if (!pending && Object.hasOwn(user.workspaces, to))
1039
+ throw new CliError(
1040
+ `Destination workspace '${to}' is already configured. Nothing changed.`,
1041
+ "usage",
1042
+ );
1043
+
1044
+ const backend = keyring();
1045
+ const readAccount = (account: string): string | null => {
1046
+ try {
1047
+ return backend?.get(account) ?? null;
1048
+ } catch {
1049
+ throw new CliError(
1050
+ "Could not inspect the system keyring. No credentials were removed.",
1051
+ "runtime",
1052
+ );
1053
+ }
1054
+ };
1055
+ // Probe exact names, including unlisted destinations and inactive OAuth records.
1056
+ for (const slug of [from, to]) {
1057
+ if (readAccount(oauthAccount(slug)) !== null || readAccount(`app:${slug}`) !== null)
1058
+ throw new CliError(
1059
+ `Workspace '${slug}' has an OAuth keyring record. API-key-only rename cannot move or replace it. Credentials and grants were left unchanged.`,
1060
+ "usage",
1061
+ );
1062
+ }
1063
+ const oldKey = readAccount(from);
1064
+ const destinationKey = readAccount(to);
1065
+ if (!pending && destinationKey !== null)
1066
+ throw new CliError(
1067
+ `Destination '${to}' already exists in the system keyring. Nothing changed.`,
1068
+ "usage",
1069
+ );
1070
+ const apiKey = entry.apiKey ?? (selected === from ? oldKey : destinationKey);
1071
+ if (!apiKey)
1072
+ throw new CliError(
1073
+ "The selected stored API key is unavailable. No credentials were removed.",
1074
+ "auth",
1075
+ );
1076
+ if (
1077
+ (oldKey !== null && oldKey !== apiKey) ||
1078
+ (destinationKey !== null && destinationKey !== apiKey)
1079
+ )
1080
+ throw new CliError(
1081
+ "A rename credential changed or conflicts with the stored profile. No credentials were removed.",
1082
+ "runtime",
1083
+ );
1084
+ const movesKeyring = pending?.keyring ?? oldKey !== null;
1085
+ if (pending && !movesKeyring && (oldKey !== null || destinationKey !== null))
1086
+ throw new CliError(
1087
+ "Credential storage changed since this rename started. No credentials were removed.",
1088
+ "runtime",
1089
+ );
1090
+ if (movesKeyring && !backend)
1091
+ throw new CliError(
1092
+ "The original system keyring is required to complete this rename.",
1093
+ "runtime",
1094
+ );
1095
+ if (movesKeyring && !backend?.create)
1096
+ throw new CliError(
1097
+ "This keyring has no atomic create-only operation, so API-key profile rename is unsupported for its stored entries. macOS Keychain and file-only profiles are supported. No credentials were changed.",
1098
+ "runtime",
1099
+ );
1100
+ if (pending?.phase === "prepared" && movesKeyring && oldKey === null)
1101
+ throw new CliError(
1102
+ "The original keyring entry is missing. Restore it before retrying the rename.",
1103
+ "runtime",
1104
+ );
1105
+
1106
+ // API keys have no historical organization-ID binding. The exact stored key,
1107
+ // never an invocation override, proves its organization and current URL slug.
1108
+ const configBefore = existsSync(path) ? readFileSync(path, "utf8") : null;
1109
+ const referenceBefore = existsSync(referencePath) ? readFileSync(referencePath, "utf8") : null;
1110
+ await validate(apiKey);
1111
+ if (
1112
+ (existsSync(path) ? readFileSync(path, "utf8") : null) !== configBefore ||
1113
+ (existsSync(referencePath) ? readFileSync(referencePath, "utf8") : null) !==
1114
+ referenceBefore ||
1115
+ readAccount(from) !== oldKey ||
1116
+ readAccount(to) !== destinationKey ||
1117
+ [from, to].some(
1118
+ (slug) => readAccount(oauthAccount(slug)) !== null || readAccount(`app:${slug}`) !== null,
1119
+ )
1120
+ )
1121
+ throw new CliError(
1122
+ "Profile storage changed during validation. Retry the rename; no credentials were removed.",
1123
+ "runtime",
1124
+ );
1125
+
1126
+ const state: ProfileRename = pending ?? { from, to, phase: "prepared", keyring: movesKeyring };
1127
+ started = true;
1128
+ if (!pending) {
1129
+ obj.api_key_profile_rename = state;
1130
+ writeUserObject(path, obj);
1131
+ }
1132
+ if (state.phase === "prepared") {
1133
+ if (movesKeyring) {
1134
+ step = "copy and verify the destination API key in the system keyring";
1135
+ // A prior prepared attempt may already have copied this exact key.
1136
+ // Otherwise the backend itself must reject a concurrent destination.
1137
+ if (destinationKey === null) backend!.create!(to, apiKey);
1138
+ if (readAccount(to) !== apiKey) throw new Error("Keyring copy could not be verified");
1139
+ }
1140
+ ws[to] = { ...(ws[from] ?? {}) };
1141
+ if (!entry.apiKey) ws[to]!.keyring = true;
1142
+ delete ws[from];
1143
+ if (user.defaultWorkspace === from) obj.default_workspace = to;
1144
+ if (obj.workspace === from) obj.workspace = to;
1145
+ obj.api_key_profile_rename = { ...state, phase: "committed" };
1146
+ step = "commit the renamed profile in the private user config";
1147
+ writeUserObject(path, obj);
1148
+ }
1149
+
1150
+ // Preserve unknown reference settings. Its inline-key format is never read
1151
+ // as a credential source and must never be rewritten as a workspace list.
1152
+ let updatedReference: string | null = null;
1153
+ const reference = readReferenceCredentials(referencePath);
1154
+ if (reference.workspaces.includes(from)) {
1155
+ const referenceObj = readUserObject(referencePath);
1156
+ referenceObj.workspaces = [
1157
+ ...new Set(reference.workspaces.map((s) => (s === from ? to : s))),
1158
+ ];
1159
+ if (referenceObj.default === from) referenceObj.default = to;
1160
+ step = "update the reference CLI's shared credentials list";
1161
+ writeUserObject(referencePath, referenceObj);
1162
+ updatedReference = referencePath;
1163
+ }
1164
+ if (movesKeyring) {
1165
+ step = "remove the old API-key entry from the system keyring";
1166
+ if (
1167
+ readAccount(to) !== apiKey ||
1168
+ (readAccount(from) !== null && readAccount(from) !== apiKey)
1169
+ )
1170
+ throw new Error("Keyring changed before cleanup");
1171
+ backend!.delete(from);
1172
+ if (readAccount(from) !== null) throw new Error("Old keyring entry remains");
1173
+ }
1174
+ step = "clear the recovery marker in the private user config";
1175
+ delete obj.api_key_profile_rename;
1176
+ writeUserObject(path, obj);
1177
+ return {
1178
+ path,
1179
+ storage: entry.apiKey ? "file" : "keychain",
1180
+ team: asString(ws[to]?.team) ?? null,
1181
+ defaultWorkspace:
1182
+ asString(obj.default_workspace) ??
1183
+ (reference.defaultWorkspace === from ? to : reference.defaultWorkspace) ??
1184
+ null,
1185
+ referenceCredentialsPath: updatedReference,
1186
+ };
1187
+ } catch (err) {
1188
+ if (!started) throw err;
1189
+ // Store errors may contain secrets. Keep a stable recovery instruction and
1190
+ // retain the marker; at least one validated copy always precedes deletion.
1191
+ throw new CliError(
1192
+ `Profile rename is incomplete: could not ${step}. No grant was revoked. A credential copy is retained under '${from}' or '${to}'. Rerun \`${retry}\` to resume; do not log out either profile during recovery.`,
1193
+ "runtime",
1194
+ );
1195
+ } finally {
1196
+ releaseLockMarker(lock);
1197
+ }
1198
+ }
1199
+
1200
+ /** Read-only report: discovery is bounded to cwd plus explicitly supplied files. */
1201
+ export function profileRenameProjectUpdates(from: string, to: string, paths: string[]) {
1202
+ return [...new Set(paths)].flatMap((path) => {
1203
+ const { settings } = readTomlFile(path);
1204
+ return settings.workspace === from ? [{ path, key: "workspace", from, to }] : [];
1205
+ });
1206
+ }
1207
+
961
1208
  export interface WriteCredentialResult {
962
1209
  /** Our config file — always written (the slug list lives there). */
963
1210
  path: string;
@@ -1708,7 +1955,7 @@ export function redactKey(key: string | undefined): string {
1708
1955
  // ---------------------------------------------------------------------------
1709
1956
 
1710
1957
  /** The settings `config set` / `config init` will write to a project file. */
1711
- export const SETTABLE_KEYS = ["team", "workspace", "sort"] as const;
1958
+ export const SETTABLE_KEYS = ["team", "workspace", "sort", "project"] as const;
1712
1959
  export type SettableKey = (typeof SETTABLE_KEYS)[number];
1713
1960
 
1714
1961
  /**
@@ -1719,6 +1966,7 @@ export type SettableKey = (typeof SETTABLE_KEYS)[number];
1719
1966
  const KEY_ALIASES: Record<SettableKey, readonly string[]> = {
1720
1967
  team: ["team", "team_id"],
1721
1968
  workspace: ["workspace"],
1969
+ project: ["project"],
1722
1970
  sort: ["sort", "issue_sort"],
1723
1971
  };
1724
1972
 
@@ -35,6 +35,8 @@ export interface KeyringBackend {
35
35
  get(account: string): string | null;
36
36
  /** Create or replace the entry. */
37
37
  set(account: string, secret: string): void;
38
+ /** Atomically create a new entry, failing if it exists. Absent when unsupported. */
39
+ create?(account: string, secret: string): void;
38
40
  /** Remove the entry. Returns false when there was none. */
39
41
  delete(account: string): boolean;
40
42
  }
@@ -118,6 +120,26 @@ export const macosKeychain: KeyringBackend = {
118
120
  }
119
121
  },
120
122
 
123
+ create(account, secret) {
124
+ // Omitting -U makes the Keychain enforce uniqueness at insertion time.
125
+ // Keep the credential on stdin, just like set(), but never replace a racer.
126
+ const line =
127
+ [
128
+ "add-generic-password",
129
+ "-a",
130
+ quoteForSecurity(account, "workspace slug"),
131
+ "-s",
132
+ KEYRING_SERVICE,
133
+ "-w",
134
+ quoteForSecurity(secret, "API key"),
135
+ ].join(" ") + "\n";
136
+ const result = runSecurity(["-i"], line);
137
+ if (result.status !== 0)
138
+ throw new KeyringError(
139
+ "Could not create the destination keychain entry; it may already exist or the keychain is unavailable.",
140
+ );
141
+ },
142
+
121
143
  delete(account) {
122
144
  const r = runSecurity(["delete-generic-password", "-a", account, "-s", KEYRING_SERVICE]);
123
145
  if (r.status === 0) return true;
@@ -145,6 +167,8 @@ function runSecretTool(args: string[], input?: string) {
145
167
  export const linuxSecretService: KeyringBackend = {
146
168
  name: "secret-service",
147
169
  label: "system keyring (Secret Service)",
170
+ // secret-tool store replaces matching items and exposes no atomic create-only
171
+ // operation. Deliberately omit create rather than emulate it with get + set.
148
172
 
149
173
  get(account) {
150
174
  const r = runSecretTool(["lookup", "service", KEYRING_SERVICE, "account", account]);
@@ -265,6 +289,10 @@ export function memoryKeyring(initial: Record<string, string> = {}): KeyringBack
265
289
  store,
266
290
  get: (a) => store.get(a) ?? null,
267
291
  set: (a, s) => void store.set(a, s),
292
+ create: (a, s) => {
293
+ if (store.has(a)) throw new KeyringError("Destination keyring entry already exists.");
294
+ store.set(a, s);
295
+ },
268
296
  delete: (a) => store.delete(a),
269
297
  };
270
298
  }
@@ -359,6 +359,16 @@ export function addGlobalOptions(program: Command): Command {
359
359
 
360
360
  /** Commands whose output `--fields` projects: everything that renders a table or a detail block. */
361
361
  export const FIELDS_COMMANDS: ReadonlySet<string> = new Set([
362
+ "release list",
363
+ "release view",
364
+ "release issues",
365
+ "release links",
366
+ "release-pipeline list",
367
+ "release-pipeline view",
368
+ "release-stage list",
369
+ "release-stage view",
370
+ "release-note list",
371
+ "release-note view",
362
372
  "whoami",
363
373
  "auth whoami",
364
374
  "auth status",
@@ -424,6 +434,12 @@ export const FIELDS_COMMANDS: ReadonlySet<string> = new Set([
424
434
 
425
435
  /** Commands with bounded lists: `--limit <n>` caps them, `--all` (or `--limit 0`) exhausts them. */
426
436
  export const LIMIT_COMMANDS: ReadonlySet<string> = new Set([
437
+ "release list",
438
+ "release issues",
439
+ "release links",
440
+ "release-pipeline list",
441
+ "release-stage list",
442
+ "release-note list",
427
443
  "changelog",
428
444
  "attachment list",
429
445
  "comment list",
@@ -501,6 +517,8 @@ export const TEAM_COMMANDS: ReadonlySet<string> = new Set([
501
517
 
502
518
  /** Commands whose confirmation prompt is bypassed by `--yes`. */
503
519
  export const YES_COMMANDS: ReadonlySet<string> = new Set([
520
+ "release remove-issue",
521
+ "release-note delete",
504
522
  "attachment delete",
505
523
  "auth logout",
506
524
  "comment delete",
@@ -68,6 +68,16 @@ import {
68
68
  } from "../services/team.js";
69
69
  import { USER_DETAIL_SHAPE, USER_ROW_SHAPE } from "../services/user.js";
70
70
  import { WEBHOOK_DETAIL_SHAPE, WEBHOOK_ROW_SHAPE } from "../services/webhook.js";
71
+ import {
72
+ RELEASE_ROW_SHAPE,
73
+ RELEASE_DETAIL_SHAPE,
74
+ PIPELINE_ROW_SHAPE,
75
+ STAGE_ROW_SHAPE,
76
+ NOTE_ROW_SHAPE,
77
+ NOTE_DETAIL_SHAPE,
78
+ RELEASE_ISSUE_SHAPE,
79
+ RELEASE_LINK_SHAPE,
80
+ } from "../services/release.js";
71
81
  import type { UpdateRow } from "../lib/status-update.js";
72
82
  import type { SettingOrigin, SettingOrigins } from "../config.js";
73
83
  import type { CommandNode } from "./introspect.js";
@@ -173,6 +183,7 @@ const SETTING_ORIGIN_SHAPE = shape<SettingOrigin>({
173
183
  const SETTING_ORIGINS_SHAPE = shape<SettingOrigins>({
174
184
  team: SETTING_ORIGIN_SHAPE,
175
185
  workspace: SETTING_ORIGIN_SHAPE,
186
+ project: SETTING_ORIGIN_SHAPE,
176
187
  sort: SETTING_ORIGIN_SHAPE,
177
188
  });
178
189
 
@@ -186,6 +197,7 @@ const CONFIG_SHOW_SHAPE: ObjectFields = {
186
197
  workspaceProfile: "string|null",
187
198
  team: "string|null",
188
199
  workspace: "string|null",
200
+ project: "string|null",
189
201
  sort: "string",
190
202
  origins: SETTING_ORIGINS_SHAPE,
191
203
  userConfigPath: "string",
@@ -311,6 +323,39 @@ const RAW_API: OutputShape = {
311
323
  // ---------------------------------------------------------------------------
312
324
 
313
325
  export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
326
+ release: null,
327
+ "release list": list(RELEASE_ROW_SHAPE),
328
+ "release view": object(RELEASE_DETAIL_SHAPE),
329
+ "release create": receipt(RELEASE_DETAIL_SHAPE),
330
+ "release update": receipt(RELEASE_DETAIL_SHAPE),
331
+ "release sync": receipt(RELEASE_DETAIL_SHAPE),
332
+ "release update-version": receipt(RELEASE_DETAIL_SHAPE),
333
+ "release complete": receipt(RELEASE_DETAIL_SHAPE),
334
+ "release issues": list(RELEASE_ISSUE_SHAPE),
335
+ "release links": list(RELEASE_LINK_SHAPE),
336
+ "release add-issue": receipt({
337
+ id: "string",
338
+ releaseId: "string",
339
+ issueId: "string",
340
+ identifier: "string",
341
+ }),
342
+ "release remove-issue": receipt({ releaseId: "string", issue: "string", removed: "boolean" }),
343
+ "release-pipeline": null,
344
+ "release-pipeline list": list(PIPELINE_ROW_SHAPE),
345
+ "release-pipeline view": object(PIPELINE_ROW_SHAPE),
346
+ "release-pipeline create": receipt(PIPELINE_ROW_SHAPE),
347
+ "release-pipeline update": receipt(PIPELINE_ROW_SHAPE),
348
+ "release-stage": null,
349
+ "release-stage list": list(STAGE_ROW_SHAPE),
350
+ "release-stage view": object(STAGE_ROW_SHAPE),
351
+ "release-stage create": receipt(STAGE_ROW_SHAPE),
352
+ "release-stage update": receipt(STAGE_ROW_SHAPE),
353
+ "release-note": null,
354
+ "release-note list": list(NOTE_ROW_SHAPE),
355
+ "release-note view": object(NOTE_DETAIL_SHAPE),
356
+ "release-note create": receipt(NOTE_DETAIL_SHAPE),
357
+ "release-note update": receipt(NOTE_DETAIL_SHAPE),
358
+ "release-note delete": receipt({ id: "string", deleted: "boolean" }),
314
359
  api: RAW_API,
315
360
 
316
361
  changelog: list({
@@ -360,6 +405,22 @@ export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
360
405
  teamMetadataRemoved: "boolean",
361
406
  }),
362
407
  "auth migrate": receipt({ success: "boolean", migrated: ["string"], path: "string" }),
408
+ "auth rename": receipt({
409
+ success: "boolean",
410
+ from: "string",
411
+ workspace: "string",
412
+ credentialType: "string",
413
+ organization: { id: "string", name: "string", urlKey: "string" },
414
+ path: "string",
415
+ storage: "string",
416
+ team: "string|null",
417
+ defaultWorkspace: "string|null",
418
+ referenceCredentialsPath: "string|null",
419
+ checkedProjectConfigs: ["string"],
420
+ projectConfigUpdates: [{ path: "string", key: "string", from: "string", to: "string" }],
421
+ globalConfigUpdates: [{ path: "string", key: "string", from: "string", to: "string" }],
422
+ otherProjects: "string",
423
+ }),
363
424
  "auth status": object({
364
425
  authenticated: "boolean",
365
426
  credentialType: "string|null",
@@ -393,7 +454,13 @@ export const OUTPUT_SHAPES: Record<string, OutputShape | null> = {
393
454
  completion: { kind: "none", note: "always prints a shell script; --json has no effect" },
394
455
 
395
456
  config: runs("config show", object(CONFIG_SHOW_SHAPE)),
396
- "config init": receipt({ success: "boolean", path: "string", team: "string", "sort?": "string" }),
457
+ "config init": receipt({
458
+ success: "boolean",
459
+ path: "string",
460
+ team: "string",
461
+ "sort?": "string",
462
+ "project?": "string",
463
+ }),
397
464
  "config set": receipt({
398
465
  success: "boolean",
399
466
  path: "string",