doordash-cli 0.3.3 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +14 -3
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +17 -8
- package/dist/cli.test.js +43 -3
- package/dist/direct-api.d.ts +55 -4
- package/dist/direct-api.js +655 -75
- package/dist/direct-api.test.js +529 -14
- package/dist/session-storage.d.ts +1 -0
- package/dist/session-storage.js +4 -0
- package/dist/session-storage.test.js +2 -1
- package/docs/examples.md +3 -3
- package/docs/install.md +15 -5
- package/man/dd-cli.1 +34 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,18 @@ All notable changes to `doordash-cli` will be documented in this file.
|
|
|
9
9
|
|
|
10
10
|
See [docs/releasing.md](docs/releasing.md) for the maintainer release flow.
|
|
11
11
|
|
|
12
|
+
## [0.4.1](https://github.com/LatencyTDH/doordash-cli/compare/v0.4.0...v0.4.1) (2026-04-10)
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* bound auth-check and restore login completion flow ([#39](https://github.com/LatencyTDH/doordash-cli/issues/39)) ([5166944](https://github.com/LatencyTDH/doordash-cli/commit/51669444dc124e39ece719624c997ab9f46acd93))
|
|
17
|
+
|
|
18
|
+
## [0.4.0](https://github.com/LatencyTDH/doordash-cli/compare/v0.3.3...v0.4.0) (2026-04-10)
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* reuse attached browser sessions for login bootstrap ([#38](https://github.com/LatencyTDH/doordash-cli/issues/38)) ([dc9410d](https://github.com/LatencyTDH/doordash-cli/commit/dc9410ddce1e72b2dce6b772787c8fb0dfa9683a))
|
|
23
|
+
|
|
12
24
|
## [0.3.3](https://github.com/LatencyTDH/doordash-cli/compare/v0.3.2...v0.3.3) (2026-04-09)
|
|
13
25
|
|
|
14
26
|
## [0.3.2](https://github.com/LatencyTDH/doordash-cli/compare/v0.3.1...v0.3.2) (2026-04-09)
|
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ It stops before checkout.
|
|
|
9
9
|
## Highlights
|
|
10
10
|
|
|
11
11
|
- **Cart-safe by design** — browse, inspect existing orders, and manage a cart; no checkout, payment, or order mutation.
|
|
12
|
+
- **Browser-first login** — `dd-cli login` reuses saved local auth or an attachable signed-in browser session when possible, and otherwise opens a temporary login window.
|
|
12
13
|
- **Direct API first** — auth, discovery, existing-order, and cart commands use DoorDash consumer-web GraphQL/HTTP rather than DOM clicking.
|
|
13
14
|
- **JSON-friendly** — every command prints structured output.
|
|
14
15
|
- **Fail-closed** — unsupported commands, flags, or unsafe payload shapes are rejected.
|
|
@@ -44,9 +45,9 @@ If you prefer to run from a checkout without linking:
|
|
|
44
45
|
npm run cli -- --help
|
|
45
46
|
```
|
|
46
47
|
|
|
47
|
-
###
|
|
48
|
+
### Optional runtime bootstrap
|
|
48
49
|
|
|
49
|
-
If
|
|
50
|
+
If your environment does not already have Playwright's bundled Chromium runtime installed, install it once:
|
|
50
51
|
|
|
51
52
|
```bash
|
|
52
53
|
doordash-cli install-browser
|
|
@@ -54,6 +55,8 @@ doordash-cli install-browser
|
|
|
54
55
|
npm run install:browser
|
|
55
56
|
```
|
|
56
57
|
|
|
58
|
+
That runtime is used when the CLI needs a local browser, including the temporary login window fallback.
|
|
59
|
+
|
|
57
60
|
## First run
|
|
58
61
|
|
|
59
62
|
```bash
|
|
@@ -65,7 +68,15 @@ doordash-cli search --query sushi
|
|
|
65
68
|
|
|
66
69
|
If you are running from a checkout without `npm link`, replace `doordash-cli` with `npm run cli --`.
|
|
67
70
|
|
|
68
|
-
|
|
71
|
+
## Login and session reuse
|
|
72
|
+
|
|
73
|
+
`login` reuses saved local auth when it is still valid. Otherwise it tries to import a discoverable attachable signed-in browser session. A merely-open Chrome/Brave window is not automatically reusable unless the CLI can actually attach to it. If no attachable session is available, it opens a temporary Chromium login window and saves the session there. If authentication still is not established, `login` exits non-zero.
|
|
74
|
+
|
|
75
|
+
`auth-check` reports whether the saved state appears logged in and can quietly import a discoverable attachable signed-in browser session unless `logout` disabled that auto-reuse.
|
|
76
|
+
|
|
77
|
+
`logout` clears persisted cookies and stored browser state, then keeps passive browser-session reuse disabled until your next explicit `dd-cli login` attempt.
|
|
78
|
+
|
|
79
|
+
If `login` opens a temporary Chromium window, the CLI now keeps checking automatically and also tells you that you can press Enter to force an immediate recheck once the page already shows you are signed in. That restores the old effective manual-completion path without giving up automatic completion when it works. If you expect reuse from another browser instead, make sure it exposes an attachable browser automation session the CLI can actually import; a merely-open browser window is not enough today, even if it is already your main browser.
|
|
69
80
|
|
|
70
81
|
## Command surface
|
|
71
82
|
|
package/dist/cli.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export declare function parseArgv(argv: string[]): {
|
|
|
6
6
|
command?: string;
|
|
7
7
|
flags: Record<string, string>;
|
|
8
8
|
};
|
|
9
|
+
export declare function commandExitCode(command: string, result: unknown): number;
|
|
9
10
|
export declare function main(argv?: string[]): Promise<void>;
|
|
10
11
|
export declare function runCli(argv?: string[]): Promise<void>;
|
|
11
12
|
export declare function isDirectExecution(argv1?: string | undefined, metaUrl?: string): boolean;
|
package/dist/cli.js
CHANGED
|
@@ -40,15 +40,15 @@ export function usage() {
|
|
|
40
40
|
" - Run with no arguments to show this help.",
|
|
41
41
|
" - Common Unicode long dashes are normalized for flags, so —help / –help work too.",
|
|
42
42
|
" - Installed command names are lowercase only: dd-cli and doordash-cli.",
|
|
43
|
-
" - install-browser downloads the
|
|
43
|
+
" - install-browser downloads the bundled Playwright Chromium runtime used when the CLI needs a local browser.",
|
|
44
44
|
" - Manual pages ship with the project: man dd-cli or man doordash-cli.",
|
|
45
|
-
" -
|
|
46
|
-
" - login
|
|
47
|
-
" -
|
|
48
|
-
" -
|
|
45
|
+
" - login reuses saved local auth when possible, otherwise imports an attachable signed-in browser session or opens a temporary Chromium login window.",
|
|
46
|
+
" - login auto-detects completion when it can; in the temporary-browser fallback you can also press Enter to force an immediate recheck once the page shows you are signed in.",
|
|
47
|
+
" - login exits non-zero if authentication is still not established.",
|
|
48
|
+
" - auth-check reports saved-session status and can quietly reuse/import an attachable signed-in browser session unless logout disabled that auto-reuse.",
|
|
49
|
+
" - logout clears saved session files and keeps passive browser-session reuse off until the next explicit login attempt.",
|
|
49
50
|
" - configurable items require explicit --options-json selections.",
|
|
50
|
-
|
|
51
|
-
" - other non-recommended nested cursor trees still fail closed until DoorDash exposes a directly provable transport.",
|
|
51
|
+
" - unsupported option trees fail closed.",
|
|
52
52
|
"",
|
|
53
53
|
"Out-of-scope commands remain intentionally unsupported:",
|
|
54
54
|
" checkout, place-order, payment actions, order mutation/cancellation",
|
|
@@ -125,6 +125,15 @@ export function parseArgv(argv) {
|
|
|
125
125
|
}
|
|
126
126
|
return { command, flags };
|
|
127
127
|
}
|
|
128
|
+
export function commandExitCode(command, result) {
|
|
129
|
+
if (command === "login" && typeof result === "object" && result !== null) {
|
|
130
|
+
const authResult = result;
|
|
131
|
+
if (authResult.success === false || authResult.isLoggedIn === false) {
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
128
137
|
export async function main(argv = process.argv.slice(2)) {
|
|
129
138
|
const { command, flags } = parseArgv(argv);
|
|
130
139
|
if (flags.version === "true") {
|
|
@@ -141,7 +150,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
141
150
|
try {
|
|
142
151
|
const result = await lib.runCommand(safeCommand, flags);
|
|
143
152
|
console.log(JSON.stringify(result, null, 2));
|
|
144
|
-
process.exitCode =
|
|
153
|
+
process.exitCode = commandExitCode(safeCommand, result);
|
|
145
154
|
}
|
|
146
155
|
finally {
|
|
147
156
|
await lib.shutdown();
|
package/dist/cli.test.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { chmodSync, readFileSync } from "node:fs";
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { mkdtemp, rm, symlink } from "node:fs/promises";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
9
9
|
import { SAFE_COMMANDS, assertAllowedFlags, assertSafeCommand } from "./lib.js";
|
|
10
|
-
import { parseArgv, version } from "./cli.js";
|
|
10
|
+
import { commandExitCode, parseArgv, version } from "./cli.js";
|
|
11
11
|
const distDir = dirname(fileURLToPath(import.meta.url));
|
|
12
12
|
const binPath = join(distDir, "bin.js");
|
|
13
|
-
function runCli(args) {
|
|
13
|
+
function runCli(args, env) {
|
|
14
14
|
return spawnSync(process.execPath, [binPath, ...args], {
|
|
15
15
|
encoding: "utf8",
|
|
16
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
16
17
|
});
|
|
17
18
|
}
|
|
18
19
|
async function runLinkedCli(linkName, args) {
|
|
@@ -112,9 +113,15 @@ test("help output shows the direct read-only/cart-safe command surface", () => {
|
|
|
112
113
|
assert.match(result.stdout, /options-json/);
|
|
113
114
|
assert.match(result.stdout, /--version, -v/);
|
|
114
115
|
assert.match(result.stdout, /man dd-cli/);
|
|
116
|
+
assert.match(result.stdout, /login reuses saved local auth when possible, otherwise imports an attachable signed-in browser session or opens a temporary Chromium login window\./);
|
|
117
|
+
assert.match(result.stdout, /login auto-detects completion when it can; in the temporary-browser fallback you can also press Enter to force an immediate recheck once the page shows you are signed in\./);
|
|
118
|
+
assert.match(result.stdout, /login exits non-zero if authentication is still not established\./);
|
|
119
|
+
assert.match(result.stdout, /auth-check reports saved-session status and can quietly reuse\/import an attachable signed-in browser session unless logout disabled that auto-reuse\./);
|
|
120
|
+
assert.match(result.stdout, /logout clears saved session files and keeps passive browser-session reuse off until the next explicit login attempt\./);
|
|
115
121
|
assert.match(result.stdout, /Out-of-scope commands remain intentionally unsupported/);
|
|
116
122
|
assert.doesNotMatch(result.stdout, /auth-bootstrap/);
|
|
117
123
|
assert.doesNotMatch(result.stdout, /auth-clear/);
|
|
124
|
+
assert.match(result.stdout, /temporary Chromium login window/i);
|
|
118
125
|
assert.doesNotMatch(result.stdout, /Dd-cli/);
|
|
119
126
|
});
|
|
120
127
|
test("repository ships man pages for the supported lowercase command names", () => {
|
|
@@ -124,6 +131,9 @@ test("repository ships man pages for the supported lowercase command names", ()
|
|
|
124
131
|
assert.match(readFileSync(ddManPath, "utf8"), /\.B login/);
|
|
125
132
|
assert.doesNotMatch(readFileSync(ddManPath, "utf8"), /auth-bootstrap/);
|
|
126
133
|
assert.doesNotMatch(readFileSync(ddManPath, "utf8"), /auth-clear/);
|
|
134
|
+
assert.match(readFileSync(ddManPath, "utf8"), /passive\s+browser-session reuse stays disabled until the next explicit/i);
|
|
135
|
+
assert.match(readFileSync(ddManPath, "utf8"), /merely-open Chrome\/Brave window is not\s+automatically reusable/i);
|
|
136
|
+
assert.match(readFileSync(ddManPath, "utf8"), /temporary Chromium.*window/i);
|
|
127
137
|
assert.doesNotMatch(readFileSync(ddManPath, "utf8"), /Dd-cli/);
|
|
128
138
|
assert.equal(readFileSync(aliasManPath, "utf8").trim(), ".so man1/dd-cli.1");
|
|
129
139
|
});
|
|
@@ -159,6 +169,36 @@ test("legacy auth command invocations point users to login/logout", () => {
|
|
|
159
169
|
assert.match(logoutRename.stderr, /Unsupported command: auth-clear/);
|
|
160
170
|
assert.match(logoutRename.stderr, /renamed it to logout/);
|
|
161
171
|
});
|
|
172
|
+
test("commandExitCode treats failed login results as non-zero without changing read-only auth-check semantics", () => {
|
|
173
|
+
assert.equal(commandExitCode("login", { success: false, isLoggedIn: false }), 1);
|
|
174
|
+
assert.equal(commandExitCode("login", { success: true, isLoggedIn: true }), 0);
|
|
175
|
+
assert.equal(commandExitCode("auth-check", { success: true, isLoggedIn: false }), 0);
|
|
176
|
+
});
|
|
177
|
+
test("logout clears persisted session artifacts in the active home directory", async () => {
|
|
178
|
+
const tempHome = await mkdtemp(join(tmpdir(), "doordash-cli-home-"));
|
|
179
|
+
const sessionDir = join(tempHome, ".config", "striderlabs-mcp-doordash");
|
|
180
|
+
const cookiesPath = join(sessionDir, "cookies.json");
|
|
181
|
+
const storageStatePath = join(sessionDir, "storage-state.json");
|
|
182
|
+
const browserImportBlockPath = join(sessionDir, "browser-import-blocked");
|
|
183
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
184
|
+
writeFileSync(cookiesPath, JSON.stringify([{ name: "session", domain: ".doordash.com" }]));
|
|
185
|
+
writeFileSync(storageStatePath, JSON.stringify({ cookies: [], origins: [] }));
|
|
186
|
+
try {
|
|
187
|
+
const result = runCli(["logout"], { HOME: tempHome });
|
|
188
|
+
assert.equal(result.status, 0);
|
|
189
|
+
assert.equal(existsSync(cookiesPath), false);
|
|
190
|
+
assert.equal(existsSync(storageStatePath), false);
|
|
191
|
+
assert.equal(existsSync(browserImportBlockPath), true);
|
|
192
|
+
const parsed = JSON.parse(result.stdout);
|
|
193
|
+
assert.equal(parsed.success, true);
|
|
194
|
+
assert.equal(parsed.cookiesPath, cookiesPath);
|
|
195
|
+
assert.equal(parsed.storageStatePath, storageStatePath);
|
|
196
|
+
assert.match(parsed.message, /disabled until the next `dd-cli login`/);
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
await rm(tempHome, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
162
202
|
test("blocked commands fail immediately", () => {
|
|
163
203
|
const result = runCli(["checkout"]);
|
|
164
204
|
assert.equal(result.status, 1);
|
package/dist/direct-api.d.ts
CHANGED
|
@@ -15,8 +15,24 @@ export type AuthResult = {
|
|
|
15
15
|
cookiesPath: string;
|
|
16
16
|
storageStatePath: string;
|
|
17
17
|
};
|
|
18
|
-
export type AuthBootstrapResult = AuthResult & {
|
|
18
|
+
export type AuthBootstrapResult = (AuthResult & {
|
|
19
|
+
success: true;
|
|
20
|
+
isLoggedIn: true;
|
|
21
|
+
message: string;
|
|
22
|
+
}) | (Omit<AuthResult, "success" | "isLoggedIn"> & {
|
|
23
|
+
success: false;
|
|
24
|
+
isLoggedIn: false;
|
|
19
25
|
message: string;
|
|
26
|
+
});
|
|
27
|
+
type ManagedBrowserLoginResult = {
|
|
28
|
+
status: "completed";
|
|
29
|
+
completion: "automatic" | "manual";
|
|
30
|
+
auth: AuthResult;
|
|
31
|
+
} | {
|
|
32
|
+
status: "timed-out";
|
|
33
|
+
auth: AuthResult;
|
|
34
|
+
} | {
|
|
35
|
+
status: "launch-failed";
|
|
20
36
|
};
|
|
21
37
|
export type SearchRestaurantResult = {
|
|
22
38
|
id: string;
|
|
@@ -456,7 +472,35 @@ type GeoAddressResponse = {
|
|
|
456
472
|
lng?: number | null;
|
|
457
473
|
} | null;
|
|
458
474
|
};
|
|
475
|
+
type BootstrapAuthSessionDeps = {
|
|
476
|
+
clearBlockedBrowserImport: () => Promise<void>;
|
|
477
|
+
checkPersistedAuth: () => Promise<AuthResult | null>;
|
|
478
|
+
importBrowserSessionIfAvailable: () => Promise<boolean>;
|
|
479
|
+
markBrowserImportAttempted: () => void;
|
|
480
|
+
getAttachedBrowserCdpCandidates: () => Promise<string[]>;
|
|
481
|
+
getReachableCdpCandidates: (candidates: string[]) => Promise<string[]>;
|
|
482
|
+
describeDesktopBrowserReuseGap: () => Promise<string | null>;
|
|
483
|
+
openUrlInAttachedBrowser: (input: {
|
|
484
|
+
cdpUrl: string;
|
|
485
|
+
targetUrl: string;
|
|
486
|
+
}) => Promise<boolean>;
|
|
487
|
+
openUrlInDefaultBrowser: (targetUrl: string) => Promise<boolean>;
|
|
488
|
+
waitForAttachedBrowserSessionImport: (input: {
|
|
489
|
+
timeoutMs: number;
|
|
490
|
+
pollIntervalMs: number;
|
|
491
|
+
}) => Promise<boolean>;
|
|
492
|
+
waitForManagedBrowserLogin: (input: {
|
|
493
|
+
targetUrl: string;
|
|
494
|
+
timeoutMs: number;
|
|
495
|
+
pollIntervalMs: number;
|
|
496
|
+
log: (message: string) => void;
|
|
497
|
+
}) => Promise<ManagedBrowserLoginResult>;
|
|
498
|
+
canPromptForManagedBrowserConfirmation: () => boolean;
|
|
499
|
+
checkAuthDirect: () => Promise<AuthResult>;
|
|
500
|
+
log: (message: string) => void;
|
|
501
|
+
};
|
|
459
502
|
export declare function checkAuthDirect(): Promise<AuthResult>;
|
|
503
|
+
export declare function bootstrapAuthSessionWithDeps(deps: BootstrapAuthSessionDeps): Promise<AuthBootstrapResult>;
|
|
460
504
|
export declare function bootstrapAuthSession(): Promise<AuthBootstrapResult>;
|
|
461
505
|
export declare function clearStoredSession(): Promise<{
|
|
462
506
|
success: true;
|
|
@@ -522,12 +566,19 @@ export declare function resolveAvailableAddressMatch(input: {
|
|
|
522
566
|
printableAddress: string | null;
|
|
523
567
|
source: SetAddressResult["matchedAddressSource"];
|
|
524
568
|
} | null;
|
|
525
|
-
export declare function
|
|
526
|
-
export declare function hasDoorDashCookies(cookies: ReadonlyArray<Pick<Cookie, "domain">>): boolean;
|
|
527
|
-
export declare function selectManagedBrowserImportMode(input: {
|
|
569
|
+
export declare function selectAttachedBrowserImportMode(input: {
|
|
528
570
|
pageUrls: readonly string[];
|
|
529
571
|
cookies: ReadonlyArray<Pick<Cookie, "domain">>;
|
|
530
572
|
}): "page" | "cookies" | "skip";
|
|
573
|
+
export declare function resolveAttachedBrowserCdpCandidates(env: NodeJS.ProcessEnv, configCandidates?: string[]): string[];
|
|
574
|
+
export declare function resolveSystemBrowserOpenCommand(targetUrl: string, targetPlatform?: NodeJS.Platform): {
|
|
575
|
+
command: string;
|
|
576
|
+
args: string[];
|
|
577
|
+
} | null;
|
|
578
|
+
export declare function summarizeDesktopBrowserReuseGap(input: {
|
|
579
|
+
processCommands: readonly string[];
|
|
580
|
+
hasAnyDevToolsActivePort: boolean;
|
|
581
|
+
}): string | null;
|
|
531
582
|
export declare function parseSearchRestaurants(body: unknown[]): SearchRestaurantResult[];
|
|
532
583
|
export declare function parseSearchRestaurantRow(entry: unknown): SearchRestaurantResult | null;
|
|
533
584
|
export declare function parseExistingOrderLifecycleStatus(orderRoot: unknown): ExistingOrderLifecycleStatus;
|