@gh-symphony/cli 0.0.22 → 0.1.3
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 +72 -77
- package/dist/{chunk-HMLBBZNY.js → chunk-2YF7PQUC.js} +16 -71
- package/dist/{chunk-IWFX2FMA.js → chunk-6I753NYO.js} +4 -1
- package/dist/{chunk-2TSM3INR.js → chunk-HQ7A3C7K.js} +575 -12
- package/dist/{chunk-36KYEDEO.js → chunk-MVRF7BES.js} +1 -10
- package/dist/{workflow-L3KT6HB7.js → chunk-NESHTYXQ.js} +27 -19
- package/dist/{chunk-2UW7NQLX.js → chunk-PEZUBHWJ.js} +1 -1
- package/dist/chunk-PG332ZS4.js +238 -0
- package/dist/{chunk-EEQQWTXS.js → chunk-WCOIVNHH.js} +213 -82
- package/dist/{chunk-QIRE2VXS.js → chunk-WOVNN5NW.js} +16 -17
- package/dist/{chunk-C67H3OUL.js → chunk-Z3NZOPLZ.js} +0 -81
- package/dist/{config-cmd-Z3A7V6NC.js → config-cmd-2ADPUYWA.js} +1 -1
- package/dist/{doctor-EJUMPBMW.js → doctor-2AXHIEAP.js} +464 -40
- package/dist/index.js +340 -294
- package/dist/{chunk-PUDXVBSN.js → repo-SUXYT4OK.js} +6272 -2996
- package/dist/{setup-TZJSM3QV.js → setup-UBHOMXUG.js} +57 -92
- package/dist/{upgrade-O33S2SJK.js → upgrade-355SQJ5P.js} +2 -2
- package/dist/{version-CW54Q7BK.js → version-4ILSDZQH.js} +1 -1
- package/dist/worker-entry.js +10 -5
- package/dist/workflow-S6YSZPQT.js +22 -0
- package/package.json +4 -4
- package/dist/chunk-DDL4BWSL.js +0 -146
- package/dist/chunk-DFLXHNYQ.js +0 -482
- package/dist/chunk-E7HYEEZD.js +0 -1318
- package/dist/chunk-GDE6FYN4.js +0 -26
- package/dist/chunk-GSX2FV3M.js +0 -103
- package/dist/chunk-ZHOKYUO3.js +0 -1047
- package/dist/init-54HMKNYI.js +0 -38
- package/dist/logs-GTZ4U5JE.js +0 -188
- package/dist/project-RMYMZSFV.js +0 -25
- package/dist/recover-LTLKMTRX.js +0 -133
- package/dist/repo-WI7GF6XQ.js +0 -749
- package/dist/run-IHN3ZL35.js +0 -122
- package/dist/start-RTAHQMR2.js +0 -19
- package/dist/status-F4D52OVK.js +0 -12
- package/dist/stop-MDKMJPVR.js +0 -10
package/dist/repo-WI7GF6XQ.js
DELETED
|
@@ -1,749 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
start_default
|
|
4
|
-
} from "./chunk-E7HYEEZD.js";
|
|
5
|
-
import "./chunk-PUDXVBSN.js";
|
|
6
|
-
import "./chunk-2UW7NQLX.js";
|
|
7
|
-
import "./chunk-2TSM3INR.js";
|
|
8
|
-
import {
|
|
9
|
-
GitHubRepositoryLookupError,
|
|
10
|
-
GitHubScopeError,
|
|
11
|
-
checkRequiredScopes,
|
|
12
|
-
createClient,
|
|
13
|
-
getGhToken,
|
|
14
|
-
getProjectDetail,
|
|
15
|
-
getRepositoryMetadata,
|
|
16
|
-
validateToken
|
|
17
|
-
} from "./chunk-C67H3OUL.js";
|
|
18
|
-
import {
|
|
19
|
-
parseWorkflowMarkdown
|
|
20
|
-
} from "./chunk-EEQQWTXS.js";
|
|
21
|
-
import {
|
|
22
|
-
status_default
|
|
23
|
-
} from "./chunk-DFLXHNYQ.js";
|
|
24
|
-
import "./chunk-36KYEDEO.js";
|
|
25
|
-
import {
|
|
26
|
-
resolveRepoRuntimeRoot
|
|
27
|
-
} from "./chunk-IWFX2FMA.js";
|
|
28
|
-
import {
|
|
29
|
-
stop_default
|
|
30
|
-
} from "./chunk-GSX2FV3M.js";
|
|
31
|
-
import {
|
|
32
|
-
rejectRemovedProjectId
|
|
33
|
-
} from "./chunk-GDE6FYN4.js";
|
|
34
|
-
import "./chunk-DDL4BWSL.js";
|
|
35
|
-
import {
|
|
36
|
-
loadActiveProjectConfig,
|
|
37
|
-
loadGlobalConfig,
|
|
38
|
-
saveGlobalConfig,
|
|
39
|
-
saveProjectConfig
|
|
40
|
-
} from "./chunk-QIRE2VXS.js";
|
|
41
|
-
|
|
42
|
-
// src/repo-runtime.ts
|
|
43
|
-
import { execFileSync } from "child_process";
|
|
44
|
-
import {
|
|
45
|
-
mkdir,
|
|
46
|
-
readdir,
|
|
47
|
-
readFile,
|
|
48
|
-
rename,
|
|
49
|
-
rm,
|
|
50
|
-
stat,
|
|
51
|
-
writeFile
|
|
52
|
-
} from "fs/promises";
|
|
53
|
-
import { basename, dirname, join, resolve } from "path";
|
|
54
|
-
var INTERNAL_PROJECT_ID = "repository";
|
|
55
|
-
var RepoRuntimeMigrationError = class extends Error {
|
|
56
|
-
};
|
|
57
|
-
function parseRepoRuntimeFlags(args) {
|
|
58
|
-
const flags = { repoDir: process.cwd() };
|
|
59
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
60
|
-
const arg = args[i];
|
|
61
|
-
const value = args[i + 1];
|
|
62
|
-
if (arg === "--repo-dir") {
|
|
63
|
-
if (!value || value.startsWith("-")) {
|
|
64
|
-
throw new Error("Option '--repo-dir' argument missing");
|
|
65
|
-
}
|
|
66
|
-
flags.repoDir = resolve(value);
|
|
67
|
-
i += 1;
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
if (arg === "--workflow-file") {
|
|
71
|
-
if (!value || value.startsWith("-")) {
|
|
72
|
-
throw new Error("Option '--workflow-file' argument missing");
|
|
73
|
-
}
|
|
74
|
-
flags.workflowFile = value;
|
|
75
|
-
i += 1;
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
if (arg?.startsWith("-")) {
|
|
79
|
-
throw new Error(`Unknown option '${arg}'`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return flags;
|
|
83
|
-
}
|
|
84
|
-
async function initRepoRuntime(flags) {
|
|
85
|
-
const repoDir = resolve(flags.repoDir);
|
|
86
|
-
const runtimeRoot = resolveRepoRuntimeRoot(repoDir);
|
|
87
|
-
await migrateLegacyRuntime(runtimeRoot);
|
|
88
|
-
const workflowPath = resolve(repoDir, flags.workflowFile ?? "WORKFLOW.md");
|
|
89
|
-
const workflow = parseWorkflowMarkdown(await readFile(workflowPath, "utf8"));
|
|
90
|
-
const repository = resolveRepository(repoDir);
|
|
91
|
-
const trackerAdapter = workflow.tracker.kind ?? "github-project";
|
|
92
|
-
const trackerBindingId = workflow.tracker.projectId ?? workflow.tracker.projectSlug ?? "";
|
|
93
|
-
const trackerSettings = {
|
|
94
|
-
...workflow.tracker.projectId ? { projectId: workflow.tracker.projectId } : {},
|
|
95
|
-
repository: `${repository.owner}/${repository.name}`
|
|
96
|
-
};
|
|
97
|
-
if (trackerAdapter === "file") {
|
|
98
|
-
if (!process.env.GH_SYMPHONY_FILE_TRACKER_ISSUES_PATH) {
|
|
99
|
-
throw new Error(
|
|
100
|
-
"File tracker repo init requires GH_SYMPHONY_FILE_TRACKER_ISSUES_PATH to point to the issues fixture."
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
trackerSettings.issuesPath = process.env.GH_SYMPHONY_FILE_TRACKER_ISSUES_PATH;
|
|
104
|
-
}
|
|
105
|
-
const projectConfig = {
|
|
106
|
-
projectId: INTERNAL_PROJECT_ID,
|
|
107
|
-
slug: basename(repoDir) || INTERNAL_PROJECT_ID,
|
|
108
|
-
displayName: `${repository.owner}/${repository.name}`,
|
|
109
|
-
workspaceDir: repoDir,
|
|
110
|
-
repository,
|
|
111
|
-
tracker: {
|
|
112
|
-
adapter: trackerAdapter,
|
|
113
|
-
bindingId: trackerBindingId,
|
|
114
|
-
settings: trackerSettings
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
await mkdir(runtimeRoot, { recursive: true });
|
|
118
|
-
await saveProjectConfig(runtimeRoot, INTERNAL_PROJECT_ID, projectConfig);
|
|
119
|
-
await saveGlobalConfig(runtimeRoot, {
|
|
120
|
-
activeProject: INTERNAL_PROJECT_ID,
|
|
121
|
-
projects: [INTERNAL_PROJECT_ID]
|
|
122
|
-
});
|
|
123
|
-
const orchestratorConfig = {
|
|
124
|
-
projectId: INTERNAL_PROJECT_ID,
|
|
125
|
-
slug: projectConfig.slug,
|
|
126
|
-
workspaceDir: repoDir,
|
|
127
|
-
repository,
|
|
128
|
-
tracker: projectConfig.tracker
|
|
129
|
-
};
|
|
130
|
-
await writeJsonFile(join(runtimeRoot, "project.json"), orchestratorConfig);
|
|
131
|
-
return {
|
|
132
|
-
configDir: runtimeRoot,
|
|
133
|
-
projectId: INTERNAL_PROJECT_ID,
|
|
134
|
-
workflowPath,
|
|
135
|
-
repository
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
async function migrateLegacyRuntime(runtimeRoot) {
|
|
139
|
-
const projectsDir = join(runtimeRoot, "projects");
|
|
140
|
-
const projectIds = await readDirectoryNames(projectsDir);
|
|
141
|
-
if (projectIds.length === 0) {
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
if (projectIds.length === 1 && projectIds[0] === INTERNAL_PROJECT_ID && await pathExists(join(runtimeRoot, "project.json"))) {
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
if (projectIds.length > 1) {
|
|
148
|
-
throw new RepoRuntimeMigrationError(
|
|
149
|
-
[
|
|
150
|
-
"Multiple legacy project runtime directories were found under .runtime/orchestrator/projects.",
|
|
151
|
-
`Found: ${projectIds.join(", ")}`,
|
|
152
|
-
"Automatic migration is only supported when exactly one project directory exists.",
|
|
153
|
-
"Manually keep the project directory you want to promote, archive or remove the others, then re-run 'gh-symphony repo init'."
|
|
154
|
-
].join("\n")
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
const sourceDir = join(projectsDir, projectIds[0]);
|
|
158
|
-
const entries = await readdir(sourceDir);
|
|
159
|
-
for (const entry of entries) {
|
|
160
|
-
const target = join(runtimeRoot, entry);
|
|
161
|
-
if (await pathExists(target)) {
|
|
162
|
-
throw new RepoRuntimeMigrationError(
|
|
163
|
-
`Cannot promote legacy runtime data because '${entry}' already exists in .runtime/orchestrator. Move or remove it, then re-run 'gh-symphony repo init'.`
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
await rename(join(sourceDir, entry), target);
|
|
167
|
-
}
|
|
168
|
-
await stripProjectIdFromRunRecords(join(runtimeRoot, "runs"));
|
|
169
|
-
await rm(projectsDir, { recursive: true, force: true });
|
|
170
|
-
}
|
|
171
|
-
async function stripProjectIdFromRunRecords(runsDir) {
|
|
172
|
-
for (const runId of await readDirectoryNames(runsDir)) {
|
|
173
|
-
const runPath = join(runsDir, runId, "run.json");
|
|
174
|
-
const run = await readJsonFile(runPath);
|
|
175
|
-
if (!run || !("projectId" in run)) {
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
delete run.projectId;
|
|
179
|
-
await writeJsonFile(runPath, run);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
async function readDirectoryNames(path) {
|
|
183
|
-
try {
|
|
184
|
-
const entries = await readdir(path, { withFileTypes: true });
|
|
185
|
-
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
186
|
-
} catch (error) {
|
|
187
|
-
if (isMissing(error)) {
|
|
188
|
-
return [];
|
|
189
|
-
}
|
|
190
|
-
throw error;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
function resolveRepository(repoDir) {
|
|
194
|
-
const remote = readGitOrigin(repoDir);
|
|
195
|
-
const cleanedRemote = remote.replace(/\.git$/, "");
|
|
196
|
-
const match = cleanedRemote.match(/github\.com[:/]([^/]+)\/([^/]+)$/) ?? cleanedRemote.match(/^([^/]+)\/([^/]+)$/);
|
|
197
|
-
if (!match) {
|
|
198
|
-
throw new Error(
|
|
199
|
-
"Unable to infer GitHub repository from git remote 'origin'. Run from a cloned GitHub repository or set origin to owner/name."
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
return {
|
|
203
|
-
owner: match[1],
|
|
204
|
-
name: match[2],
|
|
205
|
-
cloneUrl: remote.startsWith("http") ? remote : `https://github.com/${match[1]}/${match[2]}.git`,
|
|
206
|
-
path: repoDir
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
function readGitOrigin(repoDir) {
|
|
210
|
-
try {
|
|
211
|
-
return execFileSync(
|
|
212
|
-
"git",
|
|
213
|
-
["-C", repoDir, "config", "--get", "remote.origin.url"],
|
|
214
|
-
{
|
|
215
|
-
encoding: "utf8",
|
|
216
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
217
|
-
}
|
|
218
|
-
).trim();
|
|
219
|
-
} catch {
|
|
220
|
-
throw new Error(
|
|
221
|
-
"Unable to read git remote 'origin'. Run 'gh-symphony repo init' inside a cloned repository."
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
async function readJsonFile(path) {
|
|
226
|
-
try {
|
|
227
|
-
return JSON.parse(await readFile(path, "utf8"));
|
|
228
|
-
} catch (error) {
|
|
229
|
-
if (isMissing(error)) {
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
throw error;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
async function writeJsonFile(path, value) {
|
|
236
|
-
await mkdir(dirname(path), { recursive: true });
|
|
237
|
-
const temporaryPath = `${path}.tmp`;
|
|
238
|
-
await writeFile(temporaryPath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
239
|
-
await rename(temporaryPath, path);
|
|
240
|
-
}
|
|
241
|
-
async function pathExists(path) {
|
|
242
|
-
try {
|
|
243
|
-
await stat(path);
|
|
244
|
-
return true;
|
|
245
|
-
} catch (error) {
|
|
246
|
-
if (isMissing(error)) {
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
249
|
-
throw error;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
function isMissing(error) {
|
|
253
|
-
return Boolean(
|
|
254
|
-
error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// src/commands/repo.ts
|
|
259
|
-
var handler = async (args, options) => {
|
|
260
|
-
const [subcommand, ...rest] = args;
|
|
261
|
-
switch (subcommand) {
|
|
262
|
-
case "list":
|
|
263
|
-
await repoList(options);
|
|
264
|
-
break;
|
|
265
|
-
case "init":
|
|
266
|
-
await repoInit(rest, options);
|
|
267
|
-
break;
|
|
268
|
-
case "start":
|
|
269
|
-
if (rejectRemovedProjectId(rest)) return;
|
|
270
|
-
await start_default(rest, repoOptions(options));
|
|
271
|
-
break;
|
|
272
|
-
case "status":
|
|
273
|
-
if (rejectRemovedProjectId(rest)) return;
|
|
274
|
-
await status_default(rest, repoOptions(options));
|
|
275
|
-
break;
|
|
276
|
-
case "stop":
|
|
277
|
-
if (rejectRemovedProjectId(rest)) return;
|
|
278
|
-
await stop_default(rest, repoOptions(options));
|
|
279
|
-
break;
|
|
280
|
-
case "add":
|
|
281
|
-
await repoAdd(rest, options);
|
|
282
|
-
break;
|
|
283
|
-
case "remove":
|
|
284
|
-
await repoRemove(rest, options);
|
|
285
|
-
break;
|
|
286
|
-
case "sync":
|
|
287
|
-
await repoSync(rest, options);
|
|
288
|
-
break;
|
|
289
|
-
default:
|
|
290
|
-
process.stderr.write(
|
|
291
|
-
"Usage: gh-symphony repo <init|start|status|stop|list|add|remove|sync> [repo]\n"
|
|
292
|
-
);
|
|
293
|
-
process.exitCode = 2;
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
var repo_default = handler;
|
|
297
|
-
function repoOptions(options) {
|
|
298
|
-
return {
|
|
299
|
-
...options,
|
|
300
|
-
configDir: resolveRepoRuntimeRoot()
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
async function repoInit(args, options) {
|
|
304
|
-
if (rejectRemovedProjectId(args)) {
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
let flags;
|
|
308
|
-
try {
|
|
309
|
-
flags = parseRepoRuntimeFlags(args);
|
|
310
|
-
} catch (error) {
|
|
311
|
-
process.stderr.write(
|
|
312
|
-
`${error instanceof Error ? error.message : "Invalid arguments"}
|
|
313
|
-
`
|
|
314
|
-
);
|
|
315
|
-
process.stderr.write(
|
|
316
|
-
"Usage: gh-symphony repo init [--repo-dir <path>] [--workflow-file <path>]\n"
|
|
317
|
-
);
|
|
318
|
-
process.exitCode = 2;
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
try {
|
|
322
|
-
const result = await initRepoRuntime(flags);
|
|
323
|
-
if (options.json) {
|
|
324
|
-
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
process.stdout.write(
|
|
328
|
-
[
|
|
329
|
-
`Repository initialized: ${formatRepoSpec(result.repository)}`,
|
|
330
|
-
`Runtime: ${result.configDir}`,
|
|
331
|
-
`Workflow: ${result.workflowPath}`
|
|
332
|
-
].join("\n") + "\n"
|
|
333
|
-
);
|
|
334
|
-
} catch (error) {
|
|
335
|
-
process.stderr.write(
|
|
336
|
-
`${error instanceof Error ? error.message : "Repository initialization failed."}
|
|
337
|
-
`
|
|
338
|
-
);
|
|
339
|
-
process.exitCode = 1;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
function repoKey(repo) {
|
|
343
|
-
return `${repo.owner}/${repo.name}`.toLowerCase();
|
|
344
|
-
}
|
|
345
|
-
function toRepoConfigEntry(repo) {
|
|
346
|
-
return {
|
|
347
|
-
owner: repo.owner,
|
|
348
|
-
name: repo.name,
|
|
349
|
-
cloneUrl: repo.cloneUrl
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
function configuredRepositories(config) {
|
|
353
|
-
const repos = [
|
|
354
|
-
...config.repository ? [config.repository] : [],
|
|
355
|
-
...config.repositories ?? []
|
|
356
|
-
].filter(isConfiguredRepository);
|
|
357
|
-
return [...new Map(repos.map((repo) => [repoKey(repo), repo])).values()];
|
|
358
|
-
}
|
|
359
|
-
function withConfiguredRepository(config, repository) {
|
|
360
|
-
return {
|
|
361
|
-
...config,
|
|
362
|
-
repository,
|
|
363
|
-
repositories: [repository],
|
|
364
|
-
tracker: {
|
|
365
|
-
...config.tracker,
|
|
366
|
-
settings: {
|
|
367
|
-
...config.tracker.settings,
|
|
368
|
-
repository: `${repository.owner}/${repository.name}`
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
function isConfiguredRepository(repository) {
|
|
374
|
-
return Boolean(repository?.owner && repository.name);
|
|
375
|
-
}
|
|
376
|
-
function parseRepoSyncFlags(args) {
|
|
377
|
-
const flags = { dryRun: false, prune: false };
|
|
378
|
-
for (const arg of args) {
|
|
379
|
-
if (arg === "--dry-run") {
|
|
380
|
-
flags.dryRun = true;
|
|
381
|
-
} else if (arg === "--prune") {
|
|
382
|
-
flags.prune = true;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
return flags;
|
|
386
|
-
}
|
|
387
|
-
function displayScopeError(error) {
|
|
388
|
-
const plural = error.requiredScopes.length === 1 ? "" : "s";
|
|
389
|
-
process.stderr.write(
|
|
390
|
-
`Token is missing required scope${plural}: ${error.requiredScopes.join(", ")}
|
|
391
|
-
`
|
|
392
|
-
);
|
|
393
|
-
const currentSet = new Set(
|
|
394
|
-
error.currentScopes.map((scope) => scope.toLowerCase())
|
|
395
|
-
);
|
|
396
|
-
const scopesToAdd = ["repo", "read:org", "project"].filter(
|
|
397
|
-
(scope) => !currentSet.has(scope)
|
|
398
|
-
);
|
|
399
|
-
const scopeArg = scopesToAdd.length > 0 ? scopesToAdd.join(",") : error.requiredScopes.join(",");
|
|
400
|
-
process.stderr.write(
|
|
401
|
-
`Run 'gh auth refresh --scopes ${scopeArg}' and try again.
|
|
402
|
-
`
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
function formatRepoSpec(repo) {
|
|
406
|
-
return `${repo.owner}/${repo.name}`;
|
|
407
|
-
}
|
|
408
|
-
function fallbackCloneUrl(repo) {
|
|
409
|
-
return `https://github.com/${repo.owner}/${repo.name}.git`;
|
|
410
|
-
}
|
|
411
|
-
function sortRepos(repos) {
|
|
412
|
-
return [...repos].sort(
|
|
413
|
-
(left, right) => formatRepoSpec(left).localeCompare(formatRepoSpec(right))
|
|
414
|
-
);
|
|
415
|
-
}
|
|
416
|
-
function renderRepoGroup(label, repos) {
|
|
417
|
-
if (repos.length === 0) {
|
|
418
|
-
return [`${label}: none`];
|
|
419
|
-
}
|
|
420
|
-
return [
|
|
421
|
-
label,
|
|
422
|
-
...sortRepos(repos).map((repo) => ` ${formatRepoSpec(repo)}`)
|
|
423
|
-
];
|
|
424
|
-
}
|
|
425
|
-
function buildSyncedRepositories(currentRepos, linkedMap, linkedRepositories, prune) {
|
|
426
|
-
const retained = currentRepos.filter((repo) => linkedMap.has(repoKey(repo)) || !prune).map((repo) => {
|
|
427
|
-
const linked = linkedMap.get(repoKey(repo));
|
|
428
|
-
return linked ? toRepoConfigEntry(linked) : { ...repo };
|
|
429
|
-
});
|
|
430
|
-
const currentKeys = new Set(currentRepos.map((repo) => repoKey(repo)));
|
|
431
|
-
const additions = sortRepos(
|
|
432
|
-
linkedRepositories.filter((repo) => !currentKeys.has(repoKey(repo))).map(toRepoConfigEntry)
|
|
433
|
-
);
|
|
434
|
-
return [...retained, ...additions];
|
|
435
|
-
}
|
|
436
|
-
function writeRepoSummary(summary, options) {
|
|
437
|
-
if (options.json) {
|
|
438
|
-
process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
process.stdout.write(
|
|
442
|
-
[
|
|
443
|
-
`Repository sync ${summary.dryRun ? "preview" : "complete"} for ${summary.projectId}`,
|
|
444
|
-
`Mode: ${summary.prune ? "prune" : "additive"}`,
|
|
445
|
-
...renderRepoGroup("Added", summary.added),
|
|
446
|
-
...renderRepoGroup("Removed", summary.removed),
|
|
447
|
-
...renderRepoGroup("Unchanged", summary.unchanged),
|
|
448
|
-
summary.dryRun ? "No config changes written." : "Configuration updated."
|
|
449
|
-
].join("\n") + "\n"
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
async function repoList(options) {
|
|
453
|
-
const ws = await loadActiveProjectConfig(options.configDir);
|
|
454
|
-
if (!ws) {
|
|
455
|
-
process.stderr.write("No project configured.\n");
|
|
456
|
-
process.exitCode = 1;
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
if (options.json) {
|
|
460
|
-
process.stdout.write(
|
|
461
|
-
JSON.stringify(configuredRepositories(ws), null, 2) + "\n"
|
|
462
|
-
);
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
process.stdout.write("Repositories:\n");
|
|
466
|
-
for (const repo of configuredRepositories(ws)) {
|
|
467
|
-
process.stdout.write(` ${repo.owner}/${repo.name}
|
|
468
|
-
`);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
async function repoAdd(args, options) {
|
|
472
|
-
const [repoSpec] = args;
|
|
473
|
-
if (!repoSpec || !repoSpec.includes("/")) {
|
|
474
|
-
process.stderr.write("Usage: gh-symphony repo add <owner/name>\n");
|
|
475
|
-
process.exitCode = 2;
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
const global = await loadGlobalConfig(options.configDir);
|
|
479
|
-
if (!global?.activeProject) {
|
|
480
|
-
process.stderr.write("No active project.\n");
|
|
481
|
-
process.exitCode = 1;
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
const ws = await loadActiveProjectConfig(options.configDir);
|
|
485
|
-
if (!ws) {
|
|
486
|
-
process.stderr.write("Project config missing.\n");
|
|
487
|
-
process.exitCode = 1;
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
const activeProjectId = global.activeProject;
|
|
491
|
-
const [owner, name] = repoSpec.split("/");
|
|
492
|
-
if (!owner || !name) {
|
|
493
|
-
process.stderr.write("Invalid repo format. Use: owner/name\n");
|
|
494
|
-
process.exitCode = 2;
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
const requestedRepo = { owner, name };
|
|
498
|
-
const addRepository = async (repo, message, warning) => {
|
|
499
|
-
if (configuredRepositories(ws).some(
|
|
500
|
-
(entry) => repoKey(entry) === repoKey(repo)
|
|
501
|
-
)) {
|
|
502
|
-
process.stdout.write(
|
|
503
|
-
`Repository ${formatRepoSpec(repo)} is already configured.
|
|
504
|
-
`
|
|
505
|
-
);
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
await saveProjectConfig(
|
|
509
|
-
options.configDir,
|
|
510
|
-
activeProjectId,
|
|
511
|
-
withConfiguredRepository(ws, repo)
|
|
512
|
-
);
|
|
513
|
-
if (warning) {
|
|
514
|
-
process.stderr.write(`${warning}
|
|
515
|
-
`);
|
|
516
|
-
}
|
|
517
|
-
process.stdout.write(`${message}
|
|
518
|
-
`);
|
|
519
|
-
};
|
|
520
|
-
let token;
|
|
521
|
-
try {
|
|
522
|
-
token = getGhToken();
|
|
523
|
-
} catch {
|
|
524
|
-
await addRepository(
|
|
525
|
-
{
|
|
526
|
-
...requestedRepo,
|
|
527
|
-
cloneUrl: fallbackCloneUrl(requestedRepo)
|
|
528
|
-
},
|
|
529
|
-
`Added repository without validation: ${formatRepoSpec(requestedRepo)}`,
|
|
530
|
-
"Warning: GitHub authentication is unavailable, so the repository was saved without validation. Run 'gh auth login --scopes repo,read:org,project' or set GITHUB_GRAPHQL_TOKEN to validate access before saving."
|
|
531
|
-
);
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
try {
|
|
535
|
-
const repository = await getRepositoryMetadata(
|
|
536
|
-
createClient(token),
|
|
537
|
-
owner,
|
|
538
|
-
name
|
|
539
|
-
);
|
|
540
|
-
await addRepository(
|
|
541
|
-
{
|
|
542
|
-
owner: repository.owner,
|
|
543
|
-
name: repository.name,
|
|
544
|
-
cloneUrl: repository.cloneUrl || fallbackCloneUrl(repository)
|
|
545
|
-
},
|
|
546
|
-
`Added repository after validation: ${formatRepoSpec(repository)}`
|
|
547
|
-
);
|
|
548
|
-
} catch (error) {
|
|
549
|
-
if (error instanceof GitHubRepositoryLookupError && error.reason === "offline") {
|
|
550
|
-
await addRepository(
|
|
551
|
-
{
|
|
552
|
-
...requestedRepo,
|
|
553
|
-
cloneUrl: fallbackCloneUrl(requestedRepo)
|
|
554
|
-
},
|
|
555
|
-
`Added repository without validation: ${formatRepoSpec(requestedRepo)}`,
|
|
556
|
-
`Warning: ${error.message} Saved the repository without validation. ${error.remediation}`
|
|
557
|
-
);
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
if (error instanceof GitHubRepositoryLookupError) {
|
|
561
|
-
process.stderr.write(`${error.message}
|
|
562
|
-
${error.remediation}
|
|
563
|
-
`);
|
|
564
|
-
} else {
|
|
565
|
-
process.stderr.write(
|
|
566
|
-
`${error instanceof Error ? error.message : "Repository validation failed."}
|
|
567
|
-
`
|
|
568
|
-
);
|
|
569
|
-
}
|
|
570
|
-
process.exitCode = 1;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
async function repoRemove(args, options) {
|
|
574
|
-
const [repoSpec] = args;
|
|
575
|
-
if (!repoSpec || !repoSpec.includes("/")) {
|
|
576
|
-
process.stderr.write("Usage: gh-symphony repo remove <owner/name>\n");
|
|
577
|
-
process.exitCode = 2;
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
const global = await loadGlobalConfig(options.configDir);
|
|
581
|
-
if (!global?.activeProject) {
|
|
582
|
-
process.stderr.write("No active project.\n");
|
|
583
|
-
process.exitCode = 1;
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
const ws = await loadActiveProjectConfig(options.configDir);
|
|
587
|
-
if (!ws) {
|
|
588
|
-
process.stderr.write("Project config missing.\n");
|
|
589
|
-
process.exitCode = 1;
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
const [owner, name] = repoSpec.split("/");
|
|
593
|
-
const requestedRepo = { owner, name };
|
|
594
|
-
const currentRepos = configuredRepositories(ws);
|
|
595
|
-
if (!currentRepos.some((repo) => repoKey(repo) === repoKey(requestedRepo))) {
|
|
596
|
-
process.stderr.write(`Repository ${repoSpec} is not configured.
|
|
597
|
-
`);
|
|
598
|
-
process.exitCode = 1;
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
const nextRepositories = currentRepos.filter(
|
|
602
|
-
(repo) => repoKey(repo) !== repoKey(requestedRepo)
|
|
603
|
-
);
|
|
604
|
-
if (nextRepositories.length === 0) {
|
|
605
|
-
process.stderr.write(
|
|
606
|
-
"Repository removal would leave the project without a repository. Remove the project instead or add another repository first.\n"
|
|
607
|
-
);
|
|
608
|
-
process.exitCode = 1;
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
await saveProjectConfig(options.configDir, global.activeProject, {
|
|
612
|
-
...ws,
|
|
613
|
-
repository: nextRepositories[0],
|
|
614
|
-
repositories: nextRepositories,
|
|
615
|
-
tracker: {
|
|
616
|
-
...ws.tracker,
|
|
617
|
-
settings: {
|
|
618
|
-
...ws.tracker.settings,
|
|
619
|
-
repository: `${nextRepositories[0].owner}/${nextRepositories[0].name}`
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
});
|
|
623
|
-
process.stdout.write(
|
|
624
|
-
`Removed repository: ${formatRepoSpec(requestedRepo)}
|
|
625
|
-
`
|
|
626
|
-
);
|
|
627
|
-
}
|
|
628
|
-
async function repoSync(args, options) {
|
|
629
|
-
const flags = parseRepoSyncFlags(args);
|
|
630
|
-
const global = await loadGlobalConfig(options.configDir);
|
|
631
|
-
if (!global?.activeProject) {
|
|
632
|
-
process.stderr.write("No active project.\n");
|
|
633
|
-
process.exitCode = 1;
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
const ws = await loadActiveProjectConfig(options.configDir);
|
|
637
|
-
if (!ws) {
|
|
638
|
-
process.stderr.write("Project config missing.\n");
|
|
639
|
-
process.exitCode = 1;
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
const projectBindingId = typeof ws.tracker.settings?.projectId === "string" ? ws.tracker.settings.projectId : ws.tracker.bindingId;
|
|
643
|
-
if (!projectBindingId) {
|
|
644
|
-
process.stderr.write(
|
|
645
|
-
"Active project is missing its GitHub Project binding. Re-run 'gh-symphony project add'.\n"
|
|
646
|
-
);
|
|
647
|
-
process.exitCode = 1;
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
let token;
|
|
651
|
-
try {
|
|
652
|
-
token = getGhToken();
|
|
653
|
-
} catch {
|
|
654
|
-
process.stderr.write(
|
|
655
|
-
"Error: GitHub token not found. Run 'gh auth login --scopes repo,read:org,project' or set GITHUB_GRAPHQL_TOKEN.\n"
|
|
656
|
-
);
|
|
657
|
-
process.exitCode = 1;
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
const client = createClient(token);
|
|
661
|
-
try {
|
|
662
|
-
const viewer = await validateToken(client);
|
|
663
|
-
const scopeCheck = checkRequiredScopes(viewer.scopes);
|
|
664
|
-
if (!scopeCheck.valid) {
|
|
665
|
-
process.stderr.write(
|
|
666
|
-
`Error: Missing required PAT scopes: ${scopeCheck.missing.join(", ")}
|
|
667
|
-
`
|
|
668
|
-
);
|
|
669
|
-
process.exitCode = 1;
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
} catch {
|
|
673
|
-
process.stderr.write("Error: Invalid GitHub token.\n");
|
|
674
|
-
process.exitCode = 1;
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
let projectDetail;
|
|
678
|
-
try {
|
|
679
|
-
projectDetail = await getProjectDetail(client, projectBindingId);
|
|
680
|
-
} catch (error) {
|
|
681
|
-
if (error instanceof GitHubScopeError) {
|
|
682
|
-
displayScopeError(error);
|
|
683
|
-
} else {
|
|
684
|
-
process.stderr.write(
|
|
685
|
-
`${error instanceof Error ? error.message : "Failed to load linked repositories."}
|
|
686
|
-
`
|
|
687
|
-
);
|
|
688
|
-
}
|
|
689
|
-
process.exitCode = 1;
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
const currentRepos = configuredRepositories(ws);
|
|
693
|
-
const currentMap = new Map(
|
|
694
|
-
currentRepos.map((repo) => [repoKey(repo), repo])
|
|
695
|
-
);
|
|
696
|
-
const linkedMap = new Map(
|
|
697
|
-
projectDetail.linkedRepositories.map((repo) => [
|
|
698
|
-
repoKey(repo),
|
|
699
|
-
repo
|
|
700
|
-
])
|
|
701
|
-
);
|
|
702
|
-
const added = projectDetail.linkedRepositories.filter((repo) => !currentMap.has(repoKey(repo))).map(toRepoConfigEntry);
|
|
703
|
-
const removed = flags.prune ? currentRepos.filter((repo) => !linkedMap.has(repoKey(repo))).map((repo) => ({ ...repo })) : [];
|
|
704
|
-
const unchanged = flags.prune ? currentRepos.filter((repo) => linkedMap.has(repoKey(repo))).map((repo) => {
|
|
705
|
-
const linked = linkedMap.get(repoKey(repo));
|
|
706
|
-
return linked ? toRepoConfigEntry(linked) : { ...repo };
|
|
707
|
-
}) : currentRepos.map((repo) => {
|
|
708
|
-
const linked = linkedMap.get(repoKey(repo));
|
|
709
|
-
return linked ? toRepoConfigEntry(linked) : { ...repo };
|
|
710
|
-
});
|
|
711
|
-
const nextRepositories = buildSyncedRepositories(
|
|
712
|
-
currentRepos,
|
|
713
|
-
linkedMap,
|
|
714
|
-
projectDetail.linkedRepositories,
|
|
715
|
-
flags.prune
|
|
716
|
-
);
|
|
717
|
-
if (!flags.dryRun) {
|
|
718
|
-
const repository = nextRepositories[0];
|
|
719
|
-
const { repository: _legacyRepository, ...trackerSettings } = ws.tracker.settings ?? {};
|
|
720
|
-
await saveProjectConfig(options.configDir, global.activeProject, {
|
|
721
|
-
...ws,
|
|
722
|
-
...repository ? { repository } : { repository: void 0 },
|
|
723
|
-
repositories: nextRepositories,
|
|
724
|
-
tracker: {
|
|
725
|
-
...ws.tracker,
|
|
726
|
-
settings: {
|
|
727
|
-
...trackerSettings,
|
|
728
|
-
...repository ? { repository: `${repository.owner}/${repository.name}` } : {}
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
writeRepoSummary(
|
|
734
|
-
{
|
|
735
|
-
projectId: global.activeProject,
|
|
736
|
-
githubProjectId: projectBindingId,
|
|
737
|
-
dryRun: flags.dryRun,
|
|
738
|
-
prune: flags.prune,
|
|
739
|
-
added,
|
|
740
|
-
removed,
|
|
741
|
-
unchanged,
|
|
742
|
-
repositories: nextRepositories
|
|
743
|
-
},
|
|
744
|
-
options
|
|
745
|
-
);
|
|
746
|
-
}
|
|
747
|
-
export {
|
|
748
|
-
repo_default as default
|
|
749
|
-
};
|