@lsst/pik 0.5.1 → 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
package/dist/bin/pik.js CHANGED
@@ -1,3 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { program } from '../lib/program.js';
2
+ import { program, initializeProgram } from '../lib/program.js';
3
+ await initializeProgram();
3
4
  program.parse();
@@ -1,3 +1,8 @@
1
1
  import { Command } from 'commander';
2
+ import { type PikPlugin } from '@lsst/pik-core';
2
3
  export declare const program: Command;
4
+ /**
5
+ * Initialize the program by loading config and registering enabled plugins.
6
+ */
7
+ export declare function initializeProgram(): Promise<PikPlugin[]>;
3
8
  //# sourceMappingURL=program.d.ts.map
@@ -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;AAWpC,eAAO,MAAM,OAAO,SAGG,CAAC"}
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,49 +1,107 @@
1
1
  import { Command } from 'commander';
2
2
  import { select, Separator } from '@inquirer/prompts';
3
3
  import pc from 'picocolors';
4
+ import { loadConfig, isValidPlugin } from '@lsst/pik-core';
4
5
  import { selectPlugin } from '@lsst/pik-plugin-select';
5
6
  import { worktreePlugin } from '@lsst/pik-plugin-worktree';
6
7
  import pkg from '../../package.json' with { type: 'json' };
7
- // List of available plugins
8
- const plugins = [selectPlugin, worktreePlugin];
8
+ // Built-in plugins
9
+ const builtinPlugins = [selectPlugin, worktreePlugin];
10
+ /**
11
+ * Get plugins that are enabled in the config.
12
+ * - Built-in plugins are enabled if their command key exists in config
13
+ * - External plugins from the `plugins` array are added directly
14
+ */
15
+ async function getEnabledPlugins() {
16
+ const config = await loadConfig();
17
+ if (!config) {
18
+ // No config - no plugins enabled
19
+ return [];
20
+ }
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;
41
+ }
9
42
  export const program = new Command()
10
43
  .name(pkg.name)
11
44
  .description(pkg.description)
12
45
  .version(pkg.version);
13
- // Register all plugins
14
- for (const plugin of plugins) {
15
- plugin.register(program);
16
- }
17
- // Default action: show main menu if multiple plugins, otherwise run default plugin
18
- program.action(async () => {
19
- if (plugins.length === 1) {
20
- // Single plugin - run its default command
21
- const plugin = plugins[0];
22
- const cmd = program.commands.find((c) => c.name() === plugin.command);
23
- if (cmd) {
24
- await cmd.parseAsync([], { from: 'user' });
25
- }
46
+ /**
47
+ * Initialize the program by loading config and registering enabled plugins.
48
+ */
49
+ export async function initializeProgram() {
50
+ const plugins = await getEnabledPlugins();
51
+ // Register only enabled plugins
52
+ for (const plugin of plugins) {
53
+ plugin.register(program);
26
54
  }
27
- else {
28
- // Multiple plugins - show selection menu
29
- const EXIT_VALUE = Symbol('exit');
30
- const selectedPlugin = await select({
31
- message: 'Select a tool',
32
- choices: [
33
- ...plugins.map((plugin) => ({
34
- name: `${pc.bold(plugin.name)} - ${plugin.description}`,
35
- value: plugin,
36
- })),
37
- new Separator(),
38
- { name: pc.dim('Exit'), value: EXIT_VALUE },
39
- ],
40
- });
41
- if (selectedPlugin === EXIT_VALUE) {
55
+ // Default action: show main menu with enabled plugins
56
+ program.action(async () => {
57
+ if (plugins.length === 0) {
58
+ console.log(pc.yellow('No plugins configured.'));
59
+ console.log(pc.dim('Add plugin config to pik.config.ts to enable plugins.'));
60
+ console.log(pc.dim('Example: { select: {}, worktree: {} }'));
42
61
  return;
43
62
  }
44
- const cmd = program.commands.find((c) => c.name() === selectedPlugin.command);
45
- if (cmd) {
46
- await cmd.parseAsync([], { from: 'user' });
63
+ if (plugins.length === 1) {
64
+ // Single plugin - run its default command
65
+ const plugin = plugins[0];
66
+ const cmd = program.commands.find((c) => c.name() === plugin.command);
67
+ if (cmd) {
68
+ await cmd.parseAsync([], { from: 'user' });
69
+ }
47
70
  }
48
- }
49
- });
71
+ else {
72
+ // Multiple plugins - show selection menu in a loop
73
+ const EXIT_VALUE = Symbol('exit');
74
+ while (true) {
75
+ let selectedPlugin;
76
+ try {
77
+ selectedPlugin = await select({
78
+ message: 'Select a tool',
79
+ choices: [
80
+ ...plugins.map((plugin) => ({
81
+ name: `${pc.bold(plugin.name)} - ${plugin.description}`,
82
+ value: plugin,
83
+ })),
84
+ new Separator(),
85
+ { name: pc.dim('Exit'), value: EXIT_VALUE },
86
+ ],
87
+ });
88
+ }
89
+ catch (error) {
90
+ // Handle Ctrl+C
91
+ if (error instanceof Error && error.name === 'ExitPromptError') {
92
+ return;
93
+ }
94
+ throw error;
95
+ }
96
+ if (selectedPlugin === EXIT_VALUE) {
97
+ return;
98
+ }
99
+ const cmd = program.commands.find((c) => c.name() === selectedPlugin.command);
100
+ if (cmd) {
101
+ await cmd.parseAsync([], { from: 'user' });
102
+ }
103
+ }
104
+ }
105
+ });
106
+ return plugins;
107
+ }
@@ -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.1",
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",