@nicnocquee/dataqueue 1.32.0 → 1.34.0
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/ai/build-docs-content.ts +96 -0
- package/ai/build-llms-full.ts +42 -0
- package/ai/docs-content.json +278 -0
- package/ai/rules/advanced.md +94 -0
- package/ai/rules/basic.md +90 -0
- package/ai/rules/react-dashboard.md +83 -0
- package/ai/skills/dataqueue-advanced/SKILL.md +211 -0
- package/ai/skills/dataqueue-core/SKILL.md +131 -0
- package/ai/skills/dataqueue-react/SKILL.md +189 -0
- package/dist/cli.cjs +1149 -14
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +66 -1
- package/dist/cli.d.ts +66 -1
- package/dist/cli.js +1146 -13
- package/dist/cli.js.map +1 -1
- package/dist/mcp-server.cjs +186 -0
- package/dist/mcp-server.cjs.map +1 -0
- package/dist/mcp-server.d.cts +32 -0
- package/dist/mcp-server.d.ts +32 -0
- package/dist/mcp-server.js +175 -0
- package/dist/mcp-server.js.map +1 -0
- package/package.json +10 -4
- package/src/cli.test.ts +82 -6
- package/src/cli.ts +73 -10
- package/src/init-command.test.ts +449 -0
- package/src/init-command.ts +709 -0
- package/src/install-mcp-command.test.ts +216 -0
- package/src/install-mcp-command.ts +185 -0
- package/src/install-rules-command.test.ts +218 -0
- package/src/install-rules-command.ts +233 -0
- package/src/install-skills-command.test.ts +176 -0
- package/src/install-skills-command.ts +124 -0
- package/src/mcp-server.test.ts +162 -0
- package/src/mcp-server.ts +231 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
runInstallMcp,
|
|
4
|
+
upsertMcpConfig,
|
|
5
|
+
InstallMcpDeps,
|
|
6
|
+
} from './install-mcp-command.js';
|
|
7
|
+
|
|
8
|
+
describe('upsertMcpConfig', () => {
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.restoreAllMocks();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('creates new config file when it does not exist', () => {
|
|
14
|
+
// Setup
|
|
15
|
+
const deps = {
|
|
16
|
+
existsSync: vi.fn(() => false),
|
|
17
|
+
readFileSync: vi.fn(),
|
|
18
|
+
writeFileSync: vi.fn(),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Act
|
|
22
|
+
upsertMcpConfig(
|
|
23
|
+
'/path/mcp.json',
|
|
24
|
+
'dataqueue',
|
|
25
|
+
{ command: 'npx', args: ['dataqueue-cli', 'mcp'] },
|
|
26
|
+
deps,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Assert
|
|
30
|
+
const written = JSON.parse(deps.writeFileSync.mock.calls[0][1] as string);
|
|
31
|
+
expect(written.mcpServers.dataqueue).toEqual({
|
|
32
|
+
command: 'npx',
|
|
33
|
+
args: ['dataqueue-cli', 'mcp'],
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('adds to existing config without overwriting other servers', () => {
|
|
38
|
+
// Setup
|
|
39
|
+
const existing = JSON.stringify({
|
|
40
|
+
mcpServers: { other: { command: 'other' } },
|
|
41
|
+
});
|
|
42
|
+
const deps = {
|
|
43
|
+
existsSync: vi.fn(() => true),
|
|
44
|
+
readFileSync: vi.fn(() => existing),
|
|
45
|
+
writeFileSync: vi.fn(),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Act
|
|
49
|
+
upsertMcpConfig(
|
|
50
|
+
'/path/mcp.json',
|
|
51
|
+
'dataqueue',
|
|
52
|
+
{ command: 'npx', args: ['dataqueue-cli', 'mcp'] },
|
|
53
|
+
deps,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Assert
|
|
57
|
+
const written = JSON.parse(deps.writeFileSync.mock.calls[0][1] as string);
|
|
58
|
+
expect(written.mcpServers.other).toEqual({ command: 'other' });
|
|
59
|
+
expect(written.mcpServers.dataqueue).toEqual({
|
|
60
|
+
command: 'npx',
|
|
61
|
+
args: ['dataqueue-cli', 'mcp'],
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('overwrites existing dataqueue entry', () => {
|
|
66
|
+
// Setup
|
|
67
|
+
const existing = JSON.stringify({
|
|
68
|
+
mcpServers: { dataqueue: { command: 'old' } },
|
|
69
|
+
});
|
|
70
|
+
const deps = {
|
|
71
|
+
existsSync: vi.fn(() => true),
|
|
72
|
+
readFileSync: vi.fn(() => existing),
|
|
73
|
+
writeFileSync: vi.fn(),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Act
|
|
77
|
+
upsertMcpConfig('/path/mcp.json', 'dataqueue', { command: 'new' }, deps);
|
|
78
|
+
|
|
79
|
+
// Assert
|
|
80
|
+
const written = JSON.parse(deps.writeFileSync.mock.calls[0][1] as string);
|
|
81
|
+
expect(written.mcpServers.dataqueue).toEqual({ command: 'new' });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('handles malformed JSON in existing file', () => {
|
|
85
|
+
// Setup
|
|
86
|
+
const deps = {
|
|
87
|
+
existsSync: vi.fn(() => true),
|
|
88
|
+
readFileSync: vi.fn(() => 'not json'),
|
|
89
|
+
writeFileSync: vi.fn(),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Act
|
|
93
|
+
upsertMcpConfig('/path/mcp.json', 'dataqueue', { command: 'npx' }, deps);
|
|
94
|
+
|
|
95
|
+
// Assert
|
|
96
|
+
const written = JSON.parse(deps.writeFileSync.mock.calls[0][1] as string);
|
|
97
|
+
expect(written.mcpServers.dataqueue).toEqual({ command: 'npx' });
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('runInstallMcp', () => {
|
|
102
|
+
afterEach(() => {
|
|
103
|
+
vi.restoreAllMocks();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
function makeDeps(overrides: Partial<InstallMcpDeps> = {}): InstallMcpDeps {
|
|
107
|
+
return {
|
|
108
|
+
log: vi.fn(),
|
|
109
|
+
error: vi.fn(),
|
|
110
|
+
exit: vi.fn(),
|
|
111
|
+
cwd: '/project',
|
|
112
|
+
readFileSync: vi.fn(() => '{}'),
|
|
113
|
+
writeFileSync: vi.fn(),
|
|
114
|
+
mkdirSync: vi.fn(),
|
|
115
|
+
existsSync: vi.fn(() => false),
|
|
116
|
+
...overrides,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
it('installs MCP config for Cursor (option 1)', async () => {
|
|
121
|
+
// Setup
|
|
122
|
+
const deps = makeDeps({ selectedClient: '1' });
|
|
123
|
+
|
|
124
|
+
// Act
|
|
125
|
+
await runInstallMcp(deps);
|
|
126
|
+
|
|
127
|
+
// Assert
|
|
128
|
+
expect(deps.mkdirSync).toHaveBeenCalledWith('/project/.cursor', {
|
|
129
|
+
recursive: true,
|
|
130
|
+
});
|
|
131
|
+
const written = JSON.parse(
|
|
132
|
+
(deps.writeFileSync as ReturnType<typeof vi.fn>).mock
|
|
133
|
+
.calls[0][1] as string,
|
|
134
|
+
);
|
|
135
|
+
expect(written.mcpServers.dataqueue.command).toBe('npx');
|
|
136
|
+
expect(written.mcpServers.dataqueue.args).toEqual(['dataqueue-cli', 'mcp']);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('installs MCP config for Claude Code (option 2)', async () => {
|
|
140
|
+
// Setup
|
|
141
|
+
const deps = makeDeps({ selectedClient: '2' });
|
|
142
|
+
|
|
143
|
+
// Act
|
|
144
|
+
await runInstallMcp(deps);
|
|
145
|
+
|
|
146
|
+
// Assert
|
|
147
|
+
expect(deps.writeFileSync).toHaveBeenCalledWith(
|
|
148
|
+
'/project/.mcp.json',
|
|
149
|
+
expect.any(String),
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('installs MCP config for VS Code (option 3)', async () => {
|
|
154
|
+
// Setup
|
|
155
|
+
const deps = makeDeps({ selectedClient: '3' });
|
|
156
|
+
|
|
157
|
+
// Act
|
|
158
|
+
await runInstallMcp(deps);
|
|
159
|
+
|
|
160
|
+
// Assert
|
|
161
|
+
expect(deps.mkdirSync).toHaveBeenCalledWith('/project/.vscode', {
|
|
162
|
+
recursive: true,
|
|
163
|
+
});
|
|
164
|
+
expect(deps.writeFileSync).toHaveBeenCalledWith(
|
|
165
|
+
expect.stringContaining('.vscode/mcp.json'),
|
|
166
|
+
expect.any(String),
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('exits with error for invalid choice', async () => {
|
|
171
|
+
// Setup
|
|
172
|
+
const deps = makeDeps({ selectedClient: '99' });
|
|
173
|
+
|
|
174
|
+
// Act
|
|
175
|
+
await runInstallMcp(deps);
|
|
176
|
+
|
|
177
|
+
// Assert
|
|
178
|
+
expect(deps.error).toHaveBeenCalledWith(
|
|
179
|
+
expect.stringContaining('Invalid choice'),
|
|
180
|
+
);
|
|
181
|
+
expect(deps.exit).toHaveBeenCalledWith(1);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('handles install errors', async () => {
|
|
185
|
+
// Setup
|
|
186
|
+
const deps = makeDeps({
|
|
187
|
+
selectedClient: '1',
|
|
188
|
+
writeFileSync: vi.fn(() => {
|
|
189
|
+
throw new Error('permission denied');
|
|
190
|
+
}),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Act
|
|
194
|
+
await runInstallMcp(deps);
|
|
195
|
+
|
|
196
|
+
// Assert
|
|
197
|
+
expect(deps.error).toHaveBeenCalledWith(
|
|
198
|
+
'Failed to install MCP config:',
|
|
199
|
+
expect.any(Error),
|
|
200
|
+
);
|
|
201
|
+
expect(deps.exit).toHaveBeenCalledWith(1);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('logs done message on success', async () => {
|
|
205
|
+
// Setup
|
|
206
|
+
const deps = makeDeps({ selectedClient: '1' });
|
|
207
|
+
|
|
208
|
+
// Act
|
|
209
|
+
await runInstallMcp(deps);
|
|
210
|
+
|
|
211
|
+
// Assert
|
|
212
|
+
expect(deps.log).toHaveBeenCalledWith(
|
|
213
|
+
expect.stringContaining('npx dataqueue-cli mcp'),
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import readline from 'readline';
|
|
4
|
+
|
|
5
|
+
export interface InstallMcpDeps {
|
|
6
|
+
log?: (...args: unknown[]) => void;
|
|
7
|
+
error?: (...args: unknown[]) => void;
|
|
8
|
+
exit?: (code: number) => void;
|
|
9
|
+
cwd?: string;
|
|
10
|
+
readFileSync?: (p: string, enc: BufferEncoding) => string;
|
|
11
|
+
writeFileSync?: (p: string, data: string) => void;
|
|
12
|
+
mkdirSync?: (p: string, opts?: fs.MakeDirectoryOptions) => void;
|
|
13
|
+
existsSync?: (p: string) => boolean;
|
|
14
|
+
/** Override for selecting the client (skips interactive prompt). */
|
|
15
|
+
selectedClient?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface McpClientConfig {
|
|
19
|
+
label: string;
|
|
20
|
+
install: (
|
|
21
|
+
deps: Required<
|
|
22
|
+
Pick<
|
|
23
|
+
InstallMcpDeps,
|
|
24
|
+
| 'cwd'
|
|
25
|
+
| 'readFileSync'
|
|
26
|
+
| 'writeFileSync'
|
|
27
|
+
| 'mkdirSync'
|
|
28
|
+
| 'existsSync'
|
|
29
|
+
| 'log'
|
|
30
|
+
>
|
|
31
|
+
>,
|
|
32
|
+
) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Merges the dataqueue MCP server config into an existing JSON config file.
|
|
37
|
+
*
|
|
38
|
+
* @param filePath - Path to the MCP config file.
|
|
39
|
+
* @param serverKey - Key name for the server entry.
|
|
40
|
+
* @param serverConfig - Server configuration object.
|
|
41
|
+
* @param deps - Injectable file system functions.
|
|
42
|
+
*/
|
|
43
|
+
export function upsertMcpConfig(
|
|
44
|
+
filePath: string,
|
|
45
|
+
serverKey: string,
|
|
46
|
+
serverConfig: Record<string, unknown>,
|
|
47
|
+
deps: {
|
|
48
|
+
readFileSync: (p: string, enc: BufferEncoding) => string;
|
|
49
|
+
writeFileSync: (p: string, data: string) => void;
|
|
50
|
+
existsSync: (p: string) => boolean;
|
|
51
|
+
},
|
|
52
|
+
): void {
|
|
53
|
+
let config: Record<string, unknown> = {};
|
|
54
|
+
|
|
55
|
+
if (deps.existsSync(filePath)) {
|
|
56
|
+
try {
|
|
57
|
+
config = JSON.parse(deps.readFileSync(filePath, 'utf-8'));
|
|
58
|
+
} catch {
|
|
59
|
+
config = {};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!config.mcpServers || typeof config.mcpServers !== 'object') {
|
|
64
|
+
config.mcpServers = {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
(config.mcpServers as Record<string, unknown>)[serverKey] = serverConfig;
|
|
68
|
+
deps.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const MCP_SERVER_CONFIG = {
|
|
72
|
+
command: 'npx',
|
|
73
|
+
args: ['dataqueue-cli', 'mcp'],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const MCP_CLIENTS: Record<string, McpClientConfig> = {
|
|
77
|
+
'1': {
|
|
78
|
+
label: 'Cursor',
|
|
79
|
+
install: (deps) => {
|
|
80
|
+
const configDir = path.join(deps.cwd, '.cursor');
|
|
81
|
+
deps.mkdirSync(configDir, { recursive: true });
|
|
82
|
+
const configFile = path.join(configDir, 'mcp.json');
|
|
83
|
+
upsertMcpConfig(configFile, 'dataqueue', MCP_SERVER_CONFIG, deps);
|
|
84
|
+
deps.log(` ✓ .cursor/mcp.json`);
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
'2': {
|
|
88
|
+
label: 'Claude Code',
|
|
89
|
+
install: (deps) => {
|
|
90
|
+
const configFile = path.join(deps.cwd, '.mcp.json');
|
|
91
|
+
upsertMcpConfig(configFile, 'dataqueue', MCP_SERVER_CONFIG, deps);
|
|
92
|
+
deps.log(` ✓ .mcp.json`);
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
'3': {
|
|
96
|
+
label: 'VS Code (Copilot)',
|
|
97
|
+
install: (deps) => {
|
|
98
|
+
const configDir = path.join(deps.cwd, '.vscode');
|
|
99
|
+
deps.mkdirSync(configDir, { recursive: true });
|
|
100
|
+
const configFile = path.join(configDir, 'mcp.json');
|
|
101
|
+
upsertMcpConfig(configFile, 'dataqueue', MCP_SERVER_CONFIG, deps);
|
|
102
|
+
deps.log(` ✓ .vscode/mcp.json`);
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
'4': {
|
|
106
|
+
label: 'Windsurf',
|
|
107
|
+
install: (deps) => {
|
|
108
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
109
|
+
const configFile = path.join(
|
|
110
|
+
homeDir,
|
|
111
|
+
'.codeium',
|
|
112
|
+
'windsurf',
|
|
113
|
+
'mcp_config.json',
|
|
114
|
+
);
|
|
115
|
+
deps.mkdirSync(path.dirname(configFile), { recursive: true });
|
|
116
|
+
upsertMcpConfig(configFile, 'dataqueue', MCP_SERVER_CONFIG, deps);
|
|
117
|
+
deps.log(` ✓ ~/.codeium/windsurf/mcp_config.json`);
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Installs the DataQueue MCP server config for the selected AI client.
|
|
124
|
+
*
|
|
125
|
+
* @param deps - Injectable dependencies for testing.
|
|
126
|
+
*/
|
|
127
|
+
export async function runInstallMcp({
|
|
128
|
+
log = console.log,
|
|
129
|
+
error = console.error,
|
|
130
|
+
exit = (code: number) => process.exit(code),
|
|
131
|
+
cwd = process.cwd(),
|
|
132
|
+
readFileSync = fs.readFileSync,
|
|
133
|
+
writeFileSync = fs.writeFileSync,
|
|
134
|
+
mkdirSync = fs.mkdirSync,
|
|
135
|
+
existsSync = fs.existsSync,
|
|
136
|
+
selectedClient,
|
|
137
|
+
}: InstallMcpDeps = {}): Promise<void> {
|
|
138
|
+
log('DataQueue MCP Server Installer\n');
|
|
139
|
+
log('Select your AI client:\n');
|
|
140
|
+
|
|
141
|
+
for (const [key, client] of Object.entries(MCP_CLIENTS)) {
|
|
142
|
+
log(` ${key}) ${client.label}`);
|
|
143
|
+
}
|
|
144
|
+
log('');
|
|
145
|
+
|
|
146
|
+
let choice = selectedClient;
|
|
147
|
+
|
|
148
|
+
if (!choice) {
|
|
149
|
+
const rl = readline.createInterface({
|
|
150
|
+
input: process.stdin,
|
|
151
|
+
output: process.stdout,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
choice = await new Promise<string>((resolve) => {
|
|
155
|
+
rl.question('Enter choice (1-4): ', (answer) => {
|
|
156
|
+
rl.close();
|
|
157
|
+
resolve(answer.trim());
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const client = MCP_CLIENTS[choice];
|
|
163
|
+
if (!client) {
|
|
164
|
+
error(`Invalid choice: "${choice}". Expected 1-4.`);
|
|
165
|
+
exit(1);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
log(`\nInstalling MCP config for ${client.label}...`);
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
client.install({
|
|
173
|
+
cwd,
|
|
174
|
+
readFileSync,
|
|
175
|
+
writeFileSync,
|
|
176
|
+
mkdirSync,
|
|
177
|
+
existsSync,
|
|
178
|
+
log,
|
|
179
|
+
});
|
|
180
|
+
log('\nDone! The MCP server will run via: npx dataqueue-cli mcp');
|
|
181
|
+
} catch (err) {
|
|
182
|
+
error('Failed to install MCP config:', err);
|
|
183
|
+
exit(1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
runInstallRules,
|
|
4
|
+
upsertMarkedSection,
|
|
5
|
+
InstallRulesDeps,
|
|
6
|
+
} from './install-rules-command.js';
|
|
7
|
+
|
|
8
|
+
describe('upsertMarkedSection', () => {
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.restoreAllMocks();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('creates file with markers when file does not exist', () => {
|
|
14
|
+
// Setup
|
|
15
|
+
const deps = {
|
|
16
|
+
existsSync: vi.fn(() => false),
|
|
17
|
+
readFileSync: vi.fn(),
|
|
18
|
+
writeFileSync: vi.fn(),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Act
|
|
22
|
+
upsertMarkedSection('/path/file.md', 'content here', deps);
|
|
23
|
+
|
|
24
|
+
// Assert
|
|
25
|
+
expect(deps.writeFileSync).toHaveBeenCalledWith(
|
|
26
|
+
'/path/file.md',
|
|
27
|
+
expect.stringContaining('<!-- DATAQUEUE RULES START -->'),
|
|
28
|
+
);
|
|
29
|
+
expect(deps.writeFileSync).toHaveBeenCalledWith(
|
|
30
|
+
'/path/file.md',
|
|
31
|
+
expect.stringContaining('content here'),
|
|
32
|
+
);
|
|
33
|
+
expect(deps.writeFileSync).toHaveBeenCalledWith(
|
|
34
|
+
'/path/file.md',
|
|
35
|
+
expect.stringContaining('<!-- DATAQUEUE RULES END -->'),
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('replaces existing marked section', () => {
|
|
40
|
+
// Setup
|
|
41
|
+
const existing =
|
|
42
|
+
'before\n<!-- DATAQUEUE RULES START -->\nold content\n<!-- DATAQUEUE RULES END -->\nafter';
|
|
43
|
+
const deps = {
|
|
44
|
+
existsSync: vi.fn(() => true),
|
|
45
|
+
readFileSync: vi.fn(() => existing),
|
|
46
|
+
writeFileSync: vi.fn(),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Act
|
|
50
|
+
upsertMarkedSection('/path/file.md', 'new content', deps);
|
|
51
|
+
|
|
52
|
+
// Assert
|
|
53
|
+
const written = deps.writeFileSync.mock.calls[0][1] as string;
|
|
54
|
+
expect(written).toContain('before\n');
|
|
55
|
+
expect(written).toContain('new content');
|
|
56
|
+
expect(written).not.toContain('old content');
|
|
57
|
+
expect(written).toContain('\nafter');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('appends to file when no markers exist', () => {
|
|
61
|
+
// Setup
|
|
62
|
+
const deps = {
|
|
63
|
+
existsSync: vi.fn(() => true),
|
|
64
|
+
readFileSync: vi.fn(() => '# Existing content'),
|
|
65
|
+
writeFileSync: vi.fn(),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Act
|
|
69
|
+
upsertMarkedSection('/path/file.md', 'new content', deps);
|
|
70
|
+
|
|
71
|
+
// Assert
|
|
72
|
+
const written = deps.writeFileSync.mock.calls[0][1] as string;
|
|
73
|
+
expect(written).toContain('# Existing content');
|
|
74
|
+
expect(written).toContain('new content');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('runInstallRules', () => {
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
vi.restoreAllMocks();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
function makeDeps(
|
|
84
|
+
overrides: Partial<InstallRulesDeps> = {},
|
|
85
|
+
): InstallRulesDeps {
|
|
86
|
+
return {
|
|
87
|
+
log: vi.fn(),
|
|
88
|
+
error: vi.fn(),
|
|
89
|
+
exit: vi.fn(),
|
|
90
|
+
cwd: '/project',
|
|
91
|
+
readFileSync: vi.fn(() => '# Rule content'),
|
|
92
|
+
writeFileSync: vi.fn(),
|
|
93
|
+
appendFileSync: vi.fn(),
|
|
94
|
+
mkdirSync: vi.fn(),
|
|
95
|
+
existsSync: vi.fn(() => false),
|
|
96
|
+
rulesSourceDir: '/pkg/ai/rules',
|
|
97
|
+
...overrides,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
it('installs rules for Cursor (option 1)', async () => {
|
|
102
|
+
// Setup
|
|
103
|
+
const deps = makeDeps({ selectedClient: '1' });
|
|
104
|
+
|
|
105
|
+
// Act
|
|
106
|
+
await runInstallRules(deps);
|
|
107
|
+
|
|
108
|
+
// Assert
|
|
109
|
+
expect(deps.mkdirSync).toHaveBeenCalledWith('/project/.cursor/rules', {
|
|
110
|
+
recursive: true,
|
|
111
|
+
});
|
|
112
|
+
expect(deps.writeFileSync).toHaveBeenCalledWith(
|
|
113
|
+
expect.stringContaining('.cursor/rules/dataqueue-basic.mdc'),
|
|
114
|
+
expect.any(String),
|
|
115
|
+
);
|
|
116
|
+
expect(deps.writeFileSync).toHaveBeenCalledWith(
|
|
117
|
+
expect.stringContaining('.cursor/rules/dataqueue-advanced.mdc'),
|
|
118
|
+
expect.any(String),
|
|
119
|
+
);
|
|
120
|
+
expect(deps.writeFileSync).toHaveBeenCalledWith(
|
|
121
|
+
expect.stringContaining('.cursor/rules/dataqueue-react-dashboard.mdc'),
|
|
122
|
+
expect.any(String),
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('installs rules for Claude Code (option 2)', async () => {
|
|
127
|
+
// Setup
|
|
128
|
+
const deps = makeDeps({ selectedClient: '2' });
|
|
129
|
+
|
|
130
|
+
// Act
|
|
131
|
+
await runInstallRules(deps);
|
|
132
|
+
|
|
133
|
+
// Assert
|
|
134
|
+
expect(deps.writeFileSync).toHaveBeenCalledWith(
|
|
135
|
+
'/project/CLAUDE.md',
|
|
136
|
+
expect.stringContaining('<!-- DATAQUEUE RULES START -->'),
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('installs rules for AGENTS.md (option 3)', async () => {
|
|
141
|
+
// Setup
|
|
142
|
+
const deps = makeDeps({ selectedClient: '3' });
|
|
143
|
+
|
|
144
|
+
// Act
|
|
145
|
+
await runInstallRules(deps);
|
|
146
|
+
|
|
147
|
+
// Assert
|
|
148
|
+
expect(deps.writeFileSync).toHaveBeenCalledWith(
|
|
149
|
+
'/project/AGENTS.md',
|
|
150
|
+
expect.stringContaining('<!-- DATAQUEUE RULES START -->'),
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('installs rules for GitHub Copilot (option 4)', async () => {
|
|
155
|
+
// Setup
|
|
156
|
+
const deps = makeDeps({ selectedClient: '4' });
|
|
157
|
+
|
|
158
|
+
// Act
|
|
159
|
+
await runInstallRules(deps);
|
|
160
|
+
|
|
161
|
+
// Assert
|
|
162
|
+
expect(deps.mkdirSync).toHaveBeenCalledWith('/project/.github', {
|
|
163
|
+
recursive: true,
|
|
164
|
+
});
|
|
165
|
+
expect(deps.writeFileSync).toHaveBeenCalledWith(
|
|
166
|
+
'/project/.github/copilot-instructions.md',
|
|
167
|
+
expect.stringContaining('<!-- DATAQUEUE RULES START -->'),
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('installs rules for Windsurf (option 5)', async () => {
|
|
172
|
+
// Setup
|
|
173
|
+
const deps = makeDeps({ selectedClient: '5' });
|
|
174
|
+
|
|
175
|
+
// Act
|
|
176
|
+
await runInstallRules(deps);
|
|
177
|
+
|
|
178
|
+
// Assert
|
|
179
|
+
expect(deps.writeFileSync).toHaveBeenCalledWith(
|
|
180
|
+
'/project/CONVENTIONS.md',
|
|
181
|
+
expect.stringContaining('<!-- DATAQUEUE RULES START -->'),
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('exits with error for invalid choice', async () => {
|
|
186
|
+
// Setup
|
|
187
|
+
const deps = makeDeps({ selectedClient: '99' });
|
|
188
|
+
|
|
189
|
+
// Act
|
|
190
|
+
await runInstallRules(deps);
|
|
191
|
+
|
|
192
|
+
// Assert
|
|
193
|
+
expect(deps.error).toHaveBeenCalledWith(
|
|
194
|
+
expect.stringContaining('Invalid choice'),
|
|
195
|
+
);
|
|
196
|
+
expect(deps.exit).toHaveBeenCalledWith(1);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('handles install errors', async () => {
|
|
200
|
+
// Setup
|
|
201
|
+
const deps = makeDeps({
|
|
202
|
+
selectedClient: '1',
|
|
203
|
+
readFileSync: vi.fn(() => {
|
|
204
|
+
throw new Error('file not found');
|
|
205
|
+
}),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Act
|
|
209
|
+
await runInstallRules(deps);
|
|
210
|
+
|
|
211
|
+
// Assert
|
|
212
|
+
expect(deps.error).toHaveBeenCalledWith(
|
|
213
|
+
'Failed to install rules:',
|
|
214
|
+
expect.any(Error),
|
|
215
|
+
);
|
|
216
|
+
expect(deps.exit).toHaveBeenCalledWith(1);
|
|
217
|
+
});
|
|
218
|
+
});
|