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.
@@ -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 { getAccountsPath, getConfigDir, getStatePath } from "./paths.js";
5
- import { getTokenUsagePath, loadConfigFromDisk } from "./account-store.js";
6
- import { listBackups, readJsonFile } from "./storage.js";
7
- import type { PersistedState, TokenUsageTiered } from "./types.js";
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
- try {
11
- accessSync(path, constants.R_OK | constants.W_OK);
12
- return true;
13
- } catch {
14
- return false;
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
- ok: boolean;
20
- warnings: string[];
21
- errors: string[];
22
- info: Record<string, unknown>;
22
+ ok: boolean;
23
+ warnings: string[];
24
+ errors: string[];
25
+ info: Record<string, unknown>;
23
26
  }
24
27
 
25
- export function runDoctor(env: NodeJS.ProcessEnv = process.env): DoctorResult {
26
- const warnings: string[] = [];
27
- const errors: string[] = [];
28
- const configDir = getConfigDir();
29
- const accountsPath = getAccountsPath();
30
- const statePath = getStatePath();
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
- if (!getConfiguredAdminToken(env)) {
34
- warnings.push("PI_ROTATOR_ADMIN_TOKEN is not configured; dashboard and /api/* remain open on the bound interface.");
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
- if (!existsSync(configDir)) {
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
- try {
45
- loadConfigFromDisk();
46
- } catch (err) {
47
- errors.push(`Config validation failed: ${err instanceof Error ? err.message : String(err)}`);
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
- try {
51
- if (existsSync(statePath)) readJsonFile<PersistedState>(statePath);
52
- } catch (err) {
53
- errors.push(`State file is corrupted: ${err instanceof Error ? err.message : String(err)}`);
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
- try {
57
- if (existsSync(tokenUsagePath)) readJsonFile<TokenUsageTiered>(tokenUsagePath);
58
- } catch (err) {
59
- errors.push(`Token usage file is corrupted: ${err instanceof Error ? err.message : String(err)}`);
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
- return {
63
- ok: errors.length === 0,
64
- warnings,
65
- errors,
66
- info: {
67
- configDir,
68
- accountsPath,
69
- statePath,
70
- tokenUsagePath,
71
- backupCount: listBackups().length,
72
- firstBackup: listBackups()[0] ?? null,
73
- adminTokenConfigured: !!getConfiguredAdminToken(env),
74
- bindHost: env.PI_ROTATOR_BIND_HOST ?? null,
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
- console.log("Pi Antigravity Rotator Doctor");
81
- console.log();
82
- console.log(`Status: ${result.ok ? "OK" : "ERROR"}`);
83
- console.log();
84
- for (const [key, value] of Object.entries(result.info)) {
85
- console.log(`${key}: ${typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null ? value : JSON.stringify(value)}`);
86
- }
87
- if (result.warnings.length > 0) {
88
- console.log();
89
- console.log("Warnings:");
90
- for (const warning of result.warnings) console.log(`- ${warning}`);
91
- }
92
- if (result.errors.length > 0) {
93
- console.log();
94
- console.log("Errors:");
95
- for (const error of result.errors) console.log(`- ${error}`);
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
  }