Skip to content

Commit a99267e

Browse files
bespoyasovclaude
andauthored
feat(cli): introduce warehouse commands (#111)
* feat(cli): introduce `warehouse` commands * refactor: reduce duplication in MCP error handling * feat(cli): extend `warehouse` commands with `setup` * test: cover warehouse setup * fix(cli): use PLUGIN_NAME for warehouse setup skill reference The prompt hardcoded the wrong plugin prefix ("onboard-confidence") causing the agent to fail skill lookup. Use PLUGIN_NAME constant to match the pattern in migrate/prompt.ts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli): set non-zero exit code for failed validation in JSON mode The JSON early return skipped the process.exitCode = 1 check, so `warehouse validate --format json` exited 0 on failure. Move the exit code before the format branch and make fail() accept an optional message so it can set the exit code without printing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli): stringify non-string config-json from --from-file When --from-file provides configJson as a nested object, it was passed through to MCP tools as-is instead of as a JSON string. Add ensureJsonString to shared-kernel and apply it in all four warehouse commands (create, validate, create-flag-applied, create-event). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli): validate warehouse type from --from-file input The yargs choices constraint only applies to CLI args, so --from-file could pass invalid warehouse types straight to the server. Add validateWarehouseType after resolveInput in all four warehouse commands. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(cli): validate config-json locally and narrow warehouse type Move ensureJsonString and validateJsonString from shared-kernel to core/utils with tests. Make validators return their input for inline chaining. Add WarehouseType union and narrow in validateWarehouseType. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(cli): type WAREHOUSE_SKILLS with WarehouseType, drop dead check Use Record<WarehouseType, string> so the compiler enforces key completeness. Replace the unsafe cast with validateWarehouseType. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(cli): extract launchSkillChat shared helper The plugin/MCP/auth-check-then-launch-chat sequence was duplicated across warehouse/setup, sdk/setup, and migrate/launch. Extract into a shared launchSkillChat helper in features/ide/. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(cli): deduplicate assignment-table key list Extract the six param keys into a single const shared by resolveInput and requireKeys. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 28063cd commit a99267e

34 files changed

Lines changed: 1228 additions & 106 deletions

‎.claude/skills/cli/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ For parent commands with subcommands, use `noop` from `@spotify-confidence/share
3030
### Command Types
3131

3232
- **Standalone** — `login`, `logout`, `whoami`, `config`, `update` — directly perform their action
33-
- **API commands** — `flags`, `events`, `recordings`, `docs` — CRUD operations against Confidence APIs via `@network/*`
33+
- **API commands** — `flags`, `events`, `recordings`, `docs`, `warehouse` — CRUD operations against Confidence APIs via `@network/*`
3434
- **Integration** — `mcp`, `plugin`, `sdk`, `migrate` — manage IDE tooling and SDK setup
3535
- **Setup** — `flags setup`, `events setup`, `recordings setup` — delegate to quickstart TUI with pre-selected features
3636
- **TUI launcher** — `quickstart` — launches the full interactive wizard

‎AGENTS.md‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ CLI tools for setting up and integrating [Confidence](https://confidence.spotify
66

77
pnpm workspace with six packages under `packages/`:
88

9-
| Package | Published | Purpose |
10-
| ------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
11-
| `packages/shared-kernel/` | No (private) | Cross-domain types (`AuthState`, `IdeId`, `OnboardingGoal`, etc.) and helpers (`noop`, `isDefined`). No runtime dependencies. |
12-
| `packages/eslint-config/` | No (private) | Shared ESLint configuration. Exports base preset and `/react` preset with React Hooks rules. |
13-
| `packages/core/` | No (private) | Shared infrastructure — api, auth, config, session, telemetry, exec, system, sdk, mcp, utils, constants, frameworks, integrations, providers. Depends on `shared-kernel`. |
9+
| Package | Published | Purpose |
10+
| ------------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11+
| `packages/shared-kernel/` | No (private) | Cross-domain types (`AuthState`, `IdeId`, `OnboardingGoal`, etc.) and helpers (`noop`, `isDefined`). No runtime dependencies. |
12+
| `packages/eslint-config/` | No (private) | Shared ESLint configuration. Exports base preset and `/react` preset with React Hooks rules. |
13+
| `packages/core/` | No (private) | Shared infrastructure — api, auth, config, session, telemetry, exec, system, sdk, mcp, utils, constants, frameworks, integrations, providers. Depends on `shared-kernel`. |
1414
| `packages/testing/` | No (private) | Test infrastructure — auth scaffolds, project scaffolds, env helpers, terminal helpers, MSW handlers. Sub-path exports: `/auth`, `/scaffold`, `/env`, `/terminal`, `/msw`, `/e2e`. Depends on `shared-kernel`. |
15-
| `packages/quickstart/` | Yes (`@spotify-confidence/quickstart`) | Interactive TUI wizard. Depends on `core` and `shared-kernel`. |
16-
| `packages/cli/` | Yes (`@spotify-confidence/cli`) | CLI for managing Confidence (flags, events, recordings, config). Depends on `quickstart`. |
15+
| `packages/quickstart/` | Yes (`@spotify-confidence/quickstart`) | Interactive TUI wizard. Depends on `core` and `shared-kernel`. |
16+
| `packages/cli/` | Yes (`@spotify-confidence/cli`) | CLI for managing Confidence (flags, events, recordings, config). Depends on `quickstart`. |
1717

1818
### Dependency graph
1919

@@ -53,11 +53,11 @@ shared-kernel (types-only leaf)
5353
### packages/cli/ structure
5454

5555
- **`bin/cli.ts`** — Entry point (yargs, `confidence` binary)
56-
- **`src/commands/`** — Command definitions (login, logout, whoami, config, flags, events, recordings, docs, mcp, plugin, sdk, migrate, update, quickstart)
57-
- **`src/features/`** — Feature implementations (config, docs, events, flags, ide, mcp, migrate, plugin, quickstart, recordings, sdk, update)
56+
- **`src/commands/`** — Command definitions (login, logout, whoami, config, flags, events, recordings, docs, mcp, plugin, sdk, migrate, update, quickstart, warehouse)
57+
- **`src/features/`** — Feature implementations (config, docs, events, flags, ide, mcp, migrate, plugin, quickstart, recordings, sdk, update, warehouse)
5858
- **`src/input/`** — Input parsing (file reading, aliases, resolve)
5959
- **`src/output/`** — Output formatters (json, table, format detection)
60-
- **`src/network/`** — API clients (flags, events, recordings, docs, config, registry)
60+
- **`src/network/`** — API clients (flags, events, recordings, docs, config, registry, warehouse)
6161
- **`src/utils/`** — Shared utilities (auth, safely, telemetry, validation)
6262

6363
## Key Patterns

‎packages/cli/README.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,17 @@ confidence quickstart # Launch the interactive setup wizard
7272
| `confidence recordings targeting-key show <client>` | Show available targeting keys |
7373
| `confidence recordings targeting-key add` | Add a targeting key to a client |
7474

75+
### Data Warehouses
76+
77+
| Command | Description |
78+
| ---------------------------------------------------- | -------------------------------------------------- |
79+
| `confidence warehouse validate` | Validate warehouse configuration |
80+
| `confidence warehouse create` | Create a data warehouse connection |
81+
| `confidence warehouse connector create-flag-applied` | Create flag assignment data connector |
82+
| `confidence warehouse connector create-event` | Create event data connector |
83+
| `confidence warehouse assignment-table create` | Create an assignment table for experiment analysis |
84+
| `confidence warehouse crypto-key create` | Create a crypto key (Snowflake) |
85+
7586
### MCP Servers
7687

7788
| Command | Description |
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { buildTestJwt, prepareAuthTokens } from '@spotify-confidence/testing/auth';
2+
import { warehouseCommand } from '@commands/warehouse.js';
3+
import { captureOutput } from '../helpers/capture.js';
4+
import { createMockIntegration } from '../helpers/mock-integration.js';
5+
import { createRunner } from '../helpers/run-command.js';
6+
7+
const mockIntegration = createMockIntegration({
8+
detectPlugin: vi.fn().mockResolvedValue('cli'),
9+
detectMcpStatuses: vi.fn().mockResolvedValue({
10+
'confidence-flags': 'connected',
11+
'confidence-docs': 'connected',
12+
}),
13+
});
14+
15+
const mockAuthenticate = vi.fn().mockResolvedValue({
16+
accessToken: buildTestJwt({ email: 'test@example.com' }),
17+
region: 'EU',
18+
});
19+
20+
vi.mock('@spotify-confidence/core', async (importOriginal) => {
21+
const actual = await importOriginal<typeof import('@spotify-confidence/core')>();
22+
return {
23+
...actual,
24+
getIntegration: () => mockIntegration,
25+
getIntegrations: () => [mockIntegration],
26+
authenticate: (...args: unknown[]) => mockAuthenticate(...args),
27+
};
28+
});
29+
30+
const run = createRunner(warehouseCommand);
31+
32+
describe('warehouse setup', () => {
33+
it('launches chat session with correct skill for bigquery', async () => {
34+
using _auth = prepareAuthTokens('valid');
35+
using _output = captureOutput();
36+
37+
await run(['warehouse', 'setup', '--warehouse-type', 'bigquery', '--ide', 'claude']);
38+
39+
expect(mockIntegration.launchChat).toHaveBeenCalledWith(
40+
expect.objectContaining({
41+
userPrompt: expect.stringContaining('setup-warehouse-bigquery'),
42+
cwd: process.cwd(),
43+
}),
44+
);
45+
});
46+
47+
it('launches chat session with correct skill for snowflake', async () => {
48+
using _auth = prepareAuthTokens('valid');
49+
using _output = captureOutput();
50+
51+
await run(['warehouse', 'setup', '--warehouse-type', 'snowflake', '--ide', 'claude']);
52+
53+
expect(mockIntegration.launchChat).toHaveBeenCalledWith(
54+
expect.objectContaining({
55+
userPrompt: expect.stringContaining('setup-warehouse-snowflake'),
56+
}),
57+
);
58+
});
59+
60+
it('passes auth token to chat session', async () => {
61+
using _auth = prepareAuthTokens('valid');
62+
using _output = captureOutput();
63+
64+
await run(['warehouse', 'setup', '--warehouse-type', 'bigquery', '--ide', 'claude']);
65+
66+
expect(mockIntegration.launchChat).toHaveBeenCalledWith(
67+
expect.objectContaining({ token: expect.any(String) }),
68+
);
69+
});
70+
71+
it('fails when plugin is not installed', async () => {
72+
using _auth = prepareAuthTokens('valid');
73+
using output = captureOutput();
74+
vi.mocked(mockIntegration.detectPlugin).mockResolvedValueOnce(null);
75+
76+
await run(['warehouse', 'setup', '--warehouse-type', 'bigquery', '--ide', 'claude']);
77+
78+
expect(output.stderr).toContain('Confidence AI plugin not installed');
79+
expect(mockIntegration.launchChat).not.toHaveBeenCalled();
80+
});
81+
82+
it('fails when MCP servers are not installed', async () => {
83+
using _auth = prepareAuthTokens('valid');
84+
using output = captureOutput();
85+
vi.mocked(mockIntegration.detectMcpStatuses).mockResolvedValueOnce({
86+
'confidence-flags': 'not-installed',
87+
'confidence-docs': 'not-installed',
88+
});
89+
90+
await run(['warehouse', 'setup', '--warehouse-type', 'bigquery', '--ide', 'claude']);
91+
92+
expect(output.stderr).toContain('MCP servers not installed');
93+
expect(mockIntegration.launchChat).not.toHaveBeenCalled();
94+
});
95+
96+
it('fails when MCP auth is expired', async () => {
97+
using _auth = prepareAuthTokens('valid');
98+
using output = captureOutput();
99+
vi.mocked(mockIntegration.detectMcpStatuses).mockResolvedValueOnce({
100+
'confidence-flags': 'auth-expired',
101+
'confidence-docs': 'connected',
102+
});
103+
104+
await run(['warehouse', 'setup', '--warehouse-type', 'bigquery', '--ide', 'claude']);
105+
106+
expect(output.stderr).toContain('MCP server auth expired');
107+
expect(mockIntegration.launchChat).not.toHaveBeenCalled();
108+
});
109+
110+
it('fails when authentication is unavailable', async () => {
111+
using _auth = prepareAuthTokens('none');
112+
using output = captureOutput();
113+
mockAuthenticate.mockRejectedValueOnce(new Error('Authentication timed out'));
114+
115+
await run(['warehouse', 'setup', '--warehouse-type', 'bigquery', '--ide', 'claude']);
116+
117+
expect(output.stderr).toContain('Authentication failed');
118+
expect(mockIntegration.launchChat).not.toHaveBeenCalled();
119+
});
120+
});

0 commit comments

Comments
 (0)