@aspruyt/json-config-sync 2.0.2 → 2.0.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/PR.md +15 -14
- package/README.md +58 -20
- package/dist/config.d.ts +1 -1
- package/dist/config.js +14 -14
- package/dist/env.js +5 -5
- package/dist/git-ops.js +22 -22
- package/dist/index.js +28 -26
- package/dist/logger.js +11 -7
- package/dist/merge.d.ts +1 -1
- package/dist/merge.js +19 -17
- package/dist/pr-creator.d.ts +4 -4
- package/dist/pr-creator.js +32 -26
- package/dist/repo-detector.d.ts +1 -1
- package/dist/repo-detector.js +9 -9
- package/package.json +1 -1
package/PR.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
## Summary
|
|
2
|
-
|
|
3
|
-
Automated sync of `{{FILE_NAME}}` configuration file.
|
|
4
|
-
|
|
5
|
-
## Changes
|
|
6
|
-
|
|
7
|
-
- {{ACTION}} `{{FILE_NAME}}` in repository root
|
|
8
|
-
|
|
9
|
-
## Source
|
|
10
|
-
|
|
11
|
-
Configuration synced using [json-config-sync](https://github.com/anthony-spruyt/json-config-sync).
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
1
|
+
## Summary
|
|
2
|
+
|
|
3
|
+
Automated sync of `{{FILE_NAME}}` configuration file.
|
|
4
|
+
|
|
5
|
+
## Changes
|
|
6
|
+
|
|
7
|
+
- {{ACTION}} `{{FILE_NAME}}` in repository root
|
|
8
|
+
|
|
9
|
+
## Source
|
|
10
|
+
|
|
11
|
+
Configuration synced using [json-config-sync](https://github.com/anthony-spruyt/json-config-sync).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
_This PR was automatically generated by [json-config-sync](https://github.com/anthony-spruyt/json-config-sync)_
|
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ A CLI tool that syncs JSON or YAML configuration files across multiple GitHub an
|
|
|
21
21
|
- [CI/CD Integration](#cicd-integration)
|
|
22
22
|
- [Output Examples](#output-examples)
|
|
23
23
|
- [Troubleshooting](#troubleshooting)
|
|
24
|
+
- [IDE Integration](#ide-integration)
|
|
24
25
|
- [Development](#development)
|
|
25
26
|
- [License](#license)
|
|
26
27
|
|
|
@@ -136,36 +137,36 @@ json-config-sync --config ./config.yaml --work-dir ./my-temp
|
|
|
136
137
|
### Basic Structure
|
|
137
138
|
|
|
138
139
|
```yaml
|
|
139
|
-
fileName: my.config.json
|
|
140
|
-
mergeStrategy: replace
|
|
140
|
+
fileName: my.config.json # Target file (.json outputs JSON, .yaml/.yml outputs YAML)
|
|
141
|
+
mergeStrategy: replace # Default array merge strategy (optional)
|
|
141
142
|
|
|
142
|
-
content:
|
|
143
|
+
content: # Base config content (optional)
|
|
143
144
|
key: value
|
|
144
145
|
|
|
145
|
-
repos:
|
|
146
|
+
repos: # List of repositories
|
|
146
147
|
- git: git@github.com:org/repo.git
|
|
147
|
-
content:
|
|
148
|
+
content: # Per-repo overlay (optional if base content exists)
|
|
148
149
|
key: override
|
|
149
150
|
```
|
|
150
151
|
|
|
151
152
|
### Root-Level Fields
|
|
152
153
|
|
|
153
|
-
| Field | Description
|
|
154
|
-
| --------------- |
|
|
155
|
-
| `fileName` | Target file name (`.json` → JSON output, `.yaml`/`.yml` → YAML output)| Yes |
|
|
156
|
-
| `content` | Base config inherited by all repos
|
|
157
|
-
| `mergeStrategy` | Default array merge strategy: `replace`, `append`, `prepend`
|
|
158
|
-
| `repos` | Array of repository configurations
|
|
154
|
+
| Field | Description | Required |
|
|
155
|
+
| --------------- | ---------------------------------------------------------------------- | -------- |
|
|
156
|
+
| `fileName` | Target file name (`.json` → JSON output, `.yaml`/`.yml` → YAML output) | Yes |
|
|
157
|
+
| `content` | Base config inherited by all repos | No\* |
|
|
158
|
+
| `mergeStrategy` | Default array merge strategy: `replace`, `append`, `prepend` | No |
|
|
159
|
+
| `repos` | Array of repository configurations | Yes |
|
|
159
160
|
|
|
160
161
|
\* Required if any repo entry omits the `content` field.
|
|
161
162
|
|
|
162
163
|
### Per-Repo Fields
|
|
163
164
|
|
|
164
|
-
| Field | Description
|
|
165
|
-
| ---------- |
|
|
166
|
-
| `git` | Git URL (string) or array of URLs
|
|
167
|
-
| `content` | Content overlay merged onto base (optional if base exists)| No
|
|
168
|
-
| `override` | If `true`, ignore base content and use only this repo's
|
|
165
|
+
| Field | Description | Required |
|
|
166
|
+
| ---------- | ---------------------------------------------------------- | -------- |
|
|
167
|
+
| `git` | Git URL (string) or array of URLs | Yes |
|
|
168
|
+
| `content` | Content overlay merged onto base (optional if base exists) | No\* |
|
|
169
|
+
| `override` | If `true`, ignore base content and use only this repo's | No |
|
|
169
170
|
|
|
170
171
|
\* Required if no root-level `content` is defined.
|
|
171
172
|
|
|
@@ -175,8 +176,8 @@ Use `${VAR}` syntax in string values:
|
|
|
175
176
|
|
|
176
177
|
```yaml
|
|
177
178
|
content:
|
|
178
|
-
apiUrl: ${API_URL}
|
|
179
|
-
environment: ${ENV:-development}
|
|
179
|
+
apiUrl: ${API_URL} # Required - errors if not set
|
|
180
|
+
environment: ${ENV:-development} # With default value
|
|
180
181
|
secretKey: ${SECRET:?Secret required} # Required with custom error message
|
|
181
182
|
```
|
|
182
183
|
|
|
@@ -194,9 +195,9 @@ repos:
|
|
|
194
195
|
- git: git@github.com:org/repo.git
|
|
195
196
|
content:
|
|
196
197
|
features:
|
|
197
|
-
$arrayMerge: append
|
|
198
|
+
$arrayMerge: append # append | prepend | replace
|
|
198
199
|
values:
|
|
199
|
-
- custom-feature
|
|
200
|
+
- custom-feature # Results in: [core, monitoring, custom-feature]
|
|
200
201
|
```
|
|
201
202
|
|
|
202
203
|
## Examples
|
|
@@ -504,6 +505,43 @@ git config --global http.proxy http://proxy.example.com:8080
|
|
|
504
505
|
git config --global https.proxy http://proxy.example.com:8080
|
|
505
506
|
```
|
|
506
507
|
|
|
508
|
+
## IDE Integration
|
|
509
|
+
|
|
510
|
+
### VS Code YAML Schema Support
|
|
511
|
+
|
|
512
|
+
For autocomplete and validation in VS Code, install the [YAML extension](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) and add a schema reference to your config file:
|
|
513
|
+
|
|
514
|
+
**Option 1: Inline comment**
|
|
515
|
+
|
|
516
|
+
```yaml
|
|
517
|
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/anthony-spruyt/json-config-sync/main/config-schema.json
|
|
518
|
+
fileName: my.config.json
|
|
519
|
+
content:
|
|
520
|
+
key: value
|
|
521
|
+
repos:
|
|
522
|
+
- git: git@github.com:org/repo.git
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
**Option 2: VS Code settings** (`.vscode/settings.json`)
|
|
526
|
+
|
|
527
|
+
```json
|
|
528
|
+
{
|
|
529
|
+
"yaml.schemas": {
|
|
530
|
+
"https://raw.githubusercontent.com/anthony-spruyt/json-config-sync/main/config-schema.json": [
|
|
531
|
+
"**/sync-config.yaml",
|
|
532
|
+
"**/config-sync.yaml"
|
|
533
|
+
]
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
This enables:
|
|
539
|
+
|
|
540
|
+
- Autocomplete for `fileName`, `mergeStrategy`, `repos`, `content`, `git`, `override`
|
|
541
|
+
- Enum suggestions for `mergeStrategy` values (`replace`, `append`, `prepend`)
|
|
542
|
+
- Validation of required fields
|
|
543
|
+
- Hover documentation for each field
|
|
544
|
+
|
|
507
545
|
## Development
|
|
508
546
|
|
|
509
547
|
```bash
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { readFileSync } from
|
|
2
|
-
import { parse, stringify } from
|
|
3
|
-
import { deepMerge, stripMergeDirectives, createMergeContext, } from
|
|
4
|
-
import { interpolateEnvVars } from
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { parse, stringify } from "yaml";
|
|
3
|
+
import { deepMerge, stripMergeDirectives, createMergeContext, } from "./merge.js";
|
|
4
|
+
import { interpolateEnvVars } from "./env.js";
|
|
5
5
|
// =============================================================================
|
|
6
6
|
// Validation
|
|
7
7
|
// =============================================================================
|
|
8
8
|
function validateRawConfig(config) {
|
|
9
9
|
if (!config.fileName) {
|
|
10
|
-
throw new Error(
|
|
10
|
+
throw new Error("Config missing required field: fileName");
|
|
11
11
|
}
|
|
12
12
|
if (!config.repos || !Array.isArray(config.repos)) {
|
|
13
|
-
throw new Error(
|
|
13
|
+
throw new Error("Config missing required field: repos (must be an array)");
|
|
14
14
|
}
|
|
15
15
|
const hasRootContent = config.content !== undefined;
|
|
16
16
|
for (let i = 0; i < config.repos.length; i++) {
|
|
@@ -28,7 +28,7 @@ function validateRawConfig(config) {
|
|
|
28
28
|
}
|
|
29
29
|
function getGitDisplayName(git) {
|
|
30
30
|
if (Array.isArray(git)) {
|
|
31
|
-
return git[0] ||
|
|
31
|
+
return git[0] || "unknown";
|
|
32
32
|
}
|
|
33
33
|
return git;
|
|
34
34
|
}
|
|
@@ -37,7 +37,7 @@ function getGitDisplayName(git) {
|
|
|
37
37
|
// =============================================================================
|
|
38
38
|
function normalizeConfig(raw) {
|
|
39
39
|
const baseContent = raw.content ?? {};
|
|
40
|
-
const defaultStrategy = raw.mergeStrategy ??
|
|
40
|
+
const defaultStrategy = raw.mergeStrategy ?? "replace";
|
|
41
41
|
const expandedRepos = [];
|
|
42
42
|
for (const rawRepo of raw.repos) {
|
|
43
43
|
// Step 1: Expand git arrays
|
|
@@ -76,21 +76,21 @@ function normalizeConfig(raw) {
|
|
|
76
76
|
// Public API
|
|
77
77
|
// =============================================================================
|
|
78
78
|
export function loadConfig(filePath) {
|
|
79
|
-
const content = readFileSync(filePath,
|
|
79
|
+
const content = readFileSync(filePath, "utf-8");
|
|
80
80
|
const rawConfig = parse(content);
|
|
81
81
|
validateRawConfig(rawConfig);
|
|
82
82
|
return normalizeConfig(rawConfig);
|
|
83
83
|
}
|
|
84
84
|
function detectOutputFormat(fileName) {
|
|
85
|
-
const ext = fileName.toLowerCase().split(
|
|
86
|
-
if (ext ===
|
|
87
|
-
return
|
|
85
|
+
const ext = fileName.toLowerCase().split(".").pop();
|
|
86
|
+
if (ext === "yaml" || ext === "yml") {
|
|
87
|
+
return "yaml";
|
|
88
88
|
}
|
|
89
|
-
return
|
|
89
|
+
return "json";
|
|
90
90
|
}
|
|
91
91
|
export function convertContentToString(content, fileName) {
|
|
92
92
|
const format = detectOutputFormat(fileName);
|
|
93
|
-
if (format ===
|
|
93
|
+
if (format === "yaml") {
|
|
94
94
|
return stringify(content, { indent: 2 });
|
|
95
95
|
}
|
|
96
96
|
return JSON.stringify(content, null, 2);
|
package/dist/env.js
CHANGED
|
@@ -22,7 +22,7 @@ const ENV_VAR_REGEX = /\$\{([^}:]+)(?::([?-])([^}]*))?\}/g;
|
|
|
22
22
|
* Check if a value is a plain object (not null, not array).
|
|
23
23
|
*/
|
|
24
24
|
function isPlainObject(val) {
|
|
25
|
-
return typeof val ===
|
|
25
|
+
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* Process a single string value, replacing environment variable placeholders.
|
|
@@ -35,11 +35,11 @@ function processString(value, options) {
|
|
|
35
35
|
return envValue;
|
|
36
36
|
}
|
|
37
37
|
// Has default value (:-default)
|
|
38
|
-
if (modifier ===
|
|
39
|
-
return defaultOrMsg ??
|
|
38
|
+
if (modifier === "-") {
|
|
39
|
+
return defaultOrMsg ?? "";
|
|
40
40
|
}
|
|
41
41
|
// Required with message (:?message)
|
|
42
|
-
if (modifier ===
|
|
42
|
+
if (modifier === "?") {
|
|
43
43
|
const message = defaultOrMsg || `is required`;
|
|
44
44
|
throw new Error(`${varName}: ${message}`);
|
|
45
45
|
}
|
|
@@ -55,7 +55,7 @@ function processString(value, options) {
|
|
|
55
55
|
* Recursively process a value, interpolating environment variables in strings.
|
|
56
56
|
*/
|
|
57
57
|
function processValue(value, options) {
|
|
58
|
-
if (typeof value ===
|
|
58
|
+
if (typeof value === "string") {
|
|
59
59
|
return processString(value, options);
|
|
60
60
|
}
|
|
61
61
|
if (Array.isArray(value)) {
|
package/dist/git-ops.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { execSync } from
|
|
2
|
-
import { rmSync, existsSync, mkdirSync, writeFileSync, readFileSync } from
|
|
3
|
-
import { join } from
|
|
4
|
-
import { escapeShellArg } from
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { rmSync, existsSync, mkdirSync, writeFileSync, readFileSync, } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { escapeShellArg } from "./shell-utils.js";
|
|
5
5
|
export class GitOps {
|
|
6
6
|
workDir;
|
|
7
7
|
dryRun;
|
|
@@ -12,8 +12,8 @@ export class GitOps {
|
|
|
12
12
|
exec(command, cwd) {
|
|
13
13
|
return execSync(command, {
|
|
14
14
|
cwd: cwd ?? this.workDir,
|
|
15
|
-
encoding:
|
|
16
|
-
stdio: [
|
|
15
|
+
encoding: "utf-8",
|
|
16
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
17
17
|
}).trim();
|
|
18
18
|
}
|
|
19
19
|
cleanWorkspace() {
|
|
@@ -41,7 +41,7 @@ export class GitOps {
|
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
43
|
const filePath = join(this.workDir, fileName);
|
|
44
|
-
writeFileSync(filePath, content +
|
|
44
|
+
writeFileSync(filePath, content + "\n", "utf-8");
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
47
|
* Checks if writing the given content would result in changes.
|
|
@@ -49,13 +49,13 @@ export class GitOps {
|
|
|
49
49
|
*/
|
|
50
50
|
wouldChange(fileName, content) {
|
|
51
51
|
const filePath = join(this.workDir, fileName);
|
|
52
|
-
const newContent = content +
|
|
52
|
+
const newContent = content + "\n";
|
|
53
53
|
if (!existsSync(filePath)) {
|
|
54
54
|
// File doesn't exist, so writing it would be a change
|
|
55
55
|
return true;
|
|
56
56
|
}
|
|
57
57
|
try {
|
|
58
|
-
const existingContent = readFileSync(filePath,
|
|
58
|
+
const existingContent = readFileSync(filePath, "utf-8");
|
|
59
59
|
return existingContent !== newContent;
|
|
60
60
|
}
|
|
61
61
|
catch {
|
|
@@ -64,14 +64,14 @@ export class GitOps {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
hasChanges() {
|
|
67
|
-
const status = this.exec(
|
|
67
|
+
const status = this.exec("git status --porcelain", this.workDir);
|
|
68
68
|
return status.length > 0;
|
|
69
69
|
}
|
|
70
70
|
commit(message) {
|
|
71
71
|
if (this.dryRun) {
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
|
-
this.exec(
|
|
74
|
+
this.exec("git add -A", this.workDir);
|
|
75
75
|
this.exec(`git commit -m ${escapeShellArg(message)}`, this.workDir);
|
|
76
76
|
}
|
|
77
77
|
push(branchName) {
|
|
@@ -83,10 +83,10 @@ export class GitOps {
|
|
|
83
83
|
getDefaultBranch() {
|
|
84
84
|
try {
|
|
85
85
|
// Try to get the default branch from remote
|
|
86
|
-
const remoteInfo = this.exec(
|
|
86
|
+
const remoteInfo = this.exec("git remote show origin", this.workDir);
|
|
87
87
|
const match = remoteInfo.match(/HEAD branch: (\S+)/);
|
|
88
88
|
if (match) {
|
|
89
|
-
return { branch: match[1], method:
|
|
89
|
+
return { branch: match[1], method: "remote HEAD" };
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
catch {
|
|
@@ -94,27 +94,27 @@ export class GitOps {
|
|
|
94
94
|
}
|
|
95
95
|
// Try common default branch names
|
|
96
96
|
try {
|
|
97
|
-
this.exec(
|
|
98
|
-
return { branch:
|
|
97
|
+
this.exec("git rev-parse --verify origin/main", this.workDir);
|
|
98
|
+
return { branch: "main", method: "origin/main exists" };
|
|
99
99
|
}
|
|
100
100
|
catch {
|
|
101
101
|
// Try master
|
|
102
102
|
}
|
|
103
103
|
try {
|
|
104
|
-
this.exec(
|
|
105
|
-
return { branch:
|
|
104
|
+
this.exec("git rev-parse --verify origin/master", this.workDir);
|
|
105
|
+
return { branch: "master", method: "origin/master exists" };
|
|
106
106
|
}
|
|
107
107
|
catch {
|
|
108
108
|
// Default to main
|
|
109
109
|
}
|
|
110
|
-
return { branch:
|
|
110
|
+
return { branch: "main", method: "fallback default" };
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
export function sanitizeBranchName(fileName) {
|
|
114
114
|
return fileName
|
|
115
115
|
.toLowerCase()
|
|
116
|
-
.replace(/\.[^.]+$/,
|
|
117
|
-
.replace(/[^a-z0-9-]/g,
|
|
118
|
-
.replace(/-+/g,
|
|
119
|
-
.replace(/^-|-$/g,
|
|
116
|
+
.replace(/\.[^.]+$/, "") // Remove extension
|
|
117
|
+
.replace(/[^a-z0-9-]/g, "-") // Replace non-alphanumeric with dashes
|
|
118
|
+
.replace(/-+/g, "-") // Collapse multiple dashes
|
|
119
|
+
.replace(/^-|-$/g, ""); // Remove leading/trailing dashes
|
|
120
120
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { program } from
|
|
3
|
-
import { resolve, join } from
|
|
4
|
-
import { existsSync } from
|
|
5
|
-
import { randomUUID } from
|
|
6
|
-
import { loadConfig, convertContentToString } from
|
|
7
|
-
import { parseGitUrl, getRepoDisplayName } from
|
|
8
|
-
import { GitOps, sanitizeBranchName } from
|
|
9
|
-
import { createPR } from
|
|
10
|
-
import { logger } from
|
|
2
|
+
import { program } from "commander";
|
|
3
|
+
import { resolve, join } from "node:path";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { loadConfig, convertContentToString } from "./config.js";
|
|
7
|
+
import { parseGitUrl, getRepoDisplayName } from "./repo-detector.js";
|
|
8
|
+
import { GitOps, sanitizeBranchName } from "./git-ops.js";
|
|
9
|
+
import { createPR } from "./pr-creator.js";
|
|
10
|
+
import { logger } from "./logger.js";
|
|
11
11
|
/**
|
|
12
12
|
* Generates a unique workspace directory name to avoid collisions
|
|
13
13
|
* when multiple CLI instances run concurrently.
|
|
@@ -18,12 +18,12 @@ function generateWorkspaceName(index) {
|
|
|
18
18
|
return `repo-${timestamp}-${index}-${uuid}`;
|
|
19
19
|
}
|
|
20
20
|
program
|
|
21
|
-
.name(
|
|
22
|
-
.description(
|
|
23
|
-
.version(
|
|
24
|
-
.requiredOption(
|
|
25
|
-
.option(
|
|
26
|
-
.option(
|
|
21
|
+
.name("json-config-sync")
|
|
22
|
+
.description("Sync JSON configuration files across multiple repositories")
|
|
23
|
+
.version("1.0.0")
|
|
24
|
+
.requiredOption("-c, --config <path>", "Path to YAML config file")
|
|
25
|
+
.option("-d, --dry-run", "Show what would be done without making changes")
|
|
26
|
+
.option("-w, --work-dir <path>", "Temporary directory for cloning", "./tmp")
|
|
27
27
|
.parse();
|
|
28
28
|
const options = program.opts();
|
|
29
29
|
async function main() {
|
|
@@ -34,7 +34,7 @@ async function main() {
|
|
|
34
34
|
}
|
|
35
35
|
console.log(`Loading config from: ${configPath}`);
|
|
36
36
|
if (options.dryRun) {
|
|
37
|
-
console.log(
|
|
37
|
+
console.log("Running in DRY RUN mode - no changes will be made\n");
|
|
38
38
|
}
|
|
39
39
|
const config = loadConfig(configPath);
|
|
40
40
|
const branchName = `chore/sync-${sanitizeBranchName(config.fileName)}`;
|
|
@@ -55,15 +55,15 @@ async function main() {
|
|
|
55
55
|
continue;
|
|
56
56
|
}
|
|
57
57
|
const repoName = getRepoDisplayName(repoInfo);
|
|
58
|
-
const workDir = resolve(join(options.workDir ??
|
|
58
|
+
const workDir = resolve(join(options.workDir ?? "./tmp", generateWorkspaceName(i)));
|
|
59
59
|
try {
|
|
60
|
-
logger.progress(current, repoName,
|
|
60
|
+
logger.progress(current, repoName, "Processing...");
|
|
61
61
|
const gitOps = new GitOps({ workDir, dryRun: options.dryRun });
|
|
62
62
|
// Step 1: Clean workspace
|
|
63
|
-
logger.info(
|
|
63
|
+
logger.info("Cleaning workspace...");
|
|
64
64
|
gitOps.cleanWorkspace();
|
|
65
65
|
// Step 2: Clone repo
|
|
66
|
-
logger.info(
|
|
66
|
+
logger.info("Cloning repository...");
|
|
67
67
|
gitOps.clone(repoInfo.gitUrl);
|
|
68
68
|
// Step 3: Get default branch for PR base
|
|
69
69
|
const { branch: baseBranch, method: detectionMethod } = gitOps.getDefaultBranch();
|
|
@@ -72,7 +72,9 @@ async function main() {
|
|
|
72
72
|
logger.info(`Switching to branch: ${branchName}`);
|
|
73
73
|
gitOps.createBranch(branchName);
|
|
74
74
|
// Determine if creating or updating (check BEFORE writing)
|
|
75
|
-
const action = existsSync(join(workDir, config.fileName))
|
|
75
|
+
const action = existsSync(join(workDir, config.fileName))
|
|
76
|
+
? "update"
|
|
77
|
+
: "create";
|
|
76
78
|
// Step 5: Write config file
|
|
77
79
|
logger.info(`Writing ${config.fileName}...`);
|
|
78
80
|
const fileContent = convertContentToString(repoConfig.content, config.fileName);
|
|
@@ -86,17 +88,17 @@ async function main() {
|
|
|
86
88
|
return gitOps.hasChanges();
|
|
87
89
|
})();
|
|
88
90
|
if (!wouldHaveChanges) {
|
|
89
|
-
logger.skip(current, repoName,
|
|
91
|
+
logger.skip(current, repoName, "No changes detected");
|
|
90
92
|
continue;
|
|
91
93
|
}
|
|
92
94
|
// Step 7: Commit
|
|
93
|
-
logger.info(
|
|
95
|
+
logger.info("Committing changes...");
|
|
94
96
|
gitOps.commit(`chore: sync ${config.fileName}`);
|
|
95
97
|
// Step 8: Push
|
|
96
|
-
logger.info(
|
|
98
|
+
logger.info("Pushing to remote...");
|
|
97
99
|
gitOps.push(branchName);
|
|
98
100
|
// Step 9: Create PR
|
|
99
|
-
logger.info(
|
|
101
|
+
logger.info("Creating pull request...");
|
|
100
102
|
const prResult = await createPR({
|
|
101
103
|
repoInfo,
|
|
102
104
|
branchName,
|
|
@@ -124,6 +126,6 @@ async function main() {
|
|
|
124
126
|
}
|
|
125
127
|
}
|
|
126
128
|
main().catch((error) => {
|
|
127
|
-
console.error(
|
|
129
|
+
console.error("Fatal error:", error);
|
|
128
130
|
process.exit(1);
|
|
129
131
|
});
|
package/dist/logger.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import chalk from
|
|
1
|
+
import chalk from "chalk";
|
|
2
2
|
export class Logger {
|
|
3
3
|
stats = {
|
|
4
4
|
total: 0,
|
|
@@ -10,26 +10,30 @@ export class Logger {
|
|
|
10
10
|
this.stats.total = total;
|
|
11
11
|
}
|
|
12
12
|
progress(current, repoName, message) {
|
|
13
|
-
console.log(chalk.blue(`[${current}/${this.stats.total}]`) +
|
|
13
|
+
console.log(chalk.blue(`[${current}/${this.stats.total}]`) +
|
|
14
|
+
` ${repoName}: ${message}`);
|
|
14
15
|
}
|
|
15
16
|
info(message) {
|
|
16
17
|
console.log(chalk.gray(` ${message}`));
|
|
17
18
|
}
|
|
18
19
|
success(current, repoName, message) {
|
|
19
20
|
this.stats.succeeded++;
|
|
20
|
-
console.log(chalk.green(`[${current}/${this.stats.total}] ✓`) +
|
|
21
|
+
console.log(chalk.green(`[${current}/${this.stats.total}] ✓`) +
|
|
22
|
+
` ${repoName}: ${message}`);
|
|
21
23
|
}
|
|
22
24
|
skip(current, repoName, reason) {
|
|
23
25
|
this.stats.skipped++;
|
|
24
|
-
console.log(chalk.yellow(`[${current}/${this.stats.total}] ⊘`) +
|
|
26
|
+
console.log(chalk.yellow(`[${current}/${this.stats.total}] ⊘`) +
|
|
27
|
+
` ${repoName}: Skipped - ${reason}`);
|
|
25
28
|
}
|
|
26
29
|
error(current, repoName, error) {
|
|
27
30
|
this.stats.failed++;
|
|
28
|
-
console.log(chalk.red(`[${current}/${this.stats.total}] ✗`) +
|
|
31
|
+
console.log(chalk.red(`[${current}/${this.stats.total}] ✗`) +
|
|
32
|
+
` ${repoName}: ${error}`);
|
|
29
33
|
}
|
|
30
34
|
summary() {
|
|
31
|
-
console.log(
|
|
32
|
-
console.log(chalk.bold(
|
|
35
|
+
console.log("");
|
|
36
|
+
console.log(chalk.bold("Summary:"));
|
|
33
37
|
console.log(` Total: ${this.stats.total}`);
|
|
34
38
|
console.log(chalk.green(` Succeeded: ${this.stats.succeeded}`));
|
|
35
39
|
console.log(chalk.yellow(` Skipped: ${this.stats.skipped}`));
|
package/dist/merge.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Deep merge utilities for JSON configuration objects.
|
|
3
3
|
* Supports configurable array merge strategies via $arrayMerge directive.
|
|
4
4
|
*/
|
|
5
|
-
export type ArrayMergeStrategy =
|
|
5
|
+
export type ArrayMergeStrategy = "replace" | "append" | "prepend";
|
|
6
6
|
export interface MergeContext {
|
|
7
7
|
arrayStrategies: Map<string, ArrayMergeStrategy>;
|
|
8
8
|
defaultArrayStrategy: ArrayMergeStrategy;
|
package/dist/merge.js
CHANGED
|
@@ -6,18 +6,18 @@
|
|
|
6
6
|
* Check if a value is a plain object (not null, not array).
|
|
7
7
|
*/
|
|
8
8
|
function isPlainObject(val) {
|
|
9
|
-
return typeof val ===
|
|
9
|
+
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
12
|
* Merge two arrays based on the specified strategy.
|
|
13
13
|
*/
|
|
14
14
|
function mergeArrays(base, overlay, strategy) {
|
|
15
15
|
switch (strategy) {
|
|
16
|
-
case
|
|
16
|
+
case "replace":
|
|
17
17
|
return overlay;
|
|
18
|
-
case
|
|
18
|
+
case "append":
|
|
19
19
|
return [...base, ...overlay];
|
|
20
|
-
case
|
|
20
|
+
case "prepend":
|
|
21
21
|
return [...overlay, ...base];
|
|
22
22
|
default:
|
|
23
23
|
return overlay;
|
|
@@ -33,7 +33,7 @@ function extractArrayFromOverlay(overlay) {
|
|
|
33
33
|
if (Array.isArray(overlay)) {
|
|
34
34
|
return overlay;
|
|
35
35
|
}
|
|
36
|
-
if (isPlainObject(overlay) &&
|
|
36
|
+
if (isPlainObject(overlay) && "values" in overlay) {
|
|
37
37
|
const values = overlay.values;
|
|
38
38
|
if (Array.isArray(values)) {
|
|
39
39
|
return values;
|
|
@@ -45,11 +45,11 @@ function extractArrayFromOverlay(overlay) {
|
|
|
45
45
|
* Get merge strategy from an overlay object's $arrayMerge directive.
|
|
46
46
|
*/
|
|
47
47
|
function getStrategyFromOverlay(overlay) {
|
|
48
|
-
if (isPlainObject(overlay) &&
|
|
48
|
+
if (isPlainObject(overlay) && "$arrayMerge" in overlay) {
|
|
49
49
|
const strategy = overlay.$arrayMerge;
|
|
50
|
-
if (strategy ===
|
|
51
|
-
strategy ===
|
|
52
|
-
strategy ===
|
|
50
|
+
if (strategy === "replace" ||
|
|
51
|
+
strategy === "append" ||
|
|
52
|
+
strategy === "prepend") {
|
|
53
53
|
return strategy;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -63,18 +63,18 @@ function getStrategyFromOverlay(overlay) {
|
|
|
63
63
|
* @param ctx - Merge context with array strategies
|
|
64
64
|
* @param path - Current path for strategy lookup (internal)
|
|
65
65
|
*/
|
|
66
|
-
export function deepMerge(base, overlay, ctx, path =
|
|
66
|
+
export function deepMerge(base, overlay, ctx, path = "") {
|
|
67
67
|
const result = { ...base };
|
|
68
68
|
// Check for $arrayMerge directive at this level (applies to child arrays)
|
|
69
69
|
const levelStrategy = getStrategyFromOverlay(overlay);
|
|
70
70
|
for (const [key, overlayValue] of Object.entries(overlay)) {
|
|
71
71
|
// Skip directive keys in output
|
|
72
|
-
if (key.startsWith(
|
|
72
|
+
if (key.startsWith("$"))
|
|
73
73
|
continue;
|
|
74
74
|
const currentPath = path ? `${path}.${key}` : key;
|
|
75
75
|
const baseValue = base[key];
|
|
76
76
|
// If overlay is an object with $arrayMerge directive for an array field
|
|
77
|
-
if (isPlainObject(overlayValue) &&
|
|
77
|
+
if (isPlainObject(overlayValue) && "$arrayMerge" in overlayValue) {
|
|
78
78
|
const strategy = getStrategyFromOverlay(overlayValue);
|
|
79
79
|
const overlayArray = extractArrayFromOverlay(overlayValue);
|
|
80
80
|
if (strategy && overlayArray && Array.isArray(baseValue)) {
|
|
@@ -94,13 +94,15 @@ export function deepMerge(base, overlay, ctx, path = '') {
|
|
|
94
94
|
// Both are plain objects - recurse
|
|
95
95
|
if (isPlainObject(baseValue) && isPlainObject(overlayValue)) {
|
|
96
96
|
// Extract $arrayMerge for child paths if present
|
|
97
|
-
if (
|
|
97
|
+
if ("$arrayMerge" in overlayValue) {
|
|
98
98
|
const childStrategy = getStrategyFromOverlay(overlayValue);
|
|
99
99
|
if (childStrategy) {
|
|
100
100
|
// Apply to all immediate child arrays
|
|
101
101
|
for (const childKey of Object.keys(overlayValue)) {
|
|
102
|
-
if (!childKey.startsWith(
|
|
103
|
-
const childPath = currentPath
|
|
102
|
+
if (!childKey.startsWith("$")) {
|
|
103
|
+
const childPath = currentPath
|
|
104
|
+
? `${currentPath}.${childKey}`
|
|
105
|
+
: childKey;
|
|
104
106
|
ctx.arrayStrategies.set(childPath, childStrategy);
|
|
105
107
|
}
|
|
106
108
|
}
|
|
@@ -122,7 +124,7 @@ export function stripMergeDirectives(obj) {
|
|
|
122
124
|
const result = {};
|
|
123
125
|
for (const [key, value] of Object.entries(obj)) {
|
|
124
126
|
// Skip all $-prefixed keys (reserved for directives)
|
|
125
|
-
if (key.startsWith(
|
|
127
|
+
if (key.startsWith("$"))
|
|
126
128
|
continue;
|
|
127
129
|
if (isPlainObject(value)) {
|
|
128
130
|
result[key] = stripMergeDirectives(value);
|
|
@@ -139,7 +141,7 @@ export function stripMergeDirectives(obj) {
|
|
|
139
141
|
/**
|
|
140
142
|
* Create a default merge context.
|
|
141
143
|
*/
|
|
142
|
-
export function createMergeContext(defaultStrategy =
|
|
144
|
+
export function createMergeContext(defaultStrategy = "replace") {
|
|
143
145
|
return {
|
|
144
146
|
arrayStrategies: new Map(),
|
|
145
147
|
defaultArrayStrategy: defaultStrategy,
|
package/dist/pr-creator.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { RepoInfo } from
|
|
2
|
-
export { escapeShellArg } from
|
|
1
|
+
import { RepoInfo } from "./repo-detector.js";
|
|
2
|
+
export { escapeShellArg } from "./shell-utils.js";
|
|
3
3
|
export interface PROptions {
|
|
4
4
|
repoInfo: RepoInfo;
|
|
5
5
|
branchName: string;
|
|
6
6
|
baseBranch: string;
|
|
7
7
|
fileName: string;
|
|
8
|
-
action:
|
|
8
|
+
action: "create" | "update";
|
|
9
9
|
workDir: string;
|
|
10
10
|
dryRun?: boolean;
|
|
11
11
|
}
|
|
@@ -14,5 +14,5 @@ export interface PRResult {
|
|
|
14
14
|
success: boolean;
|
|
15
15
|
message: string;
|
|
16
16
|
}
|
|
17
|
-
export declare function formatPRBody(fileName: string, action:
|
|
17
|
+
export declare function formatPRBody(fileName: string, action: "create" | "update"): string;
|
|
18
18
|
export declare function createPR(options: PROptions): Promise<PRResult>;
|
package/dist/pr-creator.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { execSync } from
|
|
2
|
-
import { readFileSync, existsSync, writeFileSync, unlinkSync } from
|
|
3
|
-
import { join, dirname } from
|
|
4
|
-
import { fileURLToPath } from
|
|
5
|
-
import { escapeShellArg } from
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { readFileSync, existsSync, writeFileSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { escapeShellArg } from "./shell-utils.js";
|
|
6
6
|
// Re-export for backwards compatibility and testing
|
|
7
|
-
export { escapeShellArg } from
|
|
7
|
+
export { escapeShellArg } from "./shell-utils.js";
|
|
8
8
|
function loadPRTemplate() {
|
|
9
9
|
// Try to find PR.md in the project root
|
|
10
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
11
|
const __dirname = dirname(__filename);
|
|
12
|
-
const templatePath = join(__dirname,
|
|
12
|
+
const templatePath = join(__dirname, "..", "PR.md");
|
|
13
13
|
if (existsSync(templatePath)) {
|
|
14
|
-
return readFileSync(templatePath,
|
|
14
|
+
return readFileSync(templatePath, "utf-8");
|
|
15
15
|
}
|
|
16
16
|
// Fallback template
|
|
17
17
|
return `## Summary
|
|
@@ -25,13 +25,13 @@ Automated sync of \`{{FILE_NAME}}\` configuration file.
|
|
|
25
25
|
}
|
|
26
26
|
export function formatPRBody(fileName, action) {
|
|
27
27
|
const template = loadPRTemplate();
|
|
28
|
-
const actionText = action ===
|
|
28
|
+
const actionText = action === "create" ? "Created" : "Updated";
|
|
29
29
|
return template
|
|
30
30
|
.replace(/\{\{FILE_NAME\}\}/g, fileName)
|
|
31
31
|
.replace(/\{\{ACTION\}\}/g, actionText);
|
|
32
32
|
}
|
|
33
33
|
export async function createPR(options) {
|
|
34
|
-
const { repoInfo, branchName, baseBranch, fileName, action, workDir, dryRun } = options;
|
|
34
|
+
const { repoInfo, branchName, baseBranch, fileName, action, workDir, dryRun, } = options;
|
|
35
35
|
const title = `chore: sync ${fileName}`;
|
|
36
36
|
const body = formatPRBody(fileName, action);
|
|
37
37
|
if (dryRun) {
|
|
@@ -41,8 +41,14 @@ export async function createPR(options) {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
try {
|
|
44
|
-
if (repoInfo.type ===
|
|
45
|
-
return await createGitHubPR({
|
|
44
|
+
if (repoInfo.type === "github") {
|
|
45
|
+
return await createGitHubPR({
|
|
46
|
+
title,
|
|
47
|
+
body,
|
|
48
|
+
branchName,
|
|
49
|
+
baseBranch,
|
|
50
|
+
workDir,
|
|
51
|
+
});
|
|
46
52
|
}
|
|
47
53
|
else {
|
|
48
54
|
return await createAzureDevOpsPR({
|
|
@@ -51,7 +57,7 @@ export async function createPR(options) {
|
|
|
51
57
|
branchName,
|
|
52
58
|
baseBranch,
|
|
53
59
|
repoInfo,
|
|
54
|
-
workDir
|
|
60
|
+
workDir,
|
|
55
61
|
});
|
|
56
62
|
}
|
|
57
63
|
}
|
|
@@ -67,7 +73,7 @@ async function createGitHubPR(options) {
|
|
|
67
73
|
const { title, body, branchName, baseBranch, workDir } = options;
|
|
68
74
|
// Check if PR already exists
|
|
69
75
|
try {
|
|
70
|
-
const existingPR = execSync(`gh pr list --head ${escapeShellArg(branchName)} --json url --jq '.[0].url'`, { cwd: workDir, encoding:
|
|
76
|
+
const existingPR = execSync(`gh pr list --head ${escapeShellArg(branchName)} --json url --jq '.[0].url'`, { cwd: workDir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
71
77
|
if (existingPR) {
|
|
72
78
|
return {
|
|
73
79
|
url: existingPR,
|
|
@@ -80,16 +86,16 @@ async function createGitHubPR(options) {
|
|
|
80
86
|
// No existing PR, continue to create
|
|
81
87
|
}
|
|
82
88
|
// Write body to temp file to avoid shell escaping issues
|
|
83
|
-
const bodyFile = join(workDir,
|
|
84
|
-
writeFileSync(bodyFile, body,
|
|
89
|
+
const bodyFile = join(workDir, ".pr-body.md");
|
|
90
|
+
writeFileSync(bodyFile, body, "utf-8");
|
|
85
91
|
try {
|
|
86
|
-
const result = execSync(`gh pr create --title ${escapeShellArg(title)} --body-file ${escapeShellArg(bodyFile)} --base ${escapeShellArg(baseBranch)} --head ${escapeShellArg(branchName)}`, { cwd: workDir, encoding:
|
|
92
|
+
const result = execSync(`gh pr create --title ${escapeShellArg(title)} --body-file ${escapeShellArg(bodyFile)} --base ${escapeShellArg(baseBranch)} --head ${escapeShellArg(branchName)}`, { cwd: workDir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
87
93
|
// Extract URL from output
|
|
88
94
|
const urlMatch = result.match(/https:\/\/github\.com\/[^\s]+/);
|
|
89
95
|
return {
|
|
90
96
|
url: urlMatch?.[0] ?? result,
|
|
91
97
|
success: true,
|
|
92
|
-
message:
|
|
98
|
+
message: "PR created successfully",
|
|
93
99
|
};
|
|
94
100
|
}
|
|
95
101
|
finally {
|
|
@@ -101,12 +107,12 @@ async function createGitHubPR(options) {
|
|
|
101
107
|
}
|
|
102
108
|
async function createAzureDevOpsPR(options) {
|
|
103
109
|
const { title, body, branchName, baseBranch, repoInfo, workDir } = options;
|
|
104
|
-
const orgUrl = `https://dev.azure.com/${encodeURIComponent(repoInfo.organization ??
|
|
110
|
+
const orgUrl = `https://dev.azure.com/${encodeURIComponent(repoInfo.organization ?? "")}`;
|
|
105
111
|
// Check if PR already exists
|
|
106
112
|
try {
|
|
107
|
-
const existingPRs = execSync(`az repos pr list --repository ${escapeShellArg(repoInfo.repo)} --source-branch ${escapeShellArg(branchName)} --target-branch ${escapeShellArg(baseBranch)} --org ${escapeShellArg(orgUrl)} --project ${escapeShellArg(repoInfo.project ??
|
|
113
|
+
const existingPRs = execSync(`az repos pr list --repository ${escapeShellArg(repoInfo.repo)} --source-branch ${escapeShellArg(branchName)} --target-branch ${escapeShellArg(baseBranch)} --org ${escapeShellArg(orgUrl)} --project ${escapeShellArg(repoInfo.project ?? "")} --query "[0].pullRequestId" -o tsv`, { cwd: workDir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
108
114
|
if (existingPRs) {
|
|
109
|
-
const prUrl = `https://dev.azure.com/${encodeURIComponent(repoInfo.organization ??
|
|
115
|
+
const prUrl = `https://dev.azure.com/${encodeURIComponent(repoInfo.organization ?? "")}/${encodeURIComponent(repoInfo.project ?? "")}/_git/${encodeURIComponent(repoInfo.repo)}/pullrequest/${existingPRs}`;
|
|
110
116
|
return {
|
|
111
117
|
url: prUrl,
|
|
112
118
|
success: true,
|
|
@@ -118,15 +124,15 @@ async function createAzureDevOpsPR(options) {
|
|
|
118
124
|
// No existing PR, continue to create
|
|
119
125
|
}
|
|
120
126
|
// Write description to temp file to avoid shell escaping issues
|
|
121
|
-
const descFile = join(workDir,
|
|
122
|
-
writeFileSync(descFile, body,
|
|
127
|
+
const descFile = join(workDir, ".pr-description.md");
|
|
128
|
+
writeFileSync(descFile, body, "utf-8");
|
|
123
129
|
try {
|
|
124
|
-
const result = execSync(`az repos pr create --repository ${escapeShellArg(repoInfo.repo)} --source-branch ${escapeShellArg(branchName)} --target-branch ${escapeShellArg(baseBranch)} --title ${escapeShellArg(title)} --description @${escapeShellArg(descFile)} --org ${escapeShellArg(orgUrl)} --project ${escapeShellArg(repoInfo.project ??
|
|
125
|
-
const prUrl = `https://dev.azure.com/${encodeURIComponent(repoInfo.organization ??
|
|
130
|
+
const result = execSync(`az repos pr create --repository ${escapeShellArg(repoInfo.repo)} --source-branch ${escapeShellArg(branchName)} --target-branch ${escapeShellArg(baseBranch)} --title ${escapeShellArg(title)} --description @${escapeShellArg(descFile)} --org ${escapeShellArg(orgUrl)} --project ${escapeShellArg(repoInfo.project ?? "")} --query "pullRequestId" -o tsv`, { cwd: workDir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
131
|
+
const prUrl = `https://dev.azure.com/${encodeURIComponent(repoInfo.organization ?? "")}/${encodeURIComponent(repoInfo.project ?? "")}/_git/${encodeURIComponent(repoInfo.repo)}/pullrequest/${result}`;
|
|
126
132
|
return {
|
|
127
133
|
url: prUrl,
|
|
128
134
|
success: true,
|
|
129
|
-
message:
|
|
135
|
+
message: "PR created successfully",
|
|
130
136
|
};
|
|
131
137
|
}
|
|
132
138
|
finally {
|
package/dist/repo-detector.d.ts
CHANGED
package/dist/repo-detector.js
CHANGED
|
@@ -2,17 +2,17 @@ export function detectRepoType(gitUrl) {
|
|
|
2
2
|
// Check for Azure DevOps SSH format: git@ssh.dev.azure.com:...
|
|
3
3
|
// Use broader pattern to catch malformed Azure URLs
|
|
4
4
|
if (/^git@ssh\.dev\.azure\.com:/.test(gitUrl)) {
|
|
5
|
-
return
|
|
5
|
+
return "azure-devops";
|
|
6
6
|
}
|
|
7
7
|
// Check for Azure DevOps HTTPS format: https://dev.azure.com/...
|
|
8
8
|
if (/^https?:\/\/dev\.azure\.com\//.test(gitUrl)) {
|
|
9
|
-
return
|
|
9
|
+
return "azure-devops";
|
|
10
10
|
}
|
|
11
|
-
return
|
|
11
|
+
return "github";
|
|
12
12
|
}
|
|
13
13
|
export function parseGitUrl(gitUrl) {
|
|
14
14
|
const type = detectRepoType(gitUrl);
|
|
15
|
-
if (type ===
|
|
15
|
+
if (type === "azure-devops") {
|
|
16
16
|
return parseAzureDevOpsUrl(gitUrl);
|
|
17
17
|
}
|
|
18
18
|
return parseGitHubUrl(gitUrl);
|
|
@@ -23,7 +23,7 @@ function parseGitHubUrl(gitUrl) {
|
|
|
23
23
|
const sshMatch = gitUrl.match(/git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/);
|
|
24
24
|
if (sshMatch) {
|
|
25
25
|
return {
|
|
26
|
-
type:
|
|
26
|
+
type: "github",
|
|
27
27
|
gitUrl,
|
|
28
28
|
owner: sshMatch[1],
|
|
29
29
|
repo: sshMatch[2],
|
|
@@ -34,7 +34,7 @@ function parseGitHubUrl(gitUrl) {
|
|
|
34
34
|
const httpsMatch = gitUrl.match(/https?:\/\/github\.com\/([^/]+)\/(.+?)(?:\.git)?$/);
|
|
35
35
|
if (httpsMatch) {
|
|
36
36
|
return {
|
|
37
|
-
type:
|
|
37
|
+
type: "github",
|
|
38
38
|
gitUrl,
|
|
39
39
|
owner: httpsMatch[1],
|
|
40
40
|
repo: httpsMatch[2],
|
|
@@ -48,7 +48,7 @@ function parseAzureDevOpsUrl(gitUrl) {
|
|
|
48
48
|
const sshMatch = gitUrl.match(/git@ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/(.+?)(?:\.git)?$/);
|
|
49
49
|
if (sshMatch) {
|
|
50
50
|
return {
|
|
51
|
-
type:
|
|
51
|
+
type: "azure-devops",
|
|
52
52
|
gitUrl,
|
|
53
53
|
owner: sshMatch[1],
|
|
54
54
|
repo: sshMatch[3],
|
|
@@ -61,7 +61,7 @@ function parseAzureDevOpsUrl(gitUrl) {
|
|
|
61
61
|
const httpsMatch = gitUrl.match(/https?:\/\/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/(.+?)(?:\.git)?$/);
|
|
62
62
|
if (httpsMatch) {
|
|
63
63
|
return {
|
|
64
|
-
type:
|
|
64
|
+
type: "azure-devops",
|
|
65
65
|
gitUrl,
|
|
66
66
|
owner: httpsMatch[1],
|
|
67
67
|
repo: httpsMatch[3],
|
|
@@ -72,7 +72,7 @@ function parseAzureDevOpsUrl(gitUrl) {
|
|
|
72
72
|
throw new Error(`Unable to parse Azure DevOps URL: ${gitUrl}`);
|
|
73
73
|
}
|
|
74
74
|
export function getRepoDisplayName(repoInfo) {
|
|
75
|
-
if (repoInfo.type ===
|
|
75
|
+
if (repoInfo.type === "azure-devops") {
|
|
76
76
|
return `${repoInfo.organization}/${repoInfo.project}/${repoInfo.repo}`;
|
|
77
77
|
}
|
|
78
78
|
return `${repoInfo.owner}/${repoInfo.repo}`;
|
package/package.json
CHANGED