backlog.md 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/bin/backlog-darwin-arm64/backlog +0 -0
- package/bin/backlog-darwin-x64/backlog +0 -0
- package/bin/backlog-linux-arm64/backlog +0 -0
- package/{cli → bin/backlog-linux-x64}/backlog +0 -0
- package/bin/backlog-win32-x64/backlog.exe +0 -0
- package/cli.js +62 -0
- package/package.json +57 -46
- package/.backlog/archive/drafts/readme.md +0 -3
- package/.backlog/archive/drafts/task-41 - temporary-test-task.md +0 -13
- package/.backlog/archive/readme.md +0 -6
- package/.backlog/archive/tasks/readme.md +0 -3
- package/.backlog/archive/tasks/task-41 - cli-migrate-terminal-ui-to-bblessed.md +0 -14
- package/.backlog/config.yml +0 -7
- package/.backlog/decisions/readme.md +0 -7
- package/.backlog/docs/readme.md +0 -20
- package/.backlog/drafts/readme.md +0 -3
- package/.backlog/drafts/task-26 - docs-add-board-export-step-to-agent-dod.md +0 -21
- package/.backlog/drafts/task-28 - add-code-of-conduct.md +0 -20
- package/.backlog/drafts/task-30 - create-changelog.md +0 -19
- package/.backlog/milestones/m-0 - project-setup.md +0 -8
- package/.backlog/milestones/m-1 - cli.md +0 -8
- package/.backlog/milestones/m-2 - cli-kanban.md +0 -8
- package/.backlog/milestones/m-3 - gui.md +0 -8
- package/.backlog/milestones/m-4 - gui-kanban.md +0 -8
- package/.backlog/milestones/m-5 - gui-advanced.md +0 -12
- package/.backlog/milestones/readme.md +0 -3
- package/.backlog/readme.md +0 -5
- package/.backlog/tasks/readme.md +0 -37
- package/.backlog/tasks/task-1 - cli-setup-core-project.md +0 -23
- package/.backlog/tasks/task-10 - gui-init-packaging.md +0 -23
- package/.backlog/tasks/task-11 - gui-kanban-board.md +0 -26
- package/.backlog/tasks/task-12 - gui-advanced.md +0 -25
- package/.backlog/tasks/task-13 - cli-add-agent-instruction-prompt.md +0 -53
- package/.backlog/tasks/task-13.1 - cli-agent-instruction-file-selection.md +0 -40
- package/.backlog/tasks/task-14 - gui-introduction-screens.md +0 -21
- package/.backlog/tasks/task-15 - improve-tasks-readme-with-generic-example-and-cli-reference.md +0 -20
- package/.backlog/tasks/task-16 - improve-docs-readme-with-generic-example-and-cli-reference.md +0 -20
- package/.backlog/tasks/task-17 - improve-drafts-readme-with-generic-example-and-cli-reference.md +0 -20
- package/.backlog/tasks/task-18 - improve-decisions-readme-with-generic-example-and-cli-reference.md +0 -20
- package/.backlog/tasks/task-19 - cli-fix-default-task-status-and-remove-draft-from-statuses.md +0 -55
- package/.backlog/tasks/task-2 - cli-core-logic-library.md +0 -28
- package/.backlog/tasks/task-20 - add-agent-guideline-to-mark-tasks-in-progress-on-start.md +0 -32
- package/.backlog/tasks/task-21 - kanban-board-vertical-layout.md +0 -31
- package/.backlog/tasks/task-22 - cli-prevent-double-dash-in-task-filenames.md +0 -24
- package/.backlog/tasks/task-23 - cli-kanban-board-order-tasks-by-id-asc.md +0 -30
- package/.backlog/tasks/task-24 - handle-subtasks-in-the-kanban-view.md +0 -38
- package/.backlog/tasks/task-24.1 - cli-kanban-board-milestone-view.md +0 -19
- package/.backlog/tasks/task-25 - cli-export-kanban-board-to-readme.md +0 -28
- package/.backlog/tasks/task-27 - add-contributing-guidelines.md +0 -27
- package/.backlog/tasks/task-29 - add-github-templates.md +0 -28
- package/.backlog/tasks/task-3 - cli-implement-backlog-init.md +0 -63
- package/.backlog/tasks/task-31 - update-readme-for-open-source.md +0 -26
- package/.backlog/tasks/task-32 - cli-hide-empty-'no-status'-column.md +0 -31
- package/.backlog/tasks/task-33 - cli-export-milestones-board-as-roadmap.md +0 -20
- package/.backlog/tasks/task-34 - split-readme.md-for-users-and-contributors.md +0 -26
- package/.backlog/tasks/task-35 - finalize-package.json-metadata-for-publishing.md +0 -24
- package/.backlog/tasks/task-36 - cli-prompt-for-project-name-in-init.md +0 -24
- package/.backlog/tasks/task-37 - cli-board-view-open-tasks-in-ide.md +0 -19
- package/.backlog/tasks/task-38 - cli-improved-agent-selection-for-init.md +0 -25
- package/.backlog/tasks/task-39 - cli-fix-empty-agent-instruction-files-on-init.md +0 -31
- package/.backlog/tasks/task-4 - cli-task-management-commands.md +0 -28
- package/.backlog/tasks/task-4.1 - cli-task-create.md +0 -27
- package/.backlog/tasks/task-4.10 - use-cli-to-mark-tasks-done.md +0 -51
- package/.backlog/tasks/task-4.11 - docs-add-definition-of-done-to-agent-guidelines.md +0 -23
- package/.backlog/tasks/task-4.12 - cli-handle-task-id-conflicts-across-branches.md +0 -53
- package/.backlog/tasks/task-4.13 - cli-fix-config-command-local-global-logic.md +0 -58
- package/.backlog/tasks/task-4.2 - cli-task-list-view.md +0 -25
- package/.backlog/tasks/task-4.3 - cli-task-edit.md +0 -24
- package/.backlog/tasks/task-4.4 - cli-task-archive-transition.md +0 -27
- package/.backlog/tasks/task-4.5 - cli-init-prompts-for-reporter-name-and-global-local-config.md +0 -28
- package/.backlog/tasks/task-4.6 - cli-add-empty-assignee-array-field-for-new-tasks.md +0 -35
- package/.backlog/tasks/task-4.7 - cli-parse-unquoted-created_date.md +0 -40
- package/.backlog/tasks/task-4.8 - cli-enforce-description-header.md +0 -48
- package/.backlog/tasks/task-4.9 - cli-normalize-task-id-inputs.md +0 -66
- package/.backlog/tasks/task-40 - cli-board-command-defaults-to-view.md +0 -38
- package/.backlog/tasks/task-41 - cli-migrate-terminal-ui-to-bblessed.md +0 -93
- package/.backlog/tasks/task-41.1 - cli-bblessed-init-wizard.md +0 -42
- package/.backlog/tasks/task-41.2 - cli-bblessed-task-view.md +0 -44
- package/.backlog/tasks/task-41.3 - cli-bblessed-doc-view.md +0 -45
- package/.backlog/tasks/task-41.4 - cli-bblessed-board-view.md +0 -49
- package/.backlog/tasks/task-41.5 - cli-audit-remaining-ui-for-bblessed.md +0 -55
- package/.backlog/tasks/task-42 - visual-hierarchy.md +0 -54
- package/.backlog/tasks/task-43 - remove-duplicate-acceptance-criteria-and-style-metadata.md +0 -56
- package/.backlog/tasks/task-44 - checklist-alignment.md +0 -24
- package/.backlog/tasks/task-45 - safe-line-wrapping.md +0 -23
- package/.backlog/tasks/task-46 - split-pane-layout.md +0 -24
- package/.backlog/tasks/task-47 - sticky-header-in-detail-view.md +0 -43
- package/.backlog/tasks/task-48 - footer-hint-line.md +0 -21
- package/.backlog/tasks/task-49 - status-styling.md +0 -53
- package/.backlog/tasks/task-5 - cli-docs-decisions.md +0 -57
- package/.backlog/tasks/task-50 - borders-&-padding.md +0 -22
- package/.backlog/tasks/task-51 - code-path-styling.md +0 -23
- package/.backlog/tasks/task-52 - cli-filter-tasks-list-by-status-or-assignee.md +0 -29
- package/.backlog/tasks/task-6 - cli-packaging.md +0 -65
- package/.backlog/tasks/task-6.1 - cli-local-installation-support-for-bunx-npx.md +0 -49
- package/.backlog/tasks/task-6.2 - cli-github-actions-for-build-&-publish.md +0 -64
- package/.backlog/tasks/task-7 - cli-kanban-view.md +0 -60
- package/.backlog/tasks/task-7.1 - cli-kanban-board-detect-remote-task-status.md +0 -62
- package/.backlog/tasks/task-8 - gui-project-setup.md +0 -21
- package/.backlog/tasks/task-9 - gui-task-crud.md +0 -24
- package/.cursorrules +0 -223
- package/.gitattributes +0 -2
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -25
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -15
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -8
- package/.github/workflows/ci.yml +0 -36
- package/.husky/pre-commit +0 -1
- package/AGENTS.md +0 -65
- package/CLAUDE.md +0 -87
- package/CONTRIBUTING.md +0 -19
- package/DEVELOPMENT.md +0 -37
- package/biome.json +0 -31
- package/bun.lock +0 -152
- package/cli/.cursorrules-xh86jabm.md +0 -82
- package/cli/AGENTS-xh86jabm.md +0 -82
- package/cli/CLAUDE-xh86jabm.md +0 -82
- package/cli/cli.js +0 -19622
- package/cli/index.js +0 -2
- package/docs/npm-publishing.md +0 -69
- package/scripts/build.js +0 -73
- package/src/agent-instructions.ts +0 -54
- package/src/board.ts +0 -263
- package/src/cli.ts +0 -806
- package/src/constants/index.ts +0 -48
- package/src/core/backlog.ts +0 -183
- package/src/core/remote-tasks.ts +0 -168
- package/src/file-system/operations.ts +0 -515
- package/src/git/operations.ts +0 -189
- package/src/guidelines/.cursorrules.md +0 -82
- package/src/guidelines/AGENTS.md +0 -82
- package/src/guidelines/CLAUDE.md +0 -82
- package/src/guidelines/index.ts +0 -7
- package/src/index.ts +0 -30
- package/src/markdown/parser.ts +0 -145
- package/src/markdown/serializer.ts +0 -71
- package/src/test/agent-instructions.test.ts +0 -62
- package/src/test/board.test.ts +0 -291
- package/src/test/build.test.ts +0 -28
- package/src/test/checklist.test.ts +0 -273
- package/src/test/cli.test.ts +0 -1300
- package/src/test/code-path.test.ts +0 -204
- package/src/test/core.test.ts +0 -330
- package/src/test/filesystem.test.ts +0 -435
- package/src/test/git.test.ts +0 -26
- package/src/test/heading.test.ts +0 -102
- package/src/test/line-wrapping.test.ts +0 -252
- package/src/test/local-install.test.ts +0 -34
- package/src/test/markdown.test.ts +0 -526
- package/src/test/parallel-loading.test.ts +0 -160
- package/src/test/parent-id-normalization.test.ts +0 -48
- package/src/test/remote-id-conflict.test.ts +0 -60
- package/src/test/status-icon.test.ts +0 -93
- package/src/types/blessed.d.ts +0 -14
- package/src/types/index.ts +0 -55
- package/src/types/raw.d.ts +0 -4
- package/src/ui/board.ts +0 -322
- package/src/ui/checklist.ts +0 -103
- package/src/ui/code-path.ts +0 -113
- package/src/ui/heading.ts +0 -121
- package/src/ui/loading.ts +0 -216
- package/src/ui/status-icon.ts +0 -53
- package/src/ui/task-list.ts +0 -168
- package/src/ui/task-viewer.ts +0 -640
- package/src/ui/tui.ts +0 -301
- package/tsconfig.json +0 -26
package/src/cli.ts
DELETED
|
@@ -1,806 +0,0 @@
|
|
|
1
|
-
import { mkdir } from "node:fs/promises";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
5
|
-
import { createInterface } from "node:readline/promises";
|
|
6
|
-
import { viewTaskEnhanced } from "./ui/task-viewer.ts";
|
|
7
|
-
// Interactive TUI helpers (bblessed based)
|
|
8
|
-
import { multiSelect, promptText, scrollableViewer, selectList } from "./ui/tui.ts";
|
|
9
|
-
|
|
10
|
-
// Kanban TUI renderer
|
|
11
|
-
import { renderBoardTui } from "./ui/board.ts";
|
|
12
|
-
|
|
13
|
-
// Loading screen utilities
|
|
14
|
-
import { createLoadingScreen, withLoadingScreen } from "./ui/loading.ts";
|
|
15
|
-
|
|
16
|
-
// Remote task loading utilities
|
|
17
|
-
import { type TaskWithMetadata, loadRemoteTasks, resolveTaskConflict } from "./core/remote-tasks.ts";
|
|
18
|
-
|
|
19
|
-
import { Command } from "commander";
|
|
20
|
-
import { DEFAULT_STATUSES, FALLBACK_STATUS } from "./constants/index.ts";
|
|
21
|
-
import {
|
|
22
|
-
type AgentInstructionFile,
|
|
23
|
-
Core,
|
|
24
|
-
addAgentInstructions,
|
|
25
|
-
exportKanbanBoardToFile,
|
|
26
|
-
initializeGitRepository,
|
|
27
|
-
isGitRepository,
|
|
28
|
-
parseTask,
|
|
29
|
-
} from "./index.ts";
|
|
30
|
-
import type { DecisionLog, Document as DocType, Task } from "./types/index.ts";
|
|
31
|
-
|
|
32
|
-
const program = new Command();
|
|
33
|
-
program.name("backlog").description("Backlog project management CLI");
|
|
34
|
-
|
|
35
|
-
program
|
|
36
|
-
.command("init [projectName]")
|
|
37
|
-
.description("initialize backlog project in the current repository")
|
|
38
|
-
.action(async (projectName?: string) => {
|
|
39
|
-
try {
|
|
40
|
-
const cwd = process.cwd();
|
|
41
|
-
const isRepo = await isGitRepository(cwd);
|
|
42
|
-
|
|
43
|
-
if (!isRepo) {
|
|
44
|
-
const rl = createInterface({ input, output });
|
|
45
|
-
const answer = (await rl.question("No git repository found. Initialize one here? [y/N] ")).trim().toLowerCase();
|
|
46
|
-
rl.close();
|
|
47
|
-
|
|
48
|
-
if (answer.startsWith("y")) {
|
|
49
|
-
await initializeGitRepository(cwd);
|
|
50
|
-
} else {
|
|
51
|
-
console.log("Aborting initialization.");
|
|
52
|
-
process.exit(1);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
let name = projectName;
|
|
57
|
-
if (!name) {
|
|
58
|
-
name = await promptText("Project name:");
|
|
59
|
-
if (!name) {
|
|
60
|
-
console.log("Aborting initialization.");
|
|
61
|
-
process.exit(1);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const reporter = (await promptText("Default reporter name (leave blank to skip):")) || "";
|
|
66
|
-
let storeGlobal = false;
|
|
67
|
-
if (reporter) {
|
|
68
|
-
const store = (await promptText("Store reporter name globally? [y/N]", "N")).toLowerCase();
|
|
69
|
-
storeGlobal = store.startsWith("y");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const options = [".cursorrules", "CLAUDE.md", "AGENTS.md", "readme.md"] as const;
|
|
73
|
-
const selected = await multiSelect("Select agent instruction files to update", options as unknown as string[]);
|
|
74
|
-
const files: AgentInstructionFile[] = (selected ?? []) as AgentInstructionFile[];
|
|
75
|
-
|
|
76
|
-
const core = new Core(cwd);
|
|
77
|
-
await core.initializeProject(name);
|
|
78
|
-
console.log(`Initialized backlog project: ${name}`);
|
|
79
|
-
|
|
80
|
-
if (files.length > 0) {
|
|
81
|
-
await addAgentInstructions(cwd, core.gitOps, files);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (reporter) {
|
|
85
|
-
if (storeGlobal) {
|
|
86
|
-
const globalPath = join(homedir(), ".backlog", "user");
|
|
87
|
-
await mkdir(dirname(globalPath), { recursive: true });
|
|
88
|
-
await Bun.write(globalPath, `default_reporter: "${reporter}"\n`);
|
|
89
|
-
} else {
|
|
90
|
-
const userPath = join(cwd, ".user");
|
|
91
|
-
await Bun.write(userPath, `default_reporter: "${reporter}"\n`);
|
|
92
|
-
const gitignorePath = join(cwd, ".gitignore");
|
|
93
|
-
let gitignore = "";
|
|
94
|
-
try {
|
|
95
|
-
gitignore = await Bun.file(gitignorePath).text();
|
|
96
|
-
} catch {}
|
|
97
|
-
if (!gitignore.split(/\r?\n/).includes(".user")) {
|
|
98
|
-
gitignore += `${gitignore.endsWith("\n") ? "" : "\n"}.user\n`;
|
|
99
|
-
await Bun.write(gitignorePath, gitignore);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
} catch (err) {
|
|
104
|
-
console.error("Failed to initialize project", err);
|
|
105
|
-
process.exitCode = 1;
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
async function generateNextId(core: Core, parent?: string): Promise<string> {
|
|
110
|
-
// Load local tasks and drafts in parallel
|
|
111
|
-
const [tasks, drafts] = await Promise.all([core.filesystem.listTasks(), core.filesystem.listDrafts()]);
|
|
112
|
-
const all = [...tasks, ...drafts];
|
|
113
|
-
|
|
114
|
-
const remoteIds: string[] = [];
|
|
115
|
-
try {
|
|
116
|
-
await core.gitOps.fetch();
|
|
117
|
-
const branches = await core.gitOps.listRemoteBranches();
|
|
118
|
-
|
|
119
|
-
// Load files from all branches in parallel
|
|
120
|
-
const branchFilePromises = branches.map(async (branch) => {
|
|
121
|
-
const files = await core.gitOps.listFilesInRemoteBranch(branch, ".backlog/tasks");
|
|
122
|
-
return files
|
|
123
|
-
.map((file) => {
|
|
124
|
-
const match = file.match(/task-([\d.]+)/);
|
|
125
|
-
return match ? `task-${match[1]}` : null;
|
|
126
|
-
})
|
|
127
|
-
.filter((id): id is string => id !== null);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
const branchResults = await Promise.all(branchFilePromises);
|
|
131
|
-
for (const branchIds of branchResults) {
|
|
132
|
-
remoteIds.push(...branchIds);
|
|
133
|
-
}
|
|
134
|
-
} catch {}
|
|
135
|
-
|
|
136
|
-
if (parent) {
|
|
137
|
-
const prefix = parent.startsWith("task-") ? parent : `task-${parent}`;
|
|
138
|
-
let max = 0;
|
|
139
|
-
for (const t of tasks) {
|
|
140
|
-
if (t.id.startsWith(`${prefix}.`)) {
|
|
141
|
-
const rest = t.id.slice(prefix.length + 1);
|
|
142
|
-
const num = Number.parseInt(rest.split(".")[0] || "0", 10);
|
|
143
|
-
if (num > max) max = num;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
for (const id of remoteIds) {
|
|
147
|
-
if (id.startsWith(`${prefix}.`)) {
|
|
148
|
-
const rest = id.slice(prefix.length + 1);
|
|
149
|
-
const num = Number.parseInt(rest.split(".")[0] || "0", 10);
|
|
150
|
-
if (num > max) max = num;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return `${prefix}.${max + 1}`;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
let max = -1;
|
|
157
|
-
for (const t of all) {
|
|
158
|
-
const match = t.id.match(/^task-(\d+)/);
|
|
159
|
-
if (match) {
|
|
160
|
-
const num = Number.parseInt(match[1], 10);
|
|
161
|
-
if (num > max) max = num;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
for (const id of remoteIds) {
|
|
165
|
-
const match = id.match(/^task-(\d+)/);
|
|
166
|
-
if (match) {
|
|
167
|
-
const num = Number.parseInt(match[1], 10);
|
|
168
|
-
if (num > max) max = num;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return `task-${max + 1}`;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async function generateNextDecisionId(core: Core): Promise<string> {
|
|
175
|
-
const files = await Array.fromAsync(new Bun.Glob("decision-*.md").scan({ cwd: core.filesystem.decisionsDir }));
|
|
176
|
-
let max = 0;
|
|
177
|
-
for (const file of files) {
|
|
178
|
-
const match = file.match(/^decision-(\d+)/);
|
|
179
|
-
if (match) {
|
|
180
|
-
const num = Number.parseInt(match[1], 10);
|
|
181
|
-
if (num > max) max = num;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return `decision-${max + 1}`;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async function generateNextDocId(core: Core): Promise<string> {
|
|
188
|
-
const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: core.filesystem.docsDir }));
|
|
189
|
-
let max = 0;
|
|
190
|
-
for (const file of files) {
|
|
191
|
-
const match = file.match(/^doc-(\d+)/);
|
|
192
|
-
if (match) {
|
|
193
|
-
const num = Number.parseInt(match[1], 10);
|
|
194
|
-
if (num > max) max = num;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return `doc-${max + 1}`;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function buildTaskFromOptions(id: string, title: string, options: Record<string, unknown>): Task {
|
|
201
|
-
const parentInput = options.parent ? String(options.parent) : undefined;
|
|
202
|
-
const normalizedParent = parentInput
|
|
203
|
-
? parentInput.startsWith("task-")
|
|
204
|
-
? parentInput
|
|
205
|
-
: `task-${parentInput}`
|
|
206
|
-
: undefined;
|
|
207
|
-
|
|
208
|
-
return {
|
|
209
|
-
id,
|
|
210
|
-
title,
|
|
211
|
-
status: options.status || "",
|
|
212
|
-
assignee: options.assignee ? [String(options.assignee)] : [],
|
|
213
|
-
createdDate: new Date().toISOString().split("T")[0],
|
|
214
|
-
labels: options.labels
|
|
215
|
-
? String(options.labels)
|
|
216
|
-
.split(",")
|
|
217
|
-
.map((l: string) => l.trim())
|
|
218
|
-
.filter(Boolean)
|
|
219
|
-
: [],
|
|
220
|
-
dependencies: [],
|
|
221
|
-
description: options.description || "",
|
|
222
|
-
...(normalizedParent && { parentTaskId: normalizedParent }),
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const taskCmd = program.command("task").aliases(["tasks"]);
|
|
227
|
-
|
|
228
|
-
taskCmd
|
|
229
|
-
.command("create <title>")
|
|
230
|
-
.option("-d, --description <text>")
|
|
231
|
-
.option("-a, --assignee <assignee>")
|
|
232
|
-
.option("-s, --status <status>")
|
|
233
|
-
.option("-l, --labels <labels>")
|
|
234
|
-
.option("--draft")
|
|
235
|
-
.option("--parent <taskId>")
|
|
236
|
-
.action(async (title: string, options) => {
|
|
237
|
-
const cwd = process.cwd();
|
|
238
|
-
const core = new Core(cwd);
|
|
239
|
-
const id = await generateNextId(core, options.parent);
|
|
240
|
-
const task = buildTaskFromOptions(id, title, options);
|
|
241
|
-
if (options.draft) {
|
|
242
|
-
await core.createDraft(task, true);
|
|
243
|
-
console.log(`Created draft ${id}`);
|
|
244
|
-
} else {
|
|
245
|
-
await core.createTask(task, true);
|
|
246
|
-
console.log(`Created task ${id}`);
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
taskCmd
|
|
251
|
-
.command("list")
|
|
252
|
-
.description("list tasks grouped by status")
|
|
253
|
-
.option("-s, --status <status>", "filter tasks by status")
|
|
254
|
-
.option("-a, --assignee <assignee>", "filter tasks by assignee")
|
|
255
|
-
.option("--plain", "use plain text output instead of interactive UI")
|
|
256
|
-
.action(async (options) => {
|
|
257
|
-
const cwd = process.cwd();
|
|
258
|
-
const core = new Core(cwd);
|
|
259
|
-
const tasks = await core.filesystem.listTasks();
|
|
260
|
-
const config = await core.filesystem.loadConfig();
|
|
261
|
-
|
|
262
|
-
let filtered = tasks;
|
|
263
|
-
if (options.status) {
|
|
264
|
-
filtered = filtered.filter((t) => t.status === options.status);
|
|
265
|
-
}
|
|
266
|
-
if (options.assignee) {
|
|
267
|
-
filtered = filtered.filter((t) => t.assignee.includes(options.assignee));
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (filtered.length === 0) {
|
|
271
|
-
console.log("No tasks found.");
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Plain text output
|
|
276
|
-
if (options.plain) {
|
|
277
|
-
const groups = new Map<string, Task[]>();
|
|
278
|
-
for (const task of filtered) {
|
|
279
|
-
const status = task.status || "";
|
|
280
|
-
const list = groups.get(status) || [];
|
|
281
|
-
list.push(task);
|
|
282
|
-
groups.set(status, list);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const statuses = config?.statuses || [];
|
|
286
|
-
const ordered = [
|
|
287
|
-
...statuses.filter((s) => groups.has(s)),
|
|
288
|
-
...Array.from(groups.keys()).filter((s) => !statuses.includes(s)),
|
|
289
|
-
];
|
|
290
|
-
|
|
291
|
-
for (const status of ordered) {
|
|
292
|
-
const list = groups.get(status);
|
|
293
|
-
if (!list) continue;
|
|
294
|
-
console.log(`${status || "No Status"}:`);
|
|
295
|
-
for (const t of list) {
|
|
296
|
-
console.log(` ${t.id} - ${t.title}`);
|
|
297
|
-
}
|
|
298
|
-
console.log();
|
|
299
|
-
}
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Interactive UI
|
|
304
|
-
const selected = await selectList("Select a task", filtered, (task) => task.status || "No Status");
|
|
305
|
-
if (selected) {
|
|
306
|
-
// Show task details
|
|
307
|
-
const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: core.filesystem.tasksDir }));
|
|
308
|
-
const taskFile = files.find((f) => f.startsWith(`${selected.id} -`));
|
|
309
|
-
if (taskFile) {
|
|
310
|
-
const filePath = join(core.filesystem.tasksDir, taskFile);
|
|
311
|
-
const content = await Bun.file(filePath).text();
|
|
312
|
-
await viewTaskEnhanced(selected, content);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
taskCmd
|
|
318
|
-
.command("edit <taskId>")
|
|
319
|
-
.description("edit an existing task")
|
|
320
|
-
.option("-t, --title <title>")
|
|
321
|
-
.option("-d, --description <text>")
|
|
322
|
-
.option("-a, --assignee <assignee>")
|
|
323
|
-
.option("-s, --status <status>")
|
|
324
|
-
.option("-l, --label <labels>")
|
|
325
|
-
.option("--add-label <label>")
|
|
326
|
-
.option("--remove-label <label>")
|
|
327
|
-
.action(async (taskId: string, options) => {
|
|
328
|
-
const cwd = process.cwd();
|
|
329
|
-
const core = new Core(cwd);
|
|
330
|
-
const task = await core.filesystem.loadTask(taskId);
|
|
331
|
-
|
|
332
|
-
if (!task) {
|
|
333
|
-
console.error(`Task ${taskId} not found.`);
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (options.title) {
|
|
338
|
-
task.title = String(options.title);
|
|
339
|
-
}
|
|
340
|
-
if (options.description) {
|
|
341
|
-
task.description = String(options.description);
|
|
342
|
-
}
|
|
343
|
-
if (typeof options.assignee !== "undefined") {
|
|
344
|
-
task.assignee = [String(options.assignee)];
|
|
345
|
-
}
|
|
346
|
-
if (options.status) {
|
|
347
|
-
task.status = String(options.status);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const labels = [...task.labels];
|
|
351
|
-
if (options.label) {
|
|
352
|
-
const newLabels = String(options.label)
|
|
353
|
-
.split(",")
|
|
354
|
-
.map((l: string) => l.trim())
|
|
355
|
-
.filter(Boolean);
|
|
356
|
-
labels.splice(0, labels.length, ...newLabels);
|
|
357
|
-
}
|
|
358
|
-
if (options.addLabel) {
|
|
359
|
-
const adds = Array.isArray(options.addLabel) ? options.addLabel : [options.addLabel];
|
|
360
|
-
for (const l of adds) {
|
|
361
|
-
const trimmed = String(l).trim();
|
|
362
|
-
if (trimmed && !labels.includes(trimmed)) labels.push(trimmed);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
if (options.removeLabel) {
|
|
366
|
-
const removes = Array.isArray(options.removeLabel) ? options.removeLabel : [options.removeLabel];
|
|
367
|
-
for (const l of removes) {
|
|
368
|
-
const trimmed = String(l).trim();
|
|
369
|
-
const idx = labels.indexOf(trimmed);
|
|
370
|
-
if (idx !== -1) labels.splice(idx, 1);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
task.labels = labels;
|
|
374
|
-
task.updatedDate = new Date().toISOString().split("T")[0];
|
|
375
|
-
|
|
376
|
-
await core.updateTask(task, true);
|
|
377
|
-
console.log(`Updated task ${task.id}`);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
taskCmd
|
|
381
|
-
.command("view <taskId>")
|
|
382
|
-
.description("display task details")
|
|
383
|
-
.action(async (taskId: string) => {
|
|
384
|
-
const cwd = process.cwd();
|
|
385
|
-
const core = new Core(cwd);
|
|
386
|
-
const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: core.filesystem.tasksDir }));
|
|
387
|
-
const normalizedId = taskId.startsWith("task-") ? taskId : `task-${taskId}`;
|
|
388
|
-
const taskFile = files.find((f) => f.startsWith(`${normalizedId} -`));
|
|
389
|
-
|
|
390
|
-
if (!taskFile) {
|
|
391
|
-
console.error(`Task ${taskId} not found.`);
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const filePath = join(core.filesystem.tasksDir, taskFile);
|
|
396
|
-
const content = await Bun.file(filePath).text();
|
|
397
|
-
const task = await core.filesystem.loadTask(taskId);
|
|
398
|
-
|
|
399
|
-
if (!task) {
|
|
400
|
-
console.error(`Task ${taskId} not found.`);
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Use enhanced task viewer
|
|
405
|
-
await viewTaskEnhanced(task, content);
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
taskCmd
|
|
409
|
-
.command("archive <taskId>")
|
|
410
|
-
.description("archive a task")
|
|
411
|
-
.action(async (taskId: string) => {
|
|
412
|
-
const cwd = process.cwd();
|
|
413
|
-
const core = new Core(cwd);
|
|
414
|
-
const success = await core.archiveTask(taskId, true);
|
|
415
|
-
if (success) {
|
|
416
|
-
console.log(`Archived task ${taskId}`);
|
|
417
|
-
} else {
|
|
418
|
-
console.error(`Task ${taskId} not found.`);
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
taskCmd
|
|
423
|
-
.command("demote <taskId>")
|
|
424
|
-
.description("move task back to drafts")
|
|
425
|
-
.action(async (taskId: string) => {
|
|
426
|
-
const cwd = process.cwd();
|
|
427
|
-
const core = new Core(cwd);
|
|
428
|
-
const success = await core.demoteTask(taskId, true);
|
|
429
|
-
if (success) {
|
|
430
|
-
console.log(`Demoted task ${taskId}`);
|
|
431
|
-
} else {
|
|
432
|
-
console.error(`Task ${taskId} not found.`);
|
|
433
|
-
}
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
taskCmd.argument("[taskId]").action(async (taskId: string | undefined) => {
|
|
437
|
-
if (!taskId) {
|
|
438
|
-
taskCmd.help();
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
// Use the view command directly
|
|
442
|
-
const cwd = process.cwd();
|
|
443
|
-
const core = new Core(cwd);
|
|
444
|
-
const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: core.filesystem.tasksDir }));
|
|
445
|
-
const normalizedId = taskId.startsWith("task-") ? taskId : `task-${taskId}`;
|
|
446
|
-
const taskFile = files.find((f) => f.startsWith(`${normalizedId} -`));
|
|
447
|
-
|
|
448
|
-
if (!taskFile) {
|
|
449
|
-
console.error(`Task ${taskId} not found.`);
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
const filePath = join(core.filesystem.tasksDir, taskFile);
|
|
454
|
-
const content = await Bun.file(filePath).text();
|
|
455
|
-
await scrollableViewer(content);
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
const draftCmd = program.command("draft");
|
|
459
|
-
|
|
460
|
-
draftCmd
|
|
461
|
-
.command("create <title>")
|
|
462
|
-
.option("-d, --description <text>")
|
|
463
|
-
.option("-a, --assignee <assignee>")
|
|
464
|
-
.option("-s, --status <status>")
|
|
465
|
-
.option("-l, --labels <labels>")
|
|
466
|
-
.action(async (title: string, options) => {
|
|
467
|
-
const cwd = process.cwd();
|
|
468
|
-
const core = new Core(cwd);
|
|
469
|
-
const id = await generateNextId(core);
|
|
470
|
-
const task = buildTaskFromOptions(id, title, options);
|
|
471
|
-
await core.createDraft(task, true);
|
|
472
|
-
console.log(`Created draft ${id}`);
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
draftCmd
|
|
476
|
-
.command("archive <taskId>")
|
|
477
|
-
.description("archive a draft")
|
|
478
|
-
.action(async (taskId: string) => {
|
|
479
|
-
const cwd = process.cwd();
|
|
480
|
-
const core = new Core(cwd);
|
|
481
|
-
const success = await core.archiveDraft(taskId, true);
|
|
482
|
-
if (success) {
|
|
483
|
-
console.log(`Archived draft ${taskId}`);
|
|
484
|
-
} else {
|
|
485
|
-
console.error(`Draft ${taskId} not found.`);
|
|
486
|
-
}
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
draftCmd
|
|
490
|
-
.command("promote <taskId>")
|
|
491
|
-
.description("promote draft to task")
|
|
492
|
-
.action(async (taskId: string) => {
|
|
493
|
-
const cwd = process.cwd();
|
|
494
|
-
const core = new Core(cwd);
|
|
495
|
-
const success = await core.promoteDraft(taskId, true);
|
|
496
|
-
if (success) {
|
|
497
|
-
console.log(`Promoted draft ${taskId}`);
|
|
498
|
-
} else {
|
|
499
|
-
console.error(`Draft ${taskId} not found.`);
|
|
500
|
-
}
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
const boardCmd = program.command("board");
|
|
504
|
-
|
|
505
|
-
function addBoardOptions(cmd: Command) {
|
|
506
|
-
return cmd
|
|
507
|
-
.option("-l, --layout <layout>", "board layout (horizontal|vertical)", "horizontal")
|
|
508
|
-
.option("--vertical", "use vertical layout (shortcut for --layout vertical)");
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// TaskWithMetadata and resolveTaskConflict are now imported from remote-tasks.ts
|
|
512
|
-
|
|
513
|
-
async function handleBoardView(options: { layout?: string; vertical?: boolean }) {
|
|
514
|
-
const cwd = process.cwd();
|
|
515
|
-
const core = new Core(cwd);
|
|
516
|
-
const config = await core.filesystem.loadConfig();
|
|
517
|
-
const statuses = config?.statuses || [];
|
|
518
|
-
const resolutionStrategy = config?.taskResolutionStrategy || "most_progressed";
|
|
519
|
-
|
|
520
|
-
// Load tasks with loading screen
|
|
521
|
-
const allTasks = await withLoadingScreen("Loading tasks from local and remote branches", async () => {
|
|
522
|
-
// Load local tasks
|
|
523
|
-
const localTasks = await core.filesystem.listTasksWithMetadata();
|
|
524
|
-
const tasksById = new Map<string, TaskWithMetadata>(
|
|
525
|
-
localTasks.map((t) => [t.id, { ...t, source: "local" } as TaskWithMetadata]),
|
|
526
|
-
);
|
|
527
|
-
|
|
528
|
-
// Load remote tasks in parallel
|
|
529
|
-
const remoteTasks = await loadRemoteTasks(core.gitOps);
|
|
530
|
-
|
|
531
|
-
// Merge remote tasks with local tasks
|
|
532
|
-
for (const remoteTask of remoteTasks) {
|
|
533
|
-
const existing = tasksById.get(remoteTask.id);
|
|
534
|
-
if (!existing) {
|
|
535
|
-
tasksById.set(remoteTask.id, remoteTask);
|
|
536
|
-
} else {
|
|
537
|
-
const resolved = resolveTaskConflict(existing, remoteTask, statuses, resolutionStrategy);
|
|
538
|
-
tasksById.set(remoteTask.id, resolved);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
return Array.from(tasksById.values());
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
if (allTasks.length === 0) {
|
|
546
|
-
console.log("No tasks found.");
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const layout = options.vertical ? "vertical" : (options.layout as "horizontal" | "vertical") || "horizontal";
|
|
551
|
-
const maxColumnWidth = config?.maxColumnWidth || 20; // Default for terminal display
|
|
552
|
-
// Always use renderBoardTui which falls back to plain text if blessed is not available
|
|
553
|
-
await renderBoardTui(allTasks, statuses, layout, maxColumnWidth);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
addBoardOptions(boardCmd).description("display tasks in a Kanban board").action(handleBoardView);
|
|
557
|
-
|
|
558
|
-
addBoardOptions(boardCmd.command("view").description("display tasks in a Kanban board")).action(handleBoardView);
|
|
559
|
-
|
|
560
|
-
boardCmd
|
|
561
|
-
.command("export [filename]")
|
|
562
|
-
.description("append kanban board to readme or output file")
|
|
563
|
-
.option("-o, --output <path>", "output file (deprecated, use filename argument instead)")
|
|
564
|
-
.action(async (filename, options) => {
|
|
565
|
-
const cwd = process.cwd();
|
|
566
|
-
const core = new Core(cwd);
|
|
567
|
-
const config = await core.filesystem.loadConfig();
|
|
568
|
-
const statuses = config?.statuses || [];
|
|
569
|
-
const resolutionStrategy = config?.taskResolutionStrategy || "most_progressed";
|
|
570
|
-
|
|
571
|
-
// Load tasks with progress tracking
|
|
572
|
-
const loadingScreen = await createLoadingScreen("Loading tasks for export");
|
|
573
|
-
|
|
574
|
-
try {
|
|
575
|
-
// Load local tasks
|
|
576
|
-
loadingScreen?.update("Loading local tasks...");
|
|
577
|
-
const localTasks = await core.filesystem.listTasksWithMetadata();
|
|
578
|
-
const tasksById = new Map<string, TaskWithMetadata>(
|
|
579
|
-
localTasks.map((t) => [t.id, { ...t, source: "local" } as TaskWithMetadata]),
|
|
580
|
-
);
|
|
581
|
-
loadingScreen?.update(`Found ${localTasks.length} local tasks`);
|
|
582
|
-
|
|
583
|
-
// Load remote tasks in parallel
|
|
584
|
-
loadingScreen?.update("Loading remote tasks...");
|
|
585
|
-
const remoteTasks = await loadRemoteTasks(core.gitOps, (msg) => loadingScreen?.update(msg));
|
|
586
|
-
|
|
587
|
-
// Merge remote tasks with local tasks
|
|
588
|
-
loadingScreen?.update("Merging tasks...");
|
|
589
|
-
for (const remoteTask of remoteTasks) {
|
|
590
|
-
const existing = tasksById.get(remoteTask.id);
|
|
591
|
-
if (!existing) {
|
|
592
|
-
tasksById.set(remoteTask.id, remoteTask);
|
|
593
|
-
} else {
|
|
594
|
-
const resolved = resolveTaskConflict(existing, remoteTask, statuses, resolutionStrategy);
|
|
595
|
-
tasksById.set(remoteTask.id, resolved);
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
const allTasks = Array.from(tasksById.values());
|
|
600
|
-
loadingScreen?.update(`Total tasks: ${allTasks.length}`);
|
|
601
|
-
|
|
602
|
-
// Close loading screen before export
|
|
603
|
-
loadingScreen?.close();
|
|
604
|
-
|
|
605
|
-
// Priority: filename argument > --output option > default readme.md
|
|
606
|
-
const outputFile = filename || options.output || "readme.md";
|
|
607
|
-
const outputPath = join(cwd, outputFile as string);
|
|
608
|
-
const maxColumnWidth = config?.maxColumnWidth || 30; // Default for export
|
|
609
|
-
const addTitle = !filename && !options.output; // Add title only for default readme export
|
|
610
|
-
await exportKanbanBoardToFile(allTasks, statuses, outputPath, maxColumnWidth, addTitle);
|
|
611
|
-
console.log(`Exported board to ${outputPath}`);
|
|
612
|
-
} catch (error) {
|
|
613
|
-
loadingScreen?.close();
|
|
614
|
-
throw error;
|
|
615
|
-
}
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
const docCmd = program.command("doc");
|
|
619
|
-
|
|
620
|
-
docCmd
|
|
621
|
-
.command("create <title>")
|
|
622
|
-
.option("-p, --path <path>")
|
|
623
|
-
.option("-t, --type <type>")
|
|
624
|
-
.action(async (title: string, options) => {
|
|
625
|
-
const cwd = process.cwd();
|
|
626
|
-
const core = new Core(cwd);
|
|
627
|
-
const id = await generateNextDocId(core);
|
|
628
|
-
const document: DocType = {
|
|
629
|
-
id,
|
|
630
|
-
title,
|
|
631
|
-
type: (options.type || "other") as DocType["type"],
|
|
632
|
-
createdDate: new Date().toISOString().split("T")[0],
|
|
633
|
-
content: "",
|
|
634
|
-
};
|
|
635
|
-
await core.createDocument(document, true, options.path || "");
|
|
636
|
-
console.log(`Created document ${id}`);
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
docCmd
|
|
640
|
-
.command("list")
|
|
641
|
-
.option("--plain", "use plain text output instead of interactive UI")
|
|
642
|
-
.action(async (options) => {
|
|
643
|
-
const cwd = process.cwd();
|
|
644
|
-
const core = new Core(cwd);
|
|
645
|
-
const docs = await core.filesystem.listDocuments();
|
|
646
|
-
if (docs.length === 0) {
|
|
647
|
-
console.log("No docs found.");
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// Plain text output
|
|
652
|
-
if (options.plain) {
|
|
653
|
-
for (const d of docs) {
|
|
654
|
-
console.log(`${d.id} - ${d.title}`);
|
|
655
|
-
}
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// Interactive UI
|
|
660
|
-
const selected = await selectList("Select a document", docs);
|
|
661
|
-
if (selected) {
|
|
662
|
-
// Show document details
|
|
663
|
-
const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: core.filesystem.docsDir }));
|
|
664
|
-
const docFile = files.find((f) => f.startsWith(`${selected.id} -`) || f === `${selected.id}.md`);
|
|
665
|
-
if (docFile) {
|
|
666
|
-
const filePath = join(core.filesystem.docsDir, docFile);
|
|
667
|
-
const content = await Bun.file(filePath).text();
|
|
668
|
-
await scrollableViewer(content);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
});
|
|
672
|
-
|
|
673
|
-
// Document view command
|
|
674
|
-
docCmd
|
|
675
|
-
.command("view <docId>")
|
|
676
|
-
.description("view a document")
|
|
677
|
-
.action(async (docId: string) => {
|
|
678
|
-
const cwd = process.cwd();
|
|
679
|
-
const core = new Core(cwd);
|
|
680
|
-
const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: core.filesystem.docsDir }));
|
|
681
|
-
const normalizedId = docId.startsWith("doc-") ? docId : `doc-${docId}`;
|
|
682
|
-
const docFile = files.find((f) => f.startsWith(`${normalizedId} -`) || f === `${normalizedId}.md`);
|
|
683
|
-
|
|
684
|
-
if (!docFile) {
|
|
685
|
-
console.error(`Document ${docId} not found.`);
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
const filePath = join(core.filesystem.docsDir, docFile);
|
|
690
|
-
const content = await Bun.file(filePath).text();
|
|
691
|
-
|
|
692
|
-
// Use scrollableViewer which falls back to console.log if blessed is not available
|
|
693
|
-
await scrollableViewer(content);
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
const decisionCmd = program.command("decision");
|
|
697
|
-
|
|
698
|
-
decisionCmd
|
|
699
|
-
.command("create <title>")
|
|
700
|
-
.option("-s, --status <status>")
|
|
701
|
-
.action(async (title: string, options) => {
|
|
702
|
-
const cwd = process.cwd();
|
|
703
|
-
const core = new Core(cwd);
|
|
704
|
-
const id = await generateNextDecisionId(core);
|
|
705
|
-
const decision: DecisionLog = {
|
|
706
|
-
id,
|
|
707
|
-
title,
|
|
708
|
-
date: new Date().toISOString().split("T")[0],
|
|
709
|
-
status: (options.status || "proposed") as DecisionLog["status"],
|
|
710
|
-
context: "",
|
|
711
|
-
decision: "",
|
|
712
|
-
consequences: "",
|
|
713
|
-
};
|
|
714
|
-
await core.createDecisionLog(decision, true);
|
|
715
|
-
console.log(`Created decision ${id}`);
|
|
716
|
-
});
|
|
717
|
-
|
|
718
|
-
decisionCmd
|
|
719
|
-
.command("list")
|
|
720
|
-
.option("--plain", "use plain text output instead of interactive UI")
|
|
721
|
-
.action(async (options) => {
|
|
722
|
-
const cwd = process.cwd();
|
|
723
|
-
const core = new Core(cwd);
|
|
724
|
-
const decisions = await core.filesystem.listDecisionLogs();
|
|
725
|
-
if (decisions.length === 0) {
|
|
726
|
-
console.log("No decisions found.");
|
|
727
|
-
return;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// Plain text output
|
|
731
|
-
if (options.plain) {
|
|
732
|
-
for (const d of decisions) {
|
|
733
|
-
console.log(`${d.id} - ${d.title}`);
|
|
734
|
-
}
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// Interactive UI
|
|
739
|
-
const selected = await selectList("Select a decision", decisions);
|
|
740
|
-
if (selected) {
|
|
741
|
-
// Show decision details
|
|
742
|
-
const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: core.filesystem.decisionsDir }));
|
|
743
|
-
const decisionFile = files.find((f) => f.startsWith(`${selected.id} -`) || f === `${selected.id}.md`);
|
|
744
|
-
if (decisionFile) {
|
|
745
|
-
const filePath = join(core.filesystem.decisionsDir, decisionFile);
|
|
746
|
-
const content = await Bun.file(filePath).text();
|
|
747
|
-
await scrollableViewer(content);
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
const configCmd = program.command("config");
|
|
753
|
-
|
|
754
|
-
configCmd
|
|
755
|
-
.command("get <key>")
|
|
756
|
-
.description("get configuration value")
|
|
757
|
-
.action(async (key: string) => {
|
|
758
|
-
const cwd = process.cwd();
|
|
759
|
-
const core = new Core(cwd);
|
|
760
|
-
const localCfg = await core.filesystem.loadConfig();
|
|
761
|
-
const localVal = localCfg ? (localCfg as unknown as Record<string, unknown>)[key] : undefined;
|
|
762
|
-
if (localVal !== undefined) {
|
|
763
|
-
console.log(localVal);
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
const globalVal = await core.filesystem.getUserSetting(key, true);
|
|
767
|
-
if (globalVal !== undefined) {
|
|
768
|
-
console.log(globalVal);
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
|
-
const defaults: Record<string, unknown> = {
|
|
772
|
-
statuses: DEFAULT_STATUSES,
|
|
773
|
-
defaultStatus: FALLBACK_STATUS,
|
|
774
|
-
};
|
|
775
|
-
if (key in defaults) {
|
|
776
|
-
console.log(defaults[key]);
|
|
777
|
-
}
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
configCmd
|
|
781
|
-
.command("set <key> <value>")
|
|
782
|
-
.description("set configuration value")
|
|
783
|
-
.option("--global", "save to global user config")
|
|
784
|
-
.option("--local", "save to local project config")
|
|
785
|
-
.action(async (key: string, value: string, options) => {
|
|
786
|
-
const cwd = process.cwd();
|
|
787
|
-
const core = new Core(cwd);
|
|
788
|
-
if (options.global) {
|
|
789
|
-
await core.filesystem.setUserSetting(key, value, true);
|
|
790
|
-
console.log(`Set ${key} in global config`);
|
|
791
|
-
} else {
|
|
792
|
-
const cfg = (await core.filesystem.loadConfig()) || {
|
|
793
|
-
projectName: "",
|
|
794
|
-
statuses: [...DEFAULT_STATUSES],
|
|
795
|
-
labels: [],
|
|
796
|
-
milestones: [],
|
|
797
|
-
defaultStatus: FALLBACK_STATUS,
|
|
798
|
-
dateFormat: "YYYY-MM-DD",
|
|
799
|
-
};
|
|
800
|
-
(cfg as unknown as Record<string, unknown>)[key] = value;
|
|
801
|
-
await core.filesystem.saveConfig(cfg);
|
|
802
|
-
console.log(`Set ${key} in local config`);
|
|
803
|
-
}
|
|
804
|
-
});
|
|
805
|
-
|
|
806
|
-
program.parseAsync(process.argv);
|