@ours.network/install 0.17.0-nightly.1 → 0.17.0-nightly.2
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/README.md +49 -3
- package/install.mjs +92 -11
- package/lib/logic.mjs +94 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,8 +64,8 @@ dependency on the things it installs): an ASCII banner, tasteful colour (degrade
|
|
|
64
64
|
prints the exact manual commands and continues — it **never dead-ends**.
|
|
65
65
|
- **3/4 ours-fleet** — makes your harnesses persistent, always-online agent teams that survive
|
|
66
66
|
a reboot; runs `ours-fleet init`. Default **Yes**.
|
|
67
|
-
- **4/4 Telegram connector** — install-only (no bot tokens here),
|
|
68
|
-
boot service.
|
|
67
|
+
- **4/4 Telegram connector** — install-only (no bot tokens here), **pointed at the daemon
|
|
68
|
+
step 1 just built**, then optionally as a boot service.
|
|
69
69
|
4. **Summary + hand-off** — a recap (skipped/failed rows call out the fix), then a **literal
|
|
70
70
|
copy-paste prompt** (root identity + fleet + Telegram) with the steps for any skipped/failed
|
|
71
71
|
component dropped out. Copied to the clipboard where supported.
|
|
@@ -74,6 +74,48 @@ The human identity is created idempotently after the daemon becomes reachable. B
|
|
|
74
74
|
`curl … | bash` gives the script its input over the pipe, every prompt is read from the
|
|
75
75
|
controlling terminal (`/dev/tty`), so the flow still works piped.
|
|
76
76
|
|
|
77
|
+
## One daemon, and everything pointed at it
|
|
78
|
+
|
|
79
|
+
A clean deployment installs, configures and starts **one** ours daemon first, then wires every
|
|
80
|
+
client to that same daemon. What that takes differs per client:
|
|
81
|
+
|
|
82
|
+
- **The harness plugins** need nothing extra: each is `ours-mcp proxy`, which reads the daemon's
|
|
83
|
+
own config (`OURS_CONFIG`, else `~/.ours/config.json`). `autoStart` is off by default, so a proxy
|
|
84
|
+
whose daemon is down reports it rather than quietly starting a second one.
|
|
85
|
+
- **The Telegram connector keeps its own config file** and never inherits the daemon's, so the
|
|
86
|
+
installer writes `~/.ours-telegram/config.json` (honouring `OURS_TG_CONFIG`) **before** the
|
|
87
|
+
connector is started or installed as a service — `install-service` bakes whatever it resolves
|
|
88
|
+
into the service unit as environment variables, and those outrank the file from then on.
|
|
89
|
+
Three keys are written, so whichever connector generation is installed finds what it reads:
|
|
90
|
+
- `daemonUrl` + `daemonStateDir` — for `>=0.3.3-nightly.1`, which attaches to the running daemon
|
|
91
|
+
over `/api/v1`. **Both** are required: with neither, its SDK never reads `~/.ours/config.json`
|
|
92
|
+
and falls back to the built-in `127.0.0.1:3050`, missing a daemon on any other port; with the
|
|
93
|
+
endpoint alone it refuses outright (`INCOHERENT_SELECTION` — the daemon's API token belongs to
|
|
94
|
+
a state directory, so selecting an endpoint without one would disclose that token).
|
|
95
|
+
- `brokerUrl` — for `<=0.3.2`, which hosts its own ADAPT wrapper and meets the daemon at a broker
|
|
96
|
+
instead. It must match the daemon's or the two can never see each other.
|
|
97
|
+
|
|
98
|
+
A re-run that changes nothing rewrites nothing, and keys the installer does not own are preserved.
|
|
99
|
+
|
|
100
|
+
If `ours-mcp install-service` fails (no systemd user bus, no linger, a container, WSL without
|
|
101
|
+
systemd) it has already **stopped** the daemon it was about to supervise. The installer restarts it
|
|
102
|
+
and says plainly that the boot service is missing — a clean deployment never ends with no daemon
|
|
103
|
+
while the summary claims success.
|
|
104
|
+
|
|
105
|
+
## Release channel
|
|
106
|
+
|
|
107
|
+
`OURS_CHANNEL=nightly` (or `OURS_INSTALL_CHANNEL`) installs the `nightly` dist-tag for the packages
|
|
108
|
+
that publish one — `mcp`, `tg-connector`, and the `claude-code` / `codex` / `hermes` launchers —
|
|
109
|
+
while `@ours.network/fleet` stays `@latest` always (it publishes no nightly).
|
|
110
|
+
|
|
111
|
+
**With no explicit selection the installer follows its own version.** A published nightly build
|
|
112
|
+
carries the `-nightly.N` suffix the release bump stamps, so `npm i -g @ours.network/install@nightly`
|
|
113
|
+
builds a nightly stack, and a stable installer can never consume a nightly. This is load-bearing
|
|
114
|
+
rather than cosmetic: across `tg-connector` 0.3.2 → 0.3.3-nightly.1 the connector stopped hosting
|
|
115
|
+
its own ADAPT wrapper and became a client of the shared daemon, so mixing tags across that boundary
|
|
116
|
+
pairs a connector that needs `/api/v1` with a daemon that does not serve it. `OURS_CHANNEL` still
|
|
117
|
+
overrides in both directions.
|
|
118
|
+
|
|
77
119
|
## Non-interactive / CI / safe dry-run
|
|
78
120
|
|
|
79
121
|
```sh
|
|
@@ -96,6 +138,9 @@ is reported and left unchanged.
|
|
|
96
138
|
| `OURS_INSTALL_DRY_RUN` | walk the flow without installing or changing anything |
|
|
97
139
|
| `OURS_NPM` | npm binary to use (default `npm`) |
|
|
98
140
|
| `OURS_CONFIG` | daemon config file location (default `~/.ours/config.json`) |
|
|
141
|
+
| `OURS_STATE_DIR` | daemon state directory (default `~/.ours`) — also what the Telegram connector is told to expect |
|
|
142
|
+
| `OURS_TG_CONFIG` | Telegram connector config file location (default `~/.ours-telegram/config.json`) |
|
|
143
|
+
| `OURS_CHANNEL` | `nightly` or `latest`; unset follows the installer's own version |
|
|
99
144
|
|
|
100
145
|
## Uninstall
|
|
101
146
|
|
|
@@ -142,5 +187,6 @@ OURS_UNINSTALL_DAEMON=yes \
|
|
|
142
187
|
published components.
|
|
143
188
|
- **Idempotent + safe to re-run.** A re-run adds a skipped piece, re-points the plugins, or (only
|
|
144
189
|
when you say yes) updates a component; an already-current daemon is left untouched, its running
|
|
145
|
-
port and complete voice setup are reused everywhere
|
|
190
|
+
port and complete voice setup are reused everywhere — and the Telegram connector's daemon
|
|
191
|
+
selection is rewritten only if it actually changed. Bot tokens and fleet roles remain in the
|
|
146
192
|
copy-paste hand-off; provider keys never enter that prompt or agent chat.
|
package/install.mjs
CHANGED
|
@@ -20,21 +20,24 @@
|
|
|
20
20
|
import { spawn, spawnSync } from 'node:child_process';
|
|
21
21
|
import { readFileSync, existsSync } from 'node:fs';
|
|
22
22
|
import { homedir, userInfo, platform as osPlatform, release as osRelease } from 'node:os';
|
|
23
|
-
import { join } from 'node:path';
|
|
23
|
+
import { join, resolve } from 'node:path';
|
|
24
24
|
import { banner, heading, ok, info, warn, c, box, withSpinner, openTty, makeWriter, closeSync } from './lib/ui.mjs';
|
|
25
25
|
import { askLine, askYesNo, isCancel } from './lib/prompt.mjs';
|
|
26
26
|
import {
|
|
27
27
|
suggestPort, parsePort, validateBroker, mergeConfig, parseVersion, parseStatus,
|
|
28
28
|
detectPlatform, classifyHarnessProbe, buildHandoffPrompt,
|
|
29
|
-
voiceSetupStatus,
|
|
29
|
+
voiceSetupStatus, resolveSharedBroker, tgConfigPath, planTgDaemonConfig, daemonEndpoint,
|
|
30
30
|
DEFAULT_PORT, resolveChannel, pkgSpec,
|
|
31
31
|
} from './lib/logic.mjs';
|
|
32
32
|
import { atomicWriteConfig } from './lib/config.mjs';
|
|
33
33
|
|
|
34
34
|
const NPM = process.env.OURS_NPM || 'npm';
|
|
35
35
|
// Release channel: OURS_CHANNEL=nightly installs @nightly for mcp/tg-connector/plugin
|
|
36
|
-
// launchers but keeps @ours.network/fleet at @latest (fleet has no nightly).
|
|
37
|
-
|
|
36
|
+
// launchers but keeps @ours.network/fleet at @latest (fleet has no nightly). With no
|
|
37
|
+
// explicit selection the installer follows its OWN channel, so a nightly installer
|
|
38
|
+
// builds a nightly stack instead of silently mixing tags across the connector's
|
|
39
|
+
// architecture boundary (see resolveChannel).
|
|
40
|
+
const CHANNEL = resolveChannel(process.env.OURS_CHANNEL || process.env.OURS_INSTALL_CHANNEL, pkgVersion());
|
|
38
41
|
const spec = (pkgKey) => pkgSpec(pkgKey, CHANNEL); // → "@ours.network/<key>@<tag>"
|
|
39
42
|
let DRY = !!process.env.OURS_INSTALL_DRY_RUN;
|
|
40
43
|
const SELFHOST_URL = 'ours.network';
|
|
@@ -109,6 +112,18 @@ function writeConfigPatch(patch) {
|
|
|
109
112
|
return p;
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
// The daemon's state directory, resolved exactly the way packages/core/src/config.ts
|
|
116
|
+
// resolves it (env > config file > ~/.ours). The Telegram connector needs the ABSOLUTE
|
|
117
|
+
// path: the SDK reads the daemon's API token from it and refuses to send that token to
|
|
118
|
+
// a separately-chosen endpoint unless the state dir was chosen just as deliberately.
|
|
119
|
+
function daemonStateDir() {
|
|
120
|
+
const fromEnv = process.env.OURS_STATE_DIR?.trim();
|
|
121
|
+
if (fromEnv) return resolve(fromEnv);
|
|
122
|
+
const fromFile = readConfigObject().stateDir;
|
|
123
|
+
if (typeof fromFile === 'string' && fromFile.trim()) return resolve(fromFile.trim());
|
|
124
|
+
return join(homedir(), '.ours');
|
|
125
|
+
}
|
|
126
|
+
|
|
112
127
|
function daemonVoiceCapability() {
|
|
113
128
|
const r = run('ours-mcp', ['voice-status', '--json'], { capture: true, timeout: 6000 });
|
|
114
129
|
if (!r.ok) return null;
|
|
@@ -439,10 +454,24 @@ async function main() {
|
|
|
439
454
|
const voice = offerVoiceSetup({ readinessAfterStart: true });
|
|
440
455
|
const started = await act(`ours-mcp start (port ${chosenPort})`, async () => run('ours-mcp', ['start']));
|
|
441
456
|
const svc = await act('ours-mcp install-service (survives reboot)', async () => run('ours-mcp', ['install-service']));
|
|
442
|
-
|
|
457
|
+
// `ours-mcp install-service` STOPS the daemon before it writes the unit (core's
|
|
458
|
+
// cmdInstallService), then exits non-zero if `systemctl --user enable --now` fails —
|
|
459
|
+
// no linger, no user bus, a container, WSL without systemd. Left alone that turns a
|
|
460
|
+
// WORKING daemon into no daemon at all, while the line below still said "ready": the
|
|
461
|
+
// human identity and every MCP client then fail against a port nothing is listening
|
|
462
|
+
// on. Put the one shared daemon back up and report what is actually true.
|
|
463
|
+
let running = started.ok;
|
|
464
|
+
if (!DRY && !svc.ok) {
|
|
465
|
+
running = daemonRunning();
|
|
466
|
+
if (!running) {
|
|
467
|
+
line(info('the boot-service step stopped the daemon before it failed — restarting it.'));
|
|
468
|
+
running = run('ours-mcp', ['start']).ok || daemonRunning();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (running) line(ok(`ours core ready — running on port ${chosenPort}. No problems.`));
|
|
443
472
|
else line(warn(`could not auto-start — run '${c.cyan('ours-mcp start')}' to bring it up.`));
|
|
444
473
|
if (!svc.ok && !svc.dry) line(warn(`boot-service not installed — retry '${c.cyan('ours-mcp install-service')}' later.`));
|
|
445
|
-
if (voice.setupRan && !DRY &&
|
|
474
|
+
if (voice.setupRan && !DRY && running) {
|
|
446
475
|
const verified = daemonVoiceCapability();
|
|
447
476
|
if (verified?.ready) {
|
|
448
477
|
line(ok(`Voice transcription readiness confirmed (${verified.provider}) after the first start.`));
|
|
@@ -455,7 +484,13 @@ async function main() {
|
|
|
455
484
|
}
|
|
456
485
|
}
|
|
457
486
|
}
|
|
458
|
-
record({
|
|
487
|
+
record({
|
|
488
|
+
key: 'core',
|
|
489
|
+
label: 'ours core (daemon)',
|
|
490
|
+
state: running ? 'installed' : 'failed',
|
|
491
|
+
version: parseVersion(daemonVersionLine()),
|
|
492
|
+
note: svc.ok ? 'starts on boot' : 'running; no boot service',
|
|
493
|
+
});
|
|
459
494
|
} else {
|
|
460
495
|
// Installed: offer an update; never re-ask config; reuse the running port everywhere.
|
|
461
496
|
const daemonState = daemonLifecycleState();
|
|
@@ -639,6 +674,39 @@ async function main() {
|
|
|
639
674
|
}
|
|
640
675
|
cont(goFleet);
|
|
641
676
|
|
|
677
|
+
// Give the Telegram connector the ONE daemon this install just built: its loopback
|
|
678
|
+
// endpoint, the state directory that endpoint's API token belongs to, and — for a
|
|
679
|
+
// pre-0.3.3 connector that still meets the daemon at a broker instead — that broker.
|
|
680
|
+
// Idempotent: an unchanged selection writes nothing. Returns { changed, hadPrevious }
|
|
681
|
+
// so the caller can warn about a service unit that froze an older selection.
|
|
682
|
+
async function writeTgDaemonConfig({ chosenPort, chosenBroker, status0 }) {
|
|
683
|
+
const path = tgConfigPath(process.env, homedir());
|
|
684
|
+
const stateDir = daemonStateDir();
|
|
685
|
+
const desired = {
|
|
686
|
+
daemonUrl: daemonEndpoint(chosenPort),
|
|
687
|
+
daemonStateDir: stateDir,
|
|
688
|
+
brokerUrl: resolveSharedBroker({
|
|
689
|
+
chosenBroker,
|
|
690
|
+
statusBroker: status0.broker,
|
|
691
|
+
configBroker: readConfigObject().brokerUrl,
|
|
692
|
+
}),
|
|
693
|
+
};
|
|
694
|
+
let existing = {};
|
|
695
|
+
try { existing = JSON.parse(readFileSync(path, 'utf8')); } catch { /* absent or unreadable */ }
|
|
696
|
+
const plan = planTgDaemonConfig(existing, desired);
|
|
697
|
+
const hadPrevious = !!(plan.previous.daemonUrl || plan.previous.brokerUrl);
|
|
698
|
+
if (!plan.changed) {
|
|
699
|
+
line(ok(`Telegram connector already points at this daemon (${desired.daemonUrl}) — no change.`));
|
|
700
|
+
return { changed: false, hadPrevious };
|
|
701
|
+
}
|
|
702
|
+
await act(`write ${path} (daemon ${desired.daemonUrl}, state ${stateDir})`, async () => {
|
|
703
|
+
atomicWriteConfig(path, plan.text);
|
|
704
|
+
return { ok: true };
|
|
705
|
+
});
|
|
706
|
+
line(ok(`Telegram connector configured to use this daemon (${desired.daemonUrl}).`));
|
|
707
|
+
return { changed: true, hadPrevious };
|
|
708
|
+
}
|
|
709
|
+
|
|
642
710
|
// ============================================================================================
|
|
643
711
|
// STEP 4 / 4 — Telegram connector. Install-only (no bot tokens here). Then: run as a service?
|
|
644
712
|
// ============================================================================================
|
|
@@ -648,15 +716,28 @@ async function main() {
|
|
|
648
716
|
const goTg = yes(' Install it?', false);
|
|
649
717
|
if (goTg) {
|
|
650
718
|
await actSpin(`installing ${spec('tg-connector')}…`, `npm i -g ${spec('tg-connector')}`, () => runAsync(NPM, ['i', '-g', spec('tg-connector')]));
|
|
719
|
+
// POINT IT AT THE ONE DAEMON — BEFORE it is started or installed as a service.
|
|
720
|
+
// The connector never inherits ~/.ours/config.json (its SDK reports configPath:
|
|
721
|
+
// null unless told otherwise), and `install-service` bakes whatever it resolves
|
|
722
|
+
// into the unit as environment variables that outrank the file from then on. So
|
|
723
|
+
// the daemon's identity has to be in its config BEFORE either happens. See
|
|
724
|
+
// planTgDaemonConfig for why all three keys are written.
|
|
725
|
+
const tgConfigured = await writeTgDaemonConfig({ chosenPort, chosenBroker, status0 });
|
|
651
726
|
const asService = yes(' Keep it running in the background so it starts automatically on boot?', true);
|
|
652
727
|
if (asService) {
|
|
653
728
|
const svc = await act('ours-tg-connector install-service (starts on boot)', async () => run('ours-tg-connector', ['install-service']));
|
|
654
|
-
if (svc.ok) line(ok(
|
|
729
|
+
if (svc.ok) line(ok(`Telegram connector installed and running as a service (starts on boot), pointed at the daemon on port ${chosenPort}. No problems.`));
|
|
655
730
|
else line(warn(`connector installed, but the service didn't start — retry '${c.cyan('ours-tg-connector install-service')}'.`));
|
|
656
|
-
record({ key: 'telegram', label: 'Telegram connector', state: 'installed', version: globalVersion('@ours.network/tg-connector'), note:
|
|
731
|
+
record({ key: 'telegram', label: 'Telegram connector', state: 'installed', version: globalVersion('@ours.network/tg-connector'), note: `service (boot) · daemon ${chosenPort}` });
|
|
657
732
|
} else {
|
|
658
|
-
line(ok(`Telegram connector installed. Start it any time with '${c.cyan('ours-tg-connector start')}'. No problems.`));
|
|
659
|
-
|
|
733
|
+
line(ok(`Telegram connector installed, pointed at the daemon on port ${chosenPort}. Start it any time with '${c.cyan('ours-tg-connector start')}'. No problems.`));
|
|
734
|
+
// A connector already installed as a service froze its OLD daemon selection into
|
|
735
|
+
// the unit's environment, which outranks the file we just wrote. Config alone
|
|
736
|
+
// cannot repair that — say so plainly rather than let it look fixed.
|
|
737
|
+
if (tgConfigured.changed && tgConfigured.hadPrevious) {
|
|
738
|
+
line(warn(`if you previously ran '${c.cyan('ours-tg-connector install-service')}', re-run it — the old service froze the previous daemon selection in its unit.`));
|
|
739
|
+
}
|
|
740
|
+
record({ key: 'telegram', label: 'Telegram connector', state: 'installed', version: globalVersion('@ours.network/tg-connector'), note: `start on demand · daemon ${chosenPort}` });
|
|
660
741
|
}
|
|
661
742
|
} else {
|
|
662
743
|
line(info('skipped cleanly.'));
|
package/lib/logic.mjs
CHANGED
|
@@ -42,11 +42,29 @@ const CHANNEL_TRACKING_PKGS = new Set(['mcp', 'tg-connector', 'claude-code', 'co
|
|
|
42
42
|
const STABLE_ONLY_PKGS = new Set(['fleet']);
|
|
43
43
|
|
|
44
44
|
// Normalize a raw channel selection to 'latest' | 'nightly'. Anything unrecognized
|
|
45
|
-
// (incl. undefined/'') falls back to the
|
|
46
|
-
|
|
45
|
+
// (incl. undefined/'') falls back to the installer's OWN channel — never guesses a tag.
|
|
46
|
+
//
|
|
47
|
+
// WHY THE INSTALLER'S OWN VERSION IS A CHANNEL SIGNAL. `@ours.network/install` is
|
|
48
|
+
// published on both dist-tags from the same lockstep bump (.github/workflows/scripts/
|
|
49
|
+
// bump-versions.sh), so a nightly build carries a `-nightly.N` version and a stable
|
|
50
|
+
// build does not. Without this, `npm i -g @ours.network/install@nightly && ours-install`
|
|
51
|
+
// installed the nightly INSTALLER but @latest for everything it installs — which since
|
|
52
|
+
// tg-connector 0.3.3-nightly.1 is not merely older but a DIFFERENT ARCHITECTURE (0.3.2
|
|
53
|
+
// hosts its own ADAPT wrapper; the nightly attaches to the shared daemon over /api/v1,
|
|
54
|
+
// which only the SDK-based daemon serves). Mixing the two tags across that boundary is
|
|
55
|
+
// exactly the split-brain deployment this must not produce. A stable installer stays on
|
|
56
|
+
// @latest and can never consume a nightly; an explicit OURS_CHANNEL always wins over both.
|
|
57
|
+
export function resolveChannel(raw, selfVersion = '') {
|
|
47
58
|
const v = String(raw || '').trim().toLowerCase();
|
|
48
59
|
if (v === 'nightly' || v === 'prerelease' || v === 'next') return 'nightly';
|
|
49
|
-
|
|
60
|
+
if (v === 'latest' || v === 'stable') return DEFAULT_CHANNEL;
|
|
61
|
+
if (v) return DEFAULT_CHANNEL; // unrecognized: never guess, never inherit
|
|
62
|
+
return isNightlyVersion(selfVersion) ? 'nightly' : DEFAULT_CHANNEL;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A published nightly carries the `-nightly.N` prerelease suffix the bump script writes.
|
|
66
|
+
export function isNightlyVersion(version) {
|
|
67
|
+
return /-nightly\.\d+/.test(String(version || ''));
|
|
50
68
|
}
|
|
51
69
|
|
|
52
70
|
// The npm dist-tag to install for one package key under a channel. fleet is ALWAYS
|
|
@@ -70,6 +88,79 @@ export const RESERVED_PORTS = [3051];
|
|
|
70
88
|
export const DEFAULT_PORT = 3050;
|
|
71
89
|
export const DEFAULT_BROKER = 'wss://broker1.ours.network';
|
|
72
90
|
|
|
91
|
+
// ── Handing the Telegram connector the ONE shared daemon ───────────────────────
|
|
92
|
+
// The connector has its OWN config file and never inherits the daemon's. Two
|
|
93
|
+
// generations of it are in the wild and the installer must satisfy BOTH, because
|
|
94
|
+
// which one gets installed is a channel decision (see pkgTag):
|
|
95
|
+
//
|
|
96
|
+
// @nightly (>=0.3.3-nightly.1) — SDK-based. It ATTACHES to an already-running
|
|
97
|
+
// ours daemon over /api/v1 and has NO broker at all. It selects the daemon
|
|
98
|
+
// through @ours.network/sdk's resolveDaemonConfig, whose behaviour we verified
|
|
99
|
+
// against the published SDK 0.1.2 (the version the nightly pins):
|
|
100
|
+
// · with no overrides it reports configPath: null — the daemon's
|
|
101
|
+
// ~/.ours/config.json is NEVER read implicitly, so a daemon on any port
|
|
102
|
+
// other than the built-in 3050 is simply MISSED.
|
|
103
|
+
// · pointing OURS_CONFIG at that file, or passing an endpoint alone, throws
|
|
104
|
+
// INCOHERENT_SELECTION: "the endpoint was selected … but the state
|
|
105
|
+
// directory is the built-in default" — a deliberate credential-disclosure
|
|
106
|
+
// guard that refuses BEFORE any token is read.
|
|
107
|
+
// · endpoint AND state dir together resolve cleanly.
|
|
108
|
+
// So the ONLY correct contract is to give it BOTH daemonUrl and daemonStateDir.
|
|
109
|
+
//
|
|
110
|
+
// @latest (<=0.3.2) — the older self-hosting ADAPT wrapper. It ignores
|
|
111
|
+
// daemonUrl/daemonStateDir and meets the daemon at a BROKER instead, so its
|
|
112
|
+
// brokerUrl must match the daemon's or the two can never see each other.
|
|
113
|
+
//
|
|
114
|
+
// Writing all three keys satisfies whichever generation is installed: each reads
|
|
115
|
+
// only the keys it understands and ignores the rest. This matters at INSTALL time
|
|
116
|
+
// specifically, because `ours-tg-connector install-service` BAKES its resolved
|
|
117
|
+
// values into the service unit as environment variables, and env outranks the
|
|
118
|
+
// config file forever after — a divergence created here can never be repaired by
|
|
119
|
+
// editing that file.
|
|
120
|
+
|
|
121
|
+
// The daemon's loopback endpoint. The daemon always binds 127.0.0.1 (never a
|
|
122
|
+
// hostname), so this is the address the connector must be given.
|
|
123
|
+
export function daemonEndpoint(port) {
|
|
124
|
+
return `http://127.0.0.1:${port}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// The broker the whole deployment shares, for a <=0.3.2 connector.
|
|
128
|
+
// Precedence: what the user chose in THIS run > what the running daemon actually
|
|
129
|
+
// resolved (`ours-mcp status`, which already accounts for OURS_BROKER_URL) > what
|
|
130
|
+
// the daemon's config file says > the built-in default (identical to the daemon's
|
|
131
|
+
// DEFAULT_CONFIG.brokerUrl, so "no answer anywhere" still agrees).
|
|
132
|
+
export function resolveSharedBroker({ chosenBroker, statusBroker, configBroker } = {}) {
|
|
133
|
+
for (const candidate of [chosenBroker, statusBroker, configBroker]) {
|
|
134
|
+
const v = validateBroker(candidate ?? '');
|
|
135
|
+
if (!v.empty && v.ok) return v.value;
|
|
136
|
+
}
|
|
137
|
+
return DEFAULT_BROKER;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// The Telegram connector's own config file (mirrors its src/config.ts: OURS_TG_CONFIG,
|
|
141
|
+
// else <home>/.ours-telegram/config.json — a FIXED location, independent of its stateDir).
|
|
142
|
+
export function tgConfigPath(env = {}, home = '') {
|
|
143
|
+
return env.OURS_TG_CONFIG || `${home}/.ours-telegram/config.json`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Decide whether the connector's config needs a write, and what to write. Returns
|
|
147
|
+
// { changed, text, previous } — `changed` is false when the file already names this
|
|
148
|
+
// exact daemon and broker, so an idempotent re-run touches nothing. Unrelated keys
|
|
149
|
+
// the user or the connector added (bot tokens, STT settings) are preserved.
|
|
150
|
+
export function planTgDaemonConfig(existing, { daemonUrl, daemonStateDir, brokerUrl } = {}) {
|
|
151
|
+
const base = existing && typeof existing === 'object' ? existing : {};
|
|
152
|
+
const str = (v) => (typeof v === 'string' ? v : '');
|
|
153
|
+
const previous = {
|
|
154
|
+
daemonUrl: str(base.daemonUrl),
|
|
155
|
+
daemonStateDir: str(base.daemonStateDir),
|
|
156
|
+
brokerUrl: str(base.brokerUrl),
|
|
157
|
+
};
|
|
158
|
+
const next = { daemonUrl, daemonStateDir, brokerUrl };
|
|
159
|
+
const changed = Object.entries(next).some(([k, v]) => v !== undefined && previous[k] !== v);
|
|
160
|
+
if (!changed) return { changed: false, text: '', previous };
|
|
161
|
+
return { changed: true, text: mergeConfig(base, next), previous };
|
|
162
|
+
}
|
|
163
|
+
|
|
73
164
|
// suggestPort: pick a usable HTTP port. If `desired` is free and not reserved, keep it. Otherwise
|
|
74
165
|
// scan upward from 3060 (the brief's suggested alternate band) for the first free, non-reserved
|
|
75
166
|
// port. `isTaken(port)` is injected so this stays pure and testable (real caller probes a bind).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/install",
|
|
3
|
-
"version": "0.17.0-nightly.
|
|
3
|
+
"version": "0.17.0-nightly.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The unified ours.network stack installer (ours-install): one guided ~3-minute flow for ours core (the daemon) + the harness plugins (Claude Code / Codex) + ours-fleet + the Telegram connector, then a single copy-paste hand-off prompt. Self-contained (Node built-ins only); run as `ours-install` or via curl|bash (install.sh).",
|
|
6
6
|
"type": "module",
|