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.
- package/CHANGELOG.md +42 -1
- package/README.md +6 -6
- package/docs/ai-skills.md +9 -4
- package/docs/bundled-docs.md +1 -0
- package/docs/cli-program.md +4 -4
- package/docs/config-schema.md +1 -4
- package/docs/developing.md +1 -1
- package/docs/install.md +160 -39
- package/docs/mcp.md +38 -11
- package/docs/templates/cursor/rules/cli-program.mdc +1 -1
- package/examples/config-app/program.ts +0 -3
- package/examples/consumer-app/README.md +1 -1
- package/examples/consumer-app/src/program.ts +5 -13
- package/examples/mcp-test.ts +14 -24
- package/index.d.ts +60 -5
- package/package.json +1 -1
- package/src/builtins/builtins.test.ts +20 -4
- package/src/builtins/config.test.ts +31 -25
- package/src/builtins/config.ts +4 -3
- package/src/builtins/install.ts +50 -57
- package/src/builtins/mcp.ts +1 -1
- package/src/capabilities.ts +4 -4
- package/src/cli.ts +2 -0
- package/src/config/bootstrap.ts +167 -66
- package/src/config/context.test.ts +22 -36
- package/src/config/context.ts +5 -4
- package/src/config/file.test.ts +66 -56
- package/src/config/file.ts +33 -25
- package/src/config/resolve.test.ts +25 -1
- package/src/config/resolve.ts +43 -8
- package/src/config.integration.test.ts +17 -10
- package/src/docs/api-guide.test.ts +1 -1
- package/src/docs/docs.test.ts +1 -1
- package/src/docs/mcp-guide.ts +15 -4
- package/src/docs/mcp-resources.test.ts +63 -0
- package/src/docs/mcp-resources.ts +68 -0
- package/src/hidden-mcpb.test.ts +41 -1
- package/src/index.ts +4 -0
- package/src/install/{binary.ts → app.ts} +13 -13
- package/src/install/bootstrap.ts +22 -0
- package/src/install/detect-installed.ts +2 -97
- package/src/install/index.ts +187 -110
- package/src/install/install-validate.test.ts +61 -0
- package/src/install/install.test.ts +138 -42
- package/src/install/mcp-openclaw.test.ts +40 -0
- package/src/install/mcp-openclaw.ts +106 -0
- package/src/install/normalize.ts +35 -0
- package/src/install/paths.ts +27 -13
- package/src/install/plan.ts +30 -259
- package/src/install/shell.ts +2 -2
- package/src/install/status.test.ts +85 -0
- package/src/install/status.ts +22 -9
- package/src/install/target-base.ts +93 -0
- package/src/install/target-detect.ts +20 -0
- package/src/install/target-effective.ts +131 -0
- package/src/install/target-mcp-cli.ts +149 -0
- package/src/install/target-mcp-json.ts +130 -0
- package/src/install/target-plan-build.ts +67 -0
- package/src/install/target-registry.ts +57 -0
- package/src/install/target-scope.ts +266 -0
- package/src/install/target-skill.ts +104 -0
- package/src/install/target-types.ts +145 -0
- package/src/install/targets/app.ts +69 -0
- package/src/install/targets/chatgpt-mcp.ts +12 -0
- package/src/install/targets/claude-code-mcp.ts +15 -0
- package/src/install/targets/claude-desktop-mcp.ts +12 -0
- package/src/install/targets/claude-skill.ts +16 -0
- package/src/install/targets/codex-mcp.ts +25 -0
- package/src/install/targets/codex-skill.ts +14 -0
- package/src/install/targets/completions.ts +133 -0
- package/src/install/targets/configure.ts +59 -0
- package/src/install/targets/cursor-mcp.ts +15 -0
- package/src/install/targets/cursor-skill.ts +16 -0
- package/src/install/targets/index.ts +53 -0
- package/src/install/targets/openclaw-mcp.ts +25 -0
- package/src/install/targets/openclaw-skill.ts +17 -0
- package/src/install/targets/opencode-mcp.ts +101 -0
- package/src/install/targets/opencode-skill.ts +15 -0
- package/src/install/targets.test.ts +136 -0
- package/src/install/uninstall.ts +16 -152
- package/src/install/update.test.ts +17 -2
- package/src/install/update.ts +2 -5
- package/src/invoke.test.ts +7 -1
- package/src/mcp/bundle.ts +16 -4
- package/src/mcp/claude.test.ts +23 -4
- package/src/mcp/claude.ts +18 -13
- package/src/mcp/tools.ts +4 -1
- package/src/mcp/zip.test.ts +17 -0
- package/src/mcp/zip.ts +62 -9
- package/src/mcp.integration.test.ts +18 -1
- package/src/parse.test.ts +57 -4
- package/src/paths/host.ts +11 -11
- package/src/paths/remove-empty-dir.ts +13 -0
- package/src/skill/generate.ts +89 -5
- package/src/skill/hint.ts +5 -0
- package/src/skill/install.ts +33 -6
- package/src/skill/naming.ts +28 -0
- package/src/types.ts +65 -4
- package/src/validate.ts +79 -4
package/src/config/bootstrap.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/*
|
|
2
|
-
|
|
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 {
|
|
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
|
-
/**
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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 ?
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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(
|
|
168
|
+
process.stderr.write(` Value: `);
|
|
103
169
|
}
|
|
104
|
-
const input = readPromptLine(
|
|
170
|
+
const input = readPromptLine(sensitive);
|
|
105
171
|
if (input.length === 0 && hasCurrent) {
|
|
106
|
-
return current;
|
|
172
|
+
return { value: current };
|
|
107
173
|
}
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
137
|
-
export function runInstallConfigure(
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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 =
|
|
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(
|
|
298
|
+
fileData = readAppConfigFileRaw(resolveAppConfigPath(program));
|
|
198
299
|
}
|
|
199
|
-
const
|
|
200
|
-
|
|
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:
|
|
312
|
+
return { path, exists: appConfigInstalled(program), required };
|
|
211
313
|
}
|
|
212
314
|
|
|
213
|
-
/**
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
|
30
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
});
|
package/src/config/context.ts
CHANGED
|
@@ -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 (
|
|
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
|
}
|