Files
NianToB/scripts/bundle-preinstalled-skills.mjs

674 lines
30 KiB
JavaScript

#!/usr/bin/env zx
import 'zx/globals';
import { readFileSync, existsSync, mkdirSync, rmSync, cpSync, writeFileSync, readdirSync, statSync } from 'node:fs';
import { join, dirname, basename } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
const MANIFEST_PATH = join(ROOT, 'resources', 'skills', 'preinstalled-manifest.json');
const OUTPUT_ROOT = join(ROOT, 'build', 'preinstalled-skills');
const TMP_ROOT = join(ROOT, 'build', '.tmp-preinstalled-skills');
function loadManifest() {
if (!existsSync(MANIFEST_PATH)) {
throw new Error(`Missing manifest: ${MANIFEST_PATH}`);
}
const raw = readFileSync(MANIFEST_PATH, 'utf8');
const parsed = JSON.parse(raw);
if (!parsed || !Array.isArray(parsed.skills)) {
throw new Error('Invalid preinstalled-skills manifest format');
}
for (const item of parsed.skills) {
const hasRemoteSource = Boolean(item.repo && item.repoPath);
const hasLocalSource = Boolean(item.localPath);
if (!item.slug || (hasRemoteSource === hasLocalSource)) {
throw new Error(`Invalid manifest entry: ${JSON.stringify(item)}`);
}
}
return parsed.skills;
}
function groupByRepoRef(entries) {
const grouped = new Map();
for (const entry of entries) {
if (entry.localPath) continue;
const ref = entry.ref || 'main';
const key = `${entry.repo}#${ref}`;
if (!grouped.has(key)) grouped.set(key, { repo: entry.repo, ref, entries: [] });
grouped.get(key).entries.push(entry);
}
return [...grouped.values()];
}
function createRepoDirName(repo, ref) {
return `${repo.replace(/[\\/]/g, '__')}__${ref.replace(/[^a-zA-Z0-9._-]/g, '_')}`;
}
function toGitPath(inputPath) {
if (process.platform !== 'win32') return inputPath;
// Git on Windows accepts forward slashes and avoids backslash escape quirks.
return inputPath.replace(/\\/g, '/');
}
function normalizeRepoPath(repoPath) {
return repoPath.replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+$/, '');
}
function shouldCopySkillFile(srcPath) {
const base = basename(srcPath);
if (base === '.git') return false;
if (base === '.subset.tar') return false;
return true;
}
function toYamlString(value) {
if (/^[a-zA-Z0-9_-]+$/.test(value)) return value;
return JSON.stringify(value);
}
function rewriteSkillName(skillManifest, name) {
const raw = readFileSync(skillManifest, 'utf8');
const frontmatterMatch = raw.match(/^---\n([\s\S]*?)\n---/);
if (!frontmatterMatch) return;
const body = frontmatterMatch[1];
const nextBody = /^name:\s*.*$/m.test(body)
? body.replace(/^name:\s*.*$/m, `name: ${toYamlString(name)}`)
: `name: ${toYamlString(name)}\n${body}`;
const next = raw.replace(/^---\n[\s\S]*?\n---/, `---\n${nextBody}\n---`);
if (next !== raw) writeFileSync(skillManifest, next, 'utf8');
}
const runtimePolicies = new Map([
['docx', `## Zhinian Runtime Policy
- Do not ask the user to install any desktop office suite or GUI converter.
- For normal .docx creation/editing, use the OpenXML/.NET workflow in this skill.
- If the .NET runtime is unavailable, run the skill setup/repair path silently where possible; report a clear environment limitation only if document generation is blocked.
- If optional .doc conversion or visual preview requires an unavailable desktop converter, skip that optional step and continue. Mention the limitation only when it directly blocks the requested output.
`],
['xlsx', `## Zhinian Runtime Policy
- Do not ask the user to install any desktop office suite or GUI converter.
- Use the XML/static validation workflow as the default path.
- When running Python scripts, prefer \`uv run python\`. If a Python dependency is missing, use \`uv pip install\` instead of asking the user to install it manually.
- Do not perform desktop-engine formula recalculation in the Zhinian bundled runtime; use static validation with \`formula_check.py\`.
`],
['pptx', `## Zhinian Runtime Policy
- Do not ask the user to install any desktop office suite or GUI converter.
- Use the PptxGenJS/XML workflows in this skill as the default path.
- Use the project-bundled Node/npm modules when available. Do not ask the user to run global npm installs for \`pptxgenjs\`, \`react-icons\`, \`react\`, \`react-dom\`, or \`sharp\`.
- If optional preview/conversion tools are unavailable, skip those optional steps and continue. Mention the limitation only when it directly blocks the requested output.
`],
['pdf', `## Zhinian Runtime Policy
- Do not ask the user to install desktop office suites or unrelated external GUI tools.
- Use the Python/HTML/PDF workflows in this skill and keep outputs in project-accessible local paths.
- When running Python scripts, prefer \`uv run python\`. If a Python dependency is missing, use \`uv pip install\` instead of asking the user to install it manually.
- If an optional browser renderer is unavailable, choose another supported path or report the limitation only when it directly blocks the requested output.
- Never run task-time Playwright or Chromium installers inside a user task. Chromium is prepared by the app/runtime layer; task-time browser downloads can collide with other runs.
`],
['html-slides', `## Zhinian Runtime Policy
- Use the project/app-bundled browser runtime for optional preview checks.
- Never run \`npx playwright install chromium\`, \`playwright install chromium\`, or \`npm install -g playwright\` inside a user task.
- If browser preview is unavailable, validate the deck by static file checks and report that browser preview was skipped. Do not block deck delivery on a task-time Chromium download.
`],
]);
function insertAfterFrontmatter(raw, content) {
const frontmatterMatch = raw.match(/^---\n[\s\S]*?\n---\n?/);
if (!frontmatterMatch) {
return `${content}\n${raw}`;
}
const index = frontmatterMatch[0].length;
return `${raw.slice(0, index)}${content}\n${raw.slice(index)}`;
}
function applyRuntimePolicy(skillManifest, slug) {
const policy = runtimePolicies.get(slug);
if (!policy) return;
const raw = readFileSync(skillManifest, 'utf8');
if (raw.includes('## Zhinian Runtime Policy')) return;
writeFileSync(skillManifest, insertAfterFrontmatter(raw, policy), 'utf8');
}
function replaceFileText(filePath, replacements) {
if (!existsSync(filePath)) return;
let next = readFileSync(filePath, 'utf8');
const original = next;
for (const [pattern, replacement] of replacements) {
next = next.replace(pattern, replacement);
}
if (next !== original) writeFileSync(filePath, next, 'utf8');
}
function listTextFiles(rootDir) {
if (!existsSync(rootDir)) return [];
const files = [];
const visit = (dir) => {
for (const entry of readdirSync(dir)) {
const fullPath = join(dir, entry);
const stats = statSync(fullPath);
if (stats.isDirectory()) {
visit(fullPath);
} else if (stats.isFile()) {
files.push(fullPath);
}
}
};
visit(rootDir);
return files;
}
function findMacAppLikeDirectories(rootDir) {
if (!existsSync(rootDir)) return [];
const hits = [];
const visit = (dir) => {
for (const entry of readdirSync(dir)) {
const fullPath = join(dir, entry);
const stats = statSync(fullPath);
if (!stats.isDirectory()) continue;
if (entry.endsWith('.app')) {
hits.push(fullPath);
}
visit(fullPath);
}
};
visit(rootDir);
return hits;
}
function assertNoMacAppLikeDirectories(rootDir) {
const hits = findMacAppLikeDirectories(rootDir);
if (!hits.length) return;
const formatted = hits
.map((hit) => ` - ${hit.replace(`${ROOT}/`, '')}`)
.join('\n');
throw new Error(`Preinstalled skills must not contain .app directories because macOS codesign treats them as app bundles:\n${formatted}`);
}
function sanitizeDesktopOfficeEngineTerms(targetDir, slug) {
if (!['docx', 'xlsx', 'pptx', 'pdf'].includes(slug)) return;
if (slug === 'xlsx') {
rmSync(join(targetDir, 'scripts', 'libreoffice_recalc.py'), { force: true });
rmSync(join(targetDir, 'recalc.py'), { force: true });
mkdirSync(join(targetDir, 'references'), { recursive: true });
writeFileSync(join(targetDir, 'references', 'validate.md'), `# XLSX Formula Validation
Use static validation only in the Zhinian bundled runtime.
## Standard Path
1. Run \`formula_check.py\` on the workbook.
2. Fix malformed formulas, missing references, circular references, or packaging errors reported by the script.
3. Repack the workbook when XML edits are needed.
4. Run \`formula_check.py\` again and deliver only after it exits successfully.
Do not ask the user to install desktop office suites or GUI converters. Do not run desktop-engine recalculation in the bundled runtime.
`, 'utf8');
writeFileSync(join(targetDir, 'formula_check.py'), `#!/usr/bin/env python3
import json
import sys
from pathlib import Path
from openpyxl import load_workbook
EXCEL_ERRORS = ["#VALUE!", "#DIV/0!", "#REF!", "#NAME?", "#NULL!", "#NUM!", "#N/A"]
def scan_workbook(filename):
path = Path(filename)
if not path.exists():
return {"error": f"File {filename} does not exist"}
result = {
"status": "success",
"total_errors": 0,
"total_formulas": 0,
"error_summary": {},
}
wb_formulas = load_workbook(path, data_only=False)
try:
for sheet_name in wb_formulas.sheetnames:
sheet = wb_formulas[sheet_name]
for row in sheet.iter_rows():
for cell in row:
value = cell.value
if isinstance(value, str) and value.startswith("="):
result["total_formulas"] += 1
for error in EXCEL_ERRORS:
if error in value:
result["error_summary"].setdefault(error, {"count": 0, "locations": []})
result["error_summary"][error]["count"] += 1
result["error_summary"][error]["locations"].append(f"{sheet_name}!{cell.coordinate}")
result["total_errors"] += 1
break
finally:
wb_formulas.close()
wb_values = load_workbook(path, data_only=True)
try:
for sheet_name in wb_values.sheetnames:
sheet = wb_values[sheet_name]
for row in sheet.iter_rows():
for cell in row:
value = cell.value
if not isinstance(value, str):
continue
for error in EXCEL_ERRORS:
if error in value:
result["error_summary"].setdefault(error, {"count": 0, "locations": []})
result["error_summary"][error]["count"] += 1
result["error_summary"][error]["locations"].append(f"{sheet_name}!{cell.coordinate}")
result["total_errors"] += 1
break
finally:
wb_values.close()
if result["total_errors"] > 0:
result["status"] = "errors_found"
return result
def main():
if len(sys.argv) < 2:
print("Usage: python formula_check.py <excel_file>")
sys.exit(1)
print(json.dumps(scan_workbook(sys.argv[1]), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
`, 'utf8');
}
if (slug === 'docx') {
rmSync(join(targetDir, 'scripts', 'doc_to_docx.sh'), { force: true });
}
for (const filePath of listTextFiles(targetDir)) {
let raw;
try {
raw = readFileSync(filePath, 'utf8');
} catch {
continue;
}
if (raw.includes('\u0000')) continue;
let next = raw
.replace(/libreoffice_recalc\.py/g, 'formula_check.py')
.replace(/\brecalc\.py\b/g, 'formula_check.py')
.replace(/scripts\/doc_to_docx\.sh\s+input\.doc\s+output_dir\/?/g, 'legacy .doc conversion is not bundled; request or create a .docx file instead')
.replace(/Convert `?\.doc`?\s*→\s*`?\.docx`? if needed:[^\n]*/g, 'Legacy .doc conversion is not bundled. Ask for a .docx source or create a new .docx output.')
.replace(/Excel\/LibreOffice/g, 'Excel-compatible')
.replace(/Word\/LibreOffice/g, 'Word-compatible')
.replace(/LibreOffice/g, 'desktop office engine')
.replace(/libreoffice/g, 'desktop-office-engine')
.replace(/\bsoffice\b/g, 'desktop office converter')
.replace(/\bLibra\b/g, 'desktop office engine')
.replace(/\blibra\b/g, 'desktop-office-engine')
.replace(/desktop office engine/g, 'compatible viewer')
.replace(/desktop-office-engine/g, 'compatible-viewer')
.replace(/desktop office converter/g, 'legacy document converter')
.replace(/dynamic recalculation/g, 'runtime formula execution')
.replace(/headless recalculation/g, 'static validation')
.replace(/`sudo apt-get install pandoc`/g, 'use bundled text extraction when available')
.replace(/`sudo apt-get install poppler-utils`/g, 'use bundled PDF/image tooling when available')
.replace(/`sudo apt-get install compatible-viewer`/g, 'skip optional PDF conversion if unavailable')
.replace(/`npm install -g docx`/g, 'use the project-bundled `docx` module when available')
.replace(/`npm install -g pptxgenjs`/g, 'use the project-bundled `pptxgenjs` module')
.replace(/`npm install -g playwright`/g, 'use the project-bundled Playwright runtime when available')
.replace(/`npx playwright install chromium`/g, 'use the project-bundled Chromium browser when available')
.replace(/`playwright install chromium`/g, 'use the project-bundled Chromium browser when available')
.replace(/\bnpx\s+playwright\s+install\s+chromium\b/g, 'use the project-bundled Chromium browser when available')
.replace(/\bplaywright\s+install\s+chromium\b/g, 'use the project-bundled Chromium browser when available')
.replace(/`npm install -g react-icons react react-dom`/g, 'use project-bundled `react-icons`, `react`, and `react-dom`')
.replace(/`npm install -g sharp`/g, 'use the project-bundled `sharp` module when available')
.replace(/`pip install defusedxml`/g, '`uv pip install defusedxml`')
.replace(/`pip install "markitdown\[pptx\]"`/g, '`uv pip install "markitdown[pptx]"`')
.replace(/# Requires: pip install ([^\n]+)/g, '# Requires: uv pip install $1')
.replace(/Assumes docx is already installed globally\s*If not installed: use the project-bundled `docx` module when available/g, 'Use the project-bundled `docx` module when available')
.replace(/Required dependencies \(install if not available\):/g, 'Runtime dependencies are managed by Zhinian when available:')
.replace(/Required dependencies \(should already be installed\):/g, 'Runtime dependencies are managed by Zhinian when available:');
if (slug === 'xlsx') {
next = next
.replace(/\*\*compatible viewer Required for Formula Recalculation\*\*:[^\n]*/g, '**Static formula validation is available in the bundled runtime**: Use `formula_check.py` to scan formulas and cached error values. Do not ask the user to install external office software.')
.replace(/Recalculating formulas/g, 'Static formula validation')
.replace(/Recalculate formulas/g, 'Validate formulas')
.replace(/Recalculates all formulas/g, 'Checks formulas')
.replace(/recalculate formulas/g, 'validate formulas')
.replace(/recalculation/g, 'static validation')
.replace(/recalculate again/g, 'validate again')
.replace(/Use `formula_check\.py` for dynamic recalculation when available\./g, 'Use `formula_check.py` for static validation.')
.replace(/Dynamic Validation \(desktop office engine Headless\)/g, 'Static Formula Validation')
.replace(/Tier 2 dynamic validation/g, 'Optional dynamic validation')
.replace(/Tier 2 — Dynamic Validation/g, 'Optional Dynamic Validation')
.replace(/Run `formula_check\.py` for static validation\. Use `formula_check\.py` for dynamic recalculation when available\./g, 'Run `formula_check.py` for static formula validation. Do not run desktop-engine recalculation in the bundled runtime.');
}
if (slug === 'docx') {
next = next
.replace(/install_soffice/g, 'skip_optional_doc_converter')
.replace(/soffice_ver/g, 'converter_ver')
.replace(/soffice_found/g, 'converter_found')
.replace(/\s*"\$SCRIPT_DIR\/doc_to_docx\.sh"\n/g, '')
.replace(/^\s*echo " desktop office converter:[^\n]*\n/gm, ' echo " legacy .doc conversion: not bundled (use .docx)"\n')
.replace(
/# --- Optional: desktop office engine ---[\s\S]*?(?=\n# --- Optional: zip\/unzip ---)/,
`# --- Legacy DOC Conversion ---
printf "[WARN] %-14s not bundled — use .docx files\\n" "legacy .doc conversion"
WARNINGS=$((WARNINGS + 1))
`,
);
}
if (next !== raw) writeFileSync(filePath, next, 'utf8');
}
}
function applyNoExternalOfficePrompts(targetDir, slug) {
if (slug === 'docx') {
replaceFileText(join(targetDir, 'scripts', 'setup.sh'), [
[
/# --- LibreOffice Installation \(Optional\) ---[\s\S]*?\n# --- zip\/unzip ---/,
`# --- Legacy DOC Conversion (Optional) ---
skip_optional_doc_converter() {
step "Checking legacy .doc conversion support"
warn "Legacy .doc conversion is not bundled; use .docx files in Zhinian runtime"
}
# --- zip/unzip ---`,
],
[/echo " --minimal Only install critical dependencies \(skip pandoc, soffice, fonts\)"/g, 'echo " --minimal Only install critical dependencies (skip optional tools)"'],
]);
replaceFileText(join(targetDir, 'scripts', 'setup.ps1'), [
[
/# --- LibreOffice \(Optional\) ---[\s\S]*?\n# --- NuGet Configuration ---/,
`# --- Legacy DOC Conversion (Optional) ---
if (-not $Minimal) {
Step "Checking legacy .doc conversion support"
Warn "Legacy .doc conversion is not bundled; use .docx files in Zhinian runtime"
}
# --- NuGet Configuration ---`,
],
[/-Minimal Only install critical dependencies \(skip pandoc, soffice, fonts\)/g, '-Minimal Only install critical dependencies (skip optional tools)'],
]);
replaceFileText(join(targetDir, 'scripts', 'env_check.sh'), [
[/echo " Install: brew install --cask libreoffice"/g, 'echo " Optional .doc conversion skipped in Zhinian runtime"'],
[/echo " Install: sudo apt-get install libreoffice-core"/g, 'echo " Optional .doc conversion skipped in Zhinian runtime"'],
[/echo " Install: winget install TheDocumentFoundation\.LibreOffice"/g, 'echo " Optional .doc conversion skipped in Zhinian runtime"'],
]);
replaceFileText(join(targetDir, 'scripts', 'doc_to_docx.sh'), [
[/echo "Install LibreOffice: brew install --cask libreoffice"/g, 'echo "Optional .doc conversion unavailable in Zhinian runtime; please provide a .docx file."'],
]);
}
if (slug === 'xlsx') {
replaceFileText(join(targetDir, 'scripts', 'xlsx_reader.py'), [
[/Run: pip install pandas openpyxl/g, 'Run: uv pip install pandas openpyxl'],
]);
replaceFileText(join(targetDir, 'references', 'validate.md'), [
[
/### Install LibreOffice \(if permitted in the environment\)[\s\S]*?\n### Run headless recalculation/,
`### Optional dynamic recalculation
Do not ask the user to install LibreOffice. If LibreOffice is not already available, record "Tier 2: SKIPPED — LibreOffice not available" and continue with Tier 1 static validation.
### Run headless recalculation`,
],
[
/\*\*If LibreOffice is not installed:\*\*[\s\S]*?\n\*\*If the script times out/,
`**If LibreOffice is not installed:**
Record "Tier 2: SKIPPED — LibreOffice not available" and continue. Do not provide installation commands to the user.
**If the script times out`,
],
]);
replaceFileText(join(targetDir, 'scripts', 'libreoffice_recalc.py'), [
[
/"LibreOffice not found\. Tier 2 validation is unavailable in this environment\. "\s*\n\s*"Install LibreOffice to enable dynamic formula recalculation\.\\n"\s*\n\s*" macOS: brew install --cask libreoffice\\n"\s*\n\s*" Linux: sudo apt-get install -y libreoffice"/,
`"LibreOffice not found. Tier 2 validation is skipped in Zhinian runtime. "`,
],
[/print\(" macOS: brew install --cask libreoffice"\)\n\s*print\(" Linux: sudo apt-get install -y libreoffice"\)/g, 'print("Tier 2 dynamic validation skipped; continue with static validation.")'],
]);
}
if (slug === 'pptx') {
replaceFileText(join(targetDir, 'SKILL.md'), [
[/`pip install "markitdown\[pptx\]"` — text extraction/g, '`uv pip install "markitdown[pptx]"` — text extraction when needed'],
[/`npm install -g pptxgenjs` — creating from scratch/g, 'Use the project-bundled `pptxgenjs` module for creating from scratch'],
[/`npm install -g react-icons react react-dom sharp` — icons \(optional\)/g, 'Use project-bundled `react-icons`, `react`, `react-dom`, and `sharp` when icons are needed'],
]);
replaceFileText(join(targetDir, 'references', 'pptxgenjs.md'), [
[/Install: `npm install -g react-icons react react-dom sharp`/g, 'Use project-bundled `react-icons`, `react`, `react-dom`, and `sharp`; do not ask the user to install them globally.'],
]);
}
if (slug === 'pdf') {
replaceFileText(join(targetDir, 'SKILL.md'), [
[/`pip install reportlab`/g, '`uv pip install reportlab`'],
[/`pip install pypdf`/g, '`uv pip install pypdf`'],
[/`npm install -g playwright && npx playwright install chromium`/g, 'use the project-bundled Playwright runtime when available'],
]);
replaceFileText(join(targetDir, 'README.md'), [
[/`pip install reportlab`/g, '`uv pip install reportlab`'],
[/`pip install pypdf`/g, '`uv pip install pypdf`'],
[/`npm install -g playwright && npx playwright install chromium`/g, 'optional; use project-bundled Playwright runtime when available'],
]);
replaceFileText(join(targetDir, 'scripts', 'make.sh'), [
[/pip install failed — try: pip install reportlab pypdf matplotlib/g, 'dependency install failed — try: uv pip install reportlab pypdf matplotlib'],
[/python3 -m pip install/g, 'uv pip install'],
[` # Playwright
if node -e "require('playwright')" 2>/dev/null || \\
node -e "require(require('child_process').execSync('npm root -g').toString().trim()+'/playwright')" 2>/dev/null; then
green " ✓ playwright"
else
yellow " ⚠ playwright not found (run: make.sh fix)"
ok=false
fi`, ` # Playwright is optional in Zhinian runtime.
if node -e "require('playwright')" 2>/dev/null || \\
node -e "require('playwright-core')" 2>/dev/null; then
green " ✓ playwright"
else
yellow " ⚠ playwright not found (optional cover rendering skipped)"
fi`],
[` # Playwright
if command -v npm &>/dev/null; then
npm install -g playwright --silent 2>/dev/null && \\
npx playwright install chromium --silent 2>/dev/null && \\
green " ✓ Playwright + Chromium installed" || \\
{ yellow " playwright install failed — try manually"; rc=3; }
else
yellow " npm not found — cannot install Playwright automatically"
rc=2
fi`, ` # Playwright is optional in Zhinian runtime. Do not install global npm packages.
yellow " Playwright cover rendering is optional; skipping global install"`],
]);
replaceFileText(join(targetDir, 'scripts', 'render_cover.js'), [
[
/\/\/ ── Playwright loader \(tolerates global npm installs\) ─────────────────────────[\s\S]*?\/\/ ── Main ───────────────────────────────────────────────────────────────────────/,
`// ── Playwright loader (Zhinian runtime) ─────────────────────────────────────
function loadPlaywright() {
try { return require("playwright"); } catch (_) {}
try { return require("playwright-core"); } catch (_) {}
console.error(JSON.stringify({
status: "error",
error: "playwright runtime not available",
hint: "Playwright cover rendering is optional in Zhinian runtime. Continue without the HTML cover or contact an administrator."
}));
process.exit(2);
}
// ── Main ───────────────────────────────────────────────────────────────────────`,
],
[
/ let browser;\n try {\n browser = await chromium\.launch\(\);\n } catch \(e\) {[\s\S]*? browser = await chromium\.launch\(\);\n }\n/,
` let browser;
try {
browser = await chromium.launch();
} catch (e) {
console.error(JSON.stringify({
status: "error",
error: "Chromium is not available for Playwright cover rendering",
hint: "Cover rendering is optional in Zhinian runtime. Continue without the HTML cover or contact an administrator."
}));
process.exit(2);
}
`,
],
]);
}
}
async function extractArchive(archiveFileName, cwd) {
const prevCwd = $.cwd;
$.cwd = cwd;
try {
try {
await $`tar -xf ${archiveFileName}`;
return;
} catch (tarError) {
if (process.platform === 'win32') {
// Some Windows images expose bsdtar instead of tar.
await $`bsdtar -xf ${archiveFileName}`;
return;
}
throw tarError;
}
} finally {
$.cwd = prevCwd;
}
}
async function fetchSparseRepo(repo, ref, paths, checkoutDir) {
const remote = `https://github.com/${repo}.git`;
mkdirSync(checkoutDir, { recursive: true });
const gitCheckoutDir = toGitPath(checkoutDir);
const archiveFileName = '.subset.tar';
const archivePath = join(checkoutDir, archiveFileName);
const archivePaths = [...new Set(paths.map(normalizeRepoPath))];
await $`git init ${gitCheckoutDir}`;
await $`git -C ${gitCheckoutDir} remote add origin ${remote}`;
await $`git -C ${gitCheckoutDir} fetch --depth 1 origin ${ref}`;
// Do not checkout working tree on Windows: upstream repos may contain
// Windows-invalid paths. Export only requested directories via git archive.
await $`git -C ${gitCheckoutDir} archive --format=tar --output ${archiveFileName} FETCH_HEAD ${archivePaths}`;
await extractArchive(archiveFileName, checkoutDir);
rmSync(archivePath, { force: true });
const commit = (await $`git -C ${gitCheckoutDir} rev-parse FETCH_HEAD`).stdout.trim();
return commit;
}
echo`Bundling preinstalled skills...`;
if (process.env.SKIP_PREINSTALLED_SKILLS === '1') {
echo`⏭ SKIP_PREINSTALLED_SKILLS=1 set, skipping skills fetch.`;
process.exit(0);
}
const manifestSkills = loadManifest();
rmSync(OUTPUT_ROOT, { recursive: true, force: true });
mkdirSync(OUTPUT_ROOT, { recursive: true });
rmSync(TMP_ROOT, { recursive: true, force: true });
mkdirSync(TMP_ROOT, { recursive: true });
const lock = {
generatedAt: new Date().toISOString(),
skills: [],
};
const groups = groupByRepoRef(manifestSkills);
for (const group of groups) {
const repoDir = join(TMP_ROOT, createRepoDirName(group.repo, group.ref));
const sparsePaths = [...new Set(group.entries.map((entry) => entry.repoPath))];
echo`Fetching ${group.repo} @ ${group.ref}`;
const commit = await fetchSparseRepo(group.repo, group.ref, sparsePaths, repoDir);
echo` commit ${commit}`;
for (const entry of group.entries) {
const sourceDir = join(repoDir, entry.repoPath);
const targetDir = join(OUTPUT_ROOT, entry.slug);
if (!existsSync(sourceDir)) {
throw new Error(`Missing source path in repo checkout: ${entry.repoPath}`);
}
rmSync(targetDir, { recursive: true, force: true });
cpSync(sourceDir, targetDir, { recursive: true, dereference: true, filter: shouldCopySkillFile });
const skillManifest = join(targetDir, 'SKILL.md');
if (!existsSync(skillManifest)) {
throw new Error(`Skill ${entry.slug} is missing SKILL.md after copy`);
}
rewriteSkillName(skillManifest, entry.skillName || entry.slug);
applyRuntimePolicy(skillManifest, entry.slug);
applyNoExternalOfficePrompts(targetDir, entry.slug);
sanitizeDesktopOfficeEngineTerms(targetDir, entry.slug);
const requestedVersion = (entry.version || '').trim();
const resolvedVersion = !requestedVersion || requestedVersion === 'main'
? commit
: requestedVersion;
lock.skills.push({
slug: entry.slug,
version: resolvedVersion,
repo: entry.repo,
repoPath: entry.repoPath,
ref: group.ref,
commit,
});
echo` OK ${entry.slug}`;
}
}
for (const entry of manifestSkills.filter((item) => item.localPath)) {
const sourceDir = join(ROOT, entry.localPath);
const targetDir = join(OUTPUT_ROOT, entry.slug);
if (!existsSync(sourceDir)) {
throw new Error(`Missing local skill source: ${entry.localPath}`);
}
if (!existsSync(join(sourceDir, 'SKILL.md'))) {
throw new Error(`Local skill ${entry.slug} is missing SKILL.md: ${entry.localPath}`);
}
rmSync(targetDir, { recursive: true, force: true });
cpSync(sourceDir, targetDir, { recursive: true, dereference: true, filter: shouldCopySkillFile });
const skillManifest = join(targetDir, 'SKILL.md');
rewriteSkillName(skillManifest, entry.skillName || entry.slug);
applyRuntimePolicy(skillManifest, entry.slug);
applyNoExternalOfficePrompts(targetDir, entry.slug);
sanitizeDesktopOfficeEngineTerms(targetDir, entry.slug);
const resolvedVersion = (entry.version || 'local').trim() || 'local';
lock.skills.push({
slug: entry.slug,
version: resolvedVersion,
localPath: entry.localPath,
ref: 'local',
commit: resolvedVersion,
});
echo` OK ${entry.slug} (local)`;
}
writeFileSync(join(OUTPUT_ROOT, '.preinstalled-lock.json'), `${JSON.stringify(lock, null, 2)}\n`, 'utf8');
assertNoMacAppLikeDirectories(OUTPUT_ROOT);
rmSync(TMP_ROOT, { recursive: true, force: true });
echo`Preinstalled skills ready: ${OUTPUT_ROOT}`;