skyboard-cli 0.1.0 → 0.1.1
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/package.json +5 -1
- package/src/__tests__/auth-state.test.ts +0 -111
- package/src/__tests__/card-ref.test.ts +0 -42
- package/src/__tests__/column-match.test.ts +0 -57
- package/src/__tests__/commands.test.ts +0 -326
- package/src/__tests__/helpers.ts +0 -154
- package/src/__tests__/materialize.test.ts +0 -257
- package/src/__tests__/pds.test.ts +0 -213
- package/src/commands/add.ts +0 -47
- package/src/commands/boards.ts +0 -64
- package/src/commands/cards.ts +0 -65
- package/src/commands/cols.ts +0 -51
- package/src/commands/comment.ts +0 -64
- package/src/commands/edit.ts +0 -89
- package/src/commands/login.ts +0 -19
- package/src/commands/logout.ts +0 -14
- package/src/commands/mv.ts +0 -84
- package/src/commands/new.ts +0 -80
- package/src/commands/rm.ts +0 -79
- package/src/commands/show.ts +0 -100
- package/src/commands/status.ts +0 -78
- package/src/commands/use.ts +0 -72
- package/src/commands/whoami.ts +0 -22
- package/src/index.ts +0 -143
- package/src/lib/auth.ts +0 -244
- package/src/lib/card-ref.ts +0 -32
- package/src/lib/column-match.ts +0 -45
- package/src/lib/config.ts +0 -182
- package/src/lib/display.ts +0 -112
- package/src/lib/materialize.ts +0 -138
- package/src/lib/pds.ts +0 -440
- package/src/lib/permissions.ts +0 -28
- package/src/lib/schemas.ts +0 -97
- package/src/lib/tid.ts +0 -22
- package/src/lib/types.ts +0 -144
- package/tsconfig.json +0 -15
- package/vitest.config.ts +0 -7
package/src/commands/show.ts
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
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
|
-
}
|
package/src/commands/status.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
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
|
-
}
|
package/src/commands/use.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
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
|
-
}
|
package/src/commands/whoami.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { loadAuthInfo } from "../lib/config.js";
|
|
2
|
-
import chalk from "chalk";
|
|
3
|
-
|
|
4
|
-
export function whoamiCommand(opts: { json?: boolean }): void {
|
|
5
|
-
const info = loadAuthInfo();
|
|
6
|
-
if (!info) {
|
|
7
|
-
if (opts.json) {
|
|
8
|
-
console.log(JSON.stringify({ loggedIn: false }));
|
|
9
|
-
} else {
|
|
10
|
-
console.log("Not logged in. Run `sb login <handle>` first.");
|
|
11
|
-
}
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
if (opts.json) {
|
|
16
|
-
console.log(JSON.stringify({ loggedIn: true, ...info }, null, 2));
|
|
17
|
-
} else {
|
|
18
|
-
console.log(`${chalk.bold("Handle:")} ${info.handle}`);
|
|
19
|
-
console.log(`${chalk.bold("DID:")} ${info.did}`);
|
|
20
|
-
console.log(`${chalk.bold("PDS:")} ${info.service}`);
|
|
21
|
-
}
|
|
22
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Command } from "commander";
|
|
3
|
-
import { loginCommand } from "./commands/login.js";
|
|
4
|
-
import { logoutCommand } from "./commands/logout.js";
|
|
5
|
-
import { whoamiCommand } from "./commands/whoami.js";
|
|
6
|
-
import { statusCommand } from "./commands/status.js";
|
|
7
|
-
import { boardsCommand } from "./commands/boards.js";
|
|
8
|
-
import { useCommand } from "./commands/use.js";
|
|
9
|
-
import { addCommand } from "./commands/add.js";
|
|
10
|
-
import { colsCommand } from "./commands/cols.js";
|
|
11
|
-
import { cardsCommand } from "./commands/cards.js";
|
|
12
|
-
import { showCommand } from "./commands/show.js";
|
|
13
|
-
import { newCommand } from "./commands/new.js";
|
|
14
|
-
import { mvCommand } from "./commands/mv.js";
|
|
15
|
-
import { editCommand } from "./commands/edit.js";
|
|
16
|
-
import { commentCommand } from "./commands/comment.js";
|
|
17
|
-
import { rmCommand } from "./commands/rm.js";
|
|
18
|
-
|
|
19
|
-
const program = new Command();
|
|
20
|
-
|
|
21
|
-
program
|
|
22
|
-
.name("sb")
|
|
23
|
-
.description("Skyboard CLI — manage kanban boards on AT Protocol")
|
|
24
|
-
.version("0.1.0");
|
|
25
|
-
|
|
26
|
-
// Auth commands
|
|
27
|
-
program
|
|
28
|
-
.command("login")
|
|
29
|
-
.description("Log in via AT Protocol OAuth (opens browser)")
|
|
30
|
-
.argument("<handle>", "Your AT Protocol handle (e.g. alice.bsky.social)")
|
|
31
|
-
.action(loginCommand);
|
|
32
|
-
|
|
33
|
-
program
|
|
34
|
-
.command("logout")
|
|
35
|
-
.description("Log out and clear stored session")
|
|
36
|
-
.action(logoutCommand);
|
|
37
|
-
|
|
38
|
-
program
|
|
39
|
-
.command("whoami")
|
|
40
|
-
.description("Show current authenticated user")
|
|
41
|
-
.option("--json", "Output as JSON")
|
|
42
|
-
.action(whoamiCommand);
|
|
43
|
-
|
|
44
|
-
program
|
|
45
|
-
.command("status")
|
|
46
|
-
.description("Show current auth state and board summary")
|
|
47
|
-
.option("--json", "Output as JSON")
|
|
48
|
-
.action(statusCommand);
|
|
49
|
-
|
|
50
|
-
// Board navigation
|
|
51
|
-
program
|
|
52
|
-
.command("boards")
|
|
53
|
-
.description("List all boards (owned + joined)")
|
|
54
|
-
.option("--json", "Output as JSON")
|
|
55
|
-
.action(boardsCommand);
|
|
56
|
-
|
|
57
|
-
program
|
|
58
|
-
.command("use")
|
|
59
|
-
.description("Set default board for subsequent commands")
|
|
60
|
-
.argument("<board>", "Board name, rkey, AT URI, or web URL")
|
|
61
|
-
.action(useCommand);
|
|
62
|
-
|
|
63
|
-
program
|
|
64
|
-
.command("add")
|
|
65
|
-
.description("Join a board by AT URI or web URL")
|
|
66
|
-
.argument("<link>", "AT URI or web URL of the board")
|
|
67
|
-
.action(addCommand);
|
|
68
|
-
|
|
69
|
-
program
|
|
70
|
-
.command("cols")
|
|
71
|
-
.description("Show columns for the current board")
|
|
72
|
-
.option("--board <ref>", "Override default board")
|
|
73
|
-
.option("--json", "Output as JSON")
|
|
74
|
-
.action(colsCommand);
|
|
75
|
-
|
|
76
|
-
// Card operations
|
|
77
|
-
program
|
|
78
|
-
.command("cards")
|
|
79
|
-
.description("List cards grouped by column")
|
|
80
|
-
.option("-c, --column <column>", "Filter by column (name, prefix, or number)")
|
|
81
|
-
.option("-l, --label <label>", "Filter by label name")
|
|
82
|
-
.option("-s, --search <text>", "Search in title and description")
|
|
83
|
-
.option("--board <ref>", "Override default board")
|
|
84
|
-
.option("--json", "Output as JSON")
|
|
85
|
-
.action(cardsCommand);
|
|
86
|
-
|
|
87
|
-
program
|
|
88
|
-
.command("new")
|
|
89
|
-
.description("Create a new card")
|
|
90
|
-
.argument("<title>", "Card title")
|
|
91
|
-
.option("-c, --column <column>", "Target column (default: first column)")
|
|
92
|
-
.option("-d, --description <desc>", "Card description")
|
|
93
|
-
.option("--board <ref>", "Override default board")
|
|
94
|
-
.option("--json", "Output as JSON")
|
|
95
|
-
.action(newCommand);
|
|
96
|
-
|
|
97
|
-
program
|
|
98
|
-
.command("show")
|
|
99
|
-
.description("Show card details, comments, and history")
|
|
100
|
-
.argument("<ref>", "Card reference (rkey prefix, min 4 chars)")
|
|
101
|
-
.option("--board <ref>", "Override default board")
|
|
102
|
-
.option("--json", "Output as JSON")
|
|
103
|
-
.action(showCommand);
|
|
104
|
-
|
|
105
|
-
program
|
|
106
|
-
.command("mv")
|
|
107
|
-
.description("Move a card to a different column")
|
|
108
|
-
.argument("<ref>", "Card reference (rkey prefix)")
|
|
109
|
-
.argument("<column>", "Target column (name, prefix, or number)")
|
|
110
|
-
.option("--board <ref>", "Override default board")
|
|
111
|
-
.option("--json", "Output as JSON")
|
|
112
|
-
.action(mvCommand);
|
|
113
|
-
|
|
114
|
-
program
|
|
115
|
-
.command("edit")
|
|
116
|
-
.description("Edit card fields")
|
|
117
|
-
.argument("<ref>", "Card reference (rkey prefix)")
|
|
118
|
-
.option("-t, --title <title>", "New title")
|
|
119
|
-
.option("-d, --description <desc>", "New description")
|
|
120
|
-
.option("-l, --label <label...>", "Set labels (by name)")
|
|
121
|
-
.option("--board <ref>", "Override default board")
|
|
122
|
-
.option("--json", "Output as JSON")
|
|
123
|
-
.action(editCommand);
|
|
124
|
-
|
|
125
|
-
program
|
|
126
|
-
.command("comment")
|
|
127
|
-
.description("Add a comment to a card")
|
|
128
|
-
.argument("<ref>", "Card reference (rkey prefix)")
|
|
129
|
-
.argument("<text>", "Comment text")
|
|
130
|
-
.option("--board <ref>", "Override default board")
|
|
131
|
-
.option("--json", "Output as JSON")
|
|
132
|
-
.action(commentCommand);
|
|
133
|
-
|
|
134
|
-
program
|
|
135
|
-
.command("rm")
|
|
136
|
-
.description("Delete a card (owner only)")
|
|
137
|
-
.argument("<ref>", "Card reference (rkey prefix)")
|
|
138
|
-
.option("-f, --force", "Skip confirmation")
|
|
139
|
-
.option("--board <ref>", "Override default board")
|
|
140
|
-
.option("--json", "Output as JSON")
|
|
141
|
-
.action(rmCommand);
|
|
142
|
-
|
|
143
|
-
program.parse();
|
package/src/lib/auth.ts
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import { Agent } from "@atproto/api";
|
|
2
|
-
import { NodeOAuthClient } from "@atproto/oauth-client-node";
|
|
3
|
-
import type { NodeSavedSession, NodeSavedState } from "@atproto/oauth-client-node";
|
|
4
|
-
import { requestLocalLock } from "@atproto/oauth-client";
|
|
5
|
-
import { createServer } from "node:http";
|
|
6
|
-
import {
|
|
7
|
-
writeStateFile,
|
|
8
|
-
readStateFile,
|
|
9
|
-
deleteStateFile,
|
|
10
|
-
writeSessionFile,
|
|
11
|
-
readSessionFile,
|
|
12
|
-
deleteSessionFile,
|
|
13
|
-
loadAuthInfo,
|
|
14
|
-
saveAuthInfo,
|
|
15
|
-
clearAuthInfo,
|
|
16
|
-
} from "./config.js";
|
|
17
|
-
|
|
18
|
-
const OAUTH_SCOPE =
|
|
19
|
-
"atproto repo:dev.skyboard.board repo:dev.skyboard.task repo:dev.skyboard.op repo:dev.skyboard.trust repo:dev.skyboard.comment repo:dev.skyboard.approval repo:dev.skyboard.reaction";
|
|
20
|
-
|
|
21
|
-
// File-based stores implementing the NodeOAuthClient interfaces
|
|
22
|
-
const stateStore = {
|
|
23
|
-
async set(key: string, state: NodeSavedState): Promise<void> {
|
|
24
|
-
writeStateFile(key, state);
|
|
25
|
-
},
|
|
26
|
-
async get(key: string): Promise<NodeSavedState | undefined> {
|
|
27
|
-
return readStateFile(key) as NodeSavedState | undefined;
|
|
28
|
-
},
|
|
29
|
-
async del(key: string): Promise<void> {
|
|
30
|
-
deleteStateFile(key);
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const sessionStore = {
|
|
35
|
-
async set(sub: string, session: NodeSavedSession): Promise<void> {
|
|
36
|
-
writeSessionFile(sub, session);
|
|
37
|
-
},
|
|
38
|
-
async get(sub: string): Promise<NodeSavedSession | undefined> {
|
|
39
|
-
return readSessionFile(sub) as NodeSavedSession | undefined;
|
|
40
|
-
},
|
|
41
|
-
async del(sub: string): Promise<void> {
|
|
42
|
-
deleteSessionFile(sub);
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
function createOAuthClient(port: number): NodeOAuthClient {
|
|
47
|
-
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
48
|
-
return new NodeOAuthClient({
|
|
49
|
-
clientMetadata: {
|
|
50
|
-
client_id: `http://localhost?redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(OAUTH_SCOPE)}`,
|
|
51
|
-
client_name: "Skyboard CLI",
|
|
52
|
-
redirect_uris: [redirectUri],
|
|
53
|
-
scope: OAUTH_SCOPE,
|
|
54
|
-
grant_types: ["authorization_code", "refresh_token"],
|
|
55
|
-
response_types: ["code"],
|
|
56
|
-
token_endpoint_auth_method: "none",
|
|
57
|
-
application_type: "native",
|
|
58
|
-
dpop_bound_access_tokens: true,
|
|
59
|
-
},
|
|
60
|
-
stateStore,
|
|
61
|
-
sessionStore,
|
|
62
|
-
requestLock: requestLocalLock,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Find a free port by binding to port 0.
|
|
68
|
-
*/
|
|
69
|
-
async function findFreePort(): Promise<number> {
|
|
70
|
-
return new Promise((resolve, reject) => {
|
|
71
|
-
const srv = createServer();
|
|
72
|
-
srv.listen(0, "127.0.0.1", () => {
|
|
73
|
-
const addr = srv.address();
|
|
74
|
-
if (addr && typeof addr === "object") {
|
|
75
|
-
const port = addr.port;
|
|
76
|
-
srv.close(() => resolve(port));
|
|
77
|
-
} else {
|
|
78
|
-
srv.close(() => reject(new Error("Could not determine port")));
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
srv.on("error", reject);
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Perform OAuth loopback login. Opens browser for authorization,
|
|
87
|
-
* starts a local HTTP server for the callback.
|
|
88
|
-
* Returns the DID and handle of the authenticated user.
|
|
89
|
-
*/
|
|
90
|
-
export async function login(handle: string): Promise<{ did: string; handle: string }> {
|
|
91
|
-
const port = await findFreePort();
|
|
92
|
-
const client = createOAuthClient(port);
|
|
93
|
-
|
|
94
|
-
const authUrl = await client.authorize(handle, {
|
|
95
|
-
scope: OAUTH_SCOPE,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// Start callback server
|
|
99
|
-
return new Promise((resolve, reject) => {
|
|
100
|
-
const timeout = setTimeout(() => {
|
|
101
|
-
server.close();
|
|
102
|
-
reject(new Error("Login timed out after 120 seconds"));
|
|
103
|
-
}, 120_000);
|
|
104
|
-
|
|
105
|
-
const server = createServer(async (req, res) => {
|
|
106
|
-
if (!req.url?.startsWith("/callback")) {
|
|
107
|
-
res.writeHead(404);
|
|
108
|
-
res.end("Not found");
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
const url = new URL(req.url, `http://127.0.0.1:${port}`);
|
|
114
|
-
const { session } = await client.callback(url.searchParams);
|
|
115
|
-
|
|
116
|
-
const did = session.did;
|
|
117
|
-
// Resolve handle from DID
|
|
118
|
-
const agent = new Agent(session);
|
|
119
|
-
let resolvedHandle = handle;
|
|
120
|
-
try {
|
|
121
|
-
const profile = await agent.getProfile({ actor: did });
|
|
122
|
-
resolvedHandle = profile.data.handle;
|
|
123
|
-
} catch {
|
|
124
|
-
// Use the provided handle as fallback
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Find the PDS endpoint for this DID
|
|
128
|
-
let service = "https://bsky.social";
|
|
129
|
-
try {
|
|
130
|
-
const didDoc = await resolveDIDDocument(did);
|
|
131
|
-
if (didDoc) {
|
|
132
|
-
const services = (didDoc as Record<string, unknown>).service as
|
|
133
|
-
| Array<{ id: string; type: string; serviceEndpoint: string }>
|
|
134
|
-
| undefined;
|
|
135
|
-
const pds = services?.find(
|
|
136
|
-
(s) => s.id === "#atproto_pds" || s.type === "AtprotoPersonalDataServer",
|
|
137
|
-
);
|
|
138
|
-
if (pds?.serviceEndpoint) {
|
|
139
|
-
service = pds.serviceEndpoint;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
} catch {
|
|
143
|
-
// fallback to default
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
saveAuthInfo({ did, handle: resolvedHandle, service });
|
|
147
|
-
|
|
148
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
149
|
-
res.end(`
|
|
150
|
-
<html><body style="font-family: system-ui; text-align: center; padding: 40px;">
|
|
151
|
-
<h2>Logged in to Skyboard CLI</h2>
|
|
152
|
-
<p>You can close this tab and return to your terminal.</p>
|
|
153
|
-
</body></html>
|
|
154
|
-
`);
|
|
155
|
-
|
|
156
|
-
clearTimeout(timeout);
|
|
157
|
-
server.close();
|
|
158
|
-
resolve({ did, handle: resolvedHandle });
|
|
159
|
-
} catch (err) {
|
|
160
|
-
res.writeHead(500, { "Content-Type": "text/html" });
|
|
161
|
-
res.end(`
|
|
162
|
-
<html><body style="font-family: system-ui; text-align: center; padding: 40px;">
|
|
163
|
-
<h2>Login failed</h2>
|
|
164
|
-
<p>${err instanceof Error ? err.message : "Unknown error"}</p>
|
|
165
|
-
</body></html>
|
|
166
|
-
`);
|
|
167
|
-
clearTimeout(timeout);
|
|
168
|
-
server.close();
|
|
169
|
-
reject(err);
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
server.listen(port, "127.0.0.1", async () => {
|
|
174
|
-
// Open browser
|
|
175
|
-
try {
|
|
176
|
-
const open = (await import("open")).default;
|
|
177
|
-
await open(authUrl.toString());
|
|
178
|
-
console.log(`\nOpened browser for login. Waiting for authorization...`);
|
|
179
|
-
console.log(`If the browser didn't open, visit:\n${authUrl.toString()}\n`);
|
|
180
|
-
} catch {
|
|
181
|
-
console.log(`\nOpen this URL in your browser to log in:\n${authUrl.toString()}\n`);
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Get an authenticated Agent for the currently logged-in user.
|
|
189
|
-
* Restores the OAuth session and returns an Agent that auto-refreshes tokens.
|
|
190
|
-
*/
|
|
191
|
-
export async function getAgent(): Promise<{ agent: Agent; did: string; handle: string } | null> {
|
|
192
|
-
const authInfo = loadAuthInfo();
|
|
193
|
-
if (!authInfo) return null;
|
|
194
|
-
|
|
195
|
-
try {
|
|
196
|
-
// We need to create a client to restore the session.
|
|
197
|
-
// Since we don't know the original port, use a dummy port — session
|
|
198
|
-
// restoration doesn't need the redirect_uri to match.
|
|
199
|
-
const client = createOAuthClient(0);
|
|
200
|
-
const session = await client.restore(authInfo.did);
|
|
201
|
-
const agent = new Agent(session);
|
|
202
|
-
return { agent, did: authInfo.did, handle: authInfo.handle };
|
|
203
|
-
} catch {
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Require authentication — exit with error if not logged in.
|
|
210
|
-
*/
|
|
211
|
-
export async function requireAgent(): Promise<{ agent: Agent; did: string; handle: string }> {
|
|
212
|
-
const result = await getAgent();
|
|
213
|
-
if (!result) {
|
|
214
|
-
console.error("Not logged in. Run `sb login <handle>` first.");
|
|
215
|
-
process.exit(1);
|
|
216
|
-
}
|
|
217
|
-
return result;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export function logout(): void {
|
|
221
|
-
const authInfo = loadAuthInfo();
|
|
222
|
-
if (authInfo) {
|
|
223
|
-
deleteSessionFile(authInfo.did);
|
|
224
|
-
}
|
|
225
|
-
clearAuthInfo();
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
async function resolveDIDDocument(did: string): Promise<unknown | null> {
|
|
229
|
-
try {
|
|
230
|
-
if (did.startsWith("did:plc:")) {
|
|
231
|
-
const res = await fetch(`https://plc.directory/${did}`);
|
|
232
|
-
if (!res.ok) return null;
|
|
233
|
-
return await res.json();
|
|
234
|
-
} else if (did.startsWith("did:web:")) {
|
|
235
|
-
const host = did.slice("did:web:".length).replaceAll(":", "/");
|
|
236
|
-
const res = await fetch(`https://${host}/.well-known/did.json`);
|
|
237
|
-
if (!res.ok) return null;
|
|
238
|
-
return await res.json();
|
|
239
|
-
}
|
|
240
|
-
return null;
|
|
241
|
-
} catch {
|
|
242
|
-
return null;
|
|
243
|
-
}
|
|
244
|
-
}
|
package/src/lib/card-ref.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { MaterializedTask } from "./types.js";
|
|
2
|
-
|
|
3
|
-
const MIN_PREFIX_LEN = 4;
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Resolve a card reference (TID rkey prefix) to a unique task.
|
|
7
|
-
* Returns the matching task, or throws with a helpful message if
|
|
8
|
-
* ambiguous or not found.
|
|
9
|
-
*/
|
|
10
|
-
export function resolveCardRef(
|
|
11
|
-
ref: string,
|
|
12
|
-
tasks: MaterializedTask[],
|
|
13
|
-
): MaterializedTask {
|
|
14
|
-
if (ref.length < MIN_PREFIX_LEN) {
|
|
15
|
-
throw new Error(`Card reference too short (min ${MIN_PREFIX_LEN} chars): ${ref}`);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const matches = tasks.filter((t) => t.rkey.startsWith(ref));
|
|
19
|
-
|
|
20
|
-
if (matches.length === 0) {
|
|
21
|
-
throw new Error(`No card found matching "${ref}"`);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (matches.length > 1) {
|
|
25
|
-
const list = matches
|
|
26
|
-
.map((t) => ` ${t.rkey.slice(0, 7)} ${t.effectiveTitle}`)
|
|
27
|
-
.join("\n");
|
|
28
|
-
throw new Error(`Ambiguous reference "${ref}". Matches:\n${list}`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return matches[0];
|
|
32
|
-
}
|