Skip to content

feat: add project-navigation commands (open, pick, list, add)#1392

Open
barichter wants to merge 13 commits into
bradygaster:devfrom
barichter:feat/squad-projects-open
Open

feat: add project-navigation commands (open, pick, list, add)#1392
barichter wants to merge 13 commits into
bradygaster:devfrom
barichter:feat/squad-projects-open

Conversation

@barichter

Copy link
Copy Markdown

Depends on #1391. Until that PR merges, this PR's diff also includes its single commit (b186993). Both target dev; no retarget needed after PR1 merges.

What

PR1 added a global registry of every project where squad init runs, plus a
read-only squad projects list. This PR makes that registry actionable. It adds
four navigation commands so you can jump to, choose, alias-list, and register
your Squad projects without leaving the terminal.

squad open [name]

Resolves a registered project and launches Copilot in that project's directory.

  • squad open <name>: looks up the project by name, partial name, or path, then
    starts copilot in that directory.
  • squad open with no name, on a TTY, shows a numbered picker.
  • squad open with no name, when stdout is not a TTY, prints the project list
    and a hint to pass a name, rather than blocking on a prompt.
  • squad open <name> --print-path (-p) prints the resolved path instead of
    launching, for shell integrations like cd (squad open foo -p).

squad pick

An always-interactive opener. It presents the numbered picker of all registered
projects, prompts for a choice, then opens the chosen project in Copilot. It is
equivalent to running squad open after manually finding a name in the list.
Positional arguments are stripped so it always enters the picker; flags such as
--print-path are forwarded.

squad list

A convenience alias for squad projects. It accepts the same flags, including
--open, and behaves identically. Existing squad projects usage is unchanged.

squad add [path]

Registers a pre-existing directory in the global registry so it shows up in
squad projects, squad list, and squad pick, without re-running
squad init and without scaffolding a .squad/ directory.

  • squad add <path>: registers the directory at that path. With no path, it
    registers the current directory.
  • squad add <path> --name <name>: sets a custom display name. Without it, the
    directory basename is used.
  • Paths with spaces are handled even when passed unquoted: the remaining
    argument tokens are rejoined into the full path. If the reconstructed path does
    not exist and there were multiple tokens, it prints a hint to quote the path.

Why

A list of projects is only useful if you can act on it. These commands close the
loop: from "what have I initialized?" to "take me there," to "let me choose from
a menu," to "remember this directory too." squad open is the core verb,
squad pick is the discoverable interactive form, squad list is muscle-memory
ergonomics, and squad add lets you backfill projects that predate the registry
or that you set up by hand.

How

SDK resolver: resolveProject(query) in projects-registry.ts. Resolution
order, first match wins:

  1. Exact name match (case-insensitive on win32 and darwin). One hit returns
    { match }; several with the same name return { ambiguous }.
  2. Exact path match via samePath on the resolved absolute paths.
  3. Unique case-insensitive substring match on name. One hit returns { match },
    several return { ambiguous }.

It returns a discriminated union: { match }, { ambiguous: [...] }, or
{ notFound: true }. An empty query (after trimming) always returns
{ notFound: true }.

SDK registration: registerProject(name, path) (introduced in PR1, reused
here by squad add). It is idempotent: re-registering a path that already exists
updates that entry in place and preserves the original created_at, rather than
adding a duplicate. squad add first checks whether the path is already present
so it can report "Added" versus "Updated" accurately.

Command implementations:

  • open.ts owns all shared opener logic: the resolver call, the empty-registry
    and not-found and ambiguous messages, the numbered picker, the non-TTY list
    fallback, the missing-on-disk guard, --print-path, and the Copilot launch.
    Launch is platform-aware (shell: true on win32) and inherits stdio. If
    copilot is not on PATH (ENOENT), it prints the path with a hint instead of
    crashing.
  • pick.ts delegates to runOpen with positional args removed, so all picker
    and launch behavior is reused with no duplication.
  • projects.ts gains an --open/-o branch that delegates to runOpen;
    listing remains the default.
  • add.ts parses --name, rejoins remaining tokens into a path, resolves it to
    absolute, validates it exists and is a directory, then calls registerProject.

cli-entry.ts wires open, pick, and add to their handlers and routes both
projects and list to runProjects. command-help.ts adds dedicated help
blocks for open, pick, and add, updates the projects help to document
--open, and registers list as an alias of projects.

Scope and safety

  • squad open, squad pick, and squad list are read-only against the
    registry. They never write.
  • squad add DOES write to the registry: it calls registerProject, which adds
    or updates a single entry in projects.json. It does not scaffold .squad/,
    does not touch project contents, and does not change how squad init behaves.
    Re-adding an existing path updates in place and preserves created_at.
  • No change to PR1's behavior or to any project's .squad/ state.
  • Every command degrades gracefully on edge cases: empty registry, no match,
    ambiguous match, non-TTY invocation, missing-on-disk path, copilot not
    installed, nonexistent or non-directory add target, and spaced unquoted
    paths.

Tests / build

  • npm run build clean (tsc, both packages).
  • Lint clean (tsc --noEmit on both package tsconfigs).
  • Targeted suites green, 21 tests passing:
    • test/cli/add.test.ts (4): registers and shows up in squad list, clear
      error for a nonexistent path, spaced-path reconstruction from split argv
      tokens, and the quoting hint for a nonexistent multi-token path. All assert
      no stack trace leaks.
    • test/cli/command-help.test.ts (14): the command-help end-to-end suite,
      covering the help blocks and the command list now including the new verbs.
    • test/global-squad-isolation.test.ts (3): proves the new test harness
      redirects the global Squad path to an OS temp dir.
  • New test-isolation infrastructure: test/setup/isolate-global-squad.ts
    redirects resolveGlobalSquadPath() to a per-worker temp dir (via APPDATA,
    LOCALAPPDATA, XDG_CONFIG_HOME, and HOME), wired through vitest.config.ts
    as a setup file. Because PR1 made squad init write to the global
    projects.json, this prevents the test suite from polluting a developer's real
    registry.

Files (17 changed, +590/-4)

.changeset/squad-add-command.md
.changeset/squad-list-alias.md
.changeset/squad-open-command.md
.changeset/squad-pick-command.md
packages/squad-cli/src/cli-entry.ts
packages/squad-cli/src/cli/commands/add.ts
packages/squad-cli/src/cli/commands/open.ts
packages/squad-cli/src/cli/commands/pick.ts
packages/squad-cli/src/cli/commands/projects.ts
packages/squad-cli/src/cli/core/command-help.ts
packages/squad-sdk/src/index.ts
packages/squad-sdk/src/projects-registry.ts
test/cli/add.test.ts
test/cli/command-help.test.ts
test/global-squad-isolation.test.ts
test/setup/isolate-global-squad.ts
vitest.config.ts

Changeset

Four changeset files, all minor:

  • squad-open-command.md: @bradygaster/squad-cli minor, @bradygaster/squad-sdk minor.
  • squad-pick-command.md: @bradygaster/squad-cli minor.
  • squad-list-alias.md: @bradygaster/squad-cli minor.
  • squad-add-command.md: @bradygaster/squad-cli minor.

The SDK bump rides on the open/resolver changeset since resolveProject is a new
public SDK export; the other three are CLI-only.

brrichte_microsoft and others added 13 commits June 8, 2026 16:50
Every repo-level 'squad init' now records the project (name, path,
created_at) in a global projects.json under Squad's existing global home,
and a new 'squad projects' command lists them newest-first. The write is
additive, idempotent on path, and wrapped in try/catch so a registry
failure never blocks init.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…list

projects --open [name] delegates to runOpen, reusing the open command's picker,
resolver, and launch logic. Listing remains the default.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a new \squad pick\ command that completes the projects/pick/open trio.
The implementation is intentionally minimal and DRY: \
unPick\ forwards
only flag arguments (those starting with '-') to the existing \
unOpen\
function, stripping all positional arguments so the interactive numbered
picker is always entered. All picker rendering, TTY detection, empty-registry
messaging, print-path support, and Copilot launch logic are inherited from
open.ts without duplication. Wired in cli-entry.ts with a dynamic import
block parallel to \open\, added to the top-level help listing, and given a
dedicated entry in COMMAND_HELP between \personal\ and \preset\.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
crew->squad parity; list is an alias of projects

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…istry

PR1's registerProject-on-init writes to resolveGlobalSquadPath() (on Windows:
%APPDATA%\squad\projects.json) without isolation, causing the test suite to
pollute the developer's real global registry with junk entries on every run.

Add test/setup/isolate-global-squad.ts which, before any test runs, redirects
all env vars that resolveGlobalSquadPath() reads (APPDATA, LOCALAPPDATA,
XDG_CONFIG_HOME, HOME on non-Windows) to a fresh mkdtempSync() dir. Because
resolveGlobalSquadPath() reads process.env live on every call (no import-time
cache), this redirect propagates to every test and to child processes that
inherit process.env.

Register the setup file in vitest.config.ts via setupFiles.

Add test/global-squad-isolation.test.ts which asserts that the returned path
is under os.tmpdir() and contains 'squad-test-global-', proving isolation for
all tests in the suite.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduces \squad add [path] [--name <name>]\ which calls registerProject()
from the squad-sdk to record an existing directory in the global projects
registry (projects.json). No .squad/ scaffolding occurs; use \squad init\
for that. Completes the roster-management surface: init (scaffold+register),
add (register only), list/projects (view), open/pick (launch). Wired in
cli-entry.ts router and top-level help listing; documented in command-help.ts
as the first alphabetical COMMAND_HELP entry (before aspire).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
join remaining positional tokens into the path so an unquoted Windows path with spaces (e.g. "OneDrive - Microsoft\...") resolves correctly; existence-gated; clearer error suggests quoting. Fixes the silent wrong-add where only the first token was used.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 25, 2026 22:16
@github-actions

Copy link
Copy Markdown
Contributor

🟡 Impact Analysis — PR #1392

Risk tier: 🟡 MEDIUM

📊 Summary

Metric Count
Files changed 19
Files added 13
Files modified 6
Files deleted 0
Modules touched 4
Critical files 1

🎯 Risk Factors

  • 19 files changed (6-20 → MEDIUM)
  • 4 modules touched (2-4 → MEDIUM)
  • Critical files touched: packages/squad-sdk/src/index.ts

📦 Modules Affected

root (6 files)
  • .changeset/log-projects-at-init.md
  • .changeset/squad-add-command.md
  • .changeset/squad-list-alias.md
  • .changeset/squad-open-command.md
  • .changeset/squad-pick-command.md
  • vitest.config.ts
squad-cli (7 files)
  • packages/squad-cli/src/cli-entry.ts
  • packages/squad-cli/src/cli/commands/add.ts
  • packages/squad-cli/src/cli/commands/open.ts
  • packages/squad-cli/src/cli/commands/pick.ts
  • packages/squad-cli/src/cli/commands/projects.ts
  • packages/squad-cli/src/cli/core/command-help.ts
  • packages/squad-cli/src/cli/core/init.ts
squad-sdk (2 files)
  • packages/squad-sdk/src/index.ts
  • packages/squad-sdk/src/projects-registry.ts
tests (4 files)
  • test/cli/add.test.ts
  • test/cli/command-help.test.ts
  • test/global-squad-isolation.test.ts
  • test/setup/isolate-global-squad.ts

⚠️ Critical Files

  • packages/squad-sdk/src/index.ts

This report is generated automatically for every PR. See #733 for details.

@github-actions

Copy link
Copy Markdown
Contributor

🛫 PR Readiness Check

ℹ️ This comment updates on each push. Last checked: commit 83a4b8b

PR Scope: 📦🔧 Mixed (product + infrastructure)

⚠️ 5 item(s) to address before review

Status Check Details
Single commit 13 commits — consider squashing before review
Not in draft Ready for review
Branch up to date dev is 88 commit(s) ahead — rebase recommended
Copilot review No Copilot review yet — it may still be processing
Changeset present Changeset file found
Scope clean No .squad/ or docs/proposals/ files
No merge conflicts Merge conflicts detected — resolve before review
Copilot threads resolved No Copilot review threads
CI passing 5 check(s) still running

Files Changed (19 files, +772 −1)

File +/−
.changeset/log-projects-at-init.md +15 −0
.changeset/squad-add-command.md +7 −0
.changeset/squad-list-alias.md +7 −0
.changeset/squad-open-command.md +8 −0
.changeset/squad-pick-command.md +7 −0
packages/squad-cli/src/cli-entry.ts +29 −0
packages/squad-cli/src/cli/commands/add.ts +64 −0
packages/squad-cli/src/cli/commands/open.ts +160 −0
packages/squad-cli/src/cli/commands/pick.ts +23 −0
packages/squad-cli/src/cli/commands/projects.ts +60 −0
packages/squad-cli/src/cli/core/command-help.ts +57 −0
packages/squad-cli/src/cli/core/init.ts +10 −1
packages/squad-sdk/src/index.ts +2 −0
packages/squad-sdk/src/projects-registry.ts +143 −0
test/cli/add.test.ts +86 −0
test/cli/command-help.test.ts +5 −0
test/global-squad-isolation.test.ts +46 −0
test/setup/isolate-global-squad.ts +42 −0
vitest.config.ts +1 −0

Total: +772 −1


This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.

@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Architectural Review

⚠️ Architectural review: 2 warning(s).

Severity Category Finding Files
🟡 warning bootstrap-area 2 file(s) in the bootstrap area (packages/squad-cli/src/cli/core/) were modified. These files must maintain zero external dependencies. Review carefully. packages/squad-cli/src/cli/core/command-help.ts, packages/squad-cli/src/cli/core/init.ts
🟡 warning export-surface Package entry point(s) modified with 10 new/changed export(s). New public API surface requires careful review for backward compatibility. packages/squad-sdk/src/index.ts

Automated architectural review — informational only.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds actionable project navigation on top of the global projects registry by introducing squad open, squad pick, squad add, and the squad list alias (plus squad projects --open), along with SDK support for resolving a project from user input and test-suite isolation of global registry writes.

Changes:

  • Add CLI navigation commands: open (direct or interactive), pick (always interactive), add (register existing dir), and list (alias of projects), plus projects --open.
  • Extend the SDK with resolveProject() (and export registry helpers/types) for consistent project lookup behavior.
  • Add Vitest global setup to isolate the global Squad directory during tests, plus proof + E2E tests.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
.changeset/squad-add-command.md Changeset entry for new squad add CLI command.
.changeset/squad-list-alias.md Changeset entry for squad list aliasing squad projects.
.changeset/squad-open-command.md Changeset entry for squad open and projects --open.
.changeset/squad-pick-command.md Changeset entry for new squad pick CLI command.
.changeset/log-projects-at-init.md Changeset entry for init-time registry logging and squad projects.
packages/squad-cli/src/cli-entry.ts Wires new commands/aliases into CLI routing and top-level help output.
packages/squad-cli/src/cli/commands/add.ts Implements squad add to register an existing directory into the global registry.
packages/squad-cli/src/cli/commands/open.ts Implements squad open shared logic (resolver, picker, print-path, Copilot launch).
packages/squad-cli/src/cli/commands/pick.ts Implements squad pick by delegating to runOpen with positional args stripped.
packages/squad-cli/src/cli/commands/projects.ts Implements squad projects listing and delegates to runOpen for --open.
packages/squad-cli/src/cli/core/command-help.ts Adds help blocks and alias normalization for new/updated commands.
packages/squad-cli/src/cli/core/init.ts Registers projects during non-global squad init (best-effort).
packages/squad-sdk/src/index.ts Exports projects registry APIs/types (including resolveProject).
packages/squad-sdk/src/projects-registry.ts Implements registry read/register plus resolveProject() query resolution.
test/cli/add.test.ts E2E tests covering squad add behavior, spaced paths, and error messaging.
test/cli/command-help.test.ts Updates help/alias tests for new commands and list alias.
test/global-squad-isolation.test.ts Proof test asserting global path isolation during the test suite.
test/setup/isolate-global-squad.ts Vitest setup file redirecting global Squad path env vars to a temp dir.
vitest.config.ts Registers the Vitest setup file for global-path isolation.

Comment on lines +100 to +102
* Resolution order (all comparisons are case-insensitive on platforms where
* CASE_INSENSITIVE is true):
*
Comment on lines +133 to +135
const exactPath = entries.filter(e => samePath(path.resolve(e.path), resolvedQuery));
if (exactPath.length === 1) return { match: exactPath[0]! };

Comment on lines +121 to +123
const isTTY = process.stdout.isTTY === true;
if (!isTTY) {
const sorted = sortedNewestFirst(entries);
Comment on lines +112 to +116
console.log(`Multiple projects match "${query}":`);
for (const candidate of result.ambiguous) {
console.log(` ${candidate.name}`);
}
console.log('Be more specific.');
Comment on lines +11 to +13
import path from 'node:path';
import fs from 'node:fs';
import { readProjectsRegistry, registerProject } from '@bradygaster/squad-sdk';
Comment on lines +26 to +29
if (nameIdx !== -1) {
name = args[nameIdx + 1];
remaining = args.filter((_, i) => i !== nameIdx && i !== nameIdx + 1);
}
const entries = readProjectsRegistry();
const alreadyRegistered = entries.some(e => samePath(path.resolve(e.path), absPath));

registerProject(displayName, absPath);
expect(globalPath).toContain('squad-test-global-');
});

it('resolveGlobalSquadPath() is NOT the real %APPDATA%\\squad', () => {
Comment on lines +40 to +44
} else {
// On non-Windows, should not be under ~/.config/squad or ~/Library/...
const realHome = join(process.env['USERPROFILE'] ?? '/nonexistent', 'Library', 'Application Support', 'squad');
expect(globalPath).not.toBe(realHome);
}
@tamirdresher

Copy link
Copy Markdown
Collaborator

✅ Reviewed — clean feature, well-tested. Approving.

🔒 One security note: In open.ts, launchCopilot() uses spawn('copilot', [], { cwd: entry.path, shell: true }) on Windows. With shell: true, cmd.exe checks CWD for .cmd/.bat before PATH — so a malicious copilot.cmd planted in a registered project dir would execute instead of the real binary.

Suggested fix: Resolve copilot to its absolute path before spawning, then use shell: false. Or reuse the existing resolveCopilotCmd() pattern from agent-spawn.ts.

Low urgency (requires attacker to plant file in a cloned repo), but worth a follow-up patch. Ship it as-is if you're ok with it. 🚀

— Tamir (via Squad)

@tamirdresher tamirdresher left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. See my comment re: shell:true CWD hijack on Windows — non-blocking, follow-up fix recommended.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants