jettypod 4.4.120 → 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.
Files changed (208) hide show
  1. package/.env +2 -1
  2. package/Cargo.lock +6450 -0
  3. package/Cargo.toml +35 -0
  4. package/README.md +5 -1
  5. package/TAURI-MIGRATION-PLAN.md +840 -0
  6. package/apps/dashboard/app/connect-claude/page.tsx +5 -6
  7. package/apps/dashboard/app/decision/[id]/page.tsx +54 -49
  8. package/apps/dashboard/app/demo/gates/page.tsx +3 -5
  9. package/apps/dashboard/app/design-system/page.tsx +1 -1
  10. package/apps/dashboard/app/globals.css +74 -2
  11. package/apps/dashboard/app/install-claude/page.tsx +3 -5
  12. package/apps/dashboard/app/login/page.tsx +17 -20
  13. package/apps/dashboard/app/page.tsx +101 -48
  14. package/apps/dashboard/app/settings/page.tsx +60 -12
  15. package/apps/dashboard/app/signup/page.tsx +14 -17
  16. package/apps/dashboard/app/subscribe/page.tsx +0 -2
  17. package/apps/dashboard/app/tests/page.tsx +37 -4
  18. package/apps/dashboard/app/welcome/page.tsx +12 -15
  19. package/apps/dashboard/app/work/[id]/page.tsx +90 -75
  20. package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
  21. package/apps/dashboard/components/AppShell.tsx +70 -61
  22. package/apps/dashboard/components/CardMenu.tsx +0 -1
  23. package/apps/dashboard/components/ClaudePanel.tsx +541 -283
  24. package/apps/dashboard/components/ClaudePanelInput.tsx +23 -4
  25. package/apps/dashboard/components/ConnectClaudeScreen.tsx +1 -5
  26. package/apps/dashboard/components/CopyableId.tsx +1 -2
  27. package/apps/dashboard/components/DetailReviewActions.tsx +11 -20
  28. package/apps/dashboard/components/DragContext.tsx +132 -62
  29. package/apps/dashboard/components/DraggableCard.tsx +3 -5
  30. package/apps/dashboard/components/DropZone.tsx +5 -6
  31. package/apps/dashboard/components/EditableDetailDescription.tsx +6 -12
  32. package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
  33. package/apps/dashboard/components/EditableTitle.tsx +0 -1
  34. package/apps/dashboard/components/ElapsedTimer.tsx +15 -3
  35. package/apps/dashboard/components/EpicGroup.tsx +100 -70
  36. package/apps/dashboard/components/GateCard.tsx +0 -1
  37. package/apps/dashboard/components/GateChoiceCard.tsx +1 -2
  38. package/apps/dashboard/components/InstallClaudeScreen.tsx +1 -5
  39. package/apps/dashboard/components/JettyLoader.tsx +0 -1
  40. package/apps/dashboard/components/KanbanBoard.tsx +319 -173
  41. package/apps/dashboard/components/KanbanCard.tsx +341 -107
  42. package/apps/dashboard/components/LazyCard.tsx +62 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +0 -1
  44. package/apps/dashboard/components/MainNav.tsx +24 -25
  45. package/apps/dashboard/components/MessageBlock.tsx +93 -16
  46. package/apps/dashboard/components/ModeStartCard.tsx +0 -1
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +0 -1
  48. package/apps/dashboard/components/PlaceholderCard.tsx +0 -1
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +20 -20
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +47 -26
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +308 -223
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +303 -160
  53. package/apps/dashboard/components/ReviewFooter.tsx +12 -14
  54. package/apps/dashboard/components/SessionList.tsx +0 -1
  55. package/apps/dashboard/components/SubscribeContent.tsx +40 -11
  56. package/apps/dashboard/components/TestTree.tsx +1 -2
  57. package/apps/dashboard/components/TipCard.tsx +2 -4
  58. package/apps/dashboard/components/Toast.tsx +0 -1
  59. package/apps/dashboard/components/TypeIcon.tsx +7 -8
  60. package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +5 -17
  62. package/apps/dashboard/components/WelcomeScreen.tsx +2 -6
  63. package/apps/dashboard/components/WorkItemHeader.tsx +0 -1
  64. package/apps/dashboard/components/WorkItemTree.tsx +2 -4
  65. package/apps/dashboard/components/settings/AccountSection.tsx +27 -13
  66. package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
  67. package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
  68. package/apps/dashboard/components/settings/EnvVarsSection.tsx +20 -73
  69. package/apps/dashboard/components/settings/GeneralSection.tsx +137 -26
  70. package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
  71. package/apps/dashboard/components/settings/SettingsLayout.tsx +0 -1
  72. package/apps/dashboard/components/ui/Button.tsx +1 -1
  73. package/apps/dashboard/components/ui/Input.tsx +1 -1
  74. package/apps/dashboard/components.json +1 -1
  75. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +611 -358
  76. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +0 -1
  77. package/apps/dashboard/contexts/UsageContext.tsx +62 -31
  78. package/apps/dashboard/dev.sh +35 -0
  79. package/apps/dashboard/eslint.config.mjs +9 -9
  80. package/apps/dashboard/hooks/useWebSocket.ts +138 -83
  81. package/apps/dashboard/index.html +73 -0
  82. package/apps/dashboard/lib/data-bridge.ts +722 -0
  83. package/apps/dashboard/lib/db.ts +69 -1302
  84. package/apps/dashboard/lib/environment-config.ts +173 -0
  85. package/apps/dashboard/lib/environment-verification.ts +119 -0
  86. package/apps/dashboard/lib/kanban-utils.ts +226 -26
  87. package/apps/dashboard/lib/proof-run.ts +495 -0
  88. package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
  89. package/apps/dashboard/lib/service-recovery.ts +326 -0
  90. package/apps/dashboard/lib/session-state-machine.ts +1 -0
  91. package/apps/dashboard/lib/session-state-utils.ts +0 -164
  92. package/apps/dashboard/lib/session-stream-manager.ts +253 -122
  93. package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
  94. package/apps/dashboard/lib/tauri-bridge.ts +102 -0
  95. package/apps/dashboard/lib/tauri.ts +106 -0
  96. package/apps/dashboard/lib/utils.ts +3 -3
  97. package/apps/dashboard/next-env.d.ts +1 -1
  98. package/apps/dashboard/package.json +21 -33
  99. package/apps/dashboard/public/bug-icon.png +0 -0
  100. package/apps/dashboard/public/buoy-icon.png +0 -0
  101. package/apps/dashboard/public/in-flight-seagull.png +0 -0
  102. package/apps/dashboard/public/pier-icon.png +0 -0
  103. package/apps/dashboard/public/star-icon.png +0 -0
  104. package/apps/dashboard/public/wrench-icon.png +0 -0
  105. package/apps/dashboard/scripts/tauri-build.js +228 -0
  106. package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
  107. package/apps/dashboard/src/main.tsx +12 -0
  108. package/apps/dashboard/src/router.tsx +107 -0
  109. package/apps/dashboard/src/vite-env.d.ts +1 -0
  110. package/apps/dashboard/tsconfig.json +7 -12
  111. package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
  112. package/apps/dashboard/vite.config.ts +33 -0
  113. package/apps/update-server/src/index.ts +167 -30
  114. package/claude-hooks/global-guardrails.js +14 -13
  115. package/crates/jettypod-cli/Cargo.toml +19 -0
  116. package/crates/jettypod-cli/src/commands.rs +1249 -0
  117. package/crates/jettypod-cli/src/main.rs +595 -0
  118. package/crates/jettypod-core/Cargo.toml +26 -0
  119. package/crates/jettypod-core/build.rs +98 -0
  120. package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
  121. package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
  122. package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
  123. package/crates/jettypod-core/src/auth.rs +294 -0
  124. package/crates/jettypod-core/src/config.rs +397 -0
  125. package/crates/jettypod-core/src/db/mod.rs +507 -0
  126. package/crates/jettypod-core/src/db/recovery.rs +114 -0
  127. package/crates/jettypod-core/src/db/startup.rs +101 -0
  128. package/crates/jettypod-core/src/db/validate.rs +149 -0
  129. package/crates/jettypod-core/src/error.rs +76 -0
  130. package/crates/jettypod-core/src/git.rs +458 -0
  131. package/crates/jettypod-core/src/lib.rs +20 -0
  132. package/crates/jettypod-core/src/sessions.rs +625 -0
  133. package/crates/jettypod-core/src/skills.rs +556 -0
  134. package/crates/jettypod-core/src/work.rs +1086 -0
  135. package/crates/jettypod-core/src/worktree.rs +628 -0
  136. package/crates/jettypod-core/src/ws.rs +767 -0
  137. package/cucumber-test.cjs +6 -0
  138. package/jettypod.js +96 -4
  139. package/lib/bdd-preflight.js +96 -0
  140. package/lib/merge-lock.js +111 -253
  141. package/lib/migrations/030-rejection-round-columns.js +54 -0
  142. package/lib/migrations/031-session-isolation-index.js +17 -0
  143. package/lib/work-commands/index.js +58 -16
  144. package/lib/work-tracking/index.js +108 -8
  145. package/package.json +1 -1
  146. package/skills-templates/bug-mode/SKILL.md +43 -1
  147. package/skills-templates/chore-mode/SKILL.md +40 -1
  148. package/skills-templates/design-system-selection/SKILL.md +273 -0
  149. package/skills-templates/epic-planning/SKILL.md +14 -0
  150. package/skills-templates/feature-planning/SKILL.md +90 -1
  151. package/skills-templates/production-mode/SKILL.md +20 -0
  152. package/skills-templates/simple-improvement/SKILL.md +39 -2
  153. package/skills-templates/speed-mode/SKILL.md +10 -15
  154. package/skills-templates/stable-mode/SKILL.md +47 -0
  155. package/apps/dashboard/README.md +0 -36
  156. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -446
  157. package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
  158. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -280
  159. package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
  160. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -525
  161. package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
  162. package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
  163. package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
  164. package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
  165. package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
  166. package/apps/dashboard/app/api/kanban/route.ts +0 -15
  167. package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
  168. package/apps/dashboard/app/api/settings/general/route.ts +0 -21
  169. package/apps/dashboard/app/api/tests/route.ts +0 -9
  170. package/apps/dashboard/app/api/tests/run/route.ts +0 -82
  171. package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
  172. package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
  173. package/apps/dashboard/app/api/usage/route.ts +0 -17
  174. package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
  175. package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
  176. package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
  177. package/apps/dashboard/app/api/work/[id]/route.ts +0 -35
  178. package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -63
  179. package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
  180. package/apps/dashboard/app/layout.tsx +0 -55
  181. package/apps/dashboard/components/UpgradeBanner.tsx +0 -30
  182. package/apps/dashboard/electron/ipc-handlers.js +0 -1026
  183. package/apps/dashboard/electron/main.js +0 -2306
  184. package/apps/dashboard/electron/preload.js +0 -125
  185. package/apps/dashboard/electron/session-manager.js +0 -163
  186. package/apps/dashboard/electron-builder.config.js +0 -357
  187. package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
  188. package/apps/dashboard/lib/backlog-parser.ts +0 -50
  189. package/apps/dashboard/lib/claude-process-manager.ts +0 -529
  190. package/apps/dashboard/lib/db-bridge.ts +0 -283
  191. package/apps/dashboard/lib/prototypes.ts +0 -202
  192. package/apps/dashboard/lib/test-results-db.ts +0 -307
  193. package/apps/dashboard/lib/tests.ts +0 -282
  194. package/apps/dashboard/next.config.js +0 -66
  195. package/apps/dashboard/postcss.config.mjs +0 -7
  196. package/apps/dashboard/public/bug-icon.svg +0 -9
  197. package/apps/dashboard/public/buoy-icon.svg +0 -9
  198. package/apps/dashboard/public/file.svg +0 -1
  199. package/apps/dashboard/public/globe.svg +0 -1
  200. package/apps/dashboard/public/in-flight-seagull.svg +0 -9
  201. package/apps/dashboard/public/next.svg +0 -1
  202. package/apps/dashboard/public/pier-icon.svg +0 -14
  203. package/apps/dashboard/public/star-icon.svg +0 -9
  204. package/apps/dashboard/public/vercel.svg +0 -1
  205. package/apps/dashboard/public/window.svg +0 -1
  206. package/apps/dashboard/public/wrench-icon.svg +0 -9
  207. package/apps/dashboard/scripts/download-node.js +0 -104
  208. package/apps/dashboard/scripts/upload-to-r2.js +0 -89
@@ -0,0 +1,397 @@
1
+ //! Project configuration file I/O.
2
+ //!
3
+ //! Reads and writes `.jettypod/config.json` — project name, state,
4
+ //! and runtime settings.
5
+ //!
6
+ //! Port of `lib/config.js`.
7
+
8
+ use crate::error::{CoreError, Result};
9
+ use serde::{Deserialize, Serialize};
10
+ use std::path::Path;
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Types
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /// Top-level project configuration stored in `.jettypod/config.json`.
17
+ #[derive(Debug, Clone, Serialize, Deserialize)]
18
+ pub struct ProjectConfig {
19
+ #[serde(default = "default_name")]
20
+ pub name: String,
21
+
22
+ #[serde(default = "default_stage")]
23
+ pub stage: String,
24
+
25
+ #[serde(default = "default_bundles")]
26
+ pub bundles: Vec<String>,
27
+
28
+ #[serde(default = "default_project_state")]
29
+ pub project_state: String,
30
+
31
+ #[serde(default)]
32
+ pub project_discovery: ProjectDiscovery,
33
+
34
+ /// Optional override for the main branch name.
35
+ #[serde(rename = "mainBranch", skip_serializing_if = "Option::is_none")]
36
+ pub main_branch: Option<String>,
37
+
38
+ /// Preserve unknown fields for forward compatibility.
39
+ #[serde(flatten)]
40
+ pub extra: serde_json::Map<String, serde_json::Value>,
41
+ }
42
+
43
+ /// Project discovery state — tracks UX exploration workflow.
44
+ #[derive(Debug, Clone, Serialize, Deserialize)]
45
+ pub struct ProjectDiscovery {
46
+ #[serde(default = "default_discovery_status")]
47
+ pub status: String,
48
+
49
+ #[serde(default)]
50
+ pub prototypes: Vec<serde_json::Value>,
51
+
52
+ #[serde(default)]
53
+ pub winner: Option<String>,
54
+
55
+ #[serde(default)]
56
+ pub rationale: Option<String>,
57
+
58
+ #[serde(default)]
59
+ pub started_date: Option<String>,
60
+
61
+ #[serde(default)]
62
+ pub completed_date: Option<String>,
63
+
64
+ #[serde(default)]
65
+ pub checkpoint: DiscoveryCheckpoint,
66
+ }
67
+
68
+ /// Checkpoint state within project discovery.
69
+ #[derive(Debug, Clone, Serialize, Deserialize)]
70
+ pub struct DiscoveryCheckpoint {
71
+ #[serde(default = "default_step")]
72
+ pub step: u32,
73
+
74
+ #[serde(default)]
75
+ pub user_journey: Option<String>,
76
+
77
+ #[serde(default)]
78
+ pub ux_approach: Option<String>,
79
+
80
+ #[serde(default)]
81
+ pub epics_created: bool,
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Defaults
86
+ // ---------------------------------------------------------------------------
87
+
88
+ fn default_name() -> String {
89
+ "untitled".into()
90
+ }
91
+ fn default_stage() -> String {
92
+ "empty".into()
93
+ }
94
+ fn default_bundles() -> Vec<String> {
95
+ vec!["core".into()]
96
+ }
97
+ fn default_project_state() -> String {
98
+ "external".into()
99
+ }
100
+ fn default_discovery_status() -> String {
101
+ "not_started".into()
102
+ }
103
+ fn default_step() -> u32 {
104
+ 1
105
+ }
106
+
107
+ impl Default for ProjectDiscovery {
108
+ fn default() -> Self {
109
+ Self {
110
+ status: default_discovery_status(),
111
+ prototypes: Vec::new(),
112
+ winner: None,
113
+ rationale: None,
114
+ started_date: None,
115
+ completed_date: None,
116
+ checkpoint: DiscoveryCheckpoint::default(),
117
+ }
118
+ }
119
+ }
120
+
121
+ impl Default for DiscoveryCheckpoint {
122
+ fn default() -> Self {
123
+ Self {
124
+ step: 1,
125
+ user_journey: None,
126
+ ux_approach: None,
127
+ epics_created: false,
128
+ }
129
+ }
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Valid values
134
+ // ---------------------------------------------------------------------------
135
+
136
+ const VALID_PROJECT_STATES: &[&str] = &["internal", "external"];
137
+
138
+ /// Check whether a project state value is valid.
139
+ pub fn is_valid_project_state(state: &str) -> bool {
140
+ VALID_PROJECT_STATES.contains(&state)
141
+ }
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Operations
145
+ // ---------------------------------------------------------------------------
146
+
147
+ /// Read the project config from `.jettypod/config.json` under the given root.
148
+ ///
149
+ /// Returns defaults if the file doesn't exist or is malformed.
150
+ pub fn read(project_root: &Path) -> ProjectConfig {
151
+ let config_path = project_root.join(".jettypod").join("config.json");
152
+
153
+ if !config_path.exists() {
154
+ return default_config(project_root);
155
+ }
156
+
157
+ match std::fs::read_to_string(&config_path) {
158
+ Ok(contents) => match serde_json::from_str::<ProjectConfig>(&contents) {
159
+ Ok(mut cfg) => {
160
+ // Validate project_state — default to "external" if invalid
161
+ if !is_valid_project_state(&cfg.project_state) {
162
+ cfg.project_state = "external".into();
163
+ }
164
+ cfg
165
+ }
166
+ Err(_) => default_config(project_root),
167
+ },
168
+ Err(_) => default_config(project_root),
169
+ }
170
+ }
171
+
172
+ /// Write the config to `.jettypod/config.json`.
173
+ pub fn write(project_root: &Path, config: &ProjectConfig) -> Result<()> {
174
+ let dir = project_root.join(".jettypod");
175
+ std::fs::create_dir_all(&dir)?;
176
+
177
+ let json = serde_json::to_string_pretty(config)?;
178
+ std::fs::write(dir.join("config.json"), json)?;
179
+ Ok(())
180
+ }
181
+
182
+ /// Read, merge `updates` into the config, and write back.
183
+ ///
184
+ /// The `updates` value is a partial JSON object whose keys overwrite
185
+ /// matching keys in the current config.
186
+ pub fn update(project_root: &Path, updates: serde_json::Value) -> Result<ProjectConfig> {
187
+ // Validate project_state if present in updates
188
+ if let Some(state) = updates.get("project_state").and_then(|v| v.as_str()) {
189
+ if !is_valid_project_state(state) {
190
+ return Err(CoreError::Config(format!(
191
+ "Invalid project_state: {}. Must be 'internal' or 'external'.",
192
+ state
193
+ )));
194
+ }
195
+ }
196
+
197
+ let current = read(project_root);
198
+ let mut current_val = serde_json::to_value(&current)?;
199
+
200
+ // Merge updates into current
201
+ if let (Some(base), Some(patch)) = (current_val.as_object_mut(), updates.as_object()) {
202
+ for (key, value) in patch {
203
+ base.insert(key.clone(), value.clone());
204
+ }
205
+ }
206
+
207
+ let updated: ProjectConfig = serde_json::from_value(current_val)?;
208
+ write(project_root, &updated)?;
209
+ Ok(updated)
210
+ }
211
+
212
+ /// Check whether `.jettypod/config.json` exists.
213
+ pub fn exists(project_root: &Path) -> bool {
214
+ project_root.join(".jettypod").join("config.json").exists()
215
+ }
216
+
217
+ /// Resolve the default branch name.
218
+ ///
219
+ /// Priority: config `mainBranch` → `git symbolic-ref` → `main` → `master`.
220
+ pub fn resolve_default_branch(project_root: &Path) -> String {
221
+ let cfg = read(project_root);
222
+ if let Some(ref branch) = cfg.main_branch {
223
+ let trimmed = branch.trim();
224
+ if !trimmed.is_empty() {
225
+ return trimmed.to_string();
226
+ }
227
+ }
228
+
229
+ // Try git symbolic-ref
230
+ if let Ok(output) = std::process::Command::new("git")
231
+ .args(["symbolic-ref", "refs/remotes/origin/HEAD"])
232
+ .current_dir(project_root)
233
+ .output()
234
+ {
235
+ if output.status.success() {
236
+ let refname = String::from_utf8_lossy(&output.stdout).trim().to_string();
237
+ return refname.replace("refs/remotes/origin/", "");
238
+ }
239
+ }
240
+
241
+ // Try main, then master
242
+ for branch in &["main", "master"] {
243
+ if let Ok(output) = std::process::Command::new("git")
244
+ .args(["rev-parse", "--verify", branch])
245
+ .current_dir(project_root)
246
+ .output()
247
+ {
248
+ if output.status.success() {
249
+ return branch.to_string();
250
+ }
251
+ }
252
+ }
253
+
254
+ "main".into()
255
+ }
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // Helpers
259
+ // ---------------------------------------------------------------------------
260
+
261
+ fn default_config(project_root: &Path) -> ProjectConfig {
262
+ let name = project_root
263
+ .file_name()
264
+ .map(|n| n.to_string_lossy().to_string())
265
+ .unwrap_or_else(|| "untitled".into());
266
+
267
+ ProjectConfig {
268
+ name,
269
+ stage: "empty".into(),
270
+ bundles: vec!["core".into()],
271
+ project_state: "external".into(),
272
+ project_discovery: ProjectDiscovery::default(),
273
+ main_branch: None,
274
+ extra: serde_json::Map::new(),
275
+ }
276
+ }
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // Tests
280
+ // ---------------------------------------------------------------------------
281
+
282
+ #[cfg(test)]
283
+ mod tests {
284
+ use super::*;
285
+
286
+ #[test]
287
+ fn read_returns_defaults_when_missing() {
288
+ let dir = tempfile::tempdir().unwrap();
289
+ let cfg = read(dir.path());
290
+ assert_eq!(cfg.project_state, "external");
291
+ assert_eq!(cfg.project_discovery.status, "not_started");
292
+ }
293
+
294
+ #[test]
295
+ fn write_and_read_roundtrip() {
296
+ let dir = tempfile::tempdir().unwrap();
297
+ std::fs::create_dir_all(dir.path().join(".jettypod")).unwrap();
298
+
299
+ let mut cfg = default_config(dir.path());
300
+ cfg.project_state = "internal".into();
301
+ cfg.name = "test-project".into();
302
+
303
+ write(dir.path(), &cfg).unwrap();
304
+ let loaded = read(dir.path());
305
+
306
+ assert_eq!(loaded.name, "test-project");
307
+ assert_eq!(loaded.project_state, "internal");
308
+ }
309
+
310
+ #[test]
311
+ fn update_merges_fields() {
312
+ let dir = tempfile::tempdir().unwrap();
313
+ std::fs::create_dir_all(dir.path().join(".jettypod")).unwrap();
314
+
315
+ let cfg = default_config(dir.path());
316
+ write(dir.path(), &cfg).unwrap();
317
+
318
+ let updated = update(
319
+ dir.path(),
320
+ serde_json::json!({ "project_state": "internal" }),
321
+ )
322
+ .unwrap();
323
+
324
+ assert_eq!(updated.project_state, "internal");
325
+ // Name should be preserved
326
+ assert!(!updated.name.is_empty());
327
+ }
328
+
329
+ #[test]
330
+ fn update_rejects_invalid_state() {
331
+ let dir = tempfile::tempdir().unwrap();
332
+ std::fs::create_dir_all(dir.path().join(".jettypod")).unwrap();
333
+
334
+ let cfg = default_config(dir.path());
335
+ write(dir.path(), &cfg).unwrap();
336
+
337
+ let result = update(
338
+ dir.path(),
339
+ serde_json::json!({ "project_state": "invalid" }),
340
+ );
341
+ assert!(result.is_err());
342
+ }
343
+
344
+ #[test]
345
+ fn read_fixes_invalid_project_state() {
346
+ let dir = tempfile::tempdir().unwrap();
347
+ let jettypod_dir = dir.path().join(".jettypod");
348
+ std::fs::create_dir_all(&jettypod_dir).unwrap();
349
+ std::fs::write(
350
+ jettypod_dir.join("config.json"),
351
+ r#"{"project_state": "bogus", "name": "test"}"#,
352
+ )
353
+ .unwrap();
354
+
355
+ let cfg = read(dir.path());
356
+ assert_eq!(cfg.project_state, "external");
357
+ assert_eq!(cfg.name, "test");
358
+ }
359
+
360
+ #[test]
361
+ fn exists_works() {
362
+ let dir = tempfile::tempdir().unwrap();
363
+ assert!(!exists(dir.path()));
364
+
365
+ let jettypod_dir = dir.path().join(".jettypod");
366
+ std::fs::create_dir_all(&jettypod_dir).unwrap();
367
+ std::fs::write(jettypod_dir.join("config.json"), "{}").unwrap();
368
+
369
+ assert!(exists(dir.path()));
370
+ }
371
+
372
+ #[test]
373
+ fn preserves_extra_fields() {
374
+ let dir = tempfile::tempdir().unwrap();
375
+ let jettypod_dir = dir.path().join(".jettypod");
376
+ std::fs::create_dir_all(&jettypod_dir).unwrap();
377
+ std::fs::write(
378
+ jettypod_dir.join("config.json"),
379
+ r#"{"name":"test","custom_field":"preserved","project_state":"internal"}"#,
380
+ )
381
+ .unwrap();
382
+
383
+ let cfg = read(dir.path());
384
+ assert_eq!(
385
+ cfg.extra.get("custom_field").and_then(|v| v.as_str()),
386
+ Some("preserved")
387
+ );
388
+
389
+ // Write back and verify it's still there
390
+ write(dir.path(), &cfg).unwrap();
391
+ let reloaded = read(dir.path());
392
+ assert_eq!(
393
+ reloaded.extra.get("custom_field").and_then(|v| v.as_str()),
394
+ Some("preserved")
395
+ );
396
+ }
397
+ }