@indigoai-us/hq-cli 5.273.0-alpha.0 → 5.274.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/CHANGELOG.md +65 -2
- package/README.md +32 -1
- package/dist/command-catalog.generated.d.ts +83 -536
- package/dist/command-catalog.generated.js +129 -714
- package/dist/command-registration-plan.d.ts +12 -0
- package/dist/command-registration-plan.js +2 -0
- package/dist/commands/cloud.js +40 -0
- package/dist/commands/core-checkpoint.js +3 -1
- package/dist/commands/core-command-catalog.d.ts +32 -0
- package/dist/commands/core-command-catalog.js +58 -0
- package/dist/commands/core-ontology.js +9 -4
- package/dist/commands/core.d.ts +4 -0
- package/dist/commands/core.js +79 -75
- package/dist/commands/daemon.d.ts +0 -3
- package/dist/commands/daemon.js +1 -173
- package/dist/commands/deploy-comments.d.ts +3 -0
- package/dist/commands/deploy-comments.js +159 -0
- package/dist/commands/lanes-checks.d.ts +0 -1
- package/dist/commands/lanes-checks.js +217 -54
- package/dist/commands/lanes-hold.js +40 -2
- package/dist/commands/lanes-message.js +98 -14
- package/dist/commands/lanes-move.d.ts +12 -0
- package/dist/commands/lanes-move.js +342 -0
- package/dist/commands/lanes-resume.js +3 -1
- package/dist/commands/lanes-shared.d.ts +39 -3
- package/dist/commands/lanes-shared.js +59 -0
- package/dist/commands/lanes.d.ts +3 -5
- package/dist/commands/lanes.js +206 -478
- package/dist/commands/report.js +2 -14
- package/dist/commands/sync-mode.d.ts +0 -11
- package/dist/commands/sync-mode.js +0 -30
- package/dist/fast-core.js +1 -1
- package/dist/{commands/scaffold-fast.d.ts → fast-scaffold.d.ts} +1 -1
- package/dist/{commands/scaffold-fast.js → fast-scaffold.js} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/lib/checkpoint-ontology/apply.d.ts +8 -0
- package/dist/lib/checkpoint-ontology/apply.js +33 -0
- package/dist/lib/checkpoint-ontology/plan.d.ts +15 -0
- package/dist/lib/checkpoint-ontology/plan.js +15 -0
- package/dist/lib/daemon/catalog.d.ts +2 -18
- package/dist/lib/daemon/catalog.js +2 -18
- package/dist/lib/daemon/config.d.ts +0 -6
- package/dist/lib/daemon/config.js +2 -15
- package/dist/lib/daemon/requests.d.ts +0 -4
- package/dist/lib/daemon/requests.js +0 -2
- package/dist/lib/daemon/run.js +19 -78
- package/dist/lib/daemon/supervisor.d.ts +0 -8
- package/dist/lib/daemon/supervisor.js +1 -101
- package/dist/lib/deploy-api.d.ts +6 -0
- package/dist/lib/deploy-api.js +21 -0
- package/dist/lib/doctor/checks/scaffold-cli-floor.d.ts +7 -0
- package/dist/lib/doctor/checks/scaffold-cli-floor.js +73 -0
- package/dist/lib/doctor/fix/apply.d.ts +17 -2
- package/dist/lib/doctor/fix/apply.js +111 -57
- package/dist/lib/doctor/fix/json-output.d.ts +9 -1
- package/dist/lib/doctor/fix/json-output.js +11 -0
- package/dist/lib/doctor/registry.js +2 -0
- package/dist/lib/first-sync-next-step.d.ts +37 -0
- package/dist/lib/first-sync-next-step.js +189 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/hire-grant-dispatch.d.ts +6 -1
- package/dist/lib/lanes/hire-grant-dispatch.js +78 -3
- package/dist/lib/lanes/keep-alive.d.ts +3 -2
- package/dist/lib/lanes/keep-alive.js +2 -2
- package/dist/lib/lanes/merge-command.d.ts +1 -0
- package/dist/lib/lanes/merge-command.js +299 -33
- package/dist/lib/lanes/orphans.d.ts +7 -0
- package/dist/lib/lanes/orphans.js +39 -4
- package/dist/lib/lanes/tracker-store.d.ts +2 -18
- package/dist/lib/lanes/tracker-store.js +1 -172
- package/dist/lib/scaffold-cli-floor.d.ts +9 -0
- package/dist/lib/scaffold-cli-floor.js +50 -0
- package/dist/main.js +4 -1
- package/dist/utils/cli-telemetry.d.ts +7 -1
- package/dist/utils/cli-telemetry.js +84 -8
- package/dist/utils/contribution-table.d.ts +1 -1
- package/dist/utils/scaffold-fast-import-guard.d.ts +3 -0
- package/dist/utils/scaffold-fast-import-guard.js +28 -0
- package/dist/utils/scaffold-packlist-guard.d.ts +7 -0
- package/dist/utils/scaffold-packlist-guard.js +23 -0
- package/dist/utils/scaffold-sibling-references.d.ts +21 -0
- package/dist/utils/scaffold-sibling-references.js +156 -0
- package/package.json +2 -2
- package/dist/commands/lanes-import.d.ts +0 -14
- package/dist/commands/lanes-import.js +0 -1210
- package/dist/commands/lanes-tracker.d.ts +0 -18
- package/dist/commands/lanes-tracker.js +0 -452
- package/dist/lib/daemon/sync-runner.d.ts +0 -58
- package/dist/lib/daemon/sync-runner.js +0 -277
package/dist/commands/daemon.js
CHANGED
|
@@ -18,7 +18,7 @@ import * as os from "node:os";
|
|
|
18
18
|
import * as path from "node:path";
|
|
19
19
|
import { major as semverMajor, valid as validSemver } from "semver";
|
|
20
20
|
import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices, hqCloudUnits } from "../lib/daemon/catalog.js";
|
|
21
|
-
import { applyServiceToggle,
|
|
21
|
+
import { applyServiceToggle, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
|
|
22
22
|
import { daemonUnitInstalled } from "../lib/daemon/handoff.js";
|
|
23
23
|
import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/daemon/migrate.js";
|
|
24
24
|
import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, daemonRootRefusal, installDaemonUnit, installScopeFor, uninstallDaemonUnit, } from "../lib/daemon/install.js";
|
|
@@ -27,10 +27,7 @@ import { isDaemonProcess, runningDaemonPid } from "../lib/daemon/lock.js";
|
|
|
27
27
|
import { bundledHqCloud } from "../lib/daemon/hq-cloud.js";
|
|
28
28
|
import { MACHINE_TYPE_MARKER_PATH, detectMachineType, homeFromPasswd, } from "../lib/daemon/machine-type.js";
|
|
29
29
|
import { daemonPaths } from "../lib/daemon/paths.js";
|
|
30
|
-
import { processCommandLine } from "../lib/daemon/lock.js";
|
|
31
30
|
import { writeControlRequest } from "../lib/daemon/requests.js";
|
|
32
|
-
import { isSyncRunnerCommandLine, readWatchRunnerOwnerStatus, watchRunnerOwnerStatusPath } from "../lib/daemon/sync-runner.js";
|
|
33
|
-
import { getSyncModeForCompany, setSyncMode } from "./sync-mode.js";
|
|
34
31
|
import { resolveDefaultHqRoot } from "../utils/cognito-session.js";
|
|
35
32
|
class DaemonCommandError extends Error {
|
|
36
33
|
}
|
|
@@ -99,7 +96,6 @@ async function loadDefaults(over) {
|
|
|
99
96
|
windowsUser: over.windowsUser ?? (platform === "win32" ? mesh.resolveWindowsUser(env) : ""),
|
|
100
97
|
windowsHeadlessLauncher: over.windowsHeadlessLauncher ?? (platform === "win32" ? mesh.resolveWindowsHeadlessLauncher(env) : undefined),
|
|
101
98
|
systemUnitDir: over.systemUnitDir ?? SYSTEM_UNIT_DIR,
|
|
102
|
-
syncModeClient: over.syncModeClient,
|
|
103
99
|
};
|
|
104
100
|
}
|
|
105
101
|
function detect(d, flag, config) {
|
|
@@ -376,135 +372,6 @@ function queueRequest(d, request, what) {
|
|
|
376
372
|
const running = runningDaemonPid(paths.lockPath, d.daemonAlive) !== null;
|
|
377
373
|
d.out(running ? `Asked the daemon to ${what}.` : `Queued "${what}"; the daemon is not running, so it applies when it starts.`);
|
|
378
374
|
}
|
|
379
|
-
function daemonSyncModeClient(d) {
|
|
380
|
-
if (d.syncModeClient)
|
|
381
|
-
return d.syncModeClient();
|
|
382
|
-
return (async () => {
|
|
383
|
-
const [{ VaultClient }, { buildVaultConfig, ensureCognitoToken }] = await Promise.all([
|
|
384
|
-
import("@indigoai-us/hq-cloud"),
|
|
385
|
-
import("../utils/cognito-session.js"),
|
|
386
|
-
]);
|
|
387
|
-
const token = await ensureCognitoToken();
|
|
388
|
-
return new VaultClient(buildVaultConfig(token));
|
|
389
|
-
})();
|
|
390
|
-
}
|
|
391
|
-
function daemonSyncStatus(d) {
|
|
392
|
-
const host = currentHost(d);
|
|
393
|
-
const { paths } = host;
|
|
394
|
-
const config = readDaemonConfig(paths.configPath);
|
|
395
|
-
const daemonPid = runningDaemonPid(paths.lockPath, d.daemonAlive);
|
|
396
|
-
const daemonState = daemonPid === null ? null : readState(paths);
|
|
397
|
-
const syncUnit = daemonState?.units.find((unit) => unit.id === "sync");
|
|
398
|
-
const hqRoot = config.hqRoot ?? d.env.HQ_ROOT?.trim();
|
|
399
|
-
let ownerStatusCandidate = null;
|
|
400
|
-
if (hqRoot) {
|
|
401
|
-
try {
|
|
402
|
-
ownerStatusCandidate = readWatchRunnerOwnerStatus(watchRunnerOwnerStatusPath(hqRoot, d.home, d.env));
|
|
403
|
-
}
|
|
404
|
-
catch (error) {
|
|
405
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
406
|
-
d.err(`Could not read sync runner owner status; treating it as unavailable: ${detail}`);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
const ownerCommandLine = ownerStatusCandidate
|
|
410
|
-
? processCommandLine(ownerStatusCandidate.pid, { platform: d.platform, run: d.run })
|
|
411
|
-
: null;
|
|
412
|
-
const ownerStatus = ownerCommandLine && isSyncRunnerCommandLine(ownerCommandLine)
|
|
413
|
-
? ownerStatusCandidate
|
|
414
|
-
: null;
|
|
415
|
-
return {
|
|
416
|
-
running: syncUnit?.status === "running",
|
|
417
|
-
paused: config.syncPaused === true,
|
|
418
|
-
syncOwner: effectiveSyncOwner(config, host.detected.type),
|
|
419
|
-
owner: ownerStatus?.owner ?? (syncUnit?.status === "running" ? "hq-daemon" : null),
|
|
420
|
-
runnerPid: ownerStatus?.pid ?? syncUnit?.pid ?? null,
|
|
421
|
-
hqCloudVersion: ownerStatus?.version ?? null,
|
|
422
|
-
lastHeartbeat: syncUnit?.lastHeartbeatAt ?? ownerStatus?.heartbeatAt ?? null,
|
|
423
|
-
lastPassResult: syncUnit?.lastPassResult ?? null,
|
|
424
|
-
unitStatus: syncUnit?.status ?? "not-running",
|
|
425
|
-
reason: syncUnit?.reason ?? null,
|
|
426
|
-
logPath: paths.unitLogPath("sync"),
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
async function runDaemonSyncStatus(d, opts) {
|
|
430
|
-
const status = daemonSyncStatus(d);
|
|
431
|
-
if (opts.json) {
|
|
432
|
-
d.out(JSON.stringify(status, null, 2));
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
d.out(`Sync ${status.running ? "running" : status.unitStatus}${status.paused ? " (paused)" : ""}.`);
|
|
436
|
-
d.out(`Configured sync owner: ${status.syncOwner}.`);
|
|
437
|
-
d.out(`Owner: ${status.owner ?? "none"}; runner pid: ${status.runnerPid ?? "none"}; hq-cloud: ${status.hqCloudVersion ?? "unknown"}.`);
|
|
438
|
-
if (status.reason)
|
|
439
|
-
d.out(`Reason: ${status.reason}`);
|
|
440
|
-
d.out(`Log: ${status.logPath}`);
|
|
441
|
-
}
|
|
442
|
-
function queueDaemonSyncControl(d, request) {
|
|
443
|
-
const host = currentHost(d);
|
|
444
|
-
writeControlRequest(host.paths.requestsDir, request);
|
|
445
|
-
handOver(d, host);
|
|
446
|
-
return runningDaemonPid(host.paths.lockPath, d.daemonAlive) !== null;
|
|
447
|
-
}
|
|
448
|
-
async function runDaemonSyncNow(d, opts) {
|
|
449
|
-
const host = currentHost(d);
|
|
450
|
-
const config = readDaemonConfig(host.paths.configPath);
|
|
451
|
-
if (config.syncPaused)
|
|
452
|
-
throw new DaemonCommandError("sync is paused; run `hq daemon sync resume` first");
|
|
453
|
-
if (!enabledServices(host.detected.type, config).includes("sync")) {
|
|
454
|
-
throw new DaemonCommandError("sync is disabled for this machine; run `hq daemon enable sync` first");
|
|
455
|
-
}
|
|
456
|
-
const running = queueDaemonSyncControl(d, { action: "restart", unit: "sync" });
|
|
457
|
-
const result = { accepted: true, action: "restart-sync-runner", daemonRunning: running };
|
|
458
|
-
d.out(opts.json ? JSON.stringify(result, null, 2) : running ? "Asked the daemon to start a fresh sync pass." : "Queued a fresh sync pass for the next daemon start.");
|
|
459
|
-
}
|
|
460
|
-
async function runDaemonSyncPause(d, paused, opts) {
|
|
461
|
-
const host = currentHost(d);
|
|
462
|
-
const config = readDaemonConfig(host.paths.configPath);
|
|
463
|
-
if (config.syncPaused !== paused)
|
|
464
|
-
writeDaemonConfig(host.paths.configPath, { ...config, syncPaused: paused });
|
|
465
|
-
const daemonRunning = queueDaemonSyncControl(d, { action: "reconcile" });
|
|
466
|
-
const result = { paused, persisted: true, daemonRunning };
|
|
467
|
-
d.out(opts.json ? JSON.stringify(result, null, 2) : `Sync ${paused ? "paused" : "resumed"}${daemonRunning ? "" : "; the setting will apply when the daemon starts"}.`);
|
|
468
|
-
}
|
|
469
|
-
async function runDaemonSyncTakeOwnership(d, opts) {
|
|
470
|
-
const host = currentHost(d);
|
|
471
|
-
const config = readDaemonConfig(host.paths.configPath);
|
|
472
|
-
if (config.syncOwner !== "daemon")
|
|
473
|
-
writeDaemonConfig(host.paths.configPath, { ...config, syncOwner: "daemon" });
|
|
474
|
-
const daemonRunning = queueDaemonSyncControl(d, { action: "reconcile" });
|
|
475
|
-
const result = { syncOwner: "daemon", persisted: true, daemonRunning };
|
|
476
|
-
d.out(opts.json ? JSON.stringify(result, null, 2) : `Daemon sync ownership persisted${daemonRunning ? "; asked the daemon to reconcile" : "; it applies when the daemon starts"}.`);
|
|
477
|
-
}
|
|
478
|
-
async function runDaemonSyncModeGet(d, companySlug, opts) {
|
|
479
|
-
const result = await getSyncModeForCompany({ companySlug, vaultClient: await daemonSyncModeClient(d) });
|
|
480
|
-
const value = {
|
|
481
|
-
companySlug: result.companySlug,
|
|
482
|
-
membershipId: result.membershipId,
|
|
483
|
-
mode: result.config.syncMode,
|
|
484
|
-
isDefault: result.config.isDefault,
|
|
485
|
-
...(result.config.updatedAt ? { updatedAt: result.config.updatedAt } : {}),
|
|
486
|
-
};
|
|
487
|
-
d.out(opts.json ? JSON.stringify(value, null, 2) : `${value.companySlug}: ${value.mode}${value.isDefault ? " (default)" : ""}`);
|
|
488
|
-
}
|
|
489
|
-
async function runDaemonSyncModeSet(d, companySlug, mode, opts) {
|
|
490
|
-
if (mode !== "all" && mode !== "shared") {
|
|
491
|
-
throw new DaemonCommandError(`Invalid sync mode '${mode}'. Use all or shared.`);
|
|
492
|
-
}
|
|
493
|
-
const result = await setSyncMode({
|
|
494
|
-
mode,
|
|
495
|
-
companySlug,
|
|
496
|
-
vaultClient: await daemonSyncModeClient(d),
|
|
497
|
-
});
|
|
498
|
-
const value = {
|
|
499
|
-
companySlug: result.companySlug,
|
|
500
|
-
membershipId: result.membershipId,
|
|
501
|
-
previousMode: result.previousMode,
|
|
502
|
-
mode: result.newMode,
|
|
503
|
-
isDefault: result.newConfig.isDefault,
|
|
504
|
-
...(result.newConfig.updatedAt ? { updatedAt: result.newConfig.updatedAt } : {}),
|
|
505
|
-
};
|
|
506
|
-
d.out(opts.json ? JSON.stringify(value, null, 2) : `${value.companySlug}: ${value.previousMode} → ${value.mode}`);
|
|
507
|
-
}
|
|
508
375
|
async function runToggle(d, action, services) {
|
|
509
376
|
const host = currentHost(d);
|
|
510
377
|
const { paths } = host;
|
|
@@ -736,45 +603,6 @@ export function registerDaemonCommand(program, over = {}) {
|
|
|
736
603
|
.description("Show the machine type and each service's state")
|
|
737
604
|
.option("--json", "Print machine-readable JSON")
|
|
738
605
|
.action(wrap((d, opts) => runStatus(d, opts)));
|
|
739
|
-
const sync = daemon
|
|
740
|
-
.command("sync")
|
|
741
|
-
.description("Inspect and control the daemon's company sync runner");
|
|
742
|
-
sync
|
|
743
|
-
.command("status")
|
|
744
|
-
.description("Show sync ownership, heartbeat, last pass and log path")
|
|
745
|
-
.option("--json", "Print machine-readable JSON")
|
|
746
|
-
.action(wrap((d, opts) => runDaemonSyncStatus(d, opts)));
|
|
747
|
-
sync
|
|
748
|
-
.command("now")
|
|
749
|
-
.description("Start a fresh sync pass immediately")
|
|
750
|
-
.option("--json", "Print machine-readable JSON")
|
|
751
|
-
.action(wrap((d, opts) => runDaemonSyncNow(d, opts)));
|
|
752
|
-
sync
|
|
753
|
-
.command("pause")
|
|
754
|
-
.description("Pause daemon sync until resumed")
|
|
755
|
-
.option("--json", "Print machine-readable JSON")
|
|
756
|
-
.action(wrap((d, opts) => runDaemonSyncPause(d, true, opts)));
|
|
757
|
-
sync
|
|
758
|
-
.command("resume")
|
|
759
|
-
.description("Resume daemon sync after a pause")
|
|
760
|
-
.option("--json", "Print machine-readable JSON")
|
|
761
|
-
.action(wrap((d, opts) => runDaemonSyncPause(d, false, opts)));
|
|
762
|
-
sync
|
|
763
|
-
.command("take-ownership")
|
|
764
|
-
.description("Persist daemon ownership of desktop sync and reconcile")
|
|
765
|
-
.option("--json", "Print machine-readable JSON")
|
|
766
|
-
.action(wrap((d, opts) => runDaemonSyncTakeOwnership(d, opts)));
|
|
767
|
-
const mode = sync.command("mode").description("Read or change a company's sync footprint");
|
|
768
|
-
mode
|
|
769
|
-
.command("get <company>")
|
|
770
|
-
.description("Read a company's current all/shared sync mode")
|
|
771
|
-
.option("--json", "Print machine-readable JSON")
|
|
772
|
-
.action(wrap((d, company, opts) => runDaemonSyncModeGet(d, company, opts)));
|
|
773
|
-
mode
|
|
774
|
-
.command("set <company> <mode>")
|
|
775
|
-
.description("Set a company's sync mode to all or shared")
|
|
776
|
-
.option("--json", "Print machine-readable JSON")
|
|
777
|
-
.action(wrap((d, company, mode, opts) => runDaemonSyncModeSet(d, company, mode, opts)));
|
|
778
606
|
daemon
|
|
779
607
|
.command("logs [unit]")
|
|
780
608
|
.description("Print the supervisor log, or one service's log")
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { deployApiRequest } from "../lib/deploy-api.js";
|
|
2
|
+
import { DEFAULT_HQ_ROOT, ensureCognitoToken } from "../utils/cognito-session.js";
|
|
3
|
+
import { readActiveCompanySlug } from "./skill.js";
|
|
4
|
+
class DeployCommentsError extends Error {
|
|
5
|
+
}
|
|
6
|
+
function callerOrgHeaders(companyFlag) {
|
|
7
|
+
const slug = companyFlag ?? readActiveCompanySlug(DEFAULT_HQ_ROOT);
|
|
8
|
+
if (slug === undefined)
|
|
9
|
+
return {};
|
|
10
|
+
if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(slug)) {
|
|
11
|
+
throw new DeployCommentsError("The company slug is not valid.");
|
|
12
|
+
}
|
|
13
|
+
return { "X-Org-Slug": slug };
|
|
14
|
+
}
|
|
15
|
+
function errorCode(body, status) {
|
|
16
|
+
const code = body.error?.code;
|
|
17
|
+
return typeof code === "string" && /^[A-Z0-9_]+$/.test(code) ? code : `HTTP_${status}`;
|
|
18
|
+
}
|
|
19
|
+
async function responseError(response) {
|
|
20
|
+
const body = await response.json().catch(() => ({}));
|
|
21
|
+
const code = errorCode(body, response.status);
|
|
22
|
+
if (response.status === 401 || response.status === 403) {
|
|
23
|
+
return new DeployCommentsError(`You need to be the app owner or an org admin to read its comments. (${code})`);
|
|
24
|
+
}
|
|
25
|
+
if (response.status === 404) {
|
|
26
|
+
return new DeployCommentsError(`App was not found in the current org. (${code})`);
|
|
27
|
+
}
|
|
28
|
+
if (response.status === 429) {
|
|
29
|
+
const value = response.headers.get("Retry-After");
|
|
30
|
+
const retryAfter = value && /^\d+$/.test(value) ? value : "unknown";
|
|
31
|
+
return new DeployCommentsError(`Comment reads are rate limited. Retry-After: ${retryAfter} seconds. (${code})`);
|
|
32
|
+
}
|
|
33
|
+
return new DeployCommentsError(`Could not read deploy comments (HTTP ${response.status}, ${code}).`);
|
|
34
|
+
}
|
|
35
|
+
async function deployJson(token, route, orgHeaders) {
|
|
36
|
+
let response;
|
|
37
|
+
try {
|
|
38
|
+
response = await deployApiRequest(token, route, "GET", undefined, orgHeaders);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new DeployCommentsError("Could not reach hq-deploy.");
|
|
42
|
+
}
|
|
43
|
+
if (!response.ok)
|
|
44
|
+
throw await responseError(response);
|
|
45
|
+
try {
|
|
46
|
+
return await response.json();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw new DeployCommentsError("hq-deploy returned an invalid response.");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function resolveAppId(token, appNameOrId, orgHeaders) {
|
|
53
|
+
const result = await deployJson(token, "/api/apps", orgHeaders);
|
|
54
|
+
const apps = Array.isArray(result.apps) ? result.apps : [];
|
|
55
|
+
const byId = apps.find((app) => app && typeof app.id === "string" && app.id === appNameOrId);
|
|
56
|
+
const matches = byId
|
|
57
|
+
? [byId]
|
|
58
|
+
: apps.filter((app) => app && typeof app.id === "string" && app.name === appNameOrId);
|
|
59
|
+
const ids = [...new Set(matches.map((app) => app.id).filter((id) => typeof id === "string"))];
|
|
60
|
+
if (ids.length === 0)
|
|
61
|
+
throw new DeployCommentsError("App was not found in the current org.");
|
|
62
|
+
if (ids.length > 1)
|
|
63
|
+
throw new DeployCommentsError("More than one app has that name in the current org; use its app id.");
|
|
64
|
+
return ids[0];
|
|
65
|
+
}
|
|
66
|
+
async function listComments(token, appId, orgHeaders) {
|
|
67
|
+
const route = `/api/apps/${encodeURIComponent(appId)}/manage/comments`;
|
|
68
|
+
const result = await deployJson(token, route, orgHeaders);
|
|
69
|
+
if (typeof result.commentsEnabled !== "boolean" || !Array.isArray(result.comments)) {
|
|
70
|
+
throw new DeployCommentsError("hq-deploy returned an incomplete comments response.");
|
|
71
|
+
}
|
|
72
|
+
return { commentsEnabled: result.commentsEnabled, comments: result.comments };
|
|
73
|
+
}
|
|
74
|
+
function displayText(value) {
|
|
75
|
+
return [...String(value ?? "")].map((character) => {
|
|
76
|
+
const code = character.codePointAt(0) ?? 0;
|
|
77
|
+
return isUnsafeTerminalCodePoint(code)
|
|
78
|
+
? `\\u${code.toString(16).padStart(4, "0")}`
|
|
79
|
+
: character;
|
|
80
|
+
}).join("");
|
|
81
|
+
}
|
|
82
|
+
function isUnsafeTerminalCodePoint(code) {
|
|
83
|
+
return code <= 0x1f
|
|
84
|
+
|| (code >= 0x7f && code <= 0x9f)
|
|
85
|
+
|| (code >= 0x202a && code <= 0x202e)
|
|
86
|
+
|| (code >= 0x2066 && code <= 0x2069);
|
|
87
|
+
}
|
|
88
|
+
function safeJson(value) {
|
|
89
|
+
return JSON.stringify(value, null, 2).replace(/[\u007f-\u009f\u202a-\u202e\u2066-\u2069]/gu, (character) => `\\u${(character.codePointAt(0) ?? 0).toString(16).padStart(4, "0")}`);
|
|
90
|
+
}
|
|
91
|
+
function printCommentLine(comment) {
|
|
92
|
+
const firstLine = comment.body.split(/\r\n|\r|\n/, 1)[0] ?? "";
|
|
93
|
+
console.log([
|
|
94
|
+
displayText(comment.id),
|
|
95
|
+
displayText(comment.status),
|
|
96
|
+
displayText(comment.createdAt),
|
|
97
|
+
displayText(comment.author?.name || "Unknown author"),
|
|
98
|
+
displayText(comment.anchor?.mode || "unknown"),
|
|
99
|
+
displayText(firstLine),
|
|
100
|
+
].join(" | "));
|
|
101
|
+
}
|
|
102
|
+
async function run(action) {
|
|
103
|
+
try {
|
|
104
|
+
await action();
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.error(error instanceof DeployCommentsError ? error.message : "Deploy comments command failed.");
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function listAction(appNameOrId, options) {
|
|
112
|
+
const status = options.status ?? "all";
|
|
113
|
+
if (status !== "open" && status !== "resolved" && status !== "all") {
|
|
114
|
+
throw new DeployCommentsError("--status must be open, resolved, or all.");
|
|
115
|
+
}
|
|
116
|
+
const token = await ensureCognitoToken();
|
|
117
|
+
const orgHeaders = callerOrgHeaders(options.company);
|
|
118
|
+
const appId = await resolveAppId(token, appNameOrId, orgHeaders);
|
|
119
|
+
const result = await listComments(token, appId, orgHeaders);
|
|
120
|
+
const comments = status === "all"
|
|
121
|
+
? result.comments
|
|
122
|
+
: result.comments.filter((comment) => comment.status === status);
|
|
123
|
+
if (options.json) {
|
|
124
|
+
console.log(safeJson({ commentsEnabled: result.commentsEnabled, comments }));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (comments.length === 0) {
|
|
128
|
+
console.log("No comments.");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
for (const comment of comments)
|
|
132
|
+
printCommentLine(comment);
|
|
133
|
+
}
|
|
134
|
+
async function readAction(appNameOrId, commentId, company) {
|
|
135
|
+
const token = await ensureCognitoToken();
|
|
136
|
+
const orgHeaders = callerOrgHeaders(company);
|
|
137
|
+
const appId = await resolveAppId(token, appNameOrId, orgHeaders);
|
|
138
|
+
const result = await listComments(token, appId, orgHeaders);
|
|
139
|
+
const comment = result.comments.find((entry) => entry.id === commentId);
|
|
140
|
+
if (!comment)
|
|
141
|
+
throw new DeployCommentsError(`Comment '${displayText(commentId)}' was not found on that app.`);
|
|
142
|
+
console.log(safeJson(comment));
|
|
143
|
+
}
|
|
144
|
+
export function registerDeployCommentsCommand(deploy) {
|
|
145
|
+
const comments = deploy.command("comments").description("List and read comments on your deployed apps");
|
|
146
|
+
comments
|
|
147
|
+
.command("list <app>")
|
|
148
|
+
.description("List comments on an app in your org")
|
|
149
|
+
.option("--company <slug>", "use this company instead of the active company")
|
|
150
|
+
.option("--json", "print the API comment array as JSON")
|
|
151
|
+
.option("--status <status>", "filter by open, resolved, or all", "all")
|
|
152
|
+
.action((app, options) => run(() => listAction(app, options)));
|
|
153
|
+
comments
|
|
154
|
+
.command("read <app> <commentId>")
|
|
155
|
+
.description("Read a comment on an app in your org")
|
|
156
|
+
.option("--company <slug>", "use this company instead of the active company")
|
|
157
|
+
.action((app, commentId, options) => run(() => readAction(app, commentId, options.company)));
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=deploy-comments.js.map
|
|
@@ -4,7 +4,6 @@ import type { LaneCompanyRef, QuestionRecord } from "../lib/lanes/types.js";
|
|
|
4
4
|
export interface LaneChecksDeps {
|
|
5
5
|
hqRoot: () => string;
|
|
6
6
|
resolveCompany: (company?: string) => Promise<LaneCompanyRef>;
|
|
7
|
-
trackerEnabled: (company: LaneCompanyRef) => boolean | Promise<boolean>;
|
|
8
7
|
now: () => Date;
|
|
9
8
|
stopLane?: (laneId: string, reason: string) => Promise<void>;
|
|
10
9
|
sessionState?: (root: string, sessionId: string, now: Date) => string | null;
|