@fink-andreas/pi-linear-tools 0.4.2 → 0.5.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/cli.js CHANGED
@@ -1,6 +1,13 @@
1
1
  import { loadSettings, saveSettings } from './settings.js';
2
2
  import { createLinearClient } from './linear-client.js';
3
3
  import { resolveProjectRef } from './linear.js';
4
+ import {
5
+ explainSyncDocSetup,
6
+ initSyncDocConfig,
7
+ listSyncDocTargets,
8
+ runAllSyncDocs,
9
+ runSyncDoc,
10
+ } from './sync-doc.js';
4
11
  import {
5
12
  authenticate,
6
13
  logout,
@@ -11,12 +18,25 @@ import {
11
18
  import {
12
19
  executeIssueList,
13
20
  executeIssueView,
21
+ executeIssueActivity,
14
22
  executeIssueCreate,
15
23
  executeIssueUpdate,
16
24
  executeIssueComment,
17
25
  executeIssueStart,
18
26
  executeIssueDelete,
19
27
  executeProjectList,
28
+ executeProjectView,
29
+ executeProjectCreate,
30
+ executeProjectUpdate,
31
+ executeProjectDelete,
32
+ executeProjectArchive,
33
+ executeProjectUnarchive,
34
+ executeProjectUpdateList,
35
+ executeProjectUpdateView,
36
+ executeProjectUpdateCreate,
37
+ executeProjectUpdateUpdate,
38
+ executeProjectUpdateArchive,
39
+ executeProjectUpdateUnarchive,
20
40
  executeTeamList,
21
41
  executeMilestoneList,
22
42
  executeMilestoneView,
@@ -131,8 +151,10 @@ Usage:
131
151
  pi-linear-tools <command> [options]
132
152
 
133
153
  Commands:
134
- issue <action> [options] Manage issues
135
- project <action> [options] Manage projects
154
+ issue <action> [options] Manage issues, comments, and issue activity/history
155
+ project <action> [options] Manage projects and project metadata
156
+ project-update <action> [options] Manage project updates (Linear Updates tab entries)
157
+ sync-doc [action] [options] Sync local markdown into Linear fields
136
158
  team <action> [options] Manage teams
137
159
  milestone <action> [options] Manage milestones
138
160
 
@@ -151,6 +173,7 @@ Auth Actions:
151
173
  Issue Actions:
152
174
  list [--project X] [--states X,Y] [--assignee me|all] [--team X] [--limit N]
153
175
  view <issue> [--no-comments]
176
+ activity <issue> [--limit N] [--include-archived true|false]
154
177
  create --title X [--team X] [--project X] [--description X] [--priority 0-4] [--assignee me|ID]
155
178
  update <issue> [--title X] [--description X] [--state X] [--priority 0-4]
156
179
  [--assignee me|ID] [--milestone X] [--sub-issue-of X]
@@ -160,6 +183,20 @@ Issue Actions:
160
183
 
161
184
  Project Actions:
162
185
  list
186
+ view <project>
187
+ create --name X --teams ENG,OPS [--description X] [--lead me|ID] [--priority 0-4] [--target-date YYYY-MM-DD]
188
+ update <project> [--name X] [--description X] [--teams X,Y] [--lead me|none|ID] [--target-date YYYY-MM-DD]
189
+ delete <project>
190
+ archive <project>
191
+ unarchive <project>
192
+
193
+ Project Update Actions:
194
+ list --project X [--limit N] [--include-archived true|false]
195
+ view <project-update-id>
196
+ create --project X [--body X] [--health onTrack|atRisk|offTrack]
197
+ update <project-update-id> [--body X] [--health onTrack|atRisk|offTrack]
198
+ archive <project-update-id>
199
+ unarchive <project-update-id>
163
200
 
164
201
  Team Actions:
165
202
  list
@@ -171,6 +208,36 @@ Milestone Actions:
171
208
  update <milestone-id> [--name X] [--description X] [--target-date X] [--status X]
172
209
  delete <milestone-id>
173
210
 
211
+ Sync Doc Actions:
212
+ init [--cwd X] [--project X] [--file X]
213
+ explain
214
+ list [--config X]
215
+ run [--target X] [--config X]
216
+ check [--target X] [--config X]
217
+ run --file X --project X [--field content|description] [--marker X]
218
+ run --file X --issue X [--field description] [--marker X]
219
+
220
+ Command Notes:
221
+ issue update changes issue fields; issue activity reads the Activity timeline.
222
+ project update changes project fields; project-update manages Updates tab entries.
223
+ project-update maps to Linear project updates in the Updates tab.
224
+ sync-doc init scaffolds .linear-tools/config.json in the target folder.
225
+ sync-doc run/check defaults to all configured targets in .linear-tools/config.json.
226
+ sync-doc --target X narrows the operation to one configured target.
227
+
228
+ More Help:
229
+ pi-linear-tools issue --help
230
+ pi-linear-tools project --help
231
+ pi-linear-tools project-update --help
232
+ pi-linear-tools sync-doc --help
233
+ pi-linear-tools milestone --help
234
+
235
+ Reference Conventions:
236
+ issues issue key (ENG-123) or issue ID
237
+ projects project name or project ID
238
+ project-updates project update ID
239
+ milestones milestone ID
240
+
174
241
  Common Flags:
175
242
  --project Project name or ID
176
243
  --team Team key (e.g., ENG)
@@ -182,10 +249,21 @@ Common Flags:
182
249
  Examples:
183
250
  pi-linear-tools auth login
184
251
  pi-linear-tools auth status
252
+ pi-linear-tools issue update ENG-123 --state "In Progress" --assignee me
253
+ pi-linear-tools issue comment ENG-123 --body "Blocked on API review"
254
+ pi-linear-tools issue activity ENG-123 --limit 20
255
+ pi-linear-tools project view "Roadmap Refresh"
256
+ pi-linear-tools project update "Roadmap Refresh" --target-date 2026-04-15 --lead me
257
+ pi-linear-tools project-update create --project "Roadmap Refresh" --body "Weekly update" --health onTrack
258
+ pi-linear-tools project-update update 12345678-1234-1234-1234-123456789abc --health atRisk
259
+ pi-linear-tools sync-doc list
260
+ pi-linear-tools sync-doc init --cwd /path/to/subproject --project "Project name or ID"
261
+ pi-linear-tools sync-doc explain
262
+ pi-linear-tools sync-doc run
263
+ pi-linear-tools sync-doc check
185
264
  pi-linear-tools issue list --project MyProject --states "In Progress,Backlog"
186
265
  pi-linear-tools issue view ENG-123
187
266
  pi-linear-tools issue create --title "Fix bug" --team ENG --priority 2
188
- pi-linear-tools issue update ENG-123 --state "In Progress" --assignee me
189
267
  pi-linear-tools issue start ENG-123
190
268
  pi-linear-tools milestone list --project MyProject
191
269
  pi-linear-tools config --api-key lin_xxx
@@ -204,9 +282,15 @@ function printIssueHelp() {
204
282
  Usage:
205
283
  pi-linear-tools issue <action> [options]
206
284
 
285
+ Guidance:
286
+ Use "update" to change the issue itself: title, description, state, assignee, milestone, or parent.
287
+ Use "comment" to add a discussion entry.
288
+ Use "activity" to read the Activity timeline shown in Linear.
289
+
207
290
  Actions:
208
291
  list List issues in a project
209
292
  view View issue details
293
+ activity View issue activity/history
210
294
  create Create a new issue
211
295
  update Update an existing issue
212
296
  comment Add a comment to an issue
@@ -221,9 +305,14 @@ List Options:
221
305
  --limit N Max results (default: 50)
222
306
 
223
307
  View Options:
224
- <issue> Issue key (e.g., ENG-123) or ID
308
+ <issue> Issue key (e.g., ENG-123), ID, or issue URL
225
309
  --no-comments Exclude comments from output
226
310
 
311
+ Activity Options:
312
+ <issue> Issue key, ID, or issue URL
313
+ --limit N Max activity entries to fetch (default: 20)
314
+ --include-archived X true or false
315
+
227
316
  Create Options:
228
317
  --title X Issue title (required)
229
318
  --team X Team key, e.g., ENG (required if no default team)
@@ -254,6 +343,14 @@ Start Options:
254
343
 
255
344
  Delete Options:
256
345
  <issue> Issue key or ID
346
+
347
+ Examples:
348
+ pi-linear-tools issue view ENG-123
349
+ pi-linear-tools issue update ENG-123 --state "In Progress" --assignee me
350
+ pi-linear-tools issue update ENG-123 --milestone "Sprint 12" --priority 2
351
+ pi-linear-tools issue comment ENG-123 --body "Ready for review"
352
+ pi-linear-tools issue activity ENG-123 --limit 20
353
+ pi-linear-tools issue activity https://linear.app/workspace/issue/ENG-123/example --limit 20
257
354
  `);
258
355
  }
259
356
 
@@ -261,10 +358,175 @@ function printProjectHelp() {
261
358
  console.log(`pi-linear-tools project - Manage Linear projects
262
359
 
263
360
  Usage:
264
- pi-linear-tools project <action>
361
+ pi-linear-tools project <action> [options]
362
+
363
+ Guidance:
364
+ Use "project update" to change the project record itself: name, teams, dates, lead, description, priority.
365
+ Use "project-update" for the Updates tab entries that appear as weekly or status updates in Linear.
366
+
367
+ Actions:
368
+ list List all accessible projects
369
+ view View project details
370
+ create Create a new project
371
+ update Update an existing project
372
+ delete Delete a project
373
+ archive Archive a project
374
+ unarchive Restore an archived project
375
+
376
+ View Options:
377
+ <project> Project name or ID
378
+
379
+ Create Options:
380
+ --name X Project name (required)
381
+ --teams X,Y Team keys or IDs (required)
382
+ --description X Project description
383
+ --lead X "me" or user ID
384
+ --priority N Priority 0-4
385
+ --color X Project color (hex)
386
+ --icon X Project icon
387
+ --start-date X Planned start date (YYYY-MM-DD)
388
+ --target-date X Planned target date (YYYY-MM-DD)
389
+
390
+ Update Options:
391
+ <project> Project name or ID
392
+ --name X New name
393
+ --teams X,Y Replace associated teams
394
+ --description X New description
395
+ --lead X "me", "none", or user ID
396
+ --priority N New priority 0-4
397
+ --color X New project color (hex)
398
+ --icon X New project icon
399
+ --start-date X New planned start date
400
+ --target-date X New planned target date
401
+
402
+ Delete Options:
403
+ <project> Project name or ID
404
+
405
+ Archive Options:
406
+ <project> Project name or ID
407
+
408
+ Unarchive Options:
409
+ <project> Project name or ID
410
+
411
+ Examples:
412
+ pi-linear-tools project view "Roadmap Refresh"
413
+ pi-linear-tools project update "Roadmap Refresh" --description "New scope" --target-date 2026-04-15
414
+ pi-linear-tools project update "Roadmap Refresh" --lead me --teams ENG,OPS
415
+ pi-linear-tools project archive "Roadmap Refresh"
416
+ `);
417
+ }
418
+
419
+ function printSyncDocHelp() {
420
+ console.log(`pi-linear-tools sync-doc - Sync markdown files into Linear
421
+
422
+ Usage:
423
+ pi-linear-tools sync-doc [init|explain|list|run|check] [options]
424
+
425
+ Config:
426
+ Reads targets from .linear-tools/config.json in the current project tree and ~/.linear-tools/config.json.
427
+ Put config in the smallest folder that owns the docs being synced. The nearest config wins.
428
+ Use repo root only for targets intentionally shared across multiple subprojects. Use ~/.linear-tools/config.json for personal defaults.
429
+
430
+ Actions:
431
+ init Scaffold a starter .linear-tools/config.json in the target folder
432
+ explain Print the recommended config placement and overview/document model
433
+ list Show resolved sync targets from config
434
+ run Update Linear if the managed block differs. Defaults to all configured targets.
435
+ check Show whether a sync would change Linear. Defaults to all configured targets.
436
+
437
+ Target Options:
438
+ --target X Target name from config
439
+ --config X Explicit path to config.json or .linear-tools/
440
+ --cwd X Resolve config and files relative to X
441
+
442
+ Init Options:
443
+ --cwd X Create .linear-tools/config.json inside X (defaults to current directory)
444
+ --project X Fill the starter overview target with a project name or ID
445
+ --file X File path for the starter overview target (default: README.md)
446
+ --field X content or description (default: content)
447
+ --force Overwrite an existing config.json
448
+
449
+ One-off Run/Check Options:
450
+ --file X Markdown file to sync
451
+ --project X Linear project name, id, slug, or project URL
452
+ --issue X Linear issue key or id
453
+ --target-type X projectField, issueField, or document
454
+ --document-title X Required for one-off document targets unless inferred from file name
455
+ --document-id X Existing Linear document ID or URL identifier
456
+ --field X For projects: content or description. For issues: description
457
+ --marker X Managed block marker (default: file basename without extension)
458
+
459
+ Managed Block:
460
+ <!-- linear-tools:sync-start MARKER -->
461
+ ...generated content...
462
+ <!-- linear-tools:sync-end MARKER -->
463
+
464
+ Project Document Index:
465
+ A projectField target can set documentIndexMarker to maintain a managed list of synced document targets.
466
+ Use this for one overview doc in the project body plus separate linked Linear documents for deeper docs.
467
+
468
+ Examples:
469
+ pi-linear-tools sync-doc init --cwd /path/to/subproject --project "Project name or ID"
470
+ pi-linear-tools sync-doc explain
471
+ pi-linear-tools sync-doc list
472
+ pi-linear-tools sync-doc run
473
+ pi-linear-tools sync-doc check
474
+ pi-linear-tools sync-doc run --target project-overview
475
+ pi-linear-tools sync-doc check --target project-overview
476
+ pi-linear-tools sync-doc run --file README.md --project "Project name or ID" --field content
477
+ pi-linear-tools sync-doc run --file docs/provider.md --project "Project name or ID" --target-type document --document-title "Provider Doc"
478
+ `);
479
+ }
480
+
481
+ function printProjectUpdateHelp() {
482
+ console.log(`pi-linear-tools project-update - Manage Linear project updates
483
+
484
+ Usage:
485
+ pi-linear-tools project-update <action> [options]
486
+
487
+ Guidance:
488
+ This command manages the entries shown in a project's Updates tab.
489
+ Create an update by project name or ID, then use the returned project update ID for later view/update/archive actions.
265
490
 
266
491
  Actions:
267
- list List all accessible projects
492
+ list List updates for a project
493
+ view View project update details
494
+ create Create a new project update
495
+ update Update an existing project update
496
+ archive Archive a project update
497
+ unarchive Restore an archived project update
498
+
499
+ List Options:
500
+ --project X Project name or ID (required)
501
+ --limit N Max results (default: 10)
502
+ --include-archived X true or false
503
+
504
+ View Options:
505
+ <project-update-id> Project update ID
506
+
507
+ Create Options:
508
+ --project X Project name or ID (required)
509
+ --body X Update body in markdown
510
+ --health X onTrack, atRisk, or offTrack
511
+ --is-diff-hidden X true or false
512
+
513
+ Update Options:
514
+ <project-update-id> Project update ID
515
+ --body X Updated body in markdown
516
+ --health X onTrack, atRisk, or offTrack
517
+ --is-diff-hidden X true or false
518
+
519
+ Archive Options:
520
+ <project-update-id> Project update ID
521
+
522
+ Unarchive Options:
523
+ <project-update-id> Project update ID
524
+
525
+ Examples:
526
+ pi-linear-tools project-update list --project "Roadmap Refresh"
527
+ pi-linear-tools project-update create --project "Roadmap Refresh" --body "Weekly update" --health onTrack
528
+ pi-linear-tools project-update update 12345678-1234-1234-1234-123456789abc --body "Revised update" --health atRisk
529
+ pi-linear-tools project-update view 12345678-1234-1234-1234-123456789abc
268
530
  `);
269
531
  }
270
532
 
@@ -557,6 +819,24 @@ async function handleIssueView(args) {
557
819
  console.log(result.content[0].text);
558
820
  }
559
821
 
822
+ async function handleIssueActivity(args) {
823
+ const client = await createAuthenticatedClient();
824
+
825
+ const positional = args.filter((a) => !a.startsWith('-'));
826
+ if (positional.length === 0) {
827
+ throw new Error('Missing required argument: issue key or ID');
828
+ }
829
+
830
+ const params = {
831
+ issue: positional[0],
832
+ limit: parseNumber(readFlag(args, '--limit')),
833
+ includeArchived: parseBoolean(readFlag(args, '--include-archived')),
834
+ };
835
+
836
+ const result = await executeIssueActivity(client, params);
837
+ console.log(result.content[0].text);
838
+ }
839
+
560
840
  async function handleIssueCreate(args) {
561
841
  const client = await createAuthenticatedClient();
562
842
 
@@ -670,6 +950,8 @@ async function handleIssue(args) {
670
950
  return handleIssueList(rest);
671
951
  case 'view':
672
952
  return handleIssueView(rest);
953
+ case 'activity':
954
+ return handleIssueActivity(rest);
673
955
  case 'create':
674
956
  return handleIssueCreate(rest);
675
957
  case 'update':
@@ -694,6 +976,107 @@ async function handleProjectList() {
694
976
  console.log(result.content[0].text);
695
977
  }
696
978
 
979
+ async function handleProjectView(args) {
980
+ const client = await createAuthenticatedClient();
981
+ const positional = args.filter((a) => !a.startsWith('-'));
982
+ if (positional.length === 0) {
983
+ throw new Error('Missing required argument: project name or ID');
984
+ }
985
+
986
+ const result = await executeProjectView(client, {
987
+ project: positional[0],
988
+ });
989
+ console.log(result.content[0].text);
990
+ }
991
+
992
+ async function handleProjectCreate(args) {
993
+ const client = await createAuthenticatedClient();
994
+ const params = {
995
+ name: readFlag(args, '--name'),
996
+ teams: parseArrayValue(readFlag(args, '--teams')),
997
+ description: readFlag(args, '--description'),
998
+ lead: readFlag(args, '--lead'),
999
+ priority: parseNumber(readFlag(args, '--priority')),
1000
+ color: readFlag(args, '--color'),
1001
+ icon: readFlag(args, '--icon'),
1002
+ startDate: readFlag(args, '--start-date'),
1003
+ targetDate: readFlag(args, '--target-date'),
1004
+ };
1005
+
1006
+ if (!params.name) {
1007
+ throw new Error('Missing required flag: --name');
1008
+ }
1009
+ if (!params.teams || params.teams.length === 0) {
1010
+ throw new Error('Missing required flag: --teams');
1011
+ }
1012
+
1013
+ const result = await executeProjectCreate(client, params);
1014
+ console.log(result.content[0].text);
1015
+ }
1016
+
1017
+ async function handleProjectUpdate(args) {
1018
+ const client = await createAuthenticatedClient();
1019
+ const positional = args.filter((a) => !a.startsWith('-'));
1020
+ if (positional.length === 0) {
1021
+ throw new Error('Missing required argument: project name or ID');
1022
+ }
1023
+
1024
+ const params = {
1025
+ project: positional[0],
1026
+ name: readFlag(args, '--name'),
1027
+ teams: parseArrayValue(readFlag(args, '--teams')),
1028
+ description: readFlag(args, '--description'),
1029
+ lead: readFlag(args, '--lead'),
1030
+ priority: parseNumber(readFlag(args, '--priority')),
1031
+ color: readFlag(args, '--color'),
1032
+ icon: readFlag(args, '--icon'),
1033
+ startDate: readFlag(args, '--start-date'),
1034
+ targetDate: readFlag(args, '--target-date'),
1035
+ };
1036
+
1037
+ const result = await executeProjectUpdate(client, params);
1038
+ console.log(result.content[0].text);
1039
+ }
1040
+
1041
+ async function handleProjectDelete(args) {
1042
+ const client = await createAuthenticatedClient();
1043
+ const positional = args.filter((a) => !a.startsWith('-'));
1044
+ if (positional.length === 0) {
1045
+ throw new Error('Missing required argument: project name or ID');
1046
+ }
1047
+
1048
+ const result = await executeProjectDelete(client, {
1049
+ project: positional[0],
1050
+ });
1051
+ console.log(result.content[0].text);
1052
+ }
1053
+
1054
+ async function handleProjectArchive(args) {
1055
+ const client = await createAuthenticatedClient();
1056
+ const positional = args.filter((a) => !a.startsWith('-'));
1057
+ if (positional.length === 0) {
1058
+ throw new Error('Missing required argument: project name or ID');
1059
+ }
1060
+
1061
+ const result = await executeProjectArchive(client, {
1062
+ project: positional[0],
1063
+ });
1064
+ console.log(result.content[0].text);
1065
+ }
1066
+
1067
+ async function handleProjectUnarchive(args) {
1068
+ const client = await createAuthenticatedClient();
1069
+ const positional = args.filter((a) => !a.startsWith('-'));
1070
+ if (positional.length === 0) {
1071
+ throw new Error('Missing required argument: project name or ID');
1072
+ }
1073
+
1074
+ const result = await executeProjectUnarchive(client, {
1075
+ project: positional[0],
1076
+ });
1077
+ console.log(result.content[0].text);
1078
+ }
1079
+
697
1080
  async function handleProject(args) {
698
1081
  const [action] = args;
699
1082
 
@@ -705,11 +1088,312 @@ async function handleProject(args) {
705
1088
  switch (action) {
706
1089
  case 'list':
707
1090
  return handleProjectList();
1091
+ case 'view':
1092
+ return handleProjectView(args.slice(1));
1093
+ case 'create':
1094
+ return handleProjectCreate(args.slice(1));
1095
+ case 'update':
1096
+ return handleProjectUpdate(args.slice(1));
1097
+ case 'delete':
1098
+ return handleProjectDelete(args.slice(1));
1099
+ case 'archive':
1100
+ return handleProjectArchive(args.slice(1));
1101
+ case 'unarchive':
1102
+ return handleProjectUnarchive(args.slice(1));
708
1103
  default:
709
1104
  throw new Error(`Unknown project action: ${action}`);
710
1105
  }
711
1106
  }
712
1107
 
1108
+ // ===== PROJECT UPDATE HANDLERS =====
1109
+
1110
+ async function handleProjectUpdateListCli(args) {
1111
+ const client = await createAuthenticatedClient();
1112
+ const params = {
1113
+ project: readFlag(args, '--project'),
1114
+ limit: parseNumber(readFlag(args, '--limit')),
1115
+ includeArchived: parseBoolean(readFlag(args, '--include-archived')),
1116
+ };
1117
+
1118
+ if (!params.project) {
1119
+ throw new Error('Missing required flag: --project');
1120
+ }
1121
+
1122
+ const result = await executeProjectUpdateList(client, params);
1123
+ console.log(result.content[0].text);
1124
+ }
1125
+
1126
+ function printSyncDocResult(result) {
1127
+ if (!result.changed) {
1128
+ if (result.targetType === 'document') {
1129
+ console.log(`No sync changes needed for document "${result.entityName}" from ${result.file}`);
1130
+ return;
1131
+ }
1132
+ console.log(`No sync changes needed for ${result.entityType} "${result.entityName}" field "${result.field}" from ${result.file}`);
1133
+ return;
1134
+ }
1135
+
1136
+ if (result.mode === 'check') {
1137
+ if (result.targetType === 'document') {
1138
+ console.log(`Sync needed for document "${result.entityName}" from ${result.file}`);
1139
+ return;
1140
+ }
1141
+ console.log(`Sync needed for ${result.entityType} "${result.entityName}" field "${result.field}" from ${result.file}`);
1142
+ return;
1143
+ }
1144
+
1145
+ if (result.targetType === 'document') {
1146
+ console.log(`Synced ${result.file} to document "${result.entityName}" using marker "${result.marker}"`);
1147
+ if (result.documentUrl) {
1148
+ console.log(result.documentUrl);
1149
+ }
1150
+ return;
1151
+ }
1152
+
1153
+ console.log(`Synced ${result.file} to ${result.entityType} "${result.entityName}" field "${result.field}" using marker "${result.marker}"`);
1154
+ }
1155
+
1156
+ function printSyncDocTargets(result) {
1157
+ if (!Array.isArray(result.targets) || result.targets.length === 0) {
1158
+ console.log('No sync-doc targets configured.');
1159
+ return;
1160
+ }
1161
+
1162
+ const lines = result.targets.map((target) => {
1163
+ if (target.targetType === 'document') {
1164
+ const entityLabel = `${target.issue ? 'issue' : 'project'}:${target.entityRef}`;
1165
+ return `- ${target.name} -> document "${target.title}" linked to ${entityLabel} from ${target.file} (marker: ${target.marker})`;
1166
+ }
1167
+
1168
+ const entityLabel = `${target.targetType === 'issueField' ? 'issue' : 'project'}:${target.entityRef}`;
1169
+ const extra = target.documentIndexMarker ? `, document index: ${target.documentIndexMarker}` : '';
1170
+ return `- ${target.name} -> ${entityLabel} field "${target.field}" from ${target.file} (marker: ${target.marker}${extra})`;
1171
+ });
1172
+
1173
+ if (result.configPath) {
1174
+ console.log(`Resolved sync-doc targets from ${result.configPath}`);
1175
+ }
1176
+ console.log(lines.join('\n'));
1177
+ }
1178
+
1179
+ function printSyncDocBatchResult(result) {
1180
+ const header = result.mode === 'check'
1181
+ ? `Checked ${result.total} sync-doc target(s): ${result.changedCount} need updates, ${result.unchangedCount} unchanged`
1182
+ : `Ran ${result.total} sync-doc target(s): ${result.changedCount} updated, ${result.unchangedCount} unchanged`;
1183
+ const lines = result.results.map((entry) => {
1184
+ const status = entry.changed ? (result.mode === 'check' ? 'needs-sync' : 'synced') : 'unchanged';
1185
+ if (entry.targetType === 'document') {
1186
+ return `- [${status}] ${entry.targetName} -> document "${entry.entityName}" from ${entry.file}`;
1187
+ }
1188
+ return `- [${status}] ${entry.targetName} -> ${entry.entityType} "${entry.entityName}" field "${entry.field}" from ${entry.file}`;
1189
+ });
1190
+
1191
+ console.log([header, ...lines].join('\n'));
1192
+ }
1193
+
1194
+ function printSyncDocInitResult(result) {
1195
+ if (!result.created) {
1196
+ console.log(`Sync-doc config already exists at ${result.configPath}`);
1197
+ console.log('Use --force to overwrite it, or edit the existing config manually.');
1198
+ return;
1199
+ }
1200
+
1201
+ console.log(`${result.overwritten ? 'Rewrote' : 'Created'} ${result.configPath}`);
1202
+ console.log(`State will be written to ${result.statePath}`);
1203
+ if (result.target) {
1204
+ console.log(`Starter target: ${result.target.name} -> project:${result.target.project} field "${result.target.field}" from ${result.target.file}`);
1205
+ }
1206
+ console.log('Next steps:');
1207
+ console.log(`- Edit ${result.configPath} to add document targets for deeper docs`);
1208
+ console.log(`- Run pi-linear-tools sync-doc list --cwd ${result.cwd}`);
1209
+ console.log(`- Run pi-linear-tools sync-doc run --cwd ${result.cwd}`);
1210
+ }
1211
+
1212
+ async function handleSyncDocCommand(args) {
1213
+ const [maybeAction, ...restArgs] = args;
1214
+ const action = !maybeAction || maybeAction.startsWith('-') ? 'run' : maybeAction;
1215
+ const commandArgs = !maybeAction || maybeAction.startsWith('-') ? args : restArgs;
1216
+ const cwd = readFlag(commandArgs, '--cwd') || process.cwd();
1217
+
1218
+ if (
1219
+ action === '--help'
1220
+ || action === '-h'
1221
+ || action === 'help'
1222
+ || hasFlag(commandArgs, '--help')
1223
+ || hasFlag(commandArgs, '-h')
1224
+ ) {
1225
+ printSyncDocHelp();
1226
+ return;
1227
+ }
1228
+
1229
+ if (!['init', 'explain', 'list', 'run', 'check'].includes(action)) {
1230
+ throw new Error(`Unknown sync-doc action: ${action}`);
1231
+ }
1232
+
1233
+ if (action === 'explain') {
1234
+ console.log(explainSyncDocSetup());
1235
+ return;
1236
+ }
1237
+
1238
+ if (action === 'init') {
1239
+ const result = await initSyncDocConfig({
1240
+ cwd,
1241
+ file: readFlag(commandArgs, '--file'),
1242
+ project: readFlag(commandArgs, '--project'),
1243
+ field: readFlag(commandArgs, '--field'),
1244
+ marker: readFlag(commandArgs, '--marker'),
1245
+ name: readFlag(commandArgs, '--name'),
1246
+ documentIndexMarker: readFlag(commandArgs, '--document-index-marker'),
1247
+ force: hasFlag(commandArgs, '--force'),
1248
+ });
1249
+ printSyncDocInitResult(result);
1250
+ return;
1251
+ }
1252
+
1253
+ if (action === 'list') {
1254
+ const result = await listSyncDocTargets({
1255
+ cwd,
1256
+ configPath: readFlag(commandArgs, '--config'),
1257
+ });
1258
+ printSyncDocTargets(result);
1259
+ return;
1260
+ }
1261
+
1262
+ const client = await createAuthenticatedClient();
1263
+ const hasOneOffFlags = Boolean(
1264
+ readFlag(commandArgs, '--file')
1265
+ && (readFlag(commandArgs, '--project') || readFlag(commandArgs, '--issue'))
1266
+ );
1267
+ const hasNamedTarget = Boolean(readFlag(commandArgs, '--target'));
1268
+
1269
+ if (!hasOneOffFlags && !hasNamedTarget) {
1270
+ const result = await runAllSyncDocs(client, {
1271
+ mode: action,
1272
+ cwd,
1273
+ configPath: readFlag(commandArgs, '--config'),
1274
+ });
1275
+ printSyncDocBatchResult(result);
1276
+ return;
1277
+ }
1278
+
1279
+ const result = await runSyncDoc(client, {
1280
+ mode: action,
1281
+ cwd,
1282
+ configPath: readFlag(commandArgs, '--config'),
1283
+ targetName: readFlag(commandArgs, '--target'),
1284
+ file: readFlag(commandArgs, '--file'),
1285
+ project: readFlag(commandArgs, '--project'),
1286
+ issue: readFlag(commandArgs, '--issue'),
1287
+ targetType: readFlag(commandArgs, '--target-type'),
1288
+ documentTitle: readFlag(commandArgs, '--document-title'),
1289
+ documentId: readFlag(commandArgs, '--document-id'),
1290
+ field: readFlag(commandArgs, '--field'),
1291
+ marker: readFlag(commandArgs, '--marker'),
1292
+ });
1293
+
1294
+ printSyncDocResult(result);
1295
+ }
1296
+
1297
+ async function handleProjectUpdateViewCli(args) {
1298
+ const client = await createAuthenticatedClient();
1299
+ const positional = args.filter((a) => !a.startsWith('-'));
1300
+ if (positional.length === 0) {
1301
+ throw new Error('Missing required argument: project update ID');
1302
+ }
1303
+
1304
+ const result = await executeProjectUpdateView(client, {
1305
+ projectUpdate: positional[0],
1306
+ });
1307
+ console.log(result.content[0].text);
1308
+ }
1309
+
1310
+ async function handleProjectUpdateCreateCli(args) {
1311
+ const client = await createAuthenticatedClient();
1312
+ const params = {
1313
+ project: readFlag(args, '--project'),
1314
+ body: readFlag(args, '--body'),
1315
+ health: readFlag(args, '--health'),
1316
+ isDiffHidden: parseBoolean(readFlag(args, '--is-diff-hidden')),
1317
+ };
1318
+
1319
+ if (!params.project) {
1320
+ throw new Error('Missing required flag: --project');
1321
+ }
1322
+
1323
+ const result = await executeProjectUpdateCreate(client, params);
1324
+ console.log(result.content[0].text);
1325
+ }
1326
+
1327
+ async function handleProjectUpdateUpdateCli(args) {
1328
+ const client = await createAuthenticatedClient();
1329
+ const positional = args.filter((a) => !a.startsWith('-'));
1330
+ if (positional.length === 0) {
1331
+ throw new Error('Missing required argument: project update ID');
1332
+ }
1333
+
1334
+ const params = {
1335
+ projectUpdate: positional[0],
1336
+ body: readFlag(args, '--body'),
1337
+ health: readFlag(args, '--health'),
1338
+ isDiffHidden: parseBoolean(readFlag(args, '--is-diff-hidden')),
1339
+ };
1340
+
1341
+ const result = await executeProjectUpdateUpdate(client, params);
1342
+ console.log(result.content[0].text);
1343
+ }
1344
+
1345
+ async function handleProjectUpdateArchiveCli(args) {
1346
+ const client = await createAuthenticatedClient();
1347
+ const positional = args.filter((a) => !a.startsWith('-'));
1348
+ if (positional.length === 0) {
1349
+ throw new Error('Missing required argument: project update ID');
1350
+ }
1351
+
1352
+ const result = await executeProjectUpdateArchive(client, {
1353
+ projectUpdate: positional[0],
1354
+ });
1355
+ console.log(result.content[0].text);
1356
+ }
1357
+
1358
+ async function handleProjectUpdateUnarchiveCli(args) {
1359
+ const client = await createAuthenticatedClient();
1360
+ const positional = args.filter((a) => !a.startsWith('-'));
1361
+ if (positional.length === 0) {
1362
+ throw new Error('Missing required argument: project update ID');
1363
+ }
1364
+
1365
+ const result = await executeProjectUpdateUnarchive(client, {
1366
+ projectUpdate: positional[0],
1367
+ });
1368
+ console.log(result.content[0].text);
1369
+ }
1370
+
1371
+ async function handleProjectUpdateCommand(args) {
1372
+ const [action] = args;
1373
+
1374
+ if (!action || action === '--help' || action === '-h') {
1375
+ printProjectUpdateHelp();
1376
+ return;
1377
+ }
1378
+
1379
+ switch (action) {
1380
+ case 'list':
1381
+ return handleProjectUpdateListCli(args.slice(1));
1382
+ case 'view':
1383
+ return handleProjectUpdateViewCli(args.slice(1));
1384
+ case 'create':
1385
+ return handleProjectUpdateCreateCli(args.slice(1));
1386
+ case 'update':
1387
+ return handleProjectUpdateUpdateCli(args.slice(1));
1388
+ case 'archive':
1389
+ return handleProjectUpdateArchiveCli(args.slice(1));
1390
+ case 'unarchive':
1391
+ return handleProjectUpdateUnarchiveCli(args.slice(1));
1392
+ default:
1393
+ throw new Error(`Unknown project-update action: ${action}`);
1394
+ }
1395
+ }
1396
+
713
1397
  // ===== TEAM HANDLERS =====
714
1398
 
715
1399
  async function handleTeamList() {
@@ -877,6 +1561,16 @@ export async function runCli(argv = process.argv.slice(2)) {
877
1561
  return;
878
1562
  }
879
1563
 
1564
+ if (command === 'project-update') {
1565
+ await handleProjectUpdateCommand(rest);
1566
+ return;
1567
+ }
1568
+
1569
+ if (command === 'sync-doc') {
1570
+ await handleSyncDocCommand(rest);
1571
+ return;
1572
+ }
1573
+
880
1574
  if (command === 'team') {
881
1575
  await handleTeam(rest);
882
1576
  return;