fix(droid): align hooks with Factory 0.175
This commit is contained in:
1 parent
2f988e72ba
commit
590de9dbdb
12 files changed
+1855
-932
No files matched your search
@@ -0,0 +1,157 @@
|
||||
import fsp from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { inspectPhysicalDirectory, readPhysicalFile, type FileSnapshot } from './managed-files.js';
|
||||
import { parseStrictJsoncObject, type JsonObject } from './strict-json.js';
|
||||
|
||||
const MAX_SOURCE_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
interface PolicyLocation {
|
||||
readonly filePath: string;
|
||||
readonly label: string;
|
||||
}
|
||||
|
||||
interface PolicyLayer extends PolicyLocation {
|
||||
readonly snapshot?: FileSnapshot;
|
||||
readonly hooksDisabled?: boolean;
|
||||
readonly allowManagedHooksOnly?: boolean;
|
||||
readonly showHookOutput?: boolean;
|
||||
}
|
||||
|
||||
export interface DroidPolicyPrecondition {
|
||||
readonly filePath: string;
|
||||
readonly label: string;
|
||||
readonly maximumBytes: number;
|
||||
readonly original?: FileSnapshot;
|
||||
}
|
||||
|
||||
export interface DroidPolicyOrigin {
|
||||
readonly filePath: string;
|
||||
readonly label: string;
|
||||
}
|
||||
|
||||
export interface DroidPolicyState {
|
||||
readonly allowManagedHooksOnlyBy?: DroidPolicyOrigin;
|
||||
readonly hooksDisabled?: boolean;
|
||||
readonly hooksDisabledBy?: DroidPolicyOrigin;
|
||||
readonly preconditions: readonly DroidPolicyPrecondition[];
|
||||
}
|
||||
|
||||
function hasCode(error: unknown, code: string): boolean {
|
||||
return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
|
||||
}
|
||||
|
||||
function optionalBoolean(root: JsonObject, field: string, label: string): boolean | undefined {
|
||||
const value = root[field];
|
||||
if (value !== undefined && typeof value !== 'boolean') {
|
||||
throw new Error(`${label} field "${field}" must be a boolean`);
|
||||
}
|
||||
return value as boolean | undefined;
|
||||
}
|
||||
|
||||
function managedSettingsPath(): string {
|
||||
if (process.platform === 'darwin') {
|
||||
return '/Library/Application Support/Factory/settings.json';
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
return path.join(process.env.ProgramFiles ?? 'C:\\Program Files', 'Factory', 'settings.json');
|
||||
}
|
||||
return '/etc/factory/settings.json';
|
||||
}
|
||||
|
||||
async function readLayer(location: PolicyLocation): Promise<PolicyLayer> {
|
||||
await inspectPhysicalDirectory(path.dirname(location.filePath), `${location.label} directory`);
|
||||
const snapshot = await readPhysicalFile(location.filePath, location.label, MAX_SOURCE_BYTES);
|
||||
if (!snapshot) return location;
|
||||
const root = parseStrictJsoncObject(snapshot.contents, `${location.label} at ${location.filePath}`);
|
||||
return {
|
||||
...location,
|
||||
snapshot,
|
||||
hooksDisabled: optionalBoolean(root, 'hooksDisabled', location.label),
|
||||
allowManagedHooksOnly: optionalBoolean(root, 'allowManagedHooksOnly', location.label),
|
||||
showHookOutput: optionalBoolean(root, 'showHookOutput', location.label),
|
||||
};
|
||||
}
|
||||
|
||||
async function gitRoot(start: string): Promise<string | undefined> {
|
||||
let current = path.resolve(start);
|
||||
for (;;) {
|
||||
const marker = path.join(current, '.git');
|
||||
try {
|
||||
const metadata = await fsp.lstat(marker);
|
||||
if (metadata.isSymbolicLink() || (!metadata.isDirectory() && !metadata.isFile())) {
|
||||
throw new Error(`Factory Droid project marker is not physical: ${marker}`);
|
||||
}
|
||||
return current;
|
||||
} catch (error) {
|
||||
if (!hasCode(error, 'ENOENT') && !hasCode(error, 'ENOTDIR')) throw error;
|
||||
}
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) return undefined;
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
|
||||
function projectDirectories(root: string, current: string): string[] {
|
||||
const directories = [root];
|
||||
const relative = path.relative(root, current);
|
||||
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return directories;
|
||||
let directory = root;
|
||||
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
||||
directory = path.join(directory, segment);
|
||||
directories.push(directory);
|
||||
}
|
||||
return directories;
|
||||
}
|
||||
|
||||
async function projectLocations(): Promise<PolicyLocation[][]> {
|
||||
const current = path.resolve(process.cwd());
|
||||
const root = await gitRoot(current) ?? current;
|
||||
return projectDirectories(root, current).map((directory, index) => {
|
||||
const factory = path.join(directory, '.factory');
|
||||
const scope = index === 0 ? 'project' : `folder ${directory}`;
|
||||
return [{
|
||||
filePath: path.join(factory, 'settings.json'),
|
||||
label: `Factory Droid ${scope} settings`,
|
||||
}, {
|
||||
filePath: path.join(factory, 'settings.local.json'),
|
||||
label: `Factory Droid ${scope} local settings`,
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
function scopeValue(layers: readonly PolicyLayer[], field: 'hooksDisabled'): PolicyLayer | undefined {
|
||||
const [settings, local] = layers;
|
||||
return local[field] !== undefined ? local : settings[field] !== undefined ? settings : undefined;
|
||||
}
|
||||
|
||||
export async function readDroidPolicy(): Promise<DroidPolicyState> {
|
||||
const managedLocation: PolicyLocation = {
|
||||
filePath: managedSettingsPath(),
|
||||
label: 'Factory Droid system-managed settings',
|
||||
};
|
||||
const scopes = await projectLocations();
|
||||
const [managed, ...projectLayers] = await Promise.all([
|
||||
readLayer(managedLocation),
|
||||
...scopes.map(async (scope) => Promise.all(scope.map(readLayer))),
|
||||
]);
|
||||
const flattened = [managed, ...projectLayers.flat()];
|
||||
const allowManagedHooksOnlyBy = managed.allowManagedHooksOnly === true
|
||||
? { filePath: managed.filePath, label: managed.label }
|
||||
: undefined;
|
||||
const selected = [managed, ...projectLayers.map((scope) => scopeValue(scope, 'hooksDisabled'))]
|
||||
.find((layer) => layer?.hooksDisabled !== undefined);
|
||||
return {
|
||||
allowManagedHooksOnlyBy,
|
||||
hooksDisabled: selected?.hooksDisabled,
|
||||
hooksDisabledBy: selected
|
||||
? { filePath: selected.filePath, label: selected.label }
|
||||
: undefined,
|
||||
preconditions: flattened.map((layer) => ({
|
||||
filePath: layer.filePath,
|
||||
label: layer.label,
|
||||
maximumBytes: MAX_SOURCE_BYTES,
|
||||
original: layer.snapshot,
|
||||
})),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user