Skip to content

Port to TypeScript strict + types-as-API-spec (T0–T6) - #66

Merged
KalebCole merged 15 commits into
mainfrom
feat/typescript-port
Jul 25, 2026
Merged

Port to TypeScript strict + types-as-API-spec (T0–T6)#66
KalebCole merged 15 commits into
mainfrom
feat/typescript-port

Conversation

@KalebCole

@KalebCole KalebCole commented Jul 24, 2026

Copy link
Copy Markdown
Owner

What

Port the entire partiful-cli codebase from plain JavaScript to TypeScript strict mode, and make the src/lib/api/ layer a living API spec (typed request interfaces + Zod .passthrough() response schemas) rather than a separate hand-maintained artifact that can drift.

src/ is now 0 .js files. tsc --noEmit is clean; the full suite is green.

Why

The ~23 untyped result?.data... API-response spreads were the only record of Partiful's (unofficial, undocumented) API shape. Typing the lib layer captures that shape in code, so the spec is a byproduct of the port, not a doc that rots.

How it runs (no build step)

  • tsx@4 is registered as an ESM loader by bin/partiful; Node runs the .ts tree directly. No dist/.
  • npm run typecheck = tsc --noEmit · npm test = vitest.
  • NodeNext: import specifiers keep the .js extension even though targets are .ts (required, not a bug).

Tickets (T0–T6)

# Scope
T1 TS toolchain: strict tsconfig, tsx loader, zod
T2 docs/TYPESCRIPT-PORT-GUIDE.md convention doc
T3 src/lib/ → TS strict + author src/lib/api/{envelope,endpoints}.ts (THE SPEC)
T4 src/commands/ + src/helpers/ + cli.ts → TS strict
T5 schema api.<method> namespace driven off the endpoint registry
T6 Drift detection (src/lib/drift.ts) + gated real-API smoke suite

New capabilities

  • schema api lists every spec'd endpoint; schema api.<method> prints its { transport, host, path, requestParams, responseFields }. Legacy schema <command> (CLI-flag lookup) unchanged.
  • Drift detection: responses are diffed against the spec's declared field surface; unknown vendor fields are surfaced. Opt-in logging via PARTIFUL_DRIFT_LOG (unset = silent / 1|stderr = stderr / path = NDJSON append). Wired guarded into apiRequest() — advisory only, can never break a request.
  • Real-API smoke tests (tests/smoke-real-api.test.js): the spec verifier. Read-only endpoints, skipIf(!PARTIFUL_SMOKE), no secrets in CI.

Test status

  • Baseline at ad7be30: 195/195 green.
  • This branch: 206 passed / 6 smoke skipped (+5 schema-api, +6 drift unit tests).
  • ./bin/partiful --version and schema verified end-to-end through the tsx loader.

Faithfulness

This is an annotation-only port: zero intended runtime behavior change. An adversarial review pass (behavior drift, ??/|| swaps, ! assertions, as casts, JSON output contract, exit codes) returned SHIP with no BLOCKER/MAJOR findings — every ??/|| swap lands on fields that never hold falsy-but-non-null values; all ! assertions are guarded.

Out of scope

  • OpenAPI/TypeSpec/standalone spec file (types-as-spec fits a 7-host / 3-method / 2-transport surface without duplication).
  • Any refactor of API logic — faithful translation only.

Summary by CodeRabbit

  • New Features

    • Added API schema introspection through schema api and schema api.<method>.
    • Added event export support in JSON or CSV format, with optional file output.
    • Added TypeScript-based startup and type-checking support.
  • Bug Fixes

    • Improved error handling and response validation across CLI commands.
    • Added optional API drift reporting for unexpected response fields.
  • Tests

    • Added coverage for schema introspection, drift detection, sharing, and live API smoke checks.

- typescript@7, tsx@4, @types/node devDeps; zod runtime dep
- tsconfig: strict + NodeNext ESM + allowJs (JS/TS coexist), noEmit
- bin/partiful registers tsx/esm loader, no dist build
- scripts: typecheck (tsc --noEmit), start
- gate: typecheck clean + 195/195 tests green on still-JS tree
docs/TYPESCRIPT-PORT-GUIDE.md — enforceable file-by-file rules + worked
createEvent endpoint example (envelope generic, request interface, Zod
passthrough response, z.infer type, introspectable metadata).
- src/lib/output.ts, errors.ts, http.ts ported (strict-clean)
- src/lib/api/envelope.ts: CallableEnvelope<P> + CallableResult<D> generics
- src/lib/api/endpoints.ts: THE SPEC — request interfaces + Zod .passthrough()
  response schemas + z.infer types + introspectable metadata registry for all
  11 callable endpoints, 3 firestore ops, token refresh
- bin/partiful: register tsx loader then dynamic-import CLI (fixes ESM hoist)
- first-slice oracle met: http+createEvent typed, strict clean, 195/195 green
…auth, cohosts, upload, rsvp, events)

All 8 remaining lib modules ported. Wired into the api/ spec types
(EventDraft, RsvpDraft, PartifulConfig, endpoint request/response types).
tsc --noEmit clean; 195/195 tests green. src/lib/ is now 100% TypeScript.
All 18 remaining JS files ported (12 commands, 4 helpers, cli.ts, schema.ts).
src/ is now 100% TypeScript. Commander handlers typed; API responses
narrowed via api/ spec types + as-casts. tsc --noEmit clean; 195/195 green;
./bin/partiful --version + schema smoke-tested via tsx loader.
Adds 'schema api' (list endpoints) and 'schema api.<method>' (per-endpoint
spec: transport, host, httpMethod, path, requestParams, responseFields)
driven off the apiEndpoints registry in api/endpoints.ts. Bare 'schema' now
also lists api.* methods. Existing 'schema <command>' CLI-flag lookup
unchanged. +5 tests (tests/schema-api.test.js). 200/200 green; tsc clean.
- src/lib/drift.ts: diff passthrough responses vs spec's declared field
  surface; detectDrift() + reportDrift(). Opt-in logging via PARTIFUL_DRIFT_LOG
  (unset=silent, 1/true/stderr=stderr line, path=NDJSON append).
- Wired centrally + guarded into apiRequest() via path->method reverse map +
  envelope unwrap; advisory only, never breaks a request.
- src/lib/api/endpoints.ts: export responseSchemas registry (method->schema).
- tests/drift.test.js: 6 unit tests (no auth needed, run everywhere).
- tests/smoke-real-api.test.js: live-API spec verifier, skipIf(!PARTIFUL_SMOKE),
  read-only endpoints, documented run instructions.
- docs/TYPESCRIPT-PORT-GUIDE.md \u00a710: implemented drift+smoke strategy, CI-vs-manual.

tsc clean; 206 passed / 6 smoke skipped.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@KalebCole, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 54 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 862b3825-8160-4556-bf1c-9134f0d2bea3

📥 Commits

Reviewing files that changed from the base of the PR and between 37de5ee and fc78c4e.

📒 Files selected for processing (1)
  • tests/drift.test.js
📝 Walkthrough

Walkthrough

The PR establishes a TypeScript migration workflow, ports CLI and library modules, centralizes typed API contracts and transports, adds schema introspection and drift detection, and introduces supporting unit, smoke, and helper tests. It also documents the explore and RSVP endpoint recon and command design.

Changes

TypeScript migration and API contracts

Layer / File(s) Summary
Migration rules and runtime setup
.wayfinder/ts-port/*, docs/TYPESCRIPT-PORT-GUIDE.md, tsconfig.json, package.json, bin/partiful
Adds strict NodeNext TypeScript configuration, tsx runtime loading, migration conventions, API typing guidance, and related scripts and dependencies.
Typed API, transport, and diagnostics
src/lib/api/*, src/lib/auth.ts, src/lib/errors.ts, src/lib/http.ts, src/lib/output.ts, src/lib/drift.ts
Adds endpoint request/response schemas, callable envelopes, typed authentication, retrying HTTP and Firestore requests, structured errors, output formatters, and advisory drift reporting.
Typed domain libraries
src/lib/{events,rsvp,cohosts,dates,posters,templates,upload}.ts
Adds typed event, RSVP/questionnaire, co-host, date, poster, template, upload, and Firestore conversion contracts.
Typed commands and helpers
src/cli.ts, src/commands/*, src/helpers/*
Ports command and helper wiring to typed Commander handlers, typed option access, structured response extraction, and safer error handling.
Schema and runtime validation
tests/*, src/commands/schema.ts
Tests API schema listing, endpoint metadata, drift detection, share-helper parity, and gated read-only live API behavior.

Explore and RSVP design

Layer / File(s) Summary
Explore and RSVP recon/design
.wayfinder/*, docs/explore-command-design.md
Documents stable discovery endpoints, server-side tag filtering, RSVP and interested mutations, questionnaire constraints, command flags, output contracts, and implementation tasks.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: a strict TypeScript port plus turning the API layer into a typed spec.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/typescript-port

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

This comment was marked as outdated.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (11)
docs/explore-command-design.md-119-123 (1)

119-123: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the wire tag IDs documented by the API recon.

DISCOVER_MUSIC and DISCOVER_FOOD do not match ticket 03’s verified tagId values MUSIC and FOOD. If this example becomes the CLI’s advertised contract, users will send invalid filters.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/explore-command-design.md` around lines 119 - 123, Update the tags
example to use the API-verified wire IDs MUSIC and FOOD for the Music and Food &
Drink entries, while preserving the existing display names and DISCOVER_HOME
entry.
.wayfinder/ts-port/tickets/T0-rsvp-merged-branch-cut.md-3-3 (1)

3-3: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Synchronize all ticket statuses with .wayfinder/ts-port/map.md.

The map records T0, T3, T4, T5, and T6 as closed on July 24, 2026, but their ticket metadata still says OPEN; T5 and T6 additionally contradict their own closed Answer sections.

  • .wayfinder/ts-port/tickets/T0-rsvp-merged-branch-cut.md#L3-L3: mark T0 closed and fill in the merge/branch-cut commit.
  • .wayfinder/ts-port/tickets/T3-port-lib-layer-spec.md#L3-L3: mark T3 closed and record the completed API/spec work.
  • .wayfinder/ts-port/tickets/T4-port-commands-helpers.md#L3-L3: mark T4 closed and record the completed command/helper port.
  • .wayfinder/ts-port/tickets/T5-rewire-schema-command.md#L3-L3: change OPEN to the closed status matching its Answer.
  • .wayfinder/ts-port/tickets/T6-drift-detection-smoke-tests.md#L3-L3: change OPEN to the closed status matching its Answer.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.wayfinder/ts-port/tickets/T0-rsvp-merged-branch-cut.md at line 3,
Synchronize ticket metadata with the closed statuses in
.wayfinder/ts-port/map.md: update
.wayfinder/ts-port/tickets/T0-rsvp-merged-branch-cut.md:3 to mark T0 closed and
add its merge/branch-cut commit; update
.wayfinder/ts-port/tickets/T3-port-lib-layer-spec.md:3 and
.wayfinder/ts-port/tickets/T4-port-commands-helpers.md:3 to mark them closed and
record their completed work; update
.wayfinder/ts-port/tickets/T5-rewire-schema-command.md:3 and
.wayfinder/ts-port/tickets/T6-drift-detection-smoke-tests.md:3 from OPEN to the
closed status used by their Answer sections.
src/lib/drift.ts-95-108 (1)

95-108: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

force overrides the sink instead of just enabling logging.

With PARTIFUL_DRIFT_LOG=/tmp/drift.ndjson and force = true, || force short-circuits to the stderr branch, line is built and discarded, and nothing is appended to the configured file. Separate "is logging enabled" from "where does it go".

🐛 Proposed fix
   if (enabled) {
     const line = JSON.stringify({ drift: record });
-    if (!sink || sink === '1' || sink === 'true' || sink === 'stderr' || force) {
+    const isFileSink =
+      sink != null && sink !== '' && !['0', '1', 'true', 'false', 'stderr'].includes(sink);
+    if (!isFileSink) {
       console.error(`[drift] ${method}: unknown fields ${unknownFields.join(', ')}`);
     } else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/drift.ts` around lines 95 - 108, Update the logging destination
condition in the drift logging flow around PARTIFUL_DRIFT_LOG so force only
enables logging and does not override a configured file sink. When force is true
with a non-empty, non-special sink path, append the serialized line to that
file; reserve the stderr branch for unset or explicitly stderr-configured sinks,
while preserving existing write-failure handling.
src/commands/doctor.ts-39-40 (1)

39-40: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

replace(home, '~') corrupts the path when HOME is unset.

'' matches at index 0, so '/etc/partiful/auth.json'.replace('', '~') returns '~/etc/partiful/auth.json' — the doctor output then points at a path that does not exist. A prefix check avoids both this and mid-path substitutions.

🐛 Proposed fix
-  const home = process.env['HOME'] ?? '';
-  const displayPath = configPath.replace(home, '~');
+  const home = process.env['HOME'] || os.homedir();
+  const displayPath =
+    home && configPath.startsWith(home) ? `~${configPath.slice(home.length)}` : configPath;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/doctor.ts` around lines 39 - 40, Update the displayPath
construction near the home and configPath values to replace HOME with "~" only
when HOME is non-empty and configPath actually starts with it; otherwise
preserve configPath unchanged. Use a prefix check rather than String.replace to
prevent unset HOME and mid-path substitutions.
src/lib/auth.ts-33-38 (1)

33-38: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

process.env.HOME as string hides a real crash path.

When HOME is unset (Windows, some CI/daemon contexts), path.join(undefined, …) throws TypeError: The "path" argument must be of type string, and the cast means tsc no longer warns. os.homedir() resolves this on every platform.

🛡️ Proposed fix
+import os from 'os';
+
 export function resolveCredentialsPath(): string {
   return (
     process.env.PARTIFUL_CREDENTIALS_FILE ||
-    path.join(process.env.HOME as string, '.config/partiful/auth.json')
+    path.join(process.env.HOME || os.homedir(), '.config/partiful/auth.json')
   );
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/auth.ts` around lines 33 - 38, Update resolveCredentialsPath to use
os.homedir() instead of process.env.HOME as string when constructing the default
credentials path, importing the required os module while preserving the
PARTIFUL_CREDENTIALS_FILE override.
src/commands/events.ts-37-40 (1)

37-40: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Unchecked (e as Error).message in catch paths drops message from the error envelope for non-Error throws. The port replaced e.message with a type assertion rather than a runtime guard, so a thrown string/object yields undefined and JSON.stringify omits the key, emitting {"status":"error","error":{"code":5,"type":"internal_error"}}. src/commands/posters.ts already uses the correct instanceof form; apply it uniformly.

  • src/commands/events.ts#L37-L40: in handleError, replace (e as Error).message with e instanceof Error ? e.message : String(e).
  • src/commands/guests.ts#L123-L125: apply the same guard in the guests list catch block.
  • src/commands/guests.ts#L181-L183: apply the same guard in the guests invite catch block.
  • src/commands/rsvp.ts#L40-L43: apply the same guard in the RSVP handleError.
  • src/helpers/watch.ts#L87-L89: apply the same guard in the +watch catch block.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/events.ts` around lines 37 - 40, Replace the unsafe error type
assertions with an Error runtime check and String fallback so every catch path
preserves a message for non-Error throws. Update handleError in
src/commands/events.ts (lines 37-40), both guests catch blocks in
src/commands/guests.ts (lines 123-125 and 181-183), handleError in
src/commands/rsvp.ts (lines 40-43), and the +watch catch block in
src/helpers/watch.ts (lines 87-89); each site requires the same guarded message
handling.
src/lib/output.ts-55-65 (1)

55-65: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

escape misses \r, and empty rows produces a stray blank line.

A cell containing a bare CR (no LF) is emitted unquoted and breaks row parsing in strict CSV readers; guest/contact names come from the API, so this is externally influenced. Also formatCsv([], cols) returns header + '\n' with an empty body row, unlike formatTable's (no results) handling.

🛠️ Proposed fix
 export function formatCsv(rows: TableRow[], columns: string[]): string {
   const escape = (v: unknown): string => {
     const s = String(v ?? '');
-    return s.includes(',') || s.includes('"') || s.includes('\n')
+    return /[",\r\n]/.test(s)
       ? `"${s.replace(/"/g, '""')}"`
       : s;
   };
   const header = columns.map(escape).join(',');
+  if (rows.length === 0) return header;
   const body = rows.map((r) => columns.map((col) => escape(r[col])).join(',')).join('\n');
   return `${header}\n${body}`;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/output.ts` around lines 55 - 65, Update formatCsv’s escape helper to
quote values containing carriage returns in addition to commas, quotes, and line
feeds. Adjust the output assembly so empty rows return only the escaped header
without a trailing newline, while preserving the existing header-and-body format
for non-empty rows.
src/commands/contacts.ts-48-49 (1)

48-49: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

--limit isn't validated, so a non-numeric value silently returns zero contacts.

parseInt('abc')NaN, and slice(0, NaN) yields [] with count: 0 and no error. posters list/posters search already reject this explicitly; worth matching here.

🛠️ Proposed fix
-        const limit = opts['limit'] as number;
+        const limit = opts['limit'] as number;
+        if (!Number.isInteger(limit) || limit < 1) {
+          jsonError('--limit must be a positive integer', 3, 'validation_error');
+          return;
+        }
         contactList = contactList.slice(0, limit);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/contacts.ts` around lines 48 - 49, Validate the `opts['limit']`
value before applying `contactList.slice` in the contacts command, rejecting
non-numeric values with the same error behavior used by `posters list` and
`posters search`. Only call `slice` after validation succeeds, preserving the
existing limit behavior for valid values.
src/commands/events.ts-229-260 (1)

229-260: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Make the series flags real events create options.

events create has no declared --repeat/--count option, and template merging ignores them (mergeTemplateOpts only handles TEMPLATE_FIELDS). The only registration is inside registerBulkCommands, which merely touches the already-registered command rather than exposing these flags. Add them to events create so users can actually request series creation (or remove the unreachable support).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/events.ts` around lines 229 - 260, The series logic in the
events create flow is unreachable because repeat and count are not declared
options. Update the events create command registration to expose --repeat and
--count, and ensure template option merging preserves them alongside
TEMPLATE_FIELDS; keep the existing series handling and validation behavior
unchanged.
src/commands/guests.ts-79-93 (1)

79-93: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

--dry-run issues a live /getEventInfo request before the dry-run guard.

The metadata fetch at line 80 runs unconditionally, so guests list --dry-run isn't the offline preview the global flag advertises. Move the dry-run branch above the fetch.

🛠️ Proposed fix
+        if (globalOpts['dryRun']) {
+          jsonOutput({ dryRun: true, eventId, collection: `events/${eventId}/guests` });
+          return;
+        }
+
         let counts: Record<string, number> = {};
         let eventTitle = 'Unknown Event';
         try {
@@
         } catch {
           // API may be down, continue with Firestore guest fetch
         }
-
-        if (globalOpts['dryRun']) {
-          jsonOutput({ dryRun: true, eventId, collection: `events/${eventId}/guests` });
-          return;
-        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/guests.ts` around lines 79 - 93, The dry-run guard currently
executes after the live /getEventInfo request. In the guests command flow, move
the globalOpts['dryRun'] branch before the try block that calls apiRequest,
preserving its existing jsonOutput response and return so dry-run exits without
any network request.
src/helpers/share.ts-5-16 (1)

5-16: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

+share drops --verbose support (hardcoded false).

Every sibling command in this PR threads cmd.optsWithGlobals<Record<string, unknown>>()['verbose'] into its apiRequest calls. Here the callback parameters were renamed to _opts/_cmd and the verbose flag is hardcoded to false, so partiful +share <id> -v silently loses the request-detail stderr output that every other command still provides.

🔧 Restore verbose threading
-    .action(async (eventId: string, _opts: Record<string, unknown>, _cmd: Command) => {
+    .action(async (eventId: string, _opts: Record<string, unknown>, cmd: Command) => {
+      const globalOpts = cmd.optsWithGlobals<Record<string, unknown>>();
       try {
         const config = loadConfig();
         const token = await getValidToken(config);
@@
-        const result = await apiRequest('POST', '/getEventInfo', token, payload, false) as Record<string, unknown>;
+        const result = await apiRequest('POST', '/getEventInfo', token, payload, globalOpts['verbose'] as boolean | undefined) as Record<string, unknown>;

Also applies to: 30-41

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/helpers/share.ts` around lines 5 - 16, Restore verbose option propagation
in registerShareHelper: use the action’s Command parameter to read
optsWithGlobals<Record<string, unknown>>() and pass its verbose value to each
apiRequest call instead of hardcoding false. Preserve the existing share-link
behavior and update the callback parameter names as needed to access the
command.
🧹 Nitpick comments (13)
.wayfinder/BUILD-PROMPT.md (1)

4-4: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Update the migration baseline or mark it as historical.

Line 4 still describes the repository as plain JavaScript with no TypeScript, which conflicts with the current TypeScript strict-mode port and tsx runtime. Future agents may follow the obsolete setup instructions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.wayfinder/BUILD-PROMPT.md at line 4, Update the repository setup
description in BUILD-PROMPT.md to reflect the current TypeScript strict-mode
port and tsx runtime instead of describing the project as plain JavaScript with
no build step. Preserve the remaining Commander.js, command organization, API
access, testing, and global-install guidance unless those details are also
outdated.
.wayfinder/map.md (1)

17-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Refresh the repository/runtime description for the TypeScript port.

This map still says the project is plain JavaScript with no build step. Update it to describe the TypeScript source, tsx ESM execution, and tsc --noEmit, or explicitly label these lines as historical context.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.wayfinder/map.md around lines 17 - 20, Update the repository/runtime
description in the map to reflect the TypeScript port: identify TypeScript as
the source language, document tsx-based ESM execution, and mention tsc --noEmit
validation. Alternatively, explicitly mark the existing
Commander.js/plain-JavaScript/no-build-step statements as historical context
while preserving accurate current guidance about API access and global
installation.
package.json (1)

13-13: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Avoid registering tsx twice in the start path.

package.json preloads tsx via node --import tsx, while bin/partiful also calls register() from tsx/esm/api. Keep one loader mechanism—for example, use node bin/partiful here and retain the bin-level registration—to avoid duplicate loader setup. Official tsx documentation presents these as alternative entrypoint-registration approaches; applying both here is an inferred conflict risk. (tsx.is)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 13, Update the package.json start script to remove the
node --import tsx preload and invoke bin/partiful directly, retaining the
existing tsx/esm/api register() call in the bin entrypoint as the sole loader
mechanism.
src/lib/api/endpoints.ts (2)

331-452: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: derive the Firestore paths from a shared project constant.

getpartiful is hardcoded in three paths here and again as FIRESTORE_PROJECT in src/lib/http.ts:12; the two can silently diverge.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/api/endpoints.ts` around lines 331 - 452, The Firestore project
identifier is duplicated across the endpoint definitions and the
FIRESTORE_PROJECT constant. Update firestoreGetEvent, firestorePatchEvent, and
firestoreListDocuments to derive their paths from the shared FIRESTORE_PROJECT
value, preserving the existing endpoint paths while preventing future
divergence.

115-124: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer the Zod 4 object constructors over the legacy .passthrough() chain.

On zod 4.4.3, .passthrough() is deprecated in favor of z.looseObject() / .loose(), and the legacy methods remain only for backwards compatibility. Same applies to z.ZodTypeAny at Line 323 and Line 480 — the need for z.ZodTypeAny has been eliminated; just use z.ZodType. Purely idiomatic, no behavior change, but worth doing once now that every schema in this file is authored in one place.

♻️ Illustrative change (apply file-wide)
-export const CreateEventResponseSchema = z
-  .object({
-    id: z.string(),
-    title: z.string().optional(),
-    status: z.string().optional(),
-    startDate: z.string().optional(),
-  })
-  .passthrough();
+export const CreateEventResponseSchema = z.looseObject({
+  id: z.string(),
+  title: z.string().optional(),
+  status: z.string().optional(),
+  startDate: z.string().optional(),
+});
-function fieldsOf(schema: z.ZodTypeAny): readonly string[] {
+function fieldsOf(schema: z.ZodType): readonly string[] {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/api/endpoints.ts` around lines 115 - 124, Update
CreateEventResponseSchema and the other schemas in this file to use Zod 4
constructors: replace legacy .passthrough() usage with z.looseObject() or
.loose() while preserving unknown-key behavior, and replace z.ZodTypeAny
references near the identified locations with z.ZodType. Apply these idiomatic
updates consistently across the file without changing schema behavior.
src/lib/auth.ts (1)

62-72: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Encode the form body/query and bound the refresh call with a timeout.

refresh_token and apiKey are interpolated raw into the body and query string; URLSearchParams removes the encoding hazard. This is also the only outbound call in the auth path with no abort timeout (src/lib/upload.ts uses one), so a stalled token endpoint hangs the CLI indefinitely.

♻️ Proposed change
-  const postData = `grant_type=refresh_token&refresh_token=${config.refreshToken}`;
+  const postData = new URLSearchParams({
+    grant_type: 'refresh_token',
+    refresh_token: config.refreshToken ?? '',
+  }).toString();
 
-  const resp = await fetch(`https://${GOOGLE_TOKEN_URL}/v1/token?key=${config.apiKey}`, {
+  const url = `https://${GOOGLE_TOKEN_URL}/v1/token?key=${encodeURIComponent(config.apiKey ?? '')}`;
+  const resp = await fetch(url, {
     method: 'POST',
     headers: {
       'Content-Type': 'application/x-www-form-urlencoded',
       Referer: 'https://partiful.com/',
     },
     body: postData,
+    signal: AbortSignal.timeout(15000),
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/auth.ts` around lines 62 - 72, Update refreshAccessToken to build the
token request body and API-key query parameters with URLSearchParams rather than
raw interpolation, and add an AbortController-based timeout to the fetch
matching the existing upload timeout pattern. Pass the controller’s signal to
fetch and ensure the timeout is cleared after completion.
src/lib/http.ts (1)

121-124: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Unguarded JSON.parse leaks a raw SyntaxError past the error classifier.

A 200 response with an HTML body (proxy/captive portal) escapes as SyntaxError: Unexpected token '<' instead of a classified ApiError, bypassing the CLI's structured error envelope. src/lib/upload.ts:106-111 already handles this case; the two Firestore readers below have the same gap.

♻️ Proposed change
   const text = await resp.text();
-  const parsed = text ? JSON.parse(text) : {};
+  let parsed: unknown = {};
+  if (text) {
+    try {
+      parsed = JSON.parse(text);
+    } catch {
+      throw classifyError(resp.status, `API ${method} ${endpoint} returned non-JSON body`, text.slice(0, 200));
+    }
+  }
   checkDrift(endpoint, parsed);
   return parsed;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/http.ts` around lines 121 - 124, Update the response parsing flow
around checkDrift in the HTTP reader to catch JSON.parse failures and convert
them through the existing ApiError classification path, matching the handling
already used in upload.ts. Apply the same protection to both Firestore readers,
while preserving successful JSON parsing and the existing empty-body default.
src/lib/drift.ts (1)

35-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

One Zod key-enumeration helper implemented twice, with different capabilities. knownKeys unwraps array elements; fieldsOf does not (despite its comment claiming it does), and the registry only works because it passes element schemas by hand. Two copies of schema introspection will drift apart the next time Zod internals change.

  • src/lib/drift.ts#L35-L43: keep this implementation as the single exported helper (e.g. schemaKeys(schema): readonly string[]) and drop the unused _def/type/innerType members from the local cast.
  • src/lib/api/endpoints.ts#L323-L329: delete fieldsOf and call the shared helper, which also makes its "arrays expose their element's keys" comment true.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/drift.ts` around lines 35 - 43, The schema key-enumeration logic is
duplicated and inconsistent. In src/lib/drift.ts:35-43, retain and export the
array-aware knownKeys implementation as the shared schemaKeys helper,
simplifying its local cast to only the properties it reads; in
src/lib/api/endpoints.ts:323-329, remove fieldsOf and replace its usage with the
shared helper so array schemas use their element keys.
src/helpers/export.ts (1)

29-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the callable payload builder now that the envelope is typed.

This exact block is repeated in src/helpers/clone.ts, src/helpers/share.ts, src/commands/guests.ts, src/commands/cohosts.ts, src/commands/bulk.ts and src/commands/doctor.ts, plus three private makePayload copies in src/commands/events.ts, src/commands/rsvp.ts and src/commands/bulk.ts. A single helper returning CallableEnvelope<P> from src/lib/api/envelope.ts would make the envelope contract enforced rather than re-typed per call site.

♻️ Sketch

Add to src/lib/auth.ts (or a new src/lib/api/payload.ts):

export function makeCallablePayload<P>(config: PartifulConfig, params: P): CallableEnvelope<P> {
  return {
    data: {
      params,
      amplitudeDeviceId: config.amplitudeDeviceId || generateAmplitudeDeviceId(),
      amplitudeSessionId: Date.now(),
      userId: config.userId ?? null,
    },
  };
}

Then here:

-        const payload = {
-          data: wrapPayload(config, {
-            params: { eventId },
-            amplitudeSessionId: Date.now(),
-            userId: config.userId,
-          }),
-        };
+        const payload = makeCallablePayload(config, { eventId });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/helpers/export.ts` around lines 29 - 35, Extract the repeated callable
envelope construction into a shared makeCallablePayload helper returning
CallableEnvelope<P>, using config for amplitudeDeviceId generation,
amplitudeSessionId, and nullable userId. Replace the inline payload block in the
current export flow and the duplicated builders/call sites in clone, share,
guests, cohosts, bulk, doctor, events, and rsvp with this helper, preserving
each call site’s params.
tests/drift.test.js (1)

11-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case that goes through the callable-envelope unwrap.

Every case here passes an already-unwrapped payload, so the suite cannot catch the nesting mismatch between the declared schemas and real responses (see the consolidated comment). A test feeding { result: { data: { event: {…} } } } for getEventInfo — or exporting checkDrift from src/lib/http.ts and asserting on it — would pin that contract down.

Want me to draft those cases?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/drift.test.js` around lines 11 - 45, Add a drift-detection test that
passes the callable envelope { result: { data: { event: ... } } } to the
getEventInfo path and asserts fields are evaluated after unwrapping against the
declared event schema. Use the existing detectDrift helper unless checkDrift
from src/lib/http.ts is required to exercise the real response path, and verify
an unexpected nested field is reported while valid event fields are not.
src/commands/rsvp.ts (1)

231-236: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

program is unused in this signature.

attachRsvpVerbs only needs events/explore; dropping the first parameter (or prefixing _program) makes the contract honest for the cli.ts call site.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/rsvp.ts` around lines 231 - 236, Remove the unused program
parameter from registerRsvpCommands, updating its signature to accept only the
events and explore commands. Keep both attachRsvpVerbs calls unchanged and
update the cli.ts call site to match the revised contract.
src/lib/errors.ts (1)

11-31: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider setting name so subclasses are distinguishable in stack traces/logs.

All subclasses currently serialize/print as Error: ... since name is never assigned; type covers the JSON envelope but not raw stack output.

♻️ Optional tweak
   constructor(message: string, exitCode: number, type: string, details: unknown = null) {
     super(message);
+    this.name = new.target.name;
     this.exitCode = exitCode;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/errors.ts` around lines 11 - 31, Set the Error name within the
PartifulError constructor so instances and subclasses identify themselves by
their class name in stack traces and logs, while preserving the existing type
and JSON serialization behavior.
src/commands/events.ts (1)

496-527: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

--dry-run still triggers the interactive confirmation and a live /getEventInfo call.

The dry-run branch is evaluated only after the confirm gate, so previewing a cancel prompts the user and hits the network. Hoisting the dry-run check above the confirmation keeps previews fully non-interactive, matching events create/update.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/events.ts` around lines 496 - 527, Move the dryRun check in the
event cancellation action above the confirmation block and its `/getEventInfo`
request, while retaining the existing payload construction and JSON preview
output. Ensure `--dry-run` returns immediately without prompting or making any
live API calls; preserve confirmation and cancellation behavior for normal
execution.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.wayfinder/tickets/05-implement.md:
- Around line 15-19: Synchronize the migration documentation with the TypeScript
port: in .wayfinder/tickets/05-implement.md lines 15-19, change proposed .js
implementation paths to .ts; in .wayfinder/BUILD-PROMPT.md line 4, update or
mark the plain-JavaScript baseline as historical; in .wayfinder/map.md lines
17-20, document TypeScript and the tsx runtime; and in
docs/explore-command-design.md lines 170-172, update the documented source
layout to TypeScript. Ensure the documentation consistently reflects that src/
contains no .js files.

In `@docs/explore-command-design.md`:
- Around line 127-135: Remove guestId from the documented public JSON success
response for the explore rsvp contract, leaving the remaining fields unchanged.
Ensure the user-facing output does not expose Partiful user IDs, either by
omitting guestId or explicitly redacting its value.
- Around line 70-71: Synchronize the questionnaire contract across all
referenced documentation: in docs/explore-command-design.md lines 70-71, remove
questionnaire events from the unsupported refusal list; at lines 167-169,
replace the stale refusal guard with the verified questionnaire discovery,
answer-mapping, and questionnaireResponse validation path; and in
.wayfinder/BUILD-PROMPT.md lines 24-25, replace the obsolete open-recon blocker
with the resolved questionnaire contract.

In `@docs/TYPESCRIPT-PORT-GUIDE.md`:
- Around line 184-186: Update the create-event example around apiRequest and
CreateEventResponseSchema.parse so the Promise<unknown> response is
endpoint-aware typed or explicitly narrowed/validated before accessing
result.data. Preserve the existing fallback to an empty object and schema
parsing, while ensuring the example remains valid under strict TypeScript
settings.
- Around line 170-178: Update the createEventEndpoint metadata example to use
the EndpointMeta-compatible responseFields property instead of responseSchema,
matching the field consumed by schema api.createEvent and the existing endpoint
metadata implementation.

In `@package.json`:
- Line 45: Move the tsx package from devDependencies to dependencies in
package.json, preserving its existing version constraint so the bin/partiful
runtime import of tsx/esm/api remains available in production installs.

In `@src/helpers/watch.ts`:
- Around line 25-27: Validate the parsed interval and duration values in the
watch setup around intervalMs, durationMs, and endTime, using parseInt with an
explicit radix. Reject non-numeric or non-positive inputs before starting the
polling loop so invalid options cannot produce immediate retries or a misleading
completion; preserve normal polling and duration behavior for valid values.

In `@src/lib/auth.ts`:
- Around line 74-79: Validate the refresh response in getValidToken before using
or persisting it: require resp.ok and a present id_token, while preserving the
existing API-error message when available. Throw a clear refresh failure for
invalid responses, and only call saveConfig after validation so
config.accessToken is guaranteed; then remove the non-null assertions from the
returned token path.

In `@src/lib/dates.ts`:
- Around line 11-18: Update parseDateTime to interpret the supplied timezone
when converting dateStr into a Date, ensuring identical wall-clock input
produces the correct instant on hosts in other local timezones; alternatively,
remove the timezone parameter and all callers’ timezone forwarding, then make
the CLI/API contract explicitly local-timezone-only. Keep event.timezone
handling consistent with the chosen contract.

In `@src/lib/http.ts`:
- Around line 26-36: Align drift detection with the declared response shapes: in
src/lib/http.ts lines 26-36, update checkDrift to use per-method unwrapping
metadata instead of always inspecting result.data; in src/lib/api/endpoints.ts
lines 140-154, model getEventInfo at the event envelope depth or configure its
unwrap field; in src/lib/api/endpoints.ts lines 199-215, define home-page
responses as objects containing upcomingEvents or pastEvents arrays and ensure
responseSchemas at lines 471-472 references those object schemas.
- Around line 100-114: Update the fetch calls in apiRequest, firestoreRequest,
and firestoreListDocuments to include a request timeout via
AbortSignal.timeout(...). Use the same timeout signal consistently across all
three helpers so stalled requests abort and remain retryable through withRetry.
- Around line 38-91: Validate MAX_RETRIES in the retry configuration so invalid,
negative, or non-finite PARTIFUL_MAX_RETRIES values fall back to a non-negative
default and always allow the initial request in withRetry. Update Retry-After
handling in withRetry to distinguish valid numeric delays from HTTP-date values,
convert dates to a bounded millisecond delay, and use the existing exponential
backoff when the header is invalid or already expired.

---

Minor comments:
In @.wayfinder/ts-port/tickets/T0-rsvp-merged-branch-cut.md:
- Line 3: Synchronize ticket metadata with the closed statuses in
.wayfinder/ts-port/map.md: update
.wayfinder/ts-port/tickets/T0-rsvp-merged-branch-cut.md:3 to mark T0 closed and
add its merge/branch-cut commit; update
.wayfinder/ts-port/tickets/T3-port-lib-layer-spec.md:3 and
.wayfinder/ts-port/tickets/T4-port-commands-helpers.md:3 to mark them closed and
record their completed work; update
.wayfinder/ts-port/tickets/T5-rewire-schema-command.md:3 and
.wayfinder/ts-port/tickets/T6-drift-detection-smoke-tests.md:3 from OPEN to the
closed status used by their Answer sections.

In `@docs/explore-command-design.md`:
- Around line 119-123: Update the tags example to use the API-verified wire IDs
MUSIC and FOOD for the Music and Food & Drink entries, while preserving the
existing display names and DISCOVER_HOME entry.

In `@src/commands/contacts.ts`:
- Around line 48-49: Validate the `opts['limit']` value before applying
`contactList.slice` in the contacts command, rejecting non-numeric values with
the same error behavior used by `posters list` and `posters search`. Only call
`slice` after validation succeeds, preserving the existing limit behavior for
valid values.

In `@src/commands/doctor.ts`:
- Around line 39-40: Update the displayPath construction near the home and
configPath values to replace HOME with "~" only when HOME is non-empty and
configPath actually starts with it; otherwise preserve configPath unchanged. Use
a prefix check rather than String.replace to prevent unset HOME and mid-path
substitutions.

In `@src/commands/events.ts`:
- Around line 37-40: Replace the unsafe error type assertions with an Error
runtime check and String fallback so every catch path preserves a message for
non-Error throws. Update handleError in src/commands/events.ts (lines 37-40),
both guests catch blocks in src/commands/guests.ts (lines 123-125 and 181-183),
handleError in src/commands/rsvp.ts (lines 40-43), and the +watch catch block in
src/helpers/watch.ts (lines 87-89); each site requires the same guarded message
handling.
- Around line 229-260: The series logic in the events create flow is unreachable
because repeat and count are not declared options. Update the events create
command registration to expose --repeat and --count, and ensure template option
merging preserves them alongside TEMPLATE_FIELDS; keep the existing series
handling and validation behavior unchanged.

In `@src/commands/guests.ts`:
- Around line 79-93: The dry-run guard currently executes after the live
/getEventInfo request. In the guests command flow, move the globalOpts['dryRun']
branch before the try block that calls apiRequest, preserving its existing
jsonOutput response and return so dry-run exits without any network request.

In `@src/helpers/share.ts`:
- Around line 5-16: Restore verbose option propagation in registerShareHelper:
use the action’s Command parameter to read optsWithGlobals<Record<string,
unknown>>() and pass its verbose value to each apiRequest call instead of
hardcoding false. Preserve the existing share-link behavior and update the
callback parameter names as needed to access the command.

In `@src/lib/auth.ts`:
- Around line 33-38: Update resolveCredentialsPath to use os.homedir() instead
of process.env.HOME as string when constructing the default credentials path,
importing the required os module while preserving the PARTIFUL_CREDENTIALS_FILE
override.

In `@src/lib/drift.ts`:
- Around line 95-108: Update the logging destination condition in the drift
logging flow around PARTIFUL_DRIFT_LOG so force only enables logging and does
not override a configured file sink. When force is true with a non-empty,
non-special sink path, append the serialized line to that file; reserve the
stderr branch for unset or explicitly stderr-configured sinks, while preserving
existing write-failure handling.

In `@src/lib/output.ts`:
- Around line 55-65: Update formatCsv’s escape helper to quote values containing
carriage returns in addition to commas, quotes, and line feeds. Adjust the
output assembly so empty rows return only the escaped header without a trailing
newline, while preserving the existing header-and-body format for non-empty
rows.

---

Nitpick comments:
In @.wayfinder/BUILD-PROMPT.md:
- Line 4: Update the repository setup description in BUILD-PROMPT.md to reflect
the current TypeScript strict-mode port and tsx runtime instead of describing
the project as plain JavaScript with no build step. Preserve the remaining
Commander.js, command organization, API access, testing, and global-install
guidance unless those details are also outdated.

In @.wayfinder/map.md:
- Around line 17-20: Update the repository/runtime description in the map to
reflect the TypeScript port: identify TypeScript as the source language,
document tsx-based ESM execution, and mention tsc --noEmit validation.
Alternatively, explicitly mark the existing
Commander.js/plain-JavaScript/no-build-step statements as historical context
while preserving accurate current guidance about API access and global
installation.

In `@package.json`:
- Line 13: Update the package.json start script to remove the node --import tsx
preload and invoke bin/partiful directly, retaining the existing tsx/esm/api
register() call in the bin entrypoint as the sole loader mechanism.

In `@src/commands/events.ts`:
- Around line 496-527: Move the dryRun check in the event cancellation action
above the confirmation block and its `/getEventInfo` request, while retaining
the existing payload construction and JSON preview output. Ensure `--dry-run`
returns immediately without prompting or making any live API calls; preserve
confirmation and cancellation behavior for normal execution.

In `@src/commands/rsvp.ts`:
- Around line 231-236: Remove the unused program parameter from
registerRsvpCommands, updating its signature to accept only the events and
explore commands. Keep both attachRsvpVerbs calls unchanged and update the
cli.ts call site to match the revised contract.

In `@src/helpers/export.ts`:
- Around line 29-35: Extract the repeated callable envelope construction into a
shared makeCallablePayload helper returning CallableEnvelope<P>, using config
for amplitudeDeviceId generation, amplitudeSessionId, and nullable userId.
Replace the inline payload block in the current export flow and the duplicated
builders/call sites in clone, share, guests, cohosts, bulk, doctor, events, and
rsvp with this helper, preserving each call site’s params.

In `@src/lib/api/endpoints.ts`:
- Around line 331-452: The Firestore project identifier is duplicated across the
endpoint definitions and the FIRESTORE_PROJECT constant. Update
firestoreGetEvent, firestorePatchEvent, and firestoreListDocuments to derive
their paths from the shared FIRESTORE_PROJECT value, preserving the existing
endpoint paths while preventing future divergence.
- Around line 115-124: Update CreateEventResponseSchema and the other schemas in
this file to use Zod 4 constructors: replace legacy .passthrough() usage with
z.looseObject() or .loose() while preserving unknown-key behavior, and replace
z.ZodTypeAny references near the identified locations with z.ZodType. Apply
these idiomatic updates consistently across the file without changing schema
behavior.

In `@src/lib/auth.ts`:
- Around line 62-72: Update refreshAccessToken to build the token request body
and API-key query parameters with URLSearchParams rather than raw interpolation,
and add an AbortController-based timeout to the fetch matching the existing
upload timeout pattern. Pass the controller’s signal to fetch and ensure the
timeout is cleared after completion.

In `@src/lib/drift.ts`:
- Around line 35-43: The schema key-enumeration logic is duplicated and
inconsistent. In src/lib/drift.ts:35-43, retain and export the array-aware
knownKeys implementation as the shared schemaKeys helper, simplifying its local
cast to only the properties it reads; in src/lib/api/endpoints.ts:323-329,
remove fieldsOf and replace its usage with the shared helper so array schemas
use their element keys.

In `@src/lib/errors.ts`:
- Around line 11-31: Set the Error name within the PartifulError constructor so
instances and subclasses identify themselves by their class name in stack traces
and logs, while preserving the existing type and JSON serialization behavior.

In `@src/lib/http.ts`:
- Around line 121-124: Update the response parsing flow around checkDrift in the
HTTP reader to catch JSON.parse failures and convert them through the existing
ApiError classification path, matching the handling already used in upload.ts.
Apply the same protection to both Firestore readers, while preserving successful
JSON parsing and the existing empty-body default.

In `@tests/drift.test.js`:
- Around line 11-45: Add a drift-detection test that passes the callable
envelope { result: { data: { event: ... } } } to the getEventInfo path and
asserts fields are evaluated after unwrapping against the declared event schema.
Use the existing detectDrift helper unless checkDrift from src/lib/http.ts is
required to exercise the real response path, and verify an unexpected nested
field is reported while valid event fields are not.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dc7df35b-375b-4e28-a653-2241adf9c386

📥 Commits

Reviewing files that changed from the base of the PR and between bd2fdc4 and 9320471.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (63)
  • .wayfinder/BUILD-PROMPT.md
  • .wayfinder/map.md
  • .wayfinder/tickets/01-build-id-recon.md
  • .wayfinder/tickets/02-rsvp-endpoint-recon.md
  • .wayfinder/tickets/03-tag-filter-recon.md
  • .wayfinder/tickets/04-command-shape.md
  • .wayfinder/tickets/05-implement.md
  • .wayfinder/tickets/06-docs.md
  • .wayfinder/tickets/07-rsvp-going-live-capture.md
  • .wayfinder/ts-port/BUILD-PROMPT.md
  • .wayfinder/ts-port/map.md
  • .wayfinder/ts-port/tickets/T0-rsvp-merged-branch-cut.md
  • .wayfinder/ts-port/tickets/T1-toolchain-setup.md
  • .wayfinder/ts-port/tickets/T2-porting-convention-doc.md
  • .wayfinder/ts-port/tickets/T3-port-lib-layer-spec.md
  • .wayfinder/ts-port/tickets/T4-port-commands-helpers.md
  • .wayfinder/ts-port/tickets/T5-rewire-schema-command.md
  • .wayfinder/ts-port/tickets/T6-drift-detection-smoke-tests.md
  • bin/partiful
  • docs/TYPESCRIPT-PORT-GUIDE.md
  • docs/explore-command-design.md
  • package.json
  • src/cli.ts
  • src/commands/auth.ts
  • src/commands/blasts.ts
  • src/commands/bulk.ts
  • src/commands/cohosts.ts
  • src/commands/contacts.ts
  • src/commands/doctor.ts
  • src/commands/events.ts
  • src/commands/guests.ts
  • src/commands/posters.ts
  • src/commands/rsvp.ts
  • src/commands/schema.ts
  • src/commands/setup.ts
  • src/commands/templates.ts
  • src/helpers/clone.ts
  • src/helpers/export.js
  • src/helpers/export.ts
  • src/helpers/share.ts
  • src/helpers/watch.ts
  • src/lib/api/endpoints.ts
  • src/lib/api/envelope.ts
  • src/lib/auth.ts
  • src/lib/cohosts.js
  • src/lib/cohosts.ts
  • src/lib/dates.ts
  • src/lib/drift.ts
  • src/lib/errors.js
  • src/lib/errors.ts
  • src/lib/events.ts
  • src/lib/http.js
  • src/lib/http.ts
  • src/lib/output.js
  • src/lib/output.ts
  • src/lib/posters.ts
  • src/lib/rsvp.ts
  • src/lib/templates.ts
  • src/lib/upload.ts
  • tests/drift.test.js
  • tests/schema-api.test.js
  • tests/smoke-real-api.test.js
  • tsconfig.json
💤 Files with no reviewable changes (5)
  • src/lib/errors.js
  • src/lib/cohosts.js
  • src/lib/http.js
  • src/helpers/export.js
  • src/lib/output.js

Comment thread .wayfinder/tickets/05-implement.md
Comment thread docs/explore-command-design.md
Comment thread docs/explore-command-design.md
Comment thread docs/TYPESCRIPT-PORT-GUIDE.md
Comment thread docs/TYPESCRIPT-PORT-GUIDE.md
Comment thread src/lib/auth.ts
Comment thread src/lib/dates.ts
Comment thread src/lib/http.ts
Comment thread src/lib/http.ts
Comment thread src/lib/http.ts
CodeRabbit flagged that checkDrift unwrapped uniformly to result.data, but
getEventInfo nests at result.data.event and the homepage lists at
result.data.{upcomingEvents,pastEvents} — so those methods reported drift on
every call and schema api.<method> advertised the wrong depth. Add a
PAYLOAD_UNWRAP map + unwrapPayload() in drift.ts and descend before diffing.
Also correct the knownKeys() doc comment (Copilot): it resolves array
element schemas only, not effects/optionals. +2 drift tests. 208 green.
@KalebCole

Copy link
Copy Markdown
Owner Author

Thanks @coderabbitai / Copilot. Triage:

Fixed in this PR (404c024) — port-introduced:

  • src/lib/http.ts drift-nesting: checkDrift unwrapped uniformly to result.data, but getEventInfo nests at .data.event and the homepage lists at .data.{upcomingEvents,pastEvents}. Added a per-method PAYLOAD_UNWRAP map + unwrapPayload() in drift.ts and descend before diffing. +2 tests proving those methods no longer false-positive.
  • src/lib/drift.ts knownKeys() doc comment corrected (it resolves array element schemas only, not effects/optionals). Thanks @copilot.

Deferred to #67 — these are pre-existing in the JS baseline (ad7be30), not introduced here. This PR is an annotation-only faithful port (zero runtime behavior change by design), so fixing them belongs in its own change:

  • http.ts PARTIFUL_MAX_RETRIES NaN wedge, Retry-After HTTP-date NaN, missing fetch timeouts
  • watch.ts --interval/--duration NaN validation
  • auth.ts refresh-response trust (resp.ok/id_token)

All five are real and worth fixing — tracked in #67 with a recommended shared-helper pattern.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- recent commits ---'
git log --oneline -8

printf '%s\n' '--- changed files in claimed fix ---'
git show --stat --oneline 404c024
git show --format= --name-status 404c024

printf '%s\n' '--- src/lib/drift.ts outline ---'
ast-grep outline src/lib/drift.ts --items all --view expanded || true

printf '%s\n' '--- src/lib/http.ts outline ---'
ast-grep outline src/lib/http.ts --items all --view expanded || true

printf '%s\n' '--- drift implementation ---'
cat -n src/lib/drift.ts
printf '%s\n' '--- HTTP drift call site ---'
rg -n -C 8 'checkDrift|reportDrift|apiRequest' src/lib/http.ts

printf '%s\n' '--- drift tests ---'
cat -n tests/drift.test.js

printf '%s\n' '--- baseline comparison for deferred files ---'
git diff --stat ad7be30..HEAD -- src/lib/http.ts src/helpers/watch.ts src/lib/auth.ts
git diff --unified=3 ad7be30..HEAD -- src/lib/http.ts src/helpers/watch.ts src/lib/auth.ts

printf '%s\n' '--- GitHub issue `#67` ---'
gh issue view 67 --repo KalebCole/partiful-cli --json number,title,state,body,url,assignees

Length of output: 37082


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- endpoint specification outline ---'
ast-grep outline src/lib/api/endpoints.ts --items all --view expanded || true

printf '%s\n' '--- endpoint response schemas and API registry ---'
rg -n -C 5 \
  'GetEventInfoResponseSchema|GetMyUpcomingEventsResponseSchema|GetMyPastEventsResponseSchema|GetContactsResponseSchema|responseSchemas|apiEndpoints' \
  src/lib/api/endpoints.ts

printf '%s\n' '--- callable response consumers (current TypeScript) ---'
rg -n -C 4 \
  'getEventInfo|getMyUpcomingEventsForHomePage|getMyPastEventsForHomePage|getContacts' \
  src tests -g '*.ts' -g '*.js'

printf '%s\n' '--- relevant baseline JavaScript behavior ---'
for f in src/lib/http.js src/helpers/watch.js src/lib/auth.js; do
  echo "### ad7be30:$f"
  git show "ad7be30:$f" 2>/dev/null || echo "NOT PRESENT"
done

printf '%s\n' '--- exact port-vs-baseline checks for deferred behavior ---'
git show ad7be30:src/lib/http.js | rg -n -C 4 \
  'PARTIFUL_MAX_RETRIES|retry-after|parseFloat|fetch\(|setTimeout'
git show ad7be30:src/helpers/watch.js | rg -n -C 4 \
  'interval|duration|parseInt|setTimeout'
git show ad7be30:src/lib/auth.js | rg -n -C 5 \
  'refreshAccessToken|resp\.ok|id_token|resp\.json'

printf '%s\n' '--- commit patch for drift fix ---'
git show --format=fuller --find-renames 404c024 -- src/lib/drift.ts src/lib/http.ts tests/drift.test.js

Length of output: 66545


@KalebCole Re-review complete — no new findings in the triaged areas.

  • The unwrapPayload() routing matches the response shapes consumed by the CLI: getEventInfo → result.data.event, upcoming events → result.data.upcomingEvents, and past events → result.data.pastEvents.
  • The corresponding endpoint schemas describe the inner event records, so drift detection now compares like-for-like.
  • The regression coverage exercises all three nested paths and confirms the getEventInfo false-positive case is eliminated.
  • I confirmed the five deferred items existed in the JavaScript baseline (ad7be30). #67 is open and contains actionable scope for addressing them separately.

🐇 ✅


✏️ Learnings added
Learnt from: KalebCole
URL: https://github.com/KalebCole/partiful-cli/pull/66

Timestamp: 2026-07-24T22:48:35.535Z
Learning: In the partiful-cli TypeScript port PR `#66`, preserving JavaScript baseline runtime behavior is an explicit constraint. The `PARTIFUL_MAX_RETRIES` NaN handling, Retry-After HTTP-date handling, missing HTTP timeouts, `+watch` interval/duration validation, and token-refresh response validation are pre-existing JavaScript-baseline behaviors and are tracked separately in GitHub issue `#67`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@KalebCole

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 40 minutes.

Empty commit to force CodeRabbit + Copilot to re-review the fix commit;
their prior reviews predated 404c024. No code change. Squash-merge drops this.
The CLI has no build step — bin/partiful registers the tsx ESM loader at
runtime (import 'tsx/esm/api') to run the .ts source graph directly. tsx is
therefore load-bearing at runtime, not just for tests. As a devDependency an
'npm install --omit=dev' / global prod install shipped a CLI that could not
start. Verified: prod-only install now resolves tsx/esm/api and
./bin/partiful --version prints 2.1.0. Flagged by CodeRabbit on #66.
# Conflicts:
#	.wayfinder/ts-port/map.md
#	.wayfinder/ts-port/tickets/T1-toolchain-setup.md
#	.wayfinder/ts-port/tickets/T2-porting-convention-doc.md
#	.wayfinder/ts-port/tickets/T5-rewire-schema-command.md
#	.wayfinder/ts-port/tickets/T6-drift-detection-smoke-tests.md

This comment was marked as outdated.

Copilot's fresh review surfaced a suppressed but valid port regression: +share
stopped forwarding the global --verbose flag to apiRequest. Restore the
baseline cmd.optsWithGlobals() behavior. Also restore JS's || fallback for an
empty event title (the TS port had changed it to ??). Add 2 parity tests.

210 passed / 6 smoke skipped; tsc clean.

This comment was marked as outdated.

Copilot noted the test comment was stale: GetEventInfoResponseSchema now
declares event fields. The assertion was already correct; update the rationale.

210 passed / 6 smoke skipped; tsc clean.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/helpers/share.ts (1)

46-48: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle non-Error thrown values safely.

The assertion at Line [48] only satisfies TypeScript. If an upstream path throws a string or null, the error handler itself throws while reading .message.

Proposed fix
-        else jsonError((e as Error).message);
+        else jsonError(e instanceof Error ? e.message : String(e));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/helpers/share.ts` around lines 46 - 48, Update the catch handler in the
share helper to safely handle thrown values that are not Error instances,
including null, without accessing .message unsafely. Preserve the existing
PartifulError handling and provide a safe fallback message for other thrown
values when calling jsonError.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/helpers/share.ts`:
- Around line 37-42: Validate the unknown response from apiRequest in the
/getEventInfo flow with GetEventInfoResponseSchema before extracting title or
calling jsonOutput. Replace the unchecked result/data/event/title assertions
with the parsed schema output, and route schema parse failures through the
existing error path while preserving the { url, eventId, title } output.

In `@tests/share-helper.test.js`:
- Line 6: Update the jsonError mock in the test setup to throw or return the
required structured error shape with status and nested error code, type, and
message fields, using the project’s existing formatter if available; do not
throw a plain Error(message).

---

Outside diff comments:
In `@src/helpers/share.ts`:
- Around line 46-48: Update the catch handler in the share helper to safely
handle thrown values that are not Error instances, including null, without
accessing .message unsafely. Preserve the existing PartifulError handling and
provide a safe fallback message for other thrown values when calling jsonError.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a9f168c-a98a-4623-9a92-5b409adc6854

📥 Commits

Reviewing files that changed from the base of the PR and between 9320471 and 37de5ee.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (7)
  • .wayfinder/ts-port/map.md
  • package.json
  • src/helpers/share.ts
  • src/lib/drift.ts
  • src/lib/http.ts
  • tests/drift.test.js
  • tests/share-helper.test.js
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/drift.test.js
  • .wayfinder/ts-port/map.md
  • package.json
  • src/lib/http.ts

Comment thread src/helpers/share.ts
Comment thread tests/share-helper.test.js

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Comments suppressed due to low confidence (1)

src/commands/auth.ts:26

  • FIREBASE_API_KEY and PARTIFUL_SMS_SENDER are now masked ("..." / "****"). These constants are used to call Identity Toolkit and to detect the Partiful SMS thread; masking them will break auth login (token exchange) and likely SMS auto-retrieval. Restore the real values (or load from env with a real default) to preserve runtime behavior.
  • Files reviewed: 58/61 changed files
  • Comments generated: 0 new
  • Review effort level: Low

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants