@mrpatronz/nexusflow 0.1.9 ā 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/add-repo.d.ts +12 -0
- package/dist/commands/add-repo.d.ts.map +1 -0
- package/dist/commands/add-repo.js +126 -0
- package/dist/commands/add-repo.js.map +1 -0
- package/dist/commands/remove.d.ts +11 -0
- package/dist/commands/remove.d.ts.map +1 -0
- package/dist/commands/remove.js +82 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/core/workspace.d.ts +20 -0
- package/dist/core/workspace.d.ts.map +1 -1
- package/dist/core/workspace.js +127 -1
- package/dist/core/workspace.js.map +1 -1
- package/dist/core/worktree.d.ts +3 -2
- package/dist/core/worktree.d.ts.map +1 -1
- package/dist/core/worktree.js +9 -3
- package/dist/core/worktree.js.map +1 -1
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/base.js +9 -0
- package/dist/generators/base.js.map +1 -1
- package/dist/gui/assets/index-C8W6FoPa.js +21 -0
- package/dist/gui/assets/index-CJn2LW8K.css +2 -0
- package/dist/gui/index.html +2 -2
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +70 -2
- package/dist/server.js.map +1 -1
- package/dist/utils/update-check.d.ts +11 -0
- package/dist/utils/update-check.d.ts.map +1 -1
- package/dist/utils/update-check.js +105 -0
- package/dist/utils/update-check.js.map +1 -1
- package/extension/package-lock.json +8 -8
- package/extension/package.json +1 -1
- package/gui/src/App.tsx +268 -6
- package/gui/src/features/workspace/WorkspaceList.tsx +58 -8
- package/package.json +1 -1
- package/src/commands/add-repo.ts +141 -0
- package/src/commands/remove.ts +90 -0
- package/src/core/workspace.ts +142 -2
- package/src/core/worktree.ts +10 -2
- package/src/generators/base.ts +9 -0
- package/src/index.ts +39 -0
- package/src/server.ts +73 -2
- package/src/utils/update-check.ts +116 -0
- package/dist/gui/assets/index-7IR7ZrSG.css +0 -2
- package/dist/gui/assets/index-BfNp4LNj.js +0 -21
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module commands/remove
|
|
3
|
+
* Deletes a NexusFlow workspace and cleanly removes/prunes its git worktrees.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import { select, confirm } from '@inquirer/prompts';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import * as fs from 'node:fs/promises';
|
|
11
|
+
|
|
12
|
+
import { loadConfig } from '../core/config.js';
|
|
13
|
+
import { listWorkspaces, deleteWorkspace } from '../core/workspace.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Executes the remove command.
|
|
17
|
+
*
|
|
18
|
+
* @param workspaceArg - Optional workspace path or branch name from CLI.
|
|
19
|
+
*/
|
|
20
|
+
export async function removeCommand(workspaceArg?: string): Promise<void> {
|
|
21
|
+
console.log(chalk.bold.red('\nšļø NexusFlow ā Deleting Workspace\n'));
|
|
22
|
+
|
|
23
|
+
const config = await loadConfig();
|
|
24
|
+
let workspacePath: string | null = null;
|
|
25
|
+
let workspaceName = '';
|
|
26
|
+
|
|
27
|
+
if (workspaceArg) {
|
|
28
|
+
const resolvedPath = path.isAbsolute(workspaceArg)
|
|
29
|
+
? workspaceArg
|
|
30
|
+
: path.resolve(config.workspacesDir, workspaceArg);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await fs.access(resolvedPath);
|
|
34
|
+
workspacePath = resolvedPath;
|
|
35
|
+
workspaceName = path.basename(resolvedPath);
|
|
36
|
+
} catch {
|
|
37
|
+
const directPath = path.join(config.workspacesDir, workspaceArg);
|
|
38
|
+
try {
|
|
39
|
+
await fs.access(directPath);
|
|
40
|
+
workspacePath = directPath;
|
|
41
|
+
workspaceName = workspaceArg;
|
|
42
|
+
} catch {
|
|
43
|
+
console.error(chalk.red(`ā Workspace not found: ${workspaceArg}`));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
const workspaces = await listWorkspaces(config.workspacesDir);
|
|
49
|
+
|
|
50
|
+
if (workspaces.length === 0) {
|
|
51
|
+
console.log(chalk.yellow('No workspaces found.\n'));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const selected = await select({
|
|
56
|
+
message: 'Select a workspace to delete:',
|
|
57
|
+
choices: workspaces.map((ws) => ({
|
|
58
|
+
name: `${ws.branchName} ${chalk.dim(`(${ws.repos.length} repos)`)}`,
|
|
59
|
+
value: ws,
|
|
60
|
+
})),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
workspacePath = selected.workspacePath;
|
|
64
|
+
workspaceName = selected.branchName;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!workspacePath) return;
|
|
68
|
+
|
|
69
|
+
const confirmDelete = await confirm({
|
|
70
|
+
message: `Are you absolutely sure you want to delete the workspace "${workspaceName}"?\n This will FORCE remove all associated git worktrees and delete the folder from disk.`,
|
|
71
|
+
default: false,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!confirmDelete) {
|
|
75
|
+
console.log(chalk.yellow('\nCancelled.\n'));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const spinner = ora('Deleting workspace and pruning git worktrees...').start();
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await deleteWorkspace(workspacePath);
|
|
83
|
+
spinner.succeed(`Successfully deleted workspace ${chalk.bold(workspaceName)}`);
|
|
84
|
+
console.log();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
spinner.fail(`Failed to delete workspace ${workspaceName}`);
|
|
87
|
+
console.error(chalk.red(` ${error instanceof Error ? error.message : String(error)}`));
|
|
88
|
+
console.log();
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/core/workspace.ts
CHANGED
|
@@ -9,8 +9,12 @@ import * as fs from 'node:fs/promises';
|
|
|
9
9
|
import * as path from 'node:path';
|
|
10
10
|
import { execa } from 'execa';
|
|
11
11
|
|
|
12
|
-
import type { Feature, RepoInfo } from '../types.js';
|
|
13
|
-
import { createWorktree } from './worktree.js';
|
|
12
|
+
import type { Feature, RepoInfo, WorkspaceContext } from '../types.js';
|
|
13
|
+
import { createWorktree, removeWorktree } from './worktree.js';
|
|
14
|
+
import { detectDefaultBranch } from '../utils/git.js';
|
|
15
|
+
import { analyzeAllRepos } from '../analyzers/index.js';
|
|
16
|
+
import { generateContextFiles } from '../generators/index.js';
|
|
17
|
+
import { packWorkspace } from './packer.js';
|
|
14
18
|
|
|
15
19
|
/** Name of the per-workspace manifest file. */
|
|
16
20
|
const MANIFEST_FILE = 'nexusflow.json';
|
|
@@ -161,3 +165,139 @@ export async function loadFeatureConfig(
|
|
|
161
165
|
return null;
|
|
162
166
|
}
|
|
163
167
|
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Resolves a repo path to a full RepoInfo object.
|
|
171
|
+
*
|
|
172
|
+
* @param repoPath - Absolute path to the original repository.
|
|
173
|
+
*/
|
|
174
|
+
export async function resolveRepoInfo(repoPath: string): Promise<RepoInfo> {
|
|
175
|
+
const defaultBranch = await detectDefaultBranch(repoPath);
|
|
176
|
+
return {
|
|
177
|
+
name: path.basename(repoPath),
|
|
178
|
+
path: repoPath,
|
|
179
|
+
defaultBranch,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Deletes a workspace cleanly, removing all associated git worktrees first,
|
|
185
|
+
* then deleting the directory from disk.
|
|
186
|
+
*
|
|
187
|
+
* @param workspacePath - Absolute path to the workspace directory.
|
|
188
|
+
*/
|
|
189
|
+
export async function deleteWorkspace(
|
|
190
|
+
workspacePath: string,
|
|
191
|
+
): Promise<void> {
|
|
192
|
+
const feature = await loadFeatureConfig(workspacePath);
|
|
193
|
+
if (feature) {
|
|
194
|
+
for (const repoPath of feature.repos) {
|
|
195
|
+
const repoName = path.basename(repoPath);
|
|
196
|
+
const worktreePath = path.join(workspacePath, repoName);
|
|
197
|
+
try {
|
|
198
|
+
await removeWorktree(repoPath, worktreePath, true);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.warn(`Warning: failed to remove worktree for ${repoName} in ${repoPath}:`, error);
|
|
201
|
+
try {
|
|
202
|
+
await execa('git', ['worktree', 'prune'], { cwd: repoPath });
|
|
203
|
+
} catch (pruneError) {
|
|
204
|
+
console.warn(`Warning: failed to prune worktrees in ${repoPath}:`, pruneError);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
// Manifest is missing. Try to detect worktrees by scanning subdirectories
|
|
210
|
+
try {
|
|
211
|
+
const entries = await fs.readdir(workspacePath, { withFileTypes: true });
|
|
212
|
+
for (const entry of entries) {
|
|
213
|
+
if (entry.isDirectory()) {
|
|
214
|
+
const subPath = path.join(workspacePath, entry.name);
|
|
215
|
+
const gitFilePath = path.join(subPath, '.git');
|
|
216
|
+
try {
|
|
217
|
+
const stat = await fs.stat(gitFilePath);
|
|
218
|
+
if (stat.isFile()) {
|
|
219
|
+
const content = await fs.readFile(gitFilePath, 'utf-8');
|
|
220
|
+
const match = content.match(/gitdir:\s*(.+)\.git\/worktrees/);
|
|
221
|
+
if (match && match[1]) {
|
|
222
|
+
const mainRepoPath = path.resolve(match[1].trim());
|
|
223
|
+
await removeWorktree(mainRepoPath, subPath, true);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} catch {}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch {}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Delete the directory itself
|
|
233
|
+
try {
|
|
234
|
+
await fs.rm(workspacePath, { recursive: true, force: true });
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error(`Failed to delete workspace directory ${workspacePath}:`, error);
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Adds a repository to an existing workspace.
|
|
243
|
+
*
|
|
244
|
+
* @param workspacePath - Absolute path to the workspace directory.
|
|
245
|
+
* @param repoPath - Absolute path to the repository to add.
|
|
246
|
+
*/
|
|
247
|
+
export async function addRepoToWorkspace(
|
|
248
|
+
workspacePath: string,
|
|
249
|
+
repoPath: string,
|
|
250
|
+
): Promise<void> {
|
|
251
|
+
const feature = await loadFeatureConfig(workspacePath);
|
|
252
|
+
if (!feature) {
|
|
253
|
+
throw new Error(`Workspace manifest not found at ${workspacePath}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (feature.repos.includes(repoPath)) {
|
|
257
|
+
throw new Error(`Repository ${repoPath} is already in the workspace`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const newRepoInfo = await resolveRepoInfo(repoPath);
|
|
261
|
+
const worktreeTarget = path.join(workspacePath, newRepoInfo.name);
|
|
262
|
+
|
|
263
|
+
// 1. Create the worktree
|
|
264
|
+
await createWorktree(
|
|
265
|
+
newRepoInfo.path,
|
|
266
|
+
worktreeTarget,
|
|
267
|
+
feature.branchName,
|
|
268
|
+
newRepoInfo.defaultBranch,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// 2. Update manifest
|
|
272
|
+
feature.repos.push(repoPath);
|
|
273
|
+
await saveFeatureConfig(workspacePath, feature);
|
|
274
|
+
|
|
275
|
+
// 3. Update .gitignore at workspace root
|
|
276
|
+
try {
|
|
277
|
+
const gitignorePath = path.join(workspacePath, '.gitignore');
|
|
278
|
+
let gitignoreContent = '';
|
|
279
|
+
try {
|
|
280
|
+
gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
|
|
281
|
+
} catch {}
|
|
282
|
+
|
|
283
|
+
const entry = `/${newRepoInfo.name}/`;
|
|
284
|
+
if (!gitignoreContent.includes(entry)) {
|
|
285
|
+
gitignoreContent = gitignoreContent.trim() + '\n' + entry + '\n';
|
|
286
|
+
await fs.writeFile(gitignorePath, gitignoreContent, 'utf-8');
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.warn('Warning: Failed to update .gitignore:', error);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 4. Re-run analysis, update configs, and repack workspace
|
|
293
|
+
const allRepos = await Promise.all(feature.repos.map(resolveRepoInfo));
|
|
294
|
+
const analysis = await analyzeAllRepos(allRepos);
|
|
295
|
+
const ctx: WorkspaceContext = {
|
|
296
|
+
feature,
|
|
297
|
+
repos: allRepos,
|
|
298
|
+
analysis,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
await generateContextFiles(ctx, feature.assistants, workspacePath);
|
|
302
|
+
await packWorkspace(workspacePath);
|
|
303
|
+
}
|
package/src/core/worktree.ts
CHANGED
|
@@ -90,14 +90,22 @@ export async function createWorktree(
|
|
|
90
90
|
/**
|
|
91
91
|
* Removes an existing git worktree.
|
|
92
92
|
*
|
|
93
|
-
* Runs `git worktree remove <worktreePath>` from the main repo.
|
|
93
|
+
* Runs `git worktree remove [--force] <worktreePath>` from the main repo.
|
|
94
94
|
*
|
|
95
95
|
* @param repoPath - Absolute path to the main repo checkout.
|
|
96
96
|
* @param worktreePath - Absolute path to the worktree to remove.
|
|
97
|
+
* @param force - Whether to force removal (cleanly removes modified files).
|
|
97
98
|
*/
|
|
98
99
|
export async function removeWorktree(
|
|
99
100
|
repoPath: string,
|
|
100
101
|
worktreePath: string,
|
|
102
|
+
force = false,
|
|
101
103
|
): Promise<void> {
|
|
102
|
-
|
|
104
|
+
const args = ['worktree', 'remove'];
|
|
105
|
+
if (force) {
|
|
106
|
+
args.push('--force');
|
|
107
|
+
}
|
|
108
|
+
args.push(worktreePath);
|
|
109
|
+
await execa('git', args, { cwd: repoPath });
|
|
103
110
|
}
|
|
111
|
+
|
package/src/generators/base.ts
CHANGED
|
@@ -157,6 +157,15 @@ ${existingConfigsSection}
|
|
|
157
157
|
${resumptionSection}
|
|
158
158
|
---
|
|
159
159
|
|
|
160
|
+
## Codebase Context (Repomix)
|
|
161
|
+
|
|
162
|
+
A packed, token-efficient version of the entire multi-repo codebase is automatically generated and updated at the workspace root:
|
|
163
|
+
- **File**: \`nexusflow-context.xml\`
|
|
164
|
+
- **Purpose**: Contains the aggregated source code and structure of all projects in the workspace.
|
|
165
|
+
- **Usage**: You can read this file directly to understand relationships or search for code patterns across all projects without needing to manually traverse directories.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
160
169
|
## Task & Step-by-Step Initialization
|
|
161
170
|
|
|
162
171
|
Your very first task upon entering this workspace is to analyze the codebase and document it in a universal reference file:
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,8 @@ import { syncCommand } from './commands/sync.js';
|
|
|
24
24
|
import { commitCommand } from './commands/commit.js';
|
|
25
25
|
import { diffCommand } from './commands/diff.js';
|
|
26
26
|
import { packCommand } from './commands/pack.js';
|
|
27
|
+
import { removeCommand } from './commands/remove.js';
|
|
28
|
+
import { addRepoCommand } from './commands/add-repo.js';
|
|
27
29
|
import { mcpRunCommand, mcpSetupCommand } from './commands/mcp.js';
|
|
28
30
|
import { getCurrentVersion, checkForUpdates, printUpdateBanner } from './utils/update-check.js';
|
|
29
31
|
|
|
@@ -253,6 +255,43 @@ program
|
|
|
253
255
|
}
|
|
254
256
|
});
|
|
255
257
|
|
|
258
|
+
program
|
|
259
|
+
.command('remove')
|
|
260
|
+
.alias('rm')
|
|
261
|
+
.description('Delete a workspace and cleanly prune/remove its git worktrees')
|
|
262
|
+
.argument('[workspace]', 'Workspace name or path')
|
|
263
|
+
.action(async (workspace?: string) => {
|
|
264
|
+
try {
|
|
265
|
+
await removeCommand(workspace);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (error instanceof Error && error.message.includes('User force closed')) {
|
|
268
|
+
console.log('\nCancelled.');
|
|
269
|
+
process.exit(0);
|
|
270
|
+
}
|
|
271
|
+
console.error(error);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
program
|
|
277
|
+
.command('add-repo')
|
|
278
|
+
.alias('add')
|
|
279
|
+
.description('Add a repository to an existing workspace and update configurations')
|
|
280
|
+
.argument('[repo-path]', 'Path to the repository to add')
|
|
281
|
+
.argument('[workspace]', 'Workspace name or path')
|
|
282
|
+
.action(async (repoPath?: string, workspace?: string) => {
|
|
283
|
+
try {
|
|
284
|
+
await addRepoCommand(repoPath, workspace);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (error instanceof Error && error.message.includes('User force closed')) {
|
|
287
|
+
console.log('\nCancelled.');
|
|
288
|
+
process.exit(0);
|
|
289
|
+
}
|
|
290
|
+
console.error(error);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
256
295
|
const mcp = program.command('mcp').description('Manage the NexusFlow MCP Server for AI assistants');
|
|
257
296
|
|
|
258
297
|
mcp
|
package/src/server.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { execa } from 'execa';
|
|
|
15
15
|
|
|
16
16
|
import { loadConfig, saveConfig, getConfigDir } from './core/config.js';
|
|
17
17
|
import { scanForRepos } from './core/scanner.js';
|
|
18
|
-
import { createWorkspace, listWorkspaces, loadFeatureConfig } from './core/workspace.js';
|
|
18
|
+
import { createWorkspace, listWorkspaces, loadFeatureConfig, deleteWorkspace, addRepoToWorkspace } from './core/workspace.js';
|
|
19
19
|
import { analyzeAllRepos } from './analyzers/index.js';
|
|
20
20
|
import { generateContextFiles } from './generators/index.js';
|
|
21
21
|
import { packWorkspace } from './core/packer.js';
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
stopServices,
|
|
31
31
|
loadRunningState,
|
|
32
32
|
} from './orchestration/index.js';
|
|
33
|
-
import { checkForUpdates, getCurrentVersion } from './utils/update-check.js';
|
|
33
|
+
import { checkForUpdates, getCurrentVersion, getToolsStatus } from './utils/update-check.js';
|
|
34
34
|
import type { Feature, RepoInfo, WorkspaceContext } from './types.js';
|
|
35
35
|
|
|
36
36
|
// Resolve static files directory
|
|
@@ -353,6 +353,37 @@ app.get('/api/workspace/create-stream/:jobId', async (c) => {
|
|
|
353
353
|
});
|
|
354
354
|
});
|
|
355
355
|
|
|
356
|
+
// 7.6. Delete workspace
|
|
357
|
+
app.delete('/api/workspace/:id', async (c) => {
|
|
358
|
+
try {
|
|
359
|
+
const id = decodeURIComponent(c.req.param('id'));
|
|
360
|
+
const config = await loadConfig();
|
|
361
|
+
const workspacePath = path.join(config.workspacesDir, id);
|
|
362
|
+
|
|
363
|
+
await deleteWorkspace(workspacePath);
|
|
364
|
+
return c.json({ success: true });
|
|
365
|
+
} catch (error) {
|
|
366
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
367
|
+
return c.json({ error: msg }, 500);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// 7.7. Add repo to workspace
|
|
372
|
+
app.post('/api/workspace/:id/repo', async (c) => {
|
|
373
|
+
try {
|
|
374
|
+
const id = decodeURIComponent(c.req.param('id'));
|
|
375
|
+
const { repoPath } = await c.req.json() as { repoPath: string };
|
|
376
|
+
const config = await loadConfig();
|
|
377
|
+
const workspacePath = path.join(config.workspacesDir, id);
|
|
378
|
+
|
|
379
|
+
await addRepoToWorkspace(workspacePath, repoPath);
|
|
380
|
+
return c.json({ success: true });
|
|
381
|
+
} catch (error) {
|
|
382
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
383
|
+
return c.json({ error: msg }, 500);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
356
387
|
// 8. Open workspace in editor
|
|
357
388
|
app.post('/api/open-editor', async (c) => {
|
|
358
389
|
try {
|
|
@@ -785,6 +816,46 @@ app.get('/api/update-status', async (c) => {
|
|
|
785
816
|
}
|
|
786
817
|
});
|
|
787
818
|
|
|
819
|
+
// 17.5. Check tools updates status
|
|
820
|
+
app.get('/api/updates/tools', async (c) => {
|
|
821
|
+
try {
|
|
822
|
+
const force = c.req.query('force') === 'true';
|
|
823
|
+
const status = await getToolsStatus(force);
|
|
824
|
+
return c.json(status);
|
|
825
|
+
} catch (error) {
|
|
826
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
827
|
+
return c.json({ error: msg }, 500);
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
// 17.6. Install updates for a specific tool
|
|
832
|
+
app.post('/api/updates/install', async (c) => {
|
|
833
|
+
try {
|
|
834
|
+
const { toolId } = await c.req.json() as { toolId: string };
|
|
835
|
+
const tools = [
|
|
836
|
+
{ id: 'nexusflow', cmd: 'npm', args: ['install', '-g', '@mrpatronz/nexusflow'] },
|
|
837
|
+
{ id: 'repomix', cmd: 'npm', args: ['install', '-g', 'repomix'] },
|
|
838
|
+
{ id: 'antigravity', cmd: 'agy', args: ['update'] },
|
|
839
|
+
{ id: 'claude', cmd: 'npm', args: ['install', '-g', '@anthropic-ai/claude-code'] },
|
|
840
|
+
];
|
|
841
|
+
|
|
842
|
+
const target = tools.find(t => t.id === toolId);
|
|
843
|
+
if (!target) {
|
|
844
|
+
return c.json({ error: 'Tool not found' }, 404);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const result = await execa(target.cmd, target.args, { reject: false });
|
|
848
|
+
if (result.exitCode === 0) {
|
|
849
|
+
return c.json({ success: true, output: result.stdout });
|
|
850
|
+
} else {
|
|
851
|
+
return c.json({ error: `Update failed: ${result.stderr || result.stdout}` }, 500);
|
|
852
|
+
}
|
|
853
|
+
} catch (error) {
|
|
854
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
855
|
+
return c.json({ error: msg }, 500);
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
|
|
788
859
|
// 18. Pack workspace codebase and download
|
|
789
860
|
app.get('/api/workspace/:id/pack', async (c) => {
|
|
790
861
|
try {
|
|
@@ -145,3 +145,119 @@ export function printUpdateBanner(status: UpdateStatus): void {
|
|
|
145
145
|
console.log(chalk.yellow(`ā${border}ā`));
|
|
146
146
|
console.log();
|
|
147
147
|
}
|
|
148
|
+
|
|
149
|
+
import { execa } from 'execa';
|
|
150
|
+
|
|
151
|
+
export interface ToolUpdateStatus {
|
|
152
|
+
id: string;
|
|
153
|
+
name: string;
|
|
154
|
+
command: string;
|
|
155
|
+
installed: boolean;
|
|
156
|
+
currentVersion: string;
|
|
157
|
+
latestVersion: string;
|
|
158
|
+
updateAvailable: boolean;
|
|
159
|
+
updateCmd: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function getToolsStatus(force = false): Promise<ToolUpdateStatus[]> {
|
|
163
|
+
const currentVersion = getCurrentVersion();
|
|
164
|
+
const tools = [
|
|
165
|
+
{
|
|
166
|
+
id: 'nexusflow',
|
|
167
|
+
name: 'NexusFlow Engine',
|
|
168
|
+
command: 'nexusflow',
|
|
169
|
+
npmPackage: '@mrpatronz/nexusflow',
|
|
170
|
+
updateCmd: 'npm install -g @mrpatronz/nexusflow',
|
|
171
|
+
getCurrent: async () => currentVersion,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: 'repomix',
|
|
175
|
+
name: 'Repomix (Codebase Packer)',
|
|
176
|
+
command: 'repomix',
|
|
177
|
+
npmPackage: 'repomix',
|
|
178
|
+
updateCmd: 'npm install -g repomix',
|
|
179
|
+
getCurrent: async () => {
|
|
180
|
+
try {
|
|
181
|
+
const res = await execa('repomix', ['--version'], { reject: false });
|
|
182
|
+
if (res.exitCode === 0) return res.stdout.trim();
|
|
183
|
+
} catch {}
|
|
184
|
+
try {
|
|
185
|
+
const res = await execa('npx', ['repomix', '--version'], { reject: false });
|
|
186
|
+
if (res.exitCode === 0) return res.stdout.trim();
|
|
187
|
+
} catch {}
|
|
188
|
+
return '';
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: 'antigravity',
|
|
193
|
+
name: 'Antigravity CLI',
|
|
194
|
+
command: 'agy',
|
|
195
|
+
npmPackage: '',
|
|
196
|
+
updateCmd: 'agy update',
|
|
197
|
+
getCurrent: async () => {
|
|
198
|
+
try {
|
|
199
|
+
const res = await execa('agy', ['--version'], { reject: false });
|
|
200
|
+
if (res.exitCode === 0) return res.stdout.trim();
|
|
201
|
+
} catch {}
|
|
202
|
+
return '';
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: 'claude',
|
|
207
|
+
name: 'Claude Code CLI',
|
|
208
|
+
command: 'claude',
|
|
209
|
+
npmPackage: '@anthropic-ai/claude-code',
|
|
210
|
+
updateCmd: 'npm install -g @anthropic-ai/claude-code',
|
|
211
|
+
getCurrent: async () => {
|
|
212
|
+
try {
|
|
213
|
+
const res = await execa('claude', ['--version'], { reject: false });
|
|
214
|
+
if (res.exitCode === 0) return res.stdout.trim();
|
|
215
|
+
} catch {}
|
|
216
|
+
return '';
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
const results: ToolUpdateStatus[] = [];
|
|
222
|
+
|
|
223
|
+
for (const t of tools) {
|
|
224
|
+
let installed = false;
|
|
225
|
+
let currentVal = '';
|
|
226
|
+
let latestVal = '';
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
currentVal = await t.getCurrent();
|
|
230
|
+
installed = currentVal !== '';
|
|
231
|
+
} catch {}
|
|
232
|
+
|
|
233
|
+
if (installed && t.npmPackage) {
|
|
234
|
+
try {
|
|
235
|
+
const response = await fetch(`https://registry.npmjs.org/${t.npmPackage}/latest`, {
|
|
236
|
+
signal: AbortSignal.timeout(2000),
|
|
237
|
+
});
|
|
238
|
+
if (response.ok) {
|
|
239
|
+
const data = await response.json() as { version: string };
|
|
240
|
+
latestVal = data.version;
|
|
241
|
+
}
|
|
242
|
+
} catch {}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!latestVal) {
|
|
246
|
+
latestVal = currentVal || '1.0.0';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
results.push({
|
|
250
|
+
id: t.id,
|
|
251
|
+
name: t.name,
|
|
252
|
+
command: t.command,
|
|
253
|
+
installed,
|
|
254
|
+
currentVersion: currentVal || 'Not Installed',
|
|
255
|
+
latestVersion: latestVal,
|
|
256
|
+
updateAvailable: installed && isNewerVersion(currentVal, latestVal),
|
|
257
|
+
updateCmd: t.updateCmd,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return results;
|
|
262
|
+
}
|
|
263
|
+
|