0xray 4.0.25 → 4.0.26
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/.grok-plugin/plugin.json +1 -1
- package/.xray/features.json +1 -1
- package/dist/CHANGELOG.md +16 -0
- package/dist/cli/commands/mcp-install.d.ts +12 -0
- package/dist/cli/commands/mcp-install.js +38 -2
- package/dist/integrations/cursor/README.md +3 -1
- package/dist/integrations/cursor/hooks/cursor-hook-utils.js +5 -0
- package/dist/integrations/cursor/hooks/cursor-usage-receipt.js +94 -1
- package/dist/integrations/cursor/hooks/pre-compact.js +19 -1
- package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/dist/memory-routing/record-lesson.d.ts +7 -2
- package/dist/memory-routing/record-lesson.js +37 -3
- package/package.json +1 -1
- package/scripts/foundry/reconcile-version.mjs +24 -4
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/xray/features.json +1 -1
package/.grok-plugin/plugin.json
CHANGED
package/.xray/features.json
CHANGED
package/dist/CHANGELOG.md
CHANGED
|
@@ -41,6 +41,22 @@ The format is based on [Conventional Commits](https://www.conventionalcommits.or
|
|
|
41
41
|
- Archive root `integrations/` costume SKILL dump → `docs/archive/integrations-costume/`. Live wear is `src/integrations`.
|
|
42
42
|
- Archive root `commands/` stale copy → `docs/archive/commands-copy/`. Live wear is `src/opencode/commands`.
|
|
43
43
|
|
|
44
|
+
## [4.0.26] - 2026-09-25
|
|
45
|
+
|
|
46
|
+
### 🔄 Changes
|
|
47
|
+
|
|
48
|
+
### 🐛 Bug Fixes
|
|
49
|
+
- fix: store a lesson when the task matches no signal (e3c75b43d)
|
|
50
|
+
- fix: prove a Cursor compact cut with a session log and per-session receipt (bb3a1c68f)
|
|
51
|
+
- fix: mill check accepts a published version; mcp:status runs (a8bd79e23)
|
|
52
|
+
|
|
53
|
+
### 🔎 Other Changes
|
|
54
|
+
- Merge pull request #116 from 0xRayAI/cursor/mill-ci-mcp-status-fbe0 (ba94508d2)
|
|
55
|
+
- Merge pull request #120 from 0xRayAI/cursor/lesson-recall-fbe0 (8f4a5a62a)
|
|
56
|
+
- Merge pull request #119 from 0xRayAI/cursor/compact-receipt-proof-fbe0 (a911fd17b)
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
44
60
|
## [4.0.25] - 2026-09-24
|
|
45
61
|
|
|
46
62
|
`0xray@4.0.25` ships the plate schematics in the package. `docs-site/docs/plates/` is on the npm `files` list. A worn suit resolves `memory-recall` and `processor` from `dist/integrations/hooks/plates.cjs`.
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
export type McpInstallerAction = {
|
|
2
|
+
kind: "list" | "status" | "help";
|
|
3
|
+
} | {
|
|
4
|
+
kind: "install" | "remove";
|
|
5
|
+
name?: string;
|
|
6
|
+
} | {
|
|
7
|
+
kind: "unknown";
|
|
8
|
+
command: string;
|
|
9
|
+
};
|
|
10
|
+
/** Colon and hyphen forms documented on `npx 0xray` map to the bare dispatcher verbs. */
|
|
11
|
+
export declare function normalizeMcpInstallerToken(command: string | undefined): string | undefined;
|
|
12
|
+
export declare function classifyMcpInstallerArgs(args: readonly string[]): McpInstallerAction;
|
|
1
13
|
export declare function listMCPsCommand(): void;
|
|
2
14
|
export declare function showMCPStatusCommand(): void;
|
|
3
15
|
export declare function installMCPCommand(name: string): Promise<void>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from "fs";
|
|
2
|
-
import { join, dirname } from "path";
|
|
2
|
+
import { join, dirname, resolve } from "path";
|
|
3
3
|
import { execSync } from "child_process";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
4
5
|
import { getConfigDir } from "../../core/config-paths.js";
|
|
5
6
|
function getMCPConfigPath() {
|
|
6
7
|
return join(process.cwd(), ".xray", "mcp-registry.json");
|
|
@@ -186,9 +187,44 @@ function removeMCP(name) {
|
|
|
186
187
|
saveInstalledMCPs(installed);
|
|
187
188
|
console.log(`\n✅ Removed ${name}`);
|
|
188
189
|
}
|
|
190
|
+
const MCP_INSTALLER_ALIASES = {
|
|
191
|
+
"mcp:list": "list",
|
|
192
|
+
"mcp-list": "list",
|
|
193
|
+
"mcp:status": "status",
|
|
194
|
+
"mcp-status": "status",
|
|
195
|
+
"mcp:install": "install",
|
|
196
|
+
"mcp-install": "install",
|
|
197
|
+
"mcp:remove": "remove",
|
|
198
|
+
"mcp-remove": "remove",
|
|
199
|
+
};
|
|
200
|
+
/** Colon and hyphen forms documented on `npx 0xray` map to the bare dispatcher verbs. */
|
|
201
|
+
export function normalizeMcpInstallerToken(command) {
|
|
202
|
+
if (command === undefined)
|
|
203
|
+
return undefined;
|
|
204
|
+
return MCP_INSTALLER_ALIASES[command] ?? command;
|
|
205
|
+
}
|
|
206
|
+
export function classifyMcpInstallerArgs(args) {
|
|
207
|
+
const raw = args[0];
|
|
208
|
+
const command = normalizeMcpInstallerToken(raw);
|
|
209
|
+
if (command === undefined || command === "status")
|
|
210
|
+
return { kind: "status" };
|
|
211
|
+
if (command === "list")
|
|
212
|
+
return { kind: "list" };
|
|
213
|
+
if (command === "help" || command === "--help" || command === "-h")
|
|
214
|
+
return { kind: "help" };
|
|
215
|
+
if (command === "install" || command === "remove") {
|
|
216
|
+
const name = args[1];
|
|
217
|
+
return name === undefined ? { kind: command } : { kind: command, name };
|
|
218
|
+
}
|
|
219
|
+
return { kind: "unknown", command: raw ?? command };
|
|
220
|
+
}
|
|
221
|
+
const isDirectRun = process.argv[1] !== undefined &&
|
|
222
|
+
resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
189
223
|
async function main() {
|
|
224
|
+
if (!isDirectRun)
|
|
225
|
+
return;
|
|
190
226
|
const args = process.argv.slice(2);
|
|
191
|
-
const command = args[0];
|
|
227
|
+
const command = normalizeMcpInstallerToken(args[0]);
|
|
192
228
|
switch (command) {
|
|
193
229
|
case "list":
|
|
194
230
|
listMCPs();
|
|
@@ -40,7 +40,9 @@ Do **not** prove window pressure with UTF-8 chars÷4. `src/integrations/cursor/h
|
|
|
40
40
|
| `cursor-cloud` `run-info` / `get-events` / dashboard (model, bc-id, window) | yes — token MISS is honest |
|
|
41
41
|
| chars÷4, fill-only, `fillBytes` | **forbidden** (`ok: false`) |
|
|
42
42
|
|
|
43
|
-
Host `preCompact` fire is counted from `.xray/state/cursor-hook-invoke.log` (`event=preCompact`). Do not hand-invoke `pre-compact.js` to mint that Y.
|
|
43
|
+
Host `preCompact` fire is counted from `.xray/state/cursor-hook-invoke.log` (`event=preCompact`). Do not hand-invoke `pre-compact.js` to mint that Y. That invoke log has no session id.
|
|
44
|
+
|
|
45
|
+
A cut counts only when `.xray/state/cursor-receipts/<sessionId>.json` exists **and** `.xray/state/cursor-hook.log` contains a line with that same session id (timestamp, `context_tokens`, `context_usage_percent`, `context_window_size`, `generation_id`). `probeLogExists` is true only when that append succeeded. `.xray/state/cursor-usage-receipt.json` and `cursor-precompact.json` keep only the newest event. That single latest json is not enough — the next arm overwrites it.
|
|
44
46
|
|
|
45
47
|
Arm S landscape + receipt: `examples/killer-dual/`.
|
|
46
48
|
|
|
@@ -53,6 +53,11 @@ export {
|
|
|
53
53
|
FORBIDDEN_USAGE_METHODS,
|
|
54
54
|
invokeProbeLogPath,
|
|
55
55
|
cursorUsageReceiptPath,
|
|
56
|
+
cursorHookLogPath,
|
|
57
|
+
sanitizeSessionReceiptId,
|
|
58
|
+
cursorSessionReceiptPath,
|
|
59
|
+
appendCursorCompactProofLog,
|
|
60
|
+
writeCursorSessionReceipt,
|
|
56
61
|
parseInvokeProbeLog,
|
|
57
62
|
hostUsageFromPreCompactEvent,
|
|
58
63
|
classifyUsageCite,
|
|
@@ -28,6 +28,70 @@ export function cursorUsageReceiptPath(root) {
|
|
|
28
28
|
return path.join(root, '.xray', 'state', 'cursor-usage-receipt.json');
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/** Proof log. A cut is greppable here; cursor-hook-invoke.log has no session id. */
|
|
32
|
+
export function cursorHookLogPath(root) {
|
|
33
|
+
return path.join(root, '.xray', 'state', 'cursor-hook.log');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function logToken(value) {
|
|
37
|
+
if (value == null) return '';
|
|
38
|
+
return String(value).replace(/[\r\n]+/g, '');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Safe filename segment for `.xray/state/cursor-receipts/<id>.json`.
|
|
43
|
+
* Empty when the id cannot be kept inside that directory.
|
|
44
|
+
*/
|
|
45
|
+
export function sanitizeSessionReceiptId(sessionId) {
|
|
46
|
+
const raw = typeof sessionId === 'string' ? sessionId.trim() : '';
|
|
47
|
+
if (!raw) return '';
|
|
48
|
+
const safe = raw
|
|
49
|
+
.replace(/\0/g, '')
|
|
50
|
+
.replace(/[/\\]/g, '_')
|
|
51
|
+
.replace(/\.\./g, '_')
|
|
52
|
+
.replace(/[^A-Za-z0-9._-]+/g, '_')
|
|
53
|
+
.replace(/^[._]+/, '')
|
|
54
|
+
.replace(/[._]+$/g, '');
|
|
55
|
+
if (!safe || safe === '.' || safe === '..') return '';
|
|
56
|
+
return safe.slice(0, 180);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function cursorSessionReceiptPath(root, sessionId) {
|
|
60
|
+
const safe = sanitizeSessionReceiptId(sessionId);
|
|
61
|
+
if (!safe) return null;
|
|
62
|
+
return path.join(root, '.xray', 'state', 'cursor-receipts', `${safe}.json`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Append one preCompact proof line. True only when the append landed and
|
|
67
|
+
* the file contains that session id (grep of *.log can find the cut).
|
|
68
|
+
*/
|
|
69
|
+
export function appendCursorCompactProofLog(root, fields = {}) {
|
|
70
|
+
const sessionId = logToken(fields.sessionId);
|
|
71
|
+
const timestamp = logToken(fields.timestamp) || new Date().toISOString();
|
|
72
|
+
const line = [
|
|
73
|
+
`ts=${timestamp}`,
|
|
74
|
+
`session_id=${sessionId}`,
|
|
75
|
+
`context_tokens=${logToken(fields.context_tokens)}`,
|
|
76
|
+
`context_usage_percent=${logToken(fields.context_usage_percent)}`,
|
|
77
|
+
`context_window_size=${logToken(fields.context_window_size)}`,
|
|
78
|
+
`generation_id=${logToken(fields.generation_id)}`,
|
|
79
|
+
].join(' ');
|
|
80
|
+
const dest = cursorHookLogPath(root);
|
|
81
|
+
try {
|
|
82
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
83
|
+
fs.appendFileSync(dest, `${line}\n`);
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (!sessionId) return false;
|
|
88
|
+
try {
|
|
89
|
+
return fs.readFileSync(dest, 'utf8').includes(`session_id=${sessionId}`);
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
31
95
|
export function parseInvokeProbeLog(root) {
|
|
32
96
|
const dest = invokeProbeLogPath(root);
|
|
33
97
|
const counts = { preToolUse: 0, preCompact: 0, afterFileEdit: 0, unknown: 0 };
|
|
@@ -228,7 +292,7 @@ export function buildCursorUsageReceipt(root, input = {}) {
|
|
|
228
292
|
preToolUseCount: probe.counts.preToolUse,
|
|
229
293
|
afterFileEditCount: probe.counts.afterFileEdit,
|
|
230
294
|
eventClass,
|
|
231
|
-
probeLogExists:
|
|
295
|
+
probeLogExists: input.probeLogExists === true,
|
|
232
296
|
},
|
|
233
297
|
usage,
|
|
234
298
|
repertoire,
|
|
@@ -299,3 +363,32 @@ export function writeCursorUsageReceipt(root, input = {}) {
|
|
|
299
363
|
return null;
|
|
300
364
|
}
|
|
301
365
|
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Per-session copy of the latest precompact + usage receipts.
|
|
369
|
+
* The next arm overwrites cursor-precompact.json and cursor-usage-receipt.json.
|
|
370
|
+
*/
|
|
371
|
+
export function writeCursorSessionReceipt(root, sessionId, paths = {}) {
|
|
372
|
+
const dest = cursorSessionReceiptPath(root, sessionId);
|
|
373
|
+
if (!dest) return null;
|
|
374
|
+
if (!paths.precompactPath && !paths.usagePath) return null;
|
|
375
|
+
try {
|
|
376
|
+
const payload = {
|
|
377
|
+
sessionId: typeof sessionId === 'string' ? sessionId : null,
|
|
378
|
+
precompact: null,
|
|
379
|
+
usage: null,
|
|
380
|
+
};
|
|
381
|
+
if (paths.precompactPath && fs.existsSync(paths.precompactPath)) {
|
|
382
|
+
payload.precompact = JSON.parse(fs.readFileSync(paths.precompactPath, 'utf8'));
|
|
383
|
+
}
|
|
384
|
+
if (paths.usagePath && fs.existsSync(paths.usagePath)) {
|
|
385
|
+
payload.usage = JSON.parse(fs.readFileSync(paths.usagePath, 'utf8'));
|
|
386
|
+
}
|
|
387
|
+
if (!payload.precompact && !payload.usage) return null;
|
|
388
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
389
|
+
fs.writeFileSync(dest, JSON.stringify(payload, null, 2));
|
|
390
|
+
return dest;
|
|
391
|
+
} catch {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
@@ -18,7 +18,9 @@ import {
|
|
|
18
18
|
writeSessionBoot,
|
|
19
19
|
} from './cursor-hook-utils.js';
|
|
20
20
|
import {
|
|
21
|
+
appendCursorCompactProofLog,
|
|
21
22
|
hostUsageFromPreCompactEvent,
|
|
23
|
+
writeCursorSessionReceipt,
|
|
22
24
|
writeCursorUsageReceipt,
|
|
23
25
|
} from './cursor-usage-receipt.js';
|
|
24
26
|
|
|
@@ -132,6 +134,17 @@ async function main() {
|
|
|
132
134
|
});
|
|
133
135
|
bootPath = writeSessionBoot(root, payload);
|
|
134
136
|
stationLine = payload.stationLine;
|
|
137
|
+
const stampedAt = new Date().toISOString();
|
|
138
|
+
const proofLogged = appendCursorCompactProofLog(root, {
|
|
139
|
+
sessionId,
|
|
140
|
+
timestamp: stampedAt,
|
|
141
|
+
context_tokens: typeof event.context_tokens === 'number' ? event.context_tokens : null,
|
|
142
|
+
context_usage_percent:
|
|
143
|
+
typeof event.context_usage_percent === 'number' ? event.context_usage_percent : null,
|
|
144
|
+
context_window_size:
|
|
145
|
+
typeof event.context_window_size === 'number' ? event.context_window_size : null,
|
|
146
|
+
generation_id: generationId,
|
|
147
|
+
});
|
|
135
148
|
receiptPath = writeCursorPrecompactReceipt(root, {
|
|
136
149
|
event_class: eventClass,
|
|
137
150
|
hookEvent: 'pre_compact',
|
|
@@ -145,13 +158,18 @@ async function main() {
|
|
|
145
158
|
messages_to_compact:
|
|
146
159
|
typeof event.messages_to_compact === 'number' ? event.messages_to_compact : null,
|
|
147
160
|
bootPath,
|
|
148
|
-
timestamp:
|
|
161
|
+
timestamp: stampedAt,
|
|
149
162
|
});
|
|
150
163
|
usagePath = writeCursorUsageReceipt(root, {
|
|
151
164
|
eventClass,
|
|
152
165
|
sessionId,
|
|
166
|
+
probeLogExists: proofLogged,
|
|
153
167
|
usage: hostUsageFromPreCompactEvent(event),
|
|
154
168
|
});
|
|
169
|
+
writeCursorSessionReceipt(root, sessionId, {
|
|
170
|
+
precompactPath: receiptPath,
|
|
171
|
+
usagePath,
|
|
172
|
+
});
|
|
155
173
|
if (plates) {
|
|
156
174
|
try {
|
|
157
175
|
const named = plates.recallPlate(payload.intent || intent || '');
|
|
@@ -4,13 +4,18 @@ export interface LessonInput {
|
|
|
4
4
|
taskId: string;
|
|
5
5
|
assignedAgent: string;
|
|
6
6
|
sessionId: string;
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Signals the finished task already named. When set, text matching is not used.
|
|
9
|
+
* An empty list with a lesson still stores that lesson under a name taken from the task.
|
|
10
|
+
*/
|
|
8
11
|
signals?: string[];
|
|
9
12
|
/** Approaches, solutions, or the wrong turn from the sessions that named the law. */
|
|
10
13
|
lesson?: string;
|
|
11
14
|
}
|
|
12
15
|
/**
|
|
13
16
|
* A finished task names the signals its text actually matched and says
|
|
14
|
-
* whether the outcome was good. That is the one-tenth step.
|
|
17
|
+
* whether the outcome was good. That is the one-tenth step.
|
|
18
|
+
* A lesson with no matched signal is stored under a hyphenated name from the
|
|
19
|
+
* task, so the same task text recalls it. No lesson and no match, no write.
|
|
15
20
|
*/
|
|
16
21
|
export declare function recordLesson(input: LessonInput): string[];
|
|
@@ -1,14 +1,48 @@
|
|
|
1
1
|
import { ensureMemoryRoutingProviderSync } from "./provider-registry.js";
|
|
2
|
+
/**
|
|
3
|
+
* Hyphenated phrase whose spaced form is still inside the task.
|
|
4
|
+
* A later routing read of that same task hits the name.
|
|
5
|
+
*/
|
|
6
|
+
function signalNameForUnmatchedLesson(task) {
|
|
7
|
+
const words = task
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.split(/[^a-z0-9]+/)
|
|
10
|
+
.filter((word) => word.length >= 3);
|
|
11
|
+
const normalized = task.toLowerCase();
|
|
12
|
+
const kept = [];
|
|
13
|
+
for (const word of words) {
|
|
14
|
+
const next = [...kept, word];
|
|
15
|
+
const phrase = next.join(" ");
|
|
16
|
+
const hyphen = next.join("-");
|
|
17
|
+
if (!normalized.includes(phrase) || hyphen.length > 120)
|
|
18
|
+
break;
|
|
19
|
+
kept.push(word);
|
|
20
|
+
}
|
|
21
|
+
if (kept.length === 0)
|
|
22
|
+
return null;
|
|
23
|
+
return kept.join("-");
|
|
24
|
+
}
|
|
2
25
|
/**
|
|
3
26
|
* A finished task names the signals its text actually matched and says
|
|
4
|
-
* whether the outcome was good. That is the one-tenth step.
|
|
27
|
+
* whether the outcome was good. That is the one-tenth step.
|
|
28
|
+
* A lesson with no matched signal is stored under a hyphenated name from the
|
|
29
|
+
* task, so the same task text recalls it. No lesson and no match, no write.
|
|
5
30
|
*/
|
|
6
31
|
export function recordLesson(input) {
|
|
7
32
|
const provider = ensureMemoryRoutingProviderSync();
|
|
8
33
|
if (provider.id === "null" || !provider.ingestFeedback)
|
|
9
34
|
return [];
|
|
10
|
-
const
|
|
11
|
-
const
|
|
35
|
+
const lesson = (input.lesson ?? "").trim();
|
|
36
|
+
const supplied = input.signals;
|
|
37
|
+
const named = supplied !== undefined
|
|
38
|
+
? supplied
|
|
39
|
+
: provider.buildRoutingContext(input.operation).matchedSignals;
|
|
40
|
+
let names = [...new Set(named.filter((name) => name.length > 0))];
|
|
41
|
+
if (names.length === 0 && lesson.length > 0) {
|
|
42
|
+
const coined = signalNameForUnmatchedLesson(input.operation);
|
|
43
|
+
if (coined)
|
|
44
|
+
names = [coined];
|
|
45
|
+
}
|
|
12
46
|
if (names.length === 0)
|
|
13
47
|
return [];
|
|
14
48
|
provider.ingestFeedback({
|
package/package.json
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* npx @0xray/foundry reconcile patch --apply
|
|
12
12
|
*
|
|
13
13
|
* --apply writes package.json and the package-lock.json version fields.
|
|
14
|
-
* --check
|
|
14
|
+
* --check passes when package.json is equal to the registry (already published)
|
|
15
|
+
* or greater (the pre-publish bump). It fails when package.json is behind npm,
|
|
16
|
+
* or when the lock does not match package.json.
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
19
|
import { execSync } from "child_process";
|
|
@@ -91,6 +93,21 @@ function fail(msg) {
|
|
|
91
93
|
process.exit(1);
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Registry gate for --check.
|
|
98
|
+
* Equal to npm is a shipped release. Greater is the pre-publish bump.
|
|
99
|
+
* Only a package.json behind the registry fails.
|
|
100
|
+
* @param {string} local
|
|
101
|
+
* @param {string} npmVersion
|
|
102
|
+
* @returns {string | null}
|
|
103
|
+
*/
|
|
104
|
+
export function publishedVersionCheckError(local, npmVersion) {
|
|
105
|
+
if (compare(local, npmVersion) < 0) {
|
|
106
|
+
return `package.json (${local}) must be >= npm (${npmVersion}). Run: npx @0xray/foundry reconcile patch --apply`;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
94
111
|
function main() {
|
|
95
112
|
const local = readLocalVersion();
|
|
96
113
|
const npm = readPublishedVersion();
|
|
@@ -138,9 +155,12 @@ function main() {
|
|
|
138
155
|
return;
|
|
139
156
|
}
|
|
140
157
|
|
|
141
|
-
// Strict checks for release gate / pre-tag
|
|
142
|
-
|
|
143
|
-
|
|
158
|
+
// Strict checks for release gate / pre-tag.
|
|
159
|
+
// `local` is the version read before --apply. Apply still writes
|
|
160
|
+
// bumpVersion(max(npm, tag)) and is not this gate.
|
|
161
|
+
const behindNpm = publishedVersionCheckError(local, npm);
|
|
162
|
+
if (behindNpm) {
|
|
163
|
+
fail(behindNpm);
|
|
144
164
|
}
|
|
145
165
|
|
|
146
166
|
const pkgVersion = readLocalVersion();
|