backlog.md 0.1.0 → 0.1.3
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/cli/index.js
DELETED
package/docs/npm-publishing.md
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# NPM Publishing Setup
|
|
2
|
-
|
|
3
|
-
This guide explains how to set up npm publishing for the Backlog.md project.
|
|
4
|
-
|
|
5
|
-
## Prerequisites
|
|
6
|
-
|
|
7
|
-
1. An npm account with publish permissions for the `@backlog.md` package
|
|
8
|
-
2. Repository admin access to configure GitHub secrets
|
|
9
|
-
|
|
10
|
-
## Setting up the NPM Token
|
|
11
|
-
|
|
12
|
-
### 1. Generate an npm Access Token
|
|
13
|
-
|
|
14
|
-
1. Log in to [npmjs.com](https://www.npmjs.com)
|
|
15
|
-
2. Click on your profile picture → Access Tokens
|
|
16
|
-
3. Click "Generate New Token"
|
|
17
|
-
4. Choose "Automation" token type (recommended for CI/CD)
|
|
18
|
-
5. Name it something like "backlog-md-github-actions"
|
|
19
|
-
6. Copy the generated token (it starts with `npm_`)
|
|
20
|
-
|
|
21
|
-
### 2. Add the Token to GitHub Secrets
|
|
22
|
-
|
|
23
|
-
1. Go to the GitHub repository settings
|
|
24
|
-
2. Navigate to Settings → Secrets and variables → Actions
|
|
25
|
-
3. Click "New repository secret"
|
|
26
|
-
4. Name: `NPM_TOKEN`
|
|
27
|
-
5. Value: Paste your npm token
|
|
28
|
-
6. Click "Add secret"
|
|
29
|
-
|
|
30
|
-
## How Publishing Works
|
|
31
|
-
|
|
32
|
-
The GitHub Actions workflow (`/.github/workflows/ci.yml`) automatically publishes to npm when:
|
|
33
|
-
|
|
34
|
-
1. A new git tag is pushed that matches the pattern `v*.*.*` (e.g., `v1.0.0`)
|
|
35
|
-
2. The build and tests pass successfully
|
|
36
|
-
|
|
37
|
-
The workflow:
|
|
38
|
-
- Uses `actions/setup-node@v4` to configure npm authentication
|
|
39
|
-
- Sets the registry URL to `https://registry.npmjs.org`
|
|
40
|
-
- Uses the `NPM_TOKEN` secret for authentication
|
|
41
|
-
- Runs `npm publish --access public` to publish the package
|
|
42
|
-
|
|
43
|
-
## Troubleshooting
|
|
44
|
-
|
|
45
|
-
### Error: ENEEDAUTH
|
|
46
|
-
|
|
47
|
-
If you see this error, it means the `NPM_TOKEN` secret is either:
|
|
48
|
-
- Not configured in GitHub
|
|
49
|
-
- Invalid or expired
|
|
50
|
-
- Missing the required publish permissions
|
|
51
|
-
|
|
52
|
-
### Error: 403 Forbidden
|
|
53
|
-
|
|
54
|
-
This usually means:
|
|
55
|
-
- The token doesn't have permission to publish to the package
|
|
56
|
-
- The package name is already taken by another user
|
|
57
|
-
- You're not listed as a maintainer of the package
|
|
58
|
-
|
|
59
|
-
## Manual Publishing (Not Recommended)
|
|
60
|
-
|
|
61
|
-
If you need to publish manually:
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
npm login
|
|
65
|
-
bun run build
|
|
66
|
-
npm publish --access public
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Always prefer using the automated workflow to ensure consistency.
|
package/scripts/build.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
|
5
|
-
import { platform } from "node:os";
|
|
6
|
-
import { dirname, join } from "node:path";
|
|
7
|
-
|
|
8
|
-
const isWindows = platform() === "win32";
|
|
9
|
-
const outDir = "cli";
|
|
10
|
-
const indexFile = join(outDir, "index.js");
|
|
11
|
-
const executableName = isWindows ? "backlog.exe" : "backlog";
|
|
12
|
-
const executablePath = join(outDir, executableName);
|
|
13
|
-
|
|
14
|
-
async function runCommand(command, args) {
|
|
15
|
-
return new Promise((resolve, reject) => {
|
|
16
|
-
const child = spawn(command, args, {
|
|
17
|
-
stdio: "inherit",
|
|
18
|
-
shell: isWindows,
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
child.on("close", (code) => {
|
|
22
|
-
if (code === 0) {
|
|
23
|
-
resolve();
|
|
24
|
-
} else {
|
|
25
|
-
reject(new Error(`Command failed with exit code ${code}`));
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
child.on("error", reject);
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function build() {
|
|
34
|
-
try {
|
|
35
|
-
console.log("Building CLI...");
|
|
36
|
-
|
|
37
|
-
// Ensure output directory exists
|
|
38
|
-
await mkdir(dirname(indexFile), { recursive: true });
|
|
39
|
-
|
|
40
|
-
// Build JavaScript bundle
|
|
41
|
-
console.log("Building JavaScript bundle...");
|
|
42
|
-
await runCommand("bun", ["build", "src/cli.ts", "--outdir", outDir, "--target", "node"]);
|
|
43
|
-
|
|
44
|
-
// Create index.js wrapper
|
|
45
|
-
console.log("Creating index.js wrapper...");
|
|
46
|
-
const indexContent = isWindows ? 'import("./cli.js");' : '#!/usr/bin/env node\nimport("./cli.js");';
|
|
47
|
-
|
|
48
|
-
await writeFile(indexFile, indexContent);
|
|
49
|
-
|
|
50
|
-
// Make executable on Unix systems
|
|
51
|
-
if (!isWindows) {
|
|
52
|
-
await chmod(indexFile, "755");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Build compiled executable
|
|
56
|
-
console.log("Building compiled executable...");
|
|
57
|
-
await runCommand("bun", ["build", "src/cli.ts", "--compile", "--outfile", executablePath]);
|
|
58
|
-
|
|
59
|
-
// Make executable on Unix systems
|
|
60
|
-
if (!isWindows) {
|
|
61
|
-
await chmod(executablePath, "755");
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
console.log("Build completed successfully!");
|
|
65
|
-
console.log(`Executable: ${executablePath}`);
|
|
66
|
-
console.log(`JS Bundle: ${indexFile}`);
|
|
67
|
-
} catch (error) {
|
|
68
|
-
console.error("Build failed:", error.message);
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
build();
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { dirname, isAbsolute, join } from "node:path";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { AGENT_GUIDELINES, CLAUDE_GUIDELINES, CURSOR_GUIDELINES, README_GUIDELINES } from "./constants/index.ts";
|
|
4
|
-
import type { GitOperations } from "./git/operations.ts";
|
|
5
|
-
|
|
6
|
-
export type AgentInstructionFile = "AGENTS.md" | "CLAUDE.md" | ".cursorrules" | "readme.md";
|
|
7
|
-
|
|
8
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
|
|
10
|
-
async function loadContent(textOrPath: string): Promise<string> {
|
|
11
|
-
if (textOrPath.includes("\n")) return textOrPath;
|
|
12
|
-
try {
|
|
13
|
-
const path = isAbsolute(textOrPath) ? textOrPath : join(__dirname, textOrPath);
|
|
14
|
-
return await Bun.file(path).text();
|
|
15
|
-
} catch {
|
|
16
|
-
return textOrPath;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function addAgentInstructions(
|
|
21
|
-
projectRoot: string,
|
|
22
|
-
git?: GitOperations,
|
|
23
|
-
files: AgentInstructionFile[] = ["AGENTS.md", "CLAUDE.md", ".cursorrules"],
|
|
24
|
-
): Promise<void> {
|
|
25
|
-
const mapping: Record<AgentInstructionFile, string> = {
|
|
26
|
-
"AGENTS.md": AGENT_GUIDELINES,
|
|
27
|
-
"CLAUDE.md": CLAUDE_GUIDELINES,
|
|
28
|
-
".cursorrules": CURSOR_GUIDELINES,
|
|
29
|
-
"readme.md": README_GUIDELINES,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const paths: string[] = [];
|
|
33
|
-
for (const name of files) {
|
|
34
|
-
const content = await loadContent(mapping[name]);
|
|
35
|
-
const filePath = join(projectRoot, name);
|
|
36
|
-
let existing = "";
|
|
37
|
-
try {
|
|
38
|
-
existing = await Bun.file(filePath).text();
|
|
39
|
-
if (!existing.endsWith("\n")) existing += "\n";
|
|
40
|
-
existing += content;
|
|
41
|
-
} catch {
|
|
42
|
-
existing = content;
|
|
43
|
-
}
|
|
44
|
-
await Bun.write(filePath, existing);
|
|
45
|
-
paths.push(filePath);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (git && paths.length > 0) {
|
|
49
|
-
await git.addFiles(paths);
|
|
50
|
-
await git.commitChanges("Add AI agent instructions");
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export { loadContent as _loadAgentGuideline };
|
package/src/board.ts
DELETED
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
export interface BoardOptions {
|
|
2
|
-
statuses?: string[];
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
import { mkdir } from "node:fs/promises";
|
|
6
|
-
import { dirname } from "node:path";
|
|
7
|
-
import type { Task } from "./types/index.ts";
|
|
8
|
-
|
|
9
|
-
export type BoardLayout = "horizontal" | "vertical";
|
|
10
|
-
export type BoardFormat = "terminal" | "markdown";
|
|
11
|
-
|
|
12
|
-
interface DisplayTask {
|
|
13
|
-
id: string;
|
|
14
|
-
title: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function idSegments(id: string): number[] {
|
|
18
|
-
const normalized = id.startsWith("task-") ? id.slice(5) : id;
|
|
19
|
-
return normalized.split(".").map((part) => Number.parseInt(part, 10));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function compareIds(a: Task, b: Task): number {
|
|
23
|
-
const segA = idSegments(a.id);
|
|
24
|
-
const segB = idSegments(b.id);
|
|
25
|
-
const len = Math.max(segA.length, segB.length);
|
|
26
|
-
for (let i = 0; i < len; i++) {
|
|
27
|
-
const diff = (segA[i] ?? 0) - (segB[i] ?? 0);
|
|
28
|
-
if (diff !== 0) return diff;
|
|
29
|
-
}
|
|
30
|
-
return 0;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function wrapText(text: string, maxWidth: number): string[] {
|
|
34
|
-
if (text.length <= maxWidth) return [text];
|
|
35
|
-
|
|
36
|
-
const words = text.split(" ");
|
|
37
|
-
const lines: string[] = [];
|
|
38
|
-
let currentLine = "";
|
|
39
|
-
|
|
40
|
-
for (const word of words) {
|
|
41
|
-
if (currentLine.length + word.length + 1 <= maxWidth) {
|
|
42
|
-
currentLine += (currentLine ? " " : "") + word;
|
|
43
|
-
} else {
|
|
44
|
-
if (currentLine) lines.push(currentLine);
|
|
45
|
-
currentLine = word;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (currentLine) lines.push(currentLine);
|
|
50
|
-
return lines;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function generateKanbanBoard(
|
|
54
|
-
tasks: Task[],
|
|
55
|
-
statuses: string[] = [],
|
|
56
|
-
layout: BoardLayout = "horizontal",
|
|
57
|
-
maxColumnWidth = 20,
|
|
58
|
-
format: BoardFormat = "terminal",
|
|
59
|
-
): string {
|
|
60
|
-
const groups = new Map<string, Task[]>();
|
|
61
|
-
for (const task of tasks) {
|
|
62
|
-
const status = task.status || "";
|
|
63
|
-
const list = groups.get(status) || [];
|
|
64
|
-
list.push(task);
|
|
65
|
-
groups.set(status, list);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Map for quick lookup by id
|
|
69
|
-
const byId = new Map<string, Task>(tasks.map((t) => [t.id, t]));
|
|
70
|
-
|
|
71
|
-
// Only show statuses that have tasks (filter out empty groups)
|
|
72
|
-
const ordered =
|
|
73
|
-
tasks.length > 0
|
|
74
|
-
? [
|
|
75
|
-
...statuses.filter((s) => groups.has(s) && (groups.get(s)?.length ?? 0) > 0),
|
|
76
|
-
...Array.from(groups.keys()).filter((s) => !statuses.includes(s) && (groups.get(s)?.length ?? 0) > 0),
|
|
77
|
-
]
|
|
78
|
-
: [];
|
|
79
|
-
|
|
80
|
-
const columns: DisplayTask[][] = ordered.map((status) => {
|
|
81
|
-
const items = groups.get(status) || [];
|
|
82
|
-
const top: Task[] = [];
|
|
83
|
-
const children = new Map<string, Task[]>();
|
|
84
|
-
|
|
85
|
-
// Use compareIds for sorting instead of localeCompare
|
|
86
|
-
for (const t of items.sort(compareIds)) {
|
|
87
|
-
const parent = t.parentTaskId ? byId.get(t.parentTaskId) : undefined;
|
|
88
|
-
if (parent && parent.status === t.status) {
|
|
89
|
-
const list = children.get(parent.id) || [];
|
|
90
|
-
list.push(t);
|
|
91
|
-
children.set(parent.id, list);
|
|
92
|
-
} else {
|
|
93
|
-
top.push(t);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const result: DisplayTask[] = [];
|
|
98
|
-
for (const t of top) {
|
|
99
|
-
result.push({ id: t.id, title: t.title });
|
|
100
|
-
const subs = children.get(t.id) || [];
|
|
101
|
-
subs.sort(compareIds);
|
|
102
|
-
|
|
103
|
-
for (const [subIdx, s] of subs.entries()) {
|
|
104
|
-
const isLastSub = subIdx === subs.length - 1;
|
|
105
|
-
const prefix = isLastSub ? " └─" : " |—";
|
|
106
|
-
result.push({ id: `${prefix} ${s.id}`, title: ` ${s.title}` });
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return result;
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
if (layout === "vertical") {
|
|
114
|
-
const rows: string[] = [];
|
|
115
|
-
for (const [idx, status] of ordered.entries()) {
|
|
116
|
-
const header = status || "No Status";
|
|
117
|
-
rows.push(header);
|
|
118
|
-
rows.push("-".repeat(header.length));
|
|
119
|
-
const tasksInStatus = columns[idx];
|
|
120
|
-
for (const task of tasksInStatus) {
|
|
121
|
-
rows.push(task.id);
|
|
122
|
-
rows.push(task.title);
|
|
123
|
-
rows.push("");
|
|
124
|
-
}
|
|
125
|
-
if (tasksInStatus.length === 0) {
|
|
126
|
-
rows.push("");
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return rows.join("\n").trimEnd();
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Return empty string if no columns to show
|
|
133
|
-
if (ordered.length === 0) {
|
|
134
|
-
return "";
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const colWidths = ordered.map((status, idx) => {
|
|
138
|
-
const header = status || "No Status";
|
|
139
|
-
let width = Math.min(Math.max(header.length, 8), maxColumnWidth); // Minimum 8, max maxColumnWidth
|
|
140
|
-
for (const t of columns[idx]) {
|
|
141
|
-
// Check both task ID and title lengths separately
|
|
142
|
-
const idLength = t.id.length;
|
|
143
|
-
const titleLength = t.title.length;
|
|
144
|
-
const maxTaskWidth = Math.max(idLength, titleLength);
|
|
145
|
-
if (maxTaskWidth > width && width < maxColumnWidth) {
|
|
146
|
-
width = Math.min(maxTaskWidth, maxColumnWidth);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return width;
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// For markdown format, we need simpler output without text wrapping
|
|
153
|
-
if (format === "markdown") {
|
|
154
|
-
const headerRow = `| ${ordered.map((status) => status || "No Status").join(" | ")} |`;
|
|
155
|
-
const separatorRow = `| ${ordered.map(() => "---").join(" | ")} |`;
|
|
156
|
-
|
|
157
|
-
const maxTasks = Math.max(...columns.map((c) => c.length), 0);
|
|
158
|
-
const rows = [headerRow, separatorRow];
|
|
159
|
-
|
|
160
|
-
for (let taskIdx = 0; taskIdx < maxTasks; taskIdx++) {
|
|
161
|
-
const row = ordered.map((_, cIdx) => {
|
|
162
|
-
const task = columns[cIdx][taskIdx];
|
|
163
|
-
if (!task) return "";
|
|
164
|
-
// For markdown, combine ID and title in one cell
|
|
165
|
-
return `${task.id}: ${task.title}`;
|
|
166
|
-
});
|
|
167
|
-
rows.push(`| ${row.join(" | ")} |`);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return rows.join("\n");
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Terminal format with text wrapping
|
|
174
|
-
const pad = (text: string, width: number): string => text.padEnd(width, " ");
|
|
175
|
-
|
|
176
|
-
const headerRow = ordered.map((status, i) => pad(status || "No Status", colWidths[i])).join(" | ");
|
|
177
|
-
const separatorRow = ordered.map((_, i) => "-".repeat(colWidths[i])).join("-|-");
|
|
178
|
-
|
|
179
|
-
// Prepare wrapped tasks for each column
|
|
180
|
-
const wrappedTasks = ordered.map((_, cIdx) => {
|
|
181
|
-
return columns[cIdx].map((task) => ({
|
|
182
|
-
idLines: wrapText(task.id, colWidths[cIdx]),
|
|
183
|
-
titleLines: wrapText(task.title, colWidths[cIdx]),
|
|
184
|
-
}));
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
const maxTasks = Math.max(...columns.map((c) => c.length), 0);
|
|
188
|
-
const rows = [headerRow, separatorRow];
|
|
189
|
-
|
|
190
|
-
for (let taskIdx = 0; taskIdx < maxTasks; taskIdx++) {
|
|
191
|
-
// Get the maximum number of lines needed for this task across all columns
|
|
192
|
-
let maxTaskLines = 0;
|
|
193
|
-
for (let cIdx = 0; cIdx < ordered.length; cIdx++) {
|
|
194
|
-
if (wrappedTasks[cIdx][taskIdx]) {
|
|
195
|
-
const taskLines = wrappedTasks[cIdx][taskIdx].idLines.length + wrappedTasks[cIdx][taskIdx].titleLines.length;
|
|
196
|
-
maxTaskLines = Math.max(maxTaskLines, taskLines);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Render each line for this task
|
|
201
|
-
for (let lineIdx = 0; lineIdx < maxTaskLines; lineIdx++) {
|
|
202
|
-
const lineRow = ordered
|
|
203
|
-
.map((_, cIdx) => {
|
|
204
|
-
const wrappedTask = wrappedTasks[cIdx][taskIdx];
|
|
205
|
-
if (!wrappedTask) return pad("", colWidths[cIdx]);
|
|
206
|
-
|
|
207
|
-
const idLineCount = wrappedTask.idLines.length;
|
|
208
|
-
let text = "";
|
|
209
|
-
|
|
210
|
-
if (lineIdx < idLineCount) {
|
|
211
|
-
// Show ID lines first
|
|
212
|
-
text = wrappedTask.idLines[lineIdx];
|
|
213
|
-
} else {
|
|
214
|
-
// Then show title lines
|
|
215
|
-
const titleLineIdx = lineIdx - idLineCount;
|
|
216
|
-
if (titleLineIdx < wrappedTask.titleLines.length) {
|
|
217
|
-
text = wrappedTask.titleLines[titleLineIdx];
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return pad(text, colWidths[cIdx]);
|
|
222
|
-
})
|
|
223
|
-
.join(" | ");
|
|
224
|
-
rows.push(lineRow);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Add empty row between tasks for better separation (except after last task)
|
|
228
|
-
// Skip empty row if next task is a subtask (to keep parent and child together)
|
|
229
|
-
if (taskIdx < maxTasks - 1) {
|
|
230
|
-
const emptyRow = ordered.map((_, cIdx) => pad("", colWidths[cIdx])).join(" | ");
|
|
231
|
-
rows.push(emptyRow);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return rows.join("\n");
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
export async function exportKanbanBoardToFile(
|
|
239
|
-
tasks: Task[],
|
|
240
|
-
statuses: string[],
|
|
241
|
-
filePath: string,
|
|
242
|
-
maxColumnWidth = 20,
|
|
243
|
-
addTitle = false,
|
|
244
|
-
): Promise<void> {
|
|
245
|
-
const board = generateKanbanBoard(tasks, statuses, "horizontal", maxColumnWidth, "markdown");
|
|
246
|
-
|
|
247
|
-
let existing = "";
|
|
248
|
-
try {
|
|
249
|
-
existing = await Bun.file(filePath).text();
|
|
250
|
-
} catch {
|
|
251
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Add proper spacing and title for readme export
|
|
255
|
-
let boardContent = board;
|
|
256
|
-
if (addTitle && filePath.toLowerCase().includes("readme")) {
|
|
257
|
-
boardContent = `\n\n## Project Board\n\n${board}`;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const needsNewline = existing && !existing.endsWith("\n");
|
|
261
|
-
const content = `${existing}${needsNewline ? "\n" : ""}${boardContent}\n`;
|
|
262
|
-
await Bun.write(filePath, content);
|
|
263
|
-
}
|