aisc-cli 0.1.1__tar.gz → 0.1.2__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.
- {aisc_cli-0.1.1/src/aisc_cli.egg-info → aisc_cli-0.1.2}/PKG-INFO +12 -11
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/README.md +11 -10
- aisc_cli-0.1.2/src/aisc/VERSION +1 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/cc_switch_provider.py +20 -1
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/docker_lifecycle.py +369 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/update.py +18 -2
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/cc_switch.py +12 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/run.py +39 -37
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/main.py +17 -6
- {aisc_cli-0.1.1 → aisc_cli-0.1.2/src/aisc_cli.egg-info}/PKG-INFO +12 -11
- aisc_cli-0.1.1/src/aisc/VERSION +0 -1
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/LICENSE +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/MANIFEST.in +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/pyproject.toml +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/setup.cfg +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/__init__.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/__main__.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/__init__.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/config_reader.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/container_registry.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/data_root_store.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/docker_.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/docker_gateway.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/docker_sdk_backed.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/maintenance_lock.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/state_file.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/system.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/windows_config_reader.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/adapters/workspace_lease_store.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/__init__.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/artifact.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/bundle_fetch.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/cc_switch_resolver.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/config_service.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/conversation.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/data_migration.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/data_root.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/doctor.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/legacy_scan.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/network_subscription.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/profile_service.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/provider.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/repair.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/resources.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/runtime.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/session.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/tool_versions.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/toolchain.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/usage.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/version.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/web_gateway.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/application/workspace_reconcile.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/applog.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/__init__.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/build_progress_parser.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/__init__.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/agents.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/artifact.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/build.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/bundle.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/config.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/container.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/conversation.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/data_root.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/logs.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/network.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/profile.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/provider.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/runs.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/runtime.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/serve.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/serve_fs.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/session.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/update.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/usage.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/wizard.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/commands/workspaces.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/help_i18n.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/output.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/cli/tutorial.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/__init__.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/artifacts.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/cc_switch_release.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/config.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/data_migration.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/data_root.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/docker_ownership.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/gateway.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/models.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/toolchain.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/web_services.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/domain/workspace_lease.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/schemas/__init__.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc/schemas/config_schema.py +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc_cli.egg-info/SOURCES.txt +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc_cli.egg-info/dependency_links.txt +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc_cli.egg-info/entry_points.txt +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc_cli.egg-info/requires.txt +0 -0
- {aisc_cli-0.1.1 → aisc_cli-0.1.2}/src/aisc_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aisc-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: AISC CLI — Super Claude workstation management
|
|
5
5
|
Author: AISC contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -122,9 +122,10 @@ AISC 尚未代码签名。SmartScreen 提示时,请先确认文件来自项目
|
|
|
122
122
|
| **火山引擎 Ark** | 兼容 OpenAI 格式;需配置接入点 ID |
|
|
123
123
|
| **智谱 GLM** | GLM-5.2 旗舰模型 |
|
|
124
124
|
| **Kimi** | 月之暗面 Kimi K3 |
|
|
125
|
-
| **Codesome
|
|
125
|
+
| **Codesome V3** | 月卡/按量中转(`sk-` Key,后台建 Key 选分组) |
|
|
126
|
+
| **Codesome 二合一** | V5 月卡中转(`cr-` 卡密即 Key,Claude/GPT 双端通用) |
|
|
126
127
|
|
|
127
|
-
|
|
128
|
+
2.1.12 起(D-6.7)镜像**不再预置**任何 provider 行——以上为「添加 provider」的模板清单(Codesome 置顶默认)。所有模板的 Codex 上游走 S9a 形态(本地路由把 Responses 翻译为 Anthropic Messages,直连模板声明的 Anthropic 端点);Codesome 两模板的 Claude 侧不含模型键(官方要求,模型由 Key 的服务端分组路由),并预填官方推荐的自动压缩水位(claude 150k / codex 900k)。
|
|
128
129
|
|
|
129
130
|
## CLI 使用指南
|
|
130
131
|
|
|
@@ -390,18 +391,18 @@ MIT License,详见 [LICENSE](https://github.com/wangyuncepu/AISC/blob/main/LIC
|
|
|
390
391
|
|
|
391
392
|
### Codesome|Codex 与 Claude Code 二合一服务
|
|
392
393
|
|
|
393
|
-
Codesome 提供 API
|
|
394
|
+
Codesome 提供 API 调用形式的接入服务,有两条产品线:**V3**(Claude/GPT 月卡 + 按量,`sk-...` Key,在后台建 Key 并选分组)与**二合一/V5**(月卡,订单里的 `cr-...` 卡密直接就是 Key,Claude/GPT 双端通用,无分组)。这是 API 服务,不是 Claude 或 Codex 成品账号。
|
|
394
395
|
|
|
395
|
-
**
|
|
396
|
+
**2.1.12 起内置两个 Codesome 模板**(`codesome-v3` / `codesome-2in1`)——Workbench Providers 页「添加 provider」默认选中 Codesome,选对产品线、填入 Key 即可(表单下方「获取服务」直达注册/购买页)。两条线的地址不可混用,V3 的 `sk-` Key 不能配二合一,反之亦然。
|
|
396
397
|
|
|
397
|
-
|
|
398
|
+
手动配置时的端点(两线各自两端通用,URL 不同不可混用):
|
|
398
399
|
|
|
399
|
-
|
|
|
400
|
-
| --- | --- |
|
|
401
|
-
|
|
|
402
|
-
|
|
|
400
|
+
| 产品线 | Claude 端 | Codex / OpenAI 格式端 |
|
|
401
|
+
| --- | --- | --- |
|
|
402
|
+
| V3(sk-) | `https://cc.codesome.ai` | `https://cc.codesome.ai/v1` |
|
|
403
|
+
| 二合一(cr-) | `https://v5.codesome.cn/api` | `https://v5.codesome.cn/openai` |
|
|
403
404
|
|
|
404
|
-
Claude Code 通过 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN`
|
|
405
|
+
Claude Code 通过 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN` 配置(加 `CLAUDE_CODE_ATTRIBUTION_HEADER=0`,官方核心三行之一);Codex 使用 OpenAI 格式配置(模型统一 `gpt-5.6-terra`)。可选:自动压缩(claude `CLAUDE_AUTO_COMPACT_WINDOW`,codex `model_auto_compact_token_limit`)到值自动压缩历史。完整教程见 [Codesome 文档](https://doc.codesome.ai/)。
|
|
405
406
|
|
|
406
407
|
购买与开通(注册/下单入口,非服务使用地址):
|
|
407
408
|
|
|
@@ -91,9 +91,10 @@ AISC 尚未代码签名。SmartScreen 提示时,请先确认文件来自项目
|
|
|
91
91
|
| **火山引擎 Ark** | 兼容 OpenAI 格式;需配置接入点 ID |
|
|
92
92
|
| **智谱 GLM** | GLM-5.2 旗舰模型 |
|
|
93
93
|
| **Kimi** | 月之暗面 Kimi K3 |
|
|
94
|
-
| **Codesome
|
|
94
|
+
| **Codesome V3** | 月卡/按量中转(`sk-` Key,后台建 Key 选分组) |
|
|
95
|
+
| **Codesome 二合一** | V5 月卡中转(`cr-` 卡密即 Key,Claude/GPT 双端通用) |
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
2.1.12 起(D-6.7)镜像**不再预置**任何 provider 行——以上为「添加 provider」的模板清单(Codesome 置顶默认)。所有模板的 Codex 上游走 S9a 形态(本地路由把 Responses 翻译为 Anthropic Messages,直连模板声明的 Anthropic 端点);Codesome 两模板的 Claude 侧不含模型键(官方要求,模型由 Key 的服务端分组路由),并预填官方推荐的自动压缩水位(claude 150k / codex 900k)。
|
|
97
98
|
|
|
98
99
|
## CLI 使用指南
|
|
99
100
|
|
|
@@ -359,18 +360,18 @@ MIT License,详见 [LICENSE](https://github.com/wangyuncepu/AISC/blob/main/LIC
|
|
|
359
360
|
|
|
360
361
|
### Codesome|Codex 与 Claude Code 二合一服务
|
|
361
362
|
|
|
362
|
-
Codesome 提供 API
|
|
363
|
+
Codesome 提供 API 调用形式的接入服务,有两条产品线:**V3**(Claude/GPT 月卡 + 按量,`sk-...` Key,在后台建 Key 并选分组)与**二合一/V5**(月卡,订单里的 `cr-...` 卡密直接就是 Key,Claude/GPT 双端通用,无分组)。这是 API 服务,不是 Claude 或 Codex 成品账号。
|
|
363
364
|
|
|
364
|
-
**
|
|
365
|
+
**2.1.12 起内置两个 Codesome 模板**(`codesome-v3` / `codesome-2in1`)——Workbench Providers 页「添加 provider」默认选中 Codesome,选对产品线、填入 Key 即可(表单下方「获取服务」直达注册/购买页)。两条线的地址不可混用,V3 的 `sk-` Key 不能配二合一,反之亦然。
|
|
365
366
|
|
|
366
|
-
|
|
367
|
+
手动配置时的端点(两线各自两端通用,URL 不同不可混用):
|
|
367
368
|
|
|
368
|
-
|
|
|
369
|
-
| --- | --- |
|
|
370
|
-
|
|
|
371
|
-
|
|
|
369
|
+
| 产品线 | Claude 端 | Codex / OpenAI 格式端 |
|
|
370
|
+
| --- | --- | --- |
|
|
371
|
+
| V3(sk-) | `https://cc.codesome.ai` | `https://cc.codesome.ai/v1` |
|
|
372
|
+
| 二合一(cr-) | `https://v5.codesome.cn/api` | `https://v5.codesome.cn/openai` |
|
|
372
373
|
|
|
373
|
-
Claude Code 通过 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN`
|
|
374
|
+
Claude Code 通过 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN` 配置(加 `CLAUDE_CODE_ATTRIBUTION_HEADER=0`,官方核心三行之一);Codex 使用 OpenAI 格式配置(模型统一 `gpt-5.6-terra`)。可选:自动压缩(claude `CLAUDE_AUTO_COMPACT_WINDOW`,codex `model_auto_compact_token_limit`)到值自动压缩历史。完整教程见 [Codesome 文档](https://doc.codesome.ai/)。
|
|
374
375
|
|
|
375
376
|
购买与开通(注册/下单入口,非服务使用地址):
|
|
376
377
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.2
|
|
@@ -21,7 +21,7 @@ from aisc.domain.models import CliError, RuntimeExitCode
|
|
|
21
21
|
PROTOCOL = "aisc.cc-switch-provider/v1"
|
|
22
22
|
_ADAPTER_PATH = "/usr/local/bin/aisc-cc-provider"
|
|
23
23
|
_AGENTS = ("claude", "codex")
|
|
24
|
-
_OPS = ("list", "add", "edit", "switch", "delete", "fetch-models")
|
|
24
|
+
_OPS = ("list", "add", "edit", "switch", "delete", "fetch-models", "templates")
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def _validate(runtime_id: str, agent: str, op: str) -> None:
|
|
@@ -161,6 +161,25 @@ def list_providers(runtime_id: str, agent: str, workspace: Optional[str], execut
|
|
|
161
161
|
"operation_id": envelope.get("operation_id")}
|
|
162
162
|
|
|
163
163
|
|
|
164
|
+
def list_templates(runtime_id: str, agent: str, workspace: Optional[str],
|
|
165
|
+
executor: Any) -> Dict[str, Any]:
|
|
166
|
+
"""D-6.8: the add-provider template manifest (adapter `templates` op).
|
|
167
|
+
|
|
168
|
+
The op itself is agent-less; the transport keeps the uniform --agent
|
|
169
|
+
argument (validated, then ignored inside the adapter)."""
|
|
170
|
+
from aisc.application.data_root import workspace_state_dir
|
|
171
|
+
from pathlib import Path
|
|
172
|
+
|
|
173
|
+
ws_path = Path(workspace).resolve() if workspace else Path.cwd()
|
|
174
|
+
_validate(runtime_id, agent, "templates")
|
|
175
|
+
envelope = _exec_adapter(
|
|
176
|
+
runtime_id, workspace_state_dir(ws_path), executor, "templates", agent,
|
|
177
|
+
None, None,
|
|
178
|
+
)
|
|
179
|
+
return {"agent": agent, "templates": envelope.get("templates") or [],
|
|
180
|
+
"operation_id": envelope.get("operation_id")}
|
|
181
|
+
|
|
182
|
+
|
|
164
183
|
def add_provider(runtime_id: str, agent: str, request: Dict[str, Any],
|
|
165
184
|
workspace: Optional[str], executor: Any) -> Dict[str, Any]:
|
|
166
185
|
from aisc.application.data_root import workspace_state_dir
|
|
@@ -31,6 +31,7 @@ Stage C wires the installers' rebuild flow.
|
|
|
31
31
|
from __future__ import annotations
|
|
32
32
|
|
|
33
33
|
import json
|
|
34
|
+
import re
|
|
34
35
|
from pathlib import Path
|
|
35
36
|
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
36
37
|
|
|
@@ -541,6 +542,374 @@ def _log_tail(stdout: str, lines: int = 20) -> str:
|
|
|
541
542
|
|
|
542
543
|
CACHE_USAGE_SCHEMA = "aisc.docker-cache-usage/v1"
|
|
543
544
|
CACHE_CLEANUP_SCHEMA = "aisc.docker-cache-cleanup/v1"
|
|
545
|
+
CACHE_INSPECT_SCHEMA = "aisc.docker-cache-inspect/v1"
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
# --- v2.1.13 (docker-scan-fidelity): read-only per-resource inspection -----
|
|
549
|
+
#
|
|
550
|
+
# D-2 (2026-09-19): detect FINER and report ACCURATELY; never clean anything
|
|
551
|
+
# automatically, never add new prune paths. Primary size/detail source is
|
|
552
|
+
# `docker system df -v --format json` (Docker 29.x returns structured
|
|
553
|
+
# per-item JSON there — verified on 29.8.0; officially undocumented, so
|
|
554
|
+
# callers must tolerate absence). Fallbacks degrade per-category and mark
|
|
555
|
+
# rows unknown; nothing here blocks anything else.
|
|
556
|
+
#
|
|
557
|
+
# Accuracy notes (the "更准确" half):
|
|
558
|
+
# - Image RECLAIMABLE aggregates over-count shared layers; the accurate
|
|
559
|
+
# per-image figure is UNIQUE SIZE, summed over images with Containers==0.
|
|
560
|
+
# - `image prune` (no -a) only ever removes DANGLING images, so rows also
|
|
561
|
+
# carry will_be_cleaned to show what the CURRENT manual cleanup would hit.
|
|
562
|
+
# - Build cache has no Reclaimable boolean in df -v; it is derived from
|
|
563
|
+
# InUse. buildx du (fallback) carries it natively.
|
|
564
|
+
|
|
565
|
+
_SIZE_UNITS: Dict[str, int] = {
|
|
566
|
+
"": 1, "B": 1,
|
|
567
|
+
"K": 10**3, "KI": 2**10,
|
|
568
|
+
"M": 10**6, "MI": 2**20,
|
|
569
|
+
"G": 10**9, "GI": 2**30,
|
|
570
|
+
"T": 10**12, "TI": 2**40,
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _size_bytes(value: Any) -> Optional[int]:
|
|
575
|
+
"""Best-effort docker size strings (`35.6 kB`, `1.2GB`, ints) → bytes.
|
|
576
|
+
None for N/A / unparseable — the row is reported as unknown."""
|
|
577
|
+
if value is None:
|
|
578
|
+
return None
|
|
579
|
+
if isinstance(value, (int, float)):
|
|
580
|
+
return int(value)
|
|
581
|
+
s = str(value).strip()
|
|
582
|
+
if not s or s.upper() in ("N/A", "UNKNOWN"):
|
|
583
|
+
return None
|
|
584
|
+
m = re.match(r"^([\d.]+)\s*([A-Za-z]*)$", s)
|
|
585
|
+
if not m:
|
|
586
|
+
return None
|
|
587
|
+
try:
|
|
588
|
+
num = float(m.group(1))
|
|
589
|
+
except ValueError:
|
|
590
|
+
return None
|
|
591
|
+
unit = m.group(2).upper().rstrip("B") if m.group(2) else ""
|
|
592
|
+
if unit not in _SIZE_UNITS:
|
|
593
|
+
return None
|
|
594
|
+
return int(num * _SIZE_UNITS[unit])
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _container_rw_size(value: Any) -> Optional[int]:
|
|
598
|
+
"""`ps -s` size strings: `35.6 kB (virtual 109 MB)` → writable layer."""
|
|
599
|
+
if value is None:
|
|
600
|
+
return None
|
|
601
|
+
head = str(value).split("(virtual")[0].strip()
|
|
602
|
+
return _size_bytes(head)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _json_lines(stdout: str) -> List[Dict[str, Any]]:
|
|
606
|
+
out: List[Dict[str, Any]] = []
|
|
607
|
+
for line in (stdout or "").splitlines():
|
|
608
|
+
line = line.strip()
|
|
609
|
+
if not line.startswith("{"):
|
|
610
|
+
continue
|
|
611
|
+
try:
|
|
612
|
+
row = json.loads(line)
|
|
613
|
+
except ValueError:
|
|
614
|
+
continue
|
|
615
|
+
if isinstance(row, dict):
|
|
616
|
+
out.append(row)
|
|
617
|
+
return out
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _df_verbose(executor: Any) -> Optional[Dict[str, Any]]:
|
|
621
|
+
"""`docker system df -v --format json` → {Images, Containers, Volumes,
|
|
622
|
+
BuildCache} (no Networks section). None when unavailable."""
|
|
623
|
+
try:
|
|
624
|
+
result = executor.run_captured(
|
|
625
|
+
["system", "df", "-v", "--format", "json"], timeout=60.0
|
|
626
|
+
)
|
|
627
|
+
except Exception:
|
|
628
|
+
return None
|
|
629
|
+
if getattr(result, "exit_code", 1) != 0:
|
|
630
|
+
return None
|
|
631
|
+
text = (result.stdout or "").strip()
|
|
632
|
+
if not text:
|
|
633
|
+
return None
|
|
634
|
+
try:
|
|
635
|
+
doc = json.loads(text)
|
|
636
|
+
if isinstance(doc, dict):
|
|
637
|
+
return doc
|
|
638
|
+
except ValueError:
|
|
639
|
+
pass
|
|
640
|
+
merged: Dict[str, Any] = {}
|
|
641
|
+
for row in _json_lines(text):
|
|
642
|
+
merged.update(row)
|
|
643
|
+
return merged or None
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _buildx_du(executor: Any) -> Optional[List[Dict[str, Any]]]:
|
|
647
|
+
"""`docker buildx du --format json` (NDJSON records). None when the
|
|
648
|
+
buildx plugin is absent/old (Docker Desktop's `docker builder` aliases
|
|
649
|
+
to buildx; classic CLIs lack du entirely)."""
|
|
650
|
+
try:
|
|
651
|
+
result = executor.run_captured(
|
|
652
|
+
["buildx", "du", "--format", "json"], timeout=60.0
|
|
653
|
+
)
|
|
654
|
+
except Exception:
|
|
655
|
+
return None
|
|
656
|
+
if getattr(result, "exit_code", 1) != 0:
|
|
657
|
+
return None
|
|
658
|
+
rows = _json_lines(result.stdout or "")
|
|
659
|
+
return rows or None
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def _images_rows(executor: Any, df_v: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
663
|
+
"""Image rows. df -v Images is the ONLY real Unique/Shared source on
|
|
664
|
+
Docker 29 (image ls --format json reports N/A placeholders there); the
|
|
665
|
+
ls pass adds dangling classification for older daemons. Reclaimable =
|
|
666
|
+
Σ UniqueSize over images with no containers; dangling is the subset the
|
|
667
|
+
current manual cleanup would remove (its 24h age filter is NOT applied
|
|
668
|
+
per-row — the disclaimer says so)."""
|
|
669
|
+
rows: List[Dict[str, Any]] = []
|
|
670
|
+
ls = executor.run_captured(["images", "--format", "{{json .}}"], timeout=30.0)
|
|
671
|
+
fallback: List[Dict[str, Any]] = []
|
|
672
|
+
if getattr(ls, "exit_code", 1) == 0:
|
|
673
|
+
for row in _json_lines(ls.stdout or ""):
|
|
674
|
+
repo = str(row.get("Repository") or "")
|
|
675
|
+
fallback.append({
|
|
676
|
+
"id": str(row.get("ID") or ""),
|
|
677
|
+
"name": f"{repo}:{row.get('Tag')}",
|
|
678
|
+
"in_use": None,
|
|
679
|
+
"dangling": repo == "<none>",
|
|
680
|
+
"size": row.get("Size"),
|
|
681
|
+
"size_bytes": _size_bytes(row.get("Size")),
|
|
682
|
+
"unique_size": None,
|
|
683
|
+
"shared": None,
|
|
684
|
+
"reclaimable_bytes": None,
|
|
685
|
+
"will_be_cleaned": repo == "<none>",
|
|
686
|
+
})
|
|
687
|
+
dv = (df_v or {}).get("Images")
|
|
688
|
+
if not isinstance(dv, list) or not dv:
|
|
689
|
+
return fallback
|
|
690
|
+
for row in dv:
|
|
691
|
+
if not isinstance(row, dict):
|
|
692
|
+
continue
|
|
693
|
+
containers = str(row.get("Containers") or "").strip()
|
|
694
|
+
in_use = containers not in ("", "0", "N/A")
|
|
695
|
+
repo = str(row.get("Repository") or "")
|
|
696
|
+
dangling = repo == "<none>"
|
|
697
|
+
unique = _size_bytes(row.get("UniqueSize"))
|
|
698
|
+
rows.append({
|
|
699
|
+
"id": str(row.get("ID") or ""),
|
|
700
|
+
"name": f"{repo}:{row.get('Tag')}",
|
|
701
|
+
"in_use": in_use,
|
|
702
|
+
"dangling": dangling,
|
|
703
|
+
"size": row.get("Size"),
|
|
704
|
+
"size_bytes": _size_bytes(row.get("Size")),
|
|
705
|
+
"unique_size": row.get("UniqueSize"),
|
|
706
|
+
# SharedSize is a display STRING (e.g. "326.5MB") - it must never
|
|
707
|
+
# ride the boolean shared field (2.1.13 hand-test: the string
|
|
708
|
+
# made the Rust serde drop the whole images category).
|
|
709
|
+
"shared": None,
|
|
710
|
+
"reclaimable_bytes": (0 if in_use else unique),
|
|
711
|
+
"will_be_cleaned": dangling,
|
|
712
|
+
})
|
|
713
|
+
return rows
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def _container_rows(executor: Any) -> List[Dict[str, Any]]:
|
|
717
|
+
"""Stopped containers' writable layers are the reclaimable part; running
|
|
718
|
+
ones report unknown (they cannot be reclaimed without stopping)."""
|
|
719
|
+
ps = executor.run_captured(
|
|
720
|
+
["ps", "-a", "--size", "--format", "{{json .}}"], timeout=30.0
|
|
721
|
+
)
|
|
722
|
+
rows: List[Dict[str, Any]] = []
|
|
723
|
+
if getattr(ps, "exit_code", 1) != 0:
|
|
724
|
+
return rows
|
|
725
|
+
for row in _json_lines(ps.stdout or ""):
|
|
726
|
+
names = row.get("Names") or row.get("Name") or ""
|
|
727
|
+
name = ", ".join(str(n) for n in names) if isinstance(names, list) else str(names)
|
|
728
|
+
state = str(row.get("State") or "")
|
|
729
|
+
running = state == "running"
|
|
730
|
+
rw = _container_rw_size(row.get("Size"))
|
|
731
|
+
rows.append({
|
|
732
|
+
"id": str(row.get("ID") or ""),
|
|
733
|
+
"name": name,
|
|
734
|
+
"image": str(row.get("Image") or ""),
|
|
735
|
+
"state": state,
|
|
736
|
+
"size": row.get("Size"),
|
|
737
|
+
"size_bytes": None if running else _container_rw_size(row.get("Size")),
|
|
738
|
+
"in_use": running,
|
|
739
|
+
"reclaimable_bytes": (None if running else rw),
|
|
740
|
+
"will_be_cleaned": False, # AISC never removes user containers here
|
|
741
|
+
})
|
|
742
|
+
return rows
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def _volume_rows(executor: Any) -> List[Dict[str, Any]]:
|
|
746
|
+
"""Named/anonymous × in-use/dangling. Sizes stay unknown by default
|
|
747
|
+
(per-volume du can take minutes); AISC NEVER deletes volumes (02 §5) —
|
|
748
|
+
every row is will_be_cleaned=False and the UI says so."""
|
|
749
|
+
ls = executor.run_captured(["volume", "ls", "--format", "{{json .}}"], timeout=30.0)
|
|
750
|
+
if getattr(ls, "exit_code", 1) != 0:
|
|
751
|
+
return []
|
|
752
|
+
dang = executor.run_captured(
|
|
753
|
+
["volume", "ls", "--filter", "dangling=true", "--format", "{{json .}}"],
|
|
754
|
+
timeout=30.0,
|
|
755
|
+
)
|
|
756
|
+
dangling: set = set()
|
|
757
|
+
if getattr(dang, "exit_code", 1) == 0:
|
|
758
|
+
for row in _json_lines(dang.stdout or ""):
|
|
759
|
+
name = str(row.get("Name") or "")
|
|
760
|
+
if name:
|
|
761
|
+
dangling.add(name)
|
|
762
|
+
rows: List[Dict[str, Any]] = []
|
|
763
|
+
for row in _json_lines(ls.stdout or ""):
|
|
764
|
+
name = str(row.get("Name") or "")
|
|
765
|
+
if not name:
|
|
766
|
+
continue
|
|
767
|
+
rows.append({
|
|
768
|
+
"id": name,
|
|
769
|
+
"name": name,
|
|
770
|
+
"dangling": name in dangling,
|
|
771
|
+
"in_use": name not in dangling,
|
|
772
|
+
"size": row.get("Size"),
|
|
773
|
+
"size_bytes": _size_bytes(row.get("Size")),
|
|
774
|
+
"reclaimable_bytes": None, # volumes are never reclaimed by AISC
|
|
775
|
+
"will_be_cleaned": False,
|
|
776
|
+
})
|
|
777
|
+
return rows
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _cache_rows(executor: Any, df_v: Optional[Dict[str, Any]]) -> Tuple[List[Dict[str, Any]], bool]:
|
|
781
|
+
"""Build-cache rows + whether the buildx fallback ran. Primary: df -v
|
|
782
|
+
BuildCache (daemon-wide, deduped; Reclaimable derived from InUse).
|
|
783
|
+
Fallback: buildx du (Reclaimable natively; single builder)."""
|
|
784
|
+
rows: List[Dict[str, Any]] = []
|
|
785
|
+
dv = (df_v or {}).get("BuildCache")
|
|
786
|
+
if isinstance(dv, list) and dv:
|
|
787
|
+
for row in dv:
|
|
788
|
+
if not isinstance(row, dict):
|
|
789
|
+
continue
|
|
790
|
+
in_use = bool(row.get("InUse"))
|
|
791
|
+
size_bytes = _size_bytes(row.get("Size"))
|
|
792
|
+
rows.append({
|
|
793
|
+
"id": str(row.get("ID") or ""),
|
|
794
|
+
"kind": str(row.get("CacheType") or row.get("Type") or ""),
|
|
795
|
+
"in_use": in_use,
|
|
796
|
+
"shared": bool(row.get("Shared")),
|
|
797
|
+
"size": row.get("Size"),
|
|
798
|
+
"size_bytes": size_bytes,
|
|
799
|
+
"reclaimable_bytes": (0 if in_use else (size_bytes or 0)),
|
|
800
|
+
"will_be_cleaned": not in_use,
|
|
801
|
+
"last_used_at": row.get("LastUsedAt"),
|
|
802
|
+
"usage_count": row.get("UsageCount"),
|
|
803
|
+
})
|
|
804
|
+
return rows, False
|
|
805
|
+
bx = _buildx_du(executor)
|
|
806
|
+
if bx is not None:
|
|
807
|
+
for row in bx:
|
|
808
|
+
reclaimable = bool(row.get("Reclaimable"))
|
|
809
|
+
size_bytes = _size_bytes(row.get("Size"))
|
|
810
|
+
rows.append({
|
|
811
|
+
"id": str(row.get("ID") or ""),
|
|
812
|
+
"kind": str(row.get("Type") or row.get("CacheType") or ""),
|
|
813
|
+
"in_use": not reclaimable,
|
|
814
|
+
"shared": bool(row.get("Shared")),
|
|
815
|
+
"size": row.get("Size"),
|
|
816
|
+
"size_bytes": size_bytes,
|
|
817
|
+
"reclaimable_bytes": ((size_bytes or 0) if reclaimable else 0),
|
|
818
|
+
"will_be_cleaned": reclaimable,
|
|
819
|
+
"last_used_at": row.get("LastUsedAt"),
|
|
820
|
+
"usage_count": row.get("UsageCount"),
|
|
821
|
+
})
|
|
822
|
+
return rows, True
|
|
823
|
+
return rows, False
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def _network_rows(executor: Any) -> List[Dict[str, Any]]:
|
|
827
|
+
"""Networks are 0-byte; only the count/reference picture matters."""
|
|
828
|
+
ls = executor.run_captured(["network", "ls", "--format", "{{json .}}"], timeout=30.0)
|
|
829
|
+
rows: List[Dict[str, Any]] = []
|
|
830
|
+
if getattr(ls, "exit_code", 1) != 0:
|
|
831
|
+
return rows
|
|
832
|
+
for row in _json_lines(ls.stdout or ""):
|
|
833
|
+
name = str(row.get("Name") or "")
|
|
834
|
+
if not name:
|
|
835
|
+
continue
|
|
836
|
+
rows.append({
|
|
837
|
+
"id": name,
|
|
838
|
+
"name": name,
|
|
839
|
+
"kind": str(row.get("Driver") or ""),
|
|
840
|
+
"in_use": None, # reference counting rides the containers pass
|
|
841
|
+
"size_bytes": 0,
|
|
842
|
+
"reclaimable_bytes": 0,
|
|
843
|
+
"will_be_cleaned": False,
|
|
844
|
+
})
|
|
845
|
+
return rows
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def _category_summary(rows: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
849
|
+
reclaimable = 0
|
|
850
|
+
unknown = 0
|
|
851
|
+
for row in rows:
|
|
852
|
+
value = row.get("reclaimable_bytes")
|
|
853
|
+
if value is None:
|
|
854
|
+
unknown += 1
|
|
855
|
+
else:
|
|
856
|
+
reclaimable += int(value)
|
|
857
|
+
return {
|
|
858
|
+
"count": len(rows),
|
|
859
|
+
"reclaimable_bytes": reclaimable,
|
|
860
|
+
"unknown_count": unknown,
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def cache_inspect(executor: Any, *, cleanup_min_age_hours: int = 24) -> Dict[str, Any]:
|
|
865
|
+
"""Read-only per-resource inspection (D-2). Detects images / containers /
|
|
866
|
+
volumes / build cache / networks item-by-item, reports the deduped
|
|
867
|
+
(UNIQUE-size) reclaimable picture, and flags what the CURRENT manual
|
|
868
|
+
cleanup would hit. Absolutely no prune/rmi/rm anywhere in here, and no
|
|
869
|
+
caller ever chains this into cleanup."""
|
|
870
|
+
df = _system_df(executor)
|
|
871
|
+
docker_available = bool(df)
|
|
872
|
+
warnings: List[str] = []
|
|
873
|
+
df_v: Optional[Dict[str, Any]] = None
|
|
874
|
+
buildx_used = False
|
|
875
|
+
if docker_available:
|
|
876
|
+
df_v = _df_verbose(executor)
|
|
877
|
+
if df_v is None:
|
|
878
|
+
warnings.append("df-verbose-json-unavailable")
|
|
879
|
+
buildx_used = _buildx_du(executor) is None
|
|
880
|
+
images = _images_rows(executor, df_v)
|
|
881
|
+
containers = _container_rows(executor)
|
|
882
|
+
volumes = _volume_rows(executor)
|
|
883
|
+
cache_rows, buildx_used = _cache_rows(executor, df_v)
|
|
884
|
+
networks = _network_rows(executor)
|
|
885
|
+
|
|
886
|
+
def cat(rows: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
887
|
+
return {"rows": rows, "summary": _category_summary(rows)}
|
|
888
|
+
|
|
889
|
+
return {
|
|
890
|
+
"schema_version": CACHE_INSPECT_SCHEMA,
|
|
891
|
+
"action": "cache-inspect",
|
|
892
|
+
"docker_available": docker_available,
|
|
893
|
+
"categories": {
|
|
894
|
+
"images": cat(images),
|
|
895
|
+
"containers": cat(containers),
|
|
896
|
+
"volumes": cat(volumes),
|
|
897
|
+
"build_cache": cat(cache_rows),
|
|
898
|
+
"networks": cat(networks),
|
|
899
|
+
},
|
|
900
|
+
"capabilities": {
|
|
901
|
+
"df_verbose_json": df_v is not None,
|
|
902
|
+
"buildx_du": buildx_used,
|
|
903
|
+
},
|
|
904
|
+
"cleanup_min_age_hours": cleanup_min_age_hours,
|
|
905
|
+
"warnings": warnings,
|
|
906
|
+
"disclaimer": (
|
|
907
|
+
"Reclaimable figures are estimates (UNIQUE-size based, deduped); "
|
|
908
|
+
"actual freed space can be lower. will_be_cleaned approximates "
|
|
909
|
+
"the current manual cleanup (dangling images / not-in-use build "
|
|
910
|
+
"cache) without its age filter."
|
|
911
|
+
),
|
|
912
|
+
}
|
|
544
913
|
|
|
545
914
|
|
|
546
915
|
def _cache_cleanup_argv(kind: str, min_age_hours: int) -> List[str]:
|
|
@@ -182,7 +182,12 @@ def perform_update(
|
|
|
182
182
|
|
|
183
183
|
``workdir`` overrides the exe directory (tests). Raises UpdateError /
|
|
184
184
|
BundleFetchError fail-closed; nothing is replaced unless every check
|
|
185
|
-
passed.
|
|
185
|
+
passed.
|
|
186
|
+
|
|
187
|
+
D-8 (2026-09-19): releases no longer ship CLI archives — the NSIS
|
|
188
|
+
installer IS the CLI+UI update channel for frozen installs, and pip is
|
|
189
|
+
the other. The download path therefore only works with an explicit
|
|
190
|
+
``--from-file`` archive (manual/offline updates)."""
|
|
186
191
|
import hashlib
|
|
187
192
|
|
|
188
193
|
if form["form"] == "pip":
|
|
@@ -202,8 +207,19 @@ def perform_update(
|
|
|
202
207
|
UPDATE_ERROR_STATE,
|
|
203
208
|
"no final release published yet — nothing to update to "
|
|
204
209
|
"(explicit --version <ver> to force)")
|
|
205
|
-
if not version and _version_key(target) <= _version_key(cli_version):
|
|
210
|
+
if not from_file and not version and _version_key(target) <= _version_key(cli_version):
|
|
206
211
|
return {"status": "up-to-date", "cli_version": cli_version, "latest_final": target}
|
|
212
|
+
# D-8 (2026-09-19): releases no longer ship CLI archives. The online
|
|
213
|
+
# sidecar-swap is retired — NSIS installs update the CLI with the
|
|
214
|
+
# Workbench, pip installs via pip. Manual/offline updates keep working
|
|
215
|
+
# through --from-file.
|
|
216
|
+
if not from_file:
|
|
217
|
+
raise UpdateError(
|
|
218
|
+
UPDATE_ERROR_USAGE,
|
|
219
|
+
"aisc update no longer downloads CLI archives (releases ship "
|
|
220
|
+
"the Workbench installer only). Update paths: pip install -U "
|
|
221
|
+
"aisc-cli · Workbench 检查更新(自带最新 CLI)· or --from-file "
|
|
222
|
+
"<archive> --sha256 <hex> for a manual archive")
|
|
207
223
|
|
|
208
224
|
exe = Path(form["exe"]) if not workdir else workdir / Path(form["exe"]).name
|
|
209
225
|
exe_dir = exe.parent
|
|
@@ -55,6 +55,18 @@ def cmd_cc_switch_list(args: Any) -> Dict[str, Any]:
|
|
|
55
55
|
)
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
def cmd_cc_switch_templates(args: Any) -> Dict[str, Any]:
|
|
59
|
+
"""D-6.8: the add-provider template manifest (agent-less adapter op)."""
|
|
60
|
+
from aisc.application.cc_switch_provider import list_templates
|
|
61
|
+
|
|
62
|
+
return list_templates(
|
|
63
|
+
runtime_id=args.runtime_id,
|
|
64
|
+
agent=args.agent,
|
|
65
|
+
workspace=args.workspace,
|
|
66
|
+
executor=None,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
58
70
|
def cmd_cc_switch_add(args: Any) -> Dict[str, Any]:
|
|
59
71
|
from aisc.application.cc_switch_provider import add_provider
|
|
60
72
|
|
|
@@ -155,12 +155,6 @@ def plan_run(
|
|
|
155
155
|
# Run result (structured outcome)
|
|
156
156
|
# ---------------------------------------------------------------------------
|
|
157
157
|
|
|
158
|
-
def _name_series(container_name: str) -> str:
|
|
159
|
-
"""Container names are ``<alias>-<hash8>``; the alias may itself contain
|
|
160
|
-
dashes, the hash never does — strip the last segment to get the series
|
|
161
|
-
(rename detection keys off it, r2 #C)."""
|
|
162
|
-
return container_name.rsplit("-", 1)[0]
|
|
163
|
-
|
|
164
158
|
|
|
165
159
|
@dataclass
|
|
166
160
|
class RunResult:
|
|
@@ -177,9 +171,6 @@ class RunResult:
|
|
|
177
171
|
# r1 #2: older same-workspace containers this activation swept
|
|
178
172
|
# (stopped+removed+unregistered before the new one started).
|
|
179
173
|
replaced: List[str] = field(default_factory=list)
|
|
180
|
-
# r1 #2 (revised): a LIVE same-workspace container was REUSED — nothing
|
|
181
|
-
# was started; the summary tells the user it is already open.
|
|
182
|
-
reused: Optional[str] = None
|
|
183
174
|
|
|
184
175
|
def to_dict(self) -> Dict[str, Any]:
|
|
185
176
|
out = {
|
|
@@ -194,8 +185,6 @@ class RunResult:
|
|
|
194
185
|
out["web_gateway"] = dict(self.web_gateway)
|
|
195
186
|
if self.replaced:
|
|
196
187
|
out["replaced"] = list(self.replaced)
|
|
197
|
-
if self.reused:
|
|
198
|
-
out["reused"] = self.reused
|
|
199
188
|
return out
|
|
200
189
|
|
|
201
190
|
|
|
@@ -353,21 +342,21 @@ def run_container(
|
|
|
353
342
|
)
|
|
354
343
|
# EXISTS → proceed
|
|
355
344
|
|
|
356
|
-
# --- r1 #2
|
|
357
|
-
#
|
|
358
|
-
#
|
|
359
|
-
#
|
|
360
|
-
#
|
|
361
|
-
#
|
|
362
|
-
#
|
|
363
|
-
#
|
|
364
|
-
#
|
|
365
|
-
#
|
|
366
|
-
#
|
|
367
|
-
#
|
|
368
|
-
#
|
|
369
|
-
#
|
|
370
|
-
|
|
345
|
+
# --- r1 #2 + r2 #C, revised 2026-09-19 (cli-run-guard): `aisc run` is
|
|
346
|
+
# FORBIDDEN while a live container exists for this workspace. Previously
|
|
347
|
+
# a live same-series container was silently reused, a rename intent
|
|
348
|
+
# rebuilt around live containers, and workbench-owned runtimes were
|
|
349
|
+
# bypassed (stacking a one-shot on top of a managed runtime). Now any
|
|
350
|
+
# LIVE label-less container of this workspace (CLI- or workbench-owned,
|
|
351
|
+
# any name series) refuses the run with guidance. DEAD CLI-owned
|
|
352
|
+
# containers are still swept (stop+rm+unregister) before the new start —
|
|
353
|
+
# sweeping a dead different-series container is the rename path (r2 #C).
|
|
354
|
+
# Dead workbench singletons stay untouched (the Workbench/reconcile owns
|
|
355
|
+
# their lifecycle). --label slots remain a bypass activation: neither
|
|
356
|
+
# checked nor touched. If docker does not answer the liveness probe,
|
|
357
|
+
# judge nothing: fall through to the normal start path rather than risk
|
|
358
|
+
# tearing down a live container we could not see.
|
|
359
|
+
live_blockers: List[tuple] = []
|
|
371
360
|
if not plan.dry_run and not plan.label:
|
|
372
361
|
from aisc.adapters.container_registry import list_containers as _lc
|
|
373
362
|
from aisc.adapters.container_registry import unregister
|
|
@@ -386,8 +375,6 @@ def run_container(
|
|
|
386
375
|
_states[_nm2] = _st
|
|
387
376
|
for _nm, _meta in _lc(_reg).items():
|
|
388
377
|
_meta = _meta if isinstance(_meta, dict) else {}
|
|
389
|
-
if _meta.get("owner") == "workbench":
|
|
390
|
-
continue
|
|
391
378
|
if _meta.get("label"):
|
|
392
379
|
continue
|
|
393
380
|
try:
|
|
@@ -395,13 +382,16 @@ def run_container(
|
|
|
395
382
|
continue
|
|
396
383
|
except OSError:
|
|
397
384
|
continue
|
|
398
|
-
if (_states.get(str(_nm), "").startswith("Up")
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
385
|
+
if (_states.get(str(_nm), "") or "").startswith("Up"):
|
|
386
|
+
live_blockers.append(
|
|
387
|
+
(str(_nm), str(_meta.get("owner") or "cli")))
|
|
388
|
+
continue
|
|
389
|
+
if _meta.get("owner") == "workbench":
|
|
390
|
+
# dead GUI singleton: the Workbench/reconcile owns its
|
|
391
|
+
# lifecycle — never swept from the CLI path
|
|
402
392
|
continue
|
|
403
|
-
# dead
|
|
404
|
-
#
|
|
393
|
+
# dead or unknown-to-docker CLI-owned (any series — a
|
|
394
|
+
# different series is the rename path) → sweep and rebuild
|
|
405
395
|
exec_.run_captured(["stop", _nm], timeout=30.0)
|
|
406
396
|
_rm = exec_.run_captured(["rm", "-f", _nm], timeout=30.0)
|
|
407
397
|
if _rm.exit_code == 0 or "no such" in (_rm.stderr or "").lower():
|
|
@@ -413,9 +403,21 @@ def run_container(
|
|
|
413
403
|
except Exception:
|
|
414
404
|
pass
|
|
415
405
|
|
|
416
|
-
if
|
|
417
|
-
|
|
418
|
-
|
|
406
|
+
if live_blockers:
|
|
407
|
+
# raise OUTSIDE the sweeping try/except — a CliError must not be
|
|
408
|
+
# swallowed by the best-effort guard above
|
|
409
|
+
_nm, _owner = live_blockers[0]
|
|
410
|
+
_who = "Workbench 管理的运行时" if _owner == "workbench" else "容器"
|
|
411
|
+
raise CliError(
|
|
412
|
+
message=(
|
|
413
|
+
f"该工作区已存在运行中的{_who}「{_nm}」,aisc run 禁止重复创建。\n"
|
|
414
|
+
f" 进入容器: aisc claude / aisc codex / aisc shell\n"
|
|
415
|
+
f" 生命周期: aisc runtime stop(或 Workbench)停止后再运行\n"
|
|
416
|
+
f" 多实例: aisc run --label <槽位名>"
|
|
417
|
+
),
|
|
418
|
+
exit_code=6, error_code="AISC_ERR_CONTAINER_EXISTS",
|
|
419
|
+
data=result.to_dict(),
|
|
420
|
+
)
|
|
419
421
|
|
|
420
422
|
# --- register container in the multi-container index ---
|
|
421
423
|
# F2-C: the registry root is the WORKSPACE's state dir (workspaces/<h>/
|
|
@@ -347,6 +347,13 @@ def _build_parser() -> _AiscArgumentParser:
|
|
|
347
347
|
help="Additionally emit the FULL api_key of this provider "
|
|
348
348
|
"(edit-time explicit view, 2.1.11 P1)")
|
|
349
349
|
|
|
350
|
+
# D-6.8 (2.1.12): the add-provider template manifest (agent-less op;
|
|
351
|
+
# --agent stays required for transport uniformity and is ignored).
|
|
352
|
+
cst = cssub.add_parser("templates",
|
|
353
|
+
help="Emit the add-provider template manifest",
|
|
354
|
+
allow_abbrev=False)
|
|
355
|
+
_cc_switch_common(cst)
|
|
356
|
+
|
|
350
357
|
csa = cssub.add_parser("add", help="Add a provider (request JSON on stdin)",
|
|
351
358
|
allow_abbrev=False)
|
|
352
359
|
_cc_switch_common(csa)
|
|
@@ -516,6 +523,10 @@ def _build_parser() -> _AiscArgumentParser:
|
|
|
516
523
|
mtu = mtsub.add_parser("cache-usage", help="Read-only docker system df summary",
|
|
517
524
|
allow_abbrev=False)
|
|
518
525
|
_add_global_args(mtu, is_subparser=True)
|
|
526
|
+
mtci = mtsub.add_parser("cache-inspect",
|
|
527
|
+
help="Read-only per-item Docker resource inspection (D-2)",
|
|
528
|
+
allow_abbrev=False)
|
|
529
|
+
_add_global_args(mtci, is_subparser=True)
|
|
519
530
|
|
|
520
531
|
mtcc = mtsub.add_parser("cache-cleanup",
|
|
521
532
|
help="Prune builder cache + dangling images (until-filtered)",
|
|
@@ -1423,12 +1434,6 @@ def _cmd_run(
|
|
|
1423
1434
|
]
|
|
1424
1435
|
|
|
1425
1436
|
if effective_format == "text" and emitter is None:
|
|
1426
|
-
if out.get("reused"):
|
|
1427
|
-
print(f"ℹ 工作区已在运行中: {plan.workspace}")
|
|
1428
|
-
print(f" 容器 {out['reused']}(Up) · 复用现有容器,未新建")
|
|
1429
|
-
print(" 进入方式: aisc claude | aisc codex | aisc switch | aisc shell")
|
|
1430
|
-
print(" 如需重建: 先 aisc stop 再重新 aisc run")
|
|
1431
|
-
return out, 0, []
|
|
1432
1437
|
print(f"💯 工作区已激活: {plan.workspace}")
|
|
1433
1438
|
print(f" 容器 {plan.name}(detached) · 镜像 {plan.image} · 网络 {plan.network}")
|
|
1434
1439
|
if out.get("replaced"):
|
|
@@ -1726,6 +1731,8 @@ def _cmd_cc_switch(
|
|
|
1726
1731
|
sub = getattr(args, "cc_switch_command", None)
|
|
1727
1732
|
if sub == "list":
|
|
1728
1733
|
data = cs_cmd.cmd_cc_switch_list(args)
|
|
1734
|
+
elif sub == "templates":
|
|
1735
|
+
data = cs_cmd.cmd_cc_switch_templates(args)
|
|
1729
1736
|
elif sub == "add":
|
|
1730
1737
|
data = cs_cmd.cmd_cc_switch_add(args)
|
|
1731
1738
|
elif sub == "edit":
|
|
@@ -1954,6 +1961,10 @@ def _cmd_maintenance(
|
|
|
1954
1961
|
if sub == "cache-usage":
|
|
1955
1962
|
from aisc.application.docker_lifecycle import cache_usage
|
|
1956
1963
|
return cache_usage(executor), 0, []
|
|
1964
|
+
if sub == "cache-inspect":
|
|
1965
|
+
# D-2 read-only per-item inspection — never chained into cleanup
|
|
1966
|
+
from aisc.application.docker_lifecycle import cache_inspect
|
|
1967
|
+
return cache_inspect(executor), 0, []
|
|
1957
1968
|
if sub == "cache-cleanup":
|
|
1958
1969
|
from aisc.application.docker_lifecycle import docker_cache_cleanup
|
|
1959
1970
|
data = docker_cache_cleanup(executor, min_age_hours=args.min_age_hours)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aisc-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: AISC CLI — Super Claude workstation management
|
|
5
5
|
Author: AISC contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -122,9 +122,10 @@ AISC 尚未代码签名。SmartScreen 提示时,请先确认文件来自项目
|
|
|
122
122
|
| **火山引擎 Ark** | 兼容 OpenAI 格式;需配置接入点 ID |
|
|
123
123
|
| **智谱 GLM** | GLM-5.2 旗舰模型 |
|
|
124
124
|
| **Kimi** | 月之暗面 Kimi K3 |
|
|
125
|
-
| **Codesome
|
|
125
|
+
| **Codesome V3** | 月卡/按量中转(`sk-` Key,后台建 Key 选分组) |
|
|
126
|
+
| **Codesome 二合一** | V5 月卡中转(`cr-` 卡密即 Key,Claude/GPT 双端通用) |
|
|
126
127
|
|
|
127
|
-
|
|
128
|
+
2.1.12 起(D-6.7)镜像**不再预置**任何 provider 行——以上为「添加 provider」的模板清单(Codesome 置顶默认)。所有模板的 Codex 上游走 S9a 形态(本地路由把 Responses 翻译为 Anthropic Messages,直连模板声明的 Anthropic 端点);Codesome 两模板的 Claude 侧不含模型键(官方要求,模型由 Key 的服务端分组路由),并预填官方推荐的自动压缩水位(claude 150k / codex 900k)。
|
|
128
129
|
|
|
129
130
|
## CLI 使用指南
|
|
130
131
|
|
|
@@ -390,18 +391,18 @@ MIT License,详见 [LICENSE](https://github.com/wangyuncepu/AISC/blob/main/LIC
|
|
|
390
391
|
|
|
391
392
|
### Codesome|Codex 与 Claude Code 二合一服务
|
|
392
393
|
|
|
393
|
-
Codesome 提供 API
|
|
394
|
+
Codesome 提供 API 调用形式的接入服务,有两条产品线:**V3**(Claude/GPT 月卡 + 按量,`sk-...` Key,在后台建 Key 并选分组)与**二合一/V5**(月卡,订单里的 `cr-...` 卡密直接就是 Key,Claude/GPT 双端通用,无分组)。这是 API 服务,不是 Claude 或 Codex 成品账号。
|
|
394
395
|
|
|
395
|
-
**
|
|
396
|
+
**2.1.12 起内置两个 Codesome 模板**(`codesome-v3` / `codesome-2in1`)——Workbench Providers 页「添加 provider」默认选中 Codesome,选对产品线、填入 Key 即可(表单下方「获取服务」直达注册/购买页)。两条线的地址不可混用,V3 的 `sk-` Key 不能配二合一,反之亦然。
|
|
396
397
|
|
|
397
|
-
|
|
398
|
+
手动配置时的端点(两线各自两端通用,URL 不同不可混用):
|
|
398
399
|
|
|
399
|
-
|
|
|
400
|
-
| --- | --- |
|
|
401
|
-
|
|
|
402
|
-
|
|
|
400
|
+
| 产品线 | Claude 端 | Codex / OpenAI 格式端 |
|
|
401
|
+
| --- | --- | --- |
|
|
402
|
+
| V3(sk-) | `https://cc.codesome.ai` | `https://cc.codesome.ai/v1` |
|
|
403
|
+
| 二合一(cr-) | `https://v5.codesome.cn/api` | `https://v5.codesome.cn/openai` |
|
|
403
404
|
|
|
404
|
-
Claude Code 通过 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN`
|
|
405
|
+
Claude Code 通过 `ANTHROPIC_BASE_URL` 和 `ANTHROPIC_AUTH_TOKEN` 配置(加 `CLAUDE_CODE_ATTRIBUTION_HEADER=0`,官方核心三行之一);Codex 使用 OpenAI 格式配置(模型统一 `gpt-5.6-terra`)。可选:自动压缩(claude `CLAUDE_AUTO_COMPACT_WINDOW`,codex `model_auto_compact_token_limit`)到值自动压缩历史。完整教程见 [Codesome 文档](https://doc.codesome.ai/)。
|
|
405
406
|
|
|
406
407
|
购买与开通(注册/下单入口,非服务使用地址):
|
|
407
408
|
|
aisc_cli-0.1.1/src/aisc/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.1.1
|
|
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
|
|
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
|