@lsst/pik 0.5.2 → 0.5.3

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 CHANGED
@@ -8,7 +8,29 @@ A developer toolkit with extensible plugins for common development tasks.
8
8
  npm install -g @lsst/pik
9
9
  ```
10
10
 
11
- ## Plugins
11
+ ## Configuration
12
+
13
+ Create `pik.config.ts` in your project root:
14
+
15
+ ```typescript
16
+ export default {
17
+ // Enable select plugin
18
+ select: {
19
+ include: ['src/**/*.ts', '.env'],
20
+ },
21
+
22
+ // Enable worktree plugin
23
+ worktree: {
24
+ baseDir: '../',
25
+ copyFiles: ['.env.local'],
26
+ postCreate: 'npm install',
27
+ },
28
+ };
29
+ ```
30
+
31
+ Plugins are only available when their configuration key is present.
32
+
33
+ ## Built-in Plugins
12
34
 
13
35
  ### Select Plugin
14
36
 
@@ -22,41 +44,59 @@ Switch config options in source files using `@pik` markers.
22
44
  const env = 'LOCAL'; // @pik:option LOCAL
23
45
  ```
24
46
 
25
- #### 2. Create a config file
47
+ #### 2. Run commands
26
48
 
27
- Create `pik.config.ts` in your project root:
28
-
29
- ```typescript
30
- import { defineConfig } from '@lsst/pik';
31
-
32
- export default defineConfig({
33
- select: {
34
- include: ['src/**/*.ts', '.env'],
35
- },
36
- });
49
+ ```bash
50
+ pik select # Interactive mode
51
+ pik select list # List all selectors
52
+ pik select set Environment DEV # Set directly
37
53
  ```
38
54
 
39
- #### 3. Run commands
55
+ ### Worktree Plugin
56
+
57
+ Manage git worktrees with automatic setup.
40
58
 
41
59
  ```bash
42
- # Interactive mode
43
- pik select
60
+ pik worktree # Interactive mode
61
+ pik worktree create # Create a new worktree
62
+ pik worktree list # List all worktrees
63
+ pik worktree remove # Remove a worktree
64
+ ```
44
65
 
45
- # List all selectors
46
- pik select list
66
+ ## External Plugins
47
67
 
48
- # Set a specific option
49
- pik select set Environment DEV
68
+ Add third-party or custom plugins:
69
+
70
+ ```typescript
71
+ import { myPlugin } from 'pik-plugin-my';
72
+
73
+ export default {
74
+ plugins: [
75
+ myPlugin({ apiKey: 'xxx' }),
76
+ ],
77
+ select: { include: ['src/**/*.ts'] },
78
+ };
50
79
  ```
51
80
 
52
81
  ## Commands
53
82
 
83
+ ### Select
84
+
54
85
  | Command | Alias | Description |
55
86
  |---------|-------|-------------|
56
87
  | `pik select` | `sel` | Interactive selection mode |
57
88
  | `pik select list` | `ls` | Show all selectors and their state |
58
89
  | `pik select set <selector> <option>` | - | Set an option directly |
59
90
 
91
+ ### Worktree
92
+
93
+ | Command | Alias | Description |
94
+ |---------|-------|-------------|
95
+ | `pik worktree` | `wt` | Interactive worktree menu |
96
+ | `pik worktree create [name]` | `add` | Create a new worktree |
97
+ | `pik worktree list` | `ls` | List all worktrees |
98
+ | `pik worktree remove [path]` | `rm` | Remove a worktree |
99
+
60
100
  ## Marker Syntax
61
101
 
62
102
  - `@pik:select <name>` - Defines a selector group
@@ -68,7 +108,8 @@ Commented lines are inactive, uncommented lines are active.
68
108
 
69
109
  | Extensions | Comment Style |
70
110
  |------------|---------------|
71
- | `.ts`, `.js`, `.tsx`, `.jsx` | `//` |
111
+ | `.ts`, `.js`, `.tsx`, `.jsx`, `.mts`, `.mjs` | `//` |
112
+ | `.html`, `.htm` | `//` and `<!-- -->` |
72
113
  | `.sh`, `.bash`, `.zsh`, `.py`, `.yaml`, `.yml`, `.env` | `#` |
73
114
 
74
115
  ## License
@@ -1 +1 @@
1
- {"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../../src/lib/program.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAuB5D,eAAO,MAAM,OAAO,SAGG,CAAC;AAExB;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,CAgE9D"}
1
+ {"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../../src/lib/program.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AA6C3E,eAAO,MAAM,OAAO,SAGG,CAAC;AAExB;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,CAgE9D"}
@@ -1,23 +1,43 @@
1
1
  import { Command } from 'commander';
2
2
  import { select, Separator } from '@inquirer/prompts';
3
3
  import pc from 'picocolors';
4
- import { loadConfig } from '@lsst/pik-core';
4
+ import { loadConfig, isValidPlugin } from '@lsst/pik-core';
5
5
  import { selectPlugin } from '@lsst/pik-plugin-select';
6
6
  import { worktreePlugin } from '@lsst/pik-plugin-worktree';
7
7
  import pkg from '../../package.json' with { type: 'json' };
8
- // List of all available plugins
9
- const allPlugins = [selectPlugin, worktreePlugin];
8
+ // Built-in plugins
9
+ const builtinPlugins = [selectPlugin, worktreePlugin];
10
10
  /**
11
11
  * Get plugins that are enabled in the config.
12
- * A plugin is enabled if its command key exists in the config (even as empty object).
12
+ * - Built-in plugins are enabled if their command key exists in config
13
+ * - External plugins from the `plugins` array are added directly
13
14
  */
14
15
  async function getEnabledPlugins() {
15
16
  const config = await loadConfig();
16
17
  if (!config) {
17
- // No config - no plugins enabled for interactive mode
18
+ // No config - no plugins enabled
18
19
  return [];
19
20
  }
20
- return allPlugins.filter((plugin) => plugin.command in config);
21
+ const enabledPlugins = [];
22
+ // Add built-in plugins that have config keys
23
+ for (const plugin of builtinPlugins) {
24
+ if (plugin.command in config) {
25
+ enabledPlugins.push(plugin);
26
+ }
27
+ }
28
+ // Add external plugins from config
29
+ if (config.plugins && Array.isArray(config.plugins)) {
30
+ for (const plugin of config.plugins) {
31
+ if (isValidPlugin(plugin)) {
32
+ enabledPlugins.push(plugin);
33
+ }
34
+ else {
35
+ console.error(pc.red('Invalid plugin in config.plugins array'));
36
+ console.error(pc.dim('Each plugin must have: name, description, command, register'));
37
+ }
38
+ }
39
+ }
40
+ return enabledPlugins;
21
41
  }
22
42
  export const program = new Command()
23
43
  .name(pkg.name)
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=program.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"program.spec.d.ts","sourceRoot":"","sources":["../../src/lib/program.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,169 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { Command } from 'commander';
3
+ // Mock the config and plugins before importing program
4
+ vi.mock('@lsst/pik-core', async () => {
5
+ const actual = await vi.importActual('@lsst/pik-core');
6
+ return {
7
+ ...actual,
8
+ loadConfig: vi.fn(),
9
+ };
10
+ });
11
+ vi.mock('@lsst/pik-plugin-select', () => ({
12
+ selectPlugin: {
13
+ name: 'Select',
14
+ description: 'Mock select plugin',
15
+ command: 'select',
16
+ register: vi.fn(),
17
+ },
18
+ }));
19
+ vi.mock('@lsst/pik-plugin-worktree', () => ({
20
+ worktreePlugin: {
21
+ name: 'Worktree',
22
+ description: 'Mock worktree plugin',
23
+ command: 'worktree',
24
+ register: vi.fn(),
25
+ },
26
+ }));
27
+ describe('program', () => {
28
+ let loadConfigMock;
29
+ beforeEach(async () => {
30
+ vi.resetModules();
31
+ const pikCore = await import('@lsst/pik-core');
32
+ loadConfigMock = pikCore.loadConfig;
33
+ });
34
+ afterEach(() => {
35
+ vi.clearAllMocks();
36
+ });
37
+ describe('initializeProgram', () => {
38
+ it('should return empty array when no config exists', async () => {
39
+ loadConfigMock.mockResolvedValue(null);
40
+ const { initializeProgram } = await import('./program.js');
41
+ const plugins = await initializeProgram();
42
+ expect(plugins).toEqual([]);
43
+ });
44
+ it('should load built-in plugins when their config keys exist', async () => {
45
+ loadConfigMock.mockResolvedValue({
46
+ select: { include: ['*.ts'] },
47
+ });
48
+ const { initializeProgram } = await import('./program.js');
49
+ const plugins = await initializeProgram();
50
+ expect(plugins).toHaveLength(1);
51
+ expect(plugins[0].command).toBe('select');
52
+ });
53
+ it('should load multiple built-in plugins when configured', async () => {
54
+ loadConfigMock.mockResolvedValue({
55
+ select: { include: ['*.ts'] },
56
+ worktree: { baseDir: '../' },
57
+ });
58
+ const { initializeProgram } = await import('./program.js');
59
+ const plugins = await initializeProgram();
60
+ expect(plugins).toHaveLength(2);
61
+ expect(plugins.map((p) => p.command)).toContain('select');
62
+ expect(plugins.map((p) => p.command)).toContain('worktree');
63
+ });
64
+ it('should load external plugins from plugins array', async () => {
65
+ const externalPlugin = {
66
+ name: 'External Plugin',
67
+ description: 'Test external plugin',
68
+ command: 'external',
69
+ register: vi.fn(),
70
+ };
71
+ loadConfigMock.mockResolvedValue({
72
+ plugins: [externalPlugin],
73
+ });
74
+ const { initializeProgram } = await import('./program.js');
75
+ const plugins = await initializeProgram();
76
+ expect(plugins).toHaveLength(1);
77
+ expect(plugins[0].name).toBe('External Plugin');
78
+ expect(plugins[0].command).toBe('external');
79
+ });
80
+ it('should combine built-in and external plugins', async () => {
81
+ const externalPlugin = {
82
+ name: 'External Plugin',
83
+ description: 'Test external plugin',
84
+ command: 'external',
85
+ register: vi.fn(),
86
+ };
87
+ loadConfigMock.mockResolvedValue({
88
+ plugins: [externalPlugin],
89
+ select: { include: ['*.ts'] },
90
+ });
91
+ const { initializeProgram } = await import('./program.js');
92
+ const plugins = await initializeProgram();
93
+ expect(plugins).toHaveLength(2);
94
+ expect(plugins.map((p) => p.command)).toContain('select');
95
+ expect(plugins.map((p) => p.command)).toContain('external');
96
+ });
97
+ it('should register external plugins with the program', async () => {
98
+ const registerFn = vi.fn();
99
+ const externalPlugin = {
100
+ name: 'External Plugin',
101
+ description: 'Test external plugin',
102
+ command: 'external',
103
+ register: registerFn,
104
+ };
105
+ loadConfigMock.mockResolvedValue({
106
+ plugins: [externalPlugin],
107
+ });
108
+ const { initializeProgram } = await import('./program.js');
109
+ await initializeProgram();
110
+ expect(registerFn).toHaveBeenCalledTimes(1);
111
+ expect(registerFn).toHaveBeenCalledWith(expect.any(Command));
112
+ });
113
+ it('should skip invalid plugins and log error', async () => {
114
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
115
+ const invalidPlugin = {
116
+ name: 'Invalid',
117
+ // missing description, command, register
118
+ };
119
+ loadConfigMock.mockResolvedValue({
120
+ plugins: [invalidPlugin],
121
+ });
122
+ const { initializeProgram } = await import('./program.js');
123
+ const plugins = await initializeProgram();
124
+ expect(plugins).toHaveLength(0);
125
+ expect(consoleSpy).toHaveBeenCalled();
126
+ consoleSpy.mockRestore();
127
+ });
128
+ it('should load multiple external plugins', async () => {
129
+ const plugin1 = {
130
+ name: 'Plugin 1',
131
+ description: 'First plugin',
132
+ command: 'first',
133
+ register: vi.fn(),
134
+ };
135
+ const plugin2 = {
136
+ name: 'Plugin 2',
137
+ description: 'Second plugin',
138
+ command: 'second',
139
+ register: vi.fn(),
140
+ };
141
+ loadConfigMock.mockResolvedValue({
142
+ plugins: [plugin1, plugin2],
143
+ });
144
+ const { initializeProgram } = await import('./program.js');
145
+ const plugins = await initializeProgram();
146
+ expect(plugins).toHaveLength(2);
147
+ expect(plugins[0].command).toBe('first');
148
+ expect(plugins[1].command).toBe('second');
149
+ });
150
+ it('should handle plugin factory pattern (function returning plugin)', async () => {
151
+ // This simulates: myPlugin({ config: 'value' }) in the config
152
+ const pluginFactory = (config) => ({
153
+ name: 'Factory Plugin',
154
+ description: `Plugin with key: ${config.apiKey}`,
155
+ command: 'factory',
156
+ register: vi.fn(),
157
+ });
158
+ const factoryPlugin = pluginFactory({ apiKey: 'test-key' });
159
+ loadConfigMock.mockResolvedValue({
160
+ plugins: [factoryPlugin],
161
+ });
162
+ const { initializeProgram } = await import('./program.js');
163
+ const plugins = await initializeProgram();
164
+ expect(plugins).toHaveLength(1);
165
+ expect(plugins[0].name).toBe('Factory Plugin');
166
+ expect(plugins[0].description).toBe('Plugin with key: test-key');
167
+ });
168
+ });
169
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lsst/pik",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "CLI tool for switching config options in source files",
5
5
  "type": "module",
6
6
  "license": "MIT",