schub 0.1.2 → 0.1.4
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/README.md +27 -0
- package/dist/index.js +12830 -3057
- package/package.json +5 -2
- package/skills/create-proposal/SKILL.md +5 -1
- package/skills/create-tasks/SKILL.md +5 -4
- package/skills/implement-task/SKILL.md +6 -1
- package/skills/review-proposal/SKILL.md +3 -2
- package/skills/update-roadmap/SKILL.md +23 -0
- package/src/changes.test.ts +166 -0
- package/src/changes.ts +159 -54
- package/src/commands/adr.test.ts +6 -5
- package/src/commands/changes.test.ts +136 -14
- package/src/commands/changes.ts +102 -1
- package/src/commands/cookbook.test.ts +6 -5
- package/src/commands/init.test.ts +69 -2
- package/src/commands/init.ts +48 -5
- package/src/commands/review.test.ts +7 -6
- package/src/commands/review.ts +1 -1
- package/src/commands/roadmap.test.ts +84 -0
- package/src/commands/roadmap.ts +84 -0
- package/src/commands/tasks-create.test.ts +22 -22
- package/src/commands/tasks-implement.test.ts +253 -0
- package/src/commands/tasks-implement.ts +121 -0
- package/src/commands/tasks-list.test.ts +27 -27
- package/src/commands/tasks-update.test.ts +92 -0
- package/src/commands/tasks.ts +98 -1
- package/src/features/roadmap/index.ts +230 -0
- package/src/features/roadmap/roadmap.test.ts +77 -0
- package/src/features/tasks/constants.ts +1 -0
- package/src/features/tasks/create.ts +10 -8
- package/src/features/tasks/filesystem.test.ts +285 -18
- package/src/features/tasks/filesystem.ts +152 -39
- package/src/features/tasks/graph.ts +18 -3
- package/src/features/tasks/index.ts +10 -1
- package/src/features/tasks/worktree.ts +48 -0
- package/src/frontmatter.ts +115 -0
- package/src/index.test.ts +42 -6
- package/src/index.ts +226 -109
- package/src/opencode.test.ts +53 -0
- package/src/opencode.ts +74 -0
- package/src/tasks.ts +2 -0
- package/src/tui/App.test.tsx +418 -0
- package/src/tui/App.tsx +343 -0
- package/src/tui/components/PlanView.test.tsx +101 -0
- package/src/tui/components/PlanView.tsx +89 -0
- package/src/tui/components/PreviewPage.test.tsx +69 -0
- package/src/tui/components/PreviewPage.tsx +87 -0
- package/src/tui/components/ProposalDetailView.test.tsx +169 -0
- package/src/tui/components/ProposalDetailView.tsx +166 -0
- package/src/tui/components/RoadmapView.test.tsx +85 -0
- package/src/tui/components/RoadmapView.tsx +369 -0
- package/src/tui/components/StatusView.test.tsx +1351 -0
- package/src/tui/components/StatusView.tsx +519 -0
- package/src/tui/components/markdown-renderer.test.ts +46 -0
- package/src/tui/components/markdown-renderer.ts +89 -0
- package/src/tui/components/status-view-data.ts +322 -0
- package/src/tui/components/status-view-render.tsx +329 -0
- package/src/tui/index.ts +16 -0
- package/templates/create-proposal/adr-template.md +6 -4
- package/templates/create-proposal/cookbook-template.md +5 -3
- package/templates/create-proposal/proposal-template.md +8 -6
- package/templates/create-roadmap/roadmap.md +5 -0
- package/templates/create-tasks/task-template.md +9 -4
- package/templates/review-proposal/q&a-template.md +8 -3
- package/templates/review-proposal/review-me-template.md +6 -4
- package/templates/setup-project/project-overview-template.md +5 -0
- package/templates/setup-project/project-setup-template.md +5 -0
- package/templates/setup-project/project-wow-template.md +5 -0
- package/src/App.test.tsx +0 -93
- package/src/App.tsx +0 -155
- package/src/components/PlanView.test.tsx +0 -113
- package/src/components/PlanView.tsx +0 -160
- package/src/components/StatusView.test.tsx +0 -380
- package/src/components/StatusView.tsx +0 -367
- package/src/ide.ts +0 -7
- package/templates/templates-parity.test.ts +0 -45
- /package/src/{clipboard.ts → tui/clipboard.ts} +0 -0
- /package/src/{components → tui/components}/statusColor.ts +0 -0
- /package/src/{terminal.test.ts → tui/terminal.test.ts} +0 -0
- /package/src/{terminal.ts → tui/terminal.ts} +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { resolveGitRoot } from "../../init";
|
|
5
|
+
|
|
6
|
+
type TaskWorktreeOptions = {
|
|
7
|
+
repoRoot: string;
|
|
8
|
+
taskId: string;
|
|
9
|
+
worktreeRoot?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const runGit = (gitRoot: string, args: string[]) => {
|
|
13
|
+
return spawnSync("git", args, {
|
|
14
|
+
cwd: gitRoot,
|
|
15
|
+
encoding: "utf8",
|
|
16
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const createTaskWorktree = ({ repoRoot, taskId, worktreeRoot }: TaskWorktreeOptions) => {
|
|
21
|
+
const gitRoot = resolveGitRoot(repoRoot);
|
|
22
|
+
if (!gitRoot) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const branchName = `task/${taskId}`;
|
|
27
|
+
const resolvedRoot = worktreeRoot ? resolve(gitRoot, worktreeRoot) : join(gitRoot, ".schub", "worktrees");
|
|
28
|
+
const worktreePath = join(resolvedRoot, taskId);
|
|
29
|
+
|
|
30
|
+
if (existsSync(worktreePath)) {
|
|
31
|
+
throw new Error(`Worktree path already exists: ${worktreePath}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const branchCheck = runGit(gitRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`]);
|
|
35
|
+
if (branchCheck.status === 0) {
|
|
36
|
+
throw new Error(`Branch ${branchName} already exists.`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
mkdirSync(resolvedRoot, { recursive: true });
|
|
40
|
+
|
|
41
|
+
const worktreeResult = runGit(gitRoot, ["worktree", "add", "-b", branchName, worktreePath]);
|
|
42
|
+
if (worktreeResult.status !== 0) {
|
|
43
|
+
const message = worktreeResult.stderr?.trim();
|
|
44
|
+
throw new Error(message || "Failed to create worktree.");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { worktreePath, branchName, gitRoot };
|
|
48
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export type FrontmatterValue = string | string[];
|
|
2
|
+
export type FrontmatterData = Record<string, FrontmatterValue>;
|
|
3
|
+
|
|
4
|
+
type FrontmatterReadResult = {
|
|
5
|
+
data: FrontmatterData;
|
|
6
|
+
body: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const FRONTMATTER_PATTERN = /^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/;
|
|
10
|
+
|
|
11
|
+
const unquote = (value: string) => {
|
|
12
|
+
const trimmed = value.trim();
|
|
13
|
+
if (trimmed.length < 2) {
|
|
14
|
+
return trimmed;
|
|
15
|
+
}
|
|
16
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
17
|
+
return trimmed.slice(1, -1);
|
|
18
|
+
}
|
|
19
|
+
return trimmed;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const parseInlineList = (value: string) => {
|
|
23
|
+
const trimmed = value.trim();
|
|
24
|
+
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
28
|
+
if (!inner) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
return inner
|
|
32
|
+
.split(",")
|
|
33
|
+
.map((item) => unquote(item))
|
|
34
|
+
.map((item) => item.trim())
|
|
35
|
+
.filter(Boolean);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const parseFrontmatterLines = (lines: string[]) => {
|
|
39
|
+
const data: FrontmatterData = {};
|
|
40
|
+
let activeKey: string | null = null;
|
|
41
|
+
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
const trimmed = line.trim();
|
|
44
|
+
if (!trimmed) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const keyMatch = trimmed.match(/^([a-z0-9_]+):\s*(.*)$/);
|
|
49
|
+
if (keyMatch) {
|
|
50
|
+
const key = keyMatch[1];
|
|
51
|
+
const rawValue = keyMatch[2].trim();
|
|
52
|
+
const inlineList = parseInlineList(rawValue);
|
|
53
|
+
if (inlineList) {
|
|
54
|
+
data[key] = inlineList;
|
|
55
|
+
activeKey = null;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!rawValue) {
|
|
59
|
+
data[key] = [];
|
|
60
|
+
activeKey = key;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (rawValue === "[]") {
|
|
64
|
+
data[key] = [];
|
|
65
|
+
activeKey = null;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
data[key] = unquote(rawValue);
|
|
69
|
+
activeKey = null;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const listMatch = trimmed.match(/^-+\s*(.+)$/);
|
|
74
|
+
if (listMatch && activeKey) {
|
|
75
|
+
const current = data[activeKey];
|
|
76
|
+
if (Array.isArray(current)) {
|
|
77
|
+
current.push(unquote(listMatch[1]).trim());
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return data;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const readFrontmatter = (content: string): FrontmatterReadResult => {
|
|
86
|
+
const match = content.match(FRONTMATTER_PATTERN);
|
|
87
|
+
if (!match) {
|
|
88
|
+
return { data: {}, body: content };
|
|
89
|
+
}
|
|
90
|
+
const lines = match[1].split(/\r?\n/);
|
|
91
|
+
const data = parseFrontmatterLines(lines);
|
|
92
|
+
const body = content.slice(match[0].length);
|
|
93
|
+
return { data, body };
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const updateFrontmatterValue = (content: string, key: string, value: string) => {
|
|
97
|
+
const match = content.match(FRONTMATTER_PATTERN);
|
|
98
|
+
if (!match) {
|
|
99
|
+
throw new Error("Frontmatter block not found.");
|
|
100
|
+
}
|
|
101
|
+
const lines = match[1].split(/\r?\n/);
|
|
102
|
+
let found = false;
|
|
103
|
+
const updatedLines = lines.map((line) => {
|
|
104
|
+
const keyMatch = line.match(/^([a-z0-9_]+):/);
|
|
105
|
+
if (keyMatch?.[1] === key) {
|
|
106
|
+
found = true;
|
|
107
|
+
return `${key}: ${value}`;
|
|
108
|
+
}
|
|
109
|
+
return line;
|
|
110
|
+
});
|
|
111
|
+
if (!found) {
|
|
112
|
+
throw new Error(`Frontmatter field '${key}' not found.`);
|
|
113
|
+
}
|
|
114
|
+
return `---\n${updatedLines.join("\n")}\n---\n${content.slice(match[0].length)}`;
|
|
115
|
+
};
|
package/src/index.test.ts
CHANGED
|
@@ -3,7 +3,9 @@ import { dirname, resolve } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { spawnSync } from "bun";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const packageData = await Bun.file(new URL("../package.json", import.meta.url)).json();
|
|
7
|
+
|
|
8
|
+
test("schub help lists top-level commands", () => {
|
|
7
9
|
const testDir = dirname(fileURLToPath(import.meta.url));
|
|
8
10
|
const cliDir = resolve(testDir, "..");
|
|
9
11
|
const result = spawnSync({
|
|
@@ -13,11 +15,45 @@ test("schub help lists tasks list", () => {
|
|
|
13
15
|
|
|
14
16
|
expect(result.exitCode).toBe(0);
|
|
15
17
|
const stdout = new TextDecoder().decode(result.stdout ?? new Uint8Array());
|
|
16
|
-
expect(stdout).toContain("changes
|
|
17
|
-
expect(stdout).toContain("
|
|
18
|
-
expect(stdout).toContain("
|
|
19
|
-
expect(stdout).toContain("
|
|
20
|
-
expect(stdout).toContain("
|
|
18
|
+
expect(stdout).toContain("schub changes");
|
|
19
|
+
expect(stdout).toContain("schub tasks");
|
|
20
|
+
expect(stdout).toContain("schub roadmap");
|
|
21
|
+
expect(stdout).toContain("schub review");
|
|
22
|
+
expect(stdout).toContain("schub init");
|
|
23
|
+
expect(stdout).toContain("schub eject");
|
|
24
|
+
expect(stdout).toContain("schub ui");
|
|
25
|
+
expect(stdout).toContain("--version");
|
|
21
26
|
expect(stdout).not.toContain("lint");
|
|
22
27
|
expect(stdout).not.toContain("format");
|
|
23
28
|
});
|
|
29
|
+
|
|
30
|
+
test("schub version prints package version", () => {
|
|
31
|
+
const testDir = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const cliDir = resolve(testDir, "..");
|
|
33
|
+
const result = spawnSync({
|
|
34
|
+
cmd: ["bun", "run", "schub", "--version"],
|
|
35
|
+
cwd: cliDir,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(result.exitCode).toBe(0);
|
|
39
|
+
const stdout = new TextDecoder().decode(result.stdout ?? new Uint8Array());
|
|
40
|
+
expect(stdout.trim()).toBe(packageData.version);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("schub changes help is scoped to changes commands", () => {
|
|
44
|
+
const testDir = dirname(fileURLToPath(import.meta.url));
|
|
45
|
+
const cliDir = resolve(testDir, "..");
|
|
46
|
+
const result = spawnSync({
|
|
47
|
+
cmd: ["bun", "run", "schub", "changes", "--help"],
|
|
48
|
+
cwd: cliDir,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result.exitCode).toBe(0);
|
|
52
|
+
const stdout = new TextDecoder().decode(result.stdout ?? new Uint8Array());
|
|
53
|
+
expect(stdout).toContain("create");
|
|
54
|
+
expect(stdout).toContain("status");
|
|
55
|
+
expect(stdout).toContain("archive");
|
|
56
|
+
expect(stdout).toContain("list");
|
|
57
|
+
expect(stdout).not.toContain("project create");
|
|
58
|
+
expect(stdout).not.toContain("tasks list");
|
|
59
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,131 +1,248 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import App from "./App";
|
|
2
|
+
import yargs from "yargs";
|
|
3
|
+
import { hideBin } from "yargs/helpers";
|
|
5
4
|
import { runAdrCreate } from "./commands/adr";
|
|
6
|
-
import { runChangesCreate, runChangesStatus } from "./commands/changes";
|
|
5
|
+
import { runChangesArchive, runChangesCreate, runChangesList, runChangesStatus } from "./commands/changes";
|
|
7
6
|
import { runCookbookCreate } from "./commands/cookbook";
|
|
8
7
|
import { runEject } from "./commands/eject";
|
|
9
8
|
import { runInit } from "./commands/init";
|
|
10
9
|
import { runProjectCreate } from "./commands/project";
|
|
11
10
|
import { runReviewComplete, runReviewCreate } from "./commands/review";
|
|
12
|
-
import {
|
|
11
|
+
import { runRoadmapAdd, runRoadmapList, runRoadmapPropose } from "./commands/roadmap";
|
|
12
|
+
import { runTasksCreate, runTasksImplement, runTasksList, runTasksUpdate } from "./commands/tasks";
|
|
13
13
|
import { findSchubRoot } from "./features/tasks";
|
|
14
|
-
import {
|
|
14
|
+
import { runTui } from "./tui";
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
Commands:
|
|
19
|
-
changes create Create a change proposal
|
|
20
|
-
changes status Update change proposal status
|
|
21
|
-
project create Create project docs
|
|
22
|
-
tasks create Create task files for a change
|
|
23
|
-
tasks list List tasks
|
|
24
|
-
review create Create REVIEW_ME for a change
|
|
25
|
-
review complete Create Q&A from REVIEW_ME
|
|
26
|
-
adr create Create an ADR for a change
|
|
27
|
-
cookbook create Create a cookbook for a change
|
|
28
|
-
init Initialize .schub and install Codex skills
|
|
29
|
-
eject Copy bundled skills and templates into .schub
|
|
30
|
-
ui Launch the interactive dashboard
|
|
31
|
-
`;
|
|
16
|
+
const packageData = await Bun.file(new URL("../package.json", import.meta.url)).json();
|
|
32
17
|
|
|
33
18
|
const getStartDir = () => process.env.SCHUB_CWD ?? process.cwd();
|
|
34
19
|
|
|
35
20
|
const resolveSchubDir = () => findSchubRoot(getStartDir());
|
|
36
21
|
|
|
37
|
-
const printHelp = (exitCode = 0) => {
|
|
38
|
-
process.stdout.write(`${HELP_TEXT}\n`);
|
|
39
|
-
process.exitCode = exitCode;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const registerTerminalReset = () => {
|
|
43
|
-
process.once("exit", () => {
|
|
44
|
-
applyTerminalReset();
|
|
45
|
-
});
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const runUi = () => {
|
|
49
|
-
applyTerminalPrelude();
|
|
50
|
-
registerTerminalReset();
|
|
51
|
-
render(React.createElement(App));
|
|
52
|
-
};
|
|
53
|
-
|
|
54
22
|
const runCommand = async () => {
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
62
|
-
printHelp();
|
|
23
|
+
const rawArgs = process.argv.slice(2);
|
|
24
|
+
if (rawArgs.length === 0) {
|
|
25
|
+
runTui();
|
|
63
26
|
return;
|
|
64
27
|
}
|
|
65
28
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
29
|
+
const startDir = getStartDir();
|
|
30
|
+
const cli = yargs(hideBin(process.argv))
|
|
31
|
+
.scriptName("schub")
|
|
32
|
+
.usage("schub [command]")
|
|
33
|
+
.command(
|
|
34
|
+
"changes",
|
|
35
|
+
"Change proposal commands",
|
|
36
|
+
(command) =>
|
|
37
|
+
command
|
|
38
|
+
.command(
|
|
39
|
+
"create [args..]",
|
|
40
|
+
"Create a change proposal",
|
|
41
|
+
() => {},
|
|
42
|
+
() => {
|
|
43
|
+
runChangesCreate(rawArgs.slice(2), startDir);
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
.command(
|
|
47
|
+
"status [args..]",
|
|
48
|
+
"Update change proposal status",
|
|
49
|
+
() => {},
|
|
50
|
+
() => {
|
|
51
|
+
runChangesStatus(rawArgs.slice(2), startDir);
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
.command(
|
|
55
|
+
"archive [args..]",
|
|
56
|
+
"Archive a change proposal",
|
|
57
|
+
() => {},
|
|
58
|
+
() => {
|
|
59
|
+
runChangesArchive(rawArgs.slice(2), startDir);
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
.command(
|
|
63
|
+
"list [args..]",
|
|
64
|
+
"List change proposals",
|
|
65
|
+
() => {},
|
|
66
|
+
() => {
|
|
67
|
+
runChangesList(rawArgs.slice(2), startDir);
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
.demandCommand(1, "Provide a changes command."),
|
|
71
|
+
() => {},
|
|
72
|
+
)
|
|
73
|
+
.command(
|
|
74
|
+
"project",
|
|
75
|
+
"Project documentation commands",
|
|
76
|
+
(command) =>
|
|
77
|
+
command
|
|
78
|
+
.command(
|
|
79
|
+
"create [args..]",
|
|
80
|
+
"Create project docs",
|
|
81
|
+
() => {},
|
|
82
|
+
() => {
|
|
83
|
+
runProjectCreate(rawArgs.slice(2), startDir);
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
.demandCommand(1, "Provide a project command."),
|
|
87
|
+
() => {},
|
|
88
|
+
)
|
|
89
|
+
.command(
|
|
90
|
+
"tasks",
|
|
91
|
+
"Task commands",
|
|
92
|
+
(command) =>
|
|
93
|
+
command
|
|
94
|
+
.command(
|
|
95
|
+
"create [args..]",
|
|
96
|
+
"Create task files for a change",
|
|
97
|
+
() => {},
|
|
98
|
+
() => {
|
|
99
|
+
runTasksCreate(rawArgs.slice(2), startDir);
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
.command(
|
|
103
|
+
"list [args..]",
|
|
104
|
+
"List tasks",
|
|
105
|
+
() => {},
|
|
106
|
+
() => {
|
|
107
|
+
runTasksList(resolveSchubDir(), rawArgs.slice(2));
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
.command(
|
|
111
|
+
"update [args..]",
|
|
112
|
+
"Update backlog task status",
|
|
113
|
+
() => {},
|
|
114
|
+
() => {
|
|
115
|
+
runTasksUpdate(resolveSchubDir(), rawArgs.slice(2));
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
.command(
|
|
119
|
+
"implement [args..]",
|
|
120
|
+
"Assign a task for implementation",
|
|
121
|
+
() => {},
|
|
122
|
+
() => {
|
|
123
|
+
runTasksImplement(resolveSchubDir(), rawArgs.slice(2));
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
.demandCommand(1, "Provide a tasks command."),
|
|
127
|
+
() => {},
|
|
128
|
+
)
|
|
129
|
+
.command(
|
|
130
|
+
"roadmap",
|
|
131
|
+
"Roadmap commands",
|
|
132
|
+
(command) =>
|
|
133
|
+
command
|
|
134
|
+
.command(
|
|
135
|
+
"add [args..]",
|
|
136
|
+
"Add a roadmap item",
|
|
137
|
+
() => {},
|
|
138
|
+
() => {
|
|
139
|
+
runRoadmapAdd(startDir, rawArgs.slice(2));
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
.command(
|
|
143
|
+
"list [args..]",
|
|
144
|
+
"List roadmap items",
|
|
145
|
+
() => {},
|
|
146
|
+
() => {
|
|
147
|
+
runRoadmapList(startDir, rawArgs.slice(2));
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
.command(
|
|
151
|
+
"propose [args..]",
|
|
152
|
+
"Create proposal from a roadmap item",
|
|
153
|
+
() => {},
|
|
154
|
+
() => {
|
|
155
|
+
runRoadmapPropose(startDir, rawArgs.slice(2));
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
.demandCommand(1, "Provide a roadmap command."),
|
|
159
|
+
() => {},
|
|
160
|
+
)
|
|
161
|
+
.command(
|
|
162
|
+
"review",
|
|
163
|
+
"Review commands",
|
|
164
|
+
(command) =>
|
|
165
|
+
command
|
|
166
|
+
.command(
|
|
167
|
+
"create [args..]",
|
|
168
|
+
"Create REVIEW_ME for a change",
|
|
169
|
+
() => {},
|
|
170
|
+
() => {
|
|
171
|
+
runReviewCreate(rawArgs.slice(2), startDir);
|
|
172
|
+
},
|
|
173
|
+
)
|
|
174
|
+
.command(
|
|
175
|
+
"complete [args..]",
|
|
176
|
+
"Create Q&A from REVIEW_ME",
|
|
177
|
+
() => {},
|
|
178
|
+
() => {
|
|
179
|
+
runReviewComplete(rawArgs.slice(2), startDir);
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
.demandCommand(1, "Provide a review command."),
|
|
183
|
+
() => {},
|
|
184
|
+
)
|
|
185
|
+
.command(
|
|
186
|
+
"adr",
|
|
187
|
+
"ADR commands",
|
|
188
|
+
(command) =>
|
|
189
|
+
command
|
|
190
|
+
.command(
|
|
191
|
+
"create [args..]",
|
|
192
|
+
"Create an ADR for a change",
|
|
193
|
+
() => {},
|
|
194
|
+
() => {
|
|
195
|
+
runAdrCreate(rawArgs.slice(2), startDir);
|
|
196
|
+
},
|
|
197
|
+
)
|
|
198
|
+
.demandCommand(1, "Provide an adr command."),
|
|
199
|
+
() => {},
|
|
200
|
+
)
|
|
201
|
+
.command(
|
|
202
|
+
"cookbook",
|
|
203
|
+
"Cookbook commands",
|
|
204
|
+
(command) =>
|
|
205
|
+
command
|
|
206
|
+
.command(
|
|
207
|
+
"create [args..]",
|
|
208
|
+
"Create a cookbook for a change",
|
|
209
|
+
() => {},
|
|
210
|
+
() => {
|
|
211
|
+
runCookbookCreate(rawArgs.slice(2), startDir);
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
.demandCommand(1, "Provide a cookbook command."),
|
|
215
|
+
() => {},
|
|
216
|
+
)
|
|
217
|
+
.command(
|
|
218
|
+
"init [args..]",
|
|
219
|
+
"Initialize .schub and install Codex skills",
|
|
220
|
+
() => {},
|
|
221
|
+
async () => {
|
|
222
|
+
await runInit(rawArgs.slice(1), startDir);
|
|
223
|
+
},
|
|
224
|
+
)
|
|
225
|
+
.command(
|
|
226
|
+
"eject [args..]",
|
|
227
|
+
"Copy bundled skills and templates into .schub",
|
|
228
|
+
() => {},
|
|
229
|
+
() => {
|
|
230
|
+
runEject(rawArgs.slice(1), startDir);
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
.command(
|
|
234
|
+
"ui",
|
|
235
|
+
"Launch the interactive dashboard",
|
|
236
|
+
() => {},
|
|
237
|
+
() => {
|
|
238
|
+
runTui();
|
|
239
|
+
},
|
|
240
|
+
)
|
|
241
|
+
.help()
|
|
242
|
+
.version(packageData.version)
|
|
243
|
+
.strictCommands();
|
|
127
244
|
|
|
128
|
-
|
|
245
|
+
await cli.parseAsync();
|
|
129
246
|
};
|
|
130
247
|
|
|
131
248
|
runCommand().catch((error) => {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { buildImplementCommand, buildReviewCommand, launchOpencodeImplement } from "./opencode";
|
|
3
|
+
|
|
4
|
+
type SpawnCall = {
|
|
5
|
+
command: string;
|
|
6
|
+
args: string[];
|
|
7
|
+
options: { stdio: "ignore"; detached: true; env?: NodeJS.ProcessEnv; cwd?: string };
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const repoRoot = "/repo/schub";
|
|
11
|
+
|
|
12
|
+
const createSpawnRecorder = () => {
|
|
13
|
+
const calls: SpawnCall[] = [];
|
|
14
|
+
|
|
15
|
+
const spawner = (
|
|
16
|
+
command: string,
|
|
17
|
+
args: readonly string[],
|
|
18
|
+
options: { stdio: "ignore"; detached: true; env?: NodeJS.ProcessEnv; cwd?: string },
|
|
19
|
+
) => {
|
|
20
|
+
calls.push({ command, args: [...args], options });
|
|
21
|
+
return { unref: () => {} };
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return { calls, spawner };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
test("buildReviewCommand uses opencode run review with title", () => {
|
|
28
|
+
expect(buildReviewCommand("C0001", repoRoot)).toEqual({
|
|
29
|
+
command: "opencode",
|
|
30
|
+
args: ["run", "review", "C0001", "--title", "(schub) Review C0001"],
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("buildImplementCommand uses opencode run implement with title", () => {
|
|
35
|
+
expect(buildImplementCommand("T0001", repoRoot, "Task Title")).toEqual({
|
|
36
|
+
command: "opencode",
|
|
37
|
+
args: ["run", "implement", "T0001", "--title", "(schub) Implement T0001 Task Title"],
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("launchOpencodeImplement spawns implement command", () => {
|
|
42
|
+
const { calls, spawner } = createSpawnRecorder();
|
|
43
|
+
|
|
44
|
+
launchOpencodeImplement("T0001", repoRoot, "Task Title", undefined, spawner);
|
|
45
|
+
|
|
46
|
+
expect(calls).toEqual([
|
|
47
|
+
{
|
|
48
|
+
command: "opencode",
|
|
49
|
+
args: ["run", "implement", "T0001", "--title", "(schub) Implement T0001 Task Title"],
|
|
50
|
+
options: { stdio: "ignore", detached: true },
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
});
|