stage-tui 1.1.0 → 1.1.2
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/README.md +4 -2
- package/bin/stage +123 -0
- package/package.json +1 -1
- package/src/app.tsx +26 -31
- package/src/ui/components/branch-dialog.tsx +2 -0
- package/src/ui/components/commit-history-dialog.tsx +3 -2
- package/src/ui/components/diff-workspace.tsx +3 -0
- package/src/ui/components/section-divider.tsx +15 -0
- package/src/ui/components/splash-screen.tsx +0 -24
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ What it does:
|
|
|
12
12
|
- Fast branch switching and commit history view
|
|
13
13
|
- Optional AI-generated conventional commits
|
|
14
14
|
|
|
15
|
-
<video src="https://github.com/user-attachments/assets/
|
|
15
|
+
<video src="https://github.com/user-attachments/assets/45b376bb-09a1-4666-929a-40017ec7748d" controls></video>
|
|
16
16
|
|
|
17
17
|
## Get Started
|
|
18
18
|
|
|
@@ -40,8 +40,10 @@ stage --dev
|
|
|
40
40
|
## Use
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
|
-
stage #
|
|
43
|
+
stage # launches immediately and updates in background for next run
|
|
44
44
|
stage --dev # use local checkout at STAGE_DEV_PATH
|
|
45
|
+
stage update # update global install using the same package manager used to install stage
|
|
46
|
+
stage update --dry-run # show detected package manager + update command only
|
|
45
47
|
```
|
|
46
48
|
|
|
47
49
|
AI commit evals:
|
package/bin/stage
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { existsSync } from "node:fs"
|
|
3
3
|
import { join, resolve } from "node:path"
|
|
4
|
+
import { fileURLToPath } from "node:url"
|
|
5
|
+
|
|
6
|
+
const PACKAGE_NAME = "stage-tui"
|
|
7
|
+
type PackageManager = "bun" | "npm" | "pnpm" | "yarn"
|
|
8
|
+
type UpdateBehavior = "background" | "foreground"
|
|
4
9
|
|
|
5
10
|
function resolveDevEntry(): string {
|
|
6
11
|
const envPath = process.env.STAGE_DEV_PATH
|
|
@@ -23,6 +28,109 @@ function resolveDevEntry(): string {
|
|
|
23
28
|
)
|
|
24
29
|
}
|
|
25
30
|
|
|
31
|
+
function detectPackageManager(scriptPath: string): PackageManager | null {
|
|
32
|
+
const normalizedPath = scriptPath.replaceAll("\\", "/")
|
|
33
|
+
|
|
34
|
+
if (normalizedPath.includes("/.bun/")) {
|
|
35
|
+
return "bun"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (normalizedPath.includes("/.pnpm/") || normalizedPath.includes("/pnpm/global/")) {
|
|
39
|
+
return "pnpm"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (normalizedPath.includes("/.config/yarn/") || normalizedPath.includes("/yarn/")) {
|
|
43
|
+
return "yarn"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (normalizedPath.includes("/node_modules/")) {
|
|
47
|
+
return "npm"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildUpdateCommand(packageManager: PackageManager): string[] {
|
|
54
|
+
const packageSpecifier = `${PACKAGE_NAME}@latest`
|
|
55
|
+
|
|
56
|
+
switch (packageManager) {
|
|
57
|
+
case "bun":
|
|
58
|
+
return ["bun", "add", "-g", packageSpecifier]
|
|
59
|
+
case "pnpm":
|
|
60
|
+
return ["pnpm", "add", "-g", packageSpecifier]
|
|
61
|
+
case "yarn":
|
|
62
|
+
return ["yarn", "global", "add", packageSpecifier]
|
|
63
|
+
case "npm":
|
|
64
|
+
return ["npm", "install", "-g", packageSpecifier]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function runUpdateModeWithOptions(options: { dryRun: boolean }): Promise<void> {
|
|
69
|
+
const scriptPath = fileURLToPath(import.meta.url)
|
|
70
|
+
const packageManager = detectPackageManager(scriptPath)
|
|
71
|
+
if (options.dryRun) {
|
|
72
|
+
if (!packageManager) {
|
|
73
|
+
throw new Error(`Unable to detect how ${PACKAGE_NAME} was installed from ${scriptPath}.`)
|
|
74
|
+
}
|
|
75
|
+
const command = buildUpdateCommand(packageManager)
|
|
76
|
+
console.error(`Detected package manager: ${packageManager}`)
|
|
77
|
+
console.error(`Would run: ${command.join(" ")}`)
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return runUpdateForManager(packageManager, scriptPath, {
|
|
82
|
+
strict: true,
|
|
83
|
+
behavior: "foreground",
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function runAutoUpdateOnLaunch(): void {
|
|
88
|
+
const scriptPath = fileURLToPath(import.meta.url)
|
|
89
|
+
const packageManager = detectPackageManager(scriptPath)
|
|
90
|
+
void runUpdateForManager(packageManager, scriptPath, {
|
|
91
|
+
strict: false,
|
|
92
|
+
behavior: "background",
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function runUpdateForManager(
|
|
97
|
+
packageManager: PackageManager | null,
|
|
98
|
+
scriptPath: string,
|
|
99
|
+
options: {
|
|
100
|
+
strict: boolean
|
|
101
|
+
behavior: UpdateBehavior
|
|
102
|
+
},
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
if (!packageManager) {
|
|
105
|
+
if (!options.strict) {
|
|
106
|
+
// Launch path can be a local checkout (`bun run bin/stage` / `stage --dev`) with no package-manager signature.
|
|
107
|
+
// In that case auto-update cannot be resolved safely and must be skipped.
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
throw new Error(`Unable to detect how ${PACKAGE_NAME} was installed from ${scriptPath}.`)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const command = buildUpdateCommand(packageManager)
|
|
114
|
+
if (options.behavior === "background") {
|
|
115
|
+
Bun.spawn(command, {
|
|
116
|
+
stdin: "ignore",
|
|
117
|
+
stdout: "ignore",
|
|
118
|
+
stderr: "ignore",
|
|
119
|
+
})
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.error(`Updating ${PACKAGE_NAME} via ${packageManager}...`)
|
|
124
|
+
const child = Bun.spawn(command, { stdin: "inherit", stdout: "inherit", stderr: "inherit" })
|
|
125
|
+
|
|
126
|
+
const exitCode = await child.exited
|
|
127
|
+
if (exitCode !== 0) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Failed to update ${PACKAGE_NAME} via ${packageManager} (exit code ${exitCode}).`,
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
26
134
|
async function runDevMode(args: string[]) {
|
|
27
135
|
const entry = resolveDevEntry()
|
|
28
136
|
const child = Bun.spawn(["bun", "run", entry, ...args], {
|
|
@@ -36,6 +144,21 @@ async function runDevMode(args: string[]) {
|
|
|
36
144
|
|
|
37
145
|
try {
|
|
38
146
|
const args = process.argv.slice(2)
|
|
147
|
+
const updateModeIndex = args.findIndex((arg) => arg === "update" || arg === "--update")
|
|
148
|
+
if (updateModeIndex >= 0) {
|
|
149
|
+
const isDryRun = args.includes("--dry-run")
|
|
150
|
+
const isValidUsage =
|
|
151
|
+
updateModeIndex === 0 &&
|
|
152
|
+
(args.length === 1 || (args.length === 2 && isDryRun && args[1] === "--dry-run"))
|
|
153
|
+
if (!isValidUsage) {
|
|
154
|
+
throw new Error('Usage: stage update [--dry-run]')
|
|
155
|
+
}
|
|
156
|
+
await runUpdateModeWithOptions({ dryRun: isDryRun })
|
|
157
|
+
process.exit(0)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
runAutoUpdateOnLaunch()
|
|
161
|
+
|
|
39
162
|
const devModeIndex = args.indexOf("--dev")
|
|
40
163
|
|
|
41
164
|
if (devModeIndex >= 0) {
|
package/package.json
CHANGED
package/src/app.tsx
CHANGED
|
@@ -7,7 +7,6 @@ import { useGitTuiController } from "./hooks/use-git-tui-controller"
|
|
|
7
7
|
import { CommitDialog } from "./ui/components/commit-dialog"
|
|
8
8
|
import { DiffWorkspace } from "./ui/components/diff-workspace"
|
|
9
9
|
import { FooterBar } from "./ui/components/footer-bar"
|
|
10
|
-
import { SplashScreen } from "./ui/components/splash-screen"
|
|
11
10
|
import { ShortcutsDialog } from "./ui/components/shortcuts-dialog"
|
|
12
11
|
import { TopBar } from "./ui/components/top-bar"
|
|
13
12
|
import { resolveUiTheme } from "./ui/theme"
|
|
@@ -46,36 +45,32 @@ export function App({ config }: AppProps) {
|
|
|
46
45
|
/>
|
|
47
46
|
|
|
48
47
|
{activeScreen === "main" ? (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
/>
|
|
76
|
-
) : (
|
|
77
|
-
<SplashScreen theme={theme} />
|
|
78
|
-
)
|
|
48
|
+
<DiffWorkspace
|
|
49
|
+
hasSnapshot={controller.hasSnapshot}
|
|
50
|
+
fileRows={controller.fileRows}
|
|
51
|
+
fileIndex={controller.fileIndex}
|
|
52
|
+
selectedFilePath={controller.selectedFilePath}
|
|
53
|
+
focus={controller.focus}
|
|
54
|
+
terminalWidth={terminalWidth}
|
|
55
|
+
terminalHeight={terminalHeight}
|
|
56
|
+
diffText={controller.diffText}
|
|
57
|
+
diffMessage={controller.diffMessage}
|
|
58
|
+
diffFiletype={controller.diffFiletype}
|
|
59
|
+
diffView={config.ui.diffView}
|
|
60
|
+
onFileClick={(index) => {
|
|
61
|
+
controller.focusFiles()
|
|
62
|
+
controller.setMainFileSelection(index)
|
|
63
|
+
}}
|
|
64
|
+
onFileScroll={(direction) => {
|
|
65
|
+
controller.focusFiles()
|
|
66
|
+
if (direction === "up") {
|
|
67
|
+
controller.moveToPreviousMainFile()
|
|
68
|
+
} else {
|
|
69
|
+
controller.moveToNextMainFile()
|
|
70
|
+
}
|
|
71
|
+
}}
|
|
72
|
+
theme={theme}
|
|
73
|
+
/>
|
|
79
74
|
) : null}
|
|
80
75
|
|
|
81
76
|
{activeScreen === "branch" ? (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { InputRenderable, SelectOption } from "@opentui/core"
|
|
2
2
|
import type { RefObject } from "react"
|
|
3
3
|
|
|
4
|
+
import { SectionDivider } from "./section-divider"
|
|
4
5
|
import type { UiTheme } from "../theme"
|
|
5
6
|
import type { BranchDialogMode, FocusTarget } from "../types"
|
|
6
7
|
import { getVisibleRange } from "../list-range"
|
|
@@ -80,6 +81,7 @@ export function BranchDialog({
|
|
|
80
81
|
<box style={{ width: "100%", maxWidth: 88, gap: 1, flexDirection: "column", flexGrow: 1 }}>
|
|
81
82
|
<text fg={theme.colors.title}>change branch</text>
|
|
82
83
|
<text fg={theme.colors.subtleText}>current: {currentBranch}</text>
|
|
84
|
+
<SectionDivider theme={theme} />
|
|
83
85
|
{mode === "select" ? (
|
|
84
86
|
<>
|
|
85
87
|
<text fg={theme.colors.subtleText}>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { SelectOption } from "@opentui/core"
|
|
2
2
|
|
|
3
3
|
import { getPaneWidths, resolveRowBackground, resolveRowTextColor } from "./commit-history/layout"
|
|
4
|
+
import { SectionDivider } from "./section-divider"
|
|
4
5
|
import { getVisibleRange } from "../list-range"
|
|
5
|
-
import type { FocusTarget } from "../types"
|
|
6
|
+
import type { CommitHistoryMode, FocusTarget } from "../types"
|
|
6
7
|
import { fitLine, fitPathForWidth } from "../utils"
|
|
7
8
|
import type { UiTheme } from "../theme"
|
|
8
|
-
import type { CommitHistoryMode } from "../types"
|
|
9
9
|
|
|
10
10
|
type CommitHistoryDialogProps = {
|
|
11
11
|
open: boolean
|
|
@@ -96,6 +96,7 @@ export function CommitHistoryDialog({
|
|
|
96
96
|
? "tab: commits/files | enter: choose action | esc: close"
|
|
97
97
|
: "up/down choose action | enter confirm | esc back"}
|
|
98
98
|
</text>
|
|
99
|
+
<SectionDivider theme={theme} />
|
|
99
100
|
|
|
100
101
|
<box style={{ flexDirection: "row", flexGrow: 1, gap: 1 }}>
|
|
101
102
|
<box style={{ width: commitsPaneWidth, flexDirection: "column" }}>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { UiTheme } from "../theme"
|
|
2
|
+
import { SectionDivider } from "./section-divider"
|
|
2
3
|
import { getVisibleRange } from "../list-range"
|
|
3
4
|
import type { FileRow, FocusTarget } from "../types"
|
|
4
5
|
import { fitPathPartsForWidth } from "../utils"
|
|
@@ -52,6 +53,7 @@ export function DiffWorkspace({
|
|
|
52
53
|
<box style={{ flexDirection: "row", flexGrow: 1, gap: 1, paddingLeft: 1, paddingRight: 1 }}>
|
|
53
54
|
<box style={{ width: changesPaneWidth, flexDirection: "column" }}>
|
|
54
55
|
<text fg={theme.colors.mutedText}>changes ({fileRows.length})</text>
|
|
56
|
+
<SectionDivider theme={theme} />
|
|
55
57
|
<box
|
|
56
58
|
style={{ flexDirection: "column", flexGrow: 1 }}
|
|
57
59
|
onMouseScroll={(event) => {
|
|
@@ -113,6 +115,7 @@ export function DiffWorkspace({
|
|
|
113
115
|
</box>
|
|
114
116
|
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
115
117
|
<text fg={theme.colors.mutedText}>{paneLabel}</text>
|
|
118
|
+
<SectionDivider theme={theme} />
|
|
116
119
|
{showLoadingState ? (
|
|
117
120
|
<EmptyStatePanel
|
|
118
121
|
title="loading repository state..."
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { UiTheme } from "../theme"
|
|
2
|
+
|
|
3
|
+
const DIVIDER_LINE = "─".repeat(256)
|
|
4
|
+
|
|
5
|
+
type SectionDividerProps = {
|
|
6
|
+
theme: UiTheme
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function SectionDivider({ theme }: SectionDividerProps) {
|
|
10
|
+
return (
|
|
11
|
+
<box style={{ width: "100%", height: 1 }}>
|
|
12
|
+
<text fg={theme.colors.subtleText}>{DIVIDER_LINE}</text>
|
|
13
|
+
</box>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { UiTheme } from "../theme"
|
|
2
|
-
|
|
3
|
-
type SplashScreenProps = {
|
|
4
|
-
theme: UiTheme
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function SplashScreen({ theme }: SplashScreenProps) {
|
|
8
|
-
return (
|
|
9
|
-
<box
|
|
10
|
-
style={{
|
|
11
|
-
flexGrow: 1,
|
|
12
|
-
justifyContent: "center",
|
|
13
|
-
alignItems: "center",
|
|
14
|
-
paddingLeft: 2,
|
|
15
|
-
paddingRight: 2,
|
|
16
|
-
}}
|
|
17
|
-
>
|
|
18
|
-
<box style={{ flexDirection: "column", alignItems: "center", gap: 1 }}>
|
|
19
|
-
<ascii-font text="STAGE" font="slick" color={theme.colors.footerReady} />
|
|
20
|
-
<text fg={theme.colors.subtleText}>loading repository...</text>
|
|
21
|
-
</box>
|
|
22
|
-
</box>
|
|
23
|
-
)
|
|
24
|
-
}
|