agileflow 2.89.1 → 2.89.3

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.
@@ -0,0 +1,396 @@
1
+ /**
2
+ * path-resolver.js - Unified Path Resolution Service
3
+ *
4
+ * Provides centralized path resolution that respects user configuration.
5
+ * Loads folder names from manifest.yaml and provides consistent path
6
+ * resolution across all AgileFlow scripts and commands.
7
+ *
8
+ * Features:
9
+ * - Loads folder names from manifest.yaml
10
+ * - Caches configuration for performance
11
+ * - Validates paths before shell execution
12
+ * - Provides consistent API for all path operations
13
+ *
14
+ * Usage:
15
+ * const PathResolver = require('./path-resolver');
16
+ * const resolver = new PathResolver('/path/to/project');
17
+ *
18
+ * // Get paths
19
+ * const agileflowDir = resolver.getAgileflowDir();
20
+ * const docsDir = resolver.getDocsDir();
21
+ * const statusPath = resolver.getStatusPath();
22
+ *
23
+ * // Get all paths at once
24
+ * const paths = resolver.getAllPaths();
25
+ */
26
+
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+ const { safeLoad } = require('./yaml-utils');
30
+ const { validatePath } = require('./validate');
31
+
32
+ /**
33
+ * Unified Path Resolution Service
34
+ */
35
+ class PathResolver {
36
+ /**
37
+ * @param {string} [projectRoot] - Project root directory (auto-detected if not provided)
38
+ * @param {Object} [options={}] - Configuration options
39
+ * @param {boolean} [options.autoDetect=true] - Auto-detect project root if not found
40
+ * @param {string} [options.agileflowFolder='.agileflow'] - Default agileflow folder name
41
+ * @param {string} [options.docsFolder='docs'] - Default docs folder name
42
+ */
43
+ constructor(projectRoot, options = {}) {
44
+ const { autoDetect = true, agileflowFolder = '.agileflow', docsFolder = 'docs' } = options;
45
+
46
+ // Store defaults
47
+ this._defaultAgileflowFolder = agileflowFolder;
48
+ this._defaultDocsFolder = docsFolder;
49
+
50
+ // Find project root
51
+ if (projectRoot) {
52
+ this._projectRoot = projectRoot;
53
+ } else if (autoDetect) {
54
+ this._projectRoot = this._findProjectRoot(process.cwd());
55
+ } else {
56
+ this._projectRoot = process.cwd();
57
+ }
58
+
59
+ // Cache for manifest data
60
+ this._manifestCache = null;
61
+ this._manifestCacheTime = 0;
62
+ this._cacheMaxAge = 5000; // 5 second cache
63
+ }
64
+
65
+ /**
66
+ * Find project root by looking for common project markers
67
+ * @param {string} startDir - Directory to start searching from
68
+ * @returns {string} Project root path
69
+ * @private
70
+ */
71
+ _findProjectRoot(startDir) {
72
+ const markers = ['.agileflow', 'agileflow', '.aflow', '.git', 'package.json'];
73
+ let dir = startDir;
74
+
75
+ while (dir !== path.dirname(dir)) {
76
+ for (const marker of markers) {
77
+ if (fs.existsSync(path.join(dir, marker))) {
78
+ // If we found .agileflow, that's definitely our root
79
+ if (['.agileflow', 'agileflow', '.aflow'].includes(marker)) {
80
+ return dir;
81
+ }
82
+ // For .git and package.json, only use if no agileflow dir found above
83
+ if (!this._hasAgileflowAbove(dir)) {
84
+ return dir;
85
+ }
86
+ }
87
+ }
88
+ dir = path.dirname(dir);
89
+ }
90
+
91
+ return startDir;
92
+ }
93
+
94
+ /**
95
+ * Check if any agileflow directory exists above this directory
96
+ * @param {string} dir - Directory to check from
97
+ * @returns {boolean}
98
+ * @private
99
+ */
100
+ _hasAgileflowAbove(dir) {
101
+ let current = path.dirname(dir);
102
+ while (current !== path.dirname(current)) {
103
+ if (
104
+ fs.existsSync(path.join(current, '.agileflow')) ||
105
+ fs.existsSync(path.join(current, 'agileflow')) ||
106
+ fs.existsSync(path.join(current, '.aflow'))
107
+ ) {
108
+ return true;
109
+ }
110
+ current = path.dirname(current);
111
+ }
112
+ return false;
113
+ }
114
+
115
+ /**
116
+ * Load manifest configuration with caching
117
+ * @returns {{agileflowFolder: string, docsFolder: string}}
118
+ * @private
119
+ */
120
+ _loadManifest() {
121
+ const now = Date.now();
122
+
123
+ // Return cached if still valid
124
+ if (this._manifestCache && now - this._manifestCacheTime < this._cacheMaxAge) {
125
+ return this._manifestCache;
126
+ }
127
+
128
+ // Find the agileflow directory
129
+ const possibleFolders = ['.agileflow', 'agileflow', '.aflow'];
130
+ let manifestPath = null;
131
+
132
+ for (const folder of possibleFolders) {
133
+ const candidate = path.join(this._projectRoot, folder, '_cfg', 'manifest.yaml');
134
+ if (fs.existsSync(candidate)) {
135
+ manifestPath = candidate;
136
+ this._actualAgileflowFolder = folder;
137
+ break;
138
+ }
139
+ }
140
+
141
+ // Default values
142
+ const result = {
143
+ agileflowFolder: this._actualAgileflowFolder || this._defaultAgileflowFolder,
144
+ docsFolder: this._defaultDocsFolder,
145
+ };
146
+
147
+ if (manifestPath) {
148
+ try {
149
+ const content = fs.readFileSync(manifestPath, 'utf8');
150
+ const manifest = safeLoad(content);
151
+
152
+ if (manifest && typeof manifest === 'object') {
153
+ result.agileflowFolder = manifest.agileflow_folder || result.agileflowFolder;
154
+ result.docsFolder = manifest.docs_folder || result.docsFolder;
155
+ }
156
+ } catch {
157
+ // Use defaults on error
158
+ }
159
+ }
160
+
161
+ // Cache the result
162
+ this._manifestCache = result;
163
+ this._manifestCacheTime = now;
164
+
165
+ return result;
166
+ }
167
+
168
+ /**
169
+ * Get the project root directory
170
+ * @returns {string}
171
+ */
172
+ getProjectRoot() {
173
+ return this._projectRoot;
174
+ }
175
+
176
+ /**
177
+ * Get the AgileFlow directory path
178
+ * @returns {string}
179
+ */
180
+ getAgileflowDir() {
181
+ const { agileflowFolder } = this._loadManifest();
182
+ return path.join(this._projectRoot, agileflowFolder);
183
+ }
184
+
185
+ /**
186
+ * Get the docs directory path
187
+ * @returns {string}
188
+ */
189
+ getDocsDir() {
190
+ const { docsFolder } = this._loadManifest();
191
+ return path.join(this._projectRoot, docsFolder);
192
+ }
193
+
194
+ /**
195
+ * Get the .claude directory path
196
+ * @returns {string}
197
+ */
198
+ getClaudeDir() {
199
+ return path.join(this._projectRoot, '.claude');
200
+ }
201
+
202
+ /**
203
+ * Get the status.json path
204
+ * @returns {string}
205
+ */
206
+ getStatusPath() {
207
+ return path.join(this.getDocsDir(), '09-agents', 'status.json');
208
+ }
209
+
210
+ /**
211
+ * Get the session-state.json path
212
+ * @returns {string}
213
+ */
214
+ getSessionStatePath() {
215
+ return path.join(this.getDocsDir(), '09-agents', 'session-state.json');
216
+ }
217
+
218
+ /**
219
+ * Get the agileflow-metadata.json path
220
+ * @returns {string}
221
+ */
222
+ getMetadataPath() {
223
+ return path.join(this.getDocsDir(), '00-meta', 'agileflow-metadata.json');
224
+ }
225
+
226
+ /**
227
+ * Get the config.yaml path
228
+ * @returns {string}
229
+ */
230
+ getConfigPath() {
231
+ return path.join(this.getAgileflowDir(), 'config.yaml');
232
+ }
233
+
234
+ /**
235
+ * Get the manifest.yaml path
236
+ * @returns {string}
237
+ */
238
+ getManifestPath() {
239
+ return path.join(this.getAgileflowDir(), '_cfg', 'manifest.yaml');
240
+ }
241
+
242
+ /**
243
+ * Get the scripts directory path
244
+ * @returns {string}
245
+ */
246
+ getScriptsDir() {
247
+ return path.join(this.getAgileflowDir(), 'scripts');
248
+ }
249
+
250
+ /**
251
+ * Get the commands directory path
252
+ * @returns {string}
253
+ */
254
+ getCommandsDir() {
255
+ return path.join(this.getAgileflowDir(), 'commands');
256
+ }
257
+
258
+ /**
259
+ * Get the agents directory path
260
+ * @returns {string}
261
+ */
262
+ getAgentsDir() {
263
+ return path.join(this.getAgileflowDir(), 'agents');
264
+ }
265
+
266
+ /**
267
+ * Get all paths at once
268
+ * @returns {Object} Object with all path values
269
+ */
270
+ getAllPaths() {
271
+ return {
272
+ projectRoot: this.getProjectRoot(),
273
+ agileflowDir: this.getAgileflowDir(),
274
+ docsDir: this.getDocsDir(),
275
+ claudeDir: this.getClaudeDir(),
276
+ statusPath: this.getStatusPath(),
277
+ sessionStatePath: this.getSessionStatePath(),
278
+ metadataPath: this.getMetadataPath(),
279
+ configPath: this.getConfigPath(),
280
+ manifestPath: this.getManifestPath(),
281
+ scriptsDir: this.getScriptsDir(),
282
+ commandsDir: this.getCommandsDir(),
283
+ agentsDir: this.getAgentsDir(),
284
+ };
285
+ }
286
+
287
+ /**
288
+ * Check if we're in an AgileFlow project
289
+ * @returns {boolean}
290
+ */
291
+ isAgileflowProject() {
292
+ return fs.existsSync(this.getAgileflowDir());
293
+ }
294
+
295
+ /**
296
+ * Validate a path is within the project root (prevents path traversal)
297
+ * @param {string} targetPath - Path to validate
298
+ * @returns {{ok: boolean, resolvedPath?: string, error?: Error}}
299
+ */
300
+ validatePath(targetPath) {
301
+ return validatePath(targetPath, this._projectRoot, { allowSymlinks: false });
302
+ }
303
+
304
+ /**
305
+ * Resolve a relative path within the project
306
+ * @param {...string} segments - Path segments to join
307
+ * @returns {string} Resolved absolute path
308
+ */
309
+ resolve(...segments) {
310
+ return path.join(this._projectRoot, ...segments);
311
+ }
312
+
313
+ /**
314
+ * Get a path relative to project root
315
+ * @param {string} absolutePath - Absolute path
316
+ * @returns {string} Relative path
317
+ */
318
+ relative(absolutePath) {
319
+ return path.relative(this._projectRoot, absolutePath);
320
+ }
321
+
322
+ /**
323
+ * Clear the manifest cache (useful for testing or after config changes)
324
+ */
325
+ clearCache() {
326
+ this._manifestCache = null;
327
+ this._manifestCacheTime = 0;
328
+ }
329
+
330
+ /**
331
+ * Get the folder names from configuration
332
+ * @returns {{agileflowFolder: string, docsFolder: string}}
333
+ */
334
+ getFolderNames() {
335
+ return this._loadManifest();
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Singleton instance for convenience
341
+ * Uses auto-detection for project root
342
+ */
343
+ let defaultInstance = null;
344
+
345
+ /**
346
+ * Get the default PathResolver instance (singleton)
347
+ * @param {boolean} [forceNew=false] - Force creation of new instance
348
+ * @returns {PathResolver}
349
+ */
350
+ function getDefaultResolver(forceNew = false) {
351
+ if (!defaultInstance || forceNew) {
352
+ defaultInstance = new PathResolver();
353
+ }
354
+ return defaultInstance;
355
+ }
356
+
357
+ /**
358
+ * Convenience function to get all paths using default resolver
359
+ * @returns {Object}
360
+ */
361
+ function getAllPaths() {
362
+ return getDefaultResolver().getAllPaths();
363
+ }
364
+
365
+ /**
366
+ * Convenience function to get project root using default resolver
367
+ * @returns {string}
368
+ */
369
+ function getProjectRoot() {
370
+ return getDefaultResolver().getProjectRoot();
371
+ }
372
+
373
+ /**
374
+ * Convenience function to get agileflow dir using default resolver
375
+ * @returns {string}
376
+ */
377
+ function getAgileflowDir() {
378
+ return getDefaultResolver().getAgileflowDir();
379
+ }
380
+
381
+ /**
382
+ * Convenience function to get docs dir using default resolver
383
+ * @returns {string}
384
+ */
385
+ function getDocsDir() {
386
+ return getDefaultResolver().getDocsDir();
387
+ }
388
+
389
+ module.exports = {
390
+ PathResolver,
391
+ getDefaultResolver,
392
+ getAllPaths,
393
+ getProjectRoot,
394
+ getAgileflowDir,
395
+ getDocsDir,
396
+ };