Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

## Features

- **OAuth login** β€” authenticate once via browser; credentials are stored securely and reused across sessions.
- **Two ways to log in** β€” email and password by default, needing no setup and reaching the whole API; or `--oauth` for a browser flow that never handles your password. Credentials are stored securely and reused across sessions.
- **File & folder management** β€” list, stat, copy, move, rename, and delete files and folders from the terminal.
- **Revision history** β€” inspect every saved revision of a file and revert to any earlier version in one command.
- **Trash recovery** β€” list deleted files and restore them by file ID.
Expand Down
90 changes: 68 additions & 22 deletions docs/authentication.mdx
Original file line number Diff line number Diff line change
@@ -1,60 +1,106 @@
---
title: πŸ” Authentication
description: One-time OAuth 2.0 setup β€” log in through the browser and store a token locally.
description: Log in with email and password for a session token, or through the browser with OAuth 2.0.
---

Every command needs an authenticated pCloud session. `pcloud login` runs a
browser-based OAuth 2.0 flow once and stores the resulting token locally; after
that, commands just work.
Every command needs an authenticated pCloud session. There are two ways to get
one, and they are not equivalent β€” pick deliberately.

## πŸ› οΈ Create an OAuth application
| | `pcloud login` (session) | `pcloud login --oauth` |
| -------------------------------- | ------------------------- | ----------------------------------- |
| Setup | none | register an OAuth application first |
| You type your password | yes, once | never |
| Revisions, trash, zip, downloads | βœ… | ❌ not reachable |
| Expiry | 30 days, or 7 days unused | no fixed expiry |

You need a pCloud OAuth application to obtain a client ID and secret. Create one
from the [pCloud OAuth 2.0 documentation](https://docs.pcloud.com/methods/oauth_2.0/),
then expose the credentials in your shell or a `.env` file:
Session login is the default because it needs no setup and reaches the whole API.
OAuth exists for when you would rather the CLI never handled your password.

## πŸ”‘ Log in

```bash
pcloud login
```

You are asked for your email and password, and for a two-factor code if your
account uses one. The password is sent to pCloud over HTTPS and is never written
to disk β€” only the returned token is stored, at `~/.config/pcloud/tokens.json`
(mode `0600`).

Confirm it worked:

```bash
pcloud whoami
```

This prints the account email, plan, and quota usage.

## 🌐 Log in through the browser instead

OAuth 2.0 keeps your password out of the CLI entirely, at the cost of a one-time
setup and a narrower API surface.

First create a pCloud OAuth application from the
[pCloud OAuth 2.0 documentation](https://docs.pcloud.com/methods/oauth_2.0/) and
expose its credentials:

```bash
export PCLOUD_CLIENT_ID=your_client_id
export PCLOUD_CLIENT_SECRET=your_client_secret
```

## πŸ”‘ Log in
Then:

```bash
pcloud login
pcloud login --oauth
```

This opens your browser to the pCloud authorisation page. After you approve
access, pCloud redirects back to a local callback server and the token is saved
to `~/.config/pcloud/tokens.json` (mode `0600`). The CLI never sees your pCloud
password.
to the same place.

Once logged in, confirm the session:
> **OAuth cannot reach everything.** pCloud's access tokens do not grant access to
> `trash_list` / `trash_restore`, and the same applies to revisions, zip and
> downloads. This is a limitation of the pCloud API, not of this CLI β€” there is no
> workaround. If you need those commands, use session login.

If both are stored, the session token wins: it is the strictly more capable tier.

## πŸšͺ Log out

```bash
pcloud whoami
pcloud logout
```

This prints the account email, plan, and quota usage.

## πŸšͺ Log out
This revokes the session token with pCloud before removing the local file, so the
token is dead on their side too β€” deleting the file alone would leave it live
until it expired.

Remove the stored credentials at any time:
## 🩺 Check what your credential can reach

```bash
pcloud logout
pcloud doctor
```

Probes every command against your current credential and reports which are
reachable, which need a session token, and which call endpoints pCloud does not
expose. It also reports local sync-daemon health β€” see
[Local sync inspection](/projects/pcloud-cli/docs/sync).

## πŸ€– Bypassing the credential store

For CI or scripted contexts, set `PCLOUD_ACCESS_TOKEN` directly. When present, no
stored credentials are read or written and `login` / `logout` are unnecessary:
For CI or scripted contexts, set a token directly in the environment. When
present, no stored credentials are read or written and `login` / `logout` are
unnecessary:

```bash
export PCLOUD_ACCESS_TOKEN=your_access_token
export PCLOUD_ACCESS_TOKEN=your_access_token # OAuth tier
export PCLOUD_AUTH=your_session_token # session tier
pcloud ls /
```

`PCLOUD_AUTH` takes precedence, matching the stored-credential rule above.

## 🌍 Choosing a region

The CLI targets the EU API (`eapi.pcloud.com`) by default. If your account lives
Expand Down
46 changes: 27 additions & 19 deletions docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ description: A terminal-first CLI for pCloud β€” list folders, restore deleted f
manage files, recover what you deleted from the trash, and roll a file back to a
previous version β€” all without leaving the command line.

It authenticates with pCloud over OAuth 2.0, storing a token locally so you only
log in once. A single `PCLOUD_ACCESS_TOKEN` environment variable lets you bypass
stored credentials entirely for CI and scripted contexts.
It authenticates once and stores the token locally. A single environment variable
lets you bypass stored credentials entirely for CI and scripted contexts.

## ✨ Features

Expand All @@ -18,11 +17,15 @@ stored credentials entirely for CI and scripted contexts.
- **Trash recovery** β€” list deleted files and restore them by file ID.
- **Rewind & revisions** β€” browse a file's version history and restore an earlier
version to any destination.
- **OAuth 2.0 authentication** β€” browser-based login; the token is stored locally
at `~/.config/pcloud/tokens.json` (mode `0600`). The CLI never sees your
password.
- **CI-friendly** β€” set `PCLOUD_ACCESS_TOKEN` to skip the credential store
entirely.
- **Local sync inspection** β€” read the pCloud Drive daemon's own database to find
broken sync pairs, and prune orphaned ones the desktop app reports only as a
bogus permissions error.
- **Two ways to log in** β€” email and password by default (no setup, full API
access), or browser-based OAuth 2.0 if you would rather the CLI never handled
your password. Tokens are stored at `~/.config/pcloud/tokens.json` (mode
`0600`).
- **CI-friendly** β€” set `PCLOUD_AUTH` or `PCLOUD_ACCESS_TOKEN` to skip the
credential store entirely.
- **Regional endpoints** β€” targets the EU API by default; switch with
`PCLOUD_REGION=us`.

Expand All @@ -32,7 +35,7 @@ stored credentials entirely for CI and scripted contexts.
npm install -g @kud/pcloud-cli
```

Requires Node.js 20 or newer. The binary is exposed as `pcloud`.
Requires Node.js 24 or newer (the sync commands use the built-in `node:sqlite`). The binary is exposed as `pcloud`.

## πŸš€ Quick start

Expand All @@ -44,17 +47,22 @@ pcloud ls /
pcloud list-rewind /Documents/report.pdf
```

See [Authentication](/projects/pcloud-cli/docs/authentication) for the one-time
OAuth setup, then [Trash & restore](/projects/pcloud-cli/docs/trash) and
[Rewind & versions](/projects/pcloud-cli/docs/rewind) for recovery workflows.
See [Authentication](/projects/pcloud-cli/docs/authentication) for both login
methods and when to choose each, [Trash & restore](/projects/pcloud-cli/docs/trash)
and [Rewind & versions](/projects/pcloud-cli/docs/rewind) for recovery workflows,
and [Local sync inspection](/projects/pcloud-cli/docs/sync) for diagnosing the
desktop app's sync pairs.

## βš™οΈ Configuration

| Variable | Required | Description |
| ---------------------- | ---------------- | ------------------------------------------------- |
| `PCLOUD_CLIENT_ID` | For `login` only | OAuth application client ID |
| `PCLOUD_CLIENT_SECRET` | For `login` only | OAuth application client secret |
| `PCLOUD_ACCESS_TOKEN` | Optional | Bypasses `~/.config/pcloud/tokens.json` entirely |
| `PCLOUD_REGION` | Optional | `eu` (default) or `us` β€” selects the API endpoint |
| Variable | Required | Description |
| ---------------------- | ------------------- | ------------------------------------------------- |
| `PCLOUD_CLIENT_ID` | For `login --oauth` | OAuth application client ID |
| `PCLOUD_CLIENT_SECRET` | For `login --oauth` | OAuth application client secret |
| `PCLOUD_AUTH` | Optional | Session token β€” bypasses the credential store |
| `PCLOUD_ACCESS_TOKEN` | Optional | OAuth token β€” bypasses the credential store |
| `PCLOUD_REGION` | Optional | `eu` (default) or `us` β€” selects the API endpoint |

When `PCLOUD_ACCESS_TOKEN` is set, no stored credentials are read or written.
Neither client variable is needed for the default `pcloud login`. When either
token variable is set, no stored credentials are read or written; `PCLOUD_AUTH`
takes precedence, since a session token reaches more of the API.
48 changes: 38 additions & 10 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ const runSessionLogin = async (): Promise<void> => {
)
console.log("and downloads. Your password is sent to pCloud over HTTPS and")
console.log("is never written to disk β€” only the returned token is stored.\n")
console.log("Prefer not to type it here? `pcloud login --oauth` uses the")
console.log("browser instead, if you have an OAuth application registered.\n")

const username = await ask("Email: ")
const password = await askHidden("Password: ")
Expand Down Expand Up @@ -219,11 +221,24 @@ program
.command("login")
.description("Set up authentication with pCloud")
.option(
"--session",
"Log in with email and password for a session token (unlocks revisions, trash, zip, downloads)",
"--oauth",
"Log in through the browser instead (requires PCLOUD_CLIENT_ID and PCLOUD_CLIENT_SECRET)",
)
.option("--session", "Accepted and ignored β€” session login is the default")
.action(async (options) => {
if (options.session) {
if (options.oauth && options.session) {
console.error(
"Error: --oauth and --session ask for different flows. Pick one.\n",
)
process.exit(1)
}

// Session is the default because it is both the cheaper and the more capable
// tier: it needs no registered OAuth application, and it is the only one that
// reaches revisions, trash, zip and downloads. OAuth's advantage β€” never
// handling the password β€” is real but narrow, so it earns a flag rather than
// the default.
if (!options.oauth) {
try {
await runSessionLogin()
return
Expand All @@ -237,22 +252,35 @@ program
}

try {
console.log("\nπŸ” Welcome to pCloud CLI Setup!\n")
console.log("You will be redirected to pCloud in your browser to log in.")
console.log(
"After logging in, you'll be redirected back automatically.\n",
)

// Checked before the welcome banner rather than after it. Announcing "you
// will be redirected to your browser" and only then discovering there is
// nothing to redirect with leaves the user reading an instruction the next
// line contradicts.
const clientId = process.env.PCLOUD_CLIENT_ID
const clientSecret = process.env.PCLOUD_CLIENT_SECRET

if (!clientId || !clientSecret) {
console.error(
"\n❌ Missing credentials. Export PCLOUD_CLIENT_ID and PCLOUD_CLIENT_SECRET in your shell or .env file.\n",
"\n❌ OAuth needs PCLOUD_CLIENT_ID and PCLOUD_CLIENT_SECRET.\n",
)
console.error("Register an application at")
console.error(" https://docs.pcloud.com/methods/oauth_2.0/")
console.error("then export both in your shell or a .env file.\n")
console.error(
"Or just run `pcloud login` β€” it needs no setup at all, and",
)
console.error(
"reaches revisions, trash, zip and downloads that OAuth cannot.\n",
)
process.exit(1)
}

console.log("\nπŸ” pCloud OAuth login\n")
console.log("You will be redirected to pCloud in your browser to log in.")
console.log(
"After logging in, you'll be redirected back automatically.\n",
)

const oauth = new OAuthFlow(clientId, clientSecret, authBaseUrl)
const tokens = await oauth.authenticate()

Expand Down
66 changes: 66 additions & 0 deletions src/login.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, expect, it } from "vitest"
import { spawnSync } from "node:child_process"
import { fileURLToPath } from "node:url"

const CLI = fileURLToPath(new URL("./cli.ts", import.meta.url))
const TSX = fileURLToPath(new URL("../node_modules/.bin/tsx", import.meta.url))

// The old failure printed "You will be redirected to pCloud in your browser"
// and only then discovered it had no credentials to redirect with. Asserting the
// promise is absent is the regression test; asserting the error is present is not,
// since the error was always there β€” just below a line that contradicted it.
const PROMISED_A_BROWSER = /redirected/i

// --oauth reads only environment variables, so unlike the browse tests this needs
// no HOME isolation: there is no credential store on the path being exercised.
const runLogin = (args: string[]) => {
const env = { ...process.env } as NodeJS.ProcessEnv
delete env.PCLOUD_CLIENT_ID
delete env.PCLOUD_CLIENT_SECRET

return spawnSync(TSX, [CLI, "login", ...args], {
env,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
timeout: 60_000,
})
}

describe("login --oauth without an OAuth application", () => {
it("exits non-zero", () => {
expect(runLogin(["--oauth"]).status).toBe(1)
})

it("names both variables it needs", () => {
const { stderr } = runLogin(["--oauth"])
expect(stderr).toContain("PCLOUD_CLIENT_ID")
expect(stderr).toContain("PCLOUD_CLIENT_SECRET")
})

it("offers the setup-free alternative", () => {
expect(runLogin(["--oauth"]).stderr).toContain("pcloud login")
})

it("does not promise a browser it cannot open", () => {
const { stdout, stderr } = runLogin(["--oauth"])
expect(stdout + stderr).not.toMatch(PROMISED_A_BROWSER)
})
})

describe("login flag conflicts", () => {
it("refuses --oauth and --session together rather than silently picking one", () => {
const { status, stderr } = runLogin(["--oauth", "--session"])
expect(status).toBe(1)
expect(stderr).toMatch(/Pick one/)
})
})

describe("login default", () => {
// Killed by the timeout at the password prompt β€” reaching the prompt at all is
// the assertion, since it can only be reached through the session branch.
it("goes to session login, not OAuth", () => {
const { stdout } = runLogin([])
expect(stdout).toContain("pCloud session login")
expect(stdout).not.toMatch(PROMISED_A_BROWSER)
})
})