feat: replace profiles with kubernetes-style secrets

Replace the confused Profile abstraction with a dedicated Secret resource
following Kubernetes conventions. Servers now have env entries with inline
values or secretRef references. Env vars are resolved and passed to
containers at startup (fixes existing gap).

- Add Secret CRUD (model, repo, service, routes, CLI commands)
- Server env: {name, value} or {name, valueFrom: {secretRef: {name, key}}}
- Add env-resolver utility shared by instance startup and config generation
- Remove all profile-related code (models, services, routes, CLI, tests)
- Update backup/restore for secrets instead of profiles
- describe secret masks values by default, --show-values to reveal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michal
2026-02-22 18:40:58 +00:00
parent ede9e10990
commit 6d9a9f572c
77 changed files with 1014 additions and 1931 deletions

View File

@@ -30,7 +30,7 @@ describe('describe command', () => {
transport: 'STDIO',
packageName: '@slack/mcp',
dockerImage: null,
envTemplate: [],
env: [],
createdAt: '2025-01-01',
});
const cmd = createDescribeCommand(deps);
@@ -50,10 +50,10 @@ describe('describe command', () => {
});
it('resolves resource aliases', async () => {
const deps = makeDeps({ id: 'p1' });
const deps = makeDeps({ id: 's1' });
const cmd = createDescribeCommand(deps);
await cmd.parseAsync(['node', 'test', 'prof', 'p1']);
expect(deps.fetchResource).toHaveBeenCalledWith('profiles', 'p1');
await cmd.parseAsync(['node', 'test', 'sec', 's1']);
expect(deps.fetchResource).toHaveBeenCalledWith('secrets', 's1');
});
it('outputs JSON format', async () => {
@@ -72,26 +72,6 @@ describe('describe command', () => {
expect(deps.output[0]).toContain('name: slack');
});
it('shows profile with permissions and env overrides', async () => {
const deps = makeDeps({
id: 'p1',
name: 'production',
serverId: 'srv-1',
permissions: ['read', 'write'],
envOverrides: { FOO: 'bar', SECRET: 's3cr3t' },
createdAt: '2025-01-01',
});
const cmd = createDescribeCommand(deps);
await cmd.parseAsync(['node', 'test', 'profile', 'p1']);
const text = deps.output.join('\n');
expect(text).toContain('=== Profile: production ===');
expect(text).toContain('read, write');
expect(text).toContain('Environment Overrides:');
expect(text).toContain('FOO');
expect(text).toContain('bar');
});
it('shows project detail', async () => {
const deps = makeDeps({
id: 'proj-1',
@@ -109,6 +89,39 @@ describe('describe command', () => {
expect(text).toContain('user-1');
});
it('shows secret detail with masked values', async () => {
const deps = makeDeps({
id: 'sec-1',
name: 'ha-creds',
data: { TOKEN: 'abc123', URL: 'https://ha.local' },
createdAt: '2025-01-01',
});
const cmd = createDescribeCommand(deps);
await cmd.parseAsync(['node', 'test', 'secret', 'sec-1']);
const text = deps.output.join('\n');
expect(text).toContain('=== Secret: ha-creds ===');
expect(text).toContain('TOKEN');
expect(text).toContain('***');
expect(text).not.toContain('abc123');
expect(text).toContain('use --show-values to reveal');
});
it('shows secret detail with revealed values when --show-values', async () => {
const deps = makeDeps({
id: 'sec-1',
name: 'ha-creds',
data: { TOKEN: 'abc123' },
createdAt: '2025-01-01',
});
const cmd = createDescribeCommand(deps);
await cmd.parseAsync(['node', 'test', 'secret', 'sec-1', '--show-values']);
const text = deps.output.join('\n');
expect(text).toContain('abc123');
expect(text).not.toContain('***');
});
it('shows instance detail with container info', async () => {
const deps = makeDeps({
id: 'inst-1',