draft-board 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/app/backend/.env.example +9 -0
  2. package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_patch.txt +195 -0
  3. package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_stat.txt +6 -0
  4. package/app/backend/CURL_EXAMPLES.md +335 -0
  5. package/app/backend/ENV_SETUP.md +65 -0
  6. package/app/backend/alembic/env.py +71 -0
  7. package/app/backend/alembic/script.py.mako +28 -0
  8. package/app/backend/alembic/versions/001_initial_schema.py +104 -0
  9. package/app/backend/alembic/versions/002_add_jobs_table.py +52 -0
  10. package/app/backend/alembic/versions/003_add_workspace_table.py +48 -0
  11. package/app/backend/alembic/versions/004_add_evidence_table.py +56 -0
  12. package/app/backend/alembic/versions/005_add_verification_commands.py +32 -0
  13. package/app/backend/alembic/versions/006_add_planner_lock_table.py +39 -0
  14. package/app/backend/alembic/versions/007_add_revision_review_tables.py +126 -0
  15. package/app/backend/alembic/versions/008_add_revision_idempotency_and_traceability.py +52 -0
  16. package/app/backend/alembic/versions/009_add_job_health_fields.py +46 -0
  17. package/app/backend/alembic/versions/010_add_review_comment_line_content.py +36 -0
  18. package/app/backend/alembic/versions/011_add_analysis_cache.py +47 -0
  19. package/app/backend/alembic/versions/012_add_boards_table.py +102 -0
  20. package/app/backend/alembic/versions/013_add_ticket_blocking.py +45 -0
  21. package/app/backend/alembic/versions/014_add_agent_sessions.py +220 -0
  22. package/app/backend/alembic/versions/015_add_ticket_sort_order.py +33 -0
  23. package/app/backend/alembic/versions/03220f0b93ae_add_pr_fields_to_ticket.py +49 -0
  24. package/app/backend/alembic/versions/0c2d89fff3b1_seed_board_configs_from_yaml.py +206 -0
  25. package/app/backend/alembic/versions/3348e5cf54c1_add_merge_checklist_table.py +67 -0
  26. package/app/backend/alembic/versions/357c780ee445_add_goal_status.py +34 -0
  27. package/app/backend/alembic/versions/553340b7e26c_add_autonomy_fields_to_goal.py +65 -0
  28. package/app/backend/alembic/versions/774dc335c679_merge_migration_heads.py +23 -0
  29. package/app/backend/alembic/versions/7b307e847cbd_merge_heads.py +23 -0
  30. package/app/backend/alembic/versions/82ecd978cc70_add_missing_indexes.py +48 -0
  31. package/app/backend/alembic/versions/8ef5054dc280_add_normalized_log_entries.py +173 -0
  32. package/app/backend/alembic/versions/8f3e2bd8ea3b_merge_migration_heads.py +23 -0
  33. package/app/backend/alembic/versions/9d17f0698d3b_add_config_column_to_boards_table.py +30 -0
  34. package/app/backend/alembic/versions/add_agent_conversation_history.py +72 -0
  35. package/app/backend/alembic/versions/add_job_variant.py +34 -0
  36. package/app/backend/alembic/versions/add_performance_indexes.py +95 -0
  37. package/app/backend/alembic/versions/add_repos_and_board_repos.py +174 -0
  38. package/app/backend/alembic/versions/add_session_id_to_jobs.py +27 -0
  39. package/app/backend/alembic/versions/add_sqlite_backend_tables.py +104 -0
  40. package/app/backend/alembic/versions/b10fb0b62240_add_diff_content_to_revisions.py +34 -0
  41. package/app/backend/alembic.ini +89 -0
  42. package/app/backend/app/__init__.py +3 -0
  43. package/app/backend/app/data_dir.py +85 -0
  44. package/app/backend/app/database.py +70 -0
  45. package/app/backend/app/database_sync.py +64 -0
  46. package/app/backend/app/dependencies/__init__.py +5 -0
  47. package/app/backend/app/dependencies/auth.py +80 -0
  48. package/app/backend/app/dependencies.py +43 -0
  49. package/app/backend/app/exceptions.py +178 -0
  50. package/app/backend/app/executors/__init__.py +1 -0
  51. package/app/backend/app/executors/adapters/__init__.py +1 -0
  52. package/app/backend/app/executors/adapters/aider.py +152 -0
  53. package/app/backend/app/executors/adapters/amazon_q.py +103 -0
  54. package/app/backend/app/executors/adapters/amp.py +123 -0
  55. package/app/backend/app/executors/adapters/claude.py +177 -0
  56. package/app/backend/app/executors/adapters/cline.py +127 -0
  57. package/app/backend/app/executors/adapters/codex.py +167 -0
  58. package/app/backend/app/executors/adapters/copilot.py +202 -0
  59. package/app/backend/app/executors/adapters/cursor.py +87 -0
  60. package/app/backend/app/executors/adapters/droid.py +123 -0
  61. package/app/backend/app/executors/adapters/gemini.py +132 -0
  62. package/app/backend/app/executors/adapters/goose.py +131 -0
  63. package/app/backend/app/executors/adapters/opencode.py +123 -0
  64. package/app/backend/app/executors/adapters/qwen.py +123 -0
  65. package/app/backend/app/executors/plugins/__init__.py +1 -0
  66. package/app/backend/app/executors/registry.py +202 -0
  67. package/app/backend/app/executors/spec.py +226 -0
  68. package/app/backend/app/main.py +486 -0
  69. package/app/backend/app/middleware/__init__.py +13 -0
  70. package/app/backend/app/middleware/idempotency.py +426 -0
  71. package/app/backend/app/middleware/rate_limit.py +312 -0
  72. package/app/backend/app/middleware/security_headers.py +43 -0
  73. package/app/backend/app/middleware/timeout.py +37 -0
  74. package/app/backend/app/models/__init__.py +56 -0
  75. package/app/backend/app/models/agent_conversation_history.py +56 -0
  76. package/app/backend/app/models/agent_session.py +127 -0
  77. package/app/backend/app/models/analysis_cache.py +49 -0
  78. package/app/backend/app/models/base.py +9 -0
  79. package/app/backend/app/models/board.py +79 -0
  80. package/app/backend/app/models/board_repo.py +68 -0
  81. package/app/backend/app/models/cost_budget.py +42 -0
  82. package/app/backend/app/models/enums.py +40 -0
  83. package/app/backend/app/models/evidence.py +132 -0
  84. package/app/backend/app/models/goal.py +102 -0
  85. package/app/backend/app/models/idempotency_entry.py +30 -0
  86. package/app/backend/app/models/job.py +163 -0
  87. package/app/backend/app/models/job_queue.py +39 -0
  88. package/app/backend/app/models/kv_store.py +28 -0
  89. package/app/backend/app/models/merge_checklist.py +87 -0
  90. package/app/backend/app/models/normalized_log.py +100 -0
  91. package/app/backend/app/models/planner_lock.py +43 -0
  92. package/app/backend/app/models/rate_limit_entry.py +25 -0
  93. package/app/backend/app/models/repo.py +66 -0
  94. package/app/backend/app/models/review_comment.py +91 -0
  95. package/app/backend/app/models/review_summary.py +69 -0
  96. package/app/backend/app/models/revision.py +130 -0
  97. package/app/backend/app/models/ticket.py +223 -0
  98. package/app/backend/app/models/ticket_event.py +83 -0
  99. package/app/backend/app/models/user.py +47 -0
  100. package/app/backend/app/models/workspace.py +71 -0
  101. package/app/backend/app/redis_client.py +119 -0
  102. package/app/backend/app/routers/__init__.py +29 -0
  103. package/app/backend/app/routers/agents.py +296 -0
  104. package/app/backend/app/routers/auth.py +94 -0
  105. package/app/backend/app/routers/board.py +885 -0
  106. package/app/backend/app/routers/dashboard.py +351 -0
  107. package/app/backend/app/routers/debug.py +528 -0
  108. package/app/backend/app/routers/evidence.py +96 -0
  109. package/app/backend/app/routers/executors.py +324 -0
  110. package/app/backend/app/routers/goals.py +574 -0
  111. package/app/backend/app/routers/jobs.py +448 -0
  112. package/app/backend/app/routers/maintenance.py +172 -0
  113. package/app/backend/app/routers/merge.py +360 -0
  114. package/app/backend/app/routers/planner.py +537 -0
  115. package/app/backend/app/routers/pull_requests.py +382 -0
  116. package/app/backend/app/routers/repos.py +263 -0
  117. package/app/backend/app/routers/revisions.py +939 -0
  118. package/app/backend/app/routers/settings.py +267 -0
  119. package/app/backend/app/routers/tickets.py +2003 -0
  120. package/app/backend/app/routers/webhooks.py +143 -0
  121. package/app/backend/app/routers/websocket.py +249 -0
  122. package/app/backend/app/schemas/__init__.py +109 -0
  123. package/app/backend/app/schemas/board.py +87 -0
  124. package/app/backend/app/schemas/common.py +33 -0
  125. package/app/backend/app/schemas/evidence.py +87 -0
  126. package/app/backend/app/schemas/goal.py +90 -0
  127. package/app/backend/app/schemas/job.py +97 -0
  128. package/app/backend/app/schemas/merge.py +139 -0
  129. package/app/backend/app/schemas/planner.py +500 -0
  130. package/app/backend/app/schemas/repo.py +187 -0
  131. package/app/backend/app/schemas/review.py +137 -0
  132. package/app/backend/app/schemas/revision.py +114 -0
  133. package/app/backend/app/schemas/ticket.py +238 -0
  134. package/app/backend/app/schemas/ticket_event.py +72 -0
  135. package/app/backend/app/schemas/workspace.py +19 -0
  136. package/app/backend/app/services/__init__.py +31 -0
  137. package/app/backend/app/services/agent_memory_service.py +223 -0
  138. package/app/backend/app/services/agent_registry.py +346 -0
  139. package/app/backend/app/services/agent_session_manager.py +318 -0
  140. package/app/backend/app/services/agent_session_service.py +219 -0
  141. package/app/backend/app/services/agent_tools.py +379 -0
  142. package/app/backend/app/services/auth_service.py +98 -0
  143. package/app/backend/app/services/autonomy_service.py +380 -0
  144. package/app/backend/app/services/board_repo_service.py +201 -0
  145. package/app/backend/app/services/board_service.py +326 -0
  146. package/app/backend/app/services/cleanup_service.py +1085 -0
  147. package/app/backend/app/services/config_service.py +908 -0
  148. package/app/backend/app/services/context_gatherer.py +557 -0
  149. package/app/backend/app/services/cost_tracking_service.py +293 -0
  150. package/app/backend/app/services/cursor_log_normalizer.py +536 -0
  151. package/app/backend/app/services/delivery_pipeline.py +440 -0
  152. package/app/backend/app/services/executor_service.py +634 -0
  153. package/app/backend/app/services/git_host/__init__.py +11 -0
  154. package/app/backend/app/services/git_host/factory.py +87 -0
  155. package/app/backend/app/services/git_host/github.py +270 -0
  156. package/app/backend/app/services/git_host/gitlab.py +194 -0
  157. package/app/backend/app/services/git_host/protocol.py +75 -0
  158. package/app/backend/app/services/git_merge_simple.py +346 -0
  159. package/app/backend/app/services/git_ops.py +384 -0
  160. package/app/backend/app/services/github_service.py +233 -0
  161. package/app/backend/app/services/goal_service.py +113 -0
  162. package/app/backend/app/services/job_service.py +423 -0
  163. package/app/backend/app/services/job_watchdog_service.py +424 -0
  164. package/app/backend/app/services/langchain_adapter.py +122 -0
  165. package/app/backend/app/services/llm_provider_clients.py +351 -0
  166. package/app/backend/app/services/llm_service.py +285 -0
  167. package/app/backend/app/services/log_normalizer.py +342 -0
  168. package/app/backend/app/services/log_stream_service.py +276 -0
  169. package/app/backend/app/services/merge_checklist_service.py +264 -0
  170. package/app/backend/app/services/merge_service.py +784 -0
  171. package/app/backend/app/services/orchestrator_log.py +84 -0
  172. package/app/backend/app/services/planner_service.py +1662 -0
  173. package/app/backend/app/services/planner_tick_sync.py +1040 -0
  174. package/app/backend/app/services/queued_message_service.py +156 -0
  175. package/app/backend/app/services/reliability_wrapper.py +389 -0
  176. package/app/backend/app/services/repo_discovery_service.py +318 -0
  177. package/app/backend/app/services/review_service.py +334 -0
  178. package/app/backend/app/services/revision_service.py +389 -0
  179. package/app/backend/app/services/safe_autopilot.py +510 -0
  180. package/app/backend/app/services/sqlite_worker.py +372 -0
  181. package/app/backend/app/services/task_dispatch.py +135 -0
  182. package/app/backend/app/services/ticket_generation_service.py +1781 -0
  183. package/app/backend/app/services/ticket_service.py +486 -0
  184. package/app/backend/app/services/udar_planner_service.py +1007 -0
  185. package/app/backend/app/services/webhook_service.py +126 -0
  186. package/app/backend/app/services/workspace_service.py +465 -0
  187. package/app/backend/app/services/worktree_file_service.py +92 -0
  188. package/app/backend/app/services/worktree_validator.py +213 -0
  189. package/app/backend/app/sqlite_kv.py +278 -0
  190. package/app/backend/app/state_machine.py +128 -0
  191. package/app/backend/app/templates/__init__.py +5 -0
  192. package/app/backend/app/templates/registry.py +243 -0
  193. package/app/backend/app/utils/__init__.py +5 -0
  194. package/app/backend/app/utils/artifact_reader.py +87 -0
  195. package/app/backend/app/utils/circuit_breaker.py +229 -0
  196. package/app/backend/app/utils/db_retry.py +136 -0
  197. package/app/backend/app/utils/ignored_fields.py +123 -0
  198. package/app/backend/app/utils/validators.py +54 -0
  199. package/app/backend/app/websocket/__init__.py +5 -0
  200. package/app/backend/app/websocket/manager.py +179 -0
  201. package/app/backend/app/websocket/state_tracker.py +113 -0
  202. package/app/backend/app/worker.py +3190 -0
  203. package/app/backend/calculator_tickets.json +40 -0
  204. package/app/backend/canary_tests.sh +591 -0
  205. package/app/backend/celerybeat-schedule +0 -0
  206. package/app/backend/celerybeat-schedule-shm +0 -0
  207. package/app/backend/celerybeat-schedule-wal +0 -0
  208. package/app/backend/logs/.gitkeep +3 -0
  209. package/app/backend/multiplication_division_implementation_tickets.json +55 -0
  210. package/app/backend/multiplication_division_tickets.json +42 -0
  211. package/app/backend/pyproject.toml +45 -0
  212. package/app/backend/requirements-dev.txt +8 -0
  213. package/app/backend/requirements.txt +20 -0
  214. package/app/backend/run.sh +30 -0
  215. package/app/backend/run_with_logs.sh +10 -0
  216. package/app/backend/scientific_calculator_tickets.json +40 -0
  217. package/app/backend/scripts/extract_openapi.py +21 -0
  218. package/app/backend/scripts/seed_demo.py +187 -0
  219. package/app/backend/setup_demo_review.py +302 -0
  220. package/app/backend/test_actual_parse.py +41 -0
  221. package/app/backend/test_agent_streaming.py +61 -0
  222. package/app/backend/test_parse.py +51 -0
  223. package/app/backend/test_streaming.py +51 -0
  224. package/app/backend/test_subprocess_streaming.py +50 -0
  225. package/app/backend/tests/__init__.py +1 -0
  226. package/app/backend/tests/conftest.py +46 -0
  227. package/app/backend/tests/test_auth.py +341 -0
  228. package/app/backend/tests/test_autonomy_service.py +391 -0
  229. package/app/backend/tests/test_cleanup_service_safety.py +417 -0
  230. package/app/backend/tests/test_middleware.py +279 -0
  231. package/app/backend/tests/test_planner_providers.py +290 -0
  232. package/app/backend/tests/test_planner_unblock.py +183 -0
  233. package/app/backend/tests/test_revision_invariants.py +618 -0
  234. package/app/backend/tests/test_sqlite_kv.py +290 -0
  235. package/app/backend/tests/test_sqlite_worker.py +353 -0
  236. package/app/backend/tests/test_task_dispatch.py +100 -0
  237. package/app/backend/tests/test_ticket_validation.py +304 -0
  238. package/app/backend/tests/test_udar_agent.py +693 -0
  239. package/app/backend/tests/test_webhook_service.py +184 -0
  240. package/app/backend/tickets_output.json +59 -0
  241. package/app/backend/user_management_tickets.json +50 -0
  242. package/app/backend/uvicorn.log +0 -0
  243. package/app/draft.yaml +313 -0
  244. package/app/frontend/dist/assets/index-LcjCczu5.js +155 -0
  245. package/app/frontend/dist/assets/index-_FP_279e.css +1 -0
  246. package/app/frontend/dist/index.html +14 -0
  247. package/app/frontend/dist/vite.svg +1 -0
  248. package/app/frontend/package.json +101 -0
  249. package/bin/cli.js +527 -0
  250. package/package.json +37 -0
package/bin/cli.js ADDED
@@ -0,0 +1,527 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * draft CLI — one-command launcher for Draft.
5
+ *
6
+ * Usage:
7
+ * npx draft-board # launch in current directory
8
+ * npx draft-board --port 9000
9
+ * npx draft-board --help
10
+ */
11
+
12
+ const { execSync, spawn } = require("child_process");
13
+ const path = require("path");
14
+ const fs = require("fs");
15
+ const os = require("os");
16
+ const http = require("http");
17
+
18
+ // ── Config ──────────────────────────────────────────────────────────
19
+ const DRAFT_HOME = path.join(os.homedir(), ".draft");
20
+ const VENV_DIR = path.join(DRAFT_HOME, "venv");
21
+ const DEFAULT_PORT = 8000;
22
+
23
+ const PKG_ROOT = path.resolve(__dirname, "..");
24
+
25
+ /**
26
+ * Resolve the application root containing backend/ and frontend/.
27
+ * When installed via npm/npx, these live under npx-cli/app/.
28
+ * In the monorepo (local dev), they're one level above npx-cli/.
29
+ */
30
+ function resolveAppRoot() {
31
+ // 1) Bundled app/ directory (npm published package)
32
+ const bundled = path.join(PKG_ROOT, "app");
33
+ if (
34
+ fs.existsSync(path.join(bundled, "backend", "requirements.txt")) &&
35
+ fs.existsSync(path.join(bundled, "frontend"))
36
+ ) {
37
+ return bundled;
38
+ }
39
+
40
+ // 2) Monorepo parent (local development: npx-cli sits beside backend/ frontend/)
41
+ const mono = path.resolve(PKG_ROOT, "..");
42
+ if (
43
+ fs.existsSync(path.join(mono, "backend", "requirements.txt")) &&
44
+ fs.existsSync(path.join(mono, "frontend", "package.json"))
45
+ ) {
46
+ return mono;
47
+ }
48
+
49
+ return null;
50
+ }
51
+
52
+ /**
53
+ * Check if a pre-built frontend exists (production mode).
54
+ */
55
+ function hasPrebuiltFrontend(appRoot) {
56
+ return fs.existsSync(path.join(appRoot, "frontend", "dist", "index.html"));
57
+ }
58
+
59
+ // ── Helpers ─────────────────────────────────────────────────────────
60
+
61
+ function log(msg) {
62
+ console.log(`\x1b[36m[draft]\x1b[0m ${msg}`);
63
+ }
64
+
65
+ function logError(msg) {
66
+ console.error(`\x1b[31m[draft]\x1b[0m ${msg}`);
67
+ }
68
+
69
+ function logSuccess(msg) {
70
+ console.log(`\x1b[32m[draft]\x1b[0m ${msg}`);
71
+ }
72
+
73
+ function commandExists(cmd) {
74
+ try {
75
+ execSync(`command -v ${cmd}`, { stdio: "ignore" });
76
+ return true;
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ function getVersion(cmd) {
83
+ try {
84
+ return execSync(`${cmd} --version`, { encoding: "utf-8" }).trim();
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ function parseMajorVersion(versionStr) {
91
+ const match = versionStr?.match(/(\d+)/);
92
+ return match ? parseInt(match[1], 10) : 0;
93
+ }
94
+
95
+ /**
96
+ * Wait for a local HTTP server to respond on /health.
97
+ */
98
+ function waitForHealth(port, timeoutMs = 30000) {
99
+ return new Promise((resolve, reject) => {
100
+ const start = Date.now();
101
+ const check = () => {
102
+ const req = http.get(`http://localhost:${port}/health`, (res) => {
103
+ if (res.statusCode === 200) {
104
+ resolve();
105
+ } else if (Date.now() - start > timeoutMs) {
106
+ reject(new Error(`Backend did not become healthy within ${timeoutMs / 1000}s`));
107
+ } else {
108
+ setTimeout(check, 500);
109
+ }
110
+ });
111
+ req.on("error", () => {
112
+ if (Date.now() - start > timeoutMs) {
113
+ reject(new Error(`Backend did not start within ${timeoutMs / 1000}s`));
114
+ } else {
115
+ setTimeout(check, 500);
116
+ }
117
+ });
118
+ req.end();
119
+ };
120
+ check();
121
+ });
122
+ }
123
+
124
+ // ── Prerequisite checks ─────────────────────────────────────────────
125
+
126
+ function checkPrerequisites(needsNode) {
127
+ log("Checking prerequisites...");
128
+
129
+ // Python 3.10+
130
+ const pythonCmd = commandExists("python3") ? "python3" : commandExists("python") ? "python" : null;
131
+ if (!pythonCmd) {
132
+ logError("Python 3 not found. Install Python 3.10+ from https://www.python.org/");
133
+ process.exit(1);
134
+ }
135
+ const pyVersion = getVersion(pythonCmd);
136
+ const pyMajorMinor = pyVersion?.match(/(\d+)\.(\d+)/);
137
+ if (pyMajorMinor) {
138
+ const major = parseInt(pyMajorMinor[1]);
139
+ const minor = parseInt(pyMajorMinor[2]);
140
+ if (major < 3 || (major === 3 && minor < 10)) {
141
+ logError(`Python 3.10+ required, found ${pyVersion}`);
142
+ process.exit(1);
143
+ }
144
+ }
145
+ log(` Python: ${pyVersion}`);
146
+
147
+ // Node 18+ (only required if running in dev mode)
148
+ if (needsNode) {
149
+ const nodeVersion = getVersion("node");
150
+ if (parseMajorVersion(nodeVersion) < 18) {
151
+ logError(`Node.js 18+ required, found ${nodeVersion}`);
152
+ process.exit(1);
153
+ }
154
+ log(` Node.js: ${nodeVersion}`);
155
+ }
156
+
157
+ // Git
158
+ if (!commandExists("git")) {
159
+ logError("Git not found. Install git from https://git-scm.com/");
160
+ process.exit(1);
161
+ }
162
+ log(` Git: ${getVersion("git")?.split("\n")[0]}`);
163
+
164
+ return pythonCmd;
165
+ }
166
+
167
+ // ── Setup ───────────────────────────────────────────────────────────
168
+
169
+ function ensureVenv(pythonCmd) {
170
+ if (fs.existsSync(path.join(VENV_DIR, "bin", "python"))) {
171
+ return; // Already exists
172
+ }
173
+
174
+ log("Creating Python virtual environment...");
175
+ fs.mkdirSync(DRAFT_HOME, { recursive: true });
176
+ execSync(`${pythonCmd} -m venv "${VENV_DIR}"`, { stdio: "inherit" });
177
+ }
178
+
179
+ function installBackendDeps(appRoot) {
180
+ const reqFile = path.join(appRoot, "backend", "requirements.txt");
181
+ if (!fs.existsSync(reqFile)) {
182
+ logError(`requirements.txt not found at ${reqFile}`);
183
+ process.exit(1);
184
+ }
185
+
186
+ const pip = path.join(VENV_DIR, "bin", "pip");
187
+ log("Installing backend dependencies...");
188
+ try {
189
+ execSync(`"${pip}" install -q -r "${reqFile}"`, { stdio: "inherit" });
190
+ } catch (e) {
191
+ logError(`Failed to install backend dependencies: ${e.message}`);
192
+ process.exit(1);
193
+ }
194
+ }
195
+
196
+ function installFrontendDeps(appRoot) {
197
+ const pkgJson = path.join(appRoot, "frontend", "package.json");
198
+ const nodeModules = path.join(appRoot, "frontend", "node_modules");
199
+
200
+ if (!fs.existsSync(pkgJson)) {
201
+ logError(`frontend/package.json not found at ${pkgJson}`);
202
+ process.exit(1);
203
+ }
204
+
205
+ if (fs.existsSync(nodeModules)) {
206
+ return; // Already installed
207
+ }
208
+
209
+ log("Installing frontend dependencies...");
210
+ try {
211
+ execSync("npm install --legacy-peer-deps", {
212
+ cwd: path.join(appRoot, "frontend"),
213
+ stdio: "inherit",
214
+ });
215
+ } catch (e) {
216
+ logError(`Failed to install frontend dependencies: ${e.message}`);
217
+ process.exit(1);
218
+ }
219
+ }
220
+
221
+ function runMigrations(appRoot) {
222
+ const alembicCfg = path.join(appRoot, "backend", "alembic.ini");
223
+ if (!fs.existsSync(alembicCfg)) {
224
+ return; // No migrations to run
225
+ }
226
+
227
+ const python = path.join(VENV_DIR, "bin", "python");
228
+ log("Running database migrations...");
229
+ try {
230
+ execSync(`"${python}" -m alembic upgrade head`, {
231
+ cwd: path.join(appRoot, "backend"),
232
+ stdio: "inherit",
233
+ });
234
+ } catch {
235
+ log(" (migrations skipped — may already be up to date)");
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Symlink draft.yaml into the user's CWD if it doesn't exist.
241
+ * This lets ConfigService find it from the repo root.
242
+ */
243
+ function ensureConfig(appRoot) {
244
+ const userConfig = path.join(process.cwd(), "draft.yaml");
245
+ const bundledConfig = path.join(appRoot, "draft.yaml");
246
+
247
+ // User already has a config — use theirs
248
+ if (fs.existsSync(userConfig)) {
249
+ return;
250
+ }
251
+
252
+ // Copy bundled default config to CWD so ConfigService finds it
253
+ if (fs.existsSync(bundledConfig)) {
254
+ fs.copyFileSync(bundledConfig, userConfig);
255
+ log("Created default draft.yaml in current directory");
256
+ }
257
+ }
258
+
259
+ // ── Run ─────────────────────────────────────────────────────────────
260
+
261
+ function startProductionMode(appRoot, port) {
262
+ const python = path.join(VENV_DIR, "bin", "python");
263
+ const backendDir = path.join(appRoot, "backend");
264
+
265
+ // Symlink frontend/dist into backend so FastAPI can serve it
266
+ const backendFrontendDir = path.join(backendDir, "frontend");
267
+ const backendFrontendDist = path.join(backendFrontendDir, "dist");
268
+ const sourceDist = path.join(appRoot, "frontend", "dist");
269
+
270
+ // Ensure a clean symlink — remove stale directory/symlink if present
271
+ try {
272
+ const stat = fs.lstatSync(backendFrontendDist);
273
+ if (stat.isSymbolicLink() || stat.isDirectory()) {
274
+ fs.rmSync(backendFrontendDist, { recursive: true });
275
+ }
276
+ } catch {
277
+ // Path doesn't exist, which is fine
278
+ }
279
+ fs.mkdirSync(backendFrontendDir, { recursive: true });
280
+ fs.symlinkSync(sourceDist, backendFrontendDist);
281
+
282
+ logSuccess("Starting Draft (production mode — single process)");
283
+ logSuccess(` App: http://localhost:${port}`);
284
+ logSuccess(` API Docs: http://localhost:${port}/docs`);
285
+ console.log("");
286
+
287
+ const backend = spawn(
288
+ python,
289
+ ["-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", String(port)],
290
+ {
291
+ cwd: backendDir,
292
+ stdio: "inherit",
293
+ env: {
294
+ ...process.env,
295
+ VIRTUAL_ENV: VENV_DIR,
296
+ PATH: `${VENV_DIR}/bin:${process.env.PATH}`,
297
+ PORT: String(port),
298
+ },
299
+ }
300
+ );
301
+
302
+ // Open browser after backend is healthy
303
+ waitForHealth(port, 30000)
304
+ .then(() => {
305
+ try {
306
+ const url = `http://localhost:${port}`;
307
+ const platform = process.platform;
308
+ if (platform === "darwin") execSync(`open "${url}"`, { stdio: "ignore" });
309
+ else if (platform === "linux") execSync(`xdg-open "${url}"`, { stdio: "ignore" });
310
+ else if (platform === "win32") execSync(`start "${url}"`, { stdio: "ignore" });
311
+ } catch {
312
+ // Browser open failed silently
313
+ }
314
+ })
315
+ .catch((err) => {
316
+ logError(err.message);
317
+ });
318
+
319
+ // Graceful shutdown
320
+ const cleanup = () => {
321
+ log("Shutting down...");
322
+ backend.kill("SIGTERM");
323
+ process.exit(0);
324
+ };
325
+
326
+ process.on("SIGINT", cleanup);
327
+ process.on("SIGTERM", cleanup);
328
+
329
+ backend.on("exit", (code) => {
330
+ if (code !== 0 && code !== null) logError(`Backend exited with code ${code}`);
331
+ process.exit(code || 0);
332
+ });
333
+ }
334
+
335
+ function startDevMode(appRoot, backendPort, frontendPort) {
336
+ const python = path.join(VENV_DIR, "bin", "python");
337
+ const backendDir = path.join(appRoot, "backend");
338
+ const frontendDir = path.join(appRoot, "frontend");
339
+
340
+ logSuccess("Starting Draft (development mode — two processes)");
341
+ logSuccess(` Backend: http://localhost:${backendPort}`);
342
+ logSuccess(` Frontend: http://localhost:${frontendPort}`);
343
+ logSuccess(` API Docs: http://localhost:${backendPort}/docs`);
344
+ console.log("");
345
+
346
+ const backend = spawn(
347
+ python,
348
+ ["-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", String(backendPort), "--reload"],
349
+ {
350
+ cwd: backendDir,
351
+ stdio: "inherit",
352
+ env: {
353
+ ...process.env,
354
+ VIRTUAL_ENV: VENV_DIR,
355
+ PATH: `${VENV_DIR}/bin:${process.env.PATH}`,
356
+ },
357
+ }
358
+ );
359
+
360
+ const frontend = spawn("npx", ["vite", "--host", "--port", String(frontendPort)], {
361
+ cwd: frontendDir,
362
+ stdio: "inherit",
363
+ });
364
+
365
+ // Open browser after a short delay
366
+ setTimeout(() => {
367
+ try {
368
+ const url = `http://localhost:${frontendPort}`;
369
+ const platform = process.platform;
370
+ if (platform === "darwin") execSync(`open "${url}"`, { stdio: "ignore" });
371
+ else if (platform === "linux") execSync(`xdg-open "${url}"`, { stdio: "ignore" });
372
+ else if (platform === "win32") execSync(`start "${url}"`, { stdio: "ignore" });
373
+ } catch {
374
+ // Browser open failed silently
375
+ }
376
+ }, 3000);
377
+
378
+ // Graceful shutdown
379
+ const cleanup = () => {
380
+ log("Shutting down...");
381
+ backend.kill("SIGTERM");
382
+ frontend.kill("SIGTERM");
383
+ process.exit(0);
384
+ };
385
+
386
+ process.on("SIGINT", cleanup);
387
+ process.on("SIGTERM", cleanup);
388
+
389
+ backend.on("exit", (code) => {
390
+ if (code !== 0 && code !== null) logError(`Backend exited with code ${code}`);
391
+ frontend.kill("SIGTERM");
392
+ });
393
+
394
+ frontend.on("exit", (code) => {
395
+ if (code !== 0 && code !== null) logError(`Frontend exited with code ${code}`);
396
+ backend.kill("SIGTERM");
397
+ });
398
+ }
399
+
400
+ // ── CLI ─────────────────────────────────────────────────────────────
401
+
402
+ function parseArgs() {
403
+ const args = process.argv.slice(2);
404
+ const opts = {
405
+ port: DEFAULT_PORT,
406
+ frontendPort: 5173,
407
+ help: false,
408
+ version: false,
409
+ skipSetup: false,
410
+ dev: false,
411
+ };
412
+
413
+ for (let i = 0; i < args.length; i++) {
414
+ switch (args[i]) {
415
+ case "--help":
416
+ case "-h":
417
+ opts.help = true;
418
+ break;
419
+ case "--version":
420
+ case "-v":
421
+ opts.version = true;
422
+ break;
423
+ case "--port":
424
+ case "-p":
425
+ opts.port = parseInt(args[++i], 10) || DEFAULT_PORT;
426
+ break;
427
+ case "--frontend-port":
428
+ opts.frontendPort = parseInt(args[++i], 10) || 5173;
429
+ break;
430
+ case "--skip-setup":
431
+ opts.skipSetup = true;
432
+ break;
433
+ case "--dev":
434
+ opts.dev = true;
435
+ break;
436
+ }
437
+ }
438
+
439
+ return opts;
440
+ }
441
+
442
+ function showHelp() {
443
+ console.log(`
444
+ \x1b[1mDraft\x1b[0m — AI-powered local-first kanban board
445
+
446
+ \x1b[1mUsage:\x1b[0m
447
+ npx draft-board [options]
448
+
449
+ \x1b[1mOptions:\x1b[0m
450
+ -p, --port <port> Server port (default: 8000)
451
+ --frontend-port <port> Frontend dev port, only in --dev mode (default: 5173)
452
+ --skip-setup Skip dependency installation
453
+ --dev Force development mode (vite + uvicorn --reload)
454
+ -v, --version Show version
455
+ -h, --help Show this help message
456
+
457
+ \x1b[1mPrerequisites:\x1b[0m
458
+ - Python 3.10+
459
+ - Git
460
+
461
+ \x1b[1mAI Agents (optional):\x1b[0m
462
+ - Claude Code CLI (claude): https://docs.anthropic.com/en/docs/claude-code
463
+ - Cursor Agent CLI: https://www.cursor.com/
464
+ - Any supported executor configured in draft.yaml
465
+ `);
466
+ }
467
+
468
+ // ── Main ────────────────────────────────────────────────────────────
469
+
470
+ function main() {
471
+ const opts = parseArgs();
472
+
473
+ if (opts.version) {
474
+ const pkg = require(path.join(PKG_ROOT, "package.json"));
475
+ console.log(`draft v${pkg.version}`);
476
+ process.exit(0);
477
+ }
478
+
479
+ if (opts.help) {
480
+ showHelp();
481
+ process.exit(0);
482
+ }
483
+
484
+ const pkg = require(path.join(PKG_ROOT, "package.json"));
485
+
486
+ console.log("");
487
+ console.log(" \x1b[1m\x1b[36m╔═══════════════════════════════════╗\x1b[0m");
488
+ console.log(` \x1b[1m\x1b[36m║ Draft v${pkg.version.padEnd(15)}║\x1b[0m`);
489
+ console.log(" \x1b[1m\x1b[36m║ AI-Powered Local Kanban Board ║\x1b[0m");
490
+ console.log(" \x1b[1m\x1b[36m╚═══════════════════════════════════╝\x1b[0m");
491
+ console.log("");
492
+
493
+ const appRoot = resolveAppRoot();
494
+ if (!appRoot) {
495
+ logError("Could not find backend/ and frontend/ directories.");
496
+ logError("If installed via npm, the package may be incomplete.");
497
+ logError("Try reinstalling: npm install -g draft-board");
498
+ process.exit(1);
499
+ }
500
+ log(`App root: ${appRoot}`);
501
+
502
+ const isProduction = hasPrebuiltFrontend(appRoot) && !opts.dev;
503
+ log(`Mode: ${isProduction ? "production" : "development"}`);
504
+
505
+ const pythonCmd = checkPrerequisites(!isProduction);
506
+
507
+ if (!opts.skipSetup) {
508
+ ensureVenv(pythonCmd);
509
+ installBackendDeps(appRoot);
510
+ if (!isProduction) {
511
+ installFrontendDeps(appRoot);
512
+ }
513
+ runMigrations(appRoot);
514
+ ensureConfig(appRoot);
515
+ }
516
+
517
+ logSuccess("Setup complete!");
518
+ console.log("");
519
+
520
+ if (isProduction) {
521
+ startProductionMode(appRoot, opts.port);
522
+ } else {
523
+ startDevMode(appRoot, opts.port, opts.frontendPort);
524
+ }
525
+ }
526
+
527
+ main();
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "draft-board",
3
+ "version": "0.1.0-beta.0",
4
+ "description": "AI-powered local-first kanban board that uses AI agents to automatically implement tickets",
5
+ "bin": {
6
+ "draft-board": "./bin/cli.js"
7
+ },
8
+ "keywords": [
9
+ "kanban",
10
+ "ai",
11
+ "agent",
12
+ "claude",
13
+ "cursor",
14
+ "codex",
15
+ "gemini",
16
+ "amp",
17
+ "project-management",
18
+ "automation",
19
+ "local-first"
20
+ ],
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/doramirdor/draft"
25
+ },
26
+ "scripts": {
27
+ "prepublishOnly": "bash build.sh",
28
+ "local-test": "node bin/cli.js"
29
+ },
30
+ "engines": {
31
+ "node": ">=18.0.0"
32
+ },
33
+ "files": [
34
+ "bin/",
35
+ "app/"
36
+ ]
37
+ }