@moxxy/plugin-self-update 0.26.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/LICENSE +21 -0
- package/dist/classify.d.ts +33 -0
- package/dist/classify.d.ts.map +1 -0
- package/dist/classify.js +177 -0
- package/dist/classify.js.map +1 -0
- package/dist/core-tools/apply.d.ts +4 -0
- package/dist/core-tools/apply.d.ts.map +1 -0
- package/dist/core-tools/apply.js +43 -0
- package/dist/core-tools/apply.js.map +1 -0
- package/dist/core-tools/begin.d.ts +4 -0
- package/dist/core-tools/begin.d.ts.map +1 -0
- package/dist/core-tools/begin.js +76 -0
- package/dist/core-tools/begin.js.map +1 -0
- package/dist/core-tools/edit.d.ts +4 -0
- package/dist/core-tools/edit.d.ts.map +1 -0
- package/dist/core-tools/edit.js +33 -0
- package/dist/core-tools/edit.js.map +1 -0
- package/dist/core-tools/index.d.ts +11 -0
- package/dist/core-tools/index.d.ts.map +1 -0
- package/dist/core-tools/index.js +28 -0
- package/dist/core-tools/index.js.map +1 -0
- package/dist/core-tools/preflight.d.ts +4 -0
- package/dist/core-tools/preflight.d.ts.map +1 -0
- package/dist/core-tools/preflight.js +14 -0
- package/dist/core-tools/preflight.js.map +1 -0
- package/dist/core-tools/rollback.d.ts +4 -0
- package/dist/core-tools/rollback.d.ts.map +1 -0
- package/dist/core-tools/rollback.js +30 -0
- package/dist/core-tools/rollback.js.map +1 -0
- package/dist/core-tools/shared.d.ts +16 -0
- package/dist/core-tools/shared.d.ts.map +1 -0
- package/dist/core-tools/shared.js +12 -0
- package/dist/core-tools/shared.js.map +1 -0
- package/dist/core-tools/status.d.ts +4 -0
- package/dist/core-tools/status.d.ts.map +1 -0
- package/dist/core-tools/status.js +25 -0
- package/dist/core-tools/status.js.map +1 -0
- package/dist/core-tools/verify.d.ts +4 -0
- package/dist/core-tools/verify.d.ts.map +1 -0
- package/dist/core-tools/verify.js +39 -0
- package/dist/core-tools/verify.js.map +1 -0
- package/dist/core-tools/write.d.ts +4 -0
- package/dist/core-tools/write.d.ts.map +1 -0
- package/dist/core-tools/write.js +28 -0
- package/dist/core-tools/write.js.map +1 -0
- package/dist/core-update.d.ts +173 -0
- package/dist/core-update.d.ts.map +1 -0
- package/dist/core-update.js +626 -0
- package/dist/core-update.js.map +1 -0
- package/dist/deps.d.ts +49 -0
- package/dist/deps.d.ts.map +1 -0
- package/dist/deps.js +30 -0
- package/dist/deps.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +349 -0
- package/dist/index.js.map +1 -0
- package/dist/transaction.d.ts +80 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +188 -0
- package/dist/transaction.js.map +1 -0
- package/dist/verify.d.ts +23 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +164 -0
- package/dist/verify.js.map +1 -0
- package/package.json +61 -0
- package/src/classify.test.ts +176 -0
- package/src/classify.ts +221 -0
- package/src/core-tools/apply.ts +44 -0
- package/src/core-tools/begin.ts +94 -0
- package/src/core-tools/edit.ts +33 -0
- package/src/core-tools/index.ts +33 -0
- package/src/core-tools/preflight.ts +15 -0
- package/src/core-tools/rollback.ts +30 -0
- package/src/core-tools/shared.test.ts +16 -0
- package/src/core-tools/shared.ts +24 -0
- package/src/core-tools/status.ts +26 -0
- package/src/core-tools/verify.ts +39 -0
- package/src/core-tools/write.ts +30 -0
- package/src/core-update.test.ts +542 -0
- package/src/core-update.ts +744 -0
- package/src/deps.ts +84 -0
- package/src/discovery.test.ts +40 -0
- package/src/index.test.ts +299 -0
- package/src/index.ts +441 -0
- package/src/transaction.test.ts +143 -0
- package/src/transaction.ts +254 -0
- package/src/verify.test.ts +82 -0
- package/src/verify.ts +200 -0
package/src/classify.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { EventLogReader } from '@moxxy/sdk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Request classification for self-update. The *signals* are gathered
|
|
5
|
+
* deterministically here; the *decision* is advisory — the orchestrating
|
|
6
|
+
* skill makes the final call. This keeps mechanical inspection in code and
|
|
7
|
+
* judgement in the prompt (mirroring how self-improver splits the two).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type Tier = 'skill' | 'plugin' | 'core';
|
|
11
|
+
|
|
12
|
+
export interface ClassifyInput {
|
|
13
|
+
readonly trigger: 'error' | 'request';
|
|
14
|
+
readonly text?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ClassifySignals {
|
|
18
|
+
readonly failedTools: ReadonlyArray<string>;
|
|
19
|
+
readonly errorMessages: ReadonlyArray<string>;
|
|
20
|
+
readonly registeredTools: ReadonlyArray<string>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ClassifyResult {
|
|
24
|
+
readonly tier: Tier;
|
|
25
|
+
readonly candidateName?: string;
|
|
26
|
+
readonly evidence: ReadonlyArray<string>;
|
|
27
|
+
readonly rationale: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const CORE_HINTS = [
|
|
31
|
+
'@moxxy/core',
|
|
32
|
+
'packages/core',
|
|
33
|
+
'core/src',
|
|
34
|
+
'run-turn',
|
|
35
|
+
'session.ts',
|
|
36
|
+
'event log',
|
|
37
|
+
'new event type',
|
|
38
|
+
'loop strategy internals',
|
|
39
|
+
'registry internals',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const SKILL_HINTS = [
|
|
43
|
+
'always',
|
|
44
|
+
'whenever',
|
|
45
|
+
'every time',
|
|
46
|
+
'remember to',
|
|
47
|
+
'before you',
|
|
48
|
+
'workflow',
|
|
49
|
+
'procedure',
|
|
50
|
+
'instruction',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const PLUGIN_HINTS = [
|
|
54
|
+
'add a tool',
|
|
55
|
+
'new tool',
|
|
56
|
+
'integrate',
|
|
57
|
+
'integration',
|
|
58
|
+
'connect to',
|
|
59
|
+
'call the',
|
|
60
|
+
' api',
|
|
61
|
+
'command that',
|
|
62
|
+
'wrap',
|
|
63
|
+
'override',
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/** Gather deterministic signals from the session event log. */
|
|
67
|
+
export function gatherSignals(
|
|
68
|
+
log: EventLogReader,
|
|
69
|
+
registeredTools: ReadonlyArray<string>,
|
|
70
|
+
lookback = 40,
|
|
71
|
+
): ClassifySignals {
|
|
72
|
+
const callNames = new Map<string, string>();
|
|
73
|
+
for (const e of log.ofType('tool_call_requested')) {
|
|
74
|
+
callNames.set(String(e.callId), e.name);
|
|
75
|
+
}
|
|
76
|
+
const recent = log.slice(Math.max(0, log.length - lookback));
|
|
77
|
+
const failedTools = new Set<string>();
|
|
78
|
+
const errorMessages: string[] = [];
|
|
79
|
+
for (const e of recent) {
|
|
80
|
+
if (e.type === 'tool_result' && !e.ok) {
|
|
81
|
+
const name = callNames.get(String(e.callId));
|
|
82
|
+
if (name) failedTools.add(name);
|
|
83
|
+
if (e.error?.message) errorMessages.push(e.error.message);
|
|
84
|
+
} else if (e.type === 'error') {
|
|
85
|
+
errorMessages.push(e.message);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
failedTools: [...failedTools],
|
|
90
|
+
errorMessages,
|
|
91
|
+
registeredTools,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function includesAny(haystack: string, needles: ReadonlyArray<string>): string | null {
|
|
96
|
+
const lower = haystack.toLowerCase();
|
|
97
|
+
for (const n of needles) {
|
|
98
|
+
if (lower.includes(n)) return n;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Suggest a kebab-case artifact name from free text. */
|
|
104
|
+
export function suggestName(text: string | undefined): string | undefined {
|
|
105
|
+
if (!text) return undefined;
|
|
106
|
+
const words = text
|
|
107
|
+
.toLowerCase()
|
|
108
|
+
.replace(/[^a-z0-9\s-]/g, ' ')
|
|
109
|
+
.split(/\s+/)
|
|
110
|
+
.filter((w) => w.length > 2 && !STOP_WORDS.has(w))
|
|
111
|
+
.slice(0, 4);
|
|
112
|
+
return words.length > 0 ? words.join('-') : undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const STOP_WORDS = new Set([
|
|
116
|
+
'the',
|
|
117
|
+
'and',
|
|
118
|
+
'for',
|
|
119
|
+
'add',
|
|
120
|
+
'can',
|
|
121
|
+
'you',
|
|
122
|
+
'please',
|
|
123
|
+
'that',
|
|
124
|
+
'this',
|
|
125
|
+
'with',
|
|
126
|
+
'make',
|
|
127
|
+
'create',
|
|
128
|
+
'want',
|
|
129
|
+
'would',
|
|
130
|
+
'like',
|
|
131
|
+
'your',
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Decide the lowest-risk tier that can satisfy the request, plus the evidence
|
|
136
|
+
* behind that suggestion. Always advisory.
|
|
137
|
+
*/
|
|
138
|
+
export function classify(input: ClassifyInput, signals: ClassifySignals): ClassifyResult {
|
|
139
|
+
const evidence: string[] = [];
|
|
140
|
+
const blob = `${input.text ?? ''} ${signals.errorMessages.join(' ')}`;
|
|
141
|
+
|
|
142
|
+
const coreHit = includesAny(blob, CORE_HINTS);
|
|
143
|
+
if (coreHit) {
|
|
144
|
+
evidence.push(`mentions core internals ("${coreHit}")`);
|
|
145
|
+
return {
|
|
146
|
+
tier: 'core',
|
|
147
|
+
evidence,
|
|
148
|
+
rationale:
|
|
149
|
+
'The change appears to touch @moxxy/core internals — escalate to a Tier-2 core patch (build + overlay + restart). Confirm a plugin override cannot express it first.',
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// A referenced-but-unregistered capability ⇒ a new plugin/tool is needed.
|
|
154
|
+
const missing = signals.failedTools.filter((t) => !signals.registeredTools.includes(t));
|
|
155
|
+
if (missing.length > 0) {
|
|
156
|
+
evidence.push(`failed call to unregistered tool(s): ${missing.join(', ')}`);
|
|
157
|
+
return {
|
|
158
|
+
tier: 'plugin',
|
|
159
|
+
candidateName: suggestName(input.text) ?? missing[0],
|
|
160
|
+
evidence,
|
|
161
|
+
rationale:
|
|
162
|
+
'A capability is missing. Author a new plugin (or install a published one) that provides the tool, then hot-reload.',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// An existing tool misbehaved ⇒ usually instructions (skill) or a hook wrapper.
|
|
167
|
+
if (signals.failedTools.length > 0) {
|
|
168
|
+
evidence.push(`registered tool(s) misbehaved: ${signals.failedTools.join(', ')}`);
|
|
169
|
+
const pluginHit = includesAny(blob, PLUGIN_HINTS);
|
|
170
|
+
if (pluginHit) {
|
|
171
|
+
evidence.push(`request implies a behavior override ("${pluginHit}")`);
|
|
172
|
+
return {
|
|
173
|
+
tier: 'plugin',
|
|
174
|
+
candidateName: suggestName(input.text),
|
|
175
|
+
evidence,
|
|
176
|
+
rationale:
|
|
177
|
+
'An existing tool misbehaves and the fix is a behavior change — wrap it in a plugin (onToolResult / onToolCall) rather than editing core.',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
tier: 'skill',
|
|
182
|
+
candidateName: suggestName(input.text),
|
|
183
|
+
evidence,
|
|
184
|
+
rationale:
|
|
185
|
+
'The tool exists but was used wrongly — a skill that encodes the correct procedure is the lowest-risk fix.',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// No error signal: classify the free-text request.
|
|
190
|
+
const skillHit = includesAny(blob, SKILL_HINTS);
|
|
191
|
+
const pluginHit = includesAny(blob, PLUGIN_HINTS);
|
|
192
|
+
if (pluginHit && !skillHit) {
|
|
193
|
+
evidence.push(`request implies a new capability ("${pluginHit}")`);
|
|
194
|
+
return {
|
|
195
|
+
tier: 'plugin',
|
|
196
|
+
candidateName: suggestName(input.text),
|
|
197
|
+
evidence,
|
|
198
|
+
rationale: 'The request asks for a new action/capability — author a plugin tool and hot-reload.',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (skillHit) {
|
|
202
|
+
evidence.push(`request describes a procedure ("${skillHit}")`);
|
|
203
|
+
return {
|
|
204
|
+
tier: 'skill',
|
|
205
|
+
candidateName: suggestName(input.text),
|
|
206
|
+
evidence,
|
|
207
|
+
rationale: 'The request is a recurring procedure expressible with existing tools — author a skill.',
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Default for an unqualified "add XYZ": prefer a plugin (capability), the
|
|
212
|
+
// common case, but the skill should reconsider a skill if no code is needed.
|
|
213
|
+
evidence.push('no strong signal; defaulting to the plugin tier');
|
|
214
|
+
return {
|
|
215
|
+
tier: 'plugin',
|
|
216
|
+
candidateName: suggestName(input.text),
|
|
217
|
+
evidence,
|
|
218
|
+
rationale:
|
|
219
|
+
'Ambiguous request. Default to authoring a plugin; if the change needs no new code (only instructions), drop to the skill tier instead.',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { defineTool, z, type ToolContext, type ToolDef } from '@moxxy/sdk';
|
|
2
|
+
import { overlayPackages, readCoreJournal, restoreOverlay, writeCoreJournal } from '../core-update.js';
|
|
3
|
+
import { emitSafe } from '../deps.js';
|
|
4
|
+
import { resolveCore, snapshotDir, type CoreToolDeps } from './shared.js';
|
|
5
|
+
|
|
6
|
+
// ── self_update_core_apply ──────────────────────────────────────────────────
|
|
7
|
+
export function coreApplyTool(cd: CoreToolDeps): ToolDef {
|
|
8
|
+
const { deps } = cd;
|
|
9
|
+
return defineTool({
|
|
10
|
+
name: 'self_update_core_apply',
|
|
11
|
+
description:
|
|
12
|
+
'Overlay the verified build into the live global install (snapshotting the previous dist for rollback) and stage a restart. The new core code only activates after moxxy restarts. Requires a prior successful self_update_core_verify.',
|
|
13
|
+
inputSchema: z.object({ coreTxnId: z.string().min(1) }),
|
|
14
|
+
permission: { action: 'prompt' },
|
|
15
|
+
handler: async (input, ctx: ToolContext) => {
|
|
16
|
+
const journal = await readCoreJournal(deps.moxxyDir, input.coreTxnId);
|
|
17
|
+
if (journal.state !== 'verified') {
|
|
18
|
+
throw new Error(`core txn ${input.coreTxnId} is "${journal.state}", not "verified" — run self_update_core_verify first`);
|
|
19
|
+
}
|
|
20
|
+
const install = resolveCore(cd);
|
|
21
|
+
if (!install) throw new Error('could not resolve @moxxy/core');
|
|
22
|
+
const res = await overlayPackages({
|
|
23
|
+
repo: journal.repoDir,
|
|
24
|
+
install,
|
|
25
|
+
pkgNames: journal.packages,
|
|
26
|
+
snapshotDir: snapshotDir(deps.moxxyDir, input.coreTxnId),
|
|
27
|
+
});
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
await restoreOverlay({ install, pkgNames: journal.packages, snapshotDir: snapshotDir(deps.moxxyDir, input.coreTxnId) }).catch(() => undefined);
|
|
30
|
+
throw new Error(`overlay failed and was rolled back: ${res.message}`);
|
|
31
|
+
}
|
|
32
|
+
journal.state = 'staged_restart';
|
|
33
|
+
await writeCoreJournal(deps.moxxyDir, journal);
|
|
34
|
+
await emitSafe(deps, ctx, 'core_apply', { txnId: input.coreTxnId, applied: res.applied });
|
|
35
|
+
return {
|
|
36
|
+
ok: true,
|
|
37
|
+
applied: res.applied,
|
|
38
|
+
restartRequired: true,
|
|
39
|
+
message:
|
|
40
|
+
'Core patch overlaid. RESTART moxxy to activate it (re-run `moxxy`, or it restarts on the next launch under a supervisor). It will be committed automatically on a clean boot; use self_update_core_rollback if needed.',
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { defineTool, z, type ToolContext, type ToolDef } from '@moxxy/sdk';
|
|
2
|
+
import {
|
|
3
|
+
corePreflight,
|
|
4
|
+
countCorruptCoreTxns,
|
|
5
|
+
listCoreTxns,
|
|
6
|
+
newCoreTxnId,
|
|
7
|
+
provisionWorkspace,
|
|
8
|
+
writeCoreJournal,
|
|
9
|
+
type CoreJournal,
|
|
10
|
+
} from '../core-update.js';
|
|
11
|
+
import { emitSafe } from '../deps.js';
|
|
12
|
+
import { resolveCore, type CoreToolDeps } from './shared.js';
|
|
13
|
+
|
|
14
|
+
// ── self_update_core_begin ──────────────────────────────────────────────────
|
|
15
|
+
export function coreBeginTool(cd: CoreToolDeps): ToolDef {
|
|
16
|
+
const { deps } = cd;
|
|
17
|
+
return defineTool({
|
|
18
|
+
name: 'self_update_core_begin',
|
|
19
|
+
description:
|
|
20
|
+
'Start a Tier-2 core patch: provision a source clone pinned to the EXACT installed commit (git clone/fetch + checkout gitHead + pnpm install — this can take minutes) and open a transaction. Returns a coreTxnId and the repo path. Edit files ONLY via self_update_core_write / self_update_core_edit, then self_update_core_verify. Prefer a Tier-1 plugin override first — only patch core when truly unavoidable.',
|
|
21
|
+
inputSchema: z.object({
|
|
22
|
+
packages: z
|
|
23
|
+
.array(z.string().min(1))
|
|
24
|
+
.min(1)
|
|
25
|
+
.describe('Affected @moxxy/* package names, e.g. ["@moxxy/core"].'),
|
|
26
|
+
}),
|
|
27
|
+
permission: { action: 'prompt' },
|
|
28
|
+
handler: async (input, ctx: ToolContext) => {
|
|
29
|
+
const install = resolveCore(cd);
|
|
30
|
+
if (!install) throw new Error('could not resolve the installed @moxxy/core — cannot self-update core');
|
|
31
|
+
|
|
32
|
+
// Fail CLOSED on a corrupt journal: an unparseable in-flight journal would
|
|
33
|
+
// otherwise vanish from listCoreTxns and let this begin clobber the shared
|
|
34
|
+
// repoDir. Refuse rather than risk concurrent overlay corruption.
|
|
35
|
+
const corrupt = await countCorruptCoreTxns(deps.moxxyDir);
|
|
36
|
+
if (corrupt > 0) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`${corrupt} core-update transaction journal(s) are corrupt/unreadable — refusing to start a ` +
|
|
39
|
+
`new core update (the serialization guard can't see them). Inspect ` +
|
|
40
|
+
`~/.moxxy/self-update/core-txns and resolve them before retrying.`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
// Serialize core transactions: only ONE may be in flight at a time. Every
|
|
44
|
+
// txn shares the single provisioned workspace (repoDir), so a second
|
|
45
|
+
// concurrent txn would clobber the first's edits + build. Refuse rather
|
|
46
|
+
// than corrupt — the active txn must be finished (verify → commit) or
|
|
47
|
+
// released (self_update_core_rollback, which is a no-op overlay restore +
|
|
48
|
+
// marks it rolled_back even when nothing was applied yet).
|
|
49
|
+
const active = (await listCoreTxns(deps.moxxyDir)).find(
|
|
50
|
+
(j) => j.state !== 'committed' && j.state !== 'rolled_back',
|
|
51
|
+
);
|
|
52
|
+
if (active) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`a core update is already in progress (txn ${active.txnId}, state "${active.state}"). ` +
|
|
55
|
+
`Finish it (self_update_core_verify → self_update_commit) or release it with ` +
|
|
56
|
+
`self_update_core_rollback({ coreTxnId: "${active.txnId}" }) before starting another.`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const pf = await corePreflight(install);
|
|
61
|
+
if (!pf.ok) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`core update preflight failed: ${pf.checks.filter((c) => !c.ok).map((c) => `${c.id} (${c.detail})`).join('; ')}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const prov = await provisionWorkspace({
|
|
67
|
+
moxxyDir: deps.moxxyDir,
|
|
68
|
+
install,
|
|
69
|
+
...(deps.coreUpdate?.repoUrlOverride ? { repoUrlOverride: deps.coreUpdate.repoUrlOverride } : {}),
|
|
70
|
+
});
|
|
71
|
+
if (!prov.ok) throw new Error(`provisioning failed (escalate to the user): ${prov.message}`);
|
|
72
|
+
|
|
73
|
+
const txnId = newCoreTxnId();
|
|
74
|
+
const journal: CoreJournal = {
|
|
75
|
+
txnId,
|
|
76
|
+
createdAt: new Date().toISOString(),
|
|
77
|
+
updatedAt: new Date().toISOString(),
|
|
78
|
+
packages: [...input.packages],
|
|
79
|
+
version: install.version,
|
|
80
|
+
...(install.gitHead ? { gitHead: install.gitHead } : {}),
|
|
81
|
+
repoDir: prov.repoDir,
|
|
82
|
+
state: 'provisioned',
|
|
83
|
+
attempts: [],
|
|
84
|
+
};
|
|
85
|
+
await writeCoreJournal(deps.moxxyDir, journal);
|
|
86
|
+
await emitSafe(deps, ctx, 'core_begin', { txnId, packages: journal.packages, version: install.version });
|
|
87
|
+
return {
|
|
88
|
+
coreTxnId: txnId,
|
|
89
|
+
repoDir: prov.repoDir,
|
|
90
|
+
next: 'Edit files under the repo with self_update_core_write / self_update_core_edit (paths relative to the repo), then call self_update_core_verify.',
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { defineTool, z, type ToolContext, type ToolDef } from '@moxxy/sdk';
|
|
3
|
+
import { readCoreJournal, safeRepoPath } from '../core-update.js';
|
|
4
|
+
import { emitSafe } from '../deps.js';
|
|
5
|
+
import type { CoreToolDeps } from './shared.js';
|
|
6
|
+
|
|
7
|
+
// ── self_update_core_edit ───────────────────────────────────────────────────
|
|
8
|
+
export function coreEditTool(cd: CoreToolDeps): ToolDef {
|
|
9
|
+
const { deps } = cd;
|
|
10
|
+
return defineTool({
|
|
11
|
+
name: 'self_update_core_edit',
|
|
12
|
+
description:
|
|
13
|
+
'Find-and-replace a unique string in a file inside the provisioned core clone (path relative to the repo root). Approval-gated.',
|
|
14
|
+
inputSchema: z.object({
|
|
15
|
+
coreTxnId: z.string().min(1),
|
|
16
|
+
file: z.string().min(1),
|
|
17
|
+
oldString: z.string().min(1),
|
|
18
|
+
newString: z.string(),
|
|
19
|
+
}),
|
|
20
|
+
permission: { action: 'prompt' },
|
|
21
|
+
handler: async (input, ctx: ToolContext) => {
|
|
22
|
+
const journal = await readCoreJournal(deps.moxxyDir, input.coreTxnId);
|
|
23
|
+
const abs = safeRepoPath(journal.repoDir, input.file);
|
|
24
|
+
const cur = await fs.readFile(abs, 'utf8');
|
|
25
|
+
const count = cur.split(input.oldString).length - 1;
|
|
26
|
+
if (count === 0) throw new Error(`oldString not found in ${input.file}`);
|
|
27
|
+
if (count > 1) throw new Error(`oldString is not unique in ${input.file} (${count} matches)`);
|
|
28
|
+
await fs.writeFile(abs, cur.replace(input.oldString, input.newString), 'utf8');
|
|
29
|
+
await emitSafe(deps, ctx, 'core_edit', { txnId: input.coreTxnId, file: input.file });
|
|
30
|
+
return { ok: true, edited: input.file };
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ToolDef } from '@moxxy/sdk';
|
|
2
|
+
import type { SelfUpdateDeps } from '../deps.js';
|
|
3
|
+
import { coreApplyTool } from './apply.js';
|
|
4
|
+
import { coreBeginTool } from './begin.js';
|
|
5
|
+
import { coreEditTool } from './edit.js';
|
|
6
|
+
import { corePreflightTool } from './preflight.js';
|
|
7
|
+
import { coreRollbackTool } from './rollback.js';
|
|
8
|
+
import type { CoreToolDeps } from './shared.js';
|
|
9
|
+
import { coreStatusTool } from './status.js';
|
|
10
|
+
import { coreVerifyTool } from './verify.js';
|
|
11
|
+
import { coreWriteTool } from './write.js';
|
|
12
|
+
|
|
13
|
+
export type { CoreToolDeps } from './shared.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Composes the Tier-2 core-update tools from per-tool factories (mirroring the
|
|
17
|
+
* Tier-1 layout). `fromUrl` is the module URL used to resolve the live
|
|
18
|
+
* `@moxxy/core` install (the plugin entry's `import.meta.url`). Order preserved
|
|
19
|
+
* exactly as before extraction.
|
|
20
|
+
*/
|
|
21
|
+
export function coreTools(deps: SelfUpdateDeps, fromUrl: string): ToolDef[] {
|
|
22
|
+
const cd: CoreToolDeps = { deps, fromUrl };
|
|
23
|
+
return [
|
|
24
|
+
corePreflightTool(cd),
|
|
25
|
+
coreBeginTool(cd),
|
|
26
|
+
coreWriteTool(cd),
|
|
27
|
+
coreEditTool(cd),
|
|
28
|
+
coreVerifyTool(cd),
|
|
29
|
+
coreApplyTool(cd),
|
|
30
|
+
coreRollbackTool(cd),
|
|
31
|
+
coreStatusTool(cd),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineTool, z, type ToolDef } from '@moxxy/sdk';
|
|
2
|
+
import { corePreflight } from '../core-update.js';
|
|
3
|
+
import { resolveCore, type CoreToolDeps } from './shared.js';
|
|
4
|
+
|
|
5
|
+
// ── self_update_core_preflight ──────────────────────────────────────────────
|
|
6
|
+
export function corePreflightTool(cd: CoreToolDeps): ToolDef {
|
|
7
|
+
return defineTool({
|
|
8
|
+
name: 'self_update_core_preflight',
|
|
9
|
+
description:
|
|
10
|
+
'Read-only. Check whether a Tier-2 core patch is even possible: git + pnpm present, @moxxy/core resolvable, a pinned source commit (gitHead) and repo URL in its published metadata. Run this BEFORE attempting to patch @moxxy/core; if any check fails, do not start — tell the user.',
|
|
11
|
+
inputSchema: z.object({}),
|
|
12
|
+
permission: { action: 'allow' },
|
|
13
|
+
handler: async () => corePreflight(resolveCore(cd)),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineTool, z, type ToolContext, type ToolDef } from '@moxxy/sdk';
|
|
2
|
+
import { gcCoreTxns, readCoreJournal, restoreOverlay, writeCoreJournal } from '../core-update.js';
|
|
3
|
+
import { emitSafe } from '../deps.js';
|
|
4
|
+
import { resolveCore, snapshotDir, type CoreToolDeps } from './shared.js';
|
|
5
|
+
|
|
6
|
+
// ── self_update_core_rollback ───────────────────────────────────────────────
|
|
7
|
+
export function coreRollbackTool(cd: CoreToolDeps): ToolDef {
|
|
8
|
+
const { deps } = cd;
|
|
9
|
+
return defineTool({
|
|
10
|
+
name: 'self_update_core_rollback',
|
|
11
|
+
description:
|
|
12
|
+
'Undo a core overlay: restore the previous dist from the snapshot. A restart is required to drop the patched code. Use if a core patch built+loaded but misbehaves.',
|
|
13
|
+
inputSchema: z.object({ coreTxnId: z.string().min(1), reason: z.string().optional() }),
|
|
14
|
+
permission: { action: 'allow' },
|
|
15
|
+
handler: async (input, ctx: ToolContext) => {
|
|
16
|
+
const journal = await readCoreJournal(deps.moxxyDir, input.coreTxnId);
|
|
17
|
+
const install = resolveCore(cd);
|
|
18
|
+
if (!install) throw new Error('could not resolve @moxxy/core');
|
|
19
|
+
await restoreOverlay({ install, pkgNames: journal.packages, snapshotDir: snapshotDir(deps.moxxyDir, input.coreTxnId) });
|
|
20
|
+
journal.state = 'rolled_back';
|
|
21
|
+
await writeCoreJournal(deps.moxxyDir, journal);
|
|
22
|
+
await emitSafe(deps, ctx, 'core_rollback', { txnId: input.coreTxnId, reason: input.reason ?? null });
|
|
23
|
+
// Reclaim disk now: this txn is terminal, and its (large) dist snapshot is
|
|
24
|
+
// no longer needed. Prune older terminal core txns too. Best-effort — keep
|
|
25
|
+
// the freshly-rolled-back one + recent history. Mirrors maxTxnRetained.
|
|
26
|
+
await gcCoreTxns(deps.moxxyDir, deps.maxTxnRetained ?? 5).catch(() => undefined);
|
|
27
|
+
return { ok: true, restored: journal.packages, restartRequired: true };
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { coreTxnDir } from '../core-update.js';
|
|
4
|
+
import { snapshotDir } from './shared.js';
|
|
5
|
+
|
|
6
|
+
describe('core-tools shared helpers', () => {
|
|
7
|
+
it('snapshotDir nests "snapshot" under the txn dir', () => {
|
|
8
|
+
const moxxyDir = '/tmp/.moxxy';
|
|
9
|
+
const txnId = 'core-abc123';
|
|
10
|
+
expect(snapshotDir(moxxyDir, txnId)).toBe(path.join(coreTxnDir(moxxyDir, txnId), 'snapshot'));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('produces distinct snapshot dirs for distinct txns', () => {
|
|
14
|
+
expect(snapshotDir('/x', 'a')).not.toBe(snapshotDir('/x', 'b'));
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import { coreTxnDir } from '../core-update.js';
|
|
3
|
+
import { resolveCoreInstall, type SelfUpdateDeps } from '../deps.js';
|
|
4
|
+
import type { CoreInstallInfo } from '../core-update.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Context threaded into every Tier-2 core-tool factory. `fromUrl` is the
|
|
8
|
+
* module URL used to resolve the live `@moxxy/core` install (defaults to the
|
|
9
|
+
* plugin entry's `import.meta.url`, overridable via deps.coreUpdate.fromUrl).
|
|
10
|
+
*/
|
|
11
|
+
export interface CoreToolDeps {
|
|
12
|
+
readonly deps: SelfUpdateDeps;
|
|
13
|
+
readonly fromUrl: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Where a given core transaction parks its pre-overlay dist snapshot. */
|
|
17
|
+
export function snapshotDir(moxxyDir: string, txnId: string): string {
|
|
18
|
+
return path.join(coreTxnDir(moxxyDir, txnId), 'snapshot');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Resolve the live `@moxxy/core` install for this plugin's deps. */
|
|
22
|
+
export function resolveCore(c: CoreToolDeps): CoreInstallInfo | null {
|
|
23
|
+
return resolveCoreInstall(c.deps, c.fromUrl);
|
|
24
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineTool, z, type ToolDef } from '@moxxy/sdk';
|
|
2
|
+
import { listCoreTxns } from '../core-update.js';
|
|
3
|
+
import type { CoreToolDeps } from './shared.js';
|
|
4
|
+
|
|
5
|
+
// ── self_update_core_status ─────────────────────────────────────────────────
|
|
6
|
+
export function coreStatusTool(cd: CoreToolDeps): ToolDef {
|
|
7
|
+
const { deps } = cd;
|
|
8
|
+
return defineTool({
|
|
9
|
+
name: 'self_update_core_status',
|
|
10
|
+
description: 'List Tier-2 core-update transactions and their state.',
|
|
11
|
+
inputSchema: z.object({}),
|
|
12
|
+
permission: { action: 'allow' },
|
|
13
|
+
handler: async () => {
|
|
14
|
+
const all = await listCoreTxns(deps.moxxyDir);
|
|
15
|
+
return {
|
|
16
|
+
transactions: all.map((j) => ({
|
|
17
|
+
coreTxnId: j.txnId,
|
|
18
|
+
state: j.state,
|
|
19
|
+
packages: j.packages,
|
|
20
|
+
version: j.version,
|
|
21
|
+
updatedAt: j.updatedAt,
|
|
22
|
+
})),
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineTool, z, type ToolContext, type ToolDef } from '@moxxy/sdk';
|
|
2
|
+
import { readCoreJournal, verifyCorePackages, writeCoreJournal } from '../core-update.js';
|
|
3
|
+
import { emitSafe } from '../deps.js';
|
|
4
|
+
import { resolveCore, type CoreToolDeps } from './shared.js';
|
|
5
|
+
|
|
6
|
+
// ── self_update_core_verify ─────────────────────────────────────────────────
|
|
7
|
+
export function coreVerifyTool(cd: CoreToolDeps): ToolDef {
|
|
8
|
+
const { deps } = cd;
|
|
9
|
+
return defineTool({
|
|
10
|
+
name: 'self_update_core_verify',
|
|
11
|
+
description:
|
|
12
|
+
'Build, typecheck and test the affected core packages (and their dependents) in the clone, and confirm the patch adds no new runtime dependency. Returns stage results. Run AFTER edits; nothing in the live install changes here.',
|
|
13
|
+
inputSchema: z.object({ coreTxnId: z.string().min(1) }),
|
|
14
|
+
permission: { action: 'prompt' },
|
|
15
|
+
handler: async (input, ctx: ToolContext) => {
|
|
16
|
+
const journal = await readCoreJournal(deps.moxxyDir, input.coreTxnId);
|
|
17
|
+
const install = resolveCore(cd);
|
|
18
|
+
if (!install) throw new Error('could not resolve @moxxy/core');
|
|
19
|
+
const res = await verifyCorePackages(journal.repoDir, install, journal.packages);
|
|
20
|
+
journal.attempts.push({
|
|
21
|
+
at: new Date().toISOString(),
|
|
22
|
+
stage: 'verify',
|
|
23
|
+
ok: res.ok,
|
|
24
|
+
message: res.ok ? 'build/typecheck/test ok' : res.stages.find((s) => !s.ok)?.message ?? 'failed',
|
|
25
|
+
});
|
|
26
|
+
journal.state = res.ok ? 'verified' : journal.state;
|
|
27
|
+
await writeCoreJournal(deps.moxxyDir, journal);
|
|
28
|
+
await emitSafe(deps, ctx, res.ok ? 'core_verify_ok' : 'core_verify_failed', { txnId: input.coreTxnId });
|
|
29
|
+
return {
|
|
30
|
+
ok: res.ok,
|
|
31
|
+
stages: res.stages,
|
|
32
|
+
newDeps: res.newDeps,
|
|
33
|
+
next: res.ok
|
|
34
|
+
? 'Passed — call self_update_core_apply to overlay it into the live install (a restart is then required).'
|
|
35
|
+
: 'Fix the errors and re-verify, or escalate to the user.',
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { defineTool, z, type ToolContext, type ToolDef } from '@moxxy/sdk';
|
|
4
|
+
import { readCoreJournal, safeRepoPath } from '../core-update.js';
|
|
5
|
+
import { emitSafe } from '../deps.js';
|
|
6
|
+
import type { CoreToolDeps } from './shared.js';
|
|
7
|
+
|
|
8
|
+
// ── self_update_core_write ──────────────────────────────────────────────────
|
|
9
|
+
export function coreWriteTool(cd: CoreToolDeps): ToolDef {
|
|
10
|
+
const { deps } = cd;
|
|
11
|
+
return defineTool({
|
|
12
|
+
name: 'self_update_core_write',
|
|
13
|
+
description:
|
|
14
|
+
'Write a file inside the provisioned core clone for a transaction (paths are relative to the repo root and cannot escape it). This is an approval-gated code write — show the user the content first.',
|
|
15
|
+
inputSchema: z.object({
|
|
16
|
+
coreTxnId: z.string().min(1),
|
|
17
|
+
file: z.string().min(1).describe('Path relative to the repo root, e.g. packages/core/src/foo.ts'),
|
|
18
|
+
content: z.string(),
|
|
19
|
+
}),
|
|
20
|
+
permission: { action: 'prompt' },
|
|
21
|
+
handler: async (input, ctx: ToolContext) => {
|
|
22
|
+
const journal = await readCoreJournal(deps.moxxyDir, input.coreTxnId);
|
|
23
|
+
const abs = safeRepoPath(journal.repoDir, input.file);
|
|
24
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
25
|
+
await fs.writeFile(abs, input.content, 'utf8');
|
|
26
|
+
await emitSafe(deps, ctx, 'core_write', { txnId: input.coreTxnId, file: input.file });
|
|
27
|
+
return { ok: true, wrote: input.file };
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|