@goodtek/vibeops 0.7.22 → 1.1.1

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.
Files changed (144) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +51 -380
  3. package/dist/bootstrap/installer.js +42 -20
  4. package/dist/bootstrap/manifest.js +23 -3
  5. package/dist/cli.js +69 -154
  6. package/dist/commands/init.js +279 -114
  7. package/dist/commands/llm.js +177 -0
  8. package/dist/commands/status.js +181 -8
  9. package/dist/commands/task-add.js +89 -162
  10. package/dist/commands/task-done.js +112 -73
  11. package/dist/lib/config.js +77 -113
  12. package/dist/lib/env.js +32 -0
  13. package/dist/lib/git-config.js +21 -0
  14. package/dist/lib/git-host.js +12 -0
  15. package/dist/lib/git-policy.js +67 -0
  16. package/dist/lib/git-remote.js +108 -0
  17. package/dist/lib/git.js +44 -1
  18. package/dist/lib/init-clients.js +55 -0
  19. package/dist/lib/llm-complete.js +18 -19
  20. package/dist/lib/llm-connect.js +150 -0
  21. package/dist/lib/llm-preference.js +102 -0
  22. package/dist/lib/llm-status.js +19 -0
  23. package/dist/lib/paths.js +7 -3
  24. package/dist/lib/plan-llm-openai.js +0 -63
  25. package/dist/lib/pr-create.js +66 -0
  26. package/dist/lib/resolve-task.js +34 -0
  27. package/dist/lib/task-add-llm.js +53 -233
  28. package/dist/lib/task-context.js +68 -0
  29. package/dist/lib/task-done-llm.js +155 -0
  30. package/dist/lib/task-pr-llm.js +50 -0
  31. package/dist/lib/task-push-pr.js +131 -0
  32. package/dist/lib/task-scaffold.js +39 -43
  33. package/dist/lib/task-start.js +100 -0
  34. package/dist/lib/task-summary.js +12 -239
  35. package/dist/lib/task.js +30 -24
  36. package/dist/types/config.js +0 -16
  37. package/package.json +14 -14
  38. package/templates/.cursor/rules/02-git-safety.mdc +12 -0
  39. package/templates/AGENTS.md +27 -87
  40. package/templates/clients/claude/CLAUDE.md +28 -0
  41. package/templates/clients/cursor/.cursor/rules/01-task-source-of-truth.mdc +12 -0
  42. package/templates/clients/cursor/.cursor/rules/02-git-safety.mdc +11 -0
  43. package/templates/clients/cursor/.cursor/rules/03-docs-on-done.mdc +13 -0
  44. package/templates/core/AGENTS.md +44 -0
  45. package/templates/core/docs/logs/README.md +7 -0
  46. package/templates/core/docs/project/03-architecture.md +15 -0
  47. package/templates/core/docs/project/05-current-state.md +15 -0
  48. package/templates/core/docs/project/06-decisions.md +9 -0
  49. package/templates/core/docs/tasks/TASK-000-template.md +37 -0
  50. package/templates/skills-shared/implement-task/SKILL.md +22 -0
  51. package/templates/skills-shared/plan-task/SKILL.md +19 -0
  52. package/dist/agent/loader.js +0 -71
  53. package/dist/agent/prompt.js +0 -66
  54. package/dist/commands/agent-list.js +0 -32
  55. package/dist/commands/agent-prompt.js +0 -59
  56. package/dist/commands/agent-show.js +0 -26
  57. package/dist/commands/done.js +0 -160
  58. package/dist/commands/github-init.js +0 -554
  59. package/dist/commands/github-status.js +0 -164
  60. package/dist/commands/next.js +0 -283
  61. package/dist/commands/notion-init.js +0 -764
  62. package/dist/commands/notion-sync.js +0 -405
  63. package/dist/commands/notion-test.js +0 -595
  64. package/dist/commands/plan.js +0 -146
  65. package/dist/commands/rollback.js +0 -126
  66. package/dist/commands/start.js +0 -129
  67. package/dist/commands/task-check.js +0 -155
  68. package/dist/commands/task-generate.js +0 -206
  69. package/dist/commands/task-pull.js +0 -277
  70. package/dist/commands/task-rollback.js +0 -174
  71. package/dist/commands/task-start.js +0 -102
  72. package/dist/commands/task-status.js +0 -122
  73. package/dist/lib/brief.js +0 -356
  74. package/dist/lib/cursor-task-prompt.js +0 -60
  75. package/dist/lib/done-follow-up.js +0 -308
  76. package/dist/lib/github-cli.js +0 -247
  77. package/dist/lib/guide-state.js +0 -69
  78. package/dist/lib/mvp-artifacts.js +0 -204
  79. package/dist/lib/mvp-constants.js +0 -15
  80. package/dist/lib/mvp-diff-summary.js +0 -43
  81. package/dist/lib/mvp-done-summary.js +0 -335
  82. package/dist/lib/notion-client.js +0 -459
  83. package/dist/lib/notion-discovery.js +0 -671
  84. package/dist/lib/notion-env.js +0 -140
  85. package/dist/lib/notion-mappers.js +0 -148
  86. package/dist/lib/notion-schema.js +0 -272
  87. package/dist/lib/notion-sync.js +0 -337
  88. package/dist/lib/notion-target.js +0 -247
  89. package/dist/lib/package-json.js +0 -133
  90. package/dist/lib/plan-apply-planner.js +0 -131
  91. package/dist/lib/plan-codex-pkce.js +0 -234
  92. package/dist/lib/plan-llm-models.js +0 -237
  93. package/dist/lib/plan-llm-normalize.js +0 -146
  94. package/dist/lib/plan-llm-session.js +0 -682
  95. package/dist/lib/plan-post-apply-setup.js +0 -116
  96. package/dist/lib/project-docs.js +0 -95
  97. package/dist/lib/prompt-builder.js +0 -135
  98. package/dist/lib/resolve-command-task.js +0 -40
  99. package/dist/lib/task-add-build-prompt.js +0 -39
  100. package/dist/lib/task-add-cursor.js +0 -81
  101. package/dist/lib/task-generator.js +0 -183
  102. package/dist/lib/task-merge.js +0 -220
  103. package/dist/lib/task-prompt.js +0 -23
  104. package/dist/lib/task-pull.js +0 -354
  105. package/dist/lib/task-status-brief.js +0 -132
  106. package/dist/lib/workflow-guide.js +0 -485
  107. package/dist/status/collector.js +0 -103
  108. package/dist/status/format.js +0 -181
  109. package/dist/types/brief.js +0 -126
  110. package/templates/.cursor/rules/00-project-governance.mdc +0 -28
  111. package/templates/.cursor/rules/01-agent-orchestration.mdc +0 -48
  112. package/templates/.cursor/rules/02-task-workflow.mdc +0 -38
  113. package/templates/.cursor/rules/03-git-safety.mdc +0 -30
  114. package/templates/.cursor/rules/04-docs-update.mdc +0 -22
  115. package/templates/.vibeops/agents/architect.md +0 -47
  116. package/templates/.vibeops/agents/builder.md +0 -38
  117. package/templates/.vibeops/agents/docs.md +0 -54
  118. package/templates/.vibeops/agents/orchestrator.md +0 -40
  119. package/templates/.vibeops/agents/planner.md +0 -60
  120. package/templates/.vibeops/agents/recovery.md +0 -49
  121. package/templates/.vibeops/agents/reviewer.md +0 -47
  122. package/templates/.vibeops/agents/tester.md +0 -43
  123. package/templates/.vibeops/prompts/create-plan.md +0 -33
  124. package/templates/.vibeops/prompts/generate-tasks.md +0 -41
  125. package/templates/.vibeops/prompts/implement-task.md +0 -39
  126. package/templates/.vibeops/prompts/review-task.md +0 -34
  127. package/templates/.vibeops/prompts/rollback.md +0 -32
  128. package/templates/.vibeops/prompts/start-project.md +0 -39
  129. package/templates/.vibeops/workflows/notion-sync.md +0 -53
  130. package/templates/.vibeops/workflows/project-start.md +0 -73
  131. package/templates/.vibeops/workflows/rollback.md +0 -45
  132. package/templates/.vibeops/workflows/task-lifecycle.md +0 -71
  133. package/templates/docs/logs/README.md +0 -38
  134. package/templates/docs/project/00-overview.md +0 -27
  135. package/templates/docs/project/01-requirements.md +0 -30
  136. package/templates/docs/project/02-mvp-scope.md +0 -36
  137. package/templates/docs/project/03-architecture.md +0 -34
  138. package/templates/docs/project/04-tech-stack.md +0 -29
  139. package/templates/docs/project/05-current-state.md +0 -35
  140. package/templates/docs/project/06-decisions.md +0 -20
  141. package/templates/docs/project/07-backlog.md +0 -23
  142. package/templates/docs/project/08-env.md +0 -29
  143. package/templates/docs/project/09-deployment.md +0 -28
  144. package/templates/docs/tasks/TASK-000-template.md +0 -72
@@ -1,133 +0,0 @@
1
- import { join } from "node:path";
2
- import { readTextOrNull, writeText } from "./filesystem.js";
3
- const PACKAGE_JSON_FILE = "package.json";
4
- export async function readPackageJson(cwd) {
5
- const abs = join(cwd, PACKAGE_JSON_FILE);
6
- const raw = await readTextOrNull(abs);
7
- if (raw === null)
8
- return null;
9
- try {
10
- const data = JSON.parse(raw);
11
- return { path: abs, raw, data };
12
- }
13
- catch {
14
- return null;
15
- }
16
- }
17
- /** Read the canonical repository URL string, regardless of shape. */
18
- export function readRepositoryUrl(pkg) {
19
- const r = pkg.repository;
20
- if (typeof r === "string")
21
- return r;
22
- if (r !== null && typeof r === "object" && typeof r.url === "string")
23
- return r.url;
24
- return "";
25
- }
26
- export function readBugsUrl(pkg) {
27
- const b = pkg.bugs;
28
- if (typeof b === "string")
29
- return b;
30
- if (b !== null && typeof b === "object" && typeof b.url === "string")
31
- return b.url;
32
- return "";
33
- }
34
- export function readHomepage(pkg) {
35
- return typeof pkg.homepage === "string" ? pkg.homepage : "";
36
- }
37
- function detectIndent(raw) {
38
- // Look at the first indented line.
39
- for (const line of raw.split(/\r?\n/)) {
40
- const m = /^(\s+)\S/.exec(line);
41
- if (m === null)
42
- continue;
43
- const indent = m[1];
44
- if (indent.startsWith("\t"))
45
- return 0;
46
- if (indent.length > 0)
47
- return indent.length;
48
- }
49
- return 2;
50
- }
51
- function stringifyKeepingTrailingNewline(data, raw, indent) {
52
- const trailing = raw.endsWith("\n") ? "\n" : "";
53
- return `${JSON.stringify(data, null, indent || 2)}${trailing}`;
54
- }
55
- export function buildRepositoryFieldsPatch({ owner, repo, }) {
56
- return {
57
- repositoryUrl: `git+https://github.com/${owner}/${repo}.git`,
58
- homepage: `https://github.com/${owner}/${repo}#readme`,
59
- bugsUrl: `https://github.com/${owner}/${repo}/issues`,
60
- };
61
- }
62
- export async function updatePackageRepositoryFields(input) {
63
- const read = await readPackageJson(input.cwd);
64
- const path = join(input.cwd, PACKAGE_JSON_FILE);
65
- if (read === null) {
66
- return {
67
- ok: false,
68
- reason: "missing",
69
- diffs: [],
70
- written: false,
71
- path,
72
- };
73
- }
74
- const patch = buildRepositoryFieldsPatch(input.patch);
75
- const before = {
76
- repositoryUrl: readRepositoryUrl(read.data),
77
- homepage: readHomepage(read.data),
78
- bugsUrl: readBugsUrl(read.data),
79
- };
80
- const next = { ...read.data };
81
- // repository: always normalize to object shape with type=git.
82
- const prevRepo = read.data.repository;
83
- const repoObject = prevRepo !== null && typeof prevRepo === "object" ? { ...prevRepo } : {};
84
- repoObject.type = repoObject.type ?? "git";
85
- repoObject.url = patch.repositoryUrl;
86
- next.repository = repoObject;
87
- next.homepage = patch.homepage;
88
- const prevBugs = read.data.bugs;
89
- const bugsObject = prevBugs !== null && typeof prevBugs === "object" ? { ...prevBugs } : {};
90
- bugsObject.url = patch.bugsUrl;
91
- next.bugs = bugsObject;
92
- const diffs = [];
93
- if (before.repositoryUrl !== patch.repositoryUrl) {
94
- diffs.push({
95
- field: "repository.url",
96
- before: before.repositoryUrl,
97
- after: patch.repositoryUrl,
98
- });
99
- }
100
- if (before.homepage !== patch.homepage) {
101
- diffs.push({
102
- field: "homepage",
103
- before: before.homepage,
104
- after: patch.homepage,
105
- });
106
- }
107
- if (before.bugsUrl !== patch.bugsUrl) {
108
- diffs.push({
109
- field: "bugs.url",
110
- before: before.bugsUrl,
111
- after: patch.bugsUrl,
112
- });
113
- }
114
- if (input.dryRun === true || diffs.length === 0) {
115
- return {
116
- ok: true,
117
- diffs,
118
- next,
119
- written: false,
120
- path,
121
- };
122
- }
123
- const indent = detectIndent(read.raw);
124
- const text = stringifyKeepingTrailingNewline(next, read.raw, indent);
125
- await writeText(path, text);
126
- return {
127
- ok: true,
128
- diffs,
129
- next,
130
- written: true,
131
- path,
132
- };
133
- }
@@ -1,131 +0,0 @@
1
- import { join } from "node:path";
2
- import { codexOAuthTextCompletion } from "./plan-codex-responses.js";
3
- import { cursorAgentPrint, extractAgentAssistantText } from "./plan-llm-cursor-agent.js";
4
- import { openAiChatCompletionText } from "./plan-llm-openai.js";
5
- import { writeText } from "./filesystem.js";
6
- import { bold, dim, log } from "./logger.js";
7
- import { isGitRepository, runGit, gitCommit } from "./git.js";
8
- const PLANNER_APPLY_SYSTEM = `You are executing the VibeOps **Planner Agent** step in this run.
9
-
10
- The user message is the full planning prompt (role, ProjectBrief, output format, and file list). Follow it exactly: same doc files, same TASK template sections, same hard rules (docs only under docs/, no application code, no .vibeops edits).
11
-
12
- Reply with normal markdown only. Include the **Plan Summary** bullets first, then one fenced code block per file.
13
-
14
- For every file block:
15
- - Use a fenced block (\`\`\`markdown ... \`\`\` or plain \`\`\` ... \`\`\`).
16
- - The **first line inside the block** must be exactly: \`<!-- file: <repo-relative-path> -->\` with a path under \`docs/project/\` or \`docs/tasks/\` only.
17
- - The remaining lines are the full file body.`;
18
- const ALLOWED_PREFIXES = ["docs/project/", "docs/tasks/"];
19
- export function isSafePlannerRepoRelPath(p) {
20
- const norm = p.replace(/\\/g, "/").trim().replace(/^\/+/, "");
21
- if (norm.includes(".."))
22
- return false;
23
- return ALLOWED_PREFIXES.some((pre) => norm.startsWith(pre));
24
- }
25
- /**
26
- * Extracts `<!-- file: path -->` fenced blocks from planner markdown output.
27
- */
28
- export function parsePlannerFenceFiles(markdown) {
29
- const out = [];
30
- const re = /```(?:[a-zA-Z0-9+-]*)?\s*\n([\s\S]*?)```/g;
31
- let m;
32
- while ((m = re.exec(markdown)) !== null) {
33
- const inner = m[1].replace(/\r\n/g, "\n");
34
- const firstNl = inner.indexOf("\n");
35
- const firstLine = (firstNl >= 0 ? inner.slice(0, firstNl) : inner).trim();
36
- const body = firstNl >= 0 ? inner.slice(firstNl + 1) : "";
37
- const dm = /^\s*<!--\s*file:\s*(.+?)\s*-->\s*$/i.exec(firstLine);
38
- if (!dm)
39
- continue;
40
- const relPath = dm[1].trim().replace(/\\/g, "/");
41
- if (!isSafePlannerRepoRelPath(relPath)) {
42
- log.warn(`Skipping disallowed planner path: ${relPath}`);
43
- continue;
44
- }
45
- out.push({ relativePath: relPath, content: body });
46
- }
47
- return out;
48
- }
49
- function dedupeByPath(files) {
50
- const map = new Map();
51
- for (const f of files) {
52
- if (map.has(f.relativePath)) {
53
- log.warn(`Duplicate planner path in model output (using last): ${f.relativePath}`);
54
- }
55
- map.set(f.relativePath, f);
56
- }
57
- return [...map.values()];
58
- }
59
- export async function runPlannerApplyMarkdown(params) {
60
- const { cwd, planPromptMarkdown, provider, model } = params;
61
- if (provider === "openai") {
62
- const messages = [
63
- { role: "system", content: PLANNER_APPLY_SYSTEM },
64
- { role: "user", content: planPromptMarkdown },
65
- ];
66
- return openAiChatCompletionText({ messages, model });
67
- }
68
- if (provider === "codex-oauth") {
69
- const messages = [
70
- { role: "system", content: PLANNER_APPLY_SYSTEM },
71
- { role: "user", content: planPromptMarkdown },
72
- ];
73
- return codexOAuthTextCompletion(messages, model !== undefined ? { model } : undefined);
74
- }
75
- const prompt = `${PLANNER_APPLY_SYSTEM}\n\n--- Planning prompt ---\n${planPromptMarkdown}`;
76
- const raw = await cursorAgentPrint({ cwd, prompt, model });
77
- return extractAgentAssistantText(raw);
78
- }
79
- export async function writePlannerFiles(cwd, files) {
80
- const written = [];
81
- for (const f of files) {
82
- const abs = join(cwd, f.relativePath);
83
- await writeText(abs, f.content);
84
- written.push(f.relativePath);
85
- }
86
- return written;
87
- }
88
- export async function commitPlannerGit(cwd, repoRelativePaths, message) {
89
- if (!(await isGitRepository(cwd))) {
90
- log.warn("Not a Git repository — skipping commit.");
91
- return;
92
- }
93
- if (repoRelativePaths.length === 0)
94
- return;
95
- for (const p of repoRelativePaths) {
96
- await runGit(cwd, ["add", "--", p]);
97
- }
98
- await gitCommit(cwd, message);
99
- }
100
- export async function pushPlannerGit(cwd) {
101
- if (!(await isGitRepository(cwd))) {
102
- log.warn("Not a Git repository — skipping push.");
103
- return;
104
- }
105
- await runGit(cwd, ["push"]);
106
- }
107
- export async function applyPlannerMarkdownFlow(params) {
108
- log.blank();
109
- log.step(bold("Planner apply — calling LLM with the planning prompt"));
110
- log.info(dim(` provider: ${params.provider}${params.model ? ` model: ${params.model}` : ""}`));
111
- const rawMarkdown = await runPlannerApplyMarkdown(params);
112
- const files = dedupeByPath(parsePlannerFenceFiles(rawMarkdown));
113
- if (files.length === 0) {
114
- throw new Error("Planner apply: model output contained no usable <!-- file: ... --> fenced blocks under docs/project/ or docs/tasks/.");
115
- }
116
- if (params.dryRun) {
117
- log.blank();
118
- log.info(bold("[dry-run] Would write:"));
119
- for (const f of files) {
120
- log.info(` · ${f.relativePath} (${f.content.length} chars)`);
121
- }
122
- return { rawMarkdown, files, writtenRelativePaths: [] };
123
- }
124
- const writtenRelativePaths = await writePlannerFiles(params.cwd, files);
125
- log.blank();
126
- log.ok(`Wrote ${writtenRelativePaths.length} file(s) under docs/`);
127
- for (const p of writtenRelativePaths) {
128
- log.info(dim(` · ${p}`));
129
- }
130
- return { rawMarkdown, files, writtenRelativePaths };
131
- }
@@ -1,234 +0,0 @@
1
- /**
2
- * Browser PKCE OAuth for OpenAI Codex (ChatGPT) — same parameters as community
3
- * clients (authorize URL, localhost redirect, S256 challenge, token exchange).
4
- *
5
- * @see https://docs.rs/codex-oauth/latest/codex_oauth/
6
- */
7
- import { spawn } from "node:child_process";
8
- import { createHash, randomBytes } from "node:crypto";
9
- import { createServer } from "node:http";
10
- import { askInput } from "./inquirer-helpers.js";
11
- import { bold, cyan, dim, log, yellow } from "./logger.js";
12
- import { CODEX_OAUTH_CLIENT_ID, CODEX_OAUTH_TOKEN_URL, saveCodexAuthTokens, } from "./plan-codex-auth.js";
13
- export const CODEX_OAUTH_AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
14
- /** Must match OpenAI's registered redirect for this public Codex client. */
15
- export const CODEX_OAUTH_REDIRECT_URI = "http://localhost:1455/auth/callback";
16
- const CODEX_SCOPES = "openid profile email offline_access";
17
- const CALLBACK_WAIT_MS = 600_000;
18
- function toBase64Url(buf) {
19
- return buf
20
- .toString("base64")
21
- .replace(/\+/g, "-")
22
- .replace(/\//g, "_")
23
- .replace(/=+$/g, "");
24
- }
25
- export function createPkceVerifier() {
26
- return toBase64Url(randomBytes(32));
27
- }
28
- export function createPkceChallenge(verifier) {
29
- return toBase64Url(createHash("sha256").update(verifier).digest());
30
- }
31
- export function createOAuthState() {
32
- return randomBytes(16).toString("hex");
33
- }
34
- export function buildCodexAuthorizeUrl(params) {
35
- const challenge = createPkceChallenge(params.verifier);
36
- const url = new URL(CODEX_OAUTH_AUTHORIZE_URL);
37
- url.searchParams.set("response_type", "code");
38
- url.searchParams.set("client_id", CODEX_OAUTH_CLIENT_ID);
39
- url.searchParams.set("redirect_uri", CODEX_OAUTH_REDIRECT_URI);
40
- url.searchParams.set("scope", CODEX_SCOPES);
41
- url.searchParams.set("code_challenge", challenge);
42
- url.searchParams.set("code_challenge_method", "S256");
43
- url.searchParams.set("state", params.state);
44
- url.searchParams.set("id_token_add_organizations", "true");
45
- url.searchParams.set("codex_cli_simplified_flow", "true");
46
- url.searchParams.set("originator", "pi");
47
- return url.toString();
48
- }
49
- function openSystemBrowser(url) {
50
- const platform = process.platform;
51
- if (platform === "darwin") {
52
- spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
53
- }
54
- else if (platform === "win32") {
55
- spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
56
- }
57
- else {
58
- spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
59
- }
60
- }
61
- function parseCallbackInput(input, expectedState) {
62
- const value = input.trim();
63
- if (value.length === 0)
64
- return null;
65
- try {
66
- const u = new URL(value);
67
- if (u.searchParams.get("state") !== expectedState)
68
- return null;
69
- const code = u.searchParams.get("code");
70
- return code && code.length > 0 ? code : null;
71
- }
72
- catch {
73
- if (value.includes("code=")) {
74
- try {
75
- const params = value.startsWith("http") ? new URL(value).searchParams : new URLSearchParams(value);
76
- if (params.get("state") !== expectedState)
77
- return null;
78
- const c = params.get("code");
79
- return c && c.length > 0 ? c : null;
80
- }
81
- catch {
82
- return null;
83
- }
84
- }
85
- }
86
- return null;
87
- }
88
- export async function exchangeCodexAuthorizationCode(code, verifier) {
89
- const body = new URLSearchParams({
90
- grant_type: "authorization_code",
91
- client_id: CODEX_OAUTH_CLIENT_ID,
92
- code,
93
- code_verifier: verifier,
94
- redirect_uri: CODEX_OAUTH_REDIRECT_URI,
95
- });
96
- const res = await fetch(CODEX_OAUTH_TOKEN_URL, {
97
- method: "POST",
98
- headers: {
99
- "Content-Type": "application/x-www-form-urlencoded",
100
- Accept: "application/json",
101
- },
102
- body,
103
- });
104
- const text = await res.text();
105
- if (!res.ok) {
106
- throw new Error(`Codex token exchange failed (${res.status}): ${text.slice(0, 600)}`);
107
- }
108
- const json = JSON.parse(text);
109
- const access = typeof json.access_token === "string" ? json.access_token.trim() : "";
110
- const refresh = typeof json.refresh_token === "string" ? json.refresh_token.trim() : "";
111
- if (!access || !refresh) {
112
- throw new Error("Codex token exchange returned no access_token/refresh_token.");
113
- }
114
- const id = typeof json.id_token === "string" && json.id_token.trim().length > 0
115
- ? json.id_token.trim()
116
- : undefined;
117
- return { access_token: access, refresh_token: refresh, id_token: id };
118
- }
119
- export function startCodexCallbackServer(expectedState) {
120
- return new Promise((outerResolve) => {
121
- let outerDone = false;
122
- let deliverCode = null;
123
- const codePromise = new Promise((r) => {
124
- deliverCode = r;
125
- });
126
- const server = createServer((req, res) => {
127
- try {
128
- const addr = server.address();
129
- const port = addr?.port ?? 1455;
130
- const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
131
- if (url.pathname !== "/auth/callback") {
132
- res.writeHead(404, { "Content-Type": "text/plain" });
133
- res.end("Not found");
134
- return;
135
- }
136
- if (url.searchParams.get("state") !== expectedState) {
137
- res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
138
- res.end("<p>Invalid state. Close this tab and try again from the terminal.</p>");
139
- return;
140
- }
141
- const code = url.searchParams.get("code");
142
- if (!code) {
143
- res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
144
- res.end("<p>Missing code. Close this tab and try again.</p>");
145
- return;
146
- }
147
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
148
- res.end("<p>ChatGPT sign-in complete. You can close this tab and return to the terminal.</p>");
149
- deliverCode?.(code);
150
- deliverCode = null;
151
- server.close();
152
- }
153
- catch {
154
- res.writeHead(500);
155
- res.end();
156
- }
157
- });
158
- const finishOuter = (handle) => {
159
- if (outerDone)
160
- return;
161
- outerDone = true;
162
- outerResolve(handle);
163
- };
164
- server.once("error", () => {
165
- finishOuter(null);
166
- });
167
- server.listen(1455, "127.0.0.1", () => {
168
- finishOuter({
169
- wait: () => Promise.race([
170
- codePromise,
171
- new Promise((r) => setTimeout(() => r(null), CALLBACK_WAIT_MS)),
172
- ]),
173
- close: () => {
174
- deliverCode?.(null);
175
- deliverCode = null;
176
- server.close();
177
- },
178
- });
179
- });
180
- });
181
- }
182
- /**
183
- * Opens the system browser, completes PKCE OAuth, and writes `~/.codex/auth.json`.
184
- */
185
- export async function runCodexPkceOAuthLogin() {
186
- const verifier = createPkceVerifier();
187
- const state = createOAuthState();
188
- const authUrl = buildCodexAuthorizeUrl({ state, verifier });
189
- log.blank();
190
- log.info(bold("ChatGPT (Codex) OAuth"));
191
- log.info("A browser window will open. Sign in with your ChatGPT account that has Codex access.");
192
- log.info(`If it does not open, visit:`);
193
- log.info(` ${cyan(authUrl)}`);
194
- log.blank();
195
- const handle = await startCodexCallbackServer(state);
196
- if (handle === null) {
197
- log.warn(`${yellow("!")} Could not listen on ${cyan("localhost:1455")} (port may be in use). Complete sign-in in the browser, then paste the redirect URL below.`);
198
- openSystemBrowser(authUrl);
199
- const pasted = await askInput({
200
- message: "Paste the full redirect URL (http://localhost:1455/auth/callback?...)",
201
- nonInteractive: false,
202
- });
203
- const code = parseCallbackInput(pasted, state);
204
- if (!code) {
205
- throw new Error("Could not read authorization code from pasted URL.");
206
- }
207
- const tokens = await exchangeCodexAuthorizationCode(code, verifier);
208
- await saveCodexAuthTokens(tokens);
209
- log.ok(`Saved Codex tokens to ${dim("~/.codex/auth.json")} (or $CODEX_HOME).`);
210
- return;
211
- }
212
- try {
213
- openSystemBrowser(authUrl);
214
- log.info(dim("Waiting for redirect (up to 10 minutes). Press Ctrl+C to cancel."));
215
- let code = await handle.wait();
216
- if (code === null || code.length === 0) {
217
- log.warn("No redirect received in time — paste the callback URL from the browser.");
218
- const pasted = await askInput({
219
- message: "Paste the full redirect URL",
220
- nonInteractive: false,
221
- });
222
- code = parseCallbackInput(pasted, state);
223
- }
224
- if (!code) {
225
- throw new Error("No authorization code received.");
226
- }
227
- const tokens = await exchangeCodexAuthorizationCode(code, verifier);
228
- await saveCodexAuthTokens(tokens);
229
- log.ok(`Saved Codex tokens to ${dim("~/.codex/auth.json")} (or $CODEX_HOME).`);
230
- }
231
- finally {
232
- handle.close();
233
- }
234
- }