gbos 1.3.20 → 1.3.22
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/package.json +1 -1
- package/src/cli.js +106 -2
- package/src/commands/gitlab.js +470 -0
- package/src/commands/registry.js +314 -0
- package/src/commands/tasks.js +257 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -6,7 +6,9 @@ const program = new Command();
|
|
|
6
6
|
const authCommand = require('./commands/auth');
|
|
7
7
|
const connectCommand = require('./commands/connect');
|
|
8
8
|
const logoutCommand = require('./commands/logout');
|
|
9
|
-
const { tasksCommand, nextTaskCommand, continueCommand, fallbackCommand, addTaskCommand } = require('./commands/tasks');
|
|
9
|
+
const { tasksCommand, nextTaskCommand, continueCommand, fallbackCommand, addTaskCommand, completedCommand } = require('./commands/tasks');
|
|
10
|
+
const { syncStartCommand, syncStopCommand, syncStatusCommand, syncNowCommand, repoCreateCommand, repoListCommand, repoCloneCommand } = require('./commands/gitlab');
|
|
11
|
+
const { registryLoginCommand, registryImagesCommand, registryPushCommand, registryPullCommand } = require('./commands/registry');
|
|
10
12
|
const config = require('./lib/config');
|
|
11
13
|
const { displayStatus, printBanner } = require('./lib/display');
|
|
12
14
|
|
|
@@ -118,6 +120,13 @@ program
|
|
|
118
120
|
.description('Cancel work from the current task and revert to last completed state')
|
|
119
121
|
.action(fallbackCommand);
|
|
120
122
|
|
|
123
|
+
program
|
|
124
|
+
.command('completed')
|
|
125
|
+
.alias('done')
|
|
126
|
+
.description('Complete current task: commit, push to GitLab (creates repo if needed), and mark task done')
|
|
127
|
+
.option('-m, --message <message>', 'Custom commit message')
|
|
128
|
+
.action(completedCommand);
|
|
129
|
+
|
|
121
130
|
program
|
|
122
131
|
.command('add_task')
|
|
123
132
|
.alias('add')
|
|
@@ -130,6 +139,101 @@ program
|
|
|
130
139
|
.option('-a, --all', 'Clear all stored data including machine ID')
|
|
131
140
|
.action(logoutCommand);
|
|
132
141
|
|
|
142
|
+
// ==================== GitLab Commands ====================
|
|
143
|
+
|
|
144
|
+
const gitlabCmd = program
|
|
145
|
+
.command('gitlab')
|
|
146
|
+
.description('GitLab integration commands');
|
|
147
|
+
|
|
148
|
+
// GitLab Sync subcommands
|
|
149
|
+
const gitlabSync = gitlabCmd
|
|
150
|
+
.command('sync')
|
|
151
|
+
.description('Auto-sync repository with GitLab');
|
|
152
|
+
|
|
153
|
+
gitlabSync
|
|
154
|
+
.command('start')
|
|
155
|
+
.description('Start auto-syncing a repository')
|
|
156
|
+
.option('-p, --path <path>', 'Path to repository (defaults to current directory)')
|
|
157
|
+
.option('-i, --interval <seconds>', 'Sync interval in seconds (default: 60)', parseInt)
|
|
158
|
+
.action(syncStartCommand);
|
|
159
|
+
|
|
160
|
+
gitlabSync
|
|
161
|
+
.command('stop')
|
|
162
|
+
.description('Stop auto-syncing a repository')
|
|
163
|
+
.option('-p, --path <path>', 'Path to repository (defaults to current directory)')
|
|
164
|
+
.option('-a, --all', 'Stop all active syncs')
|
|
165
|
+
.action(syncStopCommand);
|
|
166
|
+
|
|
167
|
+
gitlabSync
|
|
168
|
+
.command('status')
|
|
169
|
+
.description('Show status of all active syncs')
|
|
170
|
+
.action(syncStatusCommand);
|
|
171
|
+
|
|
172
|
+
gitlabSync
|
|
173
|
+
.command('now')
|
|
174
|
+
.description('Force an immediate sync')
|
|
175
|
+
.option('-p, --path <path>', 'Path to repository (defaults to current directory)')
|
|
176
|
+
.action(syncNowCommand);
|
|
177
|
+
|
|
178
|
+
// GitLab Repo subcommands
|
|
179
|
+
const gitlabRepo = gitlabCmd
|
|
180
|
+
.command('repo')
|
|
181
|
+
.description('GitLab repository management');
|
|
182
|
+
|
|
183
|
+
gitlabRepo
|
|
184
|
+
.command('create <name>')
|
|
185
|
+
.description('Create a new GitLab repository')
|
|
186
|
+
.option('--private', 'Create as private repository (default)')
|
|
187
|
+
.option('--public', 'Create as public repository')
|
|
188
|
+
.option('-d, --description <description>', 'Repository description')
|
|
189
|
+
.option('--readme', 'Initialize with README')
|
|
190
|
+
.action(repoCreateCommand);
|
|
191
|
+
|
|
192
|
+
gitlabRepo
|
|
193
|
+
.command('list')
|
|
194
|
+
.description('List GitLab repositories')
|
|
195
|
+
.option('-a, --all', 'Show all accessible repositories (not just owned)')
|
|
196
|
+
.action(repoListCommand);
|
|
197
|
+
|
|
198
|
+
gitlabRepo
|
|
199
|
+
.command('clone <name>')
|
|
200
|
+
.description('Clone a GitLab repository')
|
|
201
|
+
.option('--ssh', 'Use SSH URL instead of HTTPS')
|
|
202
|
+
.option('-d, --dir <directory>', 'Target directory name')
|
|
203
|
+
.action(repoCloneCommand);
|
|
204
|
+
|
|
205
|
+
// ==================== Registry Commands ====================
|
|
206
|
+
|
|
207
|
+
const registryCmd = program
|
|
208
|
+
.command('registry')
|
|
209
|
+
.description('GitLab Container Registry commands');
|
|
210
|
+
|
|
211
|
+
registryCmd
|
|
212
|
+
.command('login')
|
|
213
|
+
.description('Login to GitLab Container Registry')
|
|
214
|
+
.option('-r, --registry <url>', 'Registry URL (defaults to registry.gitlab.com)')
|
|
215
|
+
.action(registryLoginCommand);
|
|
216
|
+
|
|
217
|
+
registryCmd
|
|
218
|
+
.command('images <project>')
|
|
219
|
+
.description('List container images in a project')
|
|
220
|
+
.option('-t, --tags', 'Show tags for each image')
|
|
221
|
+
.action(registryImagesCommand);
|
|
222
|
+
|
|
223
|
+
registryCmd
|
|
224
|
+
.command('push <image>')
|
|
225
|
+
.description('Push an image to GitLab Container Registry')
|
|
226
|
+
.option('-p, --project <project>', 'GitLab project path (e.g., group/project)')
|
|
227
|
+
.option('-r, --registry <url>', 'Registry URL (defaults to registry.gitlab.com)')
|
|
228
|
+
.action(registryPushCommand);
|
|
229
|
+
|
|
230
|
+
registryCmd
|
|
231
|
+
.command('pull <image>')
|
|
232
|
+
.description('Pull an image from GitLab Container Registry')
|
|
233
|
+
.option('-p, --project <project>', 'GitLab project path (e.g., group/project)')
|
|
234
|
+
.option('-r, --registry <url>', 'Registry URL (defaults to registry.gitlab.com)')
|
|
235
|
+
.action(registryPullCommand);
|
|
236
|
+
|
|
133
237
|
program
|
|
134
238
|
.command('help [command]')
|
|
135
239
|
.description('Display help for a specific command')
|
|
@@ -140,7 +244,7 @@ program
|
|
|
140
244
|
cmd.outputHelp();
|
|
141
245
|
} else {
|
|
142
246
|
console.log(`Unknown command: ${command}`);
|
|
143
|
-
console.log('Available commands: auth, connect, disconnect, status, tasks, next, continue, fallback, add_task, logout, help');
|
|
247
|
+
console.log('Available commands: auth, connect, disconnect, status, tasks, next, continue, completed, fallback, add_task, logout, gitlab, registry, help');
|
|
144
248
|
}
|
|
145
249
|
} else {
|
|
146
250
|
program.outputHelp();
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
const config = require('../lib/config');
|
|
2
|
+
const { displayMessageBox, fg, LOGO_PURPLE, LOGO_LIGHT, RESET, BOLD, DIM, getTerminalWidth } = require('../lib/display');
|
|
3
|
+
const { exec, spawn } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
// Colors
|
|
9
|
+
const CYAN = '\x1b[36m';
|
|
10
|
+
const GREEN = '\x1b[32m';
|
|
11
|
+
const YELLOW = '\x1b[33m';
|
|
12
|
+
const RED = '\x1b[31m';
|
|
13
|
+
|
|
14
|
+
// GitLab configuration
|
|
15
|
+
const GITLAB_CONFIG_FILE = path.join(os.homedir(), '.gbos', 'gitlab.json');
|
|
16
|
+
const SYNC_PID_DIR = path.join(os.homedir(), '.gbos', 'sync');
|
|
17
|
+
|
|
18
|
+
// Load GitLab config
|
|
19
|
+
function loadGitLabConfig() {
|
|
20
|
+
try {
|
|
21
|
+
if (fs.existsSync(GITLAB_CONFIG_FILE)) {
|
|
22
|
+
return JSON.parse(fs.readFileSync(GITLAB_CONFIG_FILE, 'utf8'));
|
|
23
|
+
}
|
|
24
|
+
} catch (e) {
|
|
25
|
+
// Ignore errors
|
|
26
|
+
}
|
|
27
|
+
return { syncs: {}, repos: [] };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Save GitLab config
|
|
31
|
+
function saveGitLabConfig(config) {
|
|
32
|
+
const dir = path.dirname(GITLAB_CONFIG_FILE);
|
|
33
|
+
if (!fs.existsSync(dir)) {
|
|
34
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
fs.writeFileSync(GITLAB_CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Get GitLab URL from session or config
|
|
40
|
+
function getGitLabUrl() {
|
|
41
|
+
const session = config.loadSession();
|
|
42
|
+
return session?.gitlab_url || process.env.GITLAB_URL || 'https://gitlab.com';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Get GitLab token
|
|
46
|
+
function getGitLabToken() {
|
|
47
|
+
const session = config.loadSession();
|
|
48
|
+
return session?.gitlab_token || process.env.GITLAB_TOKEN || null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Execute git command
|
|
52
|
+
function execGit(args, cwd = process.cwd()) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
exec(`git ${args}`, { cwd }, (error, stdout, stderr) => {
|
|
55
|
+
if (error) {
|
|
56
|
+
reject(new Error(stderr || error.message));
|
|
57
|
+
} else {
|
|
58
|
+
resolve(stdout.trim());
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ==================== SYNC COMMANDS ====================
|
|
65
|
+
|
|
66
|
+
// Start auto-sync for a directory
|
|
67
|
+
async function syncStartCommand(options) {
|
|
68
|
+
const targetPath = options.path || process.cwd();
|
|
69
|
+
const absolutePath = path.resolve(targetPath);
|
|
70
|
+
|
|
71
|
+
// Verify it's a git repository
|
|
72
|
+
try {
|
|
73
|
+
await execGit('rev-parse --is-inside-work-tree', absolutePath);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
displayMessageBox('Not a Git Repository', `${absolutePath} is not a git repository.`, 'error');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Get remote URL
|
|
80
|
+
let remoteUrl;
|
|
81
|
+
try {
|
|
82
|
+
remoteUrl = await execGit('remote get-url origin', absolutePath);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
displayMessageBox('No Remote', 'No git remote "origin" configured.', 'error');
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const interval = options.interval || 60; // Default 60 seconds
|
|
89
|
+
|
|
90
|
+
// Check if already syncing
|
|
91
|
+
const gitlabConfig = loadGitLabConfig();
|
|
92
|
+
if (gitlabConfig.syncs[absolutePath]) {
|
|
93
|
+
console.log(`\n${YELLOW}!${RESET} Sync already active for ${absolutePath}`);
|
|
94
|
+
console.log(` ${DIM}Use "gbos gitlab sync stop" to stop it first.${RESET}\n`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Create sync PID directory
|
|
99
|
+
if (!fs.existsSync(SYNC_PID_DIR)) {
|
|
100
|
+
fs.mkdirSync(SYNC_PID_DIR, { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Start background sync process
|
|
104
|
+
const syncId = Date.now().toString();
|
|
105
|
+
const pidFile = path.join(SYNC_PID_DIR, `${syncId}.pid`);
|
|
106
|
+
|
|
107
|
+
// Create sync script
|
|
108
|
+
const syncScript = `
|
|
109
|
+
while true; do
|
|
110
|
+
cd "${absolutePath}"
|
|
111
|
+
git fetch origin 2>/dev/null
|
|
112
|
+
git add -A 2>/dev/null
|
|
113
|
+
CHANGES=$(git status --porcelain)
|
|
114
|
+
if [ -n "$CHANGES" ]; then
|
|
115
|
+
git commit -m "Auto-sync: $(date '+%Y-%m-%d %H:%M:%S')" 2>/dev/null
|
|
116
|
+
git push origin HEAD 2>/dev/null
|
|
117
|
+
fi
|
|
118
|
+
sleep ${interval}
|
|
119
|
+
done
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
const child = spawn('bash', ['-c', syncScript], {
|
|
123
|
+
detached: true,
|
|
124
|
+
stdio: 'ignore',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
child.unref();
|
|
128
|
+
|
|
129
|
+
// Save PID
|
|
130
|
+
fs.writeFileSync(pidFile, child.pid.toString(), 'utf8');
|
|
131
|
+
|
|
132
|
+
// Update config
|
|
133
|
+
gitlabConfig.syncs[absolutePath] = {
|
|
134
|
+
syncId,
|
|
135
|
+
pid: child.pid,
|
|
136
|
+
pidFile,
|
|
137
|
+
remote: remoteUrl,
|
|
138
|
+
interval,
|
|
139
|
+
startedAt: new Date().toISOString(),
|
|
140
|
+
};
|
|
141
|
+
saveGitLabConfig(gitlabConfig);
|
|
142
|
+
|
|
143
|
+
console.log(`\n${GREEN}✓${RESET} ${BOLD}Auto-sync started${RESET}`);
|
|
144
|
+
console.log(` ${DIM}Path:${RESET} ${absolutePath}`);
|
|
145
|
+
console.log(` ${DIM}Remote:${RESET} ${remoteUrl}`);
|
|
146
|
+
console.log(` ${DIM}Interval:${RESET} ${interval} seconds`);
|
|
147
|
+
console.log(` ${DIM}PID:${RESET} ${child.pid}\n`);
|
|
148
|
+
console.log(` ${DIM}Use "gbos gitlab sync stop" to stop syncing.${RESET}\n`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Stop auto-sync
|
|
152
|
+
async function syncStopCommand(options) {
|
|
153
|
+
const targetPath = options.path ? path.resolve(options.path) : process.cwd();
|
|
154
|
+
|
|
155
|
+
const gitlabConfig = loadGitLabConfig();
|
|
156
|
+
|
|
157
|
+
// Find sync for this path or stop all
|
|
158
|
+
let syncsToStop = [];
|
|
159
|
+
|
|
160
|
+
if (options.all) {
|
|
161
|
+
syncsToStop = Object.entries(gitlabConfig.syncs);
|
|
162
|
+
} else if (gitlabConfig.syncs[targetPath]) {
|
|
163
|
+
syncsToStop = [[targetPath, gitlabConfig.syncs[targetPath]]];
|
|
164
|
+
} else {
|
|
165
|
+
console.log(`\n${DIM}No active sync for ${targetPath}${RESET}\n`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const [syncPath, syncInfo] of syncsToStop) {
|
|
170
|
+
try {
|
|
171
|
+
// Kill the process
|
|
172
|
+
process.kill(syncInfo.pid, 'SIGTERM');
|
|
173
|
+
} catch (e) {
|
|
174
|
+
// Process may already be dead
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Remove PID file
|
|
178
|
+
try {
|
|
179
|
+
if (fs.existsSync(syncInfo.pidFile)) {
|
|
180
|
+
fs.unlinkSync(syncInfo.pidFile);
|
|
181
|
+
}
|
|
182
|
+
} catch (e) {
|
|
183
|
+
// Ignore
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Remove from config
|
|
187
|
+
delete gitlabConfig.syncs[syncPath];
|
|
188
|
+
|
|
189
|
+
console.log(`${GREEN}✓${RESET} Stopped sync for ${syncPath}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
saveGitLabConfig(gitlabConfig);
|
|
193
|
+
console.log('');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Show sync status
|
|
197
|
+
async function syncStatusCommand() {
|
|
198
|
+
const gitlabConfig = loadGitLabConfig();
|
|
199
|
+
const syncs = Object.entries(gitlabConfig.syncs);
|
|
200
|
+
|
|
201
|
+
const termWidth = getTerminalWidth();
|
|
202
|
+
const tableWidth = Math.min(100, termWidth - 4);
|
|
203
|
+
|
|
204
|
+
console.log(`\n${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}`);
|
|
205
|
+
console.log(`${BOLD} Active Syncs${RESET}`);
|
|
206
|
+
console.log(`${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}\n`);
|
|
207
|
+
|
|
208
|
+
if (syncs.length === 0) {
|
|
209
|
+
console.log(` ${DIM}No active syncs.${RESET}`);
|
|
210
|
+
console.log(` ${DIM}Use "gbos gitlab sync start" to start syncing a repository.${RESET}\n`);
|
|
211
|
+
} else {
|
|
212
|
+
for (const [syncPath, syncInfo] of syncs) {
|
|
213
|
+
// Check if process is still running
|
|
214
|
+
let isRunning = false;
|
|
215
|
+
try {
|
|
216
|
+
process.kill(syncInfo.pid, 0);
|
|
217
|
+
isRunning = true;
|
|
218
|
+
} catch (e) {
|
|
219
|
+
isRunning = false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const status = isRunning ? `${GREEN}● running${RESET}` : `${RED}● stopped${RESET}`;
|
|
223
|
+
console.log(` ${status} ${BOLD}${syncPath}${RESET}`);
|
|
224
|
+
console.log(` ${DIM}Remote: ${syncInfo.remote}${RESET}`);
|
|
225
|
+
console.log(` ${DIM}Interval: ${syncInfo.interval}s | PID: ${syncInfo.pid}${RESET}`);
|
|
226
|
+
console.log(` ${DIM}Started: ${new Date(syncInfo.startedAt).toLocaleString()}${RESET}`);
|
|
227
|
+
console.log('');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
console.log(`${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}\n`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Force immediate sync
|
|
235
|
+
async function syncNowCommand(options) {
|
|
236
|
+
const targetPath = options.path ? path.resolve(options.path) : process.cwd();
|
|
237
|
+
|
|
238
|
+
// Verify it's a git repository
|
|
239
|
+
try {
|
|
240
|
+
await execGit('rev-parse --is-inside-work-tree', targetPath);
|
|
241
|
+
} catch (e) {
|
|
242
|
+
displayMessageBox('Not a Git Repository', `${targetPath} is not a git repository.`, 'error');
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
console.log(`\n${DIM}Syncing ${targetPath}...${RESET}\n`);
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
// Fetch from remote
|
|
250
|
+
console.log(` ${DIM}Fetching from origin...${RESET}`);
|
|
251
|
+
await execGit('fetch origin', targetPath);
|
|
252
|
+
|
|
253
|
+
// Stage all changes
|
|
254
|
+
console.log(` ${DIM}Staging changes...${RESET}`);
|
|
255
|
+
await execGit('add -A', targetPath);
|
|
256
|
+
|
|
257
|
+
// Check for changes
|
|
258
|
+
const status = await execGit('status --porcelain', targetPath);
|
|
259
|
+
|
|
260
|
+
if (status) {
|
|
261
|
+
// Commit changes
|
|
262
|
+
const commitMsg = `Manual sync: ${new Date().toISOString()}`;
|
|
263
|
+
console.log(` ${DIM}Committing changes...${RESET}`);
|
|
264
|
+
await execGit(`commit -m "${commitMsg}"`, targetPath);
|
|
265
|
+
|
|
266
|
+
// Push to remote
|
|
267
|
+
console.log(` ${DIM}Pushing to origin...${RESET}`);
|
|
268
|
+
await execGit('push origin HEAD', targetPath);
|
|
269
|
+
|
|
270
|
+
console.log(`\n${GREEN}✓${RESET} ${BOLD}Sync complete${RESET} - Changes pushed to remote.\n`);
|
|
271
|
+
} else {
|
|
272
|
+
console.log(`\n${GREEN}✓${RESET} ${BOLD}Already in sync${RESET} - No local changes to push.\n`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Pull any remote changes
|
|
276
|
+
try {
|
|
277
|
+
console.log(` ${DIM}Pulling remote changes...${RESET}`);
|
|
278
|
+
await execGit('pull origin HEAD --rebase', targetPath);
|
|
279
|
+
} catch (e) {
|
|
280
|
+
// May fail if there are conflicts
|
|
281
|
+
console.log(` ${YELLOW}!${RESET} ${DIM}Could not pull remote changes (may have conflicts).${RESET}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
} catch (error) {
|
|
285
|
+
displayMessageBox('Sync Failed', error.message, 'error');
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ==================== REPO COMMANDS ====================
|
|
291
|
+
|
|
292
|
+
// Create a new repository
|
|
293
|
+
async function repoCreateCommand(name, options) {
|
|
294
|
+
const token = getGitLabToken();
|
|
295
|
+
const gitlabUrl = getGitLabUrl();
|
|
296
|
+
|
|
297
|
+
if (!token) {
|
|
298
|
+
displayMessageBox('Not Configured', 'GitLab token not configured. Set GITLAB_TOKEN environment variable.', 'error');
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const visibility = options.private ? 'private' : (options.public ? 'public' : 'private');
|
|
303
|
+
const description = options.description || '';
|
|
304
|
+
|
|
305
|
+
console.log(`\n${DIM}Creating repository "${name}" on GitLab...${RESET}\n`);
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
const response = await fetch(`${gitlabUrl}/api/v4/projects`, {
|
|
309
|
+
method: 'POST',
|
|
310
|
+
headers: {
|
|
311
|
+
'PRIVATE-TOKEN': token,
|
|
312
|
+
'Content-Type': 'application/json',
|
|
313
|
+
},
|
|
314
|
+
body: JSON.stringify({
|
|
315
|
+
name,
|
|
316
|
+
description,
|
|
317
|
+
visibility,
|
|
318
|
+
initialize_with_readme: options.readme || false,
|
|
319
|
+
}),
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
if (!response.ok) {
|
|
323
|
+
const error = await response.json();
|
|
324
|
+
throw new Error(error.message || `Failed to create repository: ${response.status}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const repo = await response.json();
|
|
328
|
+
|
|
329
|
+
console.log(`${GREEN}✓${RESET} ${BOLD}Repository created${RESET}`);
|
|
330
|
+
console.log(` ${DIM}Name:${RESET} ${repo.name}`);
|
|
331
|
+
console.log(` ${DIM}URL:${RESET} ${repo.web_url}`);
|
|
332
|
+
console.log(` ${DIM}Clone SSH:${RESET} ${repo.ssh_url_to_repo}`);
|
|
333
|
+
console.log(` ${DIM}Clone HTTP:${RESET} ${repo.http_url_to_repo}`);
|
|
334
|
+
console.log(` ${DIM}Visibility:${RESET} ${repo.visibility}\n`);
|
|
335
|
+
|
|
336
|
+
// Save to config
|
|
337
|
+
const gitlabConfig = loadGitLabConfig();
|
|
338
|
+
gitlabConfig.repos.push({
|
|
339
|
+
id: repo.id,
|
|
340
|
+
name: repo.name,
|
|
341
|
+
path: repo.path_with_namespace,
|
|
342
|
+
url: repo.web_url,
|
|
343
|
+
ssh_url: repo.ssh_url_to_repo,
|
|
344
|
+
http_url: repo.http_url_to_repo,
|
|
345
|
+
createdAt: new Date().toISOString(),
|
|
346
|
+
});
|
|
347
|
+
saveGitLabConfig(gitlabConfig);
|
|
348
|
+
|
|
349
|
+
} catch (error) {
|
|
350
|
+
displayMessageBox('Failed', error.message, 'error');
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// List repositories
|
|
356
|
+
async function repoListCommand(options) {
|
|
357
|
+
const token = getGitLabToken();
|
|
358
|
+
const gitlabUrl = getGitLabUrl();
|
|
359
|
+
|
|
360
|
+
if (!token) {
|
|
361
|
+
displayMessageBox('Not Configured', 'GitLab token not configured. Set GITLAB_TOKEN environment variable.', 'error');
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
console.log(`\n${DIM}Fetching repositories from GitLab...${RESET}\n`);
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const owned = options.all ? '' : '&owned=true';
|
|
369
|
+
const response = await fetch(`${gitlabUrl}/api/v4/projects?per_page=50${owned}`, {
|
|
370
|
+
headers: {
|
|
371
|
+
'PRIVATE-TOKEN': token,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
if (!response.ok) {
|
|
376
|
+
throw new Error(`Failed to list repositories: ${response.status}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const repos = await response.json();
|
|
380
|
+
|
|
381
|
+
const termWidth = getTerminalWidth();
|
|
382
|
+
const tableWidth = Math.min(100, termWidth - 4);
|
|
383
|
+
|
|
384
|
+
console.log(`${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}`);
|
|
385
|
+
console.log(`${BOLD} GitLab Repositories${RESET}`);
|
|
386
|
+
console.log(`${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}\n`);
|
|
387
|
+
|
|
388
|
+
if (repos.length === 0) {
|
|
389
|
+
console.log(` ${DIM}No repositories found.${RESET}\n`);
|
|
390
|
+
} else {
|
|
391
|
+
repos.forEach((repo, i) => {
|
|
392
|
+
const visibility = repo.visibility === 'private' ? `${YELLOW}private${RESET}` : `${GREEN}${repo.visibility}${RESET}`;
|
|
393
|
+
console.log(` ${CYAN}${i + 1}.${RESET} ${BOLD}${repo.path_with_namespace}${RESET} (${visibility})`);
|
|
394
|
+
if (repo.description) {
|
|
395
|
+
console.log(` ${DIM}${repo.description.substring(0, 60)}${repo.description.length > 60 ? '...' : ''}${RESET}`);
|
|
396
|
+
}
|
|
397
|
+
console.log(` ${DIM}${repo.http_url_to_repo}${RESET}`);
|
|
398
|
+
if (i < repos.length - 1) console.log('');
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
console.log(`\n${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}`);
|
|
403
|
+
console.log(`${DIM} Total: ${repos.length} repository(ies)${RESET}\n`);
|
|
404
|
+
|
|
405
|
+
} catch (error) {
|
|
406
|
+
displayMessageBox('Failed', error.message, 'error');
|
|
407
|
+
process.exit(1);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Clone a repository
|
|
412
|
+
async function repoCloneCommand(name, options) {
|
|
413
|
+
const token = getGitLabToken();
|
|
414
|
+
const gitlabUrl = getGitLabUrl();
|
|
415
|
+
|
|
416
|
+
if (!token) {
|
|
417
|
+
displayMessageBox('Not Configured', 'GitLab token not configured. Set GITLAB_TOKEN environment variable.', 'error');
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
console.log(`\n${DIM}Searching for repository "${name}"...${RESET}\n`);
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
// Search for repository
|
|
425
|
+
const response = await fetch(`${gitlabUrl}/api/v4/projects?search=${encodeURIComponent(name)}`, {
|
|
426
|
+
headers: {
|
|
427
|
+
'PRIVATE-TOKEN': token,
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
if (!response.ok) {
|
|
432
|
+
throw new Error(`Failed to search repositories: ${response.status}`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const repos = await response.json();
|
|
436
|
+
|
|
437
|
+
if (repos.length === 0) {
|
|
438
|
+
displayMessageBox('Not Found', `Repository "${name}" not found.`, 'error');
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Find exact match or use first result
|
|
443
|
+
const repo = repos.find(r => r.name === name || r.path_with_namespace === name) || repos[0];
|
|
444
|
+
|
|
445
|
+
const cloneUrl = options.ssh ? repo.ssh_url_to_repo : repo.http_url_to_repo;
|
|
446
|
+
const targetDir = options.dir || repo.name;
|
|
447
|
+
|
|
448
|
+
console.log(` ${DIM}Cloning ${repo.path_with_namespace}...${RESET}`);
|
|
449
|
+
console.log(` ${DIM}URL: ${cloneUrl}${RESET}\n`);
|
|
450
|
+
|
|
451
|
+
await execGit(`clone ${cloneUrl} ${targetDir}`);
|
|
452
|
+
|
|
453
|
+
console.log(`${GREEN}✓${RESET} ${BOLD}Repository cloned${RESET}`);
|
|
454
|
+
console.log(` ${DIM}Location:${RESET} ${path.resolve(targetDir)}\n`);
|
|
455
|
+
|
|
456
|
+
} catch (error) {
|
|
457
|
+
displayMessageBox('Clone Failed', error.message, 'error');
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
module.exports = {
|
|
463
|
+
syncStartCommand,
|
|
464
|
+
syncStopCommand,
|
|
465
|
+
syncStatusCommand,
|
|
466
|
+
syncNowCommand,
|
|
467
|
+
repoCreateCommand,
|
|
468
|
+
repoListCommand,
|
|
469
|
+
repoCloneCommand,
|
|
470
|
+
};
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
const config = require('../lib/config');
|
|
2
|
+
const { displayMessageBox, fg, LOGO_PURPLE, LOGO_LIGHT, RESET, BOLD, DIM, getTerminalWidth } = require('../lib/display');
|
|
3
|
+
const { exec, spawn } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
// Colors
|
|
9
|
+
const CYAN = '\x1b[36m';
|
|
10
|
+
const GREEN = '\x1b[32m';
|
|
11
|
+
const YELLOW = '\x1b[33m';
|
|
12
|
+
const RED = '\x1b[31m';
|
|
13
|
+
|
|
14
|
+
// Get GitLab URL from session or config
|
|
15
|
+
function getGitLabUrl() {
|
|
16
|
+
const session = config.loadSession();
|
|
17
|
+
return session?.gitlab_url || process.env.GITLAB_URL || 'https://gitlab.com';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Get GitLab token
|
|
21
|
+
function getGitLabToken() {
|
|
22
|
+
const session = config.loadSession();
|
|
23
|
+
return session?.gitlab_token || process.env.GITLAB_TOKEN || null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Get registry URL from GitLab URL
|
|
27
|
+
function getRegistryUrl() {
|
|
28
|
+
const gitlabUrl = getGitLabUrl();
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(gitlabUrl);
|
|
31
|
+
return `registry.${url.hostname}`;
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return 'registry.gitlab.com';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Execute shell command
|
|
38
|
+
function execCommand(command, options = {}) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
exec(command, options, (error, stdout, stderr) => {
|
|
41
|
+
if (error) {
|
|
42
|
+
reject(new Error(stderr || error.message));
|
|
43
|
+
} else {
|
|
44
|
+
resolve({ stdout: stdout.trim(), stderr: stderr.trim() });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ==================== REGISTRY COMMANDS ====================
|
|
51
|
+
|
|
52
|
+
// Login to GitLab Container Registry
|
|
53
|
+
async function registryLoginCommand(options) {
|
|
54
|
+
const token = getGitLabToken();
|
|
55
|
+
const registryUrl = options.registry || getRegistryUrl();
|
|
56
|
+
|
|
57
|
+
if (!token) {
|
|
58
|
+
displayMessageBox('Not Configured', 'GitLab token not configured. Set GITLAB_TOKEN environment variable or run "gbos auth" first.', 'error');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log(`\n${DIM}Logging into GitLab Container Registry...${RESET}\n`);
|
|
63
|
+
console.log(` ${DIM}Registry:${RESET} ${registryUrl}`);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
// Use docker login with token
|
|
67
|
+
const loginProcess = spawn('docker', ['login', registryUrl, '-u', 'oauth2', '--password-stdin'], {
|
|
68
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
let stdout = '';
|
|
72
|
+
let stderr = '';
|
|
73
|
+
|
|
74
|
+
loginProcess.stdout.on('data', (data) => {
|
|
75
|
+
stdout += data.toString();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
loginProcess.stderr.on('data', (data) => {
|
|
79
|
+
stderr += data.toString();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Write token to stdin
|
|
83
|
+
loginProcess.stdin.write(token);
|
|
84
|
+
loginProcess.stdin.end();
|
|
85
|
+
|
|
86
|
+
await new Promise((resolve, reject) => {
|
|
87
|
+
loginProcess.on('close', (code) => {
|
|
88
|
+
if (code === 0) {
|
|
89
|
+
resolve();
|
|
90
|
+
} else {
|
|
91
|
+
reject(new Error(stderr || `Docker login failed with code ${code}`));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
console.log(`\n${GREEN}✓${RESET} ${BOLD}Login successful${RESET}`);
|
|
97
|
+
console.log(` ${DIM}You can now push and pull images from ${registryUrl}${RESET}\n`);
|
|
98
|
+
|
|
99
|
+
} catch (error) {
|
|
100
|
+
// Check if docker is installed
|
|
101
|
+
try {
|
|
102
|
+
await execCommand('docker --version');
|
|
103
|
+
} catch (e) {
|
|
104
|
+
displayMessageBox('Docker Not Found', 'Docker is not installed or not in PATH. Please install Docker first.', 'error');
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
displayMessageBox('Login Failed', error.message, 'error');
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// List container images in a project
|
|
114
|
+
async function registryImagesCommand(project, options) {
|
|
115
|
+
const token = getGitLabToken();
|
|
116
|
+
const gitlabUrl = getGitLabUrl();
|
|
117
|
+
|
|
118
|
+
if (!token) {
|
|
119
|
+
displayMessageBox('Not Configured', 'GitLab token not configured. Set GITLAB_TOKEN environment variable.', 'error');
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// URL encode the project path
|
|
124
|
+
const encodedProject = encodeURIComponent(project);
|
|
125
|
+
|
|
126
|
+
console.log(`\n${DIM}Fetching container images for "${project}"...${RESET}\n`);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
// First get the project to verify it exists
|
|
130
|
+
const projectResponse = await fetch(`${gitlabUrl}/api/v4/projects/${encodedProject}`, {
|
|
131
|
+
headers: {
|
|
132
|
+
'PRIVATE-TOKEN': token,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (!projectResponse.ok) {
|
|
137
|
+
if (projectResponse.status === 404) {
|
|
138
|
+
throw new Error(`Project "${project}" not found. Use the full path like "group/project".`);
|
|
139
|
+
}
|
|
140
|
+
throw new Error(`Failed to fetch project: ${projectResponse.status}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const projectData = await projectResponse.json();
|
|
144
|
+
|
|
145
|
+
// Get container repositories
|
|
146
|
+
const reposResponse = await fetch(`${gitlabUrl}/api/v4/projects/${projectData.id}/registry/repositories`, {
|
|
147
|
+
headers: {
|
|
148
|
+
'PRIVATE-TOKEN': token,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!reposResponse.ok) {
|
|
153
|
+
throw new Error(`Failed to fetch repositories: ${reposResponse.status}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const repos = await reposResponse.json();
|
|
157
|
+
|
|
158
|
+
const termWidth = getTerminalWidth();
|
|
159
|
+
const tableWidth = Math.min(100, termWidth - 4);
|
|
160
|
+
|
|
161
|
+
console.log(`${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}`);
|
|
162
|
+
console.log(`${BOLD} Container Images - ${projectData.path_with_namespace}${RESET}`);
|
|
163
|
+
console.log(`${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}\n`);
|
|
164
|
+
|
|
165
|
+
if (repos.length === 0) {
|
|
166
|
+
console.log(` ${DIM}No container images found.${RESET}`);
|
|
167
|
+
console.log(` ${DIM}Push an image with: docker push ${getRegistryUrl()}/${projectData.path_with_namespace}/<image>:<tag>${RESET}\n`);
|
|
168
|
+
} else {
|
|
169
|
+
for (const repo of repos) {
|
|
170
|
+
console.log(` ${CYAN}●${RESET} ${BOLD}${repo.path}${RESET}`);
|
|
171
|
+
console.log(` ${DIM}ID: ${repo.id} | Location: ${repo.location}${RESET}`);
|
|
172
|
+
|
|
173
|
+
// Get tags for this repository
|
|
174
|
+
if (options.tags) {
|
|
175
|
+
try {
|
|
176
|
+
const tagsResponse = await fetch(`${gitlabUrl}/api/v4/projects/${projectData.id}/registry/repositories/${repo.id}/tags`, {
|
|
177
|
+
headers: {
|
|
178
|
+
'PRIVATE-TOKEN': token,
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (tagsResponse.ok) {
|
|
183
|
+
const tags = await tagsResponse.json();
|
|
184
|
+
if (tags.length > 0) {
|
|
185
|
+
const tagList = tags.slice(0, 10).map(t => t.name).join(', ');
|
|
186
|
+
console.log(` ${DIM}Tags: ${tagList}${tags.length > 10 ? ` (+${tags.length - 10} more)` : ''}${RESET}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch (e) {
|
|
190
|
+
// Ignore tag fetch errors
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
console.log('');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log(`${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}`);
|
|
199
|
+
console.log(`${DIM} Total: ${repos.length} image(s)${RESET}\n`);
|
|
200
|
+
|
|
201
|
+
} catch (error) {
|
|
202
|
+
displayMessageBox('Failed', error.message, 'error');
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Push an image to GitLab Container Registry
|
|
208
|
+
async function registryPushCommand(image, options) {
|
|
209
|
+
const registryUrl = options.registry || getRegistryUrl();
|
|
210
|
+
|
|
211
|
+
console.log(`\n${DIM}Pushing image to GitLab Container Registry...${RESET}\n`);
|
|
212
|
+
|
|
213
|
+
// Check if the image name already includes the registry
|
|
214
|
+
let fullImage = image;
|
|
215
|
+
if (!image.includes('/') || !image.includes('.')) {
|
|
216
|
+
// User provided just an image name, need project path
|
|
217
|
+
if (!options.project) {
|
|
218
|
+
displayMessageBox('Project Required', 'Please specify the project with --project or use full image path.\n\nExample: gbos registry push myimage:latest --project group/project\nOr: gbos registry push registry.gitlab.com/group/project/image:tag', 'error');
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
fullImage = `${registryUrl}/${options.project}/${image}`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log(` ${DIM}Image:${RESET} ${image}`);
|
|
225
|
+
console.log(` ${DIM}Target:${RESET} ${fullImage}\n`);
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
// Tag the image if needed
|
|
229
|
+
if (fullImage !== image) {
|
|
230
|
+
console.log(` ${DIM}Tagging image...${RESET}`);
|
|
231
|
+
await execCommand(`docker tag ${image} ${fullImage}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Push the image
|
|
235
|
+
console.log(` ${DIM}Pushing to registry...${RESET}\n`);
|
|
236
|
+
|
|
237
|
+
const pushProcess = spawn('docker', ['push', fullImage], {
|
|
238
|
+
stdio: 'inherit',
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
await new Promise((resolve, reject) => {
|
|
242
|
+
pushProcess.on('close', (code) => {
|
|
243
|
+
if (code === 0) {
|
|
244
|
+
resolve();
|
|
245
|
+
} else {
|
|
246
|
+
reject(new Error(`Push failed with exit code ${code}`));
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
console.log(`\n${GREEN}✓${RESET} ${BOLD}Image pushed successfully${RESET}`);
|
|
252
|
+
console.log(` ${DIM}Location:${RESET} ${fullImage}\n`);
|
|
253
|
+
|
|
254
|
+
} catch (error) {
|
|
255
|
+
// Check if docker is installed
|
|
256
|
+
try {
|
|
257
|
+
await execCommand('docker --version');
|
|
258
|
+
} catch (e) {
|
|
259
|
+
displayMessageBox('Docker Not Found', 'Docker is not installed or not in PATH. Please install Docker first.', 'error');
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
displayMessageBox('Push Failed', error.message, 'error');
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Pull an image from GitLab Container Registry
|
|
269
|
+
async function registryPullCommand(image, options) {
|
|
270
|
+
const registryUrl = options.registry || getRegistryUrl();
|
|
271
|
+
|
|
272
|
+
console.log(`\n${DIM}Pulling image from GitLab Container Registry...${RESET}\n`);
|
|
273
|
+
|
|
274
|
+
// Check if the image name already includes the registry
|
|
275
|
+
let fullImage = image;
|
|
276
|
+
if (!image.includes('/') || !image.includes('.')) {
|
|
277
|
+
if (!options.project) {
|
|
278
|
+
displayMessageBox('Project Required', 'Please specify the project with --project or use full image path.', 'error');
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
fullImage = `${registryUrl}/${options.project}/${image}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
console.log(` ${DIM}Pulling:${RESET} ${fullImage}\n`);
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const pullProcess = spawn('docker', ['pull', fullImage], {
|
|
288
|
+
stdio: 'inherit',
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
await new Promise((resolve, reject) => {
|
|
292
|
+
pullProcess.on('close', (code) => {
|
|
293
|
+
if (code === 0) {
|
|
294
|
+
resolve();
|
|
295
|
+
} else {
|
|
296
|
+
reject(new Error(`Pull failed with exit code ${code}`));
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
console.log(`\n${GREEN}✓${RESET} ${BOLD}Image pulled successfully${RESET}\n`);
|
|
302
|
+
|
|
303
|
+
} catch (error) {
|
|
304
|
+
displayMessageBox('Pull Failed', error.message, 'error');
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
module.exports = {
|
|
310
|
+
registryLoginCommand,
|
|
311
|
+
registryImagesCommand,
|
|
312
|
+
registryPushCommand,
|
|
313
|
+
registryPullCommand,
|
|
314
|
+
};
|
package/src/commands/tasks.js
CHANGED
|
@@ -2,6 +2,9 @@ const api = require('../lib/api');
|
|
|
2
2
|
const config = require('../lib/config');
|
|
3
3
|
const { displayMessageBox, printBanner, printStatusTable, fg, LOGO_LIGHT, LOGO_PURPLE, RESET, BOLD, DIM, getTerminalWidth } = require('../lib/display');
|
|
4
4
|
const readline = require('readline');
|
|
5
|
+
const { exec, spawn } = require('child_process');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
5
8
|
|
|
6
9
|
// Colors for prompts
|
|
7
10
|
const CYAN = '\x1b[36m';
|
|
@@ -697,11 +700,265 @@ async function addTaskCommand() {
|
|
|
697
700
|
}
|
|
698
701
|
}
|
|
699
702
|
|
|
703
|
+
// Execute git command
|
|
704
|
+
function execGit(args, cwd = process.cwd()) {
|
|
705
|
+
return new Promise((resolve, reject) => {
|
|
706
|
+
exec(`git ${args}`, { cwd, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
707
|
+
if (error) {
|
|
708
|
+
reject(new Error(stderr || error.message));
|
|
709
|
+
} else {
|
|
710
|
+
resolve(stdout.trim());
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Get GitLab URL and token from session or env
|
|
717
|
+
function getGitLabConfig() {
|
|
718
|
+
const session = config.loadSession();
|
|
719
|
+
return {
|
|
720
|
+
url: session?.gitlab_url || process.env.GITLAB_URL || 'https://gitlab.com',
|
|
721
|
+
token: session?.gitlab_token || process.env.GITLAB_TOKEN || null,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Completed command - commit, push, and complete task
|
|
726
|
+
async function completedCommand(options) {
|
|
727
|
+
const cwd = process.cwd();
|
|
728
|
+
const dirName = path.basename(cwd);
|
|
729
|
+
|
|
730
|
+
console.log(`\n${DIM}Processing completion...${RESET}\n`);
|
|
731
|
+
|
|
732
|
+
// Step 1: Check if current directory is a git repo
|
|
733
|
+
let isGitRepo = false;
|
|
734
|
+
let hasRemote = false;
|
|
735
|
+
let remoteUrl = '';
|
|
736
|
+
|
|
737
|
+
try {
|
|
738
|
+
await execGit('rev-parse --is-inside-work-tree', cwd);
|
|
739
|
+
isGitRepo = true;
|
|
740
|
+
console.log(` ${GREEN}✓${RESET} Git repository detected`);
|
|
741
|
+
|
|
742
|
+
// Check for remote
|
|
743
|
+
try {
|
|
744
|
+
remoteUrl = await execGit('remote get-url origin', cwd);
|
|
745
|
+
hasRemote = true;
|
|
746
|
+
console.log(` ${GREEN}✓${RESET} Remote: ${remoteUrl}`);
|
|
747
|
+
} catch (e) {
|
|
748
|
+
hasRemote = false;
|
|
749
|
+
console.log(` ${YELLOW}!${RESET} No remote configured`);
|
|
750
|
+
}
|
|
751
|
+
} catch (e) {
|
|
752
|
+
isGitRepo = false;
|
|
753
|
+
console.log(` ${YELLOW}!${RESET} Not a git repository`);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Step 2: Initialize git if needed
|
|
757
|
+
if (!isGitRepo) {
|
|
758
|
+
console.log(`\n ${DIM}Initializing git repository...${RESET}`);
|
|
759
|
+
try {
|
|
760
|
+
await execGit('init', cwd);
|
|
761
|
+
isGitRepo = true;
|
|
762
|
+
console.log(` ${GREEN}✓${RESET} Git repository initialized`);
|
|
763
|
+
} catch (e) {
|
|
764
|
+
displayMessageBox('Error', `Failed to initialize git: ${e.message}`, 'error');
|
|
765
|
+
process.exit(1);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Step 3: Create GitLab repo if no remote
|
|
770
|
+
if (!hasRemote) {
|
|
771
|
+
const gitlab = getGitLabConfig();
|
|
772
|
+
|
|
773
|
+
if (!gitlab.token) {
|
|
774
|
+
console.log(`\n ${YELLOW}!${RESET} No GitLab token configured.`);
|
|
775
|
+
console.log(` ${DIM}Set GITLAB_TOKEN environment variable to auto-create repos.${RESET}`);
|
|
776
|
+
console.log(` ${DIM}Skipping remote setup...${RESET}\n`);
|
|
777
|
+
} else {
|
|
778
|
+
console.log(`\n ${DIM}Creating GitLab repository "${dirName}"...${RESET}`);
|
|
779
|
+
|
|
780
|
+
try {
|
|
781
|
+
const response = await fetch(`${gitlab.url}/api/v4/projects`, {
|
|
782
|
+
method: 'POST',
|
|
783
|
+
headers: {
|
|
784
|
+
'PRIVATE-TOKEN': gitlab.token,
|
|
785
|
+
'Content-Type': 'application/json',
|
|
786
|
+
},
|
|
787
|
+
body: JSON.stringify({
|
|
788
|
+
name: dirName,
|
|
789
|
+
visibility: 'private',
|
|
790
|
+
initialize_with_readme: false,
|
|
791
|
+
}),
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
if (response.ok) {
|
|
795
|
+
const repo = await response.json();
|
|
796
|
+
remoteUrl = repo.ssh_url_to_repo || repo.http_url_to_repo;
|
|
797
|
+
|
|
798
|
+
// Add remote
|
|
799
|
+
await execGit(`remote add origin ${remoteUrl}`, cwd);
|
|
800
|
+
hasRemote = true;
|
|
801
|
+
|
|
802
|
+
console.log(` ${GREEN}✓${RESET} GitLab repository created: ${repo.web_url}`);
|
|
803
|
+
console.log(` ${GREEN}✓${RESET} Remote added: ${remoteUrl}`);
|
|
804
|
+
} else {
|
|
805
|
+
const error = await response.json();
|
|
806
|
+
if (error.message && error.message.includes('has already been taken')) {
|
|
807
|
+
// Repo exists, try to find and add it
|
|
808
|
+
console.log(` ${YELLOW}!${RESET} Repository "${dirName}" already exists on GitLab`);
|
|
809
|
+
|
|
810
|
+
// Try to get user info and construct URL
|
|
811
|
+
try {
|
|
812
|
+
const userResponse = await fetch(`${gitlab.url}/api/v4/user`, {
|
|
813
|
+
headers: { 'PRIVATE-TOKEN': gitlab.token },
|
|
814
|
+
});
|
|
815
|
+
if (userResponse.ok) {
|
|
816
|
+
const user = await userResponse.json();
|
|
817
|
+
remoteUrl = `git@${new URL(gitlab.url).hostname}:${user.username}/${dirName}.git`;
|
|
818
|
+
await execGit(`remote add origin ${remoteUrl}`, cwd);
|
|
819
|
+
hasRemote = true;
|
|
820
|
+
console.log(` ${GREEN}✓${RESET} Remote added: ${remoteUrl}`);
|
|
821
|
+
}
|
|
822
|
+
} catch (e) {
|
|
823
|
+
console.log(` ${DIM}Could not auto-configure remote. Add manually with:${RESET}`);
|
|
824
|
+
console.log(` ${DIM}git remote add origin <your-repo-url>${RESET}`);
|
|
825
|
+
}
|
|
826
|
+
} else {
|
|
827
|
+
console.log(` ${YELLOW}!${RESET} Failed to create repo: ${error.message || response.status}`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
} catch (e) {
|
|
831
|
+
console.log(` ${YELLOW}!${RESET} Failed to create GitLab repo: ${e.message}`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Step 4: Stage all changes
|
|
837
|
+
console.log(`\n ${DIM}Staging changes...${RESET}`);
|
|
838
|
+
try {
|
|
839
|
+
await execGit('add -A', cwd);
|
|
840
|
+
console.log(` ${GREEN}✓${RESET} Changes staged`);
|
|
841
|
+
} catch (e) {
|
|
842
|
+
console.log(` ${YELLOW}!${RESET} Failed to stage: ${e.message}`);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Step 5: Check for changes to commit
|
|
846
|
+
let hasChanges = false;
|
|
847
|
+
try {
|
|
848
|
+
const status = await execGit('status --porcelain', cwd);
|
|
849
|
+
hasChanges = status.length > 0;
|
|
850
|
+
} catch (e) {
|
|
851
|
+
// Assume changes exist
|
|
852
|
+
hasChanges = true;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Step 6: Commit changes
|
|
856
|
+
if (hasChanges) {
|
|
857
|
+
console.log(` ${DIM}Committing changes...${RESET}`);
|
|
858
|
+
|
|
859
|
+
// Get current task for commit message
|
|
860
|
+
let taskInfo = '';
|
|
861
|
+
try {
|
|
862
|
+
if (config.isAuthenticated() && config.getConnection()) {
|
|
863
|
+
const currentResponse = await api.getCurrentTask();
|
|
864
|
+
const task = currentResponse.data?.task || currentResponse.data;
|
|
865
|
+
if (task) {
|
|
866
|
+
taskInfo = task.task_key ? `[${task.task_key}] ` : `[Task #${task.id}] `;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
} catch (e) {
|
|
870
|
+
// No task info available
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
|
874
|
+
const commitMessage = options.message || `${taskInfo}Completed: ${timestamp}`;
|
|
875
|
+
|
|
876
|
+
try {
|
|
877
|
+
await execGit(`commit -m "${commitMessage.replace(/"/g, '\\"')}"`, cwd);
|
|
878
|
+
console.log(` ${GREEN}✓${RESET} Changes committed: "${commitMessage}"`);
|
|
879
|
+
} catch (e) {
|
|
880
|
+
if (e.message.includes('nothing to commit')) {
|
|
881
|
+
console.log(` ${DIM}No changes to commit${RESET}`);
|
|
882
|
+
} else {
|
|
883
|
+
console.log(` ${YELLOW}!${RESET} Commit failed: ${e.message}`);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
} else {
|
|
887
|
+
console.log(` ${DIM}No changes to commit${RESET}`);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Step 7: Push to remote
|
|
891
|
+
if (hasRemote) {
|
|
892
|
+
console.log(` ${DIM}Pushing to remote...${RESET}`);
|
|
893
|
+
try {
|
|
894
|
+
// Try to get current branch
|
|
895
|
+
let branch = 'main';
|
|
896
|
+
try {
|
|
897
|
+
branch = await execGit('rev-parse --abbrev-ref HEAD', cwd);
|
|
898
|
+
} catch (e) {
|
|
899
|
+
branch = 'main';
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Push with upstream tracking
|
|
903
|
+
try {
|
|
904
|
+
await execGit(`push -u origin ${branch}`, cwd);
|
|
905
|
+
console.log(` ${GREEN}✓${RESET} Pushed to origin/${branch}`);
|
|
906
|
+
} catch (e) {
|
|
907
|
+
// If push fails, try setting upstream
|
|
908
|
+
if (e.message.includes('no upstream branch')) {
|
|
909
|
+
await execGit(`push --set-upstream origin ${branch}`, cwd);
|
|
910
|
+
console.log(` ${GREEN}✓${RESET} Pushed to origin/${branch}`);
|
|
911
|
+
} else {
|
|
912
|
+
throw e;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
} catch (e) {
|
|
916
|
+
console.log(` ${YELLOW}!${RESET} Push failed: ${e.message}`);
|
|
917
|
+
console.log(` ${DIM}You may need to push manually with: git push -u origin main${RESET}`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Step 8: Mark GBOS task as complete (if authenticated and connected)
|
|
922
|
+
if (config.isAuthenticated() && config.getConnection()) {
|
|
923
|
+
try {
|
|
924
|
+
const currentResponse = await api.getCurrentTask();
|
|
925
|
+
const task = currentResponse.data?.task || currentResponse.data;
|
|
926
|
+
|
|
927
|
+
if (task && task.status === 'in_progress') {
|
|
928
|
+
console.log(`\n ${DIM}Marking GBOS task as complete...${RESET}`);
|
|
929
|
+
await api.completeTask(task.id, {
|
|
930
|
+
completion_notes: options.message || 'Completed via gbos completed command',
|
|
931
|
+
});
|
|
932
|
+
console.log(` ${GREEN}✓${RESET} Task "${task.title || task.id}" marked as complete`);
|
|
933
|
+
}
|
|
934
|
+
} catch (e) {
|
|
935
|
+
// Task completion is optional, don't fail the whole command
|
|
936
|
+
if (e.status !== 404) {
|
|
937
|
+
console.log(` ${DIM}Note: Could not update GBOS task status${RESET}`);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Final summary
|
|
943
|
+
const termWidth = getTerminalWidth();
|
|
944
|
+
const tableWidth = Math.min(60, termWidth - 4);
|
|
945
|
+
|
|
946
|
+
console.log(`\n${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}`);
|
|
947
|
+
console.log(`${GREEN}✓${RESET} ${BOLD}Completion finished!${RESET}`);
|
|
948
|
+
console.log(`${fg(...LOGO_PURPLE)}${'─'.repeat(tableWidth)}${RESET}\n`);
|
|
949
|
+
|
|
950
|
+
if (remoteUrl) {
|
|
951
|
+
console.log(` ${DIM}Repository:${RESET} ${remoteUrl}`);
|
|
952
|
+
}
|
|
953
|
+
console.log(` ${DIM}Run "gbos continue" to start the next task.${RESET}\n`);
|
|
954
|
+
}
|
|
955
|
+
|
|
700
956
|
module.exports = {
|
|
701
957
|
tasksCommand,
|
|
702
958
|
nextTaskCommand,
|
|
703
959
|
continueCommand,
|
|
704
960
|
fallbackCommand,
|
|
705
961
|
addTaskCommand,
|
|
962
|
+
completedCommand,
|
|
706
963
|
generateAgentPrompt,
|
|
707
964
|
};
|