diff --git a/internal/cli/commands.go b/internal/cli/commands.go index e2b5163..70a16c2 100644 --- a/internal/cli/commands.go +++ b/internal/cli/commands.go @@ -83,7 +83,6 @@ type InitCmd struct { type BuildCmd struct { Args struct{ Names []string } `positional-args:"true"` NoCache bool `long:"no-cache" description:"Clear build cache and rebuild from scratch"` - Yes bool `short:"y" long:"yes" description:"Skip upload size confirmation prompt"` UI *UI `no-flag:"true"` } @@ -516,12 +515,12 @@ func (cmd *BuildCmd) Execute(args []string) error { if ui == nil { ui = defaultUI() } - return RunBuild(dir, cmd.Args.Names, cmd.NoCache, cmd.Yes, ui) + return RunBuild(dir, cmd.Args.Names, cmd.NoCache, ui) } var errInitDeclined = errors.New("no project config found; run `tx init` to set up your project") -func runBuild(dir string, names []string, noCache bool, skipConfirm bool, ui *UI) error { +func runBuild(dir string, names []string, noCache bool, ui *UI) error { buildStart := time.Now() configPath := filepath.Join(dir, ".texops.yaml") @@ -661,7 +660,7 @@ func runBuild(dir string, names []string, noCache bool, skipConfirm bool, ui *UI continue } - if err := handleUpload(ui, inst, projectID, dir, files, syncResult, skipConfirm, sp); err != nil { + if err := handleUpload(ui, inst, projectID, dir, files, syncResult, sp); err != nil { // User cancellation should abort the entire build if strings.Contains(err.Error(), "cancelled by user") { return err @@ -711,7 +710,7 @@ func runBuild(dir string, names []string, noCache bool, skipConfirm bool, ui *UI } // handleUpload processes file sync results and uploads missing files. -func handleUpload(ui *UI, inst *InstanceClient, projectID, dir string, files []FileEntry, syncResult SyncResult, skipConfirm bool, sp *Spinner) error { +func handleUpload(ui *UI, inst *InstanceClient, projectID, dir string, files []FileEntry, syncResult SyncResult, sp *Spinner) error { if len(syncResult.Missing) > 0 { knownPaths := make(map[string]bool) filesByPath := make(map[string]FileEntry) @@ -739,17 +738,6 @@ func handleUpload(ui *UI, inst *InstanceClient, projectID, dir string, files []F sp.Stop(fmt.Sprintf("%d files to upload (%s)", len(validMissing), FormatSize(uploadSize))) - const sizeThreshold = 50_000_000 - if uploadSize > sizeThreshold && !skipConfirm { - confirmed, err := ui.Confirm(fmt.Sprintf("Upload size is %s. Continue?", FormatSize(uploadSize))) - if err != nil { - return err - } - if !confirmed { - return fmt.Errorf("upload cancelled by user") - } - } - uploadLabel := fmt.Sprintf("Uploading %d files", len(validMissing)) pb := ui.Progress(uploadLabel, uploadSize) if err := inst.Upload(projectID, dir, validMissing, func(sent, total int64) { diff --git a/internal/cli/commands_test.go b/internal/cli/commands_test.go index db9632b..ea31509 100644 --- a/internal/cli/commands_test.go +++ b/internal/cli/commands_test.go @@ -721,7 +721,7 @@ documents: os.WriteFile(filepath.Join(dir, "paper.tex"), []byte("\\documentclass{article}\\begin{document}Hello\\end{document}"), 0o600) ui, buf := testUI() - err := cli.RunBuild(dir, nil, false, false, ui) + err := cli.RunBuild(dir, nil, false, ui) require.NoError(t, err) written, err := os.ReadFile(filepath.Join(dir, "paper.pdf")) @@ -812,7 +812,7 @@ documents: os.WriteFile(filepath.Join(dir, "paper.tex"), []byte("\\documentclass{article}\\begin{document}Hello\\end{document}"), 0o600) ui, buf := testUI() - err := cli.RunBuild(dir, nil, false, false, ui) + err := cli.RunBuild(dir, nil, false, ui) require.NoError(t, err) assert.Equal(t, "prj_auto123", createdProjectID) @@ -855,7 +855,7 @@ documents: t.Setenv("TX_API_URL", apiSrv.URL) ui, _ := testUI() - err := cli.RunBuild(dir, nil, false, false, ui) + err := cli.RunBuild(dir, nil, false, ui) // RunBuild will error after project creation (mock only handles /api/projects), // but the project_key generation side effect should have completed. require.Error(t, err) @@ -876,7 +876,7 @@ func TestBuildCmd_AutoInit(t *testing.T) { os.WriteFile(filepath.Join(dir, "paper.tex"), []byte(`\documentclass{article}\begin{document}Hello\end{document}`), 0o600) ui, _ := testUI() - err := cli.RunBuild(dir, nil, false, false, ui) + err := cli.RunBuild(dir, nil, false, ui) require.Error(t, err) assert.Contains(t, err.Error(), "run `tx init` to set up your project") }) @@ -891,7 +891,7 @@ func TestBuildCmd_AutoInit(t *testing.T) { ui := cli.NewUIWithTTYOptions(buf, true, false, in) // Build will init then fail on auth — that's fine, we just check init happened. - err := cli.RunBuild(dir, nil, false, false, ui) + err := cli.RunBuild(dir, nil, false, ui) configData, readErr := os.ReadFile(filepath.Join(dir, ".texops.yaml")) require.NoError(t, readErr) @@ -911,7 +911,7 @@ func TestBuildCmd_AutoInit(t *testing.T) { in := strings.NewReader("n\n") ui := cli.NewUIWithOptions(buf, true, in) - err := cli.RunBuild(dir, nil, false, false, ui) + err := cli.RunBuild(dir, nil, false, ui) require.Error(t, err) assert.Contains(t, err.Error(), "run `tx init` to set up your project") @@ -1004,7 +1004,7 @@ documents: os.WriteFile(filepath.Join(dir, "paper.tex"), []byte("\\documentclass{article}\\begin{document}Hello\\end{document}"), 0o600) ui, _ := testUI() - err := cli.RunBuild(dir, nil, true, false, ui) + err := cli.RunBuild(dir, nil, true, ui) require.NoError(t, err) require.NotNil(t, receivedBuildOptions, "build_options should be sent in request") assert.Equal(t, "true", receivedBuildOptions["no_cache"]) @@ -1089,156 +1089,13 @@ documents: os.WriteFile(filepath.Join(dir, "paper.tex"), []byte("\\documentclass{article}\\begin{document}Hello\\end{document}"), 0o600) ui, _ := testUI() - err := cli.RunBuild(dir, nil, false, false, ui) + err := cli.RunBuild(dir, nil, false, ui) require.NoError(t, err) _, hasBuildOptions := receivedBody["build_options"] assert.False(t, hasBuildOptions, "build_options should not be sent when --no-cache is not set") }) } -func TestBuildCmd_SizeConfirmation(t *testing.T) { - // Helper to set up a build environment with files that total the given size. - // The instance server reports all files as missing to trigger upload. - setupBuild := func(t *testing.T, fileSize int) string { - t.Helper() - - pdfContent := []byte("%PDF-1.4 test") - - instSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.URL.Path == "/projects/prj_conf/sync" && r.Method == "POST": - // Report main.tex as missing to trigger upload - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]any{ - "missing": []string{"main.tex"}, - }) - - case r.URL.Path == "/projects/prj_conf/upload" && r.Method == "POST": - w.WriteHeader(http.StatusOK) - - case r.URL.Path == "/projects/prj_conf/build" && r.Method == "POST": - doneData, _ := json.Marshal(map[string]any{ - "status": "success", - "pdfUrl": "/projects/prj_conf/builds/bld_001/output", - "build_id": "bld_001", - }) - w.Header().Set("Content-Type", "text/event-stream") - w.Write([]byte("event: done\ndata: " + string(doneData) + "\n\n")) - - case r.URL.Path == "/projects/prj_conf/builds/bld_001/output" && r.Method == "GET": - w.Header().Set("Content-Type", "application/pdf") - w.Write(pdfContent) - - default: - w.WriteHeader(404) - } - })) - t.Cleanup(instSrv.Close) - - apiSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.URL.Path == "/api/projects" && r.Method == "POST": - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]string{ - "id": "prj_conf", - "name": "test", - "distribution_version": "texlive:2021", - }) - case r.URL.Path == "/api/projects/prj_conf/session" && r.Method == "POST": - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]any{ - "instance_url": instSrv.URL, - "jwt": "test-jwt", - "cache_cold": false, - }) - default: - w.WriteHeader(404) - } - })) - t.Cleanup(apiSrv.Close) - - mockKeyringForAuth(t, "test-jwt-token") - - t.Setenv("TX_API_URL", apiSrv.URL) - - origNewIC := cli.NewInstanceClientFn - cli.NewInstanceClientFn = func(instanceURL, jwt string) *cli.InstanceClient { - ic := cli.NewInstanceClient(instanceURL, jwt) - ic.SetHTTPClient(instSrv.Client()) - return ic - } - t.Cleanup(func() { cli.NewInstanceClientFn = origNewIC }) - - dir := t.TempDir() - configContent := `project_key: "k7Gx9mR2pL4wN8qY5vBt3a" -texlive: "texlive:2021" -documents: - - name: main - main: main.tex -` - os.WriteFile(filepath.Join(dir, ".texops.yaml"), []byte(configContent), 0o600) - - // Create a file of the specified size - content := make([]byte, fileSize) - for i := range content { - content[i] = 'x' - } - os.WriteFile(filepath.Join(dir, "main.tex"), content, 0o600) - - return dir - } - - t.Run("large upload prompts confirmation and user accepts", func(t *testing.T) { - dir := setupBuild(t, 51_000_000) // 51 MB, over threshold - - buf := &bytes.Buffer{} - ui := cli.NewUIWithOptions(buf, true, strings.NewReader("y\n")) - err := cli.RunBuild(dir, nil, false, false, ui) - require.NoError(t, err) - assert.Contains(t, buf.String(), "Upload size") - assert.Contains(t, buf.String(), "[Y/n]") - }) - - t.Run("large upload prompts confirmation and user declines", func(t *testing.T) { - dir := setupBuild(t, 51_000_000) // 51 MB, over threshold - - buf := &bytes.Buffer{} - ui := cli.NewUIWithOptions(buf, true, strings.NewReader("n\n")) - err := cli.RunBuild(dir, nil, false, false, ui) - require.Error(t, err) - assert.Contains(t, err.Error(), "upload cancelled by user") - }) - - t.Run("large upload with --yes skips confirmation", func(t *testing.T) { - dir := setupBuild(t, 51_000_000) // 51 MB, over threshold - - ui, buf := testUI() - err := cli.RunBuild(dir, nil, false, true, ui) // skipConfirm=true - require.NoError(t, err) - assert.NotContains(t, buf.String(), "[Y/n]") - }) - - t.Run("small upload does not prompt confirmation", func(t *testing.T) { - dir := setupBuild(t, 1_000) // 1 KB, under threshold - - ui, buf := testUI() - err := cli.RunBuild(dir, nil, false, false, ui) - require.NoError(t, err) - assert.NotContains(t, buf.String(), "[Y/n]") - }) - - t.Run("large upload auto-confirms in non-TTY mode", func(t *testing.T) { - dir := setupBuild(t, 51_000_000) // 51 MB, over threshold - - ui, buf := testUI() // non-TTY - err := cli.RunBuild(dir, nil, false, false, ui) - require.NoError(t, err) - // Non-TTY should auto-confirm without prompt - assert.NotContains(t, buf.String(), "[Y/n]") - }) -} - func TestBuildCmd_MultiDocument(t *testing.T) { // multiDocSetup creates a common build environment for multi-document tests. // It returns the temp dir and tracks session/build requests. @@ -1376,7 +1233,7 @@ documents: s := multiDocSetup(t, config, "") ui, buf := testUI() - err := cli.RunBuild(s.dir, nil, false, false, ui) + err := cli.RunBuild(s.dir, nil, false, ui) require.NoError(t, err) // Should get exactly one session and one sync for same-version docs @@ -1411,7 +1268,7 @@ documents: s := multiDocSetup(t, config, "") ui, buf := testUI() - err := cli.RunBuild(s.dir, nil, false, false, ui) + err := cli.RunBuild(s.dir, nil, false, ui) require.NoError(t, err) // Should get two sessions (one per version) and two syncs @@ -1436,7 +1293,7 @@ documents: s := multiDocSetup(t, config, "") ui, _ := testUI() - err := cli.RunBuild(s.dir, []string{"paper"}, false, false, ui) + err := cli.RunBuild(s.dir, []string{"paper"}, false, ui) require.NoError(t, err) // Only one document should be built @@ -1455,7 +1312,7 @@ documents: os.WriteFile(filepath.Join(dir, ".texops.yaml"), []byte(config), 0o600) ui, _ := testUI() - err := cli.RunBuild(dir, []string{"nonexistent"}, false, false, ui) + err := cli.RunBuild(dir, []string{"nonexistent"}, false, ui) require.Error(t, err) assert.Contains(t, err.Error(), "unknown document") assert.Contains(t, err.Error(), "nonexistent") @@ -1473,7 +1330,7 @@ documents: s := multiDocSetup(t, config, "slides.tex") // slides will fail ui, buf := testUI() - err := cli.RunBuild(s.dir, nil, false, false, ui) + err := cli.RunBuild(s.dir, nil, false, ui) require.Error(t, err) assert.Contains(t, err.Error(), "one or more documents failed to build") @@ -1499,7 +1356,7 @@ documents: s := multiDocSetup(t, config, "") ui, buf := testUI() - err := cli.RunBuild(s.dir, nil, false, false, ui) + err := cli.RunBuild(s.dir, nil, false, ui) require.NoError(t, err) output := buf.String() @@ -1522,7 +1379,7 @@ documents: os.WriteFile(filepath.Join(s.dir, "chapters", "paper", "paper.tex"), []byte(`\documentclass{article}\begin{document}Paper\end{document}`), 0o600) ui, buf := testUI() - err := cli.RunBuild(s.dir, nil, false, false, ui) + err := cli.RunBuild(s.dir, nil, false, ui) require.NoError(t, err) assert.Len(t, *s.buildRequests, 2) @@ -1623,7 +1480,7 @@ documents: os.WriteFile(filepath.Join(dir, "paper.tex"), []byte(`\documentclass{article}\begin{document}Hello\end{document}`), 0o600) ui, _ := testUI() - err := cli.RunBuild(dir, nil, false, false, ui) + err := cli.RunBuild(dir, nil, false, ui) require.NoError(t, err) assert.Equal(t, "xelatex", receivedCompiler, "compiler from config should be sent in build request") @@ -1718,7 +1575,7 @@ documents: os.WriteFile(filepath.Join(dir, "slides.tex"), []byte(`\documentclass{beamer}\begin{document}Slides\end{document}`), 0o600) ui, _ := testUI() - err := cli.RunBuild(dir, nil, false, false, ui) + err := cli.RunBuild(dir, nil, false, ui) require.NoError(t, err) require.Len(t, receivedCompilers, 2) @@ -1806,7 +1663,7 @@ documents: os.WriteFile(filepath.Join(dir, "paper.tex"), []byte(`\documentclass{article}\begin{document}Hello\end{document}`), 0o600) ui, _ := testUI() - err := cli.RunBuild(dir, nil, false, false, ui) + err := cli.RunBuild(dir, nil, false, ui) require.NoError(t, err) assert.Equal(t, "pdflatex", receivedCompiler, "default compiler should be pdflatex") diff --git a/man/tx.1 b/man/tx.1 index bac4237..55e470d 100644 --- a/man/tx.1 +++ b/man/tx.1 @@ -15,7 +15,6 @@ .Nm .Cm build .Op Fl -no-cache -.Op Fl y | Fl -yes .Op Ar name ... .Nm .Cm status @@ -128,10 +127,6 @@ Flags: .Bl -tag -width Ds .It Fl -no-cache Rebuild without using the remote build cache. -.It Fl y , Fl -yes -Skip the upload size confirmation prompt. -This prompt is shown when the combined size of files to upload exceeds 50 MB. -When stdout is not a TTY, confirmations default to yes. .El .Ss status Show authentication status including email @@ -431,7 +426,7 @@ CI pipeline snippet .Pq assumes .texops.yaml No is committed : .Bd -literal -offset indent export TX_API_TOKEN="$TEXOPS_TOKEN" -tx build --yes +tx build .Ed .Pp Multi-document configuration and build: diff --git a/man/tx.1.txt b/man/tx.1.txt index 363e259..ff3a4d9 100644 --- a/man/tx.1.txt +++ b/man/tx.1.txt @@ -6,7 +6,7 @@ NAME SYNOPSIS tx login tx init [--texlive version] [--compiler name] [--main file] - tx build [--no-cache] [-y | --yes] [name ...] + tx build [--no-cache] [name ...] tx status tx token create [--name name] [--expires-in duration] [--no-expiry] tx token list @@ -87,11 +87,6 @@ COMMANDS --no-cache Rebuild without using the remote build cache. - -y, --yes - Skip the upload size confirmation prompt. This prompt is shown - when the combined size of files to upload exceeds 50 MB. When - stdout is not a TTY, confirmations default to yes. - status Show authentication status including email (when available), authentication method, and token expiry. Always exits 0, even when not @@ -295,7 +290,7 @@ EXAMPLES CI pipeline snippet (assumes .texops.yaml is committed): export TX_API_TOKEN="$TEXOPS_TOKEN" - tx build --yes + tx build Multi-document configuration and build: