@lsst/pik-plugin-select 0.8.1 → 0.9.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.
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
+ import { Parser, loadGlobalConfig, loadConfig, expandHome, BlockSelector } from "@lsst/pik-core";
3
3
  import pc from "picocolors";
4
- import { relative } from "path";
4
+ import { resolve, basename, relative } from "path";
5
5
  import { readFile, writeFile } from "fs/promises";
6
6
  import { glob } from "glob";
7
7
  import { select, Separator } from "@inquirer/prompts";
@@ -27,50 +27,112 @@ class Scanner {
27
27
  return results;
28
28
  }
29
29
  }
30
- function requireSelectConfig(config, options) {
31
- if (!config?.select) {
32
- const message = 'No pik config found or missing "select" section';
33
- if (options?.json) {
34
- console.log(JSON.stringify({ error: message }));
35
- } else {
36
- console.error(pc.red(message));
30
+ function normalizeEntry(entry) {
31
+ if (typeof entry === "string") {
32
+ const path2 = resolve(expandHome(entry));
33
+ return { path: path2, name: basename(path2) };
34
+ }
35
+ const path = resolve(expandHome(entry.path));
36
+ return {
37
+ path,
38
+ name: entry.name ?? basename(path),
39
+ selectors: entry.selectors
40
+ };
41
+ }
42
+ function isOptedIn(selector, entry) {
43
+ if (selector.isGlobal) {
44
+ return true;
45
+ }
46
+ if (entry.selectors === void 0) {
47
+ return true;
48
+ }
49
+ return entry.selectors.includes(selector.name);
50
+ }
51
+ async function collectGlobalSelectors() {
52
+ const globalConfig = await loadGlobalConfig();
53
+ if (!globalConfig?.projects?.length) {
54
+ return [];
55
+ }
56
+ const items = [];
57
+ for (const rawEntry of globalConfig.projects) {
58
+ const entry = normalizeEntry(rawEntry);
59
+ const projectConfig = await loadConfig(entry.path);
60
+ if (!projectConfig?.select) {
61
+ continue;
62
+ }
63
+ const scanner = new Scanner(projectConfig.select);
64
+ const results = await scanner.scan(entry.path);
65
+ for (const file of results) {
66
+ for (const selector of file.selectors) {
67
+ if (isOptedIn(selector, entry)) {
68
+ items.push({
69
+ project: entry.name,
70
+ root: entry.path,
71
+ file: file.path,
72
+ content: file.content,
73
+ selector
74
+ });
75
+ }
76
+ }
37
77
  }
38
- process.exit(1);
39
78
  }
40
- return config.select;
79
+ return items;
41
80
  }
42
81
  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
82
  const config = await loadConfig();
44
- const selectConfig = requireSelectConfig(config, options);
45
- const scanner = new Scanner(selectConfig);
46
- const results = await scanner.scan();
83
+ let localResults = [];
84
+ if (config?.select) {
85
+ const scanner = new Scanner(config.select);
86
+ localResults = await scanner.scan();
87
+ }
88
+ const globalItems = await collectGlobalSelectors();
47
89
  if (options.json) {
48
- const jsonOutput = results.flatMap(
90
+ const localJson = localResults.flatMap(
49
91
  (file) => file.selectors.map((selector) => ({
50
92
  name: selector.name,
51
93
  file: relative(process.cwd(), file.path),
52
94
  line: selector.line,
53
95
  activeOption: selector.getActiveOptionName(),
54
96
  isBlock: selector instanceof BlockSelector,
97
+ global: false,
98
+ project: null,
55
99
  options: selector.options.map((o) => ({ name: o.name, isActive: o.isActive }))
56
100
  }))
57
101
  );
58
- console.log(JSON.stringify(jsonOutput, null, 2));
102
+ const globalJson = globalItems.map((item) => ({
103
+ name: `${item.project}:${item.selector.name}`,
104
+ file: item.file,
105
+ line: item.selector.line,
106
+ activeOption: item.selector.getActiveOptionName(),
107
+ isBlock: item.selector instanceof BlockSelector,
108
+ global: true,
109
+ project: item.project,
110
+ options: item.selector.options.map((o) => ({ name: o.name, isActive: o.isActive }))
111
+ }));
112
+ console.log(JSON.stringify([...localJson, ...globalJson], null, 2));
59
113
  return;
60
114
  }
61
- if (results.length === 0) {
115
+ if (localResults.length === 0 && globalItems.length === 0) {
62
116
  console.log(pc.yellow("No selectors found"));
63
117
  return;
64
118
  }
65
- for (const file of results) {
119
+ for (const file of localResults) {
66
120
  const relativePath = relative(process.cwd(), file.path);
67
121
  console.log(pc.cyan(relativePath));
68
- for (const selector of file.selectors) {
69
- const activeOptionName = selector.getActiveOptionName();
122
+ printSelectors(file.selectors);
123
+ console.log();
124
+ }
125
+ if (globalItems.length > 0) {
126
+ console.log(pc.magenta("Global"));
127
+ for (const item of globalItems) {
128
+ const activeOptionName = item.selector.getActiveOptionName();
70
129
  const activeLabel = activeOptionName ? pc.green(activeOptionName) : pc.yellow("none");
71
- const blockIndicator = selector instanceof BlockSelector ? pc.dim(" [block]") : "";
72
- console.log(` ${pc.bold(selector.name)}${blockIndicator}: ${activeLabel}`);
73
- for (const option of selector.options) {
130
+ const blockIndicator = item.selector instanceof BlockSelector ? pc.dim(" [block]") : "";
131
+ const name = `${item.project}:${item.selector.name}`;
132
+ console.log(
133
+ ` ${pc.bold(name)}${blockIndicator}: ${activeLabel} ${pc.dim(`- ${item.file}`)}`
134
+ );
135
+ for (const option of item.selector.options) {
74
136
  const marker = option.isActive ? pc.green("●") : pc.dim("○");
75
137
  console.log(` ${marker} ${option.name}`);
76
138
  }
@@ -78,10 +140,63 @@ const listCommand = new Command("list").alias("ls").description("List all select
78
140
  console.log();
79
141
  }
80
142
  });
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) => {
143
+ function printSelectors(selectors) {
144
+ for (const selector of selectors) {
145
+ const activeOptionName = selector.getActiveOptionName();
146
+ const activeLabel = activeOptionName ? pc.green(activeOptionName) : pc.yellow("none");
147
+ const blockIndicator = selector instanceof BlockSelector ? pc.dim(" [block]") : "";
148
+ console.log(` ${pc.bold(selector.name)}${blockIndicator}: ${activeLabel}`);
149
+ for (const option of selector.options) {
150
+ const marker = option.isActive ? pc.green("●") : pc.dim("○");
151
+ console.log(` ${marker} ${option.name}`);
152
+ }
153
+ }
154
+ }
155
+ const setCommand = new Command("set").description("Set a specific option for a selector").argument("<selector>", 'Selector name (use "project:selector" for a global switch)').argument("<option>", "Option to activate").action(async (selectorName, optionName) => {
156
+ const colonIdx = selectorName.indexOf(":");
157
+ if (colonIdx > 0) {
158
+ const projectLabel = selectorName.slice(0, colonIdx);
159
+ const bareName = selectorName.slice(colonIdx + 1);
160
+ const handled = await trySetGlobal(projectLabel, bareName, optionName);
161
+ if (handled) {
162
+ return;
163
+ }
164
+ }
165
+ await setLocal(selectorName, optionName);
166
+ });
167
+ async function trySetGlobal(projectLabel, bareName, optionName) {
168
+ const items = await collectGlobalSelectors();
169
+ const matches = items.filter(
170
+ (item) => item.project === projectLabel && item.selector.name === bareName
171
+ );
172
+ if (matches.length === 0) {
173
+ return false;
174
+ }
175
+ for (const item of matches) {
176
+ try {
177
+ const newContent = item.selector.switchTo(item.content, optionName, item.file);
178
+ await writeFile(item.file, newContent);
179
+ console.log(
180
+ pc.green(
181
+ `✓ Set ${pc.bold(`${projectLabel}:${bareName}`)} to ${pc.bold(optionName)} in ${item.file}`
182
+ )
183
+ );
184
+ } catch (error) {
185
+ if (error instanceof Error) {
186
+ console.error(pc.red(error.message));
187
+ }
188
+ process.exit(1);
189
+ }
190
+ }
191
+ return true;
192
+ }
193
+ async function setLocal(selectorName, optionName) {
82
194
  const config = await loadConfig();
83
- const selectConfig = requireSelectConfig(config);
84
- const scanner = new Scanner(selectConfig);
195
+ if (!config?.select) {
196
+ console.error(pc.red(`Selector "${selectorName}" not found`));
197
+ process.exit(1);
198
+ }
199
+ const scanner = new Scanner(config.select);
85
200
  const results = await scanner.scan();
86
201
  let found = false;
87
202
  for (const file of results) {
@@ -107,7 +222,19 @@ const setCommand = new Command("set").description("Set a specific option for a s
107
222
  console.error(pc.red(`Selector "${selectorName}" not found`));
108
223
  process.exit(1);
109
224
  }
110
- });
225
+ }
226
+ function requireSelectConfig(config, options) {
227
+ if (!config?.select) {
228
+ const message = 'No pik config found or missing "select" section';
229
+ if (options?.json) {
230
+ console.log(JSON.stringify({ error: message }));
231
+ } else {
232
+ console.error(pc.red(message));
233
+ }
234
+ process.exit(1);
235
+ }
236
+ return config.select;
237
+ }
111
238
  const BACK_VALUE = /* @__PURE__ */ Symbol("back");
112
239
  function isExitPromptError$1(error) {
113
240
  return error instanceof Error && error.name === "ExitPromptError";
@@ -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;AAOpC,OAAO,aAAa,CAAC;AAMrB,eAAO,MAAM,WAAW,SAoDpB,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;AAMpC,OAAO,aAAa,CAAC;AAMrB,eAAO,MAAM,WAAW,SAgFpB,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;AAOpC,OAAO,aAAa,CAAC;AAErB,eAAO,MAAM,UAAU,SAwCnB,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,SAkBnB,CAAC"}
@@ -0,0 +1,22 @@
1
+ import { type BaseSelector } from '@lsst/pik-core';
2
+ import './types.js';
3
+ export interface GlobalSelectorItem {
4
+ /** Display label of the owning project (used as the `project:` namespace). */
5
+ project: string;
6
+ /** Absolute project root. */
7
+ root: string;
8
+ /** Absolute path of the file holding the selector. */
9
+ file: string;
10
+ /** File content (so callers can switch without re-reading). */
11
+ content: string;
12
+ /** The parsed selector. */
13
+ selector: BaseSelector;
14
+ }
15
+ /**
16
+ * Collect selectors that opted into cross-project visibility, by scanning each
17
+ * project registered in the user-level global config (`~/.config/pik/config.*`).
18
+ *
19
+ * Returns an empty array when there is no global config or no opted-in selectors.
20
+ */
21
+ export declare function collectGlobalSelectors(): Promise<GlobalSelectorItem[]>;
22
+ //# sourceMappingURL=global-selectors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"global-selectors.d.ts","sourceRoot":"","sources":["../../src/lib/global-selectors.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,YAAY,EAElB,MAAM,gBAAgB,CAAC;AAExB,OAAO,YAAY,CAAC;AAEpB,MAAM,WAAW,kBAAkB;IACjC,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB,2BAA2B;IAC3B,QAAQ,EAAE,YAAY,CAAC;CACxB;AAsCD;;;;;GAKG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAqC5E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lsst/pik-plugin-select",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Config selector plugin for pik CLI",
5
5
  "type": "module",
6
6
  "license": "MIT",