@ours.network/install 0.17.0-nightly.16 → 0.17.0-nightly.18

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.
@@ -22,12 +22,13 @@ import { pkgSpec, resolveChannel } from './logic.mjs';
22
22
  // effects.installedVersion('@ours.network/mcp@nightly') return null forever,
23
23
  // which fails the cowork version floor CLOSED and blanks the version column —
24
24
  // a silent regression that looks like "cowork is too old".
25
- // `required` is not a stronger default — it is the absence of a choice. The MCP
26
- // package IS the daemon now (the daemon phase installs and starts it), so an
27
- // operator who declined it would have no daemon at all, which is not a decision
28
- // anyone means to make. Declining has to be impossible rather than discouraged.
25
+ // `required` is not a stronger default — it is the absence of a choice. The
26
+ // daemon is the ours-sdk CLI and this package is the MCP server every harness
27
+ // speaks to it through, so an operator who declined it would have a daemon no
28
+ // harness can reach which is not a decision anyone means to make. Declining
29
+ // has to be impossible rather than discouraged.
29
30
  export const COMPONENTS = [
30
- { key: 'mcp', label: 'ours daemon + MCP server', pkg: '@ours.network/mcp', specKey: 'mcp', default: true, required: true },
31
+ { key: 'mcp', label: 'MCP server for your harness', pkg: '@ours.network/mcp', specKey: 'mcp', default: true, required: true },
31
32
  { key: 'tg', label: 'Telegram connector', pkg: '@ours.network/tg-connector', specKey: 'tg-connector', default: false },
32
33
  { key: 'cowork', label: 'cowork', pkg: '@ours.network/cowork', specKey: 'cowork', default: false },
33
34
  ];
package/lib/journal.mjs CHANGED
@@ -71,7 +71,10 @@ export function configJournal(effects, { dryRun = false } = {}) {
71
71
  for (const entry of entries.slice().reverse()) {
72
72
  try {
73
73
  effects.restore(entry.path, entry.snapshot);
74
- restored.push({ path: entry.path, existed: entry.snapshot?.exists !== false });
74
+ // A write that RETURNED is not a file that HOLDS. Read the bytes back.
75
+ const mismatch = verifyRestored(effects, entry);
76
+ if (mismatch) failed.push({ path: entry.path, reason: mismatch });
77
+ else restored.push({ path: entry.path, existed: entry.snapshot?.exists !== false });
75
78
  } catch (error) {
76
79
  failed.push({ path: entry.path, reason: error instanceof Error ? error.message : String(error) });
77
80
  }
@@ -82,6 +85,48 @@ export function configJournal(effects, { dryRun = false } = {}) {
82
85
  };
83
86
  }
84
87
 
88
+ /**
89
+ * Did the restore actually take? Returns null when it did, or the reason it did not.
90
+ *
91
+ * WHY THIS EXISTS. `restore` returning is evidence that a call completed, not that
92
+ * a file holds the bytes it was given. A full disk, a read-only mount, a rename
93
+ * that lands somewhere else, an editor holding the inode — each of those can let
94
+ * the write return and leave the old contents in place. Reporting on the call
95
+ * rather than on the state is how a rollback comes to LIE, and a rollback that
96
+ * lies is worse than one that admits it failed: the operator is told the machine
97
+ * is back where it was and stops looking.
98
+ *
99
+ * The read-back seam is `effects.snapshot`, the same one used to record the bytes
100
+ * in the first place — it already returns exactly the shape being compared, so
101
+ * this needs no second seam and no second notion of what a file's state is.
102
+ *
103
+ * Its own failure is a mismatch, not an exception: if the file cannot be read
104
+ * after being written, that is precisely the case this check exists to catch.
105
+ */
106
+ function verifyRestored(effects, entry) {
107
+ const expected = entry.snapshot;
108
+ let actual;
109
+ try {
110
+ actual = effects.snapshot(entry.path);
111
+ } catch (error) {
112
+ return `restored, but the file could not be read back to confirm it (${error instanceof Error ? error.message : String(error)})`;
113
+ }
114
+ const shouldExist = expected?.exists !== false;
115
+ if (!shouldExist) {
116
+ return actual?.exists ? 'this run created it and the removal did not take — the file is still there' : null;
117
+ }
118
+ if (!actual?.exists) return 'the restore reported success but the file is not there';
119
+ if (actual.text !== expected.text) return 'the restore reported success but the bytes on disk are not the previous ones';
120
+ // Bytes first, permissions second: the contents are back either way, so this
121
+ // must not be reported as "could not restore the config".
122
+ if (expected.mode !== undefined && actual.mode !== undefined && actual.mode !== expected.mode) {
123
+ return `contents restored, but the permissions are ${fmtMode(actual.mode)} and were ${fmtMode(expected.mode)} — check them before re-running`;
124
+ }
125
+ return null;
126
+ }
127
+
128
+ const fmtMode = (mode) => `0${(mode & 0o777).toString(8).padStart(3, '0')}`;
129
+
85
130
  /**
86
131
  * The report, and it is half the feature.
87
132
  *
@@ -330,6 +330,9 @@ export async function runPluginPhase(plugins, { args, effects, selection = null
330
330
  effects.out(warn(`${block.path}: ${stripped.reason}. Remove it by hand.`));
331
331
  continue;
332
332
  }
333
+ // Deliberately NOT journalled: this write IS the state its bytes describe, so
334
+ // nothing behind it can fail and leave them untrue. Adding one would undo a
335
+ // completed removal.
333
336
  await perform(effects, args.dryRun, `remove the ours managed block from ${block.path} (file kept)`, () => effects.writeText(block.path, stripped.text));
334
337
  }
335
338
  for (const dir of harness.dirs) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/install",
3
- "version": "0.17.0-nightly.16",
3
+ "version": "0.17.0-nightly.18",
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",