mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-28 00:55:16 -07:00
feat(macos): configuration window + curl-backed macOS updater (#71)
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
import {
|
||||
applyEdits,
|
||||
modify,
|
||||
parse as parseJsonc,
|
||||
type FormattingOptions,
|
||||
type ParseError,
|
||||
} from 'jsonc-parser';
|
||||
import type { ConfigValidationWarning, RawConfig, ResolvedConfig } from '../../types/config';
|
||||
import type {
|
||||
ConfigSettingsField,
|
||||
ConfigSettingsPatchOperation,
|
||||
ConfigSettingsSnapshot,
|
||||
} from '../../types/settings';
|
||||
import { resolveConfig } from '../resolve';
|
||||
import { getConfigValueAtPath } from './registry';
|
||||
|
||||
const JSONC_FORMATTING_OPTIONS: FormattingOptions = {
|
||||
insertSpaces: true,
|
||||
tabSize: 2,
|
||||
eol: '\n',
|
||||
};
|
||||
|
||||
export type ConfigSettingsPatchApplyResult =
|
||||
| {
|
||||
ok: true;
|
||||
content: string;
|
||||
rawConfig: RawConfig;
|
||||
resolvedConfig: ResolvedConfig;
|
||||
warnings: ConfigValidationWarning[];
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
content: string;
|
||||
warnings: ConfigValidationWarning[];
|
||||
error: string;
|
||||
};
|
||||
|
||||
interface ApplyConfigSettingsPatchOptions {
|
||||
content: string;
|
||||
operations: ConfigSettingsPatchOperation[];
|
||||
previousWarnings: ConfigValidationWarning[];
|
||||
}
|
||||
|
||||
interface BuildConfigSettingsSnapshotOptions {
|
||||
configPath: string;
|
||||
rawConfig: RawConfig;
|
||||
resolvedConfig: ResolvedConfig;
|
||||
warnings: ConfigValidationWarning[];
|
||||
fields: ConfigSettingsField[];
|
||||
}
|
||||
|
||||
function pathToSegments(path: string): string[] {
|
||||
return path.split('.').filter(Boolean);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function pathStartsWith(path: string, prefix: string): boolean {
|
||||
return path === prefix || path.startsWith(`${prefix}.`);
|
||||
}
|
||||
|
||||
function warningBelongsToModifiedPath(
|
||||
warning: ConfigValidationWarning,
|
||||
operation: ConfigSettingsPatchOperation,
|
||||
): boolean {
|
||||
return (
|
||||
pathStartsWith(warning.path, operation.path) || pathStartsWith(operation.path, warning.path)
|
||||
);
|
||||
}
|
||||
|
||||
function warningIdentity(warning: ConfigValidationWarning): string {
|
||||
return `${warning.path}\n${JSON.stringify(warning.value)}\n${warning.message}`;
|
||||
}
|
||||
|
||||
function parseRawConfig(content: string): RawConfig {
|
||||
const errors: ParseError[] = [];
|
||||
const parsed = parseJsonc(content || '{}', errors, {
|
||||
allowTrailingComma: true,
|
||||
disallowComments: false,
|
||||
});
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`Invalid JSONC (${errors[0]?.error ?? 'unknown'})`);
|
||||
}
|
||||
return isRecord(parsed) ? (parsed as RawConfig) : {};
|
||||
}
|
||||
|
||||
function normalizeContent(content: string): string {
|
||||
return content.trim().length > 0 ? content : '{}\n';
|
||||
}
|
||||
|
||||
function applySingleOperation(content: string, operation: ConfigSettingsPatchOperation): string {
|
||||
const edits = modify(
|
||||
content,
|
||||
pathToSegments(operation.path),
|
||||
operation.op === 'reset' ? undefined : operation.value,
|
||||
{
|
||||
formattingOptions: JSONC_FORMATTING_OPTIONS,
|
||||
getInsertionIndex: (properties) => properties.length,
|
||||
},
|
||||
);
|
||||
return applyEdits(content, edits);
|
||||
}
|
||||
|
||||
function collectModifiedWarnings(
|
||||
warnings: ConfigValidationWarning[],
|
||||
operations: ConfigSettingsPatchOperation[],
|
||||
previousWarnings: ConfigValidationWarning[],
|
||||
): ConfigValidationWarning[] {
|
||||
const previous = new Set(previousWarnings.map(warningIdentity));
|
||||
return warnings.filter((warning) => {
|
||||
if (!operations.some((operation) => warningBelongsToModifiedPath(warning, operation))) {
|
||||
return false;
|
||||
}
|
||||
return !previous.has(warningIdentity(warning));
|
||||
});
|
||||
}
|
||||
|
||||
export function applyConfigSettingsPatchToContent(
|
||||
options: ApplyConfigSettingsPatchOptions,
|
||||
): ConfigSettingsPatchApplyResult {
|
||||
let content = normalizeContent(options.content);
|
||||
|
||||
try {
|
||||
parseRawConfig(content);
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
content,
|
||||
warnings: [],
|
||||
error: error instanceof Error ? error.message : 'Invalid JSONC.',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
for (const operation of options.operations) {
|
||||
content = applySingleOperation(content, operation);
|
||||
}
|
||||
|
||||
const rawConfig = parseRawConfig(content);
|
||||
const { resolved, warnings } = resolveConfig(rawConfig);
|
||||
const modifiedWarnings = collectModifiedWarnings(
|
||||
warnings,
|
||||
options.operations,
|
||||
options.previousWarnings,
|
||||
);
|
||||
if (modifiedWarnings.length > 0) {
|
||||
return {
|
||||
ok: false,
|
||||
content,
|
||||
warnings: modifiedWarnings,
|
||||
error: 'One or more modified settings failed validation.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
content,
|
||||
rawConfig,
|
||||
resolvedConfig: resolved,
|
||||
warnings,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
content,
|
||||
warnings: [],
|
||||
error: error instanceof Error ? error.message : 'Failed to update config content.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function buildConfigSettingsSnapshot(
|
||||
options: BuildConfigSettingsSnapshotOptions,
|
||||
): ConfigSettingsSnapshot {
|
||||
const values: Record<string, unknown> = {};
|
||||
|
||||
for (const field of options.fields) {
|
||||
const rawValue = getConfigValueAtPath(options.rawConfig, field.configPath);
|
||||
const resolvedValue = getConfigValueAtPath(options.resolvedConfig, field.configPath);
|
||||
if (field.secret) {
|
||||
values[field.configPath] = {
|
||||
configured:
|
||||
(typeof rawValue === 'string' && rawValue.length > 0) ||
|
||||
(typeof resolvedValue === 'string' && resolvedValue.length > 0),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
values[field.configPath] = structuredClone(rawValue !== undefined ? rawValue : resolvedValue);
|
||||
}
|
||||
|
||||
return {
|
||||
configPath: options.configPath,
|
||||
fields: options.fields,
|
||||
values,
|
||||
warnings: options.warnings,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user