codestate-core
Version:
Core domain models, use cases, and services for CodeState
1,514 lines (1,500 loc) • 126 kB
JavaScript
// packages/core/domain/models/Result.ts
function isSuccess(result) {
return result.ok === true;
}
function isFailure(result) {
return result.ok === false;
}
// packages/core/services/config/ConfigService.ts
var ConfigService = class {
constructor(repository, logger) {
this.repository = repository;
this.logger = logger;
}
async getConfig() {
this.logger.debug("ConfigService.getConfig called");
const result = await this.repository.load();
if (isFailure(result)) {
this.logger.error("Failed to get config", { error: result.error });
} else {
this.logger.log("Config loaded", {});
}
return result;
}
async setConfig(config) {
this.logger.debug("ConfigService.setConfig called");
const result = await this.repository.save(config);
if (isFailure(result)) {
this.logger.error("Failed to save config", { error: result.error });
} else {
this.logger.log("Config saved", {});
}
return result;
}
async updateConfig(partial) {
this.logger.debug("ConfigService.updateConfig called", { partial });
const current = await this.repository.load();
if (isFailure(current)) {
this.logger.error("Failed to load config for update", { error: current.error });
return current;
}
const merged = { ...current.value, ...partial };
const saveResult = await this.repository.save(merged);
if (isFailure(saveResult)) {
this.logger.error("Failed to save updated config", { error: saveResult.error });
return { ok: false, error: saveResult.error };
}
this.logger.log("Config updated", {});
return { ok: true, value: merged };
}
};
// packages/core/domain/schemas/SchemaRegistry.ts
import { z } from "zod";
var LogLevelSchema = z.enum(["ERROR", "WARN", "LOG", "DEBUG"]);
var LoggerConfigSchema = z.object({
level: LogLevelSchema,
sinks: z.array(z.enum(["console", "file"])),
filePath: z.string().optional()
});
var FileStorageConfigSchema = z.object({
encryptionEnabled: z.boolean(),
encryptionKey: z.string().optional(),
dataDir: z.string()
});
var FeatureFlagsSchema = z.object({
experimentalTui: z.boolean(),
experimentalIde: z.boolean(),
advancedSearch: z.boolean(),
cloudSync: z.boolean()
});
var PluginEnvironmentSchema = z.enum(["cli", "tui", "ide"]);
var ErrorCodeSchema = z.enum([
"UNKNOWN",
"CONFIG_INVALID",
"STORAGE_INVALID_PATH",
"STORAGE_DECRYPTION_FAILED",
"STORAGE_READ_FAILED",
"STORAGE_WRITE_FAILED",
"STORAGE_DELETE_FAILED",
"ENCRYPTION_FAILED",
"ENCRYPTION_INVALID_FORMAT",
"SCRIPT_INVALID",
"SCRIPT_DUPLICATE",
"SCRIPT_NOT_FOUND",
"SCRIPT_PATH_INVALID",
"SCRIPT_MALICIOUS",
"GIT_NOT_REPOSITORY",
"GIT_COMMAND_FAILED",
"GIT_STASH_NOT_FOUND",
"GIT_STASH_CONFLICT",
"TERMINAL_COMMAND_FAILED",
"TERMINAL_TIMEOUT",
"TERMINAL_COMMAND_NOT_FOUND"
]);
var EncryptionConfigSchema = z.object({
enabled: z.boolean(),
encryptionKey: z.string().optional()
});
var ConfigSchema = z.object({
version: z.string(),
ide: z.string(),
encryption: EncryptionConfigSchema,
storagePath: z.string(),
logger: LoggerConfigSchema,
experimental: z.record(z.string(), z.boolean()).optional(),
extensions: z.record(z.string(), z.unknown()).optional()
});
var ScriptSchema = z.object({
name: z.string().min(1, "Script name is required"),
rootPath: z.string().min(1, "Root path is required"),
script: z.string().min(1, "Script command is required")
});
var ScriptIndexEntrySchema = z.object({
rootPath: z.string().min(1, "Root path is required"),
referenceFile: z.string().min(1, "Reference file path is required")
});
var ScriptIndexSchema = z.object({
entries: z.array(ScriptIndexEntrySchema)
});
var ScriptCollectionSchema = z.object({
scripts: z.array(ScriptSchema)
});
var GitFileStatusSchema = z.enum(["modified", "added", "deleted", "untracked", "renamed", "copied", "updated"]);
var GitFileSchema = z.object({
path: z.string(),
status: GitFileStatusSchema,
staged: z.boolean()
});
var GitStatusSchema = z.object({
isDirty: z.boolean(),
dirtyFiles: z.array(GitFileSchema),
newFiles: z.array(GitFileSchema),
modifiedFiles: z.array(GitFileSchema),
deletedFiles: z.array(GitFileSchema),
untrackedFiles: z.array(GitFileSchema)
});
var GitStashSchema = z.object({
id: z.string(),
name: z.string(),
message: z.string(),
timestamp: z.number(),
branch: z.string()
});
var GitStashResultSchema = z.object({
success: z.boolean(),
stashId: z.string().optional(),
error: z.string().optional()
});
var GitStashApplyResultSchema = z.object({
success: z.boolean(),
conflicts: z.array(z.string()).optional(),
error: z.string().optional()
});
var TerminalCommandSchema = z.object({
command: z.string(),
args: z.array(z.string()).optional(),
cwd: z.string().optional(),
env: z.record(z.string(), z.string()).optional(),
timeout: z.number().optional()
});
var TerminalResultSchema = z.object({
success: z.boolean(),
exitCode: z.number(),
stdout: z.string(),
stderr: z.string(),
duration: z.number(),
error: z.string().optional()
});
var TerminalOptionsSchema = z.object({
cwd: z.string().optional(),
env: z.record(z.string(), z.string()).optional(),
timeout: z.number().optional(),
shell: z.string().optional()
});
var FileStateSchema = z.object({
path: z.string(),
cursor: z.object({ line: z.number(), column: z.number() }).optional(),
scroll: z.object({ top: z.number(), left: z.number() }).optional(),
isActive: z.boolean()
});
var GitStateSchema = z.object({
branch: z.string(),
commit: z.string(),
isDirty: z.boolean(),
stashId: z.string().nullable().optional()
});
var SessionSchema = z.object({
id: z.string(),
name: z.string(),
projectRoot: z.string(),
createdAt: z.union([z.string(), z.date()]).transform((val) => typeof val === "string" ? new Date(val) : val),
updatedAt: z.union([z.string(), z.date()]).transform((val) => typeof val === "string" ? new Date(val) : val),
tags: z.array(z.string()),
notes: z.string().optional(),
files: z.array(FileStateSchema),
git: GitStateSchema,
extensions: z.record(z.string(), z.unknown()).optional()
});
var SessionIndexEntrySchema = z.object({
id: z.string(),
name: z.string(),
projectRoot: z.string(),
createdAt: z.union([z.string(), z.date()]),
updatedAt: z.union([z.string(), z.date()]),
tags: z.array(z.string()),
notes: z.string().optional(),
referenceFile: z.string()
});
var SessionIndexSchema = z.object({
version: z.string(),
sessions: z.array(SessionIndexEntrySchema)
});
function validateFileStorageConfig(data) {
return FileStorageConfigSchema.parse(data);
}
function validateConfig(data) {
return ConfigSchema.parse(data);
}
function validateScript(data) {
return ScriptSchema.parse(data);
}
function validateScriptIndex(data) {
return ScriptIndexSchema.parse(data);
}
function validateScriptCollection(data) {
return ScriptCollectionSchema.parse(data);
}
function validateSession(data) {
return SessionSchema.parse(data);
}
function validateSessionIndex(data) {
return SessionIndexSchema.parse(data);
}
// packages/core/domain/types/ErrorTypes.ts
var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
ErrorCode2["UNKNOWN"] = "UNKNOWN";
ErrorCode2["CONFIG_INVALID"] = "CONFIG_INVALID";
ErrorCode2["STORAGE_INVALID_PATH"] = "STORAGE_INVALID_PATH";
ErrorCode2["STORAGE_DECRYPTION_FAILED"] = "STORAGE_DECRYPTION_FAILED";
ErrorCode2["STORAGE_READ_FAILED"] = "STORAGE_READ_FAILED";
ErrorCode2["STORAGE_WRITE_FAILED"] = "STORAGE_WRITE_FAILED";
ErrorCode2["STORAGE_DELETE_FAILED"] = "STORAGE_DELETE_FAILED";
ErrorCode2["ENCRYPTION_FAILED"] = "ENCRYPTION_FAILED";
ErrorCode2["ENCRYPTION_INVALID_FORMAT"] = "ENCRYPTION_INVALID_FORMAT";
ErrorCode2["SCRIPT_INVALID"] = "SCRIPT_INVALID";
ErrorCode2["SCRIPT_DUPLICATE"] = "SCRIPT_DUPLICATE";
ErrorCode2["SCRIPT_NOT_FOUND"] = "SCRIPT_NOT_FOUND";
ErrorCode2["SCRIPT_PATH_INVALID"] = "SCRIPT_PATH_INVALID";
ErrorCode2["SCRIPT_MALICIOUS"] = "SCRIPT_MALICIOUS";
ErrorCode2["GIT_NOT_REPOSITORY"] = "GIT_NOT_REPOSITORY";
ErrorCode2["GIT_COMMAND_FAILED"] = "GIT_COMMAND_FAILED";
ErrorCode2["GIT_STASH_NOT_FOUND"] = "GIT_STASH_NOT_FOUND";
ErrorCode2["GIT_STASH_CONFLICT"] = "GIT_STASH_CONFLICT";
ErrorCode2["TERMINAL_COMMAND_FAILED"] = "TERMINAL_COMMAND_FAILED";
ErrorCode2["TERMINAL_TIMEOUT"] = "TERMINAL_TIMEOUT";
ErrorCode2["TERMINAL_COMMAND_NOT_FOUND"] = "TERMINAL_COMMAND_NOT_FOUND";
return ErrorCode2;
})(ErrorCode || {});
var AppError = class extends Error {
constructor(message, code = "UNKNOWN" /* UNKNOWN */, meta) {
super(message);
this.name = "AppError";
this.code = code;
this.meta = meta;
}
};
var ConfigError = class extends AppError {
constructor(message, meta) {
super(message, "CONFIG_INVALID" /* CONFIG_INVALID */, meta);
this.name = "ConfigError";
}
};
var StorageError = class extends AppError {
constructor(message, code = "STORAGE_READ_FAILED" /* STORAGE_READ_FAILED */, meta) {
super(message, code, meta);
this.name = "StorageError";
}
};
var EncryptionError = class extends AppError {
constructor(message, code = "ENCRYPTION_FAILED" /* ENCRYPTION_FAILED */, meta) {
super(message, code, meta);
this.name = "EncryptionError";
}
};
var ScriptError = class extends AppError {
constructor(message, code = "SCRIPT_INVALID" /* SCRIPT_INVALID */, meta) {
super(message, code, meta);
this.name = "ScriptError";
}
};
var GitError = class extends AppError {
constructor(message, code = "GIT_COMMAND_FAILED" /* GIT_COMMAND_FAILED */, meta) {
super(message, code, meta);
this.name = "GitError";
}
};
var TerminalError = class extends AppError {
constructor(message, code = "TERMINAL_COMMAND_FAILED" /* TERMINAL_COMMAND_FAILED */, meta) {
super(message, code, meta);
this.name = "TerminalError";
}
};
// packages/core/infrastructure/repositories/ConfigRepository.ts
import * as fs from "fs/promises";
import * as path from "path";
var DEFAULT_CONFIG_PATH = path.join(
process.env.HOME || process.env.USERPROFILE || ".",
".codestate",
"config.json"
);
var TEMP_SUFFIX = ".tmp";
var BACKUP_SUFFIX = ".bak";
function getDefaultConfig() {
return {
version: "1.0.0",
ide: "vscode",
encryption: { enabled: false },
storagePath: path.join(
process.env.HOME || process.env.USERPROFILE || ".",
".codestate"
),
logger: {
level: "LOG",
sinks: ["file"],
filePath: path.join(
process.env.HOME || process.env.USERPROFILE || ".",
".codestate",
"logs",
"codestate.log"
)
},
experimental: {},
extensions: {}
};
}
var ConfigRepository = class {
constructor(logger, encryption, configPath = DEFAULT_CONFIG_PATH) {
this.logger = logger;
this.encryption = encryption;
this.configPath = configPath;
}
async load() {
try {
await this.ensureDir();
this.logger.debug("Attempting to load config", { path: this.configPath });
const raw = await fs.readFile(this.configPath, { encoding: "utf8" });
let data = raw;
if (raw.startsWith("ENCRYPTED_v1")) {
this.logger.log("Config file is encrypted. Attempting decryption.", {
path: this.configPath
});
const key = "";
const decrypted = await this.encryption.decrypt(raw, key);
if (isFailure(decrypted)) {
this.logger.error("Decryption failed", { error: decrypted.error });
return { ok: false, error: decrypted.error };
}
data = decrypted.value;
}
let parsed;
try {
parsed = JSON.parse(data);
} catch (parseErr) {
this.logger.error(
"Config file is corrupt (invalid JSON). Backing up and creating default.",
{ path: this.configPath }
);
await this.backupCorruptConfig();
const defaults = getDefaultConfig();
await this.save(defaults);
return { ok: true, value: defaults };
}
let config;
try {
config = validateConfig(parsed);
} catch (validationErr) {
this.logger.error(
"Config file is corrupt (schema validation failed). Backing up and creating default.",
{ path: this.configPath }
);
await this.backupCorruptConfig();
const defaults = getDefaultConfig();
await this.save(defaults);
return { ok: true, value: defaults };
}
this.logger.log("Config loaded successfully", {
path: this.configPath,
encrypted: raw.startsWith("ENCRYPTED_v1")
});
return { ok: true, value: config };
} catch (err) {
if (err.code === "ENOENT") {
this.logger.warn("Config file not found. Creating default config.", {
path: this.configPath
});
const defaults = getDefaultConfig();
await this.save(defaults);
return { ok: true, value: defaults };
}
this.logger.error("Failed to load config", {
error: err.message,
path: this.configPath
});
return { ok: false, error: new ConfigError(err.message) };
}
}
async save(config) {
try {
await this.ensureDir();
this.logger.debug("Attempting to save config", { path: this.configPath });
const validated = validateConfig(config);
let data = JSON.stringify(validated, null, 2);
let encrypted = false;
if (config.encryption?.enabled && config.encryption.encryptionKey) {
this.logger.log("Encrypting config before save", {
path: this.configPath
});
const encResult = await this.encryption.encrypt(
data,
config.encryption.encryptionKey
);
if (isFailure(encResult)) {
this.logger.error("Encryption failed", { error: encResult.error });
return { ok: false, error: encResult.error };
}
data = encResult.value;
encrypted = true;
}
const tempPath = this.configPath + TEMP_SUFFIX;
await fs.writeFile(tempPath, data, { encoding: "utf8", mode: 384 });
this.logger.debug("Temp config file written", { tempPath });
await fs.rename(this.configPath, this.configPath + BACKUP_SUFFIX).then(() => {
this.logger.log("Config backup created", {
backupPath: this.configPath + BACKUP_SUFFIX
});
}).catch(() => {
});
await fs.rename(tempPath, this.configPath);
this.logger.log("Config saved successfully", {
path: this.configPath,
encrypted
});
return { ok: true, value: void 0 };
} catch (err) {
this.logger.error("Failed to save config", {
error: err.message,
path: this.configPath
});
return { ok: false, error: new ConfigError(err.message) };
}
}
async ensureDir() {
const dir = path.dirname(this.configPath);
await fs.mkdir(dir, { recursive: true, mode: 448 }).then(() => {
this.logger.debug("Ensured config directory exists", { dir });
}).catch(() => {
});
}
async backupCorruptConfig() {
try {
const backupPath = this.configPath + ".bak." + Date.now();
await fs.rename(this.configPath, backupPath);
this.logger.warn("Backed up corrupt config file", { backupPath });
} catch (err) {
this.logger.error("Failed to backup corrupt config file", {
error: err.message
});
}
}
};
// packages/core/infrastructure/services/FileLogger.ts
import { appendFileSync, mkdirSync } from "fs";
import * as path2 from "path";
var LOG_LEVEL_PRIORITY = {
"ERROR": 0,
"WARN": 1,
"LOG": 2,
"DEBUG": 3
};
var FileLogger = class {
constructor(config) {
if (!config.filePath)
throw new Error("FileLogger requires filePath in LoggerConfig");
this.level = config.level;
this.filePath = config.filePath;
this.ensureLogDirectory();
}
plainLog(message, meta) {
const entry = {
level: "plain",
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
message,
...meta ? { meta } : {}
};
appendFileSync(this.filePath, JSON.stringify(entry) + "\n", { encoding: "utf8" });
}
ensureLogDirectory() {
const logDir = path2.dirname(this.filePath);
try {
mkdirSync(logDir, { recursive: true });
} catch (error) {
}
}
shouldLog(messageLevel) {
return LOG_LEVEL_PRIORITY[this.level] >= LOG_LEVEL_PRIORITY[messageLevel];
}
write(level, message, meta) {
const entry = {
level,
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
message,
...meta ? { meta } : {}
};
appendFileSync(this.filePath, JSON.stringify(entry) + "\n", { encoding: "utf8" });
}
log(message, meta) {
if (!this.shouldLog("LOG"))
return;
this.write("log", message, meta);
}
error(message, meta) {
if (!this.shouldLog("ERROR"))
return;
this.write("error", message, meta);
}
warn(message, meta) {
if (!this.shouldLog("WARN"))
return;
this.write("warn", message, meta);
}
debug(message, meta) {
if (!this.shouldLog("DEBUG"))
return;
this.write("debug", message, meta);
}
};
// packages/core/infrastructure/services/BasicEncryption.ts
import {
createCipheriv,
createDecipheriv,
pbkdf2Sync,
randomBytes
} from "crypto";
var HEADER = "ENCRYPTED_v1";
var SALT_LENGTH = 16;
var IV_LENGTH = 12;
var KEY_LENGTH = 32;
var PBKDF2_ITER = 1e5;
var BasicEncryption = class {
constructor(logger) {
this.logger = logger;
}
async encrypt(data, key) {
try {
const salt = randomBytes(SALT_LENGTH);
const iv = randomBytes(IV_LENGTH);
const derivedKey = pbkdf2Sync(
key,
salt,
PBKDF2_ITER,
KEY_LENGTH,
"sha512"
);
const cipher = createCipheriv("aes-256-gcm", derivedKey, iv);
const ciphertext = Buffer.concat([
cipher.update(data, "utf8"),
cipher.final()
]);
const authTag = cipher.getAuthTag();
this.logger.debug("Data encrypted", {
algorithm: "AES-256-GCM",
operation: "encrypt"
});
return {
ok: true,
value: [
HEADER,
iv.toString("base64"),
salt.toString("base64"),
ciphertext.toString("base64"),
authTag.toString("base64")
].join(":")
};
} catch (err) {
this.logger.error("Encryption failed", {
error: err instanceof Error ? err.message : err,
operation: "encrypt"
});
return {
ok: false,
error: new EncryptionError(
"Encryption failed",
"ENCRYPTION_FAILED" /* ENCRYPTION_FAILED */,
{
originalError: err instanceof Error ? err.message : err,
operation: "encrypt"
}
)
};
}
}
async decrypt(data, key) {
try {
const parts = data.split(":");
if (parts[0] !== HEADER || parts.length !== 5) {
this.logger.error("Invalid encrypted data format", {
operation: "decrypt"
});
return {
ok: false,
error: new EncryptionError(
"Invalid encrypted data format",
"ENCRYPTION_INVALID_FORMAT" /* ENCRYPTION_INVALID_FORMAT */,
{ operation: "decrypt" }
)
};
}
const [, ivB64, saltB64, ciphertextB64, authTagB64] = parts;
const iv = Buffer.from(ivB64, "base64");
const salt = Buffer.from(saltB64, "base64");
const ciphertext = Buffer.from(ciphertextB64, "base64");
const authTag = Buffer.from(authTagB64, "base64");
const derivedKey = pbkdf2Sync(
key,
salt,
PBKDF2_ITER,
KEY_LENGTH,
"sha512"
);
const decipher = createDecipheriv("aes-256-gcm", derivedKey, iv);
decipher.setAuthTag(authTag);
const plaintext = Buffer.concat([
decipher.update(ciphertext),
decipher.final()
]);
this.logger.debug("Data decrypted", {
algorithm: "AES-256-GCM",
operation: "decrypt"
});
return { ok: true, value: plaintext.toString("utf8") };
} catch (err) {
this.logger.error("Decryption failed", {
error: err instanceof Error ? err.message : err,
operation: "decrypt"
});
return {
ok: false,
error: new EncryptionError(
"Decryption failed",
"ENCRYPTION_FAILED" /* ENCRYPTION_FAILED */,
{
originalError: err instanceof Error ? err.message : err,
operation: "decrypt"
}
)
};
}
}
};
// packages/core/services/config/ConfigFacade.ts
import * as path3 from "path";
var ConfigFacade = class {
constructor(configPath, logger, encryption) {
const _logger = logger || new FileLogger({
level: "LOG",
sinks: ["file"],
filePath: path3.join(process.env.HOME || process.env.USERPROFILE || ".", ".codestate", "logs", "codestate.log")
});
const _encryption = encryption || new BasicEncryption(_logger);
const repository = new ConfigRepository(_logger, _encryption, configPath);
this.service = new ConfigService(repository, _logger);
}
async getConfig(...args) {
return this.service.getConfig(...args);
}
async setConfig(...args) {
return this.service.setConfig(...args);
}
async updateConfig(...args) {
return this.service.updateConfig(...args);
}
};
// packages/core/use-cases/config/GetConfig.ts
var GetConfig = class {
constructor(configService) {
this.configService = configService || new ConfigFacade();
}
async execute() {
return this.configService.getConfig();
}
};
// packages/core/use-cases/config/UpdateConfig.ts
var UpdateConfig = class {
constructor(configService) {
this.configService = configService || new ConfigFacade();
}
async execute(partial) {
return this.configService.updateConfig(partial);
}
};
// packages/core/use-cases/config/ResetConfig.ts
import * as path4 from "path";
function getDefaultConfig2() {
return {
version: "1.0.0",
ide: "vscode",
encryption: { enabled: false },
storagePath: path4.join(process.env.HOME || process.env.USERPROFILE || ".", ".codestate"),
logger: {
level: "LOG",
sinks: ["file"],
filePath: path4.join(process.env.HOME || process.env.USERPROFILE || ".", ".codestate", "logs", "codestate.log")
},
experimental: {},
extensions: {}
};
}
var ResetConfig = class {
constructor(configService) {
this.configService = configService || new ConfigFacade();
}
async execute() {
const result = await this.configService.setConfig(getDefaultConfig2());
if (isFailure(result))
return { ok: false, error: result.error };
return { ok: true, value: getDefaultConfig2() };
}
};
// packages/core/use-cases/config/ExportConfig.ts
var ExportConfig = class {
constructor(configService) {
this.configService = configService || new ConfigFacade();
}
async execute() {
const result = await this.configService.getConfig();
if (isFailure(result))
return { ok: false, error: result.error };
return { ok: true, value: JSON.stringify(result.value, null, 2) };
}
};
// packages/core/use-cases/config/ImportConfig.ts
var ImportConfig = class {
constructor(configService) {
this.configService = configService || new ConfigFacade();
}
async execute(json) {
let parsed;
try {
parsed = validateConfig(JSON.parse(json));
} catch (err) {
return { ok: false, error: err };
}
const result = await this.configService.setConfig(parsed);
if (isFailure(result))
return { ok: false, error: result.error };
return { ok: true, value: parsed };
}
};
// packages/core/services/scripts/ScriptService.ts
var ScriptService = class {
constructor(repository, logger) {
this.repository = repository;
this.logger = logger;
}
async createScript(script) {
this.logger.debug("ScriptService.createScript called", { script });
const result = await this.repository.createScript(script);
if (isFailure(result)) {
this.logger.error("Failed to create script", { error: result.error, script });
} else {
this.logger.log("Script created successfully", { script });
}
return result;
}
async createScripts(scripts) {
this.logger.debug("ScriptService.createScripts called", { count: scripts.length });
const result = await this.repository.createScripts(scripts);
if (isFailure(result)) {
this.logger.error("Failed to create scripts", { error: result.error, count: scripts.length });
} else {
this.logger.log("Scripts created successfully", { count: scripts.length });
}
return result;
}
async getScriptsByRootPath(rootPath) {
this.logger.debug("ScriptService.getScriptsByRootPath called", { rootPath });
const result = await this.repository.getScriptsByRootPath(rootPath);
if (isFailure(result)) {
this.logger.error("Failed to get scripts by root path", { error: result.error, rootPath });
} else {
this.logger.log("Scripts retrieved by root path", { rootPath, count: result.value.length });
}
return result;
}
async getAllScripts() {
this.logger.debug("ScriptService.getAllScripts called");
const result = await this.repository.getAllScripts();
if (isFailure(result)) {
this.logger.error("Failed to get all scripts", { error: result.error });
} else {
this.logger.log("All scripts retrieved", { count: result.value.length });
}
return result;
}
async updateScript(name, rootPath, scriptUpdate) {
this.logger.debug("ScriptService.updateScript called", { name, rootPath, scriptUpdate });
const result = await this.repository.updateScript(name, rootPath, scriptUpdate);
if (isFailure(result)) {
this.logger.error("Failed to update script", { error: result.error, name, rootPath });
} else {
this.logger.log("Script updated successfully", { name, rootPath });
}
return result;
}
async updateScripts(updates) {
this.logger.debug("ScriptService.updateScripts called", { count: updates.length });
const result = await this.repository.updateScripts(updates);
if (isFailure(result)) {
this.logger.error("Failed to update scripts", { error: result.error, count: updates.length });
} else {
this.logger.log("Scripts updated successfully", { count: updates.length });
}
return result;
}
async deleteScript(name, rootPath) {
this.logger.debug("ScriptService.deleteScript called", { name, rootPath });
const result = await this.repository.deleteScript(name, rootPath);
if (isFailure(result)) {
this.logger.error("Failed to delete script", { error: result.error, name, rootPath });
} else {
this.logger.log("Script deleted successfully", { name, rootPath });
}
return result;
}
async deleteScripts(scripts) {
this.logger.debug("ScriptService.deleteScripts called", { count: scripts.length });
const result = await this.repository.deleteScripts(scripts);
if (isFailure(result)) {
this.logger.error("Failed to delete scripts", { error: result.error, count: scripts.length });
} else {
this.logger.log("Scripts deleted successfully", { count: scripts.length });
}
return result;
}
async deleteScriptsByRootPath(rootPath) {
this.logger.debug("ScriptService.deleteScriptsByRootPath called", { rootPath });
const result = await this.repository.deleteScriptsByRootPath(rootPath);
if (isFailure(result)) {
this.logger.error("Failed to delete scripts by root path", { error: result.error, rootPath });
} else {
this.logger.log("Scripts deleted by root path successfully", { rootPath });
}
return result;
}
async getScriptIndex() {
this.logger.debug("ScriptService.getScriptIndex called");
const result = await this.repository.loadScriptIndex();
if (isFailure(result)) {
this.logger.error("Failed to get script index", { error: result.error });
} else {
this.logger.log("Script index retrieved", { entryCount: result.value.entries.length });
}
return result;
}
async updateScriptIndex(index) {
this.logger.debug("ScriptService.updateScriptIndex called");
const result = await this.repository.saveScriptIndex(index);
if (isFailure(result)) {
this.logger.error("Failed to update script index", { error: result.error });
} else {
this.logger.log("Script index updated successfully");
}
return result;
}
};
// packages/core/infrastructure/repositories/ScriptRepository.ts
import * as fs2 from "fs/promises";
import * as path5 from "path";
var DEFAULT_SCRIPTS_DIR = path5.join(
process.env.HOME || process.env.USERPROFILE || ".",
".codestate",
"scripts"
);
var INDEX_FILE = "index.json";
var TEMP_SUFFIX2 = ".tmp";
var BACKUP_SUFFIX2 = ".bak";
var ScriptRepository = class {
constructor(logger, encryption, configService, scriptsDir = DEFAULT_SCRIPTS_DIR) {
this.logger = logger;
this.encryption = encryption;
this.configService = configService;
this.scriptsDir = scriptsDir;
}
async createScript(script) {
try {
await this.ensureScriptsDir();
const validatedScript = validateScript(script);
if (!await this.pathExists(validatedScript.rootPath)) {
this.logger.error("Root path does not exist", {
rootPath: validatedScript.rootPath
});
return {
ok: false,
error: new ScriptError(
"Root path does not exist",
"SCRIPT_PATH_INVALID" /* SCRIPT_PATH_INVALID */,
{ rootPath: validatedScript.rootPath }
)
};
}
const existingScripts = await this.getScriptsByRootPath(
validatedScript.rootPath
);
if (existingScripts.ok) {
const duplicate = existingScripts.value.find(
(s) => s.script === validatedScript.script
);
if (duplicate) {
this.logger.error("Duplicate script command found", {
script: validatedScript.script,
rootPath: validatedScript.rootPath
});
return {
ok: false,
error: new ScriptError(
"Script command already exists",
"SCRIPT_DUPLICATE" /* SCRIPT_DUPLICATE */,
{
script: validatedScript.script,
rootPath: validatedScript.rootPath
}
)
};
}
}
const collection = await this.getOrCreateScriptCollection(
validatedScript.rootPath
);
if (isFailure(collection)) {
return { ok: false, error: collection.error };
}
collection.value.scripts.push(validatedScript);
const saveResult = await this.saveScriptCollection(
validatedScript.rootPath,
collection.value
);
if (isFailure(saveResult)) {
return { ok: false, error: saveResult.error };
}
await this.updateIndexForRootPath(validatedScript.rootPath);
this.logger.log("Script created successfully", {
name: validatedScript.name,
rootPath: validatedScript.rootPath
});
return { ok: true, value: void 0 };
} catch (err) {
this.logger.error("Failed to create script", {
error: err.message,
script
});
return {
ok: false,
error: new ScriptError(err.message, "SCRIPT_INVALID" /* SCRIPT_INVALID */, {
originalError: err.message
})
};
}
}
async createScripts(scripts) {
try {
if (scripts.length === 0) {
return { ok: true, value: void 0 };
}
this.logger.debug("Creating multiple scripts", { count: scripts.length });
const scriptsByRootPath = /* @__PURE__ */ new Map();
for (const script of scripts) {
const validatedScript = validateScript(script);
if (!await this.pathExists(validatedScript.rootPath)) {
this.logger.error("Root path does not exist", {
rootPath: validatedScript.rootPath
});
return {
ok: false,
error: new ScriptError(
"Root path does not exist",
"SCRIPT_PATH_INVALID" /* SCRIPT_PATH_INVALID */,
{ rootPath: validatedScript.rootPath }
)
};
}
if (!scriptsByRootPath.has(validatedScript.rootPath)) {
scriptsByRootPath.set(validatedScript.rootPath, []);
}
scriptsByRootPath.get(validatedScript.rootPath).push(validatedScript);
}
for (const [rootPath, rootScripts] of scriptsByRootPath) {
const existingScripts = await this.getScriptsByRootPath(rootPath);
const existingCollection = existingScripts.ok ? existingScripts.value : [];
const allScripts = [...existingCollection, ...rootScripts];
const scriptCommands = /* @__PURE__ */ new Set();
const duplicates = [];
for (const script of allScripts) {
if (scriptCommands.has(script.script)) {
duplicates.push(script.script);
} else {
scriptCommands.add(script.script);
}
}
if (duplicates.length > 0) {
this.logger.error("Duplicate script commands found", {
duplicates,
rootPath
});
return {
ok: false,
error: new ScriptError(
"Duplicate script commands found",
"SCRIPT_DUPLICATE" /* SCRIPT_DUPLICATE */,
{ duplicates, rootPath }
)
};
}
const collection = { scripts: allScripts };
const saveResult = await this.saveScriptCollection(
rootPath,
collection
);
if (isFailure(saveResult)) {
return { ok: false, error: saveResult.error };
}
await this.updateIndexForRootPath(rootPath);
}
this.logger.log("Multiple scripts created successfully", {
count: scripts.length
});
return { ok: true, value: void 0 };
} catch (err) {
this.logger.error("Failed to create multiple scripts", {
error: err.message,
count: scripts.length
});
return {
ok: false,
error: new ScriptError(err.message, "SCRIPT_INVALID" /* SCRIPT_INVALID */, {
originalError: err.message
})
};
}
}
async getScriptsByRootPath(rootPath) {
try {
const collection = await this.loadScriptCollection(rootPath);
if (isFailure(collection)) {
return { ok: true, value: [] };
}
return { ok: true, value: collection.value.scripts };
} catch (err) {
this.logger.error("Failed to get scripts by root path", {
error: err.message,
rootPath
});
return {
ok: false,
error: new ScriptError(err.message, "SCRIPT_INVALID" /* SCRIPT_INVALID */, {
originalError: err.message
})
};
}
}
async getAllScripts() {
try {
const index = await this.loadScriptIndex();
if (isFailure(index)) {
return { ok: true, value: [] };
}
const allScripts = [];
for (const entry of index.value.entries) {
const scripts = await this.getScriptsByRootPath(entry.rootPath);
if (scripts.ok) {
allScripts.push(...scripts.value);
}
}
return { ok: true, value: allScripts };
} catch (err) {
this.logger.error("Failed to get all scripts", { error: err.message });
return {
ok: false,
error: new ScriptError(err.message, "SCRIPT_INVALID" /* SCRIPT_INVALID */, {
originalError: err.message
})
};
}
}
async updateScript(name, rootPath, scriptUpdate) {
try {
const scripts = await this.getScriptsByRootPath(rootPath);
if (isFailure(scripts)) {
return { ok: false, error: scripts.error };
}
const scriptIndex = scripts.value.findIndex((s) => s.name === name);
if (scriptIndex === -1) {
this.logger.error("Script not found", { name, rootPath });
return {
ok: false,
error: new ScriptError(
"Script not found",
"SCRIPT_NOT_FOUND" /* SCRIPT_NOT_FOUND */,
{ name, rootPath }
)
};
}
const updatedScript = { ...scripts.value[scriptIndex], ...scriptUpdate };
const validatedScript = validateScript(updatedScript);
const duplicate = scripts.value.find(
(s) => s.script === validatedScript.script && s.name !== name
);
if (duplicate) {
this.logger.error("Duplicate script command found", {
script: validatedScript.script,
rootPath
});
return {
ok: false,
error: new ScriptError(
"Script command already exists",
"SCRIPT_DUPLICATE" /* SCRIPT_DUPLICATE */,
{ script: validatedScript.script, rootPath }
)
};
}
scripts.value[scriptIndex] = validatedScript;
const collection = { scripts: scripts.value };
const saveResult = await this.saveScriptCollection(rootPath, collection);
if (isFailure(saveResult)) {
return { ok: false, error: saveResult.error };
}
this.logger.log("Script updated successfully", { name, rootPath });
return { ok: true, value: void 0 };
} catch (err) {
this.logger.error("Failed to update script", {
error: err.message,
name,
rootPath
});
return {
ok: false,
error: new ScriptError(err.message, "SCRIPT_INVALID" /* SCRIPT_INVALID */, {
originalError: err.message
})
};
}
}
async updateScripts(updates) {
try {
if (updates.length === 0) {
return { ok: true, value: void 0 };
}
this.logger.debug("Updating multiple scripts", { count: updates.length });
const updatesByRootPath = /* @__PURE__ */ new Map();
for (const update of updates) {
if (!updatesByRootPath.has(update.rootPath)) {
updatesByRootPath.set(update.rootPath, []);
}
updatesByRootPath.get(update.rootPath).push({ name: update.name, script: update.script });
}
for (const [rootPath, rootUpdates] of updatesByRootPath) {
const scripts = await this.getScriptsByRootPath(rootPath);
if (isFailure(scripts)) {
return { ok: false, error: scripts.error };
}
const updatedScripts = [...scripts.value];
const updatedNames = /* @__PURE__ */ new Set();
for (const update of rootUpdates) {
const scriptIndex = updatedScripts.findIndex(
(s) => s.name === update.name
);
if (scriptIndex === -1) {
this.logger.error("Script not found for update", {
name: update.name,
rootPath
});
return {
ok: false,
error: new ScriptError(
"Script not found",
"SCRIPT_NOT_FOUND" /* SCRIPT_NOT_FOUND */,
{ name: update.name, rootPath }
)
};
}
const updatedScript = {
...updatedScripts[scriptIndex],
...update.script
};
const validatedScript = validateScript(updatedScript);
updatedScripts[scriptIndex] = validatedScript;
updatedNames.add(update.name);
}
const scriptCommands = /* @__PURE__ */ new Set();
const duplicates = [];
for (const script of updatedScripts) {
if (scriptCommands.has(script.script)) {
if (!updatedNames.has(script.name)) {
duplicates.push(script.script);
}
} else {
scriptCommands.add(script.script);
}
}
if (duplicates.length > 0) {
this.logger.error("Duplicate script commands found after updates", {
duplicates,
rootPath
});
return {
ok: false,
error: new ScriptError(
"Duplicate script commands found",
"SCRIPT_DUPLICATE" /* SCRIPT_DUPLICATE */,
{ duplicates, rootPath }
)
};
}
const collection = { scripts: updatedScripts };
const saveResult = await this.saveScriptCollection(
rootPath,
collection
);
if (isFailure(saveResult)) {
return { ok: false, error: saveResult.error };
}
}
this.logger.log("Multiple scripts updated successfully", {
count: updates.length
});
return { ok: true, value: void 0 };
} catch (err) {
this.logger.error("Failed to update multiple scripts", {
error: err.message,
count: updates.length
});
return {
ok: false,
error: new ScriptError(err.message, "SCRIPT_INVALID" /* SCRIPT_INVALID */, {
originalError: err.message
})
};
}
}
async deleteScript(name, rootPath) {
try {
const scripts = await this.getScriptsByRootPath(rootPath);
if (isFailure(scripts)) {
return { ok: false, error: scripts.error };
}
const scriptIndex = scripts.value.findIndex((s) => s.name === name);
if (scriptIndex === -1) {
this.logger.error("Script not found", { name, rootPath });
return {
ok: false,
error: new ScriptError(
"Script not found",
"SCRIPT_NOT_FOUND" /* SCRIPT_NOT_FOUND */,
{ name, rootPath }
)
};
}
scripts.value.splice(scriptIndex, 1);
const collection = { scripts: scripts.value };
const saveResult = await this.saveScriptCollection(rootPath, collection);
if (isFailure(saveResult)) {
return { ok: false, error: saveResult.error };
}
this.logger.log("Script deleted successfully", { name, rootPath });
return { ok: true, value: void 0 };
} catch (err) {
this.logger.error("Failed to delete script", {
error: err.message,
name,
rootPath
});
return {
ok: false,
error: new ScriptError(err.message, "SCRIPT_INVALID" /* SCRIPT_INVALID */, {
originalError: err.message
})
};
}
}
async deleteScripts(scripts) {
try {
if (scripts.length === 0) {
return { ok: true, value: void 0 };
}
this.logger.debug("Deleting multiple scripts", { count: scripts.length });
const deletionsByRootPath = /* @__PURE__ */ new Map();
for (const script of scripts) {
if (!deletionsByRootPath.has(script.rootPath)) {
deletionsByRootPath.set(script.rootPath, []);
}
deletionsByRootPath.get(script.rootPath).push(script.name);
}
for (const [rootPath, scriptNames] of deletionsByRootPath) {
const existingScripts = await this.getScriptsByRootPath(rootPath);
if (isFailure(existingScripts)) {
return { ok: false, error: existingScripts.error };
}
const remainingScripts = existingScripts.value.filter(
(script) => !scriptNames.includes(script.name)
);
const foundNames = existingScripts.value.filter((script) => scriptNames.includes(script.name)).map((s) => s.name);
const missingNames = scriptNames.filter(
(name) => !foundNames.includes(name)
);
if (missingNames.length > 0) {
this.logger.error("Some scripts not found for deletion", {
missingNames,
rootPath
});
return {
ok: false,
error: new ScriptError(
"Some scripts not found",
"SCRIPT_NOT_FOUND" /* SCRIPT_NOT_FOUND */,
{ missingNames, rootPath }
)
};
}
const collection = { scripts: remainingScripts };
const saveResult = await this.saveScriptCollection(
rootPath,
collection
);
if (isFailure(saveResult)) {
return { ok: false, error: saveResult.error };
}
}
this.logger.log("Multiple scripts deleted successfully", {
count: scripts.length
});
return { ok: true, value: void 0 };
} catch (err) {
this.logger.error("Failed to delete multiple scripts", {
error: err.message,
count: scripts.length
});
return {
ok: false,
error: new ScriptError(err.message, "SCRIPT_INVALID" /* SCRIPT_INVALID */, {
originalError: err.message
})
};
}
}
async deleteScriptsByRootPath(rootPath) {
try {
const referenceFile = await this.getReferenceFilePath(rootPath);
if (referenceFile) {
await fs2.unlink(referenceFile).catch(() => {
});
}
await this.removeFromIndex(rootPath);
this.logger.log("Scripts deleted for root path", { rootPath });
return { ok: true, value: void 0 };
} catch (err) {
this.logger.error("Failed to delete scripts by root path", {
error: err.message,
rootPath
});
return {
ok: false,
error: new ScriptError(err.message, "SCRIPT_INVALID" /* SCRIPT_INVALID */, {
originalError: err.message
})
};
}
}
async loadScriptIndex() {
try {
await this.ensureScriptsDir();
const indexPath = path5.join(this.scriptsDir, INDEX_FILE);
try {
const raw = await fs2.readFile(indexPath, { encoding: "utf8" });
let data = raw;
if (raw.startsWith("ENCRYPTED_v1")) {
const config = await this.configService.getConfig();
if (config.ok && config.value.encryption?.enabled && config.value.encryption.encryptionKey) {
const decrypted = await this.encryption.decrypt(
raw,
config.value.encryption.encryptionKey
);
if (isFailure(decrypted)) {
this.logger.error("Failed to decrypt script index", {
error: decrypted.error
});
return { ok: false, error: decrypted.error };
}
data = decrypted.value;
}
}
const parsed = JSON.parse(data);
const index = validateScriptIndex(parsed);
this.logger.debug("Script index loaded", { indexPath });
return { ok: true, value: index };
} catch (err) {
if (err.code === "ENOENT") {
const defaultIndex = { entries: [] };
await this.saveScriptIndex(defaultIndex);
return { ok: true, value: defaultIndex };
}
throw err;
}
} catch (err) {
this.logger.error("Failed to load script index", { error: err.message });
return {
ok: false,
error: new ScriptError(err.message, "SCRIPT_INVALID" /* SCRIPT_INVALID */, {
originalError: err.message
})
};
}
}
async saveScriptIndex(index) {
try {
await this.ensureScriptsDir();
const indexPath = path5.join(this.scriptsDir, INDEX_FILE);
const validated = validateScriptIndex(index);
let data = JSON.stringify(validated, null, 2);
let encrypted = false;
const config = await this.configService.getConfig();
if (config.ok && config.value.encryption?.enabled && config.value.encryption.encryptionKey) {
const encResult = await this.encryption.encrypt(
data,
config.value.encryption.encryptionKey
);
if (isFailure(encResult)) {
this.logger.error("Failed to encrypt script index", {
error: encResult.error
});
return { ok: false, error: encResult.error };
}
data = encResult.value;
encrypted = true;
}
const tempPath = indexPath + TEMP_SUFFIX2;
await fs2.writeFile(tempPath, data, { encoding: "utf8", mode: 384 });
await fs2.rename(indexPath, indexPath + BACKUP_SUFFIX2).catch(() => {
});
await fs2.rename(tempPath, indexPath);
this.logger.log("Script index saved", { indexPath, encrypted });
return { ok: true, value: void 0 };
} catch (err) {
this.logger.error("Failed to save script index", { error: err.message });
return {
ok: false,
error: new ScriptError(err.message, "SCRIPT_INVALID" /* SCRIPT_INVALID */, {
originalError: err.message
})
};
}
}
// Private helper methods
async ensureScriptsDir() {
await fs2.mkdir(this.scriptsDir, { recursive: true, mode: 448 });
}
async pathExists(pathToCheck) {
try {
await fs2.access(pathToCheck);
return true;
} catch {
return false;
}
}
async getOrCreateScriptCollection(rootPath) {
const collection = await this.loadScriptCollection(rootPath);
if (collection.ok) {
return collection;
}
return { ok: true, value: { scripts: [] } };
}
async loadScriptCollection(rootPath) {
try {
const referenceFile = await this.getReferenceFilePath(rootPath);
if (!referenceFile) {
return {
ok: false,
error: new ScriptError(
"Reference file not found",
"SCRIPT_NOT_FOUND" /* SCRIPT_NOT_FOUND */,
{ rootPath }
)
};
}
const raw = await fs2.readFile(referenceFile, { encoding: "utf8" });
let data = raw;
if (raw.startsWith("ENCRYPTED_v1")) {
const config = await this.configService.getConfig();
if (config.ok && config.value.encryption?.enabled && config.value.encryption.encryptionKey) {
const decrypted = await this.encryption.decrypt(
raw,
config.value.