taskchef 3.0.2 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/BACKLOG.md +15 -0
- package/README.md +48 -16
- package/SPEC.md +111 -44
- package/assets/taskchef-dispatcher-instructions.md +2 -0
- package/index.js +24 -0
- package/package.json +1 -1
- package/skills/taskchef-bootstrap/SKILL.md +29 -8
- package/skills/taskchef-delegate/SKILL.md +85 -14
- package/skills/taskchef-report/SKILL.md +20 -10
- package/src/cli.js +33 -5
- package/src/delegation.js +555 -0
- package/src/github.js +108 -0
- package/src/workspace.js +152 -240
package/src/github.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
function requireRepositoryString(value, name) {
|
|
2
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
3
|
+
throw new Error(`${name} must be a non-empty string`);
|
|
4
|
+
}
|
|
5
|
+
return value.trim();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function parseGithubUrl(value, name, { allowIssueOrPull = false } = {}) {
|
|
9
|
+
let remote = requireRepositoryString(value, name);
|
|
10
|
+
const scpMatch = remote.match(/^git@github\.com:([^/]+)\/([^/]+)\/?$/i);
|
|
11
|
+
if (scpMatch) {
|
|
12
|
+
return {
|
|
13
|
+
owner: scpMatch[1],
|
|
14
|
+
repository: scpMatch[2].replace(/\.git$/i, ""),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (/^(?:www\.)?github\.com\//i.test(remote)) remote = `https://${remote}`;
|
|
18
|
+
let url;
|
|
19
|
+
try {
|
|
20
|
+
url = new URL(remote);
|
|
21
|
+
} catch {
|
|
22
|
+
throw new Error(`${name} must be a GitHub repository URL`);
|
|
23
|
+
}
|
|
24
|
+
if (
|
|
25
|
+
!["https:", "http:", "ssh:", "git:"].includes(url.protocol)
|
|
26
|
+
|| !["github.com", "www.github.com"].includes(url.hostname.toLowerCase())
|
|
27
|
+
|| (url.username && !(url.protocol === "ssh:" && url.username === "git"))
|
|
28
|
+
|| url.password
|
|
29
|
+
|| url.port
|
|
30
|
+
|| (!allowIssueOrPull && (url.search || url.hash))
|
|
31
|
+
) {
|
|
32
|
+
throw new Error(`${name} must be a GitHub repository URL`);
|
|
33
|
+
}
|
|
34
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
35
|
+
if (segments.length < 2) throw new Error(`${name} must identify one GitHub repository`);
|
|
36
|
+
const suffix = segments.slice(2);
|
|
37
|
+
if (
|
|
38
|
+
(!allowIssueOrPull && suffix.length > 0)
|
|
39
|
+
|| (
|
|
40
|
+
allowIssueOrPull
|
|
41
|
+
&& suffix.length > 0
|
|
42
|
+
&& !(suffix.length >= 2 && ["issues", "pull"].includes(suffix[0]) && /^\d+$/.test(suffix[1]))
|
|
43
|
+
)
|
|
44
|
+
) {
|
|
45
|
+
throw new Error(`${name} must identify one GitHub repository`);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
owner: segments[0],
|
|
49
|
+
repository: segments[1].replace(/\.git$/i, ""),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function canonicalGithubRepository(value, name = "githubRepos") {
|
|
54
|
+
const { owner, repository } = parseGithubUrl(value, name);
|
|
55
|
+
if (repository.length === 0) throw new Error(`${name} must identify one GitHub repository`);
|
|
56
|
+
return `https://github.com/${owner}/${repository}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function normalizeGithubRepositories(
|
|
60
|
+
value,
|
|
61
|
+
name = "githubRepos",
|
|
62
|
+
{ allowLegacyScalar = false } = {},
|
|
63
|
+
) {
|
|
64
|
+
if (allowLegacyScalar && (value === null || typeof value === "string")) {
|
|
65
|
+
value = value === null ? [] : [value];
|
|
66
|
+
}
|
|
67
|
+
if (!Array.isArray(value)) throw new Error(`${name} must be an array of GitHub repository URLs`);
|
|
68
|
+
const repositories = [];
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
for (const [index, repository] of value.entries()) {
|
|
71
|
+
const canonical = canonicalGithubRepository(repository, `${name}[${index}]`);
|
|
72
|
+
const key = canonical.toLowerCase();
|
|
73
|
+
if (!seen.has(key)) {
|
|
74
|
+
seen.add(key);
|
|
75
|
+
repositories.push(canonical);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return repositories;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function matchProjectForGithubUrl(value, projects) {
|
|
82
|
+
if (!Array.isArray(projects)) throw new Error("projects must be an array");
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = parseGithubUrl(value, "GitHub URL", { allowIssueOrPull: true });
|
|
86
|
+
} catch {
|
|
87
|
+
return { status: "unmatched", repository: null, projects: [] };
|
|
88
|
+
}
|
|
89
|
+
const repository = `https://github.com/${parsed.owner}/${parsed.repository}`;
|
|
90
|
+
const key = repository.toLowerCase();
|
|
91
|
+
const matches = projects.filter((project) =>
|
|
92
|
+
Array.isArray(project?.githubRepos)
|
|
93
|
+
&& project.githubRepos.some((candidate) => {
|
|
94
|
+
try {
|
|
95
|
+
return canonicalGithubRepository(candidate).toLowerCase() === key;
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}));
|
|
100
|
+
if (matches.length === 1) {
|
|
101
|
+
return { status: "matched", repository, project: matches[0], projects: matches };
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
status: matches.length > 1 ? "ambiguous" : "unmatched",
|
|
105
|
+
repository,
|
|
106
|
+
projects: matches,
|
|
107
|
+
};
|
|
108
|
+
}
|