@a9s/cli 1.0.8 → 1.0.10

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.
@@ -8,6 +8,8 @@ export function parseCommand(input) {
8
8
  return { type: "openRegions" };
9
9
  if (command === "resources")
10
10
  return { type: "openResources" };
11
+ if (command === "theme")
12
+ return { type: "openThemePicker" };
11
13
  const regionMatch = command.match(/^(region|use-region)\s+([a-z0-9-]+)$/i);
12
14
  if (regionMatch?.[2]) {
13
15
  return { type: "setRegion", region: regionMatch[2].toLowerCase() };
@@ -37,6 +39,9 @@ export function useCommandRouter(args) {
37
39
  case "openResources":
38
40
  args.openResourcePicker();
39
41
  return;
42
+ case "openThemePicker":
43
+ args.openThemePicker();
44
+ return;
40
45
  case "setRegion":
41
46
  args.setSelectedRegion(parsed.region);
42
47
  return;
@@ -3,10 +3,12 @@ import { textCell } from "../types.js";
3
3
  import { usePickerState } from "./usePickerState.js";
4
4
  import { usePickerTable } from "./usePickerTable.js";
5
5
  import { SERVICE_REGISTRY } from "../services.js";
6
+ import { THEMES, THEME_LABELS } from "../constants/theme.js";
6
7
  export function usePickerManager({ tableHeight, availableRegions, availableProfiles, }) {
7
8
  const region = usePickerState();
8
9
  const profile = usePickerState();
9
10
  const resource = usePickerState();
11
+ const theme = usePickerState();
10
12
  const regionRows = useMemo(() => availableRegions.map((r) => ({
11
13
  id: r.name,
12
14
  cells: { region: textCell(r.name), description: textCell(r.description) },
@@ -25,6 +27,14 @@ export function usePickerManager({ tableHeight, availableRegions, availableProfi
25
27
  },
26
28
  meta: {},
27
29
  })), []);
30
+ const themeRows = useMemo(() => Object.keys(THEMES).map((themeName) => ({
31
+ id: themeName,
32
+ cells: {
33
+ theme: textCell(THEME_LABELS[themeName]),
34
+ id: textCell(themeName),
35
+ },
36
+ meta: {},
37
+ })), []);
28
38
  const regionTable = usePickerTable({
29
39
  rows: regionRows,
30
40
  filterText: region.filter,
@@ -40,6 +50,11 @@ export function usePickerManager({ tableHeight, availableRegions, availableProfi
40
50
  filterText: resource.filter,
41
51
  maxHeight: tableHeight,
42
52
  });
53
+ const themeTable = usePickerTable({
54
+ rows: themeRows,
55
+ filterText: theme.filter,
56
+ maxHeight: tableHeight,
57
+ });
43
58
  const regionColumns = [
44
59
  { key: "region", label: "Region" },
45
60
  { key: "description", label: "Description" },
@@ -52,6 +67,10 @@ export function usePickerManager({ tableHeight, availableRegions, availableProfi
52
67
  { key: "resource", label: "Resource" },
53
68
  { key: "description", label: "Description" },
54
69
  ];
70
+ const themeColumns = [
71
+ { key: "theme", label: "Theme" },
72
+ { key: "id", label: "ID" },
73
+ ];
55
74
  const regionEntry = {
56
75
  id: "region",
57
76
  columns: regionColumns,
@@ -73,13 +92,22 @@ export function usePickerManager({ tableHeight, availableRegions, availableProfi
73
92
  ...resource,
74
93
  ...resourceTable,
75
94
  };
95
+ const themeEntry = {
96
+ id: "theme",
97
+ columns: themeColumns,
98
+ contextLabel: "Select Theme",
99
+ ...theme,
100
+ ...themeTable,
101
+ };
76
102
  const activePicker = regionEntry.open
77
103
  ? regionEntry
78
104
  : profileEntry.open
79
105
  ? profileEntry
80
106
  : resourceEntry.open
81
107
  ? resourceEntry
82
- : null;
108
+ : themeEntry.open
109
+ ? themeEntry
110
+ : null;
83
111
  const getEntry = (id) => {
84
112
  switch (id) {
85
113
  case "region":
@@ -88,6 +116,8 @@ export function usePickerManager({ tableHeight, availableRegions, availableProfi
88
116
  return profileEntry;
89
117
  case "resource":
90
118
  return resourceEntry;
119
+ case "theme":
120
+ return themeEntry;
91
121
  }
92
122
  };
93
123
  const openPicker = (id) => {
@@ -114,6 +144,9 @@ export function usePickerManager({ tableHeight, availableRegions, availableProfi
114
144
  case "profile":
115
145
  handlers.onSelectProfile(activePicker.selectedRow.id);
116
146
  break;
147
+ case "theme":
148
+ handlers.onSelectTheme(activePicker.selectedRow.id);
149
+ break;
117
150
  }
118
151
  activePicker.closePicker();
119
152
  };
@@ -121,6 +154,7 @@ export function usePickerManager({ tableHeight, availableRegions, availableProfi
121
154
  region: regionEntry,
122
155
  profile: profileEntry,
123
156
  resource: resourceEntry,
157
+ theme: themeEntry,
124
158
  activePicker,
125
159
  openPicker,
126
160
  closeActivePicker,
package/dist/src/index.js CHANGED
@@ -4,6 +4,7 @@ import { Command, Option } from "@commander-js/extra-typings";
4
4
  import { App } from "./App.js";
5
5
  import { SERVICE_REGISTRY } from "./services.js";
6
6
  import { withFullscreen } from "./utils/withFullscreen.js";
7
+ import { ThemeProvider } from "./contexts/ThemeContext.js";
7
8
  const SERVICE_IDS = Object.keys(SERVICE_REGISTRY);
8
9
  const program = new Command()
9
10
  .name("a9s")
@@ -17,7 +18,7 @@ program.parse();
17
18
  // opts() return type is fully inferred from addOption() calls via extra-typings
18
19
  const options = program.opts();
19
20
  void (async () => {
20
- const { instance, cleanup } = withFullscreen(_jsx(App, { initialService: options.service, endpointUrl: options.endpointUrl }));
21
+ const { instance, cleanup } = withFullscreen(_jsx(ThemeProvider, { children: _jsx(App, { initialService: options.service, endpointUrl: options.endpointUrl }) }));
21
22
  process.on("SIGINT", () => {
22
23
  cleanup();
23
24
  process.exit(0);
@@ -1,4 +1,5 @@
1
1
  import { atom } from "jotai";
2
+ import { loadConfig } from "../utils/config.js";
2
3
  /** Persists across HMR / re-renders. Currently selected AWS service. */
3
4
  export const currentlySelectedServiceAtom = atom("s3");
4
5
  /** Current UI mode (navigate / search / command). */
@@ -25,3 +26,5 @@ export const adapterSessionAtom = atom((get) => {
25
26
  });
26
27
  /** Toggle state for revealing/hiding secret values. Persists across HMR. */
27
28
  export const revealSecretsAtom = atom(false);
29
+ /** Active UI theme name — initialized from ~/.config/a9s/config.json on startup. */
30
+ export const themeNameAtom = atom(loadConfig().theme ?? "monokai");
@@ -0,0 +1,36 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+ import * as YAML from "yaml";
5
+ import { z } from "zod";
6
+ import { THEMES } from "../constants/theme.js";
7
+ const CONFIG_DIR = path.join(os.homedir(), ".config", "a9s");
8
+ const CONFIG_PATH = path.join(CONFIG_DIR, "config.yaml");
9
+ const THEME_NAMES = Object.keys(THEMES);
10
+ const A9sConfigSchema = z.object({
11
+ theme: z.enum(THEME_NAMES).optional(),
12
+ });
13
+ export function loadConfig() {
14
+ try {
15
+ const raw = YAML.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
16
+ return A9sConfigSchema.parse(raw);
17
+ }
18
+ catch {
19
+ return {};
20
+ }
21
+ }
22
+ export function saveConfig(update) {
23
+ try {
24
+ let existing = {};
25
+ try {
26
+ const raw = YAML.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
27
+ existing = A9sConfigSchema.parse(raw);
28
+ }
29
+ catch { }
30
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
31
+ fs.writeFileSync(CONFIG_PATH, YAML.stringify({ ...existing, ...update }));
32
+ }
33
+ catch {
34
+ // Best-effort — silently ignore write failures
35
+ }
36
+ }
@@ -5,6 +5,7 @@ import { getDefaultStore } from "jotai";
5
5
  import { unwrapDynamoValue, formatBillingMode, formatDynamoValue, getDynamoType, extractPkValue, extractSkValue, } from "./utils.js";
6
6
  import { createDynamoDBDetailCapability } from "./capabilities/detailCapability.js";
7
7
  import { createDynamoDBYankCapability } from "./capabilities/yankCapability.js";
8
+ import { SERVICE_COLORS } from "../../constants/theme.js";
8
9
  export const dynamoDBLevelAtom = atom({ kind: "tables" });
9
10
  export const dynamoDBBackStackAtom = atom([]);
10
11
  // Cache for table descriptions to avoid repeated AWS calls
@@ -303,7 +304,7 @@ export function createDynamoDBServiceAdapter(endpointUrl, region) {
303
304
  return {
304
305
  id: "dynamodb",
305
306
  label: "DynamoDB",
306
- hudColor: { bg: "green", fg: "white" },
307
+ hudColor: SERVICE_COLORS.dynamodb,
307
308
  getColumns,
308
309
  getRows,
309
310
  onSelect,
@@ -4,6 +4,7 @@ import { formatDate } from "./utils.js";
4
4
  import { createIamEditCapability } from "./capabilities/editCapability.js";
5
5
  import { createIamDetailCapability } from "./capabilities/detailCapability.js";
6
6
  import { createIamYankCapability } from "./capabilities/yankCapability.js";
7
+ import { SERVICE_COLORS } from "../../constants/theme.js";
7
8
  function getIamMeta(row) {
8
9
  return row.meta;
9
10
  }
@@ -241,7 +242,7 @@ export function createIamServiceAdapter() {
241
242
  return {
242
243
  id: "iam",
243
244
  label: "IAM",
244
- hudColor: { bg: "magenta", fg: "white" },
245
+ hudColor: SERVICE_COLORS.iam,
245
246
  getColumns,
246
247
  getRows,
247
248
  onSelect,
@@ -4,6 +4,7 @@ import { atom } from "jotai";
4
4
  import { getDefaultStore } from "jotai";
5
5
  import { createRoute53DetailCapability } from "./capabilities/detailCapability.js";
6
6
  import { createRoute53YankCapability } from "./capabilities/yankCapability.js";
7
+ import { SERVICE_COLORS } from "../../constants/theme.js";
7
8
  export const route53LevelAtom = atom({ kind: "zones" });
8
9
  export const route53BackStackAtom = atom([]);
9
10
  export function createRoute53ServiceAdapter(endpointUrl, region) {
@@ -158,7 +159,7 @@ export function createRoute53ServiceAdapter(endpointUrl, region) {
158
159
  return {
159
160
  id: "route53",
160
161
  label: "Route53",
161
- hudColor: { bg: "cyan", fg: "black" },
162
+ hudColor: SERVICE_COLORS.route53,
162
163
  getColumns,
163
164
  getRows,
164
165
  onSelect,
@@ -8,6 +8,7 @@ import { createS3EditCapability } from "./capabilities/editCapability.js";
8
8
  import { createS3DetailCapability } from "./capabilities/detailCapability.js";
9
9
  import { createS3YankCapability } from "./capabilities/yankCapability.js";
10
10
  import { createS3ActionCapability } from "./capabilities/actionCapability.js";
11
+ import { SERVICE_COLORS } from "../../constants/theme.js";
11
12
  export const s3LevelAtom = atom({ kind: "buckets" });
12
13
  export const s3BackStackAtom = atom([]);
13
14
  export function createS3ServiceAdapter(endpointUrl, region) {
@@ -132,7 +133,7 @@ export function createS3ServiceAdapter(endpointUrl, region) {
132
133
  return {
133
134
  id: "s3",
134
135
  label: "S3",
135
- hudColor: { bg: "red", fg: "white" },
136
+ hudColor: SERVICE_COLORS.s3,
136
137
  getColumns,
137
138
  getRows,
138
139
  onSelect,
@@ -7,6 +7,7 @@ import { createSecretsManagerDetailCapability } from "./capabilities/detailCapab
7
7
  import { createSecretsManagerYankCapability } from "./capabilities/yankCapability.js";
8
8
  import { createSecretsManagerActionCapability } from "./capabilities/actionCapability.js";
9
9
  import { createSecretsManagerEditCapability } from "./capabilities/editCapability.js";
10
+ import { SERVICE_COLORS } from "../../constants/theme.js";
10
11
  export const secretLevelAtom = atom({ kind: "secrets" });
11
12
  export const secretBackStackAtom = atom([]);
12
13
  function tryParseFields(secretString) {
@@ -166,7 +167,7 @@ export function createSecretsManagerServiceAdapter(endpointUrl, region) {
166
167
  return {
167
168
  id: "secretsmanager",
168
169
  label: "Secrets Manager",
169
- hudColor: { bg: "blue", fg: "white" },
170
+ hudColor: SERVICE_COLORS.secretsmanager,
170
171
  getColumns,
171
172
  getRows,
172
173
  onSelect,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a9s/cli",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "k9s-style TUI navigator for AWS services",
5
5
  "keywords": [
6
6
  "aws",
@@ -58,6 +58,7 @@
58
58
  "open": "^11.0.0",
59
59
  "open-editor": "^6.0.0",
60
60
  "react": "^19.2.4",
61
+ "yaml": "^2.8.2",
61
62
  "zod": "^4.3.6"
62
63
  },
63
64
  "devDependencies": {