@lsst/pik 0.0.1

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 ADDED
@@ -0,0 +1,71 @@
1
+ # @lsst/pik
2
+
3
+ CLI tool for switching config options in source files using `@pik` markers.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @lsst/pik
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### 1. Add markers to your files
14
+
15
+ ```typescript
16
+ // @pik:select Environment
17
+ // const env = 'DEV'; // @pik:option DEV
18
+ const env = 'LOCAL'; // @pik:option LOCAL
19
+ ```
20
+
21
+ ### 2. Create a config file
22
+
23
+ Create `pik.config.ts` in your project root:
24
+
25
+ ```typescript
26
+ import { defineConfig } from '@lsst/pik';
27
+
28
+ export default defineConfig({
29
+ include: ['src/**/*.ts', '.env'],
30
+ });
31
+ ```
32
+
33
+ ### 3. Run commands
34
+
35
+ ```bash
36
+ # List all selectors and their current state
37
+ pik list
38
+
39
+ # Set a specific option
40
+ pik set Environment DEV
41
+
42
+ # Interactive mode
43
+ pik switch
44
+ ```
45
+
46
+ ## Commands
47
+
48
+ | Command | Alias | Description |
49
+ |---------|-------|-------------|
50
+ | `pik list` | `ls` | Show all selectors and their current state |
51
+ | `pik set <selector> <option>` | - | Set an option directly |
52
+ | `pik switch` | `sw` | Interactive selection mode |
53
+ | `pik` | - | Same as `pik switch` |
54
+
55
+ ## Marker Syntax
56
+
57
+ - `@pik:select <name>` - Defines a selector group
58
+ - `@pik:option <name>` - Marks an option within a selector
59
+
60
+ Commented lines are inactive, uncommented lines are active.
61
+
62
+ ## Supported Comment Styles
63
+
64
+ | Extensions | Comment Style |
65
+ |------------|---------------|
66
+ | `.ts`, `.js`, `.tsx`, `.jsx` | `//` |
67
+ | `.sh`, `.bash`, `.zsh`, `.py`, `.yaml`, `.yml`, `.env` | `#` |
68
+
69
+ ## License
70
+
71
+ MIT
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=pik.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pik.d.ts","sourceRoot":"","sources":["../../src/bin/pik.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { program } from '../lib/program.js';
3
+ program.parse();
@@ -0,0 +1,2 @@
1
+ export { defineConfig, type PikConfig } from './lib/config.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ // Config helper for pik.config.ts
2
+ export { defineConfig } from './lib/config.js';
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare const listCommand: Command;
3
+ //# sourceMappingURL=list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/lib/commands/list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC,eAAO,MAAM,WAAW,SAuCpB,CAAC"}
@@ -0,0 +1,37 @@
1
+ import { Command } from 'commander';
2
+ import pc from 'picocolors';
3
+ import { relative } from 'path';
4
+ import { loadConfig } from '../config.js';
5
+ import { Scanner } from '../scanner.js';
6
+ export const listCommand = new Command('list')
7
+ .alias('ls')
8
+ .description('List all selectors and their current state')
9
+ .action(async () => {
10
+ const config = await loadConfig();
11
+ if (!config) {
12
+ console.error(pc.red('No pik.config.ts found'));
13
+ process.exit(1);
14
+ }
15
+ const scanner = new Scanner(config);
16
+ const results = await scanner.scan();
17
+ if (results.length === 0) {
18
+ console.log(pc.yellow('No selectors found'));
19
+ return;
20
+ }
21
+ for (const file of results) {
22
+ const relativePath = relative(process.cwd(), file.path);
23
+ console.log(pc.cyan(relativePath));
24
+ for (const selector of file.selectors) {
25
+ const activeOption = selector.options.find((o) => o.isActive);
26
+ const activeLabel = activeOption
27
+ ? pc.green(activeOption.name)
28
+ : pc.yellow('none');
29
+ console.log(` ${pc.bold(selector.name)}: ${activeLabel}`);
30
+ for (const option of selector.options) {
31
+ const marker = option.isActive ? pc.green('●') : pc.dim('○');
32
+ console.log(` ${marker} ${option.name}`);
33
+ }
34
+ }
35
+ console.log();
36
+ }
37
+ });
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare const setCommand: Command;
3
+ //# sourceMappingURL=set.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"set.d.ts","sourceRoot":"","sources":["../../../src/lib/commands/set.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,eAAO,MAAM,UAAU,SA8CnB,CAAC"}
@@ -0,0 +1,45 @@
1
+ import { Command } from 'commander';
2
+ import { writeFile } from 'fs/promises';
3
+ import { extname, relative } from 'path';
4
+ import pc from 'picocolors';
5
+ import { SingleSwitcher } from '@lsst/pik-core';
6
+ import { loadConfig } from '../config.js';
7
+ import { Scanner } from '../scanner.js';
8
+ export const setCommand = new Command('set')
9
+ .description('Set a specific option for a selector')
10
+ .argument('<selector>', 'Selector name')
11
+ .argument('<option>', 'Option to activate')
12
+ .action(async (selectorName, optionName) => {
13
+ const config = await loadConfig();
14
+ if (!config) {
15
+ console.error(pc.red('No pik.config.ts found'));
16
+ process.exit(1);
17
+ }
18
+ const scanner = new Scanner(config);
19
+ const results = await scanner.scan();
20
+ let found = false;
21
+ for (const file of results) {
22
+ const selector = file.selectors.find((s) => s.name === selectorName);
23
+ if (selector) {
24
+ found = true;
25
+ const extension = extname(file.path);
26
+ const switcher = SingleSwitcher.forExtension(extension);
27
+ try {
28
+ const newContent = switcher.switch(file.content, selector, optionName);
29
+ await writeFile(file.path, newContent);
30
+ const relativePath = relative(process.cwd(), file.path);
31
+ console.log(pc.green(`✓ Set ${pc.bold(selectorName)} to ${pc.bold(optionName)} in ${relativePath}`));
32
+ }
33
+ catch (error) {
34
+ if (error instanceof Error) {
35
+ console.error(pc.red(error.message));
36
+ }
37
+ process.exit(1);
38
+ }
39
+ }
40
+ }
41
+ if (!found) {
42
+ console.error(pc.red(`Selector "${selectorName}" not found`));
43
+ process.exit(1);
44
+ }
45
+ });
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare const switchCommand: Command;
3
+ //# sourceMappingURL=switch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"switch.d.ts","sourceRoot":"","sources":["../../../src/lib/commands/switch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAcpC,eAAO,MAAM,aAAa,SAoEtB,CAAC"}
@@ -0,0 +1,59 @@
1
+ import { Command } from 'commander';
2
+ import { writeFile } from 'fs/promises';
3
+ import { extname, relative } from 'path';
4
+ import { select } from '@inquirer/prompts';
5
+ import pc from 'picocolors';
6
+ import { SingleSwitcher } from '@lsst/pik-core';
7
+ import { loadConfig } from '../config.js';
8
+ import { Scanner } from '../scanner.js';
9
+ export const switchCommand = new Command('switch')
10
+ .alias('sw')
11
+ .description('Interactively switch options')
12
+ .action(async () => {
13
+ const config = await loadConfig();
14
+ if (!config) {
15
+ console.error(pc.red('No pik.config.ts found'));
16
+ process.exit(1);
17
+ }
18
+ const scanner = new Scanner(config);
19
+ const results = await scanner.scan();
20
+ if (results.length === 0) {
21
+ console.log(pc.yellow('No selectors found'));
22
+ return;
23
+ }
24
+ // Flatten all selectors with their file info
25
+ const choices = [];
26
+ for (const file of results) {
27
+ for (const selector of file.selectors) {
28
+ choices.push({ file, selector });
29
+ }
30
+ }
31
+ // Select which selector to switch
32
+ const selectedChoice = await select({
33
+ message: 'Select a selector to switch',
34
+ choices: choices.map((choice) => {
35
+ const relativePath = relative(process.cwd(), choice.file.path);
36
+ const activeOption = choice.selector.options.find((o) => o.isActive);
37
+ const current = activeOption ? pc.dim(` (${activeOption.name})`) : '';
38
+ return {
39
+ name: `${choice.selector.name}${current} ${pc.dim(`- ${relativePath}`)}`,
40
+ value: choice,
41
+ };
42
+ }),
43
+ });
44
+ // Select which option to activate
45
+ const selectedOption = await select({
46
+ message: `Select option for ${pc.bold(selectedChoice.selector.name)}`,
47
+ choices: selectedChoice.selector.options.map((option) => ({
48
+ name: option.isActive ? `${option.name} ${pc.green('(current)')}` : option.name,
49
+ value: option.name,
50
+ })),
51
+ });
52
+ // Apply the change
53
+ const extension = extname(selectedChoice.file.path);
54
+ const switcher = SingleSwitcher.forExtension(extension);
55
+ const newContent = switcher.switch(selectedChoice.file.content, selectedChoice.selector, selectedOption);
56
+ await writeFile(selectedChoice.file.path, newContent);
57
+ const relativePath = relative(process.cwd(), selectedChoice.file.path);
58
+ console.log(pc.green(`✓ Set ${pc.bold(selectedChoice.selector.name)} to ${pc.bold(selectedOption)} in ${relativePath}`));
59
+ });
@@ -0,0 +1,7 @@
1
+ export interface PikConfig {
2
+ /** File patterns to scan for @pik markers */
3
+ include: string[];
4
+ }
5
+ export declare function defineConfig(config: PikConfig): PikConfig;
6
+ export declare function loadConfig(cwd?: string): Promise<PikConfig | null>;
7
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,CAEzD;AAID,wBAAsB,UAAU,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAWvF"}
@@ -0,0 +1,16 @@
1
+ import { pathToFileURL } from 'url';
2
+ import { existsSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ export function defineConfig(config) {
5
+ return config;
6
+ }
7
+ const CONFIG_FILE = 'pik.config.ts';
8
+ export async function loadConfig(cwd = process.cwd()) {
9
+ const configPath = resolve(cwd, CONFIG_FILE);
10
+ if (!existsSync(configPath)) {
11
+ return null;
12
+ }
13
+ const configUrl = pathToFileURL(configPath).href;
14
+ const module = await import(configUrl);
15
+ return module.default;
16
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=config.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.spec.d.ts","sourceRoot":"","sources":["../../src/lib/config.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { writeFile, mkdir, rm } from 'fs/promises';
3
+ import { join } from 'path';
4
+ import { tmpdir } from 'os';
5
+ import { loadConfig, defineConfig } from './config.js';
6
+ describe('config', () => {
7
+ let testDir;
8
+ beforeEach(async () => {
9
+ testDir = join(tmpdir(), `pik-test-${Date.now()}`);
10
+ await mkdir(testDir, { recursive: true });
11
+ });
12
+ afterEach(async () => {
13
+ await rm(testDir, { recursive: true, force: true });
14
+ });
15
+ describe('defineConfig', () => {
16
+ it('should return the config as-is', () => {
17
+ const config = defineConfig({ include: ['*.ts'] });
18
+ expect(config).toEqual({ include: ['*.ts'] });
19
+ });
20
+ });
21
+ describe('loadConfig', () => {
22
+ it('should return null when no config file exists', async () => {
23
+ const config = await loadConfig(testDir);
24
+ expect(config).toBeNull();
25
+ });
26
+ it('should load config from pik.config.ts', async () => {
27
+ const configContent = `
28
+ export default { include: ['src/**/*.ts'] };
29
+ `;
30
+ await writeFile(join(testDir, 'pik.config.ts'), configContent);
31
+ const config = await loadConfig(testDir);
32
+ expect(config).toEqual({ include: ['src/**/*.ts'] });
33
+ });
34
+ });
35
+ });
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare const program: Command;
3
+ //# sourceMappingURL=program.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../../src/lib/program.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC,eAAO,MAAM,OAAO,SAGG,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { Command } from 'commander';
2
+ import pkg from '../../package.json' with { type: 'json' };
3
+ import { listCommand } from './commands/list.js';
4
+ import { setCommand } from './commands/set.js';
5
+ import { switchCommand } from './commands/switch.js';
6
+ export const program = new Command()
7
+ .name(pkg.name)
8
+ .description(pkg.description)
9
+ .version(pkg.version);
10
+ program.addCommand(listCommand);
11
+ program.addCommand(setCommand);
12
+ program.addCommand(switchCommand);
13
+ // Default command: interactive switch
14
+ program.action(async () => {
15
+ await switchCommand.parseAsync([]);
16
+ });
@@ -0,0 +1,13 @@
1
+ import { type Selector } from '@lsst/pik-core';
2
+ import type { PikConfig } from './config.js';
3
+ export interface FileResult {
4
+ path: string;
5
+ selectors: Selector[];
6
+ content: string;
7
+ }
8
+ export declare class Scanner {
9
+ private readonly config;
10
+ constructor(config: PikConfig);
11
+ scan(cwd?: string): Promise<FileResult[]>;
12
+ }
13
+ //# sourceMappingURL=scanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/lib/scanner.ts"],"names":[],"mappings":"AAGA,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,OAAO;IACN,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,SAAS;IAExC,IAAI,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;CAsB/D"}
@@ -0,0 +1,28 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { glob } from 'glob';
3
+ import { extname } from 'path';
4
+ import { Parser } from '@lsst/pik-core';
5
+ export class Scanner {
6
+ config;
7
+ constructor(config) {
8
+ this.config = config;
9
+ }
10
+ async scan(cwd = process.cwd()) {
11
+ const files = await glob(this.config.include, {
12
+ cwd,
13
+ absolute: true,
14
+ nodir: true,
15
+ });
16
+ const results = [];
17
+ for (const filePath of files) {
18
+ const content = await readFile(filePath, 'utf-8');
19
+ const extension = extname(filePath);
20
+ const parser = Parser.forExtension(extension);
21
+ const { selectors } = parser.parse(content);
22
+ if (selectors.length > 0) {
23
+ results.push({ path: filePath, selectors, content });
24
+ }
25
+ }
26
+ return results;
27
+ }
28
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=scanner.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scanner.spec.d.ts","sourceRoot":"","sources":["../../src/lib/scanner.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { writeFile, mkdir, rm } from 'fs/promises';
3
+ import { join } from 'path';
4
+ import { tmpdir } from 'os';
5
+ import { Scanner } from './scanner.js';
6
+ describe('Scanner', () => {
7
+ let testDir;
8
+ beforeEach(async () => {
9
+ testDir = join(tmpdir(), `pik-scanner-test-${Date.now()}`);
10
+ await mkdir(testDir, { recursive: true });
11
+ });
12
+ afterEach(async () => {
13
+ await rm(testDir, { recursive: true, force: true });
14
+ });
15
+ it('should find files with pik selectors', async () => {
16
+ const fileContent = `
17
+ // @pik:select Environment
18
+ const env = 'DEV'; // @pik:option DEV
19
+ // const env = 'LOCAL'; // @pik:option LOCAL
20
+ `.trim();
21
+ await writeFile(join(testDir, 'config.ts'), fileContent);
22
+ const scanner = new Scanner({ include: ['*.ts'] });
23
+ const results = await scanner.scan(testDir);
24
+ expect(results).toHaveLength(1);
25
+ expect(results[0].selectors).toHaveLength(1);
26
+ expect(results[0].selectors[0].name).toBe('Environment');
27
+ });
28
+ it('should return empty array when no selectors found', async () => {
29
+ await writeFile(join(testDir, 'empty.ts'), 'const x = 1;');
30
+ const scanner = new Scanner({ include: ['*.ts'] });
31
+ const results = await scanner.scan(testDir);
32
+ expect(results).toHaveLength(0);
33
+ });
34
+ it('should scan multiple files', async () => {
35
+ const file1 = `
36
+ // @pik:select Theme
37
+ const theme = 'dark'; // @pik:option dark
38
+ `.trim();
39
+ const file2 = `
40
+ // @pik:select Mode
41
+ const mode = 'dev'; // @pik:option dev
42
+ `.trim();
43
+ await writeFile(join(testDir, 'theme.ts'), file1);
44
+ await writeFile(join(testDir, 'mode.ts'), file2);
45
+ const scanner = new Scanner({ include: ['*.ts'] });
46
+ const results = await scanner.scan(testDir);
47
+ expect(results).toHaveLength(2);
48
+ });
49
+ it('should respect include patterns', async () => {
50
+ await writeFile(join(testDir, 'included.ts'), '// @pik:select A\nconst a = 1; // @pik:option a');
51
+ await writeFile(join(testDir, 'excluded.js'), '// @pik:select B\nconst b = 1; // @pik:option b');
52
+ const scanner = new Scanner({ include: ['*.ts'] });
53
+ const results = await scanner.scan(testDir);
54
+ expect(results).toHaveLength(1);
55
+ expect(results[0].selectors[0].name).toBe('A');
56
+ });
57
+ });
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@lsst/pik",
3
+ "version": "0.0.1",
4
+ "description": "CLI tool for switching config options in source files",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Yurii Tatar <lsst25@gmail.com>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/lsst25/pik.git",
11
+ "directory": "packages/cli"
12
+ },
13
+ "keywords": [
14
+ "cli",
15
+ "config",
16
+ "configuration",
17
+ "switch",
18
+ "toggle",
19
+ "environment",
20
+ "env"
21
+ ],
22
+ "bin": {
23
+ "pik": "./dist/bin/pik.js"
24
+ },
25
+ "main": "./dist/index.js",
26
+ "module": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ "./package.json": "./package.json",
30
+ ".": {
31
+ "@pik/source": "./src/index.ts",
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js",
34
+ "default": "./dist/index.js"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "!**/*.tsbuildinfo"
40
+ ],
41
+ "dependencies": {
42
+ "@inquirer/prompts": "^8.1.0",
43
+ "@lsst/pik-core": "^0.0.1",
44
+ "commander": "^14.0.2",
45
+ "glob": "^10.5.0",
46
+ "picocolors": "^1.1.1",
47
+ "tslib": "^2.3.0"
48
+ }
49
+ }