@indigoai-us/hq-cli 5.345.114 → 5.345.115
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 +10 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +5 -1
- package/dist/commands/bot-jobs.d.ts +1 -11
- package/dist/commands/bot-jobs.js +13 -34
- package/dist/commands/core-audit-log-native.js +1 -1
- package/dist/commands/core-checkpoint-native.d.ts +27 -0
- package/dist/commands/core-checkpoint-native.js +85 -2
- package/dist/commands/core-dm-bind-native.js +1 -1
- package/dist/commands/core-git-pack-extension-check-native.js +1 -1
- package/dist/commands/core-job-native-utils.d.ts +2 -2
- package/dist/commands/core-job-native-utils.js +3 -11
- package/dist/commands/core-job-notify-native.js +1 -2
- package/dist/commands/core-job-probe-native.d.ts +2 -3
- package/dist/commands/core-job-probe-native.js +14 -22
- package/dist/commands/core-job-remediate-native.js +1 -2
- package/dist/commands/core-job-run-native.js +58 -54
- package/dist/commands/core-jobs-native.d.ts +1 -3
- package/dist/commands/core-jobs-native.js +16 -271
- package/dist/commands/core-jobs-schedule.d.ts +7 -0
- package/dist/commands/core-jobs-schedule.js +243 -0
- package/dist/commands/core-native-utilities.d.ts +2 -3
- package/dist/commands/core-native-utilities.js +2 -19
- package/dist/commands/core-pin-native.js +1 -1
- package/dist/commands/core-refresh-vault-access-native.js +1 -1
- package/dist/commands/core-resolve-company-command.js +2 -17
- package/dist/commands/core-work-mesh-bind-trusted-native.js +1 -1
- package/dist/commands/core.js +1 -17
- package/dist/fast-scaffold.d.ts +1 -1
- package/dist/fast-scaffold.js +0 -38
- package/dist/lib/daemon/catalog.d.ts +0 -2
- package/dist/lib/daemon/catalog.js +3 -6
- package/dist/lib/daemon/run.js +0 -7
- package/dist/utils/resident-package-preload.js +2 -7
- package/package.json +1 -1
- package/assets/scaffold/core/scripts/audit-log.sh +0 -384
- package/assets/scaffold/core/scripts/git-pack-extension-check.sh +0 -164
- package/assets/scaffold/core/scripts/hq-dm-bind.sh +0 -590
- package/assets/scaffold/core/scripts/hq-job-notify.sh +0 -389
- package/assets/scaffold/core/scripts/hq-job-probe.sh +0 -865
- package/assets/scaffold/core/scripts/hq-job-remediate.sh +0 -359
- package/assets/scaffold/core/scripts/hq-job-run.sh +0 -1025
- package/assets/scaffold/core/scripts/hq-pin.sh +0 -118
- package/assets/scaffold/core/scripts/jobs-schedule-parse.sh +0 -310
- package/assets/scaffold/core/scripts/jobs-validate.sh +0 -482
- package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +0 -836
- package/assets/scaffold/core/scripts/refresh-vault-access.sh +0 -188
- package/assets/scaffold/core/scripts/resolve-company.sh +0 -331
- package/assets/scaffold/core/scripts/work-mesh-live-bind-trusted.sh +0 -184
- package/dist/commands/core-job-post-exit-assets.d.ts +0 -8
- package/dist/commands/core-job-post-exit-assets.js +0 -37
- package/dist/commands/core-native-jobs-flag.d.ts +0 -4
- package/dist/commands/core-native-jobs-flag.js +0 -7
- package/dist/commands/core-native-utilities-flag.d.ts +0 -4
- package/dist/commands/core-native-utilities-flag.js +0 -7
|
@@ -7,14 +7,11 @@ import { planResolvedCommandSpawn } from "../utils/windows-spawn.js";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { JSON_SCHEMA, load } from "js-yaml";
|
|
9
9
|
import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
|
|
10
|
-
import { resolveLiveRoot
|
|
11
|
-
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
10
|
+
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
12
11
|
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
13
|
-
import { coreNativeJobsEnabled } from "./core-native-jobs-flag.js";
|
|
14
12
|
import { releaseChildWrapperPackageUseLeaseAfterStart } from "../utils/child-wrapper-lease.js";
|
|
15
|
-
import { materializeCoreJobPostExitHelpers } from "./core-job-post-exit-assets.js";
|
|
16
|
-
const RUN_ASSET = "core/scripts/hq-job-run.sh";
|
|
17
13
|
const RUNNER_NAME = "hq-job-run";
|
|
14
|
+
const RUNNER_PATH = fileURLToPath(import.meta.url);
|
|
18
15
|
export const OUTPOST_IDENTITY_FILE = "/etc/outpost/codex-identity.env";
|
|
19
16
|
export const OUTPOST_RUNNER_FILE = "/usr/local/bin/outpost-runner.sh";
|
|
20
17
|
const REMOTE_LAUNCHER = fileURLToPath(new URL("./core-job-remote-launcher.js", import.meta.url));
|
|
@@ -148,6 +145,15 @@ export async function spawnProcess(request) {
|
|
|
148
145
|
if (helper === "remediate.sh" || helper === "hq-job-remediate.sh")
|
|
149
146
|
appendArgvRecord(request.env.HQ_JOB_RUN_REMEDIATE_ARGV_RECORD, "hq-job-remediate.sh", helperArgs);
|
|
150
147
|
}
|
|
148
|
+
if (request.file === "hq") {
|
|
149
|
+
const jobIndex = request.args.indexOf("job");
|
|
150
|
+
const helper = request.args[jobIndex + 1];
|
|
151
|
+
const helperArgs = request.args.slice(jobIndex + 2);
|
|
152
|
+
if (helper === "notify")
|
|
153
|
+
appendArgvRecord(request.env.HQ_JOB_RUN_NOTIFY_ARGV_RECORD, "hq-job-notify.sh", helperArgs);
|
|
154
|
+
if (helper === "remediate")
|
|
155
|
+
appendArgvRecord(request.env.HQ_JOB_RUN_REMEDIATE_ARGV_RECORD, "hq-job-remediate.sh", helperArgs);
|
|
156
|
+
}
|
|
151
157
|
if (request.file === "tmux" && request.args[0] === "new-session") {
|
|
152
158
|
const doneFile = request.env.HQ_JOB_RUN_REMOTE_DONE_FILE;
|
|
153
159
|
const exitFile = request.env.HQ_JOB_RUN_REMOTE_EXIT_FILE;
|
|
@@ -165,7 +171,8 @@ export async function spawnProcess(request) {
|
|
|
165
171
|
?? '{"retry_recommended":false,"attempted":false,"notes":"stubbed"}';
|
|
166
172
|
const code = nestedAgent ? Number(request.env.HQ_JOB_RUN_STUB_EXIT_CODE ?? "0") : 0;
|
|
167
173
|
const helperName = path.basename(request.args[0] ?? "");
|
|
168
|
-
const isRemediation = request.file === "bash" && (helperName === "remediate.sh" || helperName === "hq-job-remediate.sh")
|
|
174
|
+
const isRemediation = (request.file === "bash" && (helperName === "remediate.sh" || helperName === "hq-job-remediate.sh"))
|
|
175
|
+
|| (request.file === "hq" && request.args.includes("job") && request.args[request.args.indexOf("job") + 1] === "remediate");
|
|
169
176
|
const stdout = isRemediation ? Buffer.from(response) : Buffer.alloc(0);
|
|
170
177
|
return { code: Number.isInteger(code) ? code : 0, stdout, stderr: Buffer.alloc(0) };
|
|
171
178
|
}
|
|
@@ -359,7 +366,7 @@ function defaultDependencies(env) {
|
|
|
359
366
|
}
|
|
360
367
|
: (file) => acquireFlock(file, env),
|
|
361
368
|
sleep: stubbed ? async () => undefined : (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
362
|
-
scriptPath:
|
|
369
|
+
scriptPath: RUNNER_PATH,
|
|
363
370
|
};
|
|
364
371
|
}
|
|
365
372
|
function yqText(value, fallback = "") {
|
|
@@ -711,23 +718,25 @@ async function callNotify(jobId, jobName, outcome, summary, duration, logPath, f
|
|
|
711
718
|
emitLog("notify skipped (HQ_JOB_RUN_NO_NOTIFY=1)");
|
|
712
719
|
return;
|
|
713
720
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
721
|
+
if (notifyBin) {
|
|
722
|
+
try {
|
|
723
|
+
const info = deps.stat(notifyBin);
|
|
724
|
+
if (!(info.mode & 0o111)) {
|
|
725
|
+
try {
|
|
726
|
+
deps.chmod(notifyBin, info.mode | 0o111);
|
|
727
|
+
}
|
|
728
|
+
catch { /* shell ignores chmod failure */ }
|
|
719
729
|
}
|
|
720
|
-
catch { /* shell ignores chmod failure */ }
|
|
721
730
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
731
|
+
catch (error) {
|
|
732
|
+
if (error.code === "ENOENT") {
|
|
733
|
+
emitLog(`notify skipped — missing ${notifyBin}`);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
throw error;
|
|
727
737
|
}
|
|
728
|
-
throw error;
|
|
729
738
|
}
|
|
730
|
-
const
|
|
739
|
+
const helperArgs = [
|
|
731
740
|
"--hq-root", hqRoot,
|
|
732
741
|
"--job-id", jobId,
|
|
733
742
|
"--job-name", jobName,
|
|
@@ -740,27 +749,37 @@ async function callNotify(jobId, jobName, outcome, summary, duration, logPath, f
|
|
|
740
749
|
"--owner", owner,
|
|
741
750
|
"--notify", notifyMode,
|
|
742
751
|
];
|
|
743
|
-
const
|
|
752
|
+
const command = notifyBin ? "bash" : "hq";
|
|
753
|
+
const args = notifyBin
|
|
754
|
+
? [notifyBin, ...helperArgs]
|
|
755
|
+
: ["core", "--hq-root", hqRoot, "job", "notify", ...helperArgs];
|
|
756
|
+
const result = await runHelper(command, args, deps.cwd, { ...deps.env, HQ_ROOT: hqRoot, HQ_NO_UPDATE_CHECK: "1" }, deps, logPath);
|
|
744
757
|
if (result.code !== 0)
|
|
745
758
|
emitLog(`notify helper exited ${result.code} (ignored)`);
|
|
746
759
|
}
|
|
747
760
|
async function callRemediate(failureClass, jobId, company, hqRoot, remediateBin, logFile, deps) {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
761
|
+
if (remediateBin) {
|
|
762
|
+
try {
|
|
763
|
+
const info = deps.stat(remediateBin);
|
|
764
|
+
if (!(info.mode & 0o111)) {
|
|
765
|
+
try {
|
|
766
|
+
deps.chmod(remediateBin, info.mode | 0o111);
|
|
767
|
+
}
|
|
768
|
+
catch { /* shell ignores chmod failure */ }
|
|
753
769
|
}
|
|
754
|
-
|
|
770
|
+
}
|
|
771
|
+
catch (error) {
|
|
772
|
+
if (error.code === "ENOENT")
|
|
773
|
+
return '{"retry_recommended":false,"attempted":false,"notes":"remediate missing"}';
|
|
774
|
+
throw error;
|
|
755
775
|
}
|
|
756
776
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
const
|
|
763
|
-
const result = await runHelper("bash", args, deps.cwd, { ...deps.env, HQ_ROOT: hqRoot, HQ_JOB_REMEDIATE_LOG: logFile }, deps);
|
|
777
|
+
const helperArgs = ["--hq-root", hqRoot, "--failure-class", failureClass, "--job-id", jobId, "--company", company];
|
|
778
|
+
const command = remediateBin ? "bash" : "hq";
|
|
779
|
+
const args = remediateBin
|
|
780
|
+
? [remediateBin, ...helperArgs]
|
|
781
|
+
: ["core", "--hq-root", hqRoot, "job", "remediate", ...helperArgs];
|
|
782
|
+
const result = await runHelper(command, args, deps.cwd, { ...deps.env, HQ_ROOT: hqRoot, HQ_JOB_REMEDIATE_LOG: logFile, HQ_NO_UPDATE_CHECK: "1" }, deps);
|
|
764
783
|
if (result.stderr.length)
|
|
765
784
|
deps.appendFile(logFile, result.stderr);
|
|
766
785
|
const output = result.stdout.toString("utf8").replace(/\n+$/, "");
|
|
@@ -1113,19 +1132,12 @@ export async function runJob(argv, hqRootOverride, injected) {
|
|
|
1113
1132
|
catch (error) {
|
|
1114
1133
|
return emitDie(`failed to acquire global run lock: ${lockPath}${error instanceof Error && error.message.includes("flock") ? ` (${error.message})` : ""}`);
|
|
1115
1134
|
}
|
|
1116
|
-
let postExitHelpers = null;
|
|
1117
1135
|
try {
|
|
1118
1136
|
const startedAt = isoNow(deps);
|
|
1119
1137
|
const startedEpoch = deps.epochNow();
|
|
1120
1138
|
deps.appendFile(logFile, `=== hq-job-run ${jobId} ===\nstarted_at=${startedAt}\nruntime=${runtime}\ntimeout_seconds=${timeoutSeconds}\nsurface=${surface}\njob_file=${jobFile}\nhq_root=${hqRoot}\n`);
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
}
|
|
1124
|
-
catch (error) {
|
|
1125
|
-
deps.appendFile(logFile, `post_exit_helpers_unavailable=${error instanceof Error ? error.name : "Error"}; package lease retained\n`);
|
|
1126
|
-
}
|
|
1127
|
-
const notifyBin = postExitHelpers?.notifyBin ?? env.HQ_JOB_NOTIFY_BIN ?? path.join(path.dirname(deps.scriptPath), "hq-job-notify.sh");
|
|
1128
|
-
const remediateBin = postExitHelpers?.remediateBin ?? env.HQ_JOB_REMEDIATE_BIN ?? path.join(path.dirname(deps.scriptPath), "hq-job-remediate.sh");
|
|
1139
|
+
const notifyBin = env.HQ_JOB_NOTIFY_BIN ?? "";
|
|
1140
|
+
const remediateBin = env.HQ_JOB_REMEDIATE_BIN ?? "";
|
|
1129
1141
|
const authStatus = readAuthStatus(runtime, env, deps.home, deps);
|
|
1130
1142
|
if (authStatus !== "ok") {
|
|
1131
1143
|
const reason = authStatus === "no_cli"
|
|
@@ -1147,7 +1159,7 @@ export async function runJob(argv, hqRootOverride, injected) {
|
|
|
1147
1159
|
return emitDie(error instanceof Error ? error.message : String(error));
|
|
1148
1160
|
}
|
|
1149
1161
|
const secretNames = jobSecretNames(job);
|
|
1150
|
-
let exitCode = await runAgent(runtime, surface, prompt, runCwd, timeoutSeconds, secretNames, company, logFile, stamp, jobId, logDir, deps,
|
|
1162
|
+
let exitCode = await runAgent(runtime, surface, prompt, runCwd, timeoutSeconds, secretNames, company, logFile, stamp, jobId, logDir, deps, true);
|
|
1151
1163
|
let failureClass = "";
|
|
1152
1164
|
let nextAction = "";
|
|
1153
1165
|
let retried = 0;
|
|
@@ -1169,7 +1181,7 @@ export async function runJob(argv, hqRootOverride, injected) {
|
|
|
1169
1181
|
retried = 1;
|
|
1170
1182
|
deps.appendFile(logFile, "remediate_retry=1\n");
|
|
1171
1183
|
emitLog(`self-remediate recommended retry for ${failureClass} — retrying once`);
|
|
1172
|
-
exitCode = await runAgent(runtime, surface, prompt, runCwd, timeoutSeconds, secretNames, company, logFile, stamp, jobId, logDir, deps,
|
|
1184
|
+
exitCode = await runAgent(runtime, surface, prompt, runCwd, timeoutSeconds, secretNames, company, logFile, stamp, jobId, logDir, deps, true);
|
|
1173
1185
|
if (exitCode !== 0) {
|
|
1174
1186
|
failureClass = classifyFailure(exitCode, logFile, "", deps);
|
|
1175
1187
|
deps.appendFile(logFile, `failure_class_after_retry=${failureClass}\n`);
|
|
@@ -1203,7 +1215,6 @@ export async function runJob(argv, hqRootOverride, injected) {
|
|
|
1203
1215
|
}
|
|
1204
1216
|
finally {
|
|
1205
1217
|
await lease.release();
|
|
1206
|
-
postExitHelpers?.cleanup();
|
|
1207
1218
|
}
|
|
1208
1219
|
}
|
|
1209
1220
|
export function registerCoreJobRunCommand(job) {
|
|
@@ -1216,18 +1227,11 @@ export function registerCoreJobRunCommand(job) {
|
|
|
1216
1227
|
.action(async (args, _options, current) => {
|
|
1217
1228
|
const argv = rawArgumentsForCommand(current, args);
|
|
1218
1229
|
const root = current.parent?.parent?.opts().hqRoot ?? process.env.HQ_ROOT;
|
|
1219
|
-
if (!await coreNativeJobsEnabled()) {
|
|
1220
|
-
const resolvedRoot = resolveLiveRoot({ hqRoot: root });
|
|
1221
|
-
const result = runBundledScript({ asset: RUN_ASSET, args: argv, cwd: process.cwd(), hqRoot: resolvedRoot });
|
|
1222
|
-
if (result.code !== 0)
|
|
1223
|
-
process.exit(result.code);
|
|
1224
|
-
return;
|
|
1225
|
-
}
|
|
1226
1230
|
const result = await runJob(argv, root);
|
|
1227
1231
|
if (result !== 0)
|
|
1228
1232
|
process.exit(result);
|
|
1229
1233
|
});
|
|
1230
|
-
describeCoreCommand(command, { execution: "native",
|
|
1234
|
+
describeCoreCommand(command, { execution: "native", root: "live" });
|
|
1231
1235
|
}
|
|
1232
1236
|
function rawArgumentsForCommand(command, fallback, raw = process.argv) {
|
|
1233
1237
|
const names = [];
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/** Native TypeScript ports for the Wave 4a scheduled-jobs scaffold tools. */
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
-
export
|
|
4
|
-
export declare function runScheduleParse(argv: string[]): number;
|
|
3
|
+
export { detectHostTimezone, runScheduleParse, type ScheduleParseWriters } from "./core-jobs-schedule.js";
|
|
5
4
|
export declare function validateJobs(args: string[]): number;
|
|
6
5
|
type ReconcileCommandResult = {
|
|
7
6
|
status: number;
|
|
@@ -21,5 +20,4 @@ export declare function registerCoreJobsNativeCommand(core: Command): void;
|
|
|
21
20
|
export declare function rawArgumentsForCommand(command: Command, fallback: string[], raw?: string[]): string[];
|
|
22
21
|
/** Fast-entry adapter; the shared family flag is resolved before this module loads. */
|
|
23
22
|
export declare function runJobsFast(args: readonly string[], _stdin: string, _cwd: string, _liveRoot: string | undefined, commandName: string, hqRootArg: string | undefined): Promise<number>;
|
|
24
|
-
export {};
|
|
25
23
|
//# sourceMappingURL=core-jobs-native.d.ts.map
|
|
@@ -4,248 +4,11 @@ import { spawnSync } from "node:child_process";
|
|
|
4
4
|
import { JSON_SCHEMA, load, loadAll } from "js-yaml";
|
|
5
5
|
import { matchingSecretPatterns } from "../lib/core-utils/secret-patterns.js";
|
|
6
6
|
import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
|
|
7
|
-
import { shellDisplayPath } from "../lib/core-utils/shell-display-path.js";
|
|
8
7
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
9
|
-
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
10
8
|
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const RECONCILE_ASSET = "core/scripts/outpost-jobs-reconcile.sh";
|
|
14
|
-
const SCHEDULE_PARSE_USAGE = 'usage: jobs-schedule-parse.sh [--tz IANA] "<nl or cron schedule>"\n';
|
|
9
|
+
import { runScheduleParse } from "./core-jobs-schedule.js";
|
|
10
|
+
export { detectHostTimezone, runScheduleParse } from "./core-jobs-schedule.js";
|
|
15
11
|
const VALIDATE_USAGE = "usage: jobs-validate.sh <file-or-dir> [file-or-dir...]\n";
|
|
16
|
-
const CRON_RE = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$/;
|
|
17
|
-
const DAY_NAMES = {
|
|
18
|
-
sun: 0, sunday: 0, mon: 1, monday: 1, tue: 2, tues: 2, tuesday: 2,
|
|
19
|
-
wed: 3, wednesday: 3, thu: 4, thur: 4, thurs: 4, thursday: 4,
|
|
20
|
-
fri: 5, friday: 5, sat: 6, saturday: 6,
|
|
21
|
-
};
|
|
22
|
-
const DAY_LABELS = {
|
|
23
|
-
"0": "Sunday", "1": "Monday", "2": "Tuesday", "3": "Wednesday",
|
|
24
|
-
"4": "Thursday", "5": "Friday", "6": "Saturday", "1-5": "weekday",
|
|
25
|
-
"*": "day",
|
|
26
|
-
};
|
|
27
|
-
function parseClock(text) {
|
|
28
|
-
const match = /^(\d{1,2})(?::(\d{2}))?\s*(am|pm)?$/i.exec(text.trim().toLowerCase().replace(/ /g, ""));
|
|
29
|
-
if (!match)
|
|
30
|
-
return undefined;
|
|
31
|
-
let hour = Number.parseInt(match[1], 10);
|
|
32
|
-
const minute = Number.parseInt(match[2] || "0", 10);
|
|
33
|
-
const meridiem = match[3];
|
|
34
|
-
if (minute > 59)
|
|
35
|
-
return undefined;
|
|
36
|
-
if (meridiem) {
|
|
37
|
-
if (hour < 1 || hour > 12)
|
|
38
|
-
return undefined;
|
|
39
|
-
hour = meridiem === "am"
|
|
40
|
-
? (hour === 12 ? 0 : hour)
|
|
41
|
-
: (hour === 12 ? 12 : hour + 12);
|
|
42
|
-
}
|
|
43
|
-
else if (hour > 23) {
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
return [hour, minute];
|
|
47
|
-
}
|
|
48
|
-
function formatClock(hour, minute) {
|
|
49
|
-
const hour12 = hour % 12 || 12;
|
|
50
|
-
return `${hour12}:${String(minute).padStart(2, "0")} ${hour < 12 ? "AM" : "PM"}`;
|
|
51
|
-
}
|
|
52
|
-
function humanizeCron(cron, timezone) {
|
|
53
|
-
const fields = cron.split(/\s+/);
|
|
54
|
-
if (fields.length !== 5)
|
|
55
|
-
return `Cron \`${cron}\` (${timezone})`;
|
|
56
|
-
const [minute, hour, dayOfMonth, month, dayOfWeek] = fields;
|
|
57
|
-
if (/^\d+$/.test(minute) && /^\d+$/.test(hour) && dayOfMonth === "*"
|
|
58
|
-
&& month === "*" && (dayOfWeek === "*" || dayOfWeek === "1-5")) {
|
|
59
|
-
const day = dayOfWeek === "1-5" ? "Every weekday" : "Every day";
|
|
60
|
-
return `${day} at ${formatClock(+hour, +minute)} (${timezone})`;
|
|
61
|
-
}
|
|
62
|
-
if (/^\d+$/.test(minute) && /^\d+$/.test(hour) && dayOfMonth === "*"
|
|
63
|
-
&& month === "*" && /^\d+$/.test(dayOfWeek)) {
|
|
64
|
-
const day = DAY_LABELS[dayOfWeek] || `day ${dayOfWeek}`;
|
|
65
|
-
return `Every ${day} at ${formatClock(+hour, +minute)} (${timezone})`;
|
|
66
|
-
}
|
|
67
|
-
if (minute.startsWith("*/") && hour === "*" && dayOfMonth === "*"
|
|
68
|
-
&& month === "*" && dayOfWeek === "*") {
|
|
69
|
-
return `Every ${minute.slice(2)} minutes (${timezone})`;
|
|
70
|
-
}
|
|
71
|
-
if (minute === "0" && hour.startsWith("*/") && dayOfMonth === "*"
|
|
72
|
-
&& month === "*" && dayOfWeek === "*") {
|
|
73
|
-
return `Every ${hour.slice(2)} hours (${timezone})`;
|
|
74
|
-
}
|
|
75
|
-
if (minute === "0" && hour === "*" && dayOfMonth === "*"
|
|
76
|
-
&& month === "*" && dayOfWeek === "*") {
|
|
77
|
-
return `Every hour on the hour (${timezone})`;
|
|
78
|
-
}
|
|
79
|
-
return `Cron \`${cron}\` (${timezone})`;
|
|
80
|
-
}
|
|
81
|
-
function parseNaturalLanguage(text) {
|
|
82
|
-
const normalized = text.trim().toLowerCase();
|
|
83
|
-
let match = /^every\s+(\d+)\s+minutes?$/.exec(normalized);
|
|
84
|
-
if (match) {
|
|
85
|
-
const minutes = Number(match[1]);
|
|
86
|
-
if (minutes < 1 || minutes > 59)
|
|
87
|
-
return [null, "minute interval must be 1..59"];
|
|
88
|
-
return [`*/${minutes} * * * *`, `Every ${minutes} minutes`];
|
|
89
|
-
}
|
|
90
|
-
if (normalized === "every hour" || normalized === "hourly") {
|
|
91
|
-
return ["0 * * * *", "Every hour on the hour"];
|
|
92
|
-
}
|
|
93
|
-
match = /^every\s+(\d+)\s+hours?$/.exec(normalized);
|
|
94
|
-
if (match) {
|
|
95
|
-
const hours = Number(match[1]);
|
|
96
|
-
if (hours < 1 || hours > 23)
|
|
97
|
-
return [null, "hour interval must be 1..23"];
|
|
98
|
-
return [`0 */${hours} * * *`, `Every ${hours} hours`];
|
|
99
|
-
}
|
|
100
|
-
match = /^(?:every\s+)?(weekday|weekdays|day|daily)\s+(?:at\s+)?(.+)$/.exec(normalized);
|
|
101
|
-
if (match) {
|
|
102
|
-
const clock = parseClock(match[2]);
|
|
103
|
-
if (!clock)
|
|
104
|
-
return [null, `could not parse time: ${match[2]}`];
|
|
105
|
-
const [hour, minute] = clock;
|
|
106
|
-
const weekdays = match[1] === "weekday" || match[1] === "weekdays";
|
|
107
|
-
return [
|
|
108
|
-
`${minute} ${hour} * * ${weekdays ? "1-5" : "*"}`,
|
|
109
|
-
`${weekdays ? "Every weekday" : "Every day"} at ${formatClock(hour, minute)}`,
|
|
110
|
-
];
|
|
111
|
-
}
|
|
112
|
-
match = /^(?:every\s+)?(sun(?:day)?|mon(?:day)?|tue(?:s|sday)?|wed(?:nesday)?|thu(?:r|rs|rsday)?|fri(?:day)?|sat(?:urday)?)s?\s+(?:at\s+)?(.+)$/.exec(normalized);
|
|
113
|
-
if (match) {
|
|
114
|
-
const day = DAY_NAMES[match[1]];
|
|
115
|
-
const clock = parseClock(match[2]);
|
|
116
|
-
if (!clock)
|
|
117
|
-
return [null, `could not parse time: ${match[2]}`];
|
|
118
|
-
return [
|
|
119
|
-
`${clock[1]} ${clock[0]} * * ${day}`,
|
|
120
|
-
`Every ${DAY_LABELS[String(day)]} at ${formatClock(...clock)}`,
|
|
121
|
-
];
|
|
122
|
-
}
|
|
123
|
-
match = /^at\s+(.+?)\s+every\s+(weekday|weekdays|day|daily)$/.exec(normalized);
|
|
124
|
-
if (match) {
|
|
125
|
-
const clock = parseClock(match[1]);
|
|
126
|
-
if (!clock)
|
|
127
|
-
return [null, `could not parse time: ${match[1]}`];
|
|
128
|
-
const weekdays = match[2] === "weekday" || match[2] === "weekdays";
|
|
129
|
-
return [
|
|
130
|
-
`${clock[1]} ${clock[0]} * * ${weekdays ? "1-5" : "*"}`,
|
|
131
|
-
`${weekdays ? "Every weekday" : "Every day"} at ${formatClock(...clock)}`,
|
|
132
|
-
];
|
|
133
|
-
}
|
|
134
|
-
return [
|
|
135
|
-
null,
|
|
136
|
-
"unrecognized schedule — try 'every weekday at 9am', 'every day at 14:30', "
|
|
137
|
-
+ "'every monday at 9am', a 5-field cron, or 'every 15 minutes'",
|
|
138
|
-
];
|
|
139
|
-
}
|
|
140
|
-
function isValidTimezone(timezone) {
|
|
141
|
-
const wellFormed = /^(UTC|[A-Za-z0-9_+-]+(\/[A-Za-z0-9_+-]+)+)$/;
|
|
142
|
-
if (!wellFormed.test(timezone))
|
|
143
|
-
return false;
|
|
144
|
-
try {
|
|
145
|
-
new Intl.DateTimeFormat(undefined, { timeZone: timezone });
|
|
146
|
-
return true;
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
// Hosts without tzdata accept the same well-formed names as the shell.
|
|
150
|
-
return wellFormed.test(timezone);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
export function detectHostTimezone(override, resolveLocaltime = fs.realpathSync, readTimedatectl = () => {
|
|
154
|
-
const result = spawnSync("timedatectl", ["show", "-p", "Timezone", "--value"], { encoding: "utf8", timeout: 2000, stdio: ["ignore", "pipe", "ignore"] });
|
|
155
|
-
if (result.error || result.status !== 0)
|
|
156
|
-
return "";
|
|
157
|
-
return result.stdout || "";
|
|
158
|
-
}) {
|
|
159
|
-
if (override)
|
|
160
|
-
return override;
|
|
161
|
-
const environmentTimezone = (process.env.TZ || "").trim();
|
|
162
|
-
if (environmentTimezone && environmentTimezone.includes("/"))
|
|
163
|
-
return environmentTimezone;
|
|
164
|
-
try {
|
|
165
|
-
const realpath = resolveLocaltime("/etc/localtime");
|
|
166
|
-
const parts = realpath.split(path.sep).filter(Boolean);
|
|
167
|
-
const zoneinfo = parts.indexOf("zoneinfo");
|
|
168
|
-
if (zoneinfo >= 0 && parts.slice(zoneinfo + 1).length) {
|
|
169
|
-
return parts.slice(zoneinfo + 1).join("/");
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
catch {
|
|
173
|
-
// Ignore an unavailable local timezone link.
|
|
174
|
-
}
|
|
175
|
-
try {
|
|
176
|
-
const timezone = readTimedatectl().trim();
|
|
177
|
-
if (timezone && timezone.includes("/"))
|
|
178
|
-
return timezone;
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
// Ignore unavailable timedatectl.
|
|
182
|
-
}
|
|
183
|
-
return "UTC";
|
|
184
|
-
}
|
|
185
|
-
export function runScheduleParse(argv) {
|
|
186
|
-
let timezoneOverride = "";
|
|
187
|
-
const args = [...argv];
|
|
188
|
-
while (args.length) {
|
|
189
|
-
const argument = args[0];
|
|
190
|
-
if (argument === "--tz") {
|
|
191
|
-
if (args.length < 2) {
|
|
192
|
-
process.stderr.write(SCHEDULE_PARSE_USAGE);
|
|
193
|
-
return 2;
|
|
194
|
-
}
|
|
195
|
-
timezoneOverride = args[1];
|
|
196
|
-
args.splice(0, 2);
|
|
197
|
-
}
|
|
198
|
-
else if (argument === "--json") {
|
|
199
|
-
args.shift();
|
|
200
|
-
}
|
|
201
|
-
else if (argument === "-h" || argument === "--help") {
|
|
202
|
-
process.stderr.write(SCHEDULE_PARSE_USAGE);
|
|
203
|
-
return 2;
|
|
204
|
-
}
|
|
205
|
-
else if (argument === "--") {
|
|
206
|
-
args.shift();
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
else if (argument.startsWith("-")) {
|
|
210
|
-
process.stderr.write(`jobs-schedule-parse: unknown flag: ${argument}\n${SCHEDULE_PARSE_USAGE}`);
|
|
211
|
-
return 2;
|
|
212
|
-
}
|
|
213
|
-
else {
|
|
214
|
-
break;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
if (!args.length) {
|
|
218
|
-
process.stderr.write(SCHEDULE_PARSE_USAGE);
|
|
219
|
-
return 2;
|
|
220
|
-
}
|
|
221
|
-
const input = args.join(" ").trim();
|
|
222
|
-
if (!input) {
|
|
223
|
-
process.stdout.write('{"ok":false,"error":"empty schedule"}\n');
|
|
224
|
-
return 1;
|
|
225
|
-
}
|
|
226
|
-
const timezone = detectHostTimezone(timezoneOverride);
|
|
227
|
-
if (!isValidTimezone(timezone)) {
|
|
228
|
-
process.stdout.write(`${JSON.stringify({ ok: false, error: `invalid IANA timezone: ${timezone}` })}\n`);
|
|
229
|
-
return 1;
|
|
230
|
-
}
|
|
231
|
-
if (CRON_RE.test(input) && !/[A-Za-z]/.test(input)) {
|
|
232
|
-
process.stdout.write(`${JSON.stringify({ ok: true, cron: input, timezone, human: humanizeCron(input, timezone), source: "cron" })}\n`);
|
|
233
|
-
return 0;
|
|
234
|
-
}
|
|
235
|
-
const namedCron = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(mon-fri|monday-friday)$/i.exec(input);
|
|
236
|
-
if (namedCron) {
|
|
237
|
-
const cron = `${namedCron[1]} ${namedCron[2]} ${namedCron[3]} ${namedCron[4]} 1-5`;
|
|
238
|
-
process.stdout.write(`${JSON.stringify({ ok: true, cron, timezone, human: humanizeCron(cron, timezone), source: "cron" })}\n`);
|
|
239
|
-
return 0;
|
|
240
|
-
}
|
|
241
|
-
const [cron, message] = parseNaturalLanguage(input);
|
|
242
|
-
if (cron === null) {
|
|
243
|
-
process.stdout.write(`${JSON.stringify({ ok: false, error: message })}\n`);
|
|
244
|
-
return 1;
|
|
245
|
-
}
|
|
246
|
-
process.stdout.write(`${JSON.stringify({ ok: true, cron, timezone, human: `${message} (${timezone})`, source: "nl" })}\n`);
|
|
247
|
-
return 0;
|
|
248
|
-
}
|
|
249
12
|
function isValidCronField(value, minimum, maximum) {
|
|
250
13
|
return value.split(",").every((part) => {
|
|
251
14
|
if (part === "*")
|
|
@@ -971,9 +734,10 @@ function servicePathValue() {
|
|
|
971
734
|
function systemdQuote(value) {
|
|
972
735
|
return `"${value.replace(/%/g, "%%").replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
|
|
973
736
|
}
|
|
974
|
-
function serviceUnit(root,
|
|
737
|
+
function serviceUnit(root, id) {
|
|
975
738
|
const pathValue = systemdQuote(`PATH=${servicePathValue()}`);
|
|
976
|
-
|
|
739
|
+
const runner = path.resolve(root, "core/scripts/hq-job-run.sh");
|
|
740
|
+
return `# Generated by outpost-jobs-reconcile.sh — disposable derived state. Do not edit.\n[Unit]\nDescription=HQ Outpost scheduled job: ${id}\nDocumentation=file://${root}/core/knowledge/public/hq-core/outpost-jobs-spec.md\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nEnvironment=HQ_ROOT=${root}\nEnvironment=${pathValue}\nExecStart=${runner} --hq-root ${root} --job-id ${id}\nNice=10\n\n[Install]\nWantedBy=default.target\n`;
|
|
977
741
|
}
|
|
978
742
|
function timerUnit(id, onCalendar, randomize, root) {
|
|
979
743
|
return `# Generated by outpost-jobs-reconcile.sh — disposable derived state. Do not edit.\n[Unit]\nDescription=HQ Outpost scheduled job timer: ${id}\nDocumentation=file://${root}/core/knowledge/public/hq-core/outpost-jobs-spec.md\n\n[Timer]\nOnCalendar=${onCalendar}\nPersistent=true\nRandomizedDelaySec=${randomize}\nUnit=hq-job-${id}.service\n\n[Install]\nWantedBy=timers.target\n`;
|
|
@@ -1010,9 +774,8 @@ function installSyncHook(root) {
|
|
|
1010
774
|
"# hq-sync-runner executes personal/hooks/Stop/* after each sync cycle.",
|
|
1011
775
|
"set -u",
|
|
1012
776
|
'HQ_ROOT="${OUTPOST_HQ_ROOT:-${HQ_ROOT:-/home/ec2-user/hq}}"',
|
|
1013
|
-
'
|
|
1014
|
-
'
|
|
1015
|
-
' bash "$RECONCILE" --hq-root "$HQ_ROOT" --after-sync >>"${HOME}/.hq/jobs/reconcile/after-sync.log" 2>&1 || true',
|
|
777
|
+
'if command -v hq >/dev/null 2>&1; then',
|
|
778
|
+
' HQ_NO_UPDATE_CHECK=1 hq core --hq-root "$HQ_ROOT" jobs reconcile --after-sync >>"${HOME}/.hq/jobs/reconcile/after-sync.log" 2>&1 || true',
|
|
1016
779
|
"fi",
|
|
1017
780
|
"",
|
|
1018
781
|
].join("\n");
|
|
@@ -1087,15 +850,8 @@ export async function runReconcile(argv, rootArg, options = {}) {
|
|
|
1087
850
|
}
|
|
1088
851
|
if (!root)
|
|
1089
852
|
root = resolveLiveRoot();
|
|
1090
|
-
const
|
|
1091
|
-
const
|
|
1092
|
-
const probe = path.join(root, "core", "scripts", "hq-job-probe.sh");
|
|
1093
|
-
if (!fs.existsSync(validator))
|
|
1094
|
-
return reconcileDie("missing jobs-validate.sh");
|
|
1095
|
-
if (!fs.existsSync(runner))
|
|
1096
|
-
return reconcileDie("missing hq-job-run.sh");
|
|
1097
|
-
if (!fs.existsSync(probe))
|
|
1098
|
-
return reconcileDie("missing hq-job-probe.sh");
|
|
853
|
+
const servicePath = servicePathValue();
|
|
854
|
+
const hq = findExecutable("hq", servicePath) || "hq";
|
|
1099
855
|
const home = process.env.HOME ?? "";
|
|
1100
856
|
const unitDir = process.env.HQ_JOB_UNIT_DIR || path.join(home, ".config", "systemd", "user");
|
|
1101
857
|
const cacheDir = process.env.HQ_JOB_STATUS_CACHE_DIR || path.join(home, ".hq", "jobs", "status-cache");
|
|
@@ -1131,7 +887,7 @@ export async function runReconcile(argv, rootArg, options = {}) {
|
|
|
1131
887
|
if (readiness === "ready")
|
|
1132
888
|
continue;
|
|
1133
889
|
log(`probe ${id} (cached readiness=${readiness})`);
|
|
1134
|
-
const result = commandRunner("
|
|
890
|
+
const result = commandRunner(hq, ["core", "--hq-root", root, "job", "probe", "--hq-root", root, file]);
|
|
1135
891
|
probed += 1;
|
|
1136
892
|
const probeReadiness = probeValue(result.stdout, "readiness");
|
|
1137
893
|
if (probeReadiness) {
|
|
@@ -1209,7 +965,7 @@ export async function runReconcile(argv, rootArg, options = {}) {
|
|
|
1209
965
|
if (timezone && !` ${onCalendar} `.includes(` ${timezone} `))
|
|
1210
966
|
onCalendar = `${onCalendar} ${timezone}`;
|
|
1211
967
|
desired.add(id);
|
|
1212
|
-
const service = serviceUnit(root,
|
|
968
|
+
const service = serviceUnit(root, id);
|
|
1213
969
|
const timer = timerUnit(id, onCalendar, randomize, root);
|
|
1214
970
|
const unchanged = fs.existsSync(servicePath) && fs.existsSync(timerPath)
|
|
1215
971
|
&& fs.readFileSync(servicePath, "utf8") === service
|
|
@@ -1282,7 +1038,7 @@ export async function runReconcile(argv, rootArg, options = {}) {
|
|
|
1282
1038
|
}
|
|
1283
1039
|
export function registerCoreJobsNativeCommand(core) {
|
|
1284
1040
|
const jobs = core.command("jobs").description("Scheduled jobs helpers");
|
|
1285
|
-
const register = (name,
|
|
1041
|
+
const register = (name, summary, run) => {
|
|
1286
1042
|
const command = jobs.command(name)
|
|
1287
1043
|
.description(summary)
|
|
1288
1044
|
.helpOption(false)
|
|
@@ -1292,26 +1048,15 @@ export function registerCoreJobsNativeCommand(core) {
|
|
|
1292
1048
|
.action(async (args, _options, current) => {
|
|
1293
1049
|
const argv = rawArgumentsForCommand(current, args);
|
|
1294
1050
|
const groupHqRoot = core.opts().hqRoot;
|
|
1295
|
-
const { coreNativeJobsEnabled } = await import("./core-native-jobs-flag.js");
|
|
1296
|
-
if (!await coreNativeJobsEnabled()) {
|
|
1297
|
-
const hqRoot = asset === RECONCILE_ASSET && !argv.includes("--hq-root")
|
|
1298
|
-
&& !argv.includes("--help") && !argv.includes("-h")
|
|
1299
|
-
? resolveLiveRoot({ hqRoot: groupHqRoot })
|
|
1300
|
-
: undefined;
|
|
1301
|
-
const result = runBundledScript({ asset, args: argv, cwd: process.cwd(), ...(hqRoot ? { hqRoot } : {}) });
|
|
1302
|
-
if (result.code !== 0)
|
|
1303
|
-
process.exit(result.code);
|
|
1304
|
-
return;
|
|
1305
|
-
}
|
|
1306
1051
|
const result = await run(argv, groupHqRoot ?? process.env.HQ_ROOT);
|
|
1307
1052
|
if (result !== 0)
|
|
1308
1053
|
process.exit(result);
|
|
1309
1054
|
});
|
|
1310
|
-
describeCoreCommand(command, { execution: "native",
|
|
1055
|
+
describeCoreCommand(command, { execution: "native", root: "live" });
|
|
1311
1056
|
};
|
|
1312
|
-
register("schedule-parse",
|
|
1313
|
-
register("validate",
|
|
1314
|
-
register("reconcile",
|
|
1057
|
+
register("schedule-parse", "Resolve natural-language or raw cron schedules", (args) => runScheduleParse(args));
|
|
1058
|
+
register("validate", "Validate scheduled-job YAML files", validateJobs);
|
|
1059
|
+
register("reconcile", "Materialize scheduled jobs on this outpost", runReconcile);
|
|
1315
1060
|
}
|
|
1316
1061
|
export function rawArgumentsForCommand(command, fallback, raw = process.argv) {
|
|
1317
1062
|
const names = [];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type ScheduleParseWriters = {
|
|
2
|
+
stdout: (value: string) => void;
|
|
3
|
+
stderr: (value: string) => void;
|
|
4
|
+
};
|
|
5
|
+
export declare function detectHostTimezone(override: string, resolveLocaltime?: (file: string) => string, readTimedatectl?: () => string): string;
|
|
6
|
+
export declare function runScheduleParse(argv: string[], writers?: ScheduleParseWriters): number;
|
|
7
|
+
//# sourceMappingURL=core-jobs-schedule.d.ts.map
|