1010 * npm run generate:vhs # all chapters, 5 concurrent
1111 * npm run generate:vhs -- --chapter 03 # only chapter 03
1212 * npm run generate:vhs -- --chapter 03 --chapter 05
13+ * npm run generate:vhs -- --file path/to/demo.tape # single tape file
1314 * npm run generate:vhs -- --concurrency 3 # limit to 3 at a time
1415 *
1516 * Requirements:
@@ -21,46 +22,97 @@ const { readdirSync, statSync, existsSync, readFileSync, renameSync, writeFileSy
2122const { join, relative, dirname } = require ( 'path' ) ;
2223
2324const rootDir = join ( __dirname , '..' , '..' ) ;
24- const copilotConfigPath = join ( require ( 'os' ) . homedir ( ) , '.copilot' , 'config.json' ) ;
25-
26- // Ensure on-air mode is enabled so recordings don't show model names or quota
27- function enableOnAirMode ( ) {
25+ const homeDir = require ( 'os' ) . homedir ( ) ;
26+ const copilotConfigPath = join ( homeDir , '.copilot' , 'config.json' ) ;
27+ // Personal agents live in both ~/.copilot/agents and ~/.claude/agents
28+ const personalAgentsDirs = [
29+ { dir : join ( homeDir , '.copilot' , 'agents' ) , backup : join ( homeDir , '.copilot' , 'agents.recording-bak' ) } ,
30+ { dir : join ( homeDir , '.claude' , 'agents' ) , backup : join ( homeDir , '.claude' , 'agents.recording-bak' ) }
31+ ] ;
32+
33+ // Ensure streamer mode is enabled so recordings don't show model names or quota
34+ function enableStreamerMode ( ) {
2835 try {
2936 const config = JSON . parse ( readFileSync ( copilotConfigPath , 'utf8' ) ) ;
30- if ( ! config . on_air_mode ) {
31- config . on_air_mode = true ;
32- writeFileSync ( copilotConfigPath , JSON . stringify ( config , null , 2 ) + '\n' ) ;
33- console . log ( '🔴 On-air mode: enabled (was off)' ) ;
34- return false ; // was off, we turned it on
35- }
36- console . log ( '🔴 On-air mode: already enabled' ) ;
37- return true ; // was already on
37+ const wasOn = config . streamer_mode || false ;
38+ config . streamer_mode = true ;
39+ delete config . on_air_mode ;
40+ writeFileSync ( copilotConfigPath , JSON . stringify ( config , null , 2 ) + '\n' ) ;
41+ console . log ( `🔴 Streamer mode: ${ wasOn ? 'already enabled' : 'enabled' } ` ) ;
42+ return { wasOn } ;
3843 } catch ( e ) {
39- console . warn ( '⚠ Could not read copilot config, on-air mode not verified' ) ;
44+ console . warn ( '⚠ Could not read copilot config, streamer mode not verified' ) ;
4045 return null ;
4146 }
4247}
4348
44- // Restore on-air mode to its original state
45- function restoreOnAirMode ( wasAlreadyOn ) {
46- if ( wasAlreadyOn === false ) {
49+ // Restore streamer mode to its original state
50+ function restoreStreamerMode ( state ) {
51+ if ( state && ! state . wasOn ) {
4752 try {
4853 const config = JSON . parse ( readFileSync ( copilotConfigPath , 'utf8' ) ) ;
49- config . on_air_mode = false ;
54+ config . streamer_mode = false ;
5055 writeFileSync ( copilotConfigPath , JSON . stringify ( config , null , 2 ) + '\n' ) ;
51- console . log ( '🔴 On-air mode: restored to off' ) ;
56+ console . log ( '🔴 Streamer mode: restored to off' ) ;
5257 } catch ( e ) { /* ignore */ }
5358 }
5459}
5560
61+ // Hide personal agents so only course agents appear in /agent picker
62+ function hidePersonalAgents ( ) {
63+ const hidden = [ ] ;
64+ for ( const { dir, backup } of personalAgentsDirs ) {
65+ try {
66+ // Restore stale backup from a previous interrupted run
67+ if ( ! existsSync ( dir ) && existsSync ( backup ) ) {
68+ renameSync ( backup , dir ) ;
69+ console . log ( `👤 Restored stale backup: ${ backup } ` ) ;
70+ }
71+ if ( existsSync ( dir ) ) {
72+ renameSync ( dir , backup ) ;
73+ hidden . push ( { dir, backup } ) ;
74+ }
75+ } catch ( e ) {
76+ console . warn ( `⚠ Could not hide ${ dir } :` , e . message ) ;
77+ }
78+ }
79+ if ( hidden . length > 0 ) {
80+ console . log ( `👤 Personal agents: hidden (${ hidden . length } location(s))` ) ;
81+ }
82+ return hidden ;
83+ }
84+
85+ // Restore personal agents to their original locations
86+ function restorePersonalAgents ( hidden ) {
87+ if ( ! hidden || hidden . length === 0 ) return ;
88+ for ( const { dir, backup } of hidden ) {
89+ try {
90+ if ( existsSync ( backup ) ) {
91+ // Copilot may recreate the agents dir during recording - remove it first
92+ if ( existsSync ( dir ) ) {
93+ rmSync ( dir , { recursive : true } ) ;
94+ }
95+ renameSync ( backup , dir ) ;
96+ }
97+ } catch ( e ) {
98+ console . warn ( `⚠ Could not restore ${ dir } :` , e . message ) ;
99+ console . warn ( ` Manual fix: mv "${ backup } " "${ dir } "` ) ;
100+ }
101+ }
102+ console . log ( `👤 Personal agents: restored (${ hidden . length } location(s))` ) ;
103+ }
104+
56105// Parse CLI args
57106const args = process . argv . slice ( 2 ) ;
58107const chapters = [ ] ;
108+ const files = [ ] ;
59109let concurrency = 5 ;
60110
61111for ( let i = 0 ; i < args . length ; i ++ ) {
62112 if ( args [ i ] === '--chapter' && args [ i + 1 ] ) {
63113 chapters . push ( args [ ++ i ] ) ;
114+ } else if ( args [ i ] === '--file' && args [ i + 1 ] ) {
115+ files . push ( args [ ++ i ] ) ;
64116 } else if ( args [ i ] === '--concurrency' && args [ i + 1 ] ) {
65117 concurrency = parseInt ( args [ ++ i ] , 10 ) ;
66118 }
@@ -136,26 +188,28 @@ function runVhs(tapeFile, wrappedPath) {
136188
137189 exec ( `vhs ${ relativePath } ` , {
138190 cwd : rootDir ,
139- timeout : 180000 ,
191+ timeout : 600000 ,
140192 env : { ...process . env , PATH : wrappedPath }
141193 } , ( error ) => {
142194 const elapsed = ( ( Date . now ( ) - startTime ) / 1000 ) . toFixed ( 0 ) ;
143195
144- if ( error ) {
145- console . log ( ` ✗ ${ relativePath } (${ elapsed } s) - ${ error . message } ` ) ;
146- resolve ( { success : false , path : relativePath } ) ;
147- return ;
148- }
149-
150- // Move generated GIF to the images folder if it was created in root
196+ // Always move GIF if it was created, even if VHS exited non-zero
197+ let gifCreated = false ;
151198 if ( outputFilename ) {
152199 const generatedPath = join ( rootDir , outputFilename ) ;
153200 const targetPath = join ( imagesDir , outputFilename ) ;
154201 if ( existsSync ( generatedPath ) && generatedPath !== targetPath ) {
155202 renameSync ( generatedPath , targetPath ) ;
203+ gifCreated = true ;
156204 }
157205 }
158206
207+ if ( error && ! gifCreated ) {
208+ console . log ( ` ✗ ${ relativePath } (${ elapsed } s) - ${ error . message } ` ) ;
209+ resolve ( { success : false , path : relativePath } ) ;
210+ return ;
211+ }
212+
159213 console . log ( ` ✓ ${ relativePath } (${ elapsed } s)` ) ;
160214 resolve ( { success : true , path : relativePath } ) ;
161215 } ) ;
@@ -187,17 +241,33 @@ async function runWithConcurrency(tasks, limit) {
187241async function main ( ) {
188242 console . log ( '🎬 Generating course demos...\n' ) ;
189243
190- if ( chapters . length > 0 ) {
244+ if ( files . length > 0 ) {
245+ console . log ( `Files: ${ files . join ( ', ' ) } ` ) ;
246+ } else if ( chapters . length > 0 ) {
191247 console . log ( `Chapters: ${ chapters . join ( ', ' ) } ` ) ;
192248 }
193249 console . log ( `Concurrency: ${ concurrency } ` ) ;
194250 console . log ( '' ) ;
195251
196- // Enable on-air mode before recording
197- const wasAlreadyOn = enableOnAirMode ( ) ;
252+ // Enable streamer mode and hide personal agents before recording
253+ const streamerState = enableStreamerMode ( ) ;
254+ const agentsWereHidden = hidePersonalAgents ( ) ;
198255 console . log ( '' ) ;
199256
200- const tapeFiles = findTapeFiles ( rootDir , chapters ) ;
257+ // Resolve tape files: explicit --file paths take priority over chapter scan
258+ let tapeFiles ;
259+ if ( files . length > 0 ) {
260+ const { resolve } = require ( 'path' ) ;
261+ tapeFiles = files . map ( f => resolve ( rootDir , f ) ) . filter ( f => {
262+ if ( ! existsSync ( f ) ) {
263+ console . log ( ` ⚠ File not found: ${ f } ` ) ;
264+ return false ;
265+ }
266+ return true ;
267+ } ) ;
268+ } else {
269+ tapeFiles = findTapeFiles ( rootDir , chapters ) ;
270+ }
201271
202272 if ( tapeFiles . length === 0 ) {
203273 console . log ( 'No .tape files found' ) ;
@@ -222,7 +292,8 @@ async function main() {
222292 const results = await runWithConcurrency ( tasks , concurrency ) ;
223293
224294 cleanupCopilotWrapper ( ) ;
225- restoreOnAirMode ( wasAlreadyOn ) ;
295+ restorePersonalAgents ( agentsWereHidden ) ;
296+ restoreStreamerMode ( streamerState ) ;
226297
227298 const succeeded = results . filter ( r => r . success ) . length ;
228299 const failedResults = results . filter ( r => ! r . success ) ;
@@ -241,6 +312,7 @@ async function main() {
241312main ( ) . catch ( e => {
242313 console . error ( e ) ;
243314 cleanupCopilotWrapper ( ) ;
244- restoreOnAirMode ( false ) ; // restore on-air to off on error
315+ restorePersonalAgents ( personalAgentsDirs ) ; // always try to restore on error
316+ restoreStreamerMode ( { wasOn : false } ) ;
245317 process . exit ( 1 ) ;
246318} ) ;
0 commit comments