From 1da8d2f530542dc3620d2fe0cf7b02fda8d3f9be Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Mon, 5 Oct 2026 20:33:06 -0700 Subject: [PATCH 1/2] fix(auth): give steps that actually untrack a committed token file The tracked-token warning only said to gitignore .taskless/.env.local.json, which does nothing for a file git already tracks. It now says to run git rm --cached, check .taskless/.gitignore, and replace the token if the commit was pushed, noting that auth logout does not revoke it. --- .changeset/tracked-token-warning.md | 5 +++++ packages/cli/src/auth/token.ts | 10 ++++++++- packages/cli/test/token.test.ts | 35 +++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 .changeset/tracked-token-warning.md diff --git a/.changeset/tracked-token-warning.md b/.changeset/tracked-token-warning.md new file mode 100644 index 00000000..983cd44e --- /dev/null +++ b/.changeset/tracked-token-warning.md @@ -0,0 +1,5 @@ +--- +"@taskless/cli": patch +--- + +When `.taskless/.env.local.json` is tracked by git, the warning now gives the steps that help: `git rm --cached` to untrack it, check `.taskless/.gitignore`, and if the commit was pushed, replace the token with `auth logout` then `auth login`. Before, it only said to gitignore the file, which does nothing for a file git already tracks. diff --git a/packages/cli/src/auth/token.ts b/packages/cli/src/auth/token.ts index 150c6215..d7afefa6 100644 --- a/packages/cli/src/auth/token.ts +++ b/packages/cli/src/auth/token.ts @@ -138,8 +138,16 @@ async function warnIfTracked(cwd: string): Promise { }); }); if (output.length > 0) { + // Already in the index, so a .gitignore entry alone changes nothing: + // git keeps tracking a file it already tracks. + const cli = getCliPrefix(); console.error( - "Warning: .taskless/.env.local.json is tracked by git. This file contains authentication tokens and should be gitignored." + [ + `Warning: ${relativePath} is tracked by git. It contains an authentication token.`, + ` 1. Untrack it and keep your local copy: git rm --cached ${relativePath}`, + " 2. Make sure .taskless/.gitignore lists .env.local.json, then commit.", + ` 3. If a commit containing it was pushed, treat the token as exposed and replace it: \`${cli} auth logout\`, then \`${cli} auth login\`. Logout only deletes the local copy, so it does not revoke the old token.`, + ].join("\n") ); } } catch { diff --git a/packages/cli/test/token.test.ts b/packages/cli/test/token.test.ts index 5956ea70..b08f4f05 100644 --- a/packages/cli/test/token.test.ts +++ b/packages/cli/test/token.test.ts @@ -1,3 +1,4 @@ +import { execFileSync } from "node:child_process"; import { mkdtemp, readFile, rm, writeFile, mkdir } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -14,6 +15,7 @@ beforeEach(async () => { afterEach(async () => { vi.useRealTimers(); + vi.restoreAllMocks(); vi.unstubAllEnvs(); await rm(temporaryDirectory, { recursive: true, force: true }); }); @@ -169,3 +171,36 @@ describe("removeToken", () => { expect(await removeToken()).toBe(false); }); }); + +describe("tracked token warning", () => { + it("tells the user to untrack the file, not just gitignore it", async () => { + await writeAuthFile({ access_token: "tracked-token" }); + execFileSync("git", ["init", "-q"], { cwd: temporaryDirectory }); + execFileSync("git", ["add", ".taskless/.env.local.json"], { + cwd: temporaryDirectory, + }); + const errors: string[] = []; + vi.spyOn(console, "error").mockImplementation((message: unknown) => { + errors.push(String(message)); + }); + + expect(await getToken(temporaryDirectory)).toBe("tracked-token"); + + const warning = errors.find((line) => line.includes("tracked by git")); + expect(warning).toContain("git rm --cached .taskless/.env.local.json"); + expect(warning).toContain("auth logout"); + expect(warning).toContain("auth login"); + expect(warning).toContain("does not revoke"); + }); + + it("stays quiet when the file is not tracked", async () => { + await writeAuthFile({ access_token: "untracked-token" }); + execFileSync("git", ["init", "-q"], { cwd: temporaryDirectory }); + const spy = vi.spyOn(console, "error").mockImplementation(() => {}); + + expect(await getToken(temporaryDirectory)).toBe("untracked-token"); + expect( + spy.mock.calls.some((call) => String(call[0]).includes("tracked by git")) + ).toBe(false); + }); +}); From fc6d8555caddf9ce9a8c467a3db756fc6a645ce1 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Mon, 5 Oct 2026 21:27:11 -0700 Subject: [PATCH 2/2] refactor(auth): build the token filename from PER_REPO_AUTH_FILE everywhere The gitignore entry saveToken writes, the path the tracked-file check queries, and the warning text each spelled .env.local.json out, so a rename in one place would quietly desync the others. Most damaging would be the gitignore entry drifting from the file it is meant to ignore. --- packages/cli/src/auth/token.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/auth/token.ts b/packages/cli/src/auth/token.ts index d7afefa6..7223987a 100644 --- a/packages/cli/src/auth/token.ts +++ b/packages/cli/src/auth/token.ts @@ -68,7 +68,7 @@ export async function saveToken( const tasklessDirectory = join(cwd, ".taskless"); await mkdir(tasklessDirectory, { recursive: true }); - await addToGitignore(cwd, [".env.local.json"]); + await addToGitignore(cwd, [PER_REPO_AUTH_FILE]); await writeFile(join(tasklessDirectory, PER_REPO_AUTH_FILE), content, { mode: 0o600, }); @@ -126,7 +126,7 @@ function warnIfLegacyToken(): void { /** Warn if .env.local.json is tracked by git */ async function warnIfTracked(cwd: string): Promise { - const relativePath = ".taskless/.env.local.json"; + const relativePath = `.taskless/${PER_REPO_AUTH_FILE}`; try { const output = await new Promise((resolve, reject) => { execFile("git", ["ls-files", relativePath], { cwd }, (error, stdout) => { @@ -145,7 +145,7 @@ async function warnIfTracked(cwd: string): Promise { [ `Warning: ${relativePath} is tracked by git. It contains an authentication token.`, ` 1. Untrack it and keep your local copy: git rm --cached ${relativePath}`, - " 2. Make sure .taskless/.gitignore lists .env.local.json, then commit.", + ` 2. Make sure .taskless/.gitignore lists ${PER_REPO_AUTH_FILE}, then commit.`, ` 3. If a commit containing it was pushed, treat the token as exposed and replace it: \`${cli} auth logout\`, then \`${cli} auth login\`. Logout only deletes the local copy, so it does not revoke the old token.`, ].join("\n") );