clawdie-ai/scripts/tenant-lifecycle.ts
Clawdie AI 9605c7ad81 refactor(multitenant): collapse planTenantApply allowedResources duplication
Drop the allowedResources field from TenantApplyPlan — it was derived
field-for-field from resourceChecklist already, which was exactly the
"triplicate representation" flagged in the handoff's consolidation list.
Update scripts/tenant-lifecycle.ts to compute the same lists from the
checklist when it prints, and drop the tautological equality assertions
from the test (resourceChecklist is now the single source).

---
Build: pass | Tests: pass — 33 passed (1 file)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 19:12:12 +02:00

293 lines
9 KiB
TypeScript

import {
addTenantRecord,
getTenantRecord,
planTenantApply,
listTenantRecords,
planTenantProvisioning,
planTenantRemoval,
} from '../src/tenant-registry.js';
function usage(): never {
console.error(
'Usage: npx tsx scripts/tenant-lifecycle.ts <list|show|plan|apply|add|remove> [tenant-id]\n' +
' [--domain value] [--display-name value] [--internal-domain value]\n' +
' [--service value] [--dataset zroot/path ...]',
);
process.exit(1);
}
interface CliOptions {
tenantId?: string;
domain?: string;
displayName?: string;
internalDomain?: string;
service?: string;
datasets?: string[];
}
function parseOptions(args: string[]): CliOptions {
const result: CliOptions = {};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (!arg) continue;
if (!arg.startsWith('--') && !result.tenantId) {
result.tenantId = arg;
continue;
}
if (arg === '--domain') {
result.domain = args[++i] || '';
continue;
}
if (arg === '--display-name') {
result.displayName = args[++i] || '';
continue;
}
if (arg === '--internal-domain') {
result.internalDomain = args[++i] || '';
continue;
}
if (arg === '--service') {
result.service = args[++i] || '';
continue;
}
if (arg === '--dataset') {
const value = args[++i] || '';
if (value) {
result.datasets = [...(result.datasets || []), value];
}
continue;
}
usage();
}
return result;
}
function draftOptionsFromCli(options: CliOptions): {
displayName?: string;
domain?: string;
internalDomain?: string;
service?: string;
datasets?: string[];
} {
return {
displayName: options.displayName,
domain: options.domain,
internalDomain: options.internalDomain,
service: options.service,
datasets: options.datasets,
};
}
function printTenantSummary(
tenant: ReturnType<typeof planTenantProvisioning>['tenant'],
): void {
console.log(`Tenant: ${tenant.id}`);
console.log(`Display: ${tenant.displayName}`);
console.log(`Service: ${tenant.service}`);
console.log(`Internal domain: ${tenant.internalDomain}`);
console.log(`Databases: ${Object.values(tenant.databases).join(', ')}`);
console.log(`Worker jails: ${tenant.workerJails.join(', ')}`);
console.log(`Datasets: ${tenant.datasets.join(', ') || '(none)'}`);
}
function printProvisioningPlan(
plan: ReturnType<typeof planTenantProvisioning>,
): void {
console.log(
plan.alreadyExists
? `Tenant ${plan.tenant.id} already exists in registry. Declarative contract shown for review.`
: `Provisioning contract for new tenant ${plan.tenant.id}:`,
);
console.log(`Display: ${plan.tenant.displayName}`);
console.log(
`Registry entry: ${plan.declarativeChanges.addRegistryEntry ? 'would be added declaratively' : 'already present'}`,
);
console.log(`Service: ${plan.logicalResources.service}`);
console.log(`Internal domain: ${plan.logicalResources.internalDomain}`);
console.log(`Databases: ${plan.logicalResources.databases.join(', ')}`);
console.log(`Worker jails: ${plan.logicalResources.workerJails.join(', ')}`);
console.log(`Datasets: ${plan.logicalResources.datasets.join(', ') || '(none)'}`);
console.log('Excluded from this workflow:');
for (const item of plan.excludedResources) {
console.log(`- ${item}`);
}
console.log('Policy:');
for (const note of plan.policyNotes) {
console.log(`- ${note}`);
}
}
function printRemovalPlan(
plan: ReturnType<typeof planTenantRemoval>,
): void {
console.log(`Removal plan for tenant ${plan.tenant.id}:`);
console.log(`Display: ${plan.tenant.displayName}`);
console.log(
`Registry removal: ${plan.declarativeChanges.removeRegistryEntry ? 'allowed in future declarative flow' : 'blocked'}`,
);
console.log(`Service: ${plan.impacts.service}`);
console.log(`Internal domain: ${plan.impacts.internalDomain}`);
console.log(`Databases: ${plan.impacts.databases.join(', ')}`);
console.log(`Worker jails: ${plan.impacts.workerJails.join(', ')}`);
console.log(`Datasets: ${plan.impacts.datasets.join(', ') || '(none)'}`);
console.log(`Protected platform id: ${plan.protectedResources.platformId}`);
console.log(
`Protected shared services: ${plan.protectedResources.sharedServices.join(', ') || '(none)'}`,
);
console.log(
`Protected shared datasets: ${plan.protectedResources.sharedDatasets.join(', ') || '(none)'}`,
);
console.log(
`Protected shared jails: ${plan.protectedResources.sharedJails.join(', ') || '(none)'}`,
);
if (plan.blockers.length > 0) {
console.log('Blockers:');
for (const blocker of plan.blockers) {
console.log(`- ${blocker}`);
}
}
console.log('Policy:');
for (const note of plan.policyNotes) {
console.log(`- ${note}`);
}
console.log('');
console.log('No changes were made. This is a dry-run only.');
}
function printApplyPlan(
plan: ReturnType<typeof planTenantApply>,
): void {
console.log(`Apply plan for tenant ${plan.tenant.id}:`);
console.log(`Display: ${plan.tenant.displayName}`);
console.log(
`Automatic apply: ${plan.readyForAutomaticApply ? 'ready' : 'not ready'}`,
);
console.log('Contract checklist:');
for (const entry of plan.contractChecklist) {
console.log(`- [${entry.status}] ${entry.field} ${entry.value}: ${entry.detail}`);
}
const allowedDatabases = plan.resourceChecklist.databases.map((e) => e.name);
const allowedWorkerJails = plan.resourceChecklist.workerJails.map((e) => e.name);
const allowedDatasets = plan.resourceChecklist.datasets.map((e) => e.name);
console.log(`Allowed databases: ${allowedDatabases.join(', ')}`);
console.log(`Allowed worker jails: ${allowedWorkerJails.join(', ')}`);
console.log(`Allowed datasets: ${allowedDatasets.join(', ') || '(none)'}`);
console.log('Normalization hints:');
for (const hint of plan.normalizationHints) {
console.log(`- ${hint}`);
}
console.log('Resource checklist:');
for (const entry of plan.resourceChecklist.databases) {
console.log(`- [${entry.status}] database ${entry.name}: ${entry.detail}`);
}
for (const entry of plan.resourceChecklist.workerJails) {
console.log(`- [${entry.status}] worker-jail ${entry.name}: ${entry.detail}`);
}
for (const entry of plan.resourceChecklist.datasets) {
console.log(`- [${entry.status}] dataset ${entry.name}: ${entry.detail}`);
}
console.log('Action policy:');
for (const action of plan.actionPolicy.automaticCandidates) {
console.log(`- [future-auto] ${action.name}: ${action.detail}`);
}
for (const action of plan.actionPolicy.manualOnly) {
console.log(`- [manual] ${action.name}: ${action.detail}`);
}
for (const action of plan.actionPolicy.outOfScope) {
console.log(`- [out-of-scope] ${action.name}: ${action.detail}`);
}
if (plan.blockers.length > 0) {
console.log('Blockers:');
for (const blocker of plan.blockers) {
console.log(`- ${blocker}`);
}
}
console.log('Preflight:');
for (const check of plan.preflightChecks) {
console.log(`- [${check.status}] ${check.name}: ${check.detail}`);
}
console.log('Prerequisites:');
for (const item of plan.prerequisites) {
console.log(`- ${item}`);
}
console.log('Manual steps:');
for (const item of plan.manualSteps) {
console.log(`- ${item}`);
}
console.log('Policy:');
for (const note of plan.policyNotes) {
console.log(`- ${note}`);
}
console.log('');
console.log('No changes were made. This is a dry-run only.');
}
function main(argv: string[]): void {
try {
const [command, ...rest] = argv;
if (!command) usage();
if (command === 'list') {
for (const tenant of listTenantRecords()) {
console.log(
`${tenant.id}\t${tenant.service}\t${tenant.internalDomain}\t${tenant.displayName}`,
);
}
return;
}
const options = parseOptions(rest);
if (!options.tenantId) usage();
if (command === 'show') {
const tenant = getTenantRecord(options.tenantId);
if (!tenant) {
console.error(`Unknown tenant: ${options.tenantId}`);
process.exit(1);
}
printTenantSummary(tenant);
return;
}
if (command === 'plan') {
const plan = planTenantProvisioning(
options.tenantId,
draftOptionsFromCli(options),
);
printProvisioningPlan(plan);
return;
}
if (command === 'add') {
const added = addTenantRecord(
options.tenantId,
draftOptionsFromCli(options),
);
console.log(`Added tenant ${added.id} to infra/tenants.yaml`);
printTenantSummary(added);
return;
}
if (command === 'apply') {
const plan = planTenantApply(options.tenantId);
printApplyPlan(plan);
return;
}
if (command === 'remove') {
const plan = planTenantRemoval(options.tenantId);
printRemovalPlan(plan);
return;
}
usage();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(message);
process.exit(1);
}
}
main(process.argv.slice(2));