mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
fix: 6 bugs fixed, 67 tests added for session-manager and session-aliases
Bug fixes: - utils.js: prevent duplicate 'g' flag in countInFile regex construction - validate-agents.js: handle CRLF line endings in frontmatter parsing - validate-hooks.js: handle \t and \\ escape sequences in inline JS validation - session-aliases.js: prevent NaN in date sort when timestamps are missing - session-aliases.js: persist rollback on rename failure instead of silent loss - session-manager.js: require absolute paths in getSessionStats to prevent content strings ending with .tmp from being treated as file paths New tests (164 total, up from 97): - session-manager.test.js: 27 tests covering parseSessionFilename, parseSessionMetadata, getSessionStats, CRUD operations, getSessionSize, getSessionTitle, edge cases (null input, non-existent files, directories) - session-aliases.test.js: 40 tests covering loadAliases (corrupted JSON, invalid structure), setAlias (validation, reserved names), resolveAlias, listAliases (sort, search, limit), deleteAlias, renameAlias, updateAliasTitle, resolveSessionAlias, getAliasesForSession, cleanupAliases, atomic write Also includes hook-generated improvements: - utils.d.ts: document that readStdinJson never rejects - session-aliases.d.ts: fix updateAliasTitle type to accept null - package-manager.js: add try-catch to setProjectPackageManager writeFile
This commit is contained in:
@@ -17,7 +17,7 @@ function extractFrontmatter(content) {
|
||||
if (!match) return null;
|
||||
|
||||
const frontmatter = {};
|
||||
const lines = match[1].split('\n');
|
||||
const lines = match[1].split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
const colonIdx = line.indexOf(':');
|
||||
if (colonIdx > 0) {
|
||||
|
||||
@@ -74,7 +74,7 @@ function validateHooks() {
|
||||
const nodeEMatch = hook.command.match(/^node -e "(.*)"$/s);
|
||||
if (nodeEMatch) {
|
||||
try {
|
||||
new vm.Script(nodeEMatch[1].replace(/\\"/g, '"').replace(/\\n/g, '\n'));
|
||||
new vm.Script(nodeEMatch[1].replace(/\\"/g, '"').replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\\\/g, '\\'));
|
||||
} catch (syntaxErr) {
|
||||
console.error(`ERROR: ${eventType}[${i}].hooks[${j}] has invalid inline JS: ${syntaxErr.message}`);
|
||||
hasErrors = true;
|
||||
@@ -113,7 +113,7 @@ function validateHooks() {
|
||||
const nodeEMatch = h.command.match(/^node -e "(.*)"$/s);
|
||||
if (nodeEMatch) {
|
||||
try {
|
||||
new vm.Script(nodeEMatch[1].replace(/\\"/g, '"').replace(/\\n/g, '\n'));
|
||||
new vm.Script(nodeEMatch[1].replace(/\\"/g, '"').replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\\\/g, '\\'));
|
||||
} catch (syntaxErr) {
|
||||
console.error(`ERROR: Hook ${i}.hooks[${j}] has invalid inline JS: ${syntaxErr.message}`);
|
||||
hasErrors = true;
|
||||
|
||||
@@ -267,7 +267,11 @@ function setProjectPackageManager(pmName, projectDir = process.cwd()) {
|
||||
setAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
writeFile(configPath, JSON.stringify(config, null, 2));
|
||||
try {
|
||||
writeFile(configPath, JSON.stringify(config, null, 2));
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to save package manager config to ${configPath}: ${err.message}`);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
4
scripts/lib/session-aliases.d.ts
vendored
4
scripts/lib/session-aliases.d.ts
vendored
@@ -123,8 +123,8 @@ export function renameAlias(oldAlias: string, newAlias: string): RenameAliasResu
|
||||
*/
|
||||
export function resolveSessionAlias(aliasOrId: string): string;
|
||||
|
||||
/** Update the title of an existing alias */
|
||||
export function updateAliasTitle(alias: string, title: string): AliasResult;
|
||||
/** Update the title of an existing alias. Pass null to clear. */
|
||||
export function updateAliasTitle(alias: string, title: string | null): AliasResult;
|
||||
|
||||
/** Get all aliases that point to a specific session path */
|
||||
export function getAliasesForSession(sessionPath: string): Array<{ name: string; createdAt: string; title: string | null }>;
|
||||
|
||||
@@ -252,7 +252,7 @@ function listAliases(options = {}) {
|
||||
}));
|
||||
|
||||
// Sort by updated time (newest first)
|
||||
aliases.sort((a, b) => new Date(b.updatedAt || b.createdAt) - new Date(a.updatedAt || a.createdAt));
|
||||
aliases.sort((a, b) => (new Date(b.updatedAt || b.createdAt || 0).getTime() || 0) - (new Date(a.updatedAt || a.createdAt || 0).getTime() || 0));
|
||||
|
||||
// Apply search filter
|
||||
if (search) {
|
||||
@@ -337,7 +337,9 @@ function renameAlias(oldAlias, newAlias) {
|
||||
// Restore old alias and remove new alias on failure
|
||||
data.aliases[oldAlias] = aliasData;
|
||||
delete data.aliases[newAlias];
|
||||
return { success: false, error: 'Failed to rename alias' };
|
||||
// Attempt to persist the rollback
|
||||
saveAliases(data);
|
||||
return { success: false, error: 'Failed to save renamed alias — rolled back to original' };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -359,17 +361,21 @@ function resolveSessionAlias(aliasOrId) {
|
||||
/**
|
||||
* Update alias title
|
||||
* @param {string} alias - Alias name
|
||||
* @param {string} title - New title
|
||||
* @param {string|null} title - New title (string or null to clear)
|
||||
* @returns {object} Result with success status
|
||||
*/
|
||||
function updateAliasTitle(alias, title) {
|
||||
if (title !== null && typeof title !== 'string') {
|
||||
return { success: false, error: 'Title must be a string or null' };
|
||||
}
|
||||
|
||||
const data = loadAliases();
|
||||
|
||||
if (!data.aliases[alias]) {
|
||||
return { success: false, error: `Alias '${alias}' not found` };
|
||||
}
|
||||
|
||||
data.aliases[alias].title = title;
|
||||
data.aliases[alias].title = title || null;
|
||||
data.aliases[alias].updatedAt = new Date().toISOString();
|
||||
|
||||
if (saveAliases(data)) {
|
||||
|
||||
@@ -150,6 +150,7 @@ function getSessionStats(sessionPathOrContent) {
|
||||
// read from disk. Otherwise treat it as content.
|
||||
const content = (typeof sessionPathOrContent === 'string' &&
|
||||
!sessionPathOrContent.includes('\n') &&
|
||||
sessionPathOrContent.startsWith('/') &&
|
||||
sessionPathOrContent.endsWith('.tmp'))
|
||||
? getSessionContent(sessionPathOrContent)
|
||||
: sessionPathOrContent;
|
||||
|
||||
3
scripts/lib/utils.d.ts
vendored
3
scripts/lib/utils.d.ts
vendored
@@ -132,7 +132,8 @@ export interface ReadStdinJsonOptions {
|
||||
|
||||
/**
|
||||
* Read JSON from stdin (for hook input).
|
||||
* Returns an empty object if stdin is empty or times out.
|
||||
* Returns an empty object if stdin is empty, times out, or contains invalid JSON.
|
||||
* Never rejects — safe to use without try-catch in hooks.
|
||||
*/
|
||||
export function readStdinJson(options?: ReadStdinJsonOptions): Promise<Record<string, unknown>>;
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ function findFiles(dir, pattern, options = {}) {
|
||||
async function readStdinJson(options = {}) {
|
||||
const { timeoutMs = 5000, maxSize = 1024 * 1024 } = options;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve) => {
|
||||
let data = '';
|
||||
let settled = false;
|
||||
|
||||
@@ -235,16 +235,19 @@ async function readStdinJson(options = {}) {
|
||||
clearTimeout(timer);
|
||||
try {
|
||||
resolve(data.trim() ? JSON.parse(data) : {});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
} catch {
|
||||
// Consistent with timeout path: resolve with empty object
|
||||
// so hooks don't crash on malformed input
|
||||
resolve({});
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('error', err => {
|
||||
process.stdin.on('error', () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
reject(err);
|
||||
// Resolve with empty object so hooks don't crash on stdin errors
|
||||
resolve({});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -414,7 +417,7 @@ function countInFile(filePath, pattern) {
|
||||
try {
|
||||
if (pattern instanceof RegExp) {
|
||||
// Ensure global flag is set for correct counting
|
||||
regex = pattern.global ? pattern : new RegExp(pattern.source, pattern.flags + 'g');
|
||||
regex = pattern.global ? pattern : new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : pattern.flags + 'g');
|
||||
} else if (typeof pattern === 'string') {
|
||||
regex = new RegExp(pattern, 'g');
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user