jettypod 4.4.118 → 4.4.121
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/.env +4 -3
- package/Cargo.lock +6450 -0
- package/Cargo.toml +35 -0
- package/README.md +5 -1
- package/TAURI-MIGRATION-PLAN.md +840 -0
- package/apps/dashboard/app/connect-claude/page.tsx +5 -6
- package/apps/dashboard/app/decision/[id]/page.tsx +63 -58
- package/apps/dashboard/app/demo/gates/page.tsx +43 -45
- package/apps/dashboard/app/design-system/page.tsx +868 -0
- package/apps/dashboard/app/globals.css +80 -4
- package/apps/dashboard/app/install-claude/page.tsx +4 -6
- package/apps/dashboard/app/login/page.tsx +72 -54
- package/apps/dashboard/app/page.tsx +101 -48
- package/apps/dashboard/app/settings/page.tsx +61 -13
- package/apps/dashboard/app/signup/page.tsx +242 -0
- package/apps/dashboard/app/subscribe/page.tsx +0 -2
- package/apps/dashboard/app/tests/page.tsx +37 -4
- package/apps/dashboard/app/welcome/page.tsx +13 -16
- package/apps/dashboard/app/work/[id]/page.tsx +117 -118
- package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
- package/apps/dashboard/components/AppShell.tsx +92 -85
- package/apps/dashboard/components/CardMenu.tsx +45 -12
- package/apps/dashboard/components/ClaudePanel.tsx +771 -850
- package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
- package/apps/dashboard/components/CopyableId.tsx +3 -4
- package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
- package/apps/dashboard/components/DragContext.tsx +134 -63
- package/apps/dashboard/components/DraggableCard.tsx +3 -5
- package/apps/dashboard/components/DropZone.tsx +6 -7
- package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
- package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
- package/apps/dashboard/components/EditableTitle.tsx +26 -7
- package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
- package/apps/dashboard/components/EpicGroup.tsx +359 -0
- package/apps/dashboard/components/GateCard.tsx +79 -17
- package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
- package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
- package/apps/dashboard/components/JettyLoader.tsx +37 -0
- package/apps/dashboard/components/KanbanBoard.tsx +368 -958
- package/apps/dashboard/components/KanbanCard.tsx +740 -0
- package/apps/dashboard/components/LazyCard.tsx +62 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
- package/apps/dashboard/components/MainNav.tsx +38 -73
- package/apps/dashboard/components/MessageBlock.tsx +468 -0
- package/apps/dashboard/components/ModeStartCard.tsx +15 -16
- package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
- package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
- package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
- package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
- package/apps/dashboard/components/ReviewFooter.tsx +139 -0
- package/apps/dashboard/components/SessionList.tsx +19 -19
- package/apps/dashboard/components/SubscribeContent.tsx +91 -47
- package/apps/dashboard/components/TestTree.tsx +16 -16
- package/apps/dashboard/components/TipCard.tsx +16 -17
- package/apps/dashboard/components/Toast.tsx +5 -6
- package/apps/dashboard/components/TypeIcon.tsx +55 -0
- package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
- package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
- package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
- package/apps/dashboard/components/WorkItemTree.tsx +11 -32
- package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
- package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
- package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +74 -152
- package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
- package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
- package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
- package/apps/dashboard/components/ui/Button.tsx +104 -0
- package/apps/dashboard/components/ui/Input.tsx +78 -0
- package/apps/dashboard/components.json +1 -1
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
- package/apps/dashboard/contexts/UsageContext.tsx +87 -32
- package/apps/dashboard/dev.sh +35 -0
- package/apps/dashboard/eslint.config.mjs +9 -9
- package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
- package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
- package/apps/dashboard/hooks/useWebSocket.ts +138 -83
- package/apps/dashboard/index.html +73 -0
- package/apps/dashboard/lib/constants.ts +43 -0
- package/apps/dashboard/lib/data-bridge.ts +722 -0
- package/apps/dashboard/lib/db.ts +69 -1265
- package/apps/dashboard/lib/environment-config.ts +173 -0
- package/apps/dashboard/lib/environment-verification.ts +119 -0
- package/apps/dashboard/lib/kanban-utils.ts +270 -0
- package/apps/dashboard/lib/proof-run.ts +495 -0
- package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
- package/apps/dashboard/lib/run-migrations.js +27 -2
- package/apps/dashboard/lib/service-recovery.ts +326 -0
- package/apps/dashboard/lib/session-state-machine.ts +1 -0
- package/apps/dashboard/lib/session-state-utils.ts +0 -164
- package/apps/dashboard/lib/session-stream-manager.ts +308 -134
- package/apps/dashboard/lib/shadows.ts +7 -0
- package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
- package/apps/dashboard/lib/tauri-bridge.ts +102 -0
- package/apps/dashboard/lib/tauri.ts +106 -0
- package/apps/dashboard/lib/utils.ts +6 -0
- package/apps/dashboard/next-env.d.ts +1 -1
- package/apps/dashboard/package.json +21 -32
- package/apps/dashboard/public/bug-icon.png +0 -0
- package/apps/dashboard/public/buoy-icon.png +0 -0
- package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
- package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
- package/apps/dashboard/public/in-flight-seagull.png +0 -0
- package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
- package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
- package/apps/dashboard/public/jettypod_logo.png +0 -0
- package/apps/dashboard/public/pier-icon.png +0 -0
- package/apps/dashboard/public/star-icon.png +0 -0
- package/apps/dashboard/public/wrench-icon.png +0 -0
- package/apps/dashboard/scripts/tauri-build.js +228 -0
- package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
- package/apps/dashboard/scripts/ws-server.js +191 -0
- package/apps/dashboard/src/main.tsx +12 -0
- package/apps/dashboard/src/router.tsx +107 -0
- package/apps/dashboard/src/vite-env.d.ts +1 -0
- package/apps/dashboard/tsconfig.json +7 -12
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
- package/apps/dashboard/vite.config.ts +33 -0
- package/apps/update-server/src/index.ts +228 -80
- package/claude-hooks/global-guardrails.js +14 -13
- package/crates/jettypod-cli/Cargo.toml +19 -0
- package/crates/jettypod-cli/src/commands.rs +1249 -0
- package/crates/jettypod-cli/src/main.rs +595 -0
- package/crates/jettypod-core/Cargo.toml +26 -0
- package/crates/jettypod-core/build.rs +98 -0
- package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
- package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
- package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
- package/crates/jettypod-core/src/auth.rs +294 -0
- package/crates/jettypod-core/src/config.rs +397 -0
- package/crates/jettypod-core/src/db/mod.rs +507 -0
- package/crates/jettypod-core/src/db/recovery.rs +114 -0
- package/crates/jettypod-core/src/db/startup.rs +101 -0
- package/crates/jettypod-core/src/db/validate.rs +149 -0
- package/crates/jettypod-core/src/error.rs +76 -0
- package/crates/jettypod-core/src/git.rs +458 -0
- package/crates/jettypod-core/src/lib.rs +20 -0
- package/crates/jettypod-core/src/sessions.rs +625 -0
- package/crates/jettypod-core/src/skills.rs +556 -0
- package/crates/jettypod-core/src/work.rs +1086 -0
- package/crates/jettypod-core/src/worktree.rs +628 -0
- package/crates/jettypod-core/src/ws.rs +767 -0
- package/cucumber-test.cjs +6 -0
- package/cucumber.js +9 -3
- package/docs/COMMAND_REFERENCE.md +34 -0
- package/hooks/post-checkout +32 -75
- package/hooks/post-merge +111 -10
- package/jest.setup.js +1 -0
- package/jettypod.js +145 -116
- package/lib/bdd-preflight.js +96 -0
- package/lib/chore-taxonomy.js +33 -10
- package/lib/database.js +36 -16
- package/lib/db-watcher.js +1 -1
- package/lib/git-hooks/pre-commit +1 -1
- package/lib/jettypod-backup.js +27 -4
- package/lib/merge-lock.js +111 -253
- package/lib/migrations/027-plan-at-creation-column.js +3 -1
- package/lib/migrations/029-remove-autoincrement.js +307 -0
- package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
- package/lib/migrations/030-rejection-round-columns.js +54 -0
- package/lib/migrations/031-session-isolation-index.js +17 -0
- package/lib/migrations/index.js +47 -4
- package/lib/schema.js +10 -5
- package/lib/seed-onboarding.js +1 -1
- package/lib/update-command/index.js +9 -175
- package/lib/work-commands/index.js +144 -19
- package/lib/work-tracking/index.js +148 -27
- package/lib/worktree-diagnostics.js +16 -16
- package/lib/worktree-facade.js +1 -1
- package/lib/worktree-manager.js +8 -8
- package/lib/worktree-reconciler.js +5 -5
- package/package.json +9 -2
- package/scripts/ndjson-to-cucumber-json.js +152 -0
- package/scripts/postinstall.js +25 -0
- package/skills-templates/bug-mode/SKILL.md +79 -20
- package/skills-templates/bug-planning/SKILL.md +25 -29
- package/skills-templates/chore-mode/SKILL.md +171 -69
- package/skills-templates/chore-mode/verification.js +51 -10
- package/skills-templates/chore-planning/SKILL.md +47 -18
- package/skills-templates/design-system-selection/SKILL.md +273 -0
- package/skills-templates/epic-planning/SKILL.md +82 -48
- package/skills-templates/external-transition/SKILL.md +47 -47
- package/skills-templates/feature-planning/SKILL.md +173 -74
- package/skills-templates/production-mode/SKILL.md +69 -49
- package/skills-templates/request-routing/SKILL.md +4 -4
- package/skills-templates/simple-improvement/SKILL.md +74 -29
- package/skills-templates/speed-mode/SKILL.md +217 -141
- package/skills-templates/stable-mode/SKILL.md +148 -89
- package/apps/dashboard/README.md +0 -36
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
- package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
- package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
- package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
- package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
- package/apps/dashboard/app/api/kanban/route.ts +0 -15
- package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
- package/apps/dashboard/app/api/settings/general/route.ts +0 -21
- package/apps/dashboard/app/api/tests/route.ts +0 -9
- package/apps/dashboard/app/api/tests/run/route.ts +0 -82
- package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
- package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
- package/apps/dashboard/app/api/usage/route.ts +0 -17
- package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
- package/apps/dashboard/app/layout.tsx +0 -43
- package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
- package/apps/dashboard/electron/ipc-handlers.js +0 -1028
- package/apps/dashboard/electron/main.js +0 -2124
- package/apps/dashboard/electron/preload.js +0 -123
- package/apps/dashboard/electron/session-manager.js +0 -141
- package/apps/dashboard/electron-builder.config.js +0 -357
- package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
- package/apps/dashboard/lib/claude-process-manager.ts +0 -492
- package/apps/dashboard/lib/db-bridge.ts +0 -282
- package/apps/dashboard/lib/prototypes.ts +0 -202
- package/apps/dashboard/lib/test-results-db.ts +0 -307
- package/apps/dashboard/lib/tests.ts +0 -282
- package/apps/dashboard/next.config.js +0 -50
- package/apps/dashboard/postcss.config.mjs +0 -7
- package/apps/dashboard/public/file.svg +0 -1
- package/apps/dashboard/public/globe.svg +0 -1
- package/apps/dashboard/public/next.svg +0 -1
- package/apps/dashboard/public/vercel.svg +0 -1
- package/apps/dashboard/public/window.svg +0 -1
- package/apps/dashboard/scripts/download-node.js +0 -104
- package/apps/dashboard/scripts/upload-to-r2.js +0 -89
- package/docs/bdd-guidance.md +0 -390
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
//! Git operations — shells out to the git CLI.
|
|
2
|
+
//!
|
|
3
|
+
//! Decision D1: We use `std::process::Command` to call `git` rather than
|
|
4
|
+
//! libgit2/git2-rs, because git2 has incomplete worktree support and
|
|
5
|
+
//! the CLI gives us exact parity with the Node.js implementation.
|
|
6
|
+
|
|
7
|
+
use std::path::{Path, PathBuf};
|
|
8
|
+
use std::process::Command;
|
|
9
|
+
|
|
10
|
+
use crate::error::{GitError, Result};
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Low-level git command helpers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/// Run a git command in `cwd`, returning trimmed stdout on success.
|
|
17
|
+
/// Pipes both stdout and stderr to avoid stealing the terminal.
|
|
18
|
+
pub fn run(cwd: &Path, args: &[&str]) -> Result<String> {
|
|
19
|
+
let output = Command::new("git")
|
|
20
|
+
.args(args)
|
|
21
|
+
.current_dir(cwd)
|
|
22
|
+
.stdout(std::process::Stdio::piped())
|
|
23
|
+
.stderr(std::process::Stdio::piped())
|
|
24
|
+
.output()
|
|
25
|
+
.map_err(|e| GitError::NotAvailable(format!("{e}")))?;
|
|
26
|
+
|
|
27
|
+
if output.status.success() {
|
|
28
|
+
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
|
29
|
+
} else {
|
|
30
|
+
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
|
31
|
+
Err(GitError::CommandFailed {
|
|
32
|
+
args: args.join(" "),
|
|
33
|
+
stderr,
|
|
34
|
+
}
|
|
35
|
+
.into())
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Run a git command that may legitimately fail (e.g. branch already exists).
|
|
40
|
+
/// Returns Ok(Some(stdout)) on success, Ok(None) on non-zero exit.
|
|
41
|
+
pub fn try_run(cwd: &Path, args: &[&str]) -> Result<Option<String>> {
|
|
42
|
+
let output = Command::new("git")
|
|
43
|
+
.args(args)
|
|
44
|
+
.current_dir(cwd)
|
|
45
|
+
.stdout(std::process::Stdio::piped())
|
|
46
|
+
.stderr(std::process::Stdio::piped())
|
|
47
|
+
.output()
|
|
48
|
+
.map_err(|e| GitError::NotAvailable(format!("{e}")))?;
|
|
49
|
+
|
|
50
|
+
if output.status.success() {
|
|
51
|
+
Ok(Some(String::from_utf8_lossy(&output.stdout).trim().to_string()))
|
|
52
|
+
} else {
|
|
53
|
+
Ok(None)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Queries
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
/// Check if git is installed.
|
|
62
|
+
pub fn is_installed() -> bool {
|
|
63
|
+
Command::new("git")
|
|
64
|
+
.arg("--version")
|
|
65
|
+
.stdout(std::process::Stdio::piped())
|
|
66
|
+
.stderr(std::process::Stdio::piped())
|
|
67
|
+
.output()
|
|
68
|
+
.map(|o| o.status.success())
|
|
69
|
+
.unwrap_or(false)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Check if `cwd` is inside a git repository.
|
|
73
|
+
pub fn is_repo(cwd: &Path) -> bool {
|
|
74
|
+
try_run(cwd, &["rev-parse", "--git-dir"])
|
|
75
|
+
.ok()
|
|
76
|
+
.flatten()
|
|
77
|
+
.is_some()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Get the current branch name, or `None` if in detached HEAD state.
|
|
81
|
+
pub fn current_branch(cwd: &Path) -> Result<Option<String>> {
|
|
82
|
+
let branch = run(cwd, &["rev-parse", "--abbrev-ref", "HEAD"])?;
|
|
83
|
+
if branch == "HEAD" {
|
|
84
|
+
Ok(None) // detached HEAD
|
|
85
|
+
} else {
|
|
86
|
+
Ok(Some(branch))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Get `--show-toplevel` for `cwd`.
|
|
91
|
+
pub fn toplevel(cwd: &Path) -> Result<PathBuf> {
|
|
92
|
+
let s = run(cwd, &["rev-parse", "--show-toplevel"])?;
|
|
93
|
+
Ok(PathBuf::from(s))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Get `git status --porcelain` output. Empty string means clean.
|
|
97
|
+
pub fn status_porcelain(cwd: &Path) -> Result<String> {
|
|
98
|
+
run(cwd, &["status", "--porcelain"])
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// List changed files between two refs.
|
|
102
|
+
pub fn diff_names(cwd: &Path, from: &str, to: &str) -> Result<Vec<String>> {
|
|
103
|
+
let output = run(cwd, &["diff", "--name-only", from, to])?;
|
|
104
|
+
Ok(output.lines().map(|s| s.to_string()).collect())
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Git root detection (safe, cached-friendly)
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
/// Find the **main** repository root (never a worktree).
|
|
112
|
+
///
|
|
113
|
+
/// Uses `--git-common-dir` to find the shared `.git` directory, resolves to
|
|
114
|
+
/// an absolute path, then runs safety checks:
|
|
115
|
+
/// 1. `.jettypod` directory must exist in the root
|
|
116
|
+
/// 2. `--show-toplevel` must match the computed root
|
|
117
|
+
/// 3. Path must not contain `.jettypod-work`
|
|
118
|
+
pub fn find_main_repo_root(cwd: &Path) -> Result<PathBuf> {
|
|
119
|
+
let git_common = run(cwd, &["rev-parse", "--git-common-dir"])?;
|
|
120
|
+
|
|
121
|
+
let abs_git_dir = if Path::new(&git_common).is_absolute() {
|
|
122
|
+
PathBuf::from(&git_common)
|
|
123
|
+
} else {
|
|
124
|
+
cwd.join(&git_common)
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// The main repo root is the parent of the .git directory
|
|
128
|
+
let git_root = abs_git_dir
|
|
129
|
+
.parent()
|
|
130
|
+
.ok_or_else(|| GitError::Other("cannot determine parent of .git dir".into()))?;
|
|
131
|
+
|
|
132
|
+
// Canonicalize to handle /var ↔ /private/var on macOS
|
|
133
|
+
let git_root = git_root
|
|
134
|
+
.canonicalize()
|
|
135
|
+
.map_err(|e| GitError::Other(format!("canonicalize git root: {e}")))?;
|
|
136
|
+
|
|
137
|
+
// Safety check 1: .jettypod must exist
|
|
138
|
+
let jettypod_dir = git_root.join(".jettypod");
|
|
139
|
+
if !jettypod_dir.exists() {
|
|
140
|
+
return Err(GitError::NotARepo(format!(
|
|
141
|
+
"SAFETY: .jettypod not found in {} — may be a worktree",
|
|
142
|
+
git_root.display()
|
|
143
|
+
))
|
|
144
|
+
.into());
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Safety check 2: --show-toplevel must match
|
|
148
|
+
let top = toplevel(&git_root)?;
|
|
149
|
+
let top_canon = top
|
|
150
|
+
.canonicalize()
|
|
151
|
+
.map_err(|e| GitError::Other(format!("canonicalize toplevel: {e}")))?;
|
|
152
|
+
if top_canon != git_root {
|
|
153
|
+
return Err(GitError::NotARepo(format!(
|
|
154
|
+
"SAFETY: computed root {} != toplevel {}",
|
|
155
|
+
git_root.display(),
|
|
156
|
+
top_canon.display()
|
|
157
|
+
))
|
|
158
|
+
.into());
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Safety check 3: must not be inside a worktree path
|
|
162
|
+
let root_str = git_root.to_string_lossy();
|
|
163
|
+
if root_str.contains(".jettypod-work") {
|
|
164
|
+
return Err(GitError::NotARepo(
|
|
165
|
+
"SAFETY: computed root is inside .jettypod-work".into(),
|
|
166
|
+
)
|
|
167
|
+
.into());
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
Ok(git_root)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Branch helpers
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
/// Validate a branch name against git naming rules.
|
|
178
|
+
pub fn validate_branch_name(name: &str) -> Result<()> {
|
|
179
|
+
if name.is_empty() {
|
|
180
|
+
return Err(GitError::InvalidBranch("branch name must be non-empty".into()).into());
|
|
181
|
+
}
|
|
182
|
+
if name.contains(' ') {
|
|
183
|
+
return Err(GitError::InvalidBranch("branch name cannot contain spaces".into()).into());
|
|
184
|
+
}
|
|
185
|
+
if name.starts_with('-') || name.starts_with('.') {
|
|
186
|
+
return Err(GitError::InvalidBranch("branch name cannot start with - or .".into()).into());
|
|
187
|
+
}
|
|
188
|
+
if name.chars().any(|c| "~^:\\?*[".contains(c)) {
|
|
189
|
+
return Err(
|
|
190
|
+
GitError::InvalidBranch("branch name contains invalid characters".into()).into(),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
Ok(())
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// Convert text to a git-safe slug: lowercase, hyphens, alphanumeric only.
|
|
197
|
+
pub fn slugify(text: &str) -> String {
|
|
198
|
+
text.to_lowercase()
|
|
199
|
+
.split_whitespace()
|
|
200
|
+
.collect::<Vec<_>>()
|
|
201
|
+
.join("-")
|
|
202
|
+
.chars()
|
|
203
|
+
.filter(|c| c.is_ascii_alphanumeric() || *c == '-')
|
|
204
|
+
.collect()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// Create a branch name from work item id + title.
|
|
208
|
+
/// Returns `feature/work-{id}-{slug}`.
|
|
209
|
+
pub fn branch_name_for(id: i64, title: &str) -> String {
|
|
210
|
+
format!("feature/work-{}-{}", id, slugify(title))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// Create or checkout a branch.
|
|
214
|
+
pub fn create_or_checkout_branch(cwd: &Path, name: &str) -> Result<()> {
|
|
215
|
+
validate_branch_name(name)?;
|
|
216
|
+
|
|
217
|
+
// Try creating new branch
|
|
218
|
+
if try_run(cwd, &["checkout", "-b", name])?.is_some() {
|
|
219
|
+
return Ok(());
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Branch exists — checkout
|
|
223
|
+
run(cwd, &["checkout", name])?;
|
|
224
|
+
Ok(())
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/// Delete a branch (force).
|
|
228
|
+
pub fn delete_branch(cwd: &Path, name: &str) -> Result<()> {
|
|
229
|
+
run(cwd, &["branch", "-D", name])?;
|
|
230
|
+
Ok(())
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Worktree git commands (low-level)
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
/// Create a git worktree at `path` on a new branch from `base_branch`.
|
|
238
|
+
pub fn worktree_add(
|
|
239
|
+
repo_root: &Path,
|
|
240
|
+
branch: &str,
|
|
241
|
+
path: &Path,
|
|
242
|
+
base_branch: &str,
|
|
243
|
+
) -> Result<()> {
|
|
244
|
+
let path_str = path.to_string_lossy();
|
|
245
|
+
run(
|
|
246
|
+
repo_root,
|
|
247
|
+
&["worktree", "add", "-b", branch, &path_str, base_branch],
|
|
248
|
+
)?;
|
|
249
|
+
Ok(())
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// Remove a git worktree. Returns Ok(()) even if the directory is already gone.
|
|
253
|
+
pub fn worktree_remove(repo_root: &Path, path: &Path) -> Result<()> {
|
|
254
|
+
let path_str = path.to_string_lossy();
|
|
255
|
+
|
|
256
|
+
// Stage 1: standard remove
|
|
257
|
+
if try_run(repo_root, &["worktree", "remove", &path_str])?.is_some() {
|
|
258
|
+
return Ok(());
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Stage 2: forced remove
|
|
262
|
+
if try_run(repo_root, &["worktree", "remove", "--force", &path_str])?.is_some() {
|
|
263
|
+
return Ok(());
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Stage 3: directory removal + prune
|
|
267
|
+
if path.exists() {
|
|
268
|
+
std::fs::remove_dir_all(path)?;
|
|
269
|
+
}
|
|
270
|
+
let _ = try_run(repo_root, &["worktree", "prune"]);
|
|
271
|
+
|
|
272
|
+
Ok(())
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/// Prune stale worktree metadata.
|
|
276
|
+
pub fn worktree_prune(repo_root: &Path) -> Result<()> {
|
|
277
|
+
run(repo_root, &["worktree", "prune"])?;
|
|
278
|
+
Ok(())
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// Merge helpers
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
/// Push the current branch to origin with upstream tracking.
|
|
286
|
+
pub fn push_with_upstream(cwd: &Path, branch: &str) -> Result<()> {
|
|
287
|
+
run(cwd, &["push", "-u", "origin", branch])?;
|
|
288
|
+
Ok(())
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/// Fetch from origin.
|
|
292
|
+
pub fn fetch_origin(cwd: &Path) -> Result<()> {
|
|
293
|
+
run(cwd, &["fetch", "origin"])?;
|
|
294
|
+
Ok(())
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/// Pull the given branch from origin.
|
|
298
|
+
pub fn pull_origin(cwd: &Path, branch: &str) -> Result<()> {
|
|
299
|
+
run(cwd, &["pull", "origin", branch])?;
|
|
300
|
+
Ok(())
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/// Merge `branch` with `--no-ff` into the current branch.
|
|
304
|
+
/// Returns Ok(()) on success, or an error with conflict details.
|
|
305
|
+
pub fn merge_no_ff(cwd: &Path, branch: &str) -> Result<()> {
|
|
306
|
+
run(cwd, &["merge", "--no-ff", branch])?;
|
|
307
|
+
Ok(())
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/// Abort an in-progress merge.
|
|
311
|
+
pub fn merge_abort(cwd: &Path) -> Result<()> {
|
|
312
|
+
let _ = try_run(cwd, &["merge", "--abort"]);
|
|
313
|
+
Ok(())
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/// Checkout a branch.
|
|
317
|
+
pub fn checkout(cwd: &Path, branch: &str) -> Result<()> {
|
|
318
|
+
run(cwd, &["checkout", branch])?;
|
|
319
|
+
Ok(())
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/// Stage all changes and commit with message.
|
|
323
|
+
pub fn add_and_commit(cwd: &Path, message: &str) -> Result<()> {
|
|
324
|
+
run(cwd, &["add", "."])?;
|
|
325
|
+
run(cwd, &["commit", "-m", message])?;
|
|
326
|
+
Ok(())
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/// Push the current branch to origin.
|
|
330
|
+
pub fn push_origin(cwd: &Path, branch: &str) -> Result<()> {
|
|
331
|
+
run(cwd, &["push", "origin", branch])?;
|
|
332
|
+
Ok(())
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/// List untracked files (respecting .gitignore).
|
|
336
|
+
pub fn untracked_files(cwd: &Path) -> Result<Vec<String>> {
|
|
337
|
+
let output = run(cwd, &["ls-files", "--others", "--exclude-standard"])?;
|
|
338
|
+
if output.is_empty() {
|
|
339
|
+
Ok(Vec::new())
|
|
340
|
+
} else {
|
|
341
|
+
Ok(output.lines().map(|s| s.to_string()).collect())
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// Tests
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
#[cfg(test)]
|
|
350
|
+
mod tests {
|
|
351
|
+
use super::*;
|
|
352
|
+
use tempfile::TempDir;
|
|
353
|
+
|
|
354
|
+
fn init_repo() -> TempDir {
|
|
355
|
+
let dir = TempDir::new().unwrap();
|
|
356
|
+
run(dir.path(), &["init"]).unwrap();
|
|
357
|
+
run(dir.path(), &["config", "user.email", "test@test.com"]).unwrap();
|
|
358
|
+
run(dir.path(), &["config", "user.name", "Test"]).unwrap();
|
|
359
|
+
// Create initial commit so branches work
|
|
360
|
+
std::fs::write(dir.path().join("README.md"), "init").unwrap();
|
|
361
|
+
run(dir.path(), &["add", "."]).unwrap();
|
|
362
|
+
run(dir.path(), &["commit", "-m", "init"]).unwrap();
|
|
363
|
+
dir
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
#[test]
|
|
367
|
+
fn is_installed_returns_true() {
|
|
368
|
+
assert!(is_installed());
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
#[test]
|
|
372
|
+
fn is_repo_works() {
|
|
373
|
+
let dir = init_repo();
|
|
374
|
+
assert!(is_repo(dir.path()));
|
|
375
|
+
|
|
376
|
+
let tmp = TempDir::new().unwrap();
|
|
377
|
+
assert!(!is_repo(tmp.path()));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
#[test]
|
|
381
|
+
fn current_branch_on_main() {
|
|
382
|
+
let dir = init_repo();
|
|
383
|
+
let branch = current_branch(dir.path()).unwrap();
|
|
384
|
+
// Could be "main" or "master" depending on git config
|
|
385
|
+
assert!(branch.is_some());
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
#[test]
|
|
389
|
+
fn slugify_works() {
|
|
390
|
+
assert_eq!(slugify("Hello World"), "hello-world");
|
|
391
|
+
assert_eq!(slugify("Port work CRUD!"), "port-work-crud");
|
|
392
|
+
assert_eq!(slugify("Add feature #42"), "add-feature-42");
|
|
393
|
+
assert_eq!(slugify(" spaces everywhere "), "spaces-everywhere");
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#[test]
|
|
397
|
+
fn branch_name_for_works() {
|
|
398
|
+
assert_eq!(
|
|
399
|
+
branch_name_for(1199, "Port work item CRUD"),
|
|
400
|
+
"feature/work-1199-port-work-item-crud"
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
#[test]
|
|
405
|
+
fn validate_branch_name_catches_invalid() {
|
|
406
|
+
assert!(validate_branch_name("valid-name").is_ok());
|
|
407
|
+
assert!(validate_branch_name("feature/ok").is_ok());
|
|
408
|
+
assert!(validate_branch_name("").is_err());
|
|
409
|
+
assert!(validate_branch_name("has space").is_err());
|
|
410
|
+
assert!(validate_branch_name("-leading").is_err());
|
|
411
|
+
assert!(validate_branch_name(".leading").is_err());
|
|
412
|
+
assert!(validate_branch_name("bad~char").is_err());
|
|
413
|
+
assert!(validate_branch_name("bad[char").is_err());
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
#[test]
|
|
417
|
+
fn create_or_checkout_branch_works() {
|
|
418
|
+
let dir = init_repo();
|
|
419
|
+
create_or_checkout_branch(dir.path(), "test-branch").unwrap();
|
|
420
|
+
let branch = current_branch(dir.path()).unwrap().unwrap();
|
|
421
|
+
assert_eq!(branch, "test-branch");
|
|
422
|
+
|
|
423
|
+
// Switch back, then checkout existing branch
|
|
424
|
+
run(dir.path(), &["checkout", "master"]).ok();
|
|
425
|
+
run(dir.path(), &["checkout", "main"]).ok();
|
|
426
|
+
create_or_checkout_branch(dir.path(), "test-branch").unwrap();
|
|
427
|
+
let branch = current_branch(dir.path()).unwrap().unwrap();
|
|
428
|
+
assert_eq!(branch, "test-branch");
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
#[test]
|
|
432
|
+
fn status_porcelain_on_clean_repo() {
|
|
433
|
+
let dir = init_repo();
|
|
434
|
+
let status = status_porcelain(dir.path()).unwrap();
|
|
435
|
+
assert!(status.is_empty());
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
#[test]
|
|
439
|
+
fn worktree_add_and_remove() {
|
|
440
|
+
let dir = init_repo();
|
|
441
|
+
let wt_path = dir.path().join("wt-test");
|
|
442
|
+
let default_branch = current_branch(dir.path()).unwrap().unwrap();
|
|
443
|
+
|
|
444
|
+
worktree_add(dir.path(), "wt-branch", &wt_path, &default_branch).unwrap();
|
|
445
|
+
assert!(wt_path.exists());
|
|
446
|
+
|
|
447
|
+
worktree_remove(dir.path(), &wt_path).unwrap();
|
|
448
|
+
assert!(!wt_path.exists());
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
#[test]
|
|
452
|
+
fn untracked_files_lists_new_files() {
|
|
453
|
+
let dir = init_repo();
|
|
454
|
+
std::fs::write(dir.path().join("new.txt"), "hello").unwrap();
|
|
455
|
+
let files = untracked_files(dir.path()).unwrap();
|
|
456
|
+
assert!(files.contains(&"new.txt".to_string()));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//! JettyPod core business logic.
|
|
2
|
+
//!
|
|
3
|
+
//! This crate contains all data operations previously handled by the
|
|
4
|
+
//! Node.js sidecar: database access, work item management, git operations,
|
|
5
|
+
//! configuration, auth, sessions, WebSocket broadcasting, and skills sync.
|
|
6
|
+
//!
|
|
7
|
+
//! Used by both the Tauri desktop app and the standalone CLI.
|
|
8
|
+
|
|
9
|
+
pub mod auth;
|
|
10
|
+
pub mod config;
|
|
11
|
+
pub mod db;
|
|
12
|
+
pub mod error;
|
|
13
|
+
pub mod git;
|
|
14
|
+
pub mod sessions;
|
|
15
|
+
pub mod skills;
|
|
16
|
+
pub mod work;
|
|
17
|
+
pub mod worktree;
|
|
18
|
+
pub mod ws;
|
|
19
|
+
|
|
20
|
+
pub use error::{CoreError, Result};
|