nonoka-cli 0.2.12__tar.gz → 0.2.14__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.
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/PKG-INFO +38 -6
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/README.md +37 -5
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/pyproject.toml +1 -1
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/__init__.py +1 -1
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/cli.py +18 -6
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/commands/config_cmd.py +24 -13
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/commands/opencode_cmd.py +2 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/config/loader.py +6 -2
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/commands/test_cli_entrypoint.py +15 -1
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/commands/test_config_cmd.py +7 -1
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/commands/test_opencode_cmd.py +2 -2
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/config/test_loader.py +4 -0
- nonoka_cli-0.2.14/tests/unit/test_install_script.py +254 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/.gitignore +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/LICENSE +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/NOTICE +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/__main__.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/benchmark/__init__.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/benchmark/harbor.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/benchmark/swe_bench.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/benchmark/swe_environment.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/benchmark/swe_runner.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/benchmark/watchdog.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/bridge/__init__.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/bridge/events.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/bridge/handler.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/bridge/nonoka_tools.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/bridge/protocol.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/bridge/server.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/commands/__init__.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/commands/benchmark_cmd.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/commands/doctor_cmd.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/commands/eval_cmd.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/commands/logs_cmd.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/commands/plugin_cmd.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/commands/run_cmd.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/commands/sessions_cmd.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/config/__init__.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/config/manager.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/config/models.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/__init__.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/agent_factory.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/context.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/context_trimmer.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/git_service.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/mcp_service.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/namespaces.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/operational_signals.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/orchestrator.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/plugin_manifest.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/plugin_manifest_converter.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/project_agents.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/prompt_builder.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/repo_map_service.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/run_evidence.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/runner_service.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/scorecard.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/semantic_cache.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/session_service.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/task_state.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/tool_output_policy.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/core/tool_service.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/mcp/__init__.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/mcp/manager.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/mcp/models.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/safety/__init__.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/safety/preflight.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/safety/sandbox.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/sessions/__init__.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/sessions/manager.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/sessions/models.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/skills/__init__.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/tools/__init__.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/tools/builtins/__init__.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/tools/builtins/agent_tools.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/tools/builtins/file_tools.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/tools/loader.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/utils/__init__.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/utils/errors.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/utils/logging.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/src/nonoka_cli/utils/trace_logger.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/conftest.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/fixtures/bridge_external_capabilities.json +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/integration/test_server_external_capabilities.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/integration/test_watchdog_process_group.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/benchmark/test_harbor.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/benchmark/test_swe_environment.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/benchmark/test_watchdog.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/bridge/test_events.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/bridge/test_handler.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/bridge/test_nonoka_tools.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/bridge/test_package.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/bridge/test_protocol.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/bridge/test_server.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/commands/test_benchmark_cmd.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/commands/test_doctor_cmd.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/commands/test_eval_cmd.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/commands/test_logs_cmd.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/commands/test_plugin_cmd.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/commands/test_run_cmd.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/commands/test_swe_bench_cmd.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/config/test_models.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/core/test_agent_factory.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/core/test_context_trimmer.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/core/test_git_service.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/core/test_operational_signals.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/core/test_orchestrator.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/core/test_plugin_manifest.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/core/test_plugin_manifest_converter.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/core/test_project_agents.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/core/test_prompt_builder.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/core/test_repo_map_service.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/core/test_safety_policy.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/core/test_scorecard.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/core/test_semantic_cache.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/core/test_task_state.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/core/test_tool_output_policy.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/safety/test_preflight.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/sessions/test_manager.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/tools/test_file_tools.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/utils/test_logging.py +0 -0
- {nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/unit/utils/test_trace_logger.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nonoka-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.14
|
|
4
4
|
Summary: OpenCode backend for the Nonoka Agent framework
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -43,7 +43,7 @@ tool execution and human-in-the-loop (HITL) approval using its native tools.
|
|
|
43
43
|
The easiest way to get nonoka + OpenCode is the one-line installer:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
curl -fsSL https://
|
|
46
|
+
curl -fsSL https://raw.githubusercontent.com/fyerfyer/Nonoka-cli/main/install.sh | bash
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
The installer will:
|
|
@@ -51,7 +51,23 @@ The installer will:
|
|
|
51
51
|
1. Check Python 3.10+ and Node/npm.
|
|
52
52
|
2. Install or update OpenCode.
|
|
53
53
|
3. Install `nonoka-cli` and the OpenCode provider.
|
|
54
|
-
4.
|
|
54
|
+
4. Ask where to keep the installation, configuration, and npm packages.
|
|
55
|
+
5. Generate the Nonoka and project-level OpenCode configuration.
|
|
56
|
+
|
|
57
|
+
The prompts show what each directory contains and provide these defaults:
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
Installation directory (Python environment, launchers, and npm tools; e.g. ~/nonoka)
|
|
61
|
+
[~/.local/share/nonoka]
|
|
62
|
+
Configuration directory (config.yaml and .env; e.g. ~/.config/nonoka)
|
|
63
|
+
[~/.config/nonoka]
|
|
64
|
+
npm prefix (OpenCode/provider global packages)
|
|
65
|
+
[~/.local/share/nonoka/npm]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Press Enter to accept a default, or type a path such as `~/tools/nonoka`.
|
|
69
|
+
The generated `INSTALL_DIR/bin/nonoka` launcher remembers all three paths, so
|
|
70
|
+
using its absolute path does not require any environment exports.
|
|
55
71
|
|
|
56
72
|
After installing, configure your API key and run `nonoka`:
|
|
57
73
|
|
|
@@ -73,8 +89,24 @@ The legacy `nonoka-cli` executable remains available for compatibility.
|
|
|
73
89
|
`nonoka-cli` automatically loads `~/.config/nonoka/.env` and `./.env` on startup,
|
|
74
90
|
so you don't need to `export` every time if you save the key in `.env`.
|
|
75
91
|
|
|
76
|
-
>
|
|
77
|
-
>
|
|
92
|
+
> The installer uses `uv` by default when it is available and falls back to
|
|
93
|
+
> the isolated environment's pip. Pass `--pip` to explicitly select pip, or
|
|
94
|
+
> `--yes` to run non-interactively.
|
|
95
|
+
|
|
96
|
+
For a non-interactive custom location, use flags (environment variables with
|
|
97
|
+
the same names are also supported):
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
bash install.sh --yes --uv --npm-opencode \
|
|
101
|
+
--install-dir ~/tools/nonoka \
|
|
102
|
+
--config-dir ~/.config/nonoka \
|
|
103
|
+
--npm-prefix ~/tools/nonoka/npm
|
|
104
|
+
|
|
105
|
+
~/tools/nonoka/bin/nonoka doctor
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The corresponding variables are `NONOKA_INSTALL_DIR`, `NONOKA_CONFIG_DIR`,
|
|
109
|
+
and `NONOKA_NPM_PREFIX`. CLI flags take precedence over environment variables.
|
|
78
110
|
|
|
79
111
|
## Manual installation
|
|
80
112
|
|
|
@@ -132,7 +164,7 @@ Example output:
|
|
|
132
164
|
|
|
133
165
|
```
|
|
134
166
|
nonoka-cli doctor
|
|
135
|
-
✓ nonoka-cli 0.2.
|
|
167
|
+
✓ nonoka-cli 0.2.14
|
|
136
168
|
✓ Python 3.11
|
|
137
169
|
✓ opencode 1.18.2
|
|
138
170
|
✓ provider nonoka-opencode-provider@0.2.17
|
|
@@ -14,7 +14,7 @@ tool execution and human-in-the-loop (HITL) approval using its native tools.
|
|
|
14
14
|
The easiest way to get nonoka + OpenCode is the one-line installer:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
curl -fsSL https://
|
|
17
|
+
curl -fsSL https://raw.githubusercontent.com/fyerfyer/Nonoka-cli/main/install.sh | bash
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
The installer will:
|
|
@@ -22,7 +22,23 @@ The installer will:
|
|
|
22
22
|
1. Check Python 3.10+ and Node/npm.
|
|
23
23
|
2. Install or update OpenCode.
|
|
24
24
|
3. Install `nonoka-cli` and the OpenCode provider.
|
|
25
|
-
4.
|
|
25
|
+
4. Ask where to keep the installation, configuration, and npm packages.
|
|
26
|
+
5. Generate the Nonoka and project-level OpenCode configuration.
|
|
27
|
+
|
|
28
|
+
The prompts show what each directory contains and provide these defaults:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
Installation directory (Python environment, launchers, and npm tools; e.g. ~/nonoka)
|
|
32
|
+
[~/.local/share/nonoka]
|
|
33
|
+
Configuration directory (config.yaml and .env; e.g. ~/.config/nonoka)
|
|
34
|
+
[~/.config/nonoka]
|
|
35
|
+
npm prefix (OpenCode/provider global packages)
|
|
36
|
+
[~/.local/share/nonoka/npm]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Press Enter to accept a default, or type a path such as `~/tools/nonoka`.
|
|
40
|
+
The generated `INSTALL_DIR/bin/nonoka` launcher remembers all three paths, so
|
|
41
|
+
using its absolute path does not require any environment exports.
|
|
26
42
|
|
|
27
43
|
After installing, configure your API key and run `nonoka`:
|
|
28
44
|
|
|
@@ -44,8 +60,24 @@ The legacy `nonoka-cli` executable remains available for compatibility.
|
|
|
44
60
|
`nonoka-cli` automatically loads `~/.config/nonoka/.env` and `./.env` on startup,
|
|
45
61
|
so you don't need to `export` every time if you save the key in `.env`.
|
|
46
62
|
|
|
47
|
-
>
|
|
48
|
-
>
|
|
63
|
+
> The installer uses `uv` by default when it is available and falls back to
|
|
64
|
+
> the isolated environment's pip. Pass `--pip` to explicitly select pip, or
|
|
65
|
+
> `--yes` to run non-interactively.
|
|
66
|
+
|
|
67
|
+
For a non-interactive custom location, use flags (environment variables with
|
|
68
|
+
the same names are also supported):
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bash install.sh --yes --uv --npm-opencode \
|
|
72
|
+
--install-dir ~/tools/nonoka \
|
|
73
|
+
--config-dir ~/.config/nonoka \
|
|
74
|
+
--npm-prefix ~/tools/nonoka/npm
|
|
75
|
+
|
|
76
|
+
~/tools/nonoka/bin/nonoka doctor
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The corresponding variables are `NONOKA_INSTALL_DIR`, `NONOKA_CONFIG_DIR`,
|
|
80
|
+
and `NONOKA_NPM_PREFIX`. CLI flags take precedence over environment variables.
|
|
49
81
|
|
|
50
82
|
## Manual installation
|
|
51
83
|
|
|
@@ -103,7 +135,7 @@ Example output:
|
|
|
103
135
|
|
|
104
136
|
```
|
|
105
137
|
nonoka-cli doctor
|
|
106
|
-
✓ nonoka-cli 0.2.
|
|
138
|
+
✓ nonoka-cli 0.2.14
|
|
107
139
|
✓ Python 3.11
|
|
108
140
|
✓ opencode 1.18.2
|
|
109
141
|
✓ provider nonoka-opencode-provider@0.2.17
|
|
@@ -51,17 +51,29 @@ def _version() -> str:
|
|
|
51
51
|
return "unknown"
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
def _load_env_files() -> None:
|
|
54
|
+
def _load_env_files(config_path: Path | str | None = None) -> None:
|
|
55
55
|
"""Load .env files so config env-var substitution works transparently.
|
|
56
56
|
|
|
57
57
|
Priority (lowest to highest):
|
|
58
|
-
1. ~/.config/nonoka/.env
|
|
59
|
-
2.
|
|
58
|
+
1. $NONOKA_CONFIG_DIR/.env or ~/.config/nonoka/.env
|
|
59
|
+
2. the explicit config file's sibling .env
|
|
60
|
+
3. ./.env
|
|
60
61
|
Existing environment variables always win.
|
|
61
62
|
"""
|
|
62
|
-
|
|
63
|
+
configured_dir = Path(
|
|
64
|
+
os.getenv("NONOKA_CONFIG_DIR", str(Path.home() / ".config" / "nonoka"))
|
|
65
|
+
).expanduser()
|
|
66
|
+
candidates = [configured_dir / ".env"]
|
|
67
|
+
if config_path is not None:
|
|
68
|
+
candidates.append(Path(config_path).expanduser().resolve().parent / ".env")
|
|
63
69
|
local_env = Path.cwd() / ".env"
|
|
64
|
-
|
|
70
|
+
candidates.append(local_env)
|
|
71
|
+
seen: set[Path] = set()
|
|
72
|
+
for path in candidates:
|
|
73
|
+
path = path.resolve()
|
|
74
|
+
if path in seen:
|
|
75
|
+
continue
|
|
76
|
+
seen.add(path)
|
|
65
77
|
if path.exists():
|
|
66
78
|
load_dotenv(dotenv_path=path, override=False)
|
|
67
79
|
logger.debug("loaded_env_file", path=str(path))
|
|
@@ -140,7 +152,7 @@ def main() -> int:
|
|
|
140
152
|
|
|
141
153
|
# Load .env files after logging is configured so the debug-only path message
|
|
142
154
|
# does not leak into normal command output.
|
|
143
|
-
_load_env_files()
|
|
155
|
+
_load_env_files(args.config)
|
|
144
156
|
if args.server:
|
|
145
157
|
return server_main(config_path=args.config, model=args.model)
|
|
146
158
|
|
|
@@ -23,6 +23,16 @@ logger = structlog.get_logger("nonoka_cli.commands.config")
|
|
|
23
23
|
_GLOBAL_ENV_PATH = Path.home() / ".config" / "nonoka" / ".env"
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def _env_path_for_config(config_path: Path | None) -> Path:
|
|
27
|
+
"""Keep credentials beside an explicitly selected configuration file."""
|
|
28
|
+
if config_path is not None:
|
|
29
|
+
return config_path.expanduser().resolve().parent / ".env"
|
|
30
|
+
configured_dir = os.getenv("NONOKA_CONFIG_DIR")
|
|
31
|
+
if configured_dir:
|
|
32
|
+
return Path(configured_dir).expanduser().resolve() / ".env"
|
|
33
|
+
return _GLOBAL_ENV_PATH
|
|
34
|
+
|
|
35
|
+
|
|
26
36
|
def _load_manager(args: argparse.Namespace) -> ConfigManager:
|
|
27
37
|
"""Load or create a ConfigManager from the requested path."""
|
|
28
38
|
path = getattr(args, "config", None)
|
|
@@ -162,23 +172,23 @@ def _api_key_env_for_model(model: str) -> str:
|
|
|
162
172
|
return "OPENAI_API_KEY"
|
|
163
173
|
|
|
164
174
|
|
|
165
|
-
def _api_key_source_summary(env_var: str) -> str:
|
|
175
|
+
def _api_key_source_summary(env_var: str, env_path: Path = _GLOBAL_ENV_PATH) -> str:
|
|
166
176
|
"""Return a short description of where the API key is currently sourced."""
|
|
167
177
|
if os.getenv(env_var):
|
|
168
178
|
return f"from environment (${env_var})"
|
|
169
|
-
if _load_env_file(
|
|
170
|
-
return f"from {
|
|
179
|
+
if _load_env_file(env_path).get(env_var):
|
|
180
|
+
return f"from {env_path}"
|
|
171
181
|
return ""
|
|
172
182
|
|
|
173
183
|
|
|
174
|
-
def _collect_api_key(model: str) -> tuple[str, str, str]:
|
|
184
|
+
def _collect_api_key(model: str, env_path: Path = _GLOBAL_ENV_PATH) -> tuple[str, str, str]:
|
|
175
185
|
"""Collect API key info and optionally persist it.
|
|
176
186
|
|
|
177
187
|
Returns:
|
|
178
188
|
(env_var_name, api_key_for_config, summary_for_user)
|
|
179
189
|
"""
|
|
180
190
|
env_var = _api_key_env_for_model(model)
|
|
181
|
-
existing = os.getenv(env_var) or _load_env_file(
|
|
191
|
+
existing = os.getenv(env_var) or _load_env_file(env_path).get(env_var)
|
|
182
192
|
if existing:
|
|
183
193
|
print(f"Found {env_var} already set.")
|
|
184
194
|
return env_var, "", f"using existing ${env_var}"
|
|
@@ -203,16 +213,17 @@ def _collect_api_key(model: str) -> tuple[str, str, str]:
|
|
|
203
213
|
return env_var, "", "not saved"
|
|
204
214
|
|
|
205
215
|
# Default: save to .env
|
|
206
|
-
_write_env_file(
|
|
216
|
+
_write_env_file(env_path, env_var, key_input)
|
|
207
217
|
# Make it available for the rest of this process too.
|
|
208
218
|
os.environ[env_var] = key_input
|
|
209
|
-
print(f"Saved {env_var} to {
|
|
210
|
-
return env_var, "", f"saved to {
|
|
219
|
+
print(f"Saved {env_var} to {env_path}")
|
|
220
|
+
return env_var, "", f"saved to {env_path}"
|
|
211
221
|
|
|
212
222
|
|
|
213
223
|
def cmd_init(args: argparse.Namespace) -> int:
|
|
214
224
|
"""Create an initial nonoka config, interactively or with --yes defaults."""
|
|
215
|
-
path = Path(args.config) if args.config else ConfigLoader.DEFAULT_PATH
|
|
225
|
+
path = Path(args.config).expanduser() if args.config else ConfigLoader.DEFAULT_PATH
|
|
226
|
+
env_path = _env_path_for_config(path if args.config else None)
|
|
216
227
|
|
|
217
228
|
print(f"Creating nonoka configuration at: {path}")
|
|
218
229
|
|
|
@@ -220,7 +231,7 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
220
231
|
model = getattr(args, "model", None) or "deepseek/deepseek-v4-pro"
|
|
221
232
|
auto_approve = getattr(args, "auto_approve", False)
|
|
222
233
|
env_var = _api_key_env_for_model(model)
|
|
223
|
-
key_summary = _api_key_source_summary(env_var) or "not configured"
|
|
234
|
+
key_summary = _api_key_source_summary(env_var, env_path) or "not configured"
|
|
224
235
|
|
|
225
236
|
config = CLIConfig(
|
|
226
237
|
model=model,
|
|
@@ -240,9 +251,9 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
240
251
|
print(f"Configuration saved to {path}")
|
|
241
252
|
print(f"Model: {model}")
|
|
242
253
|
print(f"API key ({env_var}): {key_summary}")
|
|
243
|
-
if not os.getenv(env_var) and not _load_env_file(
|
|
254
|
+
if not os.getenv(env_var) and not _load_env_file(env_path).get(env_var):
|
|
244
255
|
print(
|
|
245
|
-
f"Set your API key with: nonoka
|
|
256
|
+
f"Set your API key with: nonoka config init (interactive) "
|
|
246
257
|
f"or export {env_var}=<your-key>"
|
|
247
258
|
)
|
|
248
259
|
return 0
|
|
@@ -254,7 +265,7 @@ def cmd_init(args: argparse.Namespace) -> int:
|
|
|
254
265
|
)
|
|
255
266
|
|
|
256
267
|
model = _read_input("Model identifier", "deepseek/deepseek-v4-pro")
|
|
257
|
-
env_var, api_key_value, key_summary = _collect_api_key(model)
|
|
268
|
+
env_var, api_key_value, key_summary = _collect_api_key(model, env_path)
|
|
258
269
|
|
|
259
270
|
system_prompt = _read_input(
|
|
260
271
|
"Optional system prompt",
|
|
@@ -15,6 +15,10 @@ from nonoka_cli.utils.errors import ConfigError, ConfigNotFoundError
|
|
|
15
15
|
|
|
16
16
|
logger = structlog.get_logger("nonoka_cli.config")
|
|
17
17
|
|
|
18
|
+
_DEFAULT_CONFIG_DIR = Path(
|
|
19
|
+
os.getenv("NONOKA_CONFIG_DIR", str(Path.home() / ".config" / "nonoka"))
|
|
20
|
+
).expanduser()
|
|
21
|
+
|
|
18
22
|
_ENV_PATTERN = re.compile(r"\$\{(?P<name>[A-Za-z_][A-Za-z0-9_]*)(?::-?(?P<default>[^}]*))?\}")
|
|
19
23
|
|
|
20
24
|
|
|
@@ -99,8 +103,8 @@ class ConfigLoader:
|
|
|
99
103
|
main-config entries of the same name.
|
|
100
104
|
"""
|
|
101
105
|
|
|
102
|
-
DEFAULT_PATH =
|
|
103
|
-
MCP_SERVERS_PATH =
|
|
106
|
+
DEFAULT_PATH = _DEFAULT_CONFIG_DIR / "config.yaml"
|
|
107
|
+
MCP_SERVERS_PATH = _DEFAULT_CONFIG_DIR / "mcp_servers.yaml"
|
|
104
108
|
|
|
105
109
|
@classmethod
|
|
106
110
|
def fallback_path(cls, search_dir: Path | str | None = None) -> Path:
|
|
@@ -33,10 +33,24 @@ def test_version_flag_reports_public_command(capsys: pytest.CaptureFixture[str])
|
|
|
33
33
|
def test_logging_is_configured_before_env_files_are_loaded(monkeypatch) -> None:
|
|
34
34
|
calls: list[str] = []
|
|
35
35
|
monkeypatch.setattr(cli, "setup_logging", lambda **_kwargs: calls.append("logging"))
|
|
36
|
-
monkeypatch.setattr(cli, "_load_env_files", lambda: calls.append("env"))
|
|
36
|
+
monkeypatch.setattr(cli, "_load_env_files", lambda *_args: calls.append("env"))
|
|
37
37
|
monkeypatch.setattr(cli.run_cmd, "launch_tui", lambda _args: 0)
|
|
38
38
|
|
|
39
39
|
with patch.object(cli.sys, "argv", ["nonoka"]):
|
|
40
40
|
assert cli.main() == 0
|
|
41
41
|
|
|
42
42
|
assert calls == ["logging", "env"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_explicit_config_loads_sibling_env(tmp_path, monkeypatch) -> None:
|
|
46
|
+
config_path = tmp_path / "custom" / "config.yaml"
|
|
47
|
+
env_path = config_path.parent / ".env"
|
|
48
|
+
env_path.parent.mkdir()
|
|
49
|
+
env_path.write_text("DEEPSEEK_API_KEY=test-only\n")
|
|
50
|
+
loaded: list[str] = []
|
|
51
|
+
monkeypatch.setattr(cli, "load_dotenv", lambda dotenv_path, override: loaded.append(str(dotenv_path)))
|
|
52
|
+
monkeypatch.chdir(tmp_path)
|
|
53
|
+
|
|
54
|
+
cli._load_env_files(config_path)
|
|
55
|
+
|
|
56
|
+
assert str(env_path.resolve()) in loaded
|
|
@@ -74,7 +74,6 @@ def test_write_env_file(tmp_path: Path):
|
|
|
74
74
|
def test_config_init_saves_api_key_to_env(tmp_path: Path, monkeypatch):
|
|
75
75
|
config_path = tmp_path / "config.yaml"
|
|
76
76
|
env_path = tmp_path / ".env"
|
|
77
|
-
monkeypatch.setattr(config_cmd, "_GLOBAL_ENV_PATH", env_path)
|
|
78
77
|
|
|
79
78
|
inputs = iter(["deepseek-chat", "", "d", ""])
|
|
80
79
|
def fake_read_input(prompt: str, default: str = "") -> str:
|
|
@@ -97,3 +96,10 @@ def test_config_init_saves_api_key_to_env(tmp_path: Path, monkeypatch):
|
|
|
97
96
|
|
|
98
97
|
# Clean up so the key does not leak into other tests.
|
|
99
98
|
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_config_init_uses_configured_directory_for_default_env(tmp_path: Path, monkeypatch):
|
|
102
|
+
config_dir = tmp_path / "configured"
|
|
103
|
+
monkeypatch.setenv("NONOKA_CONFIG_DIR", str(config_dir))
|
|
104
|
+
|
|
105
|
+
assert config_cmd._env_path_for_config(None) == config_dir / ".env"
|
|
@@ -53,7 +53,7 @@ def test_opencode_init_creates_file(tmp_path: Path):
|
|
|
53
53
|
assert data["provider"]["nonoka"]["options"]["requireFocusedVerification"] is True
|
|
54
54
|
assert data["provider"]["nonoka"]["options"]["verificationEnforcement"] == "strict"
|
|
55
55
|
assert data["provider"]["nonoka"]["options"]["serverCommand"] == [
|
|
56
|
-
sys.executable, "-m", "nonoka_cli", "--server",
|
|
56
|
+
sys.executable, "-m", "nonoka_cli", "--config", str(config_path), "--server",
|
|
57
57
|
]
|
|
58
58
|
|
|
59
59
|
|
|
@@ -77,7 +77,7 @@ def test_opencode_init_merges_existing(tmp_path: Path):
|
|
|
77
77
|
assert data["custom"] is True
|
|
78
78
|
assert data["provider"]["nonoka"]["options"]["configPath"] == str(config_path)
|
|
79
79
|
assert data["provider"]["nonoka"]["options"]["serverCommand"] == [
|
|
80
|
-
sys.executable, "-m", "nonoka_cli", "--server",
|
|
80
|
+
sys.executable, "-m", "nonoka_cli", "--config", str(config_path), "--server",
|
|
81
81
|
]
|
|
82
82
|
|
|
83
83
|
|
|
@@ -8,6 +8,10 @@ from nonoka_cli.config.loader import ConfigLoader, _substitute_env_vars
|
|
|
8
8
|
from nonoka_cli.utils.errors import ConfigError, ConfigNotFoundError
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
def test_default_config_assets_share_configured_directory():
|
|
12
|
+
assert ConfigLoader.MCP_SERVERS_PATH.parent == ConfigLoader.DEFAULT_PATH.parent
|
|
13
|
+
|
|
14
|
+
|
|
11
15
|
def test_substitute_env_vars_with_default():
|
|
12
16
|
value = _substitute_env_vars("${UNSET_VAR:-default_value}")
|
|
13
17
|
assert value == "default_value"
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Offline behavioural tests for install.sh path selection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
11
|
+
INSTALLER = ROOT / "install.sh"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _command(path: Path, body: str) -> None:
|
|
15
|
+
path.write_text("#!/usr/bin/env bash\nset -e\n" + body, encoding="utf-8")
|
|
16
|
+
path.chmod(0o755)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _fake_environment(tmp_path: Path) -> tuple[dict[str, str], Path]:
|
|
20
|
+
fake_bin = tmp_path / "bin"
|
|
21
|
+
fake_bin.mkdir()
|
|
22
|
+
log = tmp_path / "calls.log"
|
|
23
|
+
_command(
|
|
24
|
+
fake_bin / "python3",
|
|
25
|
+
'if [ "${1:-}" = "-c" ]; then\n'
|
|
26
|
+
' printf "3.13\\n"\n'
|
|
27
|
+
'elif [ "${1:-}" = "-m" ] && [ "${2:-}" = "venv" ]; then\n'
|
|
28
|
+
' mkdir -p "$3/bin"\n'
|
|
29
|
+
' cp "$0" "$3/bin/python"\n'
|
|
30
|
+
' cp "$(dirname "$0")/nonoka" "$3/bin/nonoka"\n'
|
|
31
|
+
'elif [ "${1:-}" = "-m" ] && [ "${2:-}" = "pip" ]; then\n'
|
|
32
|
+
' printf "python-pip:%s\\n" "${*:3}" >> "$NONOKA_TEST_LOG"\n'
|
|
33
|
+
'fi\n',
|
|
34
|
+
)
|
|
35
|
+
_command(
|
|
36
|
+
fake_bin / "uv",
|
|
37
|
+
'printf "uv:%s\\n" "$*" >> "$NONOKA_TEST_LOG"\n'
|
|
38
|
+
'if [ "${1:-}" = venv ]; then\n'
|
|
39
|
+
' mkdir -p "$2/bin"\n'
|
|
40
|
+
' touch "$2/bin/python"\n'
|
|
41
|
+
' chmod +x "$2/bin/python"\n'
|
|
42
|
+
' cp "$(dirname "$0")/nonoka" "$2/bin/nonoka"\n'
|
|
43
|
+
'fi\n',
|
|
44
|
+
)
|
|
45
|
+
_command(fake_bin / "opencode", "printf '1.18.10\\n'\n")
|
|
46
|
+
_command(fake_bin / "npm", 'printf "npm:%s\\n" "$*" >> "$NONOKA_TEST_LOG"\n')
|
|
47
|
+
_command(fake_bin / "nonoka", 'printf "nonoka:%s\\n" "$*" >> "$NONOKA_TEST_LOG"\n')
|
|
48
|
+
_command(fake_bin / "curl", "exit 0\n")
|
|
49
|
+
|
|
50
|
+
env = os.environ.copy()
|
|
51
|
+
env.update(
|
|
52
|
+
{
|
|
53
|
+
"HOME": str(tmp_path / "home"),
|
|
54
|
+
"PATH": f"{fake_bin}:/usr/bin:/bin",
|
|
55
|
+
"NONOKA_TEST_LOG": str(log),
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
for name in (
|
|
59
|
+
"VIRTUAL_ENV",
|
|
60
|
+
"NONOKA_INSTALL_DIR",
|
|
61
|
+
"NONOKA_CONFIG_DIR",
|
|
62
|
+
"NONOKA_NPM_PREFIX",
|
|
63
|
+
):
|
|
64
|
+
env.pop(name, None)
|
|
65
|
+
return env, log
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _run(tmp_path: Path, *args: str, input_text: str | None = None, env=None):
|
|
69
|
+
return subprocess.run(
|
|
70
|
+
["bash", str(INSTALLER), *args],
|
|
71
|
+
cwd=tmp_path,
|
|
72
|
+
env=env,
|
|
73
|
+
input=input_text,
|
|
74
|
+
text=True,
|
|
75
|
+
capture_output=True,
|
|
76
|
+
timeout=20,
|
|
77
|
+
check=False,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_install_dir_flags_expand_home_and_drive_all_commands(tmp_path: Path) -> None:
|
|
82
|
+
env, log = _fake_environment(tmp_path)
|
|
83
|
+
result = _run(
|
|
84
|
+
tmp_path,
|
|
85
|
+
"--yes",
|
|
86
|
+
"--uv",
|
|
87
|
+
"--no-opencode",
|
|
88
|
+
"--install-dir",
|
|
89
|
+
"~/chosen-install",
|
|
90
|
+
"--config-dir",
|
|
91
|
+
"~/chosen-config",
|
|
92
|
+
"--npm-prefix",
|
|
93
|
+
"~/chosen-npm",
|
|
94
|
+
env=env,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
assert result.returncode == 0, result.stderr
|
|
98
|
+
home = Path(env["HOME"])
|
|
99
|
+
calls = log.read_text()
|
|
100
|
+
assert f"uv:venv {home}/chosen-install/.venv --python python3" in calls
|
|
101
|
+
assert f"uv:pip install --python {home}/chosen-install/.venv/bin/python --upgrade nonoka-cli" in calls
|
|
102
|
+
assert f"nonoka:config init --yes --config {home}/chosen-config/config.yaml" in calls
|
|
103
|
+
assert f"nonoka:init --config {home}/chosen-config/config.yaml" in calls
|
|
104
|
+
assert f"npm prefix: {home}/chosen-npm" in result.stdout
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_environment_variables_select_noninteractive_layout(tmp_path: Path) -> None:
|
|
108
|
+
env, log = _fake_environment(tmp_path)
|
|
109
|
+
env.update(
|
|
110
|
+
{
|
|
111
|
+
"NONOKA_INSTALL_DIR": str(tmp_path / "env-install"),
|
|
112
|
+
"NONOKA_CONFIG_DIR": str(tmp_path / "env-config"),
|
|
113
|
+
"NONOKA_NPM_PREFIX": str(tmp_path / "env-npm"),
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
result = _run(tmp_path, "--yes", "--uv", "--no-opencode", env=env)
|
|
118
|
+
|
|
119
|
+
assert result.returncode == 0, result.stderr
|
|
120
|
+
calls = log.read_text()
|
|
121
|
+
assert f"uv:venv {tmp_path}/env-install/.venv --python python3" in calls
|
|
122
|
+
assert f"nonoka:config init --yes --config {tmp_path}/env-config/config.yaml" in calls
|
|
123
|
+
assert f"npm prefix: {tmp_path}/env-npm" in result.stdout
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_uv_is_the_default_and_targets_the_created_environment(tmp_path: Path) -> None:
|
|
127
|
+
env, log = _fake_environment(tmp_path)
|
|
128
|
+
install_dir = tmp_path / "default-uv"
|
|
129
|
+
|
|
130
|
+
result = _run(
|
|
131
|
+
tmp_path,
|
|
132
|
+
"--yes",
|
|
133
|
+
"--no-opencode",
|
|
134
|
+
"--install-dir",
|
|
135
|
+
str(install_dir),
|
|
136
|
+
env=env,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
assert result.returncode == 0, result.stderr
|
|
140
|
+
calls = log.read_text()
|
|
141
|
+
assert f"uv:venv {install_dir}/.venv --python python3" in calls
|
|
142
|
+
assert (
|
|
143
|
+
f"uv:pip install --python {install_dir}/.venv/bin/python --upgrade nonoka-cli"
|
|
144
|
+
in calls
|
|
145
|
+
)
|
|
146
|
+
assert "python-pip:" not in calls
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_pip_mode_is_pinned_to_the_created_environment(tmp_path: Path) -> None:
|
|
150
|
+
env, log = _fake_environment(tmp_path)
|
|
151
|
+
install_dir = tmp_path / "explicit-pip"
|
|
152
|
+
|
|
153
|
+
result = _run(
|
|
154
|
+
tmp_path,
|
|
155
|
+
"--yes",
|
|
156
|
+
"--pip",
|
|
157
|
+
"--no-opencode",
|
|
158
|
+
"--install-dir",
|
|
159
|
+
str(install_dir),
|
|
160
|
+
env=env,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
assert result.returncode == 0, result.stderr
|
|
164
|
+
calls = log.read_text()
|
|
165
|
+
assert "python-pip:install --upgrade nonoka-cli" in calls
|
|
166
|
+
assert f"uv:venv {install_dir}/.venv --python python3" not in calls
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_interactive_prompts_explain_each_directory(tmp_path: Path) -> None:
|
|
170
|
+
env, log = _fake_environment(tmp_path)
|
|
171
|
+
selected_install = tmp_path / "interactive-install"
|
|
172
|
+
selected_config = tmp_path / "interactive-config"
|
|
173
|
+
selected_npm = tmp_path / "interactive-npm"
|
|
174
|
+
answers = f"{selected_install}\n{selected_config}\n{selected_npm}\n"
|
|
175
|
+
|
|
176
|
+
result = _run(tmp_path, "--uv", "--no-opencode", input_text=answers, env=env)
|
|
177
|
+
|
|
178
|
+
assert result.returncode == 0, result.stderr
|
|
179
|
+
assert "Installation directory (Python environment, launchers, and npm tools" in result.stderr
|
|
180
|
+
assert "Configuration directory (config.yaml and .env" in result.stderr
|
|
181
|
+
assert "npm prefix (OpenCode/provider global packages)" in result.stderr
|
|
182
|
+
calls = log.read_text()
|
|
183
|
+
assert f"uv:venv {selected_install}/.venv --python python3" in calls
|
|
184
|
+
assert f"nonoka:config init --config {selected_config}/config.yaml" in calls
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_interactive_defaults_are_displayed_with_tilde(tmp_path: Path) -> None:
|
|
188
|
+
env, _log = _fake_environment(tmp_path)
|
|
189
|
+
|
|
190
|
+
result = _run(
|
|
191
|
+
tmp_path,
|
|
192
|
+
"--uv",
|
|
193
|
+
"--no-opencode",
|
|
194
|
+
input_text="\n\n\n",
|
|
195
|
+
env=env,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
assert result.returncode == 0, result.stderr
|
|
199
|
+
assert "[~/.local/share/nonoka]" in result.stderr
|
|
200
|
+
assert "[~/.config/nonoka]" in result.stderr
|
|
201
|
+
assert "[~/.local/share/nonoka/npm]" in result.stderr
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_launcher_preserves_paths_with_spaces_and_needs_no_exports(tmp_path: Path) -> None:
|
|
205
|
+
env, _log = _fake_environment(tmp_path)
|
|
206
|
+
install_dir = tmp_path / "install with spaces"
|
|
207
|
+
config_dir = tmp_path / "config with spaces"
|
|
208
|
+
npm_prefix = tmp_path / "npm with spaces"
|
|
209
|
+
|
|
210
|
+
result = _run(
|
|
211
|
+
tmp_path,
|
|
212
|
+
"--yes",
|
|
213
|
+
"--uv",
|
|
214
|
+
"--no-opencode",
|
|
215
|
+
"--install-dir",
|
|
216
|
+
str(install_dir),
|
|
217
|
+
"--config-dir",
|
|
218
|
+
str(config_dir),
|
|
219
|
+
"--npm-prefix",
|
|
220
|
+
str(npm_prefix),
|
|
221
|
+
env=env,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
assert result.returncode == 0, result.stderr
|
|
225
|
+
launcher = install_dir / "bin" / "nonoka"
|
|
226
|
+
alias = install_dir / "bin" / "nonoka-cli"
|
|
227
|
+
assert launcher.is_file()
|
|
228
|
+
assert os.access(launcher, os.X_OK)
|
|
229
|
+
assert alias.is_symlink()
|
|
230
|
+
assert os.readlink(alias) == "nonoka"
|
|
231
|
+
|
|
232
|
+
clean_env = env.copy()
|
|
233
|
+
clean_env.pop("VIRTUAL_ENV", None)
|
|
234
|
+
clean_env.pop("NONOKA_CONFIG_DIR", None)
|
|
235
|
+
clean_env.pop("NPM_CONFIG_PREFIX", None)
|
|
236
|
+
invoked = subprocess.run(
|
|
237
|
+
[str(launcher), "doctor"],
|
|
238
|
+
env=clean_env,
|
|
239
|
+
text=True,
|
|
240
|
+
capture_output=True,
|
|
241
|
+
timeout=10,
|
|
242
|
+
check=False,
|
|
243
|
+
)
|
|
244
|
+
assert invoked.returncode == 0, invoked.stderr
|
|
245
|
+
calls = (tmp_path / "calls.log").read_text()
|
|
246
|
+
assert "nonoka:doctor" in calls
|
|
247
|
+
|
|
248
|
+
content = launcher.read_text(encoding="utf-8")
|
|
249
|
+
escaped_venv = str(install_dir / ".venv").replace(" ", "\\ ")
|
|
250
|
+
escaped_config = str(config_dir).replace(" ", "\\ ")
|
|
251
|
+
escaped_npm = str(npm_prefix).replace(" ", "\\ ")
|
|
252
|
+
assert f"export VIRTUAL_ENV={escaped_venv}" in content
|
|
253
|
+
assert f"export NONOKA_CONFIG_DIR={escaped_config}" in content
|
|
254
|
+
assert f"export NPM_CONFIG_PREFIX={escaped_npm}" in content
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
{nonoka_cli-0.2.12 → nonoka_cli-0.2.14}/tests/integration/test_server_external_capabilities.py
RENAMED
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|