feat(P03): multi-project support, NFR milestone versioning, phase context reset, install scripts (v0.3.0)

This commit is contained in:
CI
2026-05-29 15:13:45 +00:00
parent e4bb3a9970
commit ddf04792c7
57 changed files with 1748 additions and 59 deletions
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env bash
set -euo pipefail
OPENCODE_DIR="${HOME}/.config/opencode"
CI_DIR="$(cd "$(dirname "$0")/.." && pwd)/opencode"
UNINSTALL=false
FORCE=false
for arg in "$@"; do
case "$arg" in
--uninstall) UNINSTALL=true ;;
--force) FORCE=true ;;
--help|-h)
echo "Usage: $(basename "$0") [--uninstall] [--force]"
echo ""
echo "Install CI opencode integration files to ~/.config/opencode/"
echo ""
echo " --uninstall Remove CI integration files"
echo " --force Overwrite existing files without prompting"
echo " --help Show this help"
exit 0
;;
esac
done
if [ "$UNINSTALL" = true ]; then
echo "Uninstalling CI opencode integration..."
rm -f "${OPENCODE_DIR}/agents/ci-"*.md 2>/dev/null || true
rm -f "${OPENCODE_DIR}/command/ci-"*.md 2>/dev/null || true
rm -rf "${OPENCODE_DIR}/ci/" 2>/dev/null || true
echo "CI integration files removed."
echo "Note: opencode.json permissions entry preserved (edit manually if needed)."
exit 0
fi
if [ ! -d "$CI_DIR" ]; then
echo "Error: opencode/ directory not found at ${CI_DIR}"
echo "Ensure you're running from the CI repository root."
exit 1
fi
echo "Installing CI opencode integration..."
echo " Source: ${CI_DIR}"
echo " Target: ${OPENCODE_DIR}"
echo ""
mkdir -p "${OPENCODE_DIR}/agents"
mkdir -p "${OPENCODE_DIR}/command"
mkdir -p "${OPENCODE_DIR}/ci/contexts"
mkdir -p "${OPENCODE_DIR}/ci/references"
mkdir -p "${OPENCODE_DIR}/ci/workflows"
COPIED=0
SKIPPED=0
copy_file() {
local src="$1"
local dest="$2"
if [ -f "$dest" ] && [ "$FORCE" = false ]; then
if cmp -s "$src" "$dest" 2>/dev/null; then
SKIPPED=$((SKIPPED + 1))
return
fi
echo " Conflict: $(basename "$src") already exists. Use --force to overwrite."
SKIPPED=$((SKIPPED + 1))
return
fi
cp "$src" "$dest"
COPIED=$((COPIED + 1))
}
echo "Installing agents..."
for f in "${CI_DIR}/agents/ci-"*.md; do
[ -f "$f" ] && copy_file "$f" "${OPENCODE_DIR}/agents/$(basename "$f")"
done
echo "Installing commands..."
for f in "${CI_DIR}/command/ci-"*.md; do
[ -f "$f" ] && copy_file "$f" "${OPENCODE_DIR}/command/$(basename "$f")"
done
echo "Installing contexts..."
for f in "${CI_DIR}/ci/contexts/"*.md; do
[ -f "$f" ] && copy_file "$f" "${OPENCODE_DIR}/ci/contexts/$(basename "$f")"
done
echo "Installing references..."
for f in "${CI_DIR}/ci/references/"*.md; do
[ -f "$f" ] && copy_file "$f" "${OPENCODE_DIR}/ci/references/$(basename "$f")"
done
echo "Installing workflows..."
for f in "${CI_DIR}/ci/workflows/"*.md; do
[ -f "$f" ] && copy_file "$f" "${OPENCODE_DIR}/ci/workflows/$(basename "$f")"
done
echo "Installing VERSION..."
[ -f "${CI_DIR}/ci/VERSION" ] && copy_file "${CI_DIR}/ci/VERSION" "${OPENCODE_DIR}/ci/VERSION"
echo ""
echo "Merging opencode.json permissions..."
OPENCODE_JSON="${OPENCODE_DIR}/opencode.json"
CI_JSON="${CI_DIR}/opencode.json"
if [ -f "$CI_JSON" ]; then
if [ ! -f "$OPENCODE_JSON" ]; then
cp "$CI_JSON" "$OPENCODE_JSON"
echo " Created opencode.json"
else
if command -v node &>/dev/null; then
node -e "
const fs = require('fs');
const existing = JSON.parse(fs.readFileSync('${OPENCODE_JSON}', 'utf8'));
const ci = JSON.parse(fs.readFileSync('${CI_JSON}', 'utf8'));
const merged = { ...existing };
merged.permission = merged.permission || {};
merged.permission.read = merged.permission.read || {};
merged.permission.external_directory = merged.permission.external_directory || {};
for (const [k, v] of Object.entries(ci.permission?.read || {})) {
if (!merged.permission.read[k]) merged.permission.read[k] = v;
}
for (const [k, v] of Object.entries(ci.permission?.external_directory || {})) {
if (!merged.permission.external_directory[k]) merged.permission.external_directory[k] = v;
}
fs.writeFileSync('${OPENCODE_JSON}', JSON.stringify(merged, null, 2));
console.log(' Merged permissions (preserved existing entries)');
"
else
echo " Warning: node not found. Manually merge opencode.json permissions."
echo " Add to opencode.json:"
echo ' "~/.config/opencode/ci/*": "allow" (in permission.read and permission.external_directory)'
fi
fi
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " CI ► INSTALL COMPLETE"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo " Copied: ${COPIED} files"
echo " Skipped: ${SKIPPED} files"
echo ""
echo " Commands available: ci-init, ci-run, ci-quick, ci-status,"
echo " ci-audit, ci-verify, ci-debug, ci-review, ci-ship,"
echo " ci-rollback, ci-clarify"
echo ""
echo " Run --uninstall to remove."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+127
View File
@@ -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);
}