donebear 0.3.7 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { a as toCliError, b as DONEBEAR_TOKEN_ENV, d as DEFAULT_OAUTH_CALLBACK_PATH, f as DEFAULT_OAUTH_CALLBACK_PORT, i as EXIT_CODES, l as CONFIG_DIR, n as setCurrentWorkspace, o as AUTH_FILE_PATH, r as CliError, s as CLI_NAME, t as ensureLocalContext } from "./context-store-DH561Z1u.mjs";
3
- import { n as readStdin, t as promptForValue } from "./prompt-BrfcK2Ry.mjs";
2
+ import { a as toCliError, b as DONEBEAR_TOKEN_ENV, d as DEFAULT_OAUTH_CALLBACK_PATH, f as DEFAULT_OAUTH_CALLBACK_PORT, i as EXIT_CODES, l as CONFIG_DIR, n as setCurrentWorkspace, o as AUTH_FILE_PATH, r as CliError, s as CLI_NAME, t as ensureLocalContext } from "./context-store-DuCNavgl.mjs";
3
+ import { n as readStdin, t as promptForValue } from "./prompt-GfcWO85b.mjs";
4
4
  import { createRequire } from "node:module";
5
5
  import { Command, InvalidArgumentError } from "commander";
6
6
  import { homedir } from "node:os";
@@ -18,13 +18,13 @@ import { fileURLToPath } from "node:url";
18
18
  import Database from "better-sqlite3";
19
19
  import { createInterface } from "node:readline";
20
20
  //#region \0rolldown/runtime.js
21
- var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
21
+ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
22
22
  //#endregion
23
23
  //#region src/jwt.ts
24
24
  const parseJwtBase64Url = (input) => {
25
25
  const normalized = input.replaceAll("-", "+").replaceAll("_", "/");
26
26
  const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
27
- return Buffer.from(padded, "base64").toString("utf8");
27
+ return Buffer.from(padded, "base64").toString("utf-8");
28
28
  };
29
29
  const parseJwtClaims = (token) => {
30
30
  const payloadPart = token.split(".").at(1);
@@ -115,7 +115,7 @@ const parseAuthFile = (payload) => {
115
115
  };
116
116
  const readStoredAuthSession = async () => {
117
117
  try {
118
- return parseAuthFile(await readFile(AUTH_FILE_PATH, "utf8")).session;
118
+ return parseAuthFile(await readFile(AUTH_FILE_PATH, "utf-8")).session;
119
119
  } catch (error) {
120
120
  if (isRecord(error) && error.code === "ENOENT") return null;
121
121
  if (error instanceof CliError) throw error;
@@ -132,7 +132,7 @@ const writeStoredAuthSession = async (session) => {
132
132
  recursive: true
133
133
  });
134
134
  await writeFile(AUTH_FILE_PATH, `${JSON.stringify(payload, null, 2)}\n`, {
135
- encoding: "utf8",
135
+ encoding: "utf-8",
136
136
  mode: 384
137
137
  });
138
138
  };
@@ -199,19 +199,6 @@ const tokenResponseToStoredSession = (response) => {
199
199
  } : null
200
200
  };
201
201
  };
202
- const getSupabaseUser = async (supabaseUrl, publishableKey, accessToken) => {
203
- const response = await fetch(`${supabaseUrl}/auth/v1/user`, { headers: {
204
- apikey: publishableKey,
205
- authorization: `Bearer ${accessToken}`
206
- } });
207
- if (!response.ok) throw new CliError(`Failed to load user (${response.status})`, EXIT_CODES.ERROR);
208
- const data = await response.json();
209
- if (typeof data.id !== "string") throw new CliError("No user returned for current token", EXIT_CODES.ERROR);
210
- return {
211
- email: typeof data.email === "string" ? data.email : null,
212
- id: data.id
213
- };
214
- };
215
202
  //#endregion
216
203
  //#region src/auth-session.ts
217
204
  const expiryFromClaims = (claims) => {
@@ -346,8 +333,8 @@ const requestNdjson = async (context, endpoint, options) => {
346
333
  };
347
334
  //#endregion
348
335
  //#region src/date-codecs.ts
349
- const ISO_DATE_ONLY_REGEX = /^(\d{4})-(\d{2})-(\d{2})$/;
350
- const ISO_INSTANT_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
336
+ const ISO_DATE_ONLY_REGEX = /^(\d{4})-(\d{2})-(\d{2})$/u;
337
+ const ISO_INSTANT_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/u;
351
338
  const pad = (value, length = 2) => String(value).padStart(length, "0");
352
339
  const isFiniteEpoch = (value) => typeof value === "number" && Number.isFinite(value);
353
340
  const isValidDateOnlyParts = (parts) => {
@@ -681,17 +668,17 @@ const WORKSPACE_HISTORY_QUERY = `
681
668
  }
682
669
  `;
683
670
  const API_KEY_CREATE_MUTATION = `
684
- mutation ApiKeyCreate($label: String!) {
685
- apiKeyCreate(label: $label) {
671
+ mutation ApiKeyCreate($label: String!, $scopes: [String!], $workspaceIds: [ID!], $expiresAt: DateTime) {
672
+ apiKeyCreate(label: $label, scopes: $scopes, workspaceIds: $workspaceIds, expiresAt: $expiresAt) {
686
673
  success lastSyncId
687
- apiKey { id label keyPrefix createdAt }
674
+ apiKey { id label keyPrefix scopes workspaceIds expiresAt createdAt }
688
675
  token
689
676
  }
690
677
  }
691
678
  `;
692
679
  const API_KEYS_QUERY = `
693
680
  query ApiKeys {
694
- apiKeys { id label keyPrefix lastUsedAt revokedAt createdAt }
681
+ apiKeys { id label keyPrefix scopes workspaceIds expiresAt lastUsedAt revokedAt createdAt }
695
682
  }
696
683
  `;
697
684
  const API_KEY_REVOKE_MUTATION = `
@@ -875,11 +862,14 @@ const toWorkspaceHistoryEntryFromGraphqlNode = (node) => ({
875
862
  });
876
863
  const toApiKeyRecordFromGraphqlNode = (node) => ({
877
864
  createdAt: node.createdAt,
865
+ expiresAt: node.expiresAt,
878
866
  id: node.id,
879
867
  keyPrefix: node.keyPrefix,
880
868
  label: node.label,
881
869
  lastUsedAt: node.lastUsedAt,
882
- revokedAt: node.revokedAt
870
+ revokedAt: node.revokedAt,
871
+ scopes: node.scopes,
872
+ workspaceIds: node.workspaceIds
883
873
  });
884
874
  const toChecklistItemRecord = (value) => {
885
875
  if (typeof value !== "object" || value === null) return null;
@@ -903,6 +893,26 @@ const resolveApiBaseUrl = (context) => {
903
893
  if (fromContext) return normalizeApiUrl(fromContext);
904
894
  return normalizeApiUrl(readEnvValue("DONEBEAR_API_URL") ?? "https://api.donebear.com");
905
895
  };
896
+ const VIEWER_QUERY = `
897
+ query Viewer {
898
+ viewer {
899
+ id
900
+ email
901
+ }
902
+ }
903
+ `;
904
+ const loadViewer = async (context, options) => {
905
+ const data = await requestGraphql(context, `${options.baseUrl}/graphql`, {
906
+ query: VIEWER_QUERY,
907
+ token: options.token,
908
+ variables: {}
909
+ });
910
+ if (!data.viewer) throw new CliError("Could not resolve current user. The token may be invalid or revoked.", EXIT_CODES.AUTH_REQUIRED);
911
+ return {
912
+ email: data.viewer.email ?? null,
913
+ id: data.viewer.id
914
+ };
915
+ };
906
916
  const listWorkspaces = async (context, options) => {
907
917
  return (await requestGraphql(context, `${options.baseUrl}/graphql`, {
908
918
  query: MY_WORKSPACES_QUERY,
@@ -1089,10 +1099,15 @@ const listWorkspaceHistory = async (context, options) => {
1089
1099
  })).workspaceHistory.map(toWorkspaceHistoryEntryFromGraphqlNode);
1090
1100
  };
1091
1101
  const createApiKey = async (context, options) => {
1092
- const data = await requestGraphql(context, `${options.baseUrl}/graphql`, {
1102
+ const endpoint = `${options.baseUrl}/graphql`;
1103
+ const variables = { label: options.label };
1104
+ if (options.scopes !== void 0) variables.scopes = options.scopes;
1105
+ if (options.workspaceIds !== void 0) variables.workspaceIds = options.workspaceIds;
1106
+ if (options.expiresAt !== void 0) variables.expiresAt = options.expiresAt;
1107
+ const data = await requestGraphql(context, endpoint, {
1093
1108
  query: API_KEY_CREATE_MUTATION,
1094
1109
  token: options.token,
1095
- variables: { label: options.label }
1110
+ variables
1096
1111
  });
1097
1112
  return {
1098
1113
  ...toApiKeyRecordFromGraphqlNode(data.apiKeyCreate.apiKey),
@@ -1140,10 +1155,13 @@ const resolveCommandContext = async (context) => {
1140
1155
  token
1141
1156
  };
1142
1157
  };
1143
- const requireUserId = (resolvedToken) => {
1144
- const userId = resolvedToken.user?.id ?? null;
1145
- if (!(typeof userId === "string" && userId.length > 0)) throw new CliError("Could not determine user id from auth token", EXIT_CODES.AUTH_REQUIRED);
1146
- return userId;
1158
+ const requireUserId = async (context, commandContext) => {
1159
+ const claimUserId = commandContext.resolvedToken.user?.id ?? null;
1160
+ if (typeof claimUserId === "string" && claimUserId.length > 0) return claimUserId;
1161
+ return (await loadViewer(context, {
1162
+ baseUrl: commandContext.apiBaseUrl,
1163
+ token: commandContext.token
1164
+ })).id;
1147
1165
  };
1148
1166
  //#endregion
1149
1167
  //#region src/options.ts
@@ -1238,20 +1256,68 @@ const runWithErrorHandling = async (context, action) => {
1238
1256
  };
1239
1257
  //#endregion
1240
1258
  //#region src/commands/api-key.ts
1241
- const runCreate = async (context, label) => {
1259
+ const PERMISSION_SCOPES = {
1260
+ "full-access": [
1261
+ "read",
1262
+ "write",
1263
+ "admin"
1264
+ ],
1265
+ "read-only": ["read"]
1266
+ };
1267
+ const parseDuration = (duration) => {
1268
+ const match = duration.match(/^(\d+)(d|y)$/u);
1269
+ if (!match) throw new CliError(`Invalid duration "${duration}". Use a format like "30d", "90d", or "1y".`, EXIT_CODES.VALIDATION);
1270
+ const [, rawAmount, unit] = match;
1271
+ const amount = Number(rawAmount);
1272
+ const now = /* @__PURE__ */ new Date();
1273
+ if (unit === "d") now.setDate(now.getDate() + amount);
1274
+ else now.setDate(now.getDate() + amount * 365);
1275
+ return now;
1276
+ };
1277
+ const runCreate = async (context, label, options) => {
1242
1278
  const { token } = await requireAuthToken(context);
1279
+ const baseUrl = resolveApiBaseUrl(context);
1280
+ let scopes;
1281
+ if (options.permissions !== void 0) {
1282
+ const level = options.permissions;
1283
+ if (!PERMISSION_SCOPES[level]) throw new CliError(`Invalid permissions "${options.permissions}". Use "full-access" or "read-only".`, EXIT_CODES.VALIDATION);
1284
+ scopes = PERMISSION_SCOPES[level];
1285
+ }
1286
+ let workspaceIds;
1287
+ if (options.workspaces !== void 0) {
1288
+ const slugs = options.workspaces.split(",").map((s) => s.trim()).filter(Boolean);
1289
+ if (slugs.length === 0) throw new CliError("No workspace slugs provided for --workspaces.", EXIT_CODES.VALIDATION);
1290
+ const allWorkspaces = await listWorkspaces(context, {
1291
+ baseUrl,
1292
+ token
1293
+ });
1294
+ workspaceIds = [];
1295
+ for (const slug of slugs) {
1296
+ const match = allWorkspaces.find((ws) => ws.urlKey === slug || ws.id === slug || ws.name.toLowerCase() === slug.toLowerCase());
1297
+ if (!match) throw new CliError(`Workspace "${slug}" not found. Run \`donebear workspace list\` to see available workspaces.`, EXIT_CODES.VALIDATION);
1298
+ workspaceIds.push(match.id);
1299
+ }
1300
+ }
1301
+ let expiresAt;
1302
+ if (options.expires !== void 0) expiresAt = parseDuration(options.expires).toISOString();
1243
1303
  const key = await createApiKey(context, {
1244
- baseUrl: resolveApiBaseUrl(context),
1304
+ baseUrl,
1305
+ expiresAt,
1245
1306
  label,
1246
- token
1307
+ scopes,
1308
+ token,
1309
+ workspaceIds
1247
1310
  });
1248
1311
  if (context.json) {
1249
1312
  writeOutput(context, {
1250
1313
  createdAt: key.createdAt,
1314
+ expiresAt: key.expiresAt,
1251
1315
  id: key.id,
1252
1316
  keyPrefix: key.keyPrefix,
1253
1317
  label: key.label,
1254
- token: key.token
1318
+ scopes: key.scopes,
1319
+ token: key.token,
1320
+ workspaceIds: key.workspaceIds
1255
1321
  });
1256
1322
  return;
1257
1323
  }
@@ -1259,6 +1325,10 @@ const runCreate = async (context, label) => {
1259
1325
  writeInfo(context, "");
1260
1326
  writeInfo(context, ` ${key.token}`);
1261
1327
  writeInfo(context, "");
1328
+ writeInfo(context, ` Scopes: ${key.scopes.join(", ")}`);
1329
+ if (key.workspaceIds && key.workspaceIds.length > 0) writeInfo(context, ` Restricted to workspaces: ${key.workspaceIds.join(", ")}`);
1330
+ if (key.expiresAt) writeInfo(context, ` Expires: ${new Date(key.expiresAt).toLocaleDateString()}`);
1331
+ writeInfo(context, "");
1262
1332
  writeWarning(context, "Save this key now — it will not be shown again.");
1263
1333
  };
1264
1334
  const runList = async (context) => {
@@ -1278,7 +1348,10 @@ const runList = async (context) => {
1278
1348
  for (const key of keys) {
1279
1349
  const status = key.revokedAt ? " (revoked)" : "";
1280
1350
  const lastUsed = key.lastUsedAt ? `Last used: ${new Date(key.lastUsedAt).toLocaleDateString()}` : "Never used";
1281
- writeInfo(context, `${key.keyPrefix}... ${key.label} ${lastUsed}${status} [${key.id}]`);
1351
+ const scopeStr = `[${key.scopes.join(",")}]`;
1352
+ const wsStr = key.workspaceIds && key.workspaceIds.length > 0 ? ` ws:${key.workspaceIds.length}` : "";
1353
+ const expStr = key.expiresAt ? ` exp:${new Date(key.expiresAt).toLocaleDateString()}` : "";
1354
+ writeInfo(context, `${key.keyPrefix}... ${key.label} ${scopeStr}${wsStr}${expStr} ${lastUsed}${status} [${key.id}]`);
1282
1355
  }
1283
1356
  };
1284
1357
  const runRevoke = async (context, keyId) => {
@@ -1301,6 +1374,8 @@ const registerApiKeyCommands = (program) => {
1301
1374
  const apiKey = program.command("api-key").alias("key").description("Manage personal API keys").addHelpText("after", `
1302
1375
  Examples:
1303
1376
  donebear api-key create "Raycast"
1377
+ donebear api-key create "CI" --permissions read-only --expires 90d
1378
+ donebear api-key create "Scoped" --workspaces my-org,side-project --expires 1y
1304
1379
  donebear api-key list
1305
1380
  donebear api-key revoke <key-id>
1306
1381
  `).action(async (_options, command) => {
@@ -1309,10 +1384,10 @@ Examples:
1309
1384
  await runList(context);
1310
1385
  });
1311
1386
  });
1312
- apiKey.command("create <label>").description("Create a new personal API key").action(async (label, _options, command) => {
1387
+ apiKey.command("create <label>").description("Create a new personal API key").option("--permissions <value>", "Permission level: \"full-access\" or \"read-only\"", "full-access").option("--workspaces <slugs>", "Comma-separated workspace slugs to restrict the key to").option("--expires <duration>", "Expiration duration, e.g. \"30d\", \"90d\", \"1y\"").action(async (label, options, command) => {
1313
1388
  const context = contextFromCommand(command);
1314
1389
  await runWithErrorHandling(context, async () => {
1315
- await runCreate(context, label);
1390
+ await runCreate(context, label, options);
1316
1391
  });
1317
1392
  });
1318
1393
  apiKey.command("list").description("List all personal API keys").action(async (_options, command) => {
@@ -1669,7 +1744,7 @@ const resolveTaskView = (task) => {
1669
1744
  const formatEpochDate = (epochMs) => formatInstantEpoch(epochMs);
1670
1745
  //#endregion
1671
1746
  //#region src/command-helpers.ts
1672
- const SHORT_ID_SUFFIX_PATTERN = /-([a-f0-9]{8})$/;
1747
+ const SHORT_ID_SUFFIX_PATTERN = /-([a-f0-9]{8})$/u;
1673
1748
  /**
1674
1749
  * Find a record by reference: exact ID, key, ID prefix, slug shortId, or name.
1675
1750
  * Works for any entity with id, key, and name fields.
@@ -1723,7 +1798,7 @@ const taskListLines = (tasks) => tasks.map((task) => {
1723
1798
  });
1724
1799
  //#endregion
1725
1800
  //#region src/workspace-context.ts
1726
- const WORKSPACE_URL_KEY_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1801
+ const WORKSPACE_URL_KEY_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
1727
1802
  const fetchWorkspaces = async (context, options) => {
1728
1803
  const workspaces = await listWorkspaces(context, {
1729
1804
  baseUrl: options.apiBaseUrl,
@@ -1782,7 +1857,7 @@ const byReference = (workspaces, reference) => {
1782
1857
  return null;
1783
1858
  };
1784
1859
  const toWorkspaceUrlKey = (name) => {
1785
- return name.trim().toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "").replaceAll(/-{2,}/g, "-").slice(0, 50);
1860
+ return name.trim().toLowerCase().replaceAll(/[^a-z0-9]+/gu, "-").replaceAll(/^-+|-+$/gu, "").replaceAll(/-{2,}/gu, "-").slice(0, 50);
1786
1861
  };
1787
1862
  const isValidWorkspaceUrlKey = (value) => WORKSPACE_URL_KEY_REGEX.test(value) && value.length >= 3;
1788
1863
  const resolveWorkspace = async (context, options) => {
@@ -1993,7 +2068,7 @@ const checkAuth = async (context) => {
1993
2068
  };
1994
2069
  const checkWorkspace = async () => {
1995
2070
  try {
1996
- const { ensureLocalContext } = await import("./context-store-D0Z9vbaU.mjs");
2071
+ const { ensureLocalContext } = await import("./context-store-CK4yIl9e.mjs");
1997
2072
  const localContext = await ensureLocalContext();
1998
2073
  if (!localContext.workspaceId) return {
1999
2074
  message: "No workspace selected (run `donebear workspace use`)",
@@ -2225,7 +2300,7 @@ const bytesToUuid = (bytes) => {
2225
2300
  };
2226
2301
  const createUuidV5 = (name, namespace) => {
2227
2302
  const namespaceBytes = uuidToBytes(namespace);
2228
- const hash = createHash("sha1").update(namespaceBytes).update(Buffer.from(name, "utf8")).digest();
2303
+ const hash = createHash("sha1").update(namespaceBytes).update(Buffer.from(name, "utf-8")).digest();
2229
2304
  const bytes = Uint8Array.from(hash.subarray(0, 16));
2230
2305
  const versionByte = bytes.at(6);
2231
2306
  const variantByte = bytes.at(8);
@@ -2665,7 +2740,6 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
2665
2740
  };
2666
2741
  return __assign.apply(this, arguments);
2667
2742
  };
2668
- Object.create;
2669
2743
  function __spreadArray(to, from, pack) {
2670
2744
  if (pack || arguments.length === 2) {
2671
2745
  for (var i = 0, l = from.length, ar; i < l; i++) if (ar || !(i in from)) {
@@ -2678,7 +2752,6 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
2678
2752
  function __await(v) {
2679
2753
  return this instanceof __await ? (this.v = v, this) : new __await(v);
2680
2754
  }
2681
- Object.create;
2682
2755
  const callbackiterresult = function(_super) {
2683
2756
  __extends(CallbackIterResult, _super);
2684
2757
  function CallbackIterResult(method, args, iterator) {
@@ -3515,7 +3588,8 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
3515
3588
  this.month += months;
3516
3589
  if (this.month > 12) {
3517
3590
  var yearDiv = Math.floor(this.month / 12);
3518
- this.month = pymod(this.month, 12);
3591
+ var monthMod = pymod(this.month, 12);
3592
+ this.month = monthMod;
3519
3593
  this.year += yearDiv;
3520
3594
  if (this.month === 0) {
3521
3595
  this.month = 12;
@@ -4134,8 +4208,7 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
4134
4208
  var month = Math.floor((h + l - 7 * m + 114) / 31);
4135
4209
  var day = (h + l - 7 * m + 114) % 31 + 1;
4136
4210
  var date = Date.UTC(y, month - 1, day + offset);
4137
- var yearStart = Date.UTC(y, 0, 1);
4138
- return [Math.ceil((date - yearStart) / (1e3 * 60 * 60 * 24))];
4211
+ return [Math.ceil((date - Date.UTC(y, 0, 1)) / (1e3 * 60 * 60 * 24))];
4139
4212
  }
4140
4213
  const iterinfo = function() {
4141
4214
  function Iterinfo(options) {
@@ -4461,7 +4534,8 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
4461
4534
  if (noCache === void 0) noCache = false;
4462
4535
  this._cache = noCache ? null : new Cache();
4463
4536
  this.origOptions = initializeOptions(options);
4464
- this.options = parseOptions(options).parsedOptions;
4537
+ var parsedOptions = parseOptions(options).parsedOptions;
4538
+ this.options = parsedOptions;
4465
4539
  }
4466
4540
  RRule.parseText = function(text, language) {
4467
4541
  return parseText(text, language);
@@ -5152,17 +5226,17 @@ const THINGS_WEEKDAY_TO_RRULE = [
5152
5226
  "SA"
5153
5227
  ];
5154
5228
  const parseNumericPlistField = (xml, key) => {
5155
- const match = new RegExp(`<key>${key}</key>\\s*<(integer|real)>([^<]+)</\\1>`).exec(xml);
5229
+ const match = new RegExp(`<key>${key}</key>\\s*<(integer|real)>([^<]+)</\\1>`, "u").exec(xml);
5156
5230
  if (!match?.[2]) return null;
5157
5231
  const parsed = Number(match[2]);
5158
5232
  return Number.isFinite(parsed) ? parsed : null;
5159
5233
  };
5160
- const PLIST_OFFSETS_ARRAY_PATTERN = /<key>of<\/key>\s*<array>([\s\S]*?)<\/array>/;
5234
+ const PLIST_OFFSETS_ARRAY_PATTERN = /<key>of<\/key>\s*<array>([\s\S]*?)<\/array>/u;
5161
5235
  const parseOffsets = (xml) => {
5162
5236
  const arrayMatch = PLIST_OFFSETS_ARRAY_PATTERN.exec(xml);
5163
5237
  if (!arrayMatch?.[1]) return [];
5164
5238
  const offsets = [];
5165
- for (const dictMatch of arrayMatch[1].matchAll(/<dict>([\s\S]*?)<\/dict>/g)) {
5239
+ for (const dictMatch of arrayMatch[1].matchAll(/<dict>([\s\S]*?)<\/dict>/gu)) {
5166
5240
  const [, fragment] = dictMatch;
5167
5241
  if (!fragment) continue;
5168
5242
  const weekday = parseNumericPlistField(fragment, "wd");
@@ -5290,11 +5364,11 @@ const normalizeTitle = (value, fallback) => {
5290
5364
  const trimmed = value?.trim();
5291
5365
  return trimmed && trimmed.length > 0 ? trimmed : fallback;
5292
5366
  };
5293
- const NON_ALPHANUMERIC_PATTERN = /[^A-Z0-9]+/g;
5294
- const WHITESPACE_SPLIT_PATTERN = /\s+/;
5295
- const NON_ALPHANUMERIC_STRICT_PATTERN = /[^A-Z0-9]/g;
5367
+ const NON_ALPHANUMERIC_PATTERN = /[^A-Z0-9]+/gu;
5368
+ const WHITESPACE_SPLIT_PATTERN = /\s+/u;
5369
+ const NON_ALPHANUMERIC_STRICT_PATTERN = /[^A-Z0-9]/gu;
5296
5370
  const READABLE_TEXT_PATTERN = /^[\t\n\r\u0020-\u007E\u00A0-\uFFFF]*$/u;
5297
- const EMAIL_SPLIT_PATTERN = /[,\n;]/;
5371
+ const EMAIL_SPLIT_PATTERN = /[,\n;]/u;
5298
5372
  const normalizeKeyBase = (value, fallback) => {
5299
5373
  const initials = value.toUpperCase().replace(NON_ALPHANUMERIC_PATTERN, " ").trim().split(WHITESPACE_SPLIT_PATTERN).filter((word) => word.length > 0).map((word) => word[0] ?? "").join("");
5300
5374
  const compact = value.toUpperCase().replace(NON_ALPHANUMERIC_STRICT_PATTERN, "");
@@ -5312,7 +5386,7 @@ const buildUniqueKeys = (items, options) => {
5312
5386
  const base = normalizeKeyBase(normalizeTitle(item.title, options.fallback), options.fallback).slice(0, options.maxLength);
5313
5387
  let candidate = base || options.fallback;
5314
5388
  if (used.has(candidate)) {
5315
- const suffixBase = item.uuid.replaceAll(/[^a-zA-Z0-9]/g, "").slice(-4);
5389
+ const suffixBase = item.uuid.replaceAll(/[^a-zA-Z0-9]/gu, "").slice(-4);
5316
5390
  const suffix = suffixBase.length > 0 ? suffixBase.toUpperCase() : "X";
5317
5391
  const headLength = Math.max(1, options.maxLength - suffix.length - 1);
5318
5392
  candidate = `${candidate.slice(0, headLength)}-${suffix}`;
@@ -5384,7 +5458,7 @@ const pickCompletedAt = (task) => {
5384
5458
  };
5385
5459
  const sortValue = (primary, fallback) => typeof primary === "number" ? primary : fallback;
5386
5460
  const sanitizeBuffer = (buffer) => {
5387
- const asText = buffer.toString("utf8");
5461
+ const asText = buffer.toString("utf-8");
5388
5462
  if (!asText.includes("�") && READABLE_TEXT_PATTERN.test(asText)) {
5389
5463
  const trimmed = asText.trim();
5390
5464
  if (trimmed.length > 0) return trimmed;
@@ -5998,7 +6072,7 @@ const THINGS_CONTAINER_PATH = join(homedir(), "Library/Group Containers/JLMPQHK8
5998
6072
  const THINGS_BUNDLE_NAME = "Things Database.thingsdatabase";
5999
6073
  const THINGS_SQLITE_FILE = "main.sqlite";
6000
6074
  const LEGACY_THINGS_SQLITE_FILE = "Things.sqlite3";
6001
- const URL_SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/i;
6075
+ const URL_SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/iu;
6002
6076
  const isUrlLike = (value) => URL_SCHEME_PATTERN.test(value);
6003
6077
  const resolveInputPath = (databaseUrl) => {
6004
6078
  if (!isUrlLike(databaseUrl)) return resolve(databaseUrl);
@@ -6061,7 +6135,7 @@ const toStringValue = (value) => {
6061
6135
  return trimmed.length > 0 ? trimmed : null;
6062
6136
  }
6063
6137
  if (Buffer.isBuffer(value)) {
6064
- const text = value.toString("utf8").trim();
6138
+ const text = value.toString("utf-8").trim();
6065
6139
  return text.length > 0 ? text : null;
6066
6140
  }
6067
6141
  return null;
@@ -6407,7 +6481,7 @@ const parseMetadataMode = (value) => {
6407
6481
  };
6408
6482
  const runThingsImportCommand = async (context, options, workspaceOverride) => {
6409
6483
  const commandContext = await resolveCommandContext(context);
6410
- const creatorId = requireUserId(commandContext.resolvedToken);
6484
+ const creatorId = await requireUserId(context, commandContext);
6411
6485
  const metadataMode = parseMetadataMode(options.metadataMode);
6412
6486
  const { workspace, autoSelected } = await resolveWorkspace(context, {
6413
6487
  apiBaseUrl: commandContext.apiBaseUrl,
@@ -6463,7 +6537,7 @@ const isHistoryFile = (value) => {
6463
6537
  };
6464
6538
  const loadHistory = async (historyFile) => {
6465
6539
  try {
6466
- const raw = await readFile(historyFile, "utf8");
6540
+ const raw = await readFile(historyFile, "utf-8");
6467
6541
  const parsed = JSON.parse(raw);
6468
6542
  if (isHistoryFile(parsed)) return parsed.entries;
6469
6543
  return [];
@@ -6538,7 +6612,7 @@ const runInteractiveMode = async (context, program) => {
6538
6612
  recursive: true
6539
6613
  });
6540
6614
  await writeFile(HISTORY_FILE, `${JSON.stringify(payload, null, 2)}\n`, {
6541
- encoding: "utf8",
6615
+ encoding: "utf-8",
6542
6616
  mode: 384
6543
6617
  });
6544
6618
  } catch {}
@@ -6635,7 +6709,7 @@ const runLabelShow = async (context, labelRef, options) => {
6635
6709
  writeInfo(context, `createdAt: ${formatEpochDate(label.createdAt) ?? "(none)"}`);
6636
6710
  writeInfo(context, `updatedAt: ${formatEpochDate(label.updatedAt) ?? "(none)"}`);
6637
6711
  };
6638
- const UUID_REGEX$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6712
+ const UUID_REGEX$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
6639
6713
  const resolveTaskForLabel = async (context, options) => {
6640
6714
  const taskRef = options.taskRef.trim();
6641
6715
  if (!taskRef) throw new Error("Task id is required.");
@@ -6882,7 +6956,7 @@ const getProjectStatus = (project) => {
6882
6956
  };
6883
6957
  const parseTargetDate = (value) => parseCliDateOnlyToEpoch(value, "target date");
6884
6958
  const generateProjectSlug = (name, id) => {
6885
- const slug = name.toLowerCase().trim().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-|-$/g, "").slice(0, 39);
6959
+ const slug = name.toLowerCase().trim().replaceAll(/[^a-z0-9]+/gu, "-").replaceAll(/^-|-$/gu, "").slice(0, 39);
6886
6960
  const shortId = id.replaceAll("-", "").slice(0, 8);
6887
6961
  return slug ? `${slug}-${shortId}` : shortId;
6888
6962
  };
@@ -7002,14 +7076,14 @@ const resolveProjectName = async (context, rawName) => {
7002
7076
  const trimmed = rawName.trim();
7003
7077
  if (trimmed.length > 0) return trimmed;
7004
7078
  }
7005
- const { promptForValue } = await import("./prompt-FRvfbinv.mjs");
7079
+ const { promptForValue } = await import("./prompt-B7mDo4K7.mjs");
7006
7080
  const prompted = await promptForValue(context, "Project name");
7007
7081
  if (prompted) return prompted;
7008
7082
  throw new CliError("Project name is required.", EXIT_CODES.VALIDATION, "Provide a name: donebear project add \"My project\"");
7009
7083
  };
7010
7084
  const runProjectAdd = async (context, name, options) => {
7011
7085
  const commandContext = await resolveCommandContext(context);
7012
- const userId = requireUserId(commandContext.resolvedToken);
7086
+ const userId = await requireUserId(context, commandContext);
7013
7087
  const normalizedName = await resolveProjectName(context, name);
7014
7088
  const { workspace, autoSelected } = await resolveWorkspace(context, {
7015
7089
  apiBaseUrl: commandContext.apiBaseUrl,
@@ -7253,7 +7327,7 @@ Agent tip: run "donebear spec project add" for full option reference
7253
7327
  };
7254
7328
  //#endregion
7255
7329
  //#region src/commands/task.ts
7256
- const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
7330
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
7257
7331
  const DEFAULT_TASK_LIST_LIMIT = 20;
7258
7332
  const VALID_TASK_VIEWS = new Set([
7259
7333
  "inbox",
@@ -7417,7 +7491,7 @@ const resolveNotes = async (_context, flagNotes) => {
7417
7491
  };
7418
7492
  const runTaskAdd = async (context, title, options, workspaceOverride) => {
7419
7493
  const commandContext = await resolveCommandContext(context);
7420
- const userId = requireUserId(commandContext.resolvedToken);
7494
+ const userId = await requireUserId(context, commandContext);
7421
7495
  const normalizedTitle = await resolveTitle(context, title);
7422
7496
  const notes = await resolveNotes(context, options.notes);
7423
7497
  const { workspace, autoSelected } = await resolveWorkspace(context, {
@@ -8161,6 +8235,15 @@ Examples:
8161
8235
  const OVERVIEW = {
8162
8236
  authentication: "Set DONEBEAR_TOKEN env var, or run: donebear auth login",
8163
8237
  commands: {
8238
+ "api-key": {
8239
+ description: "Manage personal API keys",
8240
+ quick_example: "donebear api-key list --json",
8241
+ subcommands: [
8242
+ "create",
8243
+ "list",
8244
+ "revoke"
8245
+ ]
8246
+ },
8164
8247
  auth: {
8165
8248
  description: "Authentication",
8166
8249
  quick_example: "donebear auth status --json",
@@ -8276,6 +8359,70 @@ const OVERVIEW = {
8276
8359
  workspace_targeting: "Prefix any command: donebear workspace=<slug> task list"
8277
8360
  };
8278
8361
  const COMMAND_SPECS = {
8362
+ "api-key create": {
8363
+ arguments: { label: {
8364
+ description: "Human-readable label for the key",
8365
+ required: true,
8366
+ type: "string"
8367
+ } },
8368
+ command: "api-key create",
8369
+ examples: [
8370
+ "donebear api-key create \"Raycast\" --json",
8371
+ "donebear api-key create \"CI\" --permissions read-only --expires 90d --json",
8372
+ "donebear api-key create \"Scoped\" --workspaces my-org,side-project --expires 1y --json"
8373
+ ],
8374
+ json_output: {
8375
+ createdAt: "string",
8376
+ expiresAt: "string | null",
8377
+ id: "string",
8378
+ keyPrefix: "string",
8379
+ label: "string",
8380
+ scopes: "string[]",
8381
+ token: "string",
8382
+ workspaceIds: "string[] | null"
8383
+ },
8384
+ notes: "The full token is only shown once at creation time. Scopes default to full-access (read, write, admin).",
8385
+ options: {
8386
+ "--expires": {
8387
+ description: "Expiration duration, e.g. \"30d\", \"90d\", \"1y\"",
8388
+ type: "string"
8389
+ },
8390
+ "--permissions": {
8391
+ default: "full-access",
8392
+ description: "\"full-access\" (read, write, admin) or \"read-only\" (read only)",
8393
+ enum: ["full-access", "read-only"],
8394
+ type: "string"
8395
+ },
8396
+ "--workspaces": {
8397
+ description: "Comma-separated workspace slugs to restrict the key to",
8398
+ type: "string"
8399
+ }
8400
+ },
8401
+ usage: "donebear api-key create <label> [options]"
8402
+ },
8403
+ "api-key list": {
8404
+ command: "api-key list",
8405
+ examples: ["donebear api-key list --json"],
8406
+ json_output: { keys: "ApiKeyRecord[]" },
8407
+ notes: "Each key includes scopes, workspaceIds, and expiresAt fields.",
8408
+ options: {},
8409
+ usage: "donebear api-key list"
8410
+ },
8411
+ "api-key revoke": {
8412
+ arguments: { "key-id": {
8413
+ description: "The UUID of the API key to revoke",
8414
+ required: true,
8415
+ type: "string"
8416
+ } },
8417
+ command: "api-key revoke",
8418
+ examples: ["donebear api-key revoke abc12345 --json"],
8419
+ json_output: {
8420
+ id: "string",
8421
+ ok: "boolean"
8422
+ },
8423
+ options: {},
8424
+ usage: "donebear api-key revoke <key-id>"
8425
+ },
8279
8426
  "auth login": {
8280
8427
  command: "auth login",
8281
8428
  examples: [
@@ -8922,6 +9069,16 @@ const COMMAND_SPECS = {
8922
9069
  }
8923
9070
  };
8924
9071
  const TOP_LEVEL_SPECS = {
9072
+ "api-key": {
9073
+ command: "api-key",
9074
+ description: "Manage personal API keys",
9075
+ subcommands: {
9076
+ create: "Create a new API key (--permissions, --workspaces, --expires)",
9077
+ list: "List all API keys with scopes and expiration",
9078
+ revoke: "Revoke an API key by id"
9079
+ },
9080
+ tip: "donebear spec api-key create — get full spec for api-key create"
9081
+ },
8925
9082
  auth: {
8926
9083
  command: "auth",
8927
9084
  description: "Authentication",
@@ -9215,8 +9372,13 @@ Examples:
9215
9372
  //#endregion
9216
9373
  //#region src/commands/whoami.ts
9217
9374
  const runWhoAmI = async (context) => {
9218
- const config = resolveSupabaseConfig();
9219
- const resolvedToken = await resolveAuthToken(context, config);
9375
+ let supabaseConfig = null;
9376
+ try {
9377
+ supabaseConfig = resolveSupabaseConfig();
9378
+ } catch {
9379
+ supabaseConfig = null;
9380
+ }
9381
+ const resolvedToken = await resolveAuthToken(context, supabaseConfig);
9220
9382
  if (!resolvedToken) {
9221
9383
  process.exitCode = EXIT_CODES.AUTH_REQUIRED;
9222
9384
  if (context.json) {
@@ -9233,7 +9395,10 @@ const runWhoAmI = async (context) => {
9233
9395
  writeInfo(context, "Run `donebear auth login`.");
9234
9396
  return;
9235
9397
  }
9236
- const user = await getSupabaseUser(config.url, config.publishableKey, resolvedToken.token);
9398
+ const user = await loadViewer(context, {
9399
+ baseUrl: resolveApiBaseUrl(context),
9400
+ token: resolvedToken.token
9401
+ });
9237
9402
  if (context.json) {
9238
9403
  writeOutput(context, {
9239
9404
  ok: true,
@@ -9594,10 +9759,10 @@ Examples:
9594
9759
  //#endregion
9595
9760
  //#region src/cli.ts
9596
9761
  const packageManifest = createRequire(import.meta.url)("../package.json");
9597
- const WORKSPACE_PREFIX_REGEX = /^workspace=(.+)$/i;
9762
+ const WORKSPACE_PREFIX_REGEX = /^workspace=(.+)$/iu;
9598
9763
  const extractWorkspaceTarget = (argv) => {
9599
9764
  const processedArgv = [...argv];
9600
- const [, , firstArg] = processedArgv;
9765
+ const firstArg = processedArgv[2];
9601
9766
  if (firstArg) {
9602
9767
  const match = WORKSPACE_PREFIX_REGEX.exec(firstArg);
9603
9768
  if (match?.[1]) {