offrouter-adapter-grok 0.2.0 → 0.2.2
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/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +99 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +365 -0
- package/dist/install.js.map +1 -0
- package/dist/profile.d.ts +27 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +71 -0
- package/dist/profile.js.map +1 -0
- package/dist/rollback.d.ts +23 -0
- package/dist/rollback.d.ts.map +1 -0
- package/dist/rollback.js +219 -0
- package/dist/rollback.js.map +1 -0
- package/package.json +10 -5
- package/src/index.ts +0 -36
- package/src/install.test.ts +0 -584
- package/src/install.ts +0 -473
- package/src/profile.ts +0 -99
- package/src/rollback.ts +0 -290
- package/tsconfig.json +0 -10
package/src/rollback.ts
DELETED
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rollback for profile-scoped Grok install state.
|
|
3
|
-
* Restores the profile's config.toml to its previous content (or removes the
|
|
4
|
-
* OffRouter-created file) and drops the install-state metadata.
|
|
5
|
-
*/
|
|
6
|
-
import {
|
|
7
|
-
access,
|
|
8
|
-
mkdir,
|
|
9
|
-
readFile,
|
|
10
|
-
rmdir,
|
|
11
|
-
unlink,
|
|
12
|
-
writeFile,
|
|
13
|
-
} from "node:fs/promises";
|
|
14
|
-
import { constants } from "node:fs";
|
|
15
|
-
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
16
|
-
import { homedir as osHomedir } from "node:os";
|
|
17
|
-
import {
|
|
18
|
-
GROK_CONFIG_REL,
|
|
19
|
-
GROK_INSTALL_STATE_REL,
|
|
20
|
-
GrokInstallStateSchema,
|
|
21
|
-
type GrokInstallState,
|
|
22
|
-
type InstallChangeKind,
|
|
23
|
-
type InstallFileChange,
|
|
24
|
-
} from "./install.js";
|
|
25
|
-
import { resolveGrokProfileDir } from "./profile.js";
|
|
26
|
-
|
|
27
|
-
export interface GrokRollbackOptions {
|
|
28
|
-
profile: string;
|
|
29
|
-
env?: NodeJS.ProcessEnv;
|
|
30
|
-
homedir?: () => string;
|
|
31
|
-
dryRun?: boolean;
|
|
32
|
-
/** Optional absolute override of the profile directory (tests). */
|
|
33
|
-
profileDir?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface GrokRollbackResult {
|
|
37
|
-
ok: boolean;
|
|
38
|
-
dryRun: boolean;
|
|
39
|
-
profile: string;
|
|
40
|
-
profileDir: string;
|
|
41
|
-
changes: InstallFileChange[];
|
|
42
|
-
message: string;
|
|
43
|
-
errorCode?: "no_install_state" | "invalid_install_state" | "rollback_error";
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function pathExists(path: string): Promise<boolean> {
|
|
47
|
-
try {
|
|
48
|
-
await access(path, constants.F_OK);
|
|
49
|
-
return true;
|
|
50
|
-
} catch {
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function changeSummary(kind: InstallChangeKind, relativePath: string): string {
|
|
56
|
-
switch (kind) {
|
|
57
|
-
case "create":
|
|
58
|
-
return `+ ${relativePath}`;
|
|
59
|
-
case "update":
|
|
60
|
-
return `~ ${relativePath}`;
|
|
61
|
-
case "unchanged":
|
|
62
|
-
return `= ${relativePath}`;
|
|
63
|
-
case "remove":
|
|
64
|
-
return `- ${relativePath}`;
|
|
65
|
-
default:
|
|
66
|
-
return `? ${relativePath}`;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function loadState(statePath: string): Promise<
|
|
71
|
-
| { ok: true; state: GrokInstallState }
|
|
72
|
-
| {
|
|
73
|
-
ok: false;
|
|
74
|
-
errorCode: "no_install_state" | "invalid_install_state";
|
|
75
|
-
message: string;
|
|
76
|
-
}
|
|
77
|
-
> {
|
|
78
|
-
if (!(await pathExists(statePath))) {
|
|
79
|
-
return {
|
|
80
|
-
ok: false,
|
|
81
|
-
errorCode: "no_install_state",
|
|
82
|
-
message: `No install state at ${statePath}. Nothing to uninstall.`,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
try {
|
|
86
|
-
const raw = await readFile(statePath, "utf8");
|
|
87
|
-
const state = GrokInstallStateSchema.parse(JSON.parse(raw));
|
|
88
|
-
return { ok: true, state };
|
|
89
|
-
} catch (err) {
|
|
90
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
91
|
-
return {
|
|
92
|
-
ok: false,
|
|
93
|
-
errorCode: "invalid_install_state",
|
|
94
|
-
message: `Invalid install state at ${statePath}: ${detail}`,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function isWindowsAbsolutePath(path: string): boolean {
|
|
100
|
-
return /^[A-Za-z]:[\\/]/.test(path);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function isInsideOrSame(root: string, candidate: string): boolean {
|
|
104
|
-
const rel = relative(root, candidate);
|
|
105
|
-
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function resolveProfileRelativePath(
|
|
109
|
-
profileDir: string,
|
|
110
|
-
relativePath: string,
|
|
111
|
-
): string | null {
|
|
112
|
-
if (
|
|
113
|
-
relativePath.trim() === "" ||
|
|
114
|
-
relativePath.includes("\0") ||
|
|
115
|
-
isAbsolute(relativePath) ||
|
|
116
|
-
isWindowsAbsolutePath(relativePath)
|
|
117
|
-
) {
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const parts = relativePath.split(/[\\/]+/);
|
|
122
|
-
if (parts.some((part) => part === "" || part === "." || part === "..")) {
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const root = resolve(profileDir);
|
|
127
|
-
const candidate = resolve(root, ...parts);
|
|
128
|
-
if (!isInsideOrSame(root, candidate)) {
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
return candidate;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function validateRollbackState(
|
|
135
|
-
profileDir: string,
|
|
136
|
-
state: GrokInstallState,
|
|
137
|
-
): { ok: true; configPath: string } | { ok: false; message: string } {
|
|
138
|
-
if (state.configRelative !== GROK_CONFIG_REL) {
|
|
139
|
-
return {
|
|
140
|
-
ok: false,
|
|
141
|
-
message: `Invalid config relative path in install state: ${state.configRelative}`,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const configPath = resolveProfileRelativePath(
|
|
146
|
-
profileDir,
|
|
147
|
-
state.configRelative,
|
|
148
|
-
);
|
|
149
|
-
if (!configPath) {
|
|
150
|
-
return {
|
|
151
|
-
ok: false,
|
|
152
|
-
message: `Invalid config relative path in install state: ${state.configRelative}`,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
for (const relativePath of Object.keys(state.files)) {
|
|
157
|
-
if (relativePath !== GROK_CONFIG_REL) {
|
|
158
|
-
return {
|
|
159
|
-
ok: false,
|
|
160
|
-
message: `Install state path is outside config root: ${relativePath}`,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
const abs = resolveProfileRelativePath(profileDir, relativePath);
|
|
164
|
-
if (!abs) {
|
|
165
|
-
return {
|
|
166
|
-
ok: false,
|
|
167
|
-
message: `Invalid install state path: ${relativePath}`,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return { ok: true, configPath };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Restore files tracked by grok-install-state.json for a profile.
|
|
177
|
-
*/
|
|
178
|
-
export async function rollbackGrokProfile(
|
|
179
|
-
options: GrokRollbackOptions,
|
|
180
|
-
): Promise<GrokRollbackResult> {
|
|
181
|
-
const {
|
|
182
|
-
profile,
|
|
183
|
-
dryRun = false,
|
|
184
|
-
env = process.env,
|
|
185
|
-
homedir = osHomedir,
|
|
186
|
-
} = options;
|
|
187
|
-
const profileDir =
|
|
188
|
-
options.profileDir ?? resolveGrokProfileDir(profile, { env, homedir });
|
|
189
|
-
const statePath = join(profileDir, GROK_INSTALL_STATE_REL);
|
|
190
|
-
|
|
191
|
-
const loaded = await loadState(statePath);
|
|
192
|
-
if (!loaded.ok) {
|
|
193
|
-
return {
|
|
194
|
-
ok: false,
|
|
195
|
-
dryRun,
|
|
196
|
-
profile,
|
|
197
|
-
profileDir,
|
|
198
|
-
changes: [],
|
|
199
|
-
message: loaded.message,
|
|
200
|
-
errorCode: loaded.errorCode,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const { state } = loaded;
|
|
205
|
-
const validated = validateRollbackState(profileDir, state);
|
|
206
|
-
if (!validated.ok) {
|
|
207
|
-
return {
|
|
208
|
-
ok: false,
|
|
209
|
-
dryRun,
|
|
210
|
-
profile,
|
|
211
|
-
profileDir,
|
|
212
|
-
changes: [],
|
|
213
|
-
message: validated.message,
|
|
214
|
-
errorCode: "invalid_install_state",
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const changes: InstallFileChange[] = [];
|
|
219
|
-
const configPath = validated.configPath;
|
|
220
|
-
const fileState = state.files[GROK_CONFIG_REL];
|
|
221
|
-
const previous = fileState?.previous ?? null;
|
|
222
|
-
|
|
223
|
-
if (previous === null) {
|
|
224
|
-
// File was created by install; remove it on rollback.
|
|
225
|
-
const existed = await pathExists(configPath);
|
|
226
|
-
const kind: InstallChangeKind = existed ? "remove" : "unchanged";
|
|
227
|
-
changes.push({
|
|
228
|
-
path: configPath,
|
|
229
|
-
relativePath: GROK_CONFIG_REL,
|
|
230
|
-
kind,
|
|
231
|
-
summary: changeSummary(kind, GROK_CONFIG_REL),
|
|
232
|
-
});
|
|
233
|
-
if (!dryRun && existed) {
|
|
234
|
-
await unlink(configPath);
|
|
235
|
-
}
|
|
236
|
-
} else {
|
|
237
|
-
let current: string | null = null;
|
|
238
|
-
if (await pathExists(configPath)) {
|
|
239
|
-
current = await readFile(configPath, "utf8");
|
|
240
|
-
}
|
|
241
|
-
let kind: InstallChangeKind;
|
|
242
|
-
if (current === previous) {
|
|
243
|
-
kind = "unchanged";
|
|
244
|
-
} else if (current === null) {
|
|
245
|
-
kind = "create"; // restore missing prior content
|
|
246
|
-
} else {
|
|
247
|
-
kind = "update";
|
|
248
|
-
}
|
|
249
|
-
changes.push({
|
|
250
|
-
path: configPath,
|
|
251
|
-
relativePath: GROK_CONFIG_REL,
|
|
252
|
-
kind,
|
|
253
|
-
summary: changeSummary(kind, GROK_CONFIG_REL),
|
|
254
|
-
});
|
|
255
|
-
if (!dryRun && kind !== "unchanged") {
|
|
256
|
-
await mkdir(dirname(configPath), { recursive: true });
|
|
257
|
-
await writeFile(configPath, previous, "utf8");
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Drop install state last
|
|
262
|
-
const stateChangeKind: InstallChangeKind = "remove";
|
|
263
|
-
changes.push({
|
|
264
|
-
path: statePath,
|
|
265
|
-
relativePath: GROK_INSTALL_STATE_REL,
|
|
266
|
-
kind: stateChangeKind,
|
|
267
|
-
summary: changeSummary(stateChangeKind, GROK_INSTALL_STATE_REL),
|
|
268
|
-
});
|
|
269
|
-
if (!dryRun) {
|
|
270
|
-
await unlink(statePath);
|
|
271
|
-
// Best-effort remove only the empty install-state directory.
|
|
272
|
-
try {
|
|
273
|
-
await rmdir(join(profileDir, ".offrouter"));
|
|
274
|
-
} catch {
|
|
275
|
-
// ignore
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const applied = changes.filter((c) => c.kind !== "unchanged");
|
|
280
|
-
return {
|
|
281
|
-
ok: true,
|
|
282
|
-
dryRun,
|
|
283
|
-
profile,
|
|
284
|
-
profileDir,
|
|
285
|
-
changes,
|
|
286
|
-
message: dryRun
|
|
287
|
-
? `Dry run: would roll back ${applied.length} change(s) for profile ${profile}.`
|
|
288
|
-
: `Rolled back OffRouter Grok install for profile ${profile} (${applied.length} change(s)).`,
|
|
289
|
-
};
|
|
290
|
-
}
|