@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.
@@ -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
+ }
@@ -1,53 +1,23 @@
1
- import { checkbox, confirm, input, search } from '@inquirer/prompts';
1
+ import { input, confirm } from '@inquirer/prompts';
2
2
  import { Command } from 'commander';
3
- import { API_KEY_LENGTH, AssistantManager, DefaultConfigManager, NodeFileSystem, SysPromptHubClient, UserConfigManager } from '@sysprompthub/sdk';
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({ environment }) {
13
- console.log('Welcome to SysPromptHub! Let\'s configure your project.\n');
14
- // Load existing configs
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
- const existingWorkspaceConfig = await this.workspaceConfigManager.load(this.fs) || {};
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
- // Environment
21
- let workspaceConfig = { ...existingWorkspaceConfig };
22
- if (environment)
23
- workspaceConfig.environment = environment;
24
- // Step 2: Pack Name (workspace-level)
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 project system prompt configuration')
206
- .addOption(InitCommand.envOption)
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
- mockWorkspaceConfig,
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 project system prompt configuration');
75
+ expect(command.description()).toBe('Initialize API key configuration');
112
76
  });
113
77
  });
114
78
  describe('run', () => {
115
- it('should prompt for API key, pack name, and assistants', async () => {
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(mocks.mockWorkspaceConfig.update).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({
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(''); // API key blank
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 for custom path when no assistants selected', async () => {
142
- promptMocks.input.mockResolvedValueOnce('a'.repeat(40)); // API key
143
- promptMocks.input.mockResolvedValueOnce('custom/path'); // custom path
144
- promptMocks.search
145
- .mockResolvedValueOnce(JSON.stringify({ name: 'owner/pack', version: 5 })) // Pack search
146
- .mockResolvedValueOnce('latest'); // Version selection
147
- promptMocks.checkbox.mockResolvedValue([]);
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(mocks.mockWorkspaceConfig.update).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({
150
- path: 'custom/path'
151
- }));
108
+ expect(promptMocks.confirm).toHaveBeenCalled();
109
+ expect(openMock).not.toHaveBeenCalled();
152
110
  });
153
- it('should not set path when blank entered for custom path', async () => {
154
- promptMocks.input.mockResolvedValueOnce('a'.repeat(40)); // API key
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(mocks.mockWorkspaceConfig.update).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({
162
- packName: 'owner/pack/latest',
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
+ }