numo-cli 1.5.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 +929 -885
- package/package.json +14 -14
package/dist/cli.cjs
CHANGED
|
@@ -3302,7 +3302,7 @@ function classifyError(err) {
|
|
|
3302
3302
|
return new CliError("UNKNOWN" /* UNKNOWN */, message, ExitCode.GENERAL, { cause: err });
|
|
3303
3303
|
}
|
|
3304
3304
|
function sanitizeErrorMessage(msg) {
|
|
3305
|
-
return msg.replace(/https?:\/\/\S+/g, "<url>").replace(/\/(?:Users|home|var|tmp)\/\S+/g, "<path>").replace(/[A-Za-z0-9_
|
|
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
3306
|
}
|
|
3307
3307
|
var ExitCode, CliError, Errors;
|
|
3308
3308
|
var init_errors = __esm({
|
|
@@ -3373,6 +3373,53 @@ var init_errors = __esm({
|
|
|
3373
3373
|
}
|
|
3374
3374
|
});
|
|
3375
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
|
+
|
|
3376
3423
|
// src/cli/lib/api-client.ts
|
|
3377
3424
|
var api_client_exports = {};
|
|
3378
3425
|
__export(api_client_exports, {
|
|
@@ -3399,14 +3446,15 @@ function toCliError(err) {
|
|
|
3399
3446
|
const e = body.error;
|
|
3400
3447
|
const kind = e.kind ?? "UNKNOWN" /* UNKNOWN */;
|
|
3401
3448
|
const exitCode = KIND_EXIT[kind] ?? ExitCode.GENERAL;
|
|
3402
|
-
return new CliError(kind, e.message ?? "Unknown error", exitCode, {
|
|
3449
|
+
return new CliError(kind, sanitizeErrorMessage(e.message ?? "Unknown error"), exitCode, {
|
|
3403
3450
|
retryable: e.retryable,
|
|
3404
3451
|
retryAfter: e.retryAfter
|
|
3405
3452
|
});
|
|
3406
3453
|
}
|
|
3407
|
-
return new CliError("UNKNOWN" /* UNKNOWN */, httpErr.message ?? "Unknown error", ExitCode.GENERAL);
|
|
3454
|
+
return new CliError("UNKNOWN" /* UNKNOWN */, sanitizeErrorMessage(httpErr.message ?? "Unknown error"), ExitCode.GENERAL);
|
|
3408
3455
|
}
|
|
3409
3456
|
async function apiHeaders() {
|
|
3457
|
+
assertSafeApiBase();
|
|
3410
3458
|
const token = await getIdToken();
|
|
3411
3459
|
return {
|
|
3412
3460
|
Authorization: `Bearer ${token}`,
|
|
@@ -3423,17 +3471,14 @@ function url(path3, params) {
|
|
|
3423
3471
|
const qs = sp.toString();
|
|
3424
3472
|
return qs ? `${u2}?${qs}` : u2;
|
|
3425
3473
|
}
|
|
3426
|
-
var
|
|
3474
|
+
var KIND_EXIT, api;
|
|
3427
3475
|
var init_api_client = __esm({
|
|
3428
3476
|
"src/cli/lib/api-client.ts"() {
|
|
3429
3477
|
"use strict";
|
|
3430
3478
|
init_credentials();
|
|
3431
3479
|
init_http();
|
|
3432
3480
|
init_errors();
|
|
3433
|
-
|
|
3434
|
-
if (API_BASE !== "http://localhost:3000" && API_BASE.startsWith("http://")) {
|
|
3435
|
-
process.stderr.write("[warn] NUMO_API_URL uses HTTP \u2014 tokens sent unencrypted. Use HTTPS in production.\n");
|
|
3436
|
-
}
|
|
3481
|
+
init_api_base();
|
|
3437
3482
|
KIND_EXIT = {
|
|
3438
3483
|
AUTH_REQUIRED: ExitCode.NO_PERM,
|
|
3439
3484
|
AUTH_EXPIRED: ExitCode.NO_PERM,
|
|
@@ -3487,7 +3532,12 @@ var init_api_client = __esm({
|
|
|
3487
3532
|
// src/cli/auth/credentials.ts
|
|
3488
3533
|
function loadCredentials() {
|
|
3489
3534
|
try {
|
|
3490
|
-
const
|
|
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"));
|
|
3491
3541
|
if (typeof data?.refreshToken !== "string" || typeof data?.uid !== "string" || typeof data?.email !== "string") {
|
|
3492
3542
|
return null;
|
|
3493
3543
|
}
|
|
@@ -3498,7 +3548,9 @@ function loadCredentials() {
|
|
|
3498
3548
|
}
|
|
3499
3549
|
function saveCredentials(creds) {
|
|
3500
3550
|
ensureConfigDir();
|
|
3501
|
-
|
|
3551
|
+
const path3 = getCredentialsPath();
|
|
3552
|
+
fs2.writeFileSync(path3, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
3553
|
+
if (process.platform !== "win32") fs2.chmodSync(path3, 384);
|
|
3502
3554
|
}
|
|
3503
3555
|
function clearCredentials() {
|
|
3504
3556
|
try {
|
|
@@ -3524,6 +3576,7 @@ async function getIdToken() {
|
|
|
3524
3576
|
return refreshInFlight;
|
|
3525
3577
|
}
|
|
3526
3578
|
async function performRefresh(creds) {
|
|
3579
|
+
assertSafeApiBase();
|
|
3527
3580
|
const { API_BASE: apiBase } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
|
|
3528
3581
|
const { http: http2 } = await Promise.resolve().then(() => (init_http(), http_exports));
|
|
3529
3582
|
const resp = await http2.post(
|
|
@@ -3543,6 +3596,7 @@ var init_credentials = __esm({
|
|
|
3543
3596
|
fs2 = __toESM(require("fs"), 1);
|
|
3544
3597
|
crypto = __toESM(require("crypto"), 1);
|
|
3545
3598
|
init_dirs();
|
|
3599
|
+
init_api_base();
|
|
3546
3600
|
refreshInFlight = null;
|
|
3547
3601
|
}
|
|
3548
3602
|
});
|
|
@@ -5261,7 +5315,12 @@ async function loadClack() {
|
|
|
5261
5315
|
}
|
|
5262
5316
|
async function promptText(opts) {
|
|
5263
5317
|
if (!isInteractive()) {
|
|
5264
|
-
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
|
+
);
|
|
5265
5324
|
}
|
|
5266
5325
|
const p = await loadClack();
|
|
5267
5326
|
const value = await p.text({
|
|
@@ -5276,7 +5335,12 @@ async function promptText(opts) {
|
|
|
5276
5335
|
}
|
|
5277
5336
|
async function promptPassword(opts) {
|
|
5278
5337
|
if (!isInteractive()) {
|
|
5279
|
-
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
|
+
);
|
|
5280
5344
|
}
|
|
5281
5345
|
const p = await loadClack();
|
|
5282
5346
|
const value = await p.password({
|
|
@@ -5290,7 +5354,12 @@ async function promptPassword(opts) {
|
|
|
5290
5354
|
}
|
|
5291
5355
|
async function promptSelect(opts) {
|
|
5292
5356
|
if (!isInteractive()) {
|
|
5293
|
-
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
|
+
);
|
|
5294
5363
|
}
|
|
5295
5364
|
const p = await loadClack();
|
|
5296
5365
|
const value = await p.select({
|
|
@@ -5318,7 +5387,12 @@ async function promptConfirm(opts) {
|
|
|
5318
5387
|
}
|
|
5319
5388
|
async function promptMultiSelect(opts) {
|
|
5320
5389
|
if (!isInteractive()) {
|
|
5321
|
-
throw new
|
|
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
|
+
);
|
|
5322
5396
|
}
|
|
5323
5397
|
const p = await loadClack();
|
|
5324
5398
|
const value = await p.multiselect({
|
|
@@ -5345,6 +5419,7 @@ var init_prompts = __esm({
|
|
|
5345
5419
|
"src/cli/lib/prompts.ts"() {
|
|
5346
5420
|
"use strict";
|
|
5347
5421
|
init_tty();
|
|
5422
|
+
init_errors();
|
|
5348
5423
|
}
|
|
5349
5424
|
});
|
|
5350
5425
|
|
|
@@ -5354,6 +5429,7 @@ __export(phone_login_exports, {
|
|
|
5354
5429
|
authenticateWithPhone: () => authenticateWithPhone
|
|
5355
5430
|
});
|
|
5356
5431
|
async function authenticateWithPhone(spinner) {
|
|
5432
|
+
assertSafeApiBase();
|
|
5357
5433
|
const p = await Promise.resolve().then(() => (init_dist2(), dist_exports));
|
|
5358
5434
|
const phone = await promptText({
|
|
5359
5435
|
message: "Phone number (with country code)",
|
|
@@ -5370,7 +5446,7 @@ async function authenticateWithPhone(spinner) {
|
|
|
5370
5446
|
const { sessionId, verifyUrl } = startResp.data;
|
|
5371
5447
|
spinner.stop("");
|
|
5372
5448
|
p.log.info("Opening browser for phone verification...");
|
|
5373
|
-
p.log.info(
|
|
5449
|
+
p.log.info(import_picocolors4.default.dim(`If the browser does not open, visit: ${verifyUrl}`));
|
|
5374
5450
|
const { default: open } = await import("open");
|
|
5375
5451
|
const cp = await open(verifyUrl);
|
|
5376
5452
|
cp.unref();
|
|
@@ -5397,15 +5473,16 @@ async function authenticateWithPhone(spinner) {
|
|
|
5397
5473
|
}
|
|
5398
5474
|
throw Errors.networkError("Phone verification timed out. Try again.");
|
|
5399
5475
|
}
|
|
5400
|
-
var
|
|
5476
|
+
var import_picocolors4, POLL_INTERVAL, POLL_TIMEOUT;
|
|
5401
5477
|
var init_phone_login = __esm({
|
|
5402
5478
|
"src/cli/auth/phone-login.ts"() {
|
|
5403
5479
|
"use strict";
|
|
5404
5480
|
init_http();
|
|
5405
|
-
|
|
5481
|
+
import_picocolors4 = __toESM(require_picocolors(), 1);
|
|
5406
5482
|
init_errors();
|
|
5407
5483
|
init_prompts();
|
|
5408
5484
|
init_api_client();
|
|
5485
|
+
init_api_base();
|
|
5409
5486
|
POLL_INTERVAL = 2e3;
|
|
5410
5487
|
POLL_TIMEOUT = 5 * 60 * 1e3;
|
|
5411
5488
|
}
|
|
@@ -5429,196 +5506,82 @@ var {
|
|
|
5429
5506
|
} = import_index.default;
|
|
5430
5507
|
|
|
5431
5508
|
// src/cli/cli.ts
|
|
5432
|
-
var
|
|
5509
|
+
var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
5433
5510
|
|
|
5434
5511
|
// src/cli/auth/login.ts
|
|
5435
5512
|
init_http();
|
|
5436
|
-
var
|
|
5513
|
+
var import_picocolors5 = __toESM(require_picocolors(), 1);
|
|
5437
5514
|
init_credentials();
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
const
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5515
|
+
|
|
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;
|
|
5456
5536
|
}
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
` ${import_picocolors2.default.dim("$")} numo tasks create --text "..." Create a task`,
|
|
5461
|
-
` ${import_picocolors2.default.dim("$")} numo profile View your profile`
|
|
5462
|
-
];
|
|
5463
|
-
console.log(`
|
|
5464
|
-
${import_picocolors2.default.bold("Available commands:")}
|
|
5465
|
-
${lines.join("\n")}
|
|
5466
|
-
`);
|
|
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]));
|
|
5467
5540
|
}
|
|
5468
|
-
|
|
5469
|
-
const
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
{ value: "phone", label: "Phone number (SMS)" }
|
|
5478
|
-
]
|
|
5479
|
-
});
|
|
5480
|
-
}
|
|
5481
|
-
const s = p.spinner();
|
|
5482
|
-
try {
|
|
5483
|
-
let result;
|
|
5484
|
-
if (method === "phone") {
|
|
5485
|
-
const { authenticateWithPhone: authenticateWithPhone2 } = await Promise.resolve().then(() => (init_phone_login(), phone_login_exports));
|
|
5486
|
-
result = await authenticateWithPhone2(s);
|
|
5487
|
-
} else {
|
|
5488
|
-
result = await authenticateWithEmail(s);
|
|
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;
|
|
5489
5550
|
}
|
|
5490
|
-
|
|
5491
|
-
refreshToken: result.refreshToken,
|
|
5492
|
-
uid: result.uid,
|
|
5493
|
-
email: result.displayName,
|
|
5494
|
-
idToken: result.idToken,
|
|
5495
|
-
idTokenExpiry: result.idTokenExpiry
|
|
5496
|
-
});
|
|
5497
|
-
s.stop(`Logged in as ${import_picocolors2.default.green(result.displayName)}`);
|
|
5498
|
-
p.outro("You are ready to go!");
|
|
5499
|
-
printSuccess(result.displayName);
|
|
5500
|
-
} catch (err) {
|
|
5501
|
-
const classified = err instanceof CliError ? err : classifyError(err);
|
|
5502
|
-
s.stop(import_picocolors2.default.red("Login failed"));
|
|
5503
|
-
p.log.error(classified.message);
|
|
5504
|
-
if (classified.options.hint) p.log.warning(classified.options.hint);
|
|
5505
|
-
process.exit(classified.exitCode);
|
|
5551
|
+
lines.push(` numo ${cmd.name.padEnd(30)} ${import_picocolors.default.dim(cmd.description)}`);
|
|
5506
5552
|
}
|
|
5553
|
+
return lines.join("\n");
|
|
5507
5554
|
}
|
|
5508
5555
|
|
|
5509
|
-
// src/cli/auth/
|
|
5510
|
-
init_http();
|
|
5511
|
-
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
5512
|
-
init_credentials();
|
|
5556
|
+
// src/cli/auth/login.ts
|
|
5513
5557
|
init_prompts();
|
|
5514
5558
|
init_errors();
|
|
5515
|
-
init_tty();
|
|
5516
5559
|
init_api_client();
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
}
|
|
5524
|
-
|
|
5525
|
-
if (password.length < 6) {
|
|
5526
|
-
throw Errors.invalidInput("Password is too weak (min 6 characters)");
|
|
5527
|
-
}
|
|
5528
|
-
return password;
|
|
5529
|
-
}
|
|
5530
|
-
function classifySignUpError(err) {
|
|
5531
|
-
if (err instanceof CliError) return err;
|
|
5532
|
-
const resp = err?.response?.data?.error;
|
|
5533
|
-
const kind = resp?.kind ?? "";
|
|
5534
|
-
const msg = resp?.message ?? "";
|
|
5535
|
-
if (kind === "CONFLICT" || msg.includes("already in use")) {
|
|
5536
|
-
return Errors.invalidInput(
|
|
5537
|
-
"Email already in use",
|
|
5538
|
-
"Already have an account? Run: numo login"
|
|
5539
|
-
);
|
|
5540
|
-
}
|
|
5541
|
-
if (msg.includes("Invalid email")) {
|
|
5542
|
-
return Errors.invalidInput("Invalid email address");
|
|
5543
|
-
}
|
|
5544
|
-
if (msg.includes("Password too weak") || msg.includes("min 6")) {
|
|
5545
|
-
return Errors.invalidInput("Password is too weak (min 6 characters)");
|
|
5546
|
-
}
|
|
5547
|
-
return classifyError(err);
|
|
5548
|
-
}
|
|
5549
|
-
async function signUp(email, password) {
|
|
5550
|
-
try {
|
|
5551
|
-
const resp = await http.post(`${API_BASE}/api/auth/register`, {
|
|
5552
|
-
email,
|
|
5553
|
-
password,
|
|
5554
|
-
tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
5555
|
-
tzOffset: (/* @__PURE__ */ new Date()).getTimezoneOffset()
|
|
5556
|
-
});
|
|
5557
|
-
return {
|
|
5558
|
-
refreshToken: resp.data.refreshToken,
|
|
5559
|
-
uid: resp.data.uid,
|
|
5560
|
-
displayName: resp.data.email ?? email,
|
|
5561
|
-
idToken: resp.data.idToken,
|
|
5562
|
-
idTokenExpiry: Date.now() + (resp.data.expiresIn || 3600) * 1e3
|
|
5563
|
-
};
|
|
5564
|
-
} catch (err) {
|
|
5565
|
-
throw classifySignUpError(err);
|
|
5560
|
+
init_api_base();
|
|
5561
|
+
|
|
5562
|
+
// src/cli/lib/quiet.ts
|
|
5563
|
+
init_tty();
|
|
5564
|
+
var noopSpinner = {
|
|
5565
|
+
start: () => {
|
|
5566
|
+
},
|
|
5567
|
+
stop: () => {
|
|
5566
5568
|
}
|
|
5569
|
+
};
|
|
5570
|
+
function isQuietMode(opts = {}) {
|
|
5571
|
+
return !!(opts.quiet || opts.json || !isInteractive());
|
|
5567
5572
|
}
|
|
5568
|
-
async function
|
|
5573
|
+
async function makeClackSpinner(quietMode) {
|
|
5574
|
+
if (quietMode) return noopSpinner;
|
|
5569
5575
|
const p = await Promise.resolve().then(() => (init_dist2(), dist_exports));
|
|
5570
|
-
p.
|
|
5571
|
-
const s = p.spinner();
|
|
5572
|
-
let spinnerActive = false;
|
|
5573
|
-
try {
|
|
5574
|
-
const email = validateEmail(
|
|
5575
|
-
options.email ?? await promptText({ message: "Email", required: true })
|
|
5576
|
-
);
|
|
5577
|
-
const rawPassword = options.password ?? await promptPassword({ message: "Password" });
|
|
5578
|
-
if (!options.password && isInteractive()) {
|
|
5579
|
-
const confirm = await promptPassword({ message: "Confirm password" });
|
|
5580
|
-
if (rawPassword !== confirm) {
|
|
5581
|
-
throw Errors.invalidInput("Passwords do not match");
|
|
5582
|
-
}
|
|
5583
|
-
}
|
|
5584
|
-
const password = validatePassword(rawPassword);
|
|
5585
|
-
s.start("Creating account...");
|
|
5586
|
-
spinnerActive = true;
|
|
5587
|
-
const result = await signUp(email, password);
|
|
5588
|
-
saveCredentials({
|
|
5589
|
-
refreshToken: result.refreshToken,
|
|
5590
|
-
uid: result.uid,
|
|
5591
|
-
email: result.displayName,
|
|
5592
|
-
idToken: result.idToken,
|
|
5593
|
-
idTokenExpiry: result.idTokenExpiry
|
|
5594
|
-
});
|
|
5595
|
-
s.stop(`Registered as ${import_picocolors3.default.green(result.displayName)}`);
|
|
5596
|
-
p.outro("Welcome to Numo!");
|
|
5597
|
-
printSuccess(result.displayName);
|
|
5598
|
-
} catch (err) {
|
|
5599
|
-
const classified = err instanceof CliError ? err : classifySignUpError(err);
|
|
5600
|
-
if (spinnerActive) s.stop(import_picocolors3.default.red("Registration failed"));
|
|
5601
|
-
p.log.error(classified.message);
|
|
5602
|
-
if (classified.options.hint) p.log.warning(classified.options.hint);
|
|
5603
|
-
process.exit(classified.exitCode);
|
|
5604
|
-
}
|
|
5576
|
+
return p.spinner();
|
|
5605
5577
|
}
|
|
5606
5578
|
|
|
5607
|
-
// src/cli/cli.ts
|
|
5608
|
-
init_credentials();
|
|
5609
|
-
|
|
5610
|
-
// src/cli/commands/tasks.ts
|
|
5611
|
-
var import_picocolors8 = __toESM(require_picocolors(), 1);
|
|
5612
|
-
|
|
5613
|
-
// src/cli/lib/actions.ts
|
|
5614
|
-
init_tty();
|
|
5615
|
-
|
|
5616
5579
|
// src/cli/lib/output.ts
|
|
5617
|
-
var
|
|
5580
|
+
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
5618
5581
|
init_tty();
|
|
5619
5582
|
|
|
5620
5583
|
// src/cli/lib/table.ts
|
|
5621
|
-
var
|
|
5584
|
+
var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
5622
5585
|
init_tty();
|
|
5623
5586
|
var BOX = isUnicodeSupported ? {
|
|
5624
5587
|
topLeft: String.fromCodePoint(9484),
|
|
@@ -5657,17 +5620,17 @@ var BOX = isUnicodeSupported ? {
|
|
|
5657
5620
|
cross: "+"
|
|
5658
5621
|
};
|
|
5659
5622
|
function renderTable(headers, rows, emptyMessage = "(no results)") {
|
|
5660
|
-
if (rows.length === 0) return
|
|
5623
|
+
if (rows.length === 0) return import_picocolors2.default.dim(emptyMessage);
|
|
5661
5624
|
const widths = headers.map(
|
|
5662
5625
|
(h2, i) => Math.max(h2.length, ...rows.map((r) => (r[i] ?? "").length))
|
|
5663
5626
|
);
|
|
5664
|
-
const
|
|
5627
|
+
const pad2 = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
|
|
5665
5628
|
const topBorder = BOX.topLeft + widths.map((w) => BOX.horizontal.repeat(w + 2)).join(BOX.midTop) + BOX.topRight;
|
|
5666
5629
|
const midBorder = BOX.midLeft + widths.map((w) => BOX.horizontal.repeat(w + 2)).join(BOX.cross) + BOX.midRight;
|
|
5667
5630
|
const bottomBorder = BOX.bottomLeft + widths.map((w) => BOX.horizontal.repeat(w + 2)).join(BOX.midBottom) + BOX.bottomRight;
|
|
5668
5631
|
const formatRow = (cells, bold = false) => BOX.vertical + cells.map((c, i) => {
|
|
5669
|
-
const padded =
|
|
5670
|
-
return ` ${bold ?
|
|
5632
|
+
const padded = pad2(c, widths[i]);
|
|
5633
|
+
return ` ${bold ? import_picocolors2.default.bold(padded) : padded} `;
|
|
5671
5634
|
}).join(BOX.vertical) + BOX.vertical;
|
|
5672
5635
|
const lines = [
|
|
5673
5636
|
topBorder,
|
|
@@ -5701,17 +5664,15 @@ function printRecord(fields) {
|
|
|
5701
5664
|
const visible = fields.filter(([, v]) => v != null && v !== "");
|
|
5702
5665
|
const maxLabel = Math.max(...visible.map(([l]) => l.length));
|
|
5703
5666
|
for (const [label, value] of visible) {
|
|
5704
|
-
console.log(` ${
|
|
5667
|
+
console.log(` ${import_picocolors3.default.bold(label.padEnd(maxLabel))} ${value}`);
|
|
5705
5668
|
}
|
|
5706
5669
|
}
|
|
5707
5670
|
function outputResult(data, asJson) {
|
|
5708
|
-
if (asJson) {
|
|
5709
|
-
printJson(data);
|
|
5710
|
-
} else if (typeof data === "string") {
|
|
5671
|
+
if (!asJson && typeof data === "string") {
|
|
5711
5672
|
console.log(data);
|
|
5712
|
-
|
|
5713
|
-
printJson(data);
|
|
5673
|
+
return;
|
|
5714
5674
|
}
|
|
5675
|
+
printJson(data);
|
|
5715
5676
|
}
|
|
5716
5677
|
function pickFields(obj, fields) {
|
|
5717
5678
|
const result = {};
|
|
@@ -5746,148 +5707,145 @@ function outputError(err, asJson) {
|
|
|
5746
5707
|
console.error(JSON.stringify(structured.toJSON(), null, 2));
|
|
5747
5708
|
} else {
|
|
5748
5709
|
console.error(`
|
|
5749
|
-
${
|
|
5710
|
+
${import_picocolors3.default.red("Error")}: ${structured.message}`);
|
|
5750
5711
|
if (structured.options.suggestion) {
|
|
5751
5712
|
console.error(`
|
|
5752
|
-
${
|
|
5713
|
+
${import_picocolors3.default.dim("Fix:")} ${import_picocolors3.default.cyan("$")} ${import_picocolors3.default.bold(structured.options.suggestion)}`);
|
|
5753
5714
|
}
|
|
5754
5715
|
if (structured.options.hint) {
|
|
5755
|
-
console.error(` ${
|
|
5716
|
+
console.error(` ${import_picocolors3.default.dim(structured.options.hint)}`);
|
|
5756
5717
|
}
|
|
5757
5718
|
console.error("");
|
|
5758
5719
|
}
|
|
5759
5720
|
process.exit(structured.exitCode);
|
|
5760
5721
|
}
|
|
5761
5722
|
|
|
5762
|
-
// src/cli/
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
// src/cli/lib/symbols.ts
|
|
5767
|
-
init_tty();
|
|
5768
|
-
var u = isUnicodeSupported;
|
|
5769
|
-
var SYM = {
|
|
5770
|
-
check: u ? "\u2714" : "v",
|
|
5771
|
-
// ✓
|
|
5772
|
-
cross: u ? "\u2718" : "x",
|
|
5773
|
-
// ✗
|
|
5774
|
-
circle: u ? "\u25CB" : "o",
|
|
5775
|
-
// ○
|
|
5776
|
-
repeat: u ? "\u21BB" : "R",
|
|
5777
|
-
// ↻
|
|
5778
|
-
undo: u ? "\u21A9" : "<-",
|
|
5779
|
-
// ↩
|
|
5780
|
-
fire: u ? "\u{1F525}" : "*",
|
|
5781
|
-
// 🔥
|
|
5782
|
-
ellipsis: u ? "\u2026" : "...",
|
|
5783
|
-
// …
|
|
5784
|
-
dash: u ? "\u2500" : "-",
|
|
5785
|
-
// ─
|
|
5786
|
-
bullet: u ? "\u2022" : "*",
|
|
5787
|
-
// •
|
|
5788
|
-
arrow: u ? "\u2192" : "->"
|
|
5789
|
-
// →
|
|
5790
|
-
};
|
|
5791
|
-
|
|
5792
|
-
// src/cli/lib/spinner.ts
|
|
5793
|
-
var FRAMES = isUnicodeSupported ? [
|
|
5794
|
-
String.fromCodePoint(10251),
|
|
5795
|
-
String.fromCodePoint(10265),
|
|
5796
|
-
String.fromCodePoint(10297),
|
|
5797
|
-
String.fromCodePoint(10296),
|
|
5798
|
-
String.fromCodePoint(10300),
|
|
5799
|
-
String.fromCodePoint(10292),
|
|
5800
|
-
String.fromCodePoint(10278),
|
|
5801
|
-
String.fromCodePoint(10279),
|
|
5802
|
-
String.fromCodePoint(10247),
|
|
5803
|
-
String.fromCodePoint(10255)
|
|
5804
|
-
] : ["-", "\\", "|", "/"];
|
|
5805
|
-
var INTERVAL = 80;
|
|
5806
|
-
var MAX_RETRIES2 = 3;
|
|
5807
|
-
function createSpinner(interactive) {
|
|
5808
|
-
if (!interactive) {
|
|
5809
|
-
return { start() {
|
|
5810
|
-
}, update() {
|
|
5811
|
-
}, stop() {
|
|
5812
|
-
}, fail() {
|
|
5813
|
-
} };
|
|
5814
|
-
}
|
|
5815
|
-
let timer = null;
|
|
5816
|
-
let frameIdx = 0;
|
|
5817
|
-
let currentMsg = "";
|
|
5818
|
-
function clear() {
|
|
5819
|
-
process.stderr.write("\r\x1B[K");
|
|
5820
|
-
}
|
|
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 });
|
|
5821
5727
|
return {
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
timer = setInterval(() => {
|
|
5828
|
-
frameIdx = (frameIdx + 1) % FRAMES.length;
|
|
5829
|
-
clear();
|
|
5830
|
-
process.stderr.write(`${import_picocolors6.default.cyan(FRAMES[frameIdx])} ${currentMsg}`);
|
|
5831
|
-
}, INTERVAL);
|
|
5832
|
-
},
|
|
5833
|
-
update(msg) {
|
|
5834
|
-
currentMsg = msg;
|
|
5835
|
-
},
|
|
5836
|
-
stop(msg) {
|
|
5837
|
-
if (timer) {
|
|
5838
|
-
clearInterval(timer);
|
|
5839
|
-
timer = null;
|
|
5840
|
-
}
|
|
5841
|
-
clear();
|
|
5842
|
-
process.stderr.write(`${import_picocolors6.default.green(SYM.check)} ${msg ?? currentMsg}
|
|
5843
|
-
`);
|
|
5844
|
-
},
|
|
5845
|
-
fail(msg) {
|
|
5846
|
-
if (timer) {
|
|
5847
|
-
clearInterval(timer);
|
|
5848
|
-
timer = null;
|
|
5849
|
-
}
|
|
5850
|
-
clear();
|
|
5851
|
-
process.stderr.write(`${import_picocolors6.default.red(SYM.cross)} ${msg}
|
|
5852
|
-
`);
|
|
5853
|
-
}
|
|
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
|
|
5854
5733
|
};
|
|
5855
5734
|
}
|
|
5856
|
-
function
|
|
5857
|
-
|
|
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);
|
|
5858
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
|
+
}
|
|
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
|
|
5859
5833
|
async function withSpinner(interactive, message, fn) {
|
|
5860
|
-
const spinner =
|
|
5834
|
+
const spinner = await makeClackSpinner(!interactive);
|
|
5861
5835
|
spinner.start(message);
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
if (status === 429 && attempt < MAX_RETRIES2) {
|
|
5870
|
-
const retryAfter = err.response?.headers?.["retry-after"];
|
|
5871
|
-
const waitMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 1e3 * Math.pow(2, attempt);
|
|
5872
|
-
spinner.update(`Rate limited, retrying in ${Math.round(waitMs / 1e3)}s...`);
|
|
5873
|
-
await delay2(waitMs);
|
|
5874
|
-
spinner.update(message);
|
|
5875
|
-
continue;
|
|
5876
|
-
}
|
|
5877
|
-
spinner.fail(message);
|
|
5878
|
-
throw err;
|
|
5879
|
-
}
|
|
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;
|
|
5880
5843
|
}
|
|
5881
|
-
throw new Error("Max retries exceeded");
|
|
5882
5844
|
}
|
|
5883
5845
|
|
|
5884
5846
|
// src/cli/lib/actions.ts
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
}
|
|
5888
|
-
function useSpinner(opts) {
|
|
5889
|
-
return isInteractive() && !opts.quiet;
|
|
5890
|
-
}
|
|
5847
|
+
var useJson = isQuietMode;
|
|
5848
|
+
var useSpinner = (opts) => !isQuietMode(opts);
|
|
5891
5849
|
async function runGet(opts) {
|
|
5892
5850
|
try {
|
|
5893
5851
|
const result = await withSpinner(
|
|
@@ -5973,25 +5931,22 @@ async function runWrite(opts) {
|
|
|
5973
5931
|
outputError(err, useJson(opts.global));
|
|
5974
5932
|
}
|
|
5975
5933
|
}
|
|
5976
|
-
async function runDelete(opts) {
|
|
5977
|
-
try {
|
|
5978
|
-
await withSpinner(
|
|
5979
|
-
useSpinner(opts.global),
|
|
5980
|
-
opts.spinnerMessage ?? "Deleting...",
|
|
5981
|
-
opts.fn
|
|
5982
|
-
);
|
|
5983
|
-
if (!opts.global.quiet) {
|
|
5984
|
-
console.log(opts.successMessage);
|
|
5985
|
-
}
|
|
5986
|
-
} catch (err) {
|
|
5987
|
-
outputError(err, useJson(opts.global));
|
|
5988
|
-
}
|
|
5989
|
-
}
|
|
5990
5934
|
|
|
5991
5935
|
// src/cli/lib/uid.ts
|
|
5992
5936
|
init_credentials();
|
|
5993
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
|
+
}
|
|
5994
5948
|
function requireUid() {
|
|
5949
|
+
if (process.env.NUMO_TOKEN) return uidFromToken(process.env.NUMO_TOKEN) ?? "";
|
|
5995
5950
|
const creds = loadCredentials();
|
|
5996
5951
|
if (!creds) throw Errors.authRequired();
|
|
5997
5952
|
return creds.uid;
|
|
@@ -5999,34 +5954,58 @@ function requireUid() {
|
|
|
5999
5954
|
|
|
6000
5955
|
// src/cli/services/tasks.ts
|
|
6001
5956
|
init_api_client();
|
|
6002
|
-
async function listTasks(
|
|
5957
|
+
async function listTasks(opts) {
|
|
6003
5958
|
return api.get("/api/tasks", {
|
|
6004
5959
|
date: opts.date,
|
|
6005
5960
|
backlog: opts.backlog ? "true" : void 0,
|
|
6006
5961
|
tag: opts.tag
|
|
6007
5962
|
});
|
|
6008
5963
|
}
|
|
6009
|
-
async function getTask(
|
|
5964
|
+
async function getTask(id) {
|
|
6010
5965
|
return api.get(`/api/tasks/${encodeURIComponent(id)}`);
|
|
6011
5966
|
}
|
|
6012
|
-
async function createTask(
|
|
5967
|
+
async function createTask(body) {
|
|
6013
5968
|
return api.post("/api/tasks", body);
|
|
6014
5969
|
}
|
|
6015
|
-
async function updateTask(
|
|
5970
|
+
async function updateTask(id, body) {
|
|
6016
5971
|
return api.patch(`/api/tasks/${encodeURIComponent(id)}`, body);
|
|
6017
5972
|
}
|
|
6018
|
-
async function deleteTask(
|
|
5973
|
+
async function deleteTask(id) {
|
|
6019
5974
|
return api.del(`/api/tasks/${encodeURIComponent(id)}`);
|
|
6020
5975
|
}
|
|
6021
|
-
async function completeTask(
|
|
5976
|
+
async function completeTask(id, date) {
|
|
6022
5977
|
return api.post(`/api/tasks/${encodeURIComponent(id)}/complete`, date ? { date } : void 0);
|
|
6023
5978
|
}
|
|
6024
|
-
async function uncompleteTask(
|
|
5979
|
+
async function uncompleteTask(taskHistoryId) {
|
|
6025
5980
|
return api.post(`/api/tasks/${encodeURIComponent(taskHistoryId)}/uncomplete`);
|
|
6026
5981
|
}
|
|
6027
5982
|
|
|
6028
5983
|
// src/cli/lib/format.ts
|
|
6029
|
-
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
|
|
6030
6009
|
function formatDate(ts) {
|
|
6031
6010
|
if (ts == null) return "";
|
|
6032
6011
|
const d = typeof ts === "number" ? new Date(ts) : new Date(ts);
|
|
@@ -6054,7 +6033,7 @@ function formatRelativeDate(ts) {
|
|
|
6054
6033
|
}
|
|
6055
6034
|
function formatTags(tags) {
|
|
6056
6035
|
if (!Array.isArray(tags) || tags.length === 0) return "";
|
|
6057
|
-
return tags.map((t2) =>
|
|
6036
|
+
return tags.map((t2) => import_picocolors6.default.cyan(`#${t2}`)).join(" ");
|
|
6058
6037
|
}
|
|
6059
6038
|
function formatDifficulty(d) {
|
|
6060
6039
|
if (d == null) return "";
|
|
@@ -6094,21 +6073,21 @@ function formatWeekdayHeader(date, streakCount) {
|
|
|
6094
6073
|
const cols = process.stdout.columns ?? 80;
|
|
6095
6074
|
const fire = SYM.fire;
|
|
6096
6075
|
const streakStr = streakCount && streakCount > 0 ? `${fire} ${streakCount}` : "";
|
|
6097
|
-
const
|
|
6098
|
-
const line1 = ` ${
|
|
6099
|
-
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)}`;
|
|
6100
6079
|
return `${line1}
|
|
6101
6080
|
${line2}`;
|
|
6102
6081
|
}
|
|
6103
6082
|
function formatKarmaGain(points, checksInRow) {
|
|
6104
|
-
const base =
|
|
6083
|
+
const base = import_picocolors6.default.green(`+${points} karma`);
|
|
6105
6084
|
if (checksInRow && checksInRow > 1) {
|
|
6106
|
-
return `${base} ${
|
|
6085
|
+
return `${base} ${import_picocolors6.default.yellow(`streak x${checksInRow}!`)}`;
|
|
6107
6086
|
}
|
|
6108
6087
|
return base;
|
|
6109
6088
|
}
|
|
6110
6089
|
function formatProgressSummary(completed, total) {
|
|
6111
|
-
return
|
|
6090
|
+
return import_picocolors6.default.dim(`${completed}/${total} done today`);
|
|
6112
6091
|
}
|
|
6113
6092
|
function formatTagsSummary(tasks) {
|
|
6114
6093
|
const counts = {};
|
|
@@ -6120,7 +6099,7 @@ function formatTagsSummary(tasks) {
|
|
|
6120
6099
|
}
|
|
6121
6100
|
const entries = Object.entries(counts);
|
|
6122
6101
|
if (entries.length === 0) return "";
|
|
6123
|
-
return entries.map(([tag, count]) => `${
|
|
6102
|
+
return entries.map(([tag, count]) => `${import_picocolors6.default.cyan("#" + tag)}${import_picocolors6.default.dim("(" + count + ")")}`).join(" ");
|
|
6124
6103
|
}
|
|
6125
6104
|
|
|
6126
6105
|
// src/cli/commands/tasks.ts
|
|
@@ -8994,6 +8973,99 @@ function parseHumanDateOnly(input) {
|
|
|
8994
8973
|
return result.slice(0, 10);
|
|
8995
8974
|
}
|
|
8996
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
|
+
|
|
8997
9069
|
// src/cli/lib/stdin.ts
|
|
8998
9070
|
var fs3 = __toESM(require("fs"), 1);
|
|
8999
9071
|
function readStdinLines() {
|
|
@@ -9002,13 +9074,16 @@ function readStdinLines() {
|
|
|
9002
9074
|
}
|
|
9003
9075
|
|
|
9004
9076
|
// src/cli/commands/tasks.ts
|
|
9005
|
-
|
|
9077
|
+
function collectValue(value, previous) {
|
|
9078
|
+
return previous.concat([value]);
|
|
9079
|
+
}
|
|
9080
|
+
async function pickTask(id, actionName) {
|
|
9006
9081
|
if (id) return id;
|
|
9007
9082
|
if (!isInteractive()) {
|
|
9008
9083
|
throw Errors.missingArg("Task ID", "id");
|
|
9009
9084
|
}
|
|
9010
|
-
const today2 = (
|
|
9011
|
-
const { tasks } = await listTasks(
|
|
9085
|
+
const today2 = localDateOnly();
|
|
9086
|
+
const { tasks } = await listTasks({ date: today2 });
|
|
9012
9087
|
const pending = tasks.filter((t2) => !t2.completed);
|
|
9013
9088
|
if (pending.length === 0) {
|
|
9014
9089
|
throw Errors.invalidInput(`No pending tasks for today (${today2}). Use: numo tasks ${actionName} <id>`);
|
|
@@ -9017,23 +9092,21 @@ async function pickTask(uid, id, actionName) {
|
|
|
9017
9092
|
message: `Select task to ${actionName}`,
|
|
9018
9093
|
options: pending.map((t2) => ({
|
|
9019
9094
|
value: t2.id,
|
|
9020
|
-
label: `${truncate(t2.text, 50)} ${
|
|
9095
|
+
label: `${truncate(t2.text, 50)} ${import_picocolors7.default.dim(t2.id)}`
|
|
9021
9096
|
}))
|
|
9022
9097
|
});
|
|
9023
9098
|
return selected;
|
|
9024
9099
|
}
|
|
9025
9100
|
function resolveDate(opts) {
|
|
9026
9101
|
if (opts.backlog) return void 0;
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
if (opts.yesterday) return fmt(new Date(today2.getTime() - 864e5));
|
|
9030
|
-
if (opts.tomorrow) return fmt(new Date(today2.getTime() + 864e5));
|
|
9102
|
+
if (opts.yesterday) return localDateOffset(-1);
|
|
9103
|
+
if (opts.tomorrow) return localDateOffset(1);
|
|
9031
9104
|
if (opts.date) {
|
|
9032
9105
|
const parsed = parseHumanDateOnly(opts.date);
|
|
9033
9106
|
if (!parsed) throw Errors.invalidInput(`Cannot parse date: "${opts.date}". Use YYYY-MM-DD or natural language (tomorrow, next monday, etc.)`);
|
|
9034
9107
|
return parsed;
|
|
9035
9108
|
}
|
|
9036
|
-
return
|
|
9109
|
+
return localDateOnly();
|
|
9037
9110
|
}
|
|
9038
9111
|
function extractTime(dueDate) {
|
|
9039
9112
|
if (typeof dueDate !== "string") return "";
|
|
@@ -9046,9 +9119,9 @@ function isRepeating(t2) {
|
|
|
9046
9119
|
return !!t2.repeat?.type && t2.repeat.type !== "none";
|
|
9047
9120
|
}
|
|
9048
9121
|
function getCheckIndicator(t2) {
|
|
9049
|
-
if (t2.completed) return
|
|
9050
|
-
if (isRepeating(t2)) return
|
|
9051
|
-
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);
|
|
9052
9125
|
}
|
|
9053
9126
|
function sortTasksForDisplay(tasks) {
|
|
9054
9127
|
const repeatOrder = { daily: 0, weekly: 1, monthly: 2 };
|
|
@@ -9071,50 +9144,60 @@ function sortTasksForDisplay(tasks) {
|
|
|
9071
9144
|
});
|
|
9072
9145
|
}
|
|
9073
9146
|
function printTaskDetail(t2) {
|
|
9074
|
-
const dim =
|
|
9147
|
+
const dim = import_picocolors7.default.dim;
|
|
9075
9148
|
console.log("");
|
|
9076
9149
|
printRecord([
|
|
9077
9150
|
["ID", dim(t2.id)],
|
|
9078
9151
|
["Text", t2.text],
|
|
9079
9152
|
["Due", formatDate(t2.dueDate) || dim("none (backlog)")],
|
|
9080
|
-
["Status", t2.completed ?
|
|
9153
|
+
["Status", t2.completed ? import_picocolors7.default.green("completed") : import_picocolors7.default.yellow("pending")],
|
|
9081
9154
|
["Tags", formatTags(t2.tags) || dim("none")],
|
|
9082
9155
|
["Difficulty", formatDifficulty(t2.difficulty) || dim("not set")],
|
|
9083
9156
|
["Duration", formatDuration(t2.duration) || dim("not set")],
|
|
9084
9157
|
["Repeat", formatRepeat(t2.repeat) || dim("none")],
|
|
9085
9158
|
["Note", t2.note || dim("none")],
|
|
9086
|
-
["Public", t2.isPublic ?
|
|
9159
|
+
["Public", t2.isPublic ? import_picocolors7.default.green("yes") : import_picocolors7.default.yellow("no")],
|
|
9087
9160
|
["Completions", String(t2.completions ?? 0)],
|
|
9088
9161
|
["Created", formatDate(t2.createdAt)]
|
|
9089
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
|
+
}
|
|
9090
9170
|
console.log("");
|
|
9091
9171
|
}
|
|
9092
9172
|
function printTaskLine(t2) {
|
|
9093
9173
|
const check = getCheckIndicator(t2);
|
|
9094
9174
|
const rawText = truncate(t2.text, 50);
|
|
9095
|
-
const text = t2.completed ?
|
|
9175
|
+
const text = t2.completed ? import_picocolors7.default.strikethrough(import_picocolors7.default.dim(rawText)) : rawText;
|
|
9096
9176
|
const time = extractTime(t2.dueDate);
|
|
9097
9177
|
const tags = formatTags(t2.tags);
|
|
9098
9178
|
const difficulty = formatDifficulty(t2.difficulty);
|
|
9099
|
-
const id =
|
|
9179
|
+
const id = import_picocolors7.default.dim(t2.id);
|
|
9100
9180
|
const parts = [check, text];
|
|
9101
|
-
if (time) parts.push(
|
|
9181
|
+
if (time) parts.push(import_picocolors7.default.cyan(time));
|
|
9102
9182
|
if (tags) parts.push(tags);
|
|
9103
|
-
if (difficulty) parts.push(
|
|
9183
|
+
if (difficulty) parts.push(import_picocolors7.default.dim(`[${difficulty}]`));
|
|
9104
9184
|
parts.push(id);
|
|
9105
9185
|
console.log(" " + parts.join(" "));
|
|
9106
9186
|
}
|
|
9187
|
+
function printPartialNotice(failed) {
|
|
9188
|
+
console.log(` ${import_picocolors7.default.yellow("! some bookkeeping was deferred")} ${import_picocolors7.default.dim(`(${(failed ?? []).join(", ")})`)}`);
|
|
9189
|
+
}
|
|
9107
9190
|
function registerTasksCommands(program3) {
|
|
9108
9191
|
const tasks = program3.command("tasks").description("Manage tasks");
|
|
9109
|
-
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() {
|
|
9110
9193
|
const opts = this.optsWithGlobals();
|
|
9111
|
-
|
|
9194
|
+
requireUid();
|
|
9112
9195
|
const date = resolveDate(opts);
|
|
9113
9196
|
await runList({
|
|
9114
9197
|
global: opts,
|
|
9115
|
-
fn: () => listTasks(
|
|
9198
|
+
fn: () => listTasks({ date, backlog: opts.backlog, tag: opts.tag }),
|
|
9116
9199
|
dataKey: "tasks",
|
|
9117
|
-
columns: ["id", "text", "dueDate", "completed", "tags"
|
|
9200
|
+
columns: ["id", "text", "dueDate", "completed", "tags"],
|
|
9118
9201
|
spinnerMessage: "Fetching tasks...",
|
|
9119
9202
|
onInteractive: (payload) => {
|
|
9120
9203
|
const items = payload.tasks;
|
|
@@ -9122,7 +9205,7 @@ function registerTasksCommands(program3) {
|
|
|
9122
9205
|
const completed = items.filter((t2) => t2.completed);
|
|
9123
9206
|
console.log("");
|
|
9124
9207
|
if (opts.backlog) {
|
|
9125
|
-
console.log(` ${
|
|
9208
|
+
console.log(` ${import_picocolors7.default.bold("Backlog")} ${import_picocolors7.default.dim(`(${items.length})`)}`);
|
|
9126
9209
|
} else {
|
|
9127
9210
|
const viewDate = date ? /* @__PURE__ */ new Date(date + "T00:00:00") : /* @__PURE__ */ new Date();
|
|
9128
9211
|
console.log(formatWeekdayHeader(viewDate, completed.length));
|
|
@@ -9135,18 +9218,18 @@ function registerTasksCommands(program3) {
|
|
|
9135
9218
|
const viewDate = date ? /* @__PURE__ */ new Date(date + "T00:00:00") : /* @__PURE__ */ new Date();
|
|
9136
9219
|
const dayName = viewDate.toLocaleDateString("en-US", { weekday: "long" });
|
|
9137
9220
|
console.log(`
|
|
9138
|
-
${
|
|
9139
|
-
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")}`);
|
|
9140
9223
|
} else {
|
|
9141
9224
|
console.log(`
|
|
9142
|
-
${
|
|
9225
|
+
${import_picocolors7.default.dim("No backlog tasks.")}`);
|
|
9143
9226
|
}
|
|
9144
9227
|
console.log("");
|
|
9145
9228
|
return;
|
|
9146
9229
|
}
|
|
9147
9230
|
if (pending.length > 0) {
|
|
9148
9231
|
console.log(`
|
|
9149
|
-
${
|
|
9232
|
+
${import_picocolors7.default.bold("Pending")} ${import_picocolors7.default.dim(`(${pending.length})`)}
|
|
9150
9233
|
`);
|
|
9151
9234
|
for (const t2 of pending) {
|
|
9152
9235
|
printTaskLine(t2);
|
|
@@ -9154,7 +9237,7 @@ function registerTasksCommands(program3) {
|
|
|
9154
9237
|
}
|
|
9155
9238
|
if (completed.length > 0) {
|
|
9156
9239
|
console.log(`
|
|
9157
|
-
${
|
|
9240
|
+
${import_picocolors7.default.dim(`Completed (${completed.length})`)}
|
|
9158
9241
|
`);
|
|
9159
9242
|
for (const t2 of completed) {
|
|
9160
9243
|
printTaskLine(t2);
|
|
@@ -9163,7 +9246,7 @@ function registerTasksCommands(program3) {
|
|
|
9163
9246
|
if (!opts.backlog) {
|
|
9164
9247
|
console.log(`
|
|
9165
9248
|
${formatProgressSummary(completed.length, items.length)}`);
|
|
9166
|
-
console.log(` ${
|
|
9249
|
+
console.log(` ${import_picocolors7.default.dim("--yesterday \xB7 --tomorrow \xB7 --date YYYY-MM-DD")}`);
|
|
9167
9250
|
}
|
|
9168
9251
|
console.log("");
|
|
9169
9252
|
}
|
|
@@ -9178,11 +9261,11 @@ Examples:
|
|
|
9178
9261
|
$ numo tasks list --tag Work # Filter by tag
|
|
9179
9262
|
$ numo tasks list --json | jq '.tasks[].text'`);
|
|
9180
9263
|
tasks.command("get [id]").description("Get a task by ID").action(async function(id) {
|
|
9181
|
-
|
|
9264
|
+
requireUid();
|
|
9182
9265
|
const taskId = await promptForMissing({ value: id, message: "Task ID" });
|
|
9183
9266
|
await runGet({
|
|
9184
9267
|
global: this.optsWithGlobals(),
|
|
9185
|
-
fn: () => getTask(
|
|
9268
|
+
fn: () => getTask(taskId),
|
|
9186
9269
|
spinnerMessage: "Fetching task...",
|
|
9187
9270
|
onInteractive: printTaskDetail
|
|
9188
9271
|
});
|
|
@@ -9190,171 +9273,165 @@ Examples:
|
|
|
9190
9273
|
Examples:
|
|
9191
9274
|
$ numo tasks get abc123
|
|
9192
9275
|
$ numo tasks get abc123 --json | jq '.text'`);
|
|
9193
|
-
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) {
|
|
9194
9277
|
const opts = this.optsWithGlobals();
|
|
9195
|
-
|
|
9196
|
-
const
|
|
9197
|
-
const
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
9202
|
-
|
|
9203
|
-
|
|
9204
|
-
|
|
9205
|
-
|
|
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",
|
|
9206
9343
|
options: [
|
|
9207
|
-
{ value: "
|
|
9208
|
-
{ value: "
|
|
9209
|
-
{ value: "
|
|
9210
|
-
{ value: "
|
|
9211
|
-
{ value: "
|
|
9212
|
-
{ value: "
|
|
9213
|
-
{ 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" }
|
|
9214
9353
|
]
|
|
9215
9354
|
});
|
|
9216
|
-
if (
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
body.dueDate = fmt(tomorrow2);
|
|
9220
|
-
} else if (schedule === "pick") {
|
|
9221
|
-
const date = await promptText({ message: "Date", placeholder: fmt(tomorrow2), required: true });
|
|
9222
|
-
body.dueDate = date;
|
|
9223
|
-
} else if (["daily", "weekly", "monthly"].includes(schedule)) {
|
|
9224
|
-
const repeat = {
|
|
9225
|
-
type: schedule,
|
|
9226
|
-
every: 1,
|
|
9227
|
-
end: "never",
|
|
9228
|
-
endDate: null,
|
|
9229
|
-
endAfter: null,
|
|
9230
|
-
monthDays: null,
|
|
9231
|
-
weekDays: null
|
|
9232
|
-
};
|
|
9233
|
-
if (schedule === "weekly") {
|
|
9234
|
-
const days = await promptMultiSelect({
|
|
9235
|
-
message: "Days of week",
|
|
9236
|
-
options: [
|
|
9237
|
-
{ value: "Mon", label: "Monday" },
|
|
9238
|
-
{ value: "Tue", label: "Tuesday" },
|
|
9239
|
-
{ value: "Wed", label: "Wednesday" },
|
|
9240
|
-
{ value: "Thu", label: "Thursday" },
|
|
9241
|
-
{ value: "Fri", label: "Friday" },
|
|
9242
|
-
{ value: "Sat", label: "Saturday" },
|
|
9243
|
-
{ value: "Sun", label: "Sunday" }
|
|
9244
|
-
],
|
|
9245
|
-
required: true
|
|
9246
|
-
});
|
|
9247
|
-
repeat.weekDays = days;
|
|
9248
|
-
} else if (schedule === "monthly") {
|
|
9249
|
-
const daysInput = await promptText({ message: "Days of month", placeholder: "1,15", required: true });
|
|
9250
|
-
repeat.monthDays = daysInput.split(",").map((s) => parseInt(s.trim()));
|
|
9251
|
-
}
|
|
9252
|
-
body.repeat = repeat;
|
|
9253
|
-
body.dueDate = fmt(today2);
|
|
9254
|
-
}
|
|
9255
|
-
}
|
|
9256
|
-
if (!opts.private && !opts.public) {
|
|
9257
|
-
const visibility = await promptSelect({
|
|
9258
|
-
message: "Visibility",
|
|
9355
|
+
if (tags.length > 0) body.tags = tags;
|
|
9356
|
+
const difficulty = await promptSelect({
|
|
9357
|
+
message: "Effort",
|
|
9259
9358
|
options: [
|
|
9260
|
-
{ value: "
|
|
9261
|
-
{ 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" }
|
|
9262
9364
|
]
|
|
9263
9365
|
});
|
|
9264
|
-
if (
|
|
9265
|
-
|
|
9266
|
-
const addDetails = await promptConfirm({
|
|
9267
|
-
message: "Add details? (tags, effort, time, note)",
|
|
9268
|
-
initialValue: false
|
|
9269
|
-
});
|
|
9270
|
-
if (addDetails) {
|
|
9271
|
-
if (!opts.tags) {
|
|
9272
|
-
const tags = await promptMultiSelect({
|
|
9273
|
-
message: "Tags",
|
|
9274
|
-
options: [
|
|
9275
|
-
{ value: "House", label: "House" },
|
|
9276
|
-
{ value: "Work", label: "Work" },
|
|
9277
|
-
{ value: "Study", label: "Study" },
|
|
9278
|
-
{ value: "Hobby", label: "Hobby" },
|
|
9279
|
-
{ value: "Health", label: "Health" },
|
|
9280
|
-
{ value: "Relationship", label: "Relationship" },
|
|
9281
|
-
{ value: "Self-care", label: "Self-care" },
|
|
9282
|
-
{ value: "Relax", label: "Relax" },
|
|
9283
|
-
{ value: "Kids", label: "Kids" }
|
|
9284
|
-
]
|
|
9285
|
-
});
|
|
9286
|
-
if (tags.length > 0) body.tags = tags;
|
|
9287
|
-
}
|
|
9288
|
-
if (opts.difficulty === void 0) {
|
|
9289
|
-
const difficulty = await promptSelect({
|
|
9290
|
-
message: "Effort",
|
|
9291
|
-
options: [
|
|
9292
|
-
{ value: "skip", label: "Skip" },
|
|
9293
|
-
{ value: "0", label: "S \u2014 Tiny" },
|
|
9294
|
-
{ value: "1", label: "M \u2014 Medium" },
|
|
9295
|
-
{ value: "2", label: "L \u2014 High" },
|
|
9296
|
-
{ value: "3", label: "XL \u2014 Huge" }
|
|
9297
|
-
]
|
|
9298
|
-
});
|
|
9299
|
-
if (difficulty !== "skip") body.difficulty = parseInt(difficulty);
|
|
9300
|
-
}
|
|
9301
|
-
const addTime = await promptConfirm({ message: "Add a specific time?", initialValue: false });
|
|
9302
|
-
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 })) {
|
|
9303
9368
|
const time = await promptText({ message: "Time", placeholder: "09:30", required: true });
|
|
9304
9369
|
body.dueDate = `${body.dueDate} ${time}`;
|
|
9305
9370
|
}
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
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;
|
|
9309
9377
|
}
|
|
9310
9378
|
}
|
|
9311
|
-
if (opts.public) body.isPublic = true;
|
|
9312
|
-
if (opts.private) body.isPublic = false;
|
|
9313
|
-
if (opts.tags && !body.tags) body.tags = opts.tags.split(",");
|
|
9314
|
-
if (opts.note && !body.note) body.note = opts.note;
|
|
9315
9379
|
} else {
|
|
9316
|
-
|
|
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) {
|
|
9317
9386
|
const parsed = parseHumanDate(opts.due);
|
|
9318
9387
|
if (!parsed) throw Errors.invalidInput(`Cannot parse date: "${opts.due}"`);
|
|
9319
9388
|
body.dueDate = parsed;
|
|
9389
|
+
} else {
|
|
9390
|
+
body.dueDate = localDateOnly();
|
|
9320
9391
|
}
|
|
9392
|
+
const repeat = buildRepeatConfig(opts);
|
|
9393
|
+
if (repeat) body.repeat = repeat;
|
|
9321
9394
|
if (opts.tags) body.tags = opts.tags.split(",");
|
|
9322
9395
|
if (opts.note) body.note = opts.note;
|
|
9323
|
-
if (opts.priority) body.priority = parseFloat(opts.priority);
|
|
9324
9396
|
if (opts.difficulty !== void 0) body.difficulty = parseInt(opts.difficulty);
|
|
9325
9397
|
if (opts.duration) body.duration = parseInt(opts.duration);
|
|
9326
|
-
if (opts.
|
|
9327
|
-
|
|
9398
|
+
if (opts.subtask && opts.subtask.length) body.subtasks = buildSubtasks(opts.subtask);
|
|
9399
|
+
body.isPublic = opts.public ? true : false;
|
|
9328
9400
|
}
|
|
9401
|
+
if (opts.clientTaskId) body.clientTaskId = opts.clientTaskId;
|
|
9402
|
+
body.listPosition = "top";
|
|
9403
|
+
normalizeDueDateInBody(body);
|
|
9329
9404
|
await runCreate({
|
|
9330
9405
|
global: opts,
|
|
9331
|
-
fn: () => createTask(
|
|
9406
|
+
fn: () => createTask(body),
|
|
9332
9407
|
dataKey: "task",
|
|
9333
9408
|
spinnerMessage: "Creating task...",
|
|
9334
9409
|
onInteractive: (_task, payload) => {
|
|
9335
|
-
const { task, karma } = payload;
|
|
9336
|
-
const check =
|
|
9410
|
+
const { task, karma, idempotentReplay } = payload;
|
|
9411
|
+
const check = import_picocolors7.default.green(SYM.check);
|
|
9337
9412
|
console.log(`
|
|
9338
|
-
${check} Created ${task.text}`);
|
|
9339
|
-
if (karma) {
|
|
9340
|
-
console.log(` ${formatKarmaGain(karma)}${" ".repeat(20)}${import_picocolors8.default.dim(task.id)}`);
|
|
9341
|
-
}
|
|
9413
|
+
${check} ${idempotentReplay ? "Exists" : "Created"} ${task.text} ${import_picocolors7.default.dim(task.id)}`);
|
|
9414
|
+
if (karma) console.log(` ${formatKarmaGain(karma)}`);
|
|
9342
9415
|
console.log("");
|
|
9343
9416
|
}
|
|
9344
9417
|
});
|
|
9345
9418
|
}).addHelpText("after", `
|
|
9346
9419
|
Examples:
|
|
9347
|
-
$ numo tasks create
|
|
9348
|
-
$ numo tasks create
|
|
9349
|
-
$ numo tasks create
|
|
9350
|
-
$ numo tasks create
|
|
9351
|
-
$ numo tasks create
|
|
9352
|
-
|
|
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) {
|
|
9353
9429
|
const opts = this.optsWithGlobals();
|
|
9354
|
-
|
|
9430
|
+
requireUid();
|
|
9355
9431
|
const taskId = await promptForMissing({ value: id, message: "Task ID" });
|
|
9432
|
+
const clearTime = opts.clearTime === true || opts.time === false;
|
|
9356
9433
|
const body = {};
|
|
9357
|
-
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;
|
|
9358
9435
|
if (!hasAnyFlag && isInteractive() && !opts.json) {
|
|
9359
9436
|
const text = await promptText({ message: "Text (enter to skip)", required: false });
|
|
9360
9437
|
if (text) body.text = text;
|
|
@@ -9364,8 +9441,6 @@ Examples:
|
|
|
9364
9441
|
if (tags) body.tags = tags.split(",");
|
|
9365
9442
|
const note = await promptText({ message: "Note (enter to skip)", required: false });
|
|
9366
9443
|
if (note) body.note = note;
|
|
9367
|
-
const priority = await promptText({ message: "Priority (enter to skip)", placeholder: "0.1\u20131.0", required: false });
|
|
9368
|
-
if (priority) body.priority = parseFloat(priority);
|
|
9369
9444
|
const difficulty = await promptText({ message: "Difficulty (enter to skip)", placeholder: "0\u20133", required: false });
|
|
9370
9445
|
if (difficulty) body.difficulty = parseInt(difficulty);
|
|
9371
9446
|
const duration = await promptText({ message: "Duration in minutes (enter to skip)", placeholder: "10", required: false });
|
|
@@ -9381,18 +9456,28 @@ Examples:
|
|
|
9381
9456
|
if (opts.public) body.isPublic = true;
|
|
9382
9457
|
if (opts.private) body.isPublic = false;
|
|
9383
9458
|
if (opts.note) body.note = opts.note;
|
|
9384
|
-
if (opts.priority) body.priority = parseFloat(opts.priority);
|
|
9385
9459
|
if (opts.difficulty !== void 0) body.difficulty = parseInt(opts.difficulty);
|
|
9386
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
|
+
}
|
|
9387
9471
|
}
|
|
9472
|
+
normalizeDueDateInBody(body);
|
|
9388
9473
|
await runWrite({
|
|
9389
9474
|
global: opts,
|
|
9390
|
-
fn: () => updateTask(
|
|
9475
|
+
fn: () => updateTask(taskId, body),
|
|
9391
9476
|
dataKey: "task",
|
|
9392
9477
|
spinnerMessage: "Updating task...",
|
|
9393
9478
|
onInteractive: (payload) => {
|
|
9394
9479
|
console.log(`
|
|
9395
|
-
${
|
|
9480
|
+
${import_picocolors7.default.green("Updated!")} ${payload.task.text} ${import_picocolors7.default.dim(payload.task.id)}
|
|
9396
9481
|
`);
|
|
9397
9482
|
}
|
|
9398
9483
|
});
|
|
@@ -9400,28 +9485,35 @@ Examples:
|
|
|
9400
9485
|
Examples:
|
|
9401
9486
|
$ numo tasks update abc123 --text "Updated text"
|
|
9402
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
|
|
9403
9490
|
$ numo tasks update abc123 --tags Work,Health
|
|
9404
|
-
$ 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`);
|
|
9405
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) {
|
|
9406
9497
|
const opts = this.optsWithGlobals();
|
|
9407
|
-
|
|
9498
|
+
requireUid();
|
|
9408
9499
|
if (opts.stdin) {
|
|
9409
9500
|
const ids = readStdinLines();
|
|
9410
9501
|
for (const lineId of ids) {
|
|
9411
9502
|
try {
|
|
9412
|
-
const result = await deleteTask(
|
|
9503
|
+
const result = await deleteTask(lineId);
|
|
9413
9504
|
printNdjsonLine({ id: lineId, ...result });
|
|
9414
9505
|
} catch (err) {
|
|
9506
|
+
process.exitCode = ExitCode.GENERAL;
|
|
9415
9507
|
printNdjsonLine({ id: lineId, error: err.message });
|
|
9416
9508
|
}
|
|
9417
9509
|
}
|
|
9418
9510
|
return;
|
|
9419
9511
|
}
|
|
9420
|
-
const taskId = await pickTask(
|
|
9512
|
+
const taskId = await pickTask(id, "delete");
|
|
9421
9513
|
if (isInteractive() && !opts.yes && !opts.json) {
|
|
9422
9514
|
let taskText = taskId;
|
|
9423
9515
|
try {
|
|
9424
|
-
const task = await getTask(
|
|
9516
|
+
const task = await getTask(taskId);
|
|
9425
9517
|
taskText = task.text ?? taskId;
|
|
9426
9518
|
} catch {
|
|
9427
9519
|
}
|
|
@@ -9430,19 +9522,20 @@ Examples:
|
|
|
9430
9522
|
initialValue: false
|
|
9431
9523
|
});
|
|
9432
9524
|
if (!confirmed) {
|
|
9433
|
-
console.log(
|
|
9525
|
+
console.log(import_picocolors7.default.dim(" Cancelled."));
|
|
9434
9526
|
return;
|
|
9435
9527
|
}
|
|
9436
9528
|
}
|
|
9437
9529
|
await runWrite({
|
|
9438
9530
|
global: opts,
|
|
9439
|
-
fn: () => deleteTask(
|
|
9531
|
+
fn: () => deleteTask(taskId),
|
|
9440
9532
|
spinnerMessage: "Deleting task...",
|
|
9441
9533
|
onInteractive: (data) => {
|
|
9442
|
-
const cross =
|
|
9534
|
+
const cross = import_picocolors7.default.red(SYM.cross);
|
|
9443
9535
|
console.log(`
|
|
9444
9536
|
${cross} Deleted ${data.taskText || taskId}`);
|
|
9445
|
-
if (data.archived) console.log(` ${
|
|
9537
|
+
if (data.archived) console.log(` ${import_picocolors7.default.dim("Archived")}`);
|
|
9538
|
+
if (data.partial) printPartialNotice(data.failed);
|
|
9446
9539
|
console.log("");
|
|
9447
9540
|
}
|
|
9448
9541
|
});
|
|
@@ -9453,31 +9546,41 @@ Examples:
|
|
|
9453
9546
|
$ numo tasks delete abc123 --json`);
|
|
9454
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) {
|
|
9455
9548
|
const opts = this.optsWithGlobals();
|
|
9456
|
-
|
|
9549
|
+
requireUid();
|
|
9550
|
+
if (opts.date && !isCompletableDate(opts.date)) {
|
|
9551
|
+
throw Errors.invalidInput("completion date must be today or yesterday");
|
|
9552
|
+
}
|
|
9457
9553
|
if (opts.stdin) {
|
|
9458
9554
|
const ids = readStdinLines();
|
|
9459
9555
|
for (const lineId of ids) {
|
|
9460
9556
|
try {
|
|
9461
|
-
const result = await completeTask(
|
|
9557
|
+
const result = await completeTask(lineId, opts.date);
|
|
9462
9558
|
printNdjsonLine({ id: lineId, ...result });
|
|
9463
9559
|
} catch (err) {
|
|
9560
|
+
process.exitCode = ExitCode.GENERAL;
|
|
9464
9561
|
printNdjsonLine({ id: lineId, error: err.message });
|
|
9465
9562
|
}
|
|
9466
9563
|
}
|
|
9467
9564
|
return;
|
|
9468
9565
|
}
|
|
9469
|
-
const taskId = await pickTask(
|
|
9566
|
+
const taskId = await pickTask(id, "complete");
|
|
9470
9567
|
await runWrite({
|
|
9471
9568
|
global: opts,
|
|
9472
|
-
fn: () => completeTask(
|
|
9569
|
+
fn: () => completeTask(taskId, opts.date),
|
|
9473
9570
|
spinnerMessage: "Completing task...",
|
|
9474
9571
|
onInteractive: (data) => {
|
|
9475
|
-
const check =
|
|
9476
|
-
|
|
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(`
|
|
9477
9578
|
${check} Done! ${data.taskText ?? taskId}`);
|
|
9478
|
-
|
|
9479
|
-
|
|
9579
|
+
if (data.karma) {
|
|
9580
|
+
console.log(` ${formatKarmaGain(data.karma, data.checksInRow)}`);
|
|
9581
|
+
}
|
|
9480
9582
|
}
|
|
9583
|
+
if (data.partial) printPartialNotice(data.failed);
|
|
9481
9584
|
console.log("");
|
|
9482
9585
|
}
|
|
9483
9586
|
});
|
|
@@ -9487,14 +9590,15 @@ Examples:
|
|
|
9487
9590
|
$ numo tasks complete abc123 --date "2026-03-26 14:30"`);
|
|
9488
9591
|
tasks.command("uncomplete [id]").description("Mark task as incomplete").option("--stdin", "Read task IDs from stdin (one per line)").action(async function(id) {
|
|
9489
9592
|
const opts = this.optsWithGlobals();
|
|
9490
|
-
|
|
9593
|
+
requireUid();
|
|
9491
9594
|
if (opts.stdin) {
|
|
9492
9595
|
const ids = readStdinLines();
|
|
9493
9596
|
for (const lineId of ids) {
|
|
9494
9597
|
try {
|
|
9495
|
-
const result = await uncompleteTask(
|
|
9598
|
+
const result = await uncompleteTask(lineId);
|
|
9496
9599
|
printNdjsonLine({ id: lineId, ...result });
|
|
9497
9600
|
} catch (err) {
|
|
9601
|
+
process.exitCode = ExitCode.GENERAL;
|
|
9498
9602
|
printNdjsonLine({ id: lineId, error: err.message });
|
|
9499
9603
|
}
|
|
9500
9604
|
}
|
|
@@ -9503,14 +9607,15 @@ Examples:
|
|
|
9503
9607
|
const taskId = await promptForMissing({ value: id, message: "Task ID" });
|
|
9504
9608
|
await runWrite({
|
|
9505
9609
|
global: opts,
|
|
9506
|
-
fn: () => uncompleteTask(
|
|
9610
|
+
fn: () => uncompleteTask(taskId),
|
|
9507
9611
|
dataKey: "task",
|
|
9508
9612
|
spinnerMessage: "Uncompleting task...",
|
|
9509
9613
|
onInteractive: (data) => {
|
|
9510
9614
|
const arrow = SYM.undo;
|
|
9511
9615
|
console.log(`
|
|
9512
|
-
${
|
|
9513
|
-
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);
|
|
9514
9619
|
console.log("");
|
|
9515
9620
|
}
|
|
9516
9621
|
});
|
|
@@ -9520,16 +9625,16 @@ Examples:
|
|
|
9520
9625
|
}
|
|
9521
9626
|
|
|
9522
9627
|
// src/cli/commands/posts.ts
|
|
9523
|
-
var
|
|
9628
|
+
var import_picocolors9 = __toESM(require_picocolors(), 1);
|
|
9524
9629
|
|
|
9525
9630
|
// src/cli/lib/pagination.ts
|
|
9526
|
-
var
|
|
9631
|
+
var import_picocolors8 = __toESM(require_picocolors(), 1);
|
|
9527
9632
|
function printPaginationHint(opts) {
|
|
9528
9633
|
if (!opts.nextCursor) return;
|
|
9529
9634
|
const parts = [opts.command, `--cursor ${opts.nextCursor}`];
|
|
9530
9635
|
if (opts.limit) parts.push(`--limit ${opts.limit}`);
|
|
9531
9636
|
console.log(`
|
|
9532
|
-
${
|
|
9637
|
+
${import_picocolors8.default.dim("Next page:")} ${import_picocolors8.default.dim("$")} numo ${parts.join(" ")}`);
|
|
9533
9638
|
}
|
|
9534
9639
|
|
|
9535
9640
|
// src/cli/services/posts.ts
|
|
@@ -9543,15 +9648,6 @@ async function listPosts(opts) {
|
|
|
9543
9648
|
async function getPost(id) {
|
|
9544
9649
|
return api.get(`/api/posts/${encodeURIComponent(id)}`);
|
|
9545
9650
|
}
|
|
9546
|
-
async function createPost(uid, body) {
|
|
9547
|
-
return api.post("/api/posts", body);
|
|
9548
|
-
}
|
|
9549
|
-
async function updatePost(uid, id, body) {
|
|
9550
|
-
return api.patch(`/api/posts/${encodeURIComponent(id)}`, body);
|
|
9551
|
-
}
|
|
9552
|
-
async function deletePost(uid, id) {
|
|
9553
|
-
await api.del(`/api/posts/${encodeURIComponent(id)}`);
|
|
9554
|
-
}
|
|
9555
9651
|
|
|
9556
9652
|
// src/cli/services/comments.ts
|
|
9557
9653
|
init_api_client();
|
|
@@ -9561,12 +9657,6 @@ async function listComments(postId, opts) {
|
|
|
9561
9657
|
limit: opts.limit?.toString()
|
|
9562
9658
|
});
|
|
9563
9659
|
}
|
|
9564
|
-
async function createComment(uid, postId, text) {
|
|
9565
|
-
return api.post(`/api/posts/${encodeURIComponent(postId)}/comments`, { text });
|
|
9566
|
-
}
|
|
9567
|
-
async function deleteComment(uid, postId, commentId) {
|
|
9568
|
-
await api.del(`/api/posts/${encodeURIComponent(postId)}/comments/${encodeURIComponent(commentId)}`);
|
|
9569
|
-
}
|
|
9570
9660
|
|
|
9571
9661
|
// src/cli/services/replies.ts
|
|
9572
9662
|
init_api_client();
|
|
@@ -9576,68 +9666,36 @@ async function listReplies(postId, commentId, opts) {
|
|
|
9576
9666
|
limit: opts.limit?.toString()
|
|
9577
9667
|
});
|
|
9578
9668
|
}
|
|
9579
|
-
async function createReply(uid, postId, commentId, text) {
|
|
9580
|
-
return api.post(`/api/posts/${encodeURIComponent(postId)}/comments/${encodeURIComponent(commentId)}/replies`, { text });
|
|
9581
|
-
}
|
|
9582
|
-
async function deleteReply(uid, postId, commentId, replyId) {
|
|
9583
|
-
await api.del(`/api/posts/${encodeURIComponent(postId)}/comments/${encodeURIComponent(commentId)}/replies/${encodeURIComponent(replyId)}`);
|
|
9584
|
-
}
|
|
9585
|
-
|
|
9586
|
-
// src/cli/types/api.ts
|
|
9587
|
-
var POST_TAGS = ["general", "hack", "story", "meme", "other", "question", "hack-tip", "activity"];
|
|
9588
9669
|
|
|
9589
9670
|
// src/cli/commands/posts.ts
|
|
9590
9671
|
init_prompts();
|
|
9591
|
-
init_tty();
|
|
9592
|
-
async function pickComment(postId, commentId) {
|
|
9593
|
-
if (commentId) return commentId;
|
|
9594
|
-
if (!isInteractive()) throw new Error("Missing required argument: commentId. Use flags in non-interactive mode.");
|
|
9595
|
-
const { comments } = await listComments(postId, { limit: 50 });
|
|
9596
|
-
if (comments.length === 0) throw new Error("No comments on this post.");
|
|
9597
|
-
return promptSelect({
|
|
9598
|
-
message: "Select comment",
|
|
9599
|
-
options: comments.map((c) => ({
|
|
9600
|
-
value: c.id,
|
|
9601
|
-
label: `${truncate(c.text ?? "", 60)} ${import_picocolors10.default.dim(c.authorName ?? c.userId ?? "")}`
|
|
9602
|
-
}))
|
|
9603
|
-
});
|
|
9604
|
-
}
|
|
9605
|
-
async function pickReply(postId, commentId, replyId) {
|
|
9606
|
-
if (replyId) return replyId;
|
|
9607
|
-
if (!isInteractive()) throw new Error("Missing required argument: replyId. Use flags in non-interactive mode.");
|
|
9608
|
-
const { replies } = await listReplies(postId, commentId, { limit: 50 });
|
|
9609
|
-
if (replies.length === 0) throw new Error("No replies on this comment.");
|
|
9610
|
-
return promptSelect({
|
|
9611
|
-
message: "Select reply",
|
|
9612
|
-
options: replies.map((r) => ({
|
|
9613
|
-
value: r.id,
|
|
9614
|
-
label: `${truncate(r.text ?? "", 60)} ${import_picocolors10.default.dim(r.userId ?? "")}`
|
|
9615
|
-
}))
|
|
9616
|
-
});
|
|
9617
|
-
}
|
|
9618
9672
|
function printPostLine(p) {
|
|
9619
|
-
const tag =
|
|
9673
|
+
const tag = import_picocolors9.default.cyan(p.tag ?? "");
|
|
9620
9674
|
const title = truncate(p.title, 55);
|
|
9621
|
-
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`) : "";
|
|
9622
9678
|
const time = formatRelativeDate(p.createdAt);
|
|
9623
|
-
const id =
|
|
9624
|
-
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}`));
|
|
9625
9682
|
if (comments) parts.push(comments);
|
|
9626
|
-
parts.push(
|
|
9683
|
+
if (likes) parts.push(likes);
|
|
9684
|
+
parts.push(import_picocolors9.default.dim(time));
|
|
9627
9685
|
parts.push(id);
|
|
9628
9686
|
console.log(" " + parts.join(" "));
|
|
9629
9687
|
}
|
|
9630
9688
|
function printPostDetail(p) {
|
|
9631
9689
|
console.log("");
|
|
9632
|
-
console.log(` ${
|
|
9690
|
+
console.log(` ${import_picocolors9.default.bold(p.title)}`);
|
|
9633
9691
|
if (p.body) {
|
|
9634
|
-
console.log(` ${
|
|
9692
|
+
console.log(` ${import_picocolors9.default.dim(SYM.dash.repeat(40))}`);
|
|
9635
9693
|
console.log(` ${p.body}`);
|
|
9636
9694
|
}
|
|
9637
|
-
console.log(` ${
|
|
9695
|
+
console.log(` ${import_picocolors9.default.dim(SYM.dash.repeat(40))}`);
|
|
9638
9696
|
printRecord([
|
|
9639
|
-
["ID",
|
|
9640
|
-
["Tag",
|
|
9697
|
+
["ID", import_picocolors9.default.dim(p.id)],
|
|
9698
|
+
["Tag", import_picocolors9.default.cyan(p.tag ?? "")],
|
|
9641
9699
|
["Author", p.authorName ?? p.authorId],
|
|
9642
9700
|
["Comments", p.commentsCount != null ? String(p.commentsCount) : null],
|
|
9643
9701
|
["Likes", p.likesCount != null ? String(p.likesCount) : null],
|
|
@@ -9646,9 +9704,9 @@ function printPostDetail(p) {
|
|
|
9646
9704
|
console.log("");
|
|
9647
9705
|
}
|
|
9648
9706
|
function printCommentLine(c) {
|
|
9649
|
-
const author =
|
|
9650
|
-
const time =
|
|
9651
|
-
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`) : "";
|
|
9652
9710
|
const text = c.text ?? "";
|
|
9653
9711
|
console.log(` ${author} ${time}${replies}`);
|
|
9654
9712
|
console.log(` ${text}`);
|
|
@@ -9657,86 +9715,77 @@ function printCommentLine(c) {
|
|
|
9657
9715
|
function printReplyLine(r) {
|
|
9658
9716
|
const text = truncate(r.text ?? "", 60);
|
|
9659
9717
|
const time = formatRelativeDate(r.createdAt);
|
|
9660
|
-
const id =
|
|
9661
|
-
console.log(` ${text} ${
|
|
9718
|
+
const id = import_picocolors9.default.dim(r.id);
|
|
9719
|
+
console.log(` ${text} ${import_picocolors9.default.dim(time)} ${id}`);
|
|
9662
9720
|
}
|
|
9663
9721
|
function registerPostsCommands(program3) {
|
|
9664
|
-
const posts = program3.command("posts").description("
|
|
9722
|
+
const posts = program3.command("posts").description("Browse community posts and comments");
|
|
9665
9723
|
posts.command("list").description("List posts").option("--cursor <cursor>").option("--limit <n>", "Max results (<=50)", "20").action(async function() {
|
|
9666
9724
|
const opts = this.optsWithGlobals();
|
|
9667
|
-
const limit = parseInt(opts.limit);
|
|
9668
9725
|
await runList({
|
|
9669
9726
|
global: opts,
|
|
9670
|
-
fn: () => listPosts({ cursor: opts.cursor, limit }),
|
|
9727
|
+
fn: () => listPosts({ cursor: opts.cursor, limit: opts.limit ? parseInt(opts.limit) : void 0 }),
|
|
9671
9728
|
dataKey: "posts",
|
|
9672
|
-
columns: ["id", "title", "
|
|
9729
|
+
columns: ["id", "tag", "title", "authorName", "commentsCount", "likesCount", "createdAt"],
|
|
9673
9730
|
spinnerMessage: "Fetching posts...",
|
|
9674
9731
|
onInteractive: (payload) => {
|
|
9675
9732
|
const items = payload.posts;
|
|
9676
9733
|
if (items.length === 0) {
|
|
9677
|
-
console.log(
|
|
9734
|
+
console.log(import_picocolors9.default.dim(" No posts found."));
|
|
9678
9735
|
return;
|
|
9679
9736
|
}
|
|
9680
|
-
console.log(
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
for (const p of items) {
|
|
9684
|
-
printPostLine(p);
|
|
9685
|
-
}
|
|
9737
|
+
console.log("");
|
|
9738
|
+
for (const p of items) printPostLine(p);
|
|
9739
|
+
console.log("");
|
|
9686
9740
|
printPaginationHint({
|
|
9687
9741
|
nextCursor: payload.nextCursor,
|
|
9688
9742
|
command: "posts list",
|
|
9689
9743
|
limit: opts.limit
|
|
9690
9744
|
});
|
|
9691
|
-
console.log("");
|
|
9692
9745
|
}
|
|
9693
9746
|
});
|
|
9694
|
-
})
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
$ numo posts list --limit 10
|
|
9698
|
-
$ numo posts list --json | jq '.posts[].title'`);
|
|
9699
|
-
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();
|
|
9700
9750
|
const postId = await promptForMissing({ value: id, message: "Post ID" });
|
|
9701
9751
|
await runGet({
|
|
9702
|
-
global:
|
|
9752
|
+
global: opts,
|
|
9703
9753
|
fn: () => getPost(postId),
|
|
9704
9754
|
spinnerMessage: "Fetching post...",
|
|
9705
|
-
onInteractive: printPostDetail
|
|
9755
|
+
onInteractive: (post) => printPostDetail(post)
|
|
9706
9756
|
});
|
|
9707
|
-
})
|
|
9708
|
-
|
|
9709
|
-
$ numo posts get abc123
|
|
9710
|
-
$ numo posts get abc123 --json`);
|
|
9711
|
-
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) {
|
|
9712
9759
|
const opts = this.optsWithGlobals();
|
|
9713
9760
|
const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
|
|
9714
9761
|
await runList({
|
|
9715
9762
|
global: opts,
|
|
9716
|
-
fn: () => listComments(resolvedPostId, { cursor: opts.cursor }),
|
|
9763
|
+
fn: () => listComments(resolvedPostId, { cursor: opts.cursor, limit: opts.limit ? parseInt(opts.limit) : void 0 }),
|
|
9717
9764
|
dataKey: "comments",
|
|
9718
|
-
columns: ["id", "userId", "text", "createdAt"],
|
|
9765
|
+
columns: ["id", "userId", "authorName", "text", "repliesCount", "createdAt"],
|
|
9719
9766
|
spinnerMessage: "Fetching comments...",
|
|
9720
9767
|
onInteractive: (payload) => {
|
|
9721
9768
|
const items = payload.comments;
|
|
9722
9769
|
if (items.length === 0) {
|
|
9723
|
-
console.log(
|
|
9770
|
+
console.log(import_picocolors9.default.dim(" No comments yet."));
|
|
9724
9771
|
return;
|
|
9725
9772
|
}
|
|
9726
9773
|
console.log(`
|
|
9727
|
-
${
|
|
9774
|
+
${import_picocolors9.default.bold("Comments")} ${import_picocolors9.default.dim(`(${items.length})`)}
|
|
9728
9775
|
`);
|
|
9729
|
-
for (const c of items)
|
|
9730
|
-
|
|
9731
|
-
|
|
9732
|
-
|
|
9776
|
+
for (const c of items) printCommentLine(c);
|
|
9777
|
+
printPaginationHint({
|
|
9778
|
+
nextCursor: payload.nextCursor,
|
|
9779
|
+
command: `posts comments ${resolvedPostId}`,
|
|
9780
|
+
limit: opts.limit
|
|
9781
|
+
});
|
|
9733
9782
|
}
|
|
9734
9783
|
});
|
|
9735
9784
|
});
|
|
9736
|
-
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) {
|
|
9737
9786
|
const opts = this.optsWithGlobals();
|
|
9738
9787
|
const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
|
|
9739
|
-
const resolvedCommentId = await
|
|
9788
|
+
const resolvedCommentId = await promptForMissing({ value: commentId, message: "Comment ID" });
|
|
9740
9789
|
await runList({
|
|
9741
9790
|
global: opts,
|
|
9742
9791
|
fn: () => listReplies(resolvedPostId, resolvedCommentId, { cursor: opts.cursor }),
|
|
@@ -9746,11 +9795,11 @@ Examples:
|
|
|
9746
9795
|
onInteractive: (payload) => {
|
|
9747
9796
|
const items = payload.replies;
|
|
9748
9797
|
if (items.length === 0) {
|
|
9749
|
-
console.log(
|
|
9798
|
+
console.log(import_picocolors9.default.dim(" No replies yet."));
|
|
9750
9799
|
return;
|
|
9751
9800
|
}
|
|
9752
9801
|
console.log(`
|
|
9753
|
-
${
|
|
9802
|
+
${import_picocolors9.default.bold("Replies")} ${import_picocolors9.default.dim(`(${items.length})`)}
|
|
9754
9803
|
`);
|
|
9755
9804
|
for (const r of items) {
|
|
9756
9805
|
printReplyLine(r);
|
|
@@ -9759,129 +9808,6 @@ Examples:
|
|
|
9759
9808
|
}
|
|
9760
9809
|
});
|
|
9761
9810
|
});
|
|
9762
|
-
posts.command("create").description("Create a new post").option("--title <title>").option("--body <body>").option("--tag <tag>", "general|hack|story|meme|other|question|hack-tip|activity").action(async function() {
|
|
9763
|
-
const opts = this.optsWithGlobals();
|
|
9764
|
-
const uid = requireUid();
|
|
9765
|
-
const title = await promptForMissing({ value: opts.title, message: "Title", placeholder: "Post title" });
|
|
9766
|
-
const postBody = await promptForMissing({ value: opts.body, message: "Body", placeholder: "Post body" });
|
|
9767
|
-
let tag = opts.tag;
|
|
9768
|
-
if (!tag) {
|
|
9769
|
-
tag = await promptSelect({
|
|
9770
|
-
message: "Tag",
|
|
9771
|
-
options: POST_TAGS.map((t2) => ({ value: t2, label: t2 }))
|
|
9772
|
-
});
|
|
9773
|
-
}
|
|
9774
|
-
const body = { title, body: postBody, tag };
|
|
9775
|
-
await runCreate({
|
|
9776
|
-
global: opts,
|
|
9777
|
-
fn: () => createPost(uid, body),
|
|
9778
|
-
dataKey: "post",
|
|
9779
|
-
spinnerMessage: "Creating post...",
|
|
9780
|
-
onInteractive: (post, payload) => {
|
|
9781
|
-
console.log(`
|
|
9782
|
-
${import_picocolors10.default.green("Posted!")} ${payload.post.title} ${import_picocolors10.default.dim(payload.post.id)}
|
|
9783
|
-
`);
|
|
9784
|
-
}
|
|
9785
|
-
});
|
|
9786
|
-
});
|
|
9787
|
-
posts.command("update [id]").description("Update a post").option("--title <title>").option("--body <body>").option("--tag <tag>").action(async function(id) {
|
|
9788
|
-
const opts = this.optsWithGlobals();
|
|
9789
|
-
const uid = requireUid();
|
|
9790
|
-
const postId = await promptForMissing({ value: id, message: "Post ID" });
|
|
9791
|
-
const body = {};
|
|
9792
|
-
const hasAnyFlag = opts.title || opts.body || opts.tag;
|
|
9793
|
-
if (!hasAnyFlag && isInteractive() && !opts.json) {
|
|
9794
|
-
const title = await promptText({ message: "Title (enter to skip)", required: false });
|
|
9795
|
-
if (title) body.title = title;
|
|
9796
|
-
const postBody = await promptText({ message: "Body (enter to skip)", required: false });
|
|
9797
|
-
if (postBody) body.body = postBody;
|
|
9798
|
-
const changeTag = await promptText({ message: "Tag (enter to skip)", placeholder: POST_TAGS.join("|"), required: false });
|
|
9799
|
-
if (changeTag) body.tag = changeTag;
|
|
9800
|
-
} else {
|
|
9801
|
-
if (opts.title) body.title = opts.title;
|
|
9802
|
-
if (opts.body) body.body = opts.body;
|
|
9803
|
-
if (opts.tag) body.tag = opts.tag;
|
|
9804
|
-
}
|
|
9805
|
-
await runWrite({
|
|
9806
|
-
global: opts,
|
|
9807
|
-
fn: () => updatePost(uid, postId, body),
|
|
9808
|
-
dataKey: "post",
|
|
9809
|
-
spinnerMessage: "Updating post...",
|
|
9810
|
-
onInteractive: (payload) => {
|
|
9811
|
-
console.log(`
|
|
9812
|
-
${import_picocolors10.default.green("Updated!")} ${payload.post.title} ${import_picocolors10.default.dim(payload.post.id)}
|
|
9813
|
-
`);
|
|
9814
|
-
}
|
|
9815
|
-
});
|
|
9816
|
-
});
|
|
9817
|
-
posts.command("delete [id]").description("Delete a post").action(async function(id) {
|
|
9818
|
-
const uid = requireUid();
|
|
9819
|
-
const postId = await promptForMissing({ value: id, message: "Post ID" });
|
|
9820
|
-
await runDelete({
|
|
9821
|
-
global: this.optsWithGlobals(),
|
|
9822
|
-
fn: () => deletePost(uid, postId),
|
|
9823
|
-
successMessage: ` ${import_picocolors10.default.green("Deleted!")} Post ${import_picocolors10.default.dim(postId)}`,
|
|
9824
|
-
spinnerMessage: "Deleting post..."
|
|
9825
|
-
});
|
|
9826
|
-
});
|
|
9827
|
-
posts.command("comment [postId]").description("Add a comment to a post").option("--text <text>").action(async function(postId) {
|
|
9828
|
-
const opts = this.optsWithGlobals();
|
|
9829
|
-
const uid = requireUid();
|
|
9830
|
-
const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
|
|
9831
|
-
const text = await promptForMissing({ value: opts.text, message: "Comment text", placeholder: "Your comment" });
|
|
9832
|
-
await runCreate({
|
|
9833
|
-
global: opts,
|
|
9834
|
-
fn: () => createComment(uid, resolvedPostId, text),
|
|
9835
|
-
dataKey: "comment",
|
|
9836
|
-
spinnerMessage: "Adding comment...",
|
|
9837
|
-
onInteractive: (comment, payload) => {
|
|
9838
|
-
console.log(`
|
|
9839
|
-
${import_picocolors10.default.green("Commented!")} ${truncate(payload.comment.text ?? "", 50)} ${import_picocolors10.default.dim(payload.comment.id)}
|
|
9840
|
-
`);
|
|
9841
|
-
}
|
|
9842
|
-
});
|
|
9843
|
-
});
|
|
9844
|
-
posts.command("comment-delete [postId] [commentId]").description("Delete a comment").action(async function(postId, commentId) {
|
|
9845
|
-
const uid = requireUid();
|
|
9846
|
-
const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
|
|
9847
|
-
const resolvedCommentId = await pickComment(resolvedPostId, commentId);
|
|
9848
|
-
await runDelete({
|
|
9849
|
-
global: this.optsWithGlobals(),
|
|
9850
|
-
fn: () => deleteComment(uid, resolvedPostId, resolvedCommentId),
|
|
9851
|
-
successMessage: ` ${import_picocolors10.default.green("Deleted!")} Comment ${import_picocolors10.default.dim(resolvedCommentId)}`,
|
|
9852
|
-
spinnerMessage: "Deleting comment..."
|
|
9853
|
-
});
|
|
9854
|
-
});
|
|
9855
|
-
posts.command("reply [postId] [commentId]").description("Add a reply to a comment").option("--text <text>").action(async function(postId, commentId) {
|
|
9856
|
-
const opts = this.optsWithGlobals();
|
|
9857
|
-
const uid = requireUid();
|
|
9858
|
-
const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
|
|
9859
|
-
const resolvedCommentId = await pickComment(resolvedPostId, commentId);
|
|
9860
|
-
const text = await promptForMissing({ value: opts.text, message: "Reply text", placeholder: "Your reply" });
|
|
9861
|
-
await runCreate({
|
|
9862
|
-
global: opts,
|
|
9863
|
-
fn: () => createReply(uid, resolvedPostId, resolvedCommentId, text),
|
|
9864
|
-
dataKey: "reply",
|
|
9865
|
-
spinnerMessage: "Adding reply...",
|
|
9866
|
-
onInteractive: (reply, payload) => {
|
|
9867
|
-
console.log(`
|
|
9868
|
-
${import_picocolors10.default.green("Replied!")} ${truncate(payload.reply.text ?? "", 50)} ${import_picocolors10.default.dim(payload.reply.id)}
|
|
9869
|
-
`);
|
|
9870
|
-
}
|
|
9871
|
-
});
|
|
9872
|
-
});
|
|
9873
|
-
posts.command("reply-delete [postId] [commentId] [replyId]").description("Delete a reply").action(async function(postId, commentId, replyId) {
|
|
9874
|
-
const uid = requireUid();
|
|
9875
|
-
const resolvedPostId = await promptForMissing({ value: postId, message: "Post ID" });
|
|
9876
|
-
const resolvedCommentId = await pickComment(resolvedPostId, commentId);
|
|
9877
|
-
const resolvedReplyId = await pickReply(resolvedPostId, resolvedCommentId, replyId);
|
|
9878
|
-
await runDelete({
|
|
9879
|
-
global: this.optsWithGlobals(),
|
|
9880
|
-
fn: () => deleteReply(uid, resolvedPostId, resolvedCommentId, resolvedReplyId),
|
|
9881
|
-
successMessage: ` ${import_picocolors10.default.green("Deleted!")} Reply ${import_picocolors10.default.dim(resolvedReplyId)}`,
|
|
9882
|
-
spinnerMessage: "Deleting reply..."
|
|
9883
|
-
});
|
|
9884
|
-
});
|
|
9885
9811
|
}
|
|
9886
9812
|
|
|
9887
9813
|
// src/cli/services/profile.ts
|
|
@@ -9911,10 +9837,47 @@ function registerProfileCommands(program3) {
|
|
|
9911
9837
|
}
|
|
9912
9838
|
|
|
9913
9839
|
// src/cli/commands/doctor.ts
|
|
9914
|
-
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);
|
|
9915
9844
|
init_credentials();
|
|
9916
9845
|
init_api_client();
|
|
9917
|
-
|
|
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
|
+
}
|
|
9918
9881
|
async function runChecks() {
|
|
9919
9882
|
const checks = [];
|
|
9920
9883
|
const nodeVersion = process.version;
|
|
@@ -9924,55 +9887,107 @@ async function runChecks() {
|
|
|
9924
9887
|
status: major >= 18 ? "ok" : "fail",
|
|
9925
9888
|
message: major >= 18 ? `Node ${nodeVersion}` : `Node ${nodeVersion} \u2014 requires >= 18`
|
|
9926
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);
|
|
9927
9901
|
checks.push({
|
|
9928
9902
|
name: "api_url",
|
|
9929
|
-
status: process.env.NUMO_API_URL ? "ok" : "warn",
|
|
9930
|
-
message: process.env.NUMO_API_URL ? `API URL: ${API_BASE}` : `NUMO_API_URL not set (using default: ${API_BASE})`
|
|
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})`
|
|
9931
9905
|
});
|
|
9906
|
+
checks.push(await checkDns(apiUrl.hostname));
|
|
9907
|
+
if (apiUrl.protocol === "https:") {
|
|
9908
|
+
checks.push(await checkTls(apiUrl.hostname));
|
|
9909
|
+
}
|
|
9932
9910
|
const creds = loadCredentials();
|
|
9933
9911
|
checks.push({
|
|
9934
9912
|
name: "credentials",
|
|
9935
9913
|
status: creds ? "ok" : "fail",
|
|
9936
|
-
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"
|
|
9937
9916
|
});
|
|
9938
9917
|
if (creds) {
|
|
9939
9918
|
try {
|
|
9940
9919
|
await getIdToken();
|
|
9941
9920
|
checks.push({ name: "token", status: "ok", message: "Token valid / refreshed" });
|
|
9942
9921
|
} catch (err) {
|
|
9943
|
-
checks.push({
|
|
9922
|
+
checks.push({
|
|
9923
|
+
name: "token",
|
|
9924
|
+
status: "fail",
|
|
9925
|
+
message: `Token refresh failed: ${errMessage(err)}`,
|
|
9926
|
+
fix: "numo login"
|
|
9927
|
+
});
|
|
9944
9928
|
}
|
|
9945
9929
|
} else {
|
|
9946
9930
|
checks.push({ name: "token", status: "fail", message: "Skipped (no credentials)" });
|
|
9947
9931
|
}
|
|
9948
9932
|
try {
|
|
9949
9933
|
const resp = await fetch(`${API_BASE}/api/health`, { signal: AbortSignal.timeout(5e3) });
|
|
9950
|
-
checks.push({
|
|
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
|
+
});
|
|
9951
9940
|
} catch (err) {
|
|
9952
|
-
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
|
+
}
|
|
9953
9969
|
}
|
|
9954
9970
|
return checks;
|
|
9955
9971
|
}
|
|
9956
9972
|
function registerDoctorCommand(program3) {
|
|
9957
9973
|
program3.command("doctor").description("Check CLI health and connectivity").action(async function() {
|
|
9958
9974
|
const opts = this.optsWithGlobals();
|
|
9959
|
-
const asJson =
|
|
9975
|
+
const asJson = isQuietMode(opts);
|
|
9960
9976
|
const checks = await runChecks();
|
|
9961
9977
|
const ok = checks.every((c) => c.status !== "fail");
|
|
9962
9978
|
if (asJson) {
|
|
9963
|
-
printJson({ ok, checks });
|
|
9979
|
+
printJson({ ok, exitCode: ok ? 0 : 1, checks });
|
|
9964
9980
|
} else {
|
|
9965
9981
|
console.log("");
|
|
9966
9982
|
for (const check of checks) {
|
|
9967
|
-
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);
|
|
9968
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
|
+
}
|
|
9969
9988
|
}
|
|
9970
9989
|
console.log("");
|
|
9971
|
-
|
|
9972
|
-
console.log(` ${import_picocolors11.default.green("All checks passed.")}`);
|
|
9973
|
-
} else {
|
|
9974
|
-
console.log(` ${import_picocolors11.default.red("Some checks failed.")}`);
|
|
9975
|
-
}
|
|
9990
|
+
console.log(` ${ok ? import_picocolors10.default.green("All checks passed.") : import_picocolors10.default.red("Some checks failed.")}`);
|
|
9976
9991
|
console.log("");
|
|
9977
9992
|
}
|
|
9978
9993
|
if (!ok) process.exit(1);
|
|
@@ -9985,11 +10000,15 @@ init_dirs();
|
|
|
9985
10000
|
// src/cli/lib/update-check.ts
|
|
9986
10001
|
var fs4 = __toESM(require("fs"), 1);
|
|
9987
10002
|
var path2 = __toESM(require("path"), 1);
|
|
9988
|
-
var
|
|
10003
|
+
var import_picocolors11 = __toESM(require_picocolors(), 1);
|
|
9989
10004
|
init_dirs();
|
|
9990
10005
|
init_tty();
|
|
9991
10006
|
var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
|
|
9992
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`;
|
|
9993
10012
|
function getStatePath() {
|
|
9994
10013
|
return path2.join(getConfigDir(), "update-check.json");
|
|
9995
10014
|
}
|
|
@@ -10016,6 +10035,12 @@ function semverGt(a, b) {
|
|
|
10016
10035
|
}
|
|
10017
10036
|
return false;
|
|
10018
10037
|
}
|
|
10038
|
+
function isBinaryInstall() {
|
|
10039
|
+
return !!process.versions.bun;
|
|
10040
|
+
}
|
|
10041
|
+
function upgradeCommand(isBinary = isBinaryInstall()) {
|
|
10042
|
+
return isBinary ? UPGRADE_BINARY : UPGRADE_NPM;
|
|
10043
|
+
}
|
|
10019
10044
|
function checkForUpdate(currentVersion) {
|
|
10020
10045
|
if (!isInteractive()) return;
|
|
10021
10046
|
if (process.env.CI || process.env.NUMO_NO_UPDATE_CHECK) return;
|
|
@@ -10024,8 +10049,8 @@ function checkForUpdate(currentVersion) {
|
|
|
10024
10049
|
if (state.latestVersion && semverGt(state.latestVersion, currentVersion)) {
|
|
10025
10050
|
process.stderr.write(
|
|
10026
10051
|
`
|
|
10027
|
-
${
|
|
10028
|
-
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
|
|
10029
10054
|
|
|
10030
10055
|
`
|
|
10031
10056
|
);
|
|
@@ -10049,10 +10074,21 @@ function fetchLatestVersion(state) {
|
|
|
10049
10074
|
}
|
|
10050
10075
|
}
|
|
10051
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
|
+
|
|
10052
10089
|
// src/cli/cli.ts
|
|
10053
|
-
init_tty();
|
|
10054
10090
|
init_errors();
|
|
10055
|
-
var CLI_VERSION = true ? "1.
|
|
10091
|
+
var CLI_VERSION = true ? "1.6.0" : "0.0.0-dev";
|
|
10056
10092
|
var program2 = new Command();
|
|
10057
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) => {
|
|
10058
10094
|
const opts = thisCommand.optsWithGlobals();
|
|
@@ -10060,130 +10096,129 @@ program2.name("numo").description("CLI for Numo \u2014 programmatic access for h
|
|
|
10060
10096
|
thisCommand.setOptionValue("json", true);
|
|
10061
10097
|
}
|
|
10062
10098
|
}).addHelpText("after", `
|
|
10063
|
-
${
|
|
10099
|
+
${import_picocolors12.default.bold("Output modes:")}
|
|
10064
10100
|
Interactive (TTY) Tables, colors, spinners
|
|
10065
10101
|
Piped / --json Clean JSON for scripting and agents
|
|
10066
10102
|
|
|
10067
|
-
${
|
|
10068
|
-
${
|
|
10069
|
-
${
|
|
10070
|
-
${
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
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);
|
|
10079
10116
|
}).addHelpText("after", `
|
|
10080
10117
|
Examples:
|
|
10081
|
-
$ numo
|
|
10082
|
-
$ 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)`);
|
|
10083
10121
|
program2.command("logout").description("Clear stored credentials").action(() => {
|
|
10084
10122
|
clearCredentials();
|
|
10085
|
-
console.log(
|
|
10086
|
-
|
|
10087
|
-
|
|
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() {
|
|
10088
10137
|
const opts = this.optsWithGlobals();
|
|
10089
|
-
const asJson =
|
|
10138
|
+
const asJson = isQuietMode(opts);
|
|
10139
|
+
const envToken = process.env.NUMO_TOKEN;
|
|
10090
10140
|
const creds = loadCredentials();
|
|
10091
|
-
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
|
+
}
|
|
10092
10159
|
if (asJson) {
|
|
10093
|
-
|
|
10160
|
+
printJson({ email, uid, tokenValid: tokenValid2, expiresIn: expiresIn2, source: "NUMO_TOKEN", autoRefresh: false });
|
|
10094
10161
|
} else {
|
|
10095
|
-
console.
|
|
10096
|
-
|
|
10097
|
-
$
|
|
10098
|
-
`);
|
|
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)")}`);
|
|
10099
10166
|
}
|
|
10100
|
-
process.
|
|
10167
|
+
if (!tokenValid2) process.exitCode = ExitCode.NO_PERM;
|
|
10168
|
+
return;
|
|
10169
|
+
}
|
|
10170
|
+
if (!creds) {
|
|
10171
|
+
outputError(Errors.authRequired(), asJson);
|
|
10101
10172
|
return;
|
|
10102
10173
|
}
|
|
10103
10174
|
const tokenValid = !!(creds.idToken && creds.idTokenExpiry && Date.now() < creds.idTokenExpiry);
|
|
10104
10175
|
const expiresIn = creds.idTokenExpiry ? Math.max(0, Math.floor((creds.idTokenExpiry - Date.now()) / 1e3)) : 0;
|
|
10105
|
-
const source =
|
|
10176
|
+
const source = "credentials_file";
|
|
10106
10177
|
if (asJson) {
|
|
10107
|
-
printJson({ email: creds.email, uid: creds.uid, tokenValid, expiresIn, source });
|
|
10178
|
+
printJson({ email: creds.email, uid: creds.uid, tokenValid, expiresIn, source, autoRefresh: true });
|
|
10108
10179
|
} else {
|
|
10109
|
-
console.log(` ${
|
|
10110
|
-
console.log(` ${
|
|
10111
|
-
console.log(` ${
|
|
10112
|
-
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()}`);
|
|
10113
10184
|
}
|
|
10114
10185
|
});
|
|
10115
10186
|
registerTasksCommands(program2);
|
|
10116
|
-
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) {
|
|
10117
|
-
const opts = this.optsWithGlobals();
|
|
10118
|
-
const uid = requireUid();
|
|
10119
|
-
const text = textParts?.join(" ");
|
|
10120
|
-
if (!text) {
|
|
10121
|
-
console.error('Usage: numo add "task text"');
|
|
10122
|
-
process.exit(ExitCode.USAGE);
|
|
10123
|
-
}
|
|
10124
|
-
const body = {
|
|
10125
|
-
text,
|
|
10126
|
-
dueDate: opts.due ? parseHumanDate(opts.due) ?? opts.due : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
10127
|
-
};
|
|
10128
|
-
if (opts.tags) body.tags = opts.tags.split(",");
|
|
10129
|
-
if (opts.public) body.isPublic = true;
|
|
10130
|
-
if (opts.private) body.isPublic = false;
|
|
10131
|
-
await runCreate({
|
|
10132
|
-
global: opts,
|
|
10133
|
-
fn: () => createTask(uid, body),
|
|
10134
|
-
dataKey: "task",
|
|
10135
|
-
spinnerMessage: "Creating task...",
|
|
10136
|
-
onInteractive: (_task, payload) => {
|
|
10137
|
-
const { task, karma } = payload;
|
|
10138
|
-
const check = import_picocolors13.default.green(SYM.check);
|
|
10139
|
-
console.log(`
|
|
10140
|
-
${check} Created ${task.text} ${import_picocolors13.default.dim(task.id)}`);
|
|
10141
|
-
if (karma) console.log(` ${formatKarmaGain(karma)}`);
|
|
10142
|
-
console.log("");
|
|
10143
|
-
}
|
|
10144
|
-
});
|
|
10145
|
-
});
|
|
10146
10187
|
registerPostsCommands(program2);
|
|
10147
10188
|
registerProfileCommands(program2);
|
|
10148
10189
|
registerDoctorCommand(program2);
|
|
10149
10190
|
program2.command("commands").description("List all available commands").action(function() {
|
|
10150
10191
|
const opts = this.optsWithGlobals();
|
|
10151
|
-
const useJson2 =
|
|
10152
|
-
const commands =
|
|
10153
|
-
function walk(cmd, prefix) {
|
|
10154
|
-
for (const sub of cmd.commands) {
|
|
10155
|
-
const fullName = prefix ? `${prefix} ${sub.name()}` : sub.name();
|
|
10156
|
-
if (sub.commands.length > 0) {
|
|
10157
|
-
walk(sub, fullName);
|
|
10158
|
-
} else {
|
|
10159
|
-
commands.push({
|
|
10160
|
-
name: fullName,
|
|
10161
|
-
description: sub.description(),
|
|
10162
|
-
options: sub.options.map((o) => o.flags)
|
|
10163
|
-
});
|
|
10164
|
-
}
|
|
10165
|
-
}
|
|
10166
|
-
}
|
|
10167
|
-
walk(program2, "");
|
|
10192
|
+
const useJson2 = isQuietMode(opts);
|
|
10193
|
+
const commands = collectCommands(program2);
|
|
10168
10194
|
if (useJson2) {
|
|
10169
|
-
console.log(JSON.stringify({ commands }));
|
|
10195
|
+
console.log(JSON.stringify({ schemaVersion: "1", cliVersion: CLI_VERSION, commands }));
|
|
10170
10196
|
} else {
|
|
10171
|
-
console.log("");
|
|
10172
|
-
let lastGroup = "";
|
|
10173
|
-
for (const cmd of commands) {
|
|
10174
|
-
const group = cmd.name.split(" ")[0];
|
|
10175
|
-
if (group !== lastGroup) {
|
|
10176
|
-
if (lastGroup) console.log("");
|
|
10177
|
-
console.log(` ${import_picocolors13.default.bold(group.charAt(0).toUpperCase() + group.slice(1) + ":")}`);
|
|
10178
|
-
lastGroup = group;
|
|
10179
|
-
}
|
|
10180
|
-
console.log(` numo ${cmd.name.padEnd(30)} ${import_picocolors13.default.dim(cmd.description)}`);
|
|
10181
|
-
}
|
|
10182
10197
|
console.log(`
|
|
10183
|
-
|
|
10198
|
+
${formatCommandMap(commands)}`);
|
|
10199
|
+
console.log(`
|
|
10200
|
+
${import_picocolors12.default.dim("Run numo <command> --help for details.")}
|
|
10184
10201
|
`);
|
|
10185
10202
|
}
|
|
10186
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
|
+
};
|
|
10187
10222
|
function buildCommandSchema(cmd, fullName) {
|
|
10188
10223
|
return {
|
|
10189
10224
|
name: fullName,
|
|
@@ -10191,14 +10226,23 @@ function buildCommandSchema(cmd, fullName) {
|
|
|
10191
10226
|
arguments: cmd.registeredArguments?.map((a) => ({
|
|
10192
10227
|
name: a.name(),
|
|
10193
10228
|
required: a.required,
|
|
10229
|
+
variadic: a.variadic,
|
|
10194
10230
|
description: a.description
|
|
10195
10231
|
})) ?? [],
|
|
10196
|
-
options: cmd.options.filter((o) => !["--json", "-q, --quiet"].includes(o.flags)).map((o) =>
|
|
10197
|
-
|
|
10198
|
-
|
|
10199
|
-
|
|
10200
|
-
|
|
10201
|
-
|
|
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
|
+
})
|
|
10202
10246
|
};
|
|
10203
10247
|
}
|
|
10204
10248
|
program2.command("schema [command]").description("Print JSON schema for a command (for AI agents)").action(function(cmdPath) {
|
|
@@ -10213,7 +10257,7 @@ program2.command("schema [command]").description("Print JSON schema for a comman
|
|
|
10213
10257
|
var walk = walk2;
|
|
10214
10258
|
const schemas = [];
|
|
10215
10259
|
walk2(program2, "");
|
|
10216
|
-
console.log(JSON.stringify({ commands: schemas }, null, 2));
|
|
10260
|
+
console.log(JSON.stringify({ schemaVersion: "1", cliVersion: CLI_VERSION, commands: schemas }, null, 2));
|
|
10217
10261
|
return;
|
|
10218
10262
|
}
|
|
10219
10263
|
const parts = cmdPath.split(" ");
|
|
@@ -10227,7 +10271,7 @@ program2.command("schema [command]").description("Print JSON schema for a comman
|
|
|
10227
10271
|
}
|
|
10228
10272
|
cmd = sub;
|
|
10229
10273
|
}
|
|
10230
|
-
console.log(JSON.stringify(buildCommandSchema(cmd, cmdPath), null, 2));
|
|
10274
|
+
console.log(JSON.stringify({ schemaVersion: "1", cliVersion: CLI_VERSION, ...buildCommandSchema(cmd, cmdPath) }, null, 2));
|
|
10231
10275
|
});
|
|
10232
10276
|
program2.command("completion <shell>").description("Generate shell completion script").action(function(shell) {
|
|
10233
10277
|
if (shell !== "zsh") {
|