@@ -21,15 +21,16 @@ usage:
2121 sideshow serve [--port N] [--open] start the surface (API + viewer)
2222 sideshow publish <file|-> [options] publish an HTML post (one html surface)
2323 --title <t> post title
24- --md <file|-> add a markdown surface (prose) — combine with html
25- --mermaid <file|-> add a mermaid surface (diagram source → SVG) — combine with html
26- --diff <file|-> add a diff surface from a unified/git patch (combine with html)
27- --terminal <file|-> add a terminal surface from monospace/ANSI output
28- --json <file|-> add a json surface from a JSON file (collapsible tree)
29- --code <file|-> add a code surface from a file (shiki-highlighted)
24+ --md <file|-> add a markdown surface (prose) — repeatable
25+ --mermaid <file|-> add a mermaid surface (diagram source → SVG) — repeatable
26+ --diff <file|-> add a diff surface from a unified/git patch — repeatable
27+ --terminal <file|-> add a terminal surface from monospace/ANSI output — repeatable
28+ --json <file|-> add a json surface from a JSON file (collapsible tree) — repeatable
29+ --code <file|-> add a code surface from a file (shiki-highlighted) — repeatable
3030 --kit <id> opt the html surface into a kit (repeatable; see "sideshow kits")
31- --image <file> upload an image and append it as an image surface
31+ --image <file> upload an image and append it as an image surface — repeatable
3232 --session <id> target session (default: auto per agent session)
33+ surfaces appear in command-line flag order; repeat a flag to add several of one kind
3334 --session-title <t> name for a newly created session — name the task,
3435 e.g. "Auth refactor" (ignored if the session exists)
3536 --agent <name> agent name for new sessions (default: $SIDESHOW_AGENT or "agent")
@@ -78,16 +79,17 @@ usage:
7879 --surface <N> target surface N (id or 0-based index) in a multi-surface post
7980 sideshow surface <sub> [options] edit individual surfaces of a post
8081 surface add <id> [flags] append a surface to an existing post
81- --md <f> markdown surface
82- --code <f> code surface (language inferred from filename)
83- --diff <f> diff surface from a patch
84- --terminal <f> terminal surface
85- --mermaid <f> mermaid surface
86- --json <f> json surface
87- --image <f> image surface (uploads the file first)
82+ --md <f> markdown surface (repeatable)
83+ --code <f> code surface (language inferred from filename; repeatable )
84+ --diff <f> diff surface from a patch (repeatable)
85+ --terminal <f> terminal surface (repeatable)
86+ --mermaid <f> mermaid surface (repeatable)
87+ --json <f> json surface (repeatable)
88+ --image <f> image surface (uploads the file first; repeatable )
8889 --layout split split layout for --diff surfaces
8990 --before <N> insert before surface N (id or index)
9091 --after <N> insert after surface N (id or index)
92+ surfaces append in command-line flag order; repeat a flag for several of one kind
9193 surface remove <id> <N> remove surface N (id or 0-based index)
9294 surface edit <id> <N> <file|-> replace surface N's content (kind preserved)
9395 surface move <id> <N> --to <M> move surface N to position M
@@ -478,6 +480,77 @@ function normalizeKits(flag) {
478480 return ids . length > 0 ? [ ...new Set ( ids ) ] : undefined ;
479481}
480482
483+ // Surface-kind flags accepted by `publish` and `surface add` (the two commands
484+ // that compose a post from one flag per surface kind). Each is declared
485+ // `multiple: true` in the parser, so a repeated flag yields an array — letting
486+ // an author emit several surfaces of the same kind (--diff a --diff b).
487+ const SURFACE_FLAGS = new Map ( [
488+ [ "md" , "markdown" ] ,
489+ [ "mermaid" , "mermaid" ] ,
490+ [ "diff" , "diff" ] ,
491+ [ "terminal" , "terminal" ] ,
492+ [ "json" , "json" ] ,
493+ [ "code" , "code" ] ,
494+ [ "image" , "image" ] ,
495+ ] ) ;
496+
497+ // Build a single surface object from one flag value. Mirrors the per-kind
498+ // construction that used to be inlined in `publish` and `surface add`.
499+ async function buildSurface ( kind , value , { session, layout } ) {
500+ const file = value || "-" ;
501+ if ( kind === "markdown" ) return { kind : "markdown" , markdown : readContent ( file ) } ;
502+ if ( kind === "mermaid" ) return { kind : "mermaid" , mermaid : readContent ( file ) } ;
503+ if ( kind === "diff" )
504+ return {
505+ kind : "diff" ,
506+ patch : readContent ( file ) ,
507+ ...( layout === "split" && { layout : "split" } ) ,
508+ } ;
509+ if ( kind === "terminal" ) return { kind : "terminal" , text : readContent ( file ) } ;
510+ if ( kind === "json" ) {
511+ const text = readContent ( file ) ;
512+ try {
513+ return { kind : "json" , data : JSON . parse ( text ) } ;
514+ } catch {
515+ fail ( `--json: invalid JSON${ value && value !== "-" ? ` in ${ value } ` : "" } ` ) ;
516+ }
517+ }
518+ if ( kind === "code" ) {
519+ const part = { kind : "code" , code : readContent ( file ) } ;
520+ const codeLang = value && value !== "-" ? inferLang ( value ) : undefined ;
521+ if ( codeLang ) part . language = codeLang ;
522+ if ( value && value !== "-" ) part . title = value . split ( "/" ) . pop ( ) || value ;
523+ return part ;
524+ }
525+ if ( kind === "image" ) {
526+ const asset = await uploadFile ( value , { session, kind : "image" } ) ;
527+ return { kind : "image" , assetId : asset . id } ;
528+ }
529+ fail ( `unknown surface kind: ${ kind } ` ) ;
530+ }
531+
532+ // Walk parseArgs `tokens` (which preserve command-line order, including
533+ // repeats when a surface flag is `multiple: true`) and build one surface per
534+ // flag occurrence, pulling successive values from each flag's value array.
535+ // Surfaces render top-to-bottom, so order is user-visible — this honors the
536+ // order the author wrote the flags, repeats included.
537+ async function surfacesFromFlags ( flags , tokens , { session, layout } ) {
538+ const idx = new Map ( ) ;
539+ const out = [ ] ;
540+ for ( const t of tokens ?? [ ] ) {
541+ if ( t . kind !== "option" || ! SURFACE_FLAGS . has ( t . name ) ) continue ;
542+ const flagName = t . name ;
543+ const arr = flags [ flagName ] ;
544+ if ( ! Array . isArray ( arr ) ) continue ;
545+ const i = idx . get ( flagName ) ?? 0 ;
546+ const value = arr [ i ] ;
547+ if ( value === undefined ) continue ;
548+ idx . set ( flagName , i + 1 ) ;
549+ out . push ( await buildSurface ( SURFACE_FLAGS . get ( flagName ) , value , { session, layout } ) ) ;
550+ }
551+ return out ;
552+ }
553+
481554async function publishSurface ( parts , flags ) {
482555 const session = await resolveSession ( flags , { create : true } ) ;
483556 return api ( "/api/surfaces" , {
@@ -545,11 +618,27 @@ const [cmd, ...rest] = process.argv.slice(2);
545618// Subcommand flag parsing. parseArgs is strict, so without this --help (or
546619// any typo) throws a raw stack trace; instead --help/-h prints usage and
547620// exits 0, and an unknown option fails with a one-line hint.
621+ //
622+ // Ids are base64url and can start with - or _ (~1/64 each). parseArgs strict
623+ // mode treats those as unknown options ("Unknown option '-6'" for an id like
624+ // "-6K4AJsKD4M"). We swap any id-shaped token that starts with a separator
625+ // for a sentinel before parsing, then restore it in the result — so positionals,
626+ // tokens, and option values all get the original id back, in the right order.
627+ const ID_LIKE = / ^ [ - _ ] (? ! [ - _ ] ) [ A - Z a - z 0 - 9 _ - ] { 7 , } $ / ;
548628function parse ( config = { } ) {
629+ const rescued = new Map ( ) ;
630+ const args = rest . map ( ( a ) => {
631+ if ( ID_LIKE . test ( a ) ) {
632+ const s = `\x00${ rescued . size } \x00` ;
633+ rescued . set ( s , a ) ;
634+ return s ;
635+ }
636+ return a ;
637+ } ) ;
549638 let parsed ;
550639 try {
551640 parsed = parseArgs ( {
552- args : rest ,
641+ args,
553642 ...config ,
554643 options : { ...config . options , help : { type : "boolean" , short : "h" } } ,
555644 } ) ;
@@ -561,6 +650,17 @@ function parse(config = {}) {
561650 console . log ( HELP ) ;
562651 process . exit ( 0 ) ;
563652 }
653+ const restore = ( v ) => ( typeof v === "string" && rescued . has ( v ) ? rescued . get ( v ) : v ) ;
654+ if ( parsed . positionals ) parsed . positionals = parsed . positionals . map ( restore ) ;
655+ if ( parsed . tokens ) {
656+ parsed . tokens = parsed . tokens . map ( ( t ) =>
657+ t . kind === "positional" && rescued . has ( t . value ) ? { ...t , value : rescued . get ( t . value ) } : t ,
658+ ) ;
659+ }
660+ for ( const k of Object . keys ( parsed . values ?? { } ) ) {
661+ const v = parsed . values [ k ] ;
662+ parsed . values [ k ] = Array . isArray ( v ) ? v . map ( restore ) : restore ( v ) ;
663+ }
564664 return parsed ;
565665}
566666
@@ -825,13 +925,13 @@ const commands = {
825925 allowPositionals : true ,
826926 options : {
827927 title : { type : "string" } ,
828- md : { type : "string" } ,
829- mermaid : { type : "string" } ,
830- diff : { type : "string" } ,
831- image : { type : "string" } ,
832- terminal : { type : "string" } ,
833- json : { type : "string" } ,
834- code : { type : "string" } ,
928+ md : { type : "string" , multiple : true } ,
929+ mermaid : { type : "string" , multiple : true } ,
930+ diff : { type : "string" , multiple : true } ,
931+ image : { type : "string" , multiple : true } ,
932+ terminal : { type : "string" , multiple : true } ,
933+ json : { type : "string" , multiple : true } ,
934+ code : { type : "string" , multiple : true } ,
835935 kit : { type : "string" , multiple : true } ,
836936 layout : { type : "string" } ,
837937 session : { type : "string" } ,
@@ -843,61 +943,15 @@ const commands = {
843943 const htmlPart = { kind : "html" , html : readContent ( positionals [ 0 ] ) } ;
844944 const kits = normalizeKits ( flags . kit ) ;
845945 if ( kits ) htmlPart . kits = kits ;
846- // Surfaces render top-to-bottom, so order is user-visible. Walk the
847- // parseArgs tokens (which preserve command-line order) and append each
848- // surface flag the first time it appears, instead of a fixed if-ladder.
849- const SURFACE_FLAGS = new Map ( [
850- [ "md" , "markdown" ] ,
851- [ "mermaid" , "mermaid" ] ,
852- [ "diff" , "diff" ] ,
853- [ "terminal" , "terminal" ] ,
854- [ "json" , "json" ] ,
855- [ "code" , "code" ] ,
856- [ "image" , "image" ] ,
857- ] ) ;
858- const orderedKinds = [ ] ;
859- const seen = new Set ( ) ;
860- for ( const t of tokens ?? [ ] ) {
861- if ( t . kind === "option" && SURFACE_FLAGS . has ( t . name ) && ! seen . has ( t . name ) ) {
862- seen . add ( t . name ) ;
863- orderedKinds . push ( SURFACE_FLAGS . get ( t . name ) ) ;
864- }
865- }
866946 // Resolve the session first so image uploads and the post share it.
867947 const session = await resolveSession ( flags , { create : true } ) ;
868- const parts = [ htmlPart ] ;
869- for ( const kind of orderedKinds ) {
870- if ( kind === "markdown" ) {
871- parts . push ( { kind : "markdown" , markdown : readContent ( flags . md || "-" ) } ) ;
872- } else if ( kind === "mermaid" ) {
873- parts . push ( { kind : "mermaid" , mermaid : readContent ( flags . mermaid || "-" ) } ) ;
874- } else if ( kind === "diff" ) {
875- parts . push ( {
876- kind : "diff" ,
877- patch : readContent ( flags . diff || "-" ) ,
878- ...( flags . layout === "split" && { layout : "split" } ) ,
879- } ) ;
880- } else if ( kind === "terminal" ) {
881- parts . push ( { kind : "terminal" , text : readContent ( flags . terminal || "-" ) } ) ;
882- } else if ( kind === "json" ) {
883- const text = readContent ( flags . json || "-" ) ;
884- try {
885- parts . push ( { kind : "json" , data : JSON . parse ( text ) } ) ;
886- } catch {
887- fail ( `--json: invalid JSON${ flags . json ? ` in ${ flags . json } ` : "" } ` ) ;
888- }
889- } else if ( kind === "code" ) {
890- const codeFile = flags . code || "-" ;
891- const part = { kind : "code" , code : readContent ( codeFile ) } ;
892- const codeLang = codeFile !== "-" ? inferLang ( codeFile ) : undefined ;
893- if ( codeLang ) part . language = codeLang ;
894- if ( codeFile !== "-" ) part . title = codeFile . split ( "/" ) . pop ( ) || codeFile ;
895- parts . push ( part ) ;
896- } else if ( kind === "image" ) {
897- const asset = await uploadFile ( flags . image , { session, kind : "image" } ) ;
898- parts . push ( { kind : "image" , assetId : asset . id } ) ;
899- }
900- }
948+ // Surfaces render top-to-bottom, so order is user-visible. `surfacesFromFlags`
949+ // walks parseArgs tokens (command-line order, repeats included) and builds
950+ // one surface per flag occurrence — so --diff a --diff b yields two diffs.
951+ const parts = [
952+ htmlPart ,
953+ ...( await surfacesFromFlags ( flags , tokens , { session, layout : flags . layout } ) ) ,
954+ ] ;
901955 outSurface ( await publishSurface ( parts , { ...flags , session } ) ) ;
902956 } ,
903957
@@ -1148,13 +1202,13 @@ const commands = {
11481202 tokens : true ,
11491203 allowPositionals : true ,
11501204 options : {
1151- md : { type : "string" } ,
1152- mermaid : { type : "string" } ,
1153- diff : { type : "string" } ,
1154- terminal : { type : "string" } ,
1155- json : { type : "string" } ,
1156- code : { type : "string" } ,
1157- image : { type : "string" } ,
1205+ md : { type : "string" , multiple : true } ,
1206+ mermaid : { type : "string" , multiple : true } ,
1207+ diff : { type : "string" , multiple : true } ,
1208+ terminal : { type : "string" , multiple : true } ,
1209+ json : { type : "string" , multiple : true } ,
1210+ code : { type : "string" , multiple : true } ,
1211+ image : { type : "string" , multiple : true } ,
11581212 before : { type : "string" } ,
11591213 after : { type : "string" } ,
11601214 layout : { type : "string" } ,
@@ -1164,57 +1218,13 @@ const commands = {
11641218 const postId = positionals [ 0 ] ;
11651219 if ( ! postId ) fail ( "usage: sideshow surface add <postId> [--md f] [--code f] ..." ) ;
11661220
1167- const SURFACE_FLAGS = new Map ( [
1168- [ "md" , "markdown" ] ,
1169- [ "mermaid" , "mermaid" ] ,
1170- [ "diff" , "diff" ] ,
1171- [ "terminal" , "terminal" ] ,
1172- [ "json" , "json" ] ,
1173- [ "code" , "code" ] ,
1174- [ "image" , "image" ] ,
1175- ] ) ;
1176- const orderedKinds = [ ] ;
1177- const seen = new Set ( ) ;
1178- for ( const t of tokens ?? [ ] ) {
1179- if ( t . kind === "option" && SURFACE_FLAGS . has ( t . name ) && ! seen . has ( t . name ) ) {
1180- seen . add ( t . name ) ;
1181- orderedKinds . push ( SURFACE_FLAGS . get ( t . name ) ) ;
1182- }
1183- }
1184- if ( orderedKinds . length === 0 ) fail ( "provide at least one surface flag (--md, --code, ...)" ) ;
11851221 const session = await resolveSession ( flags , { create : true } ) ;
1222+ const surfaces = await surfacesFromFlags ( flags , tokens , { session, layout : flags . layout } ) ;
1223+ if ( surfaces . length === 0 ) fail ( "provide at least one surface flag (--md, --code, ...)" ) ;
1224+ // Each surface is a separate append call so --before/--after positioning
1225+ // applies per surface (repeats append in command-line order).
11861226 let lastResult ;
1187- for ( const kind of orderedKinds ) {
1188- let surface ;
1189- if ( kind === "markdown" ) {
1190- surface = { kind : "markdown" , markdown : readContent ( flags . md || "-" ) } ;
1191- } else if ( kind === "mermaid" ) {
1192- surface = { kind : "mermaid" , mermaid : readContent ( flags . mermaid || "-" ) } ;
1193- } else if ( kind === "diff" ) {
1194- surface = {
1195- kind : "diff" ,
1196- patch : readContent ( flags . diff || "-" ) ,
1197- ...( flags . layout === "split" && { layout : "split" } ) ,
1198- } ;
1199- } else if ( kind === "terminal" ) {
1200- surface = { kind : "terminal" , text : readContent ( flags . terminal || "-" ) } ;
1201- } else if ( kind === "json" ) {
1202- const text = readContent ( flags . json || "-" ) ;
1203- try {
1204- surface = { kind : "json" , data : JSON . parse ( text ) } ;
1205- } catch {
1206- fail ( `--json: invalid JSON${ flags . json ? ` in ${ flags . json } ` : "" } ` ) ;
1207- }
1208- } else if ( kind === "code" ) {
1209- const codeFile = flags . code || "-" ;
1210- surface = { kind : "code" , code : readContent ( codeFile ) } ;
1211- const codeLang = codeFile !== "-" ? inferLang ( codeFile ) : undefined ;
1212- if ( codeLang ) surface . language = codeLang ;
1213- if ( codeFile !== "-" ) surface . title = codeFile . split ( "/" ) . pop ( ) || codeFile ;
1214- } else if ( kind === "image" ) {
1215- const asset = await uploadFile ( flags . image , { session, kind : "image" } ) ;
1216- surface = { kind : "image" , assetId : asset . id } ;
1217- }
1227+ for ( const surface of surfaces ) {
12181228 const body = { surface } ;
12191229 if ( flags . before !== undefined ) body . before = flags . before ;
12201230 if ( flags . after !== undefined ) body . after = flags . after ;
0 commit comments