feat(project): add project share to invite a collaborator#373
Conversation
Adds `td project share <project-ref> <email>` to invite a collaborator to a project, closing the gap alongside `project join` and `project collaborators`. - Personal/shared projects: shares via the `share_project` sync command; `--role`/`--auto-invite` are ignored with a warning (no roles apply). - Workspace projects: maps `--role guest|member|admin` (default member) to the workspace role. A non-member email errors unless `--auto-invite` is passed, which invites them to the workspace first. - Supports `--message`, `--json`, and `--dry-run`. Also extracts a shared `createProjectProgram` test helper and splits the share tests into their own `project.share.test.ts`.
doistbot
left a comment
There was a problem hiding this comment.
Thanks scottlovegrove for your contribution 🤗. The new project share command is a great addition that cleanly handles the nuances between sharing personal and workspace projects.
Few things worth tightening:
- Support the standard
--project <ref>alias for consistency, and move the workspace member validation above the--dry-runcheck so dry runs accurately reflect failure states. - Expand the
SKILL_CONTENTupdate to cover the newly added--message,--json, and--dry-runflags. - Add a test with a mocked
hasMore: trueresponse to exercise the workspace member pagination loop.
I also included a few optional follow-up notes in the details below.
Optional follow-up notes (5)
- [P3] src/commands/project/share.ts:32: Rather than inferring this type using
Awaited<ReturnType<typeof getApi>>, you can explicitly type it by importingtype { TodoistApi }from@doist/todoist-sdk. - [P3] src/commands/project/index.ts:264:
--roleis introduced here as a free-form string even though this file already useswithCaseInsensitiveChoices(...)for other enum-like options. That forcesshare.tsto hardcode and revalidate the same role list separately, so help text, shell completions, and runtime validation can drift. Build this option from the shared workspace role list and Commander choice helper instead of validating it ad hoc later. - [P3] src/commands/project/share.ts:31: This adds another copy of the
getWorkspaceUserscursor loop that already exists insrc/commands/workspace/helpers.ts(findUser/resolveWorkspaceUserRef). Pulling the exact-email membership check into that shared helper would keep workspace-user lookup behavior in one place if the paging contract or matching rules ever change. - [P3] src/commands/project/share.ts:40: Per
AGENTS.md, API paginated responses should always be destructured. Consider destructuring{ workspaceUsers, hasMore, nextCursor }directly from the response object. - [P3] src/commands/project/share.ts:79:
AGENTS.mdrequires mutating commands to output JSON usingformatJsonfromsrc/lib/output.ts. Even though there isn't a strictEntityTypefor a share result, you can useformatJson({ ... })instead ofJSON.stringify(...)to maintain consistency with the core CLI formatter. (This also applies to the JSON output on line 136).
- Accept `--project <ref>` as an alias for the positional ref, erroring if both are supplied (matches the project-context flag contract). - Move the workspace-membership check above the `--dry-run` early return so a dry run reflects the real success/failure, and simplify the auto-invite preview. - Reuse the shared `findUser` paginator from workspace/helpers instead of a local cursor loop. - Emit JSON via `formatJson` rather than `JSON.stringify`. - Expand SKILL_CONTENT to cover --project/--message/--json/--dry-run. - Add tests for paginated member lookup, the --project alias, and the positional/flag conflict.
|
@doistbot /review |
doistbot
left a comment
There was a problem hiding this comment.
Thanks scottlovegrove for your contribution 😇. The new project share flow looks sound overall, and the test coverage effectively exercises the new workspace role logic and pagination loops.
Few things worth tightening:
- Restructure the argument parsing so that using the
--projectflag doesn't cause the first positional argument to be misidentified as the collaborator email. - Add a test for
--dry-runon workspace projects when the email is not already a member and--auto-inviteis omitted to lock in the early validation logic.
I also included a few optional follow-up notes in the details below.
Optional follow-up notes (3)
- [P3] src/commands/project/test-helpers.ts:1: This file is test-only, but because it lives under
src/commands/and is not a*.test.tsfile,tsconfig.build.jsonwill emit it intodist/and pull@doist/cli-core/testinginto the production build. Move it undersrc/test-support/or exclude/rename it so test helpers stay out of release artifacts. - [P3] src/commands/project/index.ts:266: Consider using
withCaseInsensitiveChoicesandOption(both already imported in this file) for the--roleoption. This enables shell autocompletion for the valid choices and delegates validation to Commander, which allows you to remove the manualINVALID_ROLEcheck inshare.ts.typescript .addOption( withCaseInsensitiveChoices( new Option('--role <role>', 'Workspace role: guest, member, or admin (workspace projects only; default: member)'), ['guest', 'member', 'admin'] ) ) - [P3] src/commands/project/project.share.test.ts:31: Per the test conventions in
CODEBASE.md, mock entities should come fromsrc/test-support/fixtures.tsrather than being hand-built inline. This new suite starts constructing project/workspace-user objects directly here and repeats that pattern throughout the file, which makes it easier for test shapes to drift from the shared fixtures. Please switch these mocks to fixture-based objects, adding missing fixture helpers if needed.
Addresses second review round: - Rework share arg parsing into two clear modes: `--project` supplies the project and the lone positional is the email; the two-positional form is project-then-email. Throw CONFLICTING_OPTIONS if the project is given both ways, and MISSING_EMAIL when no email is supplied. - Define `--role` with `withCaseInsensitiveChoices(['guest','member','admin'])` so Commander validates and shells can complete it; drop the ad-hoc INVALID_ROLE check in share.ts. - Move the project test-program helper to src/test-support/ so it is not emitted into dist/ (and no longer drags @doist/cli-core/testing into the production build). - Add tests for dry-run-not-member validation and case-insensitive --role.
The backend rejects shares that exceed plan limits or permissions via the Sync API's per-command status; the SDK throws but drops the machine error_tag, leaving only the HTTP status and human message. Map those messages in share.ts to CliErrors with accurate codes and hints (collaborator limit, workspace guest/member limits, already a member/collaborator, unverified account, frozen project, external-invite disabled). Unrecognized errors pass through unchanged. Adds the sharing error codes to the ErrorCode union and tests for the collaborator-limit, workspace-guest-limit, and pass-through cases.
## [1.70.0](v1.69.3...v1.70.0) (2026-05-29) ### Features * **project:** add `project share` to invite a collaborator ([#373](#373)) ([514d0fd](514d0fd))
|
🎉 This PR is included in version 1.70.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
What
Adds
td project share <project-ref> <email>to invite a collaborator to a project — closes the gap alongside the existingproject joinandproject collaboratorscommands.Closes #372.
Behaviour
share_projectsync command.--role/--auto-invitedon't apply and are ignored with a warning.--role guest|member|admin(defaultmember) maps to the workspace role. If the email isn't already a workspace member, the command errors with a hint, unless--auto-inviteis passed — which invites them to the workspace first (viainviteWorkspaceUsers) and then shares.--messageattaches an optional invitation message.--dry-runpreviews (including whether an auto-invite would fire) without mutating;--jsonoutputs the result.Notes
share_projectcommand (there's no REST endpoint), so this adds ashareProjectsync wrapper inlib/api/projects-sync.ts.guest/member/admin), not theviewer/editorin the original issue text.SKILL.mdupdated and in sync.project.share.test.ts, and the sharedcreateProjectProgramtest helper was extracted for reuse.Verification
npm run type-check✅npm run check(lint + format) ✅npm run check:skill-sync✅npm test— full suite passes (1672)