@gh-symphony/cli 0.0.17 → 0.0.19
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/README.md +105 -9
- package/dist/{chunk-EFMFGOWM.js → chunk-6CI3UUMH.js} +282 -57
- package/dist/chunk-C7G7RJ4G.js +146 -0
- package/dist/{chunk-MHIWAIVD.js → chunk-GKENCODJ.js} +141 -53
- package/dist/{project-557FE2GD.js → chunk-H2YXSYOZ.js} +108 -92
- package/dist/{chunk-TF3QNWNC.js → chunk-M3IFVLQS.js} +246 -212
- package/dist/{chunk-IWR4UQEJ.js → chunk-RN2PACNV.js} +350 -523
- package/dist/chunk-TILHWBP6.js +638 -0
- package/dist/{chunk-6HBZC3BE.js → chunk-XN5ABWZ6.js} +23 -5
- package/dist/{chunk-76QPITKI.js → chunk-Y6TYJMNT.js} +1 -1
- package/dist/{config-cmd-AZ7POMAA.js → config-cmd-DNXNL26Z.js} +3 -1
- package/dist/doctor-IYHCFXOZ.js +1126 -0
- package/dist/index.js +157 -19
- package/dist/init-KZT6YNOH.js +33 -0
- package/dist/{logs-6LNGT2GF.js → logs-6JKKYDGJ.js} +1 -1
- package/dist/project-DNALEWO3.js +22 -0
- package/dist/{recover-LVBI2TGH.js → recover-C3V2QAUB.js} +3 -3
- package/dist/repo-HDDE7OUI.js +321 -0
- package/dist/{run-WITYAYFZ.js → run-XI2S5Y4V.js} +3 -3
- package/dist/setup-K4CYYJBF.js +431 -0
- package/dist/{start-JUFKNL3N.js → start-M6IQGRFO.js} +5 -5
- package/dist/{status-3WK5BWRZ.js → status-QSCFVGRQ.js} +2 -2
- package/dist/{stop-AA3AP5M6.js → stop-7MFCBQVW.js} +2 -2
- package/dist/upgrade-F4VE4XBS.js +165 -0
- package/dist/{version-YVM2A25J.js → version-Y5RYNWMF.js} +1 -1
- package/dist/worker-entry.js +39 -11
- package/dist/workflow-TBIFY5MO.js +497 -0
- package/package.json +4 -4
- package/dist/chunk-JO3AXHQI.js +0 -130
- package/dist/chunk-TH5QPO3Y.js +0 -67
- package/dist/init-EZXQAXZM.js +0 -17
- package/dist/repo-R3XBIVAX.js +0 -121
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
loadGlobalConfig,
|
|
4
|
+
loadProjectConfig
|
|
5
|
+
} from "./chunk-ROGRTUFI.js";
|
|
6
|
+
|
|
7
|
+
// src/project-selection.ts
|
|
8
|
+
import * as p from "@clack/prompts";
|
|
9
|
+
function isInteractiveTerminal() {
|
|
10
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
11
|
+
}
|
|
12
|
+
function explicitProjectRequiredMessage() {
|
|
13
|
+
return "Multiple projects are configured. Re-run with --project-id in non-interactive environments.\n";
|
|
14
|
+
}
|
|
15
|
+
async function inspectManagedProjectSelection(input) {
|
|
16
|
+
if (input.requestedProjectId) {
|
|
17
|
+
const projectConfig = await loadProjectConfig(
|
|
18
|
+
input.configDir,
|
|
19
|
+
input.requestedProjectId
|
|
20
|
+
);
|
|
21
|
+
if (!projectConfig) {
|
|
22
|
+
return {
|
|
23
|
+
kind: "requested_project_missing",
|
|
24
|
+
projectId: input.requestedProjectId,
|
|
25
|
+
message: `Project "${input.requestedProjectId}" is not configured. Run 'gh-symphony project add' or choose an existing project.`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
kind: "resolved",
|
|
30
|
+
projectId: input.requestedProjectId,
|
|
31
|
+
projectConfig
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const global = await loadGlobalConfig(input.configDir);
|
|
35
|
+
if (!global) {
|
|
36
|
+
return {
|
|
37
|
+
kind: "missing_global_config",
|
|
38
|
+
message: "No CLI configuration found. Run 'gh-symphony project add' first."
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const projectIds = global.projects ?? [];
|
|
42
|
+
if (projectIds.length === 0) {
|
|
43
|
+
return {
|
|
44
|
+
kind: "no_projects",
|
|
45
|
+
message: "No managed projects are configured. Run 'gh-symphony project add' first."
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (projectIds.length > 1 && !isInteractiveTerminal()) {
|
|
49
|
+
return {
|
|
50
|
+
kind: "multiple_projects_require_selection",
|
|
51
|
+
message: explicitProjectRequiredMessage().trimEnd()
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (global.activeProject) {
|
|
55
|
+
const projectConfig = await loadProjectConfig(
|
|
56
|
+
input.configDir,
|
|
57
|
+
global.activeProject
|
|
58
|
+
);
|
|
59
|
+
if (!projectConfig) {
|
|
60
|
+
return {
|
|
61
|
+
kind: "active_project_missing",
|
|
62
|
+
projectId: global.activeProject,
|
|
63
|
+
message: `Active project "${global.activeProject}" is configured in config.json but its project config is missing. Re-run 'gh-symphony project add' or 'gh-symphony project switch'.`
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
kind: "resolved",
|
|
68
|
+
projectId: global.activeProject,
|
|
69
|
+
projectConfig
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (projectIds.length === 1) {
|
|
73
|
+
const projectId = projectIds[0];
|
|
74
|
+
const projectConfig = await loadProjectConfig(input.configDir, projectId);
|
|
75
|
+
if (!projectConfig) {
|
|
76
|
+
return {
|
|
77
|
+
kind: "configured_project_missing",
|
|
78
|
+
projectId,
|
|
79
|
+
message: `Configured project "${projectId}" is missing its project config file. Re-run 'gh-symphony project add'.`
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
kind: "resolved",
|
|
84
|
+
projectId,
|
|
85
|
+
projectConfig
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
kind: "multiple_projects_require_selection",
|
|
90
|
+
message: "Multiple projects are configured and no active project is set. Run 'gh-symphony project switch' or re-run with --project-id."
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function resolveManagedProjectConfig(input) {
|
|
94
|
+
if (input.requestedProjectId) {
|
|
95
|
+
return loadProjectConfig(input.configDir, input.requestedProjectId);
|
|
96
|
+
}
|
|
97
|
+
const global = await loadGlobalConfig(input.configDir);
|
|
98
|
+
const projectIds = global?.projects ?? [];
|
|
99
|
+
if (projectIds.length === 0) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (projectIds.length === 1) {
|
|
103
|
+
return loadProjectConfig(input.configDir, projectIds[0]);
|
|
104
|
+
}
|
|
105
|
+
if (!isInteractiveTerminal()) {
|
|
106
|
+
process.stderr.write(explicitProjectRequiredMessage());
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const projects = await Promise.all(
|
|
111
|
+
projectIds.map(async (projectId) => ({
|
|
112
|
+
projectId,
|
|
113
|
+
config: await loadProjectConfig(input.configDir, projectId)
|
|
114
|
+
}))
|
|
115
|
+
);
|
|
116
|
+
const selected = await p.select({
|
|
117
|
+
message: "Select a project:",
|
|
118
|
+
options: projects.map(({ projectId, config }) => ({
|
|
119
|
+
value: projectId,
|
|
120
|
+
label: config?.displayName ?? config?.slug ?? projectId,
|
|
121
|
+
hint: projectId === global?.activeProject ? "current" : config && config.displayName && config.displayName !== projectId ? projectId : void 0
|
|
122
|
+
})),
|
|
123
|
+
maxItems: 10
|
|
124
|
+
});
|
|
125
|
+
if (p.isCancel(selected)) {
|
|
126
|
+
p.cancel("Cancelled.");
|
|
127
|
+
process.exitCode = 130;
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return loadProjectConfig(input.configDir, selected);
|
|
131
|
+
}
|
|
132
|
+
function handleMissingManagedProjectConfig() {
|
|
133
|
+
if (process.exitCode) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
process.stderr.write(
|
|
137
|
+
"No project configured. Run 'gh-symphony project add' first.\n"
|
|
138
|
+
);
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export {
|
|
143
|
+
inspectManagedProjectSelection,
|
|
144
|
+
resolveManagedProjectConfig,
|
|
145
|
+
handleMissingManagedProjectConfig
|
|
146
|
+
};
|
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
getGhToken
|
|
4
|
-
} from "./chunk-JO3AXHQI.js";
|
|
5
|
-
import {
|
|
6
|
-
bold,
|
|
7
|
-
cyan,
|
|
8
|
-
dim,
|
|
9
|
-
green,
|
|
10
|
-
red,
|
|
11
|
-
setNoColor,
|
|
12
|
-
yellow
|
|
13
|
-
} from "./chunk-MVRF7BES.js";
|
|
14
2
|
import {
|
|
15
3
|
OrchestratorService,
|
|
16
4
|
acquireProjectLock,
|
|
17
5
|
createStore,
|
|
18
6
|
releaseProjectLock,
|
|
19
7
|
resolveOrchestratorLogLevel
|
|
20
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-6CI3UUMH.js";
|
|
21
9
|
import {
|
|
22
10
|
deriveIssueWorkspaceKeyFromIdentifier,
|
|
23
11
|
isFileMissing,
|
|
@@ -26,14 +14,26 @@ import {
|
|
|
26
14
|
parseRecentEvents,
|
|
27
15
|
readJsonFile,
|
|
28
16
|
safeReadDir
|
|
29
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-M3IFVLQS.js";
|
|
18
|
+
import {
|
|
19
|
+
getGhToken
|
|
20
|
+
} from "./chunk-TILHWBP6.js";
|
|
21
|
+
import {
|
|
22
|
+
bold,
|
|
23
|
+
cyan,
|
|
24
|
+
dim,
|
|
25
|
+
green,
|
|
26
|
+
red,
|
|
27
|
+
setNoColor,
|
|
28
|
+
yellow
|
|
29
|
+
} from "./chunk-MVRF7BES.js";
|
|
30
30
|
import {
|
|
31
31
|
resolveRuntimeRoot
|
|
32
32
|
} from "./chunk-5NV3LSAJ.js";
|
|
33
33
|
import {
|
|
34
34
|
handleMissingManagedProjectConfig,
|
|
35
35
|
resolveManagedProjectConfig
|
|
36
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-C7G7RJ4G.js";
|
|
37
37
|
import {
|
|
38
38
|
daemonPidPath,
|
|
39
39
|
httpStatusPath,
|
|
@@ -91,7 +91,8 @@ var DashboardFsReader = class {
|
|
|
91
91
|
if (issues) {
|
|
92
92
|
return issues.map((issue) => ({
|
|
93
93
|
...issue,
|
|
94
|
-
completedOnce: issue.completedOnce ?? false
|
|
94
|
+
completedOnce: issue.completedOnce ?? false,
|
|
95
|
+
failureRetryCount: issue.failureRetryCount ?? 0
|
|
95
96
|
}));
|
|
96
97
|
}
|
|
97
98
|
const legacyLeases = await readJsonFile(join(this.projectDir(), "leases.json")) ?? [];
|
|
@@ -100,6 +101,7 @@ var DashboardFsReader = class {
|
|
|
100
101
|
identifier: lease.issueIdentifier,
|
|
101
102
|
workspaceKey: deriveIssueWorkspaceKeyFromIdentifier(lease.issueIdentifier),
|
|
102
103
|
completedOnce: false,
|
|
104
|
+
failureRetryCount: 0,
|
|
103
105
|
state: lease.status === "active" ? "claimed" : "released",
|
|
104
106
|
currentRunId: lease.status === "active" ? lease.runId : null,
|
|
105
107
|
retryEntry: null,
|
|
@@ -114,6 +116,24 @@ var DashboardFsReader = class {
|
|
|
114
116
|
const runs = await mapWithConcurrency(runIds, RUN_RECORD_LOAD_CONCURRENCY, (runId) => this.loadRun(runId));
|
|
115
117
|
return runs.filter((run) => Boolean(run));
|
|
116
118
|
}
|
|
119
|
+
async loadRunsForIssue(issueId, issueIdentifier) {
|
|
120
|
+
const runIds = await safeReadDir(join(this.projectDir(), "runs"));
|
|
121
|
+
const runs = await mapWithConcurrency(runIds, RUN_RECORD_LOAD_CONCURRENCY, async (runId) => {
|
|
122
|
+
try {
|
|
123
|
+
const run = await this.loadRun(runId);
|
|
124
|
+
if (!run) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return run.issueId === issueId || run.issueIdentifier === issueIdentifier ? run : null;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (isFileMissing(error)) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
return runs.filter((run) => Boolean(run));
|
|
136
|
+
}
|
|
117
137
|
async loadRecentRunEvents(runId, limit = DEFAULT_RECENT_EVENT_LIMIT) {
|
|
118
138
|
if (limit <= 0) {
|
|
119
139
|
return [];
|
|
@@ -170,64 +190,81 @@ async function statusForIssue(reader, issueIdentifier) {
|
|
|
170
190
|
return null;
|
|
171
191
|
}
|
|
172
192
|
const currentRunCandidate = issueRecord.currentRunId ? await reader.loadRun(issueRecord.currentRunId) : null;
|
|
173
|
-
const currentRun = isMatchingIssueRun(currentRunCandidate, reader.projectId, issueRecord.issueId, issueIdentifier) ? currentRunCandidate :
|
|
174
|
-
const
|
|
193
|
+
const currentRun = isMatchingIssueRun(currentRunCandidate, reader.projectId, issueRecord.issueId, issueIdentifier) ? currentRunCandidate : null;
|
|
194
|
+
const issueRuns = currentRun === null ? await reader.loadRunsForIssue(issueRecord.issueId, issueIdentifier) : currentRun.tokenUsage ? await reader.loadRunsForIssue(issueRecord.issueId, issueIdentifier) : null;
|
|
195
|
+
const resolvedRun = currentRun ?? findLatestRunForIssue(issueRuns ?? []);
|
|
196
|
+
const recentEvents = resolvedRun === null ? [] : await reader.loadRecentRunEvents(resolvedRun.runId);
|
|
197
|
+
const cumulativeTokens = aggregateIssueTokenUsage(issueRuns ?? []);
|
|
175
198
|
const latestEventMessage = recentEvents[recentEvents.length - 1]?.message ?? null;
|
|
176
|
-
const currentAttempt =
|
|
199
|
+
const currentAttempt = resolvedRun?.attempt ?? issueRecord.retryEntry?.attempt ?? 0;
|
|
177
200
|
return {
|
|
178
201
|
issue_identifier: issueRecord.identifier,
|
|
179
202
|
issue_id: issueRecord.issueId,
|
|
180
|
-
status:
|
|
203
|
+
status: resolvedRun?.status ?? mapIssueOrchestrationStateToStatus(issueRecord.state),
|
|
181
204
|
workspace: {
|
|
182
|
-
path:
|
|
205
|
+
path: resolvedRun?.workingDirectory ?? null
|
|
183
206
|
},
|
|
184
207
|
attempts: {
|
|
185
208
|
restart_count: Math.max(0, currentAttempt - 1),
|
|
186
209
|
current_retry_attempt: currentAttempt
|
|
187
210
|
},
|
|
188
|
-
running:
|
|
189
|
-
session_id:
|
|
190
|
-
turn_count:
|
|
191
|
-
state:
|
|
192
|
-
started_at:
|
|
193
|
-
last_event:
|
|
211
|
+
running: resolvedRun === null ? null : {
|
|
212
|
+
session_id: resolvedRun.runtimeSession?.sessionId ?? null,
|
|
213
|
+
turn_count: resolvedRun.turnCount ?? null,
|
|
214
|
+
state: resolvedRun.issueState ?? null,
|
|
215
|
+
started_at: resolvedRun.startedAt ?? null,
|
|
216
|
+
last_event: resolvedRun.lastEvent ?? null,
|
|
194
217
|
last_message: latestEventMessage,
|
|
195
|
-
last_event_at:
|
|
196
|
-
tokens:
|
|
197
|
-
input_tokens:
|
|
198
|
-
output_tokens:
|
|
199
|
-
total_tokens:
|
|
218
|
+
last_event_at: resolvedRun.lastEventAt ?? null,
|
|
219
|
+
tokens: resolvedRun.tokenUsage ? {
|
|
220
|
+
input_tokens: resolvedRun.tokenUsage.inputTokens,
|
|
221
|
+
output_tokens: resolvedRun.tokenUsage.outputTokens,
|
|
222
|
+
total_tokens: resolvedRun.tokenUsage.totalTokens,
|
|
223
|
+
cumulative_input_tokens: cumulativeTokens.inputTokens,
|
|
224
|
+
cumulative_output_tokens: cumulativeTokens.outputTokens,
|
|
225
|
+
cumulative_total_tokens: cumulativeTokens.totalTokens
|
|
200
226
|
} : null
|
|
201
227
|
},
|
|
202
|
-
retry:
|
|
203
|
-
due_at:
|
|
204
|
-
kind:
|
|
205
|
-
error:
|
|
228
|
+
retry: resolvedRun?.nextRetryAt ?? issueRecord.retryEntry?.dueAt ? {
|
|
229
|
+
due_at: resolvedRun?.nextRetryAt ?? issueRecord.retryEntry?.dueAt ?? "",
|
|
230
|
+
kind: resolvedRun?.retryKind ?? null,
|
|
231
|
+
error: resolvedRun?.lastError ?? issueRecord.retryEntry?.error ?? null
|
|
206
232
|
} : null,
|
|
207
233
|
logs: {
|
|
208
|
-
codex_session_logs:
|
|
234
|
+
codex_session_logs: resolvedRun === null ? [] : [
|
|
209
235
|
{
|
|
210
236
|
label: "worker",
|
|
211
|
-
path: join(reader.runDir(
|
|
237
|
+
path: join(reader.runDir(resolvedRun.runId), "worker.log"),
|
|
212
238
|
url: null
|
|
213
239
|
}
|
|
214
240
|
]
|
|
215
241
|
},
|
|
216
242
|
recent_events: recentEvents,
|
|
217
|
-
last_error:
|
|
243
|
+
last_error: resolvedRun?.lastError ?? issueRecord.retryEntry?.error ?? null,
|
|
218
244
|
tracked: {
|
|
219
245
|
issue_orchestration_state: issueRecord.state,
|
|
220
246
|
current_run_id: issueRecord.currentRunId,
|
|
221
247
|
workspace_key: issueRecord.workspaceKey,
|
|
222
248
|
completed_once: issueRecord.completedOnce,
|
|
223
|
-
run_phase:
|
|
224
|
-
execution_phase:
|
|
249
|
+
run_phase: resolvedRun?.runPhase ?? null,
|
|
250
|
+
execution_phase: resolvedRun?.executionPhase ?? null
|
|
225
251
|
}
|
|
226
252
|
};
|
|
227
253
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
254
|
+
function aggregateIssueTokenUsage(runs) {
|
|
255
|
+
return runs.reduce((total, run) => ({
|
|
256
|
+
inputTokens: total.inputTokens + (run.tokenUsage?.inputTokens ?? 0),
|
|
257
|
+
outputTokens: total.outputTokens + (run.tokenUsage?.outputTokens ?? 0),
|
|
258
|
+
totalTokens: total.totalTokens + (run.tokenUsage?.totalTokens ?? 0)
|
|
259
|
+
}), {
|
|
260
|
+
inputTokens: 0,
|
|
261
|
+
outputTokens: 0,
|
|
262
|
+
totalTokens: 0
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
function findLatestRunForIssue(matchingRuns) {
|
|
266
|
+
const sortedRuns = [...matchingRuns].sort((left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime());
|
|
267
|
+
return sortedRuns[0] ?? null;
|
|
231
268
|
}
|
|
232
269
|
function assertValidDashboardProjectId(projectId) {
|
|
233
270
|
if (projectId.length === 0 || projectId === "." || projectId === ".." || projectId.includes("/") || projectId.includes("\\")) {
|
|
@@ -350,7 +387,8 @@ var DEFAULT_HTTP_PORT = 4680;
|
|
|
350
387
|
var HTTP_HOST = "0.0.0.0";
|
|
351
388
|
function parseStartArgs(args) {
|
|
352
389
|
const parsed = {
|
|
353
|
-
daemon: false
|
|
390
|
+
daemon: false,
|
|
391
|
+
once: false
|
|
354
392
|
};
|
|
355
393
|
for (let i = 0; i < args.length; i += 1) {
|
|
356
394
|
const arg = args[i];
|
|
@@ -358,6 +396,10 @@ function parseStartArgs(args) {
|
|
|
358
396
|
parsed.daemon = true;
|
|
359
397
|
continue;
|
|
360
398
|
}
|
|
399
|
+
if (arg === "--once") {
|
|
400
|
+
parsed.once = true;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
361
403
|
if (arg === "--http") {
|
|
362
404
|
const value = args[i + 1];
|
|
363
405
|
if (!value || value.startsWith("-")) {
|
|
@@ -612,11 +654,16 @@ var handler = async (args, options) => {
|
|
|
612
654
|
process.stderr.write(`${parsed.error}
|
|
613
655
|
`);
|
|
614
656
|
process.stderr.write(
|
|
615
|
-
"Usage: gh-symphony start --project-id <project-id> [--daemon] [--http [port]]\n"
|
|
657
|
+
"Usage: gh-symphony start --project-id <project-id> [--daemon] [--once] [--http [port]]\n"
|
|
616
658
|
);
|
|
617
659
|
process.exitCode = 2;
|
|
618
660
|
return;
|
|
619
661
|
}
|
|
662
|
+
if (parsed.daemon && parsed.once) {
|
|
663
|
+
process.stderr.write("Options '--daemon' and '--once' cannot be used together\n");
|
|
664
|
+
process.exitCode = 2;
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
620
667
|
const projectConfig = await resolveManagedProjectConfig({
|
|
621
668
|
configDir: options.configDir,
|
|
622
669
|
requestedProjectId: parsed.projectId
|
|
@@ -724,14 +771,20 @@ var handler = async (args, options) => {
|
|
|
724
771
|
`HTTP dashboard listening on ${httpServer.url}`
|
|
725
772
|
);
|
|
726
773
|
}
|
|
727
|
-
logLine(
|
|
774
|
+
logLine(
|
|
775
|
+
dim("\xB7"),
|
|
776
|
+
dim(parsed.once ? "Running one orchestration tick" : "Press Ctrl+C to stop")
|
|
777
|
+
);
|
|
728
778
|
let shuttingDown = false;
|
|
729
779
|
let shutdownPromise = null;
|
|
780
|
+
let keepHttpAliveResolve = null;
|
|
730
781
|
const shutdown = async () => {
|
|
731
782
|
if (shuttingDown) {
|
|
732
783
|
return shutdownPromise;
|
|
733
784
|
}
|
|
734
785
|
shuttingDown = true;
|
|
786
|
+
keepHttpAliveResolve?.();
|
|
787
|
+
keepHttpAliveResolve = null;
|
|
735
788
|
const heldLock = projectLock;
|
|
736
789
|
projectLock = null;
|
|
737
790
|
shutdownPromise = shutdownForegroundOrchestrator({
|
|
@@ -743,16 +796,31 @@ var handler = async (args, options) => {
|
|
|
743
796
|
});
|
|
744
797
|
return shutdownPromise;
|
|
745
798
|
};
|
|
746
|
-
|
|
799
|
+
const handleSigint = () => {
|
|
747
800
|
void shutdown();
|
|
748
|
-
}
|
|
749
|
-
|
|
801
|
+
};
|
|
802
|
+
const handleSigterm = () => {
|
|
750
803
|
void shutdown();
|
|
751
|
-
}
|
|
804
|
+
};
|
|
805
|
+
process.on("SIGINT", handleSigint);
|
|
806
|
+
process.on("SIGTERM", handleSigterm);
|
|
752
807
|
try {
|
|
753
808
|
while (!shuttingDown) {
|
|
754
809
|
try {
|
|
755
|
-
await service.run();
|
|
810
|
+
await service.run({ once: parsed.once });
|
|
811
|
+
if (parsed.once) {
|
|
812
|
+
if (httpServer) {
|
|
813
|
+
logLine(
|
|
814
|
+
cyan("\u25A1"),
|
|
815
|
+
"One-shot tick completed; HTTP dashboard remains available until Ctrl+C"
|
|
816
|
+
);
|
|
817
|
+
await new Promise((resolve2) => {
|
|
818
|
+
keepHttpAliveResolve = resolve2;
|
|
819
|
+
});
|
|
820
|
+
} else {
|
|
821
|
+
await shutdown();
|
|
822
|
+
}
|
|
823
|
+
}
|
|
756
824
|
break;
|
|
757
825
|
} catch (error) {
|
|
758
826
|
if (shuttingDown) {
|
|
@@ -761,12 +829,32 @@ var handler = async (args, options) => {
|
|
|
761
829
|
logLine(
|
|
762
830
|
red("\u2717"),
|
|
763
831
|
red(
|
|
764
|
-
|
|
832
|
+
`${parsed.once ? "One-shot run failed" : "Run loop error"}: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
765
833
|
)
|
|
766
834
|
);
|
|
835
|
+
if (parsed.once) {
|
|
836
|
+
process.exitCode = 1;
|
|
837
|
+
await closeHttpServer(httpServer?.server).catch((closeError) => {
|
|
838
|
+
logLine(
|
|
839
|
+
yellow("\u26A0"),
|
|
840
|
+
`Failed to stop HTTP server: ${closeError instanceof Error ? closeError.message : "Unknown error"}`
|
|
841
|
+
);
|
|
842
|
+
});
|
|
843
|
+
await removeHttpBindingState(options.configDir, projectId).catch(
|
|
844
|
+
(removeError) => {
|
|
845
|
+
logLine(
|
|
846
|
+
yellow("\u26A0"),
|
|
847
|
+
`Failed to remove HTTP state: ${removeError instanceof Error ? removeError.message : "Unknown error"}`
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
767
853
|
}
|
|
768
854
|
}
|
|
769
855
|
} finally {
|
|
856
|
+
process.off("SIGINT", handleSigint);
|
|
857
|
+
process.off("SIGTERM", handleSigterm);
|
|
770
858
|
if (shutdownPromise) {
|
|
771
859
|
await shutdownPromise;
|
|
772
860
|
}
|