Skip to content

Commit b2737c1

Browse files
authored
Merge pull request #93 from nebari-dev/unified-workspace-list
6. feat: unified workspace list with remote workspace support
2 parents 6a8fd75 + b82a5c4 commit b2737c1

31 files changed

+1336
-322
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,6 @@ go.work.sum
6868
# Claude
6969
CLAUDE.md
7070
.claude/
71+
72+
# Git worktrees
73+
.worktrees/

app.go

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ func getAppDataDir() (string, error) {
3232
if err != nil {
3333
return "", err
3434
}
35-
baseDir = filepath.Join(homeDir, "Library", "Application Support", "Nebi")
35+
baseDir = filepath.Join(homeDir, "Library", "Application Support", "nebi")
3636
case "windows":
3737
// Windows: %APPDATA%\Nebi
38-
baseDir = filepath.Join(os.Getenv("APPDATA"), "Nebi")
38+
baseDir = filepath.Join(os.Getenv("APPDATA"), "nebi")
3939
default:
4040
// Linux: ~/.local/share/nebi
4141
homeDir, err := os.UserHomeDir()
@@ -81,17 +81,6 @@ func (a *App) startup(ctx context.Context) {
8181
a.ctx = ctx
8282
logToFile("=== Startup called ===")
8383

84-
// Set default admin credentials for desktop app (first-run setup)
85-
if os.Getenv("ADMIN_USERNAME") == "" {
86-
os.Setenv("ADMIN_USERNAME", "admin")
87-
}
88-
if os.Getenv("ADMIN_PASSWORD") == "" {
89-
os.Setenv("ADMIN_PASSWORD", "admin")
90-
}
91-
if os.Getenv("ADMIN_EMAIL") == "" {
92-
os.Setenv("ADMIN_EMAIL", "admin@localhost")
93-
}
94-
9584
// Set database path to user's Application Support directory for desktop app
9685
dataDir, err := getAppDataDir()
9786
if err != nil {
@@ -103,10 +92,13 @@ func (a *App) startup(ctx context.Context) {
10392
logToFile(fmt.Sprintf("Using database: %s", dbPath))
10493

10594
// Set storage directory to app data dir (fixes read-only file system error)
106-
storageDir := filepath.Join(dataDir, "environments")
107-
os.Setenv("NEBI_STORAGE_ENVIRONMENTS_DIR", storageDir)
95+
storageDir := filepath.Join(dataDir, "workspaces")
96+
os.Setenv("NEBI_STORAGE_WORKSPACES_DIR", storageDir)
10897
logToFile(fmt.Sprintf("Using storage: %s", storageDir))
10998

99+
// Ensure desktop app runs in local mode
100+
os.Setenv("NEBI_MODE", "local")
101+
110102
// Load config
111103
cfg, err := config.Load()
112104
if err != nil {
@@ -137,12 +129,6 @@ func (a *App) startup(ctx context.Context) {
137129
}
138130
logToFile("Migrations complete")
139131

140-
// Create default admin user if none exists
141-
if err := db.CreateDefaultAdmin(database); err != nil {
142-
logToFile(fmt.Sprintf("Warning creating admin: %v", err))
143-
}
144-
logToFile("Admin user checked")
145-
146132
// Start embedded API server for the frontend
147133
logToFile("Starting embedded server goroutine...")
148134
go a.startEmbeddedServer(cfg, database)

cmd/nebi/pull.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,8 @@ func setupGlobalWorkspace(name string, force bool) (string, error) {
238238

239239
wsDir := s.GlobalWorkspaceDir(name)
240240
ws := &store.LocalWorkspace{
241-
Name: name,
242-
Path: wsDir,
243-
IsGlobal: true,
241+
Name: name,
242+
Path: wsDir,
244243
}
245244
if err := s.CreateWorkspace(ws); err != nil {
246245
return "", fmt.Errorf("saving workspace: %w", err)

cmd/nebi/status.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func runStatus(cmd *cobra.Command, args []string) error {
5151
}
5252

5353
wsType := "local"
54-
if ws.IsGlobal {
54+
if s.IsGlobalWorkspace(ws) {
5555
wsType = "global"
5656
}
5757
fmt.Fprintf(os.Stdout, "Workspace: %s\n", ws.Name)

cmd/nebi/workspace.go

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,23 +64,26 @@ Examples:
6464
var wsRemoveRemote bool
6565

6666
var workspaceRemoveCmd = &cobra.Command{
67-
Use: "remove <name>",
67+
Use: "remove [name|path]",
6868
Aliases: []string{"rm"},
6969
Short: "Remove a workspace from tracking",
7070
Long: `Remove a workspace from the local index or from the server.
7171
7272
By default removes from the local index:
73+
- With no argument or ".", removes the workspace tracked in the current directory.
7374
- For global workspaces, the stored files are also deleted.
7475
- For local workspaces, only the tracking entry is removed; project files are untouched.
7576
- A bare name refers to a global workspace; use a path (with a slash) for a local workspace.
7677
7778
With --remote, deletes the workspace from the configured server.
7879
7980
Examples:
81+
nebi workspace remove # remove workspace in current directory
82+
nebi workspace remove . # same as above
8083
nebi workspace remove data-science # remove global workspace by name
8184
nebi workspace remove ./my-project # remove local workspace by path
8285
nebi workspace remove myenv --remote # delete workspace from server`,
83-
Args: cobra.ExactArgs(1),
86+
Args: cobra.MaximumNArgs(1),
8487
RunE: runWorkspaceRemove,
8588
}
8689

@@ -137,7 +140,7 @@ func runWorkspaceListLocal() error {
137140
var missing int
138141
for _, ws := range wss {
139142
wsType := "local"
140-
if ws.IsGlobal {
143+
if s.IsGlobalWorkspace(&ws) {
141144
wsType = "global"
142145
}
143146
path := ws.Path
@@ -290,9 +293,8 @@ func runWorkspacePromote(cmd *cobra.Command, args []string) error {
290293
}
291294

292295
ws := &store.LocalWorkspace{
293-
Name: name,
294-
Path: wsDir,
295-
IsGlobal: true,
296+
Name: name,
297+
Path: wsDir,
296298
}
297299
if err := s.CreateWorkspace(ws); err != nil {
298300
return fmt.Errorf("saving workspace: %w", err)
@@ -303,10 +305,17 @@ func runWorkspacePromote(cmd *cobra.Command, args []string) error {
303305
}
304306

305307
func runWorkspaceRemove(cmd *cobra.Command, args []string) error {
308+
arg := ""
309+
if len(args) > 0 {
310+
arg = args[0]
311+
}
306312
if wsRemoveRemote {
307-
return runWorkspaceRemoveServer(args[0])
313+
if arg == "" || arg == "." {
314+
return fmt.Errorf("--remote requires a workspace name")
315+
}
316+
return runWorkspaceRemoveServer(arg)
308317
}
309-
return runWorkspaceRemoveLocal(args[0])
318+
return runWorkspaceRemoveLocal(arg)
310319
}
311320

312321
func runWorkspaceRemoveServer(name string) error {
@@ -338,7 +347,20 @@ func runWorkspaceRemoveLocal(arg string) error {
338347
defer s.Close()
339348

340349
var ws *store.LocalWorkspace
341-
if strings.Contains(arg, "/") || strings.Contains(arg, string(filepath.Separator)) {
350+
if arg == "" || arg == "." {
351+
// No argument or "." — remove workspace in current directory
352+
cwd, err := os.Getwd()
353+
if err != nil {
354+
return fmt.Errorf("getting current directory: %w", err)
355+
}
356+
ws, err = s.FindWorkspaceByPath(cwd)
357+
if err != nil {
358+
return err
359+
}
360+
if ws == nil {
361+
return fmt.Errorf("no tracked workspace in current directory; run 'nebi workspace list' to see available workspaces")
362+
}
363+
} else if strings.Contains(arg, "/") || strings.Contains(arg, string(filepath.Separator)) {
342364
absPath, err := filepath.Abs(arg)
343365
if err != nil {
344366
return fmt.Errorf("resolving path: %w", err)
@@ -360,21 +382,28 @@ func runWorkspaceRemoveLocal(arg string) error {
360382
}
361383
}
362384

385+
isGlobal := s.IsGlobalWorkspace(ws)
386+
363387
// Delete directory for global workspaces
364-
if ws.IsGlobal {
388+
if isGlobal {
365389
if err := os.RemoveAll(ws.Path); err != nil {
366390
return fmt.Errorf("removing global workspace directory: %w", err)
367391
}
368392
}
369393

394+
displayName := ws.Name
395+
if arg != "" && arg != "." {
396+
displayName = arg
397+
}
398+
370399
if err := s.DeleteWorkspace(ws.ID); err != nil {
371400
return fmt.Errorf("removing workspace: %w", err)
372401
}
373402

374-
if ws.IsGlobal {
375-
fmt.Fprintf(os.Stderr, "Removed global workspace %q\n", arg)
403+
if isGlobal {
404+
fmt.Fprintf(os.Stderr, "Removed global workspace %q\n", displayName)
376405
} else {
377-
fmt.Fprintf(os.Stderr, "Removed workspace %q (project files untouched)\n", arg)
406+
fmt.Fprintf(os.Stderr, "Removed workspace %q (project files untouched)\n", displayName)
378407
}
379408
return nil
380409
}

docs/docs.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,6 +1401,67 @@ const docTemplate = `{
14011401
}
14021402
}
14031403
}
1404+
},
1405+
"put": {
1406+
"security": [
1407+
{
1408+
"BearerAuth": []
1409+
}
1410+
],
1411+
"consumes": [
1412+
"application/json"
1413+
],
1414+
"produces": [
1415+
"application/json"
1416+
],
1417+
"tags": [
1418+
"workspaces"
1419+
],
1420+
"summary": "Save pixi.toml content for a workspace",
1421+
"parameters": [
1422+
{
1423+
"type": "string",
1424+
"description": "Workspace ID",
1425+
"name": "id",
1426+
"in": "path",
1427+
"required": true
1428+
},
1429+
{
1430+
"description": "pixi.toml content",
1431+
"name": "request",
1432+
"in": "body",
1433+
"required": true,
1434+
"schema": {
1435+
"$ref": "#/definitions/handlers.SavePixiTomlRequest"
1436+
}
1437+
}
1438+
],
1439+
"responses": {
1440+
"200": {
1441+
"description": "OK",
1442+
"schema": {
1443+
"$ref": "#/definitions/handlers.PixiTomlResponse"
1444+
}
1445+
},
1446+
"400": {
1447+
"description": "Bad Request",
1448+
"schema": {
1449+
"$ref": "#/definitions/handlers.ErrorResponse"
1450+
}
1451+
},
1452+
"404": {
1453+
"description": "Not Found",
1454+
"schema": {
1455+
"$ref": "#/definitions/handlers.ErrorResponse"
1456+
}
1457+
},
1458+
"500": {
1459+
"description": "Internal Server Error",
1460+
"schema": {
1461+
"$ref": "#/definitions/handlers.ErrorResponse"
1462+
}
1463+
}
1464+
}
14041465
}
14051466
},
14061467
"/workspaces/{id}/publications": {
@@ -2006,8 +2067,14 @@ const docTemplate = `{
20062067
"package_manager": {
20072068
"type": "string"
20082069
},
2070+
"path": {
2071+
"type": "string"
2072+
},
20092073
"pixi_toml": {
20102074
"type": "string"
2075+
},
2076+
"source": {
2077+
"type": "string"
20112078
}
20122079
}
20132080
},
@@ -2207,6 +2274,17 @@ const docTemplate = `{
22072274
}
22082275
}
22092276
},
2277+
"handlers.SavePixiTomlRequest": {
2278+
"type": "object",
2279+
"required": [
2280+
"content"
2281+
],
2282+
"properties": {
2283+
"content": {
2284+
"type": "string"
2285+
}
2286+
}
2287+
},
22102288
"handlers.ShareWorkspaceRequest": {
22112289
"type": "object",
22122290
"required": [
@@ -2513,9 +2591,17 @@ const docTemplate = `{
25132591
"description": "\"pixi\" or \"uv\"",
25142592
"type": "string"
25152593
},
2594+
"path": {
2595+
"description": "filesystem path (local-mode)",
2596+
"type": "string"
2597+
},
25162598
"size_bytes": {
25172599
"type": "integer"
25182600
},
2601+
"source": {
2602+
"description": "\"managed\", \"local\"",
2603+
"type": "string"
2604+
},
25192605
"status": {
25202606
"$ref": "#/definitions/models.WorkspaceStatus"
25212607
},

0 commit comments

Comments
 (0)