@bli-cockpit/cli 0.2.242 → 0.2.244
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/dist/adapters/git-state.js +1 -0
- package/dist/adapters/raw-evidence-budget-admission.js +63 -0
- package/dist/adapters/raw-evidence-collection-state.js +6 -50
- package/dist/adapters/raw-evidence-git-diff.js +1 -0
- package/dist/agent-rules.js +8 -0
- package/dist/autostart-contract.js +12 -0
- package/dist/autostart-darwin-plist.js +2 -2
- package/dist/autostart-darwin.js +1 -1
- package/dist/autostart-remind.js +245 -0
- package/dist/autostart-self-heal.js +17 -1
- package/dist/autostart-windows-scripts.js +7 -7
- package/dist/autostart.js +16 -15
- package/dist/commands/approvals-check-where.js +14 -0
- package/dist/commands/approvals-render.js +8 -1
- package/dist/commands/approvals.js +3 -2
- package/dist/commands/autostart-command.js +10 -0
- package/dist/commands/autostart-heal.js +36 -1
- package/dist/commands/brain.js +1 -1
- package/dist/commands/cal-agenda.js +44 -0
- package/dist/commands/cal-availability-share.js +97 -0
- package/dist/commands/cal-booking.js +147 -0
- package/dist/commands/cal-join.js +62 -0
- package/dist/commands/cal-links.js +68 -0
- package/dist/commands/cal-people.js +136 -0
- package/dist/commands/cal-remind.js +176 -0
- package/dist/commands/cal-reminders.js +33 -0
- package/dist/commands/cal-suggest.js +89 -0
- package/dist/commands/cal.js +41 -1
- package/dist/commands/cycle-args.js +127 -0
- package/dist/commands/cycles.js +117 -0
- package/dist/commands/docs-comments-args.js +85 -0
- package/dist/commands/docs-comments.js +128 -0
- package/dist/commands/docs-files-args.js +49 -0
- package/dist/commands/docs-files.js +179 -0
- package/dist/commands/docs-formatting.js +45 -0
- package/dist/commands/docs-inbox-args.js +93 -0
- package/dist/commands/docs-inbox.js +126 -0
- package/dist/commands/docs-notion.js +5 -0
- package/dist/commands/docs-read.js +2 -1
- package/dist/commands/docs-recovery.js +173 -0
- package/dist/commands/docs-refs-args.js +104 -0
- package/dist/commands/docs-refs.js +153 -0
- package/dist/commands/docs-search-args.js +79 -0
- package/dist/commands/docs-search.js +64 -0
- package/dist/commands/docs-sync-args.js +82 -0
- package/dist/commands/docs-sync.js +197 -0
- package/dist/commands/docs-tree.js +136 -0
- package/dist/commands/docs.js +85 -51
- package/dist/commands/doctor-update.js +4 -1
- package/dist/commands/inbox-args.js +81 -0
- package/dist/commands/inbox.js +88 -0
- package/dist/commands/issue-batch.js +61 -0
- package/dist/commands/issue-relations.js +151 -0
- package/dist/commands/issue-write.js +38 -5
- package/dist/commands/issue.js +32 -1
- package/dist/commands/local-args-collector-sources.js +2 -1
- package/dist/commands/local-args-tower-approvals.js +16 -4
- package/dist/commands/local-args-tower-cal-booking.js +81 -0
- package/dist/commands/local-args-tower-cal-links.js +42 -0
- package/dist/commands/local-args-tower-cal-remind.js +74 -0
- package/dist/commands/local-args-tower-cal-share.js +41 -0
- package/dist/commands/local-args-tower-cal.js +81 -8
- package/dist/commands/local-args-tower-docs-msg.js +98 -9
- package/dist/commands/local-args-tower-msg.js +222 -10
- package/dist/commands/local-args-tower-search.js +32 -5
- package/dist/commands/local-args-tower-work.js +110 -34
- package/dist/commands/local-auth.js +1 -1
- package/dist/commands/local-help-commands-tower-ops.js +6 -4
- package/dist/commands/local-help-commands-tower.js +47 -31
- package/dist/commands/local-help-commands.js +3 -1
- package/dist/commands/local-help-cycles.js +9 -0
- package/dist/commands/local-help-docs-files.js +10 -0
- package/dist/commands/local-help-docs-formatting.js +7 -0
- package/dist/commands/local-help-docs-inbox.js +10 -0
- package/dist/commands/local-help-docs-recovery.js +10 -0
- package/dist/commands/local-help-docs-refs.js +11 -0
- package/dist/commands/local-help-docs-sync.js +9 -0
- package/dist/commands/local-help-inbox.js +9 -0
- package/dist/commands/local-help-project.js +21 -0
- package/dist/commands/local-help-templates.js +9 -0
- package/dist/commands/local-help-views.js +10 -0
- package/dist/commands/local-help-work-notify.js +6 -0
- package/dist/commands/local-help.js +3 -3
- package/dist/commands/manager-hints.js +4 -2
- package/dist/commands/manager-limits.json +3 -1
- package/dist/commands/manager-read-reasons.js +1 -0
- package/dist/commands/manager-render-home.js +13 -5
- package/dist/commands/manager-render.js +3 -0
- package/dist/commands/manager.js +3 -1
- package/dist/commands/mcp-stdio-probe.js +1 -0
- package/dist/commands/msg-activity.js +134 -0
- package/dist/commands/msg-channel-args.js +92 -0
- package/dist/commands/msg-channel.js +125 -0
- package/dist/commands/msg-context-args.js +67 -0
- package/dist/commands/msg-context.js +65 -0
- package/dist/commands/msg-dm.js +28 -0
- package/dist/commands/msg-file.js +185 -0
- package/dist/commands/msg-mark-args.js +78 -0
- package/dist/commands/msg-mark.js +148 -0
- package/dist/commands/msg-notify-args.js +100 -0
- package/dist/commands/msg-notify.js +122 -0
- package/dist/commands/msg-schedule-args.js +114 -0
- package/dist/commands/msg-schedule.js +226 -0
- package/dist/commands/msg-search-args.js +71 -0
- package/dist/commands/msg-search.js +48 -0
- package/dist/commands/msg-write.js +15 -1
- package/dist/commands/msg.js +62 -39
- package/dist/commands/project-args.js +82 -0
- package/dist/commands/project.js +71 -7
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/search.js +23 -0
- package/dist/commands/session-sync-failures.js +17 -4
- package/dist/commands/session-sync-health.js +7 -2
- package/dist/commands/session-sync-hold.js +42 -1
- package/dist/commands/session-sync-outside-roots.js +72 -14
- package/dist/commands/session-sync-upload.js +2 -0
- package/dist/commands/session-sync.js +1 -0
- package/dist/commands/sources-check.js +63 -2
- package/dist/commands/sources-discovery.js +1 -1
- package/dist/commands/sources-reconciliation.js +87 -0
- package/dist/commands/sync-doctor.js +14 -1
- package/dist/commands/sync-report.js +1 -1
- package/dist/commands/template-args.js +122 -0
- package/dist/commands/templates.js +162 -0
- package/dist/commands/view-args.js +131 -0
- package/dist/commands/views.js +138 -0
- package/dist/commands/work-notify-args.js +67 -0
- package/dist/commands/work-notify.js +63 -0
- package/dist/delivery-hold-notice.js +34 -0
- package/dist/evidence-transcripts-client.js +1 -0
- package/dist/log-rotation.js +2 -2
- package/dist/notifier.js +83 -0
- package/dist/process-runner.js +4 -0
- package/dist/repo-identity-git.js +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { terminalDeliveryReason } from "../raw-evidence-staging.js";
|
|
2
|
+
import { admitToStaging } from "../staging-gate.js";
|
|
3
|
+
/**
|
|
4
|
+
* The one door a file passes before its bytes may be copied: the per-sync
|
|
5
|
+
* budget below, then the disk (BLI-12832). A refusal from the disk hands the
|
|
6
|
+
* budget back, because nothing was staged and the next tick re-reads the source.
|
|
7
|
+
* A copy already staged costs the disk nothing and is never refused by it.
|
|
8
|
+
*/
|
|
9
|
+
export function admitEvidence(collection, byteLength, file) {
|
|
10
|
+
// Ticket 21952: a copy staged on an earlier tick is queued, not new bytes; re-charging it let a backlog use up the
|
|
11
|
+
// byte budget every tick. It still costs one object, and its upload resumes from the chunks the server holds.
|
|
12
|
+
const alreadyStaged = collection.staging.staged[file.contentHash] !== undefined;
|
|
13
|
+
const charged = alreadyStaged ? 0 : byteLength;
|
|
14
|
+
const budgetReason = admitToBudget(collection.budget, charged);
|
|
15
|
+
if (budgetReason)
|
|
16
|
+
return budgetReason;
|
|
17
|
+
const allowance = collection.budget.staging;
|
|
18
|
+
if (!allowance)
|
|
19
|
+
return null;
|
|
20
|
+
// BLI-12832: bytes the server refused for good are reclaimed by the prune
|
|
21
|
+
// (the refusal stays as a receipt), so copying them again would be churn.
|
|
22
|
+
if (!alreadyStaged &&
|
|
23
|
+
terminalDeliveryReason(collection.staging.delivery_attempts[file.contentHash]?.last_reason)) {
|
|
24
|
+
refundBudget(collection.budget, charged);
|
|
25
|
+
console.error("[staging-gate] not copying bytes the server refused for good", JSON.stringify({ reason: "delivery_terminal_refused", kind: file.kind, file_bytes: byteLength }));
|
|
26
|
+
return "delivery_terminal_refused";
|
|
27
|
+
}
|
|
28
|
+
const diskReason = admitToStaging(allowance, byteLength, {
|
|
29
|
+
alreadyStaged,
|
|
30
|
+
kind: file.kind,
|
|
31
|
+
});
|
|
32
|
+
if (!diskReason)
|
|
33
|
+
return null;
|
|
34
|
+
refundBudget(collection.budget, charged);
|
|
35
|
+
return `staging_deferred:${diskReason}`;
|
|
36
|
+
}
|
|
37
|
+
/** Hands back one admission that a later gate refused, so nothing was staged and nothing counts as sent. */
|
|
38
|
+
function refundBudget(budget, byteLength) {
|
|
39
|
+
budget.remainingObjects += 1;
|
|
40
|
+
budget.remainingBytes += byteLength;
|
|
41
|
+
budget.admittedBytes = Math.max(0, (budget.admittedBytes ?? 0) - byteLength);
|
|
42
|
+
budget.admittedObjects = Math.max(0, (budget.admittedObjects ?? 0) - 1);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Decrements the per-sync budget when a file fits, or returns a deferred-skip
|
|
46
|
+
* reason when it does not. The object budget bounds request count; the byte
|
|
47
|
+
* budget bounds buffered bytes.
|
|
48
|
+
*/
|
|
49
|
+
export function admitToBudget(budget, byteLength) {
|
|
50
|
+
const single = budget.singleFileByteAllowance;
|
|
51
|
+
const firstLargeFile = single && budget.remainingBytes === single.normalBytes &&
|
|
52
|
+
byteLength <= single.maxFileBytes;
|
|
53
|
+
const reason = budget.remainingObjects <= 0 ? "deferred_object_budget" :
|
|
54
|
+
byteLength > 0 && byteLength > budget.remainingBytes && !firstLargeFile ? "deferred_byte_budget" : null;
|
|
55
|
+
if (reason) {
|
|
56
|
+
[budget.deferredObjects, budget.deferredBytes] = [(budget.deferredObjects ?? 0) + 1, (budget.deferredBytes ?? 0) + byteLength];
|
|
57
|
+
return reason;
|
|
58
|
+
}
|
|
59
|
+
budget.remainingObjects -= 1;
|
|
60
|
+
budget.remainingBytes -= byteLength;
|
|
61
|
+
[budget.admittedBytes, budget.admittedObjects] = [(budget.admittedBytes ?? 0) + byteLength, (budget.admittedObjects ?? 0) + 1];
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
* contract, so `raw-evidence-completeness.ts` can read and write them without
|
|
8
8
|
* knowing about packs, budgets or disks.
|
|
9
9
|
*
|
|
10
|
-
* Two mechanics
|
|
10
|
+
* Two mechanics are exposed here rather than in any one reader, because the Codex
|
|
11
11
|
* reader, the Claude reader, the image reader and the git-diff reader all spend
|
|
12
12
|
* them:
|
|
13
13
|
*
|
|
14
14
|
* - **`admitToBudget`** — the per-sync byte and object budget (D7b),
|
|
15
15
|
* decremented in place so one `cockpit sync` across many worktrees honours
|
|
16
|
-
* one cap.
|
|
16
|
+
* one cap. Admission and disk-refusal refunds live together in
|
|
17
|
+
* `raw-evidence-budget-admission.ts`; the exports here keep reader APIs stable.
|
|
17
18
|
* - **`stageEvidenceBytes`** — bytes go on disk exactly once; content an
|
|
18
19
|
* earlier sync already staged is adopted rather than copied again.
|
|
19
20
|
*
|
|
@@ -26,8 +27,8 @@ import { RAW_EVIDENCE_UPLOAD_MAX_FILE_BYTES } from "@bli-cockpit/telemetry-core"
|
|
|
26
27
|
import crypto from "node:crypto";
|
|
27
28
|
import fs from "node:fs/promises";
|
|
28
29
|
import path from "node:path";
|
|
29
|
-
import { DELIVERY_BACKOFF_BYPASS_REASON, deliveryBackoffApplies, heldSourceKeys, readRawEvidenceStagingState, resolveStagedObject,
|
|
30
|
-
import {
|
|
30
|
+
import { DELIVERY_BACKOFF_BYPASS_REASON, deliveryBackoffApplies, heldSourceKeys, readRawEvidenceStagingState, resolveStagedObject, } from "../raw-evidence-staging.js";
|
|
31
|
+
import { openStagingAllowance, } from "../staging-gate.js";
|
|
31
32
|
import { chmodPrivate } from "./raw-evidence-pack-store.js";
|
|
32
33
|
import { GIT_DIFF_TIMEOUT_MS, MAX_GIT_DIFF_BYTES, } from "./raw-evidence-git-diff.js";
|
|
33
34
|
export const DEFAULT_SINCE_MINUTES = 24 * 60;
|
|
@@ -201,49 +202,4 @@ export async function stageEvidenceBytes(collection, options) {
|
|
|
201
202
|
collection.stagedNewCount += 1;
|
|
202
203
|
return { local_path: destination, staged_in_pack: true };
|
|
203
204
|
}
|
|
204
|
-
|
|
205
|
-
* The one door a file passes before its bytes may be copied: the per-sync
|
|
206
|
-
* budget below, then the disk (BLI-12832). A refusal from the disk hands the
|
|
207
|
-
* budget back, because nothing was staged and the next tick re-reads the source.
|
|
208
|
-
* A copy already staged costs the disk nothing and is never refused by it.
|
|
209
|
-
*/
|
|
210
|
-
export function admitEvidence(collection, byteLength, file) {
|
|
211
|
-
const budgetReason = admitToBudget(collection.budget, byteLength);
|
|
212
|
-
if (budgetReason)
|
|
213
|
-
return budgetReason;
|
|
214
|
-
const allowance = collection.budget.staging;
|
|
215
|
-
if (!allowance)
|
|
216
|
-
return null;
|
|
217
|
-
// BLI-12832: bytes the server refused for good are reclaimed by the prune
|
|
218
|
-
// (the refusal stays as a receipt), so copying them again would be churn.
|
|
219
|
-
if (collection.staging.staged[file.contentHash] === undefined &&
|
|
220
|
-
terminalDeliveryReason(collection.staging.delivery_attempts[file.contentHash]?.last_reason)) {
|
|
221
|
-
collection.budget.remainingObjects += 1;
|
|
222
|
-
collection.budget.remainingBytes += byteLength;
|
|
223
|
-
console.error("[staging-gate] not copying bytes the server refused for good", JSON.stringify({ reason: "delivery_terminal_refused", kind: file.kind, file_bytes: byteLength }));
|
|
224
|
-
return "delivery_terminal_refused";
|
|
225
|
-
}
|
|
226
|
-
const diskReason = admitToStaging(allowance, byteLength, {
|
|
227
|
-
alreadyStaged: collection.staging.staged[file.contentHash] !== undefined,
|
|
228
|
-
kind: file.kind,
|
|
229
|
-
});
|
|
230
|
-
if (!diskReason)
|
|
231
|
-
return null;
|
|
232
|
-
collection.budget.remainingObjects += 1;
|
|
233
|
-
collection.budget.remainingBytes += byteLength;
|
|
234
|
-
return `staging_deferred:${diskReason}`;
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Decrements the per-sync budget when a file fits, or returns a deferred-skip
|
|
238
|
-
* reason when it does not. The object budget bounds request count; the byte
|
|
239
|
-
* budget bounds buffered bytes.
|
|
240
|
-
*/
|
|
241
|
-
export function admitToBudget(budget, byteLength) {
|
|
242
|
-
if (budget.remainingObjects <= 0)
|
|
243
|
-
return "deferred_object_budget";
|
|
244
|
-
if (byteLength > budget.remainingBytes)
|
|
245
|
-
return "deferred_byte_budget";
|
|
246
|
-
budget.remainingObjects -= 1;
|
|
247
|
-
budget.remainingBytes -= byteLength;
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
205
|
+
export { admitEvidence, admitToBudget } from "./raw-evidence-budget-admission.js";
|
package/dist/agent-rules.js
CHANGED
|
@@ -180,6 +180,14 @@ export function cockpitAgentRulesBlock(options = {}) {
|
|
|
180
180
|
"- Computer use first, always. The human can take over the browser at any point; when that happens, stop driving, observe, and record what the human verified.",
|
|
181
181
|
"- A reviewer or QA agent spawned by the builder does not count as independent verification by itself — the receipt is what counts, not the reviewer's opinion.",
|
|
182
182
|
"- QA catches the cheap majority of bugs, not all of them. Do the pass, attach the receipts, ship; remaining bugs get fixed as normal bugs.",
|
|
183
|
+
"",
|
|
184
|
+
"## Ground rules",
|
|
185
|
+
"",
|
|
186
|
+
"- Database: raw SQL through the repo's one door. Never add Supabase JS `.from()` or `.rpc()` on the server. Every query under 500 ms through indexes. No caching to hide a slow read. Store totals, do not recount. Never OFFSET on serving paths. Never reset a database password.",
|
|
187
|
+
"- Migrations: additive ones merge on green. Stop for Edward if one can lose data or widens private reads.",
|
|
188
|
+
"- Plans: a large plan, database change or architecture change runs `plan-review` first and follows its verdict.",
|
|
189
|
+
"- Secrets: use them exec-only, never print them, and ask nobody to paste a token.",
|
|
190
|
+
"- Setups are zero-click for Edward.",
|
|
183
191
|
MANAGED_BLOCK_END,
|
|
184
192
|
].join("\n");
|
|
185
193
|
}
|
|
@@ -8,7 +8,19 @@ export const AUTOSTART_LABEL = "com.bli.cockpit.sync";
|
|
|
8
8
|
/** The name Task Scheduler files the same job under on Windows. */
|
|
9
9
|
export const WINDOWS_AUTOSTART_TASK_NAME = "BLI Cockpit Sync";
|
|
10
10
|
export const DEFAULT_AUTOSTART_INTERVAL_SECONDS = 15 * 60;
|
|
11
|
+
/**
|
|
12
|
+
* BLI-21564: the second job of the one autostart contract. It runs `cockpit cal remind`
|
|
13
|
+
* every minute (the laptop meeting-reminder tick), is installed, repaired and removed
|
|
14
|
+
* with the sync agent, and is never a long-lived process.
|
|
15
|
+
*/
|
|
16
|
+
export const REMIND_AUTOSTART_LABEL = "com.bli.cockpit.remind";
|
|
17
|
+
export const WINDOWS_REMIND_TASK_NAME = "BLI Cockpit Remind";
|
|
18
|
+
export const REMIND_INTERVAL_SECONDS = 60;
|
|
11
19
|
export const UNSUPPORTED_MESSAGE = "autostart is supported on macOS and Windows only";
|
|
20
|
+
/** Where the reminder LaunchAgent lives for a given home. */
|
|
21
|
+
export function remindPlistPathFor(homeDir) {
|
|
22
|
+
return path.join(homeDir, "Library", "LaunchAgents", `${REMIND_AUTOSTART_LABEL}.plist`);
|
|
23
|
+
}
|
|
12
24
|
/** Where the LaunchAgent lives for a given home; also the `plist_path` every
|
|
13
25
|
* result carries, including the unsupported-platform one. */
|
|
14
26
|
export function plistPathFor(homeDir) {
|
|
@@ -245,7 +245,7 @@ function renderDarwinSyncCommand(options) {
|
|
|
245
245
|
* .zshrc) still fails — but now it fails with a reason in the log instead of a
|
|
246
246
|
* bare 127.
|
|
247
247
|
*/
|
|
248
|
-
function darwinNodeResolutionPreamble(nodeExecutable) {
|
|
248
|
+
export function darwinNodeResolutionPreamble(nodeExecutable) {
|
|
249
249
|
return [
|
|
250
250
|
`node_bin=${shellQuote(nodeExecutable)}`,
|
|
251
251
|
`[ -x "$node_bin" ] || node_bin="$(${DARWIN_LOGIN_SHELL} -lc 'command -v node' 2>/dev/null)"`,
|
|
@@ -274,6 +274,6 @@ function darwinAgentRegistrationProblems(plist, expected) {
|
|
|
274
274
|
}
|
|
275
275
|
return problems;
|
|
276
276
|
}
|
|
277
|
-
function shellQuote(value) {
|
|
277
|
+
export function shellQuote(value) {
|
|
278
278
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
279
279
|
}
|
package/dist/autostart-darwin.js
CHANGED
|
@@ -151,7 +151,7 @@ export function launchdUserDomain(uid = process.getuid?.() ?? 0) {
|
|
|
151
151
|
export async function reregisterDarwinAgent(options) {
|
|
152
152
|
const domain = launchdUserDomain(options.uid);
|
|
153
153
|
const bootout = await options
|
|
154
|
-
.exec("launchctl", ["bootout", `${domain}/${AUTOSTART_LABEL}`])
|
|
154
|
+
.exec("launchctl", ["bootout", `${domain}/${options.label ?? AUTOSTART_LABEL}`])
|
|
155
155
|
.catch(() => ({ code: 1, stdout: "", stderr: "" }));
|
|
156
156
|
const bootstrap = await options
|
|
157
157
|
.exec("launchctl", ["bootstrap", domain, options.plistPath])
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileExists, REMIND_AUTOSTART_LABEL, REMIND_INTERVAL_SECONDS, remindPlistPathFor, resolveAutostartSettings, schedulerNodeExecutable, WINDOWS_REMIND_TASK_NAME, } from "./autostart-contract.js";
|
|
5
|
+
import { darwinNodeResolutionPreamble, shellQuote } from "./autostart-darwin-plist.js";
|
|
6
|
+
import { describeDifference, powershellLiteral, renderedFileDifference, renderWindowsLauncherScript, renderWindowsRegistrationScript, UTF8_BOM, WINDOWS_SYNC_LOG_LIMIT_BYTES, windowsPowerShellPath, windowsWScriptPath, } from "./autostart-windows-scripts.js";
|
|
7
|
+
import { windowsTaskRegistrationProblems } from "./autostart-windows-task-xml.js";
|
|
8
|
+
import { xmlEscape } from "./autostart-xml.js";
|
|
9
|
+
import { isDefaultDashboardUrl, getCollectorRuntimePaths } from "./local-state.js";
|
|
10
|
+
/**
|
|
11
|
+
* The reminder job: the SECOND scheduled job of the one autostart contract (BLI-21564).
|
|
12
|
+
*
|
|
13
|
+
* `com.bli.cockpit.remind` (launchd) and `BLI Cockpit Remind` (Task Scheduler) run
|
|
14
|
+
* `cockpit cal remind --quiet` every minute. They are installed, repaired and removed together with
|
|
15
|
+
* the sync agent (`autostart.ts` calls these three functions from the same front doors), honour the
|
|
16
|
+
* same opt-out marker through the same callers, and never watch a path: after a sleep the next tick
|
|
17
|
+
* runs, and the 5-minute grace decides shown versus missed. There is no always-on process.
|
|
18
|
+
*
|
|
19
|
+
* Nothing here throws for a host problem: every function answers with a named status and a message,
|
|
20
|
+
* and every deciding branch logs its reason (metadata only, never a path).
|
|
21
|
+
*/
|
|
22
|
+
const TAG = "[autostart-remind]";
|
|
23
|
+
const REMIND_LOG = "remind.log";
|
|
24
|
+
const REMIND_ERR_LOG = "remind.err.log";
|
|
25
|
+
const REMIND_SCRIPT = "autostart-remind.ps1";
|
|
26
|
+
const REMIND_LAUNCHER = "autostart-remind.vbs";
|
|
27
|
+
const REMIND_REGISTRATION = "autostart-remind-register.ps1";
|
|
28
|
+
const REMIND_COMMAND = ["cal", "remind", "--quiet"];
|
|
29
|
+
function stateFile(homeDir, name) {
|
|
30
|
+
return path.join(getCollectorRuntimePaths(homeDir ?? os.homedir()).state_dir, name);
|
|
31
|
+
}
|
|
32
|
+
function result(base) {
|
|
33
|
+
return { label: REMIND_AUTOSTART_LABEL, interval_seconds: REMIND_INTERVAL_SECONDS, ...base };
|
|
34
|
+
}
|
|
35
|
+
/** launchd's reminder plist: StartInterval 60, RunAtLoad, and deliberately no WatchPaths. */
|
|
36
|
+
export function renderRemindPlist(options) {
|
|
37
|
+
return [
|
|
38
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
39
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
40
|
+
'<plist version="1.0">',
|
|
41
|
+
"<dict>",
|
|
42
|
+
" <key>Label</key>",
|
|
43
|
+
` <string>${xmlEscape(REMIND_AUTOSTART_LABEL)}</string>`,
|
|
44
|
+
...remindProgramArguments(options),
|
|
45
|
+
" <key>StartInterval</key>",
|
|
46
|
+
` <integer>${REMIND_INTERVAL_SECONDS}</integer>`,
|
|
47
|
+
" <key>RunAtLoad</key>",
|
|
48
|
+
" <true/>",
|
|
49
|
+
" <key>StandardOutPath</key>",
|
|
50
|
+
` <string>${xmlEscape(options.stdoutPath)}</string>`,
|
|
51
|
+
" <key>StandardErrorPath</key>",
|
|
52
|
+
` <string>${xmlEscape(options.stderrPath)}</string>`,
|
|
53
|
+
"</dict>",
|
|
54
|
+
"</plist>",
|
|
55
|
+
"",
|
|
56
|
+
].join("\n");
|
|
57
|
+
}
|
|
58
|
+
function remindProgramArguments(options) {
|
|
59
|
+
const dashboardArg = isDefaultDashboardUrl(options.dashboardUrl) ? "" : ` --dashboard-url ${shellQuote(options.dashboardUrl)}`;
|
|
60
|
+
const command = [
|
|
61
|
+
...darwinNodeResolutionPreamble(options.nodeExecutable),
|
|
62
|
+
`"$node_bin" ${shellQuote(options.cliEntryPoint)} ${REMIND_COMMAND.join(" ")}${dashboardArg}`,
|
|
63
|
+
].join("; ");
|
|
64
|
+
return [" <key>ProgramArguments</key>", " <array>", " <string>/bin/zsh</string>", " <string>-c</string>", ` <string>${xmlEscape(command)}</string>`, " </array>"];
|
|
65
|
+
}
|
|
66
|
+
async function renderedDarwinPlist(options) {
|
|
67
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
68
|
+
const settings = resolveAutostartSettings(options, "darwin");
|
|
69
|
+
const node = await schedulerNodeExecutable(options, "darwin");
|
|
70
|
+
const paths = getCollectorRuntimePaths(homeDir);
|
|
71
|
+
return {
|
|
72
|
+
plistPath: remindPlistPathFor(homeDir),
|
|
73
|
+
plist: renderRemindPlist({
|
|
74
|
+
nodeExecutable: node.path,
|
|
75
|
+
cliEntryPoint: settings.cli_entry_point,
|
|
76
|
+
dashboardUrl: settings.dashboard_url,
|
|
77
|
+
stdoutPath: path.join(paths.state_dir, REMIND_LOG),
|
|
78
|
+
stderrPath: path.join(paths.state_dir, REMIND_ERR_LOG),
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** Writes the reminder plist without touching launchd (the detached healer registers it with bootout/bootstrap). */
|
|
83
|
+
export async function writeDarwinRemindPlist(options) {
|
|
84
|
+
const { plist, plistPath } = await renderedDarwinPlist(options);
|
|
85
|
+
await mkdir(path.dirname(plistPath), { recursive: true });
|
|
86
|
+
await mkdir(getCollectorRuntimePaths(options.homeDir ?? os.homedir()).state_dir, { recursive: true });
|
|
87
|
+
await writeFile(plistPath, plist, "utf8");
|
|
88
|
+
return { plist_path: plistPath };
|
|
89
|
+
}
|
|
90
|
+
function windowsPaths(homeDir) {
|
|
91
|
+
return {
|
|
92
|
+
script: stateFile(homeDir, REMIND_SCRIPT),
|
|
93
|
+
launcher: stateFile(homeDir, REMIND_LAUNCHER),
|
|
94
|
+
registration: stateFile(homeDir, REMIND_REGISTRATION),
|
|
95
|
+
log: stateFile(homeDir, REMIND_LOG),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function renderWindowsRemindScript(options) {
|
|
99
|
+
const dashboardArgs = isDefaultDashboardUrl(options.dashboardUrl) ? "" : ` --dashboard-url ${powershellLiteral(options.dashboardUrl)}`;
|
|
100
|
+
return [
|
|
101
|
+
// Same reasons as the sync script: stderr is a log channel here, so 'Continue', and failure is the exit code.
|
|
102
|
+
"$ErrorActionPreference = 'Continue'",
|
|
103
|
+
"$exitCode = 0",
|
|
104
|
+
`$nodeExecutable = ${powershellLiteral(options.nodeExecutable)}`,
|
|
105
|
+
`$cliEntryPoint = ${powershellLiteral(options.cliEntryPoint)}`,
|
|
106
|
+
`$remindLogPath = ${powershellLiteral(options.logPath)}`,
|
|
107
|
+
"$remindLogFile = Get-Item -LiteralPath $remindLogPath -ErrorAction SilentlyContinue",
|
|
108
|
+
`if ($remindLogFile -and $remindLogFile.Length -gt ${WINDOWS_SYNC_LOG_LIMIT_BYTES}) { Set-Content -LiteralPath $remindLogPath -Value '' -Encoding UTF8 }`,
|
|
109
|
+
"try {",
|
|
110
|
+
` & $nodeExecutable $cliEntryPoint ${REMIND_COMMAND.join(" ")}${dashboardArgs} 2>&1 | ForEach-Object { "$_" } | Add-Content -LiteralPath $remindLogPath -Encoding UTF8`,
|
|
111
|
+
" if ($LASTEXITCODE -ne 0) { $exitCode = $LASTEXITCODE }",
|
|
112
|
+
"} catch {",
|
|
113
|
+
" Add-Content -LiteralPath $remindLogPath -Encoding UTF8 -Value $_.Exception.Message",
|
|
114
|
+
" $exitCode = 1",
|
|
115
|
+
"}",
|
|
116
|
+
"exit $exitCode",
|
|
117
|
+
"",
|
|
118
|
+
].join("\r\n");
|
|
119
|
+
}
|
|
120
|
+
async function expectedWindowsFiles(options) {
|
|
121
|
+
const settings = resolveAutostartSettings(options, "win32");
|
|
122
|
+
const node = await schedulerNodeExecutable(options, "win32");
|
|
123
|
+
const paths = windowsPaths(options.homeDir);
|
|
124
|
+
return {
|
|
125
|
+
paths,
|
|
126
|
+
script: `${UTF8_BOM}${renderWindowsRemindScript({ nodeExecutable: node.path, cliEntryPoint: settings.cli_entry_point, dashboardUrl: settings.dashboard_url, logPath: paths.log })}`,
|
|
127
|
+
launcher: renderWindowsLauncherScript({ powershellPath: windowsPowerShellPath(), scriptPath: paths.script }),
|
|
128
|
+
registration: `${UTF8_BOM}${renderWindowsRegistrationScript({ taskName: WINDOWS_REMIND_TASK_NAME, launcherPath: paths.launcher, intervalMinutes: REMIND_INTERVAL_SECONDS / 60 })}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** Installs or repairs the reminder job for this host. Never throws. */
|
|
132
|
+
export async function installRemindAgent(options) {
|
|
133
|
+
const platform = options.platform ?? process.platform;
|
|
134
|
+
try {
|
|
135
|
+
if (platform === "darwin") {
|
|
136
|
+
const written = await writeDarwinRemindPlist(options);
|
|
137
|
+
await options.exec("launchctl", ["unload", written.plist_path]).catch(() => undefined);
|
|
138
|
+
const load = await options.exec("launchctl", ["load", written.plist_path]);
|
|
139
|
+
console.error(`${TAG} ${load.code === 0 ? "launchd job loaded" : "launchd job not loaded"}`, JSON.stringify({ label: REMIND_AUTOSTART_LABEL, exit_code: load.code, interval_seconds: REMIND_INTERVAL_SECONDS }));
|
|
140
|
+
return result({ status: "installed", path: written.plist_path, loaded: load.code === 0, ...(load.code === 0 ? {} : { message: `launchctl load exited ${load.code} for the reminder job` }) });
|
|
141
|
+
}
|
|
142
|
+
if (platform === "win32")
|
|
143
|
+
return await installWindowsRemind(options);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
console.error(`${TAG} install failed`, JSON.stringify({ reason: "install_threw", platform, error_name: error instanceof Error ? error.name : typeof error }));
|
|
147
|
+
return result({ status: "not_loaded", path: remindPlistPathFor(options.homeDir ?? os.homedir()), loaded: false, message: "the reminder job could not be written" });
|
|
148
|
+
}
|
|
149
|
+
return result({ status: "unsupported", path: remindPlistPathFor(options.homeDir ?? os.homedir()) });
|
|
150
|
+
}
|
|
151
|
+
async function installWindowsRemind(options) {
|
|
152
|
+
const expected = await expectedWindowsFiles(options);
|
|
153
|
+
await mkdir(path.dirname(expected.paths.script), { recursive: true });
|
|
154
|
+
await writeFile(expected.paths.script, expected.script, "utf8");
|
|
155
|
+
await writeFile(expected.paths.launcher, expected.launcher, "utf8");
|
|
156
|
+
await writeFile(expected.paths.registration, expected.registration, "utf8");
|
|
157
|
+
const created = await options.exec("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", expected.paths.registration]);
|
|
158
|
+
console.error(`${TAG} ${created.code === 0 ? "scheduled task registered" : "scheduled task registration failed"}`, JSON.stringify({ task_name: WINDOWS_REMIND_TASK_NAME, exit_code: created.code, interval_minutes: REMIND_INTERVAL_SECONDS / 60 }));
|
|
159
|
+
return result({
|
|
160
|
+
status: "installed", path: expected.paths.script, task_name: WINDOWS_REMIND_TASK_NAME, loaded: created.code === 0,
|
|
161
|
+
...(created.code === 0 ? {} : { message: `PowerShell registration of the reminder task exited ${created.code}` }),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/** Removes the reminder job and its files. `absent` when there was nothing, so it is safe to repeat. */
|
|
165
|
+
export async function uninstallRemindAgent(options) {
|
|
166
|
+
const platform = options.platform ?? process.platform;
|
|
167
|
+
if (platform === "darwin") {
|
|
168
|
+
const plistPath = remindPlistPathFor(options.homeDir ?? os.homedir());
|
|
169
|
+
if (!(await fileExists(plistPath)))
|
|
170
|
+
return result({ status: "absent", path: plistPath });
|
|
171
|
+
await options.exec("launchctl", ["unload", plistPath]).catch(() => undefined);
|
|
172
|
+
await rm(plistPath, { force: true });
|
|
173
|
+
console.error(`${TAG} launchd job removed`, JSON.stringify({ label: REMIND_AUTOSTART_LABEL }));
|
|
174
|
+
return result({ status: "uninstalled", path: plistPath });
|
|
175
|
+
}
|
|
176
|
+
if (platform === "win32") {
|
|
177
|
+
const paths = windowsPaths(options.homeDir);
|
|
178
|
+
const query = await options.exec("schtasks.exe", ["/Query", "/TN", WINDOWS_REMIND_TASK_NAME]);
|
|
179
|
+
const known = query.code === 0;
|
|
180
|
+
const removed = known ? await options.exec("schtasks.exe", ["/Delete", "/TN", WINDOWS_REMIND_TASK_NAME, "/F"]) : null;
|
|
181
|
+
if (removed && removed.code !== 0) {
|
|
182
|
+
console.error(`${TAG} scheduled task not removed`, JSON.stringify({ task_name: WINDOWS_REMIND_TASK_NAME, exit_code: removed.code }));
|
|
183
|
+
return result({ status: "not_loaded", path: paths.script, task_name: WINDOWS_REMIND_TASK_NAME, message: `schtasks /Delete exited ${removed.code} for the reminder task` });
|
|
184
|
+
}
|
|
185
|
+
for (const file of [paths.script, paths.launcher, paths.registration])
|
|
186
|
+
await rm(file, { force: true });
|
|
187
|
+
console.error(`${TAG} scheduled task ${known ? "removed" : "was not registered"}`, JSON.stringify({ task_name: WINDOWS_REMIND_TASK_NAME }));
|
|
188
|
+
return result({ status: known ? "uninstalled" : "absent", path: paths.script, task_name: WINDOWS_REMIND_TASK_NAME });
|
|
189
|
+
}
|
|
190
|
+
return result({ status: "unsupported", path: remindPlistPathFor(options.homeDir ?? os.homedir()) });
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Whether the reminder job is registered and still says what this build would write. Content is only compared
|
|
194
|
+
* when the caller supplied roots, exactly like the sync agent (a bare `autostart status` stays permissive).
|
|
195
|
+
*/
|
|
196
|
+
export async function remindAgentStatus(options) {
|
|
197
|
+
const platform = options.platform ?? process.platform;
|
|
198
|
+
const compare = Boolean(options.repoRoots && options.repoRoots.length > 0);
|
|
199
|
+
if (platform === "darwin")
|
|
200
|
+
return darwinRemindStatus(options, compare);
|
|
201
|
+
if (platform === "win32")
|
|
202
|
+
return windowsRemindStatus(options, compare);
|
|
203
|
+
return result({ status: "unsupported", path: remindPlistPathFor(options.homeDir ?? os.homedir()) });
|
|
204
|
+
}
|
|
205
|
+
async function darwinRemindStatus(options, compare) {
|
|
206
|
+
const plistPath = remindPlistPathFor(options.homeDir ?? os.homedir());
|
|
207
|
+
const onDisk = await readFile(plistPath, "utf8").catch(() => null);
|
|
208
|
+
if (onDisk === null)
|
|
209
|
+
return result({ status: "absent", path: plistPath });
|
|
210
|
+
const problems = [];
|
|
211
|
+
if (compare && onDisk !== (await renderedDarwinPlist(options)).plist)
|
|
212
|
+
problems.push("reminder plist does not match the current Tower runtime");
|
|
213
|
+
const list = await options.exec("launchctl", ["list", REMIND_AUTOSTART_LABEL]);
|
|
214
|
+
if (list.code !== 0)
|
|
215
|
+
problems.unshift("reminder job is not loaded");
|
|
216
|
+
console.error(`${TAG} status`, JSON.stringify({ label: REMIND_AUTOSTART_LABEL, problems: problems.length }));
|
|
217
|
+
return result({ status: problems.length === 0 ? "loaded" : "not_loaded", path: plistPath, loaded: problems.length === 0, ...(problems.length ? { message: problems.join("; ") } : {}) });
|
|
218
|
+
}
|
|
219
|
+
async function windowsRemindStatus(options, compare) {
|
|
220
|
+
const paths = windowsPaths(options.homeDir);
|
|
221
|
+
const query = await options.exec("schtasks.exe", ["/Query", "/TN", WINDOWS_REMIND_TASK_NAME, "/XML"]);
|
|
222
|
+
if (query.code !== 0) {
|
|
223
|
+
const absent = query.code === 1 && /cannot find|not found|does not exist/iu.test(`${query.stderr}\n${query.stdout}`);
|
|
224
|
+
console.error(`${TAG} task query failed`, JSON.stringify({ task_name: WINDOWS_REMIND_TASK_NAME, exit_code: query.code, status: absent ? "absent" : "not_loaded" }));
|
|
225
|
+
return result({ status: absent ? "absent" : "not_loaded", path: paths.script, task_name: WINDOWS_REMIND_TASK_NAME });
|
|
226
|
+
}
|
|
227
|
+
const problems = [
|
|
228
|
+
...(/<Enabled>\s*false\s*<\/Enabled>/iu.test(query.stdout) ? ["reminder task is disabled"] : []),
|
|
229
|
+
...windowsTaskRegistrationProblems(query.stdout, { launcherPath: paths.launcher, intervalMinutes: REMIND_INTERVAL_SECONDS / 60, wscriptPath: windowsWScriptPath() }),
|
|
230
|
+
];
|
|
231
|
+
if (!(await fileExists(paths.script)))
|
|
232
|
+
problems.push("reminder script is missing");
|
|
233
|
+
else if (!(await fileExists(paths.launcher)))
|
|
234
|
+
problems.push("reminder launcher is missing");
|
|
235
|
+
else if (compare) {
|
|
236
|
+
const expected = await expectedWindowsFiles(options);
|
|
237
|
+
for (const [label, file, text] of [["script", paths.script, expected.script], ["launcher", paths.launcher, expected.launcher]]) {
|
|
238
|
+
const difference = renderedFileDifference(await readFile(file, "utf8").catch(() => null), text);
|
|
239
|
+
if (difference)
|
|
240
|
+
problems.push(`reminder ${label} does not match the current Tower runtime (${describeDifference(difference)})`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
console.error(`${TAG} status`, JSON.stringify({ task_name: WINDOWS_REMIND_TASK_NAME, problems: problems.length }));
|
|
244
|
+
return result({ status: problems.length === 0 ? "loaded" : "not_loaded", path: paths.script, task_name: WINDOWS_REMIND_TASK_NAME, loaded: problems.length === 0, ...(problems.length ? { message: problems.join("; ") } : {}) });
|
|
245
|
+
}
|
|
@@ -53,7 +53,13 @@ async function readAutostartProblem(paths, options, platform) {
|
|
|
53
53
|
})
|
|
54
54
|
: [];
|
|
55
55
|
const marker = path.join(paths.state_dir, AUTOSTART_REPAIR_THROTTLE_MARKER);
|
|
56
|
-
|
|
56
|
+
// BLI-21564: the reminder job is repaired with the sync agent. A missing or broken reminder job is a
|
|
57
|
+
// problem of its own (an existing install that predates it gets it created here), and the opt-out
|
|
58
|
+
// marker is honoured above because an operator uninstall removed both.
|
|
59
|
+
const remindProblem = status.status === "loaded" && status.remind && status.remind.status !== "loaded" && status.remind.status !== "unsupported"
|
|
60
|
+
? [`reminder job ${status.remind.status === "absent" ? "absent" : status.remind.message ?? "not loaded"}`]
|
|
61
|
+
: [];
|
|
62
|
+
if (!absent && status.status !== "not_loaded" && runtimeProblems.length === 0 && remindProblem.length === 0) {
|
|
57
63
|
// Healthy is the steady state and stays silent. A marker left by an
|
|
58
64
|
// earlier attempt is spent: that repair worked, so the next break starts
|
|
59
65
|
// fresh instead of waiting out a retry window.
|
|
@@ -64,6 +70,7 @@ async function readAutostartProblem(paths, options, platform) {
|
|
|
64
70
|
...(absent ? ["registration absent"] : []),
|
|
65
71
|
...(status.message ? [status.message] : []),
|
|
66
72
|
...runtimeProblems,
|
|
73
|
+
...remindProblem,
|
|
67
74
|
]
|
|
68
75
|
.join("; ")
|
|
69
76
|
.slice(0, DETAIL_MAX_CHARS) || "registration not loaded";
|
|
@@ -111,6 +118,14 @@ async function repairWindowsAutostart(options, platform, absent, problem) {
|
|
|
111
118
|
exec: options.exec,
|
|
112
119
|
platform,
|
|
113
120
|
});
|
|
121
|
+
if (repaired.loaded && repaired.remind?.loaded === false) {
|
|
122
|
+
return {
|
|
123
|
+
status: "fail",
|
|
124
|
+
reason: "remind_repair_failed",
|
|
125
|
+
detail: (repaired.remind.message ?? problem).slice(0, DETAIL_MAX_CHARS),
|
|
126
|
+
step: "autostart_repair",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
114
129
|
if (repaired.loaded) {
|
|
115
130
|
return {
|
|
116
131
|
status: "ok",
|
|
@@ -182,6 +197,7 @@ function defaultSpawnDetached(command, args) {
|
|
|
182
197
|
const child = spawn(command, args, {
|
|
183
198
|
detached: true,
|
|
184
199
|
stdio: "ignore",
|
|
200
|
+
windowsHide: true,
|
|
185
201
|
});
|
|
186
202
|
child.unref();
|
|
187
203
|
return { pid: child.pid ?? null };
|
|
@@ -29,8 +29,8 @@ const WINDOWS_AUTOSTART_LAUNCHER_NAME = "autostart-sync.vbs";
|
|
|
29
29
|
const WINDOWS_SYNC_LOG_NAME = "sync.log";
|
|
30
30
|
/** With the window hidden, the log is the only place output goes; cap it so an
|
|
31
31
|
* always-on machine syncing every 15 minutes cannot grow it without bound. */
|
|
32
|
-
const WINDOWS_SYNC_LOG_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
33
|
-
const UTF8_BOM = "\uFEFF";
|
|
32
|
+
export const WINDOWS_SYNC_LOG_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
33
|
+
export const UTF8_BOM = "\uFEFF";
|
|
34
34
|
/** Where each written file lives, so the flow can name them in one breath. */
|
|
35
35
|
export function windowsAutostartScriptPath(homeDir) {
|
|
36
36
|
return windowsStateFile(homeDir, WINDOWS_AUTOSTART_SCRIPT_NAME);
|
|
@@ -160,7 +160,7 @@ export async function syncLauncherProblem(options) {
|
|
|
160
160
|
? `sync launcher does not match the current Tower runtime (${describeDifference(difference)})`
|
|
161
161
|
: null;
|
|
162
162
|
}
|
|
163
|
-
function renderedFileDifference(current, expected) {
|
|
163
|
+
export function renderedFileDifference(current, expected) {
|
|
164
164
|
const expectedLines = normalizedScriptLines(expected);
|
|
165
165
|
if (current === null) {
|
|
166
166
|
return {
|
|
@@ -201,7 +201,7 @@ function renderedFileDifference(current, expected) {
|
|
|
201
201
|
return null;
|
|
202
202
|
}
|
|
203
203
|
/** Safe to print: a reason label, a line index, two counts. No content. */
|
|
204
|
-
function describeDifference(difference) {
|
|
204
|
+
export function describeDifference(difference) {
|
|
205
205
|
if (difference.reason === "file_unreadable")
|
|
206
206
|
return "file could not be read";
|
|
207
207
|
if (difference.reason === "byte_order_mark")
|
|
@@ -218,7 +218,7 @@ function normalizedScriptLines(value) {
|
|
|
218
218
|
function hasByteOrderMark(value) {
|
|
219
219
|
return value.startsWith(UTF8_BOM);
|
|
220
220
|
}
|
|
221
|
-
function renderWindowsRegistrationScript(options) {
|
|
221
|
+
export function renderWindowsRegistrationScript(options) {
|
|
222
222
|
return [
|
|
223
223
|
"$ErrorActionPreference = 'Stop'",
|
|
224
224
|
`$taskName = ${powershellLiteral(options.taskName)}`,
|
|
@@ -254,7 +254,7 @@ function renderWindowsRegistrationScript(options) {
|
|
|
254
254
|
* doubling it; Windows paths cannot contain quotes, so the doubled quotes here
|
|
255
255
|
* are only the fixed delimiters around the two paths.
|
|
256
256
|
*/
|
|
257
|
-
function renderWindowsLauncherScript(options) {
|
|
257
|
+
export function renderWindowsLauncherScript(options) {
|
|
258
258
|
const quotedPowershell = `""${options.powershellPath}""`;
|
|
259
259
|
const quotedScript = `""${options.scriptPath}""`;
|
|
260
260
|
return [
|
|
@@ -305,6 +305,6 @@ function renderWindowsSyncScript(options) {
|
|
|
305
305
|
"",
|
|
306
306
|
].join("\r\n");
|
|
307
307
|
}
|
|
308
|
-
function powershellLiteral(value) {
|
|
308
|
+
export function powershellLiteral(value) {
|
|
309
309
|
return `'${value.replace(/'/gu, "''")}'`;
|
|
310
310
|
}
|
package/dist/autostart.js
CHANGED
|
@@ -4,6 +4,7 @@ import { darwinAgentStatus, installDarwinAgent, uninstallDarwinAgent, } from "./
|
|
|
4
4
|
import { readRegisteredDarwinRuntimePaths } from "./autostart-darwin-plist.js";
|
|
5
5
|
import { installWindowsTask, uninstallWindowsTask, windowsTaskStatus, } from "./autostart-windows.js";
|
|
6
6
|
import { readRegisteredWindowsRuntimePaths } from "./autostart-windows-scripts.js";
|
|
7
|
+
import { installRemindAgent, remindAgentStatus, uninstallRemindAgent } from "./autostart-remind.js";
|
|
7
8
|
/**
|
|
8
9
|
* Background collection's front door: install, uninstall, and status for the
|
|
9
10
|
* OS scheduler, whichever OS this is.
|
|
@@ -20,7 +21,7 @@ import { readRegisteredWindowsRuntimePaths } from "./autostart-windows-scripts.j
|
|
|
20
21
|
* autostart-contract.ts and is re-exported here because this module is the
|
|
21
22
|
* import site the rest of the collector already knows (BLI-3571).
|
|
22
23
|
*/
|
|
23
|
-
export { AUTOSTART_LABEL, DEFAULT_AUTOSTART_INTERVAL_SECONDS, WINDOWS_AUTOSTART_TASK_NAME, } from "./autostart-contract.js";
|
|
24
|
+
export { AUTOSTART_LABEL, DEFAULT_AUTOSTART_INTERVAL_SECONDS, WINDOWS_AUTOSTART_TASK_NAME, REMIND_AUTOSTART_LABEL, WINDOWS_REMIND_TASK_NAME, } from "./autostart-contract.js";
|
|
24
25
|
export { DARWIN_NETWORK_CHANGE_SIGNAL, DARWIN_NODE_MISSING_MESSAGE, writeDarwinAutostartPlist, } from "./autostart-darwin-plist.js";
|
|
25
26
|
export { launchdUserDomain, reregisterDarwinAgent, } from "./autostart-darwin.js";
|
|
26
27
|
/**
|
|
@@ -30,35 +31,35 @@ export { launchdUserDomain, reregisterDarwinAgent, } from "./autostart-darwin.js
|
|
|
30
31
|
*/
|
|
31
32
|
export async function installAutostartAgent(options) {
|
|
32
33
|
const platform = options.platform ?? process.platform;
|
|
33
|
-
if (platform
|
|
34
|
-
return installWindowsTask(options);
|
|
35
|
-
if (platform !== "darwin")
|
|
34
|
+
if (platform !== "win32" && platform !== "darwin")
|
|
36
35
|
return unsupportedResult(options.homeDir);
|
|
37
|
-
|
|
36
|
+
const sync = platform === "win32" ? await installWindowsTask(options) : await installDarwinAgent(options);
|
|
37
|
+
// BLI-21564: the reminder job is the second job of the same contract, installed and repaired with the sync agent.
|
|
38
|
+
return { ...sync, remind: await installRemindAgent(options) };
|
|
38
39
|
}
|
|
39
40
|
/**
|
|
40
|
-
* Removes the scheduled sync. Reports `absent` when there
|
|
41
|
-
* remove, so the command is safe to run repeatedly.
|
|
41
|
+
* Removes the scheduled sync and the reminder job. Reports `absent` when there
|
|
42
|
+
* was nothing to remove, so the command is safe to run repeatedly.
|
|
42
43
|
*/
|
|
43
44
|
export async function uninstallAutostartAgent(options) {
|
|
44
45
|
const platform = options.platform ?? process.platform;
|
|
45
|
-
if (platform
|
|
46
|
-
return uninstallWindowsTask(options);
|
|
47
|
-
if (platform !== "darwin")
|
|
46
|
+
if (platform !== "win32" && platform !== "darwin")
|
|
48
47
|
return unsupportedResult(options.homeDir);
|
|
49
|
-
|
|
48
|
+
const sync = platform === "win32" ? await uninstallWindowsTask(options) : await uninstallDarwinAgent(options);
|
|
49
|
+
return { ...sync, remind: await uninstallRemindAgent(options) };
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
52
|
* Reports whether the scheduled sync is installed, registered, and still
|
|
53
53
|
* matches the configured roots, cadence, dashboard URL, and Cockpit runtime.
|
|
54
|
+
* The reminder job's own reading rides beside it as `remind`; it never changes
|
|
55
|
+
* the sync agent's `status`, and the self-heal tick is what acts on it.
|
|
54
56
|
*/
|
|
55
57
|
export async function autostartStatus(options) {
|
|
56
58
|
const platform = options.platform ?? process.platform;
|
|
57
|
-
if (platform
|
|
58
|
-
return windowsTaskStatus(options);
|
|
59
|
-
if (platform !== "darwin")
|
|
59
|
+
if (platform !== "win32" && platform !== "darwin")
|
|
60
60
|
return unsupportedResult(options.homeDir);
|
|
61
|
-
|
|
61
|
+
const sync = platform === "win32" ? await windowsTaskStatus(options) : await darwinAgentStatus(options);
|
|
62
|
+
return { ...sync, remind: await remindAgentStatus(options) };
|
|
62
63
|
}
|
|
63
64
|
/**
|
|
64
65
|
* What the platform says it will run, parsed out of the registration itself
|