murmur8 4.1.1 → 4.3.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/.blueprint/agents/AGENT_SPECIFICATION_ALEX.md +33 -3
- package/.blueprint/features/feature_config-factory/FEATURE_SPEC.md +138 -0
- package/.blueprint/features/feature_config-factory/IMPLEMENTATION_PLAN.md +187 -0
- package/.blueprint/features/feature_config-factory/handoff-nigel.md +57 -0
- package/.blueprint/features/feature_export-history/FEATURE_SPEC.md +215 -0
- package/.blueprint/features/feature_export-history/IMPLEMENTATION_PLAN.md +48 -0
- package/.blueprint/features/feature_export-history/story-basic-export.md +48 -0
- package/.blueprint/features/feature_export-history/story-date-filter.md +42 -0
- package/.blueprint/features/feature_export-history/story-feature-filter.md +42 -0
- package/.blueprint/features/feature_export-history/story-file-output.md +48 -0
- package/.blueprint/features/feature_export-history/story-status-filter.md +42 -0
- package/.blueprint/features/feature_extract-prompt-util/FEATURE_SPEC.md +42 -0
- package/.blueprint/features/feature_fix-status-icons/FEATURE_SPEC.md +37 -0
- package/.blueprint/features/feature_murm-subagent/FEATURE_SPEC.md +137 -0
- package/.blueprint/features/feature_murm-subagent/SKILL_CHANGES.md +345 -0
- package/.blueprint/features/feature_split-cli-commands/FEATURE_SPEC.md +125 -0
- package/.blueprint/features/feature_split-cli-commands/IMPLEMENTATION_PLAN.md +119 -0
- package/.blueprint/features/feature_split-cli-commands/handoff-nigel.md +45 -0
- package/.blueprint/features/feature_theme-adoption/FEATURE_SPEC.md +143 -0
- package/.blueprint/features/feature_theme-adoption/IMPLEMENTATION_PLAN.md +68 -0
- package/.blueprint/features/feature_theme-adoption/handoff-nigel.md +35 -0
- package/.blueprint/templates/BACKLOG_TEMPLATE.md +46 -0
- package/README.md +26 -10
- package/SKILL.md +377 -3
- package/bin/cli.js +20 -384
- package/package.json +1 -1
- package/src/commands/feedback-config.js +32 -0
- package/src/commands/help.js +81 -0
- package/src/commands/history.js +42 -0
- package/src/commands/init.js +12 -0
- package/src/commands/insights.js +23 -0
- package/src/commands/murm-config.js +52 -0
- package/src/commands/murm.js +109 -0
- package/src/commands/queue.js +19 -0
- package/src/commands/retry-config.js +28 -0
- package/src/commands/stack-config.js +32 -0
- package/src/commands/update.js +12 -0
- package/src/commands/utils.js +24 -0
- package/src/commands/validate.js +15 -0
- package/src/config-factory.js +190 -0
- package/src/feedback.js +5 -2
- package/src/history.js +92 -1
- package/src/init.js +1 -15
- package/src/insights.js +19 -16
- package/src/retry.js +5 -2
- package/src/stack.js +4 -1
- package/src/theme.js +4 -4
- package/src/update.js +2 -15
- package/src/utils.js +26 -0
- package/src/validate.js +5 -12
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* murm command - Run multiple feature pipelines in parallel using git worktrees
|
|
3
|
+
*/
|
|
4
|
+
const {
|
|
5
|
+
formatStatus,
|
|
6
|
+
runMurm,
|
|
7
|
+
loadQueue,
|
|
8
|
+
cleanupWorktrees,
|
|
9
|
+
abortMurm,
|
|
10
|
+
getLockInfo,
|
|
11
|
+
getDetailedStatus,
|
|
12
|
+
formatDetailedStatus,
|
|
13
|
+
rollbackMurm
|
|
14
|
+
} = require('../murm');
|
|
15
|
+
|
|
16
|
+
const description = 'Run multiple feature pipelines in parallel using git worktrees';
|
|
17
|
+
const aliases = ['parallel', 'murmuration'];
|
|
18
|
+
|
|
19
|
+
async function run(args) {
|
|
20
|
+
const subArg = args[1];
|
|
21
|
+
|
|
22
|
+
if (subArg === 'status') {
|
|
23
|
+
const detailed = args.includes('--detailed') || args.includes('-d');
|
|
24
|
+
const lock = getLockInfo();
|
|
25
|
+
|
|
26
|
+
if (detailed) {
|
|
27
|
+
const details = getDetailedStatus();
|
|
28
|
+
console.log(formatDetailedStatus(details));
|
|
29
|
+
} else {
|
|
30
|
+
const queue = loadQueue();
|
|
31
|
+
|
|
32
|
+
if (!queue.features || queue.features.length === 0) {
|
|
33
|
+
if (lock) {
|
|
34
|
+
console.log(`Murmuration execution in progress (PID: ${lock.pid})`);
|
|
35
|
+
console.log(`Started: ${lock.startedAt}`);
|
|
36
|
+
console.log(`Features: ${lock.features.join(', ')}`);
|
|
37
|
+
} else {
|
|
38
|
+
console.log('No murmuration pipelines active.');
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log('Murmuration Pipeline Status\n');
|
|
44
|
+
console.log(formatStatus(queue.features));
|
|
45
|
+
const summary = {
|
|
46
|
+
running: queue.features.filter(f => f.status === 'murm_running').length,
|
|
47
|
+
pending: queue.features.filter(f => f.status === 'murm_queued').length,
|
|
48
|
+
completed: queue.features.filter(f => f.status === 'murm_complete').length,
|
|
49
|
+
failed: queue.features.filter(f => f.status === 'murm_failed').length,
|
|
50
|
+
conflicts: queue.features.filter(f => f.status === 'merge_conflict').length
|
|
51
|
+
};
|
|
52
|
+
console.log(`\nRunning: ${summary.running} | Pending: ${summary.pending} | Completed: ${summary.completed} | Failed: ${summary.failed} | Conflicts: ${summary.conflicts}`);
|
|
53
|
+
|
|
54
|
+
// Show log paths for running/failed
|
|
55
|
+
const withLogs = queue.features.filter(f =>
|
|
56
|
+
f.logPath && (f.status === 'murm_running' || f.status === 'murm_failed')
|
|
57
|
+
);
|
|
58
|
+
if (withLogs.length > 0) {
|
|
59
|
+
console.log('\nLog files:');
|
|
60
|
+
withLogs.forEach(f => console.log(` ${f.slug}: ${f.logPath}`));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log('\nTip: Use --detailed for progress bars');
|
|
64
|
+
}
|
|
65
|
+
} else if (subArg === 'rollback') {
|
|
66
|
+
const dryRunFlag = args.includes('--dry-run');
|
|
67
|
+
const forceFlag = args.includes('--force');
|
|
68
|
+
await rollbackMurm({ dryRun: dryRunFlag, force: forceFlag });
|
|
69
|
+
} else if (subArg === 'cleanup') {
|
|
70
|
+
const cleaned = await cleanupWorktrees();
|
|
71
|
+
console.log(`Cleaned ${cleaned} worktree(s).`);
|
|
72
|
+
} else if (subArg === 'abort') {
|
|
73
|
+
const cleanupFlag = args.includes('--cleanup');
|
|
74
|
+
await abortMurm({ cleanup: cleanupFlag });
|
|
75
|
+
} else {
|
|
76
|
+
const slugs = args.slice(1).filter(a => !a.startsWith('--') && !a.startsWith('-'));
|
|
77
|
+
if (slugs.length === 0) {
|
|
78
|
+
console.error('Usage: murmur8 murm <slug1> <slug2> ... [options]');
|
|
79
|
+
console.error('\nOptions:');
|
|
80
|
+
console.error(' --dry-run Preview execution plan without running');
|
|
81
|
+
console.error(' --yes, -y Skip confirmation prompt');
|
|
82
|
+
console.error(' --force Override existing lock');
|
|
83
|
+
console.error(' --verbose Stream output to console (not just logs)');
|
|
84
|
+
console.error(' --skip-preflight Skip feature validation checks');
|
|
85
|
+
console.error(' --max-concurrency=N Set max parallel pipelines (default: 3)');
|
|
86
|
+
console.error('\nSubcommands:');
|
|
87
|
+
console.error(' murm status Show status of all pipelines');
|
|
88
|
+
console.error(' murm abort Stop all running pipelines');
|
|
89
|
+
console.error(' murm cleanup Remove completed/aborted worktrees');
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const maxFlag = args.find(a => a.startsWith('--max-concurrency='));
|
|
94
|
+
const options = {
|
|
95
|
+
dryRun: args.includes('--dry-run'),
|
|
96
|
+
yes: args.includes('--yes') || args.includes('-y'),
|
|
97
|
+
force: args.includes('--force'),
|
|
98
|
+
verbose: args.includes('--verbose'),
|
|
99
|
+
skipPreflight: args.includes('--skip-preflight')
|
|
100
|
+
};
|
|
101
|
+
if (maxFlag) {
|
|
102
|
+
options.maxConcurrency = parseInt(maxFlag.split('=')[1], 10);
|
|
103
|
+
}
|
|
104
|
+
const result = await runMurm(slugs, options);
|
|
105
|
+
process.exit(result.success ? 0 : 1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { run, description, aliases };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* queue command - Show queue status or reset
|
|
3
|
+
*/
|
|
4
|
+
const { displayQueue, resetQueue } = require('../orchestrator');
|
|
5
|
+
|
|
6
|
+
const description = 'Show queue status (use "reset" to clear)';
|
|
7
|
+
|
|
8
|
+
async function run(args) {
|
|
9
|
+
const subArg = args[1];
|
|
10
|
+
|
|
11
|
+
if (subArg === 'reset') {
|
|
12
|
+
resetQueue();
|
|
13
|
+
console.log('Queue has been reset.');
|
|
14
|
+
} else {
|
|
15
|
+
displayQueue();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { run, description };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* retry-config command - Manage retry configuration for adaptive retry logic
|
|
3
|
+
*/
|
|
4
|
+
const { displayConfig, setConfigValue, resetConfig } = require('../retry');
|
|
5
|
+
|
|
6
|
+
const description = 'Manage retry configuration for adaptive retry logic';
|
|
7
|
+
|
|
8
|
+
async function run(args) {
|
|
9
|
+
const subArg = args[1];
|
|
10
|
+
|
|
11
|
+
if (subArg === 'set') {
|
|
12
|
+
const key = args[2];
|
|
13
|
+
const value = args[3];
|
|
14
|
+
if (!key || !value) {
|
|
15
|
+
console.error('Usage: retry-config set <key> <value>');
|
|
16
|
+
console.error('Valid keys: maxRetries, windowSize, highFailureThreshold');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
setConfigValue(key, value);
|
|
20
|
+
} else if (subArg === 'reset') {
|
|
21
|
+
resetConfig();
|
|
22
|
+
console.log('Retry configuration reset to defaults.');
|
|
23
|
+
} else {
|
|
24
|
+
displayConfig();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { run, description };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stack-config command - View or modify project tech stack configuration
|
|
3
|
+
*/
|
|
4
|
+
const {
|
|
5
|
+
displayStackConfig,
|
|
6
|
+
setStackConfigValue,
|
|
7
|
+
resetStackConfig
|
|
8
|
+
} = require('../stack');
|
|
9
|
+
|
|
10
|
+
const description = 'View or modify project tech stack configuration';
|
|
11
|
+
|
|
12
|
+
async function run(args) {
|
|
13
|
+
const subArg = args[1];
|
|
14
|
+
|
|
15
|
+
if (subArg === 'set') {
|
|
16
|
+
const key = args[2];
|
|
17
|
+
const value = args[3];
|
|
18
|
+
if (!key || !value) {
|
|
19
|
+
console.error('Usage: stack-config set <key> <value>');
|
|
20
|
+
console.error('Valid keys: language, runtime, packageManager, frameworks, testRunner, testCommand, linter, tools');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
setStackConfigValue(key, value);
|
|
24
|
+
} else if (subArg === 'reset') {
|
|
25
|
+
resetStackConfig();
|
|
26
|
+
console.log('Stack configuration reset to defaults.');
|
|
27
|
+
} else {
|
|
28
|
+
displayStackConfig();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { run, description };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* update command - Update agents, templates, and rituals (preserves your content)
|
|
3
|
+
*/
|
|
4
|
+
const { update } = require('../update');
|
|
5
|
+
|
|
6
|
+
const description = 'Update agents, templates, and rituals (preserves your content)';
|
|
7
|
+
|
|
8
|
+
async function run(args) {
|
|
9
|
+
await update();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = { run, description };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for CLI commands
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse common flags from command line arguments
|
|
7
|
+
* @param {string[]} args - Command line arguments
|
|
8
|
+
* @returns {Object} Parsed flags
|
|
9
|
+
*/
|
|
10
|
+
function parseFlags(args) {
|
|
11
|
+
const flags = {};
|
|
12
|
+
for (const arg of args) {
|
|
13
|
+
if (arg === '--all') flags.all = true;
|
|
14
|
+
if (arg === '--stats') flags.stats = true;
|
|
15
|
+
if (arg === '--force') flags.force = true;
|
|
16
|
+
if (arg === '--bottlenecks') flags.bottlenecks = true;
|
|
17
|
+
if (arg === '--failures') flags.failures = true;
|
|
18
|
+
if (arg === '--json') flags.json = true;
|
|
19
|
+
if (arg === '--feedback') flags.feedback = true;
|
|
20
|
+
}
|
|
21
|
+
return flags;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { parseFlags };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* validate command - Run pre-flight checks to validate project configuration
|
|
3
|
+
*/
|
|
4
|
+
const { validate, formatOutput } = require('../validate');
|
|
5
|
+
|
|
6
|
+
const description = 'Run pre-flight checks to validate project configuration';
|
|
7
|
+
|
|
8
|
+
async function run(args) {
|
|
9
|
+
const result = await validate();
|
|
10
|
+
const useColor = process.stdout.isTTY || false;
|
|
11
|
+
console.log(formatOutput(result, useColor));
|
|
12
|
+
process.exit(result.exitCode);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { run, description };
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Factory function to create a standardized config module.
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} options - Configuration options
|
|
10
|
+
* @param {string} options.name - Display name for the config (e.g., 'Retry', 'Feedback')
|
|
11
|
+
* @param {string} options.file - Path to config file (e.g., '.claude/retry-config.json')
|
|
12
|
+
* @param {Object} options.defaults - Default configuration values
|
|
13
|
+
* @param {Object} [options.validators] - Map of key -> validator function
|
|
14
|
+
* Validator returns true for valid, or error string for invalid
|
|
15
|
+
* @param {Object} [options.formatters] - Map of key -> display formatter function
|
|
16
|
+
* @param {string[]} [options.arrayKeys] - Keys that accept JSON array values
|
|
17
|
+
* @returns {Object} Config module with standard methods
|
|
18
|
+
*/
|
|
19
|
+
function createConfigModule(options) {
|
|
20
|
+
const {
|
|
21
|
+
name,
|
|
22
|
+
file,
|
|
23
|
+
defaults,
|
|
24
|
+
validators = {},
|
|
25
|
+
formatters = {},
|
|
26
|
+
arrayKeys = []
|
|
27
|
+
} = options;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Ensures the config directory exists.
|
|
31
|
+
*/
|
|
32
|
+
function ensureConfigDir() {
|
|
33
|
+
const dir = path.dirname(file);
|
|
34
|
+
if (!fs.existsSync(dir)) {
|
|
35
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns a copy of the default configuration.
|
|
41
|
+
*/
|
|
42
|
+
function getDefault() {
|
|
43
|
+
return JSON.parse(JSON.stringify(defaults));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Reads the config from file.
|
|
48
|
+
* Returns defaults if file is missing or corrupted.
|
|
49
|
+
* Merges missing keys from defaults.
|
|
50
|
+
*/
|
|
51
|
+
function read() {
|
|
52
|
+
ensureConfigDir();
|
|
53
|
+
if (!fs.existsSync(file)) {
|
|
54
|
+
return getDefault();
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
58
|
+
const parsed = JSON.parse(content);
|
|
59
|
+
// Merge missing keys from defaults
|
|
60
|
+
const merged = { ...getDefault(), ...parsed };
|
|
61
|
+
return merged;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
// Graceful degradation: return defaults on parse error
|
|
64
|
+
return getDefault();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Writes the config to file.
|
|
70
|
+
* Creates directory if needed.
|
|
71
|
+
*/
|
|
72
|
+
function write(config) {
|
|
73
|
+
ensureConfigDir();
|
|
74
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resets config to defaults by writing default config to file.
|
|
79
|
+
*/
|
|
80
|
+
function reset() {
|
|
81
|
+
write(getDefault());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Sets a config value by key.
|
|
86
|
+
* @param {string} key - Config key
|
|
87
|
+
* @param {string} value - New value (will be parsed appropriately)
|
|
88
|
+
*/
|
|
89
|
+
function setValue(key, value) {
|
|
90
|
+
const validKeys = Object.keys(defaults);
|
|
91
|
+
|
|
92
|
+
if (!validKeys.includes(key)) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Unknown config key: ${key}. Valid keys: ${validKeys.join(', ')}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let parsed = value;
|
|
99
|
+
|
|
100
|
+
// Handle array keys
|
|
101
|
+
if (arrayKeys.includes(key)) {
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(value);
|
|
104
|
+
if (!Array.isArray(parsed)) {
|
|
105
|
+
throw new Error(`${key} must be a JSON array, e.g. '["a","b"]'`);
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (err.message.includes('must be a JSON array')) throw err;
|
|
109
|
+
throw new Error(`${key} must be a valid JSON array, e.g. '["a","b"]'`);
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
// Type coercion based on default value type
|
|
113
|
+
const defaultType = typeof defaults[key];
|
|
114
|
+
if (defaultType === 'number') {
|
|
115
|
+
parsed = parseFloat(value);
|
|
116
|
+
if (isNaN(parsed)) {
|
|
117
|
+
throw new Error(`Invalid value for ${key}: ${value}. Must be a number.`);
|
|
118
|
+
}
|
|
119
|
+
} else if (defaultType === 'boolean') {
|
|
120
|
+
if (value !== 'true' && value !== 'false') {
|
|
121
|
+
throw new Error(`Invalid value for ${key}: ${value}. Must be true or false.`);
|
|
122
|
+
}
|
|
123
|
+
parsed = value === 'true';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Run custom validator if provided
|
|
128
|
+
if (validators[key]) {
|
|
129
|
+
const result = validators[key](parsed);
|
|
130
|
+
if (result !== true) {
|
|
131
|
+
throw new Error(`Invalid value for ${key}: ${value}. ${result}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const config = read();
|
|
136
|
+
config[key] = parsed;
|
|
137
|
+
write(config);
|
|
138
|
+
|
|
139
|
+
// Log confirmation
|
|
140
|
+
const display = Array.isArray(config[key]) ? JSON.stringify(config[key]) : config[key];
|
|
141
|
+
console.log(`Set ${key} = ${display}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Displays the current configuration.
|
|
146
|
+
*/
|
|
147
|
+
function display() {
|
|
148
|
+
const config = read();
|
|
149
|
+
console.log(`\n${name} Configuration\n`);
|
|
150
|
+
|
|
151
|
+
for (const [key, value] of Object.entries(config)) {
|
|
152
|
+
let displayValue;
|
|
153
|
+
|
|
154
|
+
// Use custom formatter if available
|
|
155
|
+
if (formatters[key]) {
|
|
156
|
+
displayValue = formatters[key](value);
|
|
157
|
+
} else if (Array.isArray(value)) {
|
|
158
|
+
displayValue = value.length > 0 ? value.join(', ') : '(not set)';
|
|
159
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
160
|
+
// For nested objects, format each entry
|
|
161
|
+
console.log(`\n ${key}:`);
|
|
162
|
+
for (const [subKey, subValue] of Object.entries(value)) {
|
|
163
|
+
const subDisplay = Array.isArray(subValue) ? subValue.join(' -> ') : subValue;
|
|
164
|
+
console.log(` ${subKey.padEnd(20)}: ${subDisplay}`);
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
} else if (value === '' || value === null || value === undefined) {
|
|
168
|
+
displayValue = '(not set)';
|
|
169
|
+
} else {
|
|
170
|
+
displayValue = String(value);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
console.log(` ${key.padEnd(20)}: ${displayValue}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log('');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
CONFIG_FILE: file,
|
|
181
|
+
getDefault,
|
|
182
|
+
read,
|
|
183
|
+
write,
|
|
184
|
+
reset,
|
|
185
|
+
setValue,
|
|
186
|
+
display
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = { createConfigModule };
|
package/src/feedback.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const { colorize } = require('./theme');
|
|
3
4
|
|
|
4
5
|
const CONFIG_FILE = '.claude/feedback-config.json';
|
|
5
6
|
|
|
@@ -169,10 +170,12 @@ function setConfigValue(key, value) {
|
|
|
169
170
|
*/
|
|
170
171
|
function displayConfig() {
|
|
171
172
|
const config = readConfig();
|
|
172
|
-
|
|
173
|
+
const useColor = process.stdout.isTTY;
|
|
174
|
+
|
|
175
|
+
console.log('\n' + colorize('Feedback Configuration', 'cyan', useColor) + '\n');
|
|
173
176
|
console.log(` Min rating threshold: ${config.minRatingThreshold}`);
|
|
174
177
|
console.log(` Enabled: ${config.enabled}`);
|
|
175
|
-
console.log('\n Issue Mappings:');
|
|
178
|
+
console.log('\n ' + colorize('Issue Mappings:', 'cyan', useColor));
|
|
176
179
|
for (const [issue, strategy] of Object.entries(config.issueMappings)) {
|
|
177
180
|
console.log(` ${issue.padEnd(24)}: ${strategy}`);
|
|
178
181
|
}
|
package/src/history.js
CHANGED
|
@@ -100,6 +100,96 @@ function formatDuration(ms) {
|
|
|
100
100
|
return `${minutes}m ${secs}s`;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
function isValidDateFormat(dateStr) {
|
|
104
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return false;
|
|
105
|
+
const date = new Date(dateStr);
|
|
106
|
+
return !isNaN(date.getTime());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function escapeCSVField(value) {
|
|
110
|
+
if (value === null || value === undefined) return '';
|
|
111
|
+
const str = String(value);
|
|
112
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
113
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
114
|
+
}
|
|
115
|
+
return str;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function formatCSV(entries) {
|
|
119
|
+
const headers = ['slug', 'status', 'startedAt', 'completedAt', 'totalDurationMs', 'failedStage', 'pausedAfter'];
|
|
120
|
+
const lines = [headers.join(',')];
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
const row = headers.map(h => escapeCSVField(entry[h]));
|
|
123
|
+
lines.push(row.join(','));
|
|
124
|
+
}
|
|
125
|
+
return lines.join('\n');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function formatJSON(entries) {
|
|
129
|
+
return JSON.stringify(entries, null, 2);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function exportHistory(options = {}) {
|
|
133
|
+
const { format = 'csv', since, until, status, feature, output } = options;
|
|
134
|
+
|
|
135
|
+
const history = readHistoryFile();
|
|
136
|
+
|
|
137
|
+
if (history.error === 'corrupted') {
|
|
138
|
+
return { exitCode: 1, error: "History file is corrupted. Run 'murmur8 history clear' to reset." };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (since && !isValidDateFormat(since)) {
|
|
142
|
+
return { exitCode: 1, error: 'Invalid --since format. Use YYYY-MM-DD.' };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (until && !isValidDateFormat(until)) {
|
|
146
|
+
return { exitCode: 1, error: 'Invalid --until format. Use YYYY-MM-DD.' };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const validStatuses = ['success', 'failed', 'paused'];
|
|
150
|
+
if (status && !validStatuses.includes(status)) {
|
|
151
|
+
return { exitCode: 1, error: `Invalid --status value. Use: success, failed, paused.` };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let filtered = history;
|
|
155
|
+
|
|
156
|
+
if (since) {
|
|
157
|
+
const sinceDate = new Date(since);
|
|
158
|
+
filtered = filtered.filter(e => e.completedAt && new Date(e.completedAt) >= sinceDate);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (until) {
|
|
162
|
+
const untilDate = new Date(until);
|
|
163
|
+
untilDate.setDate(untilDate.getDate() + 1);
|
|
164
|
+
filtered = filtered.filter(e => e.completedAt && new Date(e.completedAt) < untilDate);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (status) {
|
|
168
|
+
filtered = filtered.filter(e => e.status === status);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (feature) {
|
|
172
|
+
filtered = filtered.filter(e => e.slug === feature);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const formatted = format === 'json' ? formatJSON(filtered) : formatCSV(filtered);
|
|
176
|
+
|
|
177
|
+
if (output) {
|
|
178
|
+
try {
|
|
179
|
+
const dir = path.dirname(output);
|
|
180
|
+
if (!fs.existsSync(dir)) {
|
|
181
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
182
|
+
}
|
|
183
|
+
fs.writeFileSync(output, formatted);
|
|
184
|
+
return { message: `Exported ${filtered.length} entries to ${output}` };
|
|
185
|
+
} catch (err) {
|
|
186
|
+
return { exitCode: 1, error: err.message };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { output: formatted };
|
|
191
|
+
}
|
|
192
|
+
|
|
103
193
|
function formatDate(isoString) {
|
|
104
194
|
const date = new Date(isoString);
|
|
105
195
|
return date.toISOString().replace('T', ' ').slice(0, 19);
|
|
@@ -292,5 +382,6 @@ module.exports = {
|
|
|
292
382
|
displayHistory,
|
|
293
383
|
showStats,
|
|
294
384
|
clearHistory,
|
|
295
|
-
formatDuration
|
|
385
|
+
formatDuration,
|
|
386
|
+
exportHistory
|
|
296
387
|
};
|
package/src/init.js
CHANGED
|
@@ -1,26 +1,12 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const readline = require('readline');
|
|
4
3
|
|
|
5
4
|
const { detectStackConfig, writeStackConfig, CONFIG_FILE: STACK_CONFIG_FILE } = require('./stack');
|
|
5
|
+
const { prompt } = require('./utils');
|
|
6
6
|
|
|
7
7
|
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
8
8
|
const TARGET_DIR = process.cwd();
|
|
9
9
|
|
|
10
|
-
async function prompt(question) {
|
|
11
|
-
const rl = readline.createInterface({
|
|
12
|
-
input: process.stdin,
|
|
13
|
-
output: process.stdout
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
return new Promise((resolve) => {
|
|
17
|
-
rl.question(question, (answer) => {
|
|
18
|
-
rl.close();
|
|
19
|
-
resolve(answer.toLowerCase().trim());
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
10
|
function copyDir(src, dest) {
|
|
25
11
|
fs.mkdirSync(dest, { recursive: true });
|
|
26
12
|
|