@@ -10,7 +10,7 @@ import * as os from "os";
1010import * as crypto from "node:crypto" ;
1111import { Client } from "pg" ;
1212import { startMcpServer } from "../lib/mcp-server" ;
13- import { fetchIssues , fetchIssueComments , createIssueComment , fetchIssue } from "../lib/issues" ;
13+ import { fetchIssues , fetchIssueComments , createIssueComment , fetchIssue , createIssue , updateIssue , updateIssueComment } from "../lib/issues" ;
1414import { resolveBaseUrls } from "../lib/util" ;
1515import { applyInitPlan , buildInitPlan , connectWithSslFallback , DEFAULT_MONITORING_USER , redactPasswordsInSql , resolveAdminConnection , resolveMonitoringPassword , verifyInitSetup } from "../lib/init" ;
1616import * as pkce from "../lib/pkce" ;
@@ -42,7 +42,7 @@ async function execPromise(command: string): Promise<{ stdout: string; stderr: s
4242 childProcess . exec ( command , ( error , stdout , stderr ) => {
4343 if ( error ) {
4444 const err = error as Error & { code : number } ;
45- err . code = error . code ?? 1 ;
45+ err . code = typeof error . code === "number" ? error . code : 1 ;
4646 reject ( err ) ;
4747 } else {
4848 resolve ( { stdout, stderr } ) ;
@@ -56,7 +56,7 @@ async function execFilePromise(file: string, args: string[]): Promise<{ stdout:
5656 childProcess . execFile ( file , args , ( error , stdout , stderr ) => {
5757 if ( error ) {
5858 const err = error as Error & { code : number } ;
59- err . code = error . code ?? 1 ;
59+ err . code = typeof error . code === "number" ? error . code : 1 ;
6060 reject ( err ) ;
6161 } else {
6262 resolve ( { stdout, stderr } ) ;
@@ -2508,7 +2508,7 @@ issues
25082508 } ) ;
25092509
25102510issues
2511- . command ( "post_comment <issueId> <content>" )
2511+ . command ( "post-comment <issueId> <content>" )
25122512 . description ( "post a new comment to an issue" )
25132513 . option ( "--parent <uuid>" , "parent comment id" )
25142514 . option ( "--debug" , "enable debug output" )
@@ -2553,6 +2553,194 @@ issues
25532553 }
25542554 } ) ;
25552555
2556+ issues
2557+ . command ( "create <title>" )
2558+ . description ( "create a new issue" )
2559+ . option ( "--org-id <id>" , "organization id (defaults to config orgId)" , ( v ) => parseInt ( v , 10 ) )
2560+ . option ( "--project-id <id>" , "project id" , ( v ) => parseInt ( v , 10 ) )
2561+ . option ( "--description <text>" , "issue description (supports \\\\n)" )
2562+ . option (
2563+ "--label <label>" ,
2564+ "issue label (repeatable)" ,
2565+ ( value : string , previous : string [ ] ) => {
2566+ previous . push ( value ) ;
2567+ return previous ;
2568+ } ,
2569+ [ ] as string [ ]
2570+ )
2571+ . option ( "--debug" , "enable debug output" )
2572+ . option ( "--json" , "output raw JSON" )
2573+ . action ( async ( rawTitle : string , opts : { orgId ?: number ; projectId ?: number ; description ?: string ; label ?: string [ ] ; debug ?: boolean ; json ?: boolean } ) => {
2574+ try {
2575+ const rootOpts = program . opts < CliOptions > ( ) ;
2576+ const cfg = config . readConfig ( ) ;
2577+ const { apiKey } = getConfig ( rootOpts ) ;
2578+ if ( ! apiKey ) {
2579+ console . error ( "API key is required. Run 'pgai auth' first or set --api-key." ) ;
2580+ process . exitCode = 1 ;
2581+ return ;
2582+ }
2583+
2584+ const title = interpretEscapes ( String ( rawTitle || "" ) . trim ( ) ) ;
2585+ if ( ! title ) {
2586+ console . error ( "title is required" ) ;
2587+ process . exitCode = 1 ;
2588+ return ;
2589+ }
2590+
2591+ const orgId = typeof opts . orgId === "number" && ! Number . isNaN ( opts . orgId ) ? opts . orgId : cfg . orgId ;
2592+ if ( typeof orgId !== "number" ) {
2593+ console . error ( "org_id is required. Either pass --org-id or run 'pgai auth' to store it in config." ) ;
2594+ process . exitCode = 1 ;
2595+ return ;
2596+ }
2597+
2598+ const description = opts . description !== undefined ? interpretEscapes ( String ( opts . description ) ) : undefined ;
2599+ const labels = Array . isArray ( opts . label ) && opts . label . length > 0 ? opts . label . map ( String ) : undefined ;
2600+ const projectId = typeof opts . projectId === "number" && ! Number . isNaN ( opts . projectId ) ? opts . projectId : undefined ;
2601+
2602+ const { apiBaseUrl } = resolveBaseUrls ( rootOpts , cfg ) ;
2603+ const result = await createIssue ( {
2604+ apiKey,
2605+ apiBaseUrl,
2606+ title,
2607+ orgId,
2608+ description,
2609+ projectId,
2610+ labels,
2611+ debug : ! ! opts . debug ,
2612+ } ) ;
2613+ printResult ( result , opts . json ) ;
2614+ } catch ( err ) {
2615+ const message = err instanceof Error ? err . message : String ( err ) ;
2616+ console . error ( message ) ;
2617+ process . exitCode = 1 ;
2618+ }
2619+ } ) ;
2620+
2621+ issues
2622+ . command ( "update <issueId>" )
2623+ . description ( "update an existing issue (title/description/status/labels)" )
2624+ . option ( "--title <text>" , "new title (supports \\\\n)" )
2625+ . option ( "--description <text>" , "new description (supports \\\\n)" )
2626+ . option ( "--status <value>" , "status: open|closed|0|1" )
2627+ . option (
2628+ "--label <label>" ,
2629+ "set labels (repeatable). If provided, replaces existing labels." ,
2630+ ( value : string , previous : string [ ] ) => {
2631+ previous . push ( value ) ;
2632+ return previous ;
2633+ } ,
2634+ [ ] as string [ ]
2635+ )
2636+ . option ( "--clear-labels" , "set labels to an empty list" )
2637+ . option ( "--debug" , "enable debug output" )
2638+ . option ( "--json" , "output raw JSON" )
2639+ . action ( async ( issueId : string , opts : { title ?: string ; description ?: string ; status ?: string ; label ?: string [ ] ; clearLabels ?: boolean ; debug ?: boolean ; json ?: boolean } ) => {
2640+ try {
2641+ const rootOpts = program . opts < CliOptions > ( ) ;
2642+ const cfg = config . readConfig ( ) ;
2643+ const { apiKey } = getConfig ( rootOpts ) ;
2644+ if ( ! apiKey ) {
2645+ console . error ( "API key is required. Run 'pgai auth' first or set --api-key." ) ;
2646+ process . exitCode = 1 ;
2647+ return ;
2648+ }
2649+
2650+ const { apiBaseUrl } = resolveBaseUrls ( rootOpts , cfg ) ;
2651+
2652+ const title = opts . title !== undefined ? interpretEscapes ( String ( opts . title ) ) : undefined ;
2653+ const description = opts . description !== undefined ? interpretEscapes ( String ( opts . description ) ) : undefined ;
2654+
2655+ let status : number | undefined = undefined ;
2656+ if ( opts . status !== undefined ) {
2657+ const raw = String ( opts . status ) . trim ( ) . toLowerCase ( ) ;
2658+ if ( raw === "open" ) status = 0 ;
2659+ else if ( raw === "closed" ) status = 1 ;
2660+ else {
2661+ const n = Number ( raw ) ;
2662+ if ( ! Number . isFinite ( n ) ) {
2663+ console . error ( "status must be open|closed|0|1" ) ;
2664+ process . exitCode = 1 ;
2665+ return ;
2666+ }
2667+ status = n ;
2668+ }
2669+ if ( status !== 0 && status !== 1 ) {
2670+ console . error ( "status must be 0 (open) or 1 (closed)" ) ;
2671+ process . exitCode = 1 ;
2672+ return ;
2673+ }
2674+ }
2675+
2676+ let labels : string [ ] | undefined = undefined ;
2677+ if ( opts . clearLabels ) {
2678+ labels = [ ] ;
2679+ } else if ( Array . isArray ( opts . label ) && opts . label . length > 0 ) {
2680+ labels = opts . label . map ( String ) ;
2681+ }
2682+
2683+ const result = await updateIssue ( {
2684+ apiKey,
2685+ apiBaseUrl,
2686+ issueId,
2687+ title,
2688+ description,
2689+ status,
2690+ labels,
2691+ debug : ! ! opts . debug ,
2692+ } ) ;
2693+ printResult ( result , opts . json ) ;
2694+ } catch ( err ) {
2695+ const message = err instanceof Error ? err . message : String ( err ) ;
2696+ console . error ( message ) ;
2697+ process . exitCode = 1 ;
2698+ }
2699+ } ) ;
2700+
2701+ issues
2702+ . command ( "update-comment <commentId> <content>" )
2703+ . description ( "update an existing issue comment" )
2704+ . option ( "--debug" , "enable debug output" )
2705+ . option ( "--json" , "output raw JSON" )
2706+ . action ( async ( commentId : string , content : string , opts : { debug ?: boolean ; json ?: boolean } ) => {
2707+ try {
2708+ if ( opts . debug ) {
2709+ // eslint-disable-next-line no-console
2710+ console . log ( `Debug: Original content: ${ JSON . stringify ( content ) } ` ) ;
2711+ }
2712+ content = interpretEscapes ( content ) ;
2713+ if ( opts . debug ) {
2714+ // eslint-disable-next-line no-console
2715+ console . log ( `Debug: Interpreted content: ${ JSON . stringify ( content ) } ` ) ;
2716+ }
2717+
2718+ const rootOpts = program . opts < CliOptions > ( ) ;
2719+ const cfg = config . readConfig ( ) ;
2720+ const { apiKey } = getConfig ( rootOpts ) ;
2721+ if ( ! apiKey ) {
2722+ console . error ( "API key is required. Run 'pgai auth' first or set --api-key." ) ;
2723+ process . exitCode = 1 ;
2724+ return ;
2725+ }
2726+
2727+ const { apiBaseUrl } = resolveBaseUrls ( rootOpts , cfg ) ;
2728+
2729+ const result = await updateIssueComment ( {
2730+ apiKey,
2731+ apiBaseUrl,
2732+ commentId,
2733+ content,
2734+ debug : ! ! opts . debug ,
2735+ } ) ;
2736+ printResult ( result , opts . json ) ;
2737+ } catch ( err ) {
2738+ const message = err instanceof Error ? err . message : String ( err ) ;
2739+ console . error ( message ) ;
2740+ process . exitCode = 1 ;
2741+ }
2742+ } ) ;
2743+
25562744// MCP server
25572745const mcp = program . command ( "mcp" ) . description ( "MCP server integration" ) ;
25582746
0 commit comments