skyboard-cli 0.1.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.
@@ -0,0 +1,64 @@
1
+ import { requireAgent } from "../lib/auth.js";
2
+ import { fetchBoardData } from "../lib/pds.js";
3
+ import { getDefaultBoard } from "../lib/config.js";
4
+ import { resolveCardRef } from "../lib/card-ref.js";
5
+ import { generateTID, buildAtUri, TASK_COLLECTION, COMMENT_COLLECTION, BOARD_COLLECTION } from "../lib/tid.js";
6
+ import { shortRkey } from "../lib/display.js";
7
+ import chalk from "chalk";
8
+
9
+ export async function commentCommand(
10
+ cardRef: string,
11
+ text: string,
12
+ opts: { board?: string; json?: boolean },
13
+ ): Promise<void> {
14
+ const { agent, did } = await requireAgent();
15
+
16
+ const boardRef = resolveBoard(opts.board);
17
+
18
+ const data = await fetchBoardData(boardRef.did, boardRef.rkey, did);
19
+ if (!data) {
20
+ console.error(chalk.red("Board not found."));
21
+ process.exit(1);
22
+ }
23
+
24
+ let task;
25
+ try {
26
+ task = resolveCardRef(cardRef, data.tasks);
27
+ } catch (err) {
28
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
29
+ process.exit(1);
30
+ }
31
+
32
+ const boardUri = buildAtUri(boardRef.did, BOARD_COLLECTION, boardRef.rkey);
33
+ const taskUri = buildAtUri(task.did, TASK_COLLECTION, task.rkey);
34
+ const commentRkey = generateTID();
35
+
36
+ await agent.com.atproto.repo.putRecord({
37
+ repo: did,
38
+ collection: COMMENT_COLLECTION,
39
+ rkey: commentRkey,
40
+ record: {
41
+ $type: "dev.skyboard.comment",
42
+ targetTaskUri: taskUri,
43
+ boardUri,
44
+ text,
45
+ createdAt: new Date().toISOString(),
46
+ },
47
+ validate: false,
48
+ });
49
+
50
+ if (opts.json) {
51
+ console.log(JSON.stringify({ rkey: commentRkey, taskRkey: task.rkey, text }));
52
+ } else {
53
+ console.log(chalk.green(`Comment added to ${shortRkey(task.rkey)} "${task.effectiveTitle}"`));
54
+ }
55
+ }
56
+
57
+ function resolveBoard(boardOpt?: string): { did: string; rkey: string } {
58
+ const defaultBoard = getDefaultBoard();
59
+ if (!defaultBoard) {
60
+ console.error(chalk.red("No default board set. Run `sb use <board>` first."));
61
+ process.exit(1);
62
+ }
63
+ return defaultBoard;
64
+ }
@@ -0,0 +1,89 @@
1
+ import { requireAgent } from "../lib/auth.js";
2
+ import { fetchBoardData } from "../lib/pds.js";
3
+ import { getDefaultBoard } from "../lib/config.js";
4
+ import { resolveCardRef } from "../lib/card-ref.js";
5
+ import { generateTID, buildAtUri, TASK_COLLECTION, OP_COLLECTION, BOARD_COLLECTION } from "../lib/tid.js";
6
+ import { shortRkey } from "../lib/display.js";
7
+ import type { OpFields } from "../lib/types.js";
8
+ import chalk from "chalk";
9
+
10
+ export async function editCommand(
11
+ cardRef: string,
12
+ opts: { title?: string; description?: string; label?: string[]; board?: string; json?: boolean },
13
+ ): Promise<void> {
14
+ const { agent, did } = await requireAgent();
15
+
16
+ const boardRef = resolveBoard(opts.board);
17
+
18
+ const data = await fetchBoardData(boardRef.did, boardRef.rkey, did);
19
+ if (!data) {
20
+ console.error(chalk.red("Board not found."));
21
+ process.exit(1);
22
+ }
23
+
24
+ let task;
25
+ try {
26
+ task = resolveCardRef(cardRef, data.tasks);
27
+ } catch (err) {
28
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
29
+ process.exit(1);
30
+ }
31
+
32
+ const fields: OpFields = {};
33
+ if (opts.title) fields.title = opts.title;
34
+ if (opts.description) fields.description = opts.description;
35
+ if (opts.label && opts.label.length > 0) {
36
+ // Resolve label names to IDs
37
+ const labelIds: string[] = [];
38
+ for (const name of opts.label) {
39
+ const label = data.board.labels?.find(
40
+ (l) => l.name.toLowerCase() === name.toLowerCase(),
41
+ );
42
+ if (label) {
43
+ labelIds.push(label.id);
44
+ } else {
45
+ console.error(chalk.yellow(`Warning: label "${name}" not found, skipping.`));
46
+ }
47
+ }
48
+ if (labelIds.length > 0) fields.labelIds = labelIds;
49
+ }
50
+
51
+ if (Object.keys(fields).length === 0) {
52
+ console.error(chalk.red("Nothing to edit. Use -t, -d, or -l flags."));
53
+ process.exit(1);
54
+ }
55
+
56
+ const boardUri = buildAtUri(boardRef.did, BOARD_COLLECTION, boardRef.rkey);
57
+ const taskUri = buildAtUri(task.did, TASK_COLLECTION, task.rkey);
58
+ const opRkey = generateTID();
59
+
60
+ await agent.com.atproto.repo.putRecord({
61
+ repo: did,
62
+ collection: OP_COLLECTION,
63
+ rkey: opRkey,
64
+ record: {
65
+ $type: "dev.skyboard.op",
66
+ targetTaskUri: taskUri,
67
+ boardUri,
68
+ fields,
69
+ createdAt: new Date().toISOString(),
70
+ },
71
+ validate: false,
72
+ });
73
+
74
+ if (opts.json) {
75
+ console.log(JSON.stringify({ rkey: task.rkey, fields }));
76
+ } else {
77
+ const changes = Object.keys(fields).join(", ");
78
+ console.log(chalk.green(`Edited ${shortRkey(task.rkey)} "${task.effectiveTitle}" (${changes})`));
79
+ }
80
+ }
81
+
82
+ function resolveBoard(boardOpt?: string): { did: string; rkey: string } {
83
+ const defaultBoard = getDefaultBoard();
84
+ if (!defaultBoard) {
85
+ console.error(chalk.red("No default board set. Run `sb use <board>` first."));
86
+ process.exit(1);
87
+ }
88
+ return defaultBoard;
89
+ }
@@ -0,0 +1,19 @@
1
+ import { login } from "../lib/auth.js";
2
+ import { loadAuthInfo, clearDefaultBoard } from "../lib/config.js";
3
+ import chalk from "chalk";
4
+
5
+ export async function loginCommand(handle: string): Promise<void> {
6
+ const previousAuth = loadAuthInfo();
7
+ try {
8
+ console.log(`Logging in as ${chalk.bold(handle)}...`);
9
+ const { did, handle: resolvedHandle } = await login(handle);
10
+ // Clear board selection when switching accounts
11
+ if (previousAuth && previousAuth.did !== did) {
12
+ clearDefaultBoard();
13
+ }
14
+ console.log(chalk.green(`Logged in as ${resolvedHandle} (${did})`));
15
+ } catch (err) {
16
+ console.error(chalk.red(`Login failed: ${err instanceof Error ? err.message : err}`));
17
+ process.exit(1);
18
+ }
19
+ }
@@ -0,0 +1,14 @@
1
+ import { logout } from "../lib/auth.js";
2
+ import { loadAuthInfo, clearDefaultBoard } from "../lib/config.js";
3
+ import chalk from "chalk";
4
+
5
+ export function logoutCommand(): void {
6
+ const info = loadAuthInfo();
7
+ if (!info) {
8
+ console.log("Not logged in.");
9
+ return;
10
+ }
11
+ logout();
12
+ clearDefaultBoard();
13
+ console.log(chalk.green(`Logged out (was ${info.handle}).`));
14
+ }
@@ -0,0 +1,84 @@
1
+ import { requireAgent } from "../lib/auth.js";
2
+ import { fetchBoardData } from "../lib/pds.js";
3
+ import { getDefaultBoard } from "../lib/config.js";
4
+ import { resolveCardRef } from "../lib/card-ref.js";
5
+ import { resolveColumn } from "../lib/column-match.js";
6
+ import { generateTID, buildAtUri, TASK_COLLECTION, OP_COLLECTION, BOARD_COLLECTION } from "../lib/tid.js";
7
+ import { shortRkey } from "../lib/display.js";
8
+ import { generateKeyBetween } from "fractional-indexing";
9
+ import chalk from "chalk";
10
+
11
+ export async function mvCommand(
12
+ cardRef: string,
13
+ columnRef: string,
14
+ opts: { board?: string; json?: boolean },
15
+ ): Promise<void> {
16
+ const { agent, did } = await requireAgent();
17
+
18
+ const boardRef = resolveBoard(opts.board);
19
+
20
+ const data = await fetchBoardData(boardRef.did, boardRef.rkey, did);
21
+ if (!data) {
22
+ console.error(chalk.red("Board not found."));
23
+ process.exit(1);
24
+ }
25
+
26
+ let task;
27
+ try {
28
+ task = resolveCardRef(cardRef, data.tasks);
29
+ } catch (err) {
30
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
31
+ process.exit(1);
32
+ }
33
+
34
+ let targetCol;
35
+ try {
36
+ targetCol = resolveColumn(columnRef, data.board.columns);
37
+ } catch (err) {
38
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
39
+ process.exit(1);
40
+ }
41
+
42
+ // Generate position at end of target column
43
+ const colTasks = data.tasks
44
+ .filter((t) => t.effectiveColumnId === targetCol.id)
45
+ .sort((a, b) => a.effectivePosition.localeCompare(b.effectivePosition));
46
+ const lastPos = colTasks.length > 0 ? colTasks[colTasks.length - 1].effectivePosition : null;
47
+ const position = generateKeyBetween(lastPos, null);
48
+
49
+ const boardUri = buildAtUri(boardRef.did, BOARD_COLLECTION, boardRef.rkey);
50
+ const taskUri = buildAtUri(task.did, TASK_COLLECTION, task.rkey);
51
+ const opRkey = generateTID();
52
+
53
+ await agent.com.atproto.repo.putRecord({
54
+ repo: did,
55
+ collection: OP_COLLECTION,
56
+ rkey: opRkey,
57
+ record: {
58
+ $type: "dev.skyboard.op",
59
+ targetTaskUri: taskUri,
60
+ boardUri,
61
+ fields: {
62
+ columnId: targetCol.id,
63
+ position,
64
+ },
65
+ createdAt: new Date().toISOString(),
66
+ },
67
+ validate: false,
68
+ });
69
+
70
+ if (opts.json) {
71
+ console.log(JSON.stringify({ rkey: task.rkey, column: targetCol.name }));
72
+ } else {
73
+ console.log(chalk.green(`Moved ${shortRkey(task.rkey)} "${task.effectiveTitle}" → ${targetCol.name}`));
74
+ }
75
+ }
76
+
77
+ function resolveBoard(boardOpt?: string): { did: string; rkey: string } {
78
+ const defaultBoard = getDefaultBoard();
79
+ if (!defaultBoard) {
80
+ console.error(chalk.red("No default board set. Run `sb use <board>` first."));
81
+ process.exit(1);
82
+ }
83
+ return defaultBoard;
84
+ }
@@ -0,0 +1,80 @@
1
+ import { requireAgent } from "../lib/auth.js";
2
+ import { fetchBoardData } from "../lib/pds.js";
3
+ import { getDefaultBoard } from "../lib/config.js";
4
+ import { resolveColumn } from "../lib/column-match.js";
5
+ import { generateTID, buildAtUri, TASK_COLLECTION, BOARD_COLLECTION } from "../lib/tid.js";
6
+ import { shortRkey } from "../lib/display.js";
7
+ import { generateKeyBetween } from "fractional-indexing";
8
+ import chalk from "chalk";
9
+
10
+ export async function newCommand(
11
+ title: string,
12
+ opts: { column?: string; description?: string; board?: string; json?: boolean },
13
+ ): Promise<void> {
14
+ const { agent, did } = await requireAgent();
15
+
16
+ const boardRef = resolveBoard(opts.board);
17
+
18
+ const data = await fetchBoardData(boardRef.did, boardRef.rkey, did);
19
+ if (!data) {
20
+ console.error(chalk.red("Board not found."));
21
+ process.exit(1);
22
+ }
23
+
24
+ // Determine target column
25
+ const sortedColumns = [...data.board.columns].sort((a, b) => a.order - b.order);
26
+ let targetCol;
27
+ if (opts.column) {
28
+ try {
29
+ targetCol = resolveColumn(opts.column, data.board.columns);
30
+ } catch (err) {
31
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
32
+ process.exit(1);
33
+ }
34
+ } else {
35
+ targetCol = sortedColumns[0];
36
+ }
37
+
38
+ // Generate position at end of column
39
+ const colTasks = data.tasks
40
+ .filter((t) => t.effectiveColumnId === targetCol.id)
41
+ .sort((a, b) => a.effectivePosition.localeCompare(b.effectivePosition));
42
+ const lastPos = colTasks.length > 0 ? colTasks[colTasks.length - 1].effectivePosition : null;
43
+ const position = generateKeyBetween(lastPos, null);
44
+
45
+ const boardUri = buildAtUri(boardRef.did, BOARD_COLLECTION, boardRef.rkey);
46
+ const rkey = generateTID();
47
+ const now = new Date().toISOString();
48
+
49
+ await agent.com.atproto.repo.putRecord({
50
+ repo: did,
51
+ collection: TASK_COLLECTION,
52
+ rkey,
53
+ record: {
54
+ $type: "dev.skyboard.task",
55
+ title,
56
+ ...(opts.description ? { description: opts.description } : {}),
57
+ columnId: targetCol.id,
58
+ boardUri,
59
+ position,
60
+ order: 0,
61
+ createdAt: now,
62
+ },
63
+ validate: false,
64
+ });
65
+
66
+ if (opts.json) {
67
+ console.log(JSON.stringify({ rkey, title, column: targetCol.name, position }));
68
+ } else {
69
+ console.log(chalk.green(`Created: ${shortRkey(rkey)} ${title} → ${targetCol.name}`));
70
+ }
71
+ }
72
+
73
+ function resolveBoard(boardOpt?: string): { did: string; rkey: string } {
74
+ const defaultBoard = getDefaultBoard();
75
+ if (!defaultBoard) {
76
+ console.error(chalk.red("No default board set. Run `sb use <board>` first."));
77
+ process.exit(1);
78
+ }
79
+ return defaultBoard;
80
+ }
@@ -0,0 +1,79 @@
1
+ import { requireAgent } from "../lib/auth.js";
2
+ import { fetchBoardData } from "../lib/pds.js";
3
+ import { getDefaultBoard } from "../lib/config.js";
4
+ import { resolveCardRef } from "../lib/card-ref.js";
5
+ import { shortRkey } from "../lib/display.js";
6
+ import { TASK_COLLECTION } from "../lib/tid.js";
7
+ import chalk from "chalk";
8
+ import { createInterface } from "node:readline";
9
+
10
+ export async function rmCommand(
11
+ cardRef: string,
12
+ opts: { force?: boolean; board?: string; json?: boolean },
13
+ ): Promise<void> {
14
+ const { agent, did } = await requireAgent();
15
+
16
+ const boardRef = resolveBoard(opts.board);
17
+
18
+ const data = await fetchBoardData(boardRef.did, boardRef.rkey, did);
19
+ if (!data) {
20
+ console.error(chalk.red("Board not found."));
21
+ process.exit(1);
22
+ }
23
+
24
+ let task;
25
+ try {
26
+ task = resolveCardRef(cardRef, data.tasks);
27
+ } catch (err) {
28
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
29
+ process.exit(1);
30
+ }
31
+
32
+ // Can only delete your own tasks
33
+ if (task.did !== did) {
34
+ console.error(chalk.red("You can only delete your own tasks."));
35
+ process.exit(1);
36
+ }
37
+
38
+ // Confirm unless --force
39
+ if (!opts.force) {
40
+ const confirmed = await confirm(
41
+ `Delete "${task.effectiveTitle}" (${shortRkey(task.rkey)})? [y/N] `,
42
+ );
43
+ if (!confirmed) {
44
+ console.log("Cancelled.");
45
+ return;
46
+ }
47
+ }
48
+
49
+ await agent.com.atproto.repo.deleteRecord({
50
+ repo: did,
51
+ collection: TASK_COLLECTION,
52
+ rkey: task.rkey,
53
+ });
54
+
55
+ if (opts.json) {
56
+ console.log(JSON.stringify({ deleted: task.rkey }));
57
+ } else {
58
+ console.log(chalk.green(`Deleted ${shortRkey(task.rkey)} "${task.effectiveTitle}"`));
59
+ }
60
+ }
61
+
62
+ function confirm(prompt: string): Promise<boolean> {
63
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
64
+ return new Promise((resolve) => {
65
+ rl.question(prompt, (answer) => {
66
+ rl.close();
67
+ resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
68
+ });
69
+ });
70
+ }
71
+
72
+ function resolveBoard(boardOpt?: string): { did: string; rkey: string } {
73
+ const defaultBoard = getDefaultBoard();
74
+ if (!defaultBoard) {
75
+ console.error(chalk.red("No default board set. Run `sb use <board>` first."));
76
+ process.exit(1);
77
+ }
78
+ return defaultBoard;
79
+ }
@@ -0,0 +1,100 @@
1
+ import { requireAgent } from "../lib/auth.js";
2
+ import { fetchBoardData } from "../lib/pds.js";
3
+ import { getDefaultBoard } from "../lib/config.js";
4
+ import { resolveCardRef } from "../lib/card-ref.js";
5
+ import { shortRkey, formatDate } from "../lib/display.js";
6
+ import { buildAtUri, TASK_COLLECTION } from "../lib/tid.js";
7
+ import chalk from "chalk";
8
+
9
+ export async function showCommand(ref: string, opts: { board?: string; json?: boolean }): Promise<void> {
10
+ const { did } = await requireAgent();
11
+
12
+ const boardRef = resolveBoard(opts.board);
13
+
14
+ const data = await fetchBoardData(boardRef.did, boardRef.rkey, did);
15
+ if (!data) {
16
+ console.error(chalk.red("Board not found."));
17
+ process.exit(1);
18
+ }
19
+
20
+ let task;
21
+ try {
22
+ task = resolveCardRef(ref, data.tasks);
23
+ } catch (err) {
24
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
25
+ process.exit(1);
26
+ }
27
+
28
+ const col = data.board.columns.find((c) => c.id === task.effectiveColumnId);
29
+ const labels = task.effectiveLabelIds
30
+ .map((id) => data.board.labels?.find((l) => l.id === id))
31
+ .filter(Boolean);
32
+
33
+ const taskUri = buildAtUri(task.did, TASK_COLLECTION, task.rkey);
34
+ const comments = data.comments
35
+ .filter((c) => c.targetTaskUri === taskUri)
36
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
37
+
38
+ if (opts.json) {
39
+ console.log(JSON.stringify({
40
+ rkey: task.rkey,
41
+ did: task.did,
42
+ title: task.effectiveTitle,
43
+ description: task.effectiveDescription,
44
+ column: col?.name,
45
+ labels: labels.map((l) => ({ id: l!.id, name: l!.name, color: l!.color })),
46
+ createdAt: task.createdAt,
47
+ lastModifiedAt: task.lastModifiedAt,
48
+ lastModifiedBy: task.lastModifiedBy,
49
+ opsApplied: task.appliedOps.length,
50
+ comments: comments.map((c) => ({
51
+ did: c.did,
52
+ text: c.text,
53
+ createdAt: c.createdAt,
54
+ })),
55
+ }, null, 2));
56
+ return;
57
+ }
58
+
59
+ console.log();
60
+ console.log(chalk.bold(task.effectiveTitle));
61
+ console.log(chalk.dim(`${shortRkey(task.rkey)} by ${task.did.slice(0, 20)}... ${formatDate(task.createdAt)}`));
62
+ console.log();
63
+
64
+ if (col) {
65
+ console.log(`${chalk.bold("Column:")} ${col.name}`);
66
+ }
67
+ if (labels.length > 0) {
68
+ const labelStr = labels.map((l) => chalk.hex(l!.color)(`[${l!.name}]`)).join(" ");
69
+ console.log(`${chalk.bold("Labels:")} ${labelStr}`);
70
+ }
71
+ if (task.effectiveDescription) {
72
+ console.log();
73
+ console.log(task.effectiveDescription);
74
+ }
75
+
76
+ if (task.appliedOps.length > 0) {
77
+ console.log();
78
+ console.log(chalk.dim(`${task.appliedOps.length} edit(s) applied`));
79
+ }
80
+
81
+ if (comments.length > 0) {
82
+ console.log();
83
+ console.log(chalk.bold.underline("Comments"));
84
+ for (const comment of comments) {
85
+ console.log();
86
+ console.log(` ${chalk.dim(comment.did.slice(0, 20) + "...")} ${chalk.dim(formatDate(comment.createdAt))}`);
87
+ console.log(` ${comment.text}`);
88
+ }
89
+ }
90
+ console.log();
91
+ }
92
+
93
+ function resolveBoard(boardOpt?: string): { did: string; rkey: string } {
94
+ const defaultBoard = getDefaultBoard();
95
+ if (!defaultBoard) {
96
+ console.error(chalk.red("No default board set. Run `sb use <board>` first."));
97
+ process.exit(1);
98
+ }
99
+ return defaultBoard;
100
+ }
@@ -0,0 +1,78 @@
1
+ import { loadAuthInfo, getDefaultBoard } from "../lib/config.js";
2
+ import { requireAgent } from "../lib/auth.js";
3
+ import { fetchBoardData } from "../lib/pds.js";
4
+ import chalk from "chalk";
5
+
6
+ export async function statusCommand(opts: { json?: boolean }): Promise<void> {
7
+ const info = loadAuthInfo();
8
+
9
+ if (!info) {
10
+ if (opts.json) {
11
+ console.log(JSON.stringify({ loggedIn: false }, null, 2));
12
+ } else {
13
+ console.log("Not logged in. Run `sb login <handle>` first.");
14
+ }
15
+ return;
16
+ }
17
+
18
+ const boardRef = getDefaultBoard();
19
+
20
+ if (!boardRef) {
21
+ if (opts.json) {
22
+ console.log(JSON.stringify({ loggedIn: true, ...info, board: null }, null, 2));
23
+ } else {
24
+ console.log(`${chalk.bold("Handle:")} ${info.handle}`);
25
+ console.log(`${chalk.bold("DID:")} ${info.did}`);
26
+ console.log(`\nNo default board set. Run ${chalk.cyan("sb use <board>")} to select one.`);
27
+ }
28
+ return;
29
+ }
30
+
31
+ const { did } = await requireAgent();
32
+ const data = await fetchBoardData(boardRef.did, boardRef.rkey, did);
33
+
34
+ if (!data) {
35
+ if (opts.json) {
36
+ console.log(JSON.stringify({ loggedIn: true, ...info, board: null }, null, 2));
37
+ } else {
38
+ console.log(`${chalk.bold("Handle:")} ${info.handle}`);
39
+ console.log(`${chalk.bold("DID:")} ${info.did}`);
40
+ console.log(`\n${chalk.red("Board not found.")}`);
41
+ }
42
+ return;
43
+ }
44
+
45
+ const sortedColumns = [...data.board.columns].sort((a, b) => a.order - b.order);
46
+ const columns = sortedColumns.map((col, i) => {
47
+ const taskCount = data.tasks.filter((t) => t.effectiveColumnId === col.id).length;
48
+ return { index: i + 1, name: col.name, id: col.id, taskCount };
49
+ });
50
+ const totalCards = data.tasks.length;
51
+
52
+ if (opts.json) {
53
+ console.log(JSON.stringify({
54
+ loggedIn: true,
55
+ handle: info.handle,
56
+ did: info.did,
57
+ board: {
58
+ name: data.board.name,
59
+ rkey: boardRef.rkey,
60
+ did: boardRef.did,
61
+ columns: columns.map(({ index, name, taskCount }) => ({ index, name, taskCount })),
62
+ totalCards,
63
+ },
64
+ }, null, 2));
65
+ return;
66
+ }
67
+
68
+ console.log(`${chalk.bold("Handle:")} ${info.handle}`);
69
+ console.log(`${chalk.bold("DID:")} ${info.did}`);
70
+ console.log(`${chalk.bold("Board:")} ${data.board.name}`);
71
+ console.log();
72
+
73
+ for (const col of columns) {
74
+ console.log(` ${chalk.dim(`${col.index}.`)} ${col.name} ${chalk.dim(`(${col.taskCount})`)}`);
75
+ }
76
+
77
+ console.log(`\n ${totalCards} cards total`);
78
+ }
@@ -0,0 +1,72 @@
1
+ import { requireAgent } from "../lib/auth.js";
2
+ import { fetchMyBoards, fetchBoard, resolveHandle } from "../lib/pds.js";
3
+ import { setDefaultBoard, loadConfig } from "../lib/config.js";
4
+ import { BOARD_COLLECTION } from "../lib/tid.js";
5
+ import chalk from "chalk";
6
+
7
+ /**
8
+ * Parse a board reference: name, rkey, AT URI, or web URL.
9
+ * Returns { did, rkey } or null.
10
+ */
11
+ async function parseBoardRef(
12
+ ref: string,
13
+ currentDid: string,
14
+ ): Promise<{ did: string; rkey: string } | null> {
15
+ // AT URI: at://did:plc:xxx/dev.skyboard.board/rkey
16
+ if (ref.startsWith("at://")) {
17
+ const parts = ref.replace("at://", "").split("/");
18
+ if (parts.length >= 3 && parts[1] === BOARD_COLLECTION) {
19
+ return { did: parts[0], rkey: parts[2] };
20
+ }
21
+ return null;
22
+ }
23
+
24
+ // Web URL: https://skyboard.dev/board/did:plc:xxx/rkey
25
+ if (ref.startsWith("http://") || ref.startsWith("https://")) {
26
+ const url = new URL(ref);
27
+ const pathParts = url.pathname.split("/").filter(Boolean);
28
+ // /board/did:plc:xxx/rkey
29
+ if (pathParts.length >= 3 && pathParts[0] === "board") {
30
+ return { did: pathParts[1], rkey: pathParts[2] };
31
+ }
32
+ // /board/rkey (own board)
33
+ if (pathParts.length === 2 && pathParts[0] === "board") {
34
+ return { did: currentDid, rkey: pathParts[1] };
35
+ }
36
+ return null;
37
+ }
38
+
39
+ // Check known boards by name (fuzzy)
40
+ const config = loadConfig();
41
+ const lowerRef = ref.toLowerCase();
42
+ const nameMatch = config.knownBoards.find(
43
+ (b) => b.name.toLowerCase().includes(lowerRef),
44
+ );
45
+ if (nameMatch) return { did: nameMatch.did, rkey: nameMatch.rkey };
46
+
47
+ // Try as rkey for own board
48
+ const board = await fetchBoard(currentDid, ref);
49
+ if (board) return { did: currentDid, rkey: ref };
50
+
51
+ return null;
52
+ }
53
+
54
+ export async function useCommand(boardRef: string): Promise<void> {
55
+ const { did } = await requireAgent();
56
+
57
+ const parsed = await parseBoardRef(boardRef, did);
58
+ if (!parsed) {
59
+ console.error(chalk.red(`Could not resolve board: ${boardRef}`));
60
+ console.error("Try a board name, rkey, AT URI, or web URL.");
61
+ process.exit(1);
62
+ }
63
+
64
+ const board = await fetchBoard(parsed.did, parsed.rkey);
65
+ if (!board) {
66
+ console.error(chalk.red(`Board not found at ${parsed.did}/${parsed.rkey}`));
67
+ process.exit(1);
68
+ }
69
+
70
+ setDefaultBoard({ did: parsed.did, rkey: parsed.rkey, name: board.name });
71
+ console.log(chalk.green(`Default board set to: ${chalk.bold(board.name)}`));
72
+ }