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 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
- ### Browser prerequisite
48
+ ### Optional runtime bootstrap
48
49
 
49
- If you plan to sign in with `login`, install Playwright's Chromium build once:
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
- If you already have a compatible signed-in DoorDash managed-browser session available, `auth-check` may quietly reuse it instead of asking you to sign in again. Routine direct commands stay quiet; use `login` when you want explicit browser interaction.
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 matching Playwright Chromium build used by this package.",
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
- " - Direct GraphQL/HTTP is the default path for auth-check, set-address, search, menu, item, orders, order, cart, add-to-cart, and update-cart.",
46
- " - login launches Chromium for a one-time manual sign-in flow and saves reusable state for later direct API calls.",
47
- " - auth-check can quietly reuse an already-signed-in compatible managed-browser DoorDash session when one is available; use login for explicit browser interaction.",
48
- " - set-address now uses DoorDash autocomplete/get-or-create plus addConsumerAddressV2 for brand-new address enrollment when needed.",
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
- ' - standalone recommended add-ons that open a nested cursor step are supported via children, e.g. [{"groupId":"recommended_option_546935995","optionId":"546936011","children":[{"groupId":"780057412","optionId":"4702669757"}]}].',
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 = 0;
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);
@@ -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 isDoorDashUrl(value: string): boolean;
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;