Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
e8d6c99
feat(config): add TALXIS.CLI.Config core (primitives, stores, resolver)
TomProkop Apr 22, 2026
bca1c6c
feat(config): add MSAL-backed credential vault (DPAPI/Keychain/libsec…
TomProkop Apr 22, 2026
948df2d
feat(config): add HeadlessAuthRequiredException + EnsureKindAllowed h…
TomProkop Apr 22, 2026
fe4ead2
feat(config): add Dataverse provider package (authority map, scope, M…
TomProkop Apr 22, 2026
e11fe55
feat(config/dataverse): add pac-parity federated assertion callbacks
TomProkop Apr 22, 2026
8962ea5
feat(config/cli): scaffold config auth list/show/delete commands
TomProkop Apr 22, 2026
3dbbcbd
feat(config/cli): add config auth login command
TomProkop Apr 22, 2026
7ec9587
feat(config/cli): add config auth add-service-principal command
TomProkop Apr 22, 2026
f8fbd94
feat(config/cli): add config connection create/list/show/delete commands
TomProkop Apr 22, 2026
f2887c5
feat(config/cli): add config profile create/list/show/update/select/d…
TomProkop Apr 22, 2026
9239576
feat(config/cli): add config profile pin/unpin
TomProkop Apr 22, 2026
f231b40
feat(config/cli): add config profile validate with live-check seam
TomProkop Apr 22, 2026
153bae9
feat(config/cli): add config setting set/get/list
TomProkop Apr 22, 2026
e6f6fcf
docs(contributing): document five-group taxonomy with config sub-nouns
TomProkop Apr 22, 2026
83926fe
feat(cli): wire config top-level group + bootstrap TxcServices
TomProkop Apr 22, 2026
b1cdfc4
Add ProfiledCliCommand base and Dataverse runtime connection factory
TomProkop Apr 23, 2026
6701fec
Refactor Dataverse commands onto ProfiledCliCommand + remove legacy S…
TomProkop Apr 23, 2026
cefb04a
Remove secrets from PackageDeployerRequest; re-resolve credential in …
TomProkop Apr 23, 2026
d42dce2
Align CmtImportRequest with PackageDeployerRequest: no secrets on record
TomProkop Apr 23, 2026
876e126
Harden LogRedactionFilter patterns; apply redaction at JsonStderrLogg…
TomProkop Apr 23, 2026
7453208
Add ProfileEndToEndTests integration suite
TomProkop Apr 23, 2026
24c04ec
MCP: force TXC_NON_INTERACTIVE on every subprocess; document auth con…
TomProkop Apr 23, 2026
ff3ea9a
MCP: lock in per-call 'profile' argument contract with reflection tests
TomProkop Apr 23, 2026
1e1f220
Add MCP HTTP transport auth design notes (design-only, no code)
TomProkop Apr 23, 2026
eb2b8ae
Update README with profile-based auth workflow
TomProkop Apr 23, 2026
19ea619
Address PR #12 review: JSON enum converters, dead code, ct plumbing, …
TomProkop Apr 23, 2026
80d0dae
Address PR #12 round 2: bugs, DRY, dead fields, JSON clone cleanup
TomProkop Apr 23, 2026
b41c450
Add [McpIgnore] attribute to reduce MCP tool surface
TomProkop Apr 23, 2026
4d031e5
Support InteractiveBrowser auth and isolate CMT import out-of-process
TomProkop Apr 23, 2026
29bcc92
Remove legacy Dataverse auth helpers superseded by token-provider path
TomProkop Apr 23, 2026
21b8284
Add one-command profile onboarding (--url)
TomProkop Apr 23, 2026
cfbd829
Collapse duplication around interactive credential upsert
TomProkop Apr 23, 2026
128b712
Drop tracked .DS_Store; gitignore temp/ and .DS_Store
TomProkop Apr 23, 2026
d64e56a
Merge remote-tracking branch 'origin/master' into tp/config-auth-prof…
TomProkop Apr 23, 2026
ecf806e
Move Dataverse composition root into platform layer
TomProkop Apr 23, 2026
5cdb83d
Extract ISolutionInventoryService abstraction (SolutionList pilot)
TomProkop Apr 23, 2026
221b235
Move Environment/Platforms/Dataverse into Config.Providers.Dataverse
TomProkop Apr 23, 2026
73a0c90
Abstract IDataPackageService and ISolutionUninstallService
TomProkop Apr 23, 2026
4d9f209
Abstract ISolutionImportService
TomProkop Apr 23, 2026
c9041be
Extract IDeploymentHistoryService abstraction
TomProkop Apr 23, 2026
9f15cce
Extract IDeploymentDetailService abstraction
TomProkop Apr 23, 2026
ec13da2
Extract IPackageImportService abstraction
TomProkop Apr 23, 2026
89ef3db
Extract IPackageUninstallService abstraction
TomProkop Apr 23, 2026
7d447a2
Phase 1 close: Features depend only on Core + Logging
TomProkop Apr 23, 2026
782a4ea
Drop unused Workspace -> Data ProjectReference
TomProkop Apr 23, 2026
917da13
Revert LegacyAssemblyHostSubprocess.TryDeleteDirectory to internal
TomProkop Apr 23, 2026
7a51209
Phase 2A: Rename feature + Xrm platform projects
TomProkop Apr 23, 2026
3d654ca
refactor: merge Dataverse + Config.Providers.Dataverse into Platform.…
TomProkop Apr 23, 2026
12646a0
refactor: merge Shared into Config and rename to TALXIS.CLI.Core
TomProkop Apr 23, 2026
d2638cd
docs: update CONTRIBUTING with Hosts/Features/Core/Platform layout
TomProkop Apr 23, 2026
f5191ac
fix: address PR #12 review comments (round 3)
TomProkop Apr 23, 2026
3d68772
refactor: move DataverseConnectionProviderBootstrapper to Platform.Da…
TomProkop Apr 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -417,4 +417,5 @@ FodyWeavers.xsd
*.msm
*.msp

temp
temp
.DS_Store
91 changes: 68 additions & 23 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,32 @@ If you want to add a command, change a command's shape, or introduce a new surfa

## Top-level taxonomy

The CLI has four top-level groups, in this order, by design:
The CLI has five top-level groups, in this order, by design:

```
txc
├── profile # identity the CLI acts as (deferred)
├── config # identity the CLI acts as (profiles, connections, credentials, settings)
├── workspace # the local code repository (scaffold, build, validate, language-server, metamodel)
├── environment # the live target runtime footprint the workspace deploys to
└── data # data migration, transformation, imports
├── data # data migration, transformation, imports
└── docs # knowledge base for TALXIS CLI
```

These four groups are deliberately small. Adding a fifth top-level group requires a strong justification — if a new piece of functionality fits under an existing noun, put it there.
These five groups are deliberately small. Adding a sixth top-level group requires a strong justification — if a new piece of functionality fits under an existing noun, put it there.

### `config` sub-nouns

The `config` group has four sub-nouns, each owning one aspect of the resolution pipeline:

```
txc config
├── auth # credentials (OAuth tokens, service principals) stored in the OS vault
├── connection # service endpoint metadata (Dataverse environments, etc.)
├── profile # named binding of one auth × one connection — the "context" users switch between
└── setting # tool-wide preferences (log.level, log.format, telemetry.enabled)
```

The separation is deliberate. Connections are *where*, credentials are *who*, profiles are the *context* mapping them, and settings are tool-level knobs unrelated to any identity. Do not collapse them or teach one sub-noun to write into another's store.

---

Expand All @@ -27,7 +42,7 @@ These four groups are deliberately small. Adding a fifth top-level group require
Command paths describe **what the user is doing**, not **what platform implements it**.

- **No platform names in user-facing paths.** The word `dataverse` does not appear in any command path, and neither will any future platform name (`azure`, `entra`, `graph`, etc.). Users should not need to know or care which runtime their workspace artifacts land on.
- **Platforms live internally.** Platform-specific code lives under `Platforms/<Name>/` inside the owning project (e.g. `TALXIS.CLI.Environment/Platforms/Dataverse/`). Do **not** create a `<Name>CliCommand` — platforms are not command groups.
- **Platforms live in dedicated projects.** Platform-specific implementations live in `TALXIS.CLI.Platform.<Name>` projects (e.g. `TALXIS.CLI.Platform.Dataverse`). Feature projects depend on abstractions in `TALXIS.CLI.Core`, never directly on a `Platform.*` project. Provider registration happens in the host (`TALXIS.CLI` / `TALXIS.CLI.MCP`). Do **not** create a `<Name>CliCommand` — platforms are not command groups.
- **Abstractions are extracted, not speculated.** When a second platform is actually implemented, extract an interface from the shape that already exists. Do not speculate an `IEnvironmentPlatform` (or similar) before there is a second concrete implementation to validate it.

We avoid even the term "backend" for this abstraction: a Dataverse environment carries metadata for frontend (forms, apps), middle tier (business logic, plugins, workflows), and integrations. Calling it a backend understates what ships there.
Expand Down Expand Up @@ -114,23 +129,26 @@ Short-flag aliases (`-v`, `-y`, etc.) are out of scope until there is a concrete

Long, explicit names are the canonical form, but **group** commands (the ones that hold children, not leaves) carry an `Alias` so day-to-day typing and README snippets stay short. Current aliases:

| Canonical | Alias |
| ------------------ | ------- |
| `environment` | `env` |
| `env deployment` | `deploy`|
| `env package` | `pkg` |
| `env solution` | `sln` |
| `data package` | `pkg` |
| `workspace` | `ws` |
| `workspace component` | `c` |
| `workspace project` | `p` |
| Canonical | Alias |
| --------------------- | ------- |
| `config` | `c` |
| `config profile` | `p` |
| `environment` | `env` |
| `env deployment` | `deploy`|
| `env package` | `pkg` |
| `env solution` | `sln` |
| `data package` | `pkg` |
| `workspace` | `ws` |
| `workspace component` | `c` |
| `workspace project` | `p` |

Rules:

- **Aliases are for groups, not leaf verbs.** `import`, `list`, `show`, `uninstall`, `create`, `convert` etc. stay spelled out — they're already short and a single letter would be ambiguous.
- **Canonical names drive everything machine-readable.** MCP tool names, help anchors, the SDK surface — all built from `Name`. Aliases are a typing shortcut for humans; they never leak into tool IDs.
- **One alias per command.** If you find yourself reaching for a second alias, rename the canonical instead.
- **Prefer README and docs to use the alias** in example snippets — that's what the alias is for. Use the canonical name in reference tables, help output, and anywhere a reader needs to scan the full taxonomy.
- **Short-alias exception for `--profile`.** Because `config profile select` is the single most frequently typed command on a dev laptop, the `--profile` flag on every auth-requiring leaf command exposes the short form `-p`. This is the only flag-level short alias in the CLI.

---

Expand Down Expand Up @@ -165,10 +183,10 @@ We use this mechanism to pin the design of reserved-but-not-yet-implemented comm

Current reserved skeletons:

- `TALXIS.CLI.Environment.Deployment.DeploymentPatchCliCommand` → future `environment deployment patch`.
- `TALXIS.CLI.Workspace.WorkspaceValidateCliCommand` → future `workspace validate`.
- `TALXIS.CLI.Workspace.WorkspaceLanguageServerCliCommand` → future `workspace language-server`.
- `TALXIS.CLI.Workspace.Metamodel.MetamodelCliCommand` + `MetamodelDescribeCliCommand` + `MetamodelListCliCommand` → future `workspace metamodel {describe,list}`.
- `TALXIS.CLI.Features.Environment.Deployment.DeploymentPatchCliCommand` → future `environment deployment patch`.
- `TALXIS.CLI.Features.Workspace.WorkspaceValidateCliCommand` → future `workspace validate`.
- `TALXIS.CLI.Features.Workspace.WorkspaceLanguageServerCliCommand` → future `workspace language-server`.
- `TALXIS.CLI.Features.Workspace.Metamodel.MetamodelCliCommand` + `MetamodelDescribeCliCommand` + `MetamodelListCliCommand` → future `workspace metamodel {describe,list}`.

Each skeleton throws `NotImplementedException` and carries a file-top comment explaining that it is intentionally unreachable and how to activate it.

Expand Down Expand Up @@ -198,15 +216,42 @@ Never call the metamodel "the model" — it collides with the user's data model

When a second runtime platform actually needs to be supported:

1. Add `Platforms/<Name>/` inside the owning project (e.g. `TALXIS.CLI.Environment/Platforms/Azure/`).
2. Put all platform-specific services in that folder, in a `TALXIS.CLI.Environment.Platforms.<Name>` namespace.
3. Keep the command classes platform-agnostic: a single `Package.PackageImportCliCommand` dispatches to the right platform internally.
4. At this point (and not before), extract a shared abstraction — e.g. `IEnvironmentPlatform` — from the two real shapes. Do not write the interface before the second implementation exists.
1. Add a new `TALXIS.CLI.Platform.<Name>` project (e.g. `TALXIS.CLI.Platform.Azure`) alongside the existing `Platform.*` projects.
2. Put all platform-specific services in that project, under a `TALXIS.CLI.Platform.<Name>` namespace.
3. Register the provider's services in an `AddTxc<Name>Provider` extension, and wire it up from the host composition roots (`TALXIS.CLI/Program.cs` and `TALXIS.CLI.MCP/Program.cs`).
4. Keep the command classes platform-agnostic: a single `Package.PackageImportCliCommand` resolves the right implementation via the DI container, it does not reference a `Platform.*` project directly.
5. At this point (and not before), extract a shared abstraction in `TALXIS.CLI.Core` — e.g. `IEnvironmentPlatform` — from the two real shapes. Do not write the interface before the second implementation exists.

Do not add a `<Name>CliCommand`. Platforms are implementation details; they do not surface as command groups.

---

## Project layout

Projects are grouped by architectural role. Names must reflect this role:

- **Hosts** — thin entrypoints that compose DI and register commands. Nothing else.
- `TALXIS.CLI` — the `txc` CLI host.
- `TALXIS.CLI.MCP` — the MCP server host.
- **Features** — user-facing command surfaces and orchestration, organised by domain.
- `TALXIS.CLI.Features.Config`, `TALXIS.CLI.Features.Data`, `TALXIS.CLI.Features.Environment`, `TALXIS.CLI.Features.Workspace`, `TALXIS.CLI.Features.Docs`.
- **Core** — contracts, models, configuration, vault, resolution, shared utilities.
- `TALXIS.CLI.Core`.
- **Platform** — external-system adapters and SDK integration.
- `TALXIS.CLI.Platform.Dataverse`, `TALXIS.CLI.Platform.Xrm`, `TALXIS.CLI.Platform.XrmShim`.
- **Cross-cutting** — infrastructure.
- `TALXIS.CLI.Logging`.

**Layering rules:**

- Features depend on `Core` and `Logging` only. Features do **not** reference `Platform.*` projects.
- Platform depends on `Core` (and external SDKs). Platform does **not** reference `Features.*`.
- Hosts reference everything they need for composition: `Features.*`, `Platform.*`, `Core`, `Logging`.
- No feature references another feature. Shared logic goes into `Core`.
- Provider selection happens at the host composition root, never inside a command handler.

---

## Questions, disagreements, changes

If you think the philosophy above is wrong for a specific case, open an issue or a draft PR that explains the case and proposes a targeted amendment to this document. The rule is: change the document first, then write the code that follows it.
115 changes: 102 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ TALXIS CLI (`txc`) is a modular, extensible .NET global tool for automating deve

## Table of Contents
- [Installation](#installation)
- [Identity, Connections & Profiles](#identity-connections--profiles)
- [Example Usage](#example-usage)
- [Local Development & Debugging](#local-development--debugging)
- [Versioning & Release](#versioning--release)
Expand Down Expand Up @@ -50,43 +51,131 @@ After installation, use the CLI via the `txc` command in any terminal.

---

## Identity, Connections & Profiles

`txc` decouples **who you are** (credentials) from **where you target** (connections) and exposes the combination as a named **profile**. Every command that touches a live environment takes exactly one context flag — `--profile <name>` (short form `-p`). There are no raw `--environment`, `--connection-string`, or `--device-code` flags on leaf commands; to switch endpoints or identities you create (or select) a different profile.

The resolution order for the active profile is:

```
--profile flag > TXC_PROFILE env > <repo>/.txc/workspace.json > global active pointer (~/.txc/config.json)
```

Credentials never live in config files. Service-principal secrets, PATs, and certificate passwords are stored in the OS credential vault (DPAPI on Windows, Keychain on macOS, libsecret on Linux) and referenced from `credentials.json` by opaque `vault://` handles. MSAL tokens live in a separate cache file protected by the same vault.

### Interactive workflow (dev laptop)

**Quickstart — one command.** The common case (new laptop, new tenant) collapses to a single line:

```sh
txc c p create --url https://contoso.crm4.dynamics.com/
```

Behind the scenes `txc`:
1. Infers the provider from the URL host (`*.dynamics.com` → `dataverse`; gov/DoD/China endpoints covered too).
2. Derives a default profile/connection name from the first DNS label (`contoso` above). Override with `--name`.
3. Opens the browser for interactive sign-in, caches the MSAL token, and stores a credential keyed off your UPN.
4. Writes the `Credential`, `Connection`, and `Profile` records and auto-activates the new profile on first run.

Result is identical to running the three primitive commands by hand — use whichever flow you prefer.

**Advanced — primitives.** For scripted flows, service-principal onboarding, or reusing one credential across many environments:

```sh
# 1. Log in interactively (opens a browser). Creates a Credential entry
# aliased from your UPN (override with --alias) and primes the MSAL cache.
txc c auth login

# 2. Register the Dataverse environment you want to target.
txc c connection create customer-a-dev \
--provider dataverse \
--environment https://contoso.crm4.dynamics.com/

# 3. Bind credential + connection into a profile and select it.
txc c p create --name customer-a-dev \
--auth <upn-alias> \
--connection customer-a-dev
txc c p select customer-a-dev

# 4. Optional: pin this profile to the current repo.
# Writes <repo>/.txc/workspace.json so every shell in this checkout
# defaults to customer-a-dev without touching the global pointer.
txc c p pin
# Unpin when done: txc c p unpin

# 5. Sanity-check end-to-end auth + endpoint reachability.
txc c p validate
```

### Headless / CI workflow (service principal)

```sh
# Total config isolation — nothing written to $HOME on the runner.
export TXC_CONFIG_DIR="$RUNNER_TEMP/txc-config"
export TXC_NON_INTERACTIVE=1

# Secret is supplied via env var (never as --secret on the command line,
# which would leak to shell history and process listings).
export SPN_SECRET='<client-secret>'

txc c auth add-service-principal \
--tenant "$AZURE_TENANT_ID" \
--client-id "$AZURE_CLIENT_ID" \
--alias ci-spn \
--secret-from-env SPN_SECRET

txc c connection create ci-target \
--provider dataverse \
--environment "$DATAVERSE_URL"

txc c p create --name ci --auth ci-spn --connection ci-target
txc c p select ci

# Every subsequent txc call picks up TXC_CONFIG_DIR + the selected profile.
txc env pkg import TALXIS.Controls.FileExplorer.Package
```

For workload-identity federation (GitHub OIDC, Azure DevOps WIF), the Dataverse provider auto-detects `ACTIONS_ID_TOKEN_REQUEST_*` and `TXC_ADO_ID_TOKEN_REQUEST_*` env vars at acquire time — no extra flags needed.

---

## Example Usage

> [!IMPORTANT]
> `txc` runs both **Dataverse Package Deployer** and **Configuration Migration Tool (CMT)** on **modern .NET**, including **macOS** and **Linux**. The goal is a better developer experience: cross-platform automation, simpler happy-path commands, and better visibility into what happened during deploys.

The examples below assume you have an active profile (see [above](#identity-connections--profiles)). Pass `--profile <name>` (or `-p <name>`) to any command to override the active profile for a single invocation.

**Deploy the latest package from NuGet:**
```sh
txc env pkg import TALXIS.Controls.FileExplorer.Package \
--environment https://org.crm.dynamics.com
txc env pkg import TALXIS.Controls.FileExplorer.Package
```

**Inspect the latest package deployment with findings:**
```sh
txc env deploy show --package-name TALXIS.Controls.FileExplorer.Package \
--environment https://org.crm.dynamics.com
txc env deploy show --package-name TALXIS.Controls.FileExplorer.Package
```

**Uninstall a package from its source artifact:**
```sh
txc env pkg uninstall TALXIS.Controls.FileExplorer.Package \
--yes \
--environment https://org.crm.dynamics.com
txc env pkg uninstall TALXIS.Controls.FileExplorer.Package --yes
```

**Import a solution and follow the async operation when needed:**
```sh
txc env sln import ./Solutions/MySolution_managed.zip \
--environment https://org.crm.dynamics.com
txc env sln import ./Solutions/MySolution_managed.zip

txc env deploy show --async-operation-id <asyncOperationId>
```

txc env deploy show --async-operation-id <asyncOperationId> \
--environment https://org.crm.dynamics.com
**Target a different environment for a single call without switching profiles:**
```sh
txc env sln import ./Solutions/MySolution_managed.zip -p customer-b-prod
```

**Import a CMT data folder into Dataverse:**
```sh
txc data pkg import ./data-package \
--environment https://org.crm.dynamics.com
txc data pkg import ./data-package
```

**Convert Excel to CMT XML:**
Expand Down
Loading