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/AGENTS.md +2 -2
- package/README.md +9 -2
- package/dist/cli.mjs +686 -210
- package/dist/cli.mjs.map +1 -1
- package/dist/{context-store-BBEVWudm.mjs → context-store-BbZMKY0F.mjs} +3 -2
- package/dist/context-store-BbZMKY0F.mjs.map +1 -0
- package/dist/context-store-D8zfa5LS.mjs +3 -0
- package/dist/prompt--P7Y1FBL.mjs +3 -0
- package/dist/{prompt-GfcWO85b.mjs → prompt-B66eG-Zm.mjs} +16 -4
- package/dist/prompt-B66eG-Zm.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/context-store-BBEVWudm.mjs.map +0 -1
- package/dist/context-store-ClAx1T2N.mjs +0 -3
- package/dist/prompt-B7mDo4K7.mjs +0 -3
- package/dist/prompt-GfcWO85b.mjs.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as toCliError,
|
|
3
|
-
import { n as readStdin, t as promptForValue } from "./prompt-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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.
|
|
4662
|
-
|
|
4663
|
-
|
|
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
|
-
|
|
4668
|
-
|
|
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
|
|
4713
|
-
|
|
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 =
|
|
7813
|
-
return parsed instanceof
|
|
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
|
|
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:
|
|
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
|
|
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 ?
|
|
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 ===
|
|
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
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
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([
|
|
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
|
|
8977
|
-
return new Map(
|
|
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
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
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
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
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
|
|
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,
|
|
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
|
-
"
|
|
10037
|
-
"
|
|
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
|
|
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
|
-
|
|
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 = `${
|
|
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
|
|
11879
|
-
|
|
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
|