Skip to content

refactor(main): split src/main.sh into a src/main/ module #948

Description

@Chemaclass

Summary

src/main.sh is the last large flat file: 1427 lines, 24 functions. It was carried out of
#931 deliberately — it is the dispatcher and the riskiest file in src/, so it deserves its
own issue rather than being that issue's tail end.

Measured on main (8f2a1c3), after #931 and #940 left src/ at 11 modules + 15 flat files.

What is in it

FunctionLinesConcern
cmd_test485flag parsing for the default subcommand
exec_tests189the run lifecycle: dispatch, aggregate, report, exit code
cmd_bench108bench flag parsing
cmd_doc69doc flag parsing
exec_multi_assert · exec_assert · handle_assert_exit_code · cmd_assert · is_assertion_function · is_exit_code_assertion213the standalone bashunit assert path
watch_loop · watch_get_checksum73the --watch polling loop
validate_config_or_exit · set_shard_or_exit · require_non_negative_int_or_exit · require_writable_path_or_exit · abort_unknown_option135input validation shared by the parsers
cmd_init · cmd_learn · cmd_watch · cmd_upgrade87thin delegators to src/cli/
exec_benchmarks · cleanup · handle_stop_on_failure_sync62run helpers

Proposed shape

Not pre-decided. Produce the responsibility map first, as every module under #931 did — but
the seams that look real from the function list:

  • validation — the five *_or_exit / abort_* helpers, called by every parser
  • the assert subcommand — six functions, 213 lines, entirely self-contained
  • watch loop — two functions, already delegating to src/cli/watch.sh
  • flag parsingcmd_*, which is most of the bulk
  • run lifecycleexec_tests, exec_benchmarks, cleanup, handle_stop_on_failure_sync

Open question worth answering in the map: cmd_doc, cmd_init, cmd_learn, cmd_watch and
cmd_upgrade are flag parsing for subcommands whose implementations already live in
src/cli/. Should each parser move next to its implementation, or stay together as one
parsing layer? Both are defensible; decide deliberately and record it.

Why this one is riskier than the previous seven

  • It is the only caller of src/cli/*, src/learn/, src/benchmark.sh and the runner, so a
    mistake here breaks every subcommand at once rather than one module.
  • refactor(src): group cohesive flat files into modules (assert, console, util) #940 showed subcommand moves interact with build::embed_docs markers and entrypoint
    ordering; cmd_doc sits directly on that path.
  • bashunit sources src/main.shlast, after everything it dispatches to. Any split must
    preserve that position.
  • cmd_test at 485 lines is a single while/case over ~60 flags. Cutting it means cutting
    a case statement, which is the one shape where a mechanical line-range split is unsafe.

Constraints

All of ADR-010's, which by now have all drawn blood at least once:

Verification

Prove it is a relocation: the non-blank line multiset differs only by new shebangs, module
headers and source lines; function count unchanged; the built artifact's sorted code content
identical.

Then: ./bashunit tests/ · --parallel · --parallel --simple --strict · make sa ·
make lint · CI-mode ShellCheck · bash build.sh bin -v printing ✅ Build verified ✅.

Because this file owns dispatch, also smoke-test every subcommand end to end against the
built binary, not just dev mode: test, assert, doc, init, learn, watch,
upgrade, bench.

Acceptance criteria

  • Responsibility map posted here and agreed before any code moves
  • The cmd_*-next-to-implementation question answered explicitly
  • git diff shows only relocations — no renamed functions, no changed logic
  • src/main/index.sh holds only source lines and comments
  • bashunit still sources the module last
  • Every subcommand smoke-tested against the built binary
  • All suites above green, including bash build.sh bin -v
  • .claude/rules/architecture-map.md updated
  • No CHANGELOG entry — internal, no user-visible behaviour change

Do not

  • Do not split cmd_test's case statement across files
  • Do not rename functions
  • Do not move main.sh earlier in the entrypoint's source order
  • Do not run shfmt -w; make lint is the format gate

Metadata

Metadata

Assignees

No one assigned

    Labels

    refactoringRefactoring or cleaning related

    Type

    No type

    Projects

    • Status
      Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
     blocks
    (function() {
    function addCopyButtons() {
    document.querySelectorAll('pre code').forEach(function(codeBlock) {
    if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
    codeBlock.parentElement.setAttribute('data-copy-added', 'true');
    var btn = document.createElement('button');
    btn.textContent = 'Copy';
    btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
    btn.onmouseover = function() { this.style.opacity = '1'; };
    btn.onmouseout = function() { this.style.opacity = '0.7'; };
    btn.onclick = function() {
    navigator.clipboard.writeText(codeBlock.textContent).then(function() {
    btn.textContent = 'Copied!';
    setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
    });
    };
    codeBlock.parentElement.style.position = 'relative';
    codeBlock.parentElement.appendChild(btn);
    });
    }
    addCopyButtons();
    // Re-run on dynamic content
    var observer = new MutationObserver(addCopyButtons);
    observer.observe(document.body, { childList: true, subtree: true });
    })();
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    refactor(main): split src/main.sh into a src/main/ module · Issue #948 · TypedDevs/bashunit · GitHub
    Skip to content

    refactor(main): split src/main.sh into a src/main/ module #948

    Description

    @Chemaclass

    Summary

    src/main.sh is the last large flat file: 1427 lines, 24 functions. It was carried out of
    #931 deliberately — it is the dispatcher and the riskiest file in src/, so it deserves its
    own issue rather than being that issue's tail end.

    Measured on main (8f2a1c3), after #931 and #940 left src/ at 11 modules + 15 flat files.

    What is in it

    FunctionLinesConcern
    cmd_test485flag parsing for the default subcommand
    exec_tests189the run lifecycle: dispatch, aggregate, report, exit code
    cmd_bench108bench flag parsing
    cmd_doc69doc flag parsing
    exec_multi_assert · exec_assert · handle_assert_exit_code · cmd_assert · is_assertion_function · is_exit_code_assertion213the standalone bashunit assert path
    watch_loop · watch_get_checksum73the --watch polling loop
    validate_config_or_exit · set_shard_or_exit · require_non_negative_int_or_exit · require_writable_path_or_exit · abort_unknown_option135input validation shared by the parsers
    cmd_init · cmd_learn · cmd_watch · cmd_upgrade87thin delegators to src/cli/
    exec_benchmarks · cleanup · handle_stop_on_failure_sync62run helpers

    Proposed shape

    Not pre-decided. Produce the responsibility map first, as every module under #931 did — but
    the seams that look real from the function list:

    • validation — the five *_or_exit / abort_* helpers, called by every parser
    • the assert subcommand — six functions, 213 lines, entirely self-contained
    • watch loop — two functions, already delegating to src/cli/watch.sh
    • flag parsingcmd_*, which is most of the bulk
    • run lifecycleexec_tests, exec_benchmarks, cleanup, handle_stop_on_failure_sync

    Open question worth answering in the map: cmd_doc, cmd_init, cmd_learn, cmd_watch and
    cmd_upgrade are flag parsing for subcommands whose implementations already live in
    src/cli/. Should each parser move next to its implementation, or stay together as one
    parsing layer? Both are defensible; decide deliberately and record it.

    Why this one is riskier than the previous seven

    • It is the only caller of src/cli/*, src/learn/, src/benchmark.sh and the runner, so a
      mistake here breaks every subcommand at once rather than one module.
    • refactor(src): group cohesive flat files into modules (assert, console, util) #940 showed subcommand moves interact with build::embed_docs markers and entrypoint
      ordering; cmd_doc sits directly on that path.
    • bashunit sources src/main.shlast, after everything it dispatches to. Any split must
      preserve that position.
    • cmd_test at 485 lines is a single while/case over ~60 flags. Cutting it means cutting
      a case statement, which is the one shape where a mechanical line-range split is unsafe.

    Constraints

    All of ADR-010's, which by now have all drawn blood at least once:

    Verification

    Prove it is a relocation: the non-blank line multiset differs only by new shebangs, module
    headers and source lines; function count unchanged; the built artifact's sorted code content
    identical.

    Then: ./bashunit tests/ · --parallel · --parallel --simple --strict · make sa ·
    make lint · CI-mode ShellCheck · bash build.sh bin -v printing ✅ Build verified ✅.

    Because this file owns dispatch, also smoke-test every subcommand end to end against the
    built binary, not just dev mode: test, assert, doc, init, learn, watch,
    upgrade, bench.

    Acceptance criteria

    • Responsibility map posted here and agreed before any code moves
    • The cmd_*-next-to-implementation question answered explicitly
    • git diff shows only relocations — no renamed functions, no changed logic
    • src/main/index.sh holds only source lines and comments
    • bashunit still sources the module last
    • Every subcommand smoke-tested against the built binary
    • All suites above green, including bash build.sh bin -v
    • .claude/rules/architecture-map.md updated
    • No CHANGELOG entry — internal, no user-visible behaviour change

    Do not

    • Do not split cmd_test's case statement across files
    • Do not rename functions
    • Do not move main.sh earlier in the entrypoint's source order
    • Do not run shfmt -w; make lint is the format gate

    Metadata

    Metadata

    Assignees

    No one assigned

      Labels

      refactoringRefactoring or cleaning related

      Type

      No type

      Projects

      • Status
        Done

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' refactor(main): split src/main.sh into a src/main/ module · Issue #948 · TypedDevs/bashunit · GitHub
      Skip to content

      refactor(main): split src/main.sh into a src/main/ module #948

      Description

      @Chemaclass

      Summary

      src/main.sh is the last large flat file: 1427 lines, 24 functions. It was carried out of
      #931 deliberately — it is the dispatcher and the riskiest file in src/, so it deserves its
      own issue rather than being that issue's tail end.

      Measured on main (8f2a1c3), after #931 and #940 left src/ at 11 modules + 15 flat files.

      What is in it

      FunctionLinesConcern
      cmd_test485flag parsing for the default subcommand
      exec_tests189the run lifecycle: dispatch, aggregate, report, exit code
      cmd_bench108bench flag parsing
      cmd_doc69doc flag parsing
      exec_multi_assert · exec_assert · handle_assert_exit_code · cmd_assert · is_assertion_function · is_exit_code_assertion213the standalone bashunit assert path
      watch_loop · watch_get_checksum73the --watch polling loop
      validate_config_or_exit · set_shard_or_exit · require_non_negative_int_or_exit · require_writable_path_or_exit · abort_unknown_option135input validation shared by the parsers
      cmd_init · cmd_learn · cmd_watch · cmd_upgrade87thin delegators to src/cli/
      exec_benchmarks · cleanup · handle_stop_on_failure_sync62run helpers

      Proposed shape

      Not pre-decided. Produce the responsibility map first, as every module under #931 did — but
      the seams that look real from the function list:

      • validation — the five *_or_exit / abort_* helpers, called by every parser
      • the assert subcommand — six functions, 213 lines, entirely self-contained
      • watch loop — two functions, already delegating to src/cli/watch.sh
      • flag parsingcmd_*, which is most of the bulk
      • run lifecycleexec_tests, exec_benchmarks, cleanup, handle_stop_on_failure_sync

      Open question worth answering in the map: cmd_doc, cmd_init, cmd_learn, cmd_watch and
      cmd_upgrade are flag parsing for subcommands whose implementations already live in
      src/cli/. Should each parser move next to its implementation, or stay together as one
      parsing layer? Both are defensible; decide deliberately and record it.

      Why this one is riskier than the previous seven

      • It is the only caller of src/cli/*, src/learn/, src/benchmark.sh and the runner, so a
        mistake here breaks every subcommand at once rather than one module.
      • refactor(src): group cohesive flat files into modules (assert, console, util) #940 showed subcommand moves interact with build::embed_docs markers and entrypoint
        ordering; cmd_doc sits directly on that path.
      • bashunit sources src/main.shlast, after everything it dispatches to. Any split must
        preserve that position.
      • cmd_test at 485 lines is a single while/case over ~60 flags. Cutting it means cutting
        a case statement, which is the one shape where a mechanical line-range split is unsafe.

      Constraints

      All of ADR-010's, which by now have all drawn blood at least once:

      Verification

      Prove it is a relocation: the non-blank line multiset differs only by new shebangs, module
      headers and source lines; function count unchanged; the built artifact's sorted code content
      identical.

      Then: ./bashunit tests/ · --parallel · --parallel --simple --strict · make sa ·
      make lint · CI-mode ShellCheck · bash build.sh bin -v printing ✅ Build verified ✅.

      Because this file owns dispatch, also smoke-test every subcommand end to end against the
      built binary, not just dev mode: test, assert, doc, init, learn, watch,
      upgrade, bench.

      Acceptance criteria

      • Responsibility map posted here and agreed before any code moves
      • The cmd_*-next-to-implementation question answered explicitly
      • git diff shows only relocations — no renamed functions, no changed logic
      • src/main/index.sh holds only source lines and comments
      • bashunit still sources the module last
      • Every subcommand smoke-tested against the built binary
      • All suites above green, including bash build.sh bin -v
      • .claude/rules/architecture-map.md updated
      • No CHANGELOG entry — internal, no user-visible behaviour change

      Do not

      • Do not split cmd_test's case statement across files
      • Do not rename functions
      • Do not move main.sh earlier in the entrypoint's source order
      • Do not run shfmt -w; make lint is the format gate

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        refactoringRefactoring or cleaning related

        Type

        No type

        Projects

        • Status
          Done

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' refactor(main): split src/main.sh into a src/main/ module · Issue #948 · TypedDevs/bashunit · GitHub
        Skip to content

        refactor(main): split src/main.sh into a src/main/ module #948

        Description

        @Chemaclass

        Summary

        src/main.sh is the last large flat file: 1427 lines, 24 functions. It was carried out of
        #931 deliberately — it is the dispatcher and the riskiest file in src/, so it deserves its
        own issue rather than being that issue's tail end.

        Measured on main (8f2a1c3), after #931 and #940 left src/ at 11 modules + 15 flat files.

        What is in it

        FunctionLinesConcern
        cmd_test485flag parsing for the default subcommand
        exec_tests189the run lifecycle: dispatch, aggregate, report, exit code
        cmd_bench108bench flag parsing
        cmd_doc69doc flag parsing
        exec_multi_assert · exec_assert · handle_assert_exit_code · cmd_assert · is_assertion_function · is_exit_code_assertion213the standalone bashunit assert path
        watch_loop · watch_get_checksum73the --watch polling loop
        validate_config_or_exit · set_shard_or_exit · require_non_negative_int_or_exit · require_writable_path_or_exit · abort_unknown_option135input validation shared by the parsers
        cmd_init · cmd_learn · cmd_watch · cmd_upgrade87thin delegators to src/cli/
        exec_benchmarks · cleanup · handle_stop_on_failure_sync62run helpers

        Proposed shape

        Not pre-decided. Produce the responsibility map first, as every module under #931 did — but
        the seams that look real from the function list:

        • validation — the five *_or_exit / abort_* helpers, called by every parser
        • the assert subcommand — six functions, 213 lines, entirely self-contained
        • watch loop — two functions, already delegating to src/cli/watch.sh
        • flag parsingcmd_*, which is most of the bulk
        • run lifecycleexec_tests, exec_benchmarks, cleanup, handle_stop_on_failure_sync

        Open question worth answering in the map: cmd_doc, cmd_init, cmd_learn, cmd_watch and
        cmd_upgrade are flag parsing for subcommands whose implementations already live in
        src/cli/. Should each parser move next to its implementation, or stay together as one
        parsing layer? Both are defensible; decide deliberately and record it.

        Why this one is riskier than the previous seven

        • It is the only caller of src/cli/*, src/learn/, src/benchmark.sh and the runner, so a
          mistake here breaks every subcommand at once rather than one module.
        • refactor(src): group cohesive flat files into modules (assert, console, util) #940 showed subcommand moves interact with build::embed_docs markers and entrypoint
          ordering; cmd_doc sits directly on that path.
        • bashunit sources src/main.shlast, after everything it dispatches to. Any split must
          preserve that position.
        • cmd_test at 485 lines is a single while/case over ~60 flags. Cutting it means cutting
          a case statement, which is the one shape where a mechanical line-range split is unsafe.

        Constraints

        All of ADR-010's, which by now have all drawn blood at least once:

        Verification

        Prove it is a relocation: the non-blank line multiset differs only by new shebangs, module
        headers and source lines; function count unchanged; the built artifact's sorted code content
        identical.

        Then: ./bashunit tests/ · --parallel · --parallel --simple --strict · make sa ·
        make lint · CI-mode ShellCheck · bash build.sh bin -v printing ✅ Build verified ✅.

        Because this file owns dispatch, also smoke-test every subcommand end to end against the
        built binary, not just dev mode: test, assert, doc, init, learn, watch,
        upgrade, bench.

        Acceptance criteria

        • Responsibility map posted here and agreed before any code moves
        • The cmd_*-next-to-implementation question answered explicitly
        • git diff shows only relocations — no renamed functions, no changed logic
        • src/main/index.sh holds only source lines and comments
        • bashunit still sources the module last
        • Every subcommand smoke-tested against the built binary
        • All suites above green, including bash build.sh bin -v
        • .claude/rules/architecture-map.md updated
        • No CHANGELOG entry — internal, no user-visible behaviour change

        Do not

        • Do not split cmd_test's case statement across files
        • Do not rename functions
        • Do not move main.sh earlier in the entrypoint's source order
        • Do not run shfmt -w; make lint is the format gate

        Metadata

        Metadata

        Assignees

        No one assigned

          Labels

          refactoringRefactoring or cleaning related

          Type

          No type

          Projects

          • Status
            Done

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' refactor(main): split src/main.sh into a src/main/ module · Issue #948 · TypedDevs/bashunit · GitHub
          Skip to content

          refactor(main): split src/main.sh into a src/main/ module #948

          Description

          @Chemaclass

          Summary

          src/main.sh is the last large flat file: 1427 lines, 24 functions. It was carried out of
          #931 deliberately — it is the dispatcher and the riskiest file in src/, so it deserves its
          own issue rather than being that issue's tail end.

          Measured on main (8f2a1c3), after #931 and #940 left src/ at 11 modules + 15 flat files.

          What is in it

          FunctionLinesConcern
          cmd_test485flag parsing for the default subcommand
          exec_tests189the run lifecycle: dispatch, aggregate, report, exit code
          cmd_bench108bench flag parsing
          cmd_doc69doc flag parsing
          exec_multi_assert · exec_assert · handle_assert_exit_code · cmd_assert · is_assertion_function · is_exit_code_assertion213the standalone bashunit assert path
          watch_loop · watch_get_checksum73the --watch polling loop
          validate_config_or_exit · set_shard_or_exit · require_non_negative_int_or_exit · require_writable_path_or_exit · abort_unknown_option135input validation shared by the parsers
          cmd_init · cmd_learn · cmd_watch · cmd_upgrade87thin delegators to src/cli/
          exec_benchmarks · cleanup · handle_stop_on_failure_sync62run helpers

          Proposed shape

          Not pre-decided. Produce the responsibility map first, as every module under #931 did — but
          the seams that look real from the function list:

          • validation — the five *_or_exit / abort_* helpers, called by every parser
          • the assert subcommand — six functions, 213 lines, entirely self-contained
          • watch loop — two functions, already delegating to src/cli/watch.sh
          • flag parsingcmd_*, which is most of the bulk
          • run lifecycleexec_tests, exec_benchmarks, cleanup, handle_stop_on_failure_sync

          Open question worth answering in the map: cmd_doc, cmd_init, cmd_learn, cmd_watch and
          cmd_upgrade are flag parsing for subcommands whose implementations already live in
          src/cli/. Should each parser move next to its implementation, or stay together as one
          parsing layer? Both are defensible; decide deliberately and record it.

          Why this one is riskier than the previous seven

          • It is the only caller of src/cli/*, src/learn/, src/benchmark.sh and the runner, so a
            mistake here breaks every subcommand at once rather than one module.
          • refactor(src): group cohesive flat files into modules (assert, console, util) #940 showed subcommand moves interact with build::embed_docs markers and entrypoint
            ordering; cmd_doc sits directly on that path.
          • bashunit sources src/main.shlast, after everything it dispatches to. Any split must
            preserve that position.
          • cmd_test at 485 lines is a single while/case over ~60 flags. Cutting it means cutting
            a case statement, which is the one shape where a mechanical line-range split is unsafe.

          Constraints

          All of ADR-010's, which by now have all drawn blood at least once:

          Verification

          Prove it is a relocation: the non-blank line multiset differs only by new shebangs, module
          headers and source lines; function count unchanged; the built artifact's sorted code content
          identical.

          Then: ./bashunit tests/ · --parallel · --parallel --simple --strict · make sa ·
          make lint · CI-mode ShellCheck · bash build.sh bin -v printing ✅ Build verified ✅.

          Because this file owns dispatch, also smoke-test every subcommand end to end against the
          built binary, not just dev mode: test, assert, doc, init, learn, watch,
          upgrade, bench.

          Acceptance criteria

          • Responsibility map posted here and agreed before any code moves
          • The cmd_*-next-to-implementation question answered explicitly
          • git diff shows only relocations — no renamed functions, no changed logic
          • src/main/index.sh holds only source lines and comments
          • bashunit still sources the module last
          • Every subcommand smoke-tested against the built binary
          • All suites above green, including bash build.sh bin -v
          • .claude/rules/architecture-map.md updated
          • No CHANGELOG entry — internal, no user-visible behaviour change

          Do not

          • Do not split cmd_test's case statement across files
          • Do not rename functions
          • Do not move main.sh earlier in the entrypoint's source order
          • Do not run shfmt -w; make lint is the format gate

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            refactoringRefactoring or cleaning related

            Type

            No type

            Projects

            • Status
              Done

            Milestone

            No milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' refactor(main): split src/main.sh into a src/main/ module · Issue #948 · TypedDevs/bashunit · GitHub
            Skip to content

            refactor(main): split src/main.sh into a src/main/ module #948

            Description

            @Chemaclass

            Summary

            src/main.sh is the last large flat file: 1427 lines, 24 functions. It was carried out of
            #931 deliberately — it is the dispatcher and the riskiest file in src/, so it deserves its
            own issue rather than being that issue's tail end.

            Measured on main (8f2a1c3), after #931 and #940 left src/ at 11 modules + 15 flat files.

            What is in it

            FunctionLinesConcern
            cmd_test485flag parsing for the default subcommand
            exec_tests189the run lifecycle: dispatch, aggregate, report, exit code
            cmd_bench108bench flag parsing
            cmd_doc69doc flag parsing
            exec_multi_assert · exec_assert · handle_assert_exit_code · cmd_assert · is_assertion_function · is_exit_code_assertion213the standalone bashunit assert path
            watch_loop · watch_get_checksum73the --watch polling loop
            validate_config_or_exit · set_shard_or_exit · require_non_negative_int_or_exit · require_writable_path_or_exit · abort_unknown_option135input validation shared by the parsers
            cmd_init · cmd_learn · cmd_watch · cmd_upgrade87thin delegators to src/cli/
            exec_benchmarks · cleanup · handle_stop_on_failure_sync62run helpers

            Proposed shape

            Not pre-decided. Produce the responsibility map first, as every module under #931 did — but
            the seams that look real from the function list:

            • validation — the five *_or_exit / abort_* helpers, called by every parser
            • the assert subcommand — six functions, 213 lines, entirely self-contained
            • watch loop — two functions, already delegating to src/cli/watch.sh
            • flag parsingcmd_*, which is most of the bulk
            • run lifecycleexec_tests, exec_benchmarks, cleanup, handle_stop_on_failure_sync

            Open question worth answering in the map: cmd_doc, cmd_init, cmd_learn, cmd_watch and
            cmd_upgrade are flag parsing for subcommands whose implementations already live in
            src/cli/. Should each parser move next to its implementation, or stay together as one
            parsing layer? Both are defensible; decide deliberately and record it.

            Why this one is riskier than the previous seven

            • It is the only caller of src/cli/*, src/learn/, src/benchmark.sh and the runner, so a
              mistake here breaks every subcommand at once rather than one module.
            • refactor(src): group cohesive flat files into modules (assert, console, util) #940 showed subcommand moves interact with build::embed_docs markers and entrypoint
              ordering; cmd_doc sits directly on that path.
            • bashunit sources src/main.shlast, after everything it dispatches to. Any split must
              preserve that position.
            • cmd_test at 485 lines is a single while/case over ~60 flags. Cutting it means cutting
              a case statement, which is the one shape where a mechanical line-range split is unsafe.

            Constraints

            All of ADR-010's, which by now have all drawn blood at least once:

            Verification

            Prove it is a relocation: the non-blank line multiset differs only by new shebangs, module
            headers and source lines; function count unchanged; the built artifact's sorted code content
            identical.

            Then: ./bashunit tests/ · --parallel · --parallel --simple --strict · make sa ·
            make lint · CI-mode ShellCheck · bash build.sh bin -v printing ✅ Build verified ✅.

            Because this file owns dispatch, also smoke-test every subcommand end to end against the
            built binary, not just dev mode: test, assert, doc, init, learn, watch,
            upgrade, bench.

            Acceptance criteria

            • Responsibility map posted here and agreed before any code moves
            • The cmd_*-next-to-implementation question answered explicitly
            • git diff shows only relocations — no renamed functions, no changed logic
            • src/main/index.sh holds only source lines and comments
            • bashunit still sources the module last
            • Every subcommand smoke-tested against the built binary
            • All suites above green, including bash build.sh bin -v
            • .claude/rules/architecture-map.md updated
            • No CHANGELOG entry — internal, no user-visible behaviour change

            Do not

            • Do not split cmd_test's case statement across files
            • Do not rename functions
            • Do not move main.sh earlier in the entrypoint's source order
            • Do not run shfmt -w; make lint is the format gate

            Metadata

            Metadata

            Assignees

            No one assigned

              Labels

              refactoringRefactoring or cleaning related

              Type

              No type

              Projects

              • Status
                Done

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' refactor(main): split src/main.sh into a src/main/ module · Issue #948 · TypedDevs/bashunit · GitHub
              Skip to content

              refactor(main): split src/main.sh into a src/main/ module #948

              Description

              @Chemaclass

              Summary

              src/main.sh is the last large flat file: 1427 lines, 24 functions. It was carried out of
              #931 deliberately — it is the dispatcher and the riskiest file in src/, so it deserves its
              own issue rather than being that issue's tail end.

              Measured on main (8f2a1c3), after #931 and #940 left src/ at 11 modules + 15 flat files.

              What is in it

              FunctionLinesConcern
              cmd_test485flag parsing for the default subcommand
              exec_tests189the run lifecycle: dispatch, aggregate, report, exit code
              cmd_bench108bench flag parsing
              cmd_doc69doc flag parsing
              exec_multi_assert · exec_assert · handle_assert_exit_code · cmd_assert · is_assertion_function · is_exit_code_assertion213the standalone bashunit assert path
              watch_loop · watch_get_checksum73the --watch polling loop
              validate_config_or_exit · set_shard_or_exit · require_non_negative_int_or_exit · require_writable_path_or_exit · abort_unknown_option135input validation shared by the parsers
              cmd_init · cmd_learn · cmd_watch · cmd_upgrade87thin delegators to src/cli/
              exec_benchmarks · cleanup · handle_stop_on_failure_sync62run helpers

              Proposed shape

              Not pre-decided. Produce the responsibility map first, as every module under #931 did — but
              the seams that look real from the function list:

              • validation — the five *_or_exit / abort_* helpers, called by every parser
              • the assert subcommand — six functions, 213 lines, entirely self-contained
              • watch loop — two functions, already delegating to src/cli/watch.sh
              • flag parsingcmd_*, which is most of the bulk
              • run lifecycleexec_tests, exec_benchmarks, cleanup, handle_stop_on_failure_sync

              Open question worth answering in the map: cmd_doc, cmd_init, cmd_learn, cmd_watch and
              cmd_upgrade are flag parsing for subcommands whose implementations already live in
              src/cli/. Should each parser move next to its implementation, or stay together as one
              parsing layer? Both are defensible; decide deliberately and record it.

              Why this one is riskier than the previous seven

              • It is the only caller of src/cli/*, src/learn/, src/benchmark.sh and the runner, so a
                mistake here breaks every subcommand at once rather than one module.
              • refactor(src): group cohesive flat files into modules (assert, console, util) #940 showed subcommand moves interact with build::embed_docs markers and entrypoint
                ordering; cmd_doc sits directly on that path.
              • bashunit sources src/main.shlast, after everything it dispatches to. Any split must
                preserve that position.
              • cmd_test at 485 lines is a single while/case over ~60 flags. Cutting it means cutting
                a case statement, which is the one shape where a mechanical line-range split is unsafe.

              Constraints

              All of ADR-010's, which by now have all drawn blood at least once:

              Verification

              Prove it is a relocation: the non-blank line multiset differs only by new shebangs, module
              headers and source lines; function count unchanged; the built artifact's sorted code content
              identical.

              Then: ./bashunit tests/ · --parallel · --parallel --simple --strict · make sa ·
              make lint · CI-mode ShellCheck · bash build.sh bin -v printing ✅ Build verified ✅.

              Because this file owns dispatch, also smoke-test every subcommand end to end against the
              built binary, not just dev mode: test, assert, doc, init, learn, watch,
              upgrade, bench.

              Acceptance criteria

              • Responsibility map posted here and agreed before any code moves
              • The cmd_*-next-to-implementation question answered explicitly
              • git diff shows only relocations — no renamed functions, no changed logic
              • src/main/index.sh holds only source lines and comments
              • bashunit still sources the module last
              • Every subcommand smoke-tested against the built binary
              • All suites above green, including bash build.sh bin -v
              • .claude/rules/architecture-map.md updated
              • No CHANGELOG entry — internal, no user-visible behaviour change

              Do not

              • Do not split cmd_test's case statement across files
              • Do not rename functions
              • Do not move main.sh earlier in the entrypoint's source order
              • Do not run shfmt -w; make lint is the format gate

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                refactoringRefactoring or cleaning related

                Type

                No type

                Projects

                • Status
                  Done

                Milestone

                No milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); refactor(main): split src/main.sh into a src/main/ module · Issue #948 · TypedDevs/bashunit · GitHub
                Skip to content

                refactor(main): split src/main.sh into a src/main/ module #948

                Description

                @Chemaclass

                Summary

                src/main.sh is the last large flat file: 1427 lines, 24 functions. It was carried out of
                #931 deliberately — it is the dispatcher and the riskiest file in src/, so it deserves its
                own issue rather than being that issue's tail end.

                Measured on main (8f2a1c3), after #931 and #940 left src/ at 11 modules + 15 flat files.

                What is in it

                FunctionLinesConcern
                cmd_test485flag parsing for the default subcommand
                exec_tests189the run lifecycle: dispatch, aggregate, report, exit code
                cmd_bench108bench flag parsing
                cmd_doc69doc flag parsing
                exec_multi_assert · exec_assert · handle_assert_exit_code · cmd_assert · is_assertion_function · is_exit_code_assertion213the standalone bashunit assert path
                watch_loop · watch_get_checksum73the --watch polling loop
                validate_config_or_exit · set_shard_or_exit · require_non_negative_int_or_exit · require_writable_path_or_exit · abort_unknown_option135input validation shared by the parsers
                cmd_init · cmd_learn · cmd_watch · cmd_upgrade87thin delegators to src/cli/
                exec_benchmarks · cleanup · handle_stop_on_failure_sync62run helpers

                Proposed shape

                Not pre-decided. Produce the responsibility map first, as every module under #931 did — but
                the seams that look real from the function list:

                • validation — the five *_or_exit / abort_* helpers, called by every parser
                • the assert subcommand — six functions, 213 lines, entirely self-contained
                • watch loop — two functions, already delegating to src/cli/watch.sh
                • flag parsingcmd_*, which is most of the bulk
                • run lifecycleexec_tests, exec_benchmarks, cleanup, handle_stop_on_failure_sync

                Open question worth answering in the map: cmd_doc, cmd_init, cmd_learn, cmd_watch and
                cmd_upgrade are flag parsing for subcommands whose implementations already live in
                src/cli/. Should each parser move next to its implementation, or stay together as one
                parsing layer? Both are defensible; decide deliberately and record it.

                Why this one is riskier than the previous seven

                • It is the only caller of src/cli/*, src/learn/, src/benchmark.sh and the runner, so a
                  mistake here breaks every subcommand at once rather than one module.
                • refactor(src): group cohesive flat files into modules (assert, console, util) #940 showed subcommand moves interact with build::embed_docs markers and entrypoint
                  ordering; cmd_doc sits directly on that path.
                • bashunit sources src/main.shlast, after everything it dispatches to. Any split must
                  preserve that position.
                • cmd_test at 485 lines is a single while/case over ~60 flags. Cutting it means cutting
                  a case statement, which is the one shape where a mechanical line-range split is unsafe.

                Constraints

                All of ADR-010's, which by now have all drawn blood at least once:

                Verification

                Prove it is a relocation: the non-blank line multiset differs only by new shebangs, module
                headers and source lines; function count unchanged; the built artifact's sorted code content
                identical.

                Then: ./bashunit tests/ · --parallel · --parallel --simple --strict · make sa ·
                make lint · CI-mode ShellCheck · bash build.sh bin -v printing ✅ Build verified ✅.

                Because this file owns dispatch, also smoke-test every subcommand end to end against the
                built binary, not just dev mode: test, assert, doc, init, learn, watch,
                upgrade, bench.

                Acceptance criteria

                • Responsibility map posted here and agreed before any code moves
                • The cmd_*-next-to-implementation question answered explicitly
                • git diff shows only relocations — no renamed functions, no changed logic
                • src/main/index.sh holds only source lines and comments
                • bashunit still sources the module last
                • Every subcommand smoke-tested against the built binary
                • All suites above green, including bash build.sh bin -v
                • .claude/rules/architecture-map.md updated
                • No CHANGELOG entry — internal, no user-visible behaviour change

                Do not

                • Do not split cmd_test's case statement across files
                • Do not rename functions
                • Do not move main.sh earlier in the entrypoint's source order
                • Do not run shfmt -w; make lint is the format gate

                Metadata

                Metadata

                Assignees

                No one assigned

                  Labels

                  refactoringRefactoring or cleaning related

                  Type

                  No type

                  Projects

                  • Status
                    Done

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions