@ours.network/install 0.17.0-nightly.16 → 0.17.0-nightly.17
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/lib/components.mjs +6 -5
- package/lib/journal.mjs +46 -1
- package/lib/orchestrate-uninstall.mjs +3 -0
- package/package.json +1 -1
package/lib/components.mjs
CHANGED
|
@@ -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
|
|
26
|
-
//
|
|
27
|
-
// operator who declined it would have
|
|
28
|
-
//
|
|
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: '
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.17.0-nightly.17",
|
|
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",
|