@gh-symphony/cli 0.0.18 → 0.0.20
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 +85 -14
- package/dist/{project-O57C32WF.js → chunk-3AWF54PI.js} +104 -90
- package/dist/{chunk-ZYYY55WB.js → chunk-EKKT5USP.js} +74 -23
- package/dist/{chunk-LZE6YUSB.js → chunk-HZVDTAPS.js} +32 -72
- package/dist/{chunk-5YLETHMR.js → chunk-RN2PACNV.js} +345 -169
- package/dist/{chunk-62L6QQE6.js → chunk-TILHWBP6.js} +277 -1
- package/dist/{config-cmd-AZ7POMAA.js → config-cmd-DNXNL26Z.js} +3 -1
- package/dist/doctor-IYHCFXOZ.js +1126 -0
- package/dist/index.js +144 -18
- package/dist/init-KZT6YNOH.js +33 -0
- package/dist/project-UUVHS3ZR.js +22 -0
- package/dist/{recover-UGUTQTWA.js → recover-5KQI7WH5.js} +2 -2
- package/dist/repo-HDDE7OUI.js +321 -0
- package/dist/{run-5H2R6CHB.js → run-ETC5UTRA.js} +2 -2
- package/dist/setup-VWB7RZUQ.js +431 -0
- package/dist/{start-5JGGJIMC.js → start-ENFLZUI6.js} +4 -4
- package/dist/upgrade-3YNF3VKY.js +165 -0
- package/dist/{version-N7YXKG6V.js → version-NUBTTOG7.js} +1 -1
- package/dist/worker-entry.js +71 -193
- package/dist/workflow-TBIFY5MO.js +497 -0
- package/package.json +2 -2
- package/dist/chunk-7UBUBSMH.js +0 -134
- package/dist/doctor-3QT5CZN4.js +0 -532
- package/dist/init-E432UZ32.js +0 -18
- package/dist/repo-R3XBIVAX.js +0 -121
- package/dist/{chunk-OL73UN2X.js → chunk-M3IFVLQS.js} +77 -77
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
GhAuthError,
|
|
3
4
|
GitHubScopeError,
|
|
4
5
|
checkRequiredScopes,
|
|
5
6
|
createClient,
|
|
7
|
+
getGhTokenWithSource,
|
|
6
8
|
getProjectDetail,
|
|
7
9
|
listUserProjects,
|
|
10
|
+
resolveGitHubAuth,
|
|
8
11
|
validateToken
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import {
|
|
11
|
-
GhAuthError,
|
|
12
|
-
ensureGhAuth,
|
|
13
|
-
getGhToken
|
|
14
|
-
} from "./chunk-7UBUBSMH.js";
|
|
12
|
+
} from "./chunk-TILHWBP6.js";
|
|
15
13
|
import {
|
|
16
14
|
loadGlobalConfig,
|
|
17
15
|
saveGlobalConfig,
|
|
@@ -21,7 +19,7 @@ import {
|
|
|
21
19
|
// src/commands/init.ts
|
|
22
20
|
import * as p from "@clack/prompts";
|
|
23
21
|
import { createHash } from "crypto";
|
|
24
|
-
import { mkdir as mkdir3, rename, writeFile as writeFile3 } from "fs/promises";
|
|
22
|
+
import { mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
|
|
25
23
|
import { basename, dirname as dirname2, join as join3, relative, resolve } from "path";
|
|
26
24
|
|
|
27
25
|
// src/mapping/smart-defaults.ts
|
|
@@ -468,16 +466,6 @@ function generateContextYamlString(context) {
|
|
|
468
466
|
lines.push(` agent_command: ${yamlQuote(context.runtime.agent_command)}`);
|
|
469
467
|
return lines.join("\n") + "\n";
|
|
470
468
|
}
|
|
471
|
-
async function writeContextYaml(outputDir, context) {
|
|
472
|
-
await mkdir(outputDir, { recursive: true });
|
|
473
|
-
const contextPath = `${outputDir}/.gh-symphony/context.yaml`;
|
|
474
|
-
await mkdir(dirname(contextPath), { recursive: true });
|
|
475
|
-
const temporaryPath = `${contextPath}.tmp`;
|
|
476
|
-
const yamlContent = generateContextYamlString(context);
|
|
477
|
-
await writeFile(temporaryPath, yamlContent, "utf8");
|
|
478
|
-
const { rename: rename2 } = await import("fs/promises");
|
|
479
|
-
await rename2(temporaryPath, contextPath);
|
|
480
|
-
}
|
|
481
469
|
function buildContextYaml(params) {
|
|
482
470
|
const columns = params.statusField.options.map((option) => {
|
|
483
471
|
const roleMapping = inferStateRole(option.name);
|
|
@@ -858,7 +846,7 @@ function resolveRoleAction(role) {
|
|
|
858
846
|
}
|
|
859
847
|
|
|
860
848
|
// src/skills/skill-writer.ts
|
|
861
|
-
import { mkdir as mkdir2,
|
|
849
|
+
import { mkdir as mkdir2, readFile as readFile2, rename, writeFile as writeFile2 } from "fs/promises";
|
|
862
850
|
import { join as join2 } from "path";
|
|
863
851
|
function normalizeRuntimeForSkills(runtime) {
|
|
864
852
|
if (runtime === "claude-code" || runtime.includes("claude-code")) {
|
|
@@ -879,44 +867,18 @@ function resolveSkillsDir(repoRoot, runtime) {
|
|
|
879
867
|
}
|
|
880
868
|
return null;
|
|
881
869
|
}
|
|
882
|
-
|
|
883
|
-
const skillDir = join2(skillsDir, template.name);
|
|
884
|
-
const filePath = join2(skillDir, template.fileName);
|
|
885
|
-
if (!options?.overwrite) {
|
|
886
|
-
try {
|
|
887
|
-
await readFile2(filePath, "utf8");
|
|
888
|
-
return { written: false, path: filePath };
|
|
889
|
-
} catch (error) {
|
|
890
|
-
const err = error;
|
|
891
|
-
if (err.code !== "ENOENT") {
|
|
892
|
-
throw error;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
await mkdir2(skillDir, { recursive: true });
|
|
897
|
-
const content = template.generate(context);
|
|
898
|
-
const temporaryPath = `${filePath}.tmp`;
|
|
899
|
-
await writeFile2(temporaryPath, content, "utf8");
|
|
900
|
-
const { rename: rename2 } = await import("fs/promises");
|
|
901
|
-
await rename2(temporaryPath, filePath);
|
|
902
|
-
return { written: true, path: filePath };
|
|
903
|
-
}
|
|
904
|
-
async function writeAllSkills(repoRoot, runtime, templates, context, options) {
|
|
870
|
+
function buildSkillFilePlans(repoRoot, runtime, templates, context) {
|
|
905
871
|
const skillsDir = resolveSkillsDir(repoRoot, runtime);
|
|
906
872
|
if (!skillsDir) {
|
|
907
|
-
return {
|
|
908
|
-
}
|
|
909
|
-
const written = [];
|
|
910
|
-
const skipped = [];
|
|
911
|
-
for (const template of templates) {
|
|
912
|
-
const result = await writeSkillFile(skillsDir, template, context, options);
|
|
913
|
-
if (result.written) {
|
|
914
|
-
written.push(result.path);
|
|
915
|
-
} else {
|
|
916
|
-
skipped.push(result.path);
|
|
917
|
-
}
|
|
873
|
+
return { skillsDir: null, files: [] };
|
|
918
874
|
}
|
|
919
|
-
return {
|
|
875
|
+
return {
|
|
876
|
+
skillsDir,
|
|
877
|
+
files: templates.map((template) => ({
|
|
878
|
+
path: join2(skillsDir, template.name, template.fileName),
|
|
879
|
+
content: template.generate(context)
|
|
880
|
+
}))
|
|
881
|
+
};
|
|
920
882
|
}
|
|
921
883
|
|
|
922
884
|
// src/skills/templates/document.ts
|
|
@@ -1454,6 +1416,7 @@ async function abortIfCancelled(input) {
|
|
|
1454
1416
|
}
|
|
1455
1417
|
function parseInitFlags(args) {
|
|
1456
1418
|
const flags = {
|
|
1419
|
+
dryRun: false,
|
|
1457
1420
|
nonInteractive: false,
|
|
1458
1421
|
skipSkills: false,
|
|
1459
1422
|
skipContext: false
|
|
@@ -1462,6 +1425,9 @@ function parseInitFlags(args) {
|
|
|
1462
1425
|
const arg = args[i];
|
|
1463
1426
|
const next = args[i + 1];
|
|
1464
1427
|
switch (arg) {
|
|
1428
|
+
case "--dry-run":
|
|
1429
|
+
flags.dryRun = true;
|
|
1430
|
+
break;
|
|
1465
1431
|
case "--non-interactive":
|
|
1466
1432
|
flags.nonInteractive = true;
|
|
1467
1433
|
break;
|
|
@@ -1489,29 +1455,145 @@ var handler = async (args, options) => {
|
|
|
1489
1455
|
await runNonInteractive(flags, options);
|
|
1490
1456
|
return;
|
|
1491
1457
|
}
|
|
1492
|
-
await runInteractive(options);
|
|
1458
|
+
await runInteractive(flags, options);
|
|
1493
1459
|
};
|
|
1494
1460
|
var init_default = handler;
|
|
1495
|
-
async function
|
|
1461
|
+
async function resolveChangeStatus(path, content, mode) {
|
|
1462
|
+
try {
|
|
1463
|
+
const existing = await readFile3(path, "utf8");
|
|
1464
|
+
if (mode === "create-only") {
|
|
1465
|
+
return "unchanged";
|
|
1466
|
+
}
|
|
1467
|
+
return existing === content ? "unchanged" : "update";
|
|
1468
|
+
} catch (error) {
|
|
1469
|
+
const err = error;
|
|
1470
|
+
if (err.code === "ENOENT") {
|
|
1471
|
+
return "create";
|
|
1472
|
+
}
|
|
1473
|
+
throw error;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
async function planFileChange(input) {
|
|
1477
|
+
return {
|
|
1478
|
+
...input,
|
|
1479
|
+
status: await resolveChangeStatus(input.path, input.content, input.mode)
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
async function writePlannedFile(file) {
|
|
1483
|
+
if (file.status === "unchanged") {
|
|
1484
|
+
return false;
|
|
1485
|
+
}
|
|
1486
|
+
await mkdir3(dirname2(file.path), { recursive: true });
|
|
1487
|
+
const temporaryPath = `${file.path}.tmp`;
|
|
1488
|
+
await writeFile3(temporaryPath, file.content, "utf8");
|
|
1489
|
+
await rename2(temporaryPath, file.path);
|
|
1490
|
+
return true;
|
|
1491
|
+
}
|
|
1492
|
+
function resolveStatusField(projectDetail) {
|
|
1493
|
+
return projectDetail.statusFields.find((f) => f.name.toLowerCase() === "status") ?? projectDetail.statusFields[0] ?? null;
|
|
1494
|
+
}
|
|
1495
|
+
function buildAutomaticStateMappings(statusField) {
|
|
1496
|
+
const mappings = {};
|
|
1497
|
+
for (const mapping of inferAllStateRoles(statusField.options.map((o) => o.name))) {
|
|
1498
|
+
if (mapping.role) {
|
|
1499
|
+
mappings[mapping.columnName] = { role: mapping.role };
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return mappings;
|
|
1503
|
+
}
|
|
1504
|
+
async function promptStateMappings(statusField, options) {
|
|
1505
|
+
const mappings = {};
|
|
1506
|
+
const inferred = inferAllStateRoles(statusField.options.map((o) => o.name));
|
|
1507
|
+
p.log.info(
|
|
1508
|
+
`Found ${statusField.options.length} status columns on field "${statusField.name}".`
|
|
1509
|
+
);
|
|
1510
|
+
for (const mapping of inferred) {
|
|
1511
|
+
const roleOptions = [
|
|
1512
|
+
{ value: "active", label: "Active (agent works on this)" },
|
|
1513
|
+
{ value: "wait", label: "Wait (human review / hold)" },
|
|
1514
|
+
{ value: "terminal", label: "Terminal (completed)" }
|
|
1515
|
+
];
|
|
1516
|
+
const defaultRole = mapping.role ?? "wait";
|
|
1517
|
+
const sortedOptions = [
|
|
1518
|
+
roleOptions.find((o) => o.value === defaultRole),
|
|
1519
|
+
...roleOptions.filter((o) => o.value !== defaultRole)
|
|
1520
|
+
];
|
|
1521
|
+
const selectedRole = await abortIfCancelled(
|
|
1522
|
+
p.select({
|
|
1523
|
+
message: `${options?.stepLabel ?? "Step 2/2"} \u2014 Map column "${mapping.columnName}":${mapping.confidence === "high" ? " (auto-detected)" : ""}`,
|
|
1524
|
+
options: sortedOptions
|
|
1525
|
+
})
|
|
1526
|
+
);
|
|
1527
|
+
mappings[mapping.columnName] = { role: selectedRole };
|
|
1528
|
+
}
|
|
1529
|
+
return mappings;
|
|
1530
|
+
}
|
|
1531
|
+
async function planWorkflowArtifacts(opts) {
|
|
1532
|
+
const workflowMd = generateWorkflowMarkdown({
|
|
1533
|
+
projectId: opts.projectDetail.id,
|
|
1534
|
+
stateFieldName: opts.statusField.name,
|
|
1535
|
+
mappings: opts.mappings,
|
|
1536
|
+
lifecycle: toWorkflowLifecycleConfig(opts.statusField.name, opts.mappings),
|
|
1537
|
+
runtime: opts.runtime
|
|
1538
|
+
});
|
|
1539
|
+
const workflowPlan = await planFileChange({
|
|
1540
|
+
path: opts.outputPath,
|
|
1541
|
+
label: "WORKFLOW.md",
|
|
1542
|
+
content: workflowMd,
|
|
1543
|
+
mode: "overwrite"
|
|
1544
|
+
});
|
|
1545
|
+
const ecosystemPlan = await planEcosystem({
|
|
1546
|
+
cwd: opts.cwd,
|
|
1547
|
+
projectDetail: opts.projectDetail,
|
|
1548
|
+
statusField: opts.statusField,
|
|
1549
|
+
runtime: opts.runtime,
|
|
1550
|
+
skipSkills: opts.skipSkills,
|
|
1551
|
+
skipContext: opts.skipContext
|
|
1552
|
+
});
|
|
1553
|
+
return {
|
|
1554
|
+
outputPath: opts.outputPath,
|
|
1555
|
+
workflowMd,
|
|
1556
|
+
workflowPlan,
|
|
1557
|
+
ecosystemPlan
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
async function writeWorkflowPlan(workflowPlan) {
|
|
1561
|
+
return writePlannedFile(workflowPlan);
|
|
1562
|
+
}
|
|
1563
|
+
function summarizeEnvironment(env) {
|
|
1564
|
+
return [
|
|
1565
|
+
`Package manager ${env.packageManager ?? "none"}${env.lockfile ? ` (${env.lockfile})` : ""}`,
|
|
1566
|
+
`Scripts test=${env.testCommand ?? "none"} | lint=${env.lintCommand ?? "none"} | build=${env.buildCommand ?? "none"}`,
|
|
1567
|
+
`CI ${env.ciPlatform ?? "none"}`,
|
|
1568
|
+
`Monorepo ${env.monorepo ? "yes" : "no"}`,
|
|
1569
|
+
`Existing skills ${env.existingSkills.length === 0 ? "none" : env.existingSkills.join(", ")}`
|
|
1570
|
+
];
|
|
1571
|
+
}
|
|
1572
|
+
async function planEcosystem(opts) {
|
|
1496
1573
|
const { cwd, projectDetail, statusField, runtime, skipSkills, skipContext } = opts;
|
|
1497
1574
|
const ghSymphonyDir = join3(cwd, ".gh-symphony");
|
|
1498
|
-
|
|
1499
|
-
const
|
|
1500
|
-
let contextYamlWritten = false;
|
|
1575
|
+
const environment = await detectEnvironment(cwd);
|
|
1576
|
+
const files = [];
|
|
1501
1577
|
if (!skipContext) {
|
|
1502
1578
|
const contextYaml = buildContextYaml({
|
|
1503
1579
|
projectDetail,
|
|
1504
1580
|
statusField,
|
|
1505
|
-
detectedEnvironment:
|
|
1581
|
+
detectedEnvironment: environment,
|
|
1506
1582
|
runtime: {
|
|
1507
1583
|
agent: runtime,
|
|
1508
1584
|
agent_command: runtime === "codex" ? "bash -lc codex app-server" : runtime === "claude-code" ? "bash -lc claude-code" : runtime
|
|
1509
1585
|
}
|
|
1510
1586
|
});
|
|
1511
|
-
|
|
1512
|
-
|
|
1587
|
+
files.push(
|
|
1588
|
+
await planFileChange({
|
|
1589
|
+
path: join3(ghSymphonyDir, "context.yaml"),
|
|
1590
|
+
label: "Context metadata",
|
|
1591
|
+
content: generateContextYamlString(contextYaml),
|
|
1592
|
+
mode: "overwrite"
|
|
1593
|
+
})
|
|
1594
|
+
);
|
|
1513
1595
|
}
|
|
1514
|
-
const
|
|
1596
|
+
const referenceWorkflow = generateReferenceWorkflow({
|
|
1515
1597
|
runtime,
|
|
1516
1598
|
statusColumns: statusField.options.map((o) => ({
|
|
1517
1599
|
name: o.name,
|
|
@@ -1519,43 +1601,99 @@ async function writeEcosystem(opts) {
|
|
|
1519
1601
|
})),
|
|
1520
1602
|
projectId: projectDetail.id
|
|
1521
1603
|
});
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1604
|
+
files.push(
|
|
1605
|
+
await planFileChange({
|
|
1606
|
+
path: join3(ghSymphonyDir, "reference-workflow.md"),
|
|
1607
|
+
label: "Reference workflow",
|
|
1608
|
+
content: referenceWorkflow,
|
|
1609
|
+
mode: "overwrite"
|
|
1610
|
+
})
|
|
1611
|
+
);
|
|
1612
|
+
const skillsDir = skipSkills ? null : resolveSkillsDir(cwd, runtime);
|
|
1529
1613
|
if (!skipSkills && skillsDir) {
|
|
1530
|
-
const
|
|
1614
|
+
const { files: plannedSkills } = buildSkillFilePlans(
|
|
1615
|
+
cwd,
|
|
1531
1616
|
runtime,
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1617
|
+
ALL_SKILL_TEMPLATES,
|
|
1618
|
+
{
|
|
1619
|
+
runtime,
|
|
1620
|
+
projectId: projectDetail.id,
|
|
1621
|
+
githubProjectTitle: projectDetail.title,
|
|
1622
|
+
repositories: projectDetail.linkedRepositories.map((r) => ({
|
|
1623
|
+
owner: r.owner,
|
|
1624
|
+
name: r.name
|
|
1625
|
+
})),
|
|
1626
|
+
statusColumns: statusField.options.map((o) => ({
|
|
1627
|
+
id: o.id,
|
|
1628
|
+
name: o.name,
|
|
1629
|
+
role: null
|
|
1630
|
+
})),
|
|
1631
|
+
statusFieldId: statusField.id,
|
|
1632
|
+
contextYamlPath: ".gh-symphony/context.yaml",
|
|
1633
|
+
referenceWorkflowPath: ".gh-symphony/reference-workflow.md"
|
|
1634
|
+
}
|
|
1635
|
+
);
|
|
1636
|
+
for (const plannedSkill of plannedSkills) {
|
|
1637
|
+
files.push(
|
|
1638
|
+
await planFileChange({
|
|
1639
|
+
path: plannedSkill.path,
|
|
1640
|
+
label: `Skill ${basename(dirname2(plannedSkill.path))}`,
|
|
1641
|
+
content: plannedSkill.content,
|
|
1642
|
+
mode: "create-only"
|
|
1643
|
+
})
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1549
1646
|
}
|
|
1550
1647
|
return {
|
|
1551
1648
|
projectId: projectDetail.id,
|
|
1552
1649
|
githubProjectTitle: projectDetail.title,
|
|
1553
1650
|
runtime,
|
|
1554
1651
|
skillsDir,
|
|
1652
|
+
environment,
|
|
1653
|
+
files
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
async function writeEcosystem(opts) {
|
|
1657
|
+
const plan = await planEcosystem(opts);
|
|
1658
|
+
await mkdir3(join3(opts.cwd, ".gh-symphony"), { recursive: true });
|
|
1659
|
+
const contextYamlPath = join3(opts.cwd, ".gh-symphony", "context.yaml");
|
|
1660
|
+
const referenceWorkflowPath = join3(
|
|
1661
|
+
opts.cwd,
|
|
1662
|
+
".gh-symphony",
|
|
1663
|
+
"reference-workflow.md"
|
|
1664
|
+
);
|
|
1665
|
+
let contextYamlWritten = false;
|
|
1666
|
+
let referenceWorkflowWritten = false;
|
|
1667
|
+
const skillsWritten = [];
|
|
1668
|
+
const skillsSkipped = [];
|
|
1669
|
+
for (const file of plan.files) {
|
|
1670
|
+
const written = await writePlannedFile(file);
|
|
1671
|
+
if (file.path === contextYamlPath) {
|
|
1672
|
+
contextYamlWritten = written;
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
if (file.path === referenceWorkflowPath) {
|
|
1676
|
+
referenceWorkflowWritten = written;
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1679
|
+
if (file.label.startsWith("Skill ")) {
|
|
1680
|
+
const skillName = basename(dirname2(file.path));
|
|
1681
|
+
if (written) {
|
|
1682
|
+
skillsWritten.push(skillName);
|
|
1683
|
+
} else {
|
|
1684
|
+
skillsSkipped.push(skillName);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
return {
|
|
1689
|
+
projectId: plan.projectId,
|
|
1690
|
+
githubProjectTitle: plan.githubProjectTitle,
|
|
1691
|
+
runtime: plan.runtime,
|
|
1692
|
+
skillsDir: plan.skillsDir,
|
|
1555
1693
|
contextYamlWritten,
|
|
1556
|
-
referenceWorkflowWritten
|
|
1557
|
-
skillsWritten,
|
|
1558
|
-
skillsSkipped
|
|
1694
|
+
referenceWorkflowWritten,
|
|
1695
|
+
skillsWritten: skillsWritten.sort(),
|
|
1696
|
+
skillsSkipped: skillsSkipped.sort()
|
|
1559
1697
|
};
|
|
1560
1698
|
}
|
|
1561
1699
|
function printEcosystemSummary(result, workflowPath, opts) {
|
|
@@ -1598,10 +1736,65 @@ function printEcosystemSummary(result, workflowPath, opts) {
|
|
|
1598
1736
|
process.stdout.write(lines.map((l) => ` ${l}`).join("\n") + "\n");
|
|
1599
1737
|
}
|
|
1600
1738
|
}
|
|
1739
|
+
function renderDryRunPreview(workflowPath, workflowPlan, ecosystemPlan) {
|
|
1740
|
+
const cwd = process.cwd();
|
|
1741
|
+
const relWorkflow = relative(cwd, workflowPath) || "WORKFLOW.md";
|
|
1742
|
+
const statusIcon = {
|
|
1743
|
+
create: "+",
|
|
1744
|
+
update: "~",
|
|
1745
|
+
unchanged: "="
|
|
1746
|
+
};
|
|
1747
|
+
const lines = [];
|
|
1748
|
+
lines.push("Init dry-run preview");
|
|
1749
|
+
lines.push(
|
|
1750
|
+
`GitHub Project ${ecosystemPlan.githubProjectTitle} (${ecosystemPlan.projectId})`
|
|
1751
|
+
);
|
|
1752
|
+
lines.push(`Runtime ${ecosystemPlan.runtime}`);
|
|
1753
|
+
lines.push("");
|
|
1754
|
+
lines.push("Planned file changes");
|
|
1755
|
+
lines.push(
|
|
1756
|
+
` ${statusIcon[workflowPlan.status]} ${workflowPlan.status.padEnd(9)} WORKFLOW.md ${relWorkflow}`
|
|
1757
|
+
);
|
|
1758
|
+
for (const file of ecosystemPlan.files) {
|
|
1759
|
+
const relPath = relative(cwd, file.path) || file.path;
|
|
1760
|
+
lines.push(
|
|
1761
|
+
` ${statusIcon[file.status]} ${file.status.padEnd(9)} ${file.label.padEnd(36)} ${relPath}`
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
lines.push("");
|
|
1765
|
+
lines.push("Detected environment inputs");
|
|
1766
|
+
for (const line of summarizeEnvironment(ecosystemPlan.environment)) {
|
|
1767
|
+
lines.push(` ${line}`);
|
|
1768
|
+
}
|
|
1769
|
+
lines.push("");
|
|
1770
|
+
lines.push("Dry run only. No files were written.");
|
|
1771
|
+
return lines.join("\n") + "\n";
|
|
1772
|
+
}
|
|
1773
|
+
function buildDryRunJsonResult(workflowPath, workflowPlan, ecosystemPlan) {
|
|
1774
|
+
return {
|
|
1775
|
+
dryRun: true,
|
|
1776
|
+
output: workflowPath,
|
|
1777
|
+
projectId: ecosystemPlan.projectId,
|
|
1778
|
+
githubProjectTitle: ecosystemPlan.githubProjectTitle,
|
|
1779
|
+
runtime: ecosystemPlan.runtime,
|
|
1780
|
+
files: [workflowPlan, ...ecosystemPlan.files].map((file) => ({
|
|
1781
|
+
path: file.path,
|
|
1782
|
+
label: file.label,
|
|
1783
|
+
status: file.status,
|
|
1784
|
+
mode: file.mode
|
|
1785
|
+
})),
|
|
1786
|
+
environment: ecosystemPlan.environment
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
function printDryRunPreview(workflowPath, workflowPlan, ecosystemPlan) {
|
|
1790
|
+
process.stdout.write(
|
|
1791
|
+
renderDryRunPreview(workflowPath, workflowPlan, ecosystemPlan)
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1601
1794
|
async function runNonInteractive(flags, options) {
|
|
1602
1795
|
let token;
|
|
1603
1796
|
try {
|
|
1604
|
-
token =
|
|
1797
|
+
token = getGhTokenWithSource().token;
|
|
1605
1798
|
} catch {
|
|
1606
1799
|
process.stderr.write(
|
|
1607
1800
|
"Error: GitHub token not found. Run 'gh auth login --scopes repo,read:org,project' or set GITHUB_GRAPHQL_TOKEN.\n"
|
|
@@ -1649,20 +1842,13 @@ async function runNonInteractive(flags, options) {
|
|
|
1649
1842
|
process.exitCode = 1;
|
|
1650
1843
|
return;
|
|
1651
1844
|
}
|
|
1652
|
-
const statusField = githubProject
|
|
1845
|
+
const statusField = resolveStatusField(githubProject);
|
|
1653
1846
|
if (!statusField) {
|
|
1654
1847
|
process.stderr.write("Error: No status field found on the project.\n");
|
|
1655
1848
|
process.exitCode = 1;
|
|
1656
1849
|
return;
|
|
1657
1850
|
}
|
|
1658
|
-
const
|
|
1659
|
-
const inferred = inferAllStateRoles(columnNames);
|
|
1660
|
-
const mappings = {};
|
|
1661
|
-
for (const mapping of inferred) {
|
|
1662
|
-
if (mapping.role) {
|
|
1663
|
-
mappings[mapping.columnName] = { role: mapping.role };
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1851
|
+
const mappings = buildAutomaticStateMappings(statusField);
|
|
1666
1852
|
const validation = validateStateMapping(mappings);
|
|
1667
1853
|
if (!validation.valid) {
|
|
1668
1854
|
process.stderr.write(
|
|
@@ -1673,16 +1859,30 @@ Run without --non-interactive for manual mapping.
|
|
|
1673
1859
|
process.exitCode = 1;
|
|
1674
1860
|
return;
|
|
1675
1861
|
}
|
|
1676
|
-
const lifecycleConfig = toWorkflowLifecycleConfig(statusField.name, mappings);
|
|
1677
1862
|
const outputPath = resolve(flags.output ?? "WORKFLOW.md");
|
|
1678
|
-
const
|
|
1679
|
-
|
|
1680
|
-
|
|
1863
|
+
const { workflowPlan, ecosystemPlan } = await planWorkflowArtifacts({
|
|
1864
|
+
cwd: process.cwd(),
|
|
1865
|
+
outputPath,
|
|
1866
|
+
projectDetail: githubProject,
|
|
1867
|
+
statusField,
|
|
1681
1868
|
mappings,
|
|
1682
|
-
|
|
1683
|
-
|
|
1869
|
+
runtime: "codex",
|
|
1870
|
+
skipSkills: flags.skipSkills,
|
|
1871
|
+
skipContext: flags.skipContext
|
|
1684
1872
|
});
|
|
1685
|
-
|
|
1873
|
+
if (flags.dryRun) {
|
|
1874
|
+
if (options.json) {
|
|
1875
|
+
process.stdout.write(
|
|
1876
|
+
JSON.stringify(
|
|
1877
|
+
buildDryRunJsonResult(outputPath, workflowPlan, ecosystemPlan)
|
|
1878
|
+
) + "\n"
|
|
1879
|
+
);
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
printDryRunPreview(outputPath, workflowPlan, ecosystemPlan);
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
await writeWorkflowPlan(workflowPlan);
|
|
1686
1886
|
const ecosystemResult = await writeEcosystem({
|
|
1687
1887
|
cwd: process.cwd(),
|
|
1688
1888
|
projectDetail: githubProject,
|
|
@@ -1693,7 +1893,7 @@ Run without --non-interactive for manual mapping.
|
|
|
1693
1893
|
});
|
|
1694
1894
|
if (options.json) {
|
|
1695
1895
|
process.stdout.write(
|
|
1696
|
-
JSON.stringify({ output: outputPath, status:
|
|
1896
|
+
JSON.stringify({ output: outputPath, status: workflowPlan.status }) + "\n"
|
|
1697
1897
|
);
|
|
1698
1898
|
} else {
|
|
1699
1899
|
printEcosystemSummary(ecosystemResult, outputPath, {
|
|
@@ -1702,36 +1902,23 @@ Run without --non-interactive for manual mapping.
|
|
|
1702
1902
|
});
|
|
1703
1903
|
}
|
|
1704
1904
|
}
|
|
1705
|
-
async function runInteractive(options) {
|
|
1905
|
+
async function runInteractive(flags, options) {
|
|
1706
1906
|
p.intro("gh-symphony \u2014 WORKFLOW.md Setup");
|
|
1707
|
-
await runInteractiveStandalone(options);
|
|
1907
|
+
await runInteractiveStandalone(flags, options);
|
|
1708
1908
|
}
|
|
1709
|
-
async function runInteractiveStandalone(_options) {
|
|
1909
|
+
async function runInteractiveStandalone(flags, _options) {
|
|
1710
1910
|
const s1 = p.spinner();
|
|
1711
|
-
s1.start("Checking
|
|
1911
|
+
s1.start("Checking GitHub authentication...");
|
|
1712
1912
|
let client;
|
|
1713
1913
|
try {
|
|
1714
|
-
const
|
|
1715
|
-
|
|
1716
|
-
|
|
1914
|
+
const auth = await resolveGitHubAuth();
|
|
1915
|
+
const sourceLabel = auth.source === "env" ? "GITHUB_GRAPHQL_TOKEN" : "gh CLI";
|
|
1916
|
+
client = createClient(auth.token);
|
|
1917
|
+
s1.stop(`Authenticated via ${sourceLabel} as ${auth.login}`);
|
|
1717
1918
|
} catch (error) {
|
|
1718
1919
|
s1.stop("Authentication failed.");
|
|
1719
1920
|
if (error instanceof GhAuthError) {
|
|
1720
|
-
|
|
1721
|
-
p.log.error(
|
|
1722
|
-
"gh CLI\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. https://cli.github.com \uC5D0\uC11C \uC124\uCE58\uD558\uC138\uC694."
|
|
1723
|
-
);
|
|
1724
|
-
} else if (error.code === "not_authenticated") {
|
|
1725
|
-
p.log.error(
|
|
1726
|
-
"gh auth login --scopes repo,read:org,project \uB97C \uC2E4\uD589\uD558\uC138\uC694."
|
|
1727
|
-
);
|
|
1728
|
-
} else if (error.code === "missing_scopes") {
|
|
1729
|
-
p.log.error(
|
|
1730
|
-
"gh auth refresh --scopes repo,read:org,project \uB97C \uC2E4\uD589\uD558\uC138\uC694."
|
|
1731
|
-
);
|
|
1732
|
-
} else {
|
|
1733
|
-
p.log.error(error.message);
|
|
1734
|
-
}
|
|
1921
|
+
p.log.error(error.message);
|
|
1735
1922
|
} else {
|
|
1736
1923
|
p.log.error(error instanceof Error ? error.message : "Unknown error");
|
|
1737
1924
|
}
|
|
@@ -1749,7 +1936,7 @@ async function runInteractiveStandalone(_options) {
|
|
|
1749
1936
|
} catch (error) {
|
|
1750
1937
|
s2.stop("Failed to load projects.");
|
|
1751
1938
|
if (error instanceof GitHubScopeError) {
|
|
1752
|
-
displayScopeError(error, "gh-symphony init");
|
|
1939
|
+
displayScopeError(error, "gh-symphony workflow init");
|
|
1753
1940
|
} else {
|
|
1754
1941
|
p.log.error(error instanceof Error ? error.message : "Unknown error");
|
|
1755
1942
|
}
|
|
@@ -1786,7 +1973,7 @@ async function runInteractiveStandalone(_options) {
|
|
|
1786
1973
|
process.exitCode = 1;
|
|
1787
1974
|
return;
|
|
1788
1975
|
}
|
|
1789
|
-
const statusField = projectDetail
|
|
1976
|
+
const statusField = resolveStatusField(projectDetail);
|
|
1790
1977
|
if (!statusField) {
|
|
1791
1978
|
p.log.error(
|
|
1792
1979
|
"No status field found on the project. The project needs a single-select 'Status' field."
|
|
@@ -1794,33 +1981,7 @@ async function runInteractiveStandalone(_options) {
|
|
|
1794
1981
|
process.exitCode = 1;
|
|
1795
1982
|
return;
|
|
1796
1983
|
}
|
|
1797
|
-
const
|
|
1798
|
-
const inferred = inferAllStateRoles(columnNames);
|
|
1799
|
-
p.log.info(
|
|
1800
|
-
`Found ${columnNames.length} status columns on field "${statusField.name}".`
|
|
1801
|
-
);
|
|
1802
|
-
const mappings = {};
|
|
1803
|
-
for (const mapping of inferred) {
|
|
1804
|
-
const roleOptions = [
|
|
1805
|
-
{ value: "active", label: "Active (agent works on this)" },
|
|
1806
|
-
{ value: "wait", label: "Wait (human review / hold)" },
|
|
1807
|
-
{ value: "terminal", label: "Terminal (completed)" }
|
|
1808
|
-
];
|
|
1809
|
-
const defaultRole = mapping.role ?? "wait";
|
|
1810
|
-
const sortedOptions = [
|
|
1811
|
-
roleOptions.find((o) => o.value === defaultRole),
|
|
1812
|
-
...roleOptions.filter((o) => o.value !== defaultRole)
|
|
1813
|
-
];
|
|
1814
|
-
const selectedRole = await abortIfCancelled(
|
|
1815
|
-
p.select({
|
|
1816
|
-
message: `Step 2/2 \u2014 Map column "${mapping.columnName}":${mapping.confidence === "high" ? " (auto-detected)" : ""}`,
|
|
1817
|
-
options: sortedOptions
|
|
1818
|
-
})
|
|
1819
|
-
);
|
|
1820
|
-
if (selectedRole !== "skip") {
|
|
1821
|
-
mappings[mapping.columnName] = { role: selectedRole };
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1984
|
+
const mappings = await promptStateMappings(statusField);
|
|
1824
1985
|
const validation = validateStateMapping(mappings);
|
|
1825
1986
|
if (!validation.valid) {
|
|
1826
1987
|
p.log.error("Mapping validation failed:");
|
|
@@ -1833,23 +1994,29 @@ async function runInteractiveStandalone(_options) {
|
|
|
1833
1994
|
for (const warn of validation.warnings) {
|
|
1834
1995
|
p.log.warn(` \u26A0 ${warn}`);
|
|
1835
1996
|
}
|
|
1836
|
-
const
|
|
1837
|
-
const
|
|
1838
|
-
|
|
1839
|
-
|
|
1997
|
+
const outputPath = resolve(flags.output ?? "WORKFLOW.md");
|
|
1998
|
+
const { workflowPlan, ecosystemPlan } = await planWorkflowArtifacts({
|
|
1999
|
+
cwd: process.cwd(),
|
|
2000
|
+
outputPath,
|
|
2001
|
+
projectDetail,
|
|
2002
|
+
statusField,
|
|
1840
2003
|
mappings,
|
|
1841
|
-
|
|
1842
|
-
|
|
2004
|
+
runtime: "codex",
|
|
2005
|
+
skipSkills: flags.skipSkills,
|
|
2006
|
+
skipContext: flags.skipContext
|
|
1843
2007
|
});
|
|
1844
|
-
|
|
1845
|
-
|
|
2008
|
+
if (flags.dryRun) {
|
|
2009
|
+
printDryRunPreview(outputPath, workflowPlan, ecosystemPlan);
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
await writeWorkflowPlan(workflowPlan);
|
|
1846
2013
|
const ecosystemResult = await writeEcosystem({
|
|
1847
2014
|
cwd: process.cwd(),
|
|
1848
2015
|
projectDetail,
|
|
1849
2016
|
statusField,
|
|
1850
2017
|
runtime: "codex",
|
|
1851
|
-
skipSkills:
|
|
1852
|
-
skipContext:
|
|
2018
|
+
skipSkills: flags.skipSkills,
|
|
2019
|
+
skipContext: flags.skipContext
|
|
1853
2020
|
});
|
|
1854
2021
|
printEcosystemSummary(ecosystemResult, outputPath, {
|
|
1855
2022
|
interactive: true,
|
|
@@ -1893,9 +2060,18 @@ function generateProjectId(githubProjectTitle, uniqueKey) {
|
|
|
1893
2060
|
}
|
|
1894
2061
|
|
|
1895
2062
|
export {
|
|
2063
|
+
validateStateMapping,
|
|
1896
2064
|
abortIfCancelled,
|
|
1897
2065
|
init_default,
|
|
2066
|
+
resolveStatusField,
|
|
2067
|
+
buildAutomaticStateMappings,
|
|
2068
|
+
promptStateMappings,
|
|
2069
|
+
planWorkflowArtifacts,
|
|
2070
|
+
writeWorkflowPlan,
|
|
2071
|
+
planEcosystem,
|
|
1898
2072
|
writeEcosystem,
|
|
2073
|
+
renderDryRunPreview,
|
|
2074
|
+
buildDryRunJsonResult,
|
|
1899
2075
|
writeConfig,
|
|
1900
2076
|
generateProjectId
|
|
1901
2077
|
};
|