@gh-symphony/cli 0.0.1
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/LICENSE +21 -0
- package/dist/commands/config-cmd.d.ts +3 -0
- package/dist/commands/config-cmd.js +106 -0
- package/dist/commands/help.d.ts +3 -0
- package/dist/commands/help.js +47 -0
- package/dist/commands/init.d.ts +26 -0
- package/dist/commands/init.js +508 -0
- package/dist/commands/logs.d.ts +3 -0
- package/dist/commands/logs.js +123 -0
- package/dist/commands/project.d.ts +3 -0
- package/dist/commands/project.js +101 -0
- package/dist/commands/recover.d.ts +3 -0
- package/dist/commands/recover.js +117 -0
- package/dist/commands/repo.d.ts +3 -0
- package/dist/commands/repo.js +103 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.js +69 -0
- package/dist/commands/start.d.ts +3 -0
- package/dist/commands/start.js +210 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.js +218 -0
- package/dist/commands/stop.d.ts +3 -0
- package/dist/commands/stop.js +46 -0
- package/dist/commands/version.d.ts +3 -0
- package/dist/commands/version.js +21 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.js +81 -0
- package/dist/github/client.d.ts +60 -0
- package/dist/github/client.js +300 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +88 -0
- package/dist/mapping/smart-defaults.d.ts +33 -0
- package/dist/mapping/smart-defaults.js +159 -0
- package/dist/orchestrator-runtime.d.ts +5 -0
- package/dist/orchestrator-runtime.js +26 -0
- package/package.json +49 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { resolveRuntimeRoot, resolveWorkspaceConfig, syncWorkspaceToRuntime, } from "../orchestrator-runtime.js";
|
|
4
|
+
// ANSI color helpers
|
|
5
|
+
function bold(s) {
|
|
6
|
+
return `\x1b[1m${s}\x1b[0m`;
|
|
7
|
+
}
|
|
8
|
+
function dim(s) {
|
|
9
|
+
return `\x1b[2m${s}\x1b[0m`;
|
|
10
|
+
}
|
|
11
|
+
function green(s) {
|
|
12
|
+
return `\x1b[32m${s}\x1b[0m`;
|
|
13
|
+
}
|
|
14
|
+
function red(s) {
|
|
15
|
+
return `\x1b[31m${s}\x1b[0m`;
|
|
16
|
+
}
|
|
17
|
+
function yellow(s) {
|
|
18
|
+
return `\x1b[33m${s}\x1b[0m`;
|
|
19
|
+
}
|
|
20
|
+
function cyan(s) {
|
|
21
|
+
return `\x1b[36m${s}\x1b[0m`;
|
|
22
|
+
}
|
|
23
|
+
function white(s) {
|
|
24
|
+
return `\x1b[37m${s}\x1b[0m`;
|
|
25
|
+
}
|
|
26
|
+
function stripAnsi(s) {
|
|
27
|
+
// eslint-disable-next-line no-control-regex
|
|
28
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
29
|
+
}
|
|
30
|
+
function healthIcon(health) {
|
|
31
|
+
switch (health) {
|
|
32
|
+
case "idle":
|
|
33
|
+
case "running":
|
|
34
|
+
return green("●");
|
|
35
|
+
case "degraded":
|
|
36
|
+
return red("●");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function relativeTime(isoString) {
|
|
40
|
+
const now = new Date();
|
|
41
|
+
const then = new Date(isoString);
|
|
42
|
+
const diffMs = now.getTime() - then.getTime();
|
|
43
|
+
const diffS = Math.floor(diffMs / 1000);
|
|
44
|
+
const diffM = Math.floor(diffS / 60);
|
|
45
|
+
const diffH = Math.floor(diffM / 60);
|
|
46
|
+
if (diffS < 60)
|
|
47
|
+
return `${diffS}s ago`;
|
|
48
|
+
if (diffM < 60)
|
|
49
|
+
return `${diffM}m ago`;
|
|
50
|
+
return `${diffH}h ago`;
|
|
51
|
+
}
|
|
52
|
+
function truncate(s, len) {
|
|
53
|
+
if (s.length <= len)
|
|
54
|
+
return s;
|
|
55
|
+
return s.slice(0, len - 3) + "...";
|
|
56
|
+
}
|
|
57
|
+
function renderDashboard(snapshot, noColor) {
|
|
58
|
+
const apply = noColor ? (s) => stripAnsi(s) : (s) => s;
|
|
59
|
+
const lines = [];
|
|
60
|
+
// Header
|
|
61
|
+
const headerTitle = `gh-symphony ∙ ${snapshot.slug}`;
|
|
62
|
+
const headerWidth = 45;
|
|
63
|
+
const headerPadding = Math.max(0, headerWidth - stripAnsi(headerTitle).length);
|
|
64
|
+
lines.push("╭" + "─".repeat(headerWidth) + "╮");
|
|
65
|
+
lines.push("│ " + apply(bold(headerTitle)) + " ".repeat(headerPadding) + "│");
|
|
66
|
+
lines.push("╰" + "─".repeat(headerWidth) + "╯");
|
|
67
|
+
lines.push("");
|
|
68
|
+
// Health and last tick
|
|
69
|
+
const healthStr = apply(`${healthIcon(snapshot.health)} Health ${snapshot.health}`);
|
|
70
|
+
const lastTickStr = apply(`Last tick ${relativeTime(snapshot.lastTickAt)}`);
|
|
71
|
+
lines.push(` ${healthStr}${" ".repeat(Math.max(0, 30 - stripAnsi(healthStr).length))}${lastTickStr}`);
|
|
72
|
+
lines.push("");
|
|
73
|
+
// Summary stats
|
|
74
|
+
const dispatchedStr = apply(`Dispatched ${snapshot.summary.dispatched}`);
|
|
75
|
+
const activeRunsStr = apply(`Active Runs ${snapshot.summary.activeRuns}`);
|
|
76
|
+
const suppressedStr = apply(`Suppressed ${snapshot.summary.suppressed}`);
|
|
77
|
+
const recoveredStr = apply(`Recovered ${snapshot.summary.recovered}`);
|
|
78
|
+
lines.push(` ${dispatchedStr}${" ".repeat(Math.max(0, 20 - stripAnsi(dispatchedStr).length))}${activeRunsStr}`);
|
|
79
|
+
lines.push(` ${suppressedStr}${" ".repeat(Math.max(0, 20 - stripAnsi(suppressedStr).length))}${recoveredStr}`);
|
|
80
|
+
lines.push("");
|
|
81
|
+
// Active runs table
|
|
82
|
+
if (snapshot.activeRuns.length > 0) {
|
|
83
|
+
lines.push(" Active Runs:");
|
|
84
|
+
for (const run of snapshot.activeRuns) {
|
|
85
|
+
const runIdDisplay = truncate(run.runId, 12);
|
|
86
|
+
const phaseColor = run.phase === "planning"
|
|
87
|
+
? cyan
|
|
88
|
+
: run.phase === "human-review"
|
|
89
|
+
? yellow
|
|
90
|
+
: run.phase === "implementation"
|
|
91
|
+
? cyan
|
|
92
|
+
: run.phase === "awaiting-merge"
|
|
93
|
+
? yellow
|
|
94
|
+
: white;
|
|
95
|
+
const phaseStr = apply(phaseColor(run.phase));
|
|
96
|
+
const statusColor = run.status === "running"
|
|
97
|
+
? green
|
|
98
|
+
: run.status === "failed"
|
|
99
|
+
? red
|
|
100
|
+
: run.status === "succeeded"
|
|
101
|
+
? green
|
|
102
|
+
: dim;
|
|
103
|
+
const statusStr = apply(statusColor(run.status));
|
|
104
|
+
lines.push(` ${runIdDisplay} ${run.issueIdentifier} ${phaseStr} ${statusStr}`);
|
|
105
|
+
}
|
|
106
|
+
lines.push("");
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
lines.push(" No active runs.");
|
|
110
|
+
lines.push("");
|
|
111
|
+
}
|
|
112
|
+
// Retry queue
|
|
113
|
+
if (snapshot.retryQueue.length > 0) {
|
|
114
|
+
lines.push(" Retry Queue:");
|
|
115
|
+
for (const retry of snapshot.retryQueue) {
|
|
116
|
+
const runIdDisplay = truncate(retry.runId, 12);
|
|
117
|
+
const nextRetryDisplay = retry.nextRetryAt
|
|
118
|
+
? relativeTime(retry.nextRetryAt)
|
|
119
|
+
: "pending";
|
|
120
|
+
lines.push(` ${runIdDisplay} ${retry.issueIdentifier} ${apply(yellow(retry.retryKind))} ${nextRetryDisplay}`);
|
|
121
|
+
}
|
|
122
|
+
lines.push("");
|
|
123
|
+
}
|
|
124
|
+
// Last error
|
|
125
|
+
if (snapshot.lastError) {
|
|
126
|
+
lines.push(apply(red(` ✗ ${snapshot.lastError}`)));
|
|
127
|
+
lines.push("");
|
|
128
|
+
}
|
|
129
|
+
// Token usage
|
|
130
|
+
if (snapshot.codexTotals) {
|
|
131
|
+
const tokenStr = apply(`Tokens: ${snapshot.codexTotals.inputTokens} in / ${snapshot.codexTotals.outputTokens} out / ${snapshot.codexTotals.totalTokens} total`);
|
|
132
|
+
lines.push(` ${tokenStr}`);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
lines.push(" Tokens: 0 in / 0 out / 0 total");
|
|
136
|
+
}
|
|
137
|
+
return lines.join("\n");
|
|
138
|
+
}
|
|
139
|
+
function parseStatusArgs(args) {
|
|
140
|
+
const parsed = { watch: false };
|
|
141
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
142
|
+
const arg = args[i];
|
|
143
|
+
if (arg === "--watch" || arg === "-w") {
|
|
144
|
+
parsed.watch = true;
|
|
145
|
+
}
|
|
146
|
+
if (arg === "--workspace" || arg === "--workspace-id") {
|
|
147
|
+
parsed.workspaceId = args[i + 1];
|
|
148
|
+
i += 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return parsed;
|
|
152
|
+
}
|
|
153
|
+
async function readStatusSnapshot(runtimeRoot, workspaceId) {
|
|
154
|
+
try {
|
|
155
|
+
const statusPath = join(runtimeRoot, "orchestrator", "workspaces", workspaceId, "status.json");
|
|
156
|
+
const content = await readFile(statusPath, "utf-8");
|
|
157
|
+
return JSON.parse(content);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const handler = async (args, options) => {
|
|
164
|
+
const parsed = parseStatusArgs(args);
|
|
165
|
+
const wsConfig = await resolveWorkspaceConfig(options.configDir, parsed.workspaceId);
|
|
166
|
+
if (!wsConfig) {
|
|
167
|
+
process.stderr.write("No workspace configured. Run 'gh-symphony init' first.\n");
|
|
168
|
+
process.exitCode = 1;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
|
172
|
+
const workspaceId = wsConfig.workspaceId;
|
|
173
|
+
await syncWorkspaceToRuntime(options.configDir, wsConfig);
|
|
174
|
+
if (parsed.watch) {
|
|
175
|
+
// Watch mode: poll every 2 seconds
|
|
176
|
+
const clear = () => process.stdout.write("\x1b[2J\x1b[H");
|
|
177
|
+
const run = async () => {
|
|
178
|
+
clear();
|
|
179
|
+
const snapshot = await readStatusSnapshot(runtimeRoot, workspaceId);
|
|
180
|
+
if (snapshot) {
|
|
181
|
+
if (options.json) {
|
|
182
|
+
process.stdout.write(JSON.stringify(snapshot, null, 2) + "\n");
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
process.stdout.write(renderDashboard(snapshot, options.noColor) + "\n");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
process.stdout.write("Unable to read status snapshot.\n");
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
await run();
|
|
193
|
+
const interval = setInterval(() => void run(), 2000);
|
|
194
|
+
const shutdown = () => {
|
|
195
|
+
clearInterval(interval);
|
|
196
|
+
process.exit(0);
|
|
197
|
+
};
|
|
198
|
+
process.on("SIGINT", shutdown);
|
|
199
|
+
process.on("SIGTERM", shutdown);
|
|
200
|
+
// Keep alive
|
|
201
|
+
await new Promise(() => { });
|
|
202
|
+
}
|
|
203
|
+
// Single status query
|
|
204
|
+
const snapshot = await readStatusSnapshot(runtimeRoot, workspaceId);
|
|
205
|
+
if (snapshot) {
|
|
206
|
+
if (options.json) {
|
|
207
|
+
process.stdout.write(JSON.stringify(snapshot, null, 2) + "\n");
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
process.stdout.write(renderDashboard(snapshot, options.noColor) + "\n");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
process.stderr.write("Unable to read status snapshot.\n");
|
|
215
|
+
process.exitCode = 1;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
export default handler;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { daemonPidPath } from "../config.js";
|
|
3
|
+
function parseStopArgs(args) {
|
|
4
|
+
return { force: args.includes("--force") };
|
|
5
|
+
}
|
|
6
|
+
const handler = async (args, options) => {
|
|
7
|
+
const { force } = parseStopArgs(args);
|
|
8
|
+
const pidPath = daemonPidPath(options.configDir);
|
|
9
|
+
let pidStr;
|
|
10
|
+
try {
|
|
11
|
+
pidStr = await readFile(pidPath, "utf8");
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
process.stderr.write("No running daemon found (PID file missing).\n");
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const pid = Number.parseInt(pidStr.trim(), 10);
|
|
19
|
+
if (!Number.isFinite(pid)) {
|
|
20
|
+
process.stderr.write(`Invalid PID in ${pidPath}: ${pidStr}\n`);
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
// Check if process is running
|
|
26
|
+
process.kill(pid, 0);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
process.stdout.write(`Daemon (PID ${pid}) is not running. Cleaning up PID file.\n`);
|
|
30
|
+
await rm(pidPath, { force: true });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const signal = force ? "SIGKILL" : "SIGTERM";
|
|
34
|
+
try {
|
|
35
|
+
process.kill(pid, signal);
|
|
36
|
+
process.stdout.write(`Sent ${signal} to orchestrator (PID ${pid}).\n`);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
process.stderr.write(`Failed to stop process ${pid}: ${error instanceof Error ? error.message : "Unknown error"}\n`);
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
await rm(pidPath, { force: true });
|
|
44
|
+
process.stdout.write("Daemon stopped.\n");
|
|
45
|
+
};
|
|
46
|
+
export default handler;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve, dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const handler = async (_args, options) => {
|
|
5
|
+
let version = "0.0.0";
|
|
6
|
+
try {
|
|
7
|
+
const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
8
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
9
|
+
version = pkg.version ?? version;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// Fall back to default
|
|
13
|
+
}
|
|
14
|
+
if (options.json) {
|
|
15
|
+
process.stdout.write(JSON.stringify({ version }) + "\n");
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
process.stdout.write(`gh-symphony v${version}\n`);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
export default handler;
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { OrchestratorWorkspaceConfig, WorkflowLifecycleConfig } from "@hojinzs/gh-symphony-core";
|
|
2
|
+
export declare const DEFAULT_CONFIG_DIR: string;
|
|
3
|
+
export declare const CONFIG_FILE = "config.json";
|
|
4
|
+
export declare const DAEMON_PID_FILE = "daemon.pid";
|
|
5
|
+
export declare const LOGS_DIR = "logs";
|
|
6
|
+
export type CliGlobalConfig = {
|
|
7
|
+
activeWorkspace: string | null;
|
|
8
|
+
token: string | null;
|
|
9
|
+
workspaces: string[];
|
|
10
|
+
};
|
|
11
|
+
export type CliWorkspaceConfig = OrchestratorWorkspaceConfig & {
|
|
12
|
+
workflowMapping?: WorkflowMappingConfig;
|
|
13
|
+
};
|
|
14
|
+
export type WorkflowMappingConfig = {
|
|
15
|
+
stateFieldName: string;
|
|
16
|
+
columnRoles: Record<string, ColumnRole>;
|
|
17
|
+
humanReviewMode: HumanReviewMode;
|
|
18
|
+
lifecycle: WorkflowLifecycleConfig;
|
|
19
|
+
};
|
|
20
|
+
export type ColumnRole = "trigger" | "working" | "human-review" | "done" | "ignored";
|
|
21
|
+
export type HumanReviewMode = "plan-and-pr" | "plan-only" | "pr-only" | "none";
|
|
22
|
+
export declare function resolveConfigDir(override?: string): string;
|
|
23
|
+
export declare function configFilePath(configDir: string): string;
|
|
24
|
+
export declare function workspaceConfigDir(configDir: string, workspaceId: string): string;
|
|
25
|
+
export declare function workspaceConfigPath(configDir: string, workspaceId: string): string;
|
|
26
|
+
export declare function workflowMappingPath(configDir: string, workspaceId: string): string;
|
|
27
|
+
export declare function daemonPidPath(configDir: string): string;
|
|
28
|
+
export declare function logsDir(configDir: string): string;
|
|
29
|
+
export declare function orchestratorLogPath(configDir: string): string;
|
|
30
|
+
export declare function loadGlobalConfig(configDir: string): Promise<CliGlobalConfig | null>;
|
|
31
|
+
export declare function saveGlobalConfig(configDir: string, config: CliGlobalConfig): Promise<void>;
|
|
32
|
+
export declare function loadWorkspaceConfig(configDir: string, workspaceId: string): Promise<CliWorkspaceConfig | null>;
|
|
33
|
+
export declare function saveWorkspaceConfig(configDir: string, workspaceId: string, config: CliWorkspaceConfig): Promise<void>;
|
|
34
|
+
export declare function loadWorkflowMapping(configDir: string, workspaceId: string): Promise<WorkflowMappingConfig | null>;
|
|
35
|
+
export declare function saveWorkflowMapping(configDir: string, workspaceId: string, mapping: WorkflowMappingConfig): Promise<void>;
|
|
36
|
+
export declare function loadActiveWorkspaceConfig(configDir: string): Promise<CliWorkspaceConfig | null>;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
export const DEFAULT_CONFIG_DIR = join(homedir(), ".gh-symphony");
|
|
5
|
+
export const CONFIG_FILE = "config.json";
|
|
6
|
+
export const DAEMON_PID_FILE = "daemon.pid";
|
|
7
|
+
export const LOGS_DIR = "logs";
|
|
8
|
+
export function resolveConfigDir(override) {
|
|
9
|
+
return override ?? process.env.GH_SYMPHONY_CONFIG_DIR ?? DEFAULT_CONFIG_DIR;
|
|
10
|
+
}
|
|
11
|
+
export function configFilePath(configDir) {
|
|
12
|
+
return join(configDir, CONFIG_FILE);
|
|
13
|
+
}
|
|
14
|
+
export function workspaceConfigDir(configDir, workspaceId) {
|
|
15
|
+
return join(configDir, "workspaces", workspaceId);
|
|
16
|
+
}
|
|
17
|
+
export function workspaceConfigPath(configDir, workspaceId) {
|
|
18
|
+
return join(workspaceConfigDir(configDir, workspaceId), "workspace.json");
|
|
19
|
+
}
|
|
20
|
+
export function workflowMappingPath(configDir, workspaceId) {
|
|
21
|
+
return join(workspaceConfigDir(configDir, workspaceId), "workflow-mapping.json");
|
|
22
|
+
}
|
|
23
|
+
export function daemonPidPath(configDir) {
|
|
24
|
+
return join(configDir, DAEMON_PID_FILE);
|
|
25
|
+
}
|
|
26
|
+
export function logsDir(configDir) {
|
|
27
|
+
return join(configDir, LOGS_DIR);
|
|
28
|
+
}
|
|
29
|
+
export function orchestratorLogPath(configDir) {
|
|
30
|
+
return join(logsDir(configDir), "orchestrator.log");
|
|
31
|
+
}
|
|
32
|
+
export async function loadGlobalConfig(configDir) {
|
|
33
|
+
return readJsonFile(configFilePath(configDir));
|
|
34
|
+
}
|
|
35
|
+
export async function saveGlobalConfig(configDir, config) {
|
|
36
|
+
await writeJsonFile(configFilePath(configDir), config);
|
|
37
|
+
}
|
|
38
|
+
export async function loadWorkspaceConfig(configDir, workspaceId) {
|
|
39
|
+
return readJsonFile(workspaceConfigPath(configDir, workspaceId));
|
|
40
|
+
}
|
|
41
|
+
export async function saveWorkspaceConfig(configDir, workspaceId, config) {
|
|
42
|
+
await writeJsonFile(workspaceConfigPath(configDir, workspaceId), config);
|
|
43
|
+
}
|
|
44
|
+
export async function loadWorkflowMapping(configDir, workspaceId) {
|
|
45
|
+
return readJsonFile(workflowMappingPath(configDir, workspaceId));
|
|
46
|
+
}
|
|
47
|
+
export async function saveWorkflowMapping(configDir, workspaceId, mapping) {
|
|
48
|
+
await writeJsonFile(workflowMappingPath(configDir, workspaceId), mapping);
|
|
49
|
+
}
|
|
50
|
+
export async function loadActiveWorkspaceConfig(configDir) {
|
|
51
|
+
const global = await loadGlobalConfig(configDir);
|
|
52
|
+
if (!global?.activeWorkspace) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return loadWorkspaceConfig(configDir, global.activeWorkspace);
|
|
56
|
+
}
|
|
57
|
+
async function readJsonFile(path) {
|
|
58
|
+
try {
|
|
59
|
+
const raw = await readFile(path, "utf8");
|
|
60
|
+
return JSON.parse(raw);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (isFileMissing(error)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function writeJsonFile(path, value) {
|
|
70
|
+
await mkdir(dirname(path), { recursive: true });
|
|
71
|
+
const temporaryPath = `${path}.tmp`;
|
|
72
|
+
await writeFile(temporaryPath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
73
|
+
const { rename } = await import("node:fs/promises");
|
|
74
|
+
await rename(temporaryPath, path);
|
|
75
|
+
}
|
|
76
|
+
function isFileMissing(error) {
|
|
77
|
+
return Boolean(error &&
|
|
78
|
+
typeof error === "object" &&
|
|
79
|
+
"code" in error &&
|
|
80
|
+
(error.code === "ENOENT" || error.code === "ENOTDIR"));
|
|
81
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type GitHubClient = {
|
|
2
|
+
token: string;
|
|
3
|
+
apiUrl: string;
|
|
4
|
+
fetchImpl: typeof fetch;
|
|
5
|
+
};
|
|
6
|
+
export type ViewerInfo = {
|
|
7
|
+
login: string;
|
|
8
|
+
name: string | null;
|
|
9
|
+
scopes: string[];
|
|
10
|
+
};
|
|
11
|
+
export type ProjectSummary = {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
shortDescription: string;
|
|
15
|
+
url: string;
|
|
16
|
+
openItemCount: number;
|
|
17
|
+
owner: {
|
|
18
|
+
login: string;
|
|
19
|
+
type: "User" | "Organization";
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export type StatusFieldOption = {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
description: string | null;
|
|
26
|
+
color: string | null;
|
|
27
|
+
};
|
|
28
|
+
export type ProjectStatusField = {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
options: StatusFieldOption[];
|
|
32
|
+
};
|
|
33
|
+
export type LinkedRepository = {
|
|
34
|
+
owner: string;
|
|
35
|
+
name: string;
|
|
36
|
+
url: string;
|
|
37
|
+
cloneUrl: string;
|
|
38
|
+
};
|
|
39
|
+
export type ProjectDetail = {
|
|
40
|
+
id: string;
|
|
41
|
+
title: string;
|
|
42
|
+
url: string;
|
|
43
|
+
statusFields: ProjectStatusField[];
|
|
44
|
+
linkedRepositories: LinkedRepository[];
|
|
45
|
+
};
|
|
46
|
+
export declare class GitHubApiError extends Error {
|
|
47
|
+
readonly status?: number | undefined;
|
|
48
|
+
constructor(message: string, status?: number | undefined);
|
|
49
|
+
}
|
|
50
|
+
export declare function createClient(token: string, options?: {
|
|
51
|
+
apiUrl?: string;
|
|
52
|
+
fetchImpl?: typeof fetch;
|
|
53
|
+
}): GitHubClient;
|
|
54
|
+
export declare function validateToken(client: GitHubClient): Promise<ViewerInfo>;
|
|
55
|
+
export declare function checkRequiredScopes(scopes: string[]): {
|
|
56
|
+
valid: boolean;
|
|
57
|
+
missing: string[];
|
|
58
|
+
};
|
|
59
|
+
export declare function listUserProjects(client: GitHubClient): Promise<ProjectSummary[]>;
|
|
60
|
+
export declare function getProjectDetail(client: GitHubClient, projectId: string): Promise<ProjectDetail>;
|