Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions src/autostart.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,33 @@ export const WINDOWS_TASK = "DSCodex";
export const WINDOWS_RESTART_COUNT = 255;
export const WINDOWS_RESTART_INTERVAL_MINUTES = 1;

// Cleanup must remain best-effort: a corrupt pid file or failed authenticated
// shutdown cannot prevent the service manager and generated artifact from being
// removed. Report every failure only after all requested steps have run.
export async function cleanupAutostart({
stopRouter,
deactivateManager,
removeArtifact,
reloadManager,
message = "Failed to clean up DSCodex autostart",
}) {
const failures = [];
for (const operation of [stopRouter, deactivateManager, removeArtifact, reloadManager]) {
if (!operation) continue;
try {
await operation();
} catch (error) {
failures.push(error);
}
}
if (failures.length) {
const details = failures.map((error) => (
error instanceof Error ? error.message : String(error)
)).join("; ");
throw new AggregateError(failures, `${message}: ${details}`);
}
}

export function autostartKind(platform = process.platform) {
if (platform === "darwin") return "launchd";
if (platform === "win32") return "schtasks";
Expand Down
27 changes: 27 additions & 0 deletions src/catalog.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ const NATIVE_ENTRY_DEFAULTS = {
supports_reasoning_summaries: false,
};

// These fields are required by the current Codex model-catalog parser. Keep
// this deliberately narrower than the full catalog shape: native entries may
// gain optional fields independently, while doctor only needs to know that the
// merged catalog is parseable and still contains both DSCodex models.
const REQUIRED_ENTRY_FIELD_TYPES = Object.freeze({
slug: "string",
base_instructions: "string",
prefer_websockets: "boolean",
supports_reasoning_summaries: "boolean",
});

function backfillNativeEntry(model) {
const entry = clone(model);
for (const [key, value] of Object.entries(NATIVE_ENTRY_DEFAULTS)) {
Expand Down Expand Up @@ -113,6 +124,22 @@ export function buildCatalog(cache) {
};
}

export function isCatalogReady(catalog) {
if (!Array.isArray(catalog?.models) || catalog.models.length === 0) return false;

const slugs = new Set();
for (const model of catalog.models) {
if (!model || typeof model !== "object" || Array.isArray(model)) return false;
if (Object.entries(REQUIRED_ENTRY_FIELD_TYPES).some(([field, type]) => (
typeof model[field] !== type
))) return false;
if (model.slug.trim().length === 0 || slugs.has(model.slug)) return false;
slugs.add(model.slug);
}

return DEEPSEEK_MODELS.every((model) => slugs.has(model.pickerSlug));
}

export function writeCatalog({ catalogPath, catalog }) {
const temporary = `${catalogPath}.dscodex-tmp-${process.pid}`;
writeFileSync(temporary, `${JSON.stringify(catalog, null, 2)}\n`, { mode: 0o600 });
Expand Down
Loading