sfora-cli 0.8.0 → 0.9.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/dist/cli.js +18 -8
- package/dist/format/cardMarkdown.d.ts +2 -0
- package/dist/format/cardMarkdown.js +2 -0
- package/dist/local/workspace.d.ts +12 -0
- package/dist/local/workspace.js +100 -7
- package/dist/mcp-server.js +7 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import { spawn } from "node:child_process";
|
|
|
11
11
|
import { readFile as readLocalFile } from "node:fs/promises";
|
|
12
12
|
import { basename } from "node:path";
|
|
13
13
|
import { createSforaShell, createLocalShell, SforaApiClient } from "./index.js";
|
|
14
|
-
import { LocalWorkspace, initWorkspace, findWorkspace, } from "./local/workspace.js";
|
|
14
|
+
import { LocalWorkspace, initWorkspace, findWorkspace, migrateWorkspaceStages, } from "./local/workspace.js";
|
|
15
15
|
import { runMcpServer } from "./mcp-server.js";
|
|
16
16
|
import { readConfig, writeConfig, resolveSettings, upsertProfile, effectiveProfiles, DEFAULT_URL, } from "./config.js";
|
|
17
17
|
const colors = {
|
|
@@ -544,7 +544,8 @@ async function runVerb(args, fs, client) {
|
|
|
544
544
|
.catch(() => []);
|
|
545
545
|
if (cols.length === 0)
|
|
546
546
|
throw new Error(`no board columns in ${project}`);
|
|
547
|
-
|
|
547
|
+
// Default to "To do" (02-todo) — new work is up for grabs, not in triage.
|
|
548
|
+
let col = cols.find((c) => c.replace(/^\d+-/, "") === "todo") ?? cols[0];
|
|
548
549
|
if (args.column) {
|
|
549
550
|
const want = args.column
|
|
550
551
|
.toLowerCase()
|
|
@@ -560,6 +561,12 @@ async function runVerb(args, fs, client) {
|
|
|
560
561
|
// ─── Local mode (a .sfora/ directory — no server, no account) ─────
|
|
561
562
|
const LOCAL_ONLY_HINT = "cloud command — run it with --cloud (after `sfora login`), or outside the .sfora/ repo";
|
|
562
563
|
async function runLocalVerb(args, root) {
|
|
564
|
+
// One Flow: silently reshape a legacy board onto the four fixed stage dirs on
|
|
565
|
+
// open (idempotent — a no-op once the board is canonical).
|
|
566
|
+
const reshaped = await migrateWorkspaceStages(root);
|
|
567
|
+
if (reshaped.migrated) {
|
|
568
|
+
console.error(`${colors.dim}· reshaped board to the four stages (triage · to do · in progress · done); moved ${reshaped.moved} task${reshaped.moved === 1 ? "" : "s"}${colors.reset}`);
|
|
569
|
+
}
|
|
563
570
|
const ws = new LocalWorkspace(root);
|
|
564
571
|
const { fs } = createLocalShell(root);
|
|
565
572
|
const ok = (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`);
|
|
@@ -644,15 +651,18 @@ Standard tools work on the real files:
|
|
|
644
651
|
ls cat grep find head tail wc sed awk echo mv cd pwd
|
|
645
652
|
|
|
646
653
|
${colors.dim}Where things live${colors.reset}
|
|
647
|
-
/board/<
|
|
648
|
-
|
|
649
|
-
|
|
654
|
+
/board/<NN-stage>/NNNN-<slug>.md tasks — four fixed columns: 01-triage /
|
|
655
|
+
02-todo / 03-in-progress / 04-done. mv
|
|
656
|
+
between them moves a task; mv into 04-done
|
|
657
|
+
marks it done.
|
|
658
|
+
/posts/YYYY-MM-DD-<slug>.md posts
|
|
659
|
+
/docs/<slug>.md docs
|
|
650
660
|
|
|
651
661
|
${colors.dim}Try${colors.reset}
|
|
652
|
-
ls /board/
|
|
662
|
+
ls /board/02-todo
|
|
653
663
|
grep -ri todo /board
|
|
654
|
-
echo "# Fix login" > /board/
|
|
655
|
-
mv /board/
|
|
664
|
+
echo "# Fix login" > /board/02-todo/fix-login.md
|
|
665
|
+
mv /board/02-todo/0003-*.md /board/04-done/
|
|
656
666
|
|
|
657
667
|
Everything is git-versioned with your repo. ${colors.dim}Connect a team later with${colors.reset} ${colors.cyan}sfora login${colors.reset}.
|
|
658
668
|
Type ${colors.cyan}exit${colors.reset} to quit.
|
|
@@ -65,7 +65,9 @@ export function cardToMarkdown(card, board, column, commentsCount) {
|
|
|
65
65
|
["boardId", board?._id ?? ""],
|
|
66
66
|
["column", column?.name ?? ""],
|
|
67
67
|
["columnId", column?._id ?? ""],
|
|
68
|
+
["kind", card.kind], // undefined for ordinary tasks → skipped
|
|
68
69
|
["status", card.status],
|
|
70
|
+
["resolution", card.resolution], // set once a question is decided
|
|
69
71
|
["priority", card.priority ?? "none"],
|
|
70
72
|
["assignees", card.assignees ?? []],
|
|
71
73
|
["labels", card.labels ?? []],
|
|
@@ -16,6 +16,18 @@
|
|
|
16
16
|
*/
|
|
17
17
|
/** Directory name that marks a local sfora workspace. */
|
|
18
18
|
export declare const WORKSPACE_DIR = ".sfora";
|
|
19
|
+
/**
|
|
20
|
+
* Migrate an existing workspace's board onto the four fixed stage dirs (One
|
|
21
|
+
* Flow). Idempotent: a board already in the canonical shape is left untouched.
|
|
22
|
+
* Legacy columns are mapped by name (done→done, in progress→doing, triage/
|
|
23
|
+
* undecided/…→triage, else todo); each card file MOVES into its stage dir, and
|
|
24
|
+
* a card leaving an unrecognized column keeps that column's name as a label so
|
|
25
|
+
* nothing is lost. Called on open so old \`.sfora/\` dirs reshape silently.
|
|
26
|
+
*/
|
|
27
|
+
export declare function migrateWorkspaceStages(root: string): Promise<{
|
|
28
|
+
migrated: boolean;
|
|
29
|
+
moved: number;
|
|
30
|
+
}>;
|
|
19
31
|
/**
|
|
20
32
|
* Walk up from `cwd` looking for a `.sfora/` workspace. A directory only
|
|
21
33
|
* counts when it has workspace markers (board/posts/docs) — `~/.sfora` is also
|
package/dist/local/workspace.js
CHANGED
|
@@ -19,20 +19,111 @@ import { join, dirname, resolve } from "node:path";
|
|
|
19
19
|
import { parseMarkdownCard, cardFilename, numberFromFilename, columnSlugFromDirname, parseMarkdownPost, parseMarkdownNote, noteFilename, slugify, } from "../format/index.js";
|
|
20
20
|
/** Directory name that marks a local sfora workspace. */
|
|
21
21
|
export const WORKSPACE_DIR = ".sfora";
|
|
22
|
-
|
|
22
|
+
// One Flow: every board is the same four fixed stage columns, matching the cloud
|
|
23
|
+
// byte-for-byte, so `cp` stays a migration. There is no column management.
|
|
24
|
+
const DEFAULT_COLUMNS = [
|
|
25
|
+
"01-triage",
|
|
26
|
+
"02-todo",
|
|
27
|
+
"03-in-progress",
|
|
28
|
+
"04-done",
|
|
29
|
+
];
|
|
30
|
+
// Map a legacy column slug → its stage dir. Mirrors the server's migrateStages
|
|
31
|
+
// name-mapping; anything unrecognized falls to "02-todo" (with the old name kept
|
|
32
|
+
// as a label so the lane isn't lost).
|
|
33
|
+
const STAGE_DIR_BY_SLUG = {
|
|
34
|
+
triage: "01-triage",
|
|
35
|
+
undecided: "01-triage",
|
|
36
|
+
someday: "01-triage",
|
|
37
|
+
icebox: "01-triage",
|
|
38
|
+
todo: "02-todo",
|
|
39
|
+
"to-do": "02-todo",
|
|
40
|
+
"in-progress": "03-in-progress",
|
|
41
|
+
doing: "03-in-progress",
|
|
42
|
+
wip: "03-in-progress",
|
|
43
|
+
working: "03-in-progress",
|
|
44
|
+
done: "04-done",
|
|
45
|
+
complete: "04-done",
|
|
46
|
+
completed: "04-done",
|
|
47
|
+
shipped: "04-done",
|
|
48
|
+
};
|
|
23
49
|
const WORKSPACE_README = `# sfora workspace
|
|
24
50
|
|
|
25
51
|
Everything here is a plain markdown file — edit with any tool, version with git.
|
|
26
52
|
|
|
27
|
-
- \`board/<column>/NNNN-<slug>.md\` — tasks.
|
|
28
|
-
|
|
29
|
-
|
|
53
|
+
- \`board/<column>/NNNN-<slug>.md\` — tasks. Every board is the same four fixed
|
|
54
|
+
columns — \`01-triage / 02-todo / 03-in-progress / 04-done\`. Move a task by
|
|
55
|
+
moving the file between column dirs (\`mv\` works); moving into \`04-done\`
|
|
56
|
+
marks it done. Frontmatter: \`status\`, \`priority\`, \`labels\`, \`assignees\`, \`due\`.
|
|
30
57
|
- \`posts/YYYY-MM-DD-<slug>.md\` — posts. An H1 (\`# Title\`) is the title.
|
|
31
58
|
- \`docs/<slug>.md\` — docs.
|
|
32
59
|
|
|
33
60
|
Create files by hand, or use the CLI: \`sfora task plan.md\`, \`sfora post note.md\`.
|
|
34
61
|
Same format as sfora cloud — \`sfora login\` connects this workspace to a team.
|
|
35
62
|
`;
|
|
63
|
+
// Surgically add a label to a card's YAML frontmatter without reformatting the
|
|
64
|
+
// rest of the file (local files are hand-edited; we don't round-trip them).
|
|
65
|
+
function addLabelToCard(md, label) {
|
|
66
|
+
const fm = /^---\n([\s\S]*?)\n---\n?/.exec(md);
|
|
67
|
+
if (!fm)
|
|
68
|
+
return `---\nlabels: [${label}]\n---\n\n${md}`;
|
|
69
|
+
const block = fm[1];
|
|
70
|
+
const labelLine = /^labels:\s*\[(.*)\]\s*$/m.exec(block);
|
|
71
|
+
if (labelLine) {
|
|
72
|
+
const items = labelLine[1]
|
|
73
|
+
.split(",")
|
|
74
|
+
.map((s) => s.trim())
|
|
75
|
+
.filter(Boolean);
|
|
76
|
+
if (items.includes(label))
|
|
77
|
+
return md;
|
|
78
|
+
const replaced = block.replace(labelLine[0], `labels: [${[...items, label].join(", ")}]`);
|
|
79
|
+
return md.replace(block, replaced);
|
|
80
|
+
}
|
|
81
|
+
return md.replace(block, `${block}\nlabels: [${label}]`);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Migrate an existing workspace's board onto the four fixed stage dirs (One
|
|
85
|
+
* Flow). Idempotent: a board already in the canonical shape is left untouched.
|
|
86
|
+
* Legacy columns are mapped by name (done→done, in progress→doing, triage/
|
|
87
|
+
* undecided/…→triage, else todo); each card file MOVES into its stage dir, and
|
|
88
|
+
* a card leaving an unrecognized column keeps that column's name as a label so
|
|
89
|
+
* nothing is lost. Called on open so old \`.sfora/\` dirs reshape silently.
|
|
90
|
+
*/
|
|
91
|
+
export async function migrateWorkspaceStages(root) {
|
|
92
|
+
const boardDir = join(root, "board");
|
|
93
|
+
const entries = await readdir(boardDir, { withFileTypes: true }).catch(() => []);
|
|
94
|
+
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
95
|
+
if (dirs.length === 0)
|
|
96
|
+
return { migrated: false, moved: 0 };
|
|
97
|
+
const isCanonical = dirs.length === DEFAULT_COLUMNS.length &&
|
|
98
|
+
DEFAULT_COLUMNS.every((d) => dirs.includes(d));
|
|
99
|
+
if (isCanonical)
|
|
100
|
+
return { migrated: false, moved: 0 };
|
|
101
|
+
for (const d of DEFAULT_COLUMNS) {
|
|
102
|
+
await mkdir(join(boardDir, d), { recursive: true });
|
|
103
|
+
}
|
|
104
|
+
let moved = 0;
|
|
105
|
+
for (const oldDir of dirs) {
|
|
106
|
+
if (DEFAULT_COLUMNS.includes(oldDir))
|
|
107
|
+
continue; // canonical dir stays put
|
|
108
|
+
const slug = columnSlugFromDirname(oldDir);
|
|
109
|
+
const canonical = STAGE_DIR_BY_SLUG[slug];
|
|
110
|
+
const target = canonical ?? "02-todo";
|
|
111
|
+
const files = await readdir(join(boardDir, oldDir)).catch(() => []);
|
|
112
|
+
for (const f of files) {
|
|
113
|
+
if (!f.endsWith(".md"))
|
|
114
|
+
continue;
|
|
115
|
+
const src = join(boardDir, oldDir, f);
|
|
116
|
+
let md = await readFile(src, "utf8");
|
|
117
|
+
if (!canonical)
|
|
118
|
+
md = addLabelToCard(md, slug); // preserve the old lane
|
|
119
|
+
await writeFile(join(boardDir, target, f), md, "utf8");
|
|
120
|
+
await rm(src);
|
|
121
|
+
moved++;
|
|
122
|
+
}
|
|
123
|
+
await rm(join(boardDir, oldDir), { recursive: true, force: true }).catch(() => { });
|
|
124
|
+
}
|
|
125
|
+
return { migrated: true, moved };
|
|
126
|
+
}
|
|
36
127
|
/**
|
|
37
128
|
* Walk up from `cwd` looking for a `.sfora/` workspace. A directory only
|
|
38
129
|
* counts when it has workspace markers (board/posts/docs) — `~/.sfora` is also
|
|
@@ -194,8 +285,8 @@ export class LocalWorkspace {
|
|
|
194
285
|
// ─── Internals ───────────────────────────────────────────────────
|
|
195
286
|
/**
|
|
196
287
|
* Resolve a column reference (slug or name, e.g. "todo" / "In progress") to
|
|
197
|
-
* an existing column dirname. With no reference: a closed status prefers
|
|
198
|
-
* done
|
|
288
|
+
* an existing column dirname. With no reference: a closed status prefers the
|
|
289
|
+
* done column, otherwise "To do" (new work is up for grabs, not in triage).
|
|
199
290
|
*/
|
|
200
291
|
async #resolveColumn(ref, status) {
|
|
201
292
|
const columns = await this.listColumns();
|
|
@@ -217,7 +308,9 @@ export class LocalWorkspace {
|
|
|
217
308
|
if (done)
|
|
218
309
|
return done;
|
|
219
310
|
}
|
|
220
|
-
|
|
311
|
+
// Default target is "To do" (02-todo), not the leading triage column.
|
|
312
|
+
const todo = columns.find((c) => columnSlugFromDirname(c) === "todo");
|
|
313
|
+
return todo ?? columns[0];
|
|
221
314
|
}
|
|
222
315
|
async #nextNumber() {
|
|
223
316
|
let max = 0;
|
package/dist/mcp-server.js
CHANGED
|
@@ -13,18 +13,21 @@ import { createSforaShell, createLocalShell } from "./index.js";
|
|
|
13
13
|
const TOOL_DESCRIPTION = `Run a bash command against the sfora workspace — a Unix-style view where every post, task, and doc is a markdown file:
|
|
14
14
|
- /projects/<slug>/posts/<file>.md published posts
|
|
15
15
|
- /projects/<slug>/drafts/<file>.md your drafts
|
|
16
|
-
- /projects/<slug>/board/<NN-
|
|
16
|
+
- /projects/<slug>/board/<NN-stage>/<NNNN>.md tasks (kanban cards), by stage
|
|
17
17
|
- /projects/<slug>/docs/<file>.md docs / notes
|
|
18
18
|
- /projects/<slug>/pulls/<number>.md pull requests (diff + linked work), read-only
|
|
19
|
+
- /projects/<slug>/plan.md the goal + open questions
|
|
19
20
|
- /inbox/mentions.md unread mentions
|
|
20
21
|
- /me/api-key your identity
|
|
21
|
-
|
|
22
|
+
Every board is the same four fixed columns — 01-triage / 02-todo / 03-in-progress / 04-done. There is no column management; move a card into 04-done to close it. Question cards (kind: question) map their stage to the plan: triage = fuzzy, todo = up for grabs, in-progress = claimed, done = decided.
|
|
23
|
+
Examples: 'ls /projects', 'cat /projects/web/board/02-todo/*.md', 'grep -ri TODO /projects', 'echo "# Fix login\\nstatus: active" > /projects/web/board/02-todo/fix.md', 'mv /projects/web/board/02-todo/0003-*.md /projects/web/board/04-done/'.
|
|
22
24
|
Write a file to create or update the entity (frontmatter sets fields like status/priority/assignees/due). cwd and environment persist across calls.`;
|
|
23
25
|
const LOCAL_TOOL_DESCRIPTION = `Run a bash command against the local sfora workspace (a .sfora/ directory of plain markdown files, git-versioned with the repo):
|
|
24
|
-
- /board/<NN-
|
|
26
|
+
- /board/<NN-stage>/<NNNN>-<slug>.md tasks (kanban cards), by stage — 'mv' between stage dirs moves a task
|
|
25
27
|
- /posts/<YYYY-MM-DD>-<slug>.md posts
|
|
26
28
|
- /docs/<slug>.md docs / notes
|
|
27
|
-
|
|
29
|
+
Every board is the same four fixed columns — 01-triage / 02-todo / 03-in-progress / 04-done. There is no column management; moving a card into 04-done marks it done.
|
|
30
|
+
Examples: 'ls /board/02-todo', 'cat /board/02-todo/*.md', 'grep -ri TODO /', 'echo "# Fix login\\nstatus: active" > /board/02-todo/fix-login.md', 'mv /board/02-todo/0003-*.md /board/04-done/'.
|
|
28
31
|
Frontmatter sets task fields (status/priority/labels/assignees/due). cwd and environment persist across calls.`;
|
|
29
32
|
export async function runMcpServer(options) {
|
|
30
33
|
const { bash } = options.localRoot
|
package/package.json
CHANGED