fix(docs): sharing.docs.localDir is resolved against the process cwd, so the bundle follows wherever teamai ran
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 4.8k
- Forks
- 342
- Avg merge
- 13h 48m
- Merged PRs (30d)
- 211
Description
Problem
sharing.docs.localDir is resolved against the process cwd whenever it is not spelled ~/…. The team docs bundle therefore lands wherever teamai happened to be run from, and three commands each fail differently because of it — including one that deletes a directory it does not own.
This is reachable with the config teamai init itself generates. For project scope it writes a relative value (src/init.ts:845, src/init.ts:1381):
docs: { localDir: scope === 'project' ? './.teamai/docs' : '~/.teamai/docs' }
// ^^^^^^^^^^^^^^^^ relative
detectProjectConfig was deliberately made cwd-independent for #374 — "a repo with both a partition and a legacy .teamai/ must resolve to the SAME config whether run from the root or a subdirectory" (src/config.ts:278-281). The docs destination is the same property one step later in the pipeline, and it does not hold.
Cause
resolveDocsDestination (src/resources/docs.ts:13-19) has two gates and both test for ~/. Anything else falls through as a bare relative string:
export function resolveDocsDestination(teamConfig: TeamaiConfig, localConfig: LocalConfig): string {
const localDir = teamConfig.sharing.docs.localDir;
if (localConfig.scope === 'project' && localConfig.projectRoot && localDir.startsWith('~/')) {
return path.join(localConfig.projectRoot, localDir.substring(2)); // anchored
}
return expandHome(localDir); // expandHome only matches '~/' too (src/utils/fs.ts:22)
}
flowchart LR
I["sharing.docs.localDir"] --> G{"starts with '~/' ?"}
G -->|yes, project scope| A["join(projectRoot, …) — stable"]
G -->|yes, user scope| B["join(home, …) — stable"]
G -->|"no — './.teamai/docs'"| C["returned unchanged"]
C --> D["fse.copy / pathExists resolve it<br>against process.cwd()"]
D --> E["destination follows the cwd"]
teamai.yaml does not constrain the value either — localDir: z.string().default('~/.teamai/docs') (src/types.ts:56).
Impact
All three verified against the real CLI, project scope, localDir: ./.teamai/docs, commands run from <projectRoot>/packages/web:
1. teamai pull writes a second bundle and reports success
teamai pull --force (from <projectRoot>) -> <projectRoot>/.teamai/docs/guide.md
teamai pull --force (from <projectRoot>/packages/web)
-> <projectRoot>/.teamai/docs/guide.md
-> <projectRoot>/packages/web/.teamai/docs/guide.md
This is the shape #598 was filed about: the command says "Synced N docs" and they are not where they belong.
2. teamai doctor reports a false failure, with advice that makes it worse
Pulled once from the project root, then asked from the subdirectory:
✖ Team docs delivered
→ Missing from ./.teamai/docs: guide.md. Run `teamai pull --force`: a plain pull
skips a scope whose team repo has not changed, so it cannot restore these.
Nothing is missing. Following the fix creates the duplicate from (1). The Team docs delivered check added in #625 resolves through the same function, so it cannot catch this — it is self-consistent with the write path and simply relocates with it.
3. teamai uninstall deletes a directory it does not own, and misses the real one
src/uninstall.ts:516-524 re-implements the same logic inline rather than calling resolveDocsDestination. With an unrelated packages/web/.teamai/docs/my-notes.md present:
$ cd packages/web && teamai uninstall --force
Docs directory:
./.teamai/docs
✔ Removed docs: ./.teamai/docs
after:
<projectRoot>/.teamai/docs/guide.md present <- the real bundle, not cleaned up
packages/web/.teamai/docs/my-notes.md DELETED <- collateral, never part of the bundle
Platforms
The defect is platform-independent: Node resolves a relative path against the cwd on all three. Windows is worse in two ways. Modelling both path flavours through the same two gates, and comparing the destination resolved from two different cwds in one project:
posix './.teamai/docs' DRIFT /repo/project/.teamai/docs
/repo/project/packages/web/.teamai/docs
'~/.teamai/docs' stable
win32 './.teamai/docs' DRIFT C:\repo\project\.teamai\docs
D:\scratch\.teamai\docs <- a different volume
'~\.teamai\docs' DRIFT D:\scratch\~\.teamai\docs <- literal "~" directory
'~/.teamai/docs' stable
- On Windows the cwd can sit on another drive, so the bundle can leave the project's volume entirely.
~\…, the natural Windows spelling, matches neither gate:expandHometestsstartsWith('~/')only, so it is never expanded and a directory literally named~is created under the cwd. A Windows user who hand-writes the setting the obvious way gets this silently.
Repro
Project scope, sharing.docs.localDir: ./.teamai/docs (as teamai init writes it):
cd <projectRoot> && teamai pull --force→ bundle at<projectRoot>/.teamai/docscd packages/web && teamai doctor→✖ Team docs delivered — Missing from ./.teamai/docscd packages/web && teamai pull --force→ a second bundle appears underpackages/webmkdir -p .teamai/docs && touch .teamai/docs/my-notes.md && teamai uninstall --force→my-notes.mdis deleted,<projectRoot>/.teamai/docssurvives
Scripts used are throwaway; happy to attach them if useful.
Suggested direction
Anchor a relative localDir to the scope's base directory rather than the cwd, and make uninstall call the one resolver instead of its own copy:
export function resolveDocsDestination(teamConfig: TeamaiConfig, localConfig: LocalConfig): string {
const localDir = teamConfig.sharing.docs.localDir;
if (localConfig.scope === 'project' && localConfig.projectRoot && localDir.startsWith('~/')) {
return path.join(localConfig.projectRoot, localDir.substring(2));
}
- return expandHome(localDir);
+ const expanded = expandHome(localDir);
+ // A relative localDir would otherwise resolve against the process cwd, so the
+ // destination would follow whichever directory the command was run from.
+ return path.isAbsolute(expanded) ? expanded : path.join(resolveBaseDir(localConfig), expanded);
}
Worth deciding alongside it:
- Migration. Existing checkouts already have a bundle at the old cwd-relative location. Moving the destination silently orphans it; the
Team docs deliveredcheck would then report it missing, which is at least honest but noisy on first upgrade. - Whether
~\should be accepted. Either teachexpandHomethe Windows spelling, or reject a non-~/, non-absolute value at config parse time so the failure is a clear error rather than a directory named~. uninstall's duplicate. #625 extractedresolveDocsDestinationprecisely so the read and write paths agree;uninstall.tsstill has its own copy, which is how it can delete the wrong directory.
I have not written the fix — filing first because the migration question and the ~\ decision are both judgement calls.
Found while reviewing the docs delivery check in #625.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with resolveDocsDestination in src/resources/docs.ts, then compare the duplicate destination logic in src/uninstall.ts; review expandHome in src/utils/fs.ts and the project-scope config paths in src/init.ts. Verify pull, doctor, and uninstall from the project root and a subdirectory, including Windows path behavior, and resolve the migration and ~\ handling decisions before considering the work done.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100