numo-cli 1.2.0 → 1.5.0

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.
Files changed (2) hide show
  1. package/dist/cli.cjs +660 -1928
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -33,27 +33,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
33
33
  mod
34
34
  ));
35
35
 
36
- // <define:__ADMIN_UIDS__>
37
- var define_ADMIN_UIDS_default;
38
- var init_define_ADMIN_UIDS = __esm({
39
- "<define:__ADMIN_UIDS__>"() {
40
- define_ADMIN_UIDS_default = [
41
- "m4FaWpKYc6QMceKV6wiyx9uLi8n1",
42
- "yQ36wj8NXKQvcAfp8TtQ4hp8HPi1",
43
- "koOPCRPPNaZcQZp1icPq8KlluZk2",
44
- "VuT0LASAitWqB0L1Vn1aL6nAwaw2",
45
- "ODc6kNR5jMVV8cFWdKueNSUkMUm1",
46
- "usrkanvWkzdB8CwFnpSLgt9kiGD3",
47
- "akSj1Q2GUOZopvYaLLTDtXBBOYE2",
48
- "61SQrwQAp7N182uwv7M2cCismdh1"
49
- ];
50
- }
51
- });
52
-
53
36
  // node_modules/commander/lib/error.js
54
37
  var require_error = __commonJS({
55
38
  "node_modules/commander/lib/error.js"(exports2) {
56
- init_define_ADMIN_UIDS();
57
39
  var CommanderError2 = class extends Error {
58
40
  /**
59
41
  * Constructs the CommanderError class
@@ -89,7 +71,6 @@ var require_error = __commonJS({
89
71
  // node_modules/commander/lib/argument.js
90
72
  var require_argument = __commonJS({
91
73
  "node_modules/commander/lib/argument.js"(exports2) {
92
- init_define_ADMIN_UIDS();
93
74
  var { InvalidArgumentError: InvalidArgumentError2 } = require_error();
94
75
  var Argument2 = class {
95
76
  /**
@@ -217,7 +198,6 @@ var require_argument = __commonJS({
217
198
  // node_modules/commander/lib/help.js
218
199
  var require_help = __commonJS({
219
200
  "node_modules/commander/lib/help.js"(exports2) {
220
- init_define_ADMIN_UIDS();
221
201
  var { humanReadableArgName } = require_argument();
222
202
  var Help2 = class {
223
203
  constructor() {
@@ -632,7 +612,6 @@ var require_help = __commonJS({
632
612
  // node_modules/commander/lib/option.js
633
613
  var require_option = __commonJS({
634
614
  "node_modules/commander/lib/option.js"(exports2) {
635
- init_define_ADMIN_UIDS();
636
615
  var { InvalidArgumentError: InvalidArgumentError2 } = require_error();
637
616
  var Option2 = class {
638
617
  /**
@@ -905,7 +884,6 @@ var require_option = __commonJS({
905
884
  // node_modules/commander/lib/suggestSimilar.js
906
885
  var require_suggestSimilar = __commonJS({
907
886
  "node_modules/commander/lib/suggestSimilar.js"(exports2) {
908
- init_define_ADMIN_UIDS();
909
887
  var maxDistance = 3;
910
888
  function editDistance(a, b) {
911
889
  if (Math.abs(a.length - b.length) > maxDistance)
@@ -986,11 +964,10 @@ var require_suggestSimilar = __commonJS({
986
964
  // node_modules/commander/lib/command.js
987
965
  var require_command = __commonJS({
988
966
  "node_modules/commander/lib/command.js"(exports2) {
989
- init_define_ADMIN_UIDS();
990
967
  var EventEmitter = require("node:events").EventEmitter;
991
968
  var childProcess = require("node:child_process");
992
- var path4 = require("node:path");
993
- var fs6 = require("node:fs");
969
+ var path3 = require("node:path");
970
+ var fs5 = require("node:fs");
994
971
  var process2 = require("node:process");
995
972
  var { Argument: Argument2, humanReadableArgName } = require_argument();
996
973
  var { CommanderError: CommanderError2 } = require_error();
@@ -1922,11 +1899,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
1922
1899
  let launchWithNode = false;
1923
1900
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1924
1901
  function findFile(baseDir, baseName) {
1925
- const localBin = path4.resolve(baseDir, baseName);
1926
- if (fs6.existsSync(localBin)) return localBin;
1927
- if (sourceExt.includes(path4.extname(baseName))) return void 0;
1902
+ const localBin = path3.resolve(baseDir, baseName);
1903
+ if (fs5.existsSync(localBin)) return localBin;
1904
+ if (sourceExt.includes(path3.extname(baseName))) return void 0;
1928
1905
  const foundExt = sourceExt.find(
1929
- (ext) => fs6.existsSync(`${localBin}${ext}`)
1906
+ (ext) => fs5.existsSync(`${localBin}${ext}`)
1930
1907
  );
1931
1908
  if (foundExt) return `${localBin}${foundExt}`;
1932
1909
  return void 0;
@@ -1938,21 +1915,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
1938
1915
  if (this._scriptPath) {
1939
1916
  let resolvedScriptPath;
1940
1917
  try {
1941
- resolvedScriptPath = fs6.realpathSync(this._scriptPath);
1918
+ resolvedScriptPath = fs5.realpathSync(this._scriptPath);
1942
1919
  } catch (err) {
1943
1920
  resolvedScriptPath = this._scriptPath;
1944
1921
  }
1945
- executableDir = path4.resolve(
1946
- path4.dirname(resolvedScriptPath),
1922
+ executableDir = path3.resolve(
1923
+ path3.dirname(resolvedScriptPath),
1947
1924
  executableDir
1948
1925
  );
1949
1926
  }
1950
1927
  if (executableDir) {
1951
1928
  let localFile = findFile(executableDir, executableFile);
1952
1929
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
1953
- const legacyName = path4.basename(
1930
+ const legacyName = path3.basename(
1954
1931
  this._scriptPath,
1955
- path4.extname(this._scriptPath)
1932
+ path3.extname(this._scriptPath)
1956
1933
  );
1957
1934
  if (legacyName !== this._name) {
1958
1935
  localFile = findFile(
@@ -1963,7 +1940,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1963
1940
  }
1964
1941
  executableFile = localFile || executableFile;
1965
1942
  }
1966
- launchWithNode = sourceExt.includes(path4.extname(executableFile));
1943
+ launchWithNode = sourceExt.includes(path3.extname(executableFile));
1967
1944
  let proc;
1968
1945
  if (process2.platform !== "win32") {
1969
1946
  if (launchWithNode) {
@@ -2803,7 +2780,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2803
2780
  * @return {Command}
2804
2781
  */
2805
2782
  nameFromFilename(filename) {
2806
- this._name = path4.basename(filename, path4.extname(filename));
2783
+ this._name = path3.basename(filename, path3.extname(filename));
2807
2784
  return this;
2808
2785
  }
2809
2786
  /**
@@ -2817,9 +2794,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
2817
2794
  * @param {string} [path]
2818
2795
  * @return {(string|null|Command)}
2819
2796
  */
2820
- executableDir(path5) {
2821
- if (path5 === void 0) return this._executableDir;
2822
- this._executableDir = path5;
2797
+ executableDir(path4) {
2798
+ if (path4 === void 0) return this._executableDir;
2799
+ this._executableDir = path4;
2823
2800
  return this;
2824
2801
  }
2825
2802
  /**
@@ -3030,7 +3007,6 @@ Expecting one of '${allowedValues.join("', '")}'`);
3030
3007
  // node_modules/commander/index.js
3031
3008
  var require_commander = __commonJS({
3032
3009
  "node_modules/commander/index.js"(exports2) {
3033
- init_define_ADMIN_UIDS();
3034
3010
  var { Argument: Argument2 } = require_argument();
3035
3011
  var { Command: Command2 } = require_command();
3036
3012
  var { CommanderError: CommanderError2, InvalidArgumentError: InvalidArgumentError2 } = require_error();
@@ -3053,7 +3029,6 @@ var require_commander = __commonJS({
3053
3029
  // node_modules/picocolors/picocolors.js
3054
3030
  var require_picocolors = __commonJS({
3055
3031
  "node_modules/picocolors/picocolors.js"(exports2, module2) {
3056
- init_define_ADMIN_UIDS();
3057
3032
  var p = process || {};
3058
3033
  var argv = p.argv || [];
3059
3034
  var env = p.env || {};
@@ -3211,7 +3186,6 @@ var DEFAULT_TIMEOUT, MAX_RETRIES, RETRYABLE_STATUS, RETRYABLE_NETWORK_CODES, htt
3211
3186
  var init_http = __esm({
3212
3187
  "src/cli/lib/http.ts"() {
3213
3188
  "use strict";
3214
- init_define_ADMIN_UIDS();
3215
3189
  DEFAULT_TIMEOUT = 3e4;
3216
3190
  MAX_RETRIES = 3;
3217
3191
  RETRYABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
@@ -3243,31 +3217,333 @@ var init_http = __esm({
3243
3217
  }
3244
3218
  });
3245
3219
 
3246
- // src/cli/lib/config.ts
3247
- var config_exports = {};
3248
- __export(config_exports, {
3249
- getFirebaseApiKey: () => getFirebaseApiKey,
3250
- getFirebaseAppId: () => getFirebaseAppId,
3251
- getFirebaseProjectId: () => getFirebaseProjectId,
3252
- getFirestoreBaseUrl: () => getFirestoreBaseUrl
3220
+ // src/cli/lib/dirs.ts
3221
+ function getConfigDir() {
3222
+ if (process.env.NUMO_CONFIG_DIR) {
3223
+ return process.env.NUMO_CONFIG_DIR;
3224
+ }
3225
+ const xdgHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
3226
+ const xdgDir = path.join(xdgHome, "numo");
3227
+ if (fs.existsSync(xdgDir)) return xdgDir;
3228
+ if (fs.existsSync(LEGACY_DIR)) return LEGACY_DIR;
3229
+ return xdgDir;
3230
+ }
3231
+ function ensureConfigDir() {
3232
+ const dir = getConfigDir();
3233
+ if (!fs.existsSync(dir)) {
3234
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
3235
+ }
3236
+ return dir;
3237
+ }
3238
+ function getCredentialsPath() {
3239
+ return path.join(getConfigDir(), "credentials.json");
3240
+ }
3241
+ function migrateIfNeeded() {
3242
+ const xdgHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
3243
+ const xdgDir = path.join(xdgHome, "numo");
3244
+ if (process.env.NUMO_CONFIG_DIR) return;
3245
+ const legacyCreds = path.join(LEGACY_DIR, "credentials.json");
3246
+ if (!fs.existsSync(legacyCreds) || fs.existsSync(xdgDir)) return;
3247
+ try {
3248
+ fs.mkdirSync(xdgDir, { recursive: true, mode: 448 });
3249
+ const data = fs.readFileSync(legacyCreds, "utf8");
3250
+ fs.writeFileSync(path.join(xdgDir, "credentials.json"), data, { mode: 384 });
3251
+ const legacyStreaks = path.join(LEGACY_DIR, "streaks.json");
3252
+ if (fs.existsSync(legacyStreaks)) {
3253
+ const streaksData = fs.readFileSync(legacyStreaks, "utf8");
3254
+ fs.writeFileSync(path.join(xdgDir, "streaks.json"), streaksData, { mode: 384 });
3255
+ }
3256
+ process.stderr.write(`Migrated config from ${LEGACY_DIR} to ${xdgDir}
3257
+ `);
3258
+ } catch {
3259
+ }
3260
+ }
3261
+ var fs, path, os, LEGACY_DIR;
3262
+ var init_dirs = __esm({
3263
+ "src/cli/lib/dirs.ts"() {
3264
+ "use strict";
3265
+ fs = __toESM(require("fs"), 1);
3266
+ path = __toESM(require("path"), 1);
3267
+ os = __toESM(require("os"), 1);
3268
+ LEGACY_DIR = path.join(os.homedir(), ".numo");
3269
+ }
3270
+ });
3271
+
3272
+ // src/cli/lib/errors.ts
3273
+ function classifyError(err) {
3274
+ if (err instanceof CliError) return err;
3275
+ const axiosErr = err;
3276
+ if (axiosErr.code === "ECONNABORTED" || axiosErr.code === "ETIMEDOUT") return Errors.timeout();
3277
+ if (axiosErr.code === "ENOTFOUND" || axiosErr.code === "EAI_AGAIN") return Errors.networkError();
3278
+ if (axiosErr.code === "ECONNREFUSED" || axiosErr.code === "ECONNRESET") {
3279
+ return Errors.networkError("Service may be temporarily down. Try again in a moment.");
3280
+ }
3281
+ const status = axiosErr.response?.status;
3282
+ if (status === 401) return Errors.authRequired();
3283
+ if (status === 403) {
3284
+ return new CliError("AUTH_FORBIDDEN" /* AUTH_FORBIDDEN */, "Access denied", ExitCode.NO_PERM, {
3285
+ hint: "You don't have permission for this action."
3286
+ });
3287
+ }
3288
+ if (status === 404) return Errors.notFound("Resource");
3289
+ if (status === 429) {
3290
+ const retryAfter = parseInt(axiosErr.response?.headers?.["retry-after"] ?? "");
3291
+ return Errors.rateLimited(isNaN(retryAfter) ? void 0 : retryAfter);
3292
+ }
3293
+ if (status && status >= 500) {
3294
+ return new CliError("SERVICE_UNAVAILABLE" /* SERVICE_UNAVAILABLE */, "Server error", ExitCode.UNAVAILABLE, {
3295
+ hint: "This is on our end. Try again in a moment.",
3296
+ retryable: true
3297
+ });
3298
+ }
3299
+ const body = axiosErr.response?.data;
3300
+ const raw = body?.error?.message ?? axiosErr.message ?? "Unknown error";
3301
+ const message = sanitizeErrorMessage(raw);
3302
+ return new CliError("UNKNOWN" /* UNKNOWN */, message, ExitCode.GENERAL, { cause: err });
3303
+ }
3304
+ function sanitizeErrorMessage(msg) {
3305
+ return msg.replace(/https?:\/\/\S+/g, "<url>").replace(/\/(?:Users|home|var|tmp)\/\S+/g, "<path>").replace(/[A-Za-z0-9_-]{40,}/g, "<token>");
3306
+ }
3307
+ var ExitCode, CliError, Errors;
3308
+ var init_errors = __esm({
3309
+ "src/cli/lib/errors.ts"() {
3310
+ "use strict";
3311
+ ExitCode = {
3312
+ OK: 0,
3313
+ GENERAL: 1,
3314
+ USAGE: 2,
3315
+ UNAVAILABLE: 69,
3316
+ TEMP_FAIL: 75,
3317
+ NO_PERM: 77,
3318
+ CONFIG: 78,
3319
+ NOT_FOUND: 100,
3320
+ CONFLICT: 101
3321
+ };
3322
+ CliError = class extends Error {
3323
+ constructor(kind, message, exitCode = ExitCode.GENERAL, options = {}) {
3324
+ super(message);
3325
+ this.kind = kind;
3326
+ this.exitCode = exitCode;
3327
+ this.options = options;
3328
+ this.name = "CliError";
3329
+ }
3330
+ toJSON() {
3331
+ return {
3332
+ error: {
3333
+ kind: this.kind,
3334
+ code: this.exitCode,
3335
+ message: this.message,
3336
+ ...this.options.suggestion && { suggestion: this.options.suggestion },
3337
+ ...this.options.hint && { hint: this.options.hint },
3338
+ retryable: this.options.retryable ?? false,
3339
+ ...this.options.retryAfter != null && { retryAfter: this.options.retryAfter }
3340
+ }
3341
+ };
3342
+ }
3343
+ };
3344
+ Errors = {
3345
+ authRequired: () => new CliError("AUTH_REQUIRED" /* AUTH_REQUIRED */, "Not logged in", ExitCode.NO_PERM, {
3346
+ suggestion: "numo login"
3347
+ }),
3348
+ notFound: (resource, id) => new CliError("NOT_FOUND" /* NOT_FOUND */, `${resource} not found${id ? `: ${id}` : ""}`, ExitCode.NOT_FOUND, {
3349
+ suggestion: `numo ${resource.toLowerCase()}s list`
3350
+ }),
3351
+ missingArg: (name, flag) => new CliError("MISSING_ARGUMENT" /* MISSING_ARGUMENT */, `${name} is required`, ExitCode.USAGE, {
3352
+ suggestion: `Use --${flag}`,
3353
+ hint: "Run with --help for all options."
3354
+ }),
3355
+ invalidInput: (message, hint) => new CliError("INVALID_INPUT" /* INVALID_INPUT */, message, ExitCode.USAGE, { hint }),
3356
+ configMissing: (key) => new CliError("CONFIG_ERROR" /* CONFIG_ERROR */, `${key} not set`, ExitCode.CONFIG, {
3357
+ suggestion: `export ${key}=<value>`
3358
+ }),
3359
+ networkError: (hint) => new CliError("NETWORK_ERROR" /* NETWORK_ERROR */, "Can't reach Numo servers", ExitCode.UNAVAILABLE, {
3360
+ hint: hint ?? "Check your internet connection.",
3361
+ retryable: true
3362
+ }),
3363
+ timeout: () => new CliError("TIMEOUT" /* TIMEOUT */, "Request timed out", ExitCode.TEMP_FAIL, {
3364
+ hint: "The server took too long to respond. Try again.",
3365
+ retryable: true
3366
+ }),
3367
+ rateLimited: (retryAfter) => new CliError("RATE_LIMITED" /* RATE_LIMITED */, "Too many requests", ExitCode.TEMP_FAIL, {
3368
+ hint: retryAfter ? `Wait ${retryAfter} seconds and try again.` : "Wait a moment and try again.",
3369
+ retryable: true,
3370
+ retryAfter
3371
+ })
3372
+ };
3373
+ }
3374
+ });
3375
+
3376
+ // src/cli/lib/api-client.ts
3377
+ var api_client_exports = {};
3378
+ __export(api_client_exports, {
3379
+ API_BASE: () => API_BASE,
3380
+ api: () => api
3381
+ });
3382
+ function toCliError(err) {
3383
+ if (err instanceof CliError) return err;
3384
+ const httpErr = err;
3385
+ if (httpErr.code === "ECONNABORTED" || httpErr.code === "ETIMEDOUT") {
3386
+ return new CliError("TIMEOUT" /* TIMEOUT */, "Request timed out", ExitCode.TEMP_FAIL, {
3387
+ hint: "The API server took too long to respond.",
3388
+ retryable: true
3389
+ });
3390
+ }
3391
+ if (httpErr.code === "ECONNREFUSED" || httpErr.code === "ECONNRESET" || httpErr.code === "ENOTFOUND") {
3392
+ return new CliError("NETWORK_ERROR" /* NETWORK_ERROR */, "Can't reach Numo API", ExitCode.UNAVAILABLE, {
3393
+ hint: "Is the API server running? Check NUMO_API_URL.",
3394
+ retryable: true
3395
+ });
3396
+ }
3397
+ const body = httpErr.response?.data;
3398
+ if (body?.error) {
3399
+ const e = body.error;
3400
+ const kind = e.kind ?? "UNKNOWN" /* UNKNOWN */;
3401
+ const exitCode = KIND_EXIT[kind] ?? ExitCode.GENERAL;
3402
+ return new CliError(kind, e.message ?? "Unknown error", exitCode, {
3403
+ retryable: e.retryable,
3404
+ retryAfter: e.retryAfter
3405
+ });
3406
+ }
3407
+ return new CliError("UNKNOWN" /* UNKNOWN */, httpErr.message ?? "Unknown error", ExitCode.GENERAL);
3408
+ }
3409
+ async function apiHeaders() {
3410
+ const token = await getIdToken();
3411
+ return {
3412
+ Authorization: `Bearer ${token}`,
3413
+ "Content-Type": "application/json"
3414
+ };
3415
+ }
3416
+ function url(path3, params) {
3417
+ const u2 = `${API_BASE}${path3}`;
3418
+ if (!params) return u2;
3419
+ const sp = new URLSearchParams();
3420
+ for (const [k2, v] of Object.entries(params)) {
3421
+ if (v !== void 0) sp.set(k2, v);
3422
+ }
3423
+ const qs = sp.toString();
3424
+ return qs ? `${u2}?${qs}` : u2;
3425
+ }
3426
+ var API_BASE, KIND_EXIT, api;
3427
+ var init_api_client = __esm({
3428
+ "src/cli/lib/api-client.ts"() {
3429
+ "use strict";
3430
+ init_credentials();
3431
+ init_http();
3432
+ init_errors();
3433
+ API_BASE = process.env.NUMO_API_URL ?? (true ? "https://api.numo.ai" : "http://localhost:3000");
3434
+ if (API_BASE !== "http://localhost:3000" && API_BASE.startsWith("http://")) {
3435
+ process.stderr.write("[warn] NUMO_API_URL uses HTTP \u2014 tokens sent unencrypted. Use HTTPS in production.\n");
3436
+ }
3437
+ KIND_EXIT = {
3438
+ AUTH_REQUIRED: ExitCode.NO_PERM,
3439
+ AUTH_EXPIRED: ExitCode.NO_PERM,
3440
+ AUTH_FORBIDDEN: ExitCode.NO_PERM,
3441
+ INVALID_INPUT: ExitCode.USAGE,
3442
+ MISSING_ARGUMENT: ExitCode.USAGE,
3443
+ NOT_FOUND: ExitCode.NOT_FOUND,
3444
+ CONFLICT: ExitCode.CONFLICT,
3445
+ RATE_LIMITED: ExitCode.TEMP_FAIL,
3446
+ NETWORK_ERROR: ExitCode.UNAVAILABLE,
3447
+ TIMEOUT: ExitCode.TEMP_FAIL,
3448
+ SERVICE_UNAVAILABLE: ExitCode.UNAVAILABLE
3449
+ };
3450
+ api = {
3451
+ async get(path3, params) {
3452
+ try {
3453
+ const resp = await http.get(url(path3, params), { headers: await apiHeaders() });
3454
+ return resp.data;
3455
+ } catch (err) {
3456
+ throw toCliError(err);
3457
+ }
3458
+ },
3459
+ async post(path3, body) {
3460
+ try {
3461
+ const resp = await http.post(url(path3), body, { headers: await apiHeaders() });
3462
+ return resp.data;
3463
+ } catch (err) {
3464
+ throw toCliError(err);
3465
+ }
3466
+ },
3467
+ async patch(path3, body) {
3468
+ try {
3469
+ const resp = await http.patch(url(path3), body, { headers: await apiHeaders() });
3470
+ return resp.data;
3471
+ } catch (err) {
3472
+ throw toCliError(err);
3473
+ }
3474
+ },
3475
+ async del(path3) {
3476
+ try {
3477
+ const resp = await http.delete(url(path3), { headers: await apiHeaders() });
3478
+ return resp.data;
3479
+ } catch (err) {
3480
+ throw toCliError(err);
3481
+ }
3482
+ }
3483
+ };
3484
+ }
3253
3485
  });
3254
- function getFirestoreBaseUrl() {
3255
- const projectId = getFirebaseProjectId();
3256
- return `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents`;
3486
+
3487
+ // src/cli/auth/credentials.ts
3488
+ function loadCredentials() {
3489
+ try {
3490
+ const data = JSON.parse(fs2.readFileSync(getCredentialsPath(), "utf8"));
3491
+ if (typeof data?.refreshToken !== "string" || typeof data?.uid !== "string" || typeof data?.email !== "string") {
3492
+ return null;
3493
+ }
3494
+ return data;
3495
+ } catch {
3496
+ return null;
3497
+ }
3498
+ }
3499
+ function saveCredentials(creds) {
3500
+ ensureConfigDir();
3501
+ fs2.writeFileSync(getCredentialsPath(), JSON.stringify(creds, null, 2), { mode: 384 });
3257
3502
  }
3258
- function getFirebaseApiKey() {
3259
- return process.env.NUMO_FIREBASE_API_KEY ?? (true ? "AIzaSyAwJdEvE-ZPyGzAlJdqt_bMSsoSrHonniA" : "");
3503
+ function clearCredentials() {
3504
+ try {
3505
+ const credPath = getCredentialsPath();
3506
+ const stat = fs2.statSync(credPath);
3507
+ fs2.writeFileSync(credPath, crypto.randomBytes(stat.size));
3508
+ fs2.unlinkSync(credPath);
3509
+ } catch {
3510
+ }
3260
3511
  }
3261
- function getFirebaseProjectId() {
3262
- return process.env.NUMO_FIREBASE_PROJECT_ID ?? (true ? "mindist-well" : "");
3512
+ async function getIdToken() {
3513
+ const envToken = process.env.NUMO_TOKEN;
3514
+ if (envToken) return envToken;
3515
+ const creds = loadCredentials();
3516
+ if (!creds) throw new Error("Not logged in. Run: numo login");
3517
+ if (creds.idToken && creds.idTokenExpiry && Date.now() < creds.idTokenExpiry - 6e4) {
3518
+ return creds.idToken;
3519
+ }
3520
+ if (refreshInFlight) return refreshInFlight;
3521
+ refreshInFlight = performRefresh(creds).finally(() => {
3522
+ refreshInFlight = null;
3523
+ });
3524
+ return refreshInFlight;
3263
3525
  }
3264
- function getFirebaseAppId() {
3265
- return process.env.NUMO_FIREBASE_APP_ID ?? (true ? "1:767603956046:web:d2c5911157e1cb017a261f" : "");
3526
+ async function performRefresh(creds) {
3527
+ const { API_BASE: apiBase } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
3528
+ const { http: http2 } = await Promise.resolve().then(() => (init_http(), http_exports));
3529
+ const resp = await http2.post(
3530
+ `${apiBase}/api/auth/refresh`,
3531
+ { refreshToken: creds.refreshToken }
3532
+ );
3533
+ creds.idToken = resp.data.idToken;
3534
+ creds.refreshToken = resp.data.refreshToken ?? creds.refreshToken;
3535
+ creds.idTokenExpiry = Date.now() + (resp.data.expiresIn || 3600) * 1e3;
3536
+ saveCredentials(creds);
3537
+ return creds.idToken;
3266
3538
  }
3267
- var init_config = __esm({
3268
- "src/cli/lib/config.ts"() {
3539
+ var fs2, crypto, refreshInFlight;
3540
+ var init_credentials = __esm({
3541
+ "src/cli/auth/credentials.ts"() {
3269
3542
  "use strict";
3270
- init_define_ADMIN_UIDS();
3543
+ fs2 = __toESM(require("fs"), 1);
3544
+ crypto = __toESM(require("crypto"), 1);
3545
+ init_dirs();
3546
+ refreshInFlight = null;
3271
3547
  }
3272
3548
  });
3273
3549
 
@@ -3282,7 +3558,6 @@ var isUnicodeSupported;
3282
3558
  var init_tty = __esm({
3283
3559
  "src/cli/lib/tty.ts"() {
3284
3560
  "use strict";
3285
- init_define_ADMIN_UIDS();
3286
3561
  isUnicodeSupported = process.platform !== "win32" || !!process.env.WT_SESSION || process.env.TERM_PROGRAM === "vscode";
3287
3562
  }
3288
3563
  });
@@ -3291,7 +3566,6 @@ var init_tty = __esm({
3291
3566
  var require_src = __commonJS({
3292
3567
  "node_modules/sisteransi/src/index.js"(exports2, module2) {
3293
3568
  "use strict";
3294
- init_define_ADMIN_UIDS();
3295
3569
  var ESC = "\x1B";
3296
3570
  var CSI = `${ESC}[`;
3297
3571
  var beep = "\x07";
@@ -3433,7 +3707,6 @@ function St(t2, e) {
3433
3707
  var import_node_util, import_node_process, k, import_node_readline, import_sisteransi, import_node_tty, at, lt, ht, O, y, L, P, M, ct, ft, X, pt, S, T, Z, Ft, j, Q, dt, tt, U, et, mt, st, it, gt, G, vt, Et, At, _, bt, z, rt, nt, B, Vt, kt, yt, Lt, Mt, Tt, Wt, $t;
3434
3708
  var init_dist = __esm({
3435
3709
  "node_modules/@clack/core/dist/index.mjs"() {
3436
- init_define_ADMIN_UIDS();
3437
3710
  import_node_util = require("node:util");
3438
3711
  import_node_process = require("node:process");
3439
3712
  k = __toESM(require("node:readline"), 1);
@@ -4100,7 +4373,6 @@ function qt({ style: e = "heavy", max: r = 100, size: s = 40, ...i } = {}) {
4100
4373
  var import_node_util2, import_node_process2, import_node_fs, import_node_path, import_sisteransi2, ee, ce, Me, I2, Re, $e, de, V, he, h, x2, Oe, Pe, z2, H2, te, U2, q2, Ne, se, pe, We, me, ge, Ge, fe, Fe, ye, Ee, W2, ve, mt2, gt2, ft2, we, re, ie, Ae, ne, Ft2, yt2, Le, Et2, D2, ae, je, vt2, Ce, ke, wt2, Ve, Se, He, At2, Ue, Ke, Ct2, Ie, St2, It2, bt2, X2, Xe, xt2, _t2, Dt2, Tt2, Mt2, Rt, Ot, Pt, R2, Nt, Wt2, Gt, Q2, Lt2, jt, kt2, Vt2, Ht, Ut, Kt, be, ze, oe, Jt, Xt, Qe, K2, Yt, zt, Qt, Zt;
4101
4374
  var init_dist2 = __esm({
4102
4375
  "node_modules/@clack/prompts/dist/index.mjs"() {
4103
- init_define_ADMIN_UIDS();
4104
4376
  init_dist();
4105
4377
  init_dist();
4106
4378
  import_node_util2 = require("node:util");
@@ -5072,318 +5344,74 @@ async function promptForMissing(opts) {
5072
5344
  var init_prompts = __esm({
5073
5345
  "src/cli/lib/prompts.ts"() {
5074
5346
  "use strict";
5075
- init_define_ADMIN_UIDS();
5076
5347
  init_tty();
5077
5348
  }
5078
5349
  });
5079
5350
 
5080
- // src/cli/lib/errors.ts
5081
- function classifyError(err) {
5082
- if (err instanceof CliError) return err;
5083
- const axiosErr = err;
5084
- if (axiosErr.code === "ECONNABORTED" || axiosErr.code === "ETIMEDOUT") return Errors.timeout();
5085
- if (axiosErr.code === "ENOTFOUND" || axiosErr.code === "EAI_AGAIN") return Errors.networkError();
5086
- if (axiosErr.code === "ECONNREFUSED" || axiosErr.code === "ECONNRESET") {
5087
- return Errors.networkError("Service may be temporarily down. Try again in a moment.");
5351
+ // src/cli/auth/phone-login.ts
5352
+ var phone_login_exports = {};
5353
+ __export(phone_login_exports, {
5354
+ authenticateWithPhone: () => authenticateWithPhone
5355
+ });
5356
+ async function authenticateWithPhone(spinner) {
5357
+ const p = await Promise.resolve().then(() => (init_dist2(), dist_exports));
5358
+ const phone = await promptText({
5359
+ message: "Phone number (with country code)",
5360
+ placeholder: "+380501234567",
5361
+ required: true
5362
+ });
5363
+ if (!/^\+[0-9]{7,15}$/.test(phone)) {
5364
+ throw Errors.invalidInput("Invalid phone number. Use E.164 format: +<country code><number>", "Example: +380501234567");
5088
5365
  }
5089
- const status = axiosErr.response?.status;
5090
- if (status === 401) return Errors.authRequired();
5091
- if (status === 403) {
5092
- return new CliError("AUTH_FORBIDDEN" /* AUTH_FORBIDDEN */, "Access denied", ExitCode.NO_PERM, {
5093
- hint: "You don't have permission for this action."
5094
- });
5095
- }
5096
- if (status === 404) return Errors.notFound("Resource");
5097
- if (status === 429) {
5098
- const retryAfter = parseInt(axiosErr.response?.headers?.["retry-after"] ?? "");
5099
- return Errors.rateLimited(isNaN(retryAfter) ? void 0 : retryAfter);
5100
- }
5101
- if (status && status >= 500) {
5102
- return new CliError("SERVICE_UNAVAILABLE" /* SERVICE_UNAVAILABLE */, "Server error", ExitCode.UNAVAILABLE, {
5103
- hint: "This is on our end. Try again in a moment.",
5104
- retryable: true
5105
- });
5106
- }
5107
- const body = axiosErr.response?.data;
5108
- const raw = body?.error?.message ?? axiosErr.message ?? "Unknown error";
5109
- const message = sanitizeErrorMessage(raw);
5110
- return new CliError("UNKNOWN" /* UNKNOWN */, message, ExitCode.GENERAL, { cause: err });
5111
- }
5112
- function sanitizeErrorMessage(msg) {
5113
- return msg.replace(/https?:\/\/\S+/g, "<url>").replace(/\/(?:Users|home|var|tmp)\/\S+/g, "<path>").replace(/[A-Za-z0-9_-]{40,}/g, "<token>");
5114
- }
5115
- var ExitCode, CliError, Errors;
5116
- var init_errors = __esm({
5117
- "src/cli/lib/errors.ts"() {
5118
- "use strict";
5119
- init_define_ADMIN_UIDS();
5120
- ExitCode = {
5121
- OK: 0,
5122
- GENERAL: 1,
5123
- USAGE: 2,
5124
- UNAVAILABLE: 69,
5125
- TEMP_FAIL: 75,
5126
- NO_PERM: 77,
5127
- CONFIG: 78,
5128
- NOT_FOUND: 100,
5129
- CONFLICT: 101
5130
- };
5131
- CliError = class extends Error {
5132
- constructor(kind, message, exitCode = ExitCode.GENERAL, options = {}) {
5133
- super(message);
5134
- this.kind = kind;
5135
- this.exitCode = exitCode;
5136
- this.options = options;
5137
- this.name = "CliError";
5138
- }
5139
- toJSON() {
5366
+ spinner.start("Starting phone verification...");
5367
+ const startResp = await http.post(`${API_BASE}/api/auth/phone/start`, {
5368
+ phoneNumber: phone
5369
+ });
5370
+ const { sessionId, verifyUrl } = startResp.data;
5371
+ spinner.stop("");
5372
+ p.log.info("Opening browser for phone verification...");
5373
+ p.log.info(import_picocolors.default.dim(`If the browser does not open, visit: ${verifyUrl}`));
5374
+ const { default: open } = await import("open");
5375
+ const cp = await open(verifyUrl);
5376
+ cp.unref();
5377
+ spinner.start("Waiting for verification in browser...");
5378
+ const deadline = Date.now() + POLL_TIMEOUT;
5379
+ while (Date.now() < deadline) {
5380
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL));
5381
+ try {
5382
+ const pollResp = await http.get(`${API_BASE}/api/auth/phone/poll?session=${sessionId}`);
5383
+ if (pollResp.status === 200 && pollResp.data.idToken) {
5140
5384
  return {
5141
- error: {
5142
- kind: this.kind,
5143
- code: this.exitCode,
5144
- message: this.message,
5145
- ...this.options.suggestion && { suggestion: this.options.suggestion },
5146
- ...this.options.hint && { hint: this.options.hint },
5147
- retryable: this.options.retryable ?? false,
5148
- ...this.options.retryAfter != null && { retryAfter: this.options.retryAfter }
5149
- }
5385
+ refreshToken: pollResp.data.refreshToken,
5386
+ uid: pollResp.data.uid,
5387
+ displayName: phone,
5388
+ idToken: pollResp.data.idToken,
5389
+ idTokenExpiry: Date.now() + (pollResp.data.expiresIn || 3600) * 1e3
5150
5390
  };
5151
5391
  }
5152
- };
5153
- Errors = {
5154
- authRequired: () => new CliError("AUTH_REQUIRED" /* AUTH_REQUIRED */, "Not logged in", ExitCode.NO_PERM, {
5155
- suggestion: "numo login"
5156
- }),
5157
- notFound: (resource, id) => new CliError("NOT_FOUND" /* NOT_FOUND */, `${resource} not found${id ? `: ${id}` : ""}`, ExitCode.NOT_FOUND, {
5158
- suggestion: `numo ${resource.toLowerCase()}s list`
5159
- }),
5160
- missingArg: (name, flag) => new CliError("MISSING_ARGUMENT" /* MISSING_ARGUMENT */, `${name} is required`, ExitCode.USAGE, {
5161
- suggestion: `Use --${flag}`,
5162
- hint: "Run with --help for all options."
5163
- }),
5164
- invalidInput: (message, hint) => new CliError("INVALID_INPUT" /* INVALID_INPUT */, message, ExitCode.USAGE, { hint }),
5165
- configMissing: (key) => new CliError("CONFIG_ERROR" /* CONFIG_ERROR */, `${key} not set`, ExitCode.CONFIG, {
5166
- suggestion: `export ${key}=<value>`
5167
- }),
5168
- networkError: (hint) => new CliError("NETWORK_ERROR" /* NETWORK_ERROR */, "Can't reach Numo servers", ExitCode.UNAVAILABLE, {
5169
- hint: hint ?? "Check your internet connection.",
5170
- retryable: true
5171
- }),
5172
- timeout: () => new CliError("TIMEOUT" /* TIMEOUT */, "Request timed out", ExitCode.TEMP_FAIL, {
5173
- hint: "The server took too long to respond. Try again.",
5174
- retryable: true
5175
- }),
5176
- rateLimited: (retryAfter) => new CliError("RATE_LIMITED" /* RATE_LIMITED */, "Too many requests", ExitCode.TEMP_FAIL, {
5177
- hint: retryAfter ? `Wait ${retryAfter} seconds and try again.` : "Wait a moment and try again.",
5178
- retryable: true,
5179
- retryAfter
5180
- })
5181
- };
5182
- }
5183
- });
5184
-
5185
- // src/cli/auth/local-server.ts
5186
- function findFreePort() {
5187
- return new Promise((resolve, reject) => {
5188
- const server = net.createServer();
5189
- server.listen(0, "127.0.0.1", () => {
5190
- const address = server.address();
5191
- server.close(() => resolve(address.port));
5192
- });
5193
- server.on("error", reject);
5194
- });
5195
- }
5196
- function serveHtmlAndWaitForCallback(port, html, expectedState) {
5197
- return new Promise((resolve, reject) => {
5198
- const connections = /* @__PURE__ */ new Set();
5199
- const server = http2.createServer((req, res) => {
5200
- const parsed = url.parse(req.url ?? "", true);
5201
- if (parsed.pathname === "/callback") {
5202
- if (parsed.query.state !== expectedState) {
5203
- res.writeHead(403, { "Content-Type": "text/html", "Connection": "close" });
5204
- res.end("<html><body><h2>Invalid state parameter. Authentication rejected.</h2></body></html>");
5205
- return;
5206
- }
5207
- res.writeHead(200, { "Content-Type": "text/html", "Connection": "close", "Cache-Control": "no-store" });
5208
- res.end("<html><body><h2>Authentication complete. You can close this tab.</h2></body></html>");
5209
- clearTimeout(timer);
5210
- server.close();
5211
- for (const conn of connections) conn.destroy();
5212
- resolve(parsed.query);
5213
- return;
5214
- }
5215
- if (parsed.pathname !== "/") {
5216
- res.writeHead(404, { "Content-Type": "text/plain", "Connection": "close" });
5217
- res.end("Not Found");
5218
- return;
5392
+ } catch (err) {
5393
+ if (err.response?.status === 404) {
5394
+ throw Errors.networkError("Verification session expired. Try again.");
5219
5395
  }
5220
- res.writeHead(200, {
5221
- "Content-Type": "text/html",
5222
- "Cache-Control": "no-store",
5223
- "Content-Security-Policy": "default-src 'self' https://www.gstatic.com 'unsafe-inline'"
5224
- });
5225
- res.end(html);
5226
- });
5227
- server.on("connection", (conn) => {
5228
- connections.add(conn);
5229
- conn.on("close", () => connections.delete(conn));
5230
- });
5231
- server.listen(port, "127.0.0.1");
5232
- server.on("error", reject);
5233
- const timer = setTimeout(() => {
5234
- server.close();
5235
- for (const conn of connections) conn.destroy();
5236
- reject(new Error("Timeout waiting for authentication (5 min)"));
5237
- }, 5 * 60 * 1e3);
5238
- timer.unref();
5239
- });
5240
- }
5241
- var http2, net, url;
5242
- var init_local_server = __esm({
5243
- "src/cli/auth/local-server.ts"() {
5244
- "use strict";
5245
- init_define_ADMIN_UIDS();
5246
- http2 = __toESM(require("http"), 1);
5247
- net = __toESM(require("net"), 1);
5248
- url = __toESM(require("url"), 1);
5249
- }
5250
- });
5251
-
5252
- // src/cli/auth/phone-login.ts
5253
- var phone_login_exports = {};
5254
- __export(phone_login_exports, {
5255
- authenticateWithPhone: () => authenticateWithPhone
5256
- });
5257
- function buildSmsPageHtml(apiKey, projectId, appId, phoneNumber, callbackUrl, state) {
5258
- const configJson = JSON.stringify({ apiKey, projectId, appId, phoneNumber, callbackUrl, state });
5259
- return `<!DOCTYPE html>
5260
- <html>
5261
- <head>
5262
- <meta charset="UTF-8">
5263
- <title>Numo \u2014 Sending SMS</title>
5264
- <style>
5265
- body { font-family: sans-serif; max-width: 420px; margin: 80px auto; padding: 20px; text-align: center; }
5266
- .spinner { display: inline-block; width: 20px; height: 20px; border: 3px solid #ccc; border-top-color: #4285f4; border-radius: 50%; animation: spin 0.8s linear infinite; vertical-align: middle; margin-right: 8px; }
5267
- @keyframes spin { to { transform: rotate(360deg); } }
5268
- .error { color: #d32f2f; font-size: 14px; margin-top: 16px; }
5269
- .success { color: #2e7d32; }
5270
- </style>
5271
- </head>
5272
- <body>
5273
- <h2>Numo</h2>
5274
- <p id="status"><span class="spinner"></span> Sending SMS...</p>
5275
- <p id="error" class="error"></p>
5276
-
5277
- <div id="recaptcha-container"></div>
5278
-
5279
- <script>window.__NUMO__ = ${configJson};</script>
5280
- <script type="module">
5281
- import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.7.0/firebase-app.js';
5282
- import { getAuth, RecaptchaVerifier, signInWithPhoneNumber } from 'https://www.gstatic.com/firebasejs/10.7.0/firebase-auth.js';
5283
-
5284
- const cfg = window.__NUMO__;
5285
- const app = initializeApp({
5286
- apiKey: cfg.apiKey,
5287
- projectId: cfg.projectId,
5288
- appId: cfg.appId,
5289
- authDomain: cfg.projectId + '.firebaseapp.com',
5290
- });
5291
- const auth = getAuth(app);
5292
-
5293
- try {
5294
- const verifier = new RecaptchaVerifier(auth, 'recaptcha-container', { size: 'invisible' });
5295
- const confirmationResult = await signInWithPhoneNumber(auth, cfg.phoneNumber, verifier);
5296
-
5297
- const params = new URLSearchParams({ verificationId: confirmationResult.verificationId, state: cfg.state });
5298
- document.getElementById('status').innerHTML = '<span class="success">SMS sent! Return to the terminal to enter the code.</span>';
5299
- window.location.href = cfg.callbackUrl + '?' + params.toString();
5300
- } catch (e) {
5301
- document.getElementById('status').textContent = '';
5302
- document.getElementById('error').textContent = e.message;
5303
5396
  }
5304
- </script>
5305
- </body>
5306
- </html>`;
5307
- }
5308
- async function authenticateWithPhone(spinner) {
5309
- const fbApiKey = getFirebaseApiKey();
5310
- const projectId = getFirebaseProjectId();
5311
- const appId = getFirebaseAppId();
5312
- if (!fbApiKey) {
5313
- throw Errors.configMissing("NUMO_FIREBASE_API_KEY");
5314
- }
5315
- if (!projectId) {
5316
- throw Errors.configMissing("NUMO_FIREBASE_PROJECT_ID");
5317
- }
5318
- if (!appId) {
5319
- throw Errors.configMissing("NUMO_FIREBASE_APP_ID");
5320
- }
5321
- const p = await Promise.resolve().then(() => (init_dist2(), dist_exports));
5322
- const phone = await promptText({
5323
- message: "Phone number (with country code)",
5324
- placeholder: "+380501234567",
5325
- required: true
5326
- });
5327
- if (!/^\+[0-9]{7,15}$/.test(phone)) {
5328
- throw Errors.invalidInput("Invalid phone number. Use E.164 format: +<country code><number>", "Example: +380501234567");
5329
- }
5330
- const port = await findFreePort();
5331
- const callbackUrl = `http://localhost:${port}/callback`;
5332
- const state = crypto2.randomUUID();
5333
- const html = buildSmsPageHtml(fbApiKey, projectId, appId, phone, callbackUrl, state);
5334
- p.log.info("Opening browser to send SMS...");
5335
- p.log.info(import_picocolors.default.dim(`If the browser does not open, visit: http://localhost:${port}`));
5336
- const { default: open } = await import("open");
5337
- const cp = await open(`http://localhost:${port}`);
5338
- cp.unref();
5339
- const callbackParams = await serveHtmlAndWaitForCallback(port, html, state);
5340
- const verificationId = callbackParams.verificationId;
5341
- if (!verificationId) {
5342
- throw Errors.networkError("Failed to send SMS. Try again.");
5343
- }
5344
- p.log.success("SMS sent");
5345
- const otp = await promptText({
5346
- message: "Enter the 6-digit code from SMS",
5347
- placeholder: "123456",
5348
- required: true
5349
- });
5350
- spinner.start("Verifying code...");
5351
- const resp = await http.post(
5352
- `https://identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber?key=${fbApiKey}`,
5353
- { sessionInfo: verificationId, code: otp }
5354
- );
5355
- const { refreshToken, localId: uid, idToken, phoneNumber } = resp.data;
5356
- if (!refreshToken || !uid) {
5357
- throw new Error("Incomplete credentials received");
5358
5397
  }
5359
- return {
5360
- refreshToken,
5361
- uid,
5362
- displayName: phoneNumber || phone,
5363
- idToken,
5364
- idTokenExpiry: idToken ? Date.now() + 3600 * 1e3 : void 0
5365
- };
5398
+ throw Errors.networkError("Phone verification timed out. Try again.");
5366
5399
  }
5367
- var crypto2, import_picocolors;
5400
+ var import_picocolors, POLL_INTERVAL, POLL_TIMEOUT;
5368
5401
  var init_phone_login = __esm({
5369
5402
  "src/cli/auth/phone-login.ts"() {
5370
5403
  "use strict";
5371
- init_define_ADMIN_UIDS();
5372
- crypto2 = __toESM(require("crypto"), 1);
5373
5404
  init_http();
5374
5405
  import_picocolors = __toESM(require_picocolors(), 1);
5375
5406
  init_errors();
5376
- init_local_server();
5377
- init_config();
5378
5407
  init_prompts();
5408
+ init_api_client();
5409
+ POLL_INTERVAL = 2e3;
5410
+ POLL_TIMEOUT = 5 * 60 * 1e3;
5379
5411
  }
5380
5412
  });
5381
5413
 
5382
- // src/cli/cli.ts
5383
- init_define_ADMIN_UIDS();
5384
-
5385
5414
  // node_modules/commander/esm.mjs
5386
- init_define_ADMIN_UIDS();
5387
5415
  var import_index = __toESM(require_commander(), 1);
5388
5416
  var {
5389
5417
  program,
@@ -5404,178 +5432,34 @@ var {
5404
5432
  var import_picocolors13 = __toESM(require_picocolors(), 1);
5405
5433
 
5406
5434
  // src/cli/auth/login.ts
5407
- init_define_ADMIN_UIDS();
5408
5435
  init_http();
5409
5436
  var import_picocolors2 = __toESM(require_picocolors(), 1);
5410
-
5411
- // src/cli/auth/credentials.ts
5412
- init_define_ADMIN_UIDS();
5413
- var fs2 = __toESM(require("fs"), 1);
5414
- var crypto = __toESM(require("crypto"), 1);
5415
-
5416
- // src/cli/lib/dirs.ts
5417
- init_define_ADMIN_UIDS();
5418
- var fs = __toESM(require("fs"), 1);
5419
- var path = __toESM(require("path"), 1);
5420
- var os = __toESM(require("os"), 1);
5421
- var LEGACY_DIR = path.join(os.homedir(), ".numo");
5422
- function getConfigDir() {
5423
- if (process.env.NUMO_CONFIG_DIR) {
5424
- return process.env.NUMO_CONFIG_DIR;
5425
- }
5426
- const xdgHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
5427
- const xdgDir = path.join(xdgHome, "numo");
5428
- if (fs.existsSync(xdgDir)) return xdgDir;
5429
- if (fs.existsSync(LEGACY_DIR)) return LEGACY_DIR;
5430
- return xdgDir;
5431
- }
5432
- function ensureConfigDir() {
5433
- const dir = getConfigDir();
5434
- if (!fs.existsSync(dir)) {
5435
- fs.mkdirSync(dir, { recursive: true, mode: 448 });
5436
- }
5437
- return dir;
5438
- }
5439
- function getCredentialsPath() {
5440
- return path.join(getConfigDir(), "credentials.json");
5441
- }
5442
- function migrateIfNeeded() {
5443
- const xdgHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
5444
- const xdgDir = path.join(xdgHome, "numo");
5445
- if (process.env.NUMO_CONFIG_DIR) return;
5446
- const legacyCreds = path.join(LEGACY_DIR, "credentials.json");
5447
- if (!fs.existsSync(legacyCreds) || fs.existsSync(xdgDir)) return;
5448
- try {
5449
- fs.mkdirSync(xdgDir, { recursive: true, mode: 448 });
5450
- const data = fs.readFileSync(legacyCreds, "utf8");
5451
- fs.writeFileSync(path.join(xdgDir, "credentials.json"), data, { mode: 384 });
5452
- const legacyStreaks = path.join(LEGACY_DIR, "streaks.json");
5453
- if (fs.existsSync(legacyStreaks)) {
5454
- const streaksData = fs.readFileSync(legacyStreaks, "utf8");
5455
- fs.writeFileSync(path.join(xdgDir, "streaks.json"), streaksData, { mode: 384 });
5456
- }
5457
- process.stderr.write(`Migrated config from ${LEGACY_DIR} to ${xdgDir}
5458
- `);
5459
- } catch {
5460
- }
5461
- }
5462
-
5463
- // src/cli/auth/credentials.ts
5464
- function loadCredentials() {
5465
- try {
5466
- const data = JSON.parse(fs2.readFileSync(getCredentialsPath(), "utf8"));
5467
- if (typeof data?.refreshToken !== "string" || typeof data?.uid !== "string" || typeof data?.email !== "string") {
5468
- return null;
5469
- }
5470
- return data;
5471
- } catch {
5472
- return null;
5473
- }
5474
- }
5475
- function saveCredentials(creds) {
5476
- ensureConfigDir();
5477
- fs2.writeFileSync(getCredentialsPath(), JSON.stringify(creds, null, 2), { mode: 384 });
5478
- }
5479
- function clearCredentials() {
5480
- try {
5481
- const credPath = getCredentialsPath();
5482
- const stat = fs2.statSync(credPath);
5483
- fs2.writeFileSync(credPath, crypto.randomBytes(stat.size));
5484
- fs2.unlinkSync(credPath);
5485
- } catch {
5486
- }
5487
- }
5488
- var refreshInFlight = null;
5489
- async function getIdToken() {
5490
- const envToken = process.env.NUMO_TOKEN;
5491
- if (envToken) return envToken;
5492
- const creds = loadCredentials();
5493
- if (!creds) throw new Error("Not logged in. Run: numo login");
5494
- if (creds.idToken && creds.idTokenExpiry && Date.now() < creds.idTokenExpiry - 6e4) {
5495
- return creds.idToken;
5496
- }
5497
- if (refreshInFlight) return refreshInFlight;
5498
- refreshInFlight = performRefresh(creds).finally(() => {
5499
- refreshInFlight = null;
5500
- });
5501
- return refreshInFlight;
5502
- }
5503
- async function performRefresh(creds) {
5504
- const { getFirebaseApiKey: getFirebaseApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
5505
- const fbApiKey = getFirebaseApiKey2();
5506
- if (!fbApiKey) throw new Error("NUMO_FIREBASE_API_KEY not set");
5507
- const { http: http3 } = await Promise.resolve().then(() => (init_http(), http_exports));
5508
- const resp = await http3.post(
5509
- `https://securetoken.googleapis.com/v1/token?key=${fbApiKey}`,
5510
- { grant_type: "refresh_token", refresh_token: creds.refreshToken },
5511
- { headers: { "Content-Type": "application/json" } }
5512
- );
5513
- creds.idToken = resp.data.id_token;
5514
- creds.idTokenExpiry = Date.now() + (parseInt(resp.data.expires_in) || 3600) * 1e3;
5515
- saveCredentials(creds);
5516
- return creds.idToken;
5517
- }
5518
-
5519
- // src/cli/auth/login.ts
5520
- init_config();
5437
+ init_credentials();
5521
5438
  init_prompts();
5522
5439
  init_errors();
5523
-
5524
- // src/cli/lib/uid.ts
5525
- init_define_ADMIN_UIDS();
5526
- init_errors();
5527
- var ADMIN_UIDS = typeof define_ADMIN_UIDS_default !== "undefined" ? define_ADMIN_UIDS_default : [];
5528
- function requireUid() {
5529
- const creds = loadCredentials();
5530
- if (!creds) throw Errors.authRequired();
5531
- return creds.uid;
5532
- }
5533
- function isAdmin() {
5534
- const creds = loadCredentials();
5535
- return !!creds && ADMIN_UIDS.includes(creds.uid);
5536
- }
5537
- function requireAdmin() {
5538
- const creds = loadCredentials();
5539
- if (!creds) throw Errors.authRequired();
5540
- if (!ADMIN_UIDS.includes(creds.uid)) {
5541
- throw new CliError("AUTH_FORBIDDEN" /* AUTH_FORBIDDEN */, "Admin access required", ExitCode.NO_PERM, {
5542
- hint: "Your account does not have admin privileges."
5543
- });
5544
- }
5545
- return creds.uid;
5546
- }
5547
-
5548
- // src/cli/auth/login.ts
5440
+ init_api_client();
5549
5441
  async function authenticateWithEmail(spinner) {
5550
- const fbApiKey = getFirebaseApiKey();
5551
- if (!fbApiKey) {
5552
- throw Errors.configMissing("NUMO_FIREBASE_API_KEY");
5553
- }
5554
5442
  const email = await promptText({ message: "Email", required: true });
5555
5443
  const password = await promptPassword({ message: "Password" });
5556
5444
  spinner.start("Signing in...");
5557
5445
  const resp = await http.post(
5558
- `https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=${fbApiKey}`,
5559
- { email, password, returnSecureToken: true },
5560
- { headers: { "Content-Type": "application/json" } }
5446
+ `${API_BASE}/api/auth/login`,
5447
+ { email, password }
5561
5448
  );
5562
5449
  return {
5563
5450
  refreshToken: resp.data.refreshToken,
5564
- uid: resp.data.localId,
5565
- displayName: resp.data.email,
5451
+ uid: resp.data.uid,
5452
+ displayName: resp.data.email ?? email,
5566
5453
  idToken: resp.data.idToken,
5567
- idTokenExpiry: Date.now() + (parseInt(resp.data.expiresIn) || 3600) * 1e3
5454
+ idTokenExpiry: Date.now() + (resp.data.expiresIn || 3600) * 1e3
5568
5455
  };
5569
5456
  }
5570
5457
  function printSuccess(displayName) {
5571
5458
  const lines = [
5572
5459
  ` ${import_picocolors2.default.dim("$")} numo tasks list --date YYYY-MM-DD List tasks for a date`,
5573
- ` ${import_picocolors2.default.dim("$")} numo tasks create --text "..." Create a task`
5460
+ ` ${import_picocolors2.default.dim("$")} numo tasks create --text "..." Create a task`,
5461
+ ` ${import_picocolors2.default.dim("$")} numo profile View your profile`
5574
5462
  ];
5575
- if (isAdmin()) {
5576
- lines.push(` ${import_picocolors2.default.dim("$")} numo posts list Browse community posts`);
5577
- }
5578
- lines.push(` ${import_picocolors2.default.dim("$")} numo profile View your profile`);
5579
5463
  console.log(`
5580
5464
  ${import_picocolors2.default.bold("Available commands:")}
5581
5465
  ${lines.join("\n")}
@@ -5623,176 +5507,13 @@ async function login(options = {}) {
5623
5507
  }
5624
5508
 
5625
5509
  // src/cli/auth/register.ts
5626
- init_define_ADMIN_UIDS();
5627
5510
  init_http();
5628
5511
  var import_picocolors3 = __toESM(require_picocolors(), 1);
5629
- init_config();
5512
+ init_credentials();
5630
5513
  init_prompts();
5631
5514
  init_errors();
5632
-
5633
- // src/cli/lib/firestore.ts
5634
- init_define_ADMIN_UIDS();
5635
- init_http();
5636
- init_config();
5637
- function toValue(v) {
5638
- if (v === null || v === void 0) return { nullValue: null };
5639
- if (typeof v === "string") return { stringValue: v };
5640
- if (typeof v === "boolean") return { booleanValue: v };
5641
- if (typeof v === "number") {
5642
- return Number.isInteger(v) ? { integerValue: String(v) } : { doubleValue: v };
5643
- }
5644
- if (Array.isArray(v)) {
5645
- return { arrayValue: { values: v.map(toValue) } };
5646
- }
5647
- if (typeof v === "object") {
5648
- return { mapValue: { fields: toFirestoreFields(v) } };
5649
- }
5650
- return { stringValue: String(v) };
5651
- }
5652
- function toFirestoreFields(obj) {
5653
- const fields = {};
5654
- for (const [k2, v] of Object.entries(obj)) {
5655
- fields[k2] = toValue(v);
5656
- }
5657
- return fields;
5658
- }
5659
- function fromValue(v) {
5660
- if ("stringValue" in v) return v.stringValue;
5661
- if ("integerValue" in v) return parseInt(v.integerValue, 10);
5662
- if ("doubleValue" in v) return v.doubleValue;
5663
- if ("booleanValue" in v) return v.booleanValue;
5664
- if ("nullValue" in v) return null;
5665
- if ("arrayValue" in v) return (v.arrayValue.values ?? []).map(fromValue);
5666
- if ("mapValue" in v) return fromFirestoreFields(v.mapValue.fields ?? {});
5667
- return null;
5668
- }
5669
- function fromFirestoreFields(fields) {
5670
- const obj = {};
5671
- for (const [k2, v] of Object.entries(fields)) {
5672
- obj[k2] = fromValue(v);
5673
- }
5674
- return obj;
5675
- }
5676
- function fromFirestoreDoc(doc) {
5677
- const id = doc.name.split("/").pop();
5678
- return { id, ...doc.fields ? fromFirestoreFields(doc.fields) : {} };
5679
- }
5680
- async function authHeaders() {
5681
- const idToken = await getIdToken();
5682
- return { Authorization: `Bearer ${idToken}`, "Content-Type": "application/json" };
5683
- }
5684
- async function getDoc(path4) {
5685
- const url2 = `${getFirestoreBaseUrl()}/${path4}`;
5686
- const { data } = await http.get(url2, { headers: await authHeaders() });
5687
- return fromFirestoreDoc(data);
5688
- }
5689
- async function createDoc(collectionPath, data) {
5690
- const url2 = `${getFirestoreBaseUrl()}/${collectionPath}`;
5691
- const resp = await http.post(url2, { fields: toFirestoreFields(data) }, { headers: await authHeaders() });
5692
- return fromFirestoreDoc(resp.data);
5693
- }
5694
- async function setDoc(docPath, data) {
5695
- const url2 = `${getFirestoreBaseUrl()}/${docPath}`;
5696
- const resp = await http.patch(url2, { fields: toFirestoreFields(data) }, { headers: await authHeaders() });
5697
- return fromFirestoreDoc(resp.data);
5698
- }
5699
- async function updateDoc(path4, data, fieldMask) {
5700
- const url2 = `${getFirestoreBaseUrl()}/${path4}`;
5701
- const qs = new URLSearchParams();
5702
- for (const f of fieldMask) qs.append("updateMask.fieldPaths", f);
5703
- const resp = await http.patch(`${url2}?${qs}`, { fields: toFirestoreFields(data) }, { headers: await authHeaders() });
5704
- return fromFirestoreDoc(resp.data);
5705
- }
5706
- async function deleteDoc(path4) {
5707
- const url2 = `${getFirestoreBaseUrl()}/${path4}`;
5708
- await http.delete(url2, { headers: await authHeaders() });
5709
- }
5710
- async function runQuery(parentPath, collectionId, opts) {
5711
- const base = getFirestoreBaseUrl();
5712
- const url2 = parentPath ? `${base}/${parentPath}:runQuery` : `${base}:runQuery`;
5713
- const structuredQuery = {
5714
- from: [{ collectionId }]
5715
- };
5716
- if (opts.where && opts.where.length > 0) {
5717
- if (opts.where.length === 1) {
5718
- const w = opts.where[0];
5719
- structuredQuery.where = {
5720
- fieldFilter: {
5721
- field: { fieldPath: w.field },
5722
- op: w.op,
5723
- value: toValue(w.value)
5724
- }
5725
- };
5726
- } else {
5727
- structuredQuery.where = {
5728
- compositeFilter: {
5729
- op: "AND",
5730
- filters: opts.where.map((w) => ({
5731
- fieldFilter: {
5732
- field: { fieldPath: w.field },
5733
- op: w.op,
5734
- value: toValue(w.value)
5735
- }
5736
- }))
5737
- }
5738
- };
5739
- }
5740
- }
5741
- if (opts.orderBy && opts.orderBy.length > 0) {
5742
- structuredQuery.orderBy = opts.orderBy.map((o) => ({
5743
- field: { fieldPath: o.field },
5744
- direction: o.direction ?? "ASCENDING"
5745
- }));
5746
- }
5747
- if (opts.limit) {
5748
- structuredQuery.limit = opts.limit;
5749
- }
5750
- if (opts.startAfter && opts.startAfter.length > 0) {
5751
- structuredQuery.startAt = {
5752
- values: opts.startAfter.map(toValue),
5753
- before: false
5754
- };
5755
- }
5756
- const headers = await authHeaders();
5757
- const { data } = await http.post(url2, { structuredQuery }, { headers });
5758
- return data.filter((r) => r.document).map((r) => fromFirestoreDoc(r.document));
5759
- }
5760
- async function commit(writes) {
5761
- const baseUrl = getFirestoreBaseUrl();
5762
- const dbUrl = baseUrl.replace("/documents", "");
5763
- const url2 = `${dbUrl}/documents:commit`;
5764
- const toResourceName = (path4) => `${baseUrl}/${path4}`.replace("https://firestore.googleapis.com/v1/", "");
5765
- const ops = writes.map((w) => {
5766
- if (w.type === "delete") {
5767
- return { delete: toResourceName(w.path) };
5768
- }
5769
- const op = {};
5770
- if (w.type === "update" && w.data) {
5771
- op.update = {
5772
- name: toResourceName(w.path),
5773
- fields: toFirestoreFields(w.data)
5774
- };
5775
- if (w.fieldMask) {
5776
- op.updateMask = { fieldPaths: w.fieldMask };
5777
- }
5778
- }
5779
- if (w.type === "transform") {
5780
- op.update = {
5781
- name: toResourceName(w.path),
5782
- fields: {}
5783
- };
5784
- op.updateTransforms = (w.transforms ?? []).map((t2) => ({
5785
- fieldPath: t2.field,
5786
- increment: toValue(t2.increment)
5787
- }));
5788
- }
5789
- return op;
5790
- });
5791
- await http.post(url2, { writes: ops }, { headers: await authHeaders() });
5792
- }
5793
-
5794
- // src/cli/auth/register.ts
5795
5515
  init_tty();
5516
+ init_api_client();
5796
5517
  function validateEmail(email) {
5797
5518
  const trimmed = email.trim();
5798
5519
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
@@ -5806,92 +5527,44 @@ function validatePassword(password) {
5806
5527
  }
5807
5528
  return password;
5808
5529
  }
5809
- function generateUsername(email, uid) {
5810
- const emailPart = email.split("@")[0].slice(0, 7);
5811
- const uidPart = uid.slice(0, 4);
5812
- return `${emailPart}-${uidPart}`;
5813
- }
5814
- function randomAvatar() {
5815
- const index = Math.floor(Math.random() * 6) + 1;
5816
- return `assets/avatar${index}-min.png`;
5817
- }
5818
- function buildUserDoc(email, uid, username, avatar, now2 = Date.now()) {
5819
- return {
5820
- email,
5821
- userId: uid,
5822
- signUpTag: "Numo Sign up",
5823
- createdAt: now2,
5824
- username,
5825
- defaultAvatar: avatar,
5826
- preference: {
5827
- tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
5828
- timezones: [(/* @__PURE__ */ new Date()).getTimezoneOffset()]
5829
- },
5830
- fcmToken: [],
5831
- badges: [],
5832
- reminderPlatform: "imessage",
5833
- onboarding: {},
5834
- postOnboardingV1Status: "not-completed"
5835
- };
5836
- }
5837
5530
  function classifySignUpError(err) {
5838
5531
  if (err instanceof CliError) return err;
5839
5532
  const resp = err?.response?.data?.error;
5533
+ const kind = resp?.kind ?? "";
5840
5534
  const msg = resp?.message ?? "";
5841
- if (msg.includes("EMAIL_EXISTS")) {
5535
+ if (kind === "CONFLICT" || msg.includes("already in use")) {
5842
5536
  return Errors.invalidInput(
5843
5537
  "Email already in use",
5844
5538
  "Already have an account? Run: numo login"
5845
5539
  );
5846
5540
  }
5847
- if (msg.includes("INVALID_EMAIL")) {
5541
+ if (msg.includes("Invalid email")) {
5848
5542
  return Errors.invalidInput("Invalid email address");
5849
5543
  }
5850
- if (msg.includes("WEAK_PASSWORD")) {
5544
+ if (msg.includes("Password too weak") || msg.includes("min 6")) {
5851
5545
  return Errors.invalidInput("Password is too weak (min 6 characters)");
5852
5546
  }
5853
- if (msg.includes("OPERATION_NOT_ALLOWED")) {
5854
- return new CliError(
5855
- "AUTH_FORBIDDEN" /* AUTH_FORBIDDEN */,
5856
- "Email registration is disabled",
5857
- ExitCode.NO_PERM
5858
- );
5859
- }
5860
5547
  return classifyError(err);
5861
5548
  }
5862
5549
  async function signUp(email, password) {
5863
- const fbApiKey = getFirebaseApiKey();
5864
- if (!fbApiKey) throw Errors.configMissing("NUMO_FIREBASE_API_KEY");
5865
5550
  try {
5866
- const resp = await http.post(
5867
- `https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=${fbApiKey}`,
5868
- { email, password, returnSecureToken: true },
5869
- { headers: { "Content-Type": "application/json" } }
5870
- );
5551
+ const resp = await http.post(`${API_BASE}/api/auth/register`, {
5552
+ email,
5553
+ password,
5554
+ tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
5555
+ tzOffset: (/* @__PURE__ */ new Date()).getTimezoneOffset()
5556
+ });
5871
5557
  return {
5872
5558
  refreshToken: resp.data.refreshToken,
5873
- uid: resp.data.localId,
5874
- displayName: resp.data.email,
5559
+ uid: resp.data.uid,
5560
+ displayName: resp.data.email ?? email,
5875
5561
  idToken: resp.data.idToken,
5876
- idTokenExpiry: Date.now() + (parseInt(resp.data.expiresIn) || 3600) * 1e3
5562
+ idTokenExpiry: Date.now() + (resp.data.expiresIn || 3600) * 1e3
5877
5563
  };
5878
5564
  } catch (err) {
5879
5565
  throw classifySignUpError(err);
5880
5566
  }
5881
5567
  }
5882
- async function setupUserProfile(uid, email) {
5883
- const now2 = Date.now();
5884
- const username = generateUsername(email, uid);
5885
- const avatar = randomAvatar();
5886
- await setDoc(`users/${uid}`, buildUserDoc(email, uid, username, avatar, now2));
5887
- await commit([
5888
- {
5889
- type: "transform",
5890
- path: "appLinks/users",
5891
- transforms: [{ field: "count", increment: 1 }]
5892
- }
5893
- ]);
5894
- }
5895
5568
  async function register(options = {}) {
5896
5569
  const p = await Promise.resolve().then(() => (init_dist2(), dist_exports));
5897
5570
  p.intro(import_picocolors3.default.bold("Numo \u2014 Register"));
@@ -5919,8 +5592,6 @@ async function register(options = {}) {
5919
5592
  idToken: result.idToken,
5920
5593
  idTokenExpiry: result.idTokenExpiry
5921
5594
  });
5922
- s.message("Setting up profile...");
5923
- await setupUserProfile(result.uid, email);
5924
5595
  s.stop(`Registered as ${import_picocolors3.default.green(result.displayName)}`);
5925
5596
  p.outro("Welcome to Numo!");
5926
5597
  printSuccess(result.displayName);
@@ -5933,86 +5604,20 @@ async function register(options = {}) {
5933
5604
  }
5934
5605
  }
5935
5606
 
5936
- // src/cli/lib/streaks.ts
5937
- init_define_ADMIN_UIDS();
5938
- var fs3 = __toESM(require("fs"), 1);
5939
- var path2 = __toESM(require("path"), 1);
5940
- function getStreaksPath() {
5941
- return path2.join(getConfigDir(), "streaks.json");
5942
- }
5943
- function loadStreaks() {
5944
- try {
5945
- const raw = fs3.readFileSync(getStreaksPath(), "utf-8");
5946
- return JSON.parse(raw);
5947
- } catch {
5948
- return [];
5949
- }
5950
- }
5951
- function saveStreaks(entries) {
5952
- const dir = path2.dirname(getStreaksPath());
5953
- if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
5954
- fs3.writeFileSync(getStreaksPath(), JSON.stringify(entries, null, 2), { mode: 384 });
5955
- }
5956
- function clearStreaks() {
5957
- try {
5958
- fs3.unlinkSync(getStreaksPath());
5959
- } catch {
5960
- }
5961
- }
5962
- function recordDailyStreak(taskId, date) {
5963
- const entries = loadStreaks();
5964
- const dateStr = date.slice(0, 10);
5965
- const existing = entries.find((e) => e.taskId === taskId);
5966
- if (existing) {
5967
- const lastDate = new Date(existing.lastCheck);
5968
- const thisDate = new Date(dateStr);
5969
- const diffMs = thisDate.getTime() - lastDate.getTime();
5970
- const diffDays2 = Math.floor(diffMs / 864e5);
5971
- if (diffDays2 === 1) {
5972
- existing.checksInRow += 1;
5973
- } else if (diffDays2 > 1) {
5974
- existing.checksInRow = 1;
5975
- }
5976
- existing.lastCheck = dateStr;
5977
- saveStreaks(entries);
5978
- return existing.checksInRow;
5979
- }
5980
- entries.push({ taskId, lastCheck: dateStr, checksInRow: 1 });
5981
- saveStreaks(entries);
5982
- return 1;
5983
- }
5984
- function revertDailyStreak(taskId) {
5985
- const entries = loadStreaks();
5986
- const idx = entries.findIndex((e) => e.taskId === taskId);
5987
- if (idx !== -1) {
5988
- entries[idx].checksInRow = Math.max(1, entries[idx].checksInRow - 1);
5989
- saveStreaks(entries);
5990
- }
5991
- }
5992
- function removeDailyStreak(taskId) {
5993
- const entries = loadStreaks().filter((e) => e.taskId !== taskId);
5994
- saveStreaks(entries);
5995
- }
5996
- function getCompletedTodayCount() {
5997
- const today2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5998
- return loadStreaks().filter((e) => e.lastCheck === today2).length;
5999
- }
5607
+ // src/cli/cli.ts
5608
+ init_credentials();
6000
5609
 
6001
5610
  // src/cli/commands/tasks.ts
6002
- init_define_ADMIN_UIDS();
6003
5611
  var import_picocolors8 = __toESM(require_picocolors(), 1);
6004
5612
 
6005
5613
  // src/cli/lib/actions.ts
6006
- init_define_ADMIN_UIDS();
6007
5614
  init_tty();
6008
5615
 
6009
5616
  // src/cli/lib/output.ts
6010
- init_define_ADMIN_UIDS();
6011
5617
  var import_picocolors5 = __toESM(require_picocolors(), 1);
6012
5618
  init_tty();
6013
5619
 
6014
5620
  // src/cli/lib/table.ts
6015
- init_define_ADMIN_UIDS();
6016
5621
  var import_picocolors4 = __toESM(require_picocolors(), 1);
6017
5622
  init_tty();
6018
5623
  var BOX = isUnicodeSupported ? {
@@ -6155,12 +5760,10 @@ ${import_picocolors5.default.red("Error")}: ${structured.message}`);
6155
5760
  }
6156
5761
 
6157
5762
  // src/cli/lib/spinner.ts
6158
- init_define_ADMIN_UIDS();
6159
5763
  var import_picocolors6 = __toESM(require_picocolors(), 1);
6160
5764
  init_tty();
6161
5765
 
6162
5766
  // src/cli/lib/symbols.ts
6163
- init_define_ADMIN_UIDS();
6164
5767
  init_tty();
6165
5768
  var u = isUnicodeSupported;
6166
5769
  var SYM = {
@@ -6348,679 +5951,83 @@ async function runCreate(opts) {
6348
5951
  outputError(err, useJson(opts.global));
6349
5952
  }
6350
5953
  }
6351
- async function runWrite(opts) {
6352
- try {
6353
- const payload = await withSpinner(
6354
- useSpinner(opts.global),
6355
- opts.spinnerMessage ?? "Updating...",
6356
- opts.fn
6357
- );
6358
- const item = opts.dataKey ? payload[opts.dataKey] : payload;
6359
- if (useJson(opts.global)) {
6360
- printJson(selectFields(payload, opts.global.json));
6361
- } else if (opts.onInteractive) {
6362
- opts.onInteractive(item);
6363
- } else {
6364
- if (opts.successMessage) console.log(opts.successMessage);
6365
- if (item && Object.keys(item).length > 0) {
6366
- outputResult(item, false);
6367
- }
6368
- }
6369
- } catch (err) {
6370
- outputError(err, useJson(opts.global));
6371
- }
6372
- }
6373
- async function runDelete(opts) {
6374
- try {
6375
- await withSpinner(
6376
- useSpinner(opts.global),
6377
- opts.spinnerMessage ?? "Deleting...",
6378
- opts.fn
6379
- );
6380
- if (!opts.global.quiet) {
6381
- console.log(opts.successMessage);
6382
- }
6383
- } catch (err) {
6384
- outputError(err, useJson(opts.global));
6385
- }
6386
- }
6387
-
6388
- // src/cli/services/tasks.ts
6389
- init_define_ADMIN_UIDS();
6390
- var crypto3 = __toESM(require("crypto"), 1);
6391
-
6392
- // src/shared/index.ts
6393
- init_define_ADMIN_UIDS();
6394
-
6395
- // src/shared/types/task.ts
6396
- init_define_ADMIN_UIDS();
6397
-
6398
- // src/shared/types/post.ts
6399
- init_define_ADMIN_UIDS();
6400
-
6401
- // src/shared/types/comment.ts
6402
- init_define_ADMIN_UIDS();
6403
-
6404
- // src/shared/types/reply.ts
6405
- init_define_ADMIN_UIDS();
6406
-
6407
- // src/shared/constants.ts
6408
- init_define_ADMIN_UIDS();
6409
- var POST_TAGS = [
6410
- "general",
6411
- "hack",
6412
- "story",
6413
- "meme",
6414
- "other",
6415
- "question",
6416
- "hack-tip",
6417
- "activity"
6418
- ];
6419
- var MAX_TASKS_PER_REQUEST = 200;
6420
- var KARMA_POINTS = {
6421
- addTask: 2,
6422
- completeTask: 5,
6423
- completeSubtask: [2, 5],
6424
- splitTask: 10,
6425
- createPost: 10,
6426
- addComment: 10
6427
- };
6428
- var DIFFICULTY_BONUS = [0, 5, 15, 45];
6429
- var MAX_KARMA_PER_COMPLETE = 100;
6430
- var MAX_POSTS_PER_REQUEST = 50;
6431
- var MAX_COMMENTS_PER_REQUEST = 100;
6432
- var MAX_REPLIES_PER_REQUEST = 100;
6433
-
6434
- // src/cli/lib/validation.ts
6435
- init_define_ADMIN_UIDS();
6436
- init_errors();
6437
- function validateDocId(id, label = "Document ID") {
6438
- if (!id || typeof id !== "string") {
6439
- throw new Error(`${label} is required`);
6440
- }
6441
- const trimmed = id.trim();
6442
- if (trimmed.length === 0) {
6443
- throw new Error(`${label} cannot be empty`);
6444
- }
6445
- if (trimmed.length > 1500) {
6446
- throw new Error(`${label} is too long (max 1500 characters)`);
6447
- }
6448
- if (trimmed.includes("/")) {
6449
- throw new Error(`${label} cannot contain '/'`);
6450
- }
6451
- if (trimmed === "." || trimmed === "..") {
6452
- throw new Error(`${label} cannot be '.' or '..'`);
6453
- }
6454
- if (trimmed.startsWith("__") && trimmed.endsWith("__")) {
6455
- throw new Error(`${label} cannot be a reserved Firestore ID (double underscore wrapped)`);
6456
- }
6457
- return trimmed;
6458
- }
6459
- function checkOwnership(doc, uid, action, userField = "userId") {
6460
- const docUid = doc[userField] ?? doc.authorId;
6461
- if (docUid && docUid !== uid) {
6462
- throw new CliError("AUTH_FORBIDDEN" /* AUTH_FORBIDDEN */, `You can only ${action} your own content`, ExitCode.NO_PERM);
6463
- }
6464
- }
6465
- async function incrementField(path4, field, delta) {
6466
- await commit([{
6467
- type: "transform",
6468
- path: path4,
6469
- transforms: [{ field, increment: delta }]
6470
- }]);
6471
- }
6472
-
6473
- // src/cli/lib/karma.ts
6474
- init_define_ADMIN_UIDS();
6475
- function karmaPath(uid, id) {
6476
- return `users/${uid}/karma/${id}`;
6477
- }
6478
- async function giveKarma(uid, entity, entityId, karma, text) {
6479
- const id = `${entity}_${entityId}`;
6480
- const record = { entity, entityId, karma, text, createdAt: Date.now(), userId: uid };
6481
- await commit([
6482
- { type: "update", path: karmaPath(uid, id), data: record },
6483
- { type: "transform", path: `users/${uid}`, transforms: [{ field: "karmaCount", increment: karma }] }
6484
- ]);
6485
- }
6486
- async function removeKarma(uid, entity, entityId) {
6487
- const id = `${entity}_${entityId}`;
6488
- try {
6489
- const doc = await getDoc(karmaPath(uid, id));
6490
- const karma = doc.karma ?? 0;
6491
- const writes = [
6492
- { type: "delete", path: karmaPath(uid, id) }
6493
- ];
6494
- if (karma > 0) {
6495
- writes.push({
6496
- type: "transform",
6497
- path: `users/${uid}`,
6498
- transforms: [{ field: "karmaCount", increment: -karma }]
6499
- });
6500
- }
6501
- await commit(writes);
6502
- } catch {
6503
- }
6504
- }
6505
-
6506
- // src/cli/services/tasks.ts
6507
- var WEEKDAY_MAP = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
6508
- function parseDate(s) {
6509
- const [datePart, timePart] = s.split(" ");
6510
- const [y2, m, d] = datePart.split("-").map(Number);
6511
- if (timePart) {
6512
- const [h2, min] = timePart.split(":").map(Number);
6513
- return new Date(y2, m - 1, d, h2, min);
6514
- }
6515
- return new Date(y2, m - 1, d, 0, 0);
6516
- }
6517
- function formatDate(d) {
6518
- const yyyy = d.getFullYear();
6519
- const mm = String(d.getMonth() + 1).padStart(2, "0");
6520
- const dd = String(d.getDate()).padStart(2, "0");
6521
- const hh = String(d.getHours()).padStart(2, "0");
6522
- const min = String(d.getMinutes()).padStart(2, "0");
6523
- return `${yyyy}-${mm}-${dd} ${hh}:${min}`;
6524
- }
6525
- function endOfDay(dateStr) {
6526
- return dateStr.slice(0, 10) + " 23:59";
6527
- }
6528
- function todayFormatted() {
6529
- return formatDate(startOfDay(/* @__PURE__ */ new Date()));
6530
- }
6531
- function startOfDay(d) {
6532
- return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
6533
- }
6534
- function endOfDayDate(d) {
6535
- return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);
6536
- }
6537
- function addDays(d, n) {
6538
- const r = new Date(d);
6539
- r.setDate(r.getDate() + n);
6540
- return r;
6541
- }
6542
- function addMonths(d, n) {
6543
- const r = new Date(d);
6544
- r.setMonth(r.getMonth() + n);
6545
- return r;
6546
- }
6547
- function diffDays(a, b) {
6548
- return Math.floor((a.getTime() - b.getTime()) / 864e5);
6549
- }
6550
- function diffWeeks(a, b) {
6551
- return Math.floor(diffDays(a, b) / 7);
6552
- }
6553
- function diffMonths(a, b) {
6554
- return (a.getFullYear() - b.getFullYear()) * 12 + (a.getMonth() - b.getMonth());
6555
- }
6556
- function getClosestNextWeekDay(baseDate, weekDays) {
6557
- const dayNums = new Set(weekDays.map((d) => WEEKDAY_MAP[d]));
6558
- for (let offset = 1; offset <= 7; offset++) {
6559
- const candidate = addDays(baseDate, offset);
6560
- if (dayNums.has(candidate.getDay())) {
6561
- candidate.setHours(baseDate.getHours(), baseDate.getMinutes(), 0, 0);
6562
- return candidate;
6563
- }
6564
- }
6565
- return addDays(baseDate, 7);
6566
- }
6567
- function isRepeating(task) {
6568
- return task.repeat?.type !== "none";
6569
- }
6570
- function getTaskRemindDate(task) {
6571
- if (!task.dueDate || task.completed) return null;
6572
- const d = parseDate(task.dueDate);
6573
- if (d.getHours() === 0 && d.getMinutes() === 0) return null;
6574
- const utcY = d.getUTCFullYear();
6575
- const utcM = String(d.getUTCMonth() + 1).padStart(2, "0");
6576
- const utcD = String(d.getUTCDate()).padStart(2, "0");
6577
- const utcH = String(d.getUTCHours()).padStart(2, "0");
6578
- const utcMin = String(d.getUTCMinutes()).padStart(2, "0");
6579
- return `${utcY}-${utcM}-${utcD} ${utcH}:${utcMin}`;
6580
- }
6581
- function getNextDueDate(task) {
6582
- if (!task.dueDate) return todayFormatted();
6583
- const { repeat } = task;
6584
- const every = repeat.every ?? 1;
6585
- const now2 = endOfDayDate(/* @__PURE__ */ new Date());
6586
- let nextDate = parseDate(task.dueDate);
6587
- const calculatePeriods = (diffFn) => {
6588
- const diff = diffFn(now2, nextDate);
6589
- const passedPeriods = Math.max(0, Math.floor(diff / every));
6590
- return (passedPeriods + 1) * every;
6591
- };
6592
- if (repeat.type === "daily") {
6593
- nextDate = addDays(nextDate, calculatePeriods(diffDays));
6594
- } else if (repeat.type === "weekly") {
6595
- if (repeat.weekDays && repeat.weekDays.length > 0) {
6596
- const closestNext = getClosestNextWeekDay(parseDate(task.dueDate), repeat.weekDays);
6597
- if (now2 < closestNext) {
6598
- nextDate = closestNext;
6599
- } else {
6600
- const periods = calculatePeriods(diffWeeks);
6601
- const advanced = addDays(nextDate, periods * 7);
6602
- const dayDiff = (closestNext.getDay() - advanced.getDay() + 7) % 7;
6603
- nextDate = addDays(advanced, dayDiff);
6604
- nextDate.setHours(parseDate(task.dueDate).getHours(), parseDate(task.dueDate).getMinutes(), 0, 0);
6605
- }
6606
- } else {
6607
- nextDate = addDays(nextDate, calculatePeriods(diffWeeks) * 7);
6608
- }
6609
- } else if (repeat.type === "monthly") {
6610
- nextDate = addMonths(nextDate, calculatePeriods(diffMonths));
6611
- }
6612
- return formatDate(nextDate);
6613
- }
6614
- function taskPath(uid, id) {
6615
- return `users/${uid}/tasks/${id}`;
6616
- }
6617
- function taskHistoryPath(uid, id) {
6618
- return `users/${uid}/tasksHistory/${id}`;
6619
- }
6620
- function orderingPath(uid) {
6621
- return `users/${uid}/order/tasks`;
6622
- }
6623
- function progressPath(uid, date) {
6624
- return `users/${uid}/progress/${date}`;
6625
- }
6626
- function routineStreakPath(uid, taskId) {
6627
- return `users/${uid}/routineStreaks/${taskId}`;
6628
- }
6629
- function archivePath(uid, taskId) {
6630
- return `archive/${uid}/tasks/${taskId}`;
6631
- }
6632
- function archiveCounterPath(uid) {
6633
- return `archive/${uid}`;
6634
- }
6635
- function activityTotalsPath(uid) {
6636
- return `users/${uid}/activity/totals`;
6637
- }
6638
- function reversedTimestamp(len) {
6639
- const maxTs = 9999999999999;
6640
- const reversed = String(maxTs - Date.now());
6641
- return reversed.slice(0, len);
6642
- }
6643
- function randomAlphanumeric(len) {
6644
- const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
6645
- const limit = 252;
6646
- let result = "";
6647
- while (result.length < len) {
6648
- const bytes = crypto3.randomBytes(len - result.length);
6649
- for (let i = 0; i < bytes.length && result.length < len; i++) {
6650
- if (bytes[i] < limit) result += chars[bytes[i] % chars.length];
6651
- }
6652
- }
6653
- return result;
6654
- }
6655
- function generateTaskId(task) {
6656
- const repeatType = task.repeat?.type === "none" ? "simple" : task.repeat?.type;
6657
- const textSlug = String(task.text ?? "").slice(0, 8).toLowerCase().replace(/[^a-z0-9]/g, "-");
6658
- const uniquePart = reversedTimestamp(8) + randomAlphanumeric(7);
6659
- if (!task.dueDate) return `${repeatType}_${textSlug}_${uniquePart}`;
6660
- const dateSlug = String(task.dueDate).replace(/[^a-z0-9]/g, "-");
6661
- return `${repeatType}_${textSlug}_${dateSlug}_${uniquePart}`;
6662
- }
6663
- function logSideEffectResults(label, results) {
6664
- for (const r of results) {
6665
- if (r.status === "rejected") {
6666
- const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
6667
- process.stderr.write(`[warn] ${label}: ${msg}
6668
- `);
6669
- }
6670
- }
6671
- }
6672
- async function addToOrdering(uid, taskId, position = "end") {
6673
- let ordering = [];
6674
- try {
6675
- const doc = await getDoc(orderingPath(uid));
6676
- if (Array.isArray(doc.tasksListOrdering)) ordering = doc.tasksListOrdering;
6677
- } catch {
6678
- }
6679
- if (!ordering.includes(taskId)) {
6680
- if (position === "start") ordering.unshift(taskId);
6681
- else ordering.push(taskId);
6682
- }
6683
- await updateDoc(orderingPath(uid), { tasksListOrdering: ordering }, ["tasksListOrdering"]);
6684
- }
6685
- async function removeFromOrdering(uid, taskId) {
6686
- try {
6687
- const doc = await getDoc(orderingPath(uid));
6688
- if (Array.isArray(doc.tasksListOrdering)) {
6689
- const ordering = doc.tasksListOrdering.filter((id) => id !== taskId);
6690
- await updateDoc(orderingPath(uid), { tasksListOrdering: ordering }, ["tasksListOrdering"]);
6691
- }
6692
- } catch {
6693
- }
6694
- }
6695
- async function addToProgress(uid, taskId, date) {
6696
- const dateKey = date.slice(0, 10);
6697
- let tasks = [];
6698
- try {
6699
- const doc = await getDoc(progressPath(uid, dateKey));
6700
- if (Array.isArray(doc.tasks)) tasks = doc.tasks;
6701
- } catch {
6702
- }
6703
- if (!tasks.includes(taskId)) tasks.push(taskId);
6704
- await setDoc(progressPath(uid, dateKey), { tasks });
6705
- }
6706
- async function removeFromProgress(uid, taskId, date) {
6707
- const dateKey = date.slice(0, 10);
6708
- try {
6709
- const doc = await getDoc(progressPath(uid, dateKey));
6710
- if (Array.isArray(doc.tasks)) {
6711
- const tasks = doc.tasks.filter((id) => id !== taskId);
6712
- await setDoc(progressPath(uid, dateKey), { tasks });
6713
- }
6714
- } catch {
6715
- }
6716
- }
6717
- async function recordRoutineStreak(uid, taskId) {
6718
- let streak = {};
6719
- try {
6720
- streak = await getDoc(routineStreakPath(uid, taskId));
6721
- } catch {
6722
- }
6723
- const newStreak = (streak.streak ?? 0) + 1;
6724
- const longestStreak = Math.max(streak.longestStreak ?? 0, newStreak);
6725
- await setDoc(routineStreakPath(uid, taskId), {
6726
- taskId,
6727
- userId: uid,
6728
- streak: newStreak,
6729
- longestStreak,
6730
- lastCompletedAt: Date.now()
6731
- });
6732
- }
6733
- async function revertRoutineStreak(uid, taskId) {
5954
+ async function runWrite(opts) {
6734
5955
  try {
6735
- const doc = await getDoc(routineStreakPath(uid, taskId));
6736
- const newStreak = Math.max(0, (doc.streak ?? 0) - 1);
6737
- await setDoc(routineStreakPath(uid, taskId), {
6738
- ...doc,
6739
- streak: newStreak,
6740
- lastCompletedAt: newStreak > 0 ? doc.lastCompletedAt : null
6741
- });
6742
- } catch {
5956
+ const payload = await withSpinner(
5957
+ useSpinner(opts.global),
5958
+ opts.spinnerMessage ?? "Updating...",
5959
+ opts.fn
5960
+ );
5961
+ const item = opts.dataKey ? payload[opts.dataKey] : payload;
5962
+ if (useJson(opts.global)) {
5963
+ printJson(selectFields(payload, opts.global.json));
5964
+ } else if (opts.onInteractive) {
5965
+ opts.onInteractive(payload);
5966
+ } else {
5967
+ if (opts.successMessage) console.log(opts.successMessage);
5968
+ if (item && Object.keys(item).length > 0) {
5969
+ outputResult(item, false);
5970
+ }
5971
+ }
5972
+ } catch (err) {
5973
+ outputError(err, useJson(opts.global));
6743
5974
  }
6744
5975
  }
6745
- async function deleteRoutineStreak(uid, taskId) {
5976
+ async function runDelete(opts) {
6746
5977
  try {
6747
- await deleteDoc(routineStreakPath(uid, taskId));
6748
- } catch {
5978
+ await withSpinner(
5979
+ useSpinner(opts.global),
5980
+ opts.spinnerMessage ?? "Deleting...",
5981
+ opts.fn
5982
+ );
5983
+ if (!opts.global.quiet) {
5984
+ console.log(opts.successMessage);
5985
+ }
5986
+ } catch (err) {
5987
+ outputError(err, useJson(opts.global));
6749
5988
  }
6750
5989
  }
6751
- function computeCompleteKarma(task, checksInRow) {
6752
- const difficultyIdx = task.difficulty ?? 0;
6753
- const bonus = DIFFICULTY_BONUS[difficultyIdx] ?? 0;
6754
- const raw = (KARMA_POINTS.completeTask + bonus) * checksInRow;
6755
- return Math.min(raw, MAX_KARMA_PER_COMPLETE);
5990
+
5991
+ // src/cli/lib/uid.ts
5992
+ init_credentials();
5993
+ init_errors();
5994
+ function requireUid() {
5995
+ const creds = loadCredentials();
5996
+ if (!creds) throw Errors.authRequired();
5997
+ return creds.uid;
6756
5998
  }
5999
+
6000
+ // src/cli/services/tasks.ts
6001
+ init_api_client();
6757
6002
  async function listTasks(uid, opts) {
6758
- const [activeTasks, historyTasks] = await Promise.all([
6759
- runQuery(`users/${uid}`, "tasks", { where: [], limit: MAX_TASKS_PER_REQUEST }),
6760
- opts.backlog ? Promise.resolve([]) : runQuery(`users/${uid}`, "tasksHistory", { where: [], limit: MAX_TASKS_PER_REQUEST })
6761
- ]);
6762
- let pending = activeTasks;
6763
- let completed = historyTasks;
6764
- if (opts.backlog) {
6765
- pending = pending.filter((t2) => t2.backlog === true);
6766
- } else if (opts.date) {
6767
- const today2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6768
- const isToday = opts.date === today2;
6769
- if (isToday) {
6770
- const cutoff = endOfDay(opts.date);
6771
- pending = pending.filter((t2) => typeof t2.dueDate === "string" && t2.dueDate <= cutoff);
6772
- } else {
6773
- pending = pending.filter((t2) => typeof t2.dueDate === "string" && t2.dueDate.slice(0, 10) === opts.date);
6774
- }
6775
- completed = completed.filter((t2) => {
6776
- if (typeof t2.dueDate !== "string") return false;
6777
- return t2.dueDate.slice(0, 10) === opts.date;
6778
- });
6779
- }
6780
- const applyTag = (items) => opts.tag ? items.filter((t2) => Array.isArray(t2.tags) && t2.tags.includes(opts.tag)) : items;
6781
- completed.forEach((t2) => {
6782
- t2.completed = true;
6003
+ return api.get("/api/tasks", {
6004
+ date: opts.date,
6005
+ backlog: opts.backlog ? "true" : void 0,
6006
+ tag: opts.tag
6783
6007
  });
6784
- const filteredPending = applyTag(pending);
6785
- const filteredCompleted = applyTag(completed);
6786
- const allTasks = [...filteredPending, ...filteredCompleted];
6787
- return { tasks: allTasks, count: allTasks.length, pendingCount: filteredPending.length, completedCount: filteredCompleted.length };
6788
6008
  }
6789
6009
  async function getTask(uid, id) {
6790
- validateDocId(id, "Task ID");
6791
- return getDoc(taskPath(uid, id));
6010
+ return api.get(`/api/tasks/${encodeURIComponent(id)}`);
6792
6011
  }
6793
6012
  async function createTask(uid, body) {
6794
- const now2 = Date.now();
6795
- const dueDate = body.dueDate ?? null;
6796
- const taskData = {
6797
- text: body.text,
6798
- userId: uid,
6799
- isPublic: body.isPublic ?? true,
6800
- completed: false,
6801
- completedAt: null,
6802
- createdAt: now2,
6803
- dueDate,
6804
- remindDate: null,
6805
- tags: Array.isArray(body.tags) ? body.tags : [],
6806
- assets: [],
6807
- note: body.note ?? "",
6808
- priority: typeof body.priority === "number" ? body.priority : 0,
6809
- difficulty: body.difficulty ?? null,
6810
- duration: typeof body.duration === "number" ? body.duration : 10,
6811
- backlog: dueDate == null,
6812
- parentTaskId: null,
6813
- completions: 0,
6814
- repeat: body.repeat ?? { type: "none", every: null, end: null, endDate: null, endAfter: null, monthDays: null, weekDays: null },
6815
- subtasks: Array.isArray(body.subtasks) ? body.subtasks.map((s) => ({ id: crypto3.randomUUID(), text: s.text, completed: false })) : [],
6816
- withTime: dueDate != null && !/\b00:00$/.test(dueDate),
6817
- listPosition: null,
6818
- source: "cli"
6819
- };
6820
- taskData.remindDate = getTaskRemindDate(taskData);
6821
- const taskId = generateTaskId(taskData);
6822
- const doc = await setDoc(taskPath(uid, taskId), taskData);
6823
- const createResults = await Promise.allSettled([
6824
- giveKarma(uid, "addTask", taskId, KARMA_POINTS.addTask, String(taskData.text)),
6825
- addToOrdering(uid, taskId, "end"),
6826
- incrementField(activityTotalsPath(uid), "tasks.active", 1)
6827
- ]);
6828
- logSideEffectResults("createTask", createResults);
6829
- return { task: doc, karma: KARMA_POINTS.addTask };
6013
+ return api.post("/api/tasks", body);
6830
6014
  }
6831
6015
  async function updateTask(uid, id, body) {
6832
- validateDocId(id, "Task ID");
6833
- const allowed = ["text", "isPublic", "dueDate", "tags", "note", "priority", "difficulty", "duration", "repeat", "subtasks"];
6834
- const update = {};
6835
- const fieldMask = [];
6836
- for (const key of allowed) {
6837
- if (key in body) {
6838
- update[key] = body[key];
6839
- fieldMask.push(key);
6840
- }
6841
- }
6842
- if ("dueDate" in update) {
6843
- const newDueDate = update.dueDate;
6844
- update.backlog = newDueDate == null;
6845
- if (!fieldMask.includes("backlog")) fieldMask.push("backlog");
6846
- }
6847
- await updateDoc(taskPath(uid, id), update, fieldMask);
6848
- if ("dueDate" in body) {
6849
- await addToOrdering(uid, id, "end");
6850
- }
6851
- const updated = await getDoc(taskPath(uid, id));
6852
- const recalculatedRemindDate = getTaskRemindDate(updated);
6853
- if (updated.remindDate !== recalculatedRemindDate) {
6854
- await updateDoc(taskPath(uid, id), { remindDate: recalculatedRemindDate }, ["remindDate"]);
6855
- const final = await getDoc(taskPath(uid, id));
6856
- return { task: final };
6857
- }
6858
- return { task: updated };
6016
+ return api.patch(`/api/tasks/${encodeURIComponent(id)}`, body);
6859
6017
  }
6860
6018
  async function deleteTask(uid, id) {
6861
- validateDocId(id, "Task ID");
6862
- let taskData = {};
6863
- try {
6864
- taskData = await getDoc(taskPath(uid, id));
6865
- } catch {
6866
- }
6867
- const hasData = Object.keys(taskData).length > 0;
6868
- const writes = [
6869
- { type: "delete", path: taskPath(uid, id) }
6870
- ];
6871
- if (hasData) {
6872
- writes.push({
6873
- type: "update",
6874
- path: archivePath(uid, id),
6875
- data: { ...taskData, archived: true, archivedAt: Date.now() }
6876
- });
6877
- writes.push({
6878
- type: "transform",
6879
- path: archiveCounterPath(uid),
6880
- transforms: [{ field: "archivedTasksCount", increment: 1 }]
6881
- });
6882
- }
6883
- await commit(writes);
6884
- try {
6885
- const historyDocs = await runQuery(`users/${uid}`, "tasksHistory", {
6886
- where: [{ field: "parentTaskId", op: "EQUAL", value: id }],
6887
- limit: MAX_TASKS_PER_REQUEST
6888
- });
6889
- const historyResults = await Promise.allSettled(historyDocs.map((h2) => deleteDoc(taskHistoryPath(uid, h2.id))));
6890
- logSideEffectResults("deleteTask.history", historyResults);
6891
- } catch (err) {
6892
- process.stderr.write(`[warn] deleteTask.history: ${err instanceof Error ? err.message : String(err)}
6893
- `);
6894
- }
6895
- const deleteResults = await Promise.allSettled([
6896
- removeFromOrdering(uid, id),
6897
- deleteRoutineStreak(uid, id),
6898
- incrementField(activityTotalsPath(uid), "tasks.active", -1)
6899
- ]);
6900
- logSideEffectResults("deleteTask", deleteResults);
6901
- removeDailyStreak(id);
6902
- return { deleted: true, taskText: String(taskData.text ?? ""), archived: hasData };
6019
+ return api.del(`/api/tasks/${encodeURIComponent(id)}`);
6903
6020
  }
6904
6021
  async function completeTask(uid, id, date) {
6905
- validateDocId(id, "Task ID");
6906
- const task = await getDoc(taskPath(uid, id));
6907
- const completedAt = date ? new Date(date).getTime() : Date.now();
6908
- const repeating = isRepeating(task);
6909
- const historyId = repeating ? `${id}-${completedAt}` : id;
6910
- const historyData = {
6911
- ...task,
6912
- id: historyId,
6913
- completedAt,
6914
- completed: true,
6915
- parentTaskId: id,
6916
- isHistoryTask: true,
6917
- completions: (task.completions ?? 0) + 1,
6918
- remindDate: null
6919
- };
6920
- delete historyData.id;
6921
- const writes = [];
6922
- if (repeating) {
6923
- const nextDueDate = getNextDueDate(task);
6924
- const resetSubtasks = (task.subtasks ?? []).map((s) => ({ ...s, completed: false }));
6925
- const updatedData = {
6926
- dueDate: nextDueDate,
6927
- completions: (task.completions ?? 0) + 1,
6928
- completedAt,
6929
- subtasks: resetSubtasks
6930
- };
6931
- writes.push({
6932
- type: "update",
6933
- path: taskPath(uid, id),
6934
- data: updatedData,
6935
- fieldMask: Object.keys(updatedData)
6936
- });
6937
- } else {
6938
- writes.push({ type: "delete", path: taskPath(uid, id) });
6939
- }
6940
- writes.push({
6941
- type: "update",
6942
- path: taskHistoryPath(uid, historyId),
6943
- data: historyData
6944
- });
6945
- await commit(writes);
6946
- if (repeating) {
6947
- try {
6948
- const afterUpdate = await getDoc(taskPath(uid, id));
6949
- await updateDoc(taskPath(uid, id), { remindDate: getTaskRemindDate(afterUpdate) }, ["remindDate"]);
6950
- } catch {
6951
- }
6952
- }
6953
- const completeDateStr = date ?? formatDate(/* @__PURE__ */ new Date());
6954
- const entityId = repeating ? `${id}_${completeDateStr.slice(0, 10)}` : id;
6955
- const checksInRow = recordDailyStreak(id, completeDateStr);
6956
- const karmaPoints = computeCompleteKarma(task, checksInRow);
6957
- const sideEffects = [
6958
- giveKarma(uid, "completeTask", entityId, karmaPoints, task.text),
6959
- addToProgress(uid, id, completeDateStr),
6960
- incrementField(activityTotalsPath(uid), "tasks.completed", 1)
6961
- ];
6962
- if (repeating) {
6963
- sideEffects.push(recordRoutineStreak(uid, id));
6964
- } else {
6965
- sideEffects.push(removeFromOrdering(uid, id));
6966
- sideEffects.push(incrementField(activityTotalsPath(uid), "tasks.active", -1));
6967
- }
6968
- const completeResults = await Promise.allSettled(sideEffects);
6969
- logSideEffectResults("completeTask", completeResults);
6970
- return { completed: true, taskHistory: historyData, karma: karmaPoints, checksInRow, taskText: task.text };
6022
+ return api.post(`/api/tasks/${encodeURIComponent(id)}/complete`, date ? { date } : void 0);
6971
6023
  }
6972
6024
  async function uncompleteTask(uid, taskHistoryId) {
6973
- validateDocId(taskHistoryId, "Task history ID");
6974
- const history = await getDoc(taskHistoryPath(uid, taskHistoryId));
6975
- const parentTaskId = history.parentTaskId;
6976
- if (!parentTaskId) throw new Error("Not a history record");
6977
- const repeating = isRepeating(history);
6978
- const restoredTask = {
6979
- ...history,
6980
- completed: false,
6981
- completedAt: null,
6982
- completions: Math.max(0, (history.completions ?? 0) - 1)
6983
- };
6984
- delete restoredTask.isHistoryTask;
6985
- delete restoredTask.id;
6986
- restoredTask.parentTaskId = null;
6987
- if (repeating) {
6988
- restoredTask.dueDate = history.dueDate;
6989
- }
6990
- const writes = [
6991
- { type: "update", path: taskPath(uid, parentTaskId), data: restoredTask },
6992
- { type: "delete", path: taskHistoryPath(uid, taskHistoryId) }
6993
- ];
6994
- await commit(writes);
6995
- try {
6996
- const updated = await getDoc(taskPath(uid, parentTaskId));
6997
- await updateDoc(taskPath(uid, parentTaskId), { remindDate: getTaskRemindDate(updated) }, ["remindDate"]);
6998
- } catch {
6999
- }
7000
- const completedAtDate = history.completedAt ? formatDate(new Date(history.completedAt)) : history.dueDate ?? formatDate(/* @__PURE__ */ new Date());
7001
- const entityId = repeating ? `${parentTaskId}_${completedAtDate.slice(0, 10)}` : parentTaskId;
7002
- const sideEffects = [
7003
- removeKarma(uid, "completeTask", entityId),
7004
- removeFromProgress(uid, parentTaskId, completedAtDate),
7005
- incrementField(activityTotalsPath(uid), "tasks.completed", -1)
7006
- ];
7007
- revertDailyStreak(parentTaskId);
7008
- if (repeating) {
7009
- sideEffects.push(revertRoutineStreak(uid, parentTaskId));
7010
- } else {
7011
- sideEffects.push(addToOrdering(uid, parentTaskId, "end"));
7012
- sideEffects.push(incrementField(activityTotalsPath(uid), "tasks.active", 1));
7013
- }
7014
- const uncompleteResults = await Promise.allSettled(sideEffects);
7015
- logSideEffectResults("uncompleteTask", uncompleteResults);
7016
- const final = await getDoc(taskPath(uid, parentTaskId));
7017
- return { uncompleted: true, task: final, karmaReverted: true };
6025
+ return api.post(`/api/tasks/${encodeURIComponent(taskHistoryId)}/uncomplete`);
7018
6026
  }
7019
6027
 
7020
6028
  // src/cli/lib/format.ts
7021
- init_define_ADMIN_UIDS();
7022
6029
  var import_picocolors7 = __toESM(require_picocolors(), 1);
7023
- function formatDate2(ts) {
6030
+ function formatDate(ts) {
7024
6031
  if (ts == null) return "";
7025
6032
  const d = typeof ts === "number" ? new Date(ts) : new Date(ts);
7026
6033
  if (isNaN(d.getTime())) return String(ts);
@@ -7043,7 +6050,7 @@ function formatRelativeDate(ts) {
7043
6050
  if (hours < 24) return `${hours}h ago`;
7044
6051
  const days = Math.floor(hours / 24);
7045
6052
  if (days < 30) return `${days}d ago`;
7046
- return formatDate2(ts);
6053
+ return formatDate(ts);
7047
6054
  }
7048
6055
  function formatTags(tags) {
7049
6056
  if (!Array.isArray(tags) || tags.length === 0) return "";
@@ -7121,26 +6128,7 @@ init_prompts();
7121
6128
  init_tty();
7122
6129
  init_errors();
7123
6130
 
7124
- // src/cli/lib/parse-date.ts
7125
- init_define_ADMIN_UIDS();
7126
-
7127
- // node_modules/chrono-node/dist/esm/index.js
7128
- init_define_ADMIN_UIDS();
7129
-
7130
- // node_modules/chrono-node/dist/esm/locales/en/index.js
7131
- init_define_ADMIN_UIDS();
7132
-
7133
- // node_modules/chrono-node/dist/esm/chrono.js
7134
- init_define_ADMIN_UIDS();
7135
-
7136
- // node_modules/chrono-node/dist/esm/results.js
7137
- init_define_ADMIN_UIDS();
7138
-
7139
- // node_modules/chrono-node/dist/esm/utils/dates.js
7140
- init_define_ADMIN_UIDS();
7141
-
7142
6131
  // node_modules/chrono-node/dist/esm/types.js
7143
- init_define_ADMIN_UIDS();
7144
6132
  var Meridiem;
7145
6133
  (function(Meridiem2) {
7146
6134
  Meridiem2[Meridiem2["AM"] = 0] = "AM";
@@ -7199,7 +6187,6 @@ function implySimilarTime(component, target) {
7199
6187
  }
7200
6188
 
7201
6189
  // node_modules/chrono-node/dist/esm/timezone.js
7202
- init_define_ADMIN_UIDS();
7203
6190
  var TIMEZONE_ABBR_MAP = {
7204
6191
  ACDT: 630,
7205
6192
  ACST: 570,
@@ -7469,7 +6456,6 @@ function toTimezoneOffset(timezoneInput, date, timezoneOverrides = {}) {
7469
6456
  }
7470
6457
 
7471
6458
  // node_modules/chrono-node/dist/esm/calculation/duration.js
7472
- init_define_ADMIN_UIDS();
7473
6459
  var EmptyDuration = {
7474
6460
  day: 0,
7475
6461
  second: 0,
@@ -7870,17 +6856,7 @@ var ParsingResult = class _ParsingResult {
7870
6856
  }
7871
6857
  };
7872
6858
 
7873
- // node_modules/chrono-node/dist/esm/locales/en/configuration.js
7874
- init_define_ADMIN_UIDS();
7875
-
7876
- // node_modules/chrono-node/dist/esm/locales/en/parsers/ENTimeUnitWithinFormatParser.js
7877
- init_define_ADMIN_UIDS();
7878
-
7879
- // node_modules/chrono-node/dist/esm/locales/en/constants.js
7880
- init_define_ADMIN_UIDS();
7881
-
7882
6859
  // node_modules/chrono-node/dist/esm/utils/pattern.js
7883
- init_define_ADMIN_UIDS();
7884
6860
  function repeatedTimeunitPattern(prefix, singleTimeunitPattern, connectorPattern = "\\s{0,5},?\\s{0,5}") {
7885
6861
  const singleTimeunitPatternNoCapture = singleTimeunitPattern.replace(/\((?!\?)/g, "(?:");
7886
6862
  return `${prefix}${singleTimeunitPatternNoCapture}(?:${connectorPattern}${singleTimeunitPatternNoCapture}){0,10}`;
@@ -7902,7 +6878,6 @@ function matchAnyPattern(dictionary) {
7902
6878
  }
7903
6879
 
7904
6880
  // node_modules/chrono-node/dist/esm/calculation/years.js
7905
- init_define_ADMIN_UIDS();
7906
6881
  function findMostLikelyADYear(yearNumber) {
7907
6882
  if (yearNumber < 100) {
7908
6883
  if (yearNumber > 50) {
@@ -8180,7 +7155,6 @@ function collectDateTimeFragment(fragments, match) {
8180
7155
  }
8181
7156
 
8182
7157
  // node_modules/chrono-node/dist/esm/common/parsers/AbstractParserWithWordBoundary.js
8183
- init_define_ADMIN_UIDS();
8184
7158
  var AbstractParserWithWordBoundaryChecking = class {
8185
7159
  innerPatternHasChange(context, currentInnerPattern) {
8186
7160
  return this.innerPattern(context) !== currentInnerPattern;
@@ -8240,7 +7214,6 @@ var ENTimeUnitWithinFormatParser = class extends AbstractParserWithWordBoundaryC
8240
7214
  };
8241
7215
 
8242
7216
  // node_modules/chrono-node/dist/esm/locales/en/parsers/ENMonthNameLittleEndianParser.js
8243
- init_define_ADMIN_UIDS();
8244
7217
  var PATTERN = new RegExp(`(?:on\\s{0,3})?(${ORDINAL_NUMBER_PATTERN})(?:\\s{0,3}(?:to|\\-|\\\u2013|until|through|till)?\\s{0,3}(${ORDINAL_NUMBER_PATTERN}))?(?:-|/|\\s{0,3}(?:of)?\\s{0,3})(${matchAnyPattern(MONTH_DICTIONARY)})(?:(?:-|/|,?\\s{0,3})(${YEAR_PATTERN}(?!\\w)))?(?=\\W|$)`, "i");
8245
7218
  var DATE_GROUP = 1;
8246
7219
  var DATE_TO_GROUP = 2;
@@ -8277,7 +7250,6 @@ var ENMonthNameLittleEndianParser = class extends AbstractParserWithWordBoundary
8277
7250
  };
8278
7251
 
8279
7252
  // node_modules/chrono-node/dist/esm/locales/en/parsers/ENMonthNameMiddleEndianParser.js
8280
- init_define_ADMIN_UIDS();
8281
7253
  var PATTERN2 = new RegExp(`(${matchAnyPattern(MONTH_DICTIONARY)})(?:-|/|\\s*,?\\s*)(${ORDINAL_NUMBER_PATTERN})(?!\\s*(?:am|pm))\\s*(?:(?:to|\\-)\\s*(${ORDINAL_NUMBER_PATTERN})\\s*)?(?:(?:-|/|\\s*,\\s*|\\s+)(${YEAR_PATTERN}))?(?=\\W|$)(?!\\:\\d)`, "i");
8282
7254
  var MONTH_NAME_GROUP2 = 1;
8283
7255
  var DATE_GROUP2 = 2;
@@ -8327,7 +7299,6 @@ var ENMonthNameMiddleEndianParser = class extends AbstractParserWithWordBoundary
8327
7299
  };
8328
7300
 
8329
7301
  // node_modules/chrono-node/dist/esm/locales/en/parsers/ENMonthNameParser.js
8330
- init_define_ADMIN_UIDS();
8331
7302
  var PATTERN3 = new RegExp(`((?:in)\\s*)?(${matchAnyPattern(MONTH_DICTIONARY)})\\s*(?:(?:,|-|of)?\\s*(${YEAR_PATTERN})?)?(?=[^\\s\\w]|\\s+[^0-9]|\\s+$|$)`, "i");
8332
7303
  var PREFIX_GROUP = 1;
8333
7304
  var MONTH_NAME_GROUP3 = 2;
@@ -8358,7 +7329,6 @@ var ENMonthNameParser = class extends AbstractParserWithWordBoundaryChecking {
8358
7329
  };
8359
7330
 
8360
7331
  // node_modules/chrono-node/dist/esm/locales/en/parsers/ENYearMonthDayParser.js
8361
- init_define_ADMIN_UIDS();
8362
7332
  var PATTERN4 = new RegExp(`([0-9]{4})[-\\.\\/\\s](?:(${matchAnyPattern(MONTH_DICTIONARY)})|([0-9]{1,2}))[-\\.\\/\\s]([0-9]{1,2})(?=\\W|$)`, "i");
8363
7333
  var YEAR_NUMBER_GROUP = 1;
8364
7334
  var MONTH_NAME_GROUP4 = 2;
@@ -8397,7 +7367,6 @@ var ENYearMonthDayParser = class extends AbstractParserWithWordBoundaryChecking
8397
7367
  };
8398
7368
 
8399
7369
  // node_modules/chrono-node/dist/esm/locales/en/parsers/ENSlashMonthFormatParser.js
8400
- init_define_ADMIN_UIDS();
8401
7370
  var PATTERN5 = new RegExp("([0-9]|0[1-9]|1[012])/([0-9]{4})", "i");
8402
7371
  var MONTH_GROUP = 1;
8403
7372
  var YEAR_GROUP4 = 2;
@@ -8412,11 +7381,7 @@ var ENSlashMonthFormatParser = class extends AbstractParserWithWordBoundaryCheck
8412
7381
  }
8413
7382
  };
8414
7383
 
8415
- // node_modules/chrono-node/dist/esm/locales/en/parsers/ENTimeExpressionParser.js
8416
- init_define_ADMIN_UIDS();
8417
-
8418
7384
  // node_modules/chrono-node/dist/esm/common/parsers/AbstractTimeExpressionParser.js
8419
- init_define_ADMIN_UIDS();
8420
7385
  function primaryTimePattern(leftBoundary, primaryPrefix, primarySuffix, flags) {
8421
7386
  return new RegExp(`${leftBoundary}${primaryPrefix}(\\d{1,4})(?:(?:\\.|:|\uFF1A)(\\d{1,2})(?:(?::|\uFF1A)(\\d{2})(?:\\.(\\d{1,6}))?)?)?(?:\\s*(a\\.m\\.|p\\.m\\.|am?|pm?))?${primarySuffix}`, flags);
8422
7387
  }
@@ -8772,7 +7737,6 @@ var ENTimeExpressionParser = class extends AbstractTimeExpressionParser {
8772
7737
  };
8773
7738
 
8774
7739
  // node_modules/chrono-node/dist/esm/locales/en/parsers/ENTimeUnitAgoFormatParser.js
8775
- init_define_ADMIN_UIDS();
8776
7740
  var PATTERN6 = new RegExp(`(${TIME_UNITS_PATTERN})\\s{0,5}(?:ago|before|earlier)(?=\\W|$)`, "i");
8777
7741
  var STRICT_PATTERN = new RegExp(`(${TIME_UNITS_NO_ABBR_PATTERN})\\s{0,5}(?:ago|before|earlier)(?=\\W|$)`, "i");
8778
7742
  var ENTimeUnitAgoFormatParser = class extends AbstractParserWithWordBoundaryChecking {
@@ -8794,7 +7758,6 @@ var ENTimeUnitAgoFormatParser = class extends AbstractParserWithWordBoundaryChec
8794
7758
  };
8795
7759
 
8796
7760
  // node_modules/chrono-node/dist/esm/locales/en/parsers/ENTimeUnitLaterFormatParser.js
8797
- init_define_ADMIN_UIDS();
8798
7761
  var PATTERN7 = new RegExp(`(${TIME_UNITS_PATTERN})\\s{0,5}(?:later|after|from now|henceforth|forward|out)(?=(?:\\W|$))`, "i");
8799
7762
  var STRICT_PATTERN2 = new RegExp(`(${TIME_UNITS_NO_ABBR_PATTERN})\\s{0,5}(later|after|from now)(?=\\W|$)`, "i");
8800
7763
  var GROUP_NUM_TIMEUNITS = 1;
@@ -8816,14 +7779,7 @@ var ENTimeUnitLaterFormatParser = class extends AbstractParserWithWordBoundaryCh
8816
7779
  }
8817
7780
  };
8818
7781
 
8819
- // node_modules/chrono-node/dist/esm/locales/en/refiners/ENMergeDateRangeRefiner.js
8820
- init_define_ADMIN_UIDS();
8821
-
8822
- // node_modules/chrono-node/dist/esm/common/refiners/AbstractMergeDateRangeRefiner.js
8823
- init_define_ADMIN_UIDS();
8824
-
8825
7782
  // node_modules/chrono-node/dist/esm/common/abstractRefiners.js
8826
- init_define_ADMIN_UIDS();
8827
7783
  var Filter = class {
8828
7784
  refine(context, results) {
8829
7785
  return results.filter((r) => this.isValid(context, r));
@@ -8921,14 +7877,7 @@ var ENMergeDateRangeRefiner = class extends AbstractMergeDateRangeRefiner {
8921
7877
  }
8922
7878
  };
8923
7879
 
8924
- // node_modules/chrono-node/dist/esm/locales/en/refiners/ENMergeDateTimeRefiner.js
8925
- init_define_ADMIN_UIDS();
8926
-
8927
- // node_modules/chrono-node/dist/esm/common/refiners/AbstractMergeDateTimeRefiner.js
8928
- init_define_ADMIN_UIDS();
8929
-
8930
7880
  // node_modules/chrono-node/dist/esm/calculation/mergingCalculation.js
8931
- init_define_ADMIN_UIDS();
8932
7881
  function mergeDateTimeResult(dateResult, timeResult) {
8933
7882
  const result = dateResult.clone();
8934
7883
  const beginDate = dateResult.start;
@@ -9013,11 +7962,7 @@ var ENMergeDateTimeRefiner = class extends AbstractMergeDateTimeRefiner {
9013
7962
  }
9014
7963
  };
9015
7964
 
9016
- // node_modules/chrono-node/dist/esm/configurations.js
9017
- init_define_ADMIN_UIDS();
9018
-
9019
7965
  // node_modules/chrono-node/dist/esm/common/refiners/ExtractTimezoneAbbrRefiner.js
9020
- init_define_ADMIN_UIDS();
9021
7966
  var TIMEZONE_NAME_PATTERN = new RegExp("^\\s*,?\\s*\\(?([A-Z]{2,4})\\)?(?=\\W|$)", "i");
9022
7967
  var ExtractTimezoneAbbrRefiner = class {
9023
7968
  timezoneOverrides;
@@ -9069,7 +8014,6 @@ var ExtractTimezoneAbbrRefiner = class {
9069
8014
  };
9070
8015
 
9071
8016
  // node_modules/chrono-node/dist/esm/common/refiners/ExtractTimezoneOffsetRefiner.js
9072
- init_define_ADMIN_UIDS();
9073
8017
  var TIMEZONE_OFFSET_PATTERN = new RegExp("^\\s*(?:\\(?(?:GMT|UTC)\\s?)?([+-])(\\d{1,2})(?::?(\\d{2}))?\\)?", "i");
9074
8018
  var TIMEZONE_OFFSET_SIGN_GROUP = 1;
9075
8019
  var TIMEZONE_OFFSET_HOUR_OFFSET_GROUP = 2;
@@ -9108,7 +8052,6 @@ var ExtractTimezoneOffsetRefiner = class {
9108
8052
  };
9109
8053
 
9110
8054
  // node_modules/chrono-node/dist/esm/common/refiners/OverlapRemovalRefiner.js
9111
- init_define_ADMIN_UIDS();
9112
8055
  var OverlapRemovalRefiner = class {
9113
8056
  refine(context, results) {
9114
8057
  if (results.length < 2) {
@@ -9145,7 +8088,6 @@ var OverlapRemovalRefiner = class {
9145
8088
  };
9146
8089
 
9147
8090
  // node_modules/chrono-node/dist/esm/common/refiners/ForwardDateRefiner.js
9148
- init_define_ADMIN_UIDS();
9149
8091
  var ForwardDateRefiner = class {
9150
8092
  refine(context, results) {
9151
8093
  if (!context.option.forwardDate) {
@@ -9211,7 +8153,6 @@ var ForwardDateRefiner = class {
9211
8153
  };
9212
8154
 
9213
8155
  // node_modules/chrono-node/dist/esm/common/refiners/UnlikelyFormatFilter.js
9214
- init_define_ADMIN_UIDS();
9215
8156
  var UnlikelyFormatFilter = class extends Filter {
9216
8157
  strictMode;
9217
8158
  constructor(strictMode) {
@@ -9254,7 +8195,6 @@ var UnlikelyFormatFilter = class extends Filter {
9254
8195
  };
9255
8196
 
9256
8197
  // node_modules/chrono-node/dist/esm/common/parsers/ISOFormatParser.js
9257
- init_define_ADMIN_UIDS();
9258
8198
  var PATTERN8 = new RegExp("([0-9]{4})\\-([0-9]{1,2})\\-([0-9]{1,2})(?:T([0-9]{1,2}):([0-9]{1,2})(?::([0-9]{1,2})(?:\\.(\\d{1,4}))?)?(Z|([+-]\\d{2}):?(\\d{2})?)?)?(?=\\W|$)", "i");
9259
8199
  var YEAR_NUMBER_GROUP2 = 1;
9260
8200
  var MONTH_NUMBER_GROUP2 = 2;
@@ -9308,7 +8248,6 @@ var ISOFormatParser = class extends AbstractParserWithWordBoundaryChecking {
9308
8248
  };
9309
8249
 
9310
8250
  // node_modules/chrono-node/dist/esm/common/refiners/MergeWeekdayComponentRefiner.js
9311
- init_define_ADMIN_UIDS();
9312
8251
  var MergeWeekdayComponentRefiner = class extends MergingRefiner {
9313
8252
  mergeResults(textBetween, currentResult, nextResult) {
9314
8253
  const newResult = nextResult.clone();
@@ -9339,11 +8278,7 @@ function includeCommonConfiguration(configuration2, strictMode = false) {
9339
8278
  return configuration2;
9340
8279
  }
9341
8280
 
9342
- // node_modules/chrono-node/dist/esm/locales/en/parsers/ENCasualDateParser.js
9343
- init_define_ADMIN_UIDS();
9344
-
9345
8281
  // node_modules/chrono-node/dist/esm/common/casualReferences.js
9346
- init_define_ADMIN_UIDS();
9347
8282
  function now(reference) {
9348
8283
  const targetDate = reference.getDateWithAdjustedTimezone();
9349
8284
  const component = new ParsingComponents(reference, {});
@@ -9489,7 +8424,6 @@ var ENCasualDateParser = class extends AbstractParserWithWordBoundaryChecking {
9489
8424
  };
9490
8425
 
9491
8426
  // node_modules/chrono-node/dist/esm/locales/en/parsers/ENCasualTimeParser.js
9492
- init_define_ADMIN_UIDS();
9493
8427
  var PATTERN10 = /(?:this)?\s{0,3}(morning|afternoon|evening|night|midnight|midday|noon)(?=\W|$)/i;
9494
8428
  var ENCasualTimeParser = class extends AbstractParserWithWordBoundaryChecking {
9495
8429
  innerPattern() {
@@ -9523,11 +8457,7 @@ var ENCasualTimeParser = class extends AbstractParserWithWordBoundaryChecking {
9523
8457
  }
9524
8458
  };
9525
8459
 
9526
- // node_modules/chrono-node/dist/esm/locales/en/parsers/ENWeekdayParser.js
9527
- init_define_ADMIN_UIDS();
9528
-
9529
8460
  // node_modules/chrono-node/dist/esm/calculation/weekdays.js
9530
- init_define_ADMIN_UIDS();
9531
8461
  function createParsingComponentsAtWeekday(reference, weekday, modifier) {
9532
8462
  const refDate = reference.getDateWithAdjustedTimezone();
9533
8463
  const daysToWeekday = getDaysToWeekday(refDate, weekday, modifier);
@@ -9630,7 +8560,6 @@ var ENWeekdayParser = class extends AbstractParserWithWordBoundaryChecking {
9630
8560
  };
9631
8561
 
9632
8562
  // node_modules/chrono-node/dist/esm/locales/en/parsers/ENRelativeDateFormatParser.js
9633
- init_define_ADMIN_UIDS();
9634
8563
  var PATTERN12 = new RegExp(`(this|last|past|next|after\\s*this)\\s*(${matchAnyPattern(TIME_UNIT_DICTIONARY)})(?=\\s*)(?=\\W|$)`, "i");
9635
8564
  var MODIFIER_WORD_GROUP = 1;
9636
8565
  var RELATIVE_WORD_GROUP = 2;
@@ -9676,7 +8605,6 @@ var ENRelativeDateFormatParser = class extends AbstractParserWithWordBoundaryChe
9676
8605
  };
9677
8606
 
9678
8607
  // node_modules/chrono-node/dist/esm/common/parsers/SlashDateFormatParser.js
9679
- init_define_ADMIN_UIDS();
9680
8608
  var PATTERN13 = new RegExp("([^\\d]|^)([0-3]{0,1}[0-9]{1})[\\/\\.\\-]([0-3]{0,1}[0-9]{1})(?:[\\/\\.\\-]([0-9]{4}|[0-9]{2}))?(\\W|$)", "i");
9681
8609
  var OPENING_GROUP = 1;
9682
8610
  var ENDING_GROUP = 5;
@@ -9745,7 +8673,6 @@ var SlashDateFormatParser = class {
9745
8673
  };
9746
8674
 
9747
8675
  // node_modules/chrono-node/dist/esm/locales/en/parsers/ENTimeUnitCasualRelativeFormatParser.js
9748
- init_define_ADMIN_UIDS();
9749
8676
  var PATTERN14 = new RegExp(`(this|last|past|next|after|\\+|-)\\s*(${TIME_UNITS_PATTERN})(?=\\W|$)`, "i");
9750
8677
  var PATTERN_NO_ABBR = new RegExp(`(this|last|past|next|after|\\+|-)\\s*(${TIME_UNITS_NO_ABBR_PATTERN})(?=\\W|$)`, "i");
9751
8678
  var ENTimeUnitCasualRelativeFormatParser = class extends AbstractParserWithWordBoundaryChecking {
@@ -9775,7 +8702,6 @@ var ENTimeUnitCasualRelativeFormatParser = class extends AbstractParserWithWordB
9775
8702
  };
9776
8703
 
9777
8704
  // node_modules/chrono-node/dist/esm/locales/en/refiners/ENMergeRelativeAfterDateRefiner.js
9778
- init_define_ADMIN_UIDS();
9779
8705
  function IsPositiveFollowingReference(result) {
9780
8706
  return result.text.match(/^[+-]/i) != null;
9781
8707
  }
@@ -9800,7 +8726,6 @@ var ENMergeRelativeAfterDateRefiner = class extends MergingRefiner {
9800
8726
  };
9801
8727
 
9802
8728
  // node_modules/chrono-node/dist/esm/locales/en/refiners/ENMergeRelativeFollowByDateRefiner.js
9803
- init_define_ADMIN_UIDS();
9804
8729
  function hasImpliedEarlierReferenceDate(result) {
9805
8730
  return result.text.match(/\s+(before|from)$/i) != null;
9806
8731
  }
@@ -9831,7 +8756,6 @@ var ENMergeRelativeFollowByDateRefiner = class extends MergingRefiner {
9831
8756
  };
9832
8757
 
9833
8758
  // node_modules/chrono-node/dist/esm/locales/en/refiners/ENExtractYearSuffixRefiner.js
9834
- init_define_ADMIN_UIDS();
9835
8759
  var YEAR_SUFFIX_PATTERN = new RegExp(`^\\s*(${YEAR_PATTERN})`, "i");
9836
8760
  var YEAR_GROUP6 = 1;
9837
8761
  var ENExtractYearSuffixRefiner = class {
@@ -9863,7 +8787,6 @@ var ENExtractYearSuffixRefiner = class {
9863
8787
  };
9864
8788
 
9865
8789
  // node_modules/chrono-node/dist/esm/locales/en/refiners/ENUnlikelyFormatFilter.js
9866
- init_define_ADMIN_UIDS();
9867
8790
  var ENUnlikelyFormatFilter = class extends Filter {
9868
8791
  constructor() {
9869
8792
  super();
@@ -10045,7 +8968,7 @@ var GB = new Chrono(configuration.createCasualConfiguration(true));
10045
8968
 
10046
8969
  // node_modules/chrono-node/dist/esm/index.js
10047
8970
  var casual2 = casual;
10048
- function parseDate2(text, ref, option) {
8971
+ function parseDate(text, ref, option) {
10049
8972
  return casual2.parseDate(text, ref, option);
10050
8973
  }
10051
8974
 
@@ -10054,7 +8977,7 @@ function parseHumanDate(input) {
10054
8977
  const trimmed = input.trim();
10055
8978
  if (!trimmed) return null;
10056
8979
  if (/^\d{4}-\d{2}-\d{2}/.test(trimmed)) return trimmed;
10057
- const parsed = parseDate2(trimmed, /* @__PURE__ */ new Date(), { forwardDate: true });
8980
+ const parsed = parseDate(trimmed, /* @__PURE__ */ new Date(), { forwardDate: true });
10058
8981
  if (!parsed) return null;
10059
8982
  const date = parsed.toISOString().slice(0, 10);
10060
8983
  const hours = parsed.getHours();
@@ -10072,10 +8995,9 @@ function parseHumanDateOnly(input) {
10072
8995
  }
10073
8996
 
10074
8997
  // src/cli/lib/stdin.ts
10075
- init_define_ADMIN_UIDS();
10076
- var fs4 = __toESM(require("fs"), 1);
8998
+ var fs3 = __toESM(require("fs"), 1);
10077
8999
  function readStdinLines() {
10078
- const input = fs4.readFileSync(0, "utf8");
9000
+ const input = fs3.readFileSync(0, "utf8");
10079
9001
  return input.split("\n").map((l) => l.trim()).filter(Boolean);
10080
9002
  }
10081
9003
 
@@ -10095,7 +9017,7 @@ async function pickTask(uid, id, actionName) {
10095
9017
  message: `Select task to ${actionName}`,
10096
9018
  options: pending.map((t2) => ({
10097
9019
  value: t2.id,
10098
- label: `${truncate(String(t2.text), 50)} ${import_picocolors8.default.dim(String(t2.id))}`
9020
+ label: `${truncate(t2.text, 50)} ${import_picocolors8.default.dim(t2.id)}`
10099
9021
  }))
10100
9022
  });
10101
9023
  return selected;
@@ -10120,13 +9042,12 @@ function extractTime(dueDate) {
10120
9042
  const time = parts[1];
10121
9043
  return time === "00:00" ? "" : time;
10122
9044
  }
10123
- function isRepeating2(t2) {
10124
- const repeat = t2.repeat;
10125
- return !!repeat?.type && repeat.type !== "none";
9045
+ function isRepeating(t2) {
9046
+ return !!t2.repeat?.type && t2.repeat.type !== "none";
10126
9047
  }
10127
9048
  function getCheckIndicator(t2) {
10128
9049
  if (t2.completed) return import_picocolors8.default.green(SYM.check);
10129
- if (isRepeating2(t2)) return import_picocolors8.default.blue(SYM.repeat);
9050
+ if (isRepeating(t2)) return import_picocolors8.default.blue(SYM.repeat);
10130
9051
  return import_picocolors8.default.dim(SYM.circle);
10131
9052
  }
10132
9053
  function sortTasksForDisplay(tasks) {
@@ -10134,16 +9055,16 @@ function sortTasksForDisplay(tasks) {
10134
9055
  return [...tasks].sort((a, b) => {
10135
9056
  const timeA = extractTime(a.dueDate);
10136
9057
  const timeB = extractTime(b.dueDate);
10137
- const repA = isRepeating2(a);
10138
- const repB = isRepeating2(b);
9058
+ const repA = isRepeating(a);
9059
+ const repB = isRepeating(b);
10139
9060
  if (timeA && !timeB) return -1;
10140
9061
  if (!timeA && timeB) return 1;
10141
9062
  if (timeA && timeB) return timeA.localeCompare(timeB);
10142
9063
  if (repA && !repB) return -1;
10143
9064
  if (!repA && repB) return 1;
10144
9065
  if (repA && repB) {
10145
- const ra = a.repeat?.type ?? "";
10146
- const rb = b.repeat?.type ?? "";
9066
+ const ra = a.repeat.type ?? "";
9067
+ const rb = b.repeat.type ?? "";
10147
9068
  return (repeatOrder[ra] ?? 99) - (repeatOrder[rb] ?? 99);
10148
9069
  }
10149
9070
  return 0;
@@ -10155,7 +9076,7 @@ function printTaskDetail(t2) {
10155
9076
  printRecord([
10156
9077
  ["ID", dim(t2.id)],
10157
9078
  ["Text", t2.text],
10158
- ["Due", formatDate2(t2.dueDate) || dim("none (backlog)")],
9079
+ ["Due", formatDate(t2.dueDate) || dim("none (backlog)")],
10159
9080
  ["Status", t2.completed ? import_picocolors8.default.green("completed") : import_picocolors8.default.yellow("pending")],
10160
9081
  ["Tags", formatTags(t2.tags) || dim("none")],
10161
9082
  ["Difficulty", formatDifficulty(t2.difficulty) || dim("not set")],
@@ -10164,18 +9085,18 @@ function printTaskDetail(t2) {
10164
9085
  ["Note", t2.note || dim("none")],
10165
9086
  ["Public", t2.isPublic ? import_picocolors8.default.green("yes") : import_picocolors8.default.yellow("no")],
10166
9087
  ["Completions", String(t2.completions ?? 0)],
10167
- ["Created", formatDate2(t2.createdAt)]
9088
+ ["Created", formatDate(t2.createdAt)]
10168
9089
  ]);
10169
9090
  console.log("");
10170
9091
  }
10171
9092
  function printTaskLine(t2) {
10172
9093
  const check = getCheckIndicator(t2);
10173
- const rawText = truncate(String(t2.text ?? ""), 50);
9094
+ const rawText = truncate(t2.text, 50);
10174
9095
  const text = t2.completed ? import_picocolors8.default.strikethrough(import_picocolors8.default.dim(rawText)) : rawText;
10175
9096
  const time = extractTime(t2.dueDate);
10176
9097
  const tags = formatTags(t2.tags);
10177
9098
  const difficulty = formatDifficulty(t2.difficulty);
10178
- const id = import_picocolors8.default.dim(String(t2.id ?? ""));
9099
+ const id = import_picocolors8.default.dim(t2.id);
10179
9100
  const parts = [check, text];
10180
9101
  if (time) parts.push(import_picocolors8.default.cyan(time));
10181
9102
  if (tags) parts.push(tags);
@@ -10204,8 +9125,7 @@ function registerTasksCommands(program3) {
10204
9125
  console.log(` ${import_picocolors8.default.bold("Backlog")} ${import_picocolors8.default.dim(`(${items.length})`)}`);
10205
9126
  } else {
10206
9127
  const viewDate = date ? /* @__PURE__ */ new Date(date + "T00:00:00") : /* @__PURE__ */ new Date();
10207
- const streakCount = getCompletedTodayCount();
10208
- console.log(formatWeekdayHeader(viewDate, streakCount));
9128
+ console.log(formatWeekdayHeader(viewDate, completed.length));
10209
9129
  }
10210
9130
  const tagLine = formatTagsSummary(items);
10211
9131
  if (tagLine) console.log(`
@@ -10412,11 +9332,10 @@ Examples:
10412
9332
  dataKey: "task",
10413
9333
  spinnerMessage: "Creating task...",
10414
9334
  onInteractive: (_task, payload) => {
10415
- const task = payload.task;
9335
+ const { task, karma } = payload;
10416
9336
  const check = import_picocolors8.default.green(SYM.check);
10417
9337
  console.log(`
10418
9338
  ${check} Created ${task.text}`);
10419
- const karma = payload.karma;
10420
9339
  if (karma) {
10421
9340
  console.log(` ${formatKarmaGain(karma)}${" ".repeat(20)}${import_picocolors8.default.dim(task.id)}`);
10422
9341
  }
@@ -10471,9 +9390,9 @@ Examples:
10471
9390
  fn: () => updateTask(uid, taskId, body),
10472
9391
  dataKey: "task",
10473
9392
  spinnerMessage: "Updating task...",
10474
- onInteractive: (task) => {
9393
+ onInteractive: (payload) => {
10475
9394
  console.log(`
10476
- ${import_picocolors8.default.green("Updated!")} ${task.text} ${import_picocolors8.default.dim(task.id)}
9395
+ ${import_picocolors8.default.green("Updated!")} ${payload.task.text} ${import_picocolors8.default.dim(payload.task.id)}
10477
9396
  `);
10478
9397
  }
10479
9398
  });
@@ -10503,7 +9422,7 @@ Examples:
10503
9422
  let taskText = taskId;
10504
9423
  try {
10505
9424
  const task = await getTask(uid, taskId);
10506
- taskText = String(task.text ?? taskId);
9425
+ taskText = task.text ?? taskId;
10507
9426
  } catch {
10508
9427
  }
10509
9428
  const confirmed = await promptConfirm({
@@ -10521,9 +9440,8 @@ Examples:
10521
9440
  spinnerMessage: "Deleting task...",
10522
9441
  onInteractive: (data) => {
10523
9442
  const cross = import_picocolors8.default.red(SYM.cross);
10524
- const text = data.taskText || taskId;
10525
9443
  console.log(`
10526
- ${cross} Deleted ${text}`);
9444
+ ${cross} Deleted ${data.taskText || taskId}`);
10527
9445
  if (data.archived) console.log(` ${import_picocolors8.default.dim("Archived")}`);
10528
9446
  console.log("");
10529
9447
  }
@@ -10555,13 +9473,10 @@ Examples:
10555
9473
  spinnerMessage: "Completing task...",
10556
9474
  onInteractive: (data) => {
10557
9475
  const check = import_picocolors8.default.green(SYM.check);
10558
- const text = data.taskText ?? taskId;
10559
9476
  console.log(`
10560
- ${check} Done! ${text}`);
10561
- const karma = data.karma;
10562
- const checksInRow = data.checksInRow;
10563
- if (karma) {
10564
- console.log(` ${formatKarmaGain(karma, checksInRow)}`);
9477
+ ${check} Done! ${data.taskText ?? taskId}`);
9478
+ if (data.karma) {
9479
+ console.log(` ${formatKarmaGain(data.karma, data.checksInRow)}`);
10565
9480
  }
10566
9481
  console.log("");
10567
9482
  }
@@ -10593,9 +9508,8 @@ Examples:
10593
9508
  spinnerMessage: "Uncompleting task...",
10594
9509
  onInteractive: (data) => {
10595
9510
  const arrow = SYM.undo;
10596
- const text = data.text ?? taskId;
10597
9511
  console.log(`
10598
- ${import_picocolors8.default.yellow(arrow)} Reverted ${text}`);
9512
+ ${import_picocolors8.default.yellow(arrow)} Reverted ${data.task.text ?? taskId}`);
10599
9513
  console.log(` ${import_picocolors8.default.dim("Karma adjustment applied")}`);
10600
9514
  console.log("");
10601
9515
  }
@@ -10606,13 +9520,10 @@ Examples:
10606
9520
  }
10607
9521
 
10608
9522
  // src/cli/commands/posts.ts
10609
- init_define_ADMIN_UIDS();
10610
9523
  var import_picocolors10 = __toESM(require_picocolors(), 1);
10611
9524
 
10612
9525
  // src/cli/lib/pagination.ts
10613
- init_define_ADMIN_UIDS();
10614
9526
  var import_picocolors9 = __toESM(require_picocolors(), 1);
10615
- init_errors();
10616
9527
  function printPaginationHint(opts) {
10617
9528
  if (!opts.nextCursor) return;
10618
9529
  const parts = [opts.command, `--cursor ${opts.nextCursor}`];
@@ -10622,223 +9533,59 @@ ${import_picocolors9.default.dim("Next page:")} ${import_picocolors9.default.dim
10622
9533
  }
10623
9534
 
10624
9535
  // src/cli/services/posts.ts
10625
- init_define_ADMIN_UIDS();
10626
- function generateSlug(title) {
10627
- return title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").slice(0, 80) + "-" + Date.now().toString(36);
10628
- }
9536
+ init_api_client();
10629
9537
  async function listPosts(opts) {
10630
- const lim = Math.min(opts.limit ?? 20, MAX_POSTS_PER_REQUEST);
10631
- const queryOpts = {
10632
- orderBy: [{ field: "createdAt", direction: "DESCENDING" }],
10633
- limit: lim + 1
10634
- };
10635
- if (opts.cursor) {
10636
- const cursorDoc = await getDoc(`posts/${opts.cursor}`);
10637
- if (cursorDoc.createdAt != null) {
10638
- queryOpts.startAfter = [cursorDoc.createdAt];
10639
- }
10640
- }
10641
- const docs = await runQuery("", "posts", queryOpts);
10642
- const hasMore = docs.length > lim;
10643
- const posts = docs.slice(0, lim);
10644
- return {
10645
- posts,
10646
- nextCursor: hasMore ? posts[posts.length - 1].id : void 0
10647
- };
9538
+ return api.get("/api/posts", {
9539
+ cursor: opts.cursor,
9540
+ limit: opts.limit?.toString()
9541
+ });
10648
9542
  }
10649
9543
  async function getPost(id) {
10650
- validateDocId(id, "Post ID");
10651
- const post = await getDoc(`posts/${id}`);
10652
- if (post.authorId) {
10653
- try {
10654
- const author = await getDoc(`users/${post.authorId}`);
10655
- post.authorName = author.username ?? null;
10656
- } catch {
10657
- }
10658
- }
10659
- return post;
9544
+ return api.get(`/api/posts/${encodeURIComponent(id)}`);
10660
9545
  }
10661
9546
  async function createPost(uid, body) {
10662
- const now2 = Date.now();
10663
- const postData = {
10664
- title: body.title,
10665
- body: body.body,
10666
- tag: body.tag,
10667
- authorId: uid,
10668
- slug: generateSlug(body.title),
10669
- isPublic: body.isPublic !== false,
10670
- createdAt: now2,
10671
- updatedAt: now2,
10672
- commentsCount: 0,
10673
- likesCount: 0
10674
- };
10675
- const doc = await createDoc("posts", postData);
10676
- const postId = doc.id;
10677
- try {
10678
- await giveKarma(uid, "createPost", postId, KARMA_POINTS.createPost, String(body.title));
10679
- } catch {
10680
- }
10681
- try {
10682
- await incrementField(`users/${uid}/activity/totals`, "posts.written", 1);
10683
- } catch {
10684
- }
10685
- return { post: doc, karma: KARMA_POINTS.createPost };
9547
+ return api.post("/api/posts", body);
10686
9548
  }
10687
9549
  async function updatePost(uid, id, body) {
10688
- validateDocId(id, "Post ID");
10689
- const post = await getDoc(`posts/${id}`);
10690
- checkOwnership(post, uid, "update");
10691
- const allowed = ["title", "body", "tag", "isPublic"];
10692
- const update = { updatedAt: Date.now() };
10693
- const fieldMask = ["updatedAt"];
10694
- for (const key of allowed) {
10695
- if (key in body) {
10696
- update[key] = body[key];
10697
- fieldMask.push(key);
10698
- }
10699
- }
10700
- await updateDoc(`posts/${id}`, update, fieldMask);
10701
- const updated = await getDoc(`posts/${id}`);
10702
- return { post: updated };
9550
+ return api.patch(`/api/posts/${encodeURIComponent(id)}`, body);
10703
9551
  }
10704
9552
  async function deletePost(uid, id) {
10705
- validateDocId(id, "Post ID");
10706
- const post = await getDoc(`posts/${id}`);
10707
- checkOwnership(post, uid, "delete");
10708
- await deleteDoc(`posts/${id}`);
9553
+ await api.del(`/api/posts/${encodeURIComponent(id)}`);
10709
9554
  }
10710
9555
 
10711
9556
  // src/cli/services/comments.ts
10712
- init_define_ADMIN_UIDS();
9557
+ init_api_client();
10713
9558
  async function listComments(postId, opts) {
10714
- validateDocId(postId, "Post ID");
10715
- const lim = Math.min(opts.limit ?? 20, MAX_COMMENTS_PER_REQUEST);
10716
- const queryOpts = {
10717
- orderBy: [{ field: "createdAt", direction: "ASCENDING" }],
10718
- limit: lim + 1
10719
- };
10720
- if (opts.cursor) {
10721
- const cursorDoc = await getDoc(`posts/${postId}/comments/${opts.cursor}`);
10722
- if (cursorDoc.createdAt != null) {
10723
- queryOpts.startAfter = [cursorDoc.createdAt];
10724
- }
10725
- }
10726
- const docs = await runQuery(`posts/${postId}`, "comments", queryOpts);
10727
- const hasMore = docs.length > lim;
10728
- const comments = docs.slice(0, lim);
10729
- const uids = [...new Set(comments.map((c) => c.userId).filter(Boolean))];
10730
- const userMap = {};
10731
- await Promise.all(uids.map(async (uid) => {
10732
- try {
10733
- const user = await getDoc(`users/${uid}`);
10734
- if (user.username) userMap[uid] = user.username;
10735
- } catch {
10736
- }
10737
- }));
10738
- for (const c of comments) {
10739
- c.authorName = userMap[c.userId] ?? null;
10740
- }
10741
- return {
10742
- comments,
10743
- nextCursor: hasMore ? comments[comments.length - 1].id : void 0
10744
- };
9559
+ return api.get(`/api/posts/${encodeURIComponent(postId)}/comments`, {
9560
+ cursor: opts.cursor,
9561
+ limit: opts.limit?.toString()
9562
+ });
10745
9563
  }
10746
9564
  async function createComment(uid, postId, text) {
10747
- validateDocId(postId, "Post ID");
10748
- const now2 = Date.now();
10749
- const commentData = {
10750
- postId,
10751
- userId: uid,
10752
- text,
10753
- createdAt: now2,
10754
- updatedAt: now2,
10755
- textLength: text.length,
10756
- likes: [],
10757
- repliesCount: 0
10758
- };
10759
- const doc = await createDoc(`posts/${postId}/comments`, commentData);
10760
- const commentId = doc.id;
10761
- await incrementField(`posts/${postId}`, "commentsCount", 1);
10762
- try {
10763
- await giveKarma(uid, "addComment", `${postId}_${commentId}`, KARMA_POINTS.addComment, text);
10764
- } catch {
10765
- }
10766
- try {
10767
- await incrementField(`users/${uid}/activity/totals`, "comments.written", 1);
10768
- } catch {
10769
- }
10770
- return { comment: doc, karma: KARMA_POINTS.addComment };
9565
+ return api.post(`/api/posts/${encodeURIComponent(postId)}/comments`, { text });
10771
9566
  }
10772
9567
  async function deleteComment(uid, postId, commentId) {
10773
- validateDocId(postId, "Post ID");
10774
- validateDocId(commentId, "Comment ID");
10775
- const comment = await getDoc(`posts/${postId}/comments/${commentId}`);
10776
- checkOwnership(comment, uid, "delete");
10777
- await deleteDoc(`posts/${postId}/comments/${commentId}`);
10778
- await incrementField(`posts/${postId}`, "commentsCount", -1);
9568
+ await api.del(`/api/posts/${encodeURIComponent(postId)}/comments/${encodeURIComponent(commentId)}`);
10779
9569
  }
10780
9570
 
10781
9571
  // src/cli/services/replies.ts
10782
- init_define_ADMIN_UIDS();
9572
+ init_api_client();
10783
9573
  async function listReplies(postId, commentId, opts) {
10784
- validateDocId(postId, "Post ID");
10785
- validateDocId(commentId, "Comment ID");
10786
- const lim = Math.min(opts.limit ?? 20, MAX_REPLIES_PER_REQUEST);
10787
- const queryOpts = {
10788
- orderBy: [{ field: "createdAt", direction: "ASCENDING" }],
10789
- limit: lim + 1
10790
- };
10791
- if (opts.cursor) {
10792
- const cursorDoc = await getDoc(`posts/${postId}/comments/${commentId}/replies/${opts.cursor}`);
10793
- if (cursorDoc.createdAt != null) {
10794
- queryOpts.startAfter = [cursorDoc.createdAt];
10795
- }
10796
- }
10797
- const docs = await runQuery(`posts/${postId}/comments/${commentId}`, "replies", queryOpts);
10798
- const hasMore = docs.length > lim;
10799
- const replies = docs.slice(0, lim);
10800
- return {
10801
- replies,
10802
- nextCursor: hasMore ? replies[replies.length - 1].id : void 0
10803
- };
9574
+ return api.get(`/api/posts/${encodeURIComponent(postId)}/comments/${encodeURIComponent(commentId)}/replies`, {
9575
+ cursor: opts.cursor,
9576
+ limit: opts.limit?.toString()
9577
+ });
10804
9578
  }
10805
9579
  async function createReply(uid, postId, commentId, text) {
10806
- validateDocId(postId, "Post ID");
10807
- validateDocId(commentId, "Comment ID");
10808
- const now2 = Date.now();
10809
- const replyData = {
10810
- postId,
10811
- userId: uid,
10812
- text,
10813
- createdAt: now2,
10814
- updatedAt: now2,
10815
- textLength: text.length,
10816
- likes: [],
10817
- parentCommentId: commentId
10818
- };
10819
- const doc = await createDoc(`posts/${postId}/comments/${commentId}/replies`, replyData);
10820
- const replyId = doc.id;
10821
- await incrementField(`posts/${postId}/comments/${commentId}`, "repliesCount", 1);
10822
- try {
10823
- await giveKarma(uid, "addComment", `${postId}_${replyId}`, KARMA_POINTS.addComment, text);
10824
- } catch {
10825
- }
10826
- try {
10827
- await incrementField(`users/${uid}/activity/totals`, "replies.written", 1);
10828
- } catch {
10829
- }
10830
- return { reply: doc, karma: KARMA_POINTS.addComment };
9580
+ return api.post(`/api/posts/${encodeURIComponent(postId)}/comments/${encodeURIComponent(commentId)}/replies`, { text });
10831
9581
  }
10832
9582
  async function deleteReply(uid, postId, commentId, replyId) {
10833
- validateDocId(postId, "Post ID");
10834
- validateDocId(commentId, "Comment ID");
10835
- validateDocId(replyId, "Reply ID");
10836
- const reply = await getDoc(`posts/${postId}/comments/${commentId}/replies/${replyId}`);
10837
- checkOwnership(reply, uid, "delete");
10838
- await deleteDoc(`posts/${postId}/comments/${commentId}/replies/${replyId}`);
10839
- await incrementField(`posts/${postId}/comments/${commentId}`, "repliesCount", -1);
9583
+ await api.del(`/api/posts/${encodeURIComponent(postId)}/comments/${encodeURIComponent(commentId)}/replies/${encodeURIComponent(replyId)}`);
10840
9584
  }
10841
9585
 
9586
+ // src/cli/types/api.ts
9587
+ var POST_TAGS = ["general", "hack", "story", "meme", "other", "question", "hack-tip", "activity"];
9588
+
10842
9589
  // src/cli/commands/posts.ts
10843
9590
  init_prompts();
10844
9591
  init_tty();
@@ -10851,7 +9598,7 @@ async function pickComment(postId, commentId) {
10851
9598
  message: "Select comment",
10852
9599
  options: comments.map((c) => ({
10853
9600
  value: c.id,
10854
- label: `${truncate(String(c.text ?? ""), 60)} ${import_picocolors10.default.dim(String(c.authorName ?? c.userId ?? ""))}`
9601
+ label: `${truncate(c.text ?? "", 60)} ${import_picocolors10.default.dim(c.authorName ?? c.userId ?? "")}`
10855
9602
  }))
10856
9603
  });
10857
9604
  }
@@ -10864,16 +9611,16 @@ async function pickReply(postId, commentId, replyId) {
10864
9611
  message: "Select reply",
10865
9612
  options: replies.map((r) => ({
10866
9613
  value: r.id,
10867
- label: `${truncate(String(r.text ?? ""), 60)} ${import_picocolors10.default.dim(String(r.userId ?? ""))}`
9614
+ label: `${truncate(r.text ?? "", 60)} ${import_picocolors10.default.dim(r.userId ?? "")}`
10868
9615
  }))
10869
9616
  });
10870
9617
  }
10871
9618
  function printPostLine(p) {
10872
- const tag = import_picocolors10.default.cyan(String(p.tag ?? ""));
10873
- const title = truncate(String(p.title ?? ""), 55);
9619
+ const tag = import_picocolors10.default.cyan(p.tag ?? "");
9620
+ const title = truncate(p.title, 55);
10874
9621
  const comments = p.commentsCount ? import_picocolors10.default.dim(`${p.commentsCount} comments`) : "";
10875
9622
  const time = formatRelativeDate(p.createdAt);
10876
- const id = import_picocolors10.default.dim(String(p.id ?? ""));
9623
+ const id = import_picocolors10.default.dim(p.id);
10877
9624
  const parts = [tag, import_picocolors10.default.bold(title)];
10878
9625
  if (comments) parts.push(comments);
10879
9626
  parts.push(import_picocolors10.default.dim(time));
@@ -10890,7 +9637,7 @@ function printPostDetail(p) {
10890
9637
  console.log(` ${import_picocolors10.default.dim(SYM.dash.repeat(40))}`);
10891
9638
  printRecord([
10892
9639
  ["ID", import_picocolors10.default.dim(p.id)],
10893
- ["Tag", import_picocolors10.default.cyan(String(p.tag ?? ""))],
9640
+ ["Tag", import_picocolors10.default.cyan(p.tag ?? "")],
10894
9641
  ["Author", p.authorName ?? p.authorId],
10895
9642
  ["Comments", p.commentsCount != null ? String(p.commentsCount) : null],
10896
9643
  ["Likes", p.likesCount != null ? String(p.likesCount) : null],
@@ -10899,18 +9646,18 @@ function printPostDetail(p) {
10899
9646
  console.log("");
10900
9647
  }
10901
9648
  function printCommentLine(c) {
10902
- const author = import_picocolors10.default.bold(String(c.authorName ?? c.userId ?? ""));
9649
+ const author = import_picocolors10.default.bold(c.authorName ?? c.userId ?? "");
10903
9650
  const time = import_picocolors10.default.dim(formatRelativeDate(c.createdAt));
10904
9651
  const replies = c.repliesCount ? import_picocolors10.default.dim(`\xB7 ${c.repliesCount} replies`) : "";
10905
- const text = String(c.text ?? "");
9652
+ const text = c.text ?? "";
10906
9653
  console.log(` ${author} ${time}${replies}`);
10907
9654
  console.log(` ${text}`);
10908
9655
  console.log("");
10909
9656
  }
10910
9657
  function printReplyLine(r) {
10911
- const text = truncate(String(r.text ?? ""), 60);
9658
+ const text = truncate(r.text ?? "", 60);
10912
9659
  const time = formatRelativeDate(r.createdAt);
10913
- const id = import_picocolors10.default.dim(String(r.id ?? ""));
9660
+ const id = import_picocolors10.default.dim(r.id);
10914
9661
  console.log(` ${text} ${import_picocolors10.default.dim(time)} ${id}`);
10915
9662
  }
10916
9663
  function registerPostsCommands(program3) {
@@ -11012,148 +9759,135 @@ Examples:
11012
9759
  }
11013
9760
  });
11014
9761
  });
11015
- if (isAdmin()) {
11016
- posts.command("create").description("Create a new post").option("--title <title>").option("--body <body>").option("--tag <tag>", "general|hack|story|meme|other|question|hack-tip|activity").action(async function() {
11017
- const opts = this.optsWithGlobals();
11018
- const uid = requireAdmin();
11019
- const title = await promptForMissing({ value: opts.title, message: "Title", placeholder: "Post title" });
11020
- const postBody = await promptForMissing({ value: opts.body, message: "Body", placeholder: "Post body" });
11021
- let tag = opts.tag;
11022
- if (!tag) {
11023
- tag = await promptSelect({
11024
- message: "Tag",
11025
- options: POST_TAGS.map((t2) => ({ value: t2, label: t2 }))
11026
- });
11027
- }
11028
- const body = { title, body: postBody, tag };
11029
- await runCreate({
11030
- global: opts,
11031
- fn: () => createPost(uid, body),
11032
- dataKey: "post",
11033
- spinnerMessage: "Creating post...",
11034
- onInteractive: (post) => {
11035
- console.log(`
11036
- ${import_picocolors10.default.green("Posted!")} ${post.title} ${import_picocolors10.default.dim(post.id)}
11037
- `);
11038
- }
9762
+ posts.command("create").description("Create a new post").option("--title <title>").option("--body <body>").option("--tag <tag>", "general|hack|story|meme|other|question|hack-tip|activity").action(async function() {
9763
+ const opts = this.optsWithGlobals();
9764
+ const uid = requireUid();
9765
+ const title = await promptForMissing({ value: opts.title, message: "Title", placeholder: "Post title" });
9766
+ const postBody = await promptForMissing({ value: opts.body, message: "Body", placeholder: "Post body" });
9767
+ let tag = opts.tag;
9768
+ if (!tag) {
9769
+ tag = await promptSelect({
9770
+ message: "Tag",
9771
+ options: POST_TAGS.map((t2) => ({ value: t2, label: t2 }))
11039
9772
  });
9773
+ }
9774
+ const body = { title, body: postBody, tag };
9775
+ await runCreate({
9776
+ global: opts,
9777
+ fn: () => createPost(uid, body),
9778
+ dataKey: "post",
9779
+ spinnerMessage: "Creating post...",
9780
+ onInteractive: (post, payload) => {
9781
+ console.log(`
9782
+ ${import_picocolors10.default.green("Posted!")} ${payload.post.title} ${import_picocolors10.default.dim(payload.post.id)}
9783
+ `);
9784
+ }
11040
9785
  });
11041
- posts.command("update [id]").description("Update a post").option("--title <title>").option("--body <body>").option("--tag <tag>").action(async function(id) {
11042
- const opts = this.optsWithGlobals();
11043
- const uid = requireAdmin();
11044
- const postId = await promptForMissing({ value: id, message: "Post ID" });
11045
- const body = {};
11046
- const hasAnyFlag = opts.title || opts.body || opts.tag;
11047
- if (!hasAnyFlag && isInteractive() && !opts.json) {
11048
- const title = await promptText({ message: "Title (enter to skip)", required: false });
11049
- if (title) body.title = title;
11050
- const postBody = await promptText({ message: "Body (enter to skip)", required: false });
11051
- if (postBody) body.body = postBody;
11052
- const changeTag = await promptText({ message: "Tag (enter to skip)", placeholder: POST_TAGS.join("|"), required: false });
11053
- if (changeTag) body.tag = changeTag;
11054
- } else {
11055
- if (opts.title) body.title = opts.title;
11056
- if (opts.body) body.body = opts.body;
11057
- if (opts.tag) body.tag = opts.tag;
11058
- }
11059
- await runWrite({
11060
- global: opts,
11061
- fn: () => updatePost(uid, postId, body),
11062
- dataKey: "post",
11063
- spinnerMessage: "Updating post...",
11064
- onInteractive: (post) => {
11065
- console.log(`
11066
- ${import_picocolors10.default.green("Updated!")} ${post.title} ${import_picocolors10.default.dim(post.id)}
9786
+ });
9787
+ posts.command("update [id]").description("Update a post").option("--title <title>").option("--body <body>").option("--tag <tag>").action(async function(id) {
9788
+ const opts = this.optsWithGlobals();
9789
+ const uid = requireUid();
9790
+ const postId = await promptForMissing({ value: id, message: "Post ID" });
9791
+ const body = {};
9792
+ const hasAnyFlag = opts.title || opts.body || opts.tag;
9793
+ if (!hasAnyFlag && isInteractive() && !opts.json) {
9794
+ const title = await promptText({ message: "Title (enter to skip)", required: false });
9795
+ if (title) body.title = title;
9796
+ const postBody = await promptText({ message: "Body (enter to skip)", required: false });
9797
+ if (postBody) body.body = postBody;
9798
+ const changeTag = await promptText({ message: "Tag (enter to skip)", placeholder: POST_TAGS.join("|"), required: false });
9799
+ if (changeTag) body.tag = changeTag;
9800
+ } else {
9801
+ if (opts.title) body.title = opts.title;
9802
+ if (opts.body) body.body = opts.body;
9803
+ if (opts.tag) body.tag = opts.tag;
9804
+ }
9805
+ await runWrite({
9806
+ global: opts,
9807
+ fn: () => updatePost(uid, postId, body),
9808
+ dataKey: "post",
9809
+ spinnerMessage: "Updating post...",
9810
+ onInteractive: (payload) => {
9811
+ console.log(`
9812
+ ${import_picocolors10.default.green("Updated!")} ${payload.post.title} ${import_picocolors10.default.dim(payload.post.id)}
11067
9813
  `);
11068
- }
11069
- });
9814
+ }
11070
9815
  });
11071
- posts.command("delete [id]").description("Delete a post").action(async function(id) {
11072
- const uid = requireAdmin();
11073
- const postId = await promptForMissing({ value: id, message: "Post ID" });
11074
- await runDelete({
11075
- global: this.optsWithGlobals(),
11076
- fn: () => deletePost(uid, postId),
11077
- successMessage: ` ${import_picocolors10.default.green("Deleted!")} Post ${import_picocolors10.default.dim(postId)}`,
11078
- spinnerMessage: "Deleting post..."
11079
- });
9816
+ });
9817
+ posts.command("delete [id]").description("Delete a post").action(async function(id) {
9818
+ const uid = requireUid();
9819
+ const postId = await promptForMissing({ value: id, message: "Post ID" });
9820
+ await runDelete({
9821
+ global: this.optsWithGlobals(),
9822
+ fn: () => deletePost(uid, postId),
9823
+ successMessage: ` ${import_picocolors10.default.green("Deleted!")} Post ${import_picocolors10.default.dim(postId)}`,
9824
+ spinnerMessage: "Deleting post..."
11080
9825
  });
11081
- posts.command("comment [postId]").description("Add a comment to a post").option("--text <text>").action(async function(postId) {
11082
- const opts = this.optsWithGlobals();
11083
- const uid = requireAdmin();
11084
- const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
11085
- const text = await promptForMissing({ value: opts.text, message: "Comment text", placeholder: "Your comment" });
11086
- await runCreate({
11087
- global: opts,
11088
- fn: () => createComment(uid, resolvedPostId, text),
11089
- dataKey: "comment",
11090
- spinnerMessage: "Adding comment...",
11091
- onInteractive: (comment) => {
11092
- console.log(`
11093
- ${import_picocolors10.default.green("Commented!")} ${truncate(String(comment.text ?? ""), 50)} ${import_picocolors10.default.dim(comment.id)}
9826
+ });
9827
+ posts.command("comment [postId]").description("Add a comment to a post").option("--text <text>").action(async function(postId) {
9828
+ const opts = this.optsWithGlobals();
9829
+ const uid = requireUid();
9830
+ const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
9831
+ const text = await promptForMissing({ value: opts.text, message: "Comment text", placeholder: "Your comment" });
9832
+ await runCreate({
9833
+ global: opts,
9834
+ fn: () => createComment(uid, resolvedPostId, text),
9835
+ dataKey: "comment",
9836
+ spinnerMessage: "Adding comment...",
9837
+ onInteractive: (comment, payload) => {
9838
+ console.log(`
9839
+ ${import_picocolors10.default.green("Commented!")} ${truncate(payload.comment.text ?? "", 50)} ${import_picocolors10.default.dim(payload.comment.id)}
11094
9840
  `);
11095
- }
11096
- });
9841
+ }
11097
9842
  });
11098
- posts.command("comment-delete [postId] [commentId]").description("Delete a comment").action(async function(postId, commentId) {
11099
- const uid = requireAdmin();
11100
- const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
11101
- const resolvedCommentId = await pickComment(resolvedPostId, commentId);
11102
- await runDelete({
11103
- global: this.optsWithGlobals(),
11104
- fn: () => deleteComment(uid, resolvedPostId, resolvedCommentId),
11105
- successMessage: ` ${import_picocolors10.default.green("Deleted!")} Comment ${import_picocolors10.default.dim(resolvedCommentId)}`,
11106
- spinnerMessage: "Deleting comment..."
11107
- });
9843
+ });
9844
+ posts.command("comment-delete [postId] [commentId]").description("Delete a comment").action(async function(postId, commentId) {
9845
+ const uid = requireUid();
9846
+ const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
9847
+ const resolvedCommentId = await pickComment(resolvedPostId, commentId);
9848
+ await runDelete({
9849
+ global: this.optsWithGlobals(),
9850
+ fn: () => deleteComment(uid, resolvedPostId, resolvedCommentId),
9851
+ successMessage: ` ${import_picocolors10.default.green("Deleted!")} Comment ${import_picocolors10.default.dim(resolvedCommentId)}`,
9852
+ spinnerMessage: "Deleting comment..."
11108
9853
  });
11109
- posts.command("reply [postId] [commentId]").description("Add a reply to a comment").option("--text <text>").action(async function(postId, commentId) {
11110
- const opts = this.optsWithGlobals();
11111
- const uid = requireAdmin();
11112
- const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
11113
- const resolvedCommentId = await pickComment(resolvedPostId, commentId);
11114
- const text = await promptForMissing({ value: opts.text, message: "Reply text", placeholder: "Your reply" });
11115
- await runCreate({
11116
- global: opts,
11117
- fn: () => createReply(uid, resolvedPostId, resolvedCommentId, text),
11118
- dataKey: "reply",
11119
- spinnerMessage: "Adding reply...",
11120
- onInteractive: (reply) => {
11121
- console.log(`
11122
- ${import_picocolors10.default.green("Replied!")} ${truncate(String(reply.text ?? ""), 50)} ${import_picocolors10.default.dim(reply.id)}
9854
+ });
9855
+ posts.command("reply [postId] [commentId]").description("Add a reply to a comment").option("--text <text>").action(async function(postId, commentId) {
9856
+ const opts = this.optsWithGlobals();
9857
+ const uid = requireUid();
9858
+ const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
9859
+ const resolvedCommentId = await pickComment(resolvedPostId, commentId);
9860
+ const text = await promptForMissing({ value: opts.text, message: "Reply text", placeholder: "Your reply" });
9861
+ await runCreate({
9862
+ global: opts,
9863
+ fn: () => createReply(uid, resolvedPostId, resolvedCommentId, text),
9864
+ dataKey: "reply",
9865
+ spinnerMessage: "Adding reply...",
9866
+ onInteractive: (reply, payload) => {
9867
+ console.log(`
9868
+ ${import_picocolors10.default.green("Replied!")} ${truncate(payload.reply.text ?? "", 50)} ${import_picocolors10.default.dim(payload.reply.id)}
11123
9869
  `);
11124
- }
11125
- });
9870
+ }
11126
9871
  });
11127
- posts.command("reply-delete [postId] [commentId] [replyId]").description("Delete a reply").action(async function(postId, commentId, replyId) {
11128
- const uid = requireAdmin();
11129
- const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
11130
- const resolvedCommentId = await pickComment(resolvedPostId, commentId);
11131
- const resolvedReplyId = await pickReply(resolvedPostId, resolvedCommentId, replyId);
11132
- await runDelete({
11133
- global: this.optsWithGlobals(),
11134
- fn: () => deleteReply(uid, resolvedPostId, resolvedCommentId, resolvedReplyId),
11135
- successMessage: ` ${import_picocolors10.default.green("Deleted!")} Reply ${import_picocolors10.default.dim(resolvedReplyId)}`,
11136
- spinnerMessage: "Deleting reply..."
11137
- });
9872
+ });
9873
+ posts.command("reply-delete [postId] [commentId] [replyId]").description("Delete a reply").action(async function(postId, commentId, replyId) {
9874
+ const uid = requireUid();
9875
+ const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
9876
+ const resolvedCommentId = await pickComment(resolvedPostId, commentId);
9877
+ const resolvedReplyId = await pickReply(resolvedPostId, resolvedCommentId, replyId);
9878
+ await runDelete({
9879
+ global: this.optsWithGlobals(),
9880
+ fn: () => deleteReply(uid, resolvedPostId, resolvedCommentId, resolvedReplyId),
9881
+ successMessage: ` ${import_picocolors10.default.green("Deleted!")} Reply ${import_picocolors10.default.dim(resolvedReplyId)}`,
9882
+ spinnerMessage: "Deleting reply..."
11138
9883
  });
11139
- }
9884
+ });
11140
9885
  }
11141
9886
 
11142
- // src/cli/commands/profile.ts
11143
- init_define_ADMIN_UIDS();
11144
-
11145
9887
  // src/cli/services/profile.ts
11146
- init_define_ADMIN_UIDS();
9888
+ init_api_client();
11147
9889
  async function getProfile() {
11148
- const creds = loadCredentials();
11149
- if (!creds) throw new Error("Not logged in. Run: numo login");
11150
- const doc = await getDoc(`users/${creds.uid}`);
11151
- return {
11152
- uid: creds.uid,
11153
- email: creds.email ?? null,
11154
- username: doc.username ?? null,
11155
- photoURL: doc.photoURL ?? null
11156
- };
9890
+ return api.get("/api/profile");
11157
9891
  }
11158
9892
 
11159
9893
  // src/cli/commands/profile.ts
@@ -11177,10 +9911,9 @@ function registerProfileCommands(program3) {
11177
9911
  }
11178
9912
 
11179
9913
  // src/cli/commands/doctor.ts
11180
- init_define_ADMIN_UIDS();
11181
9914
  var import_picocolors11 = __toESM(require_picocolors(), 1);
11182
- init_config();
11183
- init_config();
9915
+ init_credentials();
9916
+ init_api_client();
11184
9917
  init_tty();
11185
9918
  async function runChecks() {
11186
9919
  const checks = [];
@@ -11191,6 +9924,11 @@ async function runChecks() {
11191
9924
  status: major >= 18 ? "ok" : "fail",
11192
9925
  message: major >= 18 ? `Node ${nodeVersion}` : `Node ${nodeVersion} \u2014 requires >= 18`
11193
9926
  });
9927
+ checks.push({
9928
+ name: "api_url",
9929
+ status: process.env.NUMO_API_URL ? "ok" : "warn",
9930
+ message: process.env.NUMO_API_URL ? `API URL: ${API_BASE}` : `NUMO_API_URL not set (using default: ${API_BASE})`
9931
+ });
11194
9932
  const creds = loadCredentials();
11195
9933
  checks.push({
11196
9934
  name: "credentials",
@@ -11207,18 +9945,11 @@ async function runChecks() {
11207
9945
  } else {
11208
9946
  checks.push({ name: "token", status: "fail", message: "Skipped (no credentials)" });
11209
9947
  }
11210
- const apiKey = getFirebaseApiKey();
11211
- checks.push({
11212
- name: "api_key",
11213
- status: apiKey ? "ok" : "fail",
11214
- message: apiKey ? "Firebase API key configured" : "NUMO_FIREBASE_API_KEY not set"
11215
- });
11216
9948
  try {
11217
- const baseUrl = getFirestoreBaseUrl();
11218
- const resp = await fetch(baseUrl, { signal: AbortSignal.timeout(5e3) });
11219
- checks.push({ name: "firebase_reachable", status: "ok", message: `Firebase reachable (HTTP ${resp.status})` });
9949
+ const resp = await fetch(`${API_BASE}/api/health`, { signal: AbortSignal.timeout(5e3) });
9950
+ checks.push({ name: "api_reachable", status: "ok", message: `API server reachable (HTTP ${resp.status})` });
11220
9951
  } catch (err) {
11221
- checks.push({ name: "firebase_reachable", status: "fail", message: `Firebase unreachable: ${err.message}` });
9952
+ checks.push({ name: "api_reachable", status: "fail", message: `API server unreachable: ${err.message}` });
11222
9953
  }
11223
9954
  return checks;
11224
9955
  }
@@ -11248,20 +9979,23 @@ function registerDoctorCommand(program3) {
11248
9979
  });
11249
9980
  }
11250
9981
 
9982
+ // src/cli/cli.ts
9983
+ init_dirs();
9984
+
11251
9985
  // src/cli/lib/update-check.ts
11252
- init_define_ADMIN_UIDS();
11253
- var fs5 = __toESM(require("fs"), 1);
11254
- var path3 = __toESM(require("path"), 1);
9986
+ var fs4 = __toESM(require("fs"), 1);
9987
+ var path2 = __toESM(require("path"), 1);
11255
9988
  var import_picocolors12 = __toESM(require_picocolors(), 1);
9989
+ init_dirs();
11256
9990
  init_tty();
11257
9991
  var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
11258
9992
  var PACKAGE_NAME = "numo-cli";
11259
9993
  function getStatePath() {
11260
- return path3.join(getConfigDir(), "update-check.json");
9994
+ return path2.join(getConfigDir(), "update-check.json");
11261
9995
  }
11262
9996
  function loadState() {
11263
9997
  try {
11264
- return JSON.parse(fs5.readFileSync(getStatePath(), "utf8"));
9998
+ return JSON.parse(fs4.readFileSync(getStatePath(), "utf8"));
11265
9999
  } catch {
11266
10000
  return { lastCheck: 0 };
11267
10001
  }
@@ -11269,7 +10003,7 @@ function loadState() {
11269
10003
  function saveState(state) {
11270
10004
  try {
11271
10005
  ensureConfigDir();
11272
- fs5.writeFileSync(getStatePath(), JSON.stringify(state), { mode: 384 });
10006
+ fs4.writeFileSync(getStatePath(), JSON.stringify(state), { mode: 384 });
11273
10007
  } catch {
11274
10008
  }
11275
10009
  }
@@ -11318,7 +10052,7 @@ function fetchLatestVersion(state) {
11318
10052
  // src/cli/cli.ts
11319
10053
  init_tty();
11320
10054
  init_errors();
11321
- var CLI_VERSION = true ? "1.2.0" : "0.0.0-dev";
10055
+ var CLI_VERSION = true ? "1.5.0" : "0.0.0-dev";
11322
10056
  var program2 = new Command();
11323
10057
  program2.name("numo").description("CLI for Numo \u2014 programmatic access for humans and AI agents").version(CLI_VERSION).option("--json [fields]", "Output as JSON (optionally: comma-separated field names)").option("-q, --quiet", "Suppress interactive output, implies --json").hook("preAction", (thisCommand) => {
11324
10058
  const opts = thisCommand.optsWithGlobals();
@@ -11340,7 +10074,7 @@ program2.command("login").description("Login with your Numo account").option("--
11340
10074
  Examples:
11341
10075
  $ numo login # Interactive (email/password)
11342
10076
  $ numo login --phone # SMS OTP flow`);
11343
- program2.command("register").description("Create a new Numo account").option("--email <email>", "Email address").option("--password <password>", "Password (min 6 characters)").action(async (opts) => {
10077
+ program2.command("register").description("Create a new Numo account").option("--email <email>", "Email address").option("--password <password>", "Password (min 6 chars; visible in ps/history \u2014 prefer interactive mode)").action(async (opts) => {
11344
10078
  await register(opts);
11345
10079
  }).addHelpText("after", `
11346
10080
  Examples:
@@ -11348,7 +10082,6 @@ Examples:
11348
10082
  $ numo register --email user@example.com --password s3cret # Non-interactive`);
11349
10083
  program2.command("logout").description("Clear stored credentials").action(() => {
11350
10084
  clearCredentials();
11351
- clearStreaks();
11352
10085
  console.log(import_picocolors13.default.green("Logged out."));
11353
10086
  });
11354
10087
  program2.command("whoami").description("Show current auth status (no API call)").action(function() {
@@ -11401,17 +10134,16 @@ program2.command("add [text...]").description("Quick-add a task (today, public,
11401
10134
  dataKey: "task",
11402
10135
  spinnerMessage: "Creating task...",
11403
10136
  onInteractive: (_task, payload) => {
11404
- const task = payload.task;
10137
+ const { task, karma } = payload;
11405
10138
  const check = import_picocolors13.default.green(SYM.check);
11406
10139
  console.log(`
11407
10140
  ${check} Created ${task.text} ${import_picocolors13.default.dim(task.id)}`);
11408
- const karma = payload.karma;
11409
10141
  if (karma) console.log(` ${formatKarmaGain(karma)}`);
11410
10142
  console.log("");
11411
10143
  }
11412
10144
  });
11413
10145
  });
11414
- if (isAdmin()) registerPostsCommands(program2);
10146
+ registerPostsCommands(program2);
11415
10147
  registerProfileCommands(program2);
11416
10148
  registerDoctorCommand(program2);
11417
10149
  program2.command("commands").description("List all available commands").action(function() {