forge-workflow 0.0.4 → 0.0.6
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/commands/dev.md +345 -340
- package/.claude/commands/plan.md +566 -521
- package/.claude/commands/premerge.md +186 -176
- package/.claude/commands/research.md +42 -42
- package/.claude/commands/review.md +448 -442
- package/.claude/commands/rollback.md +721 -721
- package/.claude/commands/ship.md +212 -164
- package/.claude/commands/sonarcloud.md +152 -152
- package/.claude/commands/status.md +90 -48
- package/.claude/commands/validate.md +288 -282
- package/.claude/commands/verify.md +269 -221
- package/.claude/rules/greptile-review-process.md +285 -285
- package/.claude/rules/workflow.md +121 -105
- package/.claude/scripts/greptile-resolve.sh +558 -526
- package/.claude/scripts/load-env.sh +32 -32
- package/.cline/workflows/dev.md +342 -337
- package/.cline/workflows/plan.md +563 -518
- package/.cline/workflows/premerge.md +183 -173
- package/.cline/workflows/research.md +39 -39
- package/.cline/workflows/review.md +445 -439
- package/.cline/workflows/rollback.md +718 -718
- package/.cline/workflows/ship.md +209 -161
- package/.cline/workflows/sonarcloud.md +146 -146
- package/.cline/workflows/status.md +87 -45
- package/.cline/workflows/validate.md +285 -279
- package/.cline/workflows/verify.md +266 -218
- package/.codex/config.toml +11 -11
- package/.codex/skills/dev/SKILL.md +345 -340
- package/.codex/skills/plan/SKILL.md +566 -521
- package/.codex/skills/premerge/SKILL.md +186 -176
- package/.codex/skills/research/SKILL.md +42 -42
- package/.codex/skills/review/SKILL.md +448 -442
- package/.codex/skills/rollback/SKILL.md +721 -721
- package/.codex/skills/ship/SKILL.md +212 -164
- package/.codex/skills/sonarcloud/SKILL.md +149 -149
- package/.codex/skills/status/SKILL.md +90 -48
- package/.codex/skills/validate/SKILL.md +288 -282
- package/.codex/skills/verify/SKILL.md +269 -221
- package/.cursor/commands/dev.md +342 -337
- package/.cursor/commands/plan.md +563 -518
- package/.cursor/commands/premerge.md +183 -173
- package/.cursor/commands/research.md +39 -39
- package/.cursor/commands/review.md +445 -439
- package/.cursor/commands/rollback.md +718 -718
- package/.cursor/commands/ship.md +209 -161
- package/.cursor/commands/sonarcloud.md +146 -146
- package/.cursor/commands/status.md +87 -45
- package/.cursor/commands/validate.md +285 -279
- package/.cursor/commands/verify.md +266 -218
- package/.cursor/rules/permissions-guidance.mdc +37 -37
- package/.forge/hooks/check-tdd.js +240 -240
- package/.github/PLUGIN_TEMPLATE.json +32 -32
- package/.github/prompts/dev.prompt.md +347 -342
- package/.github/prompts/plan.prompt.md +568 -523
- package/.github/prompts/premerge.prompt.md +188 -178
- package/.github/prompts/research.prompt.md +44 -44
- package/.github/prompts/review.prompt.md +450 -444
- package/.github/prompts/rollback.prompt.md +723 -723
- package/.github/prompts/ship.prompt.md +214 -166
- package/.github/prompts/sonarcloud.prompt.md +151 -151
- package/.github/prompts/status.prompt.md +92 -50
- package/.github/prompts/validate.prompt.md +290 -284
- package/.github/prompts/verify.prompt.md +271 -223
- package/.github/workflows/beads-to-github.yml +56 -0
- package/.github/workflows/github-to-beads.yml +97 -0
- package/.kilocode/workflows/dev.md +346 -341
- package/.kilocode/workflows/plan.md +567 -522
- package/.kilocode/workflows/premerge.md +187 -177
- package/.kilocode/workflows/research.md +43 -43
- package/.kilocode/workflows/review.md +449 -443
- package/.kilocode/workflows/rollback.md +722 -722
- package/.kilocode/workflows/ship.md +213 -165
- package/.kilocode/workflows/sonarcloud.md +150 -150
- package/.kilocode/workflows/status.md +91 -49
- package/.kilocode/workflows/validate.md +289 -283
- package/.kilocode/workflows/verify.md +270 -222
- package/.mcp.json.example +12 -12
- package/.opencode/commands/dev.md +345 -340
- package/.opencode/commands/plan.md +566 -521
- package/.opencode/commands/premerge.md +186 -176
- package/.opencode/commands/research.md +42 -42
- package/.opencode/commands/review.md +448 -442
- package/.opencode/commands/rollback.md +721 -721
- package/.opencode/commands/ship.md +212 -164
- package/.opencode/commands/sonarcloud.md +149 -149
- package/.opencode/commands/status.md +90 -48
- package/.opencode/commands/validate.md +288 -282
- package/.opencode/commands/verify.md +269 -221
- package/.roo/commands/dev.md +346 -341
- package/.roo/commands/plan.md +567 -522
- package/.roo/commands/premerge.md +187 -177
- package/.roo/commands/research.md +43 -43
- package/.roo/commands/review.md +449 -443
- package/.roo/commands/rollback.md +722 -722
- package/.roo/commands/ship.md +213 -165
- package/.roo/commands/sonarcloud.md +150 -150
- package/.roo/commands/status.md +91 -49
- package/.roo/commands/validate.md +289 -283
- package/.roo/commands/verify.md +270 -222
- package/AGENTS.md +272 -175
- package/CLAUDE.md +110 -100
- package/README.md +429 -416
- package/bin/forge-cmd.js +317 -313
- package/bin/forge-preflight.js +322 -309
- package/bin/forge.js +4765 -4303
- package/docs/AGENT_INSTALL_PROMPT.md +342 -342
- package/docs/BEADS_GITHUB_SYNC.md +251 -251
- package/docs/ENHANCED_ONBOARDING.md +612 -602
- package/docs/EXAMPLES.md +482 -482
- package/docs/GREPTILE_SETUP.md +400 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
- package/docs/ROADMAP.md +359 -359
- package/docs/SETUP.md +663 -631
- package/docs/TOOLCHAIN.md +653 -630
- package/docs/VALIDATION.md +363 -363
- package/install.sh +40 -1056
- package/lefthook.yml +50 -39
- package/lib/agents/README.md +198 -198
- package/lib/agents/claude.plugin.json +28 -28
- package/lib/agents/cline.plugin.json +22 -22
- package/lib/agents/codex.plugin.json +19 -19
- package/lib/agents/copilot.plugin.json +24 -24
- package/lib/agents/cursor.plugin.json +25 -25
- package/lib/agents/kilocode.plugin.json +22 -22
- package/lib/agents/opencode.plugin.json +20 -20
- package/lib/agents/roo.plugin.json +23 -23
- package/lib/agents-config.js +2112 -2112
- package/lib/beads-health-check.js +143 -0
- package/lib/beads-setup.js +341 -0
- package/lib/beads-sync-scaffold.js +260 -0
- package/lib/commands/_registry.js +134 -0
- package/lib/commands/clean.js +181 -0
- package/lib/commands/dev.js +571 -513
- package/lib/commands/plan.js +692 -692
- package/lib/commands/push.js +196 -0
- package/lib/commands/recommend.js +119 -119
- package/lib/commands/ship.js +377 -377
- package/lib/commands/status.js +378 -378
- package/lib/commands/sync.js +55 -0
- package/lib/commands/team.js +37 -0
- package/lib/commands/test.js +207 -0
- package/lib/commands/validate.js +602 -602
- package/lib/commands/worktree.js +310 -0
- package/lib/context-merge.js +359 -359
- package/lib/dep-guard/analyzer.js +294 -294
- package/lib/dep-guard/behavior-detector.js +98 -98
- package/lib/dep-guard/contract-detector.js +162 -162
- package/lib/dep-guard/import-detector.js +498 -498
- package/lib/dep-guard/path-utils.js +13 -13
- package/lib/dep-guard/rubric.js +120 -120
- package/lib/dep-guard/task-parser.js +318 -318
- package/lib/detect-agent.js +191 -191
- package/lib/detect-worktree.js +47 -47
- package/lib/docs-command.js +51 -0
- package/lib/docs-copy.js +50 -0
- package/lib/file-hash.js +26 -26
- package/lib/freshness-token.js +148 -0
- package/lib/greptile-match.js +80 -0
- package/lib/husky-migration.js +450 -0
- package/lib/lefthook-check.js +65 -0
- package/lib/pat-setup.js +207 -0
- package/lib/plugin-catalog.js +350 -350
- package/lib/plugin-manager.js +166 -166
- package/lib/plugin-recommender.js +141 -141
- package/lib/project-discovery.js +491 -491
- package/lib/reset.js +309 -0
- package/lib/setup-action-log.js +139 -139
- package/lib/setup-summary-renderer.js +106 -106
- package/lib/setup-utils.js +96 -0
- package/lib/setup.js +192 -192
- package/lib/smart-merge.js +64 -0
- package/lib/symlink-utils.js +81 -0
- package/lib/task-ownership.js +117 -0
- package/lib/workflow-profiles.js +197 -197
- package/package.json +131 -128
- package/scripts/beads-context.sh +426 -0
- package/scripts/beads-context.test.js +567 -0
- package/scripts/behavioral-judge.sh +378 -0
- package/scripts/benchmark.js +85 -0
- package/scripts/branch-protection.js +183 -0
- package/scripts/check-agents.js +172 -0
- package/scripts/check-forge-token.js +98 -0
- package/scripts/commitlint.js +42 -0
- package/scripts/conflict-detect.sh +323 -0
- package/scripts/dep-guard-analyze.js +71 -0
- package/scripts/dep-guard.sh +789 -0
- package/scripts/eval_win.py +249 -0
- package/scripts/file-index.sh +493 -0
- package/scripts/forge-team/index.sh +86 -0
- package/scripts/forge-team/lib/agent-prompt.sh +52 -0
- package/scripts/forge-team/lib/claim.sh +256 -0
- package/scripts/forge-team/lib/dashboard.sh +341 -0
- package/scripts/forge-team/lib/epic.sh +332 -0
- package/scripts/forge-team/lib/hooks.sh +253 -0
- package/scripts/forge-team/lib/identity.sh +235 -0
- package/scripts/forge-team/lib/sync-github.sh +317 -0
- package/scripts/forge-team/lib/verify.sh +284 -0
- package/scripts/forge-team/lib/workload.sh +296 -0
- package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
- package/scripts/forge-team/tests/claim.test.sh +179 -0
- package/scripts/forge-team/tests/dashboard.test.sh +170 -0
- package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
- package/scripts/forge-team/tests/epic.test.sh +176 -0
- package/scripts/forge-team/tests/hooks.test.sh +239 -0
- package/scripts/forge-team/tests/identity.test.sh +176 -0
- package/scripts/forge-team/tests/integration.test.sh +371 -0
- package/scripts/forge-team/tests/sync-github.test.sh +209 -0
- package/scripts/forge-team/tests/verify.test.sh +314 -0
- package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
- package/scripts/forge-team/tests/workload.test.sh +209 -0
- package/scripts/github-beads-sync/comment.mjs +64 -0
- package/scripts/github-beads-sync/config.mjs +148 -0
- package/scripts/github-beads-sync/github-api.mjs +131 -0
- package/scripts/github-beads-sync/index.mjs +332 -0
- package/scripts/github-beads-sync/label-mapper.mjs +54 -0
- package/scripts/github-beads-sync/mapping.mjs +78 -0
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
- package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
- package/scripts/github-beads-sync/run-bd.mjs +159 -0
- package/scripts/github-beads-sync/sanitize.mjs +121 -0
- package/scripts/github-beads-sync.config.json +26 -0
- package/scripts/improve-command.js +375 -0
- package/scripts/lib/eval-runner.js +268 -0
- package/scripts/lib/eval-schema.js +135 -0
- package/scripts/lib/eval-storage.js +78 -0
- package/scripts/lib/grading.js +203 -0
- package/scripts/lib/jsonl-lock.sh +48 -0
- package/scripts/lib/sanitize.sh +116 -0
- package/scripts/lib/transcript-parser.js +63 -0
- package/scripts/lint.js +47 -0
- package/scripts/migrate-to-bun-test.js +412 -0
- package/scripts/pr-coordinator.sh +706 -0
- package/scripts/run-command-eval.js +236 -0
- package/scripts/smart-status.sh +809 -0
- package/scripts/sync-commands.js +571 -0
- package/scripts/sync-utils.sh +455 -0
- package/scripts/test-dashboard.js +123 -0
- package/scripts/test.js +46 -0
- package/scripts/validate.sh +94 -0
- package/skills/parallel-deep-research/SKILL.md +108 -108
- package/skills/parallel-deep-research/evals/README.md +27 -27
- package/skills/parallel-deep-research/evals/evals.json +62 -62
- package/skills/sonarcloud-analysis/SKILL.md +171 -171
- package/skills/sonarcloud-analysis/evals/README.md +27 -27
- package/skills/sonarcloud-analysis/evals/evals.json +50 -50
- package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
- package/.cursor/hooks/state/continual-learning-index.json +0 -19
- package/.cursor/hooks/state/continual-learning.json +0 -8
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config loader for GitHub-Beads issue sync.
|
|
3
|
+
* @module scripts/github-beads-sync/config
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default configuration for GitHub-Beads sync.
|
|
10
|
+
* @type {Readonly<SyncConfig>}
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_CONFIG = Object.freeze({
|
|
13
|
+
labelToType: Object.freeze({
|
|
14
|
+
bug: 'bug',
|
|
15
|
+
enhancement: 'feature',
|
|
16
|
+
documentation: 'task',
|
|
17
|
+
question: 'task',
|
|
18
|
+
}),
|
|
19
|
+
labelToPriority: Object.freeze({
|
|
20
|
+
P0: 0, critical: 0,
|
|
21
|
+
P1: 1, high: 1,
|
|
22
|
+
P2: 2, medium: 2,
|
|
23
|
+
P3: 3, low: 3,
|
|
24
|
+
P4: 4, backlog: 4,
|
|
25
|
+
}),
|
|
26
|
+
defaultType: 'task',
|
|
27
|
+
defaultPriority: 2,
|
|
28
|
+
mapAssignee: true,
|
|
29
|
+
publicRepoGate: 'none',
|
|
30
|
+
gateLabelName: 'beads-track',
|
|
31
|
+
gateAssociations: Object.freeze(['MEMBER', 'COLLABORATOR', 'OWNER']),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Deep-merge source into target (one level of nesting).
|
|
36
|
+
* Arrays and scalars from source overwrite target.
|
|
37
|
+
* Plain objects are merged key-by-key.
|
|
38
|
+
* @param {object} target
|
|
39
|
+
* @param {object} source
|
|
40
|
+
* @returns {object} Merged result (new object)
|
|
41
|
+
*/
|
|
42
|
+
function deepMerge(target, source) {
|
|
43
|
+
const result = { ...target };
|
|
44
|
+
for (const key of Object.keys(source)) {
|
|
45
|
+
const srcVal = source[key];
|
|
46
|
+
const tgtVal = target[key];
|
|
47
|
+
if (
|
|
48
|
+
srcVal !== null &&
|
|
49
|
+
typeof srcVal === 'object' &&
|
|
50
|
+
!Array.isArray(srcVal) &&
|
|
51
|
+
tgtVal !== null &&
|
|
52
|
+
typeof tgtVal === 'object' &&
|
|
53
|
+
!Array.isArray(tgtVal)
|
|
54
|
+
) {
|
|
55
|
+
result[key] = { ...tgtVal, ...srcVal };
|
|
56
|
+
} else {
|
|
57
|
+
result[key] = srcVal;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Clone DEFAULT_CONFIG into a mutable plain object.
|
|
65
|
+
* @returns {object}
|
|
66
|
+
*/
|
|
67
|
+
function cloneDefaults() {
|
|
68
|
+
return {
|
|
69
|
+
...DEFAULT_CONFIG,
|
|
70
|
+
labelToType: { ...DEFAULT_CONFIG.labelToType },
|
|
71
|
+
labelToPriority: { ...DEFAULT_CONFIG.labelToPriority },
|
|
72
|
+
gateAssociations: [...DEFAULT_CONFIG.gateAssociations],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Load and validate sync configuration.
|
|
78
|
+
* - No arguments: returns a copy of DEFAULT_CONFIG.
|
|
79
|
+
* - With path: reads JSON file and deep-merges with defaults (user overrides win).
|
|
80
|
+
* - Missing file: returns defaults (no throw).
|
|
81
|
+
* - Invalid JSON: throws with helpful message.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} [configPath] - Path to a JSON config file.
|
|
84
|
+
* @returns {object} Merged configuration object.
|
|
85
|
+
*/
|
|
86
|
+
export function loadConfig(configPath) {
|
|
87
|
+
if (!configPath) {
|
|
88
|
+
return cloneDefaults();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let raw;
|
|
92
|
+
try {
|
|
93
|
+
raw = readFileSync(configPath, 'utf-8');
|
|
94
|
+
} catch {
|
|
95
|
+
// File not found or unreadable — return defaults
|
|
96
|
+
return cloneDefaults();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let userConfig;
|
|
100
|
+
try {
|
|
101
|
+
userConfig = JSON.parse(raw);
|
|
102
|
+
} catch (parseErr) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Invalid JSON in config file: ${configPath}\n` +
|
|
105
|
+
`Parse error: ${parseErr.message}`,
|
|
106
|
+
{ cause: parseErr }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const merged = deepMerge(cloneDefaults(), userConfig);
|
|
111
|
+
return validateConfig(merged);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Validate and normalize config values.
|
|
116
|
+
* Replaces invalid types with defaults rather than throwing.
|
|
117
|
+
* @param {object} cfg
|
|
118
|
+
* @returns {object} Validated config
|
|
119
|
+
*/
|
|
120
|
+
function validateConfig(cfg) {
|
|
121
|
+
const defaults = cloneDefaults();
|
|
122
|
+
if (!cfg.labelToType || typeof cfg.labelToType !== 'object' || Array.isArray(cfg.labelToType)) {
|
|
123
|
+
cfg.labelToType = defaults.labelToType;
|
|
124
|
+
}
|
|
125
|
+
if (!cfg.labelToPriority || typeof cfg.labelToPriority !== 'object' || Array.isArray(cfg.labelToPriority)) {
|
|
126
|
+
cfg.labelToPriority = defaults.labelToPriority;
|
|
127
|
+
}
|
|
128
|
+
if (typeof cfg.defaultPriority !== 'number') {
|
|
129
|
+
cfg.defaultPriority = defaults.defaultPriority;
|
|
130
|
+
}
|
|
131
|
+
if (typeof cfg.mapAssignee !== 'boolean') {
|
|
132
|
+
cfg.mapAssignee = defaults.mapAssignee;
|
|
133
|
+
}
|
|
134
|
+
if (!Array.isArray(cfg.gateAssociations)) {
|
|
135
|
+
cfg.gateAssociations = defaults.gateAssociations;
|
|
136
|
+
}
|
|
137
|
+
const VALID_GATE_VALUES = ['none', 'author_association', 'label'];
|
|
138
|
+
if (!VALID_GATE_VALUES.includes(cfg.publicRepoGate)) {
|
|
139
|
+
cfg.publicRepoGate = defaults.publicRepoGate;
|
|
140
|
+
}
|
|
141
|
+
if (typeof cfg.defaultType !== 'string' || !cfg.defaultType) {
|
|
142
|
+
cfg.defaultType = defaults.defaultType;
|
|
143
|
+
}
|
|
144
|
+
if (typeof cfg.gateLabelName !== 'string' || !cfg.gateLabelName) {
|
|
145
|
+
cfg.gateLabelName = defaults.gateLabelName;
|
|
146
|
+
}
|
|
147
|
+
return cfg;
|
|
148
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub API caller module using `gh api` CLI.
|
|
3
|
+
* Builds argument arrays for execFileSync('gh', args) — no shell invocation.
|
|
4
|
+
* @module github-api
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
|
|
9
|
+
const SYNC_MARKER = '<!-- beads-sync:';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds args for listing all comments on a GitHub issue.
|
|
13
|
+
* @param {string} owner - Repository owner
|
|
14
|
+
* @param {string} repo - Repository name
|
|
15
|
+
* @param {number|string} issueNumber - Issue number
|
|
16
|
+
* @returns {string[]} Args array for execFileSync('gh', args)
|
|
17
|
+
*/
|
|
18
|
+
export function buildFindCommentsArgs(owner, repo, issueNumber) {
|
|
19
|
+
return [
|
|
20
|
+
'api',
|
|
21
|
+
`repos/${owner}/${repo}/issues/${issueNumber}/comments`,
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Finds the beads-sync comment in a list of comment objects.
|
|
27
|
+
* @param {Array<{id: number, body: string}>} comments - Array of GitHub comment objects
|
|
28
|
+
* @returns {{id: number, body: string}|null} The sync comment or null
|
|
29
|
+
*/
|
|
30
|
+
export function parseFindSyncComment(comments) {
|
|
31
|
+
const match = comments.find((c) => c.body && c.body.includes(SYNC_MARKER));
|
|
32
|
+
if (!match) return null;
|
|
33
|
+
return { id: match.id, body: match.body };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Builds args for creating a comment on a GitHub issue.
|
|
38
|
+
* @param {string} owner - Repository owner
|
|
39
|
+
* @param {string} repo - Repository name
|
|
40
|
+
* @param {number|string} issueNumber - Issue number
|
|
41
|
+
* @param {string} body - Comment body
|
|
42
|
+
* @returns {string[]} Args array for execFileSync('gh', args)
|
|
43
|
+
*/
|
|
44
|
+
export function buildCreateCommentArgs(owner, repo, issueNumber, body) {
|
|
45
|
+
return [
|
|
46
|
+
'api',
|
|
47
|
+
`repos/${owner}/${repo}/issues/${issueNumber}/comments`,
|
|
48
|
+
'-f',
|
|
49
|
+
`body=${body}`,
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Builds args for editing an existing comment.
|
|
55
|
+
* @param {string} owner - Repository owner
|
|
56
|
+
* @param {string} repo - Repository name
|
|
57
|
+
* @param {number|string} commentId - Comment ID
|
|
58
|
+
* @param {string} body - Updated comment body
|
|
59
|
+
* @returns {string[]} Args array for execFileSync('gh', args)
|
|
60
|
+
*/
|
|
61
|
+
export function buildEditCommentArgs(owner, repo, commentId, body) {
|
|
62
|
+
return [
|
|
63
|
+
'api',
|
|
64
|
+
`repos/${owner}/${repo}/issues/comments/${commentId}`,
|
|
65
|
+
'-X',
|
|
66
|
+
'PATCH',
|
|
67
|
+
'-f',
|
|
68
|
+
`body=${body}`,
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Builds args for closing a GitHub issue.
|
|
74
|
+
* @param {string} owner - Repository owner
|
|
75
|
+
* @param {string} repo - Repository name
|
|
76
|
+
* @param {number|string} issueNumber - Issue number
|
|
77
|
+
* @returns {string[]} Args array for execFileSync('gh', args)
|
|
78
|
+
*/
|
|
79
|
+
export function buildCloseIssueArgs(owner, repo, issueNumber) {
|
|
80
|
+
return [
|
|
81
|
+
'api',
|
|
82
|
+
`repos/${owner}/${repo}/issues/${issueNumber}`,
|
|
83
|
+
'-X',
|
|
84
|
+
'PATCH',
|
|
85
|
+
'-f',
|
|
86
|
+
'state=closed',
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Finds the existing beads-sync comment on an issue, or returns null.
|
|
92
|
+
* @param {string} owner - Repository owner
|
|
93
|
+
* @param {string} repo - Repository name
|
|
94
|
+
* @param {number|string} issueNumber - Issue number
|
|
95
|
+
* @returns {{id: number, body: string}|null}
|
|
96
|
+
*/
|
|
97
|
+
export function findSyncComment(owner, repo, issueNumber) {
|
|
98
|
+
const args = buildFindCommentsArgs(owner, repo, issueNumber);
|
|
99
|
+
const raw = execFileSync('gh', args, { encoding: 'utf-8' });
|
|
100
|
+
const comments = JSON.parse(raw);
|
|
101
|
+
return parseFindSyncComment(comments);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Creates or edits the beads-sync comment on an issue.
|
|
106
|
+
* If a sync comment already exists, edits it; otherwise creates a new one.
|
|
107
|
+
* @param {string} owner - Repository owner
|
|
108
|
+
* @param {string} repo - Repository name
|
|
109
|
+
* @param {number|string} issueNumber - Issue number
|
|
110
|
+
* @param {string} body - Comment body (should contain the beads-sync marker)
|
|
111
|
+
* @returns {void}
|
|
112
|
+
*/
|
|
113
|
+
export function createOrEditComment(owner, repo, issueNumber, body) {
|
|
114
|
+
const existing = findSyncComment(owner, repo, issueNumber);
|
|
115
|
+
const args = existing
|
|
116
|
+
? buildEditCommentArgs(owner, repo, existing.id, body)
|
|
117
|
+
: buildCreateCommentArgs(owner, repo, issueNumber, body);
|
|
118
|
+
execFileSync('gh', args, { encoding: 'utf-8' });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Closes a GitHub issue.
|
|
123
|
+
* @param {string} owner - Repository owner
|
|
124
|
+
* @param {string} repo - Repository name
|
|
125
|
+
* @param {number|string} issueNumber - Issue number
|
|
126
|
+
* @returns {void}
|
|
127
|
+
*/
|
|
128
|
+
export function closeIssue(owner, repo, issueNumber) {
|
|
129
|
+
const args = buildCloseIssueArgs(owner, repo, issueNumber);
|
|
130
|
+
execFileSync('gh', args, { encoding: 'utf-8' });
|
|
131
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main entry point for GitHub-Beads issue sync.
|
|
3
|
+
* Orchestrates all Wave 1 modules to handle GitHub webhook events.
|
|
4
|
+
*
|
|
5
|
+
* @module scripts/github-beads-sync/index
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, appendFileSync } from 'node:fs';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { loadConfig } from './config.mjs';
|
|
11
|
+
import { sanitizeTitle } from './sanitize.mjs';
|
|
12
|
+
import { mapLabels } from './label-mapper.mjs';
|
|
13
|
+
import { bdCreate as realBdCreate, bdClose as realBdClose, bdShow as realBdShow } from './run-bd.mjs';
|
|
14
|
+
import { getBeadsId as realGetBeadsId, setBeadsId as realSetBeadsId } from './mapping.mjs';
|
|
15
|
+
import { buildComment, parseComment } from './comment.mjs';
|
|
16
|
+
import {
|
|
17
|
+
findSyncComment as realFindSyncComment,
|
|
18
|
+
createOrEditComment as realCreateOrEditComment,
|
|
19
|
+
} from './github-api.mjs';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if a sender login matches known bot patterns.
|
|
23
|
+
* @param {string} login - GitHub sender login
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
function isBot(login) {
|
|
27
|
+
if (!login) return false;
|
|
28
|
+
return login.includes('[bot]') || login === 'github-actions';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if an issue has the skip-beads-sync label.
|
|
33
|
+
* @param {Array<{name: string}|string>} labels
|
|
34
|
+
* @returns {boolean}
|
|
35
|
+
*/
|
|
36
|
+
function hasSkipLabel(labels) {
|
|
37
|
+
if (!labels) return false;
|
|
38
|
+
return labels.some((l) => {
|
|
39
|
+
const name = typeof l === 'string' ? l : l.name;
|
|
40
|
+
return name === 'skip-beads-sync';
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Handle a GitHub issue "opened" event.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} event - Parsed GitHub webhook payload
|
|
48
|
+
* @param {object} options
|
|
49
|
+
* @param {string} [options.configPath] - Path to config JSON
|
|
50
|
+
* @param {string} options.mappingPath - Path to mapping JSON
|
|
51
|
+
* @param {string} options.owner - Repository owner
|
|
52
|
+
* @param {string} options.repo - Repository name
|
|
53
|
+
* @param {object} [options.bd] - Dependency injection for bd functions
|
|
54
|
+
* @param {object} [options.github] - Dependency injection for github-api functions
|
|
55
|
+
* @param {object} [options.mapping] - Dependency injection for mapping functions
|
|
56
|
+
* @param {object} [options.configOverride] - Merge into loaded config (for testing)
|
|
57
|
+
* @returns {object} Result object
|
|
58
|
+
*/
|
|
59
|
+
export function handleOpened(event, options = {}) {
|
|
60
|
+
const {
|
|
61
|
+
configPath,
|
|
62
|
+
mappingPath,
|
|
63
|
+
owner,
|
|
64
|
+
repo,
|
|
65
|
+
bd = {},
|
|
66
|
+
github = {},
|
|
67
|
+
mapping = {},
|
|
68
|
+
configOverride,
|
|
69
|
+
} = options;
|
|
70
|
+
|
|
71
|
+
const bdCreate = bd.bdCreate ?? realBdCreate;
|
|
72
|
+
const findSyncComment = github.findSyncComment ?? realFindSyncComment;
|
|
73
|
+
const createOrEditComment = github.createOrEditComment ?? realCreateOrEditComment;
|
|
74
|
+
const getBeadsId = mapping.getBeadsId ?? realGetBeadsId;
|
|
75
|
+
const setBeadsId = mapping.setBeadsId ?? realSetBeadsId;
|
|
76
|
+
|
|
77
|
+
// 1. Load config
|
|
78
|
+
let config = loadConfig(configPath);
|
|
79
|
+
if (configOverride) {
|
|
80
|
+
config = { ...config, ...configOverride };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 2. Extract event data
|
|
84
|
+
const issue = event.issue;
|
|
85
|
+
const issueNumber = issue.number;
|
|
86
|
+
const rawTitle = issue.title;
|
|
87
|
+
const labels = issue.labels || [];
|
|
88
|
+
const assignee = config.mapAssignee ? issue.assignee?.login : undefined;
|
|
89
|
+
const htmlUrl = issue.html_url;
|
|
90
|
+
const body = issue.body ?? '';
|
|
91
|
+
const authorAssociation = issue.author_association;
|
|
92
|
+
|
|
93
|
+
// 3. Guard: bot actor
|
|
94
|
+
if (isBot(event.sender?.login)) {
|
|
95
|
+
return { skipped: true, reason: 'bot actor' };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 4. Guard: skip label
|
|
99
|
+
if (hasSkipLabel(labels)) {
|
|
100
|
+
return { skipped: true, reason: 'skip label' };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 5. Guard: no-beads body
|
|
104
|
+
if (body.includes('no-beads')) {
|
|
105
|
+
return { skipped: true, reason: 'no-beads in body' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 6. Guard: public repo gate
|
|
109
|
+
if (config.publicRepoGate === 'author_association') {
|
|
110
|
+
const allowed = config.gateAssociations || [];
|
|
111
|
+
if (!allowed.includes(authorAssociation)) {
|
|
112
|
+
return { skipped: true, reason: 'author not authorized' };
|
|
113
|
+
}
|
|
114
|
+
} else if (config.publicRepoGate === 'label') {
|
|
115
|
+
const gateLabelName = config.gateLabelName || 'beads-track';
|
|
116
|
+
const hasGateLabel = labels.some((l) => {
|
|
117
|
+
const name = typeof l === 'string' ? l : l.name;
|
|
118
|
+
return name === gateLabelName;
|
|
119
|
+
});
|
|
120
|
+
if (!hasGateLabel) {
|
|
121
|
+
return { skipped: true, reason: `missing required label: ${gateLabelName}` };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 7. Idempotency — check mapping file first (fast, survives comment deletion)
|
|
126
|
+
const existingBeadsId = getBeadsId(mappingPath, issueNumber);
|
|
127
|
+
if (existingBeadsId) {
|
|
128
|
+
// Mapping exists — ensure the GitHub comment is present (repair if deleted)
|
|
129
|
+
const existingComment = findSyncComment(owner, repo, issueNumber);
|
|
130
|
+
if (!existingComment) {
|
|
131
|
+
const externalRef = `gh-${issueNumber}`;
|
|
132
|
+
const commentBody = buildComment(existingBeadsId, issueNumber, { externalRef });
|
|
133
|
+
createOrEditComment(owner, repo, issueNumber, commentBody);
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
skipped: true,
|
|
137
|
+
reason: 'already synced (mapping file)',
|
|
138
|
+
beadsId: existingBeadsId,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 7b. Fallback idempotency — check for existing sync comment
|
|
143
|
+
const existingComment = findSyncComment(owner, repo, issueNumber);
|
|
144
|
+
if (existingComment) {
|
|
145
|
+
const parsed = parseComment(existingComment.body);
|
|
146
|
+
// Repair mapping file from comment
|
|
147
|
+
if (parsed?.beadsId) {
|
|
148
|
+
setBeadsId(mappingPath, issueNumber, parsed.beadsId);
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
skipped: true,
|
|
152
|
+
reason: 'already synced (comment)',
|
|
153
|
+
beadsId: parsed?.beadsId ?? null,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 8. Sanitize title (labels mapped from raw names — sanitization only needed for CLI args)
|
|
158
|
+
const { sanitized: sanitizedTitle, warnings: titleWarnings } = sanitizeTitle(rawTitle);
|
|
159
|
+
if (titleWarnings.length) console.warn('sanitize:', titleWarnings);
|
|
160
|
+
|
|
161
|
+
// 9. Map labels using raw names (case-insensitive matching in mapLabels handles normalization)
|
|
162
|
+
const rawLabelNames = labels.map((l) => (typeof l === 'string' ? l : l.name));
|
|
163
|
+
const { type, priority } = mapLabels(rawLabelNames, config);
|
|
164
|
+
|
|
165
|
+
// 10. Create beads issue
|
|
166
|
+
const externalRef = `gh-${issueNumber}`;
|
|
167
|
+
const beadsId = bdCreate({
|
|
168
|
+
title: sanitizedTitle,
|
|
169
|
+
type,
|
|
170
|
+
priority,
|
|
171
|
+
assignee,
|
|
172
|
+
description: htmlUrl,
|
|
173
|
+
externalRef,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (!beadsId) {
|
|
177
|
+
return { success: false, reason: 'bd create failed — no beads ID returned', issueNumber };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 11. Update mapping
|
|
181
|
+
setBeadsId(mappingPath, issueNumber, beadsId);
|
|
182
|
+
|
|
183
|
+
// 12. Build and post comment
|
|
184
|
+
const commentBody = buildComment(beadsId, issueNumber, { type, priority, externalRef });
|
|
185
|
+
createOrEditComment(owner, repo, issueNumber, commentBody);
|
|
186
|
+
|
|
187
|
+
// 13. Return success
|
|
188
|
+
return { success: true, beadsId, issueNumber };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Handle a GitHub issue "closed" event.
|
|
193
|
+
*
|
|
194
|
+
* @param {object} event - Parsed GitHub webhook payload
|
|
195
|
+
* @param {object} options
|
|
196
|
+
* @param {string} options.mappingPath - Path to mapping JSON
|
|
197
|
+
* @param {string} options.owner - Repository owner
|
|
198
|
+
* @param {string} options.repo - Repository name
|
|
199
|
+
* @param {object} [options.bd] - Dependency injection for bd functions
|
|
200
|
+
* @param {object} [options.github] - Dependency injection for github-api functions
|
|
201
|
+
* @param {object} [options.mapping] - Dependency injection for mapping functions
|
|
202
|
+
* @returns {object} Result object
|
|
203
|
+
*/
|
|
204
|
+
export function handleClosed(event, options = {}) {
|
|
205
|
+
const {
|
|
206
|
+
mappingPath,
|
|
207
|
+
owner,
|
|
208
|
+
repo,
|
|
209
|
+
bd = {},
|
|
210
|
+
github = {},
|
|
211
|
+
mapping = {},
|
|
212
|
+
} = options;
|
|
213
|
+
|
|
214
|
+
const bdClose = bd.bdClose ?? realBdClose;
|
|
215
|
+
const bdShow = bd.bdShow ?? realBdShow;
|
|
216
|
+
const getBeadsId = mapping.getBeadsId ?? realGetBeadsId;
|
|
217
|
+
const findSyncComment = github.findSyncComment ?? realFindSyncComment;
|
|
218
|
+
|
|
219
|
+
// 1. Extract issue number, labels, and state reason
|
|
220
|
+
const issueNumber = event.issue.number;
|
|
221
|
+
const labels = event.issue.labels || [];
|
|
222
|
+
const stateReason = event.issue.state_reason;
|
|
223
|
+
|
|
224
|
+
// 2. Guard: bot actor
|
|
225
|
+
if (isBot(event.sender?.login)) {
|
|
226
|
+
return { skipped: true, reason: 'bot actor' };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 3. Guard: skip label
|
|
230
|
+
if (hasSkipLabel(labels)) {
|
|
231
|
+
return { skipped: true, reason: 'skip label' };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 4. Guard: skip "not planned" closures (only close on "completed")
|
|
235
|
+
if (stateReason && stateReason !== 'completed') {
|
|
236
|
+
return { skipped: true, reason: `closed as ${stateReason}` };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 5. Read mapping
|
|
240
|
+
let beadsId = getBeadsId(mappingPath, issueNumber);
|
|
241
|
+
|
|
242
|
+
// 6. Fallback: find via sync comment
|
|
243
|
+
if (!beadsId) {
|
|
244
|
+
const comment = findSyncComment(owner, repo, issueNumber);
|
|
245
|
+
if (comment) {
|
|
246
|
+
const parsed = parseComment(comment.body);
|
|
247
|
+
// Verify the comment's tagged issue number matches current issue (prevents cross-issue close)
|
|
248
|
+
if (parsed && parsed.issueNumber === issueNumber) {
|
|
249
|
+
beadsId = parsed.beadsId;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 7. No beads link found
|
|
255
|
+
if (!beadsId) {
|
|
256
|
+
return { skipped: true, reason: 'no beads link found' };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 8. Check if already closed
|
|
260
|
+
const status = bdShow(beadsId);
|
|
261
|
+
if (status === null) {
|
|
262
|
+
return { skipped: true, reason: 'could not determine beads status' };
|
|
263
|
+
}
|
|
264
|
+
if (status === 'closed') {
|
|
265
|
+
return { skipped: true, reason: 'already closed' };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 9. Close beads issue
|
|
269
|
+
bdClose(beadsId, `Closed via GitHub issue #${issueNumber}`);
|
|
270
|
+
|
|
271
|
+
// 10. Return success
|
|
272
|
+
return { success: true, beadsId, issueNumber };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// CLI entry point
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
279
|
+
if (process.argv[1] === __filename) {
|
|
280
|
+
const action = process.argv[2];
|
|
281
|
+
const eventPath = process.env.GITHUB_EVENT_PATH || process.argv[3];
|
|
282
|
+
|
|
283
|
+
if (!action || !eventPath) {
|
|
284
|
+
console.error('Usage: node index.mjs <opened|closed> [event-path]');
|
|
285
|
+
console.error(' GITHUB_EVENT_PATH env var is used if event-path arg is omitted.');
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const event = JSON.parse(readFileSync(eventPath, 'utf-8'));
|
|
290
|
+
const [owner, repo] = (process.env.GITHUB_REPOSITORY || 'unknown/unknown').split('/');
|
|
291
|
+
|
|
292
|
+
const options = {
|
|
293
|
+
configPath: process.env.BEADS_SYNC_CONFIG || undefined,
|
|
294
|
+
mappingPath: process.env.BEADS_SYNC_MAPPING || '.github/beads-mapping.json',
|
|
295
|
+
owner,
|
|
296
|
+
repo,
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Validate action explicitly — only allow known actions
|
|
300
|
+
const VALID_ACTIONS = ['opened', 'closed'];
|
|
301
|
+
if (!VALID_ACTIONS.includes(action)) {
|
|
302
|
+
console.error(`Unknown action: "${action}". Expected one of: ${VALID_ACTIONS.join(', ')}`);
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const handler = action === 'opened' ? handleOpened : handleClosed;
|
|
307
|
+
|
|
308
|
+
Promise.resolve(handler(event, options))
|
|
309
|
+
.then((result) => {
|
|
310
|
+
console.log(JSON.stringify(result, null, 2));
|
|
311
|
+
|
|
312
|
+
// Exit non-zero on logical failure (e.g., bdCreate returned null)
|
|
313
|
+
if (result.success === false) {
|
|
314
|
+
console.error(`Sync failed: ${result.reason || 'unknown'}`);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Write to GITHUB_OUTPUT if available
|
|
319
|
+
const outputPath = process.env.GITHUB_OUTPUT;
|
|
320
|
+
if (outputPath) {
|
|
321
|
+
for (const [key, value] of Object.entries(result)) {
|
|
322
|
+
if (value != null) {
|
|
323
|
+
appendFileSync(outputPath, `${key}=${value}\n`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
})
|
|
328
|
+
.catch((err) => {
|
|
329
|
+
console.error('Fatal:', err.message);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps GitHub issue labels to Beads type and priority.
|
|
3
|
+
*
|
|
4
|
+
* @param {Array<string|{name: string}>} labels - GitHub labels (strings or objects)
|
|
5
|
+
* @param {object} config - Mapping configuration
|
|
6
|
+
* @param {Record<string, string>} config.labelToType - Label name to Beads type
|
|
7
|
+
* @param {Record<string, number>} config.labelToPriority - Label name to priority number
|
|
8
|
+
* @param {string} [config.defaultType="task"] - Fallback type
|
|
9
|
+
* @param {number} [config.defaultPriority=2] - Fallback priority
|
|
10
|
+
* @returns {{ type: string, priority: number }}
|
|
11
|
+
*/
|
|
12
|
+
export function mapLabels(labels, config) {
|
|
13
|
+
const {
|
|
14
|
+
defaultType = 'task',
|
|
15
|
+
defaultPriority = 2,
|
|
16
|
+
} = config;
|
|
17
|
+
const labelToType = (config.labelToType && typeof config.labelToType === 'object' && !Array.isArray(config.labelToType)) ? config.labelToType : {};
|
|
18
|
+
const labelToPriority = (config.labelToPriority && typeof config.labelToPriority === 'object' && !Array.isArray(config.labelToPriority)) ? config.labelToPriority : {};
|
|
19
|
+
|
|
20
|
+
// Normalize labels to lowercase strings
|
|
21
|
+
const names = labels.map((l) =>
|
|
22
|
+
(typeof l === 'string' ? l : l.name).toLowerCase()
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Build lowercase lookup maps
|
|
26
|
+
const typeLookup = Object.create(null);
|
|
27
|
+
for (const [key, value] of Object.entries(labelToType)) {
|
|
28
|
+
typeLookup[key.toLowerCase()] = value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const priorityLookup = Object.create(null);
|
|
32
|
+
for (const [key, value] of Object.entries(labelToPriority)) {
|
|
33
|
+
priorityLookup[key.toLowerCase()] = value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// First match wins
|
|
37
|
+
let type = defaultType;
|
|
38
|
+
for (const name of names) {
|
|
39
|
+
if (name in typeLookup) {
|
|
40
|
+
type = typeLookup[name];
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let priority = defaultPriority;
|
|
46
|
+
for (const name of names) {
|
|
47
|
+
if (name in priorityLookup) {
|
|
48
|
+
priority = priorityLookup[name];
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { type, priority };
|
|
54
|
+
}
|