@backburner/cli 0.1.0 → 0.1.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/README.md +28 -24
- package/dist/src/cli/commands/broker-smoke.js +2 -3
- package/dist/src/cli/commands/broker.js +2 -3
- package/dist/src/cli/commands/journal.js +2 -3
- package/dist/src/cli/commands/tui.js +8 -9
- package/dist/src/cli/dispatcher.js +7 -0
- package/dist/src/cli/init.js +10 -7
- package/dist/src/cli/runtime/run-context.js +6 -6
- package/dist/src/onboarding/config-writer.js +10 -9
- package/dist/src/onboarding/prompt.js +104 -23
- package/dist/src/onboarding/services/demo-repo.js +33 -0
- package/dist/src/onboarding/services/label-provisioner.js +39 -0
- package/dist/src/onboarding/services/provider-discovery.js +71 -6
- package/dist/src/onboarding/steps/configure-models.js +100 -39
- package/dist/src/onboarding/steps/configure-paths.js +5 -5
- package/dist/src/onboarding/steps/discover-providers.js +9 -3
- package/dist/src/onboarding/steps/generate-config.js +3 -1
- package/dist/src/onboarding/steps/provision-labels.js +104 -0
- package/dist/src/onboarding/steps/select-repos.js +112 -9
- package/dist/src/onboarding/steps/show-summary.js +54 -4
- package/dist/src/onboarding/steps/welcome.js +2 -1
- package/dist/src/utils/paths.js +13 -1
- package/package.json +2 -1
|
@@ -3,31 +3,36 @@ const PROVIDER_SPECS = [
|
|
|
3
3
|
name: "claude",
|
|
4
4
|
command: "claude",
|
|
5
5
|
versionArgs: ["--version"],
|
|
6
|
-
versionExtract: (out) => out.trim().split("\n")[0] ?? out.trim()
|
|
6
|
+
versionExtract: (out) => out.trim().split("\n")[0] ?? out.trim(),
|
|
7
|
+
modelListCommands: [["models"], ["models", "list"], ["model", "list"]]
|
|
7
8
|
},
|
|
8
9
|
{
|
|
9
10
|
name: "codex",
|
|
10
11
|
command: "codex",
|
|
11
12
|
versionArgs: ["--version"],
|
|
12
|
-
versionExtract: (out) => out.trim()
|
|
13
|
+
versionExtract: (out) => out.trim(),
|
|
14
|
+
modelListCommands: [["models"], ["models", "list"], ["model", "list"]]
|
|
13
15
|
},
|
|
14
16
|
{
|
|
15
17
|
name: "gemini",
|
|
16
18
|
command: "gemini",
|
|
17
19
|
versionArgs: ["--version"],
|
|
18
|
-
versionExtract: (out) => out.trim()
|
|
20
|
+
versionExtract: (out) => out.trim(),
|
|
21
|
+
modelListCommands: [["models"], ["models", "list"], ["model", "list"]]
|
|
19
22
|
},
|
|
20
23
|
{
|
|
21
24
|
name: "opencode",
|
|
22
25
|
command: "opencode",
|
|
23
26
|
versionArgs: ["--version"],
|
|
24
|
-
versionExtract: (out) => out.trim()
|
|
27
|
+
versionExtract: (out) => out.trim(),
|
|
28
|
+
modelListCommands: [["models"], ["models", "list"], ["model", "list"]]
|
|
25
29
|
},
|
|
26
30
|
{
|
|
27
31
|
name: "antigravity",
|
|
28
32
|
command: "agy",
|
|
29
33
|
versionArgs: ["--version"],
|
|
30
|
-
versionExtract: (out) => out.trim()
|
|
34
|
+
versionExtract: (out) => out.trim(),
|
|
35
|
+
modelListCommands: [["models"], ["models", "list"], ["model", "list"]]
|
|
31
36
|
}
|
|
32
37
|
];
|
|
33
38
|
export class ProviderDiscoveryService {
|
|
@@ -44,15 +49,75 @@ export class ProviderDiscoveryService {
|
|
|
44
49
|
timeoutMs: 5_000
|
|
45
50
|
});
|
|
46
51
|
const version = spec.versionExtract?.(result.stdout) ?? result.stdout.trim();
|
|
52
|
+
const models = await this.discoverModels(spec);
|
|
47
53
|
return {
|
|
48
54
|
name: spec.name,
|
|
49
55
|
command: spec.command,
|
|
50
56
|
available: true,
|
|
51
|
-
...(version.length > 0 ? { version } : {})
|
|
57
|
+
...(version.length > 0 ? { version } : {}),
|
|
58
|
+
...(models.length > 0 ? { models } : {})
|
|
52
59
|
};
|
|
53
60
|
}
|
|
54
61
|
catch {
|
|
55
62
|
return { name: spec.name, command: spec.command, available: false };
|
|
56
63
|
}
|
|
57
64
|
}
|
|
65
|
+
async discoverModels(spec) {
|
|
66
|
+
for (const args of spec.modelListCommands ?? []) {
|
|
67
|
+
try {
|
|
68
|
+
const result = await this.commandRunner.run(spec.command, args, {
|
|
69
|
+
timeoutMs: 5_000
|
|
70
|
+
});
|
|
71
|
+
const models = parseModelListOutput(result.stdout);
|
|
72
|
+
if (models.length > 0) {
|
|
73
|
+
return models;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Model listing is optional. Keep provider discovery available and fall back to defaults.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function parseModelListOutput(stdout) {
|
|
84
|
+
const models = [];
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
for (const rawLine of stdout.split(/\r?\n/)) {
|
|
87
|
+
const model = normalizeModelListLine(rawLine);
|
|
88
|
+
if (model.length === 0 || seen.has(model)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
models.push(model);
|
|
92
|
+
seen.add(model);
|
|
93
|
+
}
|
|
94
|
+
return models;
|
|
95
|
+
}
|
|
96
|
+
function normalizeModelListLine(line) {
|
|
97
|
+
const withoutBullet = line
|
|
98
|
+
.trim()
|
|
99
|
+
.replace(/^[*\-]\s*/, "")
|
|
100
|
+
.replace(/^\[[ xX]\]\s*/, "")
|
|
101
|
+
.replace(/^\d+[.)]\s*/, "")
|
|
102
|
+
.trim();
|
|
103
|
+
if (withoutBullet.length === 0 || withoutBullet.startsWith("#")) {
|
|
104
|
+
return "";
|
|
105
|
+
}
|
|
106
|
+
const jsonName = /"(?<key>id|name|model)"\s*:\s*"(?<value>[^"]+)"/.exec(withoutBullet);
|
|
107
|
+
if (jsonName?.groups?.value !== undefined) {
|
|
108
|
+
return jsonName.groups.value.trim();
|
|
109
|
+
}
|
|
110
|
+
const tableCells = withoutBullet.split("|").map((cell) => cell.trim()).filter(Boolean);
|
|
111
|
+
if (tableCells.length > 0 && tableCells.every((cell) => /^-+$/.test(cell))) {
|
|
112
|
+
return "";
|
|
113
|
+
}
|
|
114
|
+
if (tableCells.length > 1) {
|
|
115
|
+
return stripAnnotation(tableCells[0] ?? "");
|
|
116
|
+
}
|
|
117
|
+
return stripAnnotation(withoutBullet);
|
|
118
|
+
}
|
|
119
|
+
function stripAnnotation(value) {
|
|
120
|
+
return value
|
|
121
|
+
.replace(/\s+\((default|recommended|latest|beta|preview)\)$/i, "")
|
|
122
|
+
.trim();
|
|
58
123
|
}
|
|
@@ -7,17 +7,19 @@ const STRONG_ROLES = [
|
|
|
7
7
|
"retrospective"
|
|
8
8
|
];
|
|
9
9
|
const CHEAP_ROLES = ["implementation", "discussion"];
|
|
10
|
-
/**
|
|
10
|
+
/** Provider-specific fallbacks used when the CLI cannot list Backburner model names. */
|
|
11
11
|
const DEFAULT_MODELS = {
|
|
12
12
|
claude: { strong: "claude-sonnet-4-6" },
|
|
13
|
-
codex: { strong: "
|
|
13
|
+
codex: { strong: "sol", cheap: "luna" },
|
|
14
14
|
gemini: { strong: "gemini-3.1-pro-preview", cheap: "gemini-3-flash-preview" },
|
|
15
15
|
opencode: { strong: "anthropic/claude-sonnet-4-6", cheap: "google/gemini-2.0-flash" },
|
|
16
16
|
antigravity: { strong: "Gemini 3.1 Pro (High)", cheap: "Gemini 3.5 Flash (Low)" }
|
|
17
17
|
};
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
const BACKBURNER_MODEL_DEFAULTS = {
|
|
19
|
+
strong: "sol",
|
|
20
|
+
balanced: "terra",
|
|
21
|
+
cheap: "luna"
|
|
22
|
+
};
|
|
21
23
|
export class ConfigureModelsStep {
|
|
22
24
|
name = "configure-models";
|
|
23
25
|
description = "Configure model assignments";
|
|
@@ -29,35 +31,31 @@ export class ConfigureModelsStep {
|
|
|
29
31
|
message: "Skipped: no providers available to configure"
|
|
30
32
|
};
|
|
31
33
|
}
|
|
32
|
-
const providerOptions = available.map((p) =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
34
|
+
const providerOptions = available.map((p) => {
|
|
35
|
+
const modelNote = p.models !== undefined && p.models.length > 0
|
|
36
|
+
? `, ${p.models.length} model${p.models.length === 1 ? "" : "s"}`
|
|
37
|
+
: "";
|
|
38
|
+
return `${p.name}${p.version !== undefined ? ` (${p.version}${modelNote})` : modelNote}`;
|
|
39
|
+
});
|
|
40
|
+
process.stdout.write("\nSelect providers to enable. Backburner will configure strong and cheap agents for each selected provider.\n");
|
|
41
|
+
process.stdout.write("Strong role tasks: product, planning, review, discussion, implementation_manager, retrospective\n");
|
|
42
|
+
process.stdout.write("Cheap role tasks: implementation, discussion\n");
|
|
43
|
+
const selectedProviderIndices = await prompter.multiSelect("Providers to enable:", providerOptions, available.map((_, index) => index));
|
|
44
|
+
const selectedProviders = selectedProviderIndices
|
|
45
|
+
.map((index) => available[index])
|
|
46
|
+
.filter((provider) => provider !== undefined);
|
|
47
|
+
if (selectedProviders.length === 0) {
|
|
48
|
+
return { status: "failed", message: "No providers selected for agent configuration" };
|
|
47
49
|
}
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const agents = [];
|
|
51
|
+
for (const provider of selectedProviders) {
|
|
52
|
+
const strongDefault = selectDefaultModel(provider, "strong");
|
|
53
|
+
const cheapDefault = selectDefaultModel(provider, "cheap");
|
|
54
|
+
showAvailableModels(provider, strongDefault, cheapDefault);
|
|
55
|
+
const strongModel = await prompter.ask(`Model for ${provider.name} (strong):`, strongDefault);
|
|
56
|
+
const cheapModel = await prompter.ask(`Model for ${provider.name} (cheap):`, cheapDefault);
|
|
57
|
+
agents.push(buildAgent(`${provider.name}-strong`, provider.name, provider.command, "strong", STRONG_ROLES, strongModel || strongDefault), buildAgent(`${provider.name}-cheap`, provider.name, provider.command, "cheap", CHEAP_ROLES, cheapModel || cheapDefault));
|
|
52
58
|
}
|
|
53
|
-
const strongDefaults = DEFAULT_MODELS[strongProvider.name];
|
|
54
|
-
const cheapDefaults = DEFAULT_MODELS[cheapProvider.name];
|
|
55
|
-
const strongModel = await prompter.ask(`Model for ${strongProvider.name} (strong):`, strongDefaults.strong);
|
|
56
|
-
const cheapModel = await prompter.ask(`Model for ${cheapProvider.name} (cheap):`, cheapDefaults.cheap ?? cheapDefaults.strong);
|
|
57
|
-
const agents = [
|
|
58
|
-
buildAgent(`${strongProvider.name}-strong`, strongProvider.name, strongProvider.command, "strong", STRONG_ROLES, strongModel || strongDefaults.strong),
|
|
59
|
-
buildAgent(`${cheapProvider.name}-cheap`, cheapProvider.name, cheapProvider.command, "cheap", CHEAP_ROLES, cheapModel || (cheapDefaults.cheap ?? cheapDefaults.strong))
|
|
60
|
-
];
|
|
61
59
|
if (ctx.configDraft === undefined) {
|
|
62
60
|
ctx.configDraft = buildEmptyDraft(ctx);
|
|
63
61
|
}
|
|
@@ -67,18 +65,81 @@ export class ConfigureModelsStep {
|
|
|
67
65
|
message: `Configured ${agents.length} agents`,
|
|
68
66
|
details: agents.map((a) => {
|
|
69
67
|
const modelNote = a.model !== undefined ? ` (${a.model})` : "";
|
|
70
|
-
return `${a.id}: ${a.provider}${modelNote} [${a.modelClass}]
|
|
68
|
+
return `${a.id}: ${a.provider}${modelNote} [${a.modelClass}] -> ${a.roles.join(", ")}`;
|
|
71
69
|
})
|
|
72
70
|
};
|
|
73
71
|
}
|
|
74
72
|
}
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
function showAvailableModels(provider, strongDefault, cheapDefault) {
|
|
74
|
+
const models = provider.models ?? [];
|
|
75
|
+
if (models.length === 0) {
|
|
76
|
+
process.stdout.write(`\n${provider.name} did not expose a model list. Press enter to use the defaults.\n`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const shownModels = rankModelOptions(models, [strongDefault, cheapDefault]);
|
|
80
|
+
process.stdout.write(`\nAvailable ${provider.name} models`);
|
|
81
|
+
if (shownModels.length < models.length) {
|
|
82
|
+
process.stdout.write(` (showing ${shownModels.length} of ${models.length})`);
|
|
83
|
+
}
|
|
84
|
+
process.stdout.write(":\n");
|
|
85
|
+
for (const model of shownModels) {
|
|
86
|
+
const labels = [
|
|
87
|
+
model === strongDefault ? "strong default" : undefined,
|
|
88
|
+
model === cheapDefault ? "cheap default" : undefined
|
|
89
|
+
].filter((label) => label !== undefined);
|
|
90
|
+
const suffix = labels.length > 0 ? ` (${labels.join(", ")})` : "";
|
|
91
|
+
process.stdout.write(` - ${model}${suffix}\n`);
|
|
92
|
+
}
|
|
93
|
+
process.stdout.write("Press enter at each prompt to accept the default, or type any model name.\n");
|
|
94
|
+
}
|
|
95
|
+
function rankModelOptions(models, defaults) {
|
|
96
|
+
const ranked = [];
|
|
97
|
+
const add = (model) => {
|
|
98
|
+
if (model !== undefined && model.length > 0 && !ranked.includes(model)) {
|
|
99
|
+
ranked.push(model);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
for (const defaultModel of defaults) {
|
|
103
|
+
add(defaultModel);
|
|
104
|
+
}
|
|
105
|
+
for (const target of ["sol", "terra", "luna"]) {
|
|
106
|
+
add(findDiscoveredModel(models, [target]));
|
|
107
|
+
}
|
|
108
|
+
for (const model of models) {
|
|
109
|
+
add(model);
|
|
110
|
+
if (ranked.length >= 12) {
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return ranked;
|
|
115
|
+
}
|
|
116
|
+
function selectDefaultModel(provider, modelClass) {
|
|
117
|
+
const configuredDefaults = DEFAULT_MODELS[provider.name];
|
|
118
|
+
const backburnerDefault = BACKBURNER_MODEL_DEFAULTS[modelClass];
|
|
119
|
+
const providerDefault = configuredDefaults[modelClass] ?? configuredDefaults.strong;
|
|
120
|
+
const discoveredBackburnerDefault = findDiscoveredModel(provider.models ?? [], [
|
|
121
|
+
backburnerDefault
|
|
122
|
+
]);
|
|
123
|
+
const discoveredProviderDefault = providerDefault !== undefined
|
|
124
|
+
? findDiscoveredModel(provider.models ?? [], [providerDefault])
|
|
125
|
+
: undefined;
|
|
126
|
+
return discoveredBackburnerDefault ?? discoveredProviderDefault ?? providerDefault ?? backburnerDefault;
|
|
127
|
+
}
|
|
128
|
+
function findDiscoveredModel(models, candidates) {
|
|
129
|
+
for (const candidate of candidates) {
|
|
130
|
+
const match = models.find((model) => model.toLowerCase() === candidate.toLowerCase());
|
|
131
|
+
if (match !== undefined) {
|
|
132
|
+
return match;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const candidate of candidates) {
|
|
136
|
+
const normalizedCandidate = candidate.toLowerCase();
|
|
137
|
+
const match = models.find((model) => model.toLowerCase().includes(normalizedCandidate));
|
|
138
|
+
if (match !== undefined) {
|
|
139
|
+
return match;
|
|
140
|
+
}
|
|
80
141
|
}
|
|
81
|
-
return
|
|
142
|
+
return undefined;
|
|
82
143
|
}
|
|
83
144
|
function buildAgent(id, provider, command, modelClass, roles, model) {
|
|
84
145
|
return {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import { defaultBackburnerManagementRoot, defaultBackburnerOutputsRoot, defaultWorkspacesRootForCodeRoot } from "../../utils/paths.js";
|
|
2
|
+
import { defaultBackburnerManagementRoot, defaultBackburnerOutputsRoot, defaultWorkspacesRootForCodeRoot, resolveUserPath } from "../../utils/paths.js";
|
|
3
3
|
export class ConfigurePathsStep {
|
|
4
4
|
name = "configure-paths";
|
|
5
5
|
description = "Configure directories";
|
|
@@ -15,10 +15,10 @@ export class ConfigurePathsStep {
|
|
|
15
15
|
const outputsDir = await prompter.ask("Outputs directory (state, logs):", defaultOutputs);
|
|
16
16
|
const logDir = await prompter.ask("Log directory:", ctx.logDir || path.join(outputsDir || defaultOutputs, "logs"));
|
|
17
17
|
const workspacesDir = await prompter.ask("Local workspaces directory (where Backburner creates git worktrees):", defaultWorkspaces);
|
|
18
|
-
ctx.managementDir = managementDir || defaultManagement;
|
|
19
|
-
ctx.outputsDir = outputsDir || defaultOutputs;
|
|
20
|
-
ctx.logDir = logDir || path.join(ctx.outputsDir, "logs");
|
|
21
|
-
ctx.workspacesDir = workspacesDir || defaultWorkspaces;
|
|
18
|
+
ctx.managementDir = resolveUserPath(managementDir || defaultManagement);
|
|
19
|
+
ctx.outputsDir = resolveUserPath(outputsDir || defaultOutputs);
|
|
20
|
+
ctx.logDir = resolveUserPath(logDir || path.join(ctx.outputsDir, "logs"));
|
|
21
|
+
ctx.workspacesDir = resolveUserPath(workspacesDir || defaultWorkspaces);
|
|
22
22
|
return {
|
|
23
23
|
status: "passed",
|
|
24
24
|
message: "Directories configured",
|
|
@@ -11,9 +11,15 @@ export class DiscoverProvidersStep {
|
|
|
11
11
|
ctx.discoveredProviders = await service.discoverAll();
|
|
12
12
|
const available = ctx.discoveredProviders.filter((p) => p.available);
|
|
13
13
|
const unavailable = ctx.discoveredProviders.filter((p) => !p.available);
|
|
14
|
-
const details = ctx.discoveredProviders.map((p) =>
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
const details = ctx.discoveredProviders.map((p) => {
|
|
15
|
+
if (!p.available) {
|
|
16
|
+
return `x ${p.name} (${p.command}): not found`;
|
|
17
|
+
}
|
|
18
|
+
const modelNote = p.models !== undefined && p.models.length > 0
|
|
19
|
+
? `, ${p.models.length} model${p.models.length === 1 ? "" : "s"} discovered`
|
|
20
|
+
: ", model list unavailable";
|
|
21
|
+
return `ok ${p.name} (${p.command}): ${p.version ?? "found"}${modelNote}`;
|
|
22
|
+
});
|
|
17
23
|
if (available.length === 0) {
|
|
18
24
|
ctx.warnings.push("No AI providers found. Install at least one of: antigravity, gemini, claude, codex, opencode");
|
|
19
25
|
return {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { ConfigWriter } from "../config-writer.js";
|
|
3
|
+
import { DEFAULT_BACKBURNER_LABELS } from "../config-writer.js";
|
|
3
4
|
export class GenerateConfigStep {
|
|
4
5
|
name = "generate-config";
|
|
5
6
|
description = "Generating configuration";
|
|
@@ -101,6 +102,7 @@ function buildRepoDraft(repo, codeRoot) {
|
|
|
101
102
|
name: repo.name,
|
|
102
103
|
localPath,
|
|
103
104
|
defaultBranch: repo.defaultBranch || "main",
|
|
104
|
-
enabled: true
|
|
105
|
+
enabled: true,
|
|
106
|
+
labels: { ...DEFAULT_BACKBURNER_LABELS }
|
|
105
107
|
};
|
|
106
108
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { GitHubLabelProvisioner } from "../services/label-provisioner.js";
|
|
2
|
+
const LABEL_DEFINITIONS = {
|
|
3
|
+
agentInProgress: {
|
|
4
|
+
description: "Backburner is actively working on this item.",
|
|
5
|
+
color: "6F42C1"
|
|
6
|
+
},
|
|
7
|
+
agentProductApproved: {
|
|
8
|
+
description: "Approve Backburner to start product discovery from this issue.",
|
|
9
|
+
color: "2DA44E"
|
|
10
|
+
},
|
|
11
|
+
agentPlanApproved: {
|
|
12
|
+
description: "Approve Backburner to implement an accepted plan.",
|
|
13
|
+
color: "0969DA"
|
|
14
|
+
},
|
|
15
|
+
agentReview: {
|
|
16
|
+
description: "Ask Backburner to review this pull request.",
|
|
17
|
+
color: "BF8700"
|
|
18
|
+
},
|
|
19
|
+
planBreakdownNeeded: {
|
|
20
|
+
description: "Ask Backburner to break a plan into implementation packets.",
|
|
21
|
+
color: "8250DF"
|
|
22
|
+
},
|
|
23
|
+
agentSyncParentBranch: {
|
|
24
|
+
description: "Allow Backburner to sync the parent branch from the default branch.",
|
|
25
|
+
color: "1F883D"
|
|
26
|
+
},
|
|
27
|
+
prepareForMerge: {
|
|
28
|
+
description: "Ask Backburner to prepare this pull request for merge.",
|
|
29
|
+
color: "CF222E"
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
export class ProvisionLabelsStep {
|
|
33
|
+
commandRunner;
|
|
34
|
+
name = "provision-labels";
|
|
35
|
+
description = "Creating GitHub labels";
|
|
36
|
+
constructor(commandRunner) {
|
|
37
|
+
this.commandRunner = commandRunner;
|
|
38
|
+
}
|
|
39
|
+
async run(ctx, _prompter) {
|
|
40
|
+
const draft = ctx.configDraft;
|
|
41
|
+
if (draft === undefined || draft.repos.length === 0) {
|
|
42
|
+
return {
|
|
43
|
+
status: "skipped",
|
|
44
|
+
message: "Skipped: no repositories selected for label creation"
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const targets = buildLabelTargets(draft);
|
|
48
|
+
const provisioner = new GitHubLabelProvisioner(this.commandRunner);
|
|
49
|
+
const results = await provisioner.provisionLabels(targets);
|
|
50
|
+
const failures = results.filter((result) => result.status === "failed");
|
|
51
|
+
if (failures.length > 0) {
|
|
52
|
+
ctx.warnings.push("Some GitHub labels could not be created. You can retry onboarding or create them manually from repos.json.");
|
|
53
|
+
return {
|
|
54
|
+
status: "warning",
|
|
55
|
+
message: `Created labels with ${failures.length} failure${failures.length === 1 ? "" : "s"}`,
|
|
56
|
+
details: [
|
|
57
|
+
...failures.map((failure) => {
|
|
58
|
+
const suffix = failure.message !== undefined ? `: ${failure.message}` : "";
|
|
59
|
+
return `${failure.repoSlug}: ${failure.labelName}${suffix}`;
|
|
60
|
+
}),
|
|
61
|
+
...buildManualLabelDetails(targets)
|
|
62
|
+
]
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
status: "passed",
|
|
67
|
+
message: `Created or updated ${results.length} GitHub label${results.length === 1 ? "" : "s"}`,
|
|
68
|
+
details: targets.map((target) => `${target.owner}/${target.name}: ${target.labels.length} labels`)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function buildManualLabelDetails(targets) {
|
|
73
|
+
return targets.flatMap((target) => {
|
|
74
|
+
const repoSlug = `${target.owner}/${target.name}`;
|
|
75
|
+
return [
|
|
76
|
+
`Manual labels page for ${repoSlug}: https://github.com/${repoSlug}/labels`,
|
|
77
|
+
`Required labels: ${target.labels.map((label) => label.name).join(", ")}`
|
|
78
|
+
];
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function buildLabelTargets(draft) {
|
|
82
|
+
return draft.repos.map((repo) => ({
|
|
83
|
+
owner: repo.owner,
|
|
84
|
+
name: repo.name,
|
|
85
|
+
labels: buildRepoLabels(repo)
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
function buildRepoLabels(repo) {
|
|
89
|
+
return [
|
|
90
|
+
buildLabel("agentInProgress", repo.labels.agentInProgress),
|
|
91
|
+
buildLabel("agentProductApproved", repo.labels.agentProductApproved),
|
|
92
|
+
buildLabel("agentPlanApproved", repo.labels.agentPlanApproved),
|
|
93
|
+
buildLabel("agentReview", repo.labels.agentReview),
|
|
94
|
+
buildLabel("planBreakdownNeeded", repo.labels.planBreakdownNeeded),
|
|
95
|
+
buildLabel("agentSyncParentBranch", repo.labels.agentSyncParentBranch),
|
|
96
|
+
buildLabel("prepareForMerge", repo.labels.prepareForMerge)
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
function buildLabel(key, name) {
|
|
100
|
+
return {
|
|
101
|
+
name,
|
|
102
|
+
...LABEL_DEFINITIONS[key]
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -1,8 +1,20 @@
|
|
|
1
|
+
import { defaultDemoRepoName } from "../services/demo-repo.js";
|
|
1
2
|
export class SelectReposStep {
|
|
3
|
+
demoRepoCreator;
|
|
2
4
|
name = "select-repos";
|
|
3
5
|
description = "Select repositories to manage";
|
|
6
|
+
constructor(demoRepoCreator) {
|
|
7
|
+
this.demoRepoCreator = demoRepoCreator;
|
|
8
|
+
}
|
|
4
9
|
async run(ctx, prompter) {
|
|
5
10
|
const { discoveredRepos } = ctx;
|
|
11
|
+
const setupChoice = await this.promptSetupChoice(ctx, prompter);
|
|
12
|
+
if (setupChoice === "demo") {
|
|
13
|
+
return await this.promptCreateDemoRepo(ctx, prompter);
|
|
14
|
+
}
|
|
15
|
+
if (setupChoice === "skip") {
|
|
16
|
+
return this.noRepositoryWarning();
|
|
17
|
+
}
|
|
6
18
|
if (discoveredRepos.length === 0) {
|
|
7
19
|
return await this.promptManualEntry(ctx, prompter);
|
|
8
20
|
}
|
|
@@ -29,15 +41,7 @@ export class SelectReposStep {
|
|
|
29
41
|
}
|
|
30
42
|
}
|
|
31
43
|
if (selected.length === 0) {
|
|
32
|
-
|
|
33
|
-
if (fallback) {
|
|
34
|
-
return await this.promptManualEntry(ctx, prompter);
|
|
35
|
-
}
|
|
36
|
-
return {
|
|
37
|
-
status: "warning",
|
|
38
|
-
message: "No repositories selected",
|
|
39
|
-
details: ["You can add repositories by editing repos.json later."]
|
|
40
|
-
};
|
|
44
|
+
return await this.promptNoSelection(ctx, prompter);
|
|
41
45
|
}
|
|
42
46
|
ctx.selectedRepos = selected;
|
|
43
47
|
return {
|
|
@@ -49,6 +53,95 @@ export class SelectReposStep {
|
|
|
49
53
|
})
|
|
50
54
|
};
|
|
51
55
|
}
|
|
56
|
+
async promptSetupChoice(ctx, prompter) {
|
|
57
|
+
const hasDiscoveredRepos = ctx.discoveredRepos.length > 0;
|
|
58
|
+
const options = [
|
|
59
|
+
hasDiscoveredRepos
|
|
60
|
+
? "Select existing repositories (Backburner will create/update its labels on them)"
|
|
61
|
+
: "Enter an existing repository manually",
|
|
62
|
+
...(ctx.githubUser !== undefined ? ["Create a private Backburner demo repo"] : []),
|
|
63
|
+
"Skip repository setup for now"
|
|
64
|
+
];
|
|
65
|
+
const choice = await prompter.choose("How do you want to set up repositories?", options, 0);
|
|
66
|
+
if (choice === 0) {
|
|
67
|
+
return "existing";
|
|
68
|
+
}
|
|
69
|
+
if (ctx.githubUser !== undefined && choice === 1) {
|
|
70
|
+
return "demo";
|
|
71
|
+
}
|
|
72
|
+
return "skip";
|
|
73
|
+
}
|
|
74
|
+
async promptNoSelection(ctx, prompter) {
|
|
75
|
+
const options = [
|
|
76
|
+
"Add an existing repository manually",
|
|
77
|
+
...(ctx.githubUser !== undefined ? ["Create a private Backburner demo repo"] : []),
|
|
78
|
+
"Skip repository setup for now"
|
|
79
|
+
];
|
|
80
|
+
const defaultIndex = 0;
|
|
81
|
+
const choice = await prompter.choose("No repositories selected. How do you want to continue?", options, defaultIndex);
|
|
82
|
+
if (choice === 0) {
|
|
83
|
+
return await this.promptManualEntry(ctx, prompter);
|
|
84
|
+
}
|
|
85
|
+
if (ctx.githubUser !== undefined && choice === 1) {
|
|
86
|
+
return await this.promptCreateDemoRepo(ctx, prompter);
|
|
87
|
+
}
|
|
88
|
+
return this.noRepositoryWarning();
|
|
89
|
+
}
|
|
90
|
+
async promptCreateDemoRepo(ctx, prompter) {
|
|
91
|
+
if (ctx.githubUser === undefined) {
|
|
92
|
+
return this.noRepositoryWarning();
|
|
93
|
+
}
|
|
94
|
+
if (this.demoRepoCreator === undefined) {
|
|
95
|
+
return {
|
|
96
|
+
status: "warning",
|
|
97
|
+
message: "Private demo repo creation is unavailable",
|
|
98
|
+
details: ["You can create a private repository manually and re-run onboarding."]
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const repoName = (await prompter.ask("Private demo repository name:", defaultDemoRepoName())).trim();
|
|
102
|
+
if (!isValidRepoName(repoName)) {
|
|
103
|
+
return {
|
|
104
|
+
status: "warning",
|
|
105
|
+
message: "Private demo repo was not created",
|
|
106
|
+
details: ["Repository names can contain letters, numbers, dots, underscores, and hyphens."]
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const confirmed = await prompter.confirm(`Create private GitHub repository ${ctx.githubUser}/${repoName} and clone it under ${ctx.codeRoot}?`, false);
|
|
110
|
+
if (!confirmed) {
|
|
111
|
+
return this.noRepositoryWarning();
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const repo = await this.demoRepoCreator.createPrivateDemoRepo({
|
|
115
|
+
owner: ctx.githubUser,
|
|
116
|
+
name: repoName,
|
|
117
|
+
codeRoot: ctx.codeRoot
|
|
118
|
+
});
|
|
119
|
+
ctx.discoveredRepos = [repo, ...ctx.discoveredRepos];
|
|
120
|
+
ctx.selectedRepos = [repo];
|
|
121
|
+
return {
|
|
122
|
+
status: "passed",
|
|
123
|
+
message: `Private demo repository selected: ${repo.owner}/${repo.name}`,
|
|
124
|
+
details: [
|
|
125
|
+
"Visibility: private",
|
|
126
|
+
`Local clone: ${repo.localPath ?? `${repo.owner}/${repo.name}`}`
|
|
127
|
+
]
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
return {
|
|
132
|
+
status: "failed",
|
|
133
|
+
message: `Failed to create private demo repo: ${err instanceof Error ? err.message : String(err)}`,
|
|
134
|
+
issues: [
|
|
135
|
+
{
|
|
136
|
+
title: "Private demo repo creation failed",
|
|
137
|
+
cause: err instanceof Error ? err.message : String(err),
|
|
138
|
+
suggestedFix: "Create a private GitHub repository manually, clone it locally, then re-run onboarding.",
|
|
139
|
+
canAutoRemediate: false
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
52
145
|
async promptManualEntry(ctx, prompter) {
|
|
53
146
|
const owner = (await prompter.ask("Repository owner (e.g. myorg):")).trim();
|
|
54
147
|
const name = (await prompter.ask("Repository name (e.g. my-repo):")).trim();
|
|
@@ -73,4 +166,14 @@ export class SelectReposStep {
|
|
|
73
166
|
message: `1 repository selected: ${owner}/${name}`
|
|
74
167
|
};
|
|
75
168
|
}
|
|
169
|
+
noRepositoryWarning() {
|
|
170
|
+
return {
|
|
171
|
+
status: "warning",
|
|
172
|
+
message: "No repositories selected",
|
|
173
|
+
details: ["You can add repositories by editing repos.json later."]
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function isValidRepoName(name) {
|
|
178
|
+
return /^[A-Za-z0-9._-]+$/.test(name);
|
|
76
179
|
}
|