@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
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
import { Command, 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 inquirer from 'inquirer';
|
|
7
|
+
import Database from 'better-sqlite3';
|
|
8
|
+
import { findHQRoot } from '../lib/workspace.js';
|
|
9
|
+
import { getWorkspaceInfo, createEphemeralAgent, } from '../lib/agents/commands.js';
|
|
10
|
+
import { shouldOutputJson, outputPromptAsJson, outputErrorAsJson, createMetadata, buildPromptConfig, } from '../lib/prompt-json.js';
|
|
11
|
+
import { styles } from '../lib/styles.js';
|
|
12
|
+
import { DEFAULT_EXECUTION_CONFIG, } from '../lib/execution/types.js';
|
|
13
|
+
import { runExecution, isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled } from '../lib/execution/runners.js';
|
|
14
|
+
import { ExecutionStorage } from '../lib/execution/storage.js';
|
|
15
|
+
import { loadExecutionConfig, getTerminalApp, promptTerminalPreference, getShell, promptShellPreference, hasTerminalPreference, hasShellPreference, } from '../lib/execution/config.js';
|
|
16
|
+
import { hasDevcontainerConfig } from '../lib/execution/devcontainer.js';
|
|
17
|
+
// Catch-all devcontainer image for directories without .devcontainer
|
|
18
|
+
const CATCHALL_DEVCONTAINER_IMAGE = 'ghcr.io/chrismcdermut/proletariat-claude:latest';
|
|
19
|
+
/**
|
|
20
|
+
* Check for uncommitted changes in git repo
|
|
21
|
+
*/
|
|
22
|
+
function hasUncommittedChanges(dir) {
|
|
23
|
+
try {
|
|
24
|
+
const status = execSync('git status --porcelain', { cwd: dir, encoding: 'utf-8' });
|
|
25
|
+
return status.trim().length > 0;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Check if directory is a git repo
|
|
33
|
+
*/
|
|
34
|
+
function isGitRepo(dir) {
|
|
35
|
+
try {
|
|
36
|
+
execSync('git rev-parse --git-dir', { cwd: dir, stdio: 'pipe' });
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export default class Claude extends Command {
|
|
44
|
+
static description = 'Quick launch Claude Code for ad-hoc sessions (works anywhere)';
|
|
45
|
+
static examples = [
|
|
46
|
+
'<%= config.bin %> <%= command.id %>',
|
|
47
|
+
'<%= config.bin %> <%= command.id %> --slug debug-auth --permission-mode danger',
|
|
48
|
+
'<%= config.bin %> <%= command.id %> --environment devcontainer --display-mode background',
|
|
49
|
+
'<%= config.bin %> <%= command.id %> --prompt "help me debug this function"',
|
|
50
|
+
];
|
|
51
|
+
static flags = {
|
|
52
|
+
json: Flags.boolean({
|
|
53
|
+
description: 'Output prompt configuration as JSON (for AI agents/scripts)',
|
|
54
|
+
default: false,
|
|
55
|
+
}),
|
|
56
|
+
slug: Flags.string({
|
|
57
|
+
char: 's',
|
|
58
|
+
description: 'Session name for pane/tab title',
|
|
59
|
+
}),
|
|
60
|
+
'permission-mode': Flags.string({
|
|
61
|
+
char: 'p',
|
|
62
|
+
description: 'Permission mode (danger: skip prompts, safe: require approval)',
|
|
63
|
+
options: ['danger', 'safe'],
|
|
64
|
+
}),
|
|
65
|
+
environment: Flags.string({
|
|
66
|
+
char: 'e',
|
|
67
|
+
description: 'Where to run (devcontainer or host)',
|
|
68
|
+
options: ['devcontainer', 'host'],
|
|
69
|
+
}),
|
|
70
|
+
'display-mode': Flags.string({
|
|
71
|
+
char: 'd',
|
|
72
|
+
description: 'How to display output',
|
|
73
|
+
options: ['terminal', 'background', 'foreground'],
|
|
74
|
+
}),
|
|
75
|
+
prompt: Flags.string({
|
|
76
|
+
description: 'Initial task/prompt for Claude',
|
|
77
|
+
}),
|
|
78
|
+
directory: Flags.string({
|
|
79
|
+
description: 'Directory to run in (default: cwd)',
|
|
80
|
+
}),
|
|
81
|
+
// Inside HQ only flags
|
|
82
|
+
project: Flags.string({
|
|
83
|
+
char: 'P',
|
|
84
|
+
description: 'Project for adhoc ticket (inside HQ only)',
|
|
85
|
+
}),
|
|
86
|
+
title: Flags.string({
|
|
87
|
+
char: 't',
|
|
88
|
+
description: 'Ticket title (inside HQ only)',
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
async run() {
|
|
92
|
+
const { flags } = await this.parse(Claude);
|
|
93
|
+
const jsonMode = shouldOutputJson(flags);
|
|
94
|
+
// Determine working directory
|
|
95
|
+
const workDir = flags.directory ? path.resolve(flags.directory) : process.cwd();
|
|
96
|
+
if (!fs.existsSync(workDir)) {
|
|
97
|
+
if (jsonMode) {
|
|
98
|
+
outputErrorAsJson('DIRECTORY_NOT_FOUND', `Directory not found: ${workDir}`, createMetadata('claude', flags));
|
|
99
|
+
this.exit(1);
|
|
100
|
+
}
|
|
101
|
+
this.error(`Directory not found: ${workDir}`);
|
|
102
|
+
}
|
|
103
|
+
// Check if we're inside an HQ
|
|
104
|
+
const hqPath = findHQRoot(workDir);
|
|
105
|
+
if (hqPath) {
|
|
106
|
+
await this.runInsideHQ(hqPath, workDir, flags, jsonMode);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
await this.runOutsideHQ(workDir, flags, jsonMode);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Run in "yolo mode" - outside any HQ
|
|
114
|
+
* No ticket creation, no tracking, just launch Claude
|
|
115
|
+
*/
|
|
116
|
+
async runOutsideHQ(workDir, flags, jsonMode) {
|
|
117
|
+
this.log('');
|
|
118
|
+
this.log(styles.header('🚀 Ad-hoc Claude Session (Yolo Mode)'));
|
|
119
|
+
this.log(styles.muted(' No HQ detected - running without tracking'));
|
|
120
|
+
this.log('');
|
|
121
|
+
// Prompt for slug (session name)
|
|
122
|
+
let slug = flags.slug;
|
|
123
|
+
if (!slug) {
|
|
124
|
+
if (jsonMode) {
|
|
125
|
+
outputPromptAsJson({
|
|
126
|
+
type: 'input',
|
|
127
|
+
name: 'slug',
|
|
128
|
+
message: 'Session name (for tab/pane title):',
|
|
129
|
+
}, createMetadata('claude', flags));
|
|
130
|
+
}
|
|
131
|
+
const { inputSlug } = await inquirer.prompt([
|
|
132
|
+
{
|
|
133
|
+
type: 'input',
|
|
134
|
+
name: 'inputSlug',
|
|
135
|
+
message: 'Session name (for tab/pane title):',
|
|
136
|
+
default: path.basename(workDir),
|
|
137
|
+
validate: (input) => input.trim() ? true : 'Session name required',
|
|
138
|
+
},
|
|
139
|
+
]);
|
|
140
|
+
slug = inputSlug.trim();
|
|
141
|
+
}
|
|
142
|
+
// Determine if devcontainer is available
|
|
143
|
+
const hasProjectDevcontainer = hasDevcontainerConfig(workDir);
|
|
144
|
+
// Prompt for environment
|
|
145
|
+
let environment = 'host';
|
|
146
|
+
if (flags.environment) {
|
|
147
|
+
environment = flags.environment;
|
|
148
|
+
}
|
|
149
|
+
else if (!jsonMode) {
|
|
150
|
+
// Check devcontainer prerequisites upfront
|
|
151
|
+
const dockerRunning = isDockerRunning();
|
|
152
|
+
const devcontainerCliInstalled = isDevcontainerCliInstalled();
|
|
153
|
+
const devcontainerReady = dockerRunning && devcontainerCliInstalled;
|
|
154
|
+
// Build devcontainer label with missing requirements
|
|
155
|
+
let devcontainerLabel = hasProjectDevcontainer
|
|
156
|
+
? '🐳 devcontainer (uses project config, sandboxed)'
|
|
157
|
+
: '🐳 devcontainer (uses catch-all container, sandboxed)';
|
|
158
|
+
if (!devcontainerReady) {
|
|
159
|
+
const missing = [];
|
|
160
|
+
if (!dockerRunning)
|
|
161
|
+
missing.push('Docker');
|
|
162
|
+
if (!devcontainerCliInstalled)
|
|
163
|
+
missing.push('devcontainer CLI');
|
|
164
|
+
devcontainerLabel = `🐳 devcontainer (requires: ${missing.join(', ')})`;
|
|
165
|
+
}
|
|
166
|
+
// Loop to handle Docker not running
|
|
167
|
+
let environmentSelected = false;
|
|
168
|
+
while (!environmentSelected) {
|
|
169
|
+
// eslint-disable-next-line no-await-in-loop -- Interactive user prompt in loop
|
|
170
|
+
const { selectedEnv } = await inquirer.prompt([
|
|
171
|
+
{
|
|
172
|
+
type: 'list',
|
|
173
|
+
name: 'selectedEnv',
|
|
174
|
+
message: 'Where should Claude run?',
|
|
175
|
+
choices: [
|
|
176
|
+
{
|
|
177
|
+
name: devcontainerLabel,
|
|
178
|
+
value: 'devcontainer',
|
|
179
|
+
disabled: !devcontainerReady,
|
|
180
|
+
},
|
|
181
|
+
{ name: '💻 host (runs directly on your machine)', value: 'host' },
|
|
182
|
+
],
|
|
183
|
+
default: devcontainerReady ? 'devcontainer' : 'host',
|
|
184
|
+
},
|
|
185
|
+
]);
|
|
186
|
+
if (selectedEnv === 'devcontainer') {
|
|
187
|
+
// Double-check prerequisites (in case user retried after starting Docker)
|
|
188
|
+
if (!isDockerRunning()) {
|
|
189
|
+
this.log('');
|
|
190
|
+
this.warn('Docker is not running. Please start Docker Desktop or select "host".');
|
|
191
|
+
this.log('');
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (!isDevcontainerCliInstalled()) {
|
|
195
|
+
this.log('');
|
|
196
|
+
this.warn('devcontainer CLI is not installed.\n' +
|
|
197
|
+
'Install with: npm install -g @devcontainers/cli\n' +
|
|
198
|
+
'Or select "host" to run directly on your machine.');
|
|
199
|
+
this.log('');
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
// Check GitHub token is available for git push operations
|
|
203
|
+
if (!isGitHubTokenAvailable()) {
|
|
204
|
+
const tokenChoices = [
|
|
205
|
+
{ name: 'Yes, continue anyway (git push may fail)', value: 'continue' },
|
|
206
|
+
{ name: 'No, let me run gh auth login first', value: 'cancel' },
|
|
207
|
+
{ name: 'Switch to host mode instead', value: 'host' },
|
|
208
|
+
];
|
|
209
|
+
const tokenMessage = 'GitHub token not found. Git push may fail. Continue without token?';
|
|
210
|
+
if (jsonMode) {
|
|
211
|
+
outputPromptAsJson(buildPromptConfig('list', 'tokenAction', tokenMessage, tokenChoices), createMetadata('claude', flags));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
this.log('');
|
|
215
|
+
this.warn('GitHub token not found.\n' +
|
|
216
|
+
'Git push operations may fail inside the container.\n' +
|
|
217
|
+
'Run `gh auth login` to authenticate, or continue without token.');
|
|
218
|
+
this.log('');
|
|
219
|
+
// eslint-disable-next-line no-await-in-loop -- Interactive user prompt in loop
|
|
220
|
+
const { tokenAction } = await inquirer.prompt([
|
|
221
|
+
{
|
|
222
|
+
type: 'list',
|
|
223
|
+
name: 'tokenAction',
|
|
224
|
+
message: tokenMessage,
|
|
225
|
+
choices: tokenChoices,
|
|
226
|
+
default: 'continue',
|
|
227
|
+
},
|
|
228
|
+
]);
|
|
229
|
+
if (tokenAction === 'cancel') {
|
|
230
|
+
this.log(styles.muted('Run `gh auth login` and try again.'));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (tokenAction === 'host') {
|
|
234
|
+
environment = 'host';
|
|
235
|
+
environmentSelected = true;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
// tokenAction === 'continue' - fall through to devcontainer setup
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
environment = selectedEnv;
|
|
242
|
+
environmentSelected = true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Prompt for display mode
|
|
246
|
+
let displayMode = 'terminal';
|
|
247
|
+
if (flags['display-mode']) {
|
|
248
|
+
displayMode = flags['display-mode'];
|
|
249
|
+
}
|
|
250
|
+
else if (!jsonMode) {
|
|
251
|
+
const { selectedDisplay } = await inquirer.prompt([
|
|
252
|
+
{
|
|
253
|
+
type: 'list',
|
|
254
|
+
name: 'selectedDisplay',
|
|
255
|
+
message: 'How should output be displayed?',
|
|
256
|
+
choices: [
|
|
257
|
+
{ name: '🖥️ New tab - Opens in new terminal tab (recommended)', value: 'terminal' },
|
|
258
|
+
{ name: '▶️ Foreground - Run in current terminal (blocking)', value: 'foreground' },
|
|
259
|
+
{ name: '📦 Background - Runs detached, reattach later', value: 'background' },
|
|
260
|
+
],
|
|
261
|
+
default: 'terminal',
|
|
262
|
+
},
|
|
263
|
+
]);
|
|
264
|
+
displayMode = selectedDisplay;
|
|
265
|
+
}
|
|
266
|
+
// Prompt for permission mode
|
|
267
|
+
let sandboxed = true;
|
|
268
|
+
if (flags['permission-mode']) {
|
|
269
|
+
sandboxed = flags['permission-mode'] === 'safe';
|
|
270
|
+
}
|
|
271
|
+
else if (!jsonMode) {
|
|
272
|
+
const { permissionMode } = await inquirer.prompt([
|
|
273
|
+
{
|
|
274
|
+
type: 'list',
|
|
275
|
+
name: 'permissionMode',
|
|
276
|
+
message: 'Permission mode:',
|
|
277
|
+
choices: [
|
|
278
|
+
{ name: '⚠️ danger - Skip permission checks (faster)', value: 'danger' },
|
|
279
|
+
{ name: '🔒 safe - Requires approval for dangerous operations', value: 'safe' },
|
|
280
|
+
],
|
|
281
|
+
default: 'danger',
|
|
282
|
+
},
|
|
283
|
+
]);
|
|
284
|
+
sandboxed = permissionMode === 'safe';
|
|
285
|
+
}
|
|
286
|
+
// Warn about uncommitted changes in danger mode
|
|
287
|
+
if (!sandboxed && isGitRepo(workDir) && hasUncommittedChanges(workDir)) {
|
|
288
|
+
this.log('');
|
|
289
|
+
this.warn('Running in danger mode with uncommitted changes!');
|
|
290
|
+
this.log(styles.muted(' Consider committing or stashing changes first.'));
|
|
291
|
+
this.log('');
|
|
292
|
+
if (!jsonMode) {
|
|
293
|
+
const { proceed } = await inquirer.prompt([
|
|
294
|
+
{
|
|
295
|
+
type: 'list',
|
|
296
|
+
name: 'proceed',
|
|
297
|
+
message: 'Continue anyway?',
|
|
298
|
+
choices: [
|
|
299
|
+
{ name: 'Yes, proceed', value: true },
|
|
300
|
+
{ name: 'No, cancel', value: false },
|
|
301
|
+
],
|
|
302
|
+
default: true,
|
|
303
|
+
},
|
|
304
|
+
]);
|
|
305
|
+
if (!proceed) {
|
|
306
|
+
this.log(styles.muted('Cancelled.'));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// Build session name
|
|
312
|
+
const sessionName = `adhoc-${slug}`;
|
|
313
|
+
// Prepare devcontainer if needed and not present
|
|
314
|
+
let devcontainerConfigDir = workDir;
|
|
315
|
+
let cleanupDevcontainer;
|
|
316
|
+
if (environment === 'devcontainer' && !hasProjectDevcontainer) {
|
|
317
|
+
// Check if catch-all image is available
|
|
318
|
+
const imageCheck = await this.checkCatchallImage();
|
|
319
|
+
if (!imageCheck.available) {
|
|
320
|
+
if (jsonMode) {
|
|
321
|
+
outputErrorAsJson('CONTAINER_IMAGE_UNAVAILABLE', imageCheck.error, createMetadata('claude', flags));
|
|
322
|
+
this.exit(1);
|
|
323
|
+
}
|
|
324
|
+
this.error(imageCheck.error);
|
|
325
|
+
}
|
|
326
|
+
// Create temporary devcontainer config using catch-all image
|
|
327
|
+
this.log(styles.muted(` Using catch-all devcontainer: ${CATCHALL_DEVCONTAINER_IMAGE}`));
|
|
328
|
+
const devcontainerSetup = await this.setupCatchallDevcontainer(workDir, slug);
|
|
329
|
+
devcontainerConfigDir = devcontainerSetup.configDir;
|
|
330
|
+
cleanupDevcontainer = devcontainerSetup.cleanup;
|
|
331
|
+
}
|
|
332
|
+
// Build minimal execution context for yolo mode
|
|
333
|
+
// Use devcontainerConfigDir for worktreePath so runner finds .devcontainer there
|
|
334
|
+
const context = {
|
|
335
|
+
ticketId: 'ADHOC',
|
|
336
|
+
ticketTitle: `Ad-hoc: ${slug}`,
|
|
337
|
+
agentName: 'adhoc',
|
|
338
|
+
agentDir: workDir,
|
|
339
|
+
worktreePath: devcontainerConfigDir, // Runner looks here for .devcontainer
|
|
340
|
+
branch: 'adhoc',
|
|
341
|
+
actionName: slug,
|
|
342
|
+
actionPrompt: flags.prompt,
|
|
343
|
+
modifiesCode: false, // Don't try to create branches
|
|
344
|
+
};
|
|
345
|
+
// Load execution config (use defaults for yolo mode)
|
|
346
|
+
const executionConfig = { ...DEFAULT_EXECUTION_CONFIG };
|
|
347
|
+
executionConfig.sandboxed = sandboxed;
|
|
348
|
+
executionConfig.outputMode = 'interactive';
|
|
349
|
+
// For terminal mode, prompt for terminal preference
|
|
350
|
+
if (displayMode === 'terminal' && !jsonMode) {
|
|
351
|
+
const homePrltDir = path.join(process.env.HOME || '', '.proletariat');
|
|
352
|
+
fs.mkdirSync(homePrltDir, { recursive: true });
|
|
353
|
+
const tempDbPath = path.join(homePrltDir, 'adhoc.db');
|
|
354
|
+
const tempDb = new Database(tempDbPath);
|
|
355
|
+
// Ensure settings table exists
|
|
356
|
+
tempDb.exec(`
|
|
357
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
358
|
+
key TEXT PRIMARY KEY,
|
|
359
|
+
value TEXT NOT NULL
|
|
360
|
+
)
|
|
361
|
+
`);
|
|
362
|
+
if (!hasTerminalPreference(tempDb)) {
|
|
363
|
+
const terminalApp = await promptTerminalPreference(tempDb);
|
|
364
|
+
executionConfig.terminal.app = terminalApp;
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
executionConfig.terminal.app = await getTerminalApp(tempDb);
|
|
368
|
+
}
|
|
369
|
+
if (!hasShellPreference(tempDb)) {
|
|
370
|
+
const shell = await promptShellPreference(tempDb);
|
|
371
|
+
executionConfig.shell = shell;
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
executionConfig.shell = await getShell(tempDb);
|
|
375
|
+
}
|
|
376
|
+
tempDb.close();
|
|
377
|
+
}
|
|
378
|
+
// Show summary
|
|
379
|
+
this.log('');
|
|
380
|
+
this.log(styles.muted(` Session: ${sessionName}`));
|
|
381
|
+
this.log(styles.muted(` Directory: ${workDir}`));
|
|
382
|
+
this.log(styles.muted(` Environment: ${environment === 'devcontainer' ? '🐳' : '💻'} ${environment}`));
|
|
383
|
+
this.log(styles.muted(` Display: ${displayMode}`));
|
|
384
|
+
this.log(styles.muted(` Permissions: ${sandboxed ? '🔒 safe' : '⚠️ danger'}`));
|
|
385
|
+
if (flags.prompt) {
|
|
386
|
+
this.log(styles.muted(` Initial prompt: "${flags.prompt.substring(0, 50)}${flags.prompt.length > 50 ? '...' : ''}"`));
|
|
387
|
+
}
|
|
388
|
+
this.log('');
|
|
389
|
+
// Run execution
|
|
390
|
+
this.log(styles.muted('Starting Claude...'));
|
|
391
|
+
try {
|
|
392
|
+
const result = await runExecution(environment, context, 'claude-code', executionConfig, {
|
|
393
|
+
displayMode,
|
|
394
|
+
sessionManager: environment === 'devcontainer' ? 'tmux' : undefined,
|
|
395
|
+
});
|
|
396
|
+
if (result.success) {
|
|
397
|
+
this.log('');
|
|
398
|
+
this.log(styles.success(`✓ Session started: ${sessionName}`));
|
|
399
|
+
if (displayMode === 'background') {
|
|
400
|
+
this.log(styles.muted(` Reattach with: tmux attach -t "${sessionName}"`));
|
|
401
|
+
}
|
|
402
|
+
// Clean up temp devcontainer config after session starts (container has cached the config)
|
|
403
|
+
if (cleanupDevcontainer) {
|
|
404
|
+
cleanupDevcontainer();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
// Clean up on failure
|
|
409
|
+
if (cleanupDevcontainer) {
|
|
410
|
+
cleanupDevcontainer();
|
|
411
|
+
}
|
|
412
|
+
this.error(`Failed to start session: ${result.error}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
// Clean up on error
|
|
417
|
+
if (cleanupDevcontainer) {
|
|
418
|
+
cleanupDevcontainer();
|
|
419
|
+
}
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Run in "tracked mode" - inside an HQ
|
|
425
|
+
* Creates adhoc ticket, ephemeral agent, full tracking
|
|
426
|
+
*/
|
|
427
|
+
async runInsideHQ(hqPath, workDir, flags, jsonMode) {
|
|
428
|
+
this.log('');
|
|
429
|
+
this.log(styles.header('🚀 Ad-hoc Claude Session (Tracked)'));
|
|
430
|
+
this.log(styles.muted(` HQ: ${hqPath}`));
|
|
431
|
+
this.log('');
|
|
432
|
+
// Get workspace info
|
|
433
|
+
let workspaceInfo;
|
|
434
|
+
try {
|
|
435
|
+
workspaceInfo = getWorkspaceInfo();
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
if (jsonMode) {
|
|
439
|
+
outputErrorAsJson('WORKSPACE_ERROR', 'Failed to get workspace info', createMetadata('claude', flags));
|
|
440
|
+
this.exit(1);
|
|
441
|
+
}
|
|
442
|
+
this.error('Failed to get workspace info');
|
|
443
|
+
}
|
|
444
|
+
// Open database
|
|
445
|
+
const dbPath = path.join(hqPath, '.proletariat', 'workspace.db');
|
|
446
|
+
const db = new Database(dbPath);
|
|
447
|
+
const executionStorage = new ExecutionStorage(db);
|
|
448
|
+
try {
|
|
449
|
+
// Import PMO storage for ticket/project operations
|
|
450
|
+
const { getPMOContext } = await import('../lib/pmo/index.js');
|
|
451
|
+
let pmoPath;
|
|
452
|
+
let storage;
|
|
453
|
+
try {
|
|
454
|
+
const pmoContext = await getPMOContext();
|
|
455
|
+
pmoPath = pmoContext.pmoPath;
|
|
456
|
+
storage = pmoContext.storage;
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
if (jsonMode) {
|
|
460
|
+
outputErrorAsJson('PMO_NOT_FOUND', 'PMO not found. Run "prlt pmo init" first.', createMetadata('claude', flags));
|
|
461
|
+
this.exit(1);
|
|
462
|
+
}
|
|
463
|
+
this.error('PMO not found. Run "prlt pmo init" first.');
|
|
464
|
+
}
|
|
465
|
+
// Select project
|
|
466
|
+
let projectId = flags.project;
|
|
467
|
+
if (!projectId) {
|
|
468
|
+
const projects = await storage.listProjects();
|
|
469
|
+
if (projects.length === 0) {
|
|
470
|
+
db.close();
|
|
471
|
+
if (jsonMode) {
|
|
472
|
+
outputErrorAsJson('NO_PROJECTS', 'No projects found. Create a project first.', createMetadata('claude', flags));
|
|
473
|
+
this.exit(1);
|
|
474
|
+
}
|
|
475
|
+
this.error('No projects found. Create a project first.');
|
|
476
|
+
}
|
|
477
|
+
if (jsonMode) {
|
|
478
|
+
outputPromptAsJson({
|
|
479
|
+
type: 'list',
|
|
480
|
+
name: 'project',
|
|
481
|
+
message: 'Select project for adhoc ticket:',
|
|
482
|
+
choices: projects.map((p) => ({ name: `${p.name} (${p.id})`, value: p.id })),
|
|
483
|
+
}, createMetadata('claude', flags));
|
|
484
|
+
}
|
|
485
|
+
const { selectedProject } = await inquirer.prompt([
|
|
486
|
+
{
|
|
487
|
+
type: 'list',
|
|
488
|
+
name: 'selectedProject',
|
|
489
|
+
message: 'Select project for adhoc ticket:',
|
|
490
|
+
choices: projects.map((p) => ({ name: `${p.name} (${p.id})`, value: p.id })),
|
|
491
|
+
},
|
|
492
|
+
]);
|
|
493
|
+
projectId = selectedProject;
|
|
494
|
+
}
|
|
495
|
+
// Get ticket title
|
|
496
|
+
let ticketTitle = flags.title;
|
|
497
|
+
if (!ticketTitle) {
|
|
498
|
+
if (jsonMode) {
|
|
499
|
+
outputPromptAsJson({
|
|
500
|
+
type: 'input',
|
|
501
|
+
name: 'title',
|
|
502
|
+
message: 'Ticket title:',
|
|
503
|
+
}, createMetadata('claude', flags));
|
|
504
|
+
}
|
|
505
|
+
const { inputTitle } = await inquirer.prompt([
|
|
506
|
+
{
|
|
507
|
+
type: 'input',
|
|
508
|
+
name: 'inputTitle',
|
|
509
|
+
message: 'Ticket title:',
|
|
510
|
+
default: `Ad-hoc session: ${path.basename(workDir)}`,
|
|
511
|
+
validate: (input) => input.trim() ? true : 'Title required',
|
|
512
|
+
},
|
|
513
|
+
]);
|
|
514
|
+
ticketTitle = inputTitle.trim();
|
|
515
|
+
}
|
|
516
|
+
// Get optional description
|
|
517
|
+
let ticketDescription;
|
|
518
|
+
if (!jsonMode) {
|
|
519
|
+
const { inputDesc } = await inquirer.prompt([
|
|
520
|
+
{
|
|
521
|
+
type: 'input',
|
|
522
|
+
name: 'inputDesc',
|
|
523
|
+
message: 'Description (optional):',
|
|
524
|
+
},
|
|
525
|
+
]);
|
|
526
|
+
if (inputDesc.trim()) {
|
|
527
|
+
ticketDescription = inputDesc.trim();
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// Prompt for environment first (before creating ticket) so user can cancel early
|
|
531
|
+
const hasProjectDevcontainer = hasDevcontainerConfig(workDir);
|
|
532
|
+
// Check devcontainer prerequisites upfront
|
|
533
|
+
const dockerRunning = isDockerRunning();
|
|
534
|
+
const devcontainerCliInstalled = isDevcontainerCliInstalled();
|
|
535
|
+
const devcontainerReady = dockerRunning && devcontainerCliInstalled;
|
|
536
|
+
// Build devcontainer label with missing requirements
|
|
537
|
+
let devcontainerLabel = hasProjectDevcontainer
|
|
538
|
+
? '🐳 devcontainer (uses project config, sandboxed)'
|
|
539
|
+
: '🐳 devcontainer (uses catch-all container, sandboxed)';
|
|
540
|
+
if (!devcontainerReady) {
|
|
541
|
+
const missing = [];
|
|
542
|
+
if (!dockerRunning)
|
|
543
|
+
missing.push('Docker');
|
|
544
|
+
if (!devcontainerCliInstalled)
|
|
545
|
+
missing.push('devcontainer CLI');
|
|
546
|
+
devcontainerLabel = `🐳 devcontainer (requires: ${missing.join(', ')})`;
|
|
547
|
+
}
|
|
548
|
+
let environment = 'host';
|
|
549
|
+
if (flags.environment) {
|
|
550
|
+
environment = flags.environment;
|
|
551
|
+
}
|
|
552
|
+
else if (!jsonMode) {
|
|
553
|
+
let environmentSelected = false;
|
|
554
|
+
while (!environmentSelected) {
|
|
555
|
+
// eslint-disable-next-line no-await-in-loop -- Interactive user prompt in loop
|
|
556
|
+
const { selectedEnv } = await inquirer.prompt([
|
|
557
|
+
{
|
|
558
|
+
type: 'list',
|
|
559
|
+
name: 'selectedEnv',
|
|
560
|
+
message: 'Where should Claude run?',
|
|
561
|
+
choices: [
|
|
562
|
+
{
|
|
563
|
+
name: devcontainerLabel,
|
|
564
|
+
value: 'devcontainer',
|
|
565
|
+
disabled: !devcontainerReady,
|
|
566
|
+
},
|
|
567
|
+
{ name: '💻 host (runs directly on your machine)', value: 'host' },
|
|
568
|
+
],
|
|
569
|
+
default: devcontainerReady ? 'devcontainer' : 'host',
|
|
570
|
+
},
|
|
571
|
+
]);
|
|
572
|
+
if (selectedEnv === 'devcontainer') {
|
|
573
|
+
// Double-check prerequisites (in case user retried after starting Docker)
|
|
574
|
+
if (!isDockerRunning()) {
|
|
575
|
+
this.log('');
|
|
576
|
+
this.warn('Docker is not running. Please start Docker Desktop or select "host".');
|
|
577
|
+
this.log('');
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (!isDevcontainerCliInstalled()) {
|
|
581
|
+
this.log('');
|
|
582
|
+
this.warn('devcontainer CLI is not installed.\n' +
|
|
583
|
+
'Install with: npm install -g @devcontainers/cli\n' +
|
|
584
|
+
'Or select "host" to run directly on your machine.');
|
|
585
|
+
this.log('');
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
// Check GitHub token is available for git push operations
|
|
589
|
+
if (!isGitHubTokenAvailable()) {
|
|
590
|
+
const tokenChoices = [
|
|
591
|
+
{ name: 'Yes, continue anyway (git push may fail)', value: 'continue' },
|
|
592
|
+
{ name: 'No, let me run gh auth login first', value: 'cancel' },
|
|
593
|
+
{ name: 'Switch to host mode instead', value: 'host' },
|
|
594
|
+
];
|
|
595
|
+
const tokenMessage = 'GitHub token not found. Git push may fail. Continue without token?';
|
|
596
|
+
if (jsonMode) {
|
|
597
|
+
outputPromptAsJson(buildPromptConfig('list', 'tokenAction', tokenMessage, tokenChoices), createMetadata('claude', flags));
|
|
598
|
+
db.close();
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
this.log('');
|
|
602
|
+
this.warn('GitHub token not found.\n' +
|
|
603
|
+
'Git push operations may fail inside the container.\n' +
|
|
604
|
+
'Run `gh auth login` to authenticate, or continue without token.');
|
|
605
|
+
this.log('');
|
|
606
|
+
// eslint-disable-next-line no-await-in-loop -- Interactive user prompt in loop
|
|
607
|
+
const { tokenAction } = await inquirer.prompt([
|
|
608
|
+
{
|
|
609
|
+
type: 'list',
|
|
610
|
+
name: 'tokenAction',
|
|
611
|
+
message: tokenMessage,
|
|
612
|
+
choices: tokenChoices,
|
|
613
|
+
default: 'continue',
|
|
614
|
+
},
|
|
615
|
+
]);
|
|
616
|
+
if (tokenAction === 'cancel') {
|
|
617
|
+
db.close();
|
|
618
|
+
this.log(styles.muted('Run `gh auth login` and try again.'));
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (tokenAction === 'host') {
|
|
622
|
+
environment = 'host';
|
|
623
|
+
environmentSelected = true;
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
// tokenAction === 'continue' - fall through to devcontainer setup
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
environment = selectedEnv;
|
|
630
|
+
environmentSelected = true;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
// Prompt for display mode
|
|
634
|
+
let displayMode = 'terminal';
|
|
635
|
+
if (flags['display-mode']) {
|
|
636
|
+
displayMode = flags['display-mode'];
|
|
637
|
+
}
|
|
638
|
+
else if (!jsonMode) {
|
|
639
|
+
const { selectedDisplay } = await inquirer.prompt([
|
|
640
|
+
{
|
|
641
|
+
type: 'list',
|
|
642
|
+
name: 'selectedDisplay',
|
|
643
|
+
message: 'How should output be displayed?',
|
|
644
|
+
choices: [
|
|
645
|
+
{ name: '🖥️ New tab - Opens in new terminal tab (recommended)', value: 'terminal' },
|
|
646
|
+
{ name: '▶️ Foreground - Run in current terminal (blocking)', value: 'foreground' },
|
|
647
|
+
{ name: '📦 Background - Runs detached, reattach later', value: 'background' },
|
|
648
|
+
],
|
|
649
|
+
default: 'terminal',
|
|
650
|
+
},
|
|
651
|
+
]);
|
|
652
|
+
displayMode = selectedDisplay;
|
|
653
|
+
}
|
|
654
|
+
// Prompt for permission mode
|
|
655
|
+
let sandboxed = true;
|
|
656
|
+
if (flags['permission-mode']) {
|
|
657
|
+
sandboxed = flags['permission-mode'] === 'safe';
|
|
658
|
+
}
|
|
659
|
+
else if (!jsonMode) {
|
|
660
|
+
const containerNote = environment === 'devcontainer' ? ' (container provides additional isolation)' : '';
|
|
661
|
+
const { permissionMode } = await inquirer.prompt([
|
|
662
|
+
{
|
|
663
|
+
type: 'list',
|
|
664
|
+
name: 'permissionMode',
|
|
665
|
+
message: `Permission mode${containerNote}:`,
|
|
666
|
+
choices: [
|
|
667
|
+
{ name: '⚠️ danger - Skip permission checks (faster)', value: 'danger' },
|
|
668
|
+
{ name: '🔒 safe - Requires approval for dangerous operations', value: 'safe' },
|
|
669
|
+
],
|
|
670
|
+
default: 'danger',
|
|
671
|
+
},
|
|
672
|
+
]);
|
|
673
|
+
sandboxed = permissionMode === 'safe';
|
|
674
|
+
}
|
|
675
|
+
// Warn about uncommitted changes in danger mode
|
|
676
|
+
if (!sandboxed && isGitRepo(workDir) && hasUncommittedChanges(workDir)) {
|
|
677
|
+
this.log('');
|
|
678
|
+
this.warn('Running in danger mode with uncommitted changes!');
|
|
679
|
+
this.log(styles.muted(' Consider committing or stashing changes first.'));
|
|
680
|
+
this.log('');
|
|
681
|
+
if (!jsonMode) {
|
|
682
|
+
const { proceed } = await inquirer.prompt([
|
|
683
|
+
{
|
|
684
|
+
type: 'list',
|
|
685
|
+
name: 'proceed',
|
|
686
|
+
message: 'Continue anyway?',
|
|
687
|
+
choices: [
|
|
688
|
+
{ name: 'Yes, proceed', value: true },
|
|
689
|
+
{ name: 'No, cancel', value: false },
|
|
690
|
+
],
|
|
691
|
+
default: true,
|
|
692
|
+
},
|
|
693
|
+
]);
|
|
694
|
+
if (!proceed) {
|
|
695
|
+
this.log(styles.muted('Cancelled.'));
|
|
696
|
+
db.close();
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
// Now create ticket (after all prompts so user can cancel early without orphaned ticket)
|
|
702
|
+
const ticket = await storage.createTicket(projectId, {
|
|
703
|
+
title: ticketTitle,
|
|
704
|
+
description: ticketDescription,
|
|
705
|
+
category: 'adhoc',
|
|
706
|
+
priority: 'P2',
|
|
707
|
+
});
|
|
708
|
+
this.log(styles.success(` Created ticket: ${ticket.id}`));
|
|
709
|
+
// Now use the slug from ticket title if not provided
|
|
710
|
+
// Note: slug reserved for future branch naming
|
|
711
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
712
|
+
const _slug = flags.slug || ticketTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-').substring(0, 30);
|
|
713
|
+
// Create ephemeral agent (with rollback on failure)
|
|
714
|
+
this.log(styles.muted(' Creating ephemeral agent...'));
|
|
715
|
+
let ephemeralResult;
|
|
716
|
+
try {
|
|
717
|
+
ephemeralResult = await createEphemeralAgent(workspaceInfo, {
|
|
718
|
+
skipDevcontainer: environment === 'host',
|
|
719
|
+
log: (msg) => this.log(styles.muted(` ${msg}`)),
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
catch (agentError) {
|
|
723
|
+
// Rollback: delete the ticket we just created
|
|
724
|
+
this.log(styles.muted(' Rolling back ticket creation due to agent error...'));
|
|
725
|
+
try {
|
|
726
|
+
await storage.deleteTicket(ticket.id);
|
|
727
|
+
this.log(styles.muted(` Deleted orphaned ticket: ${ticket.id}`));
|
|
728
|
+
}
|
|
729
|
+
catch {
|
|
730
|
+
this.warn(`Failed to delete orphaned ticket ${ticket.id}. Manual cleanup may be needed.`);
|
|
731
|
+
}
|
|
732
|
+
throw agentError;
|
|
733
|
+
}
|
|
734
|
+
const agentName = ephemeralResult.name;
|
|
735
|
+
const agentDir = ephemeralResult.worktreePath;
|
|
736
|
+
this.log(styles.success(` Agent: ${agentName}`));
|
|
737
|
+
// Build execution context
|
|
738
|
+
const context = {
|
|
739
|
+
ticketId: ticket.id,
|
|
740
|
+
ticketTitle: ticket.title,
|
|
741
|
+
ticketDescription: ticket.description,
|
|
742
|
+
agentName,
|
|
743
|
+
agentDir,
|
|
744
|
+
worktreePath: agentDir,
|
|
745
|
+
branch: 'main', // Adhoc sessions work on main
|
|
746
|
+
hqPath,
|
|
747
|
+
pmoPath,
|
|
748
|
+
actionId: 'adhoc',
|
|
749
|
+
actionName: 'Ad-hoc',
|
|
750
|
+
actionPrompt: flags.prompt,
|
|
751
|
+
modifiesCode: false, // Don't manage branches for adhoc
|
|
752
|
+
};
|
|
753
|
+
// Create execution record
|
|
754
|
+
const execution = executionStorage.createExecution({
|
|
755
|
+
ticketId: ticket.id,
|
|
756
|
+
agentName,
|
|
757
|
+
executor: 'claude-code',
|
|
758
|
+
environment,
|
|
759
|
+
displayMode,
|
|
760
|
+
sandboxed,
|
|
761
|
+
branch: 'main',
|
|
762
|
+
});
|
|
763
|
+
// Update ticket assignee
|
|
764
|
+
await storage.updateTicket(ticket.id, { assignee: agentName });
|
|
765
|
+
// Load execution config
|
|
766
|
+
const executionConfig = loadExecutionConfig(db);
|
|
767
|
+
executionConfig.sandboxed = sandboxed;
|
|
768
|
+
executionConfig.outputMode = 'interactive';
|
|
769
|
+
// For terminal mode, ensure terminal preference is set
|
|
770
|
+
if (displayMode === 'terminal' && !jsonMode) {
|
|
771
|
+
if (!hasTerminalPreference(db)) {
|
|
772
|
+
executionConfig.terminal.app = await promptTerminalPreference(db);
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
executionConfig.terminal.app = await getTerminalApp(db);
|
|
776
|
+
}
|
|
777
|
+
if (!hasShellPreference(db)) {
|
|
778
|
+
executionConfig.shell = await promptShellPreference(db);
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
executionConfig.shell = await getShell(db);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
// Show summary
|
|
785
|
+
this.log('');
|
|
786
|
+
this.log(styles.muted(` Ticket: ${ticket.id}`));
|
|
787
|
+
this.log(styles.muted(` Agent: ${agentName}`));
|
|
788
|
+
this.log(styles.muted(` Work ID: ${execution.id}`));
|
|
789
|
+
this.log(styles.muted(` Environment: ${environment === 'devcontainer' ? '🐳' : '💻'} ${environment}`));
|
|
790
|
+
this.log(styles.muted(` Display: ${displayMode}`));
|
|
791
|
+
this.log(styles.muted(` Permissions: ${sandboxed ? '🔒 safe' : '⚠️ danger'}`));
|
|
792
|
+
if (flags.prompt) {
|
|
793
|
+
this.log(styles.muted(` Initial prompt: "${flags.prompt.substring(0, 50)}${flags.prompt.length > 50 ? '...' : ''}"`));
|
|
794
|
+
}
|
|
795
|
+
this.log('');
|
|
796
|
+
// Run execution
|
|
797
|
+
this.log(styles.muted('Starting Claude...'));
|
|
798
|
+
const result = await runExecution(environment, context, 'claude-code', executionConfig, {
|
|
799
|
+
displayMode,
|
|
800
|
+
sessionManager: environment === 'devcontainer' ? 'tmux' : undefined,
|
|
801
|
+
});
|
|
802
|
+
if (result.success) {
|
|
803
|
+
executionStorage.updateStatus(execution.id, 'running');
|
|
804
|
+
executionStorage.updateProcessInfo(execution.id, {
|
|
805
|
+
pid: result.pid,
|
|
806
|
+
containerId: result.containerId,
|
|
807
|
+
sessionId: result.sessionId,
|
|
808
|
+
logPath: result.logPath,
|
|
809
|
+
});
|
|
810
|
+
this.log('');
|
|
811
|
+
this.log(styles.success(`✓ Session started (${execution.id})`));
|
|
812
|
+
this.log('');
|
|
813
|
+
this.log(styles.muted('Commands:'));
|
|
814
|
+
this.log(styles.muted(` prlt work status View work status`));
|
|
815
|
+
this.log(styles.muted(` prlt session attach Attach to session`));
|
|
816
|
+
this.log(styles.muted(` prlt work stop ${execution.id} Stop work`));
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
executionStorage.updateStatus(execution.id, 'failed');
|
|
820
|
+
this.error(`Failed to start session: ${result.error}`);
|
|
821
|
+
}
|
|
822
|
+
db.close();
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
db.close();
|
|
826
|
+
throw error;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Set up catch-all devcontainer for directories without one
|
|
831
|
+
* Uses a temp directory to avoid polluting user's cwd
|
|
832
|
+
* Returns { configDir, workDir } where configDir contains .devcontainer
|
|
833
|
+
*/
|
|
834
|
+
async setupCatchallDevcontainer(workDir, slug) {
|
|
835
|
+
// Create temp directory for devcontainer config
|
|
836
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'prlt-adhoc-'));
|
|
837
|
+
const devcontainerDir = path.join(tempDir, '.devcontainer');
|
|
838
|
+
fs.mkdirSync(devcontainerDir, { recursive: true });
|
|
839
|
+
// Create minimal devcontainer.json using catch-all image
|
|
840
|
+
// Use absolute path for mount to point to user's actual workDir
|
|
841
|
+
const devcontainerJson = {
|
|
842
|
+
name: `adhoc-${slug}`,
|
|
843
|
+
image: CATCHALL_DEVCONTAINER_IMAGE,
|
|
844
|
+
customizations: {
|
|
845
|
+
vscode: {
|
|
846
|
+
extensions: ['anthropic.claude-code'],
|
|
847
|
+
},
|
|
848
|
+
},
|
|
849
|
+
remoteUser: 'node',
|
|
850
|
+
workspaceFolder: '/workspace',
|
|
851
|
+
mounts: [
|
|
852
|
+
// Use absolute path to user's workDir, not ${localWorkspaceFolder}
|
|
853
|
+
`source=${workDir},target=/workspace,type=bind`,
|
|
854
|
+
],
|
|
855
|
+
containerEnv: {
|
|
856
|
+
ANTHROPIC_API_KEY: '${localEnv:ANTHROPIC_API_KEY}',
|
|
857
|
+
},
|
|
858
|
+
};
|
|
859
|
+
fs.writeFileSync(path.join(devcontainerDir, 'devcontainer.json'), JSON.stringify(devcontainerJson, null, 2));
|
|
860
|
+
this.log(styles.muted(' Created temporary .devcontainer config'));
|
|
861
|
+
// Cleanup function to remove temp directory
|
|
862
|
+
const cleanup = () => {
|
|
863
|
+
try {
|
|
864
|
+
if (fs.existsSync(tempDir)) {
|
|
865
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
catch {
|
|
869
|
+
// Ignore cleanup errors
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
return { configDir: tempDir, workDir, cleanup };
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Check if catch-all container image is available
|
|
876
|
+
* Returns true if image exists locally or can be pulled
|
|
877
|
+
*/
|
|
878
|
+
async checkCatchallImage() {
|
|
879
|
+
try {
|
|
880
|
+
// First check if image exists locally
|
|
881
|
+
execSync(`docker image inspect ${CATCHALL_DEVCONTAINER_IMAGE}`, { stdio: 'pipe' });
|
|
882
|
+
return { available: true };
|
|
883
|
+
}
|
|
884
|
+
catch {
|
|
885
|
+
// Image not local, try to pull it
|
|
886
|
+
this.log(styles.muted(` Pulling container image: ${CATCHALL_DEVCONTAINER_IMAGE}`));
|
|
887
|
+
try {
|
|
888
|
+
execSync(`docker pull ${CATCHALL_DEVCONTAINER_IMAGE}`, { stdio: 'pipe', timeout: 120000 });
|
|
889
|
+
return { available: true };
|
|
890
|
+
}
|
|
891
|
+
catch {
|
|
892
|
+
return {
|
|
893
|
+
available: false,
|
|
894
|
+
error: `Failed to pull catch-all container image: ${CATCHALL_DEVCONTAINER_IMAGE}. Try running on host instead, or ensure Docker is configured correctly.`,
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|