@domainlang/cli 0.5.2 → 0.7.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.
Files changed (38) hide show
  1. package/README.md +16 -16
  2. package/out/dependency-commands.d.ts +1 -1
  3. package/out/dependency-commands.js +52 -24
  4. package/out/dependency-commands.js.map +1 -1
  5. package/out/main.d.ts +1 -1
  6. package/out/main.js +1 -1
  7. package/out/main.js.map +1 -1
  8. package/out/services/dependency-analyzer.d.ts +59 -0
  9. package/out/services/dependency-analyzer.js +260 -0
  10. package/out/services/dependency-analyzer.js.map +1 -0
  11. package/out/services/dependency-resolver.d.ts +148 -0
  12. package/out/services/dependency-resolver.js +448 -0
  13. package/out/services/dependency-resolver.js.map +1 -0
  14. package/out/services/git-url-resolver.d.ts +158 -0
  15. package/out/services/git-url-resolver.js +408 -0
  16. package/out/services/git-url-resolver.js.map +1 -0
  17. package/out/services/governance-validator.d.ts +56 -0
  18. package/out/services/governance-validator.js +171 -0
  19. package/out/services/governance-validator.js.map +1 -0
  20. package/out/services/index.d.ts +12 -0
  21. package/out/services/index.js +13 -0
  22. package/out/services/index.js.map +1 -0
  23. package/out/services/semver.d.ts +98 -0
  24. package/out/services/semver.js +195 -0
  25. package/out/services/semver.js.map +1 -0
  26. package/out/services/types.d.ts +58 -0
  27. package/out/services/types.js +8 -0
  28. package/out/services/types.js.map +1 -0
  29. package/package.json +4 -3
  30. package/src/dependency-commands.ts +59 -24
  31. package/src/main.ts +1 -1
  32. package/src/services/dependency-analyzer.ts +329 -0
  33. package/src/services/dependency-resolver.ts +546 -0
  34. package/src/services/git-url-resolver.ts +504 -0
  35. package/src/services/governance-validator.ts +226 -0
  36. package/src/services/index.ts +13 -0
  37. package/src/services/semver.ts +213 -0
  38. package/src/services/types.ts +81 -0
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Governance and Compliance Validation Service (CLI-only)
3
+ *
4
+ * Enforces organizational policies and best practices:
5
+ * - Allowed/blocked dependency sources
6
+ * - Version policy enforcement (no pre-release in production)
7
+ * - Team ownership validation
8
+ * - License compliance
9
+ * - Audit trail generation
10
+ *
11
+ * Governance policies are defined in the `governance` section of model.yaml:
12
+ *
13
+ * ```yaml
14
+ * governance:
15
+ * allowedSources:
16
+ * - github.com/acme
17
+ * requireStableVersions: true
18
+ * requireTeamOwnership: true
19
+ * ```
20
+ */
21
+
22
+ import type { LockFile, GovernancePolicy, GovernanceMetadata, GovernanceViolation } from './types.js';
23
+ import path from 'node:path';
24
+ import fs from 'node:fs/promises';
25
+ import YAML from 'yaml';
26
+ import { isPreRelease } from './semver.js';
27
+
28
+ /** Locked dependency entry from lock file */
29
+ interface LockedDependency {
30
+ resolved: string;
31
+ ref: string;
32
+ commit: string;
33
+ }
34
+
35
+ /**
36
+ * Validates dependencies against organizational governance policies.
37
+ */
38
+ export class GovernanceValidator {
39
+ constructor(private readonly policy: GovernancePolicy) {}
40
+
41
+ /**
42
+ * Validates a lock file against governance policies.
43
+ */
44
+ async validate(lockFile: LockFile, workspaceRoot: string): Promise<GovernanceViolation[]> {
45
+ const violations: GovernanceViolation[] = [];
46
+
47
+ // Validate each dependency
48
+ for (const [packageKey, locked] of Object.entries(lockFile.dependencies)) {
49
+ violations.push(
50
+ ...this.validateAllowedSources(packageKey, locked),
51
+ ...this.validateBlockedPackages(packageKey),
52
+ ...this.validateVersionStability(packageKey, locked)
53
+ );
54
+ }
55
+
56
+ // Validate workspace metadata
57
+ if (this.policy.requireTeamOwnership) {
58
+ const metadata = await this.loadGovernanceMetadata(workspaceRoot);
59
+ if (!metadata.team || !metadata.contact) {
60
+ violations.push({
61
+ type: 'missing-metadata',
62
+ packageKey: 'workspace',
63
+ message: 'Missing required team ownership metadata in model.yaml',
64
+ severity: 'warning',
65
+ });
66
+ }
67
+ }
68
+
69
+ return violations;
70
+ }
71
+
72
+ /**
73
+ * Checks if package source is allowed by policy.
74
+ */
75
+ private validateAllowedSources(
76
+ packageKey: string,
77
+ locked: LockedDependency
78
+ ): GovernanceViolation[] {
79
+ if (!this.policy.allowedSources || this.policy.allowedSources.length === 0) {
80
+ return [];
81
+ }
82
+
83
+ const isAllowed = this.policy.allowedSources.some(
84
+ pattern => locked.resolved.includes(pattern) || packageKey.startsWith(pattern)
85
+ );
86
+
87
+ if (!isAllowed) {
88
+ return [{
89
+ type: 'blocked-source',
90
+ packageKey,
91
+ message: `Package from unauthorized source: ${locked.resolved}`,
92
+ severity: 'error',
93
+ }];
94
+ }
95
+
96
+ return [];
97
+ }
98
+
99
+ /**
100
+ * Checks if package is explicitly blocked by policy.
101
+ */
102
+ private validateBlockedPackages(packageKey: string): GovernanceViolation[] {
103
+ if (!this.policy.blockedPackages) {
104
+ return [];
105
+ }
106
+
107
+ const isBlocked = this.policy.blockedPackages.some(
108
+ pattern => packageKey.includes(pattern)
109
+ );
110
+
111
+ if (isBlocked) {
112
+ return [{
113
+ type: 'blocked-source',
114
+ packageKey,
115
+ message: `Package is blocked by governance policy`,
116
+ severity: 'error',
117
+ }];
118
+ }
119
+
120
+ return [];
121
+ }
122
+
123
+ /**
124
+ * Checks if package version meets stability requirements.
125
+ */
126
+ private validateVersionStability(
127
+ packageKey: string,
128
+ locked: Pick<LockedDependency, 'ref'>
129
+ ): GovernanceViolation[] {
130
+ if (!this.policy.requireStableVersions) {
131
+ return [];
132
+ }
133
+
134
+ if (isPreRelease(locked.ref)) {
135
+ return [{
136
+ type: 'unstable-version',
137
+ packageKey,
138
+ message: `Pre-release ref not allowed: ${locked.ref}`,
139
+ severity: 'error',
140
+ }];
141
+ }
142
+
143
+ return [];
144
+ }
145
+
146
+ /**
147
+ * Loads governance metadata from model.yaml.
148
+ */
149
+ async loadGovernanceMetadata(workspaceRoot: string): Promise<GovernanceMetadata> {
150
+ const manifestPath = path.join(workspaceRoot, 'model.yaml');
151
+
152
+ try {
153
+ const content = await fs.readFile(manifestPath, 'utf-8');
154
+ const manifest = YAML.parse(content) as {
155
+ metadata?: GovernanceMetadata;
156
+ };
157
+
158
+ return manifest.metadata ?? {};
159
+ } catch {
160
+ return {};
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Generates an audit report for compliance tracking.
166
+ */
167
+ async generateAuditReport(lockFile: LockFile, workspaceRoot: string): Promise<string> {
168
+ const metadata = await this.loadGovernanceMetadata(workspaceRoot);
169
+ const violations = await this.validate(lockFile, workspaceRoot);
170
+
171
+ // Build header section
172
+ const headerLines = [
173
+ '=== Dependency Audit Report ===',
174
+ '',
175
+ `Workspace: ${workspaceRoot}`,
176
+ `Team: ${metadata.team ?? 'N/A'}`,
177
+ `Contact: ${metadata.contact ?? 'N/A'}`,
178
+ `Domain: ${metadata.domain ?? 'N/A'}`,
179
+ '',
180
+ 'Dependencies:',
181
+ ];
182
+
183
+ // Build dependencies section
184
+ const depLines: string[] = [];
185
+ for (const [packageKey, locked] of Object.entries(lockFile.dependencies)) {
186
+ depLines.push(
187
+ ` - ${packageKey}@${locked.ref}`,
188
+ ` Source: ${locked.resolved}`,
189
+ ` Commit: ${locked.commit}`
190
+ );
191
+ }
192
+
193
+ // Build violations section
194
+ const violationLines = violations.length > 0
195
+ ? [
196
+ '',
197
+ 'Violations:',
198
+ ...violations.map(v =>
199
+ ` [${v.severity.toUpperCase()}] ${v.packageKey}: ${v.message}`
200
+ )
201
+ ]
202
+ : ['', '\u2713 No policy violations detected'];
203
+
204
+ return [...headerLines, ...depLines, ...violationLines].join('\n');
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Loads governance policy from model.yaml governance section.
210
+ */
211
+ export async function loadGovernancePolicy(workspaceRoot: string): Promise<GovernancePolicy> {
212
+ const manifestPath = path.join(workspaceRoot, 'model.yaml');
213
+
214
+ try {
215
+ const content = await fs.readFile(manifestPath, 'utf-8');
216
+ const manifest = YAML.parse(content) as {
217
+ governance?: GovernancePolicy;
218
+ };
219
+
220
+ // Return governance section or empty policy if not defined
221
+ return manifest.governance ?? {};
222
+ } catch {
223
+ // No manifest or parse error = permissive defaults
224
+ return {};
225
+ }
226
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * CLI Services Index
3
+ *
4
+ * Exports all CLI-only services for package management.
5
+ * These services contain network operations and should never be used in LSP.
6
+ */
7
+
8
+ export * from './types.js';
9
+ export * from './semver.js';
10
+ export * from './git-url-resolver.js';
11
+ export * from './dependency-resolver.js';
12
+ export * from './dependency-analyzer.js';
13
+ export * from './governance-validator.js';
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Semantic Versioning Utilities (CLI-only)
3
+ *
4
+ * Centralized SemVer parsing, comparison, and validation for the dependency system.
5
+ * All version-related logic should use these utilities to ensure consistency.
6
+ *
7
+ * Supported formats:
8
+ * - "1.0.0" or "v1.0.0" (tags)
9
+ * - "1.0.0-alpha.1" (pre-release)
10
+ * - "main", "develop" (branches)
11
+ * - "abc123def" (commit SHAs, 7-40 hex chars)
12
+ */
13
+
14
+ import type { SemVer, RefType, ParsedRef } from './types.js';
15
+
16
+ // ============================================================================
17
+ // Parsing
18
+ // ============================================================================
19
+
20
+ /**
21
+ * Parses a version string into SemVer components.
22
+ * Returns undefined if not a valid SemVer.
23
+ *
24
+ * @example
25
+ * parseSemVer("v1.2.3") // { major: 1, minor: 2, patch: 3, original: "v1.2.3" }
26
+ * parseSemVer("1.0.0-alpha") // { major: 1, minor: 0, patch: 0, prerelease: "alpha", ... }
27
+ * parseSemVer("main") // undefined (not SemVer)
28
+ */
29
+ export function parseSemVer(version: string): SemVer | undefined {
30
+ // Strip leading 'v' if present
31
+ const normalized = version.startsWith('v') ? version.slice(1) : version;
32
+
33
+ // Match semver pattern: major.minor.patch[-prerelease]
34
+ const match = normalized.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
35
+ if (!match) return undefined;
36
+
37
+ return {
38
+ major: parseInt(match[1], 10),
39
+ minor: parseInt(match[2], 10),
40
+ patch: parseInt(match[3], 10),
41
+ preRelease: match[4],
42
+ original: version,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Detects the type of a git ref based on its format.
48
+ *
49
+ * @example
50
+ * detectRefType("v1.0.0") // 'tag'
51
+ * detectRefType("1.2.3") // 'tag'
52
+ * detectRefType("main") // 'branch'
53
+ * detectRefType("abc123def") // 'commit'
54
+ */
55
+ export function detectRefType(ref: string): RefType {
56
+ // Commit SHA: 7-40 hex characters
57
+ if (/^[0-9a-f]{7,40}$/i.test(ref)) {
58
+ return 'commit';
59
+ }
60
+ // Tags typically start with 'v' followed by semver
61
+ if (/^v?\d+\.\d+\.\d+/.test(ref)) {
62
+ return 'tag';
63
+ }
64
+ // Everything else is treated as a branch
65
+ return 'branch';
66
+ }
67
+
68
+ /**
69
+ * Parses a ref string into a structured ParsedRef with type and optional SemVer.
70
+ */
71
+ export function parseRef(ref: string): ParsedRef {
72
+ const type = detectRefType(ref);
73
+ const semver = type === 'tag' ? parseSemVer(ref) : undefined;
74
+
75
+ return { original: ref, type, semver };
76
+ }
77
+
78
+ // ============================================================================
79
+ // Comparison
80
+ // ============================================================================
81
+
82
+ /**
83
+ * Compares two SemVer versions.
84
+ * Returns: negative if a < b, positive if a > b, zero if equal.
85
+ *
86
+ * @example
87
+ * compareSemVer(parse("1.0.0"), parse("2.0.0")) // negative (a < b)
88
+ * compareSemVer(parse("1.5.0"), parse("1.2.0")) // positive (a > b)
89
+ * compareSemVer(parse("1.0.0-alpha"), parse("1.0.0")) // negative (prerelease < release)
90
+ */
91
+ export function compareSemVer(a: SemVer, b: SemVer): number {
92
+ if (a.major !== b.major) return a.major - b.major;
93
+ if (a.minor !== b.minor) return a.minor - b.minor;
94
+ if (a.patch !== b.patch) return a.patch - b.patch;
95
+
96
+ // Pre-release versions are lower than release versions
97
+ if (a.preRelease && !b.preRelease) return -1;
98
+ if (!a.preRelease && b.preRelease) return 1;
99
+ if (a.preRelease && b.preRelease) {
100
+ return a.preRelease.localeCompare(b.preRelease);
101
+ }
102
+
103
+ return 0;
104
+ }
105
+
106
+ /**
107
+ * Picks the latest from a list of SemVer refs.
108
+ * Returns the ref string (with original 'v' prefix if present).
109
+ *
110
+ * @example
111
+ * pickLatestSemVer(["v1.0.0", "v1.5.0", "v1.2.0"]) // "v1.5.0"
112
+ */
113
+ export function pickLatestSemVer(refs: string[]): string | undefined {
114
+ const parsed = refs
115
+ .map(ref => ({ ref, semver: parseSemVer(ref) }))
116
+ .filter((item): item is { ref: string; semver: SemVer } => item.semver !== undefined);
117
+
118
+ if (parsed.length === 0) return undefined;
119
+
120
+ parsed.sort((a, b) => compareSemVer(b.semver, a.semver)); // Descending
121
+ return parsed[0].ref;
122
+ }
123
+
124
+ /**
125
+ * Sorts version strings in descending order (newest first).
126
+ * Non-SemVer refs are sorted lexicographically at the end.
127
+ *
128
+ * @example
129
+ * sortVersionsDescending(["v1.0.0", "v2.0.0", "v1.5.0"]) // ["v2.0.0", "v1.5.0", "v1.0.0"]
130
+ */
131
+ export function sortVersionsDescending(versions: string[]): string[] {
132
+ return [...versions].sort((a, b) => {
133
+ const semverA = parseSemVer(a);
134
+ const semverB = parseSemVer(b);
135
+
136
+ // Both are SemVer - compare semantically
137
+ if (semverA && semverB) {
138
+ return compareSemVer(semverB, semverA); // Descending
139
+ }
140
+
141
+ // SemVer comes before non-SemVer
142
+ if (semverA && !semverB) return -1;
143
+ if (!semverA && semverB) return 1;
144
+
145
+ // Both non-SemVer - lexicographic
146
+ return b.localeCompare(a);
147
+ });
148
+ }
149
+
150
+ // ============================================================================
151
+ // Validation
152
+ // ============================================================================
153
+
154
+ /**
155
+ * Checks if a version/ref is a pre-release.
156
+ *
157
+ * Pre-release identifiers: alpha, beta, rc, pre, dev, snapshot
158
+ *
159
+ * @example
160
+ * isPreRelease("v1.0.0") // false
161
+ * isPreRelease("v1.0.0-alpha") // true
162
+ * isPreRelease("v1.0.0-rc.1") // true
163
+ */
164
+ export function isPreRelease(ref: string): boolean {
165
+ const semver = parseSemVer(ref);
166
+ if (semver?.preRelease) {
167
+ return true;
168
+ }
169
+
170
+ // Also check for common pre-release patterns without proper SemVer
171
+ const clean = ref.replace(/^v/, '');
172
+ return /-(alpha|beta|rc|pre|dev|snapshot)/i.test(clean);
173
+ }
174
+
175
+ /**
176
+ * Checks if two SemVer versions are compatible (same major version).
177
+ *
178
+ * @example
179
+ * areSameMajor(parse("1.0.0"), parse("1.5.0")) // true
180
+ * areSameMajor(parse("1.0.0"), parse("2.0.0")) // false
181
+ */
182
+ export function areSameMajor(a: SemVer, b: SemVer): boolean {
183
+ return a.major === b.major;
184
+ }
185
+
186
+ /**
187
+ * Gets the major version number from a ref string.
188
+ * Returns undefined if not a valid SemVer.
189
+ */
190
+ export function getMajorVersion(ref: string): number | undefined {
191
+ return parseSemVer(ref)?.major;
192
+ }
193
+
194
+ // ============================================================================
195
+ // Filtering
196
+ // ============================================================================
197
+
198
+ /**
199
+ * Filters refs to only stable versions (excludes pre-releases).
200
+ *
201
+ * @example
202
+ * filterStableVersions(["v1.0.0", "v1.1.0-alpha", "v1.2.0"]) // ["v1.0.0", "v1.2.0"]
203
+ */
204
+ export function filterStableVersions(refs: string[]): string[] {
205
+ return refs.filter(ref => !isPreRelease(ref));
206
+ }
207
+
208
+ /**
209
+ * Filters refs to only SemVer tags (excludes branches and commits).
210
+ */
211
+ export function filterSemVerTags(refs: string[]): string[] {
212
+ return refs.filter(ref => detectRefType(ref) === 'tag' && parseSemVer(ref) !== undefined);
213
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Types for CLI-only package management services.
3
+ *
4
+ * These types support git-based dependency resolution and governance
5
+ * that only runs in the CLI context (never in LSP).
6
+ */
7
+
8
+ import type { RefType } from '@domainlang/language';
9
+
10
+ // Re-export types that are shared with language package
11
+ export type {
12
+ LockFile,
13
+ LockedDependency,
14
+ ModelManifest,
15
+ DependencySpec,
16
+ ExtendedDependencySpec,
17
+ PathAliases,
18
+ GovernancePolicy,
19
+ GovernanceMetadata,
20
+ GovernanceViolation,
21
+ DependencyTreeNode,
22
+ ReverseDependency,
23
+ VersionPolicy,
24
+ SemVer,
25
+ RefType,
26
+ ParsedRef,
27
+ } from '@domainlang/language';
28
+
29
+ /**
30
+ * Parsed git import URL information.
31
+ */
32
+ export interface GitImportInfo {
33
+ /** Original import string */
34
+ original: string;
35
+ /** Detected platform (github, gitlab, bitbucket, generic) */
36
+ platform: 'github' | 'gitlab' | 'bitbucket' | 'generic';
37
+ /** Repository owner/organization */
38
+ owner: string;
39
+ /** Repository name */
40
+ repo: string;
41
+ /** Version/tag/branch/commit */
42
+ version: string;
43
+ /** Full repository URL without version */
44
+ repoUrl: string;
45
+ /** Entry point file (default: index.dlang) */
46
+ entryPoint: string;
47
+ }
48
+
49
+ /**
50
+ * Package configuration during dependency resolution.
51
+ */
52
+ export interface ResolvingPackage {
53
+ name?: string;
54
+ version?: string;
55
+ entry?: string;
56
+ dependencies?: Record<string, string>;
57
+ overrides?: Record<string, string>;
58
+ }
59
+
60
+ /**
61
+ * Dependency graph for resolution.
62
+ */
63
+ export interface DependencyGraph {
64
+ nodes: Record<string, DependencyGraphNode>;
65
+ root: string;
66
+ }
67
+
68
+ /**
69
+ * Node in the dependency graph.
70
+ */
71
+ export interface DependencyGraphNode {
72
+ packageKey: string;
73
+ refConstraint: string;
74
+ constraints?: Set<string>;
75
+ repoUrl?: string;
76
+ dependencies: Record<string, string>;
77
+ dependents: string[];
78
+ resolvedRef?: string;
79
+ refType?: RefType;
80
+ commitHash?: string;
81
+ }