@proletariat/cli 0.3.35 → 0.3.40
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 +37 -2
- package/bin/dev.js +0 -0
- package/dist/commands/agent/auth.d.ts +12 -2
- package/dist/commands/agent/auth.js +128 -4
- package/dist/commands/agent/list.js +16 -7
- package/dist/commands/agent/status.js +32 -4
- package/dist/commands/board/watch.js +6 -0
- package/dist/commands/branch/list.d.ts +1 -0
- package/dist/commands/branch/list.js +43 -12
- package/dist/commands/branch/where.js +9 -19
- package/dist/commands/category/list.d.ts +2 -1
- package/dist/commands/category/list.js +38 -13
- package/dist/commands/{claude.d.ts → claude/index.d.ts} +1 -1
- package/dist/commands/{claude.js → claude/index.js} +12 -12
- package/dist/commands/claude/open.d.ts +13 -0
- package/dist/commands/claude/open.js +175 -0
- package/dist/commands/diet.js +18 -2
- package/dist/commands/docker/logs.js +7 -3
- package/dist/commands/docker/shell.js +6 -0
- package/dist/commands/docker/start.js +20 -4
- package/dist/commands/docker/sync.d.ts +4 -0
- package/dist/commands/docker/sync.js +30 -2
- package/dist/commands/epic/show.d.ts +13 -0
- package/dist/commands/epic/show.js +16 -0
- package/dist/commands/epic/ticket.js +7 -24
- package/dist/commands/epic/view.js +27 -0
- package/dist/commands/execution/config.d.ts +0 -4
- package/dist/commands/execution/config.js +14 -46
- package/dist/commands/execution/index.js +2 -1
- package/dist/commands/execution/logs.js +7 -1
- package/dist/commands/execution/stop.js +2 -1
- package/dist/commands/execution/view.js +30 -26
- package/dist/commands/init.js +2 -19
- package/dist/commands/label/create.js +2 -1
- package/dist/commands/label/delete.js +2 -1
- package/dist/commands/label/group/create.js +2 -1
- package/dist/commands/label/group/list.js +2 -1
- package/dist/commands/label/list.js +2 -1
- package/dist/commands/mcp-server.js +27 -1
- package/dist/commands/phase/template/list.js +2 -1
- package/dist/commands/pmo/init.js +12 -40
- package/dist/commands/project/create.js +3 -4
- package/dist/commands/project/update.js +5 -6
- package/dist/commands/pull.js +24 -0
- package/dist/commands/qa/index.d.ts +54 -0
- package/dist/commands/qa/index.js +762 -0
- package/dist/commands/repo/view.js +2 -8
- package/dist/commands/session/attach.js +4 -4
- package/dist/commands/session/create.d.ts +19 -0
- package/dist/commands/session/create.js +102 -0
- package/dist/commands/session/health.js +4 -23
- package/dist/commands/session/index.js +14 -1
- package/dist/commands/session/list.js +9 -8
- package/dist/commands/session/peek.d.ts +38 -0
- package/dist/commands/session/peek.js +316 -0
- package/dist/commands/session/poke.d.ts +27 -0
- package/dist/commands/session/poke.js +219 -0
- package/dist/commands/spec/view.js +29 -0
- package/dist/commands/template/list.js +2 -1
- package/dist/commands/theme/add-names.d.ts +4 -0
- package/dist/commands/theme/add-names.js +11 -1
- package/dist/commands/theme/create.d.ts +2 -0
- package/dist/commands/theme/create.js +8 -0
- package/dist/commands/ticket/bulk.js +2 -2
- package/dist/commands/ticket/complete.js +2 -2
- package/dist/commands/ticket/create.js +21 -0
- package/dist/commands/ticket/delete.js +8 -0
- package/dist/commands/ticket/edit.js +25 -0
- package/dist/commands/ticket/epic.js +17 -43
- package/dist/commands/ticket/index.js +2 -2
- package/dist/commands/ticket/move.js +25 -2
- package/dist/commands/ticket/resolve.js +3 -4
- package/dist/commands/ticket/show.d.ts +13 -0
- package/dist/commands/ticket/show.js +16 -0
- package/dist/commands/ticket/template/list.js +2 -1
- package/dist/commands/ticket/view.d.ts +0 -1
- package/dist/commands/ticket/view.js +30 -1
- package/dist/commands/work/index.js +4 -0
- package/dist/commands/work/spawn-all.js +1 -1
- package/dist/commands/work/spawn.js +15 -4
- package/dist/commands/work/start.js +186 -103
- package/dist/commands/work/status.d.ts +14 -0
- package/dist/commands/work/status.js +60 -0
- package/dist/commands/work/watch.js +1 -1
- package/dist/commands/workflow/index.js +2 -1
- package/dist/commands/workflow/show.d.ts +13 -0
- package/dist/commands/workflow/show.js +16 -0
- package/dist/commands/workspace/add.js +15 -0
- package/dist/commands/workspace/list.js +2 -1
- package/dist/commands/workspace/prune.js +7 -7
- package/dist/hooks/init.js +10 -2
- package/dist/lib/agents/commands.d.ts +5 -0
- package/dist/lib/agents/commands.js +143 -97
- package/dist/lib/branch/index.d.ts +1 -0
- package/dist/lib/database/drizzle-schema.d.ts +465 -0
- package/dist/lib/database/drizzle-schema.js +53 -0
- package/dist/lib/database/index.d.ts +47 -1
- package/dist/lib/database/index.js +138 -20
- package/dist/lib/execution/config.d.ts +15 -1
- package/dist/lib/execution/config.js +28 -0
- package/dist/lib/execution/runners.d.ts +45 -0
- package/dist/lib/execution/runners.js +187 -26
- package/dist/lib/execution/session-utils.d.ts +16 -1
- package/dist/lib/execution/session-utils.js +71 -4
- package/dist/lib/execution/spawner.js +15 -2
- package/dist/lib/execution/storage.d.ts +6 -1
- package/dist/lib/execution/storage.js +35 -5
- package/dist/lib/execution/types.d.ts +3 -0
- package/dist/lib/mcp/tools/board.js +4 -6
- package/dist/lib/mcp/tools/cli-passthrough.js +25 -6
- package/dist/lib/mcp/tools/epic.js +8 -3
- package/dist/lib/mcp/tools/index.d.ts +1 -0
- package/dist/lib/mcp/tools/index.js +1 -0
- package/dist/lib/mcp/tools/spec.js +1 -1
- package/dist/lib/mcp/tools/ticket.js +11 -9
- package/dist/lib/mcp/tools/tmux.d.ts +16 -0
- package/dist/lib/mcp/tools/tmux.js +182 -0
- package/dist/lib/mcp/tools/work.js +148 -6
- package/dist/lib/mcp/types.d.ts +10 -0
- package/dist/lib/multiline-input.js +2 -1
- package/dist/lib/pmo/base-command.js +4 -4
- package/dist/lib/pmo/schema.d.ts +1 -1
- package/dist/lib/pmo/schema.js +1 -0
- package/dist/lib/pmo/storage/actions.js +1 -1
- package/dist/lib/pmo/storage/base.js +402 -50
- package/dist/lib/pmo/storage/dependencies.d.ts +1 -0
- package/dist/lib/pmo/storage/dependencies.js +11 -3
- package/dist/lib/pmo/storage/epics.js +1 -1
- package/dist/lib/pmo/storage/helpers.d.ts +4 -4
- package/dist/lib/pmo/storage/helpers.js +36 -26
- package/dist/lib/pmo/storage/projects.d.ts +2 -0
- package/dist/lib/pmo/storage/projects.js +207 -119
- package/dist/lib/pmo/storage/specs.d.ts +2 -0
- package/dist/lib/pmo/storage/specs.js +274 -188
- package/dist/lib/pmo/storage/tickets.d.ts +2 -0
- package/dist/lib/pmo/storage/tickets.js +350 -290
- package/dist/lib/pmo/storage/types.d.ts +1 -0
- package/dist/lib/pmo/storage/views.d.ts +2 -0
- package/dist/lib/pmo/storage/views.js +183 -130
- package/dist/lib/prompt-command.d.ts +20 -0
- package/dist/lib/prompt-command.js +38 -2
- package/dist/lib/prompt-json.d.ts +41 -4
- package/dist/lib/prompt-json.js +138 -7
- package/dist/lib/styles.d.ts +37 -0
- package/dist/lib/styles.js +73 -0
- package/oclif.manifest.json +4046 -3385
- package/package.json +11 -6
- package/LICENSE +0 -190
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { PromptCommand } from '../../lib/prompt-command.js';
|
|
7
|
+
import { machineOutputFlags } from '../../lib/pmo/index.js';
|
|
8
|
+
import Database from 'better-sqlite3';
|
|
9
|
+
import { findHQRoot } from '../../lib/workspace.js';
|
|
10
|
+
import { getWorkspaceInfo, createEphemeralAgent, } from '../../lib/agents/commands.js';
|
|
11
|
+
import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
|
|
12
|
+
import { styles } from '../../lib/styles.js';
|
|
13
|
+
import { DEFAULT_EXECUTION_CONFIG, } from '../../lib/execution/types.js';
|
|
14
|
+
import { runExecution, isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled } from '../../lib/execution/runners.js';
|
|
15
|
+
import { ExecutionStorage } from '../../lib/execution/storage.js';
|
|
16
|
+
import { loadExecutionConfig, getTerminalApp, promptTerminalPreference, getShell, promptShellPreference, hasTerminalPreference, hasShellPreference, } from '../../lib/execution/config.js';
|
|
17
|
+
import { hasDevcontainerConfig } from '../../lib/execution/devcontainer.js';
|
|
18
|
+
// Catch-all devcontainer image for directories without .devcontainer
|
|
19
|
+
const CATCHALL_DEVCONTAINER_IMAGE = 'ghcr.io/chrismcdermut/proletariat-claude:latest';
|
|
20
|
+
export default class QA extends PromptCommand {
|
|
21
|
+
static description = 'Spawn an exploratory QA agent to autonomously test the CLI (no ticket required)';
|
|
22
|
+
static aliases = ['explore'];
|
|
23
|
+
static examples = [
|
|
24
|
+
'<%= config.bin %> <%= command.id %> # Quick launch QA agent',
|
|
25
|
+
'<%= config.bin %> <%= command.id %> --seed # Seed test data first',
|
|
26
|
+
'<%= config.bin %> <%= command.id %> --watch # Stream agent\'s tmux screen',
|
|
27
|
+
'<%= config.bin %> <%= command.id %> --environment host # Run on host instead of container',
|
|
28
|
+
'<%= config.bin %> <%= command.id %> --seed --watch # Seed data and watch live',
|
|
29
|
+
];
|
|
30
|
+
static flags = {
|
|
31
|
+
...machineOutputFlags,
|
|
32
|
+
seed: Flags.boolean({
|
|
33
|
+
char: 's',
|
|
34
|
+
description: 'Seed test data before starting QA (runs seed-explore-data.mjs)',
|
|
35
|
+
default: false,
|
|
36
|
+
}),
|
|
37
|
+
watch: Flags.boolean({
|
|
38
|
+
char: 'w',
|
|
39
|
+
description: 'Stream the agent\'s tmux screen to your terminal in real-time',
|
|
40
|
+
default: false,
|
|
41
|
+
}),
|
|
42
|
+
environment: Flags.string({
|
|
43
|
+
char: 'e',
|
|
44
|
+
description: 'Where to run (devcontainer or host)',
|
|
45
|
+
options: ['devcontainer', 'host'],
|
|
46
|
+
}),
|
|
47
|
+
'display-mode': Flags.string({
|
|
48
|
+
char: 'd',
|
|
49
|
+
description: 'How to display output (foreground=current terminal, terminal=new tab, background=detached)',
|
|
50
|
+
options: ['terminal', 'background', 'foreground'],
|
|
51
|
+
}),
|
|
52
|
+
'permission-mode': Flags.string({
|
|
53
|
+
char: 'p',
|
|
54
|
+
description: 'Permission mode (danger: skip prompts, safe: require approval)',
|
|
55
|
+
options: ['danger', 'safe'],
|
|
56
|
+
}),
|
|
57
|
+
prompt: Flags.string({
|
|
58
|
+
description: 'Additional instructions to append to the QA prompt',
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
async run() {
|
|
62
|
+
const { flags } = await this.parse(QA);
|
|
63
|
+
const jsonMode = shouldOutputJson(flags);
|
|
64
|
+
const workDir = process.cwd();
|
|
65
|
+
// Check if we're inside an HQ
|
|
66
|
+
const hqPath = findHQRoot(workDir);
|
|
67
|
+
if (hqPath) {
|
|
68
|
+
await this.runTracked(hqPath, workDir, flags, jsonMode);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
await this.runYolo(workDir, flags, jsonMode);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Load the explore-cli action prompt from PMO storage.
|
|
76
|
+
*/
|
|
77
|
+
async getExploreCLIPrompt() {
|
|
78
|
+
try {
|
|
79
|
+
const { getPMOContext } = await import('../../lib/pmo/index.js');
|
|
80
|
+
const pmoContext = await getPMOContext();
|
|
81
|
+
const action = await pmoContext.storage.getAction('explore-cli');
|
|
82
|
+
if (action) {
|
|
83
|
+
return { prompt: action.prompt, endPrompt: action.endPrompt ?? undefined };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// PMO not available - use fallback
|
|
88
|
+
}
|
|
89
|
+
// Fallback: return a minimal explore-cli prompt
|
|
90
|
+
return {
|
|
91
|
+
prompt: `# Action: Explore CLI (Autonomous QA)
|
|
92
|
+
|
|
93
|
+
You are an AI QA tester for the prlt CLI. You have access to a tmux session where the CLI is running.
|
|
94
|
+
Your job is to **systematically explore every menu, try every option, and find bugs**.
|
|
95
|
+
|
|
96
|
+
## Getting Started
|
|
97
|
+
|
|
98
|
+
1. Start a tmux session for testing:
|
|
99
|
+
\`\`\`
|
|
100
|
+
tmux_start_session({ session: "qa-test", command: "prlt" })
|
|
101
|
+
\`\`\`
|
|
102
|
+
2. Wait a moment, then capture the screen to see the main menu:
|
|
103
|
+
\`\`\`
|
|
104
|
+
tmux_capture_pane({ session: "qa-test" })
|
|
105
|
+
\`\`\`
|
|
106
|
+
3. Begin systematic exploration of all menus and features.
|
|
107
|
+
|
|
108
|
+
## When You Find a Bug
|
|
109
|
+
|
|
110
|
+
1. Document exact reproduction steps
|
|
111
|
+
2. Capture the screen showing the bug
|
|
112
|
+
3. File a ticket using the ticket_create MCP tool with category "bug"
|
|
113
|
+
|
|
114
|
+
## Session Management
|
|
115
|
+
|
|
116
|
+
- If the CLI crashes, restart it: kill the session and start a new one
|
|
117
|
+
- If you get stuck, use Ctrl+C to escape, or kill and restart the session`,
|
|
118
|
+
endPrompt: `## Wrap-Up
|
|
119
|
+
|
|
120
|
+
Summarize your findings: list all bugs found with their ticket IDs, areas tested, and overall assessment.
|
|
121
|
+
Clean up your tmux session when done.`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Run seed-explore-data.mjs to pre-populate test data.
|
|
126
|
+
*/
|
|
127
|
+
async runSeedData(hqPath) {
|
|
128
|
+
// Look for seed script in the prlt source repo or fallback paths
|
|
129
|
+
const seedPaths = [
|
|
130
|
+
path.join(hqPath, 'scripts', 'seed-explore-data.mjs'),
|
|
131
|
+
path.join(hqPath, 'repos', 'proletariat', 'scripts', 'seed-explore-data.mjs'),
|
|
132
|
+
];
|
|
133
|
+
// Also check workspace repos for the script
|
|
134
|
+
try {
|
|
135
|
+
const workspaceInfo = getWorkspaceInfo();
|
|
136
|
+
for (const repo of workspaceInfo.repositories) {
|
|
137
|
+
const repoSeedPath = path.join(workspaceInfo.path, repo.path || repo.name, 'scripts', 'seed-explore-data.mjs');
|
|
138
|
+
if (!seedPaths.includes(repoSeedPath)) {
|
|
139
|
+
seedPaths.push(repoSeedPath);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Workspace info not available
|
|
145
|
+
}
|
|
146
|
+
// Find the global prlt installation directory for bundled seed script
|
|
147
|
+
try {
|
|
148
|
+
const prltPath = execSync('which prlt', { encoding: 'utf-8' }).trim();
|
|
149
|
+
const prltDir = path.dirname(path.dirname(prltPath)); // Go up from bin/ to package root
|
|
150
|
+
const globalSeedPath = path.join(prltDir, 'lib', 'node_modules', '@proletariat', 'cli', 'scripts', 'seed-explore-data.mjs');
|
|
151
|
+
if (!seedPaths.includes(globalSeedPath)) {
|
|
152
|
+
seedPaths.push(globalSeedPath);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Can't find prlt path
|
|
157
|
+
}
|
|
158
|
+
for (const seedPath of seedPaths) {
|
|
159
|
+
if (fs.existsSync(seedPath)) {
|
|
160
|
+
this.log(styles.muted(` Running seed script: ${seedPath}`));
|
|
161
|
+
try {
|
|
162
|
+
execSync(`node "${seedPath}"`, {
|
|
163
|
+
cwd: hqPath,
|
|
164
|
+
stdio: 'inherit',
|
|
165
|
+
timeout: 60_000,
|
|
166
|
+
});
|
|
167
|
+
this.log(styles.success(' Test data seeded successfully'));
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
this.warn(`Seed script failed: ${error instanceof Error ? error.message : error}`);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// No seed script found - use prlt commands directly as fallback
|
|
177
|
+
this.log(styles.muted(' Seed script not found, creating minimal test data with prlt commands...'));
|
|
178
|
+
try {
|
|
179
|
+
execSync('prlt project create --name "QA Test Project" --description "Auto-generated project for QA testing" --json 2>/dev/null || true', {
|
|
180
|
+
cwd: hqPath,
|
|
181
|
+
encoding: 'utf-8',
|
|
182
|
+
timeout: 30_000,
|
|
183
|
+
});
|
|
184
|
+
execSync('prlt ticket create --title "Sample ticket for QA" --priority P2 --category feature --project "QA Test Project" --json 2>/dev/null || true', {
|
|
185
|
+
cwd: hqPath,
|
|
186
|
+
encoding: 'utf-8',
|
|
187
|
+
timeout: 30_000,
|
|
188
|
+
});
|
|
189
|
+
this.log(styles.success(' Minimal test data created'));
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
this.warn('Could not seed test data. QA agent will work with existing data.');
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Run in tracked mode - inside an HQ
|
|
199
|
+
* Creates an ephemeral QA agent with full tracking
|
|
200
|
+
*/
|
|
201
|
+
async runTracked(hqPath, workDir, flags, jsonMode) {
|
|
202
|
+
this.log('');
|
|
203
|
+
this.log(styles.header('🔍 Exploratory QA Agent'));
|
|
204
|
+
this.log(styles.muted(` HQ: ${hqPath}`));
|
|
205
|
+
this.log('');
|
|
206
|
+
// Get workspace info
|
|
207
|
+
let workspaceInfo;
|
|
208
|
+
try {
|
|
209
|
+
workspaceInfo = getWorkspaceInfo();
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
if (jsonMode) {
|
|
213
|
+
outputErrorAsJson('WORKSPACE_ERROR', 'Failed to get workspace info', createMetadata('qa', flags));
|
|
214
|
+
this.exit(1);
|
|
215
|
+
}
|
|
216
|
+
this.error('Failed to get workspace info');
|
|
217
|
+
}
|
|
218
|
+
// Open database
|
|
219
|
+
const dbPath = path.join(hqPath, '.proletariat', 'workspace.db');
|
|
220
|
+
const db = new Database(dbPath);
|
|
221
|
+
const executionStorage = new ExecutionStorage(db);
|
|
222
|
+
try {
|
|
223
|
+
// Seed data if requested
|
|
224
|
+
if (flags.seed) {
|
|
225
|
+
this.log(styles.muted(' Seeding test data...'));
|
|
226
|
+
await this.runSeedData(hqPath);
|
|
227
|
+
this.log('');
|
|
228
|
+
}
|
|
229
|
+
// Get PMO context for ticket creation
|
|
230
|
+
const { getPMOContext } = await import('../../lib/pmo/index.js');
|
|
231
|
+
let pmoPath;
|
|
232
|
+
let storage;
|
|
233
|
+
try {
|
|
234
|
+
const pmoContext = await getPMOContext();
|
|
235
|
+
pmoPath = pmoContext.pmoPath;
|
|
236
|
+
storage = pmoContext.storage;
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
if (jsonMode) {
|
|
240
|
+
outputErrorAsJson('PMO_NOT_FOUND', 'PMO not found. Run "prlt pmo init" first.', createMetadata('qa', flags));
|
|
241
|
+
this.exit(1);
|
|
242
|
+
}
|
|
243
|
+
this.error('PMO not found. Run "prlt pmo init" first.');
|
|
244
|
+
}
|
|
245
|
+
// Select project for filing bugs
|
|
246
|
+
const jsonModeConfig = jsonMode ? { flags: flags, commandName: 'qa' } : null;
|
|
247
|
+
const projects = await storage.listProjects();
|
|
248
|
+
let projectId;
|
|
249
|
+
if (projects.length === 0) {
|
|
250
|
+
db.close();
|
|
251
|
+
if (jsonMode) {
|
|
252
|
+
outputErrorAsJson('NO_PROJECTS', 'No projects found. Create a project first, or use --seed.', createMetadata('qa', flags));
|
|
253
|
+
this.exit(1);
|
|
254
|
+
}
|
|
255
|
+
this.error('No projects found. Create a project first, or use --seed to create test data.');
|
|
256
|
+
}
|
|
257
|
+
else if (projects.length === 1) {
|
|
258
|
+
projectId = projects[0].id;
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
const { selectedProject } = await this.prompt([
|
|
262
|
+
{
|
|
263
|
+
type: 'list',
|
|
264
|
+
name: 'selectedProject',
|
|
265
|
+
message: 'Select project for QA (bugs will be filed here):',
|
|
266
|
+
choices: projects.map((p) => ({
|
|
267
|
+
name: `${p.name} (${p.id})`,
|
|
268
|
+
value: p.id,
|
|
269
|
+
command: `prlt qa --json`,
|
|
270
|
+
})),
|
|
271
|
+
},
|
|
272
|
+
], jsonModeConfig);
|
|
273
|
+
if (jsonMode) {
|
|
274
|
+
db.close();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
projectId = selectedProject;
|
|
278
|
+
}
|
|
279
|
+
// Resolve environment
|
|
280
|
+
const environment = await this.resolveEnvironment(workDir, flags, jsonMode, jsonModeConfig);
|
|
281
|
+
if (jsonMode && !flags.environment) {
|
|
282
|
+
db.close();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// Resolve display mode (--watch forces foreground)
|
|
286
|
+
let displayMode;
|
|
287
|
+
if (flags.watch) {
|
|
288
|
+
displayMode = 'foreground';
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
displayMode = await this.resolveDisplayMode(flags, jsonMode, jsonModeConfig, environment);
|
|
292
|
+
if (jsonMode && !flags['display-mode'] && !flags.watch) {
|
|
293
|
+
db.close();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Resolve permission mode (default to danger for QA since it's in a container)
|
|
298
|
+
const sandboxed = await this.resolvePermissionMode(flags, jsonMode, jsonModeConfig, environment, displayMode);
|
|
299
|
+
if (jsonMode && !flags['permission-mode']) {
|
|
300
|
+
db.close();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
// Create an adhoc QA ticket
|
|
304
|
+
const ticket = await storage.createTicket(projectId, {
|
|
305
|
+
title: `QA: Exploratory testing session`,
|
|
306
|
+
description: 'Automated QA exploration session spawned by `prlt qa`',
|
|
307
|
+
category: 'test',
|
|
308
|
+
priority: 'P2',
|
|
309
|
+
});
|
|
310
|
+
this.log(styles.success(` Created QA ticket: ${ticket.id}`));
|
|
311
|
+
// Create ephemeral agent
|
|
312
|
+
this.log(styles.muted(' Creating ephemeral agent...'));
|
|
313
|
+
let ephemeralResult;
|
|
314
|
+
try {
|
|
315
|
+
ephemeralResult = await createEphemeralAgent(workspaceInfo, {
|
|
316
|
+
skipDevcontainer: environment === 'host',
|
|
317
|
+
log: (msg) => this.log(styles.muted(` ${msg}`)),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
catch (agentError) {
|
|
321
|
+
// Rollback ticket
|
|
322
|
+
this.log(styles.muted(' Rolling back ticket creation...'));
|
|
323
|
+
try {
|
|
324
|
+
await storage.deleteTicket(ticket.id);
|
|
325
|
+
}
|
|
326
|
+
catch { /* ignore */ }
|
|
327
|
+
throw agentError;
|
|
328
|
+
}
|
|
329
|
+
const agentName = ephemeralResult.name;
|
|
330
|
+
const agentDir = ephemeralResult.worktreePath;
|
|
331
|
+
this.log(styles.success(` Agent: ${agentName}`));
|
|
332
|
+
// Load explore-cli action prompt
|
|
333
|
+
const actionData = await this.getExploreCLIPrompt();
|
|
334
|
+
let actionPrompt = actionData.prompt;
|
|
335
|
+
if (flags.prompt) {
|
|
336
|
+
actionPrompt += `\n\n## Additional Instructions\n\n${flags.prompt}`;
|
|
337
|
+
}
|
|
338
|
+
// Build execution context
|
|
339
|
+
const context = {
|
|
340
|
+
ticketId: ticket.id,
|
|
341
|
+
ticketTitle: ticket.title,
|
|
342
|
+
ticketDescription: ticket.description,
|
|
343
|
+
agentName,
|
|
344
|
+
agentDir,
|
|
345
|
+
worktreePath: agentDir,
|
|
346
|
+
branch: 'main',
|
|
347
|
+
hqPath,
|
|
348
|
+
pmoPath,
|
|
349
|
+
actionId: 'explore-cli',
|
|
350
|
+
actionName: 'Explore-CLI',
|
|
351
|
+
actionPrompt,
|
|
352
|
+
actionEndPrompt: actionData.endPrompt,
|
|
353
|
+
modifiesCode: false,
|
|
354
|
+
};
|
|
355
|
+
// Create execution record
|
|
356
|
+
const execution = executionStorage.createExecution({
|
|
357
|
+
ticketId: ticket.id,
|
|
358
|
+
agentName,
|
|
359
|
+
executor: 'claude-code',
|
|
360
|
+
environment,
|
|
361
|
+
displayMode,
|
|
362
|
+
sandboxed,
|
|
363
|
+
branch: 'main',
|
|
364
|
+
});
|
|
365
|
+
// Update ticket assignee
|
|
366
|
+
await storage.updateTicket(ticket.id, { assignee: agentName });
|
|
367
|
+
// Load execution config
|
|
368
|
+
const executionConfig = loadExecutionConfig(db);
|
|
369
|
+
executionConfig.sandboxed = sandboxed;
|
|
370
|
+
executionConfig.outputMode = 'interactive';
|
|
371
|
+
// For terminal mode, ensure terminal preference is set
|
|
372
|
+
if (displayMode === 'terminal' && !jsonMode) {
|
|
373
|
+
if (!hasTerminalPreference(db)) {
|
|
374
|
+
executionConfig.terminal.app = await promptTerminalPreference(db);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
executionConfig.terminal.app = await getTerminalApp(db);
|
|
378
|
+
}
|
|
379
|
+
if (!hasShellPreference(db)) {
|
|
380
|
+
executionConfig.shell = await promptShellPreference(db);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
executionConfig.shell = await getShell(db);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// Show summary
|
|
387
|
+
this.log('');
|
|
388
|
+
this.log(styles.muted(` Ticket: ${ticket.id}`));
|
|
389
|
+
this.log(styles.muted(` Agent: ${agentName}`));
|
|
390
|
+
this.log(styles.muted(` Work ID: ${execution.id}`));
|
|
391
|
+
this.log(styles.muted(` Environment: ${environment === 'devcontainer' ? '🐳' : '💻'} ${environment}`));
|
|
392
|
+
this.log(styles.muted(` Display: ${displayMode}${flags.watch ? ' (watch mode)' : ''}`));
|
|
393
|
+
this.log(styles.muted(` Permissions: ${sandboxed ? '🔒 safe' : '⚠️ danger'}`));
|
|
394
|
+
this.log('');
|
|
395
|
+
// Run execution
|
|
396
|
+
this.log(styles.muted('Starting QA agent...'));
|
|
397
|
+
const result = await runExecution(environment, context, 'claude-code', executionConfig, {
|
|
398
|
+
displayMode,
|
|
399
|
+
sessionManager: environment === 'devcontainer' ? 'tmux' : undefined,
|
|
400
|
+
});
|
|
401
|
+
if (result.success) {
|
|
402
|
+
executionStorage.updateStatus(execution.id, 'running');
|
|
403
|
+
executionStorage.updateProcessInfo(execution.id, {
|
|
404
|
+
pid: result.pid,
|
|
405
|
+
containerId: result.containerId,
|
|
406
|
+
sessionId: result.sessionId,
|
|
407
|
+
logPath: result.logPath,
|
|
408
|
+
});
|
|
409
|
+
this.log('');
|
|
410
|
+
this.log(styles.success(`✓ QA session started (${execution.id})`));
|
|
411
|
+
this.log('');
|
|
412
|
+
this.log(styles.muted('Commands:'));
|
|
413
|
+
this.log(styles.muted(` prlt work status View work status`));
|
|
414
|
+
this.log(styles.muted(` prlt session attach Attach to session`));
|
|
415
|
+
this.log(styles.muted(` prlt work stop ${execution.id} Stop QA`));
|
|
416
|
+
if (!flags.watch && result.sessionId) {
|
|
417
|
+
this.log(styles.muted(` tmux attach -t "${result.sessionId}" Watch live`));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
executionStorage.updateStatus(execution.id, 'failed');
|
|
422
|
+
this.error(`Failed to start QA session: ${result.error}`);
|
|
423
|
+
}
|
|
424
|
+
db.close();
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
db.close();
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Run in yolo mode - outside any HQ
|
|
433
|
+
*/
|
|
434
|
+
async runYolo(workDir, flags, jsonMode) {
|
|
435
|
+
this.log('');
|
|
436
|
+
this.log(styles.header('🔍 Exploratory QA Agent (Yolo Mode)'));
|
|
437
|
+
this.log(styles.muted(' No HQ detected - running without tracking'));
|
|
438
|
+
this.log('');
|
|
439
|
+
if (flags.seed) {
|
|
440
|
+
this.log(styles.warning(' --seed requires an HQ. Skipping seed step.'));
|
|
441
|
+
this.log(styles.muted(' Run "prlt init" first to set up an HQ, then use --seed.'));
|
|
442
|
+
this.log('');
|
|
443
|
+
}
|
|
444
|
+
const jsonModeConfig = jsonMode ? { flags: flags, commandName: 'qa' } : null;
|
|
445
|
+
// Resolve environment
|
|
446
|
+
const environment = await this.resolveEnvironment(workDir, flags, jsonMode, jsonModeConfig);
|
|
447
|
+
if (jsonMode && !flags.environment)
|
|
448
|
+
return;
|
|
449
|
+
// Resolve display mode (--watch forces foreground)
|
|
450
|
+
let displayMode;
|
|
451
|
+
if (flags.watch) {
|
|
452
|
+
displayMode = 'foreground';
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
displayMode = await this.resolveDisplayMode(flags, jsonMode, jsonModeConfig, environment);
|
|
456
|
+
if (jsonMode && !flags['display-mode'] && !flags.watch)
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
// Resolve permission mode
|
|
460
|
+
const sandboxed = await this.resolvePermissionMode(flags, jsonMode, jsonModeConfig, environment, displayMode);
|
|
461
|
+
if (jsonMode && !flags['permission-mode'])
|
|
462
|
+
return;
|
|
463
|
+
const sessionName = `qa-explore-${Date.now().toString(36)}`;
|
|
464
|
+
// Load explore-cli prompt
|
|
465
|
+
const actionData = await this.getExploreCLIPrompt();
|
|
466
|
+
let actionPrompt = actionData.prompt;
|
|
467
|
+
if (flags.prompt) {
|
|
468
|
+
actionPrompt += `\n\n## Additional Instructions\n\n${flags.prompt}`;
|
|
469
|
+
}
|
|
470
|
+
// Prepare devcontainer if needed
|
|
471
|
+
const hasProjectDevcontainer = hasDevcontainerConfig(workDir);
|
|
472
|
+
let devcontainerConfigDir = workDir;
|
|
473
|
+
let cleanupDevcontainer;
|
|
474
|
+
if (environment === 'devcontainer' && !hasProjectDevcontainer) {
|
|
475
|
+
const imageCheck = await this.checkCatchallImage();
|
|
476
|
+
if (!imageCheck.available) {
|
|
477
|
+
if (jsonMode) {
|
|
478
|
+
outputErrorAsJson('CONTAINER_IMAGE_UNAVAILABLE', imageCheck.error, createMetadata('qa', flags));
|
|
479
|
+
this.exit(1);
|
|
480
|
+
}
|
|
481
|
+
this.error(imageCheck.error);
|
|
482
|
+
}
|
|
483
|
+
this.log(styles.muted(` Using catch-all devcontainer: ${CATCHALL_DEVCONTAINER_IMAGE}`));
|
|
484
|
+
const devcontainerSetup = await this.setupCatchallDevcontainer(workDir, 'qa-explore');
|
|
485
|
+
devcontainerConfigDir = devcontainerSetup.configDir;
|
|
486
|
+
cleanupDevcontainer = devcontainerSetup.cleanup;
|
|
487
|
+
}
|
|
488
|
+
// Build execution context
|
|
489
|
+
const context = {
|
|
490
|
+
ticketId: 'QA',
|
|
491
|
+
ticketTitle: 'Exploratory QA Session',
|
|
492
|
+
agentName: 'qa-agent',
|
|
493
|
+
agentDir: workDir,
|
|
494
|
+
worktreePath: devcontainerConfigDir,
|
|
495
|
+
branch: 'main',
|
|
496
|
+
actionId: 'explore-cli',
|
|
497
|
+
actionName: 'Explore-CLI',
|
|
498
|
+
actionPrompt,
|
|
499
|
+
actionEndPrompt: actionData.endPrompt,
|
|
500
|
+
modifiesCode: false,
|
|
501
|
+
};
|
|
502
|
+
// Load execution config
|
|
503
|
+
const executionConfig = { ...DEFAULT_EXECUTION_CONFIG };
|
|
504
|
+
executionConfig.sandboxed = sandboxed;
|
|
505
|
+
executionConfig.outputMode = 'interactive';
|
|
506
|
+
// For terminal mode, prompt for terminal preference
|
|
507
|
+
if (displayMode === 'terminal' && !jsonMode) {
|
|
508
|
+
const homePrltDir = path.join(process.env.HOME || '', '.proletariat');
|
|
509
|
+
fs.mkdirSync(homePrltDir, { recursive: true });
|
|
510
|
+
const tempDbPath = path.join(homePrltDir, 'adhoc.db');
|
|
511
|
+
const tempDb = new Database(tempDbPath);
|
|
512
|
+
tempDb.exec(`
|
|
513
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
514
|
+
key TEXT PRIMARY KEY,
|
|
515
|
+
value TEXT NOT NULL
|
|
516
|
+
)
|
|
517
|
+
`);
|
|
518
|
+
if (!hasTerminalPreference(tempDb)) {
|
|
519
|
+
executionConfig.terminal.app = await promptTerminalPreference(tempDb);
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
executionConfig.terminal.app = await getTerminalApp(tempDb);
|
|
523
|
+
}
|
|
524
|
+
if (!hasShellPreference(tempDb)) {
|
|
525
|
+
executionConfig.shell = await promptShellPreference(tempDb);
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
executionConfig.shell = await getShell(tempDb);
|
|
529
|
+
}
|
|
530
|
+
tempDb.close();
|
|
531
|
+
}
|
|
532
|
+
// Show summary
|
|
533
|
+
this.log('');
|
|
534
|
+
this.log(styles.muted(` Session: ${sessionName}`));
|
|
535
|
+
this.log(styles.muted(` Directory: ${workDir}`));
|
|
536
|
+
this.log(styles.muted(` Environment: ${environment === 'devcontainer' ? '🐳' : '💻'} ${environment}`));
|
|
537
|
+
this.log(styles.muted(` Display: ${displayMode}${flags.watch ? ' (watch mode)' : ''}`));
|
|
538
|
+
this.log(styles.muted(` Permissions: ${sandboxed ? '🔒 safe' : '⚠️ danger'}`));
|
|
539
|
+
this.log('');
|
|
540
|
+
// Run execution
|
|
541
|
+
this.log(styles.muted('Starting QA agent...'));
|
|
542
|
+
try {
|
|
543
|
+
const result = await runExecution(environment, context, 'claude-code', executionConfig, {
|
|
544
|
+
displayMode,
|
|
545
|
+
sessionManager: environment === 'devcontainer' ? 'tmux' : undefined,
|
|
546
|
+
});
|
|
547
|
+
if (result.success) {
|
|
548
|
+
this.log('');
|
|
549
|
+
this.log(styles.success(`✓ QA session started: ${sessionName}`));
|
|
550
|
+
if (displayMode === 'background' && result.sessionId) {
|
|
551
|
+
this.log(styles.muted(` Watch live: tmux attach -t "${result.sessionId}"`));
|
|
552
|
+
}
|
|
553
|
+
if (cleanupDevcontainer)
|
|
554
|
+
cleanupDevcontainer();
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
if (cleanupDevcontainer)
|
|
558
|
+
cleanupDevcontainer();
|
|
559
|
+
this.error(`Failed to start QA session: ${result.error}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
catch (error) {
|
|
563
|
+
if (cleanupDevcontainer)
|
|
564
|
+
cleanupDevcontainer();
|
|
565
|
+
throw error;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// ─── Shared helpers ─────────────────────────────────────────────────
|
|
569
|
+
/**
|
|
570
|
+
* Resolve execution environment (devcontainer vs host).
|
|
571
|
+
*/
|
|
572
|
+
async resolveEnvironment(workDir, flags, jsonMode, jsonModeConfig) {
|
|
573
|
+
if (flags.environment) {
|
|
574
|
+
return flags.environment;
|
|
575
|
+
}
|
|
576
|
+
const hasProjectDevcontainer = hasDevcontainerConfig(workDir);
|
|
577
|
+
const devcontainerLabel = hasProjectDevcontainer
|
|
578
|
+
? '🐳 devcontainer (uses project config, sandboxed)'
|
|
579
|
+
: '🐳 devcontainer (uses catch-all container, sandboxed)';
|
|
580
|
+
if (jsonMode) {
|
|
581
|
+
await this.prompt([
|
|
582
|
+
{
|
|
583
|
+
type: 'list',
|
|
584
|
+
name: 'selectedEnv',
|
|
585
|
+
message: 'Where should the QA agent run?',
|
|
586
|
+
choices: [
|
|
587
|
+
{ name: devcontainerLabel, value: 'devcontainer', command: `prlt qa --environment devcontainer --json` },
|
|
588
|
+
{ name: '💻 host (runs directly on your machine)', value: 'host', command: `prlt qa --environment host --json` },
|
|
589
|
+
],
|
|
590
|
+
default: 'devcontainer',
|
|
591
|
+
},
|
|
592
|
+
], jsonModeConfig);
|
|
593
|
+
return 'host'; // unreachable after JSON output
|
|
594
|
+
}
|
|
595
|
+
// Interactive: loop to handle Docker not running
|
|
596
|
+
while (true) {
|
|
597
|
+
// eslint-disable-next-line no-await-in-loop -- Interactive user prompt in loop
|
|
598
|
+
const { selectedEnv } = await this.prompt([
|
|
599
|
+
{
|
|
600
|
+
type: 'list',
|
|
601
|
+
name: 'selectedEnv',
|
|
602
|
+
message: 'Where should the QA agent run?',
|
|
603
|
+
choices: [
|
|
604
|
+
{ name: devcontainerLabel, value: 'devcontainer' },
|
|
605
|
+
{ name: '💻 host (runs directly on your machine)', value: 'host' },
|
|
606
|
+
],
|
|
607
|
+
default: 'devcontainer',
|
|
608
|
+
},
|
|
609
|
+
], null);
|
|
610
|
+
if (selectedEnv === 'devcontainer') {
|
|
611
|
+
if (!isDockerRunning()) {
|
|
612
|
+
this.log('');
|
|
613
|
+
this.warn('Docker is not running. Please start Docker and try again.');
|
|
614
|
+
this.log('');
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
if (!isDevcontainerCliInstalled()) {
|
|
618
|
+
this.log('');
|
|
619
|
+
this.warn('devcontainer CLI is not installed.\n' +
|
|
620
|
+
'Install with: npm install -g @devcontainers/cli\n' +
|
|
621
|
+
'Or select "host" to run directly on your machine.');
|
|
622
|
+
this.log('');
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (!isGitHubTokenAvailable()) {
|
|
626
|
+
this.log('');
|
|
627
|
+
this.warn('GitHub token not found. Git push may fail inside the container.');
|
|
628
|
+
this.log('');
|
|
629
|
+
// eslint-disable-next-line no-await-in-loop -- Interactive user prompt in loop
|
|
630
|
+
const { tokenAction } = await this.prompt([
|
|
631
|
+
{
|
|
632
|
+
type: 'list',
|
|
633
|
+
name: 'tokenAction',
|
|
634
|
+
message: 'Continue without GitHub token?',
|
|
635
|
+
choices: [
|
|
636
|
+
{ name: 'Yes, continue anyway', value: 'continue' },
|
|
637
|
+
{ name: 'No, let me run gh auth login first', value: 'cancel' },
|
|
638
|
+
{ name: 'Switch to host mode instead', value: 'host' },
|
|
639
|
+
],
|
|
640
|
+
default: 'continue',
|
|
641
|
+
},
|
|
642
|
+
], null);
|
|
643
|
+
if (tokenAction === 'cancel') {
|
|
644
|
+
this.log(styles.muted('Run `gh auth login` and try again.'));
|
|
645
|
+
this.exit(0);
|
|
646
|
+
}
|
|
647
|
+
if (tokenAction === 'host')
|
|
648
|
+
return 'host';
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return selectedEnv;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Resolve display mode.
|
|
656
|
+
*/
|
|
657
|
+
async resolveDisplayMode(flags, jsonMode, jsonModeConfig, environment) {
|
|
658
|
+
if (flags['display-mode']) {
|
|
659
|
+
return flags['display-mode'];
|
|
660
|
+
}
|
|
661
|
+
const { selectedDisplay } = await this.prompt([
|
|
662
|
+
{
|
|
663
|
+
type: 'list',
|
|
664
|
+
name: 'selectedDisplay',
|
|
665
|
+
message: 'How should output be displayed?',
|
|
666
|
+
choices: [
|
|
667
|
+
{ name: '▶️ Foreground - Run in current terminal, watch live (recommended for QA)', value: 'foreground', command: `prlt qa --environment ${environment} --display-mode foreground --json` },
|
|
668
|
+
{ name: '🖥️ New tab - Opens in new terminal tab', value: 'terminal', command: `prlt qa --environment ${environment} --display-mode terminal --json` },
|
|
669
|
+
{ name: '📦 Background - Runs detached, reattach later', value: 'background', command: `prlt qa --environment ${environment} --display-mode background --json` },
|
|
670
|
+
],
|
|
671
|
+
default: 'foreground',
|
|
672
|
+
},
|
|
673
|
+
], jsonModeConfig);
|
|
674
|
+
if (jsonMode)
|
|
675
|
+
return 'foreground'; // unreachable
|
|
676
|
+
return selectedDisplay;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Resolve permission mode. Defaults to danger for QA (container provides isolation).
|
|
680
|
+
*/
|
|
681
|
+
async resolvePermissionMode(flags, jsonMode, jsonModeConfig, environment, displayMode) {
|
|
682
|
+
if (flags['permission-mode']) {
|
|
683
|
+
return flags['permission-mode'] === 'safe';
|
|
684
|
+
}
|
|
685
|
+
const containerNote = environment === 'devcontainer' ? ' (container provides isolation)' : '';
|
|
686
|
+
const { permissionMode } = await this.prompt([
|
|
687
|
+
{
|
|
688
|
+
type: 'list',
|
|
689
|
+
name: 'permissionMode',
|
|
690
|
+
message: `Permission mode${containerNote}:`,
|
|
691
|
+
choices: [
|
|
692
|
+
{ name: '⚠️ danger - Skip permission checks (faster, recommended for QA)', value: 'danger', command: `prlt qa --environment ${environment} --display-mode ${displayMode} --permission-mode danger --json` },
|
|
693
|
+
{ name: '🔒 safe - Requires approval for dangerous operations', value: 'safe', command: `prlt qa --environment ${environment} --display-mode ${displayMode} --permission-mode safe --json` },
|
|
694
|
+
],
|
|
695
|
+
default: 'danger',
|
|
696
|
+
},
|
|
697
|
+
], jsonModeConfig);
|
|
698
|
+
if (jsonMode)
|
|
699
|
+
return true; // unreachable
|
|
700
|
+
return permissionMode === 'safe';
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Set up catch-all devcontainer for directories without one.
|
|
704
|
+
*/
|
|
705
|
+
async setupCatchallDevcontainer(workDir, slug) {
|
|
706
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'prlt-qa-'));
|
|
707
|
+
const devcontainerDir = path.join(tempDir, '.devcontainer');
|
|
708
|
+
fs.mkdirSync(devcontainerDir, { recursive: true });
|
|
709
|
+
const devcontainerJson = {
|
|
710
|
+
name: `qa-${slug}`,
|
|
711
|
+
image: CATCHALL_DEVCONTAINER_IMAGE,
|
|
712
|
+
customizations: {
|
|
713
|
+
vscode: {
|
|
714
|
+
extensions: ['anthropic.claude-code'],
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
remoteUser: 'node',
|
|
718
|
+
workspaceFolder: '/workspace',
|
|
719
|
+
mounts: [
|
|
720
|
+
`source=${workDir},target=/workspace,type=bind`,
|
|
721
|
+
],
|
|
722
|
+
containerEnv: {
|
|
723
|
+
ANTHROPIC_API_KEY: '${localEnv:ANTHROPIC_API_KEY}',
|
|
724
|
+
},
|
|
725
|
+
};
|
|
726
|
+
fs.writeFileSync(path.join(devcontainerDir, 'devcontainer.json'), JSON.stringify(devcontainerJson, null, 2));
|
|
727
|
+
this.log(styles.muted(' Created temporary .devcontainer config'));
|
|
728
|
+
const cleanup = () => {
|
|
729
|
+
try {
|
|
730
|
+
if (fs.existsSync(tempDir)) {
|
|
731
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
// Ignore cleanup errors
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
return { configDir: tempDir, workDir, cleanup };
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Check if catch-all container image is available.
|
|
742
|
+
*/
|
|
743
|
+
async checkCatchallImage() {
|
|
744
|
+
try {
|
|
745
|
+
execSync(`docker image inspect ${CATCHALL_DEVCONTAINER_IMAGE}`, { stdio: 'pipe' });
|
|
746
|
+
return { available: true };
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
this.log(styles.muted(` Pulling container image: ${CATCHALL_DEVCONTAINER_IMAGE}`));
|
|
750
|
+
try {
|
|
751
|
+
execSync(`docker pull ${CATCHALL_DEVCONTAINER_IMAGE}`, { stdio: 'pipe', timeout: 120000 });
|
|
752
|
+
return { available: true };
|
|
753
|
+
}
|
|
754
|
+
catch {
|
|
755
|
+
return {
|
|
756
|
+
available: false,
|
|
757
|
+
error: `Failed to pull catch-all container image: ${CATCHALL_DEVCONTAINER_IMAGE}. Try running on host instead.`,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|