teams-cli 0.1.5__tar.gz → 0.1.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {teams_cli-0.1.5 → teams_cli-0.1.7}/.github/workflows/ci.yml +6 -6
- teams_cli-0.1.7/.github/workflows/dependencies.yml +28 -0
- {teams_cli-0.1.5 → teams_cli-0.1.7}/.github/workflows/release.yml +21 -21
- {teams_cli-0.1.5 → teams_cli-0.1.7}/CHANGELOG.md +16 -0
- {teams_cli-0.1.5 → teams_cli-0.1.7}/Cargo.lock +6 -6
- {teams_cli-0.1.5 → teams_cli-0.1.7}/Cargo.toml +1 -1
- {teams_cli-0.1.5 → teams_cli-0.1.7}/PKG-INFO +8 -2
- {teams_cli-0.1.5 → teams_cli-0.1.7}/README.md +7 -1
- {teams_cli-0.1.5 → teams_cli-0.1.7}/src/auth.rs +27 -0
- {teams_cli-0.1.5 → teams_cli-0.1.7}/src/cli.rs +29 -3
- {teams_cli-0.1.5 → teams_cli-0.1.7}/src/config.rs +56 -2
- {teams_cli-0.1.5 → teams_cli-0.1.7}/src/main.rs +87 -10
- {teams_cli-0.1.5 → teams_cli-0.1.7}/src/schema.rs +64 -18
- {teams_cli-0.1.5 → teams_cli-0.1.7}/tests/cli.rs +52 -0
- teams_cli-0.1.5/.github/dependabot.yml +0 -6
- {teams_cli-0.1.5 → teams_cli-0.1.7}/.gitignore +0 -0
- {teams_cli-0.1.5 → teams_cli-0.1.7}/.mise.toml +0 -0
- {teams_cli-0.1.5 → teams_cli-0.1.7}/DESIGN.md +0 -0
- {teams_cli-0.1.5 → teams_cli-0.1.7}/LICENSE +0 -0
- {teams_cli-0.1.5 → teams_cli-0.1.7}/Makefile +0 -0
- {teams_cli-0.1.5 → teams_cli-0.1.7}/PRODUCT.md +0 -0
- {teams_cli-0.1.5 → teams_cli-0.1.7}/docs/releases.md +0 -0
- {teams_cli-0.1.5 → teams_cli-0.1.7}/examples/validate_schema.rs +0 -0
- {teams_cli-0.1.5 → teams_cli-0.1.7}/pyproject.toml +0 -0
- {teams_cli-0.1.5 → teams_cli-0.1.7}/src/error.rs +0 -0
- {teams_cli-0.1.5 → teams_cli-0.1.7}/src/graph.rs +0 -0
- {teams_cli-0.1.5 → teams_cli-0.1.7}/src/lib.rs +0 -0
- {teams_cli-0.1.5 → teams_cli-0.1.7}/src/output.rs +0 -0
- {teams_cli-0.1.5 → teams_cli-0.1.7}/src/teams_cli_alias.rs +0 -0
- {teams_cli-0.1.5 → teams_cli-0.1.7}/src/tui.rs +0 -0
- {teams_cli-0.1.5 → teams_cli-0.1.7}/tests/package_alias.rs +0 -0
- {teams_cli-0.1.5 → teams_cli-0.1.7}/vership.toml +0 -0
|
@@ -17,24 +17,24 @@ jobs:
|
|
|
17
17
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
18
18
|
runs-on: ${{ matrix.os }}
|
|
19
19
|
steps:
|
|
20
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
|
20
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
21
21
|
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c
|
|
22
22
|
with:
|
|
23
23
|
toolchain: 1.96.0
|
|
24
24
|
components: rustfmt, clippy
|
|
25
|
-
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
|
25
|
+
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
|
26
26
|
- run: cargo test --locked --all-targets
|
|
27
27
|
|
|
28
28
|
quality:
|
|
29
29
|
name: Formatting and lints
|
|
30
30
|
runs-on: ubuntu-latest
|
|
31
31
|
steps:
|
|
32
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
|
32
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
33
33
|
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c
|
|
34
34
|
with:
|
|
35
35
|
toolchain: 1.96.0
|
|
36
36
|
components: rustfmt, clippy
|
|
37
|
-
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
|
37
|
+
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
|
38
38
|
- run: cargo fmt -- --check
|
|
39
39
|
- run: cargo clippy --locked --all-targets -- -D warnings
|
|
40
40
|
|
|
@@ -42,12 +42,12 @@ jobs:
|
|
|
42
42
|
name: Dependency audit
|
|
43
43
|
runs-on: ubuntu-latest
|
|
44
44
|
steps:
|
|
45
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
|
45
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
46
46
|
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c
|
|
47
47
|
with:
|
|
48
48
|
toolchain: 1.96.0
|
|
49
49
|
components: rustfmt, clippy
|
|
50
|
-
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
|
50
|
+
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
|
51
51
|
- name: Install cargo-audit
|
|
52
52
|
run: cargo install cargo-audit --version 0.22.2 --locked
|
|
53
53
|
- name: Audit Cargo.lock
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Dependency health
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: "7 6 * * 1"
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
id-token: write
|
|
11
|
+
packages: read
|
|
12
|
+
pull-requests: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
update:
|
|
16
|
+
uses: rvben/upd/.github/workflows/dependency-health.yml@ecb605d32c4edc8dee4091aeb30b3d37ebdd290a # v0.10.0
|
|
17
|
+
with:
|
|
18
|
+
langs: rust,actions
|
|
19
|
+
lock: true
|
|
20
|
+
min-age: 7d
|
|
21
|
+
max-bump: minor
|
|
22
|
+
prepare-command: >-
|
|
23
|
+
rustup toolchain install 1.96.0 --profile minimal --component clippy,rustfmt &&
|
|
24
|
+
rustup default 1.96.0
|
|
25
|
+
validation-command: make check
|
|
26
|
+
fail-on-blocked: false
|
|
27
|
+
branch: deps/upd
|
|
28
|
+
commit-message: "chore(deps): weekly dependency update"
|
|
@@ -23,7 +23,7 @@ jobs:
|
|
|
23
23
|
name: Verify release version
|
|
24
24
|
runs-on: ubuntu-latest
|
|
25
25
|
steps:
|
|
26
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
|
26
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
27
27
|
- name: Match tag to package version
|
|
28
28
|
env:
|
|
29
29
|
REQUESTED_VERSION: ${{ inputs.version }}
|
|
@@ -69,13 +69,13 @@ jobs:
|
|
|
69
69
|
id-token: write
|
|
70
70
|
attestations: write
|
|
71
71
|
steps:
|
|
72
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
|
72
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
73
73
|
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c
|
|
74
74
|
with:
|
|
75
75
|
toolchain: 1.96.0
|
|
76
76
|
components: rustfmt, clippy
|
|
77
77
|
targets: ${{ matrix.target }}
|
|
78
|
-
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
|
|
78
|
+
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
|
79
79
|
with:
|
|
80
80
|
key: ${{ matrix.target }}
|
|
81
81
|
- run: cargo build --locked --release --target ${{ matrix.target }}
|
|
@@ -89,10 +89,10 @@ jobs:
|
|
|
89
89
|
rm -rf dist/package
|
|
90
90
|
- name: Attest build provenance
|
|
91
91
|
if: ${{ inputs.dry_run != true }}
|
|
92
|
-
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6
|
|
92
|
+
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
|
|
93
93
|
with:
|
|
94
94
|
subject-path: dist/*.tar.gz
|
|
95
|
-
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
|
|
95
|
+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
96
96
|
with:
|
|
97
97
|
name: binary-${{ matrix.target }}
|
|
98
98
|
path: dist/*.tar.gz
|
|
@@ -119,7 +119,7 @@ jobs:
|
|
|
119
119
|
manylinux: auto
|
|
120
120
|
runs-on: ${{ matrix.os }}
|
|
121
121
|
steps:
|
|
122
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
|
122
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
123
123
|
- uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
|
|
124
124
|
with:
|
|
125
125
|
maturin-version: v1.14.1
|
|
@@ -127,12 +127,12 @@ jobs:
|
|
|
127
127
|
target: ${{ matrix.target }}
|
|
128
128
|
manylinux: ${{ matrix.manylinux }}
|
|
129
129
|
args: --release --locked --compatibility pypi --out dist
|
|
130
|
-
- uses: astral-sh/setup-uv@
|
|
130
|
+
- uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
|
131
131
|
if: ${{ inputs.dry_run == true }}
|
|
132
132
|
- name: Validate wheel metadata
|
|
133
133
|
if: ${{ inputs.dry_run == true }}
|
|
134
134
|
run: uvx --from twine==6.2.0 twine check --strict dist/*.whl
|
|
135
|
-
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
|
|
135
|
+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
136
136
|
with:
|
|
137
137
|
name: wheel-${{ matrix.target }}
|
|
138
138
|
path: dist/*.whl
|
|
@@ -143,7 +143,7 @@ jobs:
|
|
|
143
143
|
needs: [version, verify]
|
|
144
144
|
runs-on: ubuntu-latest
|
|
145
145
|
steps:
|
|
146
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
|
146
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
147
147
|
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c
|
|
148
148
|
with:
|
|
149
149
|
toolchain: 1.96.0
|
|
@@ -158,12 +158,12 @@ jobs:
|
|
|
158
158
|
command: sdist
|
|
159
159
|
args: --out dist
|
|
160
160
|
- run: cp target/package/*.crate dist/
|
|
161
|
-
- uses: astral-sh/setup-uv@
|
|
161
|
+
- uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
|
162
162
|
if: ${{ inputs.dry_run == true }}
|
|
163
163
|
- name: Validate source distribution metadata
|
|
164
164
|
if: ${{ inputs.dry_run == true }}
|
|
165
165
|
run: uvx --from twine==6.2.0 twine check --strict dist/*.tar.gz
|
|
166
|
-
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
|
|
166
|
+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
167
167
|
with:
|
|
168
168
|
name: source-distributions
|
|
169
169
|
path: dist/*
|
|
@@ -178,13 +178,13 @@ jobs:
|
|
|
178
178
|
id-token: write
|
|
179
179
|
attestations: write
|
|
180
180
|
steps:
|
|
181
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
|
182
|
-
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
|
|
181
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
182
|
+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
183
183
|
with:
|
|
184
184
|
pattern: binary-*
|
|
185
185
|
path: dist
|
|
186
186
|
merge-multiple: true
|
|
187
|
-
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
|
|
187
|
+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
188
188
|
with:
|
|
189
189
|
name: source-distributions
|
|
190
190
|
path: dist
|
|
@@ -198,7 +198,7 @@ jobs:
|
|
|
198
198
|
run: sha256sum ./*.tar.gz ./*.crate ./*.json > SHA256SUMS
|
|
199
199
|
- name: Attest artifacts and SBOM
|
|
200
200
|
if: ${{ inputs.dry_run != true }}
|
|
201
|
-
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6
|
|
201
|
+
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
|
|
202
202
|
with:
|
|
203
203
|
subject-path: dist/*
|
|
204
204
|
sbom-path: dist/teams-cli.cdx.json
|
|
@@ -217,7 +217,7 @@ jobs:
|
|
|
217
217
|
permissions:
|
|
218
218
|
id-token: write
|
|
219
219
|
steps:
|
|
220
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
|
220
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
221
221
|
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c
|
|
222
222
|
with:
|
|
223
223
|
toolchain: 1.96.0
|
|
@@ -239,7 +239,7 @@ jobs:
|
|
|
239
239
|
- name: Get temporary crates.io token
|
|
240
240
|
if: steps.crate.outputs.exists != 'true'
|
|
241
241
|
id: crates-auth
|
|
242
|
-
uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18
|
|
242
|
+
uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
|
|
243
243
|
- if: steps.crate.outputs.exists != 'true'
|
|
244
244
|
run: cargo publish --locked
|
|
245
245
|
env:
|
|
@@ -258,18 +258,18 @@ jobs:
|
|
|
258
258
|
permissions:
|
|
259
259
|
id-token: write
|
|
260
260
|
steps:
|
|
261
|
-
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
|
|
261
|
+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
262
262
|
with:
|
|
263
263
|
pattern: wheel-*
|
|
264
264
|
path: dist
|
|
265
265
|
merge-multiple: true
|
|
266
|
-
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
|
|
266
|
+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
267
267
|
with:
|
|
268
268
|
name: source-distributions
|
|
269
269
|
path: source
|
|
270
270
|
- name: Collect Python distributions
|
|
271
271
|
run: find source -name '*.tar.gz' -exec cp {} dist/ \;
|
|
272
|
-
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
|
|
272
|
+
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
273
273
|
with:
|
|
274
274
|
packages-dir: dist
|
|
275
275
|
|
|
@@ -283,7 +283,7 @@ jobs:
|
|
|
283
283
|
with:
|
|
284
284
|
toolchain: 1.96.0
|
|
285
285
|
components: rustfmt, clippy
|
|
286
|
-
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
|
|
286
|
+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
|
287
287
|
with:
|
|
288
288
|
python-version: "3.x"
|
|
289
289
|
- name: Install and run the crates.io package
|
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.7](https://github.com/rvben/teams-cli/compare/v0.1.6...v0.1.7) - 2026-09-27
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **deps**: update rustls to 0.23.45 for RUSTSEC-2026-0285 ([d2338ff](https://github.com/rvben/teams-cli/commit/d2338ff8abf676501931c5ba7225097bb154f39c))
|
|
15
|
+
|
|
16
|
+
## [0.1.6](https://github.com/rvben/teams-cli/compare/v0.1.5...v0.1.6) - 2026-09-03
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **auth**: standardize authentication workflow ([bb0019f](https://github.com/rvben/teams-cli/commit/bb0019f0ceb86dbb963f3499348c58a95922081e))
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **auth**: tolerate missing Linux credential service ([7764383](https://github.com/rvben/teams-cli/commit/776438359f3c913d0514de921042d770c6a88bf0))
|
|
25
|
+
|
|
10
26
|
## [0.1.5](https://github.com/rvben/teams-cli/compare/v0.1.4...v0.1.5) - 2026-08-26
|
|
11
27
|
|
|
12
28
|
### Added
|
|
@@ -316,9 +316,9 @@ checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
|
|
316
316
|
|
|
317
317
|
[[package]]
|
|
318
318
|
name = "chacha20"
|
|
319
|
-
version = "0.10.
|
|
319
|
+
version = "0.10.2"
|
|
320
320
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
321
|
-
checksum = "
|
|
321
|
+
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
|
322
322
|
dependencies = [
|
|
323
323
|
"cfg-if",
|
|
324
324
|
"cpufeatures 0.3.0",
|
|
@@ -2557,9 +2557,9 @@ dependencies = [
|
|
|
2557
2557
|
|
|
2558
2558
|
[[package]]
|
|
2559
2559
|
name = "rustls"
|
|
2560
|
-
version = "0.23.
|
|
2560
|
+
version = "0.23.45"
|
|
2561
2561
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2562
|
-
checksum = "
|
|
2562
|
+
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
|
2563
2563
|
dependencies = [
|
|
2564
2564
|
"aws-lc-rs",
|
|
2565
2565
|
"once_cell",
|
|
@@ -2964,7 +2964,7 @@ dependencies = [
|
|
|
2964
2964
|
|
|
2965
2965
|
[[package]]
|
|
2966
2966
|
name = "teams-cli"
|
|
2967
|
-
version = "0.1.
|
|
2967
|
+
version = "0.1.7"
|
|
2968
2968
|
dependencies = [
|
|
2969
2969
|
"assert_cmd",
|
|
2970
2970
|
"base64 0.23.1",
|
|
@@ -2997,7 +2997,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
|
2997
2997
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
|
2998
2998
|
dependencies = [
|
|
2999
2999
|
"fastrand",
|
|
3000
|
-
"getrandom 0.4
|
|
3000
|
+
"getrandom 0.3.4",
|
|
3001
3001
|
"once_cell",
|
|
3002
3002
|
"rustix",
|
|
3003
3003
|
"windows-sys 0.61.2",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: teams-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
License-File: LICENSE
|
|
@@ -115,8 +115,14 @@ teams auth login --channel-history # once, with administrator approval
|
|
|
115
115
|
teams messages list --team TEAM_ID --channel CHANNEL_ID
|
|
116
116
|
teams messages send --chat CHAT_ID --body "On it."
|
|
117
117
|
printf 'Status update' | teams messages send --chat CHAT_ID --body -
|
|
118
|
+
teams auth status
|
|
119
|
+
teams auth status --offline
|
|
120
|
+
teams profile list
|
|
121
|
+
teams profile use work
|
|
122
|
+
teams profile remove old --yes
|
|
118
123
|
teams config show
|
|
119
|
-
teams
|
|
124
|
+
teams config path
|
|
125
|
+
teams doctor --offline
|
|
120
126
|
```
|
|
121
127
|
|
|
122
128
|
Collection commands expose `--limit` and `--fields`; Graph-native collections use `--cursor`, while joined teams and channels use `--offset` because those endpoints do not accept `$top`. With a terminal they render concise text; when stdout is piped they emit JSON envelopes with continuation metadata and `truncated`.
|
|
@@ -99,8 +99,14 @@ teams auth login --channel-history # once, with administrator approval
|
|
|
99
99
|
teams messages list --team TEAM_ID --channel CHANNEL_ID
|
|
100
100
|
teams messages send --chat CHAT_ID --body "On it."
|
|
101
101
|
printf 'Status update' | teams messages send --chat CHAT_ID --body -
|
|
102
|
+
teams auth status
|
|
103
|
+
teams auth status --offline
|
|
104
|
+
teams profile list
|
|
105
|
+
teams profile use work
|
|
106
|
+
teams profile remove old --yes
|
|
102
107
|
teams config show
|
|
103
|
-
teams
|
|
108
|
+
teams config path
|
|
109
|
+
teams doctor --offline
|
|
104
110
|
```
|
|
105
111
|
|
|
106
112
|
Collection commands expose `--limit` and `--fields`; Graph-native collections use `--cursor`, while joined teams and channels use `--offset` because those endpoints do not accept `$top`. With a terminal they render concise text; when stdout is piped they emit JSON envelopes with continuation metadata and `truncated`.
|
|
@@ -104,12 +104,29 @@ fn load(profile_name: &str) -> Result<TokenBundle, AppError> {
|
|
|
104
104
|
pub fn logout(profile_name: &str) -> Result<(), AppError> {
|
|
105
105
|
match entry(profile_name)?.delete_credential() {
|
|
106
106
|
Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
|
|
107
|
+
Err(e) if secret_service_is_unavailable(&e) => Ok(()),
|
|
107
108
|
Err(e) => Err(AppError::Unexpected(format!(
|
|
108
109
|
"could not remove stored credentials: {e}"
|
|
109
110
|
))),
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
#[cfg(target_os = "linux")]
|
|
115
|
+
fn secret_service_is_unavailable(error: &keyring::Error) -> bool {
|
|
116
|
+
match error {
|
|
117
|
+
keyring::Error::PlatformFailure(source) => {
|
|
118
|
+
let message = source.to_string();
|
|
119
|
+
message.contains("org.freedesktop.secrets") && message.contains("not provided")
|
|
120
|
+
}
|
|
121
|
+
_ => false,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#[cfg(not(target_os = "linux"))]
|
|
126
|
+
fn secret_service_is_unavailable(_error: &keyring::Error) -> bool {
|
|
127
|
+
false
|
|
128
|
+
}
|
|
129
|
+
|
|
113
130
|
pub async fn access_token(profile_name: &str, profile: &Profile) -> Result<String, AppError> {
|
|
114
131
|
if let Ok(token) = std::env::var("TEAMS_ACCESS_TOKEN") {
|
|
115
132
|
return Ok(token);
|
|
@@ -385,4 +402,14 @@ mod tests {
|
|
|
385
402
|
fn channel_history_is_explicitly_opted_in() {
|
|
386
403
|
assert!(scopes(true).contains(CHANNEL_HISTORY_SCOPE));
|
|
387
404
|
}
|
|
405
|
+
|
|
406
|
+
#[cfg(target_os = "linux")]
|
|
407
|
+
#[test]
|
|
408
|
+
fn missing_secret_service_is_treated_as_an_empty_store() {
|
|
409
|
+
let error = keyring::Error::PlatformFailure(Box::new(std::io::Error::other(
|
|
410
|
+
"The name org.freedesktop.secrets was not provided by any .service files",
|
|
411
|
+
)));
|
|
412
|
+
|
|
413
|
+
assert!(super::secret_service_is_unavailable(&error));
|
|
414
|
+
}
|
|
388
415
|
}
|
|
@@ -20,6 +20,9 @@ pub struct Cli {
|
|
|
20
20
|
/// Suppress progress and informational messages
|
|
21
21
|
#[arg(long, global = true)]
|
|
22
22
|
pub quiet: bool,
|
|
23
|
+
/// Skip confirmation prompts for destructive operations
|
|
24
|
+
#[arg(long, short = 'y', global = true)]
|
|
25
|
+
pub yes: bool,
|
|
23
26
|
/// Alias for --output json
|
|
24
27
|
#[arg(long, global = true, hide = true)]
|
|
25
28
|
pub json: bool,
|
|
@@ -36,6 +39,11 @@ pub enum Command {
|
|
|
36
39
|
#[command(subcommand)]
|
|
37
40
|
command: AuthCommand,
|
|
38
41
|
},
|
|
42
|
+
/// List, select, or remove configuration profiles
|
|
43
|
+
Profile {
|
|
44
|
+
#[command(subcommand)]
|
|
45
|
+
command: ProfileCommand,
|
|
46
|
+
},
|
|
39
47
|
/// Inspect resolved, secret-free configuration
|
|
40
48
|
Config {
|
|
41
49
|
#[command(subcommand)]
|
|
@@ -63,7 +71,11 @@ pub enum Command {
|
|
|
63
71
|
/// Open the keyboard-first terminal interface
|
|
64
72
|
Tui(TuiArgs),
|
|
65
73
|
/// Check configuration, credential storage, and Graph access
|
|
66
|
-
Doctor
|
|
74
|
+
Doctor {
|
|
75
|
+
/// Check local state without contacting Microsoft Graph
|
|
76
|
+
#[arg(long)]
|
|
77
|
+
offline: bool,
|
|
78
|
+
},
|
|
67
79
|
/// Describe supported and deliberately unsupported capabilities
|
|
68
80
|
Capabilities,
|
|
69
81
|
/// Emit the offline CLI Spec v0.3 contract
|
|
@@ -118,8 +130,22 @@ pub enum AuthCommand {
|
|
|
118
130
|
},
|
|
119
131
|
/// Remove locally stored credentials
|
|
120
132
|
Logout,
|
|
121
|
-
///
|
|
122
|
-
Status
|
|
133
|
+
/// Verify authentication and show the selected profile's status
|
|
134
|
+
Status {
|
|
135
|
+
/// Inspect local credential state without contacting Microsoft Graph
|
|
136
|
+
#[arg(long)]
|
|
137
|
+
offline: bool,
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[derive(Debug, Subcommand)]
|
|
142
|
+
pub enum ProfileCommand {
|
|
143
|
+
/// List configured profiles and identify the active one
|
|
144
|
+
List,
|
|
145
|
+
/// Select the default profile for future commands
|
|
146
|
+
Use { name: String },
|
|
147
|
+
/// Remove a profile and its stored credential
|
|
148
|
+
Remove { name: String },
|
|
123
149
|
}
|
|
124
150
|
|
|
125
151
|
#[derive(Debug, Subcommand)]
|
|
@@ -44,6 +44,15 @@ struct ConfigFile {
|
|
|
44
44
|
profiles: BTreeMap<String, Profile>,
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
#[derive(Debug, Serialize)]
|
|
48
|
+
pub struct ProfileSummary {
|
|
49
|
+
pub name: String,
|
|
50
|
+
pub active: bool,
|
|
51
|
+
pub tenant: String,
|
|
52
|
+
pub client_id: String,
|
|
53
|
+
pub read_only: bool,
|
|
54
|
+
}
|
|
55
|
+
|
|
47
56
|
pub fn path() -> PathBuf {
|
|
48
57
|
if let Some(base) = std::env::var_os("XDG_CONFIG_HOME").filter(|v| !v.is_empty()) {
|
|
49
58
|
PathBuf::from(base).join("teams").join("config.toml")
|
|
@@ -61,6 +70,11 @@ pub fn save(profile_name: &str, profile: Profile) -> Result<PathBuf, AppError> {
|
|
|
61
70
|
let mut config = read_file()?;
|
|
62
71
|
config.profiles.insert(profile_name.to_string(), profile);
|
|
63
72
|
config.active_profile = Some(profile_name.to_string());
|
|
73
|
+
write_file(&path, &config)?;
|
|
74
|
+
Ok(path)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn write_file(path: &std::path::Path, config: &ConfigFile) -> Result<(), AppError> {
|
|
64
78
|
let parent = path
|
|
65
79
|
.parent()
|
|
66
80
|
.ok_or_else(|| AppError::Unexpected("configuration path has no parent".into()))?;
|
|
@@ -77,8 +91,8 @@ pub fn save(profile_name: &str, profile: Profile) -> Result<PathBuf, AppError> {
|
|
|
77
91
|
use std::os::unix::fs::PermissionsExt;
|
|
78
92
|
std::fs::set_permissions(temp.path(), std::fs::Permissions::from_mode(0o600))?;
|
|
79
93
|
}
|
|
80
|
-
temp.persist(
|
|
81
|
-
Ok(
|
|
94
|
+
temp.persist(path).map_err(|error| error.error)?;
|
|
95
|
+
Ok(())
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
pub fn load(requested: Option<&str>) -> Result<(String, Profile), AppError> {
|
|
@@ -159,6 +173,46 @@ pub fn configured_profile(requested: Option<&str>) -> Option<(String, Profile)>
|
|
|
159
173
|
load(requested).ok()
|
|
160
174
|
}
|
|
161
175
|
|
|
176
|
+
pub fn profile_summaries() -> Result<Vec<ProfileSummary>, AppError> {
|
|
177
|
+
let config = read_file()?;
|
|
178
|
+
Ok(config
|
|
179
|
+
.profiles
|
|
180
|
+
.iter()
|
|
181
|
+
.map(|(name, profile)| ProfileSummary {
|
|
182
|
+
name: name.clone(),
|
|
183
|
+
active: config.active_profile.as_deref() == Some(name.as_str()),
|
|
184
|
+
tenant: profile.tenant.clone(),
|
|
185
|
+
client_id: profile.client_id.clone(),
|
|
186
|
+
read_only: profile.read_only,
|
|
187
|
+
})
|
|
188
|
+
.collect())
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
pub fn use_profile(name: &str) -> Result<(), AppError> {
|
|
192
|
+
let path = path();
|
|
193
|
+
let mut config = read_file()?;
|
|
194
|
+
if !config.profiles.contains_key(name) {
|
|
195
|
+
return Err(AppError::InvalidInput(format!(
|
|
196
|
+
"profile '{name}' is not configured"
|
|
197
|
+
)));
|
|
198
|
+
}
|
|
199
|
+
config.active_profile = Some(name.into());
|
|
200
|
+
write_file(&path, &config)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
pub fn remove_profile(name: &str) -> Result<bool, AppError> {
|
|
204
|
+
let path = path();
|
|
205
|
+
let mut config = read_file()?;
|
|
206
|
+
let removed = config.profiles.remove(name).is_some();
|
|
207
|
+
if removed {
|
|
208
|
+
if config.active_profile.as_deref() == Some(name) {
|
|
209
|
+
config.active_profile = config.profiles.keys().next().cloned();
|
|
210
|
+
}
|
|
211
|
+
write_file(&path, &config)?;
|
|
212
|
+
}
|
|
213
|
+
Ok(removed)
|
|
214
|
+
}
|
|
215
|
+
|
|
162
216
|
fn read_file() -> Result<ConfigFile, AppError> {
|
|
163
217
|
match std::fs::read_to_string(path()) {
|
|
164
218
|
Ok(body) => toml::from_str(&body)
|
|
@@ -7,7 +7,7 @@ use serde_json::Value;
|
|
|
7
7
|
use teams_cli::auth;
|
|
8
8
|
use teams_cli::cli::{
|
|
9
9
|
AuthCommand, ChannelsCommand, ChatsCommand, Cli, Command, ConfigCommand, InitArgs,
|
|
10
|
-
MessagesCommand, OffsetPageArgs, PageArgs, TuiArgs,
|
|
10
|
+
MessagesCommand, OffsetPageArgs, PageArgs, ProfileCommand, TuiArgs,
|
|
11
11
|
};
|
|
12
12
|
use teams_cli::config::{self, Profile};
|
|
13
13
|
use teams_cli::error::AppError;
|
|
@@ -51,6 +51,7 @@ async fn main() {
|
|
|
51
51
|
|
|
52
52
|
async fn dispatch(cli: Cli, out: Output) -> Result<(), AppError> {
|
|
53
53
|
let profile_arg = cli.profile.as_deref();
|
|
54
|
+
let yes = cli.yes;
|
|
54
55
|
let command = match cli.command {
|
|
55
56
|
Some(command) => command,
|
|
56
57
|
None if io::stdin().is_terminal() && io::stdout().is_terminal() && !config::exists() => {
|
|
@@ -78,6 +79,7 @@ async fn dispatch(cli: Cli, out: Output) -> Result<(), AppError> {
|
|
|
78
79
|
match command {
|
|
79
80
|
Command::Init(args) => init(profile_arg.unwrap_or("default"), args, out).await,
|
|
80
81
|
Command::Auth { command } => auth_command(profile_arg, command, out).await,
|
|
82
|
+
Command::Profile { command } => profile_command(command, yes, out),
|
|
81
83
|
Command::Config { command } => config_command(profile_arg, command, out),
|
|
82
84
|
Command::Whoami => {
|
|
83
85
|
let client = client(profile_arg).await?;
|
|
@@ -154,7 +156,7 @@ async fn dispatch(cli: Cli, out: Output) -> Result<(), AppError> {
|
|
|
154
156
|
})
|
|
155
157
|
}
|
|
156
158
|
Command::Tui(args) => tui_command(profile_arg, args).await,
|
|
157
|
-
Command::Doctor => doctor(profile_arg, out).await,
|
|
159
|
+
Command::Doctor { offline } => doctor(profile_arg, offline, out).await,
|
|
158
160
|
Command::Capabilities => capabilities(out),
|
|
159
161
|
Command::Schema { command } => {
|
|
160
162
|
let value = schema::generate(command.as_deref());
|
|
@@ -307,7 +309,7 @@ async fn auth_command(
|
|
|
307
309
|
let value = serde_json::json!({"profile":name,"signed_in":false});
|
|
308
310
|
out.value(&value, || format!("Signed out profile '{name}'."))
|
|
309
311
|
}
|
|
310
|
-
AuthCommand::Status => {
|
|
312
|
+
AuthCommand::Status { offline } => {
|
|
311
313
|
let configured = config::configured_profile(profile_arg);
|
|
312
314
|
let name = configured
|
|
313
315
|
.as_ref()
|
|
@@ -316,7 +318,12 @@ async fn auth_command(
|
|
|
316
318
|
let signed_in = auth::has_token(name);
|
|
317
319
|
let scopes = auth::granted_scopes(name);
|
|
318
320
|
let channel_history = auth::channel_history_granted(name);
|
|
319
|
-
let
|
|
321
|
+
let identity = if !offline && configured.is_some() && signed_in {
|
|
322
|
+
Some(client(Some(name)).await?.me().await?)
|
|
323
|
+
} else {
|
|
324
|
+
None
|
|
325
|
+
};
|
|
326
|
+
let value = serde_json::json!({"profile":name,"configured":configured.is_some(),"signed_in":signed_in,"verified":identity.is_some(),"identity":identity,"config_path":config::path(),"read_only":configured.as_ref().is_some_and(|(_, profile)| profile.read_only),"granted_scopes":scopes,"channel_history":channel_history});
|
|
320
327
|
out.value(&value, || {
|
|
321
328
|
format!(
|
|
322
329
|
"Profile: {name}\nConfigured: {}\nSigned in: {}\nRead only: {}\nChannel history: {}\nConfig: {}",
|
|
@@ -331,6 +338,59 @@ async fn auth_command(
|
|
|
331
338
|
}
|
|
332
339
|
}
|
|
333
340
|
|
|
341
|
+
fn profile_command(command: ProfileCommand, yes: bool, out: Output) -> Result<(), AppError> {
|
|
342
|
+
match command {
|
|
343
|
+
ProfileCommand::List => {
|
|
344
|
+
let profiles = config::profile_summaries()?;
|
|
345
|
+
out.value(
|
|
346
|
+
&serde_json::json!({"items": profiles, "total": profiles.len()}),
|
|
347
|
+
|| {
|
|
348
|
+
if profiles.is_empty() {
|
|
349
|
+
"No profiles configured. Run `teams init`.".into()
|
|
350
|
+
} else {
|
|
351
|
+
profiles
|
|
352
|
+
.iter()
|
|
353
|
+
.map(|profile| {
|
|
354
|
+
format!(
|
|
355
|
+
"{} {} {}",
|
|
356
|
+
if profile.active { "*" } else { " " },
|
|
357
|
+
profile.name,
|
|
358
|
+
profile.tenant
|
|
359
|
+
)
|
|
360
|
+
})
|
|
361
|
+
.collect::<Vec<_>>()
|
|
362
|
+
.join("\n")
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
ProfileCommand::Use { name } => {
|
|
368
|
+
config::use_profile(&name)?;
|
|
369
|
+
out.value(
|
|
370
|
+
&serde_json::json!({"profile": name, "active": true}),
|
|
371
|
+
|| format!("Active profile set to '{name}'."),
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
ProfileCommand::Remove { name } => {
|
|
375
|
+
if !yes {
|
|
376
|
+
return Err(AppError::InvalidInput(
|
|
377
|
+
"profile removal requires --yes".into(),
|
|
378
|
+
));
|
|
379
|
+
}
|
|
380
|
+
auth::logout(&name)?;
|
|
381
|
+
if !config::remove_profile(&name)? {
|
|
382
|
+
return Err(AppError::InvalidInput(format!(
|
|
383
|
+
"profile '{name}' is not configured"
|
|
384
|
+
)));
|
|
385
|
+
}
|
|
386
|
+
out.value(
|
|
387
|
+
&serde_json::json!({"profile": name, "removed": true}),
|
|
388
|
+
|| format!("Removed profile '{name}'."),
|
|
389
|
+
)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
334
394
|
async fn client(profile_arg: Option<&str>) -> Result<GraphClient, AppError> {
|
|
335
395
|
let (name, profile) = config::load(profile_arg)?;
|
|
336
396
|
let token = auth::access_token(&name, &profile).await?;
|
|
@@ -541,7 +601,7 @@ struct Check {
|
|
|
541
601
|
detail: String,
|
|
542
602
|
}
|
|
543
603
|
|
|
544
|
-
async fn doctor(profile_arg: Option<&str>, out: Output) -> Result<(), AppError> {
|
|
604
|
+
async fn doctor(profile_arg: Option<&str>, offline: bool, out: Output) -> Result<(), AppError> {
|
|
545
605
|
let mut checks = Vec::new();
|
|
546
606
|
let configured = config::configured_profile(profile_arg);
|
|
547
607
|
checks.push(Check {
|
|
@@ -593,7 +653,7 @@ async fn doctor(profile_arg: Option<&str>, out: Output) -> Result<(), AppError>
|
|
|
593
653
|
});
|
|
594
654
|
let mut identity_ok = false;
|
|
595
655
|
let mut teams_ok = false;
|
|
596
|
-
let (identity_detail, teams_detail) = if configured.is_some() && signed_in {
|
|
656
|
+
let (identity_detail, teams_detail) = if !offline && configured.is_some() && signed_in {
|
|
597
657
|
match client(profile_arg).await {
|
|
598
658
|
Ok(client) => {
|
|
599
659
|
let identity_detail = match client.me().await {
|
|
@@ -623,21 +683,38 @@ async fn doctor(profile_arg: Option<&str>, out: Output) -> Result<(), AppError>
|
|
|
623
683
|
}
|
|
624
684
|
}
|
|
625
685
|
} else {
|
|
626
|
-
let detail =
|
|
686
|
+
let detail = if offline {
|
|
687
|
+
"network check skipped"
|
|
688
|
+
} else {
|
|
689
|
+
"skipped until setup is complete"
|
|
690
|
+
}
|
|
691
|
+
.to_string();
|
|
627
692
|
(detail.clone(), detail)
|
|
628
693
|
};
|
|
629
694
|
checks.push(Check {
|
|
630
695
|
name: "microsoft_identity",
|
|
631
|
-
status: if
|
|
696
|
+
status: if offline {
|
|
697
|
+
"skip"
|
|
698
|
+
} else if identity_ok {
|
|
699
|
+
"pass"
|
|
700
|
+
} else {
|
|
701
|
+
"fail"
|
|
702
|
+
},
|
|
632
703
|
detail: identity_detail,
|
|
633
704
|
});
|
|
634
705
|
checks.push(Check {
|
|
635
706
|
name: "microsoft_teams",
|
|
636
|
-
status: if
|
|
707
|
+
status: if offline {
|
|
708
|
+
"skip"
|
|
709
|
+
} else if teams_ok {
|
|
710
|
+
"pass"
|
|
711
|
+
} else {
|
|
712
|
+
"fail"
|
|
713
|
+
},
|
|
637
714
|
detail: teams_detail,
|
|
638
715
|
});
|
|
639
716
|
let healthy = checks.iter().all(|check| check.status != "fail");
|
|
640
|
-
let value = serde_json::json!({"checks":checks,"healthy":healthy});
|
|
717
|
+
let value = serde_json::json!({"checks":checks,"healthy":healthy,"offline":offline});
|
|
641
718
|
out.value(&value, || {
|
|
642
719
|
checks
|
|
643
720
|
.iter()
|
|
@@ -130,20 +130,57 @@ pub fn generate(command_filter: Option<&str>) -> Value {
|
|
|
130
130
|
"idempotent",
|
|
131
131
|
vec![field("profile", "string"), field("signed_in", "boolean")],
|
|
132
132
|
),
|
|
133
|
+
{
|
|
134
|
+
let mut value = single(
|
|
135
|
+
"auth status",
|
|
136
|
+
"Verify authentication and inspect credential state",
|
|
137
|
+
"read_only",
|
|
138
|
+
vec![
|
|
139
|
+
field("profile", "string"),
|
|
140
|
+
field("configured", "boolean"),
|
|
141
|
+
field("signed_in", "boolean"),
|
|
142
|
+
field("verified", "boolean"),
|
|
143
|
+
nullable_field("identity", "object"),
|
|
144
|
+
field("config_path", "string"),
|
|
145
|
+
field("read_only", "boolean"),
|
|
146
|
+
nullable_array_field("granted_scopes", "string"),
|
|
147
|
+
nullable_field("channel_history", "boolean"),
|
|
148
|
+
],
|
|
149
|
+
);
|
|
150
|
+
value["args"] = json!([arg(
|
|
151
|
+
"--offline",
|
|
152
|
+
"boolean",
|
|
153
|
+
"Inspect local credential state without contacting Microsoft Graph"
|
|
154
|
+
)]);
|
|
155
|
+
value
|
|
156
|
+
},
|
|
133
157
|
single(
|
|
134
|
-
"
|
|
135
|
-
"
|
|
158
|
+
"profile list",
|
|
159
|
+
"List configured profiles and identify the active one",
|
|
136
160
|
"read_only",
|
|
137
|
-
vec![
|
|
138
|
-
field("profile", "string"),
|
|
139
|
-
field("configured", "boolean"),
|
|
140
|
-
field("signed_in", "boolean"),
|
|
141
|
-
field("config_path", "string"),
|
|
142
|
-
field("read_only", "boolean"),
|
|
143
|
-
nullable_array_field("granted_scopes", "string"),
|
|
144
|
-
nullable_field("channel_history", "boolean"),
|
|
145
|
-
],
|
|
161
|
+
vec![array_field("items", "object"), field("total", "integer")],
|
|
146
162
|
),
|
|
163
|
+
{
|
|
164
|
+
let mut value = single(
|
|
165
|
+
"profile use",
|
|
166
|
+
"Select the default profile for future commands",
|
|
167
|
+
"idempotent",
|
|
168
|
+
vec![field("profile", "string"), field("active", "boolean")],
|
|
169
|
+
);
|
|
170
|
+
value["args"] = json!([required_arg("name", "string", "Profile name")]);
|
|
171
|
+
value
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
let mut value = single(
|
|
175
|
+
"profile remove",
|
|
176
|
+
"Remove a profile and its stored credential",
|
|
177
|
+
"idempotent",
|
|
178
|
+
vec![field("profile", "string"), field("removed", "boolean")],
|
|
179
|
+
);
|
|
180
|
+
value["args"] = json!([required_arg("name", "string", "Profile name")]);
|
|
181
|
+
value["confirmation_bypass_arg"] = json!("--yes");
|
|
182
|
+
value
|
|
183
|
+
},
|
|
147
184
|
single(
|
|
148
185
|
"config show",
|
|
149
186
|
"Print the resolved profile without credentials",
|
|
@@ -246,12 +283,20 @@ pub fn generate(command_filter: Option<&str>) -> Value {
|
|
|
246
283
|
value
|
|
247
284
|
},
|
|
248
285
|
json!({"name":"tui", "description":"Open the keyboard-first terminal interface", "effects":"read_only", "output_kind":"opaque", "media_type":"text/plain", "requires_tty":true, "args":[arg("--demo", "boolean", "Use deterministic sample data"), arg("--snapshot", "boolean", "Print one ANSI-free demo frame")]}),
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
286
|
+
{
|
|
287
|
+
let mut value = single(
|
|
288
|
+
"doctor",
|
|
289
|
+
"Check configuration, credential storage, Microsoft identity, and Teams provisioning",
|
|
290
|
+
"read_only",
|
|
291
|
+
vec![array_field("checks", "object"), field("healthy", "boolean")],
|
|
292
|
+
);
|
|
293
|
+
value["args"] = json!([arg(
|
|
294
|
+
"--offline",
|
|
295
|
+
"boolean",
|
|
296
|
+
"Check local state without contacting Microsoft Graph"
|
|
297
|
+
)]);
|
|
298
|
+
value
|
|
299
|
+
},
|
|
255
300
|
single(
|
|
256
301
|
"capabilities",
|
|
257
302
|
"Describe supported and deliberately unsupported capabilities",
|
|
@@ -287,7 +332,8 @@ pub fn generate(command_filter: Option<&str>) -> Value {
|
|
|
287
332
|
"global_args": [
|
|
288
333
|
{"name":"--output", "short":"-o", "type":"string", "enum":["auto","text","json"], "default":"auto", "description":"Output format"},
|
|
289
334
|
{"name":"--profile", "type":"string", "description":"Configuration profile"},
|
|
290
|
-
{"name":"--quiet", "type":"boolean", "description":"Suppress informational stderr output"}
|
|
335
|
+
{"name":"--quiet", "type":"boolean", "description":"Suppress informational stderr output"},
|
|
336
|
+
{"name":"--yes", "short":"-y", "type":"boolean", "description":"Skip confirmation prompts for destructive operations"}
|
|
291
337
|
],
|
|
292
338
|
"commands": commands,
|
|
293
339
|
"errors": error::ALL.iter().map(|e| json!({"kind":e.kind,"exit_code":e.exit_code,"retryable":e.retryable,"description":e.description})).collect::<Vec<_>>(),
|
|
@@ -142,6 +142,58 @@ fn init_can_create_a_read_only_profile_and_config_show_resolves_it() {
|
|
|
142
142
|
.stdout(predicate::str::contains("\"read_only\": true"));
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
#[test]
|
|
146
|
+
fn profile_lifecycle_and_offline_auth_status_are_local() {
|
|
147
|
+
let temp = tempfile::tempdir().unwrap();
|
|
148
|
+
for name in ["work", "sandbox"] {
|
|
149
|
+
Command::cargo_bin("teams")
|
|
150
|
+
.unwrap()
|
|
151
|
+
.env("XDG_CONFIG_HOME", temp.path())
|
|
152
|
+
.args(["--profile", name, "init", "--no-login"])
|
|
153
|
+
.assert()
|
|
154
|
+
.success();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
Command::cargo_bin("teams")
|
|
158
|
+
.unwrap()
|
|
159
|
+
.env("XDG_CONFIG_HOME", temp.path())
|
|
160
|
+
.args(["profile", "use", "work"])
|
|
161
|
+
.assert()
|
|
162
|
+
.success();
|
|
163
|
+
|
|
164
|
+
let list = Command::cargo_bin("teams")
|
|
165
|
+
.unwrap()
|
|
166
|
+
.env("XDG_CONFIG_HOME", temp.path())
|
|
167
|
+
.args(["--output", "json", "profile", "list"])
|
|
168
|
+
.output()
|
|
169
|
+
.unwrap();
|
|
170
|
+
assert!(list.status.success());
|
|
171
|
+
let value: Value = serde_json::from_slice(&list.stdout).unwrap();
|
|
172
|
+
assert_eq!(value["total"], 2);
|
|
173
|
+
assert!(
|
|
174
|
+
value["items"]
|
|
175
|
+
.as_array()
|
|
176
|
+
.unwrap()
|
|
177
|
+
.iter()
|
|
178
|
+
.any(|profile| { profile["name"] == "work" && profile["active"] == true })
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
Command::cargo_bin("teams")
|
|
182
|
+
.unwrap()
|
|
183
|
+
.env("XDG_CONFIG_HOME", temp.path())
|
|
184
|
+
.args(["auth", "status", "--offline"])
|
|
185
|
+
.assert()
|
|
186
|
+
.success()
|
|
187
|
+
.stdout(predicate::str::contains("\"verified\": false"));
|
|
188
|
+
|
|
189
|
+
Command::cargo_bin("teams")
|
|
190
|
+
.unwrap()
|
|
191
|
+
.env("XDG_CONFIG_HOME", temp.path())
|
|
192
|
+
.args(["profile", "remove", "sandbox", "--yes"])
|
|
193
|
+
.assert()
|
|
194
|
+
.success();
|
|
195
|
+
}
|
|
196
|
+
|
|
145
197
|
#[test]
|
|
146
198
|
fn suite_exit_codes_and_error_kinds_are_stable() {
|
|
147
199
|
let schema: Value = serde_json::from_slice(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|