argsbarg 4.0.3 → 4.1.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +42 -1
  2. package/README.md +6 -6
  3. package/docs/ai-skills.md +9 -4
  4. package/docs/bundled-docs.md +1 -0
  5. package/docs/cli-program.md +4 -4
  6. package/docs/config-schema.md +1 -4
  7. package/docs/developing.md +1 -1
  8. package/docs/install.md +160 -39
  9. package/docs/mcp.md +38 -11
  10. package/docs/templates/cursor/rules/cli-program.mdc +1 -1
  11. package/examples/config-app/program.ts +0 -3
  12. package/examples/consumer-app/README.md +1 -1
  13. package/examples/consumer-app/src/program.ts +5 -13
  14. package/examples/mcp-test.ts +14 -24
  15. package/index.d.ts +60 -5
  16. package/package.json +1 -1
  17. package/src/builtins/builtins.test.ts +20 -4
  18. package/src/builtins/config.test.ts +31 -25
  19. package/src/builtins/config.ts +4 -3
  20. package/src/builtins/install.ts +50 -57
  21. package/src/builtins/mcp.ts +1 -1
  22. package/src/capabilities.ts +4 -4
  23. package/src/cli.ts +2 -0
  24. package/src/config/bootstrap.ts +167 -66
  25. package/src/config/context.test.ts +22 -36
  26. package/src/config/context.ts +5 -4
  27. package/src/config/file.test.ts +66 -56
  28. package/src/config/file.ts +33 -25
  29. package/src/config/resolve.test.ts +25 -1
  30. package/src/config/resolve.ts +43 -8
  31. package/src/config.integration.test.ts +17 -10
  32. package/src/docs/api-guide.test.ts +1 -1
  33. package/src/docs/docs.test.ts +1 -1
  34. package/src/docs/mcp-guide.ts +15 -4
  35. package/src/docs/mcp-resources.test.ts +63 -0
  36. package/src/docs/mcp-resources.ts +68 -0
  37. package/src/hidden-mcpb.test.ts +41 -1
  38. package/src/index.ts +4 -0
  39. package/src/install/{binary.ts → app.ts} +13 -13
  40. package/src/install/bootstrap.ts +22 -0
  41. package/src/install/detect-installed.ts +2 -97
  42. package/src/install/index.ts +187 -110
  43. package/src/install/install-validate.test.ts +61 -0
  44. package/src/install/install.test.ts +138 -42
  45. package/src/install/mcp-openclaw.test.ts +40 -0
  46. package/src/install/mcp-openclaw.ts +106 -0
  47. package/src/install/normalize.ts +35 -0
  48. package/src/install/paths.ts +27 -13
  49. package/src/install/plan.ts +30 -259
  50. package/src/install/shell.ts +2 -2
  51. package/src/install/status.test.ts +85 -0
  52. package/src/install/status.ts +22 -9
  53. package/src/install/target-base.ts +93 -0
  54. package/src/install/target-detect.ts +20 -0
  55. package/src/install/target-effective.ts +131 -0
  56. package/src/install/target-mcp-cli.ts +149 -0
  57. package/src/install/target-mcp-json.ts +130 -0
  58. package/src/install/target-plan-build.ts +67 -0
  59. package/src/install/target-registry.ts +57 -0
  60. package/src/install/target-scope.ts +266 -0
  61. package/src/install/target-skill.ts +104 -0
  62. package/src/install/target-types.ts +145 -0
  63. package/src/install/targets/app.ts +69 -0
  64. package/src/install/targets/chatgpt-mcp.ts +12 -0
  65. package/src/install/targets/claude-code-mcp.ts +15 -0
  66. package/src/install/targets/claude-desktop-mcp.ts +12 -0
  67. package/src/install/targets/claude-skill.ts +16 -0
  68. package/src/install/targets/codex-mcp.ts +25 -0
  69. package/src/install/targets/codex-skill.ts +14 -0
  70. package/src/install/targets/completions.ts +133 -0
  71. package/src/install/targets/configure.ts +59 -0
  72. package/src/install/targets/cursor-mcp.ts +15 -0
  73. package/src/install/targets/cursor-skill.ts +16 -0
  74. package/src/install/targets/index.ts +53 -0
  75. package/src/install/targets/openclaw-mcp.ts +25 -0
  76. package/src/install/targets/openclaw-skill.ts +17 -0
  77. package/src/install/targets/opencode-mcp.ts +101 -0
  78. package/src/install/targets/opencode-skill.ts +15 -0
  79. package/src/install/targets.test.ts +136 -0
  80. package/src/install/uninstall.ts +16 -152
  81. package/src/install/update.test.ts +17 -2
  82. package/src/install/update.ts +2 -5
  83. package/src/invoke.test.ts +7 -1
  84. package/src/mcp/bundle.ts +16 -4
  85. package/src/mcp/claude.test.ts +23 -4
  86. package/src/mcp/claude.ts +18 -13
  87. package/src/mcp/tools.ts +4 -1
  88. package/src/mcp/zip.test.ts +17 -0
  89. package/src/mcp/zip.ts +62 -9
  90. package/src/mcp.integration.test.ts +18 -1
  91. package/src/parse.test.ts +57 -4
  92. package/src/paths/host.ts +11 -11
  93. package/src/paths/remove-empty-dir.ts +13 -0
  94. package/src/skill/generate.ts +89 -5
  95. package/src/skill/hint.ts +5 -0
  96. package/src/skill/install.ts +33 -6
  97. package/src/skill/naming.ts +28 -0
  98. package/src/types.ts +65 -4
  99. package/src/validate.ts +79 -4
@@ -1,8 +1,9 @@
1
1
  /*
2
- Bootstrap: load config, validate, resolve, export, TTY prompts, install --configure.
2
+ Loads the app config file, runs setup prompts when needed, and checks that required
3
+ settings are present before the CLI or MCP server handles a request.
3
4
  */
4
5
 
5
- import { existsSync, readSync } from "node:fs";
6
+ import { readSync } from "node:fs";
6
7
  import type { CliAppConfigEntry, CliProgram } from "../types.ts";
7
8
  import {
8
9
  configEntryRequired,
@@ -11,6 +12,7 @@ import {
11
12
  jsonSchemaRequiredKeys,
12
13
  } from "./entry.ts";
13
14
  import {
15
+ appConfigInstalled,
14
16
  displayAppConfigPath,
15
17
  readAppConfigFile,
16
18
  readAppConfigFileRaw,
@@ -19,6 +21,7 @@ import {
19
21
  } from "./file.ts";
20
22
  import type { ResolvedConfig } from "./resolve.ts";
21
23
  import {
24
+ captureMappedHostEnv,
22
25
  exportConfigToEnv,
23
26
  formatMissingConfigMessage,
24
27
  missingRequiredConfig,
@@ -27,87 +30,171 @@ import {
27
30
  } from "./resolve.ts";
28
31
  import { effectiveJsonSchema } from "./schema.ts";
29
32
 
33
+ export { displayAppConfigPath } from "./file.ts";
34
+
35
+ /** Tells ensureAppConfig whether to prompt the user and how strictly to require settings. */
30
36
  export interface EnsureAppConfigOpts {
37
+ /** In a terminal, ask for any required settings that are still empty (or run the full configure wizard). */
31
38
  interactive: boolean;
39
+ /** Stop the program with an error if required settings are still missing after loading. */
32
40
  exitOnMissing: boolean;
41
+ /** With `interactive`, walk through every setting instead of only the missing required ones. */
33
42
  configure?: boolean;
34
43
  }
35
44
 
45
+ /** Config as read from disk, plus the final values after env vars and defaults are applied. */
36
46
  export interface ConfigBootstrapResult {
47
+ /** Key/value pairs stored in the config file. */
37
48
  fileData: Record<string, unknown>;
49
+ /** Effective values the app will use (file, defaults, and shell env combined). */
38
50
  resolved: ResolvedConfig;
39
51
  }
40
52
 
41
- /** Load, validate, resolve, and export config. */
53
+ /** Read the config file, merge env overrides, and export mapped values into `process.env`. */
42
54
  export function bootstrapAppConfig(
43
55
  program: CliProgram,
44
56
  opts: { validateFile: boolean },
45
57
  ): ConfigBootstrapResult {
46
- const path = resolveAppConfigPath(program);
47
- const fileData = opts.validateFile ? readAppConfigFile(program) : readAppConfigFileRaw(path);
48
- const resolved = resolveAppConfig(program, fileData);
49
- exportConfigToEnv(program, resolved);
58
+ const fileData = opts.validateFile
59
+ ? readAppConfigFile(program)
60
+ : readAppConfigFileRaw(resolveAppConfigPath(program));
61
+ const hostEnv = captureMappedHostEnv(program);
62
+ const resolved = resolveAppConfig(program, fileData, hostEnv);
63
+ exportConfigToEnv(program, resolved, hostEnv);
50
64
  return { fileData, resolved };
51
65
  }
52
66
 
53
- function readPromptLine(mask: boolean): string {
54
- if (!mask) {
55
- const buf = Buffer.alloc(4096);
56
- const n = readSync(0, buf, { length: 4096 });
57
- return buf.toString("utf8", 0, n).replace(/\r?\n$/, "");
58
- }
59
- let result = "";
60
- const buf = Buffer.alloc(1);
61
- while (true) {
62
- const n = readSync(0, buf, { length: 1 });
63
- if (n <= 0) {
64
- break;
65
- }
66
- const byte = buf[0];
67
- if (byte === undefined) {
68
- continue;
67
+ /** Read a line from the terminal without showing what the user types (for tokens and passwords). */
68
+ function readSensitiveLine(): string {
69
+ const stdin = process.stdin;
70
+ const canRaw = stdin.isTTY && typeof stdin.setRawMode === "function";
71
+ const wasRaw = canRaw && stdin.isRaw;
72
+ if (canRaw) {
73
+ try {
74
+ stdin.setRawMode(true);
75
+ } catch {
76
+ // Best-effort: read still works if raw mode is unavailable.
69
77
  }
70
- if (byte === 10 || byte === 13) {
71
- break;
78
+ }
79
+ try {
80
+ let result = "";
81
+ const buf = Buffer.alloc(1);
82
+ while (true) {
83
+ const n = readSync(0, buf, { length: 1 });
84
+ if (n <= 0) {
85
+ break;
86
+ }
87
+ const byte = buf[0];
88
+ if (byte === 3) {
89
+ // Raw mode delivers Ctrl+C as ETX instead of SIGINT.
90
+ process.stderr.write("\n");
91
+ process.exit(130);
92
+ }
93
+ if (byte === 4) {
94
+ break;
95
+ }
96
+ if (byte === 10 || byte === 13) {
97
+ break;
98
+ }
99
+ if (byte === 127 || byte === 8) {
100
+ if (result.length > 0) {
101
+ result = result.slice(0, -1);
102
+ process.stderr.write("\b \b");
103
+ }
104
+ continue;
105
+ }
106
+ result += String.fromCharCode(byte);
107
+ process.stderr.write("*");
72
108
  }
73
- if (byte === 127 || byte === 8) {
74
- result = result.slice(0, -1);
75
- continue;
109
+ process.stderr.write("\n");
110
+ return result;
111
+ } finally {
112
+ if (canRaw) {
113
+ try {
114
+ stdin.setRawMode(!!wasRaw);
115
+ } catch {
116
+ // Ignore restore failures.
117
+ }
76
118
  }
77
- result += String.fromCharCode(byte);
78
- process.stderr.write("*");
79
119
  }
80
- process.stderr.write("\n");
81
- return result;
82
120
  }
83
121
 
122
+ /** Read one line of user input; sensitive settings use a hidden prompt. */
123
+ function readPromptLine(mask: boolean): string {
124
+ if (mask) {
125
+ return readSensitiveLine();
126
+ }
127
+ const buf = Buffer.alloc(4096);
128
+ const n = readSync(0, buf, { length: 4096 });
129
+ return buf.toString("utf8", 0, n).replace(/\r?\n$/, "");
130
+ }
131
+
132
+ /** Whether this setting already has a non-empty value in the user's shell environment. */
133
+ function resolvedFromEnv(
134
+ entry: CliAppConfigEntry,
135
+ hostEnv: Record<string, string | undefined>,
136
+ ): boolean {
137
+ if (!entry.env) {
138
+ return false;
139
+ }
140
+ const val = hostEnv[entry.env];
141
+ return val !== undefined && val.length > 0;
142
+ }
143
+
144
+ /** Ask the user for one setting and return what they chose (or nothing if they skipped it). */
84
145
  function promptConfigKey(
85
146
  key: string,
86
147
  entry: CliAppConfigEntry,
87
148
  current: unknown,
88
149
  configure: boolean,
89
150
  jsonSchemaRequired: Set<string> | undefined,
90
- ): unknown {
91
- const title = entry.title ?? defaultConfigEntryTitle(key);
151
+ hostEnv: Record<string, string | undefined>,
152
+ ): { value: unknown } {
153
+ const baseTitle = entry.title ?? defaultConfigEntryTitle(key);
154
+ const titleWithEnv = entry.env ? `${baseTitle} (${entry.env})` : baseTitle;
92
155
  const required = configEntryRequired(key, entry, jsonSchemaRequired);
93
- const heading = required || !configure ? title : `${title} (optional)`;
156
+ const heading = required || !configure ? titleWithEnv : `${titleWithEnv} (optional)`;
94
157
  process.stderr.write(`${heading}\n`);
95
158
  process.stderr.write(` ${entry.description}\n`);
96
159
  const hasCurrent = current !== undefined && current !== null && String(current).length > 0;
160
+ const sensitive = configEntrySensitive(key, entry);
97
161
  if (hasCurrent) {
98
- const sensitive = configEntrySensitive(key, entry);
99
- process.stderr.write(`Current: ${sensitive ? "REDACTED" : stringifyConfigValue(current)}\n`);
100
- process.stderr.write("Value (Enter to keep): ");
162
+ process.stderr.write(` Current: ${sensitive ? "REDACTED" : stringifyConfigValue(current)}\n`);
163
+ const acceptPrompt = resolvedFromEnv(entry, hostEnv)
164
+ ? ` Value (Enter to copy from env): `
165
+ : ` Value (Enter to keep): `;
166
+ process.stderr.write(acceptPrompt);
101
167
  } else {
102
- process.stderr.write("Value: ");
168
+ process.stderr.write(` Value: `);
103
169
  }
104
- const input = readPromptLine(hasCurrent && configEntrySensitive(key, entry));
170
+ const input = readPromptLine(sensitive);
105
171
  if (input.length === 0 && hasCurrent) {
106
- return current;
172
+ return { value: current };
107
173
  }
108
- return input.length > 0 ? input : undefined;
174
+ if (input.length > 0) {
175
+ return { value: input };
176
+ }
177
+ return { value: undefined };
178
+ }
179
+
180
+ /** Options for the interactive `install --configure` wizard. */
181
+ export interface RunInstallConfigureOpts {
182
+ /** Where the wizard was started from (standalone command vs right after install). */
183
+ context?: "standalone" | "after-install";
184
+ }
185
+
186
+ /** Print the "Configuration Setup" heading before the configure prompts. */
187
+ function writeConfigureSetupHeading(): void {
188
+ process.stderr.write("\nConfiguration Setup\n\n");
189
+ }
190
+
191
+ /** Whether this app has any settings worth prompting during configure. */
192
+ function shouldShowConfigureSetupHeading(program: CliProgram): boolean {
193
+ if (!program.appConfig) return false;
194
+ return Object.keys(program.appConfig.entries).length > 0;
109
195
  }
110
196
 
197
+ /** Ask the user for each required setting that is still empty; returns updates to save to the config file. */
111
198
  function promptMissingRequired(program: CliProgram): Record<string, unknown> {
112
199
  const appConfig = program.appConfig;
113
200
  const updates: Record<string, unknown> = {};
@@ -116,7 +203,9 @@ function promptMissingRequired(program: CliProgram): Record<string, unknown> {
116
203
  }
117
204
  const jsonSchema = effectiveJsonSchema(program);
118
205
  const fromSchema = jsonSchema ? jsonSchemaRequiredKeys(jsonSchema) : undefined;
206
+ const hostEnv = captureMappedHostEnv(program);
119
207
  const { resolved } = bootstrapAppConfig(program, { validateFile: false });
208
+ let headingWritten = false;
120
209
  for (const [key, entry] of Object.entries(appConfig.entries)) {
121
210
  if (!configEntryRequired(key, entry, fromSchema)) {
122
211
  continue;
@@ -125,7 +214,11 @@ function promptMissingRequired(program: CliProgram): Record<string, unknown> {
125
214
  if (current !== undefined && current !== null && String(current).length > 0) {
126
215
  continue;
127
216
  }
128
- const value = promptConfigKey(key, entry, undefined, false, fromSchema);
217
+ if (!headingWritten) {
218
+ writeConfigureSetupHeading();
219
+ headingWritten = true;
220
+ }
221
+ const { value } = promptConfigKey(key, entry, undefined, false, fromSchema, hostEnv);
129
222
  if (value !== undefined && String(value).length > 0) {
130
223
  updates[key] = value;
131
224
  }
@@ -133,8 +226,11 @@ function promptMissingRequired(program: CliProgram): Record<string, unknown> {
133
226
  return updates;
134
227
  }
135
228
 
136
- /** Interactive `install --configure`. */
137
- export function runInstallConfigure(program: CliProgram): { path: string; changed: boolean } {
229
+ /** Run the full interactive configure wizard (`install --configure`). */
230
+ export function runInstallConfigure(
231
+ program: CliProgram,
232
+ _opts: RunInstallConfigureOpts = {},
233
+ ): { path: string; changed: boolean } {
138
234
  if (!program.appConfig) {
139
235
  throw new Error("install --configure requires program.appConfig on the program root.");
140
236
  }
@@ -151,16 +247,21 @@ export function runInstallConfigure(program: CliProgram): { path: string; change
151
247
 
152
248
  const path = resolveAppConfigPath(program);
153
249
  const existing = readAppConfigFileRaw(path);
250
+ const hostEnv = captureMappedHostEnv(program);
154
251
  const jsonSchema = effectiveJsonSchema(program);
155
252
  const fromSchema = jsonSchema ? jsonSchemaRequiredKeys(jsonSchema) : undefined;
156
- const { resolved } = bootstrapAppConfig(program, { validateFile: false });
253
+ const resolved = resolveAppConfig(program, existing, hostEnv);
157
254
  const next: Record<string, unknown> = { ...existing };
158
255
  let changed = false;
159
256
 
257
+ if (shouldShowConfigureSetupHeading(program)) {
258
+ writeConfigureSetupHeading();
259
+ }
260
+
160
261
  for (const [key, entry] of Object.entries(program.appConfig.entries)) {
161
- const current = resolved[key];
162
262
  const before = next[key];
163
- const value = promptConfigKey(key, entry, current, true, fromSchema);
263
+ const current = resolved[key];
264
+ const { value } = promptConfigKey(key, entry, current, true, fromSchema, hostEnv);
164
265
  if (value !== undefined && String(value).length > 0) {
165
266
  if (JSON.stringify(value) !== JSON.stringify(before)) {
166
267
  changed = true;
@@ -171,14 +272,14 @@ export function runInstallConfigure(program: CliProgram): { path: string; change
171
272
 
172
273
  if (changed) {
173
274
  writeAppConfigFile(program, next);
174
- const updated = resolveAppConfig(program, next);
175
- exportConfigToEnv(program, updated);
275
+ const updated = resolveAppConfig(program, next, hostEnv);
276
+ exportConfigToEnv(program, updated, hostEnv);
176
277
  return { path, changed: true };
177
278
  }
178
279
  return { path, changed: false };
179
280
  }
180
281
 
181
- /** Config status for install --status (values never included). */
282
+ /** Summary for `install --status`: config path, whether the file exists, and which required settings are set (never their values). */
182
283
  export function appConfigStatus(program: CliProgram):
183
284
  | {
184
285
  path: string;
@@ -189,15 +290,16 @@ export function appConfigStatus(program: CliProgram):
189
290
  if (!program.appConfig) {
190
291
  return undefined;
191
292
  }
192
- const path = resolveAppConfigPath(program);
293
+ const path = displayAppConfigPath(program);
193
294
  let fileData: Record<string, unknown> = {};
194
295
  try {
195
296
  fileData = readAppConfigFile(program);
196
297
  } catch {
197
- fileData = readAppConfigFileRaw(path);
298
+ fileData = readAppConfigFileRaw(resolveAppConfigPath(program));
198
299
  }
199
- const resolved = resolveAppConfig(program, fileData);
200
- exportConfigToEnv(program, resolved);
300
+ const hostEnv = captureMappedHostEnv(program);
301
+ const resolved = resolveAppConfig(program, fileData, hostEnv);
302
+ exportConfigToEnv(program, resolved, hostEnv);
201
303
  const jsonSchema = effectiveJsonSchema(program);
202
304
  const fromSchema = jsonSchema ? jsonSchemaRequiredKeys(jsonSchema) : undefined;
203
305
  const required = Object.entries(program.appConfig.entries)
@@ -207,10 +309,10 @@ export function appConfigStatus(program: CliProgram):
207
309
  set:
208
310
  resolved[key] !== undefined && resolved[key] !== null && String(resolved[key]).length > 0,
209
311
  }));
210
- return { path, exists: existsSync(path), required };
312
+ return { path, exists: appConfigInstalled(program), required };
211
313
  }
212
314
 
213
- /** Loads config, optionally prompts, and enforces required keys. */
315
+ /** Load config at startup, optionally prompt the user, and fail if required settings are still missing. */
214
316
  export function ensureAppConfig(
215
317
  program: CliProgram,
216
318
  opts: EnsureAppConfigOpts,
@@ -228,15 +330,16 @@ export function ensureAppConfig(
228
330
  process.exit(1);
229
331
  }
230
332
 
231
- let resolved = resolveAppConfig(program, fileData);
232
- exportConfigToEnv(program, resolved);
333
+ const hostEnv = captureMappedHostEnv(program);
334
+ let resolved = resolveAppConfig(program, fileData, hostEnv);
335
+ exportConfigToEnv(program, resolved, hostEnv);
233
336
 
234
337
  if (opts.interactive && process.stdin.isTTY) {
235
338
  if (opts.configure) {
236
- runInstallConfigure(program);
339
+ runInstallConfigure(program, { context: "standalone" });
237
340
  fileData = readAppConfigFileRaw(resolveAppConfigPath(program));
238
- resolved = resolveAppConfig(program, fileData);
239
- exportConfigToEnv(program, resolved);
341
+ resolved = resolveAppConfig(program, fileData, hostEnv);
342
+ exportConfigToEnv(program, resolved, hostEnv);
240
343
  return { fileData, resolved };
241
344
  }
242
345
  const updates = promptMissingRequired(program);
@@ -244,8 +347,8 @@ export function ensureAppConfig(
244
347
  const merged = { ...fileData, ...updates };
245
348
  writeAppConfigFile(program, merged);
246
349
  fileData = merged;
247
- resolved = resolveAppConfig(program, fileData);
248
- exportConfigToEnv(program, resolved);
350
+ resolved = resolveAppConfig(program, fileData, hostEnv);
351
+ exportConfigToEnv(program, resolved, hostEnv);
249
352
  }
250
353
  }
251
354
 
@@ -261,5 +364,3 @@ export function ensureAppConfig(
261
364
 
262
365
  return { fileData, resolved };
263
366
  }
264
-
265
- export { displayAppConfigPath };
@@ -4,33 +4,31 @@ import { tmpdir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
5
  import type { CliProgram } from "../types.ts";
6
6
  import { createAppConfigSnapshot } from "./context.ts";
7
- import { resolveAppConfigDir } from "./file.ts";
7
+ import { resolveAppConfigDir, resolveAppConfigPath } from "./file.ts";
8
8
  import { resolveAppConfig } from "./resolve.ts";
9
9
 
10
- function configProgram(configPath: string): CliProgram {
11
- return {
12
- key: "ctx-test",
13
- version: "1.0.0",
14
- description: "Context test.",
15
- appConfig: {
16
- path: configPath,
17
- entries: {
18
- apiToken: { description: "Token.", env: "API_TOKEN" },
19
- note: { description: "Note.", required: false },
20
- },
10
+ const program: CliProgram = {
11
+ key: "ctx-test",
12
+ version: "1.0.0",
13
+ description: "Context test.",
14
+ appConfig: {
15
+ entries: {
16
+ apiToken: { description: "Token.", env: "API_TOKEN" },
17
+ note: { description: "Note.", required: false },
21
18
  },
22
- handler: () => {},
23
- };
24
- }
19
+ },
20
+ handler: () => {},
21
+ };
25
22
 
26
23
  describe("config/context", () => {
27
24
  test("AppConfigSnapshot get, require, read, set", () => {
28
25
  const dir = mkdtempSync(join(tmpdir(), "ctx-test-"));
29
- const path = join(dir, "config");
30
- const program = configProgram(path);
26
+ const prevHome = process.env.HOME;
27
+ process.env.HOME = dir;
31
28
  const prevToken = process.env.API_TOKEN;
32
29
  delete process.env.API_TOKEN;
33
30
  try {
31
+ const path = resolveAppConfigPath(program);
34
32
  const fileData = { apiToken: "tok", note: "hello" };
35
33
  const resolved = resolveAppConfig(program, fileData);
36
34
  const ctx = createAppConfigSnapshot(program, fileData, resolved);
@@ -44,6 +42,8 @@ describe("config/context", () => {
44
42
  expect(ctx.get("note")).toBe("updated");
45
43
  expect(ctx.read().note).toBe("updated");
46
44
  } finally {
45
+ if (prevHome === undefined) delete process.env.HOME;
46
+ else process.env.HOME = prevHome;
47
47
  if (prevToken === undefined) delete process.env.API_TOKEN;
48
48
  else process.env.API_TOKEN = prevToken;
49
49
  rmSync(dir, { recursive: true, force: true });
@@ -51,39 +51,25 @@ describe("config/context", () => {
51
51
  });
52
52
 
53
53
  test("EmptyAppConfigSnapshot when program.appConfig unset", () => {
54
- const program: CliProgram = {
54
+ const programWithoutConfig: CliProgram = {
55
55
  key: "x",
56
56
  version: "1.0.0",
57
57
  description: "No config.",
58
58
  handler: () => {},
59
59
  };
60
- const empty = createAppConfigSnapshot(program, {}, {});
60
+ const empty = createAppConfigSnapshot(programWithoutConfig, {}, {});
61
61
  expect(empty.get("any")).toBeUndefined();
62
62
  expect(() => empty.set("any", "v")).toThrow(/program.appConfig is not set/);
63
63
  expect(empty.path).toContain("x");
64
- expect(empty.path.endsWith("/config") || empty.path.endsWith("\\config")).toBe(true);
64
+ expect(empty.path.endsWith("/config.json") || empty.path.endsWith("\\config.json")).toBe(true);
65
65
  expect(empty.dir).toBe(dirname(empty.path));
66
66
  });
67
67
 
68
- test("AppConfigSnapshot path uses OS default when program.appConfig.path omitted", () => {
69
- const program: CliProgram = {
70
- key: "ctx-test",
71
- version: "1.0.0",
72
- description: "Context test.",
73
- appConfig: {
74
- entries: { note: { description: "Note." } },
75
- },
76
- handler: () => {},
77
- };
68
+ test("AppConfigSnapshot path uses OS default from program key", () => {
78
69
  const ctx = createAppConfigSnapshot(program, {}, {});
79
70
  expect(ctx.path).toContain("ctx_test");
80
- expect(ctx.path.endsWith("/config") || ctx.path.endsWith("\\config")).toBe(true);
71
+ expect(ctx.path.endsWith("/config.json") || ctx.path.endsWith("\\config.json")).toBe(true);
81
72
  expect(ctx.dir).toBe(resolveAppConfigDir(program));
82
73
  expect(ctx.dir).toBe(dirname(ctx.path));
83
74
  });
84
-
85
- test("resolveAppConfigDir honors custom program.appConfig.path", () => {
86
- const program = configProgram("/tmp/custom/settings.json");
87
- expect(resolveAppConfigDir(program)).toBe("/tmp/custom");
88
- });
89
75
  });
@@ -5,7 +5,7 @@ Handler-facing resolved app config snapshot (ctx.appConfig).
5
5
  import type { CliProgram } from "../types.ts";
6
6
  import { resolveAppConfigDir, resolveAppConfigPath, writeAppConfigFile } from "./file.ts";
7
7
  import type { ResolvedConfig } from "./resolve.ts";
8
- import { exportConfigToEnv, resolveAppConfig } from "./resolve.ts";
8
+ import { captureMappedHostEnv, exportConfigToEnv, resolveAppConfig } from "./resolve.ts";
9
9
 
10
10
  /** Empty snapshot when program.appConfig is not set. */
11
11
  export class EmptyAppConfigSnapshot {
@@ -71,18 +71,19 @@ export class AppConfigSnapshot {
71
71
 
72
72
  set(key: string, value: unknown): void {
73
73
  this.assertEntryKey(key);
74
+ const hostEnv = captureMappedHostEnv(this.program);
74
75
  const next = { ...this.fileData, [key]: value };
75
76
  writeAppConfigFile(this.program, next);
76
77
  this.fileData = next;
77
- this.snapshot = resolveAppConfig(this.program, next);
78
- exportConfigToEnv(this.program, this.snapshot);
78
+ this.snapshot = resolveAppConfig(this.program, next, hostEnv);
79
+ exportConfigToEnv(this.program, this.snapshot, hostEnv);
79
80
  }
80
81
 
81
82
  read(): ResolvedConfig {
82
83
  return { ...this.snapshot };
83
84
  }
84
85
 
85
- /** Resolved absolute path to the app JSON config file (honors `program.appConfig.path` or OS default). */
86
+ /** Resolved absolute path to the app JSON config file (`~/.local/lib/<key>/config.json`). */
86
87
  get path(): string {
87
88
  return resolveAppConfigPath(this.program);
88
89
  }