@khangal.j/fireside-cli 0.0.5 → 0.0.7

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.
Files changed (2) hide show
  1. package/dist/index.js +293 -28
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/index.ts
27
- var import_promises2 = require("fs/promises");
27
+ var import_promises3 = require("fs/promises");
28
28
  var import_commander = require("commander");
29
29
  var import_picocolors = __toESM(require("picocolors"));
30
30
 
@@ -487,6 +487,51 @@ function openBrowser(url) {
487
487
  }
488
488
  }
489
489
 
490
+ // src/lib/project-config.ts
491
+ var import_promises2 = require("fs/promises");
492
+ var import_node_path2 = require("path");
493
+ var PROJECT_CONFIG_FILENAME = ".fireside.json";
494
+ async function loadProjectConfig(startDir = process.cwd()) {
495
+ const { root } = (0, import_node_path2.parse)(startDir);
496
+ let dir = startDir;
497
+ for (; ; ) {
498
+ const candidate = (0, import_node_path2.join)(dir, PROJECT_CONFIG_FILENAME);
499
+ try {
500
+ const raw = await (0, import_promises2.readFile)(candidate, "utf8");
501
+ return { config: JSON.parse(raw), path: candidate };
502
+ } catch (error) {
503
+ const code = error.code;
504
+ if (error instanceof SyntaxError) {
505
+ throw new Error(
506
+ `Invalid ${PROJECT_CONFIG_FILENAME} at ${candidate}: ${error.message}`
507
+ );
508
+ }
509
+ if (code !== "ENOENT") {
510
+ return null;
511
+ }
512
+ }
513
+ if (dir === root) {
514
+ return null;
515
+ }
516
+ const parent = (0, import_node_path2.dirname)(dir);
517
+ if (parent === dir) {
518
+ return null;
519
+ }
520
+ dir = parent;
521
+ }
522
+ }
523
+ async function loadDefaultProject(startDir) {
524
+ const loaded = await loadProjectConfig(startDir);
525
+ const project = loaded?.config.project;
526
+ return typeof project === "string" && project.trim() ? project.trim() : void 0;
527
+ }
528
+ async function writeProjectConfig(dir, config) {
529
+ const path = (0, import_node_path2.join)(dir, PROJECT_CONFIG_FILENAME);
530
+ await (0, import_promises2.writeFile)(path, `${JSON.stringify(config, null, 2)}
531
+ `);
532
+ return path;
533
+ }
534
+
490
535
  // src/index.ts
491
536
  function printInfo(message) {
492
537
  console.log(`${import_picocolors.default.cyan("[i]")} ${message}`);
@@ -708,6 +753,71 @@ function serializeTaskHandoff(taskEntry, taskHandoff) {
708
753
  }
709
754
  };
710
755
  }
756
+ function shortId(id) {
757
+ return id.slice(0, 8);
758
+ }
759
+ function leanAssignees(assignees) {
760
+ return assignees.map(
761
+ (member) => member.username ? `@${member.username}` : member.email
762
+ );
763
+ }
764
+ function serializeTaskRow(taskEntry) {
765
+ const assignees = leanAssignees(taskEntry.task.assignees);
766
+ return {
767
+ id: shortId(taskEntry.task.id),
768
+ title: taskEntry.task.title,
769
+ project: taskEntry.project.title,
770
+ board: taskEntry.board.title,
771
+ column: taskEntry.column.title,
772
+ role: taskEntry.column.role,
773
+ position: taskEntry.task.position,
774
+ ...taskEntry.task.dueDate ? { dueDate: taskEntry.task.dueDate } : {},
775
+ ...assignees.length ? { assignees } : {},
776
+ ...taskEntry.task.description ? { hasDescription: true } : {}
777
+ };
778
+ }
779
+ function serializeAssignedRow(task) {
780
+ const assignees = leanAssignees(task.assignees);
781
+ return {
782
+ id: shortId(task.id),
783
+ title: task.title,
784
+ project: task.projectTitle,
785
+ board: task.boardTitle,
786
+ column: task.columnTitle,
787
+ ...task.dueDate ? { dueDate: task.dueDate } : {},
788
+ ...assignees.length ? { assignees } : {},
789
+ ...task.description ? { hasDescription: true } : {}
790
+ };
791
+ }
792
+ function leanHandoff(handoff) {
793
+ const handle = (user) => user.username ? `@${user.username}` : user.email;
794
+ return {
795
+ id: handoff.id,
796
+ status: handoff.status,
797
+ summary: handoff.summary,
798
+ from: handle(handoff.fromUser),
799
+ to: handle(handoff.toUser),
800
+ next: handle(handoff.nextAssigneeUser),
801
+ contextMarkdown: handoff.contextMarkdown,
802
+ createdAt: handoff.createdAt
803
+ };
804
+ }
805
+ function serializeTaskFull(taskEntry, contextRevision, taskHandoff) {
806
+ return {
807
+ id: taskEntry.task.id,
808
+ title: taskEntry.task.title,
809
+ project: taskEntry.project.title,
810
+ board: taskEntry.board.title,
811
+ column: taskEntry.column.title,
812
+ role: taskEntry.column.role,
813
+ position: taskEntry.task.position,
814
+ dueDate: taskEntry.task.dueDate,
815
+ assignees: leanAssignees(taskEntry.task.assignees),
816
+ description: taskEntry.task.description,
817
+ ...contextRevision !== void 0 ? { contextRevision } : {},
818
+ ...taskHandoff !== void 0 ? { handoff: taskHandoff ? leanHandoff(taskHandoff) : null } : {}
819
+ };
820
+ }
711
821
  function printTaskSummary(taskEntry) {
712
822
  console.log(formatHeading(taskEntry.task.title, taskEntry.task.id));
713
823
  console.log(
@@ -778,7 +888,7 @@ function printTaskHandoff(taskEntry, taskHandoff) {
778
888
  }
779
889
  async function readTextFile(filePath, label) {
780
890
  try {
781
- return await (0, import_promises2.readFile)(filePath, "utf8");
891
+ return await (0, import_promises3.readFile)(filePath, "utf8");
782
892
  } catch (error) {
783
893
  const message = error instanceof Error ? error.message : `Unable to read ${label} file.`;
784
894
  throw new Error(`Failed to read ${label} file: ${message}`);
@@ -862,6 +972,23 @@ async function loadCliContextWithCurrentUser(baseUrl) {
862
972
  user: await getCurrentUser(cliContext.baseUrl, cliContext.accessToken)
863
973
  };
864
974
  }
975
+ async function resolveProjectSelector(explicit, allProjects) {
976
+ if (allProjects) {
977
+ return void 0;
978
+ }
979
+ if (explicit) {
980
+ return explicit;
981
+ }
982
+ return loadDefaultProject();
983
+ }
984
+ function findTaskMatches(entries, taskId) {
985
+ const trimmed = taskId.trim();
986
+ const exact = entries.filter((entry) => entry.task.id === trimmed);
987
+ if (exact.length || trimmed.length < 4) {
988
+ return exact;
989
+ }
990
+ return entries.filter((entry) => entry.task.id.startsWith(trimmed));
991
+ }
865
992
  async function loadProjectBoardsResults(baseUrl, accessToken, projectSelector) {
866
993
  const projects = await listProjects(baseUrl, accessToken);
867
994
  const selectedProjects = projectSelector ? [resolveByIdOrTitle(projects, projectSelector, "project")] : projects;
@@ -873,13 +1000,30 @@ async function loadProjectBoardsResults(baseUrl, accessToken, projectSelector) {
873
1000
  );
874
1001
  }
875
1002
  async function resolveTaskEntry(baseUrl, accessToken, taskId, projectSelector) {
876
- const taskEntries = flattenTaskEntries(
877
- await loadProjectBoardsResults(baseUrl, accessToken, projectSelector)
878
- ).filter((taskEntry) => taskEntry.task.id === taskId);
879
- if (!taskEntries.length) {
1003
+ const scoped = await resolveProjectSelector(projectSelector, false);
1004
+ let matches = findTaskMatches(
1005
+ flattenTaskEntries(
1006
+ await loadProjectBoardsResults(baseUrl, accessToken, scoped)
1007
+ ),
1008
+ taskId
1009
+ );
1010
+ if (!matches.length && scoped && !projectSelector) {
1011
+ matches = findTaskMatches(
1012
+ flattenTaskEntries(
1013
+ await loadProjectBoardsResults(baseUrl, accessToken, void 0)
1014
+ ),
1015
+ taskId
1016
+ );
1017
+ }
1018
+ if (!matches.length) {
880
1019
  throw new Error(`Task ${taskId} was not found.`);
881
1020
  }
882
- return taskEntries[0];
1021
+ if (matches.length > 1) {
1022
+ throw new Error(
1023
+ `Task id "${taskId}" is ambiguous (${matches.length} matches). Use more characters or the full id.`
1024
+ );
1025
+ }
1026
+ return matches[0];
883
1027
  }
884
1028
  function parseTargetPosition(value) {
885
1029
  if (value === void 0) {
@@ -1015,7 +1159,7 @@ function printAssignedTasks(tasks, projectFilter) {
1015
1159
  }
1016
1160
  }
1017
1161
  var program = new import_commander.Command();
1018
- program.name("fireside").description("Fireside CLI").version("0.0.2").configureOutput({
1162
+ program.name("fireside").description("Fireside CLI").version("0.0.7").configureOutput({
1019
1163
  outputError: (message, write) => write(import_picocolors.default.red(message))
1020
1164
  }).showHelpAfterError();
1021
1165
  addBaseUrlOption(
@@ -1084,18 +1228,23 @@ addBaseUrlOption(
1084
1228
  })
1085
1229
  );
1086
1230
  addBaseUrlOption(
1087
- program.command("my-stuff").description("List tasks currently assigned to you").option("--json", "Print assigned tasks as JSON").option("-p, --project <project>", "Filter by project id or title").action(
1231
+ program.command("my-stuff").description("List tasks currently assigned to you").option("--json", "Print assigned tasks as compact JSON").option("--raw", "With --json, print the full unabridged shape (larger)").option("-p, --project <project>", "Filter by project id or title").option("--all-projects", "Ignore the default project; span all projects").action(
1088
1232
  async (options) => {
1089
1233
  const state = await requireAuthState();
1090
1234
  const configState = await loadConfigState();
1091
1235
  const baseUrl = await resolveBaseUrl(options.baseUrl, configState);
1092
- const tasks = await listAssignedTasks(baseUrl, state.accessToken);
1093
- const filteredTasks = filterAssignedTasksByProject(
1094
- tasks,
1095
- options.project
1236
+ const projectFilter = await resolveProjectSelector(
1237
+ options.project,
1238
+ options.allProjects
1096
1239
  );
1240
+ const tasks = await listAssignedTasks(baseUrl, state.accessToken);
1241
+ const filteredTasks = filterAssignedTasksByProject(tasks, projectFilter);
1097
1242
  if (options.json) {
1098
- console.log(JSON.stringify(filteredTasks, null, 2));
1243
+ if (options.raw) {
1244
+ console.log(JSON.stringify(filteredTasks, null, 2));
1245
+ } else {
1246
+ console.log(JSON.stringify(filteredTasks.map(serializeAssignedRow)));
1247
+ }
1099
1248
  return;
1100
1249
  }
1101
1250
  printAssignedTasks(filteredTasks, options.project);
@@ -1114,13 +1263,50 @@ addBaseUrlOption(
1114
1263
  printProjects(projects);
1115
1264
  })
1116
1265
  );
1266
+ var configCommand = program.command("config").description("Manage CLI configuration");
1267
+ addBaseUrlOption(
1268
+ configCommand.command("show").description("Show resolved defaults (base URL, default project)").action(async (options) => {
1269
+ const configState = await loadConfigState();
1270
+ const baseUrl = await resolveBaseUrl(options.baseUrl, configState);
1271
+ const loaded = await loadProjectConfig();
1272
+ printKeyValue("Base URL", baseUrl);
1273
+ if (loaded?.config.project) {
1274
+ printKeyValue("Default project", loaded.config.project);
1275
+ printKeyValue(" from", loaded.path);
1276
+ } else {
1277
+ printKeyValue("Default project", import_picocolors.default.dim("Not set"));
1278
+ }
1279
+ })
1280
+ );
1281
+ addBaseUrlOption(
1282
+ configCommand.command("use-project <project>").description(
1283
+ `Set the default project for this directory (writes ${PROJECT_CONFIG_FILENAME})`
1284
+ ).action(async (project, options) => {
1285
+ const { accessToken, baseUrl } = await loadCliContext(options.baseUrl);
1286
+ const projects = await listProjects(baseUrl, accessToken);
1287
+ const resolved = resolveByIdOrTitle(projects, project, "project");
1288
+ const path = await writeProjectConfig(process.cwd(), {
1289
+ project: resolved.title
1290
+ });
1291
+ printSuccess(`Default project set to ${import_picocolors.default.cyan(resolved.title)}.`);
1292
+ printKeyValue("Wrote", path);
1293
+ })
1294
+ );
1117
1295
  var tasksCommand = program.command("tasks").description("Interact with tasks");
1118
1296
  addBaseUrlOption(
1119
- tasksCommand.command("list").description("List accessible tasks").option("--json", "Print tasks as JSON").option("-p, --project <project>", "Filter by project id or title").option("--board <board>", "Filter by board id or title").option("--column <column>", "Filter by column id or title").action(
1297
+ tasksCommand.command("list").description(
1298
+ "List tasks assigned to you in the default project (.fireside.json)"
1299
+ ).option("--json", "Print tasks as compact JSON").option("--raw", "With --json, print the full unabridged shape (larger)").option("-p, --project <project>", "Filter by project id or title").option("--board <board>", "Filter by board id or title").option("--column <column>", "Filter by column id or title").option("--all", "Show everyone's tasks across all projects").option("--everyone", "Include tasks not assigned to you").option("--all-projects", "Ignore the default project; span all projects").action(
1120
1300
  async (options) => {
1121
1301
  const { accessToken, baseUrl } = await loadCliContext(options.baseUrl);
1302
+ const includeEveryone = Boolean(options.all || options.everyone);
1303
+ const spanAllProjects = Boolean(options.all || options.allProjects);
1304
+ const projectSelector = await resolveProjectSelector(
1305
+ options.project,
1306
+ spanAllProjects
1307
+ );
1122
1308
  let taskEntries = flattenTaskEntries(
1123
- await loadProjectBoardsResults(baseUrl, accessToken, options.project)
1309
+ await loadProjectBoardsResults(baseUrl, accessToken, projectSelector)
1124
1310
  );
1125
1311
  if (options.board) {
1126
1312
  taskEntries = taskEntries.filter(
@@ -1132,14 +1318,30 @@ addBaseUrlOption(
1132
1318
  (taskEntry) => matchIdOrTitle(taskEntry.column, options.column)
1133
1319
  );
1134
1320
  }
1135
- if (options.json) {
1136
- console.log(
1137
- JSON.stringify(
1138
- taskEntries.map((taskEntry) => serializeTaskEntry(taskEntry)),
1139
- null,
1140
- 2
1321
+ if (!includeEveryone) {
1322
+ const currentUser = await getCurrentUser(baseUrl, accessToken);
1323
+ taskEntries = taskEntries.filter(
1324
+ (taskEntry) => taskEntry.task.assignees.some(
1325
+ (assignee) => assignee.id === currentUser.id
1141
1326
  )
1142
1327
  );
1328
+ }
1329
+ if (options.json) {
1330
+ if (options.raw) {
1331
+ console.log(
1332
+ JSON.stringify(
1333
+ taskEntries.map((taskEntry) => serializeTaskEntry(taskEntry)),
1334
+ null,
1335
+ 2
1336
+ )
1337
+ );
1338
+ } else {
1339
+ console.log(
1340
+ JSON.stringify(
1341
+ taskEntries.map((taskEntry) => serializeTaskRow(taskEntry))
1342
+ )
1343
+ );
1344
+ }
1143
1345
  return;
1144
1346
  }
1145
1347
  if (!taskEntries.length) {
@@ -1153,7 +1355,52 @@ addBaseUrlOption(
1153
1355
  )
1154
1356
  );
1155
1357
  addBaseUrlOption(
1156
- tasksCommand.command("get <taskId>").description("Show a task by id").option("--json", "Print the task as JSON").option("-p, --project <project>", "Limit lookup to a project id or title").option("--context", "Load the latest saved task context").option("--handoff", "Load the active handoff context").action(
1358
+ tasksCommand.command("columns").description("List a board's columns (id, role, title) without tasks").option("--json", "Print columns as JSON").option("-p, --project <project>", "Filter by project id or title").option("--board <board>", "Board id or title").action(
1359
+ async (options) => {
1360
+ const { accessToken, baseUrl } = await loadCliContext(options.baseUrl);
1361
+ const projectSelector = await resolveProjectSelector(
1362
+ options.project,
1363
+ options.allProjects
1364
+ );
1365
+ const [projectBoardsResult] = await loadProjectBoardsResults(
1366
+ baseUrl,
1367
+ accessToken,
1368
+ projectSelector
1369
+ );
1370
+ if (!projectBoardsResult) {
1371
+ printWarning("No project found.");
1372
+ return;
1373
+ }
1374
+ const board = resolveBoard(
1375
+ projectBoardsResult.boardsData.boards,
1376
+ options.board
1377
+ );
1378
+ const columns = [...board.columns].sort(
1379
+ (left, right) => left.position - right.position
1380
+ );
1381
+ if (options.json) {
1382
+ console.log(
1383
+ JSON.stringify(
1384
+ columns.map((column) => ({
1385
+ id: column.id,
1386
+ role: column.role,
1387
+ title: column.title
1388
+ }))
1389
+ )
1390
+ );
1391
+ return;
1392
+ }
1393
+ console.log(
1394
+ formatHeading(`${projectBoardsResult.project.title} / ${board.title}`)
1395
+ );
1396
+ for (const column of columns) {
1397
+ console.log(` ${import_picocolors.default.dim(column.role.padEnd(8))} ${column.title}`);
1398
+ }
1399
+ }
1400
+ )
1401
+ );
1402
+ addBaseUrlOption(
1403
+ tasksCommand.command("get <taskId>").description("Show a task by id (accepts a short id prefix)").option("--json", "Print the task as JSON").option("--raw", "With --json, print the full unabridged shape (larger)").option("-p, --project <project>", "Limit lookup to a project id or title").option("--context", "Load the latest saved task context").option("--handoff", "Load the active handoff context").action(
1157
1404
  async (taskId, options) => {
1158
1405
  const { accessToken, baseUrl } = await loadCliContext(options.baseUrl);
1159
1406
  const taskEntry = await resolveTaskEntry(
@@ -1179,7 +1426,7 @@ addBaseUrlOption(
1179
1426
  if (options.json) {
1180
1427
  console.log(
1181
1428
  JSON.stringify(
1182
- serializeTaskEntry(taskEntry, contextRevision, taskHandoff),
1429
+ options.raw ? serializeTaskEntry(taskEntry, contextRevision, taskHandoff) : serializeTaskFull(taskEntry, contextRevision, taskHandoff),
1183
1430
  null,
1184
1431
  2
1185
1432
  )
@@ -1191,7 +1438,10 @@ addBaseUrlOption(
1191
1438
  )
1192
1439
  );
1193
1440
  addBaseUrlOption(
1194
- tasksCommand.command("create").description("Create a task").requiredOption("-p, --project <project>", "Project id or title").requiredOption("-t, --title <title>", "Task title").requiredOption("-c, --column <column>", "Column id or title").option("--board <board>", "Board id or title").option("--description <description>", "Task description").option("--description-file <path>", "Read task description from a file").option("--due-date <date>", "Due date in YYYY-MM-DD format").option(
1441
+ tasksCommand.command("create").description("Create a task").option(
1442
+ "-p, --project <project>",
1443
+ "Project id or title (defaults to .fireside.json)"
1444
+ ).requiredOption("-t, --title <title>", "Task title").requiredOption("-c, --column <column>", "Column id or title").option("--board <board>", "Board id or title").option("--description <description>", "Task description").option("--description-file <path>", "Read task description from a file").option("--due-date <date>", "Due date in YYYY-MM-DD format").option(
1195
1445
  "-a, --assignee <member>",
1196
1446
  "Assign a member by @username, email, id, or me",
1197
1447
  collectOptionValue,
@@ -1199,10 +1449,16 @@ addBaseUrlOption(
1199
1449
  ).option("--json", "Print the created task as JSON").action(
1200
1450
  async (options) => {
1201
1451
  const { accessToken, baseUrl, user } = await loadCliContextWithCurrentUser(options.baseUrl);
1452
+ const projectSelector = options.project ?? await loadDefaultProject();
1453
+ if (!projectSelector) {
1454
+ throw new Error(
1455
+ `No project. Pass -p <project> or add ${PROJECT_CONFIG_FILENAME} (\`fireside config use-project <project>\`).`
1456
+ );
1457
+ }
1202
1458
  const [projectBoardsResult] = await loadProjectBoardsResults(
1203
1459
  baseUrl,
1204
1460
  accessToken,
1205
- options.project
1461
+ projectSelector
1206
1462
  );
1207
1463
  const board = resolveBoard(
1208
1464
  projectBoardsResult.boardsData.boards,
@@ -1249,7 +1505,10 @@ addBaseUrlOption(
1249
1505
  )
1250
1506
  );
1251
1507
  addBaseUrlOption(
1252
- tasksCommand.command("bulk-create").description("Create many tasks at once from a file").requiredOption("-p, --project <project>", "Project id or title").requiredOption(
1508
+ tasksCommand.command("bulk-create").description("Create many tasks at once from a file").option(
1509
+ "-p, --project <project>",
1510
+ "Project id or title (defaults to .fireside.json)"
1511
+ ).requiredOption(
1253
1512
  "--file <path>",
1254
1513
  "Tasks file: JSON array, `{ tasks: [...] }`, or one title per line"
1255
1514
  ).option("--board <board>", "Board id or title").option(
@@ -1269,6 +1528,12 @@ addBaseUrlOption(
1269
1528
  ).option("--dry-run", "Resolve and preview tasks without creating them").option("--json", "Print results as JSON").action(
1270
1529
  async (options) => {
1271
1530
  const { accessToken, baseUrl, user } = await loadCliContextWithCurrentUser(options.baseUrl);
1531
+ const projectSelector = options.project ?? await loadDefaultProject();
1532
+ if (!projectSelector) {
1533
+ throw new Error(
1534
+ `No project. Pass -p <project> or add ${PROJECT_CONFIG_FILENAME} (\`fireside config use-project <project>\`).`
1535
+ );
1536
+ }
1272
1537
  const rows = parseBulkTaskRows(
1273
1538
  await readTextFile(options.file, "tasks")
1274
1539
  );
@@ -1278,7 +1543,7 @@ addBaseUrlOption(
1278
1543
  const [projectBoardsResult] = await loadProjectBoardsResults(
1279
1544
  baseUrl,
1280
1545
  accessToken,
1281
- options.project
1546
+ projectSelector
1282
1547
  );
1283
1548
  const board = resolveBoard(
1284
1549
  projectBoardsResult.boardsData.boards,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khangal.j/fireside-cli",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Fireside CLI",
5
5
  "license": "MIT",
6
6
  "private": false,