Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/start-tunnel-node-pty.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@bradygaster/squad-cli': patch
---

Validate node-pty availability before side effects in squad start --tunnel
31 changes: 31 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/squad-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -208,5 +208,9 @@
"type": "git",
"url": "git+https://github.com/bradygaster/squad.git",
"directory": "packages/squad-cli"
},
"optionalDependencies": {
"node-pty": "^1.1.0",
"qrcode-terminal": "^0.12.0"
}
}
37 changes: 32 additions & 5 deletions packages/squad-cli/src/cli/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const RESET = '\x1b[0m';
const DIM = '\x1b[2m';
const GREEN = '\x1b[32m';
const YELLOW = '\x1b[33m';
const MISSING_MODULE_RE =
/\b(?:MODULE_NOT_FOUND|ERR_MODULE_NOT_FOUND)\b|Cannot find module|Cannot find package/i;

export interface StartOptions {
tunnel: boolean;
Expand All @@ -38,7 +40,35 @@ export interface StartOptions {
command?: string;
}

async function checkNodePty(): Promise<any> {
try {
return await import('node-pty');
} catch (err) {
const detail = err instanceof Error ? err.message : String(err);
if (MISSING_MODULE_RE.test(detail)) {
throw new Error('node-pty not available. Install it for PTY support:');
}
throw new Error('node-pty not available. Install it for PTY support:', {
cause: detail,
});
}
}

export async function runStart(cwd: string, options: StartOptions): Promise<void> {
// ─── Verify node-pty availability FIRST (before any side effects) ───
let nodePty: any;
try {
nodePty = await checkNodePty();
} catch (err) {
const detail = err instanceof Error && typeof err.cause === 'string' ? err.cause : undefined;
console.error(`${YELLOW}\u2717${RESET} ${(err as Error).message}`);
console.error(` ${DIM}npm install -g node-pty${RESET}`);
if (detail) {
console.error(`\n${DIM}Error: ${detail}${RESET}`);
}
process.exit(1);
}

const { repo, branch } = getGitInfo(cwd);
const machine = getMachineId();
const squadDir = storage.existsSync(path.join(cwd, '.squad'))
Expand All @@ -47,7 +77,7 @@ export async function runStart(cwd: string, options: StartOptions): Promise<void
? path.join(cwd, '.ai-team')
: '';

// ─── Setup remote bridge FIRST (before PTY takes over terminal) ───
// ─── Setup remote bridge (after verifying PTY is available) ───
let bridge: RemoteBridge | null = null;
let tunnelUrl = '';

Expand Down Expand Up @@ -105,7 +135,7 @@ export async function runStart(cwd: string, options: StartOptions): Promise<void
tunnelUrl = tunnelUrlWithToken;
console.log(`${GREEN}✓${RESET} Remote: ${BOLD}${tunnelUrlWithToken}${RESET}`);
try {
// @ts-ignore
// @ts-expect-error — qrcode-terminal is an optional dependency
const qrcode = (await import('qrcode-terminal')) as any;
qrcode.default.generate(tunnelUrlWithToken, { small: true }, (code: string) => { console.log(code); });
} catch {}
Expand All @@ -120,9 +150,6 @@ export async function runStart(cwd: string, options: StartOptions): Promise<void
}

// ─── Spawn copilot in PTY ─────────────────────────────────
// Dynamic import node-pty (native module)
// @ts-expect-error — node-pty is an optional native dependency
const nodePty = await import('node-pty');

const copilotExePath = path.join(
'C:', 'ProgramData', 'global-npm', 'node_modules', '@github', 'copilot',
Expand Down
Loading
Loading