flower-trellis 0.6.10-beta.4 → 0.6.10-beta.6

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.
package/README.md CHANGED
@@ -172,7 +172,7 @@ flower-trellis plugin
172
172
 
173
173
  交互管理器采用 `发现 / 已安装 / 来源 / 问题` 四个页签。Trellis 项目的 `发现` 页会展示 `flower/skill-garden` 内置入口,按 Enter 直接管理工作流强化与可选通用技能;原 `flower-trellis skill` 命令继续保留为高级兼容入口。`发现` 同时合并全部已启用来源的 Plugin,并保留来源标签和即时搜索;未登录 GitLab 来源会直接进入 Device Flow,GitHub 公共来源无需登录。`来源` 页的“新增来源”可选择 GitHub 公共仓库或 GitLab Marketplace;GitHub 会先在临时缓存中下载固定快照、检测格式、展示可导入与忽略组件,确认后才保存。ref 留空时使用仓库默认分支;出现多个格式入口时会要求选择,公开 GitHub 跨仓 Marketplace 条目和 `plugins/*` 多 Plugin 仓库也可识别。
174
174
 
175
- 可选通用技能包含 `aliyun-ops`,统一提供阿里云 DMS、SLS、MSE 与 ACK 查询和排障能力;ACK 支持通过短期 KubeConfig 和 Workbench 查询私网 Kubernetes 资源,DMS 写操作通过数据变更工单进入审批流。通用技能需手动选择启用,不会复制用户私有 AK/SK 或 Workbench 配置。Codex 通用技能安装到 `.agents/skills`,Claude 安装到 `.claude/skills`;已有 `.codex` 或 `.agents` 时使用共享目标,已有 `.claude` 时使用 Claude 目标,三个目录都不存在时默认同时安装两份。空目录也可通过 Skill Garden 入口管理通用技能,无需初始化 Trellis。
175
+ 可选通用技能包含 `aliyun-ops`,统一提供阿里云 DMS、SLS、MSE 与 ACK 查询和排障能力;ACK 支持通过短期 KubeConfig 和 Workbench 查询私网 Kubernetes 资源,并提供 Deployment 切镜像与改环境变量的受控命令(默认预览、显式 `--yes` 才执行,可通过 ENV 白名单限定命名空间与镜像仓库),DMS 写操作通过数据变更工单进入审批流。通用技能需手动选择启用,不会复制用户私有 AK/SK 或 Workbench 配置。Codex 通用技能安装到 `.agents/skills`,Claude 安装到 `.claude/skills`;已有 `.codex` 或 `.agents` 时使用共享目标,已有 `.claude` 时使用 Claude 目标,三个目录都不存在时默认同时安装两份。空目录也可通过 Skill Garden 入口管理通用技能,无需初始化 Trellis。
176
176
 
177
177
  安装、更新和卸载都会先展示 dry-run、依赖、capability 和目标文件变化,确认后才写入项目。Plugin 作者使用的 `plugin init`、`plugin validate` 继续保留在高级命令中,不占用普通用户的管理器首页。
178
178
 
@@ -603,6 +603,17 @@ def _tracked_files(repo_root: Path, relative: str) -> list[str]:
603
603
  return sorted(path for path in output.split("\0") if path)
604
604
 
605
605
 
606
+ def _untracked_files(repo_root: Path, relative: str) -> list[str]:
607
+ """列出路径下可由 Git 纳管的未跟踪文件。"""
608
+ code, output, error = run_git(
609
+ ["ls-files", "-z", "--others", "--exclude-standard", "--", relative],
610
+ cwd=repo_root,
611
+ )
612
+ if code != 0:
613
+ raise TaskLifecycleError("git-ls-files-failed", error.strip() or "无法读取未跟踪文件")
614
+ return sorted(path for path in output.split("\0") if path)
615
+
616
+
606
617
  def _run_git_with_index(
607
618
  args: list[str],
608
619
  repo_root: Path,
@@ -1245,18 +1256,36 @@ def _write_batch_and_commit(
1245
1256
  message: str,
1246
1257
  baseline: dict[str, str],
1247
1258
  transaction: dict[str, Any],
1259
+ *,
1260
+ commit_paths: Optional[list[str]] = None,
1261
+ expected_files: Optional[set[str]] = None,
1248
1262
  ) -> str:
1249
- """原子写入一批 JSON,并以可恢复的精确 pathspec 创建本地提交。"""
1263
+ """原子写入一批 JSON,并以可恢复的精确 pathspec 创建本地提交。
1264
+
1265
+ Args:
1266
+ repo_root: Git worktree 根目录。
1267
+ writes: 需要原子写入的 JSON 路径与数据。
1268
+ message: maintenance commit 消息。
1269
+ baseline: 事务开始时固定的完整分支引用与 HEAD。
1270
+ transaction: 操作类型与可恢复结果。
1271
+ commit_paths: 可选的精确提交 pathspec;缺省仅提交写入的 JSON。
1272
+ expected_files: 可选的预期提交文件全集;缺省等于写入的 JSON。
1273
+
1274
+ Returns:
1275
+ 新创建并绑定到固定分支的提交哈希。
1276
+ """
1250
1277
  originals = [(path, path.read_bytes()) for path, _ in writes]
1251
1278
  relative_paths = [_task_relative(repo_root, path) for path, _ in writes]
1279
+ exact_paths = relative_paths if commit_paths is None else commit_paths
1280
+ exact_files = set(relative_paths) if expected_files is None else expected_files
1252
1281
  try:
1253
1282
  for path, data in writes:
1254
1283
  if not write_json(path, data):
1255
1284
  raise TaskLifecycleError("write-failed", f"无法写入:{_task_relative(repo_root, path)}")
1256
1285
  return _commit_exact_paths(
1257
1286
  repo_root,
1258
- relative_paths,
1259
- set(relative_paths),
1287
+ exact_paths,
1288
+ exact_files,
1260
1289
  message,
1261
1290
  baseline,
1262
1291
  transaction,
@@ -1337,18 +1366,31 @@ def reconcile_legacy_tasks(repo_root: Path, migration_time: Optional[str] = None
1337
1366
  return result
1338
1367
  timestamp = migration_time or utc_now()
1339
1368
  writes: list[tuple[Path, dict[str, Any]]] = []
1369
+ commit_paths: list[str] = []
1370
+ expected_files: set[str] = set()
1340
1371
  for task_dir, data in legacy_dirs:
1341
1372
  task_json = task_dir / FILE_TASK_JSON
1342
1373
  task_ref = _task_relative(repo_root, task_dir)
1374
+ task_json_ref = f"{task_ref}/{FILE_TASK_JSON}"
1343
1375
  path_status = _git_path_status(repo_root, [task_ref])
1376
+ status_records = _status_records(path_status)
1344
1377
  dirty_paths = {
1345
1378
  path
1346
- for _, paths in _status_records(path_status)
1379
+ for _, paths in status_records
1347
1380
  for path in paths
1348
1381
  }
1382
+ tracked_files = _tracked_files(repo_root, task_ref)
1383
+ untracked_files = set(_untracked_files(repo_root, task_ref)) if not tracked_files else set()
1384
+ entirely_untracked = bool(
1385
+ status_records
1386
+ and not tracked_files
1387
+ and task_json_ref in untracked_files
1388
+ and dirty_paths == untracked_files
1389
+ and all(git_status == "??" for git_status, _ in status_records)
1390
+ )
1349
1391
  runner_owned = (
1350
1392
  task_ref in recorded
1351
- and dirty_paths == {f"{task_ref}/{FILE_TASK_JSON}"}
1393
+ and dirty_paths == {task_json_ref}
1352
1394
  and (
1353
1395
  recorded_digests.get(task_ref) == _task_json_sha256(task_json)
1354
1396
  or _legacy_runner_bookkeeping_matches(
@@ -1359,10 +1401,10 @@ def reconcile_legacy_tasks(repo_root: Path, migration_time: Optional[str] = None
1359
1401
  )
1360
1402
  )
1361
1403
  )
1362
- if path_status and not runner_owned:
1404
+ if path_status and not runner_owned and not entirely_untracked:
1363
1405
  result["deferred"].append({"task": task_ref, "reason": "candidate-dirty"})
1364
1406
  continue
1365
- if not _tracked_files(repo_root, task_ref):
1407
+ if not tracked_files and not entirely_untracked:
1366
1408
  result["deferred"].append({"task": task_ref, "reason": "candidate-untracked"})
1367
1409
  continue
1368
1410
  next_data = dict(data)
@@ -1375,7 +1417,7 @@ def reconcile_legacy_tasks(repo_root: Path, migration_time: Optional[str] = None
1375
1417
  task_dir,
1376
1418
  repo_root,
1377
1419
  next_data,
1378
- delivery_verified=not path_status or runner_owned,
1420
+ delivery_verified=not path_status or runner_owned or entirely_untracked,
1379
1421
  closed_at=timestamp,
1380
1422
  )
1381
1423
  if evaluated["status"] == "error":
@@ -1389,6 +1431,13 @@ def reconcile_legacy_tasks(repo_root: Path, migration_time: Optional[str] = None
1389
1431
  result["deferred"].append({"task": task_ref, "reason": "unknown-task-status"})
1390
1432
  continue
1391
1433
  writes.append((task_json, next_data))
1434
+ if entirely_untracked:
1435
+ # 新任务尚无 HEAD 基线,只能把可纳管的完整目录作为一个原子任务记录提交。
1436
+ commit_paths.append(task_ref)
1437
+ expected_files.update(untracked_files)
1438
+ else:
1439
+ commit_paths.append(task_json_ref)
1440
+ expected_files.add(task_json_ref)
1392
1441
  result["migrated"].append({"task": task_ref, "closeout": migration_status})
1393
1442
  if writes:
1394
1443
  result["commit"] = _write_batch_and_commit(
@@ -1397,6 +1446,8 @@ def reconcile_legacy_tasks(repo_root: Path, migration_time: Optional[str] = None
1397
1446
  "chore(task): reconcile closeout metadata",
1398
1447
  baseline,
1399
1448
  {"operation": "reconciliation", "result": result},
1449
+ commit_paths=commit_paths,
1450
+ expected_files=expected_files,
1400
1451
  )
1401
1452
  return result
1402
1453
 
@@ -1,7 +1,7 @@
1
1
  {
2
- "syncedAt": "2026-09-23T07:35:26.298Z",
2
+ "syncedAt": "2026-09-23T10:30:48.306Z",
3
3
  "syncedFrom": "vendor/skill-garden",
4
- "sourceCommit": "4ac24e07dffb6fa8d6e32a1344394881da9c7f15",
4
+ "sourceCommit": "f2feb8b818f72b124466a186987690f69665e8d1",
5
5
  "common": {
6
6
  "codexSkills": [
7
7
  "aliyun-ops",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: aliyun-ops
3
- description: "统一查询阿里云 DMS、SLS、MSE/Nacos 与 ACK 运维数据。当需要查询线上数据库、执行只读 SQL、预览或提交 DMS 数据变更工单、拉取 SLS 日志或指标、核对 Nacos 配置、查看 ACK 集群和节点,或通过 Workbench 查询私网 Kubernetes 资源、ConfigMap 与 Secret 键名时使用。"
3
+ description: "统一查询阿里云 DMS、SLS、MSE/Nacos 与 ACK 运维数据。当需要查询线上数据库、执行只读 SQL、预览或提交 DMS 数据变更工单、拉取 SLS 日志或指标、核对 Nacos 配置、查看 ACK 集群和节点、通过 Workbench 查询私网 Kubernetes 资源、ConfigMap 与 Secret 键名,或在用户确认后受控切换 Deployment 镜像与环境变量、等待发布结果时使用。"
4
4
  ---
5
5
 
6
6
  # 阿里云运维查询
@@ -14,7 +14,7 @@ description: "统一查询阿里云 DMS、SLS、MSE/Nacos 与 ACK 运维数据
14
14
  | 线上数据库查询、DMS 实例/库、数据变更工单 | `scripts/dms.py` | `references/dms.md` |
15
15
  | SLS 日志、metricstore 指标、LOG V1 签名 | `scripts/sls_get_logs.py` | `references/sls.md` |
16
16
  | MSE 集群、Nacos 配置与配置历史 | `scripts/mse.py` | `references/mse.md` |
17
- | ACK 集群、节点、KubeConfig、Kubernetes 资源只读查询 | `scripts/ack.py` | `references/ack.md` |
17
+ | ACK 集群、节点、KubeConfig、Kubernetes 资源只读查询;Deployment 切镜像、改环境变量、等待发布 | `scripts/ack.py` | `references/ack.md` |
18
18
 
19
19
  DMS 与 MSE 复用 `scripts/aliyun_rpc_v1.py`。SLS 使用独立的 LOG V1 签名,ACK 使用 `scripts/ack_roa_v3.py` 的 ACS3-HMAC-SHA256 签名,禁止混用三种协议。新增其它产品时,业务参数、响应解析和安全边界继续放在产品脚本与 reference 中。
20
20
 
@@ -34,7 +34,8 @@ DMS 与 MSE 复用 `scripts/aliyun_rpc_v1.py`。SLS 使用独立的 LOG V1 签
34
34
  - 默认读取顺序为进程环境 → `~/.config/aliyun-ops/env` → 产品旧路径;后读文件只补空值,不覆盖已有变量。
35
35
  - DMS 写操作只能走工单,且真实提交必须显式带 `--yes`。MSE 本期只提供读命令。
36
36
  - MSE 当前配置与历史配置无 `--grep` 时只输出摘要;不得为了方便绕过这一边界输出完整配置。
37
- - ACK 全部命令只读。私网 APIServer 使用短期内网 KubeConfig 加 Workbench 跳板;不提供任意远程 shell 或 `apply/delete/patch/scale`。
37
+ - ACK 查询命令只读。私网 APIServer 使用短期内网 KubeConfig 加 Workbench 跳板;不提供任意远程 shell 或 `apply/delete/patch/scale`。
38
+ - ACK 写入只能通过 `set-image` / `set-env`:默认只预览,用户确认后显式带 `--yes` 才执行;`ALIYUN_ACK_WRITE_NAMESPACES`、`ALIYUN_ACK_IMAGE_PREFIXES` 配置时强制白名单。禁止绕过这两个命令,直接用 `workbench exec`、手工 KubeConfig 或临时脚本执行任何写操作;需求超出这两个命令时,先向用户说明,不要自行变通。
38
39
  - ACK 查询先列资源摘要,再按用户明确目标读取单个对象。Secret 只输出键名和脱敏值,KubeConfig 默认不回显正文。
39
40
 
40
41
  配置模板位于 `assets/env.example`。初始化新文件时由用户主动执行:
@@ -14,3 +14,7 @@ ALIYUN_SLS_REGION=cn-hangzhou
14
14
  # MSE:endpoint 留空时由 region 自动生成。
15
15
  ALIYUN_MSE_REGION=cn-hangzhou
16
16
  ALIYUN_MSE_ENDPOINT=
17
+
18
+ # ACK 写入白名单:逗号分隔,留空表示不限制;配置后 set-image/set-env 只允许命中的目标。
19
+ ALIYUN_ACK_WRITE_NAMESPACES=
20
+ ALIYUN_ACK_IMAGE_PREFIXES=
@@ -1,6 +1,6 @@
1
- # 阿里云 ACK 只读查询
1
+ # 阿里云 ACK 查询与受控变更
2
2
 
3
- 用于查询 ACK 控制面信息,以及在本机无法访问私网 APIServer 时,通过 ACK Worker 和 Workbench CLI 查询 Kubernetes 对象。全部入口只读,不提供集群或工作负载变更能力。
3
+ 用于查询 ACK 控制面信息,以及在本机无法访问私网 APIServer 时,通过 ACK Worker 和 Workbench CLI 查询 Kubernetes 对象。查询入口全部只读;变更只提供 Deployment 切镜像与改环境变量两个受控命令,默认预览、显式 `--yes` 才执行。
4
4
 
5
5
  ## 查询顺序
6
6
 
@@ -57,6 +57,35 @@ Workbench 路径固定签发内网临时 KubeConfig,默认有效期 30 分钟
57
57
 
58
58
  Secret 的 `data` 和 `stringData` 值始终输出 `<REDACTED>`,只保留键名。ConfigMap 可在用户明确指定名称后输出正文;列表摘要只显示名称和键。
59
59
 
60
+ ## 受控写命令
61
+
62
+ 只在用户明确要求变更时使用。先不带 `--yes` 运行预览,把集群、命名空间、Deployment、容器、当前值 → 目标值和白名单状态展示给用户,获得确认后原样加 `--yes` 重跑。
63
+
64
+ ```bash
65
+ # 切换或回退镜像(回退 = 用旧 tag 再执行一次)
66
+ python3 scripts/ack.py set-image --cluster <cluster-id> --namespace <namespace> \
67
+ --deployment <deployment> [--container <container>] --image <registry/repo:tag>
68
+
69
+ # 设置或删除指定环境变量键(均可重复)
70
+ python3 scripts/ack.py set-env --cluster <cluster-id> --namespace <namespace> \
71
+ --deployment <deployment> [--container <container>] --set KEY=VALUE --unset OLD_KEY
72
+
73
+ # 确认后执行并在同一会话等待发布完成
74
+ python3 scripts/ack.py set-image ... --yes --wait --timeout 300
75
+
76
+ # 只读等待或查看发布状态
77
+ python3 scripts/ack.py rollout-status --cluster <cluster-id> --namespace <namespace> \
78
+ --deployment <deployment> --timeout 300
79
+ ```
80
+
81
+ - 写命令固定经 Workbench 执行,复用临时内网 KubeConfig 与两端清理流程,无需 `--via-workbench`。
82
+ - Deployment 只有一个容器时可省略 `--container`;多个容器必须显式指定。
83
+ - 镜像必须带 `:tag` 或 `@sha256:` digest;当前值与目标值一致时不执行写入。
84
+ - `set-env` 只修改指定键,拒绝修改通过 `valueFrom` 引用 Secret/ConfigMap 的键;不支持批量导入。
85
+ - `--timeout` 为 10~570 秒。写入成功但等待或清理失败时,错误会注明"写入已生效",不要重复执行写命令,先用 `rollout-status` 或 `get` 核对。
86
+ - 在私有 ENV 中配置 `ALIYUN_ACK_WRITE_NAMESPACES`、`ALIYUN_ACK_IMAGE_PREFIXES`(逗号分隔)后,未命中的命名空间或镜像在任何云调用前失败;留空表示不限制。
87
+ - 不提供重启、扩缩容、删除、任意 `patch` 或远程 shell。需求超出以上命令时向用户说明,禁止改用 `workbench exec` 或临时脚本自行执行写操作。
88
+
60
89
  ## Workbench 前置条件
61
90
 
62
91
  - 本机已安装官方 `workbench` CLI,`workbench version` 可执行。
@@ -78,14 +107,14 @@ Secret 的 `data` 和 `stringData` 值始终输出 `<REDACTED>`,只保留键
78
107
  | 列集群 | `cs:GetClusters` |
79
108
  | 集群详情 | `cs:DescribeClusterDetail` |
80
109
  | 关联云资源 | `cs:DescribeClusterResources` |
81
- | 节点列表与自动选节点 | `cs:DescribeClusterNodes` |
82
- | KubeConfig 与 `get` | `cs:DescribeClusterUserKubeconfig` |
110
+ | 节点列表与 Workbench 自动选节点 | `cs:DescribeClusterNodes` |
111
+ | KubeConfig、`get`、受控写命令与 `rollout-status` | `cs:DescribeClusterUserKubeconfig` |
83
112
 
84
113
  临时 KubeConfig 有效期还可通过 `cs:KubeConfigDurationMinutes` 条件收敛。
85
114
 
86
115
  ### ACK 集群 RBAC
87
116
 
88
- KubeConfig 只代表当前 RAM 身份。该身份还必须在目标集群与命名空间内拥有对应资源的 `get`、列表场景的 `list` 权限。OpenAPI 成功不代表 Kubernetes RBAC 已授权。
117
+ KubeConfig 只代表当前 RAM 身份。该身份还必须在目标集群与命名空间内拥有对应资源的 `get`、列表场景的 `list` 权限;受控写命令还需要 `deployments` 的 `patch` 权限,`--wait` / `rollout-status` 需要 `deployments`、`replicasets` 的 `get`/`list`/`watch`。OpenAPI 成功不代表 Kubernetes RBAC 已授权。
89
118
 
90
119
  ### Workbench / ECS RAM
91
120
 
@@ -104,6 +133,7 @@ KubeConfig 只代表当前 RAM 身份。该身份还必须在目标集群与命
104
133
  - `无法连通 APIServer`:先看 `detail`。只有内网端点时改用 `--via-workbench`,不建议为了查询临时开放生产公网端点。
105
134
  - `Forbidden`:检查 ACK 集群 RBAC;这与 `cs:*` RAM 权限无关。
106
135
  - `profile not found`:运行 `workbench config list` 核对 profile,并通过 `--workbench-profile` 指定。
136
+ - Workbench 上传偶发失败:脚本会自动重试一次;仍失败时错误只保留去掉进度动画后的真实原因。
107
137
  - Workbench 登录或上传 403:逐项核对 Workbench/ECS RAM Action、实例范围和服务关联角色。
108
138
  - 无可自动选择 Worker:运行 `nodes` 查看状态,确认存在 Running/Ready 的阿里云 Worker,或显式传 `--instance-id`。
109
139
  - 远端没有 `kubectl`:更换 ACK Worker 或修复节点工具环境;脚本不会自动安装软件。