specweave 0.32.2 → 0.32.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.
- package/CLAUDE.md +39 -0
- package/bin/specweave.js +34 -0
- package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.d.ts +100 -0
- package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.d.ts.map +1 -0
- package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.js +291 -0
- package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.d.ts +103 -0
- package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.js +310 -0
- package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-permission-gate.d.ts +126 -0
- package/dist/plugins/specweave-jira/lib/jira-permission-gate.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-permission-gate.js +207 -0
- package/dist/plugins/specweave-jira/lib/jira-permission-gate.js.map +1 -0
- package/dist/src/adapters/codex/README.md +1 -1
- package/dist/src/adapters/codex/adapter.js +1 -1
- package/dist/src/cli/commands/archive.d.ts +2 -0
- package/dist/src/cli/commands/archive.d.ts.map +1 -1
- package/dist/src/cli/commands/archive.js +33 -0
- package/dist/src/cli/commands/archive.js.map +1 -1
- package/dist/src/cli/commands/context.d.ts +92 -0
- package/dist/src/cli/commands/context.d.ts.map +1 -0
- package/dist/src/cli/commands/context.js +205 -0
- package/dist/src/cli/commands/context.js.map +1 -0
- package/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +111 -69
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/helpers/init/external-import.d.ts +3 -0
- package/dist/src/cli/helpers/init/external-import.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/external-import.js +17 -4
- package/dist/src/cli/helpers/init/external-import.js.map +1 -1
- package/dist/src/cli/helpers/init/index.d.ts +1 -0
- package/dist/src/cli/helpers/init/index.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/index.js +2 -0
- package/dist/src/cli/helpers/init/index.js.map +1 -1
- package/dist/src/cli/helpers/init/jira-ado-auto-detect.d.ts +70 -0
- package/dist/src/cli/helpers/init/jira-ado-auto-detect.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/jira-ado-auto-detect.js +214 -4
- package/dist/src/cli/helpers/init/jira-ado-auto-detect.js.map +1 -1
- package/dist/src/cli/helpers/init/living-docs-preflight.d.ts +4 -0
- package/dist/src/cli/helpers/init/living-docs-preflight.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/living-docs-preflight.js +34 -3
- package/dist/src/cli/helpers/init/living-docs-preflight.js.map +1 -1
- package/dist/src/cli/helpers/init/testing-config.d.ts +3 -0
- package/dist/src/cli/helpers/init/testing-config.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/testing-config.js +9 -2
- package/dist/src/cli/helpers/init/testing-config.js.map +1 -1
- package/dist/src/cli/helpers/init/translation-config.d.ts +3 -0
- package/dist/src/cli/helpers/init/translation-config.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/translation-config.js +21 -4
- package/dist/src/cli/helpers/init/translation-config.js.map +1 -1
- package/dist/src/cli/helpers/init/wizard-navigation.d.ts +45 -0
- package/dist/src/cli/helpers/init/wizard-navigation.d.ts.map +1 -0
- package/dist/src/cli/helpers/init/wizard-navigation.js +97 -0
- package/dist/src/cli/helpers/init/wizard-navigation.js.map +1 -0
- package/dist/src/core/increment/increment-archiver.d.ts +25 -4
- package/dist/src/core/increment/increment-archiver.d.ts.map +1 -1
- package/dist/src/core/increment/increment-archiver.js +64 -20
- package/dist/src/core/increment/increment-archiver.js.map +1 -1
- package/dist/src/core/increment/increment-utils.d.ts +65 -0
- package/dist/src/core/increment/increment-utils.d.ts.map +1 -1
- package/dist/src/core/increment/increment-utils.js +114 -0
- package/dist/src/core/increment/increment-utils.js.map +1 -1
- package/dist/src/core/living-docs/feature-archiver.d.ts +4 -0
- package/dist/src/core/living-docs/feature-archiver.d.ts.map +1 -1
- package/dist/src/core/living-docs/feature-archiver.js +32 -10
- package/dist/src/core/living-docs/feature-archiver.js.map +1 -1
- package/dist/src/core/living-docs/feature-id-manager.d.ts.map +1 -1
- package/dist/src/core/living-docs/feature-id-manager.js +7 -3
- package/dist/src/core/living-docs/feature-id-manager.js.map +1 -1
- package/dist/src/core/living-docs/governance/ecosystem-detector.d.ts +38 -0
- package/dist/src/core/living-docs/governance/ecosystem-detector.d.ts.map +1 -0
- package/dist/src/core/living-docs/governance/ecosystem-detector.js +325 -0
- package/dist/src/core/living-docs/governance/ecosystem-detector.js.map +1 -0
- package/dist/src/core/living-docs/governance/frontend-standards-parser.d.ts +74 -0
- package/dist/src/core/living-docs/governance/frontend-standards-parser.d.ts.map +1 -0
- package/dist/src/core/living-docs/governance/frontend-standards-parser.js +366 -0
- package/dist/src/core/living-docs/governance/frontend-standards-parser.js.map +1 -0
- package/dist/src/core/living-docs/governance/go-standards-parser.d.ts +64 -0
- package/dist/src/core/living-docs/governance/go-standards-parser.d.ts.map +1 -0
- package/dist/src/core/living-docs/governance/go-standards-parser.js +229 -0
- package/dist/src/core/living-docs/governance/go-standards-parser.js.map +1 -0
- package/dist/src/core/living-docs/governance/index.d.ts +50 -0
- package/dist/src/core/living-docs/governance/index.d.ts.map +1 -0
- package/dist/src/core/living-docs/governance/index.js +56 -0
- package/dist/src/core/living-docs/governance/index.js.map +1 -0
- package/dist/src/core/living-docs/governance/java-standards-parser.d.ts +89 -0
- package/dist/src/core/living-docs/governance/java-standards-parser.d.ts.map +1 -0
- package/dist/src/core/living-docs/governance/java-standards-parser.js +356 -0
- package/dist/src/core/living-docs/governance/java-standards-parser.js.map +1 -0
- package/dist/src/core/living-docs/governance/python-standards-parser.d.ts +83 -0
- package/dist/src/core/living-docs/governance/python-standards-parser.d.ts.map +1 -0
- package/dist/src/core/living-docs/governance/python-standards-parser.js +347 -0
- package/dist/src/core/living-docs/governance/python-standards-parser.js.map +1 -0
- package/dist/src/core/living-docs/governance/standards-generator.d.ts +38 -0
- package/dist/src/core/living-docs/governance/standards-generator.d.ts.map +1 -0
- package/dist/src/core/living-docs/governance/standards-generator.js +476 -0
- package/dist/src/core/living-docs/governance/standards-generator.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.d.ts.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.js +54 -2
- package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.js.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.d.ts +5 -1
- package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.d.ts.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.js +358 -30
- package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.js.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts +44 -0
- package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.d.ts +6 -3
- package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.js +17 -8
- package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
- package/dist/src/core/living-docs/module-analyzer.d.ts +22 -0
- package/dist/src/core/living-docs/module-analyzer.d.ts.map +1 -1
- package/dist/src/core/living-docs/module-analyzer.js +123 -19
- package/dist/src/core/living-docs/module-analyzer.js.map +1 -1
- package/dist/src/core/llm/provider-factory.js +2 -2
- package/dist/src/core/llm/provider-factory.js.map +1 -1
- package/dist/src/core/llm/providers/anthropic-provider.js +1 -1
- package/dist/src/core/llm/providers/bedrock-provider.d.ts.map +1 -1
- package/dist/src/core/llm/providers/bedrock-provider.js +8 -4
- package/dist/src/core/llm/providers/bedrock-provider.js.map +1 -1
- package/dist/src/importers/jira-importer.d.ts +14 -0
- package/dist/src/importers/jira-importer.d.ts.map +1 -1
- package/dist/src/importers/jira-importer.js +75 -0
- package/dist/src/importers/jira-importer.js.map +1 -1
- package/dist/src/integrations/jira/jira-token-provider.d.ts +93 -0
- package/dist/src/integrations/jira/jira-token-provider.d.ts.map +1 -0
- package/dist/src/integrations/jira/jira-token-provider.js +160 -0
- package/dist/src/integrations/jira/jira-token-provider.js.map +1 -0
- package/dist/src/sync/ado-reconciler.d.ts +92 -0
- package/dist/src/sync/ado-reconciler.d.ts.map +1 -0
- package/dist/src/sync/ado-reconciler.js +335 -0
- package/dist/src/sync/ado-reconciler.js.map +1 -0
- package/dist/src/sync/jira-reconciler.d.ts +106 -0
- package/dist/src/sync/jira-reconciler.d.ts.map +1 -0
- package/dist/src/sync/jira-reconciler.js +405 -0
- package/dist/src/sync/jira-reconciler.js.map +1 -0
- package/dist/src/types/model-selection.d.ts +6 -4
- package/dist/src/types/model-selection.d.ts.map +1 -1
- package/dist/src/types/model-selection.js +3 -1
- package/dist/src/types/model-selection.js.map +1 -1
- package/dist/src/utils/external-tool-drift-detector.d.ts +1 -1
- package/dist/src/utils/external-tool-drift-detector.d.ts.map +1 -1
- package/dist/src/utils/external-tool-drift-detector.js +5 -4
- package/dist/src/utils/external-tool-drift-detector.js.map +1 -1
- package/dist/src/utils/feature-id-derivation.d.ts +8 -3
- package/dist/src/utils/feature-id-derivation.d.ts.map +1 -1
- package/dist/src/utils/feature-id-derivation.js +14 -6
- package/dist/src/utils/feature-id-derivation.js.map +1 -1
- package/dist/src/utils/model-selection.d.ts +3 -4
- package/dist/src/utils/model-selection.d.ts.map +1 -1
- package/dist/src/utils/model-selection.js +3 -4
- package/dist/src/utils/model-selection.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/agents/code-standards-detective/AGENT.md +48 -0
- package/plugins/specweave/commands/specweave-costs.md +4 -4
- package/plugins/specweave/commands/specweave-do.md +9 -9
- package/plugins/specweave/commands/specweave-done.md +13 -0
- package/plugins/specweave/commands/specweave-validate.md +27 -1
- package/plugins/specweave/hooks/hooks.json +10 -0
- package/plugins/specweave/hooks/spec-project-validator.sh +80 -25
- package/plugins/specweave/hooks/v2/guards/increment-duplicate-guard.sh +135 -0
- package/plugins/specweave/scripts/read-costs.sh +3 -3
- package/plugins/specweave/skills/code-standards-analyzer/SKILL.md +58 -6
- package/plugins/specweave/skills/increment-planner/SKILL.md +56 -25
- package/plugins/specweave/skills/increment-planner/templates/spec-multi-project.md +4 -2
- package/plugins/specweave/skills/increment-planner/templates/spec-single-project.md +2 -1
- package/plugins/specweave/skills/increment-planner/templates/tasks-multi-project.md +1 -1
- package/plugins/specweave/skills/increment-planner/templates/tasks-single-project.md +1 -1
- package/plugins/specweave-ado/commands/cleanup-duplicates.md +212 -0
- package/plugins/specweave-ado/commands/reconcile.md +120 -0
- package/plugins/specweave-ado/lib/ado-duplicate-detector.js +279 -0
- package/plugins/specweave-ado/lib/ado-duplicate-detector.ts +407 -0
- package/plugins/specweave-github/agents/github-manager/AGENT.md +2 -2
- package/plugins/specweave-infrastructure/skills/hetzner-provisioner/README.md +1 -1
- package/plugins/specweave-jira/agents/jira-manager/AGENT.md +1 -1
- package/plugins/specweave-jira/agents/jira-multi-project-mapper/AGENT.md +530 -0
- package/plugins/specweave-jira/agents/jira-sync-judge/AGENT.md +438 -0
- package/plugins/specweave-jira/commands/cleanup-duplicates.md +219 -0
- package/plugins/specweave-jira/commands/close.md +297 -0
- package/plugins/specweave-jira/commands/create.md +198 -0
- package/plugins/specweave-jira/commands/reconcile.md +123 -0
- package/plugins/specweave-jira/commands/status.md +215 -0
- package/plugins/specweave-jira/lib/jira-duplicate-detector.js +296 -0
- package/plugins/specweave-jira/lib/jira-duplicate-detector.ts +434 -0
- package/plugins/specweave-jira/lib/jira-permission-gate.js +160 -0
- package/plugins/specweave-jira/lib/jira-permission-gate.ts +276 -0
- package/plugins/specweave-jira/lib/jira-profile-resolver.js +222 -0
- package/plugins/specweave-jira/lib/jira-profile-resolver.ts +427 -0
- package/plugins/specweave-jira/reference/jira-specweave-mapping.md +16 -11
- package/plugins/specweave-release/commands/specweave-release-npm.md +140 -14
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JIRA Permission Gate
|
|
3
|
+
*
|
|
4
|
+
* Validates that sync permissions are enabled before allowing JIRA write operations.
|
|
5
|
+
* This ensures manual JIRA commands respect the same permission settings as the
|
|
6
|
+
* sync-coordinator.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const gate = await createJiraPermissionGate();
|
|
11
|
+
* const result = gate.checkWritePermission();
|
|
12
|
+
* if (!result.allowed) {
|
|
13
|
+
* console.log(result.reason);
|
|
14
|
+
* return;
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @module jira-permission-gate
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { promises as fs } from 'node:fs';
|
|
22
|
+
import * as path from 'node:path';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Permission check result
|
|
26
|
+
*/
|
|
27
|
+
export interface PermissionCheckResult {
|
|
28
|
+
/**
|
|
29
|
+
* Whether the operation is allowed
|
|
30
|
+
*/
|
|
31
|
+
allowed: boolean;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Human-readable reason for the decision
|
|
35
|
+
*/
|
|
36
|
+
reason: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Suggested action if permission denied
|
|
40
|
+
*/
|
|
41
|
+
suggestedAction?: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Which setting controls this permission
|
|
45
|
+
*/
|
|
46
|
+
settingPath?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Sync settings from config.json
|
|
51
|
+
*/
|
|
52
|
+
export interface SyncSettings {
|
|
53
|
+
canUpsertInternalItems: boolean;
|
|
54
|
+
canUpdateExternalItems: boolean;
|
|
55
|
+
canUpdateStatus: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Default settings (all disabled for safety)
|
|
60
|
+
*/
|
|
61
|
+
export const DEFAULT_SYNC_SETTINGS: SyncSettings = {
|
|
62
|
+
canUpsertInternalItems: false,
|
|
63
|
+
canUpdateExternalItems: false,
|
|
64
|
+
canUpdateStatus: false,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* JIRA Permission Gate
|
|
69
|
+
*
|
|
70
|
+
* Checks permission settings before allowing JIRA write operations.
|
|
71
|
+
*/
|
|
72
|
+
export class JiraPermissionGate {
|
|
73
|
+
private settings: SyncSettings;
|
|
74
|
+
private configPath: string;
|
|
75
|
+
|
|
76
|
+
constructor(settings: SyncSettings, configPath: string) {
|
|
77
|
+
this.settings = settings;
|
|
78
|
+
this.configPath = configPath;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if write operations (create/update issues) are allowed
|
|
83
|
+
*
|
|
84
|
+
* Requires: canUpdateExternalItems = true
|
|
85
|
+
*/
|
|
86
|
+
checkWritePermission(): PermissionCheckResult {
|
|
87
|
+
if (this.settings.canUpdateExternalItems) {
|
|
88
|
+
return {
|
|
89
|
+
allowed: true,
|
|
90
|
+
reason: 'Write operations permitted (canUpdateExternalItems=true)',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
allowed: false,
|
|
96
|
+
reason: 'Permission denied: JIRA updates are disabled.',
|
|
97
|
+
suggestedAction: `Enable sync.settings.canUpdateExternalItems in ${this.configPath}`,
|
|
98
|
+
settingPath: 'sync.settings.canUpdateExternalItems',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if status updates (transitions) are allowed
|
|
104
|
+
*
|
|
105
|
+
* Requires: canUpdateStatus = true
|
|
106
|
+
*/
|
|
107
|
+
checkStatusPermission(): PermissionCheckResult {
|
|
108
|
+
if (this.settings.canUpdateStatus) {
|
|
109
|
+
return {
|
|
110
|
+
allowed: true,
|
|
111
|
+
reason: 'Status updates permitted (canUpdateStatus=true)',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
allowed: false,
|
|
117
|
+
reason: 'Permission denied: JIRA status transitions are disabled.',
|
|
118
|
+
suggestedAction: `Enable sync.settings.canUpdateStatus in ${this.configPath}`,
|
|
119
|
+
settingPath: 'sync.settings.canUpdateStatus',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check if internal item creation is allowed
|
|
125
|
+
*
|
|
126
|
+
* Requires: canUpsertInternalItems = true
|
|
127
|
+
*/
|
|
128
|
+
checkCreateInternalPermission(): PermissionCheckResult {
|
|
129
|
+
if (this.settings.canUpsertInternalItems) {
|
|
130
|
+
return {
|
|
131
|
+
allowed: true,
|
|
132
|
+
reason: 'Internal item creation permitted (canUpsertInternalItems=true)',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
allowed: false,
|
|
138
|
+
reason: 'Permission denied: Creating internal items is disabled.',
|
|
139
|
+
suggestedAction: `Enable sync.settings.canUpsertInternalItems in ${this.configPath}`,
|
|
140
|
+
settingPath: 'sync.settings.canUpsertInternalItems',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if close operation is allowed (requires both write AND status)
|
|
146
|
+
*
|
|
147
|
+
* Requires: canUpdateExternalItems = true AND canUpdateStatus = true
|
|
148
|
+
*/
|
|
149
|
+
checkClosePermission(): PermissionCheckResult {
|
|
150
|
+
const writeCheck = this.checkWritePermission();
|
|
151
|
+
const statusCheck = this.checkStatusPermission();
|
|
152
|
+
|
|
153
|
+
if (writeCheck.allowed && statusCheck.allowed) {
|
|
154
|
+
return {
|
|
155
|
+
allowed: true,
|
|
156
|
+
reason: 'Close operations permitted (canUpdateExternalItems=true, canUpdateStatus=true)',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const missingPermissions: string[] = [];
|
|
161
|
+
if (!writeCheck.allowed) {
|
|
162
|
+
missingPermissions.push('canUpdateExternalItems');
|
|
163
|
+
}
|
|
164
|
+
if (!statusCheck.allowed) {
|
|
165
|
+
missingPermissions.push('canUpdateStatus');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
allowed: false,
|
|
170
|
+
reason: `Permission denied: Closing JIRA issues requires ${missingPermissions.join(' and ')}.`,
|
|
171
|
+
suggestedAction: `Enable ${missingPermissions.map(p => `sync.settings.${p}`).join(' and ')} in ${this.configPath}`,
|
|
172
|
+
settingPath: missingPermissions.map(p => `sync.settings.${p}`).join(', '),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get current settings
|
|
178
|
+
*/
|
|
179
|
+
getSettings(): SyncSettings {
|
|
180
|
+
return { ...this.settings };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get human-readable permission summary
|
|
185
|
+
*/
|
|
186
|
+
getPermissionSummary(): string {
|
|
187
|
+
const parts: string[] = [];
|
|
188
|
+
|
|
189
|
+
if (this.settings.canUpdateExternalItems) {
|
|
190
|
+
parts.push('create/update JIRA issues');
|
|
191
|
+
}
|
|
192
|
+
if (this.settings.canUpdateStatus) {
|
|
193
|
+
parts.push('transition issue status');
|
|
194
|
+
}
|
|
195
|
+
if (this.settings.canUpsertInternalItems) {
|
|
196
|
+
parts.push('create internal items');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (parts.length === 0) {
|
|
200
|
+
return 'All JIRA write operations disabled (read-only mode)';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return `Allowed: ${parts.join(', ')}`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Create a JiraPermissionGate from config.json
|
|
209
|
+
*
|
|
210
|
+
* @param projectRoot - Project root directory (defaults to cwd)
|
|
211
|
+
* @returns JiraPermissionGate instance
|
|
212
|
+
*/
|
|
213
|
+
export async function createJiraPermissionGate(
|
|
214
|
+
projectRoot: string = process.cwd()
|
|
215
|
+
): Promise<JiraPermissionGate> {
|
|
216
|
+
const configPath = path.join(projectRoot, '.specweave', 'config.json');
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
220
|
+
const config = JSON.parse(content);
|
|
221
|
+
|
|
222
|
+
const settings: SyncSettings = {
|
|
223
|
+
canUpsertInternalItems: config?.sync?.settings?.canUpsertInternalItems ?? false,
|
|
224
|
+
canUpdateExternalItems: config?.sync?.settings?.canUpdateExternalItems ?? false,
|
|
225
|
+
canUpdateStatus: config?.sync?.settings?.canUpdateStatus ?? false,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
return new JiraPermissionGate(settings, configPath);
|
|
229
|
+
} catch {
|
|
230
|
+
// Return gate with default (disabled) settings if config not found
|
|
231
|
+
return new JiraPermissionGate(DEFAULT_SYNC_SETTINGS, configPath);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Quick check: Are JIRA write operations allowed?
|
|
237
|
+
*
|
|
238
|
+
* Convenience function for simple permission checks.
|
|
239
|
+
*
|
|
240
|
+
* @param projectRoot - Project root directory
|
|
241
|
+
* @returns Permission check result
|
|
242
|
+
*/
|
|
243
|
+
export async function canWriteToJira(
|
|
244
|
+
projectRoot: string = process.cwd()
|
|
245
|
+
): Promise<PermissionCheckResult> {
|
|
246
|
+
const gate = await createJiraPermissionGate(projectRoot);
|
|
247
|
+
return gate.checkWritePermission();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Quick check: Are JIRA status updates allowed?
|
|
252
|
+
*
|
|
253
|
+
* @param projectRoot - Project root directory
|
|
254
|
+
* @returns Permission check result
|
|
255
|
+
*/
|
|
256
|
+
export async function canUpdateJiraStatus(
|
|
257
|
+
projectRoot: string = process.cwd()
|
|
258
|
+
): Promise<PermissionCheckResult> {
|
|
259
|
+
const gate = await createJiraPermissionGate(projectRoot);
|
|
260
|
+
return gate.checkStatusPermission();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Quick check: Can JIRA issues be closed?
|
|
265
|
+
*
|
|
266
|
+
* @param projectRoot - Project root directory
|
|
267
|
+
* @returns Permission check result
|
|
268
|
+
*/
|
|
269
|
+
export async function canCloseJiraIssue(
|
|
270
|
+
projectRoot: string = process.cwd()
|
|
271
|
+
): Promise<PermissionCheckResult> {
|
|
272
|
+
const gate = await createJiraPermissionGate(projectRoot);
|
|
273
|
+
return gate.checkClosePermission();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export default JiraPermissionGate;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
class JiraProfileResolver {
|
|
4
|
+
constructor(projectRoot = process.cwd()) {
|
|
5
|
+
this.projectRoot = projectRoot;
|
|
6
|
+
this.configPath = path.join(projectRoot, ".specweave", "config.json");
|
|
7
|
+
this.incrementsPath = path.join(projectRoot, ".specweave", "increments");
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the JIRA profile for an increment
|
|
11
|
+
*
|
|
12
|
+
* Priority:
|
|
13
|
+
* 1. Increment's metadata.json -> external_sync.jira.profile
|
|
14
|
+
* 2. Config.json -> sync.defaultProfile (v0.31.0+)
|
|
15
|
+
* 3. Config.json -> sync.activeProfile (legacy fallback)
|
|
16
|
+
*
|
|
17
|
+
* @param incrementId - Increment ID (e.g., "0093-my-feature")
|
|
18
|
+
* @returns Profile resolution result
|
|
19
|
+
*/
|
|
20
|
+
async resolveProfile(incrementId) {
|
|
21
|
+
const config = await this.loadConfig();
|
|
22
|
+
if (!config) {
|
|
23
|
+
return {
|
|
24
|
+
success: false,
|
|
25
|
+
error: "Failed to load .specweave/config.json",
|
|
26
|
+
incrementId
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const incrementProfile = await this.getIncrementProfile(incrementId);
|
|
30
|
+
const globalDefaultProfile = config.sync?.defaultProfile ?? config.sync?.activeProfile;
|
|
31
|
+
const profileName = incrementProfile || globalDefaultProfile;
|
|
32
|
+
if (!profileName) {
|
|
33
|
+
return {
|
|
34
|
+
success: false,
|
|
35
|
+
error: "No JIRA profile configured. Set sync.defaultProfile in config.json or external_sync.jira.profile in increment metadata.",
|
|
36
|
+
incrementId
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const profiles = config.sync?.profiles || {};
|
|
40
|
+
const profileConfig = profiles[profileName];
|
|
41
|
+
if (!profileConfig) {
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
error: `Profile "${profileName}" not found in config.sync.profiles`,
|
|
45
|
+
incrementId
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (profileConfig.provider !== "jira") {
|
|
49
|
+
return {
|
|
50
|
+
success: false,
|
|
51
|
+
error: `Profile "${profileName}" is not a JIRA profile (provider: ${profileConfig.provider})`,
|
|
52
|
+
incrementId
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (!profileConfig.config?.domain || !profileConfig.config?.projectKey) {
|
|
56
|
+
return {
|
|
57
|
+
success: false,
|
|
58
|
+
error: `Profile "${profileName}" missing required fields (domain, projectKey)`,
|
|
59
|
+
incrementId
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
success: true,
|
|
64
|
+
profile: {
|
|
65
|
+
profileName,
|
|
66
|
+
domain: profileConfig.config.domain,
|
|
67
|
+
projectKey: profileConfig.config.projectKey,
|
|
68
|
+
displayName: profileConfig.displayName,
|
|
69
|
+
boardId: profileConfig.config.boardId,
|
|
70
|
+
boardName: profileConfig.config.boardName,
|
|
71
|
+
instanceType: profileConfig.config.instanceType ?? "cloud"
|
|
72
|
+
},
|
|
73
|
+
source: incrementProfile ? "increment" : "global",
|
|
74
|
+
incrementId
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get increment's stored JIRA profile name
|
|
79
|
+
*
|
|
80
|
+
* @param incrementId - Increment ID
|
|
81
|
+
* @returns Profile name or null if not set
|
|
82
|
+
*/
|
|
83
|
+
async getIncrementProfile(incrementId) {
|
|
84
|
+
const metadataPath = path.join(this.incrementsPath, incrementId, "metadata.json");
|
|
85
|
+
try {
|
|
86
|
+
const content = await fs.readFile(metadataPath, "utf-8");
|
|
87
|
+
const metadata = JSON.parse(content);
|
|
88
|
+
return metadata?.external_sync?.jira?.profile || metadata?.external_ids?.jira?.profile || null;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get increment's stored JIRA issue key
|
|
95
|
+
*
|
|
96
|
+
* @param incrementId - Increment ID
|
|
97
|
+
* @returns Issue key (e.g., PROJ-123) or null if not linked
|
|
98
|
+
*/
|
|
99
|
+
async getIncrementIssueKey(incrementId) {
|
|
100
|
+
const metadataPath = path.join(this.incrementsPath, incrementId, "metadata.json");
|
|
101
|
+
try {
|
|
102
|
+
const content = await fs.readFile(metadataPath, "utf-8");
|
|
103
|
+
const metadata = JSON.parse(content);
|
|
104
|
+
return metadata?.external_sync?.jira?.issueKey || metadata?.external_ids?.jira?.epic || metadata?.external_ids?.jira?.issueKey || null;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Set increment's JIRA profile
|
|
111
|
+
*
|
|
112
|
+
* Stores the profile name in the increment's metadata.json
|
|
113
|
+
*
|
|
114
|
+
* @param incrementId - Increment ID
|
|
115
|
+
* @param profileName - Profile name to set
|
|
116
|
+
*/
|
|
117
|
+
async setIncrementProfile(incrementId, profileName) {
|
|
118
|
+
const metadataPath = path.join(this.incrementsPath, incrementId, "metadata.json");
|
|
119
|
+
let metadata = {};
|
|
120
|
+
try {
|
|
121
|
+
const content = await fs.readFile(metadataPath, "utf-8");
|
|
122
|
+
metadata = JSON.parse(content);
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
if (!metadata.external_sync) {
|
|
126
|
+
metadata.external_sync = {};
|
|
127
|
+
}
|
|
128
|
+
if (!metadata.external_sync.jira) {
|
|
129
|
+
metadata.external_sync.jira = {};
|
|
130
|
+
}
|
|
131
|
+
metadata.external_sync.jira.profile = profileName;
|
|
132
|
+
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2) + "\n", "utf-8");
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Set increment's JIRA issue key
|
|
136
|
+
*
|
|
137
|
+
* @param incrementId - Increment ID
|
|
138
|
+
* @param issueKey - JIRA issue key (e.g., PROJ-123)
|
|
139
|
+
*/
|
|
140
|
+
async setIncrementIssueKey(incrementId, issueKey) {
|
|
141
|
+
const metadataPath = path.join(this.incrementsPath, incrementId, "metadata.json");
|
|
142
|
+
let metadata = {};
|
|
143
|
+
try {
|
|
144
|
+
const content = await fs.readFile(metadataPath, "utf-8");
|
|
145
|
+
metadata = JSON.parse(content);
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
if (!metadata.external_sync) {
|
|
149
|
+
metadata.external_sync = {};
|
|
150
|
+
}
|
|
151
|
+
if (!metadata.external_sync.jira) {
|
|
152
|
+
metadata.external_sync.jira = {};
|
|
153
|
+
}
|
|
154
|
+
const jiraSync = metadata.external_sync.jira;
|
|
155
|
+
jiraSync.issueKey = issueKey;
|
|
156
|
+
jiraSync.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
157
|
+
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2) + "\n", "utf-8");
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* List available JIRA profiles
|
|
161
|
+
*
|
|
162
|
+
* @returns Array of profile names
|
|
163
|
+
*/
|
|
164
|
+
async listJiraProfiles() {
|
|
165
|
+
const config = await this.loadConfig();
|
|
166
|
+
if (!config?.sync?.profiles) {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
const profiles = config.sync.profiles;
|
|
170
|
+
return Object.entries(profiles).filter(([_, p]) => p.provider === "jira").map(([name]) => name);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Get profile details by name
|
|
174
|
+
*
|
|
175
|
+
* @param profileName - Profile name
|
|
176
|
+
* @returns Profile config or null
|
|
177
|
+
*/
|
|
178
|
+
async getProfileByName(profileName) {
|
|
179
|
+
const config = await this.loadConfig();
|
|
180
|
+
if (!config?.sync?.profiles) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const profileConfig = config.sync.profiles[profileName];
|
|
184
|
+
if (!profileConfig || profileConfig.provider !== "jira") {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
profileName,
|
|
189
|
+
domain: profileConfig.config.domain,
|
|
190
|
+
projectKey: profileConfig.config.projectKey,
|
|
191
|
+
displayName: profileConfig.displayName,
|
|
192
|
+
boardId: profileConfig.config.boardId,
|
|
193
|
+
boardName: profileConfig.config.boardName,
|
|
194
|
+
instanceType: profileConfig.config.instanceType ?? "cloud"
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Load config.json
|
|
199
|
+
*/
|
|
200
|
+
async loadConfig() {
|
|
201
|
+
try {
|
|
202
|
+
const content = await fs.readFile(this.configPath, "utf-8");
|
|
203
|
+
return JSON.parse(content);
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function createJiraProfileResolver(projectRoot = process.cwd()) {
|
|
210
|
+
return new JiraProfileResolver(projectRoot);
|
|
211
|
+
}
|
|
212
|
+
async function resolveJiraProfile(incrementId, projectRoot = process.cwd()) {
|
|
213
|
+
const resolver = new JiraProfileResolver(projectRoot);
|
|
214
|
+
return resolver.resolveProfile(incrementId);
|
|
215
|
+
}
|
|
216
|
+
var jira_profile_resolver_default = JiraProfileResolver;
|
|
217
|
+
export {
|
|
218
|
+
JiraProfileResolver,
|
|
219
|
+
createJiraProfileResolver,
|
|
220
|
+
jira_profile_resolver_default as default,
|
|
221
|
+
resolveJiraProfile
|
|
222
|
+
};
|