diff --git a/packages/squad-cli/package.json b/packages/squad-cli/package.json index 75ea34c7..469b5809 100644 --- a/packages/squad-cli/package.json +++ b/packages/squad-cli/package.json @@ -173,7 +173,7 @@ "postinstall": "node scripts/patch-ink-rendering.mjs", "prepublishOnly": "npm run build", "build": "tsc -p tsconfig.json && npm run postbuild && npm run bundle", - "postbuild": "node -e \"require('fs').cpSync('src/remote-ui', 'dist/remote-ui', {recursive: true})\"", + "postbuild": "node -e \"require('fs').cpSync('src/remote-ui', 'dist/remote-ui', {recursive: true}); require('fs').cpSync('../squad-sdk/dist/presets/builtin', 'dist/presets/builtin', {recursive: true})\"", "bundle": "node scripts/bundle.mjs" }, "engines": { diff --git a/packages/squad-cli/src/cli/commands/loop.ts b/packages/squad-cli/src/cli/commands/loop.ts index 9b127486..1c2690cb 100644 --- a/packages/squad-cli/src/cli/commands/loop.ts +++ b/packages/squad-cli/src/cli/commands/loop.ts @@ -124,8 +124,16 @@ export function parseLoopFile(content: string): { frontmatter: LoopFrontmatter; /** Returns the content of a starter loop.md for --init. */ export function generateLoopFile(): string { const here = path.dirname(fileURLToPath(import.meta.url)); - // Walk up from src/cli/commands (or dist/cli/commands) to package root - const templatePath = path.resolve(here, '..', '..', '..', 'templates', 'loop.md'); + // Candidate offsets from `here` to squad-cli's package root (which holds + // templates/loop.md): 3 levels up from unbundled dist/cli/commands/, or + // 1 level up when squad-cli's source is esbuild-bundled into a single + // dist/squad.js (airgap build) — bundling collapses `here` straight to + // dist/, so the unbundled offset alone resolves to the wrong directory. + const candidates = [ + path.resolve(here, '..', '..', '..', 'templates', 'loop.md'), + path.resolve(here, '..', 'templates', 'loop.md'), + ]; + const templatePath = candidates.find((c) => existsSync(c)) ?? candidates[0]!; return readFileSync(templatePath, 'utf-8'); } diff --git a/packages/squad-cli/src/cli/commands/rc.ts b/packages/squad-cli/src/cli/commands/rc.ts index b03093b4..8b214bd3 100644 --- a/packages/squad-cli/src/cli/commands/rc.ts +++ b/packages/squad-cli/src/cli/commands/rc.ts @@ -8,8 +8,8 @@ import path from 'node:path'; // createReadStream retained — streaming not in StorageProvider scope import { createReadStream } from 'node:fs'; -import { fileURLToPath } from 'node:url'; import { FSStorageProvider, RemoteBridge } from '@deduvafork/squad-sdk'; +import { resolveRemoteUiDir } from '../core/remote-ui-dir.js'; const storage = new FSStorageProvider(); import type { RemoteBridgeConfig } from '@deduvafork/squad-sdk'; @@ -111,10 +111,7 @@ export async function runRC(cwd: string, options: RCOptions): Promise { // Serve PWA static files bridge.setStaticHandler((req, res) => { - const uiDir = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - '../../remote-ui' - ); + const uiDir = resolveRemoteUiDir(); // #18: Guard against malformed URI encoding let decodedUrl: string; diff --git a/packages/squad-cli/src/cli/commands/start.ts b/packages/squad-cli/src/cli/commands/start.ts index 880dd609..e8da66eb 100644 --- a/packages/squad-cli/src/cli/commands/start.ts +++ b/packages/squad-cli/src/cli/commands/start.ts @@ -12,9 +12,9 @@ import path from 'node:path'; // createReadStream retained — streaming not in StorageProvider scope import { createReadStream } from 'node:fs'; -import { fileURLToPath } from 'node:url'; import { FSStorageProvider, RemoteBridge } from '@deduvafork/squad-sdk'; import { withAdditionalMcpConfig } from '../core/copilot-invocation.js'; +import { resolveRemoteUiDir } from '../core/remote-ui-dir.js'; const storage = new FSStorageProvider(); import type { RemoteBridgeConfig } from '@deduvafork/squad-sdk'; @@ -93,7 +93,7 @@ export async function runStart(cwd: string, options: StartOptions): Promise { - const uiDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../remote-ui'); + const uiDir = resolveRemoteUiDir(); let decodedUrl: string; try { const parsed = new URL(req.url || '/', `http://${req.headers.host}`); diff --git a/packages/squad-cli/src/cli/core/remote-ui-dir.ts b/packages/squad-cli/src/cli/core/remote-ui-dir.ts new file mode 100644 index 00000000..0f048cfa --- /dev/null +++ b/packages/squad-cli/src/cli/core/remote-ui-dir.ts @@ -0,0 +1,26 @@ +/** + * Resolve the built PWA static assets directory ("remote-ui") served by + * `squad rc` and `squad start`. + */ + +import path from 'node:path'; +import { existsSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +/** + * squad-cli's own build copies src/remote-ui/ to dist/remote-ui/ (postbuild + * step), so the target is always "whatever dist/ actually is" — but that + * directory sits at a different depth relative to this module depending on + * whether squad-cli runs unbundled (dist/cli/commands/rc.js, 2 levels above + * dist) or as squad-cli's single-file bundle (dist/squad.js, 0 levels above + * dist, since bundling collapses every module's import.meta.url to the + * bundle's own location). Try both. + */ +export function resolveRemoteUiDir(): string { + const here = path.dirname(fileURLToPath(import.meta.url)); + const candidates = [ + path.resolve(here, '../../remote-ui'), + path.resolve(here, 'remote-ui'), + ]; + return candidates.find((c) => existsSync(c)) ?? candidates[0]!; +} diff --git a/packages/squad-sdk/src/presets/index.ts b/packages/squad-sdk/src/presets/index.ts index 80f793b7..79a39c4b 100644 --- a/packages/squad-sdk/src/presets/index.ts +++ b/packages/squad-sdk/src/presets/index.ts @@ -11,7 +11,7 @@ import path from 'node:path'; import os from 'node:os'; -import { readdirSync, statSync, lstatSync, rmSync } from 'node:fs'; +import { readdirSync, statSync, lstatSync, rmSync, existsSync } from 'node:fs'; import { execFileSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { FSStorageProvider } from '../storage/fs-storage-provider.js'; @@ -654,7 +654,22 @@ function copyDirRecursive(src: string, dest: string): void { */ export function getBuiltinPresetsDir(): string { const thisFile = fileURLToPath(import.meta.url); - return path.join(path.dirname(thisFile), 'builtin'); + const dir = path.dirname(thisFile); + + // Unbundled: this file is squad-sdk's own dist/presets/index.js, and + // squad-sdk's build copies src/presets/builtin -> dist/presets/builtin + // alongside it. + const unbundledPath = path.join(dir, 'builtin'); + if (existsSync(unbundledPath)) { + return unbundledPath; + } + + // Bundled: squad-sdk's source is esbuild-bundled into squad-cli's + // single-file dist/squad.js (airgap build), so `dir` is squad-cli/dist/ + // instead of squad-sdk/dist/presets/ — the 'presets' path segment that + // unbundled resolution relied on is gone. squad-cli's own build copies + // squad-sdk's built-in presets to dist/presets/builtin as a sibling. + return path.join(dir, 'presets', 'builtin'); } /** diff --git a/packages/squad-sdk/src/sharing/consult.ts b/packages/squad-sdk/src/sharing/consult.ts index 5a46ae91..05d91313 100644 --- a/packages/squad-sdk/src/sharing/consult.ts +++ b/packages/squad-sdk/src/sharing/consult.ts @@ -91,19 +91,30 @@ This project is in **consult mode**. Your personal squad has been copied into \` function getSquadAgentTemplatePath(): string | null { // Use fileURLToPath for cross-platform compatibility (handles Windows drive letters, URL encoding) const currentDir = path.dirname(fileURLToPath(import.meta.url)); - - // Try relative to this file (in dist/) + + // Try relative to this file, assuming it's squad-sdk's own dist/sharing/consult.js + // (true when squad-sdk runs unbundled, e.g. as a normal node_modules dependency). const distPath = path.resolve(currentDir, '../../templates/squad.agent.md.template'); if (storage.existsSync(distPath)) { return distPath; } - - // Try relative to package root + + // When squad-sdk's source is esbuild-bundled into squad-cli's single-file + // dist/squad.js (airgap build), import.meta.url points at squad-cli/dist/ + // instead of squad-sdk/dist/sharing/ — the two extra path segments above no + // longer apply. squad-cli ships its own synced copy of the templates dir as + // a sibling of dist/, so check that location before falling back further. + const siblingPath = path.resolve(currentDir, '../templates/squad.agent.md.template'); + if (storage.existsSync(siblingPath)) { + return siblingPath; + } + + // Monorepo dev fallback — checked last, same reasoning as getSDKTemplatesDir(). const pkgPath = path.resolve(currentDir, '../../../templates/squad.agent.md.template'); if (storage.existsSync(pkgPath)) { return pkgPath; } - + return null; }