feat(cli): add machine-readable CLI help, recommendation, and configuration flows
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
import * as plugins from "./plugins.js";
|
||||
import { rename, writeFile } from "fs/promises";
|
||||
|
||||
export const CLI_NAMESPACE = "@git.zone/cli";
|
||||
|
||||
const isPlainObject = (value: unknown): value is Record<string, any> => {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
};
|
||||
|
||||
export const getSmartconfigPath = (cwd: string = process.cwd()): string => {
|
||||
return plugins.path.join(cwd, ".smartconfig.json");
|
||||
};
|
||||
|
||||
export const readSmartconfigFile = async (
|
||||
cwd: string = process.cwd(),
|
||||
): Promise<Record<string, any>> => {
|
||||
const smartconfigPath = getSmartconfigPath(cwd);
|
||||
if (!(await plugins.smartfs.file(smartconfigPath).exists())) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const content = (await plugins.smartfs
|
||||
.file(smartconfigPath)
|
||||
.encoding("utf8")
|
||||
.read()) as string;
|
||||
if (content.trim() === "") {
|
||||
return {};
|
||||
}
|
||||
return JSON.parse(content);
|
||||
};
|
||||
|
||||
export const writeSmartconfigFile = async (
|
||||
data: Record<string, any>,
|
||||
cwd: string = process.cwd(),
|
||||
): Promise<void> => {
|
||||
const smartconfigPath = getSmartconfigPath(cwd);
|
||||
const tempPath = `${smartconfigPath}.tmp-${Date.now()}`;
|
||||
const content = JSON.stringify(data, null, 2);
|
||||
await writeFile(tempPath, content, "utf8");
|
||||
await rename(tempPath, smartconfigPath);
|
||||
};
|
||||
|
||||
export const normalizeCliConfigPath = (configPath: string): string => {
|
||||
const trimmedPath = configPath.trim();
|
||||
if (!trimmedPath || trimmedPath === CLI_NAMESPACE) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (trimmedPath.startsWith(`${CLI_NAMESPACE}.`)) {
|
||||
return trimmedPath.slice(`${CLI_NAMESPACE}.`.length);
|
||||
}
|
||||
|
||||
return trimmedPath;
|
||||
};
|
||||
|
||||
export const getCliConfigPathSegments = (configPath: string): string[] => {
|
||||
const normalizedPath = normalizeCliConfigPath(configPath);
|
||||
if (!normalizedPath) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return normalizedPath
|
||||
.split(".")
|
||||
.map((segment) => segment.trim())
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
export const getCliNamespaceConfig = (
|
||||
smartconfigData: Record<string, any>,
|
||||
): Record<string, any> => {
|
||||
const cliConfig = smartconfigData[CLI_NAMESPACE];
|
||||
if (isPlainObject(cliConfig)) {
|
||||
return cliConfig;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export const getCliConfigValueFromData = (
|
||||
smartconfigData: Record<string, any>,
|
||||
configPath: string,
|
||||
): any => {
|
||||
const segments = getCliConfigPathSegments(configPath);
|
||||
let currentValue: any = getCliNamespaceConfig(smartconfigData);
|
||||
|
||||
for (const segment of segments) {
|
||||
if (!isPlainObject(currentValue) && !Array.isArray(currentValue)) {
|
||||
return undefined;
|
||||
}
|
||||
currentValue = (currentValue as any)?.[segment];
|
||||
}
|
||||
|
||||
return currentValue;
|
||||
};
|
||||
|
||||
export const getCliConfigValue = async <T>(
|
||||
configPath: string,
|
||||
defaultValue: T,
|
||||
cwd: string = process.cwd(),
|
||||
): Promise<T> => {
|
||||
const smartconfigData = await readSmartconfigFile(cwd);
|
||||
const configValue = getCliConfigValueFromData(smartconfigData, configPath);
|
||||
|
||||
if (configValue === undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (isPlainObject(defaultValue) && isPlainObject(configValue)) {
|
||||
return {
|
||||
...defaultValue,
|
||||
...configValue,
|
||||
} as T;
|
||||
}
|
||||
|
||||
return configValue as T;
|
||||
};
|
||||
|
||||
export const setCliConfigValueInData = (
|
||||
smartconfigData: Record<string, any>,
|
||||
configPath: string,
|
||||
value: any,
|
||||
): Record<string, any> => {
|
||||
const segments = getCliConfigPathSegments(configPath);
|
||||
|
||||
if (!isPlainObject(smartconfigData[CLI_NAMESPACE])) {
|
||||
smartconfigData[CLI_NAMESPACE] = {};
|
||||
}
|
||||
|
||||
if (segments.length === 0) {
|
||||
smartconfigData[CLI_NAMESPACE] = value;
|
||||
return smartconfigData;
|
||||
}
|
||||
|
||||
let currentValue = smartconfigData[CLI_NAMESPACE];
|
||||
for (const segment of segments.slice(0, -1)) {
|
||||
if (!isPlainObject(currentValue[segment])) {
|
||||
currentValue[segment] = {};
|
||||
}
|
||||
currentValue = currentValue[segment];
|
||||
}
|
||||
|
||||
currentValue[segments[segments.length - 1]] = value;
|
||||
return smartconfigData;
|
||||
};
|
||||
|
||||
export const unsetCliConfigValueInData = (
|
||||
smartconfigData: Record<string, any>,
|
||||
configPath: string,
|
||||
): boolean => {
|
||||
const segments = getCliConfigPathSegments(configPath);
|
||||
if (segments.length === 0) {
|
||||
if (smartconfigData[CLI_NAMESPACE] !== undefined) {
|
||||
delete smartconfigData[CLI_NAMESPACE];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const parentSegments = segments.slice(0, -1);
|
||||
let currentValue: any = getCliNamespaceConfig(smartconfigData);
|
||||
const objectPath: Array<Record<string, any>> = [currentValue];
|
||||
|
||||
for (const segment of parentSegments) {
|
||||
if (!isPlainObject(currentValue[segment])) {
|
||||
return false;
|
||||
}
|
||||
currentValue = currentValue[segment];
|
||||
objectPath.push(currentValue);
|
||||
}
|
||||
|
||||
const lastSegment = segments[segments.length - 1];
|
||||
if (!(lastSegment in currentValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
delete currentValue[lastSegment];
|
||||
|
||||
for (let i = objectPath.length - 1; i >= 1; i--) {
|
||||
if (Object.keys(objectPath[i]).length > 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const parentObject = objectPath[i - 1];
|
||||
const parentKey = parentSegments[i - 1];
|
||||
delete parentObject[parentKey];
|
||||
}
|
||||
|
||||
if (Object.keys(getCliNamespaceConfig(smartconfigData)).length === 0) {
|
||||
delete smartconfigData[CLI_NAMESPACE];
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
Reference in New Issue
Block a user