@gh-symphony/cli 0.0.1 → 0.0.2
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/dist/ansi.d.ts +15 -0
- package/dist/ansi.js +53 -0
- package/dist/commands/config-cmd.js +11 -27
- package/dist/commands/help.js +14 -6
- package/dist/commands/init.d.ts +30 -7
- package/dist/commands/init.js +421 -284
- package/dist/commands/logs.js +4 -4
- package/dist/commands/project.js +34 -34
- package/dist/commands/recover.js +14 -14
- package/dist/commands/repo.js +13 -13
- package/dist/commands/run.js +16 -16
- package/dist/commands/start.js +61 -37
- package/dist/commands/status.js +60 -63
- package/dist/commands/tenant.d.ts +3 -0
- package/dist/commands/tenant.js +402 -0
- package/dist/config.d.ts +20 -19
- package/dist/config.js +17 -17
- package/dist/context/context-types.d.ts +36 -0
- package/dist/context/context-types.js +1 -0
- package/dist/context/generate-context-yaml.d.ts +15 -0
- package/dist/context/generate-context-yaml.js +129 -0
- package/dist/dashboard/renderer.d.ts +9 -0
- package/dist/dashboard/renderer.js +220 -0
- package/dist/detection/environment-detector.d.ts +11 -0
- package/dist/detection/environment-detector.js +140 -0
- package/dist/github/client.d.ts +11 -0
- package/dist/github/client.js +59 -11
- package/dist/github/gh-auth.d.ts +34 -0
- package/dist/github/gh-auth.js +110 -0
- package/dist/index.js +1 -0
- package/dist/mapping/smart-defaults.d.ts +9 -25
- package/dist/mapping/smart-defaults.js +52 -125
- package/dist/orchestrator-runtime.d.ts +4 -4
- package/dist/orchestrator-runtime.js +27 -12
- package/dist/skills/skill-writer.d.ts +14 -0
- package/dist/skills/skill-writer.js +62 -0
- package/dist/skills/templates/commit.d.ts +2 -0
- package/dist/skills/templates/commit.js +45 -0
- package/dist/skills/templates/document.d.ts +7 -0
- package/dist/skills/templates/document.js +16 -0
- package/dist/skills/templates/gh-project.d.ts +2 -0
- package/dist/skills/templates/gh-project.js +88 -0
- package/dist/skills/templates/gh-symphony.d.ts +2 -0
- package/dist/skills/templates/gh-symphony.js +125 -0
- package/dist/skills/templates/index.d.ts +8 -0
- package/dist/skills/templates/index.js +28 -0
- package/dist/skills/templates/land.d.ts +2 -0
- package/dist/skills/templates/land.js +59 -0
- package/dist/skills/templates/pull.d.ts +2 -0
- package/dist/skills/templates/pull.js +41 -0
- package/dist/skills/templates/push.d.ts +2 -0
- package/dist/skills/templates/push.js +36 -0
- package/dist/skills/types.d.ts +23 -0
- package/dist/skills/types.js +1 -0
- package/dist/workflow/generate-reference-workflow.d.ts +9 -0
- package/dist/workflow/generate-reference-workflow.js +261 -0
- package/dist/workflow/generate-workflow-md.d.ts +12 -0
- package/dist/workflow/generate-workflow-md.js +134 -0
- package/package.json +5 -4
package/dist/config.js
CHANGED
|
@@ -11,14 +11,14 @@ export function resolveConfigDir(override) {
|
|
|
11
11
|
export function configFilePath(configDir) {
|
|
12
12
|
return join(configDir, CONFIG_FILE);
|
|
13
13
|
}
|
|
14
|
-
export function
|
|
15
|
-
return join(configDir, "
|
|
14
|
+
export function tenantConfigDir(configDir, tenantId) {
|
|
15
|
+
return join(configDir, "tenants", tenantId);
|
|
16
16
|
}
|
|
17
|
-
export function
|
|
18
|
-
return join(
|
|
17
|
+
export function tenantConfigPath(configDir, tenantId) {
|
|
18
|
+
return join(tenantConfigDir(configDir, tenantId), "tenant.json");
|
|
19
19
|
}
|
|
20
|
-
export function workflowMappingPath(configDir,
|
|
21
|
-
return join(
|
|
20
|
+
export function workflowMappingPath(configDir, tenantId) {
|
|
21
|
+
return join(tenantConfigDir(configDir, tenantId), "workflow-mapping.json");
|
|
22
22
|
}
|
|
23
23
|
export function daemonPidPath(configDir) {
|
|
24
24
|
return join(configDir, DAEMON_PID_FILE);
|
|
@@ -35,24 +35,24 @@ export async function loadGlobalConfig(configDir) {
|
|
|
35
35
|
export async function saveGlobalConfig(configDir, config) {
|
|
36
36
|
await writeJsonFile(configFilePath(configDir), config);
|
|
37
37
|
}
|
|
38
|
-
export async function
|
|
39
|
-
return readJsonFile(
|
|
38
|
+
export async function loadTenantConfig(configDir, tenantId) {
|
|
39
|
+
return readJsonFile(tenantConfigPath(configDir, tenantId));
|
|
40
40
|
}
|
|
41
|
-
export async function
|
|
42
|
-
await writeJsonFile(
|
|
41
|
+
export async function saveTenantConfig(configDir, tenantId, config) {
|
|
42
|
+
await writeJsonFile(tenantConfigPath(configDir, tenantId), config);
|
|
43
43
|
}
|
|
44
|
-
export async function loadWorkflowMapping(configDir,
|
|
45
|
-
return readJsonFile(workflowMappingPath(configDir,
|
|
44
|
+
export async function loadWorkflowMapping(configDir, tenantId) {
|
|
45
|
+
return readJsonFile(workflowMappingPath(configDir, tenantId));
|
|
46
46
|
}
|
|
47
|
-
export async function saveWorkflowMapping(configDir,
|
|
48
|
-
await writeJsonFile(workflowMappingPath(configDir,
|
|
47
|
+
export async function saveWorkflowMapping(configDir, tenantId, mapping) {
|
|
48
|
+
await writeJsonFile(workflowMappingPath(configDir, tenantId), mapping);
|
|
49
49
|
}
|
|
50
|
-
export async function
|
|
50
|
+
export async function loadActiveTenantConfig(configDir) {
|
|
51
51
|
const global = await loadGlobalConfig(configDir);
|
|
52
|
-
if (!global?.
|
|
52
|
+
if (!global?.activeTenant) {
|
|
53
53
|
return null;
|
|
54
54
|
}
|
|
55
|
-
return
|
|
55
|
+
return loadTenantConfig(configDir, global.activeTenant);
|
|
56
56
|
}
|
|
57
57
|
async function readJsonFile(path) {
|
|
58
58
|
try {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { DetectedEnvironment } from "../detection/environment-detector.js";
|
|
2
|
+
export type ContextYaml = {
|
|
3
|
+
schema_version: 1;
|
|
4
|
+
collected_at: string;
|
|
5
|
+
project: {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
url: string;
|
|
9
|
+
};
|
|
10
|
+
status_field: {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
columns: Array<{
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
color: string | null;
|
|
17
|
+
inferred_role: "active" | "wait" | "terminal" | null;
|
|
18
|
+
confidence: "high" | "low";
|
|
19
|
+
}>;
|
|
20
|
+
};
|
|
21
|
+
text_fields: Array<{
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
data_type: string;
|
|
25
|
+
}>;
|
|
26
|
+
repositories: Array<{
|
|
27
|
+
owner: string;
|
|
28
|
+
name: string;
|
|
29
|
+
clone_url: string;
|
|
30
|
+
}>;
|
|
31
|
+
detected_environment: DetectedEnvironment;
|
|
32
|
+
runtime: {
|
|
33
|
+
agent: string;
|
|
34
|
+
agent_command: string;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ProjectDetail, ProjectStatusField } from "../github/client.js";
|
|
2
|
+
import type { DetectedEnvironment } from "../detection/environment-detector.js";
|
|
3
|
+
import type { ContextYaml } from "./context-types.js";
|
|
4
|
+
export type BuildContextYamlParams = {
|
|
5
|
+
projectDetail: ProjectDetail;
|
|
6
|
+
statusField: ProjectStatusField;
|
|
7
|
+
detectedEnvironment: DetectedEnvironment;
|
|
8
|
+
runtime: {
|
|
9
|
+
agent: string;
|
|
10
|
+
agent_command: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export declare function generateContextYamlString(context: ContextYaml): string;
|
|
14
|
+
export declare function writeContextYaml(outputDir: string, context: ContextYaml): Promise<void>;
|
|
15
|
+
export declare function buildContextYaml(params: BuildContextYamlParams): ContextYaml;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { inferStateRole } from "../mapping/smart-defaults.js";
|
|
4
|
+
function yamlQuote(value) {
|
|
5
|
+
const specialChars = /[:#{'"\]{}()\\[]|\n/;
|
|
6
|
+
if (specialChars.test(value)) {
|
|
7
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
export function generateContextYamlString(context) {
|
|
12
|
+
const lines = [];
|
|
13
|
+
lines.push("schema_version: 1");
|
|
14
|
+
lines.push(`collected_at: ${context.collected_at}`);
|
|
15
|
+
lines.push("");
|
|
16
|
+
lines.push("project:");
|
|
17
|
+
lines.push(` id: ${context.project.id}`);
|
|
18
|
+
lines.push(` title: ${yamlQuote(context.project.title)}`);
|
|
19
|
+
lines.push(` url: ${context.project.url}`);
|
|
20
|
+
lines.push("");
|
|
21
|
+
lines.push("status_field:");
|
|
22
|
+
lines.push(` id: ${context.status_field.id}`);
|
|
23
|
+
lines.push(` name: ${yamlQuote(context.status_field.name)}`);
|
|
24
|
+
lines.push(" columns:");
|
|
25
|
+
for (const column of context.status_field.columns) {
|
|
26
|
+
lines.push(` - id: ${column.id}`);
|
|
27
|
+
lines.push(` name: ${yamlQuote(column.name)}`);
|
|
28
|
+
lines.push(` color: ${column.color === null ? "null" : yamlQuote(column.color)}`);
|
|
29
|
+
lines.push(` inferred_role: ${column.inferred_role === null ? "null" : column.inferred_role}`);
|
|
30
|
+
lines.push(` confidence: ${column.confidence}`);
|
|
31
|
+
}
|
|
32
|
+
lines.push("");
|
|
33
|
+
lines.push("text_fields:");
|
|
34
|
+
if (context.text_fields.length === 0) {
|
|
35
|
+
lines.push(" []");
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
for (const field of context.text_fields) {
|
|
39
|
+
lines.push(` - id: ${field.id}`);
|
|
40
|
+
lines.push(` name: ${yamlQuote(field.name)}`);
|
|
41
|
+
lines.push(` data_type: ${field.data_type}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
lines.push("");
|
|
45
|
+
lines.push("repositories:");
|
|
46
|
+
if (context.repositories.length === 0) {
|
|
47
|
+
lines.push(" []");
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
for (const repo of context.repositories) {
|
|
51
|
+
lines.push(` - owner: ${repo.owner}`);
|
|
52
|
+
lines.push(` name: ${repo.name}`);
|
|
53
|
+
lines.push(` clone_url: ${repo.clone_url}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
lines.push("");
|
|
57
|
+
lines.push("detected_environment:");
|
|
58
|
+
lines.push(` packageManager: ${context.detected_environment.packageManager === null ? "null" : yamlQuote(context.detected_environment.packageManager)}`);
|
|
59
|
+
lines.push(` lockfile: ${context.detected_environment.lockfile === null ? "null" : yamlQuote(context.detected_environment.lockfile)}`);
|
|
60
|
+
lines.push(` testCommand: ${context.detected_environment.testCommand === null ? "null" : yamlQuote(context.detected_environment.testCommand)}`);
|
|
61
|
+
lines.push(` buildCommand: ${context.detected_environment.buildCommand === null ? "null" : yamlQuote(context.detected_environment.buildCommand)}`);
|
|
62
|
+
lines.push(` lintCommand: ${context.detected_environment.lintCommand === null ? "null" : yamlQuote(context.detected_environment.lintCommand)}`);
|
|
63
|
+
lines.push(` ciPlatform: ${context.detected_environment.ciPlatform === null ? "null" : yamlQuote(context.detected_environment.ciPlatform)}`);
|
|
64
|
+
lines.push(` monorepo: ${context.detected_environment.monorepo}`);
|
|
65
|
+
lines.push(" existingSkills:");
|
|
66
|
+
if (context.detected_environment.existingSkills.length === 0) {
|
|
67
|
+
lines.push(" []");
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
for (const skill of context.detected_environment.existingSkills) {
|
|
71
|
+
lines.push(` - ${yamlQuote(skill)}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
lines.push("");
|
|
75
|
+
lines.push("runtime:");
|
|
76
|
+
lines.push(` agent: ${yamlQuote(context.runtime.agent)}`);
|
|
77
|
+
lines.push(` agent_command: ${yamlQuote(context.runtime.agent_command)}`);
|
|
78
|
+
return lines.join("\n") + "\n";
|
|
79
|
+
}
|
|
80
|
+
export async function writeContextYaml(outputDir, context) {
|
|
81
|
+
await mkdir(outputDir, { recursive: true });
|
|
82
|
+
const contextPath = `${outputDir}/.gh-symphony/context.yaml`;
|
|
83
|
+
await mkdir(dirname(contextPath), { recursive: true });
|
|
84
|
+
const temporaryPath = `${contextPath}.tmp`;
|
|
85
|
+
const yamlContent = generateContextYamlString(context);
|
|
86
|
+
await writeFile(temporaryPath, yamlContent, "utf8");
|
|
87
|
+
const { rename } = await import("node:fs/promises");
|
|
88
|
+
await rename(temporaryPath, contextPath);
|
|
89
|
+
}
|
|
90
|
+
export function buildContextYaml(params) {
|
|
91
|
+
const columns = params.statusField.options.map((option) => {
|
|
92
|
+
const roleMapping = inferStateRole(option.name);
|
|
93
|
+
return {
|
|
94
|
+
id: option.id,
|
|
95
|
+
name: option.name,
|
|
96
|
+
color: option.color,
|
|
97
|
+
inferred_role: roleMapping.role,
|
|
98
|
+
confidence: roleMapping.confidence,
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
const textFields = params.projectDetail.textFields.map((field) => ({
|
|
102
|
+
id: field.id,
|
|
103
|
+
name: field.name,
|
|
104
|
+
data_type: field.dataType,
|
|
105
|
+
}));
|
|
106
|
+
const repositories = params.projectDetail.linkedRepositories.map((repo) => ({
|
|
107
|
+
owner: repo.owner,
|
|
108
|
+
name: repo.name,
|
|
109
|
+
clone_url: repo.cloneUrl,
|
|
110
|
+
}));
|
|
111
|
+
return {
|
|
112
|
+
schema_version: 1,
|
|
113
|
+
collected_at: new Date().toISOString(),
|
|
114
|
+
project: {
|
|
115
|
+
id: params.projectDetail.id,
|
|
116
|
+
title: params.projectDetail.title,
|
|
117
|
+
url: params.projectDetail.url,
|
|
118
|
+
},
|
|
119
|
+
status_field: {
|
|
120
|
+
id: params.statusField.id,
|
|
121
|
+
name: params.statusField.name,
|
|
122
|
+
columns,
|
|
123
|
+
},
|
|
124
|
+
text_fields: textFields,
|
|
125
|
+
repositories,
|
|
126
|
+
detected_environment: params.detectedEnvironment,
|
|
127
|
+
runtime: params.runtime,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TenantStatusSnapshot } from "@gh-symphony/core";
|
|
2
|
+
export type DashboardOptions = {
|
|
3
|
+
terminalWidth: number;
|
|
4
|
+
noColor: boolean;
|
|
5
|
+
maxAgents?: number;
|
|
6
|
+
/** Override Date.now() for deterministic testing */
|
|
7
|
+
now?: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function renderDashboard(snapshots: TenantStatusSnapshot[], options: DashboardOptions): string;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// ── Dashboard Renderer (Elixir-parity) ──────────────────────────────────────
|
|
2
|
+
import { bold, dim, green, red, yellow, cyan, magenta, blue, stripAnsi, } from "../ansi.js";
|
|
3
|
+
// ── Column widths (from Elixir spec) ─────────────────────────────────────────
|
|
4
|
+
const COL_ID = 8;
|
|
5
|
+
const COL_STAGE = 14;
|
|
6
|
+
const COL_PID = 8;
|
|
7
|
+
const COL_AGE_TURN = 12;
|
|
8
|
+
const COL_TOKENS = 10;
|
|
9
|
+
const COL_SESSION = 14;
|
|
10
|
+
/** ID header width accounts for "● " prefix in data rows */
|
|
11
|
+
const COL_ID_HEADER = COL_ID + 2;
|
|
12
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
13
|
+
const identity = (s) => s;
|
|
14
|
+
function makeColors(noColor) {
|
|
15
|
+
if (noColor) {
|
|
16
|
+
return {
|
|
17
|
+
bold: identity,
|
|
18
|
+
dim: identity,
|
|
19
|
+
green: identity,
|
|
20
|
+
red: identity,
|
|
21
|
+
yellow: identity,
|
|
22
|
+
cyan: identity,
|
|
23
|
+
magenta: identity,
|
|
24
|
+
blue: identity,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return { bold, dim, green, red, yellow, cyan, magenta, blue };
|
|
28
|
+
}
|
|
29
|
+
function pad(s, width, align = "left") {
|
|
30
|
+
const visible = stripAnsi(s);
|
|
31
|
+
if (visible.length >= width)
|
|
32
|
+
return visible.slice(0, width);
|
|
33
|
+
const padding = " ".repeat(width - visible.length);
|
|
34
|
+
return align === "right" ? padding + s : s + padding;
|
|
35
|
+
}
|
|
36
|
+
function compactSessionId(id) {
|
|
37
|
+
if (!id)
|
|
38
|
+
return "\u2014";
|
|
39
|
+
if (id.length <= 10)
|
|
40
|
+
return id;
|
|
41
|
+
return `${id.slice(0, 4)}...${id.slice(-6)}`;
|
|
42
|
+
}
|
|
43
|
+
function fmtTokens(n) {
|
|
44
|
+
return n.toLocaleString("en-US");
|
|
45
|
+
}
|
|
46
|
+
function fmtAge(startedAt, now) {
|
|
47
|
+
if (!startedAt)
|
|
48
|
+
return "0m";
|
|
49
|
+
const diffMs = now - new Date(startedAt).getTime();
|
|
50
|
+
if (diffMs < 0)
|
|
51
|
+
return "0m";
|
|
52
|
+
const totalMin = Math.floor(diffMs / 60_000);
|
|
53
|
+
if (totalMin < 60)
|
|
54
|
+
return `${totalMin}m`;
|
|
55
|
+
const h = Math.floor(totalMin / 60);
|
|
56
|
+
const m = totalMin % 60;
|
|
57
|
+
return `${h}h ${m}m`;
|
|
58
|
+
}
|
|
59
|
+
function fmtRuntime(ms) {
|
|
60
|
+
if (ms <= 0)
|
|
61
|
+
return "0h 0m";
|
|
62
|
+
const totalMin = Math.floor(ms / 60_000);
|
|
63
|
+
const h = Math.floor(totalMin / 60);
|
|
64
|
+
const m = totalMin % 60;
|
|
65
|
+
return `${h}h ${m}m`;
|
|
66
|
+
}
|
|
67
|
+
function fmtRetryTime(nextRetryAt, now) {
|
|
68
|
+
if (!nextRetryAt)
|
|
69
|
+
return "\u2014";
|
|
70
|
+
const diffMs = new Date(nextRetryAt).getTime() - now;
|
|
71
|
+
if (diffMs <= 0)
|
|
72
|
+
return "now";
|
|
73
|
+
const totalSec = Math.ceil(diffMs / 1000);
|
|
74
|
+
if (totalSec < 60)
|
|
75
|
+
return `${totalSec}s`;
|
|
76
|
+
const m = Math.floor(totalSec / 60);
|
|
77
|
+
const s = totalSec % 60;
|
|
78
|
+
return s > 0 ? `${m}m ${s}s` : `${m}m`;
|
|
79
|
+
}
|
|
80
|
+
const COL_SEPARATORS = 6;
|
|
81
|
+
function eventColWidth(termWidth) {
|
|
82
|
+
const fixed = 2 +
|
|
83
|
+
COL_ID_HEADER +
|
|
84
|
+
COL_STAGE +
|
|
85
|
+
COL_PID +
|
|
86
|
+
COL_AGE_TURN +
|
|
87
|
+
COL_TOKENS +
|
|
88
|
+
COL_SESSION +
|
|
89
|
+
COL_SEPARATORS;
|
|
90
|
+
return Math.max(5, termWidth - fixed);
|
|
91
|
+
}
|
|
92
|
+
// ── Status dot ───────────────────────────────────────────────────────────────
|
|
93
|
+
function statusDot(run, c) {
|
|
94
|
+
const event = run.lastEvent;
|
|
95
|
+
if (event === null || event === undefined || run.status === "failed")
|
|
96
|
+
return c.red("\u25CF");
|
|
97
|
+
if (event === "token_count")
|
|
98
|
+
return c.yellow("\u25CF");
|
|
99
|
+
if (event === "task_started")
|
|
100
|
+
return c.green("\u25CF");
|
|
101
|
+
if (event === "turn_completed")
|
|
102
|
+
return c.magenta("\u25CF");
|
|
103
|
+
return c.blue("\u25CF");
|
|
104
|
+
}
|
|
105
|
+
// ── Section builders ─────────────────────────────────────────────────────────
|
|
106
|
+
function titleBar(width, c) {
|
|
107
|
+
const title = " gh-symphony ";
|
|
108
|
+
const side = Math.max(0, Math.floor((width - title.length) / 2));
|
|
109
|
+
const right = Math.max(0, width - side - title.length);
|
|
110
|
+
return c.bold("\u2550".repeat(side) + title + "\u2550".repeat(right));
|
|
111
|
+
}
|
|
112
|
+
function sectionDivider(label, width, c) {
|
|
113
|
+
const prefix = `\u2500\u2500 ${label} `;
|
|
114
|
+
const fill = "\u2500".repeat(Math.max(0, width - prefix.length));
|
|
115
|
+
return c.dim(prefix + fill);
|
|
116
|
+
}
|
|
117
|
+
function buildSummaryLines(snapshots, options, c) {
|
|
118
|
+
const now = options.now ?? Date.now();
|
|
119
|
+
const lines = [];
|
|
120
|
+
const totalActive = snapshots.reduce((sum, s) => sum + s.summary.activeRuns, 0);
|
|
121
|
+
const agentStr = options.maxAgents != null
|
|
122
|
+
? `${totalActive}/${options.maxAgents}`
|
|
123
|
+
: `${totalActive}`;
|
|
124
|
+
const totIn = snapshots.reduce((sum, s) => sum + (s.codexTotals?.inputTokens ?? 0), 0);
|
|
125
|
+
const totOut = snapshots.reduce((sum, s) => sum + (s.codexTotals?.outputTokens ?? 0), 0);
|
|
126
|
+
const totAll = snapshots.reduce((sum, s) => sum + (s.codexTotals?.totalTokens ?? 0), 0);
|
|
127
|
+
const allStarts = snapshots
|
|
128
|
+
.flatMap((s) => s.activeRuns)
|
|
129
|
+
.map((r) => r.startedAt)
|
|
130
|
+
.filter((t) => t != null)
|
|
131
|
+
.map((t) => new Date(t).getTime());
|
|
132
|
+
const runtimeMs = allStarts.length > 0 ? now - Math.min(...allStarts) : 0;
|
|
133
|
+
const runtime = fmtRuntime(runtimeMs);
|
|
134
|
+
lines.push(` ${c.dim("Agents")} ${c.bold(agentStr)} ${c.dim("Runtime")} ${c.bold(runtime)} ${c.dim("Tokens")} ${fmtTokens(totIn)} in / ${fmtTokens(totOut)} out / ${c.bold(fmtTokens(totAll))} total`);
|
|
135
|
+
const hasLimits = snapshots.some((s) => s.rateLimits != null);
|
|
136
|
+
const limitStr = hasLimits ? "active" : "standard";
|
|
137
|
+
lines.push(` ${c.dim("Rate Limits")} ${limitStr}`);
|
|
138
|
+
return lines;
|
|
139
|
+
}
|
|
140
|
+
function tableHeaderRow(c) {
|
|
141
|
+
const cols = [
|
|
142
|
+
pad("ID", COL_ID_HEADER),
|
|
143
|
+
pad("STAGE", COL_STAGE),
|
|
144
|
+
pad("PID", COL_PID),
|
|
145
|
+
pad("AGE/TURN", COL_AGE_TURN),
|
|
146
|
+
pad("TOKENS", COL_TOKENS),
|
|
147
|
+
pad("SESSION", COL_SESSION),
|
|
148
|
+
"EVENT",
|
|
149
|
+
].join(" ");
|
|
150
|
+
return ` ${c.dim(cols)}`;
|
|
151
|
+
}
|
|
152
|
+
function activeRunRow(run, now, evtWidth, c) {
|
|
153
|
+
const dot = statusDot(run, c);
|
|
154
|
+
const id = pad(run.issueIdentifier, COL_ID);
|
|
155
|
+
const stage = pad(run.issueState, COL_STAGE);
|
|
156
|
+
const pid = pad(run.processId != null ? String(run.processId) : "\u2014", COL_PID);
|
|
157
|
+
const age = fmtAge(run.startedAt, now);
|
|
158
|
+
const turn = run.turnCount ?? 0;
|
|
159
|
+
const ageTurn = pad(`${age}/${turn}`, COL_AGE_TURN);
|
|
160
|
+
const tokens = pad(fmtTokens(run.tokenUsage?.totalTokens ?? 0), COL_TOKENS, "right");
|
|
161
|
+
const sessionId = run.runtimeSession?.sessionId ?? run.runtimeSession?.threadId ?? null;
|
|
162
|
+
const session = pad(compactSessionId(sessionId), COL_SESSION);
|
|
163
|
+
const event = pad(run.lastEvent ?? "\u2014", evtWidth);
|
|
164
|
+
const columns = [id, stage, pid, ageTurn, tokens, session, event].join(" ");
|
|
165
|
+
return ` ${dot} ${columns}`;
|
|
166
|
+
}
|
|
167
|
+
function retryRow(entry, snapshot, now, c) {
|
|
168
|
+
const id = entry.issueIdentifier;
|
|
169
|
+
const kind = entry.retryKind;
|
|
170
|
+
const timeStr = fmtRetryTime(entry.nextRetryAt, now);
|
|
171
|
+
const matchingRun = snapshot.activeRuns.find((r) => r.runId === entry.runId);
|
|
172
|
+
const errorHint = matchingRun?.lastEvent ?? "";
|
|
173
|
+
return ` ${c.yellow("\u21BB")} ${id} ${kind} retrying in ${timeStr}${errorHint ? " " + errorHint : ""}`;
|
|
174
|
+
}
|
|
175
|
+
// ── Main Renderer ────────────────────────────────────────────────────────────
|
|
176
|
+
export function renderDashboard(snapshots, options) {
|
|
177
|
+
const width = options.terminalWidth || 115;
|
|
178
|
+
const now = options.now ?? Date.now();
|
|
179
|
+
const c = makeColors(options.noColor);
|
|
180
|
+
const evtWidth = eventColWidth(width);
|
|
181
|
+
const lines = [];
|
|
182
|
+
lines.push(titleBar(width, c));
|
|
183
|
+
lines.push(...buildSummaryLines(snapshots, options, c));
|
|
184
|
+
lines.push("");
|
|
185
|
+
for (const snap of snapshots) {
|
|
186
|
+
const hasActiveRuns = snap.activeRuns.length > 0;
|
|
187
|
+
const hasRetries = snap.retryQueue.length > 0;
|
|
188
|
+
if (!hasActiveRuns && !hasRetries)
|
|
189
|
+
continue;
|
|
190
|
+
lines.push(sectionDivider(snap.slug, width, c));
|
|
191
|
+
if (hasActiveRuns) {
|
|
192
|
+
lines.push(tableHeaderRow(c));
|
|
193
|
+
for (const rawRun of snap.activeRuns) {
|
|
194
|
+
const run = rawRun;
|
|
195
|
+
lines.push(activeRunRow(run, now, evtWidth, c));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
lines.push("");
|
|
199
|
+
}
|
|
200
|
+
const allRetries = [];
|
|
201
|
+
for (const snap of snapshots) {
|
|
202
|
+
for (const entry of snap.retryQueue) {
|
|
203
|
+
allRetries.push({ entry, snapshot: snap });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (allRetries.length > 0) {
|
|
207
|
+
lines.push(sectionDivider("Backoff Queue", width, c));
|
|
208
|
+
for (const { entry, snapshot } of allRetries) {
|
|
209
|
+
lines.push(retryRow(entry, snapshot, now, c));
|
|
210
|
+
}
|
|
211
|
+
lines.push("");
|
|
212
|
+
}
|
|
213
|
+
const result = lines.map((line) => {
|
|
214
|
+
const visible = stripAnsi(line);
|
|
215
|
+
if (visible.length <= width)
|
|
216
|
+
return line;
|
|
217
|
+
return visible.slice(0, width);
|
|
218
|
+
});
|
|
219
|
+
return result.join("\n");
|
|
220
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type DetectedEnvironment = {
|
|
2
|
+
packageManager: "pnpm" | "npm" | "yarn" | "bun" | null;
|
|
3
|
+
lockfile: string | null;
|
|
4
|
+
testCommand: string | null;
|
|
5
|
+
buildCommand: string | null;
|
|
6
|
+
lintCommand: string | null;
|
|
7
|
+
ciPlatform: "github-actions" | null;
|
|
8
|
+
monorepo: boolean;
|
|
9
|
+
existingSkills: string[];
|
|
10
|
+
};
|
|
11
|
+
export declare function detectEnvironment(cwd: string): Promise<DetectedEnvironment>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
function isFileMissing(error) {
|
|
4
|
+
return Boolean(error &&
|
|
5
|
+
typeof error === "object" &&
|
|
6
|
+
"code" in error &&
|
|
7
|
+
error.code === "ENOENT");
|
|
8
|
+
}
|
|
9
|
+
async function fileExists(path) {
|
|
10
|
+
try {
|
|
11
|
+
await access(path);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (isFileMissing(error)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function readJsonFile(path) {
|
|
22
|
+
try {
|
|
23
|
+
const raw = await readFile(path, "utf8");
|
|
24
|
+
return JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (isFileMissing(error)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (error instanceof SyntaxError) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function detectPackageManager(cwd) {
|
|
37
|
+
const lockfiles = [
|
|
38
|
+
{ name: "pnpm-lock.yaml", manager: "pnpm" },
|
|
39
|
+
{ name: "bun.lock", manager: "bun" },
|
|
40
|
+
{ name: "bun.lockb", manager: "bun" },
|
|
41
|
+
{ name: "yarn.lock", manager: "yarn" },
|
|
42
|
+
{ name: "package-lock.json", manager: "npm" },
|
|
43
|
+
];
|
|
44
|
+
for (const { name, manager } of lockfiles) {
|
|
45
|
+
const exists = await fileExists(join(cwd, name));
|
|
46
|
+
if (exists) {
|
|
47
|
+
return { packageManager: manager, lockfile: name };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { packageManager: null, lockfile: null };
|
|
51
|
+
}
|
|
52
|
+
async function detectScripts(cwd) {
|
|
53
|
+
const packageJson = await readJsonFile(join(cwd, "package.json"));
|
|
54
|
+
if (!packageJson?.scripts) {
|
|
55
|
+
return { testCommand: null, buildCommand: null, lintCommand: null };
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
testCommand: packageJson.scripts.test ?? null,
|
|
59
|
+
buildCommand: packageJson.scripts.build ?? null,
|
|
60
|
+
lintCommand: packageJson.scripts.lint ?? null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async function detectCiPlatform(cwd) {
|
|
64
|
+
const workflowsDir = join(cwd, ".github", "workflows");
|
|
65
|
+
const exists = await fileExists(workflowsDir);
|
|
66
|
+
return exists ? "github-actions" : null;
|
|
67
|
+
}
|
|
68
|
+
async function detectMonorepo(cwd) {
|
|
69
|
+
// Check for pnpm-workspace.yaml
|
|
70
|
+
const hasPnpmWorkspace = await fileExists(join(cwd, "pnpm-workspace.yaml"));
|
|
71
|
+
if (hasPnpmWorkspace) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
// Check for lerna.json
|
|
75
|
+
const hasLerna = await fileExists(join(cwd, "lerna.json"));
|
|
76
|
+
if (hasLerna) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
// Check for workspaces field in package.json
|
|
80
|
+
const packageJson = await readJsonFile(join(cwd, "package.json"));
|
|
81
|
+
if (packageJson?.workspaces) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
async function detectExistingSkills(cwd) {
|
|
87
|
+
const skills = [];
|
|
88
|
+
// Check .claude/skills/
|
|
89
|
+
const claudeSkillsDir = join(cwd, ".claude", "skills");
|
|
90
|
+
try {
|
|
91
|
+
const { readdirSync } = await import("node:fs");
|
|
92
|
+
const entries = readdirSync(claudeSkillsDir, { withFileTypes: true });
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
if (entry.isDirectory()) {
|
|
95
|
+
skills.push(entry.name);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (!isFileMissing(error)) {
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Check .codex/skills/
|
|
105
|
+
const codexSkillsDir = join(cwd, ".codex", "skills");
|
|
106
|
+
try {
|
|
107
|
+
const { readdirSync } = await import("node:fs");
|
|
108
|
+
const entries = readdirSync(codexSkillsDir, { withFileTypes: true });
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
if (entry.isDirectory()) {
|
|
111
|
+
skills.push(entry.name);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (!isFileMissing(error)) {
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return skills;
|
|
121
|
+
}
|
|
122
|
+
export async function detectEnvironment(cwd) {
|
|
123
|
+
const [{ packageManager, lockfile }, { testCommand, buildCommand, lintCommand }, ciPlatform, monorepo, existingSkills,] = await Promise.all([
|
|
124
|
+
detectPackageManager(cwd),
|
|
125
|
+
detectScripts(cwd),
|
|
126
|
+
detectCiPlatform(cwd),
|
|
127
|
+
detectMonorepo(cwd),
|
|
128
|
+
detectExistingSkills(cwd),
|
|
129
|
+
]);
|
|
130
|
+
return {
|
|
131
|
+
packageManager,
|
|
132
|
+
lockfile,
|
|
133
|
+
testCommand,
|
|
134
|
+
buildCommand,
|
|
135
|
+
lintCommand,
|
|
136
|
+
ciPlatform,
|
|
137
|
+
monorepo,
|
|
138
|
+
existingSkills,
|
|
139
|
+
};
|
|
140
|
+
}
|
package/dist/github/client.d.ts
CHANGED
|
@@ -36,17 +36,28 @@ export type LinkedRepository = {
|
|
|
36
36
|
url: string;
|
|
37
37
|
cloneUrl: string;
|
|
38
38
|
};
|
|
39
|
+
export type ProjectTextField = {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
dataType: string;
|
|
43
|
+
};
|
|
39
44
|
export type ProjectDetail = {
|
|
40
45
|
id: string;
|
|
41
46
|
title: string;
|
|
42
47
|
url: string;
|
|
43
48
|
statusFields: ProjectStatusField[];
|
|
49
|
+
textFields: ProjectTextField[];
|
|
44
50
|
linkedRepositories: LinkedRepository[];
|
|
45
51
|
};
|
|
46
52
|
export declare class GitHubApiError extends Error {
|
|
47
53
|
readonly status?: number | undefined;
|
|
48
54
|
constructor(message: string, status?: number | undefined);
|
|
49
55
|
}
|
|
56
|
+
export declare class GitHubScopeError extends GitHubApiError {
|
|
57
|
+
readonly requiredScopes: string[];
|
|
58
|
+
readonly currentScopes: string[];
|
|
59
|
+
constructor(message: string, requiredScopes: string[], currentScopes: string[]);
|
|
60
|
+
}
|
|
50
61
|
export declare function createClient(token: string, options?: {
|
|
51
62
|
apiUrl?: string;
|
|
52
63
|
fetchImpl?: typeof fetch;
|