Skip to content
Open
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
2 changes: 1 addition & 1 deletion packages/squad-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
12 changes: 10 additions & 2 deletions packages/squad-cli/src/cli/commands/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

Expand Down
7 changes: 2 additions & 5 deletions packages/squad-cli/src/cli/commands/rc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -111,10 +111,7 @@ export async function runRC(cwd: string, options: RCOptions): Promise<void> {

// 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;
Expand Down
4 changes: 2 additions & 2 deletions packages/squad-cli/src/cli/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -93,7 +93,7 @@ export async function runStart(cwd: string, options: StartOptions): Promise<void

// PWA static files
bridge.setStaticHandler((req, res) => {
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}`);
Expand Down
26 changes: 26 additions & 0 deletions packages/squad-cli/src/cli/core/remote-ui-dir.ts
Original file line number Diff line number Diff line change
@@ -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]!;
}
19 changes: 17 additions & 2 deletions packages/squad-sdk/src/presets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
}

/**
Expand Down
21 changes: 16 additions & 5 deletions packages/squad-sdk/src/sharing/consult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Loading