@vladimirven/openswe 0.1.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 (117) hide show
  1. package/AGENTS.md +203 -0
  2. package/CLAUDE.md +203 -0
  3. package/README.md +166 -0
  4. package/bun.lock +447 -0
  5. package/bunfig.toml +4 -0
  6. package/package.json +42 -0
  7. package/src/app.tsx +84 -0
  8. package/src/components/App.tsx +526 -0
  9. package/src/components/ConfirmDialog.tsx +88 -0
  10. package/src/components/Footer.tsx +50 -0
  11. package/src/components/HelpModal.tsx +136 -0
  12. package/src/components/IssueSelectorModal.tsx +701 -0
  13. package/src/components/ManualSessionModal.tsx +191 -0
  14. package/src/components/PhaseProgress.tsx +45 -0
  15. package/src/components/Preview.tsx +249 -0
  16. package/src/components/ProviderSwitcherModal.tsx +156 -0
  17. package/src/components/ScrollableText.tsx +120 -0
  18. package/src/components/SessionCard.tsx +60 -0
  19. package/src/components/SessionList.tsx +79 -0
  20. package/src/components/SessionTerminal.tsx +89 -0
  21. package/src/components/StatusBar.tsx +84 -0
  22. package/src/components/ThemeSwitcherModal.tsx +237 -0
  23. package/src/components/index.ts +58 -0
  24. package/src/components/session-utils.ts +337 -0
  25. package/src/components/theme.ts +206 -0
  26. package/src/components/types.ts +215 -0
  27. package/src/config/defaults.ts +44 -0
  28. package/src/config/env.ts +67 -0
  29. package/src/config/global.ts +252 -0
  30. package/src/config/index.ts +171 -0
  31. package/src/config/types.ts +131 -0
  32. package/src/core/.gitkeep +0 -0
  33. package/src/core/index.ts +5 -0
  34. package/src/core/parser.ts +62 -0
  35. package/src/core/process-manager.ts +52 -0
  36. package/src/core/session.ts +423 -0
  37. package/src/core/tmux.ts +206 -0
  38. package/src/git/.gitkeep +0 -0
  39. package/src/git/index.ts +8 -0
  40. package/src/git/repo.ts +443 -0
  41. package/src/git/worktree.ts +317 -0
  42. package/src/github/.gitkeep +0 -0
  43. package/src/github/client.ts +208 -0
  44. package/src/github/index.ts +8 -0
  45. package/src/github/issues.ts +351 -0
  46. package/src/index.ts +369 -0
  47. package/src/prompts/.gitkeep +0 -0
  48. package/src/prompts/index.ts +1 -0
  49. package/src/prompts/swe-system.ts +22 -0
  50. package/src/providers/claude.ts +103 -0
  51. package/src/providers/index.ts +21 -0
  52. package/src/providers/opencode.ts +98 -0
  53. package/src/providers/registry.ts +53 -0
  54. package/src/providers/types.ts +117 -0
  55. package/src/store/buffers.ts +234 -0
  56. package/src/store/db.test.ts +579 -0
  57. package/src/store/db.ts +249 -0
  58. package/src/store/index.ts +101 -0
  59. package/src/store/project.ts +119 -0
  60. package/src/store/schema.sql +71 -0
  61. package/src/store/sessions.ts +454 -0
  62. package/src/store/types.ts +194 -0
  63. package/src/theme/context.tsx +170 -0
  64. package/src/theme/custom.ts +134 -0
  65. package/src/theme/index.ts +58 -0
  66. package/src/theme/loader.ts +264 -0
  67. package/src/theme/themes/aura.json +69 -0
  68. package/src/theme/themes/ayu.json +80 -0
  69. package/src/theme/themes/carbonfox.json +248 -0
  70. package/src/theme/themes/catppuccin-frappe.json +233 -0
  71. package/src/theme/themes/catppuccin-macchiato.json +233 -0
  72. package/src/theme/themes/catppuccin.json +112 -0
  73. package/src/theme/themes/cobalt2.json +228 -0
  74. package/src/theme/themes/cursor.json +249 -0
  75. package/src/theme/themes/dracula.json +219 -0
  76. package/src/theme/themes/everforest.json +241 -0
  77. package/src/theme/themes/flexoki.json +237 -0
  78. package/src/theme/themes/github.json +233 -0
  79. package/src/theme/themes/gruvbox.json +242 -0
  80. package/src/theme/themes/kanagawa.json +77 -0
  81. package/src/theme/themes/lucent-orng.json +237 -0
  82. package/src/theme/themes/material.json +235 -0
  83. package/src/theme/themes/matrix.json +77 -0
  84. package/src/theme/themes/mercury.json +252 -0
  85. package/src/theme/themes/monokai.json +221 -0
  86. package/src/theme/themes/nightowl.json +221 -0
  87. package/src/theme/themes/nord.json +223 -0
  88. package/src/theme/themes/one-dark.json +84 -0
  89. package/src/theme/themes/opencode.json +245 -0
  90. package/src/theme/themes/orng.json +249 -0
  91. package/src/theme/themes/osaka-jade.json +93 -0
  92. package/src/theme/themes/palenight.json +222 -0
  93. package/src/theme/themes/rosepine.json +234 -0
  94. package/src/theme/themes/solarized.json +223 -0
  95. package/src/theme/themes/synthwave84.json +226 -0
  96. package/src/theme/themes/tokyonight.json +243 -0
  97. package/src/theme/themes/vercel.json +245 -0
  98. package/src/theme/themes/vesper.json +218 -0
  99. package/src/theme/themes/zenburn.json +223 -0
  100. package/src/theme/types.ts +225 -0
  101. package/src/types/sql.d.ts +4 -0
  102. package/src/utils/ansi-parser.ts +225 -0
  103. package/src/utils/format.ts +46 -0
  104. package/src/utils/id.ts +15 -0
  105. package/src/utils/logger.ts +112 -0
  106. package/src/utils/prerequisites.ts +118 -0
  107. package/src/utils/shell.ts +9 -0
  108. package/src/wizard/flows.ts +419 -0
  109. package/src/wizard/index.ts +37 -0
  110. package/src/wizard/prompts.ts +190 -0
  111. package/src/workspace/detect.test.ts +51 -0
  112. package/src/workspace/detect.ts +223 -0
  113. package/src/workspace/index.ts +71 -0
  114. package/src/workspace/init.ts +131 -0
  115. package/src/workspace/paths.ts +143 -0
  116. package/src/workspace/project.ts +164 -0
  117. package/tsconfig.json +22 -0
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Format a number of tokens for display (e.g., 2300 -> "2.3k")
3
+ */
4
+ export function formatTokens(tokens: number): string {
5
+ if (tokens >= 1_000_000) {
6
+ return `${(tokens / 1_000_000).toFixed(1)}M`
7
+ }
8
+ if (tokens >= 1000) {
9
+ return `${(tokens / 1000).toFixed(1)}k`
10
+ }
11
+ return String(tokens)
12
+ }
13
+
14
+ /**
15
+ * Format a date as relative time (e.g., "3d ago", "2h ago")
16
+ */
17
+ export function formatRelativeTime(date: Date): string {
18
+ const now = new Date()
19
+ const diffMs = now.getTime() - date.getTime()
20
+
21
+ if (diffMs < 0) {
22
+ return "in the future"
23
+ }
24
+
25
+ const diffSecs = Math.floor(diffMs / 1000)
26
+ const diffMins = Math.floor(diffSecs / 60)
27
+ const diffHours = Math.floor(diffMins / 60)
28
+ const diffDays = Math.floor(diffHours / 24)
29
+ const diffWeeks = Math.floor(diffDays / 7)
30
+ const diffMonths = Math.floor(diffDays / 30)
31
+
32
+ if (diffMonths > 0) return `${diffMonths}mo ago`
33
+ if (diffWeeks > 0) return `${diffWeeks}w ago`
34
+ if (diffDays > 0) return `${diffDays}d ago`
35
+ if (diffHours > 0) return `${diffHours}h ago`
36
+ if (diffMins > 0) return `${diffMins}m ago`
37
+ return "just now"
38
+ }
39
+
40
+ /**
41
+ * Truncate a string to a maximum length, adding ellipsis if truncated
42
+ */
43
+ export function truncate(str: string, maxLength: number): string {
44
+ if (str.length <= maxLength) return str
45
+ return `${str.slice(0, maxLength - 1)}…`
46
+ }
@@ -0,0 +1,15 @@
1
+ import { v4 as uuidv4 } from "uuid"
2
+
3
+ /**
4
+ * Generate a unique identifier (UUID v4)
5
+ */
6
+ export function generateId(): string {
7
+ return uuidv4()
8
+ }
9
+
10
+ /**
11
+ * Generate a short ID (first 8 characters of UUID)
12
+ */
13
+ export function generateShortId(): string {
14
+ return uuidv4().slice(0, 8)
15
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Logging utility for OpenSWE
3
+ */
4
+
5
+ import type { LogLevel } from "../config/types"
6
+
7
+ // Re-export LogLevel for convenience
8
+ export type { LogLevel }
9
+
10
+ const LOG_LEVELS: Record<LogLevel, number> = {
11
+ debug: 0,
12
+ info: 1,
13
+ warn: 2,
14
+ error: 3,
15
+ }
16
+
17
+ // Can be overridden by config/env
18
+ let currentLevel: LogLevel = "info"
19
+ let logFileSink: any = null
20
+
21
+ /**
22
+ * Set the current log level
23
+ */
24
+ export function setLogLevel(level: LogLevel) {
25
+ currentLevel = level
26
+ }
27
+
28
+ /**
29
+ * Initialize file logging
30
+ * @param path - Absolute path to the log file
31
+ */
32
+ export function initFileLogging(path: string) {
33
+ try {
34
+ const file = Bun.file(path)
35
+ logFileSink = file.writer()
36
+ } catch (error) {
37
+ console.error("Failed to initialize file logging:", error)
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Get the current log level
43
+ */
44
+ export function getLogLevel(): LogLevel {
45
+ return currentLevel
46
+ }
47
+
48
+ function shouldLog(level: LogLevel): boolean {
49
+ return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel]
50
+ }
51
+
52
+ function formatTimestamp(): string {
53
+ return new Date().toISOString()
54
+ }
55
+
56
+ function formatValue(value: unknown): string {
57
+ if (value === null) return "null"
58
+ if (value === undefined) return "undefined"
59
+ if (typeof value === "string") return value
60
+ if (value instanceof Error) {
61
+ return `${value.name}: ${value.message}${value.stack ? `\n${value.stack}` : ""}`
62
+ }
63
+ try {
64
+ return JSON.stringify(value, null, 2)
65
+ } catch {
66
+ return String(value)
67
+ }
68
+ }
69
+
70
+ function formatMessage(level: LogLevel, args: unknown[]): string {
71
+ const timestamp = formatTimestamp()
72
+ const levelStr = level.toUpperCase().padEnd(5)
73
+ const message = args.map(formatValue).join(" ")
74
+ return `[${timestamp}] [${levelStr}] ${message}`
75
+ }
76
+
77
+ function log(level: LogLevel, args: unknown[]) {
78
+ if (shouldLog(level)) {
79
+ const msg = formatMessage(level, args)
80
+
81
+ // Console output
82
+ switch (level) {
83
+ case "debug":
84
+ console.debug(msg)
85
+ break
86
+ case "info":
87
+ console.log(msg)
88
+ break
89
+ case "warn":
90
+ console.warn(msg)
91
+ break
92
+ case "error":
93
+ console.error(msg)
94
+ break
95
+ }
96
+
97
+ // File output
98
+ if (logFileSink) {
99
+ logFileSink.write(msg + "\n")
100
+ logFileSink.flush()
101
+ }
102
+ }
103
+ }
104
+
105
+ export const logger = {
106
+ debug: (...args: unknown[]) => log("debug", args),
107
+ info: (...args: unknown[]) => log("info", args),
108
+ warn: (...args: unknown[]) => log("warn", args),
109
+ error: (...args: unknown[]) => log("error", args),
110
+ }
111
+
112
+ export default logger
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Prerequisites checker for OpenSWE
3
+ *
4
+ * Verifies required tools are installed before starting the TUI.
5
+ */
6
+
7
+ import { checkGitInstalled } from "../git"
8
+ import { checkGhCli } from "../github"
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ /** Result of prerequisite checks */
15
+ export interface PrerequisiteResult {
16
+ /** Whether all prerequisites are met */
17
+ success: boolean
18
+ /** List of error messages for unmet prerequisites */
19
+ errors: string[]
20
+ /** List of warning messages (non-fatal) */
21
+ warnings: string[]
22
+ }
23
+
24
+ // ============================================================================
25
+ // Prerequisite Checks
26
+ // ============================================================================
27
+
28
+ /**
29
+ * Check all prerequisites for running OpenSWE
30
+ *
31
+ * Verifies:
32
+ * - Git is installed
33
+ * - GitHub CLI (gh) is installed and authenticated
34
+ *
35
+ * @returns Result indicating whether prerequisites are met
36
+ */
37
+ export async function checkPrerequisites(): Promise<PrerequisiteResult> {
38
+ const errors: string[] = []
39
+ const warnings: string[] = []
40
+
41
+ // Check git installation
42
+ const gitInstalled = await checkGitInstalled()
43
+ if (!gitInstalled) {
44
+ errors.push("Git is not installed. Install it from https://git-scm.com/")
45
+ }
46
+
47
+ // Check gh CLI installation and authentication
48
+ const ghResult = await checkGhCli()
49
+ if (!ghResult.installed) {
50
+ errors.push(
51
+ "GitHub CLI (gh) is not installed. Install it from https://cli.github.com/"
52
+ )
53
+ } else if (!ghResult.authenticated) {
54
+ errors.push(
55
+ ghResult.error ?? "Not authenticated with GitHub. Run: gh auth login"
56
+ )
57
+ }
58
+
59
+ // Check if opencode is available (warning only, not required for all operations)
60
+ const opencodeAvailable = await isCommandAvailable("opencode")
61
+ if (!opencodeAvailable) {
62
+ warnings.push(
63
+ "opencode command not found in PATH. Sessions won't be able to start until it's installed."
64
+ )
65
+ }
66
+
67
+ return {
68
+ success: errors.length === 0,
69
+ errors,
70
+ warnings,
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Check if a command is available in PATH
76
+ *
77
+ * @param command - Command name to check
78
+ * @returns True if command is available
79
+ */
80
+ async function isCommandAvailable(command: string): Promise<boolean> {
81
+ try {
82
+ const proc = Bun.spawn(["which", command], {
83
+ stdout: "pipe",
84
+ stderr: "pipe",
85
+ })
86
+ const exitCode = await proc.exited
87
+ return exitCode === 0
88
+ } catch {
89
+ return false
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Format prerequisite errors for display
95
+ *
96
+ * @param result - Prerequisite check result
97
+ * @returns Formatted string for console output
98
+ */
99
+ export function formatPrerequisiteErrors(result: PrerequisiteResult): string {
100
+ const lines: string[] = []
101
+
102
+ if (result.errors.length > 0) {
103
+ lines.push("Prerequisites not met:")
104
+ for (const error of result.errors) {
105
+ lines.push(` - ${error}`)
106
+ }
107
+ }
108
+
109
+ if (result.warnings.length > 0) {
110
+ if (lines.length > 0) lines.push("")
111
+ lines.push("Warnings:")
112
+ for (const warning of result.warnings) {
113
+ lines.push(` - ${warning}`)
114
+ }
115
+ }
116
+
117
+ return lines.join("\n")
118
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Quote a string for use in a shell command.
3
+ * Wraps the string in single quotes and escapes any single quotes within it.
4
+ * This preserves all other characters (including newlines) literally.
5
+ */
6
+ export function shellQuote(s: string): string {
7
+ // Replace ' with '\'' (close quote, literal quote, open quote)
8
+ return "'" + s.replace(/'/g, "'\\''") + "'"
9
+ }