@lsst/pik-plugin-select 0.7.0 → 0.8.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 CHANGED
@@ -40,6 +40,47 @@ pik select list # List all selectors
40
40
  pik select set <name> <option> # Set directly
41
41
  ```
42
42
 
43
+ ## Profiles
44
+
45
+ Switch multiple selectors at once using profiles. Configure them in `pik.config.ts`:
46
+
47
+ ```typescript
48
+ import { defineConfig } from '@lsst/pik';
49
+
50
+ export default defineConfig({
51
+ select: {
52
+ include: ['src/**/*.ts', '.env'],
53
+ profiles: {
54
+ dev: {
55
+ Environment: 'Development',
56
+ Database: 'SQLite',
57
+ Theme: 'dark',
58
+ },
59
+ prod: {
60
+ Environment: 'Production',
61
+ Database: 'Postgres',
62
+ Theme: 'light',
63
+ },
64
+ },
65
+ },
66
+ });
67
+ ```
68
+
69
+ ### Profile Commands
70
+
71
+ ```bash
72
+ pik select profiles # List all profiles with their status
73
+ pik select profile # Interactive profile picker
74
+ pik select profile dev # Apply a profile directly
75
+ ```
76
+
77
+ ### Profile Status
78
+
79
+ When listing profiles, each shows its status:
80
+ - **Green (●)** - Fully active: all selectors match
81
+ - **Yellow (●)** - Partially active: some selectors match
82
+ - **Gray (○)** - Inactive: no selectors match
83
+
43
84
  ## Marker Syntax
44
85
 
45
86
  - `@pik:select <name>` - Defines a selector group
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { selectPlugin } from './lib/plugin.js';
2
- export type { SelectConfig } from './lib/types.js';
2
+ export type { SelectConfig, ProfileMapping, ProfilesConfig } from './lib/types.js';
3
+ export type { ProfileStatus, SelectorMappingStatus } from './lib/types/profile-status.js';
3
4
  export { Scanner, type FileResult } from './lib/scanner.js';
5
+ export { Profile, type FoundSelector } from './lib/profile/index.js';
4
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAG/C,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAGnD,OAAO,EAAE,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAG/C,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACnF,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAG1F,OAAO,EAAE,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG5D,OAAO,EAAE,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAC"}
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Command } from "commander";
2
+ import { Parser, loadConfig, BlockSelector } from "@lsst/pik-core";
2
3
  import pc from "picocolors";
3
4
  import { relative } from "path";
4
- import { Parser, loadConfig, BlockSwitcher, SingleSwitcher } from "@lsst/pik-core";
5
5
  import { readFile, writeFile } from "fs/promises";
6
6
  import { glob } from "glob";
7
7
  import { select, Separator } from "@inquirer/prompts";
@@ -27,32 +27,22 @@ class Scanner {
27
27
  return results;
28
28
  }
29
29
  }
30
- function hasBlockOptions$2(selector) {
31
- return selector.blockOptions.length > 0;
32
- }
33
- function getActiveOptionName$1(selector) {
34
- if (hasBlockOptions$2(selector)) {
35
- return selector.blockOptions.find((b) => b.isActive)?.name ?? null;
36
- }
37
- return selector.options.find((o) => o.isActive)?.name ?? null;
38
- }
39
- function getAllOptions$1(selector) {
40
- if (hasBlockOptions$2(selector)) {
41
- return selector.blockOptions.map((b) => ({ name: b.name, isActive: b.isActive }));
42
- }
43
- return selector.options.map((o) => ({ name: o.name, isActive: o.isActive }));
44
- }
45
- const listCommand = new Command("list").alias("ls").description("List all selectors and their current state").option("--json", "Output in JSON format").action(async (options) => {
46
- const config = await loadConfig();
30
+ function requireSelectConfig(config, options) {
47
31
  if (!config?.select) {
48
- if (options.json) {
49
- console.log(JSON.stringify({ error: 'No pik config found or missing "select" section' }));
32
+ const message = 'No pik config found or missing "select" section';
33
+ if (options?.json) {
34
+ console.log(JSON.stringify({ error: message }));
50
35
  } else {
51
- console.error(pc.red('No pik config found or missing "select" section'));
36
+ console.error(pc.red(message));
52
37
  }
53
38
  process.exit(1);
54
39
  }
55
- const scanner = new Scanner(config.select);
40
+ return config.select;
41
+ }
42
+ const listCommand = new Command("list").alias("ls").description("List all selectors and their current state").option("--json", "Output in JSON format").action(async (options) => {
43
+ const config = await loadConfig();
44
+ const selectConfig = requireSelectConfig(config, options);
45
+ const scanner = new Scanner(selectConfig);
56
46
  const results = await scanner.scan();
57
47
  if (options.json) {
58
48
  const jsonOutput = results.flatMap(
@@ -60,9 +50,9 @@ const listCommand = new Command("list").alias("ls").description("List all select
60
50
  name: selector.name,
61
51
  file: relative(process.cwd(), file.path),
62
52
  line: selector.line,
63
- activeOption: getActiveOptionName$1(selector),
64
- isBlock: hasBlockOptions$2(selector),
65
- options: getAllOptions$1(selector)
53
+ activeOption: selector.getActiveOptionName(),
54
+ isBlock: selector instanceof BlockSelector,
55
+ options: selector.options.map((o) => ({ name: o.name, isActive: o.isActive }))
66
56
  }))
67
57
  );
68
58
  console.log(JSON.stringify(jsonOutput, null, 2));
@@ -76,11 +66,11 @@ const listCommand = new Command("list").alias("ls").description("List all select
76
66
  const relativePath = relative(process.cwd(), file.path);
77
67
  console.log(pc.cyan(relativePath));
78
68
  for (const selector of file.selectors) {
79
- const activeOptionName = getActiveOptionName$1(selector);
69
+ const activeOptionName = selector.getActiveOptionName();
80
70
  const activeLabel = activeOptionName ? pc.green(activeOptionName) : pc.yellow("none");
81
- const blockIndicator = hasBlockOptions$2(selector) ? pc.dim(" [block]") : "";
71
+ const blockIndicator = selector instanceof BlockSelector ? pc.dim(" [block]") : "";
82
72
  console.log(` ${pc.bold(selector.name)}${blockIndicator}: ${activeLabel}`);
83
- for (const option of getAllOptions$1(selector)) {
73
+ for (const option of selector.options) {
84
74
  const marker = option.isActive ? pc.green("●") : pc.dim("○");
85
75
  console.log(` ${marker} ${option.name}`);
86
76
  }
@@ -88,16 +78,10 @@ const listCommand = new Command("list").alias("ls").description("List all select
88
78
  console.log();
89
79
  }
90
80
  });
91
- function hasBlockOptions$1(selector) {
92
- return selector.blockOptions.length > 0;
93
- }
94
81
  const setCommand = new Command("set").description("Set a specific option for a selector").argument("<selector>", "Selector name").argument("<option>", "Option to activate").action(async (selectorName, optionName) => {
95
82
  const config = await loadConfig();
96
- if (!config?.select) {
97
- console.error(pc.red('No pik config found or missing "select" section'));
98
- process.exit(1);
99
- }
100
- const scanner = new Scanner(config.select);
83
+ const selectConfig = requireSelectConfig(config);
84
+ const scanner = new Scanner(selectConfig);
101
85
  const results = await scanner.scan();
102
86
  let found = false;
103
87
  for (const file of results) {
@@ -105,14 +89,7 @@ const setCommand = new Command("set").description("Set a specific option for a s
105
89
  if (selector) {
106
90
  found = true;
107
91
  try {
108
- let newContent;
109
- if (hasBlockOptions$1(selector)) {
110
- const switcher = BlockSwitcher.forFilePath(file.path);
111
- newContent = switcher.switch(file.content, selector, optionName);
112
- } else {
113
- const switcher = SingleSwitcher.forFilePath(file.path);
114
- newContent = switcher.switch(file.content, selector, optionName);
115
- }
92
+ const newContent = selector.switchTo(file.content, optionName, file.path);
116
93
  await writeFile(file.path, newContent);
117
94
  const relativePath = relative(process.cwd(), file.path);
118
95
  console.log(
@@ -132,31 +109,13 @@ const setCommand = new Command("set").description("Set a specific option for a s
132
109
  }
133
110
  });
134
111
  const BACK_VALUE = /* @__PURE__ */ Symbol("back");
135
- function isExitPromptError(error) {
112
+ function isExitPromptError$1(error) {
136
113
  return error instanceof Error && error.name === "ExitPromptError";
137
114
  }
138
- function hasBlockOptions(selector) {
139
- return selector.blockOptions.length > 0;
140
- }
141
- function getActiveOptionName(selector) {
142
- if (hasBlockOptions(selector)) {
143
- return selector.blockOptions.find((b) => b.isActive)?.name ?? null;
144
- }
145
- return selector.options.find((o) => o.isActive)?.name ?? null;
146
- }
147
- function getAllOptions(selector) {
148
- if (hasBlockOptions(selector)) {
149
- return selector.blockOptions.map((b) => ({ name: b.name, isActive: b.isActive }));
150
- }
151
- return selector.options.map((o) => ({ name: o.name, isActive: o.isActive }));
152
- }
153
115
  const switchCommand = new Command("switch").alias("sw").description("Interactively switch options").action(async () => {
154
116
  const config = await loadConfig();
155
- if (!config?.select) {
156
- console.error(pc.red('No pik config found or missing "select" section'));
157
- process.exit(1);
158
- }
159
- const scanner = new Scanner(config.select);
117
+ const selectConfig = requireSelectConfig(config);
118
+ const scanner = new Scanner(selectConfig);
160
119
  const results = await scanner.scan();
161
120
  if (results.length === 0) {
162
121
  console.log(pc.yellow("No selectors found"));
@@ -176,9 +135,9 @@ const switchCommand = new Command("switch").alias("sw").description("Interactive
176
135
  choices: [
177
136
  ...choices.map((choice) => {
178
137
  const relativePath2 = relative(process.cwd(), choice.file.path);
179
- const activeOptionName = getActiveOptionName(choice.selector);
138
+ const activeOptionName = choice.selector.getActiveOptionName();
180
139
  const current = activeOptionName ? pc.dim(` (${activeOptionName})`) : "";
181
- const blockIndicator = hasBlockOptions(choice.selector) ? pc.dim(" [block]") : "";
140
+ const blockIndicator = choice.selector instanceof BlockSelector ? pc.dim(" [block]") : "";
182
141
  return {
183
142
  name: `${choice.selector.name}${blockIndicator}${current} ${pc.dim(`- ${relativePath2}`)}`,
184
143
  value: choice
@@ -189,7 +148,7 @@ const switchCommand = new Command("switch").alias("sw").description("Interactive
189
148
  ]
190
149
  });
191
150
  } catch (error) {
192
- if (isExitPromptError(error)) {
151
+ if (isExitPromptError$1(error)) {
193
152
  process.exit(0);
194
153
  }
195
154
  throw error;
@@ -198,12 +157,11 @@ const switchCommand = new Command("switch").alias("sw").description("Interactive
198
157
  return;
199
158
  }
200
159
  let selectedOption;
201
- const allOptions = getAllOptions(selectedChoice.selector);
202
160
  try {
203
161
  selectedOption = await select({
204
162
  message: `Select option for ${pc.bold(selectedChoice.selector.name)}`,
205
163
  choices: [
206
- ...allOptions.map((option) => ({
164
+ ...selectedChoice.selector.options.map((option) => ({
207
165
  name: option.isActive ? `${option.name} ${pc.green("(current)")}` : option.name,
208
166
  value: option.name
209
167
  })),
@@ -212,7 +170,7 @@ const switchCommand = new Command("switch").alias("sw").description("Interactive
212
170
  ]
213
171
  });
214
172
  } catch (error) {
215
- if (isExitPromptError(error)) {
173
+ if (isExitPromptError$1(error)) {
216
174
  process.exit(0);
217
175
  }
218
176
  throw error;
@@ -220,22 +178,11 @@ const switchCommand = new Command("switch").alias("sw").description("Interactive
220
178
  if (selectedOption === BACK_VALUE) {
221
179
  continue;
222
180
  }
223
- let newContent;
224
- if (hasBlockOptions(selectedChoice.selector)) {
225
- const switcher = BlockSwitcher.forFilePath(selectedChoice.file.path);
226
- newContent = switcher.switch(
227
- selectedChoice.file.content,
228
- selectedChoice.selector,
229
- selectedOption
230
- );
231
- } else {
232
- const switcher = SingleSwitcher.forFilePath(selectedChoice.file.path);
233
- newContent = switcher.switch(
234
- selectedChoice.file.content,
235
- selectedChoice.selector,
236
- selectedOption
237
- );
238
- }
181
+ const newContent = selectedChoice.selector.switchTo(
182
+ selectedChoice.file.content,
183
+ selectedOption,
184
+ selectedChoice.file.path
185
+ );
239
186
  await writeFile(selectedChoice.file.path, newContent);
240
187
  const relativePath = relative(process.cwd(), selectedChoice.file.path);
241
188
  console.log(
@@ -246,6 +193,310 @@ const switchCommand = new Command("switch").alias("sw").description("Interactive
246
193
  return;
247
194
  }
248
195
  });
196
+ class Profile {
197
+ constructor(name, mapping) {
198
+ this.name = name;
199
+ this.mapping = mapping;
200
+ }
201
+ /**
202
+ * Find a selector by name across scan results
203
+ */
204
+ static findSelector(results, selectorName) {
205
+ for (const file of results) {
206
+ const selector = file.selectors.find((s) => s.name === selectorName);
207
+ if (selector) {
208
+ return { file, selector };
209
+ }
210
+ }
211
+ return null;
212
+ }
213
+ /**
214
+ * Compute the status of this profile against scan results
215
+ */
216
+ computeStatus(results) {
217
+ const mappings = [];
218
+ for (const [selectorName, expectedOption] of Object.entries(this.mapping)) {
219
+ const found = Profile.findSelector(results, selectorName);
220
+ if (!found) {
221
+ mappings.push({
222
+ selectorName,
223
+ expectedOption,
224
+ currentOption: null,
225
+ filePath: "",
226
+ isMatched: false,
227
+ error: `Selector "${selectorName}" not found`
228
+ });
229
+ continue;
230
+ }
231
+ const currentOption = found.selector.getActiveOptionName();
232
+ const optionExists = found.selector.optionExists(expectedOption);
233
+ if (!optionExists) {
234
+ mappings.push({
235
+ selectorName,
236
+ expectedOption,
237
+ currentOption,
238
+ filePath: found.file.path,
239
+ isMatched: false,
240
+ error: `Option "${expectedOption}" not found in selector "${selectorName}"`
241
+ });
242
+ continue;
243
+ }
244
+ mappings.push({
245
+ selectorName,
246
+ expectedOption,
247
+ currentOption,
248
+ filePath: found.file.path,
249
+ isMatched: currentOption === expectedOption
250
+ });
251
+ }
252
+ const matchedCount = mappings.filter((m) => m.isMatched).length;
253
+ const totalCount = mappings.length;
254
+ return {
255
+ name: this.name,
256
+ mappings,
257
+ isFullyActive: matchedCount === totalCount && totalCount > 0,
258
+ isPartiallyActive: matchedCount > 0 && matchedCount < totalCount,
259
+ matchedCount,
260
+ totalCount
261
+ };
262
+ }
263
+ /**
264
+ * Apply this profile to the scanned files
265
+ */
266
+ async apply(results) {
267
+ const applyResults = [];
268
+ const changesByFile = /* @__PURE__ */ new Map();
269
+ for (const [selectorName, optionName] of Object.entries(this.mapping)) {
270
+ const found = Profile.findSelector(results, selectorName);
271
+ if (!found) {
272
+ applyResults.push({
273
+ selectorName,
274
+ optionName,
275
+ filePath: "",
276
+ success: false,
277
+ error: `Selector "${selectorName}" not found`
278
+ });
279
+ continue;
280
+ }
281
+ const optionExists = found.selector.optionExists(optionName);
282
+ if (!optionExists) {
283
+ const availableOptions = found.selector.options.map((o) => o.name).join(", ");
284
+ applyResults.push({
285
+ selectorName,
286
+ optionName,
287
+ filePath: found.file.path,
288
+ success: false,
289
+ error: `Option "${optionName}" not found. Available: ${availableOptions}`
290
+ });
291
+ continue;
292
+ }
293
+ const existing = changesByFile.get(found.file.path);
294
+ if (existing) {
295
+ existing.push({
296
+ selectorName,
297
+ optionName,
298
+ selector: found.selector
299
+ });
300
+ } else {
301
+ changesByFile.set(found.file.path, [
302
+ {
303
+ selectorName,
304
+ optionName,
305
+ selector: found.selector
306
+ }
307
+ ]);
308
+ }
309
+ }
310
+ for (const [filePath, changes] of changesByFile) {
311
+ let content = await readFile(filePath, "utf-8");
312
+ for (const change of changes) {
313
+ try {
314
+ const parser = Parser.forFilePath(filePath);
315
+ const { selectors } = parser.parse(content);
316
+ const freshSelector = selectors.find((s) => s.name === change.selectorName);
317
+ if (!freshSelector) {
318
+ applyResults.push({
319
+ selectorName: change.selectorName,
320
+ optionName: change.optionName,
321
+ filePath,
322
+ success: false,
323
+ error: `Selector "${change.selectorName}" not found after previous changes`
324
+ });
325
+ continue;
326
+ }
327
+ content = freshSelector.switchTo(content, change.optionName, filePath);
328
+ applyResults.push({
329
+ selectorName: change.selectorName,
330
+ optionName: change.optionName,
331
+ filePath,
332
+ success: true
333
+ });
334
+ } catch (error) {
335
+ applyResults.push({
336
+ selectorName: change.selectorName,
337
+ optionName: change.optionName,
338
+ filePath,
339
+ success: false,
340
+ error: error instanceof Error ? error.message : String(error)
341
+ });
342
+ }
343
+ }
344
+ await writeFile(filePath, content);
345
+ }
346
+ return applyResults;
347
+ }
348
+ /**
349
+ * Compute statuses for multiple profiles
350
+ */
351
+ static computeAllStatuses(profiles, results) {
352
+ return Object.entries(profiles).map(
353
+ ([name, mapping]) => new Profile(name, mapping).computeStatus(results)
354
+ );
355
+ }
356
+ }
357
+ function isExitPromptError(error) {
358
+ return error instanceof Error && error.name === "ExitPromptError";
359
+ }
360
+ const profileCommand = new Command("profile").description("Apply a profile to switch multiple selectors at once").argument("[name]", "Profile name to apply").action(async (profileName) => {
361
+ const config = await loadConfig();
362
+ const selectConfig = requireSelectConfig(config);
363
+ const profiles = selectConfig.profiles;
364
+ if (!profiles || Object.keys(profiles).length === 0) {
365
+ console.error(pc.red("No profiles configured"));
366
+ process.exit(1);
367
+ }
368
+ const scanner = new Scanner(selectConfig);
369
+ const results = await scanner.scan();
370
+ if (!profileName) {
371
+ const statuses = Profile.computeAllStatuses(profiles, results);
372
+ try {
373
+ profileName = await select({
374
+ message: "Select a profile to apply",
375
+ choices: [
376
+ ...statuses.map((status) => {
377
+ let statusIndicator;
378
+ if (status.isFullyActive) {
379
+ statusIndicator = pc.green("●");
380
+ } else if (status.isPartiallyActive) {
381
+ statusIndicator = pc.yellow("●");
382
+ } else {
383
+ statusIndicator = pc.dim("○");
384
+ }
385
+ const countInfo = pc.dim(`(${status.matchedCount}/${status.totalCount})`);
386
+ return {
387
+ name: `${statusIndicator} ${status.name} ${countInfo}`,
388
+ value: status.name
389
+ };
390
+ }),
391
+ new Separator(),
392
+ { name: pc.dim("← Cancel"), value: "" }
393
+ ]
394
+ });
395
+ } catch (error) {
396
+ if (isExitPromptError(error)) {
397
+ process.exit(0);
398
+ }
399
+ throw error;
400
+ }
401
+ if (!profileName) {
402
+ return;
403
+ }
404
+ }
405
+ const profileMapping = profiles[profileName];
406
+ if (!profileMapping) {
407
+ const availableProfiles = Object.keys(profiles).join(", ");
408
+ console.error(pc.red(`Profile "${profileName}" not found`));
409
+ console.error(pc.dim(`Available profiles: ${availableProfiles}`));
410
+ process.exit(1);
411
+ }
412
+ const profile = new Profile(profileName, profileMapping);
413
+ const applyResults = await profile.apply(results);
414
+ const successes = applyResults.filter((r) => r.success);
415
+ const failures = applyResults.filter((r) => !r.success);
416
+ for (const result of successes) {
417
+ const relativePath = relative(process.cwd(), result.filePath);
418
+ console.log(
419
+ pc.green(
420
+ `✓ Set ${pc.bold(result.selectorName)} to ${pc.bold(result.optionName)} in ${relativePath}`
421
+ )
422
+ );
423
+ }
424
+ for (const result of failures) {
425
+ console.error(pc.red(`✗ ${result.selectorName}: ${result.error}`));
426
+ }
427
+ if (failures.length > 0) {
428
+ console.log();
429
+ console.log(
430
+ pc.yellow(`Applied ${successes.length}/${applyResults.length} selector(s) from profile "${profileName}"`)
431
+ );
432
+ process.exit(1);
433
+ } else {
434
+ console.log();
435
+ console.log(pc.green(`✓ Applied profile "${profileName}" (${successes.length} selector(s))`));
436
+ }
437
+ });
438
+ const profilesCommand = new Command("profiles").description("List all profiles and their status").option("--json", "Output in JSON format").action(async (options) => {
439
+ const config = await loadConfig();
440
+ const selectConfig = requireSelectConfig(config, options);
441
+ const profiles = selectConfig.profiles;
442
+ if (!profiles || Object.keys(profiles).length === 0) {
443
+ if (options.json) {
444
+ console.log(JSON.stringify({ error: "No profiles configured" }));
445
+ } else {
446
+ console.log(pc.yellow("No profiles configured"));
447
+ }
448
+ return;
449
+ }
450
+ const scanner = new Scanner(selectConfig);
451
+ const results = await scanner.scan();
452
+ const statuses = Profile.computeAllStatuses(profiles, results);
453
+ if (options.json) {
454
+ const jsonOutput = statuses.map((status) => ({
455
+ name: status.name,
456
+ isFullyActive: status.isFullyActive,
457
+ isPartiallyActive: status.isPartiallyActive,
458
+ matchedCount: status.matchedCount,
459
+ totalCount: status.totalCount,
460
+ mappings: status.mappings.map((m) => ({
461
+ selectorName: m.selectorName,
462
+ expectedOption: m.expectedOption,
463
+ currentOption: m.currentOption,
464
+ filePath: m.filePath ? relative(process.cwd(), m.filePath) : null,
465
+ isMatched: m.isMatched,
466
+ error: m.error
467
+ }))
468
+ }));
469
+ console.log(JSON.stringify(jsonOutput, null, 2));
470
+ return;
471
+ }
472
+ for (const status of statuses) {
473
+ let statusIndicator;
474
+ if (status.isFullyActive) {
475
+ statusIndicator = pc.green("●");
476
+ } else if (status.isPartiallyActive) {
477
+ statusIndicator = pc.yellow("●");
478
+ } else {
479
+ statusIndicator = pc.dim("○");
480
+ }
481
+ const countInfo = pc.dim(`(${status.matchedCount}/${status.totalCount})`);
482
+ console.log(`${statusIndicator} ${pc.bold(status.name)} ${countInfo}`);
483
+ for (const mapping of status.mappings) {
484
+ const mappingIndicator = mapping.isMatched ? pc.green("✓") : pc.dim("○");
485
+ const currentValue = mapping.currentOption ?? pc.dim("none");
486
+ const expectedValue = mapping.expectedOption;
487
+ if (mapping.error) {
488
+ console.log(` ${pc.red("✗")} ${mapping.selectorName}: ${pc.red(mapping.error)}`);
489
+ } else if (mapping.isMatched) {
490
+ console.log(` ${mappingIndicator} ${mapping.selectorName}: ${pc.green(currentValue)}`);
491
+ } else {
492
+ console.log(
493
+ ` ${mappingIndicator} ${mapping.selectorName}: ${currentValue} ${pc.dim(`→ ${expectedValue}`)}`
494
+ );
495
+ }
496
+ }
497
+ console.log();
498
+ }
499
+ });
249
500
  const selectPlugin = {
250
501
  name: "Select",
251
502
  description: "Switch config options using @pik markers",
@@ -256,6 +507,8 @@ const selectPlugin = {
256
507
  selectCmd.addCommand(listCommand);
257
508
  selectCmd.addCommand(setCommand);
258
509
  selectCmd.addCommand(switchCommand);
510
+ selectCmd.addCommand(profileCommand);
511
+ selectCmd.addCommand(profilesCommand);
259
512
  selectCmd.action(async () => {
260
513
  await switchCommand.parseAsync([], { from: "user" });
261
514
  });
@@ -265,6 +518,7 @@ const selectPlugin = {
265
518
  }
266
519
  };
267
520
  export {
521
+ Profile,
268
522
  Scanner,
269
523
  selectPlugin
270
524
  };
@@ -1 +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;AAKpC,OAAO,aAAa,CAAC;AAiCrB,eAAO,MAAM,WAAW,SA4DpB,CAAC"}
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/lib/commands/list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC,OAAO,aAAa,CAAC;AAMrB,eAAO,MAAM,WAAW,SAoDpB,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { Command } from 'commander';
2
+ import '../types.js';
3
+ export declare const profileCommand: Command;
4
+ //# sourceMappingURL=profile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../../src/lib/commands/profile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,OAAO,aAAa,CAAC;AAMrB,eAAO,MAAM,cAAc,SAkGvB,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { Command } from 'commander';
2
+ import '../types.js';
3
+ export declare const profilesCommand: Command;
4
+ //# sourceMappingURL=profiles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../../../src/lib/commands/profiles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC,OAAO,aAAa,CAAC;AAMrB,eAAO,MAAM,eAAe,SA0ExB,CAAC"}
@@ -1 +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;AAMpC,OAAO,aAAa,CAAC;AASrB,eAAO,MAAM,UAAU,SAqDnB,CAAC"}
1
+ {"version":3,"file":"set.d.ts","sourceRoot":"","sources":["../../../src/lib/commands/set.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC,OAAO,aAAa,CAAC;AAErB,eAAO,MAAM,UAAU,SAwCnB,CAAC"}
@@ -1 +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;AAOpC,OAAO,aAAa,CAAC;AAwCrB,eAAO,MAAM,aAAa,SAuHtB,CAAC"}
1
+ {"version":3,"file":"switch.d.ts","sourceRoot":"","sources":["../../../src/lib/commands/switch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,OAAO,aAAa,CAAC;AAarB,eAAO,MAAM,aAAa,SAqGtB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/lib/plugin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAKhD,eAAO,MAAM,YAAY,EAAE,SA4B1B,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/lib/plugin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAOhD,eAAO,MAAM,YAAY,EAAE,SA8B1B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { Profile, type FoundSelector, type ApplyResult } from './profile.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/profile/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,46 @@
1
+ import { type BaseSelector } from '@lsst/pik-core';
2
+ import type { FileResult } from '../scanner.js';
3
+ import type { ProfileMapping } from '../types.js';
4
+ import type { ProfileStatus } from '../types/profile-status.js';
5
+ /**
6
+ * Result of finding a selector by name
7
+ */
8
+ export interface FoundSelector {
9
+ file: FileResult;
10
+ selector: BaseSelector;
11
+ }
12
+ /**
13
+ * Result of applying a single selector change
14
+ */
15
+ export interface ApplyResult {
16
+ selectorName: string;
17
+ optionName: string;
18
+ filePath: string;
19
+ success: boolean;
20
+ error?: string;
21
+ }
22
+ /**
23
+ * Represents a profile that maps selector names to option names
24
+ */
25
+ export declare class Profile {
26
+ readonly name: string;
27
+ readonly mapping: ProfileMapping;
28
+ constructor(name: string, mapping: ProfileMapping);
29
+ /**
30
+ * Find a selector by name across scan results
31
+ */
32
+ static findSelector(results: FileResult[], selectorName: string): FoundSelector | null;
33
+ /**
34
+ * Compute the status of this profile against scan results
35
+ */
36
+ computeStatus(results: FileResult[]): ProfileStatus;
37
+ /**
38
+ * Apply this profile to the scanned files
39
+ */
40
+ apply(results: FileResult[]): Promise<ApplyResult[]>;
41
+ /**
42
+ * Compute statuses for multiple profiles
43
+ */
44
+ static computeAllStatuses(profiles: Record<string, ProfileMapping>, results: FileResult[]): ProfileStatus[];
45
+ }
46
+ //# sourceMappingURL=profile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../../src/lib/profile/profile.ts"],"names":[],"mappings":"AACA,OAAO,EAAU,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,aAAa,EAAyB,MAAM,4BAA4B,CAAC;AAEvF;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,YAAY,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,qBAAa,OAAO;aAEA,IAAI,EAAE,MAAM;aACZ,OAAO,EAAE,cAAc;gBADvB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,cAAc;IAGzC;;OAEG;IACH,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,YAAY,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI;IAUtF;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,aAAa;IAuDnD;;OAEG;IACG,KAAK,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAsG1D;;OAEG;IACH,MAAM,CAAC,kBAAkB,CACvB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,EACxC,OAAO,EAAE,UAAU,EAAE,GACpB,aAAa,EAAE;CAKnB"}
@@ -1,8 +1,8 @@
1
- import { type Selector } from '@lsst/pik-core';
1
+ import { type BaseSelector } from '@lsst/pik-core';
2
2
  import type { SelectConfig } from './types.js';
3
3
  export interface FileResult {
4
4
  path: string;
5
- selectors: Selector[];
5
+ selectors: BaseSelector[];
6
6
  content: string;
7
7
  }
8
8
  export declare class Scanner {
@@ -1 +1 @@
1
- {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/lib/scanner.ts"],"names":[],"mappings":"AAEA,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,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,YAAY;IAE3C,IAAI,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;CAqB/D"}
1
+ {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/lib/scanner.ts"],"names":[],"mappings":"AAEA,OAAO,EAAU,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,YAAY,EAAE,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,OAAO;IACN,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,YAAY;IAE3C,IAAI,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;CAqB/D"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Status of a single selector mapping within a profile
3
+ */
4
+ export interface SelectorMappingStatus {
5
+ /** Name of the selector */
6
+ selectorName: string;
7
+ /** Expected option from the profile */
8
+ expectedOption: string;
9
+ /** Current active option in the file */
10
+ currentOption: string | null;
11
+ /** Path to the file containing the selector */
12
+ filePath: string;
13
+ /** Whether the current option matches the expected option */
14
+ isMatched: boolean;
15
+ /** Error message if selector or option not found */
16
+ error?: string;
17
+ }
18
+ /**
19
+ * Status of a profile
20
+ */
21
+ export interface ProfileStatus {
22
+ /** Profile name */
23
+ name: string;
24
+ /** Status of each selector mapping */
25
+ mappings: SelectorMappingStatus[];
26
+ /** All mappings match their expected options */
27
+ isFullyActive: boolean;
28
+ /** Some mappings match their expected options */
29
+ isPartiallyActive: boolean;
30
+ /** Number of matched mappings */
31
+ matchedCount: number;
32
+ /** Total number of mappings in the profile */
33
+ totalCount: number;
34
+ }
35
+ //# sourceMappingURL=profile-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile-status.d.ts","sourceRoot":"","sources":["../../../src/lib/types/profile-status.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,2BAA2B;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,cAAc,EAAE,MAAM,CAAC;IACvB,wCAAwC;IACxC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,SAAS,EAAE,OAAO,CAAC;IACnB,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,sCAAsC;IACtC,QAAQ,EAAE,qBAAqB,EAAE,CAAC;IAClC,gDAAgD;IAChD,aAAa,EAAE,OAAO,CAAC;IACvB,iDAAiD;IACjD,iBAAiB,EAAE,OAAO,CAAC;IAC3B,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,8CAA8C;IAC9C,UAAU,EAAE,MAAM,CAAC;CACpB"}
@@ -1,9 +1,23 @@
1
+ /**
2
+ * Maps selector names to option names within a profile
3
+ */
4
+ export interface ProfileMapping {
5
+ [selectorName: string]: string;
6
+ }
7
+ /**
8
+ * Collection of named profiles
9
+ */
10
+ export interface ProfilesConfig {
11
+ [profileName: string]: ProfileMapping;
12
+ }
1
13
  /**
2
14
  * Configuration for the select plugin
3
15
  */
4
16
  export interface SelectConfig {
5
17
  /** File patterns to scan for @pik markers */
6
18
  include: string[];
19
+ /** Named profiles that apply multiple selector options at once */
20
+ profiles?: ProfilesConfig;
7
21
  }
8
22
  /**
9
23
  * Extend PikConfig to include select plugin config
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,6CAA6C;IAC7C,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;GAEG;AACH,OAAO,QAAQ,gBAAgB,CAAC;IAC9B,UAAU,SAAS;QACjB,MAAM,CAAC,EAAE,YAAY,CAAC;KACvB;CACF"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,CAAC;CACvC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,6CAA6C;IAC7C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED;;GAEG;AACH,OAAO,QAAQ,gBAAgB,CAAC;IAC9B,UAAU,SAAS;QACjB,MAAM,CAAC,EAAE,YAAY,CAAC;KACvB;CACF"}
@@ -0,0 +1,12 @@
1
+ import type { PikConfig } from '@lsst/pik-core';
2
+ import type { SelectConfig } from '../types.js';
3
+ /**
4
+ * Validates that config has a select section.
5
+ * Exits with error if not found.
6
+ *
7
+ * @returns The select config (type-narrowed)
8
+ */
9
+ export declare function requireSelectConfig(config: PikConfig | null, options?: {
10
+ json?: boolean;
11
+ }): SelectConfig;
12
+ //# sourceMappingURL=requireSelectConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"requireSelectConfig.d.ts","sourceRoot":"","sources":["../../../src/lib/validation/requireSelectConfig.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,SAAS,GAAG,IAAI,EACxB,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAC3B,YAAY,CAWd"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lsst/pik-plugin-select",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "Config selector plugin for pik CLI",
5
5
  "type": "module",
6
6
  "license": "MIT",