Spaces:
Runtime error
Runtime error
File size: 8,140 Bytes
a6b96c2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | "use strict";
/**
* Core Utilities β Shared low-level utility primitives
*
* ADR-857 rollout phase 2c: extracted from core.cts (issue #877).
* Owns POSIX path normalization, sub-repo/subdirectory scanning,
* phase file stats, slug/one-liner/plan-id helpers, and time-ago.
* Behaviour is preserved byte-for-behaviour from the prior location;
* only the module boundary moved. core.cjs re-exports every public symbol
* here under its own `export =` object so existing consumers are unaffected.
*
* New imports should pull core-utils helpers from core-utils.cjs directly.
*
* Dependencies (leaf modules only β no core.cjs, no loadConfig):
* - node:fs / node:path (stdlib)
* - ./phase-id.cjs (comparePhaseNum, used by readSubdirectories)
* - ./planning-workspace.cjs (findContextMdIn, used by getPhaseFileStats)
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
// eslint-disable-next-line @typescript-eslint/no-require-imports
const phaseIdModule = require("./phase-id.cjs");
const { comparePhaseNum } = phaseIdModule;
// eslint-disable-next-line @typescript-eslint/no-require-imports
const planningWorkspace = require("./planning-workspace.cjs");
const { findContextMdIn } = planningWorkspace;
// βββ Path helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/** Normalize a relative path to always use forward slashes (cross-platform). */
function toPosixPath(p) {
return p.split(node_path_1.default.sep).join('/');
}
/**
* Scan immediate child directories for separate git repos.
* Returns a sorted array of directory names that have their own `.git`.
* Excludes hidden directories and node_modules.
*/
function detectSubRepos(cwd) {
const results = [];
try {
const entries = node_fs_1.default.readdirSync(cwd, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory())
continue;
if (entry.name.startsWith('.') || entry.name === 'node_modules')
continue;
const gitPath = node_path_1.default.join(cwd, entry.name, '.git');
try {
if (node_fs_1.default.existsSync(gitPath)) {
results.push(entry.name);
}
}
catch { /* ignore */ }
}
}
catch { /* ignore */ }
return results.sort();
}
// βββ Summary body helpers βββββββββββββββββββββββββββββββββββββββββββββββββ
/**
* Extract a one-liner from the summary body when it's not in frontmatter.
*/
function extractOneLinerFromBody(content) {
if (!content)
return null;
const normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const body = normalized.replace(/^---\n[\s\S]*?\n---\n*/, '');
const match = body.match(/^#[^\n]*\n+\*\*([^*\n]+)\*\*([^\n]*)/m);
if (!match)
return null;
const boldInner = match[1].trim();
const afterBold = match[2];
if (/:\s*$/.test(boldInner)) {
const prose = afterBold.trim();
return prose.length > 0 ? prose : null;
}
return boldInner.length > 0 ? boldInner : null;
}
// βββ Misc utilities βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function pathExistsInternal(cwd, targetPath) {
const fullPath = node_path_1.default.isAbsolute(targetPath) ? targetPath : node_path_1.default.join(cwd, targetPath);
try {
node_fs_1.default.statSync(fullPath);
return true;
}
catch {
return false;
}
}
function generateSlugInternal(text) {
if (!text)
return null;
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').substring(0, 60);
}
// βββ Phase file helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/** Filter a file list to just PLAN.md / *-PLAN.md entries. */
function filterPlanFiles(files) {
return files.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
}
/** Filter a file list to just SUMMARY.md / *-SUMMARY.md entries. */
function filterSummaryFiles(files) {
return files.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
}
/**
* Read a phase directory and return counts/flags for common file types.
*/
function getPhaseFileStats(phaseDir) {
const files = node_fs_1.default.readdirSync(phaseDir);
return {
plans: filterPlanFiles(files),
summaries: filterSummaryFiles(files),
hasResearch: files.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'),
hasContext: findContextMdIn(files) !== null,
hasVerification: files.some(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md'),
hasReviews: files.some(f => f.endsWith('-REVIEWS.md') || f === 'REVIEWS.md'),
};
}
/**
* Read immediate child directories from a path.
* Returns [] if the path doesn't exist or can't be read.
* Pass sort=true to apply comparePhaseNum ordering.
*/
function readSubdirectories(dirPath, sort = false) {
try {
const entries = node_fs_1.default.readdirSync(dirPath, { withFileTypes: true });
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
return sort ? dirs.sort((a, b) => comparePhaseNum(a, b)) : dirs;
}
catch {
return [];
}
}
/**
* Format a Date as a fuzzy relative time string (e.g. "5 minutes ago").
*/
function timeAgo(date) {
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
if (seconds < 5)
return 'just now';
if (seconds < 60)
return `${seconds} seconds ago`;
const minutes = Math.floor(seconds / 60);
if (minutes === 1)
return '1 minute ago';
if (minutes < 60)
return `${minutes} minutes ago`;
const hours = Math.floor(minutes / 60);
if (hours === 1)
return '1 hour ago';
if (hours < 24)
return `${hours} hours ago`;
const days = Math.floor(hours / 24);
if (days === 1)
return '1 day ago';
if (days < 30)
return `${days} days ago`;
const months = Math.floor(days / 30);
if (months === 1)
return '1 month ago';
if (months < 12)
return `${months} months ago`;
const years = Math.floor(days / 365);
if (years === 1)
return '1 year ago';
return `${years} years ago`;
}
// βββ Plan ID helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/**
* Extract the canonical plan ID from a filename.
* Private to the core cluster β exported so core.cjs:searchPhaseInDir can
* import it from this leaf without circular dependency, but NOT re-exported
* from core.cjs's public `export =` block.
*/
function extractCanonicalPlanId(filename) {
const base = filename.replace(/-PLAN\.md$/i, '').replace(/-SUMMARY\.md$/i, '').replace(/\.md$/i, '');
const parts = base.split('-').filter(Boolean);
const tokenRe = /^\d+[A-Z]?(?:\.\d+)*$/i;
const phaseIdx = parts.findIndex(p => tokenRe.test(p));
if (phaseIdx >= 0 && phaseIdx + 1 < parts.length && tokenRe.test(parts[phaseIdx + 1])) {
return `${parts[phaseIdx]}-${parts[phaseIdx + 1]}`;
}
return base;
}
module.exports = {
toPosixPath,
detectSubRepos,
extractOneLinerFromBody,
pathExistsInternal,
generateSlugInternal,
filterPlanFiles,
filterSummaryFiles,
getPhaseFileStats,
readSubdirectories,
timeAgo,
extractCanonicalPlanId,
};
|