@shaztech/video-pipeline 1.0.1

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.
@@ -0,0 +1 @@
1
+ .react-flow{direction:ltr;--xy-edge-stroke-default: #b1b1b7;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #555;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(255, 255, 255, .5);--xy-minimap-background-color-default: #fff;--xy-minimap-mask-background-color-default: rgba(240, 240, 240, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #e2e2e2;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: transparent;--xy-background-pattern-dots-color-default: #91919a;--xy-background-pattern-lines-color-default: #eee;--xy-background-pattern-cross-color-default: #e2e2e2;background-color:var(--xy-background-color, var(--xy-background-color-default));--xy-node-color-default: inherit;--xy-node-border-default: 1px solid #1a192b;--xy-node-background-color-default: #fff;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #1a192b;--xy-node-border-radius-default: 3px;--xy-handle-background-color-default: #1a192b;--xy-handle-border-color-default: #fff;--xy-selection-background-color-default: rgba(0, 89, 220, .08);--xy-selection-border-default: 1px dotted rgba(0, 89, 220, .8);--xy-controls-button-background-color-default: #fefefe;--xy-controls-button-background-color-hover-default: #f4f4f4;--xy-controls-button-color-default: inherit;--xy-controls-button-color-hover-default: inherit;--xy-controls-button-border-color-default: #eee;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #ffffff;--xy-edge-label-color-default: inherit;--xy-resize-background-color-default: #3367d9}.react-flow.dark{--xy-edge-stroke-default: #3e3e3e;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #727272;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(150, 150, 150, .25);--xy-minimap-background-color-default: #141414;--xy-minimap-mask-background-color-default: rgba(60, 60, 60, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #2b2b2b;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: #141414;--xy-background-pattern-dots-color-default: #777;--xy-background-pattern-lines-color-default: #777;--xy-background-pattern-cross-color-default: #777;--xy-node-color-default: #f8f8f8;--xy-node-border-default: 1px solid #3c3c3c;--xy-node-background-color-default: #1e1e1e;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #999;--xy-handle-background-color-default: #bebebe;--xy-handle-border-color-default: #1e1e1e;--xy-selection-background-color-default: rgba(200, 200, 220, .08);--xy-selection-border-default: 1px dotted rgba(200, 200, 220, .8);--xy-controls-button-background-color-default: #2b2b2b;--xy-controls-button-background-color-hover-default: #3e3e3e;--xy-controls-button-color-default: #f8f8f8;--xy-controls-button-color-hover-default: #fff;--xy-controls-button-border-color-default: #5b5b5b;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #141414;--xy-edge-label-color-default: #f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props, var(--xy-background-color, var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{position:absolute;width:100%;height:100%;top:0;left:0}.react-flow__pane{z-index:1}.react-flow__pane.draggable{cursor:grab}.react-flow__pane.dragging{cursor:grabbing}.react-flow__pane.selection{cursor:pointer}.react-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.react-flow__renderer{z-index:4}.react-flow__selection{z-index:6}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible{outline:none}.react-flow__edge-path{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default));stroke-width:var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));fill:none}.react-flow__connection-path{stroke:var(--xy-connectionline-stroke, var(--xy-connectionline-stroke-default));stroke-width:var(--xy-connectionline-stroke-width, var(--xy-connectionline-stroke-width-default));fill:none}.react-flow .react-flow__edges{position:absolute}.react-flow .react-flow__edges svg{overflow:visible;position:absolute;pointer-events:none}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.react-flow__edge.animated path.react-flow__edge-interaction{stroke-dasharray:none;animation:none}.react-flow__edge.inactive{pointer-events:none}.react-flow__edge.selected,.react-flow__edge:focus,.react-flow__edge:focus-visible{outline:none}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge.selectable:focus .react-flow__edge-path,.react-flow__edge.selectable:focus-visible .react-flow__edge-path{stroke:var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default))}.react-flow__edge-textwrapper{pointer-events:all}.react-flow__edge .react-flow__edge-text{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__arrowhead polyline{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__arrowhead polyline.arrowclosed{fill:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__connection{pointer-events:none}.react-flow__connection .animated{stroke-dasharray:5;animation:dashdraw .5s linear infinite}svg.react-flow__connectionline{z-index:1001;overflow:visible;position:absolute}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default}.react-flow__node.selectable{cursor:pointer}.react-flow__node.draggable{cursor:grab;pointer-events:all}.react-flow__node.draggable.dragging{cursor:grabbing}.react-flow__nodesselection{z-index:3;transform-origin:left top;pointer-events:none}.react-flow__nodesselection-rect{position:absolute;pointer-events:all;cursor:grab}.react-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px;width:6px;height:6px;background-color:var(--xy-handle-background-color, var(--xy-handle-background-color-default));border:1px solid var(--xy-handle-border-color, var(--xy-handle-border-color-default));border-radius:100%}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;left:50%;bottom:0;transform:translate(-50%,50%)}.react-flow__handle-top{top:0;left:50%;transform:translate(-50%,-50%)}.react-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.react-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.react-flow__edgeupdater{cursor:move;pointer-events:all}.react-flow__pane.selection .react-flow__panel{pointer-events:none}.react-flow__panel{position:absolute;z-index:5;margin:15px}.react-flow__panel.top{top:0}.react-flow__panel.bottom{bottom:0}.react-flow__panel.top.center,.react-flow__panel.bottom.center{left:50%;transform:translate(-15px) translate(-50%)}.react-flow__panel.left{left:0}.react-flow__panel.right{right:0}.react-flow__panel.left.center,.react-flow__panel.right.center{top:50%;transform:translateY(-15px) translateY(-50%)}.react-flow__attribution{font-size:10px;background:var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));padding:2px 3px;margin:0}.react-flow__attribution a{text-decoration:none;color:#999}@keyframes dashdraw{0%{stroke-dashoffset:10}}.react-flow__edgelabel-renderer{position:absolute;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;left:0;top:0}.react-flow__viewport-portal{position:absolute;width:100%;height:100%;left:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__minimap{background:var( --xy-minimap-background-color-props, var(--xy-minimap-background-color, var(--xy-minimap-background-color-default)) )}.react-flow__minimap-svg{display:block}.react-flow__minimap-mask{fill:var( --xy-minimap-mask-background-color-props, var(--xy-minimap-mask-background-color, var(--xy-minimap-mask-background-color-default)) );stroke:var( --xy-minimap-mask-stroke-color-props, var(--xy-minimap-mask-stroke-color, var(--xy-minimap-mask-stroke-color-default)) );stroke-width:var( --xy-minimap-mask-stroke-width-props, var(--xy-minimap-mask-stroke-width, var(--xy-minimap-mask-stroke-width-default)) )}.react-flow__minimap-node{fill:var( --xy-minimap-node-background-color-props, var(--xy-minimap-node-background-color, var(--xy-minimap-node-background-color-default)) );stroke:var( --xy-minimap-node-stroke-color-props, var(--xy-minimap-node-stroke-color, var(--xy-minimap-node-stroke-color-default)) );stroke-width:var( --xy-minimap-node-stroke-width-props, var(--xy-minimap-node-stroke-width, var(--xy-minimap-node-stroke-width-default)) )}.react-flow__background-pattern.dots{fill:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-dots-color-default)) )}.react-flow__background-pattern.lines{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-lines-color-default)) )}.react-flow__background-pattern.cross{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-cross-color-default)) )}.react-flow__controls{display:flex;flex-direction:column;box-shadow:var(--xy-controls-box-shadow, var(--xy-controls-box-shadow-default))}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{display:flex;justify-content:center;align-items:center;height:26px;width:26px;padding:4px;border:none;background:var(--xy-controls-button-background-color, var(--xy-controls-button-background-color-default));border-bottom:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) );color:var( --xy-controls-button-color-props, var(--xy-controls-button-color, var(--xy-controls-button-color-default)) );cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__controls-button svg{width:100%;max-width:12px;max-height:12px;fill:currentColor}.react-flow__edge.updating .react-flow__edge-path{stroke:#777}.react-flow__edge-text{font-size:10px}.react-flow__node.selectable:focus,.react-flow__node.selectable:focus-visible{outline:none}.react-flow__node-input,.react-flow__node-default,.react-flow__node-output,.react-flow__node-group{padding:10px;border-radius:var(--xy-node-border-radius, var(--xy-node-border-radius-default));width:150px;font-size:12px;color:var(--xy-node-color, var(--xy-node-color-default));text-align:center;border:var(--xy-node-border, var(--xy-node-border-default));background-color:var(--xy-node-background-color, var(--xy-node-background-color-default))}.react-flow__node-input.selectable:hover,.react-flow__node-default.selectable:hover,.react-flow__node-output.selectable:hover,.react-flow__node-group.selectable:hover{box-shadow:var(--xy-node-boxshadow-hover, var(--xy-node-boxshadow-hover-default))}.react-flow__node-input.selectable.selected,.react-flow__node-input.selectable:focus,.react-flow__node-input.selectable:focus-visible,.react-flow__node-default.selectable.selected,.react-flow__node-default.selectable:focus,.react-flow__node-default.selectable:focus-visible,.react-flow__node-output.selectable.selected,.react-flow__node-output.selectable:focus,.react-flow__node-output.selectable:focus-visible,.react-flow__node-group.selectable.selected,.react-flow__node-group.selectable:focus,.react-flow__node-group.selectable:focus-visible{box-shadow:var(--xy-node-boxshadow-selected, var(--xy-node-boxshadow-selected-default))}.react-flow__node-group{background-color:var(--xy-node-group-background-color, var(--xy-node-group-background-color-default))}.react-flow__nodesselection-rect,.react-flow__selection{background:var(--xy-selection-background-color, var(--xy-selection-background-color-default));border:var(--xy-selection-border, var(--xy-selection-border-default))}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible,.react-flow__selection:focus,.react-flow__selection:focus-visible{outline:none}.react-flow__controls-button:hover{background:var( --xy-controls-button-background-color-hover-props, var(--xy-controls-button-background-color-hover, var(--xy-controls-button-background-color-hover-default)) );color:var( --xy-controls-button-color-hover-props, var(--xy-controls-button-color-hover, var(--xy-controls-button-color-hover-default)) )}.react-flow__controls-button:disabled{pointer-events:none}.react-flow__controls-button:disabled svg{fill-opacity:.4}.react-flow__controls-button:last-child{border-bottom:none}.react-flow__controls.horizontal .react-flow__controls-button{border-bottom:none;border-right:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) )}.react-flow__controls.horizontal .react-flow__controls-button:last-child{border-right:none}.react-flow__resize-control{position:absolute}.react-flow__resize-control.left,.react-flow__resize-control.right{cursor:ew-resize}.react-flow__resize-control.top,.react-flow__resize-control.bottom{cursor:ns-resize}.react-flow__resize-control.top.left,.react-flow__resize-control.bottom.right{cursor:nwse-resize}.react-flow__resize-control.bottom.left,.react-flow__resize-control.top.right{cursor:nesw-resize}.react-flow__resize-control.handle{width:5px;height:5px;border:1px solid #fff;border-radius:1px;background-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));translate:-50% -50%}.react-flow__resize-control.handle.left{left:0;top:50%}.react-flow__resize-control.handle.right{left:100%;top:50%}.react-flow__resize-control.handle.top{left:50%;top:0}.react-flow__resize-control.handle.bottom{left:50%;top:100%}.react-flow__resize-control.handle.top.left,.react-flow__resize-control.handle.bottom.left{left:0}.react-flow__resize-control.handle.top.right,.react-flow__resize-control.handle.bottom.right{left:100%}.react-flow__resize-control.line{border-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));border-width:0;border-style:solid}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;transform:translate(-50%);top:0;height:100%}.react-flow__resize-control.line.left{left:0;border-left-width:1px}.react-flow__resize-control.line.right{left:100%;border-right-width:1px}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{height:1px;transform:translateY(-50%);left:0;width:100%}.react-flow__resize-control.line.top{top:0;border-top-width:1px}.react-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}.react-flow__edge-textbg{fill:var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default))}.react-flow__edge-text{fill:var(--xy-edge-label-color, var(--xy-edge-label-color-default))}._toolbar_1uqg7_1{position:absolute;top:0;left:0;right:0;z-index:10;display:flex;align-items:center;justify-content:space-between;padding:8px 14px;background:#0d1117eb;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border-bottom:1px solid #21262d}._left_1uqg7_16{display:flex;align-items:center;gap:10px}._right_1uqg7_22{display:flex;align-items:center;gap:8px}._pipelineName_1uqg7_28{font-size:13px;font-weight:700;color:#e0e0e0;letter-spacing:.02em;cursor:text}._pipelineName_1uqg7_28:hover{color:#fff}._pipelineNameInput_1uqg7_40{font-size:13px;font-weight:700;color:#e0e0e0;letter-spacing:.02em;background:transparent;border:none;border-bottom:1px solid #388bfd;outline:none;padding:0;min-width:80px;width:auto}._divider_1uqg7_54{width:1px;height:20px;background:#21262d}._addBtn_1uqg7_60{border:none;border-radius:6px;font-size:12px;font-weight:600;padding:6px 12px;cursor:pointer;transition:opacity .15s,transform .1s}._addBtn_1uqg7_60:hover{opacity:.85;transform:translateY(-1px)}._addBtn_1uqg7_60:active{transform:translateY(0)}._cutter_1uqg7_79{background:#e9456026;color:#e94560;border:1px solid rgba(233,69,96,.4)}._stitcher_1uqg7_85{background:#53348333;color:#a371f7;border:1px solid rgba(163,113,247,.4)}._outputFolder_1uqg7_91{background:#2ea04326;color:#3fb950;border:1px solid rgba(46,160,67,.4)}._inputFile_1uqg7_97{background:#388bfd26;color:#388bfd;border:1px solid rgba(56,139,253,.4)}._inputFolder_1uqg7_103{background:#d2992226;color:#d29922;border:1px solid rgba(210,153,34,.4)}._saveBtn_1uqg7_109{background:#21262d;border:1px solid #30363d;border-radius:6px;color:#c9d1d9;font-size:12px;font-weight:600;padding:6px 14px;cursor:pointer;position:relative;transition:background .15s,border-color .15s,color .15s;display:flex;align-items:center;gap:6px}._saveBtn_1uqg7_109:hover:not(:disabled){background:#30363d;border-color:#8b949e}._saveBtn_1uqg7_109:disabled{opacity:.6;cursor:default}._dirty_1uqg7_135{border-color:#e94560;color:#e94560}._savedGreen_1uqg7_140{color:#3fb950;border-color:#3fb95066}._errorRed_1uqg7_145{color:#f85149;border-color:#f8514966}._serverStatus_1uqg7_150{display:flex;align-items:center;gap:5px;font-size:11px;font-weight:600;color:#484f58;transition:color .3s}._serverStatus_1uqg7_150._online_1uqg7_160{color:#3fb950}._serverStatus_1uqg7_150._offline_1uqg7_161{color:#f85149}._serverDot_1uqg7_163{width:7px;height:7px;border-radius:50%;flex-shrink:0;background:currentColor;transition:box-shadow .3s}._serverStatus_1uqg7_150._online_1uqg7_160 ._serverDot_1uqg7_163{box-shadow:0 0 5px #3fb950b3}._serverStatus_1uqg7_150._offline_1uqg7_161 ._serverDot_1uqg7_163{box-shadow:0 0 5px #f85149b3}._serverLabel_1uqg7_175{white-space:nowrap}._saveIcon_1uqg7_179{width:13px;height:13px;flex-shrink:0}._node_9qeze_1{background:#16213e;border:1.5px solid #0f3460;border-radius:10px;min-width:260px;max-width:320px;box-shadow:0 4px 20px #00000080;font-size:12px;color:#c9d1d9;transition:border-color .15s;position:relative}._deleteBtn_9qeze_14{position:absolute;top:-8px;right:-8px;width:18px;height:18px;border-radius:50%;background:#21262d;border:1.5px solid #30363d;color:#6e7681;font-size:11px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;opacity:0;transition:opacity .15s,background .15s,color .15s;z-index:10}._node_9qeze_1:hover ._deleteBtn_9qeze_14{opacity:1}._deleteBtn_9qeze_14:hover{background:#e94560;border-color:#e94560;color:#fff}._node_9qeze_1._selected_9qeze_46{border-color:#e94560;box-shadow:0 0 0 2px #e9456040,0 4px 20px #00000080}._cutter_9qeze_51{border-top:3px solid #e94560}._stitcher_9qeze_52{border-top:3px solid #533483}._outputFolder_9qeze_53{border-top:3px solid #2ea043}._inputFile_9qeze_54{border-top:3px solid #388bfd}._inputFolder_9qeze_55{border-top:3px solid #d29922}._header_9qeze_57{display:flex;align-items:center;gap:6px;padding:8px 10px 6px;border-bottom:1px solid #0f3460}._badge_9qeze_65{font-size:10px;font-weight:700;letter-spacing:.03em;color:#8b949e;text-transform:uppercase;white-space:nowrap}._labelInput_9qeze_74{flex:1;background:transparent;border:none;outline:none;color:#e0e0e0;font-size:13px;font-weight:600;min-width:0}._body_9qeze_85{padding:10px;display:flex;flex-direction:column;gap:5px}._fieldLabel_9qeze_92{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:#8b949e;margin-top:4px}._input_9qeze_54{width:100%;background:#0d1117;border:1px solid #21262d;border-radius:5px;color:#c9d1d9;font-size:12px;padding:5px 8px;outline:none;transition:border-color .15s}._input_9qeze_54:focus{border-color:#388bfd}._radioGroup_9qeze_116{display:flex;flex-direction:column;gap:3px}._radio_9qeze_116{display:flex;align-items:center;gap:5px;cursor:pointer;font-size:11px;color:#c9d1d9}._checkboxRow_9qeze_131{display:flex;gap:12px;margin-top:2px}._checkbox_9qeze_131{display:flex;align-items:center;gap:4px;cursor:pointer;font-size:11px;color:#c9d1d9}._sectionHeader_9qeze_146{display:flex;align-items:center;justify-content:space-between;margin-top:4px}._addBtn_9qeze_153{background:#21262d;border:1px solid #30363d;border-radius:4px;color:#8b949e;font-size:10px;padding:2px 7px;cursor:pointer;transition:background .1s}._addBtn_9qeze_153:hover{background:#30363d;color:#c9d1d9}._inputRow_9qeze_169{display:flex;flex-direction:column;gap:3px}._inputRowMain_9qeze_175{display:flex;gap:4px;align-items:center}._removeBtn_9qeze_181{background:none;border:none;color:#6e7681;cursor:pointer;font-size:12px;padding:2px 4px;border-radius:3px;flex-shrink:0;transition:color .1s}._removeBtn_9qeze_181:hover{color:#e94560}._hint_9qeze_197{font-size:10px;color:#6e7681;font-style:italic;margin-bottom:2px}._dragHandle_9qeze_204{cursor:grab;color:#6e7681;font-size:14px;padding:0 4px;flex-shrink:0;-webkit-user-select:none;user-select:none;line-height:1;touch-action:none}._dragHandle_9qeze_204:hover{color:#c9d1d9}._dragHandle_9qeze_204:active{cursor:grabbing}._inputRow_9qeze_169._dragging_9qeze_223{opacity:.35}._inputRow_9qeze_169._dropTarget_9qeze_227{border-top:2px solid #388bfd;margin-top:-2px}._inputRow_9qeze_169._dropAfter_9qeze_232{border-bottom:2px solid #388bfd;margin-bottom:-2px}._edgeItem_9qeze_237{flex:1;display:flex;align-items:center;gap:6px;background:#0d1117;border:1px solid #21262d;border-radius:5px;padding:5px 8px;color:#8b949e;font-size:12px;min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}._stepperRow_9qeze_254{display:flex;gap:4px;align-items:stretch}._stepperRow_9qeze_254 ._input_9qeze_54{flex:1}._stepperBtns_9qeze_264{display:flex;flex-direction:column;gap:1px;flex-shrink:0}._stepBtn_9qeze_271{background:#21262d;border:1px solid #30363d;border-radius:3px;color:#8b949e;font-size:8px;padding:0 5px;cursor:pointer;line-height:1;flex:1;transition:background .1s,color .1s}._stepBtn_9qeze_271:hover{background:#30363d;color:#c9d1d9}._pencilBtn_9qeze_289{background:none;border:1px solid transparent;border-radius:3px;color:#6e7681;cursor:pointer;font-size:13px;padding:1px 4px;flex-shrink:0;transition:color .1s,border-color .1s;line-height:1.2}._pencilBtn_9qeze_289:hover{color:#c9d1d9;border-color:#30363d}._pencilBtn_9qeze_289._pencilActive_9qeze_307{color:#388bfd;border-color:#388bfd}._durationOverrideRow_9qeze_312{display:flex;align-items:center;gap:4px;padding-left:22px}._durationOverrideLabel_9qeze_319{font-size:10px;color:#6e7681;white-space:nowrap;flex-shrink:0}._resetBtn_9qeze_326{background:none;border:1px solid #30363d;border-radius:3px;color:#6e7681;cursor:pointer;font-size:12px;padding:1px 5px;flex-shrink:0;transition:color .1s;line-height:1.4}._resetBtn_9qeze_326:hover{color:#c9d1d9}._edgeBadge_9qeze_343{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:#a371f7;background:#1a1030;border:1px solid #533483;border-radius:3px;padding:1px 4px;flex-shrink:0}._handle_9qeze_356{width:10px;height:10px;background:#533483;border:2px solid #a371f7;border-radius:50%}._container_1ymg3_1{display:flex;flex-direction:column;gap:2px;width:100%}._wrapper_1ymg3_8{display:flex;align-items:center;background:#0d1117;border:1px solid #21262d;border-radius:5px;transition:border-color .15s,background .15s;overflow:hidden;width:100%}._wrapper_1ymg3_8:focus-within{border-color:#388bfd}._wrapper_1ymg3_8._dragOver_1ymg3_23{border-color:#388bfd;background:#0d1a2d}._textInput_1ymg3_28{flex:1;background:transparent;border:none;color:#c9d1d9;font-size:12px;padding:5px 8px;outline:none;min-width:0}._textInput_1ymg3_28::placeholder{color:#484f58}._browseBtn_1ymg3_43{background:none;border:none;border-left:1px solid #21262d;color:#6e7681;cursor:pointer;font-size:12px;padding:4px 7px;line-height:1;flex-shrink:0;transition:color .1s,background .1s}._browseBtn_1ymg3_43:hover{color:#c9d1d9;background:#21262d}._wrapper_1ymg3_8._error_1ymg3_61{border-color:#e94560}._errorMsg_1ymg3_65{font-size:10px;color:#e94560;padding:0 2px}._hidden_1ymg3_71{display:none}.app{height:100%;display:flex;flex-direction:column}.canvas-wrapper{flex:1;margin-top:49px}.react-flow__edge-path{stroke:#8b949e;stroke-width:2}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge:hover .react-flow__edge-path{stroke:#a371f7}.react-flow__connection-path{stroke:#a371f7;stroke-width:2}.react-flow__controls{background:#0d1117;border:1px solid #21262d;border-radius:6px;overflow:hidden}.react-flow__controls-button{background:#0d1117;border-bottom:1px solid #21262d;fill:#8b949e}.react-flow__controls-button:hover{background:#21262d;fill:#c9d1d9}
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Video Pipeline Editor</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+ html, body, #root { height: 100%; width: 100%; overflow: hidden; }
10
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #1a1a2e; color: #e0e0e0; }
11
+ </style>
12
+ <script type="module" crossorigin src="./assets/index-DV6clFYC.js"></script>
13
+ <link rel="stylesheet" crossorigin href="./assets/index-K7ALkYj6.css">
14
+ </head>
15
+ <body>
16
+ <div id="root"></div>
17
+ </body>
18
+ </html>
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@shaztech/video-pipeline",
3
+ "version": "1.0.1",
4
+ "description": "Visual node-based video processing pipeline CLI",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "bin": {
8
+ "video-pipeline": "./bin.js"
9
+ },
10
+ "files": [
11
+ "bin.js",
12
+ "src/",
13
+ "dist/"
14
+ ],
15
+ "engines": {
16
+ "node": ">=20.0.0"
17
+ },
18
+ "dependencies": {
19
+ "@shaztech/video-cutter": "^1.0.0",
20
+ "@shaztech/video-stitcher": "^1.0.0",
21
+ "ajv": "^8.18.0",
22
+ "chalk": "^5.3.0",
23
+ "commander": "^14.0.0",
24
+ "execa": "^9.5.2",
25
+ "express": "^5.0.1",
26
+ "glob": "^11.0.0",
27
+ "open": "^10.1.0",
28
+ "ora": "^8.0.1",
29
+ "p-queue": "^8.1.0",
30
+ "ws": "^8.17.0"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "scripts": {
36
+ "test": "vitest run --coverage"
37
+ },
38
+ "devDependencies": {
39
+ "@vitest/coverage-v8": "^4.1.1",
40
+ "vitest": "^4.1.1"
41
+ }
42
+ }
package/src/cli.js ADDED
@@ -0,0 +1,35 @@
1
+ /*
2
+ * Copyright 2026 Shazron Abdullah
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { Command } from 'commander'
18
+ import { createCommand } from './commands/create.js'
19
+ import { editCommand } from './commands/edit.js'
20
+ import { runCommand } from './commands/run.js'
21
+ import { validateCommand } from './commands/validate.js'
22
+
23
+ const program = new Command()
24
+
25
+ program
26
+ .name('video-pipeline')
27
+ .description('Visual node-based video processing pipeline')
28
+ .version('1.0.0')
29
+
30
+ program.addCommand(createCommand())
31
+ program.addCommand(editCommand())
32
+ program.addCommand(runCommand())
33
+ program.addCommand(validateCommand())
34
+
35
+ program.parse()
@@ -0,0 +1,43 @@
1
+ /*
2
+ * Copyright 2026 Shazron Abdullah
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { Command } from 'commander'
18
+ import { writeFileSync, existsSync } from 'fs'
19
+ import path from 'path'
20
+ import chalk from 'chalk'
21
+ import { createDefaultSpec } from '../spec/defaultSpec.js'
22
+
23
+ export function createCommand() {
24
+ return new Command('create')
25
+ .description('Create a new pipeline spec file')
26
+ .argument('[name]', 'Pipeline name (also used as filename)', 'pipeline')
27
+ .option('-f, --force', 'Overwrite if file already exists')
28
+ .action((name, opts) => {
29
+ const filename = name.endsWith('.json') ? name : `${name}.json`
30
+ const filepath = path.resolve(process.cwd(), filename)
31
+
32
+ if (existsSync(filepath) && !opts.force) {
33
+ console.error(chalk.red(`File already exists: ${filename}`))
34
+ console.error(chalk.dim('Use --force to overwrite'))
35
+ process.exit(1)
36
+ }
37
+
38
+ const spec = createDefaultSpec(name.replace(/\.json$/, ''))
39
+ writeFileSync(filepath, JSON.stringify(spec, null, 2) + '\n', 'utf8')
40
+ console.log(chalk.green(`Created: ${filename}`))
41
+ console.log(chalk.dim(`Edit it visually: video-pipeline edit ${filename}`))
42
+ })
43
+ }
@@ -0,0 +1,57 @@
1
+ /*
2
+ * Copyright 2026 Shazron Abdullah
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { Command } from 'commander'
18
+ import { existsSync, writeFileSync } from 'fs'
19
+ import path from 'path'
20
+ import chalk from 'chalk'
21
+ import { createDefaultSpec } from '../spec/defaultSpec.js'
22
+ import { startEditorServer } from '../server/index.js'
23
+
24
+ export function editCommand() {
25
+ return new Command('edit')
26
+ .description('Open the visual pipeline editor in a browser')
27
+ .argument('[spec]', 'Path to pipeline spec JSON file', 'pipeline.json')
28
+ .option('-p, --port <port>', 'Server port (default: auto-detect)', parseInt)
29
+ .action(async (specArg, opts) => {
30
+ const specPath = path.resolve(process.cwd(), specArg)
31
+
32
+ if (!existsSync(specPath)) {
33
+ const name = path.basename(specArg, '.json')
34
+ const spec = createDefaultSpec(name)
35
+ writeFileSync(specPath, JSON.stringify(spec, null, 2) + '\n', 'utf8')
36
+ console.log(chalk.dim(`Created new spec: ${specArg}`))
37
+ }
38
+
39
+ const { url } = await startEditorServer(specPath, { port: opts.port })
40
+
41
+ console.log(chalk.green(`\n Editor ready: ${url}\n`))
42
+ console.log(chalk.dim(' Press Ctrl+C to stop\n'))
43
+
44
+ const { default: open } = await import('open')
45
+ await open(url)
46
+
47
+ // Keep process alive
48
+ await new Promise((resolve) => {
49
+ process.once('SIGINT', () => {
50
+ console.log(chalk.dim('\n Editor closed.'))
51
+ resolve()
52
+ })
53
+ })
54
+
55
+ process.exit(0)
56
+ })
57
+ }
@@ -0,0 +1,64 @@
1
+ /*
2
+ * Copyright 2026 Shazron Abdullah
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { Command } from 'commander'
18
+ import { readFileSync, existsSync } from 'fs'
19
+ import chalk from 'chalk'
20
+ import { validateSpec } from '../spec/schema.js'
21
+ import { executePipeline } from '../executor/index.js'
22
+
23
+ export function runCommand() {
24
+ return new Command('run')
25
+ .description('Execute a pipeline from a spec file')
26
+ .argument('<spec>', 'Path to pipeline spec JSON file')
27
+ .option('--keep-temp', 'Keep intermediate temp files after execution')
28
+ .option('--dry-run', 'Print execution plan without running')
29
+ .option('--overwrite', 'Overwrite output files if they already exist')
30
+ .action(async (specArg, opts) => {
31
+ if (!existsSync(specArg)) {
32
+ console.error(chalk.red(`File not found: ${specArg}`))
33
+ process.exit(1)
34
+ }
35
+
36
+ let spec
37
+ try {
38
+ spec = JSON.parse(readFileSync(specArg, 'utf8'))
39
+ } catch (err) {
40
+ console.error(chalk.red(`Invalid JSON: ${err.message}`))
41
+ process.exit(1)
42
+ }
43
+
44
+ const validation = validateSpec(spec)
45
+ if (!validation.valid) {
46
+ console.error(chalk.red('Invalid pipeline spec:'))
47
+ for (const err of validation.errors) {
48
+ console.error(chalk.red(` • ${err}`))
49
+ }
50
+ process.exit(1)
51
+ }
52
+
53
+ try {
54
+ await executePipeline(spec, {
55
+ keepTemp: opts.keepTemp ?? false,
56
+ dryRun: opts.dryRun ?? false,
57
+ overwrite: opts.overwrite ?? false
58
+ })
59
+ } catch (err) {
60
+ console.error(chalk.red(`Pipeline failed: ${err.message}`))
61
+ process.exit(1)
62
+ }
63
+ })
64
+ }
@@ -0,0 +1,79 @@
1
+ /*
2
+ * Copyright 2026 Shazron Abdullah
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { Command } from 'commander'
18
+ import { readFileSync, existsSync } from 'fs'
19
+ import { fileURLToPath } from 'url'
20
+ import path from 'path'
21
+ import chalk from 'chalk'
22
+ import Ajv from 'ajv'
23
+ import { validateSpec } from '../spec/schema.js'
24
+
25
+ // Resolve the schema relative to the repo root (works both installed and local)
26
+ const here = path.dirname(fileURLToPath(import.meta.url))
27
+ const schemaPath = path.resolve(here, '../../../../pipeline.schema.json')
28
+ const schema = JSON.parse(readFileSync(schemaPath, 'utf8'))
29
+
30
+ const ajv = new Ajv({ allErrors: true, strict: false })
31
+ const ajvValidate = ajv.compile(schema)
32
+
33
+ export function validateCommand() {
34
+ return new Command('validate')
35
+ .description('Validate a pipeline spec file against the JSON schema and semantic rules')
36
+ .argument('<spec>', 'Path to pipeline spec JSON file')
37
+ .action((specArg) => {
38
+ if (!existsSync(specArg)) {
39
+ console.error(chalk.red(`File not found: ${specArg}`))
40
+ process.exit(1)
41
+ }
42
+
43
+ let spec
44
+ try {
45
+ spec = JSON.parse(readFileSync(specArg, 'utf8'))
46
+ } catch (err) {
47
+ console.error(chalk.red(`Invalid JSON: ${err.message}`))
48
+ process.exit(1)
49
+ }
50
+
51
+ const errors = []
52
+
53
+ // 1. JSON Schema validation
54
+ const schemaValid = ajvValidate(spec)
55
+ if (!schemaValid) {
56
+ for (const err of ajvValidate.errors) {
57
+ const loc = err.instancePath || '(root)'
58
+ errors.push(`Schema: ${loc} ${err.message}`)
59
+ }
60
+ }
61
+
62
+ // 2. Semantic validation (duplicate ids, edge references, etc.)
63
+ const result = validateSpec(spec)
64
+ if (!result.valid) {
65
+ errors.push(...result.errors)
66
+ }
67
+
68
+ if (errors.length === 0) {
69
+ console.log(chalk.green(`✓ Valid pipeline: ${spec.name}`))
70
+ console.log(chalk.dim(` ${spec.nodes.length} node(s), ${spec.edges.length} edge(s)`))
71
+ } else {
72
+ console.error(chalk.red('✗ Invalid pipeline:'))
73
+ for (const err of errors) {
74
+ console.error(chalk.red(` • ${err}`))
75
+ }
76
+ process.exit(1)
77
+ }
78
+ })
79
+ }
@@ -0,0 +1,148 @@
1
+ /*
2
+ * Copyright 2026 Shazron Abdullah
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import os from 'os'
18
+ import path from 'path'
19
+ import { mkdirSync, rmSync } from 'fs'
20
+ import chalk from 'chalk'
21
+ import PQueue from 'p-queue'
22
+ import { topoSort } from './topoSort.js'
23
+ import { handleVideoCutter } from './nodeHandlers/video-cutter.js'
24
+ import { handleVideoStitcher } from './nodeHandlers/video-stitcher.js'
25
+ import { handleInputFile } from './nodeHandlers/input-file.js'
26
+ import { handleInputFolder } from './nodeHandlers/input-folder.js'
27
+
28
+ const HANDLERS = {
29
+ 'video-cutter': handleVideoCutter,
30
+ 'video-stitcher': handleVideoStitcher,
31
+ 'output-folder': async () => {}, // resolved during cutter/stitcher execution
32
+ 'input-file': handleInputFile,
33
+ 'input-folder': handleInputFolder
34
+ }
35
+
36
+ /**
37
+ * Executes a validated pipeline spec.
38
+ *
39
+ * @param {object} spec - parsed pipeline spec
40
+ * @param {{ keepTemp?: boolean, dryRun?: boolean }} opts
41
+ */
42
+ export async function executePipeline(spec, opts = {}) {
43
+ const { keepTemp = false, dryRun = false, overwrite = false } = opts
44
+
45
+ const levels = topoSort(spec.nodes, spec.edges)
46
+
47
+ const tempRoot = path.join(
48
+ os.tmpdir(),
49
+ 'video-pipeline',
50
+ `${spec.name}-${Date.now()}`
51
+ )
52
+
53
+ if (!dryRun) {
54
+ mkdirSync(tempRoot, { recursive: true })
55
+ }
56
+
57
+ console.log(chalk.bold(`\nPipeline: ${spec.name}`))
58
+ console.log(chalk.dim(` ${spec.nodes.length} node(s) across ${levels.length} level(s)`))
59
+ if (dryRun) console.log(chalk.yellow(' [dry-run mode]\n'))
60
+
61
+ // context stores runtime outputs keyed by nodeId
62
+ const context = new Map()
63
+
64
+ // Build a nodeId -> node map and edge lookup
65
+ const nodeMap = new Map(spec.nodes.map((n) => [n.id, n]))
66
+
67
+ for (let i = 0; i < levels.length; i++) {
68
+ const level = levels[i]
69
+ console.log(chalk.dim(`\nLevel ${i + 1}: ${level.join(', ')}`))
70
+
71
+ const queue = new PQueue({ concurrency: os.cpus().length })
72
+
73
+ const tasks = level.map((nodeId) =>
74
+ queue.add(async () => {
75
+ const node = nodeMap.get(nodeId)
76
+ const handler = HANDLERS[node.type]
77
+
78
+ if (!handler) {
79
+ throw new Error(`No handler for node type: ${node.type}`)
80
+ }
81
+
82
+ // Find edges where this node is the target
83
+ const incomingEdges = spec.edges.filter((e) => e.target === nodeId)
84
+
85
+ if (node.type === 'video-stitcher') {
86
+ // Collect output-folder paths from connected output-folder nodes
87
+ const outputFolderPaths = spec.edges
88
+ .filter((e) => e.source === nodeId)
89
+ .map((e) => nodeMap.get(e.target))
90
+ .filter((n) => n?.type === 'output-folder' && n.config?.path)
91
+ .map((n) => n.config.path)
92
+
93
+ // If no output configured (and no output-folder node), derive default from the single cutter's input dir
94
+ let effectiveNode = node
95
+ if (!node.config.output && outputFolderPaths.length === 0) {
96
+ const cutters = spec.nodes.filter((n) => n.type === 'video-cutter')
97
+ if (cutters.length === 1 && cutters[0].config.input) {
98
+ const inputDir = path.dirname(cutters[0].config.input)
99
+ effectiveNode = { ...node, config: { ...node.config, output: path.join(inputDir, 'stitch-output') } }
100
+ }
101
+ }
102
+ await handler(effectiveNode, context, tempRoot, incomingEdges, { dryRun, overwrite, outputFolderPaths })
103
+ } else if (node.type === 'video-cutter') {
104
+ // Collect output-folder paths from connected output-folder nodes
105
+ const outputFolderPaths = spec.edges
106
+ .filter((e) => e.source === nodeId)
107
+ .map((e) => nodeMap.get(e.target))
108
+ .filter((n) => n?.type === 'output-folder' && n.config?.path)
109
+ .map((n) => n.config.path)
110
+
111
+ // Collect input files from connected input-file / input-folder nodes
112
+ const inputFiles = spec.edges
113
+ .filter((e) => e.target === nodeId)
114
+ .map((e) => nodeMap.get(e.source))
115
+ .filter((n) => n?.type === 'input-file' || n?.type === 'input-folder')
116
+ .flatMap((n) => context.get(n.id)?.outputs ?? [])
117
+
118
+ await handler(node, context, tempRoot, { dryRun, overwrite, outputFolderPaths, inputFiles })
119
+ } else {
120
+ await handler(node, context, tempRoot, { dryRun, overwrite })
121
+ }
122
+ })
123
+ )
124
+
125
+ await Promise.all(tasks)
126
+ await queue.onIdle()
127
+ }
128
+
129
+ // Print final outputs
130
+ console.log(chalk.green('\n✓ Pipeline complete\n'))
131
+ for (const [nodeId, ctx] of context) {
132
+ if (ctx.outputs?.length) {
133
+ for (const f of ctx.outputs) {
134
+ console.log(chalk.green(` Output [${nodeId}]: ${f}`))
135
+ }
136
+ }
137
+ }
138
+
139
+ if (!dryRun && !keepTemp) {
140
+ try {
141
+ rmSync(tempRoot, { recursive: true, force: true })
142
+ } catch {
143
+ // Non-fatal: temp cleanup failure
144
+ }
145
+ } else if (!dryRun && keepTemp) {
146
+ console.log(chalk.dim(`\n Temp files: ${tempRoot}`))
147
+ }
148
+ }
@@ -0,0 +1,41 @@
1
+ /*
2
+ * Copyright 2026 Shazron Abdullah
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import os from 'os'
18
+ import path from 'path'
19
+
20
+ function expandPath(p) {
21
+ return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : p
22
+ }
23
+
24
+ /**
25
+ * Resolves the configured file path and stores it as a single-element outputs array.
26
+ *
27
+ * @param {object} node - the spec node
28
+ * @param {Map} context - runtime context keyed by nodeId
29
+ * @param {string} _tempRoot - unused
30
+ * @param {object} opts - { dryRun }
31
+ */
32
+ export async function handleInputFile(node, context, _tempRoot, opts = {}) {
33
+ const { config } = node
34
+ const filePath = config.path ? expandPath(config.path) : null
35
+
36
+ if (!filePath && !opts.dryRun) {
37
+ throw new Error(`Node "${node.id}" (input-file): no file path configured`)
38
+ }
39
+
40
+ context.set(node.id, { outputs: filePath ? [filePath] : [] })
41
+ }
@@ -0,0 +1,55 @@
1
+ /*
2
+ * Copyright 2026 Shazron Abdullah
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import os from 'os'
18
+ import path from 'path'
19
+ import { glob } from 'glob'
20
+ import chalk from 'chalk'
21
+
22
+ function expandPath(p) {
23
+ return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : p
24
+ }
25
+
26
+ /**
27
+ * Globs the configured folder using the optional filter pattern and stores
28
+ * all matching file paths as the outputs array.
29
+ *
30
+ * @param {object} node - the spec node
31
+ * @param {Map} context - runtime context keyed by nodeId
32
+ * @param {string} _tempRoot - unused
33
+ * @param {object} opts - { dryRun }
34
+ */
35
+ export async function handleInputFolder(node, context, _tempRoot, opts = {}) {
36
+ const { config } = node
37
+ const folderPath = config.path ? expandPath(config.path) : null
38
+
39
+ if (!folderPath && !opts.dryRun) {
40
+ throw new Error(`Node "${node.id}" (input-folder): no folder path configured`)
41
+ }
42
+
43
+ if (opts.dryRun || !folderPath) {
44
+ context.set(node.id, { outputs: [] })
45
+ return
46
+ }
47
+
48
+ // Use the user-supplied filter as a glob pattern; default to all files (non-recursive top level)
49
+ const pattern = config.filter?.trim() || '*'
50
+ const files = (await glob(pattern, { cwd: folderPath, absolute: true, nodir: true })).sort()
51
+
52
+ console.log(chalk.dim(` Input folder: ${folderPath} — ${files.length} file(s) matched`))
53
+
54
+ context.set(node.id, { outputs: files })
55
+ }