@ouro.bot/cli 0.1.0-alpha.904 → 0.1.0-alpha.905
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.json +6 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/daemon/cli-defaults.js +17 -0
- package/dist/heart/daemon/cli-exec.js +68 -3
- package/dist/heart/daemon/cli-help.js +6 -0
- package/dist/heart/daemon/cli-parse.js +7 -0
- package/dist/heart/versioning/ouro-version-manager.js +27 -4
- package/dist/heart/versioning/release-sources.js +173 -0
- package/dist/heart/versioning/self-update-defaults.js +294 -0
- package/dist/heart/versioning/self-update.js +196 -0
- package/dist/heart/versioning/update-state.js +126 -0
- package/dist/heart/versioning/updater-launchd.js +150 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.905",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Ouro keeps itself current without a human. New `ouro self-update` runs one unattended pass. It honors an `ouro rollback` pin for seven days and then rewrites the intent to latest (pins now record pinnedAt, and a pin without one gets it written into version-intent.json when first seen). It asks registry.npmjs.org, the npm registry configured on the machine and GitHub releases for the newest @ouro.bot/cli and installs from whichever serves it, so a network that blocks npmjs or a lagging npm mirror no longer strands a machine; a GitHub release counts only when github-actions[bot] created the release and its asset under this repository's v<version> download URL. It validates and runs the new version's --version before activating it, swaps CurrentVersion atomically, restarts the daemon only if it was already running (a stopped daemon stays stopped, and a dev-mode daemon is left alone), and when the restarted daemon does not answer with the new version it reactivates the previous one, pins it with reason auto-rollback and never retries that release; a newer release replaces the auto-rollback pin. When nothing is newer but a running daemon still reports an older version than CurrentVersion, it restarts the daemon with the same health check and rollback, and the current status names any release source that did not answer. Each pass writes ~/.ouro-cli/update-state.json, and `ouro status` ends with an Updates line that starts with \"needs attention\" when the last pass failed, the updater has not run for three hours, or the launchd agent is missing. On macOS, `ouro up` and `ouro self-update` install bot.ouro.updater, a launchd agent that runs the pass at login and hourly, independent of the daemon, preferring a Homebrew node of the same major version so nvm switches do not rewrite it; `ouro up` does not reload the agent while a live pass holds the update lock, the lock is reclaimed only when its owner process is gone, and the `ouro up` it runs to restart the daemon has stdin closed and OURO_UNATTENDED=1 so its prompts take their default answer. After publishing to npm, CI downloads the exact tarball npm serves, attests its build provenance and attaches it to a GitHub release. Rolling back below this version stops unattended updates until `ouro up --latest`. A package end-to-end test serves the packed build as a GitHub release and runs `ouro self-update` in a clean HOME. See docs/known-issues-and-recovery.md. The Unraid template and the Sanctuary bundle runtime move to this version."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
4
10
|
{
|
|
5
11
|
"version": "0.1.0-alpha.904",
|
|
6
12
|
"changes": [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0"?>
|
|
2
2
|
<Container version="2">
|
|
3
3
|
<Name>ouro-butler</Name>
|
|
4
|
-
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.905</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -68,6 +68,8 @@ const agent_discovery_1 = require("./agent-discovery");
|
|
|
68
68
|
const bundle_manifest_1 = require("../../mind/bundle-manifest");
|
|
69
69
|
const ouro_bot_global_installer_1 = require("../versioning/ouro-bot-global-installer");
|
|
70
70
|
const version_intent_1 = require("../versioning/version-intent");
|
|
71
|
+
const self_update_defaults_1 = require("../versioning/self-update-defaults");
|
|
72
|
+
const update_state_1 = require("../versioning/update-state");
|
|
71
73
|
const logs_prune_1 = require("./logs-prune");
|
|
72
74
|
const daemon_health_1 = require("./daemon-health");
|
|
73
75
|
const log_tailer_1 = require("./log-tailer");
|
|
@@ -445,6 +447,11 @@ function defaultPrepareDaemonRuntimeReplacement() {
|
|
|
445
447
|
(0, child_process_1.execSync)("/bin/sleep 1", { stdio: "ignore" });
|
|
446
448
|
}
|
|
447
449
|
async function defaultPromptInput(question) {
|
|
450
|
+
// The unattended updater runs `ouro up` with no one to answer; take the
|
|
451
|
+
// default answer instead of waiting on a stdin that never ends. Piped
|
|
452
|
+
// stdin (scripts, tests) still answers prompts.
|
|
453
|
+
if (process.env.OURO_UNATTENDED === "1")
|
|
454
|
+
return "";
|
|
448
455
|
const readline = await Promise.resolve().then(() => __importStar(require("readline/promises")));
|
|
449
456
|
const rl = readline.createInterface({
|
|
450
457
|
input: process.stdin,
|
|
@@ -813,6 +820,16 @@ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SO
|
|
|
813
820
|
getCurrentCliVersion: () => (0, ouro_version_manager_1.getCurrentVersion)({}),
|
|
814
821
|
getPreviousCliVersion: () => (0, ouro_version_manager_1.getPreviousVersion)({}),
|
|
815
822
|
listCliVersions: () => (0, ouro_version_manager_1.listInstalledVersions)({}),
|
|
823
|
+
runSelfUpdate: async () => {
|
|
824
|
+
const { runSelfUpdate } = await Promise.resolve().then(() => __importStar(require("../versioning/self-update")));
|
|
825
|
+
const { createDefaultSelfUpdateDeps } = await Promise.resolve().then(() => __importStar(require("../versioning/self-update-defaults")));
|
|
826
|
+
return runSelfUpdate(createDefaultSelfUpdateDeps({ socketPath }));
|
|
827
|
+
},
|
|
828
|
+
ensureUpdaterAgent: () => (0, self_update_defaults_1.ensureDefaultUpdaterAgent)(),
|
|
829
|
+
readUpdateStatusLine: () => (0, update_state_1.formatUpdateStatusLine)((0, update_state_1.readUpdateState)(), {
|
|
830
|
+
now: Date.now(),
|
|
831
|
+
updaterInstalled: (0, self_update_defaults_1.isDefaultUpdaterAgentInstalled)(),
|
|
832
|
+
}),
|
|
816
833
|
reExecFromNewVersion: (reArgs) => {
|
|
817
834
|
const entry = path.join((0, ouro_version_manager_1.getOuroCliHome)(), "CurrentVersion", "node_modules", "@ouro.bot", "cli", "dist", "heart", "daemon", "ouro-entry.js");
|
|
818
835
|
require("child_process").execFileSync("node", [entry, ...reArgs], { stdio: "inherit" });
|
|
@@ -5956,6 +5956,46 @@ function writeVersionIntentWithRecoveryLauncher(deps, intent) {
|
|
|
5956
5956
|
}
|
|
5957
5957
|
deps.writeVersionIntent(intent);
|
|
5958
5958
|
}
|
|
5959
|
+
const SELF_UPDATE_FAILURE_OUTCOMES = new Set(["unreachable", "install-failed", "rejected", "rolled-back"]);
|
|
5960
|
+
async function executeSelfUpdate(command, deps) {
|
|
5961
|
+
if (!deps.runSelfUpdate) {
|
|
5962
|
+
return returnCliFailure(deps, "self-update is not available in this runtime");
|
|
5963
|
+
}
|
|
5964
|
+
const result = await deps.runSelfUpdate();
|
|
5965
|
+
const lines = [`self-update: ${result.outcome}: ${result.summary}`];
|
|
5966
|
+
// Install or refresh the hourly updater after the pass so its first run
|
|
5967
|
+
// (RunAtLoad) does not race this one for the update lock.
|
|
5968
|
+
if (deps.ensureUpdaterAgent) {
|
|
5969
|
+
try {
|
|
5970
|
+
const agent = deps.ensureUpdaterAgent();
|
|
5971
|
+
if (agent === "installed")
|
|
5972
|
+
lines.push("unattended updater: installed (runs hourly and at login)");
|
|
5973
|
+
else if (agent === "written-reload-pending")
|
|
5974
|
+
lines.push("unattended updater: refreshed; reloads on the next interactive run");
|
|
5975
|
+
else if (agent === null && !command.unattended)
|
|
5976
|
+
lines.push("unattended updater: not available on this platform");
|
|
5977
|
+
}
|
|
5978
|
+
catch (error) {
|
|
5979
|
+
lines.push(`unattended updater: could not install: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`);
|
|
5980
|
+
}
|
|
5981
|
+
}
|
|
5982
|
+
const message = lines.join("\n");
|
|
5983
|
+
if (SELF_UPDATE_FAILURE_OUTCOMES.has(result.outcome)) {
|
|
5984
|
+
return returnCliFailure(deps, message);
|
|
5985
|
+
}
|
|
5986
|
+
deps.writeStdout(message);
|
|
5987
|
+
return message;
|
|
5988
|
+
}
|
|
5989
|
+
function withUpdateStatusLine(message, deps) {
|
|
5990
|
+
let line = null;
|
|
5991
|
+
try {
|
|
5992
|
+
line = deps.readUpdateStatusLine?.() ?? null;
|
|
5993
|
+
}
|
|
5994
|
+
catch {
|
|
5995
|
+
line = null;
|
|
5996
|
+
}
|
|
5997
|
+
return line ? `${message}\n\n${line}` : message;
|
|
5998
|
+
}
|
|
5959
5999
|
async function performSystemSetup(deps) {
|
|
5960
6000
|
// Install ouro command to PATH (non-blocking)
|
|
5961
6001
|
if (deps.installOuroCommand) {
|
|
@@ -6031,6 +6071,21 @@ async function performSystemSetup(deps) {
|
|
|
6031
6071
|
}
|
|
6032
6072
|
/* v8 ignore stop */
|
|
6033
6073
|
}
|
|
6074
|
+
// Keep the unattended updater installed so updates do not depend on the daemon.
|
|
6075
|
+
if (deps.ensureUpdaterAgent) {
|
|
6076
|
+
try {
|
|
6077
|
+
deps.ensureUpdaterAgent();
|
|
6078
|
+
}
|
|
6079
|
+
catch (error) {
|
|
6080
|
+
(0, runtime_1.emitNervesEvent)({
|
|
6081
|
+
level: "warn",
|
|
6082
|
+
component: "daemon",
|
|
6083
|
+
event: "daemon.system_setup_updater_agent_error",
|
|
6084
|
+
message: "failed to install the unattended updater launch agent",
|
|
6085
|
+
meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
|
|
6086
|
+
});
|
|
6087
|
+
}
|
|
6088
|
+
}
|
|
6034
6089
|
// Register .ouro bundle type (UTI on macOS)
|
|
6035
6090
|
await registerOuroBundleTypeNonBlocking(deps);
|
|
6036
6091
|
}
|
|
@@ -7274,7 +7329,13 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
7274
7329
|
}
|
|
7275
7330
|
targetVersion = previousVersion;
|
|
7276
7331
|
}
|
|
7277
|
-
writeVersionIntentWithRecoveryLauncher(deps, {
|
|
7332
|
+
writeVersionIntentWithRecoveryLauncher(deps, {
|
|
7333
|
+
schemaVersion: 1,
|
|
7334
|
+
mode: "pinned",
|
|
7335
|
+
targetVersion,
|
|
7336
|
+
pinnedAt: new Date((deps.now ?? Date.now)()).toISOString(),
|
|
7337
|
+
reason: "rollback",
|
|
7338
|
+
});
|
|
7278
7339
|
deps.activateCliVersion(targetVersion);
|
|
7279
7340
|
// Stop daemon (non-fatal if not running)
|
|
7280
7341
|
try {
|
|
@@ -7291,6 +7352,10 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
7291
7352
|
if (command.kind === "a2a.identity" || command.kind === "a2a.message" || command.kind === "a2a.outbox") {
|
|
7292
7353
|
return executeA2AClientCommand(command, deps);
|
|
7293
7354
|
}
|
|
7355
|
+
// ── self-update (unattended-safe update pass; never starts a stopped daemon) ──
|
|
7356
|
+
if (command.kind === "self-update") {
|
|
7357
|
+
return executeSelfUpdate(command, deps);
|
|
7358
|
+
}
|
|
7294
7359
|
// ── versions command (local install list + published update truth, no daemon socket needed) ──
|
|
7295
7360
|
if (command.kind === "versions") {
|
|
7296
7361
|
const versions = deps.listCliVersions?.() ?? [];
|
|
@@ -8979,7 +9044,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
8979
9044
|
if (command.kind === "daemon.status" && (0, cli_render_1.isDaemonUnavailableError)(error)) {
|
|
8980
9045
|
const message = command.json
|
|
8981
9046
|
? daemonUnavailableStatusJsonOutput(deps.socketPath, deps.healthFilePath)
|
|
8982
|
-
: (0, cli_render_1.daemonUnavailableStatusOutput)(deps.socketPath, deps.healthFilePath);
|
|
9047
|
+
: withUpdateStatusLine((0, cli_render_1.daemonUnavailableStatusOutput)(deps.socketPath, deps.healthFilePath), deps);
|
|
8983
9048
|
deps.writeStdout(message);
|
|
8984
9049
|
return message;
|
|
8985
9050
|
}
|
|
@@ -8994,7 +9059,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
8994
9059
|
const message = command.kind === "daemon.status"
|
|
8995
9060
|
? command.json
|
|
8996
9061
|
? formatDaemonStatusJsonOutput(response)
|
|
8997
|
-
: (0, cli_render_1.formatDaemonStatusOutput)(response, fallbackMessage)
|
|
9062
|
+
: withUpdateStatusLine((0, cli_render_1.formatDaemonStatusOutput)(response, fallbackMessage), deps)
|
|
8998
9063
|
: command.kind === "habit.probe" && command.json
|
|
8999
9064
|
? habitProbeJsonResponse(response)
|
|
9000
9065
|
: fallbackMessage;
|
|
@@ -62,6 +62,12 @@ exports.COMMAND_REGISTRY = {
|
|
|
62
62
|
usage: "ouro rollback [<version>]",
|
|
63
63
|
example: "ouro rollback 0.1.0-alpha.250",
|
|
64
64
|
},
|
|
65
|
+
"self-update": {
|
|
66
|
+
category: "Lifecycle",
|
|
67
|
+
description: "Update the installed CLI to the newest release from npm or GitHub, restart a running daemon on it, roll back if it fails its checks, and install the hourly launchd updater on macOS",
|
|
68
|
+
usage: "ouro self-update [--unattended]",
|
|
69
|
+
example: "ouro self-update",
|
|
70
|
+
},
|
|
65
71
|
versions: {
|
|
66
72
|
category: "Lifecycle",
|
|
67
73
|
description: "List installed CLI versions and show the active version intent mode and target",
|
|
@@ -2988,6 +2988,13 @@ function parseOuroCommand(args) {
|
|
|
2988
2988
|
throw new Error("Usage: ouro move <agent> here");
|
|
2989
2989
|
return { kind: "agent.move", agent: second };
|
|
2990
2990
|
}
|
|
2991
|
+
if (head === "self-update") {
|
|
2992
|
+
const rest = args.slice(1);
|
|
2993
|
+
const unknown = rest.filter((token) => token !== "--unattended");
|
|
2994
|
+
if (unknown.length > 0)
|
|
2995
|
+
throw new Error("Usage: ouro self-update [--unattended]");
|
|
2996
|
+
return { kind: "self-update", ...(rest.includes("--unattended") ? { unattended: true } : {}) };
|
|
2997
|
+
}
|
|
2991
2998
|
if (head === "stop" || head === "down")
|
|
2992
2999
|
return { kind: "daemon.stop" };
|
|
2993
3000
|
if (head === "status") {
|
|
@@ -240,6 +240,7 @@ function activateVersion(version, deps) {
|
|
|
240
240
|
const readlinkSync = deps.readlinkSync ?? fs.readlinkSync;
|
|
241
241
|
const unlinkSync = deps.unlinkSync ?? fs.unlinkSync;
|
|
242
242
|
const symlinkSync = deps.symlinkSync ?? fs.symlinkSync;
|
|
243
|
+
const renameSync = deps.renameSync ?? fs.renameSync;
|
|
243
244
|
const existsSync = deps.existsSync ?? fs.existsSync;
|
|
244
245
|
/* v8 ignore stop */
|
|
245
246
|
const currentVersionPath = path.join(cliHome, "CurrentVersion");
|
|
@@ -269,11 +270,33 @@ function activateVersion(version, deps) {
|
|
|
269
270
|
}
|
|
270
271
|
symlinkSync(oldTarget, previousPath);
|
|
271
272
|
}
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
273
|
+
// Swap CurrentVersion atomically: build the new link beside it and rename
|
|
274
|
+
// it over the old one, so a process killed mid-swap never leaves the
|
|
275
|
+
// machine without a CurrentVersion.
|
|
276
|
+
const tempPath = `${currentVersionPath}.next-${process.pid}`;
|
|
277
|
+
try {
|
|
278
|
+
unlinkSync(tempPath);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
// No leftover from an earlier interrupted swap.
|
|
282
|
+
}
|
|
283
|
+
symlinkSync(newTarget, tempPath);
|
|
284
|
+
try {
|
|
285
|
+
renameSync(tempPath, currentVersionPath);
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
// rename cannot replace a real directory; fall back to unlink + link.
|
|
289
|
+
try {
|
|
290
|
+
unlinkSync(tempPath);
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
// Already gone.
|
|
294
|
+
}
|
|
295
|
+
if (existsSync(currentVersionPath)) {
|
|
296
|
+
unlinkSync(currentVersionPath);
|
|
297
|
+
}
|
|
298
|
+
symlinkSync(newTarget, currentVersionPath);
|
|
275
299
|
}
|
|
276
|
-
symlinkSync(newTarget, currentVersionPath);
|
|
277
300
|
}
|
|
278
301
|
/**
|
|
279
302
|
* Compare two version strings of the form `0.1.0-alpha.{n}`. Returns
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.GITHUB_RELEASE_PUBLISHER = exports.GITHUB_DOWNLOAD_BASE = exports.GITHUB_RELEASES_API = exports.NPMJS_REGISTRY = exports.CLI_PACKAGE = void 0;
|
|
37
|
+
exports.selfUpdateE2eOverrides = selfUpdateE2eOverrides;
|
|
38
|
+
exports.githubTarballName = githubTarballName;
|
|
39
|
+
exports.lookupLatestRelease = lookupLatestRelease;
|
|
40
|
+
exports.installRelease = installRelease;
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
43
|
+
const ouro_version_manager_1 = require("./ouro-version-manager");
|
|
44
|
+
exports.CLI_PACKAGE = "@ouro.bot/cli";
|
|
45
|
+
exports.NPMJS_REGISTRY = "https://registry.npmjs.org/";
|
|
46
|
+
exports.GITHUB_RELEASES_API = "https://api.github.com/repos/ourostack/ouroboros/releases?per_page=20";
|
|
47
|
+
exports.GITHUB_DOWNLOAD_BASE = "https://github.com/ourostack/ouroboros/releases/download/";
|
|
48
|
+
/** Only releases and assets that our release workflow created are trusted. */
|
|
49
|
+
exports.GITHUB_RELEASE_PUBLISHER = "github-actions[bot]";
|
|
50
|
+
const LOOKUP_TIMEOUT_MS = 15_000;
|
|
51
|
+
const INSTALL_TIMEOUT_MS = 10 * 60_000;
|
|
52
|
+
/**
|
|
53
|
+
* The package end-to-end test serves a packed build as a GitHub release from
|
|
54
|
+
* a loopback HTTP server. Honored only with OURO_SELF_UPDATE_E2E=1 and a
|
|
55
|
+
* loopback base URL, so it cannot point a real machine at another host.
|
|
56
|
+
*/
|
|
57
|
+
function selfUpdateE2eOverrides(env) {
|
|
58
|
+
const base = env.OURO_SELF_UPDATE_E2E_GITHUB_BASE?.replace(/\/+$/, "");
|
|
59
|
+
if (env.OURO_SELF_UPDATE_E2E !== "1" || !base)
|
|
60
|
+
return null;
|
|
61
|
+
if (!/^http:\/\/(127\.0\.0\.1|localhost):\d+$/.test(base))
|
|
62
|
+
return null;
|
|
63
|
+
return { github: { apiUrl: `${base}/releases`, downloadBase: `${base}/releases/download/` }, githubOnly: true };
|
|
64
|
+
}
|
|
65
|
+
function githubTarballName(version) {
|
|
66
|
+
return `ouro.bot-cli-${version}.tgz`;
|
|
67
|
+
}
|
|
68
|
+
function message(error) {
|
|
69
|
+
return error instanceof Error ? error.message : String(error);
|
|
70
|
+
}
|
|
71
|
+
async function askNpmjs(deps) {
|
|
72
|
+
const body = await deps.fetchJson(`${exports.NPMJS_REGISTRY}${exports.CLI_PACKAGE.replace("/", "%2f")}`, LOOKUP_TIMEOUT_MS);
|
|
73
|
+
const version = body?.["dist-tags"]?.latest;
|
|
74
|
+
if (typeof version !== "string")
|
|
75
|
+
throw new Error("registry response has no latest dist-tag");
|
|
76
|
+
return { source: "npmjs", version };
|
|
77
|
+
}
|
|
78
|
+
async function askConfiguredRegistry(deps) {
|
|
79
|
+
const stdout = await deps.execFile("npm", ["view", exports.CLI_PACKAGE, "dist-tags.latest", "--json"], { timeoutMs: LOOKUP_TIMEOUT_MS });
|
|
80
|
+
const version = JSON.parse(stdout.trim());
|
|
81
|
+
if (typeof version !== "string")
|
|
82
|
+
throw new Error("npm view returned no latest dist-tag");
|
|
83
|
+
return { source: "npm-registry", version };
|
|
84
|
+
}
|
|
85
|
+
async function askGithub(deps) {
|
|
86
|
+
const github = deps.github ?? { apiUrl: exports.GITHUB_RELEASES_API, downloadBase: exports.GITHUB_DOWNLOAD_BASE };
|
|
87
|
+
const releases = await deps.fetchJson(github.apiUrl, LOOKUP_TIMEOUT_MS);
|
|
88
|
+
if (!Array.isArray(releases))
|
|
89
|
+
throw new Error("GitHub releases response is not a list");
|
|
90
|
+
let best = null;
|
|
91
|
+
for (const release of releases) {
|
|
92
|
+
if (release.draft === true || typeof release.tag_name !== "string")
|
|
93
|
+
continue;
|
|
94
|
+
const tag = /^v(\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?)$/.exec(release.tag_name);
|
|
95
|
+
if (!tag || release.author?.login !== exports.GITHUB_RELEASE_PUBLISHER)
|
|
96
|
+
continue;
|
|
97
|
+
const version = tag[1];
|
|
98
|
+
const name = githubTarballName(version);
|
|
99
|
+
const expectedUrl = `${github.downloadBase}v${version}/${name}`;
|
|
100
|
+
const asset = (release.assets ?? []).find((entry) => entry.name === name &&
|
|
101
|
+
entry.browser_download_url === expectedUrl &&
|
|
102
|
+
entry.uploader?.login === exports.GITHUB_RELEASE_PUBLISHER);
|
|
103
|
+
if (!asset)
|
|
104
|
+
continue;
|
|
105
|
+
if (!best || (0, ouro_version_manager_1.compareCliVersions)(version, best.version) > 0) {
|
|
106
|
+
best = { source: "github", version, tarballUrl: expectedUrl };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!best)
|
|
110
|
+
throw new Error(`no GitHub release published by ${exports.GITHUB_RELEASE_PUBLISHER} carries an npm tarball`);
|
|
111
|
+
return best;
|
|
112
|
+
}
|
|
113
|
+
async function lookupLatestRelease(deps) {
|
|
114
|
+
const asks = [
|
|
115
|
+
["npmjs", () => askNpmjs(deps)],
|
|
116
|
+
["npm-registry", () => askConfiguredRegistry(deps)],
|
|
117
|
+
["github", () => askGithub(deps)],
|
|
118
|
+
];
|
|
119
|
+
const answers = await Promise.all(asks.filter(([source]) => !deps.githubOnly || source === "github").map(async ([source, ask]) => {
|
|
120
|
+
try {
|
|
121
|
+
return await ask();
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
return { source, error: message(error) };
|
|
125
|
+
}
|
|
126
|
+
}));
|
|
127
|
+
let latestVersion = null;
|
|
128
|
+
for (const answer of answers) {
|
|
129
|
+
if (answer.version && (!latestVersion || (0, ouro_version_manager_1.compareCliVersions)(answer.version, latestVersion) > 0)) {
|
|
130
|
+
latestVersion = answer.version;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
(0, runtime_1.emitNervesEvent)({
|
|
134
|
+
component: "daemon",
|
|
135
|
+
event: "daemon.self_update_lookup",
|
|
136
|
+
message: "looked up the latest published CLI release",
|
|
137
|
+
meta: {
|
|
138
|
+
latestVersion,
|
|
139
|
+
answers: answers.map((answer) => `${answer.source}=${answer.version ?? `error: ${answer.error}`}`).join("; "),
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
return { latestVersion, answers };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Install `version` into `~/.ouro-cli/versions/<version>`, trying each source
|
|
146
|
+
* that can serve it. Returns the source that worked.
|
|
147
|
+
*/
|
|
148
|
+
async function installRelease(version, lookup, cliHome, deps) {
|
|
149
|
+
const prefix = path.join(cliHome, "versions", version);
|
|
150
|
+
const github = lookup.answers.find((answer) => answer.source === "github" && answer.version === version && answer.tarballUrl);
|
|
151
|
+
const attempts = [
|
|
152
|
+
["npm-registry", ["install", "--prefix", prefix, `${exports.CLI_PACKAGE}@${version}`]],
|
|
153
|
+
...(github ? [["github", ["install", "--prefix", prefix, github.tarballUrl]]] : []),
|
|
154
|
+
["npmjs", ["install", "--prefix", prefix, `${exports.CLI_PACKAGE}@${version}`, "--registry", exports.NPMJS_REGISTRY]],
|
|
155
|
+
];
|
|
156
|
+
const failures = [];
|
|
157
|
+
for (const [source, args] of attempts.filter(([source]) => !deps.githubOnly || source === "github")) {
|
|
158
|
+
try {
|
|
159
|
+
await deps.execFile("npm", args, { timeoutMs: INSTALL_TIMEOUT_MS });
|
|
160
|
+
(0, runtime_1.emitNervesEvent)({
|
|
161
|
+
component: "daemon",
|
|
162
|
+
event: "daemon.self_update_installed",
|
|
163
|
+
message: "installed CLI release for self-update",
|
|
164
|
+
meta: { version, source },
|
|
165
|
+
});
|
|
166
|
+
return source;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
failures.push(`${source}: ${message(error).split("\n")[0]}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
throw new Error(`could not install ${version} from any source (${failures.join("; ")})`);
|
|
173
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.releaseSourceDeps = void 0;
|
|
37
|
+
exports.isSelfUpdateLockHeld = isSelfUpdateLockHeld;
|
|
38
|
+
exports.createDefaultSelfUpdateDeps = createDefaultSelfUpdateDeps;
|
|
39
|
+
exports.ensureDefaultUpdaterAgent = ensureDefaultUpdaterAgent;
|
|
40
|
+
exports.isDefaultUpdaterAgentInstalled = isDefaultUpdaterAgentInstalled;
|
|
41
|
+
/* v8 ignore start -- production wiring for self-update: real network, npm, launchd and daemon socket; the logic it feeds is unit-tested through injected deps @preserve */
|
|
42
|
+
const child_process_1 = require("child_process");
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const os = __importStar(require("os"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
47
|
+
const cli_render_1 = require("../daemon/cli-render");
|
|
48
|
+
const socket_client_1 = require("../daemon/socket-client");
|
|
49
|
+
const ouro_version_manager_1 = require("./ouro-version-manager");
|
|
50
|
+
const release_sources_1 = require("./release-sources");
|
|
51
|
+
const update_state_1 = require("./update-state");
|
|
52
|
+
const updater_launchd_1 = require("./updater-launchd");
|
|
53
|
+
const version_intent_1 = require("./version-intent");
|
|
54
|
+
const DAEMON_UP_TIMEOUT_MS = 10 * 60_000;
|
|
55
|
+
const DAEMON_HEALTH_WAIT_MS = 90_000;
|
|
56
|
+
function execFileText(command, args, options) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
(0, child_process_1.execFile)(command, args, { timeout: options.timeoutMs, maxBuffer: 16 * 1024 * 1024, env: process.env }, (error, stdout, stderr) => {
|
|
59
|
+
if (error) {
|
|
60
|
+
const detail = String(stderr || "").trim().split("\n").slice(-3).join(" ");
|
|
61
|
+
reject(new Error(`${command} ${args[0] ?? ""} failed: ${detail || error.message}`));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
resolve(String(stdout));
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async function fetchJson(url, timeoutMs) {
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
71
|
+
try {
|
|
72
|
+
const response = await fetch(url, {
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
headers: { accept: "application/json", "user-agent": "ouro-self-update" },
|
|
75
|
+
});
|
|
76
|
+
if (!response.ok)
|
|
77
|
+
throw new Error(`HTTP ${response.status} from ${new URL(url).host}`);
|
|
78
|
+
return await response.json();
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
82
|
+
throw new Error(`timed out after ${Math.round(timeoutMs / 1000)}s reaching ${new URL(url).host}`);
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Run a command with stdin closed and OURO_UNATTENDED=1. `ouro up` can prompt
|
|
92
|
+
* (for example about manually cloned bundles); unattended, every prompt takes
|
|
93
|
+
* its default answer at once instead of waiting out the restart timeout.
|
|
94
|
+
*/
|
|
95
|
+
function runDetachedFromStdin(command, args, timeoutMs) {
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const child = (0, child_process_1.spawn)(command, args, { stdio: ["ignore", "ignore", "pipe"], env: { ...process.env, OURO_UNATTENDED: "1" } });
|
|
98
|
+
let stderr = "";
|
|
99
|
+
child.stderr?.on("data", (chunk) => { stderr = (stderr + String(chunk)).slice(-4000); });
|
|
100
|
+
const timer = setTimeout(() => {
|
|
101
|
+
child.kill("SIGTERM");
|
|
102
|
+
reject(new Error(`${command} ${args.join(" ")} timed out after ${Math.round(timeoutMs / 1000)}s`));
|
|
103
|
+
}, timeoutMs);
|
|
104
|
+
child.on("error", (error) => {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
reject(error);
|
|
107
|
+
});
|
|
108
|
+
child.on("close", (code) => {
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
if (code === 0)
|
|
111
|
+
resolve();
|
|
112
|
+
else
|
|
113
|
+
reject(new Error(`${args.join(" ")} exited ${code}: ${stderr.trim().split("\n").slice(-3).join(" ")}`));
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
exports.releaseSourceDeps = { fetchJson, execFile: execFileText, ...(0, release_sources_1.selfUpdateE2eOverrides)(process.env) };
|
|
118
|
+
function isPidAlive(pid) {
|
|
119
|
+
try {
|
|
120
|
+
process.kill(pid, 0);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function lockPath(cliHome) {
|
|
128
|
+
return path.join(cliHome, "self-update.lock");
|
|
129
|
+
}
|
|
130
|
+
/** The pid holding the update lock when that process is still alive. */
|
|
131
|
+
function liveLockOwner(cliHome) {
|
|
132
|
+
try {
|
|
133
|
+
const pid = Number.parseInt(fs.readFileSync(lockPath(cliHome), "utf8"), 10);
|
|
134
|
+
return Number.isFinite(pid) && pid > 0 && isPidAlive(pid) ? pid : null;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** True when another live process is in the middle of an update pass. */
|
|
141
|
+
function isSelfUpdateLockHeld(cliHome) {
|
|
142
|
+
const owner = liveLockOwner(cliHome);
|
|
143
|
+
return owner !== null && owner !== process.pid;
|
|
144
|
+
}
|
|
145
|
+
function acquireLock(cliHome) {
|
|
146
|
+
const file = lockPath(cliHome);
|
|
147
|
+
fs.mkdirSync(cliHome, { recursive: true });
|
|
148
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
149
|
+
try {
|
|
150
|
+
const fd = fs.openSync(file, "wx", 0o600);
|
|
151
|
+
fs.writeSync(fd, `${process.pid}\n`);
|
|
152
|
+
fs.closeSync(fd);
|
|
153
|
+
return () => {
|
|
154
|
+
try {
|
|
155
|
+
fs.unlinkSync(file);
|
|
156
|
+
}
|
|
157
|
+
catch { /* already released */ }
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (error.code !== "EEXIST")
|
|
162
|
+
throw error;
|
|
163
|
+
// A lock is stale only when its owner is gone; a long install by a live
|
|
164
|
+
// owner keeps it, however long it takes.
|
|
165
|
+
if (liveLockOwner(cliHome) !== null)
|
|
166
|
+
return null;
|
|
167
|
+
try {
|
|
168
|
+
fs.unlinkSync(file);
|
|
169
|
+
}
|
|
170
|
+
catch { /* raced with the owner releasing it */ }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
function versionEntry(cliHome, version) {
|
|
176
|
+
return path.join(cliHome, "versions", version, "node_modules", "@ouro.bot", "cli", "dist", "heart", "daemon", "ouro-entry.js");
|
|
177
|
+
}
|
|
178
|
+
async function runningDaemonVersion(socketPath) {
|
|
179
|
+
try {
|
|
180
|
+
const response = await (0, socket_client_1.sendDaemonCommand)(socketPath, { kind: "daemon.status" });
|
|
181
|
+
return (0, cli_render_1.parseStatusPayload)(response.data)?.overview.version ?? null;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function createDefaultSelfUpdateDeps(options = {}) {
|
|
188
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
189
|
+
const cliHome = (0, ouro_version_manager_1.getOuroCliHome)(homeDir);
|
|
190
|
+
// The package end-to-end test must never see a daemon on the host machine.
|
|
191
|
+
const socketPath = process.env.OURO_SELF_UPDATE_E2E === "1"
|
|
192
|
+
? path.join(cliHome, "e2e-no-daemon.sock")
|
|
193
|
+
: options.socketPath ?? socket_client_1.DEFAULT_DAEMON_SOCKET_PATH;
|
|
194
|
+
const launcher = path.join(cliHome, "bin", "ouro-launcher.js");
|
|
195
|
+
return {
|
|
196
|
+
now: () => Date.now(),
|
|
197
|
+
getCurrentVersion: () => (0, ouro_version_manager_1.getCurrentVersion)({ homeDir }),
|
|
198
|
+
readIntent: () => (0, version_intent_1.readVersionIntent)({ homeDir }),
|
|
199
|
+
writeIntent: (intent) => (0, version_intent_1.writeVersionIntent)(intent, { homeDir }),
|
|
200
|
+
readState: () => (0, update_state_1.readUpdateState)({ homeDir }),
|
|
201
|
+
writeState: (state) => (0, update_state_1.writeUpdateState)(state, { homeDir }),
|
|
202
|
+
lookupLatest: () => (0, release_sources_1.lookupLatestRelease)(exports.releaseSourceDeps),
|
|
203
|
+
install: (version, lookup) => (0, release_sources_1.installRelease)(version, lookup, cliHome, exports.releaseSourceDeps),
|
|
204
|
+
validate: (version) => (0, ouro_version_manager_1.validateInstalledVersionForActivation)(version, { homeDir }),
|
|
205
|
+
smokeTest: async (version) => {
|
|
206
|
+
try {
|
|
207
|
+
const output = (await execFileText(process.execPath, [versionEntry(cliHome, version), "--version"], { timeoutMs: 60_000 })).trim();
|
|
208
|
+
return output.includes(version) ? { ok: true } : { ok: false, detail: `--version printed "${output.slice(0, 120)}"` };
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
return { ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
activate: (version) => {
|
|
215
|
+
(0, ouro_version_manager_1.activateVersion)(version, { homeDir });
|
|
216
|
+
(0, ouro_version_manager_1.pruneOldVersions)(undefined, { homeDir });
|
|
217
|
+
},
|
|
218
|
+
isDaemonRunning: () => (0, socket_client_1.checkDaemonSocketAlive)(socketPath),
|
|
219
|
+
runningDaemonVersion: async () => (await (0, socket_client_1.checkDaemonSocketAlive)(socketPath)) ? runningDaemonVersion(socketPath) : null,
|
|
220
|
+
restartDaemon: async (expectedVersion) => {
|
|
221
|
+
try {
|
|
222
|
+
await runDetachedFromStdin(process.execPath, [launcher, "up"], DAEMON_UP_TIMEOUT_MS);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
return { ok: false, detail: error instanceof Error ? error.message : String(error) };
|
|
226
|
+
}
|
|
227
|
+
const deadline = Date.now() + DAEMON_HEALTH_WAIT_MS;
|
|
228
|
+
let seen = null;
|
|
229
|
+
while (Date.now() < deadline) {
|
|
230
|
+
seen = await runningDaemonVersion(socketPath);
|
|
231
|
+
if (seen === expectedVersion)
|
|
232
|
+
return { ok: true };
|
|
233
|
+
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
234
|
+
}
|
|
235
|
+
return { ok: false, detail: seen ? `daemon answered with ${seen}, expected ${expectedVersion}` : "daemon did not answer after ouro up" };
|
|
236
|
+
},
|
|
237
|
+
isDevMode: () => fs.existsSync(path.join(cliHome, "dev-config.json")),
|
|
238
|
+
acquireLock: () => acquireLock(cliHome),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function nodeMajorOf(nodePath) {
|
|
242
|
+
try {
|
|
243
|
+
fs.accessSync(nodePath, fs.constants.X_OK);
|
|
244
|
+
const version = (0, child_process_1.execFileSync)(nodePath, ["--version"], { encoding: "utf8", timeout: 10_000, stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
245
|
+
const major = Number.parseInt(version.replace(/^v/, ""), 10);
|
|
246
|
+
return Number.isFinite(major) ? major : null;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function ensureDefaultUpdaterAgent(options = {}) {
|
|
253
|
+
// The package end-to-end test runs in a throwaway HOME; it must never load
|
|
254
|
+
// a real launch agent on the host.
|
|
255
|
+
if (process.platform !== "darwin" || process.env.OURO_SELF_UPDATE_E2E === "1")
|
|
256
|
+
return null;
|
|
257
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
258
|
+
const result = (0, updater_launchd_1.ensureUpdaterAgent)({
|
|
259
|
+
homeDir,
|
|
260
|
+
userUid: os.userInfo().uid,
|
|
261
|
+
nodePath: (0, updater_launchd_1.chooseUpdaterNodePath)({
|
|
262
|
+
execPath: process.execPath,
|
|
263
|
+
runningMajor: Number.parseInt(process.versions.node, 10),
|
|
264
|
+
majorOf: nodeMajorOf,
|
|
265
|
+
}),
|
|
266
|
+
readFile: (filePath) => {
|
|
267
|
+
try {
|
|
268
|
+
return fs.readFileSync(filePath, "utf8");
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
writeFile: (filePath, content) => fs.writeFileSync(filePath, content, "utf8"),
|
|
275
|
+
mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
|
|
276
|
+
exec: (command) => { require("child_process").execSync(command, { stdio: "ignore" }); },
|
|
277
|
+
runningUnderUpdaterAgent: process.env.XPC_SERVICE_NAME === updater_launchd_1.UPDATER_PLIST_LABEL,
|
|
278
|
+
updateInProgress: isSelfUpdateLockHeld((0, ouro_version_manager_1.getOuroCliHome)(homeDir)),
|
|
279
|
+
});
|
|
280
|
+
(0, runtime_1.emitNervesEvent)({
|
|
281
|
+
component: "daemon",
|
|
282
|
+
event: "daemon.self_update_agent_checked",
|
|
283
|
+
message: "checked the unattended updater launch agent",
|
|
284
|
+
meta: { result },
|
|
285
|
+
});
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
function isDefaultUpdaterAgentInstalled(options = {}) {
|
|
289
|
+
if (process.platform !== "darwin")
|
|
290
|
+
return null;
|
|
291
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
292
|
+
return fs.existsSync(path.join(homeDir, "Library", "LaunchAgents", `${updater_launchd_1.UPDATER_PLIST_LABEL}.plist`));
|
|
293
|
+
}
|
|
294
|
+
/* v8 ignore stop */
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PIN_MAX_AGE_MS = void 0;
|
|
4
|
+
exports.runSelfUpdate = runSelfUpdate;
|
|
5
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
6
|
+
const ouro_version_manager_1 = require("./ouro-version-manager");
|
|
7
|
+
/**
|
|
8
|
+
* One unattended update pass. It never starts a stopped daemon, never leaves
|
|
9
|
+
* the machine on a version that failed its checks, and always records what
|
|
10
|
+
* happened in update-state.json so `ouro status` can report it.
|
|
11
|
+
*
|
|
12
|
+
* Steps:
|
|
13
|
+
* 1. Honor a human pin (`ouro rollback`) for PIN_MAX_AGE_MS, then expire it
|
|
14
|
+
* by rewriting the intent to "latest". A pin without a timestamp gets
|
|
15
|
+
* one the first time a pass sees it.
|
|
16
|
+
* 2. Ask every release source for the newest version. When nothing is newer
|
|
17
|
+
* but a running daemon still reports an older version (a pass was killed
|
|
18
|
+
* between activation and restart), restart it on CurrentVersion.
|
|
19
|
+
* 3. Install it, validate the payload and run its `--version` smoke test.
|
|
20
|
+
* 4. Activate it. If the daemon was running, restart it on the new version
|
|
21
|
+
* and check that it answers; otherwise leave the daemon stopped.
|
|
22
|
+
* 5. On a failed check, reactivate the previous version, pin it with reason
|
|
23
|
+
* "auto-rollback", restart the daemon on it, and never retry the failed
|
|
24
|
+
* version.
|
|
25
|
+
*/
|
|
26
|
+
exports.PIN_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
27
|
+
function errorText(error) {
|
|
28
|
+
return error instanceof Error ? error.message : String(error);
|
|
29
|
+
}
|
|
30
|
+
const FAILURE_OUTCOMES = ["unreachable", "install-failed", "rejected", "rolled-back"];
|
|
31
|
+
async function runSelfUpdate(deps) {
|
|
32
|
+
const release = deps.acquireLock();
|
|
33
|
+
if (!release) {
|
|
34
|
+
return { outcome: "busy", summary: "another ouro self-update is already running" };
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
return await runLocked(deps);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
release();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function runLocked(deps) {
|
|
44
|
+
const now = deps.now();
|
|
45
|
+
const previous = deps.readState();
|
|
46
|
+
const failedVersions = [...(previous?.failedVersions ?? [])];
|
|
47
|
+
const currentVersion = deps.getCurrentVersion();
|
|
48
|
+
const finish = (outcome, fields) => {
|
|
49
|
+
const { summary, ...rest } = fields;
|
|
50
|
+
const state = {
|
|
51
|
+
schemaVersion: 1,
|
|
52
|
+
lastRunAt: new Date(now).toISOString(),
|
|
53
|
+
outcome,
|
|
54
|
+
currentVersion: deps.getCurrentVersion(),
|
|
55
|
+
failedVersions,
|
|
56
|
+
consecutiveFailures: FAILURE_OUTCOMES.includes(outcome) ? (previous?.consecutiveFailures ?? 0) + 1 : 0,
|
|
57
|
+
...rest,
|
|
58
|
+
detail: rest.detail ?? summary,
|
|
59
|
+
};
|
|
60
|
+
deps.writeState(state);
|
|
61
|
+
(0, runtime_1.emitNervesEvent)({
|
|
62
|
+
level: FAILURE_OUTCOMES.includes(outcome) ? "warn" : "info",
|
|
63
|
+
component: "daemon",
|
|
64
|
+
event: "daemon.self_update_result",
|
|
65
|
+
message: summary,
|
|
66
|
+
meta: { outcome, currentVersion: state.currentVersion, latestVersion: state.latestVersion ?? null },
|
|
67
|
+
});
|
|
68
|
+
return { outcome, summary, state };
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Restart the daemon on `target`. On a failed health check, never retry
|
|
72
|
+
* `target`, pin and reactivate `fallback`, and restart on it. Returns the
|
|
73
|
+
* rolled-back result, or null when `target` came up healthy.
|
|
74
|
+
*/
|
|
75
|
+
const restartOrRollBack = async (target, fallback, source) => {
|
|
76
|
+
const health = await deps.restartDaemon(target);
|
|
77
|
+
if (health.ok)
|
|
78
|
+
return null;
|
|
79
|
+
failedVersions.push(target);
|
|
80
|
+
if (!fallback) {
|
|
81
|
+
return finish("rolled-back", {
|
|
82
|
+
summary: `${target} did not come up healthy (${health.detail ?? "no detail"}) and there is no previous version to return to`,
|
|
83
|
+
latestVersion: target,
|
|
84
|
+
source,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
deps.writeIntent({
|
|
88
|
+
schemaVersion: 1,
|
|
89
|
+
mode: "pinned",
|
|
90
|
+
targetVersion: fallback,
|
|
91
|
+
pinnedAt: new Date(now).toISOString(),
|
|
92
|
+
reason: "auto-rollback",
|
|
93
|
+
});
|
|
94
|
+
deps.activate(fallback);
|
|
95
|
+
const restored = await deps.restartDaemon(fallback);
|
|
96
|
+
return finish("rolled-back", {
|
|
97
|
+
summary: `${target} did not come up healthy (${health.detail ?? "no detail"}); rolled back to ${fallback}${restored.ok ? "" : ` but the daemon did not answer on it either (${restored.detail ?? "no detail"})`}`,
|
|
98
|
+
latestVersion: target,
|
|
99
|
+
source,
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
// 1. Pins.
|
|
103
|
+
const intent = deps.readIntent();
|
|
104
|
+
if (intent?.mode === "pinned" && intent.reason !== "auto-rollback") {
|
|
105
|
+
let pinnedAt = intent.pinnedAt;
|
|
106
|
+
if (!pinnedAt || Number.isNaN(Date.parse(pinnedAt))) {
|
|
107
|
+
// A pin written before pins carried a timestamp: start its clock now and
|
|
108
|
+
// record it in the intent itself, so the pin cannot outlive its expiry.
|
|
109
|
+
pinnedAt = new Date(now).toISOString();
|
|
110
|
+
deps.writeIntent({ ...intent, pinnedAt });
|
|
111
|
+
}
|
|
112
|
+
const expiresAtMs = Date.parse(pinnedAt) + exports.PIN_MAX_AGE_MS;
|
|
113
|
+
if (now < expiresAtMs) {
|
|
114
|
+
const until = new Date(expiresAtMs).toISOString();
|
|
115
|
+
return finish("pinned", {
|
|
116
|
+
summary: `pinned to ${intent.targetVersion} by ouro rollback; unattended updates resume after ${until} (run \`ouro up --latest\` to resume now)`,
|
|
117
|
+
detail: `pin honored until ${until}`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
(0, runtime_1.emitNervesEvent)({
|
|
121
|
+
level: "warn",
|
|
122
|
+
component: "daemon",
|
|
123
|
+
event: "daemon.self_update_pin_expired",
|
|
124
|
+
message: "a rollback pin older than the maximum age no longer blocks updates",
|
|
125
|
+
meta: { targetVersion: intent.targetVersion, pinnedAt },
|
|
126
|
+
});
|
|
127
|
+
deps.writeIntent({ schemaVersion: 1, mode: "latest", targetVersion: intent.targetVersion });
|
|
128
|
+
}
|
|
129
|
+
// 2. Lookup.
|
|
130
|
+
const lookup = await deps.lookupLatest();
|
|
131
|
+
const sourceSummary = lookup.answers.map((answer) => `${answer.source}: ${answer.version ?? answer.error}`).join("; ");
|
|
132
|
+
const latest = lookup.latestVersion;
|
|
133
|
+
if (!latest) {
|
|
134
|
+
return finish("unreachable", { summary: `no release source answered (${sourceSummary})` });
|
|
135
|
+
}
|
|
136
|
+
const lookupSource = lookup.answers.find((answer) => answer.version === latest)?.source;
|
|
137
|
+
if (currentVersion && (0, ouro_version_manager_1.compareCliVersions)(latest, currentVersion) <= 0) {
|
|
138
|
+
// A pass killed between activation and restart leaves the old daemon
|
|
139
|
+
// running; finish the job instead of reporting "current" forever.
|
|
140
|
+
const daemonVersion = deps.isDevMode() ? null : await deps.runningDaemonVersion();
|
|
141
|
+
if (daemonVersion && daemonVersion !== currentVersion) {
|
|
142
|
+
const restart = await restartOrRollBack(currentVersion, daemonVersion);
|
|
143
|
+
if (restart)
|
|
144
|
+
return restart;
|
|
145
|
+
return finish("updated", {
|
|
146
|
+
summary: `daemon was still running ${daemonVersion}; restarted it on ${currentVersion} and it is healthy`,
|
|
147
|
+
latestVersion: latest,
|
|
148
|
+
source: lookupSource,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const failedSources = lookup.answers.filter((answer) => !answer.version).map((answer) => `${answer.source}: ${answer.error}`);
|
|
152
|
+
return finish("current", {
|
|
153
|
+
summary: `current at ${currentVersion}${failedSources.length ? ` (unreachable: ${failedSources.join("; ")})` : ""}`,
|
|
154
|
+
latestVersion: latest,
|
|
155
|
+
source: lookupSource,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (failedVersions.includes(latest)) {
|
|
159
|
+
return finish("rejected", {
|
|
160
|
+
summary: `latest ${latest} failed its checks on this machine earlier; staying on ${currentVersion} until a newer release ships`,
|
|
161
|
+
latestVersion: latest,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// 3. Install, validate, smoke test.
|
|
165
|
+
let installSource;
|
|
166
|
+
try {
|
|
167
|
+
installSource = await deps.install(latest, lookup);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
return finish("install-failed", { summary: errorText(error), latestVersion: latest });
|
|
171
|
+
}
|
|
172
|
+
const validation = deps.validate(latest);
|
|
173
|
+
const smoke = validation.ok ? await deps.smokeTest(latest) : { ok: false, detail: validation.message };
|
|
174
|
+
if (!smoke.ok) {
|
|
175
|
+
failedVersions.push(latest);
|
|
176
|
+
return finish("rejected", {
|
|
177
|
+
summary: `${latest} failed its pre-activation check: ${smoke.detail ?? "no detail"}; staying on ${currentVersion}`,
|
|
178
|
+
latestVersion: latest,
|
|
179
|
+
source: installSource,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
// 4. Activate, then restart a running daemon on the new version.
|
|
183
|
+
const daemonWasRunning = !deps.isDevMode() && await deps.isDaemonRunning();
|
|
184
|
+
deps.writeIntent({ schemaVersion: 1, mode: "latest", targetVersion: latest });
|
|
185
|
+
deps.activate(latest);
|
|
186
|
+
if (daemonWasRunning) {
|
|
187
|
+
const restart = await restartOrRollBack(latest, currentVersion, installSource);
|
|
188
|
+
if (restart)
|
|
189
|
+
return restart;
|
|
190
|
+
}
|
|
191
|
+
return finish("updated", {
|
|
192
|
+
summary: `updated ${currentVersion ?? "unknown"} -> ${latest} via ${installSource}${daemonWasRunning ? "; daemon restarted and healthy" : "; daemon was not running and was left stopped"}`,
|
|
193
|
+
latestVersion: latest,
|
|
194
|
+
source: installSource,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.UPDATER_INTERVAL_SECONDS = void 0;
|
|
37
|
+
exports.updateStatePath = updateStatePath;
|
|
38
|
+
exports.readUpdateState = readUpdateState;
|
|
39
|
+
exports.writeUpdateState = writeUpdateState;
|
|
40
|
+
exports.formatUpdateStatusLine = formatUpdateStatusLine;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const os = __importStar(require("os"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
45
|
+
exports.UPDATER_INTERVAL_SECONDS = 60 * 60;
|
|
46
|
+
function updateStatePath(homeDir = os.homedir()) {
|
|
47
|
+
return path.join(homeDir, ".ouro-cli", "update-state.json");
|
|
48
|
+
}
|
|
49
|
+
function readUpdateState(deps = {}) {
|
|
50
|
+
const readFileSync = deps.readFileSync ?? fs.readFileSync;
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(readFileSync(updateStatePath(deps.homeDir), "utf8"));
|
|
53
|
+
if (parsed?.schemaVersion !== 1 || typeof parsed.lastRunAt !== "string" || typeof parsed.outcome !== "string")
|
|
54
|
+
return null;
|
|
55
|
+
return {
|
|
56
|
+
...parsed,
|
|
57
|
+
failedVersions: Array.isArray(parsed.failedVersions) ? parsed.failedVersions.filter((v) => typeof v === "string") : [],
|
|
58
|
+
consecutiveFailures: typeof parsed.consecutiveFailures === "number" ? parsed.consecutiveFailures : 0,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function writeUpdateState(state, deps = {}) {
|
|
66
|
+
const writeFileSync = deps.writeFileSync ?? fs.writeFileSync;
|
|
67
|
+
const renameSync = deps.renameSync ?? fs.renameSync;
|
|
68
|
+
const mkdirSync = deps.mkdirSync ?? fs.mkdirSync;
|
|
69
|
+
const destination = updateStatePath(deps.homeDir);
|
|
70
|
+
mkdirSync(path.dirname(destination), { recursive: true });
|
|
71
|
+
const temporary = `${destination}.${process.pid}.tmp`;
|
|
72
|
+
writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
73
|
+
renameSync(temporary, destination);
|
|
74
|
+
(0, runtime_1.emitNervesEvent)({
|
|
75
|
+
level: ["updated", "current", "pinned"].includes(state.outcome) ? "info" : "warn",
|
|
76
|
+
component: "daemon",
|
|
77
|
+
event: "daemon.self_update_state_written",
|
|
78
|
+
message: "recorded the unattended updater outcome",
|
|
79
|
+
meta: { outcome: state.outcome, currentVersion: state.currentVersion, latestVersion: state.latestVersion ?? null, detail: state.detail ?? null },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function formatAge(ms) {
|
|
83
|
+
const minutes = Math.max(0, Math.round(ms / 60_000));
|
|
84
|
+
if (minutes < 60)
|
|
85
|
+
return `${minutes}m ago`;
|
|
86
|
+
const hours = Math.round(minutes / 60);
|
|
87
|
+
if (hours < 48)
|
|
88
|
+
return `${hours}h ago`;
|
|
89
|
+
return `${Math.round(hours / 24)}d ago`;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* One line for `ouro status`. Problems lead with "needs attention" so a
|
|
93
|
+
* stuck machine is visible at a glance.
|
|
94
|
+
*/
|
|
95
|
+
function formatUpdateStatusLine(state, options) {
|
|
96
|
+
if (!state) {
|
|
97
|
+
return options.updaterInstalled === false
|
|
98
|
+
? "Updates: needs attention: the unattended updater is not installed; run `ouro self-update` once"
|
|
99
|
+
: "Updates: no update check recorded yet; run `ouro self-update`";
|
|
100
|
+
}
|
|
101
|
+
const ageMs = options.now - Date.parse(state.lastRunAt);
|
|
102
|
+
const age = formatAge(ageMs);
|
|
103
|
+
const stale = ageMs > exports.UPDATER_INTERVAL_SECONDS * 1000 * 3;
|
|
104
|
+
const version = state.currentVersion ?? "unknown version";
|
|
105
|
+
let line;
|
|
106
|
+
switch (state.outcome) {
|
|
107
|
+
case "updated":
|
|
108
|
+
line = `Updates: updated to ${version} ${age}${state.source ? ` via ${state.source}` : ""}`;
|
|
109
|
+
break;
|
|
110
|
+
case "current":
|
|
111
|
+
line = `Updates: current at ${version} (checked ${age}${state.source ? ` via ${state.source}` : ""})`;
|
|
112
|
+
break;
|
|
113
|
+
case "pinned":
|
|
114
|
+
line = `Updates: pinned to ${version} (checked ${age}; ${state.detail ?? "pin honored"})`;
|
|
115
|
+
break;
|
|
116
|
+
default:
|
|
117
|
+
line = `Updates: needs attention: ${state.outcome} ${age} on ${version}${state.latestVersion ? ` (latest ${state.latestVersion})` : ""}: ${state.detail ?? "no detail"}`;
|
|
118
|
+
}
|
|
119
|
+
if (stale) {
|
|
120
|
+
line += `; needs attention: the updater has not run since ${state.lastRunAt}`;
|
|
121
|
+
}
|
|
122
|
+
if (options.updaterInstalled === false) {
|
|
123
|
+
line += "; needs attention: the unattended updater is not installed; run `ouro self-update` once";
|
|
124
|
+
}
|
|
125
|
+
return line;
|
|
126
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.STABLE_NODE_PATHS = exports.UPDATER_PLIST_LABEL = void 0;
|
|
37
|
+
exports.chooseUpdaterNodePath = chooseUpdaterNodePath;
|
|
38
|
+
exports.updaterPlistPath = updaterPlistPath;
|
|
39
|
+
exports.generateUpdaterPlist = generateUpdaterPlist;
|
|
40
|
+
exports.ensureUpdaterAgent = ensureUpdaterAgent;
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
43
|
+
const update_state_1 = require("./update-state");
|
|
44
|
+
/**
|
|
45
|
+
* A launchd agent that runs `ouro self-update --unattended` every hour and at
|
|
46
|
+
* login, independent of the Ouro daemon. Updates keep flowing when the
|
|
47
|
+
* daemon is stopped, crashed, or was never started on this machine.
|
|
48
|
+
*/
|
|
49
|
+
exports.UPDATER_PLIST_LABEL = "bot.ouro.updater";
|
|
50
|
+
/** Node binaries that survive nvm switches and Node upgrades, in preference order. */
|
|
51
|
+
exports.STABLE_NODE_PATHS = ["/opt/homebrew/bin/node", "/usr/local/bin/node"];
|
|
52
|
+
/**
|
|
53
|
+
* Pick the node binary the updater agent runs. A stable path (Homebrew) is
|
|
54
|
+
* preferred when it exists and runs the same major Node version as this
|
|
55
|
+
* process, so the plist does not churn every time another nvm node runs
|
|
56
|
+
* `ouro up`; otherwise the running node is used.
|
|
57
|
+
*/
|
|
58
|
+
function chooseUpdaterNodePath(options) {
|
|
59
|
+
for (const candidate of options.candidates ?? exports.STABLE_NODE_PATHS) {
|
|
60
|
+
if (options.majorOf(candidate) === options.runningMajor)
|
|
61
|
+
return candidate;
|
|
62
|
+
}
|
|
63
|
+
return options.execPath;
|
|
64
|
+
}
|
|
65
|
+
function updaterPlistPath(homeDir) {
|
|
66
|
+
return path.join(homeDir, "Library", "LaunchAgents", `${exports.UPDATER_PLIST_LABEL}.plist`);
|
|
67
|
+
}
|
|
68
|
+
function xmlEscape(value) {
|
|
69
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
70
|
+
}
|
|
71
|
+
function generateUpdaterPlist(options) {
|
|
72
|
+
const launcher = path.join(options.homeDir, ".ouro-cli", "bin", "ouro-launcher.js");
|
|
73
|
+
const envPath = [path.dirname(options.nodePath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"].join(":");
|
|
74
|
+
return [
|
|
75
|
+
`<?xml version="1.0" encoding="UTF-8"?>`,
|
|
76
|
+
`<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">`,
|
|
77
|
+
`<plist version="1.0">`,
|
|
78
|
+
`<dict>`,
|
|
79
|
+
` <key>Label</key>`,
|
|
80
|
+
` <string>${exports.UPDATER_PLIST_LABEL}</string>`,
|
|
81
|
+
` <key>ProgramArguments</key>`,
|
|
82
|
+
` <array>`,
|
|
83
|
+
` <string>${xmlEscape(options.nodePath)}</string>`,
|
|
84
|
+
` <string>${xmlEscape(launcher)}</string>`,
|
|
85
|
+
` <string>self-update</string>`,
|
|
86
|
+
` <string>--unattended</string>`,
|
|
87
|
+
` </array>`,
|
|
88
|
+
` <key>RunAtLoad</key>`,
|
|
89
|
+
` <true/>`,
|
|
90
|
+
` <key>StartInterval</key>`,
|
|
91
|
+
` <integer>${update_state_1.UPDATER_INTERVAL_SECONDS}</integer>`,
|
|
92
|
+
` <key>ProcessType</key>`,
|
|
93
|
+
` <string>Background</string>`,
|
|
94
|
+
` <key>EnvironmentVariables</key>`,
|
|
95
|
+
` <dict>`,
|
|
96
|
+
` <key>PATH</key>`,
|
|
97
|
+
` <string>${xmlEscape(envPath)}</string>`,
|
|
98
|
+
` </dict>`,
|
|
99
|
+
`</dict>`,
|
|
100
|
+
`</plist>`,
|
|
101
|
+
``,
|
|
102
|
+
].join("\n");
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Write the updater plist when it is missing or stale (for example after the
|
|
106
|
+
* node binary moved) and load it. When the updater agent itself is running,
|
|
107
|
+
* or another process holds the update lock, reloading would kill the run in
|
|
108
|
+
* progress, so it only rewrites the file and the next interactive
|
|
109
|
+
* `ouro self-update` or `ouro up` reloads it.
|
|
110
|
+
*/
|
|
111
|
+
function ensureUpdaterAgent(deps) {
|
|
112
|
+
const plistPath = updaterPlistPath(deps.homeDir);
|
|
113
|
+
const desired = generateUpdaterPlist({ homeDir: deps.homeDir, nodePath: deps.nodePath });
|
|
114
|
+
const existing = deps.readFile(plistPath);
|
|
115
|
+
const domain = `gui/${deps.userUid}`;
|
|
116
|
+
let loaded = true;
|
|
117
|
+
try {
|
|
118
|
+
deps.exec(`launchctl print ${domain}/${exports.UPDATER_PLIST_LABEL}`);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
loaded = false;
|
|
122
|
+
}
|
|
123
|
+
if (existing === desired && loaded)
|
|
124
|
+
return "unchanged";
|
|
125
|
+
deps.mkdirp(path.dirname(plistPath));
|
|
126
|
+
deps.writeFile(plistPath, desired);
|
|
127
|
+
if (deps.runningUnderUpdaterAgent || deps.updateInProgress) {
|
|
128
|
+
(0, runtime_1.emitNervesEvent)({
|
|
129
|
+
component: "daemon",
|
|
130
|
+
event: "daemon.self_update_agent_written",
|
|
131
|
+
message: "rewrote the updater launch agent; reload waits for the next interactive run",
|
|
132
|
+
meta: { plistPath, updateInProgress: deps.updateInProgress },
|
|
133
|
+
});
|
|
134
|
+
return "written-reload-pending";
|
|
135
|
+
}
|
|
136
|
+
if (loaded) {
|
|
137
|
+
try {
|
|
138
|
+
deps.exec(`launchctl bootout ${domain}/${exports.UPDATER_PLIST_LABEL}`);
|
|
139
|
+
}
|
|
140
|
+
catch { /* already gone */ }
|
|
141
|
+
}
|
|
142
|
+
deps.exec(`launchctl bootstrap ${domain} "${plistPath}"`);
|
|
143
|
+
(0, runtime_1.emitNervesEvent)({
|
|
144
|
+
component: "daemon",
|
|
145
|
+
event: "daemon.self_update_agent_installed",
|
|
146
|
+
message: "installed the unattended updater launch agent",
|
|
147
|
+
meta: { plistPath, intervalSeconds: update_state_1.UPDATER_INTERVAL_SECONDS },
|
|
148
|
+
});
|
|
149
|
+
return "installed";
|
|
150
|
+
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.905",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.905",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.905",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=22"
|
|
6
6
|
},
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"test": "vitest run",
|
|
37
37
|
"test:integration": "npm run build && vitest run --config vitest.integration.config.ts",
|
|
38
38
|
"test:e2e:package": "npm run build && node scripts/package-e2e.cjs",
|
|
39
|
+
"test:e2e:self-update": "node scripts/self-update-package-e2e.cjs",
|
|
39
40
|
"test:e2e:ouro-bot-package": "node scripts/ouro-bot-package-e2e.cjs",
|
|
40
41
|
"test:e2e:real-smoke": "npm run build && node scripts/nightly-real-smoke.cjs",
|
|
41
42
|
"typecheck:mailbox-ui": "tsc --noEmit -p packages/mailbox-ui/tsconfig.json",
|