@magnusekdahl/parallix 1.1.0 → 1.2.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/config/integration-pipelines.json +9 -0
- package/config/workflow.config.schema.json +12 -0
- package/docs/adr/0032-mission-refinement-state-and-usage-budget-signals.md +10 -8
- package/docs/adr/0036-mission-sizing-and-dependency-wave-heuristics.md +14 -12
- package/docs/adr/0041-integration-pipeline-gates.md +1 -1
- package/docs/adr/0047-per-mission-change-size-budget.md +161 -0
- package/docs/adr/index.md +1 -0
- package/docs/use-cases.md +44 -0
- package/lib/agents/agents.js +149 -100
- package/lib/agents/claude-telemetry.js +14 -10
- package/lib/agents/claude.js +32 -11
- package/lib/agents/codex-telemetry.js +32 -19
- package/lib/agents/codex.js +36 -11
- package/lib/agents/limit-hit.js +42 -25
- package/lib/agents/mistral-telemetry.js +1 -1
- package/lib/agents/mistral.js +13 -3
- package/lib/agents/opencode-export.js +11 -5
- package/lib/agents/opencode-telemetry.js +75 -90
- package/lib/agents/opencode.js +86 -29
- package/lib/agents/stage-telemetry.js +3 -7
- package/lib/commands/active.js +139 -94
- package/lib/commands/checkpoint.js +3 -1
- package/lib/commands/config.js +5 -3
- package/lib/commands/coverage-gate.js +17 -10
- package/lib/commands/diff.js +11 -5
- package/lib/commands/draft.js +95 -55
- package/lib/commands/handoff.js +253 -65
- package/lib/commands/integrate.js +288 -188
- package/lib/commands/mission-start.js +39 -34
- package/lib/commands/rebase.js +32 -21
- package/lib/commands/repair-handoff.js +21 -18
- package/lib/commands/resolve-conflict.js +5 -2
- package/lib/commands/review.js +1 -0
- package/lib/commands/setup-review.js +1 -0
- package/lib/commands/stats-backfill.js +50 -41
- package/lib/commands/stats.js +653 -204
- package/lib/commands/status.js +33 -28
- package/lib/core/fmt.js +58 -15
- package/lib/core/git.js +7 -5
- package/lib/core/gitignore.js +6 -4
- package/lib/core/mission-utils.js +161 -88
- package/lib/core/nels.js +199 -0
- package/lib/core/persistent-data-migration.js +55 -23
- package/lib/core/product-config.js +44 -23
- package/lib/core/runtime-matrix.js +15 -2
- package/lib/core/spawn-tee.js +40 -13
- package/lib/core/state-map.js +32 -13
- package/lib/core/storage.js +19 -5
- package/lib/core/subagent-limit.js +28 -0
- package/lib/core/verification.js +29 -12
- package/lib/review/rebase.js +11 -3
- package/lib/review/review-adapter.js +93 -6
- package/lib/review/review-artifacts.js +89 -23
- package/lib/review/review-commands.js +139 -45
- package/lib/review/review-events.js +103 -52
- package/lib/review/review-loop.js +117 -87
- package/lib/review/review-polling.js +25 -5
- package/lib/review/review-prompts.js +36 -5
- package/lib/review/review-state.js +39 -15
- package/lib/tools/backlog.js +162 -71
- package/lib/tools/forgejo.js +290 -129
- package/lib/tools/gatekeeper.js +15 -0
- package/lib/tools/redgreen.js +220 -0
- package/lib/tools/sessions.js +12 -5
- package/lib/tools/setup-review.js +170 -70
- package/package.json +11 -3
- package/prompts/draft.md +6 -0
- package/templates/mission-scaffold.md +1 -1
|
@@ -17,25 +17,30 @@ let tokenNameCounter = 0;
|
|
|
17
17
|
const REVIEW_TOKEN_SCOPES = ['write:user', 'write:repository', 'write:issue', 'write:organization'];
|
|
18
18
|
const PASSWORD_PROMPT_OPTIONS = { hidden: false };
|
|
19
19
|
|
|
20
|
+
/** @param {string} repo @returns {{owner: string, repo: string}|null} */
|
|
20
21
|
function parseRepoSlug(repo) {
|
|
21
|
-
if (typeof repo !== 'string') return null;
|
|
22
|
+
if (typeof repo !== 'string') {return null;}
|
|
22
23
|
const match = repo.trim().match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
23
|
-
if (!match) return null;
|
|
24
|
+
if (!match) {return null;}
|
|
24
25
|
return { owner: match[1], repo: match[2] };
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
/** @param {string[]} values @returns {string[]} */
|
|
27
29
|
function unique(values) {
|
|
28
30
|
return [...new Set(values.filter(Boolean))];
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
/** @param {string|undefined} baseUrl @returns {string|undefined} */
|
|
31
34
|
function normalizeBaseUrl(baseUrl) {
|
|
32
35
|
return typeof baseUrl === 'string' ? baseUrl.replace(/\/+$/, '') : baseUrl;
|
|
33
36
|
}
|
|
34
37
|
|
|
38
|
+
/** @param {string} repoName @param {string} user @param {string} [suffix] @returns {string} */
|
|
35
39
|
function buildTokenName(repoName, user, suffix = `${Date.now()}-${++tokenNameCounter}`) {
|
|
36
40
|
return `workflow-${repoName}-${user}-${suffix}`;
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
/** @param {string} user @param {{allowBlank?: boolean}} [options] @returns {string} */
|
|
39
44
|
function passwordPrompt(user, options = {}) {
|
|
40
45
|
const { allowBlank = false } = options;
|
|
41
46
|
return allowBlank
|
|
@@ -43,22 +48,30 @@ function passwordPrompt(user, options = {}) {
|
|
|
43
48
|
: `Password for ${user} (input visible): `;
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
/** @param {string|undefined} value @param {boolean} [defaultValue] @returns {boolean} */
|
|
46
52
|
function parseYesNo(value, defaultValue = true) {
|
|
47
|
-
if (typeof value !== 'string' || value.trim() === '') return defaultValue;
|
|
53
|
+
if (typeof value !== 'string' || value.trim() === '') {return defaultValue;}
|
|
48
54
|
const normalized = value.trim().toLowerCase();
|
|
49
|
-
if (['y', 'yes'].includes(normalized)) return true;
|
|
50
|
-
if (['n', 'no'].includes(normalized)) return false;
|
|
55
|
+
if (['y', 'yes'].includes(normalized)) {return true;}
|
|
56
|
+
if (['n', 'no'].includes(normalized)) {return false;}
|
|
51
57
|
return defaultValue;
|
|
52
58
|
}
|
|
53
59
|
|
|
60
|
+
/** @param {string} [rootDir] @returns {string} */
|
|
54
61
|
function defaultProductName(rootDir = process.cwd()) {
|
|
55
62
|
return path.basename(rootDir) || 'Workflow Project';
|
|
56
63
|
}
|
|
57
64
|
|
|
65
|
+
/** @param {string} [rootDir] @param {string} [ownerLogin] @returns {string} */
|
|
58
66
|
function defaultRepoSlug(rootDir = process.cwd(), ownerLogin = 'human') {
|
|
59
67
|
return `${ownerLogin}/${path.basename(rootDir) || 'project'}`;
|
|
60
68
|
}
|
|
61
69
|
|
|
70
|
+
/**
|
|
71
|
+
* @param {string} rootDir
|
|
72
|
+
* @param {{spawnSyncFn?: Function, configuredPrimaryBranch?: string|null}} [options]
|
|
73
|
+
* @returns {string}
|
|
74
|
+
*/
|
|
62
75
|
function detectPrimaryBranchDefault(rootDir = process.cwd(), options = {}) {
|
|
63
76
|
const { spawnSyncFn = spawnSync, configuredPrimaryBranch = null } = options;
|
|
64
77
|
const listResult = spawnSyncFn(
|
|
@@ -67,7 +80,7 @@ function detectPrimaryBranchDefault(rootDir = process.cwd(), options = {}) {
|
|
|
67
80
|
{ encoding: 'utf8' }
|
|
68
81
|
);
|
|
69
82
|
const branches = listResult.status === 0
|
|
70
|
-
? (listResult.stdout || '').split('\n').map(value => value.trim()).filter(Boolean)
|
|
83
|
+
? (listResult.stdout || '').split('\n').map(/** @param {string} value */ value => value.trim()).filter(Boolean)
|
|
71
84
|
: [];
|
|
72
85
|
|
|
73
86
|
if (configuredPrimaryBranch && branches.includes(configuredPrimaryBranch)) {
|
|
@@ -82,9 +95,9 @@ function detectPrimaryBranchDefault(rootDir = process.cwd(), options = {}) {
|
|
|
82
95
|
return currentBranch;
|
|
83
96
|
}
|
|
84
97
|
|
|
85
|
-
if (branches.includes('main')) return 'main';
|
|
86
|
-
if (branches.includes('master')) return 'master';
|
|
87
|
-
if (configuredPrimaryBranch) return configuredPrimaryBranch;
|
|
98
|
+
if (branches.includes('main')) {return 'main';}
|
|
99
|
+
if (branches.includes('master')) {return 'master';}
|
|
100
|
+
if (configuredPrimaryBranch) {return configuredPrimaryBranch;}
|
|
88
101
|
return 'main';
|
|
89
102
|
}
|
|
90
103
|
|
|
@@ -98,6 +111,7 @@ function standardLayoutDescription() {
|
|
|
98
111
|
].join('; ');
|
|
99
112
|
}
|
|
100
113
|
|
|
114
|
+
/** @param {{reviewProvider?: string, baseUrl?: string, reviewRemote?: string, reviewRepo?: string}} answers @returns {{provider: string, baseUrl?: string, remote?: string, repo?: string}} */
|
|
101
115
|
function buildReviewAdapterConfig(answers) {
|
|
102
116
|
const provider = answers.reviewProvider || 'forgejo';
|
|
103
117
|
if (provider !== 'forgejo') {
|
|
@@ -111,11 +125,13 @@ function buildReviewAdapterConfig(answers) {
|
|
|
111
125
|
};
|
|
112
126
|
}
|
|
113
127
|
|
|
128
|
+
/** @param {{missionsBaseDir?: string, branchPrefix?: string, worktreePattern?: string, primaryBranch?: string, productName?: string, tasksProvider?: string, tasksStorage?: string, verificationCommand?: string, verificationDefaultArea?: string, reviewProvider?: string, baseUrl?: string, reviewRemote?: string, reviewRepo?: string}} answers @returns {{product: {name: string, targetUser: string}, adapters: {tasks: {provider: string, storage: string}, missions: {baseDir: string, branchPrefix: string, worktreePattern: string, primaryBranch?: string}, verification: {command: string, defaultArea: string}, review: {provider: string, baseUrl?: string, remote?: string, repo?: string}, agents: {}}}} */
|
|
114
129
|
function buildWorkflowConfig(answers) {
|
|
130
|
+
/** @type {{baseDir: string, branchPrefix: string, worktreePattern: string, primaryBranch?: string}} */
|
|
115
131
|
const missions = {
|
|
116
|
-
baseDir: answers.missionsBaseDir,
|
|
117
|
-
branchPrefix: answers.branchPrefix,
|
|
118
|
-
worktreePattern: answers.worktreePattern,
|
|
132
|
+
baseDir: /** @type {string} */ (answers.missionsBaseDir),
|
|
133
|
+
branchPrefix: /** @type {string} */ (answers.branchPrefix),
|
|
134
|
+
worktreePattern: /** @type {string} */ (answers.worktreePattern),
|
|
119
135
|
};
|
|
120
136
|
if (
|
|
121
137
|
typeof answers.primaryBranch === 'string' &&
|
|
@@ -127,18 +143,18 @@ function buildWorkflowConfig(answers) {
|
|
|
127
143
|
|
|
128
144
|
return {
|
|
129
145
|
product: {
|
|
130
|
-
name: answers.productName,
|
|
146
|
+
name: /** @type {string} */ (answers.productName),
|
|
131
147
|
targetUser: 'Engineering teams using git, task tracking, and code review',
|
|
132
148
|
},
|
|
133
149
|
adapters: {
|
|
134
150
|
tasks: {
|
|
135
|
-
provider: answers.tasksProvider,
|
|
136
|
-
storage: answers.tasksStorage,
|
|
151
|
+
provider: /** @type {string} */ (answers.tasksProvider),
|
|
152
|
+
storage: /** @type {string} */ (answers.tasksStorage),
|
|
137
153
|
},
|
|
138
154
|
missions,
|
|
139
155
|
verification: {
|
|
140
|
-
command: answers.verificationCommand,
|
|
141
|
-
defaultArea: answers.verificationDefaultArea,
|
|
156
|
+
command: /** @type {string} */ (answers.verificationCommand),
|
|
157
|
+
defaultArea: /** @type {string} */ (answers.verificationDefaultArea),
|
|
142
158
|
},
|
|
143
159
|
review: buildReviewAdapterConfig(answers),
|
|
144
160
|
agents: {},
|
|
@@ -146,12 +162,14 @@ function buildWorkflowConfig(answers) {
|
|
|
146
162
|
};
|
|
147
163
|
}
|
|
148
164
|
|
|
165
|
+
/** @param {string} rootDir @param {{[key: string]: any}} config @returns {string} */
|
|
149
166
|
function writeWorkflowConfig(rootDir, config) {
|
|
150
167
|
const configPath = path.join(rootDir, 'workflow.config.json');
|
|
151
168
|
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
152
169
|
return configPath;
|
|
153
170
|
}
|
|
154
171
|
|
|
172
|
+
/** @returns {string[]} */
|
|
155
173
|
function suggestedForgejoUsers() {
|
|
156
174
|
return unique([
|
|
157
175
|
...eligibleAgentsForStep('active'),
|
|
@@ -159,18 +177,21 @@ function suggestedForgejoUsers() {
|
|
|
159
177
|
]);
|
|
160
178
|
}
|
|
161
179
|
|
|
180
|
+
/** @param {string} user @param {string} [forgejoHome] @returns {string} */
|
|
162
181
|
function tokenFilePath(user, forgejoHome = resolveForgejoHome()) {
|
|
163
182
|
return path.join(forgejoHome, 'tokens', user);
|
|
164
183
|
}
|
|
165
184
|
|
|
185
|
+
/** @param {string} [forgejoHome] @returns {string} */
|
|
166
186
|
function ensureTokenDir(forgejoHome = resolveForgejoHome()) {
|
|
167
187
|
const dir = path.join(forgejoHome, 'tokens');
|
|
168
188
|
fs.mkdirSync(dir, { recursive: true });
|
|
169
189
|
return dir;
|
|
170
190
|
}
|
|
171
191
|
|
|
192
|
+
/** @param {string} [rootDir] @param {string} [explicitForgejoHome] @returns {string} */
|
|
172
193
|
function resolveBootstrapForgejoHome(rootDir = process.cwd(), explicitForgejoHome) {
|
|
173
|
-
if (explicitForgejoHome) return explicitForgejoHome;
|
|
194
|
+
if (explicitForgejoHome) {return explicitForgejoHome;}
|
|
174
195
|
return path.join(rootDir, '.forgejo-local');
|
|
175
196
|
}
|
|
176
197
|
|
|
@@ -178,10 +199,15 @@ function readConfiguredReviewRemote(rootDir = process.cwd(), remoteName = 'revie
|
|
|
178
199
|
const result = spawnSync('git', ['-C', rootDir, 'remote', 'get-url', remoteName], {
|
|
179
200
|
encoding: 'utf8',
|
|
180
201
|
});
|
|
181
|
-
if (result.status !== 0) return null;
|
|
202
|
+
if (result.status !== 0) {return null;}
|
|
182
203
|
return (result.stdout || '').trim() || null;
|
|
183
204
|
}
|
|
184
205
|
|
|
206
|
+
/**
|
|
207
|
+
* @param {string} rootDir
|
|
208
|
+
* @param {{users?: string[], remoteName?: string, remoteUrlFn?: Function, getRemoteUrlFn?: Function, tokenPathFn?: Function, requestFn?: Function}} [options]
|
|
209
|
+
* @returns {{required: boolean, ok: boolean, issues: string[], steps: string[]}}
|
|
210
|
+
*/
|
|
185
211
|
function evaluateReviewSetup(rootDir = process.cwd(), options = {}) {
|
|
186
212
|
const {
|
|
187
213
|
users = suggestedForgejoUsers(),
|
|
@@ -214,7 +240,7 @@ function evaluateReviewSetup(rootDir = process.cwd(), options = {}) {
|
|
|
214
240
|
const configuredRemoteName = remoteName || reviewAdapter.remote || 'review';
|
|
215
241
|
const issues = [];
|
|
216
242
|
const steps = [];
|
|
217
|
-
const missingUsers = users.filter(user => !fs.existsSync(tokenPathFn(user)));
|
|
243
|
+
const missingUsers = users.filter(/** @param {string} user */ user => !fs.existsSync(tokenPathFn(user)));
|
|
218
244
|
|
|
219
245
|
if (missingUsers.length > 0) {
|
|
220
246
|
issues.push(`missing Forgejo tokens for: ${missingUsers.join(', ')}`);
|
|
@@ -247,7 +273,7 @@ function evaluateReviewSetup(rootDir = process.cwd(), options = {}) {
|
|
|
247
273
|
} else if (!probe.ok && probe.statusCode === 404) {
|
|
248
274
|
issues.push(`Forgejo review repo ${review.repo} is missing or inaccessible for ${user}`);
|
|
249
275
|
steps.push('Run `px setup` to recreate the review repo and refresh token/remote wiring.');
|
|
250
|
-
} else if (!probe.ok && probe.statusCode
|
|
276
|
+
} else if (!probe.ok && probe.statusCode === null) {
|
|
251
277
|
issues.push(`Forgejo at ${normalizedBaseUrl} is unreachable while validating ${user}`);
|
|
252
278
|
steps.push('Start Forgejo and rerun `px verify-env` or `px setup-review`.');
|
|
253
279
|
}
|
|
@@ -261,6 +287,12 @@ function evaluateReviewSetup(rootDir = process.cwd(), options = {}) {
|
|
|
261
287
|
return { required: true, ok: false, issues, steps };
|
|
262
288
|
}
|
|
263
289
|
|
|
290
|
+
/**
|
|
291
|
+
* @param {string} method
|
|
292
|
+
* @param {string} requestUrl
|
|
293
|
+
* @param {{basicAuth?: {user: string, password: string}, token?: string, body?: {[key: string]: any}}} [options]
|
|
294
|
+
* @returns {{ok: boolean, statusCode: number|null, data: any, error: string|null}}
|
|
295
|
+
*/
|
|
264
296
|
function apiRequest(method, requestUrl, options = {}) {
|
|
265
297
|
const args = [
|
|
266
298
|
'-s',
|
|
@@ -288,6 +320,7 @@ function apiRequest(method, requestUrl, options = {}) {
|
|
|
288
320
|
return {
|
|
289
321
|
ok: false,
|
|
290
322
|
statusCode: null,
|
|
323
|
+
data: null,
|
|
291
324
|
error: (result.stderr || result.stdout || 'curl failed').trim(),
|
|
292
325
|
};
|
|
293
326
|
}
|
|
@@ -314,6 +347,7 @@ function apiRequest(method, requestUrl, options = {}) {
|
|
|
314
347
|
};
|
|
315
348
|
}
|
|
316
349
|
|
|
350
|
+
/** @param {string} baseUrl @param {string} user @param {string} password @param {string} tokenName @param {Function} [requestFn] @param {string[]} [scopes] @returns {{ok: boolean, token?: string, error?: string, statusCode?: number|null, response?: any}} */
|
|
317
351
|
function createToken(baseUrl, user, password, tokenName, requestFn = apiRequest, scopes = REVIEW_TOKEN_SCOPES) {
|
|
318
352
|
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
319
353
|
const result = requestFn('POST', `${normalizedBaseUrl}/api/v1/users/${encodeURIComponent(user)}/tokens`, {
|
|
@@ -340,6 +374,7 @@ function createToken(baseUrl, user, password, tokenName, requestFn = apiRequest,
|
|
|
340
374
|
return { ok: true, token };
|
|
341
375
|
}
|
|
342
376
|
|
|
377
|
+
/** @param {string} baseUrl @param {string} repoSlug @param {string} ownerLogin @param {string} ownerToken @param {Function} [requestFn] @returns {{ok: boolean, created?: boolean, error?: string, response?: any}} */
|
|
343
378
|
function ensureRepo(baseUrl, repoSlug, ownerLogin, ownerToken, requestFn = apiRequest) {
|
|
344
379
|
const repoInfo = parseRepoSlug(repoSlug);
|
|
345
380
|
if (!repoInfo) {
|
|
@@ -379,6 +414,7 @@ function ensureRepo(baseUrl, repoSlug, ownerLogin, ownerToken, requestFn = apiRe
|
|
|
379
414
|
return { ok: true, created: true };
|
|
380
415
|
}
|
|
381
416
|
|
|
417
|
+
/** @param {string} baseUrl @param {string} repoSlug @param {string} ownerToken @param {string} collaborator @param {string} [permission] @param {Function} [requestFn] @returns {{ok: boolean, created?: boolean, skipped?: boolean, error?: string, response?: any}} */
|
|
382
418
|
function ensureRepoCollaborator(baseUrl, repoSlug, ownerToken, collaborator, permission = 'write', requestFn = apiRequest) {
|
|
383
419
|
const repoInfo = parseRepoSlug(repoSlug);
|
|
384
420
|
if (!repoInfo) {
|
|
@@ -403,13 +439,14 @@ function ensureRepoCollaborator(baseUrl, repoSlug, ownerToken, collaborator, per
|
|
|
403
439
|
return { ok: true, created: true };
|
|
404
440
|
}
|
|
405
441
|
|
|
442
|
+
/** @param {string} baseUrl @param {string} repoSlug @param {string} ownerToken @param {string[]} [collaborators] @param {string} [permission] @param {Function} [requestFn] @returns {{ok: boolean, created: string[]}} */
|
|
406
443
|
function ensureRepoCollaborators(baseUrl, repoSlug, ownerToken, collaborators = [], permission = 'write', requestFn = apiRequest) {
|
|
407
|
-
const uniqueCollaborators = unique(collaborators.map(user => (typeof user === 'string' ? user.trim() : '')).filter(Boolean));
|
|
444
|
+
const uniqueCollaborators = unique(collaborators.map(/** @param {string|boolean} user */ user => (typeof user === 'string' ? user.trim() : '')).filter(Boolean));
|
|
408
445
|
const created = [];
|
|
409
446
|
for (const collaborator of uniqueCollaborators) {
|
|
410
447
|
const result = ensureRepoCollaborator(baseUrl, repoSlug, ownerToken, collaborator, permission, requestFn);
|
|
411
448
|
if (!result.ok) {
|
|
412
|
-
return
|
|
449
|
+
return { ok: false, created: [] };
|
|
413
450
|
}
|
|
414
451
|
if (result.created) {
|
|
415
452
|
created.push(collaborator);
|
|
@@ -418,6 +455,7 @@ function ensureRepoCollaborators(baseUrl, repoSlug, ownerToken, collaborators =
|
|
|
418
455
|
return { ok: true, created };
|
|
419
456
|
}
|
|
420
457
|
|
|
458
|
+
/** @param {string} rootDir @param {string} remoteName @param {string} remoteUrl @returns {{ok: boolean, created?: boolean, updated?: boolean, error?: string}} */
|
|
421
459
|
function ensureReviewRemote(rootDir, remoteName, remoteUrl) {
|
|
422
460
|
const current = readConfiguredReviewRemote(rootDir, remoteName);
|
|
423
461
|
const args = current
|
|
@@ -433,6 +471,11 @@ function ensureReviewRemote(rootDir, remoteName, remoteUrl) {
|
|
|
433
471
|
return { ok: true, created: !current, updated: Boolean(current && current !== remoteUrl) };
|
|
434
472
|
}
|
|
435
473
|
|
|
474
|
+
/**
|
|
475
|
+
* @param {string} prompt
|
|
476
|
+
* @param {{hidden?: boolean, input?: NodeJS.ReadStream, output?: NodeJS.WriteStream}} [options]
|
|
477
|
+
* @returns {Promise<string>}
|
|
478
|
+
*/
|
|
436
479
|
async function promptLine(prompt, options = {}) {
|
|
437
480
|
const { hidden = false, input = process.stdin, output = process.stdout } = options;
|
|
438
481
|
return await new Promise((resolve) => {
|
|
@@ -446,7 +489,7 @@ async function promptLine(prompt, options = {}) {
|
|
|
446
489
|
}
|
|
447
490
|
|
|
448
491
|
const originalWrite = output.write.bind(output);
|
|
449
|
-
const maskChunk = (chunk) => {
|
|
492
|
+
const maskChunk = /** @param {string|Buffer} chunk */ (chunk) => {
|
|
450
493
|
const value = typeof chunk === 'string' ? chunk : String(chunk);
|
|
451
494
|
let masked = '';
|
|
452
495
|
for (const character of value) {
|
|
@@ -464,18 +507,22 @@ async function promptLine(prompt, options = {}) {
|
|
|
464
507
|
}
|
|
465
508
|
return masked;
|
|
466
509
|
};
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
510
|
+
/** @type {import('readline').Interface & {stdoutMuted?: boolean}} */
|
|
511
|
+
const rlAny = rl;
|
|
512
|
+
/** @type {Function} */
|
|
513
|
+
// @ts-expect-error overriding Writable.write with custom signature
|
|
514
|
+
output.write = /** @type {any} */ ((chunk, encoding, cb) => {
|
|
515
|
+
if (rlAny.stdoutMuted) {
|
|
516
|
+
const masked = maskChunk(/** @type {string | Buffer} */ (chunk));
|
|
470
517
|
if (masked) {
|
|
471
|
-
return originalWrite(masked, encoding, cb);
|
|
518
|
+
return /** @type {Function} */ (originalWrite)(masked, encoding, cb);
|
|
472
519
|
}
|
|
473
|
-
if (typeof cb === 'function') cb();
|
|
520
|
+
if (typeof cb === 'function') {cb();}
|
|
474
521
|
return true;
|
|
475
522
|
}
|
|
476
|
-
return originalWrite(chunk, encoding, cb);
|
|
477
|
-
};
|
|
478
|
-
|
|
523
|
+
return /** @type {Function} */ (originalWrite)(chunk, encoding, cb);
|
|
524
|
+
});
|
|
525
|
+
rlAny.stdoutMuted = true;
|
|
479
526
|
originalWrite(prompt);
|
|
480
527
|
rl.question('', answer => {
|
|
481
528
|
output.write = originalWrite;
|
|
@@ -486,10 +533,18 @@ async function promptLine(prompt, options = {}) {
|
|
|
486
533
|
});
|
|
487
534
|
}
|
|
488
535
|
|
|
536
|
+
/** @param {{statusCode?: number}} result @returns {boolean} */
|
|
489
537
|
function isAuthFailure(result) {
|
|
490
538
|
return Boolean(result && (result.statusCode === 401 || result.statusCode === 403));
|
|
491
539
|
}
|
|
492
540
|
|
|
541
|
+
/**
|
|
542
|
+
* @param {{baseUrl: string, repo: string, ownerLogin: string, ownerPassword: string}} setup
|
|
543
|
+
* @param {string} user
|
|
544
|
+
* @param {string} initialPassword
|
|
545
|
+
* @param {{allowBlank?: boolean, log?: Function, promptFn?: Function, maxAttempts?: number, requestFn?: Function, scopes?: string[]}} [options]
|
|
546
|
+
* @returns {Promise<{ok: boolean, token?: string, error?: string, skipped?: boolean, statusCode?: number|null, response?: any}>}
|
|
547
|
+
*/
|
|
493
548
|
async function createTokenWithRetries(setup, user, initialPassword, options = {}) {
|
|
494
549
|
const {
|
|
495
550
|
allowBlank = false,
|
|
@@ -538,8 +593,8 @@ async function createTokenWithRetries(setup, user, initialPassword, options = {}
|
|
|
538
593
|
requestFn,
|
|
539
594
|
scopes
|
|
540
595
|
);
|
|
541
|
-
if (tokenResult.ok) return tokenResult;
|
|
542
|
-
if (!isAuthFailure(tokenResult) || attempt === maxAttempts || typeof promptFn !== 'function') {
|
|
596
|
+
if (tokenResult.ok) {return tokenResult;}
|
|
597
|
+
if (!isAuthFailure(/** @type {{statusCode?: number}} */ (tokenResult)) || attempt === maxAttempts || typeof promptFn !== 'function') {
|
|
543
598
|
return tokenResult;
|
|
544
599
|
}
|
|
545
600
|
|
|
@@ -550,6 +605,11 @@ async function createTokenWithRetries(setup, user, initialPassword, options = {}
|
|
|
550
605
|
return { ok: false, error: `token creation failed for ${user}` };
|
|
551
606
|
}
|
|
552
607
|
|
|
608
|
+
/**
|
|
609
|
+
* @param {string} [rootDir]
|
|
610
|
+
* @param {{promptFn?: Function, log?: Function, users?: string[]}} [options]
|
|
611
|
+
* @returns {Promise<{baseUrl: string, repo: string, ownerLogin: string, ownerPassword: string, agentPasswords: Array<{user: string, password: string}>, agentUsers: string[]}>}
|
|
612
|
+
*/
|
|
553
613
|
async function collectSetupAnswers(rootDir = process.cwd(), options = {}) {
|
|
554
614
|
const {
|
|
555
615
|
promptFn = promptLine,
|
|
@@ -568,7 +628,7 @@ async function collectSetupAnswers(rootDir = process.cwd(), options = {}) {
|
|
|
568
628
|
const ownerLogin = (await promptFn(`Forgejo login with repo-create access [${defaultOwnerLogin}]: `)) || defaultOwnerLogin;
|
|
569
629
|
const ownerPassword = await promptFn(passwordPrompt(ownerLogin), PASSWORD_PROMPT_OPTIONS);
|
|
570
630
|
const userList = (await promptFn(`Agent Forgejo users [${defaultUsers}]: `)) || defaultUsers;
|
|
571
|
-
const agentUsers = unique(userList.split(',').map(value => value.trim()).filter(Boolean));
|
|
631
|
+
const agentUsers = unique(userList.split(',').map(/** @param {string} value */ value => value.trim()).filter(Boolean));
|
|
572
632
|
const agentPasswords = [];
|
|
573
633
|
|
|
574
634
|
for (const user of agentUsers) {
|
|
@@ -588,6 +648,11 @@ async function collectSetupAnswers(rootDir = process.cwd(), options = {}) {
|
|
|
588
648
|
};
|
|
589
649
|
}
|
|
590
650
|
|
|
651
|
+
/**
|
|
652
|
+
* @param {string} [rootDir]
|
|
653
|
+
* @param {{promptFn?: Function, log?: Function, users?: string[], loadWorkflowConfigFn?: Function, detectPrimaryBranchDefaultFn?: Function}} [options]
|
|
654
|
+
* @returns {Promise<{productName: string, ownerLogin: string, baseUrl: string, reviewRepo: string, reviewRemote: string, bootstrapReview: boolean, ownerPassword: string, agentPasswords: Array<{user: string, password: string}>, agentUsers: string[], tasksProvider: string, tasksStorage: string, missionsBaseDir: string, branchPrefix: string, primaryBranch: string, worktreePattern: string, verificationCommand: string, verificationDefaultArea: string}>}
|
|
655
|
+
*/
|
|
591
656
|
async function collectWizardAnswers(rootDir = process.cwd(), options = {}) {
|
|
592
657
|
const {
|
|
593
658
|
promptFn = promptLine,
|
|
@@ -668,7 +733,7 @@ async function collectWizardAnswers(rootDir = process.cwd(), options = {}) {
|
|
|
668
733
|
log(fmt.status('INFO', 'Forgejo bootstrap is enabled. Hidden password prompts are next: first for the repo-creating login, then once per listed agent user.'));
|
|
669
734
|
ownerPassword = await promptFn(passwordPrompt(ownerLogin), PASSWORD_PROMPT_OPTIONS);
|
|
670
735
|
const userList = (await promptFn(`Agent Forgejo users [${defaultUsers}]: `)) || defaultUsers;
|
|
671
|
-
agentUsers = unique(userList.split(',').map(value => value.trim()).filter(Boolean));
|
|
736
|
+
agentUsers = unique(userList.split(',').map(/** @param {string} value */ value => value.trim()).filter(Boolean));
|
|
672
737
|
for (const user of agentUsers) {
|
|
673
738
|
const password = await promptFn(passwordPrompt(user, { allowBlank: true }), PASSWORD_PROMPT_OPTIONS);
|
|
674
739
|
if (password) {
|
|
@@ -677,7 +742,7 @@ async function collectWizardAnswers(rootDir = process.cwd(), options = {}) {
|
|
|
677
742
|
}
|
|
678
743
|
}
|
|
679
744
|
|
|
680
|
-
return {
|
|
745
|
+
return /** @type {{productName: string, ownerLogin: string, baseUrl: string, reviewRepo: string, reviewRemote: string, bootstrapReview: boolean, ownerPassword: string, agentPasswords: Array<{user: string, password: string}>, agentUsers: string[], tasksProvider: string, tasksStorage: string, missionsBaseDir: string, branchPrefix: string, primaryBranch: string, worktreePattern: string, verificationCommand: string, verificationDefaultArea: string}} */ ({
|
|
681
746
|
productName,
|
|
682
747
|
ownerLogin,
|
|
683
748
|
baseUrl,
|
|
@@ -688,9 +753,10 @@ async function collectWizardAnswers(rootDir = process.cwd(), options = {}) {
|
|
|
688
753
|
agentPasswords,
|
|
689
754
|
agentUsers,
|
|
690
755
|
...defaults,
|
|
691
|
-
};
|
|
756
|
+
});
|
|
692
757
|
}
|
|
693
758
|
|
|
759
|
+
/** @param {string} user @param {string} token @param {string} [forgejoHome] @returns {string} */
|
|
694
760
|
function writeToken(user, token, forgejoHome = resolveForgejoHome()) {
|
|
695
761
|
ensureTokenDir(forgejoHome);
|
|
696
762
|
const file = tokenFilePath(user, forgejoHome);
|
|
@@ -706,13 +772,22 @@ function writeToken(user, token, forgejoHome = resolveForgejoHome()) {
|
|
|
706
772
|
* @param {string} baseUrl - Forgejo base URL
|
|
707
773
|
* @param {string} repoSlug - Repository slug for token naming
|
|
708
774
|
* @param {string} ownerToken - Owner's Forgejo PAT
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
775
|
+
* @param {Array<{user: string, password: string}>} agentPasswords - Array of {user, password} to create tokens for
|
|
776
|
+
* @param {{owner: string, repo: string}} repoInfo - Parsed repo slug {owner, repo}
|
|
777
|
+
* @param {{requestFn?: Function, writeTokenFn?: Function, buildTokenNameFn?: Function, forgejoHome?: string}} options - Injected dependencies
|
|
778
|
+
* @returns {{ ok: boolean, createdTokens: Array<{user: string, path: string}>, warnings?: Array<{user: string, error: string, response?: any}>, error?: string }}
|
|
779
|
+
*/
|
|
780
|
+
/**
|
|
781
|
+
* @param {string} baseUrl
|
|
782
|
+
* @param {string} repoSlug
|
|
783
|
+
* @param {string} ownerToken
|
|
784
|
+
* @param {Array<{user: string, password: string}>} agentPasswords
|
|
785
|
+
* @param {{owner: string, repo: string}} repoInfo
|
|
786
|
+
* @param {{requestFn?: Function, writeTokenFn?: Function, buildTokenNameFn?: Function, forgejoHome?: string}} [options]
|
|
787
|
+
* @returns {{ok: boolean, createdTokens: Array<{user: string, path: string}>, warnings?: Array<{user: string, error: string, response?: any}>, error?: string}}
|
|
713
788
|
*/
|
|
714
789
|
function tokenCreateViaOwnerToken(baseUrl, repoSlug, ownerToken, agentPasswords, repoInfo, options = {}) {
|
|
715
|
-
const { requestFn = apiRequest, writeTokenFn
|
|
790
|
+
const { requestFn = apiRequest, writeTokenFn = writeToken, buildTokenNameFn = buildTokenName, forgejoHome: injectedForgejoHome } = options;
|
|
716
791
|
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
717
792
|
const forgejoHome = typeof injectedForgejoHome === 'string' ? injectedForgejoHome : resolveForgejoHome();
|
|
718
793
|
|
|
@@ -757,6 +832,12 @@ function tokenCreateViaOwnerToken(baseUrl, repoSlug, ownerToken, agentPasswords,
|
|
|
757
832
|
return { ok: true, createdTokens, warnings };
|
|
758
833
|
}
|
|
759
834
|
|
|
835
|
+
/**
|
|
836
|
+
* @param {string} rootDir
|
|
837
|
+
* @param {{baseUrl: string, repo: string, ownerLogin: string, ownerPassword: string, agentPasswords: Array<{user: string, password: string}>, agentUsers?: string[]}} setup
|
|
838
|
+
* @param {{log?: Function, promptFn?: Function, requestFn?: Function, maxPasswordAttempts?: number, interactive?: boolean, forgejoHome?: string}} [options]
|
|
839
|
+
* @returns {Promise<{ok: boolean, warnings?: Array<{user: string, error: string, response?: any}>, createdTokens?: Array<{user: string, path: string}>, error?: string, response?: any}>}
|
|
840
|
+
*/
|
|
760
841
|
async function bootstrapReviewSurface(rootDir = process.cwd(), setup, options = {}) {
|
|
761
842
|
const {
|
|
762
843
|
log = fmt.log.info,
|
|
@@ -781,7 +862,7 @@ async function bootstrapReviewSurface(rootDir = process.cwd(), setup, options =
|
|
|
781
862
|
// Original interactive path: prompt for owner password, create tokens for all users
|
|
782
863
|
const ownerTokenResult = await createTokenWithRetries({
|
|
783
864
|
...setup,
|
|
784
|
-
baseUrl,
|
|
865
|
+
baseUrl: /** @type {string} */ (baseUrl),
|
|
785
866
|
}, setup.ownerLogin, setup.ownerPassword, {
|
|
786
867
|
allowBlank: false,
|
|
787
868
|
log,
|
|
@@ -789,24 +870,27 @@ async function bootstrapReviewSurface(rootDir = process.cwd(), setup, options =
|
|
|
789
870
|
maxAttempts: maxPasswordAttempts,
|
|
790
871
|
requestFn,
|
|
791
872
|
});
|
|
792
|
-
if (!ownerTokenResult.ok) return ownerTokenResult;
|
|
873
|
+
if (!ownerTokenResult.ok) {return ownerTokenResult;}
|
|
874
|
+
const ownerToken = /** @type {string} */ (ownerTokenResult.token);
|
|
793
875
|
|
|
794
|
-
const ownerTokenPath = writeToken(setup.ownerLogin,
|
|
795
|
-
|
|
796
|
-
|
|
876
|
+
const ownerTokenPath = writeToken(setup.ownerLogin, ownerToken, forgejoHome);
|
|
877
|
+
// @ts-expect-error repo may be undefined
|
|
878
|
+
const repoResult = ensureRepo(baseUrl, setup.repo, setup.ownerLogin, ownerToken, requestFn);
|
|
879
|
+
if (!repoResult.ok) {return repoResult;}
|
|
797
880
|
|
|
798
|
-
|
|
799
|
-
|
|
881
|
+
// @ts-expect-error repo may be undefined
|
|
882
|
+
const collaboratorResult = ensureRepoCollaborators(baseUrl, setup.repo, ownerToken, collaboratorUsers, 'write', requestFn);
|
|
883
|
+
if (!collaboratorResult.ok) {return collaboratorResult;}
|
|
800
884
|
if (collaboratorResult.created.length > 0) {
|
|
801
|
-
log(fmt.status('PASS', `Granted write access on ${setup.repo} to ${collaboratorResult.created.join(', ')}`));
|
|
885
|
+
log(fmt.status('PASS', `Granted write access on ${setup.repo || ''} to ${collaboratorResult.created.join(', ')}`));
|
|
802
886
|
}
|
|
803
887
|
|
|
804
888
|
const createdTokens = [{ user: setup.ownerLogin, path: ownerTokenPath }];
|
|
805
|
-
const warnings = [];
|
|
889
|
+
const warnings = /** @type {Array<{user: string, error: string, response?: any}>} */ ([]);
|
|
806
890
|
for (const agent of setup.agentPasswords) {
|
|
807
891
|
const tokenResult = await createTokenWithRetries({
|
|
808
892
|
...setup,
|
|
809
|
-
baseUrl,
|
|
893
|
+
baseUrl: /** @type {string} */ (baseUrl),
|
|
810
894
|
}, agent.user, agent.password, {
|
|
811
895
|
allowBlank: true,
|
|
812
896
|
log,
|
|
@@ -817,21 +901,21 @@ async function bootstrapReviewSurface(rootDir = process.cwd(), setup, options =
|
|
|
817
901
|
if (!tokenResult.ok) {
|
|
818
902
|
warnings.push({
|
|
819
903
|
user: agent.user,
|
|
820
|
-
error: tokenResult.error,
|
|
904
|
+
error: /** @type {string} */ (tokenResult.error),
|
|
821
905
|
response: tokenResult.response,
|
|
822
906
|
});
|
|
823
907
|
continue;
|
|
824
908
|
}
|
|
825
|
-
if (tokenResult.skipped) continue;
|
|
826
|
-
createdTokens.push({ user: agent.user, path: writeToken(agent.user, tokenResult.token, forgejoHome) });
|
|
909
|
+
if (tokenResult.skipped) {continue;}
|
|
910
|
+
createdTokens.push({ user: agent.user, path: writeToken(agent.user, /** @type {string} */ (tokenResult.token), forgejoHome) });
|
|
827
911
|
}
|
|
828
912
|
|
|
829
913
|
const remoteUrl = reviewRemoteUrl(rootDir);
|
|
830
914
|
const remoteName = resolveReviewAdapter(rootDir).remote || 'review';
|
|
831
915
|
const remoteResult = remoteUrl ? ensureReviewRemote(rootDir, remoteName, remoteUrl) : { ok: true, created: false, updated: false };
|
|
832
|
-
if (!remoteResult.ok) return remoteResult;
|
|
916
|
+
if (!remoteResult.ok) {return remoteResult;}
|
|
833
917
|
|
|
834
|
-
|
|
918
|
+
log(fmt.status('PASS', `Forgejo repo ${setup.repo} ${repoResult.created ? 'created' : 'already exists'}.`));
|
|
835
919
|
for (const tokenInfo of createdTokens) {
|
|
836
920
|
log(fmt.status('PASS', `Wrote Forgejo token for ${tokenInfo.user} to ${tokenInfo.path}`));
|
|
837
921
|
}
|
|
@@ -856,22 +940,25 @@ async function bootstrapReviewSurface(rootDir = process.cwd(), setup, options =
|
|
|
856
940
|
ownerToken = fs.readFileSync(ownerTokenPath, 'utf8').trim();
|
|
857
941
|
}
|
|
858
942
|
if (!ownerToken) {
|
|
859
|
-
return { ok: false, error: `No owner token found for ${setup.ownerLogin} at ${ownerTokenPath}. A token file for an existing user (typically human) is required to bootstrap new agent tokens.` };
|
|
943
|
+
return { ok: false, error: `No owner token found for ${/** @type {string} */ (setup.ownerLogin)} at ${ownerTokenPath}. A token file for an existing user (typically human) is required to bootstrap new agent tokens.` };
|
|
860
944
|
}
|
|
861
945
|
|
|
946
|
+
// @ts-expect-error repo may be undefined
|
|
862
947
|
const repoResult = ensureRepo(baseUrl, setup.repo, setup.ownerLogin, ownerToken, requestFn);
|
|
863
|
-
if (!repoResult.ok) return repoResult;
|
|
948
|
+
if (!repoResult.ok) {return repoResult;}
|
|
864
949
|
|
|
950
|
+
// @ts-expect-error repo may be undefined
|
|
865
951
|
const collaboratorResult = ensureRepoCollaborators(baseUrl, setup.repo, ownerToken, collaboratorUsers, 'write', requestFn);
|
|
866
|
-
if (!collaboratorResult.ok) return collaboratorResult;
|
|
952
|
+
if (!collaboratorResult.ok) {return collaboratorResult;}
|
|
867
953
|
if (collaboratorResult.created.length > 0) {
|
|
868
954
|
log(fmt.status('PASS', `Granted write access on ${setup.repo} to ${collaboratorResult.created.join(', ')}`));
|
|
869
955
|
}
|
|
870
956
|
|
|
957
|
+
// @ts-expect-error repo may be undefined
|
|
871
958
|
const tokensResult = tokenCreateViaOwnerToken(baseUrl, setup.repo, ownerToken, setup.agentPasswords, repoInfo, { requestFn, writeTokenFn: writeToken, forgejoHome, log });
|
|
872
|
-
if (!tokensResult.ok) return tokensResult;
|
|
959
|
+
if (!tokensResult.ok) {return tokensResult;}
|
|
873
960
|
|
|
874
|
-
const { createdTokens, warnings } = tokensResult;
|
|
961
|
+
const { createdTokens, warnings = [] } = /** @type {{createdTokens: Array<{user: string, path: string}>, warnings: Array<{user: string, error: string, response?: any}>}} */ (tokensResult);
|
|
875
962
|
for (const tokenInfo of createdTokens) {
|
|
876
963
|
log(fmt.status('PASS', `Wrote Forgejo token for ${tokenInfo.user} to ${tokenInfo.path}`));
|
|
877
964
|
}
|
|
@@ -885,14 +972,19 @@ async function bootstrapReviewSurface(rootDir = process.cwd(), setup, options =
|
|
|
885
972
|
const remoteUrl = reviewRemoteUrl(rootDir);
|
|
886
973
|
const remoteName = resolveReviewAdapter(rootDir).remote || 'review';
|
|
887
974
|
const remoteResult = remoteUrl ? ensureReviewRemote(rootDir, remoteName, remoteUrl) : { ok: true, created: false, updated: false };
|
|
888
|
-
if (!remoteResult.ok) return remoteResult;
|
|
975
|
+
if (!remoteResult.ok) {return remoteResult;}
|
|
889
976
|
|
|
890
|
-
log(fmt.status('PASS', `Forgejo repo ${setup.repo} ${repoResult.created ? 'created' : 'already exists'}.`));
|
|
977
|
+
log(fmt.status('PASS', `Forgejo repo ${/** @type {string} */ (setup.repo)} ${repoResult.created ? 'created' : 'already exists'}.`));
|
|
891
978
|
log(fmt.status('PASS', `Review remote "${remoteName}" now points at ${remoteUrl}`));
|
|
892
979
|
|
|
893
980
|
return { ok: true, warnings, createdTokens };
|
|
894
981
|
}
|
|
895
982
|
|
|
983
|
+
/**
|
|
984
|
+
* @param {any[]} _args
|
|
985
|
+
* @param {{rootDir?: string, log?: Function, error?: Function, exit?: Function, promptFn?: Function, forgejoHome?: string}} [options]
|
|
986
|
+
* @returns {Promise<void>}
|
|
987
|
+
*/
|
|
896
988
|
async function setupReview(_args, options = {}) {
|
|
897
989
|
const rootDir = options.rootDir || process.cwd();
|
|
898
990
|
const log = options.log || fmt.log.plain;
|
|
@@ -905,7 +997,7 @@ async function setupReview(_args, options = {}) {
|
|
|
905
997
|
forgejoHome: resolveBootstrapForgejoHome(rootDir, options.forgejoHome),
|
|
906
998
|
});
|
|
907
999
|
if (!result.ok) {
|
|
908
|
-
error(fmt.status('FAIL', result.error));
|
|
1000
|
+
error(fmt.status('FAIL', /** @type {string} */ (result.error || '')));
|
|
909
1001
|
if (result.response) {
|
|
910
1002
|
error(fmt.status('INFO', `Forgejo response: ${JSON.stringify(result.response)}`));
|
|
911
1003
|
}
|
|
@@ -914,8 +1006,9 @@ async function setupReview(_args, options = {}) {
|
|
|
914
1006
|
}
|
|
915
1007
|
}
|
|
916
1008
|
|
|
1009
|
+
/** @param {string} rootDir @param {{verifyFn?: Function}} [options] @returns {Promise<{status: number}>} */
|
|
917
1010
|
async function runVerifyEnv(rootDir, options = {}) {
|
|
918
|
-
const verifyFn = options.verifyFn || ((cwd) => spawnSync('node', ['workflow', 'verify-env'], {
|
|
1011
|
+
const verifyFn = options.verifyFn || /** @param {string} cwd @returns {{status: number}} */ ((/** @type {string} */ cwd) => spawnSync('node', ['workflow', 'verify-env'], {
|
|
919
1012
|
cwd,
|
|
920
1013
|
stdio: 'inherit',
|
|
921
1014
|
encoding: 'utf8',
|
|
@@ -923,6 +1016,7 @@ async function runVerifyEnv(rootDir, options = {}) {
|
|
|
923
1016
|
return verifyFn(rootDir);
|
|
924
1017
|
}
|
|
925
1018
|
|
|
1019
|
+
/** @param {string} rootDir @param {{log?: Function}} [options] @returns {{productName: string, reviewProvider: string, tasksProvider: string, tasksStorage: string, missionsBaseDir: string, branchPrefix: string, primaryBranch: string, worktreePattern: string, verificationCommand: string, verificationDefaultArea: string, bootstrapReview: boolean, ownerLogin?: string, baseUrl?: string, reviewRepo?: string, reviewRemote?: string, ownerPassword?: string, agentPasswords?: Array<{user: string, password: string}>, agentUsers?: string[]}} */
|
|
926
1020
|
function buildNonInteractiveAnswers(rootDir, options = {}) {
|
|
927
1021
|
const log = options.log || fmt.log.plain;
|
|
928
1022
|
const env = process.env;
|
|
@@ -970,6 +1064,11 @@ function buildNonInteractiveAnswers(rootDir, options = {}) {
|
|
|
970
1064
|
return answers;
|
|
971
1065
|
}
|
|
972
1066
|
|
|
1067
|
+
/**
|
|
1068
|
+
* @param {any[]} _args
|
|
1069
|
+
* @param {{rootDir?: string, log?: Function, error?: Function, exit?: Function, promptFn?: Function, forgejoHome?: string}} [options]
|
|
1070
|
+
* @returns {Promise<void>}
|
|
1071
|
+
*/
|
|
973
1072
|
async function setupWizard(_args, options = {}) {
|
|
974
1073
|
const rootDir = options.rootDir || process.cwd();
|
|
975
1074
|
const log = options.log || fmt.log.plain;
|
|
@@ -979,8 +1078,9 @@ async function setupWizard(_args, options = {}) {
|
|
|
979
1078
|
const forgejoHome = resolveBootstrapForgejoHome(rootDir, options.forgejoHome);
|
|
980
1079
|
|
|
981
1080
|
const nonInteractive = (_args && _args.includes('--non-interactive')) || process.env.WORKFLOW_SETUP_NON_INTERACTIVE === '1';
|
|
1081
|
+
/** @type {{productName: string, ownerLogin: string, baseUrl: string, reviewRepo: string, reviewRemote: string, bootstrapReview: boolean, ownerPassword: string, agentPasswords: Array<{user: string, password: string}>, agentUsers: string[], tasksProvider: string, tasksStorage: string, missionsBaseDir: string, branchPrefix: string, primaryBranch: string, worktreePattern: string, verificationCommand: string, verificationDefaultArea: string}} */
|
|
982
1082
|
const answers = nonInteractive
|
|
983
|
-
? buildNonInteractiveAnswers(rootDir, { log })
|
|
1083
|
+
? /** @type {{productName: string, ownerLogin: string, baseUrl: string, reviewRepo: string, reviewRemote: string, bootstrapReview: boolean, ownerPassword: string, agentPasswords: Array<{user: string, password: string}>, agentUsers: string[], tasksProvider: string, tasksStorage: string, missionsBaseDir: string, branchPrefix: string, primaryBranch: string, worktreePattern: string, verificationCommand: string, verificationDefaultArea: string}} */ (buildNonInteractiveAnswers(rootDir, { log }))
|
|
984
1084
|
: await collectWizardAnswers(rootDir, { ...options, promptFn, log });
|
|
985
1085
|
const configPath = writeWorkflowConfig(rootDir, buildWorkflowConfig(answers));
|
|
986
1086
|
log(fmt.status('PASS', `Wrote ${configPath}`));
|
|
@@ -1009,7 +1109,7 @@ async function setupWizard(_args, options = {}) {
|
|
|
1009
1109
|
forgejoHome,
|
|
1010
1110
|
});
|
|
1011
1111
|
if (!result.ok) {
|
|
1012
|
-
error(fmt.status('FAIL', result.error));
|
|
1112
|
+
error(fmt.status('FAIL', /** @type {string} */ (result.error || '')));
|
|
1013
1113
|
if (result.response) {
|
|
1014
1114
|
error(fmt.status('INFO', `Forgejo response: ${JSON.stringify(result.response)}`));
|
|
1015
1115
|
}
|
|
@@ -1020,7 +1120,7 @@ async function setupWizard(_args, options = {}) {
|
|
|
1020
1120
|
log(fmt.status('INFO', 'Skipped Forgejo bootstrap. You can run `px setup-review` later if needed.'));
|
|
1021
1121
|
}
|
|
1022
1122
|
|
|
1023
|
-
const verifyResult = await runVerifyEnv(rootDir, options);
|
|
1123
|
+
const verifyResult = await runVerifyEnv(rootDir, /** @type {{verifyFn?: Function}} */ (options));
|
|
1024
1124
|
if (verifyResult && typeof verifyResult.status === 'number' && verifyResult.status !== 0) {
|
|
1025
1125
|
exit(verifyResult.status);
|
|
1026
1126
|
}
|