@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,515 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { installClaudeHooks } from "../install-claude-hooks";
|
|
6
|
+
import { installCodexHooks } from "../install-codex-hooks";
|
|
7
|
+
import { installKimiHooks } from "../install-kimi-hooks";
|
|
8
|
+
import { installSkill } from "../install-skill";
|
|
9
|
+
import { probeHosts, detectedHostNames } from "../../core/host-probe";
|
|
10
|
+
import type { HostProbeResult } from "../../core/host-probe";
|
|
11
|
+
import type { HostName } from "../../hosts";
|
|
12
|
+
import { getConfigValue, parseAgentsMdConfig } from "../../core/config";
|
|
13
|
+
|
|
14
|
+
interface InitOptions {
|
|
15
|
+
target?: string;
|
|
16
|
+
nonInteractive?: boolean;
|
|
17
|
+
skipHooks?: boolean;
|
|
18
|
+
skipSkills?: boolean;
|
|
19
|
+
skipCodexHooks?: boolean;
|
|
20
|
+
baseBranch?: string;
|
|
21
|
+
/** Explicit comma-separated host list, e.g. "claude,codex" */
|
|
22
|
+
hosts?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type BaseBranchSource = "option" | "config" | "agents-md" | "claude-md" | "git-branch" | "prompt";
|
|
26
|
+
|
|
27
|
+
interface BaseBranchResolution {
|
|
28
|
+
branch: string;
|
|
29
|
+
source: BaseBranchSource;
|
|
30
|
+
candidates: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const HOOK_SPECS: { gxpmFile: string; topLevelFile: string; argsForwarding: string }[] = [
|
|
34
|
+
{ gxpmFile: "gxpm-pre-commit", topLevelFile: "pre-commit", argsForwarding: "" },
|
|
35
|
+
{ gxpmFile: "gxpm-commit-msg", topLevelFile: "commit-msg", argsForwarding: ' "$1"' },
|
|
36
|
+
{ gxpmFile: "gxpm-pre-push", topLevelFile: "pre-push", argsForwarding: ' "$@"' },
|
|
37
|
+
{ gxpmFile: "gxpm-post-merge", topLevelFile: "post-merge", argsForwarding: ' "$@"' },
|
|
38
|
+
{ gxpmFile: "gxpm-post-checkout", topLevelFile: "post-checkout", argsForwarding: ' "$@"' },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const GXPM_GITIGNORE_ENTRY = ".gxpm/";
|
|
42
|
+
const GXPM_GITIGNORE_EQUIVALENTS = new Set([".gxpm", ".gxpm/", "/.gxpm", "/.gxpm/"]);
|
|
43
|
+
const CORE_BASE_BRANCHES = ["main", "master", "develop", "development", "dev", "trunk"] as const;
|
|
44
|
+
|
|
45
|
+
function dispatcherScript(gxpmFile: string, argsForwarding: string): string {
|
|
46
|
+
return `#!/bin/bash
|
|
47
|
+
# gxpm dispatcher (installed by gxpm init)
|
|
48
|
+
set -e
|
|
49
|
+
HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
50
|
+
if [ -x "$HOOK_DIR/${gxpmFile}" ]; then
|
|
51
|
+
"$HOOK_DIR/${gxpmFile}"${argsForwarding}
|
|
52
|
+
fi
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function installGitHooks(target: string, gxpmRoot: string): string[] {
|
|
57
|
+
const templatesDir = join(gxpmRoot, "templates", "hooks");
|
|
58
|
+
const hooksDir = join(target, ".githooks");
|
|
59
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
60
|
+
|
|
61
|
+
const installed: string[] = [];
|
|
62
|
+
for (const spec of HOOK_SPECS) {
|
|
63
|
+
const src = join(templatesDir, spec.gxpmFile);
|
|
64
|
+
const dst = join(hooksDir, spec.gxpmFile);
|
|
65
|
+
if (existsSync(src)) {
|
|
66
|
+
copyFileSync(src, dst);
|
|
67
|
+
execSync(`chmod +x "${dst}"`);
|
|
68
|
+
installed.push(dst);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const topLevelPath = join(hooksDir, spec.topLevelFile);
|
|
72
|
+
if (!existsSync(topLevelPath)) {
|
|
73
|
+
writeFileSync(topLevelPath, dispatcherScript(spec.gxpmFile, spec.argsForwarding));
|
|
74
|
+
execSync(`chmod +x "${topLevelPath}"`);
|
|
75
|
+
installed.push(topLevelPath);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const absoluteHooksPath = join(resolve(target), ".githooks");
|
|
80
|
+
let currentHooksPath = "";
|
|
81
|
+
try {
|
|
82
|
+
currentHooksPath = execSync("git config core.hooksPath", { cwd: target }).toString().trim();
|
|
83
|
+
} catch {
|
|
84
|
+
// unset
|
|
85
|
+
}
|
|
86
|
+
if (currentHooksPath !== absoluteHooksPath) {
|
|
87
|
+
execSync(`git config core.hooksPath "${absoluteHooksPath}"`, { cwd: target });
|
|
88
|
+
}
|
|
89
|
+
return installed;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function ensureGxpmDir(target: string): string[] {
|
|
93
|
+
const dirs = [
|
|
94
|
+
join(target, ".gxpm", "issues"),
|
|
95
|
+
join(target, ".gxpm", "local"),
|
|
96
|
+
join(target, ".gxpm", "out-of-scope"),
|
|
97
|
+
join(target, ".gxpm", "wiki"),
|
|
98
|
+
];
|
|
99
|
+
for (const dir of dirs) {
|
|
100
|
+
mkdirSync(dir, { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
return dirs;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function ensureGxpmGitignore(target: string): string | null {
|
|
106
|
+
const gitignorePath = join(target, ".gitignore");
|
|
107
|
+
if (!existsSync(gitignorePath)) {
|
|
108
|
+
writeFileSync(gitignorePath, `${GXPM_GITIGNORE_ENTRY}\n`);
|
|
109
|
+
return gitignorePath;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const current = readFileSync(gitignorePath, "utf8");
|
|
113
|
+
const hasEntry = current
|
|
114
|
+
.split(/\r?\n/)
|
|
115
|
+
.map((line) => line.trim())
|
|
116
|
+
.some((line) => GXPM_GITIGNORE_EQUIVALENTS.has(line));
|
|
117
|
+
if (hasEntry) return null;
|
|
118
|
+
|
|
119
|
+
const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
|
|
120
|
+
writeFileSync(gitignorePath, `${current}${prefix}${GXPM_GITIGNORE_ENTRY}\n`);
|
|
121
|
+
return gitignorePath;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function ensureDefaultConfig(target: string, baseBranch: string, overwriteBaseBranch = false): string | null {
|
|
125
|
+
const configPath = join(target, ".gxpm", "config.json");
|
|
126
|
+
if (existsSync(configPath)) {
|
|
127
|
+
const existing = JSON.parse(readFileSync(configPath, "utf8")) as Record<string, unknown>;
|
|
128
|
+
const worktree = (existing.worktree ?? {}) as Record<string, unknown>;
|
|
129
|
+
if (!overwriteBaseBranch && typeof worktree.baseBranch === "string" && worktree.baseBranch.trim()) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
existing.worktree = { ...worktree, baseBranch };
|
|
133
|
+
writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
134
|
+
return configPath;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const defaultConfig = {
|
|
138
|
+
worktree: { enforcement: "optional", default: "ask", baseBranch },
|
|
139
|
+
sync: { provider: "none", autoSync: false, syncArtifacts: true },
|
|
140
|
+
update_check: true,
|
|
141
|
+
};
|
|
142
|
+
writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + "\n");
|
|
143
|
+
return configPath;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isGitRepo(dir: string): boolean {
|
|
147
|
+
const gitDir = join(dir, ".git");
|
|
148
|
+
if (!existsSync(gitDir)) return false;
|
|
149
|
+
try {
|
|
150
|
+
return statSync(gitDir).isDirectory();
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isTty(): boolean {
|
|
157
|
+
try {
|
|
158
|
+
return (process.stdin as any).isTTY === true && (process.stdout as any).isTTY === true;
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function normalizeBranchName(raw: string): string {
|
|
165
|
+
return raw
|
|
166
|
+
.trim()
|
|
167
|
+
.replace(/^refs\/heads\//, "")
|
|
168
|
+
.replace(/^refs\/remotes\//, "");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function normalizeRemoteBranchName(raw: string): string {
|
|
172
|
+
const normalized = normalizeBranchName(raw);
|
|
173
|
+
const slash = normalized.indexOf("/");
|
|
174
|
+
return slash === -1 ? normalized : normalized.slice(slash + 1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function readDocumentBaseBranch(target: string, filename: "AGENTS.md" | "CLAUDE.md"): string | undefined {
|
|
178
|
+
const path = join(target, filename);
|
|
179
|
+
if (!existsSync(path)) return undefined;
|
|
180
|
+
const parsed = parseAgentsMdConfig(readFileSync(path, "utf8"));
|
|
181
|
+
const branch = ((parsed.worktree ?? {}) as { baseBranch?: string }).baseBranch;
|
|
182
|
+
return typeof branch === "string" && branch.trim() ? normalizeBranchName(branch) : undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function listLikelyBaseBranches(target: string): string[] {
|
|
186
|
+
const branchNames = new Set<string>();
|
|
187
|
+
|
|
188
|
+
const collect = (cmd: string[], remote = false) => {
|
|
189
|
+
const result = Bun.spawnSync({
|
|
190
|
+
cmd,
|
|
191
|
+
cwd: target,
|
|
192
|
+
stdout: "pipe",
|
|
193
|
+
stderr: "pipe",
|
|
194
|
+
});
|
|
195
|
+
if (result.exitCode !== 0) return;
|
|
196
|
+
for (const line of result.stdout.toString().split("\n")) {
|
|
197
|
+
const value = line.trim();
|
|
198
|
+
if (!value || value.includes(" -> ") || value.endsWith("/HEAD")) continue;
|
|
199
|
+
branchNames.add(remote ? normalizeRemoteBranchName(value) : normalizeBranchName(value));
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
collect(["git", "branch", "--format=%(refname:short)"]);
|
|
204
|
+
collect(["git", "branch", "-r", "--format=%(refname:short)"], true);
|
|
205
|
+
collect(["git", "branch", "--show-current"]);
|
|
206
|
+
|
|
207
|
+
return CORE_BASE_BRANCHES.filter((branch) => branchNames.has(branch));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function promptBaseBranch(candidates: string[]): BaseBranchResolution {
|
|
211
|
+
console.log("");
|
|
212
|
+
if (candidates.length > 0) {
|
|
213
|
+
console.log("Multiple likely base branches detected:");
|
|
214
|
+
candidates.forEach((branch, index) => console.log(` ${index + 1}. ${branch}`));
|
|
215
|
+
process.stdout.write(`Select gxpm worktree base branch [${candidates[0]}]: `);
|
|
216
|
+
} else {
|
|
217
|
+
console.log("gxpm could not infer a worktree base branch from AGENTS.md, CLAUDE.md, or git branches.");
|
|
218
|
+
process.stdout.write("Enter gxpm worktree base branch: ");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const answer = readLineSync().trim();
|
|
222
|
+
if (answer === "" && candidates.length > 0) {
|
|
223
|
+
return { branch: candidates[0], source: "prompt", candidates };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const selected = candidates[Number(answer) - 1] ?? answer;
|
|
227
|
+
const branch = normalizeBranchName(selected);
|
|
228
|
+
if (!branch) {
|
|
229
|
+
throw new Error("Base branch cannot be empty. Re-run gxpm init --base-branch <branch>.");
|
|
230
|
+
}
|
|
231
|
+
return { branch, source: "prompt", candidates };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function resolveInitBaseBranch(options: InitOptions, target: string): BaseBranchResolution {
|
|
235
|
+
if (options.baseBranch) {
|
|
236
|
+
return { branch: normalizeBranchName(options.baseBranch), source: "option", candidates: [] };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const configured = getConfigValue({ root: target, key: "worktree.baseBranch" });
|
|
240
|
+
if (typeof configured.value === "string" && configured.value.trim()) {
|
|
241
|
+
return { branch: normalizeBranchName(configured.value), source: "config", candidates: [] };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const docCandidates = [
|
|
245
|
+
{ source: "agents-md" as const, branch: readDocumentBaseBranch(target, "AGENTS.md") },
|
|
246
|
+
{ source: "claude-md" as const, branch: readDocumentBaseBranch(target, "CLAUDE.md") },
|
|
247
|
+
].filter((item): item is { source: "agents-md" | "claude-md"; branch: string } => !!item.branch);
|
|
248
|
+
const uniqueDocBranches = Array.from(new Set(docCandidates.map((item) => item.branch)));
|
|
249
|
+
if (uniqueDocBranches.length === 1) {
|
|
250
|
+
return {
|
|
251
|
+
branch: uniqueDocBranches[0],
|
|
252
|
+
source: docCandidates.find((item) => item.branch === uniqueDocBranches[0])?.source ?? "agents-md",
|
|
253
|
+
candidates: uniqueDocBranches,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (uniqueDocBranches.length > 1) {
|
|
257
|
+
if (!options.nonInteractive && isTty()) return promptBaseBranch(uniqueDocBranches);
|
|
258
|
+
throw new Error(
|
|
259
|
+
`Conflicting gxpm worktree.baseBranch values found in AGENTS.md/CLAUDE.md: ${uniqueDocBranches.join(", ")}. ` +
|
|
260
|
+
"Re-run gxpm init --base-branch <branch>.",
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const candidates = listLikelyBaseBranches(target);
|
|
265
|
+
if (candidates.length === 1) {
|
|
266
|
+
return { branch: candidates[0], source: "git-branch", candidates };
|
|
267
|
+
}
|
|
268
|
+
if (candidates.length > 1) {
|
|
269
|
+
if (!options.nonInteractive && isTty()) return promptBaseBranch(candidates);
|
|
270
|
+
throw new Error(
|
|
271
|
+
`Multiple likely base branches found: ${candidates.join(", ")}. ` +
|
|
272
|
+
"Re-run gxpm init --base-branch <branch> or add worktree.baseBranch to AGENTS.md/CLAUDE.md.",
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (!options.nonInteractive && isTty()) return promptBaseBranch(candidates);
|
|
276
|
+
throw new Error(
|
|
277
|
+
"Could not infer gxpm worktree base branch. Re-run gxpm init --base-branch <branch> " +
|
|
278
|
+
"or add worktree.baseBranch to AGENTS.md/CLAUDE.md.",
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Resolve which hosts to configure.
|
|
284
|
+
*
|
|
285
|
+
* Priority:
|
|
286
|
+
* 1. --hosts <list> → explicit override
|
|
287
|
+
* 2. --non-interactive → auto-select all detected hosts
|
|
288
|
+
* 3. TTY interactive → probe, print table, prompt user
|
|
289
|
+
* 4. Fallback → auto-select all detected hosts
|
|
290
|
+
*/
|
|
291
|
+
function resolveSelectedHosts(options: InitOptions, target: string): HostName[] {
|
|
292
|
+
// 1. Explicit --hosts override
|
|
293
|
+
if (options.hosts) {
|
|
294
|
+
const names = options.hosts.split(",").map((s) => s.trim()).filter(Boolean) as HostName[];
|
|
295
|
+
return names;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const probed = probeHosts(target);
|
|
299
|
+
const detected = probed.filter((r) => r.detected);
|
|
300
|
+
|
|
301
|
+
// Nothing detected → empty (user can still --hosts)
|
|
302
|
+
if (detected.length === 0) {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 2. Non-interactive → auto-select detected
|
|
307
|
+
if (options.nonInteractive) {
|
|
308
|
+
return detected.map((r) => r.host);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 3. TTY interactive → show detection table, prompt
|
|
312
|
+
if (isTty()) {
|
|
313
|
+
return promptHostSelection(detected, probed);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 4. Fallback → auto-select detected
|
|
317
|
+
return detected.map((r) => r.host);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function promptHostSelection(detected: HostProbeResult[], all: HostProbeResult[]): HostName[] {
|
|
321
|
+
console.log("");
|
|
322
|
+
console.log("Detected agent CLIs:");
|
|
323
|
+
console.log(" Host CLI Repo Config User Config");
|
|
324
|
+
console.log(" ─────────────────────────────────────────────");
|
|
325
|
+
for (const r of all) {
|
|
326
|
+
const mark = r.detected ? "✓" : " ";
|
|
327
|
+
const cli = r.cliInstalled ? "✓" : " ";
|
|
328
|
+
const repo = r.repoConfigExists ? "✓" : " ";
|
|
329
|
+
const user = r.userConfigExists ? "✓" : " ";
|
|
330
|
+
console.log(` ${mark} ${r.host.padEnd(10)} ${cli} ${repo} ${user} ${r.displayName}`);
|
|
331
|
+
}
|
|
332
|
+
console.log("");
|
|
333
|
+
|
|
334
|
+
// If only one detected, default to yes
|
|
335
|
+
if (detected.length === 1) {
|
|
336
|
+
const r = detected[0];
|
|
337
|
+
process.stdout.write(`Configure gxpm for ${r.displayName}? [Y/n] `);
|
|
338
|
+
const answer = readLineSync().trim().toLowerCase();
|
|
339
|
+
if (answer === "" || answer === "y" || answer === "yes") {
|
|
340
|
+
return [r.host];
|
|
341
|
+
}
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Multiple detected → ask each
|
|
346
|
+
const selected: HostName[] = [];
|
|
347
|
+
for (const r of detected) {
|
|
348
|
+
process.stdout.write(`Configure gxpm for ${r.displayName}? [Y/n] `);
|
|
349
|
+
const answer = readLineSync().trim().toLowerCase();
|
|
350
|
+
if (answer === "" || answer === "y" || answer === "yes") {
|
|
351
|
+
selected.push(r.host);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return selected;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function readLineSync(): string {
|
|
358
|
+
const buffer = Buffer.alloc(1024);
|
|
359
|
+
let result = "";
|
|
360
|
+
while (true) {
|
|
361
|
+
const bytesRead = readSync(0, buffer, 0, 1024, null);
|
|
362
|
+
if (bytesRead === 0) break;
|
|
363
|
+
const chunk = buffer.toString("utf8", 0, bytesRead);
|
|
364
|
+
result += chunk;
|
|
365
|
+
if (chunk.includes("\n")) break;
|
|
366
|
+
}
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function runInitCommand(argv: string[]) {
|
|
371
|
+
const options: InitOptions = {};
|
|
372
|
+
let i = 0;
|
|
373
|
+
while (i < argv.length) {
|
|
374
|
+
const arg = argv[i];
|
|
375
|
+
if (arg === "--target") {
|
|
376
|
+
options.target = argv[++i];
|
|
377
|
+
} else if (arg === "--non-interactive") {
|
|
378
|
+
options.nonInteractive = true;
|
|
379
|
+
} else if (arg === "--skip-hooks") {
|
|
380
|
+
options.skipHooks = true;
|
|
381
|
+
} else if (arg === "--skip-skills") {
|
|
382
|
+
options.skipSkills = true;
|
|
383
|
+
} else if (arg === "--skip-codex-hooks") {
|
|
384
|
+
options.skipCodexHooks = true;
|
|
385
|
+
} else if (arg === "--base-branch") {
|
|
386
|
+
options.baseBranch = argv[++i];
|
|
387
|
+
} else if (arg === "--hosts") {
|
|
388
|
+
options.hosts = argv[++i];
|
|
389
|
+
}
|
|
390
|
+
i++;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const target = resolve(options.target ?? process.cwd());
|
|
394
|
+
const gxpmRoot = resolve(import.meta.dir, "../..");
|
|
395
|
+
|
|
396
|
+
if (!isGitRepo(target)) {
|
|
397
|
+
throw new Error(`Not a git repository: ${target}. gxpm requires git.`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const baseBranch = resolveInitBaseBranch(options, target);
|
|
401
|
+
|
|
402
|
+
// Resolve which hosts to configure
|
|
403
|
+
const selectedHosts = resolveSelectedHosts(options, target);
|
|
404
|
+
|
|
405
|
+
const results: Record<string, string[] | string> = {
|
|
406
|
+
dirs: [],
|
|
407
|
+
hooks: [],
|
|
408
|
+
claudeHooks: [],
|
|
409
|
+
codexHooks: [],
|
|
410
|
+
kimiHooks: [],
|
|
411
|
+
skills: [],
|
|
412
|
+
config: [],
|
|
413
|
+
gitignore: [],
|
|
414
|
+
hostsConfigured: selectedHosts.join(",") || "none",
|
|
415
|
+
baseBranch: `${baseBranch.branch} (${baseBranch.source})`,
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
// 1. Ensure .gxpm/ directory structure
|
|
419
|
+
results.dirs = ensureGxpmDir(target);
|
|
420
|
+
const gitignorePath = ensureGxpmGitignore(target);
|
|
421
|
+
if (gitignorePath) results.gitignore.push(gitignorePath);
|
|
422
|
+
|
|
423
|
+
// 2. Git hooks (always, host-agnostic)
|
|
424
|
+
if (!options.skipHooks) {
|
|
425
|
+
results.hooks = installGitHooks(target, gxpmRoot);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// 3. Claude hooks (only if claude is among selected hosts)
|
|
429
|
+
if (selectedHosts.includes("claude")) {
|
|
430
|
+
const hasRepoClaude = existsSync(join(target, ".claude"));
|
|
431
|
+
const hasUserClaude = existsSync(join(homedir(), ".claude"));
|
|
432
|
+
if (hasRepoClaude || hasUserClaude) {
|
|
433
|
+
const scope = hasRepoClaude ? "repo" : "user";
|
|
434
|
+
const claudeResult = installClaudeHooks({ scope, target: scope === "repo" ? target : undefined });
|
|
435
|
+
results.claudeHooks = [claudeResult.settingsJsonPath];
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// 4. Codex hooks (only if codex is among selected hosts)
|
|
440
|
+
if (!options.skipCodexHooks && selectedHosts.includes("codex")) {
|
|
441
|
+
const hasRepoCodex = existsSync(join(target, ".codex"));
|
|
442
|
+
const hasUserCodex = existsSync(join(homedir(), ".codex"));
|
|
443
|
+
if (hasRepoCodex || hasUserCodex) {
|
|
444
|
+
const scope = hasRepoCodex ? "repo" : "user";
|
|
445
|
+
installCodexHooks({ scope, target: scope === "repo" ? target : undefined, gxpmRoot });
|
|
446
|
+
results.codexHooks = ["hooks.json installed"];
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// 5. Kimi hooks (only if kimi is among selected hosts)
|
|
451
|
+
if (selectedHosts.includes("kimi")) {
|
|
452
|
+
const kimiResult = installKimiHooks();
|
|
453
|
+
results.kimiHooks = [kimiResult.configTomlPath];
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// 4. Skills (only for selected hosts)
|
|
457
|
+
if (!options.skipSkills && selectedHosts.length > 0) {
|
|
458
|
+
results.skills = installSkill({ hosts: selectedHosts, root: gxpmRoot });
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// 5. Default config
|
|
462
|
+
const configPath = ensureDefaultConfig(target, baseBranch.branch, baseBranch.source === "option");
|
|
463
|
+
if (configPath) results.config.push(configPath);
|
|
464
|
+
|
|
465
|
+
// Output
|
|
466
|
+
if (argv.includes("--json")) {
|
|
467
|
+
console.log(JSON.stringify({ target, ...results }, null, 2));
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
console.log(`gxpm init completed for ${target}`);
|
|
472
|
+
console.log("");
|
|
473
|
+
console.log(`Hosts configured: ${results.hostsConfigured}`);
|
|
474
|
+
console.log(`Base branch: ${baseBranch.branch} (${baseBranch.source})`);
|
|
475
|
+
console.log("");
|
|
476
|
+
console.log(`Created directories: ${results.dirs.length}`);
|
|
477
|
+
for (const d of results.dirs) console.log(` ${d}`);
|
|
478
|
+
if (results.gitignore.length > 0) {
|
|
479
|
+
console.log("");
|
|
480
|
+
console.log(`Updated gitignore: ${results.gitignore.length}`);
|
|
481
|
+
for (const path of results.gitignore) console.log(` ${path}`);
|
|
482
|
+
}
|
|
483
|
+
console.log("");
|
|
484
|
+
console.log(`Installed git hooks: ${results.hooks.length}`);
|
|
485
|
+
for (const h of results.hooks) console.log(` ${h}`);
|
|
486
|
+
if (results.claudeHooks.length > 0) {
|
|
487
|
+
console.log("");
|
|
488
|
+
console.log(`Installed Claude hooks: ${results.claudeHooks.length}`);
|
|
489
|
+
for (const h of results.claudeHooks) console.log(` ${h}`);
|
|
490
|
+
}
|
|
491
|
+
if (results.kimiHooks.length > 0) {
|
|
492
|
+
console.log("");
|
|
493
|
+
console.log(`Installed Kimi hooks: ${results.kimiHooks.length}`);
|
|
494
|
+
for (const h of results.kimiHooks) console.log(` ${h}`);
|
|
495
|
+
}
|
|
496
|
+
if (results.codexHooks.length > 0) {
|
|
497
|
+
console.log("");
|
|
498
|
+
console.log(`Installed Codex hooks: ${results.codexHooks.length}`);
|
|
499
|
+
for (const h of results.codexHooks) console.log(` ${h}`);
|
|
500
|
+
}
|
|
501
|
+
if (results.skills.length > 0) {
|
|
502
|
+
console.log("");
|
|
503
|
+
console.log(`Installed skills: ${results.skills.length}`);
|
|
504
|
+
for (const s of results.skills) console.log(` ${s}`);
|
|
505
|
+
}
|
|
506
|
+
if (results.config.length > 0) {
|
|
507
|
+
console.log("");
|
|
508
|
+
console.log(`Initialized config: ${results.config[0]}`);
|
|
509
|
+
}
|
|
510
|
+
console.log("");
|
|
511
|
+
console.log("Next steps:");
|
|
512
|
+
console.log(" gxpm config list");
|
|
513
|
+
console.log(" gxpm doctor --json");
|
|
514
|
+
console.log(" gxpm verify");
|
|
515
|
+
}
|