ralphctl 0.1.0 → 0.1.2
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 +58 -24
- package/dist/add-HGJCLWED.mjs +14 -0
- package/dist/add-MRGCS3US.mjs +14 -0
- package/dist/chunk-6PYTKGB5.mjs +316 -0
- package/dist/chunk-7TG3EAQ2.mjs +20 -0
- package/dist/chunk-EKMZZRWI.mjs +521 -0
- package/dist/chunk-JON4GCLR.mjs +59 -0
- package/dist/chunk-LOR7QBXX.mjs +3683 -0
- package/dist/chunk-MNMQC36F.mjs +556 -0
- package/dist/chunk-MRKOFVTM.mjs +537 -0
- package/dist/chunk-NTWO2LXB.mjs +52 -0
- package/dist/chunk-QBXHAXHI.mjs +562 -0
- package/dist/chunk-WGHJI3OI.mjs +214 -0
- package/dist/cli.mjs +4245 -0
- package/dist/create-MG7E7PLQ.mjs +10 -0
- package/dist/handle-UG5M2OON.mjs +22 -0
- package/dist/multiline-OHSNFCRG.mjs +40 -0
- package/dist/project-NT3L4FTB.mjs +28 -0
- package/dist/resolver-WSFWKACM.mjs +153 -0
- package/dist/sprint-4VHDLGFN.mjs +37 -0
- package/dist/wizard-LRELAN2J.mjs +196 -0
- package/package.json +19 -28
- package/CHANGELOG.md +0 -94
- package/bin/ralphctl +0 -13
- package/src/ai/executor.ts +0 -973
- package/src/ai/lifecycle.ts +0 -45
- package/src/ai/parser.ts +0 -40
- package/src/ai/permissions.ts +0 -207
- package/src/ai/process-manager.ts +0 -248
- package/src/ai/prompts/index.ts +0 -89
- package/src/ai/rate-limiter.ts +0 -89
- package/src/ai/runner.ts +0 -478
- package/src/ai/session.ts +0 -319
- package/src/ai/task-context.ts +0 -270
- package/src/cli-metadata.ts +0 -7
- package/src/cli.ts +0 -65
- package/src/commands/completion/index.ts +0 -33
- package/src/commands/config/config.ts +0 -58
- package/src/commands/config/index.ts +0 -33
- package/src/commands/dashboard/dashboard.ts +0 -5
- package/src/commands/dashboard/index.ts +0 -6
- package/src/commands/doctor/doctor.ts +0 -271
- package/src/commands/doctor/index.ts +0 -25
- package/src/commands/progress/index.ts +0 -25
- package/src/commands/progress/log.ts +0 -64
- package/src/commands/progress/show.ts +0 -14
- package/src/commands/project/add.ts +0 -336
- package/src/commands/project/index.ts +0 -104
- package/src/commands/project/list.ts +0 -31
- package/src/commands/project/remove.ts +0 -43
- package/src/commands/project/repo.ts +0 -118
- package/src/commands/project/show.ts +0 -49
- package/src/commands/sprint/close.ts +0 -180
- package/src/commands/sprint/context.ts +0 -109
- package/src/commands/sprint/create.ts +0 -60
- package/src/commands/sprint/current.ts +0 -75
- package/src/commands/sprint/delete.ts +0 -72
- package/src/commands/sprint/health.ts +0 -229
- package/src/commands/sprint/ideate.ts +0 -496
- package/src/commands/sprint/index.ts +0 -226
- package/src/commands/sprint/list.ts +0 -86
- package/src/commands/sprint/plan-utils.ts +0 -207
- package/src/commands/sprint/plan.ts +0 -549
- package/src/commands/sprint/refine.ts +0 -359
- package/src/commands/sprint/requirements.ts +0 -58
- package/src/commands/sprint/show.ts +0 -140
- package/src/commands/sprint/start.ts +0 -119
- package/src/commands/sprint/switch.ts +0 -20
- package/src/commands/task/add.ts +0 -316
- package/src/commands/task/import.ts +0 -150
- package/src/commands/task/index.ts +0 -123
- package/src/commands/task/list.ts +0 -145
- package/src/commands/task/next.ts +0 -45
- package/src/commands/task/remove.ts +0 -47
- package/src/commands/task/reorder.ts +0 -45
- package/src/commands/task/show.ts +0 -111
- package/src/commands/task/status.ts +0 -99
- package/src/commands/ticket/add.ts +0 -265
- package/src/commands/ticket/edit.ts +0 -166
- package/src/commands/ticket/index.ts +0 -114
- package/src/commands/ticket/list.ts +0 -128
- package/src/commands/ticket/refine-utils.ts +0 -89
- package/src/commands/ticket/refine.ts +0 -268
- package/src/commands/ticket/remove.ts +0 -48
- package/src/commands/ticket/show.ts +0 -74
- package/src/completion/handle.ts +0 -30
- package/src/completion/resolver.ts +0 -241
- package/src/interactive/dashboard.ts +0 -268
- package/src/interactive/escapable.ts +0 -81
- package/src/interactive/file-browser.ts +0 -153
- package/src/interactive/index.ts +0 -429
- package/src/interactive/menu.ts +0 -403
- package/src/interactive/selectors.ts +0 -273
- package/src/interactive/wizard.ts +0 -221
- package/src/providers/claude.ts +0 -53
- package/src/providers/copilot.ts +0 -86
- package/src/providers/index.ts +0 -43
- package/src/providers/types.ts +0 -85
- package/src/schemas/index.ts +0 -130
- package/src/store/config.ts +0 -74
- package/src/store/progress.ts +0 -230
- package/src/store/project.ts +0 -276
- package/src/store/sprint.ts +0 -229
- package/src/store/task.ts +0 -443
- package/src/store/ticket.ts +0 -178
- package/src/theme/index.ts +0 -215
- package/src/theme/ui.ts +0 -872
- package/src/utils/detect-scripts.ts +0 -247
- package/src/utils/editor-input.ts +0 -41
- package/src/utils/editor.ts +0 -37
- package/src/utils/exit-codes.ts +0 -27
- package/src/utils/file-lock.ts +0 -135
- package/src/utils/git.ts +0 -185
- package/src/utils/ids.ts +0 -37
- package/src/utils/issue-fetch.ts +0 -244
- package/src/utils/json-extract.ts +0 -62
- package/src/utils/multiline.ts +0 -61
- package/src/utils/path-selector.ts +0 -236
- package/src/utils/paths.ts +0 -108
- package/src/utils/provider.ts +0 -34
- package/src/utils/requirements-export.ts +0 -63
- package/src/utils/storage.ts +0 -107
- package/tsconfig.json +0 -25
- /package/{src/ai → dist}/prompts/ideate-auto.md +0 -0
- /package/{src/ai → dist}/prompts/ideate.md +0 -0
- /package/{src/ai → dist}/prompts/plan-auto.md +0 -0
- /package/{src/ai → dist}/prompts/plan-common.md +0 -0
- /package/{src/ai → dist}/prompts/plan-interactive.md +0 -0
- /package/{src/ai → dist}/prompts/task-execution.md +0 -0
- /package/{src/ai → dist}/prompts/ticket-refine.md +0 -0
package/README.md
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
[](https://www.npmjs.com/package/ralphctl)
|
|
2
|
+
[](https://www.npmjs.com/package/ralphctl)
|
|
2
3
|
[](https://github.com/lukas-grigis/ralphctl/actions/workflows/ci.yml)
|
|
3
4
|
[](./LICENSE)
|
|
4
5
|
[](https://www.typescriptlang.org/)
|
|
5
6
|
[](https://nodejs.org/)
|
|
7
|
+
[](https://prettier.io/)
|
|
8
|
+
[](https://eslint.org/)
|
|
6
9
|
[](./CONTRIBUTING.md)
|
|
10
|
+
[](https://docs.anthropic.com/en/docs/claude-code)
|
|
11
|
+
[](https://docs.github.com/en/copilot/github-copilot-in-the-cli)
|
|
12
|
+
[](https://github.com/lukas-grigis/ralphctl)
|
|
7
13
|
|
|
8
14
|
```
|
|
9
15
|
🍩 ██████╗ █████╗ ██╗ ██████╗ ██╗ ██╗ ██████╗████████╗██╗ 🍩
|
|
@@ -87,30 +93,47 @@ Or just run `ralphctl` with no arguments for an interactive menu that walks you
|
|
|
87
93
|
|
|
88
94
|
## CLI Overview
|
|
89
95
|
|
|
96
|
+
### Getting Started
|
|
97
|
+
|
|
98
|
+
| Command | Description |
|
|
99
|
+
| ------------------------------------------------ | ----------------------------------- |
|
|
100
|
+
| `ralphctl` | Interactive menu mode (recommended) |
|
|
101
|
+
| `ralphctl doctor` | Check environment health |
|
|
102
|
+
| `ralphctl config set provider <claude\|copilot>` | Set AI provider |
|
|
103
|
+
| `ralphctl config show` | Show current configuration |
|
|
104
|
+
| `ralphctl completion install` | Enable shell tab-completion |
|
|
105
|
+
|
|
106
|
+
### Project & Sprint Setup
|
|
107
|
+
|
|
108
|
+
| Command | Description |
|
|
109
|
+
| ------------------------ | -------------------------------- |
|
|
110
|
+
| `ralphctl project add` | Register a project and its repos |
|
|
111
|
+
| `ralphctl sprint create` | Create a new sprint (draft) |
|
|
112
|
+
| `ralphctl sprint list` | List all sprints |
|
|
113
|
+
| `ralphctl sprint show` | Show current sprint details |
|
|
114
|
+
| `ralphctl sprint switch` | Quick sprint switcher |
|
|
115
|
+
| `ralphctl ticket add` | Add a work item to a sprint |
|
|
116
|
+
|
|
117
|
+
### AI-Assisted Planning
|
|
118
|
+
|
|
90
119
|
| Command | Description |
|
|
91
120
|
| ------------------------------ | --------------------------------------- |
|
|
92
|
-
| `ralphctl`
|
|
93
|
-
| `ralphctl
|
|
94
|
-
| `ralphctl config show` | Show current configuration |
|
|
95
|
-
| `ralphctl config set` | Set configuration values |
|
|
96
|
-
| `ralphctl project add` | Register a project and its repos |
|
|
97
|
-
| `ralphctl sprint create` | Create a new sprint |
|
|
98
|
-
| `ralphctl sprint list` | List all sprints |
|
|
99
|
-
| `ralphctl sprint show` | Show current sprint details |
|
|
100
|
-
| `ralphctl sprint switch` | Quick sprint switcher |
|
|
101
|
-
| `ralphctl ticket add` | Add a work item to a sprint |
|
|
102
|
-
| `ralphctl sprint refine` | Refine requirements with AI |
|
|
103
|
-
| `ralphctl sprint plan` | Generate tasks from requirements |
|
|
121
|
+
| `ralphctl sprint refine` | Clarify requirements with AI (WHAT) |
|
|
122
|
+
| `ralphctl sprint plan` | Generate tasks from requirements (HOW) |
|
|
104
123
|
| `ralphctl sprint ideate` | Quick single-session refine + plan |
|
|
105
124
|
| `ralphctl sprint requirements` | Export refined requirements to markdown |
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
|
110
|
-
|
|
|
111
|
-
| `ralphctl
|
|
112
|
-
| `ralphctl
|
|
113
|
-
| `ralphctl
|
|
125
|
+
|
|
126
|
+
### Execution & Monitoring
|
|
127
|
+
|
|
128
|
+
| Command | Description |
|
|
129
|
+
| ------------------------ | --------------------------------- |
|
|
130
|
+
| `ralphctl sprint start` | Execute tasks with AI |
|
|
131
|
+
| `ralphctl sprint health` | Diagnose blockers and stale tasks |
|
|
132
|
+
| `ralphctl dashboard` | Sprint overview with progress bar |
|
|
133
|
+
| `ralphctl task list` | List tasks in the current sprint |
|
|
134
|
+
| `ralphctl task next` | Show the next unblocked task |
|
|
135
|
+
| `ralphctl sprint close` | Close an active sprint |
|
|
136
|
+
| `ralphctl sprint delete` | Delete a sprint permanently |
|
|
114
137
|
|
|
115
138
|
Run `ralphctl <command> --help` for details on any command.
|
|
116
139
|
|
|
@@ -127,6 +150,18 @@ ralphctl config set provider copilot # Use GitHub Copilot
|
|
|
127
150
|
|
|
128
151
|
Auto-prompts on first AI command if not set. Both CLIs must be in your PATH and authenticated.
|
|
129
152
|
|
|
153
|
+
### Provider Differences
|
|
154
|
+
|
|
155
|
+
| Feature | Claude Code | GitHub Copilot |
|
|
156
|
+
| --------------------------- | ------------------------------------ | -------------------------------------------------------------------- |
|
|
157
|
+
| Status | GA | Public preview |
|
|
158
|
+
| Headless execution | `-p --output-format json` | `-p -s --autopilot --no-ask-user` |
|
|
159
|
+
| Session IDs | In JSON output (`session_id`) | Captured via `--share` output file |
|
|
160
|
+
| Session resume (`--resume`) | Full support | Supported when session ID is available |
|
|
161
|
+
| Per-tool permissions | Settings files + `--permission-mode` | `--allow-all-tools` (all-or-nothing by default) |
|
|
162
|
+
| Fine-grained tool control | `allow`/`deny` in settings files | `--allow-tool`, `--deny-tool` flags (not yet used) |
|
|
163
|
+
| Rate limit detection | Validated patterns | Borrowed from Claude — not yet validated against real Copilot errors |
|
|
164
|
+
|
|
130
165
|
---
|
|
131
166
|
|
|
132
167
|
## Documentation
|
|
@@ -158,14 +193,13 @@ export RALPHCTL_ROOT="/path/to/custom/data-dir"
|
|
|
158
193
|
git clone https://github.com/lukas-grigis/ralphctl.git
|
|
159
194
|
cd ralphctl
|
|
160
195
|
pnpm install
|
|
161
|
-
pnpm dev --help # Run CLI in dev mode
|
|
162
|
-
pnpm build #
|
|
196
|
+
pnpm dev --help # Run CLI in dev mode (tsx, no build needed)
|
|
197
|
+
pnpm build # Compile for npm distribution (tsup)
|
|
198
|
+
pnpm typecheck # Type check
|
|
163
199
|
pnpm test # Run tests
|
|
164
200
|
pnpm lint # Lint
|
|
165
201
|
```
|
|
166
202
|
|
|
167
|
-
No compilation step — the CLI runs TypeScript source directly via [tsx](https://tsx.is/).
|
|
168
|
-
|
|
169
203
|
---
|
|
170
204
|
|
|
171
205
|
## Contributing
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
addSingleTicketInteractive,
|
|
4
|
+
ticketAddCommand
|
|
5
|
+
} from "./chunk-MNMQC36F.mjs";
|
|
6
|
+
import "./chunk-7TG3EAQ2.mjs";
|
|
7
|
+
import "./chunk-WGHJI3OI.mjs";
|
|
8
|
+
import "./chunk-EKMZZRWI.mjs";
|
|
9
|
+
import "./chunk-6PYTKGB5.mjs";
|
|
10
|
+
import "./chunk-QBXHAXHI.mjs";
|
|
11
|
+
export {
|
|
12
|
+
addSingleTicketInteractive,
|
|
13
|
+
ticketAddCommand
|
|
14
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
addCheckScriptToRepository,
|
|
4
|
+
projectAddCommand
|
|
5
|
+
} from "./chunk-MRKOFVTM.mjs";
|
|
6
|
+
import "./chunk-NTWO2LXB.mjs";
|
|
7
|
+
import "./chunk-7TG3EAQ2.mjs";
|
|
8
|
+
import "./chunk-WGHJI3OI.mjs";
|
|
9
|
+
import "./chunk-6PYTKGB5.mjs";
|
|
10
|
+
import "./chunk-QBXHAXHI.mjs";
|
|
11
|
+
export {
|
|
12
|
+
addCheckScriptToRepository,
|
|
13
|
+
projectAddCommand
|
|
14
|
+
};
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/utils/paths.ts
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { dirname, isAbsolute, join, resolve, sep } from "path";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
import { lstat, realpath, stat } from "fs/promises";
|
|
9
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
var __dirname = dirname(__filename);
|
|
11
|
+
function getRepoRoot() {
|
|
12
|
+
let dir = __dirname;
|
|
13
|
+
while (dir !== dirname(dir)) {
|
|
14
|
+
if (existsSync(join(dir, "package.json"))) {
|
|
15
|
+
return dir;
|
|
16
|
+
}
|
|
17
|
+
dir = dirname(dir);
|
|
18
|
+
}
|
|
19
|
+
return join(__dirname, "..", "..");
|
|
20
|
+
}
|
|
21
|
+
function getDataDir() {
|
|
22
|
+
return process.env["RALPHCTL_ROOT"] ?? join(homedir(), ".ralphctl");
|
|
23
|
+
}
|
|
24
|
+
function getConfigPath() {
|
|
25
|
+
return join(getDataDir(), "config.json");
|
|
26
|
+
}
|
|
27
|
+
function getProjectsFilePath() {
|
|
28
|
+
return join(getDataDir(), "projects.json");
|
|
29
|
+
}
|
|
30
|
+
function getSprintsDir() {
|
|
31
|
+
return join(getDataDir(), "sprints");
|
|
32
|
+
}
|
|
33
|
+
function getSprintDir(sprintId) {
|
|
34
|
+
const sprintsDir = getSprintsDir();
|
|
35
|
+
const resolved = resolve(sprintsDir, sprintId);
|
|
36
|
+
if (!resolved.startsWith(sprintsDir + sep) && resolved !== sprintsDir) {
|
|
37
|
+
throw new Error(`Path traversal detected in sprint ID: ${sprintId}`);
|
|
38
|
+
}
|
|
39
|
+
return resolved;
|
|
40
|
+
}
|
|
41
|
+
function getSprintFilePath(sprintId) {
|
|
42
|
+
return join(getSprintDir(sprintId), "sprint.json");
|
|
43
|
+
}
|
|
44
|
+
function getTasksFilePath(sprintId) {
|
|
45
|
+
return join(getSprintDir(sprintId), "tasks.json");
|
|
46
|
+
}
|
|
47
|
+
function getProgressFilePath(sprintId) {
|
|
48
|
+
return join(getSprintDir(sprintId), "progress.md");
|
|
49
|
+
}
|
|
50
|
+
function getRefinementDir(sprintId, ticketId) {
|
|
51
|
+
return join(getSprintDir(sprintId), "refinement", ticketId);
|
|
52
|
+
}
|
|
53
|
+
function getPlanningDir(sprintId) {
|
|
54
|
+
return join(getSprintDir(sprintId), "planning");
|
|
55
|
+
}
|
|
56
|
+
function getIdeateDir(sprintId, ticketId) {
|
|
57
|
+
return join(getSprintDir(sprintId), "ideation", ticketId);
|
|
58
|
+
}
|
|
59
|
+
function getSchemaPath(schemaName) {
|
|
60
|
+
return join(getRepoRoot(), "schemas", schemaName);
|
|
61
|
+
}
|
|
62
|
+
function assertSafeCwd(path) {
|
|
63
|
+
if (!path || path.includes("\0") || path.includes("\n") || path.includes("\r")) {
|
|
64
|
+
throw new Error("Unsafe path for cwd: contains null bytes or newlines");
|
|
65
|
+
}
|
|
66
|
+
if (!isAbsolute(path)) {
|
|
67
|
+
throw new Error(`Unsafe path for cwd: must be absolute, got: ${path}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function expandTilde(path) {
|
|
71
|
+
if (path === "~") return homedir();
|
|
72
|
+
if (path.startsWith("~/")) return homedir() + path.slice(1);
|
|
73
|
+
return path;
|
|
74
|
+
}
|
|
75
|
+
async function validateProjectPath(path) {
|
|
76
|
+
try {
|
|
77
|
+
const resolved = resolve(expandTilde(path));
|
|
78
|
+
const lstats = await lstat(resolved);
|
|
79
|
+
if (lstats.isSymbolicLink()) {
|
|
80
|
+
const realPath = await realpath(resolved);
|
|
81
|
+
const realStats = await stat(realPath);
|
|
82
|
+
if (!realStats.isDirectory()) {
|
|
83
|
+
return "Symlink target is not a directory";
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if (!lstats.isDirectory()) {
|
|
88
|
+
return "Path is not a directory";
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
} catch {
|
|
92
|
+
return "Directory does not exist";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/utils/storage.ts
|
|
97
|
+
import { access, appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
|
|
98
|
+
import { dirname as dirname2 } from "path";
|
|
99
|
+
var ValidationError = class extends Error {
|
|
100
|
+
path;
|
|
101
|
+
constructor(message, path, cause) {
|
|
102
|
+
super(message, { cause });
|
|
103
|
+
this.name = "ValidationError";
|
|
104
|
+
this.path = path;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
var FileNotFoundError = class extends Error {
|
|
108
|
+
path;
|
|
109
|
+
constructor(message, path) {
|
|
110
|
+
super(message);
|
|
111
|
+
this.name = "FileNotFoundError";
|
|
112
|
+
this.path = path;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
async function ensureDir(dirPath) {
|
|
116
|
+
await mkdir(dirPath, { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
async function removeDir(dirPath) {
|
|
119
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
async function fileExists(filePath) {
|
|
122
|
+
try {
|
|
123
|
+
await access(filePath);
|
|
124
|
+
return true;
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function listDirs(dirPath) {
|
|
130
|
+
try {
|
|
131
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
132
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
133
|
+
} catch {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function readValidatedJson(filePath, schema) {
|
|
138
|
+
let content;
|
|
139
|
+
try {
|
|
140
|
+
content = await readFile(filePath, "utf-8");
|
|
141
|
+
} catch (err) {
|
|
142
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
143
|
+
throw new FileNotFoundError(`File not found: ${filePath}`, filePath);
|
|
144
|
+
}
|
|
145
|
+
throw err;
|
|
146
|
+
}
|
|
147
|
+
let data;
|
|
148
|
+
try {
|
|
149
|
+
data = JSON.parse(content);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
throw new ValidationError(`Invalid JSON in ${filePath}`, filePath, err);
|
|
152
|
+
}
|
|
153
|
+
const result = schema.safeParse(data);
|
|
154
|
+
if (!result.success) {
|
|
155
|
+
const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
156
|
+
throw new ValidationError(`Validation failed for ${filePath}:
|
|
157
|
+
${issues}`, filePath, result.error);
|
|
158
|
+
}
|
|
159
|
+
return result.data;
|
|
160
|
+
}
|
|
161
|
+
async function writeValidatedJson(filePath, data, schema) {
|
|
162
|
+
const result = schema.safeParse(data);
|
|
163
|
+
if (!result.success) {
|
|
164
|
+
const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
165
|
+
throw new ValidationError(`Validation failed before writing to ${filePath}:
|
|
166
|
+
${issues}`, filePath, result.error);
|
|
167
|
+
}
|
|
168
|
+
await ensureDir(dirname2(filePath));
|
|
169
|
+
await writeFile(filePath, JSON.stringify(result.data, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
170
|
+
}
|
|
171
|
+
async function appendToFile(filePath, content) {
|
|
172
|
+
await ensureDir(dirname2(filePath));
|
|
173
|
+
await appendFile(filePath, content, { encoding: "utf-8", mode: 384 });
|
|
174
|
+
}
|
|
175
|
+
async function readTextFile(filePath) {
|
|
176
|
+
try {
|
|
177
|
+
return await readFile(filePath, "utf-8");
|
|
178
|
+
} catch (err) {
|
|
179
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
180
|
+
throw new FileNotFoundError(`File not found: ${filePath}`, filePath);
|
|
181
|
+
}
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/schemas/index.ts
|
|
187
|
+
import { z } from "zod";
|
|
188
|
+
var SprintStatusSchema = z.enum(["draft", "active", "closed"]);
|
|
189
|
+
var TaskStatusSchema = z.enum(["todo", "in_progress", "done"]);
|
|
190
|
+
var RequirementStatusSchema = z.enum(["pending", "approved"]);
|
|
191
|
+
var RepositorySchema = z.object({
|
|
192
|
+
name: z.string().min(1),
|
|
193
|
+
// Auto-derived from basename(path)
|
|
194
|
+
path: z.string().min(1),
|
|
195
|
+
// Absolute path
|
|
196
|
+
checkScript: z.string().optional()
|
|
197
|
+
// e.g., "pnpm install && pnpm typecheck && pnpm lint && pnpm test"
|
|
198
|
+
});
|
|
199
|
+
var ProjectSchema = z.object({
|
|
200
|
+
name: z.string().min(1).regex(/^[a-z0-9-]+$/, "Project name must be a slug (lowercase, numbers, hyphens only)"),
|
|
201
|
+
displayName: z.string().min(1),
|
|
202
|
+
repositories: z.array(RepositorySchema).min(1),
|
|
203
|
+
description: z.string().optional()
|
|
204
|
+
});
|
|
205
|
+
var ProjectsSchema = z.array(ProjectSchema);
|
|
206
|
+
var TicketSchema = z.object({
|
|
207
|
+
id: z.string().min(1),
|
|
208
|
+
// Internal UUID8 (auto-generated)
|
|
209
|
+
title: z.string().min(1),
|
|
210
|
+
description: z.string().optional(),
|
|
211
|
+
link: z.url().optional(),
|
|
212
|
+
projectName: z.string().min(1),
|
|
213
|
+
// References Project.name
|
|
214
|
+
affectedRepositories: z.array(z.string()).optional(),
|
|
215
|
+
// Repository paths selected during planning
|
|
216
|
+
requirementStatus: RequirementStatusSchema.default("pending"),
|
|
217
|
+
requirements: z.string().optional()
|
|
218
|
+
// Refined requirements (set during sprint refine)
|
|
219
|
+
});
|
|
220
|
+
var TaskSchema = z.object({
|
|
221
|
+
id: z.string().min(1),
|
|
222
|
+
// UUID8
|
|
223
|
+
name: z.string().min(1),
|
|
224
|
+
description: z.string().optional(),
|
|
225
|
+
steps: z.array(z.string()).default([]),
|
|
226
|
+
status: TaskStatusSchema.default("todo"),
|
|
227
|
+
order: z.number().int().positive(),
|
|
228
|
+
ticketId: z.string().optional(),
|
|
229
|
+
// References Ticket.id (internal)
|
|
230
|
+
blockedBy: z.array(z.string()).default([]),
|
|
231
|
+
projectPath: z.string().min(1),
|
|
232
|
+
// Single path for execution
|
|
233
|
+
verified: z.boolean().default(false),
|
|
234
|
+
// Whether verification passed
|
|
235
|
+
verificationOutput: z.string().optional()
|
|
236
|
+
// Output from verification run
|
|
237
|
+
});
|
|
238
|
+
var TasksSchema = z.array(TaskSchema);
|
|
239
|
+
var ImportTaskSchema = z.object({
|
|
240
|
+
id: z.string().optional(),
|
|
241
|
+
// Local ID for referencing in blockedBy
|
|
242
|
+
name: z.string().min(1),
|
|
243
|
+
// Required
|
|
244
|
+
description: z.string().optional(),
|
|
245
|
+
steps: z.array(z.string()).optional(),
|
|
246
|
+
ticketId: z.string().optional(),
|
|
247
|
+
blockedBy: z.array(z.string()).optional(),
|
|
248
|
+
projectPath: z.string().min(1)
|
|
249
|
+
// Required - execution directory
|
|
250
|
+
});
|
|
251
|
+
var ImportTasksSchema = z.array(ImportTaskSchema);
|
|
252
|
+
var RefinedRequirementSchema = z.object({
|
|
253
|
+
ref: z.string().min(1),
|
|
254
|
+
requirements: z.string().min(1)
|
|
255
|
+
});
|
|
256
|
+
var RefinedRequirementsSchema = z.array(RefinedRequirementSchema);
|
|
257
|
+
var IdeateOutputSchema = z.object({
|
|
258
|
+
requirements: z.string().min(1),
|
|
259
|
+
tasks: ImportTasksSchema
|
|
260
|
+
});
|
|
261
|
+
var SprintSchema = z.object({
|
|
262
|
+
id: z.string().regex(/^\d{8}-\d{6}-[a-z0-9-]+$/, "Invalid sprint ID format"),
|
|
263
|
+
name: z.string().min(1),
|
|
264
|
+
status: SprintStatusSchema.default("draft"),
|
|
265
|
+
createdAt: z.iso.datetime(),
|
|
266
|
+
activatedAt: z.iso.datetime().nullable().default(null),
|
|
267
|
+
closedAt: z.iso.datetime().nullable().default(null),
|
|
268
|
+
tickets: z.array(TicketSchema).default([]),
|
|
269
|
+
checkRanAt: z.record(z.string(), z.iso.datetime()).default({}),
|
|
270
|
+
branch: z.string().nullable().default(null)
|
|
271
|
+
});
|
|
272
|
+
var AiProviderSchema = z.enum(["claude", "copilot"]);
|
|
273
|
+
var ConfigSchema = z.object({
|
|
274
|
+
currentSprint: z.string().nullable().default(null),
|
|
275
|
+
aiProvider: AiProviderSchema.nullable().default(null),
|
|
276
|
+
editor: z.string().nullable().default(null)
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
export {
|
|
280
|
+
getDataDir,
|
|
281
|
+
getConfigPath,
|
|
282
|
+
getProjectsFilePath,
|
|
283
|
+
getSprintsDir,
|
|
284
|
+
getSprintDir,
|
|
285
|
+
getSprintFilePath,
|
|
286
|
+
getTasksFilePath,
|
|
287
|
+
getProgressFilePath,
|
|
288
|
+
getRefinementDir,
|
|
289
|
+
getPlanningDir,
|
|
290
|
+
getIdeateDir,
|
|
291
|
+
getSchemaPath,
|
|
292
|
+
assertSafeCwd,
|
|
293
|
+
expandTilde,
|
|
294
|
+
validateProjectPath,
|
|
295
|
+
ValidationError,
|
|
296
|
+
FileNotFoundError,
|
|
297
|
+
ensureDir,
|
|
298
|
+
removeDir,
|
|
299
|
+
fileExists,
|
|
300
|
+
listDirs,
|
|
301
|
+
readValidatedJson,
|
|
302
|
+
writeValidatedJson,
|
|
303
|
+
appendToFile,
|
|
304
|
+
readTextFile,
|
|
305
|
+
SprintStatusSchema,
|
|
306
|
+
TaskStatusSchema,
|
|
307
|
+
RequirementStatusSchema,
|
|
308
|
+
ProjectsSchema,
|
|
309
|
+
TasksSchema,
|
|
310
|
+
ImportTasksSchema,
|
|
311
|
+
RefinedRequirementsSchema,
|
|
312
|
+
IdeateOutputSchema,
|
|
313
|
+
SprintSchema,
|
|
314
|
+
AiProviderSchema,
|
|
315
|
+
ConfigSchema
|
|
316
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/utils/exit-codes.ts
|
|
4
|
+
var EXIT_SUCCESS = 0;
|
|
5
|
+
var EXIT_ERROR = 1;
|
|
6
|
+
var EXIT_NO_TASKS = 2;
|
|
7
|
+
var EXIT_ALL_BLOCKED = 3;
|
|
8
|
+
var EXIT_INTERRUPTED = 130;
|
|
9
|
+
function exitWithCode(code) {
|
|
10
|
+
process.exit(code);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
EXIT_SUCCESS,
|
|
15
|
+
EXIT_ERROR,
|
|
16
|
+
EXIT_NO_TASKS,
|
|
17
|
+
EXIT_ALL_BLOCKED,
|
|
18
|
+
EXIT_INTERRUPTED,
|
|
19
|
+
exitWithCode
|
|
20
|
+
};
|