mirror of
https://github.com/Tencent/teamai-cli.git
synced 2026-10-02 03:14:40 +08:00
* fix(env,hooks,mcp,status): name the entries that are not delivered (#822) env list, mcp list, hooks list, status and list <env|hooks|mcp> resolve the entry types to show what reaches this directory, but dropped the resolution notices: an entry an unknown key (a mistyped role:) or a removed key (roles: on env, projects:) takes out of the delivered set was silently missing from the list, and status counted around it. pull and doctor report these; now the list commands do too, via reportEntryResolution. env add on a variable carrying a removed per-entry key kept reporting plain 'Updated' — #833 taught it to warn for keys the schema does not know, but a removed key is in the shape on purpose (so it can be detected), so it stayed silent. Warn the same way for those. * test(e2e): match the delivered DEVOPS_ONLY form, not its name in the notice env list now reports the withheld per-entry `roles:` variable by name (#822), so the whole-output not.toContain('DEVOPS_ONLY') assertion tripped on the delivery notice itself. The variable stays out of the delivered list; match the listed form `DEVOPS_ONLY=` instead. * fix(env): point env add at the namespace file, not a key drop The review of #851 found the update-path warning told users to remove a per-entry `roles:`/`projects:` key in place, which delivers a root-scoped secret to the whole team. The remediation now reuses `moveTo`, the same remedy pull's notice names, so it points at the namespace file to move the entry into (with the manifest declaration to add when nothing declares it). `moveTo` and `TargetFiles` move from module-private to exported for this. The review also found skill-data/setup/references/manage-admin.md still said only pull and doctor report undelivered entries, while this branch made the list commands and status report them too. It now names them, as docs/usage-guide.md does. * fix(entries): report notices alongside resolution failures * fix(entries): scope list warnings and document env updates * docs(entries): align changelog with list and env warnings --------- Co-authored-by: ydflow <ydflow@users.noreply.github.com> Co-authored-by: ydflow <314143294+ydflow@users.noreply.github.com>
This commit is contained in:
+2
-2
@@ -6,10 +6,10 @@ All notable changes to this project will be documented in this file. See [standa
|
||||
|
||||
### 💥 Breaking Changes
|
||||
|
||||
- An env variable, hook or MCP server with a key its schema does not know, such as a misspelled `role:` or a hand-added `notes:`, is no longer delivered to anyone: the key used to be dropped silently, so a misspelled restriction shipped the entry to every member. `teamai pull` and `teamai doctor` name the file, the entry and the key. `teamai env add`, `teamai env remove` and `teamai remove mcp` keep the key when they rewrite the file. A key that a later version adds to these entries is unknown to this one too, so an entry that uses it is not delivered to a member still on this version: upgrade every member before the team uses a new entry key, as for a new `resources:` key (for [#822](https://github.com/Tencent/teamai-cli/issues/822)).
|
||||
- An env variable, hook or MCP server with a key its schema does not know, such as a misspelled `role:` or a hand-added `notes:`, is no longer delivered to anyone: the key used to be dropped silently, so a misspelled restriction shipped the entry to every member. `teamai pull`, the list commands, `teamai status` and `teamai doctor` name the file, the entry and the key. `teamai env add` also warns when it updates a variable carrying the unknown key; it, `teamai env remove` and `teamai remove mcp` keep the key when they rewrite the file. A key that a later version adds to these entries is unknown to this one too, so an entry that uses it is not delivered to a member still on this version: upgrade every member before the team uses a new entry key, as for a new `resources:` key (for [#822](https://github.com/Tencent/teamai-cli/issues/822)).
|
||||
- `manifest/projects.yaml` and `manifest/roles.yaml` now reject a resource namespace that is not a single path segment, as a project id already had to be (the id keeps its own narrower ASCII rule). A namespace becomes a directory component (`skills/<namespace>/`, `agents/<namespace>/`, `learnings/<namespace>/`), so `../evil`, `a/b`, `C:evil`, a bare `..`, any name with a trailing `.` or space — which Win32 strips, making `.. ` arrive as `..` and `frontend.` as `frontend` — and a Windows device name such as `CON` or `COM1` under `resources:` no longer parse; the error names the offending entry. Nothing else is rejected: a namespace that is a plain directory name still parses, non-ASCII names and names with a space included. A manifest that fails to parse now reports the offending entry on one line (`Invalid projects manifest: projects.0.resources.skills.1: ...`) instead of dumping a raw validation object. Two namespaces of one resource type that differ only by case (`frontend`, `Frontend`) are rejected too, within a manifest and between the two, since they name one directory on Windows and macOS. A manifest that ships any of these — a device name, a trailing `.`, a case-only pair — parsed before and fails every pull now; rename the directory and the entry together.
|
||||
- **Upgrade every member before a team declares a new axis.** `resources:` in `manifest/roles.yaml` and `manifest/projects.yaml` accepts `env`, `hooks`, `mcp`, `models` and `docs`, but teamai 0.25.0 and the 0.26.0 betas reject a `resources:` key they do not know, so a team that declares one breaks pull for every member still on those versions. From this version on, an unknown `resources:` key prints one warning naming the role or project and the key, and the scope syncs as if the key were absent; `teamai roles` and `teamai projects` keep the key when they save the manifest. `teamai roles|projects add/update --namespaces` never write the new keys, so nothing declares them until an admin does by hand (for [#707](https://github.com/Tencent/teamai-cli/issues/707)).
|
||||
- Per-entry scoping of env variables, hooks and MCP servers gives way to namespace files (see Features). `projects:` on an `env/env.yaml` variable, a `hooks/hooks.yaml` hook or an `mcp/mcp.yaml` server, and `roles:` on an env variable, existed only in the 0.26.0 betas and are removed: such an entry now reaches nobody, and each pull warns with the namespace file to move it to, one per listed id, so a project-only value never falls through to the whole team. `roles:` on hooks and MCP servers, which 0.25.0 shipped, is deprecated: it keeps filtering for one more minor release as 0.25.0 did, a name repeated in one file under different `roles:` included, pull warns once per run and `teamai doctor` has a check, both naming every target file. There is no automatic migration; move each entry into the file the warning names and drop the key. A member with no role in a team with `roles.yaml` received every `roles:`-scoped hook and server; once they move into `hooks/<ns>/` or `mcp/<ns>/`, that member no longer does (for [#707](https://github.com/Tencent/teamai-cli/issues/707)).
|
||||
- Per-entry scoping of env variables, hooks and MCP servers gives way to namespace files (see Features). `projects:` on an `env/env.yaml` variable, a `hooks/hooks.yaml` hook or an `mcp/mcp.yaml` server, and `roles:` on an env variable, existed only in the 0.26.0 betas and are removed: such an entry now reaches nobody, and pull, the list commands and status warn with the namespace file to move it to, one per listed id, so a project-only value never falls through to the whole team. `teamai env add` also warns when it updates an existing variable carrying either removed key, and preserves the key. `roles:` on hooks and MCP servers, which 0.25.0 shipped, is deprecated: it keeps filtering for one more minor release as 0.25.0 did, a name repeated in one file under different `roles:` included, pull warns once per run and `teamai doctor` has a check, both naming every target file. There is no automatic migration; move each entry into the file the warning names and drop the key. A member with no role in a team with `roles.yaml` received every `roles:`-scoped hook and server; once they move into `hooks/<ns>/` or `mcp/<ns>/`, that member no longer does (for [#707](https://github.com/Tencent/teamai-cli/issues/707)).
|
||||
|
||||
### ✨ Features
|
||||
|
||||
|
||||
@@ -418,12 +418,18 @@ declares one of the new axes.
|
||||
|
||||
The per-entry keys go away. `projects:` on env, hooks and MCP, and `roles:` on
|
||||
env, existed only in the 0.26.0 betas: an entry that carries one reaches nobody,
|
||||
and pull warns with the namespace file to move it to, one per listed id.
|
||||
and pull, `status`, `env list`, `mcp list`, `hooks list` and
|
||||
`list <env|hooks|mcp> --source repo` warn with the namespace file to move it to,
|
||||
one per listed id.
|
||||
When `teamai env add` updates a variable still carrying one of these removed
|
||||
keys, it preserves the key and warns that pull will not deliver the variable,
|
||||
naming the namespace file to move it to.
|
||||
`roles:` on hooks and MCP shipped in 0.25.0 and keeps filtering for one more
|
||||
minor release; pull warns once per run and `doctor` has an informational check,
|
||||
both naming every target file. Model profiles are strict, so a per-entry key
|
||||
fails the file. An env, hook or MCP entry with any other key its schema does not
|
||||
know, such as a mistyped `role:`, reaches nobody too, and pull and `doctor` name
|
||||
know, such as a mistyped `role:`, reaches nobody too. Pull, `status`, `env list`,
|
||||
`mcp list`, `hooks list`, `list <env|hooks|mcp> --source repo` and `doctor` name
|
||||
the file, the entry and the key (#822); `env add`, `env remove` and `remove mcp`
|
||||
keep such a key when they rewrite the file. A key that a later version adds is
|
||||
unknown to this one as well, so an entry that uses it is not delivered to a member
|
||||
|
||||
+11
-7
@@ -995,8 +995,9 @@ projects:
|
||||
repeats.
|
||||
- **Where a value comes from.** `teamai env list`, `teamai mcp list`,
|
||||
`teamai hooks list` and `teamai list <env|hooks|mcp> --source repo` show each
|
||||
entry's namespace and whether it overrides the root; `teamai status` counts per
|
||||
namespace; `teamai doctor` lists each override as a note.
|
||||
entry's namespace and whether it overrides the root, and name every entry that
|
||||
is not delivered, with why; `teamai status` counts per namespace and names
|
||||
them too; `teamai doctor` lists each override as a note.
|
||||
- **Upgrade every member first.** teamai 0.25.0 and the 0.26.0 betas reject a
|
||||
`resources:` key they do not know, so declaring `env`, `hooks` or `mcp` breaks
|
||||
their pull. From this version on, an unknown `resources:` key only warns, and
|
||||
@@ -1006,18 +1007,21 @@ The per-entry keys these files replace:
|
||||
|
||||
| Key | On | Now |
|
||||
|---|---|---|
|
||||
| `projects:` | env, hooks, MCP | removed: the entry reaches nobody, and each pull warns with the file to move it to |
|
||||
| `projects:` | env, hooks, MCP | removed: the entry reaches nobody; pull, the list commands and status warn with the file to move it to |
|
||||
| `roles:` | env | removed, the same way |
|
||||
| `roles:` | hooks, MCP | deprecated: still filters for one minor release, as in 0.25.0, including a name the root file repeats under different `roles:`; pull warns and `teamai doctor` has a check, both naming every target file |
|
||||
|
||||
There is no automatic migration: move each entry into the namespace file the
|
||||
warning names, and drop the key.
|
||||
When `teamai env add` updates an existing variable that still carries a removed
|
||||
per-entry `projects:` or `roles:` key, it keeps that key and warns that pull
|
||||
will not deliver the variable, naming the namespace file to move it to.
|
||||
|
||||
An entry with any other key its schema does not know, such as a mistyped `role:`,
|
||||
reaches nobody as well, and pull and `teamai doctor` name the file, the entry and
|
||||
the key. Correct the key or remove it. A key that a later teamai version adds is
|
||||
unknown to an older one too, so upgrade every member before the team uses a new
|
||||
entry key.
|
||||
reaches nobody as well, and pull, the list commands, status and `teamai doctor` name the
|
||||
file, the entry and the key. Correct the key or remove it. A key that a later
|
||||
teamai version adds is unknown to an older one too, so upgrade every member
|
||||
before the team uses a new entry key.
|
||||
|
||||
A hooks or MCP file that has none of its top-level keys, such as `server:` for
|
||||
`servers:`, is treated like a file that does not parse: pull keeps the installed
|
||||
|
||||
@@ -910,8 +910,9 @@ projects:
|
||||
- **旧模式**(成员没有角色,且团队没有 `projects.yaml`)只读取根目录文件,行为不变;
|
||||
`teamai doctor` 会列出根文件中重复的名字。
|
||||
- **值从哪里来。** `teamai env list`、`teamai mcp list`、`teamai hooks list` 与
|
||||
`teamai list <env|hooks|mcp> --source repo` 会给出每个条目的 namespace 以及是否覆盖了
|
||||
根条目;`teamai status` 按 namespace 计数;`teamai doctor` 以提示信息列出每一处覆盖。
|
||||
`teamai list <env|hooks|mcp> --source repo` 会给出每个条目的 namespace、是否覆盖了
|
||||
根条目,并指出每个未下发的条目及其原因;`teamai status` 按 namespace 计数并同样
|
||||
指出它们;`teamai doctor` 以提示信息列出每一处覆盖。
|
||||
- **先让所有成员升级。** teamai 0.25.0 与 0.26.0 beta 会拒绝不认识的 `resources:` key,
|
||||
声明 `env`、`hooks` 或 `mcp` 会让这些版本的 pull 失败。从本版本起,未知的
|
||||
`resources:` key 只会给出警告,`teamai roles` 与 `teamai projects` 保存 manifest 时也会保留它。
|
||||
@@ -920,14 +921,16 @@ projects:
|
||||
|
||||
| Key | 适用于 | 现在 |
|
||||
|---|---|---|
|
||||
| `projects:` | env、hooks、MCP | 已移除:该条目不再下发给任何人,每次 pull 都会警告并给出应迁往的文件 |
|
||||
| `projects:` | env、hooks、MCP | 已移除:该条目不再下发给任何人;pull、各 list 命令和 status 都会警告并给出应迁往的文件 |
|
||||
| `roles:` | env | 已移除,处理方式相同 |
|
||||
| `roles:` | hooks、MCP | 已弃用:在一个次版本内仍像 0.25.0 一样按角色过滤,根文件中以不同 `roles:` 重复的名字也照旧生效;pull 会警告,`teamai doctor` 有一项检查,两者都会列出每个目标文件 |
|
||||
|
||||
没有自动迁移:把每个条目移到警告给出的 namespace 文件中,并删掉该 key。
|
||||
如果 `teamai env add` 更新的已有变量仍带有已移除的按条目 `projects:` 或 `roles:` key,
|
||||
命令会保留该 key,并警告 pull 不会下发这个变量,同时指出应迁往的 namespace 文件。
|
||||
|
||||
条目若带有其 schema 不认识的其他 key(例如拼错的 `role:`),同样不会下发给任何人;
|
||||
pull 与 `teamai doctor` 会指出文件、条目和该 key。请改正或删除这个 key。
|
||||
pull、各 list 命令、status 与 `teamai doctor` 会指出文件、条目和该 key。请改正或删除这个 key。
|
||||
较新版本 teamai 新增的 key 对旧版本同样是未知 key,因此团队使用新的条目 key 之前,
|
||||
请先让所有成员升级。
|
||||
|
||||
|
||||
@@ -171,12 +171,17 @@ and push it with git. `teamai doctor` lists each override.
|
||||
state is kept. Fix the file the warning names. A hooks or MCP file with none of
|
||||
its top-level keys (`server:` for `servers:`) counts as one that does not parse.
|
||||
- Per-entry `projects:` (and `roles:` on env) no longer works: such an entry reaches
|
||||
nobody. `roles:` on hooks and MCP still filters for one more minor release. Pull
|
||||
and `teamai doctor` name the namespace file each entry belongs in; move it there.
|
||||
nobody. `roles:` on hooks and MCP still filters for one more minor release. Pull,
|
||||
the list commands (`teamai env list`, `teamai mcp list`, `teamai hooks list`,
|
||||
`teamai list <env|hooks|mcp> --source repo`), `teamai status` and
|
||||
`teamai doctor` name the namespace file each entry belongs in; move it there.
|
||||
When `teamai env add` updates a variable carrying either removed key, it keeps
|
||||
the key and warns that pull will not deliver the variable, naming that file.
|
||||
- An env, hook or MCP entry with a key its schema does not know (a mistyped `role:`)
|
||||
also reaches nobody. Pull and `teamai doctor` name the file, entry and key; correct
|
||||
the key or remove it. A key a later teamai version adds is unknown to an older one,
|
||||
so upgrade every member before the team uses a new entry key.
|
||||
also reaches nobody. Pull, the list commands, `teamai status` and
|
||||
`teamai doctor` name the file, entry and key; correct the key or remove it.
|
||||
A key a later teamai version adds is unknown to an older one, so upgrade every
|
||||
member before the team uses a new entry key.
|
||||
- Team model profiles work the same way: `models/<ns>/models.yaml`, declared under
|
||||
`resources.models`, replaces the root profile with the same `id` for members who
|
||||
have `<ns>` active. A member's API key is bound to the profile's gateway origin:
|
||||
|
||||
@@ -343,7 +343,10 @@ describe('project-scoped hooks, MCP servers and env variables via the real CLI (
|
||||
const envList = await runCLI(['env', 'list'], projectRoot, home);
|
||||
expect(envList.code, envList.output).toBe(0);
|
||||
expect(envList.output).toMatch(/BILLING_URL=\S+ {2}\(billing\)/);
|
||||
expect(envList.output).not.toContain('DEVOPS_ONLY');
|
||||
// The delivery notice for the withheld per-entry `roles:` key names
|
||||
// DEVOPS_ONLY in its warning; the variable itself must stay out of the
|
||||
// delivered list, where it would print as `DEVOPS_ONLY=<masked>`.
|
||||
expect(envList.output).not.toMatch(/DEVOPS_ONLY=/);
|
||||
}, 60_000);
|
||||
|
||||
it('warns about per-entry projects:, naming the namespace file to move the entry to', async () => {
|
||||
|
||||
@@ -23,6 +23,7 @@ vi.mock('../utils/logger.js', () => ({
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
dim: vi.fn(),
|
||||
persist: vi.fn(),
|
||||
},
|
||||
spinner: vi.fn(() => ({
|
||||
start: vi.fn().mockReturnThis(),
|
||||
@@ -37,6 +38,7 @@ vi.mock('../utils/logger.js', () => ({
|
||||
import { envList, envAdd, envRemove } from '../env-commands.js';
|
||||
import { requireInit } from '../config.js';
|
||||
import { log } from '../utils/logger.js';
|
||||
import { resetWarnOnce } from '../utils/warn-once.js';
|
||||
import { pullRepo } from '../utils/git.js';
|
||||
import type { TeamaiConfig, LocalConfig } from '../types.js';
|
||||
|
||||
@@ -82,6 +84,9 @@ scope: 'user',
|
||||
vi.mocked(log.success).mockClear();
|
||||
vi.mocked(log.error).mockClear();
|
||||
vi.mocked(log.dim).mockClear();
|
||||
vi.mocked(log.warn).mockClear();
|
||||
vi.mocked(log.persist).mockClear();
|
||||
resetWarnOnce();
|
||||
consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
@@ -189,6 +194,40 @@ scope: 'user',
|
||||
|
||||
expect(log.dim).toHaveBeenCalledWith(expect.stringContaining('My API endpoint'));
|
||||
});
|
||||
|
||||
it('names the variable an unknown key takes out of the delivered set (#822)', async () => {
|
||||
await fse.writeFile(
|
||||
path.join(repoPath, 'env', 'env.yaml'),
|
||||
YAML.stringify({
|
||||
variables: [
|
||||
{ key: 'GOOD_URL', value: 'https://good.example' },
|
||||
{ key: 'CACHE_TTL', value: '60', role: ['frontend'] },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
await envList({});
|
||||
|
||||
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('env/env.yaml: variable "CACHE_TTL" has unknown key `role:`, so this entry is not delivered.'));
|
||||
const allOutput = consoleSpy.mock.calls.map(c => c[0]).join('\n');
|
||||
expect(allOutput).toContain('GOOD_URL');
|
||||
expect(allOutput).not.toContain('CACHE_TTL');
|
||||
});
|
||||
|
||||
it('names the variable a removed per-entry key takes out of the delivered set (#822)', async () => {
|
||||
await fse.writeFile(
|
||||
path.join(repoPath, 'env', 'env.yaml'),
|
||||
YAML.stringify({
|
||||
variables: [
|
||||
{ key: 'DB_URL', value: 'postgres://db', roles: ['legacy'] },
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
await envList({});
|
||||
|
||||
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('env/env.yaml: variable "DB_URL" is scoped with per-entry `roles:`, which this version no longer reads, so it reaches nobody.'));
|
||||
});
|
||||
});
|
||||
|
||||
// ─── envAdd ──────────────────────────────────────────────
|
||||
@@ -280,6 +319,71 @@ scope: 'user',
|
||||
});
|
||||
});
|
||||
|
||||
it('says an updated variable it cannot deliver is undelivered, naming the namespace file to move it to (#822)', async () => {
|
||||
// `roles:` on env is no longer read, so this update reaches nobody —
|
||||
// "Updated" alone would read as success.
|
||||
await fse.writeFile(
|
||||
path.join(repoPath, 'env', 'env.yaml'),
|
||||
YAML.stringify({
|
||||
variables: [{ key: 'DB_URL', value: 'old', roles: ['legacy'] }],
|
||||
}),
|
||||
);
|
||||
|
||||
await envAdd('DB_URL', 'new', {});
|
||||
|
||||
// The remedy has to name the namespace file, as pull's notice does:
|
||||
// dropping the key in env/env.yaml would deliver the secret to everyone.
|
||||
// No role or project declares `legacy`, so the notice says which
|
||||
// declaration makes env/legacy/env.yaml reach it.
|
||||
expect(log.warn).toHaveBeenCalledWith(
|
||||
'env/env.yaml: variable "DB_URL" is scoped with per-entry `roles:`, which this version no longer reads, '
|
||||
+ 'so pull does not deliver it. Move it to env/legacy/env.yaml (declare env: [legacy] for role legacy '
|
||||
+ 'in manifest/roles.yaml) and drop the key.',
|
||||
);
|
||||
expect(log.success).toHaveBeenCalledWith('Updated env variable: DB_URL=new');
|
||||
});
|
||||
|
||||
// A role that declares the namespace names the file alone, as pull does.
|
||||
it('names the declared namespace file an updated variable belongs in (#822)', async () => {
|
||||
await fse.outputFile(path.join(repoPath, 'manifest', 'roles.yaml'), YAML.stringify({
|
||||
version: 1,
|
||||
roles: [{ id: 'legacy', description: '', resources: { knowledge: [], skills: [], env: ['legacy'] } }],
|
||||
}));
|
||||
await fse.writeFile(
|
||||
path.join(repoPath, 'env', 'env.yaml'),
|
||||
YAML.stringify({
|
||||
variables: [{ key: 'DB_URL', value: 'old', roles: ['legacy'] }],
|
||||
}),
|
||||
);
|
||||
|
||||
await envAdd('DB_URL', 'new', {});
|
||||
|
||||
expect(log.warn).toHaveBeenCalledWith(
|
||||
'env/env.yaml: variable "DB_URL" is scoped with per-entry `roles:`, which this version no longer reads, '
|
||||
+ 'so pull does not deliver it. Move it to env/legacy/env.yaml and drop the key.',
|
||||
);
|
||||
expect(log.success).toHaveBeenCalledWith('Updated env variable: DB_URL=new');
|
||||
});
|
||||
|
||||
// The same guidance pull gives when no role or project declares the id:
|
||||
// the namespace file the entry belongs in, with the declaration to add.
|
||||
it('names the namespace file to declare when no role declares the removed key\'s id (#822)', async () => { await fse.writeFile(
|
||||
path.join(repoPath, 'env', 'env.yaml'),
|
||||
YAML.stringify({
|
||||
variables: [{ key: 'DB_URL', value: 'old', projects: ['checkout'], roles: ['legacy'] }],
|
||||
}),
|
||||
);
|
||||
|
||||
await envAdd('DB_URL', 'new', {});
|
||||
|
||||
expect(log.warn).toHaveBeenCalledWith(
|
||||
'env/env.yaml: variable "DB_URL" is scoped with per-entry `projects:` and `roles:`, which this version '
|
||||
+ 'no longer reads, so pull does not deliver it. Copy it into each of env/checkout/env.yaml (declare env: '
|
||||
+ '[checkout] for project checkout in manifest/projects.yaml), env/legacy/env.yaml (declare env: [legacy] '
|
||||
+ 'for role legacy in manifest/roles.yaml) and drop the key.',
|
||||
);
|
||||
});
|
||||
|
||||
// A variable with a misspelled `roles:` reaches nobody (#822); a rewrite
|
||||
// that drops the key would deliver it to the whole team.
|
||||
it('preserves a key env does not know on a variable it updates', async () => {
|
||||
|
||||
@@ -35,6 +35,7 @@ vi.mock('../utils/logger.js', () => ({
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
persist: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -45,6 +46,7 @@ import { getHookStatus, reconcileHooks, reconcileHooksToAllTools, reconcileTeamH
|
||||
import { resolveTeamHookEntries } from '../resources/hooks.js';
|
||||
import { log } from '../utils/logger.js';
|
||||
import { hooksInject, hooksRemove, hooksList } from '../hooks-cmd.js';
|
||||
import { resetWarnOnce } from '../utils/warn-once.js';
|
||||
import { TeamaiConfigSchema } from '../types.js';
|
||||
|
||||
const mockedAutoDetectInit = autoDetectInit as Mock;
|
||||
@@ -60,13 +62,13 @@ const mockedParseTeamHooks = resolveTeamHookEntries as Mock;
|
||||
* The resolved team hooks (B), as `[hook, source, replaces]` or a bare hook
|
||||
* from hooks/hooks.yaml, plus the optional builtin override.
|
||||
*/
|
||||
function hooksYaml(hooks: (Record<string, unknown> | [Record<string, unknown>, string, string | null])[], builtin?: unknown) {
|
||||
function hooksYaml(hooks: (Record<string, unknown> | [Record<string, unknown>, string, string | null])[], builtin?: unknown, notices?: { kind: 'unknown-key' | 'removed-key' | 'deprecated-roles' | 'file-note'; message: string }[]) {
|
||||
const entries = hooks.map((hook) => {
|
||||
const [entry, source, replaces] = Array.isArray(hook) ? hook : [hook, 'hooks/hooks.yaml', null];
|
||||
const namespace = source === 'hooks/hooks.yaml' ? null : source.split('/')[1];
|
||||
return { entry, name: entry.id, source, namespace, replaces };
|
||||
});
|
||||
return { resolution: { kind: 'resolved', entries, active: [], notices: [], repeated: [] }, builtin: { known: true, override: builtin } };
|
||||
return { resolution: { kind: 'resolved', entries, active: [], notices: notices ?? [], repeated: [] }, builtin: { known: true, override: builtin } };
|
||||
}
|
||||
const mockedLog = log as unknown as { info: Mock; success: Mock; warn: Mock; error: Mock; debug: Mock };
|
||||
|
||||
@@ -273,6 +275,29 @@ describe('hooksList', () => {
|
||||
expect(text).toContain('[lint] Stop → npm run lint:checkout (tools: all) from checkout, overrides root');
|
||||
expect(text).toContain('[orders] Stop → echo orders (tools: all) from checkout');
|
||||
});
|
||||
|
||||
it('names the hook an unknown key takes out of the delivered set (#822)', async () => {
|
||||
resetWarnOnce();
|
||||
mockedParseTeamHooks.mockResolvedValue(hooksYaml([
|
||||
{ id: 'good-hook', event: 'SessionStart', command: 'echo good', description: 'ok' },
|
||||
], undefined, [{
|
||||
kind: 'unknown-key',
|
||||
message: 'hooks/hooks.yaml: hook "scoped-hook" has unknown key `role:`, so this entry is not delivered. '
|
||||
+ 'Correct the key or remove it.',
|
||||
}]));
|
||||
|
||||
const out: string[] = [];
|
||||
const spy = vi.spyOn(console, 'log').mockImplementation((m?: unknown) => { out.push(String(m)); });
|
||||
try {
|
||||
await hooksList({});
|
||||
} finally {
|
||||
spy.mockRestore();
|
||||
}
|
||||
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('hook "scoped-hook" has unknown key `role:`, so this entry is not delivered.'));
|
||||
const text = out.join('\n');
|
||||
expect(text).toContain('[good-hook] SessionStart');
|
||||
expect(text).not.toContain('scoped-hook]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hooksList', () => {
|
||||
|
||||
@@ -17,13 +17,14 @@ vi.mock('../utils/fs.js', () => ({
|
||||
readJson: vi.fn().mockResolvedValue(null),
|
||||
}));
|
||||
vi.mock('../utils/logger.js', () => ({
|
||||
log: { info: vi.fn(), success: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
||||
log: { info: vi.fn(), success: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), persist: vi.fn() },
|
||||
}));
|
||||
|
||||
import { autoDetectInit } from '../config.js';
|
||||
import { resolveEntriesFor } from '../namespaced-entries.js';
|
||||
import { mcpInject, mcpList } from '../mcp-cmd.js';
|
||||
import { reconcileMcpForConfig } from '../mcp-reconcile.js';
|
||||
import { resetWarnOnce } from '../utils/warn-once.js';
|
||||
|
||||
const mockedAutoDetectInit = autoDetectInit as Mock;
|
||||
const mockedResolve = resolveEntriesFor as Mock;
|
||||
@@ -58,6 +59,7 @@ async function listOutput(): Promise<string> {
|
||||
|
||||
describe('mcpList', () => {
|
||||
beforeEach(() => {
|
||||
resetWarnOnce();
|
||||
mockedAutoDetectInit.mockResolvedValue({
|
||||
localConfig: { repo: { localPath: '/repo' }, scope: 'user', additionalRoles: [] },
|
||||
teamConfig: { toolPaths: {} },
|
||||
@@ -90,14 +92,56 @@ describe('mcpList', () => {
|
||||
it('reports a set that cannot be resolved instead of listing part of it', async () => {
|
||||
mockedResolve.mockResolvedValue({
|
||||
kind: 'failed',
|
||||
notices: [],
|
||||
notices: [{
|
||||
kind: 'unknown-key',
|
||||
message: 'mcp/mcp.yaml: server "hidden" has unknown key `role:`, so this entry is not delivered.',
|
||||
}],
|
||||
failure: { kind: 'two-namespaces', type: 'mcp', name: 'db', first: 'mcp/checkout/mcp.yaml', second: 'mcp/billing/mcp.yaml' },
|
||||
});
|
||||
const { log } = await import('../utils/logger.js');
|
||||
await listOutput();
|
||||
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('server "hidden" has unknown key `role:`'));
|
||||
expect(log.error).toHaveBeenCalledWith(expect.stringContaining('server "db" is defined in both mcp/checkout/mcp.yaml and mcp/billing/mcp.yaml'));
|
||||
process.exitCode = 0;
|
||||
});
|
||||
|
||||
it('names the server a removed per-entry key takes out of the delivered set (#822)', async () => {
|
||||
mockedResolve.mockResolvedValue({
|
||||
...resolved([
|
||||
[{ name: 'good_server', transport: 'stdio', command: 'echo' }, 'mcp/mcp.yaml', null],
|
||||
]),
|
||||
notices: [{
|
||||
kind: 'removed-key' as const,
|
||||
message: 'mcp/mcp.yaml: server "scoped_server" is scoped with per-entry `projects:`, '
|
||||
+ 'which this version no longer reads, so it reaches nobody. '
|
||||
+ 'It lists no id: remove it, or move it to the namespace file it is meant for.',
|
||||
}],
|
||||
});
|
||||
const { log } = await import('../utils/logger.js');
|
||||
const text = await listOutput();
|
||||
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining(
|
||||
'server "scoped_server" is scoped with per-entry `projects:`, which this version no longer reads, so it reaches nobody.',
|
||||
));
|
||||
expect(text).toContain('good_server');
|
||||
expect(text).not.toContain('scoped_server');
|
||||
});
|
||||
|
||||
it('leaves delivered deprecated-role notices to pull and doctor', async () => {
|
||||
mockedResolve.mockResolvedValue({
|
||||
...resolved([[
|
||||
{ name: 'scoped_server', transport: 'http', url: 'https://example.com/mcp', roles: ['worker'] },
|
||||
'mcp/mcp.yaml', null,
|
||||
]]),
|
||||
notices: [{
|
||||
kind: 'deprecated-roles',
|
||||
message: 'mcp/mcp.yaml: server "scoped_server" uses deprecated per-entry `roles:`.',
|
||||
}],
|
||||
});
|
||||
const { log } = await import('../utils/logger.js');
|
||||
vi.mocked(log.warn).mockClear();
|
||||
await listOutput();
|
||||
expect(log.warn).not.toHaveBeenCalledWith(expect.stringContaining('deprecated per-entry `roles:`'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('mcpInject', () => {
|
||||
|
||||
@@ -23,12 +23,14 @@ vi.mock('../utils/logger.js', () => ({
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
dim: vi.fn(),
|
||||
persist: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { list, status } from '../status.js';
|
||||
import type { TeamaiConfig, LocalConfig } from '../types.js';
|
||||
import { log } from '../utils/logger.js';
|
||||
import { resetWarnOnce } from '../utils/warn-once.js';
|
||||
|
||||
function makeTeamConfig(): TeamaiConfig {
|
||||
return {
|
||||
@@ -61,10 +63,12 @@ describe('teamai list / status resource coverage', () => {
|
||||
let tmpDir: string;
|
||||
let homeDir: string;
|
||||
let repoPath: string;
|
||||
let localConfig: LocalConfig;
|
||||
let lines: string[];
|
||||
let spy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(async () => {
|
||||
resetWarnOnce();
|
||||
tmpDir = await fse.mkdtemp(path.join(os.tmpdir(), 'teamai-list-'));
|
||||
homeDir = path.join(tmpDir, 'home');
|
||||
repoPath = path.join(tmpDir, 'repo');
|
||||
@@ -102,7 +106,7 @@ describe('teamai list / status resource coverage', () => {
|
||||
);
|
||||
await fse.writeFile(path.join(repoPath, 'agents', 'reviewer.md'), '# Reviewer\n');
|
||||
|
||||
const localConfig: LocalConfig = {
|
||||
localConfig = {
|
||||
repo: { localPath: repoPath, remote: 'https://example.com/repo.git' },
|
||||
username: 'u',
|
||||
updatePolicy: 'auto',
|
||||
@@ -157,6 +161,47 @@ describe('teamai list / status resource coverage', () => {
|
||||
expect(out).toMatch(/mcp:\s*1/);
|
||||
});
|
||||
|
||||
it('names an undelivered MCP entry even when another entry makes resolution fail', async () => {
|
||||
localConfig.primaryRole = 'worker';
|
||||
await fse.outputFile(path.join(repoPath, 'manifest', 'roles.yaml'), [
|
||||
'version: 1',
|
||||
'roles:',
|
||||
' - id: worker',
|
||||
' resources:',
|
||||
' knowledge: []',
|
||||
' skills: []',
|
||||
' agents: []',
|
||||
' mcp: [one, two]',
|
||||
].join('\n'));
|
||||
await fse.writeFile(path.join(repoPath, 'mcp', 'mcp.yaml'), [
|
||||
'servers:',
|
||||
' - name: hidden',
|
||||
' transport: http',
|
||||
' url: https://example.com/hidden',
|
||||
' role: worker',
|
||||
].join('\n'));
|
||||
for (const namespace of ['one', 'two']) {
|
||||
await fse.outputFile(path.join(repoPath, 'mcp', namespace, 'mcp.yaml'), [
|
||||
'servers:',
|
||||
' - name: duplicate',
|
||||
' transport: http',
|
||||
` url: https://example.com/${namespace}`,
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
vi.mocked(log.warn).mockClear();
|
||||
await status({});
|
||||
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('server "hidden" has unknown key `role:`'));
|
||||
expect(lines.join('\n')).toContain('mcp: 0 (cannot be resolved; run `teamai doctor`)');
|
||||
|
||||
resetWarnOnce();
|
||||
vi.mocked(log.warn).mockClear();
|
||||
lines.length = 0;
|
||||
await list('mcp', { source: 'repo' });
|
||||
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('server "hidden" has unknown key `role:`'));
|
||||
expect(lines.join('\n')).toContain('server "duplicate" is defined in both');
|
||||
});
|
||||
|
||||
it('status counts nested rule files', async () => {
|
||||
await fse.ensureDir(path.join(repoPath, 'rules', 'common'));
|
||||
await fse.writeFile(path.join(repoPath, 'rules', 'common', 'example.md'), '# Rule\n');
|
||||
|
||||
+26
-1
@@ -3,7 +3,7 @@ import { pullRepo } from './utils/git.js';
|
||||
import { pathExists } from './utils/fs.js';
|
||||
import { log, spinner } from './utils/logger.js';
|
||||
import { EnvHandler, maskEnvValue, ENV_KEY_RE, envEntryReader, unknownEnvVariableKeys, type EnvYaml } from './resources/env.js';
|
||||
import { describeEntryFailure, describeOrigin, entryFileAbsolutePath, entryFilePath, entryNamespaceFromFlags, resolveEntriesFor } from './namespaced-entries.js';
|
||||
import { describeEntryFailure, describeOrigin, entryFileAbsolutePath, entryFilePath, entryNamespaceFromFlags, moveTo, reportUndeliveredEntryNotices, resolveEntriesFor, TargetFiles } from './namespaced-entries.js';
|
||||
import type { GlobalOptions, LocalConfig } from './types.js';
|
||||
import { isSelfMode } from './types.js';
|
||||
|
||||
@@ -21,10 +21,14 @@ export async function envList(options: GlobalOptions & { reveal?: boolean }): Pr
|
||||
|
||||
const resolution = await resolveEntriesFor(envEntryReader, localConfig);
|
||||
if (resolution.kind === 'failed') {
|
||||
reportUndeliveredEntryNotices(resolution);
|
||||
log.error(describeEntryFailure(resolution.failure));
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
// An entry an unknown or removed key takes out of the delivered set never
|
||||
// appears in the list below, so say why it is missing (#822).
|
||||
reportUndeliveredEntryNotices(resolution);
|
||||
const variables = resolution.entries;
|
||||
if (variables.length === 0) {
|
||||
log.info('No env variables defined');
|
||||
@@ -102,6 +106,27 @@ export async function envAdd(
|
||||
+ `Correct the ${one ? 'key' : 'keys'} or remove ${one ? 'it' : 'them'} in ${relativePath}.`,
|
||||
);
|
||||
}
|
||||
// Same for a removed per-entry key, which the schema keeps so it can be
|
||||
// detected rather than stripped: `roles:` on env and `projects:` reach nobody.
|
||||
const updated = envConfig.variables[existingIdx];
|
||||
const removed: string[] = [];
|
||||
if (updated.projects !== undefined) removed.push('projects');
|
||||
if (updated.roles !== undefined) removed.push('roles');
|
||||
if (removed.length > 0) {
|
||||
// The remediation has to name the namespace file, as pull's notice does:
|
||||
// dropping a root-scoped key where it sits would deliver the secret to
|
||||
// everyone — the outcome the per-entry key was scoping against.
|
||||
const targets = new TargetFiles(repoPath, 'env');
|
||||
const files: string[] = [];
|
||||
for (const key of removed) {
|
||||
files.push(...await targets.forIds(key as 'roles' | 'projects', updated[key as 'roles' | 'projects'] ?? []));
|
||||
}
|
||||
log.warn(
|
||||
`${relativePath}: variable "${key}" is scoped with per-entry `
|
||||
+ `${removed.map((k) => `\`${k}:\``).join(' and ')}, which this version no longer reads, `
|
||||
+ `so pull does not deliver it. ${moveTo(files)}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const newVar: { key: string; value: string; description?: string } = { key, value };
|
||||
if (options.description) {
|
||||
|
||||
+4
-1
@@ -3,7 +3,7 @@ import { autoDetectInit } from './config.js';
|
||||
import { reconcileHooks, reconcileHooksToAllTools, reconcileTeamHooksForConfig, sweepLegacyProjectHooks, getHookStatus, hasInstalledCodexTrustGatedTool, codexTrustReminder, type HookStatus } from './hooks.js';
|
||||
import { applyBuiltinOverride, installedBuiltinHookDefs } from './builtin-hooks.js';
|
||||
import { resolveTeamHookEntries } from './resources/hooks.js';
|
||||
import { describeEntryFailure, describeOrigin } from './namespaced-entries.js';
|
||||
import { describeEntryFailure, describeOrigin, reportUndeliveredEntryNotices } from './namespaced-entries.js';
|
||||
import { log } from './utils/logger.js';
|
||||
import type { GlobalOptions, HookDef } from './types.js';
|
||||
import {
|
||||
@@ -143,6 +143,9 @@ export async function hooksList(_options: GlobalOptions): Promise<void> {
|
||||
// reconcile engine applies it, so the listing must too or it shows hooks
|
||||
// that were just removed from the settings files.
|
||||
const { resolution: teamHooks, builtin } = await resolveTeamHookEntries(localConfig);
|
||||
// A hook an unknown or removed key takes out of the delivered set never
|
||||
// appears in the team-hooks section below, so say why it is missing (#822).
|
||||
reportUndeliveredEntryNotices(teamHooks);
|
||||
const builtinOverride = builtin.known ? builtin.override : undefined;
|
||||
const rows: HookListRow[] = [];
|
||||
// One settings file is one install, so list it once, for the target that owns
|
||||
|
||||
+5
-1
@@ -1,7 +1,7 @@
|
||||
import path from 'node:path';
|
||||
import { autoDetectInit } from './config.js';
|
||||
import { mcpEntryReader, teamMcpToDef } from './resources/mcp.js';
|
||||
import { describeEntryFailure, describeOrigin, resolveEntriesFor } from './namespaced-entries.js';
|
||||
import { describeEntryFailure, describeOrigin, reportUndeliveredEntryNotices, resolveEntriesFor } from './namespaced-entries.js';
|
||||
import {
|
||||
reconcileMcpForConfig,
|
||||
resolveMcpTargets,
|
||||
@@ -28,10 +28,14 @@ export async function mcpList(_options: GlobalOptions): Promise<void> {
|
||||
const { localConfig, teamConfig } = await autoDetectInit(undefined, { dryRun: true });
|
||||
const resolution = await resolveEntriesFor(mcpEntryReader, localConfig);
|
||||
if (resolution.kind === 'failed') {
|
||||
reportUndeliveredEntryNotices(resolution);
|
||||
log.error(describeEntryFailure(resolution.failure));
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
// A server an unknown or removed key takes out of the delivered set never
|
||||
// appears in the list below, so say why it is missing (#822).
|
||||
reportUndeliveredEntryNotices(resolution);
|
||||
const servers = resolution.entries;
|
||||
|
||||
if (servers.length === 0) {
|
||||
|
||||
@@ -423,19 +423,24 @@ async function keepScopedEntry(
|
||||
return roles === null || scope.roles.some((role) => roles.includes(role));
|
||||
}
|
||||
|
||||
function moveTo(files: string[]): string {
|
||||
/**
|
||||
* Where an entry carrying a removed per-entry key belongs: the namespace files
|
||||
* its listed ids declare, or the removal. Shared with the write path (`env
|
||||
* add`), whose remediation has to name the same file — telling a user to drop
|
||||
* a root-scoped key where it sits would deliver the value to the whole team.
|
||||
*/
|
||||
export function moveTo(files: readonly string[]): string {
|
||||
if (files.length === 0) return 'It lists no id: remove it, or move it to the namespace file it is meant for.';
|
||||
if (files.length === 1) return `Move it to ${files[0]} and drop the key.`;
|
||||
return `Copy it into each of ${files.join(', ')} and drop the key.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace files an id's entries belong in: the namespaces its role or
|
||||
* project declares for the type, or `<type>/<id>/` with the declaration to add
|
||||
* when it declares none. The manifests are read at most once, and only when an
|
||||
* entry carries a per-entry key.
|
||||
*/
|
||||
class TargetFiles {
|
||||
export class TargetFiles {
|
||||
private roles: ReturnType<typeof loadRolesManifestIfPresent> | null = null;
|
||||
private projects: ReturnType<typeof loadProjectsManifest> | null = null;
|
||||
|
||||
@@ -521,13 +526,24 @@ export function describeEntryFailure(failure: EntryFailure): string {
|
||||
* stale entries.
|
||||
*/
|
||||
export function reportEntryResolution(resolution: EntryResolution<unknown>): void {
|
||||
const messages = resolution.notices.map((notice) => notice.message);
|
||||
if (resolution.kind === 'failed') messages.push(describeEntryFailure(resolution.failure));
|
||||
for (const message of messages) {
|
||||
reportNotices(resolution.notices);
|
||||
if (resolution.kind === 'failed') {
|
||||
const message = describeEntryFailure(resolution.failure);
|
||||
if (warnOnce(message)) log.persist(message);
|
||||
}
|
||||
}
|
||||
|
||||
/** List/status report only entries omitted from delivery; pull reports every notice. */
|
||||
export function reportUndeliveredEntryNotices(resolution: Pick<EntryResolution<unknown>, 'notices'>): void {
|
||||
reportNotices(resolution.notices.filter((notice) => notice.kind === 'unknown-key' || notice.kind === 'removed-key'));
|
||||
}
|
||||
|
||||
function reportNotices(notices: readonly EntryNotice[]): void {
|
||||
for (const notice of notices) {
|
||||
if (warnOnce(notice.message)) log.persist(notice.message);
|
||||
}
|
||||
}
|
||||
|
||||
/** Where an entry comes from, for the list commands, `status` and `doctor`. */
|
||||
export function describeOrigin(entry: ResolvedEntry<unknown>): string {
|
||||
if (entry.namespace === null) return 'root';
|
||||
|
||||
+22
-11
@@ -23,7 +23,7 @@ import { mcpEntryReader } from './resources/mcp.js';
|
||||
import { resolveTeamHookEntries } from './resources/hooks.js';
|
||||
import { envEntryReader } from './resources/env.js';
|
||||
import {
|
||||
describeEntryFailure, describeOrigin, describeOrigins, resolveEntriesFor,
|
||||
describeEntryFailure, describeOrigin, describeOrigins, reportUndeliveredEntryNotices, resolveEntriesFor,
|
||||
type EntryResolution, type EntryType,
|
||||
} from './namespaced-entries.js';
|
||||
|
||||
@@ -109,6 +109,9 @@ export async function status(options: GlobalOptions): Promise<void> {
|
||||
counts[type] = resolution.kind === 'resolved' ? resolution.entries.length : 0;
|
||||
if (resolution.kind === 'failed') origins[type] = ' (cannot be resolved; run `teamai doctor`)';
|
||||
else if (resolution.entries.some((entry) => entry.namespace !== null)) origins[type] = ` (${describeOrigins(resolution.entries)})`;
|
||||
// An entry an unknown or removed key takes out of the delivered set is
|
||||
// invisible in the count, so name it here too (#822).
|
||||
reportUndeliveredEntryNotices(resolution);
|
||||
};
|
||||
count('env', await resolveEntriesFor(envEntryReader, localConfig));
|
||||
|
||||
@@ -327,18 +330,22 @@ async function printRepoSection(
|
||||
if (t === 'env') {
|
||||
const env = await resolveEntriesFor(envEntryReader, localConfig);
|
||||
if (env.kind === 'failed') {
|
||||
reportUndeliveredEntryNotices(env);
|
||||
console.log(` ${describeEntryFailure(env.failure)}`);
|
||||
} else if (env.entries.length === 0) {
|
||||
console.log(' (none)');
|
||||
} else {
|
||||
if (options.reveal) {
|
||||
process.stderr.write('[warn] Env values will be shown in plaintext\n');
|
||||
}
|
||||
for (const v of env.entries) {
|
||||
const display = options.reveal ? v.entry.value : maskEnvValue(v.entry.value);
|
||||
console.log(` ${v.name}=${display} (${describeOrigin(v)})`);
|
||||
if (options.verbose && v.entry.description) {
|
||||
console.log(` ${v.entry.description}`);
|
||||
reportUndeliveredEntryNotices(env);
|
||||
if (env.entries.length === 0) {
|
||||
console.log(' (none)');
|
||||
} else {
|
||||
if (options.reveal) {
|
||||
process.stderr.write('[warn] Env values will be shown in plaintext\n');
|
||||
}
|
||||
for (const v of env.entries) {
|
||||
const display = options.reveal ? v.entry.value : maskEnvValue(v.entry.value);
|
||||
console.log(` ${v.name}=${display} (${describeOrigin(v)})`);
|
||||
if (options.verbose && v.entry.description) {
|
||||
console.log(` ${v.entry.description}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -348,9 +355,11 @@ async function printRepoSection(
|
||||
if (t === 'mcp') {
|
||||
const mcp = await resolveEntriesFor(mcpEntryReader, localConfig);
|
||||
if (mcp.kind === 'failed') {
|
||||
reportUndeliveredEntryNotices(mcp);
|
||||
console.log(` ${describeEntryFailure(mcp.failure)}`);
|
||||
return;
|
||||
}
|
||||
reportUndeliveredEntryNotices(mcp);
|
||||
if (mcp.entries.length === 0) {
|
||||
console.log(' (none)');
|
||||
return;
|
||||
@@ -371,9 +380,11 @@ async function printRepoSection(
|
||||
if (t === 'hooks') {
|
||||
const { resolution: hooks } = await resolveTeamHookEntries(localConfig);
|
||||
if (hooks.kind === 'failed') {
|
||||
reportUndeliveredEntryNotices(hooks);
|
||||
console.log(` ${describeEntryFailure(hooks.failure)}`);
|
||||
return;
|
||||
}
|
||||
reportUndeliveredEntryNotices(hooks);
|
||||
if (hooks.entries.length === 0) {
|
||||
console.log(' (none)');
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user