mirror of
https://github.com/selfxyz/self.git
synced 2026-01-09 14:48:06 -05:00
* refactor: remove namespace imports * refactor: use named fs imports * refactor(app): replace path and fs namespace imports * format * format
385 lines
11 KiB
JavaScript
385 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||
// SPDX-License-Identifier: BUSL-1.1
|
||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||
|
||
/**
|
||
* Migration tool to convert composite SPDX headers to canonical multi-line format
|
||
*/
|
||
|
||
import { readdirSync, statSync, readFileSync, writeFileSync } from 'fs';
|
||
import path from 'path';
|
||
|
||
// Current composite format
|
||
const COMPOSITE_HEADER =
|
||
'// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11';
|
||
|
||
// New canonical format
|
||
const CANONICAL_HEADER_LINES = [
|
||
'// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.',
|
||
'// SPDX-License-Identifier: BUSL-1.1',
|
||
'// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.',
|
||
];
|
||
|
||
function findFiles(
|
||
dir,
|
||
extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
|
||
) {
|
||
const files = [];
|
||
|
||
function traverse(currentDir) {
|
||
const items = readdirSync(currentDir);
|
||
|
||
for (const item of items) {
|
||
const fullPath = path.join(currentDir, item);
|
||
const stat = statSync(fullPath);
|
||
|
||
if (stat.isDirectory()) {
|
||
// Skip common directories
|
||
if (
|
||
![
|
||
'node_modules',
|
||
'.git',
|
||
'dist',
|
||
'build',
|
||
'coverage',
|
||
'ios',
|
||
'android',
|
||
'.next',
|
||
'.turbo',
|
||
'target',
|
||
'Pods',
|
||
'.tamagui',
|
||
].includes(item)
|
||
) {
|
||
traverse(fullPath);
|
||
}
|
||
} else if (extensions.some(ext => item.endsWith(ext))) {
|
||
files.push(fullPath);
|
||
}
|
||
}
|
||
}
|
||
|
||
traverse(dir);
|
||
return files;
|
||
}
|
||
|
||
function analyzeFile(filePath) {
|
||
const content = readFileSync(filePath, 'utf8');
|
||
const lines = content.split('\n');
|
||
|
||
let i = 0;
|
||
// Skip shebang if present
|
||
if (lines[i]?.startsWith('#!')) i++;
|
||
// Skip leading blank lines
|
||
while (i < lines.length && lines[i].trim() === '') i++;
|
||
|
||
const currentLine = lines[i];
|
||
|
||
if (currentLine === COMPOSITE_HEADER) {
|
||
return {
|
||
type: 'composite',
|
||
headerIndex: i,
|
||
needsMigration: true,
|
||
};
|
||
} else if (currentLine === CANONICAL_HEADER_LINES[0]) {
|
||
// Check if it's the full canonical format
|
||
const isCanonical =
|
||
lines[i + 1] === CANONICAL_HEADER_LINES[1] &&
|
||
lines[i + 2] === CANONICAL_HEADER_LINES[2];
|
||
return {
|
||
type: isCanonical ? 'canonical' : 'partial_canonical',
|
||
headerIndex: i,
|
||
needsMigration: !isCanonical,
|
||
};
|
||
} else if (currentLine?.includes('SPDX-License-Identifier')) {
|
||
return {
|
||
type: 'other_spdx',
|
||
headerIndex: i,
|
||
needsMigration: false,
|
||
};
|
||
} else {
|
||
return {
|
||
type: 'no_header',
|
||
headerIndex: -1,
|
||
needsMigration: false,
|
||
};
|
||
}
|
||
}
|
||
|
||
function migrateFile(filePath, dryRun = false) {
|
||
const content = readFileSync(filePath, 'utf8');
|
||
const lines = content.split('\n');
|
||
const analysis = analyzeFile(filePath);
|
||
|
||
if (!analysis.needsMigration) {
|
||
return { success: false, reason: 'No migration needed' };
|
||
}
|
||
|
||
if (analysis.type === 'composite') {
|
||
// Replace the composite line with canonical lines
|
||
lines.splice(analysis.headerIndex, 1, ...CANONICAL_HEADER_LINES);
|
||
|
||
if (!dryRun) {
|
||
const newContent = lines.join('\n');
|
||
writeFileSync(filePath, newContent, 'utf8');
|
||
}
|
||
|
||
return { success: true, reason: 'Migrated composite to canonical' };
|
||
}
|
||
|
||
return { success: false, reason: 'Unknown migration path' };
|
||
}
|
||
|
||
function removeHeaderFromFile(filePath, dryRun = false) {
|
||
const content = readFileSync(filePath, 'utf8');
|
||
const lines = content.split('\n');
|
||
const analysis = analyzeFile(filePath);
|
||
|
||
if (analysis.headerIndex === -1) {
|
||
return { success: false, reason: 'No header found' };
|
||
}
|
||
|
||
if (analysis.type === 'composite') {
|
||
// Remove the composite header line
|
||
lines.splice(analysis.headerIndex, 1);
|
||
|
||
// Also remove the following empty line if it exists
|
||
if (lines[analysis.headerIndex] === '') {
|
||
lines.splice(analysis.headerIndex, 1);
|
||
}
|
||
|
||
if (!dryRun) {
|
||
const newContent = lines.join('\n');
|
||
writeFileSync(filePath, newContent, 'utf8');
|
||
}
|
||
|
||
return { success: true, reason: 'Removed composite header' };
|
||
} else if (analysis.type === 'canonical') {
|
||
// Remove all 3 canonical header lines
|
||
lines.splice(analysis.headerIndex, 3);
|
||
|
||
// Also remove the following empty line if it exists
|
||
if (lines[analysis.headerIndex] === '') {
|
||
lines.splice(analysis.headerIndex, 1);
|
||
}
|
||
|
||
if (!dryRun) {
|
||
const newContent = lines.join('\n');
|
||
writeFileSync(filePath, newContent, 'utf8');
|
||
}
|
||
|
||
return { success: true, reason: 'Removed canonical header' };
|
||
}
|
||
|
||
return { success: false, reason: 'Unknown header type' };
|
||
}
|
||
|
||
function generateReport(projectRoot) {
|
||
const files = findFiles(projectRoot);
|
||
const report = {
|
||
composite: [],
|
||
canonical: [],
|
||
partial_canonical: [],
|
||
other_spdx: [],
|
||
no_header: [],
|
||
total: files.length,
|
||
};
|
||
|
||
for (const file of files) {
|
||
const analysis = analyzeFile(file);
|
||
report[analysis.type].push({
|
||
file: path.relative(projectRoot, file),
|
||
needsMigration: analysis.needsMigration,
|
||
});
|
||
}
|
||
|
||
return report;
|
||
}
|
||
|
||
function main() {
|
||
const args = process.argv.slice(2);
|
||
const command = args[0];
|
||
const isDryRun = args.includes('--dry-run');
|
||
const isVerbose = args.includes('--verbose');
|
||
const projectRoot =
|
||
args.find(arg => !arg.startsWith('--') && arg !== command) || process.cwd();
|
||
|
||
switch (command) {
|
||
case 'analyze':
|
||
case 'report': {
|
||
const report = generateReport(projectRoot);
|
||
|
||
console.log('📊 License Header Migration Analysis\n');
|
||
console.log(`Total files analyzed: ${report.total}\n`);
|
||
|
||
console.log(
|
||
`🔶 Composite headers (need migration): ${report.composite.length}`,
|
||
);
|
||
if (report.composite.length > 0 && isVerbose) {
|
||
report.composite
|
||
.slice(0, 10)
|
||
.forEach(item => console.log(` ${item.file}`));
|
||
if (report.composite.length > 10) {
|
||
console.log(` ... and ${report.composite.length - 10} more`);
|
||
}
|
||
console.log();
|
||
}
|
||
|
||
console.log(`✅ Canonical headers: ${report.canonical.length}`);
|
||
console.log(
|
||
`⚠️ Partial canonical headers: ${report.partial_canonical.length}`,
|
||
);
|
||
console.log(`ℹ️ Other SPDX headers: ${report.other_spdx.length}`);
|
||
console.log(`❌ No headers: ${report.no_header.length}`);
|
||
|
||
if (report.composite.length > 0) {
|
||
console.log(
|
||
`\n🚀 Ready to migrate ${report.composite.length} files with composite headers`,
|
||
);
|
||
console.log(
|
||
'Run: node scripts/migrate-license-headers.mjs migrate [--dry-run] to proceed',
|
||
);
|
||
} else {
|
||
console.log(
|
||
'\n✨ No migration needed - all headers are already in canonical format!',
|
||
);
|
||
}
|
||
|
||
break;
|
||
}
|
||
|
||
case 'migrate': {
|
||
const files = findFiles(projectRoot);
|
||
const results = { migrated: 0, skipped: 0, errors: 0 };
|
||
|
||
console.log(
|
||
`🔄 ${isDryRun ? 'DRY RUN: ' : ''}Migrating license headers...\n`,
|
||
);
|
||
|
||
for (const file of files) {
|
||
try {
|
||
const result = migrateFile(file, isDryRun);
|
||
if (result.success) {
|
||
results.migrated++;
|
||
console.log(
|
||
`✅ ${isDryRun ? '[DRY RUN] ' : ''}Migrated: ${path.relative(projectRoot, file)}`,
|
||
);
|
||
} else {
|
||
results.skipped++;
|
||
if (isVerbose) {
|
||
console.log(
|
||
`⏭️ Skipped: ${path.relative(projectRoot, file)} (${result.reason})`,
|
||
);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
results.errors++;
|
||
console.error(`❌ Error processing ${file}: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
console.log(`\n📊 Migration Summary:`);
|
||
console.log(` Migrated: ${results.migrated}`);
|
||
console.log(` Skipped: ${results.skipped}`);
|
||
console.log(` Errors: ${results.errors}`);
|
||
|
||
if (isDryRun && results.migrated > 0) {
|
||
console.log('\n🚀 Run without --dry-run to apply changes');
|
||
}
|
||
|
||
break;
|
||
}
|
||
|
||
case 'remove': {
|
||
const files = findFiles(projectRoot);
|
||
const results = { removed: 0, skipped: 0, errors: 0 };
|
||
|
||
console.log(
|
||
`🗑️ ${isDryRun ? 'DRY RUN: ' : ''}Removing license headers...\n`,
|
||
);
|
||
|
||
for (const file of files) {
|
||
try {
|
||
const result = removeHeaderFromFile(file, isDryRun);
|
||
if (result.success) {
|
||
results.removed++;
|
||
console.log(
|
||
`✅ ${isDryRun ? '[DRY RUN] ' : ''}Removed: ${path.relative(projectRoot, file)}`,
|
||
);
|
||
} else {
|
||
results.skipped++;
|
||
if (isVerbose) {
|
||
console.log(
|
||
`⏭️ Skipped: ${path.relative(projectRoot, file)} (${result.reason})`,
|
||
);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
results.errors++;
|
||
console.error(`❌ Error processing ${file}: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
console.log(`\n📊 Removal Summary:`);
|
||
console.log(` Removed: ${results.removed}`);
|
||
console.log(` Skipped: ${results.skipped}`);
|
||
console.log(` Errors: ${results.errors}`);
|
||
|
||
if (isDryRun && results.removed > 0) {
|
||
console.log('\n🚀 Run without --dry-run to apply changes');
|
||
}
|
||
|
||
break;
|
||
}
|
||
|
||
case 'migrate-single': {
|
||
const targetFile = args[1];
|
||
if (!targetFile) {
|
||
console.error('❌ Please specify a file to migrate');
|
||
process.exit(1);
|
||
}
|
||
|
||
const fullPath = path.resolve(targetFile);
|
||
const result = migrateFile(fullPath, isDryRun);
|
||
|
||
if (result.success) {
|
||
console.log(
|
||
`✅ ${isDryRun ? '[DRY RUN] ' : ''}Migrated: ${targetFile}`,
|
||
);
|
||
} else {
|
||
console.log(`⏭️ ${targetFile}: ${result.reason}`);
|
||
}
|
||
|
||
break;
|
||
}
|
||
|
||
default:
|
||
console.log(`Usage: node scripts/migrate-license-headers.mjs <command> [options]
|
||
|
||
Commands:
|
||
analyze, report Generate analysis report of current header formats
|
||
migrate Migrate all composite headers to canonical format
|
||
remove Remove license headers from files
|
||
migrate-single Migrate a single file
|
||
|
||
Options:
|
||
--dry-run Show what would be changed without making changes
|
||
--verbose Show detailed output
|
||
|
||
Examples:
|
||
node scripts/migrate-license-headers.mjs analyze --verbose
|
||
node scripts/migrate-license-headers.mjs migrate --dry-run
|
||
node scripts/migrate-license-headers.mjs remove common --dry-run
|
||
node scripts/migrate-license-headers.mjs migrate packages/mobile-sdk-alpha
|
||
node scripts/migrate-license-headers.mjs migrate-single src/index.ts --dry-run
|
||
`);
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||
main();
|
||
}
|