cinna-cli 0.2.0__tar.gz → 0.2.1__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.
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/PKG-INFO +1 -1
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/pyproject.toml +1 -1
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/account.py +58 -4
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/context.py +10 -2
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/main.py +5 -4
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/mcp_proxy.py +87 -11
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_account.py +116 -8
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_context.py +3 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/uv.lock +1 -1
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/.github/workflows/publish.yml +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/.gitignore +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/LICENSE.md +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/README.md +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/docs/README.md +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/docs/interface.md +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/docs/mutagen_capabilities.md +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/__init__.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/auth.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/bootstrap.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/client.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/config.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/console.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/errors.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/logging.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/mutagen_runtime.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/sync.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/sync_session.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/sync_ssh_shim.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/sync_tui.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/templates/ACCOUNT_CLAUDE.md.template +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/templates/CLAUDE.md.template +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/templates/__init__.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/__init__.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/conftest.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_auth.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_bootstrap.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_client.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_config.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_main.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_mutagen_runtime.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_sync.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_sync_session.py +0 -0
- {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_sync_ssh_shim.py +0 -0
|
@@ -161,6 +161,44 @@ def parse_account_setup_input(raw_input: str) -> tuple[str, str]:
|
|
|
161
161
|
return platform_url, text
|
|
162
162
|
|
|
163
163
|
|
|
164
|
+
def default_account_dir_name(platform_url: str) -> str:
|
|
165
|
+
"""Derive a default workspace folder name from the platform domain.
|
|
166
|
+
|
|
167
|
+
e.g. ``https://demo-core.opencinna.io`` → ``demo-core_opencinna_io``.
|
|
168
|
+
Hostname only (creds/port stripped); every run of non
|
|
169
|
+
``[A-Za-z0-9-]`` characters collapses to a single underscore. Falls back to
|
|
170
|
+
``DEFAULT_ACCOUNT_DIR`` when the URL has no usable host.
|
|
171
|
+
"""
|
|
172
|
+
host = urlparse(platform_url).netloc or platform_url.strip()
|
|
173
|
+
host = host.split("@")[-1].split(":")[0] # strip user:pass@ and :port
|
|
174
|
+
slug = re.sub(r"[^A-Za-z0-9-]+", "_", host).strip("_")
|
|
175
|
+
return slug or DEFAULT_ACCOUNT_DIR
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _prompt_account_dir(default: str) -> str:
|
|
179
|
+
"""Ask for the workspace folder name, offering ``default``.
|
|
180
|
+
|
|
181
|
+
Works in the ``curl ... | python3 -`` bootstrap too: there stdin carries the
|
|
182
|
+
installer script (not keystrokes), so when stdin is not a TTY we talk to the
|
|
183
|
+
controlling terminal via ``/dev/tty`` as long as stdout is a TTY. With no
|
|
184
|
+
terminal attached (CI, captured output) we return ``default`` unchanged so
|
|
185
|
+
non-interactive runs stay non-interactive.
|
|
186
|
+
"""
|
|
187
|
+
if sys.stdin.isatty():
|
|
188
|
+
return click.prompt("Workspace folder name", default=default)
|
|
189
|
+
|
|
190
|
+
if not sys.stdout.isatty():
|
|
191
|
+
return default
|
|
192
|
+
try:
|
|
193
|
+
with open("/dev/tty", "r+") as tty:
|
|
194
|
+
tty.write(f"Workspace folder name [{default}]: ")
|
|
195
|
+
tty.flush()
|
|
196
|
+
line = tty.readline()
|
|
197
|
+
except OSError:
|
|
198
|
+
return default
|
|
199
|
+
return line.strip() or default
|
|
200
|
+
|
|
201
|
+
|
|
164
202
|
def _exchange_account_setup_token(
|
|
165
203
|
platform_url: str, token: str, machine_name: str
|
|
166
204
|
) -> dict:
|
|
@@ -335,8 +373,13 @@ def _write_account_mcp_config(account_root: Path) -> None:
|
|
|
335
373
|
``POST /account/knowledge/search`` — the account analogue of the per-agent
|
|
336
374
|
workspace's knowledge tool. Auto-generated infra: overwritten on every
|
|
337
375
|
``cinna account setup`` / ``cinna account refresh-context``.
|
|
376
|
+
|
|
377
|
+
The config path is written **relative** to the account root (anchored at the
|
|
378
|
+
launch cwd, which MCP clients set to the workspace folder) so the folder can
|
|
379
|
+
be moved without breaking the proxy. ``run_mcp_proxy`` additionally walks up
|
|
380
|
+
from cwd, which self-heals older configs that stored an absolute path.
|
|
338
381
|
"""
|
|
339
|
-
account_config =
|
|
382
|
+
account_config = f"{CONFIG_DIR}/{ACCOUNT_CONFIG_FILE}"
|
|
340
383
|
|
|
341
384
|
mcp_json = {
|
|
342
385
|
"mcpServers": {
|
|
@@ -434,11 +477,23 @@ def _install_context_package(
|
|
|
434
477
|
|
|
435
478
|
|
|
436
479
|
def run_account_setup(
|
|
437
|
-
setup_input: str, machine_name: str, dir_name: str =
|
|
480
|
+
setup_input: str, machine_name: str, dir_name: str | None = None
|
|
438
481
|
) -> None:
|
|
439
|
-
"""Full account setup flow — called by `cinna account setup <token_or_url>`.
|
|
482
|
+
"""Full account setup flow — called by `cinna account setup <token_or_url>`.
|
|
483
|
+
|
|
484
|
+
When ``dir_name`` is not given (no ``--dir``), the folder name defaults to
|
|
485
|
+
the platform domain normalized (e.g. ``demo-core_opencinna_io``); the user
|
|
486
|
+
can accept it or type their own at the prompt.
|
|
487
|
+
"""
|
|
440
488
|
total = 3
|
|
441
489
|
|
|
490
|
+
# Parse before touching the filesystem / network so we can derive the
|
|
491
|
+
# default folder name from the platform domain (and fail fast on bad input).
|
|
492
|
+
platform_url, token = parse_account_setup_input(setup_input)
|
|
493
|
+
|
|
494
|
+
if not dir_name:
|
|
495
|
+
dir_name = _prompt_account_dir(default_account_dir_name(platform_url))
|
|
496
|
+
|
|
442
497
|
# Guard the target directory before burning the single-use setup token.
|
|
443
498
|
account_root = Path.cwd() / dir_name
|
|
444
499
|
if account_config_path(account_root).exists():
|
|
@@ -449,7 +504,6 @@ def run_account_setup(
|
|
|
449
504
|
|
|
450
505
|
# Step 1: Exchange the account setup token
|
|
451
506
|
console.step(1, total, "Authenticating...")
|
|
452
|
-
platform_url, token = parse_account_setup_input(setup_input)
|
|
453
507
|
payload = _exchange_account_setup_token(platform_url, token, machine_name)
|
|
454
508
|
|
|
455
509
|
# Step 2: Materialize the account workspace
|
|
@@ -114,6 +114,14 @@ def generate_context_files(
|
|
|
114
114
|
(workspace_root / "CLAUDE.md").write_text(claude_md)
|
|
115
115
|
|
|
116
116
|
|
|
117
|
+
# Relative (workspace-root-anchored) path to the per-agent config. Written into
|
|
118
|
+
# the generated MCP configs so the workspace folder can be moved without
|
|
119
|
+
# breaking the proxy: MCP clients launch `cinna mcp-proxy` with cwd set to the
|
|
120
|
+
# workspace root, and the proxy resolves this path against cwd. (`run_mcp_proxy`
|
|
121
|
+
# also falls back to walking up from cwd, healing older absolute-path configs.)
|
|
122
|
+
_REL_CONFIG_PATH = ".cinna/config.json"
|
|
123
|
+
|
|
124
|
+
|
|
117
125
|
def generate_mcp_json(config: CinnaConfig, workspace_root: Path) -> None:
|
|
118
126
|
"""Generate .mcp.json for Claude Code MCP tool discovery."""
|
|
119
127
|
mcp_config = {
|
|
@@ -122,7 +130,7 @@ def generate_mcp_json(config: CinnaConfig, workspace_root: Path) -> None:
|
|
|
122
130
|
"command": "cinna",
|
|
123
131
|
"args": ["mcp-proxy"],
|
|
124
132
|
"env": {
|
|
125
|
-
"CINNA_CONFIG":
|
|
133
|
+
"CINNA_CONFIG": _REL_CONFIG_PATH,
|
|
126
134
|
},
|
|
127
135
|
}
|
|
128
136
|
}
|
|
@@ -138,7 +146,7 @@ def generate_opencode_json(config: CinnaConfig, workspace_root: Path) -> None:
|
|
|
138
146
|
"type": "local",
|
|
139
147
|
"command": ["cinna", "mcp-proxy"],
|
|
140
148
|
"environment": {
|
|
141
|
-
"CINNA_CONFIG":
|
|
149
|
+
"CINNA_CONFIG": _REL_CONFIG_PATH,
|
|
142
150
|
},
|
|
143
151
|
"enabled": True,
|
|
144
152
|
}
|
|
@@ -131,11 +131,12 @@ def account():
|
|
|
131
131
|
@click.option(
|
|
132
132
|
"--dir",
|
|
133
133
|
"dir_name",
|
|
134
|
-
default=
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
default=None,
|
|
135
|
+
help="Directory to create the account workspace in "
|
|
136
|
+
"(default: the platform domain, e.g. demo-core_opencinna_io; "
|
|
137
|
+
"you'll be prompted to accept or change it)",
|
|
137
138
|
)
|
|
138
|
-
def account_setup(setup_input: tuple[str, ...], name: str | None, dir_name: str):
|
|
139
|
+
def account_setup(setup_input: tuple[str, ...], name: str | None, dir_name: str | None):
|
|
139
140
|
"""Set up an account workspace from an account setup token.
|
|
140
141
|
|
|
141
142
|
Accepts any of these formats (paste directly from Settings → Local
|
|
@@ -187,28 +187,105 @@ def _setup_mcp_logging(workspace_root: Path) -> None:
|
|
|
187
187
|
root.addHandler(handler)
|
|
188
188
|
|
|
189
189
|
|
|
190
|
+
def _resolve_proxy_context() -> tuple[str | None, Path | None]:
|
|
191
|
+
"""Resolve ``(mode, workspace_root)`` for the proxy, tolerant of a moved folder.
|
|
192
|
+
|
|
193
|
+
``mode`` is ``"account"`` or ``"agent"``; ``workspace_root`` is the directory
|
|
194
|
+
holding ``.cinna/``. Returns ``(None, None)`` when nothing can be located.
|
|
195
|
+
|
|
196
|
+
The ``CINNA_ACCOUNT_CONFIG`` / ``CINNA_CONFIG`` env vars (set by the generated
|
|
197
|
+
``.mcp.json`` / ``opencode.json``) primarily **select the mode**; their value
|
|
198
|
+
is only a hint for locating the config. We resolve the workspace root in order:
|
|
199
|
+
|
|
200
|
+
1. The env value taken literally — an absolute path (legacy configs) or a
|
|
201
|
+
path relative to the launch cwd (the portable form newer configs write).
|
|
202
|
+
2. A walk up from the launch cwd for the mode's config file. MCP clients
|
|
203
|
+
launch the proxy with cwd set to the workspace folder, so this heals
|
|
204
|
+
stale absolute paths after the folder is moved — no regeneration needed.
|
|
205
|
+
|
|
206
|
+
When neither env var is set, we auto-detect the nearest ``.cinna/`` from cwd
|
|
207
|
+
(an agent's ``config.json`` nested under an account wins over the account's
|
|
208
|
+
``account.json`` because it is found first).
|
|
209
|
+
"""
|
|
210
|
+
from cinna.account import ACCOUNT_CONFIG_FILE, find_account_root
|
|
211
|
+
from cinna.config import CONFIG_DIR, CONFIG_FILE, find_workspace_root
|
|
212
|
+
from cinna.errors import AccountConfigNotFoundError, ConfigNotFoundError
|
|
213
|
+
|
|
214
|
+
cwd = Path.cwd()
|
|
215
|
+
account_env = os.environ.get("CINNA_ACCOUNT_CONFIG")
|
|
216
|
+
config_env = os.environ.get("CINNA_CONFIG")
|
|
217
|
+
|
|
218
|
+
def _root_from_env(env_value: str | None, filename: str) -> Path | None:
|
|
219
|
+
"""Workspace root if env_value points at an existing .cinna/<filename>."""
|
|
220
|
+
if not env_value:
|
|
221
|
+
return None
|
|
222
|
+
candidates = [Path(env_value)]
|
|
223
|
+
if not Path(env_value).is_absolute():
|
|
224
|
+
candidates.append(cwd / env_value)
|
|
225
|
+
for cand in candidates:
|
|
226
|
+
if cand.is_file():
|
|
227
|
+
return cand.resolve().parent.parent
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
if account_env is not None:
|
|
231
|
+
root = _root_from_env(account_env, ACCOUNT_CONFIG_FILE)
|
|
232
|
+
if root is None:
|
|
233
|
+
try:
|
|
234
|
+
root = find_account_root(cwd)
|
|
235
|
+
except AccountConfigNotFoundError:
|
|
236
|
+
root = None
|
|
237
|
+
if root is not None:
|
|
238
|
+
return "account", root
|
|
239
|
+
|
|
240
|
+
if config_env is not None:
|
|
241
|
+
root = _root_from_env(config_env, CONFIG_FILE)
|
|
242
|
+
if root is None:
|
|
243
|
+
try:
|
|
244
|
+
root = find_workspace_root(cwd)
|
|
245
|
+
except ConfigNotFoundError:
|
|
246
|
+
root = None
|
|
247
|
+
if root is not None:
|
|
248
|
+
return "agent", root
|
|
249
|
+
|
|
250
|
+
# No usable env hint — auto-detect the nearest .cinna/ walking up from cwd.
|
|
251
|
+
if account_env is None and config_env is None:
|
|
252
|
+
current = cwd.resolve()
|
|
253
|
+
while True:
|
|
254
|
+
cinna_dir = current / CONFIG_DIR
|
|
255
|
+
if (cinna_dir / ACCOUNT_CONFIG_FILE).is_file():
|
|
256
|
+
return "account", current
|
|
257
|
+
if (cinna_dir / CONFIG_FILE).is_file():
|
|
258
|
+
return "agent", current
|
|
259
|
+
parent = current.parent
|
|
260
|
+
if parent == current:
|
|
261
|
+
break
|
|
262
|
+
current = parent
|
|
263
|
+
|
|
264
|
+
return None, None
|
|
265
|
+
|
|
266
|
+
|
|
190
267
|
def run_mcp_proxy():
|
|
191
268
|
"""Entry point for `cinna mcp-proxy` — run as MCP stdio server.
|
|
192
269
|
|
|
193
|
-
Two modes, selected by environment variable
|
|
270
|
+
Two modes, selected by environment variable (see ``_resolve_proxy_context``
|
|
271
|
+
for how the workspace root is located in a move-tolerant way):
|
|
194
272
|
|
|
195
273
|
* ``CINNA_ACCOUNT_CONFIG`` set → **account mode**: search the account user's
|
|
196
274
|
platform knowledge sources (used by the account workspace's `.mcp.json`).
|
|
197
275
|
* ``CINNA_CONFIG`` set → **per-agent mode**: search a single agent's
|
|
198
276
|
knowledge base (used by a per-agent workspace's `.mcp.json`).
|
|
199
277
|
"""
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if not account_config_path and not config_path:
|
|
278
|
+
mode, workspace_root = _resolve_proxy_context()
|
|
279
|
+
if mode is None:
|
|
203
280
|
raise SystemExit(
|
|
204
|
-
"
|
|
281
|
+
"Could not locate a cinna workspace. Set CINNA_CONFIG or "
|
|
282
|
+
"CINNA_ACCOUNT_CONFIG, or run `cinna mcp-proxy` from inside a "
|
|
283
|
+
"workspace folder (one containing .cinna/)."
|
|
205
284
|
)
|
|
206
285
|
|
|
207
|
-
if
|
|
208
|
-
# Account mode: <root>/.cinna/account.json → workspace root is parent.parent
|
|
209
|
-
workspace_root = Path(account_config_path).parent.parent
|
|
286
|
+
if mode == "account":
|
|
210
287
|
_setup_mcp_logging(workspace_root)
|
|
211
|
-
logger.info("MCP proxy starting in account mode (
|
|
288
|
+
logger.info("MCP proxy starting in account mode (workspace=%s)", workspace_root)
|
|
212
289
|
|
|
213
290
|
from cinna.account import load_account_config
|
|
214
291
|
|
|
@@ -224,9 +301,8 @@ def run_mcp_proxy():
|
|
|
224
301
|
logger.exception("Failed to create account MCP server")
|
|
225
302
|
raise
|
|
226
303
|
else:
|
|
227
|
-
workspace_root = Path(config_path).parent.parent
|
|
228
304
|
_setup_mcp_logging(workspace_root)
|
|
229
|
-
logger.info("MCP proxy starting (
|
|
305
|
+
logger.info("MCP proxy starting (workspace=%s)", workspace_root)
|
|
230
306
|
|
|
231
307
|
try:
|
|
232
308
|
config = load_config(workspace_root)
|
|
@@ -119,6 +119,25 @@ def test_parse_account_rejects_per_agent_url():
|
|
|
119
119
|
parse_account_setup_input("https://h.example/api/cli-setup/tok_abc")
|
|
120
120
|
|
|
121
121
|
|
|
122
|
+
# --- default_account_dir_name ---
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@pytest.mark.parametrize(
|
|
126
|
+
"platform_url,expected",
|
|
127
|
+
[
|
|
128
|
+
("https://demo-core.opencinna.io", "demo-core_opencinna_io"),
|
|
129
|
+
("https://demo-core.opencinna.io/api", "demo-core_opencinna_io"),
|
|
130
|
+
("http://localhost:8000/api", "localhost"),
|
|
131
|
+
("https://user:pass@app.example.com:443", "app_example_com"),
|
|
132
|
+
("", "my-cinna"),
|
|
133
|
+
],
|
|
134
|
+
)
|
|
135
|
+
def test_default_account_dir_name(platform_url, expected):
|
|
136
|
+
from cinna.account import default_account_dir_name
|
|
137
|
+
|
|
138
|
+
assert default_account_dir_name(platform_url) == expected
|
|
139
|
+
|
|
140
|
+
|
|
122
141
|
# --- account config I/O ---
|
|
123
142
|
|
|
124
143
|
|
|
@@ -222,7 +241,8 @@ def test_account_setup_creates_workspace(
|
|
|
222
241
|
)
|
|
223
242
|
assert captured["body"]["machine_name"] == "laptop"
|
|
224
243
|
|
|
225
|
-
|
|
244
|
+
# No --dir given → folder name defaults to the normalized platform domain.
|
|
245
|
+
root = tmp_path / "platform_example_com"
|
|
226
246
|
cfg = load_account_config(root)
|
|
227
247
|
assert cfg.account_token == "account-jwt-once"
|
|
228
248
|
assert cfg.platform_url == "https://platform.example.com"
|
|
@@ -247,13 +267,11 @@ def test_account_setup_creates_workspace(
|
|
|
247
267
|
proxy = mcp["mcpServers"]["platform-knowledge"]
|
|
248
268
|
assert proxy["command"] == "cinna"
|
|
249
269
|
assert proxy["args"] == ["mcp-proxy"]
|
|
250
|
-
assert proxy["env"]["CINNA_ACCOUNT_CONFIG"] ==
|
|
270
|
+
assert proxy["env"]["CINNA_ACCOUNT_CONFIG"] == ".cinna/account.json"
|
|
251
271
|
opencode = json.loads((root / "opencode.json").read_text())
|
|
252
272
|
oc_proxy = opencode["mcp"]["platform-knowledge"]
|
|
253
273
|
assert oc_proxy["command"] == ["cinna", "mcp-proxy"]
|
|
254
|
-
assert oc_proxy["environment"]["CINNA_ACCOUNT_CONFIG"] ==
|
|
255
|
-
account_config_path(root)
|
|
256
|
-
)
|
|
274
|
+
assert oc_proxy["environment"]["CINNA_ACCOUNT_CONFIG"] == ".cinna/account.json"
|
|
257
275
|
|
|
258
276
|
# Context package extracted under context/.
|
|
259
277
|
mock_client.download_context_package.assert_called_once()
|
|
@@ -325,6 +343,8 @@ def test_account_setup_refuses_existing_workspace(
|
|
|
325
343
|
"https://platform.example.com/api/cli-setup/account/TOK",
|
|
326
344
|
"--name",
|
|
327
345
|
"laptop",
|
|
346
|
+
"--dir",
|
|
347
|
+
"my-cinna",
|
|
328
348
|
],
|
|
329
349
|
)
|
|
330
350
|
assert result.exit_code != 0
|
|
@@ -376,14 +396,55 @@ def test_account_setup_continues_on_context_failure(
|
|
|
376
396
|
assert "Context package download failed" in result.output
|
|
377
397
|
assert "refresh-context" in result.output
|
|
378
398
|
|
|
379
|
-
# The workspace itself is fully materialized.
|
|
380
|
-
root = tmp_path / "
|
|
399
|
+
# The workspace itself is fully materialized (domain-derived default dir).
|
|
400
|
+
root = tmp_path / "platform_example_com"
|
|
381
401
|
assert load_account_config(root).account_token == "account-jwt-once"
|
|
382
402
|
assert (root / "agents").is_dir()
|
|
383
403
|
assert (root / "CLAUDE.md").is_file()
|
|
384
404
|
assert not (root / "context").exists()
|
|
385
405
|
|
|
386
406
|
|
|
407
|
+
@patch("cinna.account.AccountClient")
|
|
408
|
+
@patch("cinna.account.httpx.post")
|
|
409
|
+
def test_account_setup_explicit_dir_overrides_domain_default(
|
|
410
|
+
mock_post, mock_client_cls, runner, tmp_path, monkeypatch
|
|
411
|
+
):
|
|
412
|
+
"""An explicit ``--dir`` wins over the domain-derived default."""
|
|
413
|
+
monkeypatch.chdir(tmp_path)
|
|
414
|
+
mock_client = mock_client_cls.return_value.__enter__.return_value
|
|
415
|
+
mock_client.download_context_package.return_value = make_context_tarball()
|
|
416
|
+
|
|
417
|
+
class FakeResponse:
|
|
418
|
+
status_code = 200
|
|
419
|
+
text = ""
|
|
420
|
+
|
|
421
|
+
def json(self):
|
|
422
|
+
return {
|
|
423
|
+
"account_token": "account-jwt-once",
|
|
424
|
+
"platform_url": "https://platform.example.com",
|
|
425
|
+
"frontend_url": "https://ui.example.com",
|
|
426
|
+
"machine_name": "laptop",
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
mock_post.return_value = FakeResponse()
|
|
430
|
+
|
|
431
|
+
result = runner.invoke(
|
|
432
|
+
cli,
|
|
433
|
+
[
|
|
434
|
+
"account",
|
|
435
|
+
"setup",
|
|
436
|
+
"https://platform.example.com/api/cli-setup/account/TOK",
|
|
437
|
+
"--name",
|
|
438
|
+
"laptop",
|
|
439
|
+
"--dir",
|
|
440
|
+
"my-custom-folder",
|
|
441
|
+
],
|
|
442
|
+
)
|
|
443
|
+
assert result.exit_code == 0, result.output
|
|
444
|
+
assert (tmp_path / "my-custom-folder" / ".cinna" / "account.json").is_file()
|
|
445
|
+
assert not (tmp_path / "platform_example_com").exists()
|
|
446
|
+
|
|
447
|
+
|
|
387
448
|
# --- cinna account refresh-context ---
|
|
388
449
|
|
|
389
450
|
|
|
@@ -529,7 +590,8 @@ def test_write_account_mcp_config_wires_account_mode(account_root):
|
|
|
529
590
|
|
|
530
591
|
_write_account_mcp_config(account_root)
|
|
531
592
|
|
|
532
|
-
|
|
593
|
+
# Relative path (anchored at the workspace folder) so the folder is portable.
|
|
594
|
+
expected_cfg = ".cinna/account.json"
|
|
533
595
|
|
|
534
596
|
mcp = json.loads((account_root / ".mcp.json").read_text())
|
|
535
597
|
proxy = mcp["mcpServers"]["platform-knowledge"]
|
|
@@ -599,6 +661,52 @@ def account_cfg_token(account_root: Path) -> str:
|
|
|
599
661
|
return load_account_config(account_root).account_token
|
|
600
662
|
|
|
601
663
|
|
|
664
|
+
# --- move-tolerant proxy context resolution ---
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def test_proxy_context_relative_path_resolved_from_cwd(account_root, monkeypatch):
|
|
668
|
+
"""A relative CINNA_ACCOUNT_CONFIG resolves against the launch cwd (the
|
|
669
|
+
workspace folder MCP clients cd into) — the portable form newer configs write."""
|
|
670
|
+
from cinna.mcp_proxy import _resolve_proxy_context
|
|
671
|
+
|
|
672
|
+
monkeypatch.chdir(account_root)
|
|
673
|
+
monkeypatch.setenv("CINNA_ACCOUNT_CONFIG", ".cinna/account.json")
|
|
674
|
+
monkeypatch.delenv("CINNA_CONFIG", raising=False)
|
|
675
|
+
|
|
676
|
+
mode, root = _resolve_proxy_context()
|
|
677
|
+
assert mode == "account"
|
|
678
|
+
assert root == account_root.resolve()
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def test_proxy_context_stale_absolute_path_heals_via_cwd(account_root, monkeypatch):
|
|
682
|
+
"""A stale absolute CINNA_ACCOUNT_CONFIG (folder moved) falls back to walking
|
|
683
|
+
up from cwd, so an unregenerated legacy config keeps working after a move."""
|
|
684
|
+
from cinna.mcp_proxy import _resolve_proxy_context
|
|
685
|
+
|
|
686
|
+
monkeypatch.chdir(account_root)
|
|
687
|
+
monkeypatch.setenv(
|
|
688
|
+
"CINNA_ACCOUNT_CONFIG", "/old/location/my-cinna/.cinna/account.json"
|
|
689
|
+
)
|
|
690
|
+
monkeypatch.delenv("CINNA_CONFIG", raising=False)
|
|
691
|
+
|
|
692
|
+
mode, root = _resolve_proxy_context()
|
|
693
|
+
assert mode == "account"
|
|
694
|
+
assert root == account_root.resolve()
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def test_proxy_context_autodetects_nearest_cinna_without_env(account_root, monkeypatch):
|
|
698
|
+
"""With no env hint, the proxy auto-detects the nearest .cinna/ from cwd."""
|
|
699
|
+
from cinna.mcp_proxy import _resolve_proxy_context
|
|
700
|
+
|
|
701
|
+
monkeypatch.chdir(account_root)
|
|
702
|
+
monkeypatch.delenv("CINNA_ACCOUNT_CONFIG", raising=False)
|
|
703
|
+
monkeypatch.delenv("CINNA_CONFIG", raising=False)
|
|
704
|
+
|
|
705
|
+
mode, root = _resolve_proxy_context()
|
|
706
|
+
assert mode == "account"
|
|
707
|
+
assert root == account_root.resolve()
|
|
708
|
+
|
|
709
|
+
|
|
602
710
|
# --- cinna account agents ---
|
|
603
711
|
|
|
604
712
|
|
|
@@ -43,6 +43,8 @@ def test_generate_mcp_json(workspace_root, sample_config):
|
|
|
43
43
|
server = data["mcpServers"]["agent-knowledge"]
|
|
44
44
|
assert server["command"] == "cinna"
|
|
45
45
|
assert server["args"] == ["mcp-proxy"]
|
|
46
|
+
# Relative path so the workspace folder stays portable when moved.
|
|
47
|
+
assert server["env"]["CINNA_CONFIG"] == ".cinna/config.json"
|
|
46
48
|
|
|
47
49
|
|
|
48
50
|
def test_generate_opencode_json(workspace_root, sample_config):
|
|
@@ -60,6 +62,7 @@ def test_generate_opencode_json(workspace_root, sample_config):
|
|
|
60
62
|
assert server["type"] == "local"
|
|
61
63
|
assert server["command"] == ["cinna", "mcp-proxy"]
|
|
62
64
|
assert server["enabled"] is True
|
|
65
|
+
assert server["environment"]["CINNA_CONFIG"] == ".cinna/config.json"
|
|
63
66
|
|
|
64
67
|
|
|
65
68
|
def test_generate_gitignore(workspace_root):
|
|
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
|