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 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/5f61e323-bb5e-4b11-9352-182d1a884feb" controls></video>
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 # use installed npm package
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stage-tui",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "private": false,
5
5
  "description": "Minimalist TUI Git client",
6
6
  "homepage": "https://github.com/jenslys/stage",
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
- controller.hasSnapshot ? (
50
- <DiffWorkspace
51
- hasSnapshot={controller.hasSnapshot}
52
- fileRows={controller.fileRows}
53
- fileIndex={controller.fileIndex}
54
- selectedFilePath={controller.selectedFilePath}
55
- focus={controller.focus}
56
- terminalWidth={terminalWidth}
57
- terminalHeight={terminalHeight}
58
- diffText={controller.diffText}
59
- diffMessage={controller.diffMessage}
60
- diffFiletype={controller.diffFiletype}
61
- diffView={config.ui.diffView}
62
- onFileClick={(index) => {
63
- controller.focusFiles()
64
- controller.setMainFileSelection(index)
65
- }}
66
- onFileScroll={(direction) => {
67
- controller.focusFiles()
68
- if (direction === "up") {
69
- controller.moveToPreviousMainFile()
70
- } else {
71
- controller.moveToNextMainFile()
72
- }
73
- }}
74
- theme={theme}
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
- }