specweave 0.23.8 → 0.23.12
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/.claude-plugin/marketplace.json +7 -7
- package/CLAUDE.md +391 -1338
- package/dist/src/cli/commands/cleanup-cache.d.ts +14 -0
- package/dist/src/cli/commands/cleanup-cache.d.ts.map +1 -0
- package/dist/src/cli/commands/cleanup-cache.js +63 -0
- package/dist/src/cli/commands/cleanup-cache.js.map +1 -0
- package/dist/src/cli/commands/init.js +40 -0
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/commands/migrate-config.d.ts +22 -0
- package/dist/src/cli/commands/migrate-config.d.ts.map +1 -0
- package/dist/src/cli/commands/migrate-config.js +149 -0
- package/dist/src/cli/commands/migrate-config.js.map +1 -0
- package/dist/src/cli/helpers/async-project-loader.d.ts +148 -0
- package/dist/src/cli/helpers/async-project-loader.d.ts.map +1 -0
- package/dist/src/cli/helpers/async-project-loader.js +351 -0
- package/dist/src/cli/helpers/async-project-loader.js.map +1 -0
- package/dist/src/cli/helpers/cancelation-handler.d.ts +123 -0
- package/dist/src/cli/helpers/cancelation-handler.d.ts.map +1 -0
- package/dist/src/cli/helpers/cancelation-handler.js +187 -0
- package/dist/src/cli/helpers/cancelation-handler.js.map +1 -0
- package/dist/src/cli/helpers/import-strategy-prompter.d.ts +43 -0
- package/dist/src/cli/helpers/import-strategy-prompter.d.ts.map +1 -0
- package/dist/src/cli/helpers/import-strategy-prompter.js +136 -0
- package/dist/src/cli/helpers/import-strategy-prompter.js.map +1 -0
- package/dist/src/cli/helpers/issue-tracker/ado.d.ts +5 -2
- package/dist/src/cli/helpers/issue-tracker/ado.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/ado.js +90 -40
- package/dist/src/cli/helpers/issue-tracker/ado.js.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/index.js +112 -60
- package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/jira.d.ts +26 -2
- package/dist/src/cli/helpers/issue-tracker/jira.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/jira.js +197 -132
- package/dist/src/cli/helpers/issue-tracker/jira.js.map +1 -1
- package/dist/src/cli/helpers/progress-tracker.d.ts +121 -0
- package/dist/src/cli/helpers/progress-tracker.d.ts.map +1 -0
- package/dist/src/cli/helpers/progress-tracker.js +202 -0
- package/dist/src/cli/helpers/progress-tracker.js.map +1 -0
- package/dist/src/cli/helpers/project-count-fetcher.d.ts +69 -0
- package/dist/src/cli/helpers/project-count-fetcher.d.ts.map +1 -0
- package/dist/src/cli/helpers/project-count-fetcher.js +173 -0
- package/dist/src/cli/helpers/project-count-fetcher.js.map +1 -0
- package/dist/src/config/types.d.ts +14 -14
- package/dist/src/core/cache/cache-manager.d.ts +119 -0
- package/dist/src/core/cache/cache-manager.d.ts.map +1 -0
- package/dist/src/core/cache/cache-manager.js +304 -0
- package/dist/src/core/cache/cache-manager.js.map +1 -0
- package/dist/src/core/cache/rate-limit-checker.d.ts +92 -0
- package/dist/src/core/cache/rate-limit-checker.d.ts.map +1 -0
- package/dist/src/core/cache/rate-limit-checker.js +160 -0
- package/dist/src/core/cache/rate-limit-checker.js.map +1 -0
- package/dist/src/core/config/config-manager.d.ts +135 -0
- package/dist/src/core/config/config-manager.d.ts.map +1 -0
- package/dist/src/core/config/config-manager.js +341 -0
- package/dist/src/core/config/config-manager.js.map +1 -0
- package/dist/src/core/config/config-migrator.d.ts +102 -0
- package/dist/src/core/config/config-migrator.d.ts.map +1 -0
- package/dist/src/core/config/config-migrator.js +367 -0
- package/dist/src/core/config/config-migrator.js.map +1 -0
- package/dist/src/core/config/index.d.ts +10 -0
- package/dist/src/core/config/index.d.ts.map +1 -0
- package/dist/src/core/config/index.js +10 -0
- package/dist/src/core/config/index.js.map +1 -0
- package/dist/src/core/config/types.d.ts +216 -0
- package/dist/src/core/config/types.d.ts.map +1 -0
- package/dist/src/core/config/types.js +32 -0
- package/dist/src/core/config/types.js.map +1 -0
- package/dist/src/core/progress/cancelation-handler.d.ts +79 -0
- package/dist/src/core/progress/cancelation-handler.d.ts.map +1 -0
- package/dist/src/core/progress/cancelation-handler.js +111 -0
- package/dist/src/core/progress/cancelation-handler.js.map +1 -0
- package/dist/src/core/progress/import-state.d.ts +71 -0
- package/dist/src/core/progress/import-state.d.ts.map +1 -0
- package/dist/src/core/progress/import-state.js +96 -0
- package/dist/src/core/progress/import-state.js.map +1 -0
- package/dist/src/core/progress/progress-tracker.d.ts +139 -0
- package/dist/src/core/progress/progress-tracker.d.ts.map +1 -0
- package/dist/src/core/progress/progress-tracker.js +223 -0
- package/dist/src/core/progress/progress-tracker.js.map +1 -0
- package/dist/src/init/architecture/types.d.ts +6 -6
- package/dist/src/integrations/ado/ado-client.d.ts +25 -0
- package/dist/src/integrations/ado/ado-client.d.ts.map +1 -1
- package/dist/src/integrations/ado/ado-client.js +67 -0
- package/dist/src/integrations/ado/ado-client.js.map +1 -1
- package/dist/src/integrations/ado/ado-dependency-loader.d.ts +99 -0
- package/dist/src/integrations/ado/ado-dependency-loader.d.ts.map +1 -0
- package/dist/src/integrations/ado/ado-dependency-loader.js +207 -0
- package/dist/src/integrations/ado/ado-dependency-loader.js.map +1 -0
- package/dist/src/integrations/jira/jira-client.d.ts +32 -0
- package/dist/src/integrations/jira/jira-client.d.ts.map +1 -1
- package/dist/src/integrations/jira/jira-client.js +81 -0
- package/dist/src/integrations/jira/jira-client.js.map +1 -1
- package/dist/src/integrations/jira/jira-dependency-loader.d.ts +101 -0
- package/dist/src/integrations/jira/jira-dependency-loader.d.ts.map +1 -0
- package/dist/src/integrations/jira/jira-dependency-loader.js +200 -0
- package/dist/src/integrations/jira/jira-dependency-loader.js.map +1 -0
- package/dist/src/integrations/jira/jira-hierarchy-mapper.d.ts +104 -0
- package/dist/src/integrations/jira/jira-hierarchy-mapper.d.ts.map +1 -0
- package/dist/src/integrations/jira/jira-hierarchy-mapper.js +178 -0
- package/dist/src/integrations/jira/jira-hierarchy-mapper.js.map +1 -0
- package/package.json +1 -1
- package/plugins/specweave/.claude-plugin/plugin.json +20 -0
- package/plugins/specweave/agents/architect/AGENT.md +100 -602
- package/plugins/specweave/agents/pm/AGENT.md +96 -597
- package/plugins/specweave/agents/pm/AGENT.md.bak +1893 -0
- package/plugins/specweave/agents/pm/AGENT.md.bak2 +1754 -0
- package/plugins/specweave/commands/check-hooks.md +257 -0
- package/plugins/specweave/commands/migrate-config.md +104 -0
- package/plugins/specweave/hooks/post-edit-spec.sh +202 -31
- package/plugins/specweave/hooks/post-task-completion.sh +225 -228
- package/plugins/specweave/hooks/post-write-spec.sh +207 -31
- package/plugins/specweave/hooks/pre-edit-spec.sh +151 -0
- package/plugins/specweave/hooks/pre-task-completion.sh +5 -7
- package/plugins/specweave/hooks/pre-write-spec.sh +151 -0
- package/plugins/specweave/hooks/test-pretooluse-env.sh +72 -0
- package/plugins/specweave/skills/compliance-architecture/SKILL.md +374 -0
- package/plugins/specweave/skills/external-sync-wizard/SKILL.md +610 -0
- package/plugins/specweave/skills/pm-closure-validation/SKILL.md +541 -0
- package/plugins/specweave/skills/roadmap-planner/SKILL.md +473 -0
- package/plugins/specweave-ado/commands/refresh-cache.js +25 -0
- package/plugins/specweave-ado/commands/refresh-cache.ts +40 -0
- package/plugins/specweave-ado/hooks/post-task-completion.sh +1 -1
- package/plugins/specweave-github/hooks/post-task-completion.sh +1 -1
- package/plugins/specweave-jira/commands/refresh-cache.js +25 -0
- package/plugins/specweave-jira/commands/refresh-cache.ts +40 -0
- package/plugins/specweave-jira/hooks/post-task-completion.sh +1 -1
- package/plugins/specweave-kafka-streams/commands/topology.md +437 -0
- package/plugins/specweave-n8n/commands/workflow-template.md +262 -0
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +228 -6333
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migrate Config Command
|
|
3
|
+
*
|
|
4
|
+
* Migrates old .env-only configuration to split secrets/config format
|
|
5
|
+
*
|
|
6
|
+
* @module cli/commands/migrate-config
|
|
7
|
+
*/
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import inquirer from 'inquirer';
|
|
10
|
+
import ora from 'ora';
|
|
11
|
+
import { ConfigMigrator } from '../../core/config/config-migrator.js';
|
|
12
|
+
/**
|
|
13
|
+
* Migrate configuration from .env-only to split format
|
|
14
|
+
*
|
|
15
|
+
* @param options - Command options
|
|
16
|
+
*/
|
|
17
|
+
export async function migrateConfig(options = {}) {
|
|
18
|
+
const { dryRun = false, force = false, yes = false } = options;
|
|
19
|
+
console.log(chalk.bold.cyan('\n🔄 SpecWeave Configuration Migration\n'));
|
|
20
|
+
console.log(chalk.gray('Migrating from .env-only to split secrets/config format\n'));
|
|
21
|
+
const migrator = new ConfigMigrator(process.cwd());
|
|
22
|
+
// Check if migration is needed
|
|
23
|
+
if (!force) {
|
|
24
|
+
const spinner = ora('Checking if migration is needed...').start();
|
|
25
|
+
const needed = await migrator.needsMigration();
|
|
26
|
+
if (!needed) {
|
|
27
|
+
spinner.succeed('Migration not needed');
|
|
28
|
+
console.log(chalk.green('\n✅ Your project already uses the split format!'));
|
|
29
|
+
console.log(chalk.gray(' Secrets are in .env, config is in .specweave/config.json\n'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
spinner.succeed('Migration needed - old format detected');
|
|
33
|
+
}
|
|
34
|
+
// Preview migration
|
|
35
|
+
console.log(chalk.bold('\n📋 Migration Preview\n'));
|
|
36
|
+
const spinner = ora('Analyzing .env file...').start();
|
|
37
|
+
let preview;
|
|
38
|
+
try {
|
|
39
|
+
preview = await migrator.preview();
|
|
40
|
+
spinner.succeed('Analysis complete');
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
spinner.fail('Analysis failed');
|
|
44
|
+
console.error(chalk.red(`\n❌ Error: ${error.message}\n`));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// Display classification results
|
|
48
|
+
console.log(chalk.bold('\nClassification Results:'));
|
|
49
|
+
console.log(chalk.green(` Secrets: ${preview.secretsCount} variables`));
|
|
50
|
+
console.log(chalk.blue(` Config: ${preview.configCount} variables`));
|
|
51
|
+
if (preview.warnings.length > 0) {
|
|
52
|
+
console.log(chalk.yellow('\n⚠️ Warnings:'));
|
|
53
|
+
preview.warnings.forEach(w => console.log(chalk.yellow(` - ${w}`)));
|
|
54
|
+
}
|
|
55
|
+
// Show detailed breakdown
|
|
56
|
+
console.log(chalk.bold('\n📊 Detailed Breakdown:\n'));
|
|
57
|
+
const secretVars = preview.classified.filter(c => c.type === 'secret');
|
|
58
|
+
const configVars = preview.classified.filter(c => c.type === 'config');
|
|
59
|
+
if (secretVars.length > 0) {
|
|
60
|
+
console.log(chalk.green.bold(' Secrets (will stay in .env):'));
|
|
61
|
+
secretVars.forEach(v => {
|
|
62
|
+
const maskedValue = maskSecret(v.value);
|
|
63
|
+
console.log(chalk.gray(` ${v.key}=${maskedValue}`));
|
|
64
|
+
console.log(chalk.gray(` └─ ${v.reason}\n`));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (configVars.length > 0) {
|
|
68
|
+
console.log(chalk.blue.bold(' Configuration (will move to config.json):'));
|
|
69
|
+
configVars.forEach(v => {
|
|
70
|
+
console.log(chalk.gray(` ${v.key}=${v.value}`));
|
|
71
|
+
console.log(chalk.gray(` └─ ${v.reason}\n`));
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// Dry run mode - stop here
|
|
75
|
+
if (dryRun) {
|
|
76
|
+
console.log(chalk.yellow('\n🔍 Dry run mode - no changes made\n'));
|
|
77
|
+
console.log(chalk.gray('Run without --dry-run to perform migration\n'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Confirm migration
|
|
81
|
+
if (!yes) {
|
|
82
|
+
console.log(chalk.bold('\n❓ Migration Actions:\n'));
|
|
83
|
+
console.log(chalk.gray(' 1. Backup .env → .env.backup.TIMESTAMP'));
|
|
84
|
+
console.log(chalk.gray(' 2. Update .env (keep only secrets)'));
|
|
85
|
+
console.log(chalk.gray(' 3. Create/update .specweave/config.json (add config)'));
|
|
86
|
+
console.log(chalk.gray(' 4. Generate .env.example (team template)\n'));
|
|
87
|
+
const { confirm } = await inquirer.prompt([
|
|
88
|
+
{
|
|
89
|
+
type: 'confirm',
|
|
90
|
+
name: 'confirm',
|
|
91
|
+
message: 'Proceed with migration?',
|
|
92
|
+
default: false
|
|
93
|
+
}
|
|
94
|
+
]);
|
|
95
|
+
if (!confirm) {
|
|
96
|
+
console.log(chalk.yellow('\n⚠️ Migration cancelled\n'));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Perform migration
|
|
101
|
+
console.log(chalk.bold('\n🚀 Performing Migration...\n'));
|
|
102
|
+
const migrationSpinner = ora('Migrating configuration...').start();
|
|
103
|
+
let result;
|
|
104
|
+
try {
|
|
105
|
+
result = await migrator.migrate({ backup: true, force });
|
|
106
|
+
migrationSpinner.succeed('Migration complete');
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
migrationSpinner.fail('Migration failed');
|
|
110
|
+
console.error(chalk.red(`\n❌ Error: ${error.message}\n`));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Display results
|
|
114
|
+
console.log(chalk.bold.green('\n✅ Migration Successful!\n'));
|
|
115
|
+
console.log(chalk.bold('Summary:'));
|
|
116
|
+
console.log(chalk.green(` ✓ ${result.secretsCount} secrets kept in .env`));
|
|
117
|
+
console.log(chalk.blue(` ✓ ${result.configCount} config items moved to config.json`));
|
|
118
|
+
if (result.backupPath) {
|
|
119
|
+
console.log(chalk.gray(` ✓ Backup created: ${result.backupPath}`));
|
|
120
|
+
}
|
|
121
|
+
console.log(chalk.gray(` ✓ .env.example generated\n`));
|
|
122
|
+
// Show next steps
|
|
123
|
+
console.log(chalk.bold('📝 Next Steps:\n'));
|
|
124
|
+
console.log(chalk.cyan(' 1. Review .specweave/config.json (commit to git)'));
|
|
125
|
+
console.log(chalk.cyan(' 2. Share .env.example with team (commit to git)'));
|
|
126
|
+
console.log(chalk.cyan(' 3. Team members: cp .env.example .env (fill in tokens)'));
|
|
127
|
+
console.log(chalk.cyan(' 4. Verify: specweave init . (should detect existing config)\n'));
|
|
128
|
+
console.log(chalk.bold('🎯 Benefits:\n'));
|
|
129
|
+
console.log(chalk.green(' ✓ Team shares configuration via git'));
|
|
130
|
+
console.log(chalk.green(' ✓ Secrets stay local (never committed)'));
|
|
131
|
+
console.log(chalk.green(' ✓ Type-safe configuration with validation'));
|
|
132
|
+
console.log(chalk.green(' ✓ Easy onboarding for new team members\n'));
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Mask secret value for display
|
|
136
|
+
*
|
|
137
|
+
* @param value - Secret value
|
|
138
|
+
* @returns Masked value
|
|
139
|
+
*/
|
|
140
|
+
function maskSecret(value) {
|
|
141
|
+
if (value.length <= 8) {
|
|
142
|
+
return '***';
|
|
143
|
+
}
|
|
144
|
+
const visible = 4;
|
|
145
|
+
const start = value.substring(0, visible);
|
|
146
|
+
const end = value.substring(value.length - visible);
|
|
147
|
+
return `${start}${'*'.repeat(value.length - visible * 2)}${end}`;
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=migrate-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate-config.js","sourceRoot":"","sources":["../../../../src/cli/commands/migrate-config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async Project Loader
|
|
3
|
+
*
|
|
4
|
+
* Advanced batch fetching with:
|
|
5
|
+
* - Smart pagination (50-project batches)
|
|
6
|
+
* - Real-time progress tracking
|
|
7
|
+
* - Graceful cancelation (Ctrl+C)
|
|
8
|
+
* - Retry logic with exponential backoff
|
|
9
|
+
* - Rate limit handling
|
|
10
|
+
* - Graceful degradation (reduce batch size on timeout)
|
|
11
|
+
* - Continue-on-failure error handling
|
|
12
|
+
* - Comprehensive error logging
|
|
13
|
+
*
|
|
14
|
+
* @module cli/helpers/async-project-loader
|
|
15
|
+
*/
|
|
16
|
+
import { Logger } from '../../utils/logger.js';
|
|
17
|
+
import type { JiraCredentials, AdoCredentials } from './project-count-fetcher.js';
|
|
18
|
+
/**
|
|
19
|
+
* Project provider type
|
|
20
|
+
*/
|
|
21
|
+
export type ProjectProvider = 'jira' | 'ado';
|
|
22
|
+
/**
|
|
23
|
+
* Project metadata
|
|
24
|
+
*/
|
|
25
|
+
export interface Project {
|
|
26
|
+
id: string;
|
|
27
|
+
key: string;
|
|
28
|
+
name: string;
|
|
29
|
+
projectTypeKey?: string;
|
|
30
|
+
simplified?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Fetch options
|
|
34
|
+
*/
|
|
35
|
+
export interface FetchOptions {
|
|
36
|
+
batchSize?: number;
|
|
37
|
+
updateFrequency?: number;
|
|
38
|
+
showEta?: boolean;
|
|
39
|
+
stateFile?: string;
|
|
40
|
+
errorLogFile?: string;
|
|
41
|
+
logger?: Logger;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Fetch result
|
|
45
|
+
*/
|
|
46
|
+
export interface FetchResult {
|
|
47
|
+
projects: Project[];
|
|
48
|
+
succeeded: number;
|
|
49
|
+
failed: number;
|
|
50
|
+
skipped: number;
|
|
51
|
+
errors: FetchError[];
|
|
52
|
+
canceled?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Fetch error with context
|
|
56
|
+
*/
|
|
57
|
+
export interface FetchError {
|
|
58
|
+
projectKey: string;
|
|
59
|
+
error: string;
|
|
60
|
+
timestamp: string;
|
|
61
|
+
suggestion: string;
|
|
62
|
+
retryAttempts?: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Async Project Loader
|
|
66
|
+
*
|
|
67
|
+
* Handles batch fetching from issue trackers with advanced features:
|
|
68
|
+
* - Smart pagination (50-project batches for optimal performance)
|
|
69
|
+
* - Real-time progress tracking with ETA
|
|
70
|
+
* - Graceful cancelation with state persistence
|
|
71
|
+
* - Retry logic with exponential backoff (1s, 2s, 4s)
|
|
72
|
+
* - Rate limit detection and throttling
|
|
73
|
+
* - Graceful degradation (reduce batch size on timeout)
|
|
74
|
+
* - Continue-on-failure (single error doesn't block batch)
|
|
75
|
+
* - Comprehensive error logging
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* const loader = new AsyncProjectLoader(credentials, 'jira', {
|
|
80
|
+
* batchSize: 50,
|
|
81
|
+
* showEta: true
|
|
82
|
+
* });
|
|
83
|
+
*
|
|
84
|
+
* const result = await loader.fetchAllProjects(127);
|
|
85
|
+
* console.log(`Imported ${result.succeeded}/${result.projects.length} projects`);
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export declare class AsyncProjectLoader {
|
|
89
|
+
private credentials;
|
|
90
|
+
private provider;
|
|
91
|
+
private options;
|
|
92
|
+
private progressTracker?;
|
|
93
|
+
private cancelHandler?;
|
|
94
|
+
private currentBatchSize;
|
|
95
|
+
constructor(credentials: JiraCredentials | AdoCredentials, provider: ProjectProvider, options?: FetchOptions);
|
|
96
|
+
/**
|
|
97
|
+
* Fetch all projects with smart pagination
|
|
98
|
+
*
|
|
99
|
+
* @param totalCount - Total number of projects to fetch
|
|
100
|
+
* @returns Fetch result with projects and statistics
|
|
101
|
+
*/
|
|
102
|
+
fetchAllProjects(totalCount: number): Promise<FetchResult>;
|
|
103
|
+
/**
|
|
104
|
+
* Fetch single batch with pagination
|
|
105
|
+
*
|
|
106
|
+
* @param offset - Starting offset
|
|
107
|
+
* @param limit - Number of projects to fetch
|
|
108
|
+
* @returns Array of projects
|
|
109
|
+
*/
|
|
110
|
+
fetchBatch(offset: number, limit: number): Promise<Project[]>;
|
|
111
|
+
/**
|
|
112
|
+
* Fetch JIRA batch with pagination
|
|
113
|
+
*/
|
|
114
|
+
private fetchJiraBatch;
|
|
115
|
+
/**
|
|
116
|
+
* Fetch Azure DevOps batch with pagination
|
|
117
|
+
*/
|
|
118
|
+
private fetchAdoBatch;
|
|
119
|
+
/**
|
|
120
|
+
* Fetch batch with retry logic and exponential backoff
|
|
121
|
+
*/
|
|
122
|
+
private fetchBatchWithRetry;
|
|
123
|
+
/**
|
|
124
|
+
* Check rate limit headers and throttle if needed
|
|
125
|
+
*/
|
|
126
|
+
private checkRateLimit;
|
|
127
|
+
/**
|
|
128
|
+
* Save partial state on cancelation
|
|
129
|
+
*/
|
|
130
|
+
private savePartialState;
|
|
131
|
+
/**
|
|
132
|
+
* Log error to file
|
|
133
|
+
*/
|
|
134
|
+
private logError;
|
|
135
|
+
/**
|
|
136
|
+
* Get suggestion for error
|
|
137
|
+
*/
|
|
138
|
+
private getSuggestion;
|
|
139
|
+
/**
|
|
140
|
+
* Check if error is auth-related (4XX)
|
|
141
|
+
*/
|
|
142
|
+
private isAuthError;
|
|
143
|
+
/**
|
|
144
|
+
* Check if error is timeout-related
|
|
145
|
+
*/
|
|
146
|
+
private isTimeoutError;
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=async-project-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-project-loader.d.ts","sourceRoot":"","sources":["../../../../src/cli/helpers/async-project-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,OAAO,EAAE,MAAM,EAAiB,MAAM,uBAAuB,CAAC;AAG9D,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAoB,MAAM,4BAA4B,CAAC;AAEpG;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,KAAK,CAAC;AAE7C;;GAEG;AACH,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,WAAW,CAAmC;IACtD,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,OAAO,CAAyB;IAExC,OAAO,CAAC,eAAe,CAAC,CAAkB;IAC1C,OAAO,CAAC,aAAa,CAAC,CAAqB;IAE3C,OAAO,CAAC,gBAAgB,CAAS;gBAG/B,WAAW,EAAE,eAAe,GAAG,cAAc,EAC7C,QAAQ,EAAE,eAAe,EACzB,OAAO,GAAE,YAAiB;IAkB5B;;;;;OAKG;IACG,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAwGhE;;;;;;OAMG;IACG,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAUnE;;OAEG;YACW,cAAc;IAwC5B;;OAEG;YACW,aAAa;IA8B3B;;OAEG;YACW,mBAAmB;IA4BjC;;OAEG;YACW,cAAc;IAiB5B;;OAEG;YACW,gBAAgB;IAgC9B;;OAEG;YACW,QAAQ;IAgBtB;;OAEG;IACH,OAAO,CAAC,aAAa;IA0BrB;;OAEG;IACH,OAAO,CAAC,WAAW;IAKnB;;OAEG;IACH,OAAO,CAAC,cAAc;CAKvB"}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async Project Loader
|
|
3
|
+
*
|
|
4
|
+
* Advanced batch fetching with:
|
|
5
|
+
* - Smart pagination (50-project batches)
|
|
6
|
+
* - Real-time progress tracking
|
|
7
|
+
* - Graceful cancelation (Ctrl+C)
|
|
8
|
+
* - Retry logic with exponential backoff
|
|
9
|
+
* - Rate limit handling
|
|
10
|
+
* - Graceful degradation (reduce batch size on timeout)
|
|
11
|
+
* - Continue-on-failure error handling
|
|
12
|
+
* - Comprehensive error logging
|
|
13
|
+
*
|
|
14
|
+
* @module cli/helpers/async-project-loader
|
|
15
|
+
*/
|
|
16
|
+
import { promises as fs } from 'fs';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import chalk from 'chalk';
|
|
19
|
+
import { consoleLogger } from '../../utils/logger.js';
|
|
20
|
+
import { ProgressTracker } from './progress-tracker.js';
|
|
21
|
+
import { CancelationHandler } from './cancelation-handler.js';
|
|
22
|
+
/**
|
|
23
|
+
* Async Project Loader
|
|
24
|
+
*
|
|
25
|
+
* Handles batch fetching from issue trackers with advanced features:
|
|
26
|
+
* - Smart pagination (50-project batches for optimal performance)
|
|
27
|
+
* - Real-time progress tracking with ETA
|
|
28
|
+
* - Graceful cancelation with state persistence
|
|
29
|
+
* - Retry logic with exponential backoff (1s, 2s, 4s)
|
|
30
|
+
* - Rate limit detection and throttling
|
|
31
|
+
* - Graceful degradation (reduce batch size on timeout)
|
|
32
|
+
* - Continue-on-failure (single error doesn't block batch)
|
|
33
|
+
* - Comprehensive error logging
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const loader = new AsyncProjectLoader(credentials, 'jira', {
|
|
38
|
+
* batchSize: 50,
|
|
39
|
+
* showEta: true
|
|
40
|
+
* });
|
|
41
|
+
*
|
|
42
|
+
* const result = await loader.fetchAllProjects(127);
|
|
43
|
+
* console.log(`Imported ${result.succeeded}/${result.projects.length} projects`);
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export class AsyncProjectLoader {
|
|
47
|
+
constructor(credentials, provider, options = {}) {
|
|
48
|
+
this.credentials = credentials;
|
|
49
|
+
this.provider = provider;
|
|
50
|
+
// Set defaults
|
|
51
|
+
this.options = {
|
|
52
|
+
batchSize: options.batchSize ?? 50,
|
|
53
|
+
updateFrequency: options.updateFrequency ?? 5,
|
|
54
|
+
showEta: options.showEta ?? true,
|
|
55
|
+
stateFile: options.stateFile ?? '.specweave/cache/import-state.json',
|
|
56
|
+
errorLogFile: options.errorLogFile ?? '.specweave/logs/import-errors.log',
|
|
57
|
+
logger: options.logger ?? consoleLogger
|
|
58
|
+
};
|
|
59
|
+
this.currentBatchSize = this.options.batchSize;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Fetch all projects with smart pagination
|
|
63
|
+
*
|
|
64
|
+
* @param totalCount - Total number of projects to fetch
|
|
65
|
+
* @returns Fetch result with projects and statistics
|
|
66
|
+
*/
|
|
67
|
+
async fetchAllProjects(totalCount) {
|
|
68
|
+
const projects = [];
|
|
69
|
+
const errors = [];
|
|
70
|
+
// Initialize progress tracker
|
|
71
|
+
this.progressTracker = new ProgressTracker({
|
|
72
|
+
total: totalCount,
|
|
73
|
+
updateFrequency: this.options.updateFrequency,
|
|
74
|
+
showEta: this.options.showEta,
|
|
75
|
+
logger: this.options.logger
|
|
76
|
+
});
|
|
77
|
+
// Initialize cancelation handler
|
|
78
|
+
this.cancelHandler = new CancelationHandler({
|
|
79
|
+
stateFile: this.options.stateFile,
|
|
80
|
+
logger: this.options.logger
|
|
81
|
+
});
|
|
82
|
+
// Register cleanup callback (save state on Ctrl+C)
|
|
83
|
+
this.cancelHandler.onCleanup(async () => {
|
|
84
|
+
await this.savePartialState(projects, totalCount, errors);
|
|
85
|
+
});
|
|
86
|
+
// Main fetch loop
|
|
87
|
+
let offset = 0;
|
|
88
|
+
while (offset < totalCount) {
|
|
89
|
+
// Check for cancelation
|
|
90
|
+
if (this.cancelHandler.shouldCancel()) {
|
|
91
|
+
this.progressTracker.cancel();
|
|
92
|
+
this.cancelHandler.suggestResume('/specweave-jira:import-projects --resume');
|
|
93
|
+
return {
|
|
94
|
+
projects,
|
|
95
|
+
succeeded: this.progressTracker.getSummary().succeeded,
|
|
96
|
+
failed: this.progressTracker.getSummary().failed,
|
|
97
|
+
skipped: this.progressTracker.getSummary().skipped,
|
|
98
|
+
errors,
|
|
99
|
+
canceled: true
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// Calculate batch size (last batch may be smaller)
|
|
103
|
+
const limit = Math.min(this.currentBatchSize, totalCount - offset);
|
|
104
|
+
try {
|
|
105
|
+
// Fetch batch with retry logic
|
|
106
|
+
const batch = await this.fetchBatchWithRetry(offset, limit);
|
|
107
|
+
projects.push(...batch);
|
|
108
|
+
// Update progress for each item in batch
|
|
109
|
+
batch.forEach(project => {
|
|
110
|
+
this.progressTracker.update(project.key, 'success');
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
// Continue-on-failure: Log error, skip batch, continue
|
|
115
|
+
const batchKey = `BATCH_${offset}-${offset + limit}`;
|
|
116
|
+
const fetchError = {
|
|
117
|
+
projectKey: batchKey,
|
|
118
|
+
error: error.message,
|
|
119
|
+
timestamp: new Date().toISOString(),
|
|
120
|
+
suggestion: this.getSuggestion(error),
|
|
121
|
+
retryAttempts: 3
|
|
122
|
+
};
|
|
123
|
+
errors.push(fetchError);
|
|
124
|
+
await this.logError(fetchError);
|
|
125
|
+
// Update progress (mark all as failed)
|
|
126
|
+
for (let i = 0; i < limit; i++) {
|
|
127
|
+
this.progressTracker.update(`${batchKey}_${i}`, 'failure');
|
|
128
|
+
}
|
|
129
|
+
// Check if we should reduce batch size (graceful degradation)
|
|
130
|
+
if (this.isTimeoutError(error) && this.currentBatchSize > 10) {
|
|
131
|
+
const oldSize = this.currentBatchSize;
|
|
132
|
+
this.currentBatchSize = Math.max(10, Math.floor(this.currentBatchSize / 2));
|
|
133
|
+
this.options.logger.log(chalk.yellow(`⚠️ Timeout detected. Reducing batch size: ${oldSize} → ${this.currentBatchSize}`));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
offset += limit;
|
|
137
|
+
}
|
|
138
|
+
// Finish progress tracking
|
|
139
|
+
this.progressTracker.finish();
|
|
140
|
+
// Clear cancelation state (successful completion)
|
|
141
|
+
await this.cancelHandler.clearState();
|
|
142
|
+
// Cleanup
|
|
143
|
+
this.cancelHandler.dispose();
|
|
144
|
+
return {
|
|
145
|
+
projects,
|
|
146
|
+
succeeded: this.progressTracker.getSummary().succeeded,
|
|
147
|
+
failed: this.progressTracker.getSummary().failed,
|
|
148
|
+
skipped: this.progressTracker.getSummary().skipped,
|
|
149
|
+
errors
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Fetch single batch with pagination
|
|
154
|
+
*
|
|
155
|
+
* @param offset - Starting offset
|
|
156
|
+
* @param limit - Number of projects to fetch
|
|
157
|
+
* @returns Array of projects
|
|
158
|
+
*/
|
|
159
|
+
async fetchBatch(offset, limit) {
|
|
160
|
+
if (this.provider === 'jira') {
|
|
161
|
+
return this.fetchJiraBatch(offset, limit);
|
|
162
|
+
}
|
|
163
|
+
else if (this.provider === 'ado') {
|
|
164
|
+
return this.fetchAdoBatch(offset, limit);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
throw new Error(`Unsupported provider: ${this.provider}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Fetch JIRA batch with pagination
|
|
172
|
+
*/
|
|
173
|
+
async fetchJiraBatch(offset, limit) {
|
|
174
|
+
const creds = this.credentials;
|
|
175
|
+
const { domain, email, token, instanceType } = creds;
|
|
176
|
+
const apiVersion = instanceType === 'cloud' ? '3' : '2';
|
|
177
|
+
const endpoint = instanceType === 'cloud'
|
|
178
|
+
? `/rest/api/${apiVersion}/project/search?startAt=${offset}&maxResults=${limit}`
|
|
179
|
+
: `/rest/api/${apiVersion}/project?startAt=${offset}&maxResults=${limit}`;
|
|
180
|
+
const url = `https://${domain}${endpoint}`;
|
|
181
|
+
const response = await fetch(url, {
|
|
182
|
+
method: 'GET',
|
|
183
|
+
headers: {
|
|
184
|
+
Authorization: `Basic ${Buffer.from(`${email}:${token}`).toString('base64')}`,
|
|
185
|
+
Accept: 'application/json',
|
|
186
|
+
'Content-Type': 'application/json'
|
|
187
|
+
},
|
|
188
|
+
signal: AbortSignal.timeout(30000) // 30 second timeout
|
|
189
|
+
});
|
|
190
|
+
// Check rate limit
|
|
191
|
+
await this.checkRateLimit(response.headers);
|
|
192
|
+
if (!response.ok) {
|
|
193
|
+
const errorBody = await response.text().catch(() => 'No error body');
|
|
194
|
+
throw new Error(`JIRA API error: ${response.status} ${response.statusText}. ${errorBody}`);
|
|
195
|
+
}
|
|
196
|
+
if (instanceType === 'cloud') {
|
|
197
|
+
const data = (await response.json());
|
|
198
|
+
return data.values;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// Server returns array directly
|
|
202
|
+
return (await response.json());
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Fetch Azure DevOps batch with pagination
|
|
207
|
+
*/
|
|
208
|
+
async fetchAdoBatch(offset, limit) {
|
|
209
|
+
const creds = this.credentials;
|
|
210
|
+
const { organization, pat } = creds;
|
|
211
|
+
const url = `https://dev.azure.com/${organization}/_apis/projects?$top=${limit}&$skip=${offset}&api-version=7.0`;
|
|
212
|
+
const response = await fetch(url, {
|
|
213
|
+
method: 'GET',
|
|
214
|
+
headers: {
|
|
215
|
+
Authorization: `Basic ${Buffer.from(`:${pat}`).toString('base64')}`,
|
|
216
|
+
Accept: 'application/json'
|
|
217
|
+
},
|
|
218
|
+
signal: AbortSignal.timeout(30000) // 30 second timeout
|
|
219
|
+
});
|
|
220
|
+
if (!response.ok) {
|
|
221
|
+
const errorBody = await response.text().catch(() => 'No error body');
|
|
222
|
+
throw new Error(`Azure DevOps API error: ${response.status} ${response.statusText}. ${errorBody}`);
|
|
223
|
+
}
|
|
224
|
+
const data = (await response.json());
|
|
225
|
+
return data.value.map(p => ({
|
|
226
|
+
id: p.id,
|
|
227
|
+
key: p.name.toUpperCase().replace(/\s+/g, '-'),
|
|
228
|
+
name: p.name
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Fetch batch with retry logic and exponential backoff
|
|
233
|
+
*/
|
|
234
|
+
async fetchBatchWithRetry(offset, limit) {
|
|
235
|
+
const delays = [1000, 2000, 4000]; // 1s, 2s, 4s
|
|
236
|
+
let lastError = null;
|
|
237
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
238
|
+
try {
|
|
239
|
+
return await this.fetchBatch(offset, limit);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
lastError = error;
|
|
243
|
+
// Don't retry on auth failures (4XX except 429)
|
|
244
|
+
if (this.isAuthError(error) && !error.message.includes('429')) {
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
// Retry on network errors, 5XX, or 429 errors
|
|
248
|
+
if (attempt < 2) {
|
|
249
|
+
this.options.logger.log(chalk.yellow(`⚠️ Retry ${attempt + 1}/3 after ${delays[attempt]}ms... (${error.message})`));
|
|
250
|
+
await new Promise(resolve => setTimeout(resolve, delays[attempt]));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
throw lastError || new Error('Failed to fetch batch after 3 attempts');
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Check rate limit headers and throttle if needed
|
|
258
|
+
*/
|
|
259
|
+
async checkRateLimit(headers) {
|
|
260
|
+
const remaining = headers.get('X-RateLimit-Remaining');
|
|
261
|
+
const reset = headers.get('X-RateLimit-Reset');
|
|
262
|
+
if (remaining && parseInt(remaining, 10) < 10) {
|
|
263
|
+
const resetTime = reset ? parseInt(reset, 10) * 1000 : Date.now() + 60000;
|
|
264
|
+
const waitMs = Math.max(0, resetTime - Date.now());
|
|
265
|
+
const waitSec = Math.ceil(waitMs / 1000);
|
|
266
|
+
this.options.logger.log(chalk.yellow(`⚠️ Rate limit threshold reached (${remaining} requests remaining). Pausing ${waitSec}s...`));
|
|
267
|
+
await new Promise(resolve => setTimeout(resolve, waitMs + 1000)); // Add 1s buffer
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Save partial state on cancelation
|
|
272
|
+
*/
|
|
273
|
+
async savePartialState(projects, total, errors) {
|
|
274
|
+
const summary = this.progressTracker.getSummary();
|
|
275
|
+
// Calculate remaining projects
|
|
276
|
+
const completed = projects.length;
|
|
277
|
+
const remaining = [];
|
|
278
|
+
// Note: We don't have the list of all projects here, so remaining will be empty
|
|
279
|
+
// In a real implementation, this would be populated from the initial project list
|
|
280
|
+
const state = {
|
|
281
|
+
operation: `${this.provider}-import`,
|
|
282
|
+
provider: this.provider,
|
|
283
|
+
domain: 'credentials' in this.credentials ? this.credentials.domain : undefined,
|
|
284
|
+
timestamp: new Date().toISOString(),
|
|
285
|
+
version: '1.0',
|
|
286
|
+
total,
|
|
287
|
+
completed,
|
|
288
|
+
succeeded: summary.succeeded,
|
|
289
|
+
failed: summary.failed,
|
|
290
|
+
skipped: summary.skipped,
|
|
291
|
+
remaining,
|
|
292
|
+
errors
|
|
293
|
+
};
|
|
294
|
+
await this.cancelHandler.saveState(state);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Log error to file
|
|
298
|
+
*/
|
|
299
|
+
async logError(error) {
|
|
300
|
+
try {
|
|
301
|
+
// Ensure log directory exists
|
|
302
|
+
const logDir = path.dirname(this.options.errorLogFile);
|
|
303
|
+
await fs.mkdir(logDir, { recursive: true });
|
|
304
|
+
// Format log entry
|
|
305
|
+
const logEntry = `[${error.timestamp}] ${error.projectKey}: ${error.error} (${error.suggestion})\n`;
|
|
306
|
+
// Append to log file
|
|
307
|
+
await fs.appendFile(this.options.errorLogFile, logEntry, 'utf-8');
|
|
308
|
+
}
|
|
309
|
+
catch (logError) {
|
|
310
|
+
this.options.logger.error('Failed to write error log:', logError);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Get suggestion for error
|
|
315
|
+
*/
|
|
316
|
+
getSuggestion(error) {
|
|
317
|
+
const message = error.message || '';
|
|
318
|
+
if (message.includes('401') || message.includes('403')) {
|
|
319
|
+
return 'Check credentials and project permissions';
|
|
320
|
+
}
|
|
321
|
+
if (message.includes('404')) {
|
|
322
|
+
return 'Project may have been deleted or archived';
|
|
323
|
+
}
|
|
324
|
+
if (message.includes('429')) {
|
|
325
|
+
return 'Rate limit exceeded (throttling applied)';
|
|
326
|
+
}
|
|
327
|
+
if (message.includes('ETIMEDOUT') || message.includes('timeout')) {
|
|
328
|
+
return 'Network timeout (try again or reduce batch size)';
|
|
329
|
+
}
|
|
330
|
+
if (message.includes('5')) {
|
|
331
|
+
return 'API server error (retrying with backoff)';
|
|
332
|
+
}
|
|
333
|
+
return 'Unknown error (check logs for details)';
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Check if error is auth-related (4XX)
|
|
337
|
+
*/
|
|
338
|
+
isAuthError(error) {
|
|
339
|
+
const message = error.message || '';
|
|
340
|
+
return /\b40[0134]\b/.test(message);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Check if error is timeout-related
|
|
344
|
+
*/
|
|
345
|
+
isTimeoutError(error) {
|
|
346
|
+
const message = error.message || '';
|
|
347
|
+
const code = error.code || '';
|
|
348
|
+
return message.includes('timeout') || code === 'ETIMEDOUT' || message.includes('ETIMEDOUT');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
//# sourceMappingURL=async-project-loader.js.map
|