@proletariat/cli 0.3.9 → 0.3.11
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 +25 -0
- package/bin/dev.js +0 -0
- package/dist/commands/action/index.js +1 -1
- package/dist/commands/action/run.js +8 -12
- package/dist/commands/agent/auth.d.ts +30 -0
- package/dist/commands/agent/auth.js +172 -0
- package/dist/commands/agent/discover.d.ts +9 -0
- package/dist/commands/agent/discover.js +67 -0
- package/dist/commands/agent/index.js +47 -12
- package/dist/commands/agent/list.d.ts +4 -1
- package/dist/commands/agent/list.js +78 -16
- package/dist/commands/agent/login.js +35 -31
- package/dist/commands/agent/restart.js +2 -0
- package/dist/commands/agent/shell.js +78 -19
- package/dist/commands/agent/staff/add.js +1 -12
- package/dist/commands/agent/staff/remove.js +9 -7
- package/dist/commands/agent/status.js +17 -4
- package/dist/commands/agent/temp/cleanup.js +7 -3
- package/dist/commands/agent/themes/index.js +4 -5
- package/dist/commands/agent/themes/list.js +5 -5
- package/dist/commands/agent/visit.js +17 -4
- package/dist/commands/branch/create.d.ts +4 -0
- package/dist/commands/branch/create.js +16 -8
- package/dist/commands/branch/index.js +1 -1
- package/dist/commands/branch/where.js +1 -0
- package/dist/commands/claude.d.ts +38 -0
- package/dist/commands/claude.js +899 -0
- package/dist/commands/commit.js +1 -1
- package/dist/commands/config/index.d.ts +12 -0
- package/dist/commands/config/index.js +271 -0
- package/dist/commands/docker/clean.js +2 -2
- package/dist/commands/docker/index.js +2 -2
- package/dist/commands/docker/list.js +3 -8
- package/dist/commands/docker/logs.js +2 -2
- package/dist/commands/docker/prune.js +1 -1
- package/dist/commands/docker/restart.js +2 -2
- package/dist/commands/docker/shell.js +2 -2
- package/dist/commands/docker/start.js +2 -2
- package/dist/commands/docker/status.js +1 -1
- package/dist/commands/docker/stop.js +2 -2
- package/dist/commands/docker/sync.js +2 -2
- package/dist/commands/epic/index.js +1 -1
- package/dist/commands/epic/link/index.js +25 -14
- package/dist/commands/epic/link/remove.js +2 -0
- package/dist/commands/epic/list.js +5 -5
- package/dist/commands/epic/progress.js +10 -4
- package/dist/commands/epic/spec.js +2 -0
- package/dist/commands/epic/ticket.js +3 -0
- package/dist/commands/execution/stop.js +1 -0
- package/dist/commands/init.js +4 -4
- package/dist/commands/project/index.js +1 -1
- package/dist/commands/project/spec.js +7 -0
- package/dist/commands/repo/add.js +1 -0
- package/dist/commands/repo/remove.js +1 -0
- package/dist/commands/roadmap/add-project.d.ts +18 -0
- package/dist/commands/roadmap/add-project.js +135 -0
- package/dist/commands/roadmap/create.d.ts +22 -0
- package/dist/commands/roadmap/create.js +156 -0
- package/dist/commands/roadmap/delete.d.ts +17 -0
- package/dist/commands/roadmap/delete.js +104 -0
- package/dist/commands/roadmap/generate.d.ts +22 -0
- package/dist/commands/roadmap/generate.js +201 -0
- package/dist/commands/roadmap/index.d.ts +13 -0
- package/dist/commands/roadmap/index.js +61 -0
- package/dist/commands/roadmap/list.d.ts +12 -0
- package/dist/commands/roadmap/list.js +42 -0
- package/dist/commands/roadmap/remove-project.d.ts +18 -0
- package/dist/commands/roadmap/remove-project.js +147 -0
- package/dist/commands/roadmap/reorder.d.ts +17 -0
- package/dist/commands/roadmap/reorder.js +157 -0
- package/dist/commands/roadmap/update.d.ts +19 -0
- package/dist/commands/roadmap/update.js +136 -0
- package/dist/commands/roadmap/view.d.ts +16 -0
- package/dist/commands/roadmap/view.js +103 -0
- package/dist/commands/spec/index.js +1 -1
- package/dist/commands/spec/link/index.js +24 -13
- package/dist/commands/spec/link/remove.js +2 -0
- package/dist/commands/status/index.js +1 -1
- package/dist/commands/status/list.js +0 -8
- package/dist/commands/template/delete.js +2 -0
- package/dist/commands/terminal/title.d.ts +12 -0
- package/dist/commands/terminal/title.js +48 -0
- package/dist/commands/ticket/complete.js +2 -0
- package/dist/commands/ticket/create.js +4 -2
- package/dist/commands/ticket/delete.js +2 -0
- package/dist/commands/ticket/edit.js +8 -2
- package/dist/commands/ticket/link/index.js +17 -3
- package/dist/commands/ticket/link/remove.js +2 -0
- package/dist/commands/ticket/list.js +1 -2
- package/dist/commands/ticket/move.js +2 -0
- package/dist/commands/ticket/project.js +3 -1
- package/dist/commands/ticket/reassign.js +2 -0
- package/dist/commands/ticket/spec.js +4 -2
- package/dist/commands/ticket/template/apply.js +4 -3
- package/dist/commands/ticket/template/create.js +2 -0
- package/dist/commands/ticket/template/index.js +1 -1
- package/dist/commands/ticket/update.js +2 -0
- package/dist/commands/work/index.js +1 -1
- package/dist/commands/work/revise.js +7 -1
- package/dist/commands/work/spawn.d.ts +2 -1
- package/dist/commands/work/spawn.js +131 -36
- package/dist/commands/work/start.d.ts +2 -1
- package/dist/commands/work/start.js +349 -69
- package/dist/commands/work/watch.js +10 -2
- package/dist/commands/workflow/create.js +3 -3
- package/dist/commands/workflow/switch.js +2 -1
- package/dist/commands/workspace/remove.js +0 -8
- package/dist/commands/workspace/use.js +1 -9
- package/dist/lib/agents/commands.js +18 -13
- package/dist/lib/database/index.d.ts +19 -12
- package/dist/lib/database/index.js +158 -42
- package/dist/lib/docker/resolve.js +1 -1
- package/dist/lib/execution/config.d.ts +6 -0
- package/dist/lib/execution/config.js +15 -2
- package/dist/lib/execution/devcontainer.d.ts +2 -0
- package/dist/lib/execution/devcontainer.js +41 -9
- package/dist/lib/execution/runners.d.ts +85 -3
- package/dist/lib/execution/runners.js +925 -228
- package/dist/lib/execution/spawner.d.ts +2 -2
- package/dist/lib/execution/spawner.js +4 -3
- package/dist/lib/execution/storage.d.ts +2 -1
- package/dist/lib/execution/storage.js +9 -13
- package/dist/lib/execution/types.d.ts +10 -1
- package/dist/lib/execution/types.js +3 -1
- package/dist/lib/init/index.js +1 -0
- package/dist/lib/machine-config.js +1 -1
- package/dist/lib/pmo/base-command.js +5 -9
- package/dist/lib/pmo/index.js +2 -0
- package/dist/lib/pmo/schema.d.ts +6 -0
- package/dist/lib/pmo/schema.js +36 -0
- package/dist/lib/pmo/storage/base.js +3 -3
- package/dist/lib/pmo/storage/index.d.ts +16 -1
- package/dist/lib/pmo/storage/index.js +45 -0
- package/dist/lib/pmo/storage/roadmaps.d.ts +62 -0
- package/dist/lib/pmo/storage/roadmaps.js +301 -0
- package/dist/lib/pmo/storage/specs.js +2 -0
- package/dist/lib/pmo/storage/types.d.ts +14 -0
- package/dist/lib/pmo/sync-manager.d.ts +1 -1
- package/dist/lib/pmo/sync-manager.js +1 -1
- package/dist/lib/pmo/types.d.ts +41 -0
- package/dist/lib/pmo/utils.d.ts +2 -0
- package/dist/lib/pmo/utils.js +22 -1
- package/dist/lib/repos/index.js +7 -1
- package/dist/lib/terminal.d.ts +31 -0
- package/dist/lib/terminal.js +48 -0
- package/dist/lib/themes.d.ts +21 -3
- package/dist/lib/themes.js +80 -23
- package/dist/lib/workspace-config.d.ts +80 -0
- package/dist/lib/workspace-config.js +100 -0
- package/oclif.manifest.json +4065 -3225
- package/package.json +10 -6
- package/LICENSE +0 -21
|
@@ -7,7 +7,7 @@ import { styles } from '../../lib/styles.js';
|
|
|
7
7
|
import { getWorkspaceInfo } from '../../lib/agents/commands.js';
|
|
8
8
|
import { ExecutionStorage } from '../../lib/execution/storage.js';
|
|
9
9
|
import { hasDevcontainerConfig } from '../../lib/execution/devcontainer.js';
|
|
10
|
-
import { spawnForColumn, getAvailableAgents, isDockerRunning, } from '../../lib/execution/spawner.js';
|
|
10
|
+
import { spawnForColumn, getAvailableAgents, isDockerRunning, isDevcontainerCliInstalled, } from '../../lib/execution/spawner.js';
|
|
11
11
|
import { promptExecutionSettings } from '../../lib/execution/config.js';
|
|
12
12
|
import { shouldOutputJson, outputPromptAsJson, outputErrorAsJson, createMetadata, buildPromptConfig, } from '../../lib/prompt-json.js';
|
|
13
13
|
export default class WorkWatch extends PMOCommand {
|
|
@@ -169,11 +169,17 @@ export default class WorkWatch extends PMOCommand {
|
|
|
169
169
|
this.warn('Docker is not running. Agents will run on host instead of devcontainer.\n' +
|
|
170
170
|
'Start Docker Desktop for sandboxed execution.');
|
|
171
171
|
}
|
|
172
|
+
// Devcontainer CLI check
|
|
173
|
+
const devcontainerCliInstalled = isDevcontainerCliInstalled();
|
|
174
|
+
if (hasDevcontainer && dockerRunning && !devcontainerCliInstalled) {
|
|
175
|
+
this.warn('devcontainer CLI is not installed. Agents will run on host instead of devcontainer.\n' +
|
|
176
|
+
'Install with: npm install -g @devcontainers/cli');
|
|
177
|
+
}
|
|
172
178
|
// Prompt for environment and display mode if not provided
|
|
173
179
|
this.environment = 'host';
|
|
174
180
|
this.displayMode = 'terminal';
|
|
175
181
|
if (!flags.mode) {
|
|
176
|
-
if (hasDevcontainer && dockerRunning) {
|
|
182
|
+
if (hasDevcontainer && dockerRunning && devcontainerCliInstalled) {
|
|
177
183
|
// Prompt for environment choice
|
|
178
184
|
const { selectedEnvironment } = await inquirer.prompt([
|
|
179
185
|
{
|
|
@@ -275,8 +281,10 @@ export default class WorkWatch extends PMOCommand {
|
|
|
275
281
|
this.log(styles.muted('Press Ctrl+C to stop watching'));
|
|
276
282
|
this.log('');
|
|
277
283
|
while (this.isRunning) {
|
|
284
|
+
// eslint-disable-next-line no-await-in-loop -- Continuous polling loop
|
|
278
285
|
await this.pollForNewTickets(flags, executionStorage, workspaceInfo, db);
|
|
279
286
|
// Wait for interval
|
|
287
|
+
// eslint-disable-next-line no-await-in-loop -- Poll interval delay
|
|
280
288
|
await new Promise(resolve => setTimeout(resolve, flags.interval * 1000));
|
|
281
289
|
}
|
|
282
290
|
db.close();
|
|
@@ -72,10 +72,9 @@ export default class WorkflowCreate extends PMOCommand {
|
|
|
72
72
|
name: name,
|
|
73
73
|
description: flags.description,
|
|
74
74
|
});
|
|
75
|
-
// If statuses were provided, add them
|
|
75
|
+
// If statuses were provided, add them sequentially for consistent ordering
|
|
76
76
|
if (flags.statuses) {
|
|
77
|
-
const statusNames = flags.statuses.split(',').map(s => s.trim()).filter(
|
|
78
|
-
const defaultCategories = ['backlog', 'unstarted', 'started', 'started', 'completed'];
|
|
77
|
+
const statusNames = flags.statuses.split(',').map(s => s.trim()).filter(Boolean);
|
|
79
78
|
for (let i = 0; i < statusNames.length; i++) {
|
|
80
79
|
const statusName = statusNames[i];
|
|
81
80
|
// Assign categories based on position: first = backlog, second = unstarted, middle = started, last = completed
|
|
@@ -92,6 +91,7 @@ export default class WorkflowCreate extends PMOCommand {
|
|
|
92
91
|
else {
|
|
93
92
|
category = 'started';
|
|
94
93
|
}
|
|
94
|
+
// eslint-disable-next-line no-await-in-loop
|
|
95
95
|
await this.storage.createStatus(workflow.id, {
|
|
96
96
|
name: statusName,
|
|
97
97
|
category: category,
|
|
@@ -93,7 +93,7 @@ export default class WorkflowSwitch extends PMOCommand {
|
|
|
93
93
|
const defaultStatus = newStatuses.find(s => s.isDefault) || newStatuses[0];
|
|
94
94
|
// Update project workflow_id
|
|
95
95
|
await this.storage.updateProject(projectId, { workflowId: workflowId });
|
|
96
|
-
// Migrate tickets to new statuses
|
|
96
|
+
// Migrate tickets to new statuses - sequential for data integrity
|
|
97
97
|
let migratedCount = 0;
|
|
98
98
|
for (const ticket of tickets) {
|
|
99
99
|
// Get old status category
|
|
@@ -102,6 +102,7 @@ export default class WorkflowSwitch extends PMOCommand {
|
|
|
102
102
|
const newStatus = categoryToStatus[oldCategory] ||
|
|
103
103
|
(defaultStatus ? { id: defaultStatus.id, name: defaultStatus.name } : null);
|
|
104
104
|
if (newStatus) {
|
|
105
|
+
// eslint-disable-next-line no-await-in-loop
|
|
105
106
|
await this.storage.moveTicket(projectId, ticket.id, newStatus.name);
|
|
106
107
|
migratedCount++;
|
|
107
108
|
}
|
|
@@ -25,14 +25,6 @@ export default class WorkspaceRemove extends Command {
|
|
|
25
25
|
const { args, flags } = await this.parse(WorkspaceRemove);
|
|
26
26
|
// Check if JSON output mode is active
|
|
27
27
|
const jsonMode = shouldOutputJson(flags);
|
|
28
|
-
// Helper to handle errors in JSON mode
|
|
29
|
-
const handleError = (code, message) => {
|
|
30
|
-
if (jsonMode) {
|
|
31
|
-
outputErrorAsJson(code, message, createMetadata('workspace remove', flags));
|
|
32
|
-
this.exit(1);
|
|
33
|
-
}
|
|
34
|
-
this.error(message);
|
|
35
|
-
};
|
|
36
28
|
const input = args.nameOrPath;
|
|
37
29
|
// Resolve workspace path
|
|
38
30
|
const workspacePath = await this.resolveWorkspacePath(input, jsonMode, flags);
|
|
@@ -4,7 +4,7 @@ import inquirer from 'inquirer';
|
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
5
|
import { isValidHQ } from '../../lib/workspace.js';
|
|
6
6
|
import { findWorkspacesByName, findWorkspaceByPath, setActiveWorkspace, normalizePath, getRegisteredWorkspaces, } from '../../lib/machine-config.js';
|
|
7
|
-
import { shouldOutputJson, outputPromptAsJson,
|
|
7
|
+
import { shouldOutputJson, outputPromptAsJson, createMetadata, buildPromptConfig, } from '../../lib/prompt-json.js';
|
|
8
8
|
export default class WorkspaceUse extends Command {
|
|
9
9
|
static description = 'Set the active workspace';
|
|
10
10
|
static examples = [
|
|
@@ -27,14 +27,6 @@ export default class WorkspaceUse extends Command {
|
|
|
27
27
|
const { args, flags } = await this.parse(WorkspaceUse);
|
|
28
28
|
// Check if JSON output mode is active
|
|
29
29
|
const jsonMode = shouldOutputJson(flags);
|
|
30
|
-
// Helper to handle errors in JSON mode
|
|
31
|
-
const handleError = (code, message) => {
|
|
32
|
-
if (jsonMode) {
|
|
33
|
-
outputErrorAsJson(code, message, createMetadata('workspace use', flags));
|
|
34
|
-
this.exit(1);
|
|
35
|
-
}
|
|
36
|
-
this.error(message);
|
|
37
|
-
};
|
|
38
30
|
const input = args.nameOrPath;
|
|
39
31
|
// First, try to find by path
|
|
40
32
|
const normalizedPath = normalizePath(input);
|
|
@@ -5,8 +5,8 @@ import * as fs from 'node:fs';
|
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import { execSync } from 'node:child_process';
|
|
7
7
|
import inquirer from 'inquirer';
|
|
8
|
-
import { getWorkspaceConfig, getWorkspaceAgents, getWorkspaceRepositories, getAgentWorktrees, addAgentsToDatabase, removeAgentsFromDatabase, addEphemeralAgentToDatabase, getEphemeralAgentNames, getActiveTheme, markAgentCleaned,
|
|
9
|
-
import { isValidAgentName, getSuggestedAgentNames, generateEphemeralAgentName, getThemePersistentDir, getThemeEphemeralDir, } from '../themes.js';
|
|
8
|
+
import { getWorkspaceConfig, getWorkspaceAgents, getWorkspaceRepositories, getAgentWorktrees, addAgentsToDatabase, removeAgentsFromDatabase, addEphemeralAgentToDatabase, getEphemeralAgentNames, getActiveTheme, markAgentCleaned, discoverAgentsOnDisk } from '../database/index.js';
|
|
9
|
+
import { isValidAgentName, getSuggestedAgentNames, generateEphemeralAgentName, getThemePersistentDir, getThemeEphemeralDir, extractBaseName, getAgentBaseName, } from '../themes.js';
|
|
10
10
|
import { createDevcontainerConfig } from '../execution/devcontainer.js';
|
|
11
11
|
import { getPMOContext } from '../pmo/index.js';
|
|
12
12
|
/**
|
|
@@ -29,8 +29,8 @@ export function getWorkspaceInfo() {
|
|
|
29
29
|
try {
|
|
30
30
|
const config = getWorkspaceConfig(hqPath);
|
|
31
31
|
if (config) {
|
|
32
|
-
//
|
|
33
|
-
|
|
32
|
+
// Discover agents on disk and sync with database
|
|
33
|
+
discoverAgentsOnDisk(hqPath);
|
|
34
34
|
const agents = getWorkspaceAgents(hqPath);
|
|
35
35
|
const repositories = getWorkspaceRepositories(hqPath);
|
|
36
36
|
const activeTheme = getActiveTheme(hqPath);
|
|
@@ -66,8 +66,8 @@ export function getWorkspaceInfo() {
|
|
|
66
66
|
try {
|
|
67
67
|
const config = getWorkspaceConfig(currentDir);
|
|
68
68
|
if (config) {
|
|
69
|
-
//
|
|
70
|
-
|
|
69
|
+
// Discover agents on disk and sync with database
|
|
70
|
+
discoverAgentsOnDisk(currentDir);
|
|
71
71
|
const agents = getWorkspaceAgents(currentDir);
|
|
72
72
|
const repositories = getWorkspaceRepositories(currentDir);
|
|
73
73
|
const activeTheme = getActiveTheme(currentDir);
|
|
@@ -366,10 +366,12 @@ export async function removeAgentsFromWorkspace(workspaceInfo, agentNames) {
|
|
|
366
366
|
// Clear ticket assignees for removed agents
|
|
367
367
|
try {
|
|
368
368
|
const { storage } = await getPMOContext();
|
|
369
|
+
// eslint-disable-next-line unicorn/no-useless-undefined
|
|
369
370
|
const allTickets = await storage.listTickets(undefined);
|
|
370
371
|
for (const ticket of allTickets) {
|
|
371
372
|
if (ticket.assignee && removed.includes(ticket.assignee)) {
|
|
372
373
|
// Pass null to clear the assignee in the database
|
|
374
|
+
// eslint-disable-next-line no-await-in-loop -- Sequential updates for cleanup
|
|
373
375
|
await storage.updateTicket(ticket.id, { assignee: null });
|
|
374
376
|
}
|
|
375
377
|
}
|
|
@@ -388,8 +390,8 @@ export async function removeAgentsFromWorkspace(workspaceInfo, agentNames) {
|
|
|
388
390
|
export async function createEphemeralAgent(workspaceInfo, options) {
|
|
389
391
|
// Get existing agent names for uniqueness check
|
|
390
392
|
const existingNames = new Set([
|
|
391
|
-
...
|
|
392
|
-
...
|
|
393
|
+
...Array.from(getEphemeralAgentNames(workspaceInfo.path)),
|
|
394
|
+
...workspaceInfo.agents.map(a => a.name.toLowerCase())
|
|
393
395
|
]);
|
|
394
396
|
const log = options?.log;
|
|
395
397
|
// Get theme: use provided themeId, or fall back to workspace's active theme
|
|
@@ -400,6 +402,9 @@ export async function createEphemeralAgent(workspaceInfo, options) {
|
|
|
400
402
|
// Use theme-specific ephemeral directory
|
|
401
403
|
const ephemeralDir = themeId ? getThemeEphemeralDir(themeId) : workspaceInfo.ephemeralAgentsDir;
|
|
402
404
|
const tempAgentsBasePath = path.join(workspaceInfo.path, 'agents', ephemeralDir);
|
|
405
|
+
// Extract base names currently in use by active agents
|
|
406
|
+
// This helps the generator prefer fresh base names
|
|
407
|
+
const inUseBaseNames = new Set(workspaceInfo.agents.map(agent => getAgentBaseName(agent).toLowerCase()));
|
|
403
408
|
// Create a conflict checker for external resources (tmux sessions, directories)
|
|
404
409
|
const checkExternalConflict = (candidateName) => {
|
|
405
410
|
// Check if a tmux session with this name already exists (could be from manual creation)
|
|
@@ -421,12 +426,12 @@ export async function createEphemeralAgent(workspaceInfo, options) {
|
|
|
421
426
|
const nameOptions = {
|
|
422
427
|
themeId,
|
|
423
428
|
checkExternalConflict,
|
|
424
|
-
onConflictSkipped
|
|
429
|
+
onConflictSkipped,
|
|
430
|
+
inUseBaseNames
|
|
425
431
|
};
|
|
426
432
|
const agentName = generateEphemeralAgentName(existingNames, nameOptions);
|
|
427
|
-
// Extract base name from the generated name (e.g., "bezos" from "bold-bezos-
|
|
428
|
-
const
|
|
429
|
-
const baseName = parts.length >= 3 ? parts.slice(1, -1).join('-') : agentName;
|
|
433
|
+
// Extract base name from the generated name (e.g., "bezos" from "bold-bezos" or "bold-bezos-2")
|
|
434
|
+
const baseName = extractBaseName(agentName);
|
|
430
435
|
// Create temp agents directory if it doesn't exist
|
|
431
436
|
if (!fs.existsSync(tempAgentsBasePath)) {
|
|
432
437
|
fs.mkdirSync(tempAgentsBasePath, { recursive: true });
|
|
@@ -452,7 +457,7 @@ export async function createEphemeralAgent(workspaceInfo, options) {
|
|
|
452
457
|
stdio: 'pipe'
|
|
453
458
|
});
|
|
454
459
|
}
|
|
455
|
-
catch
|
|
460
|
+
catch {
|
|
456
461
|
// If worktree creation fails, try to just create the directory
|
|
457
462
|
// The agent can still work without a worktree (e.g., for non-git projects)
|
|
458
463
|
if (!fs.existsSync(worktreePath)) {
|
|
@@ -38,7 +38,6 @@ export interface AgentTheme {
|
|
|
38
38
|
export interface AgentThemeName {
|
|
39
39
|
theme_id: string;
|
|
40
40
|
name: string;
|
|
41
|
-
used: boolean;
|
|
42
41
|
}
|
|
43
42
|
export interface AgentWorktree {
|
|
44
43
|
agent_name: string;
|
|
@@ -125,6 +124,20 @@ export declare function markAgentCleaned(workspacePath: string, agentName: strin
|
|
|
125
124
|
* Returns list of agents that were cleaned up.
|
|
126
125
|
*/
|
|
127
126
|
export declare function syncAgentsWithDisk(workspacePath: string): string[];
|
|
127
|
+
export interface DiscoverResult {
|
|
128
|
+
discovered: {
|
|
129
|
+
name: string;
|
|
130
|
+
type: AgentType;
|
|
131
|
+
path: string;
|
|
132
|
+
}[];
|
|
133
|
+
cleaned: string[];
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Discover agents on disk that aren't in the database and register them.
|
|
137
|
+
* Also cleans up agents in DB whose directories no longer exist.
|
|
138
|
+
* Returns both discovered and cleaned agents.
|
|
139
|
+
*/
|
|
140
|
+
export declare function discoverAgentsOnDisk(workspacePath: string): DiscoverResult;
|
|
128
141
|
/**
|
|
129
142
|
* Get all repositories in workspace
|
|
130
143
|
*/
|
|
@@ -162,21 +175,15 @@ export declare function deleteTheme(workspacePath: string, themeId: string): boo
|
|
|
162
175
|
/**
|
|
163
176
|
* Get names for a theme
|
|
164
177
|
*/
|
|
165
|
-
export declare function getThemeNames(workspacePath: string, themeId: string
|
|
178
|
+
export declare function getThemeNames(workspacePath: string, themeId: string): AgentThemeName[];
|
|
166
179
|
/**
|
|
167
|
-
* Get available
|
|
168
|
-
*
|
|
180
|
+
* Get available names for a theme.
|
|
181
|
+
* A name is available if:
|
|
182
|
+
* 1. No staff agent exists in the database with that name (case-insensitive), OR
|
|
183
|
+
* 2. The agent exists but its worktree directory is missing (manually deleted)
|
|
169
184
|
*/
|
|
170
185
|
export declare function getAvailableThemeNames(workspacePath: string, themeId: string): string[];
|
|
171
186
|
/**
|
|
172
187
|
* Add names to a theme (case-insensitive uniqueness)
|
|
173
188
|
*/
|
|
174
189
|
export declare function addThemeNames(workspacePath: string, themeId: string, names: string[]): void;
|
|
175
|
-
/**
|
|
176
|
-
* Mark a theme name as used
|
|
177
|
-
*/
|
|
178
|
-
export declare function markThemeNameUsed(workspacePath: string, themeId: string, name: string): void;
|
|
179
|
-
/**
|
|
180
|
-
* Mark a theme name as available
|
|
181
|
-
*/
|
|
182
|
-
export declare function markThemeNameAvailable(workspacePath: string, themeId: string, name: string): void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
-
import { getThemePersistentDir } from '../themes.js';
|
|
4
|
+
import { getThemePersistentDir, isEphemeralAgentName } from '../themes.js';
|
|
5
5
|
import { PMO_SCHEMA_SQL } from '../pmo/schema.js';
|
|
6
6
|
const CREATE_TABLES_SQL = `
|
|
7
7
|
-- Core workspace metadata
|
|
@@ -39,7 +39,6 @@ CREATE TABLE IF NOT EXISTS agent_themes (
|
|
|
39
39
|
CREATE TABLE IF NOT EXISTS agent_theme_names (
|
|
40
40
|
theme_id TEXT NOT NULL,
|
|
41
41
|
name TEXT NOT NULL,
|
|
42
|
-
used BOOLEAN DEFAULT FALSE,
|
|
43
42
|
PRIMARY KEY (theme_id, name),
|
|
44
43
|
FOREIGN KEY (theme_id) REFERENCES agent_themes(id) ON DELETE CASCADE
|
|
45
44
|
);
|
|
@@ -102,6 +101,20 @@ function ensureEphemeralAgentTypes(db) {
|
|
|
102
101
|
WHERE type != 'ephemeral'
|
|
103
102
|
AND name GLOB '*-*-[0-9]*'
|
|
104
103
|
`);
|
|
104
|
+
// Also detect numberless ephemeral names (e.g., bold-bezos) using isEphemeralAgentName()
|
|
105
|
+
// This catches agents that match the adjective-name pattern but don't have a number suffix
|
|
106
|
+
const potentialEphemeral = db.prepare(`
|
|
107
|
+
SELECT name FROM agents
|
|
108
|
+
WHERE type != 'ephemeral'
|
|
109
|
+
AND name LIKE '%-%'
|
|
110
|
+
AND name NOT GLOB '*-*-[0-9]*'
|
|
111
|
+
`).all();
|
|
112
|
+
const updateStmt = db.prepare("UPDATE agents SET type = 'ephemeral' WHERE name = ?");
|
|
113
|
+
for (const agent of potentialEphemeral) {
|
|
114
|
+
if (isEphemeralAgentName(agent.name)) {
|
|
115
|
+
updateStmt.run(agent.name);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
105
118
|
}
|
|
106
119
|
/**
|
|
107
120
|
* Get the database path for a workspace
|
|
@@ -125,6 +138,7 @@ export function openWorkspaceDatabase(workspacePath) {
|
|
|
125
138
|
}
|
|
126
139
|
const db = new Database(dbPath);
|
|
127
140
|
db.pragma('foreign_keys = ON');
|
|
141
|
+
db.pragma('busy_timeout = 5000'); // Wait up to 5 seconds if database is locked
|
|
128
142
|
// Ensure ephemeral agents are correctly typed
|
|
129
143
|
ensureEphemeralAgentTypes(db);
|
|
130
144
|
// Ensure theme tables exist
|
|
@@ -140,13 +154,35 @@ export function openWorkspaceDatabase(workspacePath) {
|
|
|
140
154
|
CREATE TABLE IF NOT EXISTS agent_theme_names (
|
|
141
155
|
theme_id TEXT NOT NULL,
|
|
142
156
|
name TEXT NOT NULL,
|
|
143
|
-
used BOOLEAN DEFAULT FALSE,
|
|
144
157
|
PRIMARY KEY (theme_id, name),
|
|
145
158
|
FOREIGN KEY (theme_id) REFERENCES agent_themes(id) ON DELETE CASCADE
|
|
146
159
|
);
|
|
147
160
|
CREATE INDEX IF NOT EXISTS idx_theme_names_theme ON agent_theme_names(theme_id);
|
|
148
161
|
CREATE INDEX IF NOT EXISTS idx_agents_theme ON agents(theme_id);
|
|
149
162
|
`);
|
|
163
|
+
// Migration: drop 'used' column if it exists (no longer needed)
|
|
164
|
+
try {
|
|
165
|
+
const tableInfo = db.prepare("PRAGMA table_info(agent_theme_names)").all();
|
|
166
|
+
if (tableInfo.some(col => col.name === 'used')) {
|
|
167
|
+
// SQLite doesn't support DROP COLUMN directly, so recreate the table
|
|
168
|
+
db.exec(`
|
|
169
|
+
CREATE TABLE IF NOT EXISTS agent_theme_names_new (
|
|
170
|
+
theme_id TEXT NOT NULL,
|
|
171
|
+
name TEXT NOT NULL,
|
|
172
|
+
PRIMARY KEY (theme_id, name),
|
|
173
|
+
FOREIGN KEY (theme_id) REFERENCES agent_themes(id) ON DELETE CASCADE
|
|
174
|
+
);
|
|
175
|
+
INSERT OR IGNORE INTO agent_theme_names_new (theme_id, name)
|
|
176
|
+
SELECT theme_id, name FROM agent_theme_names;
|
|
177
|
+
DROP TABLE agent_theme_names;
|
|
178
|
+
ALTER TABLE agent_theme_names_new RENAME TO agent_theme_names;
|
|
179
|
+
CREATE INDEX IF NOT EXISTS idx_theme_names_theme ON agent_theme_names(theme_id);
|
|
180
|
+
`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Ignore migration errors - table might not exist yet
|
|
185
|
+
}
|
|
150
186
|
return db;
|
|
151
187
|
}
|
|
152
188
|
/**
|
|
@@ -454,6 +490,93 @@ export function syncAgentsWithDisk(workspacePath) {
|
|
|
454
490
|
}
|
|
455
491
|
return cleanedAgents;
|
|
456
492
|
}
|
|
493
|
+
/**
|
|
494
|
+
* Discover agents on disk that aren't in the database and register them.
|
|
495
|
+
* Also cleans up agents in DB whose directories no longer exist.
|
|
496
|
+
* Returns both discovered and cleaned agents.
|
|
497
|
+
*/
|
|
498
|
+
export function discoverAgentsOnDisk(workspacePath) {
|
|
499
|
+
const result = { discovered: [], cleaned: [] };
|
|
500
|
+
// First, clean up missing agents
|
|
501
|
+
result.cleaned = syncAgentsWithDisk(workspacePath);
|
|
502
|
+
// Get existing ACTIVE agents from DB (case-insensitive lookup)
|
|
503
|
+
const activeAgents = getWorkspaceAgents(workspacePath, false); // Only active agents
|
|
504
|
+
const activeNames = new Set(activeAgents.map(a => a.name.toLowerCase()));
|
|
505
|
+
// Get ALL agents including cleaned (for reactivation)
|
|
506
|
+
const allAgents = getWorkspaceAgents(workspacePath, true);
|
|
507
|
+
const cleanedAgents = new Map(allAgents.filter(a => a.status === 'cleaned').map(a => [a.name.toLowerCase(), a]));
|
|
508
|
+
const db = openWorkspaceDatabase(workspacePath);
|
|
509
|
+
try {
|
|
510
|
+
// Scan staff directory
|
|
511
|
+
const staffDir = path.join(workspacePath, 'agents', 'staff');
|
|
512
|
+
if (fs.existsSync(staffDir)) {
|
|
513
|
+
const staffEntries = fs.readdirSync(staffDir, { withFileTypes: true });
|
|
514
|
+
for (const entry of staffEntries) {
|
|
515
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
516
|
+
const nameLower = entry.name.toLowerCase();
|
|
517
|
+
if (!activeNames.has(nameLower)) {
|
|
518
|
+
const worktreePath = `agents/staff/${entry.name}`;
|
|
519
|
+
const now = new Date().toISOString();
|
|
520
|
+
// Check if this is a cleaned agent that should be reactivated
|
|
521
|
+
const cleanedAgent = cleanedAgents.get(nameLower);
|
|
522
|
+
if (cleanedAgent) {
|
|
523
|
+
// Reactivate the cleaned agent
|
|
524
|
+
db.prepare(`
|
|
525
|
+
UPDATE agents SET status = 'active', cleaned_at = NULL, worktree_path = ?
|
|
526
|
+
WHERE LOWER(name) = LOWER(?)
|
|
527
|
+
`).run(worktreePath, entry.name);
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
// Register new agent
|
|
531
|
+
db.prepare(`
|
|
532
|
+
INSERT INTO agents (name, type, status, worktree_path, created_at)
|
|
533
|
+
VALUES (?, 'persistent', 'active', ?, ?)
|
|
534
|
+
`).run(entry.name, worktreePath, now);
|
|
535
|
+
}
|
|
536
|
+
result.discovered.push({ name: entry.name, type: 'persistent', path: worktreePath });
|
|
537
|
+
activeNames.add(nameLower);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// Scan temp directory
|
|
543
|
+
const tempDir = path.join(workspacePath, 'agents', 'temp');
|
|
544
|
+
if (fs.existsSync(tempDir)) {
|
|
545
|
+
const tempEntries = fs.readdirSync(tempDir, { withFileTypes: true });
|
|
546
|
+
for (const entry of tempEntries) {
|
|
547
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
548
|
+
const nameLower = entry.name.toLowerCase();
|
|
549
|
+
if (!activeNames.has(nameLower)) {
|
|
550
|
+
const worktreePath = `agents/temp/${entry.name}`;
|
|
551
|
+
const now = new Date().toISOString();
|
|
552
|
+
// Check if this is a cleaned agent that should be reactivated
|
|
553
|
+
const cleanedAgent = cleanedAgents.get(nameLower);
|
|
554
|
+
if (cleanedAgent) {
|
|
555
|
+
// Reactivate the cleaned agent
|
|
556
|
+
db.prepare(`
|
|
557
|
+
UPDATE agents SET status = 'active', cleaned_at = NULL, worktree_path = ?
|
|
558
|
+
WHERE LOWER(name) = LOWER(?)
|
|
559
|
+
`).run(worktreePath, entry.name);
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
// Register new agent
|
|
563
|
+
db.prepare(`
|
|
564
|
+
INSERT INTO agents (name, type, status, worktree_path, created_at)
|
|
565
|
+
VALUES (?, 'ephemeral', 'active', ?, ?)
|
|
566
|
+
`).run(entry.name, worktreePath, now);
|
|
567
|
+
}
|
|
568
|
+
result.discovered.push({ name: entry.name, type: 'ephemeral', path: worktreePath });
|
|
569
|
+
activeNames.add(nameLower);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
finally {
|
|
576
|
+
db.close();
|
|
577
|
+
}
|
|
578
|
+
return result;
|
|
579
|
+
}
|
|
457
580
|
/**
|
|
458
581
|
* Get all repositories in workspace
|
|
459
582
|
*/
|
|
@@ -477,17 +600,10 @@ export function getAgentWorktrees(workspacePath, agentName) {
|
|
|
477
600
|
*/
|
|
478
601
|
export function removeAgentsFromDatabase(workspacePath, agentNames) {
|
|
479
602
|
const db = openWorkspaceDatabase(workspacePath);
|
|
480
|
-
const getAgent = db.prepare('SELECT theme_id, name FROM agents WHERE name = ?');
|
|
481
603
|
const deleteAgent = db.prepare('DELETE FROM agents WHERE name = ?');
|
|
482
|
-
const clearUsedFlag = db.prepare('UPDATE agent_theme_names SET used = 0 WHERE theme_id = ? AND name = ?');
|
|
483
604
|
// Note: agent_worktrees will be deleted automatically due to CASCADE
|
|
484
605
|
const transaction = db.transaction(() => {
|
|
485
606
|
for (const agentName of agentNames) {
|
|
486
|
-
// Clear used flag if agent came from a theme
|
|
487
|
-
const agent = getAgent.get(agentName);
|
|
488
|
-
if (agent?.theme_id) {
|
|
489
|
-
clearUsedFlag.run(agent.theme_id, agentName);
|
|
490
|
-
}
|
|
491
607
|
deleteAgent.run(agentName);
|
|
492
608
|
}
|
|
493
609
|
});
|
|
@@ -551,31 +667,47 @@ export function deleteTheme(workspacePath, themeId) {
|
|
|
551
667
|
/**
|
|
552
668
|
* Get names for a theme
|
|
553
669
|
*/
|
|
554
|
-
export function getThemeNames(workspacePath, themeId
|
|
670
|
+
export function getThemeNames(workspacePath, themeId) {
|
|
555
671
|
const db = openWorkspaceDatabase(workspacePath);
|
|
556
|
-
const
|
|
557
|
-
? 'SELECT * FROM agent_theme_names WHERE theme_id = ? ORDER BY name'
|
|
558
|
-
: 'SELECT * FROM agent_theme_names WHERE theme_id = ? AND used = 0 ORDER BY name';
|
|
559
|
-
const names = db.prepare(query).all(themeId);
|
|
672
|
+
const names = db.prepare('SELECT * FROM agent_theme_names WHERE theme_id = ? ORDER BY name').all(themeId);
|
|
560
673
|
db.close();
|
|
561
674
|
return names;
|
|
562
675
|
}
|
|
563
676
|
/**
|
|
564
|
-
* Get available
|
|
565
|
-
*
|
|
677
|
+
* Get available names for a theme.
|
|
678
|
+
* A name is available if:
|
|
679
|
+
* 1. No staff agent exists in the database with that name (case-insensitive), OR
|
|
680
|
+
* 2. The agent exists but its worktree directory is missing (manually deleted)
|
|
566
681
|
*/
|
|
567
682
|
export function getAvailableThemeNames(workspacePath, themeId) {
|
|
568
683
|
const db = openWorkspaceDatabase(workspacePath);
|
|
569
|
-
// Get
|
|
570
|
-
const names = db.prepare('SELECT name FROM agent_theme_names WHERE theme_id = ?
|
|
571
|
-
// Get existing
|
|
572
|
-
const existingAgents = db.prepare(
|
|
573
|
-
|
|
684
|
+
// Get all theme names
|
|
685
|
+
const names = db.prepare('SELECT name FROM agent_theme_names WHERE theme_id = ? ORDER BY name').all(themeId);
|
|
686
|
+
// Get existing staff agents with their worktree paths (persistent type only)
|
|
687
|
+
const existingAgents = db.prepare(`
|
|
688
|
+
SELECT LOWER(name) as name, worktree_path
|
|
689
|
+
FROM agents
|
|
690
|
+
WHERE type = 'persistent' AND (status = 'active' OR status IS NULL)
|
|
691
|
+
`).all();
|
|
574
692
|
db.close();
|
|
575
|
-
//
|
|
693
|
+
// Build a set of names that are truly in use (agent exists AND worktree exists)
|
|
694
|
+
const inUseNames = new Set();
|
|
695
|
+
for (const agent of existingAgents) {
|
|
696
|
+
if (agent.worktree_path) {
|
|
697
|
+
const fullPath = path.join(workspacePath, agent.worktree_path);
|
|
698
|
+
if (fs.existsSync(fullPath)) {
|
|
699
|
+
inUseNames.add(agent.name);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
// No worktree path means we can't verify - treat as in use to be safe
|
|
704
|
+
inUseNames.add(agent.name);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
// Filter out names that are truly in use
|
|
576
708
|
return names
|
|
577
709
|
.map(n => n.name)
|
|
578
|
-
.filter(name => !
|
|
710
|
+
.filter(name => !inUseNames.has(name.toLowerCase()));
|
|
579
711
|
}
|
|
580
712
|
/**
|
|
581
713
|
* Add names to a theme (case-insensitive uniqueness)
|
|
@@ -585,8 +717,8 @@ export function addThemeNames(workspacePath, themeId, names) {
|
|
|
585
717
|
// Check for existing name (case-insensitive)
|
|
586
718
|
const checkExisting = db.prepare('SELECT name FROM agent_theme_names WHERE theme_id = ? AND LOWER(name) = LOWER(?)');
|
|
587
719
|
const insertName = db.prepare(`
|
|
588
|
-
INSERT INTO agent_theme_names (theme_id, name
|
|
589
|
-
VALUES (?,
|
|
720
|
+
INSERT INTO agent_theme_names (theme_id, name)
|
|
721
|
+
VALUES (?, ?)
|
|
590
722
|
`);
|
|
591
723
|
const transaction = db.transaction(() => {
|
|
592
724
|
for (const name of names) {
|
|
@@ -601,19 +733,3 @@ export function addThemeNames(workspacePath, themeId, names) {
|
|
|
601
733
|
transaction();
|
|
602
734
|
db.close();
|
|
603
735
|
}
|
|
604
|
-
/**
|
|
605
|
-
* Mark a theme name as used
|
|
606
|
-
*/
|
|
607
|
-
export function markThemeNameUsed(workspacePath, themeId, name) {
|
|
608
|
-
const db = openWorkspaceDatabase(workspacePath);
|
|
609
|
-
db.prepare('UPDATE agent_theme_names SET used = 1 WHERE theme_id = ? AND name = ?').run(themeId, name);
|
|
610
|
-
db.close();
|
|
611
|
-
}
|
|
612
|
-
/**
|
|
613
|
-
* Mark a theme name as available
|
|
614
|
-
*/
|
|
615
|
-
export function markThemeNameAvailable(workspacePath, themeId, name) {
|
|
616
|
-
const db = openWorkspaceDatabase(workspacePath);
|
|
617
|
-
db.prepare('UPDATE agent_theme_names SET used = 0 WHERE theme_id = ? AND name = ?').run(themeId, name);
|
|
618
|
-
db.close();
|
|
619
|
-
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Resolves various identifiers to Docker container IDs.
|
|
5
5
|
* Accepts: execution ID (WORK-XXX), agent name, or container ID.
|
|
6
6
|
*/
|
|
7
|
-
import { execSync } from 'child_process';
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
8
|
/**
|
|
9
9
|
* Validate and sanitize a container ID to prevent shell injection.
|
|
10
10
|
* Docker container IDs are 64-character hex strings (or 12-char short form).
|
|
@@ -8,6 +8,7 @@ import Database from 'better-sqlite3';
|
|
|
8
8
|
import { ExecutionConfig, TerminalApp, Shell, DisplayMode, OutputMode, ExecutionEnvironment } from './types.js';
|
|
9
9
|
declare const CONFIG_KEYS: {
|
|
10
10
|
terminalApp: string;
|
|
11
|
+
terminalOpenInBackground: string;
|
|
11
12
|
shell: string;
|
|
12
13
|
defaultMode: string;
|
|
13
14
|
defaultExecutor: string;
|
|
@@ -46,6 +47,11 @@ export declare function saveShell(db: Database.Database, shell: Shell): void;
|
|
|
46
47
|
* When enabled and using iTerm, tmux -CC is used for native tab integration.
|
|
47
48
|
*/
|
|
48
49
|
export declare function saveTmuxControlMode(db: Database.Database, enabled: boolean): void;
|
|
50
|
+
/**
|
|
51
|
+
* Save terminal open in background preference.
|
|
52
|
+
* When enabled, new terminal tabs open without stealing focus from current window.
|
|
53
|
+
*/
|
|
54
|
+
export declare function saveTerminalOpenInBackground(db: Database.Database, enabled: boolean): void;
|
|
49
55
|
/**
|
|
50
56
|
* Check if terminal app preference has been set
|
|
51
57
|
*/
|
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
import inquirer from 'inquirer';
|
|
8
8
|
import { DEFAULT_EXECUTION_CONFIG } from './types.js';
|
|
9
9
|
import { isGHInstalled, isGHAuthenticated } from '../pr/index.js';
|
|
10
|
-
import { execSync } from 'child_process';
|
|
10
|
+
import { execSync } from 'node:child_process';
|
|
11
11
|
const SETTINGS_TABLE = 'workspace_settings';
|
|
12
12
|
// Config keys stored in workspace_settings table
|
|
13
13
|
const CONFIG_KEYS = {
|
|
14
14
|
terminalApp: 'execution.terminal.app',
|
|
15
|
+
terminalOpenInBackground: 'execution.terminal.open_in_background',
|
|
15
16
|
shell: 'execution.shell',
|
|
16
17
|
defaultMode: 'execution.default_mode',
|
|
17
18
|
defaultExecutor: 'execution.default_executor',
|
|
@@ -56,7 +57,12 @@ export function loadExecutionConfig(db) {
|
|
|
56
57
|
// Load terminal app
|
|
57
58
|
const terminalApp = getSetting(db, CONFIG_KEYS.terminalApp);
|
|
58
59
|
if (terminalApp) {
|
|
59
|
-
config.terminal = { app: terminalApp };
|
|
60
|
+
config.terminal = { ...config.terminal, app: terminalApp };
|
|
61
|
+
}
|
|
62
|
+
// Load terminal open in background setting
|
|
63
|
+
const terminalOpenInBackground = getSetting(db, CONFIG_KEYS.terminalOpenInBackground);
|
|
64
|
+
if (terminalOpenInBackground !== null) {
|
|
65
|
+
config.terminal = { ...config.terminal, openInBackground: terminalOpenInBackground === 'true' };
|
|
60
66
|
}
|
|
61
67
|
// Load shell
|
|
62
68
|
const shell = getSetting(db, CONFIG_KEYS.shell);
|
|
@@ -152,6 +158,13 @@ export function saveShell(db, shell) {
|
|
|
152
158
|
export function saveTmuxControlMode(db, enabled) {
|
|
153
159
|
setSetting(db, CONFIG_KEYS.tmuxControlMode, enabled.toString());
|
|
154
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Save terminal open in background preference.
|
|
163
|
+
* When enabled, new terminal tabs open without stealing focus from current window.
|
|
164
|
+
*/
|
|
165
|
+
export function saveTerminalOpenInBackground(db, enabled) {
|
|
166
|
+
setSetting(db, CONFIG_KEYS.terminalOpenInBackground, enabled.toString());
|
|
167
|
+
}
|
|
155
168
|
/**
|
|
156
169
|
* Check if terminal app preference has been set
|
|
157
170
|
*/
|
|
@@ -12,6 +12,8 @@ export interface DevcontainerOptions {
|
|
|
12
12
|
memory?: string;
|
|
13
13
|
cpus?: number;
|
|
14
14
|
timezone?: string;
|
|
15
|
+
/** prlt channel: "npm", "npm:dev", "gh", "gh:dev", "mount", or version like "npm:1.2.3" */
|
|
16
|
+
prltChannel?: string;
|
|
15
17
|
}
|
|
16
18
|
export interface DevcontainerJson {
|
|
17
19
|
name: string;
|