2026-03-20 01:16:27 +08:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
/**
|
|
|
|
|
* Sync webapp skills: pins b2e and b2x template packages to latest npm versions,
|
|
|
|
|
* runs npm install, then copies skills from dist/.a4drules/skills/ into skills/.
|
|
|
|
|
* Run from repo root.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const fs = require('fs');
|
|
|
|
|
const path = require('path');
|
|
|
|
|
const { execSync } = require('child_process');
|
|
|
|
|
const { copyRecursive } = require('./lib/copy-recursive');
|
|
|
|
|
|
|
|
|
|
const TEMPLATE_PACKAGES = [
|
2026-03-31 04:50:18 +08:00
|
|
|
'@salesforce/ui-bundle-template-app-react-sample-b2e',
|
|
|
|
|
'@salesforce/ui-bundle-template-app-react-sample-b2x',
|
2026-03-20 01:16:27 +08:00
|
|
|
];
|
|
|
|
|
const PACKAGE_NAME = TEMPLATE_PACKAGES[0]; // used for syncing skills
|
|
|
|
|
const SKILLS_SRC = 'dist/.a4drules/skills';
|
|
|
|
|
|
|
|
|
|
const repoRoot = process.cwd();
|
|
|
|
|
const pkgPath = path.join(repoRoot, 'package.json');
|
|
|
|
|
const skillsDir = path.join(repoRoot, 'skills');
|
|
|
|
|
|
|
|
|
|
// ── Pin template packages to latest npm versions ────────────────────
|
|
|
|
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
|
|
|
let pkgChanged = false;
|
|
|
|
|
for (const name of TEMPLATE_PACKAGES) {
|
|
|
|
|
const current = (pkg.devDependencies || {})[name];
|
|
|
|
|
if (!current || current.startsWith('file:')) continue;
|
|
|
|
|
let latest;
|
|
|
|
|
try {
|
|
|
|
|
latest = execSync(`npm view ${name} version`, { encoding: 'utf8' }).trim();
|
|
|
|
|
} catch (_) {
|
|
|
|
|
console.warn(`Could not resolve ${name} on npm, using current version.`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (current !== latest) {
|
|
|
|
|
console.log(`${name}: ${current} -> ${latest}`);
|
|
|
|
|
pkg.devDependencies[name] = latest;
|
|
|
|
|
pkgChanged = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (pkgChanged) {
|
|
|
|
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Install ──────────────────────────────────────────────────────────
|
|
|
|
|
console.log('Installing dependencies...');
|
|
|
|
|
execSync('npm install', { cwd: repoRoot, stdio: 'inherit' });
|
|
|
|
|
|
|
|
|
|
// ── Sync skills ──────────────────────────────────────────────────────
|
|
|
|
|
const pkgRoot = path.join(repoRoot, 'node_modules', PACKAGE_NAME.replace('/', path.sep));
|
|
|
|
|
if (!fs.existsSync(pkgRoot)) {
|
|
|
|
|
console.error(`Package not found at ${pkgRoot}.`);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const srcDir = path.join(pkgRoot, SKILLS_SRC);
|
|
|
|
|
if (!fs.existsSync(srcDir)) {
|
|
|
|
|
console.error(`Skills not found at ${srcDir}.`);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true });
|
|
|
|
|
|
|
|
|
|
function addWebappPrefix(name) {
|
|
|
|
|
const parts = name.split('-');
|
|
|
|
|
if (parts.length < 2) return name;
|
|
|
|
|
if (parts[1] === 'webapp') return name;
|
|
|
|
|
return parts[0] + '-webapp-' + parts.slice(1).join('-');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Dirs in skills/ that look like synced webapp skills (e.g. *-webapp-*, creating-webapp). */
|
|
|
|
|
function isSyncedWebappSkillDir(name) {
|
|
|
|
|
return name.includes('webapp');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Set front matter `name` in SKILL.md to match the destination folder name. */
|
|
|
|
|
function setSkillFrontMatterName(skillDir, destName) {
|
|
|
|
|
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
|
|
|
if (!fs.existsSync(skillPath)) return;
|
|
|
|
|
let content = fs.readFileSync(skillPath, 'utf8');
|
|
|
|
|
content = content.replace(/^name:\s*.+$/m, `name: ${destName}`);
|
|
|
|
|
fs.writeFileSync(skillPath, content, 'utf8');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Clean up: remove skills no longer in the package ───────────────────
|
|
|
|
|
const srcNames = fs.readdirSync(srcDir).filter((name) =>
|
|
|
|
|
fs.statSync(path.join(srcDir, name)).isDirectory()
|
|
|
|
|
);
|
|
|
|
|
const currentDestNames = new Set(srcNames.map(addWebappPrefix));
|
|
|
|
|
|
|
|
|
|
for (const name of fs.readdirSync(skillsDir)) {
|
|
|
|
|
const dirPath = path.join(skillsDir, name);
|
|
|
|
|
if (!fs.statSync(dirPath).isDirectory()) continue;
|
|
|
|
|
if (!isSyncedWebappSkillDir(name)) continue;
|
|
|
|
|
if (currentDestNames.has(name)) continue;
|
|
|
|
|
fs.rmSync(dirPath, { recursive: true });
|
|
|
|
|
console.log(`Removed skills/${name}/ (no longer in package)`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Copy each skill from package ──────────────────────────────────────
|
|
|
|
|
const syncedDirs = [];
|
|
|
|
|
for (const srcName of srcNames) {
|
|
|
|
|
const src = path.join(srcDir, srcName);
|
|
|
|
|
const destName = addWebappPrefix(srcName);
|
|
|
|
|
const dest = path.join(skillsDir, destName);
|
|
|
|
|
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true });
|
|
|
|
|
copyRecursive(src, dest);
|
|
|
|
|
setSkillFrontMatterName(dest, destName);
|
|
|
|
|
syncedDirs.push(destName);
|
|
|
|
|
console.log(`Synced skills/${destName}/`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const version = JSON.parse(
|
|
|
|
|
fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8')
|
|
|
|
|
).version;
|
|
|
|
|
console.log(`Done — synced ${syncedDirs.length} skills from ${PACKAGE_NAME}@${version}.`);
|