@openforge-app/plugin-sdk 0.1.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/dist/backend.d.ts +3 -0
- package/dist/backend.js +3 -0
- package/dist/context.d.ts +14 -0
- package/dist/context.js +30 -0
- package/dist/domain.d.ts +579 -0
- package/dist/domain.js +323 -0
- package/dist/frontend.d.ts +7 -0
- package/dist/frontend.js +9 -0
- package/dist/helpers.d.ts +4 -0
- package/dist/helpers.js +9 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +6 -0
- package/dist/manifest.d.ts +58 -0
- package/dist/manifest.js +93 -0
- package/dist/markdown.d.ts +5 -0
- package/dist/markdown.js +54 -0
- package/dist/numberParsing.d.ts +1 -0
- package/dist/numberParsing.js +8 -0
- package/dist/openforgePackageMetadataSchema.json +70 -0
- package/dist/prStatusPresentation.d.ts +30 -0
- package/dist/prStatusPresentation.js +151 -0
- package/dist/projectFileTree.d.ts +68 -0
- package/dist/projectFileTree.js +141 -0
- package/dist/sanitize.d.ts +6 -0
- package/dist/sanitize.js +13 -0
- package/dist/svelteHostRuntimeContract.d.mts +18 -0
- package/dist/svelteHostRuntimeContract.mjs +81 -0
- package/dist/testing.d.ts +223 -0
- package/dist/testing.js +625 -0
- package/dist/types.d.ts +350 -0
- package/dist/types.js +22 -0
- package/dist/ui/MarkdownContent.svelte +30 -0
- package/dist/ui/ResizablePanel.svelte +146 -0
- package/dist/vite.d.ts +15 -0
- package/dist/vite.js +78 -0
- package/package.json +81 -0
package/dist/domain.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
export function isClosedOrMergedPullRequest(state) {
|
|
2
|
+
return state === 'closed' || state === 'merged';
|
|
3
|
+
}
|
|
4
|
+
export function isMergedPullRequest(pr) {
|
|
5
|
+
return pr.state === 'merged' || pr.merged_at != null;
|
|
6
|
+
}
|
|
7
|
+
export function isClosedUnmergedPullRequest(pr) {
|
|
8
|
+
return pr.state === 'closed' && pr.merged_at == null;
|
|
9
|
+
}
|
|
10
|
+
export function hasMergeConflicts(pr) {
|
|
11
|
+
if (pr.state !== 'open')
|
|
12
|
+
return false;
|
|
13
|
+
const mergeableState = pr.mergeable_state?.toLowerCase() ?? null;
|
|
14
|
+
return mergeableState === 'dirty' || mergeableState === 'conflicting';
|
|
15
|
+
}
|
|
16
|
+
function mergeReadinessDetail(code, message) {
|
|
17
|
+
return { code, message };
|
|
18
|
+
}
|
|
19
|
+
function mergeReadinessResult(pr, status, action, blockers, warnings) {
|
|
20
|
+
return {
|
|
21
|
+
status,
|
|
22
|
+
action,
|
|
23
|
+
blockers,
|
|
24
|
+
warnings,
|
|
25
|
+
freshness: {
|
|
26
|
+
sourceSha: pr.head_sha ?? null,
|
|
27
|
+
checkedAt: pr.updated_at ?? null,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const MERGE_READINESS_STATUSES = [
|
|
32
|
+
'ready_to_merge',
|
|
33
|
+
'ready_to_enqueue',
|
|
34
|
+
'queued_pull_request',
|
|
35
|
+
'readiness_unknown',
|
|
36
|
+
'blocked',
|
|
37
|
+
];
|
|
38
|
+
const MERGE_READINESS_ACTIONS = [
|
|
39
|
+
'merge',
|
|
40
|
+
'enqueue',
|
|
41
|
+
'wait_for_queue',
|
|
42
|
+
'wait_for_github',
|
|
43
|
+
'resolve_blockers',
|
|
44
|
+
];
|
|
45
|
+
function isMergeReadinessStatus(value) {
|
|
46
|
+
return MERGE_READINESS_STATUSES.includes(value);
|
|
47
|
+
}
|
|
48
|
+
function isMergeReadinessAction(value) {
|
|
49
|
+
return MERGE_READINESS_ACTIONS.includes(value);
|
|
50
|
+
}
|
|
51
|
+
function parseMergeReadinessDetails(value) {
|
|
52
|
+
if (Array.isArray(value))
|
|
53
|
+
return value;
|
|
54
|
+
if (!value)
|
|
55
|
+
return [];
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(value);
|
|
58
|
+
return Array.isArray(parsed)
|
|
59
|
+
? parsed.filter((detail) => typeof detail?.code === 'string' && typeof detail?.message === 'string')
|
|
60
|
+
: [];
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function isUnresolvedConversationDetail(detail) {
|
|
67
|
+
return detail.code === 'unresolved_conversations';
|
|
68
|
+
}
|
|
69
|
+
function hasNoPublishedChecksForUnstableMergeability(pr) {
|
|
70
|
+
const mergeableState = pr.mergeable_state?.toLowerCase() ?? null;
|
|
71
|
+
const ciStatus = pr.ci_status?.toLowerCase() ?? null;
|
|
72
|
+
return mergeableState === 'unstable' && (ciStatus === null || ciStatus === 'none');
|
|
73
|
+
}
|
|
74
|
+
function downgradeNoCheckPersistedFailures(pr, blockers) {
|
|
75
|
+
if (!hasNoPublishedChecksForUnstableMergeability(pr))
|
|
76
|
+
return blockers;
|
|
77
|
+
return blockers.map((blocker) => blocker.code === 'checks_failed'
|
|
78
|
+
? mergeReadinessDetail('checks_pending', 'Required checks are still running.')
|
|
79
|
+
: blocker);
|
|
80
|
+
}
|
|
81
|
+
function removeUnresolvedConversationDetails(details) {
|
|
82
|
+
return details.filter((detail) => !isUnresolvedConversationDetail(detail));
|
|
83
|
+
}
|
|
84
|
+
function shouldIgnorePersistedUnresolvedConversationDetails(pr, blockers, warnings) {
|
|
85
|
+
return pr.unaddressed_comment_count === 0
|
|
86
|
+
&& (blockers.some(isUnresolvedConversationDetail) || warnings.some(isUnresolvedConversationDetail));
|
|
87
|
+
}
|
|
88
|
+
function isPersistedMergeReadinessCurrent(pr) {
|
|
89
|
+
const sourceSha = pr.readiness_source_head_sha ?? null;
|
|
90
|
+
const headSha = pr.head_sha ?? null;
|
|
91
|
+
if (!sourceSha || !headSha || sourceSha !== headSha)
|
|
92
|
+
return false;
|
|
93
|
+
const checkedAt = pr.readiness_updated_at ?? null;
|
|
94
|
+
const updatedAt = pr.updated_at ?? null;
|
|
95
|
+
return checkedAt !== null && (updatedAt === null || checkedAt >= updatedAt);
|
|
96
|
+
}
|
|
97
|
+
export function getPersistedMergeReadiness(pr) {
|
|
98
|
+
const status = pr.merge_readiness_status ?? null;
|
|
99
|
+
const action = pr.merge_readiness_action ?? null;
|
|
100
|
+
if (!isMergeReadinessStatus(status) || !isMergeReadinessAction(action))
|
|
101
|
+
return null;
|
|
102
|
+
if (!isPersistedMergeReadinessCurrent(pr))
|
|
103
|
+
return null;
|
|
104
|
+
let blockers = downgradeNoCheckPersistedFailures(pr, parseMergeReadinessDetails(pr.merge_readiness_blockers));
|
|
105
|
+
let warnings = parseMergeReadinessDetails(pr.merge_readiness_warnings);
|
|
106
|
+
if (shouldIgnorePersistedUnresolvedConversationDetails(pr, blockers, warnings)) {
|
|
107
|
+
blockers = removeUnresolvedConversationDetails(blockers);
|
|
108
|
+
warnings = removeUnresolvedConversationDetails(warnings);
|
|
109
|
+
if (status === 'blocked' && blockers.length === 0)
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
status,
|
|
114
|
+
action,
|
|
115
|
+
blockers,
|
|
116
|
+
warnings,
|
|
117
|
+
freshness: {
|
|
118
|
+
sourceSha: pr.readiness_source_head_sha ?? null,
|
|
119
|
+
checkedAt: pr.readiness_updated_at ?? null,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function hasMergeReadinessOptions(options) {
|
|
124
|
+
return options.requireBranchUpToDate === true
|
|
125
|
+
|| options.requireConversationResolution === true
|
|
126
|
+
|| options.requireMergeQueue === true;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Explains whether a pull request is ready for a direct merge, queue enqueueing,
|
|
130
|
+
* waiting on GitHub/merge queue, or blocked by hard requirements.
|
|
131
|
+
*/
|
|
132
|
+
export function getMergeReadiness(pr, options = {}) {
|
|
133
|
+
const warnings = [];
|
|
134
|
+
const blockers = [];
|
|
135
|
+
if (pr.state !== 'open') {
|
|
136
|
+
blockers.push(mergeReadinessDetail(pr.state === 'merged' ? 'already_merged' : 'pull_request_closed', pr.state === 'merged' ? 'Pull request is already merged.' : 'Pull request is closed.'));
|
|
137
|
+
return mergeReadinessResult(pr, 'blocked', 'resolve_blockers', blockers, warnings);
|
|
138
|
+
}
|
|
139
|
+
const persisted = hasMergeReadinessOptions(options) ? null : getPersistedMergeReadiness(pr);
|
|
140
|
+
if (persisted)
|
|
141
|
+
return persisted;
|
|
142
|
+
const mergeableState = pr.mergeable_state?.toLowerCase() ?? null;
|
|
143
|
+
const ciStatus = pr.ci_status?.toLowerCase() ?? null;
|
|
144
|
+
const reviewStatus = pr.review_status?.toLowerCase() ?? null;
|
|
145
|
+
const unaddressedCommentCount = pr.unaddressed_comment_count ?? 0;
|
|
146
|
+
if (pr.draft === true) {
|
|
147
|
+
blockers.push(mergeReadinessDetail('draft', 'Pull request is still marked as draft.'));
|
|
148
|
+
}
|
|
149
|
+
if (reviewStatus === 'changes_requested') {
|
|
150
|
+
blockers.push(mergeReadinessDetail('changes_requested', 'Review changes have been requested.'));
|
|
151
|
+
}
|
|
152
|
+
if (ciStatus === 'pending' || ciStatus === 'queued' || ciStatus === 'in_progress') {
|
|
153
|
+
blockers.push(mergeReadinessDetail('checks_pending', 'Required checks are still running.'));
|
|
154
|
+
}
|
|
155
|
+
else if (ciStatus === 'failure' || ciStatus === 'error' || ciStatus === 'cancelled' || ciStatus === 'timed_out' || ciStatus === 'action_required') {
|
|
156
|
+
blockers.push(mergeReadinessDetail('checks_failed', 'Required checks are failing.'));
|
|
157
|
+
}
|
|
158
|
+
const hasFailedChecks = blockers.some((blocker) => blocker.code === 'checks_failed');
|
|
159
|
+
const hasPendingChecks = blockers.some((blocker) => blocker.code === 'checks_pending');
|
|
160
|
+
if (mergeableState === 'unstable' && !hasFailedChecks && !hasPendingChecks) {
|
|
161
|
+
blockers.push(hasNoPublishedChecksForUnstableMergeability(pr)
|
|
162
|
+
? mergeReadinessDetail('checks_pending', 'Required checks are still running.')
|
|
163
|
+
: mergeReadinessDetail('checks_failed', 'GitHub reports failing or unstable required checks.'));
|
|
164
|
+
}
|
|
165
|
+
if (mergeableState === 'dirty' || mergeableState === 'conflicting') {
|
|
166
|
+
blockers.push(mergeReadinessDetail('merge_conflict', 'Pull request has merge conflicts.'));
|
|
167
|
+
}
|
|
168
|
+
else if (mergeableState === 'blocked') {
|
|
169
|
+
blockers.push(mergeReadinessDetail('mergeability_blocked', 'GitHub reports that mergeability is blocked.'));
|
|
170
|
+
}
|
|
171
|
+
else if (mergeableState === 'behind') {
|
|
172
|
+
if (options.requireBranchUpToDate === true) {
|
|
173
|
+
blockers.push(mergeReadinessDetail('branch_out_of_date', 'Branch must be updated before merging.'));
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
warnings.push(mergeReadinessDetail('branch_behind', 'Branch is behind the base branch.'));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (unaddressedCommentCount > 0) {
|
|
180
|
+
const detail = mergeReadinessDetail('unresolved_conversations', 'Pull request has unresolved conversations.');
|
|
181
|
+
if (options.requireConversationResolution === true) {
|
|
182
|
+
blockers.push(detail);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
warnings.push(detail);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (blockers.length > 0) {
|
|
189
|
+
return mergeReadinessResult(pr, 'blocked', 'resolve_blockers', blockers, warnings);
|
|
190
|
+
}
|
|
191
|
+
if (pr.is_queued === true) {
|
|
192
|
+
return mergeReadinessResult(pr, 'queued_pull_request', 'wait_for_queue', blockers, warnings);
|
|
193
|
+
}
|
|
194
|
+
const hasDirectMergeability = mergeableState === 'clean' || mergeableState === 'behind';
|
|
195
|
+
const hasNoCiStatus = ciStatus === null || ciStatus === 'none';
|
|
196
|
+
const hasNoReviewStatus = reviewStatus === null || reviewStatus === 'none';
|
|
197
|
+
const isUnprotectedFallback = mergeableState === null && pr.mergeable === true && hasNoCiStatus && hasNoReviewStatus;
|
|
198
|
+
if (isUnprotectedFallback) {
|
|
199
|
+
warnings.push(mergeReadinessDetail('unprotected_fallback', 'Using simple mergeability because no protected-branch checks or review state are available.'));
|
|
200
|
+
}
|
|
201
|
+
if (hasDirectMergeability || isUnprotectedFallback) {
|
|
202
|
+
return mergeReadinessResult(pr, options.requireMergeQueue === true ? 'ready_to_enqueue' : 'ready_to_merge', options.requireMergeQueue === true ? 'enqueue' : 'merge', blockers, warnings);
|
|
203
|
+
}
|
|
204
|
+
if (mergeableState === 'unknown' || pr.mergeable === null || (mergeableState === null && pr.mergeable !== false)) {
|
|
205
|
+
warnings.push(mergeReadinessDetail('mergeability_unknown', 'GitHub has not reported definitive mergeability yet.'));
|
|
206
|
+
return mergeReadinessResult(pr, 'readiness_unknown', 'wait_for_github', blockers, warnings);
|
|
207
|
+
}
|
|
208
|
+
blockers.push(mergeReadinessDetail('mergeability_blocked', 'Pull request is not mergeable.'));
|
|
209
|
+
return mergeReadinessResult(pr, 'blocked', 'resolve_blockers', blockers, warnings);
|
|
210
|
+
}
|
|
211
|
+
/** Check if a PR is ready for a direct merge action. */
|
|
212
|
+
export function isReadyToMerge(pr, options) {
|
|
213
|
+
const readiness = getMergeReadiness(pr, options);
|
|
214
|
+
return readiness.status === 'ready_to_merge' && readiness.action === 'merge';
|
|
215
|
+
}
|
|
216
|
+
/** Check if a user-initiated merge affordance may be shown/executed now. */
|
|
217
|
+
export function canMergePullRequest(pr) {
|
|
218
|
+
const readiness = getMergeReadiness(pr);
|
|
219
|
+
return readiness.status === 'ready_to_merge' && readiness.action === 'merge';
|
|
220
|
+
}
|
|
221
|
+
/** Check if a user-initiated merge-queue enqueue affordance may be shown/executed now. */
|
|
222
|
+
export function canEnqueuePullRequest(pr) {
|
|
223
|
+
const readiness = getMergeReadiness(pr);
|
|
224
|
+
return readiness.status === 'ready_to_enqueue' && readiness.action === 'enqueue';
|
|
225
|
+
}
|
|
226
|
+
function mergeReadinessPriority(pr) {
|
|
227
|
+
if (pr.state !== 'open')
|
|
228
|
+
return pr.state === 'merged' ? 100 : 90;
|
|
229
|
+
const readiness = getMergeReadiness(pr);
|
|
230
|
+
switch (readiness.status) {
|
|
231
|
+
case 'ready_to_merge': return 600;
|
|
232
|
+
case 'ready_to_enqueue': return 590;
|
|
233
|
+
case 'blocked': {
|
|
234
|
+
const blockerCodes = new Set(readiness.blockers.map((blocker) => blocker.code));
|
|
235
|
+
return blockerCodes.size === 1 && blockerCodes.has('checks_pending') ? 350 : 500;
|
|
236
|
+
}
|
|
237
|
+
case 'readiness_unknown': return 300;
|
|
238
|
+
case 'queued_pull_request': return 250;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
export function getMostAttentionWorthyPullRequest(prs) {
|
|
242
|
+
let best = null;
|
|
243
|
+
let bestPriority = Number.NEGATIVE_INFINITY;
|
|
244
|
+
for (const pr of prs) {
|
|
245
|
+
const priority = mergeReadinessPriority(pr);
|
|
246
|
+
if (priority > bestPriority) {
|
|
247
|
+
best = pr;
|
|
248
|
+
bestPriority = priority;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return best;
|
|
252
|
+
}
|
|
253
|
+
/** Check if GitHub reports a PR as queued in a merge queue. */
|
|
254
|
+
export function isQueuedForMerge(pr) {
|
|
255
|
+
return pr.state === 'open' && pr.is_queued;
|
|
256
|
+
}
|
|
257
|
+
/** Preserves optimistic and definitive states across transient background syncs */
|
|
258
|
+
export function preservePullRequestState(oldPr, newPr) {
|
|
259
|
+
if (!oldPr)
|
|
260
|
+
return newPr;
|
|
261
|
+
const result = { ...newPr };
|
|
262
|
+
// Preserve irreversible merged state if new PR hasn't caught up.
|
|
263
|
+
// Closed-but-unmerged PRs can be reopened, so a fresh open state must win.
|
|
264
|
+
if (oldPr.state === 'merged' && result.state === 'open') {
|
|
265
|
+
result.state = 'merged';
|
|
266
|
+
result.merged_at = oldPr.merged_at;
|
|
267
|
+
}
|
|
268
|
+
// Preserve definitive mergeability if new state is transient
|
|
269
|
+
const isTransient = result.mergeable === null || result.mergeable_state === 'unknown' || result.mergeable_state === null;
|
|
270
|
+
const oldIsDefinitive = oldPr.mergeable_state !== 'unknown' && oldPr.mergeable_state !== null;
|
|
271
|
+
if (isTransient && oldIsDefinitive) {
|
|
272
|
+
result.mergeable = oldPr.mergeable;
|
|
273
|
+
result.mergeable_state = oldPr.mergeable_state;
|
|
274
|
+
}
|
|
275
|
+
const oldReadiness = getPersistedMergeReadiness(oldPr);
|
|
276
|
+
const newReadiness = getPersistedMergeReadiness(result);
|
|
277
|
+
const sameReadinessSource = (oldPr.readiness_source_head_sha ?? oldPr.head_sha ?? null) === (result.readiness_source_head_sha ?? result.head_sha ?? null);
|
|
278
|
+
const newReadinessIsTransient = newReadiness === null || newReadiness.status === 'readiness_unknown';
|
|
279
|
+
if (sameReadinessSource && oldReadiness && oldReadiness.status !== 'readiness_unknown' && newReadinessIsTransient) {
|
|
280
|
+
result.merge_readiness_status = oldPr.merge_readiness_status;
|
|
281
|
+
result.merge_readiness_action = oldPr.merge_readiness_action;
|
|
282
|
+
result.merge_readiness_blockers = oldPr.merge_readiness_blockers;
|
|
283
|
+
result.merge_readiness_warnings = oldPr.merge_readiness_warnings;
|
|
284
|
+
result.readiness_source_head_sha = oldPr.readiness_source_head_sha;
|
|
285
|
+
result.readiness_updated_at = oldPr.readiness_updated_at;
|
|
286
|
+
result.merge_group_sha = oldPr.merge_group_sha;
|
|
287
|
+
result.required_checks_policy_known = oldPr.required_checks_policy_known;
|
|
288
|
+
result.required_reviews_policy_known = oldPr.required_reviews_policy_known;
|
|
289
|
+
result.merge_queue_required = oldPr.merge_queue_required;
|
|
290
|
+
result.merge_queue_state = oldPr.merge_queue_state;
|
|
291
|
+
}
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
/** GitHub label that suppresses a PR from review counting and grays out its card. */
|
|
295
|
+
export const DO_NOT_REVIEW_LABEL = 'DO NOT REVIEW';
|
|
296
|
+
/** True when the PR carries the "DO NOT REVIEW" label (case-insensitive, trimmed). */
|
|
297
|
+
export function hasDoNotReviewLabel(pr) {
|
|
298
|
+
return (pr.labels ?? []).some((label) => label.name.trim().toUpperCase() === DO_NOT_REVIEW_LABEL);
|
|
299
|
+
}
|
|
300
|
+
export function parseCheckRuns(json) {
|
|
301
|
+
if (!json)
|
|
302
|
+
return [];
|
|
303
|
+
try {
|
|
304
|
+
return JSON.parse(json);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/** Split check runs into visible (non-passing) and a count of hidden passing checks. */
|
|
311
|
+
export function splitCheckRuns(checks) {
|
|
312
|
+
const visible = [];
|
|
313
|
+
let passingCount = 0;
|
|
314
|
+
for (const check of checks) {
|
|
315
|
+
if (check.status === 'completed' && check.conclusion === 'success') {
|
|
316
|
+
passingCount++;
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
visible.push(check);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return { visible, passingCount };
|
|
323
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, Disposable, FrontendBackendBridge, FrontendOpenForgeAPI, FrontendPlugin, FrontendPluginContext, FrontendSettingsRegistry, FrontendTaskPaneRegistry, FrontendViewRegistry, NavigationAPI, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, PluginSettingsSectionProps, PluginSettingsSectionRegistration, PluginStorageScope, PluginTaskPaneProps, PluginTaskPaneTabRegistration, PluginViewProps, PluginViewRegistration } from './types';
|
|
2
|
+
export declare const OPENFORGE_FRONTEND_PLUGIN_MARKER = "__openforgeFrontendPlugin";
|
|
3
|
+
export type MarkedFrontendPlugin<TPlugin extends FrontendPlugin = FrontendPlugin> = TPlugin & {
|
|
4
|
+
readonly [OPENFORGE_FRONTEND_PLUGIN_MARKER]: true;
|
|
5
|
+
};
|
|
6
|
+
export declare function defineFrontendPlugin<const TPlugin extends FrontendPlugin>(plugin: TPlugin): MarkedFrontendPlugin<TPlugin>;
|
|
7
|
+
export type { CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, Disposable, FrontendBackendBridge, FrontendOpenForgeAPI, FrontendPlugin, FrontendPluginContext, FrontendSettingsRegistry, FrontendTaskPaneRegistry, FrontendViewRegistry, NavigationAPI, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, PluginSettingsSectionProps, PluginSettingsSectionRegistration, PluginStorageScope, PluginTaskPaneProps, PluginTaskPaneTabRegistration, PluginViewProps, PluginViewRegistration, };
|
package/dist/frontend.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const OPENFORGE_FRONTEND_PLUGIN_MARKER = '__openforgeFrontendPlugin';
|
|
2
|
+
export function defineFrontendPlugin(plugin) {
|
|
3
|
+
Object.defineProperty(plugin, OPENFORGE_FRONTEND_PLUGIN_MARKER, {
|
|
4
|
+
value: true,
|
|
5
|
+
enumerable: false,
|
|
6
|
+
configurable: false,
|
|
7
|
+
});
|
|
8
|
+
return plugin;
|
|
9
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { OpenForgePackageMetadata, OpenForgePluginCapability } from './types';
|
|
2
|
+
export declare function getRequiredOpenForgeCapabilities(metadata: OpenForgePackageMetadata): OpenForgePluginCapability[];
|
|
3
|
+
export declare function hasFrontendEntry(metadata: OpenForgePackageMetadata): boolean;
|
|
4
|
+
export declare function hasBackendEntry(metadata: OpenForgePackageMetadata): boolean;
|
package/dist/helpers.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function getRequiredOpenForgeCapabilities(metadata) {
|
|
2
|
+
return metadata.requires ?? [];
|
|
3
|
+
}
|
|
4
|
+
export function hasFrontendEntry(metadata) {
|
|
5
|
+
return typeof metadata.frontend === 'string' && metadata.frontend.length > 0;
|
|
6
|
+
}
|
|
7
|
+
export function hasBackendEntry(metadata) {
|
|
8
|
+
return typeof metadata.backend === 'string' && metadata.backend.length > 0;
|
|
9
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { OPENFORGE_PACKAGE_METADATA_SCHEMA, OPENFORGE_PLUGIN_CAPABILITIES, isOpenForgePackageMetadata, isPluginPackageMetadata, isSupportedOpenForgeApiVersion, validateOpenForgePackageMetadata, validatePluginPackageMetadata, } from './manifest';
|
|
2
|
+
export { createMemoryPluginStorage, createMockBackendOpenForgeApi, createMockFrontendOpenForgeApi, createMockOpenForgeApi, createMockPluginContext, createOpenForgeRegistryFake, createTestingCalls, TestingOpenForgeRegistryFake, TestingSubscriptionSink, } from './testing';
|
|
3
|
+
export { MAX_SUPPORTED_API_VERSION, MIN_SUPPORTED_API_VERSION, OPENFORGE_PLUGIN_API_VERSION, SUPPORTED_OPENFORGE_API_VERSIONS, isPluginViewKey, makePluginViewKey, parsePluginViewKey, } from './types';
|
|
4
|
+
export type { AttentionAPI, BackendReadyState, CommandDescriptor, CommandRegistry, CommandRegistration, CommandShortcutMetadata, ConfigureStartPromptContributionRequest, CreateTaskRequest, Disposable, FileSystemAPI, ImplementationRun, JsonObject, JsonPrimitive, JsonSchema, JsonValue, MaybePromise, KeyValueConfigAPI, NotificationRequest, NavigationAPI, NotificationsAPI, OpenForgeCommonAPI, OpenForgeContextSnapshot, OpenForgeNavigationRequest, OpenForgeNavigationSnapshot, OpenForgePackageMetadata, OpenForgePluginCapability, OpenForgePluginContext, OpenForgePluginPackageJson, PluginComponentLoader, PluginComponentModule, PluginEntry, PluginSettingsSectionProps, PluginState, PluginTaskPaneProps, PluginViewProps, PluginStorage, PluginStorageScope, PluginViewKey, ProjectsAPI, ShellAPI, ShellResizeRequest, ShellSessionRequest, ShellSpawnRequest, ShellWriteRequest, StartPromptContribution, StartTaskImplementationRequest, SubscriptionSink, SupportedOpenForgeApiVersion, SystemAPI, TasksAPI, ValidationError, } from './types';
|
|
5
|
+
export type { MockBackendOpenForgeAPI, MockFrontendOpenForgeAPI, TestingBackgroundServiceContribution, TestingBackendMethodContribution, TestingCommandContribution, TestingContributionBase, TestingEventListenerContribution, TestingOpenForgeApiCalls, TestingOpenForgeApiOptions, TestingOpenForgeRegistrySnapshot, TestingRuntimeKind, TestingRuntimeScope, TestingSettingsSectionContribution, TestingTaskPaneTabContribution, TestingViewContribution, } from './testing';
|
|
6
|
+
export { parseStrictFiniteNumber } from './numberParsing';
|
|
7
|
+
export { buildProjectFileTree, flattenVisibleProjectFileTree, formatProjectFileTreeSize, getProjectFileTreeDepth, getProjectFileTreeItemAccessibility, getProjectFileTreeKeyboardAction, getProjectFileTreeParentPath, hasProjectFileTreeShortcutModifier, projectFileTreePathToId, } from './projectFileTree';
|
|
8
|
+
export type { ProjectFileTreeEntry, ProjectFileTreeItemAccessibility, ProjectFileTreeKeyboardAction, ProjectFileTreeNode, } from './projectFileTree';
|
|
9
|
+
export { canMergePullRequest, getMergeReadiness, hasMergeConflicts, isClosedUnmergedPullRequest, isMergedPullRequest, isQueuedForMerge, isReadyToMerge, parseCheckRuns, preservePullRequestState, splitCheckRuns, } from './domain';
|
|
10
|
+
export type * from './domain';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { OPENFORGE_PACKAGE_METADATA_SCHEMA, OPENFORGE_PLUGIN_CAPABILITIES, isOpenForgePackageMetadata, isPluginPackageMetadata, isSupportedOpenForgeApiVersion, validateOpenForgePackageMetadata, validatePluginPackageMetadata, } from './manifest';
|
|
2
|
+
export { createMemoryPluginStorage, createMockBackendOpenForgeApi, createMockFrontendOpenForgeApi, createMockOpenForgeApi, createMockPluginContext, createOpenForgeRegistryFake, createTestingCalls, TestingOpenForgeRegistryFake, TestingSubscriptionSink, } from './testing';
|
|
3
|
+
export { MAX_SUPPORTED_API_VERSION, MIN_SUPPORTED_API_VERSION, OPENFORGE_PLUGIN_API_VERSION, SUPPORTED_OPENFORGE_API_VERSIONS, isPluginViewKey, makePluginViewKey, parsePluginViewKey, } from './types';
|
|
4
|
+
export { parseStrictFiniteNumber } from './numberParsing';
|
|
5
|
+
export { buildProjectFileTree, flattenVisibleProjectFileTree, formatProjectFileTreeSize, getProjectFileTreeDepth, getProjectFileTreeItemAccessibility, getProjectFileTreeKeyboardAction, getProjectFileTreeParentPath, hasProjectFileTreeShortcutModifier, projectFileTreePathToId, } from './projectFileTree';
|
|
6
|
+
export { canMergePullRequest, getMergeReadiness, hasMergeConflicts, isClosedUnmergedPullRequest, isMergedPullRequest, isQueuedForMerge, isReadyToMerge, parseCheckRuns, preservePullRequestState, splitCheckRuns, } from './domain';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { OpenForgePackageMetadata, OpenForgePluginCapability, ValidationError } from './types';
|
|
2
|
+
import { SUPPORTED_OPENFORGE_API_VERSIONS } from './types';
|
|
3
|
+
export declare const OPENFORGE_PACKAGE_METADATA_SCHEMA: {
|
|
4
|
+
$schema: string;
|
|
5
|
+
$id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
description: string;
|
|
8
|
+
type: string;
|
|
9
|
+
additionalProperties: boolean;
|
|
10
|
+
required: string[];
|
|
11
|
+
properties: {
|
|
12
|
+
id: {
|
|
13
|
+
type: string;
|
|
14
|
+
minLength: number;
|
|
15
|
+
pattern: string;
|
|
16
|
+
description: string;
|
|
17
|
+
};
|
|
18
|
+
apiVersion: {
|
|
19
|
+
enum: number[];
|
|
20
|
+
};
|
|
21
|
+
displayName: {
|
|
22
|
+
type: string;
|
|
23
|
+
minLength: number;
|
|
24
|
+
};
|
|
25
|
+
description: {
|
|
26
|
+
type: string;
|
|
27
|
+
minLength: number;
|
|
28
|
+
};
|
|
29
|
+
icon: {
|
|
30
|
+
type: string;
|
|
31
|
+
minLength: number;
|
|
32
|
+
description: string;
|
|
33
|
+
};
|
|
34
|
+
frontend: {
|
|
35
|
+
type: string;
|
|
36
|
+
minLength: number;
|
|
37
|
+
description: string;
|
|
38
|
+
};
|
|
39
|
+
backend: {
|
|
40
|
+
type: string;
|
|
41
|
+
minLength: number;
|
|
42
|
+
description: string;
|
|
43
|
+
};
|
|
44
|
+
requires: {
|
|
45
|
+
type: string;
|
|
46
|
+
uniqueItems: boolean;
|
|
47
|
+
items: {
|
|
48
|
+
enum: string[];
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
export declare const OPENFORGE_PLUGIN_CAPABILITIES: readonly OpenForgePluginCapability[];
|
|
54
|
+
export declare function isSupportedOpenForgeApiVersion(apiVersion: unknown): apiVersion is (typeof SUPPORTED_OPENFORGE_API_VERSIONS)[number];
|
|
55
|
+
export declare function validateOpenForgePackageMetadata(data: unknown): ValidationError[];
|
|
56
|
+
export declare const validatePluginPackageMetadata: typeof validateOpenForgePackageMetadata;
|
|
57
|
+
export declare function isOpenForgePackageMetadata(data: unknown): data is OpenForgePackageMetadata;
|
|
58
|
+
export declare const isPluginPackageMetadata: typeof isOpenForgePackageMetadata;
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import packageMetadataSchemaData from './openforgePackageMetadataSchema.json';
|
|
2
|
+
import { SUPPORTED_OPENFORGE_API_VERSIONS } from './types';
|
|
3
|
+
export const OPENFORGE_PACKAGE_METADATA_SCHEMA = packageMetadataSchemaData;
|
|
4
|
+
export const OPENFORGE_PLUGIN_CAPABILITIES = packageMetadataSchemaData.properties.requires.items.enum;
|
|
5
|
+
const CAPABILITIES = new Set(OPENFORGE_PLUGIN_CAPABILITIES);
|
|
6
|
+
function isString(value) {
|
|
7
|
+
return typeof value === 'string';
|
|
8
|
+
}
|
|
9
|
+
function isNonEmptyString(value) {
|
|
10
|
+
return isString(value) && value.length > 0;
|
|
11
|
+
}
|
|
12
|
+
function isObject(value) {
|
|
13
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function validateRequiredString(value, path) {
|
|
16
|
+
if (!isNonEmptyString(value)) {
|
|
17
|
+
return [{ path, message: 'Required string' }];
|
|
18
|
+
}
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
function validateOptionalString(value, path) {
|
|
22
|
+
if (value === undefined) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
if (!isNonEmptyString(value)) {
|
|
26
|
+
return [{ path, message: 'Must be a non-empty string' }];
|
|
27
|
+
}
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
export function isSupportedOpenForgeApiVersion(apiVersion) {
|
|
31
|
+
return typeof apiVersion === 'number'
|
|
32
|
+
&& Number.isInteger(apiVersion)
|
|
33
|
+
&& SUPPORTED_OPENFORGE_API_VERSIONS.includes(apiVersion);
|
|
34
|
+
}
|
|
35
|
+
function validateApiVersion(value) {
|
|
36
|
+
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
|
37
|
+
return [{ path: 'apiVersion', message: 'Required integer' }];
|
|
38
|
+
}
|
|
39
|
+
if (!isSupportedOpenForgeApiVersion(value)) {
|
|
40
|
+
return [{ path: 'apiVersion', message: `API version ${value} not supported (supported: ${SUPPORTED_OPENFORGE_API_VERSIONS.join(', ')})` }];
|
|
41
|
+
}
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
function validateRequires(value) {
|
|
45
|
+
const errors = [];
|
|
46
|
+
if (value === undefined) {
|
|
47
|
+
return errors;
|
|
48
|
+
}
|
|
49
|
+
if (!Array.isArray(value)) {
|
|
50
|
+
return [{ path: 'requires', message: 'Must be an array' }];
|
|
51
|
+
}
|
|
52
|
+
value.forEach((item, index) => {
|
|
53
|
+
const path = `requires[${index}]`;
|
|
54
|
+
if (!isString(item)) {
|
|
55
|
+
errors.push({ path, message: 'Must be a string' });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!CAPABILITIES.has(item)) {
|
|
59
|
+
errors.push({ path, message: `Unknown OpenForge capability "${item}"` });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return errors;
|
|
63
|
+
}
|
|
64
|
+
export function validateOpenForgePackageMetadata(data) {
|
|
65
|
+
const errors = [];
|
|
66
|
+
if (!isObject(data)) {
|
|
67
|
+
return [{ path: '', message: 'OpenForge package metadata must be an object' }];
|
|
68
|
+
}
|
|
69
|
+
errors.push(...validateRequiredString(data.id, 'id'));
|
|
70
|
+
errors.push(...validateApiVersion(data.apiVersion));
|
|
71
|
+
errors.push(...validateRequiredString(data.displayName, 'displayName'));
|
|
72
|
+
errors.push(...validateRequiredString(data.description, 'description'));
|
|
73
|
+
errors.push(...validateOptionalString(data.icon, 'icon'));
|
|
74
|
+
errors.push(...validateOptionalString(data.frontend, 'frontend'));
|
|
75
|
+
errors.push(...validateOptionalString(data.backend, 'backend'));
|
|
76
|
+
errors.push(...validateRequires(data.requires));
|
|
77
|
+
if (data.contributes !== undefined) {
|
|
78
|
+
errors.push({ path: 'contributes', message: 'Manifest contribution arrays are not supported; register contributions at runtime' });
|
|
79
|
+
}
|
|
80
|
+
for (const key of Object.keys(data)) {
|
|
81
|
+
if (!Object.prototype.hasOwnProperty.call(OPENFORGE_PACKAGE_METADATA_SCHEMA.properties, key)) {
|
|
82
|
+
if (key !== 'contributes') {
|
|
83
|
+
errors.push({ path: key, message: 'Unknown OpenForge package metadata field' });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return errors;
|
|
88
|
+
}
|
|
89
|
+
export const validatePluginPackageMetadata = validateOpenForgePackageMetadata;
|
|
90
|
+
export function isOpenForgePackageMetadata(data) {
|
|
91
|
+
return validateOpenForgePackageMetadata(data).length === 0;
|
|
92
|
+
}
|
|
93
|
+
export const isPluginPackageMetadata = isOpenForgePackageMetadata;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export interface RenderMarkdownOptions {
|
|
2
|
+
imageBaseUrl?: string | null;
|
|
3
|
+
}
|
|
4
|
+
export declare function resolveMarkdownImageSrc(src: string | null, imageBaseUrl: string | null | undefined): string | null;
|
|
5
|
+
export declare function renderMarkdownHtml(content: string, options?: RenderMarkdownOptions): string;
|
package/dist/markdown.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { marked } from 'marked';
|
|
2
|
+
import { sanitizeHtml } from './sanitize';
|
|
3
|
+
const RELATIVE_PARENT_SEGMENT = /^\.\.\//;
|
|
4
|
+
const RELATIVE_CURRENT_SEGMENT = /^\.\//;
|
|
5
|
+
const markedOptions = {
|
|
6
|
+
gfm: true,
|
|
7
|
+
breaks: true,
|
|
8
|
+
};
|
|
9
|
+
function hasAbsoluteOrSpecialUrl(value) {
|
|
10
|
+
return /^[a-z][a-z\d+.-]*:/i.test(value) || value.startsWith('//') || value.startsWith('#');
|
|
11
|
+
}
|
|
12
|
+
function normalizeRepoRelativeImagePath(value) {
|
|
13
|
+
let normalized = value.startsWith('/') ? value.slice(1) : value;
|
|
14
|
+
while (RELATIVE_CURRENT_SEGMENT.test(normalized)) {
|
|
15
|
+
normalized = normalized.replace(RELATIVE_CURRENT_SEGMENT, '');
|
|
16
|
+
}
|
|
17
|
+
while (RELATIVE_PARENT_SEGMENT.test(normalized)) {
|
|
18
|
+
normalized = normalized.replace(RELATIVE_PARENT_SEGMENT, '');
|
|
19
|
+
}
|
|
20
|
+
return normalized;
|
|
21
|
+
}
|
|
22
|
+
function withTrailingSlash(value) {
|
|
23
|
+
return value.endsWith('/') ? value : `${value}/`;
|
|
24
|
+
}
|
|
25
|
+
export function resolveMarkdownImageSrc(src, imageBaseUrl) {
|
|
26
|
+
if (!src || !imageBaseUrl)
|
|
27
|
+
return null;
|
|
28
|
+
const trimmedSrc = src.trim();
|
|
29
|
+
if (!trimmedSrc || hasAbsoluteOrSpecialUrl(trimmedSrc))
|
|
30
|
+
return null;
|
|
31
|
+
try {
|
|
32
|
+
return new URL(normalizeRepoRelativeImagePath(trimmedSrc), withTrailingSlash(imageBaseUrl)).href;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function resolveMarkdownImageSources(html, imageBaseUrl) {
|
|
39
|
+
if (!imageBaseUrl || typeof document === 'undefined')
|
|
40
|
+
return html;
|
|
41
|
+
const template = document.createElement('template');
|
|
42
|
+
template.innerHTML = html;
|
|
43
|
+
for (const image of template.content.querySelectorAll('img[src]')) {
|
|
44
|
+
const resolvedSrc = resolveMarkdownImageSrc(image.getAttribute('src'), imageBaseUrl);
|
|
45
|
+
if (resolvedSrc) {
|
|
46
|
+
image.setAttribute('src', resolvedSrc);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return template.innerHTML;
|
|
50
|
+
}
|
|
51
|
+
export function renderMarkdownHtml(content, options = {}) {
|
|
52
|
+
const rawHtml = marked.parse(content, markedOptions);
|
|
53
|
+
return sanitizeHtml(resolveMarkdownImageSources(rawHtml, options.imageBaseUrl));
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function parseStrictFiniteNumber(value: string): number | null;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const STRICT_FINITE_NUMBER_PATTERN = /^[+-]?(?:\d+(?:\.\d+)?|\.\d+)$/;
|
|
2
|
+
export function parseStrictFiniteNumber(value) {
|
|
3
|
+
if (!STRICT_FINITE_NUMBER_PATTERN.test(value)) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
const parsed = Number(value);
|
|
7
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
8
|
+
}
|