@dabble/linear-cli 1.0.0 → 1.0.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/bin/linear.mjs CHANGED
@@ -55,11 +55,6 @@ function loadConfig() {
55
55
  // Fall back to env vars if not set by config file
56
56
  if (!LINEAR_API_KEY) LINEAR_API_KEY = process.env.LINEAR_API_KEY || '';
57
57
  if (!TEAM_KEY) TEAM_KEY = process.env.LINEAR_TEAM || '';
58
-
59
- if (!LINEAR_API_KEY || !TEAM_KEY) {
60
- console.error(colors.red("Error: No config file found or missing API key or team key. Run 'linear login' first."));
61
- process.exit(1);
62
- }
63
58
  }
64
59
 
65
60
  function checkAuth() {
@@ -185,9 +180,11 @@ async function cmdIssues(args) {
185
180
  unblocked: 'boolean', u: 'boolean',
186
181
  all: 'boolean', a: 'boolean',
187
182
  open: 'boolean', o: 'boolean',
183
+ backlog: 'boolean', b: 'boolean',
188
184
  mine: 'boolean', m: 'boolean',
189
185
  'in-progress': 'boolean',
190
186
  project: 'string', p: 'string',
187
+ milestone: 'string',
191
188
  state: 'string', s: 'string',
192
189
  label: 'string', l: 'string',
193
190
  });
@@ -196,8 +193,11 @@ async function cmdIssues(args) {
196
193
  const unblocked = opts.unblocked || opts.u;
197
194
  const allStates = opts.all || opts.a;
198
195
  const openOnly = opts.open || opts.o;
196
+ const backlogOnly = opts.backlog || opts.b;
199
197
  const mineOnly = opts.mine || opts.m;
200
- const stateFilter = inProgress ? 'started' : (opts.state || opts.s || 'backlog');
198
+ const projectFilter = opts.project || opts.p;
199
+ const milestoneFilter = opts.milestone;
200
+ const stateFilter = opts.state || opts.s;
201
201
  const labelFilter = opts.label || opts.l;
202
202
 
203
203
  // Get current user ID for filtering/sorting
@@ -211,8 +211,10 @@ async function cmdIssues(args) {
211
211
  identifier
212
212
  title
213
213
  priority
214
+ sortOrder
214
215
  state { name type }
215
216
  project { name }
217
+ projectMilestone { name }
216
218
  assignee { id name }
217
219
  labels { nodes { name } }
218
220
  relations(first: 20) {
@@ -232,13 +234,16 @@ async function cmdIssues(args) {
232
234
  // Check if any issues have assignees (to decide whether to show column)
233
235
  const hasAssignees = issues.some(i => i.assignee);
234
236
 
235
- // Sort: assigned to you first, then by identifier
237
+ // Sort: assigned to you first, then by priority, then by sortOrder
236
238
  issues.sort((a, b) => {
237
239
  const aIsMine = a.assignee?.id === viewerId;
238
240
  const bIsMine = b.assignee?.id === viewerId;
239
241
  if (aIsMine && !bIsMine) return -1;
240
242
  if (!aIsMine && bIsMine) return 1;
241
- return a.identifier.localeCompare(b.identifier);
243
+ // Then by priority (higher = more urgent)
244
+ if ((b.priority || 0) !== (a.priority || 0)) return (b.priority || 0) - (a.priority || 0);
245
+ // Then by sortOrder
246
+ return (b.sortOrder || 0) - (a.sortOrder || 0);
242
247
  });
243
248
 
244
249
  // Helper to format issue row
@@ -256,7 +261,7 @@ async function cmdIssues(args) {
256
261
  return row;
257
262
  };
258
263
 
259
- // Helper to apply common filters (mine, label)
264
+ // Helper to apply common filters (mine, label, project, milestone)
260
265
  const applyFilters = (list) => {
261
266
  let filtered = list;
262
267
  if (mineOnly) {
@@ -267,6 +272,16 @@ async function cmdIssues(args) {
267
272
  i.labels?.nodes?.some(l => l.name.toLowerCase() === labelFilter.toLowerCase())
268
273
  );
269
274
  }
275
+ if (projectFilter) {
276
+ filtered = filtered.filter(i =>
277
+ i.project?.name?.toLowerCase().includes(projectFilter.toLowerCase())
278
+ );
279
+ }
280
+ if (milestoneFilter) {
281
+ filtered = filtered.filter(i =>
282
+ i.projectMilestone?.name?.toLowerCase().includes(milestoneFilter.toLowerCase())
283
+ );
284
+ }
270
285
  return filtered;
271
286
  };
272
287
 
@@ -306,14 +321,28 @@ async function cmdIssues(args) {
306
321
 
307
322
  console.log(colors.bold('Open Issues:\n'));
308
323
  console.log(formatTable(filtered.map(formatRow)));
309
- } else {
324
+ } else if (inProgress) {
325
+ let filtered = issues.filter(i => i.state.type === 'started');
326
+ filtered = applyFilters(filtered);
327
+
328
+ console.log(colors.bold('In Progress:\n'));
329
+ console.log(formatTable(filtered.map(formatRow)));
330
+ } else if (backlogOnly || stateFilter) {
331
+ const targetState = stateFilter || 'backlog';
310
332
  let filtered = issues.filter(i =>
311
- i.state.type === stateFilter || i.state.name.toLowerCase() === stateFilter.toLowerCase()
333
+ i.state.type === targetState || i.state.name.toLowerCase() === targetState.toLowerCase()
312
334
  );
313
335
 
314
336
  filtered = applyFilters(filtered);
315
337
 
316
- console.log(colors.bold(`Issues (${stateFilter}):\n`));
338
+ console.log(colors.bold(`Issues (${targetState}):\n`));
339
+ console.log(formatTable(filtered.map(formatRow)));
340
+ } else {
341
+ // Default: show backlog
342
+ let filtered = issues.filter(i => i.state.type === 'backlog');
343
+ filtered = applyFilters(filtered);
344
+
345
+ console.log(colors.bold('Issues (backlog):\n'));
317
346
  console.log(formatTable(filtered.map(formatRow)));
318
347
  }
319
348
  }
@@ -473,6 +502,7 @@ async function cmdIssueCreate(args) {
473
502
  title: 'string', t: 'string',
474
503
  description: 'string', d: 'string',
475
504
  project: 'string', p: 'string',
505
+ milestone: 'string',
476
506
  parent: 'string',
477
507
  state: 'string', s: 'string',
478
508
  assign: 'boolean',
@@ -485,6 +515,7 @@ async function cmdIssueCreate(args) {
485
515
  const title = opts.title || opts.t || opts._[0];
486
516
  const description = opts.description || opts.d || '';
487
517
  const project = opts.project || opts.p;
518
+ const milestone = opts.milestone;
488
519
  const parent = opts.parent;
489
520
  const shouldAssign = opts.assign;
490
521
  const estimate = (opts.estimate || opts.e || '').toLowerCase();
@@ -494,7 +525,7 @@ async function cmdIssueCreate(args) {
494
525
 
495
526
  if (!title) {
496
527
  console.error(colors.red('Error: Title is required'));
497
- console.error('Usage: linear issue create --title "Issue title" [--project "..."] [--parent ISSUE-X] [--estimate M] [--assign] [--label bug] [--blocks ISSUE-X] [--blocked-by ISSUE-X]');
528
+ console.error('Usage: linear issue create --title "Issue title" [--project "..."] [--milestone "..."] [--parent ISSUE-X] [--estimate M] [--assign] [--label bug] [--blocks ISSUE-X] [--blocked-by ISSUE-X]');
498
529
  process.exit(1);
499
530
  }
500
531
 
@@ -513,17 +544,47 @@ async function cmdIssueCreate(args) {
513
544
  process.exit(1);
514
545
  }
515
546
 
516
- // Look up project ID
547
+ // Look up project and milestone IDs
517
548
  let projectId = null;
518
- if (project) {
549
+ let milestoneId = null;
550
+ if (project || milestone) {
519
551
  const projectsResult = await gql(`{
520
552
  team(id: "${TEAM_KEY}") {
521
- projects(first: 50) { nodes { id name } }
553
+ projects(first: 50) {
554
+ nodes {
555
+ id name
556
+ projectMilestones { nodes { id name } }
557
+ }
558
+ }
522
559
  }
523
560
  }`);
524
561
  const projects = projectsResult.data?.team?.projects?.nodes || [];
525
- const match = projects.find(p => p.name.toLowerCase().includes(project.toLowerCase()));
526
- if (match) projectId = match.id;
562
+
563
+ if (project) {
564
+ const projectMatch = projects.find(p => p.name.toLowerCase().includes(project.toLowerCase()));
565
+ if (projectMatch) {
566
+ projectId = projectMatch.id;
567
+ // Look for milestone within this project
568
+ if (milestone) {
569
+ const milestoneMatch = projectMatch.projectMilestones?.nodes?.find(m =>
570
+ m.name.toLowerCase().includes(milestone.toLowerCase())
571
+ );
572
+ if (milestoneMatch) milestoneId = milestoneMatch.id;
573
+ }
574
+ }
575
+ } else if (milestone) {
576
+ // Search all projects for the milestone
577
+ for (const p of projects) {
578
+ const milestoneMatch = p.projectMilestones?.nodes?.find(m =>
579
+ m.name.toLowerCase().includes(milestone.toLowerCase())
580
+ );
581
+ if (milestoneMatch) {
582
+ projectId = p.id; // Auto-set project from milestone
583
+ milestoneId = milestoneMatch.id;
584
+ break;
585
+ }
586
+ }
587
+ }
527
588
  }
528
589
 
529
590
  // Look up label ID
@@ -561,6 +622,7 @@ async function cmdIssueCreate(args) {
561
622
 
562
623
  const input = { teamId, title, description };
563
624
  if (projectId) input.projectId = projectId;
625
+ if (milestoneId) input.projectMilestoneId = milestoneId;
564
626
  if (parent) input.parentId = parent;
565
627
  if (assigneeId) input.assigneeId = assigneeId;
566
628
  if (estimate) input.estimate = ESTIMATE_MAP[estimate];
@@ -616,6 +678,8 @@ async function cmdIssueUpdate(args) {
616
678
  title: 'string', t: 'string',
617
679
  description: 'string', d: 'string',
618
680
  state: 'string', s: 'string',
681
+ project: 'string', p: 'string',
682
+ milestone: 'string',
619
683
  append: 'string', a: 'string',
620
684
  blocks: 'string',
621
685
  'blocked-by': 'string',
@@ -623,6 +687,8 @@ async function cmdIssueUpdate(args) {
623
687
 
624
688
  const blocksIssue = opts.blocks;
625
689
  const blockedByIssue = opts['blocked-by'];
690
+ const projectName = opts.project || opts.p;
691
+ const milestoneName = opts.milestone;
626
692
  const input = {};
627
693
 
628
694
  if (opts.title || opts.t) input.title = opts.title || opts.t;
@@ -649,6 +715,46 @@ async function cmdIssueUpdate(args) {
649
715
  if (match) input.stateId = match.id;
650
716
  }
651
717
 
718
+ // Handle project and milestone
719
+ if (projectName || milestoneName) {
720
+ const projectsResult = await gql(`{
721
+ team(id: "${TEAM_KEY}") {
722
+ projects(first: 50) {
723
+ nodes {
724
+ id name
725
+ projectMilestones { nodes { id name } }
726
+ }
727
+ }
728
+ }
729
+ }`);
730
+ const projects = projectsResult.data?.team?.projects?.nodes || [];
731
+
732
+ if (projectName) {
733
+ const projectMatch = projects.find(p => p.name.toLowerCase().includes(projectName.toLowerCase()));
734
+ if (projectMatch) {
735
+ input.projectId = projectMatch.id;
736
+ if (milestoneName) {
737
+ const milestoneMatch = projectMatch.projectMilestones?.nodes?.find(m =>
738
+ m.name.toLowerCase().includes(milestoneName.toLowerCase())
739
+ );
740
+ if (milestoneMatch) input.projectMilestoneId = milestoneMatch.id;
741
+ }
742
+ }
743
+ } else if (milestoneName) {
744
+ // Search all projects for the milestone
745
+ for (const p of projects) {
746
+ const milestoneMatch = p.projectMilestones?.nodes?.find(m =>
747
+ m.name.toLowerCase().includes(milestoneName.toLowerCase())
748
+ );
749
+ if (milestoneMatch) {
750
+ input.projectId = p.id;
751
+ input.projectMilestoneId = milestoneMatch.id;
752
+ break;
753
+ }
754
+ }
755
+ }
756
+ }
757
+
652
758
  // Handle blocking relations (can be set even without other updates)
653
759
  const hasRelationUpdates = blocksIssue || blockedByIssue;
654
760
 
@@ -990,6 +1096,746 @@ async function cmdProjectComplete(args) {
990
1096
  }
991
1097
  }
992
1098
 
1099
+ // ============================================================================
1100
+ // MILESTONES
1101
+ // ============================================================================
1102
+
1103
+ async function cmdMilestones(args) {
1104
+ const opts = parseArgs(args, {
1105
+ project: 'string', p: 'string',
1106
+ all: 'boolean', a: 'boolean',
1107
+ });
1108
+ const projectFilter = opts.project || opts.p;
1109
+ const showAll = opts.all || opts.a;
1110
+
1111
+ const query = `{
1112
+ team(id: "${TEAM_KEY}") {
1113
+ projects(first: 50) {
1114
+ nodes {
1115
+ id name state
1116
+ projectMilestones {
1117
+ nodes { id name targetDate sortOrder status }
1118
+ }
1119
+ }
1120
+ }
1121
+ }
1122
+ }`;
1123
+
1124
+ const result = await gql(query);
1125
+ let projects = result.data?.team?.projects?.nodes || [];
1126
+
1127
+ // Filter to active projects unless --all
1128
+ if (!showAll) {
1129
+ projects = projects.filter(p => !['completed', 'canceled'].includes(p.state));
1130
+ }
1131
+
1132
+ // Filter by project name if specified
1133
+ if (projectFilter) {
1134
+ projects = projects.filter(p => p.name.toLowerCase().includes(projectFilter.toLowerCase()));
1135
+ }
1136
+
1137
+ console.log(colors.bold('Milestones:\n'));
1138
+ for (const project of projects) {
1139
+ const milestones = project.projectMilestones?.nodes || [];
1140
+ if (milestones.length === 0) continue;
1141
+
1142
+ console.log(colors.bold(project.name));
1143
+ for (const m of milestones) {
1144
+ const date = m.targetDate ? ` (${m.targetDate})` : '';
1145
+ const status = m.status !== 'planned' ? ` [${m.status}]` : '';
1146
+ console.log(` ${m.name}${date}${status}`);
1147
+ }
1148
+ console.log('');
1149
+ }
1150
+ }
1151
+
1152
+ async function cmdMilestoneShow(args) {
1153
+ const milestoneName = args[0];
1154
+ if (!milestoneName) {
1155
+ console.error(colors.red('Error: Milestone name required'));
1156
+ process.exit(1);
1157
+ }
1158
+
1159
+ const query = `{
1160
+ team(id: "${TEAM_KEY}") {
1161
+ projects(first: 50) {
1162
+ nodes {
1163
+ name
1164
+ projectMilestones {
1165
+ nodes {
1166
+ id name description targetDate status sortOrder
1167
+ issues { nodes { identifier title state { name type } } }
1168
+ }
1169
+ }
1170
+ }
1171
+ }
1172
+ }
1173
+ }`;
1174
+
1175
+ const result = await gql(query);
1176
+ const projects = result.data?.team?.projects?.nodes || [];
1177
+
1178
+ let milestone = null;
1179
+ let projectName = '';
1180
+ for (const p of projects) {
1181
+ const m = p.projectMilestones?.nodes?.find(m =>
1182
+ m.name.toLowerCase().includes(milestoneName.toLowerCase())
1183
+ );
1184
+ if (m) {
1185
+ milestone = m;
1186
+ projectName = p.name;
1187
+ break;
1188
+ }
1189
+ }
1190
+
1191
+ if (!milestone) {
1192
+ console.error(colors.red(`Milestone not found: ${milestoneName}`));
1193
+ process.exit(1);
1194
+ }
1195
+
1196
+ console.log(`# ${milestone.name}\n`);
1197
+ console.log(`Project: ${projectName}`);
1198
+ console.log(`Status: ${milestone.status}`);
1199
+ if (milestone.targetDate) console.log(`Target: ${milestone.targetDate}`);
1200
+ if (milestone.description) console.log(`\n## Description\n${milestone.description}`);
1201
+
1202
+ const issues = milestone.issues?.nodes || [];
1203
+ if (issues.length > 0) {
1204
+ // Group by state type
1205
+ const done = issues.filter(i => i.state.type === 'completed');
1206
+ const inProgress = issues.filter(i => i.state.type === 'started');
1207
+ const backlog = issues.filter(i => !['completed', 'started', 'canceled'].includes(i.state.type));
1208
+
1209
+ console.log('\n## Issues\n');
1210
+ if (inProgress.length > 0) {
1211
+ console.log('### In Progress');
1212
+ inProgress.forEach(i => console.log(`- ${i.identifier}: ${i.title}`));
1213
+ console.log('');
1214
+ }
1215
+ if (backlog.length > 0) {
1216
+ console.log('### Backlog');
1217
+ backlog.forEach(i => console.log(`- ${i.identifier}: ${i.title}`));
1218
+ console.log('');
1219
+ }
1220
+ if (done.length > 0) {
1221
+ console.log('### Done');
1222
+ done.forEach(i => console.log(`- ${i.identifier}: ${i.title}`));
1223
+ console.log('');
1224
+ }
1225
+ }
1226
+ }
1227
+
1228
+ async function cmdMilestoneCreate(args) {
1229
+ const opts = parseArgs(args, {
1230
+ name: 'string', n: 'string',
1231
+ project: 'string', p: 'string',
1232
+ description: 'string', d: 'string',
1233
+ 'target-date': 'string',
1234
+ });
1235
+
1236
+ const name = opts.name || opts.n || opts._[0];
1237
+ const projectName = opts.project || opts.p;
1238
+ const description = opts.description || opts.d;
1239
+ const targetDate = opts['target-date'];
1240
+
1241
+ if (!name) {
1242
+ console.error(colors.red('Error: Milestone name required'));
1243
+ console.error('Usage: linear milestone create "Name" --project "Project" [--target-date 2024-03-01]');
1244
+ process.exit(1);
1245
+ }
1246
+
1247
+ if (!projectName) {
1248
+ console.error(colors.red('Error: Project required (--project)'));
1249
+ process.exit(1);
1250
+ }
1251
+
1252
+ // Find project
1253
+ const projectsResult = await gql(`{
1254
+ team(id: "${TEAM_KEY}") {
1255
+ projects(first: 50) { nodes { id name } }
1256
+ }
1257
+ }`);
1258
+ const projects = projectsResult.data?.team?.projects?.nodes || [];
1259
+ const project = projects.find(p => p.name.toLowerCase().includes(projectName.toLowerCase()));
1260
+
1261
+ if (!project) {
1262
+ console.error(colors.red(`Project not found: ${projectName}`));
1263
+ process.exit(1);
1264
+ }
1265
+
1266
+ const mutation = `
1267
+ mutation($input: ProjectMilestoneCreateInput!) {
1268
+ projectMilestoneCreate(input: $input) {
1269
+ success
1270
+ projectMilestone { id name }
1271
+ }
1272
+ }
1273
+ `;
1274
+
1275
+ const input = { projectId: project.id, name };
1276
+ if (description) input.description = description;
1277
+ if (targetDate) input.targetDate = targetDate;
1278
+
1279
+ const result = await gql(mutation, { input });
1280
+
1281
+ if (result.data?.projectMilestoneCreate?.success) {
1282
+ console.log(colors.green(`Created milestone: ${name}`));
1283
+ console.log(`Project: ${project.name}`);
1284
+ } else {
1285
+ console.error(colors.red('Failed to create milestone'));
1286
+ console.error(result.errors?.[0]?.message || JSON.stringify(result));
1287
+ process.exit(1);
1288
+ }
1289
+ }
1290
+
1291
+ // ============================================================================
1292
+ // ROADMAP
1293
+ // ============================================================================
1294
+
1295
+ async function cmdRoadmap(args) {
1296
+ const opts = parseArgs(args, { all: 'boolean', a: 'boolean' });
1297
+ const showAll = opts.all || opts.a;
1298
+
1299
+ // Fetch projects and milestones
1300
+ const projectsQuery = `{
1301
+ team(id: "${TEAM_KEY}") {
1302
+ projects(first: 50) {
1303
+ nodes {
1304
+ id name state priority sortOrder targetDate startDate
1305
+ projectMilestones {
1306
+ nodes {
1307
+ id name targetDate status sortOrder
1308
+ }
1309
+ }
1310
+ }
1311
+ }
1312
+ }
1313
+ }`;
1314
+
1315
+ // Fetch issues separately to avoid complexity limits
1316
+ const issuesQuery = `{
1317
+ team(id: "${TEAM_KEY}") {
1318
+ issues(first: 200) {
1319
+ nodes {
1320
+ id identifier title state { name type } sortOrder priority
1321
+ project { id }
1322
+ projectMilestone { id }
1323
+ }
1324
+ }
1325
+ }
1326
+ }`;
1327
+
1328
+ const [projectsResult, issuesResult] = await Promise.all([
1329
+ gql(projectsQuery),
1330
+ gql(issuesQuery)
1331
+ ]);
1332
+
1333
+ let projects = projectsResult.data?.team?.projects?.nodes || [];
1334
+ const allIssues = issuesResult.data?.team?.issues?.nodes || [];
1335
+
1336
+ // Sort by sortOrder descending (higher = first)
1337
+ projects.sort((a, b) => (b.sortOrder || 0) - (a.sortOrder || 0));
1338
+
1339
+ if (!showAll) {
1340
+ projects = projects.filter(p => !['completed', 'canceled'].includes(p.state));
1341
+ }
1342
+
1343
+ console.log(colors.bold('Roadmap\n'));
1344
+
1345
+ for (const project of projects) {
1346
+ const issues = allIssues.filter(i => i.project?.id === project.id);
1347
+ const milestones = project.projectMilestones?.nodes || [];
1348
+
1349
+ // Count issues by state
1350
+ const done = issues.filter(i => i.state.type === 'completed').length;
1351
+ const inProgress = issues.filter(i => i.state.type === 'started').length;
1352
+ const backlog = issues.filter(i => !['completed', 'started', 'canceled'].includes(i.state.type)).length;
1353
+
1354
+ // Project header
1355
+ const dates = [];
1356
+ if (project.startDate) dates.push(`start: ${project.startDate}`);
1357
+ if (project.targetDate) dates.push(`target: ${project.targetDate}`);
1358
+ const dateStr = dates.length > 0 ? ` (${dates.join(', ')})` : '';
1359
+ const priorityStr = project.priority > 0 ? ` [P${project.priority}]` : '';
1360
+
1361
+ console.log(colors.bold(`${project.name}${priorityStr}${dateStr}`));
1362
+ console.log(` ${colors.green(`✓ ${done}`)} done | ${colors.yellow(`→ ${inProgress}`)} in progress | ${colors.gray(`○ ${backlog}`)} backlog`);
1363
+
1364
+ // Show milestones with their issues
1365
+ if (milestones.length > 0) {
1366
+ // Sort milestones by sortOrder descending
1367
+ milestones.sort((a, b) => (b.sortOrder || 0) - (a.sortOrder || 0));
1368
+
1369
+ for (const m of milestones) {
1370
+ // Count issues in this milestone from project issues
1371
+ const mIssues = issues.filter(i => i.projectMilestone?.id === m.id);
1372
+ const mDone = mIssues.filter(i => i.state.type === 'completed').length;
1373
+ const mTotal = mIssues.length;
1374
+ const statusIcon = m.status === 'completed' ? colors.green('✓') :
1375
+ m.status === 'inProgress' ? colors.yellow('→') : '○';
1376
+ const targetStr = m.targetDate ? ` (${m.targetDate})` : '';
1377
+
1378
+ console.log(` ${statusIcon} ${m.name}${targetStr}: ${mDone}/${mTotal} done`);
1379
+
1380
+ // Show non-completed issues in this milestone
1381
+ const issuesInMilestone = mIssues.filter(i =>
1382
+ !['completed', 'canceled'].includes(i.state.type)
1383
+ );
1384
+
1385
+ // Sort by priority then sortOrder
1386
+ issuesInMilestone.sort((a, b) => {
1387
+ if ((b.priority || 0) !== (a.priority || 0)) return (b.priority || 0) - (a.priority || 0);
1388
+ return (b.sortOrder || 0) - (a.sortOrder || 0);
1389
+ });
1390
+
1391
+ for (const i of issuesInMilestone.slice(0, 5)) {
1392
+ const stateIcon = i.state.type === 'started' ? colors.yellow('→') : '○';
1393
+ console.log(` ${stateIcon} ${i.identifier}: ${i.title}`);
1394
+ }
1395
+ if (issuesInMilestone.length > 5) {
1396
+ console.log(colors.gray(` ... and ${issuesInMilestone.length - 5} more`));
1397
+ }
1398
+ }
1399
+ }
1400
+
1401
+ // Show issues not in any milestone
1402
+ const unmilestonedIssues = issues.filter(i =>
1403
+ !i.projectMilestone &&
1404
+ !['completed', 'canceled'].includes(i.state.type)
1405
+ );
1406
+
1407
+ if (unmilestonedIssues.length > 0 && milestones.length > 0) {
1408
+ console.log(colors.gray(` (${unmilestonedIssues.length} issues not in milestones)`));
1409
+ } else if (unmilestonedIssues.length > 0) {
1410
+ // Sort by priority then sortOrder
1411
+ unmilestonedIssues.sort((a, b) => {
1412
+ if ((b.priority || 0) !== (a.priority || 0)) return (b.priority || 0) - (a.priority || 0);
1413
+ return (b.sortOrder || 0) - (a.sortOrder || 0);
1414
+ });
1415
+
1416
+ for (const i of unmilestonedIssues.slice(0, 5)) {
1417
+ const stateIcon = i.state.type === 'started' ? colors.yellow('→') : '○';
1418
+ console.log(` ${stateIcon} ${i.identifier}: ${i.title}`);
1419
+ }
1420
+ if (unmilestonedIssues.length > 5) {
1421
+ console.log(colors.gray(` ... and ${unmilestonedIssues.length - 5} more`));
1422
+ }
1423
+ }
1424
+
1425
+ console.log('');
1426
+ }
1427
+ }
1428
+
1429
+ // ============================================================================
1430
+ // REORDERING
1431
+ // ============================================================================
1432
+
1433
+ async function cmdProjectsReorder(args) {
1434
+ if (args.length < 2) {
1435
+ console.error(colors.red('Error: At least 2 project names required'));
1436
+ console.error('Usage: linear projects reorder "Project A" "Project B" "Project C"');
1437
+ process.exit(1);
1438
+ }
1439
+
1440
+ // Get all projects
1441
+ const projectsResult = await gql(`{
1442
+ team(id: "${TEAM_KEY}") {
1443
+ projects(first: 50) { nodes { id name sortOrder } }
1444
+ }
1445
+ }`);
1446
+ const allProjects = projectsResult.data?.team?.projects?.nodes || [];
1447
+
1448
+ // Match provided names to projects
1449
+ const orderedProjects = [];
1450
+ for (const name of args) {
1451
+ const match = allProjects.find(p => p.name.toLowerCase().includes(name.toLowerCase()));
1452
+ if (!match) {
1453
+ console.error(colors.red(`Project not found: ${name}`));
1454
+ process.exit(1);
1455
+ }
1456
+ orderedProjects.push(match);
1457
+ }
1458
+
1459
+ // Assign new sortOrders (highest first)
1460
+ const baseSort = Math.max(...allProjects.map(p => p.sortOrder || 0)) + 1000;
1461
+ const mutations = [];
1462
+
1463
+ for (let i = 0; i < orderedProjects.length; i++) {
1464
+ const newSortOrder = baseSort - (i * 1000);
1465
+ mutations.push(gql(`
1466
+ mutation {
1467
+ projectUpdate(id: "${orderedProjects[i].id}", input: { sortOrder: ${newSortOrder} }) {
1468
+ success
1469
+ }
1470
+ }
1471
+ `));
1472
+ }
1473
+
1474
+ await Promise.all(mutations);
1475
+ console.log(colors.green('Reordered projects:'));
1476
+ orderedProjects.forEach((p, i) => console.log(` ${i + 1}. ${p.name}`));
1477
+ }
1478
+
1479
+ async function cmdProjectMove(args) {
1480
+ const opts = parseArgs(args, {
1481
+ before: 'string',
1482
+ after: 'string',
1483
+ });
1484
+
1485
+ const projectName = opts._[0];
1486
+ const beforeName = opts.before;
1487
+ const afterName = opts.after;
1488
+
1489
+ if (!projectName) {
1490
+ console.error(colors.red('Error: Project name required'));
1491
+ console.error('Usage: linear project move "Project" --before "Other" or --after "Other"');
1492
+ process.exit(1);
1493
+ }
1494
+
1495
+ if (!beforeName && !afterName) {
1496
+ console.error(colors.red('Error: --before or --after required'));
1497
+ process.exit(1);
1498
+ }
1499
+
1500
+ // Get all projects
1501
+ const projectsResult = await gql(`{
1502
+ team(id: "${TEAM_KEY}") {
1503
+ projects(first: 50) { nodes { id name sortOrder } }
1504
+ }
1505
+ }`);
1506
+ const projects = projectsResult.data?.team?.projects?.nodes || [];
1507
+ projects.sort((a, b) => (b.sortOrder || 0) - (a.sortOrder || 0));
1508
+
1509
+ const project = projects.find(p => p.name.toLowerCase().includes(projectName.toLowerCase()));
1510
+ const target = projects.find(p => p.name.toLowerCase().includes((beforeName || afterName).toLowerCase()));
1511
+
1512
+ if (!project) {
1513
+ console.error(colors.red(`Project not found: ${projectName}`));
1514
+ process.exit(1);
1515
+ }
1516
+ if (!target) {
1517
+ console.error(colors.red(`Target project not found: ${beforeName || afterName}`));
1518
+ process.exit(1);
1519
+ }
1520
+
1521
+ const targetIdx = projects.findIndex(p => p.id === target.id);
1522
+ let newSortOrder;
1523
+
1524
+ if (beforeName) {
1525
+ // Insert before target (higher sortOrder)
1526
+ const prevProject = projects[targetIdx - 1];
1527
+ if (prevProject) {
1528
+ newSortOrder = (target.sortOrder + prevProject.sortOrder) / 2;
1529
+ } else {
1530
+ newSortOrder = target.sortOrder + 1000;
1531
+ }
1532
+ } else {
1533
+ // Insert after target (lower sortOrder)
1534
+ const nextProject = projects[targetIdx + 1];
1535
+ if (nextProject) {
1536
+ newSortOrder = (target.sortOrder + nextProject.sortOrder) / 2;
1537
+ } else {
1538
+ newSortOrder = target.sortOrder - 1000;
1539
+ }
1540
+ }
1541
+
1542
+ await gql(`
1543
+ mutation {
1544
+ projectUpdate(id: "${project.id}", input: { sortOrder: ${newSortOrder} }) {
1545
+ success
1546
+ }
1547
+ }
1548
+ `);
1549
+
1550
+ console.log(colors.green(`Moved "${project.name}" ${beforeName ? 'before' : 'after'} "${target.name}"`));
1551
+ }
1552
+
1553
+ async function cmdMilestonesReorder(args) {
1554
+ const opts = parseArgs(args, { project: 'string', p: 'string' });
1555
+ const projectName = opts.project || opts.p;
1556
+ const milestoneNames = opts._;
1557
+
1558
+ if (!projectName) {
1559
+ console.error(colors.red('Error: --project required'));
1560
+ console.error('Usage: linear milestones reorder "M1" "M2" "M3" --project "Project"');
1561
+ process.exit(1);
1562
+ }
1563
+
1564
+ if (milestoneNames.length < 2) {
1565
+ console.error(colors.red('Error: At least 2 milestone names required'));
1566
+ process.exit(1);
1567
+ }
1568
+
1569
+ // Get project with milestones
1570
+ const projectsResult = await gql(`{
1571
+ team(id: "${TEAM_KEY}") {
1572
+ projects(first: 50) {
1573
+ nodes {
1574
+ id name
1575
+ projectMilestones { nodes { id name sortOrder } }
1576
+ }
1577
+ }
1578
+ }
1579
+ }`);
1580
+ const projects = projectsResult.data?.team?.projects?.nodes || [];
1581
+ const project = projects.find(p => p.name.toLowerCase().includes(projectName.toLowerCase()));
1582
+
1583
+ if (!project) {
1584
+ console.error(colors.red(`Project not found: ${projectName}`));
1585
+ process.exit(1);
1586
+ }
1587
+
1588
+ const allMilestones = project.projectMilestones?.nodes || [];
1589
+
1590
+ // Match provided names to milestones
1591
+ const orderedMilestones = [];
1592
+ for (const name of milestoneNames) {
1593
+ const match = allMilestones.find(m => m.name.toLowerCase().includes(name.toLowerCase()));
1594
+ if (!match) {
1595
+ console.error(colors.red(`Milestone not found: ${name}`));
1596
+ process.exit(1);
1597
+ }
1598
+ orderedMilestones.push(match);
1599
+ }
1600
+
1601
+ // Assign new sortOrders
1602
+ const baseSort = Math.max(...allMilestones.map(m => m.sortOrder || 0)) + 1000;
1603
+ const mutations = [];
1604
+
1605
+ for (let i = 0; i < orderedMilestones.length; i++) {
1606
+ const newSortOrder = baseSort - (i * 1000);
1607
+ mutations.push(gql(`
1608
+ mutation {
1609
+ projectMilestoneUpdate(id: "${orderedMilestones[i].id}", input: { sortOrder: ${newSortOrder} }) {
1610
+ success
1611
+ }
1612
+ }
1613
+ `));
1614
+ }
1615
+
1616
+ await Promise.all(mutations);
1617
+ console.log(colors.green(`Reordered milestones in ${project.name}:`));
1618
+ orderedMilestones.forEach((m, i) => console.log(` ${i + 1}. ${m.name}`));
1619
+ }
1620
+
1621
+ async function cmdMilestoneMove(args) {
1622
+ const opts = parseArgs(args, {
1623
+ before: 'string',
1624
+ after: 'string',
1625
+ });
1626
+
1627
+ const milestoneName = opts._[0];
1628
+ const beforeName = opts.before;
1629
+ const afterName = opts.after;
1630
+
1631
+ if (!milestoneName) {
1632
+ console.error(colors.red('Error: Milestone name required'));
1633
+ process.exit(1);
1634
+ }
1635
+
1636
+ if (!beforeName && !afterName) {
1637
+ console.error(colors.red('Error: --before or --after required'));
1638
+ process.exit(1);
1639
+ }
1640
+
1641
+ // Get all projects with milestones
1642
+ const projectsResult = await gql(`{
1643
+ team(id: "${TEAM_KEY}") {
1644
+ projects(first: 50) {
1645
+ nodes {
1646
+ id name
1647
+ projectMilestones { nodes { id name sortOrder } }
1648
+ }
1649
+ }
1650
+ }
1651
+ }`);
1652
+ const projects = projectsResult.data?.team?.projects?.nodes || [];
1653
+
1654
+ // Find milestone and its project
1655
+ let milestone = null;
1656
+ let projectMilestones = [];
1657
+ for (const p of projects) {
1658
+ const m = p.projectMilestones?.nodes?.find(m =>
1659
+ m.name.toLowerCase().includes(milestoneName.toLowerCase())
1660
+ );
1661
+ if (m) {
1662
+ milestone = m;
1663
+ projectMilestones = p.projectMilestones.nodes;
1664
+ projectMilestones.sort((a, b) => (b.sortOrder || 0) - (a.sortOrder || 0));
1665
+ break;
1666
+ }
1667
+ }
1668
+
1669
+ if (!milestone) {
1670
+ console.error(colors.red(`Milestone not found: ${milestoneName}`));
1671
+ process.exit(1);
1672
+ }
1673
+
1674
+ const target = projectMilestones.find(m =>
1675
+ m.name.toLowerCase().includes((beforeName || afterName).toLowerCase())
1676
+ );
1677
+
1678
+ if (!target) {
1679
+ console.error(colors.red(`Target milestone not found: ${beforeName || afterName}`));
1680
+ process.exit(1);
1681
+ }
1682
+
1683
+ const targetIdx = projectMilestones.findIndex(m => m.id === target.id);
1684
+ let newSortOrder;
1685
+
1686
+ if (beforeName) {
1687
+ const prevMilestone = projectMilestones[targetIdx - 1];
1688
+ if (prevMilestone) {
1689
+ newSortOrder = (target.sortOrder + prevMilestone.sortOrder) / 2;
1690
+ } else {
1691
+ newSortOrder = target.sortOrder + 1000;
1692
+ }
1693
+ } else {
1694
+ const nextMilestone = projectMilestones[targetIdx + 1];
1695
+ if (nextMilestone) {
1696
+ newSortOrder = (target.sortOrder + nextMilestone.sortOrder) / 2;
1697
+ } else {
1698
+ newSortOrder = target.sortOrder - 1000;
1699
+ }
1700
+ }
1701
+
1702
+ await gql(`
1703
+ mutation {
1704
+ projectMilestoneUpdate(id: "${milestone.id}", input: { sortOrder: ${newSortOrder} }) {
1705
+ success
1706
+ }
1707
+ }
1708
+ `);
1709
+
1710
+ console.log(colors.green(`Moved "${milestone.name}" ${beforeName ? 'before' : 'after'} "${target.name}"`));
1711
+ }
1712
+
1713
+ async function cmdIssuesReorder(args) {
1714
+ if (args.length < 2) {
1715
+ console.error(colors.red('Error: At least 2 issue IDs required'));
1716
+ console.error('Usage: linear issues reorder ISSUE-1 ISSUE-2 ISSUE-3');
1717
+ process.exit(1);
1718
+ }
1719
+
1720
+ // Get issues to verify they exist
1721
+ const query = `{
1722
+ team(id: "${TEAM_KEY}") {
1723
+ issues(first: 100) {
1724
+ nodes { id identifier sortOrder }
1725
+ }
1726
+ }
1727
+ }`;
1728
+
1729
+ const result = await gql(query);
1730
+ const allIssues = result.data?.team?.issues?.nodes || [];
1731
+
1732
+ // Match provided IDs to issues
1733
+ const orderedIssues = [];
1734
+ for (const id of args) {
1735
+ const match = allIssues.find(i => i.identifier === id.toUpperCase());
1736
+ if (!match) {
1737
+ console.error(colors.red(`Issue not found: ${id}`));
1738
+ process.exit(1);
1739
+ }
1740
+ orderedIssues.push(match);
1741
+ }
1742
+
1743
+ // Assign new sortOrders
1744
+ const baseSort = Math.max(...allIssues.map(i => i.sortOrder || 0)) + 1000;
1745
+ const mutations = [];
1746
+
1747
+ for (let i = 0; i < orderedIssues.length; i++) {
1748
+ const newSortOrder = baseSort - (i * 1000);
1749
+ mutations.push(gql(`
1750
+ mutation {
1751
+ issueUpdate(id: "${orderedIssues[i].identifier}", input: { sortOrder: ${newSortOrder} }) {
1752
+ success
1753
+ }
1754
+ }
1755
+ `));
1756
+ }
1757
+
1758
+ await Promise.all(mutations);
1759
+ console.log(colors.green('Reordered issues:'));
1760
+ orderedIssues.forEach((i, idx) => console.log(` ${idx + 1}. ${i.identifier}`));
1761
+ }
1762
+
1763
+ async function cmdIssueMove(args) {
1764
+ const opts = parseArgs(args, {
1765
+ before: 'string',
1766
+ after: 'string',
1767
+ });
1768
+
1769
+ const issueId = opts._[0];
1770
+ const beforeId = opts.before;
1771
+ const afterId = opts.after;
1772
+
1773
+ if (!issueId) {
1774
+ console.error(colors.red('Error: Issue ID required'));
1775
+ console.error('Usage: linear issue move ISSUE-1 --before ISSUE-2 or --after ISSUE-2');
1776
+ process.exit(1);
1777
+ }
1778
+
1779
+ if (!beforeId && !afterId) {
1780
+ console.error(colors.red('Error: --before or --after required'));
1781
+ process.exit(1);
1782
+ }
1783
+
1784
+ // Get issues
1785
+ const query = `{
1786
+ team(id: "${TEAM_KEY}") {
1787
+ issues(first: 100) {
1788
+ nodes { id identifier sortOrder }
1789
+ }
1790
+ }
1791
+ }`;
1792
+
1793
+ const result = await gql(query);
1794
+ const issues = result.data?.team?.issues?.nodes || [];
1795
+ issues.sort((a, b) => (b.sortOrder || 0) - (a.sortOrder || 0));
1796
+
1797
+ const issue = issues.find(i => i.identifier === issueId.toUpperCase());
1798
+ const target = issues.find(i => i.identifier === (beforeId || afterId).toUpperCase());
1799
+
1800
+ if (!issue) {
1801
+ console.error(colors.red(`Issue not found: ${issueId}`));
1802
+ process.exit(1);
1803
+ }
1804
+ if (!target) {
1805
+ console.error(colors.red(`Target issue not found: ${beforeId || afterId}`));
1806
+ process.exit(1);
1807
+ }
1808
+
1809
+ const targetIdx = issues.findIndex(i => i.identifier === target.identifier);
1810
+ let newSortOrder;
1811
+
1812
+ if (beforeId) {
1813
+ const prevIssue = issues[targetIdx - 1];
1814
+ if (prevIssue) {
1815
+ newSortOrder = (target.sortOrder + prevIssue.sortOrder) / 2;
1816
+ } else {
1817
+ newSortOrder = target.sortOrder + 1000;
1818
+ }
1819
+ } else {
1820
+ const nextIssue = issues[targetIdx + 1];
1821
+ if (nextIssue) {
1822
+ newSortOrder = (target.sortOrder + nextIssue.sortOrder) / 2;
1823
+ } else {
1824
+ newSortOrder = target.sortOrder - 1000;
1825
+ }
1826
+ }
1827
+
1828
+ await gql(`
1829
+ mutation {
1830
+ issueUpdate(id: "${issue.identifier}", input: { sortOrder: ${newSortOrder} }) {
1831
+ success
1832
+ }
1833
+ }
1834
+ `);
1835
+
1836
+ console.log(colors.green(`Moved ${issue.identifier} ${beforeId ? 'before' : 'after'} ${target.identifier}`));
1837
+ }
1838
+
993
1839
  // ============================================================================
994
1840
  // LABELS
995
1841
  // ============================================================================
@@ -1684,17 +2530,30 @@ async function cmdStandup(args) {
1684
2530
  // ============================================================================
1685
2531
 
1686
2532
  async function cmdLogin(args) {
1687
- const opts = parseArgs(args, { global: 'boolean', g: 'boolean' });
1688
- const saveGlobal = opts.global || opts.g;
1689
-
1690
2533
  console.log(colors.bold('Linear CLI Login\n'));
1691
- console.log('Opening Linear API settings in your browser...');
2534
+
2535
+ // Ask where to save credentials
2536
+ console.log('Where would you like to save your credentials?\n');
2537
+ console.log(' 1. This project only (./.linear)');
2538
+ console.log(' 2. Global, for all projects (~/.linear)');
2539
+ console.log('');
2540
+
2541
+ const locationChoice = await prompt('Enter number: ');
2542
+ if (locationChoice !== '1' && locationChoice !== '2') {
2543
+ console.error(colors.red('Error: Please enter 1 or 2'));
2544
+ process.exit(1);
2545
+ }
2546
+ const saveGlobal = locationChoice === '2';
2547
+ console.log('');
2548
+
2549
+ // Explain and prompt before opening browser
2550
+ console.log('To authenticate, you\'ll need a Linear API key.');
1692
2551
  console.log(colors.gray('(Create a new personal API key if you don\'t have one)\n'));
2552
+ await prompt('Press Enter to open Linear\'s API settings in your browser...');
1693
2553
 
1694
2554
  openBrowser('https://linear.app/settings/api');
1695
2555
 
1696
- await new Promise(r => setTimeout(r, 1000));
1697
-
2556
+ console.log('');
1698
2557
  const apiKey = await prompt('Paste your API key: ');
1699
2558
 
1700
2559
  if (!apiKey) {
@@ -1725,10 +2584,15 @@ async function cmdLogin(args) {
1725
2584
  console.log(` ${teams.length + 1}. Create a new team...`);
1726
2585
  console.log('');
1727
2586
 
1728
- const selection = await prompt('Enter number [1]: ') || '1';
2587
+ const selection = await prompt('Enter number: ');
2588
+ const selectionNum = parseInt(selection);
2589
+ if (!selection || isNaN(selectionNum) || selectionNum < 1 || selectionNum > teams.length + 1) {
2590
+ console.error(colors.red('Error: Invalid selection'));
2591
+ process.exit(1);
2592
+ }
1729
2593
  let selectedKey = '';
1730
2594
 
1731
- if (parseInt(selection) === teams.length + 1) {
2595
+ if (selectionNum === teams.length + 1) {
1732
2596
  // Create new team
1733
2597
  console.log('');
1734
2598
  const teamName = await prompt('Team name: ');
@@ -1760,12 +2624,7 @@ async function cmdLogin(args) {
1760
2624
  process.exit(1);
1761
2625
  }
1762
2626
  } else {
1763
- const idx = parseInt(selection) - 1;
1764
- if (idx < 0 || idx >= teams.length) {
1765
- console.error(colors.red('Error: Invalid selection'));
1766
- process.exit(1);
1767
- }
1768
- selectedKey = teams[idx].key;
2627
+ selectedKey = teams[selectionNum - 1].key;
1769
2628
  }
1770
2629
 
1771
2630
  // Save config
@@ -1869,20 +2728,27 @@ USAGE:
1869
2728
  linear <command> [options]
1870
2729
 
1871
2730
  AUTHENTICATION:
1872
- login [--global] Login and save credentials to .linear
1873
- --global, -g Save to ~/.linear instead of ./.linear
2731
+ login Login and save credentials to .linear
1874
2732
  logout Remove saved credentials
1875
2733
  whoami Show current user and team
1876
2734
 
2735
+ PLANNING:
2736
+ roadmap [options] Overview of projects, milestones, and issues
2737
+ --all, -a Include completed projects
2738
+
1877
2739
  ISSUES:
1878
- issues [options] List issues (yours shown first)
2740
+ issues [options] List issues (default: backlog, yours first)
1879
2741
  --unblocked, -u Show only unblocked issues
1880
2742
  --open, -o Show all non-completed/canceled issues
2743
+ --backlog, -b Show only backlog issues
1881
2744
  --all, -a Show all states (including completed)
1882
2745
  --mine, -m Show only issues assigned to you
1883
2746
  --in-progress Show issues in progress
1884
- --state, -s <state> Filter by state (default: backlog)
2747
+ --project, -p <name> Filter by project
2748
+ --milestone <name> Filter by milestone
2749
+ --state, -s <state> Filter by state
1885
2750
  --label, -l <name> Filter by label
2751
+ issues reorder <ids...> Reorder issues by listing IDs in order
1886
2752
 
1887
2753
  issue show <id> Show issue details with parent context
1888
2754
  issue start <id> Assign to yourself and set to In Progress
@@ -1890,6 +2756,7 @@ ISSUES:
1890
2756
  --title, -t <title> Issue title (required)
1891
2757
  --description, -d <desc> Issue description
1892
2758
  --project, -p <name> Add to project
2759
+ --milestone <name> Add to milestone
1893
2760
  --parent <id> Parent issue (for sub-issues)
1894
2761
  --assign Assign to yourself
1895
2762
  --estimate, -e <size> Estimate: XS, S, M, L, XL
@@ -1900,20 +2767,47 @@ ISSUES:
1900
2767
  --title, -t <title> New title
1901
2768
  --description, -d <desc> New description
1902
2769
  --state, -s <state> New state
2770
+ --project, -p <name> Move to project
2771
+ --milestone <name> Move to milestone
1903
2772
  --append, -a <text> Append to description
1904
2773
  --blocks <id> Add blocking relation
1905
2774
  --blocked-by <id> Add blocked-by relation
1906
2775
  issue close <id> Mark issue as done
1907
2776
  issue comment <id> <body> Add a comment
2777
+ issue move <id> Move issue in sort order
2778
+ --before <id> Move before this issue
2779
+ --after <id> Move after this issue
1908
2780
 
1909
2781
  PROJECTS:
1910
2782
  projects [options] List projects
1911
2783
  --all, -a Include completed projects
1912
- project show <name> Show project details
2784
+ projects reorder <names..> Reorder projects by listing names in order
2785
+
2786
+ project show <name> Show project details with issues
1913
2787
  project create [options] Create a new project
1914
2788
  --name, -n <name> Project name (required)
1915
2789
  --description, -d <desc> Project description
1916
2790
  project complete <name> Mark project as completed
2791
+ project move <name> Move project in sort order
2792
+ --before <name> Move before this project
2793
+ --after <name> Move after this project
2794
+
2795
+ MILESTONES:
2796
+ milestones [options] List milestones by project
2797
+ --project, -p <name> Filter by project
2798
+ --all, -a Include completed projects
2799
+ milestones reorder <names> Reorder milestones (requires --project)
2800
+ --project, -p <name> Project containing milestones
2801
+
2802
+ milestone show <name> Show milestone with issues
2803
+ milestone create [options] Create a new milestone
2804
+ --name, -n <name> Milestone name (required)
2805
+ --project, -p <name> Project (required)
2806
+ --description, -d <desc> Milestone description
2807
+ --target-date <date> Target date (YYYY-MM-DD)
2808
+ milestone move <name> Move milestone in sort order
2809
+ --before <name> Move before this milestone
2810
+ --after <name> Move after this milestone
1917
2811
 
1918
2812
  LABELS:
1919
2813
  labels List all labels
@@ -1945,15 +2839,12 @@ CONFIGURATION:
1945
2839
  team=ISSUE
1946
2840
 
1947
2841
  EXAMPLES:
1948
- linear login # First-time setup
2842
+ linear roadmap # See all projects and milestones
1949
2843
  linear issues --unblocked # Find workable issues
1950
- linear issues --in-progress # See what you're working on
1951
- linear issue show ISSUE-1 # View with parent context
1952
- linear issue start ISSUE-1 # Assign and start working
1953
- linear issue create --title "Fix bug" --project "Phase 1" --assign --estimate M
1954
- linear issue create --title "Needs API key" --blocked-by ISSUE-5
1955
- linear issue update ISSUE-1 --append "Found the root cause..."
1956
- linear branch ISSUE-5 # Create git branch for issue
2844
+ linear issues --project "Phase 1" # Issues in a project
2845
+ linear issue create --title "Fix bug" --milestone "Beta" --estimate M
2846
+ linear projects reorder "Phase 1" "Phase 2" "Phase 3"
2847
+ linear project move "Phase 3" --before "Phase 1"
1957
2848
  `);
1958
2849
  }
1959
2850
 
@@ -1983,10 +2874,16 @@ async function main() {
1983
2874
  case 'whoami':
1984
2875
  await cmdWhoami();
1985
2876
  break;
1986
- case 'issues':
2877
+ case 'issues': {
1987
2878
  checkAuth();
1988
- await cmdIssues(args.slice(1));
2879
+ // Check for "issues reorder" subcommand
2880
+ if (args[1] === 'reorder') {
2881
+ await cmdIssuesReorder(args.slice(2));
2882
+ } else {
2883
+ await cmdIssues(args.slice(1));
2884
+ }
1989
2885
  break;
2886
+ }
1990
2887
  case 'issue': {
1991
2888
  checkAuth();
1992
2889
  const subcmd = args[1];
@@ -1998,16 +2895,23 @@ async function main() {
1998
2895
  case 'start': await cmdIssueStart(subargs); break;
1999
2896
  case 'close': await cmdIssueClose(subargs); break;
2000
2897
  case 'comment': await cmdIssueComment(subargs); break;
2898
+ case 'move': await cmdIssueMove(subargs); break;
2001
2899
  default:
2002
2900
  console.error(`Unknown issue command: ${subcmd}`);
2003
2901
  process.exit(1);
2004
2902
  }
2005
2903
  break;
2006
2904
  }
2007
- case 'projects':
2905
+ case 'projects': {
2008
2906
  checkAuth();
2009
- await cmdProjects(args.slice(1));
2907
+ // Check for "projects reorder" subcommand
2908
+ if (args[1] === 'reorder') {
2909
+ await cmdProjectsReorder(args.slice(2));
2910
+ } else {
2911
+ await cmdProjects(args.slice(1));
2912
+ }
2010
2913
  break;
2914
+ }
2011
2915
  case 'project': {
2012
2916
  checkAuth();
2013
2917
  const subcmd = args[1];
@@ -2016,12 +2920,41 @@ async function main() {
2016
2920
  case 'show': await cmdProjectShow(subargs); break;
2017
2921
  case 'create': await cmdProjectCreate(subargs); break;
2018
2922
  case 'complete': await cmdProjectComplete(subargs); break;
2923
+ case 'move': await cmdProjectMove(subargs); break;
2019
2924
  default:
2020
2925
  console.error(`Unknown project command: ${subcmd}`);
2021
2926
  process.exit(1);
2022
2927
  }
2023
2928
  break;
2024
2929
  }
2930
+ case 'milestones': {
2931
+ checkAuth();
2932
+ // Check for "milestones reorder" subcommand
2933
+ if (args[1] === 'reorder') {
2934
+ await cmdMilestonesReorder(args.slice(2));
2935
+ } else {
2936
+ await cmdMilestones(args.slice(1));
2937
+ }
2938
+ break;
2939
+ }
2940
+ case 'milestone': {
2941
+ checkAuth();
2942
+ const subcmd = args[1];
2943
+ const subargs = args.slice(2);
2944
+ switch (subcmd) {
2945
+ case 'show': await cmdMilestoneShow(subargs); break;
2946
+ case 'create': await cmdMilestoneCreate(subargs); break;
2947
+ case 'move': await cmdMilestoneMove(subargs); break;
2948
+ default:
2949
+ console.error(`Unknown milestone command: ${subcmd}`);
2950
+ process.exit(1);
2951
+ }
2952
+ break;
2953
+ }
2954
+ case 'roadmap':
2955
+ checkAuth();
2956
+ await cmdRoadmap(args.slice(1));
2957
+ break;
2025
2958
  case 'labels':
2026
2959
  checkAuth();
2027
2960
  await cmdLabels();