@geminix/gxpm 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +148 -0
- package/CANON.md +53 -0
- package/CLAUDE.md +60 -0
- package/CONTEXT.md +49 -0
- package/DEBUG.md +59 -0
- package/ISSUE_CONTEXT.md +25 -0
- package/README.md +143 -0
- package/VERSION +1 -0
- package/agents/cleanup-auditor/cleanup-auditor.md +56 -0
- package/agents/grill-master.md +26 -0
- package/agents/implementer.md +32 -0
- package/agents/review-army/accessibility-reviewer.md +54 -0
- package/agents/review-army/code-quality-reviewer.md +54 -0
- package/agents/review-army/security-reviewer.md +56 -0
- package/agents/review-army/spec-compliance-reviewer.md +51 -0
- package/agents/review-army/test-reviewer.md +55 -0
- package/agents/reviewer.md +59 -0
- package/agents/ship-audit-army/docs-auditor.md +53 -0
- package/agents/ship-audit-army/performance-auditor.md +52 -0
- package/agents/ship-audit-army/security-auditor.md +52 -0
- package/agents/specifier.md +55 -0
- package/agents/triage-officer.md +27 -0
- package/bin/gxpm +17 -0
- package/bin/gxpm-browser +17 -0
- package/bin/gxpm-config +15 -0
- package/bin/gxpm-eval +13 -0
- package/bin/gxpm-global-discover +15 -0
- package/bin/gxpm-init +38 -0
- package/bin/gxpm-investigate +194 -0
- package/bin/gxpm-uninstall +15 -0
- package/bin/gxpm-update-check +165 -0
- package/commands/build.md +40 -0
- package/commands/help.md +53 -0
- package/commands/plan.md +34 -0
- package/commands/refine.md +46 -0
- package/commands/review.md +34 -0
- package/commands/ship.md +37 -0
- package/core/ac-check.ts +20 -0
- package/core/agent-runtime.ts +363 -0
- package/core/artifact-validator.ts +151 -0
- package/core/artifacts.ts +313 -0
- package/core/autopilot.ts +250 -0
- package/core/capabilities.ts +779 -0
- package/core/checkpoint.ts +370 -0
- package/core/cleanup.ts +32 -0
- package/core/command-probe.ts +82 -0
- package/core/config.ts +533 -0
- package/core/contracts/behavior-spec.schema.ts +38 -0
- package/core/contracts/converter.ts +61 -0
- package/core/contracts/host.ts +43 -0
- package/core/converters/converter.ts +93 -0
- package/core/converters/index.ts +8 -0
- package/core/converters/managed-artifact.ts +119 -0
- package/core/converters/parser.ts +159 -0
- package/core/converters/template-renderer.ts +35 -0
- package/core/converters/writer.ts +61 -0
- package/core/dag-executor.ts +426 -0
- package/core/dag-loader.ts +292 -0
- package/core/dag-schemas.ts +150 -0
- package/core/dispatch.ts +125 -0
- package/core/evidence.ts +148 -0
- package/core/gate.ts +269 -0
- package/core/hook-engine.ts +566 -0
- package/core/host-probe.ts +64 -0
- package/core/implement.ts +16 -0
- package/core/isolation-errors.ts +174 -0
- package/core/isolation-resolver.ts +921 -0
- package/core/issue-context.ts +381 -0
- package/core/issue-readiness.ts +457 -0
- package/core/issue-sync.ts +427 -0
- package/core/issues.ts +132 -0
- package/core/land.ts +108 -0
- package/core/orchestrator.ts +54 -0
- package/core/phase-artifact.ts +32 -0
- package/core/phase-gates.ts +130 -0
- package/core/phase-rewind.ts +94 -0
- package/core/plan-lint.ts +61 -0
- package/core/plan.ts +77 -0
- package/core/port-allocation.ts +50 -0
- package/core/pr-check.ts +15 -0
- package/core/preset-system/preset-resolver.ts +221 -0
- package/core/project-init-status.ts +127 -0
- package/core/qa.ts +15 -0
- package/core/resilience.ts +165 -0
- package/core/runs.ts +288 -0
- package/core/safe-path.test.ts +80 -0
- package/core/safe-path.ts +60 -0
- package/core/sdd-gate.test.ts +98 -0
- package/core/sdd-gate.ts +134 -0
- package/core/self-review.ts +62 -0
- package/core/session.ts +70 -0
- package/core/ship.ts +86 -0
- package/core/specify.ts +173 -0
- package/core/state.ts +1002 -0
- package/core/template-engine.ts +152 -0
- package/core/template-resolver.test.ts +70 -0
- package/core/template-resolver.ts +156 -0
- package/core/triage.ts +26 -0
- package/core/verify.ts +15 -0
- package/core/wiki-native.ts +2423 -0
- package/core/wiki.ts +27 -0
- package/core/workflow-event-emitter.ts +163 -0
- package/core/workflows/engine.ts +273 -0
- package/core/workflows/expressions.ts +76 -0
- package/core/workflows/index.ts +38 -0
- package/core/workflows/steps/command.ts +43 -0
- package/core/workflows/steps/gate.ts +47 -0
- package/core/workflows/steps/gxpm.ts +44 -0
- package/core/workflows/steps/linear.ts +31 -0
- package/core/workflows/steps/shell.ts +65 -0
- package/core/workflows/types.ts +62 -0
- package/core/workspace-runtime.ts +227 -0
- package/core/worktree-init-steps.ts +647 -0
- package/core/worktree-init.ts +330 -0
- package/core/worktree-owner.ts +143 -0
- package/docs/GXPM_VERIFY.md +98 -0
- package/docs/INSTALL_FOR_AGENTS.md +113 -0
- package/docs/README.md +57 -0
- package/docs/adr/adr-005-multi-platform-skill-converter.md +72 -0
- package/docs/agents/domain.md +30 -0
- package/docs/agents/issue-tracker.md +30 -0
- package/docs/agents/triage-labels.md +32 -0
- package/docs/architecture/gxpm-architecture-diagram.md +265 -0
- package/docs/architecture/gxpm-current-architecture.md +175 -0
- package/docs/architecture/gxpm-current-flow.md +278 -0
- package/docs/architecture/gxpm-replacement-architecture.md +211 -0
- package/docs/architecture/gxpm-target-architecture.md +449 -0
- package/docs/architecture/gxpm-v0-contract.md +311 -0
- package/docs/architecture/layered-workflow-boundaries.md +193 -0
- package/docs/architecture/preset-system.md +126 -0
- package/docs/architecture/scaffold-northstar.md +23 -0
- package/docs/brainstorms/2026-05-14-bdd-then-tdd-design.md +320 -0
- package/docs/brainstorms/README.md +22 -0
- package/docs/brainstorms/docs-knowledge-system-requirements.md +29 -0
- package/docs/governance/beta-skill-promotion.md +39 -0
- package/docs/governance/development-contract.md +144 -0
- package/docs/governance/gherkin-style.md +90 -0
- package/docs/governance/host-adapter.md +56 -0
- package/docs/governance/skill-authoring.md +87 -0
- package/docs/governance/skill-testing.md +356 -0
- package/docs/governance/template-authoring.md +53 -0
- package/docs/migrations/v0.2.md +51 -0
- package/docs/plans/README.md +23 -0
- package/docs/plans/bdd-then-tdd-plan.md +1767 -0
- package/docs/plans/docs-knowledge-system-plan.md +31 -0
- package/docs/plans/spec-kit-sdd-adoption-plan.md +305 -0
- package/docs/research/agents-md-best-practices.md +207 -0
- package/docs/research/archon-study.md +351 -0
- package/docs/research/claude-hooks-study.md +440 -0
- package/docs/research/codex-hooks-study.md +624 -0
- package/docs/research/everything-claude-code-study.md +252 -0
- package/docs/research/from-skills-to-layered-workflow.md +322 -0
- package/docs/research/gsd-study.md +69 -0
- package/docs/research/kimi-hooks-study.md +274 -0
- package/docs/research/mattpocock-skills-comparison.md +429 -0
- package/docs/research/mattpocock-skills-study.md +275 -0
- package/docs/research/oh-my-codex-study.md +279 -0
- package/docs/research/perplexity-agent-skills-design.md +168 -0
- package/docs/research/pmc-gstack-skill-study.md +122 -0
- package/docs/research/spec-kit-study.md +224 -0
- package/docs/research/superpowers-study.md +209 -0
- package/docs/roadmap/initial-roadmap.md +53 -0
- package/docs/solutions/README.md +45 -0
- package/docs/solutions/artifact-nesting-recovery.md +58 -0
- package/docs/solutions/session-context-restore-practice.md +67 -0
- package/docs/solutions/workflow/version-drift-recovery.md +49 -0
- package/docs/solutions/worktree-gate-recovery.md +62 -0
- package/docs/specs/README.md +28 -0
- package/docs/specs/claude.md +45 -0
- package/docs/specs/codex.md +44 -0
- package/docs/specs/cursor.md +44 -0
- package/hosts/adapters/claude.ts +29 -0
- package/hosts/adapters/codex.ts +27 -0
- package/hosts/adapters/cursor.ts +27 -0
- package/hosts/adapters/kimi.ts +27 -0
- package/hosts/claude.ts +23 -0
- package/hosts/codex.ts +26 -0
- package/hosts/cursor.ts +19 -0
- package/hosts/index.ts +33 -0
- package/hosts/registry.test.ts +52 -0
- package/hosts/registry.ts +57 -0
- package/hosts/schema.ts +58 -0
- package/package.json +52 -0
- package/scripts/browser.ts +185 -0
- package/scripts/cleanup.ts +142 -0
- package/scripts/commands/artifact.ts +115 -0
- package/scripts/commands/autopilot.ts +143 -0
- package/scripts/commands/capability.ts +57 -0
- package/scripts/commands/config.ts +69 -0
- package/scripts/commands/dag.ts +126 -0
- package/scripts/commands/feedback.ts +123 -0
- package/scripts/commands/gate.ts +291 -0
- package/scripts/commands/helpers.ts +126 -0
- package/scripts/commands/hook.ts +66 -0
- package/scripts/commands/init.ts +515 -0
- package/scripts/commands/issue.ts +825 -0
- package/scripts/commands/phase.ts +61 -0
- package/scripts/commands/preset.ts +159 -0
- package/scripts/commands/runtime.ts +199 -0
- package/scripts/commands/specify.ts +71 -0
- package/scripts/commands/upgrade.ts +243 -0
- package/scripts/commands/verify.ts +183 -0
- package/scripts/commands/wiki.ts +242 -0
- package/scripts/commands/workflow.ts +131 -0
- package/scripts/dev-skill.ts +55 -0
- package/scripts/discover-skills.ts +116 -0
- package/scripts/doctor.ts +410 -0
- package/scripts/dogfood-check.ts +125 -0
- package/scripts/eval-functional.ts +218 -0
- package/scripts/eval.ts +246 -0
- package/scripts/gen-skill-docs.ts +201 -0
- package/scripts/global-discover.ts +217 -0
- package/scripts/governance-check.ts +75 -0
- package/scripts/gxpm-check.ts +12 -0
- package/scripts/gxpm.ts +216 -0
- package/scripts/host-config.ts +62 -0
- package/scripts/install-claude-hooks.ts +138 -0
- package/scripts/install-codex-hooks.ts +271 -0
- package/scripts/install-hooks.ts +128 -0
- package/scripts/install-kimi-hooks.ts +92 -0
- package/scripts/install-skill.ts +184 -0
- package/scripts/phase-artifact-commands.ts +100 -0
- package/scripts/post-land-sync.ts +46 -0
- package/scripts/scaffold-check.ts +85 -0
- package/scripts/skill-naming-check.ts +78 -0
- package/scripts/skill-structure-check.ts +157 -0
- package/scripts/skills-lock-check.ts +60 -0
- package/scripts/sync-markdown-artifacts.ts +172 -0
- package/scripts/uninstall.ts +162 -0
- package/scripts/version.ts +47 -0
- package/scripts/wait-pr-ready.ts +407 -0
- package/skills/gxpm/SKILL.md +485 -0
- package/skills/gxpm/SKILL.md.tmpl +422 -0
- package/skills/gxpm/references/CANON.md +53 -0
- package/skills/gxpm/references/key-rules.md +130 -0
- package/skills/gxpm-architecture/SKILL.md +106 -0
- package/skills/gxpm-architecture/references/DEEPENING.md +37 -0
- package/skills/gxpm-architecture/references/INTERFACE-DESIGN.md +44 -0
- package/skills/gxpm-autopilot/SKILL.md +116 -0
- package/skills/gxpm-autopilot/SKILL.md.tmpl +107 -0
- package/skills/gxpm-browser/SKILL.md +105 -0
- package/skills/gxpm-browser/SKILL.md.tmpl +41 -0
- package/skills/gxpm-browser/references/commands.md +43 -0
- package/skills/gxpm-browser/references/evidence-path.md +20 -0
- package/skills/gxpm-build/SKILL.md +78 -0
- package/skills/gxpm-cleanup/SKILL.md +76 -0
- package/skills/gxpm-debug-issue/SKILL.md +39 -0
- package/skills/gxpm-diagnose/SKILL.md +220 -0
- package/skills/gxpm-diagnose/SKILL.md.tmpl +31 -0
- package/skills/gxpm-diagnose/references/feedback-loop.md +34 -0
- package/skills/gxpm-diagnose/references/feedback-loops.md +43 -0
- package/skills/gxpm-diagnose/references/phases.md +60 -0
- package/skills/gxpm-eval/SKILL.md +78 -0
- package/skills/gxpm-explore-codebase/SKILL.md +36 -0
- package/skills/gxpm-explore-codebase/scripts/summarize-communities.ts +51 -0
- package/skills/gxpm-feedback/SKILL.md +122 -0
- package/skills/gxpm-grill/SKILL.md +159 -0
- package/skills/gxpm-grill/SKILL.md.tmpl +77 -0
- package/skills/gxpm-grill/references/documentation-templates.md +56 -0
- package/skills/gxpm-grill/references/process.md +25 -0
- package/skills/gxpm-handoff/SKILL.md +112 -0
- package/skills/gxpm-hygiene/SKILL.md +69 -0
- package/skills/gxpm-implementer/SKILL.md +142 -0
- package/skills/gxpm-implementer/SKILL.md.tmpl +141 -0
- package/skills/gxpm-linear/SKILL.md +282 -0
- package/skills/gxpm-linear/SKILL.md.tmpl +86 -0
- package/skills/gxpm-linear/references/commands.md +75 -0
- package/skills/gxpm-linear/references/workflows.md +120 -0
- package/skills/gxpm-planning/SKILL.md +134 -0
- package/skills/gxpm-prototype/SKILL.md +64 -0
- package/skills/gxpm-refactor-safely/SKILL.md +62 -0
- package/skills/gxpm-review-army/SKILL.md +117 -0
- package/skills/gxpm-review-changes/SKILL.md +36 -0
- package/skills/gxpm-setup/SKILL.md +101 -0
- package/skills/gxpm-specifier/SKILL.md +135 -0
- package/skills/gxpm-tdd/SKILL.md +187 -0
- package/skills/gxpm-tdd/references/interface-design.md +23 -0
- package/skills/gxpm-tdd/references/mocking.md +27 -0
- package/skills/gxpm-tdd/references/red-green-refactor.md +61 -0
- package/skills/gxpm-tdd/references/troubleshooting.md +28 -0
- package/skills/gxpm-tdd/references/workflow.md +50 -0
- package/skills/gxpm-tdd/testing-anti-patterns.tmpl +304 -0
- package/skills/gxpm-triage/SKILL.md +160 -0
- package/skills/gxpm-verify/SKILL.md +107 -0
- package/skills/gxpm-write-skill/SKILL.md +131 -0
- package/skills/gxpm-zoom-out/SKILL.md +69 -0
- package/skills/maintain-hygiene-skills-lock/SKILL.md +54 -0
- package/skills/maintain-hygiene-skills-lock/SKILL.md.tmpl +53 -0
- package/templates/constitution-template.md +63 -0
- package/templates/hooks/gxpm-commit-msg +16 -0
- package/templates/hooks/gxpm-post-checkout +19 -0
- package/templates/hooks/gxpm-post-commit +7 -0
- package/templates/hooks/gxpm-post-merge +29 -0
- package/templates/hooks/gxpm-pre-commit +39 -0
- package/templates/hooks/gxpm-pre-push +33 -0
- package/templates/plan-template.md.tmpl +46 -0
- package/templates/spec-template.md.tmpl +63 -0
- package/templates/specify-stub.tmpl +22 -0
- package/templates/tasks-template.md.tmpl +32 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_GXPM_ROOT = resolve(import.meta.dir, "..");
|
|
5
|
+
|
|
6
|
+
export interface SkillNamingCheckOptions {
|
|
7
|
+
root?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function validateSkillNaming(options: SkillNamingCheckOptions = {}): string[] {
|
|
11
|
+
const root = options.root ?? DEFAULT_GXPM_ROOT;
|
|
12
|
+
const skillsDir = join(root, "skills");
|
|
13
|
+
const errors: string[] = [];
|
|
14
|
+
|
|
15
|
+
if (!existsSync(skillsDir) || !statSync(skillsDir).isDirectory()) {
|
|
16
|
+
return [`skills directory not found: ${skillsDir}`];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const entries = readdirSync(skillsDir);
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const entryPath = join(skillsDir, entry);
|
|
22
|
+
if (!statSync(entryPath).isDirectory()) continue;
|
|
23
|
+
if (entry.startsWith(".")) continue;
|
|
24
|
+
|
|
25
|
+
const isGxpmSkill = entry === "gxpm" || entry.startsWith("gxpm-");
|
|
26
|
+
|
|
27
|
+
if (!isGxpmSkill) {
|
|
28
|
+
console.warn(`[skill-naming] warning: skill "${entry}" does not use gxpm- prefix (recommended for gxpm official skills)`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const skillMdPath = join(entryPath, "SKILL.md");
|
|
33
|
+
const skillTmplPath = join(entryPath, "SKILL.md.tmpl");
|
|
34
|
+
|
|
35
|
+
let sourcePath: string | null = null;
|
|
36
|
+
if (existsSync(skillTmplPath)) {
|
|
37
|
+
sourcePath = skillTmplPath;
|
|
38
|
+
} else if (existsSync(skillMdPath)) {
|
|
39
|
+
sourcePath = skillMdPath;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!sourcePath) {
|
|
43
|
+
errors.push(`skill "${entry}" missing SKILL.md or SKILL.md.tmpl`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const content = readFileSync(sourcePath, "utf8");
|
|
48
|
+
if (!content.startsWith("---")) {
|
|
49
|
+
errors.push(`skill "${entry}" frontmatter missing`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
54
|
+
if (!frontmatterMatch) {
|
|
55
|
+
errors.push(`skill "${entry}" frontmatter malformed`);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const frontmatter = frontmatterMatch[1];
|
|
60
|
+
if (!frontmatter.includes("name:")) {
|
|
61
|
+
errors.push(`skill "${entry}" frontmatter missing name`);
|
|
62
|
+
}
|
|
63
|
+
if (!frontmatter.includes("description:")) {
|
|
64
|
+
errors.push(`skill "${entry}" frontmatter missing description`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return errors;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (import.meta.main) {
|
|
72
|
+
const errors = validateSkillNaming();
|
|
73
|
+
if (errors.length > 0) {
|
|
74
|
+
console.error(errors.map((error) => `skill-naming: ${error}`).join("\n"));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
console.log("skill naming check passed");
|
|
78
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join, relative, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface SkillStructureViolation {
|
|
5
|
+
skillPath: string;
|
|
6
|
+
missing: string[];
|
|
7
|
+
warnings: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const REQUIRED_SECTIONS = [
|
|
11
|
+
{
|
|
12
|
+
id: "entry",
|
|
13
|
+
names: ["入口条件", "入口", "触发条件", "Entry Conditions", "When to Use", "Recognition criteria"],
|
|
14
|
+
label: "入口条件 (Entry Conditions)",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: "process",
|
|
18
|
+
names: ["可操作流程", "流程", "操作步骤", "Process", "Steps", "Workflow"],
|
|
19
|
+
label: "可操作流程 (Process)",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "redflags",
|
|
23
|
+
names: ["红旗清单", "红旗", "反模式", "常见陷阱", "Red Flags", "Anti-Patterns", "Pitfalls"],
|
|
24
|
+
label: "红旗/反模式 (Red Flags)",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "verification",
|
|
28
|
+
names: ["验证清单", "出口条件", "验证", "Checklist", "Verification", "Exit Conditions"],
|
|
29
|
+
label: "验证清单/出口条件 (Verification)",
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const OPTIONAL_SECTIONS = [
|
|
34
|
+
{
|
|
35
|
+
id: "phrases",
|
|
36
|
+
names: ["常见说辞表", "说辞", "Phrases", "Common Phrases"],
|
|
37
|
+
label: "常见说辞表 (Common Phrases)",
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function hasSection(content: string, names: string[]): boolean {
|
|
42
|
+
const pattern = new RegExp(
|
|
43
|
+
`^#{2,3}\\s*(?:\\[[ x]\\]\\s*)?(${names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`,
|
|
44
|
+
"im",
|
|
45
|
+
);
|
|
46
|
+
return pattern.test(content);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function walkDir(dir: string, callback: (path: string) => void) {
|
|
50
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
51
|
+
const fullPath = join(dir, entry.name);
|
|
52
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
53
|
+
walkDir(fullPath, callback);
|
|
54
|
+
} else if (entry.isFile()) {
|
|
55
|
+
callback(fullPath);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function validateSkillStructure(root: string): SkillStructureViolation[] {
|
|
61
|
+
const violations: SkillStructureViolation[] = [];
|
|
62
|
+
const skillsDir = join(root, "skills");
|
|
63
|
+
|
|
64
|
+
if (!existsSync(skillsDir)) {
|
|
65
|
+
return [{ skillPath: "skills/", missing: ["skills directory not found"], warnings: [] }];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
walkDir(skillsDir, (fullPath: string) => {
|
|
69
|
+
if (!fullPath.endsWith("SKILL.md")) return;
|
|
70
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
71
|
+
const relPath = relative(root, fullPath);
|
|
72
|
+
const missing: string[] = [];
|
|
73
|
+
const warnings: string[] = [];
|
|
74
|
+
|
|
75
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
76
|
+
if (!hasSection(content, section.names)) {
|
|
77
|
+
missing.push(section.label);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const section of OPTIONAL_SECTIONS) {
|
|
82
|
+
if (!hasSection(content, section.names)) {
|
|
83
|
+
warnings.push(`可选: ${section.label}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Gatekeeping skills (workflow/verify/ship) should have phrases
|
|
88
|
+
if (/\b(triage|plan|verify|review|ship|debug|diagnose|hygiene)\b/.test(relPath)) {
|
|
89
|
+
if (!hasSection(content, OPTIONAL_SECTIONS[0].names)) {
|
|
90
|
+
warnings.push(`建议补充: ${OPTIONAL_SECTIONS[0].label} (此 skill 涉及 gatekeeping 或交互)`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (missing.length > 0 || warnings.length > 0) {
|
|
95
|
+
violations.push({ skillPath: relPath, missing, warnings });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return violations;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function formatSkillStructureErrors(violations: SkillStructureViolation[]): { errors: string[]; warnings: string[] } {
|
|
103
|
+
if (violations.length === 0) return { errors: [], warnings: [] };
|
|
104
|
+
|
|
105
|
+
const errors: string[] = [];
|
|
106
|
+
const warnings: string[] = [];
|
|
107
|
+
const missingOnly = violations.filter((v) => v.missing.length > 0);
|
|
108
|
+
const warningOnly = violations.filter((v) => v.missing.length === 0 && v.warnings.length > 0);
|
|
109
|
+
|
|
110
|
+
if (missingOnly.length > 0) {
|
|
111
|
+
errors.push(`SKILL.md structure violations found: ${missingOnly.length} skill(s) missing required sections`);
|
|
112
|
+
for (const v of missingOnly) {
|
|
113
|
+
errors.push(`\n ${v.skillPath}:`);
|
|
114
|
+
for (const m of v.missing) {
|
|
115
|
+
errors.push(` ❌ MISSING: ${m}`);
|
|
116
|
+
}
|
|
117
|
+
for (const w of v.warnings) {
|
|
118
|
+
errors.push(` ⚠️ ${w}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (warningOnly.length > 0) {
|
|
124
|
+
warnings.push(`SKILL.md structure warnings: ${warningOnly.length} skill(s) with optional section gaps`);
|
|
125
|
+
for (const v of warningOnly) {
|
|
126
|
+
warnings.push(`\n ${v.skillPath}:`);
|
|
127
|
+
for (const w of v.warnings) {
|
|
128
|
+
warnings.push(` ⚠️ ${w}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const guide = [
|
|
134
|
+
"\nEach SKILL.md must contain these sections (## or ### heading):",
|
|
135
|
+
" 1. 入口条件 / Entry Conditions / Recognition criteria / When to Use",
|
|
136
|
+
" 2. 可操作流程 / Process / Steps / Workflow",
|
|
137
|
+
" 3. 红旗清单 / 反模式 / Red Flags / Anti-Patterns / Pitfalls",
|
|
138
|
+
" 4. 验证清单 / 出口条件 / Verification / Checklist",
|
|
139
|
+
"Optional but recommended for gatekeeping skills:",
|
|
140
|
+
" 5. 常见说辞表 / Common Phrases",
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
errors: errors.length > 0 ? [...errors, ...guide] : [],
|
|
145
|
+
warnings: warnings.length > 0 ? [...warnings, ...guide] : [],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (import.meta.main) {
|
|
150
|
+
const root = process.argv[2] ?? resolve(import.meta.dir, "..");
|
|
151
|
+
const violations = validateSkillStructure(root);
|
|
152
|
+
if (violations.length > 0) {
|
|
153
|
+
console.error(formatSkillStructureErrors(violations).join("\n"));
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
console.log("All SKILL.md files conform to the five-section structure.");
|
|
157
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
interface SkillsLock {
|
|
6
|
+
version: number;
|
|
7
|
+
skills: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function validateSkillsLock(options: { root?: string } = {}): string[] {
|
|
11
|
+
const root = options.root ?? resolve(import.meta.dir, "..");
|
|
12
|
+
const lockPath = resolve(root, "skills-lock.json");
|
|
13
|
+
const skillsDir = resolve(root, "skills");
|
|
14
|
+
|
|
15
|
+
if (!existsSync(lockPath)) {
|
|
16
|
+
return ["skills-lock.json not found"];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let lock: SkillsLock;
|
|
20
|
+
try {
|
|
21
|
+
lock = JSON.parse(readFileSync(lockPath, "utf8")) as SkillsLock;
|
|
22
|
+
} catch {
|
|
23
|
+
return ["skills-lock.json is not valid JSON"];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (lock.version !== 1) {
|
|
27
|
+
return [`skills-lock.json version ${lock.version} is not supported`];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const errors: string[] = [];
|
|
31
|
+
const expectedSkills = new Set(Object.keys(lock.skills));
|
|
32
|
+
|
|
33
|
+
for (const [skillName, expectedHash] of Object.entries(lock.skills)) {
|
|
34
|
+
const skillPath = resolve(skillsDir, skillName, "SKILL.md");
|
|
35
|
+
if (!existsSync(skillPath)) {
|
|
36
|
+
errors.push(`skill ${skillName}: SKILL.md missing`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const content = readFileSync(skillPath);
|
|
40
|
+
const actualHash = createHash("sha256").update(content).digest("hex");
|
|
41
|
+
if (actualHash !== expectedHash) {
|
|
42
|
+
errors.push(`skill ${skillName}: hash mismatch (expected ${expectedHash}, got ${actualHash})`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check for unlisted skills
|
|
47
|
+
if (existsSync(skillsDir)) {
|
|
48
|
+
const entries = readdirSync(skillsDir, { withFileTypes: true }) as unknown as { name: string; isDirectory: () => boolean }[];
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (entry.isDirectory() && !expectedSkills.has(entry.name)) {
|
|
51
|
+
const skillPath = resolve(skillsDir, entry.name, "SKILL.md");
|
|
52
|
+
if (existsSync(skillPath)) {
|
|
53
|
+
errors.push(`skill ${entry.name}: not listed in skills-lock.json`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return errors;
|
|
60
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
interface StoredArtifact {
|
|
5
|
+
schemaVersion: 1;
|
|
6
|
+
issueId: string;
|
|
7
|
+
type: string;
|
|
8
|
+
writtenAt: string;
|
|
9
|
+
payload: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ARTIFACT_TO_MARKDOWN: Record<string, string> = {
|
|
13
|
+
"acceptance-contract": "01-spec.md",
|
|
14
|
+
"implementation-plan": "03-plan.md",
|
|
15
|
+
"self-review": "04-review.md",
|
|
16
|
+
"ship-readiness": "05-ship.md",
|
|
17
|
+
"pr-check": "05-ship.md",
|
|
18
|
+
"verify-findings": "05-ship.md",
|
|
19
|
+
"qa-findings": "05-ship.md",
|
|
20
|
+
"land-findings": "05-ship.md",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function artifactToMarkdown(artifact: StoredArtifact): string {
|
|
24
|
+
const { type, writtenAt, payload } = artifact;
|
|
25
|
+
const p = payload as Record<string, unknown>;
|
|
26
|
+
|
|
27
|
+
const lines: string[] = [];
|
|
28
|
+
lines.push(`# ${type}`);
|
|
29
|
+
lines.push("");
|
|
30
|
+
lines.push(`> Artifact type: \`${type}\``);
|
|
31
|
+
lines.push(`> Written at: ${writtenAt}`);
|
|
32
|
+
lines.push("");
|
|
33
|
+
|
|
34
|
+
if (type === "acceptance-contract") {
|
|
35
|
+
lines.push(`## Goal`);
|
|
36
|
+
lines.push(String(p.goal ?? "N/A"));
|
|
37
|
+
lines.push("");
|
|
38
|
+
lines.push(`## Scope`);
|
|
39
|
+
lines.push(String(p.scope ?? "N/A"));
|
|
40
|
+
lines.push("");
|
|
41
|
+
lines.push(`## Non-Goals`);
|
|
42
|
+
lines.push(String(p.nonGoals ?? "N/A"));
|
|
43
|
+
lines.push("");
|
|
44
|
+
lines.push(`## Success Criteria`);
|
|
45
|
+
lines.push(String(p.successCriteria ?? "N/A"));
|
|
46
|
+
lines.push("");
|
|
47
|
+
lines.push(`## Risks`);
|
|
48
|
+
lines.push(String(p.risks ?? "N/A"));
|
|
49
|
+
} else if (type === "implementation-plan") {
|
|
50
|
+
lines.push(`## Objective`);
|
|
51
|
+
lines.push(String(p.objective ?? "N/A"));
|
|
52
|
+
lines.push("");
|
|
53
|
+
lines.push(`## Scope`);
|
|
54
|
+
lines.push(String(p.scope ?? "N/A"));
|
|
55
|
+
lines.push("");
|
|
56
|
+
lines.push(`## Constitution Check`);
|
|
57
|
+
const cc = p.constitutionCheck as Record<string, unknown> | undefined;
|
|
58
|
+
if (cc) {
|
|
59
|
+
for (const [k, v] of Object.entries(cc)) {
|
|
60
|
+
lines.push(`- ${k}: ${v}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
lines.push("");
|
|
64
|
+
lines.push(`## Steps`);
|
|
65
|
+
const steps = p.steps as Array<Record<string, unknown>> | undefined;
|
|
66
|
+
if (steps) {
|
|
67
|
+
for (const step of steps) {
|
|
68
|
+
lines.push(`### ${step.id ?? "?"}: ${step.title ?? "?"}`);
|
|
69
|
+
lines.push(String(step.description ?? ""));
|
|
70
|
+
lines.push("");
|
|
71
|
+
lines.push(`**Acceptance Criteria:** ${step.acceptanceCriteria ?? "N/A"}`);
|
|
72
|
+
lines.push("");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} else if (type === "self-review") {
|
|
76
|
+
lines.push(`## Blocking`);
|
|
77
|
+
const blocking = p.blocking as string[] | undefined;
|
|
78
|
+
lines.push(blocking && blocking.length > 0 ? blocking.map((b) => `- ${b}`).join("\n") : "None");
|
|
79
|
+
lines.push("");
|
|
80
|
+
lines.push(`## Important`);
|
|
81
|
+
const important = p.important as string[] | undefined;
|
|
82
|
+
lines.push(important && important.length > 0 ? important.map((b) => `- ${b}`).join("\n") : "None");
|
|
83
|
+
lines.push("");
|
|
84
|
+
lines.push(`## Suggestions`);
|
|
85
|
+
const suggestions = p.suggestions as string[] | undefined;
|
|
86
|
+
lines.push(suggestions && suggestions.length > 0 ? suggestions.map((b) => `- ${b}`).join("\n") : "None");
|
|
87
|
+
lines.push("");
|
|
88
|
+
lines.push(`## Conclusion`);
|
|
89
|
+
lines.push(String(p.conclusion ?? "N/A"));
|
|
90
|
+
} else if (type === "ship-readiness") {
|
|
91
|
+
lines.push(`## Ready`);
|
|
92
|
+
lines.push(String(p.ready ?? "N/A"));
|
|
93
|
+
lines.push("");
|
|
94
|
+
lines.push(`## Ship Type`);
|
|
95
|
+
lines.push(String(p.shipType ?? "N/A"));
|
|
96
|
+
lines.push("");
|
|
97
|
+
lines.push(`## Changes`);
|
|
98
|
+
lines.push(String(p.changes ?? "N/A"));
|
|
99
|
+
lines.push("");
|
|
100
|
+
lines.push(`## Risk`);
|
|
101
|
+
lines.push(String(p.risk ?? "N/A"));
|
|
102
|
+
lines.push("");
|
|
103
|
+
lines.push(`## Rollback`);
|
|
104
|
+
lines.push(String(p.rollback ?? "N/A"));
|
|
105
|
+
} else {
|
|
106
|
+
lines.push("## Payload");
|
|
107
|
+
lines.push("```json");
|
|
108
|
+
lines.push(JSON.stringify(payload, null, 2));
|
|
109
|
+
lines.push("```");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return lines.join("\n") + "\n";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function main() {
|
|
116
|
+
const args = process.argv.slice(2);
|
|
117
|
+
const issueId = args[0];
|
|
118
|
+
if (!issueId) {
|
|
119
|
+
console.error("Usage: bun run scripts/sync-markdown-artifacts.ts <issue-id>");
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const root = process.cwd();
|
|
124
|
+
const artifactsDir = join(root, ".gxpm", "issues", issueId, "artifacts");
|
|
125
|
+
const docsDir = join(root, ".gxpm", "issues", issueId, "docs");
|
|
126
|
+
|
|
127
|
+
if (!existsSync(artifactsDir)) {
|
|
128
|
+
console.error(`Artifacts directory not found: ${artifactsDir}`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
mkdirSync(docsDir, { recursive: true });
|
|
133
|
+
|
|
134
|
+
const artifacts = readdirSync(artifactsDir)
|
|
135
|
+
.filter((f) => f.endsWith(".json"))
|
|
136
|
+
.map((f) => {
|
|
137
|
+
const content = readFileSync(join(artifactsDir, f), "utf8");
|
|
138
|
+
return JSON.parse(content) as StoredArtifact;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const written: string[] = [];
|
|
142
|
+
for (const artifact of artifacts) {
|
|
143
|
+
const mdFile = ARTIFACT_TO_MARKDOWN[artifact.type];
|
|
144
|
+
if (!mdFile) continue;
|
|
145
|
+
|
|
146
|
+
const mdPath = join(docsDir, mdFile);
|
|
147
|
+
const mdContent = artifactToMarkdown(artifact);
|
|
148
|
+
|
|
149
|
+
// Append with separator if file already exists for multi-artifact types like 05-ship.md
|
|
150
|
+
if (existsSync(mdPath)) {
|
|
151
|
+
const existing = readFileSync(mdPath, "utf8");
|
|
152
|
+
writeFileSync(mdPath, existing + "\n---\n\n" + mdContent);
|
|
153
|
+
} else {
|
|
154
|
+
writeFileSync(mdPath, mdContent);
|
|
155
|
+
}
|
|
156
|
+
written.push(`${artifact.type} → docs/${mdFile}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (written.length === 0) {
|
|
160
|
+
console.log("No markdown-mappable artifacts found.");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.log(`Synced ${written.length} artifacts to markdown for ${issueId}:`);
|
|
165
|
+
for (const line of written) {
|
|
166
|
+
console.log(` ${line}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
main();
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
interface UninstallOptions {
|
|
6
|
+
target?: string;
|
|
7
|
+
home?: string;
|
|
8
|
+
dryRun?: boolean;
|
|
9
|
+
purge?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface PlannedAction {
|
|
13
|
+
kind: "remove" | "update";
|
|
14
|
+
path: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const GIT_HOOKS = [
|
|
18
|
+
"pre-commit",
|
|
19
|
+
"commit-msg",
|
|
20
|
+
"pre-push",
|
|
21
|
+
"post-merge",
|
|
22
|
+
"gxpm-pre-commit",
|
|
23
|
+
"gxpm-commit-msg",
|
|
24
|
+
"gxpm-pre-push",
|
|
25
|
+
"gxpm-post-merge",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const CODEX_HOOK_SCRIPTS = [
|
|
29
|
+
"gxpm-session-start.sh",
|
|
30
|
+
"gxpm-user-prompt-submit.sh",
|
|
31
|
+
"gxpm-pre-tool-use.sh",
|
|
32
|
+
"gxpm-stop.sh",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export function planUninstall(options: UninstallOptions = {}): PlannedAction[] {
|
|
36
|
+
const home = options.home ?? homedir();
|
|
37
|
+
const target = resolve(options.target ?? process.cwd());
|
|
38
|
+
const actions: PlannedAction[] = [
|
|
39
|
+
{ kind: "remove", path: join(home, ".codex", "skills", "gxpm") },
|
|
40
|
+
{ kind: "remove", path: join(home, ".claude", "skills", "gxpm") },
|
|
41
|
+
...GIT_HOOKS.map((hook) => ({ kind: "remove" as const, path: join(target, ".githooks", hook) })),
|
|
42
|
+
...CODEX_HOOK_SCRIPTS.map((script) => ({
|
|
43
|
+
kind: "remove" as const,
|
|
44
|
+
path: join(target, ".codex", "hooks", script),
|
|
45
|
+
})),
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const hooksJson = join(target, ".codex", "hooks.json");
|
|
49
|
+
if (existsSync(hooksJson)) {
|
|
50
|
+
actions.push({ kind: "update", path: hooksJson });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (options.purge) {
|
|
54
|
+
actions.push({ kind: "remove", path: join(home, ".gxpm") });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return actions;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function runUninstall(options: UninstallOptions = {}): PlannedAction[] {
|
|
61
|
+
const actions = planUninstall(options);
|
|
62
|
+
if (options.dryRun) {
|
|
63
|
+
return actions;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const action of actions) {
|
|
67
|
+
if (action.kind === "remove") {
|
|
68
|
+
removePath(action.path);
|
|
69
|
+
} else {
|
|
70
|
+
removeCodexHookEntries(action.path);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return actions;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function removePath(path: string) {
|
|
78
|
+
if (!existsSync(path)) return;
|
|
79
|
+
rmSync(path, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function removeCodexHookEntries(path: string) {
|
|
83
|
+
if (!existsSync(path)) return;
|
|
84
|
+
let parsed: any;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
87
|
+
} catch {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!parsed || typeof parsed !== "object" || !parsed.hooks || typeof parsed.hooks !== "object") {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const eventName of Object.keys(parsed.hooks)) {
|
|
96
|
+
const entries = Array.isArray(parsed.hooks[eventName]) ? parsed.hooks[eventName] : [];
|
|
97
|
+
const nextEntries = entries
|
|
98
|
+
.map((entry: any) => {
|
|
99
|
+
const hooks = Array.isArray(entry?.hooks) ? entry.hooks : [];
|
|
100
|
+
const kept = hooks.filter((hook: any) => {
|
|
101
|
+
const command = typeof hook?.command === "string" ? hook.command : "";
|
|
102
|
+
return !isGxpmOwnedCodexHookCommand(command);
|
|
103
|
+
});
|
|
104
|
+
return { ...entry, hooks: kept };
|
|
105
|
+
})
|
|
106
|
+
.filter((entry: any) => Array.isArray(entry.hooks) && entry.hooks.length > 0);
|
|
107
|
+
|
|
108
|
+
if (nextEntries.length > 0) {
|
|
109
|
+
parsed.hooks[eventName] = nextEntries;
|
|
110
|
+
} else {
|
|
111
|
+
delete parsed.hooks[eventName];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
writeFileSync(path, JSON.stringify(parsed, null, 2) + "\n");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isGxpmOwnedCodexHookCommand(command: string) {
|
|
119
|
+
if (CODEX_HOOK_SCRIPTS.some((script) => command.includes(script))) return true;
|
|
120
|
+
return /^gxpm hook \S+ --host codex(?:\s|$)/.test(command.trim());
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function describeAction(action: PlannedAction, dryRun: boolean) {
|
|
124
|
+
const prefix = dryRun ? "would" : "did";
|
|
125
|
+
if (action.kind === "remove") {
|
|
126
|
+
const type = existsSync(action.path) && lstatSync(action.path).isDirectory() ? "directory" : "path";
|
|
127
|
+
return `${prefix} remove ${type}: ${action.path}`;
|
|
128
|
+
}
|
|
129
|
+
return `${prefix} update file: ${action.path}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseArgs(argv: string[]): UninstallOptions {
|
|
133
|
+
const options: UninstallOptions = {};
|
|
134
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
135
|
+
const arg = argv[index];
|
|
136
|
+
if (arg === "--dry-run") {
|
|
137
|
+
options.dryRun = true;
|
|
138
|
+
} else if (arg === "--purge") {
|
|
139
|
+
options.purge = true;
|
|
140
|
+
} else if (arg === "--target") {
|
|
141
|
+
options.target = argv[++index];
|
|
142
|
+
} else if (arg === "--home") {
|
|
143
|
+
options.home = argv[++index];
|
|
144
|
+
} else {
|
|
145
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return options;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (import.meta.main) {
|
|
152
|
+
try {
|
|
153
|
+
const options = parseArgs(Bun.argv.slice(2));
|
|
154
|
+
const actions = runUninstall(options);
|
|
155
|
+
for (const action of actions) {
|
|
156
|
+
console.log(describeAction(action, options.dryRun ?? false));
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_GXPM_ROOT = resolve(import.meta.dir, "..");
|
|
5
|
+
const VERSION_PATTERN = /^\d+\.\d+\.\d+\.\d+$/;
|
|
6
|
+
|
|
7
|
+
export function readGxpmVersion(input: { root?: string } = {}): string {
|
|
8
|
+
const root = input.root ?? DEFAULT_GXPM_ROOT;
|
|
9
|
+
const versionPath = join(root, "VERSION");
|
|
10
|
+
if (!existsSync(versionPath)) {
|
|
11
|
+
throw new Error(`VERSION file not found: ${versionPath}`);
|
|
12
|
+
}
|
|
13
|
+
const version = readFileSync(versionPath, "utf8").trim();
|
|
14
|
+
if (!version) {
|
|
15
|
+
throw new Error("VERSION file is empty");
|
|
16
|
+
}
|
|
17
|
+
return version;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function validateVersionTruth(input: { root?: string } = {}): string[] {
|
|
21
|
+
const root = input.root ?? DEFAULT_GXPM_ROOT;
|
|
22
|
+
const errors: string[] = [];
|
|
23
|
+
let version = "";
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
version = readGxpmVersion({ root });
|
|
27
|
+
} catch (error) {
|
|
28
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (version && !VERSION_PATTERN.test(version)) {
|
|
32
|
+
errors.push(`VERSION must use four-segment numeric format, got: ${version}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
|
|
37
|
+
version?: string;
|
|
38
|
+
};
|
|
39
|
+
if (version && pkg.version !== version) {
|
|
40
|
+
errors.push(`package.json version (${pkg.version ?? "<missing>"}) must match VERSION (${version})`);
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
errors.push(`package.json version check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return errors;
|
|
47
|
+
}
|