Skip to content

Commit d5ad28e

Browse files
vanpeltClaudeclaude
authored
Fix/codespace boot resilience (#209)
Makes our codespace resilient to starting up in the non-default branch. --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent 5bb4d97 commit d5ad28e

File tree

8 files changed

+499
-18
lines changed

8 files changed

+499
-18
lines changed

.devcontainer/features/feature/catnip-upgrade-and-start.sh

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,32 @@ export PATH="$OPT_DIR/bin:$HOME/.local/bin:$PATH"
1212

1313
# 1. Capture current environment to /etc/default/catnip
1414
log "Updating catnip service environment..."
15-
sudo tee -a /etc/default/catnip >/dev/null <<EOF
1615

17-
# Updated with current codespace environment ($(date))
18-
$(printenv | sed 's/^/export /')
19-
EOF
16+
# Create a temporary file with properly quoted exports
17+
TEMP_ENV=$(mktemp)
18+
trap "rm -f $TEMP_ENV" EXIT
19+
20+
# Use bash's declare -px to get properly quoted environment exports
21+
# This handles all special characters, quotes, newlines, etc. correctly
22+
{
23+
echo ""
24+
echo "# ==================== RUNTIME ENVIRONMENT ===================="
25+
echo "# Updated from codespace environment at $(date)"
26+
echo "# This section is regenerated on each startup"
27+
echo ""
28+
29+
# Export all current environment variables with proper shell quoting
30+
# declare -px outputs variables in a format safe for re-sourcing
31+
declare -px
32+
33+
} > "$TEMP_ENV"
34+
35+
# Remove any previous runtime environment section and append the new one
36+
# This preserves the template but prevents duplicates
37+
sudo sed -i '/^# ==================== RUNTIME ENVIRONMENT ====================/,$d' /etc/default/catnip
38+
sudo cat "$TEMP_ENV" | sudo tee -a /etc/default/catnip >/dev/null
39+
sudo chmod 644 /etc/default/catnip
40+
2041
ok "Environment captured"
2142

2243
# 2. Attempt upgrade with timeout (non-blocking)
@@ -30,6 +51,25 @@ else
3051
else
3152
warn "Upgrade check failed (exit $EXIT_CODE), proceeding with existing version"
3253
fi
54+
55+
# If upgrade failed/timed-out, check if it left us with a backup but no binary
56+
# This can happen if timeout occurs after backup creation but before install
57+
log "Checking for orphaned backup files..."
58+
for BACKUP_PATH in "$HOME/.local/bin/catnip.backup" "$OPT_DIR/bin/catnip.backup"; do
59+
if [ -f "$BACKUP_PATH" ]; then
60+
BINARY_PATH="${BACKUP_PATH%.backup}"
61+
if [ ! -f "$BINARY_PATH" ]; then
62+
warn "Found backup at $BACKUP_PATH but no binary at $BINARY_PATH - restoring backup"
63+
mv "$BACKUP_PATH" "$BINARY_PATH"
64+
chmod +x "$BINARY_PATH"
65+
ok "Restored backup to $BINARY_PATH"
66+
else
67+
# Backup exists but so does binary - clean up backup
68+
log "Removing stale backup at $BACKUP_PATH"
69+
rm -f "$BACKUP_PATH"
70+
fi
71+
fi
72+
done
3373
fi
3474

3575
# 3. Start service (always runs regardless of upgrade outcome)

.devcontainer/features/feature/catnip-vscode-extension/src/extension.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
import * as vscode from "vscode";
2-
import { exec } from "child_process";
3-
import { promisify } from "util";
42
import * as http from "http";
53
import * as QRCode from "qrcode";
64

7-
const execAsync = promisify(exec);
8-
95
// Device detection function
106
function isMobileDevice(): boolean {
117
// In VSCode extension context, we can check the environment
@@ -493,7 +489,7 @@ export function activate(context: vscode.ExtensionContext) {
493489
await vscode.workspace.fs.stat(uri);
494490
// Directory exists locally, open it in a new window
495491
await vscode.commands.executeCommand("vscode.openFolder", uri, true);
496-
} catch (statError) {
492+
} catch (_statError) {
497493
// Directory doesn't exist locally - likely in a container scenario
498494
// Open catnip interface in browser instead
499495

container/internal/cmd/upgrade.go

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,35 @@ func runUpgrade(cmd *cobra.Command, args []string) error {
114114
fmt.Printf("Current version: %s\n", currentVersion)
115115
fmt.Printf("Latest version: %s\n", latestVersion)
116116

117-
// Check if upgrade is needed
118-
if !force && currentVersion == latestVersion {
119-
fmt.Println("✅ Already running the latest version")
120-
return nil
117+
// Check if upgrade is needed using semantic version comparison
118+
if !force {
119+
comparison, err := compareVersions(currentVersion, latestVersion)
120+
if err != nil {
121+
return fmt.Errorf("failed to compare versions: %w", err)
122+
}
123+
124+
// Debug: show comparison result
125+
if os.Getenv("DEBUG") == "1" {
126+
fmt.Printf("🔍 Version comparison: %d (current vs latest)\n", comparison)
127+
}
128+
129+
if comparison > 0 {
130+
// Current version is newer than latest
131+
if strings.Contains(currentVersion, "-") {
132+
fmt.Printf("✅ Already running newer version %s (dev/pre-release, latest stable is %s)\n", currentVersion, latestVersion)
133+
} else {
134+
fmt.Printf("✅ Already running newer version %s (latest is %s)\n", currentVersion, latestVersion)
135+
}
136+
return nil
137+
}
138+
139+
if comparison == 0 {
140+
// Current version equals latest
141+
fmt.Println("✅ Already running the latest version")
142+
return nil
143+
}
144+
145+
// comparison < 0: upgrade needed, continue below
121146
}
122147

123148
if checkOnly {
@@ -185,6 +210,68 @@ func getLatestVersion(includeDev bool) (string, error) {
185210
}
186211
}
187212

213+
// parseVersion extracts the base version and pre-release suffix from a version string
214+
// Examples:
215+
//
216+
// "v0.11.1-dev" -> ("0.11.1", "-dev")
217+
// "0.11.2" -> ("0.11.2", "")
218+
// "v1.2.3-rc.1" -> ("1.2.3", "-rc.1")
219+
func parseVersion(version string) (baseVersion string, suffix string) {
220+
// Strip leading 'v' if present
221+
version = strings.TrimPrefix(version, "v")
222+
223+
// Split on first hyphen to separate base version from suffix
224+
if idx := strings.Index(version, "-"); idx != -1 {
225+
return version[:idx], version[idx:]
226+
}
227+
228+
return version, ""
229+
}
230+
231+
// compareVersions compares two semantic versions
232+
// Returns:
233+
//
234+
// -1 if v1 < v2
235+
// 0 if v1 == v2 (comparing base versions)
236+
// 1 if v1 > v2
237+
func compareVersions(v1, v2 string) (int, error) {
238+
base1, _ := parseVersion(v1)
239+
base2, _ := parseVersion(v2)
240+
241+
// Parse version components
242+
parts1 := strings.Split(base1, ".")
243+
parts2 := strings.Split(base2, ".")
244+
245+
// Ensure we have at least 3 parts (major.minor.patch)
246+
for len(parts1) < 3 {
247+
parts1 = append(parts1, "0")
248+
}
249+
for len(parts2) < 3 {
250+
parts2 = append(parts2, "0")
251+
}
252+
253+
// Compare each component
254+
for i := 0; i < 3; i++ {
255+
var num1, num2 int
256+
if _, err := fmt.Sscanf(parts1[i], "%d", &num1); err != nil {
257+
return 0, fmt.Errorf("invalid version component in %s: %s", v1, parts1[i])
258+
}
259+
if _, err := fmt.Sscanf(parts2[i], "%d", &num2); err != nil {
260+
return 0, fmt.Errorf("invalid version component in %s: %s", v2, parts2[i])
261+
}
262+
263+
if num1 < num2 {
264+
return -1, nil
265+
}
266+
if num1 > num2 {
267+
return 1, nil
268+
}
269+
}
270+
271+
// Base versions are equal
272+
return 0, nil
273+
}
274+
188275
func confirmUpgrade(currentVersion, latestVersion string) bool {
189276
// Check if we're in a TTY (interactive terminal)
190277
if !term.IsTerminal(int(os.Stdin.Fd())) {

0 commit comments

Comments
 (0)