321 lines
10 KiB
TypeScript
321 lines
10 KiB
TypeScript
|
|
import fs from 'node:fs';
|
||
|
|
import path from 'node:path';
|
||
|
|
import {
|
||
|
|
readMatrixCliConfig,
|
||
|
|
resetMatrixCliConfig,
|
||
|
|
resolvePackageRegistry,
|
||
|
|
setMatrixCliConfigValue,
|
||
|
|
type IMatrixCliConfig,
|
||
|
|
} from '../utils/config-store.js';
|
||
|
|
|
||
|
|
export interface IConfigCommandOptions {
|
||
|
|
readonly cwd?: string;
|
||
|
|
readonly configPath?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface IConfigGetResult {
|
||
|
|
readonly key: string;
|
||
|
|
readonly value: string | undefined;
|
||
|
|
readonly source?: 'explicit' | 'env' | 'config' | 'default';
|
||
|
|
readonly configPath: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface IConfigListResult {
|
||
|
|
readonly configPath: string;
|
||
|
|
readonly values: IMatrixCliConfig;
|
||
|
|
readonly effective: {
|
||
|
|
readonly registry: string;
|
||
|
|
readonly registrySource: 'explicit' | 'env' | 'config' | 'default';
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface IConfigSetResult {
|
||
|
|
readonly key: string;
|
||
|
|
readonly value: string;
|
||
|
|
readonly configPath: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface IConfigResetResult {
|
||
|
|
readonly configPath: string;
|
||
|
|
readonly values: IMatrixCliConfig;
|
||
|
|
}
|
||
|
|
|
||
|
|
export type ConfigClassificationKind =
|
||
|
|
| 'direct-actor-input'
|
||
|
|
| 'package-declaration'
|
||
|
|
| 'runtime-host-desired-state'
|
||
|
|
| 'credential-link-state'
|
||
|
|
| 'runtime-host-generated-state'
|
||
|
|
| 'runtime-host-preset'
|
||
|
|
| 'platform-deployment'
|
||
|
|
| 'deletion-candidate'
|
||
|
|
| 'unknown';
|
||
|
|
|
||
|
|
export interface IConfigExplainResult {
|
||
|
|
readonly path: string;
|
||
|
|
readonly relativePath: string;
|
||
|
|
readonly exists: boolean;
|
||
|
|
readonly kind: ConfigClassificationKind;
|
||
|
|
readonly owner: string;
|
||
|
|
readonly authoredBy: string;
|
||
|
|
readonly directRunReads: boolean;
|
||
|
|
readonly managedHostReads: boolean;
|
||
|
|
readonly generated: boolean;
|
||
|
|
readonly recommendedAction: string;
|
||
|
|
readonly notes: readonly string[];
|
||
|
|
}
|
||
|
|
|
||
|
|
function assertSupportedKey(key: string): 'registry' {
|
||
|
|
const normalized = key.trim();
|
||
|
|
if (normalized !== 'registry') {
|
||
|
|
throw new Error(`MX_CONFIG_INVALID: unsupported config key "${key}"`);
|
||
|
|
}
|
||
|
|
return normalized;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function configGetCommand(
|
||
|
|
key: string,
|
||
|
|
options: IConfigCommandOptions = {},
|
||
|
|
): Promise<IConfigGetResult> {
|
||
|
|
const normalizedKey = assertSupportedKey(key);
|
||
|
|
const stored = readMatrixCliConfig(options);
|
||
|
|
const effective = resolvePackageRegistry(undefined, options);
|
||
|
|
return {
|
||
|
|
key: normalizedKey,
|
||
|
|
value: stored.values.registry ?? effective.registry,
|
||
|
|
source: stored.values.registry ? 'config' : effective.source,
|
||
|
|
configPath: stored.configPath,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function configListCommand(options: IConfigCommandOptions = {}): Promise<IConfigListResult> {
|
||
|
|
const stored = readMatrixCliConfig(options);
|
||
|
|
const effective = resolvePackageRegistry(undefined, options);
|
||
|
|
return {
|
||
|
|
configPath: stored.configPath,
|
||
|
|
values: stored.values,
|
||
|
|
effective: {
|
||
|
|
registry: effective.registry,
|
||
|
|
registrySource: effective.source,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function configSetCommand(
|
||
|
|
key: string,
|
||
|
|
value: string,
|
||
|
|
options: IConfigCommandOptions = {},
|
||
|
|
): Promise<IConfigSetResult> {
|
||
|
|
const normalizedKey = assertSupportedKey(key);
|
||
|
|
const result = setMatrixCliConfigValue(normalizedKey, value, options);
|
||
|
|
return {
|
||
|
|
key: normalizedKey,
|
||
|
|
value: result.values.registry ?? '',
|
||
|
|
configPath: result.configPath,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function configResetCommand(options: IConfigCommandOptions = {}): Promise<IConfigResetResult> {
|
||
|
|
return resetMatrixCliConfig(options);
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function configExplainCommand(
|
||
|
|
targetPath: string,
|
||
|
|
options: IConfigCommandOptions = {},
|
||
|
|
): Promise<IConfigExplainResult> {
|
||
|
|
const cwd = options.cwd ?? process.cwd();
|
||
|
|
const absolutePath = path.resolve(cwd, targetPath);
|
||
|
|
const relativePath = normalizePath(path.relative(cwd, absolutePath) || path.basename(absolutePath));
|
||
|
|
const exists = fs.existsSync(absolutePath);
|
||
|
|
return {
|
||
|
|
path: absolutePath,
|
||
|
|
relativePath,
|
||
|
|
exists,
|
||
|
|
...classifyConfigPath(relativePath),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function classifyConfigPath(relativePath: string): Omit<IConfigExplainResult, 'path' | 'relativePath' | 'exists'> {
|
||
|
|
const normalized = normalizePath(relativePath);
|
||
|
|
const basename = path.posix.basename(normalized);
|
||
|
|
const segments = normalized.split('/').filter(Boolean);
|
||
|
|
|
||
|
|
if (basename === 'ActorRunnerSmokeActor.mjs' || basename.endsWith('Actor.ts') || basename.endsWith('Actor.mjs')) {
|
||
|
|
return classification({
|
||
|
|
kind: 'direct-actor-input',
|
||
|
|
owner: 'Standalone Actor Runner',
|
||
|
|
authoredBy: 'CLI caller or package author',
|
||
|
|
directRunReads: true,
|
||
|
|
managedHostReads: false,
|
||
|
|
generated: false,
|
||
|
|
recommendedAction: 'keep as direct actor input; do not register Host-managed state',
|
||
|
|
notes: ['Direct actor execution receives this path from the CLI caller.'],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (basename === 'matrix.json') {
|
||
|
|
return classification({
|
||
|
|
kind: 'package-declaration',
|
||
|
|
owner: 'Package',
|
||
|
|
authoredBy: 'package author or scaffold',
|
||
|
|
directRunReads: true,
|
||
|
|
managedHostReads: true,
|
||
|
|
generated: false,
|
||
|
|
recommendedAction: 'keep; package declarations are shared by direct and managed execution',
|
||
|
|
notes: [`${basename} is package-authored declaration state.`],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (basename === 'package.json.matrix') {
|
||
|
|
return classification({
|
||
|
|
kind: 'package-declaration',
|
||
|
|
owner: 'Package',
|
||
|
|
authoredBy: 'package author or scaffold',
|
||
|
|
directRunReads: true,
|
||
|
|
managedHostReads: true,
|
||
|
|
generated: false,
|
||
|
|
recommendedAction: 'migrate to matrix.json when a package-local matrix.json exists',
|
||
|
|
notes: ['This is a duplicate package manifest candidate, not Runtime Host state.'],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (basename === 'host.json' || basename === 'workspace.json' || basename === 'daemon.json') {
|
||
|
|
return classification({
|
||
|
|
kind: 'runtime-host-desired-state',
|
||
|
|
owner: 'Runtime Host',
|
||
|
|
authoredBy: 'developer, operator, or Host setup tool',
|
||
|
|
directRunReads: false,
|
||
|
|
managedHostReads: true,
|
||
|
|
generated: false,
|
||
|
|
recommendedAction: 'normalize behind the Runtime Host Plan before changing shape',
|
||
|
|
notes: ['Direct actor/package execution must not read this file.'],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (basename.endsWith('.environment.json') && segments.includes('.matrix')) {
|
||
|
|
return classification({
|
||
|
|
kind: 'runtime-host-desired-state',
|
||
|
|
owner: 'Runtime Host',
|
||
|
|
authoredBy: 'developer, operator, or package environment author',
|
||
|
|
directRunReads: false,
|
||
|
|
managedHostReads: true,
|
||
|
|
generated: false,
|
||
|
|
recommendedAction: 'normalize into Runtime Host desired-state model',
|
||
|
|
notes: ['Package environment manifests belong to managed execution, not direct cloud-bus run.'],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (segments.includes('credentials') || basename === 'matrix-api-key.json' || basename === 'hivecast-link.json') {
|
||
|
|
return classification({
|
||
|
|
kind: 'credential-link-state',
|
||
|
|
owner: 'Credentials layer',
|
||
|
|
authoredBy: 'CLI, cloud exchange, or operator',
|
||
|
|
directRunReads: true,
|
||
|
|
managedHostReads: true,
|
||
|
|
generated: false,
|
||
|
|
recommendedAction: 'keep secret; never commit; use only through configured credential resolvers',
|
||
|
|
notes: ['Direct-run may read stored API keys; Runtime Host may read Host/link credentials.'],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (
|
||
|
|
basename === 'host.status.json'
|
||
|
|
|| basename === 'metadata.webapp'
|
||
|
|
|| segments.includes('runtimes')
|
||
|
|
|| segments.includes('runtime-env')
|
||
|
|
|| segments.includes('package-records')
|
||
|
|
) {
|
||
|
|
return classification({
|
||
|
|
kind: 'runtime-host-generated-state',
|
||
|
|
owner: 'Runtime Host reconciler',
|
||
|
|
authoredBy: 'generated by Host runtime',
|
||
|
|
directRunReads: false,
|
||
|
|
managedHostReads: true,
|
||
|
|
generated: true,
|
||
|
|
recommendedAction: 'keep out of authored config; safe to regenerate from managed Host state',
|
||
|
|
notes: ['Direct actor/package execution must not create this state.'],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (
|
||
|
|
normalized.includes('/deployments/nodes/')
|
||
|
|
|| normalized.startsWith('deployments/nodes/')
|
||
|
|
|| normalized.includes('/dist/profiles/')
|
||
|
|
|| normalized.startsWith('dist/profiles/')
|
||
|
|
|| basename.endsWith('.profile.json')
|
||
|
|
|| basename.endsWith('.node.json')
|
||
|
|
) {
|
||
|
|
return classification({
|
||
|
|
kind: 'runtime-host-preset',
|
||
|
|
owner: 'Distribution profile resolver',
|
||
|
|
authoredBy: 'release engineer',
|
||
|
|
directRunReads: false,
|
||
|
|
managedHostReads: true,
|
||
|
|
generated: false,
|
||
|
|
recommendedAction: 'keep until profile/preset cleanup unifies resolver semantics',
|
||
|
|
notes: ['Profile and node presets are managed Host inputs, not direct-run inputs.'],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (
|
||
|
|
normalized.includes('/deployments/environments/')
|
||
|
|
|| normalized.startsWith('deployments/environments/')
|
||
|
|
|| normalized.includes('/deployments/topologies/')
|
||
|
|
|| normalized.startsWith('deployments/topologies/')
|
||
|
|
|| normalized.endsWith('/deployments/schema.json')
|
||
|
|
|| normalized === 'deployments/schema.json'
|
||
|
|
|| normalized.includes('/packages/') && normalized.includes('-deployment/')
|
||
|
|
|| normalized.startsWith('packages/') && normalized.includes('-deployment/')
|
||
|
|
) {
|
||
|
|
return classification({
|
||
|
|
kind: 'deletion-candidate',
|
||
|
|
owner: 'Historical deployment layer',
|
||
|
|
authoredBy: 'historical deployment tooling',
|
||
|
|
directRunReads: false,
|
||
|
|
managedHostReads: false,
|
||
|
|
generated: false,
|
||
|
|
recommendedAction: 'delete only after grep/test proof shows no live reader',
|
||
|
|
notes: ['This path matches Phase 6 deletion candidates.'],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (
|
||
|
|
basename === 'docker-compose.yml'
|
||
|
|
|| basename === 'Dockerfile'
|
||
|
|
|| normalized.includes('/systemd/')
|
||
|
|
|| normalized.includes('/native-dist/')
|
||
|
|
) {
|
||
|
|
return classification({
|
||
|
|
kind: 'platform-deployment',
|
||
|
|
owner: 'Platform operator',
|
||
|
|
authoredBy: 'operator or release engineer',
|
||
|
|
directRunReads: false,
|
||
|
|
managedHostReads: false,
|
||
|
|
generated: false,
|
||
|
|
recommendedAction: 'treat as deployment packaging, not package/runtime config',
|
||
|
|
notes: ['Platform deployment files sit outside direct and managed package execution.'],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return classification({
|
||
|
|
kind: 'unknown',
|
||
|
|
owner: 'unknown',
|
||
|
|
authoredBy: 'unknown',
|
||
|
|
directRunReads: false,
|
||
|
|
managedHostReads: false,
|
||
|
|
generated: false,
|
||
|
|
recommendedAction: 'inspect before changing; add a classifier case if this becomes a recurring config path',
|
||
|
|
notes: ['No current config simplification rule matched this path.'],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function classification(
|
||
|
|
result: Omit<IConfigExplainResult, 'path' | 'relativePath' | 'exists'>,
|
||
|
|
): Omit<IConfigExplainResult, 'path' | 'relativePath' | 'exists'> {
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
function normalizePath(value: string): string {
|
||
|
|
return value.replace(/\\/g, '/');
|
||
|
|
}
|