@ours.network/install 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,7 +34,8 @@ connector from another daemon still require explicit confirmation.
34
34
  - Installs `@ours.network/cli`, `@ours.network/mcp`,
35
35
  `@ours.network/tg-connector`, `@ours.network/cowork`, and
36
36
  `@ours.network/fleet` on one release channel.
37
- - Configures, starts, and enables the single shared daemon.
37
+ - Configures, starts, and enables the single shared daemon with a CLI-managed
38
+ user systemd service on Linux or LaunchAgent on macOS.
38
39
  - Creates the daemon's Human identity (historically called the root identity),
39
40
  or preserves the existing one on a re-run.
40
41
  - Installs the ours plugin into safely detected Claude Code, Codex, and Hermes
package/install.sh CHANGED
@@ -40,13 +40,13 @@ PKG="${OURS_INSTALL_PKG:-@ours.network/install}"
40
40
  NPM_BIN="${OURS_NPM:-npm}"
41
41
 
42
42
  # --- 1) Node.js + npm check + friendly guidance ------------------------------------------------
43
- # The installer needs Node.js ≥ 20 (and npm to fetch the command). If missing, don't fail
43
+ # The installer needs Node.js ≥ 22 (and npm to fetch the command). If missing, don't fail
44
44
  # cryptically: explain what Node is and how to get it for this OS, point at nodejs.org, and exit
45
45
  # cleanly (0) so a piped run ends gracefully rather than with a scary non-zero error.
46
46
  if ! command -v node >/dev/null 2>&1 || ! command -v "$NPM_BIN" >/dev/null 2>&1; then
47
47
  os="$(uname -s 2>/dev/null || echo unknown)"
48
48
  printf '\n'
49
- say "ours needs Node.js (version 20 or newer, which includes npm) — it isn't installed yet."
49
+ say "ours needs Node.js (version 22 or newer, which includes npm) — it isn't installed yet."
50
50
  say "Node.js is a common, free runtime; here's how to get it:"
51
51
  case "$os" in
52
52
  Darwin)
@@ -489,11 +489,8 @@ export async function runServicePhase(args, effects, dir, port) {
489
489
  const plan = planServiceInstall({
490
490
  stateDir: dir, home: effects.home, readText: effects.readText, platform: effects.platform?.platform,
491
491
  });
492
- // NOT a refusal and NOT a failure: the daemon is running and correct, and only
493
- // the boot service could not be installed `ours daemon install-service` throws
494
- // on any non-linux platform. The run CONTINUES, says so, and the summary marks
495
- // it, because the person this hurts reboots in a fortnight and finds nothing
496
- // listening.
492
+ // NOT a refusal and NOT a failure: this is retained for platforms with no CLI
493
+ // service adapter. Linux and macOS both take the normal install path.
497
494
  if (plan.action === 'unsupported') {
498
495
  effects.out(warn(`ours: ${plan.message}`));
499
496
  effects.out(info(`Your daemon is installed and running now. To start it after a reboot, run: ${plan.manual.join(' ')} ${join(dir, 'config.json')}`));
@@ -507,13 +504,9 @@ export async function runServicePhase(args, effects, dir, port) {
507
504
  const adopting = plan.action === 'adopt';
508
505
  if (adopting) effects.out(info(plan.notice));
509
506
  const command = serviceInstallCommand({ stateDir: dir, adoptLegacyUnit: adopting });
510
- // The unit's bytes BEFORE, so "did it change?" survives the loss of --json.
511
- // `ours daemon install-service --json` used to answer that itself; ours-mcp's
512
- // takes no flags and reports nothing machine-readable. Assuming `changed: true`
513
- // would make every re-run claim it rewrote the unit, which turns the honest
514
- // "everything already correct" line into one that never appears — a screen that
515
- // is wrong in the reassuring direction. So the installer does the comparison it
516
- // used to delegate: it already reads this file to classify it.
507
+ // The service definition's bytes BEFORE are retained for Linux's established
508
+ // reporting path. On Darwin the CLI's JSON result is authoritative because it
509
+ // owns the LaunchAgent and launchctl transaction end to end.
517
510
  const unitBefore = plan.unitPath ? effects.readText(plan.unitPath) : null;
518
511
  let outcome;
519
512
  try {
@@ -531,11 +524,13 @@ export async function runServicePhase(args, effects, dir, port) {
531
524
  // which is the safe direction for a summary line.
532
525
  const changed = args.dryRun
533
526
  ? false
534
- : (() => {
535
- const after = plan.unitPath ? effects.readText(plan.unitPath) : null;
536
- if (unitBefore === null || after === null) return true;
537
- return unitBefore !== after;
538
- })();
527
+ : plan.platform === 'darwin'
528
+ ? readChanged(outcome.stdout)
529
+ : (() => {
530
+ const after = plan.unitPath ? effects.readText(plan.unitPath) : null;
531
+ if (unitBefore === null || after === null) return true;
532
+ return unitBefore !== after;
533
+ })();
539
534
  return { step: { id: 'service', changed, reason: changed ? undefined : 'unit unchanged' }, plan };
540
535
  }
541
536
 
@@ -724,7 +719,7 @@ export async function askBroker(args, effects) {
724
719
  * Pre-flight: the two disasters worth catching before anything is touched.
725
720
  *
726
721
  * Carried over from the v2 body deliberately. Native Windows and a Node older
727
- * than 20 are not "the install went badly", they are "this cannot work here",
722
+ * than 22 are not "the install went badly", they are "this cannot work here",
728
723
  * and finding that out after the daemon package is on disk helps nobody.
729
724
  *
730
725
  * NOT carried over: v2 also exited when no harness was found. Under v3 the
@@ -751,12 +746,9 @@ export function runPreflight(effects) {
751
746
  return { ok: false, platform: plat };
752
747
  }
753
748
  effects.out(ok(`Platform: ${plat.label} (supported)`));
754
- if (effects.platform?.platform && effects.platform.platform !== 'linux') {
755
- effects.out(info(`On ${plat.label} the daemon runs, but installing a BOOT SERVICE is not available — you will start it yourself after a reboot.`));
756
- }
757
749
  const version = String(effects.nodeVersion ?? '0');
758
- if (Number.parseInt(version.split('.')[0], 10) < 20) {
759
- effects.out(warn(`Node.js ${version} — ours needs v20 or newer. Update Node and re-run.`));
750
+ if (Number.parseInt(version.split('.')[0], 10) < 22) {
751
+ effects.out(warn(`Node.js ${version} — ours needs v22 or newer. Update Node and re-run.`));
760
752
  return { ok: false, platform: plat, node: version };
761
753
  }
762
754
  effects.out(ok(`Node.js ${version}`));
@@ -1123,8 +1115,8 @@ export async function runInstall(argv, effects) {
1123
1115
  state: target.action === 'create' ? 'installed' : 'current',
1124
1116
  note: `port ${target.port}`,
1125
1117
  }];
1126
- // The skip must not read as success: same "needs attention" mark a failed
1127
- // component gets, so the closing screen cannot say everything is clean.
1118
+ // A future supported runtime may still lack a CLI service adapter. Do not
1119
+ // render that partial state as success.
1128
1120
  if (daemon.serviceUnsupported) {
1129
1121
  summary.push({
1130
1122
  key: 'service',
package/lib/plan.mjs CHANGED
@@ -9,6 +9,7 @@ import { join, resolve, basename } from 'node:path';
9
9
  export const CLI_UNIT_MARKER = '# Managed by @ours.network/cli';
10
10
  export const SYSTEMD_USER_DIR = ['.config', 'systemd', 'user'];
11
11
  export const DEFAULT_SYSTEMD_UNIT = 'ours.service';
12
+ export const DEFAULT_LAUNCHD_LABEL = 'solutions.adaptframework.ours';
12
13
 
13
14
  // -----------------------------------------------------------------------------
14
15
  // Which unit file does this state directory own?
@@ -46,6 +47,23 @@ export function unitPathForStateDir(stateDir, home) {
46
47
  return { ...derived, path: join(home, ...SYSTEMD_USER_DIR, derived.unit) };
47
48
  }
48
49
 
50
+ /**
51
+ * Read-only launchd label derivation for accurate progress output. The CLI owns
52
+ * the plist path, generation, permissions, conflict handling and launchctl.
53
+ *
54
+ * SOURCE OF TRUTH IS @ours.network/cli 2.6.1 service-instance.ts. Keep this in
55
+ * the same table tests as the systemd derivation above.
56
+ */
57
+ export function launchdLabelForStateDir(stateDir) {
58
+ const derived = unitNameForStateDir(stateDir);
59
+ if (!derived.ok) return derived;
60
+ return {
61
+ ok: true,
62
+ label: derived.instance ? `${DEFAULT_LAUNCHD_LABEL}.${derived.instance}` : DEFAULT_LAUNCHD_LABEL,
63
+ instance: derived.instance,
64
+ };
65
+ }
66
+
49
67
  // -----------------------------------------------------------------------------
50
68
  // The unmarked-unit case — the migration blocker
51
69
  // -----------------------------------------------------------------------------
@@ -110,39 +128,28 @@ export function classifyUnit(text) {
110
128
  * default that spreads to the other cases.
111
129
  */
112
130
  export function planServiceInstall({ stateDir, home, readText, platform = 'linux' }) {
113
- // THE BOOT SERVICE IS LINUX-ONLY, AND NOT BECAUSE THIS FILE SAYS SO.
114
- //
115
- // `ours daemon install-service` in @ours.network/cli builds its adapter with
116
- // `createLinuxUserSystemdAdapter()` and no platform branch at all, and that
117
- // factory's FIRST line is
118
- // if (deps.platform !== 'linux') throw new Error('service management is not
119
- // supported on <platform>; use an external launcher for `ours daemon serve`')
120
- // — verified by reading the published 0.4.1 tarball, which contains zero
121
- // occurrences of launchd, LaunchAgents or plist.
122
- //
123
- // So calling it on macOS does not degrade, it THROWS. Before this, a Mac user
124
- // was told their platform was supported, watched the CLI install, the config
125
- // write and the daemon start, and then got an exception and a rolled-back
126
- // config. Skipping the step leaves them a working daemon and one true sentence
127
- // instead — which is the whole of this change.
128
- //
129
- // A real launchd adapter belongs in the SDK CLI, not here. Nothing in this
130
- // package can install a launchd agent, and pretending otherwise by writing a
131
- // plist ourselves would put a second service implementation in a second repo.
132
- // `ours daemon install-service` supports Linux/systemd only: its adapter throws
133
- // for any other platform, so calling it would fail the run rather than degrade.
134
- // Skip it and say so; the daemon itself is unaffected.
135
- if (platform && platform !== 'linux') {
131
+ // @ours.network/cli 2.6.1 dispatches this same command to user systemd on
132
+ // Linux and launchd on macOS. Other platforms still have no service adapter.
133
+ if (platform && platform !== 'linux' && platform !== 'darwin') {
136
134
  return {
137
135
  action: 'unsupported',
138
136
  platform,
139
137
  reason: 'no-service-manager',
140
- message: platform === 'darwin'
141
- ? 'installing a boot service is not available on macOS — the ours CLI can only manage a Linux user systemd service'
142
- : `installing a boot service is not available on ${platform} — the ours CLI can only manage a Linux user systemd service`,
138
+ message: `installing a boot service is not available on ${platform} the ours CLI supports Linux user systemd and macOS launchd`,
143
139
  manual: ['ours', 'daemon', 'serve', '--config'],
144
140
  };
145
141
  }
142
+
143
+ if (platform === 'darwin') {
144
+ const derived = launchdLabelForStateDir(stateDir);
145
+ if (!derived.ok) {
146
+ return { action: 'refuse', exitCode: 2, reason: 'unusable-state-dir', message: derived.reason };
147
+ }
148
+ return {
149
+ action: 'install', platform, unit: derived.label, instance: derived.instance,
150
+ };
151
+ }
152
+
146
153
  const derived = unitPathForStateDir(stateDir, home);
147
154
  if (!derived.ok) {
148
155
  return { action: 'refuse', exitCode: 2, reason: 'unusable-state-dir', message: derived.reason };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/install",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "description": "The all-in-one ours.network installer: one shared daemon, MCP, cowork, Telegram, Fleet, harness plugins, Human identity, stopped Fleet starter, progress UI, and guided next steps.",
6
6
  "type": "module",
@@ -25,7 +25,7 @@
25
25
  "directory": "packages/installer"
26
26
  },
27
27
  "engines": {
28
- "node": ">=20"
28
+ "node": ">=22"
29
29
  },
30
30
  "scripts": {
31
31
  "test": "node --test"