diff --git a/README.md b/README.md index 0c92fe4..8dd2e0c 100644 --- a/README.md +++ b/README.md @@ -86,10 +86,10 @@ Store them once per machine: ``` $ tfscli auth login -Server URL (e.g. https://tfs.company.com:8080/tfs): https://tfs.company.com:8080/tfs +Server URL (e.g. https://tfs.example.com:8080/tfs): https://tfs.example.com:8080/tfs Collection (e.g. DefaultCollection): DefaultCollection Personal access token: -Logged in to https://tfs.company.com:8080/tfs, collection DefaultCollection, as Jane Doe +Logged in to https://tfs.example.com:8080/tfs, collection DefaultCollection, as Jane Doe ``` The token is typed with echo turned off. The three values are checked against the server before anything is written: a rejected token, or a URL and collection that lead nowhere, is reported as an error and leaves nothing behind. The command takes no flags, refuses `--api-version`, and sends its check without `api-version` whatever the config file or `TFSCLI_API_VERSION` say. @@ -106,7 +106,7 @@ The credential is written to `$XDG_DATA_HOME/tfscli/auth.json`, or `~/.local/sha Where an interactive login is impossible, put the same JSON into `TFSCLI_AUTH`. When the variable is set, `auth.json` is not read: ``` -TFSCLI_AUTH='{"url": "https://tfs.company.com:8080/tfs", "collection": "DefaultCollection", "pat": "…"}' +TFSCLI_AUTH='{"url": "https://tfs.example.com:8080/tfs", "collection": "DefaultCollection", "pat": "…"}' ``` All three fields are required. The URL, the collection, and the token always come from the same source, and no flag, separate environment variable, or config key can supply any of them, so a mistyped or injected server address cannot make tfscli send the token to another host. @@ -323,8 +323,8 @@ An empty result reads `Work items: none` or `Relations: none`. The time of the r ``` $ tfscli wit work-items get -p MyProject 12345 --verbose -OPTIONS https://tfs.company.com:8080/tfs/DefaultCollection/_apis 200 41.2087ms -GET https://tfs.company.com:8080/tfs/DefaultCollection/MyProject/_apis/wit/workitems/12345?api-version=7.1 200 86.4512ms +OPTIONS https://tfs.example.com:8080/tfs/DefaultCollection/_apis 200 41.2087ms +GET https://tfs.example.com:8080/tfs/DefaultCollection/MyProject/_apis/wit/workitems/12345?api-version=7.1 200 86.4512ms ``` Lines starting with `[warn]` report a condition worth knowing, such as disabled certificate verification; lines starting with `[info]` report what tfscli did on its own, such as sending a request without a version: @@ -374,7 +374,7 @@ $ tfscli wit work-items get -p MyProject 12345 Error [config]: The requested REST API version of 7.2 is out of range for this server. The latest REST API version this server supports is 7.1. (api-version "7.2" is set by TFSCLI_API_VERSION; remove it to let the server choose the version, or set one the server supports) (HTTP 400) $ tfscli wit work-items get -p MyProject 12345 -Error [network]: cannot reach https://tfs.company.com:8080 +Error [network]: cannot reach https://tfs.example.com:8080 ``` ## Known limitations diff --git a/docs/archive/2026-10-01-tasks-login-api-version.md b/docs/archive/2026-10-01-tasks-login-api-version.md index 6103b48..552cf83 100644 --- a/docs/archive/2026-10-01-tasks-login-api-version.md +++ b/docs/archive/2026-10-01-tasks-login-api-version.md @@ -350,7 +350,7 @@ supports. Also establish: older servers, since `verify` in `internal/cli/auth.go` relies on it (see TD-01 in `docs/tech-debt.md` for the anonymous case); - whether `connectionData` also answers when the URL entered at login already - contains a collection, such as `https://tfs.company.com:8080/tfs/DefaultCollection`. + contains a collection, such as `https://tfs.example.com:8080/tfs/DefaultCollection`. If it does, login accepts and stores that URL, every later command appends the collection a second time, and the user receives a bare `not_found` with no hint at the cause. diff --git a/docs/project-brief.md b/docs/project-brief.md index ccdb95f..abd9888 100644 --- a/docs/project-brief.md +++ b/docs/project-brief.md @@ -66,7 +66,7 @@ An AI agent can call `tfscli`, read work item content, and use it meaningfully i ```json { - "url": "https://tfs.company.com:8080/tfs", + "url": "https://tfs.example.com:8080/tfs", "collection": "DefaultCollection", "pat": "..." } @@ -119,7 +119,7 @@ Error [not_found]: work item 99999 not found in project MyProject (HTTP 404) Error [forbidden]: no access to project MyProject (HTTP 403) Error [server]: TFS returned HTTP 500 Error [config]: not logged in: no credential at ~/.local/share/tfscli/auth.json (run "tfscli auth login", or set TFSCLI_AUTH) -Error [network]: cannot reach https://tfs.company.com:8080 +Error [network]: cannot reach https://tfs.example.com:8080 ``` ## Key Dependencies diff --git a/internal/apiclient/apiclient_test.go b/internal/apiclient/apiclient_test.go index d24ddf6..a14fe9f 100644 --- a/internal/apiclient/apiclient_test.go +++ b/internal/apiclient/apiclient_test.go @@ -458,9 +458,9 @@ func TestNewRejectsBadURL(t *testing.T) { name string url string }{ - {"unparseable", "http://tfs.company.com:pot/tfs"}, - {"missing scheme", "tfs.company.com:8080/tfs"}, - {"unsupported scheme", "ftp://tfs.company.com/tfs"}, + {"unparseable", "http://tfs.example.com:pot/tfs"}, + {"missing scheme", "tfs.example.com:8080/tfs"}, + {"unsupported scheme", "ftp://tfs.example.com/tfs"}, } for _, tt := range tests { @@ -479,7 +479,7 @@ func TestNewRejectsBadURL(t *testing.T) { func TestNewWarnsWhenVerificationDisabled(t *testing.T) { logger := &recordingLogger{} if _, err := New(&config.Config{ - URL: "https://tfs.company.com:8080/tfs", + URL: "https://tfs.example.com:8080/tfs", Collection: "DefaultCollection", PAT: testPAT, APIVersion: "7.2", @@ -496,7 +496,7 @@ func TestNewWarnsWhenVerificationDisabled(t *testing.T) { func TestNewStaysSilentWhenVerificationIsOn(t *testing.T) { logger := &recordingLogger{} if _, err := New(&config.Config{ - URL: "https://tfs.company.com:8080/tfs", + URL: "https://tfs.example.com:8080/tfs", Collection: "DefaultCollection", PAT: testPAT, APIVersion: "7.2", @@ -526,7 +526,7 @@ func TestNewRejectsBadCABundle(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := New(&config.Config{ - URL: "https://tfs.company.com:8080/tfs", + URL: "https://tfs.example.com:8080/tfs", Collection: "DefaultCollection", PAT: testPAT, APIVersion: "7.2", diff --git a/internal/cli/auth.go b/internal/cli/auth.go index 36c9eeb..de9f183 100644 --- a/internal/cli/auth.go +++ b/internal/cli/auth.go @@ -148,7 +148,7 @@ func (g *globals) login(ctx context.Context) error { // promptAuth asks for the URL, the collection, and the token. Prompts go to // stderr so that stdout carries only the result. func (g *globals) promptAuth() (*config.Auth, error) { - _, _ = fmt.Fprint(g.stderr, "Server URL (e.g. https://tfs.company.com:8080/tfs): ") + _, _ = fmt.Fprint(g.stderr, "Server URL (e.g. https://tfs.example.com:8080/tfs): ") raw, err := g.stdin.ReadLine() if err != nil { return nil, inputError("the server URL", err) @@ -211,7 +211,7 @@ func refuseUserinfo(raw string) error { Category: tfserr.Config, Message: "the server URL contains a user name or password; tfscli authenticates only with " + "the personal access token it asks for next. Run tfscli auth login again and enter the URL " + - "without them, e.g. https://tfs.company.com/tfs", + "without them, e.g. https://tfs.example.com/tfs", } } diff --git a/internal/cli/auth_test.go b/internal/cli/auth_test.go index 84eb9d4..528873e 100644 --- a/internal/cli/auth_test.go +++ b/internal/cli/auth_test.go @@ -338,20 +338,20 @@ func TestAuthLoginFailuresWriteNothing(t *testing.T) { }, { name: "url without a scheme", - in: func(string) prompter { return typed("tfs.company.com/tfs", "DefaultCollection", "secret-token") }, - want: "Error [config]: url \"tfs.company.com/tfs\" must start with http:// or https://\n", + in: func(string) prompter { return typed("tfs.example.com/tfs", "DefaultCollection", "secret-token") }, + want: "Error [config]: url \"tfs.example.com/tfs\" must start with http:// or https://\n", }, { name: "url with a user name and password", in: func(string) prompter { - return typed("https://user:hunter2@tfs.company.com/tfs", "DefaultCollection", "secret-token") + return typed("https://user:hunter2@tfs.example.com/tfs", "DefaultCollection", "secret-token") }, - want: "Error [config]: the server URL contains a user name or password; tfscli authenticates only with the personal access token it asks for next. Run tfscli auth login again and enter the URL without them, e.g. https://tfs.company.com/tfs\n", + want: "Error [config]: the server URL contains a user name or password; tfscli authenticates only with the personal access token it asks for next. Run tfscli auth login again and enter the URL without them, e.g. https://tfs.example.com/tfs\n", }, { name: "url with a user name only", in: func(string) prompter { - return typed("https://user@tfs.company.com/tfs", "DefaultCollection", "secret-token") + return typed("https://user@tfs.example.com/tfs", "DefaultCollection", "secret-token") }, want: "Error [config]: the server URL contains a user name or password;", }, diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 3f15b57..fa656e6 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -27,7 +27,7 @@ const workItemResponse = `{ "System.AssignedTo": { "displayName": "Jane Doe", "uniqueName": "COMPANY\\j.doe", - "imageUrl": "https://tfs.company.com:8080/DefaultCollection/_api/_common/identityImage?id=2f1a8c3e" + "imageUrl": "https://tfs.example.com:8080/DefaultCollection/_api/_common/identityImage?id=2f1a8c3e" }, "System.CreatedDate": "2026-06-14T09:12:33.117Z", "System.Description": "
See work item 12345.
`, - want: "See [work item 12345](https://tfs.company.com/wi/12345).", + html: `See work item 12345.
`, + want: "See [work item 12345](https://tfs.example.com/wi/12345).", }, { name: "paragraphs are separated by a blank line", diff --git a/internal/log/log_test.go b/internal/log/log_test.go index 10a6d71..d3b37fc 100644 --- a/internal/log/log_test.go +++ b/internal/log/log_test.go @@ -11,9 +11,9 @@ func TestWriterLoggerLogRequest(t *testing.T) { var sb strings.Builder l := New(&sb) - l.LogRequest("GET", "https://tfs.company.com:8080/DefaultCollection/MyProject/_apis/wit/workitems/12345?api-version=7.2", 200, 245*time.Millisecond) + l.LogRequest("GET", "https://tfs.example.com:8080/DefaultCollection/MyProject/_apis/wit/workitems/12345?api-version=7.2", 200, 245*time.Millisecond) - want := "GET https://tfs.company.com:8080/DefaultCollection/MyProject/_apis/wit/workitems/12345?api-version=7.2 200 245ms\n" + want := "GET https://tfs.example.com:8080/DefaultCollection/MyProject/_apis/wit/workitems/12345?api-version=7.2 200 245ms\n" if got := sb.String(); got != want { t.Errorf("LogRequest() wrote %q, want %q", got, want) } @@ -23,18 +23,18 @@ func TestWriterLoggerLogRequestOneLinePerCall(t *testing.T) { var sb strings.Builder l := New(&sb) - l.LogRequest("GET", "https://tfs.company.com/coll/_apis/a", 200, time.Second) - l.LogRequest("GET", "https://tfs.company.com/coll/_apis/b", 404, 30*time.Millisecond) + l.LogRequest("GET", "https://tfs.example.com/coll/_apis/a", 200, time.Second) + l.LogRequest("GET", "https://tfs.example.com/coll/_apis/b", 404, 30*time.Millisecond) got := sb.String() lines := strings.Split(strings.TrimSuffix(got, "\n"), "\n") if len(lines) != 2 { t.Fatalf("LogRequest() x2 wrote %d lines, want 2: %q", len(lines), got) } - if want := "GET https://tfs.company.com/coll/_apis/a 200 1s"; lines[0] != want { + if want := "GET https://tfs.example.com/coll/_apis/a 200 1s"; lines[0] != want { t.Errorf("first line = %q, want %q", lines[0], want) } - if want := "GET https://tfs.company.com/coll/_apis/b 404 30ms"; lines[1] != want { + if want := "GET https://tfs.example.com/coll/_apis/b 404 30ms"; lines[1] != want { t.Errorf("second line = %q, want %q", lines[1], want) } } @@ -72,7 +72,7 @@ func TestNoopDoesNotPanic(t *testing.T) { // safe to make. l := Noop() - l.LogRequest("GET", "https://tfs.company.com/coll/_apis/a", 200, time.Second) + l.LogRequest("GET", "https://tfs.example.com/coll/_apis/a", 200, time.Second) l.Warn("TLS verification disabled") l.Info("api-version not negotiated") } diff --git a/internal/tfserr/tfserr_test.go b/internal/tfserr/tfserr_test.go index 67006d5..0dbb825 100644 --- a/internal/tfserr/tfserr_test.go +++ b/internal/tfserr/tfserr_test.go @@ -43,7 +43,7 @@ func TestPrintWithoutHTTPStatus(t *testing.T) { {Forbidden, "no access to project MyProject", "Error [forbidden]: no access to project MyProject\n"}, {Server, "TFS returned an invalid response", "Error [server]: TFS returned an invalid response\n"}, {Config, "not logged in: no credential at ~/.local/share/tfscli/auth.json", "Error [config]: not logged in: no credential at ~/.local/share/tfscli/auth.json\n"}, - {Network, "cannot reach https://tfs.company.com:8080", "Error [network]: cannot reach https://tfs.company.com:8080\n"}, + {Network, "cannot reach https://tfs.example.com:8080", "Error [network]: cannot reach https://tfs.example.com:8080\n"}, } for _, tt := range tests { t.Run(string(tt.category), func(t *testing.T) { diff --git a/internal/workitem/testdata/bug.json b/internal/workitem/testdata/bug.json index e496c00..35f6914 100644 --- a/internal/workitem/testdata/bug.json +++ b/internal/workitem/testdata/bug.json @@ -10,28 +10,28 @@ "System.Reason": "New", "System.AssignedTo": { "displayName": "Jane Doe", - "url": "https://tfs.company.com:8080/DefaultCollection/_apis/Identities/2f1a8c3e-0b9d-4f77-9a1c-6d5e4b3a2109", + "url": "https://tfs.example.com:8080/DefaultCollection/_apis/Identities/2f1a8c3e-0b9d-4f77-9a1c-6d5e4b3a2109", "id": "2f1a8c3e-0b9d-4f77-9a1c-6d5e4b3a2109", "uniqueName": "COMPANY\\j.doe", - "imageUrl": "https://tfs.company.com:8080/DefaultCollection/_api/_common/identityImage?id=2f1a8c3e-0b9d-4f77-9a1c-6d5e4b3a2109", + "imageUrl": "https://tfs.example.com:8080/DefaultCollection/_api/_common/identityImage?id=2f1a8c3e-0b9d-4f77-9a1c-6d5e4b3a2109", "descriptor": "win.Uy0xLTUtMjEtMTIzNDU2Nzg5MC0xMjM0NTY3ODkwLTEyMzQ1Njc4OTAtMTAwMQ" }, "System.CreatedDate": "2026-06-14T09:12:33.117Z", "System.CreatedBy": { "displayName": "John Smith", - "url": "https://tfs.company.com:8080/DefaultCollection/_apis/Identities/8c7b6a59-4d3e-2f1a-9b8c-7d6e5f4a3b21", + "url": "https://tfs.example.com:8080/DefaultCollection/_apis/Identities/8c7b6a59-4d3e-2f1a-9b8c-7d6e5f4a3b21", "id": "8c7b6a59-4d3e-2f1a-9b8c-7d6e5f4a3b21", "uniqueName": "COMPANY\\j.smith", - "imageUrl": "https://tfs.company.com:8080/DefaultCollection/_api/_common/identityImage?id=8c7b6a59-4d3e-2f1a-9b8c-7d6e5f4a3b21", + "imageUrl": "https://tfs.example.com:8080/DefaultCollection/_api/_common/identityImage?id=8c7b6a59-4d3e-2f1a-9b8c-7d6e5f4a3b21", "descriptor": "win.Uy0xLTUtMjEtMTIzNDU2Nzg5MC0xMjM0NTY3ODkwLTEyMzQ1Njc4OTAtMTAwMg" }, "System.ChangedDate": "2026-07-01T15:48:02Z", "System.ChangedBy": { "displayName": "Jane Doe", - "url": "https://tfs.company.com:8080/DefaultCollection/_apis/Identities/2f1a8c3e-0b9d-4f77-9a1c-6d5e4b3a2109", + "url": "https://tfs.example.com:8080/DefaultCollection/_apis/Identities/2f1a8c3e-0b9d-4f77-9a1c-6d5e4b3a2109", "id": "2f1a8c3e-0b9d-4f77-9a1c-6d5e4b3a2109", "uniqueName": "COMPANY\\j.doe", - "imageUrl": "https://tfs.company.com:8080/DefaultCollection/_api/_common/identityImage?id=2f1a8c3e-0b9d-4f77-9a1c-6d5e4b3a2109", + "imageUrl": "https://tfs.example.com:8080/DefaultCollection/_api/_common/identityImage?id=2f1a8c3e-0b9d-4f77-9a1c-6d5e4b3a2109", "descriptor": "win.Uy0xLTUtMjEtMTIzNDU2Nzg5MC0xMjM0NTY3ODkwLTEyMzQ1Njc4OTAtMTAwMQ" }, "System.CommentCount": 3, @@ -44,16 +44,16 @@ "Microsoft.VSTS.CMMI.Blocked": "No", "Custom.RequiresRegression": true, "System.Description": "