@indigoai-us/hq-cli 5.299.0 → 5.301.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 +26 -0
- package/assets/scaffold/core/scripts/token-usage-report.sh +9 -0
- package/dist/command-catalog.generated.d.ts +32 -6
- package/dist/command-catalog.generated.js +38 -6
- package/dist/commands/__fixtures__/access-vault.d.ts +2 -0
- package/dist/commands/__fixtures__/access-vault.js +1 -1
- package/dist/commands/access.d.ts +2 -1
- package/dist/commands/access.js +58 -49
- package/dist/commands/cloud-provision.js +51 -2
- package/dist/commands/cloud.js +12 -7
- package/dist/commands/core-company-index-scope-flag.d.ts +3 -0
- package/dist/commands/core-company-index-scope-flag.js +6 -0
- package/dist/commands/core-job-native-utils.d.ts +6 -0
- package/dist/commands/core-job-native-utils.js +80 -0
- package/dist/commands/core-job-native.d.ts +2 -4
- package/dist/commands/core-job-native.js +6 -10
- package/dist/commands/core-job-notify-native.d.ts +36 -0
- package/dist/commands/core-job-notify-native.js +359 -0
- package/dist/commands/core-job-remediate-native.d.ts +26 -0
- package/dist/commands/core-job-remediate-native.js +374 -0
- package/dist/commands/core.js +26 -8
- package/dist/commands/files.d.ts +1 -0
- package/dist/commands/files.js +90 -14
- package/dist/commands/integrations-api.d.ts +6 -6
- package/dist/commands/integrations-api.js +4 -13
- package/dist/commands/integrations-manage.js +1 -4
- package/dist/commands/lanes-message.js +78 -37
- package/dist/commands/lanes-resume.js +247 -210
- package/dist/commands/lanes-shared.d.ts +5 -1
- package/dist/commands/lanes.d.ts +4 -0
- package/dist/commands/lanes.js +7 -4
- package/dist/commands/meetings.js +35 -16
- package/dist/commands/safe-extract.d.ts +4 -4
- package/dist/commands/safe-extract.js +6 -7
- package/dist/lib/core-utils/token-usage-report.js +17 -3
- package/dist/lib/index-render/companies.d.ts +4 -1
- package/dist/lib/index-render/companies.js +66 -3
- package/dist/lib/index-render/index.d.ts +1 -0
- package/dist/lib/index-render/index.js +2 -1
- package/dist/lib/index-render/shared.d.ts +1 -1
- package/dist/lib/index-render/shared.js +4 -2
- package/dist/lib/lanes/reconcile.js +22 -4
- package/dist/lib/lanes/story-switch.d.ts +21 -0
- package/dist/lib/lanes/story-switch.js +75 -0
- package/dist/lib/lanes/types.d.ts +25 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watcher.js +15 -4
- package/dist/lib/mesh/live/daemon/mqtt-events.d.ts +7 -0
- package/dist/lib/mesh/live/daemon/mqtt-events.js +2 -0
- package/dist/lib/mesh/live/daemon/mqtt-events.test-d.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/mqtt-events.test-d.js +10 -0
- package/dist/lib/mesh/live/daemon/presence.d.ts +2 -2
- package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +2 -2
- package/dist/utils/self-update.js +51 -4
- package/dist/utils/staged-npm-install.d.ts +16 -0
- package/dist/utils/staged-npm-install.js +264 -0
- package/dist/utils/update-command-supervisor.cjs +53 -1
- package/dist/utils/version-gate.d.ts +8 -0
- package/dist/utils/version-gate.js +82 -15
- package/package.json +2 -2
- package/dist/lib/install-path-compatibility-flag.d.ts +0 -9
- package/dist/lib/install-path-compatibility-flag.js +0 -12
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
2
|
+
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
3
|
+
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
4
|
+
import { coreNativeJobsEnabled } from "./core-native-jobs-flag.js";
|
|
5
|
+
/** Preserve raw argv (including the option terminator) through Commander. */
|
|
6
|
+
export function rawJobArgumentsForCommand(command, fallback, raw = process.argv) {
|
|
7
|
+
const names = [];
|
|
8
|
+
for (let current = command; current?.parent; current = current.parent) {
|
|
9
|
+
names.unshift(current.name());
|
|
10
|
+
}
|
|
11
|
+
let start = -1;
|
|
12
|
+
let cursor = 0;
|
|
13
|
+
for (const name of names) {
|
|
14
|
+
const index = raw.indexOf(name, cursor);
|
|
15
|
+
if (index < 0)
|
|
16
|
+
return command.args.length ? command.args : fallback;
|
|
17
|
+
cursor = index + 1;
|
|
18
|
+
start = cursor;
|
|
19
|
+
}
|
|
20
|
+
return start >= 0 ? raw.slice(start) : (command.args.length ? command.args : fallback);
|
|
21
|
+
}
|
|
22
|
+
/** Shared gated registration path for each leaf in the singular job group. */
|
|
23
|
+
export function registerCoreJobLeaf(group, core, name, asset, summary, run, canUseLiveRoot, valueOptions, booleanOptions = []) {
|
|
24
|
+
const command = group.command(name)
|
|
25
|
+
.description(summary)
|
|
26
|
+
.helpOption(false)
|
|
27
|
+
.allowUnknownOption()
|
|
28
|
+
.allowExcessArguments(true)
|
|
29
|
+
.argument("[args...]", "Arguments passed to the scaffold helper")
|
|
30
|
+
.action(async (args, _options, current) => {
|
|
31
|
+
const argv = rawJobArgumentsForCommand(current, args);
|
|
32
|
+
const groupHqRoot = core.opts().hqRoot;
|
|
33
|
+
const hqRoot = canUseLiveRoot(argv)
|
|
34
|
+
? resolveLiveRoot({ hqRoot: leafRootOverride(argv, valueOptions, booleanOptions) ?? groupHqRoot })
|
|
35
|
+
: groupHqRoot ?? "";
|
|
36
|
+
if (!await coreNativeJobsEnabled()) {
|
|
37
|
+
const result = runBundledScript({ asset, args: argv, cwd: process.cwd(), hqRoot: hqRoot || undefined });
|
|
38
|
+
if (result.code !== 0)
|
|
39
|
+
process.exit(result.code);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const result = run(argv, hqRoot);
|
|
43
|
+
if (result !== 0)
|
|
44
|
+
process.exit(result);
|
|
45
|
+
});
|
|
46
|
+
describeCoreCommand(command, { execution: "native", asset, root: "live" });
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Find a leaf --hq-root without resolving anything until the leaf parser has
|
|
50
|
+
* had the chance to return its own help or usage error. The option lists are
|
|
51
|
+
* the same grammar used by the shell and native leaves.
|
|
52
|
+
*/
|
|
53
|
+
function leafRootOverride(argv, valueOptions, booleanOptions) {
|
|
54
|
+
let root;
|
|
55
|
+
for (let index = 0; index < argv.length;) {
|
|
56
|
+
const flag = argv[index];
|
|
57
|
+
if (flag === "--")
|
|
58
|
+
return root;
|
|
59
|
+
if (flag === "--hq-root") {
|
|
60
|
+
const value = argv[index + 1] ?? "";
|
|
61
|
+
if (!value)
|
|
62
|
+
return undefined;
|
|
63
|
+
root = value;
|
|
64
|
+
index += 2;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (valueOptions.includes(flag)) {
|
|
68
|
+
index += 2;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (booleanOptions.includes(flag)) {
|
|
72
|
+
index += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
// The preflight parser has already rejected invalid options and operands.
|
|
76
|
+
return root;
|
|
77
|
+
}
|
|
78
|
+
return root;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=core-job-native-utils.js.map
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
/** Native
|
|
1
|
+
/** Native hq core job command group. */
|
|
2
2
|
import type { Command } from "commander";
|
|
3
|
-
|
|
4
|
-
export declare function registerCoreJobCommand(core: Command): void;
|
|
5
|
-
/** Keep eager `core` registration complete while the jobs fast route loads only its own module. */
|
|
3
|
+
/** Register all job leaves on one singular group plus the existing plural group. */
|
|
6
4
|
export declare function registerCoreJobsNativeCommand(core: Command): void;
|
|
7
5
|
//# sourceMappingURL=core-job-native.d.ts.map
|
|
@@ -1,19 +1,15 @@
|
|
|
1
|
+
import { registerCoreJobNotifyNativeCommand } from "./core-job-notify-native.js";
|
|
1
2
|
import { registerCoreJobProbeCommand } from "./core-job-probe-native.js";
|
|
3
|
+
import { registerCoreJobRemediateNativeCommand } from "./core-job-remediate-native.js";
|
|
2
4
|
import { registerCoreJobRunCommand } from "./core-job-run-native.js";
|
|
3
5
|
import { registerCoreJobsNativeCommand as registerPluralJobsCommand } from "./core-jobs-native.js";
|
|
4
|
-
const JOB_GROUP_DESCRIPTION = "Run or
|
|
5
|
-
|
|
6
|
-
const job = core.command("job").description(JOB_GROUP_DESCRIPTION);
|
|
7
|
-
registerCoreJobProbeCommand(job);
|
|
8
|
-
}
|
|
9
|
-
export function registerCoreJobCommand(core) {
|
|
10
|
-
const job = core.command("job").description(JOB_GROUP_DESCRIPTION);
|
|
11
|
-
registerCoreJobRunCommand(job);
|
|
12
|
-
}
|
|
13
|
-
/** Keep eager `core` registration complete while the jobs fast route loads only its own module. */
|
|
6
|
+
const JOB_GROUP_DESCRIPTION = "Run, inspect, notify, or remediate one scheduled job";
|
|
7
|
+
/** Register all job leaves on one singular group plus the existing plural group. */
|
|
14
8
|
export function registerCoreJobsNativeCommand(core) {
|
|
15
9
|
const job = core.command("job").description(JOB_GROUP_DESCRIPTION);
|
|
10
|
+
registerCoreJobNotifyNativeCommand(job, core);
|
|
16
11
|
registerCoreJobProbeCommand(job);
|
|
12
|
+
registerCoreJobRemediateNativeCommand(job, core);
|
|
17
13
|
registerCoreJobRunCommand(job);
|
|
18
14
|
registerPluralJobsCommand(core);
|
|
19
15
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Native TypeScript port of hq-job-notify.sh. */
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
type Environment = Record<string, string | undefined>;
|
|
4
|
+
export type JobNotifyProcessResult = {
|
|
5
|
+
code: number;
|
|
6
|
+
stdout: string;
|
|
7
|
+
};
|
|
8
|
+
export type JobNotifyRunOptions = {
|
|
9
|
+
cwd: string;
|
|
10
|
+
env: Environment;
|
|
11
|
+
captureStdout: boolean;
|
|
12
|
+
logPath?: string;
|
|
13
|
+
};
|
|
14
|
+
export type JobNotifyDependencies = {
|
|
15
|
+
env: Environment;
|
|
16
|
+
cwd: string;
|
|
17
|
+
now: () => Date;
|
|
18
|
+
exists: (file: string) => boolean;
|
|
19
|
+
readFile: (file: string) => string;
|
|
20
|
+
mkdir: (directory: string) => void;
|
|
21
|
+
writeFile: (file: string, content: string) => void;
|
|
22
|
+
appendFile: (file: string, content: string) => void;
|
|
23
|
+
chmod: (file: string, mode: number) => void;
|
|
24
|
+
commandExists: (command: string) => boolean;
|
|
25
|
+
run: (command: string, args: string[], options: JobNotifyRunOptions) => JobNotifyProcessResult;
|
|
26
|
+
stdout: (text: string) => void;
|
|
27
|
+
stderr: (text: string) => void;
|
|
28
|
+
};
|
|
29
|
+
export declare function defaultJobNotifyDependencies(env?: Environment, cwd?: string): JobNotifyDependencies;
|
|
30
|
+
export declare function runJobNotify(argv: string[], options?: {
|
|
31
|
+
hqRoot?: string;
|
|
32
|
+
dependencies?: Partial<JobNotifyDependencies>;
|
|
33
|
+
}): number;
|
|
34
|
+
export declare function registerCoreJobNotifyNativeCommand(group: Command, core: Command): void;
|
|
35
|
+
export {};
|
|
36
|
+
//# sourceMappingURL=core-job-notify-native.d.ts.map
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { load } from "js-yaml";
|
|
6
|
+
import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
|
|
7
|
+
import { registerCoreJobLeaf } from "./core-job-native-utils.js";
|
|
8
|
+
const ASSET = "core/scripts/hq-job-notify.sh";
|
|
9
|
+
const USAGE = [
|
|
10
|
+
"hq-job-notify.sh — dm-only remediation alerts for Outpost scheduled jobs (US-006).",
|
|
11
|
+
"",
|
|
12
|
+
'Delivers via schedule-alerts profile with v1 channel=dm only (`hq dm` to owner).',
|
|
13
|
+
"If profile channel is slack|email|outpost-session, still deliver dm and log",
|
|
14
|
+
"channel_unimplemented. Never targets the original /schedule create session.",
|
|
15
|
+
"",
|
|
16
|
+
"Escalation collapse:",
|
|
17
|
+
" - first failure → DM immediately",
|
|
18
|
+
" - consecutive failures → at most one failure DM per 24h per job",
|
|
19
|
+
" - recovery after failures → recovered DM",
|
|
20
|
+
" - success after success → ok DM (every successful run)",
|
|
21
|
+
"",
|
|
22
|
+
"Notify delivery failure is logged and never fails the caller (exit 0).",
|
|
23
|
+
"",
|
|
24
|
+
"Usage:",
|
|
25
|
+
" core/scripts/hq-job-notify.sh \\",
|
|
26
|
+
" --job-id <id> --job-name <name> --outcome <ok|failed|blocked> \\",
|
|
27
|
+
" --summary <one-line> --duration <sec> --log-path <path> \\",
|
|
28
|
+
" [--failure-class <class>] [--next-action <text>] \\",
|
|
29
|
+
" [--owner <email|prs_*>] [--notify <dm|none|profile>] \\",
|
|
30
|
+
" [--hq-root <dir>]",
|
|
31
|
+
"",
|
|
32
|
+
"Env / overrides (tests):",
|
|
33
|
+
" HQ_ROOT, HOME, HQ_JOB_ALERT_DIR, HQ_JOB_NOTIFY_DM_BIN, HQ_JOB_NOTIFY_NOW_EPOCH",
|
|
34
|
+
" HQ_JOB_NOTIFY_COLLAPSE_SEC (default 86400)",
|
|
35
|
+
"",
|
|
36
|
+
"Exit: 0 always (delivery errors logged only); 2 usage.",
|
|
37
|
+
"",
|
|
38
|
+
"set -euo pipefail",
|
|
39
|
+
"",
|
|
40
|
+
"",
|
|
41
|
+
].join("\n");
|
|
42
|
+
function defaultCommandExists(command, env) {
|
|
43
|
+
if (command.includes(path.sep)) {
|
|
44
|
+
try {
|
|
45
|
+
fs.accessSync(command, fs.constants.X_OK);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const directory of (env.PATH ?? "").split(path.delimiter)) {
|
|
53
|
+
if (!directory)
|
|
54
|
+
continue;
|
|
55
|
+
try {
|
|
56
|
+
fs.accessSync(path.join(directory, command), fs.constants.X_OK);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch { /* Continue PATH lookup. */ }
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
function defaultRun(command, args, options) {
|
|
64
|
+
let logFd;
|
|
65
|
+
try {
|
|
66
|
+
if (options.logPath) {
|
|
67
|
+
try {
|
|
68
|
+
logFd = fs.openSync(options.logPath, "a");
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// The shell treats failed output redirection as a failed delivery and
|
|
72
|
+
// continues the caller successfully; runJobNotify records that result.
|
|
73
|
+
return { code: 1, stdout: "" };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const result = spawnSync(command, args, {
|
|
77
|
+
cwd: options.cwd,
|
|
78
|
+
env: options.env,
|
|
79
|
+
encoding: "utf8",
|
|
80
|
+
stdio: options.captureStdout ? ["ignore", "pipe", "ignore"] : ["ignore", logFd ?? "ignore", logFd ?? "ignore"],
|
|
81
|
+
});
|
|
82
|
+
if (result.error) {
|
|
83
|
+
if (logFd !== undefined)
|
|
84
|
+
fs.writeSync(logFd, `${result.error.message}\n`);
|
|
85
|
+
return { code: 127, stdout: "" };
|
|
86
|
+
}
|
|
87
|
+
const signalNumber = result.signal ? os.constants.signals[result.signal] : undefined;
|
|
88
|
+
return {
|
|
89
|
+
code: result.status ?? (signalNumber ? 128 + signalNumber : 1),
|
|
90
|
+
stdout: result.stdout ?? "",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
if (logFd !== undefined)
|
|
95
|
+
fs.closeSync(logFd);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function defaultJobNotifyDependencies(env = process.env, cwd = process.cwd()) {
|
|
99
|
+
return {
|
|
100
|
+
env,
|
|
101
|
+
cwd,
|
|
102
|
+
now: () => new Date(),
|
|
103
|
+
exists: (file) => fs.existsSync(file),
|
|
104
|
+
readFile: (file) => fs.readFileSync(file, "utf8"),
|
|
105
|
+
mkdir: (directory) => fs.mkdirSync(directory, { recursive: true }),
|
|
106
|
+
writeFile: (file, content) => fs.writeFileSync(file, content),
|
|
107
|
+
appendFile: (file, content) => fs.appendFileSync(file, content),
|
|
108
|
+
chmod: (file, mode) => fs.chmodSync(file, mode),
|
|
109
|
+
commandExists: (command) => defaultCommandExists(command, env),
|
|
110
|
+
run: defaultRun,
|
|
111
|
+
stdout: (text) => { process.stdout.write(text); },
|
|
112
|
+
stderr: (text) => { process.stderr.write(text); },
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function parseArguments(argv, deps) {
|
|
116
|
+
const values = {
|
|
117
|
+
hqRoot: deps.env.HQ_ROOT ?? "",
|
|
118
|
+
jobId: "", jobName: "", outcome: "", summary: "", duration: "", logPath: "",
|
|
119
|
+
failureClass: "", nextAction: "", owner: "", notifyMode: "",
|
|
120
|
+
};
|
|
121
|
+
const aliases = {
|
|
122
|
+
"--hq-root": "hqRoot", "--job-id": "jobId", "--job-name": "jobName",
|
|
123
|
+
"--outcome": "outcome", "--summary": "summary", "--duration": "duration",
|
|
124
|
+
"--log-path": "logPath", "--failure-class": "failureClass",
|
|
125
|
+
"--next-action": "nextAction", "--owner": "owner", "--notify": "notifyMode",
|
|
126
|
+
};
|
|
127
|
+
for (let index = 0; index < argv.length;) {
|
|
128
|
+
const flag = argv[index];
|
|
129
|
+
if (flag === "-h" || flag === "--help") {
|
|
130
|
+
deps.stdout(USAGE);
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
if (flag === "--")
|
|
134
|
+
break;
|
|
135
|
+
const key = aliases[flag];
|
|
136
|
+
if (key) {
|
|
137
|
+
const value = argv[index + 1] ?? "";
|
|
138
|
+
if ((key === "hqRoot" || key === "jobId" || key === "outcome") && !value) {
|
|
139
|
+
deps.stderr(`hq-job-notify: --${key === "hqRoot" ? "hq-root" : key === "jobId" ? "job-id" : "outcome"} requires ${key === "hqRoot" ? "a directory" : "a value"}\n`);
|
|
140
|
+
return 2;
|
|
141
|
+
}
|
|
142
|
+
values[key] = value;
|
|
143
|
+
index += 2;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (flag.startsWith("-")) {
|
|
147
|
+
deps.stderr(`hq-job-notify: unknown option: ${flag}\n`);
|
|
148
|
+
return 2;
|
|
149
|
+
}
|
|
150
|
+
deps.stderr(`hq-job-notify: unexpected argument: ${flag}\n`);
|
|
151
|
+
return 2;
|
|
152
|
+
}
|
|
153
|
+
if (!values.jobId) {
|
|
154
|
+
deps.stderr("hq-job-notify: usage: hq-job-notify.sh --job-id <id> --outcome <ok|failed|blocked> ...\n");
|
|
155
|
+
return 2;
|
|
156
|
+
}
|
|
157
|
+
if (!["ok", "failed", "blocked"].includes(values.outcome)) {
|
|
158
|
+
deps.stderr("hq-job-notify: --outcome must be ok|failed|blocked\n");
|
|
159
|
+
return 2;
|
|
160
|
+
}
|
|
161
|
+
if (!values.jobName)
|
|
162
|
+
values.jobName = values.jobId;
|
|
163
|
+
if (!values.summary)
|
|
164
|
+
values.summary = values.outcome;
|
|
165
|
+
if (!values.duration)
|
|
166
|
+
values.duration = "0";
|
|
167
|
+
return values;
|
|
168
|
+
}
|
|
169
|
+
function canUseLiveRootForArguments(argv) {
|
|
170
|
+
const parsed = parseArguments(argv, {
|
|
171
|
+
env: {},
|
|
172
|
+
stdout: () => undefined,
|
|
173
|
+
stderr: () => undefined,
|
|
174
|
+
});
|
|
175
|
+
return typeof parsed !== "number";
|
|
176
|
+
}
|
|
177
|
+
function profileChannel(profilePath, deps) {
|
|
178
|
+
if (!deps.exists(profilePath) || !deps.commandExists("yq"))
|
|
179
|
+
return "dm";
|
|
180
|
+
try {
|
|
181
|
+
const loaded = load(deps.readFile(profilePath));
|
|
182
|
+
const channel = loaded?.channel;
|
|
183
|
+
return channel === undefined || channel === null || channel === false || channel === "" ? "dm" : String(channel);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// The shell suppresses yq parse errors and falls back through `|| echo dm`.
|
|
187
|
+
return "dm";
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function emailFromWhoami(output) {
|
|
191
|
+
for (const line of output.split(/\r?\n/)) {
|
|
192
|
+
if (!line.startsWith("email:"))
|
|
193
|
+
continue;
|
|
194
|
+
const separator = line.indexOf(": ");
|
|
195
|
+
return (separator < 0 ? line : line.slice(separator + 2)).trimEnd();
|
|
196
|
+
}
|
|
197
|
+
return "";
|
|
198
|
+
}
|
|
199
|
+
function normalizedEpoch(value) {
|
|
200
|
+
if (!/^-?\d+$/.test(value))
|
|
201
|
+
throw new Error(`invalid HQ_JOB_NOTIFY_NOW_EPOCH: ${value}`);
|
|
202
|
+
const result = Number(value);
|
|
203
|
+
if (!Number.isSafeInteger(result))
|
|
204
|
+
throw new Error(`invalid HQ_JOB_NOTIFY_NOW_EPOCH: ${value}`);
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
export function runJobNotify(argv, options = {}) {
|
|
208
|
+
const deps = {
|
|
209
|
+
...defaultJobNotifyDependencies(options.dependencies?.env ?? process.env, options.dependencies?.cwd ?? process.cwd()),
|
|
210
|
+
...options.dependencies,
|
|
211
|
+
};
|
|
212
|
+
if (options.hqRoot)
|
|
213
|
+
deps.env = { ...deps.env, HQ_ROOT: options.hqRoot };
|
|
214
|
+
const parsed = parseArguments(argv, deps);
|
|
215
|
+
if (typeof parsed === "number")
|
|
216
|
+
return parsed;
|
|
217
|
+
const hqRoot = parsed.hqRoot || options.hqRoot || deps.env.HQ_ROOT || "";
|
|
218
|
+
if (hqRoot)
|
|
219
|
+
deps.env = { ...deps.env, HQ_ROOT: hqRoot };
|
|
220
|
+
const nowEpoch = deps.env.HQ_JOB_NOTIFY_NOW_EPOCH
|
|
221
|
+
? normalizedEpoch(deps.env.HQ_JOB_NOTIFY_NOW_EPOCH)
|
|
222
|
+
: Math.floor(deps.now().getTime() / 1000);
|
|
223
|
+
const collapseSecText = deps.env.HQ_JOB_NOTIFY_COLLAPSE_SEC || "86400";
|
|
224
|
+
if (!/^-?\d+$/.test(collapseSecText))
|
|
225
|
+
throw new Error(`invalid HQ_JOB_NOTIFY_COLLAPSE_SEC: ${collapseSecText}`);
|
|
226
|
+
const collapseSec = Number(collapseSecText);
|
|
227
|
+
const alertBase = deps.env.HQ_JOB_ALERT_DIR || path.join(deps.env.HOME || path.sep, ".hq", "jobs", "alerts");
|
|
228
|
+
const stateDir = path.join(alertBase, parsed.jobId);
|
|
229
|
+
deps.mkdir(stateDir);
|
|
230
|
+
const stateFile = path.join(stateDir, "state.json");
|
|
231
|
+
const deliveryLog = path.join(stateDir, "delivery.log");
|
|
232
|
+
const log = (message) => deps.stderr(`hq-job-notify: ${message}\n`);
|
|
233
|
+
let notifyMode = parsed.notifyMode;
|
|
234
|
+
if (!notifyMode || notifyMode === "null")
|
|
235
|
+
notifyMode = "profile";
|
|
236
|
+
if (notifyMode === "none") {
|
|
237
|
+
log(`notify=none — skipping delivery for job ${parsed.jobId}`);
|
|
238
|
+
deps.stdout('{"delivered":false,"reason":"notify_none"}\n');
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
const selectedChannel = notifyMode === "profile"
|
|
242
|
+
? profileChannel(path.join(hqRoot, "personal/settings/schedule-alerts.yaml"), deps)
|
|
243
|
+
: "dm";
|
|
244
|
+
if (notifyMode !== "profile" && notifyMode !== "dm")
|
|
245
|
+
log(`unknown notify mode '${notifyMode}' — falling back to dm`);
|
|
246
|
+
let channelUnimplemented = 0;
|
|
247
|
+
if (selectedChannel !== "dm" && selectedChannel !== "") {
|
|
248
|
+
channelUnimplemented = 1;
|
|
249
|
+
log(`channel_unimplemented=${selectedChannel} — delivering dm fallback`);
|
|
250
|
+
}
|
|
251
|
+
let owner = parsed.owner;
|
|
252
|
+
if (!owner || owner === "null") {
|
|
253
|
+
owner = "";
|
|
254
|
+
if (deps.commandExists("hq")) {
|
|
255
|
+
owner = emailFromWhoami(deps.run("hq", ["whoami"], {
|
|
256
|
+
cwd: deps.cwd, env: deps.env, captureStdout: true,
|
|
257
|
+
}).stdout);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (!owner) {
|
|
261
|
+
log("no owner recipient — cannot dm (logged only); never fails the job");
|
|
262
|
+
deps.appendFile(deliveryLog, '{"delivered":false,"reason":"missing_owner"}\n');
|
|
263
|
+
}
|
|
264
|
+
let previous = {};
|
|
265
|
+
if (deps.exists(stateFile))
|
|
266
|
+
previous = JSON.parse(deps.readFile(stateFile));
|
|
267
|
+
const previousOutcome = previous.last_outcome == null ? "" : String(previous.last_outcome);
|
|
268
|
+
const previousFailureText = String(previous.last_failure_dm_epoch ?? 0);
|
|
269
|
+
const previousFailureEpoch = /^\d+$/.test(previousFailureText) ? Number(previousFailureText) : 0;
|
|
270
|
+
const kind = parsed.outcome === "ok"
|
|
271
|
+
? previousOutcome === "failed" || previousOutcome === "blocked" ? "recovered" : "ok"
|
|
272
|
+
: previousOutcome === "failed" || previousOutcome === "blocked"
|
|
273
|
+
? nowEpoch - previousFailureEpoch < collapseSec && previousFailureEpoch > 0 ? "suppressed" : "failed"
|
|
274
|
+
: "failed";
|
|
275
|
+
if (kind === "suppressed") {
|
|
276
|
+
log(`collapse: suppressing failure dm for job ${parsed.jobId} (within ${collapseSecText}s window)`);
|
|
277
|
+
const updatedAt = deps.now().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
278
|
+
deps.writeFile(stateFile, serializeJqJson({
|
|
279
|
+
last_outcome: parsed.outcome,
|
|
280
|
+
last_failure_dm_epoch: previousFailureEpoch,
|
|
281
|
+
last_updated_at: updatedAt,
|
|
282
|
+
last_failure_class: parsed.failureClass,
|
|
283
|
+
last_delivery: "suppressed",
|
|
284
|
+
}, "compact"));
|
|
285
|
+
try {
|
|
286
|
+
deps.chmod(stateFile, 0o600);
|
|
287
|
+
}
|
|
288
|
+
catch { /* chmod failure is ignored by the shell's `|| true`. */ }
|
|
289
|
+
deps.stdout(serializeJqJson({ delivered: false, reason: "collapsed_24h", channel_unimplemented: channelUnimplemented === 1 }, "compact"));
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
const statusWord = kind === "recovered" ? "recovered" : parsed.outcome;
|
|
293
|
+
const classPart = parsed.failureClass && parsed.failureClass !== "null" ? ` class=${parsed.failureClass}` : "";
|
|
294
|
+
const actionPart = parsed.nextAction ? ` next: ${parsed.nextAction}` : "";
|
|
295
|
+
let headline;
|
|
296
|
+
if (kind === "ok" || kind === "recovered")
|
|
297
|
+
headline = `Scheduled job '${parsed.jobName}' ${kind} (${parsed.duration}s)`;
|
|
298
|
+
else
|
|
299
|
+
headline = `Scheduled job '${parsed.jobName}' ${statusWord}${classPart} (${parsed.duration}s)`;
|
|
300
|
+
const details = `job=${parsed.jobId}\nstatus=${statusWord}\nclassification=${parsed.failureClass || "none"}\nsummary=${parsed.summary}\nduration_seconds=${parsed.duration}\nlog=${parsed.logPath || "none"}${actionPart}`;
|
|
301
|
+
let delivered = 0;
|
|
302
|
+
let deliverRc;
|
|
303
|
+
let dmBin = deps.env.HQ_JOB_NOTIFY_DM_BIN || "";
|
|
304
|
+
if (!dmBin && deps.commandExists("hq"))
|
|
305
|
+
dmBin = "hq";
|
|
306
|
+
if (!owner) {
|
|
307
|
+
log("skip dm send — missing owner");
|
|
308
|
+
deliverRc = 1;
|
|
309
|
+
}
|
|
310
|
+
else if (!dmBin) {
|
|
311
|
+
log("skip dm send — hq CLI not found");
|
|
312
|
+
deliverRc = 1;
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
const args = dmBin === "hq"
|
|
316
|
+
? ["dm", owner, headline, "--details", details]
|
|
317
|
+
: [owner, headline, details];
|
|
318
|
+
deliverRc = deps.run(dmBin, args, { cwd: deps.cwd, env: deps.env, captureStdout: false, logPath: deliveryLog }).code;
|
|
319
|
+
if (deliverRc === 0) {
|
|
320
|
+
delivered = 1;
|
|
321
|
+
log(`dm delivered to ${owner} kind=${kind} job=${parsed.jobId}`);
|
|
322
|
+
}
|
|
323
|
+
else
|
|
324
|
+
log(`dm delivery failed rc=${deliverRc} job=${parsed.jobId} (logged; not failing run)`);
|
|
325
|
+
}
|
|
326
|
+
let failureDmEpoch = previousFailureEpoch;
|
|
327
|
+
if (kind === "failed" && delivered === 1)
|
|
328
|
+
failureDmEpoch = nowEpoch;
|
|
329
|
+
else if (kind === "ok" || kind === "recovered")
|
|
330
|
+
failureDmEpoch = 0;
|
|
331
|
+
const updatedAt = deps.now().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
332
|
+
deps.writeFile(stateFile, serializeJqJson({
|
|
333
|
+
last_outcome: parsed.outcome,
|
|
334
|
+
last_failure_dm_epoch: failureDmEpoch,
|
|
335
|
+
last_updated_at: updatedAt,
|
|
336
|
+
last_failure_class: parsed.failureClass,
|
|
337
|
+
last_delivery: kind,
|
|
338
|
+
last_delivered: delivered === 1,
|
|
339
|
+
}, "compact"));
|
|
340
|
+
try {
|
|
341
|
+
deps.chmod(stateFile, 0o600);
|
|
342
|
+
}
|
|
343
|
+
catch { /* chmod failure is ignored by the shell's `|| true`. */ }
|
|
344
|
+
deps.stdout(serializeJqJson({
|
|
345
|
+
delivered: delivered === 1,
|
|
346
|
+
kind,
|
|
347
|
+
owner,
|
|
348
|
+
channel_unimplemented: channelUnimplemented === 1,
|
|
349
|
+
deliver_rc: deliverRc,
|
|
350
|
+
}, "compact"));
|
|
351
|
+
return 0;
|
|
352
|
+
}
|
|
353
|
+
export function registerCoreJobNotifyNativeCommand(group, core) {
|
|
354
|
+
registerCoreJobLeaf(group, core, "notify", ASSET, "Send a scheduled-job status alert to its owner", (argv, hqRoot) => runJobNotify(argv, { hqRoot }), canUseLiveRootForArguments, [
|
|
355
|
+
"--hq-root", "--job-id", "--job-name", "--outcome", "--summary", "--duration", "--log-path",
|
|
356
|
+
"--failure-class", "--next-action", "--owner", "--notify",
|
|
357
|
+
]);
|
|
358
|
+
}
|
|
359
|
+
//# sourceMappingURL=core-job-notify-native.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Native TypeScript port of hq-job-remediate.sh. */
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
type Environment = Record<string, string | undefined>;
|
|
4
|
+
export type JobRemediateRunOptions = {
|
|
5
|
+
cwd: string;
|
|
6
|
+
env: Environment;
|
|
7
|
+
logPath: string;
|
|
8
|
+
};
|
|
9
|
+
export type JobRemediateDependencies = {
|
|
10
|
+
env: Environment;
|
|
11
|
+
cwd: string;
|
|
12
|
+
/** Guard seam: this command must never inspect credential file contents. */
|
|
13
|
+
readFile: (file: string) => string;
|
|
14
|
+
commandExists: (command: string) => boolean;
|
|
15
|
+
run: (command: string, args: string[], options: JobRemediateRunOptions) => number;
|
|
16
|
+
stdout: (text: string) => void;
|
|
17
|
+
stderr: (text: string) => void;
|
|
18
|
+
};
|
|
19
|
+
export declare function defaultJobRemediateDependencies(env?: Environment, cwd?: string): JobRemediateDependencies;
|
|
20
|
+
export declare function runJobRemediate(argv: string[], options?: {
|
|
21
|
+
hqRoot?: string;
|
|
22
|
+
dependencies?: Partial<JobRemediateDependencies>;
|
|
23
|
+
}): number;
|
|
24
|
+
export declare function registerCoreJobRemediateNativeCommand(group: Command, core: Command): void;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=core-job-remediate-native.d.ts.map
|