@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.
Files changed (46) hide show
  1. package/dist/commands/add-repo.d.ts +12 -0
  2. package/dist/commands/add-repo.d.ts.map +1 -0
  3. package/dist/commands/add-repo.js +126 -0
  4. package/dist/commands/add-repo.js.map +1 -0
  5. package/dist/commands/remove.d.ts +11 -0
  6. package/dist/commands/remove.d.ts.map +1 -0
  7. package/dist/commands/remove.js +82 -0
  8. package/dist/commands/remove.js.map +1 -0
  9. package/dist/core/workspace.d.ts +20 -0
  10. package/dist/core/workspace.d.ts.map +1 -1
  11. package/dist/core/workspace.js +127 -1
  12. package/dist/core/workspace.js.map +1 -1
  13. package/dist/core/worktree.d.ts +3 -2
  14. package/dist/core/worktree.d.ts.map +1 -1
  15. package/dist/core/worktree.js +9 -3
  16. package/dist/core/worktree.js.map +1 -1
  17. package/dist/generators/base.d.ts.map +1 -1
  18. package/dist/generators/base.js +9 -0
  19. package/dist/generators/base.js.map +1 -1
  20. package/dist/gui/assets/index-C8W6FoPa.js +21 -0
  21. package/dist/gui/assets/index-CJn2LW8K.css +2 -0
  22. package/dist/gui/index.html +2 -2
  23. package/dist/index.js +39 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/server.d.ts.map +1 -1
  26. package/dist/server.js +70 -2
  27. package/dist/server.js.map +1 -1
  28. package/dist/utils/update-check.d.ts +11 -0
  29. package/dist/utils/update-check.d.ts.map +1 -1
  30. package/dist/utils/update-check.js +105 -0
  31. package/dist/utils/update-check.js.map +1 -1
  32. package/extension/package-lock.json +8 -8
  33. package/extension/package.json +1 -1
  34. package/gui/src/App.tsx +268 -6
  35. package/gui/src/features/workspace/WorkspaceList.tsx +58 -8
  36. package/package.json +1 -1
  37. package/src/commands/add-repo.ts +141 -0
  38. package/src/commands/remove.ts +90 -0
  39. package/src/core/workspace.ts +142 -2
  40. package/src/core/worktree.ts +10 -2
  41. package/src/generators/base.ts +9 -0
  42. package/src/index.ts +39 -0
  43. package/src/server.ts +73 -2
  44. package/src/utils/update-check.ts +116 -0
  45. package/dist/gui/assets/index-7IR7ZrSG.css +0 -2
  46. 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
+ }
@@ -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
+ }
@@ -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
- await execa('git', ['worktree', 'remove', worktreePath], { cwd: repoPath });
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
+
@@ -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
+