agentic-dev 0.2.12 → 0.2.13
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/.agent/prd.json +29 -0
- package/.agent/progress.txt +1 -0
- package/.agent/prompt.md +21 -0
- package/.agent/ralph-loop-state.json +13 -0
- package/.agent/ralph-supervisor-state.json +12 -0
- package/.agent/ralph-supervisor.sh +238 -0
- package/.agent/ralph.sh +305 -0
- package/.agent/runs/README.md +7 -0
- package/.agent/sdd-build-ast-audit.json +13 -0
- package/.claude/CLAUDE.md +44 -0
- package/.claude/agentic-dev.json +3 -0
- package/.claude/agents/ai-dev.md +27 -0
- package/.claude/agents/backend-dev.md +26 -0
- package/.claude/agents/db-dev.md +26 -0
- package/.claude/agents/devops.md +27 -0
- package/.claude/agents/frontend-dev.md +25 -0
- package/.claude/agents/github-ops.md +25 -0
- package/.claude/agents/test-dev.md +26 -0
- package/.claude/agents/uiux-designer.md +25 -0
- package/.claude/settings.json +49 -0
- package/.claude/settings.local.json +8 -0
- package/.claude/skills/sdd/SKILL.md +189 -0
- package/.claude/skills/sdd/agents/openai.yaml +4 -0
- package/.claude/skills/sdd/references/section-map.md +67 -0
- package/.claude/workspace-config.json +3 -0
- package/.codex/agentic-dev.json +3 -0
- package/.codex/agents/README.md +22 -0
- package/.codex/agents/api.toml +11 -0
- package/.codex/agents/architecture.toml +11 -0
- package/.codex/agents/ci.toml +11 -0
- package/.codex/agents/gitops.toml +11 -0
- package/.codex/agents/orchestrator.toml +11 -0
- package/.codex/agents/quality.toml +11 -0
- package/.codex/agents/runtime.toml +11 -0
- package/.codex/agents/security.toml +11 -0
- package/.codex/agents/specs.toml +11 -0
- package/.codex/agents/ui.toml +11 -0
- package/.codex/config.toml +46 -0
- package/.codex/skills/SKILL.md +13 -0
- package/.codex/skills/sdd/SKILL.md +189 -0
- package/.codex/skills/sdd/agents/openai.yaml +4 -0
- package/.codex/skills/sdd/references/section-map.md +67 -0
- package/README.md +69 -58
- package/bin/agentic-dev.mjs +162 -11
- package/lib/github.mjs +246 -0
- package/lib/orchestration-assets.mjs +249 -0
- package/lib/scaffold.mjs +89 -0
- package/package.json +5 -2
package/bin/agentic-dev.mjs
CHANGED
|
@@ -8,15 +8,18 @@ import {
|
|
|
8
8
|
DEFAULT_TEMPLATE_OWNER,
|
|
9
9
|
ensureTargetDir,
|
|
10
10
|
fetchTemplateRepos,
|
|
11
|
+
finalizeRepositoryGit,
|
|
11
12
|
installTemplateRepo,
|
|
12
13
|
parseArgs,
|
|
13
14
|
resolveTemplateRepo,
|
|
15
|
+
resolveGitHubOrchestration,
|
|
14
16
|
usage,
|
|
15
17
|
} from "../lib/scaffold.mjs";
|
|
16
18
|
|
|
17
19
|
const DEFAULT_TARGET_DIR = ".";
|
|
18
20
|
const DEFAULT_APP_MODE = "fullstack";
|
|
19
21
|
const DEFAULT_AI_PROVIDERS = ["codex", "claude"];
|
|
22
|
+
const DEFAULT_PROVIDER_PROFILES = ["codex-subscription", "claude-subscription"];
|
|
20
23
|
|
|
21
24
|
const GITHUB_AUTH_CHOICES = [
|
|
22
25
|
{
|
|
@@ -72,6 +75,28 @@ const AI_PROVIDER_CHOICES = [
|
|
|
72
75
|
},
|
|
73
76
|
];
|
|
74
77
|
|
|
78
|
+
const PROVIDER_PROFILE_CHOICES = [
|
|
79
|
+
{ label: "codex-subscription", value: "codex-subscription", description: "Codex subscription/runtime access" },
|
|
80
|
+
{ label: "codex-api", value: "codex-api", description: "Codex API access" },
|
|
81
|
+
{ label: "claude-subscription", value: "claude-subscription", description: "Claude subscription/runtime access" },
|
|
82
|
+
{ label: "claude-api", value: "claude-api", description: "Claude API access" },
|
|
83
|
+
{ label: "openai-api", value: "openai-api", description: "OpenAI API access" },
|
|
84
|
+
{ label: "ollama-self-hosted", value: "ollama-self-hosted", description: "Local Ollama runtime" },
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const GITHUB_PROJECT_MODE_CHOICES = [
|
|
88
|
+
{
|
|
89
|
+
label: "create-if-missing",
|
|
90
|
+
value: "create-if-missing",
|
|
91
|
+
description: "Use an existing GitHub Project with the same title, otherwise create it",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
label: "use-existing",
|
|
95
|
+
value: "use-existing",
|
|
96
|
+
description: "Require an existing GitHub Project and fail if it does not exist",
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
|
|
75
100
|
const CONFIRM_CHOICES = [
|
|
76
101
|
{
|
|
77
102
|
label: "Proceed",
|
|
@@ -128,6 +153,16 @@ function uniqueProviders(providers) {
|
|
|
128
153
|
: [];
|
|
129
154
|
}
|
|
130
155
|
|
|
156
|
+
function normalizeProviderProfiles(providerProfiles) {
|
|
157
|
+
const normalized = Array.isArray(providerProfiles)
|
|
158
|
+
? providerProfiles.map((profile) => profile.trim()).filter(Boolean)
|
|
159
|
+
: [];
|
|
160
|
+
if (normalized.length > 0) {
|
|
161
|
+
return [...new Set(normalized)];
|
|
162
|
+
}
|
|
163
|
+
return [...DEFAULT_PROVIDER_PROFILES];
|
|
164
|
+
}
|
|
165
|
+
|
|
131
166
|
function hydrateOptions(options) {
|
|
132
167
|
const state = {
|
|
133
168
|
targetDir: (options.targetDir || DEFAULT_TARGET_DIR).trim() || DEFAULT_TARGET_DIR,
|
|
@@ -137,6 +172,10 @@ function hydrateOptions(options) {
|
|
|
137
172
|
githubPat: options.githubPat || "",
|
|
138
173
|
appMode: (options.appMode || DEFAULT_APP_MODE).trim() || DEFAULT_APP_MODE,
|
|
139
174
|
aiProviders: normalizeProviders(options.aiProviders),
|
|
175
|
+
providerProfiles: normalizeProviderProfiles(options.providerProfiles),
|
|
176
|
+
githubRepositoryInput: (options.githubRepositoryInput || "").trim(),
|
|
177
|
+
githubProjectMode: (options.githubProjectMode || "create-if-missing").trim() || "create-if-missing",
|
|
178
|
+
githubProjectTitle: (options.githubProjectTitle || "").trim(),
|
|
140
179
|
force: Boolean(options.force),
|
|
141
180
|
skipBootstrap: Boolean(options.skipBootstrap),
|
|
142
181
|
owner: options.owner || DEFAULT_TEMPLATE_OWNER,
|
|
@@ -145,6 +184,12 @@ function hydrateOptions(options) {
|
|
|
145
184
|
if (!state.projectName) {
|
|
146
185
|
state.projectName = inferProjectName(state.targetDir);
|
|
147
186
|
}
|
|
187
|
+
if (!state.githubRepositoryInput) {
|
|
188
|
+
state.githubRepositoryInput = state.projectName;
|
|
189
|
+
}
|
|
190
|
+
if (!state.githubProjectTitle) {
|
|
191
|
+
state.githubProjectTitle = `${state.projectName} Delivery`;
|
|
192
|
+
}
|
|
148
193
|
|
|
149
194
|
return state;
|
|
150
195
|
}
|
|
@@ -166,6 +211,7 @@ function sanitizeStateForInstall(state) {
|
|
|
166
211
|
targetDir: (state.targetDir || DEFAULT_TARGET_DIR).trim() || DEFAULT_TARGET_DIR,
|
|
167
212
|
projectName: (state.projectName || inferProjectName(state.targetDir)).trim(),
|
|
168
213
|
aiProviders: normalizeProviders(state.aiProviders),
|
|
214
|
+
providerProfiles: normalizeProviderProfiles(state.providerProfiles),
|
|
169
215
|
};
|
|
170
216
|
}
|
|
171
217
|
|
|
@@ -193,6 +239,13 @@ function buildSteps(state, repos) {
|
|
|
193
239
|
description: "Written into scaffold metadata and Claude workspace nickname.",
|
|
194
240
|
placeholder: inferProjectName(state.targetDir),
|
|
195
241
|
},
|
|
242
|
+
{
|
|
243
|
+
key: "githubRepositoryInput",
|
|
244
|
+
type: "text",
|
|
245
|
+
label: "GitHub repository",
|
|
246
|
+
description: "Use owner/name, a GitHub URL, or just a repo name to create/use the repository.",
|
|
247
|
+
placeholder: state.projectName,
|
|
248
|
+
},
|
|
196
249
|
{
|
|
197
250
|
key: "githubAuthMode",
|
|
198
251
|
type: "single",
|
|
@@ -213,6 +266,20 @@ function buildSteps(state, repos) {
|
|
|
213
266
|
}
|
|
214
267
|
|
|
215
268
|
steps.push(
|
|
269
|
+
{
|
|
270
|
+
key: "githubProjectMode",
|
|
271
|
+
type: "single",
|
|
272
|
+
label: "GitHub project mode",
|
|
273
|
+
description: "Choose whether the orchestration must reuse an existing GitHub Project or create one if needed.",
|
|
274
|
+
choices: GITHUB_PROJECT_MODE_CHOICES,
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
key: "githubProjectTitle",
|
|
278
|
+
type: "text",
|
|
279
|
+
label: "GitHub project title",
|
|
280
|
+
description: "This project is used for SDD task orchestration.",
|
|
281
|
+
placeholder: `${state.projectName} Delivery`,
|
|
282
|
+
},
|
|
216
283
|
{
|
|
217
284
|
key: "template",
|
|
218
285
|
type: "single",
|
|
@@ -227,11 +294,18 @@ function buildSteps(state, repos) {
|
|
|
227
294
|
description: "Choose how much of the bootstrap flow should run after clone.",
|
|
228
295
|
choices: APP_MODE_CHOICES,
|
|
229
296
|
},
|
|
297
|
+
{
|
|
298
|
+
key: "providerProfiles",
|
|
299
|
+
type: "multi",
|
|
300
|
+
label: "AI runtime profiles",
|
|
301
|
+
description: "Use Space to toggle provider/runtime profiles.",
|
|
302
|
+
choices: PROVIDER_PROFILE_CHOICES,
|
|
303
|
+
},
|
|
230
304
|
{
|
|
231
305
|
key: "aiProviders",
|
|
232
306
|
type: "multi",
|
|
233
|
-
label: "
|
|
234
|
-
description: "Use Space to toggle
|
|
307
|
+
label: "Workspace agent surfaces",
|
|
308
|
+
description: "Use Space to toggle Codex/Claude workspace assets.",
|
|
235
309
|
choices: AI_PROVIDER_CHOICES,
|
|
236
310
|
},
|
|
237
311
|
);
|
|
@@ -275,6 +349,12 @@ function stepValue(state, step) {
|
|
|
275
349
|
if (step.key === "projectName") {
|
|
276
350
|
return state.projectName || step.placeholder || inferProjectName(state.targetDir);
|
|
277
351
|
}
|
|
352
|
+
if (step.key === "githubRepositoryInput") {
|
|
353
|
+
return state.githubRepositoryInput || step.placeholder || state.projectName;
|
|
354
|
+
}
|
|
355
|
+
if (step.key === "githubProjectTitle") {
|
|
356
|
+
return state.githubProjectTitle || step.placeholder || `${state.projectName} Delivery`;
|
|
357
|
+
}
|
|
278
358
|
if (step.key === "githubPat") {
|
|
279
359
|
return state.githubPat || "";
|
|
280
360
|
}
|
|
@@ -356,10 +436,14 @@ function executionPreview(state, selectedRepo) {
|
|
|
356
436
|
const lines = [
|
|
357
437
|
`Project directory: ${path.resolve(process.cwd(), state.targetDir)}`,
|
|
358
438
|
`Project name: ${state.projectName}`,
|
|
439
|
+
`GitHub repository: ${state.githubRepositoryInput}`,
|
|
440
|
+
`GitHub project mode: ${state.githubProjectMode}`,
|
|
441
|
+
`GitHub project title: ${state.githubProjectTitle}`,
|
|
359
442
|
`GitHub auth mode: ${state.githubAuthMode}`,
|
|
360
443
|
`Template repo: ${selectedRepo?.name || state.template}`,
|
|
361
444
|
`App mode: ${state.appMode}`,
|
|
362
445
|
`AI providers: ${state.aiProviders.join(", ")}`,
|
|
446
|
+
`Provider profiles: ${state.providerProfiles.join(", ")}`,
|
|
363
447
|
`Allow non-empty directory: ${state.force ? "yes" : "no"}`,
|
|
364
448
|
];
|
|
365
449
|
|
|
@@ -368,14 +452,17 @@ function executionPreview(state, selectedRepo) {
|
|
|
368
452
|
}
|
|
369
453
|
|
|
370
454
|
lines.push("Run plan:");
|
|
371
|
-
lines.push(` 1.
|
|
372
|
-
lines.push(
|
|
373
|
-
lines.push(
|
|
455
|
+
lines.push(` 1. Ensure GitHub repo ${state.githubRepositoryInput}`);
|
|
456
|
+
lines.push(` 2. Ensure GitHub Project ${state.githubProjectTitle}`);
|
|
457
|
+
lines.push(` 3. Clone ${selectedRepo?.name || state.template}`);
|
|
458
|
+
lines.push(" 4. Install shared sub-agent assets and orchestration workflow");
|
|
459
|
+
lines.push(" 5. Copy .env.example to .env if needed");
|
|
460
|
+
lines.push(" 6. Run pnpm install");
|
|
374
461
|
if (state.appMode === "backend") {
|
|
375
|
-
lines.push("
|
|
462
|
+
lines.push(" 7. Skip browser install and parity bootstrap");
|
|
376
463
|
} else {
|
|
377
|
-
lines.push("
|
|
378
|
-
lines.push("
|
|
464
|
+
lines.push(" 7. Install Playwright Chromium for the default frontend target");
|
|
465
|
+
lines.push(" 8. Run frontend parity bootstrap");
|
|
379
466
|
}
|
|
380
467
|
|
|
381
468
|
return lines;
|
|
@@ -464,6 +551,22 @@ function validateStepValue(step, state, value) {
|
|
|
464
551
|
return trimmed;
|
|
465
552
|
}
|
|
466
553
|
|
|
554
|
+
if (step.key === "githubRepositoryInput") {
|
|
555
|
+
const trimmed = value.trim();
|
|
556
|
+
if (!trimmed) {
|
|
557
|
+
throw new Error("GitHub repository cannot be empty.");
|
|
558
|
+
}
|
|
559
|
+
return trimmed;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (step.key === "githubProjectTitle") {
|
|
563
|
+
const trimmed = value.trim();
|
|
564
|
+
if (!trimmed) {
|
|
565
|
+
throw new Error("GitHub project title cannot be empty.");
|
|
566
|
+
}
|
|
567
|
+
return trimmed;
|
|
568
|
+
}
|
|
569
|
+
|
|
467
570
|
if (step.key === "githubPat") {
|
|
468
571
|
const trimmed = value.trim();
|
|
469
572
|
if (!trimmed) {
|
|
@@ -480,6 +583,14 @@ function validateStepValue(step, state, value) {
|
|
|
480
583
|
return providers;
|
|
481
584
|
}
|
|
482
585
|
|
|
586
|
+
if (step.key === "providerProfiles") {
|
|
587
|
+
const profiles = normalizeProviderProfiles(value);
|
|
588
|
+
if (profiles.length === 0) {
|
|
589
|
+
throw new Error("Select at least one provider profile.");
|
|
590
|
+
}
|
|
591
|
+
return profiles;
|
|
592
|
+
}
|
|
593
|
+
|
|
483
594
|
if (step.key === "force" && value === false) {
|
|
484
595
|
throw new Error("Prompt cancelled");
|
|
485
596
|
}
|
|
@@ -603,14 +714,22 @@ async function runWizard(initialState, repos) {
|
|
|
603
714
|
}
|
|
604
715
|
|
|
605
716
|
if (step.type === "multi" && chunk === " ") {
|
|
606
|
-
const selected = new Set(
|
|
717
|
+
const selected = new Set(
|
|
718
|
+
step.key === "providerProfiles"
|
|
719
|
+
? normalizeProviderProfiles(state.providerProfiles)
|
|
720
|
+
: uniqueProviders(state.aiProviders),
|
|
721
|
+
);
|
|
607
722
|
const currentChoice = step.choices[cursors.get(step.key)];
|
|
608
723
|
if (selected.has(currentChoice.value)) {
|
|
609
724
|
selected.delete(currentChoice.value);
|
|
610
725
|
} else {
|
|
611
726
|
selected.add(currentChoice.value);
|
|
612
727
|
}
|
|
613
|
-
|
|
728
|
+
if (step.key === "providerProfiles") {
|
|
729
|
+
state.providerProfiles = [...selected];
|
|
730
|
+
} else {
|
|
731
|
+
state.aiProviders = [...selected];
|
|
732
|
+
}
|
|
614
733
|
rerender();
|
|
615
734
|
return undefined;
|
|
616
735
|
}
|
|
@@ -698,6 +817,10 @@ async function promptForMultiChoice(rl, label, choices, defaults) {
|
|
|
698
817
|
}
|
|
699
818
|
}
|
|
700
819
|
|
|
820
|
+
async function promptMultiProfiles(rl, label, choices, defaults) {
|
|
821
|
+
return promptForMultiChoice(rl, label, choices, defaults);
|
|
822
|
+
}
|
|
823
|
+
|
|
701
824
|
function printFallbackPlan(state, repos) {
|
|
702
825
|
const selectedRepo = resolveTemplateRepo(state.template, repos);
|
|
703
826
|
console.log("");
|
|
@@ -719,6 +842,9 @@ async function promptLinearly(initialState, repos) {
|
|
|
719
842
|
const projectNameAnswer = await rl.question(`Project name [${state.projectName || defaultProjectName}]: `);
|
|
720
843
|
state.projectName = projectNameAnswer.trim() || state.projectName || defaultProjectName;
|
|
721
844
|
|
|
845
|
+
const repoAnswer = await rl.question(`GitHub repository [${state.githubRepositoryInput || state.projectName}]: `);
|
|
846
|
+
state.githubRepositoryInput = repoAnswer.trim() || state.githubRepositoryInput || state.projectName;
|
|
847
|
+
|
|
722
848
|
state.githubAuthMode = await promptForChoice(rl, "GitHub auth mode", GITHUB_AUTH_CHOICES);
|
|
723
849
|
if (state.githubAuthMode === "pat") {
|
|
724
850
|
state.githubPat = (await rl.question("GitHub PAT: ")).trim();
|
|
@@ -727,8 +853,19 @@ async function promptLinearly(initialState, repos) {
|
|
|
727
853
|
}
|
|
728
854
|
}
|
|
729
855
|
|
|
856
|
+
state.githubProjectMode = await promptForChoice(rl, "GitHub project mode", GITHUB_PROJECT_MODE_CHOICES);
|
|
857
|
+
const projectTitleAnswer = await rl.question(`GitHub project title [${state.githubProjectTitle || `${state.projectName} Delivery`}]: `);
|
|
858
|
+
state.githubProjectTitle =
|
|
859
|
+
projectTitleAnswer.trim() || state.githubProjectTitle || `${state.projectName} Delivery`;
|
|
860
|
+
|
|
730
861
|
state.template = await promptForChoice(rl, `Template repo from ${state.owner}`, buildTemplateChoices(repos));
|
|
731
862
|
state.appMode = await promptForChoice(rl, "App mode", APP_MODE_CHOICES);
|
|
863
|
+
state.providerProfiles = await promptMultiProfiles(
|
|
864
|
+
rl,
|
|
865
|
+
"AI runtime profiles",
|
|
866
|
+
PROVIDER_PROFILE_CHOICES,
|
|
867
|
+
state.providerProfiles,
|
|
868
|
+
);
|
|
732
869
|
state.aiProviders = await promptForMultiChoice(rl, "AI providers", AI_PROVIDER_CHOICES, state.aiProviders);
|
|
733
870
|
|
|
734
871
|
if (!state.force && directoryHasUserFiles(state.targetDir)) {
|
|
@@ -789,6 +926,12 @@ function validateNonInteractiveState(state) {
|
|
|
789
926
|
if (!state.projectName) {
|
|
790
927
|
state.projectName = inferProjectName(state.targetDir);
|
|
791
928
|
}
|
|
929
|
+
if (!state.githubRepositoryInput) {
|
|
930
|
+
state.githubRepositoryInput = state.projectName;
|
|
931
|
+
}
|
|
932
|
+
if (!state.githubProjectTitle) {
|
|
933
|
+
state.githubProjectTitle = `${state.projectName} Delivery`;
|
|
934
|
+
}
|
|
792
935
|
if (state.githubAuthMode === "pat" && !state.githubPat) {
|
|
793
936
|
throw new Error("`--github-auth pat` requires `--github-pat`.");
|
|
794
937
|
}
|
|
@@ -843,6 +986,7 @@ async function main() {
|
|
|
843
986
|
: await promptLinearly(state, repos);
|
|
844
987
|
|
|
845
988
|
applyRuntimeGitHubAuth(resolvedState);
|
|
989
|
+
const orchestration = await resolveGitHubOrchestration(resolvedState);
|
|
846
990
|
const selectedRepo = resolveTemplateRepo(resolvedState.template, repos);
|
|
847
991
|
const destinationRoot = ensureTargetDir(resolvedState.targetDir, {
|
|
848
992
|
force: resolvedState.force,
|
|
@@ -850,9 +994,16 @@ async function main() {
|
|
|
850
994
|
const result = installTemplateRepo({
|
|
851
995
|
destinationRoot: path.resolve(destinationRoot),
|
|
852
996
|
templateRepo: selectedRepo,
|
|
853
|
-
setupSelections:
|
|
997
|
+
setupSelections: {
|
|
998
|
+
...resolvedState,
|
|
999
|
+
...orchestration,
|
|
1000
|
+
},
|
|
854
1001
|
skipBootstrap: resolvedState.skipBootstrap,
|
|
855
1002
|
});
|
|
1003
|
+
finalizeRepositoryGit(path.resolve(destinationRoot), {
|
|
1004
|
+
...resolvedState,
|
|
1005
|
+
...orchestration,
|
|
1006
|
+
});
|
|
856
1007
|
printSuccess(result);
|
|
857
1008
|
} catch (error) {
|
|
858
1009
|
console.error(error.message);
|
package/lib/github.mjs
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
function ghToken() {
|
|
5
|
+
const envToken =
|
|
6
|
+
process.env.GH_TOKEN || process.env.GITHUB_TOKEN || process.env.AGENTIC_GITHUB_TOKEN || "";
|
|
7
|
+
if (envToken) {
|
|
8
|
+
return envToken;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const result = spawnSync("gh", ["auth", "token"], {
|
|
12
|
+
encoding: "utf-8",
|
|
13
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
14
|
+
});
|
|
15
|
+
if (result.status === 0) {
|
|
16
|
+
return (result.stdout || "").trim();
|
|
17
|
+
}
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function githubRequest(url, { method = "GET", body } = {}) {
|
|
22
|
+
const token = ghToken();
|
|
23
|
+
if (!token) {
|
|
24
|
+
throw new Error("GitHub authentication is required. Set GH_TOKEN, GITHUB_TOKEN, or AGENTIC_GITHUB_TOKEN.");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
method,
|
|
29
|
+
headers: {
|
|
30
|
+
Accept: "application/vnd.github+json",
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
"User-Agent": "agentic-dev",
|
|
33
|
+
Authorization: `Bearer ${token}`,
|
|
34
|
+
},
|
|
35
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
const payload = await response.text();
|
|
40
|
+
throw new Error(`GitHub API request failed (${response.status}): ${payload}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (response.status === 204) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return response.json();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function githubGraphql(query, variables = {}) {
|
|
50
|
+
const token = ghToken();
|
|
51
|
+
if (!token) {
|
|
52
|
+
throw new Error("GitHub authentication is required. Set GH_TOKEN, GITHUB_TOKEN, or AGENTIC_GITHUB_TOKEN.");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const response = await fetch("https://api.github.com/graphql", {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: {
|
|
58
|
+
Accept: "application/vnd.github+json",
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
"User-Agent": "agentic-dev",
|
|
61
|
+
Authorization: `Bearer ${token}`,
|
|
62
|
+
},
|
|
63
|
+
body: JSON.stringify({ query, variables }),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const payload = await response.json();
|
|
67
|
+
if (!response.ok || payload.errors) {
|
|
68
|
+
throw new Error(`GitHub GraphQL request failed: ${JSON.stringify(payload.errors || payload)}`);
|
|
69
|
+
}
|
|
70
|
+
return payload.data;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function normalizeRepoInput(input, { fallbackOwner = "say828", fallbackName = "agentic-service" } = {}) {
|
|
74
|
+
const normalized = (input || "").trim();
|
|
75
|
+
if (!normalized) {
|
|
76
|
+
return {
|
|
77
|
+
owner: fallbackOwner,
|
|
78
|
+
name: fallbackName,
|
|
79
|
+
slug: `${fallbackOwner}/${fallbackName}`,
|
|
80
|
+
cloneUrl: `https://github.com/${fallbackOwner}/${fallbackName}.git`,
|
|
81
|
+
htmlUrl: `https://github.com/${fallbackOwner}/${fallbackName}`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const withoutGit = normalized.replace(/\.git$/, "");
|
|
86
|
+
const httpsMatch = withoutGit.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)$/);
|
|
87
|
+
if (httpsMatch) {
|
|
88
|
+
const [, owner, name] = httpsMatch;
|
|
89
|
+
return {
|
|
90
|
+
owner,
|
|
91
|
+
name,
|
|
92
|
+
slug: `${owner}/${name}`,
|
|
93
|
+
cloneUrl: `https://github.com/${owner}/${name}.git`,
|
|
94
|
+
htmlUrl: `https://github.com/${owner}/${name}`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const slugMatch = withoutGit.match(/^([^/]+)\/([^/]+)$/);
|
|
99
|
+
if (slugMatch) {
|
|
100
|
+
const [, owner, name] = slugMatch;
|
|
101
|
+
return {
|
|
102
|
+
owner,
|
|
103
|
+
name,
|
|
104
|
+
slug: `${owner}/${name}`,
|
|
105
|
+
cloneUrl: `https://github.com/${owner}/${name}.git`,
|
|
106
|
+
htmlUrl: `https://github.com/${owner}/${name}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
owner: fallbackOwner,
|
|
112
|
+
name: withoutGit,
|
|
113
|
+
slug: `${fallbackOwner}/${withoutGit}`,
|
|
114
|
+
cloneUrl: `https://github.com/${fallbackOwner}/${withoutGit}.git`,
|
|
115
|
+
htmlUrl: `https://github.com/${fallbackOwner}/${withoutGit}`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function getAuthenticatedViewer() {
|
|
120
|
+
const data = await githubGraphql(`
|
|
121
|
+
query ViewerIdentity {
|
|
122
|
+
viewer {
|
|
123
|
+
login
|
|
124
|
+
id
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
`);
|
|
128
|
+
return data.viewer;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function ensureGitHubRepository(repoInput, { visibility = "private" } = {}) {
|
|
132
|
+
const viewer = await getAuthenticatedViewer();
|
|
133
|
+
const target = normalizeRepoInput(repoInput, {
|
|
134
|
+
fallbackOwner: viewer.login,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const existing = await githubRequest(`https://api.github.com/repos/${target.owner}/${target.name}`);
|
|
139
|
+
return {
|
|
140
|
+
owner: existing.owner.login,
|
|
141
|
+
name: existing.name,
|
|
142
|
+
slug: existing.full_name,
|
|
143
|
+
cloneUrl: existing.clone_url,
|
|
144
|
+
htmlUrl: existing.html_url,
|
|
145
|
+
created: false,
|
|
146
|
+
};
|
|
147
|
+
} catch (error) {
|
|
148
|
+
const message = String(error.message || error);
|
|
149
|
+
if (!message.includes("(404)")) {
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (target.owner !== viewer.login) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`Repository ${target.slug} does not exist. Automatic creation currently supports only the authenticated user namespace (${viewer.login}).`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const created = await githubRequest("https://api.github.com/user/repos", {
|
|
161
|
+
method: "POST",
|
|
162
|
+
body: {
|
|
163
|
+
name: target.name,
|
|
164
|
+
private: visibility !== "public",
|
|
165
|
+
auto_init: false,
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
owner: created.owner.login,
|
|
171
|
+
name: created.name,
|
|
172
|
+
slug: created.full_name,
|
|
173
|
+
cloneUrl: created.clone_url,
|
|
174
|
+
htmlUrl: created.html_url,
|
|
175
|
+
created: true,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function ensureGitHubProject({ ownerLogin, title, mode = "create-if-missing" }) {
|
|
180
|
+
const data = await githubGraphql(
|
|
181
|
+
`
|
|
182
|
+
query OwnerProjects($login: String!) {
|
|
183
|
+
user(login: $login) {
|
|
184
|
+
id
|
|
185
|
+
projectsV2(first: 50) {
|
|
186
|
+
nodes {
|
|
187
|
+
id
|
|
188
|
+
title
|
|
189
|
+
number
|
|
190
|
+
url
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
`,
|
|
196
|
+
{ login: ownerLogin },
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const owner = data.user;
|
|
200
|
+
if (!owner) {
|
|
201
|
+
throw new Error(`Unable to load GitHub projects for ${ownerLogin}.`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const existing = owner.projectsV2.nodes.find((project) => project.title === title);
|
|
205
|
+
if (existing) {
|
|
206
|
+
return {
|
|
207
|
+
id: existing.id,
|
|
208
|
+
title: existing.title,
|
|
209
|
+
number: existing.number,
|
|
210
|
+
url: existing.url,
|
|
211
|
+
created: false,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (mode !== "create-if-missing") {
|
|
216
|
+
throw new Error(`GitHub project not found: ${title}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const created = await githubGraphql(
|
|
220
|
+
`
|
|
221
|
+
mutation CreateProject($ownerId: ID!, $title: String!) {
|
|
222
|
+
createProjectV2(input: { ownerId: $ownerId, title: $title }) {
|
|
223
|
+
projectV2 {
|
|
224
|
+
id
|
|
225
|
+
title
|
|
226
|
+
number
|
|
227
|
+
url
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
`,
|
|
232
|
+
{
|
|
233
|
+
ownerId: owner.id,
|
|
234
|
+
title,
|
|
235
|
+
},
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const project = created.createProjectV2.projectV2;
|
|
239
|
+
return {
|
|
240
|
+
id: project.id,
|
|
241
|
+
title: project.title,
|
|
242
|
+
number: project.number,
|
|
243
|
+
url: project.url,
|
|
244
|
+
created: true,
|
|
245
|
+
};
|
|
246
|
+
}
|