@profoundlogic/coderflow-cli 0.2.1
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/LICENSE.txt +322 -0
- package/README.md +102 -0
- package/coder.js +202 -0
- package/lib/commands/apply.js +238 -0
- package/lib/commands/attach.js +143 -0
- package/lib/commands/config.js +226 -0
- package/lib/commands/containers.js +213 -0
- package/lib/commands/discard.js +167 -0
- package/lib/commands/interactive.js +292 -0
- package/lib/commands/jira.js +464 -0
- package/lib/commands/license.js +172 -0
- package/lib/commands/list.js +104 -0
- package/lib/commands/login.js +329 -0
- package/lib/commands/logs.js +66 -0
- package/lib/commands/profile.js +539 -0
- package/lib/commands/reject.js +53 -0
- package/lib/commands/results.js +89 -0
- package/lib/commands/run.js +237 -0
- package/lib/commands/server.js +537 -0
- package/lib/commands/status.js +39 -0
- package/lib/commands/test.js +335 -0
- package/lib/config.js +378 -0
- package/lib/help.js +444 -0
- package/lib/http-client.js +180 -0
- package/lib/oidc.js +126 -0
- package/lib/profile.js +296 -0
- package/lib/state-capture.js +336 -0
- package/lib/task-grouping.js +210 -0
- package/lib/terminal-client.js +162 -0
- package/package.json +35 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: coder containers - List and manage containers via server API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createInterface } from 'readline';
|
|
6
|
+
import { request } from '../http-client.js';
|
|
7
|
+
|
|
8
|
+
function parseCleanArgs(args) {
|
|
9
|
+
const options = {
|
|
10
|
+
olderThan: null,
|
|
11
|
+
stoppedOnly: false,
|
|
12
|
+
dryRun: false,
|
|
13
|
+
skipConfirm: false
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
for (const arg of args) {
|
|
17
|
+
if (arg === '--dry-run') {
|
|
18
|
+
options.dryRun = true;
|
|
19
|
+
} else if (arg === '--yes' || arg === '-y') {
|
|
20
|
+
options.skipConfirm = true;
|
|
21
|
+
} else if (arg === '--stopped') {
|
|
22
|
+
options.stoppedOnly = true;
|
|
23
|
+
} else if (arg.startsWith('--older-than=')) {
|
|
24
|
+
const value = arg.substring('--older-than='.length);
|
|
25
|
+
const match = value.match(/^(\d+)d$/);
|
|
26
|
+
if (match) {
|
|
27
|
+
options.olderThan = parseInt(match[1], 10);
|
|
28
|
+
} else {
|
|
29
|
+
console.error('Error: --older-than must be in format: Xd (e.g., 7d, 30d)');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
console.error(`Error: Unknown option: ${arg}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return options;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function confirm(message) {
|
|
42
|
+
const rl = createInterface({
|
|
43
|
+
input: process.stdin,
|
|
44
|
+
output: process.stdout
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
rl.question(message, (answer) => {
|
|
49
|
+
rl.close();
|
|
50
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function formatAge(createdAt) {
|
|
56
|
+
if (!createdAt) {
|
|
57
|
+
return 'unknown';
|
|
58
|
+
}
|
|
59
|
+
const createdDate = new Date(createdAt);
|
|
60
|
+
const diffMs = Date.now() - createdDate.getTime();
|
|
61
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
62
|
+
if (diffHours < 24) {
|
|
63
|
+
return `${diffHours}h ago`;
|
|
64
|
+
}
|
|
65
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
66
|
+
return `${diffDays}d ago`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function filterContainers(containers, options) {
|
|
70
|
+
return containers.filter((container) => {
|
|
71
|
+
if (options.stoppedOnly && container.status === 'running') {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (options.olderThan !== null && container.createdAt) {
|
|
76
|
+
const createdDate = new Date(container.createdAt);
|
|
77
|
+
const ageInDays = (Date.now() - createdDate.getTime()) / (1000 * 60 * 60 * 24);
|
|
78
|
+
if (ageInDays < options.olderThan) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return true;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function fetchContainers() {
|
|
88
|
+
const data = await request('/containers');
|
|
89
|
+
return data.containers || [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function listContainers() {
|
|
93
|
+
console.log('Fetching Coder containers...\n');
|
|
94
|
+
|
|
95
|
+
const containers = await fetchContainers();
|
|
96
|
+
|
|
97
|
+
if (containers.length === 0) {
|
|
98
|
+
console.log('No Coder containers found.');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(`Found ${containers.length} container(s):\n`);
|
|
103
|
+
|
|
104
|
+
for (const container of containers) {
|
|
105
|
+
const statusIcon = container.status === 'running' ? 'ā¶' : 'ā ';
|
|
106
|
+
console.log(` ${statusIcon} ${container.name} (${container.containerId})`);
|
|
107
|
+
console.log(` Status: ${container.status}`);
|
|
108
|
+
if (container.environment) {
|
|
109
|
+
console.log(` Env: ${container.environment}`);
|
|
110
|
+
}
|
|
111
|
+
if (container.createdAt) {
|
|
112
|
+
console.log(` Created: ${new Date(container.createdAt).toLocaleString()} (${formatAge(container.createdAt)})`);
|
|
113
|
+
}
|
|
114
|
+
console.log('');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function deleteContainer(container) {
|
|
119
|
+
await request(`/containers/${container.name}`, {
|
|
120
|
+
method: 'DELETE'
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function cleanContainers(args) {
|
|
125
|
+
const options = parseCleanArgs(args);
|
|
126
|
+
|
|
127
|
+
console.log('Fetching Coder containers...\n');
|
|
128
|
+
|
|
129
|
+
const allContainers = await fetchContainers();
|
|
130
|
+
|
|
131
|
+
if (allContainers.length === 0) {
|
|
132
|
+
console.log('No Coder containers found. Nothing to clean.');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const containersToDelete = filterContainers(allContainers, options);
|
|
137
|
+
|
|
138
|
+
if (containersToDelete.length === 0) {
|
|
139
|
+
console.log('No containers match the specified criteria. Nothing to clean.');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.log(`Found ${containersToDelete.length} container(s) to clean:\n`);
|
|
144
|
+
|
|
145
|
+
for (const container of containersToDelete) {
|
|
146
|
+
const statusIcon = container.status === 'running' ? 'ā¶' : 'ā ';
|
|
147
|
+
console.log(` ${statusIcon} ${container.name} (${container.containerId})`);
|
|
148
|
+
if (container.createdAt) {
|
|
149
|
+
console.log(` Created: ${new Date(container.createdAt).toLocaleString()} (${formatAge(container.createdAt)})`);
|
|
150
|
+
}
|
|
151
|
+
console.log('');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const filters = [];
|
|
155
|
+
if (options.stoppedOnly) {
|
|
156
|
+
filters.push('stopped only');
|
|
157
|
+
}
|
|
158
|
+
if (options.olderThan !== null) {
|
|
159
|
+
filters.push(`older than ${options.olderThan} days`);
|
|
160
|
+
}
|
|
161
|
+
if (filters.length > 0) {
|
|
162
|
+
console.log(`Filters: ${filters.join(', ')}\n`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (options.dryRun) {
|
|
166
|
+
console.log('ā Dry run complete. No containers were deleted.');
|
|
167
|
+
console.log('Run without --dry-run to actually delete these containers.');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!options.skipConfirm) {
|
|
172
|
+
const shouldDelete = await confirm(`Delete ${containersToDelete.length} container(s)? [y/N] `);
|
|
173
|
+
if (!shouldDelete) {
|
|
174
|
+
console.log('Cancelled. No containers were deleted.');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let deletedCount = 0;
|
|
180
|
+
let errorCount = 0;
|
|
181
|
+
|
|
182
|
+
for (const container of containersToDelete) {
|
|
183
|
+
try {
|
|
184
|
+
await deleteContainer(container);
|
|
185
|
+
deletedCount += 1;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error(`ā Failed to delete ${container.name}: ${error.message}`);
|
|
188
|
+
errorCount += 1;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
console.log(`\nā Cleanup complete`);
|
|
193
|
+
console.log(` Deleted: ${deletedCount} container(s)`);
|
|
194
|
+
if (errorCount > 0) {
|
|
195
|
+
console.log(` Errors: ${errorCount} container(s)`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function handleContainers(args = []) {
|
|
200
|
+
const subcommand = args[0];
|
|
201
|
+
|
|
202
|
+
if (subcommand === 'clean') {
|
|
203
|
+
await cleanContainers(args.slice(1));
|
|
204
|
+
} else if (!subcommand) {
|
|
205
|
+
await listContainers();
|
|
206
|
+
} else {
|
|
207
|
+
console.error(`Error: Unknown subcommand: ${subcommand}`);
|
|
208
|
+
console.error('Usage: coder containers [clean]');
|
|
209
|
+
console.error(' coder containers # List all containers');
|
|
210
|
+
console.error(' coder containers clean # Clean containers');
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: coder discard - Discard all uncommitted changes from local repos
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { request } from '../http-client.js';
|
|
6
|
+
import readline from 'readline';
|
|
7
|
+
|
|
8
|
+
export async function discardChanges(args) {
|
|
9
|
+
const flags = parseFlags(args);
|
|
10
|
+
|
|
11
|
+
// Get environment (from flag or default from server)
|
|
12
|
+
let environment = flags.env;
|
|
13
|
+
if (!environment) {
|
|
14
|
+
// Get default environment from server
|
|
15
|
+
try {
|
|
16
|
+
const envList = await request('/environments');
|
|
17
|
+
environment = envList.default_environment;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error('Error: Failed to get default environment from server');
|
|
20
|
+
console.error('Usage: coder discard [--env=<environment>] [--yes]');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!environment) {
|
|
26
|
+
console.error('Error: No environment specified and no default environment configured on server');
|
|
27
|
+
console.error('Usage: coder discard [--env=<environment>] [--yes]');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(`Discarding changes in environment: ${environment}\n`);
|
|
32
|
+
|
|
33
|
+
// Get repos from environment configuration
|
|
34
|
+
const envData = await request(`/environments/${environment}`);
|
|
35
|
+
|
|
36
|
+
if (!envData.repos || envData.repos.length === 0) {
|
|
37
|
+
console.log('No repositories configured for this environment.');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check which repos have changes (using paths from environment config)
|
|
42
|
+
const { existsSync } = await import('fs');
|
|
43
|
+
const { execSync } = await import('child_process');
|
|
44
|
+
|
|
45
|
+
const reposWithChanges = [];
|
|
46
|
+
for (const repo of envData.repos) {
|
|
47
|
+
// Use repo.path if specified, otherwise use repo.name (same logic as server patches endpoint)
|
|
48
|
+
const localPath = repo.path || repo.name;
|
|
49
|
+
const repoPath = `./${localPath}`;
|
|
50
|
+
|
|
51
|
+
// Check if repo exists
|
|
52
|
+
if (!existsSync(repoPath)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// Check if repo has changes (staged, unstaged, or untracked)
|
|
58
|
+
const status = execSync(`git -C ${repoPath} status --porcelain`, { encoding: 'utf8', stdio: 'pipe' });
|
|
59
|
+
if (status.trim().length > 0) {
|
|
60
|
+
reposWithChanges.push({ ...repo, localPath, repoPath });
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// Skip repos with git errors
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (reposWithChanges.length === 0) {
|
|
69
|
+
console.log('No repositories with uncommitted changes found.\n');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(`Found ${reposWithChanges.length} ${reposWithChanges.length === 1 ? 'repository' : 'repositories'} with changes:\n`);
|
|
74
|
+
|
|
75
|
+
// List repos with changes
|
|
76
|
+
for (const repo of reposWithChanges) {
|
|
77
|
+
console.log(` - ${repo.name} (${repo.localPath})`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Confirm before discarding
|
|
81
|
+
if (!flags.yes) {
|
|
82
|
+
console.log('\nThis will:');
|
|
83
|
+
console.log(' 1. Unstage all changes (git reset HEAD)');
|
|
84
|
+
console.log(' 2. Discard all uncommitted changes (git restore .)');
|
|
85
|
+
console.log(' 3. Remove untracked files (git clean -fd)');
|
|
86
|
+
console.log('\nā ļø WARNING: This action cannot be undone!\n');
|
|
87
|
+
|
|
88
|
+
const answer = await prompt('Continue? [y/N] ');
|
|
89
|
+
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
|
|
90
|
+
console.log('Cancelled.');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log('');
|
|
96
|
+
|
|
97
|
+
// Discard changes in each repo
|
|
98
|
+
for (const repo of reposWithChanges) {
|
|
99
|
+
console.log(`š¦ ${repo.name}`);
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// 1. Unstage all changes
|
|
103
|
+
try {
|
|
104
|
+
execSync(`git -C ${repo.repoPath} reset HEAD`, { stdio: 'pipe' });
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// Ignore errors - might not have staged changes
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 2. Discard all uncommitted changes
|
|
110
|
+
execSync(`git -C ${repo.repoPath} restore .`, { stdio: 'pipe' });
|
|
111
|
+
|
|
112
|
+
// 3. Remove untracked files and directories
|
|
113
|
+
execSync(`git -C ${repo.repoPath} clean -fd`, { stdio: 'pipe' });
|
|
114
|
+
|
|
115
|
+
console.log(` ā Changes discarded successfully\n`);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
// Try fallback method for older git versions
|
|
118
|
+
try {
|
|
119
|
+
execSync(`git -C ${repo.repoPath} checkout -- .`, { stdio: 'pipe' });
|
|
120
|
+
execSync(`git -C ${repo.repoPath} clean -fd`, { stdio: 'pipe' });
|
|
121
|
+
console.log(` ā Changes discarded successfully\n`);
|
|
122
|
+
} catch (fallbackError) {
|
|
123
|
+
console.log(` ā Failed to discard changes: ${error.message}`);
|
|
124
|
+
console.log(` You may need to discard manually\n`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log('ā Discard complete\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Parse command line flags
|
|
134
|
+
*/
|
|
135
|
+
function parseFlags(args) {
|
|
136
|
+
const flags = {
|
|
137
|
+
yes: false,
|
|
138
|
+
env: null
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
for (const arg of args) {
|
|
142
|
+
if (arg === '--yes' || arg === '-y') {
|
|
143
|
+
flags.yes = true;
|
|
144
|
+
} else if (arg.startsWith('--env=')) {
|
|
145
|
+
flags.env = arg.substring('--env='.length);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return flags;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Simple prompt for user input
|
|
154
|
+
*/
|
|
155
|
+
function prompt(question) {
|
|
156
|
+
return new Promise((resolve) => {
|
|
157
|
+
const rl = readline.createInterface({
|
|
158
|
+
input: process.stdin,
|
|
159
|
+
output: process.stdout
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
rl.question(question, (answer) => {
|
|
163
|
+
rl.close();
|
|
164
|
+
resolve(answer);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: coder start <environment> - Start interactive container
|
|
3
|
+
* Command: coder shell <environment> - Start interactive shell
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { request } from '../http-client.js';
|
|
7
|
+
import { getDefaultEnvironment, saveLastContainerId } from '../config.js';
|
|
8
|
+
import { connectTerminal } from '../terminal-client.js';
|
|
9
|
+
|
|
10
|
+
function parseEnvPair(input) {
|
|
11
|
+
const [rawKey, ...rest] = input.split('=');
|
|
12
|
+
if (!rawKey || rest.length === 0) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const key = rawKey.trim();
|
|
16
|
+
const value = rest.join('=').trim();
|
|
17
|
+
if (!key || !value) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return { key, value };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseKeyValue(pair) {
|
|
24
|
+
const [rawKey, ...rest] = pair.split('=');
|
|
25
|
+
if (!rawKey || rest.length === 0) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const key = rawKey.trim();
|
|
29
|
+
const value = rest.join('=').trim();
|
|
30
|
+
if (!key || !value) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return { key, value };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function startInteractive(args = []) {
|
|
37
|
+
let environment = null;
|
|
38
|
+
let noAttach = false;
|
|
39
|
+
let startShell = false;
|
|
40
|
+
let withLocalState = false;
|
|
41
|
+
const envVars = {}; // Environment variables to pass to container
|
|
42
|
+
const branches = {}; // Branch selections
|
|
43
|
+
|
|
44
|
+
// Parse arguments
|
|
45
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
46
|
+
const arg = args[index];
|
|
47
|
+
if (arg === '--no-attach') {
|
|
48
|
+
noAttach = true;
|
|
49
|
+
} else if (arg === '--shell') {
|
|
50
|
+
startShell = true;
|
|
51
|
+
} else if (arg === '--with-local-state') {
|
|
52
|
+
withLocalState = true;
|
|
53
|
+
} else if (arg === '--branch') {
|
|
54
|
+
const next = args[index + 1];
|
|
55
|
+
if (!next) {
|
|
56
|
+
console.error('Error: --branch requires REPO=BRANCH or BRANCH');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
const parsed = parseKeyValue(next);
|
|
60
|
+
if (parsed) {
|
|
61
|
+
// Format: --branch profoundjs=feature-xyz
|
|
62
|
+
branches[parsed.key] = parsed.value;
|
|
63
|
+
} else {
|
|
64
|
+
// Format: --branch feature-xyz (will be applied to first selectable repo)
|
|
65
|
+
branches._default = next.trim();
|
|
66
|
+
}
|
|
67
|
+
index += 1; // Skip value token
|
|
68
|
+
} else if (arg.startsWith('--branch=')) {
|
|
69
|
+
const pair = arg.substring('--branch='.length);
|
|
70
|
+
const parsed = parseKeyValue(pair);
|
|
71
|
+
if (parsed) {
|
|
72
|
+
// Format: --branch=profoundjs=feature-xyz
|
|
73
|
+
branches[parsed.key] = parsed.value;
|
|
74
|
+
} else {
|
|
75
|
+
// Format: --branch=feature-xyz (will be applied to first selectable repo)
|
|
76
|
+
branches._default = pair.trim();
|
|
77
|
+
}
|
|
78
|
+
} else if (arg === '--agent') {
|
|
79
|
+
const next = args[index + 1];
|
|
80
|
+
if (!next) {
|
|
81
|
+
console.error('Error: --agent requires a value (claude or codex)');
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
envVars.CODER_AGENT = next;
|
|
85
|
+
index += 1;
|
|
86
|
+
} else if (arg.startsWith('--agent=')) {
|
|
87
|
+
envVars.CODER_AGENT = arg.substring('--agent='.length);
|
|
88
|
+
} else if (arg === '--env') {
|
|
89
|
+
const next = args[index + 1];
|
|
90
|
+
if (!next) {
|
|
91
|
+
console.error('Error: --env requires KEY=VALUE');
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
const parsed = parseEnvPair(next);
|
|
95
|
+
if (!parsed) {
|
|
96
|
+
console.error(`Error: Invalid --env value: ${next}`);
|
|
97
|
+
console.error('Expected: --env KEY=VALUE');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
envVars[parsed.key] = parsed.value;
|
|
101
|
+
index += 1;
|
|
102
|
+
} else if (arg.startsWith('--env=')) {
|
|
103
|
+
const parsed = parseEnvPair(arg.substring('--env='.length));
|
|
104
|
+
if (!parsed) {
|
|
105
|
+
console.error(`Error: Invalid --env format: ${arg}`);
|
|
106
|
+
console.error('Expected: --env KEY=VALUE');
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
envVars[parsed.key] = parsed.value;
|
|
110
|
+
} else if (arg === '--environment') {
|
|
111
|
+
const next = args[index + 1];
|
|
112
|
+
if (!next) {
|
|
113
|
+
console.error('Error: --environment requires a value');
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
environment = next;
|
|
117
|
+
index += 1;
|
|
118
|
+
} else if (arg.startsWith('--environment=')) {
|
|
119
|
+
environment = arg.substring('--environment='.length);
|
|
120
|
+
} else if (!environment && !arg.startsWith('--')) {
|
|
121
|
+
environment = arg;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!environment) {
|
|
126
|
+
const configDefault = await getDefaultEnvironment();
|
|
127
|
+
environment = configDefault || null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (environment) {
|
|
131
|
+
console.log(`Starting interactive session with environment: ${environment}`);
|
|
132
|
+
} else {
|
|
133
|
+
console.log('Starting interactive session (no environment specified ā relying on server default)');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Display agent override if specified
|
|
137
|
+
if (envVars.CODER_AGENT) {
|
|
138
|
+
console.log(`Agent override: ${envVars.CODER_AGENT}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Display other environment variables if any
|
|
142
|
+
const otherEnvVars = Object.entries(envVars).filter(([k]) => k !== 'CODER_AGENT');
|
|
143
|
+
if (otherEnvVars.length > 0) {
|
|
144
|
+
console.log(`Environment variables: ${otherEnvVars.map(([k, v]) => `${k}=${v}`).join(', ')}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Display branch selections if any
|
|
148
|
+
if (Object.keys(branches).length > 0) {
|
|
149
|
+
console.log(`Branch selections: ${Object.entries(branches).map(([k, v]) => `${k}=${v}`).join(', ')}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const requestBody = {};
|
|
153
|
+
if (environment) {
|
|
154
|
+
requestBody.environment = environment;
|
|
155
|
+
}
|
|
156
|
+
if (Object.keys(envVars).length > 0) {
|
|
157
|
+
requestBody.env_vars = envVars;
|
|
158
|
+
}
|
|
159
|
+
if (Object.keys(branches).length > 0) {
|
|
160
|
+
requestBody.branches = branches;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Capture local repository state if requested
|
|
164
|
+
if (withLocalState) {
|
|
165
|
+
console.log('\nš Capturing local repository state...');
|
|
166
|
+
requestBody.capture_local_state = true;
|
|
167
|
+
requestBody.source_path = process.cwd();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const data = await request('/containers/interactive', {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
body: JSON.stringify(requestBody)
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
console.log(`\nā Container started successfully`);
|
|
176
|
+
console.log(` Container ID: ${data.containerId}`);
|
|
177
|
+
console.log(` Environment: ${data.environment}`);
|
|
178
|
+
console.log(` Image: ${data.image}`);
|
|
179
|
+
console.log(` Default Agent: ${data.defaultAgent}`);
|
|
180
|
+
|
|
181
|
+
// Show local state summary if it was captured
|
|
182
|
+
if (data.localState) {
|
|
183
|
+
console.log('');
|
|
184
|
+
console.log('š¦ Local State Applied:');
|
|
185
|
+
console.log(` Repositories: ${data.localState.repos_found.join(', ')}`);
|
|
186
|
+
|
|
187
|
+
// Show details for each repo
|
|
188
|
+
for (const repo of data.localState.repos_found) {
|
|
189
|
+
const repoState = data.localState.repositories[repo];
|
|
190
|
+
if (repoState) {
|
|
191
|
+
const details = [];
|
|
192
|
+
if (repoState.current_branch) {
|
|
193
|
+
details.push(`branch: ${repoState.current_branch}`);
|
|
194
|
+
}
|
|
195
|
+
if (repoState.unstaged) {
|
|
196
|
+
const fileCount = (repoState.unstaged.match(/^diff --git/gm) || []).length;
|
|
197
|
+
details.push(`${fileCount} unstaged`);
|
|
198
|
+
}
|
|
199
|
+
if (repoState.staged) {
|
|
200
|
+
const fileCount = (repoState.staged.match(/^diff --git/gm) || []).length;
|
|
201
|
+
details.push(`${fileCount} staged`);
|
|
202
|
+
}
|
|
203
|
+
if (repoState.untracked_files && Object.keys(repoState.untracked_files).length > 0) {
|
|
204
|
+
details.push(`${Object.keys(repoState.untracked_files).length} untracked`);
|
|
205
|
+
}
|
|
206
|
+
if (details.length > 0) {
|
|
207
|
+
console.log(` ${repo}: ${details.join(', ')}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (data.localState.repos_missing && data.localState.repos_missing.length > 0) {
|
|
213
|
+
console.log(` Missing: ${data.localState.repos_missing.join(', ')} (using defaults)`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
console.log(' ā
State capture complete');
|
|
217
|
+
|
|
218
|
+
// Give container time to apply state (it runs during startup)
|
|
219
|
+
console.log(' ā³ Waiting for state application...');
|
|
220
|
+
|
|
221
|
+
// Poll for summary with retries (up to 30 seconds for large workspaces)
|
|
222
|
+
let summaryCheck = null;
|
|
223
|
+
const maxRetries = 30; // 30 seconds total
|
|
224
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
225
|
+
try {
|
|
226
|
+
summaryCheck = await request(`/containers/${data.containerId}/local-state-summary`);
|
|
227
|
+
break; // Got the summary, exit loop
|
|
228
|
+
} catch (err) {
|
|
229
|
+
// Summary not ready yet, wait and retry
|
|
230
|
+
if (i < maxRetries - 1) {
|
|
231
|
+
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second between retries
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Display summary results
|
|
237
|
+
if (summaryCheck && summaryCheck.has_warnings) {
|
|
238
|
+
console.log('');
|
|
239
|
+
console.log(' ā ļø State applied with warnings:');
|
|
240
|
+
for (const warning of summaryCheck.warnings || []) {
|
|
241
|
+
console.log(` ${warning}`);
|
|
242
|
+
}
|
|
243
|
+
console.log('');
|
|
244
|
+
console.log(' š” Check git status in container to verify state');
|
|
245
|
+
} else if (summaryCheck) {
|
|
246
|
+
console.log(' ā
State applied successfully');
|
|
247
|
+
} else {
|
|
248
|
+
// Timed out waiting for summary
|
|
249
|
+
console.log(' ā ļø Timed out waiting for state application');
|
|
250
|
+
console.log(' š” Check /task-output/log.txt in container for details');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Save this as the last container
|
|
255
|
+
await saveLastContainerId(data.containerId);
|
|
256
|
+
|
|
257
|
+
if (noAttach) {
|
|
258
|
+
console.log('\nTo connect manually, run:');
|
|
259
|
+
console.log(` coder attach`);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const cmd = startShell ? null : data.shellCommand;
|
|
264
|
+
const descriptiveAction = startShell ? 'Connecting to shell' : `Starting ${data.defaultAgent}`;
|
|
265
|
+
|
|
266
|
+
console.log(`\n${descriptiveAction}...\n`);
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const exitCode = await connectTerminal(data.containerId, cmd);
|
|
270
|
+
|
|
271
|
+
if (exitCode === 0) {
|
|
272
|
+
console.log(`\nā Disconnected from container ${data.containerId}`);
|
|
273
|
+
} else if (exitCode === 130) {
|
|
274
|
+
console.log(`\nā Terminal session interrupted`);
|
|
275
|
+
} else {
|
|
276
|
+
console.error(`\nā Terminal session exited with code ${exitCode}`);
|
|
277
|
+
}
|
|
278
|
+
} catch (error) {
|
|
279
|
+
console.error(`\nā Failed to connect to terminal: ${error.message}`);
|
|
280
|
+
console.log('\nContainer remains running.');
|
|
281
|
+
console.log('\nTo connect manually, run:');
|
|
282
|
+
console.log(` coder attach`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
console.log('\nContainer is still running and will auto-cleanup after 2 hours of inactivity.');
|
|
287
|
+
console.log(`Container ID: ${data.containerId}`);
|
|
288
|
+
console.log('\nTo reconnect to this container:');
|
|
289
|
+
console.log(` coder attach`);
|
|
290
|
+
console.log('\nTo start a fresh session:');
|
|
291
|
+
console.log(` coder start${environment ? ' ' + environment : ' <environment>'}`);
|
|
292
|
+
}
|