HEX
Server: Apache/2.4.46 (Win64) OpenSSL/1.1.1j PHP/8.4.25
System: Windows NT DESKTOP-4TAV2RJ 10.0 build 19045 (Windows 10) AMD64
User: fred (0)
PHP: 8.4.25
Disabled: NONE
Upload Files
File: C:/Users/fred/.codex/.tmp/plugins/plugins/wix/skills/wix-headless/scripts/check-manifest.mjs
#!/usr/bin/env node
// Post-phase manifest check + template-copy recovery.
//
// Verifies that every file declared in each loaded pack's `creates:` array
// for the named phase exists on disk. If a file is missing, attempts to
// recover by copying the canonical template from <SKILL_ROOT>/references/astro/templates/<pack>/.
// Outputs a JSON summary of present / recovered / errored files.
//
// Usage (both modes work):
//   node <SKILL_ROOT>/scripts/check-manifest.mjs <project-dir> <phase> <packs-csv>
//   curl -s https://dev.wix.com/skills/wix-headless/scripts/check-manifest.mjs \
//     | node --input-type=module - <project-dir> <phase> <packs-csv>
//
//   <phase> ∈ { "components", "pages" }
//   <packs-csv> = comma-separated pack names (loaded verticals), e.g. "stores,ecom,cms"
//
// Note: `node <(curl ...)` does NOT work for .mjs files — Node sees /dev/fd/N
// with no extension and rejects ESM syntax. Use the stdin form above.
//
// Skill-local file reads (vertical pack markdowns, template files) auto-detect
// whether they can resolve on disk (tgz install) and fall back to HTTP fetch
// otherwise (stream via stdin).
//
// Behavior:
//   - For each pack, parses `references/verticals/<pack>.md` to extract the
//     `creates:` array.
//   - For each `creates:` entry where phase matches:
//       * If file exists in the project → "present"
//       * If missing AND a template exists at `references/astro/templates/<pack>/<tail>`
//         (where <tail> is the path under src/pages/ for page files, or the
//         basename for everything else) → copy/fetch it; record as "recovered".
//       * Otherwise → record as "missing" with a remediation hint.
//   - Exit 0 on happy path or recoverable misses.
//   - Exit 1 if any file is unrecoverably missing.

import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from "node:fs";
import { join, dirname, basename, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const SKILL_URL = "https://dev.wix.com/skills/wix-headless";

// Mode detection: prefer on-disk skill root if reachable, else use HTTP.
// When invoked as `node <(curl ...)`, import.meta.url is `file:///dev/fd/N`
// and the candidate root won't contain `references/verticals` — so we fall
// through to URL mode automatically.
let SKILL_ROOT_DISK = null;
try {
  const scriptDir = dirname(fileURLToPath(import.meta.url));
  const candidate = resolve(scriptDir, "..");
  if (existsSync(join(candidate, "references/verticals"))) {
    SKILL_ROOT_DISK = candidate;
  }
} catch {
  // fileURLToPath may fail on non-file URLs; fall through to URL mode.
}

// Read a skill-local file (relative path like "references/verticals/stores.md").
// Returns null when the file doesn't exist.
async function readSkillText(relPath) {
  if (SKILL_ROOT_DISK) {
    const p = join(SKILL_ROOT_DISK, relPath);
    if (!existsSync(p)) return null;
    return readFileSync(p, "utf8");
  }
  const url = `${SKILL_URL}/${relPath}`;
  const r = await fetch(url);
  if (r.status === 404) return null;
  if (!r.ok) throw new Error(`fetch ${url}: HTTP ${r.status}`);
  return await r.text();
}

// Copy a skill-local file at relPath into destPath (in the user's project).
// Returns true on success, false if the source doesn't exist.
async function copySkillFile(relPath, destPath) {
  if (SKILL_ROOT_DISK) {
    const src = join(SKILL_ROOT_DISK, relPath);
    if (!existsSync(src)) return false;
    mkdirSync(dirname(destPath), { recursive: true });
    copyFileSync(src, destPath);
    return true;
  }
  const text = await readSkillText(relPath);
  if (text === null) return false;
  mkdirSync(dirname(destPath), { recursive: true });
  writeFileSync(destPath, text);
  return true;
}

const [, , projectDir, phase, packsCsv] = process.argv;

if (!projectDir || !phase || !packsCsv) {
  console.error("usage: check-manifest.mjs <project-dir> <phase> <packs-csv>");
  process.exit(2);
}

if (phase !== "components" && phase !== "pages") {
  console.error(`check-manifest: invalid phase "${phase}" — must be "components" or "pages"`);
  process.exit(2);
}

const packs = packsCsv.split(",").map((p) => p.trim()).filter(Boolean);

// Map a `creates:` file path to its template path (relative to skill root).
// Heuristic: `src/pages/<X>` preserves <X> under references/astro/templates/<pack>/;
// everything else uses basename only.
function templateRelPath(packName, srcPath) {
  const pagesMatch = srcPath.match(/^src\/pages\/(.+)$/);
  const tail = pagesMatch ? pagesMatch[1] : basename(srcPath);
  return `references/astro/templates/${packName}/${tail}`;
}

// Parse `creates:` block from a vertical pack's markdown frontmatter.
// Format (one per line):
//   - { file: src/utils/back-in-stock.ts,          phase: components }
function parseCreates(text) {
  const lines = text.split("\n");
  const entries = [];
  let inCreates = false;
  for (const line of lines) {
    if (/^creates:\s*$/.test(line)) {
      inCreates = true;
      continue;
    }
    if (inCreates) {
      // Block ends at a non-indented, non-blank line that doesn't start with `-`.
      if (/^[^\s-]/.test(line)) {
        inCreates = false;
        continue;
      }
      const m = line.match(/^\s*-\s*\{\s*file:\s*([^,]+?),\s*phase:\s*([\w-]+)\s*\}/);
      if (m) entries.push({ file: m[1].trim(), phase: m[2].trim() });
    }
  }
  return entries;
}

const present = [];
const recovered = [];
const missing = [];

for (const pack of packs) {
  const verticalRel = `references/verticals/${pack}.md`;
  const text = await readSkillText(verticalRel);
  if (text === null) {
    missing.push({
      pack,
      path: null,
      code: "PACK_NOT_FOUND",
      remediation: `vertical pack file not found at ${verticalRel} — pack "${pack}" may not be a valid loaded vertical`,
    });
    continue;
  }

  const entries = parseCreates(text).filter((e) => e.phase === phase);

  for (const { file } of entries) {
    const destPath = join(projectDir, file);
    if (existsSync(destPath)) {
      present.push({ pack, path: file });
      continue;
    }

    const templateRel = templateRelPath(pack, file);
    const ok = await copySkillFile(templateRel, destPath);
    if (ok) {
      recovered.push({
        pack,
        path: file,
        source: "template-copy",
        template: templateRel,
      });
      continue;
    }

    missing.push({
      pack,
      path: file,
      code: "PHASE_FILE_MISSING",
      remediation: `the ${pack} agent did not write this file and the pack ships no template at ${templateRel}. Re-dispatch the ${phase} scope, or report the gap to the pack maintainer.`,
    });
  }
}

const summary = {
  phase,
  packs,
  counts: {
    present: present.length,
    recovered: recovered.length,
    missing: missing.length,
  },
  present,
  recovered,
  missing,
};
console.log(JSON.stringify(summary, null, 2));

process.exit(missing.length > 0 ? 1 : 0);