pan-wizard 3.14.0 → 3.15.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.
@@ -1,171 +1,171 @@
1
- /**
2
- * Utils — Shared utility functions used across multiple modules
3
- *
4
- * Functions here were previously duplicated in core.cjs, commands.cjs,
5
- * state.cjs, and phase.cjs. Now centralized for single-source-of-truth.
6
- */
7
-
8
- const fs = require('fs');
9
- const os = require('os');
10
- const path = require('path');
11
- const {
12
- PLANNING_DIR,
13
- PHASES_DIR,
14
- MILESTONES_DIR,
15
- isPlanFile,
16
- isSummaryFile,
17
- PHASE_DIR_RE,
18
- } = require('./constants.cjs');
19
-
20
- // ─── File utilities ──────────────────────────────────────────────────────────
21
-
22
- /**
23
- * Read and parse a JSON file, returning null on any failure.
24
- * @param {string} filePath - Absolute path to the JSON file
25
- * @returns {Object|null} Parsed JSON object, or null if unreadable/unparseable
26
- */
27
- function readJsonFile(filePath) {
28
- try {
29
- return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
30
- } catch {
31
- return null;
32
- }
33
- }
34
-
35
- /**
36
- * Remove surrounding quotes (single or double) from a string.
37
- * @param {string} str - Input string possibly wrapped in quotes
38
- * @returns {string} String with leading/trailing quotes removed
39
- */
40
- function removeQuotes(str) {
41
- return str.replace(/^["']|["']$/g, '');
42
- }
43
-
44
- // ─── Phase directory utilities ───────────────────────────────────────────────
45
-
46
- /**
47
- * Build the absolute path to the .planning directory.
48
- * @param {string} cwd - Project root directory
49
- * @returns {string} Absolute path to .planning/
50
- */
51
- function planningPath(cwd) {
52
- return path.join(cwd, PLANNING_DIR);
53
- }
54
-
55
- /**
56
- * Build the absolute path to the phases directory.
57
- * @param {string} cwd - Project root directory
58
- * @returns {string} Absolute path to .planning/phases/
59
- */
60
- function phasesPath(cwd) {
61
- return path.join(cwd, PLANNING_DIR, PHASES_DIR);
62
- }
63
-
64
- /**
65
- * Build the absolute path to the milestones directory.
66
- * @param {string} cwd - Project root directory
67
- * @returns {string} Absolute path to .planning/milestones/
68
- */
69
- function milestonesPath(cwd) {
70
- return path.join(cwd, PLANNING_DIR, MILESTONES_DIR);
71
- }
72
-
73
- /**
74
- * Read phase directories from the phases folder, sorted by phase number.
75
- * @param {string} cwd - Project root directory
76
- * @returns {string[]} Sorted array of directory names, or empty array on failure
77
- */
78
- function listPhaseDirs(cwd) {
79
- const { comparePhaseNum } = require('./core.cjs');
80
- try {
81
- const entries = fs.readdirSync(phasesPath(cwd), { withFileTypes: true });
82
- return entries
83
- .filter(e => e.isDirectory())
84
- .map(e => e.name)
85
- .sort((a, b) => comparePhaseNum(a, b));
86
- } catch {
87
- return [];
88
- }
89
- }
90
-
91
- /**
92
- * Filter an array of filenames to only plan files, sorted.
93
- * @param {string[]} files - Array of filenames
94
- * @returns {string[]} Sorted plan filenames
95
- */
96
- function filterPlanFiles(files) {
97
- return files.filter(isPlanFile).sort();
98
- }
99
-
100
- /**
101
- * Filter an array of filenames to only summary files, sorted.
102
- * @param {string[]} files - Array of filenames
103
- * @returns {string[]} Sorted summary filenames
104
- */
105
- function filterSummaryFiles(files) {
106
- return files.filter(isSummaryFile).sort();
107
- }
108
-
109
- /**
110
- * Extract the phase number and name from a phase directory name.
111
- * e.g. "01-setup-auth" → { number: "01", name: "setup-auth" }
112
- * @param {string} dirName - Phase directory name
113
- * @returns {{ number: string, name: string|null }} Parsed phase info
114
- */
115
- function parsePhaseDir(dirName) {
116
- const match = dirName.match(PHASE_DIR_RE);
117
- if (!match) return { number: dirName, name: null };
118
- return {
119
- number: match[1],
120
- name: match[2] || null,
121
- };
122
- }
123
-
124
- /**
125
- * Classify phase status from file counts. Returns a granular status string.
126
- * @param {number} planCount - Number of plan files
127
- * @param {number} summaryCount - Number of summary files
128
- * @param {{hasContext?: boolean, hasResearch?: boolean}} [flags] - Extra file flags
129
- * @returns {string} One of: 'complete', 'partial', 'planned', 'researched', 'discussed', 'empty'
130
- */
131
- function classifyPhaseStatus(planCount, summaryCount, flags = {}) {
132
- if (summaryCount >= planCount && planCount > 0) return 'complete';
133
- if (summaryCount > 0) return 'partial';
134
- if (planCount > 0) return 'planned';
135
- if (flags.hasResearch) return 'researched';
136
- if (flags.hasContext) return 'discussed';
137
- return 'empty';
138
- }
139
-
140
- /**
141
- * Check if a file is accessible (exists and is readable).
142
- * @param {string} filePath - Absolute path to check
143
- * @returns {boolean}
144
- */
145
- function fileAccessible(filePath) {
146
- try { fs.accessSync(filePath, fs.constants.R_OK); return true; } catch { return false; }
147
- }
148
-
149
- /**
150
- * Detect whether Brave Search API key is available (env var or key file).
151
- * @returns {boolean}
152
- */
153
- function hasBraveSearchKey() {
154
- if (process.env.BRAVE_API_KEY) return true;
155
- return fileAccessible(path.join(os.homedir(), '.pan-wizard', 'brave_api_key'));
156
- }
157
-
158
- module.exports = {
159
- readJsonFile,
160
- removeQuotes,
161
- planningPath,
162
- phasesPath,
163
- milestonesPath,
164
- listPhaseDirs,
165
- filterPlanFiles,
166
- filterSummaryFiles,
167
- parsePhaseDir,
168
- classifyPhaseStatus,
169
- fileAccessible,
170
- hasBraveSearchKey,
171
- };
1
+ /**
2
+ * Utils — Shared utility functions used across multiple modules
3
+ *
4
+ * Functions here were previously duplicated in core.cjs, commands.cjs,
5
+ * state.cjs, and phase.cjs. Now centralized for single-source-of-truth.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const os = require('os');
10
+ const path = require('path');
11
+ const {
12
+ PLANNING_DIR,
13
+ PHASES_DIR,
14
+ MILESTONES_DIR,
15
+ isPlanFile,
16
+ isSummaryFile,
17
+ PHASE_DIR_RE,
18
+ } = require('./constants.cjs');
19
+
20
+ // ─── File utilities ──────────────────────────────────────────────────────────
21
+
22
+ /**
23
+ * Read and parse a JSON file, returning null on any failure.
24
+ * @param {string} filePath - Absolute path to the JSON file
25
+ * @returns {Object|null} Parsed JSON object, or null if unreadable/unparseable
26
+ */
27
+ function readJsonFile(filePath) {
28
+ try {
29
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Remove surrounding quotes (single or double) from a string.
37
+ * @param {string} str - Input string possibly wrapped in quotes
38
+ * @returns {string} String with leading/trailing quotes removed
39
+ */
40
+ function removeQuotes(str) {
41
+ return str.replace(/^["']|["']$/g, '');
42
+ }
43
+
44
+ // ─── Phase directory utilities ───────────────────────────────────────────────
45
+
46
+ /**
47
+ * Build the absolute path to the .planning directory.
48
+ * @param {string} cwd - Project root directory
49
+ * @returns {string} Absolute path to .planning/
50
+ */
51
+ function planningPath(cwd) {
52
+ return path.join(cwd, PLANNING_DIR);
53
+ }
54
+
55
+ /**
56
+ * Build the absolute path to the phases directory.
57
+ * @param {string} cwd - Project root directory
58
+ * @returns {string} Absolute path to .planning/phases/
59
+ */
60
+ function phasesPath(cwd) {
61
+ return path.join(cwd, PLANNING_DIR, PHASES_DIR);
62
+ }
63
+
64
+ /**
65
+ * Build the absolute path to the milestones directory.
66
+ * @param {string} cwd - Project root directory
67
+ * @returns {string} Absolute path to .planning/milestones/
68
+ */
69
+ function milestonesPath(cwd) {
70
+ return path.join(cwd, PLANNING_DIR, MILESTONES_DIR);
71
+ }
72
+
73
+ /**
74
+ * Read phase directories from the phases folder, sorted by phase number.
75
+ * @param {string} cwd - Project root directory
76
+ * @returns {string[]} Sorted array of directory names, or empty array on failure
77
+ */
78
+ function listPhaseDirs(cwd) {
79
+ const { comparePhaseNum } = require('./core.cjs');
80
+ try {
81
+ const entries = fs.readdirSync(phasesPath(cwd), { withFileTypes: true });
82
+ return entries
83
+ .filter(e => e.isDirectory())
84
+ .map(e => e.name)
85
+ .sort((a, b) => comparePhaseNum(a, b));
86
+ } catch {
87
+ return [];
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Filter an array of filenames to only plan files, sorted.
93
+ * @param {string[]} files - Array of filenames
94
+ * @returns {string[]} Sorted plan filenames
95
+ */
96
+ function filterPlanFiles(files) {
97
+ return files.filter(isPlanFile).sort();
98
+ }
99
+
100
+ /**
101
+ * Filter an array of filenames to only summary files, sorted.
102
+ * @param {string[]} files - Array of filenames
103
+ * @returns {string[]} Sorted summary filenames
104
+ */
105
+ function filterSummaryFiles(files) {
106
+ return files.filter(isSummaryFile).sort();
107
+ }
108
+
109
+ /**
110
+ * Extract the phase number and name from a phase directory name.
111
+ * e.g. "01-setup-auth" → { number: "01", name: "setup-auth" }
112
+ * @param {string} dirName - Phase directory name
113
+ * @returns {{ number: string, name: string|null }} Parsed phase info
114
+ */
115
+ function parsePhaseDir(dirName) {
116
+ const match = dirName.match(PHASE_DIR_RE);
117
+ if (!match) return { number: dirName, name: null };
118
+ return {
119
+ number: match[1],
120
+ name: match[2] || null,
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Classify phase status from file counts. Returns a granular status string.
126
+ * @param {number} planCount - Number of plan files
127
+ * @param {number} summaryCount - Number of summary files
128
+ * @param {{hasContext?: boolean, hasResearch?: boolean}} [flags] - Extra file flags
129
+ * @returns {string} One of: 'complete', 'partial', 'planned', 'researched', 'discussed', 'empty'
130
+ */
131
+ function classifyPhaseStatus(planCount, summaryCount, flags = {}) {
132
+ if (summaryCount >= planCount && planCount > 0) return 'complete';
133
+ if (summaryCount > 0) return 'partial';
134
+ if (planCount > 0) return 'planned';
135
+ if (flags.hasResearch) return 'researched';
136
+ if (flags.hasContext) return 'discussed';
137
+ return 'empty';
138
+ }
139
+
140
+ /**
141
+ * Check if a file is accessible (exists and is readable).
142
+ * @param {string} filePath - Absolute path to check
143
+ * @returns {boolean}
144
+ */
145
+ function fileAccessible(filePath) {
146
+ try { fs.accessSync(filePath, fs.constants.R_OK); return true; } catch { return false; }
147
+ }
148
+
149
+ /**
150
+ * Detect whether Brave Search API key is available (env var or key file).
151
+ * @returns {boolean}
152
+ */
153
+ function hasBraveSearchKey() {
154
+ if (process.env.BRAVE_API_KEY) return true;
155
+ return fileAccessible(path.join(os.homedir(), '.pan-wizard', 'brave_api_key'));
156
+ }
157
+
158
+ module.exports = {
159
+ readJsonFile,
160
+ removeQuotes,
161
+ planningPath,
162
+ phasesPath,
163
+ milestonesPath,
164
+ listPhaseDirs,
165
+ filterPlanFiles,
166
+ filterSummaryFiles,
167
+ parsePhaseDir,
168
+ classifyPhaseStatus,
169
+ fileAccessible,
170
+ hasBraveSearchKey,
171
+ };