@proletariat/cli 0.3.45 → 0.3.46
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/dist/commands/config/index.js +39 -1
- package/dist/commands/linear/auth.d.ts +14 -0
- package/dist/commands/linear/auth.js +211 -0
- package/dist/commands/linear/import.d.ts +21 -0
- package/dist/commands/linear/import.js +260 -0
- package/dist/commands/linear/status.d.ts +11 -0
- package/dist/commands/linear/status.js +88 -0
- package/dist/commands/linear/sync.d.ts +15 -0
- package/dist/commands/linear/sync.js +233 -0
- package/dist/commands/orchestrator/attach.d.ts +9 -1
- package/dist/commands/orchestrator/attach.js +67 -13
- package/dist/commands/orchestrator/index.js +22 -7
- package/dist/commands/ticket/link/duplicates.d.ts +15 -0
- package/dist/commands/ticket/link/duplicates.js +95 -0
- package/dist/commands/ticket/link/index.js +14 -0
- package/dist/commands/ticket/link/relates.d.ts +15 -0
- package/dist/commands/ticket/link/relates.js +95 -0
- package/dist/commands/work/revise.js +4 -3
- package/dist/commands/work/spawn.d.ts +5 -0
- package/dist/commands/work/spawn.js +195 -14
- package/dist/commands/work/start.js +75 -19
- package/dist/lib/execution/config.d.ts +15 -0
- package/dist/lib/execution/config.js +54 -0
- package/dist/lib/execution/devcontainer.d.ts +6 -3
- package/dist/lib/execution/devcontainer.js +39 -12
- package/dist/lib/execution/runners.d.ts +28 -32
- package/dist/lib/execution/runners.js +345 -275
- package/dist/lib/execution/spawner.js +62 -5
- package/dist/lib/execution/types.d.ts +4 -0
- package/dist/lib/execution/types.js +3 -0
- package/dist/lib/external-issues/adapters.d.ts +26 -0
- package/dist/lib/external-issues/adapters.js +251 -0
- package/dist/lib/external-issues/index.d.ts +10 -0
- package/dist/lib/external-issues/index.js +14 -0
- package/dist/lib/external-issues/mapper.d.ts +21 -0
- package/dist/lib/external-issues/mapper.js +86 -0
- package/dist/lib/external-issues/types.d.ts +144 -0
- package/dist/lib/external-issues/types.js +26 -0
- package/dist/lib/external-issues/validation.d.ts +34 -0
- package/dist/lib/external-issues/validation.js +219 -0
- package/dist/lib/linear/client.d.ts +55 -0
- package/dist/lib/linear/client.js +254 -0
- package/dist/lib/linear/config.d.ts +37 -0
- package/dist/lib/linear/config.js +100 -0
- package/dist/lib/linear/index.d.ts +11 -0
- package/dist/lib/linear/index.js +10 -0
- package/dist/lib/linear/mapper.d.ts +67 -0
- package/dist/lib/linear/mapper.js +219 -0
- package/dist/lib/linear/sync.d.ts +37 -0
- package/dist/lib/linear/sync.js +89 -0
- package/dist/lib/linear/types.d.ts +139 -0
- package/dist/lib/linear/types.js +34 -0
- package/dist/lib/mcp/helpers.d.ts +8 -0
- package/dist/lib/mcp/helpers.js +10 -0
- package/dist/lib/mcp/tools/board.js +63 -11
- package/dist/lib/pmo/schema.d.ts +2 -0
- package/dist/lib/pmo/schema.js +20 -0
- package/dist/lib/pmo/storage/base.js +92 -13
- package/dist/lib/pmo/storage/dependencies.js +15 -0
- package/dist/lib/prompt-json.d.ts +4 -0
- package/oclif.manifest.json +2867 -2380
- package/package.json +2 -1
|
@@ -11,9 +11,9 @@ import { getWorkColumnSetting, findColumnByName } from '../../lib/pmo/utils.js';
|
|
|
11
11
|
import { styles } from '../../lib/styles.js';
|
|
12
12
|
import { getWorkspaceInfo, createEphemeralAgent, getTicketTmuxSession, killTmuxSession, findWorktreeForBranch, resolveAgentDir, } from '../../lib/agents/commands.js';
|
|
13
13
|
import { generateBranchName, DEFAULT_EXECUTION_CONFIG, } from '../../lib/execution/types.js';
|
|
14
|
-
import { runExecution, isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled, dockerCredentialsExist, getDockerCredentialInfo } from '../../lib/execution/runners.js';
|
|
14
|
+
import { runExecution, isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled, dockerCredentialsExist, getDockerCredentialInfo, isClaudeExecutor, getExecutorDisplayName } from '../../lib/execution/runners.js';
|
|
15
15
|
import { ExecutionStorage, ContainerStorage } from '../../lib/execution/storage.js';
|
|
16
|
-
import { loadExecutionConfig, getTerminalApp, promptTerminalPreference, getShell, promptShellPreference, hasTerminalPreference, hasShellPreference, getOrPromptCoderName, getAuthMethod, saveAuthMethod } from '../../lib/execution/config.js';
|
|
16
|
+
import { loadExecutionConfig, getTerminalApp, promptTerminalPreference, getShell, promptShellPreference, hasTerminalPreference, hasShellPreference, getOrPromptCoderName, getAuthMethod, saveAuthMethod, getCreatePrDefault } from '../../lib/execution/config.js';
|
|
17
17
|
import { hasDevcontainerConfig } from '../../lib/execution/devcontainer.js';
|
|
18
18
|
import { detectRepoWorktrees, resolveWorktreePath } from '../../lib/execution/context.js';
|
|
19
19
|
import { isGHInstalled, isGHAuthenticated } from '../../lib/pr/index.js';
|
|
@@ -130,7 +130,7 @@ export default class WorkStart extends PMOCommand {
|
|
|
130
130
|
default: false,
|
|
131
131
|
}),
|
|
132
132
|
'permission-mode': Flags.string({
|
|
133
|
-
description: 'Permission mode for
|
|
133
|
+
description: 'Permission mode for selected executor (danger=skip checks, safe=require approval)',
|
|
134
134
|
options: ['danger', 'safe'],
|
|
135
135
|
}),
|
|
136
136
|
'skip-permissions': Flags.boolean({
|
|
@@ -277,7 +277,14 @@ export default class WorkStart extends PMOCommand {
|
|
|
277
277
|
if (allFlagsProvided && !flags.yes) {
|
|
278
278
|
// All flags provided but no --yes: return confirmation_needed with plan
|
|
279
279
|
const metadata = createMetadata('work start', flags);
|
|
280
|
-
|
|
280
|
+
// Resolve PR mode using same priority as execution: flags > workspace config > default
|
|
281
|
+
const earlyConfigPrDefault = getCreatePrDefault(db);
|
|
282
|
+
const earlyResolvedPr = flags['create-pr'] ? 'create-pr'
|
|
283
|
+
: flags['no-pr'] ? 'no-pr'
|
|
284
|
+
: earlyConfigPrDefault === true ? 'create-pr'
|
|
285
|
+
: earlyConfigPrDefault === false ? 'no-pr'
|
|
286
|
+
: 'no-pr';
|
|
287
|
+
metadata.resolvedPRMode = earlyResolvedPr;
|
|
281
288
|
// Build the confirm command with --yes
|
|
282
289
|
let confirmCmd = `prlt work start ${ticketId}`;
|
|
283
290
|
if (flags.action)
|
|
@@ -1024,7 +1031,8 @@ export default class WorkStart extends PMOCommand {
|
|
|
1024
1031
|
// Track whether user explicitly chose to use API key instead of OAuth
|
|
1025
1032
|
let useApiKey = flags['use-api-key'] || false;
|
|
1026
1033
|
// Auth method resolution for devcontainer environment
|
|
1027
|
-
|
|
1034
|
+
// Only needed for Claude Code executor - other executors handle auth differently
|
|
1035
|
+
if (environment === 'devcontainer' && !useApiKey && isClaudeExecutor(executor)) {
|
|
1028
1036
|
// Check for saved auth method preference
|
|
1029
1037
|
const savedAuthMethod = getAuthMethod(db);
|
|
1030
1038
|
const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
|
|
@@ -1212,10 +1220,11 @@ export default class WorkStart extends PMOCommand {
|
|
|
1212
1220
|
jsonMode,
|
|
1213
1221
|
flags: { 'permission-mode': flags['permission-mode'] },
|
|
1214
1222
|
});
|
|
1223
|
+
const executorName = getExecutorDisplayName(executor);
|
|
1215
1224
|
permissionResolver.addPrompt({
|
|
1216
1225
|
flagName: 'permission-mode',
|
|
1217
1226
|
type: 'list',
|
|
1218
|
-
message: `Permission mode for
|
|
1227
|
+
message: `Permission mode for ${executorName}${containerNote}:`,
|
|
1219
1228
|
default: 'danger',
|
|
1220
1229
|
choices: () => [
|
|
1221
1230
|
{ name: '⚠️ danger - Skip permission checks (faster, container provides isolation)', value: 'danger' },
|
|
@@ -1227,24 +1236,34 @@ export default class WorkStart extends PMOCommand {
|
|
|
1227
1236
|
sandboxed = resolvedPermission['permission-mode'] === 'safe';
|
|
1228
1237
|
}
|
|
1229
1238
|
// Prompt for PR creation when work is complete
|
|
1230
|
-
//
|
|
1239
|
+
// Resolution order: explicit flags > workspace config default > interactive prompt
|
|
1231
1240
|
let createPR = false;
|
|
1241
|
+
let prModeSource = 'default'; // Track where PR mode was resolved from for display
|
|
1232
1242
|
const ghAvailable = isGHInstalled() && isGHAuthenticated();
|
|
1233
|
-
|
|
1243
|
+
const configPrDefault = getCreatePrDefault(db);
|
|
1234
1244
|
if (flags['create-pr']) {
|
|
1235
1245
|
createPR = true;
|
|
1246
|
+
prModeSource = 'flag --create-pr';
|
|
1236
1247
|
}
|
|
1237
1248
|
else if (flags['no-pr']) {
|
|
1238
1249
|
createPR = false;
|
|
1250
|
+
prModeSource = 'flag --no-pr';
|
|
1251
|
+
}
|
|
1252
|
+
else if (context.modifiesCode === false) {
|
|
1253
|
+
// Non-code-modifying actions (groom, review, resolve) default to no PR
|
|
1254
|
+
createPR = false;
|
|
1255
|
+
prModeSource = 'action (non-code-modifying)';
|
|
1256
|
+
}
|
|
1257
|
+
else if (configPrDefault !== null) {
|
|
1258
|
+
// Workspace config default is set - use it deterministically
|
|
1259
|
+
createPR = configPrDefault;
|
|
1260
|
+
prModeSource = 'workspace config (execution.create_pr_default)';
|
|
1239
1261
|
}
|
|
1240
1262
|
else if (ghAvailable) {
|
|
1241
|
-
if (
|
|
1242
|
-
// Non-code-modifying actions (groom, review, resolve) default to no PR
|
|
1243
|
-
createPR = false;
|
|
1244
|
-
}
|
|
1245
|
-
else if (jsonMode && flags.yes) {
|
|
1263
|
+
if (jsonMode && flags.yes) {
|
|
1246
1264
|
// In JSON mode with --yes, default to creating PR for code-modifying actions
|
|
1247
1265
|
createPR = true;
|
|
1266
|
+
prModeSource = 'default (--json --yes)';
|
|
1248
1267
|
}
|
|
1249
1268
|
else {
|
|
1250
1269
|
// Use FlagResolver for PR choice
|
|
@@ -1266,9 +1285,14 @@ export default class WorkStart extends PMOCommand {
|
|
|
1266
1285
|
});
|
|
1267
1286
|
const prResult = await prResolver.resolve();
|
|
1268
1287
|
createPR = prResult.prChoice === 'yes';
|
|
1288
|
+
prModeSource = 'interactive prompt';
|
|
1269
1289
|
}
|
|
1270
1290
|
}
|
|
1271
|
-
|
|
1291
|
+
else {
|
|
1292
|
+
prModeSource = 'default (gh CLI not available)';
|
|
1293
|
+
}
|
|
1294
|
+
// R1: Show clear PR mode in preflight summary
|
|
1295
|
+
// R2: Show strong warning when --no-pr is active
|
|
1272
1296
|
if (!jsonMode) {
|
|
1273
1297
|
this.log('');
|
|
1274
1298
|
this.log(styles.header(`🚀 Starting work: ${ticket.id}: ${ticket.title}`));
|
|
@@ -1286,12 +1310,30 @@ export default class WorkStart extends PMOCommand {
|
|
|
1286
1310
|
else {
|
|
1287
1311
|
this.log(styles.warning(` Permissions: ⚠️ danger (--dangerously-skip-permissions)`));
|
|
1288
1312
|
}
|
|
1289
|
-
this.log(styles.muted(` Output: ${outputMode === 'interactive' ?
|
|
1290
|
-
|
|
1313
|
+
this.log(styles.muted(` Output: ${outputMode === 'interactive' ? `streaming (watch ${getExecutorDisplayName(executor)} work)` : 'print (final result only)'}`));
|
|
1314
|
+
// PR mode with clear source indication
|
|
1315
|
+
if (createPR) {
|
|
1316
|
+
this.log(styles.success(` PR mode: create-pr (${prModeSource})`));
|
|
1317
|
+
}
|
|
1318
|
+
else {
|
|
1319
|
+
this.log(styles.warning(` PR mode: no-pr (${prModeSource})`));
|
|
1320
|
+
}
|
|
1321
|
+
// Strong warning when no-pr is active
|
|
1322
|
+
if (!createPR && context.modifiesCode !== false) {
|
|
1323
|
+
this.log('');
|
|
1324
|
+
this.log(styles.warning(` ⚠️ WARNING: PR creation is DISABLED. Branch will be pushed but NO pull request will be created.`));
|
|
1325
|
+
this.log(styles.warning(` To create a PR later: prlt pr create ${ticketId}`));
|
|
1326
|
+
}
|
|
1291
1327
|
this.log(styles.muted(` Worktree: ${worktreePath}`));
|
|
1292
1328
|
this.log(styles.muted(` Branch: ${branch}`));
|
|
1293
1329
|
this.log('');
|
|
1294
1330
|
}
|
|
1331
|
+
// R2: Include PR mode warning in JSON metadata
|
|
1332
|
+
if (jsonMode && !createPR && context.modifiesCode !== false) {
|
|
1333
|
+
// This will be included in the metadata of the JSON output at the end
|
|
1334
|
+
// We log a warning here for non-JSON consumers that may be watching stderr
|
|
1335
|
+
this.warn(`PR creation is DISABLED (${prModeSource}). Branch will be pushed without a PR. To create later: prlt pr create ${ticketId}`);
|
|
1336
|
+
}
|
|
1295
1337
|
// Add createPR to context
|
|
1296
1338
|
context.createPR = createPR;
|
|
1297
1339
|
// Handle git operations
|
|
@@ -1589,9 +1631,13 @@ export default class WorkStart extends PMOCommand {
|
|
|
1589
1631
|
});
|
|
1590
1632
|
// Output results
|
|
1591
1633
|
if (jsonMode) {
|
|
1592
|
-
// Output JSON execution result with resolved PR mode
|
|
1634
|
+
// Output JSON execution result with resolved PR mode and source
|
|
1593
1635
|
const metadata = createMetadata('work start', flags);
|
|
1594
1636
|
metadata.resolvedPRMode = createPR ? 'create-pr' : 'no-pr';
|
|
1637
|
+
metadata.prModeSource = prModeSource;
|
|
1638
|
+
if (!createPR && context.modifiesCode !== false) {
|
|
1639
|
+
metadata.prWarning = `PR creation is DISABLED (${prModeSource}). Branch will be pushed without a PR. To create later: prlt pr create ${ticketId}`;
|
|
1640
|
+
}
|
|
1595
1641
|
outputExecutionResultAsJson([{
|
|
1596
1642
|
workId: execution.id,
|
|
1597
1643
|
ticketId: ticket.id,
|
|
@@ -1609,6 +1655,12 @@ export default class WorkStart extends PMOCommand {
|
|
|
1609
1655
|
this.log(styles.muted(` prlt work status View work status`));
|
|
1610
1656
|
this.log(styles.muted(` prlt work ready ${ticketId} Mark ready for review`));
|
|
1611
1657
|
this.log(styles.muted(` prlt work stop ${execution.id} Stop work`));
|
|
1658
|
+
// R5: Post-run reminder when branch is pushed without PR
|
|
1659
|
+
if (!createPR && context.modifiesCode !== false) {
|
|
1660
|
+
this.log('');
|
|
1661
|
+
this.log(styles.warning(`Note: No PR will be auto-created. To create one later:`));
|
|
1662
|
+
this.log(styles.warning(` prlt pr create ${ticketId}`));
|
|
1663
|
+
}
|
|
1612
1664
|
}
|
|
1613
1665
|
}
|
|
1614
1666
|
else {
|
|
@@ -1617,6 +1669,7 @@ export default class WorkStart extends PMOCommand {
|
|
|
1617
1669
|
// Output JSON failure result with resolved PR mode
|
|
1618
1670
|
const failMetadata = createMetadata('work start', flags);
|
|
1619
1671
|
failMetadata.resolvedPRMode = createPR ? 'create-pr' : 'no-pr';
|
|
1672
|
+
failMetadata.prModeSource = prModeSource;
|
|
1620
1673
|
outputExecutionResultAsJson([{
|
|
1621
1674
|
workId: execution.id,
|
|
1622
1675
|
ticketId: ticket.id,
|
|
@@ -1694,13 +1747,15 @@ export default class WorkStart extends PMOCommand {
|
|
|
1694
1747
|
return;
|
|
1695
1748
|
}
|
|
1696
1749
|
// Prompt for permissions mode once for all tickets (TKT-513)
|
|
1750
|
+
const batchExecutor = flags.executor || DEFAULT_EXECUTION_CONFIG.defaultExecutor;
|
|
1751
|
+
const batchExecutorName = getExecutorDisplayName(batchExecutor);
|
|
1697
1752
|
let batchPermissionMode = flags['permission-mode'];
|
|
1698
1753
|
if (!batchPermissionMode) {
|
|
1699
1754
|
const { permissionMode } = await this.prompt([
|
|
1700
1755
|
{
|
|
1701
1756
|
type: 'list',
|
|
1702
1757
|
name: 'permissionMode',
|
|
1703
|
-
message:
|
|
1758
|
+
message: `Permission mode for ${batchExecutorName}:`,
|
|
1704
1759
|
choices: [
|
|
1705
1760
|
{ name: '⚠️ danger - Skip permission checks (faster, container provides isolation)', value: 'danger', command: 'prlt work start --all --permission-mode danger --json' },
|
|
1706
1761
|
{ name: '🔒 safe - Requires approval for dangerous operations', value: 'safe', command: 'prlt work start --all --permission-mode safe --json' },
|
|
@@ -1717,7 +1772,8 @@ export default class WorkStart extends PMOCommand {
|
|
|
1717
1772
|
});
|
|
1718
1773
|
// Track whether user explicitly chose to use API key instead of OAuth
|
|
1719
1774
|
let batchUseApiKey = false;
|
|
1720
|
-
|
|
1775
|
+
// Credential check only applies to Claude Code executor
|
|
1776
|
+
if (anyUseDevcontainer && isClaudeExecutor(batchExecutor)) {
|
|
1721
1777
|
const hasCredentials = dockerCredentialsExist();
|
|
1722
1778
|
if (!hasCredentials) {
|
|
1723
1779
|
const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
|
|
@@ -23,12 +23,14 @@ declare const CONFIG_KEYS: {
|
|
|
23
23
|
dockerNetwork: string;
|
|
24
24
|
dockerMemory: string;
|
|
25
25
|
dockerCpus: string;
|
|
26
|
+
firewallAllowlistDomains: string;
|
|
26
27
|
vmDefaultHost: string;
|
|
27
28
|
vmUser: string;
|
|
28
29
|
vmKeyPath: string;
|
|
29
30
|
vmSyncMethod: string;
|
|
30
31
|
coderName: string;
|
|
31
32
|
authMethod: string;
|
|
33
|
+
createPrDefault: string;
|
|
32
34
|
};
|
|
33
35
|
/**
|
|
34
36
|
* Load execution config from database, merging with defaults
|
|
@@ -56,6 +58,10 @@ export declare function saveTmuxControlMode(db: Database.Database, enabled: bool
|
|
|
56
58
|
* When enabled, new terminal tabs open without stealing focus from current window.
|
|
57
59
|
*/
|
|
58
60
|
export declare function saveTerminalOpenInBackground(db: Database.Database, enabled: boolean): void;
|
|
61
|
+
/**
|
|
62
|
+
* Save extra firewall allowlist domains.
|
|
63
|
+
*/
|
|
64
|
+
export declare function saveFirewallAllowlistDomains(db: Database.Database, domains: string[]): void;
|
|
59
65
|
/**
|
|
60
66
|
* Save auth method preference (oauth or apikey)
|
|
61
67
|
*/
|
|
@@ -69,6 +75,15 @@ export declare function getAuthMethod(db: Database.Database): AuthMethod | null;
|
|
|
69
75
|
* Clear saved auth method preference (will prompt again next time)
|
|
70
76
|
*/
|
|
71
77
|
export declare function clearAuthMethod(db: Database.Database): void;
|
|
78
|
+
/**
|
|
79
|
+
* Get saved PR creation default preference.
|
|
80
|
+
* Returns null if no preference has been saved (user should be prompted).
|
|
81
|
+
*/
|
|
82
|
+
export declare function getCreatePrDefault(db: Database.Database): boolean | null;
|
|
83
|
+
/**
|
|
84
|
+
* Save PR creation default preference.
|
|
85
|
+
*/
|
|
86
|
+
export declare function saveCreatePrDefault(db: Database.Database, createPr: boolean): void;
|
|
72
87
|
/**
|
|
73
88
|
* Check if terminal app preference has been set
|
|
74
89
|
*/
|
|
@@ -27,12 +27,14 @@ const CONFIG_KEYS = {
|
|
|
27
27
|
dockerNetwork: 'execution.docker.network',
|
|
28
28
|
dockerMemory: 'execution.docker.memory',
|
|
29
29
|
dockerCpus: 'execution.docker.cpus',
|
|
30
|
+
firewallAllowlistDomains: 'execution.firewall.allowlist_domains',
|
|
30
31
|
vmDefaultHost: 'execution.vm.default_host',
|
|
31
32
|
vmUser: 'execution.vm.user',
|
|
32
33
|
vmKeyPath: 'execution.vm.key_path',
|
|
33
34
|
vmSyncMethod: 'execution.vm.sync_method',
|
|
34
35
|
coderName: 'coder.name',
|
|
35
36
|
authMethod: 'execution.auth_method',
|
|
37
|
+
createPrDefault: 'execution.create_pr_default',
|
|
36
38
|
};
|
|
37
39
|
/**
|
|
38
40
|
* Get a setting value from the database
|
|
@@ -103,6 +105,11 @@ export function loadExecutionConfig(db) {
|
|
|
103
105
|
if (authMethod) {
|
|
104
106
|
config.authMethod = authMethod;
|
|
105
107
|
}
|
|
108
|
+
// Load create PR default preference
|
|
109
|
+
const createPrDefault = getSetting(db, CONFIG_KEYS.createPrDefault);
|
|
110
|
+
if (createPrDefault !== null) {
|
|
111
|
+
config.createPrDefault = createPrDefault === 'true';
|
|
112
|
+
}
|
|
106
113
|
// Load tmux settings
|
|
107
114
|
const tmuxSession = getSetting(db, CONFIG_KEYS.tmuxSession);
|
|
108
115
|
if (tmuxSession) {
|
|
@@ -133,6 +140,28 @@ export function loadExecutionConfig(db) {
|
|
|
133
140
|
if (dockerCpus) {
|
|
134
141
|
config.docker = { ...config.docker, cpus: parseInt(dockerCpus, 10) };
|
|
135
142
|
}
|
|
143
|
+
// Load firewall allowlist domains
|
|
144
|
+
const firewallAllowlistDomains = getSetting(db, CONFIG_KEYS.firewallAllowlistDomains);
|
|
145
|
+
if (firewallAllowlistDomains) {
|
|
146
|
+
let parsed = [];
|
|
147
|
+
try {
|
|
148
|
+
const jsonValue = JSON.parse(firewallAllowlistDomains);
|
|
149
|
+
if (Array.isArray(jsonValue)) {
|
|
150
|
+
parsed = jsonValue
|
|
151
|
+
.filter((domain) => typeof domain === 'string')
|
|
152
|
+
.map(domain => domain.trim())
|
|
153
|
+
.filter(Boolean);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Backward-compatible fallback: comma-separated string
|
|
158
|
+
parsed = firewallAllowlistDomains
|
|
159
|
+
.split(',')
|
|
160
|
+
.map(domain => domain.trim())
|
|
161
|
+
.filter(Boolean);
|
|
162
|
+
}
|
|
163
|
+
config.firewall = { ...config.firewall, allowlistDomains: parsed };
|
|
164
|
+
}
|
|
136
165
|
// Load VM settings
|
|
137
166
|
const vmDefaultHost = getSetting(db, CONFIG_KEYS.vmDefaultHost);
|
|
138
167
|
if (vmDefaultHost) {
|
|
@@ -184,6 +213,13 @@ export function saveTmuxControlMode(db, enabled) {
|
|
|
184
213
|
export function saveTerminalOpenInBackground(db, enabled) {
|
|
185
214
|
setSetting(db, CONFIG_KEYS.terminalOpenInBackground, enabled.toString());
|
|
186
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Save extra firewall allowlist domains.
|
|
218
|
+
*/
|
|
219
|
+
export function saveFirewallAllowlistDomains(db, domains) {
|
|
220
|
+
const cleaned = [...new Set(domains.map(domain => domain.trim()).filter(Boolean))];
|
|
221
|
+
setSetting(db, CONFIG_KEYS.firewallAllowlistDomains, JSON.stringify(cleaned));
|
|
222
|
+
}
|
|
187
223
|
/**
|
|
188
224
|
* Save auth method preference (oauth or apikey)
|
|
189
225
|
*/
|
|
@@ -206,6 +242,24 @@ export function getAuthMethod(db) {
|
|
|
206
242
|
export function clearAuthMethod(db) {
|
|
207
243
|
db.prepare(`DELETE FROM ${SETTINGS_TABLE} WHERE key = ?`).run(CONFIG_KEYS.authMethod);
|
|
208
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Get saved PR creation default preference.
|
|
247
|
+
* Returns null if no preference has been saved (user should be prompted).
|
|
248
|
+
*/
|
|
249
|
+
export function getCreatePrDefault(db) {
|
|
250
|
+
const value = getSetting(db, CONFIG_KEYS.createPrDefault);
|
|
251
|
+
if (value === 'true')
|
|
252
|
+
return true;
|
|
253
|
+
if (value === 'false')
|
|
254
|
+
return false;
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Save PR creation default preference.
|
|
259
|
+
*/
|
|
260
|
+
export function saveCreatePrDefault(db, createPr) {
|
|
261
|
+
setSetting(db, CONFIG_KEYS.createPrDefault, createPr.toString());
|
|
262
|
+
}
|
|
209
263
|
/**
|
|
210
264
|
* Check if terminal app preference has been set
|
|
211
265
|
*/
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Generates .devcontainer/ configuration for agent sandboxed execution.
|
|
5
5
|
* Uses a custom Dockerfile with network firewall for security sandboxing.
|
|
6
6
|
*/
|
|
7
|
-
import { ExecutionConfig } from './types.js';
|
|
7
|
+
import { ExecutionConfig, ExecutorType } from './types.js';
|
|
8
8
|
export type MountMode = 'worktree' | 'clone';
|
|
9
9
|
export interface DevcontainerOptions {
|
|
10
10
|
agentName: string;
|
|
@@ -21,6 +21,8 @@ export interface DevcontainerOptions {
|
|
|
21
21
|
gitUserName?: string;
|
|
22
22
|
/** Git user.email for commit attribution (detected from gh/git config on host) */
|
|
23
23
|
gitUserEmail?: string;
|
|
24
|
+
/** Executor type - determines which CLI tools to install and which API domains to whitelist */
|
|
25
|
+
executor?: ExecutorType;
|
|
24
26
|
}
|
|
25
27
|
export interface DevcontainerJson {
|
|
26
28
|
name: string;
|
|
@@ -58,9 +60,10 @@ export declare function generateDevcontainerJson(options: DevcontainerOptions, c
|
|
|
58
60
|
export declare function generateDockerfile(options: DevcontainerOptions): string;
|
|
59
61
|
/**
|
|
60
62
|
* Generate firewall initialization script.
|
|
61
|
-
* Whitelists only necessary domains for
|
|
63
|
+
* Whitelists only necessary domains for the configured executor.
|
|
64
|
+
* Claude Code requires api.anthropic.com; Codex requires api.openai.com.
|
|
62
65
|
*/
|
|
63
|
-
export declare function generateFirewallScript(): string;
|
|
66
|
+
export declare function generateFirewallScript(executor?: ExecutorType, extraAllowedDomains?: string[]): string;
|
|
64
67
|
/**
|
|
65
68
|
* Generate prlt setup script.
|
|
66
69
|
* Rebuilds better-sqlite3 if prlt is mounted from host (not installed via npm).
|
|
@@ -35,13 +35,16 @@ export function generateDevcontainerJson(options, config) {
|
|
|
35
35
|
if (channel.registry === 'gh') {
|
|
36
36
|
buildArgs.GITHUB_TOKEN = '${localEnv:GITHUB_TOKEN}';
|
|
37
37
|
}
|
|
38
|
+
// Determine if this is a Claude Code executor (for Claude-specific mounts/config)
|
|
39
|
+
const isClaude = !options.executor || options.executor === 'claude-code';
|
|
38
40
|
// Build mounts array - parent repo mounts only needed for worktree mode
|
|
39
41
|
// TKT-801: Use consistency=cached to reduce grpcfuse contention on Docker Desktop.
|
|
40
42
|
// This helps prevent kernel panics when multiple containers mount the same paths concurrently.
|
|
41
43
|
const mounts = [
|
|
42
44
|
'source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached',
|
|
43
45
|
'source=claude-bash-history,target=/commandhistory,type=volume',
|
|
44
|
-
|
|
46
|
+
// Claude credentials volume - only needed for Claude Code executor
|
|
47
|
+
...(isClaude ? ['source=claude-credentials,target=/home/node/.claude,type=volume'] : []),
|
|
45
48
|
// NOTE: ~/.claude.json is COPIED (not mounted) to /workspace/.claude.json
|
|
46
49
|
// to avoid corruption from concurrent writes by multiple containers
|
|
47
50
|
// NOTE: SSH agent socket mounting doesn't work reliably on Docker Desktop for Mac
|
|
@@ -170,9 +173,9 @@ RUN mkdir -p /home/node/.npm-global/bin /home/node/.npm-global/lib \\
|
|
|
170
173
|
ENV NPM_CONFIG_PREFIX=/home/node/.npm-global
|
|
171
174
|
ENV PATH=/home/node/.npm-global/bin:\$PATH
|
|
172
175
|
|
|
173
|
-
# Install pnpm and
|
|
176
|
+
# Install pnpm and executor CLI as node user so files are owned correctly
|
|
174
177
|
USER node
|
|
175
|
-
RUN npm install -g pnpm && npm install -g @anthropic-ai/claude-code
|
|
178
|
+
RUN npm install -g pnpm && npm install -g @anthropic-ai/claude-code${options.executor === 'codex' ? ' && npm install -g @openai/codex' : ''}
|
|
176
179
|
USER root
|
|
177
180
|
|
|
178
181
|
# Install prlt CLI from public npm
|
|
@@ -207,9 +210,15 @@ WORKDIR /workspace
|
|
|
207
210
|
}
|
|
208
211
|
/**
|
|
209
212
|
* Generate firewall initialization script.
|
|
210
|
-
* Whitelists only necessary domains for
|
|
213
|
+
* Whitelists only necessary domains for the configured executor.
|
|
214
|
+
* Claude Code requires api.anthropic.com; Codex requires api.openai.com.
|
|
211
215
|
*/
|
|
212
|
-
export function generateFirewallScript() {
|
|
216
|
+
export function generateFirewallScript(executor, extraAllowedDomains = []) {
|
|
217
|
+
const extraDomainCommands = [...new Set(extraAllowedDomains
|
|
218
|
+
.map(domain => domain.trim().toLowerCase())
|
|
219
|
+
.filter(domain => /^[a-z0-9.-]+$/.test(domain)))]
|
|
220
|
+
.map(domain => `add_domain "${domain}"`)
|
|
221
|
+
.join('\n');
|
|
213
222
|
return `#!/bin/bash
|
|
214
223
|
set -e
|
|
215
224
|
|
|
@@ -291,9 +300,12 @@ add_domain "objects.githubusercontent.com"
|
|
|
291
300
|
add_domain "raw.githubusercontent.com"
|
|
292
301
|
add_domain "npm.pkg.github.com"
|
|
293
302
|
|
|
294
|
-
# Add
|
|
303
|
+
# Add executor-specific API domains
|
|
295
304
|
add_domain "api.anthropic.com"
|
|
296
305
|
add_domain "console.anthropic.com"
|
|
306
|
+
${executor === 'codex' ? `# Codex API domains
|
|
307
|
+
add_domain "api.openai.com"
|
|
308
|
+
add_domain "openai.com"` : ''}
|
|
297
309
|
add_domain "statsigapi.net"
|
|
298
310
|
add_domain "sentry.io"
|
|
299
311
|
add_domain "registry.npmjs.org"
|
|
@@ -301,6 +313,19 @@ add_domain "npmjs.com"
|
|
|
301
313
|
add_domain "nodejs.org"
|
|
302
314
|
add_domain "update.code.visualstudio.com"
|
|
303
315
|
add_domain "vscode.download.prss.microsoft.com"
|
|
316
|
+
${extraDomainCommands ? `# Additional user-configured allowlist domains
|
|
317
|
+
${extraDomainCommands}` : ''}
|
|
318
|
+
|
|
319
|
+
# Runtime allowlist domains (comma-separated), e.g. PRLT_EXTRA_ALLOWLIST_DOMAINS=api.staging.example.com
|
|
320
|
+
if [ -n "\${PRLT_EXTRA_ALLOWLIST_DOMAINS:-}" ]; then
|
|
321
|
+
IFS=',' read -ra EXTRA_DOMAINS <<< "$PRLT_EXTRA_ALLOWLIST_DOMAINS"
|
|
322
|
+
for domain in "\${EXTRA_DOMAINS[@]}"; do
|
|
323
|
+
domain="\${domain//[[:space:]]/}"
|
|
324
|
+
if [ -n "$domain" ]; then
|
|
325
|
+
add_domain "$domain"
|
|
326
|
+
fi
|
|
327
|
+
done
|
|
328
|
+
fi
|
|
304
329
|
|
|
305
330
|
# Allow traffic to whitelisted IPs
|
|
306
331
|
iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT
|
|
@@ -522,6 +547,8 @@ configure_git_identity
|
|
|
522
547
|
|
|
523
548
|
# Check if prlt is already installed globally (via npm)
|
|
524
549
|
# TKT-954: Also check for updates - Docker layer caching may have installed an older version
|
|
550
|
+
# TKT-1029: Don't exit early - continue to workspace dependency installation
|
|
551
|
+
PRLT_CONFIGURED=false
|
|
525
552
|
if command -v prlt &> /dev/null; then
|
|
526
553
|
PRLT_PATH=$(which prlt)
|
|
527
554
|
if [[ "$PRLT_PATH" == "/home/node/.npm-global/bin/prlt" ]]; then
|
|
@@ -544,12 +571,12 @@ if command -v prlt &> /dev/null; then
|
|
|
544
571
|
else
|
|
545
572
|
echo "prlt v\${CURRENT_VERSION} is up to date"
|
|
546
573
|
fi
|
|
547
|
-
|
|
574
|
+
PRLT_CONFIGURED=true
|
|
548
575
|
fi
|
|
549
576
|
fi
|
|
550
577
|
|
|
551
|
-
# Check if mounted prlt exists at /opt/prlt
|
|
552
|
-
if [ -d "/opt/prlt/apps/cli" ]; then
|
|
578
|
+
# Check if mounted prlt exists at /opt/prlt (skip if already configured via npm)
|
|
579
|
+
if [ "$PRLT_CONFIGURED" = "false" ] && [ -d "/opt/prlt/apps/cli" ]; then
|
|
553
580
|
echo "Setting up mounted prlt..."
|
|
554
581
|
|
|
555
582
|
PRLT_LOCAL="/home/node/.prlt-local"
|
|
@@ -597,7 +624,7 @@ WRAPPER_EOF
|
|
|
597
624
|
# Create prltdev symlink for consistency with dev environment
|
|
598
625
|
ln -sf "$WRAPPER" /home/node/.npm-global/bin/prltdev
|
|
599
626
|
echo "prlt wrapper ready at $WRAPPER (also available as prltdev)"
|
|
600
|
-
|
|
627
|
+
elif [ "$PRLT_CONFIGURED" = "false" ]; then
|
|
601
628
|
echo "No mounted prlt found, skipping setup"
|
|
602
629
|
fi
|
|
603
630
|
|
|
@@ -647,8 +674,8 @@ export function createDevcontainerConfig(options, config) {
|
|
|
647
674
|
const dockerfile = generateDockerfile(options);
|
|
648
675
|
const dockerfilePath = path.join(devcontainerDir, 'Dockerfile');
|
|
649
676
|
fs.writeFileSync(dockerfilePath, dockerfile);
|
|
650
|
-
// Generate and write firewall script
|
|
651
|
-
const firewallScript = generateFirewallScript();
|
|
677
|
+
// Generate and write firewall script (executor-aware for API domain whitelisting)
|
|
678
|
+
const firewallScript = generateFirewallScript(options.executor, config?.firewall.allowlistDomains ?? []);
|
|
652
679
|
const firewallScriptPath = path.join(devcontainerDir, 'init-firewall.sh');
|
|
653
680
|
fs.writeFileSync(firewallScriptPath, firewallScript, { mode: 0o755 });
|
|
654
681
|
// Generate and write prlt setup script
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Implementations for each execution environment (devcontainer, host, docker, vm).
|
|
5
5
|
*/
|
|
6
|
-
import { ExecutionEnvironment, DisplayMode, SessionManager, ExecutorType, ExecutionContext, ExecutionConfig } from './types.js';
|
|
6
|
+
import { ExecutionEnvironment, DisplayMode, OutputMode, SessionManager, ExecutorType, ExecutionContext, ExecutionConfig } from './types.js';
|
|
7
7
|
/**
|
|
8
8
|
* Build a unified name for tmux sessions, window names, and tab titles.
|
|
9
9
|
* Format: "{ticketId}-{action}-{agentName}"
|
|
@@ -41,17 +41,6 @@ export declare function buildTmuxAttachCommand(useControlMode: boolean, includeU
|
|
|
41
41
|
*/
|
|
42
42
|
export declare function configureITermTmuxPreferences(mode: 'tab' | 'window'): void;
|
|
43
43
|
export declare function configureITermTmuxWindowMode(mode: 'tab' | 'window'): void;
|
|
44
|
-
/**
|
|
45
|
-
* Build the tmux script that runs inside the container.
|
|
46
|
-
* In background mode: kills PID 1 (sleep infinity) after Claude exits to stop/remove container.
|
|
47
|
-
* In terminal/foreground mode: drops into exec bash for user inspection.
|
|
48
|
-
*/
|
|
49
|
-
export declare function buildTmuxScript(sessionName: string, claudeCmd: string, displayMode: DisplayMode): string;
|
|
50
|
-
/**
|
|
51
|
-
* Get the auto-remove flags for docker run based on display mode.
|
|
52
|
-
* Background mode containers get --rm so Docker removes them when they stop.
|
|
53
|
-
*/
|
|
54
|
-
export declare function getDockerAutoRemoveFlags(displayMode: DisplayMode): string[];
|
|
55
44
|
/**
|
|
56
45
|
* Check if the claude-credentials Docker volume exists.
|
|
57
46
|
*/
|
|
@@ -74,40 +63,41 @@ export declare function getDockerCredentialInfo(): {
|
|
|
74
63
|
expiresAt: Date;
|
|
75
64
|
subscriptionType?: string;
|
|
76
65
|
} | null;
|
|
66
|
+
export declare function getExecutorCommand(executor: ExecutorType, prompt: string, skipPermissions?: boolean): {
|
|
67
|
+
cmd: string;
|
|
68
|
+
args: string[];
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Check if an executor is Claude Code.
|
|
72
|
+
* Used to gate Claude-specific flags and configuration.
|
|
73
|
+
*/
|
|
74
|
+
export declare function isClaudeExecutor(executor: ExecutorType): boolean;
|
|
77
75
|
/**
|
|
78
|
-
*
|
|
76
|
+
* Get the display name for an executor type.
|
|
79
77
|
*/
|
|
78
|
+
export declare function getExecutorDisplayName(executor: ExecutorType): string;
|
|
79
|
+
/**
|
|
80
|
+
* Get the npm package name for an executor (for container installation).
|
|
81
|
+
*/
|
|
82
|
+
export declare function getExecutorPackage(executor: ExecutorType): string | null;
|
|
80
83
|
export interface PreflightResult {
|
|
81
84
|
ok: boolean;
|
|
82
85
|
error?: string;
|
|
83
86
|
}
|
|
84
87
|
/**
|
|
85
|
-
* Check
|
|
86
|
-
* Returns a PreflightResult with ok=true if the binary is found,
|
|
87
|
-
* or ok=false with a descriptive error and remediation hint.
|
|
88
|
+
* Check executor binary availability on host.
|
|
88
89
|
*/
|
|
89
90
|
export declare function checkExecutorOnHost(executor: ExecutorType): PreflightResult;
|
|
90
91
|
/**
|
|
91
|
-
* Check
|
|
92
|
-
* Returns a PreflightResult with ok=true if the binary is found,
|
|
93
|
-
* or ok=false with a descriptive error and remediation hint.
|
|
92
|
+
* Check executor binary availability inside a container.
|
|
94
93
|
*/
|
|
95
94
|
export declare function checkExecutorInContainer(executor: ExecutorType, containerId: string): PreflightResult;
|
|
96
95
|
/**
|
|
97
|
-
* Run preflight checks for
|
|
98
|
-
* Validates that the executor binary is available before spawning.
|
|
99
|
-
*
|
|
100
|
-
* Checks performed per environment:
|
|
101
|
-
* - host: Verify binary on PATH
|
|
102
|
-
* - devcontainer: Verify binary inside container (if container running)
|
|
103
|
-
* - docker: Verify binary on host (used in docker run command)
|
|
104
|
-
* - vm: Verify binary on host (will be checked on remote separately)
|
|
96
|
+
* Run executor preflight checks for the target environment.
|
|
105
97
|
*/
|
|
106
|
-
export declare function runExecutorPreflight(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
args: string[];
|
|
110
|
-
};
|
|
98
|
+
export declare function runExecutorPreflight(environment: ExecutionEnvironment, executor: ExecutorType, options?: {
|
|
99
|
+
containerId?: string;
|
|
100
|
+
}): PreflightResult;
|
|
111
101
|
export interface RunnerResult {
|
|
112
102
|
success: boolean;
|
|
113
103
|
pid?: string;
|
|
@@ -167,6 +157,12 @@ export declare function isContainerRunning(containerName: string): boolean;
|
|
|
167
157
|
* Get the container ID for a running container.
|
|
168
158
|
*/
|
|
169
159
|
export declare function getContainerId(containerName: string): string | null;
|
|
160
|
+
/**
|
|
161
|
+
* Build the command to run Claude inside the container.
|
|
162
|
+
* Uses docker exec for direct container access.
|
|
163
|
+
* Uses a prompt file to avoid shell escaping issues.
|
|
164
|
+
*/
|
|
165
|
+
export declare function buildDevcontainerCommand(context: ExecutionContext, executor: ExecutorType, promptFile: string, containerId?: string, outputMode?: OutputMode, sandboxed?: boolean, displayMode?: DisplayMode): string;
|
|
170
166
|
/**
|
|
171
167
|
* Run command inside a Docker container.
|
|
172
168
|
* Uses raw Docker commands for filesystem isolation - no devcontainer CLI required.
|