donebear 0.4.3 → 0.4.4

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/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { a as toCliError, b as DONEBEAR_TOKEN_ENV, d as DEFAULT_OAUTH_CALLBACK_PATH, f as DEFAULT_OAUTH_CALLBACK_PORT, i as EXIT_CODES, l as CONFIG_DIR, n as setCurrentWorkspace, o as AUTH_FILE_PATH, r as CliError, s as CLI_NAME, t as ensureLocalContext } from "./context-store-BBEVWudm.mjs";
3
- import { n as readStdin, t as promptForValue } from "./prompt-GfcWO85b.mjs";
2
+ import { a as toCliError, d as DEFAULT_APP_URL, f as DEFAULT_OAUTH_CALLBACK_PATH, i as EXIT_CODES, l as CONFIG_DIR, n as setCurrentWorkspace, o as AUTH_FILE_PATH, p as DEFAULT_OAUTH_CALLBACK_PORT, r as CliError, s as CLI_NAME, t as ensureLocalContext, x as DONEBEAR_TOKEN_ENV } from "./context-store-BbZMKY0F.mjs";
3
+ import { n as readStdin, t as promptForValue } from "./prompt-B66eG-Zm.mjs";
4
4
  import { createRequire } from "node:module";
5
5
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
6
6
  import { basename, join, resolve } from "node:path";
@@ -18,7 +18,27 @@ import { createServer } from "node:http";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import Database from "better-sqlite3";
20
20
  //#region \0rolldown/runtime.js
21
+ var __create = Object.create;
22
+ var __defProp = Object.defineProperty;
23
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
24
+ var __getOwnPropNames = Object.getOwnPropertyNames;
25
+ var __getProtoOf = Object.getPrototypeOf;
26
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
21
27
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
28
+ var __copyProps = (to, from, except, desc) => {
29
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
30
+ key = keys[i];
31
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
32
+ get: ((k) => from[k]).bind(null, key),
33
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
34
+ });
35
+ }
36
+ return to;
37
+ };
38
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
39
+ value: mod,
40
+ enumerable: true
41
+ }) : target, mod));
22
42
  //#endregion
23
43
  //#region src/output.ts
24
44
  const writeLine = (stream, message) => {
@@ -1291,7 +1311,7 @@ const WORKSPACE_INVITATION_CREATE_MUTATION = `
1291
1311
  mutation WorkspaceInvitationCreate($input: WorkspaceInvitationCreateInput!) {
1292
1312
  workspaceInvitationCreate(input: $input) {
1293
1313
  success lastSyncId
1294
- invitation { id email code role expiresAt createdAt usedAt usedByUserId workspaceId }
1314
+ invitation { id email code role expiresAt createdAt usedAt usedByUserId workspaceId kind projectId }
1295
1315
  }
1296
1316
  }
1297
1317
  `;
@@ -1766,6 +1786,8 @@ const listWorkspaceInvitations = async (context, options) => await paginateGraph
1766
1786
  const createWorkspaceInvitation = async (context, options) => {
1767
1787
  const input = omitUndefined({
1768
1788
  email: options.email,
1789
+ kind: options.kind,
1790
+ projectId: options.projectId,
1769
1791
  role: options.role,
1770
1792
  workspaceId: options.workspaceId
1771
1793
  });
@@ -2231,6 +2253,183 @@ const createOAuthState = () => randomBytes(24).toString("hex");
2231
2253
  const createCodeVerifier = () => randomBytes(64).toString("base64url");
2232
2254
  const createCodeChallenge = (verifier) => createHash("sha256").update(verifier).digest().toString("base64url");
2233
2255
  //#endregion
2256
+ //#region src/api/agent-signup.ts
2257
+ const parseJson = async (response) => {
2258
+ const raw = await response.text();
2259
+ if (raw.length === 0) return null;
2260
+ try {
2261
+ return JSON.parse(raw);
2262
+ } catch {
2263
+ return raw;
2264
+ }
2265
+ };
2266
+ const throwForFailure = (status, payload) => {
2267
+ const message = typeof payload === "string" && payload.length > 0 ? payload : extractErrorMessage(payload);
2268
+ const record = typeof payload === "object" && payload !== null ? payload : {};
2269
+ const code = typeof record.code === "string" ? record.code : null;
2270
+ if (status === 409 || code === "DUPLICATE_EMAIL") throw new CliError(message, EXIT_CODES.AUTH_REQUIRED, "Run `donebear auth login` with this email. Do not start another signup.");
2271
+ if (status === 429 || code === "RATE_LIMIT_EXCEEDED") throw new CliError(message, EXIT_CODES.ERROR, "Wait, then retry.");
2272
+ if (status === 400 || code === "INVALID_REQUEST") throw new CliError(message, EXIT_CODES.VALIDATION);
2273
+ throw new CliError(message, EXIT_CODES.ERROR);
2274
+ };
2275
+ const request = async (endpoint, options) => {
2276
+ const headers = new Headers({ Accept: "application/json" });
2277
+ if (options.token) headers.set("Authorization", `Bearer ${options.token}`);
2278
+ if (options.body !== void 0) headers.set("Content-Type", "application/json");
2279
+ let response;
2280
+ try {
2281
+ response = await fetch(endpoint, {
2282
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
2283
+ headers,
2284
+ method: options.method ?? "GET"
2285
+ });
2286
+ } catch (error) {
2287
+ throw new CliError("Cannot connect to Done Bear API.", EXIT_CODES.NETWORK, error instanceof Error ? error.message : void 0);
2288
+ }
2289
+ const payload = await parseJson(response);
2290
+ if (!response.ok) throwForFailure(response.status, payload);
2291
+ return payload;
2292
+ };
2293
+ const startAgentSignup = async (_context, apiBaseUrl, input) => {
2294
+ return await request(`${apiBaseUrl}/api/agent-signup`, {
2295
+ body: {
2296
+ email: input.email,
2297
+ ...input.name ? { name: input.name } : {}
2298
+ },
2299
+ method: "POST"
2300
+ });
2301
+ };
2302
+ const pollAgentSignup = async (_context, apiBaseUrl, pollToken) => {
2303
+ return await request(`${apiBaseUrl}/api/agent-signup/poll`, {
2304
+ method: "GET",
2305
+ token: pollToken
2306
+ });
2307
+ };
2308
+ const verifyAgentSignupCode = async (_context, apiBaseUrl, input) => {
2309
+ return await request(`${apiBaseUrl}/api/agent-signup/verify`, {
2310
+ body: input,
2311
+ method: "POST"
2312
+ });
2313
+ };
2314
+ //#endregion
2315
+ //#region src/commands/auth-signup.ts
2316
+ const MISSING_EMAIL_MESSAGE = "Pass --email with the human's address. Do not invent one.";
2317
+ const POLL_INTERVAL_MS = 2e3;
2318
+ const defaultClock = {
2319
+ now: () => Date.now(),
2320
+ sleep: (ms, signal) => new Promise((resolve, reject) => {
2321
+ if (signal.aborted) {
2322
+ resolve();
2323
+ return;
2324
+ }
2325
+ const timer = setTimeout(resolve, ms);
2326
+ signal.addEventListener("abort", () => {
2327
+ clearTimeout(timer);
2328
+ reject(/* @__PURE__ */ new Error("aborted"));
2329
+ }, { once: true });
2330
+ })
2331
+ };
2332
+ const isComplete = (value) => value.status === "complete";
2333
+ const printWaiting = (context, verifyUrl, instructions) => {
2334
+ writeWarning(context, instructions.agent);
2335
+ writeWarning(context, instructions.human);
2336
+ writeWarning(context, `Verify link: ${verifyUrl}`);
2337
+ writeWarning(context, "Waiting for the human to verify. The CLI will continue once that happens.");
2338
+ };
2339
+ const sessionFromComplete = (complete) => ({
2340
+ accessToken: complete.apiKey.key,
2341
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2342
+ expiresAt: null,
2343
+ provider: "oauth2",
2344
+ refreshToken: null,
2345
+ tokenType: "bearer",
2346
+ user: {
2347
+ email: complete.user.email,
2348
+ id: complete.user.id
2349
+ }
2350
+ });
2351
+ const printComplete = (context, complete, verifyUrl) => {
2352
+ if (context.json) {
2353
+ writeJson({
2354
+ apiKey: complete.apiKey,
2355
+ email: complete.email,
2356
+ instructions: {
2357
+ agent: "Store the API key as DONEBEAR_TOKEN. Paste mcp.cursor into the MCP config. Do not log the full key.",
2358
+ human: "Verification is done."
2359
+ },
2360
+ mcp: complete.mcp,
2361
+ ok: true,
2362
+ sessionStored: true,
2363
+ status: "complete",
2364
+ user: complete.user,
2365
+ verifyUrl,
2366
+ workspace: complete.workspace
2367
+ });
2368
+ return;
2369
+ }
2370
+ writeSuccess(context, "Signup complete. API key stored in the CLI session.");
2371
+ writeInfo(context, `Signed in as ${complete.user.email}`);
2372
+ writeInfo(context, `Workspace: ${complete.workspace.name} (${complete.workspace.urlKey ?? complete.workspace.id})`);
2373
+ writeInfo(context, "API key (shown once):");
2374
+ writeInfo(context, complete.apiKey.key);
2375
+ writeInfo(context, "Paste-ready MCP config for Cursor:");
2376
+ writeInfo(context, JSON.stringify(complete.mcp.cursor, null, 2));
2377
+ writeInfo(context, complete.mcp.claude);
2378
+ };
2379
+ const runAgentSignup = async (context, options, clock = defaultClock) => {
2380
+ const email = options.email?.trim() ?? "";
2381
+ if (email.length === 0) throw new CliError(MISSING_EMAIL_MESSAGE, EXIT_CODES.VALIDATION, "Example: donebear auth signup --email person@example.com");
2382
+ const apiBaseUrl = resolveApiBaseUrl(context);
2383
+ const started = await startAgentSignup(context, apiBaseUrl, {
2384
+ email,
2385
+ name: options.name?.trim() || void 0
2386
+ });
2387
+ printWaiting(context, started.verifyUrl, started.instructions);
2388
+ const abort = new AbortController();
2389
+ const canPrompt = options.interactive && process.stdin.isTTY && !context.json && !context.quiet;
2390
+ let submittedCode = null;
2391
+ const promptTask = canPrompt ? promptForValue(context, "6-digit email code", abort.signal).then((value) => {
2392
+ submittedCode = value;
2393
+ }) : Promise.resolve();
2394
+ const deadline = clock.now() + options.timeout * 1e3;
2395
+ try {
2396
+ while (clock.now() < deadline) {
2397
+ if (submittedCode) {
2398
+ const verified = await verifyAgentSignupCode(context, apiBaseUrl, {
2399
+ code: submittedCode,
2400
+ pollToken: started.pollToken
2401
+ });
2402
+ if (isComplete(verified)) {
2403
+ abort.abort();
2404
+ await writeStoredAuthSession(sessionFromComplete(verified));
2405
+ printComplete(context, verified, started.verifyUrl);
2406
+ return;
2407
+ }
2408
+ if (verified.status === "consumed") throw new CliError(verified.message, EXIT_CODES.ERROR);
2409
+ submittedCode = null;
2410
+ }
2411
+ const polled = await pollAgentSignup(context, apiBaseUrl, started.pollToken);
2412
+ if (isComplete(polled)) {
2413
+ abort.abort();
2414
+ await writeStoredAuthSession(sessionFromComplete(polled));
2415
+ printComplete(context, polled, started.verifyUrl);
2416
+ return;
2417
+ }
2418
+ if (polled.status === "consumed") throw new CliError(polled.message, EXIT_CODES.ERROR);
2419
+ try {
2420
+ await clock.sleep(POLL_INTERVAL_MS, abort.signal);
2421
+ } catch {
2422
+ break;
2423
+ }
2424
+ }
2425
+ } finally {
2426
+ abort.abort();
2427
+ await promptTask.catch(() => void 0);
2428
+ }
2429
+ throw new CliError("Signup timed out before the email was verified.", EXIT_CODES.ERROR, "Ask the human to open the verify link, then run this command again if the code expired.");
2430
+ };
2431
+ const parseSignupTimeout = (value) => parsePositiveInteger(value, "Timeout");
2432
+ //#endregion
2234
2433
  //#region src/commands/auth.ts
2235
2434
  const runLogin = async (context, options) => {
2236
2435
  const { authorizeUrl, tokenUrl } = buildOAuthUrls(resolveSupabaseConfig());
@@ -2333,6 +2532,7 @@ const getOptionalSupabaseConfig = () => {
2333
2532
  const registerAuthCommands = (program) => {
2334
2533
  const auth = program.command("auth").description("Authenticate donebear CLI").addHelpText("after", `
2335
2534
  Examples:
2535
+ donebear auth signup --email person@example.com --json
2336
2536
  donebear auth login
2337
2537
  donebear auth login --no-open (print URL, don't open browser)
2338
2538
  donebear auth status --json
@@ -2361,6 +2561,25 @@ Non-interactive / CI use:
2361
2561
  await runLogin(context, options);
2362
2562
  });
2363
2563
  });
2564
+ auth.command("signup").description("Create a Done Bear account from a human email (agent signup)").option("--email <email>", "Human email to verify. Do not invent one.").option("--name <name>", "Optional display name").option("--timeout <seconds>", "Seconds to wait for email verification", parseSignupTimeout, 600).option("--no-interactive", "Never prompt for the email code; poll the verify link instead").addHelpText("after", `
2565
+ Creates a real human account. One person must verify the email (code or link).
2566
+ Do not invent an email address. If the address is already registered, run
2567
+ \`donebear auth login\` instead.
2568
+
2569
+ Examples:
2570
+ donebear auth signup --email person@example.com
2571
+ donebear auth signup --email person@example.com --json --no-interactive
2572
+ `).action(async (options, command) => {
2573
+ const context = contextFromCommand(command);
2574
+ await runWithErrorHandling(context, async () => {
2575
+ await runAgentSignup(context, {
2576
+ email: options.email,
2577
+ interactive: options.interactive !== false,
2578
+ name: options.name,
2579
+ timeout: options.timeout
2580
+ });
2581
+ });
2582
+ });
2364
2583
  auth.command("status").description("Check authentication status").action(async (_options, command) => {
2365
2584
  const context = contextFromCommand(command);
2366
2585
  await runWithErrorHandling(context, async () => {
@@ -2566,6 +2785,11 @@ const createTeamProjectId = (params) => createCompositeSyncId("TeamProject", [
2566
2785
  params.teamId,
2567
2786
  params.projectId
2568
2787
  ]);
2788
+ const createTeamMembershipId = (params) => createCompositeSyncId("TeamMembership", [
2789
+ params.workspaceId,
2790
+ params.teamId,
2791
+ params.userId
2792
+ ]);
2569
2793
  const createProjectMembershipId = (params) => createCompositeSyncId("ProjectMembership", [
2570
2794
  params.workspaceId,
2571
2795
  params.projectId,
@@ -3987,7 +4211,7 @@ const checkAuth = async (context) => {
3987
4211
  };
3988
4212
  const checkWorkspace = async () => {
3989
4213
  try {
3990
- const { ensureLocalContext } = await import("./context-store-ClAx1T2N.mjs");
4214
+ const { ensureLocalContext } = await import("./context-store-D8zfa5LS.mjs");
3991
4215
  const localContext = await ensureLocalContext();
3992
4216
  if (!localContext.workspaceId) return {
3993
4217
  message: "No workspace selected (run `donebear workspace use`)",
@@ -4309,6 +4533,10 @@ state and start date rather than stored as a column:
4309
4533
 
4310
4534
  - inbox captured, not yet placed
4311
4535
  - today start date today or earlier
4536
+
4537
+ A private email address (Settings on the web) can also drop a message into
4538
+ today as a task: subject is the title, body is the notes. Slack capture is
4539
+ unchanged.
4312
4540
  - upcoming start date in the future
4313
4541
  - anytime actionable now, no date pressure
4314
4542
  - someday parked, off every working list
@@ -4324,7 +4552,9 @@ completed task's view is logbook, so \`--state done --view today\` matches
4324
4552
  nothing. What was finished today or this week comes off the completion date,
4325
4553
  not the ordering: the list prints it in the \`completed\` column, \`task show
4326
4554
  --json\` reports it as \`completedAtIso\`, and each row from \`task list --json\`
4327
- carries \`completedAt\`.
4555
+ carries \`completedAt\`. The iOS and web apps can keep completed to-dos on Today
4556
+ until a day roll or Log Completed (Settings → Preferences → Logging). The CLI
4557
+ still treats every completed task as logbook.
4328
4558
 
4329
4559
  donebear task add "Renew passport" --when upcoming --deadline 2030-03-01 --json
4330
4560
  donebear task list --view logbook --json
@@ -4382,8 +4612,16 @@ Clear a date with the explicit flag rather than passing an empty value:
4382
4612
  donebear task edit 12 --clear-deadline --json
4383
4613
  donebear project edit launch --clear-target-date --json
4384
4614
 
4385
- Repeating tasks exist in Done Bear but are not editable from the CLI. If a user
4386
- asks to make something repeat, say so rather than creating a run of copies.`,
4615
+ Repeat rules are configured in the app and cannot be edited from the CLI. If a user
4616
+ asks to make something repeat, say so rather than creating a run of copies.
4617
+
4618
+ Upcoming copies can be edited or completed early. Fixed repeats keep their
4619
+ calendar schedule; after-completion repeats count from actual completion.
4620
+ CLI date edits affect the copy only. Use When in the app to choose Make
4621
+ Exception or Update Rule for a fixed series. Custom and count-limited
4622
+ patterns need the Repeat editor.
4623
+ In the app, search for Repeating to see rules and upcoming copies. Select multiple
4624
+ repeating tasks and use More to pause, resume, or stop their series.`,
4387
4625
  title: "Dates and deadlines",
4388
4626
  whenToUse: "Setting or clearing a due date, or choosing between a deadline and a start date."
4389
4627
  },
@@ -4400,6 +4638,10 @@ never have to read a task first just to preserve its other fields.
4400
4638
  - cleared use the paired flag: \`--clear-deadline\`, \`--clear-project\`,
4401
4639
  \`--clear-team\`, \`--clear-description\`.
4402
4640
 
4641
+ The description is markdown. Write a link as a bare URL (\`https://…\`), which
4642
+ every client shows as a link; \`<https://…>\` shows its angle brackets on iOS
4643
+ and here.
4644
+
4403
4645
  Not on \`task edit\`:
4404
4646
 
4405
4647
  - labels \`donebear label add|remove\`
@@ -4593,9 +4835,21 @@ off unless the user named a second one:
4593
4835
  Ids and names never cross workspaces. A missing task, project, or label is
4594
4836
  missing from *this* workspace before it is deleted.
4595
4837
 
4838
+ A workspace owner can turn on domain join from Settings → Members. People who
4839
+ later sign in with a verified address on that work domain join as members,
4840
+ without an invite code. Personal email hosts cannot be used. The CLI has no
4841
+ domain-join command; joining by code remains \`donebear workspace join\`.
4842
+
4596
4843
  Auth precedence: \`DONEBEAR_TOKEN\` in the environment, then the stored session
4597
- from \`donebear auth login\`. Set the env var for anything non-interactive; the
4598
- browser login flow will hang in CI.
4844
+ from \`donebear auth login\` or \`donebear auth signup\`. Set the env var for
4845
+ anything non-interactive; the browser login flow will hang in CI.
4846
+
4847
+ New accounts from a coding agent: \`donebear auth signup --email <human-email>
4848
+ --json --no-interactive\`. Do not invent an email. One human step verifies it
4849
+ (link or 6-digit code). The CLI then stores a \`db_\` key with read+write and
4850
+ prints paste-ready MCP config for \`https://mcp.donebear.com/mcp\`. If that
4851
+ email is already registered, run \`donebear auth login\` instead. WorkOS
4852
+ \`agent_auth\` and RFC 8628 device code are not offered.
4599
4853
 
4600
4854
  export DONEBEAR_TOKEN=<token>
4601
4855
  donebear whoami --json
@@ -4605,6 +4859,15 @@ write fails with an explicit message rather than silently doing nothing — tell
4605
4859
  the user they need a write-scoped key rather than trying to work around it.
4606
4860
  \`donebear doctor --json\` reports which of these is actually in effect.
4607
4861
 
4862
+ ## Invites and share links
4863
+
4864
+ \`donebear workspace invite --email user@example.com\` sends a single-use
4865
+ email invite. \`donebear workspace invite --share --copy\` mints a reusable
4866
+ join link at \`https://donebear.com/join?code=\`. \`--project <id>\` on
4867
+ \`--share\` scopes the link: whoever opens it joins the whole workspace as a
4868
+ member, gets that project, and uses a seat. Join with
4869
+ \`donebear workspace join <code>\` or by opening the link while signed in.
4870
+
4608
4871
  ## Task attachments (read-only)
4609
4872
 
4610
4873
  Tasks can carry file/image attachments, capped at 10 MB per file and roughly
@@ -4625,6 +4888,9 @@ Three commands orient you, in order of cost:
4625
4888
  donebear guide <topic> concepts and conventions, no auth
4626
4889
  donebear context --json the live workspace: counts, recent tasks, projects
4627
4890
 
4891
+ No account yet: \`donebear auth signup --email <human-email> --json --no-interactive\`.
4892
+ Do not invent the email. A human verifies it, then the CLI stores a \`db_\` key.
4893
+
4628
4894
  \`donebear spec <command> <subcommand>\` narrows to one command's flags, output
4629
4895
  shape, and examples. Read that before guessing at a flag.
4630
4896
 
@@ -4653,22 +4919,25 @@ workspace from the token.`,
4653
4919
 
4654
4920
  Two importers exist, and they are not the same thing.
4655
4921
 
4922
+ **In the apps**, Settings → Integrations & Import (web) and Settings → Import
4923
+ (iOS: Reminders, Todoist, Things) cover Todoist, TickTick, Microsoft To Do, Any.do, Apple
4924
+ Reminders, Things 3, Trello, Asana, Notion, Google Tasks, and plain CSV, JSON or
4925
+ iCal files. Things 3 is no longer CLI-only: on iPhone, Things → Settings →
4926
+ General → Diagnostics → Enter Code → \`491348\` → Send Things Database, then
4927
+ upload the \`.aar\` under Settings → Import → Things, or \`main.sqlite\` on the web.
4928
+
4656
4929
  **In the terminal**, \`donebear import things\` reads a copy of a Things 3
4657
4930
  SQLite database and writes it into a workspace. It is the only import this CLI
4658
- performs. \`donebear spec import things\` lists its flags.
4931
+ performs. \`donebear spec import things\` lists its flags. It is the power-user
4932
+ path; send people to the apps first.
4659
4933
 
4660
4934
  On macOS it finds that database itself — \`donebear import things --dry-run\`
4661
- with no other flags is the whole command. This is worth volunteering, because
4662
- Things keeps its database inside a folder Finder hides, under a dated directory
4663
- whose name nobody can guess, and people hunting for it in Finder is the single
4664
- most common way this import stalls. \`--database-url\` is for a database
4665
- somewhere unusual: a backup, an external disk, another machine's copy.
4935
+ with no other flags is the whole command. \`--database-url\` is for a database
4936
+ somewhere unusual: an iPhone export, a backup, an external disk, another
4937
+ machine's copy.
4666
4938
 
4667
- **In the web app**, Settings → Integrations & Import covers Todoist, TickTick,
4668
- Microsoft To Do, Any.do, Apple Reminders, Things 3, Trello, Asana, Notion,
4669
- Google Tasks, and plain CSV, JSON or iCal files. If a user asks how to move a
4670
- backlog in from anything other than Things, that is where to send them; there
4671
- is no CLI equivalent to offer.
4939
+ If a user asks how to move a backlog in from anything other than Things, that is
4940
+ the web import screen; there is no CLI equivalent to offer.
4672
4941
 
4673
4942
  Each of those is a file the user exports and uploads — Todoist from Settings →
4674
4943
  Backups, Google Tasks from Google Takeout, Microsoft To Do from a read-only
@@ -4677,6 +4946,13 @@ that may change: a read-only connect flow is being built so Done Bear can make
4677
4946
  that call instead, so treat the Graph export as the current route and check the
4678
4947
  import screen before promising a connection.
4679
4948
 
4949
+ Every importer, here and in the apps, only reads the source; none of them
4950
+ writes, edits or deletes anything in the app being left. The one path that
4951
+ does is Reminders Inbox on iPhone (Settings → Import → Reminders Inbox), which
4952
+ is a capture bridge rather than an import: a chosen Reminders list shows at the
4953
+ top of Inbox, and importing a to-do from there creates the task and removes it
4954
+ from Reminders after a confirmation. It has no CLI equivalent.
4955
+
4680
4956
  The other direction is here though: \`donebear export\` writes a portable JSON
4681
4957
  archive of a workspace, and the web importer restores one.
4682
4958
 
@@ -4709,9 +4985,8 @@ only what is new.
4709
4985
 
4710
4986
  There is no undo for \`donebear import things\`; a run has to be unpicked by
4711
4987
  hand. The web importer does offer one on its completion screen, but the receipt
4712
- lives in the browser that ran the import, so it cannot be reached from the CLI,
4713
- another device or the iOS app. Say that plainly rather than sending someone
4714
- looking for a flag.`,
4988
+ lives in the browser that ran the import. An iOS import is undone with the
4989
+ usual undo for that change, not from the web receipt.`,
4715
4990
  title: "Importing from another app",
4716
4991
  whenToUse: "Moving a backlog in from Things 3 or another task manager, or restoring an export."
4717
4992
  }
@@ -4952,9 +5227,10 @@ const createDeterministicImportId = (params) => createDeterministicId(THINGS_IMP
4952
5227
  //#endregion
4953
5228
  //#region ../../packages/import-core/src/things/labels.ts
4954
5229
  const normalizeThingsLabelLookupKey = (value) => value.trim().toLocaleLowerCase();
5230
+ const normalizeThingsTeamLookupKey = (value) => value.trim().toLocaleLowerCase();
4955
5231
  //#endregion
4956
5232
  //#region ../../packages/import-core/src/things/values.ts
4957
- var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
5233
+ var import_rrule = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
4958
5234
  (function webpackUniversalModuleDefinition(root, factory) {
4959
5235
  if (typeof exports === "object" && typeof module === "object") module.exports = factory();
4960
5236
  else if (typeof define === "function" && define.amd) define([], factory);
@@ -7708,7 +7984,7 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
7708
7984
  return __webpack_exports__;
7709
7985
  })();
7710
7986
  });
7711
- })))();
7987
+ })))(), 1);
7712
7988
  const PACKED_DATE_MULTIPLIER = 128;
7713
7989
  const PACKED_DATE_DAY_MASK = 31;
7714
7990
  const PACKED_DATE_MONTH_MASK = 15;
@@ -7769,6 +8045,7 @@ const combineThingsReminderAt = (options) => {
7769
8045
  };
7770
8046
  //#endregion
7771
8047
  //#region ../../packages/import-core/src/things/recurrence.ts
8048
+ const { RRule, rrulestr } = typeof import_rrule.RRule === "function" ? import_rrule : import_rrule.default;
7772
8049
  const THINGS_FREQUENCY_TO_RRULE = new Map([
7773
8050
  [4, "YEARLY"],
7774
8051
  [8, "MONTHLY"],
@@ -7809,8 +8086,8 @@ const parseOffsets = (xml) => {
7809
8086
  };
7810
8087
  const parseRRule = (rule) => {
7811
8088
  try {
7812
- const parsed = (0, import_rrule.rrulestr)(rule);
7813
- return parsed instanceof import_rrule.RRule ? parsed : null;
8089
+ const parsed = rrulestr(rule);
8090
+ return parsed instanceof RRule ? parsed : null;
7814
8091
  } catch {
7815
8092
  return null;
7816
8093
  }
@@ -8016,14 +8293,14 @@ const pickCompletedAt = (task) => {
8016
8293
  return decodeThingsUnixSeconds(task.stopDate) ?? decodeThingsUnixSeconds(task.userModificationDate) ?? decodeThingsUnixSeconds(task.creationDate);
8017
8294
  };
8018
8295
  const sortValue = (primary, fallback) => typeof primary === "number" ? primary : fallback;
8019
- const textDecoder = new TextDecoder();
8296
+ const textDecoder$1 = new TextDecoder();
8020
8297
  const encodeBase64 = (value) => {
8021
8298
  let binary = "";
8022
8299
  for (const byte of value) binary += String.fromCodePoint(byte);
8023
8300
  return btoa(binary);
8024
8301
  };
8025
8302
  const sanitizeBinary = (value) => {
8026
- const asText = textDecoder.decode(value);
8303
+ const asText = textDecoder$1.decode(value);
8027
8304
  if (!asText.includes("�") && READABLE_TEXT_PATTERN.test(asText)) {
8028
8305
  const trimmed = asText.trim();
8029
8306
  if (trimmed.length > 0) return trimmed;
@@ -8183,19 +8460,25 @@ const buildChecklistMutation = (options) => {
8183
8460
  };
8184
8461
  const buildTeamActions = (ctx, areas) => {
8185
8462
  const teams = [];
8463
+ const teamMemberships = [];
8464
+ const membershipIds = /* @__PURE__ */ new Set();
8186
8465
  for (const area of [...areas].toSorted((a, b) => {
8187
8466
  const indexComparison = compareNullableNumber(a.sortIndex, b.sortIndex);
8188
8467
  if (indexComparison !== 0) return indexComparison;
8189
8468
  return a.uuid.localeCompare(b.uuid);
8190
8469
  })) {
8191
- const teamId = createDeterministicImportId({
8470
+ const teamName = normalizeTitle(area.title, `Area ${area.uuid.slice(0, 8)}`);
8471
+ const teamLookupKey = normalizeThingsTeamLookupKey(teamName);
8472
+ const existingTeamId = ctx.existingTeamIdByName.get(teamLookupKey);
8473
+ const teamId = existingTeamId ?? createDeterministicImportId({
8192
8474
  modelName: "Team",
8193
8475
  sourceId: area.uuid,
8194
8476
  workspaceId: ctx.workspaceId
8195
8477
  });
8478
+ ctx.existingTeamIdByName.set(teamLookupKey, teamId);
8196
8479
  ctx.areaTeamIdBySourceId.set(area.uuid, teamId);
8197
8480
  const areaTagTitles = (ctx.areaTagMap.get(area.uuid) ?? []).map((link) => ctx.tagTitleById.get(link.tagId) ?? null).filter((title) => typeof title === "string");
8198
- teams.push({
8481
+ if (!existingTeamId) teams.push({
8199
8482
  modelId: teamId,
8200
8483
  modelName: "Team",
8201
8484
  payload: {
@@ -8204,14 +8487,36 @@ const buildTeamActions = (ctx, areas) => {
8204
8487
  description: appendMetadataBlock(null, ctx.metadataMode, buildAreaMetadata(area, areaTagTitles)),
8205
8488
  icon: null,
8206
8489
  key: ctx.teamKeys.get(area.uuid) ?? "AREA",
8207
- name: normalizeTitle(area.title, `Area ${area.uuid.slice(0, 8)}`),
8490
+ name: teamName,
8208
8491
  updatedAt: ctx.createdAtFallback,
8209
8492
  workspaceId: ctx.workspaceId
8210
8493
  },
8211
8494
  sourceId: area.uuid
8212
8495
  });
8496
+ const membershipId = createTeamMembershipId({
8497
+ teamId,
8498
+ userId: ctx.creatorId,
8499
+ workspaceId: ctx.workspaceId
8500
+ });
8501
+ if (!membershipIds.has(membershipId)) {
8502
+ membershipIds.add(membershipId);
8503
+ teamMemberships.push({
8504
+ modelId: membershipId,
8505
+ modelName: "TeamMembership",
8506
+ payload: {
8507
+ createdAt: ctx.createdAtFallback,
8508
+ teamId,
8509
+ userId: ctx.creatorId,
8510
+ workspaceId: ctx.workspaceId
8511
+ },
8512
+ sourceId: `${area.uuid}:${ctx.creatorId}`
8513
+ });
8514
+ }
8213
8515
  }
8214
- return teams;
8516
+ return {
8517
+ teamMemberships,
8518
+ teams
8519
+ };
8215
8520
  };
8216
8521
  const buildLabelActions = (ctx, tags) => {
8217
8522
  const labels = [];
@@ -8244,6 +8549,7 @@ const buildLabelActions = (ctx, tags) => {
8244
8549
  };
8245
8550
  const buildProjectActions = (ctx, sourceProjects) => {
8246
8551
  const projects = [];
8552
+ const projectMemberships = [];
8247
8553
  const teamProjects = [];
8248
8554
  for (const project of [...sourceProjects].toSorted((a, b) => {
8249
8555
  const indexComparison = compareNullableNumber(a.sortIndex, b.sortIndex);
@@ -8279,6 +8585,21 @@ const buildProjectActions = (ctx, sourceProjects) => {
8279
8585
  },
8280
8586
  sourceId: project.uuid
8281
8587
  });
8588
+ projectMemberships.push({
8589
+ modelId: createProjectMembershipId({
8590
+ projectId,
8591
+ userId: ctx.creatorId,
8592
+ workspaceId: ctx.workspaceId
8593
+ }),
8594
+ modelName: "ProjectMembership",
8595
+ payload: {
8596
+ createdAt: decodeThingsUnixSeconds(project.creationDate) ?? ctx.createdAtFallback,
8597
+ projectId,
8598
+ userId: ctx.creatorId,
8599
+ workspaceId: ctx.workspaceId
8600
+ },
8601
+ sourceId: `${project.uuid}:${ctx.creatorId}`
8602
+ });
8282
8603
  if (project.areaId !== null) {
8283
8604
  const teamId = ctx.areaTeamIdBySourceId.get(project.areaId);
8284
8605
  if (teamId) teamProjects.push({
@@ -8299,6 +8620,7 @@ const buildProjectActions = (ctx, sourceProjects) => {
8299
8620
  }
8300
8621
  }
8301
8622
  return {
8623
+ projectMemberships,
8302
8624
  projects,
8303
8625
  teamProjects
8304
8626
  };
@@ -8551,6 +8873,7 @@ const buildThingsImportPlan = (options) => {
8551
8873
  createdAtFallback: Date.now(),
8552
8874
  creatorId: options.creatorId,
8553
8875
  existingLabelIdByTitle: existingLabelsByTitle ?? /* @__PURE__ */ new Map(),
8876
+ existingTeamIdByName: options.existingTeamsByName ?? /* @__PURE__ */ new Map(),
8554
8877
  headingIdBySourceId: /* @__PURE__ */ new Map(),
8555
8878
  metadataMode: options.metadataMode,
8556
8879
  projectIdBySourceId: /* @__PURE__ */ new Map(),
@@ -8570,12 +8893,15 @@ const buildThingsImportPlan = (options) => {
8570
8893
  todayDateOnly: localDateToDateOnlyEpoch(/* @__PURE__ */ new Date()),
8571
8894
  workspaceId
8572
8895
  };
8573
- const teams = options.skipTeams ? [] : buildTeamActions(ctx, snapshot.areas);
8896
+ const { teamMemberships, teams } = options.skipTeams ? {
8897
+ teamMemberships: [],
8898
+ teams: []
8899
+ } : buildTeamActions(ctx, snapshot.areas);
8574
8900
  const labels = buildLabelActions(ctx, snapshot.tags);
8575
8901
  const sourceProjects = snapshot.tasks.filter((task) => classifyTaskKind(task) === "project");
8576
8902
  const sourceHeadings = snapshot.tasks.filter((task) => classifyTaskKind(task) === "heading");
8577
8903
  const sourceTasks = snapshot.tasks.filter((task) => classifyTaskKind(task) === "task");
8578
- const { projects, teamProjects } = buildProjectActions(ctx, sourceProjects);
8904
+ const { projectMemberships, projects, teamProjects } = buildProjectActions(ctx, sourceProjects);
8579
8905
  const headings = buildHeadingActions(ctx, sourceHeadings);
8580
8906
  const { tasks, taskLabels, checklistItems } = buildTaskActions(ctx, sourceTasks);
8581
8907
  const phases = [
@@ -8583,6 +8909,10 @@ const buildThingsImportPlan = (options) => {
8583
8909
  items: teams,
8584
8910
  name: "teams"
8585
8911
  },
8912
+ {
8913
+ items: teamMemberships,
8914
+ name: "teamMemberships"
8915
+ },
8586
8916
  {
8587
8917
  items: labels,
8588
8918
  name: "labels"
@@ -8591,6 +8921,10 @@ const buildThingsImportPlan = (options) => {
8591
8921
  items: projects,
8592
8922
  name: "projects"
8593
8923
  },
8924
+ {
8925
+ items: projectMemberships,
8926
+ name: "projectMemberships"
8927
+ },
8594
8928
  {
8595
8929
  items: teamProjects,
8596
8930
  name: "teamProjects"
@@ -8632,6 +8966,155 @@ const buildThingsImportPlan = (options) => {
8632
8966
  };
8633
8967
  };
8634
8968
  //#endregion
8969
+ //#region ../../packages/import-core/src/things/snapshot.ts
8970
+ const textDecoder = new TextDecoder();
8971
+ const THINGS_REQUIRED_TABLE = "TMTask";
8972
+ const THINGS_SQLITE_FILE = "main.sqlite";
8973
+ const LEGACY_THINGS_SQLITE_FILE = "Things.sqlite3";
8974
+ const toThingsStringValue = (value) => {
8975
+ if (typeof value === "string") {
8976
+ const trimmed = value.trim();
8977
+ return trimmed.length > 0 ? trimmed : null;
8978
+ }
8979
+ if (value instanceof Uint8Array) {
8980
+ const text = textDecoder.decode(value).trim();
8981
+ return text.length > 0 ? text : null;
8982
+ }
8983
+ return null;
8984
+ };
8985
+ const toThingsNumberValue = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
8986
+ const toThingsBooleanValue = (value) => {
8987
+ const numeric = toThingsNumberValue(value);
8988
+ return numeric === null ? null : numeric !== 0;
8989
+ };
8990
+ const normalizeThingsTaskRecord = (row) => {
8991
+ const uuid = toThingsStringValue(row.uuid);
8992
+ if (!uuid) return null;
8993
+ return {
8994
+ afterCompletionReferenceDate: toThingsNumberValue(row.rt1_afterCompletionReferenceDate ?? row.afterCompletionReferenceDate),
8995
+ areaId: toThingsStringValue(row.area),
8996
+ contactId: toThingsStringValue(row.contact ?? row.delegate),
8997
+ creationDate: toThingsNumberValue(row.creationDate),
8998
+ deadline: toThingsNumberValue(row.deadline ?? row.dueDate),
8999
+ deadlineOffset: toThingsNumberValue(row.t2_deadlineOffset ?? row.dueDateOffset),
9000
+ deadlineSuppressionDate: toThingsNumberValue(row.deadlineSuppressionDate ?? row.dueDateSuppressionDate),
9001
+ headingId: toThingsStringValue(row.heading ?? row.actionGroup),
9002
+ instanceCreationCount: toThingsNumberValue(row.rt1_instanceCreationCount ?? row.instanceCreationCount),
9003
+ instanceCreationPaused: toThingsBooleanValue(row.rt1_instanceCreationPaused ?? row.instanceCreationPaused) ?? false,
9004
+ instanceCreationStartDate: toThingsNumberValue(row.rt1_instanceCreationStartDate ?? row.instanceCreationStartDate),
9005
+ lastReminderInteractionDate: toThingsNumberValue(row.lastReminderInteractionDate ?? row.lastAlarmInteractionDate),
9006
+ nextInstanceStartDate: toThingsNumberValue(row.rt1_nextInstanceStartDate ?? row.nextInstanceStartDate),
9007
+ notes: toThingsStringValue(row.notes),
9008
+ projectId: toThingsStringValue(row.project),
9009
+ raw: row,
9010
+ recurrenceRuleRaw: toThingsStringValue(row.rt1_recurrenceRule ?? row.recurrenceRule),
9011
+ reminderTime: toThingsNumberValue(row.reminderTime),
9012
+ repeatTemplateSourceId: toThingsStringValue(row.rt1_repeatingTemplate ?? row.repeatingTemplate),
9013
+ repeaterMigrationDate: toThingsNumberValue(row.repeaterMigrationDate),
9014
+ repeaterRaw: toThingsStringValue(row.repeater),
9015
+ sortIndex: toThingsNumberValue(row.index),
9016
+ start: toThingsNumberValue(row.start),
9017
+ startBucket: toThingsNumberValue(row.startBucket),
9018
+ startDate: toThingsNumberValue(row.startDate),
9019
+ status: toThingsNumberValue(row.status),
9020
+ stopDate: toThingsNumberValue(row.stopDate),
9021
+ title: toThingsStringValue(row.title),
9022
+ todayIndex: toThingsNumberValue(row.todayIndex),
9023
+ todayIndexReferenceDate: toThingsNumberValue(row.todayIndexReferenceDate),
9024
+ trashed: toThingsBooleanValue(row.trashed) ?? false,
9025
+ type: toThingsNumberValue(row.type),
9026
+ userModificationDate: toThingsNumberValue(row.userModificationDate),
9027
+ uuid
9028
+ };
9029
+ };
9030
+ const normalizeThingsAreaRecord = (row) => {
9031
+ const uuid = toThingsStringValue(row.uuid);
9032
+ if (!uuid) return null;
9033
+ return {
9034
+ raw: row,
9035
+ sortIndex: toThingsNumberValue(row.index),
9036
+ title: toThingsStringValue(row.title),
9037
+ uuid,
9038
+ visible: toThingsBooleanValue(row.visible)
9039
+ };
9040
+ };
9041
+ const normalizeThingsTagRecord = (row) => {
9042
+ const uuid = toThingsStringValue(row.uuid);
9043
+ if (!uuid) return null;
9044
+ return {
9045
+ parentId: toThingsStringValue(row.parent),
9046
+ raw: row,
9047
+ shortcut: toThingsStringValue(row.shortcut),
9048
+ sortIndex: toThingsNumberValue(row.index),
9049
+ title: toThingsStringValue(row.title),
9050
+ usedDate: toThingsNumberValue(row.usedDate),
9051
+ uuid
9052
+ };
9053
+ };
9054
+ const normalizeThingsChecklistItemRecord = (row) => {
9055
+ const uuid = toThingsStringValue(row.uuid);
9056
+ if (!uuid) return null;
9057
+ return {
9058
+ creationDate: toThingsNumberValue(row.creationDate),
9059
+ raw: row,
9060
+ sortIndex: toThingsNumberValue(row.index),
9061
+ status: toThingsNumberValue(row.status),
9062
+ stopDate: toThingsNumberValue(row.stopDate),
9063
+ taskId: toThingsStringValue(row.task),
9064
+ title: toThingsStringValue(row.title),
9065
+ userModificationDate: toThingsNumberValue(row.userModificationDate),
9066
+ uuid
9067
+ };
9068
+ };
9069
+ const normalizeThingsTaskTagRecord = (row) => {
9070
+ const taskId = toThingsStringValue(row.tasks);
9071
+ const tagId = toThingsStringValue(row.tags);
9072
+ return taskId && tagId ? {
9073
+ tagId,
9074
+ taskId
9075
+ } : null;
9076
+ };
9077
+ const normalizeThingsAreaTagRecord = (row) => {
9078
+ const areaId = toThingsStringValue(row.areas);
9079
+ const tagId = toThingsStringValue(row.tags);
9080
+ return areaId && tagId ? {
9081
+ areaId,
9082
+ tagId
9083
+ } : null;
9084
+ };
9085
+ const normalizeThingsContactRecord = (row) => {
9086
+ const uuid = toThingsStringValue(row.uuid);
9087
+ if (!uuid) return null;
9088
+ return {
9089
+ appleAddressBookId: toThingsStringValue(row.appleAddressBookId),
9090
+ displayName: toThingsStringValue(row.displayName),
9091
+ emails: toThingsStringValue(row.emails),
9092
+ firstName: toThingsStringValue(row.firstName),
9093
+ lastName: toThingsStringValue(row.lastName),
9094
+ raw: row,
9095
+ uuid
9096
+ };
9097
+ };
9098
+ const mapPresent = (rows, normalize) => rows.map(normalize).filter((record) => record !== null);
9099
+ /**
9100
+ * Turns raw Things tables into the snapshot `buildThingsImportPlan` consumes.
9101
+ * Callers supply the SQL; this is the one place row → record mapping lives.
9102
+ */
9103
+ const buildThingsDatabaseSnapshot = (reader, databasePath) => {
9104
+ if (!reader.hasTable("TMTask")) throw new Error("This does not look like a Things 3 database. Select the main.sqlite file that contains the TMTask table.");
9105
+ return {
9106
+ areaTags: reader.hasTable("TMAreaTag") ? mapPresent(reader.readRows("TMAreaTag"), normalizeThingsAreaTagRecord) : [],
9107
+ areas: reader.hasTable("TMArea") ? mapPresent(reader.readRows("TMArea"), normalizeThingsAreaRecord) : [],
9108
+ checklistItems: reader.hasTable("TMChecklistItem") ? mapPresent(reader.readRows("TMChecklistItem"), normalizeThingsChecklistItemRecord) : [],
9109
+ contacts: reader.hasTable("TMContact") ? mapPresent(reader.readRows("TMContact"), normalizeThingsContactRecord) : [],
9110
+ databasePath,
9111
+ tags: reader.hasTable("TMTag") ? mapPresent(reader.readRows("TMTag"), normalizeThingsTagRecord) : [],
9112
+ taskColumns: reader.getTableColumns(THINGS_REQUIRED_TABLE),
9113
+ taskTags: reader.hasTable("TMTaskTag") ? mapPresent(reader.readRows("TMTaskTag"), normalizeThingsTaskTagRecord) : [],
9114
+ tasks: mapPresent(reader.readRows(THINGS_REQUIRED_TABLE), normalizeThingsTaskRecord)
9115
+ };
9116
+ };
9117
+ //#endregion
8635
9118
  //#region ../../packages/sync-ids/src/meet-done-bear.ts
8636
9119
  /**
8637
9120
  * Ids for the "Meet Done Bear" sample project every new workspace is seeded
@@ -8685,8 +9168,6 @@ const listMeetDoneBearArchiveTargets = (options) => {
8685
9168
  //#region src/import/things-sqlite.ts
8686
9169
  const THINGS_CONTAINER_PATH = join(homedir(), "Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac");
8687
9170
  const THINGS_BUNDLE_NAME = "Things Database.thingsdatabase";
8688
- const THINGS_SQLITE_FILE = "main.sqlite";
8689
- const LEGACY_THINGS_SQLITE_FILE = "Things.sqlite3";
8690
9171
  const URL_SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/iu;
8691
9172
  const isUrlLike = (value) => URL_SCHEME_PATTERN.test(value);
8692
9173
  const resolveInputPath = (databaseUrl) => {
@@ -8699,7 +9180,7 @@ const resolveInputPath = (databaseUrl) => {
8699
9180
  const expandThingsDatabasePath = (inputPath) => {
8700
9181
  const resolvedPath = resolve(inputPath);
8701
9182
  const lastSegment = basename(resolvedPath);
8702
- if (lastSegment === THINGS_SQLITE_FILE || lastSegment === LEGACY_THINGS_SQLITE_FILE) return resolvedPath;
9183
+ if (lastSegment === "main.sqlite" || lastSegment === "Things.sqlite3") return resolvedPath;
8703
9184
  if (lastSegment === THINGS_BUNDLE_NAME) return join(resolvedPath, THINGS_SQLITE_FILE);
8704
9185
  if (lastSegment.startsWith("ThingsData-")) return join(resolvedPath, THINGS_BUNDLE_NAME, THINGS_SQLITE_FILE);
8705
9186
  return resolvedPath;
@@ -8744,130 +9225,6 @@ const getTableColumns = (database, tableName) => {
8744
9225
  if (!hasTable(database, tableName)) return [];
8745
9226
  return database.prepare(`PRAGMA table_info(${tableName})`).all().map((row) => typeof row.name === "string" ? row.name : null).filter((name) => name !== null);
8746
9227
  };
8747
- const toStringValue = (value) => {
8748
- if (typeof value === "string") {
8749
- const trimmed = value.trim();
8750
- return trimmed.length > 0 ? trimmed : null;
8751
- }
8752
- if (Buffer.isBuffer(value)) {
8753
- const text = value.toString("utf-8").trim();
8754
- return text.length > 0 ? text : null;
8755
- }
8756
- return null;
8757
- };
8758
- const toNumberValue = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
8759
- const toBooleanValue = (value) => {
8760
- const numeric = toNumberValue(value);
8761
- return numeric === null ? null : numeric !== 0;
8762
- };
8763
- const normalizeTaskRecord = (row) => {
8764
- const uuid = toStringValue(row.uuid);
8765
- if (!uuid) return null;
8766
- return {
8767
- afterCompletionReferenceDate: toNumberValue(row.rt1_afterCompletionReferenceDate ?? row.afterCompletionReferenceDate),
8768
- areaId: toStringValue(row.area),
8769
- contactId: toStringValue(row.contact ?? row.delegate),
8770
- creationDate: toNumberValue(row.creationDate),
8771
- deadline: toNumberValue(row.deadline ?? row.dueDate),
8772
- deadlineOffset: toNumberValue(row.t2_deadlineOffset ?? row.dueDateOffset),
8773
- deadlineSuppressionDate: toNumberValue(row.deadlineSuppressionDate ?? row.dueDateSuppressionDate),
8774
- headingId: toStringValue(row.heading ?? row.actionGroup),
8775
- instanceCreationCount: toNumberValue(row.rt1_instanceCreationCount ?? row.instanceCreationCount),
8776
- instanceCreationPaused: toBooleanValue(row.rt1_instanceCreationPaused ?? row.instanceCreationPaused) ?? false,
8777
- instanceCreationStartDate: toNumberValue(row.rt1_instanceCreationStartDate ?? row.instanceCreationStartDate),
8778
- lastReminderInteractionDate: toNumberValue(row.lastReminderInteractionDate ?? row.lastAlarmInteractionDate),
8779
- nextInstanceStartDate: toNumberValue(row.rt1_nextInstanceStartDate ?? row.nextInstanceStartDate),
8780
- notes: toStringValue(row.notes),
8781
- projectId: toStringValue(row.project),
8782
- raw: row,
8783
- recurrenceRuleRaw: toStringValue(row.rt1_recurrenceRule ?? row.recurrenceRule),
8784
- reminderTime: toNumberValue(row.reminderTime),
8785
- repeatTemplateSourceId: toStringValue(row.rt1_repeatingTemplate ?? row.repeatingTemplate),
8786
- repeaterMigrationDate: toNumberValue(row.repeaterMigrationDate),
8787
- repeaterRaw: toStringValue(row.repeater),
8788
- sortIndex: toNumberValue(row.index),
8789
- start: toNumberValue(row.start),
8790
- startBucket: toNumberValue(row.startBucket),
8791
- startDate: toNumberValue(row.startDate),
8792
- status: toNumberValue(row.status),
8793
- stopDate: toNumberValue(row.stopDate),
8794
- title: toStringValue(row.title),
8795
- todayIndex: toNumberValue(row.todayIndex),
8796
- todayIndexReferenceDate: toNumberValue(row.todayIndexReferenceDate),
8797
- trashed: toBooleanValue(row.trashed) ?? false,
8798
- type: toNumberValue(row.type),
8799
- userModificationDate: toNumberValue(row.userModificationDate),
8800
- uuid
8801
- };
8802
- };
8803
- const normalizeAreaRecord = (row) => {
8804
- const uuid = toStringValue(row.uuid);
8805
- if (!uuid) return null;
8806
- return {
8807
- raw: row,
8808
- sortIndex: toNumberValue(row.index),
8809
- title: toStringValue(row.title),
8810
- uuid,
8811
- visible: toBooleanValue(row.visible)
8812
- };
8813
- };
8814
- const normalizeTagRecord = (row) => {
8815
- const uuid = toStringValue(row.uuid);
8816
- if (!uuid) return null;
8817
- return {
8818
- parentId: toStringValue(row.parent),
8819
- raw: row,
8820
- shortcut: toStringValue(row.shortcut),
8821
- sortIndex: toNumberValue(row.index),
8822
- title: toStringValue(row.title),
8823
- usedDate: toNumberValue(row.usedDate),
8824
- uuid
8825
- };
8826
- };
8827
- const normalizeChecklistItemRecord = (row) => {
8828
- const uuid = toStringValue(row.uuid);
8829
- if (!uuid) return null;
8830
- return {
8831
- creationDate: toNumberValue(row.creationDate),
8832
- raw: row,
8833
- sortIndex: toNumberValue(row.index),
8834
- status: toNumberValue(row.status),
8835
- stopDate: toNumberValue(row.stopDate),
8836
- taskId: toStringValue(row.task),
8837
- title: toStringValue(row.title),
8838
- userModificationDate: toNumberValue(row.userModificationDate),
8839
- uuid
8840
- };
8841
- };
8842
- const normalizeTaskTagRecord = (row) => {
8843
- const taskId = toStringValue(row.tasks);
8844
- const tagId = toStringValue(row.tags);
8845
- return taskId && tagId ? {
8846
- tagId,
8847
- taskId
8848
- } : null;
8849
- };
8850
- const normalizeAreaTagRecord = (row) => {
8851
- const areaId = toStringValue(row.areas);
8852
- const tagId = toStringValue(row.tags);
8853
- return areaId && tagId ? {
8854
- areaId,
8855
- tagId
8856
- } : null;
8857
- };
8858
- const normalizeContactRecord = (row) => {
8859
- const uuid = toStringValue(row.uuid);
8860
- if (!uuid) return null;
8861
- return {
8862
- appleAddressBookId: toStringValue(row.appleAddressBookId),
8863
- displayName: toStringValue(row.displayName),
8864
- emails: toStringValue(row.emails),
8865
- firstName: toStringValue(row.firstName),
8866
- lastName: toStringValue(row.lastName),
8867
- raw: row,
8868
- uuid
8869
- };
8870
- };
8871
9228
  const resolveThingsDatabasePath = (databaseUrl) => expandThingsDatabasePath(resolveInputPath(databaseUrl));
8872
9229
  const resolveDefaultThingsDatabasePath = () => {
8873
9230
  const resolved = findLatestThingsDatabasePath();
@@ -8882,17 +9239,11 @@ const loadThingsDatabase = (databaseUrl) => {
8882
9239
  readonly: true
8883
9240
  });
8884
9241
  try {
8885
- return {
8886
- areaTags: hasTable(database, "TMAreaTag") ? readRows(database, "TMAreaTag").map(normalizeAreaTagRecord).filter((record) => record !== null) : [],
8887
- areas: hasTable(database, "TMArea") ? readRows(database, "TMArea").map(normalizeAreaRecord).filter((record) => record !== null) : [],
8888
- checklistItems: hasTable(database, "TMChecklistItem") ? readRows(database, "TMChecklistItem").map(normalizeChecklistItemRecord).filter((record) => record !== null) : [],
8889
- contacts: hasTable(database, "TMContact") ? readRows(database, "TMContact").map(normalizeContactRecord).filter((record) => record !== null) : [],
8890
- databasePath,
8891
- tags: hasTable(database, "TMTag") ? readRows(database, "TMTag").map(normalizeTagRecord).filter((record) => record !== null) : [],
8892
- taskColumns: getTableColumns(database, "TMTask"),
8893
- taskTags: hasTable(database, "TMTaskTag") ? readRows(database, "TMTaskTag").map(normalizeTaskTagRecord).filter((record) => record !== null) : [],
8894
- tasks: readRows(database, "TMTask").map(normalizeTaskRecord).filter((record) => record !== null)
8895
- };
9242
+ return buildThingsDatabaseSnapshot({
9243
+ getTableColumns: (tableName) => getTableColumns(database, tableName),
9244
+ hasTable: (tableName) => hasTable(database, tableName),
9245
+ readRows: (tableName) => readRows(database, tableName)
9246
+ }, databasePath);
8896
9247
  } finally {
8897
9248
  database.close();
8898
9249
  }
@@ -8900,7 +9251,15 @@ const loadThingsDatabase = (databaseUrl) => {
8900
9251
  //#endregion
8901
9252
  //#region src/import/things-runner.ts
8902
9253
  const WORKSPACE_SCOPED_MODELS = SYNC_CONTENT_MODELS;
8903
- const INSERT_ONLY_MODELS = new Set(["TaskLabel", "TeamProject"]);
9254
+ const INSERT_ONLY_MODELS = new Set([
9255
+ "TaskLabel",
9256
+ "TeamProject",
9257
+ "TeamMembership",
9258
+ "ProjectMembership"
9259
+ ]);
9260
+ /** Membership joins are not in SYNC_CONTENT_MODELS (identity surface), but
9261
+ * Things import writes the importer's own rows and needs them for skip-on-rerun. */
9262
+ const THINGS_MEMBERSHIP_MODELS = ["TeamMembership", "ProjectMembership"];
8904
9263
  const chunk = (items, size) => {
8905
9264
  const result = [];
8906
9265
  for (let index = 0; index < items.length; index += size) result.push(items.slice(index, index + size));
@@ -8944,9 +9303,9 @@ const loadWorkspaceScopedRowsByModel = async (context, options) => {
8944
9303
  };
8945
9304
  const loadExistingImportState = async (options) => {
8946
9305
  let modelNames;
8947
- if (options.dryRun) modelNames = ["Label"];
8948
- else if (options.skipTeams) modelNames = WORKSPACE_SCOPED_MODELS.filter((m) => m !== "Team" && m !== "TeamProject");
8949
- else modelNames = WORKSPACE_SCOPED_MODELS;
9306
+ if (options.dryRun) modelNames = options.skipTeams ? ["Label"] : ["Label", "Team"];
9307
+ else if (options.skipTeams) modelNames = [...WORKSPACE_SCOPED_MODELS.filter((m) => m !== "Team" && m !== "TeamProject"), "ProjectMembership"];
9308
+ else modelNames = [...WORKSPACE_SCOPED_MODELS, ...THINGS_MEMBERSHIP_MODELS];
8950
9309
  const rowsByModel = await loadWorkspaceScopedRowsByModel(options.context, {
8951
9310
  apiBaseUrl: options.commandContext.apiBaseUrl,
8952
9311
  modelNames,
@@ -8955,6 +9314,7 @@ const loadExistingImportState = async (options) => {
8955
9314
  });
8956
9315
  return {
8957
9316
  existingLabelIdsByTitle: buildExistingLabelIdsByTitle(rowsByModel),
9317
+ existingTeamIdsByName: buildExistingTeamIdsByName(rowsByModel),
8958
9318
  rowsByModel
8959
9319
  };
8960
9320
  };
@@ -8972,9 +9332,23 @@ const buildExistingLabelIdsByTitle = (rowsByModel) => {
8972
9332
  }
8973
9333
  return labelsByTitle;
8974
9334
  };
9335
+ const buildExistingTeamIdsByName = (rowsByModel) => {
9336
+ const rows = rowsByModel.get("Team") ?? [];
9337
+ const teamsByName = /* @__PURE__ */ new Map();
9338
+ for (const row of rows) {
9339
+ if (typeof row !== "object" || row === null) continue;
9340
+ const record = row;
9341
+ if (record.__class !== "Team") continue;
9342
+ const id = typeof record.id === "string" ? record.id : null;
9343
+ const name = typeof record.name === "string" ? record.name.trim() : "";
9344
+ if (!id || name.length === 0) continue;
9345
+ teamsByName.set(normalizeThingsTeamLookupKey(name), id);
9346
+ }
9347
+ return teamsByName;
9348
+ };
8975
9349
  const buildExistingIds = (rowsByModel) => {
8976
- const entries = WORKSPACE_SCOPED_MODELS.map((modelName) => [modelName, collectRowIds(rowsByModel.get(modelName) ?? [], modelName)]);
8977
- return new Map(entries);
9350
+ const modelNames = [...WORKSPACE_SCOPED_MODELS, ...THINGS_MEMBERSHIP_MODELS];
9351
+ return new Map(modelNames.map((modelName) => [modelName, collectRowIds(rowsByModel.get(modelName) ?? [], modelName)]));
8978
9352
  };
8979
9353
  const buildTransactions = (phaseItems, options) => {
8980
9354
  const transactions = [];
@@ -8992,6 +9366,7 @@ const buildTransactions = (phaseItems, options) => {
8992
9366
  if (action === "INSERT") {
8993
9367
  inserted += 1;
8994
9368
  existingIds.add(item.modelId);
9369
+ options.existingIds.set(item.modelName, existingIds);
8995
9370
  } else updated += 1;
8996
9371
  transactions.push({
8997
9372
  action,
@@ -9077,6 +9452,7 @@ const runThingsImport = async (options) => {
9077
9452
  const plan = buildThingsImportPlan({
9078
9453
  creatorId: options.creatorId,
9079
9454
  existingLabelsByTitle: existingState.existingLabelIdsByTitle,
9455
+ existingTeamsByName: existingState.existingTeamIdsByName,
9080
9456
  metadataMode: options.metadataMode,
9081
9457
  skipTeams: options.skipTeams,
9082
9458
  snapshot,
@@ -9305,6 +9681,30 @@ const runLabelCreate = async (context, title, options) => {
9305
9681
  }
9306
9682
  writeSuccess(context, `Created label: ${title.trim()}`);
9307
9683
  };
9684
+ /**
9685
+ * A TaskLabel row is keyed on task, label and workspace together, so a DELETE
9686
+ * needs `workspaceId` as much as an INSERT does: without it the server cannot
9687
+ * address the row and rejects the mutation.
9688
+ */
9689
+ const buildTaskLabelTransaction = (options) => {
9690
+ const { action, clientId, labelId, taskId, workspaceId } = options;
9691
+ const key = {
9692
+ labelId,
9693
+ taskId,
9694
+ workspaceId
9695
+ };
9696
+ return {
9697
+ action,
9698
+ clientId,
9699
+ clientTxId: randomUUID(),
9700
+ modelId: `${taskId}:${labelId}`,
9701
+ modelName: "TaskLabel",
9702
+ payload: action === "INSERT" ? {
9703
+ ...key,
9704
+ createdAt: Date.now()
9705
+ } : key
9706
+ };
9707
+ };
9308
9708
  const runLabelAdd = async (context, taskRef, labelRef, options) => {
9309
9709
  const commandContext = await resolveCommandContext(context);
9310
9710
  const workspaceRef = normalizeWorkspaceRef(options.workspace);
@@ -9322,19 +9722,13 @@ const runLabelAdd = async (context, taskRef, labelRef, options) => {
9322
9722
  baseUrl: commandContext.apiBaseUrl,
9323
9723
  request: {
9324
9724
  batchId: randomUUID(),
9325
- transactions: [{
9725
+ transactions: [buildTaskLabelTransaction({
9326
9726
  action: "INSERT",
9327
9727
  clientId: commandContext.localContext.clientId,
9328
- clientTxId: randomUUID(),
9329
- modelId: `${task.id}:${label.id}`,
9330
- modelName: "TaskLabel",
9331
- payload: {
9332
- createdAt: Date.now(),
9333
- labelId: label.id,
9334
- taskId: task.id,
9335
- workspaceId: task.workspaceId
9336
- }
9337
- }]
9728
+ labelId: label.id,
9729
+ taskId: task.id,
9730
+ workspaceId: task.workspaceId
9731
+ })]
9338
9732
  },
9339
9733
  token: commandContext.token
9340
9734
  });
@@ -9366,17 +9760,13 @@ const runLabelRemove = async (context, taskRef, labelRef, options) => {
9366
9760
  baseUrl: commandContext.apiBaseUrl,
9367
9761
  request: {
9368
9762
  batchId: randomUUID(),
9369
- transactions: [{
9763
+ transactions: [buildTaskLabelTransaction({
9370
9764
  action: "DELETE",
9371
9765
  clientId: commandContext.localContext.clientId,
9372
- clientTxId: randomUUID(),
9373
- modelId: `${task.id}:${label.id}`,
9374
- modelName: "TaskLabel",
9375
- payload: {
9376
- labelId: label.id,
9377
- taskId: task.id
9378
- }
9379
- }]
9766
+ labelId: label.id,
9767
+ taskId: task.id,
9768
+ workspaceId: task.workspaceId
9769
+ })]
9380
9770
  },
9381
9771
  token: commandContext.token
9382
9772
  });
@@ -9581,7 +9971,7 @@ const resolveProjectName = async (context, rawName) => {
9581
9971
  const trimmed = rawName.trim();
9582
9972
  if (trimmed.length > 0) return trimmed;
9583
9973
  }
9584
- const { promptForValue } = await import("./prompt-B7mDo4K7.mjs");
9974
+ const { promptForValue } = await import("./prompt--P7Y1FBL.mjs");
9585
9975
  const prompted = await promptForValue(context, "Project name");
9586
9976
  if (prompted) return prompted;
9587
9977
  throw new CliError("Project name is required.", EXIT_CODES.VALIDATION, "Provide a name: donebear project add \"My project\"");
@@ -10017,7 +10407,7 @@ Examples:
10017
10407
  //#endregion
10018
10408
  //#region src/commands/spec.ts
10019
10409
  const OVERVIEW = {
10020
- authentication: "Set DONEBEAR_TOKEN env var, or run: donebear auth login",
10410
+ authentication: "Set DONEBEAR_TOKEN env var, run: donebear auth login, or create an account with donebear auth signup --email <human-email>",
10021
10411
  commands: {
10022
10412
  "api-key": {
10023
10413
  description: "Manage personal API keys",
@@ -10033,8 +10423,9 @@ const OVERVIEW = {
10033
10423
  quick_example: "donebear auth status --json",
10034
10424
  subcommands: [
10035
10425
  "login",
10036
- "status",
10037
- "logout"
10426
+ "logout",
10427
+ "signup",
10428
+ "status"
10038
10429
  ]
10039
10430
  },
10040
10431
  comment: {
@@ -10264,6 +10655,44 @@ const COMMAND_SPECS = {
10264
10655
  },
10265
10656
  usage: "donebear auth login [options]"
10266
10657
  },
10658
+ "auth signup": {
10659
+ command: "auth signup",
10660
+ examples: ["donebear auth signup --email person@example.com", "donebear auth signup --email person@example.com --json --no-interactive"],
10661
+ json_output: {
10662
+ apiKey: "{ id, key, prefix, scopes }",
10663
+ email: "string",
10664
+ instructions: "{ agent, human }",
10665
+ mcp: "{ url, headers, cursor, claude }",
10666
+ ok: "boolean",
10667
+ sessionStored: "boolean",
10668
+ status: "complete",
10669
+ user: "{ id, email }",
10670
+ verifyUrl: "string",
10671
+ workspace: "{ id, name, urlKey }"
10672
+ },
10673
+ non_interactive: "Requires --email. Do not invent an address. --json and --no-interactive print the verify link on stderr, poll until the human verifies, then emit one complete JSON object on stdout. If the email is already registered, run donebear auth login instead.",
10674
+ notes: "Creates a real human workspace after email verification, then mints a db_ API key with read+write (not admin). WorkOS agent_auth and RFC 8628 device code are not offered.",
10675
+ options: {
10676
+ "--email": {
10677
+ description: "Human email to verify. Required. Do not invent an address.",
10678
+ type: "string"
10679
+ },
10680
+ "--name": {
10681
+ description: "Optional display name",
10682
+ type: "string"
10683
+ },
10684
+ "--no-interactive": {
10685
+ description: "Never prompt for the email code; poll the verify link instead",
10686
+ type: "boolean"
10687
+ },
10688
+ "--timeout": {
10689
+ default: 600,
10690
+ description: "Seconds to wait for email verification",
10691
+ type: "number"
10692
+ }
10693
+ },
10694
+ usage: "donebear auth signup [options]"
10695
+ },
10267
10696
  "auth status": {
10268
10697
  command: "auth status",
10269
10698
  examples: ["donebear auth status --json"],
@@ -11120,6 +11549,44 @@ const COMMAND_SPECS = {
11120
11549
  options: {},
11121
11550
  usage: "donebear whoami"
11122
11551
  },
11552
+ "workspace invite": {
11553
+ arguments: { workspace: {
11554
+ description: "Workspace id, urlKey, or name. Defaults to the current workspace.",
11555
+ required: false,
11556
+ type: "string"
11557
+ } },
11558
+ command: "workspace invite",
11559
+ examples: [
11560
+ "donebear workspace invite --email user@example.com --role member",
11561
+ "donebear workspace invite --share --copy",
11562
+ "donebear workspace invite --share --project <project-id>"
11563
+ ],
11564
+ json_output: {
11565
+ invitation: "WorkspaceInvitationRecord",
11566
+ workspace: "WorkspaceRecord"
11567
+ },
11568
+ notes: "Email invites are single-use. --share mints a reusable /join?code= link. --project requires --share.",
11569
+ options: {
11570
+ "--email": {
11571
+ description: "Email address to invite (single-use)",
11572
+ type: "string"
11573
+ },
11574
+ "--project": {
11575
+ description: "Scope a share link so joiners also become members of this project",
11576
+ type: "string"
11577
+ },
11578
+ "--role": {
11579
+ default: "member",
11580
+ description: "Role for an email invite: member|admin",
11581
+ type: "string"
11582
+ },
11583
+ "--share": {
11584
+ description: "Create a reusable join link instead of an email invite",
11585
+ type: "boolean"
11586
+ }
11587
+ },
11588
+ usage: "donebear workspace invite [workspace] [options]"
11589
+ },
11123
11590
  "workspace list": {
11124
11591
  command: "workspace list",
11125
11592
  examples: ["donebear workspace list --json"],
@@ -11162,10 +11629,11 @@ const TOP_LEVEL_SPECS = {
11162
11629
  auth: {
11163
11630
  command: "auth",
11164
11631
  description: "Authentication",
11165
- non_interactive: "Set DONEBEAR_TOKEN=<token> to skip interactive login.",
11632
+ non_interactive: "Set DONEBEAR_TOKEN=<token> to skip interactive login. New accounts: donebear auth signup --email <human-email> --json --no-interactive.",
11166
11633
  subcommands: {
11167
11634
  login: "OAuth login (Google or GitHub)",
11168
11635
  logout: "Sign out and remove stored session",
11636
+ signup: "Create an account from a human email (agent signup)",
11169
11637
  status: "Show auth status"
11170
11638
  },
11171
11639
  tip: "donebear spec auth login: get full spec for auth login"
@@ -11252,7 +11720,7 @@ const TOP_LEVEL_SPECS = {
11252
11720
  create: "Create a new workspace",
11253
11721
  current: "Show the active workspace",
11254
11722
  invitations: "List pending invitations",
11255
- invite: "Invite a user by email",
11723
+ invite: "Invite by email, or mint a reusable --share join link",
11256
11724
  join: "Join a workspace by invite",
11257
11725
  list: "List all workspaces",
11258
11726
  members: "List workspace members",
@@ -11300,6 +11768,7 @@ Examples:
11300
11768
  donebear spec project list Detailed spec for project list
11301
11769
  donebear spec import things Detailed spec for the Things importer
11302
11770
  donebear spec search Detailed spec for search
11771
+ donebear spec auth signup Agent signup from a human email
11303
11772
  donebear spec auth login Auth login options and non-interactive notes
11304
11773
  `).action(async (command, subcommand, _options, cmd) => {
11305
11774
  const context = contextFromCommand(cmd);
@@ -11758,10 +12227,14 @@ const runWorkspaceInvite = async (context, workspaceRefArg, workspaceOverride, o
11758
12227
  token: commandContext.token,
11759
12228
  workspaceRef: workspaceOverride ?? workspaceRefArg ?? null
11760
12229
  });
12230
+ if (options.share && options.email) throw new CliError("Use --share or --email, not both.", EXIT_CODES.VALIDATION);
12231
+ if (options.project && !options.share) throw new CliError("--project requires --share.", EXIT_CODES.VALIDATION);
11761
12232
  const invitation = await createWorkspaceInvitation(context, {
11762
12233
  baseUrl: commandContext.apiBaseUrl,
11763
12234
  email: options.email,
11764
- role: options.role ?? "member",
12235
+ kind: options.share ? "share" : void 0,
12236
+ projectId: options.project,
12237
+ role: options.share ? "member" : options.role ?? "member",
11765
12238
  token: commandContext.token,
11766
12239
  workspaceId: workspace.id
11767
12240
  });
@@ -11777,7 +12250,7 @@ const runWorkspaceInvite = async (context, workspaceRefArg, workspaceOverride, o
11777
12250
  writeInfo(context, `role: ${invitation.role}`);
11778
12251
  writeInfo(context, `expires: ${invitation.expiresAt}`);
11779
12252
  if (invitation.email) writeInfo(context, `email: ${invitation.email}`);
11780
- const link = `${commandContext.apiBaseUrl}/invite/${invitation.code}`;
12253
+ const link = `${DEFAULT_APP_URL}/join?code=${encodeURIComponent(invitation.code)}`;
11781
12254
  writeInfo(context, `link: ${link}`);
11782
12255
  if (context.copy) {
11783
12256
  if (copyToClipboard(link)) writeInfo(context, "Link copied to clipboard.");
@@ -11793,6 +12266,8 @@ Examples:
11793
12266
  donebear workspace members
11794
12267
  donebear workspace invitations
11795
12268
  donebear workspace invite --email user@example.com --role member
12269
+ donebear workspace invite --share --copy
12270
+ donebear workspace invite --share --project <project-id>
11796
12271
  `).action(async (_options, command) => {
11797
12272
  const context = contextFromCommand(command);
11798
12273
  await runWithErrorHandling(context, async () => {
@@ -11847,7 +12322,7 @@ Examples:
11847
12322
  await runWorkspaceInvitations(context, workspaceRef, workspaceOverride);
11848
12323
  });
11849
12324
  });
11850
- workspace.command("invite").description("Create an invitation for the current or specified workspace").argument("[workspace]", "Workspace id, slug, or exact name").option("--email <email>", "Email address to invite").option("--role <role>", "Role for the invitee: member|admin", "member").action(async (workspaceRef, options, command) => {
12325
+ workspace.command("invite").description("Create an invitation for the current or specified workspace").argument("[workspace]", "Workspace id, slug, or exact name").option("--email <email>", "Email address to invite").option("--share", "Create a reusable join link").option("--project <id>", "Scope a share link: joiners join the whole workspace as a member, get this project, and use a seat").option("--role <role>", "Role for the invitee: member|admin", "member").action(async (workspaceRef, options, command) => {
11851
12326
  const context = contextFromCommand(command);
11852
12327
  await runWithErrorHandling(context, async () => {
11853
12328
  await runWorkspaceInvite(context, workspaceRef, workspaceOverride, options);
@@ -11875,8 +12350,9 @@ With auth:
11875
12350
  donebear import things --dry-run --json
11876
12351
 
11877
12352
  Auth:
11878
- donebear auth login OAuth browser login
11879
- DONEBEAR_TOKEN=<token> Non-interactive / CI: set this env var
12353
+ donebear auth signup --email you@… Create an account (human verifies email)
12354
+ donebear auth login OAuth browser login
12355
+ DONEBEAR_TOKEN=<token> Non-interactive / CI: set this env var
11880
12356
 
11881
12357
  Workspace targeting:
11882
12358
  donebear workspace=myorg task list