@foundation0/api 1.1.2 → 1.1.4
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/agents.ts +7 -6
- package/git.ts +2 -2
- package/libs/curl.test.ts +130 -0
- package/libs/curl.ts +770 -0
- package/mcp/AGENTS.md +130 -0
- package/mcp/server.test.ts +464 -250
- package/mcp/server.ts +2449 -1673
- package/net.ts +170 -0
- package/package.json +5 -3
- package/projects.ts +582 -79
package/projects.ts
CHANGED
|
@@ -94,6 +94,9 @@ type ProjectSyncTaskOptions = {
|
|
|
94
94
|
maxBodyLength?: number
|
|
95
95
|
requestTimeoutMs?: number
|
|
96
96
|
throttleMs?: number
|
|
97
|
+
retryMax?: number
|
|
98
|
+
retryBaseDelayMs?: number
|
|
99
|
+
retryMaxDelayMs?: number
|
|
97
100
|
skipDependencies?: boolean
|
|
98
101
|
verbose?: boolean
|
|
99
102
|
}
|
|
@@ -119,6 +122,14 @@ type ProjectWriteGitTaskOptions = {
|
|
|
119
122
|
taskSignature?: string
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
type ProjectCreateGitIssueOptions = {
|
|
126
|
+
owner?: string
|
|
127
|
+
repo?: string
|
|
128
|
+
title: string
|
|
129
|
+
body?: string
|
|
130
|
+
labels?: string[]
|
|
131
|
+
}
|
|
132
|
+
|
|
122
133
|
export type ProjectGitTaskRecord = {
|
|
123
134
|
number: number
|
|
124
135
|
title: string
|
|
@@ -160,6 +171,22 @@ export type ProjectWriteGitTaskResult = {
|
|
|
160
171
|
issue: ProjectGitTaskRecord
|
|
161
172
|
}
|
|
162
173
|
|
|
174
|
+
export type ProjectGitIssueRecord = {
|
|
175
|
+
number: number
|
|
176
|
+
title: string
|
|
177
|
+
body: string
|
|
178
|
+
state: string
|
|
179
|
+
labels: string[]
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export type ProjectCreateGitIssueResult = {
|
|
183
|
+
projectName: string
|
|
184
|
+
owner: string
|
|
185
|
+
repo: string
|
|
186
|
+
issueNumber: number
|
|
187
|
+
issue: ProjectGitIssueRecord
|
|
188
|
+
}
|
|
189
|
+
|
|
163
190
|
type ProjectSearchTargetSection = 'docs' | 'spec'
|
|
164
191
|
|
|
165
192
|
type ProjectSearchOptions = {
|
|
@@ -227,7 +254,7 @@ export type ProjectSearchResult = {
|
|
|
227
254
|
output: string
|
|
228
255
|
}
|
|
229
256
|
|
|
230
|
-
const CLI_NAME = '
|
|
257
|
+
const CLI_NAME = 'f0'
|
|
231
258
|
const VERSION_RE = 'v?\\d+(?:\\.\\d+){2,}'
|
|
232
259
|
const VERSION_RE_CORE = `(?:${VERSION_RE})`
|
|
233
260
|
const ACTIVE_EXT_PRIORITY = ['.md', '.json', '.yaml', '.yml', '.ts', '.js', '.txt']
|
|
@@ -268,10 +295,10 @@ function resolveProjectsRoot(processRoot: string): string {
|
|
|
268
295
|
}
|
|
269
296
|
|
|
270
297
|
export function usage(): string {
|
|
271
|
-
return `Usage:\n ${CLI_NAME} projects <project-name> --generate-spec\n ${CLI_NAME} projects <project-name> <file-path> --set-active\n ${CLI_NAME} projects
|
|
272
|
-
`Examples:\n ${CLI_NAME} projects index --generate-spec\n ${CLI_NAME} projects index /implementation-plan.v0.0.1 --set-active\n ${CLI_NAME} projects index /implementation-plan --set-active --latest\n ${CLI_NAME} projects index sync-tasks\n ${CLI_NAME} projects index sync-tasks --verbose\n ${CLI_NAME} projects index sync-tasks --request-timeout-ms 60000 --throttle-ms 250\n ${CLI_NAME} projects index sync-tasks --skip-dependencies\n ${CLI_NAME} projects index clear-issues --dry-run\n ${CLI_NAME} projects index clear-issues --force\n ${CLI_NAME} projects index clear-issues --state all --force\n ${CLI_NAME} projects --list\n\n` +
|
|
298
|
+
return `Usage:\n ${CLI_NAME} projects <project-name> --generate-spec\n ${CLI_NAME} projects <project-name> <file-path> --set-active\n ${CLI_NAME} projects <project-name> --sync-tasks (alias: --sync-issues)\n ${CLI_NAME} projects <project-name> --clear-issues\n ${CLI_NAME} projects --list\n\n` +
|
|
299
|
+
`Examples:\n ${CLI_NAME} projects index --generate-spec\n ${CLI_NAME} projects index /implementation-plan.v0.0.1 --set-active\n ${CLI_NAME} projects index /implementation-plan --set-active --latest\n ${CLI_NAME} projects index sync-tasks\n ${CLI_NAME} projects index sync-tasks --verbose\n ${CLI_NAME} projects index sync-tasks --request-timeout-ms 60000 --throttle-ms 250\n ${CLI_NAME} projects index sync-tasks --retry 5 --retry-base-delay-ms 500\n ${CLI_NAME} projects index sync-tasks --skip-dependencies\n ${CLI_NAME} projects index clear-issues --dry-run\n ${CLI_NAME} projects index clear-issues --force\n ${CLI_NAME} projects index clear-issues --state all --force\n ${CLI_NAME} projects --list\n\n` +
|
|
273
300
|
`file-path is relative to the project root (leading slash required).\n` +
|
|
274
|
-
`Sync-tasks flags: --verbose --max --max-body --request-timeout-ms --throttle-ms --skip-dependencies --owner --repo --label --prefix --tracks.\n` +
|
|
301
|
+
`Sync-tasks flags: --verbose --max --max-body --request-timeout-ms --throttle-ms --retry --retry-base-delay-ms --retry-max-delay-ms --no-retry --skip-dependencies --owner --repo --label --prefix --tracks.\n` +
|
|
275
302
|
`Use --verbose with clear-issues to print each deletion.\n` +
|
|
276
303
|
`Use --latest to resolve /file-name to the latest version.\n` +
|
|
277
304
|
`The active file created is [file].active.<ext>.\n`
|
|
@@ -1943,7 +1970,10 @@ function parseSyncTasksArgs(argv: string[]): {
|
|
|
1943
1970
|
prefix: '[TASK]',
|
|
1944
1971
|
dryRun: false,
|
|
1945
1972
|
requestTimeoutMs: 60_000,
|
|
1946
|
-
throttleMs:
|
|
1973
|
+
throttleMs: 200,
|
|
1974
|
+
retryMax: 5,
|
|
1975
|
+
retryBaseDelayMs: 500,
|
|
1976
|
+
retryMaxDelayMs: 10_000,
|
|
1947
1977
|
skipDependencies: false,
|
|
1948
1978
|
verbose: false,
|
|
1949
1979
|
}
|
|
@@ -2053,6 +2083,41 @@ function parseSyncTasksArgs(argv: string[]): {
|
|
|
2053
2083
|
continue
|
|
2054
2084
|
}
|
|
2055
2085
|
|
|
2086
|
+
if (arg === '--retry') {
|
|
2087
|
+
const value = Number(argv[i + 1])
|
|
2088
|
+
if (!Number.isFinite(value) || value < 0 || Math.floor(value) !== value) {
|
|
2089
|
+
throw new Error(`Invalid --retry value: ${argv[i + 1]}`)
|
|
2090
|
+
}
|
|
2091
|
+
options.retryMax = value
|
|
2092
|
+
i += 1
|
|
2093
|
+
continue
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
if (arg === '--retry-base-delay-ms') {
|
|
2097
|
+
const value = Number(argv[i + 1])
|
|
2098
|
+
if (!Number.isFinite(value) || value < 0 || Math.floor(value) !== value) {
|
|
2099
|
+
throw new Error(`Invalid --retry-base-delay-ms value: ${argv[i + 1]}`)
|
|
2100
|
+
}
|
|
2101
|
+
options.retryBaseDelayMs = value
|
|
2102
|
+
i += 1
|
|
2103
|
+
continue
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
if (arg === '--retry-max-delay-ms') {
|
|
2107
|
+
const value = Number(argv[i + 1])
|
|
2108
|
+
if (!Number.isFinite(value) || value < 0 || Math.floor(value) !== value) {
|
|
2109
|
+
throw new Error(`Invalid --retry-max-delay-ms value: ${argv[i + 1]}`)
|
|
2110
|
+
}
|
|
2111
|
+
options.retryMaxDelayMs = value
|
|
2112
|
+
i += 1
|
|
2113
|
+
continue
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
if (arg === '--no-retry') {
|
|
2117
|
+
options.retryMax = 0
|
|
2118
|
+
continue
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2056
2121
|
if (arg === '--skip-dependencies') {
|
|
2057
2122
|
options.skipDependencies = true
|
|
2058
2123
|
continue
|
|
@@ -2567,6 +2632,21 @@ function toProjectGitTaskRecord(issue: GitIssueLike): ProjectGitTaskRecord | nul
|
|
|
2567
2632
|
}
|
|
2568
2633
|
}
|
|
2569
2634
|
|
|
2635
|
+
function toProjectGitIssueRecord(issue: GitIssueLike): ProjectGitIssueRecord | null {
|
|
2636
|
+
const raw = issue as Record<string, unknown>
|
|
2637
|
+
|
|
2638
|
+
const issueNumber = Number(raw.number)
|
|
2639
|
+
if (!Number.isInteger(issueNumber) || issueNumber <= 0) return null
|
|
2640
|
+
|
|
2641
|
+
return {
|
|
2642
|
+
number: issueNumber,
|
|
2643
|
+
title: typeof raw.title === 'string' ? raw.title : `Issue #${issueNumber}`,
|
|
2644
|
+
body: typeof raw.body === 'string' ? raw.body : '',
|
|
2645
|
+
state: typeof raw.state === 'string' ? raw.state : 'unknown',
|
|
2646
|
+
labels: normalizeIssueLabels(raw.labels),
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2570
2650
|
function resolveProjectGitRepository(
|
|
2571
2651
|
projectName: string,
|
|
2572
2652
|
processRoot: string,
|
|
@@ -2660,24 +2740,141 @@ type GitIssueLike = {
|
|
|
2660
2740
|
body?: unknown
|
|
2661
2741
|
}
|
|
2662
2742
|
|
|
2743
|
+
type NetworkRetryOptions = {
|
|
2744
|
+
maxRetries: number
|
|
2745
|
+
baseDelayMs: number
|
|
2746
|
+
maxDelayMs: number
|
|
2747
|
+
retryOnStatuses: ReadonlySet<number>
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
const DEFAULT_RETRYABLE_HTTP_STATUSES = new Set<number>([408, 425, 429, 500, 502, 503, 504])
|
|
2751
|
+
|
|
2752
|
+
function resolveNetworkRetryOptions(options: {
|
|
2753
|
+
retryMax?: number
|
|
2754
|
+
retryBaseDelayMs?: number
|
|
2755
|
+
retryMaxDelayMs?: number
|
|
2756
|
+
}): NetworkRetryOptions {
|
|
2757
|
+
const maxRetries = Number.isFinite(options.retryMax) ? Math.max(0, Math.floor(options.retryMax ?? 0)) : 0
|
|
2758
|
+
const baseDelayMs = Number.isFinite(options.retryBaseDelayMs) ? Math.max(0, Math.floor(options.retryBaseDelayMs ?? 0)) : 0
|
|
2759
|
+
const maxDelayMs = Number.isFinite(options.retryMaxDelayMs) ? Math.max(0, Math.floor(options.retryMaxDelayMs ?? 0)) : 0
|
|
2760
|
+
|
|
2761
|
+
return {
|
|
2762
|
+
maxRetries,
|
|
2763
|
+
baseDelayMs,
|
|
2764
|
+
maxDelayMs,
|
|
2765
|
+
retryOnStatuses: DEFAULT_RETRYABLE_HTTP_STATUSES,
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
|
|
2770
|
+
|
|
2771
|
+
function retryDelayMs(retryNumber: number, baseDelayMs: number, maxDelayMs: number): number {
|
|
2772
|
+
if (retryNumber <= 0) {
|
|
2773
|
+
return 0
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
const exponent = Math.max(0, retryNumber - 1)
|
|
2777
|
+
const raw = baseDelayMs * (2 ** exponent)
|
|
2778
|
+
const capped = maxDelayMs > 0 ? Math.min(raw, maxDelayMs) : raw
|
|
2779
|
+
const jitter = 0.5 + Math.random()
|
|
2780
|
+
return Math.max(0, Math.floor(capped * jitter))
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
function isRetryableError(error: unknown, retryOnStatuses: ReadonlySet<number>): boolean {
|
|
2784
|
+
if (!(error instanceof Error)) {
|
|
2785
|
+
return false
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
const message = String(error.message ?? '').toLowerCase()
|
|
2789
|
+
const statusMatch = message.match(/status\\s*=\\s*(\\d{3})/)
|
|
2790
|
+
if (statusMatch) {
|
|
2791
|
+
const status = Number(statusMatch[1])
|
|
2792
|
+
if (Number.isInteger(status) && retryOnStatuses.has(status)) {
|
|
2793
|
+
return true
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
if (message.includes('request timed out')) return true
|
|
2798
|
+
if (message.includes('timeout')) return true
|
|
2799
|
+
if (message.includes('fetch failed')) return true
|
|
2800
|
+
if (message.includes('network')) return true
|
|
2801
|
+
if (message.includes('socket')) return true
|
|
2802
|
+
if (message.includes('econnreset')) return true
|
|
2803
|
+
if (message.includes('econnrefused')) return true
|
|
2804
|
+
if (message.includes('enotfound')) return true
|
|
2805
|
+
if (message.includes('eai_again')) return true
|
|
2806
|
+
if (message.includes('tls')) return true
|
|
2807
|
+
|
|
2808
|
+
return false
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
async function withRetry<T>(
|
|
2812
|
+
label: string,
|
|
2813
|
+
operation: () => Promise<T>,
|
|
2814
|
+
retry: NetworkRetryOptions,
|
|
2815
|
+
log: ((message: string) => void) | null,
|
|
2816
|
+
shouldRetryValue?: (value: T) => string | null,
|
|
2817
|
+
): Promise<T> {
|
|
2818
|
+
const maxAttempts = Math.max(1, retry.maxRetries + 1)
|
|
2819
|
+
|
|
2820
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
2821
|
+
try {
|
|
2822
|
+
const value = await operation()
|
|
2823
|
+
const retryReason = shouldRetryValue ? shouldRetryValue(value) : null
|
|
2824
|
+
if (retryReason && attempt < maxAttempts) {
|
|
2825
|
+
const delay = retryDelayMs(attempt, retry.baseDelayMs, retry.maxDelayMs)
|
|
2826
|
+
log?.(`${label}: retrying (attempt ${attempt}/${maxAttempts}) reason=${retryReason} delay=${delay}ms`)
|
|
2827
|
+
if (delay > 0) await sleep(delay)
|
|
2828
|
+
continue
|
|
2829
|
+
}
|
|
2830
|
+
return value
|
|
2831
|
+
} catch (error) {
|
|
2832
|
+
if (attempt >= maxAttempts || !isRetryableError(error, retry.retryOnStatuses)) {
|
|
2833
|
+
throw error
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
const delay = retryDelayMs(attempt, retry.baseDelayMs, retry.maxDelayMs)
|
|
2837
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
2838
|
+
log?.(`${label}: retrying (attempt ${attempt}/${maxAttempts}) error=${message} delay=${delay}ms`)
|
|
2839
|
+
if (delay > 0) await sleep(delay)
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
// Unreachable, but keeps TS happy.
|
|
2844
|
+
throw new Error(`${label}: retry loop exhausted`)
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2663
2847
|
async function fetchAllIssues(
|
|
2664
2848
|
issueList: GitServiceApiMethod,
|
|
2665
2849
|
owner: string,
|
|
2666
2850
|
repo: string,
|
|
2667
2851
|
state: 'open' | 'closed' | 'all',
|
|
2668
2852
|
projectName: string,
|
|
2669
|
-
log?: (message: string) => void
|
|
2853
|
+
log?: (message: string) => void,
|
|
2854
|
+
requestOptions?: { retry?: NetworkRetryOptions; throttleMs?: number },
|
|
2670
2855
|
): Promise<GitIssueLike[]> {
|
|
2671
2856
|
const allIssues: GitIssueLike[] = []
|
|
2672
2857
|
let page = 1
|
|
2858
|
+
const retry = requestOptions?.retry ?? resolveNetworkRetryOptions({ retryMax: 0, retryBaseDelayMs: 0, retryMaxDelayMs: 0 })
|
|
2859
|
+
const throttleMs = requestOptions?.throttleMs ?? 0
|
|
2673
2860
|
|
|
2674
2861
|
while (true) {
|
|
2675
2862
|
log?.(`Listing ${state} issues page ${page} for ${owner}/${repo}...`)
|
|
2676
|
-
const listResult = await
|
|
2677
|
-
state
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2863
|
+
const listResult = await withRetry(
|
|
2864
|
+
`issue.list ${owner}/${repo} state=${state} page=${page}`,
|
|
2865
|
+
() => issueList(owner, repo, {
|
|
2866
|
+
state,
|
|
2867
|
+
limit: ISSUE_LIST_PAGE_SIZE,
|
|
2868
|
+
query: { page },
|
|
2869
|
+
}),
|
|
2870
|
+
retry,
|
|
2871
|
+
log ?? null,
|
|
2872
|
+
(result) => {
|
|
2873
|
+
const response = result as GitServiceApiExecutionResult
|
|
2874
|
+
if (response.ok) return null
|
|
2875
|
+
return retry.retryOnStatuses.has(response.status) ? `status=${response.status}` : null
|
|
2876
|
+
},
|
|
2877
|
+
)
|
|
2681
2878
|
|
|
2682
2879
|
if (!listResult.ok) {
|
|
2683
2880
|
throw new Error(
|
|
@@ -2697,12 +2894,205 @@ async function fetchAllIssues(
|
|
|
2697
2894
|
break
|
|
2698
2895
|
}
|
|
2699
2896
|
page += 1
|
|
2897
|
+
|
|
2898
|
+
if (throttleMs > 0) {
|
|
2899
|
+
await sleep(throttleMs)
|
|
2900
|
+
}
|
|
2700
2901
|
}
|
|
2701
2902
|
|
|
2702
2903
|
log?.(`Loaded ${allIssues.length} ${state} issues in ${owner}/${repo} across pages.`)
|
|
2703
2904
|
return allIssues
|
|
2704
2905
|
}
|
|
2705
2906
|
|
|
2907
|
+
async function findIssueNumberByTaskId(
|
|
2908
|
+
issueList: GitServiceApiMethod,
|
|
2909
|
+
owner: string,
|
|
2910
|
+
repo: string,
|
|
2911
|
+
taskId: string,
|
|
2912
|
+
projectName: string,
|
|
2913
|
+
retry: NetworkRetryOptions,
|
|
2914
|
+
throttleMs: number,
|
|
2915
|
+
log: ((message: string) => void) | null,
|
|
2916
|
+
): Promise<number | null> {
|
|
2917
|
+
let page = 1
|
|
2918
|
+
|
|
2919
|
+
while (true) {
|
|
2920
|
+
const listResult = await withRetry(
|
|
2921
|
+
`issue.list (lookup ${taskId}) ${owner}/${repo} page=${page}`,
|
|
2922
|
+
() => issueList(owner, repo, {
|
|
2923
|
+
state: 'open',
|
|
2924
|
+
limit: ISSUE_LIST_PAGE_SIZE,
|
|
2925
|
+
query: { page },
|
|
2926
|
+
}),
|
|
2927
|
+
retry,
|
|
2928
|
+
log,
|
|
2929
|
+
(result) => {
|
|
2930
|
+
const response = result as GitServiceApiExecutionResult
|
|
2931
|
+
if (response.ok) return null
|
|
2932
|
+
return retry.retryOnStatuses.has(response.status) ? `status=${response.status}` : null
|
|
2933
|
+
},
|
|
2934
|
+
)
|
|
2935
|
+
|
|
2936
|
+
if (!listResult.ok) {
|
|
2937
|
+
throw new Error(
|
|
2938
|
+
`Failed to list open issues for ${owner}/${repo}: status=${listResult.status} url=${listResult.request.url}`
|
|
2939
|
+
)
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
if (!Array.isArray(listResult.body)) {
|
|
2943
|
+
throw new Error(`Expected array from issue.list for ${projectName} (open issues lookup, page ${page})`)
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
const batch = listResult.body as GitIssueLike[]
|
|
2947
|
+
for (const issue of batch) {
|
|
2948
|
+
const record = toProjectGitTaskRecord(issue)
|
|
2949
|
+
if (record?.taskId === taskId) {
|
|
2950
|
+
return record.number
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
if (batch.length === 0) {
|
|
2955
|
+
break
|
|
2956
|
+
}
|
|
2957
|
+
page += 1
|
|
2958
|
+
|
|
2959
|
+
if (throttleMs > 0) {
|
|
2960
|
+
await sleep(throttleMs)
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
return null
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
async function findIssueNumberByTaskIdEventually(
|
|
2968
|
+
issueList: GitServiceApiMethod,
|
|
2969
|
+
owner: string,
|
|
2970
|
+
repo: string,
|
|
2971
|
+
taskId: string,
|
|
2972
|
+
projectName: string,
|
|
2973
|
+
retry: NetworkRetryOptions,
|
|
2974
|
+
throttleMs: number,
|
|
2975
|
+
log: ((message: string) => void) | null,
|
|
2976
|
+
attempts: number,
|
|
2977
|
+
delayMs: number,
|
|
2978
|
+
): Promise<number | null> {
|
|
2979
|
+
const totalAttempts = Math.max(1, Math.floor(attempts))
|
|
2980
|
+
|
|
2981
|
+
for (let i = 0; i < totalAttempts; i += 1) {
|
|
2982
|
+
const existing = await findIssueNumberByTaskId(issueList, owner, repo, taskId, projectName, retry, throttleMs, log)
|
|
2983
|
+
if (existing) {
|
|
2984
|
+
return existing
|
|
2985
|
+
}
|
|
2986
|
+
if (i < totalAttempts - 1 && delayMs > 0) {
|
|
2987
|
+
await sleep(delayMs)
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
return null
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
async function createTaskIssueNumberWithRetry(
|
|
2995
|
+
issueCreate: GitServiceApiMethod,
|
|
2996
|
+
issueList: GitServiceApiMethod,
|
|
2997
|
+
owner: string,
|
|
2998
|
+
repo: string,
|
|
2999
|
+
taskId: string,
|
|
3000
|
+
payload: Record<string, unknown>,
|
|
3001
|
+
projectName: string,
|
|
3002
|
+
retry: NetworkRetryOptions,
|
|
3003
|
+
throttleMs: number,
|
|
3004
|
+
log: ((message: string) => void) | null,
|
|
3005
|
+
): Promise<number> {
|
|
3006
|
+
const maxAttempts = Math.max(1, retry.maxRetries + 1)
|
|
3007
|
+
const verifyDelayMs = retry.baseDelayMs > 0 ? Math.min(500, retry.baseDelayMs) : 0
|
|
3008
|
+
const verifyBudgetMs = retry.maxDelayMs > 0 ? Math.min(10_000, retry.maxDelayMs) : 0
|
|
3009
|
+
const verifyAttempts = verifyDelayMs > 0 ? Math.max(1, Math.ceil(verifyBudgetMs / verifyDelayMs)) : 1
|
|
3010
|
+
|
|
3011
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
3012
|
+
try {
|
|
3013
|
+
const createResult = await issueCreate(owner, repo, { data: payload }) as GitServiceApiExecutionResult
|
|
3014
|
+
|
|
3015
|
+
if (createResult.ok) {
|
|
3016
|
+
const issueNumber = buildIssueNumberFromPayload(createResult.body)
|
|
3017
|
+
if (issueNumber === null) {
|
|
3018
|
+
throw new Error(
|
|
3019
|
+
`Created issue response for ${taskId} in ${owner}/${repo} is invalid: ${JSON.stringify(createResult.body)}`
|
|
3020
|
+
)
|
|
3021
|
+
}
|
|
3022
|
+
return issueNumber
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
const shouldRetry = retry.retryOnStatuses.has(createResult.status) && attempt < maxAttempts
|
|
3026
|
+
if (!shouldRetry) {
|
|
3027
|
+
const bodySummary = (() => {
|
|
3028
|
+
try {
|
|
3029
|
+
if (typeof createResult.body === 'string') {
|
|
3030
|
+
return createResult.body.slice(0, 500)
|
|
3031
|
+
}
|
|
3032
|
+
return JSON.stringify(createResult.body)?.slice(0, 500) ?? String(createResult.body)
|
|
3033
|
+
} catch {
|
|
3034
|
+
return String(createResult.body)
|
|
3035
|
+
}
|
|
3036
|
+
})()
|
|
3037
|
+
throw new Error(
|
|
3038
|
+
`Failed to create issue for ${taskId} in ${owner}/${repo}: status=${createResult.status} url=${createResult.request.url} body=${bodySummary}`
|
|
3039
|
+
)
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
log?.(`Create issue for ${taskId}: retryable response status=${createResult.status}; checking if issue already exists...`)
|
|
3043
|
+
const existing = await findIssueNumberByTaskIdEventually(
|
|
3044
|
+
issueList,
|
|
3045
|
+
owner,
|
|
3046
|
+
repo,
|
|
3047
|
+
taskId,
|
|
3048
|
+
projectName,
|
|
3049
|
+
retry,
|
|
3050
|
+
throttleMs,
|
|
3051
|
+
log,
|
|
3052
|
+
verifyAttempts,
|
|
3053
|
+
verifyDelayMs,
|
|
3054
|
+
)
|
|
3055
|
+
if (existing) {
|
|
3056
|
+
log?.(`Create issue for ${taskId}: found existing issue #${existing} after failure; continuing.`)
|
|
3057
|
+
return existing
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
const delay = retryDelayMs(attempt, retry.baseDelayMs, retry.maxDelayMs)
|
|
3061
|
+
log?.(`Create issue for ${taskId}: retrying (attempt ${attempt}/${maxAttempts}) delay=${delay}ms`)
|
|
3062
|
+
if (delay > 0) await sleep(delay)
|
|
3063
|
+
} catch (error) {
|
|
3064
|
+
if (attempt >= maxAttempts || !isRetryableError(error, retry.retryOnStatuses)) {
|
|
3065
|
+
throw error
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
3069
|
+
log?.(`Create issue for ${taskId}: retryable error (${message}); checking if issue already exists...`)
|
|
3070
|
+
const existing = await findIssueNumberByTaskIdEventually(
|
|
3071
|
+
issueList,
|
|
3072
|
+
owner,
|
|
3073
|
+
repo,
|
|
3074
|
+
taskId,
|
|
3075
|
+
projectName,
|
|
3076
|
+
retry,
|
|
3077
|
+
throttleMs,
|
|
3078
|
+
log,
|
|
3079
|
+
verifyAttempts,
|
|
3080
|
+
verifyDelayMs,
|
|
3081
|
+
)
|
|
3082
|
+
if (existing) {
|
|
3083
|
+
log?.(`Create issue for ${taskId}: found existing issue #${existing} after error; continuing.`)
|
|
3084
|
+
return existing
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
const delay = retryDelayMs(attempt, retry.baseDelayMs, retry.maxDelayMs)
|
|
3088
|
+
log?.(`Create issue for ${taskId}: retrying (attempt ${attempt}/${maxAttempts}) delay=${delay}ms`)
|
|
3089
|
+
if (delay > 0) await sleep(delay)
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
throw new Error(`Create issue for ${taskId}: retry loop exhausted`)
|
|
3094
|
+
}
|
|
3095
|
+
|
|
2706
3096
|
function issueDependenciesMatches(taskDeps: string[], issue: ProjectSyncIssueRecord): boolean {
|
|
2707
3097
|
const fromBody = issue.dependenciesFromBody
|
|
2708
3098
|
if (fromBody) {
|
|
@@ -2977,9 +3367,21 @@ export async function writeGitTask(
|
|
|
2977
3367
|
}
|
|
2978
3368
|
|
|
2979
3369
|
if (!issueNumber) {
|
|
2980
|
-
const createResult = await issueCreate(owner, repo, payload)
|
|
3370
|
+
const createResult = await issueCreate(owner, repo, { data: payload })
|
|
2981
3371
|
if (!createResult.ok) {
|
|
2982
|
-
|
|
3372
|
+
const bodySummary = (() => {
|
|
3373
|
+
try {
|
|
3374
|
+
if (typeof createResult.body === 'string') {
|
|
3375
|
+
return createResult.body.slice(0, 500)
|
|
3376
|
+
}
|
|
3377
|
+
return JSON.stringify(createResult.body)?.slice(0, 500) ?? String(createResult.body)
|
|
3378
|
+
} catch {
|
|
3379
|
+
return String(createResult.body)
|
|
3380
|
+
}
|
|
3381
|
+
})()
|
|
3382
|
+
throw new Error(
|
|
3383
|
+
`Failed to create task in ${owner}/${repo}: status=${createResult.status} url=${createResult.request.url} body=${bodySummary}`
|
|
3384
|
+
)
|
|
2983
3385
|
}
|
|
2984
3386
|
|
|
2985
3387
|
const issue = toProjectGitTaskRecord(createResult.body as GitIssueLike)
|
|
@@ -2997,9 +3399,21 @@ export async function writeGitTask(
|
|
|
2997
3399
|
}
|
|
2998
3400
|
}
|
|
2999
3401
|
|
|
3000
|
-
const updateResult = await issueEdit(owner, repo, issueNumber, payload)
|
|
3402
|
+
const updateResult = await issueEdit(owner, repo, issueNumber, { data: payload })
|
|
3001
3403
|
if (!updateResult.ok) {
|
|
3002
|
-
|
|
3404
|
+
const bodySummary = (() => {
|
|
3405
|
+
try {
|
|
3406
|
+
if (typeof updateResult.body === 'string') {
|
|
3407
|
+
return updateResult.body.slice(0, 500)
|
|
3408
|
+
}
|
|
3409
|
+
return JSON.stringify(updateResult.body)?.slice(0, 500) ?? String(updateResult.body)
|
|
3410
|
+
} catch {
|
|
3411
|
+
return String(updateResult.body)
|
|
3412
|
+
}
|
|
3413
|
+
})()
|
|
3414
|
+
throw new Error(
|
|
3415
|
+
`Failed to update issue #${issueNumber} for ${owner}/${repo}: status=${updateResult.status} url=${updateResult.request.url} body=${bodySummary}`
|
|
3416
|
+
)
|
|
3003
3417
|
}
|
|
3004
3418
|
|
|
3005
3419
|
const issue = toProjectGitTaskRecord(updateResult.body as GitIssueLike)
|
|
@@ -3017,6 +3431,63 @@ export async function writeGitTask(
|
|
|
3017
3431
|
}
|
|
3018
3432
|
}
|
|
3019
3433
|
|
|
3434
|
+
export async function createGitIssue(
|
|
3435
|
+
projectName: string,
|
|
3436
|
+
options: ProjectCreateGitIssueOptions,
|
|
3437
|
+
processRoot: string = process.cwd()
|
|
3438
|
+
): Promise<ProjectCreateGitIssueResult> {
|
|
3439
|
+
const { owner, repo } = resolveProjectGitRepository(projectName, processRoot, options)
|
|
3440
|
+
const title = typeof options.title === 'string' ? options.title.trim() : ''
|
|
3441
|
+
if (!title) {
|
|
3442
|
+
throw new Error('Issue title is required.')
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
const payload: Record<string, unknown> = { title }
|
|
3446
|
+
if (typeof options.body === 'string') payload.body = options.body
|
|
3447
|
+
if (options.labels) payload.labels = options.labels
|
|
3448
|
+
|
|
3449
|
+
const git = createGitServiceApi({
|
|
3450
|
+
config: {
|
|
3451
|
+
platform: 'GITEA',
|
|
3452
|
+
giteaHost: resolveGiteaHost(),
|
|
3453
|
+
giteaToken: process.env.GITEA_TOKEN,
|
|
3454
|
+
},
|
|
3455
|
+
defaultOwner: owner,
|
|
3456
|
+
defaultRepo: repo,
|
|
3457
|
+
})
|
|
3458
|
+
|
|
3459
|
+
const issueCreate = resolveIssueApiMethod(git, 'create')
|
|
3460
|
+
const createResult = await issueCreate(owner, repo, { data: payload })
|
|
3461
|
+
if (!createResult.ok) {
|
|
3462
|
+
const bodySummary = (() => {
|
|
3463
|
+
try {
|
|
3464
|
+
if (typeof createResult.body === 'string') {
|
|
3465
|
+
return createResult.body.slice(0, 500)
|
|
3466
|
+
}
|
|
3467
|
+
return JSON.stringify(createResult.body)?.slice(0, 500) ?? String(createResult.body)
|
|
3468
|
+
} catch {
|
|
3469
|
+
return String(createResult.body)
|
|
3470
|
+
}
|
|
3471
|
+
})()
|
|
3472
|
+
throw new Error(
|
|
3473
|
+
`Failed to create issue in ${owner}/${repo}: status=${createResult.status} url=${createResult.request.url} body=${bodySummary}`
|
|
3474
|
+
)
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
const issue = toProjectGitIssueRecord(createResult.body as GitIssueLike)
|
|
3478
|
+
if (!issue) {
|
|
3479
|
+
throw new Error(`Create issue response invalid for ${owner}/${repo}`)
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
return {
|
|
3483
|
+
projectName,
|
|
3484
|
+
owner,
|
|
3485
|
+
repo,
|
|
3486
|
+
issueNumber: issue.number,
|
|
3487
|
+
issue,
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3020
3491
|
export async function syncTasks(
|
|
3021
3492
|
projectName: string,
|
|
3022
3493
|
options: Partial<ProjectSyncTaskOptions> = {},
|
|
@@ -3036,8 +3507,11 @@ export async function syncTasks(
|
|
|
3036
3507
|
dryRun: options.dryRun ?? false,
|
|
3037
3508
|
max: options.max,
|
|
3038
3509
|
maxBodyLength: options.maxBodyLength,
|
|
3039
|
-
requestTimeoutMs: options.requestTimeoutMs,
|
|
3040
|
-
throttleMs: options.throttleMs,
|
|
3510
|
+
requestTimeoutMs: options.requestTimeoutMs ?? 60_000,
|
|
3511
|
+
throttleMs: options.throttleMs ?? 200,
|
|
3512
|
+
retryMax: options.retryMax ?? 5,
|
|
3513
|
+
retryBaseDelayMs: options.retryBaseDelayMs ?? 500,
|
|
3514
|
+
retryMaxDelayMs: options.retryMaxDelayMs ?? 10_000,
|
|
3041
3515
|
skipDependencies: options.skipDependencies,
|
|
3042
3516
|
owner,
|
|
3043
3517
|
repo,
|
|
@@ -3058,7 +3532,9 @@ export async function syncTasks(
|
|
|
3058
3532
|
log?.(`Resolved tracks file: ${tracksPath}`)
|
|
3059
3533
|
log?.(`Using issue destination: ${owner}/${repo}`)
|
|
3060
3534
|
log?.(`HTTP request timeout: ${syncOptions.requestTimeoutMs ?? 'default'}`)
|
|
3535
|
+
log?.(`HTTP transport: ${(process.env.EXAMPLE_GIT_HTTP_TRANSPORT ?? '').trim() || '(auto)'}`)
|
|
3061
3536
|
log?.(`HTTP throttle: ${syncOptions.throttleMs ?? 0}ms`)
|
|
3537
|
+
log?.(`HTTP retry: max=${syncOptions.retryMax ?? 0} baseDelayMs=${syncOptions.retryBaseDelayMs ?? 0} maxDelayMs=${syncOptions.retryMaxDelayMs ?? 0}`)
|
|
3062
3538
|
log?.(`Dependency sync: ${syncOptions.skipDependencies ? 'skipped' : 'enabled'}`)
|
|
3063
3539
|
const markdown = await fs.readFile(tracksPath, 'utf8')
|
|
3064
3540
|
log?.(`Loaded tracks content (${markdown.length} chars).`)
|
|
@@ -3089,8 +3565,10 @@ export async function syncTasks(
|
|
|
3089
3565
|
const issueList = resolveIssueApiMethod(git, 'list')
|
|
3090
3566
|
const issueCreate = resolveIssueApiMethod(git, 'create')
|
|
3091
3567
|
const issueEdit = resolveIssueApiMethod(git, 'edit')
|
|
3568
|
+
const issueView = resolveIssueApiMethod(git, 'view')
|
|
3092
3569
|
|
|
3093
|
-
const
|
|
3570
|
+
const retry = resolveNetworkRetryOptions(syncOptions)
|
|
3571
|
+
const allIssues = await fetchAllIssues(issueList, owner, repo, 'all', projectName, log, { retry, throttleMs: syncOptions.throttleMs ?? 0 })
|
|
3094
3572
|
const existing = indexExistingTaskIssues(allIssues, projectName)
|
|
3095
3573
|
const indexedIssueCount = [...existing.values()].reduce((acc, bucket) => acc + bucket.length, 0)
|
|
3096
3574
|
log?.(`Loaded ${indexedIssueCount} task-like issues in ${owner}/${repo}.`)
|
|
@@ -3106,9 +3584,6 @@ export async function syncTasks(
|
|
|
3106
3584
|
unchanged: 0,
|
|
3107
3585
|
}
|
|
3108
3586
|
|
|
3109
|
-
const sleep = (ms: number) =>
|
|
3110
|
-
new Promise<void>((resolve) => setTimeout(resolve, ms))
|
|
3111
|
-
|
|
3112
3587
|
for (let i = 0; i < selected.length; i += 1) {
|
|
3113
3588
|
const task = selected[i]
|
|
3114
3589
|
const marker = `[${i + 1}/${selected.length}] ${task.id}`
|
|
@@ -3130,17 +3605,55 @@ export async function syncTasks(
|
|
|
3130
3605
|
log?.(`${marker} updating #${targetIssue.number}`)
|
|
3131
3606
|
if (!syncOptions.dryRun) {
|
|
3132
3607
|
log?.(`${marker} sending update request...`)
|
|
3133
|
-
const updatedResult = await
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3608
|
+
const updatedResult = await withRetry(
|
|
3609
|
+
`${marker} issue.edit #${targetIssue.number}`,
|
|
3610
|
+
() => issueEdit(owner, repo, targetIssue.number, {
|
|
3611
|
+
data: {
|
|
3612
|
+
title: payload.title,
|
|
3613
|
+
body: payload.body,
|
|
3614
|
+
labels: payload.labels,
|
|
3615
|
+
task_id: payload.task_id,
|
|
3616
|
+
task_dependencies: payload.task_dependencies,
|
|
3617
|
+
},
|
|
3618
|
+
}) as Promise<GitServiceApiExecutionResult>,
|
|
3619
|
+
retry,
|
|
3620
|
+
log,
|
|
3621
|
+
(result) => {
|
|
3622
|
+
if (result.ok) return null
|
|
3623
|
+
return retry.retryOnStatuses.has(result.status) ? `status=${result.status}` : null
|
|
3140
3624
|
},
|
|
3141
|
-
|
|
3625
|
+
)
|
|
3142
3626
|
|
|
3143
3627
|
if (!updatedResult.ok) {
|
|
3628
|
+
const isTimeoutOrRetryable = retry.retryOnStatuses.has(updatedResult.status)
|
|
3629
|
+
if (isTimeoutOrRetryable) {
|
|
3630
|
+
try {
|
|
3631
|
+
const viewResult = await withRetry(
|
|
3632
|
+
`${marker} issue.view #${targetIssue.number}`,
|
|
3633
|
+
() => issueView(owner, repo, targetIssue.number) as Promise<GitServiceApiExecutionResult<GitIssueLike>>,
|
|
3634
|
+
retry,
|
|
3635
|
+
log,
|
|
3636
|
+
(result) => {
|
|
3637
|
+
if (result.ok) return null
|
|
3638
|
+
return retry.retryOnStatuses.has(result.status) ? `status=${result.status}` : null
|
|
3639
|
+
},
|
|
3640
|
+
)
|
|
3641
|
+
|
|
3642
|
+
if (viewResult.ok) {
|
|
3643
|
+
const record = toProjectGitTaskRecord(viewResult.body as GitIssueLike)
|
|
3644
|
+
const expectedHash = computeTaskHash(task)
|
|
3645
|
+
const depsMatch = areDependencyListsEqual(task.deps, record?.taskDependenciesFromPayload ?? [])
|
|
3646
|
+
const signatureMatch = record?.taskSignature?.toLowerCase() === expectedHash.toLowerCase()
|
|
3647
|
+
if (signatureMatch && depsMatch) {
|
|
3648
|
+
log?.(`${marker} update likely succeeded (verified after retryable failure)`)
|
|
3649
|
+
continue
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
} catch {
|
|
3653
|
+
// best effort
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3144
3657
|
throw new Error(
|
|
3145
3658
|
`Failed to update issue ${targetIssue.number} for ${owner}/${repo}: status=${updatedResult.status} url=${updatedResult.request.url}`
|
|
3146
3659
|
)
|
|
@@ -3155,45 +3668,25 @@ export async function syncTasks(
|
|
|
3155
3668
|
|
|
3156
3669
|
log?.(`${marker} creating new task issue`)
|
|
3157
3670
|
if (!syncOptions.dryRun) {
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
try {
|
|
3178
|
-
if (typeof createdResult.body === 'string') {
|
|
3179
|
-
return createdResult.body.slice(0, 500)
|
|
3180
|
-
}
|
|
3181
|
-
return JSON.stringify(createdResult.body)?.slice(0, 500) ?? String(createdResult.body)
|
|
3182
|
-
} catch {
|
|
3183
|
-
return String(createdResult.body)
|
|
3184
|
-
}
|
|
3185
|
-
})()
|
|
3186
|
-
throw new Error(
|
|
3187
|
-
`Failed to create issue for ${task.id} in ${owner}/${repo}: status=${createdResult.status} url=${createdResult.request.url} body=${bodySummary}`
|
|
3188
|
-
)
|
|
3189
|
-
}
|
|
3190
|
-
|
|
3191
|
-
const issueNumber = buildIssueNumberFromPayload(createdResult.body)
|
|
3192
|
-
if (issueNumber === null) {
|
|
3193
|
-
throw new Error(
|
|
3194
|
-
`Created issue response for ${task.id} in ${owner}/${repo} is invalid: ${JSON.stringify(createdResult.body)}`
|
|
3195
|
-
)
|
|
3196
|
-
}
|
|
3671
|
+
log?.(`${marker} sending create request...`)
|
|
3672
|
+
const issueNumber = await createTaskIssueNumberWithRetry(
|
|
3673
|
+
issueCreate,
|
|
3674
|
+
issueList,
|
|
3675
|
+
owner,
|
|
3676
|
+
repo,
|
|
3677
|
+
task.id,
|
|
3678
|
+
{
|
|
3679
|
+
title: payload.title,
|
|
3680
|
+
body: payload.body,
|
|
3681
|
+
labels: payload.labels,
|
|
3682
|
+
task_id: payload.task_id,
|
|
3683
|
+
task_dependencies: payload.task_dependencies,
|
|
3684
|
+
},
|
|
3685
|
+
projectName,
|
|
3686
|
+
retry,
|
|
3687
|
+
syncOptions.throttleMs ?? 0,
|
|
3688
|
+
log,
|
|
3689
|
+
)
|
|
3197
3690
|
issueNumbersByTask.set(task.id, issueNumber)
|
|
3198
3691
|
|
|
3199
3692
|
log?.(`${marker} created #${issueNumber}`)
|
|
@@ -3246,15 +3739,20 @@ export async function syncTasks(
|
|
|
3246
3739
|
.filter((value): value is number => value !== undefined && value > 0)
|
|
3247
3740
|
|
|
3248
3741
|
log?.(`Issue #${issueNumber}: syncing dependencies (${desiredDependencyIssues.length} desired)...`)
|
|
3249
|
-
const syncResult = await
|
|
3250
|
-
owner
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3742
|
+
const syncResult = await withRetry(
|
|
3743
|
+
`issue.dependencies ${owner}/${repo} #${issueNumber}`,
|
|
3744
|
+
() => syncIssueDependenciesViaApi(
|
|
3745
|
+
owner,
|
|
3746
|
+
repo,
|
|
3747
|
+
issueNumber,
|
|
3748
|
+
desiredDependencyIssues,
|
|
3749
|
+
host,
|
|
3750
|
+
token,
|
|
3751
|
+
syncOptions.dryRun,
|
|
3752
|
+
log
|
|
3753
|
+
),
|
|
3754
|
+
retry,
|
|
3755
|
+
log,
|
|
3258
3756
|
)
|
|
3259
3757
|
dependencyStats.added += syncResult.added
|
|
3260
3758
|
dependencyStats.removed += syncResult.removed
|
|
@@ -3420,7 +3918,12 @@ export async function main(argv: string[], processRoot: string = process.cwd()):
|
|
|
3420
3918
|
throw new Error('Missing required arguments: <project-name>.')
|
|
3421
3919
|
}
|
|
3422
3920
|
|
|
3423
|
-
if (
|
|
3921
|
+
if (
|
|
3922
|
+
maybeTarget === 'sync-tasks' ||
|
|
3923
|
+
maybeTarget === '--sync-tasks' ||
|
|
3924
|
+
maybeTarget === 'sync-issues' ||
|
|
3925
|
+
maybeTarget === '--sync-issues'
|
|
3926
|
+
) {
|
|
3424
3927
|
const parsedSync = parseSyncTasksArgs(rest)
|
|
3425
3928
|
if (parsedSync.unknownFlags.length > 0) {
|
|
3426
3929
|
throw new Error(`Unknown flags: ${parsedSync.unknownFlags.join(', ')}`)
|
|
@@ -3432,7 +3935,7 @@ export async function main(argv: string[], processRoot: string = process.cwd()):
|
|
|
3432
3935
|
return `${CLI_NAME}: synced ${result.totalTasks} tasks for ${result.projectName}${dryRun} from ${path.relative(processRoot, result.tracksPath)}${truncated} (created ${result.created}, updated ${result.updated}, skipped ${result.skipped})`
|
|
3433
3936
|
}
|
|
3434
3937
|
|
|
3435
|
-
if (maybeTarget === 'clear-issues') {
|
|
3938
|
+
if (maybeTarget === 'clear-issues' || maybeTarget === '--clear-issues') {
|
|
3436
3939
|
const parsedClear = parseClearIssuesArgs(rest)
|
|
3437
3940
|
if (parsedClear.unknownFlags.length > 0) {
|
|
3438
3941
|
throw new Error(`Unknown flags: ${parsedClear.unknownFlags.join(', ')}`)
|