expxagents 0.20.1 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/src/commands/__tests__/outdated.test.d.ts +1 -0
- package/dist/cli/src/commands/__tests__/outdated.test.js +76 -0
- package/dist/cli/src/commands/create.js +2 -1
- package/dist/cli/src/commands/doctor.js +6 -4
- package/dist/cli/src/commands/init.js +11 -2
- package/dist/cli/src/commands/install.js +4 -2
- package/dist/cli/src/commands/list.js +5 -3
- package/dist/cli/src/commands/mcp.js +2 -1
- package/dist/cli/src/commands/onboarding.js +4 -2
- package/dist/cli/src/commands/outdated.d.ts +14 -0
- package/dist/cli/src/commands/outdated.js +88 -0
- package/dist/cli/src/commands/publish.js +2 -1
- package/dist/cli/src/commands/reorganize.js +2 -1
- package/dist/cli/src/commands/run.js +4 -2
- package/dist/cli/src/commands/sync-templates.js +2 -1
- package/dist/cli/src/commands/update.d.ts +2 -0
- package/dist/cli/src/commands/update.js +81 -0
- package/dist/cli/src/commands/whoami.js +3 -2
- package/dist/cli/src/index.js +5 -12
- package/dist/cli/src/mcp/validate.js +2 -1
- package/dist/cli/src/utils/__tests__/command-prefix.test.d.ts +1 -0
- package/dist/cli/src/utils/__tests__/command-prefix.test.js +35 -0
- package/dist/cli/src/utils/__tests__/global-install.test.d.ts +1 -0
- package/dist/cli/src/utils/__tests__/global-install.test.js +25 -0
- package/dist/cli/src/utils/command-prefix.d.ts +5 -0
- package/dist/cli/src/utils/command-prefix.js +15 -0
- package/dist/cli/src/utils/ensure-server.js +55 -5
- package/dist/cli/src/utils/global-install.d.ts +6 -0
- package/dist/cli/src/utils/global-install.js +15 -0
- package/dist/cli/src/utils/version.d.ts +1 -0
- package/dist/cli/src/utils/version.js +12 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdirSync, writeFileSync, rmSync, mkdtempSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { scanInstalledSquads } from '../outdated.js';
|
|
6
|
+
describe('scanInstalledSquads', () => {
|
|
7
|
+
let tempDir;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
tempDir = mkdtempSync(join(tmpdir(), 'squads-test-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
it('returns empty array when directory does not exist', () => {
|
|
15
|
+
const result = scanInstalledSquads('/nonexistent/path');
|
|
16
|
+
expect(result).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
it('returns empty array when no @scope directories exist', () => {
|
|
19
|
+
mkdirSync(join(tempDir, 'local-squad'), { recursive: true });
|
|
20
|
+
writeFileSync(join(tempDir, 'local-squad', 'squad.yaml'), 'squad:\n version: "1.0.0"');
|
|
21
|
+
const result = scanInstalledSquads(tempDir);
|
|
22
|
+
expect(result).toEqual([]);
|
|
23
|
+
});
|
|
24
|
+
it('finds squads under @scope directories', () => {
|
|
25
|
+
const squadDir = join(tempDir, '@thulio', 'brand-squad');
|
|
26
|
+
mkdirSync(squadDir, { recursive: true });
|
|
27
|
+
writeFileSync(join(squadDir, 'squad.yaml'), 'squad:\n code: brand-squad\n version: "1.2.0"');
|
|
28
|
+
const result = scanInstalledSquads(tempDir);
|
|
29
|
+
expect(result).toEqual([
|
|
30
|
+
{ fullName: '@thulio/brand-squad', currentVersion: '1.2.0' },
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
it('skips squads without squad.yaml', () => {
|
|
34
|
+
const squadDir = join(tempDir, '@thulio', 'no-yaml');
|
|
35
|
+
mkdirSync(squadDir, { recursive: true });
|
|
36
|
+
const result = scanInstalledSquads(tempDir);
|
|
37
|
+
expect(result).toEqual([]);
|
|
38
|
+
});
|
|
39
|
+
it('skips squads with invalid yaml', () => {
|
|
40
|
+
const squadDir = join(tempDir, '@thulio', 'bad-yaml');
|
|
41
|
+
mkdirSync(squadDir, { recursive: true });
|
|
42
|
+
writeFileSync(join(squadDir, 'squad.yaml'), '}{invalid');
|
|
43
|
+
const result = scanInstalledSquads(tempDir);
|
|
44
|
+
expect(result).toEqual([]);
|
|
45
|
+
});
|
|
46
|
+
it('skips squads without version field', () => {
|
|
47
|
+
const squadDir = join(tempDir, '@thulio', 'no-version');
|
|
48
|
+
mkdirSync(squadDir, { recursive: true });
|
|
49
|
+
writeFileSync(join(squadDir, 'squad.yaml'), 'squad:\n code: no-version');
|
|
50
|
+
const result = scanInstalledSquads(tempDir);
|
|
51
|
+
expect(result).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
it('finds squads with nested tarball structure (@scope/name/name/squad.yaml)', () => {
|
|
54
|
+
const nestedDir = join(tempDir, '@community', 'copy-squad', 'copy-squad');
|
|
55
|
+
mkdirSync(nestedDir, { recursive: true });
|
|
56
|
+
writeFileSync(join(nestedDir, 'squad.yaml'), 'squad:\n code: copy-squad\n version: "2.0.0"');
|
|
57
|
+
const result = scanInstalledSquads(tempDir);
|
|
58
|
+
expect(result).toEqual([
|
|
59
|
+
{ fullName: '@community/copy-squad', currentVersion: '2.0.0' },
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
it('finds multiple squads across scopes', () => {
|
|
63
|
+
const squad1 = join(tempDir, '@thulio', 'squad-a');
|
|
64
|
+
const squad2 = join(tempDir, '@community', 'squad-b');
|
|
65
|
+
mkdirSync(squad1, { recursive: true });
|
|
66
|
+
mkdirSync(squad2, { recursive: true });
|
|
67
|
+
writeFileSync(join(squad1, 'squad.yaml'), 'squad:\n version: "1.0.0"');
|
|
68
|
+
writeFileSync(join(squad2, 'squad.yaml'), 'squad:\n version: "2.0.0"');
|
|
69
|
+
const result = scanInstalledSquads(tempDir);
|
|
70
|
+
expect(result).toHaveLength(2);
|
|
71
|
+
expect(result).toEqual(expect.arrayContaining([
|
|
72
|
+
{ fullName: '@thulio/squad-a', currentVersion: '1.0.0' },
|
|
73
|
+
{ fullName: '@community/squad-b', currentVersion: '2.0.0' },
|
|
74
|
+
]));
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -2,6 +2,7 @@ import { spawn } from 'child_process';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import { getCoreAsset } from '../utils/config.js';
|
|
5
|
+
import { getCommandPrefix } from '../utils/command-prefix.js';
|
|
5
6
|
export async function createCommand(description) {
|
|
6
7
|
const cwd = process.cwd();
|
|
7
8
|
const architectPath = getCoreAsset('solution-architect.agent.md');
|
|
@@ -46,7 +47,7 @@ export async function createCommand(description) {
|
|
|
46
47
|
});
|
|
47
48
|
child.on('exit', (code) => {
|
|
48
49
|
if (code === 0) {
|
|
49
|
-
console.log(
|
|
50
|
+
console.log(`\nSquad created! Run \`${getCommandPrefix()} list\` to see it.`);
|
|
50
51
|
}
|
|
51
52
|
else {
|
|
52
53
|
console.error(`\nArchitect exited with code ${code}`);
|
|
@@ -2,8 +2,10 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { execSync } from 'child_process';
|
|
4
4
|
import { getAssetsDir } from '../utils/config.js';
|
|
5
|
+
import { getCommandPrefix } from '../utils/command-prefix.js';
|
|
5
6
|
export async function doctorCommand() {
|
|
6
7
|
const cwd = process.cwd();
|
|
8
|
+
const cmd = getCommandPrefix();
|
|
7
9
|
const checks = [];
|
|
8
10
|
// Node.js version
|
|
9
11
|
const nodeVersion = process.version;
|
|
@@ -44,7 +46,7 @@ export async function doctorCommand() {
|
|
|
44
46
|
checks.push({
|
|
45
47
|
name: 'MCP integrations',
|
|
46
48
|
status: 'warn',
|
|
47
|
-
message:
|
|
49
|
+
message: `No MCPs configured. Run: ${cmd} mcp setup <id>`,
|
|
48
50
|
});
|
|
49
51
|
}
|
|
50
52
|
// Project structure
|
|
@@ -54,7 +56,7 @@ export async function doctorCommand() {
|
|
|
54
56
|
checks.push({
|
|
55
57
|
name: `Directory: ${dir}`,
|
|
56
58
|
status: exists ? 'ok' : 'warn',
|
|
57
|
-
message: exists ? 'exists' :
|
|
59
|
+
message: exists ? 'exists' : `missing — run \`${cmd} init\``,
|
|
58
60
|
});
|
|
59
61
|
}
|
|
60
62
|
// .env file
|
|
@@ -62,7 +64,7 @@ export async function doctorCommand() {
|
|
|
62
64
|
checks.push({
|
|
63
65
|
name: '.env file',
|
|
64
66
|
status: envExists ? 'ok' : 'warn',
|
|
65
|
-
message: envExists ? 'exists' :
|
|
67
|
+
message: envExists ? 'exists' : `missing — run \`${cmd} init\``,
|
|
66
68
|
});
|
|
67
69
|
// Company profile
|
|
68
70
|
const companyPath = path.join(cwd, '_expxagents', '_memory', 'company.md');
|
|
@@ -72,7 +74,7 @@ export async function doctorCommand() {
|
|
|
72
74
|
checks.push({
|
|
73
75
|
name: 'Company profile',
|
|
74
76
|
status: configured ? 'ok' : 'warn',
|
|
75
|
-
message: configured ? 'configured' :
|
|
77
|
+
message: configured ? 'configured' : `not configured — run \`${cmd} onboarding\``,
|
|
76
78
|
});
|
|
77
79
|
}
|
|
78
80
|
// Squads count
|
|
@@ -3,6 +3,8 @@ import path from 'path';
|
|
|
3
3
|
import crypto from 'crypto';
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
5
|
import { getTemplateDir, getAssetsDir } from '../utils/config.js';
|
|
6
|
+
import { tryGlobalInstall } from '../utils/global-install.js';
|
|
7
|
+
import { getVersion } from '../utils/version.js';
|
|
6
8
|
function getDefaultTemplate(file) {
|
|
7
9
|
if (file === 'company.md') {
|
|
8
10
|
return `<!-- NOT CONFIGURED -->
|
|
@@ -408,6 +410,13 @@ BRIDGE_TIMEOUT_MS=300000
|
|
|
408
410
|
fs.writeFileSync(gitignorePath, gitignoreEntries.join('\n') + '\n', 'utf-8');
|
|
409
411
|
console.log(' Created .gitignore');
|
|
410
412
|
}
|
|
413
|
+
// Try global install
|
|
414
|
+
const version = getVersion();
|
|
415
|
+
const globalInstalled = tryGlobalInstall(version);
|
|
416
|
+
const prefix = globalInstalled ? 'expxagents' : 'npx expxagents';
|
|
417
|
+
if (globalInstalled) {
|
|
418
|
+
console.log('\n ✓ CLI installed globally');
|
|
419
|
+
}
|
|
411
420
|
// MCP Setup
|
|
412
421
|
const mcpsDir = path.resolve('mcps');
|
|
413
422
|
if (fs.existsSync(mcpsDir)) {
|
|
@@ -438,7 +447,7 @@ BRIDGE_TIMEOUT_MS=300000
|
|
|
438
447
|
}
|
|
439
448
|
}
|
|
440
449
|
}
|
|
441
|
-
console.log(
|
|
450
|
+
console.log(`\n Run: ${prefix} mcp setup <id> to configure additional MCPs\n`);
|
|
442
451
|
}
|
|
443
452
|
// Install Python dependencies
|
|
444
453
|
const pythonDeps = ['aiohttp', 'aiofiles', 'python-dotenv'];
|
|
@@ -461,7 +470,7 @@ BRIDGE_TIMEOUT_MS=300000
|
|
|
461
470
|
console.log('\nUpdate complete! Assets refreshed to latest version.');
|
|
462
471
|
}
|
|
463
472
|
else {
|
|
464
|
-
console.log(
|
|
473
|
+
console.log(`\nProject initialized! Run \`${prefix} onboarding\` to configure your profile.`);
|
|
465
474
|
console.log('Use /expxagents in Claude Code to get started.');
|
|
466
475
|
}
|
|
467
476
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { getCommandPrefix } from '../utils/command-prefix.js';
|
|
3
4
|
const SKILL_CATALOG = {
|
|
4
5
|
'web-search': {
|
|
5
6
|
description: 'Web search capability (built-in)',
|
|
@@ -34,6 +35,7 @@ const SKILL_CATALOG = {
|
|
|
34
35
|
};
|
|
35
36
|
export async function installCommand(skillName) {
|
|
36
37
|
const cwd = process.cwd();
|
|
38
|
+
const cmd = getCommandPrefix();
|
|
37
39
|
const skillsDir = path.join(cwd, 'skills');
|
|
38
40
|
if (!skillName) {
|
|
39
41
|
console.log('\n Available skills:\n');
|
|
@@ -41,13 +43,13 @@ export async function installCommand(skillName) {
|
|
|
41
43
|
const installed = fs.existsSync(path.join(skillsDir, name));
|
|
42
44
|
console.log(` ${installed ? '[installed]' : ' '} ${name} — ${info.description}`);
|
|
43
45
|
}
|
|
44
|
-
console.log(
|
|
46
|
+
console.log(`\n Usage: ${cmd} install <skill-name>\n`);
|
|
45
47
|
return;
|
|
46
48
|
}
|
|
47
49
|
const skill = SKILL_CATALOG[skillName];
|
|
48
50
|
if (!skill) {
|
|
49
51
|
console.error(` Skill "${skillName}" not found in catalog.`);
|
|
50
|
-
console.log(
|
|
52
|
+
console.log(` Run \`${cmd} install\` to see available skills.`);
|
|
51
53
|
process.exit(1);
|
|
52
54
|
}
|
|
53
55
|
const skillDir = path.join(skillsDir, skillName);
|
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import yaml from 'js-yaml';
|
|
4
4
|
import { loadSkills } from '../../../core/skills-loader.js';
|
|
5
5
|
import { readState } from '../../../core/state-manager.js';
|
|
6
|
+
import { getCommandPrefix } from '../utils/command-prefix.js';
|
|
6
7
|
function walkSquads(squadsDir) {
|
|
7
8
|
if (!fs.existsSync(squadsDir))
|
|
8
9
|
return [];
|
|
@@ -70,6 +71,7 @@ function capitalize(s) {
|
|
|
70
71
|
}
|
|
71
72
|
export async function listCommand() {
|
|
72
73
|
const cwd = process.cwd();
|
|
74
|
+
const cmd = getCommandPrefix();
|
|
73
75
|
const squadsDir = path.join(cwd, 'squads');
|
|
74
76
|
const skillsDir = path.join(cwd, 'skills');
|
|
75
77
|
console.log('Squads:');
|
|
@@ -77,7 +79,7 @@ export async function listCommand() {
|
|
|
77
79
|
if (fs.existsSync(squadsDir)) {
|
|
78
80
|
const allSquads = walkSquads(squadsDir);
|
|
79
81
|
if (allSquads.length === 0) {
|
|
80
|
-
console.log(
|
|
82
|
+
console.log(` No squads found. Run \`${cmd} create\` to create one.\n`);
|
|
81
83
|
}
|
|
82
84
|
else {
|
|
83
85
|
const hierarchical = allSquads
|
|
@@ -113,14 +115,14 @@ export async function listCommand() {
|
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
else {
|
|
116
|
-
console.log(
|
|
118
|
+
console.log(` No squads directory. Run \`${cmd} init\` first.\n`);
|
|
117
119
|
}
|
|
118
120
|
// List skills (unchanged)
|
|
119
121
|
console.log('Skills:');
|
|
120
122
|
console.log('-------');
|
|
121
123
|
const skills = loadSkills(skillsDir);
|
|
122
124
|
if (skills.length === 0) {
|
|
123
|
-
console.log(
|
|
125
|
+
console.log(` No skills installed. Run \`${cmd} install <skill>\` to add one.`);
|
|
124
126
|
}
|
|
125
127
|
else {
|
|
126
128
|
for (const skill of skills) {
|
|
@@ -7,6 +7,7 @@ import { detectMcp, detectExtension, resolvePlatformBinary } from '../mcp/detect
|
|
|
7
7
|
import { writeMcpConfig, removeMcpConfig } from '../mcp/setup.js';
|
|
8
8
|
import { getConfiguredMcpIds } from '../mcp/validate.js';
|
|
9
9
|
import { getAssetsDir } from '../utils/config.js';
|
|
10
|
+
import { getCommandPrefix } from '../utils/command-prefix.js';
|
|
10
11
|
function getMcpsDir() {
|
|
11
12
|
const local = path.resolve('mcps');
|
|
12
13
|
if (fs.existsSync(local))
|
|
@@ -54,7 +55,7 @@ export function mcpCommand() {
|
|
|
54
55
|
}
|
|
55
56
|
console.log('');
|
|
56
57
|
}
|
|
57
|
-
console.log(
|
|
58
|
+
console.log(`Run: ${getCommandPrefix()} mcp setup <id> to configure\n`);
|
|
58
59
|
});
|
|
59
60
|
mcp
|
|
60
61
|
.command('setup <ids...>')
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import readline from 'readline';
|
|
4
|
+
import { getCommandPrefix } from '../utils/command-prefix.js';
|
|
4
5
|
function ask(rl, question) {
|
|
5
6
|
return new Promise(resolve => rl.question(question, resolve));
|
|
6
7
|
}
|
|
@@ -8,8 +9,9 @@ export async function onboardingCommand() {
|
|
|
8
9
|
const cwd = process.cwd();
|
|
9
10
|
const companyPath = path.join(cwd, '_expxagents', '_memory', 'company.md');
|
|
10
11
|
const prefsPath = path.join(cwd, '_expxagents', '_memory', 'preferences.md');
|
|
12
|
+
const cmd = getCommandPrefix();
|
|
11
13
|
if (!fs.existsSync(path.dirname(companyPath))) {
|
|
12
|
-
console.error(
|
|
14
|
+
console.error(`Project not initialized. Run \`${cmd} init\` first.`);
|
|
13
15
|
process.exit(1);
|
|
14
16
|
}
|
|
15
17
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -31,7 +33,7 @@ export async function onboardingCommand() {
|
|
|
31
33
|
const companyContent = `# Company Profile\n\n## Company\n- **Name:** ${companyName}\n- **Website:** ${companyUrl}\n- **Sector:** ${sector}\n- **Description:** ${description}\n\n## Target Audience\n- **Primary:** ${audience}\n\n## Tone of Voice\n- **Style:** ${toneStyle}\n`;
|
|
32
34
|
fs.writeFileSync(companyPath, companyContent, 'utf-8');
|
|
33
35
|
console.log('\n Profile saved! You can edit it anytime at _expxagents/_memory/company.md');
|
|
34
|
-
console.log(
|
|
36
|
+
console.log(` Run \`${cmd} create\` to design your first squad.\n`);
|
|
35
37
|
}
|
|
36
38
|
finally {
|
|
37
39
|
rl.close();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { RegistryClient } from '@expxagents/registry-client';
|
|
3
|
+
export interface InstalledSquad {
|
|
4
|
+
fullName: string;
|
|
5
|
+
currentVersion: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function scanInstalledSquads(squadsDir: string): InstalledSquad[];
|
|
8
|
+
export interface OutdatedResult {
|
|
9
|
+
fullName: string;
|
|
10
|
+
current: string;
|
|
11
|
+
latest: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function checkOutdated(client: RegistryClient, squads: InstalledSquad[]): Promise<OutdatedResult[]>;
|
|
14
|
+
export declare const outdatedCommand: Command;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { resolve, join } from 'node:path';
|
|
3
|
+
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { RegistryClient, readCredentials } from '@expxagents/registry-client';
|
|
5
|
+
import { load as loadYaml } from 'js-yaml';
|
|
6
|
+
import { getCommandPrefix } from '../utils/command-prefix.js';
|
|
7
|
+
export function scanInstalledSquads(squadsDir) {
|
|
8
|
+
const installed = [];
|
|
9
|
+
if (!existsSync(squadsDir))
|
|
10
|
+
return installed;
|
|
11
|
+
for (const entry of readdirSync(squadsDir, { withFileTypes: true })) {
|
|
12
|
+
if (!entry.isDirectory() || !entry.name.startsWith('@'))
|
|
13
|
+
continue;
|
|
14
|
+
const scope = entry.name.slice(1); // remove @
|
|
15
|
+
const scopeDir = join(squadsDir, entry.name);
|
|
16
|
+
for (const squadEntry of readdirSync(scopeDir, { withFileTypes: true })) {
|
|
17
|
+
if (!squadEntry.isDirectory())
|
|
18
|
+
continue;
|
|
19
|
+
// Tarball may extract as @scope/name/squad.yaml or @scope/name/name/squad.yaml
|
|
20
|
+
const directPath = join(scopeDir, squadEntry.name, 'squad.yaml');
|
|
21
|
+
const nestedPath = join(scopeDir, squadEntry.name, squadEntry.name, 'squad.yaml');
|
|
22
|
+
const yamlPath = existsSync(directPath) ? directPath : existsSync(nestedPath) ? nestedPath : null;
|
|
23
|
+
if (!yamlPath)
|
|
24
|
+
continue;
|
|
25
|
+
try {
|
|
26
|
+
const raw = readFileSync(yamlPath, 'utf-8');
|
|
27
|
+
const parsed = loadYaml(raw);
|
|
28
|
+
const version = parsed?.squad?.version ?? parsed?.version;
|
|
29
|
+
if (version) {
|
|
30
|
+
installed.push({
|
|
31
|
+
fullName: `@${scope}/${squadEntry.name}`,
|
|
32
|
+
currentVersion: String(version),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Skip squads with invalid yaml
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return installed;
|
|
42
|
+
}
|
|
43
|
+
export async function checkOutdated(client, squads) {
|
|
44
|
+
const outdated = [];
|
|
45
|
+
for (const squad of squads) {
|
|
46
|
+
try {
|
|
47
|
+
const meta = await client.getSquad(squad.fullName);
|
|
48
|
+
if (meta.latestVersion && meta.latestVersion !== squad.currentVersion) {
|
|
49
|
+
outdated.push({
|
|
50
|
+
fullName: squad.fullName,
|
|
51
|
+
current: squad.currentVersion,
|
|
52
|
+
latest: meta.latestVersion,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Squad not found on registry — skip (local-only squad)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return outdated;
|
|
61
|
+
}
|
|
62
|
+
export const outdatedCommand = new Command('outdated')
|
|
63
|
+
.description('Check installed squads for available updates')
|
|
64
|
+
.option('--dir <dir>', 'Squads directory', './squads')
|
|
65
|
+
.action(async (options) => {
|
|
66
|
+
const squadsDir = resolve(options.dir);
|
|
67
|
+
const installed = scanInstalledSquads(squadsDir);
|
|
68
|
+
if (installed.length === 0) {
|
|
69
|
+
console.log('No registry squads installed.');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const creds = readCredentials();
|
|
73
|
+
const client = new RegistryClient({
|
|
74
|
+
registryUrl: creds?.registryUrl || 'https://expxagents-marketplace-production.up.railway.app',
|
|
75
|
+
apiKey: creds?.apiKey,
|
|
76
|
+
});
|
|
77
|
+
console.log(`Checking ${installed.length} squad(s)...\n`);
|
|
78
|
+
const outdated = await checkOutdated(client, installed);
|
|
79
|
+
if (outdated.length === 0) {
|
|
80
|
+
console.log('All squads are up to date.');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
console.log('Updates available:\n');
|
|
84
|
+
for (const o of outdated) {
|
|
85
|
+
console.log(` ${o.fullName} ${o.current} \u2192 ${o.latest}`);
|
|
86
|
+
}
|
|
87
|
+
console.log(`\nRun \`${getCommandPrefix()} update\` to update all.`);
|
|
88
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { RegistryClient, readCredentials } from '@expxagents/registry-client';
|
|
4
|
+
import { getCommandPrefix } from '../utils/command-prefix.js';
|
|
4
5
|
export const publishCommand = new Command('publish')
|
|
5
6
|
.description('Publish a squad to the registry')
|
|
6
7
|
.argument('[dir]', 'Squad directory', '.')
|
|
@@ -8,7 +9,7 @@ export const publishCommand = new Command('publish')
|
|
|
8
9
|
.action(async (dir, options) => {
|
|
9
10
|
const creds = readCredentials();
|
|
10
11
|
if (!creds) {
|
|
11
|
-
console.error(
|
|
12
|
+
console.error(`Not logged in. Run: ${getCommandPrefix()} login`);
|
|
12
13
|
process.exit(1);
|
|
13
14
|
}
|
|
14
15
|
const squadDir = resolve(dir);
|
|
@@ -2,6 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import * as readline from 'readline';
|
|
4
4
|
import yaml from 'js-yaml';
|
|
5
|
+
import { getCommandPrefix } from '../utils/command-prefix.js';
|
|
5
6
|
// Walk squadsDir recursively, return all squad summaries
|
|
6
7
|
function discoverAllSquads(squadsDir) {
|
|
7
8
|
const result = [];
|
|
@@ -214,7 +215,7 @@ export async function reorganizeCommand() {
|
|
|
214
215
|
const cwd = process.cwd();
|
|
215
216
|
const squadsDir = path.join(cwd, 'squads');
|
|
216
217
|
if (!fs.existsSync(squadsDir)) {
|
|
217
|
-
console.error(
|
|
218
|
+
console.error(`No squads directory found. Run \`${getCommandPrefix()} init\` first.`);
|
|
218
219
|
process.exit(1);
|
|
219
220
|
}
|
|
220
221
|
console.log('Escaneando squads...\n');
|
|
@@ -5,6 +5,7 @@ import { loadSquad, findSquadDir } from '../../../core/squad-loader.js';
|
|
|
5
5
|
import { createInitialState, readState, writeState, updateAgentStatus, updateStep, setHandoff, setSquadStatus, HANDOFF_DELAY_MS, } from '../../../core/state-manager.js';
|
|
6
6
|
import { loadSkills } from '../../../core/skills-loader.js';
|
|
7
7
|
import { getCoreAsset } from '../utils/config.js';
|
|
8
|
+
import { getCommandPrefix } from '../utils/command-prefix.js';
|
|
8
9
|
import { runWithProvider } from '../runners/provider-runner.js';
|
|
9
10
|
import { estimateCost, formatCost } from '../runners/cost-tracker.js';
|
|
10
11
|
function delay(ms) {
|
|
@@ -135,8 +136,9 @@ export async function runCommand(name) {
|
|
|
135
136
|
const squadsDir = path.join(cwd, 'squads');
|
|
136
137
|
const squadDir = findSquadDir(squadsDir, name);
|
|
137
138
|
const skillsDir = path.join(cwd, 'skills');
|
|
139
|
+
const cmd = getCommandPrefix();
|
|
138
140
|
if (!squadDir) {
|
|
139
|
-
console.error(`Squad "${name}" not found. Run
|
|
141
|
+
console.error(`Squad "${name}" not found. Run \`${cmd} list\` to see available squads.`);
|
|
140
142
|
process.exit(1);
|
|
141
143
|
}
|
|
142
144
|
let config;
|
|
@@ -155,7 +157,7 @@ export async function runCommand(name) {
|
|
|
155
157
|
}
|
|
156
158
|
const existingState = readState(squadDir);
|
|
157
159
|
if (existingState && existingState.status === 'running') {
|
|
158
|
-
console.error(`Squad "${name}" is already running. Use
|
|
160
|
+
console.error(`Squad "${name}" is already running. Use \`${cmd} stop ${name}\` first.`);
|
|
159
161
|
process.exit(1);
|
|
160
162
|
}
|
|
161
163
|
const outputDir = getNextVersionDir(squadDir);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { findSquadDir, loadSquad } from '../../../core/squad-loader.js';
|
|
4
|
+
import { getCommandPrefix } from '../utils/command-prefix.js';
|
|
4
5
|
/**
|
|
5
6
|
* Find all .pen files in a squad's templates/ directory.
|
|
6
7
|
*/
|
|
@@ -65,7 +66,7 @@ export async function syncTemplatesCommand(squadFilter) {
|
|
|
65
66
|
const cwd = process.cwd();
|
|
66
67
|
const squadsDir = path.join(cwd, 'squads');
|
|
67
68
|
if (!fs.existsSync(squadsDir)) {
|
|
68
|
-
console.error(
|
|
69
|
+
console.error(`No squads/ directory found. Run \`${getCommandPrefix()} init\` first.`);
|
|
69
70
|
process.exit(1);
|
|
70
71
|
}
|
|
71
72
|
let squadDirs;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import { RegistryClient, readCredentials } from '@expxagents/registry-client';
|
|
5
|
+
import { scanInstalledSquads, checkOutdated } from './outdated.js';
|
|
6
|
+
function confirm(question) {
|
|
7
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8
|
+
return new Promise((res) => {
|
|
9
|
+
rl.question(question, (answer) => {
|
|
10
|
+
rl.close();
|
|
11
|
+
res(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
async function updateSquads(client, squads, targetDir) {
|
|
16
|
+
for (const squad of squads) {
|
|
17
|
+
try {
|
|
18
|
+
console.log(` Updating ${squad.fullName} ${squad.current} \u2192 ${squad.latest}...`);
|
|
19
|
+
const result = await client.installWithDeps(squad.fullName, squad.latest, targetDir);
|
|
20
|
+
console.log(` \u2713 ${result.root}`);
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
24
|
+
console.error(` \u2717 ${squad.fullName}: ${message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export const updateCommand = new Command('update')
|
|
29
|
+
.description('Update installed squads to their latest versions')
|
|
30
|
+
.argument('[squad]', 'Specific squad to update (@scope/name)')
|
|
31
|
+
.option('--dir <dir>', 'Squads directory', './squads')
|
|
32
|
+
.option('--yes', 'Skip confirmation prompt')
|
|
33
|
+
.action(async (squad, options) => {
|
|
34
|
+
const squadsDir = resolve(options.dir);
|
|
35
|
+
const creds = readCredentials();
|
|
36
|
+
const client = new RegistryClient({
|
|
37
|
+
registryUrl: creds?.registryUrl || 'https://expxagents-marketplace-production.up.railway.app',
|
|
38
|
+
apiKey: creds?.apiKey,
|
|
39
|
+
});
|
|
40
|
+
// If specific squad provided, update just that one
|
|
41
|
+
if (squad) {
|
|
42
|
+
try {
|
|
43
|
+
console.log(`Updating ${squad}...`);
|
|
44
|
+
const result = await client.installWithDeps(squad, 'latest', squadsDir);
|
|
45
|
+
console.log(`\u2713 Updated ${result.root}`);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
49
|
+
console.error(`\u2717 Update failed: ${message}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Otherwise: scan all, check for updates, confirm, update
|
|
55
|
+
const installed = scanInstalledSquads(squadsDir);
|
|
56
|
+
if (installed.length === 0) {
|
|
57
|
+
console.log('No registry squads installed.');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
console.log(`Checking ${installed.length} squad(s)...\n`);
|
|
61
|
+
const outdated = await checkOutdated(client, installed);
|
|
62
|
+
if (outdated.length === 0) {
|
|
63
|
+
console.log('All squads are up to date.');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
console.log('Updates available:\n');
|
|
67
|
+
for (const o of outdated) {
|
|
68
|
+
console.log(` ${o.fullName} ${o.current} \u2192 ${o.latest}`);
|
|
69
|
+
}
|
|
70
|
+
console.log('');
|
|
71
|
+
if (!options.yes) {
|
|
72
|
+
const ok = await confirm(`Update ${outdated.length} squad(s)? (y/N) `);
|
|
73
|
+
if (!ok) {
|
|
74
|
+
console.log('Cancelled.');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
console.log('');
|
|
79
|
+
await updateSquads(client, outdated, squadsDir);
|
|
80
|
+
console.log('\nDone.');
|
|
81
|
+
});
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { RegistryClient, readCredentials } from '@expxagents/registry-client';
|
|
3
|
+
import { getCommandPrefix } from '../utils/command-prefix.js';
|
|
3
4
|
export const whoamiCommand = new Command('whoami')
|
|
4
5
|
.description('Show current registry identity')
|
|
5
6
|
.action(async () => {
|
|
6
7
|
const creds = readCredentials();
|
|
7
8
|
if (!creds) {
|
|
8
|
-
console.error(
|
|
9
|
+
console.error(`Not logged in. Run: ${getCommandPrefix()} login`);
|
|
9
10
|
process.exit(1);
|
|
10
11
|
}
|
|
11
12
|
const client = new RegistryClient({ registryUrl: creds.registryUrl, apiKey: creds.apiKey });
|
|
@@ -17,7 +18,7 @@ export const whoamiCommand = new Command('whoami')
|
|
|
17
18
|
console.log(` Registry: ${creds.registryUrl}`);
|
|
18
19
|
}
|
|
19
20
|
catch {
|
|
20
|
-
console.error(
|
|
21
|
+
console.error(`\u2717 Invalid credentials. Run: ${getCommandPrefix()} login`);
|
|
21
22
|
process.exit(1);
|
|
22
23
|
}
|
|
23
24
|
});
|
package/dist/cli/src/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
1
|
import { Command } from 'commander';
|
|
3
2
|
import { initCommand } from './commands/init.js';
|
|
4
3
|
import { createCommand } from './commands/create.js';
|
|
@@ -25,17 +24,9 @@ import { publishCommand } from './commands/publish.js';
|
|
|
25
24
|
import { registryInstallCommand } from './commands/registry-install.js';
|
|
26
25
|
import { searchCommand } from './commands/search.js';
|
|
27
26
|
import { infoCommand } from './commands/info.js';
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const pkgPath = `${findPackageRoot()}/package.json`;
|
|
32
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
33
|
-
return pkg.version ?? '0.0.0';
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
return '0.0.0';
|
|
37
|
-
}
|
|
38
|
-
}
|
|
27
|
+
import { outdatedCommand } from './commands/outdated.js';
|
|
28
|
+
import { updateCommand } from './commands/update.js';
|
|
29
|
+
import { getVersion } from './utils/version.js';
|
|
39
30
|
const program = new Command();
|
|
40
31
|
program
|
|
41
32
|
.name('expxagents')
|
|
@@ -121,4 +112,6 @@ program.addCommand(publishCommand);
|
|
|
121
112
|
program.addCommand(registryInstallCommand);
|
|
122
113
|
program.addCommand(searchCommand);
|
|
123
114
|
program.addCommand(infoCommand);
|
|
115
|
+
program.addCommand(outdatedCommand);
|
|
116
|
+
program.addCommand(updateCommand);
|
|
124
117
|
program.parse();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { getCommandPrefix } from '../utils/command-prefix.js';
|
|
3
4
|
export function getConfiguredMcpIds(projectDir) {
|
|
4
5
|
const mcpPath = path.join(projectDir, '.mcp.json');
|
|
5
6
|
if (!fs.existsSync(mcpPath))
|
|
@@ -18,6 +19,6 @@ export function validateSquadMcps(squadMcps, configuredMcps) {
|
|
|
18
19
|
const missing = squadMcps.filter(m => !configuredMcps.includes(m));
|
|
19
20
|
if (missing.length > 0) {
|
|
20
21
|
throw new Error(`Squad requires MCPs not configured: ${missing.join(', ')}.\n` +
|
|
21
|
-
`Run:
|
|
22
|
+
`Run: ${getCommandPrefix()} mcp setup ${missing.join(' ')}`);
|
|
22
23
|
}
|
|
23
24
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { getCommandPrefix } from '../command-prefix.js';
|
|
3
|
+
describe('getCommandPrefix', () => {
|
|
4
|
+
const originalArgv = process.argv;
|
|
5
|
+
const originalEnv = { ...process.env };
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
process.argv = originalArgv;
|
|
8
|
+
process.env = originalEnv;
|
|
9
|
+
});
|
|
10
|
+
it('returns "npx expxagents" when npm_execpath contains npx', () => {
|
|
11
|
+
process.env.npm_execpath = '/usr/local/lib/node_modules/npm/bin/npx-cli.js';
|
|
12
|
+
process.argv = [process.argv[0], '/usr/local/bin/expxagents'];
|
|
13
|
+
expect(getCommandPrefix()).toBe('npx expxagents');
|
|
14
|
+
});
|
|
15
|
+
it('returns "npx expxagents" when argv[1] contains _npx (fallback)', () => {
|
|
16
|
+
delete process.env.npm_execpath;
|
|
17
|
+
process.argv = [process.argv[0], '/home/user/.npm/_npx/abc123/bin/expxagents.js'];
|
|
18
|
+
expect(getCommandPrefix()).toBe('npx expxagents');
|
|
19
|
+
});
|
|
20
|
+
it('returns "expxagents" for global install on Unix', () => {
|
|
21
|
+
delete process.env.npm_execpath;
|
|
22
|
+
process.argv = [process.argv[0], '/usr/local/bin/expxagents'];
|
|
23
|
+
expect(getCommandPrefix()).toBe('expxagents');
|
|
24
|
+
});
|
|
25
|
+
it('returns "expxagents" for global install on Windows', () => {
|
|
26
|
+
delete process.env.npm_execpath;
|
|
27
|
+
process.argv = [process.argv[0], 'C:\\Users\\X\\AppData\\Roaming\\npm\\expxagents'];
|
|
28
|
+
expect(getCommandPrefix()).toBe('expxagents');
|
|
29
|
+
});
|
|
30
|
+
it('returns "expxagents" when both env and argv are empty', () => {
|
|
31
|
+
delete process.env.npm_execpath;
|
|
32
|
+
process.argv = [process.argv[0], ''];
|
|
33
|
+
expect(getCommandPrefix()).toBe('expxagents');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import { tryGlobalInstall } from '../global-install.js';
|
|
4
|
+
vi.mock('child_process', () => ({
|
|
5
|
+
execSync: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
const mockExecSync = vi.mocked(execSync);
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.restoreAllMocks();
|
|
10
|
+
});
|
|
11
|
+
describe('tryGlobalInstall', () => {
|
|
12
|
+
it('returns true when npm install -g succeeds', () => {
|
|
13
|
+
mockExecSync.mockReturnValue(Buffer.from(''));
|
|
14
|
+
expect(tryGlobalInstall('0.20.1')).toBe(true);
|
|
15
|
+
expect(mockExecSync).toHaveBeenCalledWith('npm install -g expxagents@0.20.1', { stdio: 'pipe', timeout: 60000 });
|
|
16
|
+
});
|
|
17
|
+
it('returns false when npm install -g fails', () => {
|
|
18
|
+
mockExecSync.mockImplementation(() => { throw new Error('EACCES'); });
|
|
19
|
+
expect(tryGlobalInstall('0.20.1')).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
it('never throws', () => {
|
|
22
|
+
mockExecSync.mockImplementation(() => { throw new Error('network error'); });
|
|
23
|
+
expect(() => tryGlobalInstall('1.0.0')).not.toThrow();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects whether the CLI is running globally or via npx.
|
|
3
|
+
* Returns 'expxagents' for global, 'npx expxagents' for npx.
|
|
4
|
+
*/
|
|
5
|
+
export function getCommandPrefix() {
|
|
6
|
+
const npmExecPath = process.env.npm_execpath ?? '';
|
|
7
|
+
if (npmExecPath.includes('npx')) {
|
|
8
|
+
return 'npx expxagents';
|
|
9
|
+
}
|
|
10
|
+
const execPath = process.argv[1] ?? '';
|
|
11
|
+
if (execPath.includes('_npx')) {
|
|
12
|
+
return 'npx expxagents';
|
|
13
|
+
}
|
|
14
|
+
return 'expxagents';
|
|
15
|
+
}
|
|
@@ -1,7 +1,53 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
1
|
+
import { execSync, spawn } from 'child_process';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { resolveServerPaths } from './server-paths.js';
|
|
5
|
+
const MAX_HEALTH_CHECK_ATTEMPTS = 30;
|
|
6
|
+
const HEALTH_CHECK_INTERVAL_MS = 500;
|
|
7
|
+
/**
|
|
8
|
+
* Kill the process listening on the given port (cross-platform).
|
|
9
|
+
*/
|
|
10
|
+
function killProcessOnPort(port) {
|
|
11
|
+
if (process.platform === 'win32') {
|
|
12
|
+
// Find PID using netstat, then kill it
|
|
13
|
+
try {
|
|
14
|
+
const output = execSync(`netstat -ano | findstr :${port} | findstr LISTENING`, {
|
|
15
|
+
encoding: 'utf-8',
|
|
16
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
17
|
+
});
|
|
18
|
+
const pids = new Set(output
|
|
19
|
+
.split('\n')
|
|
20
|
+
.map((line) => line.trim().split(/\s+/).pop())
|
|
21
|
+
.filter((pid) => !!pid && pid !== '0'));
|
|
22
|
+
for (const pid of pids) {
|
|
23
|
+
execSync(`taskkill /PID ${pid} /F`, { stdio: 'ignore' });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new Error(`Could not stop existing server on port ${port}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
execSync(`lsof -ti:${port} | xargs kill -9`, { stdio: 'ignore' });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Poll the health endpoint until the server responds OK or max attempts reached.
|
|
36
|
+
*/
|
|
37
|
+
async function waitForHealthy(port) {
|
|
38
|
+
for (let i = 0; i < MAX_HEALTH_CHECK_ATTEMPTS; i++) {
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(`http://localhost:${port}/api/health`);
|
|
41
|
+
if (res.ok)
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Server not ready yet
|
|
46
|
+
}
|
|
47
|
+
await new Promise((resolve) => setTimeout(resolve, HEALTH_CHECK_INTERVAL_MS));
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
5
51
|
/**
|
|
6
52
|
* Ensure the production server is running on the given port.
|
|
7
53
|
* If a dev server is detected (health OK but no dashboard), it is killed and replaced.
|
|
@@ -32,8 +78,7 @@ export async function ensureProductionServer(port) {
|
|
|
32
78
|
// Server is running but not serving dashboard (dev mode) — kill it
|
|
33
79
|
console.log('Detected dev server on port ' + port + ', restarting in production mode...');
|
|
34
80
|
try {
|
|
35
|
-
|
|
36
|
-
execSync(`lsof -ti:${port} | xargs kill -9`, { stdio: 'ignore' });
|
|
81
|
+
killProcessOnPort(port);
|
|
37
82
|
// Wait for port to free up
|
|
38
83
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
39
84
|
}
|
|
@@ -71,6 +116,11 @@ export async function ensureProductionServer(port) {
|
|
|
71
116
|
console.error(`Failed to start server: ${err.message}`);
|
|
72
117
|
process.exit(1);
|
|
73
118
|
});
|
|
74
|
-
//
|
|
75
|
-
|
|
119
|
+
// Poll health endpoint until server is ready (up to 15 seconds)
|
|
120
|
+
const healthy = await waitForHealthy(port);
|
|
121
|
+
if (!healthy) {
|
|
122
|
+
console.error(`Server failed to start on port ${port} within ${(MAX_HEALTH_CHECK_ATTEMPTS * HEALTH_CHECK_INTERVAL_MS) / 1000} seconds.`);
|
|
123
|
+
console.error('Check the server logs above for errors.');
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
76
126
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Attempts to install expxagents globally via npm.
|
|
4
|
+
* Returns true if successful, false otherwise.
|
|
5
|
+
* Never throws — failures are silent.
|
|
6
|
+
*/
|
|
7
|
+
export function tryGlobalInstall(version) {
|
|
8
|
+
try {
|
|
9
|
+
execSync(`npm install -g expxagents@${version}`, { stdio: 'pipe', timeout: 60000 });
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getVersion(): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { findPackageRoot } from './config.js';
|
|
3
|
+
export function getVersion() {
|
|
4
|
+
try {
|
|
5
|
+
const pkgPath = `${findPackageRoot()}/package.json`;
|
|
6
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
7
|
+
return pkg.version ?? '0.0.0';
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return '0.0.0';
|
|
11
|
+
}
|
|
12
|
+
}
|