boringpm 0.1.2

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/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # CLI
2
+
3
+ Project manager CLI for scripts, automation, and agent workflows.
4
+
5
+ ## Commands
6
+
7
+ When installed globally, the command is:
8
+
9
+ - `boringpm`
10
+
11
+ ## Scripts
12
+
13
+ ```bash
14
+ npm run dev -- --help
15
+ npm run lint
16
+ npm run build
17
+ ```
18
+
19
+ ## Login UX (recommended)
20
+
21
+ Interactive login with persisted token:
22
+
23
+ ```bash
24
+ npm run dev -- login
25
+ npm run dev -- whoami
26
+ npm run dev -- project list
27
+ npm run dev -- logout
28
+ ```
29
+
30
+ This stores auth session data in:
31
+
32
+ - `~/.projectmanager/auth.json`
33
+
34
+ ## Project reference behavior
35
+
36
+ Commands that target a project accept either:
37
+
38
+ - project ID
39
+ - project name (case-insensitive exact match)
40
+
41
+ If multiple projects have the same name, use the project ID.
42
+
43
+ ## Team management examples
44
+
45
+ ```bash
46
+ # Create and manage teams
47
+ npm run dev -- team create "Engineering Team"
48
+ npm run dev -- team list
49
+ npm run dev -- team view <team-id>
50
+ npm run dev -- team update <team-id> --name "New Team Name"
51
+
52
+ # Add and remove team members (owner only)
53
+ npm run dev -- team add-member <team-id> <user-id>
54
+ npm run dev -- team remove-member <team-id> <user-id>
55
+
56
+ # List projects in a team
57
+ npm run dev -- team projects <team-id>
58
+
59
+ # Delete team (owner only, blocked if projects exist)
60
+ npm run dev -- team delete <team-id>
61
+ ```
62
+
63
+ ## Project management examples
64
+
65
+ ```bash
66
+ # Create project (optionally linked to team)
67
+ npm run dev -- project create "My Project"
68
+ npm run dev -- project create "Team Project" --team <team-id>
69
+
70
+ # View projects
71
+ npm run dev -- project list
72
+ npm run dev -- project view minigta
73
+ ```
74
+
75
+ ## Single-entity view examples
76
+
77
+ ```bash
78
+ npm run dev -- plan view <plan-id>
79
+ npm run dev -- task view <task-id>
80
+ ```
81
+
82
+ ## Spec-first workflow examples
83
+
84
+ ```bash
85
+ # Create and approve spec first
86
+ npm run dev -- plan create minigta "Auth hardening spec" --content "..."
87
+ npm run dev -- plan approve <plan-id>
88
+
89
+ # Create task linked to approved spec
90
+ npm run dev -- task create minigta "Implement auth guard" --plan <plan-id>
91
+ ```
92
+
93
+ ## Agent workflow examples
94
+
95
+ ```bash
96
+ # See only tasks assigned to you in one project
97
+ npm run dev -- task list minigta --mine
98
+
99
+ # See your tasks across all visible projects
100
+ npm run dev -- task mine --status in_progress
101
+
102
+ # Claim a specific task
103
+ npm run dev -- task claim <task-id>
104
+
105
+ # Claim the next pending task in the project
106
+ npm run dev -- task next minigta
107
+
108
+ # Add test scenarios and record results
109
+ npm run dev -- task test-add minigta <task-id> "happy path" --steps "..." --expected "..."
110
+ npm run dev -- task tests minigta <task-id>
111
+ npm run dev -- task test-result minigta <task-id> <scenario-id> pass --evidence "CI run #123"
112
+
113
+ # Move task to testing, then complete with note
114
+ npm run dev -- task move <task-id> testing
115
+ npm run dev -- task done <task-id> --note "Implemented endpoint + tests"
116
+
117
+ # Forced completion requires waiver metadata
118
+ npm run dev -- task done <task-id> --force --waiver-reason "prod incident" --approved-by lead-user-id
119
+
120
+ # Release assignment (defaults to pending when in_progress/testing)
121
+ npm run dev -- task release <task-id>
122
+
123
+ # Add or read task comments
124
+ npm run dev -- task comment minigta <task-id> "started implementation"
125
+ npm run dev -- task comments minigta <task-id>
126
+
127
+ # Legacy alias (also clears assignee)
128
+ npm run dev -- task unclaim <task-id>
129
+
130
+ # Delete commands prompt for confirmation by default
131
+ npm run dev -- task delete <task-id>
132
+ npm run dev -- plan delete <plan-id>
133
+
134
+ # Use --force / -f for non-interactive runs
135
+ npm run dev -- task delete <task-id> --force
136
+ npm run dev -- plan delete <plan-id> -f
137
+ ```
138
+
139
+ ## Completion gate
140
+
141
+ `task done` is server-enforced and succeeds only when:
142
+
143
+ - task status is `testing`
144
+ - linked spec plan is `approved`
145
+ - at least one test scenario exists
146
+ - every test scenario has `status=pass`
147
+
148
+ Use `--force --waiver-reason --approved-by` only for audited exceptions.
149
+
150
+ ## Non-interactive auth options
151
+
152
+ ### 1) Environment token
153
+
154
+ ```bash
155
+ export PM_AUTH_TOKEN=<token>
156
+ ```
157
+
158
+ ### 2) Per-command token
159
+
160
+ ```bash
161
+ npm run dev -- --token <token> project list
162
+ ```
163
+
164
+ ### 3) Raw auth endpoints
165
+
166
+ ```bash
167
+ npm run dev -- auth send-link you@company.com
168
+ npm run dev -- auth verify you@company.com <code>
169
+ npm run dev -- auth me --token <token>
170
+ ```
package/dist/api.js ADDED
@@ -0,0 +1,487 @@
1
+ import { z } from "zod";
2
+ const apiResponseSchema = (dataSchema) => z.object({
3
+ data: dataSchema,
4
+ });
5
+ const teamSchema = z.object({
6
+ id: z.string(),
7
+ name: z.string(),
8
+ owner: z.string(),
9
+ members: z.array(z.string()),
10
+ created_at: z.string(),
11
+ updated_at: z.string(),
12
+ });
13
+ const projectSchema = z.object({
14
+ id: z.string(),
15
+ name: z.string(),
16
+ created_at: z.string(),
17
+ owner: z.string(),
18
+ participants: z.array(z.string()),
19
+ team: z.string().nullable(),
20
+ });
21
+ const planSchema = z.object({
22
+ id: z.string(),
23
+ title: z.string(),
24
+ content: z.string(),
25
+ created_at: z.string(),
26
+ updated_at: z.string(),
27
+ owner: z.string(),
28
+ project: z.string(),
29
+ spec_status: z.enum(["draft", "approved", "deprecated"]),
30
+ });
31
+ const validationSnapshotSchema = z.object({
32
+ spec_plan_id: z.string(),
33
+ spec_plan_status: z.enum(["draft", "approved", "deprecated"]),
34
+ total_scenarios: z.number(),
35
+ pass_scenarios: z.number(),
36
+ all_passed: z.boolean(),
37
+ validated_by: z.string(),
38
+ validated_at: z.string(),
39
+ waived: z.boolean(),
40
+ waiver_reason: z.string().nullable(),
41
+ approved_by: z.string().nullable(),
42
+ scenario_statuses: z.array(z.object({
43
+ id: z.string(),
44
+ status: z.enum(["pending", "pass", "fail", "blocked"]),
45
+ ran_at: z.string().nullable(),
46
+ ran_by: z.string().nullable(),
47
+ })),
48
+ });
49
+ const taskSchema = z.object({
50
+ id: z.string(),
51
+ title: z.string(),
52
+ description: z.string(),
53
+ created_at: z.string(),
54
+ owner: z.string(),
55
+ assignee: z.string().nullable(),
56
+ agent: z.string().nullable(),
57
+ status: z.enum(["pending", "in_progress", "testing", "completed", "archived"]),
58
+ project: z.string(),
59
+ plan: z.string().nullable(),
60
+ updated_at: z.string(),
61
+ completed_note: z.string().nullable(),
62
+ completed_at: z.string().nullable(),
63
+ validation_snapshot: validationSnapshotSchema.nullable(),
64
+ });
65
+ const testScenarioSchema = z.object({
66
+ id: z.string(),
67
+ task: z.string(),
68
+ title: z.string(),
69
+ type: z.enum(["manual", "automated", "integration", "e2e"]),
70
+ steps: z.string(),
71
+ expected_result: z.string(),
72
+ status: z.enum(["pending", "pass", "fail", "blocked"]),
73
+ evidence: z.string().nullable(),
74
+ ran_by: z.string().nullable(),
75
+ ran_at: z.string().nullable(),
76
+ created_at: z.string(),
77
+ updated_at: z.string(),
78
+ });
79
+ const commentSchema = z.object({
80
+ id: z.string(),
81
+ task: z.string(),
82
+ author: z.string(),
83
+ content: z.string(),
84
+ created_at: z.string(),
85
+ });
86
+ const boardDataSchema = z.object({
87
+ project: projectSchema,
88
+ plans: z.array(planSchema),
89
+ tasks: z.array(taskSchema),
90
+ groupedTasks: z.record(z.array(taskSchema)),
91
+ });
92
+ const authUserSchema = z.object({
93
+ id: z.string(),
94
+ email: z.string().nullable(),
95
+ });
96
+ export class ApiClient {
97
+ baseUrl;
98
+ authToken;
99
+ actorName;
100
+ constructor(config) {
101
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
102
+ this.authToken = config.authToken;
103
+ this.actorName = config.actorName;
104
+ }
105
+ async health() {
106
+ return this.request("/health", {
107
+ method: "GET",
108
+ });
109
+ }
110
+ async sendMagicLink(email) {
111
+ const payload = await this.request("/auth/magic-link", {
112
+ method: "POST",
113
+ body: { email },
114
+ });
115
+ return apiResponseSchema(z.object({ sent: z.boolean() })).parse(payload).data;
116
+ }
117
+ async verifyMagicCode(email, code) {
118
+ const payload = await this.request("/auth/verify", {
119
+ method: "POST",
120
+ body: { email, code },
121
+ });
122
+ return apiResponseSchema(z.object({ token: z.string(), user: authUserSchema })).parse(payload)
123
+ .data;
124
+ }
125
+ async me() {
126
+ const payload = await this.request("/auth/me", {
127
+ method: "GET",
128
+ authToken: this.requireToken(),
129
+ });
130
+ return apiResponseSchema(authUserSchema).parse(payload).data;
131
+ }
132
+ async createTeam(input) {
133
+ const payload = await this.request("/teams", {
134
+ method: "POST",
135
+ body: input,
136
+ authToken: this.requireToken(),
137
+ });
138
+ return apiResponseSchema(teamSchema).parse(payload).data;
139
+ }
140
+ async listTeams() {
141
+ const payload = await this.request("/teams", {
142
+ method: "GET",
143
+ authToken: this.requireToken(),
144
+ });
145
+ return apiResponseSchema(z.array(teamSchema)).parse(payload).data;
146
+ }
147
+ async getTeam(teamId) {
148
+ const payload = await this.request(`/teams/${teamId}`, {
149
+ method: "GET",
150
+ authToken: this.requireToken(),
151
+ });
152
+ return apiResponseSchema(teamSchema).parse(payload).data;
153
+ }
154
+ async updateTeam(input) {
155
+ const payload = await this.request(`/teams/${input.teamId}`, {
156
+ method: "PATCH",
157
+ body: {
158
+ name: input.name,
159
+ },
160
+ authToken: this.requireToken(),
161
+ });
162
+ return apiResponseSchema(teamSchema).parse(payload).data;
163
+ }
164
+ async deleteTeam(teamId) {
165
+ const payload = await this.request(`/teams/${teamId}`, {
166
+ method: "DELETE",
167
+ authToken: this.requireToken(),
168
+ });
169
+ return apiResponseSchema(z.object({
170
+ id: z.string(),
171
+ deleted: z.boolean(),
172
+ })).parse(payload).data;
173
+ }
174
+ async addTeamMember(teamId, userId) {
175
+ const payload = await this.request(`/teams/${teamId}/members`, {
176
+ method: "POST",
177
+ body: { userId },
178
+ authToken: this.requireToken(),
179
+ });
180
+ return apiResponseSchema(teamSchema).parse(payload).data;
181
+ }
182
+ async removeTeamMember(teamId, userId) {
183
+ const payload = await this.request(`/teams/${teamId}/members/${userId}`, {
184
+ method: "DELETE",
185
+ authToken: this.requireToken(),
186
+ });
187
+ return apiResponseSchema(teamSchema).parse(payload).data;
188
+ }
189
+ async listTeamProjects(teamId) {
190
+ const payload = await this.request(`/teams/${teamId}/projects`, {
191
+ method: "GET",
192
+ authToken: this.requireToken(),
193
+ });
194
+ return apiResponseSchema(z.array(projectSchema)).parse(payload).data;
195
+ }
196
+ async createTeamProject(input) {
197
+ const payload = await this.request(`/teams/${input.teamId}/projects`, {
198
+ method: "POST",
199
+ body: {
200
+ name: input.name,
201
+ participants: input.participants,
202
+ },
203
+ authToken: this.requireToken(),
204
+ });
205
+ return apiResponseSchema(projectSchema).parse(payload).data;
206
+ }
207
+ async listProjects() {
208
+ const payload = await this.request("/projects", {
209
+ method: "GET",
210
+ authToken: this.requireToken(),
211
+ });
212
+ return apiResponseSchema(z.array(projectSchema)).parse(payload).data;
213
+ }
214
+ async getProject(projectId) {
215
+ const payload = await this.request(`/projects/${projectId}`, {
216
+ method: "GET",
217
+ authToken: this.requireToken(),
218
+ });
219
+ return apiResponseSchema(projectSchema).parse(payload).data;
220
+ }
221
+ async createProject(input) {
222
+ const payload = await this.request("/projects", {
223
+ method: "POST",
224
+ body: input,
225
+ authToken: this.requireToken(),
226
+ });
227
+ return apiResponseSchema(projectSchema).parse(payload).data;
228
+ }
229
+ async addProjectParticipant(projectId, participantUserId) {
230
+ const payload = await this.request(`/projects/${projectId}/participants`, {
231
+ method: "POST",
232
+ body: { userId: participantUserId },
233
+ authToken: this.requireToken(),
234
+ });
235
+ return apiResponseSchema(projectSchema).parse(payload).data;
236
+ }
237
+ async removeProjectParticipant(projectId, participantUserId) {
238
+ const payload = await this.request(`/projects/${projectId}/participants/${participantUserId}`, {
239
+ method: "DELETE",
240
+ authToken: this.requireToken(),
241
+ });
242
+ return apiResponseSchema(projectSchema).parse(payload).data;
243
+ }
244
+ async listPlans(projectId) {
245
+ const payload = await this.request(`/projects/${projectId}/plans`, {
246
+ method: "GET",
247
+ authToken: this.requireToken(),
248
+ });
249
+ return apiResponseSchema(z.array(planSchema)).parse(payload).data;
250
+ }
251
+ async getPlan(planId) {
252
+ const payload = await this.request(`/plans/${planId}`, {
253
+ method: "GET",
254
+ authToken: this.requireToken(),
255
+ });
256
+ return apiResponseSchema(planSchema).parse(payload).data;
257
+ }
258
+ async createPlan(input) {
259
+ const payload = await this.request(`/projects/${input.projectId}/plans`, {
260
+ method: "POST",
261
+ body: {
262
+ title: input.title,
263
+ content: input.content,
264
+ spec_status: input.spec_status,
265
+ },
266
+ authToken: this.requireToken(),
267
+ });
268
+ return apiResponseSchema(planSchema).parse(payload).data;
269
+ }
270
+ async updatePlan(input) {
271
+ const payload = await this.request(`/plans/${input.planId}`, {
272
+ method: "PATCH",
273
+ body: {
274
+ title: input.title,
275
+ content: input.content,
276
+ spec_status: input.spec_status,
277
+ },
278
+ authToken: this.requireToken(),
279
+ });
280
+ return apiResponseSchema(planSchema).parse(payload).data;
281
+ }
282
+ async deletePlan(planId) {
283
+ const payload = await this.request(`/plans/${planId}`, {
284
+ method: "DELETE",
285
+ authToken: this.requireToken(),
286
+ });
287
+ return apiResponseSchema(z.object({
288
+ id: z.string(),
289
+ deleted: z.boolean(),
290
+ unlinkedTasks: z.number(),
291
+ })).parse(payload).data;
292
+ }
293
+ async listTasks(projectId, filters) {
294
+ const query = new URLSearchParams();
295
+ if (filters?.status) {
296
+ query.set("status", filters.status);
297
+ }
298
+ if (filters?.mine) {
299
+ query.set("mine", "true");
300
+ }
301
+ if (filters?.assignee !== undefined) {
302
+ query.set("assignee", filters.assignee === null ? "none" : filters.assignee);
303
+ }
304
+ if (filters?.agent !== undefined) {
305
+ query.set("agent", filters.agent === null ? "none" : filters.agent);
306
+ }
307
+ const queryString = query.toString();
308
+ const path = queryString
309
+ ? `/projects/${projectId}/tasks?${queryString}`
310
+ : `/projects/${projectId}/tasks`;
311
+ const payload = await this.request(path, {
312
+ method: "GET",
313
+ authToken: this.requireToken(),
314
+ });
315
+ return apiResponseSchema(z.array(taskSchema)).parse(payload).data;
316
+ }
317
+ async getTask(taskId) {
318
+ const payload = await this.request(`/tasks/${taskId}`, {
319
+ method: "GET",
320
+ authToken: this.requireToken(),
321
+ });
322
+ return apiResponseSchema(taskSchema).parse(payload).data;
323
+ }
324
+ async createTask(input) {
325
+ const payload = await this.request(`/projects/${input.projectId}/tasks`, {
326
+ method: "POST",
327
+ body: {
328
+ title: input.title,
329
+ description: input.description,
330
+ status: input.status,
331
+ plan: input.plan,
332
+ agent: input.agent,
333
+ },
334
+ authToken: this.requireToken(),
335
+ });
336
+ return apiResponseSchema(taskSchema).parse(payload).data;
337
+ }
338
+ async updateTask(input) {
339
+ const payload = await this.request(`/tasks/${input.taskId}`, {
340
+ method: "PATCH",
341
+ body: {
342
+ title: input.title,
343
+ description: input.description,
344
+ status: input.status,
345
+ plan: input.plan,
346
+ assignee: input.assignee,
347
+ agent: input.agent,
348
+ },
349
+ authToken: this.requireToken(),
350
+ });
351
+ return apiResponseSchema(taskSchema).parse(payload).data;
352
+ }
353
+ async claimTask(taskId) {
354
+ const payload = await this.request(`/tasks/${taskId}/claim`, {
355
+ method: "POST",
356
+ authToken: this.requireToken(),
357
+ });
358
+ return apiResponseSchema(taskSchema).parse(payload).data;
359
+ }
360
+ async releaseTask(input) {
361
+ const payload = await this.request(`/tasks/${input.taskId}/release`, {
362
+ method: "POST",
363
+ body: {
364
+ status: input.status,
365
+ },
366
+ authToken: this.requireToken(),
367
+ });
368
+ return apiResponseSchema(taskSchema).parse(payload).data;
369
+ }
370
+ async claimNextTask(projectId) {
371
+ const payload = await this.request(`/projects/${projectId}/tasks/next`, {
372
+ method: "POST",
373
+ authToken: this.requireToken(),
374
+ });
375
+ return apiResponseSchema(taskSchema).parse(payload).data;
376
+ }
377
+ async completeTask(input) {
378
+ const payload = await this.request(`/tasks/${input.taskId}/done`, {
379
+ method: "POST",
380
+ body: {
381
+ note: input.note,
382
+ force: input.force,
383
+ waiver_reason: input.waiver_reason,
384
+ approved_by: input.approved_by,
385
+ },
386
+ authToken: this.requireToken(),
387
+ });
388
+ return apiResponseSchema(taskSchema).parse(payload).data;
389
+ }
390
+ async listTestScenarios(taskId) {
391
+ const payload = await this.request(`/tasks/${taskId}/test-scenarios`, {
392
+ method: "GET",
393
+ authToken: this.requireToken(),
394
+ });
395
+ return apiResponseSchema(z.array(testScenarioSchema)).parse(payload).data;
396
+ }
397
+ async createTestScenario(input) {
398
+ const payload = await this.request(`/tasks/${input.taskId}/test-scenarios`, {
399
+ method: "POST",
400
+ body: {
401
+ title: input.title,
402
+ type: input.type,
403
+ steps: input.steps,
404
+ expected_result: input.expected_result,
405
+ },
406
+ authToken: this.requireToken(),
407
+ });
408
+ return apiResponseSchema(testScenarioSchema).parse(payload).data;
409
+ }
410
+ async updateTestScenarioResult(input) {
411
+ const payload = await this.request(`/tasks/${input.taskId}/test-scenarios/${input.scenarioId}/result`, {
412
+ method: "PATCH",
413
+ body: {
414
+ status: input.status,
415
+ evidence: input.evidence,
416
+ },
417
+ authToken: this.requireToken(),
418
+ });
419
+ return apiResponseSchema(testScenarioSchema).parse(payload).data;
420
+ }
421
+ async listTaskComments(taskId) {
422
+ const payload = await this.request(`/tasks/${taskId}/comments`, {
423
+ method: "GET",
424
+ authToken: this.requireToken(),
425
+ });
426
+ return apiResponseSchema(z.array(commentSchema)).parse(payload).data;
427
+ }
428
+ async createTaskComment(taskId, content) {
429
+ const payload = await this.request(`/tasks/${taskId}/comments`, {
430
+ method: "POST",
431
+ body: {
432
+ content,
433
+ },
434
+ authToken: this.requireToken(),
435
+ });
436
+ return apiResponseSchema(commentSchema).parse(payload).data;
437
+ }
438
+ async deleteTask(taskId) {
439
+ const payload = await this.request(`/tasks/${taskId}`, {
440
+ method: "DELETE",
441
+ authToken: this.requireToken(),
442
+ });
443
+ return apiResponseSchema(z.object({
444
+ id: z.string(),
445
+ deleted: z.boolean(),
446
+ })).parse(payload).data;
447
+ }
448
+ async board(projectId) {
449
+ const payload = await this.request(`/projects/${projectId}/board`, {
450
+ method: "GET",
451
+ authToken: this.requireToken(),
452
+ });
453
+ return apiResponseSchema(boardDataSchema).parse(payload).data;
454
+ }
455
+ requireToken() {
456
+ if (!this.authToken) {
457
+ throw new Error("Missing auth token. Provide --token, PM_AUTH_TOKEN, or log in to save a session token.");
458
+ }
459
+ return this.authToken;
460
+ }
461
+ async request(path, options = {}) {
462
+ const headers = {
463
+ "Content-Type": "application/json",
464
+ };
465
+ if (options.authToken) {
466
+ headers.Authorization = `Bearer ${options.authToken}`;
467
+ headers["x-auth-token"] = options.authToken;
468
+ }
469
+ if (this.actorName) {
470
+ headers["x-actor-name"] = this.actorName;
471
+ }
472
+ const response = await fetch(`${this.baseUrl}${path}`, {
473
+ method: options.method ?? "GET",
474
+ headers,
475
+ body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
476
+ });
477
+ const text = await response.text();
478
+ const payload = text ? JSON.parse(text) : {};
479
+ if (!response.ok) {
480
+ const message = typeof payload?.error === "string"
481
+ ? payload.error
482
+ : `Request failed with status ${response.status}`;
483
+ throw new Error(message);
484
+ }
485
+ return payload;
486
+ }
487
+ }
@@ -0,0 +1,50 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { mkdir, rm, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ const AUTH_DIRECTORY = join(homedir(), ".projectmanager");
6
+ const AUTH_FILE = join(AUTH_DIRECTORY, "auth.json");
7
+ export function getAuthFilePath() {
8
+ return AUTH_FILE;
9
+ }
10
+ export function loadStoredAuthSession() {
11
+ if (!existsSync(AUTH_FILE)) {
12
+ return null;
13
+ }
14
+ try {
15
+ const raw = readFileSync(AUTH_FILE, "utf8");
16
+ const parsed = JSON.parse(raw);
17
+ if (typeof parsed.token !== "string" || !parsed.token.trim()) {
18
+ return null;
19
+ }
20
+ return {
21
+ token: parsed.token,
22
+ user: parsed.user &&
23
+ typeof parsed.user.id === "string" &&
24
+ (typeof parsed.user.email === "string" || parsed.user.email === null)
25
+ ? {
26
+ id: parsed.user.id,
27
+ email: parsed.user.email,
28
+ }
29
+ : undefined,
30
+ savedAt: typeof parsed.savedAt === "string" && parsed.savedAt.trim()
31
+ ? parsed.savedAt
32
+ : new Date().toISOString(),
33
+ };
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ export function loadStoredAuthToken() {
40
+ return loadStoredAuthSession()?.token;
41
+ }
42
+ export async function saveStoredAuthSession(session) {
43
+ await mkdir(AUTH_DIRECTORY, { recursive: true });
44
+ await writeFile(AUTH_FILE, JSON.stringify(session, null, 2), {
45
+ mode: 0o600,
46
+ });
47
+ }
48
+ export async function clearStoredAuthSession() {
49
+ await rm(AUTH_FILE, { force: true });
50
+ }