feat(core,cli): harden install engine and extend skill lint and interview eval
- update only touches the recorded project root; project installs do not follow symlinks or junctions; repeated --force rotates .orig backups; uninstall never deletes outside the recorded root - run npm/npx without a shell on Windows and pin optional tool versions - skip the update check in CI and non-interactive output - lint: cross-document section pointers, preflight row ids, gate rule numbers, per-document size budgets, gate copy equality - eval: scenarios S19-S21, route and read-cost rejudging, interpretation card judging, temp-dir previews and table-form cards no longer fail
This commit is contained in:
parent
ee3bef2ce5
commit
9bcb45cbd3
122 changed files with 5836 additions and 831 deletions
|
|
@ -3,6 +3,7 @@
|
|||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { isAllowedSkillFile } from "./skill-allowlist.mjs";
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const src = path.resolve(here, "../../skill");
|
||||
|
|
@ -14,15 +15,25 @@ const dest = path.resolve(here, "../dist/skill");
|
|||
// 배포물은 어느 OS 에서 만들었는지와 무관하게 같아야 한다.
|
||||
const TEXT = new Set([".md", ".txt", ".json", ".yml", ".yaml"]);
|
||||
|
||||
async function copyDir(from, to) {
|
||||
await fs.mkdir(to, { recursive: true });
|
||||
const excluded = [];
|
||||
|
||||
async function copyDir(from, to, rel = "") {
|
||||
for (const e of await fs.readdir(from, { withFileTypes: true })) {
|
||||
if (e.name === "node_modules" || e.name === "package.json") continue;
|
||||
if (e.name === "node_modules") continue;
|
||||
const r = rel ? `${rel}/${e.name}` : e.name;
|
||||
const s = path.join(from, e.name);
|
||||
const d = path.join(to, e.name);
|
||||
if (e.isDirectory()) {
|
||||
await copyDir(s, d);
|
||||
} else if (TEXT.has(path.extname(e.name))) {
|
||||
await copyDir(s, d, r);
|
||||
continue;
|
||||
}
|
||||
// 허용 목록 밖(테스트·스크래치·dotfile·package.json 등)은 사용자 설치물에 싣지 않는다
|
||||
if (!e.isFile() || !isAllowedSkillFile(r)) {
|
||||
if (e.name !== "package.json") excluded.push(r);
|
||||
continue;
|
||||
}
|
||||
await fs.mkdir(to, { recursive: true });
|
||||
if (TEXT.has(path.extname(e.name))) {
|
||||
const text = await fs.readFile(s, "utf8");
|
||||
await fs.writeFile(d, text.replace(/\r\n/g, "\n"), "utf8");
|
||||
} else {
|
||||
|
|
@ -40,3 +51,4 @@ await fs.writeFile(path.join(dest, ".designpaca_version"), `${pkg.version}\n`, "
|
|||
|
||||
const count = (await fs.readdir(dest, { recursive: true })).length;
|
||||
console.log(`스킬 번들 완료: ${count}개 항목 → dist/skill`);
|
||||
if (excluded.length > 0) console.log(` 설치물에서 제외: ${excluded.join(", ")}`);
|
||||
|
|
|
|||
21
packages/cli/scripts/skill-allowlist.mjs
Normal file
21
packages/cli/scripts/skill-allowlist.mjs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// 사용자 설치물(dist/skill → 각 하네스의 스킬 디렉터리)에 실을 파일의 허용 목록.
|
||||
// bundle-skill.mjs(복사 시)와 verify-package.mjs(배포 직전)가 같은 규칙을 쓴다.
|
||||
//
|
||||
// 이유: 예전에는 packages/skill 아래를 통째로 복사해서 추적되지 않은 스크래치·.env·로컬 메모가
|
||||
// 로컬 publish 때 npm 에 실릴 수 있었고, 개발용 계약 테스트(design-gate-contract.test.mjs)가
|
||||
// 모든 사용자 스킬 디렉터리에 설치됐다. 새 종류의 파일을 실으려면 여기서 명시적으로 연다.
|
||||
|
||||
/** posix 상대 경로(rel) 가 설치물에 들어가도 되는가 */
|
||||
export function isAllowedSkillFile(rel) {
|
||||
const parts = rel.split("/");
|
||||
if (parts.some((p) => p.startsWith(".") && p !== ".designpaca_version")) return false;
|
||||
if (/\.test\.[cm]?[jt]s$/.test(rel)) return false;
|
||||
if (parts.length === 1) {
|
||||
return ["SKILL.md", "CHANGELOG.md", "THIRD_PARTY_NOTICES.md", ".designpaca_version"].includes(rel);
|
||||
}
|
||||
const [top] = parts;
|
||||
if (top === "references") return rel.endsWith(".md");
|
||||
if (top === "agents") return /\.ya?ml$/.test(rel);
|
||||
if (top === "tools") return parts.length === 2 && rel.endsWith(".mjs");
|
||||
return false;
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { isAllowedSkillFile } from "./skill-allowlist.mjs";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const must = ["dist/index.js", "dist/skill/SKILL.md", "dist/skill/.designpaca_version"];
|
||||
|
|
@ -26,4 +27,17 @@ if (stamp !== pkg.version) {
|
|||
console.error(`버전 불일치: package.json=${pkg.version} / 스킬 스탬프=${stamp}`);
|
||||
process.exit(1);
|
||||
}
|
||||
// 허용 목록 밖 파일(테스트·스크래치·.env 등)이 설치물에 섞였으면 배포하지 않는다
|
||||
const skillDir = path.join(root, "dist/skill");
|
||||
const unexpected = [];
|
||||
for (const e of await fs.readdir(skillDir, { recursive: true, withFileTypes: true })) {
|
||||
if (!e.isFile() && !e.isSymbolicLink()) continue;
|
||||
const rel = path.relative(skillDir, path.join(e.parentPath ?? e.path, e.name)).split(path.sep).join("/");
|
||||
if (e.isSymbolicLink() || !isAllowedSkillFile(rel)) unexpected.push(rel);
|
||||
}
|
||||
if (unexpected.length) {
|
||||
console.error(`스킬 설치물에 허용 목록 밖 파일:\n${unexpected.map((m) => ` - ${m}`).join("\n")}`);
|
||||
console.error("scripts/skill-allowlist.mjs 를 확인하고 pnpm build 를 다시 실행해라.");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`배포 점검 통과 (v${pkg.version})`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue