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
5 changes: 4 additions & 1 deletion src/commands/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export function setupAuthCommands(program: Command): void {
);
}
} catch {
// No token found anywhere, proceed with login
// resolveApiToken throws when no token exists in any source — expected for fresh installs
}
}

Expand Down Expand Up @@ -211,6 +211,7 @@ export function setupAuthCommands(program: Command): void {
token = resolved.token;
source = resolved.source;
} catch {
// resolveApiToken throws when no token is configured — not an error, just unauthenticated
outputSuccess({
authenticated: false,
message:
Expand All @@ -227,6 +228,7 @@ export function setupAuthCommands(program: Command): void {
user: { id: viewer.id, name: viewer.name, email: viewer.email },
});
} catch {
// validateApiToken throws on invalid/expired/revoked tokens
outputSuccess({
authenticated: false,
source: SOURCE_LABELS[source],
Expand Down Expand Up @@ -255,6 +257,7 @@ export function setupAuthCommands(program: Command): void {
warning: `A token is still active via ${SOURCE_LABELS[source]}.`,
});
} catch {
// no other token source active — clean logout
outputSuccess({ message: "Authentication token removed." });
}
}),
Expand Down
1 change: 1 addition & 0 deletions src/commands/documents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export function extractDocumentIdFromUrl(url: string): string | null {

return docSlug.substring(lastHyphenIndex + 1) || null;
} catch {
// malformed URL → not a valid document link
return null;
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/common/embed-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function isLinearUploadUrl(url: string): boolean {
const urlObj = new URL(url);
return urlObj.hostname === "uploads.linear.app";
} catch {
// malformed URL → not a linear upload URL
return false;
}
}
Expand All @@ -69,6 +70,7 @@ export function extractFilenameFromUrl(url: string): string {
const parts = new URL(url).pathname.split("/");
return parts[parts.length - 1] || "download";
} catch {
// malformed URL → fall back to generic filename
return "download";
}
}
1 change: 1 addition & 0 deletions src/common/identifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export function tryParseIssueIdentifier(
try {
return parseIssueIdentifier(identifier);
} catch {
// parseIssueIdentifier throws on malformed identifiers — expected for non-issue inputs
return null;
}
}
13 changes: 11 additions & 2 deletions src/common/token-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ export function getStoredToken(): string | null {
try {
const encrypted = fs.readFileSync(legacy, "utf8").trim();
return decryptToken(encrypted);
} catch {
} catch (err) {
// file exists but can't be decrypted — warn instead of silently returning null
const detail = err instanceof Error ? err.message : String(err);
console.error(
`Warning: stored token at ${legacy} is corrupted: ${detail}`,
);
return null;
}
Comment on lines 57 to 67
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

Legacy token fallback catch block also logs "... is corrupted" for any readFileSync()/decryptToken() error. If the file exists but is unreadable (EACCES/EPERM) this message is misleading; consider checking errno codes and using a different warning for access errors, reserving the corruption warning for decryption failures.

Copilot uses AI. Check for mistakes.
}
Expand All @@ -67,7 +72,11 @@ export function getStoredToken(): string | null {
try {
const encrypted = fs.readFileSync(tokenPath, "utf8").trim();
return decryptToken(encrypted);
} catch {
} catch (err) {
const detail = err instanceof Error ? err.message : String(err);
console.error(
`Warning: stored token at ${tokenPath} is corrupted: ${detail}`,
);
return null;
Comment on lines 72 to 80
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

getStoredToken() now logs "... is corrupted" for any error thrown by readFileSync() or decryptToken(). That can produce misleading warnings for non-corruption cases (e.g., EACCES/EPERM permission issues, or ENOENT if the file is deleted between existsSync() and readFileSync()). Consider splitting the read/decrypt steps (or checking NodeJS.ErrnoException.code) so that ENOENT still returns null silently, permission errors report as access problems, and only decryption/parse failures are labeled as corruption.

Copilot uses AI. Check for mistakes.
}
}
Expand Down
14 changes: 11 additions & 3 deletions src/services/file-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export class FileService {
error: `File already exists: ${outputPath}. Use --overwrite to replace.`,
};
} catch {
// File doesn't exist, we can proceed
// access() throws ENOENT when file doesn't exist — that's the expected path
}
}

Expand Down Expand Up @@ -266,10 +266,18 @@ export class FileService {
// Check if file exists
try {
await access(filePath);
} catch {
} catch (err) {
// access() fails with ENOENT for missing files, EACCES for permission issues
const detail =
err instanceof Error && "code" in err
? (err as NodeJS.ErrnoException).code
: undefined;
return {
success: false,
error: `File not found: ${filePath}`,
error:
detail === "ENOENT"
? `File not found: ${filePath}`
: `Cannot access file: ${filePath} (${detail || (err instanceof Error ? err.message : String(err))})`,
};
Comment on lines +269 to 281
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

uploadFile() intends to surface the underlying access error, but for errors that do have a .code the message currently only includes the code (e.g., "(EACCES)") and drops the actual error message/path details. Consider including both code and the original err.message when available so the returned error is consistently actionable (especially for permission issues).

Copilot uses AI. Check for mistakes.
Comment on lines +269 to 281
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

New behavior distinguishes ENOENT vs other access() failures, but tests only cover ENOENT. Add a unit test for a non-ENOENT failure (e.g., EACCES) to assert the new "Cannot access file" branch and message formatting.

Copilot uses AI. Check for mistakes.
}

Expand Down
5 changes: 4 additions & 1 deletion tests/unit/services/file-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,10 @@ describe("downloadFile", () => {

describe("uploadFile", () => {
it("returns error when file not found", async () => {
vi.mocked(access).mockRejectedValue(new Error("ENOENT"));
const err = Object.assign(new Error("ENOENT: no such file or directory"), {
code: "ENOENT",
});
vi.mocked(access).mockRejectedValue(err);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice — the mock now properly simulates a real Node.js filesystem error with code. Since the implementation branches on code === "ENOENT", consider adding a parallel test for the other branch:

it("returns descriptive error on permission denied", async () => {
  const err = Object.assign(new Error("EACCES: permission denied"), {
    code: "EACCES",
  });
  vi.mocked(access).mockRejectedValue(err);

  const service = new FileService(TEST_TOKEN);
  const result = await service.uploadFile("/path/to/restricted.png");

  expect(result.success).toBe(false);
  expect(result.error).toContain("Cannot access file");
});

const service = new FileService(TEST_TOKEN);
const result = await service.uploadFile("/path/to/missing.png");
Expand Down
Loading