@@ -16,6 +16,36 @@ import {
1616 type Credentials ,
1717 type Effort ,
1818} from '@deepcode/core' ;
19+ import { execFile } from 'node:child_process' ;
20+ import { promisify } from 'node:util' ;
21+
22+ const execFileAsync = promisify ( execFile ) ;
23+
24+ /** Environment for spawning git: strips inherited GIT_* (e.g. a GIT_DIR leaked
25+ * from a parent git hook) so the call targets `cwd`, not the hook's repo. */
26+ function gitEnv ( ) : NodeJS . ProcessEnv {
27+ const env : NodeJS . ProcessEnv = { ...process . env } ;
28+ for ( const k of Object . keys ( env ) ) if ( k . startsWith ( 'GIT_' ) ) delete env [ k ] ;
29+ return env ;
30+ }
31+
32+ /** Run a git subcommand in `cwd`, never throwing. */
33+ async function runGit (
34+ cwd : string ,
35+ args : string [ ] ,
36+ ) : Promise < { ok : boolean ; stdout : string ; stderr : string } > {
37+ try {
38+ const { stdout, stderr } = await execFileAsync ( 'git' , args , {
39+ cwd,
40+ env : gitEnv ( ) ,
41+ maxBuffer : 8 * 1024 * 1024 ,
42+ } ) ;
43+ return { ok : true , stdout, stderr } ;
44+ } catch ( err ) {
45+ const e = err as { stdout ?: string ; stderr ?: string ; message ?: string } ;
46+ return { ok : false , stdout : e . stdout ?? '' , stderr : e . stderr ?? e . message ?? 'git failed' } ;
47+ }
48+ }
1949
2050export interface SessionContext {
2151 cwd : string ;
@@ -749,6 +779,97 @@ function historyToMarkdown(history: StoredMessage[]): string {
749779 return out . join ( '\n' ) ;
750780}
751781
782+ export const DiffCommand : SlashCommand = {
783+ name : '/diff' ,
784+ description : 'Show uncommitted changes in the working tree (git diff + untracked files).' ,
785+ async run ( _args , ctx ) {
786+ const inside = await runGit ( ctx . cwd , [ 'rev-parse' , '--is-inside-work-tree' ] ) ;
787+ if ( ! inside . ok || inside . stdout . trim ( ) !== 'true' ) {
788+ return [ 'Not a git repository (or git is unavailable) — nothing to diff.' ] ;
789+ }
790+ const status = await runGit ( ctx . cwd , [ 'status' , '--short' ] ) ;
791+ if ( status . ok && status . stdout . trim ( ) === '' ) {
792+ return [ 'Working tree clean — no uncommitted changes.' ] ;
793+ }
794+ const lines : string [ ] = [ 'Uncommitted changes:' , '' ] ;
795+ if ( status . stdout . trim ( ) ) {
796+ lines . push ( ...status . stdout . trimEnd ( ) . split ( '\n' ) , '' ) ;
797+ }
798+ // `diff HEAD` covers staged + unstaged edits to tracked files. It fails in a
799+ // repo with no commits yet (no HEAD) — that's fine, status/untracked still show.
800+ const diff = await runGit ( ctx . cwd , [ '--no-pager' , 'diff' , 'HEAD' ] ) ;
801+ const MAX = 300 ;
802+ if ( diff . ok && diff . stdout . trim ( ) ) {
803+ const diffLines = diff . stdout . split ( '\n' ) ;
804+ lines . push ( ...diffLines . slice ( 0 , MAX ) ) ;
805+ if ( diffLines . length > MAX ) {
806+ lines . push ( `… (${ diffLines . length - MAX } more lines — run \`git diff\` for the full diff)` ) ;
807+ }
808+ }
809+ const untracked = await runGit ( ctx . cwd , [ 'ls-files' , '--others' , '--exclude-standard' ] ) ;
810+ if ( untracked . ok && untracked . stdout . trim ( ) ) {
811+ lines . push ( '' , 'Untracked files:' ) ;
812+ for ( const f of untracked . stdout . trim ( ) . split ( '\n' ) ) lines . push ( ` ? ${ f } ` ) ;
813+ }
814+ return lines ;
815+ } ,
816+ } ;
817+
818+ export const ReleaseNotesCommand : SlashCommand = {
819+ name : '/release-notes' ,
820+ description : 'Show the latest CHANGELOG entry.' ,
821+ async run ( _args , ctx ) {
822+ const fs = await import ( 'node:fs/promises' ) ;
823+ const path = await import ( 'node:path' ) ;
824+ // Walk up from cwd looking for CHANGELOG.md (repo root may be above cwd).
825+ let dir = ctx . cwd ;
826+ let changelog : string | null = null ;
827+ for ( let i = 0 ; i < 8 ; i ++ ) {
828+ try {
829+ changelog = await fs . readFile ( path . join ( dir , 'CHANGELOG.md' ) , 'utf8' ) ;
830+ break ;
831+ } catch {
832+ const parent = path . dirname ( dir ) ;
833+ if ( parent === dir ) break ;
834+ dir = parent ;
835+ }
836+ }
837+ if ( ! changelog ) {
838+ return [ 'No CHANGELOG.md found (searched from cwd up to the filesystem root).' ] ;
839+ }
840+ const all = changelog . split ( '\n' ) ;
841+ const firstH2 = all . findIndex ( ( l ) => l . startsWith ( '## ' ) ) ;
842+ if ( firstH2 === - 1 ) return all . slice ( 0 , 40 ) ;
843+ const afterFirst = all . slice ( firstH2 + 1 ) ;
844+ const nextRel = afterFirst . findIndex ( ( l ) => l . startsWith ( '## ' ) ) ;
845+ const end = nextRel === - 1 ? all . length : firstH2 + 1 + nextRel ;
846+ const section = all . slice ( firstH2 , end ) ;
847+ while ( section . length && section [ section . length - 1 ] ! . trim ( ) === '' ) section . pop ( ) ;
848+ return section ;
849+ } ,
850+ } ;
851+
852+ export const BugCommand : SlashCommand = {
853+ name : '/bug' ,
854+ aliases : [ '/feedback' ] ,
855+ description : 'Report a bug or give feedback (prints a prefilled GitHub issue link).' ,
856+ run ( args , ctx ) {
857+ const title = args . join ( ' ' ) . trim ( ) ;
858+ const params = new URLSearchParams ( ) ;
859+ if ( title ) params . set ( 'title' , title ) ;
860+ params . set (
861+ 'body' ,
862+ `<!-- Describe the issue above. -->\n\n---\nModel: ${ ctx . model } · Mode: ${ ctx . mode } · Effort: ${ ctx . effort } ` ,
863+ ) ;
864+ return [
865+ 'Report a bug or request a feature:' ,
866+ ` https://github.com/oratis/deepcode/issues/new?${ params . toString ( ) } ` ,
867+ '' ,
868+ 'Or browse existing issues: https://github.com/oratis/deepcode/issues' ,
869+ ] ;
870+ } ,
871+ } ;
872+
752873export const BUILTIN_COMMANDS : SlashCommand [ ] = [
753874 HelpCommand ,
754875 ClearCommand ,
@@ -775,6 +896,9 @@ export const BUILTIN_COMMANDS: SlashCommand[] = [
775896 SkillsCommand ,
776897 ExportCommand ,
777898 CompactCommand ,
899+ DiffCommand ,
900+ ReleaseNotesCommand ,
901+ BugCommand ,
778902] ;
779903
780904// ──────────────────────────────────────────────────────────────────────────
0 commit comments