donebear 0.4.2 → 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 +8 -8
- package/README.md +9 -2
- package/dist/cli.mjs +879 -343
- 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 +2 -6
- 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
|
});
|
|
@@ -2143,9 +2165,21 @@ const parsePositiveInteger = (value, label) => {
|
|
|
2143
2165
|
};
|
|
2144
2166
|
//#endregion
|
|
2145
2167
|
//#region src/oauth-callback.ts
|
|
2168
|
+
const CALLBACK_HTML_HEADERS = {
|
|
2169
|
+
connection: "close",
|
|
2170
|
+
"content-type": "text/html; charset=utf-8"
|
|
2171
|
+
};
|
|
2146
2172
|
const successHtml = () => `<!doctype html><html><head><meta charset="utf-8"/><title>Done Bear CLI</title></head><body><h1>Authentication complete</h1><p>You can return to your terminal.</p></body></html>`;
|
|
2147
2173
|
const escapeHtml = (text) => text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
2148
2174
|
const errorHtml = (message) => `<!doctype html><html><head><meta charset="utf-8"/><title>Done Bear CLI</title></head><body><h1>Authentication failed</h1><p>${escapeHtml(message)}</p></body></html>`;
|
|
2175
|
+
const writeCallbackHtml = (request, response, statusCode, html, onFlushed) => {
|
|
2176
|
+
response.shouldKeepAlive = false;
|
|
2177
|
+
response.writeHead(statusCode, CALLBACK_HTML_HEADERS);
|
|
2178
|
+
response.end(html, () => {
|
|
2179
|
+
request.socket.destroy();
|
|
2180
|
+
onFlushed?.();
|
|
2181
|
+
});
|
|
2182
|
+
};
|
|
2149
2183
|
const waitForOAuthCode = async (options) => await new Promise((resolve, reject) => {
|
|
2150
2184
|
const host = options.redirectUrl.hostname;
|
|
2151
2185
|
const port = Number(options.redirectUrl.port);
|
|
@@ -2159,6 +2193,7 @@ const waitForOAuthCode = async (options) => await new Promise((resolve, reject)
|
|
|
2159
2193
|
if (settled) return;
|
|
2160
2194
|
settled = true;
|
|
2161
2195
|
clearTimeout(timeout);
|
|
2196
|
+
server.closeAllConnections();
|
|
2162
2197
|
server.close(() => {
|
|
2163
2198
|
if (result.error) {
|
|
2164
2199
|
reject(result.error);
|
|
@@ -2169,41 +2204,40 @@ const waitForOAuthCode = async (options) => await new Promise((resolve, reject)
|
|
|
2169
2204
|
};
|
|
2170
2205
|
const server = createServer((request, response) => {
|
|
2171
2206
|
if (!request.url) {
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2207
|
+
writeCallbackHtml(request, response, 400, errorHtml("Missing request URL"), () => {
|
|
2208
|
+
finish({ error: new CliError("OAuth callback is missing a request URL", EXIT_CODES.ERROR) });
|
|
2209
|
+
});
|
|
2175
2210
|
return;
|
|
2176
2211
|
}
|
|
2177
2212
|
const url = new URL(request.url, options.redirectUrl.origin);
|
|
2178
2213
|
if (url.pathname !== pathname) {
|
|
2179
|
-
|
|
2180
|
-
response.end(errorHtml("Invalid callback path"));
|
|
2214
|
+
writeCallbackHtml(request, response, 404, errorHtml("Invalid callback path"));
|
|
2181
2215
|
return;
|
|
2182
2216
|
}
|
|
2183
2217
|
const providerError = url.searchParams.get("error");
|
|
2184
2218
|
if (providerError) {
|
|
2185
2219
|
const description = url.searchParams.get("error_description") ?? providerError;
|
|
2186
|
-
response
|
|
2187
|
-
|
|
2188
|
-
|
|
2220
|
+
writeCallbackHtml(request, response, 400, errorHtml(description), () => {
|
|
2221
|
+
finish({ error: new CliError(`OAuth provider returned an error: ${description}`, EXIT_CODES.ERROR) });
|
|
2222
|
+
});
|
|
2189
2223
|
return;
|
|
2190
2224
|
}
|
|
2191
2225
|
if (url.searchParams.get("state") !== options.expectedState) {
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2226
|
+
writeCallbackHtml(request, response, 400, errorHtml("State verification failed"), () => {
|
|
2227
|
+
finish({ error: new CliError("OAuth state verification failed", EXIT_CODES.ERROR) });
|
|
2228
|
+
});
|
|
2195
2229
|
return;
|
|
2196
2230
|
}
|
|
2197
2231
|
const code = url.searchParams.get("code");
|
|
2198
2232
|
if (!code) {
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2233
|
+
writeCallbackHtml(request, response, 400, errorHtml("Authorization code was missing"), () => {
|
|
2234
|
+
finish({ error: new CliError("OAuth callback is missing an authorization code", EXIT_CODES.ERROR) });
|
|
2235
|
+
});
|
|
2202
2236
|
return;
|
|
2203
2237
|
}
|
|
2204
|
-
response
|
|
2205
|
-
|
|
2206
|
-
|
|
2238
|
+
writeCallbackHtml(request, response, 200, successHtml(), () => {
|
|
2239
|
+
finish({ code });
|
|
2240
|
+
});
|
|
2207
2241
|
});
|
|
2208
2242
|
server.on("error", (error) => {
|
|
2209
2243
|
finish({ error: new CliError(`Failed to start callback server on ${host}:${port}: ${error.message}`, EXIT_CODES.ERROR) });
|
|
@@ -2219,6 +2253,183 @@ const createOAuthState = () => randomBytes(24).toString("hex");
|
|
|
2219
2253
|
const createCodeVerifier = () => randomBytes(64).toString("base64url");
|
|
2220
2254
|
const createCodeChallenge = (verifier) => createHash("sha256").update(verifier).digest().toString("base64url");
|
|
2221
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
|
|
2222
2433
|
//#region src/commands/auth.ts
|
|
2223
2434
|
const runLogin = async (context, options) => {
|
|
2224
2435
|
const { authorizeUrl, tokenUrl } = buildOAuthUrls(resolveSupabaseConfig());
|
|
@@ -2321,6 +2532,7 @@ const getOptionalSupabaseConfig = () => {
|
|
|
2321
2532
|
const registerAuthCommands = (program) => {
|
|
2322
2533
|
const auth = program.command("auth").description("Authenticate donebear CLI").addHelpText("after", `
|
|
2323
2534
|
Examples:
|
|
2535
|
+
donebear auth signup --email person@example.com --json
|
|
2324
2536
|
donebear auth login
|
|
2325
2537
|
donebear auth login --no-open (print URL, don't open browser)
|
|
2326
2538
|
donebear auth status --json
|
|
@@ -2349,6 +2561,25 @@ Non-interactive / CI use:
|
|
|
2349
2561
|
await runLogin(context, options);
|
|
2350
2562
|
});
|
|
2351
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
|
+
});
|
|
2352
2583
|
auth.command("status").description("Check authentication status").action(async (_options, command) => {
|
|
2353
2584
|
const context = contextFromCommand(command);
|
|
2354
2585
|
await runWithErrorHandling(context, async () => {
|
|
@@ -2428,23 +2659,32 @@ const toTaskMarker = (task) => {
|
|
|
2428
2659
|
if (state === "archived") return "[-]";
|
|
2429
2660
|
return "[ ]";
|
|
2430
2661
|
};
|
|
2662
|
+
const completedOn = (task) => {
|
|
2663
|
+
if (task.completedAt === null) return null;
|
|
2664
|
+
const date = new Date(task.completedAt);
|
|
2665
|
+
return Number.isNaN(date.getTime()) ? null : formatDateOnlyEpoch(dateOnlyEpochFromLocalDate(date));
|
|
2666
|
+
};
|
|
2667
|
+
const deadlineDetail = (deadline) => deadline ? `due ${deadline}` : "";
|
|
2431
2668
|
const taskListHeaders = () => [
|
|
2432
2669
|
"number",
|
|
2433
2670
|
"title",
|
|
2434
2671
|
"state",
|
|
2435
|
-
"deadline"
|
|
2672
|
+
"deadline",
|
|
2673
|
+
"completed"
|
|
2436
2674
|
];
|
|
2437
2675
|
const taskListRows = (tasks) => tasks.map((task) => [
|
|
2438
2676
|
String(task.number),
|
|
2439
2677
|
task.title,
|
|
2440
2678
|
getTaskState(task),
|
|
2441
|
-
formatDateOnlyEpoch(task.deadlineAt) ?? ""
|
|
2679
|
+
formatDateOnlyEpoch(task.deadlineAt) ?? "",
|
|
2680
|
+
completedOn(task) ?? ""
|
|
2442
2681
|
]);
|
|
2443
2682
|
const taskListLines = (tasks) => tasks.map((task) => {
|
|
2444
2683
|
const marker = toTaskMarker(task);
|
|
2445
2684
|
const deadline = formatDateOnlyEpoch(task.deadlineAt);
|
|
2446
|
-
const
|
|
2447
|
-
|
|
2685
|
+
const completed = completedOn(task);
|
|
2686
|
+
const detail = completed ? `done ${completed}` : deadlineDetail(deadline);
|
|
2687
|
+
return `${marker} ${task.number} ${task.title}${detail ? ` ${detail}` : ""}`;
|
|
2448
2688
|
});
|
|
2449
2689
|
//#endregion
|
|
2450
2690
|
//#region src/confirm.ts
|
|
@@ -2466,29 +2706,29 @@ const confirmAction = (context, message) => {
|
|
|
2466
2706
|
});
|
|
2467
2707
|
};
|
|
2468
2708
|
//#endregion
|
|
2469
|
-
//#region ../../
|
|
2709
|
+
//#region ../../node_modules/uuid/dist-node/regex.js
|
|
2470
2710
|
var regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/i;
|
|
2471
2711
|
//#endregion
|
|
2472
|
-
//#region ../../
|
|
2712
|
+
//#region ../../node_modules/uuid/dist-node/validate.js
|
|
2473
2713
|
function validate(uuid) {
|
|
2474
2714
|
return typeof uuid === "string" && regex_default.test(uuid);
|
|
2475
2715
|
}
|
|
2476
2716
|
//#endregion
|
|
2477
|
-
//#region ../../
|
|
2717
|
+
//#region ../../node_modules/uuid/dist-node/parse.js
|
|
2478
2718
|
function parse(uuid) {
|
|
2479
2719
|
if (!validate(uuid)) throw TypeError("Invalid UUID");
|
|
2480
2720
|
let v;
|
|
2481
2721
|
return Uint8Array.of((v = parseInt(uuid.slice(0, 8), 16)) >>> 24, v >>> 16 & 255, v >>> 8 & 255, v & 255, (v = parseInt(uuid.slice(9, 13), 16)) >>> 8, v & 255, (v = parseInt(uuid.slice(14, 18), 16)) >>> 8, v & 255, (v = parseInt(uuid.slice(19, 23), 16)) >>> 8, v & 255, (v = parseInt(uuid.slice(24, 36), 16)) / 1099511627776 & 255, v / 4294967296 & 255, v >>> 24 & 255, v >>> 16 & 255, v >>> 8 & 255, v & 255);
|
|
2482
2722
|
}
|
|
2483
2723
|
//#endregion
|
|
2484
|
-
//#region ../../
|
|
2724
|
+
//#region ../../node_modules/uuid/dist-node/stringify.js
|
|
2485
2725
|
const byteToHex = [];
|
|
2486
2726
|
for (let i = 0; i < 256; ++i) byteToHex.push((i + 256).toString(16).slice(1));
|
|
2487
2727
|
function unsafeStringify(arr, offset = 0) {
|
|
2488
2728
|
return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
|
|
2489
2729
|
}
|
|
2490
2730
|
//#endregion
|
|
2491
|
-
//#region ../../
|
|
2731
|
+
//#region ../../node_modules/uuid/dist-node/v35.js
|
|
2492
2732
|
function stringToBytes(str) {
|
|
2493
2733
|
str = unescape(encodeURIComponent(str));
|
|
2494
2734
|
const bytes = new Uint8Array(str.length);
|
|
@@ -2517,14 +2757,14 @@ function v35(version, hash, value, namespace, buf, offset) {
|
|
|
2517
2757
|
return unsafeStringify(bytes);
|
|
2518
2758
|
}
|
|
2519
2759
|
//#endregion
|
|
2520
|
-
//#region ../../
|
|
2760
|
+
//#region ../../node_modules/uuid/dist-node/sha1.js
|
|
2521
2761
|
function sha1(bytes) {
|
|
2522
2762
|
if (Array.isArray(bytes)) bytes = Buffer.from(bytes);
|
|
2523
2763
|
else if (typeof bytes === "string") bytes = Buffer.from(bytes, "utf8");
|
|
2524
2764
|
return createHash("sha1").update(bytes).digest();
|
|
2525
2765
|
}
|
|
2526
2766
|
//#endregion
|
|
2527
|
-
//#region ../../
|
|
2767
|
+
//#region ../../node_modules/uuid/dist-node/v5.js
|
|
2528
2768
|
function v5(value, namespace, buf, offset) {
|
|
2529
2769
|
return v35(80, sha1, value, namespace, buf, offset);
|
|
2530
2770
|
}
|
|
@@ -2545,6 +2785,11 @@ const createTeamProjectId = (params) => createCompositeSyncId("TeamProject", [
|
|
|
2545
2785
|
params.teamId,
|
|
2546
2786
|
params.projectId
|
|
2547
2787
|
]);
|
|
2788
|
+
const createTeamMembershipId = (params) => createCompositeSyncId("TeamMembership", [
|
|
2789
|
+
params.workspaceId,
|
|
2790
|
+
params.teamId,
|
|
2791
|
+
params.userId
|
|
2792
|
+
]);
|
|
2548
2793
|
const createProjectMembershipId = (params) => createCompositeSyncId("ProjectMembership", [
|
|
2549
2794
|
params.workspaceId,
|
|
2550
2795
|
params.projectId,
|
|
@@ -2552,9 +2797,7 @@ const createProjectMembershipId = (params) => createCompositeSyncId("ProjectMemb
|
|
|
2552
2797
|
]);
|
|
2553
2798
|
//#endregion
|
|
2554
2799
|
//#region src/import/model-id.ts
|
|
2555
|
-
const IMPORT_ID_NAMESPACE = "b1f9f0ac-9f76-54ed-8eb0-7d6d3a0a0a31";
|
|
2556
2800
|
const IDEMPOTENCY_ID_NAMESPACE = "4c8af5e2-1d36-5a0b-9c47-2f81b3d6e955";
|
|
2557
|
-
const createDeterministicImportId = (params) => createDeterministicId(IMPORT_ID_NAMESPACE, `things:${params.workspaceId}:${params.modelName}:${params.sourceId}`);
|
|
2558
2801
|
const createIdempotentMutationId = (params) => createDeterministicId(IDEMPOTENCY_ID_NAMESPACE, `idem:${params.workspaceId}:${params.modelName}:${params.part}:${params.key}`);
|
|
2559
2802
|
//#endregion
|
|
2560
2803
|
//#region src/workspace-context.ts
|
|
@@ -2631,19 +2874,35 @@ const resolveWorkspace = async (context, options) => {
|
|
|
2631
2874
|
//#endregion
|
|
2632
2875
|
//#region src/commands/task.ts
|
|
2633
2876
|
const DEFAULT_TASK_LIST_LIMIT = 20;
|
|
2634
|
-
|
|
2877
|
+
/** The views `--when` can place a task in: the ones with start fields. */
|
|
2878
|
+
const ACTIVE_TASK_VIEWS = new Set([
|
|
2635
2879
|
"inbox",
|
|
2636
2880
|
"anytime",
|
|
2637
2881
|
"today",
|
|
2638
2882
|
"upcoming",
|
|
2639
|
-
"someday"
|
|
2883
|
+
"someday"
|
|
2884
|
+
]);
|
|
2885
|
+
/** The views `--view` can filter a list by, which adds the two read-only ones. */
|
|
2886
|
+
const VALID_TASK_VIEWS = new Set([
|
|
2887
|
+
...ACTIVE_TASK_VIEWS,
|
|
2640
2888
|
"logbook",
|
|
2641
2889
|
"trash"
|
|
2642
2890
|
]);
|
|
2643
2891
|
const parseTaskView = (value) => {
|
|
2644
2892
|
const normalized = value.trim().toLowerCase();
|
|
2645
2893
|
if (VALID_TASK_VIEWS.has(normalized)) return normalized;
|
|
2646
|
-
throw new Error(`Invalid view "${value}". Expected one of:
|
|
2894
|
+
throw new Error(`Invalid view "${value}". Expected one of: ${[...VALID_TASK_VIEWS].join(", ")}.`);
|
|
2895
|
+
};
|
|
2896
|
+
/**
|
|
2897
|
+
* The view a `--when` may name. Logbook and Trash are derived from state, not
|
|
2898
|
+
* from a start field, so there are no start fields to build for them: passing
|
|
2899
|
+
* either used to fall through `buildTaskStartFields` and quietly land the task
|
|
2900
|
+
* in Inbox. Completing and archiving have their own commands.
|
|
2901
|
+
*/
|
|
2902
|
+
const parseTaskWhenView = (value) => {
|
|
2903
|
+
const view = parseTaskView(value);
|
|
2904
|
+
if (view === "logbook" || view === "trash") throw new Error(`Invalid --when "${value}". Expected one of: ${[...ACTIVE_TASK_VIEWS].join(", ")}. Use "task done" to complete a task and "task archive" to archive one.`);
|
|
2905
|
+
return view;
|
|
2647
2906
|
};
|
|
2648
2907
|
const parseDeadline = (value) => parseCliDateOnlyToEpoch(value, "deadline date");
|
|
2649
2908
|
/**
|
|
@@ -2667,7 +2926,7 @@ const parseDeadline = (value) => parseCliDateOnlyToEpoch(value, "deadline date")
|
|
|
2667
2926
|
* there would make "typed nothing" indistinguishable from "typed --when today".
|
|
2668
2927
|
*/
|
|
2669
2928
|
const resolveAddView = (when, deadlineEpoch, hasContainer = false) => {
|
|
2670
|
-
if (when) return
|
|
2929
|
+
if (when) return parseTaskWhenView(when);
|
|
2671
2930
|
if (deadlineEpoch !== null) return "anytime";
|
|
2672
2931
|
return hasContainer ? "inbox" : "today";
|
|
2673
2932
|
};
|
|
@@ -2990,7 +3249,7 @@ const applyScalarEdits = (updates, options) => {
|
|
|
2990
3249
|
const description = options.description.trim();
|
|
2991
3250
|
updates.description = description.length > 0 ? description : null;
|
|
2992
3251
|
}
|
|
2993
|
-
if (typeof options.when === "string") Object.assign(updates, buildTaskStartFields(
|
|
3252
|
+
if (typeof options.when === "string") Object.assign(updates, buildTaskStartFields(parseTaskWhenView(options.when)));
|
|
2994
3253
|
if (options.clearDeadline) updates.deadlineAt = null;
|
|
2995
3254
|
else if (typeof options.deadline === "string") updates.deadlineAt = parseDeadline(options.deadline);
|
|
2996
3255
|
};
|
|
@@ -3155,33 +3414,7 @@ const runChecklistList = async (context, taskId, workspaceOption) => {
|
|
|
3155
3414
|
writeInfo(context, `Checklist for: ${task.title}`);
|
|
3156
3415
|
for (const item of sorted) writeInfo(context, `${item.completedAt === null ? "[ ]" : "[x]"} ${item.id.slice(0, 8)} ${item.title}`);
|
|
3157
3416
|
};
|
|
3158
|
-
const
|
|
3159
|
-
const task = program.command("task").alias("tasks").description("Manage tasks").addHelpText("after", `
|
|
3160
|
-
Examples:
|
|
3161
|
-
donebear task list --json
|
|
3162
|
-
donebear task add "Fix login bug" --when today --json
|
|
3163
|
-
donebear task add "Write tests" --deadline 2026-03-15 --project PROJ --json
|
|
3164
|
-
donebear task edit 12 --deadline 2026-03-05 --json
|
|
3165
|
-
donebear task done 12 --json
|
|
3166
|
-
donebear task list --state done --limit 50 --json
|
|
3167
|
-
|
|
3168
|
-
Date format: YYYY-MM-DD (e.g. 2026-03-15)
|
|
3169
|
-
Task number: workspace-scoped positive integer (e.g. 12)
|
|
3170
|
-
View buckets: inbox | anytime | today | upcoming | someday
|
|
3171
|
-
New tasks default to today; to anytime with --deadline and no --when; to
|
|
3172
|
-
inbox with --project or --team and no --when. Pass --when inbox to capture
|
|
3173
|
-
without scheduling.
|
|
3174
|
-
|
|
3175
|
-
Agent tip: run "donebear spec task add" for full option reference
|
|
3176
|
-
`).action(async (_options, command) => {
|
|
3177
|
-
const context = contextFromCommand(command);
|
|
3178
|
-
await runWithErrorHandling(context, async () => {
|
|
3179
|
-
await runTaskList(context, {
|
|
3180
|
-
limit: DEFAULT_TASK_LIST_LIMIT,
|
|
3181
|
-
state: "open"
|
|
3182
|
-
}, workspaceOverride);
|
|
3183
|
-
});
|
|
3184
|
-
});
|
|
3417
|
+
const registerTaskSubcommands = (task, workspaceOverride) => {
|
|
3185
3418
|
task.command("list").alias("ls").description("List tasks").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-s, --state <state>", "open|done|archived|all", "open").option("-n, --limit <count>", "Maximum number of tasks", (value) => parsePositiveInteger(value, "Limit"), DEFAULT_TASK_LIST_LIMIT).option("-q, --search <query>", "Filter by title or description").option("-v, --view <view>", "Filter by view (inbox|anytime|today|upcoming|someday|logbook|trash)").addHelpText("after", `
|
|
3186
3419
|
Examples:
|
|
3187
3420
|
donebear task list --json
|
|
@@ -3308,12 +3541,8 @@ without scheduling.
|
|
|
3308
3541
|
});
|
|
3309
3542
|
});
|
|
3310
3543
|
});
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
await runWithErrorHandling(context, async () => {
|
|
3314
|
-
await runChecklistList(context, taskId, options.workspace);
|
|
3315
|
-
});
|
|
3316
|
-
});
|
|
3544
|
+
};
|
|
3545
|
+
const registerChecklistSubcommands = (checklist) => {
|
|
3317
3546
|
checklist.command("add").description("Add a checklist item").argument("<task-number>", "Workspace-scoped task number").argument("<title>", "Item title").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, title, options, command) => {
|
|
3318
3547
|
const context = contextFromCommand(command);
|
|
3319
3548
|
await runWithErrorHandling(context, async () => {
|
|
@@ -3516,6 +3745,41 @@ without scheduling.
|
|
|
3516
3745
|
});
|
|
3517
3746
|
});
|
|
3518
3747
|
};
|
|
3748
|
+
const registerTaskCommands = (program, workspaceOverride) => {
|
|
3749
|
+
const task = program.command("task").alias("tasks").description("Manage tasks").addHelpText("after", `
|
|
3750
|
+
Examples:
|
|
3751
|
+
donebear task list --json
|
|
3752
|
+
donebear task add "Fix login bug" --when today --json
|
|
3753
|
+
donebear task add "Write tests" --deadline 2026-03-15 --project PROJ --json
|
|
3754
|
+
donebear task edit 12 --deadline 2026-03-05 --json
|
|
3755
|
+
donebear task done 12 --json
|
|
3756
|
+
donebear task list --state done --limit 50 --json
|
|
3757
|
+
|
|
3758
|
+
Date format: YYYY-MM-DD (e.g. 2026-03-15)
|
|
3759
|
+
Task number: workspace-scoped positive integer (e.g. 12)
|
|
3760
|
+
View buckets: inbox | anytime | today | upcoming | someday
|
|
3761
|
+
New tasks default to today; to anytime with --deadline and no --when; to
|
|
3762
|
+
inbox with --project or --team and no --when. Pass --when inbox to capture
|
|
3763
|
+
without scheduling.
|
|
3764
|
+
|
|
3765
|
+
Agent tip: run "donebear spec task add" for full option reference
|
|
3766
|
+
`).action(async (_options, command) => {
|
|
3767
|
+
const context = contextFromCommand(command);
|
|
3768
|
+
await runWithErrorHandling(context, async () => {
|
|
3769
|
+
await runTaskList(context, {
|
|
3770
|
+
limit: DEFAULT_TASK_LIST_LIMIT,
|
|
3771
|
+
state: "open"
|
|
3772
|
+
}, workspaceOverride);
|
|
3773
|
+
});
|
|
3774
|
+
});
|
|
3775
|
+
registerTaskSubcommands(task, workspaceOverride);
|
|
3776
|
+
registerChecklistSubcommands(task.command("checklist").alias("cl").description("Manage task checklist items").argument("<task-number>", "Workspace-scoped task number").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
|
|
3777
|
+
const context = contextFromCommand(command);
|
|
3778
|
+
await runWithErrorHandling(context, async () => {
|
|
3779
|
+
await runChecklistList(context, taskId, options.workspace);
|
|
3780
|
+
});
|
|
3781
|
+
}));
|
|
3782
|
+
};
|
|
3519
3783
|
//#endregion
|
|
3520
3784
|
//#region src/commands/comment.ts
|
|
3521
3785
|
/**
|
|
@@ -3947,7 +4211,7 @@ const checkAuth = async (context) => {
|
|
|
3947
4211
|
};
|
|
3948
4212
|
const checkWorkspace = async () => {
|
|
3949
4213
|
try {
|
|
3950
|
-
const { ensureLocalContext } = await import("./context-store-
|
|
4214
|
+
const { ensureLocalContext } = await import("./context-store-D8zfa5LS.mjs");
|
|
3951
4215
|
const localContext = await ensureLocalContext();
|
|
3952
4216
|
if (!localContext.workspaceId) return {
|
|
3953
4217
|
message: "No workspace selected (run `donebear workspace use`)",
|
|
@@ -4269,17 +4533,31 @@ state and start date rather than stored as a column:
|
|
|
4269
4533
|
|
|
4270
4534
|
- inbox captured, not yet placed
|
|
4271
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.
|
|
4272
4540
|
- upcoming start date in the future
|
|
4273
4541
|
- anytime actionable now, no date pressure
|
|
4274
4542
|
- someday parked, off every working list
|
|
4275
4543
|
- logbook completed
|
|
4276
4544
|
- trash archived
|
|
4277
4545
|
|
|
4278
|
-
\`--when\` sets the bucket on create and edit
|
|
4279
|
-
|
|
4546
|
+
\`--when\` sets the bucket on create and edit, and takes the first five only:
|
|
4547
|
+
logbook and trash come from a task's state, so \`task done\` and \`task archive\`
|
|
4548
|
+
put a task there, not \`--when\`. On list, \`--state open|done|archived|all\`
|
|
4549
|
+
reaches the Logbook and Trash, and so does \`--view logbook\` or \`--view trash\`,
|
|
4550
|
+
which widen the state for you. Do not pair \`--state done\` with a bucket view: a
|
|
4551
|
+
completed task's view is logbook, so \`--state done --view today\` matches
|
|
4552
|
+
nothing. What was finished today or this week comes off the completion date,
|
|
4553
|
+
not the ordering: the list prints it in the \`completed\` column, \`task show
|
|
4554
|
+
--json\` reports it as \`completedAtIso\`, and each row from \`task list --json\`
|
|
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.
|
|
4280
4558
|
|
|
4281
4559
|
donebear task add "Renew passport" --when upcoming --deadline 2030-03-01 --json
|
|
4282
|
-
donebear task list --
|
|
4560
|
+
donebear task list --view logbook --json
|
|
4283
4561
|
|
|
4284
4562
|
\`donebear task add\` defaults to today, so a task created without \`--when\`
|
|
4285
4563
|
starts today and shows up on the user's working list straight away. That is the
|
|
@@ -4334,8 +4612,16 @@ Clear a date with the explicit flag rather than passing an empty value:
|
|
|
4334
4612
|
donebear task edit 12 --clear-deadline --json
|
|
4335
4613
|
donebear project edit launch --clear-target-date --json
|
|
4336
4614
|
|
|
4337
|
-
|
|
4338
|
-
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.`,
|
|
4339
4625
|
title: "Dates and deadlines",
|
|
4340
4626
|
whenToUse: "Setting or clearing a due date, or choosing between a deadline and a start date."
|
|
4341
4627
|
},
|
|
@@ -4352,6 +4638,10 @@ never have to read a task first just to preserve its other fields.
|
|
|
4352
4638
|
- cleared use the paired flag: \`--clear-deadline\`, \`--clear-project\`,
|
|
4353
4639
|
\`--clear-team\`, \`--clear-description\`.
|
|
4354
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
|
+
|
|
4355
4645
|
Not on \`task edit\`:
|
|
4356
4646
|
|
|
4357
4647
|
- labels \`donebear label add|remove\`
|
|
@@ -4496,10 +4786,16 @@ a slightly wrong fit. Check \`donebear label list --json\` first and prefer an
|
|
|
4496
4786
|
existing label when it is a reasonable match. Teams are set up in the app;
|
|
4497
4787
|
there is no team create command.
|
|
4498
4788
|
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4789
|
+
A project is either visible to the whole workspace (the default) or private
|
|
4790
|
+
to its members. A private project and its tasks are invisible to everyone
|
|
4791
|
+
else — absent from \`project list\`, \`task list\` and \`search\`, not merely
|
|
4792
|
+
hidden. Visibility is set in the app.
|
|
4793
|
+
|
|
4794
|
+
On a workspace-visible project, \`donebear project join\` and \`donebear
|
|
4795
|
+
project leave\` only change which ones sit in your sidebar's Your projects
|
|
4796
|
+
list: neither grants nor removes access. On a private project those same rows
|
|
4797
|
+
are the access, so leaving one gives up your ability to see it. Both are
|
|
4798
|
+
idempotent, and creating a project already joins you to it.`,
|
|
4503
4799
|
title: "Projects, teams, and labels",
|
|
4504
4800
|
whenToUse: "Deciding where a task belongs, creating a project or label, or joining/leaving a project."
|
|
4505
4801
|
},
|
|
@@ -4539,9 +4835,21 @@ off unless the user named a second one:
|
|
|
4539
4835
|
Ids and names never cross workspaces. A missing task, project, or label is
|
|
4540
4836
|
missing from *this* workspace before it is deleted.
|
|
4541
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
|
+
|
|
4542
4843
|
Auth precedence: \`DONEBEAR_TOKEN\` in the environment, then the stored session
|
|
4543
|
-
from \`donebear auth login\`. Set the env var for
|
|
4544
|
-
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.
|
|
4545
4853
|
|
|
4546
4854
|
export DONEBEAR_TOKEN=<token>
|
|
4547
4855
|
donebear whoami --json
|
|
@@ -4551,6 +4859,15 @@ write fails with an explicit message rather than silently doing nothing — tell
|
|
|
4551
4859
|
the user they need a write-scoped key rather than trying to work around it.
|
|
4552
4860
|
\`donebear doctor --json\` reports which of these is actually in effect.
|
|
4553
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
|
+
|
|
4554
4871
|
## Task attachments (read-only)
|
|
4555
4872
|
|
|
4556
4873
|
Tasks can carry file/image attachments, capped at 10 MB per file and roughly
|
|
@@ -4571,6 +4888,9 @@ Three commands orient you, in order of cost:
|
|
|
4571
4888
|
donebear guide <topic> concepts and conventions, no auth
|
|
4572
4889
|
donebear context --json the live workspace: counts, recent tasks, projects
|
|
4573
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
|
+
|
|
4574
4894
|
\`donebear spec <command> <subcommand>\` narrows to one command's flags, output
|
|
4575
4895
|
shape, and examples. Read that before guessing at a flag.
|
|
4576
4896
|
|
|
@@ -4599,22 +4919,25 @@ workspace from the token.`,
|
|
|
4599
4919
|
|
|
4600
4920
|
Two importers exist, and they are not the same thing.
|
|
4601
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
|
+
|
|
4602
4929
|
**In the terminal**, \`donebear import things\` reads a copy of a Things 3
|
|
4603
4930
|
SQLite database and writes it into a workspace. It is the only import this CLI
|
|
4604
|
-
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.
|
|
4605
4933
|
|
|
4606
4934
|
On macOS it finds that database itself — \`donebear import things --dry-run\`
|
|
4607
|
-
with no other flags is the whole command.
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
most common way this import stalls. \`--database-url\` is for a database
|
|
4611
|
-
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.
|
|
4612
4938
|
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
Google Tasks, and plain CSV, JSON or iCal files. If a user asks how to move a
|
|
4616
|
-
backlog in from anything other than Things, that is where to send them; there
|
|
4617
|
-
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.
|
|
4618
4941
|
|
|
4619
4942
|
Each of those is a file the user exports and uploads — Todoist from Settings →
|
|
4620
4943
|
Backups, Google Tasks from Google Takeout, Microsoft To Do from a read-only
|
|
@@ -4623,6 +4946,13 @@ that may change: a read-only connect flow is being built so Done Bear can make
|
|
|
4623
4946
|
that call instead, so treat the Graph export as the current route and check the
|
|
4624
4947
|
import screen before promising a connection.
|
|
4625
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
|
+
|
|
4626
4956
|
The other direction is here though: \`donebear export\` writes a portable JSON
|
|
4627
4957
|
archive of a workspace, and the web importer restores one.
|
|
4628
4958
|
|
|
@@ -4655,9 +4985,8 @@ only what is new.
|
|
|
4655
4985
|
|
|
4656
4986
|
There is no undo for \`donebear import things\`; a run has to be unpicked by
|
|
4657
4987
|
hand. The web importer does offer one on its completion screen, but the receipt
|
|
4658
|
-
lives in the browser that ran the import
|
|
4659
|
-
|
|
4660
|
-
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.`,
|
|
4661
4990
|
title: "Importing from another app",
|
|
4662
4991
|
whenToUse: "Moving a backlog in from Things 3 or another task manager, or restoring an export."
|
|
4663
4992
|
}
|
|
@@ -4892,60 +5221,16 @@ const getUtcCalendarDayEndEpoch = (date) => {
|
|
|
4892
5221
|
return Date.UTC(parts.year, parts.month - 1, parts.day, 23, 59, 59, 999);
|
|
4893
5222
|
};
|
|
4894
5223
|
//#endregion
|
|
4895
|
-
//#region ../../packages/
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
* with.
|
|
4899
|
-
*
|
|
4900
|
-
* `manage-api` seeds the rows; `manage-frontend` and the CLI have to recognise
|
|
4901
|
-
* and trash the same rows when an import lands, and neither can read the ids
|
|
4902
|
-
* off the server. All three therefore re-derive them from the workspace id,
|
|
4903
|
-
* which only works while the namespace and the row counts agree — so they live
|
|
4904
|
-
* here rather than in each app.
|
|
4905
|
-
*
|
|
4906
|
-
* Unlike the composite ids in `./index`, these have no Swift port and no entry
|
|
4907
|
-
* in `test-vectors.json`: iOS never seeds or dismisses the sample project.
|
|
4908
|
-
*/
|
|
4909
|
-
const MEET_DONE_BEAR_ID_NAMESPACE = "b7e1d4a0-6c2f-4a8e-9d13-5f0c8a2b7e91";
|
|
4910
|
-
const createMeetDoneBearProjectId = (workspaceId) => createDeterministicId(MEET_DONE_BEAR_ID_NAMESPACE, `MeetDoneBear:Project:${workspaceId}`);
|
|
4911
|
-
const createMeetDoneBearHeadingId = (workspaceId, index) => createDeterministicId(MEET_DONE_BEAR_ID_NAMESPACE, `MeetDoneBear:Heading:${workspaceId}:${index}`);
|
|
4912
|
-
const createMeetDoneBearTaskId = (workspaceId, index) => createDeterministicId(MEET_DONE_BEAR_ID_NAMESPACE, `MeetDoneBear:Task:${workspaceId}:${index}`);
|
|
4913
|
-
//#endregion
|
|
4914
|
-
//#region src/import/meet-done-bear.ts
|
|
4915
|
-
/**
|
|
4916
|
-
* Sample rows that still exist in the pre-import bootstrap. Missing or
|
|
4917
|
-
* already-trashed rows are omitted so a later import does not fail.
|
|
4918
|
-
*/
|
|
4919
|
-
const listMeetDoneBearArchiveTargets = (options) => {
|
|
4920
|
-
const projectId = createMeetDoneBearProjectId(options.workspaceId);
|
|
4921
|
-
if (!options.existingProjectIds.has(projectId)) return [];
|
|
4922
|
-
const targets = [{
|
|
4923
|
-
modelId: projectId,
|
|
4924
|
-
modelName: "Project"
|
|
4925
|
-
}];
|
|
4926
|
-
const headingIds = options.existingHeadingIds;
|
|
4927
|
-
if (headingIds) for (let index = 0; index < 3; index += 1) {
|
|
4928
|
-
const modelId = createMeetDoneBearHeadingId(options.workspaceId, index);
|
|
4929
|
-
if (headingIds.has(modelId)) targets.push({
|
|
4930
|
-
modelId,
|
|
4931
|
-
modelName: "ProjectHeading"
|
|
4932
|
-
});
|
|
4933
|
-
}
|
|
4934
|
-
for (let index = 0; index < 9; index += 1) {
|
|
4935
|
-
const modelId = createMeetDoneBearTaskId(options.workspaceId, index);
|
|
4936
|
-
if (options.existingTaskIds.has(modelId)) targets.push({
|
|
4937
|
-
modelId,
|
|
4938
|
-
modelName: "Task"
|
|
4939
|
-
});
|
|
4940
|
-
}
|
|
4941
|
-
return targets;
|
|
4942
|
-
};
|
|
5224
|
+
//#region ../../packages/import-core/src/things/ids.ts
|
|
5225
|
+
const THINGS_IMPORT_ID_NAMESPACE = "b1f9f0ac-9f76-54ed-8eb0-7d6d3a0a0a31";
|
|
5226
|
+
const createDeterministicImportId = (params) => createDeterministicId(THINGS_IMPORT_ID_NAMESPACE, `things:${params.workspaceId}:${params.modelName}:${params.sourceId}`);
|
|
4943
5227
|
//#endregion
|
|
4944
|
-
//#region
|
|
5228
|
+
//#region ../../packages/import-core/src/things/labels.ts
|
|
4945
5229
|
const normalizeThingsLabelLookupKey = (value) => value.trim().toLocaleLowerCase();
|
|
5230
|
+
const normalizeThingsTeamLookupKey = (value) => value.trim().toLocaleLowerCase();
|
|
4946
5231
|
//#endregion
|
|
4947
|
-
//#region
|
|
4948
|
-
var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
5232
|
+
//#region ../../packages/import-core/src/things/values.ts
|
|
5233
|
+
var import_rrule = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
4949
5234
|
(function webpackUniversalModuleDefinition(root, factory) {
|
|
4950
5235
|
if (typeof exports === "object" && typeof module === "object") module.exports = factory();
|
|
4951
5236
|
else if (typeof define === "function" && define.amd) define([], factory);
|
|
@@ -7699,7 +7984,7 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
7699
7984
|
return __webpack_exports__;
|
|
7700
7985
|
})();
|
|
7701
7986
|
});
|
|
7702
|
-
})))();
|
|
7987
|
+
})))(), 1);
|
|
7703
7988
|
const PACKED_DATE_MULTIPLIER = 128;
|
|
7704
7989
|
const PACKED_DATE_DAY_MASK = 31;
|
|
7705
7990
|
const PACKED_DATE_MONTH_MASK = 15;
|
|
@@ -7759,7 +8044,8 @@ const combineThingsReminderAt = (options) => {
|
|
|
7759
8044
|
return baseInstant === null ? null : baseInstant + offset;
|
|
7760
8045
|
};
|
|
7761
8046
|
//#endregion
|
|
7762
|
-
//#region
|
|
8047
|
+
//#region ../../packages/import-core/src/things/recurrence.ts
|
|
8048
|
+
const { RRule, rrulestr } = typeof import_rrule.RRule === "function" ? import_rrule : import_rrule.default;
|
|
7763
8049
|
const THINGS_FREQUENCY_TO_RRULE = new Map([
|
|
7764
8050
|
[4, "YEARLY"],
|
|
7765
8051
|
[8, "MONTHLY"],
|
|
@@ -7800,8 +8086,8 @@ const parseOffsets = (xml) => {
|
|
|
7800
8086
|
};
|
|
7801
8087
|
const parseRRule = (rule) => {
|
|
7802
8088
|
try {
|
|
7803
|
-
const parsed =
|
|
7804
|
-
return parsed instanceof
|
|
8089
|
+
const parsed = rrulestr(rule);
|
|
8090
|
+
return parsed instanceof RRule ? parsed : null;
|
|
7805
8091
|
} catch {
|
|
7806
8092
|
return null;
|
|
7807
8093
|
}
|
|
@@ -7900,7 +8186,7 @@ const resolveThingsRepeatInstanceDate = (task, timeZone) => {
|
|
|
7900
8186
|
return decodeThingsPackedCalendarDate(task.startDate, repeatTimeZone) ?? decodeThingsPackedCalendarDate(task.nextInstanceStartDate, repeatTimeZone) ?? null;
|
|
7901
8187
|
};
|
|
7902
8188
|
//#endregion
|
|
7903
|
-
//#region
|
|
8189
|
+
//#region ../../packages/import-core/src/things/mapper.ts
|
|
7904
8190
|
const MAX_CHECKLIST_TITLE_LENGTH = 500;
|
|
7905
8191
|
const classifyTaskKind = (task) => {
|
|
7906
8192
|
if (task.type === 2) return "heading";
|
|
@@ -8007,14 +8293,14 @@ const pickCompletedAt = (task) => {
|
|
|
8007
8293
|
return decodeThingsUnixSeconds(task.stopDate) ?? decodeThingsUnixSeconds(task.userModificationDate) ?? decodeThingsUnixSeconds(task.creationDate);
|
|
8008
8294
|
};
|
|
8009
8295
|
const sortValue = (primary, fallback) => typeof primary === "number" ? primary : fallback;
|
|
8010
|
-
const textDecoder = new TextDecoder();
|
|
8296
|
+
const textDecoder$1 = new TextDecoder();
|
|
8011
8297
|
const encodeBase64 = (value) => {
|
|
8012
8298
|
let binary = "";
|
|
8013
8299
|
for (const byte of value) binary += String.fromCodePoint(byte);
|
|
8014
8300
|
return btoa(binary);
|
|
8015
8301
|
};
|
|
8016
8302
|
const sanitizeBinary = (value) => {
|
|
8017
|
-
const asText = textDecoder.decode(value);
|
|
8303
|
+
const asText = textDecoder$1.decode(value);
|
|
8018
8304
|
if (!asText.includes("�") && READABLE_TEXT_PATTERN.test(asText)) {
|
|
8019
8305
|
const trimmed = asText.trim();
|
|
8020
8306
|
if (trimmed.length > 0) return trimmed;
|
|
@@ -8174,19 +8460,25 @@ const buildChecklistMutation = (options) => {
|
|
|
8174
8460
|
};
|
|
8175
8461
|
const buildTeamActions = (ctx, areas) => {
|
|
8176
8462
|
const teams = [];
|
|
8463
|
+
const teamMemberships = [];
|
|
8464
|
+
const membershipIds = /* @__PURE__ */ new Set();
|
|
8177
8465
|
for (const area of [...areas].toSorted((a, b) => {
|
|
8178
8466
|
const indexComparison = compareNullableNumber(a.sortIndex, b.sortIndex);
|
|
8179
8467
|
if (indexComparison !== 0) return indexComparison;
|
|
8180
8468
|
return a.uuid.localeCompare(b.uuid);
|
|
8181
8469
|
})) {
|
|
8182
|
-
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({
|
|
8183
8474
|
modelName: "Team",
|
|
8184
8475
|
sourceId: area.uuid,
|
|
8185
8476
|
workspaceId: ctx.workspaceId
|
|
8186
8477
|
});
|
|
8478
|
+
ctx.existingTeamIdByName.set(teamLookupKey, teamId);
|
|
8187
8479
|
ctx.areaTeamIdBySourceId.set(area.uuid, teamId);
|
|
8188
8480
|
const areaTagTitles = (ctx.areaTagMap.get(area.uuid) ?? []).map((link) => ctx.tagTitleById.get(link.tagId) ?? null).filter((title) => typeof title === "string");
|
|
8189
|
-
teams.push({
|
|
8481
|
+
if (!existingTeamId) teams.push({
|
|
8190
8482
|
modelId: teamId,
|
|
8191
8483
|
modelName: "Team",
|
|
8192
8484
|
payload: {
|
|
@@ -8195,14 +8487,36 @@ const buildTeamActions = (ctx, areas) => {
|
|
|
8195
8487
|
description: appendMetadataBlock(null, ctx.metadataMode, buildAreaMetadata(area, areaTagTitles)),
|
|
8196
8488
|
icon: null,
|
|
8197
8489
|
key: ctx.teamKeys.get(area.uuid) ?? "AREA",
|
|
8198
|
-
name:
|
|
8490
|
+
name: teamName,
|
|
8199
8491
|
updatedAt: ctx.createdAtFallback,
|
|
8200
8492
|
workspaceId: ctx.workspaceId
|
|
8201
8493
|
},
|
|
8202
8494
|
sourceId: area.uuid
|
|
8203
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
|
+
}
|
|
8204
8515
|
}
|
|
8205
|
-
return
|
|
8516
|
+
return {
|
|
8517
|
+
teamMemberships,
|
|
8518
|
+
teams
|
|
8519
|
+
};
|
|
8206
8520
|
};
|
|
8207
8521
|
const buildLabelActions = (ctx, tags) => {
|
|
8208
8522
|
const labels = [];
|
|
@@ -8235,6 +8549,7 @@ const buildLabelActions = (ctx, tags) => {
|
|
|
8235
8549
|
};
|
|
8236
8550
|
const buildProjectActions = (ctx, sourceProjects) => {
|
|
8237
8551
|
const projects = [];
|
|
8552
|
+
const projectMemberships = [];
|
|
8238
8553
|
const teamProjects = [];
|
|
8239
8554
|
for (const project of [...sourceProjects].toSorted((a, b) => {
|
|
8240
8555
|
const indexComparison = compareNullableNumber(a.sortIndex, b.sortIndex);
|
|
@@ -8270,6 +8585,21 @@ const buildProjectActions = (ctx, sourceProjects) => {
|
|
|
8270
8585
|
},
|
|
8271
8586
|
sourceId: project.uuid
|
|
8272
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
|
+
});
|
|
8273
8603
|
if (project.areaId !== null) {
|
|
8274
8604
|
const teamId = ctx.areaTeamIdBySourceId.get(project.areaId);
|
|
8275
8605
|
if (teamId) teamProjects.push({
|
|
@@ -8290,6 +8620,7 @@ const buildProjectActions = (ctx, sourceProjects) => {
|
|
|
8290
8620
|
}
|
|
8291
8621
|
}
|
|
8292
8622
|
return {
|
|
8623
|
+
projectMemberships,
|
|
8293
8624
|
projects,
|
|
8294
8625
|
teamProjects
|
|
8295
8626
|
};
|
|
@@ -8542,6 +8873,7 @@ const buildThingsImportPlan = (options) => {
|
|
|
8542
8873
|
createdAtFallback: Date.now(),
|
|
8543
8874
|
creatorId: options.creatorId,
|
|
8544
8875
|
existingLabelIdByTitle: existingLabelsByTitle ?? /* @__PURE__ */ new Map(),
|
|
8876
|
+
existingTeamIdByName: options.existingTeamsByName ?? /* @__PURE__ */ new Map(),
|
|
8545
8877
|
headingIdBySourceId: /* @__PURE__ */ new Map(),
|
|
8546
8878
|
metadataMode: options.metadataMode,
|
|
8547
8879
|
projectIdBySourceId: /* @__PURE__ */ new Map(),
|
|
@@ -8558,15 +8890,18 @@ const buildThingsImportPlan = (options) => {
|
|
|
8558
8890
|
maxLength: 24
|
|
8559
8891
|
}),
|
|
8560
8892
|
timeZone: options.timeZone,
|
|
8561
|
-
todayDateOnly:
|
|
8893
|
+
todayDateOnly: localDateToDateOnlyEpoch(/* @__PURE__ */ new Date()),
|
|
8562
8894
|
workspaceId
|
|
8563
8895
|
};
|
|
8564
|
-
const teams = options.skipTeams ?
|
|
8896
|
+
const { teamMemberships, teams } = options.skipTeams ? {
|
|
8897
|
+
teamMemberships: [],
|
|
8898
|
+
teams: []
|
|
8899
|
+
} : buildTeamActions(ctx, snapshot.areas);
|
|
8565
8900
|
const labels = buildLabelActions(ctx, snapshot.tags);
|
|
8566
8901
|
const sourceProjects = snapshot.tasks.filter((task) => classifyTaskKind(task) === "project");
|
|
8567
8902
|
const sourceHeadings = snapshot.tasks.filter((task) => classifyTaskKind(task) === "heading");
|
|
8568
8903
|
const sourceTasks = snapshot.tasks.filter((task) => classifyTaskKind(task) === "task");
|
|
8569
|
-
const { projects, teamProjects } = buildProjectActions(ctx, sourceProjects);
|
|
8904
|
+
const { projectMemberships, projects, teamProjects } = buildProjectActions(ctx, sourceProjects);
|
|
8570
8905
|
const headings = buildHeadingActions(ctx, sourceHeadings);
|
|
8571
8906
|
const { tasks, taskLabels, checklistItems } = buildTaskActions(ctx, sourceTasks);
|
|
8572
8907
|
const phases = [
|
|
@@ -8574,6 +8909,10 @@ const buildThingsImportPlan = (options) => {
|
|
|
8574
8909
|
items: teams,
|
|
8575
8910
|
name: "teams"
|
|
8576
8911
|
},
|
|
8912
|
+
{
|
|
8913
|
+
items: teamMemberships,
|
|
8914
|
+
name: "teamMemberships"
|
|
8915
|
+
},
|
|
8577
8916
|
{
|
|
8578
8917
|
items: labels,
|
|
8579
8918
|
name: "labels"
|
|
@@ -8582,6 +8921,10 @@ const buildThingsImportPlan = (options) => {
|
|
|
8582
8921
|
items: projects,
|
|
8583
8922
|
name: "projects"
|
|
8584
8923
|
},
|
|
8924
|
+
{
|
|
8925
|
+
items: projectMemberships,
|
|
8926
|
+
name: "projectMemberships"
|
|
8927
|
+
},
|
|
8585
8928
|
{
|
|
8586
8929
|
items: teamProjects,
|
|
8587
8930
|
name: "teamProjects"
|
|
@@ -8623,11 +8966,208 @@ const buildThingsImportPlan = (options) => {
|
|
|
8623
8966
|
};
|
|
8624
8967
|
};
|
|
8625
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
|
|
9118
|
+
//#region ../../packages/sync-ids/src/meet-done-bear.ts
|
|
9119
|
+
/**
|
|
9120
|
+
* Ids for the "Meet Done Bear" sample project every new workspace is seeded
|
|
9121
|
+
* with.
|
|
9122
|
+
*
|
|
9123
|
+
* `manage-api` seeds the rows; `manage-frontend` and the CLI have to recognise
|
|
9124
|
+
* and trash the same rows when an import lands, and neither can read the ids
|
|
9125
|
+
* off the server. All three therefore re-derive them from the workspace id,
|
|
9126
|
+
* which only works while the namespace and the row counts agree — so they live
|
|
9127
|
+
* here rather than in each app.
|
|
9128
|
+
*
|
|
9129
|
+
* iOS ports `createMeetDoneBearProjectId` / `isMeetDoneBearProjectId` in
|
|
9130
|
+
* `DeterministicIds.swift` so Quiet Invite can ignore seed tasks. There is
|
|
9131
|
+
* still no vector section — dedicated Swift tests pin the same namespace.
|
|
9132
|
+
*/
|
|
9133
|
+
const MEET_DONE_BEAR_ID_NAMESPACE = "b7e1d4a0-6c2f-4a8e-9d13-5f0c8a2b7e91";
|
|
9134
|
+
const createMeetDoneBearProjectId = (workspaceId) => createDeterministicId(MEET_DONE_BEAR_ID_NAMESPACE, `MeetDoneBear:Project:${workspaceId}`);
|
|
9135
|
+
const createMeetDoneBearHeadingId = (workspaceId, index) => createDeterministicId(MEET_DONE_BEAR_ID_NAMESPACE, `MeetDoneBear:Heading:${workspaceId}:${index}`);
|
|
9136
|
+
const createMeetDoneBearTaskId = (workspaceId, index) => createDeterministicId(MEET_DONE_BEAR_ID_NAMESPACE, `MeetDoneBear:Task:${workspaceId}:${index}`);
|
|
9137
|
+
//#endregion
|
|
9138
|
+
//#region src/import/meet-done-bear.ts
|
|
9139
|
+
/**
|
|
9140
|
+
* Sample rows that still exist in the pre-import bootstrap. Missing or
|
|
9141
|
+
* already-trashed rows are omitted so a later import does not fail.
|
|
9142
|
+
*/
|
|
9143
|
+
const listMeetDoneBearArchiveTargets = (options) => {
|
|
9144
|
+
const projectId = createMeetDoneBearProjectId(options.workspaceId);
|
|
9145
|
+
if (!options.existingProjectIds.has(projectId)) return [];
|
|
9146
|
+
const targets = [{
|
|
9147
|
+
modelId: projectId,
|
|
9148
|
+
modelName: "Project"
|
|
9149
|
+
}];
|
|
9150
|
+
const headingIds = options.existingHeadingIds;
|
|
9151
|
+
if (headingIds) for (let index = 0; index < 3; index += 1) {
|
|
9152
|
+
const modelId = createMeetDoneBearHeadingId(options.workspaceId, index);
|
|
9153
|
+
if (headingIds.has(modelId)) targets.push({
|
|
9154
|
+
modelId,
|
|
9155
|
+
modelName: "ProjectHeading"
|
|
9156
|
+
});
|
|
9157
|
+
}
|
|
9158
|
+
for (let index = 0; index < 9; index += 1) {
|
|
9159
|
+
const modelId = createMeetDoneBearTaskId(options.workspaceId, index);
|
|
9160
|
+
if (options.existingTaskIds.has(modelId)) targets.push({
|
|
9161
|
+
modelId,
|
|
9162
|
+
modelName: "Task"
|
|
9163
|
+
});
|
|
9164
|
+
}
|
|
9165
|
+
return targets;
|
|
9166
|
+
};
|
|
9167
|
+
//#endregion
|
|
8626
9168
|
//#region src/import/things-sqlite.ts
|
|
8627
9169
|
const THINGS_CONTAINER_PATH = join(homedir(), "Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac");
|
|
8628
9170
|
const THINGS_BUNDLE_NAME = "Things Database.thingsdatabase";
|
|
8629
|
-
const THINGS_SQLITE_FILE = "main.sqlite";
|
|
8630
|
-
const LEGACY_THINGS_SQLITE_FILE = "Things.sqlite3";
|
|
8631
9171
|
const URL_SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/iu;
|
|
8632
9172
|
const isUrlLike = (value) => URL_SCHEME_PATTERN.test(value);
|
|
8633
9173
|
const resolveInputPath = (databaseUrl) => {
|
|
@@ -8640,7 +9180,7 @@ const resolveInputPath = (databaseUrl) => {
|
|
|
8640
9180
|
const expandThingsDatabasePath = (inputPath) => {
|
|
8641
9181
|
const resolvedPath = resolve(inputPath);
|
|
8642
9182
|
const lastSegment = basename(resolvedPath);
|
|
8643
|
-
if (lastSegment ===
|
|
9183
|
+
if (lastSegment === "main.sqlite" || lastSegment === "Things.sqlite3") return resolvedPath;
|
|
8644
9184
|
if (lastSegment === THINGS_BUNDLE_NAME) return join(resolvedPath, THINGS_SQLITE_FILE);
|
|
8645
9185
|
if (lastSegment.startsWith("ThingsData-")) return join(resolvedPath, THINGS_BUNDLE_NAME, THINGS_SQLITE_FILE);
|
|
8646
9186
|
return resolvedPath;
|
|
@@ -8685,130 +9225,6 @@ const getTableColumns = (database, tableName) => {
|
|
|
8685
9225
|
if (!hasTable(database, tableName)) return [];
|
|
8686
9226
|
return database.prepare(`PRAGMA table_info(${tableName})`).all().map((row) => typeof row.name === "string" ? row.name : null).filter((name) => name !== null);
|
|
8687
9227
|
};
|
|
8688
|
-
const toStringValue = (value) => {
|
|
8689
|
-
if (typeof value === "string") {
|
|
8690
|
-
const trimmed = value.trim();
|
|
8691
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
8692
|
-
}
|
|
8693
|
-
if (Buffer.isBuffer(value)) {
|
|
8694
|
-
const text = value.toString("utf-8").trim();
|
|
8695
|
-
return text.length > 0 ? text : null;
|
|
8696
|
-
}
|
|
8697
|
-
return null;
|
|
8698
|
-
};
|
|
8699
|
-
const toNumberValue = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
8700
|
-
const toBooleanValue = (value) => {
|
|
8701
|
-
const numeric = toNumberValue(value);
|
|
8702
|
-
return numeric === null ? null : numeric !== 0;
|
|
8703
|
-
};
|
|
8704
|
-
const normalizeTaskRecord = (row) => {
|
|
8705
|
-
const uuid = toStringValue(row.uuid);
|
|
8706
|
-
if (!uuid) return null;
|
|
8707
|
-
return {
|
|
8708
|
-
afterCompletionReferenceDate: toNumberValue(row.rt1_afterCompletionReferenceDate ?? row.afterCompletionReferenceDate),
|
|
8709
|
-
areaId: toStringValue(row.area),
|
|
8710
|
-
contactId: toStringValue(row.contact ?? row.delegate),
|
|
8711
|
-
creationDate: toNumberValue(row.creationDate),
|
|
8712
|
-
deadline: toNumberValue(row.deadline ?? row.dueDate),
|
|
8713
|
-
deadlineOffset: toNumberValue(row.t2_deadlineOffset ?? row.dueDateOffset),
|
|
8714
|
-
deadlineSuppressionDate: toNumberValue(row.deadlineSuppressionDate ?? row.dueDateSuppressionDate),
|
|
8715
|
-
headingId: toStringValue(row.heading ?? row.actionGroup),
|
|
8716
|
-
instanceCreationCount: toNumberValue(row.rt1_instanceCreationCount ?? row.instanceCreationCount),
|
|
8717
|
-
instanceCreationPaused: toBooleanValue(row.rt1_instanceCreationPaused ?? row.instanceCreationPaused) ?? false,
|
|
8718
|
-
instanceCreationStartDate: toNumberValue(row.rt1_instanceCreationStartDate ?? row.instanceCreationStartDate),
|
|
8719
|
-
lastReminderInteractionDate: toNumberValue(row.lastReminderInteractionDate ?? row.lastAlarmInteractionDate),
|
|
8720
|
-
nextInstanceStartDate: toNumberValue(row.rt1_nextInstanceStartDate ?? row.nextInstanceStartDate),
|
|
8721
|
-
notes: toStringValue(row.notes),
|
|
8722
|
-
projectId: toStringValue(row.project),
|
|
8723
|
-
raw: row,
|
|
8724
|
-
recurrenceRuleRaw: toStringValue(row.rt1_recurrenceRule ?? row.recurrenceRule),
|
|
8725
|
-
reminderTime: toNumberValue(row.reminderTime),
|
|
8726
|
-
repeatTemplateSourceId: toStringValue(row.rt1_repeatingTemplate ?? row.repeatingTemplate),
|
|
8727
|
-
repeaterMigrationDate: toNumberValue(row.repeaterMigrationDate),
|
|
8728
|
-
repeaterRaw: toStringValue(row.repeater),
|
|
8729
|
-
sortIndex: toNumberValue(row.index),
|
|
8730
|
-
start: toNumberValue(row.start),
|
|
8731
|
-
startBucket: toNumberValue(row.startBucket),
|
|
8732
|
-
startDate: toNumberValue(row.startDate),
|
|
8733
|
-
status: toNumberValue(row.status),
|
|
8734
|
-
stopDate: toNumberValue(row.stopDate),
|
|
8735
|
-
title: toStringValue(row.title),
|
|
8736
|
-
todayIndex: toNumberValue(row.todayIndex),
|
|
8737
|
-
todayIndexReferenceDate: toNumberValue(row.todayIndexReferenceDate),
|
|
8738
|
-
trashed: toBooleanValue(row.trashed) ?? false,
|
|
8739
|
-
type: toNumberValue(row.type),
|
|
8740
|
-
userModificationDate: toNumberValue(row.userModificationDate),
|
|
8741
|
-
uuid
|
|
8742
|
-
};
|
|
8743
|
-
};
|
|
8744
|
-
const normalizeAreaRecord = (row) => {
|
|
8745
|
-
const uuid = toStringValue(row.uuid);
|
|
8746
|
-
if (!uuid) return null;
|
|
8747
|
-
return {
|
|
8748
|
-
raw: row,
|
|
8749
|
-
sortIndex: toNumberValue(row.index),
|
|
8750
|
-
title: toStringValue(row.title),
|
|
8751
|
-
uuid,
|
|
8752
|
-
visible: toBooleanValue(row.visible)
|
|
8753
|
-
};
|
|
8754
|
-
};
|
|
8755
|
-
const normalizeTagRecord = (row) => {
|
|
8756
|
-
const uuid = toStringValue(row.uuid);
|
|
8757
|
-
if (!uuid) return null;
|
|
8758
|
-
return {
|
|
8759
|
-
parentId: toStringValue(row.parent),
|
|
8760
|
-
raw: row,
|
|
8761
|
-
shortcut: toStringValue(row.shortcut),
|
|
8762
|
-
sortIndex: toNumberValue(row.index),
|
|
8763
|
-
title: toStringValue(row.title),
|
|
8764
|
-
usedDate: toNumberValue(row.usedDate),
|
|
8765
|
-
uuid
|
|
8766
|
-
};
|
|
8767
|
-
};
|
|
8768
|
-
const normalizeChecklistItemRecord = (row) => {
|
|
8769
|
-
const uuid = toStringValue(row.uuid);
|
|
8770
|
-
if (!uuid) return null;
|
|
8771
|
-
return {
|
|
8772
|
-
creationDate: toNumberValue(row.creationDate),
|
|
8773
|
-
raw: row,
|
|
8774
|
-
sortIndex: toNumberValue(row.index),
|
|
8775
|
-
status: toNumberValue(row.status),
|
|
8776
|
-
stopDate: toNumberValue(row.stopDate),
|
|
8777
|
-
taskId: toStringValue(row.task),
|
|
8778
|
-
title: toStringValue(row.title),
|
|
8779
|
-
userModificationDate: toNumberValue(row.userModificationDate),
|
|
8780
|
-
uuid
|
|
8781
|
-
};
|
|
8782
|
-
};
|
|
8783
|
-
const normalizeTaskTagRecord = (row) => {
|
|
8784
|
-
const taskId = toStringValue(row.tasks);
|
|
8785
|
-
const tagId = toStringValue(row.tags);
|
|
8786
|
-
return taskId && tagId ? {
|
|
8787
|
-
tagId,
|
|
8788
|
-
taskId
|
|
8789
|
-
} : null;
|
|
8790
|
-
};
|
|
8791
|
-
const normalizeAreaTagRecord = (row) => {
|
|
8792
|
-
const areaId = toStringValue(row.areas);
|
|
8793
|
-
const tagId = toStringValue(row.tags);
|
|
8794
|
-
return areaId && tagId ? {
|
|
8795
|
-
areaId,
|
|
8796
|
-
tagId
|
|
8797
|
-
} : null;
|
|
8798
|
-
};
|
|
8799
|
-
const normalizeContactRecord = (row) => {
|
|
8800
|
-
const uuid = toStringValue(row.uuid);
|
|
8801
|
-
if (!uuid) return null;
|
|
8802
|
-
return {
|
|
8803
|
-
appleAddressBookId: toStringValue(row.appleAddressBookId),
|
|
8804
|
-
displayName: toStringValue(row.displayName),
|
|
8805
|
-
emails: toStringValue(row.emails),
|
|
8806
|
-
firstName: toStringValue(row.firstName),
|
|
8807
|
-
lastName: toStringValue(row.lastName),
|
|
8808
|
-
raw: row,
|
|
8809
|
-
uuid
|
|
8810
|
-
};
|
|
8811
|
-
};
|
|
8812
9228
|
const resolveThingsDatabasePath = (databaseUrl) => expandThingsDatabasePath(resolveInputPath(databaseUrl));
|
|
8813
9229
|
const resolveDefaultThingsDatabasePath = () => {
|
|
8814
9230
|
const resolved = findLatestThingsDatabasePath();
|
|
@@ -8823,17 +9239,11 @@ const loadThingsDatabase = (databaseUrl) => {
|
|
|
8823
9239
|
readonly: true
|
|
8824
9240
|
});
|
|
8825
9241
|
try {
|
|
8826
|
-
return {
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
databasePath,
|
|
8832
|
-
tags: hasTable(database, "TMTag") ? readRows(database, "TMTag").map(normalizeTagRecord).filter((record) => record !== null) : [],
|
|
8833
|
-
taskColumns: getTableColumns(database, "TMTask"),
|
|
8834
|
-
taskTags: hasTable(database, "TMTaskTag") ? readRows(database, "TMTaskTag").map(normalizeTaskTagRecord).filter((record) => record !== null) : [],
|
|
8835
|
-
tasks: readRows(database, "TMTask").map(normalizeTaskRecord).filter((record) => record !== null)
|
|
8836
|
-
};
|
|
9242
|
+
return buildThingsDatabaseSnapshot({
|
|
9243
|
+
getTableColumns: (tableName) => getTableColumns(database, tableName),
|
|
9244
|
+
hasTable: (tableName) => hasTable(database, tableName),
|
|
9245
|
+
readRows: (tableName) => readRows(database, tableName)
|
|
9246
|
+
}, databasePath);
|
|
8837
9247
|
} finally {
|
|
8838
9248
|
database.close();
|
|
8839
9249
|
}
|
|
@@ -8841,7 +9251,15 @@ const loadThingsDatabase = (databaseUrl) => {
|
|
|
8841
9251
|
//#endregion
|
|
8842
9252
|
//#region src/import/things-runner.ts
|
|
8843
9253
|
const WORKSPACE_SCOPED_MODELS = SYNC_CONTENT_MODELS;
|
|
8844
|
-
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"];
|
|
8845
9263
|
const chunk = (items, size) => {
|
|
8846
9264
|
const result = [];
|
|
8847
9265
|
for (let index = 0; index < items.length; index += size) result.push(items.slice(index, index + size));
|
|
@@ -8885,9 +9303,9 @@ const loadWorkspaceScopedRowsByModel = async (context, options) => {
|
|
|
8885
9303
|
};
|
|
8886
9304
|
const loadExistingImportState = async (options) => {
|
|
8887
9305
|
let modelNames;
|
|
8888
|
-
if (options.dryRun) modelNames = ["Label"];
|
|
8889
|
-
else if (options.skipTeams) modelNames = WORKSPACE_SCOPED_MODELS.filter((m) => m !== "Team" && m !== "TeamProject");
|
|
8890
|
-
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];
|
|
8891
9309
|
const rowsByModel = await loadWorkspaceScopedRowsByModel(options.context, {
|
|
8892
9310
|
apiBaseUrl: options.commandContext.apiBaseUrl,
|
|
8893
9311
|
modelNames,
|
|
@@ -8896,6 +9314,7 @@ const loadExistingImportState = async (options) => {
|
|
|
8896
9314
|
});
|
|
8897
9315
|
return {
|
|
8898
9316
|
existingLabelIdsByTitle: buildExistingLabelIdsByTitle(rowsByModel),
|
|
9317
|
+
existingTeamIdsByName: buildExistingTeamIdsByName(rowsByModel),
|
|
8899
9318
|
rowsByModel
|
|
8900
9319
|
};
|
|
8901
9320
|
};
|
|
@@ -8913,9 +9332,23 @@ const buildExistingLabelIdsByTitle = (rowsByModel) => {
|
|
|
8913
9332
|
}
|
|
8914
9333
|
return labelsByTitle;
|
|
8915
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
|
+
};
|
|
8916
9349
|
const buildExistingIds = (rowsByModel) => {
|
|
8917
|
-
const
|
|
8918
|
-
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)]));
|
|
8919
9352
|
};
|
|
8920
9353
|
const buildTransactions = (phaseItems, options) => {
|
|
8921
9354
|
const transactions = [];
|
|
@@ -8933,6 +9366,7 @@ const buildTransactions = (phaseItems, options) => {
|
|
|
8933
9366
|
if (action === "INSERT") {
|
|
8934
9367
|
inserted += 1;
|
|
8935
9368
|
existingIds.add(item.modelId);
|
|
9369
|
+
options.existingIds.set(item.modelName, existingIds);
|
|
8936
9370
|
} else updated += 1;
|
|
8937
9371
|
transactions.push({
|
|
8938
9372
|
action,
|
|
@@ -9018,6 +9452,7 @@ const runThingsImport = async (options) => {
|
|
|
9018
9452
|
const plan = buildThingsImportPlan({
|
|
9019
9453
|
creatorId: options.creatorId,
|
|
9020
9454
|
existingLabelsByTitle: existingState.existingLabelIdsByTitle,
|
|
9455
|
+
existingTeamsByName: existingState.existingTeamIdsByName,
|
|
9021
9456
|
metadataMode: options.metadataMode,
|
|
9022
9457
|
skipTeams: options.skipTeams,
|
|
9023
9458
|
snapshot,
|
|
@@ -9246,6 +9681,30 @@ const runLabelCreate = async (context, title, options) => {
|
|
|
9246
9681
|
}
|
|
9247
9682
|
writeSuccess(context, `Created label: ${title.trim()}`);
|
|
9248
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
|
+
};
|
|
9249
9708
|
const runLabelAdd = async (context, taskRef, labelRef, options) => {
|
|
9250
9709
|
const commandContext = await resolveCommandContext(context);
|
|
9251
9710
|
const workspaceRef = normalizeWorkspaceRef(options.workspace);
|
|
@@ -9263,19 +9722,13 @@ const runLabelAdd = async (context, taskRef, labelRef, options) => {
|
|
|
9263
9722
|
baseUrl: commandContext.apiBaseUrl,
|
|
9264
9723
|
request: {
|
|
9265
9724
|
batchId: randomUUID(),
|
|
9266
|
-
transactions: [{
|
|
9725
|
+
transactions: [buildTaskLabelTransaction({
|
|
9267
9726
|
action: "INSERT",
|
|
9268
9727
|
clientId: commandContext.localContext.clientId,
|
|
9269
|
-
|
|
9270
|
-
|
|
9271
|
-
|
|
9272
|
-
|
|
9273
|
-
createdAt: Date.now(),
|
|
9274
|
-
labelId: label.id,
|
|
9275
|
-
taskId: task.id,
|
|
9276
|
-
workspaceId: task.workspaceId
|
|
9277
|
-
}
|
|
9278
|
-
}]
|
|
9728
|
+
labelId: label.id,
|
|
9729
|
+
taskId: task.id,
|
|
9730
|
+
workspaceId: task.workspaceId
|
|
9731
|
+
})]
|
|
9279
9732
|
},
|
|
9280
9733
|
token: commandContext.token
|
|
9281
9734
|
});
|
|
@@ -9307,17 +9760,13 @@ const runLabelRemove = async (context, taskRef, labelRef, options) => {
|
|
|
9307
9760
|
baseUrl: commandContext.apiBaseUrl,
|
|
9308
9761
|
request: {
|
|
9309
9762
|
batchId: randomUUID(),
|
|
9310
|
-
transactions: [{
|
|
9763
|
+
transactions: [buildTaskLabelTransaction({
|
|
9311
9764
|
action: "DELETE",
|
|
9312
9765
|
clientId: commandContext.localContext.clientId,
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
labelId: label.id,
|
|
9318
|
-
taskId: task.id
|
|
9319
|
-
}
|
|
9320
|
-
}]
|
|
9766
|
+
labelId: label.id,
|
|
9767
|
+
taskId: task.id,
|
|
9768
|
+
workspaceId: task.workspaceId
|
|
9769
|
+
})]
|
|
9321
9770
|
},
|
|
9322
9771
|
token: commandContext.token
|
|
9323
9772
|
});
|
|
@@ -9522,7 +9971,7 @@ const resolveProjectName = async (context, rawName) => {
|
|
|
9522
9971
|
const trimmed = rawName.trim();
|
|
9523
9972
|
if (trimmed.length > 0) return trimmed;
|
|
9524
9973
|
}
|
|
9525
|
-
const { promptForValue } = await import("./prompt
|
|
9974
|
+
const { promptForValue } = await import("./prompt--P7Y1FBL.mjs");
|
|
9526
9975
|
const prompted = await promptForValue(context, "Project name");
|
|
9527
9976
|
if (prompted) return prompted;
|
|
9528
9977
|
throw new CliError("Project name is required.", EXIT_CODES.VALIDATION, "Provide a name: donebear project add \"My project\"");
|
|
@@ -9958,7 +10407,7 @@ Examples:
|
|
|
9958
10407
|
//#endregion
|
|
9959
10408
|
//#region src/commands/spec.ts
|
|
9960
10409
|
const OVERVIEW = {
|
|
9961
|
-
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>",
|
|
9962
10411
|
commands: {
|
|
9963
10412
|
"api-key": {
|
|
9964
10413
|
description: "Manage personal API keys",
|
|
@@ -9974,8 +10423,9 @@ const OVERVIEW = {
|
|
|
9974
10423
|
quick_example: "donebear auth status --json",
|
|
9975
10424
|
subcommands: [
|
|
9976
10425
|
"login",
|
|
9977
|
-
"
|
|
9978
|
-
"
|
|
10426
|
+
"logout",
|
|
10427
|
+
"signup",
|
|
10428
|
+
"status"
|
|
9979
10429
|
]
|
|
9980
10430
|
},
|
|
9981
10431
|
comment: {
|
|
@@ -10205,6 +10655,44 @@ const COMMAND_SPECS = {
|
|
|
10205
10655
|
},
|
|
10206
10656
|
usage: "donebear auth login [options]"
|
|
10207
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
|
+
},
|
|
10208
10696
|
"auth status": {
|
|
10209
10697
|
command: "auth status",
|
|
10210
10698
|
examples: ["donebear auth status --json"],
|
|
@@ -10878,17 +11366,16 @@ const COMMAND_SPECS = {
|
|
|
10878
11366
|
command: "task list",
|
|
10879
11367
|
examples: [
|
|
10880
11368
|
"donebear task list --json",
|
|
10881
|
-
"donebear task list --
|
|
11369
|
+
"donebear task list --view logbook --json",
|
|
10882
11370
|
"donebear task list --view today --json",
|
|
10883
11371
|
"donebear task list --search \"login\" --json"
|
|
10884
11372
|
],
|
|
10885
11373
|
json_output: {
|
|
10886
11374
|
autoSelected: "boolean",
|
|
10887
11375
|
count: "number",
|
|
10888
|
-
limited: "TaskRecord[]",
|
|
10889
|
-
ok: "boolean",
|
|
10890
|
-
search: "string | null",
|
|
10891
11376
|
state: "string",
|
|
11377
|
+
tasks: "TaskRecord[]",
|
|
11378
|
+
total: "number",
|
|
10892
11379
|
view: "string | null",
|
|
10893
11380
|
workspace: "WorkspaceSummary"
|
|
10894
11381
|
},
|
|
@@ -10913,13 +11400,15 @@ const COMMAND_SPECS = {
|
|
|
10913
11400
|
type: "string"
|
|
10914
11401
|
},
|
|
10915
11402
|
"--view": {
|
|
10916
|
-
description: "Filter by view bucket",
|
|
11403
|
+
description: "Filter by view bucket. logbook and trash widen the state to done and archived, so they read the completed and archived tasks whatever --state says; every other view reads --state, and pairing --state done with one of them matches nothing",
|
|
10917
11404
|
enum: [
|
|
10918
11405
|
"inbox",
|
|
10919
11406
|
"anytime",
|
|
10920
11407
|
"today",
|
|
10921
11408
|
"upcoming",
|
|
10922
|
-
"someday"
|
|
11409
|
+
"someday",
|
|
11410
|
+
"logbook",
|
|
11411
|
+
"trash"
|
|
10923
11412
|
],
|
|
10924
11413
|
type: "string"
|
|
10925
11414
|
},
|
|
@@ -11060,6 +11549,44 @@ const COMMAND_SPECS = {
|
|
|
11060
11549
|
options: {},
|
|
11061
11550
|
usage: "donebear whoami"
|
|
11062
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
|
+
},
|
|
11063
11590
|
"workspace list": {
|
|
11064
11591
|
command: "workspace list",
|
|
11065
11592
|
examples: ["donebear workspace list --json"],
|
|
@@ -11102,10 +11629,11 @@ const TOP_LEVEL_SPECS = {
|
|
|
11102
11629
|
auth: {
|
|
11103
11630
|
command: "auth",
|
|
11104
11631
|
description: "Authentication",
|
|
11105
|
-
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.",
|
|
11106
11633
|
subcommands: {
|
|
11107
11634
|
login: "OAuth login (Google or GitHub)",
|
|
11108
11635
|
logout: "Sign out and remove stored session",
|
|
11636
|
+
signup: "Create an account from a human email (agent signup)",
|
|
11109
11637
|
status: "Show auth status"
|
|
11110
11638
|
},
|
|
11111
11639
|
tip: "donebear spec auth login: get full spec for auth login"
|
|
@@ -11192,7 +11720,7 @@ const TOP_LEVEL_SPECS = {
|
|
|
11192
11720
|
create: "Create a new workspace",
|
|
11193
11721
|
current: "Show the active workspace",
|
|
11194
11722
|
invitations: "List pending invitations",
|
|
11195
|
-
invite: "Invite a
|
|
11723
|
+
invite: "Invite by email, or mint a reusable --share join link",
|
|
11196
11724
|
join: "Join a workspace by invite",
|
|
11197
11725
|
list: "List all workspaces",
|
|
11198
11726
|
members: "List workspace members",
|
|
@@ -11240,6 +11768,7 @@ Examples:
|
|
|
11240
11768
|
donebear spec project list Detailed spec for project list
|
|
11241
11769
|
donebear spec import things Detailed spec for the Things importer
|
|
11242
11770
|
donebear spec search Detailed spec for search
|
|
11771
|
+
donebear spec auth signup Agent signup from a human email
|
|
11243
11772
|
donebear spec auth login Auth login options and non-interactive notes
|
|
11244
11773
|
`).action(async (command, subcommand, _options, cmd) => {
|
|
11245
11774
|
const context = contextFromCommand(cmd);
|
|
@@ -11698,10 +12227,14 @@ const runWorkspaceInvite = async (context, workspaceRefArg, workspaceOverride, o
|
|
|
11698
12227
|
token: commandContext.token,
|
|
11699
12228
|
workspaceRef: workspaceOverride ?? workspaceRefArg ?? null
|
|
11700
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);
|
|
11701
12232
|
const invitation = await createWorkspaceInvitation(context, {
|
|
11702
12233
|
baseUrl: commandContext.apiBaseUrl,
|
|
11703
12234
|
email: options.email,
|
|
11704
|
-
|
|
12235
|
+
kind: options.share ? "share" : void 0,
|
|
12236
|
+
projectId: options.project,
|
|
12237
|
+
role: options.share ? "member" : options.role ?? "member",
|
|
11705
12238
|
token: commandContext.token,
|
|
11706
12239
|
workspaceId: workspace.id
|
|
11707
12240
|
});
|
|
@@ -11717,7 +12250,7 @@ const runWorkspaceInvite = async (context, workspaceRefArg, workspaceOverride, o
|
|
|
11717
12250
|
writeInfo(context, `role: ${invitation.role}`);
|
|
11718
12251
|
writeInfo(context, `expires: ${invitation.expiresAt}`);
|
|
11719
12252
|
if (invitation.email) writeInfo(context, `email: ${invitation.email}`);
|
|
11720
|
-
const link = `${
|
|
12253
|
+
const link = `${DEFAULT_APP_URL}/join?code=${encodeURIComponent(invitation.code)}`;
|
|
11721
12254
|
writeInfo(context, `link: ${link}`);
|
|
11722
12255
|
if (context.copy) {
|
|
11723
12256
|
if (copyToClipboard(link)) writeInfo(context, "Link copied to clipboard.");
|
|
@@ -11733,6 +12266,8 @@ Examples:
|
|
|
11733
12266
|
donebear workspace members
|
|
11734
12267
|
donebear workspace invitations
|
|
11735
12268
|
donebear workspace invite --email user@example.com --role member
|
|
12269
|
+
donebear workspace invite --share --copy
|
|
12270
|
+
donebear workspace invite --share --project <project-id>
|
|
11736
12271
|
`).action(async (_options, command) => {
|
|
11737
12272
|
const context = contextFromCommand(command);
|
|
11738
12273
|
await runWithErrorHandling(context, async () => {
|
|
@@ -11787,7 +12322,7 @@ Examples:
|
|
|
11787
12322
|
await runWorkspaceInvitations(context, workspaceRef, workspaceOverride);
|
|
11788
12323
|
});
|
|
11789
12324
|
});
|
|
11790
|
-
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) => {
|
|
11791
12326
|
const context = contextFromCommand(command);
|
|
11792
12327
|
await runWithErrorHandling(context, async () => {
|
|
11793
12328
|
await runWorkspaceInvite(context, workspaceRef, workspaceOverride, options);
|
|
@@ -11815,8 +12350,9 @@ With auth:
|
|
|
11815
12350
|
donebear import things --dry-run --json
|
|
11816
12351
|
|
|
11817
12352
|
Auth:
|
|
11818
|
-
donebear auth
|
|
11819
|
-
|
|
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
|
|
11820
12356
|
|
|
11821
12357
|
Workspace targeting:
|
|
11822
12358
|
donebear workspace=myorg task list
|