hiringcafe-cli 0.1.4__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 chen-gdp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: hiringcafe-cli
3
+ Version: 0.1.4
4
+ Summary: Spec-driven Python CLI for Hiring Cafe job search and saved-job tracking
5
+ Author: chen-gdp
6
+ Author-email: chen-gdp <146709874+chen-gdp@users.noreply.github.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Requires-Python: >=3.11
18
+ Project-URL: Homepage, https://github.com/chen-gdp/hiringcafe-cli
19
+ Project-URL: Repository, https://github.com/chen-gdp/hiringcafe-cli
20
+ Project-URL: Issues, https://github.com/chen-gdp/hiringcafe-cli/issues
21
+ Project-URL: Changelog, https://github.com/chen-gdp/hiringcafe-cli/blob/main/CHANGELOG.md
22
+ Description-Content-Type: text/markdown
23
+
24
+ # HiringCafe CLI
25
+
26
+ Give your AI agent Hiring Cafe superpowers: search jobs, inspect postings, and keep your Hiring Cafe saved-job board in sync without a browser.
27
+
28
+ ```bash
29
+ hiringcafe search "python developer" --pages 3 --jsonl
30
+ ```
31
+
32
+ The result is stable JSON/JSONL, explicit exit codes, cached anonymous reads, bounded retries, and authenticated actions against your real Hiring Cafe account.
33
+
34
+ > **Unofficial client.** This project is not affiliated with, endorsed by, or supported by Hiring Cafe. Use your own account, respect the service, and expect upstream APIs to change.
35
+
36
+ ## For agents
37
+
38
+ Agents can install and verify the CLI in three commands:
39
+
40
+ ```bash
41
+ uv tool install hiringcafe-cli
42
+ hiringcafe search "python developer" --page 0 --json
43
+ ```
44
+
45
+ For persistent installations, use `uv tool install hiringcafe-cli` or `pipx install hiringcafe-cli`.
46
+
47
+ Feed the output directly to your tooling:
48
+
49
+ ```bash
50
+ # One JSON object per page
51
+ hiringcafe search "python developer" --pages 3 --jsonl
52
+
53
+ # One line per job
54
+ hiringcafe search "python developer" --pages 3 --json | jq -c '.jobs[]'
55
+
56
+ # Machine-readable result count
57
+ hiringcafe count "python developer" --json
58
+ ```
59
+
60
+ Import an existing Firebase refresh token without an interactive password prompt:
61
+
62
+ ```bash
63
+ printf '%s\n' "$HIRINGCAFE_REFRESH_TOKEN" | hiringcafe auth import
64
+ hiringcafe saved-jobs list --json
65
+ ```
66
+
67
+ Safe automation contract:
68
+
69
+ - JSON success output goes to stdout; cache diagnostics go to stderr.
70
+ - Stable exit codes describe transport, input, rate-limit, auth, and conflict outcomes.
71
+ - Passwords and Firebase ID tokens are never written to disk.
72
+ - Authenticated mutations are never retried or cached.
73
+ - Anonymous search/count responses use a 15-minute cache; job details use 24 hours.
74
+
75
+ ## For humans
76
+
77
+ ```bash
78
+ hiringcafe auth login --email you@example.com
79
+ hiringcafe search "python developer" --page 0 --json
80
+ hiringcafe job show <objectID> --json
81
+ hiringcafe saved-jobs list --json
82
+ hiringcafe auth logout
83
+ ```
84
+
85
+ The password is read through hidden input, exchanged once, and discarded.
86
+
87
+ ## Saved jobs
88
+
89
+ These commands modify your real Hiring Cafe account. Review object IDs and confirmation prompts before changing data.
90
+
91
+ ```bash
92
+ hiringcafe saved-jobs save <objectID> --stage saved
93
+ hiringcafe saved-jobs stage <objectID> --stage interviewing
94
+ hiringcafe saved-jobs remove <objectID> --yes
95
+ ```
96
+
97
+ Valid stages are `saved`, `applied`, `interviewing`, `offer`, `rejected`, `ghosted`, and `hidden`.
98
+
99
+ ## Local data
100
+
101
+ | Data | Location | Notes |
102
+ |---|---|---|
103
+ | Refresh token | `~/.config/hiringcafe-cli/session.json` | owner-only file; removed by logout |
104
+ | Anonymous cache | `~/.cache/hiringcafe-cli/cache/v1/` | public search/job data only |
105
+
106
+ Set `HIRINGCAFE_CONFIG_HOME` or `HIRINGCAFE_CACHE_HOME` to use different roots.
107
+
108
+ ```bash
109
+ hiringcafe search "python developer" --no-cache
110
+ hiringcafe search "python developer" --cache-ttl 3600
111
+ hiringcafe cache purge
112
+ ```
113
+
114
+ ## Exit codes
115
+
116
+ | Code | Agent action |
117
+ |---:|---|
118
+ | `0` | success |
119
+ | `1` | transport, HTTP, parsing, or API error; inspect stderr |
120
+ | `2` | invalid arguments or search state; fix the request |
121
+ | `3` | rate limited; wait and retry |
122
+ | `4` | no stored credential; authenticate first |
123
+ | `5` | stored credential rejected; re-authenticate |
124
+ | `6` | write conflict or prerequisite missing; refresh state |
125
+
126
+ ## Development
127
+
128
+ Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).
129
+
130
+ ```bash
131
+ uv sync
132
+ uv run pytest
133
+ uv run hiringcafe --help
134
+ uv run pre-commit run --all-files
135
+ ```
136
+
137
+ Normal tests do not access the network. The verified anonymous endpoint probe is opt-in:
138
+
139
+ ```bash
140
+ LIVE=1 uv run hiringcafe probe
141
+ ```
142
+
143
+ Every implementation change traces to `specs/pr-*/spec.md` and the roadmap. Never commit credentials, Firebase tokens, cookies, real UIDs, emails, or private account data.
144
+
145
+ Beta status: search, job details, authentication, and saved-job workflows are implemented. Browser OAuth login remains intentionally deferred. See [`roadmap.md`](roadmap.md) and [`specs/`](specs/).
@@ -0,0 +1,122 @@
1
+ # HiringCafe CLI
2
+
3
+ Give your AI agent Hiring Cafe superpowers: search jobs, inspect postings, and keep your Hiring Cafe saved-job board in sync without a browser.
4
+
5
+ ```bash
6
+ hiringcafe search "python developer" --pages 3 --jsonl
7
+ ```
8
+
9
+ The result is stable JSON/JSONL, explicit exit codes, cached anonymous reads, bounded retries, and authenticated actions against your real Hiring Cafe account.
10
+
11
+ > **Unofficial client.** This project is not affiliated with, endorsed by, or supported by Hiring Cafe. Use your own account, respect the service, and expect upstream APIs to change.
12
+
13
+ ## For agents
14
+
15
+ Agents can install and verify the CLI in three commands:
16
+
17
+ ```bash
18
+ uv tool install hiringcafe-cli
19
+ hiringcafe search "python developer" --page 0 --json
20
+ ```
21
+
22
+ For persistent installations, use `uv tool install hiringcafe-cli` or `pipx install hiringcafe-cli`.
23
+
24
+ Feed the output directly to your tooling:
25
+
26
+ ```bash
27
+ # One JSON object per page
28
+ hiringcafe search "python developer" --pages 3 --jsonl
29
+
30
+ # One line per job
31
+ hiringcafe search "python developer" --pages 3 --json | jq -c '.jobs[]'
32
+
33
+ # Machine-readable result count
34
+ hiringcafe count "python developer" --json
35
+ ```
36
+
37
+ Import an existing Firebase refresh token without an interactive password prompt:
38
+
39
+ ```bash
40
+ printf '%s\n' "$HIRINGCAFE_REFRESH_TOKEN" | hiringcafe auth import
41
+ hiringcafe saved-jobs list --json
42
+ ```
43
+
44
+ Safe automation contract:
45
+
46
+ - JSON success output goes to stdout; cache diagnostics go to stderr.
47
+ - Stable exit codes describe transport, input, rate-limit, auth, and conflict outcomes.
48
+ - Passwords and Firebase ID tokens are never written to disk.
49
+ - Authenticated mutations are never retried or cached.
50
+ - Anonymous search/count responses use a 15-minute cache; job details use 24 hours.
51
+
52
+ ## For humans
53
+
54
+ ```bash
55
+ hiringcafe auth login --email you@example.com
56
+ hiringcafe search "python developer" --page 0 --json
57
+ hiringcafe job show <objectID> --json
58
+ hiringcafe saved-jobs list --json
59
+ hiringcafe auth logout
60
+ ```
61
+
62
+ The password is read through hidden input, exchanged once, and discarded.
63
+
64
+ ## Saved jobs
65
+
66
+ These commands modify your real Hiring Cafe account. Review object IDs and confirmation prompts before changing data.
67
+
68
+ ```bash
69
+ hiringcafe saved-jobs save <objectID> --stage saved
70
+ hiringcafe saved-jobs stage <objectID> --stage interviewing
71
+ hiringcafe saved-jobs remove <objectID> --yes
72
+ ```
73
+
74
+ Valid stages are `saved`, `applied`, `interviewing`, `offer`, `rejected`, `ghosted`, and `hidden`.
75
+
76
+ ## Local data
77
+
78
+ | Data | Location | Notes |
79
+ |---|---|---|
80
+ | Refresh token | `~/.config/hiringcafe-cli/session.json` | owner-only file; removed by logout |
81
+ | Anonymous cache | `~/.cache/hiringcafe-cli/cache/v1/` | public search/job data only |
82
+
83
+ Set `HIRINGCAFE_CONFIG_HOME` or `HIRINGCAFE_CACHE_HOME` to use different roots.
84
+
85
+ ```bash
86
+ hiringcafe search "python developer" --no-cache
87
+ hiringcafe search "python developer" --cache-ttl 3600
88
+ hiringcafe cache purge
89
+ ```
90
+
91
+ ## Exit codes
92
+
93
+ | Code | Agent action |
94
+ |---:|---|
95
+ | `0` | success |
96
+ | `1` | transport, HTTP, parsing, or API error; inspect stderr |
97
+ | `2` | invalid arguments or search state; fix the request |
98
+ | `3` | rate limited; wait and retry |
99
+ | `4` | no stored credential; authenticate first |
100
+ | `5` | stored credential rejected; re-authenticate |
101
+ | `6` | write conflict or prerequisite missing; refresh state |
102
+
103
+ ## Development
104
+
105
+ Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).
106
+
107
+ ```bash
108
+ uv sync
109
+ uv run pytest
110
+ uv run hiringcafe --help
111
+ uv run pre-commit run --all-files
112
+ ```
113
+
114
+ Normal tests do not access the network. The verified anonymous endpoint probe is opt-in:
115
+
116
+ ```bash
117
+ LIVE=1 uv run hiringcafe probe
118
+ ```
119
+
120
+ Every implementation change traces to `specs/pr-*/spec.md` and the roadmap. Never commit credentials, Firebase tokens, cookies, real UIDs, emails, or private account data.
121
+
122
+ Beta status: search, job details, authentication, and saved-job workflows are implemented. Browser OAuth login remains intentionally deferred. See [`roadmap.md`](roadmap.md) and [`specs/`](specs/).
@@ -0,0 +1,74 @@
1
+ [project]
2
+ name = "hiringcafe-cli"
3
+ version = "0.1.4"
4
+ description = "Spec-driven Python CLI for Hiring Cafe job search and saved-job tracking"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.11"
9
+ dependencies = []
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Environment :: Console",
13
+ "Intended Audience :: Developers",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ "Programming Language :: Python :: 3.14",
19
+ ]
20
+
21
+ [[project.authors]]
22
+ name = "chen-gdp"
23
+ email = "146709874+chen-gdp@users.noreply.github.com"
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/chen-gdp/hiringcafe-cli"
27
+ Repository = "https://github.com/chen-gdp/hiringcafe-cli"
28
+ Issues = "https://github.com/chen-gdp/hiringcafe-cli/issues"
29
+ Changelog = "https://github.com/chen-gdp/hiringcafe-cli/blob/main/CHANGELOG.md"
30
+
31
+ [project.scripts]
32
+ hiringcafe = "hiringcafe_cli:main"
33
+
34
+ [dependency-groups]
35
+ dev = [
36
+ "pre-commit>=4.3.0",
37
+ "pytest>=9.0.0",
38
+ "ruff>=0.14.0",
39
+ ]
40
+
41
+ [build-system]
42
+ requires = ["uv_build>=0.11.16,<0.12.0"]
43
+ build-backend = "uv_build"
44
+
45
+ [tool.ruff]
46
+ line-length = 120
47
+ target-version = "py311"
48
+ exclude = [
49
+ ".venv",
50
+ "dist",
51
+ "__pycache__",
52
+ ]
53
+
54
+ [tool.ruff.lint]
55
+ select = [
56
+ "E",
57
+ "F",
58
+ "I",
59
+ "B",
60
+ "UP",
61
+ "S",
62
+ ]
63
+ ignore = [
64
+ "S310",
65
+ "S311",
66
+ "UP017",
67
+ ]
68
+
69
+ [tool.ruff.lint.per-file-ignores]
70
+ "tests/*" = [
71
+ "S101",
72
+ "S105",
73
+ "S106",
74
+ ]
@@ -0,0 +1,62 @@
1
+ [project]
2
+ name = "hiringcafe-cli"
3
+ version = "0.1.4"
4
+ description = "Spec-driven Python CLI for Hiring Cafe job search and saved-job tracking"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "chen-gdp", email = "146709874+chen-gdp@users.noreply.github.com" }
10
+ ]
11
+ requires-python = ">=3.11"
12
+ dependencies = []
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Environment :: Console",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Programming Language :: Python :: 3.14",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/chen-gdp/hiringcafe-cli"
26
+ Repository = "https://github.com/chen-gdp/hiringcafe-cli"
27
+ Issues = "https://github.com/chen-gdp/hiringcafe-cli/issues"
28
+ Changelog = "https://github.com/chen-gdp/hiringcafe-cli/blob/main/CHANGELOG.md"
29
+
30
+ [dependency-groups]
31
+ dev = [
32
+ "pre-commit>=4.3.0",
33
+ "pytest>=9.0.0",
34
+ "ruff>=0.14.0",
35
+ ]
36
+
37
+ [project.scripts]
38
+ hiringcafe = "hiringcafe_cli:main"
39
+
40
+ [build-system]
41
+ requires = ["uv_build>=0.11.16,<0.12.0"]
42
+ build-backend = "uv_build"
43
+
44
+ [tool.ruff]
45
+ line-length = 120
46
+ target-version = "py311"
47
+ exclude = [".venv", "dist", "__pycache__"]
48
+
49
+ [tool.ruff.lint]
50
+ select = ["E", "F", "I", "B", "UP", "S"]
51
+ ignore = [
52
+ "S310", # all transport URLs are fixed HTTPS constants or verified site URLs
53
+ "S311", # random jitter is for retry timing, not cryptography
54
+ "UP017", # timezone.utc is the Python 3.11-compatible spelling
55
+ ]
56
+
57
+ [tool.ruff.lint.per-file-ignores]
58
+ "tests/*" = [
59
+ "S101", # assertions are the project's test contract
60
+ "S105", # synthetic credential names in isolated tests
61
+ "S106", # synthetic credential values in isolated tests
62
+ ]
@@ -0,0 +1,152 @@
1
+ """HiringCafe CLI package interface."""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from hiringcafe_cli import (
7
+ auth,
8
+ count,
9
+ job,
10
+ probe,
11
+ saved_jobs_list,
12
+ saved_jobs_remove,
13
+ saved_jobs_save,
14
+ saved_jobs_stage,
15
+ search,
16
+ )
17
+ from hiringcafe_cli.cache import CacheStore, active_cache_root
18
+ from hiringcafe_cli.firebase import FirebaseAuthError, FirebaseTransportError
19
+ from hiringcafe_cli.http_client import HireCafeHTTPError, HireCafeTransportError
20
+ from hiringcafe_cli.saved_jobs import VALID_STAGES
21
+ from hiringcafe_cli.saved_jobs_list import FirestoreError
22
+ from hiringcafe_cli.search_state import InvalidSearchState
23
+
24
+ __version__ = "0.1.0"
25
+
26
+ EXIT_CONTRACT = """\
27
+ Exit codes:
28
+ 0 success
29
+ 1 transport, HTTP, parsing, or API error
30
+ 2 invalid arguments or search state
31
+ 3 rate limited
32
+ 4 no stored credential
33
+ 5 stored credential rejected
34
+ 6 write conflict or prerequisite missing
35
+ """
36
+
37
+
38
+ def build_parser() -> argparse.ArgumentParser:
39
+ """Build the root CLI parser.
40
+
41
+ Returns:
42
+ An argparse parser implementing the PR 0.1 bootstrap contract.
43
+ """
44
+ parser = argparse.ArgumentParser(
45
+ prog="hiringcafe",
46
+ description="Hiring Cafe command-line interface",
47
+ epilog=EXIT_CONTRACT,
48
+ formatter_class=argparse.RawDescriptionHelpFormatter,
49
+ )
50
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
51
+ cache_options = argparse.ArgumentParser(add_help=False)
52
+ cache_options.add_argument("--no-cache", action="store_true")
53
+ cache_options.add_argument("--cache-ttl", type=int)
54
+ subparsers = parser.add_subparsers(dest="command")
55
+ probe_parser = subparsers.add_parser("probe", help="run the opt-in live endpoint probe")
56
+ probe_parser.set_defaults(handler=probe.main)
57
+ search_parser = subparsers.add_parser("search", help="search jobs", parents=[cache_options])
58
+ search_parser.add_argument("query", nargs="?", default="")
59
+ search_parser.add_argument("--page", type=int, default=0)
60
+ search_parser.add_argument("--pages", type=int, default=1)
61
+ search_parser.add_argument("--raw-search-state")
62
+ search_parser.add_argument("--json", action="store_true")
63
+ search_parser.add_argument("--jsonl", action="store_true")
64
+ search_parser.set_defaults(handler=search.run)
65
+ count_parser = subparsers.add_parser("count", help="print total result count", parents=[cache_options])
66
+ count_parser.add_argument("query", nargs="?", default="")
67
+ count_parser.add_argument("--raw-search-state")
68
+ count_parser.add_argument("--json", action="store_true")
69
+ count_parser.set_defaults(handler=count.run)
70
+ job_parser = subparsers.add_parser("job", help="inspect job details")
71
+ job_subparsers = job_parser.add_subparsers(dest="job_command", required=True)
72
+ job_show_parser = job_subparsers.add_parser("show", help="show a job description", parents=[cache_options])
73
+ job_show_parser.add_argument("object_id")
74
+ job_show_parser.add_argument("--json", action="store_true")
75
+ job_show_parser.set_defaults(handler=job.run)
76
+ auth_parser = subparsers.add_parser("auth", help="manage Firebase authentication")
77
+ auth_subparsers = auth_parser.add_subparsers(dest="auth_command", required=True)
78
+ auth_login_parser = auth_subparsers.add_parser("login", help="log in to Firebase")
79
+ auth_login_parser.add_argument("--email", required=True)
80
+ auth_login_parser.set_defaults(handler=auth.login_email)
81
+ auth_import_parser = auth_subparsers.add_parser("import", help="import a refresh token")
82
+ auth_import_parser.set_defaults(handler=auth.import_token)
83
+ auth_status_parser = auth_subparsers.add_parser("status", help="show credential status")
84
+ auth_status_parser.add_argument("--json", action="store_true")
85
+ auth_status_parser.set_defaults(handler=auth.status)
86
+ auth_logout_parser = auth_subparsers.add_parser("logout", help="remove the local session")
87
+ auth_logout_parser.set_defaults(handler=auth.logout)
88
+ saved_parser = subparsers.add_parser("saved-jobs", help="manage saved jobs")
89
+ saved_subparsers = saved_parser.add_subparsers(dest="saved_command", required=True)
90
+ saved_list_parser = saved_subparsers.add_parser("list", help="list saved jobs")
91
+ saved_list_parser.add_argument("--stage", choices=sorted(VALID_STAGES))
92
+ saved_list_parser.add_argument("--json", action="store_true")
93
+ saved_list_parser.set_defaults(handler=saved_jobs_list.run)
94
+ saved_save_parser = saved_subparsers.add_parser("save", help="save a job")
95
+ saved_save_parser.add_argument("object_id")
96
+ saved_save_parser.add_argument("--stage", default="saved")
97
+ saved_save_parser.set_defaults(handler=saved_jobs_save.run)
98
+ saved_stage_parser = saved_subparsers.add_parser("stage", help="update a job stage")
99
+ saved_stage_parser.add_argument("object_id")
100
+ saved_stage_parser.add_argument("--stage", required=True)
101
+ saved_stage_parser.set_defaults(handler=saved_jobs_stage.run)
102
+ saved_remove_parser = saved_subparsers.add_parser("remove", help="remove a saved job")
103
+ saved_remove_parser.add_argument("object_id")
104
+ saved_remove_parser.add_argument("--yes", action="store_true")
105
+ saved_remove_parser.set_defaults(handler=saved_jobs_remove.run)
106
+ cache_parser = subparsers.add_parser("cache", help="manage anonymous read cache")
107
+ cache_subparsers = cache_parser.add_subparsers(dest="cache_command", required=True)
108
+ cache_purge_parser = cache_subparsers.add_parser("purge", help="remove cached anonymous reads")
109
+ cache_purge_parser.set_defaults(handler=cache_purge)
110
+ return parser
111
+
112
+
113
+ def cache_purge(argv=None, json_output: bool = False) -> int:
114
+ """Purge all anonymous cache files."""
115
+ CacheStore(active_cache_root()).purge()
116
+ print("Cache purged")
117
+ return 0
118
+
119
+
120
+ def main(argv: list[str] | None = None) -> None:
121
+ """Run the command-line interface.
122
+
123
+ Args:
124
+ argv: Optional argv list; defaults to ``sys.argv[1:]``.
125
+ """
126
+ arguments = build_parser().parse_args(argv)
127
+ handler = getattr(arguments, "handler", None)
128
+ if handler is not None:
129
+ json_output = getattr(arguments, "json", False)
130
+ try:
131
+ raise SystemExit(handler(arguments, json_output))
132
+ except InvalidSearchState as error:
133
+ print(f"hiringcafe: {error}", file=sys.stderr)
134
+ raise SystemExit(2) from error
135
+ except ValueError as error:
136
+ print(f"hiringcafe: {error}", file=sys.stderr)
137
+ raise SystemExit(1) from error
138
+ except FirebaseAuthError as error:
139
+ print(f"hiringcafe: {error}", file=sys.stderr)
140
+ raise SystemExit(5) from error
141
+ except FirebaseTransportError as error:
142
+ print(f"hiringcafe: {error}", file=sys.stderr)
143
+ raise SystemExit(1) from error
144
+ except FirestoreError as error:
145
+ print(f"hiringcafe: {error}", file=sys.stderr)
146
+ raise SystemExit(1) from error
147
+ except HireCafeHTTPError as error:
148
+ print(f"hiringcafe: {error}", file=sys.stderr)
149
+ raise SystemExit(3 if error.status == 429 else 1) from error
150
+ except HireCafeTransportError as error:
151
+ print(f"hiringcafe: {error}", file=sys.stderr)
152
+ raise SystemExit(1) from error
@@ -0,0 +1,95 @@
1
+ """Firebase authentication commands."""
2
+
3
+ import getpass
4
+ import json
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from hiringcafe_cli.firebase import FirebaseClient
10
+ from hiringcafe_cli.session import SessionStore
11
+
12
+
13
+ def default_session_path() -> Path:
14
+ """Return the platform user config session path."""
15
+ config_home = os.environ.get("HIRINGCAFE_CONFIG_HOME")
16
+ if config_home:
17
+ return Path(config_home) / "session.json"
18
+ return Path.home() / ".config" / "hiringcafe-cli" / "session.json"
19
+
20
+
21
+ def store() -> SessionStore:
22
+ """Create the session store using the selected configuration root."""
23
+ return SessionStore(default_session_path())
24
+
25
+
26
+ def import_token(argv: object, json_output: bool = False) -> int:
27
+ """Import a refresh token through stdin or the nonprinting environment."""
28
+ token = sys.stdin.readline().strip() or os.environ.get("HIRINGCAFE_REFRESH_TOKEN", "")
29
+ if not token:
30
+ print("hiringcafe: no refresh token provided on stdin or HIRINGCAFE_REFRESH_TOKEN", file=sys.stderr)
31
+ return 2
32
+ credentials = FirebaseClient().refresh(token)
33
+ store().save(credentials["refreshToken"])
34
+ print("Authenticated Firebase session saved")
35
+ return 0
36
+
37
+
38
+ def login_email(argv: object, json_output: bool = False) -> int:
39
+ """Exchange Firebase email/password credentials once and save only the refresh token."""
40
+ password = os.environ.get("HIRINGCAFE_PASSWORD") or getpass.getpass("Firebase password: ")
41
+ credentials = FirebaseClient().login_with_password(argv.email, password)
42
+ del password
43
+ store().save(credentials["refreshToken"])
44
+ print("Authenticated Firebase session saved")
45
+ return 0
46
+
47
+
48
+ def status(argv: object, json_output: bool = False) -> int:
49
+ """Report whether the stored Firebase credential can obtain an ID token.
50
+
51
+ Args:
52
+ argv: Unused CLI namespace.
53
+ json_output: Whether stdout must be machine-readable.
54
+
55
+ Returns:
56
+ Exit code 0 when usable, 4 when no session exists.
57
+ """
58
+ session = store().load()
59
+ if session is None:
60
+ if json_output:
61
+ print(json.dumps({"version": 1, "authenticated": False}, separators=(",", ":")))
62
+ else:
63
+ print("No stored credential", file=sys.stderr)
64
+ return 4
65
+ FirebaseClient().refresh(session.refresh_token)
66
+ if json_output:
67
+ payload = json.dumps(
68
+ {"version": 1, "authenticated": True},
69
+ separators=(",", ":"),
70
+ )
71
+ print(payload)
72
+ else:
73
+ print("Authenticated")
74
+ return 0
75
+
76
+
77
+ def logout(argv: object, json_output: bool = False) -> int:
78
+ """Delete the local Firebase session.
79
+
80
+ Args:
81
+ argv: Unused CLI namespace.
82
+ json_output: Unused; logout output remains intentionally human-only.
83
+
84
+ Returns:
85
+ Exit code 0 after local credential removal.
86
+ """
87
+ session_store = store()
88
+ had_session = session_store.load() is not None
89
+ try:
90
+ session_store.delete()
91
+ except OSError as error:
92
+ print(f"hiringcafe: cannot remove session: {error}", file=sys.stderr)
93
+ return 1
94
+ print("Local Firebase session removed" if had_session else "No local Firebase session")
95
+ return 0