@goodtek/vibeops 0.4.0 → 1.0.0

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 (106) hide show
  1. package/CHANGELOG.md +198 -1
  2. package/README.md +62 -402
  3. package/dist/bootstrap/installer.js +4 -16
  4. package/dist/cli.js +52 -244
  5. package/dist/commands/init.js +3 -3
  6. package/dist/commands/status.js +145 -8
  7. package/dist/commands/task-add.js +100 -0
  8. package/dist/commands/task-done.js +117 -74
  9. package/dist/lib/config.js +2 -118
  10. package/dist/lib/git.js +190 -8
  11. package/dist/lib/llm-complete.js +62 -0
  12. package/dist/lib/paths.js +7 -3
  13. package/dist/lib/plan-llm-openai.js +0 -63
  14. package/dist/lib/resolve-task.js +34 -0
  15. package/dist/lib/slug.js +11 -0
  16. package/dist/lib/task-add-llm.js +81 -0
  17. package/dist/lib/task-context.js +68 -0
  18. package/dist/lib/task-done-llm.js +148 -0
  19. package/dist/lib/task-merge.js +115 -0
  20. package/dist/lib/task-scaffold.js +70 -92
  21. package/dist/lib/task-start.js +72 -0
  22. package/dist/lib/task-summary.js +12 -239
  23. package/dist/lib/task.js +80 -37
  24. package/dist/types/config.js +0 -16
  25. package/package.json +3 -4
  26. package/templates/.cursor/rules/01-task-source-of-truth.mdc +12 -0
  27. package/templates/.cursor/rules/02-git-safety.mdc +11 -0
  28. package/templates/.cursor/rules/03-docs-on-done.mdc +13 -0
  29. package/templates/.cursor/skills/implement-task/SKILL.md +22 -0
  30. package/templates/.cursor/skills/plan-task/SKILL.md +19 -0
  31. package/templates/AGENTS.md +26 -87
  32. package/templates/docs/logs/README.md +4 -35
  33. package/templates/docs/project/03-architecture.md +10 -29
  34. package/templates/docs/project/05-current-state.md +7 -27
  35. package/templates/docs/project/06-decisions.md +6 -17
  36. package/templates/docs/tasks/TASK-000-template.md +9 -44
  37. package/dist/agent/loader.js +0 -71
  38. package/dist/agent/prompt.js +0 -66
  39. package/dist/commands/agent-list.js +0 -32
  40. package/dist/commands/agent-prompt.js +0 -59
  41. package/dist/commands/agent-show.js +0 -26
  42. package/dist/commands/github-init.js +0 -554
  43. package/dist/commands/github-status.js +0 -164
  44. package/dist/commands/notion-init.js +0 -764
  45. package/dist/commands/notion-sync.js +0 -405
  46. package/dist/commands/notion-test.js +0 -595
  47. package/dist/commands/plan.js +0 -343
  48. package/dist/commands/task-check.js +0 -155
  49. package/dist/commands/task-generate.js +0 -206
  50. package/dist/commands/task-pull.js +0 -277
  51. package/dist/commands/task-rollback.js +0 -174
  52. package/dist/commands/task-start.js +0 -102
  53. package/dist/lib/brief.js +0 -356
  54. package/dist/lib/github-cli.js +0 -247
  55. package/dist/lib/notion-client.js +0 -459
  56. package/dist/lib/notion-discovery.js +0 -671
  57. package/dist/lib/notion-env.js +0 -140
  58. package/dist/lib/notion-mappers.js +0 -148
  59. package/dist/lib/notion-schema.js +0 -272
  60. package/dist/lib/notion-sync.js +0 -337
  61. package/dist/lib/notion-target.js +0 -247
  62. package/dist/lib/package-json.js +0 -133
  63. package/dist/lib/plan-apply-planner.js +0 -131
  64. package/dist/lib/plan-codex-pkce.js +0 -234
  65. package/dist/lib/plan-llm-models.js +0 -237
  66. package/dist/lib/plan-llm-normalize.js +0 -146
  67. package/dist/lib/plan-llm-session.js +0 -682
  68. package/dist/lib/plan-post-apply-setup.js +0 -116
  69. package/dist/lib/project-docs.js +0 -95
  70. package/dist/lib/prompt-builder.js +0 -135
  71. package/dist/lib/task-generator.js +0 -183
  72. package/dist/lib/task-prompt.js +0 -23
  73. package/dist/lib/task-pull.js +0 -354
  74. package/dist/status/collector.js +0 -103
  75. package/dist/status/format.js +0 -177
  76. package/dist/types/brief.js +0 -126
  77. package/templates/.cursor/rules/00-project-governance.mdc +0 -28
  78. package/templates/.cursor/rules/01-agent-orchestration.mdc +0 -48
  79. package/templates/.cursor/rules/02-task-workflow.mdc +0 -38
  80. package/templates/.cursor/rules/03-git-safety.mdc +0 -30
  81. package/templates/.cursor/rules/04-docs-update.mdc +0 -22
  82. package/templates/.vibeops/agents/architect.md +0 -47
  83. package/templates/.vibeops/agents/builder.md +0 -38
  84. package/templates/.vibeops/agents/docs.md +0 -54
  85. package/templates/.vibeops/agents/orchestrator.md +0 -40
  86. package/templates/.vibeops/agents/planner.md +0 -60
  87. package/templates/.vibeops/agents/recovery.md +0 -49
  88. package/templates/.vibeops/agents/reviewer.md +0 -47
  89. package/templates/.vibeops/agents/tester.md +0 -43
  90. package/templates/.vibeops/prompts/create-plan.md +0 -33
  91. package/templates/.vibeops/prompts/generate-tasks.md +0 -41
  92. package/templates/.vibeops/prompts/implement-task.md +0 -39
  93. package/templates/.vibeops/prompts/review-task.md +0 -34
  94. package/templates/.vibeops/prompts/rollback.md +0 -32
  95. package/templates/.vibeops/prompts/start-project.md +0 -39
  96. package/templates/.vibeops/workflows/notion-sync.md +0 -53
  97. package/templates/.vibeops/workflows/project-start.md +0 -73
  98. package/templates/.vibeops/workflows/rollback.md +0 -45
  99. package/templates/.vibeops/workflows/task-lifecycle.md +0 -71
  100. package/templates/docs/project/00-overview.md +0 -27
  101. package/templates/docs/project/01-requirements.md +0 -30
  102. package/templates/docs/project/02-mvp-scope.md +0 -36
  103. package/templates/docs/project/04-tech-stack.md +0 -29
  104. package/templates/docs/project/07-backlog.md +0 -23
  105. package/templates/docs/project/08-env.md +0 -29
  106. package/templates/docs/project/09-deployment.md +0 -28
@@ -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
- }