@sysprompthub/cli 1.1.0 → 2.0.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/README.md +83 -41
- package/dist/commands/agent-add.js +50 -0
- package/dist/commands/agent-commands.test.js +119 -0
- package/dist/commands/agent-remove.js +31 -0
- package/dist/commands/assistant-clear.js +50 -0
- package/dist/commands/assistant-commands.test.js +97 -0
- package/dist/commands/assistant-set.js +38 -0
- package/dist/commands/base-command.js +1 -0
- package/dist/commands/claude.js +23 -0
- package/dist/commands/cli-integration.test.js +8 -0
- package/dist/commands/copilot.js +23 -0
- package/dist/commands/custom-add.js +48 -0
- package/dist/commands/custom-commands.test.js +122 -0
- package/dist/commands/custom-remove.js +25 -0
- package/dist/commands/custom.js +12 -0
- package/dist/commands/init.js +13 -164
- package/dist/commands/init.test.js +26 -77
- package/dist/commands/show.js +99 -0
- package/dist/commands/show.test.js +242 -0
- package/dist/commands/sync.js +12 -10
- package/dist/commands/sync.test.js +35 -25
- package/dist/config-helpers.js +35 -0
- package/dist/index.js +11 -1
- package/dist/version.js +1 -0
- package/package.json +14 -8
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { CustomAddCommand } from './custom-add.js';
|
|
3
|
+
import { CustomRemoveCommand } from './custom-remove.js';
|
|
4
|
+
vi.mock('../config-helpers.js', () => ({
|
|
5
|
+
loadProjectConfigEditor: vi.fn(),
|
|
6
|
+
validatePackName: vi.fn()
|
|
7
|
+
}));
|
|
8
|
+
vi.mock('is-valid-path', () => ({
|
|
9
|
+
default: vi.fn()
|
|
10
|
+
}));
|
|
11
|
+
describe('CustomAddCommand', () => {
|
|
12
|
+
let command;
|
|
13
|
+
let helpers;
|
|
14
|
+
let mockEditor;
|
|
15
|
+
let isValidPath;
|
|
16
|
+
let consoleErrorSpy;
|
|
17
|
+
let consoleLogSpy;
|
|
18
|
+
let processExitSpy;
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
helpers = await import('../config-helpers.js');
|
|
21
|
+
const pathModule = await import('is-valid-path');
|
|
22
|
+
isValidPath = vi.mocked(pathModule.default);
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
mockEditor = {
|
|
25
|
+
packs: [],
|
|
26
|
+
addCustom: vi.fn()
|
|
27
|
+
};
|
|
28
|
+
vi.mocked(helpers.loadProjectConfigEditor).mockResolvedValue(mockEditor);
|
|
29
|
+
vi.mocked(helpers.validatePackName).mockReturnValue(true);
|
|
30
|
+
isValidPath.mockReturnValue(true);
|
|
31
|
+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
32
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
33
|
+
processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
34
|
+
command = new CustomAddCommand();
|
|
35
|
+
});
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
consoleErrorSpy.mockRestore();
|
|
38
|
+
consoleLogSpy.mockRestore();
|
|
39
|
+
processExitSpy.mockRestore();
|
|
40
|
+
});
|
|
41
|
+
describe('run', () => {
|
|
42
|
+
it('should add custom pack', async () => {
|
|
43
|
+
await command.run('docs/prompt.md', 'owner/pack/latest', {});
|
|
44
|
+
expect(mockEditor.addCustom).toHaveBeenCalledWith({
|
|
45
|
+
path: 'docs/prompt.md',
|
|
46
|
+
pack: 'owner/pack/latest',
|
|
47
|
+
environment: undefined
|
|
48
|
+
});
|
|
49
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('✓ Custom pack added: docs/prompt.md -> owner/pack/latest');
|
|
50
|
+
});
|
|
51
|
+
it('should exit with error on absolute path', async () => {
|
|
52
|
+
await command.run('/absolute/path.md', 'owner/pack/latest', {});
|
|
53
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Error: Path must be relative to the current working directory');
|
|
54
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
55
|
+
expect(mockEditor.addCustom).not.toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
it('should exit with error on invalid path', async () => {
|
|
58
|
+
isValidPath.mockReturnValue(false);
|
|
59
|
+
await command.run('invalid|path', 'owner/pack/latest', {});
|
|
60
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Error: Invalid path format');
|
|
61
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
62
|
+
expect(mockEditor.addCustom).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
it('should exit with error on invalid pack name', async () => {
|
|
65
|
+
vi.mocked(helpers.validatePackName).mockReturnValue(false);
|
|
66
|
+
await command.run('docs/prompt.md', 'invalid-pack', {});
|
|
67
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Error: Invalid pack name: invalid-pack');
|
|
68
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
69
|
+
expect(mockEditor.addCustom).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe('CustomRemoveCommand', () => {
|
|
74
|
+
let command;
|
|
75
|
+
let helpers;
|
|
76
|
+
let mockEditor;
|
|
77
|
+
let consoleErrorSpy;
|
|
78
|
+
let consoleLogSpy;
|
|
79
|
+
let consoleWarnSpy;
|
|
80
|
+
let processExitSpy;
|
|
81
|
+
beforeEach(async () => {
|
|
82
|
+
helpers = await import('../config-helpers.js');
|
|
83
|
+
vi.clearAllMocks();
|
|
84
|
+
mockEditor = {
|
|
85
|
+
packs: [],
|
|
86
|
+
removeCustom: vi.fn()
|
|
87
|
+
};
|
|
88
|
+
vi.mocked(helpers.loadProjectConfigEditor).mockResolvedValue(mockEditor);
|
|
89
|
+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
90
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
91
|
+
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
92
|
+
processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
93
|
+
command = new CustomRemoveCommand();
|
|
94
|
+
});
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
consoleErrorSpy.mockRestore();
|
|
97
|
+
consoleLogSpy.mockRestore();
|
|
98
|
+
consoleWarnSpy.mockRestore();
|
|
99
|
+
processExitSpy.mockRestore();
|
|
100
|
+
});
|
|
101
|
+
describe('run', () => {
|
|
102
|
+
it('should remove custom pack', async () => {
|
|
103
|
+
vi.mocked(mockEditor.removeCustom).mockResolvedValue(true);
|
|
104
|
+
await command.run('docs/prompt.md');
|
|
105
|
+
expect(mockEditor.removeCustom).toHaveBeenCalledWith('docs/prompt.md');
|
|
106
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('✓ Custom pack removed: docs/prompt.md');
|
|
107
|
+
});
|
|
108
|
+
it('should exit with error when pack not found', async () => {
|
|
109
|
+
vi.mocked(mockEditor.removeCustom).mockResolvedValue(false);
|
|
110
|
+
await command.run('docs/prompt.md');
|
|
111
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith("Error: Custom pack with path 'docs/prompt.md' not found");
|
|
112
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
113
|
+
});
|
|
114
|
+
it('should warn when file deletion fails', async () => {
|
|
115
|
+
const error = new Error('File not found');
|
|
116
|
+
vi.mocked(mockEditor.removeCustom).mockResolvedValue(error);
|
|
117
|
+
await command.run('docs/prompt.md');
|
|
118
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('✓ Custom pack removed: docs/prompt.md');
|
|
119
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith('⚠ Warning: Could not delete prompt file: File not found');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { loadProjectConfigEditor } from '../config-helpers.js';
|
|
3
|
+
export class CustomRemoveCommand {
|
|
4
|
+
async run(path) {
|
|
5
|
+
const editor = await loadProjectConfigEditor();
|
|
6
|
+
const result = await editor.removeCustom(path);
|
|
7
|
+
if (result === false) {
|
|
8
|
+
console.error(`Error: Custom pack with path '${path}' not found`);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
console.log(`✓ Custom pack removed: ${path}`);
|
|
13
|
+
if (result !== true) {
|
|
14
|
+
console.warn(`⚠ Warning: Could not delete prompt file: ${result.message}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
create() {
|
|
18
|
+
const command = new Command('remove');
|
|
19
|
+
command
|
|
20
|
+
.description('Remove a custom path configuration')
|
|
21
|
+
.argument('<path>', 'Path to remove')
|
|
22
|
+
.action(async (path) => await this.run(path));
|
|
23
|
+
return command;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { CustomAddCommand } from './custom-add.js';
|
|
3
|
+
import { CustomRemoveCommand } from './custom-remove.js';
|
|
4
|
+
export function createCustomCommand() {
|
|
5
|
+
const custom = new Command('custom');
|
|
6
|
+
custom.description('Manage custom path configurations');
|
|
7
|
+
const addCmd = new CustomAddCommand();
|
|
8
|
+
const removeCmd = new CustomRemoveCommand();
|
|
9
|
+
custom.addCommand(addCmd.create());
|
|
10
|
+
custom.addCommand(removeCmd.create());
|
|
11
|
+
return custom;
|
|
12
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -1,53 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { input, confirm } from '@inquirer/prompts';
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import { API_KEY_LENGTH,
|
|
4
|
-
import { isAbsolute } from 'path';
|
|
5
|
-
import isValidPath from 'is-valid-path';
|
|
3
|
+
import { API_KEY_LENGTH, NodeFileSystem, UserConfigManager } from '@sysprompthub/sdk';
|
|
6
4
|
import open from 'open';
|
|
7
5
|
import { BaseCommand } from "./base-command.js";
|
|
8
6
|
export class InitCommand extends BaseCommand {
|
|
9
7
|
fs = new NodeFileSystem();
|
|
10
|
-
workspaceConfigManager = new DefaultConfigManager();
|
|
11
8
|
userConfigManager = new UserConfigManager();
|
|
12
|
-
async run(
|
|
13
|
-
console.log('Welcome to SysPromptHub
|
|
14
|
-
// Load existing
|
|
9
|
+
async run(_options) {
|
|
10
|
+
console.log('Welcome to SysPromptHub!\n');
|
|
11
|
+
// Load existing user config
|
|
15
12
|
const existingUserConfig = await this.userConfigManager.load(this.fs);
|
|
16
|
-
|
|
17
|
-
// Step 1: API Key (user-level)
|
|
13
|
+
// Prompt for API Key
|
|
18
14
|
const apiKey = await this.promptForApiKey(existingUserConfig?.apiKey);
|
|
19
15
|
await this.userConfigManager.save(this.fs, { apiKey });
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const client = new SysPromptHubClient(apiKey, workspaceConfig.environment);
|
|
26
|
-
workspaceConfig.packName = await this.promptForPackName(client, workspaceConfig.packName);
|
|
27
|
-
// Step 3: Assistants (workspace-level)
|
|
28
|
-
workspaceConfig.assistants = await this.promptForAssistants(workspaceConfig.assistants);
|
|
29
|
-
// Step 4: Custom Path (workspace-level, if no assistants selected)
|
|
30
|
-
if (!workspaceConfig.assistants || workspaceConfig.assistants.length === 0) {
|
|
31
|
-
workspaceConfig.path = await this.promptForCustomPath(workspaceConfig.path);
|
|
32
|
-
}
|
|
33
|
-
// Save workspace configuration
|
|
34
|
-
await this.workspaceConfigManager.update(this.fs, workspaceConfig);
|
|
35
|
-
console.log('\n✓ User config saved to ~/.sysprompthub/config.json');
|
|
36
|
-
console.log('✓ Project config saved to .sysprompthub.json');
|
|
37
|
-
// Ask if user wants to sync now
|
|
38
|
-
const shouldSync = await confirm({
|
|
39
|
-
message: 'Would you like to sync your prompts now?',
|
|
40
|
-
default: true
|
|
41
|
-
});
|
|
42
|
-
if (shouldSync) {
|
|
43
|
-
console.log('\nSyncing prompts...');
|
|
44
|
-
const { SyncCommand } = await import('./sync.js');
|
|
45
|
-
const syncCommand = new SyncCommand();
|
|
46
|
-
await syncCommand.run({});
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
console.log('Run "sysprompthub sync" to download your prompts.\n');
|
|
50
|
-
}
|
|
16
|
+
console.log('\n✓ API key saved to ~/.sysprompthub/config.json');
|
|
17
|
+
console.log('\nNext steps:');
|
|
18
|
+
console.log(' • Set primary pack: sysprompthub copilot set <pack>');
|
|
19
|
+
console.log(' • Add an agent: sysprompthub copilot agent add <name> <pack>');
|
|
20
|
+
console.log(' • Run sync: sysprompthub sync\n');
|
|
51
21
|
}
|
|
52
22
|
async promptForApiKey(existing) {
|
|
53
23
|
const apiKey = await input({
|
|
@@ -79,132 +49,11 @@ export class InitCommand extends BaseCommand {
|
|
|
79
49
|
}
|
|
80
50
|
return apiKey;
|
|
81
51
|
}
|
|
82
|
-
async promptForPackName(client, existing) {
|
|
83
|
-
if (existing) {
|
|
84
|
-
const keepExisting = await confirm({
|
|
85
|
-
message: `Keep existing pack "${existing}"?`,
|
|
86
|
-
default: true
|
|
87
|
-
});
|
|
88
|
-
if (keepExisting) {
|
|
89
|
-
return existing;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
const packInfo = await this.searchAndSelectPack(client);
|
|
93
|
-
const version = await this.selectPackVersion(packInfo.name, packInfo.latestVersion);
|
|
94
|
-
return `${packInfo.name}/${version}`;
|
|
95
|
-
}
|
|
96
|
-
async searchAndSelectPack(client) {
|
|
97
|
-
const packResult = await search({
|
|
98
|
-
message: 'Type to search for packs...',
|
|
99
|
-
source: async (term) => {
|
|
100
|
-
if (!term || term.length < 3) {
|
|
101
|
-
return [];
|
|
102
|
-
}
|
|
103
|
-
try {
|
|
104
|
-
const results = await client.searchPacks(term);
|
|
105
|
-
return results.map(result => ({
|
|
106
|
-
name: result.name,
|
|
107
|
-
value: JSON.stringify({ name: result.name, latestVersion: result.version }),
|
|
108
|
-
description: `Current version: ${result.version}`
|
|
109
|
-
}));
|
|
110
|
-
}
|
|
111
|
-
catch (error) {
|
|
112
|
-
return [];
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
return JSON.parse(packResult);
|
|
117
|
-
}
|
|
118
|
-
async selectPackVersion(packName, latestVersion) {
|
|
119
|
-
const versionChoice = await search({
|
|
120
|
-
message: `Select version for ${packName}:`,
|
|
121
|
-
source: async () => {
|
|
122
|
-
const choices = [
|
|
123
|
-
{
|
|
124
|
-
name: 'latest',
|
|
125
|
-
value: 'latest',
|
|
126
|
-
description: 'Always use the latest version'
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
name: `current (v${latestVersion})`,
|
|
130
|
-
value: String(latestVersion),
|
|
131
|
-
description: 'Use current version and manually update'
|
|
132
|
-
}
|
|
133
|
-
];
|
|
134
|
-
if (latestVersion > 1) {
|
|
135
|
-
choices.push({
|
|
136
|
-
name: '(other)',
|
|
137
|
-
value: 'custom',
|
|
138
|
-
description: 'Choose an older version'
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
return choices;
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
if (versionChoice === 'custom') {
|
|
145
|
-
return await this.promptForCustomVersion(packName, latestVersion);
|
|
146
|
-
}
|
|
147
|
-
return versionChoice === 'latest' ? 'latest' : Number(versionChoice);
|
|
148
|
-
}
|
|
149
|
-
async promptForCustomVersion(packName, latestVersion) {
|
|
150
|
-
const customVersion = await input({
|
|
151
|
-
message: `Enter version number for ${packName} (1-${latestVersion}):`,
|
|
152
|
-
default: String(latestVersion),
|
|
153
|
-
validate: (value) => {
|
|
154
|
-
if (!value) {
|
|
155
|
-
return 'Version is required';
|
|
156
|
-
}
|
|
157
|
-
if (!SysPromptHubClient.validateVersion(value, latestVersion)) {
|
|
158
|
-
return `Version must be between 1 and ${latestVersion}`;
|
|
159
|
-
}
|
|
160
|
-
return true;
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
return Number(customVersion);
|
|
164
|
-
}
|
|
165
|
-
async promptForAssistants(existing) {
|
|
166
|
-
const am = new AssistantManager();
|
|
167
|
-
const choices = [];
|
|
168
|
-
am.forEach((assistant) => {
|
|
169
|
-
choices.push({
|
|
170
|
-
value: assistant.type,
|
|
171
|
-
name: assistant.type,
|
|
172
|
-
checked: existing?.includes(assistant.type) || assistant.type === 'copilot'
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
choices.push({
|
|
176
|
-
value: 'other',
|
|
177
|
-
name: 'Other (custom path)',
|
|
178
|
-
checked: existing?.includes('other')
|
|
179
|
-
});
|
|
180
|
-
const selected = await checkbox({
|
|
181
|
-
message: 'Select your coding assistants:',
|
|
182
|
-
choices
|
|
183
|
-
});
|
|
184
|
-
return selected.filter((s) => s !== 'other');
|
|
185
|
-
}
|
|
186
|
-
async promptForCustomPath(existing) {
|
|
187
|
-
const path = await input({
|
|
188
|
-
message: 'Enter the location where you want to save the prompt:',
|
|
189
|
-
default: existing,
|
|
190
|
-
validate: (value) => {
|
|
191
|
-
if (value.length === 0)
|
|
192
|
-
return true;
|
|
193
|
-
if (isAbsolute(value))
|
|
194
|
-
return 'Path must be relative to the current working directory';
|
|
195
|
-
if (!isValidPath(value))
|
|
196
|
-
return 'Please enter a valid path';
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
return path.length > 0 ? path : undefined;
|
|
201
|
-
}
|
|
202
52
|
create() {
|
|
203
53
|
const command = new Command('init');
|
|
204
54
|
command
|
|
205
|
-
.description('Initialize
|
|
206
|
-
.
|
|
207
|
-
.action(async (env) => await this.run(env));
|
|
55
|
+
.description('Initialize API key configuration')
|
|
56
|
+
.action(async (options) => await this.run(options));
|
|
208
57
|
return command;
|
|
209
58
|
}
|
|
210
59
|
}
|
|
@@ -2,8 +2,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
2
2
|
import { InitCommand } from './init.js';
|
|
3
3
|
vi.mock('@inquirer/prompts', () => ({
|
|
4
4
|
input: vi.fn(),
|
|
5
|
-
search: vi.fn(),
|
|
6
|
-
checkbox: vi.fn(),
|
|
7
5
|
confirm: vi.fn()
|
|
8
6
|
}));
|
|
9
7
|
vi.mock('open', () => ({
|
|
@@ -16,20 +14,10 @@ vi.mock('@sysprompthub/sdk', () => {
|
|
|
16
14
|
write: vi.fn(),
|
|
17
15
|
createDir: vi.fn()
|
|
18
16
|
};
|
|
19
|
-
const mockWorkspaceConfig = {
|
|
20
|
-
load: vi.fn(),
|
|
21
|
-
update: vi.fn()
|
|
22
|
-
};
|
|
23
17
|
const mockUserConfig = {
|
|
24
18
|
load: vi.fn(),
|
|
25
19
|
save: vi.fn()
|
|
26
20
|
};
|
|
27
|
-
const mockClient = {
|
|
28
|
-
searchPacks: vi.fn()
|
|
29
|
-
};
|
|
30
|
-
const mockAssistantManager = {
|
|
31
|
-
forEach: vi.fn()
|
|
32
|
-
};
|
|
33
21
|
return {
|
|
34
22
|
NodeFileSystem: class {
|
|
35
23
|
exists = mockFs.exists;
|
|
@@ -37,27 +25,14 @@ vi.mock('@sysprompthub/sdk', () => {
|
|
|
37
25
|
write = mockFs.write;
|
|
38
26
|
createDir = mockFs.createDir;
|
|
39
27
|
},
|
|
40
|
-
DefaultConfigManager: class {
|
|
41
|
-
load = mockWorkspaceConfig.load;
|
|
42
|
-
update = mockWorkspaceConfig.update;
|
|
43
|
-
},
|
|
44
28
|
UserConfigManager: class {
|
|
45
29
|
load = mockUserConfig.load;
|
|
46
30
|
save = mockUserConfig.save;
|
|
47
31
|
},
|
|
48
|
-
SysPromptHubClient: class {
|
|
49
|
-
searchPacks = mockClient.searchPacks;
|
|
50
|
-
},
|
|
51
|
-
AssistantManager: class {
|
|
52
|
-
forEach = mockAssistantManager.forEach;
|
|
53
|
-
},
|
|
54
32
|
API_KEY_LENGTH: 40,
|
|
55
33
|
__mocks: {
|
|
56
34
|
mockFs,
|
|
57
|
-
|
|
58
|
-
mockUserConfig,
|
|
59
|
-
mockClient,
|
|
60
|
-
mockAssistantManager
|
|
35
|
+
mockUserConfig
|
|
61
36
|
}
|
|
62
37
|
};
|
|
63
38
|
});
|
|
@@ -74,8 +49,6 @@ describe('InitCommand', () => {
|
|
|
74
49
|
mocks = sdk.__mocks;
|
|
75
50
|
promptMocks = {
|
|
76
51
|
input: vi.mocked(prompts.input),
|
|
77
|
-
search: vi.mocked(prompts.search),
|
|
78
|
-
checkbox: vi.mocked(prompts.checkbox),
|
|
79
52
|
confirm: vi.mocked(prompts.confirm)
|
|
80
53
|
};
|
|
81
54
|
openMock = vi.mocked(open.default);
|
|
@@ -84,18 +57,9 @@ describe('InitCommand', () => {
|
|
|
84
57
|
mocks.mockFs.read.mockReset();
|
|
85
58
|
mocks.mockFs.write.mockReset();
|
|
86
59
|
mocks.mockFs.createDir.mockReset();
|
|
87
|
-
mocks.mockWorkspaceConfig.load.mockReset().mockResolvedValue(null);
|
|
88
|
-
mocks.mockWorkspaceConfig.update.mockReset();
|
|
89
60
|
mocks.mockUserConfig.load.mockReset().mockResolvedValue(null);
|
|
90
61
|
mocks.mockUserConfig.save.mockReset();
|
|
91
|
-
mocks.mockClient.searchPacks.mockReset().mockResolvedValue([]);
|
|
92
|
-
mocks.mockAssistantManager.forEach.mockReset().mockImplementation((cb) => {
|
|
93
|
-
cb({ type: 'copilot' });
|
|
94
|
-
cb({ type: 'claude' });
|
|
95
|
-
});
|
|
96
62
|
promptMocks.input.mockReset();
|
|
97
|
-
promptMocks.search.mockReset();
|
|
98
|
-
promptMocks.checkbox.mockReset();
|
|
99
63
|
promptMocks.confirm.mockReset();
|
|
100
64
|
openMock.mockReset();
|
|
101
65
|
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
@@ -108,62 +72,47 @@ describe('InitCommand', () => {
|
|
|
108
72
|
it('should create a Command with correct configuration', () => {
|
|
109
73
|
const command = initCommand.create();
|
|
110
74
|
expect(command.name()).toBe('init');
|
|
111
|
-
expect(command.description()).toBe('Initialize
|
|
75
|
+
expect(command.description()).toBe('Initialize API key configuration');
|
|
112
76
|
});
|
|
113
77
|
});
|
|
114
78
|
describe('run', () => {
|
|
115
|
-
it('should prompt for API key
|
|
79
|
+
it('should prompt for API key and save it', async () => {
|
|
116
80
|
promptMocks.input.mockResolvedValue('a'.repeat(40));
|
|
117
|
-
promptMocks.search
|
|
118
|
-
.mockResolvedValueOnce(JSON.stringify({ name: 'owner/pack', version: 5 })) // Pack search
|
|
119
|
-
.mockResolvedValueOnce('latest'); // Version selection
|
|
120
|
-
promptMocks.checkbox.mockResolvedValue(['copilot']);
|
|
121
81
|
await initCommand.run({});
|
|
122
82
|
expect(promptMocks.input).toHaveBeenCalled();
|
|
123
|
-
expect(promptMocks.search).toHaveBeenCalledTimes(2);
|
|
124
|
-
expect(promptMocks.checkbox).toHaveBeenCalled();
|
|
125
83
|
expect(mocks.mockUserConfig.save).toHaveBeenCalledWith(expect.any(Object), { apiKey: 'a'.repeat(40) });
|
|
126
|
-
expect(
|
|
127
|
-
packName: 'owner/pack/latest',
|
|
128
|
-
assistants: ['copilot']
|
|
129
|
-
}));
|
|
84
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('\n✓ API key saved to ~/.sysprompthub/config.json');
|
|
130
85
|
});
|
|
131
86
|
it('should keep existing API key if blank entered', async () => {
|
|
132
87
|
mocks.mockUserConfig.load.mockResolvedValue({ apiKey: 'existing'.padEnd(40, 'x') });
|
|
133
|
-
promptMocks.input.mockResolvedValueOnce('');
|
|
134
|
-
promptMocks.search
|
|
135
|
-
.mockResolvedValueOnce(JSON.stringify({ name: 'owner/pack', version: 5 })) // Pack search
|
|
136
|
-
.mockResolvedValueOnce('latest'); // Version selection
|
|
137
|
-
promptMocks.checkbox.mockResolvedValue(['copilot']);
|
|
88
|
+
promptMocks.input.mockResolvedValueOnce('');
|
|
138
89
|
await initCommand.run({});
|
|
139
90
|
expect(mocks.mockUserConfig.save).toHaveBeenCalledWith(expect.any(Object), { apiKey: 'existing'.padEnd(40, 'x') });
|
|
140
91
|
});
|
|
141
|
-
it('should prompt
|
|
142
|
-
promptMocks.input
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
promptMocks.
|
|
92
|
+
it('should prompt to open browser when blank key entered with no existing key', async () => {
|
|
93
|
+
promptMocks.input
|
|
94
|
+
.mockResolvedValueOnce('')
|
|
95
|
+
.mockResolvedValueOnce('a'.repeat(40));
|
|
96
|
+
promptMocks.confirm.mockResolvedValue(true);
|
|
97
|
+
await initCommand.run({});
|
|
98
|
+
expect(promptMocks.confirm).toHaveBeenCalled();
|
|
99
|
+
expect(openMock).toHaveBeenCalledWith('https://app.sysprompthub.com/api-keys');
|
|
100
|
+
expect(mocks.mockUserConfig.save).toHaveBeenCalledWith(expect.any(Object), { apiKey: 'a'.repeat(40) });
|
|
101
|
+
});
|
|
102
|
+
it('should not open browser if user declines', async () => {
|
|
103
|
+
promptMocks.input
|
|
104
|
+
.mockResolvedValueOnce('')
|
|
105
|
+
.mockResolvedValueOnce('b'.repeat(40));
|
|
106
|
+
promptMocks.confirm.mockResolvedValue(false);
|
|
148
107
|
await initCommand.run({});
|
|
149
|
-
expect(
|
|
150
|
-
|
|
151
|
-
}));
|
|
108
|
+
expect(promptMocks.confirm).toHaveBeenCalled();
|
|
109
|
+
expect(openMock).not.toHaveBeenCalled();
|
|
152
110
|
});
|
|
153
|
-
it('should
|
|
154
|
-
promptMocks.input.
|
|
155
|
-
promptMocks.input.mockResolvedValueOnce(''); // blank custom path
|
|
156
|
-
promptMocks.search
|
|
157
|
-
.mockResolvedValueOnce(JSON.stringify({ name: 'owner/pack', version: 5 })) // Pack search
|
|
158
|
-
.mockResolvedValueOnce('latest'); // Version selection
|
|
159
|
-
promptMocks.checkbox.mockResolvedValue([]);
|
|
111
|
+
it('should display next steps after saving', async () => {
|
|
112
|
+
promptMocks.input.mockResolvedValue('a'.repeat(40));
|
|
160
113
|
await initCommand.run({});
|
|
161
|
-
expect(
|
|
162
|
-
|
|
163
|
-
assistants: []
|
|
164
|
-
}));
|
|
165
|
-
const updateCall = mocks.mockWorkspaceConfig.update.mock.calls[0][1];
|
|
166
|
-
expect(updateCall.path).toBeUndefined();
|
|
114
|
+
expect(consoleLogSpy).toHaveBeenCalledWith('\nNext steps:');
|
|
115
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(' • Set primary pack: sysprompthub copilot set <pack>');
|
|
167
116
|
});
|
|
168
117
|
});
|
|
169
118
|
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { loadProjectConfigEditor } from '../config-helpers.js';
|
|
3
|
+
import { assistantManager } from '@sysprompthub/sdk';
|
|
4
|
+
export class ShowCommand {
|
|
5
|
+
static INDENT = "\n ";
|
|
6
|
+
async run() {
|
|
7
|
+
const editor = await loadProjectConfigEditor();
|
|
8
|
+
if (!editor.packs || editor.packs.length === 0) {
|
|
9
|
+
console.log('No packs configured for this project.');
|
|
10
|
+
console.log('\nGet started:');
|
|
11
|
+
console.log(' • sysprompthub copilot set <pack>');
|
|
12
|
+
console.log(' • sysprompthub claude set <pack>');
|
|
13
|
+
console.log(' • sysprompthub custom add <path> <pack>');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
// Separate non-custom and custom packs
|
|
17
|
+
const assistantPacks = editor.packs.filter(p => p.type !== 'custom');
|
|
18
|
+
const customPacks = editor.packs.filter(p => p.type === 'custom');
|
|
19
|
+
// Sort assistant packs by type then agent name
|
|
20
|
+
const sortedAssistantPacks = assistantPacks.sort((a, b) => {
|
|
21
|
+
if (a.type !== b.type) {
|
|
22
|
+
return a.type.localeCompare(b.type);
|
|
23
|
+
}
|
|
24
|
+
const aName = a.agent?.name || '';
|
|
25
|
+
const bName = b.agent?.name || '';
|
|
26
|
+
return aName.localeCompare(bName);
|
|
27
|
+
});
|
|
28
|
+
// Sort custom packs by path
|
|
29
|
+
const sortedCustomPacks = customPacks.sort((a, b) => {
|
|
30
|
+
return (a.path || '').localeCompare(b.path || '');
|
|
31
|
+
});
|
|
32
|
+
// Display assistant packs
|
|
33
|
+
for (const pack of sortedAssistantPacks) {
|
|
34
|
+
this.displayAssistantPack(pack);
|
|
35
|
+
}
|
|
36
|
+
// Display custom packs
|
|
37
|
+
for (const pack of sortedCustomPacks) {
|
|
38
|
+
this.displayCustomPack(pack);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
displayAssistantPack(pack) {
|
|
42
|
+
if (pack.type === 'custom')
|
|
43
|
+
return;
|
|
44
|
+
const { agentName, prompt } = this.packConfigInfo(pack);
|
|
45
|
+
// Extract frontmatter (all properties except 'name')
|
|
46
|
+
const frontmatter = {};
|
|
47
|
+
if (pack.agent) {
|
|
48
|
+
for (const [key, value] of Object.entries(pack.agent)) {
|
|
49
|
+
if (key !== 'name') {
|
|
50
|
+
frontmatter[key] = value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
let details = {
|
|
55
|
+
pack: pack.pack,
|
|
56
|
+
prompt,
|
|
57
|
+
...frontmatter
|
|
58
|
+
};
|
|
59
|
+
if (pack.environment)
|
|
60
|
+
details.environment = pack.environment;
|
|
61
|
+
let name = pack.agent ? `${pack.type} ${agentName} "${pack.agent.name}"` : pack.type;
|
|
62
|
+
console.log([name, ...this.formatDetails(details)].join(ShowCommand.INDENT) + "\n");
|
|
63
|
+
}
|
|
64
|
+
packConfigInfo(pack) {
|
|
65
|
+
const assistantClass = assistantManager.getAssistantClass(pack.type);
|
|
66
|
+
return {
|
|
67
|
+
agentName: assistantClass.agents === false ? undefined : assistantClass.agents.name,
|
|
68
|
+
prompt: new assistantClass().promptPath(pack)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
formatDetails(details) {
|
|
72
|
+
const prefixLength = Math.max(...Object.keys(details).map(k => k.length)) + 2;
|
|
73
|
+
return Object.entries(details).map(([key, value]) => {
|
|
74
|
+
const prefix = `${key}:`.padEnd(prefixLength);
|
|
75
|
+
return `${prefix}${value}`;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
displayCustomPack(pack) {
|
|
79
|
+
if (pack.type !== 'custom')
|
|
80
|
+
return;
|
|
81
|
+
console.log(`custom "${pack.path}": ${pack.pack}${this.formatEnv(pack.environment)}`);
|
|
82
|
+
}
|
|
83
|
+
formatEnv(env) {
|
|
84
|
+
return env ? ` [${env}]` : '';
|
|
85
|
+
}
|
|
86
|
+
formatFrontmatter(agent) {
|
|
87
|
+
const entries = Object.entries(agent)
|
|
88
|
+
.filter(([key]) => key !== 'name')
|
|
89
|
+
.map(([key, value]) => `${key}=${value}`);
|
|
90
|
+
return entries.length > 0 ? ` (${entries.join(', ')})` : '';
|
|
91
|
+
}
|
|
92
|
+
create() {
|
|
93
|
+
const command = new Command('show');
|
|
94
|
+
command
|
|
95
|
+
.description('Show all configured packs for this project')
|
|
96
|
+
.action(async () => await this.run());
|
|
97
|
+
return command;
|
|
98
|
+
}
|
|
99
|
+
}
|