numo-cli 1.3.0 → 1.6.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.
- package/LICENSE +1 -1
- package/README.md +13 -118
- package/dist/cli.cjs +1287 -1206
- package/package.json +14 -14
package/dist/cli.cjs
CHANGED
|
@@ -3217,6 +3217,390 @@ var init_http = __esm({
|
|
|
3217
3217
|
}
|
|
3218
3218
|
});
|
|
3219
3219
|
|
|
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(/\beyJ[\w-]+\.[\w-]+\.[\w-]+\b/g, "<jwt>").replace(/[\w.+-]+@[\w-]+\.[\w.-]+/g, "<email>").replace(/[A-Za-z0-9_=+/-]{32,}/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-base.ts
|
|
3377
|
+
function isLoopback(host) {
|
|
3378
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
3379
|
+
}
|
|
3380
|
+
function classifyApiBase(base = API_BASE, fromEnv = FROM_ENV) {
|
|
3381
|
+
let u2;
|
|
3382
|
+
try {
|
|
3383
|
+
u2 = new URL(base);
|
|
3384
|
+
} catch {
|
|
3385
|
+
return { ok: false, message: `Invalid NUMO_API_URL: ${base}` };
|
|
3386
|
+
}
|
|
3387
|
+
if (u2.protocol !== "http:" && u2.protocol !== "https:") {
|
|
3388
|
+
return { ok: false, message: `NUMO_API_URL must be http(s) \u2014 got "${u2.protocol}"` };
|
|
3389
|
+
}
|
|
3390
|
+
const insecure = u2.protocol === "http:" && !isLoopback(u2.hostname);
|
|
3391
|
+
const trusted = isLoopback(u2.hostname) || u2.hostname === "numo.ai" || u2.hostname.endsWith(".numo.ai");
|
|
3392
|
+
if (fromEnv && !trusted && process.env.NUMO_ALLOW_CUSTOM_HOST !== "1") {
|
|
3393
|
+
return {
|
|
3394
|
+
ok: false,
|
|
3395
|
+
message: `Refusing to send credentials to untrusted host "${u2.hostname}". Use https://api.numo.ai, or set NUMO_ALLOW_CUSTOM_HOST=1 for a self-hosted/staging server.`
|
|
3396
|
+
};
|
|
3397
|
+
}
|
|
3398
|
+
return { ok: true, insecure };
|
|
3399
|
+
}
|
|
3400
|
+
function assertSafeApiBase() {
|
|
3401
|
+
const verdict = classifyApiBase();
|
|
3402
|
+
if (!verdict.ok) {
|
|
3403
|
+
throw new CliError("CONFIG_ERROR" /* CONFIG_ERROR */, verdict.message, ExitCode.CONFIG, {
|
|
3404
|
+
suggestion: "export NUMO_ALLOW_CUSTOM_HOST=1"
|
|
3405
|
+
});
|
|
3406
|
+
}
|
|
3407
|
+
if (verdict.insecure && !warnedInsecure) {
|
|
3408
|
+
warnedInsecure = true;
|
|
3409
|
+
process.stderr.write("[warn] NUMO_API_URL uses HTTP \u2014 tokens sent unencrypted. Use HTTPS in production.\n");
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
var API_BASE, FROM_ENV, warnedInsecure;
|
|
3413
|
+
var init_api_base = __esm({
|
|
3414
|
+
"src/cli/lib/api-base.ts"() {
|
|
3415
|
+
"use strict";
|
|
3416
|
+
init_errors();
|
|
3417
|
+
API_BASE = process.env.NUMO_API_URL ?? (true ? "https://api.numo.ai" : "http://localhost:3000");
|
|
3418
|
+
FROM_ENV = process.env.NUMO_API_URL !== void 0;
|
|
3419
|
+
warnedInsecure = false;
|
|
3420
|
+
}
|
|
3421
|
+
});
|
|
3422
|
+
|
|
3423
|
+
// src/cli/lib/api-client.ts
|
|
3424
|
+
var api_client_exports = {};
|
|
3425
|
+
__export(api_client_exports, {
|
|
3426
|
+
API_BASE: () => API_BASE,
|
|
3427
|
+
api: () => api
|
|
3428
|
+
});
|
|
3429
|
+
function toCliError(err) {
|
|
3430
|
+
if (err instanceof CliError) return err;
|
|
3431
|
+
const httpErr = err;
|
|
3432
|
+
if (httpErr.code === "ECONNABORTED" || httpErr.code === "ETIMEDOUT") {
|
|
3433
|
+
return new CliError("TIMEOUT" /* TIMEOUT */, "Request timed out", ExitCode.TEMP_FAIL, {
|
|
3434
|
+
hint: "The API server took too long to respond.",
|
|
3435
|
+
retryable: true
|
|
3436
|
+
});
|
|
3437
|
+
}
|
|
3438
|
+
if (httpErr.code === "ECONNREFUSED" || httpErr.code === "ECONNRESET" || httpErr.code === "ENOTFOUND") {
|
|
3439
|
+
return new CliError("NETWORK_ERROR" /* NETWORK_ERROR */, "Can't reach Numo API", ExitCode.UNAVAILABLE, {
|
|
3440
|
+
hint: "Is the API server running? Check NUMO_API_URL.",
|
|
3441
|
+
retryable: true
|
|
3442
|
+
});
|
|
3443
|
+
}
|
|
3444
|
+
const body = httpErr.response?.data;
|
|
3445
|
+
if (body?.error) {
|
|
3446
|
+
const e = body.error;
|
|
3447
|
+
const kind = e.kind ?? "UNKNOWN" /* UNKNOWN */;
|
|
3448
|
+
const exitCode = KIND_EXIT[kind] ?? ExitCode.GENERAL;
|
|
3449
|
+
return new CliError(kind, sanitizeErrorMessage(e.message ?? "Unknown error"), exitCode, {
|
|
3450
|
+
retryable: e.retryable,
|
|
3451
|
+
retryAfter: e.retryAfter
|
|
3452
|
+
});
|
|
3453
|
+
}
|
|
3454
|
+
return new CliError("UNKNOWN" /* UNKNOWN */, sanitizeErrorMessage(httpErr.message ?? "Unknown error"), ExitCode.GENERAL);
|
|
3455
|
+
}
|
|
3456
|
+
async function apiHeaders() {
|
|
3457
|
+
assertSafeApiBase();
|
|
3458
|
+
const token = await getIdToken();
|
|
3459
|
+
return {
|
|
3460
|
+
Authorization: `Bearer ${token}`,
|
|
3461
|
+
"Content-Type": "application/json"
|
|
3462
|
+
};
|
|
3463
|
+
}
|
|
3464
|
+
function url(path3, params) {
|
|
3465
|
+
const u2 = `${API_BASE}${path3}`;
|
|
3466
|
+
if (!params) return u2;
|
|
3467
|
+
const sp = new URLSearchParams();
|
|
3468
|
+
for (const [k2, v] of Object.entries(params)) {
|
|
3469
|
+
if (v !== void 0) sp.set(k2, v);
|
|
3470
|
+
}
|
|
3471
|
+
const qs = sp.toString();
|
|
3472
|
+
return qs ? `${u2}?${qs}` : u2;
|
|
3473
|
+
}
|
|
3474
|
+
var KIND_EXIT, api;
|
|
3475
|
+
var init_api_client = __esm({
|
|
3476
|
+
"src/cli/lib/api-client.ts"() {
|
|
3477
|
+
"use strict";
|
|
3478
|
+
init_credentials();
|
|
3479
|
+
init_http();
|
|
3480
|
+
init_errors();
|
|
3481
|
+
init_api_base();
|
|
3482
|
+
KIND_EXIT = {
|
|
3483
|
+
AUTH_REQUIRED: ExitCode.NO_PERM,
|
|
3484
|
+
AUTH_EXPIRED: ExitCode.NO_PERM,
|
|
3485
|
+
AUTH_FORBIDDEN: ExitCode.NO_PERM,
|
|
3486
|
+
INVALID_INPUT: ExitCode.USAGE,
|
|
3487
|
+
MISSING_ARGUMENT: ExitCode.USAGE,
|
|
3488
|
+
NOT_FOUND: ExitCode.NOT_FOUND,
|
|
3489
|
+
CONFLICT: ExitCode.CONFLICT,
|
|
3490
|
+
RATE_LIMITED: ExitCode.TEMP_FAIL,
|
|
3491
|
+
NETWORK_ERROR: ExitCode.UNAVAILABLE,
|
|
3492
|
+
TIMEOUT: ExitCode.TEMP_FAIL,
|
|
3493
|
+
SERVICE_UNAVAILABLE: ExitCode.UNAVAILABLE
|
|
3494
|
+
};
|
|
3495
|
+
api = {
|
|
3496
|
+
async get(path3, params) {
|
|
3497
|
+
try {
|
|
3498
|
+
const resp = await http.get(url(path3, params), { headers: await apiHeaders() });
|
|
3499
|
+
return resp.data;
|
|
3500
|
+
} catch (err) {
|
|
3501
|
+
throw toCliError(err);
|
|
3502
|
+
}
|
|
3503
|
+
},
|
|
3504
|
+
async post(path3, body) {
|
|
3505
|
+
try {
|
|
3506
|
+
const resp = await http.post(url(path3), body, { headers: await apiHeaders() });
|
|
3507
|
+
return resp.data;
|
|
3508
|
+
} catch (err) {
|
|
3509
|
+
throw toCliError(err);
|
|
3510
|
+
}
|
|
3511
|
+
},
|
|
3512
|
+
async patch(path3, body) {
|
|
3513
|
+
try {
|
|
3514
|
+
const resp = await http.patch(url(path3), body, { headers: await apiHeaders() });
|
|
3515
|
+
return resp.data;
|
|
3516
|
+
} catch (err) {
|
|
3517
|
+
throw toCliError(err);
|
|
3518
|
+
}
|
|
3519
|
+
},
|
|
3520
|
+
async del(path3) {
|
|
3521
|
+
try {
|
|
3522
|
+
const resp = await http.delete(url(path3), { headers: await apiHeaders() });
|
|
3523
|
+
return resp.data;
|
|
3524
|
+
} catch (err) {
|
|
3525
|
+
throw toCliError(err);
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
};
|
|
3529
|
+
}
|
|
3530
|
+
});
|
|
3531
|
+
|
|
3532
|
+
// src/cli/auth/credentials.ts
|
|
3533
|
+
function loadCredentials() {
|
|
3534
|
+
try {
|
|
3535
|
+
const path3 = getCredentialsPath();
|
|
3536
|
+
if (process.platform !== "win32" && fs2.statSync(path3).mode & 63) {
|
|
3537
|
+
process.stderr.write(`[warn] credentials file is group/other-readable. Run: chmod 600 ${path3}
|
|
3538
|
+
`);
|
|
3539
|
+
}
|
|
3540
|
+
const data = JSON.parse(fs2.readFileSync(path3, "utf8"));
|
|
3541
|
+
if (typeof data?.refreshToken !== "string" || typeof data?.uid !== "string" || typeof data?.email !== "string") {
|
|
3542
|
+
return null;
|
|
3543
|
+
}
|
|
3544
|
+
return data;
|
|
3545
|
+
} catch {
|
|
3546
|
+
return null;
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
function saveCredentials(creds) {
|
|
3550
|
+
ensureConfigDir();
|
|
3551
|
+
const path3 = getCredentialsPath();
|
|
3552
|
+
fs2.writeFileSync(path3, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
3553
|
+
if (process.platform !== "win32") fs2.chmodSync(path3, 384);
|
|
3554
|
+
}
|
|
3555
|
+
function clearCredentials() {
|
|
3556
|
+
try {
|
|
3557
|
+
const credPath = getCredentialsPath();
|
|
3558
|
+
const stat = fs2.statSync(credPath);
|
|
3559
|
+
fs2.writeFileSync(credPath, crypto.randomBytes(stat.size));
|
|
3560
|
+
fs2.unlinkSync(credPath);
|
|
3561
|
+
} catch {
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
async function getIdToken() {
|
|
3565
|
+
const envToken = process.env.NUMO_TOKEN;
|
|
3566
|
+
if (envToken) return envToken;
|
|
3567
|
+
const creds = loadCredentials();
|
|
3568
|
+
if (!creds) throw new Error("Not logged in. Run: numo login");
|
|
3569
|
+
if (creds.idToken && creds.idTokenExpiry && Date.now() < creds.idTokenExpiry - 6e4) {
|
|
3570
|
+
return creds.idToken;
|
|
3571
|
+
}
|
|
3572
|
+
if (refreshInFlight) return refreshInFlight;
|
|
3573
|
+
refreshInFlight = performRefresh(creds).finally(() => {
|
|
3574
|
+
refreshInFlight = null;
|
|
3575
|
+
});
|
|
3576
|
+
return refreshInFlight;
|
|
3577
|
+
}
|
|
3578
|
+
async function performRefresh(creds) {
|
|
3579
|
+
assertSafeApiBase();
|
|
3580
|
+
const { API_BASE: apiBase } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
|
|
3581
|
+
const { http: http2 } = await Promise.resolve().then(() => (init_http(), http_exports));
|
|
3582
|
+
const resp = await http2.post(
|
|
3583
|
+
`${apiBase}/api/auth/refresh`,
|
|
3584
|
+
{ refreshToken: creds.refreshToken }
|
|
3585
|
+
);
|
|
3586
|
+
creds.idToken = resp.data.idToken;
|
|
3587
|
+
creds.refreshToken = resp.data.refreshToken ?? creds.refreshToken;
|
|
3588
|
+
creds.idTokenExpiry = Date.now() + (resp.data.expiresIn || 3600) * 1e3;
|
|
3589
|
+
saveCredentials(creds);
|
|
3590
|
+
return creds.idToken;
|
|
3591
|
+
}
|
|
3592
|
+
var fs2, crypto, refreshInFlight;
|
|
3593
|
+
var init_credentials = __esm({
|
|
3594
|
+
"src/cli/auth/credentials.ts"() {
|
|
3595
|
+
"use strict";
|
|
3596
|
+
fs2 = __toESM(require("fs"), 1);
|
|
3597
|
+
crypto = __toESM(require("crypto"), 1);
|
|
3598
|
+
init_dirs();
|
|
3599
|
+
init_api_base();
|
|
3600
|
+
refreshInFlight = null;
|
|
3601
|
+
}
|
|
3602
|
+
});
|
|
3603
|
+
|
|
3220
3604
|
// src/cli/lib/tty.ts
|
|
3221
3605
|
function isInteractive() {
|
|
3222
3606
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
@@ -4931,7 +5315,12 @@ async function loadClack() {
|
|
|
4931
5315
|
}
|
|
4932
5316
|
async function promptText(opts) {
|
|
4933
5317
|
if (!isInteractive()) {
|
|
4934
|
-
throw new
|
|
5318
|
+
throw new CliError(
|
|
5319
|
+
"MISSING_ARGUMENT" /* MISSING_ARGUMENT */,
|
|
5320
|
+
`Missing required input: ${opts.message}`,
|
|
5321
|
+
ExitCode.USAGE,
|
|
5322
|
+
{ hint: "Use flags in non-interactive mode." }
|
|
5323
|
+
);
|
|
4935
5324
|
}
|
|
4936
5325
|
const p = await loadClack();
|
|
4937
5326
|
const value = await p.text({
|
|
@@ -4946,7 +5335,12 @@ async function promptText(opts) {
|
|
|
4946
5335
|
}
|
|
4947
5336
|
async function promptPassword(opts) {
|
|
4948
5337
|
if (!isInteractive()) {
|
|
4949
|
-
throw new
|
|
5338
|
+
throw new CliError(
|
|
5339
|
+
"MISSING_ARGUMENT" /* MISSING_ARGUMENT */,
|
|
5340
|
+
`Missing required input: ${opts.message}`,
|
|
5341
|
+
ExitCode.USAGE,
|
|
5342
|
+
{ hint: "Use flags in non-interactive mode." }
|
|
5343
|
+
);
|
|
4950
5344
|
}
|
|
4951
5345
|
const p = await loadClack();
|
|
4952
5346
|
const value = await p.password({
|
|
@@ -4960,7 +5354,12 @@ async function promptPassword(opts) {
|
|
|
4960
5354
|
}
|
|
4961
5355
|
async function promptSelect(opts) {
|
|
4962
5356
|
if (!isInteractive()) {
|
|
4963
|
-
throw new
|
|
5357
|
+
throw new CliError(
|
|
5358
|
+
"MISSING_ARGUMENT" /* MISSING_ARGUMENT */,
|
|
5359
|
+
`Missing required input: ${opts.message}`,
|
|
5360
|
+
ExitCode.USAGE,
|
|
5361
|
+
{ hint: "Use flags in non-interactive mode." }
|
|
5362
|
+
);
|
|
4964
5363
|
}
|
|
4965
5364
|
const p = await loadClack();
|
|
4966
5365
|
const value = await p.select({
|
|
@@ -4988,137 +5387,39 @@ async function promptConfirm(opts) {
|
|
|
4988
5387
|
}
|
|
4989
5388
|
async function promptMultiSelect(opts) {
|
|
4990
5389
|
if (!isInteractive()) {
|
|
4991
|
-
throw new
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
required: opts.required ?? false
|
|
4998
|
-
});
|
|
4999
|
-
if (p.isCancel(value)) {
|
|
5000
|
-
process.exit(130);
|
|
5001
|
-
}
|
|
5002
|
-
return value;
|
|
5003
|
-
}
|
|
5004
|
-
async function promptForMissing(opts) {
|
|
5005
|
-
if (opts.value !== void 0 && opts.value !== "") {
|
|
5006
|
-
return opts.value;
|
|
5007
|
-
}
|
|
5008
|
-
return promptText({
|
|
5009
|
-
message: opts.message,
|
|
5010
|
-
placeholder: opts.placeholder,
|
|
5011
|
-
required: opts.required ?? true
|
|
5012
|
-
});
|
|
5013
|
-
}
|
|
5014
|
-
var init_prompts = __esm({
|
|
5015
|
-
"src/cli/lib/prompts.ts"() {
|
|
5016
|
-
"use strict";
|
|
5017
|
-
init_tty();
|
|
5018
|
-
}
|
|
5019
|
-
});
|
|
5020
|
-
|
|
5021
|
-
// src/cli/lib/errors.ts
|
|
5022
|
-
function classifyError(err) {
|
|
5023
|
-
if (err instanceof CliError) return err;
|
|
5024
|
-
const axiosErr = err;
|
|
5025
|
-
if (axiosErr.code === "ECONNABORTED" || axiosErr.code === "ETIMEDOUT") return Errors.timeout();
|
|
5026
|
-
if (axiosErr.code === "ENOTFOUND" || axiosErr.code === "EAI_AGAIN") return Errors.networkError();
|
|
5027
|
-
if (axiosErr.code === "ECONNREFUSED" || axiosErr.code === "ECONNRESET") {
|
|
5028
|
-
return Errors.networkError("Service may be temporarily down. Try again in a moment.");
|
|
5029
|
-
}
|
|
5030
|
-
const status = axiosErr.response?.status;
|
|
5031
|
-
if (status === 401) return Errors.authRequired();
|
|
5032
|
-
if (status === 403) {
|
|
5033
|
-
return new CliError("AUTH_FORBIDDEN" /* AUTH_FORBIDDEN */, "Access denied", ExitCode.NO_PERM, {
|
|
5034
|
-
hint: "You don't have permission for this action."
|
|
5035
|
-
});
|
|
5036
|
-
}
|
|
5037
|
-
if (status === 404) return Errors.notFound("Resource");
|
|
5038
|
-
if (status === 429) {
|
|
5039
|
-
const retryAfter = parseInt(axiosErr.response?.headers?.["retry-after"] ?? "");
|
|
5040
|
-
return Errors.rateLimited(isNaN(retryAfter) ? void 0 : retryAfter);
|
|
5390
|
+
throw new CliError(
|
|
5391
|
+
"MISSING_ARGUMENT" /* MISSING_ARGUMENT */,
|
|
5392
|
+
`Missing required input: ${opts.message}`,
|
|
5393
|
+
ExitCode.USAGE,
|
|
5394
|
+
{ hint: "Use flags in non-interactive mode." }
|
|
5395
|
+
);
|
|
5041
5396
|
}
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5397
|
+
const p = await loadClack();
|
|
5398
|
+
const value = await p.multiselect({
|
|
5399
|
+
message: opts.message,
|
|
5400
|
+
options: opts.options,
|
|
5401
|
+
required: opts.required ?? false
|
|
5402
|
+
});
|
|
5403
|
+
if (p.isCancel(value)) {
|
|
5404
|
+
process.exit(130);
|
|
5047
5405
|
}
|
|
5048
|
-
|
|
5049
|
-
const raw = body?.error?.message ?? axiosErr.message ?? "Unknown error";
|
|
5050
|
-
const message = sanitizeErrorMessage(raw);
|
|
5051
|
-
return new CliError("UNKNOWN" /* UNKNOWN */, message, ExitCode.GENERAL, { cause: err });
|
|
5406
|
+
return value;
|
|
5052
5407
|
}
|
|
5053
|
-
function
|
|
5054
|
-
|
|
5408
|
+
async function promptForMissing(opts) {
|
|
5409
|
+
if (opts.value !== void 0 && opts.value !== "") {
|
|
5410
|
+
return opts.value;
|
|
5411
|
+
}
|
|
5412
|
+
return promptText({
|
|
5413
|
+
message: opts.message,
|
|
5414
|
+
placeholder: opts.placeholder,
|
|
5415
|
+
required: opts.required ?? true
|
|
5416
|
+
});
|
|
5055
5417
|
}
|
|
5056
|
-
var
|
|
5057
|
-
|
|
5058
|
-
"src/cli/lib/errors.ts"() {
|
|
5418
|
+
var init_prompts = __esm({
|
|
5419
|
+
"src/cli/lib/prompts.ts"() {
|
|
5059
5420
|
"use strict";
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
GENERAL: 1,
|
|
5063
|
-
USAGE: 2,
|
|
5064
|
-
UNAVAILABLE: 69,
|
|
5065
|
-
TEMP_FAIL: 75,
|
|
5066
|
-
NO_PERM: 77,
|
|
5067
|
-
CONFIG: 78,
|
|
5068
|
-
NOT_FOUND: 100,
|
|
5069
|
-
CONFLICT: 101
|
|
5070
|
-
};
|
|
5071
|
-
CliError = class extends Error {
|
|
5072
|
-
constructor(kind, message, exitCode = ExitCode.GENERAL, options = {}) {
|
|
5073
|
-
super(message);
|
|
5074
|
-
this.kind = kind;
|
|
5075
|
-
this.exitCode = exitCode;
|
|
5076
|
-
this.options = options;
|
|
5077
|
-
this.name = "CliError";
|
|
5078
|
-
}
|
|
5079
|
-
toJSON() {
|
|
5080
|
-
return {
|
|
5081
|
-
error: {
|
|
5082
|
-
kind: this.kind,
|
|
5083
|
-
code: this.exitCode,
|
|
5084
|
-
message: this.message,
|
|
5085
|
-
...this.options.suggestion && { suggestion: this.options.suggestion },
|
|
5086
|
-
...this.options.hint && { hint: this.options.hint },
|
|
5087
|
-
retryable: this.options.retryable ?? false,
|
|
5088
|
-
...this.options.retryAfter != null && { retryAfter: this.options.retryAfter }
|
|
5089
|
-
}
|
|
5090
|
-
};
|
|
5091
|
-
}
|
|
5092
|
-
};
|
|
5093
|
-
Errors = {
|
|
5094
|
-
authRequired: () => new CliError("AUTH_REQUIRED" /* AUTH_REQUIRED */, "Not logged in", ExitCode.NO_PERM, {
|
|
5095
|
-
suggestion: "numo login"
|
|
5096
|
-
}),
|
|
5097
|
-
notFound: (resource, id) => new CliError("NOT_FOUND" /* NOT_FOUND */, `${resource} not found${id ? `: ${id}` : ""}`, ExitCode.NOT_FOUND, {
|
|
5098
|
-
suggestion: `numo ${resource.toLowerCase()}s list`
|
|
5099
|
-
}),
|
|
5100
|
-
missingArg: (name, flag) => new CliError("MISSING_ARGUMENT" /* MISSING_ARGUMENT */, `${name} is required`, ExitCode.USAGE, {
|
|
5101
|
-
suggestion: `Use --${flag}`,
|
|
5102
|
-
hint: "Run with --help for all options."
|
|
5103
|
-
}),
|
|
5104
|
-
invalidInput: (message, hint) => new CliError("INVALID_INPUT" /* INVALID_INPUT */, message, ExitCode.USAGE, { hint }),
|
|
5105
|
-
configMissing: (key) => new CliError("CONFIG_ERROR" /* CONFIG_ERROR */, `${key} not set`, ExitCode.CONFIG, {
|
|
5106
|
-
suggestion: `export ${key}=<value>`
|
|
5107
|
-
}),
|
|
5108
|
-
networkError: (hint) => new CliError("NETWORK_ERROR" /* NETWORK_ERROR */, "Can't reach Numo servers", ExitCode.UNAVAILABLE, {
|
|
5109
|
-
hint: hint ?? "Check your internet connection.",
|
|
5110
|
-
retryable: true
|
|
5111
|
-
}),
|
|
5112
|
-
timeout: () => new CliError("TIMEOUT" /* TIMEOUT */, "Request timed out", ExitCode.TEMP_FAIL, {
|
|
5113
|
-
hint: "The server took too long to respond. Try again.",
|
|
5114
|
-
retryable: true
|
|
5115
|
-
}),
|
|
5116
|
-
rateLimited: (retryAfter) => new CliError("RATE_LIMITED" /* RATE_LIMITED */, "Too many requests", ExitCode.TEMP_FAIL, {
|
|
5117
|
-
hint: retryAfter ? `Wait ${retryAfter} seconds and try again.` : "Wait a moment and try again.",
|
|
5118
|
-
retryable: true,
|
|
5119
|
-
retryAfter
|
|
5120
|
-
})
|
|
5121
|
-
};
|
|
5421
|
+
init_tty();
|
|
5422
|
+
init_errors();
|
|
5122
5423
|
}
|
|
5123
5424
|
});
|
|
5124
5425
|
|
|
@@ -5128,6 +5429,7 @@ __export(phone_login_exports, {
|
|
|
5128
5429
|
authenticateWithPhone: () => authenticateWithPhone
|
|
5129
5430
|
});
|
|
5130
5431
|
async function authenticateWithPhone(spinner) {
|
|
5432
|
+
assertSafeApiBase();
|
|
5131
5433
|
const p = await Promise.resolve().then(() => (init_dist2(), dist_exports));
|
|
5132
5434
|
const phone = await promptText({
|
|
5133
5435
|
message: "Phone number (with country code)",
|
|
@@ -5144,7 +5446,7 @@ async function authenticateWithPhone(spinner) {
|
|
|
5144
5446
|
const { sessionId, verifyUrl } = startResp.data;
|
|
5145
5447
|
spinner.stop("");
|
|
5146
5448
|
p.log.info("Opening browser for phone verification...");
|
|
5147
|
-
p.log.info(
|
|
5449
|
+
p.log.info(import_picocolors4.default.dim(`If the browser does not open, visit: ${verifyUrl}`));
|
|
5148
5450
|
const { default: open } = await import("open");
|
|
5149
5451
|
const cp = await open(verifyUrl);
|
|
5150
5452
|
cp.unref();
|
|
@@ -5171,15 +5473,16 @@ async function authenticateWithPhone(spinner) {
|
|
|
5171
5473
|
}
|
|
5172
5474
|
throw Errors.networkError("Phone verification timed out. Try again.");
|
|
5173
5475
|
}
|
|
5174
|
-
var
|
|
5476
|
+
var import_picocolors4, POLL_INTERVAL, POLL_TIMEOUT;
|
|
5175
5477
|
var init_phone_login = __esm({
|
|
5176
5478
|
"src/cli/auth/phone-login.ts"() {
|
|
5177
5479
|
"use strict";
|
|
5178
5480
|
init_http();
|
|
5179
|
-
|
|
5481
|
+
import_picocolors4 = __toESM(require_picocolors(), 1);
|
|
5180
5482
|
init_errors();
|
|
5181
5483
|
init_prompts();
|
|
5182
|
-
|
|
5484
|
+
init_api_client();
|
|
5485
|
+
init_api_base();
|
|
5183
5486
|
POLL_INTERVAL = 2e3;
|
|
5184
5487
|
POLL_TIMEOUT = 5 * 60 * 1e3;
|
|
5185
5488
|
}
|
|
@@ -5203,297 +5506,82 @@ var {
|
|
|
5203
5506
|
} = import_index.default;
|
|
5204
5507
|
|
|
5205
5508
|
// src/cli/cli.ts
|
|
5206
|
-
var
|
|
5509
|
+
var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
5207
5510
|
|
|
5208
5511
|
// src/cli/auth/login.ts
|
|
5209
5512
|
init_http();
|
|
5210
|
-
var
|
|
5211
|
-
|
|
5212
|
-
// src/cli/auth/credentials.ts
|
|
5213
|
-
var fs2 = __toESM(require("fs"), 1);
|
|
5214
|
-
var crypto = __toESM(require("crypto"), 1);
|
|
5215
|
-
|
|
5216
|
-
// src/cli/lib/dirs.ts
|
|
5217
|
-
var fs = __toESM(require("fs"), 1);
|
|
5218
|
-
var path = __toESM(require("path"), 1);
|
|
5219
|
-
var os = __toESM(require("os"), 1);
|
|
5220
|
-
var LEGACY_DIR = path.join(os.homedir(), ".numo");
|
|
5221
|
-
function getConfigDir() {
|
|
5222
|
-
if (process.env.NUMO_CONFIG_DIR) {
|
|
5223
|
-
return process.env.NUMO_CONFIG_DIR;
|
|
5224
|
-
}
|
|
5225
|
-
const xdgHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
5226
|
-
const xdgDir = path.join(xdgHome, "numo");
|
|
5227
|
-
if (fs.existsSync(xdgDir)) return xdgDir;
|
|
5228
|
-
if (fs.existsSync(LEGACY_DIR)) return LEGACY_DIR;
|
|
5229
|
-
return xdgDir;
|
|
5230
|
-
}
|
|
5231
|
-
function ensureConfigDir() {
|
|
5232
|
-
const dir = getConfigDir();
|
|
5233
|
-
if (!fs.existsSync(dir)) {
|
|
5234
|
-
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
5235
|
-
}
|
|
5236
|
-
return dir;
|
|
5237
|
-
}
|
|
5238
|
-
function getCredentialsPath() {
|
|
5239
|
-
return path.join(getConfigDir(), "credentials.json");
|
|
5240
|
-
}
|
|
5241
|
-
function migrateIfNeeded() {
|
|
5242
|
-
const xdgHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
5243
|
-
const xdgDir = path.join(xdgHome, "numo");
|
|
5244
|
-
if (process.env.NUMO_CONFIG_DIR) return;
|
|
5245
|
-
const legacyCreds = path.join(LEGACY_DIR, "credentials.json");
|
|
5246
|
-
if (!fs.existsSync(legacyCreds) || fs.existsSync(xdgDir)) return;
|
|
5247
|
-
try {
|
|
5248
|
-
fs.mkdirSync(xdgDir, { recursive: true, mode: 448 });
|
|
5249
|
-
const data = fs.readFileSync(legacyCreds, "utf8");
|
|
5250
|
-
fs.writeFileSync(path.join(xdgDir, "credentials.json"), data, { mode: 384 });
|
|
5251
|
-
const legacyStreaks = path.join(LEGACY_DIR, "streaks.json");
|
|
5252
|
-
if (fs.existsSync(legacyStreaks)) {
|
|
5253
|
-
const streaksData = fs.readFileSync(legacyStreaks, "utf8");
|
|
5254
|
-
fs.writeFileSync(path.join(xdgDir, "streaks.json"), streaksData, { mode: 384 });
|
|
5255
|
-
}
|
|
5256
|
-
process.stderr.write(`Migrated config from ${LEGACY_DIR} to ${xdgDir}
|
|
5257
|
-
`);
|
|
5258
|
-
} catch {
|
|
5259
|
-
}
|
|
5260
|
-
}
|
|
5261
|
-
|
|
5262
|
-
// src/cli/auth/credentials.ts
|
|
5263
|
-
function loadCredentials() {
|
|
5264
|
-
try {
|
|
5265
|
-
const data = JSON.parse(fs2.readFileSync(getCredentialsPath(), "utf8"));
|
|
5266
|
-
if (typeof data?.refreshToken !== "string" || typeof data?.uid !== "string" || typeof data?.email !== "string") {
|
|
5267
|
-
return null;
|
|
5268
|
-
}
|
|
5269
|
-
return data;
|
|
5270
|
-
} catch {
|
|
5271
|
-
return null;
|
|
5272
|
-
}
|
|
5273
|
-
}
|
|
5274
|
-
function saveCredentials(creds) {
|
|
5275
|
-
ensureConfigDir();
|
|
5276
|
-
fs2.writeFileSync(getCredentialsPath(), JSON.stringify(creds, null, 2), { mode: 384 });
|
|
5277
|
-
}
|
|
5278
|
-
function clearCredentials() {
|
|
5279
|
-
try {
|
|
5280
|
-
const credPath = getCredentialsPath();
|
|
5281
|
-
const stat = fs2.statSync(credPath);
|
|
5282
|
-
fs2.writeFileSync(credPath, crypto.randomBytes(stat.size));
|
|
5283
|
-
fs2.unlinkSync(credPath);
|
|
5284
|
-
} catch {
|
|
5285
|
-
}
|
|
5286
|
-
}
|
|
5287
|
-
var refreshInFlight = null;
|
|
5288
|
-
async function getIdToken() {
|
|
5289
|
-
const envToken = process.env.NUMO_TOKEN;
|
|
5290
|
-
if (envToken) return envToken;
|
|
5291
|
-
const creds = loadCredentials();
|
|
5292
|
-
if (!creds) throw new Error("Not logged in. Run: numo login");
|
|
5293
|
-
if (creds.idToken && creds.idTokenExpiry && Date.now() < creds.idTokenExpiry - 6e4) {
|
|
5294
|
-
return creds.idToken;
|
|
5295
|
-
}
|
|
5296
|
-
if (refreshInFlight) return refreshInFlight;
|
|
5297
|
-
refreshInFlight = performRefresh(creds).finally(() => {
|
|
5298
|
-
refreshInFlight = null;
|
|
5299
|
-
});
|
|
5300
|
-
return refreshInFlight;
|
|
5301
|
-
}
|
|
5302
|
-
async function performRefresh(creds) {
|
|
5303
|
-
const apiBase = process.env.NUMO_API_URL ?? "http://localhost:3000";
|
|
5304
|
-
const { http: http2 } = await Promise.resolve().then(() => (init_http(), http_exports));
|
|
5305
|
-
const resp = await http2.post(
|
|
5306
|
-
`${apiBase}/api/auth/refresh`,
|
|
5307
|
-
{ refreshToken: creds.refreshToken }
|
|
5308
|
-
);
|
|
5309
|
-
creds.idToken = resp.data.idToken;
|
|
5310
|
-
creds.refreshToken = resp.data.refreshToken ?? creds.refreshToken;
|
|
5311
|
-
creds.idTokenExpiry = Date.now() + (resp.data.expiresIn || 3600) * 1e3;
|
|
5312
|
-
saveCredentials(creds);
|
|
5313
|
-
return creds.idToken;
|
|
5314
|
-
}
|
|
5513
|
+
var import_picocolors5 = __toESM(require_picocolors(), 1);
|
|
5514
|
+
init_credentials();
|
|
5315
5515
|
|
|
5316
|
-
// src/cli/
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
method = await promptSelect({
|
|
5353
|
-
message: "How would you like to sign in?",
|
|
5354
|
-
options: [
|
|
5355
|
-
{ value: "email", label: "Email & password" },
|
|
5356
|
-
{ value: "phone", label: "Phone number (SMS)" }
|
|
5357
|
-
]
|
|
5358
|
-
});
|
|
5359
|
-
}
|
|
5360
|
-
const s = p.spinner();
|
|
5361
|
-
try {
|
|
5362
|
-
let result;
|
|
5363
|
-
if (method === "phone") {
|
|
5364
|
-
const { authenticateWithPhone: authenticateWithPhone2 } = await Promise.resolve().then(() => (init_phone_login(), phone_login_exports));
|
|
5365
|
-
result = await authenticateWithPhone2(s);
|
|
5366
|
-
} else {
|
|
5367
|
-
result = await authenticateWithEmail(s);
|
|
5368
|
-
}
|
|
5369
|
-
saveCredentials({
|
|
5370
|
-
refreshToken: result.refreshToken,
|
|
5371
|
-
uid: result.uid,
|
|
5372
|
-
email: result.displayName,
|
|
5373
|
-
idToken: result.idToken,
|
|
5374
|
-
idTokenExpiry: result.idTokenExpiry
|
|
5375
|
-
});
|
|
5376
|
-
s.stop(`Logged in as ${import_picocolors2.default.green(result.displayName)}`);
|
|
5377
|
-
p.outro("You are ready to go!");
|
|
5378
|
-
printSuccess(result.displayName);
|
|
5379
|
-
} catch (err) {
|
|
5380
|
-
const classified = err instanceof CliError ? err : classifyError(err);
|
|
5381
|
-
s.stop(import_picocolors2.default.red("Login failed"));
|
|
5382
|
-
p.log.error(classified.message);
|
|
5383
|
-
if (classified.options.hint) p.log.warning(classified.options.hint);
|
|
5384
|
-
process.exit(classified.exitCode);
|
|
5516
|
+
// src/cli/lib/command-map.ts
|
|
5517
|
+
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
5518
|
+
function collectCommands(root) {
|
|
5519
|
+
const out = [];
|
|
5520
|
+
function walk(cmd, prefix) {
|
|
5521
|
+
for (const sub of cmd.commands) {
|
|
5522
|
+
const fullName = prefix ? `${prefix} ${sub.name()}` : sub.name();
|
|
5523
|
+
if (sub.commands.length > 0) {
|
|
5524
|
+
walk(sub, fullName);
|
|
5525
|
+
} else {
|
|
5526
|
+
out.push({
|
|
5527
|
+
name: fullName,
|
|
5528
|
+
description: sub.description(),
|
|
5529
|
+
options: sub.options.map((o) => o.flags)
|
|
5530
|
+
});
|
|
5531
|
+
}
|
|
5532
|
+
}
|
|
5533
|
+
}
|
|
5534
|
+
walk(root, "");
|
|
5535
|
+
return out;
|
|
5536
|
+
}
|
|
5537
|
+
var FOCUS_GROUPS = /* @__PURE__ */ new Set(["tasks", "posts", "profile", "doctor"]);
|
|
5538
|
+
function focusCommands(commands) {
|
|
5539
|
+
return commands.filter((c) => FOCUS_GROUPS.has(c.name.split(" ")[0]));
|
|
5540
|
+
}
|
|
5541
|
+
function formatCommandMap(commands) {
|
|
5542
|
+
const lines = [];
|
|
5543
|
+
let lastGroup = "";
|
|
5544
|
+
for (const cmd of commands) {
|
|
5545
|
+
const group = cmd.name.split(" ")[0];
|
|
5546
|
+
if (group !== lastGroup) {
|
|
5547
|
+
if (lastGroup) lines.push("");
|
|
5548
|
+
lines.push(` ${import_picocolors.default.bold(group.charAt(0).toUpperCase() + group.slice(1) + ":")}`);
|
|
5549
|
+
lastGroup = group;
|
|
5550
|
+
}
|
|
5551
|
+
lines.push(` numo ${cmd.name.padEnd(30)} ${import_picocolors.default.dim(cmd.description)}`);
|
|
5385
5552
|
}
|
|
5553
|
+
return lines.join("\n");
|
|
5386
5554
|
}
|
|
5387
5555
|
|
|
5388
|
-
// src/cli/auth/
|
|
5389
|
-
init_http();
|
|
5390
|
-
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
5556
|
+
// src/cli/auth/login.ts
|
|
5391
5557
|
init_prompts();
|
|
5392
5558
|
init_errors();
|
|
5559
|
+
init_api_client();
|
|
5560
|
+
init_api_base();
|
|
5561
|
+
|
|
5562
|
+
// src/cli/lib/quiet.ts
|
|
5393
5563
|
init_tty();
|
|
5394
|
-
var
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
throw Errors.invalidInput("Invalid email address");
|
|
5399
|
-
}
|
|
5400
|
-
return trimmed;
|
|
5401
|
-
}
|
|
5402
|
-
function validatePassword(password) {
|
|
5403
|
-
if (password.length < 6) {
|
|
5404
|
-
throw Errors.invalidInput("Password is too weak (min 6 characters)");
|
|
5405
|
-
}
|
|
5406
|
-
return password;
|
|
5407
|
-
}
|
|
5408
|
-
function classifySignUpError(err) {
|
|
5409
|
-
if (err instanceof CliError) return err;
|
|
5410
|
-
const resp = err?.response?.data?.error;
|
|
5411
|
-
const kind = resp?.kind ?? "";
|
|
5412
|
-
const msg = resp?.message ?? "";
|
|
5413
|
-
if (kind === "CONFLICT" || msg.includes("already in use")) {
|
|
5414
|
-
return Errors.invalidInput(
|
|
5415
|
-
"Email already in use",
|
|
5416
|
-
"Already have an account? Run: numo login"
|
|
5417
|
-
);
|
|
5418
|
-
}
|
|
5419
|
-
if (msg.includes("Invalid email")) {
|
|
5420
|
-
return Errors.invalidInput("Invalid email address");
|
|
5421
|
-
}
|
|
5422
|
-
if (msg.includes("Password too weak") || msg.includes("min 6")) {
|
|
5423
|
-
return Errors.invalidInput("Password is too weak (min 6 characters)");
|
|
5424
|
-
}
|
|
5425
|
-
return classifyError(err);
|
|
5426
|
-
}
|
|
5427
|
-
async function signUp(email, password) {
|
|
5428
|
-
try {
|
|
5429
|
-
const resp = await http.post(`${API_BASE3}/api/auth/register`, {
|
|
5430
|
-
email,
|
|
5431
|
-
password,
|
|
5432
|
-
tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
5433
|
-
tzOffset: (/* @__PURE__ */ new Date()).getTimezoneOffset()
|
|
5434
|
-
});
|
|
5435
|
-
return {
|
|
5436
|
-
refreshToken: resp.data.refreshToken,
|
|
5437
|
-
uid: resp.data.uid,
|
|
5438
|
-
displayName: resp.data.email ?? email,
|
|
5439
|
-
idToken: resp.data.idToken,
|
|
5440
|
-
idTokenExpiry: Date.now() + (resp.data.expiresIn || 3600) * 1e3
|
|
5441
|
-
};
|
|
5442
|
-
} catch (err) {
|
|
5443
|
-
throw classifySignUpError(err);
|
|
5564
|
+
var noopSpinner = {
|
|
5565
|
+
start: () => {
|
|
5566
|
+
},
|
|
5567
|
+
stop: () => {
|
|
5444
5568
|
}
|
|
5569
|
+
};
|
|
5570
|
+
function isQuietMode(opts = {}) {
|
|
5571
|
+
return !!(opts.quiet || opts.json || !isInteractive());
|
|
5445
5572
|
}
|
|
5446
|
-
async function
|
|
5573
|
+
async function makeClackSpinner(quietMode) {
|
|
5574
|
+
if (quietMode) return noopSpinner;
|
|
5447
5575
|
const p = await Promise.resolve().then(() => (init_dist2(), dist_exports));
|
|
5448
|
-
p.
|
|
5449
|
-
const s = p.spinner();
|
|
5450
|
-
let spinnerActive = false;
|
|
5451
|
-
try {
|
|
5452
|
-
const email = validateEmail(
|
|
5453
|
-
options.email ?? await promptText({ message: "Email", required: true })
|
|
5454
|
-
);
|
|
5455
|
-
const rawPassword = options.password ?? await promptPassword({ message: "Password" });
|
|
5456
|
-
if (!options.password && isInteractive()) {
|
|
5457
|
-
const confirm = await promptPassword({ message: "Confirm password" });
|
|
5458
|
-
if (rawPassword !== confirm) {
|
|
5459
|
-
throw Errors.invalidInput("Passwords do not match");
|
|
5460
|
-
}
|
|
5461
|
-
}
|
|
5462
|
-
const password = validatePassword(rawPassword);
|
|
5463
|
-
s.start("Creating account...");
|
|
5464
|
-
spinnerActive = true;
|
|
5465
|
-
const result = await signUp(email, password);
|
|
5466
|
-
saveCredentials({
|
|
5467
|
-
refreshToken: result.refreshToken,
|
|
5468
|
-
uid: result.uid,
|
|
5469
|
-
email: result.displayName,
|
|
5470
|
-
idToken: result.idToken,
|
|
5471
|
-
idTokenExpiry: result.idTokenExpiry
|
|
5472
|
-
});
|
|
5473
|
-
s.stop(`Registered as ${import_picocolors3.default.green(result.displayName)}`);
|
|
5474
|
-
p.outro("Welcome to Numo!");
|
|
5475
|
-
printSuccess(result.displayName);
|
|
5476
|
-
} catch (err) {
|
|
5477
|
-
const classified = err instanceof CliError ? err : classifySignUpError(err);
|
|
5478
|
-
if (spinnerActive) s.stop(import_picocolors3.default.red("Registration failed"));
|
|
5479
|
-
p.log.error(classified.message);
|
|
5480
|
-
if (classified.options.hint) p.log.warning(classified.options.hint);
|
|
5481
|
-
process.exit(classified.exitCode);
|
|
5482
|
-
}
|
|
5576
|
+
return p.spinner();
|
|
5483
5577
|
}
|
|
5484
5578
|
|
|
5485
|
-
// src/cli/commands/tasks.ts
|
|
5486
|
-
var import_picocolors8 = __toESM(require_picocolors(), 1);
|
|
5487
|
-
|
|
5488
|
-
// src/cli/lib/actions.ts
|
|
5489
|
-
init_tty();
|
|
5490
|
-
|
|
5491
5579
|
// src/cli/lib/output.ts
|
|
5492
|
-
var
|
|
5580
|
+
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
5493
5581
|
init_tty();
|
|
5494
5582
|
|
|
5495
5583
|
// src/cli/lib/table.ts
|
|
5496
|
-
var
|
|
5584
|
+
var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
5497
5585
|
init_tty();
|
|
5498
5586
|
var BOX = isUnicodeSupported ? {
|
|
5499
5587
|
topLeft: String.fromCodePoint(9484),
|
|
@@ -5532,17 +5620,17 @@ var BOX = isUnicodeSupported ? {
|
|
|
5532
5620
|
cross: "+"
|
|
5533
5621
|
};
|
|
5534
5622
|
function renderTable(headers, rows, emptyMessage = "(no results)") {
|
|
5535
|
-
if (rows.length === 0) return
|
|
5623
|
+
if (rows.length === 0) return import_picocolors2.default.dim(emptyMessage);
|
|
5536
5624
|
const widths = headers.map(
|
|
5537
5625
|
(h2, i) => Math.max(h2.length, ...rows.map((r) => (r[i] ?? "").length))
|
|
5538
5626
|
);
|
|
5539
|
-
const
|
|
5627
|
+
const pad2 = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
|
|
5540
5628
|
const topBorder = BOX.topLeft + widths.map((w) => BOX.horizontal.repeat(w + 2)).join(BOX.midTop) + BOX.topRight;
|
|
5541
5629
|
const midBorder = BOX.midLeft + widths.map((w) => BOX.horizontal.repeat(w + 2)).join(BOX.cross) + BOX.midRight;
|
|
5542
5630
|
const bottomBorder = BOX.bottomLeft + widths.map((w) => BOX.horizontal.repeat(w + 2)).join(BOX.midBottom) + BOX.bottomRight;
|
|
5543
5631
|
const formatRow = (cells, bold = false) => BOX.vertical + cells.map((c, i) => {
|
|
5544
|
-
const padded =
|
|
5545
|
-
return ` ${bold ?
|
|
5632
|
+
const padded = pad2(c, widths[i]);
|
|
5633
|
+
return ` ${bold ? import_picocolors2.default.bold(padded) : padded} `;
|
|
5546
5634
|
}).join(BOX.vertical) + BOX.vertical;
|
|
5547
5635
|
const lines = [
|
|
5548
5636
|
topBorder,
|
|
@@ -5576,17 +5664,15 @@ function printRecord(fields) {
|
|
|
5576
5664
|
const visible = fields.filter(([, v]) => v != null && v !== "");
|
|
5577
5665
|
const maxLabel = Math.max(...visible.map(([l]) => l.length));
|
|
5578
5666
|
for (const [label, value] of visible) {
|
|
5579
|
-
console.log(` ${
|
|
5667
|
+
console.log(` ${import_picocolors3.default.bold(label.padEnd(maxLabel))} ${value}`);
|
|
5580
5668
|
}
|
|
5581
5669
|
}
|
|
5582
5670
|
function outputResult(data, asJson) {
|
|
5583
|
-
if (asJson) {
|
|
5584
|
-
printJson(data);
|
|
5585
|
-
} else if (typeof data === "string") {
|
|
5671
|
+
if (!asJson && typeof data === "string") {
|
|
5586
5672
|
console.log(data);
|
|
5587
|
-
|
|
5588
|
-
printJson(data);
|
|
5673
|
+
return;
|
|
5589
5674
|
}
|
|
5675
|
+
printJson(data);
|
|
5590
5676
|
}
|
|
5591
5677
|
function pickFields(obj, fields) {
|
|
5592
5678
|
const result = {};
|
|
@@ -5621,148 +5707,145 @@ function outputError(err, asJson) {
|
|
|
5621
5707
|
console.error(JSON.stringify(structured.toJSON(), null, 2));
|
|
5622
5708
|
} else {
|
|
5623
5709
|
console.error(`
|
|
5624
|
-
${
|
|
5710
|
+
${import_picocolors3.default.red("Error")}: ${structured.message}`);
|
|
5625
5711
|
if (structured.options.suggestion) {
|
|
5626
5712
|
console.error(`
|
|
5627
|
-
${
|
|
5713
|
+
${import_picocolors3.default.dim("Fix:")} ${import_picocolors3.default.cyan("$")} ${import_picocolors3.default.bold(structured.options.suggestion)}`);
|
|
5628
5714
|
}
|
|
5629
5715
|
if (structured.options.hint) {
|
|
5630
|
-
console.error(` ${
|
|
5716
|
+
console.error(` ${import_picocolors3.default.dim(structured.options.hint)}`);
|
|
5631
5717
|
}
|
|
5632
5718
|
console.error("");
|
|
5633
5719
|
}
|
|
5634
5720
|
process.exit(structured.exitCode);
|
|
5635
5721
|
}
|
|
5636
5722
|
|
|
5637
|
-
// src/cli/
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
// src/cli/lib/symbols.ts
|
|
5642
|
-
init_tty();
|
|
5643
|
-
var u = isUnicodeSupported;
|
|
5644
|
-
var SYM = {
|
|
5645
|
-
check: u ? "\u2714" : "v",
|
|
5646
|
-
// ✓
|
|
5647
|
-
cross: u ? "\u2718" : "x",
|
|
5648
|
-
// ✗
|
|
5649
|
-
circle: u ? "\u25CB" : "o",
|
|
5650
|
-
// ○
|
|
5651
|
-
repeat: u ? "\u21BB" : "R",
|
|
5652
|
-
// ↻
|
|
5653
|
-
undo: u ? "\u21A9" : "<-",
|
|
5654
|
-
// ↩
|
|
5655
|
-
fire: u ? "\u{1F525}" : "*",
|
|
5656
|
-
// 🔥
|
|
5657
|
-
ellipsis: u ? "\u2026" : "...",
|
|
5658
|
-
// …
|
|
5659
|
-
dash: u ? "\u2500" : "-",
|
|
5660
|
-
// ─
|
|
5661
|
-
bullet: u ? "\u2022" : "*",
|
|
5662
|
-
// •
|
|
5663
|
-
arrow: u ? "\u2192" : "->"
|
|
5664
|
-
// →
|
|
5665
|
-
};
|
|
5666
|
-
|
|
5667
|
-
// src/cli/lib/spinner.ts
|
|
5668
|
-
var FRAMES = isUnicodeSupported ? [
|
|
5669
|
-
String.fromCodePoint(10251),
|
|
5670
|
-
String.fromCodePoint(10265),
|
|
5671
|
-
String.fromCodePoint(10297),
|
|
5672
|
-
String.fromCodePoint(10296),
|
|
5673
|
-
String.fromCodePoint(10300),
|
|
5674
|
-
String.fromCodePoint(10292),
|
|
5675
|
-
String.fromCodePoint(10278),
|
|
5676
|
-
String.fromCodePoint(10279),
|
|
5677
|
-
String.fromCodePoint(10247),
|
|
5678
|
-
String.fromCodePoint(10255)
|
|
5679
|
-
] : ["-", "\\", "|", "/"];
|
|
5680
|
-
var INTERVAL = 80;
|
|
5681
|
-
var MAX_RETRIES2 = 3;
|
|
5682
|
-
function createSpinner(interactive) {
|
|
5683
|
-
if (!interactive) {
|
|
5684
|
-
return { start() {
|
|
5685
|
-
}, update() {
|
|
5686
|
-
}, stop() {
|
|
5687
|
-
}, fail() {
|
|
5688
|
-
} };
|
|
5689
|
-
}
|
|
5690
|
-
let timer = null;
|
|
5691
|
-
let frameIdx = 0;
|
|
5692
|
-
let currentMsg = "";
|
|
5693
|
-
function clear() {
|
|
5694
|
-
process.stderr.write("\r\x1B[K");
|
|
5695
|
-
}
|
|
5723
|
+
// src/cli/auth/login.ts
|
|
5724
|
+
async function postLogin(email, password) {
|
|
5725
|
+
assertSafeApiBase();
|
|
5726
|
+
const resp = await http.post(`${API_BASE}/api/auth/login`, { email, password });
|
|
5696
5727
|
return {
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
timer = setInterval(() => {
|
|
5703
|
-
frameIdx = (frameIdx + 1) % FRAMES.length;
|
|
5704
|
-
clear();
|
|
5705
|
-
process.stderr.write(`${import_picocolors6.default.cyan(FRAMES[frameIdx])} ${currentMsg}`);
|
|
5706
|
-
}, INTERVAL);
|
|
5707
|
-
},
|
|
5708
|
-
update(msg) {
|
|
5709
|
-
currentMsg = msg;
|
|
5710
|
-
},
|
|
5711
|
-
stop(msg) {
|
|
5712
|
-
if (timer) {
|
|
5713
|
-
clearInterval(timer);
|
|
5714
|
-
timer = null;
|
|
5715
|
-
}
|
|
5716
|
-
clear();
|
|
5717
|
-
process.stderr.write(`${import_picocolors6.default.green(SYM.check)} ${msg ?? currentMsg}
|
|
5718
|
-
`);
|
|
5719
|
-
},
|
|
5720
|
-
fail(msg) {
|
|
5721
|
-
if (timer) {
|
|
5722
|
-
clearInterval(timer);
|
|
5723
|
-
timer = null;
|
|
5724
|
-
}
|
|
5725
|
-
clear();
|
|
5726
|
-
process.stderr.write(`${import_picocolors6.default.red(SYM.cross)} ${msg}
|
|
5727
|
-
`);
|
|
5728
|
-
}
|
|
5728
|
+
refreshToken: resp.data.refreshToken,
|
|
5729
|
+
uid: resp.data.uid,
|
|
5730
|
+
displayName: resp.data.email ?? email,
|
|
5731
|
+
idToken: resp.data.idToken,
|
|
5732
|
+
idTokenExpiry: Date.now() + (resp.data.expiresIn || 3600) * 1e3
|
|
5729
5733
|
};
|
|
5730
5734
|
}
|
|
5731
|
-
function
|
|
5732
|
-
|
|
5735
|
+
async function authenticateInteractive(spinner) {
|
|
5736
|
+
const email = await promptText({ message: "Email", required: true });
|
|
5737
|
+
const password = await promptPassword({ message: "Password" });
|
|
5738
|
+
spinner.start("Signing in...");
|
|
5739
|
+
return postLogin(email, password);
|
|
5740
|
+
}
|
|
5741
|
+
function printSuccess(root) {
|
|
5742
|
+
const body = root ? formatCommandMap(focusCommands(collectCommands(root))) : ` ${import_picocolors5.default.dim("Run")} numo commands ${import_picocolors5.default.dim("to list every command.")}`;
|
|
5743
|
+
console.log(`
|
|
5744
|
+
${import_picocolors5.default.bold("Available commands:")}
|
|
5745
|
+
${body}`);
|
|
5746
|
+
console.log(` ${import_picocolors5.default.dim("Run")} numo commands ${import_picocolors5.default.dim("to see all commands.")}
|
|
5747
|
+
`);
|
|
5748
|
+
}
|
|
5749
|
+
async function login(options = {}, root) {
|
|
5750
|
+
const envEmail = process.env.NUMO_LOGIN_EMAIL;
|
|
5751
|
+
const envPassword = process.env.NUMO_LOGIN_PASSWORD;
|
|
5752
|
+
const hasEnvCreds = !!(envEmail && envPassword);
|
|
5753
|
+
const quietMode = isQuietMode(options);
|
|
5754
|
+
if (quietMode && options.phone) {
|
|
5755
|
+
outputError(
|
|
5756
|
+
Errors.invalidInput(
|
|
5757
|
+
"--phone requires an interactive terminal for SMS OTP entry",
|
|
5758
|
+
"Use NUMO_LOGIN_EMAIL + NUMO_LOGIN_PASSWORD env vars for non-interactive login."
|
|
5759
|
+
),
|
|
5760
|
+
true
|
|
5761
|
+
);
|
|
5762
|
+
}
|
|
5763
|
+
if (quietMode && !hasEnvCreds && !options.phone) {
|
|
5764
|
+
outputError(
|
|
5765
|
+
Errors.configMissing("NUMO_LOGIN_EMAIL and NUMO_LOGIN_PASSWORD"),
|
|
5766
|
+
true
|
|
5767
|
+
);
|
|
5768
|
+
}
|
|
5769
|
+
const p = await Promise.resolve().then(() => (init_dist2(), dist_exports));
|
|
5770
|
+
if (!quietMode) p.intro(import_picocolors5.default.bold("Numo \u2014 Login"));
|
|
5771
|
+
let method = options.phone ? "phone" : "email";
|
|
5772
|
+
if (!options.phone && !hasEnvCreds && !quietMode) {
|
|
5773
|
+
method = await promptSelect({
|
|
5774
|
+
message: "How would you like to sign in?",
|
|
5775
|
+
options: [
|
|
5776
|
+
{ value: "email", label: "Email & password" },
|
|
5777
|
+
{ value: "phone", label: "Phone number (SMS)" }
|
|
5778
|
+
]
|
|
5779
|
+
});
|
|
5780
|
+
}
|
|
5781
|
+
const s = await makeClackSpinner(quietMode);
|
|
5782
|
+
try {
|
|
5783
|
+
let result;
|
|
5784
|
+
if (method === "phone") {
|
|
5785
|
+
const { authenticateWithPhone: authenticateWithPhone2 } = await Promise.resolve().then(() => (init_phone_login(), phone_login_exports));
|
|
5786
|
+
result = await authenticateWithPhone2(s);
|
|
5787
|
+
} else if (hasEnvCreds) {
|
|
5788
|
+
s.start("Signing in...");
|
|
5789
|
+
result = await postLogin(envEmail, envPassword);
|
|
5790
|
+
} else {
|
|
5791
|
+
result = await authenticateInteractive(s);
|
|
5792
|
+
}
|
|
5793
|
+
saveCredentials({
|
|
5794
|
+
refreshToken: result.refreshToken,
|
|
5795
|
+
uid: result.uid,
|
|
5796
|
+
email: result.displayName,
|
|
5797
|
+
idToken: result.idToken,
|
|
5798
|
+
idTokenExpiry: result.idTokenExpiry
|
|
5799
|
+
});
|
|
5800
|
+
if (quietMode) {
|
|
5801
|
+
printJson({
|
|
5802
|
+
ok: true,
|
|
5803
|
+
uid: result.uid,
|
|
5804
|
+
email: result.displayName,
|
|
5805
|
+
idToken: result.idToken,
|
|
5806
|
+
idTokenExpiry: result.idTokenExpiry
|
|
5807
|
+
});
|
|
5808
|
+
return;
|
|
5809
|
+
}
|
|
5810
|
+
s.stop(`Logged in as ${import_picocolors5.default.green(result.displayName)}`);
|
|
5811
|
+
p.outro("You are ready to go!");
|
|
5812
|
+
printSuccess(root);
|
|
5813
|
+
} catch (err) {
|
|
5814
|
+
const classified = err instanceof CliError ? err : classifyError(err);
|
|
5815
|
+
if (quietMode) {
|
|
5816
|
+
outputError(classified, true);
|
|
5817
|
+
}
|
|
5818
|
+
s.stop(import_picocolors5.default.red("Login failed"));
|
|
5819
|
+
p.log.error(classified.message);
|
|
5820
|
+
if (classified.options.hint) p.log.warning(classified.options.hint);
|
|
5821
|
+
process.exit(classified.exitCode);
|
|
5822
|
+
}
|
|
5733
5823
|
}
|
|
5824
|
+
|
|
5825
|
+
// src/cli/cli.ts
|
|
5826
|
+
init_credentials();
|
|
5827
|
+
init_dirs();
|
|
5828
|
+
|
|
5829
|
+
// src/cli/commands/tasks.ts
|
|
5830
|
+
var import_picocolors7 = __toESM(require_picocolors(), 1);
|
|
5831
|
+
|
|
5832
|
+
// src/cli/lib/spinner.ts
|
|
5734
5833
|
async function withSpinner(interactive, message, fn) {
|
|
5735
|
-
const spinner =
|
|
5834
|
+
const spinner = await makeClackSpinner(!interactive);
|
|
5736
5835
|
spinner.start(message);
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
if (status === 429 && attempt < MAX_RETRIES2) {
|
|
5745
|
-
const retryAfter = err.response?.headers?.["retry-after"];
|
|
5746
|
-
const waitMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 1e3 * Math.pow(2, attempt);
|
|
5747
|
-
spinner.update(`Rate limited, retrying in ${Math.round(waitMs / 1e3)}s...`);
|
|
5748
|
-
await delay2(waitMs);
|
|
5749
|
-
spinner.update(message);
|
|
5750
|
-
continue;
|
|
5751
|
-
}
|
|
5752
|
-
spinner.fail(message);
|
|
5753
|
-
throw err;
|
|
5754
|
-
}
|
|
5836
|
+
try {
|
|
5837
|
+
const result = await fn();
|
|
5838
|
+
spinner.stop(message);
|
|
5839
|
+
return result;
|
|
5840
|
+
} catch (err) {
|
|
5841
|
+
spinner.stop(message, 2);
|
|
5842
|
+
throw err;
|
|
5755
5843
|
}
|
|
5756
|
-
throw new Error("Max retries exceeded");
|
|
5757
5844
|
}
|
|
5758
5845
|
|
|
5759
5846
|
// src/cli/lib/actions.ts
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
}
|
|
5763
|
-
function useSpinner(opts) {
|
|
5764
|
-
return isInteractive() && !opts.quiet;
|
|
5765
|
-
}
|
|
5847
|
+
var useJson = isQuietMode;
|
|
5848
|
+
var useSpinner = (opts) => !isQuietMode(opts);
|
|
5766
5849
|
async function runGet(opts) {
|
|
5767
5850
|
try {
|
|
5768
5851
|
const result = await withSpinner(
|
|
@@ -5848,157 +5931,81 @@ async function runWrite(opts) {
|
|
|
5848
5931
|
outputError(err, useJson(opts.global));
|
|
5849
5932
|
}
|
|
5850
5933
|
}
|
|
5851
|
-
async function runDelete(opts) {
|
|
5852
|
-
try {
|
|
5853
|
-
await withSpinner(
|
|
5854
|
-
useSpinner(opts.global),
|
|
5855
|
-
opts.spinnerMessage ?? "Deleting...",
|
|
5856
|
-
opts.fn
|
|
5857
|
-
);
|
|
5858
|
-
if (!opts.global.quiet) {
|
|
5859
|
-
console.log(opts.successMessage);
|
|
5860
|
-
}
|
|
5861
|
-
} catch (err) {
|
|
5862
|
-
outputError(err, useJson(opts.global));
|
|
5863
|
-
}
|
|
5864
|
-
}
|
|
5865
5934
|
|
|
5866
5935
|
// src/cli/lib/uid.ts
|
|
5936
|
+
init_credentials();
|
|
5867
5937
|
init_errors();
|
|
5938
|
+
function uidFromToken(token) {
|
|
5939
|
+
try {
|
|
5940
|
+
const payload = JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64").toString("utf8"));
|
|
5941
|
+
if (typeof payload.user_id === "string") return payload.user_id;
|
|
5942
|
+
if (typeof payload.sub === "string") return payload.sub;
|
|
5943
|
+
return null;
|
|
5944
|
+
} catch {
|
|
5945
|
+
return null;
|
|
5946
|
+
}
|
|
5947
|
+
}
|
|
5868
5948
|
function requireUid() {
|
|
5949
|
+
if (process.env.NUMO_TOKEN) return uidFromToken(process.env.NUMO_TOKEN) ?? "";
|
|
5869
5950
|
const creds = loadCredentials();
|
|
5870
5951
|
if (!creds) throw Errors.authRequired();
|
|
5871
5952
|
return creds.uid;
|
|
5872
5953
|
}
|
|
5873
5954
|
|
|
5874
|
-
// src/cli/lib/api-client.ts
|
|
5875
|
-
init_http();
|
|
5876
|
-
init_errors();
|
|
5877
|
-
var API_BASE4 = process.env.NUMO_API_URL ?? "http://localhost:3000";
|
|
5878
|
-
if (API_BASE4 !== "http://localhost:3000" && API_BASE4.startsWith("http://")) {
|
|
5879
|
-
process.stderr.write("[warn] NUMO_API_URL uses HTTP \u2014 tokens sent unencrypted. Use HTTPS in production.\n");
|
|
5880
|
-
}
|
|
5881
|
-
var KIND_EXIT = {
|
|
5882
|
-
AUTH_REQUIRED: ExitCode.NO_PERM,
|
|
5883
|
-
AUTH_EXPIRED: ExitCode.NO_PERM,
|
|
5884
|
-
AUTH_FORBIDDEN: ExitCode.NO_PERM,
|
|
5885
|
-
INVALID_INPUT: ExitCode.USAGE,
|
|
5886
|
-
MISSING_ARGUMENT: ExitCode.USAGE,
|
|
5887
|
-
NOT_FOUND: ExitCode.NOT_FOUND,
|
|
5888
|
-
CONFLICT: ExitCode.CONFLICT,
|
|
5889
|
-
RATE_LIMITED: ExitCode.TEMP_FAIL,
|
|
5890
|
-
NETWORK_ERROR: ExitCode.UNAVAILABLE,
|
|
5891
|
-
TIMEOUT: ExitCode.TEMP_FAIL,
|
|
5892
|
-
SERVICE_UNAVAILABLE: ExitCode.UNAVAILABLE
|
|
5893
|
-
};
|
|
5894
|
-
function toCliError(err) {
|
|
5895
|
-
if (err instanceof CliError) return err;
|
|
5896
|
-
const httpErr = err;
|
|
5897
|
-
if (httpErr.code === "ECONNABORTED" || httpErr.code === "ETIMEDOUT") {
|
|
5898
|
-
return new CliError("TIMEOUT" /* TIMEOUT */, "Request timed out", ExitCode.TEMP_FAIL, {
|
|
5899
|
-
hint: "The API server took too long to respond.",
|
|
5900
|
-
retryable: true
|
|
5901
|
-
});
|
|
5902
|
-
}
|
|
5903
|
-
if (httpErr.code === "ECONNREFUSED" || httpErr.code === "ECONNRESET" || httpErr.code === "ENOTFOUND") {
|
|
5904
|
-
return new CliError("NETWORK_ERROR" /* NETWORK_ERROR */, "Can't reach Numo API", ExitCode.UNAVAILABLE, {
|
|
5905
|
-
hint: "Is the API server running? Check NUMO_API_URL.",
|
|
5906
|
-
retryable: true
|
|
5907
|
-
});
|
|
5908
|
-
}
|
|
5909
|
-
const body = httpErr.response?.data;
|
|
5910
|
-
if (body?.error) {
|
|
5911
|
-
const e = body.error;
|
|
5912
|
-
const kind = e.kind ?? "UNKNOWN" /* UNKNOWN */;
|
|
5913
|
-
const exitCode = KIND_EXIT[kind] ?? ExitCode.GENERAL;
|
|
5914
|
-
return new CliError(kind, e.message ?? "Unknown error", exitCode, {
|
|
5915
|
-
retryable: e.retryable,
|
|
5916
|
-
retryAfter: e.retryAfter
|
|
5917
|
-
});
|
|
5918
|
-
}
|
|
5919
|
-
return new CliError("UNKNOWN" /* UNKNOWN */, httpErr.message ?? "Unknown error", ExitCode.GENERAL);
|
|
5920
|
-
}
|
|
5921
|
-
async function apiHeaders() {
|
|
5922
|
-
const token = await getIdToken();
|
|
5923
|
-
return {
|
|
5924
|
-
Authorization: `Bearer ${token}`,
|
|
5925
|
-
"Content-Type": "application/json"
|
|
5926
|
-
};
|
|
5927
|
-
}
|
|
5928
|
-
function url(path3, params) {
|
|
5929
|
-
const u2 = `${API_BASE4}${path3}`;
|
|
5930
|
-
if (!params) return u2;
|
|
5931
|
-
const sp = new URLSearchParams();
|
|
5932
|
-
for (const [k2, v] of Object.entries(params)) {
|
|
5933
|
-
if (v !== void 0) sp.set(k2, v);
|
|
5934
|
-
}
|
|
5935
|
-
const qs = sp.toString();
|
|
5936
|
-
return qs ? `${u2}?${qs}` : u2;
|
|
5937
|
-
}
|
|
5938
|
-
var api = {
|
|
5939
|
-
async get(path3, params) {
|
|
5940
|
-
try {
|
|
5941
|
-
const resp = await http.get(url(path3, params), { headers: await apiHeaders() });
|
|
5942
|
-
return resp.data;
|
|
5943
|
-
} catch (err) {
|
|
5944
|
-
throw toCliError(err);
|
|
5945
|
-
}
|
|
5946
|
-
},
|
|
5947
|
-
async post(path3, body) {
|
|
5948
|
-
try {
|
|
5949
|
-
const resp = await http.post(url(path3), body, { headers: await apiHeaders() });
|
|
5950
|
-
return resp.data;
|
|
5951
|
-
} catch (err) {
|
|
5952
|
-
throw toCliError(err);
|
|
5953
|
-
}
|
|
5954
|
-
},
|
|
5955
|
-
async patch(path3, body) {
|
|
5956
|
-
try {
|
|
5957
|
-
const resp = await http.patch(url(path3), body, { headers: await apiHeaders() });
|
|
5958
|
-
return resp.data;
|
|
5959
|
-
} catch (err) {
|
|
5960
|
-
throw toCliError(err);
|
|
5961
|
-
}
|
|
5962
|
-
},
|
|
5963
|
-
async del(path3) {
|
|
5964
|
-
try {
|
|
5965
|
-
const resp = await http.delete(url(path3), { headers: await apiHeaders() });
|
|
5966
|
-
return resp.data;
|
|
5967
|
-
} catch (err) {
|
|
5968
|
-
throw toCliError(err);
|
|
5969
|
-
}
|
|
5970
|
-
}
|
|
5971
|
-
};
|
|
5972
|
-
|
|
5973
5955
|
// src/cli/services/tasks.ts
|
|
5974
|
-
|
|
5956
|
+
init_api_client();
|
|
5957
|
+
async function listTasks(opts) {
|
|
5975
5958
|
return api.get("/api/tasks", {
|
|
5976
5959
|
date: opts.date,
|
|
5977
5960
|
backlog: opts.backlog ? "true" : void 0,
|
|
5978
5961
|
tag: opts.tag
|
|
5979
5962
|
});
|
|
5980
5963
|
}
|
|
5981
|
-
async function getTask(
|
|
5964
|
+
async function getTask(id) {
|
|
5982
5965
|
return api.get(`/api/tasks/${encodeURIComponent(id)}`);
|
|
5983
5966
|
}
|
|
5984
|
-
async function createTask(
|
|
5967
|
+
async function createTask(body) {
|
|
5985
5968
|
return api.post("/api/tasks", body);
|
|
5986
5969
|
}
|
|
5987
|
-
async function updateTask(
|
|
5970
|
+
async function updateTask(id, body) {
|
|
5988
5971
|
return api.patch(`/api/tasks/${encodeURIComponent(id)}`, body);
|
|
5989
5972
|
}
|
|
5990
|
-
async function deleteTask(
|
|
5973
|
+
async function deleteTask(id) {
|
|
5991
5974
|
return api.del(`/api/tasks/${encodeURIComponent(id)}`);
|
|
5992
5975
|
}
|
|
5993
|
-
async function completeTask(
|
|
5976
|
+
async function completeTask(id, date) {
|
|
5994
5977
|
return api.post(`/api/tasks/${encodeURIComponent(id)}/complete`, date ? { date } : void 0);
|
|
5995
5978
|
}
|
|
5996
|
-
async function uncompleteTask(
|
|
5979
|
+
async function uncompleteTask(taskHistoryId) {
|
|
5997
5980
|
return api.post(`/api/tasks/${encodeURIComponent(taskHistoryId)}/uncomplete`);
|
|
5998
5981
|
}
|
|
5999
5982
|
|
|
6000
5983
|
// src/cli/lib/format.ts
|
|
6001
|
-
var
|
|
5984
|
+
var import_picocolors6 = __toESM(require_picocolors(), 1);
|
|
5985
|
+
|
|
5986
|
+
// src/cli/lib/symbols.ts
|
|
5987
|
+
init_tty();
|
|
5988
|
+
var u = isUnicodeSupported;
|
|
5989
|
+
var SYM = {
|
|
5990
|
+
check: u ? "\u2714" : "v",
|
|
5991
|
+
// ✓
|
|
5992
|
+
cross: u ? "\u2718" : "x",
|
|
5993
|
+
// ✗
|
|
5994
|
+
circle: u ? "\u25CB" : "o",
|
|
5995
|
+
// ○
|
|
5996
|
+
repeat: u ? "\u21BB" : "R",
|
|
5997
|
+
// ↻
|
|
5998
|
+
undo: u ? "\u21A9" : "<-",
|
|
5999
|
+
// ↩
|
|
6000
|
+
fire: u ? "\u{1F525}" : "*",
|
|
6001
|
+
// 🔥
|
|
6002
|
+
ellipsis: u ? "\u2026" : "...",
|
|
6003
|
+
// …
|
|
6004
|
+
dash: u ? "\u2500" : "-"
|
|
6005
|
+
// ─
|
|
6006
|
+
};
|
|
6007
|
+
|
|
6008
|
+
// src/cli/lib/format.ts
|
|
6002
6009
|
function formatDate(ts) {
|
|
6003
6010
|
if (ts == null) return "";
|
|
6004
6011
|
const d = typeof ts === "number" ? new Date(ts) : new Date(ts);
|
|
@@ -6026,7 +6033,7 @@ function formatRelativeDate(ts) {
|
|
|
6026
6033
|
}
|
|
6027
6034
|
function formatTags(tags) {
|
|
6028
6035
|
if (!Array.isArray(tags) || tags.length === 0) return "";
|
|
6029
|
-
return tags.map((t2) =>
|
|
6036
|
+
return tags.map((t2) => import_picocolors6.default.cyan(`#${t2}`)).join(" ");
|
|
6030
6037
|
}
|
|
6031
6038
|
function formatDifficulty(d) {
|
|
6032
6039
|
if (d == null) return "";
|
|
@@ -6066,21 +6073,21 @@ function formatWeekdayHeader(date, streakCount) {
|
|
|
6066
6073
|
const cols = process.stdout.columns ?? 80;
|
|
6067
6074
|
const fire = SYM.fire;
|
|
6068
6075
|
const streakStr = streakCount && streakCount > 0 ? `${fire} ${streakCount}` : "";
|
|
6069
|
-
const
|
|
6070
|
-
const line1 = ` ${
|
|
6071
|
-
const line2 = ` ${
|
|
6076
|
+
const pad2 = Math.max(0, cols - weekday.length - streakStr.length - 4);
|
|
6077
|
+
const line1 = ` ${import_picocolors6.default.bold(weekday)}${" ".repeat(pad2)}${streakStr}`;
|
|
6078
|
+
const line2 = ` ${import_picocolors6.default.dim(fullDate)}`;
|
|
6072
6079
|
return `${line1}
|
|
6073
6080
|
${line2}`;
|
|
6074
6081
|
}
|
|
6075
6082
|
function formatKarmaGain(points, checksInRow) {
|
|
6076
|
-
const base =
|
|
6083
|
+
const base = import_picocolors6.default.green(`+${points} karma`);
|
|
6077
6084
|
if (checksInRow && checksInRow > 1) {
|
|
6078
|
-
return `${base} ${
|
|
6085
|
+
return `${base} ${import_picocolors6.default.yellow(`streak x${checksInRow}!`)}`;
|
|
6079
6086
|
}
|
|
6080
6087
|
return base;
|
|
6081
6088
|
}
|
|
6082
6089
|
function formatProgressSummary(completed, total) {
|
|
6083
|
-
return
|
|
6090
|
+
return import_picocolors6.default.dim(`${completed}/${total} done today`);
|
|
6084
6091
|
}
|
|
6085
6092
|
function formatTagsSummary(tasks) {
|
|
6086
6093
|
const counts = {};
|
|
@@ -6092,7 +6099,7 @@ function formatTagsSummary(tasks) {
|
|
|
6092
6099
|
}
|
|
6093
6100
|
const entries = Object.entries(counts);
|
|
6094
6101
|
if (entries.length === 0) return "";
|
|
6095
|
-
return entries.map(([tag, count]) => `${
|
|
6102
|
+
return entries.map(([tag, count]) => `${import_picocolors6.default.cyan("#" + tag)}${import_picocolors6.default.dim("(" + count + ")")}`).join(" ");
|
|
6096
6103
|
}
|
|
6097
6104
|
|
|
6098
6105
|
// src/cli/commands/tasks.ts
|
|
@@ -8966,6 +8973,99 @@ function parseHumanDateOnly(input) {
|
|
|
8966
8973
|
return result.slice(0, 10);
|
|
8967
8974
|
}
|
|
8968
8975
|
|
|
8976
|
+
// src/cli/lib/task-dates.ts
|
|
8977
|
+
function pad(n) {
|
|
8978
|
+
return String(n).padStart(2, "0");
|
|
8979
|
+
}
|
|
8980
|
+
function localDateOnly(d = /* @__PURE__ */ new Date()) {
|
|
8981
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
8982
|
+
}
|
|
8983
|
+
function localDateOffset(n, base = /* @__PURE__ */ new Date()) {
|
|
8984
|
+
const d = new Date(base);
|
|
8985
|
+
d.setDate(d.getDate() + n);
|
|
8986
|
+
return localDateOnly(d);
|
|
8987
|
+
}
|
|
8988
|
+
function toApiDueDate(s) {
|
|
8989
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(s) ? `${s} 00:00` : s.slice(0, 16);
|
|
8990
|
+
}
|
|
8991
|
+
function dueDateHasTime(dueDate) {
|
|
8992
|
+
if (!dueDate) return false;
|
|
8993
|
+
const m = dueDate.match(/ (\d{2}):(\d{2})$/);
|
|
8994
|
+
if (!m) return false;
|
|
8995
|
+
return !(m[1] === "00" && m[2] === "00");
|
|
8996
|
+
}
|
|
8997
|
+
function normalizeDueDateInBody(body) {
|
|
8998
|
+
if (typeof body.dueDate === "string") {
|
|
8999
|
+
const due = toApiDueDate(body.dueDate);
|
|
9000
|
+
body.dueDate = due;
|
|
9001
|
+
body.withTime = dueDateHasTime(due);
|
|
9002
|
+
}
|
|
9003
|
+
}
|
|
9004
|
+
function isCompletableDate(date, now2 = /* @__PURE__ */ new Date()) {
|
|
9005
|
+
const dateOnly = date.slice(0, 10);
|
|
9006
|
+
return dateOnly === localDateOnly(now2) || dateOnly === localDateOffset(-1, now2);
|
|
9007
|
+
}
|
|
9008
|
+
|
|
9009
|
+
// src/cli/lib/task-repeat.ts
|
|
9010
|
+
init_errors();
|
|
9011
|
+
var WEEKDAY_LOOKUP = {
|
|
9012
|
+
mon: "Mon",
|
|
9013
|
+
monday: "Mon",
|
|
9014
|
+
tue: "Tue",
|
|
9015
|
+
tues: "Tue",
|
|
9016
|
+
tuesday: "Tue",
|
|
9017
|
+
wed: "Wed",
|
|
9018
|
+
weds: "Wed",
|
|
9019
|
+
wednesday: "Wed",
|
|
9020
|
+
thu: "Thu",
|
|
9021
|
+
thur: "Thu",
|
|
9022
|
+
thurs: "Thu",
|
|
9023
|
+
thursday: "Thu",
|
|
9024
|
+
fri: "Fri",
|
|
9025
|
+
friday: "Fri",
|
|
9026
|
+
sat: "Sat",
|
|
9027
|
+
saturday: "Sat",
|
|
9028
|
+
sun: "Sun",
|
|
9029
|
+
sunday: "Sun"
|
|
9030
|
+
};
|
|
9031
|
+
var REPEAT_TYPES = ["daily", "weekly", "monthly", "none"];
|
|
9032
|
+
function normalizeWeekday(s) {
|
|
9033
|
+
const day = WEEKDAY_LOOKUP[s.trim().toLowerCase()];
|
|
9034
|
+
if (!day) throw Errors.invalidInput(`Invalid weekday: "${s}". Use Mon,Tue,Wed,Thu,Fri,Sat,Sun`);
|
|
9035
|
+
return day;
|
|
9036
|
+
}
|
|
9037
|
+
function buildRepeatConfig(opts) {
|
|
9038
|
+
if (opts.repeat === void 0) return void 0;
|
|
9039
|
+
const type = opts.repeat;
|
|
9040
|
+
if (!REPEAT_TYPES.includes(type)) {
|
|
9041
|
+
throw Errors.invalidInput(`--repeat must be one of: ${REPEAT_TYPES.join(", ")}`);
|
|
9042
|
+
}
|
|
9043
|
+
const every = opts.every !== void 0 ? parseInt(opts.every, 10) : 1;
|
|
9044
|
+
if (!Number.isFinite(every) || every < 1 || every > 365) {
|
|
9045
|
+
throw Errors.invalidInput("--every must be an integer 1-365");
|
|
9046
|
+
}
|
|
9047
|
+
const repeat = { type, every, custom: false, monthDays: null, weekDays: null };
|
|
9048
|
+
if (type === "weekly" && opts.weekdays) {
|
|
9049
|
+
repeat.weekDays = opts.weekdays.split(",").map((d) => normalizeWeekday(d));
|
|
9050
|
+
}
|
|
9051
|
+
if (type === "monthly" && opts.monthDays) {
|
|
9052
|
+
repeat.monthDays = opts.monthDays.split(",").map((s) => {
|
|
9053
|
+
const n = parseInt(s.trim(), 10);
|
|
9054
|
+
if (!Number.isFinite(n) || n < 1 || n > 31) {
|
|
9055
|
+
throw Errors.invalidInput(`--month-days must be 1-31 (got "${s.trim()}")`);
|
|
9056
|
+
}
|
|
9057
|
+
return n;
|
|
9058
|
+
});
|
|
9059
|
+
}
|
|
9060
|
+
return repeat;
|
|
9061
|
+
}
|
|
9062
|
+
|
|
9063
|
+
// src/cli/lib/task-subtasks.ts
|
|
9064
|
+
var import_node_crypto = require("node:crypto");
|
|
9065
|
+
function buildSubtasks(texts) {
|
|
9066
|
+
return texts.map((t2) => t2.trim()).filter((t2) => t2.length > 0).map((text) => ({ id: (0, import_node_crypto.randomUUID)(), text, completed: false }));
|
|
9067
|
+
}
|
|
9068
|
+
|
|
8969
9069
|
// src/cli/lib/stdin.ts
|
|
8970
9070
|
var fs3 = __toESM(require("fs"), 1);
|
|
8971
9071
|
function readStdinLines() {
|
|
@@ -8974,13 +9074,16 @@ function readStdinLines() {
|
|
|
8974
9074
|
}
|
|
8975
9075
|
|
|
8976
9076
|
// src/cli/commands/tasks.ts
|
|
8977
|
-
|
|
9077
|
+
function collectValue(value, previous) {
|
|
9078
|
+
return previous.concat([value]);
|
|
9079
|
+
}
|
|
9080
|
+
async function pickTask(id, actionName) {
|
|
8978
9081
|
if (id) return id;
|
|
8979
9082
|
if (!isInteractive()) {
|
|
8980
9083
|
throw Errors.missingArg("Task ID", "id");
|
|
8981
9084
|
}
|
|
8982
|
-
const today2 = (
|
|
8983
|
-
const { tasks } = await listTasks(
|
|
9085
|
+
const today2 = localDateOnly();
|
|
9086
|
+
const { tasks } = await listTasks({ date: today2 });
|
|
8984
9087
|
const pending = tasks.filter((t2) => !t2.completed);
|
|
8985
9088
|
if (pending.length === 0) {
|
|
8986
9089
|
throw Errors.invalidInput(`No pending tasks for today (${today2}). Use: numo tasks ${actionName} <id>`);
|
|
@@ -8989,23 +9092,21 @@ async function pickTask(uid, id, actionName) {
|
|
|
8989
9092
|
message: `Select task to ${actionName}`,
|
|
8990
9093
|
options: pending.map((t2) => ({
|
|
8991
9094
|
value: t2.id,
|
|
8992
|
-
label: `${truncate(t2.text, 50)} ${
|
|
9095
|
+
label: `${truncate(t2.text, 50)} ${import_picocolors7.default.dim(t2.id)}`
|
|
8993
9096
|
}))
|
|
8994
9097
|
});
|
|
8995
9098
|
return selected;
|
|
8996
9099
|
}
|
|
8997
9100
|
function resolveDate(opts) {
|
|
8998
9101
|
if (opts.backlog) return void 0;
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
if (opts.yesterday) return fmt(new Date(today2.getTime() - 864e5));
|
|
9002
|
-
if (opts.tomorrow) return fmt(new Date(today2.getTime() + 864e5));
|
|
9102
|
+
if (opts.yesterday) return localDateOffset(-1);
|
|
9103
|
+
if (opts.tomorrow) return localDateOffset(1);
|
|
9003
9104
|
if (opts.date) {
|
|
9004
9105
|
const parsed = parseHumanDateOnly(opts.date);
|
|
9005
9106
|
if (!parsed) throw Errors.invalidInput(`Cannot parse date: "${opts.date}". Use YYYY-MM-DD or natural language (tomorrow, next monday, etc.)`);
|
|
9006
9107
|
return parsed;
|
|
9007
9108
|
}
|
|
9008
|
-
return
|
|
9109
|
+
return localDateOnly();
|
|
9009
9110
|
}
|
|
9010
9111
|
function extractTime(dueDate) {
|
|
9011
9112
|
if (typeof dueDate !== "string") return "";
|
|
@@ -9018,9 +9119,9 @@ function isRepeating(t2) {
|
|
|
9018
9119
|
return !!t2.repeat?.type && t2.repeat.type !== "none";
|
|
9019
9120
|
}
|
|
9020
9121
|
function getCheckIndicator(t2) {
|
|
9021
|
-
if (t2.completed) return
|
|
9022
|
-
if (isRepeating(t2)) return
|
|
9023
|
-
return
|
|
9122
|
+
if (t2.completed) return import_picocolors7.default.green(SYM.check);
|
|
9123
|
+
if (isRepeating(t2)) return import_picocolors7.default.blue(SYM.repeat);
|
|
9124
|
+
return import_picocolors7.default.dim(SYM.circle);
|
|
9024
9125
|
}
|
|
9025
9126
|
function sortTasksForDisplay(tasks) {
|
|
9026
9127
|
const repeatOrder = { daily: 0, weekly: 1, monthly: 2 };
|
|
@@ -9043,50 +9144,60 @@ function sortTasksForDisplay(tasks) {
|
|
|
9043
9144
|
});
|
|
9044
9145
|
}
|
|
9045
9146
|
function printTaskDetail(t2) {
|
|
9046
|
-
const dim =
|
|
9147
|
+
const dim = import_picocolors7.default.dim;
|
|
9047
9148
|
console.log("");
|
|
9048
9149
|
printRecord([
|
|
9049
9150
|
["ID", dim(t2.id)],
|
|
9050
9151
|
["Text", t2.text],
|
|
9051
9152
|
["Due", formatDate(t2.dueDate) || dim("none (backlog)")],
|
|
9052
|
-
["Status", t2.completed ?
|
|
9153
|
+
["Status", t2.completed ? import_picocolors7.default.green("completed") : import_picocolors7.default.yellow("pending")],
|
|
9053
9154
|
["Tags", formatTags(t2.tags) || dim("none")],
|
|
9054
9155
|
["Difficulty", formatDifficulty(t2.difficulty) || dim("not set")],
|
|
9055
9156
|
["Duration", formatDuration(t2.duration) || dim("not set")],
|
|
9056
9157
|
["Repeat", formatRepeat(t2.repeat) || dim("none")],
|
|
9057
9158
|
["Note", t2.note || dim("none")],
|
|
9058
|
-
["Public", t2.isPublic ?
|
|
9159
|
+
["Public", t2.isPublic ? import_picocolors7.default.green("yes") : import_picocolors7.default.yellow("no")],
|
|
9059
9160
|
["Completions", String(t2.completions ?? 0)],
|
|
9060
9161
|
["Created", formatDate(t2.createdAt)]
|
|
9061
9162
|
]);
|
|
9163
|
+
if (Array.isArray(t2.subtasks) && t2.subtasks.length > 0) {
|
|
9164
|
+
console.log(` ${import_picocolors7.default.bold("Subtasks")}`);
|
|
9165
|
+
for (const s of t2.subtasks) {
|
|
9166
|
+
const box = s.completed ? import_picocolors7.default.green(SYM.check) : import_picocolors7.default.dim(SYM.circle);
|
|
9167
|
+
console.log(` ${box} ${s.completed ? import_picocolors7.default.strikethrough(dim(s.text)) : s.text}`);
|
|
9168
|
+
}
|
|
9169
|
+
}
|
|
9062
9170
|
console.log("");
|
|
9063
9171
|
}
|
|
9064
9172
|
function printTaskLine(t2) {
|
|
9065
9173
|
const check = getCheckIndicator(t2);
|
|
9066
9174
|
const rawText = truncate(t2.text, 50);
|
|
9067
|
-
const text = t2.completed ?
|
|
9175
|
+
const text = t2.completed ? import_picocolors7.default.strikethrough(import_picocolors7.default.dim(rawText)) : rawText;
|
|
9068
9176
|
const time = extractTime(t2.dueDate);
|
|
9069
9177
|
const tags = formatTags(t2.tags);
|
|
9070
9178
|
const difficulty = formatDifficulty(t2.difficulty);
|
|
9071
|
-
const id =
|
|
9179
|
+
const id = import_picocolors7.default.dim(t2.id);
|
|
9072
9180
|
const parts = [check, text];
|
|
9073
|
-
if (time) parts.push(
|
|
9181
|
+
if (time) parts.push(import_picocolors7.default.cyan(time));
|
|
9074
9182
|
if (tags) parts.push(tags);
|
|
9075
|
-
if (difficulty) parts.push(
|
|
9183
|
+
if (difficulty) parts.push(import_picocolors7.default.dim(`[${difficulty}]`));
|
|
9076
9184
|
parts.push(id);
|
|
9077
9185
|
console.log(" " + parts.join(" "));
|
|
9078
9186
|
}
|
|
9187
|
+
function printPartialNotice(failed) {
|
|
9188
|
+
console.log(` ${import_picocolors7.default.yellow("! some bookkeeping was deferred")} ${import_picocolors7.default.dim(`(${(failed ?? []).join(", ")})`)}`);
|
|
9189
|
+
}
|
|
9079
9190
|
function registerTasksCommands(program3) {
|
|
9080
9191
|
const tasks = program3.command("tasks").description("Manage tasks");
|
|
9081
|
-
tasks.command("list").description("List tasks by date or backlog").option("--date <date>",
|
|
9192
|
+
tasks.command("list").description("List tasks by date or backlog").option("--date <date>", 'YYYY-MM-DD or natural language ("tomorrow", "next monday")').option("--backlog", "Show backlog tasks").option("--tag <tag>", "Filter by tag").option("--yesterday", "Show yesterday's tasks").option("--tomorrow", "Show tomorrow's tasks").action(async function() {
|
|
9082
9193
|
const opts = this.optsWithGlobals();
|
|
9083
|
-
|
|
9194
|
+
requireUid();
|
|
9084
9195
|
const date = resolveDate(opts);
|
|
9085
9196
|
await runList({
|
|
9086
9197
|
global: opts,
|
|
9087
|
-
fn: () => listTasks(
|
|
9198
|
+
fn: () => listTasks({ date, backlog: opts.backlog, tag: opts.tag }),
|
|
9088
9199
|
dataKey: "tasks",
|
|
9089
|
-
columns: ["id", "text", "dueDate", "completed", "tags"
|
|
9200
|
+
columns: ["id", "text", "dueDate", "completed", "tags"],
|
|
9090
9201
|
spinnerMessage: "Fetching tasks...",
|
|
9091
9202
|
onInteractive: (payload) => {
|
|
9092
9203
|
const items = payload.tasks;
|
|
@@ -9094,7 +9205,7 @@ function registerTasksCommands(program3) {
|
|
|
9094
9205
|
const completed = items.filter((t2) => t2.completed);
|
|
9095
9206
|
console.log("");
|
|
9096
9207
|
if (opts.backlog) {
|
|
9097
|
-
console.log(` ${
|
|
9208
|
+
console.log(` ${import_picocolors7.default.bold("Backlog")} ${import_picocolors7.default.dim(`(${items.length})`)}`);
|
|
9098
9209
|
} else {
|
|
9099
9210
|
const viewDate = date ? /* @__PURE__ */ new Date(date + "T00:00:00") : /* @__PURE__ */ new Date();
|
|
9100
9211
|
console.log(formatWeekdayHeader(viewDate, completed.length));
|
|
@@ -9107,18 +9218,18 @@ function registerTasksCommands(program3) {
|
|
|
9107
9218
|
const viewDate = date ? /* @__PURE__ */ new Date(date + "T00:00:00") : /* @__PURE__ */ new Date();
|
|
9108
9219
|
const dayName = viewDate.toLocaleDateString("en-US", { weekday: "long" });
|
|
9109
9220
|
console.log(`
|
|
9110
|
-
${
|
|
9111
|
-
console.log(` ${
|
|
9221
|
+
${import_picocolors7.default.dim(`No tasks for ${dayName}. Enjoy your day!`)}`);
|
|
9222
|
+
console.log(` ${import_picocolors7.default.dim("--yesterday \xB7 --tomorrow \xB7 --date YYYY-MM-DD")}`);
|
|
9112
9223
|
} else {
|
|
9113
9224
|
console.log(`
|
|
9114
|
-
${
|
|
9225
|
+
${import_picocolors7.default.dim("No backlog tasks.")}`);
|
|
9115
9226
|
}
|
|
9116
9227
|
console.log("");
|
|
9117
9228
|
return;
|
|
9118
9229
|
}
|
|
9119
9230
|
if (pending.length > 0) {
|
|
9120
9231
|
console.log(`
|
|
9121
|
-
${
|
|
9232
|
+
${import_picocolors7.default.bold("Pending")} ${import_picocolors7.default.dim(`(${pending.length})`)}
|
|
9122
9233
|
`);
|
|
9123
9234
|
for (const t2 of pending) {
|
|
9124
9235
|
printTaskLine(t2);
|
|
@@ -9126,7 +9237,7 @@ function registerTasksCommands(program3) {
|
|
|
9126
9237
|
}
|
|
9127
9238
|
if (completed.length > 0) {
|
|
9128
9239
|
console.log(`
|
|
9129
|
-
${
|
|
9240
|
+
${import_picocolors7.default.dim(`Completed (${completed.length})`)}
|
|
9130
9241
|
`);
|
|
9131
9242
|
for (const t2 of completed) {
|
|
9132
9243
|
printTaskLine(t2);
|
|
@@ -9135,7 +9246,7 @@ function registerTasksCommands(program3) {
|
|
|
9135
9246
|
if (!opts.backlog) {
|
|
9136
9247
|
console.log(`
|
|
9137
9248
|
${formatProgressSummary(completed.length, items.length)}`);
|
|
9138
|
-
console.log(` ${
|
|
9249
|
+
console.log(` ${import_picocolors7.default.dim("--yesterday \xB7 --tomorrow \xB7 --date YYYY-MM-DD")}`);
|
|
9139
9250
|
}
|
|
9140
9251
|
console.log("");
|
|
9141
9252
|
}
|
|
@@ -9150,11 +9261,11 @@ Examples:
|
|
|
9150
9261
|
$ numo tasks list --tag Work # Filter by tag
|
|
9151
9262
|
$ numo tasks list --json | jq '.tasks[].text'`);
|
|
9152
9263
|
tasks.command("get [id]").description("Get a task by ID").action(async function(id) {
|
|
9153
|
-
|
|
9264
|
+
requireUid();
|
|
9154
9265
|
const taskId = await promptForMissing({ value: id, message: "Task ID" });
|
|
9155
9266
|
await runGet({
|
|
9156
9267
|
global: this.optsWithGlobals(),
|
|
9157
|
-
fn: () => getTask(
|
|
9268
|
+
fn: () => getTask(taskId),
|
|
9158
9269
|
spinnerMessage: "Fetching task...",
|
|
9159
9270
|
onInteractive: printTaskDetail
|
|
9160
9271
|
});
|
|
@@ -9162,171 +9273,165 @@ Examples:
|
|
|
9162
9273
|
Examples:
|
|
9163
9274
|
$ numo tasks get abc123
|
|
9164
9275
|
$ numo tasks get abc123 --json | jq '.text'`);
|
|
9165
|
-
tasks.command("create").description("Create a
|
|
9276
|
+
tasks.command("create [text...]").description("Create a task \u2014 quick via text/flags, or an interactive wizard").option("--text <text>", "Task text (alternative to positional text)").option("--due <date>", 'Due date: YYYY-MM-DD, "YYYY-MM-DD HH:mm", or natural language').option("--backlog", "No due date (Someday / backlog)").option("--repeat <type>", "Recurring routine: daily | weekly | monthly").option("--weekdays <days>", "For --repeat weekly: comma list e.g. Mon,Wed,Fri").option("--month-days <days>", "For --repeat monthly: comma list e.g. 1,15").option("--every <n>", "Repeat interval (every N days/weeks/months)").option("--tags <tags>", "Comma-separated tags").option("--public", "Make task public").option("--private", "Make task private (default)").option("--note <note>", "Private note").option("--difficulty <n>", "Effort 0\u20133 (S/M/L/XL)").option("--duration <n>", "Duration in minutes").option("--subtask <text>", 'Add a subtask (repeatable: --subtask "a" --subtask "b")', collectValue, []).option("--client-task-id <id>", "Idempotency key \u2014 retrying with the same id returns the existing task instead of duplicating").action(async function(textParts) {
|
|
9166
9277
|
const opts = this.optsWithGlobals();
|
|
9167
|
-
|
|
9168
|
-
const
|
|
9169
|
-
const
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9176
|
-
|
|
9177
|
-
|
|
9278
|
+
requireUid();
|
|
9279
|
+
const providedText = (textParts && textParts.length ? textParts.join(" ") : void 0) ?? opts.text;
|
|
9280
|
+
const hasQuickInput = !!providedText || opts.due || opts.backlog || opts.repeat || opts.tags || opts.note || opts.difficulty !== void 0 || opts.duration || opts.public || opts.private || opts.subtask && opts.subtask.length;
|
|
9281
|
+
const useWizard = isInteractive() && !opts.json && !hasQuickInput;
|
|
9282
|
+
const body = {};
|
|
9283
|
+
if (useWizard) {
|
|
9284
|
+
body.text = await promptForMissing({ value: providedText, message: "Task text", placeholder: "What do you need to do?" });
|
|
9285
|
+
const fmt = (d) => localDateOnly(d);
|
|
9286
|
+
const today2 = /* @__PURE__ */ new Date();
|
|
9287
|
+
const tomorrow2 = new Date(today2);
|
|
9288
|
+
tomorrow2.setDate(tomorrow2.getDate() + 1);
|
|
9289
|
+
const schedule = await promptSelect({
|
|
9290
|
+
message: "When?",
|
|
9291
|
+
options: [
|
|
9292
|
+
{ value: "today", label: `Today (${fmt(today2)})` },
|
|
9293
|
+
{ value: "tomorrow", label: `Tomorrow (${fmt(tomorrow2)})` },
|
|
9294
|
+
{ value: "pick", label: "Pick a date..." },
|
|
9295
|
+
{ value: "someday", label: "Someday (backlog)" },
|
|
9296
|
+
{ value: "daily", label: "Every day (routine)" },
|
|
9297
|
+
{ value: "weekly", label: "Every week (routine)" },
|
|
9298
|
+
{ value: "monthly", label: "Every month (routine)" }
|
|
9299
|
+
]
|
|
9300
|
+
});
|
|
9301
|
+
if (schedule === "today") {
|
|
9302
|
+
body.dueDate = fmt(today2);
|
|
9303
|
+
} else if (schedule === "tomorrow") {
|
|
9304
|
+
body.dueDate = fmt(tomorrow2);
|
|
9305
|
+
} else if (schedule === "pick") {
|
|
9306
|
+
body.dueDate = await promptText({ message: "Date", placeholder: fmt(tomorrow2), required: true });
|
|
9307
|
+
} else if (schedule === "someday") {
|
|
9308
|
+
body.backlog = true;
|
|
9309
|
+
} else {
|
|
9310
|
+
const repeat = { type: schedule, every: 1, custom: false, monthDays: null, weekDays: null };
|
|
9311
|
+
if (schedule === "weekly") {
|
|
9312
|
+
repeat.weekDays = await promptMultiSelect({
|
|
9313
|
+
message: "Days of week",
|
|
9314
|
+
options: [
|
|
9315
|
+
{ value: "Mon", label: "Monday" },
|
|
9316
|
+
{ value: "Tue", label: "Tuesday" },
|
|
9317
|
+
{ value: "Wed", label: "Wednesday" },
|
|
9318
|
+
{ value: "Thu", label: "Thursday" },
|
|
9319
|
+
{ value: "Fri", label: "Friday" },
|
|
9320
|
+
{ value: "Sat", label: "Saturday" },
|
|
9321
|
+
{ value: "Sun", label: "Sunday" }
|
|
9322
|
+
],
|
|
9323
|
+
required: true
|
|
9324
|
+
});
|
|
9325
|
+
} else if (schedule === "monthly") {
|
|
9326
|
+
const daysInput = await promptText({ message: "Days of month", placeholder: "1,15", required: true });
|
|
9327
|
+
repeat.monthDays = daysInput.split(",").map((s) => parseInt(s.trim()));
|
|
9328
|
+
}
|
|
9329
|
+
body.repeat = repeat;
|
|
9330
|
+
body.dueDate = fmt(today2);
|
|
9331
|
+
}
|
|
9332
|
+
const visibility = await promptSelect({
|
|
9333
|
+
message: "Visibility",
|
|
9334
|
+
options: [
|
|
9335
|
+
{ value: "private", label: "Private \u2014 only you can see it" },
|
|
9336
|
+
{ value: "public", label: "Public \u2014 visible in community" }
|
|
9337
|
+
]
|
|
9338
|
+
});
|
|
9339
|
+
body.isPublic = visibility === "public";
|
|
9340
|
+
if (await promptConfirm({ message: "Add details? (tags, effort, time, note)", initialValue: false })) {
|
|
9341
|
+
const tags = await promptMultiSelect({
|
|
9342
|
+
message: "Tags",
|
|
9178
9343
|
options: [
|
|
9179
|
-
{ value: "
|
|
9180
|
-
{ value: "
|
|
9181
|
-
{ value: "
|
|
9182
|
-
{ value: "
|
|
9183
|
-
{ value: "
|
|
9184
|
-
{ value: "
|
|
9185
|
-
{ value: "
|
|
9344
|
+
{ value: "House", label: "House" },
|
|
9345
|
+
{ value: "Work", label: "Work" },
|
|
9346
|
+
{ value: "Study", label: "Study" },
|
|
9347
|
+
{ value: "Hobby", label: "Hobby" },
|
|
9348
|
+
{ value: "Health", label: "Health" },
|
|
9349
|
+
{ value: "Relationship", label: "Relationship" },
|
|
9350
|
+
{ value: "Self-care", label: "Self-care" },
|
|
9351
|
+
{ value: "Relax", label: "Relax" },
|
|
9352
|
+
{ value: "Kids", label: "Kids" }
|
|
9186
9353
|
]
|
|
9187
9354
|
});
|
|
9188
|
-
if (
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
body.dueDate = fmt(tomorrow2);
|
|
9192
|
-
} else if (schedule === "pick") {
|
|
9193
|
-
const date = await promptText({ message: "Date", placeholder: fmt(tomorrow2), required: true });
|
|
9194
|
-
body.dueDate = date;
|
|
9195
|
-
} else if (["daily", "weekly", "monthly"].includes(schedule)) {
|
|
9196
|
-
const repeat = {
|
|
9197
|
-
type: schedule,
|
|
9198
|
-
every: 1,
|
|
9199
|
-
end: "never",
|
|
9200
|
-
endDate: null,
|
|
9201
|
-
endAfter: null,
|
|
9202
|
-
monthDays: null,
|
|
9203
|
-
weekDays: null
|
|
9204
|
-
};
|
|
9205
|
-
if (schedule === "weekly") {
|
|
9206
|
-
const days = await promptMultiSelect({
|
|
9207
|
-
message: "Days of week",
|
|
9208
|
-
options: [
|
|
9209
|
-
{ value: "Mon", label: "Monday" },
|
|
9210
|
-
{ value: "Tue", label: "Tuesday" },
|
|
9211
|
-
{ value: "Wed", label: "Wednesday" },
|
|
9212
|
-
{ value: "Thu", label: "Thursday" },
|
|
9213
|
-
{ value: "Fri", label: "Friday" },
|
|
9214
|
-
{ value: "Sat", label: "Saturday" },
|
|
9215
|
-
{ value: "Sun", label: "Sunday" }
|
|
9216
|
-
],
|
|
9217
|
-
required: true
|
|
9218
|
-
});
|
|
9219
|
-
repeat.weekDays = days;
|
|
9220
|
-
} else if (schedule === "monthly") {
|
|
9221
|
-
const daysInput = await promptText({ message: "Days of month", placeholder: "1,15", required: true });
|
|
9222
|
-
repeat.monthDays = daysInput.split(",").map((s) => parseInt(s.trim()));
|
|
9223
|
-
}
|
|
9224
|
-
body.repeat = repeat;
|
|
9225
|
-
body.dueDate = fmt(today2);
|
|
9226
|
-
}
|
|
9227
|
-
}
|
|
9228
|
-
if (!opts.private && !opts.public) {
|
|
9229
|
-
const visibility = await promptSelect({
|
|
9230
|
-
message: "Visibility",
|
|
9355
|
+
if (tags.length > 0) body.tags = tags;
|
|
9356
|
+
const difficulty = await promptSelect({
|
|
9357
|
+
message: "Effort",
|
|
9231
9358
|
options: [
|
|
9232
|
-
{ value: "
|
|
9233
|
-
{ value: "
|
|
9359
|
+
{ value: "skip", label: "Skip" },
|
|
9360
|
+
{ value: "0", label: "S \u2014 Tiny" },
|
|
9361
|
+
{ value: "1", label: "M \u2014 Medium" },
|
|
9362
|
+
{ value: "2", label: "L \u2014 High" },
|
|
9363
|
+
{ value: "3", label: "XL \u2014 Huge" }
|
|
9234
9364
|
]
|
|
9235
9365
|
});
|
|
9236
|
-
if (
|
|
9237
|
-
|
|
9238
|
-
const addDetails = await promptConfirm({
|
|
9239
|
-
message: "Add details? (tags, effort, time, note)",
|
|
9240
|
-
initialValue: false
|
|
9241
|
-
});
|
|
9242
|
-
if (addDetails) {
|
|
9243
|
-
if (!opts.tags) {
|
|
9244
|
-
const tags = await promptMultiSelect({
|
|
9245
|
-
message: "Tags",
|
|
9246
|
-
options: [
|
|
9247
|
-
{ value: "House", label: "House" },
|
|
9248
|
-
{ value: "Work", label: "Work" },
|
|
9249
|
-
{ value: "Study", label: "Study" },
|
|
9250
|
-
{ value: "Hobby", label: "Hobby" },
|
|
9251
|
-
{ value: "Health", label: "Health" },
|
|
9252
|
-
{ value: "Relationship", label: "Relationship" },
|
|
9253
|
-
{ value: "Self-care", label: "Self-care" },
|
|
9254
|
-
{ value: "Relax", label: "Relax" },
|
|
9255
|
-
{ value: "Kids", label: "Kids" }
|
|
9256
|
-
]
|
|
9257
|
-
});
|
|
9258
|
-
if (tags.length > 0) body.tags = tags;
|
|
9259
|
-
}
|
|
9260
|
-
if (opts.difficulty === void 0) {
|
|
9261
|
-
const difficulty = await promptSelect({
|
|
9262
|
-
message: "Effort",
|
|
9263
|
-
options: [
|
|
9264
|
-
{ value: "skip", label: "Skip" },
|
|
9265
|
-
{ value: "0", label: "S \u2014 Tiny" },
|
|
9266
|
-
{ value: "1", label: "M \u2014 Medium" },
|
|
9267
|
-
{ value: "2", label: "L \u2014 High" },
|
|
9268
|
-
{ value: "3", label: "XL \u2014 Huge" }
|
|
9269
|
-
]
|
|
9270
|
-
});
|
|
9271
|
-
if (difficulty !== "skip") body.difficulty = parseInt(difficulty);
|
|
9272
|
-
}
|
|
9273
|
-
const addTime = await promptConfirm({ message: "Add a specific time?", initialValue: false });
|
|
9274
|
-
if (addTime && body.dueDate) {
|
|
9366
|
+
if (difficulty !== "skip") body.difficulty = parseInt(difficulty);
|
|
9367
|
+
if (body.dueDate && await promptConfirm({ message: "Add a specific time?", initialValue: false })) {
|
|
9275
9368
|
const time = await promptText({ message: "Time", placeholder: "09:30", required: true });
|
|
9276
9369
|
body.dueDate = `${body.dueDate} ${time}`;
|
|
9277
9370
|
}
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9371
|
+
const note = await promptText({ message: "Note (enter to skip)", placeholder: "Private note", required: false });
|
|
9372
|
+
if (note) body.note = note;
|
|
9373
|
+
const subs = await promptText({ message: "Subtasks (comma-separated, enter to skip)", placeholder: "Step 1, Step 2", required: false });
|
|
9374
|
+
if (subs) {
|
|
9375
|
+
const built = buildSubtasks(subs.split(","));
|
|
9376
|
+
if (built.length) body.subtasks = built;
|
|
9281
9377
|
}
|
|
9282
9378
|
}
|
|
9283
|
-
if (opts.public) body.isPublic = true;
|
|
9284
|
-
if (opts.private) body.isPublic = false;
|
|
9285
|
-
if (opts.tags && !body.tags) body.tags = opts.tags.split(",");
|
|
9286
|
-
if (opts.note && !body.note) body.note = opts.note;
|
|
9287
9379
|
} else {
|
|
9288
|
-
|
|
9380
|
+
const text = providedText ?? (isInteractive() && !opts.json ? await promptText({ message: "Task text", placeholder: "What do you need to do?", required: true }) : void 0);
|
|
9381
|
+
if (!text) throw Errors.missingArg("Task text", "text");
|
|
9382
|
+
body.text = text;
|
|
9383
|
+
if (opts.backlog) {
|
|
9384
|
+
body.backlog = true;
|
|
9385
|
+
} else if (opts.due) {
|
|
9289
9386
|
const parsed = parseHumanDate(opts.due);
|
|
9290
9387
|
if (!parsed) throw Errors.invalidInput(`Cannot parse date: "${opts.due}"`);
|
|
9291
9388
|
body.dueDate = parsed;
|
|
9389
|
+
} else {
|
|
9390
|
+
body.dueDate = localDateOnly();
|
|
9292
9391
|
}
|
|
9392
|
+
const repeat = buildRepeatConfig(opts);
|
|
9393
|
+
if (repeat) body.repeat = repeat;
|
|
9293
9394
|
if (opts.tags) body.tags = opts.tags.split(",");
|
|
9294
9395
|
if (opts.note) body.note = opts.note;
|
|
9295
|
-
if (opts.priority) body.priority = parseFloat(opts.priority);
|
|
9296
9396
|
if (opts.difficulty !== void 0) body.difficulty = parseInt(opts.difficulty);
|
|
9297
9397
|
if (opts.duration) body.duration = parseInt(opts.duration);
|
|
9298
|
-
if (opts.
|
|
9299
|
-
|
|
9398
|
+
if (opts.subtask && opts.subtask.length) body.subtasks = buildSubtasks(opts.subtask);
|
|
9399
|
+
body.isPublic = opts.public ? true : false;
|
|
9300
9400
|
}
|
|
9401
|
+
if (opts.clientTaskId) body.clientTaskId = opts.clientTaskId;
|
|
9402
|
+
body.listPosition = "top";
|
|
9403
|
+
normalizeDueDateInBody(body);
|
|
9301
9404
|
await runCreate({
|
|
9302
9405
|
global: opts,
|
|
9303
|
-
fn: () => createTask(
|
|
9406
|
+
fn: () => createTask(body),
|
|
9304
9407
|
dataKey: "task",
|
|
9305
9408
|
spinnerMessage: "Creating task...",
|
|
9306
9409
|
onInteractive: (_task, payload) => {
|
|
9307
|
-
const { task, karma } = payload;
|
|
9308
|
-
const check =
|
|
9410
|
+
const { task, karma, idempotentReplay } = payload;
|
|
9411
|
+
const check = import_picocolors7.default.green(SYM.check);
|
|
9309
9412
|
console.log(`
|
|
9310
|
-
${check} Created ${task.text}`);
|
|
9311
|
-
if (karma) {
|
|
9312
|
-
console.log(` ${formatKarmaGain(karma)}${" ".repeat(20)}${import_picocolors8.default.dim(task.id)}`);
|
|
9313
|
-
}
|
|
9413
|
+
${check} ${idempotentReplay ? "Exists" : "Created"} ${task.text} ${import_picocolors7.default.dim(task.id)}`);
|
|
9414
|
+
if (karma) console.log(` ${formatKarmaGain(karma)}`);
|
|
9314
9415
|
console.log("");
|
|
9315
9416
|
}
|
|
9316
9417
|
});
|
|
9317
9418
|
}).addHelpText("after", `
|
|
9318
9419
|
Examples:
|
|
9319
|
-
$ numo tasks create
|
|
9320
|
-
$ numo tasks create
|
|
9321
|
-
$ numo tasks create
|
|
9322
|
-
$ numo tasks create
|
|
9323
|
-
$ numo tasks create
|
|
9324
|
-
|
|
9420
|
+
$ numo tasks create # Interactive wizard
|
|
9421
|
+
$ numo tasks create "Buy groceries" # Quick (today, private, top)
|
|
9422
|
+
$ numo tasks create "Meeting" --due "2026-03-27 14:30" # With a time
|
|
9423
|
+
$ numo tasks create "Standup" --repeat weekly --weekdays Mon,Wed,Fri
|
|
9424
|
+
$ numo tasks create "Pay rent" --repeat monthly --month-days 1
|
|
9425
|
+
$ numo tasks create "Read later" --backlog # Someday / no due date
|
|
9426
|
+
$ numo tasks create "Trip" --subtask "Book hotel" --subtask "Pack"
|
|
9427
|
+
$ numo tasks create "Review PR" --tags Work --difficulty 2 --public`);
|
|
9428
|
+
tasks.command("update [id]").description("Update a task").option("--text <text>", "Task text").option("--due <date>", 'Due date: YYYY-MM-DD, "YYYY-MM-DD HH:mm", or natural language').option("--clear-time", "Strip time-of-day; treat task as all-day").option("--no-time", "Alias of --clear-time").option("--tags <tags>", "Comma-separated tags").option("--public", "Make task public").option("--private", "Make task private").option("--note <note>", "Task note").option("--difficulty <n>", "Difficulty 0-3 (S/M/L/XL)").option("--duration <n>", "Duration in minutes").option("--repeat <type>", "Set recurrence: daily | weekly | monthly | none").option("--weekdays <days>", "For --repeat weekly: comma list e.g. Mon,Wed").option("--month-days <days>", "For --repeat monthly: comma list e.g. 1,15").option("--every <n>", "Repeat interval (every N)").option("--backlog", "Move to backlog (clear the due date)").option("--subtask <text>", 'Replace subtasks (repeatable: --subtask "a" --subtask "b")', collectValue, []).action(async function(id) {
|
|
9325
9429
|
const opts = this.optsWithGlobals();
|
|
9326
|
-
|
|
9430
|
+
requireUid();
|
|
9327
9431
|
const taskId = await promptForMissing({ value: id, message: "Task ID" });
|
|
9432
|
+
const clearTime = opts.clearTime === true || opts.time === false;
|
|
9328
9433
|
const body = {};
|
|
9329
|
-
const hasAnyFlag = opts.text || opts.due || opts.tags || opts.public || opts.private || opts.note || opts.
|
|
9434
|
+
const hasAnyFlag = opts.text || opts.due || opts.tags || opts.public || opts.private || opts.note || opts.difficulty !== void 0 || opts.duration || clearTime || opts.repeat !== void 0 || opts.backlog || opts.weekdays || opts.monthDays || opts.every || opts.subtask && opts.subtask.length;
|
|
9330
9435
|
if (!hasAnyFlag && isInteractive() && !opts.json) {
|
|
9331
9436
|
const text = await promptText({ message: "Text (enter to skip)", required: false });
|
|
9332
9437
|
if (text) body.text = text;
|
|
@@ -9336,8 +9441,6 @@ Examples:
|
|
|
9336
9441
|
if (tags) body.tags = tags.split(",");
|
|
9337
9442
|
const note = await promptText({ message: "Note (enter to skip)", required: false });
|
|
9338
9443
|
if (note) body.note = note;
|
|
9339
|
-
const priority = await promptText({ message: "Priority (enter to skip)", placeholder: "0.1\u20131.0", required: false });
|
|
9340
|
-
if (priority) body.priority = parseFloat(priority);
|
|
9341
9444
|
const difficulty = await promptText({ message: "Difficulty (enter to skip)", placeholder: "0\u20133", required: false });
|
|
9342
9445
|
if (difficulty) body.difficulty = parseInt(difficulty);
|
|
9343
9446
|
const duration = await promptText({ message: "Duration in minutes (enter to skip)", placeholder: "10", required: false });
|
|
@@ -9353,18 +9456,28 @@ Examples:
|
|
|
9353
9456
|
if (opts.public) body.isPublic = true;
|
|
9354
9457
|
if (opts.private) body.isPublic = false;
|
|
9355
9458
|
if (opts.note) body.note = opts.note;
|
|
9356
|
-
if (opts.priority) body.priority = parseFloat(opts.priority);
|
|
9357
9459
|
if (opts.difficulty !== void 0) body.difficulty = parseInt(opts.difficulty);
|
|
9358
9460
|
if (opts.duration) body.duration = parseInt(opts.duration);
|
|
9461
|
+
const repeat = buildRepeatConfig(opts);
|
|
9462
|
+
if (repeat) body.repeat = repeat;
|
|
9463
|
+
if (opts.backlog) body.dueDate = null;
|
|
9464
|
+
if (opts.subtask && opts.subtask.length) body.subtasks = buildSubtasks(opts.subtask);
|
|
9465
|
+
}
|
|
9466
|
+
if (clearTime) {
|
|
9467
|
+
const currentDue = typeof body.dueDate === "string" ? body.dueDate : (await getTask(taskId)).dueDate;
|
|
9468
|
+
if (currentDue && currentDue.length >= 10) {
|
|
9469
|
+
body.dueDate = `${currentDue.slice(0, 10)} 00:00`;
|
|
9470
|
+
}
|
|
9359
9471
|
}
|
|
9472
|
+
normalizeDueDateInBody(body);
|
|
9360
9473
|
await runWrite({
|
|
9361
9474
|
global: opts,
|
|
9362
|
-
fn: () => updateTask(
|
|
9475
|
+
fn: () => updateTask(taskId, body),
|
|
9363
9476
|
dataKey: "task",
|
|
9364
9477
|
spinnerMessage: "Updating task...",
|
|
9365
9478
|
onInteractive: (payload) => {
|
|
9366
9479
|
console.log(`
|
|
9367
|
-
${
|
|
9480
|
+
${import_picocolors7.default.green("Updated!")} ${payload.task.text} ${import_picocolors7.default.dim(payload.task.id)}
|
|
9368
9481
|
`);
|
|
9369
9482
|
}
|
|
9370
9483
|
});
|
|
@@ -9372,28 +9485,35 @@ Examples:
|
|
|
9372
9485
|
Examples:
|
|
9373
9486
|
$ numo tasks update abc123 --text "Updated text"
|
|
9374
9487
|
$ numo tasks update abc123 --due 2026-03-28
|
|
9488
|
+
$ numo tasks update abc123 --no-time # strip time-of-day; task becomes all-day
|
|
9489
|
+
$ numo tasks update abc123 --clear-time # same as --no-time
|
|
9375
9490
|
$ numo tasks update abc123 --tags Work,Health
|
|
9376
|
-
$ numo tasks update abc123 --difficulty 2 --note "Important"
|
|
9491
|
+
$ numo tasks update abc123 --difficulty 2 --note "Important"
|
|
9492
|
+
$ numo tasks update abc123 --repeat weekly --weekdays Mon,Thu
|
|
9493
|
+
$ numo tasks update abc123 --repeat none # stop repeating
|
|
9494
|
+
$ numo tasks update abc123 --backlog # clear due date
|
|
9495
|
+
$ numo tasks update abc123 --subtask "Step 1" --subtask "Step 2" # replaces subtasks`);
|
|
9377
9496
|
tasks.command("delete [id]").description("Delete a task").option("--yes", "Skip confirmation prompt").option("--stdin", "Read task IDs from stdin (one per line)").action(async function(id) {
|
|
9378
9497
|
const opts = this.optsWithGlobals();
|
|
9379
|
-
|
|
9498
|
+
requireUid();
|
|
9380
9499
|
if (opts.stdin) {
|
|
9381
9500
|
const ids = readStdinLines();
|
|
9382
9501
|
for (const lineId of ids) {
|
|
9383
9502
|
try {
|
|
9384
|
-
const result = await deleteTask(
|
|
9503
|
+
const result = await deleteTask(lineId);
|
|
9385
9504
|
printNdjsonLine({ id: lineId, ...result });
|
|
9386
9505
|
} catch (err) {
|
|
9506
|
+
process.exitCode = ExitCode.GENERAL;
|
|
9387
9507
|
printNdjsonLine({ id: lineId, error: err.message });
|
|
9388
9508
|
}
|
|
9389
9509
|
}
|
|
9390
9510
|
return;
|
|
9391
9511
|
}
|
|
9392
|
-
const taskId = await pickTask(
|
|
9512
|
+
const taskId = await pickTask(id, "delete");
|
|
9393
9513
|
if (isInteractive() && !opts.yes && !opts.json) {
|
|
9394
9514
|
let taskText = taskId;
|
|
9395
9515
|
try {
|
|
9396
|
-
const task = await getTask(
|
|
9516
|
+
const task = await getTask(taskId);
|
|
9397
9517
|
taskText = task.text ?? taskId;
|
|
9398
9518
|
} catch {
|
|
9399
9519
|
}
|
|
@@ -9402,19 +9522,20 @@ Examples:
|
|
|
9402
9522
|
initialValue: false
|
|
9403
9523
|
});
|
|
9404
9524
|
if (!confirmed) {
|
|
9405
|
-
console.log(
|
|
9525
|
+
console.log(import_picocolors7.default.dim(" Cancelled."));
|
|
9406
9526
|
return;
|
|
9407
9527
|
}
|
|
9408
9528
|
}
|
|
9409
9529
|
await runWrite({
|
|
9410
9530
|
global: opts,
|
|
9411
|
-
fn: () => deleteTask(
|
|
9531
|
+
fn: () => deleteTask(taskId),
|
|
9412
9532
|
spinnerMessage: "Deleting task...",
|
|
9413
9533
|
onInteractive: (data) => {
|
|
9414
|
-
const cross =
|
|
9534
|
+
const cross = import_picocolors7.default.red(SYM.cross);
|
|
9415
9535
|
console.log(`
|
|
9416
9536
|
${cross} Deleted ${data.taskText || taskId}`);
|
|
9417
|
-
if (data.archived) console.log(` ${
|
|
9537
|
+
if (data.archived) console.log(` ${import_picocolors7.default.dim("Archived")}`);
|
|
9538
|
+
if (data.partial) printPartialNotice(data.failed);
|
|
9418
9539
|
console.log("");
|
|
9419
9540
|
}
|
|
9420
9541
|
});
|
|
@@ -9425,31 +9546,41 @@ Examples:
|
|
|
9425
9546
|
$ numo tasks delete abc123 --json`);
|
|
9426
9547
|
tasks.command("complete [id]").description("Mark task as complete").option("--date <datetime>", 'Completion datetime "YYYY-MM-DD HH:mm"').option("--stdin", "Read task IDs from stdin (one per line)").action(async function(id) {
|
|
9427
9548
|
const opts = this.optsWithGlobals();
|
|
9428
|
-
|
|
9549
|
+
requireUid();
|
|
9550
|
+
if (opts.date && !isCompletableDate(opts.date)) {
|
|
9551
|
+
throw Errors.invalidInput("completion date must be today or yesterday");
|
|
9552
|
+
}
|
|
9429
9553
|
if (opts.stdin) {
|
|
9430
9554
|
const ids = readStdinLines();
|
|
9431
9555
|
for (const lineId of ids) {
|
|
9432
9556
|
try {
|
|
9433
|
-
const result = await completeTask(
|
|
9557
|
+
const result = await completeTask(lineId, opts.date);
|
|
9434
9558
|
printNdjsonLine({ id: lineId, ...result });
|
|
9435
9559
|
} catch (err) {
|
|
9560
|
+
process.exitCode = ExitCode.GENERAL;
|
|
9436
9561
|
printNdjsonLine({ id: lineId, error: err.message });
|
|
9437
9562
|
}
|
|
9438
9563
|
}
|
|
9439
9564
|
return;
|
|
9440
9565
|
}
|
|
9441
|
-
const taskId = await pickTask(
|
|
9566
|
+
const taskId = await pickTask(id, "complete");
|
|
9442
9567
|
await runWrite({
|
|
9443
9568
|
global: opts,
|
|
9444
|
-
fn: () => completeTask(
|
|
9569
|
+
fn: () => completeTask(taskId, opts.date),
|
|
9445
9570
|
spinnerMessage: "Completing task...",
|
|
9446
9571
|
onInteractive: (data) => {
|
|
9447
|
-
const check =
|
|
9448
|
-
|
|
9572
|
+
const check = import_picocolors7.default.green(SYM.check);
|
|
9573
|
+
if (data.alreadyCompleted) {
|
|
9574
|
+
console.log(`
|
|
9575
|
+
${check} Already done ${data.taskText ?? taskId}`);
|
|
9576
|
+
} else {
|
|
9577
|
+
console.log(`
|
|
9449
9578
|
${check} Done! ${data.taskText ?? taskId}`);
|
|
9450
|
-
|
|
9451
|
-
|
|
9579
|
+
if (data.karma) {
|
|
9580
|
+
console.log(` ${formatKarmaGain(data.karma, data.checksInRow)}`);
|
|
9581
|
+
}
|
|
9452
9582
|
}
|
|
9583
|
+
if (data.partial) printPartialNotice(data.failed);
|
|
9453
9584
|
console.log("");
|
|
9454
9585
|
}
|
|
9455
9586
|
});
|
|
@@ -9459,14 +9590,15 @@ Examples:
|
|
|
9459
9590
|
$ numo tasks complete abc123 --date "2026-03-26 14:30"`);
|
|
9460
9591
|
tasks.command("uncomplete [id]").description("Mark task as incomplete").option("--stdin", "Read task IDs from stdin (one per line)").action(async function(id) {
|
|
9461
9592
|
const opts = this.optsWithGlobals();
|
|
9462
|
-
|
|
9593
|
+
requireUid();
|
|
9463
9594
|
if (opts.stdin) {
|
|
9464
9595
|
const ids = readStdinLines();
|
|
9465
9596
|
for (const lineId of ids) {
|
|
9466
9597
|
try {
|
|
9467
|
-
const result = await uncompleteTask(
|
|
9598
|
+
const result = await uncompleteTask(lineId);
|
|
9468
9599
|
printNdjsonLine({ id: lineId, ...result });
|
|
9469
9600
|
} catch (err) {
|
|
9601
|
+
process.exitCode = ExitCode.GENERAL;
|
|
9470
9602
|
printNdjsonLine({ id: lineId, error: err.message });
|
|
9471
9603
|
}
|
|
9472
9604
|
}
|
|
@@ -9475,14 +9607,15 @@ Examples:
|
|
|
9475
9607
|
const taskId = await promptForMissing({ value: id, message: "Task ID" });
|
|
9476
9608
|
await runWrite({
|
|
9477
9609
|
global: opts,
|
|
9478
|
-
fn: () => uncompleteTask(
|
|
9610
|
+
fn: () => uncompleteTask(taskId),
|
|
9479
9611
|
dataKey: "task",
|
|
9480
9612
|
spinnerMessage: "Uncompleting task...",
|
|
9481
9613
|
onInteractive: (data) => {
|
|
9482
9614
|
const arrow = SYM.undo;
|
|
9483
9615
|
console.log(`
|
|
9484
|
-
${
|
|
9485
|
-
console.log(` ${
|
|
9616
|
+
${import_picocolors7.default.yellow(arrow)} Reverted ${data.task.text ?? taskId}`);
|
|
9617
|
+
console.log(` ${import_picocolors7.default.dim("Karma adjustment applied")}`);
|
|
9618
|
+
if (data.partial) printPartialNotice(data.failed);
|
|
9486
9619
|
console.log("");
|
|
9487
9620
|
}
|
|
9488
9621
|
});
|
|
@@ -9492,19 +9625,20 @@ Examples:
|
|
|
9492
9625
|
}
|
|
9493
9626
|
|
|
9494
9627
|
// src/cli/commands/posts.ts
|
|
9495
|
-
var
|
|
9628
|
+
var import_picocolors9 = __toESM(require_picocolors(), 1);
|
|
9496
9629
|
|
|
9497
9630
|
// src/cli/lib/pagination.ts
|
|
9498
|
-
var
|
|
9631
|
+
var import_picocolors8 = __toESM(require_picocolors(), 1);
|
|
9499
9632
|
function printPaginationHint(opts) {
|
|
9500
9633
|
if (!opts.nextCursor) return;
|
|
9501
9634
|
const parts = [opts.command, `--cursor ${opts.nextCursor}`];
|
|
9502
9635
|
if (opts.limit) parts.push(`--limit ${opts.limit}`);
|
|
9503
9636
|
console.log(`
|
|
9504
|
-
${
|
|
9637
|
+
${import_picocolors8.default.dim("Next page:")} ${import_picocolors8.default.dim("$")} numo ${parts.join(" ")}`);
|
|
9505
9638
|
}
|
|
9506
9639
|
|
|
9507
9640
|
// src/cli/services/posts.ts
|
|
9641
|
+
init_api_client();
|
|
9508
9642
|
async function listPosts(opts) {
|
|
9509
9643
|
return api.get("/api/posts", {
|
|
9510
9644
|
cursor: opts.cursor,
|
|
@@ -9514,99 +9648,54 @@ async function listPosts(opts) {
|
|
|
9514
9648
|
async function getPost(id) {
|
|
9515
9649
|
return api.get(`/api/posts/${encodeURIComponent(id)}`);
|
|
9516
9650
|
}
|
|
9517
|
-
async function createPost(uid, body) {
|
|
9518
|
-
return api.post("/api/posts", body);
|
|
9519
|
-
}
|
|
9520
|
-
async function updatePost(uid, id, body) {
|
|
9521
|
-
return api.patch(`/api/posts/${encodeURIComponent(id)}`, body);
|
|
9522
|
-
}
|
|
9523
|
-
async function deletePost(uid, id) {
|
|
9524
|
-
await api.del(`/api/posts/${encodeURIComponent(id)}`);
|
|
9525
|
-
}
|
|
9526
9651
|
|
|
9527
9652
|
// src/cli/services/comments.ts
|
|
9653
|
+
init_api_client();
|
|
9528
9654
|
async function listComments(postId, opts) {
|
|
9529
9655
|
return api.get(`/api/posts/${encodeURIComponent(postId)}/comments`, {
|
|
9530
9656
|
cursor: opts.cursor,
|
|
9531
9657
|
limit: opts.limit?.toString()
|
|
9532
9658
|
});
|
|
9533
9659
|
}
|
|
9534
|
-
async function createComment(uid, postId, text) {
|
|
9535
|
-
return api.post(`/api/posts/${encodeURIComponent(postId)}/comments`, { text });
|
|
9536
|
-
}
|
|
9537
|
-
async function deleteComment(uid, postId, commentId) {
|
|
9538
|
-
await api.del(`/api/posts/${encodeURIComponent(postId)}/comments/${encodeURIComponent(commentId)}`);
|
|
9539
|
-
}
|
|
9540
9660
|
|
|
9541
9661
|
// src/cli/services/replies.ts
|
|
9662
|
+
init_api_client();
|
|
9542
9663
|
async function listReplies(postId, commentId, opts) {
|
|
9543
9664
|
return api.get(`/api/posts/${encodeURIComponent(postId)}/comments/${encodeURIComponent(commentId)}/replies`, {
|
|
9544
9665
|
cursor: opts.cursor,
|
|
9545
9666
|
limit: opts.limit?.toString()
|
|
9546
9667
|
});
|
|
9547
9668
|
}
|
|
9548
|
-
async function createReply(uid, postId, commentId, text) {
|
|
9549
|
-
return api.post(`/api/posts/${encodeURIComponent(postId)}/comments/${encodeURIComponent(commentId)}/replies`, { text });
|
|
9550
|
-
}
|
|
9551
|
-
async function deleteReply(uid, postId, commentId, replyId) {
|
|
9552
|
-
await api.del(`/api/posts/${encodeURIComponent(postId)}/comments/${encodeURIComponent(commentId)}/replies/${encodeURIComponent(replyId)}`);
|
|
9553
|
-
}
|
|
9554
|
-
|
|
9555
|
-
// src/cli/types/api.ts
|
|
9556
|
-
var POST_TAGS = ["general", "hack", "story", "meme", "other", "question", "hack-tip", "activity"];
|
|
9557
9669
|
|
|
9558
9670
|
// src/cli/commands/posts.ts
|
|
9559
9671
|
init_prompts();
|
|
9560
|
-
init_tty();
|
|
9561
|
-
async function pickComment(postId, commentId) {
|
|
9562
|
-
if (commentId) return commentId;
|
|
9563
|
-
if (!isInteractive()) throw new Error("Missing required argument: commentId. Use flags in non-interactive mode.");
|
|
9564
|
-
const { comments } = await listComments(postId, { limit: 50 });
|
|
9565
|
-
if (comments.length === 0) throw new Error("No comments on this post.");
|
|
9566
|
-
return promptSelect({
|
|
9567
|
-
message: "Select comment",
|
|
9568
|
-
options: comments.map((c) => ({
|
|
9569
|
-
value: c.id,
|
|
9570
|
-
label: `${truncate(c.text ?? "", 60)} ${import_picocolors10.default.dim(c.authorName ?? c.userId ?? "")}`
|
|
9571
|
-
}))
|
|
9572
|
-
});
|
|
9573
|
-
}
|
|
9574
|
-
async function pickReply(postId, commentId, replyId) {
|
|
9575
|
-
if (replyId) return replyId;
|
|
9576
|
-
if (!isInteractive()) throw new Error("Missing required argument: replyId. Use flags in non-interactive mode.");
|
|
9577
|
-
const { replies } = await listReplies(postId, commentId, { limit: 50 });
|
|
9578
|
-
if (replies.length === 0) throw new Error("No replies on this comment.");
|
|
9579
|
-
return promptSelect({
|
|
9580
|
-
message: "Select reply",
|
|
9581
|
-
options: replies.map((r) => ({
|
|
9582
|
-
value: r.id,
|
|
9583
|
-
label: `${truncate(r.text ?? "", 60)} ${import_picocolors10.default.dim(r.userId ?? "")}`
|
|
9584
|
-
}))
|
|
9585
|
-
});
|
|
9586
|
-
}
|
|
9587
9672
|
function printPostLine(p) {
|
|
9588
|
-
const tag =
|
|
9673
|
+
const tag = import_picocolors9.default.cyan(p.tag ?? "");
|
|
9589
9674
|
const title = truncate(p.title, 55);
|
|
9590
|
-
const
|
|
9675
|
+
const author = p.authorName ?? p.authorId;
|
|
9676
|
+
const comments = p.commentsCount ? import_picocolors9.default.dim(`${p.commentsCount} comments`) : "";
|
|
9677
|
+
const likes = p.likesCount ? import_picocolors9.default.dim(`${p.likesCount} likes`) : "";
|
|
9591
9678
|
const time = formatRelativeDate(p.createdAt);
|
|
9592
|
-
const id =
|
|
9593
|
-
const parts = [tag,
|
|
9679
|
+
const id = import_picocolors9.default.dim(p.id);
|
|
9680
|
+
const parts = [tag, import_picocolors9.default.bold(title)];
|
|
9681
|
+
if (author) parts.push(import_picocolors9.default.dim(`by ${author}`));
|
|
9594
9682
|
if (comments) parts.push(comments);
|
|
9595
|
-
parts.push(
|
|
9683
|
+
if (likes) parts.push(likes);
|
|
9684
|
+
parts.push(import_picocolors9.default.dim(time));
|
|
9596
9685
|
parts.push(id);
|
|
9597
9686
|
console.log(" " + parts.join(" "));
|
|
9598
9687
|
}
|
|
9599
9688
|
function printPostDetail(p) {
|
|
9600
9689
|
console.log("");
|
|
9601
|
-
console.log(` ${
|
|
9690
|
+
console.log(` ${import_picocolors9.default.bold(p.title)}`);
|
|
9602
9691
|
if (p.body) {
|
|
9603
|
-
console.log(` ${
|
|
9692
|
+
console.log(` ${import_picocolors9.default.dim(SYM.dash.repeat(40))}`);
|
|
9604
9693
|
console.log(` ${p.body}`);
|
|
9605
9694
|
}
|
|
9606
|
-
console.log(` ${
|
|
9695
|
+
console.log(` ${import_picocolors9.default.dim(SYM.dash.repeat(40))}`);
|
|
9607
9696
|
printRecord([
|
|
9608
|
-
["ID",
|
|
9609
|
-
["Tag",
|
|
9697
|
+
["ID", import_picocolors9.default.dim(p.id)],
|
|
9698
|
+
["Tag", import_picocolors9.default.cyan(p.tag ?? "")],
|
|
9610
9699
|
["Author", p.authorName ?? p.authorId],
|
|
9611
9700
|
["Comments", p.commentsCount != null ? String(p.commentsCount) : null],
|
|
9612
9701
|
["Likes", p.likesCount != null ? String(p.likesCount) : null],
|
|
@@ -9615,9 +9704,9 @@ function printPostDetail(p) {
|
|
|
9615
9704
|
console.log("");
|
|
9616
9705
|
}
|
|
9617
9706
|
function printCommentLine(c) {
|
|
9618
|
-
const author =
|
|
9619
|
-
const time =
|
|
9620
|
-
const replies = c.repliesCount ?
|
|
9707
|
+
const author = import_picocolors9.default.bold(c.authorName ?? c.userId ?? "");
|
|
9708
|
+
const time = import_picocolors9.default.dim(formatRelativeDate(c.createdAt));
|
|
9709
|
+
const replies = c.repliesCount ? import_picocolors9.default.dim(`\xB7 ${c.repliesCount} replies`) : "";
|
|
9621
9710
|
const text = c.text ?? "";
|
|
9622
9711
|
console.log(` ${author} ${time}${replies}`);
|
|
9623
9712
|
console.log(` ${text}`);
|
|
@@ -9626,86 +9715,77 @@ function printCommentLine(c) {
|
|
|
9626
9715
|
function printReplyLine(r) {
|
|
9627
9716
|
const text = truncate(r.text ?? "", 60);
|
|
9628
9717
|
const time = formatRelativeDate(r.createdAt);
|
|
9629
|
-
const id =
|
|
9630
|
-
console.log(` ${text} ${
|
|
9718
|
+
const id = import_picocolors9.default.dim(r.id);
|
|
9719
|
+
console.log(` ${text} ${import_picocolors9.default.dim(time)} ${id}`);
|
|
9631
9720
|
}
|
|
9632
9721
|
function registerPostsCommands(program3) {
|
|
9633
|
-
const posts = program3.command("posts").description("
|
|
9722
|
+
const posts = program3.command("posts").description("Browse community posts and comments");
|
|
9634
9723
|
posts.command("list").description("List posts").option("--cursor <cursor>").option("--limit <n>", "Max results (<=50)", "20").action(async function() {
|
|
9635
9724
|
const opts = this.optsWithGlobals();
|
|
9636
|
-
const limit = parseInt(opts.limit);
|
|
9637
9725
|
await runList({
|
|
9638
9726
|
global: opts,
|
|
9639
|
-
fn: () => listPosts({ cursor: opts.cursor, limit }),
|
|
9727
|
+
fn: () => listPosts({ cursor: opts.cursor, limit: opts.limit ? parseInt(opts.limit) : void 0 }),
|
|
9640
9728
|
dataKey: "posts",
|
|
9641
|
-
columns: ["id", "title", "
|
|
9729
|
+
columns: ["id", "tag", "title", "authorName", "commentsCount", "likesCount", "createdAt"],
|
|
9642
9730
|
spinnerMessage: "Fetching posts...",
|
|
9643
9731
|
onInteractive: (payload) => {
|
|
9644
9732
|
const items = payload.posts;
|
|
9645
9733
|
if (items.length === 0) {
|
|
9646
|
-
console.log(
|
|
9734
|
+
console.log(import_picocolors9.default.dim(" No posts found."));
|
|
9647
9735
|
return;
|
|
9648
9736
|
}
|
|
9649
|
-
console.log(
|
|
9650
|
-
|
|
9651
|
-
|
|
9652
|
-
for (const p of items) {
|
|
9653
|
-
printPostLine(p);
|
|
9654
|
-
}
|
|
9737
|
+
console.log("");
|
|
9738
|
+
for (const p of items) printPostLine(p);
|
|
9739
|
+
console.log("");
|
|
9655
9740
|
printPaginationHint({
|
|
9656
9741
|
nextCursor: payload.nextCursor,
|
|
9657
9742
|
command: "posts list",
|
|
9658
9743
|
limit: opts.limit
|
|
9659
9744
|
});
|
|
9660
|
-
console.log("");
|
|
9661
9745
|
}
|
|
9662
9746
|
});
|
|
9663
|
-
})
|
|
9664
|
-
|
|
9665
|
-
|
|
9666
|
-
$ numo posts list --limit 10
|
|
9667
|
-
$ numo posts list --json | jq '.posts[].title'`);
|
|
9668
|
-
posts.command("get [id]").description("Get a post by ID").action(async function(id) {
|
|
9747
|
+
});
|
|
9748
|
+
posts.command("get [id]").description("Get post details").action(async function(id) {
|
|
9749
|
+
const opts = this.optsWithGlobals();
|
|
9669
9750
|
const postId = await promptForMissing({ value: id, message: "Post ID" });
|
|
9670
9751
|
await runGet({
|
|
9671
|
-
global:
|
|
9752
|
+
global: opts,
|
|
9672
9753
|
fn: () => getPost(postId),
|
|
9673
9754
|
spinnerMessage: "Fetching post...",
|
|
9674
|
-
onInteractive: printPostDetail
|
|
9755
|
+
onInteractive: (post) => printPostDetail(post)
|
|
9675
9756
|
});
|
|
9676
|
-
})
|
|
9677
|
-
|
|
9678
|
-
$ numo posts get abc123
|
|
9679
|
-
$ numo posts get abc123 --json`);
|
|
9680
|
-
posts.command("comments [postId]").description("List comments on a post").option("--cursor <cursor>").action(async function(postId) {
|
|
9757
|
+
});
|
|
9758
|
+
posts.command("comments [postId]").description("List comments on a post").option("--cursor <cursor>").option("--limit <n>", "Max results (<=50)", "20").action(async function(postId) {
|
|
9681
9759
|
const opts = this.optsWithGlobals();
|
|
9682
9760
|
const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
|
|
9683
9761
|
await runList({
|
|
9684
9762
|
global: opts,
|
|
9685
|
-
fn: () => listComments(resolvedPostId, { cursor: opts.cursor }),
|
|
9763
|
+
fn: () => listComments(resolvedPostId, { cursor: opts.cursor, limit: opts.limit ? parseInt(opts.limit) : void 0 }),
|
|
9686
9764
|
dataKey: "comments",
|
|
9687
|
-
columns: ["id", "userId", "text", "createdAt"],
|
|
9765
|
+
columns: ["id", "userId", "authorName", "text", "repliesCount", "createdAt"],
|
|
9688
9766
|
spinnerMessage: "Fetching comments...",
|
|
9689
9767
|
onInteractive: (payload) => {
|
|
9690
9768
|
const items = payload.comments;
|
|
9691
9769
|
if (items.length === 0) {
|
|
9692
|
-
console.log(
|
|
9770
|
+
console.log(import_picocolors9.default.dim(" No comments yet."));
|
|
9693
9771
|
return;
|
|
9694
9772
|
}
|
|
9695
9773
|
console.log(`
|
|
9696
|
-
${
|
|
9774
|
+
${import_picocolors9.default.bold("Comments")} ${import_picocolors9.default.dim(`(${items.length})`)}
|
|
9697
9775
|
`);
|
|
9698
|
-
for (const c of items)
|
|
9699
|
-
|
|
9700
|
-
|
|
9701
|
-
|
|
9776
|
+
for (const c of items) printCommentLine(c);
|
|
9777
|
+
printPaginationHint({
|
|
9778
|
+
nextCursor: payload.nextCursor,
|
|
9779
|
+
command: `posts comments ${resolvedPostId}`,
|
|
9780
|
+
limit: opts.limit
|
|
9781
|
+
});
|
|
9702
9782
|
}
|
|
9703
9783
|
});
|
|
9704
9784
|
});
|
|
9705
|
-
posts.command("replies [postId] [commentId]").description("List replies
|
|
9785
|
+
posts.command("replies [postId] [commentId]").description("List replies on a comment").option("--cursor <cursor>").option("--limit <n>", "Max results (<=50)", "20").action(async function(postId, commentId) {
|
|
9706
9786
|
const opts = this.optsWithGlobals();
|
|
9707
9787
|
const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
|
|
9708
|
-
const resolvedCommentId = await
|
|
9788
|
+
const resolvedCommentId = await promptForMissing({ value: commentId, message: "Comment ID" });
|
|
9709
9789
|
await runList({
|
|
9710
9790
|
global: opts,
|
|
9711
9791
|
fn: () => listReplies(resolvedPostId, resolvedCommentId, { cursor: opts.cursor }),
|
|
@@ -9715,11 +9795,11 @@ Examples:
|
|
|
9715
9795
|
onInteractive: (payload) => {
|
|
9716
9796
|
const items = payload.replies;
|
|
9717
9797
|
if (items.length === 0) {
|
|
9718
|
-
console.log(
|
|
9798
|
+
console.log(import_picocolors9.default.dim(" No replies yet."));
|
|
9719
9799
|
return;
|
|
9720
9800
|
}
|
|
9721
9801
|
console.log(`
|
|
9722
|
-
${
|
|
9802
|
+
${import_picocolors9.default.bold("Replies")} ${import_picocolors9.default.dim(`(${items.length})`)}
|
|
9723
9803
|
`);
|
|
9724
9804
|
for (const r of items) {
|
|
9725
9805
|
printReplyLine(r);
|
|
@@ -9728,132 +9808,10 @@ Examples:
|
|
|
9728
9808
|
}
|
|
9729
9809
|
});
|
|
9730
9810
|
});
|
|
9731
|
-
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() {
|
|
9732
|
-
const opts = this.optsWithGlobals();
|
|
9733
|
-
const uid = requireUid();
|
|
9734
|
-
const title = await promptForMissing({ value: opts.title, message: "Title", placeholder: "Post title" });
|
|
9735
|
-
const postBody = await promptForMissing({ value: opts.body, message: "Body", placeholder: "Post body" });
|
|
9736
|
-
let tag = opts.tag;
|
|
9737
|
-
if (!tag) {
|
|
9738
|
-
tag = await promptSelect({
|
|
9739
|
-
message: "Tag",
|
|
9740
|
-
options: POST_TAGS.map((t2) => ({ value: t2, label: t2 }))
|
|
9741
|
-
});
|
|
9742
|
-
}
|
|
9743
|
-
const body = { title, body: postBody, tag };
|
|
9744
|
-
await runCreate({
|
|
9745
|
-
global: opts,
|
|
9746
|
-
fn: () => createPost(uid, body),
|
|
9747
|
-
dataKey: "post",
|
|
9748
|
-
spinnerMessage: "Creating post...",
|
|
9749
|
-
onInteractive: (post, payload) => {
|
|
9750
|
-
console.log(`
|
|
9751
|
-
${import_picocolors10.default.green("Posted!")} ${payload.post.title} ${import_picocolors10.default.dim(payload.post.id)}
|
|
9752
|
-
`);
|
|
9753
|
-
}
|
|
9754
|
-
});
|
|
9755
|
-
});
|
|
9756
|
-
posts.command("update [id]").description("Update a post").option("--title <title>").option("--body <body>").option("--tag <tag>").action(async function(id) {
|
|
9757
|
-
const opts = this.optsWithGlobals();
|
|
9758
|
-
const uid = requireUid();
|
|
9759
|
-
const postId = await promptForMissing({ value: id, message: "Post ID" });
|
|
9760
|
-
const body = {};
|
|
9761
|
-
const hasAnyFlag = opts.title || opts.body || opts.tag;
|
|
9762
|
-
if (!hasAnyFlag && isInteractive() && !opts.json) {
|
|
9763
|
-
const title = await promptText({ message: "Title (enter to skip)", required: false });
|
|
9764
|
-
if (title) body.title = title;
|
|
9765
|
-
const postBody = await promptText({ message: "Body (enter to skip)", required: false });
|
|
9766
|
-
if (postBody) body.body = postBody;
|
|
9767
|
-
const changeTag = await promptText({ message: "Tag (enter to skip)", placeholder: POST_TAGS.join("|"), required: false });
|
|
9768
|
-
if (changeTag) body.tag = changeTag;
|
|
9769
|
-
} else {
|
|
9770
|
-
if (opts.title) body.title = opts.title;
|
|
9771
|
-
if (opts.body) body.body = opts.body;
|
|
9772
|
-
if (opts.tag) body.tag = opts.tag;
|
|
9773
|
-
}
|
|
9774
|
-
await runWrite({
|
|
9775
|
-
global: opts,
|
|
9776
|
-
fn: () => updatePost(uid, postId, body),
|
|
9777
|
-
dataKey: "post",
|
|
9778
|
-
spinnerMessage: "Updating post...",
|
|
9779
|
-
onInteractive: (payload) => {
|
|
9780
|
-
console.log(`
|
|
9781
|
-
${import_picocolors10.default.green("Updated!")} ${payload.post.title} ${import_picocolors10.default.dim(payload.post.id)}
|
|
9782
|
-
`);
|
|
9783
|
-
}
|
|
9784
|
-
});
|
|
9785
|
-
});
|
|
9786
|
-
posts.command("delete [id]").description("Delete a post").action(async function(id) {
|
|
9787
|
-
const uid = requireUid();
|
|
9788
|
-
const postId = await promptForMissing({ value: id, message: "Post ID" });
|
|
9789
|
-
await runDelete({
|
|
9790
|
-
global: this.optsWithGlobals(),
|
|
9791
|
-
fn: () => deletePost(uid, postId),
|
|
9792
|
-
successMessage: ` ${import_picocolors10.default.green("Deleted!")} Post ${import_picocolors10.default.dim(postId)}`,
|
|
9793
|
-
spinnerMessage: "Deleting post..."
|
|
9794
|
-
});
|
|
9795
|
-
});
|
|
9796
|
-
posts.command("comment [postId]").description("Add a comment to a post").option("--text <text>").action(async function(postId) {
|
|
9797
|
-
const opts = this.optsWithGlobals();
|
|
9798
|
-
const uid = requireUid();
|
|
9799
|
-
const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
|
|
9800
|
-
const text = await promptForMissing({ value: opts.text, message: "Comment text", placeholder: "Your comment" });
|
|
9801
|
-
await runCreate({
|
|
9802
|
-
global: opts,
|
|
9803
|
-
fn: () => createComment(uid, resolvedPostId, text),
|
|
9804
|
-
dataKey: "comment",
|
|
9805
|
-
spinnerMessage: "Adding comment...",
|
|
9806
|
-
onInteractive: (comment, payload) => {
|
|
9807
|
-
console.log(`
|
|
9808
|
-
${import_picocolors10.default.green("Commented!")} ${truncate(payload.comment.text ?? "", 50)} ${import_picocolors10.default.dim(payload.comment.id)}
|
|
9809
|
-
`);
|
|
9810
|
-
}
|
|
9811
|
-
});
|
|
9812
|
-
});
|
|
9813
|
-
posts.command("comment-delete [postId] [commentId]").description("Delete a comment").action(async function(postId, commentId) {
|
|
9814
|
-
const uid = requireUid();
|
|
9815
|
-
const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
|
|
9816
|
-
const resolvedCommentId = await pickComment(resolvedPostId, commentId);
|
|
9817
|
-
await runDelete({
|
|
9818
|
-
global: this.optsWithGlobals(),
|
|
9819
|
-
fn: () => deleteComment(uid, resolvedPostId, resolvedCommentId),
|
|
9820
|
-
successMessage: ` ${import_picocolors10.default.green("Deleted!")} Comment ${import_picocolors10.default.dim(resolvedCommentId)}`,
|
|
9821
|
-
spinnerMessage: "Deleting comment..."
|
|
9822
|
-
});
|
|
9823
|
-
});
|
|
9824
|
-
posts.command("reply [postId] [commentId]").description("Add a reply to a comment").option("--text <text>").action(async function(postId, commentId) {
|
|
9825
|
-
const opts = this.optsWithGlobals();
|
|
9826
|
-
const uid = requireUid();
|
|
9827
|
-
const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
|
|
9828
|
-
const resolvedCommentId = await pickComment(resolvedPostId, commentId);
|
|
9829
|
-
const text = await promptForMissing({ value: opts.text, message: "Reply text", placeholder: "Your reply" });
|
|
9830
|
-
await runCreate({
|
|
9831
|
-
global: opts,
|
|
9832
|
-
fn: () => createReply(uid, resolvedPostId, resolvedCommentId, text),
|
|
9833
|
-
dataKey: "reply",
|
|
9834
|
-
spinnerMessage: "Adding reply...",
|
|
9835
|
-
onInteractive: (reply, payload) => {
|
|
9836
|
-
console.log(`
|
|
9837
|
-
${import_picocolors10.default.green("Replied!")} ${truncate(payload.reply.text ?? "", 50)} ${import_picocolors10.default.dim(payload.reply.id)}
|
|
9838
|
-
`);
|
|
9839
|
-
}
|
|
9840
|
-
});
|
|
9841
|
-
});
|
|
9842
|
-
posts.command("reply-delete [postId] [commentId] [replyId]").description("Delete a reply").action(async function(postId, commentId, replyId) {
|
|
9843
|
-
const uid = requireUid();
|
|
9844
|
-
const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
|
|
9845
|
-
const resolvedCommentId = await pickComment(resolvedPostId, commentId);
|
|
9846
|
-
const resolvedReplyId = await pickReply(resolvedPostId, resolvedCommentId, replyId);
|
|
9847
|
-
await runDelete({
|
|
9848
|
-
global: this.optsWithGlobals(),
|
|
9849
|
-
fn: () => deleteReply(uid, resolvedPostId, resolvedCommentId, resolvedReplyId),
|
|
9850
|
-
successMessage: ` ${import_picocolors10.default.green("Deleted!")} Reply ${import_picocolors10.default.dim(resolvedReplyId)}`,
|
|
9851
|
-
spinnerMessage: "Deleting reply..."
|
|
9852
|
-
});
|
|
9853
|
-
});
|
|
9854
9811
|
}
|
|
9855
9812
|
|
|
9856
9813
|
// src/cli/services/profile.ts
|
|
9814
|
+
init_api_client();
|
|
9857
9815
|
async function getProfile() {
|
|
9858
9816
|
return api.get("/api/profile");
|
|
9859
9817
|
}
|
|
@@ -9879,9 +9837,47 @@ function registerProfileCommands(program3) {
|
|
|
9879
9837
|
}
|
|
9880
9838
|
|
|
9881
9839
|
// src/cli/commands/doctor.ts
|
|
9882
|
-
var
|
|
9883
|
-
|
|
9884
|
-
var
|
|
9840
|
+
var import_picocolors10 = __toESM(require_picocolors(), 1);
|
|
9841
|
+
var import_dns = require("dns");
|
|
9842
|
+
var import_net = require("net");
|
|
9843
|
+
var import_tls = __toESM(require("tls"), 1);
|
|
9844
|
+
init_credentials();
|
|
9845
|
+
init_api_client();
|
|
9846
|
+
init_api_base();
|
|
9847
|
+
init_errors();
|
|
9848
|
+
function errMessage(err) {
|
|
9849
|
+
return sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
|
|
9850
|
+
}
|
|
9851
|
+
async function checkDns(hostname) {
|
|
9852
|
+
if ((0, import_net.isIP)(hostname) !== 0 || hostname === "localhost") {
|
|
9853
|
+
return { name: "dns", status: "ok", message: `DNS skipped (${hostname} is a literal address)` };
|
|
9854
|
+
}
|
|
9855
|
+
try {
|
|
9856
|
+
const addrs = await import_dns.promises.resolve(hostname);
|
|
9857
|
+
return { name: "dns", status: "ok", message: `DNS ${hostname} \u2192 ${addrs[0]}` };
|
|
9858
|
+
} catch (err) {
|
|
9859
|
+
return { name: "dns", status: "fail", message: `DNS lookup failed for ${hostname}: ${errMessage(err)}` };
|
|
9860
|
+
}
|
|
9861
|
+
}
|
|
9862
|
+
async function checkTls(hostname) {
|
|
9863
|
+
return new Promise((resolve) => {
|
|
9864
|
+
const socket = import_tls.default.connect(
|
|
9865
|
+
{ host: hostname, port: 443, timeout: 5e3, servername: hostname },
|
|
9866
|
+
() => {
|
|
9867
|
+
const proto = socket.getProtocol() ?? "unknown";
|
|
9868
|
+
socket.end();
|
|
9869
|
+
resolve({ name: "tls", status: "ok", message: `TLS handshake OK (${proto})` });
|
|
9870
|
+
}
|
|
9871
|
+
);
|
|
9872
|
+
socket.on("error", (err) => {
|
|
9873
|
+
resolve({ name: "tls", status: "fail", message: `TLS handshake failed: ${errMessage(err)}` });
|
|
9874
|
+
});
|
|
9875
|
+
socket.on("timeout", () => {
|
|
9876
|
+
socket.destroy();
|
|
9877
|
+
resolve({ name: "tls", status: "fail", message: "TLS handshake timed out after 5s" });
|
|
9878
|
+
});
|
|
9879
|
+
});
|
|
9880
|
+
}
|
|
9885
9881
|
async function runChecks() {
|
|
9886
9882
|
const checks = [];
|
|
9887
9883
|
const nodeVersion = process.version;
|
|
@@ -9891,68 +9887,128 @@ async function runChecks() {
|
|
|
9891
9887
|
status: major >= 18 ? "ok" : "fail",
|
|
9892
9888
|
message: major >= 18 ? `Node ${nodeVersion}` : `Node ${nodeVersion} \u2014 requires >= 18`
|
|
9893
9889
|
});
|
|
9890
|
+
const verdict = classifyApiBase();
|
|
9891
|
+
if (!verdict.ok) {
|
|
9892
|
+
checks.push({
|
|
9893
|
+
name: "api_url",
|
|
9894
|
+
status: "fail",
|
|
9895
|
+
message: verdict.message,
|
|
9896
|
+
fix: "Use https://api.numo.ai, or export NUMO_ALLOW_CUSTOM_HOST=1 for a self-hosted server"
|
|
9897
|
+
});
|
|
9898
|
+
return checks;
|
|
9899
|
+
}
|
|
9900
|
+
const apiUrl = new URL(API_BASE);
|
|
9894
9901
|
checks.push({
|
|
9895
9902
|
name: "api_url",
|
|
9896
|
-
status: process.env.NUMO_API_URL ? "ok" : "warn",
|
|
9897
|
-
message: process.env.NUMO_API_URL ? `API URL: ${
|
|
9903
|
+
status: verdict.insecure ? "warn" : process.env.NUMO_API_URL ? "ok" : "warn",
|
|
9904
|
+
message: verdict.insecure ? `API URL: ${API_BASE} (HTTP \u2014 tokens unencrypted)` : process.env.NUMO_API_URL ? `API URL: ${API_BASE}` : `NUMO_API_URL not set (using default: ${API_BASE})`
|
|
9898
9905
|
});
|
|
9906
|
+
checks.push(await checkDns(apiUrl.hostname));
|
|
9907
|
+
if (apiUrl.protocol === "https:") {
|
|
9908
|
+
checks.push(await checkTls(apiUrl.hostname));
|
|
9909
|
+
}
|
|
9899
9910
|
const creds = loadCredentials();
|
|
9900
9911
|
checks.push({
|
|
9901
9912
|
name: "credentials",
|
|
9902
9913
|
status: creds ? "ok" : "fail",
|
|
9903
|
-
message: creds ? `Logged in as ${creds.email}` : "Not logged in
|
|
9914
|
+
message: creds ? `Logged in as ${creds.email}` : "Not logged in",
|
|
9915
|
+
fix: creds ? void 0 : "numo login"
|
|
9904
9916
|
});
|
|
9905
9917
|
if (creds) {
|
|
9906
9918
|
try {
|
|
9907
9919
|
await getIdToken();
|
|
9908
9920
|
checks.push({ name: "token", status: "ok", message: "Token valid / refreshed" });
|
|
9909
9921
|
} catch (err) {
|
|
9910
|
-
checks.push({
|
|
9922
|
+
checks.push({
|
|
9923
|
+
name: "token",
|
|
9924
|
+
status: "fail",
|
|
9925
|
+
message: `Token refresh failed: ${errMessage(err)}`,
|
|
9926
|
+
fix: "numo login"
|
|
9927
|
+
});
|
|
9911
9928
|
}
|
|
9912
9929
|
} else {
|
|
9913
9930
|
checks.push({ name: "token", status: "fail", message: "Skipped (no credentials)" });
|
|
9914
9931
|
}
|
|
9915
9932
|
try {
|
|
9916
|
-
const resp = await fetch(`${
|
|
9917
|
-
checks.push({
|
|
9933
|
+
const resp = await fetch(`${API_BASE}/api/health`, { signal: AbortSignal.timeout(5e3) });
|
|
9934
|
+
checks.push({
|
|
9935
|
+
name: "api_reachable",
|
|
9936
|
+
status: resp.ok ? "ok" : "fail",
|
|
9937
|
+
message: `API /api/health \u2192 HTTP ${resp.status}`,
|
|
9938
|
+
fix: resp.ok ? void 0 : "Check NUMO_API_URL and your network connection"
|
|
9939
|
+
});
|
|
9918
9940
|
} catch (err) {
|
|
9919
|
-
checks.push({
|
|
9941
|
+
checks.push({
|
|
9942
|
+
name: "api_reachable",
|
|
9943
|
+
status: "fail",
|
|
9944
|
+
message: `API server unreachable: ${errMessage(err)}`,
|
|
9945
|
+
fix: "Check NUMO_API_URL and your network connection"
|
|
9946
|
+
});
|
|
9947
|
+
}
|
|
9948
|
+
if (creds) {
|
|
9949
|
+
try {
|
|
9950
|
+
const token = await getIdToken();
|
|
9951
|
+
const resp = await fetch(`${API_BASE}/api/tasks?backlog=true`, {
|
|
9952
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
9953
|
+
signal: AbortSignal.timeout(5e3)
|
|
9954
|
+
});
|
|
9955
|
+
checks.push({
|
|
9956
|
+
name: "auth",
|
|
9957
|
+
status: resp.ok ? "ok" : "fail",
|
|
9958
|
+
message: resp.ok ? `Authenticated request OK (HTTP ${resp.status})` : `Authenticated request failed: HTTP ${resp.status}`,
|
|
9959
|
+
fix: resp.ok ? void 0 : "numo login"
|
|
9960
|
+
});
|
|
9961
|
+
} catch (err) {
|
|
9962
|
+
checks.push({
|
|
9963
|
+
name: "auth",
|
|
9964
|
+
status: "fail",
|
|
9965
|
+
message: `Authenticated request error: ${errMessage(err)}`,
|
|
9966
|
+
fix: "numo login"
|
|
9967
|
+
});
|
|
9968
|
+
}
|
|
9920
9969
|
}
|
|
9921
9970
|
return checks;
|
|
9922
9971
|
}
|
|
9923
9972
|
function registerDoctorCommand(program3) {
|
|
9924
9973
|
program3.command("doctor").description("Check CLI health and connectivity").action(async function() {
|
|
9925
9974
|
const opts = this.optsWithGlobals();
|
|
9926
|
-
const asJson =
|
|
9975
|
+
const asJson = isQuietMode(opts);
|
|
9927
9976
|
const checks = await runChecks();
|
|
9928
9977
|
const ok = checks.every((c) => c.status !== "fail");
|
|
9929
9978
|
if (asJson) {
|
|
9930
|
-
printJson({ ok, checks });
|
|
9979
|
+
printJson({ ok, exitCode: ok ? 0 : 1, checks });
|
|
9931
9980
|
} else {
|
|
9932
9981
|
console.log("");
|
|
9933
9982
|
for (const check of checks) {
|
|
9934
|
-
const icon = check.status === "ok" ?
|
|
9983
|
+
const icon = check.status === "ok" ? import_picocolors10.default.green(SYM.check) : check.status === "warn" ? import_picocolors10.default.yellow("!") : import_picocolors10.default.red(SYM.cross);
|
|
9935
9984
|
console.log(` ${icon} ${check.message}`);
|
|
9985
|
+
if (check.fix) {
|
|
9986
|
+
console.log(` ${import_picocolors10.default.dim("Fix:")} ${import_picocolors10.default.cyan("$")} ${import_picocolors10.default.bold(check.fix)}`);
|
|
9987
|
+
}
|
|
9936
9988
|
}
|
|
9937
9989
|
console.log("");
|
|
9938
|
-
|
|
9939
|
-
console.log(` ${import_picocolors11.default.green("All checks passed.")}`);
|
|
9940
|
-
} else {
|
|
9941
|
-
console.log(` ${import_picocolors11.default.red("Some checks failed.")}`);
|
|
9942
|
-
}
|
|
9990
|
+
console.log(` ${ok ? import_picocolors10.default.green("All checks passed.") : import_picocolors10.default.red("Some checks failed.")}`);
|
|
9943
9991
|
console.log("");
|
|
9944
9992
|
}
|
|
9945
9993
|
if (!ok) process.exit(1);
|
|
9946
9994
|
});
|
|
9947
9995
|
}
|
|
9948
9996
|
|
|
9997
|
+
// src/cli/cli.ts
|
|
9998
|
+
init_dirs();
|
|
9999
|
+
|
|
9949
10000
|
// src/cli/lib/update-check.ts
|
|
9950
10001
|
var fs4 = __toESM(require("fs"), 1);
|
|
9951
10002
|
var path2 = __toESM(require("path"), 1);
|
|
9952
|
-
var
|
|
10003
|
+
var import_picocolors11 = __toESM(require_picocolors(), 1);
|
|
10004
|
+
init_dirs();
|
|
9953
10005
|
init_tty();
|
|
9954
10006
|
var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
|
|
9955
10007
|
var PACKAGE_NAME = "numo-cli";
|
|
10008
|
+
var REPO = "mindistio/numo-cli";
|
|
10009
|
+
var INSTALL_SCRIPT_URL = `https://raw.githubusercontent.com/${REPO}/main/install.sh`;
|
|
10010
|
+
var UPGRADE_NPM = `npm i -g ${PACKAGE_NAME}`;
|
|
10011
|
+
var UPGRADE_BINARY = `curl -fsSL ${INSTALL_SCRIPT_URL} | bash`;
|
|
9956
10012
|
function getStatePath() {
|
|
9957
10013
|
return path2.join(getConfigDir(), "update-check.json");
|
|
9958
10014
|
}
|
|
@@ -9979,6 +10035,12 @@ function semverGt(a, b) {
|
|
|
9979
10035
|
}
|
|
9980
10036
|
return false;
|
|
9981
10037
|
}
|
|
10038
|
+
function isBinaryInstall() {
|
|
10039
|
+
return !!process.versions.bun;
|
|
10040
|
+
}
|
|
10041
|
+
function upgradeCommand(isBinary = isBinaryInstall()) {
|
|
10042
|
+
return isBinary ? UPGRADE_BINARY : UPGRADE_NPM;
|
|
10043
|
+
}
|
|
9982
10044
|
function checkForUpdate(currentVersion) {
|
|
9983
10045
|
if (!isInteractive()) return;
|
|
9984
10046
|
if (process.env.CI || process.env.NUMO_NO_UPDATE_CHECK) return;
|
|
@@ -9987,8 +10049,8 @@ function checkForUpdate(currentVersion) {
|
|
|
9987
10049
|
if (state.latestVersion && semverGt(state.latestVersion, currentVersion)) {
|
|
9988
10050
|
process.stderr.write(
|
|
9989
10051
|
`
|
|
9990
|
-
${
|
|
9991
|
-
Run ${
|
|
10052
|
+
${import_picocolors11.default.yellow("Update available")} ${import_picocolors11.default.dim(currentVersion)} ${import_picocolors11.default.dim("\u2192")} ${import_picocolors11.default.green(state.latestVersion)}
|
|
10053
|
+
Run ${import_picocolors11.default.cyan(upgradeCommand())} to update
|
|
9992
10054
|
|
|
9993
10055
|
`
|
|
9994
10056
|
);
|
|
@@ -10012,10 +10074,21 @@ function fetchLatestVersion(state) {
|
|
|
10012
10074
|
}
|
|
10013
10075
|
}
|
|
10014
10076
|
|
|
10077
|
+
// src/cli/lib/guide.ts
|
|
10078
|
+
var import_fs = require("fs");
|
|
10079
|
+
var import_path = require("path");
|
|
10080
|
+
function getAgentGuide() {
|
|
10081
|
+
if (true) return '# AGENTS.md \u2014 numo-cli for AI Agents\n\nInstructions for AI agents (Claude, GPT, Cursor, Copilot, etc.) integrating with [numo-cli](https://www.npmjs.com/package/numo-cli).\n\n## What is numo-cli?\n\n`numo` is the command-line client for the **Numo ADHD planner** \u2014 programmatic access to tasks, community posts, and profiles for any agent that can run shell commands. Invoke it as `numo <command>`; assume it is already installed and on `PATH`.\n\n## Authentication\n\n### Interactive (humans)\n\n```bash\nnumo login\n```\nPrompts for email + password, or use `--phone` for SMS OTP. Credentials are stored locally.\n\n### Non-interactive (agents / CI)\n\n**For long-running sessions (recommended) \u2014 email + password via env vars:**\n\n```bash\nexport NUMO_LOGIN_EMAIL="you@example.com"\nexport NUMO_LOGIN_PASSWORD="..."\nnumo login --json\n# stdout: {"ok":true,"uid":"...","email":"...","idToken":"...","idTokenExpiry":...}\n```\n\nThis path caches credentials locally and auto-refreshes the ID token in the background \u2014 every subsequent `numo` invocation in the session keeps working past the ~1-hour ID-token expiry.\n\n**For one-shot calls \u2014 pre-existing ID token:**\n\n```bash\nexport NUMO_TOKEN="<id-token>"\nnumo tasks list --json\n```\n\n`NUMO_TOKEN` does **not** auto-refresh \u2014 it is treated as opaque, single-use credentials. When the ID token expires (typically ~1 hour after issue), API calls will start returning 401 with no recovery path. Use this only for short scripts; otherwise prefer the email/password path above. Inspect remaining validity with `numo whoami --json` (decodes the JWT `exp` claim and reports `autoRefresh: false`).\n\n**Custom server host:** by default credentials are only sent to `*.numo.ai` (or loopback). To point `NUMO_API_URL` at a self-hosted/staging host, set `NUMO_ALLOW_CUSTOM_HOST=1` \u2014 otherwise credential-sending commands fail with a `CONFIG_ERROR` (exit 78). Offline commands (`commands`, `schema`, `--help`) are never gated.\n\n## JSON Mode\n\nAll commands output JSON when:\n1. stdout is piped (automatic detection)\n2. `--json` flag is passed\n3. `-q` / `--quiet` flag is passed (implies `--json`, suppresses interactive output)\n\n`numo tasks create` defaults to **private** tasks (`isPublic: false`) in every mode \u2014 interactive shells, JSON mode, scripts. Public tasks require an explicit `--public` flag (or picking "Public" in the interactive Visibility prompt). This is a stability guarantee per W-121 \u2014 the CLI must not accidentally expose tasks to the public community feed. New tasks are always inserted at the **top** of the list (`listPosition: \'top\'`).\n\n## Core Commands\n\n### Tasks\n\n```bash\n# List today\'s tasks\nnumo tasks list --json\n# \u2192 {"tasks":[...],"count":N,"pendingCount":N,"completedCount":N}\n\n# List by date (accepts natural language: tomorrow, next monday, in 3 days)\nnumo tasks list --date "next monday" --json\n\n# List backlog (undated tasks)\nnumo tasks list --backlog --json\n\n# Create a task (positional text or --text; private + inserted at top by default)\nnumo tasks create "Buy groceries" --due "2026-04-03" --json\nnumo tasks create --text "Weekly review" --due "next monday" --json\n# \u2192 {"task":{...},"karma":N}\n\n# Quick add (today, private), and recurring routines\nnumo tasks create "Call dentist" --due tomorrow --tags Health --json\nnumo tasks create "Standup" --repeat weekly --weekdays Mon,Wed,Fri --json\nnumo tasks create "Read later" --backlog --json\nnumo tasks create "Trip" --subtask "Book hotel" --subtask "Pack" --json # repeatable --subtask\n\n# Get task details\nnumo tasks get <taskId> --json\n\n# Update a task\nnumo tasks update <taskId> --text "New text" --due "2026-04-05" --json\nnumo tasks update <taskId> --no-time --json # strip time-of-day (all-day task)\n\n# Complete a task\nnumo tasks complete <taskId> --json\n# \u2192 {"completed":true,"task":{...}|null,"taskHistory":{...},"karma":N,"checksInRow":N}\n\n# Uncomplete (restore from history)\nnumo tasks uncomplete <historyId> --json\n\n# Delete a task (archives it)\nnumo tasks delete <taskId> --json\n```\n\n### Recurring reminders (natural-language requests)\n\nA request like *"remind me to plan my week every Monday"* maps to a recurring task. There is no separate "reminder" entity \u2014 a reminder **is** a task with a `remindDate`. Resolve two things the phrasing leaves implicit:\n\n1. **Anchor the first occurrence.** `--repeat weekly --weekdays Mon` alone sets `dueDate` to *today*, so the first instance can land before the intended weekday. Pass `--due "next monday"` to anchor it.\n2. **"Remind" needs a time.** An all-day task has `remindDate: null` (no notification fires). Add a time so the app derives `remindDate` (default lead ~3h before due).\n\n```bash\n# "remind me to plan my week every Monday" \u2192\nnumo tasks create "Plan my week" --due "next monday 09:00" --repeat weekly --weekdays Mon --json\n# \u2192 dueDate 2026-06-22 09:00, remindDate 2026-06-22 06:00, repeat weekly on Mon\n```\n\nPick a sensible default time (e.g. 09:00) or ask the user. `--due` accepts natural language (`"next monday"`, `"next monday 09:00"`).\n\n### Community (read-only)\n\n```bash\nnumo posts list --json # list posts (with commentsCount + likesCount)\nnumo posts get <postId> --json # post details\nnumo posts comments <postId> --json # list comments on a post\nnumo posts replies <postId> <commentId> --json # list replies to a comment\n```\n\n### Profile & Discovery\n\n```bash\nnumo profile --json # Current user profile\nnumo commands --json # List all available commands\nnumo schema "tasks create" # JSON schema for a specific command\nnumo doctor --json # Health check (DNS, TLS, /api/health, auth)\n```\n\n## Batch Operations\n\n```bash\n# Complete multiple tasks via stdin (one ID per line)\necho -e "taskId1\\ntaskId2" | numo tasks complete --stdin --json\n\n# Pipe IDs from list\nnumo tasks list --json --tag Work | jq -r \'.tasks[].id\' | numo tasks complete --stdin\n```\n\n## Error Handling\n\nAll errors return structured JSON on stderr:\n\n```json\n{\n "error": {\n "kind": "NOT_FOUND",\n "code": 100,\n "message": "Task not found",\n "suggestion": "numo tasks list"\n }\n}\n```\n\n### Error kinds\n\n`AUTH_REQUIRED`, `AUTH_EXPIRED`, `AUTH_FORBIDDEN`, `INVALID_INPUT`, `MISSING_ARGUMENT`, `NOT_FOUND`, `CONFLICT`, `NETWORK_ERROR`, `TIMEOUT`, `RATE_LIMITED`, `SERVICE_UNAVAILABLE`, `CONFIG_ERROR`, `INTERNAL`, `UNKNOWN`.\n\n### Exit codes\n\n| Code | Meaning |\n|------|---------|\n| `0` | OK |\n| `1` | General error |\n| `2` | Usage error (missing argument, invalid input) |\n| `69` | Service unavailable (network, server down) |\n| `75` | Temporary failure (timeout, rate limit) |\n| `77` | No permission (auth required / forbidden) |\n| `78` | Configuration error (missing env var, etc.) |\n| `100`| Not found |\n| `101`| Conflict |\n\n## Tips for Agents\n\n- Always pass `--json` or `-q` for structured output.\n- Use `numo commands --json` and `numo schema <command>` for runtime introspection. Both payloads include `schemaVersion` and `cliVersion` at the root \u2014 agents that pin behavior should branch on `schemaVersion`.\n- Natural language dates work for both `--date` and `--due`: `"tomorrow"`, `"next monday"`, `"in 3 days"`.\n- Task IDs are stable; store them for later operations.\n- `numo tasks create` defaults to **private** in every mode (see "JSON Mode" above) and inserts new tasks at the top of the list. Pass `--public` to make a task public.\n- For rate-limited errors (`429`), the response includes `retryAfter` (seconds) and `retryable: true`.\n- **PII in profile output:** `numo profile --json` includes `photoURL`, a signed storage URL containing a long-lived access token in the query string. Do not pipe `profile` output to public logs, build outputs, or screenshots \u2014 the URL grants read access to the avatar bytes for as long as it stays valid.\n';
|
|
10082
|
+
try {
|
|
10083
|
+
return (0, import_fs.readFileSync)((0, import_path.join)(process.cwd(), "AGENTS.md"), "utf8");
|
|
10084
|
+
} catch {
|
|
10085
|
+
return "Agent guide is unavailable in this build.\nSee https://github.com/mindistio/numo-cli/blob/main/AGENTS.md";
|
|
10086
|
+
}
|
|
10087
|
+
}
|
|
10088
|
+
|
|
10015
10089
|
// src/cli/cli.ts
|
|
10016
|
-
init_tty();
|
|
10017
10090
|
init_errors();
|
|
10018
|
-
var CLI_VERSION = true ? "1.
|
|
10091
|
+
var CLI_VERSION = true ? "1.6.0" : "0.0.0-dev";
|
|
10019
10092
|
var program2 = new Command();
|
|
10020
10093
|
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) => {
|
|
10021
10094
|
const opts = thisCommand.optsWithGlobals();
|
|
@@ -10023,130 +10096,129 @@ program2.name("numo").description("CLI for Numo \u2014 programmatic access for h
|
|
|
10023
10096
|
thisCommand.setOptionValue("json", true);
|
|
10024
10097
|
}
|
|
10025
10098
|
}).addHelpText("after", `
|
|
10026
|
-
${
|
|
10099
|
+
${import_picocolors12.default.bold("Output modes:")}
|
|
10027
10100
|
Interactive (TTY) Tables, colors, spinners
|
|
10028
10101
|
Piped / --json Clean JSON for scripting and agents
|
|
10029
10102
|
|
|
10030
|
-
${
|
|
10031
|
-
${
|
|
10032
|
-
${
|
|
10033
|
-
${
|
|
10034
|
-
|
|
10035
|
-
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
10103
|
+
${import_picocolors12.default.bold("Examples:")}
|
|
10104
|
+
${import_picocolors12.default.dim("$")} numo login
|
|
10105
|
+
${import_picocolors12.default.dim("$")} numo tasks list
|
|
10106
|
+
${import_picocolors12.default.dim("$")} numo tasks create --text "Buy groceries" --due tomorrow
|
|
10107
|
+
|
|
10108
|
+
${import_picocolors12.default.bold("Environment:")}
|
|
10109
|
+
NUMO_API_URL API server URL
|
|
10110
|
+
NUMO_TOKEN Pre-existing ID token (skips local credentials)
|
|
10111
|
+
NUMO_LOGIN_EMAIL Email for non-interactive login
|
|
10112
|
+
NUMO_LOGIN_PASSWORD Password for non-interactive login
|
|
10113
|
+
NUMO_NO_UPDATE_CHECK Disable update notifications`);
|
|
10114
|
+
program2.command("login").description("Login with your Numo account").option("--phone", "Login with phone number (SMS OTP)").action(async function() {
|
|
10115
|
+
await login(this.optsWithGlobals(), program2);
|
|
10042
10116
|
}).addHelpText("after", `
|
|
10043
10117
|
Examples:
|
|
10044
|
-
$ numo
|
|
10045
|
-
$ numo
|
|
10118
|
+
$ numo login # Interactive (email/password)
|
|
10119
|
+
$ numo login --phone # SMS OTP flow
|
|
10120
|
+
$ NUMO_LOGIN_EMAIL=\u2026 NUMO_LOGIN_PASSWORD=\u2026 numo login --json # Non-interactive (CI/agents)`);
|
|
10046
10121
|
program2.command("logout").description("Clear stored credentials").action(() => {
|
|
10047
10122
|
clearCredentials();
|
|
10048
|
-
console.log(
|
|
10049
|
-
|
|
10050
|
-
|
|
10123
|
+
console.log(import_picocolors12.default.green("Logged out."));
|
|
10124
|
+
if (process.env.NUMO_TOKEN) {
|
|
10125
|
+
console.log(import_picocolors12.default.yellow("\n Note: NUMO_TOKEN env var is still set. Unset it to fully de-authenticate."));
|
|
10126
|
+
}
|
|
10127
|
+
}).addHelpText("after", `
|
|
10128
|
+
Examples:
|
|
10129
|
+
$ numo logout
|
|
10130
|
+
|
|
10131
|
+
If NUMO_TOKEN env var is set, it is not cleared by logout. Unset it separately:
|
|
10132
|
+
$ unset NUMO_TOKEN`);
|
|
10133
|
+
program2.command("whoami").description("Show current auth status (no API call)").addHelpText("after", `
|
|
10134
|
+
Examples:
|
|
10135
|
+
$ numo whoami
|
|
10136
|
+
$ numo whoami --json # \u2192 {"email":"...","uid":"...","tokenValid":true,"expiresIn":N,"source":"..."}`).action(function() {
|
|
10051
10137
|
const opts = this.optsWithGlobals();
|
|
10052
|
-
const asJson =
|
|
10138
|
+
const asJson = isQuietMode(opts);
|
|
10139
|
+
const envToken = process.env.NUMO_TOKEN;
|
|
10053
10140
|
const creds = loadCredentials();
|
|
10054
|
-
if (!creds) {
|
|
10141
|
+
if (!creds && envToken) {
|
|
10142
|
+
let tokenValid2 = false;
|
|
10143
|
+
let expiresIn2 = 0;
|
|
10144
|
+
let email = null;
|
|
10145
|
+
let uid = null;
|
|
10146
|
+
try {
|
|
10147
|
+
const payloadB64 = envToken.split(".")[1] ?? "";
|
|
10148
|
+
const payload = JSON.parse(Buffer.from(payloadB64, "base64").toString("utf8"));
|
|
10149
|
+
if (typeof payload.exp === "number") {
|
|
10150
|
+
const expMs = payload.exp * 1e3;
|
|
10151
|
+
tokenValid2 = Date.now() < expMs;
|
|
10152
|
+
expiresIn2 = Math.max(0, Math.floor((expMs - Date.now()) / 1e3));
|
|
10153
|
+
}
|
|
10154
|
+
if (typeof payload.email === "string") email = payload.email;
|
|
10155
|
+
if (typeof payload.user_id === "string") uid = payload.user_id;
|
|
10156
|
+
else if (typeof payload.sub === "string") uid = payload.sub;
|
|
10157
|
+
} catch {
|
|
10158
|
+
}
|
|
10055
10159
|
if (asJson) {
|
|
10056
|
-
|
|
10160
|
+
printJson({ email, uid, tokenValid: tokenValid2, expiresIn: expiresIn2, source: "NUMO_TOKEN", autoRefresh: false });
|
|
10057
10161
|
} else {
|
|
10058
|
-
console.
|
|
10059
|
-
|
|
10060
|
-
$
|
|
10061
|
-
`);
|
|
10162
|
+
if (email) console.log(` ${import_picocolors12.default.bold("Email")} ${email}`);
|
|
10163
|
+
if (uid) console.log(` ${import_picocolors12.default.bold("UID")} ${uid}`);
|
|
10164
|
+
console.log(` ${import_picocolors12.default.bold("Token")} ${tokenValid2 ? import_picocolors12.default.green(`valid (expires in ${Math.floor(expiresIn2 / 60)}m)`) : import_picocolors12.default.red("expired or malformed")}`);
|
|
10165
|
+
console.log(` ${import_picocolors12.default.bold("Auth")} NUMO_TOKEN env var ${import_picocolors12.default.dim("(no auto-refresh; use NUMO_LOGIN_EMAIL/PASSWORD for long sessions)")}`);
|
|
10062
10166
|
}
|
|
10063
|
-
process.
|
|
10167
|
+
if (!tokenValid2) process.exitCode = ExitCode.NO_PERM;
|
|
10168
|
+
return;
|
|
10169
|
+
}
|
|
10170
|
+
if (!creds) {
|
|
10171
|
+
outputError(Errors.authRequired(), asJson);
|
|
10064
10172
|
return;
|
|
10065
10173
|
}
|
|
10066
10174
|
const tokenValid = !!(creds.idToken && creds.idTokenExpiry && Date.now() < creds.idTokenExpiry);
|
|
10067
10175
|
const expiresIn = creds.idTokenExpiry ? Math.max(0, Math.floor((creds.idTokenExpiry - Date.now()) / 1e3)) : 0;
|
|
10068
|
-
const source =
|
|
10176
|
+
const source = "credentials_file";
|
|
10069
10177
|
if (asJson) {
|
|
10070
|
-
printJson({ email: creds.email, uid: creds.uid, tokenValid, expiresIn, source });
|
|
10178
|
+
printJson({ email: creds.email, uid: creds.uid, tokenValid, expiresIn, source, autoRefresh: true });
|
|
10071
10179
|
} else {
|
|
10072
|
-
console.log(` ${
|
|
10073
|
-
console.log(` ${
|
|
10074
|
-
console.log(` ${
|
|
10075
|
-
console.log(` ${
|
|
10180
|
+
console.log(` ${import_picocolors12.default.bold("Email")} ${creds.email}`);
|
|
10181
|
+
console.log(` ${import_picocolors12.default.bold("UID")} ${creds.uid}`);
|
|
10182
|
+
console.log(` ${import_picocolors12.default.bold("Token")} ${tokenValid ? import_picocolors12.default.green(`valid (expires in ${Math.floor(expiresIn / 60)}m)`) : import_picocolors12.default.yellow("expired (will auto-refresh)")}`);
|
|
10183
|
+
console.log(` ${import_picocolors12.default.bold("Auth")} ${getCredentialsPath()}`);
|
|
10076
10184
|
}
|
|
10077
10185
|
});
|
|
10078
10186
|
registerTasksCommands(program2);
|
|
10079
|
-
program2.command("add [text...]").description("Quick-add a task (today, public, no wizard)").option("--due <date>", "Due date YYYY-MM-DD (default: today)").option("--tags <tags>", "Comma-separated tags").option("--public", "Make task public (default)").option("--private", "Make task private").action(async function(textParts) {
|
|
10080
|
-
const opts = this.optsWithGlobals();
|
|
10081
|
-
const uid = requireUid();
|
|
10082
|
-
const text = textParts?.join(" ");
|
|
10083
|
-
if (!text) {
|
|
10084
|
-
console.error('Usage: numo add "task text"');
|
|
10085
|
-
process.exit(ExitCode.USAGE);
|
|
10086
|
-
}
|
|
10087
|
-
const body = {
|
|
10088
|
-
text,
|
|
10089
|
-
dueDate: opts.due ? parseHumanDate(opts.due) ?? opts.due : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
10090
|
-
};
|
|
10091
|
-
if (opts.tags) body.tags = opts.tags.split(",");
|
|
10092
|
-
if (opts.public) body.isPublic = true;
|
|
10093
|
-
if (opts.private) body.isPublic = false;
|
|
10094
|
-
await runCreate({
|
|
10095
|
-
global: opts,
|
|
10096
|
-
fn: () => createTask(uid, body),
|
|
10097
|
-
dataKey: "task",
|
|
10098
|
-
spinnerMessage: "Creating task...",
|
|
10099
|
-
onInteractive: (_task, payload) => {
|
|
10100
|
-
const { task, karma } = payload;
|
|
10101
|
-
const check = import_picocolors13.default.green(SYM.check);
|
|
10102
|
-
console.log(`
|
|
10103
|
-
${check} Created ${task.text} ${import_picocolors13.default.dim(task.id)}`);
|
|
10104
|
-
if (karma) console.log(` ${formatKarmaGain(karma)}`);
|
|
10105
|
-
console.log("");
|
|
10106
|
-
}
|
|
10107
|
-
});
|
|
10108
|
-
});
|
|
10109
10187
|
registerPostsCommands(program2);
|
|
10110
10188
|
registerProfileCommands(program2);
|
|
10111
10189
|
registerDoctorCommand(program2);
|
|
10112
10190
|
program2.command("commands").description("List all available commands").action(function() {
|
|
10113
10191
|
const opts = this.optsWithGlobals();
|
|
10114
|
-
const useJson2 =
|
|
10115
|
-
const commands =
|
|
10116
|
-
function walk(cmd, prefix) {
|
|
10117
|
-
for (const sub of cmd.commands) {
|
|
10118
|
-
const fullName = prefix ? `${prefix} ${sub.name()}` : sub.name();
|
|
10119
|
-
if (sub.commands.length > 0) {
|
|
10120
|
-
walk(sub, fullName);
|
|
10121
|
-
} else {
|
|
10122
|
-
commands.push({
|
|
10123
|
-
name: fullName,
|
|
10124
|
-
description: sub.description(),
|
|
10125
|
-
options: sub.options.map((o) => o.flags)
|
|
10126
|
-
});
|
|
10127
|
-
}
|
|
10128
|
-
}
|
|
10129
|
-
}
|
|
10130
|
-
walk(program2, "");
|
|
10192
|
+
const useJson2 = isQuietMode(opts);
|
|
10193
|
+
const commands = collectCommands(program2);
|
|
10131
10194
|
if (useJson2) {
|
|
10132
|
-
console.log(JSON.stringify({ commands }));
|
|
10195
|
+
console.log(JSON.stringify({ schemaVersion: "1", cliVersion: CLI_VERSION, commands }));
|
|
10133
10196
|
} else {
|
|
10134
|
-
console.log("");
|
|
10135
|
-
let lastGroup = "";
|
|
10136
|
-
for (const cmd of commands) {
|
|
10137
|
-
const group = cmd.name.split(" ")[0];
|
|
10138
|
-
if (group !== lastGroup) {
|
|
10139
|
-
if (lastGroup) console.log("");
|
|
10140
|
-
console.log(` ${import_picocolors13.default.bold(group.charAt(0).toUpperCase() + group.slice(1) + ":")}`);
|
|
10141
|
-
lastGroup = group;
|
|
10142
|
-
}
|
|
10143
|
-
console.log(` numo ${cmd.name.padEnd(30)} ${import_picocolors13.default.dim(cmd.description)}`);
|
|
10144
|
-
}
|
|
10145
10197
|
console.log(`
|
|
10146
|
-
|
|
10198
|
+
${formatCommandMap(commands)}`);
|
|
10199
|
+
console.log(`
|
|
10200
|
+
${import_picocolors12.default.dim("Run numo <command> --help for details.")}
|
|
10147
10201
|
`);
|
|
10148
10202
|
}
|
|
10149
10203
|
});
|
|
10204
|
+
program2.command("guide").alias("agents").description("Print the full agent integration guide (AGENTS.md)").addHelpText("after", `
|
|
10205
|
+
Examples:
|
|
10206
|
+
$ numo guide # full agent guide (Markdown)
|
|
10207
|
+
$ numo agents # alias
|
|
10208
|
+
$ numo guide --json # \u2192 {"schemaVersion":"1","cliVersion":"...","guide":"..."}`).action(function() {
|
|
10209
|
+
const opts = this.optsWithGlobals();
|
|
10210
|
+
const useJson2 = isQuietMode(opts);
|
|
10211
|
+
const guide = getAgentGuide();
|
|
10212
|
+
if (useJson2) {
|
|
10213
|
+
console.log(JSON.stringify({ schemaVersion: "1", cliVersion: CLI_VERSION, guide }));
|
|
10214
|
+
} else {
|
|
10215
|
+
console.log(guide);
|
|
10216
|
+
}
|
|
10217
|
+
});
|
|
10218
|
+
var OPTION_ENUMS = {
|
|
10219
|
+
"--repeat": ["daily", "weekly", "monthly", "none"],
|
|
10220
|
+
"--difficulty": [0, 1, 2, 3]
|
|
10221
|
+
};
|
|
10150
10222
|
function buildCommandSchema(cmd, fullName) {
|
|
10151
10223
|
return {
|
|
10152
10224
|
name: fullName,
|
|
@@ -10154,14 +10226,23 @@ function buildCommandSchema(cmd, fullName) {
|
|
|
10154
10226
|
arguments: cmd.registeredArguments?.map((a) => ({
|
|
10155
10227
|
name: a.name(),
|
|
10156
10228
|
required: a.required,
|
|
10229
|
+
variadic: a.variadic,
|
|
10157
10230
|
description: a.description
|
|
10158
10231
|
})) ?? [],
|
|
10159
|
-
options: cmd.options.filter((o) => !["--json", "-q, --quiet"].includes(o.flags)).map((o) =>
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10164
|
-
|
|
10232
|
+
options: cmd.options.filter((o) => !["--json", "-q, --quiet"].includes(o.flags)).map((o) => {
|
|
10233
|
+
const takesValue = o.required || o.optional;
|
|
10234
|
+
const repeatable = Array.isArray(o.defaultValue);
|
|
10235
|
+
const opt = {
|
|
10236
|
+
flags: o.flags,
|
|
10237
|
+
description: o.description,
|
|
10238
|
+
type: takesValue ? repeatable ? "string[]" : "string" : "boolean",
|
|
10239
|
+
required: o.required,
|
|
10240
|
+
default: o.defaultValue
|
|
10241
|
+
};
|
|
10242
|
+
if (repeatable) opt.repeatable = true;
|
|
10243
|
+
if (OPTION_ENUMS[o.long]) opt.enum = OPTION_ENUMS[o.long];
|
|
10244
|
+
return opt;
|
|
10245
|
+
})
|
|
10165
10246
|
};
|
|
10166
10247
|
}
|
|
10167
10248
|
program2.command("schema [command]").description("Print JSON schema for a command (for AI agents)").action(function(cmdPath) {
|
|
@@ -10176,7 +10257,7 @@ program2.command("schema [command]").description("Print JSON schema for a comman
|
|
|
10176
10257
|
var walk = walk2;
|
|
10177
10258
|
const schemas = [];
|
|
10178
10259
|
walk2(program2, "");
|
|
10179
|
-
console.log(JSON.stringify({ commands: schemas }, null, 2));
|
|
10260
|
+
console.log(JSON.stringify({ schemaVersion: "1", cliVersion: CLI_VERSION, commands: schemas }, null, 2));
|
|
10180
10261
|
return;
|
|
10181
10262
|
}
|
|
10182
10263
|
const parts = cmdPath.split(" ");
|
|
@@ -10190,7 +10271,7 @@ program2.command("schema [command]").description("Print JSON schema for a comman
|
|
|
10190
10271
|
}
|
|
10191
10272
|
cmd = sub;
|
|
10192
10273
|
}
|
|
10193
|
-
console.log(JSON.stringify(buildCommandSchema(cmd, cmdPath), null, 2));
|
|
10274
|
+
console.log(JSON.stringify({ schemaVersion: "1", cliVersion: CLI_VERSION, ...buildCommandSchema(cmd, cmdPath) }, null, 2));
|
|
10194
10275
|
});
|
|
10195
10276
|
program2.command("completion <shell>").description("Generate shell completion script").action(function(shell) {
|
|
10196
10277
|
if (shell !== "zsh") {
|