feat(P03): multi-project support, NFR milestone versioning, phase context reset, install scripts
---ci---
phase: 3
milestone: v0.3.0
status: complete
decisions:
- id: D-006
decision: Multi-project via .ci/<slug>/ subdirectories and config.json registry
rationale: Backward compatible migration from flat files; slug-based namespacing for branches and commits
confidence: 0.92
alternatives: [Git worktrees, Separate repos with subtrees]
- id: D-007
decision: NFR milestones use progressive patch versioning (no minor tag)
rationale: NFR phases (fix/chore/docs/perf/refactor/test) don't represent feature delivery; patch increments reflect incremental improvement only
confidence: 0.90
alternatives: [Treat all milestones uniformly, Skip versioning for NFR]
- id: D-008
decision: Phase context reset via git checkpoint + fresh agent spawn
rationale: Git-native architecture makes full state serialization safe; fresh context prevents accumulated conversation drift
confidence: 0.88
alternatives: [Context compaction, Sliding window summarization]
- id: D-009
decision: Install via both npm postinstall and standalone bash script
rationale: Postinstall only fires on npm install -g; standalone script covers manual/cloned installs
confidence: 0.95
alternatives: [Postinstall only, Makefile target]
---/ci---
Source code:
- Added ProjectEntry, projects[], active_project to CIConfig
- Added project?: string to CiMetadata, CommitScope, all commit input types
- CiFiles: multi-project support (projectSlug, listProjects, addProject, migrateFlatToProject, isNfrMilestone)
- GitContext: projectSlug support, detectProjectFromCommit(), isNfrMilestone()
- GitBranch: project-prefixed branch naming via prefix()
- commit-builder/parser: project field in ---ci--- blocks
- config.ts: initCI() accepts projectSlug/projectName
- Implemented parseRoadmapMd phase parsing
- 284 tests passing (66 new tests)
Install scripts:
- scripts/install.sh: Standalone bash installer
- scripts/postinstall.js: npm postinstall (global installs only)
OpenCode integration:
- All 18 agents updated with multi-project project_context
- All 11 workflows updated with Step 0: Confirm Active Project
- All 5 references updated (branch-strategy, ci-files-discipline, commit-schema, decision-engine, git-context-loading)
- All 3 contexts updated (dev, research, review)
- VERSION bumped to 0.3.0
Package:
- Added files field, postinstall script, install script alias
- Version bumped to 0.3.0
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { execSync } = require("child_process");
|
||||
|
||||
const OPENCODE_DIR = path.join(process.env.HOME || "/root", ".config", "opencode");
|
||||
|
||||
function getPackageDir() {
|
||||
try {
|
||||
return path.resolve(__dirname, "..");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isGlobalInstall() {
|
||||
if (process.env.npm_config_global === "true") return true;
|
||||
if (process.env.npm_config_global === "1") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function copyFile(src, dest, force) {
|
||||
if (!fs.existsSync(src)) return { copied: 0, skipped: 0 };
|
||||
|
||||
const dir = path.dirname(dest);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
if (fs.existsSync(dest) && !force) {
|
||||
try {
|
||||
const srcContent = fs.readFileSync(src, "utf8");
|
||||
const destContent = fs.readFileSync(dest, "utf8");
|
||||
if (srcContent === destContent) return { copied: 0, skipped: 1 };
|
||||
} catch {}
|
||||
return { copied: 0, skipped: 1 };
|
||||
}
|
||||
|
||||
fs.copyFileSync(src, dest);
|
||||
return { copied: 1, skipped: 0 };
|
||||
}
|
||||
|
||||
function install() {
|
||||
const pkgDir = getPackageDir();
|
||||
if (!pkgDir) {
|
||||
console.log("CI postinstall: Could not determine package directory. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
const opencodeDir = path.join(pkgDir, "opencode");
|
||||
if (!fs.existsSync(opencodeDir)) {
|
||||
console.log("CI postinstall: opencode/ directory not found. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isGlobalInstall()) {
|
||||
console.log("CI postinstall: Not a global install. Skipping opencode integration.");
|
||||
console.log(" Run `npx ci-install` or `./scripts/install.sh` to install manually.");
|
||||
return;
|
||||
}
|
||||
|
||||
let copied = 0;
|
||||
let skipped = 0;
|
||||
|
||||
function copyGlob(srcDir, destDir, pattern) {
|
||||
if (!fs.existsSync(srcDir)) return;
|
||||
const entries = fs.readdirSync(srcDir).filter((f) => {
|
||||
if (pattern instanceof RegExp) return pattern.test(f);
|
||||
return f.startsWith(pattern);
|
||||
});
|
||||
for (const entry of entries) {
|
||||
const result = copyFile(path.join(srcDir, entry), path.join(destDir, entry), false);
|
||||
copied += result.copied;
|
||||
skipped += result.skipped;
|
||||
}
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.join(OPENCODE_DIR, "agents"), { recursive: true });
|
||||
fs.mkdirSync(path.join(OPENCODE_DIR, "command"), { recursive: true });
|
||||
fs.mkdirSync(path.join(OPENCODE_DIR, "ci", "contexts"), { recursive: true });
|
||||
fs.mkdirSync(path.join(OPENCODE_DIR, "ci", "references"), { recursive: true });
|
||||
fs.mkdirSync(path.join(OPENCODE_DIR, "ci", "workflows"), { recursive: true });
|
||||
|
||||
copyGlob(path.join(opencodeDir, "agents"), path.join(OPENCODE_DIR, "agents"), /^ci-/);
|
||||
copyGlob(path.join(opencodeDir, "command"), path.join(OPENCODE_DIR, "command"), /^ci-/);
|
||||
copyGlob(path.join(opencodeDir, "ci", "contexts"), path.join(OPENCODE_DIR, "ci", "contexts"), /\.md$/);
|
||||
copyGlob(path.join(opencodeDir, "ci", "references"), path.join(OPENCODE_DIR, "ci", "references"), /\.md$/);
|
||||
copyGlob(path.join(opencodeDir, "ci", "workflows"), path.join(OPENCODE_DIR, "ci", "workflows"), /\.md$/);
|
||||
|
||||
const versionFile = path.join(opencodeDir, "ci", "VERSION");
|
||||
if (fs.existsSync(versionFile)) {
|
||||
const result = copyFile(versionFile, path.join(OPENCODE_DIR, "ci", "VERSION"), false);
|
||||
copied += result.copied;
|
||||
skipped += result.skipped;
|
||||
}
|
||||
|
||||
const ciJsonPath = path.join(opencodeDir, "opencode.json");
|
||||
const targetJsonPath = path.join(OPENCODE_DIR, "opencode.json");
|
||||
|
||||
if (fs.existsSync(ciJsonPath)) {
|
||||
if (!fs.existsSync(targetJsonPath)) {
|
||||
fs.copyFileSync(ciJsonPath, targetJsonPath);
|
||||
} else {
|
||||
try {
|
||||
const existing = JSON.parse(fs.readFileSync(targetJsonPath, "utf8"));
|
||||
const ciJson = JSON.parse(fs.readFileSync(ciJsonPath, "utf8"));
|
||||
existing.permission = existing.permission || {};
|
||||
existing.permission.read = existing.permission.read || {};
|
||||
existing.permission.external_directory = existing.permission.external_directory || {};
|
||||
for (const [k, v] of Object.entries(ciJson.permission?.read || {})) {
|
||||
if (!existing.permission.read[k]) existing.permission.read[k] = v;
|
||||
}
|
||||
for (const [k, v] of Object.entries(ciJson.permission?.external_directory || {})) {
|
||||
if (!existing.permission.external_directory[k]) existing.permission.external_directory[k] = v;
|
||||
}
|
||||
fs.writeFileSync(targetJsonPath, JSON.stringify(existing, null, 2));
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`CI postinstall: ${copied} files installed, ${skipped} skipped.`);
|
||||
}
|
||||
|
||||
try {
|
||||
install();
|
||||
} catch (err) {
|
||||
console.log("CI postinstall: Non-fatal error:", err.message);
|
||||
}
|
||||
Reference in New Issue
Block a user