linear-sdk-cli 0.1.0 → 0.1.2
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/CHANGELOG.md +46 -46
- package/COVERAGE.md +5 -5
- package/MIGRATING.md +69 -71
- package/README.md +88 -96
- package/package.json +8 -9
- package/skills/linear-sdk-cli/SKILL.md +3 -13
- package/skills/linear-sdk-cli/references/api.md +9 -9
- package/skills/linear-sdk-cli/references/auth.md +0 -18
- package/skills/linear-sdk-cli/references/commands.md +2 -2
- package/skills/linear-sdk-cli/references/config.md +11 -9
- package/skills/linear-sdk-cli/references/issue.md +12 -63
- package/skills/linear-sdk-cli/references/team.md +0 -16
- package/skills/linear-sdk-cli/scripts/generate-docs.ts +2 -6
- package/src/bin/linear.ts +6 -16
- package/src/cli.ts +1 -3
- package/src/commands/agent-session.ts +5 -13
- package/src/commands/api.ts +9 -15
- package/src/commands/attach.ts +4 -15
- package/src/commands/comment.ts +2 -6
- package/src/commands/completion.ts +6 -2
- package/src/commands/cycle.ts +1 -6
- package/src/commands/discover.ts +2 -6
- package/src/commands/document.ts +1 -4
- package/src/commands/initiative.ts +1 -5
- package/src/commands/issue.ts +88 -376
- package/src/commands/meta.ts +31 -139
- package/src/commands/milestone.ts +1 -4
- package/src/commands/notification.ts +6 -15
- package/src/commands/project.ts +4 -17
- package/src/commands/team.ts +0 -13
- package/src/commands/webhook.ts +2 -12
- package/src/config.ts +183 -500
- package/src/context.ts +4 -13
- package/src/git.ts +3 -28
- package/src/lib/body.ts +2 -26
- package/src/lib/errors.ts +7 -32
- package/src/lib/introspect.ts +7 -76
- package/src/lib/keyring.ts +9 -51
- package/src/lib/options.ts +12 -108
- package/src/lib/output-shapes.ts +9 -81
- package/src/lib/pagination.ts +17 -50
- package/src/lib/resolve.ts +7 -40
- package/src/lib/shape.ts +12 -16
- package/src/lib/upload.ts +1 -3
- package/src/output/format.ts +18 -96
- package/src/output/table.ts +7 -7
- package/src/services/agent-session.ts +3 -11
- package/src/services/attachment.ts +5 -14
- package/src/services/comment.ts +4 -12
- package/src/services/cycle.ts +4 -11
- package/src/services/document.ts +10 -29
- package/src/services/favorite.ts +9 -15
- package/src/services/initiative-update.ts +15 -16
- package/src/services/initiative.ts +10 -15
- package/src/services/issue.ts +18 -262
- package/src/services/label.ts +2 -12
- package/src/services/milestone.ts +5 -8
- package/src/services/notification.ts +2 -16
- package/src/services/organization.ts +18 -23
- package/src/services/project.ts +34 -55
- package/src/services/roadmap.ts +6 -7
- package/src/services/state.ts +3 -9
- package/src/services/team.ts +24 -67
- package/src/services/user.ts +2 -2
- package/src/services/webhook.ts +17 -28
- package/skills/linear-sdk-cli/references/open.md +0 -29
- package/src/commands/open.ts +0 -84
- package/src/lib/open.ts +0 -32
- package/src/output/color.ts +0 -52
- package/src/output/markdown.ts +0 -133
- package/src/output/pager.ts +0 -131
package/src/commands/meta.ts
CHANGED
|
@@ -10,8 +10,6 @@ import { withRetry } from "../client.js";
|
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
11
|
import {
|
|
12
12
|
writeCredential,
|
|
13
|
-
probeKeyringCredential,
|
|
14
|
-
adoptKeyringCredential,
|
|
15
13
|
removeCredential,
|
|
16
14
|
setDefaultWorkspace,
|
|
17
15
|
listCredentials,
|
|
@@ -27,8 +25,8 @@ import {
|
|
|
27
25
|
} from "../config.js";
|
|
28
26
|
import { keyring } from "../lib/keyring.js";
|
|
29
27
|
import { readStdinSync } from "../lib/body.js";
|
|
30
|
-
import { authError,
|
|
31
|
-
import {
|
|
28
|
+
import { authError, usageError } from "../lib/errors.js";
|
|
29
|
+
import { promptSecret, promptSelect } from "../lib/prompt.js";
|
|
32
30
|
import { listTeams } from "../services/team.js";
|
|
33
31
|
import { ISSUE_SORTS } from "../services/issue.js";
|
|
34
32
|
import { firstTeam, type Context } from "../context.js";
|
|
@@ -60,79 +58,6 @@ const whoamiAction = action(async (ctx: Context) => {
|
|
|
60
58
|
);
|
|
61
59
|
});
|
|
62
60
|
|
|
63
|
-
interface AuthIdentity {
|
|
64
|
-
user: { id: string; name: string; email: string };
|
|
65
|
-
organization: { id: string; name: string; urlKey: string };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
interface AuthValidationClient {
|
|
69
|
-
viewer: Promise<{ id: string; name: string; email: string }>;
|
|
70
|
-
organization: Promise<{ id: string; name: string; urlKey: string }>;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export type AuthValidationClientFactory = (apiKey: string) => AuthValidationClient;
|
|
74
|
-
|
|
75
|
-
const defaultAuthValidationClientFactory: AuthValidationClientFactory = (apiKey) =>
|
|
76
|
-
new LinearClient({ apiKey }) as unknown as AuthValidationClient;
|
|
77
|
-
let authValidationClientFactory = defaultAuthValidationClientFactory;
|
|
78
|
-
|
|
79
|
-
/** Test seam for command-level auth tests; production always uses LinearClient. */
|
|
80
|
-
export function setAuthValidationClientFactoryForTests(
|
|
81
|
-
factory: AuthValidationClientFactory | undefined,
|
|
82
|
-
): void {
|
|
83
|
-
authValidationClientFactory = factory ?? defaultAuthValidationClientFactory;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Merge the local and global login spellings without silently choosing one. */
|
|
87
|
-
export function selectLoginKey(
|
|
88
|
-
localKey: string | undefined,
|
|
89
|
-
globalKey: string | undefined,
|
|
90
|
-
): string | undefined {
|
|
91
|
-
if (localKey !== undefined && globalKey !== undefined) {
|
|
92
|
-
throw usageError("Pass only one of --key or --api-key to `auth login`.");
|
|
93
|
-
}
|
|
94
|
-
return localKey ?? globalKey;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Validate a credential and prove that an explicitly requested slug matches it. */
|
|
98
|
-
export async function validateAuthCredential(
|
|
99
|
-
apiKey: string,
|
|
100
|
-
requestedWorkspace?: string,
|
|
101
|
-
): Promise<AuthIdentity> {
|
|
102
|
-
const client = authValidationClientFactory(apiKey);
|
|
103
|
-
let me;
|
|
104
|
-
let org;
|
|
105
|
-
try {
|
|
106
|
-
me = await withRetry(() => client.viewer);
|
|
107
|
-
org = await withRetry(() => client.organization);
|
|
108
|
-
} catch (err) {
|
|
109
|
-
const normalized = normalizeError(err);
|
|
110
|
-
if (normalized.code === "auth" || normalized.code === "forbidden") {
|
|
111
|
-
throw authError("That API key was rejected by Linear.");
|
|
112
|
-
}
|
|
113
|
-
throw normalized;
|
|
114
|
-
}
|
|
115
|
-
if (requestedWorkspace && requestedWorkspace !== org.urlKey) {
|
|
116
|
-
throw usageError(
|
|
117
|
-
`--workspace '${requestedWorkspace}' does not match the key's workspace '${org.urlKey}'. Store credentials under their Linear workspace slug.`,
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
return {
|
|
121
|
-
user: { id: me.id, name: me.name, email: me.email },
|
|
122
|
-
organization: { id: org.id, name: org.name, urlKey: org.urlKey },
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function explicitWorkspaceSlug(input: string): string {
|
|
127
|
-
const slug = input.trim();
|
|
128
|
-
if (!/^[a-z0-9](?:[a-z0-9-]{0,98}[a-z0-9])?$/.test(slug)) {
|
|
129
|
-
throw usageError(
|
|
130
|
-
"Workspace slug must be its lowercase Linear URL key (letters, numbers, and hyphens).",
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
return slug;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
61
|
export function registerMeta(program: Command): void {
|
|
137
62
|
program.command("whoami").description("Show the authenticated user").action(whoamiAction);
|
|
138
63
|
|
|
@@ -150,7 +75,10 @@ export function registerMeta(program: Command): void {
|
|
|
150
75
|
.command("login")
|
|
151
76
|
.description("Validate and store a Linear API key for a workspace")
|
|
152
77
|
.option("--key <key>", "API key (otherwise prompted; '-' reads it from stdin)")
|
|
153
|
-
.option(
|
|
78
|
+
.option(
|
|
79
|
+
"--plaintext",
|
|
80
|
+
"Store the key in the config file (0600) instead of the system keyring",
|
|
81
|
+
)
|
|
154
82
|
.action(
|
|
155
83
|
// The global `--workspace <slug>` selects the slug to store under
|
|
156
84
|
// (default: derived from the key's organization urlKey).
|
|
@@ -159,14 +87,14 @@ export function registerMeta(program: Command): void {
|
|
|
159
87
|
// it is a credential, and it must not reach the screen or scrollback.
|
|
160
88
|
// Nothing below echoes it either: the receipt reports the user and
|
|
161
89
|
// where the key went, never the value.
|
|
162
|
-
let key: string | undefined =
|
|
90
|
+
let key: string | undefined = opts.key;
|
|
163
91
|
if (key === "-") {
|
|
164
92
|
key = readStdinSync();
|
|
165
93
|
} else if (key) {
|
|
166
94
|
// argv is world-readable (`ps`) and lands in shell history; say so
|
|
167
95
|
// once, quietly, and carry on — the script author chose it.
|
|
168
96
|
ctx.output.warn(
|
|
169
|
-
"
|
|
97
|
+
"--key on the command line is visible to other processes and shell history; prefer the prompt or `--key -` (stdin).",
|
|
170
98
|
);
|
|
171
99
|
} else {
|
|
172
100
|
key = await promptSecret(ctx, "Linear API key:", { required: true });
|
|
@@ -174,18 +102,25 @@ export function registerMeta(program: Command): void {
|
|
|
174
102
|
key = key.trim();
|
|
175
103
|
if (!key) throw usageError("No API key provided.");
|
|
176
104
|
// Validate before persisting and learn the workspace slug.
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
105
|
+
const client = new LinearClient({ apiKey: key });
|
|
106
|
+
let me;
|
|
107
|
+
let org;
|
|
108
|
+
try {
|
|
109
|
+
me = await client.viewer;
|
|
110
|
+
org = await client.organization;
|
|
111
|
+
} catch {
|
|
112
|
+
throw authError("That API key was rejected by Linear.");
|
|
113
|
+
}
|
|
114
|
+
const slug: string = ctx.options.workspace ?? org.urlKey;
|
|
180
115
|
const { path, storage, keyringLabel } = writeCredential(slug, key, {
|
|
181
116
|
plaintext: opts.plaintext === true,
|
|
182
117
|
});
|
|
183
118
|
const where =
|
|
184
119
|
storage === "keychain"
|
|
185
|
-
? `
|
|
120
|
+
? `Key saved to the ${keyringLabel} (service linear-cli, account '${slug}'); ${path} records the workspace.`
|
|
186
121
|
: opts.plaintext
|
|
187
|
-
? `
|
|
188
|
-
: `
|
|
122
|
+
? `Key saved to ${path} (plaintext, 0600).`
|
|
123
|
+
: `Key saved to ${path} (plaintext, 0600 — no system keyring on this platform).`;
|
|
189
124
|
ctx.output.emit(
|
|
190
125
|
{
|
|
191
126
|
success: true,
|
|
@@ -202,45 +137,6 @@ export function registerMeta(program: Command): void {
|
|
|
202
137
|
}),
|
|
203
138
|
);
|
|
204
139
|
|
|
205
|
-
// adopt ------------------------------------------------------------------
|
|
206
|
-
auth
|
|
207
|
-
.command("adopt <slug>")
|
|
208
|
-
.description("Adopt an existing named credential from the shared OS keyring")
|
|
209
|
-
.action(
|
|
210
|
-
action(async (ctx: Context, _opts, input: string) => {
|
|
211
|
-
const slug = explicitWorkspaceSlug(input);
|
|
212
|
-
if (ctx.options.apiKey !== undefined) {
|
|
213
|
-
throw usageError(
|
|
214
|
-
"`auth adopt` reads only the named OS keyring entry; remove --api-key (or use `auth login --key -` to supply a key on stdin).",
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
if (ctx.options.workspace && ctx.options.workspace !== slug) {
|
|
218
|
-
throw usageError(
|
|
219
|
-
`The positional workspace '${slug}' conflicts with --workspace '${ctx.options.workspace}'.`,
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Probe one exact account only. No keyring enumeration and no secret
|
|
224
|
-
// on argv, stdout, stderr, or disk.
|
|
225
|
-
const found = probeKeyringCredential(slug);
|
|
226
|
-
const identity = await validateAuthCredential(found.apiKey, slug);
|
|
227
|
-
const adopted = adoptKeyringCredential(slug, found.apiKey);
|
|
228
|
-
ctx.output.emit(
|
|
229
|
-
{
|
|
230
|
-
success: true,
|
|
231
|
-
workspace: slug,
|
|
232
|
-
user: identity.user,
|
|
233
|
-
storage: adopted.storage,
|
|
234
|
-
path: adopted.path,
|
|
235
|
-
},
|
|
236
|
-
() =>
|
|
237
|
-
ctx.output.success(
|
|
238
|
-
`Adopted workspace '${slug}' from the ${adopted.keyringLabel}; ${adopted.path} now records the workspace without storing the secret.`,
|
|
239
|
-
),
|
|
240
|
-
);
|
|
241
|
-
}),
|
|
242
|
-
);
|
|
243
|
-
|
|
244
140
|
// list --------------------------------------------------------------------
|
|
245
141
|
auth
|
|
246
142
|
.command("list")
|
|
@@ -367,7 +263,6 @@ export function registerMeta(program: Command): void {
|
|
|
367
263
|
.join(", ")}). Pass --workspace <slug> to choose which to remove.`,
|
|
368
264
|
);
|
|
369
265
|
}
|
|
370
|
-
if (!(await confirmDestructive(ctx, `Remove credential for workspace '${slug}'?`))) return;
|
|
371
266
|
const removed = removeCredential(slug);
|
|
372
267
|
ctx.output.emit({ success: true, workspace: slug, removed }, () =>
|
|
373
268
|
removed
|
|
@@ -384,9 +279,7 @@ export function registerMeta(program: Command): void {
|
|
|
384
279
|
// show (default) ----------------------------------------------------------
|
|
385
280
|
config
|
|
386
281
|
.command("show", { isDefault: true })
|
|
387
|
-
.description(
|
|
388
|
-
"Show the resolved configuration and where each value came from (secrets redacted)",
|
|
389
|
-
)
|
|
282
|
+
.description("Show the resolved configuration and where each value came from (secrets redacted)")
|
|
390
283
|
.action(
|
|
391
284
|
action(async (ctx) => {
|
|
392
285
|
// Re-resolve to expose sources/paths beyond what Context keeps.
|
|
@@ -406,10 +299,8 @@ export function registerMeta(program: Command): void {
|
|
|
406
299
|
if (o.source === "none") return "default";
|
|
407
300
|
return o.path ? `${o.source}: ${o.path}` : o.source;
|
|
408
301
|
};
|
|
409
|
-
const show = (
|
|
410
|
-
|
|
411
|
-
value: string | undefined,
|
|
412
|
-
): string | undefined => (value === undefined ? undefined : `${value} (${from(key)})`);
|
|
302
|
+
const show = (key: keyof typeof c.origins, value: string | undefined): string | undefined =>
|
|
303
|
+
value === undefined ? undefined : `${value} (${from(key)})`;
|
|
413
304
|
ctx.output.detail(
|
|
414
305
|
{
|
|
415
306
|
apiKey: redactKey(c.apiKey),
|
|
@@ -418,6 +309,7 @@ export function registerMeta(program: Command): void {
|
|
|
418
309
|
team: c.team ?? null,
|
|
419
310
|
workspace: c.workspace ?? null,
|
|
420
311
|
sort: c.sort,
|
|
312
|
+
vcs: c.vcs,
|
|
421
313
|
origins: c.origins,
|
|
422
314
|
userConfigPath: c.userConfigPath,
|
|
423
315
|
projectConfigPath: c.projectConfigPath ?? null,
|
|
@@ -429,6 +321,7 @@ export function registerMeta(program: Command): void {
|
|
|
429
321
|
["Team", show("team", c.team)],
|
|
430
322
|
["Workspace", show("workspace", c.workspace)],
|
|
431
323
|
["Sort", show("sort", c.sort)],
|
|
324
|
+
["VCS", show("vcs", c.vcs)],
|
|
432
325
|
["User config", c.userConfigPath],
|
|
433
326
|
["Project config", c.projectConfigPath],
|
|
434
327
|
["Global config", c.globalConfigPath],
|
|
@@ -465,15 +358,11 @@ export function registerMeta(program: Command): void {
|
|
|
465
358
|
.map((t) => ({ name: `${t.name} (${t.key})`, value: t.key })),
|
|
466
359
|
);
|
|
467
360
|
}
|
|
468
|
-
const settings: Partial<Record<SettableKey, string>> = {
|
|
469
|
-
team: validateSetting("team", team),
|
|
470
|
-
};
|
|
361
|
+
const settings: Partial<Record<SettableKey, string>> = { team: validateSetting("team", team) };
|
|
471
362
|
if (opts.sort !== undefined) settings.sort = validateSetting("sort", opts.sort);
|
|
472
363
|
initProjectConfig(path, settings, { force: opts.force === true });
|
|
473
364
|
ctx.output.emit({ success: true, path, ...settings }, () =>
|
|
474
|
-
ctx.output.success(
|
|
475
|
-
`Wrote ${path} (team ${settings.team}${settings.sort ? `, sort ${settings.sort}` : ""}).`,
|
|
476
|
-
),
|
|
365
|
+
ctx.output.success(`Wrote ${path} (team ${settings.team}${settings.sort ? `, sort ${settings.sort}` : ""}).`),
|
|
477
366
|
);
|
|
478
367
|
}),
|
|
479
368
|
);
|
|
@@ -522,6 +411,9 @@ function validateSetting(key: SettableKey, value: string): string {
|
|
|
522
411
|
throw usageError(`Invalid sort '${v}'. Valid values: ${ISSUE_SORTS.join(", ")}.`);
|
|
523
412
|
}
|
|
524
413
|
return v;
|
|
414
|
+
case "vcs":
|
|
415
|
+
if (!["git", "jj"].includes(v)) throw usageError(`Invalid vcs '${v}'. Valid values: git, jj.`);
|
|
416
|
+
return v;
|
|
525
417
|
case "team":
|
|
526
418
|
// A team key: letters and digits, as Linear issues them (TES, ENG2).
|
|
527
419
|
if (!/^[A-Za-z][A-Za-z0-9]*$/.test(v)) {
|
|
@@ -34,10 +34,7 @@ export function formatMilestoneProgress(progress: number | null): string {
|
|
|
34
34
|
return `${Math.round(progress)}%`;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const PROJECT_SCOPE = [
|
|
38
|
-
"-p, --project <name>",
|
|
39
|
-
"the milestone's project, when <id> is a name (names are unique per project only)",
|
|
40
|
-
] as const;
|
|
37
|
+
const PROJECT_SCOPE = ["-p, --project <name>", "the milestone's project, when <id> is a name (names are unique per project only)"] as const;
|
|
41
38
|
|
|
42
39
|
export function registerMilestone(program: Command): void {
|
|
43
40
|
const milestone = program
|
|
@@ -8,7 +8,6 @@ import { confirmDestructive } from "../lib/prompt.js";
|
|
|
8
8
|
import type { Context } from "../context.js";
|
|
9
9
|
import * as svc from "../services/notification.js";
|
|
10
10
|
import type { Column } from "../output/table.js";
|
|
11
|
-
import { ExitCode } from "../lib/errors.js";
|
|
12
11
|
|
|
13
12
|
const ROW_COLUMNS: Column<svc.NotificationRow>[] = [
|
|
14
13
|
{ key: "type", header: "Type", value: (r) => r.type, max: 24 },
|
|
@@ -68,20 +67,10 @@ export function registerNotification(program: Command): void {
|
|
|
68
67
|
action(async (ctx: Context) => {
|
|
69
68
|
const res = await svc.markAllRead(ctx.client);
|
|
70
69
|
ctx.output.emit(res, () => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
ctx.output.warn(
|
|
75
|
-
`Marked ${res.count} of ${res.attempted} notification(s) read; ${res.failed.length} failed`,
|
|
76
|
-
);
|
|
77
|
-
for (const f of res.failed) ctx.output.warn(`${f.id}: ${f.error}`);
|
|
78
|
-
}
|
|
70
|
+
ctx.output.success(`Marked ${res.count} of ${res.attempted} notification(s) read`);
|
|
71
|
+
// A partial batch says so out loud instead of hiding behind the count.
|
|
72
|
+
for (const f of res.failed) ctx.output.warn(`${f.id}: ${f.error}`);
|
|
79
73
|
});
|
|
80
|
-
// A batch receipt belongs on stdout even when only part of the batch
|
|
81
|
-
// succeeded, but exit zero would let `read-all && ...` treat that as a
|
|
82
|
-
// complete success. Preserve the receipt and make the process outcome
|
|
83
|
-
// unambiguous.
|
|
84
|
-
if (!res.success) process.exitCode = ExitCode.Runtime;
|
|
85
74
|
}),
|
|
86
75
|
);
|
|
87
76
|
|
|
@@ -93,7 +82,9 @@ export function registerNotification(program: Command): void {
|
|
|
93
82
|
action(async (ctx: Context, _opts, id: string) => {
|
|
94
83
|
if (!(await confirmDestructive(ctx, `Archive notification ${id}?`))) return;
|
|
95
84
|
const success = await svc.archiveNotification(ctx.client, id);
|
|
96
|
-
ctx.output.emit({ id, archived: success }, () =>
|
|
85
|
+
ctx.output.emit({ id, archived: success }, () =>
|
|
86
|
+
ctx.output.success(`Archived ${id}`),
|
|
87
|
+
);
|
|
97
88
|
}),
|
|
98
89
|
);
|
|
99
90
|
|
package/src/commands/project.ts
CHANGED
|
@@ -36,10 +36,7 @@ function formatProgress(p: number | null): string {
|
|
|
36
36
|
* stderr, once; `--quiet` silences it, `--json` stdout never carries it.
|
|
37
37
|
* Shared by `project list` and the issue queries.
|
|
38
38
|
*/
|
|
39
|
-
export function noteWorkspaceWide(
|
|
40
|
-
ctx: Context,
|
|
41
|
-
opts: { team?: unknown; allTeams?: boolean },
|
|
42
|
-
): void {
|
|
39
|
+
export function noteWorkspaceWide(ctx: Context, opts: { team?: unknown; allTeams?: boolean }): void {
|
|
43
40
|
const teamGiven = Array.isArray(opts.team) ? opts.team.length > 0 : opts.team !== undefined;
|
|
44
41
|
if (!teamGiven && !opts.allTeams && !ctx.defaultTeam) {
|
|
45
42
|
ctx.output.info(
|
|
@@ -100,14 +97,8 @@ export function registerProject(program: Command): void {
|
|
|
100
97
|
["Progress", detail.progress !== null ? formatProgress(detail.progress) : null],
|
|
101
98
|
["Priority", detail.priorityLabel],
|
|
102
99
|
["Lead", detail.lead?.displayName ?? null],
|
|
103
|
-
[
|
|
104
|
-
|
|
105
|
-
detail.teams.length ? detail.teams.map((t) => `${t.key} ${t.name}`).join(", ") : null,
|
|
106
|
-
],
|
|
107
|
-
[
|
|
108
|
-
"Members",
|
|
109
|
-
detail.members.length ? detail.members.map((m) => m.displayName).join(", ") : null,
|
|
110
|
-
],
|
|
100
|
+
["Teams", detail.teams.length ? detail.teams.map((t) => `${t.key} ${t.name}`).join(", ") : null],
|
|
101
|
+
["Members", detail.members.length ? detail.members.map((m) => m.displayName).join(", ") : null],
|
|
111
102
|
["Labels", detail.labels.length ? detail.labels.map((l) => l.name).join(", ") : null],
|
|
112
103
|
["Start", detail.startDate],
|
|
113
104
|
["Target", detail.targetDate],
|
|
@@ -326,11 +317,7 @@ export function registerProject(program: Command): void {
|
|
|
326
317
|
{ key: "name", header: "Name", value: (m) => m.name, max: 40 },
|
|
327
318
|
{ key: "target", header: "Target", value: (m) => m.targetDate ?? "—" },
|
|
328
319
|
// A milestone's progress is already a percentage (TES-648), unlike a project's.
|
|
329
|
-
{
|
|
330
|
-
key: "progress",
|
|
331
|
-
header: "Progress",
|
|
332
|
-
value: (m) => formatMilestoneProgress(m.progress),
|
|
333
|
-
},
|
|
320
|
+
{ key: "progress", header: "Progress", value: (m) => formatMilestoneProgress(m.progress) },
|
|
334
321
|
],
|
|
335
322
|
rows,
|
|
336
323
|
);
|
package/src/commands/team.ts
CHANGED
|
@@ -59,19 +59,6 @@ export function registerTeam(program: Command): void {
|
|
|
59
59
|
}),
|
|
60
60
|
);
|
|
61
61
|
|
|
62
|
-
// id ----------------------------------------------------------------------
|
|
63
|
-
team
|
|
64
|
-
.command("id [key]")
|
|
65
|
-
.description("Print a team's UUID (defaults to the configured team)")
|
|
66
|
-
.action(
|
|
67
|
-
action(async (ctx: Context, _opts, keyArg?: string) => {
|
|
68
|
-
const resolved = await svc.getTeamIdentity(ctx.client, keyArg, ctx.defaultTeam);
|
|
69
|
-
// Human mode is deliberately a single UUID for command substitution;
|
|
70
|
-
// JSON retains the resolved key and name as useful context.
|
|
71
|
-
ctx.output.emit(resolved, () => process.stdout.write(resolved.id + "\n"));
|
|
72
|
-
}),
|
|
73
|
-
);
|
|
74
|
-
|
|
75
62
|
// view --------------------------------------------------------------------
|
|
76
63
|
team
|
|
77
64
|
.command("view [key]")
|
package/src/commands/webhook.ts
CHANGED
|
@@ -91,12 +91,7 @@ export function registerWebhook(program: Command): void {
|
|
|
91
91
|
secret: opts.secret,
|
|
92
92
|
});
|
|
93
93
|
ctx.output.emit(
|
|
94
|
-
{
|
|
95
|
-
id: created.id,
|
|
96
|
-
url: created.url,
|
|
97
|
-
enabled: created.enabled,
|
|
98
|
-
resourceTypes: created.resourceTypes,
|
|
99
|
-
},
|
|
94
|
+
{ id: created.id, url: created.url, enabled: created.enabled, resourceTypes: created.resourceTypes },
|
|
100
95
|
() => ctx.output.success(`Created webhook ${created.id}`),
|
|
101
96
|
);
|
|
102
97
|
}),
|
|
@@ -126,12 +121,7 @@ export function registerWebhook(program: Command): void {
|
|
|
126
121
|
secret: opts.secret,
|
|
127
122
|
});
|
|
128
123
|
ctx.output.emit(
|
|
129
|
-
{
|
|
130
|
-
id: updated.id,
|
|
131
|
-
url: updated.url,
|
|
132
|
-
enabled: updated.enabled,
|
|
133
|
-
resourceTypes: updated.resourceTypes,
|
|
134
|
-
},
|
|
124
|
+
{ id: updated.id, url: updated.url, enabled: updated.enabled, resourceTypes: updated.resourceTypes },
|
|
135
125
|
() => ctx.output.success(`Updated webhook ${updated.id}`),
|
|
136
126
|
);
|
|
137
127
|
}),
|