pi-antigravity-rotator 2.2.2 → 2.3.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/CHANGELOG.md +11 -0
- package/README.md +9 -1
- package/package.json +1 -1
- package/src/account-store.ts +109 -168
- package/src/admin-auth.ts +74 -83
- package/src/cli.ts +62 -46
- package/src/config-defaults.ts +64 -0
- package/src/config-storage.ts +21 -0
- package/src/dashboard.ts +310 -3193
- package/src/db-store.ts +155 -0
- package/src/doctor.ts +96 -78
- package/src/index.ts +200 -149
- package/src/onboarding.ts +352 -113
- package/src/paths.ts +34 -31
- package/src/proxy.ts +1751 -1270
- package/src/responses-store.ts +150 -178
- package/src/rotator.ts +3041 -2310
- package/src/settings-repository.ts +314 -0
- package/src/static/dashboard.css +1224 -0
- package/src/static/dashboard.js +1834 -0
- package/src/types.ts +501 -397
package/src/db-store.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Convenience façade over the settings repository.
|
|
2
|
+
//
|
|
3
|
+
// Maintains full backward compatibility with the previous module API while
|
|
4
|
+
// delegating all persistence to an ISettingsRepository implementation.
|
|
5
|
+
// Both PostgresSettingsRepository and FileSettingsRepository are fully
|
|
6
|
+
// self-contained — callers no longer need to branch on isDbConfigured().
|
|
7
|
+
|
|
8
|
+
import type { Config, PersistedState, TokenUsageTiered } from "./types.js";
|
|
9
|
+
import type { PersistedResponsesStore } from "./responses-store.js";
|
|
10
|
+
import { validateConfig } from "./validators.js";
|
|
11
|
+
import { applyConfigDefaults } from "./config-defaults.js";
|
|
12
|
+
import {
|
|
13
|
+
type ISettingsRepository,
|
|
14
|
+
PostgresSettingsRepository,
|
|
15
|
+
FileSettingsRepository,
|
|
16
|
+
} from "./settings-repository.js";
|
|
17
|
+
|
|
18
|
+
export type { PersistedResponsesStore } from "./responses-store.js";
|
|
19
|
+
|
|
20
|
+
// ----- Repository strategy -----
|
|
21
|
+
|
|
22
|
+
function createRepository(): ISettingsRepository {
|
|
23
|
+
if (process.env.PI_ROTATOR_DATABASE_URL || process.env.DATABASE_URL) {
|
|
24
|
+
return new PostgresSettingsRepository();
|
|
25
|
+
}
|
|
26
|
+
return new FileSettingsRepository();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const repository: ISettingsRepository = createRepository();
|
|
30
|
+
|
|
31
|
+
let initialized = false;
|
|
32
|
+
|
|
33
|
+
function assertInitialized(): void {
|
|
34
|
+
if (!initialized) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"db-store: repository not initialized. Call initDb() before accessing settings.",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ----- Backward-compatible public API -----
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Whether persistence is backed by PostgreSQL (true) or disk files (false).
|
|
45
|
+
* Callers that need to know the storage backend (e.g. for diagnostics or
|
|
46
|
+
* doctor output) can use this, but most callers should NOT need to branch
|
|
47
|
+
* on this — both repositories handle their own I/O.
|
|
48
|
+
*/
|
|
49
|
+
export function isDbConfigured(): boolean {
|
|
50
|
+
return !!(process.env.PI_ROTATOR_DATABASE_URL || process.env.DATABASE_URL);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function initDb(): Promise<void> {
|
|
54
|
+
await repository.init();
|
|
55
|
+
initialized = true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function closeDb(): Promise<void> {
|
|
59
|
+
await repository.close();
|
|
60
|
+
initialized = false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --- Accounts config ---
|
|
64
|
+
|
|
65
|
+
export function getCachedConfig(): Config | null {
|
|
66
|
+
assertInitialized();
|
|
67
|
+
const raw = repository.get("accounts_json");
|
|
68
|
+
if (!raw) return null;
|
|
69
|
+
try {
|
|
70
|
+
const parsed = JSON.parse(raw);
|
|
71
|
+
const validation = validateConfig(parsed);
|
|
72
|
+
if (validation.ok && validation.value) {
|
|
73
|
+
return applyConfigDefaults(validation.value);
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error(`Failed to parse accounts config from repository: ${err}`);
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function setCachedConfig(config: Config): void {
|
|
82
|
+
assertInitialized();
|
|
83
|
+
const withDefaults = applyConfigDefaults(config);
|
|
84
|
+
repository.set("accounts_json", JSON.stringify(withDefaults, null, 2));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- Admin token ---
|
|
88
|
+
|
|
89
|
+
export function getCachedAdminToken(): string | null {
|
|
90
|
+
assertInitialized();
|
|
91
|
+
const raw = repository.get("admin_token");
|
|
92
|
+
return raw ? raw.trim() : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function setCachedAdminToken(token: string): void {
|
|
96
|
+
assertInitialized();
|
|
97
|
+
repository.set("admin_token", token.trim());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --- Rotator state ---
|
|
101
|
+
|
|
102
|
+
export function getCachedState(): PersistedState | null {
|
|
103
|
+
assertInitialized();
|
|
104
|
+
const raw = repository.get("rotator_state");
|
|
105
|
+
if (!raw) return null;
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(raw) as PersistedState;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error(`Failed to parse rotator state from repository: ${err}`);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function setCachedState(state: PersistedState): void {
|
|
115
|
+
assertInitialized();
|
|
116
|
+
repository.set("rotator_state", JSON.stringify(state));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// --- Token usage ---
|
|
120
|
+
|
|
121
|
+
export function getCachedTokenUsage(): TokenUsageTiered | null {
|
|
122
|
+
assertInitialized();
|
|
123
|
+
const raw = repository.get("token_usage");
|
|
124
|
+
if (!raw) return null;
|
|
125
|
+
try {
|
|
126
|
+
return JSON.parse(raw) as TokenUsageTiered;
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.error(`Failed to parse token usage from repository: ${err}`);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function setCachedTokenUsage(usage: TokenUsageTiered): void {
|
|
134
|
+
assertInitialized();
|
|
135
|
+
repository.set("token_usage", JSON.stringify(usage));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- Responses store ---
|
|
139
|
+
|
|
140
|
+
export function getCachedResponsesStore(): PersistedResponsesStore | null {
|
|
141
|
+
assertInitialized();
|
|
142
|
+
const raw = repository.get("responses_store");
|
|
143
|
+
if (!raw) return null;
|
|
144
|
+
try {
|
|
145
|
+
return JSON.parse(raw) as PersistedResponsesStore;
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.error(`Failed to parse responses store from repository: ${err}`);
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function setCachedResponsesStore(store: PersistedResponsesStore): void {
|
|
153
|
+
assertInitialized();
|
|
154
|
+
repository.set("responses_store", JSON.stringify(store));
|
|
155
|
+
}
|
package/src/doctor.ts
CHANGED
|
@@ -1,97 +1,115 @@
|
|
|
1
1
|
import { accessSync, constants, existsSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
2
|
import { getConfiguredAdminToken } from "./admin-auth.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
3
|
+
import { getConfigDir } from "./paths.js";
|
|
4
|
+
import { listBackups } from "./storage.js";
|
|
5
|
+
import {
|
|
6
|
+
isDbConfigured,
|
|
7
|
+
getCachedConfig,
|
|
8
|
+
getCachedState,
|
|
9
|
+
getCachedTokenUsage,
|
|
10
|
+
} from "./db-store.js";
|
|
8
11
|
|
|
9
12
|
function checkWritable(path: string): boolean {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
try {
|
|
14
|
+
accessSync(path, constants.R_OK | constants.W_OK);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export interface DoctorResult {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
ok: boolean;
|
|
23
|
+
warnings: string[];
|
|
24
|
+
errors: string[];
|
|
25
|
+
info: Record<string, unknown>;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
export function runDoctor(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const tokenUsagePath = getTokenUsagePath();
|
|
28
|
+
export async function runDoctor(
|
|
29
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
30
|
+
): Promise<DoctorResult> {
|
|
31
|
+
const warnings: string[] = [];
|
|
32
|
+
const errors: string[] = [];
|
|
33
|
+
const configDir = getConfigDir();
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
if (!getConfiguredAdminToken(env)) {
|
|
36
|
+
warnings.push(
|
|
37
|
+
"PI_ROTATOR_ADMIN_TOKEN is not configured; dashboard and /api/* remain open on the bound interface.",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
errors.push(`Config directory missing: ${configDir}`);
|
|
39
|
-
}
|
|
40
|
-
if (!checkWritable(configDir)) {
|
|
41
|
-
errors.push(`Config directory is not writable: ${configDir}`);
|
|
42
|
-
}
|
|
41
|
+
const dbConfigured = isDbConfigured();
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
if (dbConfigured) {
|
|
44
|
+
// When using PostgreSQL, configDir is not required for data persistence
|
|
45
|
+
if (!existsSync(configDir)) {
|
|
46
|
+
warnings.push(
|
|
47
|
+
`Config directory does not exist (${configDir}), but storage backend is PostgreSQL — this is expected in containerised environments.`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
if (!existsSync(configDir)) {
|
|
52
|
+
errors.push(`Config directory missing: ${configDir}`);
|
|
53
|
+
} else if (!checkWritable(configDir)) {
|
|
54
|
+
errors.push(`Config directory is not writable: ${configDir}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
49
57
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
// Validate config — read from repository (handles both DB and file)
|
|
59
|
+
const cfg = getCachedConfig();
|
|
60
|
+
if (!cfg) {
|
|
61
|
+
warnings.push(
|
|
62
|
+
"No accounts config found. Run 'pi-antigravity-rotator login' to add your first account.",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
55
65
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
// Validate state
|
|
67
|
+
const state = getCachedState();
|
|
68
|
+
if (state && typeof state !== "object") {
|
|
69
|
+
warnings.push("Rotator state data is corrupted.");
|
|
70
|
+
}
|
|
61
71
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
// Validate token usage
|
|
73
|
+
const usage = getCachedTokenUsage();
|
|
74
|
+
if (usage && typeof usage !== "object") {
|
|
75
|
+
warnings.push("Token usage data is corrupted.");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const backups = dbConfigured ? [] : listBackups();
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
ok: errors.length === 0,
|
|
82
|
+
warnings,
|
|
83
|
+
errors,
|
|
84
|
+
info: {
|
|
85
|
+
configDir,
|
|
86
|
+
backupCount: backups.length,
|
|
87
|
+
firstBackup: backups[0] ?? null,
|
|
88
|
+
adminTokenConfigured: !!getConfiguredAdminToken(env),
|
|
89
|
+
bindHost: env.PI_ROTATOR_BIND_HOST ?? null,
|
|
90
|
+
storageBackend: dbConfigured ? "postgresql" : "file",
|
|
91
|
+
},
|
|
92
|
+
};
|
|
77
93
|
}
|
|
78
94
|
|
|
79
95
|
export function printDoctorReport(result: DoctorResult): void {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
console.log("Pi Antigravity Rotator Doctor");
|
|
97
|
+
console.log();
|
|
98
|
+
console.log(`Status: ${result.ok ? "OK" : "ERROR"}`);
|
|
99
|
+
console.log();
|
|
100
|
+
for (const [key, value] of Object.entries(result.info)) {
|
|
101
|
+
console.log(
|
|
102
|
+
`${key}: ${typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null ? value : JSON.stringify(value)}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (result.warnings.length > 0) {
|
|
106
|
+
console.log();
|
|
107
|
+
console.log("Warnings:");
|
|
108
|
+
for (const warning of result.warnings) console.log(`- ${warning}`);
|
|
109
|
+
}
|
|
110
|
+
if (result.errors.length > 0) {
|
|
111
|
+
console.log();
|
|
112
|
+
console.log("Errors:");
|
|
113
|
+
for (const error of result.errors) console.log(`- ${error}`);
|
|
114
|
+
}
|
|
97
115
|
}
|