agileflow 2.90.7 → 2.92.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/CHANGELOG.md +10 -0
- package/README.md +6 -6
- package/lib/README.md +178 -0
- package/lib/codebase-indexer.js +818 -0
- package/lib/colors.js +190 -12
- package/lib/consent.js +232 -0
- package/lib/correlation.js +277 -0
- package/lib/error-codes.js +46 -0
- package/lib/errors.js +48 -6
- package/lib/file-cache.js +182 -0
- package/lib/format-error.js +156 -0
- package/lib/path-resolver.js +155 -7
- package/lib/paths.js +212 -20
- package/lib/placeholder-registry.js +205 -0
- package/lib/registry-di.js +358 -0
- package/lib/result-schema.js +363 -0
- package/lib/result.js +210 -0
- package/lib/session-registry.js +13 -0
- package/lib/session-state-machine.js +465 -0
- package/lib/validate-commands.js +308 -0
- package/lib/validate-names.js +3 -3
- package/lib/validate.js +116 -52
- package/package.json +4 -1
- package/scripts/af +34 -0
- package/scripts/agent-loop.js +63 -9
- package/scripts/agileflow-configure.js +2 -2
- package/scripts/agileflow-welcome.js +435 -23
- package/scripts/archive-completed-stories.sh +57 -11
- package/scripts/claude-tmux.sh +102 -0
- package/scripts/damage-control-bash.js +3 -70
- package/scripts/damage-control-edit.js +3 -20
- package/scripts/damage-control-write.js +3 -20
- package/scripts/dependency-check.js +310 -0
- package/scripts/get-env.js +11 -4
- package/scripts/lib/configure-detect.js +23 -1
- package/scripts/lib/configure-features.js +43 -2
- package/scripts/lib/context-formatter.js +771 -0
- package/scripts/lib/context-loader.js +699 -0
- package/scripts/lib/damage-control-utils.js +107 -0
- package/scripts/lib/json-utils.sh +162 -0
- package/scripts/lib/state-migrator.js +353 -0
- package/scripts/lib/story-state-machine.js +437 -0
- package/scripts/obtain-context.js +118 -1048
- package/scripts/pre-push-check.sh +46 -0
- package/scripts/precompact-context.sh +36 -11
- package/scripts/query-codebase.js +538 -0
- package/scripts/ralph-loop.js +5 -5
- package/scripts/session-manager.js +220 -42
- package/scripts/spawn-parallel.js +651 -0
- package/scripts/tui/blessed/data/watcher.js +180 -0
- package/scripts/tui/blessed/index.js +244 -0
- package/scripts/tui/blessed/panels/output.js +101 -0
- package/scripts/tui/blessed/panels/sessions.js +150 -0
- package/scripts/tui/blessed/panels/trace.js +97 -0
- package/scripts/tui/blessed/ui/help.js +77 -0
- package/scripts/tui/blessed/ui/screen.js +52 -0
- package/scripts/tui/blessed/ui/statusbar.js +47 -0
- package/scripts/tui/blessed/ui/tabbar.js +99 -0
- package/scripts/tui/index.js +38 -30
- package/scripts/validators/README.md +143 -0
- package/scripts/validators/component-validator.js +239 -0
- package/scripts/validators/json-schema-validator.js +186 -0
- package/scripts/validators/markdown-validator.js +152 -0
- package/scripts/validators/migration-validator.js +129 -0
- package/scripts/validators/security-validator.js +380 -0
- package/scripts/validators/story-format-validator.js +197 -0
- package/scripts/validators/test-result-validator.js +114 -0
- package/scripts/validators/workflow-validator.js +247 -0
- package/src/core/agents/accessibility.md +6 -0
- package/src/core/agents/adr-writer.md +6 -0
- package/src/core/agents/analytics.md +6 -0
- package/src/core/agents/api.md +6 -0
- package/src/core/agents/ci.md +6 -0
- package/src/core/agents/codebase-query.md +261 -0
- package/src/core/agents/compliance.md +6 -0
- package/src/core/agents/configuration-damage-control.md +6 -0
- package/src/core/agents/configuration-visual-e2e.md +6 -0
- package/src/core/agents/database.md +10 -0
- package/src/core/agents/datamigration.md +6 -0
- package/src/core/agents/design.md +6 -0
- package/src/core/agents/devops.md +6 -0
- package/src/core/agents/documentation.md +6 -0
- package/src/core/agents/epic-planner.md +6 -0
- package/src/core/agents/integrations.md +6 -0
- package/src/core/agents/mentor.md +6 -0
- package/src/core/agents/mobile.md +6 -0
- package/src/core/agents/monitoring.md +6 -0
- package/src/core/agents/multi-expert.md +6 -0
- package/src/core/agents/performance.md +6 -0
- package/src/core/agents/product.md +6 -0
- package/src/core/agents/qa.md +6 -0
- package/src/core/agents/readme-updater.md +6 -0
- package/src/core/agents/refactor.md +6 -0
- package/src/core/agents/research.md +6 -0
- package/src/core/agents/security.md +6 -0
- package/src/core/agents/testing.md +10 -0
- package/src/core/agents/ui.md +6 -0
- package/src/core/commands/adr.md +114 -0
- package/src/core/commands/agent.md +120 -0
- package/src/core/commands/assign.md +145 -0
- package/src/core/commands/audit.md +401 -0
- package/src/core/commands/babysit.md +32 -5
- package/src/core/commands/board.md +1 -0
- package/src/core/commands/changelog.md +118 -0
- package/src/core/commands/configure.md +42 -6
- package/src/core/commands/diagnose.md +114 -0
- package/src/core/commands/epic.md +205 -1
- package/src/core/commands/handoff.md +128 -0
- package/src/core/commands/help.md +76 -0
- package/src/core/commands/metrics.md +1 -0
- package/src/core/commands/pr.md +96 -0
- package/src/core/commands/research/analyze.md +1 -0
- package/src/core/commands/research/ask.md +2 -0
- package/src/core/commands/research/import.md +1 -0
- package/src/core/commands/research/list.md +2 -0
- package/src/core/commands/research/synthesize.md +584 -0
- package/src/core/commands/research/view.md +2 -0
- package/src/core/commands/roadmap/analyze.md +400 -0
- package/src/core/commands/session/new.md +113 -6
- package/src/core/commands/session/spawn.md +197 -0
- package/src/core/commands/sprint.md +22 -0
- package/src/core/commands/status.md +200 -1
- package/src/core/commands/story/list.md +9 -9
- package/src/core/commands/story/view.md +1 -0
- package/src/core/commands/story.md +143 -4
- package/src/core/experts/codebase-query/expertise.yaml +190 -0
- package/src/core/experts/codebase-query/question.md +73 -0
- package/src/core/experts/codebase-query/self-improve.md +105 -0
- package/src/core/templates/agileflow-metadata.json +55 -2
- package/src/core/templates/plan-template.md +125 -0
- package/src/core/templates/story-lifecycle.md +213 -0
- package/src/core/templates/story-template.md +4 -0
- package/src/core/templates/tdd-test-template.js +241 -0
- package/tools/cli/commands/setup.js +86 -0
- package/tools/cli/installers/core/installer.js +94 -0
- package/tools/cli/installers/ide/_base-ide.js +20 -11
- package/tools/cli/installers/ide/codex.js +29 -47
- package/tools/cli/lib/config-manager.js +17 -2
- package/tools/cli/lib/content-transformer.js +271 -0
- package/tools/cli/lib/error-handler.js +14 -22
- package/tools/cli/lib/ide-error-factory.js +421 -0
- package/tools/cli/lib/ide-health-monitor.js +364 -0
- package/tools/cli/lib/ide-registry.js +114 -1
- package/tools/cli/lib/ui.js +14 -25
package/lib/colors.js
CHANGED
|
@@ -11,21 +11,123 @@
|
|
|
11
11
|
* - Cyan (#00CED1): 4.6:1 ✓ (meets AA for normal text)
|
|
12
12
|
* - Brand (#e8683a): 3.8:1 ✓ (meets AA for large text/UI elements)
|
|
13
13
|
*
|
|
14
|
+
* WCAG AAA Contrast Ratios (high-contrast mode, 7:1+ ratio):
|
|
15
|
+
* - HC Green (#7CFC00): 11.3:1 ✓ (lawn green - meets AAA)
|
|
16
|
+
* - HC Red (#FF6B6B): 5.0:1 → #FF9999 8.1:1 ✓ (light coral - meets AAA)
|
|
17
|
+
* - HC Yellow (#FFFF00): 19.6:1 ✓ (pure yellow - meets AAA)
|
|
18
|
+
* - HC Cyan (#00FFFF): 14.0:1 ✓ (aqua - meets AAA)
|
|
19
|
+
* - HC White (#FFFFFF): 21.0:1 ✓ (pure white - meets AAA)
|
|
20
|
+
*
|
|
14
21
|
* Note: Standard ANSI colors vary by terminal theme. The above ratios
|
|
15
22
|
* are for typical dark terminal configurations.
|
|
16
23
|
*/
|
|
17
24
|
|
|
25
|
+
// High-contrast mode detection
|
|
26
|
+
let _highContrastMode = null;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if high-contrast mode is enabled.
|
|
30
|
+
* Checks: AGILEFLOW_HIGH_CONTRAST env var, or cached value.
|
|
31
|
+
* @returns {boolean} True if high-contrast mode is enabled
|
|
32
|
+
*/
|
|
33
|
+
function isHighContrast() {
|
|
34
|
+
if (_highContrastMode !== null) {
|
|
35
|
+
return _highContrastMode;
|
|
36
|
+
}
|
|
37
|
+
const envValue = process.env.AGILEFLOW_HIGH_CONTRAST;
|
|
38
|
+
return envValue === '1' || envValue === 'true' || envValue === 'yes';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Enable or disable high-contrast mode programmatically.
|
|
43
|
+
* @param {boolean} enabled - Whether to enable high-contrast mode
|
|
44
|
+
*/
|
|
45
|
+
function setHighContrast(enabled) {
|
|
46
|
+
_highContrastMode = enabled;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reset high-contrast mode to use environment variable.
|
|
51
|
+
*/
|
|
52
|
+
function resetHighContrast() {
|
|
53
|
+
_highContrastMode = null;
|
|
54
|
+
}
|
|
55
|
+
|
|
18
56
|
/**
|
|
19
57
|
* Brand color hex value for chalk compatibility.
|
|
20
58
|
* Use with chalk.hex(BRAND_HEX) in files that use chalk.
|
|
21
59
|
*/
|
|
22
60
|
const BRAND_HEX = '#e8683a';
|
|
23
61
|
|
|
62
|
+
/**
|
|
63
|
+
* WCAG AAA high-contrast color palette (7:1+ contrast ratio).
|
|
64
|
+
* Used when AGILEFLOW_HIGH_CONTRAST=1 or --high-contrast flag.
|
|
65
|
+
*/
|
|
66
|
+
const hc = {
|
|
67
|
+
// Reset and modifiers
|
|
68
|
+
reset: '\x1b[0m',
|
|
69
|
+
bold: '\x1b[1m',
|
|
70
|
+
dim: '\x1b[0m', // No dimming in high-contrast (use regular text)
|
|
71
|
+
italic: '\x1b[3m',
|
|
72
|
+
underline: '\x1b[4m',
|
|
73
|
+
|
|
74
|
+
// High-contrast standard colors (bright variants for max visibility)
|
|
75
|
+
red: '\x1b[91m', // Bright red
|
|
76
|
+
green: '\x1b[92m', // Bright green
|
|
77
|
+
yellow: '\x1b[93m', // Bright yellow
|
|
78
|
+
blue: '\x1b[94m', // Bright blue
|
|
79
|
+
magenta: '\x1b[95m', // Bright magenta
|
|
80
|
+
cyan: '\x1b[96m', // Bright cyan
|
|
81
|
+
white: '\x1b[97m', // Bright white
|
|
82
|
+
|
|
83
|
+
// Bright variants (same in high-contrast mode)
|
|
84
|
+
brightBlack: '\x1b[37m', // Use white instead of gray
|
|
85
|
+
brightRed: '\x1b[91m',
|
|
86
|
+
brightGreen: '\x1b[92m',
|
|
87
|
+
brightYellow: '\x1b[93m',
|
|
88
|
+
brightBlue: '\x1b[94m',
|
|
89
|
+
brightMagenta: '\x1b[95m',
|
|
90
|
+
brightCyan: '\x1b[96m',
|
|
91
|
+
brightWhite: '\x1b[97m',
|
|
92
|
+
|
|
93
|
+
// 256-color high-contrast alternatives (all 7:1+ ratio)
|
|
94
|
+
mintGreen: '\x1b[92m', // Bright green
|
|
95
|
+
peach: '\x1b[93m', // Bright yellow
|
|
96
|
+
coral: '\x1b[91m', // Bright red
|
|
97
|
+
lightGreen: '\x1b[92m', // Bright green
|
|
98
|
+
lightYellow: '\x1b[93m', // Bright yellow
|
|
99
|
+
lightPink: '\x1b[91m', // Bright red
|
|
100
|
+
skyBlue: '\x1b[96m', // Bright cyan
|
|
101
|
+
lavender: '\x1b[95m', // Bright magenta
|
|
102
|
+
softGold: '\x1b[93m', // Bright yellow
|
|
103
|
+
teal: '\x1b[96m', // Bright cyan
|
|
104
|
+
slate: '\x1b[97m', // White (instead of gray)
|
|
105
|
+
rose: '\x1b[91m', // Bright red
|
|
106
|
+
amber: '\x1b[93m', // Bright yellow
|
|
107
|
+
powder: '\x1b[96m', // Bright cyan
|
|
108
|
+
|
|
109
|
+
// Brand color - use bright orange/yellow for visibility
|
|
110
|
+
brand: '\x1b[38;2;255;165;0m', // Bright orange (#FFA500 - 8.0:1 ratio)
|
|
111
|
+
orange: '\x1b[38;2;255;165;0m',
|
|
112
|
+
|
|
113
|
+
// Background colors (same as standard)
|
|
114
|
+
bgRed: '\x1b[41m',
|
|
115
|
+
bgGreen: '\x1b[42m',
|
|
116
|
+
bgYellow: '\x1b[43m',
|
|
117
|
+
bgBlue: '\x1b[44m',
|
|
118
|
+
|
|
119
|
+
// Semantic aliases
|
|
120
|
+
success: '\x1b[92m',
|
|
121
|
+
error: '\x1b[91m',
|
|
122
|
+
warning: '\x1b[93m',
|
|
123
|
+
info: '\x1b[96m',
|
|
124
|
+
};
|
|
125
|
+
|
|
24
126
|
/**
|
|
25
127
|
* ANSI color codes for terminal output.
|
|
26
128
|
* Includes standard colors, 256-color palette, and brand colors.
|
|
27
129
|
*/
|
|
28
|
-
const
|
|
130
|
+
const cStandard = {
|
|
29
131
|
// Reset and modifiers
|
|
30
132
|
reset: '\x1b[0m',
|
|
31
133
|
bold: '\x1b[1m',
|
|
@@ -85,6 +187,36 @@ const c = {
|
|
|
85
187
|
info: '\x1b[36m', // Same as cyan
|
|
86
188
|
};
|
|
87
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Get the active color palette based on high-contrast mode.
|
|
192
|
+
* @returns {Object} Color palette object (either cStandard or hc)
|
|
193
|
+
*/
|
|
194
|
+
function getColors() {
|
|
195
|
+
return isHighContrast() ? hc : cStandard;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// For backwards compatibility, export a Proxy that delegates to the active palette
|
|
199
|
+
const c = new Proxy(
|
|
200
|
+
{},
|
|
201
|
+
{
|
|
202
|
+
get(_, prop) {
|
|
203
|
+
return getColors()[prop];
|
|
204
|
+
},
|
|
205
|
+
has(_, prop) {
|
|
206
|
+
return prop in cStandard;
|
|
207
|
+
},
|
|
208
|
+
ownKeys() {
|
|
209
|
+
return Object.keys(cStandard);
|
|
210
|
+
},
|
|
211
|
+
getOwnPropertyDescriptor(_, prop) {
|
|
212
|
+
if (prop in cStandard) {
|
|
213
|
+
return { enumerable: true, configurable: true, value: getColors()[prop] };
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
|
|
88
220
|
/**
|
|
89
221
|
* Box drawing characters for tables and borders.
|
|
90
222
|
*/
|
|
@@ -114,18 +246,49 @@ const box = {
|
|
|
114
246
|
};
|
|
115
247
|
|
|
116
248
|
/**
|
|
117
|
-
*
|
|
249
|
+
* Get status indicators with current color palette.
|
|
250
|
+
* Uses a Proxy to dynamically generate colored indicators.
|
|
118
251
|
*/
|
|
119
|
-
const status =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
252
|
+
const status = new Proxy(
|
|
253
|
+
{},
|
|
254
|
+
{
|
|
255
|
+
get(_, prop) {
|
|
256
|
+
const colors = getColors();
|
|
257
|
+
const indicators = {
|
|
258
|
+
success: `${colors.green}✓${colors.reset}`,
|
|
259
|
+
warning: `${colors.yellow}⚠️${colors.reset}`,
|
|
260
|
+
error: `${colors.red}✗${colors.reset}`,
|
|
261
|
+
info: `${colors.cyan}ℹ${colors.reset}`,
|
|
262
|
+
pending: `${isHighContrast() ? colors.white : colors.dim}○${colors.reset}`,
|
|
263
|
+
inProgress: `${colors.yellow}◐${colors.reset}`,
|
|
264
|
+
done: `${colors.green}●${colors.reset}`,
|
|
265
|
+
blocked: `${colors.red}◆${colors.reset}`,
|
|
266
|
+
};
|
|
267
|
+
return indicators[prop];
|
|
268
|
+
},
|
|
269
|
+
has(_, prop) {
|
|
270
|
+
return [
|
|
271
|
+
'success',
|
|
272
|
+
'warning',
|
|
273
|
+
'error',
|
|
274
|
+
'info',
|
|
275
|
+
'pending',
|
|
276
|
+
'inProgress',
|
|
277
|
+
'done',
|
|
278
|
+
'blocked',
|
|
279
|
+
].includes(prop);
|
|
280
|
+
},
|
|
281
|
+
ownKeys() {
|
|
282
|
+
return ['success', 'warning', 'error', 'info', 'pending', 'inProgress', 'done', 'blocked'];
|
|
283
|
+
},
|
|
284
|
+
getOwnPropertyDescriptor(_, prop) {
|
|
285
|
+
if (status.has(_, prop)) {
|
|
286
|
+
return { enumerable: true, configurable: true };
|
|
287
|
+
}
|
|
288
|
+
return undefined;
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
);
|
|
129
292
|
|
|
130
293
|
/**
|
|
131
294
|
* Wrap text with color codes.
|
|
@@ -199,9 +362,22 @@ function brand(text) {
|
|
|
199
362
|
}
|
|
200
363
|
|
|
201
364
|
module.exports = {
|
|
365
|
+
// Color palettes
|
|
202
366
|
c,
|
|
367
|
+
cStandard,
|
|
368
|
+
hc,
|
|
369
|
+
getColors,
|
|
370
|
+
|
|
371
|
+
// High-contrast mode control
|
|
372
|
+
isHighContrast,
|
|
373
|
+
setHighContrast,
|
|
374
|
+
resetHighContrast,
|
|
375
|
+
|
|
376
|
+
// UI elements
|
|
203
377
|
box,
|
|
204
378
|
status,
|
|
379
|
+
|
|
380
|
+
// Helper functions
|
|
205
381
|
colorize,
|
|
206
382
|
dim,
|
|
207
383
|
bold,
|
|
@@ -209,5 +385,7 @@ module.exports = {
|
|
|
209
385
|
warning,
|
|
210
386
|
error,
|
|
211
387
|
brand,
|
|
388
|
+
|
|
389
|
+
// Constants
|
|
212
390
|
BRAND_HEX,
|
|
213
391
|
};
|
package/lib/consent.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* consent.js
|
|
3
|
+
*
|
|
4
|
+
* GDPR consent handling for AgileFlow (US-0149)
|
|
5
|
+
*
|
|
6
|
+
* Manages privacy consent during setup:
|
|
7
|
+
* - Prompts user to acknowledge privacy policy
|
|
8
|
+
* - Stores consent timestamp in .agileflow/config/consent.json
|
|
9
|
+
* - Supports --accept-privacy flag for CI environments
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const readline = require('readline');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Consent configuration
|
|
18
|
+
*/
|
|
19
|
+
const CONSENT_FILE = '.agileflow/config/consent.json';
|
|
20
|
+
const PRIVACY_POLICY_URL =
|
|
21
|
+
'https://github.com/projectquestorg/AgileFlow/blob/main/packages/cli/PRIVACY.md';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Consent status types
|
|
25
|
+
*/
|
|
26
|
+
const ConsentStatus = {
|
|
27
|
+
ACCEPTED: 'accepted',
|
|
28
|
+
DECLINED: 'declined',
|
|
29
|
+
PENDING: 'pending',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if consent has been given
|
|
34
|
+
* @returns {{ hasConsent: boolean, consent: Object | null }}
|
|
35
|
+
*/
|
|
36
|
+
function checkConsent() {
|
|
37
|
+
const consentPath = path.resolve(process.cwd(), CONSENT_FILE);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
if (fs.existsSync(consentPath)) {
|
|
41
|
+
const consent = JSON.parse(fs.readFileSync(consentPath, 'utf8'));
|
|
42
|
+
return {
|
|
43
|
+
hasConsent: consent.status === ConsentStatus.ACCEPTED,
|
|
44
|
+
consent,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Ignore errors, treat as no consent
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { hasConsent: false, consent: null };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Record consent
|
|
56
|
+
* @param {string} status - 'accepted' or 'declined'
|
|
57
|
+
* @param {Object} options - Additional options
|
|
58
|
+
* @param {string} options.method - How consent was given ('interactive', 'flag', 'api')
|
|
59
|
+
* @param {string} options.version - Privacy policy version
|
|
60
|
+
* @returns {{ ok: boolean, path: string }}
|
|
61
|
+
*/
|
|
62
|
+
function recordConsent(status, options = {}) {
|
|
63
|
+
const { method = 'interactive', version = '1.0.0' } = options;
|
|
64
|
+
|
|
65
|
+
const consentPath = path.resolve(process.cwd(), CONSENT_FILE);
|
|
66
|
+
const consentDir = path.dirname(consentPath);
|
|
67
|
+
|
|
68
|
+
// Ensure directory exists
|
|
69
|
+
if (!fs.existsSync(consentDir)) {
|
|
70
|
+
fs.mkdirSync(consentDir, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const consent = {
|
|
74
|
+
status,
|
|
75
|
+
timestamp: new Date().toISOString(),
|
|
76
|
+
method,
|
|
77
|
+
policy_version: version,
|
|
78
|
+
policy_url: PRIVACY_POLICY_URL,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
fs.writeFileSync(consentPath, JSON.stringify(consent, null, 2));
|
|
83
|
+
return { ok: true, path: consentPath };
|
|
84
|
+
} catch (err) {
|
|
85
|
+
return { ok: false, error: err.message };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Prompt user for consent interactively
|
|
91
|
+
* @param {Object} options - Prompt options
|
|
92
|
+
* @param {WritableStream} options.output - Output stream (default: process.stdout)
|
|
93
|
+
* @param {ReadableStream} options.input - Input stream (default: process.stdin)
|
|
94
|
+
* @returns {Promise<{ accepted: boolean }>}
|
|
95
|
+
*/
|
|
96
|
+
async function promptConsent(options = {}) {
|
|
97
|
+
const { output = process.stdout, input = process.stdin } = options;
|
|
98
|
+
|
|
99
|
+
const rl = readline.createInterface({
|
|
100
|
+
input,
|
|
101
|
+
output,
|
|
102
|
+
terminal: false,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Display privacy notice
|
|
106
|
+
const notice = `
|
|
107
|
+
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
108
|
+
┃ Privacy Notice ┃
|
|
109
|
+
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
110
|
+
|
|
111
|
+
AgileFlow respects your privacy:
|
|
112
|
+
|
|
113
|
+
• All data is stored locally on your machine
|
|
114
|
+
• No telemetry, analytics, or tracking
|
|
115
|
+
• No data is transmitted to external servers
|
|
116
|
+
• You can delete all data at any time
|
|
117
|
+
|
|
118
|
+
For details, see: ${PRIVACY_POLICY_URL}
|
|
119
|
+
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
output.write(notice);
|
|
123
|
+
|
|
124
|
+
return new Promise(resolve => {
|
|
125
|
+
const question = 'Do you accept the privacy policy? (yes/no): ';
|
|
126
|
+
output.write(question);
|
|
127
|
+
|
|
128
|
+
rl.once('line', answer => {
|
|
129
|
+
rl.close();
|
|
130
|
+
const normalized = answer.toLowerCase().trim();
|
|
131
|
+
const accepted = normalized === 'yes' || normalized === 'y';
|
|
132
|
+
resolve({ accepted });
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Handle consent during setup
|
|
139
|
+
* @param {Object} options - Setup options
|
|
140
|
+
* @param {boolean} options.acceptPrivacy - --accept-privacy flag was passed
|
|
141
|
+
* @param {boolean} options.silent - Silent mode (no prompts)
|
|
142
|
+
* @param {WritableStream} options.output - Output stream
|
|
143
|
+
* @param {ReadableStream} options.input - Input stream
|
|
144
|
+
* @returns {Promise<{ ok: boolean, status: string, skipped: boolean }>}
|
|
145
|
+
*/
|
|
146
|
+
async function handleSetupConsent(options = {}) {
|
|
147
|
+
const {
|
|
148
|
+
acceptPrivacy = false,
|
|
149
|
+
silent = false,
|
|
150
|
+
output = process.stdout,
|
|
151
|
+
input = process.stdin,
|
|
152
|
+
} = options;
|
|
153
|
+
|
|
154
|
+
// Check if consent already given
|
|
155
|
+
const { hasConsent, consent } = checkConsent();
|
|
156
|
+
if (hasConsent) {
|
|
157
|
+
return { ok: true, status: 'already_consented', skipped: false, consent };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// If --accept-privacy flag provided
|
|
161
|
+
if (acceptPrivacy) {
|
|
162
|
+
const result = recordConsent(ConsentStatus.ACCEPTED, { method: 'flag' });
|
|
163
|
+
return { ok: result.ok, status: 'accepted_via_flag', skipped: false };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// If silent mode (CI without flag)
|
|
167
|
+
if (silent) {
|
|
168
|
+
return { ok: false, status: 'consent_required', skipped: true };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Interactive prompt
|
|
172
|
+
const { accepted } = await promptConsent({ output, input });
|
|
173
|
+
|
|
174
|
+
if (accepted) {
|
|
175
|
+
const result = recordConsent(ConsentStatus.ACCEPTED, { method: 'interactive' });
|
|
176
|
+
return { ok: result.ok, status: 'accepted_interactive', skipped: false };
|
|
177
|
+
} else {
|
|
178
|
+
const result = recordConsent(ConsentStatus.DECLINED, { method: 'interactive' });
|
|
179
|
+
return { ok: false, status: 'declined', skipped: false };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get consent status for display
|
|
185
|
+
* @returns {{ status: string, timestamp: string | null, method: string | null }}
|
|
186
|
+
*/
|
|
187
|
+
function getConsentStatus() {
|
|
188
|
+
const { hasConsent, consent } = checkConsent();
|
|
189
|
+
|
|
190
|
+
if (!consent) {
|
|
191
|
+
return { status: ConsentStatus.PENDING, timestamp: null, method: null };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
status: consent.status,
|
|
196
|
+
timestamp: consent.timestamp,
|
|
197
|
+
method: consent.method,
|
|
198
|
+
policyVersion: consent.policy_version,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Revoke consent (delete consent file)
|
|
204
|
+
* @returns {{ ok: boolean }}
|
|
205
|
+
*/
|
|
206
|
+
function revokeConsent() {
|
|
207
|
+
const consentPath = path.resolve(process.cwd(), CONSENT_FILE);
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
if (fs.existsSync(consentPath)) {
|
|
211
|
+
fs.unlinkSync(consentPath);
|
|
212
|
+
}
|
|
213
|
+
return { ok: true };
|
|
214
|
+
} catch (err) {
|
|
215
|
+
return { ok: false, error: err.message };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = {
|
|
220
|
+
// Constants
|
|
221
|
+
CONSENT_FILE,
|
|
222
|
+
PRIVACY_POLICY_URL,
|
|
223
|
+
ConsentStatus,
|
|
224
|
+
|
|
225
|
+
// Functions
|
|
226
|
+
checkConsent,
|
|
227
|
+
recordConsent,
|
|
228
|
+
promptConsent,
|
|
229
|
+
handleSetupConsent,
|
|
230
|
+
getConsentStatus,
|
|
231
|
+
revokeConsent,
|
|
232
|
+
};
|