@brimveyn/aimux 1.20.4 → 1.22.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.
- package/README.md +27 -25
- package/package.json +2 -2
- package/src/app-runtime/auto-commit-driver.ts +22 -27
- package/src/app-runtime/backend-attach-runtime.ts +43 -36
- package/src/app-runtime/backend-runtime-events.ts +107 -88
- package/src/app-runtime/editor-actions.ts +201 -0
- package/src/app-runtime/git-actions.ts +197 -0
- package/src/app-runtime/measure-ref.ts +19 -0
- package/src/app-runtime/navigation-actions.ts +213 -0
- package/src/app-runtime/project-actions.ts +188 -0
- package/src/app-runtime/prompt-injection.ts +123 -0
- package/src/app-runtime/selection.ts +29 -0
- package/src/app-runtime/settings-actions.ts +156 -0
- package/src/app-runtime/setup-actions.ts +237 -0
- package/src/app-runtime/side-effect-context.ts +33 -0
- package/src/app-runtime/side-effects.ts +305 -1589
- package/src/app-runtime/snippet-actions.ts +3 -2
- package/src/app-runtime/split-drag-controller.ts +4 -8
- package/src/app-runtime/tab-actions.ts +218 -0
- package/src/app-runtime/tab-runtime-timeouts.ts +1 -1
- package/src/app-runtime/use-auto-commit-driver.ts +14 -13
- package/src/app-runtime/use-backend-runtime.ts +12 -11
- package/src/app-runtime/use-directory-search.ts +5 -4
- package/src/app-runtime/use-mouse-handlers.ts +28 -42
- package/src/app-runtime/{use-workspace-autosave.ts → use-project-autosave.ts} +4 -4
- package/src/app-runtime/use-renderer-bindings.ts +2 -1
- package/src/app-runtime/use-setup-runner.ts +96 -0
- package/src/app-runtime/use-terminal-resize.ts +28 -33
- package/src/app-runtime/workspace-actions.ts +366 -0
- package/src/app-runtime/workspace-activity.ts +138 -0
- package/src/app-runtime/workspace-naming.ts +90 -0
- package/src/app.tsx +102 -85
- package/src/assets/sounds/bell.wav +0 -0
- package/src/assets/sounds/ding.wav +0 -0
- package/src/assets/sounds/plane.wav +0 -0
- package/src/cli/client/daemon-client.ts +2 -2
- package/src/cli/client/project-resolver.ts +112 -0
- package/src/cli/commands/project/close.ts +32 -0
- package/src/cli/commands/project/create.ts +93 -0
- package/src/cli/commands/project/list.ts +25 -0
- package/src/cli/commands/project/show.ts +32 -0
- package/src/cli/commands/{workspace → project}/switch.ts +19 -19
- package/src/cli/commands/tab/await.ts +2 -2
- package/src/cli/commands/tab/close.ts +3 -3
- package/src/cli/commands/tab/create.ts +74 -73
- package/src/cli/commands/tab/focus.ts +3 -3
- package/src/cli/commands/tab/list.ts +6 -6
- package/src/cli/commands/tab/run.ts +2 -2
- package/src/cli/commands/tab/send.ts +2 -2
- package/src/cli/commands/tab/snapshot.ts +2 -2
- package/src/cli/commands/tab/tail.ts +2 -2
- package/src/cli/commands/tab/wait.ts +2 -2
- package/src/cli/commands/worker/await.ts +3 -3
- package/src/cli/commands/worker/doctor.ts +30 -30
- package/src/cli/commands/worker/list.ts +15 -15
- package/src/cli/commands/worker/prompt.ts +3 -3
- package/src/cli/commands/worker/run.ts +23 -23
- package/src/cli/commands/worker/shared.ts +53 -59
- package/src/cli/commands/worker/stop.ts +29 -29
- package/src/cli/commands/worker/submit.ts +3 -3
- package/src/cli/commands/{worktree → workspace}/create-core.ts +26 -26
- package/src/cli/commands/workspace/create.ts +30 -68
- package/src/cli/commands/workspace/list.ts +52 -9
- package/src/cli/commands/workspace/remove.ts +81 -0
- package/src/cli/completion/plan.ts +2 -2
- package/src/cli/completion/sources.ts +18 -18
- package/src/cli/context.ts +13 -13
- package/src/cli/flags.ts +30 -10
- package/src/cli/index.ts +26 -16
- package/src/cli/output.ts +1 -1
- package/src/cli/registry.ts +12 -12
- package/src/config/loader.ts +16 -5
- package/src/config.ts +149 -101
- package/src/daemon/catalog-writer.ts +67 -67
- package/src/daemon/daemon.ts +205 -201
- package/src/daemon/session-manager.ts +39 -39
- package/src/daemon/session-registry.ts +14 -14
- package/src/git/divergence.ts +33 -2
- package/src/git/git-poller.ts +1 -1
- package/src/git/git-status.ts +2 -2
- package/src/git/{move-worktree.ts → move-workspace.ts} +5 -5
- package/src/git/pr-merge.ts +64 -0
- package/src/git/pr-status-poller.ts +57 -0
- package/src/git/pr-status.ts +227 -0
- package/src/git/repo-discovery.ts +1 -1
- package/src/git/use-repo-discovery.ts +1 -1
- package/src/git/workspace-branch-poller.ts +54 -0
- package/src/git/workspace-divergence-poller.ts +65 -0
- package/src/git/worktree.ts +29 -0
- package/src/index.tsx +11 -6
- package/src/input/keymap/help-entries.ts +7 -5
- package/src/input/modes/bridge.ts +13 -12
- package/src/input/modes/handlers/shared.ts +1 -1
- package/src/input/modes/transitions.ts +34 -21
- package/src/input/modes/types.ts +48 -31
- package/src/ipc/manager-protocol.ts +44 -44
- package/src/ipc/protocol.ts +192 -166
- package/src/platform/open-url.ts +39 -0
- package/src/platform/play-sound.ts +184 -0
- package/src/platform/project-search.ts +8 -8
- package/src/platform/worktree-paths.ts +8 -6
- package/src/pty/assistant-question-extractor.ts +1 -1
- package/src/pty/assistant-status-detection-loop.ts +79 -55
- package/src/pty/assistant-status-detector.ts +40 -15
- package/src/pty/command-registry.ts +81 -1
- package/src/restart-terminal-manager.ts +1 -1
- package/src/services/ai-usage/provider.ts +9 -2
- package/src/services/ai-usage/spawn.ts +3 -1
- package/src/session-backend/bootstrap.ts +4 -4
- package/src/session-backend/local-session-backend.ts +78 -75
- package/src/session-backend/remote-session-backend.ts +64 -52
- package/src/session-backend/types.ts +43 -32
- package/src/settings/live.ts +90 -0
- package/src/settings/search.ts +43 -0
- package/src/settings/sections/about.ts +49 -0
- package/src/settings/sections/appearance.ts +54 -0
- package/src/settings/sections/automation.ts +105 -0
- package/src/settings/sections/commands.ts +72 -0
- package/src/settings/sections/editor.ts +56 -0
- package/src/settings/sections/experimental.ts +57 -0
- package/src/settings/sections/git.ts +112 -0
- package/src/settings/sections/index.ts +95 -0
- package/src/settings/sections/integrations.ts +23 -0
- package/src/settings/sections/layout.ts +72 -0
- package/src/settings/sections/notifications.ts +99 -0
- package/src/settings/sections/setup.ts +54 -0
- package/src/settings/sections/status-bar.ts +63 -0
- package/src/settings/settings-store.ts +207 -0
- package/src/settings/types.ts +108 -0
- package/src/snippets/run-shell-var.ts +48 -22
- package/src/state/actions.ts +342 -0
- package/src/state/app-store.ts +2 -1
- package/src/state/bars.ts +75 -0
- package/src/state/dispatch-ref.ts +1 -1
- package/src/state/git-pane-sizing.ts +0 -9
- package/src/state/layout-resize.ts +2 -2
- package/src/state/pr-status-store.ts +39 -0
- package/src/state/project-catalog.ts +275 -0
- package/src/state/project-data.ts +143 -0
- package/src/state/{session-persistence.ts → project-persistence.ts} +77 -73
- package/src/state/project-save.ts +46 -0
- package/src/state/project-workspaces.ts +392 -0
- package/src/state/reducers/auto-commit-state.ts +11 -10
- package/src/state/reducers/git-commit-modal-state.ts +122 -0
- package/src/state/reducers/git-mode-state.ts +1 -1
- package/src/state/reducers/git-panel-state.ts +10 -53
- package/src/state/reducers/modal-state.ts +290 -681
- package/src/state/reducers/multi-repo-state.ts +3 -6
- package/src/state/reducers/project-state.ts +312 -0
- package/src/state/reducers/settings-state.ts +80 -0
- package/src/state/reducers/tab-state.ts +107 -60
- package/src/state/reducers/ui-state.ts +86 -15
- package/src/state/selectors.ts +49 -37
- package/src/state/store.ts +83 -57
- package/src/state/types.ts +261 -417
- package/src/state/validation.ts +14 -12
- package/src/terminal-manager/manager-client.ts +32 -32
- package/src/terminal-manager/terminal-manager.ts +24 -24
- package/src/ui/components/git/diff-renderer/pierre-diff.tsx +2 -1
- package/src/ui/components/git/git-panel.tsx +2 -2
- package/src/ui/components/git/git-view.tsx +12 -12
- package/src/ui/components/git/image-diff/image-diff-view.tsx +1 -1
- package/src/ui/components/git/pane/git-pane-header.tsx +105 -39
- package/src/ui/components/git/pane/git-pane-widget.tsx +37 -23
- package/src/ui/components/git/pane/pr-checks-panel.tsx +197 -0
- package/src/ui/components/git/pane/pr-state-row.tsx +103 -0
- package/src/ui/components/layout/bar.tsx +191 -0
- package/src/ui/components/layout/sidebar/project-list.tsx +444 -0
- package/src/ui/components/layout/sidebar/tab-item.tsx +27 -12
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +1 -1
- package/src/ui/components/layout/sidebar/workspace-row.tsx +253 -0
- package/src/ui/components/layout/status-bar.tsx +6 -2
- package/src/ui/components/layout/terminal-pane.tsx +34 -29
- package/src/ui/components/layout/top-tab-bar.tsx +48 -37
- package/src/ui/components/modals/git/git-commit-modal.tsx +8 -8
- package/src/ui/components/modals/{sessions/create-session-modal.tsx → projects/create-project-modal.tsx} +12 -12
- package/src/ui/components/modals/{sessions/session-name-modal.tsx → projects/project-name-modal.tsx} +2 -2
- package/src/ui/components/modals/{sessions/session-picker-modal.tsx → projects/project-picker-modal.tsx} +33 -35
- package/src/ui/components/modals/settings/settings-search-modal.tsx +70 -0
- package/src/ui/components/modals/shared/form.tsx +34 -22
- package/src/ui/components/modals/shared/picker.tsx +10 -10
- package/src/ui/components/modals/shared/{worktree-delete-confirm.tsx → workspace-delete-confirm.tsx} +9 -9
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +30 -324
- package/src/ui/components/modals/workspace/create-workspace-modal.tsx +101 -0
- package/src/ui/components/modals/{worktree/worktree-move-confirm-modal.tsx → workspace/workspace-move-confirm-modal.tsx} +5 -5
- package/src/ui/components/modals/{worktree/worktree-move-modal.tsx → workspace/workspace-move-modal.tsx} +29 -27
- package/src/ui/components/primitives/input-field.tsx +7 -3
- package/src/ui/components/primitives/surface.tsx +3 -0
- package/src/ui/components/settings/row-value.tsx +74 -0
- package/src/ui/components/settings/settings-row.tsx +78 -0
- package/src/ui/components/settings/settings-view.tsx +169 -0
- package/src/ui/components/setup/setup-widget.tsx +152 -0
- package/src/ui/flash/build-labels.ts +27 -27
- package/src/ui/hooks/use-activity-sprite.ts +73 -0
- package/src/ui/hooks/use-scroll-active-into-view.ts +26 -0
- package/src/ui/{session-ordering.ts → project-ordering.ts} +4 -4
- package/src/ui/root.tsx +164 -230
- package/src/ui/status-bar-model.ts +48 -34
- package/src/ui/terminal-graphics/kitty.ts +28 -2
- package/src/ui/terminal-graphics/sprites/done@330/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/done@330/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/idle@1000/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/idle@1000/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/waiting@170/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/waiting@170/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/2.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/3.png +0 -0
- package/src/ui/terminal-graphics/sprites.ts +246 -0
- package/src/ui/truncate.ts +7 -0
- package/src/ui/widgets/registry.tsx +21 -0
- package/src/ui/widgets/widget-context-menu.ts +85 -0
- package/src/update.ts +2 -2
- package/src/app-runtime/session-actions.ts +0 -187
- package/src/cli/client/workspace-resolver.ts +0 -110
- package/src/cli/commands/workspace/close.ts +0 -32
- package/src/cli/commands/workspace/show.ts +0 -32
- package/src/cli/commands/worktree/create.ts +0 -55
- package/src/cli/commands/worktree/list.ts +0 -68
- package/src/cli/commands/worktree/remove.ts +0 -81
- package/src/git/worktree-branch-poller.ts +0 -54
- package/src/git/worktree-divergence-poller.ts +0 -59
- package/src/state/reducers/session-state.ts +0 -262
- package/src/state/session-catalog.ts +0 -143
- package/src/state/session-worktrees.ts +0 -260
- package/src/state/workspace-save.ts +0 -45
- package/src/ui/components/git/pane/git-pane-context-menu.ts +0 -26
- package/src/ui/components/layout/sidebar/sidebar.tsx +0 -163
- package/src/ui/components/layout/sidebar/use-top-tab-bar-auto-scroll.ts +0 -30
- package/src/ui/components/layout/sidebar/workspace-list.tsx +0 -418
- package/src/ui/components/layout/sidebar/worktree-row.tsx +0 -149
- /package/src/ui/{components/git/image-diff → terminal-graphics}/dimensions.ts +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ProjectRecord, WorkspaceRecord } from '../../../state/types'
|
|
2
2
|
import type { DaemonClient } from '../../client/daemon-client'
|
|
3
3
|
|
|
4
4
|
import { createGitWorktree, removeGitWorktree, resolveGitRef } from '../../../git/worktree'
|
|
5
|
-
import {
|
|
5
|
+
import { IPC_CAPABILITY_WORKSPACE_LIFECYCLE_EVENTS } from '../../../ipc/protocol'
|
|
6
6
|
import { createPrefixedId } from '../../../platform/id'
|
|
7
7
|
import {
|
|
8
8
|
assertSafeAimuxWorktreePath,
|
|
@@ -11,59 +11,59 @@ import {
|
|
|
11
11
|
pruneEmptyWorktreeParent,
|
|
12
12
|
} from '../../../platform/worktree-paths'
|
|
13
13
|
|
|
14
|
-
export interface
|
|
14
|
+
export interface CreateWorkspaceParams {
|
|
15
15
|
/** Base ref for the branch (callers default to 'HEAD'). */
|
|
16
16
|
base: string
|
|
17
17
|
/** Branch name (callers default to `aimux/<name>`). */
|
|
18
18
|
branch: string
|
|
19
19
|
daemon: DaemonClient
|
|
20
20
|
name: string
|
|
21
|
-
|
|
21
|
+
project: ProjectRecord
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Create a git worktree + its catalog record for a
|
|
26
|
-
* `
|
|
27
|
-
* capability BEFORE touching disk, and rolls back the on-disk
|
|
25
|
+
* Create a git worktree + its catalog record for a project. Shared by
|
|
26
|
+
* `workspace create` and `tab create --new-workspace`. Checks the daemon
|
|
27
|
+
* capability BEFORE touching disk, and rolls back the on-disk workspace if
|
|
28
28
|
* catalog registration fails (so `worktree list` never surfaces an orphan).
|
|
29
29
|
* Throws on any failure; returns the registered record on success.
|
|
30
30
|
*/
|
|
31
|
-
export async function
|
|
32
|
-
params:
|
|
33
|
-
): Promise<
|
|
34
|
-
const { base, branch, daemon, name,
|
|
31
|
+
export async function createProjectWorkspace(
|
|
32
|
+
params: CreateWorkspaceParams
|
|
33
|
+
): Promise<WorkspaceRecord> {
|
|
34
|
+
const { base, branch, daemon, name, project } = params
|
|
35
35
|
|
|
36
|
-
const primary =
|
|
36
|
+
const primary = project.workspaces?.find((w) => w.source === 'primary')
|
|
37
37
|
if (!primary) {
|
|
38
38
|
throw new Error(
|
|
39
|
-
`
|
|
39
|
+
`project "${project.name}" has no primary workspace — set --project when creating it`
|
|
40
40
|
)
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// Check the daemon's capability BEFORE mutating disk — otherwise a capability
|
|
44
44
|
// mismatch would leave a git worktree on disk with no catalog record.
|
|
45
|
-
if (!daemon.hasCapability(
|
|
45
|
+
if (!daemon.hasCapability(IPC_CAPABILITY_WORKSPACE_LIFECYCLE_EVENTS)) {
|
|
46
46
|
throw new Error(
|
|
47
|
-
'daemon predates
|
|
47
|
+
'daemon predates workspaceLifecycleEvents capability — restart aimux to pick up the new daemon'
|
|
48
48
|
)
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// Verify the base ref in the REPO WE ARE ABOUT TO USE, before touching disk.
|
|
52
|
-
// The
|
|
53
|
-
// project A while the resolved
|
|
52
|
+
// The project decides the repo, so a caller who believes it is orchestrating
|
|
53
|
+
// project A while the resolved project points at project B would otherwise
|
|
54
54
|
// get either a confusing bare git error or — when the ref exists in both repos
|
|
55
55
|
// — a silent success in the wrong project.
|
|
56
56
|
if ((await resolveGitRef(primary.repoRoot, base)) === undefined) {
|
|
57
57
|
throw new Error(
|
|
58
|
-
`base ref "${base}" does not exist in ${primary.repoRoot} (
|
|
58
|
+
`base ref "${base}" does not exist in ${primary.repoRoot} (project "${project.name}") — check that this is the repository you meant`
|
|
59
59
|
)
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
const
|
|
62
|
+
const workspaceId = createPrefixedId('workspace')
|
|
63
63
|
const targetPath = makeWorktreePath({
|
|
64
64
|
repoRoot: primary.repoRoot,
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
workspaceId,
|
|
66
|
+
workspaceName: name,
|
|
67
67
|
})
|
|
68
68
|
await ensureAimuxWorktreeRoot()
|
|
69
69
|
await assertSafeAimuxWorktreePath(targetPath)
|
|
@@ -83,12 +83,12 @@ export async function createWorkspaceWorktree(
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
const now = new Date().toISOString()
|
|
86
|
-
const record:
|
|
86
|
+
const record: WorkspaceRecord = {
|
|
87
87
|
baseRef: base,
|
|
88
88
|
branch,
|
|
89
89
|
createdAt: now,
|
|
90
90
|
createdByAimux: true,
|
|
91
|
-
id:
|
|
91
|
+
id: workspaceId,
|
|
92
92
|
name,
|
|
93
93
|
path: targetPath,
|
|
94
94
|
repoRoot: primary.repoRoot,
|
|
@@ -97,16 +97,16 @@ export async function createWorkspaceWorktree(
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
try {
|
|
100
|
-
await daemon.expectOk('
|
|
100
|
+
await daemon.expectOk('addWorkspaceRecord', { projectId: project.id, workspace: record })
|
|
101
101
|
} catch (error) {
|
|
102
|
-
// Catalog registration failed — roll back the on-disk
|
|
102
|
+
// Catalog registration failed — roll back the on-disk workspace so
|
|
103
103
|
// `worktree list` doesn't perpetually surface an orphan. Swallow rollback
|
|
104
104
|
// errors: report the original failure, the real problem to surface.
|
|
105
105
|
try {
|
|
106
106
|
await removeGitWorktree({ force: true, repoPath: primary.repoRoot, targetPath })
|
|
107
107
|
await pruneEmptyWorktreeParent(targetPath)
|
|
108
108
|
} catch {
|
|
109
|
-
// Best-effort rollback; leave the git-side
|
|
109
|
+
// Best-effort rollback; leave the git-side workspace if it can't be removed
|
|
110
110
|
// cleanly. `worktree list` will flag it as gitTracked with no catalog.
|
|
111
111
|
}
|
|
112
112
|
throw error
|
|
@@ -1,93 +1,55 @@
|
|
|
1
|
-
import { resolve as resolvePath } from 'node:path'
|
|
2
|
-
|
|
3
1
|
import type { CliCommand } from '../../registry'
|
|
4
2
|
|
|
5
|
-
import { IPC_CAPABILITY_WORKSPACE_LIFECYCLE } from '../../../ipc/protocol'
|
|
6
3
|
import { SHARED_FLAGS } from '../../flags'
|
|
7
|
-
import { EXIT_OK,
|
|
8
|
-
|
|
9
|
-
const DEFAULT_WAIT_TIMEOUT_MS = 30_000
|
|
4
|
+
import { EXIT_OK, writeJson } from '../../output'
|
|
5
|
+
import { createProjectWorkspace } from './create-core'
|
|
10
6
|
|
|
11
7
|
export const workspaceCreate: CliCommand = {
|
|
12
|
-
args: [
|
|
8
|
+
args: [],
|
|
13
9
|
flags: [
|
|
14
10
|
...SHARED_FLAGS,
|
|
15
11
|
{
|
|
16
|
-
complete: { kind: '
|
|
17
|
-
description: '
|
|
12
|
+
complete: { kind: 'none' },
|
|
13
|
+
description: 'display name for the new workspace',
|
|
18
14
|
kind: 'string',
|
|
19
|
-
name: '
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
description: 'immediately switch the running UI to the new workspace',
|
|
23
|
-
kind: 'boolean',
|
|
24
|
-
name: 'switch',
|
|
15
|
+
name: 'name',
|
|
25
16
|
},
|
|
26
17
|
{
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
complete: { kind: 'none' },
|
|
19
|
+
description: 'branch name (defaults to aimux/<name>)',
|
|
20
|
+
kind: 'string',
|
|
21
|
+
name: 'branch',
|
|
30
22
|
},
|
|
31
23
|
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
complete: { kind: 'dynamic', source: 'git-ref' },
|
|
25
|
+
description: 'base ref for the branch (defaults to HEAD)',
|
|
26
|
+
kind: 'string',
|
|
27
|
+
name: 'base',
|
|
35
28
|
},
|
|
36
29
|
],
|
|
37
30
|
group: 'workspace',
|
|
38
31
|
run: async (ctx) => {
|
|
39
|
-
const name = ctx.args.
|
|
32
|
+
const name = ctx.args.flags.name
|
|
40
33
|
if (typeof name !== 'string' || name.length === 0) {
|
|
41
|
-
throw new Error('
|
|
42
|
-
}
|
|
43
|
-
const projectRaw =
|
|
44
|
-
typeof ctx.args.flags.project === 'string' ? ctx.args.flags.project : undefined
|
|
45
|
-
const projectPath = projectRaw === undefined ? undefined : resolvePath(projectRaw)
|
|
46
|
-
const doSwitch = ctx.args.flags.switch === true
|
|
47
|
-
const wait = ctx.args.flags.wait === true
|
|
48
|
-
const timeoutMs =
|
|
49
|
-
typeof ctx.args.flags.timeout === 'number' ? ctx.args.flags.timeout : DEFAULT_WAIT_TIMEOUT_MS
|
|
50
|
-
|
|
51
|
-
if (wait && !doSwitch) {
|
|
52
|
-
// Without --switch, the UI never emits an ack event, so there's
|
|
53
|
-
// nothing meaningful for --wait to wait on.
|
|
54
|
-
throw new Error('--wait requires --switch (nothing to wait for otherwise)')
|
|
34
|
+
throw new Error('--name is required')
|
|
55
35
|
}
|
|
36
|
+
const branch =
|
|
37
|
+
typeof ctx.args.flags.branch === 'string' ? ctx.args.flags.branch : `aimux/${name}`
|
|
38
|
+
const base = typeof ctx.args.flags.base === 'string' ? ctx.args.flags.base : 'HEAD'
|
|
56
39
|
|
|
40
|
+
const project = ctx.getProject()
|
|
57
41
|
const daemon = await ctx.getDaemon()
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
writeJson({ name, projectPath, switch: doSwitch })
|
|
67
|
-
return EXIT_OK
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Subscribe BEFORE sending the request. The UI's create+switch path
|
|
71
|
-
// relays as `workspaceSwitched` once handleCreateSessionEffect has
|
|
72
|
-
// dispatched load-session. Match on name+projectPath since the CLI
|
|
73
|
-
// doesn't know the id the UI will assign to the new session.
|
|
74
|
-
const settled = new Promise<number>((resolve) => {
|
|
75
|
-
const off = daemon.on('workspaceSwitched', (payload) => {
|
|
76
|
-
off()
|
|
77
|
-
clearTimeout(timer)
|
|
78
|
-
writeJson({ name, projectPath, sessionId: payload.sessionId, switch: doSwitch })
|
|
79
|
-
resolve(EXIT_OK)
|
|
80
|
-
})
|
|
81
|
-
const timer = setTimeout(() => {
|
|
82
|
-
off()
|
|
83
|
-
writeJson({ error: 'timed out waiting for workspaceSwitched', name, projectPath })
|
|
84
|
-
resolve(EXIT_TIMEOUT)
|
|
85
|
-
}, timeoutMs)
|
|
42
|
+
const record = await createProjectWorkspace({ base, branch, daemon, name, project })
|
|
43
|
+
|
|
44
|
+
writeJson({
|
|
45
|
+
branch: record.branch,
|
|
46
|
+
id: record.id,
|
|
47
|
+
name: record.name,
|
|
48
|
+
path: record.path,
|
|
49
|
+
repoRoot: record.repoRoot,
|
|
86
50
|
})
|
|
87
|
-
|
|
88
|
-
await daemon.expectOk('createWorkspace', { name, projectPath, switch: doSwitch })
|
|
89
|
-
return settled
|
|
51
|
+
return EXIT_OK
|
|
90
52
|
},
|
|
91
|
-
summary: 'Create a new workspace
|
|
53
|
+
summary: 'Create a new workspace in the active project',
|
|
92
54
|
verb: 'create',
|
|
93
55
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CliCommand } from '../../registry'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { listGitWorktrees } from '../../../git/worktree'
|
|
4
|
+
import { IPC_CAPABILITY_LIST_TABS } from '../../../ipc/protocol'
|
|
4
5
|
import { SHARED_FLAGS } from '../../flags'
|
|
5
6
|
import { EXIT_OK, writeJson } from '../../output'
|
|
6
7
|
|
|
@@ -8,18 +9,60 @@ export const workspaceList: CliCommand = {
|
|
|
8
9
|
args: [],
|
|
9
10
|
flags: SHARED_FLAGS,
|
|
10
11
|
group: 'workspace',
|
|
11
|
-
run: async () => {
|
|
12
|
-
const
|
|
12
|
+
run: async (ctx) => {
|
|
13
|
+
const project = ctx.getProject()
|
|
14
|
+
const records = project.workspaces ?? []
|
|
15
|
+
|
|
16
|
+
// Count live tabs per workspace so an orchestrator can see co-location
|
|
17
|
+
// (several workers sharing one workspace) at a glance. Best-effort: `null`
|
|
18
|
+
// when the daemon is unreachable or predates the listTabs capability —
|
|
19
|
+
// `worktree list` otherwise reads purely from the catalog + git, so we don't
|
|
20
|
+
// want a down daemon to fail it.
|
|
21
|
+
const tabCounts = new Map<string, number>()
|
|
22
|
+
let tabCountsAvailable = false
|
|
23
|
+
try {
|
|
24
|
+
const daemon = await ctx.getDaemon()
|
|
25
|
+
if (daemon.hasCapability(IPC_CAPABILITY_LIST_TABS)) {
|
|
26
|
+
const { tabs } = await daemon.listTabs(project.id)
|
|
27
|
+
for (const tab of tabs) {
|
|
28
|
+
if (tab.workspaceId != null) {
|
|
29
|
+
tabCounts.set(tab.workspaceId, (tabCounts.get(tab.workspaceId) ?? 0) + 1)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
tabCountsAvailable = true
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// daemon unreachable — leave tabCount null rather than failing the list.
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Cross-check against git so we can flag catalog entries that git no
|
|
39
|
+
// longer knows about (prunable / vanished) — otherwise the CLI would
|
|
40
|
+
// happily report workspaces that don't exist on disk.
|
|
41
|
+
const primary = records.find((w) => w.source === 'primary')
|
|
42
|
+
const gitPaths = new Set<string>()
|
|
43
|
+
if (primary) {
|
|
44
|
+
for (const w of await listGitWorktrees(primary.repoRoot)) {
|
|
45
|
+
if (w.prunable !== true) gitPaths.add(w.path)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
13
49
|
writeJson({
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
50
|
+
activeWorkspaceId: project.activeWorkspaceId ?? null,
|
|
51
|
+
projectId: project.id,
|
|
52
|
+
workspaces: records.map((w) => ({
|
|
53
|
+
branch: w.branch,
|
|
54
|
+
createdByAimux: w.createdByAimux,
|
|
55
|
+
gitTracked: primary ? gitPaths.has(w.path) : null,
|
|
56
|
+
id: w.id,
|
|
57
|
+
name: w.name,
|
|
58
|
+
path: w.path,
|
|
59
|
+
repoRoot: w.repoRoot,
|
|
60
|
+
source: w.source,
|
|
61
|
+
tabCount: tabCountsAvailable ? (tabCounts.get(w.id) ?? 0) : null,
|
|
19
62
|
})),
|
|
20
63
|
})
|
|
21
64
|
return EXIT_OK
|
|
22
65
|
},
|
|
23
|
-
summary: 'List
|
|
66
|
+
summary: 'List workspaces for a project',
|
|
24
67
|
verb: 'list',
|
|
25
68
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { CliCommand } from '../../registry'
|
|
2
|
+
|
|
3
|
+
import { removeGitWorktree } from '../../../git/worktree'
|
|
4
|
+
import {
|
|
5
|
+
IPC_CAPABILITY_LIST_TABS,
|
|
6
|
+
IPC_CAPABILITY_WORKSPACE_LIFECYCLE_EVENTS,
|
|
7
|
+
} from '../../../ipc/protocol'
|
|
8
|
+
import { pruneEmptyWorktreeParent } from '../../../platform/worktree-paths'
|
|
9
|
+
import { SHARED_FLAGS } from '../../flags'
|
|
10
|
+
import { EXIT_OK, writeJson } from '../../output'
|
|
11
|
+
|
|
12
|
+
export const workspaceRemove: CliCommand = {
|
|
13
|
+
args: [{ complete: { kind: 'dynamic', source: 'workspace' }, name: 'workspace', required: true }],
|
|
14
|
+
flags: [
|
|
15
|
+
...SHARED_FLAGS,
|
|
16
|
+
{ description: 'pass --force to git worktree remove', kind: 'boolean', name: 'force' },
|
|
17
|
+
],
|
|
18
|
+
group: 'workspace',
|
|
19
|
+
run: async (ctx) => {
|
|
20
|
+
const target = ctx.args.positionals[0]
|
|
21
|
+
if (typeof target !== 'string' || target.length === 0) {
|
|
22
|
+
throw new Error('workspace id or path is required')
|
|
23
|
+
}
|
|
24
|
+
const force = ctx.args.flags.force === true
|
|
25
|
+
|
|
26
|
+
const project = ctx.getProject()
|
|
27
|
+
const workspace =
|
|
28
|
+
project.workspaces?.find((w) => w.id === target) ??
|
|
29
|
+
project.workspaces?.find((w) => w.path === target)
|
|
30
|
+
if (!workspace) {
|
|
31
|
+
const known = project.workspaces?.map((w) => w.id).join(', ') ?? '(none)'
|
|
32
|
+
throw new Error(`unknown workspace: ${target} (known: ${known})`)
|
|
33
|
+
}
|
|
34
|
+
if (workspace.source === 'primary') {
|
|
35
|
+
throw new Error('refusing to remove the primary workspace')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const primary = project.workspaces?.find((w) => w.source === 'primary')
|
|
39
|
+
if (!primary) {
|
|
40
|
+
throw new Error('project has no primary workspace — cannot resolve repoRoot for git remove')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const daemon = await ctx.getDaemon()
|
|
44
|
+
if (!daemon.hasCapability(IPC_CAPABILITY_WORKSPACE_LIFECYCLE_EVENTS)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
'daemon predates workspaceLifecycleEvents capability — restart aimux to pick up the new daemon'
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
if (daemon.hasCapability(IPC_CAPABILITY_LIST_TABS)) {
|
|
50
|
+
const live = (await daemon.listTabs(project.id)).tabs.filter(
|
|
51
|
+
(tab) => tab.workspaceId === workspace.id
|
|
52
|
+
)
|
|
53
|
+
if (live.length > 0) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`refusing to remove workspace with live tabs: ${live.map((tab) => tab.id).join(', ')}`
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// All capability and liveness checks happen before touching git. If git
|
|
61
|
+
// refuses a dirty workspace the catalog remains unchanged.
|
|
62
|
+
await removeGitWorktree({ force, repoPath: primary.repoRoot, targetPath: workspace.path })
|
|
63
|
+
await pruneEmptyWorktreeParent(workspace.path)
|
|
64
|
+
try {
|
|
65
|
+
await daemon.expectOk('removeWorkspaceRecord', {
|
|
66
|
+
projectId: project.id,
|
|
67
|
+
workspaceId: workspace.id,
|
|
68
|
+
})
|
|
69
|
+
} catch (error) {
|
|
70
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
71
|
+
throw new Error(
|
|
72
|
+
`worktree removed from git but catalog reconciliation failed for ${workspace.id}: ${message}`
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
writeJson({ id: workspace.id, name: workspace.name, path: workspace.path })
|
|
77
|
+
return EXIT_OK
|
|
78
|
+
},
|
|
79
|
+
summary: 'Remove a workspace from the active project',
|
|
80
|
+
verb: 'remove',
|
|
81
|
+
}
|
|
@@ -51,10 +51,10 @@ const COMPLETION_SUBCOMMANDS: readonly CompletionCandidate[] = [
|
|
|
51
51
|
]
|
|
52
52
|
|
|
53
53
|
const GROUP_DESCRIPTIONS: Record<string, string> = {
|
|
54
|
+
project: 'Projects (projects) in the profile catalog',
|
|
54
55
|
tab: 'Drive individual tabs (create, send, snapshot, await)',
|
|
55
56
|
worker: 'Named agent workers — the preferred orchestration surface',
|
|
56
|
-
workspace: '
|
|
57
|
-
worktree: 'Git worktrees attached to the active workspace',
|
|
57
|
+
workspace: 'Git workspaces attached to the active project',
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function groupCandidates(): CompletionCandidate[] {
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* shell must never see a stack trace where a completion list belongs.
|
|
9
9
|
*
|
|
10
10
|
* Phase 1 implements only the sources that read local state (built-in
|
|
11
|
-
* assistants, the
|
|
12
|
-
*
|
|
11
|
+
* assistants, the project catalog). Daemon-backed sources — tabs, workers,
|
|
12
|
+
* workspaces — and git refs return nothing until phase 2 wires them up.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { DynamicCompletionSource } from '../flags'
|
|
@@ -24,20 +24,20 @@ function assistantCandidates(): CompletionCandidate[] {
|
|
|
24
24
|
}))
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
async function
|
|
28
|
-
// Imported lazily: the
|
|
27
|
+
async function projectCandidates(): Promise<CompletionCandidate[]> {
|
|
28
|
+
// Imported lazily: the project catalog pulls in config + state modules that
|
|
29
29
|
// the static-source paths (groups, verbs, flags) have no reason to load.
|
|
30
|
-
const {
|
|
31
|
-
const
|
|
30
|
+
const { listProjects } = await import('../client/project-resolver')
|
|
31
|
+
const projects = listProjects()
|
|
32
32
|
const nameCounts = new Map<string, number>()
|
|
33
|
-
for (const
|
|
34
|
-
nameCounts.set(
|
|
33
|
+
for (const project of projects) {
|
|
34
|
+
nameCounts.set(project.name, (nameCounts.get(project.name) ?? 0) + 1)
|
|
35
35
|
}
|
|
36
|
-
return
|
|
37
|
-
// Ambiguous names can't be resolved by `--
|
|
38
|
-
(nameCounts.get(
|
|
39
|
-
? { description:
|
|
40
|
-
: { description:
|
|
36
|
+
return projects.map((project) =>
|
|
37
|
+
// Ambiguous names can't be resolved by `--project`, so offer the id.
|
|
38
|
+
(nameCounts.get(project.name) ?? 0) > 1
|
|
39
|
+
? { description: project.name, value: project.id }
|
|
40
|
+
: { description: project.id, value: project.name }
|
|
41
41
|
)
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -45,10 +45,10 @@ async function resolveSource(source: DynamicCompletionSource): Promise<Completio
|
|
|
45
45
|
switch (source) {
|
|
46
46
|
case 'assistant':
|
|
47
47
|
return assistantCandidates()
|
|
48
|
-
case '
|
|
49
|
-
return await
|
|
50
|
-
// Phase 2: 'tab' | 'worker' | '
|
|
51
|
-
// deadline; 'git-ref' needs a `git for-each-ref` in the
|
|
48
|
+
case 'project':
|
|
49
|
+
return await projectCandidates()
|
|
50
|
+
// Phase 2: 'tab' | 'worker' | 'workspace' need a daemon round-trip under a
|
|
51
|
+
// deadline; 'git-ref' needs a `git for-each-ref` in the project repo.
|
|
52
52
|
default:
|
|
53
53
|
return []
|
|
54
54
|
}
|
|
@@ -56,7 +56,7 @@ async function resolveSource(source: DynamicCompletionSource): Promise<Completio
|
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Resolve a dynamic source, filter by the partial word, and re-apply the
|
|
59
|
-
* prefix the planner stripped (e.g. `--
|
|
59
|
+
* prefix the planner stripped (e.g. `--project=`). Always resolves.
|
|
60
60
|
*/
|
|
61
61
|
export async function resolveDynamicCandidates(
|
|
62
62
|
source: DynamicCompletionSource,
|
package/src/cli/context.ts
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ProjectRecord } from '../state/types'
|
|
2
2
|
import type { DaemonClient } from './client/daemon-client'
|
|
3
|
-
import type {
|
|
3
|
+
import type { ProjectOrigin } from './client/project-resolver'
|
|
4
4
|
import type { ParsedArgs } from './flags'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Per-command context handed to `CliCommand.run`. Commands lazily resolve
|
|
8
|
-
* the
|
|
9
|
-
* only reads flags (e.g. `
|
|
8
|
+
* the project + daemon via the helpers on this object so a command that
|
|
9
|
+
* only reads flags (e.g. `project list`) doesn't have to open a socket.
|
|
10
10
|
*/
|
|
11
11
|
export interface CliContext {
|
|
12
12
|
args: ParsedArgs
|
|
13
13
|
/** Already-running daemon client, populated on first call to `daemon()`. */
|
|
14
14
|
getDaemon: () => Promise<DaemonClient>
|
|
15
|
-
/** Resolved
|
|
16
|
-
|
|
15
|
+
/** Resolved project, populated on first call to `project()`. */
|
|
16
|
+
getProject: () => ProjectRecord
|
|
17
17
|
/**
|
|
18
|
-
* Every catalogued
|
|
19
|
-
* gone, or did the active
|
|
18
|
+
* Every catalogued project. Commands that must answer "is my worker really
|
|
19
|
+
* gone, or did the active project move?" need the whole catalog, not just
|
|
20
20
|
* the resolved record. Optional so test fixtures can inject one.
|
|
21
21
|
*/
|
|
22
|
-
|
|
22
|
+
getProjects?: () => ProjectRecord[]
|
|
23
23
|
/**
|
|
24
|
-
* Where `
|
|
25
|
-
* (`
|
|
26
|
-
* `active` can drift under a command while the UI switches
|
|
24
|
+
* Where `getProject()` came from — `flag` (`--project`), `env`
|
|
25
|
+
* (`AIMUX_PROJECT`), or `active` (most recently opened project). Only
|
|
26
|
+
* `active` can drift under a command while the UI switches projects, so
|
|
27
27
|
* commands that must not follow the UI branch on this. Optional so test
|
|
28
28
|
* fixtures can build a minimal context.
|
|
29
29
|
*/
|
|
30
|
-
|
|
30
|
+
getProjectOrigin?: () => ProjectOrigin
|
|
31
31
|
}
|
package/src/cli/flags.ts
CHANGED
|
@@ -15,8 +15,8 @@ export type DynamicCompletionSource =
|
|
|
15
15
|
| 'git-ref'
|
|
16
16
|
| 'tab'
|
|
17
17
|
| 'worker'
|
|
18
|
+
| 'project'
|
|
18
19
|
| 'workspace'
|
|
19
|
-
| 'worktree'
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Where a flag value or positional draws its shell-completion candidates.
|
|
@@ -67,6 +67,23 @@ export function parseArgs(
|
|
|
67
67
|
): ParsedArgs {
|
|
68
68
|
const flagByName = new Map<string, FlagSpec>()
|
|
69
69
|
for (const spec of flagSpecs) flagByName.set(spec.name, spec)
|
|
70
|
+
// Deprecated aliases from the project/workspace rename. Unlike the command
|
|
71
|
+
// groups these are safe to map silently: --workspace named a project and
|
|
72
|
+
// --worktree named what is now a workspace, so each old name still points at
|
|
73
|
+
// the same object.
|
|
74
|
+
// ponytail: drop with the aimux-sessions.json fallback.
|
|
75
|
+
// Resolved against the declared specs, never against another alias —
|
|
76
|
+
// otherwise --worktree would chain through the --workspace alias onto
|
|
77
|
+
// --project, which is a different object entirely.
|
|
78
|
+
for (const [old, current] of [
|
|
79
|
+
['workspace', 'project'],
|
|
80
|
+
['worktree', 'workspace'],
|
|
81
|
+
['new-worktree', 'new-workspace'],
|
|
82
|
+
] as const) {
|
|
83
|
+
if (flagByName.has(old)) continue
|
|
84
|
+
const spec = flagSpecs.find((entry) => entry.name === current)
|
|
85
|
+
if (spec) flagByName.set(old, spec)
|
|
86
|
+
}
|
|
70
87
|
|
|
71
88
|
const flags: Record<string, string | number | boolean> = {}
|
|
72
89
|
const positionals: string[] = []
|
|
@@ -85,18 +102,21 @@ export function parseArgs(
|
|
|
85
102
|
if (!spec) {
|
|
86
103
|
throw new CliUsageError(`unknown flag: --${name}`)
|
|
87
104
|
}
|
|
105
|
+
// Deprecated aliases resolve to the current spec, so results are always
|
|
106
|
+
// keyed by the canonical name regardless of which spelling was typed.
|
|
107
|
+
const key = spec.name
|
|
88
108
|
if (spec.kind === 'boolean') {
|
|
89
109
|
if (eq !== -1) {
|
|
90
110
|
throw new CliUsageError(`flag --${name} does not take a value`)
|
|
91
111
|
}
|
|
92
|
-
flags[
|
|
112
|
+
flags[key] = true
|
|
93
113
|
continue
|
|
94
114
|
}
|
|
95
115
|
if (spec.kind === 'optional-string') {
|
|
96
116
|
// Value binds ONLY in the `=` form (`--flag=value`). A bare `--flag`
|
|
97
117
|
// must not swallow the next token (it may be a positional), so it
|
|
98
118
|
// parses as boolean `true`.
|
|
99
|
-
flags[
|
|
119
|
+
flags[key] = eq === -1 ? true : token.slice(eq + 1)
|
|
100
120
|
continue
|
|
101
121
|
}
|
|
102
122
|
const raw = eq === -1 ? argv[++i] : token.slice(eq + 1)
|
|
@@ -108,9 +128,9 @@ export function parseArgs(
|
|
|
108
128
|
if (!Number.isFinite(parsed)) {
|
|
109
129
|
throw new CliUsageError(`flag --${name} must be a number (got: ${raw})`)
|
|
110
130
|
}
|
|
111
|
-
flags[
|
|
131
|
+
flags[key] = parsed
|
|
112
132
|
} else {
|
|
113
|
-
flags[
|
|
133
|
+
flags[key] = raw
|
|
114
134
|
}
|
|
115
135
|
continue
|
|
116
136
|
}
|
|
@@ -129,17 +149,17 @@ export function parseArgs(
|
|
|
129
149
|
}
|
|
130
150
|
|
|
131
151
|
/**
|
|
132
|
-
* Shared flag set — every command takes these. `--
|
|
133
|
-
* target
|
|
152
|
+
* Shared flag set — every command takes these. `--project` selects the
|
|
153
|
+
* target project (by id or name); `--profile` overrides `AIMUX_PROFILE`
|
|
134
154
|
* before any runtime path is resolved; `--json` is a no-op kept for
|
|
135
155
|
* consistency with future formats.
|
|
136
156
|
*/
|
|
137
157
|
export const SHARED_FLAGS: readonly FlagSpec[] = [
|
|
138
158
|
{
|
|
139
|
-
complete: { kind: 'dynamic', source: '
|
|
140
|
-
description: '
|
|
159
|
+
complete: { kind: 'dynamic', source: 'project' },
|
|
160
|
+
description: 'project id or name',
|
|
141
161
|
kind: 'string',
|
|
142
|
-
name: '
|
|
162
|
+
name: 'project',
|
|
143
163
|
},
|
|
144
164
|
{
|
|
145
165
|
complete: { kind: 'none' },
|