@marcfargas/go-easy 0.2.0 → 0.3.0

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.
Files changed (90) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +48 -44
  3. package/dist/auth-flow.d.ts +50 -0
  4. package/dist/auth-flow.d.ts.map +1 -0
  5. package/dist/auth-flow.js +219 -0
  6. package/dist/auth-flow.js.map +1 -0
  7. package/dist/auth-server.d.ts +18 -0
  8. package/dist/auth-server.d.ts.map +1 -0
  9. package/dist/auth-server.js +327 -0
  10. package/dist/auth-server.js.map +1 -0
  11. package/dist/auth-store.d.ts +81 -0
  12. package/dist/auth-store.d.ts.map +1 -0
  13. package/dist/auth-store.js +185 -0
  14. package/dist/auth-store.js.map +1 -0
  15. package/dist/auth.d.ts +21 -15
  16. package/dist/auth.d.ts.map +1 -1
  17. package/dist/auth.js +90 -50
  18. package/dist/auth.js.map +1 -1
  19. package/dist/bin/calendar.js +69 -7
  20. package/dist/bin/calendar.js.map +1 -1
  21. package/dist/bin/drive.js +2 -0
  22. package/dist/bin/drive.js.map +1 -1
  23. package/dist/bin/easy.d.ts +11 -0
  24. package/dist/bin/easy.d.ts.map +1 -0
  25. package/dist/bin/easy.js +140 -0
  26. package/dist/bin/easy.js.map +1 -0
  27. package/dist/bin/gmail.d.ts +6 -1
  28. package/dist/bin/gmail.d.ts.map +1 -1
  29. package/dist/bin/gmail.js +85 -13
  30. package/dist/bin/gmail.js.map +1 -1
  31. package/dist/bin/tasks.d.ts +17 -0
  32. package/dist/bin/tasks.d.ts.map +1 -0
  33. package/dist/bin/tasks.js +190 -0
  34. package/dist/bin/tasks.js.map +1 -0
  35. package/dist/calendar/helpers.d.ts +13 -1
  36. package/dist/calendar/helpers.d.ts.map +1 -1
  37. package/dist/calendar/helpers.js +112 -3
  38. package/dist/calendar/helpers.js.map +1 -1
  39. package/dist/calendar/index.d.ts +2 -2
  40. package/dist/calendar/index.d.ts.map +1 -1
  41. package/dist/calendar/index.js +9 -1
  42. package/dist/calendar/index.js.map +1 -1
  43. package/dist/calendar/types.d.ts +84 -0
  44. package/dist/calendar/types.d.ts.map +1 -1
  45. package/dist/calendar/types.js +7 -0
  46. package/dist/calendar/types.js.map +1 -1
  47. package/dist/errors.d.ts +24 -0
  48. package/dist/errors.d.ts.map +1 -1
  49. package/dist/errors.js +22 -2
  50. package/dist/errors.js.map +1 -1
  51. package/dist/gmail/helpers.d.ts +8 -1
  52. package/dist/gmail/helpers.d.ts.map +1 -1
  53. package/dist/gmail/helpers.js +31 -5
  54. package/dist/gmail/helpers.js.map +1 -1
  55. package/dist/gmail/index.d.ts +14 -5
  56. package/dist/gmail/index.d.ts.map +1 -1
  57. package/dist/gmail/index.js +90 -20
  58. package/dist/gmail/index.js.map +1 -1
  59. package/dist/gmail/markdown.d.ts +22 -0
  60. package/dist/gmail/markdown.d.ts.map +1 -0
  61. package/dist/gmail/markdown.js +30 -0
  62. package/dist/gmail/markdown.js.map +1 -0
  63. package/dist/gmail/types.d.ts +19 -3
  64. package/dist/gmail/types.d.ts.map +1 -1
  65. package/dist/index.d.ts +7 -2
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +7 -2
  68. package/dist/index.js.map +1 -1
  69. package/dist/scopes.d.ts +16 -0
  70. package/dist/scopes.d.ts.map +1 -0
  71. package/dist/scopes.js +28 -0
  72. package/dist/scopes.js.map +1 -0
  73. package/dist/tasks/helpers.d.ts +10 -0
  74. package/dist/tasks/helpers.d.ts.map +1 -0
  75. package/dist/tasks/helpers.js +33 -0
  76. package/dist/tasks/helpers.js.map +1 -0
  77. package/dist/tasks/index.d.ts +63 -0
  78. package/dist/tasks/index.d.ts.map +1 -0
  79. package/dist/tasks/index.js +253 -0
  80. package/dist/tasks/index.js.map +1 -0
  81. package/dist/tasks/types.d.ts +79 -0
  82. package/dist/tasks/types.d.ts.map +1 -0
  83. package/dist/tasks/types.js +5 -0
  84. package/dist/tasks/types.js.map +1 -0
  85. package/package.json +33 -5
  86. package/skills/go-easy/SKILL.md +146 -0
  87. package/skills/go-easy/calendar.md +366 -0
  88. package/skills/go-easy/drive.md +309 -0
  89. package/skills/go-easy/gmail.md +478 -0
  90. package/skills/go-easy/tasks.md +260 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,81 @@
1
1
  # @marcfargas/go-easy
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`8cf8524`](https://github.com/marcfargas/go-easy/commit/8cf85243659e1151b896a69f5405534062a82899) Thanks [@marcfargas](https://github.com/marcfargas)! - Beta release — own auth system, Google Tasks, reply CLI, file-based body flags, Markdown emails.
8
+
9
+ ### Auth (BREAKING)
10
+
11
+ go-easy now owns its own OAuth2 tokens at `~/.go-easy/` instead of reading from legacy CLI stores (`~/.gmcli`, `~/.gdcli`, `~/.gccli`).
12
+
13
+ - `npx go-easy auth add <email>` — agent-compatible two-phase OAuth flow (start → poll)
14
+ - `npx go-easy auth list` — list configured accounts and scopes
15
+ - `npx go-easy auth remove <email> --confirm` — remove an account
16
+ - One combined token per account covers Gmail + Drive + Calendar + Tasks
17
+ - Specific error codes with `fix` field: `AUTH_NO_ACCOUNT`, `AUTH_MISSING_SCOPE`, `AUTH_TOKEN_REVOKED`, `AUTH_NO_CREDENTIALS`
18
+
19
+ ### Google Tasks (NEW)
20
+
21
+ New service module and CLI for Google Tasks API:
22
+
23
+ - `npx go-tasks <account> lists` — list task lists
24
+ - `npx go-tasks <account> tasks <listId>` — list tasks with pagination
25
+ - `npx go-tasks <account> get/add/update/complete/move/delete` — full CRUD
26
+ - `npx go-tasks <account> create-list/delete-list/clear` — list management
27
+ - Subtask support via `--parent` flag
28
+ - Library: `@marcfargas/go-easy/tasks` export
29
+ - Requires re-auth for existing accounts (`npx go-easy auth add <email>`)
30
+
31
+ ### Gmail CLI
32
+
33
+ - Add `reply` command — reply and reply-all with `--reply-all` flag (DESTRUCTIVE, requires `--confirm`)
34
+ - Add `--in-reply-to` flag for `draft` command (thread association)
35
+ - Add `--cc` and `--bcc` flags for `draft` and `send` commands
36
+ - Add `--page-token` for `search` and `drafts` pagination
37
+
38
+ ### Body Flags (BREAKING)
39
+
40
+ Replace inline `--body`, `--html`, `--md` flags with file-based alternatives:
41
+
42
+ - `--body-text-file=<path>` — read plain text body from UTF-8 file
43
+ - `--body-html-file=<path>` — read HTML body from UTF-8 file
44
+ - `--body-md-file=<path>` — read Markdown body from file (auto-converted to HTML)
45
+
46
+ This eliminates shell escaping, encoding, and multiline issues for agent use.
47
+
48
+ ### Markdown Email Support
49
+
50
+ - New `markdown` option on `send`, `reply`, `forward`, and `createDraft`
51
+ - Auto-converts Markdown to email-safe HTML with inline styles
52
+ - GFM support: tables, strikethrough, code blocks, links, lists
53
+ - `markdownToHtml()` helper exported from `@marcfargas/go-easy/gmail`
54
+
55
+ ### Forward Improvements
56
+
57
+ - Forward creates a draft by default (WRITE, no safety gate)
58
+ - `--send-now --confirm` to send immediately (DESTRUCTIVE)
59
+ - Attachment filtering: `--include=name` and `--exclude=name` (substring match)
60
+ - `--no-thread` to break out of the original thread
61
+ - Body content appears above the forwarded message
62
+
63
+ ### Calendar
64
+
65
+ - Support all event types: working location, out-of-office, focus time, birthday
66
+ - `--page-token` for events pagination
67
+ - Fix `update` command: use PATCH instead of PUT to prevent data loss on partial updates
68
+
69
+ ### Drive
70
+
71
+ - `--page-token` for `ls` and `search` pagination
72
+
73
+ ### Fixes
74
+
75
+ - RFC 2047 encode Subject headers with non-ASCII characters
76
+ - Fix forward threading (keep in original thread by default)
77
+ - Fix auth HTML pages: add `<meta charset="utf-8">` for emoji rendering
78
+
3
79
  ## 0.2.0
4
80
 
5
81
  ### Minor Changes
package/README.md CHANGED
@@ -3,11 +3,11 @@
3
3
  > Google APIs made easy — Gmail, Drive & Calendar. For AI agents and humans.
4
4
 
5
5
  Thin TypeScript wrappers over Google's individual `@googleapis/*` packages with:
6
- - **Simple auth** — multi-account OAuth2 with token import from existing tools
6
+ - **Own auth** — unified OAuth2 with combined tokens, agent-compatible two-phase flow
7
7
  - **Agent-friendly types** — structured `GmailMessage`, `DriveFile`, `CalendarEvent`
8
8
  - **Safety guards** — destructive operations (send, share, delete) require explicit confirmation
9
9
  - **JSON gateways** — CLI tools that always output structured JSON
10
- - **Progressive skills** — designed for AI agent consumption (pi coding agent)
10
+ - **File-based body** — email bodies read from files, not CLI args (no shell escaping issues)
11
11
 
12
12
  ## Installation
13
13
 
@@ -25,41 +25,48 @@ Requires **Node.js ≥ 20**.
25
25
 
26
26
  ## Auth Setup
27
27
 
28
- go-easy uses OAuth2 tokens stored per-service. Each service reads from its own token store:
28
+ go-easy manages its own OAuth2 tokens in `~/.go-easy/`.
29
29
 
30
- | Service | Token store |
31
- |---|---|
32
- | Gmail | `~/.gmcli/accounts.json` |
33
- | Drive | `~/.gdcli/accounts.json` |
34
- | Calendar | `~/.gccli/accounts.json` |
30
+ ### Prerequisites
35
31
 
36
- Each `accounts.json` file contains an array of accounts:
32
+ 1. Create a project in [Google Cloud Console](https://console.cloud.google.com/)
33
+ 2. Enable the Gmail, Drive, and Calendar APIs
34
+ 3. Create OAuth2 credentials (Desktop application type)
35
+ 4. Save credentials to `~/.go-easy/credentials.json`:
37
36
 
38
37
  ```json
39
- [
40
- {
41
- "email": "you@example.com",
42
- "oauth2": {
43
- "clientId": "YOUR_CLIENT_ID.apps.googleusercontent.com",
44
- "clientSecret": "YOUR_CLIENT_SECRET",
45
- "refreshToken": "YOUR_REFRESH_TOKEN"
46
- }
47
- }
48
- ]
38
+ {
39
+ "clientId": "YOUR_CLIENT_ID.apps.googleusercontent.com",
40
+ "clientSecret": "YOUR_CLIENT_SECRET"
41
+ }
49
42
  ```
50
43
 
51
- To obtain credentials:
52
- 1. Create a project in [Google Cloud Console](https://console.cloud.google.com/)
53
- 2. Enable the Gmail, Drive, and/or Calendar APIs
54
- 3. Create OAuth2 credentials (Desktop application)
55
- 4. Use the OAuth2 playground or a local flow to obtain a refresh token
56
- 5. Place the token in the appropriate `accounts.json` file
44
+ ### Add an account
45
+
46
+ ```bash
47
+ npx go-easy auth add you@example.com
48
+ # { "status": "started", "authUrl": "https://accounts.google.com/..." }
49
+ # Open the URL, authorize, then poll:
50
+ npx go-easy auth add you@example.com
51
+ # → { "status": "complete", "email": "you@example.com", "scopes": ["gmail", "drive", "calendar"] }
52
+ ```
53
+
54
+ One combined token covers Gmail + Drive + Calendar. The flow is agent-compatible — two separate CLI calls (start + poll), no streaming stdout needed.
55
+
56
+ ### Manage accounts
57
+
58
+ ```bash
59
+ npx go-easy auth list # List configured accounts
60
+ npx go-easy auth add you@example.com # Add or upgrade account
61
+ npx go-easy auth remove you@example.com --confirm # Remove account
62
+ ```
57
63
 
58
64
  ## Quick Start
59
65
 
60
66
  ```ts
61
67
  import { getAuth } from '@marcfargas/go-easy/auth';
62
68
  import { search, send } from '@marcfargas/go-easy/gmail';
69
+ import { setSafetyContext } from '@marcfargas/go-easy';
63
70
 
64
71
  const auth = await getAuth('gmail', 'you@example.com');
65
72
 
@@ -68,8 +75,6 @@ const results = await search(auth, { query: 'is:unread from:client' });
68
75
  console.log(results.items);
69
76
 
70
77
  // Send (DESTRUCTIVE — requires safety context)
71
- import { setSafetyContext } from '@marcfargas/go-easy';
72
-
73
78
  setSafetyContext({
74
79
  confirm: async (op) => {
75
80
  console.log(`⚠️ ${op.description}`);
@@ -80,7 +85,7 @@ setSafetyContext({
80
85
  await send(auth, {
81
86
  to: 'client@example.com',
82
87
  subject: 'Invoice attached',
83
- html: '<h1>Invoice</h1><p>Please find attached.</p>',
88
+ markdown: '# Invoice\n\nPlease find attached.',
84
89
  attachments: ['./invoice.pdf'],
85
90
  });
86
91
  ```
@@ -93,7 +98,8 @@ All gateway CLIs output JSON to stdout and work via `npx`:
93
98
  # Gmail
94
99
  npx go-gmail you@example.com search "is:unread" --max=10
95
100
  npx go-gmail you@example.com get <messageId>
96
- npx go-gmail you@example.com send --to=x@y.com --subject="Hi" --body="Hello" --confirm
101
+ npx go-gmail you@example.com reply <messageId> --body-text-file=reply.txt --confirm
102
+ npx go-gmail you@example.com send --to=x@y.com --subject="Hi" --body-text-file=body.txt --confirm
97
103
 
98
104
  # Drive
99
105
  npx go-drive you@example.com ls
@@ -106,6 +112,8 @@ npx go-calendar you@example.com create primary --summary="Meeting" --start=... -
106
112
  npx go-calendar you@example.com freebusy primary --from=... --to=...
107
113
  ```
108
114
 
115
+ Body content is always read from files (`--body-text-file`, `--body-html-file`, `--body-md-file`), never passed inline.
116
+
109
117
  Destructive operations require `--confirm`. Without it, they show what *would* happen and exit with code 2.
110
118
 
111
119
  ## Services
@@ -129,9 +137,10 @@ Destructive operations require `--confirm`. Without it, they show what *would* h
129
137
  | `createDraft` | WRITE | Create a draft (no send) |
130
138
  | `listDrafts` | READ | List existing drafts |
131
139
  | `batchModifyLabels` | WRITE | Add/remove labels on multiple messages |
132
- | `send` | ⚠️ DESTRUCTIVE | Send a new email |
133
- | `reply` | ⚠️ DESTRUCTIVE | Reply to a message (preserves thread) |
134
- | `forward` | ⚠️ DESTRUCTIVE | Forward a message with attachments |
140
+ | `markdownToHtml` | | Convert Markdown to email-safe HTML |
141
+ | `send` | ⚠️ DESTRUCTIVE | Send a new email (supports `markdown` option) |
142
+ | `reply` | ⚠️ DESTRUCTIVE | Reply / reply-all to a message |
143
+ | `forward` | WRITE / ⚠️ DESTRUCTIVE | Forward as draft (default) or send (`sendNow`). Attachment filtering. |
135
144
  | `sendDraft` | ⚠️ DESTRUCTIVE | Send an existing draft |
136
145
 
137
146
  ### Drive
@@ -161,8 +170,8 @@ Destructive operations require `--confirm`. Without it, they show what *would* h
161
170
  | `listEvents` | READ | List events with time range, search, pagination |
162
171
  | `getEvent` | READ | Get a single event by ID |
163
172
  | `queryFreeBusy` | READ | Check availability across calendars |
164
- | `createEvent` | WRITE | Create an event (with attendees, all-day, location) |
165
- | `updateEvent` | WRITE | Update an existing event |
173
+ | `createEvent` | WRITE | Create an event (with attendees, all-day, location, OOO, focus time) |
174
+ | `updateEvent` | WRITE | Update an existing event (full replace) |
166
175
  | `deleteEvent` | ⚠️ DESTRUCTIVE | Delete an event (warns about attendee cancellation) |
167
176
 
168
177
  ## Safety Model
@@ -183,24 +192,23 @@ Without one, all destructive operations are blocked by default.
183
192
  go-easy uses **subpath exports** — import only what you need:
184
193
 
185
194
  ```ts
186
- // Subpath imports (recommended)
187
195
  import { getAuth } from '@marcfargas/go-easy/auth';
188
196
  import { search, send } from '@marcfargas/go-easy/gmail';
189
- import { listFiles, upload } from '@marcfargas/go-easy/drive';
197
+ import { listFiles, uploadFile } from '@marcfargas/go-easy/drive';
190
198
  import { listEvents, createEvent } from '@marcfargas/go-easy/calendar';
191
- import { setSafetyContext } from '@marcfargas/go-easy'; // root: safety, errors, shared utils
199
+ import { setSafetyContext } from '@marcfargas/go-easy';
192
200
  ```
193
201
 
194
202
  | Import path | What's in it |
195
203
  |---|---|
196
204
  | `@marcfargas/go-easy` | Safety context, errors, plus `gmail`/`drive`/`calendar` as namespaces |
197
- | `@marcfargas/go-easy/auth` | `getAuth`, `listAccounts`, `clearAuthCache` |
205
+ | `@marcfargas/go-easy/auth` | `getAuth`, `listAccounts`, `listAllAccounts`, `clearAuthCache` |
206
+ | `@marcfargas/go-easy/auth-store` | `readAccountStore`, `writeAccountStore`, `findAccount`, etc. |
207
+ | `@marcfargas/go-easy/scopes` | `SCOPES`, `ALL_SCOPES`, `scopeToService` |
198
208
  | `@marcfargas/go-easy/gmail` | All Gmail operations |
199
209
  | `@marcfargas/go-easy/drive` | All Drive operations |
200
210
  | `@marcfargas/go-easy/calendar` | All Calendar operations |
201
211
 
202
- The root export also re-exports each service as a namespace, so `import { gmail } from '@marcfargas/go-easy'` works if you prefer a single import.
203
-
204
212
  ## Development
205
213
 
206
214
  ```bash
@@ -211,10 +219,6 @@ npm run lint # type-check without emitting
211
219
  npm run dev # watch mode
212
220
  ```
213
221
 
214
- ## Contributing
215
-
216
- Found a bug or have a feature request? [Open an issue](https://github.com/marcfargas/go-easy/issues).
217
-
218
222
  ## License
219
223
 
220
224
  MIT
@@ -0,0 +1,50 @@
1
+ /**
2
+ * auth-flow — Two-phase OAuth flow for agent-compatible auth.
3
+ *
4
+ * Phase 1 (start): Spawn background auth server, return URL.
5
+ * Phase 2 (poll): Check pending file for completion status.
6
+ *
7
+ * The agent calls `npx go-easy auth add <email>` which:
8
+ * - On first call: starts the server, returns { status: "started", authUrl }
9
+ * - On subsequent calls: polls and returns current status
10
+ * - When user completes auth: returns { status: "complete" }
11
+ */
12
+ export type AuthFlowStatus = {
13
+ status: 'started';
14
+ authUrl: string;
15
+ expiresIn: number;
16
+ } | {
17
+ status: 'waiting';
18
+ authUrl: string;
19
+ expiresIn: number;
20
+ } | {
21
+ status: 'complete';
22
+ email: string;
23
+ scopes: string[];
24
+ } | {
25
+ status: 'partial';
26
+ email: string;
27
+ grantedScopes: string[];
28
+ missingScopes: string[];
29
+ message: string;
30
+ } | {
31
+ status: 'denied';
32
+ message: string;
33
+ } | {
34
+ status: 'expired';
35
+ message: string;
36
+ } | {
37
+ status: 'error';
38
+ message: string;
39
+ };
40
+ /**
41
+ * Start or poll the auth flow for an email.
42
+ *
43
+ * This is the single entry point — handles all states:
44
+ * - No session → start new auth server
45
+ * - Pending session with live server → return waiting/started
46
+ * - Completed session → return result, clean up
47
+ * - Stale session (dead pid) → clean up, restart
48
+ */
49
+ export declare function authAdd(email: string): Promise<AuthFlowStatus>;
50
+ //# sourceMappingURL=auth-flow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-flow.d.ts","sourceRoot":"","sources":["../src/auth-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAiBH,MAAM,MAAM,cAAc,GACtB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,GACvD;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,EAAE,CAAC;IAAC,aAAa,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACvG;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACtC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAmBzC;;;;;;;;GAQG;AACH,wBAAsB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAyGpE"}
@@ -0,0 +1,219 @@
1
+ /**
2
+ * auth-flow — Two-phase OAuth flow for agent-compatible auth.
3
+ *
4
+ * Phase 1 (start): Spawn background auth server, return URL.
5
+ * Phase 2 (poll): Check pending file for completion status.
6
+ *
7
+ * The agent calls `npx go-easy auth add <email>` which:
8
+ * - On first call: starts the server, returns { status: "started", authUrl }
9
+ * - On subsequent calls: polls and returns current status
10
+ * - When user completes auth: returns { status: "complete" }
11
+ */
12
+ import { readFile, unlink, stat } from 'node:fs/promises';
13
+ import { join } from 'node:path';
14
+ import { spawn } from 'node:child_process';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { getPendingDir, readAccountStore, readCredentials, findAccount, } from './auth-store.js';
17
+ import { ALL_SCOPES, scopeToService } from './scopes.js';
18
+ import { AuthError } from './errors.js';
19
+ // ─── Public API ────────────────────────────────────────────
20
+ /**
21
+ * Start or poll the auth flow for an email.
22
+ *
23
+ * This is the single entry point — handles all states:
24
+ * - No session → start new auth server
25
+ * - Pending session with live server → return waiting/started
26
+ * - Completed session → return result, clean up
27
+ * - Stale session (dead pid) → clean up, restart
28
+ */
29
+ export async function authAdd(email) {
30
+ // Check if already fully configured
31
+ const store = await readAccountStore();
32
+ if (store) {
33
+ const account = findAccount(store, email);
34
+ if (account?.tokens.combined) {
35
+ const hasAll = ALL_SCOPES.every((s) => account.tokens.combined.scopes.includes(s));
36
+ if (hasAll) {
37
+ // Clean up any stale pending file
38
+ await cleanupPending(email);
39
+ return {
40
+ status: 'complete',
41
+ email: account.email,
42
+ scopes: account.tokens.combined.scopes.map((s) => scopeToService(s) ?? s),
43
+ };
44
+ }
45
+ }
46
+ }
47
+ // Check credentials exist
48
+ const creds = await readCredentials();
49
+ if (!creds) {
50
+ throw new AuthError('AUTH_NO_CREDENTIALS', {
51
+ message: 'OAuth client credentials not found at ~/.go-easy/credentials.json',
52
+ });
53
+ }
54
+ // Check for pending session
55
+ const pending = await readPending(email);
56
+ if (pending) {
57
+ // Terminal states: return and clean up
58
+ if (pending.status === 'complete') {
59
+ await cleanupPending(email);
60
+ return {
61
+ status: 'complete',
62
+ email: pending.email ?? email,
63
+ scopes: pending.scopes ?? [],
64
+ };
65
+ }
66
+ if (pending.status === 'partial') {
67
+ await cleanupPending(email);
68
+ return {
69
+ status: 'partial',
70
+ email: pending.email ?? email,
71
+ grantedScopes: pending.grantedScopes ?? [],
72
+ missingScopes: pending.missingScopes ?? [],
73
+ message: pending.message ?? 'Partial authorization',
74
+ };
75
+ }
76
+ if (pending.status === 'denied') {
77
+ await cleanupPending(email);
78
+ return {
79
+ status: 'denied',
80
+ message: pending.message ?? 'User declined authorization',
81
+ };
82
+ }
83
+ if (pending.status === 'expired') {
84
+ await cleanupPending(email);
85
+ return {
86
+ status: 'expired',
87
+ message: pending.message ?? 'Authorization timed out',
88
+ };
89
+ }
90
+ if (pending.status === 'error') {
91
+ await cleanupPending(email);
92
+ return {
93
+ status: 'error',
94
+ message: pending.message ?? 'Unknown error during authorization',
95
+ };
96
+ }
97
+ // Active session (waiting/started)
98
+ if (pending.status === 'waiting' || pending.status === 'started') {
99
+ // Check if the server process is still alive
100
+ if (pending.pid && isProcessAlive(pending.pid)) {
101
+ // Check if expired by time
102
+ if (pending.expiresAt && new Date(pending.expiresAt) < new Date()) {
103
+ await cleanupPending(email);
104
+ return {
105
+ status: 'expired',
106
+ message: 'Authorization timed out',
107
+ };
108
+ }
109
+ // Still waiting — return current state
110
+ const expiresIn = pending.expiresAt
111
+ ? Math.max(0, Math.floor((new Date(pending.expiresAt).getTime() - Date.now()) / 1000))
112
+ : 300;
113
+ return {
114
+ status: 'waiting',
115
+ authUrl: pending.authUrl ?? '',
116
+ expiresIn,
117
+ };
118
+ }
119
+ // PID is dead — stale session, clean up and restart
120
+ await cleanupPending(email);
121
+ }
122
+ }
123
+ // Start new auth flow
124
+ return startAuthServer(email);
125
+ }
126
+ // ─── Internal ──────────────────────────────────────────────
127
+ function pendingFilePath(email) {
128
+ return join(getPendingDir(), `${email.toLowerCase().trim()}.json`);
129
+ }
130
+ async function readPending(email) {
131
+ try {
132
+ const raw = await readFile(pendingFilePath(email), 'utf-8');
133
+ return JSON.parse(raw);
134
+ }
135
+ catch {
136
+ return null;
137
+ }
138
+ }
139
+ async function cleanupPending(email) {
140
+ try {
141
+ await unlink(pendingFilePath(email));
142
+ }
143
+ catch {
144
+ // Already deleted or never existed
145
+ }
146
+ }
147
+ function isProcessAlive(pid) {
148
+ try {
149
+ process.kill(pid, 0); // Signal 0 = just check existence
150
+ return true;
151
+ }
152
+ catch {
153
+ return false;
154
+ }
155
+ }
156
+ /**
157
+ * Spawn the auth-server as a fully detached background process.
158
+ *
159
+ * Uses stdio: 'ignore' so the child has no pipe ties to the parent.
160
+ * The child writes its startup info to the pending/<email>.json file.
161
+ * We poll that file briefly to get the authUrl.
162
+ */
163
+ async function startAuthServer(email) {
164
+ // Resolve the auth-server script path
165
+ const thisDir = typeof __dirname !== 'undefined'
166
+ ? __dirname
167
+ : fileURLToPath(new URL('.', import.meta.url));
168
+ const serverScript = join(thisDir, 'auth-server.js');
169
+ // Verify the script exists
170
+ try {
171
+ await stat(serverScript);
172
+ }
173
+ catch {
174
+ throw new AuthError('AUTH_ERROR', {
175
+ message: `Auth server script not found: ${serverScript}`,
176
+ });
177
+ }
178
+ // Clean up any old pending file first
179
+ await cleanupPending(email);
180
+ // Spawn fully detached — no pipes, child survives parent exit
181
+ const child = spawn(process.execPath, // node.exe
182
+ [serverScript, email.toLowerCase().trim(), '0'], {
183
+ detached: true,
184
+ stdio: 'ignore',
185
+ windowsHide: true,
186
+ });
187
+ child.unref();
188
+ // Poll the pending file for the server's startup info
189
+ const maxWait = 10_000; // 10s
190
+ const pollInterval = 100; // 100ms
191
+ const start = Date.now();
192
+ while (Date.now() - start < maxWait) {
193
+ await sleep(pollInterval);
194
+ const pending = await readPending(email);
195
+ if (pending && pending.authUrl) {
196
+ return {
197
+ status: 'started',
198
+ authUrl: pending.authUrl,
199
+ expiresIn: pending.expiresAt
200
+ ? Math.max(0, Math.floor((new Date(pending.expiresAt).getTime() - Date.now()) / 1000))
201
+ : 300,
202
+ };
203
+ }
204
+ // If the server wrote an error/terminal state, return it
205
+ if (pending && pending.status === 'error') {
206
+ return {
207
+ status: 'error',
208
+ message: pending.message ?? 'Auth server failed to start',
209
+ };
210
+ }
211
+ }
212
+ throw new AuthError('AUTH_ERROR', {
213
+ message: 'Auth server did not start within 10 seconds',
214
+ });
215
+ }
216
+ function sleep(ms) {
217
+ return new Promise((resolve) => setTimeout(resolve, ms));
218
+ }
219
+ //# sourceMappingURL=auth-flow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-flow.js","sourceRoot":"","sources":["../src/auth-flow.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * auth-server — Background loopback OAuth callback server.
4
+ *
5
+ * Spawned as a detached child by auth-flow.ts.
6
+ * Listens on 127.0.0.1:<port>, waits for Google OAuth callback,
7
+ * exchanges code for token, writes result to pending/<email>.json,
8
+ * updates accounts.json.
9
+ *
10
+ * Communicates with parent via:
11
+ * 1. stdout: JSON line with { port, pid, authUrl } (parent reads this, then detaches)
12
+ * 2. pending/<email>.json file (poll target for subsequent CLI calls)
13
+ *
14
+ * Usage (not meant to be called directly):
15
+ * node auth-server.js <email> <port>
16
+ */
17
+ export {};
18
+ //# sourceMappingURL=auth-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-server.d.ts","sourceRoot":"","sources":["../src/auth-server.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;GAcG"}