Implement settings service and routes for file-based settings management

- Add SettingsService to handle reading/writing global and project settings.
- Introduce API routes for managing settings, including global settings, credentials, and project-specific settings.
- Implement migration functionality to transfer settings from localStorage to file-based storage.
- Create common utilities for settings routes and integrate logging for error handling.
- Update server entry point to include new settings routes.
This commit is contained in:
Cody Seibert
2025-12-20 01:52:25 -05:00
parent 8fcc6cb4db
commit 0c6447a6f5
42 changed files with 4516 additions and 1984 deletions

View File

@@ -0,0 +1,15 @@
/**
* Common utilities for settings routes
*/
import { createLogger } from "../../lib/logger.js";
import {
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
export const logger = createLogger("Settings");
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };
export const logError = createLogError(logger);

View File

@@ -0,0 +1,38 @@
/**
* Settings routes - HTTP API for persistent file-based settings
*/
import { Router } from "express";
import type { SettingsService } from "../../services/settings-service.js";
import { createGetGlobalHandler } from "./routes/get-global.js";
import { createUpdateGlobalHandler } from "./routes/update-global.js";
import { createGetCredentialsHandler } from "./routes/get-credentials.js";
import { createUpdateCredentialsHandler } from "./routes/update-credentials.js";
import { createGetProjectHandler } from "./routes/get-project.js";
import { createUpdateProjectHandler } from "./routes/update-project.js";
import { createMigrateHandler } from "./routes/migrate.js";
import { createStatusHandler } from "./routes/status.js";
export function createSettingsRoutes(settingsService: SettingsService): Router {
const router = Router();
// Status endpoint (check if migration needed)
router.get("/status", createStatusHandler(settingsService));
// Global settings
router.get("/global", createGetGlobalHandler(settingsService));
router.put("/global", createUpdateGlobalHandler(settingsService));
// Credentials (separate for security)
router.get("/credentials", createGetCredentialsHandler(settingsService));
router.put("/credentials", createUpdateCredentialsHandler(settingsService));
// Project settings
router.post("/project", createGetProjectHandler(settingsService));
router.put("/project", createUpdateProjectHandler(settingsService));
// Migration from localStorage
router.post("/migrate", createMigrateHandler(settingsService));
return router;
}

View File

@@ -0,0 +1,23 @@
/**
* GET /api/settings/credentials - Get credentials (masked for security)
*/
import type { Request, Response } from "express";
import type { SettingsService } from "../../../services/settings-service.js";
import { getErrorMessage, logError } from "../common.js";
export function createGetCredentialsHandler(settingsService: SettingsService) {
return async (_req: Request, res: Response): Promise<void> => {
try {
const credentials = await settingsService.getMaskedCredentials();
res.json({
success: true,
credentials,
});
} catch (error) {
logError(error, "Get credentials failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,23 @@
/**
* GET /api/settings/global - Get global settings
*/
import type { Request, Response } from "express";
import type { SettingsService } from "../../../services/settings-service.js";
import { getErrorMessage, logError } from "../common.js";
export function createGetGlobalHandler(settingsService: SettingsService) {
return async (_req: Request, res: Response): Promise<void> => {
try {
const settings = await settingsService.getGlobalSettings();
res.json({
success: true,
settings,
});
} catch (error) {
logError(error, "Get global settings failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,34 @@
/**
* POST /api/settings/project - Get project settings
* Uses POST because projectPath may contain special characters
*/
import type { Request, Response } from "express";
import type { SettingsService } from "../../../services/settings-service.js";
import { getErrorMessage, logError } from "../common.js";
export function createGetProjectHandler(settingsService: SettingsService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath } = req.body as { projectPath?: string };
if (!projectPath || typeof projectPath !== "string") {
res.status(400).json({
success: false,
error: "projectPath is required",
});
return;
}
const settings = await settingsService.getProjectSettings(projectPath);
res.json({
success: true,
settings,
});
} catch (error) {
logError(error, "Get project settings failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,54 @@
/**
* POST /api/settings/migrate - Migrate settings from localStorage
*/
import type { Request, Response } from "express";
import type { SettingsService } from "../../../services/settings-service.js";
import { getErrorMessage, logError, logger } from "../common.js";
export function createMigrateHandler(settingsService: SettingsService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { data } = req.body as {
data?: {
"automaker-storage"?: string;
"automaker-setup"?: string;
"worktree-panel-collapsed"?: string;
"file-browser-recent-folders"?: string;
"automaker:lastProjectDir"?: string;
};
};
if (!data || typeof data !== "object") {
res.status(400).json({
success: false,
error: "data object is required containing localStorage data",
});
return;
}
logger.info("Starting settings migration from localStorage");
const result = await settingsService.migrateFromLocalStorage(data);
if (result.success) {
logger.info(
`Migration successful: ${result.migratedProjectCount} projects migrated`
);
} else {
logger.warn(`Migration completed with errors: ${result.errors.join(", ")}`);
}
res.json({
success: result.success,
migratedGlobalSettings: result.migratedGlobalSettings,
migratedCredentials: result.migratedCredentials,
migratedProjectCount: result.migratedProjectCount,
errors: result.errors,
});
} catch (error) {
logError(error, "Migration failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,28 @@
/**
* GET /api/settings/status - Get settings migration status
* Returns whether settings files exist (to determine if migration is needed)
*/
import type { Request, Response } from "express";
import type { SettingsService } from "../../../services/settings-service.js";
import { getErrorMessage, logError } from "../common.js";
export function createStatusHandler(settingsService: SettingsService) {
return async (_req: Request, res: Response): Promise<void> => {
try {
const hasGlobalSettings = await settingsService.hasGlobalSettings();
const hasCredentials = await settingsService.hasCredentials();
res.json({
success: true,
hasGlobalSettings,
hasCredentials,
dataDir: settingsService.getDataDir(),
needsMigration: !hasGlobalSettings,
});
} catch (error) {
logError(error, "Get settings status failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,39 @@
/**
* PUT /api/settings/credentials - Update credentials
*/
import type { Request, Response } from "express";
import type { SettingsService } from "../../../services/settings-service.js";
import type { Credentials } from "../../../types/settings.js";
import { getErrorMessage, logError } from "../common.js";
export function createUpdateCredentialsHandler(
settingsService: SettingsService
) {
return async (req: Request, res: Response): Promise<void> => {
try {
const updates = req.body as Partial<Credentials>;
if (!updates || typeof updates !== "object") {
res.status(400).json({
success: false,
error: "Invalid request body - expected credentials object",
});
return;
}
await settingsService.updateCredentials(updates);
// Return masked credentials for confirmation
const masked = await settingsService.getMaskedCredentials();
res.json({
success: true,
credentials: masked,
});
} catch (error) {
logError(error, "Update credentials failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,34 @@
/**
* PUT /api/settings/global - Update global settings
*/
import type { Request, Response } from "express";
import type { SettingsService } from "../../../services/settings-service.js";
import type { GlobalSettings } from "../../../types/settings.js";
import { getErrorMessage, logError } from "../common.js";
export function createUpdateGlobalHandler(settingsService: SettingsService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const updates = req.body as Partial<GlobalSettings>;
if (!updates || typeof updates !== "object") {
res.status(400).json({
success: false,
error: "Invalid request body - expected settings object",
});
return;
}
const settings = await settingsService.updateGlobalSettings(updates);
res.json({
success: true,
settings,
});
} catch (error) {
logError(error, "Update global settings failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,48 @@
/**
* PUT /api/settings/project - Update project settings
*/
import type { Request, Response } from "express";
import type { SettingsService } from "../../../services/settings-service.js";
import type { ProjectSettings } from "../../../types/settings.js";
import { getErrorMessage, logError } from "../common.js";
export function createUpdateProjectHandler(settingsService: SettingsService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, updates } = req.body as {
projectPath?: string;
updates?: Partial<ProjectSettings>;
};
if (!projectPath || typeof projectPath !== "string") {
res.status(400).json({
success: false,
error: "projectPath is required",
});
return;
}
if (!updates || typeof updates !== "object") {
res.status(400).json({
success: false,
error: "updates object is required",
});
return;
}
const settings = await settingsService.updateProjectSettings(
projectPath,
updates
);
res.json({
success: true,
settings,
});
} catch (error) {
logError(error, "Update project settings failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}