feat: add project-navigation commands (open, pick, list, add)#1392
feat: add project-navigation commands (open, pick, list, add)#1392barichter wants to merge 13 commits into
Conversation
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>
🟡 Impact Analysis — PR #1392Risk tier: 🟡 MEDIUM 📊 Summary
🎯 Risk Factors
📦 Modules Affectedroot (6 files)
squad-cli (7 files)
squad-sdk (2 files)
tests (4 files)
|
🛫 PR Readiness Check
PR Scope: 📦🔧 Mixed (product + infrastructure)
|
| 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.
🏗️ Architectural Review
Automated architectural review — informational only. |
There was a problem hiding this comment.
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), andlist(alias ofprojects), plusprojects --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. |
| * Resolution order (all comparisons are case-insensitive on platforms where | ||
| * CASE_INSENSITIVE is true): | ||
| * |
| const exactPath = entries.filter(e => samePath(path.resolve(e.path), resolvedQuery)); | ||
| if (exactPath.length === 1) return { match: exactPath[0]! }; | ||
|
|
| const isTTY = process.stdout.isTTY === true; | ||
| if (!isTTY) { | ||
| const sorted = sortedNewestFirst(entries); |
| console.log(`Multiple projects match "${query}":`); | ||
| for (const candidate of result.ambiguous) { | ||
| console.log(` ${candidate.name}`); | ||
| } | ||
| console.log('Be more specific.'); |
| import path from 'node:path'; | ||
| import fs from 'node:fs'; | ||
| import { readProjectsRegistry, registerProject } from '@bradygaster/squad-sdk'; |
| 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', () => { |
| } 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); | ||
| } |
|
✅ Reviewed — clean feature, well-tested. Approving. 🔒 One security note: In Suggested fix: Resolve 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
left a comment
There was a problem hiding this comment.
Approved. See my comment re: shell:true CWD hijack on Windows — non-blocking, follow-up fix recommended.
What
PR1 added a global registry of every project where
squad initruns, plus aread-only
squad projectslist. This PR makes that registry actionable. It addsfour 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, thenstarts
copilotin that directory.squad openwith no name, on a TTY, shows a numbered picker.squad openwith no name, when stdout is not a TTY, prints the project listand a hint to pass a name, rather than blocking on a prompt.
squad open <name> --print-path(-p) prints the resolved path instead oflaunching, for shell integrations like
cd (squad open foo -p).squad pickAn 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 openafter manually finding a name in the list.Positional arguments are stripped so it always enters the picker; flags such as
--print-pathare forwarded.squad listA convenience alias for
squad projects. It accepts the same flags, including--open, and behaves identically. Existingsquad projectsusage is unchanged.squad add [path]Registers a pre-existing directory in the global registry so it shows up in
squad projects,squad list, andsquad pick, without re-runningsquad initand without scaffolding a.squad/directory.squad add <path>: registers the directory at that path. With no path, itregisters the current directory.
squad add <path> --name <name>: sets a custom display name. Without it, thedirectory basename is used.
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 openis the core verb,squad pickis the discoverable interactive form,squad listis muscle-memoryergonomics, and
squad addlets you backfill projects that predate the registryor that you set up by hand.
How
SDK resolver:
resolveProject(query)inprojects-registry.ts. Resolutionorder, first match wins:
{ match }; several with the same name return{ ambiguous }.samePathon the resolved absolute paths.{ 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, reusedhere by
squad add). It is idempotent: re-registering a path that already existsupdates that entry in place and preserves the original
created_at, rather thanadding a duplicate.
squad addfirst checks whether the path is already presentso it can report "Added" versus "Updated" accurately.
Command implementations:
open.tsowns all shared opener logic: the resolver call, the empty-registryand 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: trueon win32) and inherits stdio. Ifcopilotis not on PATH (ENOENT), it prints the path with a hint instead ofcrashing.
pick.tsdelegates torunOpenwith positional args removed, so all pickerand launch behavior is reused with no duplication.
projects.tsgains an--open/-obranch that delegates torunOpen;listing remains the default.
add.tsparses--name, rejoins remaining tokens into a path, resolves it toabsolute, validates it exists and is a directory, then calls
registerProject.cli-entry.tswiresopen,pick, andaddto their handlers and routes bothprojectsandlisttorunProjects.command-help.tsadds dedicated helpblocks for
open,pick, andadd, updates theprojectshelp to document--open, and registerslistas an alias ofprojects.Scope and safety
squad open,squad pick, andsquad listare read-only against theregistry. They never write.
squad addDOES write to the registry: it callsregisterProject, which addsor updates a single entry in
projects.json. It does not scaffold.squad/,does not touch project contents, and does not change how
squad initbehaves.Re-adding an existing path updates in place and preserves
created_at..squad/state.ambiguous match, non-TTY invocation, missing-on-disk path,
copilotnotinstalled, nonexistent or non-directory
addtarget, and spaced unquotedpaths.
Tests / build
npm run buildclean (tsc, both packages).tsc --noEmiton both package tsconfigs).test/cli/add.test.ts(4): registers and shows up insquad list, clearerror 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 harnessredirects the global Squad path to an OS temp dir.
test/setup/isolate-global-squad.tsredirects
resolveGlobalSquadPath()to a per-worker temp dir (viaAPPDATA,LOCALAPPDATA,XDG_CONFIG_HOME, andHOME), wired throughvitest.config.tsas a setup file. Because PR1 made
squad initwrite to the globalprojects.json, this prevents the test suite from polluting a developer's realregistry.
Files (17 changed, +590/-4)
Changeset
Four changeset files, all minor:
squad-open-command.md:@bradygaster/squad-climinor,@bradygaster/squad-sdkminor.squad-pick-command.md:@bradygaster/squad-climinor.squad-list-alias.md:@bradygaster/squad-climinor.squad-add-command.md:@bradygaster/squad-climinor.The SDK bump rides on the open/resolver changeset since
resolveProjectis a newpublic SDK export; the other three are CLI-only.