configorama 1.1.0 → 1.2.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 +10 -1
- package/cli.js +14 -1
- package/package.json +9 -4
- package/plugins/onepassword/index.js +15 -4
- package/plugins/onepassword/normalize.js +13 -1
- package/src/errors.js +3 -0
- package/src/index.js +19 -0
- package/src/main.js +18 -29
- package/src/utils/parsing/detectFormat.js +70 -0
- package/src/utils/parsing/detectFormat.test.js +68 -0
- package/src/utils/parsing/parse.js +1 -35
- package/src/utils/setup/applyAnswers.js +41 -0
- package/src/utils/setup/setupEngine.js +85 -0
- package/src/utils/setup/stdoutRedirect.js +48 -0
- package/src/utils/setup/writeAnswers.js +50 -0
- package/src/utils/setup/writeDotenv.js +142 -0
- package/src/utils/ui/configWizard.js +20 -1
- package/types/src/capabilities.d.ts +8 -0
- package/types/src/capabilities.d.ts.map +1 -0
- package/types/src/display.d.ts +62 -0
- package/types/src/display.d.ts.map +1 -0
- package/types/src/errors.d.ts +20 -0
- package/types/src/errors.d.ts.map +1 -0
- package/types/src/index.d.ts +119 -0
- package/types/src/index.d.ts.map +1 -1
- package/types/src/main.d.ts +23 -16
- package/types/src/main.d.ts.map +1 -1
- package/types/src/metadata.d.ts +28 -0
- package/types/src/metadata.d.ts.map +1 -0
- package/types/src/parsers/dotenv.d.ts +6 -0
- package/types/src/parsers/dotenv.d.ts.map +1 -0
- package/types/src/parsers/esm.d.ts.map +1 -1
- package/types/src/parsers/typescript.d.ts.map +1 -1
- package/types/src/resolvers/valueFromCron.d.ts.map +1 -1
- package/types/src/resolvers/valueFromEval.d.ts.map +1 -1
- package/types/src/resolvers/valueFromFile.d.ts +4 -0
- package/types/src/resolvers/valueFromFile.d.ts.map +1 -1
- package/types/src/resolvers/valueFromGit.d.ts.map +1 -1
- package/types/src/resolvers/valueFromOptions.d.ts +1 -0
- package/types/src/resolvers/valueFromOptions.d.ts.map +1 -1
- package/types/src/sync.d.ts.map +1 -1
- package/types/src/utils/BoundedMap.d.ts +10 -0
- package/types/src/utils/BoundedMap.d.ts.map +1 -0
- package/types/src/utils/PromiseTracker.d.ts +1 -1
- package/types/src/utils/PromiseTracker.d.ts.map +1 -1
- package/types/src/utils/filters/filterArgs.d.ts +13 -0
- package/types/src/utils/filters/filterArgs.d.ts.map +1 -0
- package/types/src/utils/filters/oneOf.d.ts +7 -0
- package/types/src/utils/filters/oneOf.d.ts.map +1 -0
- package/types/src/utils/introspection/audit.d.ts +14 -0
- package/types/src/utils/introspection/audit.d.ts.map +1 -0
- package/types/src/utils/introspection/graph.d.ts +4 -0
- package/types/src/utils/introspection/graph.d.ts.map +1 -0
- package/types/src/utils/introspection/model.d.ts +43 -0
- package/types/src/utils/introspection/model.d.ts.map +1 -0
- package/types/src/utils/parsing/commentAnnotations.d.ts +18 -0
- package/types/src/utils/parsing/commentAnnotations.d.ts.map +1 -0
- package/types/src/utils/parsing/detectFormat.d.ts +17 -0
- package/types/src/utils/parsing/detectFormat.d.ts.map +1 -0
- package/types/src/utils/parsing/enrichMetadata.d.ts.map +1 -1
- package/types/src/utils/parsing/extractComment.d.ts +20 -0
- package/types/src/utils/parsing/extractComment.d.ts.map +1 -0
- package/types/src/utils/parsing/parse.d.ts.map +1 -1
- package/types/src/utils/parsing/preProcess.d.ts.map +1 -1
- package/types/src/utils/paths/fileType.d.ts +15 -0
- package/types/src/utils/paths/fileType.d.ts.map +1 -0
- package/types/src/utils/paths/findLineForKey.d.ts +9 -0
- package/types/src/utils/paths/findLineForKey.d.ts.map +1 -1
- package/types/src/utils/paths/ignorePaths.d.ts +5 -0
- package/types/src/utils/paths/ignorePaths.d.ts.map +1 -0
- package/types/src/utils/redaction/redact.d.ts +10 -0
- package/types/src/utils/redaction/redact.d.ts.map +1 -0
- package/types/src/utils/redaction/setupRedaction.d.ts +5 -0
- package/types/src/utils/redaction/setupRedaction.d.ts.map +1 -0
- package/types/src/utils/requirements/configRequirements.d.ts +70 -0
- package/types/src/utils/requirements/configRequirements.d.ts.map +1 -0
- package/types/src/utils/requirements/serializeRequirements.d.ts +63 -0
- package/types/src/utils/requirements/serializeRequirements.d.ts.map +1 -0
- package/types/src/utils/security/dotenvFileRefs.d.ts +13 -0
- package/types/src/utils/security/dotenvFileRefs.d.ts.map +1 -0
- package/types/src/utils/security/evalSafety.d.ts +17 -0
- package/types/src/utils/security/evalSafety.d.ts.map +1 -0
- package/types/src/utils/security/safetyPolicy.d.ts +15 -0
- package/types/src/utils/security/safetyPolicy.d.ts.map +1 -0
- package/types/src/utils/setup/applyAnswers.d.ts +34 -0
- package/types/src/utils/setup/applyAnswers.d.ts.map +1 -0
- package/types/src/utils/setup/setupEngine.d.ts +89 -0
- package/types/src/utils/setup/setupEngine.d.ts.map +1 -0
- package/types/src/utils/setup/stdoutRedirect.d.ts +10 -0
- package/types/src/utils/setup/stdoutRedirect.d.ts.map +1 -0
- package/types/src/utils/setup/writeAnswers.d.ts +17 -0
- package/types/src/utils/setup/writeAnswers.d.ts.map +1 -0
- package/types/src/utils/setup/writeDotenv.d.ts +27 -0
- package/types/src/utils/setup/writeDotenv.d.ts.map +1 -0
- package/types/src/utils/strings/didYouMean.d.ts +23 -0
- package/types/src/utils/strings/didYouMean.d.ts.map +1 -0
- package/types/src/utils/strings/formatFunctionArgs.d.ts.map +1 -1
- package/types/src/utils/strings/replaceAll.d.ts.map +1 -1
- package/types/src/utils/strings/splitByComma.d.ts.map +1 -1
- package/types/src/utils/ui/configWizard.d.ts +51 -7
- package/types/src/utils/ui/configWizard.d.ts.map +1 -1
- package/types/src/utils/ui/createEditorLink.d.ts +7 -0
- package/types/src/utils/ui/createEditorLink.d.ts.map +1 -1
- package/types/src/utils/ui/promptDescriptors.d.ts +39 -0
- package/types/src/utils/ui/promptDescriptors.d.ts.map +1 -0
- package/types/src/utils/variables/cleanVariable.d.ts.map +1 -1
- package/types/src/utils/variables/getVariableType.d.ts.map +1 -1
- package/types/src/utils/variables/variableUtils.d.ts +1 -1
package/README.md
CHANGED
|
@@ -2304,7 +2304,16 @@ stage: ${opt:stage | help('Deployment stage, e.g. dev or prod')}
|
|
|
2304
2304
|
|
|
2305
2305
|
The [Variable Source Types](#variable-source-types) table describes how the wizard treats each source.
|
|
2306
2306
|
|
|
2307
|
-
|
|
2307
|
+
The wizard prints a summary of your answers with sensitive values redacted, applies them to the current resolution run, and never edits the source config file.
|
|
2308
|
+
|
|
2309
|
+
Programmatic access to the same engine returns the answers without applying them anywhere:
|
|
2310
|
+
|
|
2311
|
+
```javascript
|
|
2312
|
+
const result = await configorama.setup('config.yml')
|
|
2313
|
+
// { schemaVersion, configPath, requirements, answers, redactedAnswers }
|
|
2314
|
+
```
|
|
2315
|
+
|
|
2316
|
+
To apply answers to explicit targets - your current shell, a one-off command, a `.env.local`, or a JSON answers file - use [`configx setup`](./packages/configx/README.md#setup-wizard). Its safe write behavior (0600 permissions, managed-block merge, sensitive-value confirmation) is built on the same engine.
|
|
2308
2317
|
|
|
2309
2318
|
---
|
|
2310
2319
|
|
package/cli.js
CHANGED
|
@@ -61,7 +61,8 @@ Commands:
|
|
|
61
61
|
(default) Resolve <file> and print the result
|
|
62
62
|
inspect <file> Introspect a config without resolving it (full model)
|
|
63
63
|
--view requirements|audit|graph for a single slice
|
|
64
|
-
setup <file>
|
|
64
|
+
setup <file> Prompt for missing config values, then resolve with the
|
|
65
|
+
answers applied (summary redacts sensitive values)
|
|
65
66
|
capabilities Print the machine-readable CLI contract (JSON)
|
|
66
67
|
|
|
67
68
|
Options:
|
|
@@ -449,6 +450,17 @@ if (requirementsMode) {
|
|
|
449
450
|
// Process the configuration
|
|
450
451
|
const shouldRedactSetupStdout = options.setup && !argv.output && !argv.copy
|
|
451
452
|
let setupRequirementsForRedaction = []
|
|
453
|
+
|
|
454
|
+
// If stdin ends mid-wizard the pending prompt promise never settles, the event
|
|
455
|
+
// loop drains, and node would exit 0 as if setup succeeded. Fail closed instead.
|
|
456
|
+
const onSetupBeforeExit = () => {
|
|
457
|
+
process.stderr.write('configorama: setup cancelled (input ended before completion)\n')
|
|
458
|
+
process.exit(1)
|
|
459
|
+
}
|
|
460
|
+
if (options.setup) {
|
|
461
|
+
process.on('beforeExit', onSetupBeforeExit)
|
|
462
|
+
}
|
|
463
|
+
|
|
452
464
|
const configPromise = shouldRedactSetupStdout
|
|
453
465
|
? (() => {
|
|
454
466
|
const instance = new Configorama(inputFile, options)
|
|
@@ -461,6 +473,7 @@ const configPromise = shouldRedactSetupStdout
|
|
|
461
473
|
|
|
462
474
|
configPromise
|
|
463
475
|
.then((config) => {
|
|
476
|
+
process.removeListener('beforeExit', onSetupBeforeExit)
|
|
464
477
|
let outputConfig = shouldRedactSetupStdout
|
|
465
478
|
? redactConfigByRequirements(config, setupRequirementsForRedaction)
|
|
466
479
|
: config
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "configorama",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Variable support for configuration files",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
"types",
|
|
18
18
|
"index.d.ts",
|
|
19
19
|
"package.json",
|
|
20
|
-
"package-lock.json",
|
|
21
20
|
"README.md",
|
|
22
21
|
"!plugins/*/*.test.js",
|
|
23
22
|
"!plugins/*/example",
|
|
@@ -48,7 +47,10 @@
|
|
|
48
47
|
"publish": "git push origin && git push origin --tags",
|
|
49
48
|
"release:patch": "npm version patch && npm publish",
|
|
50
49
|
"release:minor": "npm version minor && npm publish",
|
|
51
|
-
"release:major": "npm version major && npm publish"
|
|
50
|
+
"release:major": "npm version major && npm publish",
|
|
51
|
+
"release": "lerna publish",
|
|
52
|
+
"version:all": "lerna version --no-push --no-git-tag-version",
|
|
53
|
+
"test:all": "pnpm -r test"
|
|
52
54
|
},
|
|
53
55
|
"author": "David Wells",
|
|
54
56
|
"license": "MIT",
|
|
@@ -82,6 +84,7 @@
|
|
|
82
84
|
"devDependencies": {
|
|
83
85
|
"@cdktf/hcl2json": "^0.21.0",
|
|
84
86
|
"@types/node": "^24.10.1",
|
|
87
|
+
"lerna": "^9.0.0",
|
|
85
88
|
"markdown-magic": "^4.8.0",
|
|
86
89
|
"tsx": "^4.7.0",
|
|
87
90
|
"typescript": "^5.8.3",
|
|
@@ -106,5 +109,7 @@
|
|
|
106
109
|
"ts-node": {
|
|
107
110
|
"optional": true
|
|
108
111
|
}
|
|
109
|
-
}
|
|
112
|
+
},
|
|
113
|
+
"packageManager": "pnpm@10.33.0",
|
|
114
|
+
"gitHead": "2ce70bfa849433ba74c60fa3c1cd3dba942b6556"
|
|
110
115
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* 1Password variable source for configorama
|
|
2
2
|
Resolves ${op:alias} and ${op(item)} references through the op CLI */
|
|
3
3
|
const { readSecretRef, getItem } = require('./op-cli')
|
|
4
|
-
const { validateAliasName, normalizeRefValue } = require('./normalize')
|
|
4
|
+
const { validateAliasName, normalizeRefValue, isItemId } = require('./normalize')
|
|
5
5
|
const { selectField, trySelectField } = require('./fields')
|
|
6
6
|
const { parseStructuredSecret, getKeyPath } = require('./parser')
|
|
7
7
|
|
|
@@ -173,10 +173,21 @@ function createOnePasswordResolver(options = {}) {
|
|
|
173
173
|
|
|
174
174
|
validateAliasName(alias)
|
|
175
175
|
const reference = aliasRefs[alias]
|
|
176
|
-
if (
|
|
177
|
-
|
|
176
|
+
if (reference) {
|
|
177
|
+
return { reference, keyPath, alias }
|
|
178
178
|
}
|
|
179
|
-
|
|
179
|
+
// Not a configured alias. A bare 1Password item ID (26-char base32) is
|
|
180
|
+
// unambiguous, so accept it as a direct item reference. Configured aliases
|
|
181
|
+
// always take precedence (checked above), so this only affects strings that
|
|
182
|
+
// would otherwise error as an unknown alias.
|
|
183
|
+
if (isItemId(alias)) {
|
|
184
|
+
return {
|
|
185
|
+
reference: { kind: 'item', item: alias, vault: undefined, section: undefined, field: undefined },
|
|
186
|
+
keyPath,
|
|
187
|
+
alias: undefined,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
throw new Error(`Unknown 1Password alias "${alias}". Configure refs.${alias}.`)
|
|
180
191
|
}
|
|
181
192
|
|
|
182
193
|
/**
|
|
@@ -2,8 +2,20 @@
|
|
|
2
2
|
Validates aliases, parses private item links, rejects public share links */
|
|
3
3
|
|
|
4
4
|
const ALIAS_PATTERN = /^[A-Za-z0-9_]+$/
|
|
5
|
+
// 1Password item and vault IDs are 26-character lowercase base32 strings.
|
|
6
|
+
const ITEM_ID_PATTERN = /^[a-z0-9]{26}$/
|
|
5
7
|
const PRIVATE_LINK_PREFIXES = ['https://start.1password.com/open/i', 'onepassword://open/i']
|
|
6
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Whether a string has the shape of a 1Password item ID (26-char base32).
|
|
11
|
+
* Used to let colon syntax accept a bare item ID where an alias would go.
|
|
12
|
+
* @param {string} value - Candidate string
|
|
13
|
+
* @returns {boolean} True when it looks like an item ID
|
|
14
|
+
*/
|
|
15
|
+
function isItemId(value) {
|
|
16
|
+
return ITEM_ID_PATTERN.test(value)
|
|
17
|
+
}
|
|
18
|
+
|
|
7
19
|
/**
|
|
8
20
|
* Validate an alias name from the refs config.
|
|
9
21
|
* Dots are reserved as the key path separator so they cannot appear in aliases.
|
|
@@ -142,4 +154,4 @@ function normalizeObjectRef(value) {
|
|
|
142
154
|
}
|
|
143
155
|
}
|
|
144
156
|
|
|
145
|
-
module.exports = { validateAliasName, normalizeRefValue, parsePrivateLink, isPrivateLink }
|
|
157
|
+
module.exports = { validateAliasName, normalizeRefValue, parsePrivateLink, isPrivateLink, isItemId }
|
package/src/errors.js
CHANGED
|
@@ -16,6 +16,9 @@ const ERROR_CODES = [
|
|
|
16
16
|
{ code: 'no_input_file', description: 'No config file was provided on the command line.' },
|
|
17
17
|
{ code: 'file_not_found', description: 'The provided config file path does not exist.' },
|
|
18
18
|
{ code: 'path_not_found', description: 'A jq-style extraction path matched nothing in the resolved config.' },
|
|
19
|
+
{ code: 'invalid_env_key', description: 'A setup write target received a key that is not a valid dotenv name.' },
|
|
20
|
+
{ code: 'target_file_exists', description: 'A setup write target already exists and neither --merge nor --force was passed.' },
|
|
21
|
+
{ code: 'managed_block_invalid', description: 'A dotenv merge target has missing, duplicated, or out-of-order managed block markers.' },
|
|
19
22
|
{ code: 'configorama_error', description: 'Generic, unclassified configorama error.' },
|
|
20
23
|
]
|
|
21
24
|
|
package/src/index.js
CHANGED
|
@@ -4,6 +4,9 @@ const enrichMetadata = require('./utils/parsing/enrichMetadata')
|
|
|
4
4
|
const { buildVariableSyntax } = require('./utils/variables/variableUtils')
|
|
5
5
|
const { serializeRequirements } = require('./utils/requirements/serializeRequirements')
|
|
6
6
|
const { buildConfigRequirements } = require('./utils/requirements/configRequirements')
|
|
7
|
+
const { runSetup } = require('./utils/setup/setupEngine')
|
|
8
|
+
const { writeDotenv } = require('./utils/setup/writeDotenv')
|
|
9
|
+
const { writeAnswers } = require('./utils/setup/writeAnswers')
|
|
7
10
|
const { buildIntrospection } = require('./utils/introspection/model')
|
|
8
11
|
const { buildAuditReport } = require('./utils/introspection/audit')
|
|
9
12
|
const { formatGraph } = require('./utils/introspection/graph')
|
|
@@ -142,6 +145,22 @@ module.exports.analyze = async (configPathOrObject, settings = {}) => {
|
|
|
142
145
|
return analysis
|
|
143
146
|
}
|
|
144
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Run the interactive setup wizard and return grouped answers without applying them
|
|
150
|
+
* @param {string|Object} configPathOrObject - Path to config file or raw config object
|
|
151
|
+
* @param {object} [settings] - Same settings as the main API, plus promptRenderer/streams
|
|
152
|
+
* @return {Promise<object>} { schemaVersion, configPath, requirements, answers, redactedAnswers }
|
|
153
|
+
*/
|
|
154
|
+
module.exports.setup = async (configPathOrObject, settings = {}) => {
|
|
155
|
+
return runSetup(configPathOrObject, settings, { analyze: module.exports.analyze })
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Safe writers for persisting setup answers (0600, atomic, overwrite-guarded)
|
|
160
|
+
*/
|
|
161
|
+
module.exports.writeDotenv = writeDotenv
|
|
162
|
+
module.exports.writeAnswers = writeAnswers
|
|
163
|
+
|
|
145
164
|
module.exports.introspect = async (configPathOrObject, settings = {}) => {
|
|
146
165
|
const analysis = await module.exports.analyze(configPathOrObject, {
|
|
147
166
|
...settings,
|
package/src/main.js
CHANGED
|
@@ -110,9 +110,8 @@ const { validateOneOf } = require('./utils/filters/oneOf')
|
|
|
110
110
|
const chalk = require('./utils/ui/chalk')
|
|
111
111
|
const deepLog = require('./utils/ui/deep-log')
|
|
112
112
|
const { logHeader } = require('./utils/ui/logs')
|
|
113
|
-
const {
|
|
114
|
-
const {
|
|
115
|
-
const { redactUserInputsByRequirements } = require('./utils/redaction/setupRedaction')
|
|
113
|
+
const { runSetup } = require('./utils/setup/setupEngine')
|
|
114
|
+
const { applyAnswers } = require('./utils/setup/applyAnswers')
|
|
116
115
|
/* Display */
|
|
117
116
|
const { displayNoVariablesFound, displayVariableDetails, displayUniqueVariables, displayConfigurableVariables } = require('./display')
|
|
118
117
|
/* Metadata */
|
|
@@ -176,7 +175,6 @@ const functionPrefixPattern = /^> function /
|
|
|
176
175
|
|
|
177
176
|
let DEBUG = process.argv.includes('--debug') ? true : false
|
|
178
177
|
let VERBOSE = process.argv.includes('--verbose') ? true : false
|
|
179
|
-
let SETUP_MODE = process.argv.includes('--setup') ? true : false
|
|
180
178
|
// DEBUG = true
|
|
181
179
|
let DEBUG_TYPE = false
|
|
182
180
|
|
|
@@ -188,8 +186,8 @@ class Configorama {
|
|
|
188
186
|
}
|
|
189
187
|
|
|
190
188
|
const options = opts || {}
|
|
191
|
-
// Setup wizard
|
|
192
|
-
this.setupMode =
|
|
189
|
+
// Setup wizard is explicit opt-in: the CLI translates --setup/`setup` into options.setup
|
|
190
|
+
this.setupMode = options.setup === true
|
|
193
191
|
// Set opts to pass into JS file calls
|
|
194
192
|
this.settings = Object.assign({}, {
|
|
195
193
|
// Allow unknown ${xyz:...} syntax where xyz is not a registered resolver
|
|
@@ -940,34 +938,25 @@ class Configorama {
|
|
|
940
938
|
if (this.setupMode) {
|
|
941
939
|
logHeader('Setup Mode')
|
|
942
940
|
// deepLog('enrich', enrich)
|
|
943
|
-
const
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
941
|
+
const setupResult = await runSetup(this.configFilePath || this.config, this.settings, {
|
|
942
|
+
analysis: Object.assign({ originalConfig: this.originalConfig }, enrich),
|
|
943
|
+
})
|
|
944
|
+
this.setupRequirements = setupResult.requirements
|
|
945
|
+
|
|
946
|
+
// Summary shows only answered groups, sensitive values redacted
|
|
947
|
+
const displayInputs = {}
|
|
948
|
+
for (const [group, values] of Object.entries(setupResult.redactedAnswers)) {
|
|
949
|
+
if (Object.keys(values).length > 0) {
|
|
950
|
+
displayInputs[group] = values
|
|
951
|
+
}
|
|
952
|
+
}
|
|
947
953
|
|
|
948
954
|
logHeader('User Inputs Summary')
|
|
949
955
|
console.log()
|
|
950
956
|
console.log(JSON.stringify(displayInputs, null, 2))
|
|
951
957
|
|
|
952
|
-
//
|
|
953
|
-
|
|
954
|
-
// Apply user inputs to options and environment
|
|
955
|
-
if (userInputs.options) {
|
|
956
|
-
Object.assign(this.options, userInputs.options)
|
|
957
|
-
}
|
|
958
|
-
if (userInputs.env) {
|
|
959
|
-
Object.assign(process.env, userInputs.env)
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
if (userInputs.self) {
|
|
963
|
-
Object.assign(this.config, userInputs.self)
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
if (userInputs.dotProp) {
|
|
967
|
-
for (const [key, value] of Object.entries(userInputs.dotProp)) {
|
|
968
|
-
dotProp.set(this.config, key, value)
|
|
969
|
-
}
|
|
970
|
-
}
|
|
958
|
+
// Apply user inputs to options, environment, and config
|
|
959
|
+
applyAnswers({ options: this.options, env: process.env, config: this.config }, setupResult.answers)
|
|
971
960
|
|
|
972
961
|
console.log()
|
|
973
962
|
logHeader('Resolving Configuration')
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Detects a config file's format from raw content when its name/extension is
|
|
2
|
+
// ambiguous. Returns a file-extension string for the parser dispatch in parse.js.
|
|
3
|
+
// Best-effort heuristic — an explicit, recognized extension always wins over this.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Whether every meaningful line is a flat KEY=VALUE assignment (dotenv shape).
|
|
7
|
+
* Deliberately variable-syntax-agnostic: it inspects line structure, not the
|
|
8
|
+
* variable delimiters, so it works whether a config uses ${...}, $[...], or a
|
|
9
|
+
* custom syntax. Blank lines and #/; comments are ignored.
|
|
10
|
+
* @param {string} text - Trimmed file contents
|
|
11
|
+
* @returns {boolean} True for flat key=value content with no YAML/TOML structure
|
|
12
|
+
*/
|
|
13
|
+
function isFlatKeyValue(text) {
|
|
14
|
+
const lines = text.split('\n')
|
|
15
|
+
let assignments = 0
|
|
16
|
+
for (const raw of lines) {
|
|
17
|
+
const line = raw.trim()
|
|
18
|
+
if (!line || line.startsWith('#') || line.startsWith(';')) continue
|
|
19
|
+
if (/^(?:export\s+)?[A-Za-z_][A-Za-z0-9_.-]*\s*=/.test(line)) {
|
|
20
|
+
assignments++
|
|
21
|
+
continue
|
|
22
|
+
}
|
|
23
|
+
// A non-blank, non-comment line that isn't an assignment (e.g. a YAML
|
|
24
|
+
// mapping, a TOML section, or freeform text) — not a flat env file.
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
return assignments > 0
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Detect config format from file contents when the extension is missing or
|
|
32
|
+
* unrecognized.
|
|
33
|
+
* @param {string} contents - Raw file contents
|
|
34
|
+
* @returns {string} Detected file extension (e.g. '.json', '.yml', '.toml', '.env')
|
|
35
|
+
*/
|
|
36
|
+
function detectFormat(contents) {
|
|
37
|
+
const trimmed = contents.trimStart()
|
|
38
|
+
|
|
39
|
+
// JSON object: starts with {
|
|
40
|
+
if (trimmed[0] === '{') return '.json'
|
|
41
|
+
|
|
42
|
+
// TOML section headers must be checked before JSON array (both start with [)
|
|
43
|
+
// TOML: [section.subsection] (dots distinguish from INI)
|
|
44
|
+
if (/^\[[\w-]+\.[\w.-]+\]/.test(trimmed)) return '.toml'
|
|
45
|
+
// TOML: array-of-tables [[section]]
|
|
46
|
+
if (/^\[\[[\w.-]+\]\]/.test(trimmed)) return '.toml'
|
|
47
|
+
|
|
48
|
+
// JSON array: starts with [ followed by non-word char (quotes, numbers, braces, whitespace)
|
|
49
|
+
if (trimmed[0] === '[') return '.json'
|
|
50
|
+
|
|
51
|
+
// TOML: multi-line strings
|
|
52
|
+
if (trimmed.startsWith('"""')) return '.toml'
|
|
53
|
+
|
|
54
|
+
// YAML: starts with document marker
|
|
55
|
+
if (trimmed.startsWith('---')) return '.yml'
|
|
56
|
+
|
|
57
|
+
// HCL: terraform keywords
|
|
58
|
+
if (/^(resource|variable|locals|provider|data|module|output|terraform)\s/.test(trimmed)) return '.tf'
|
|
59
|
+
|
|
60
|
+
// Flat KEY=VALUE content (env-file shape) -> dotenv parser. dotenv is lossless
|
|
61
|
+
// for string values, which is what configorama resolves ${...} into. TOML
|
|
62
|
+
// errors on unquoted values; ini truncates values at ';' (breaks connection
|
|
63
|
+
// strings) and mangles 'export' prefixes.
|
|
64
|
+
if (isFlatKeyValue(trimmed)) return '.env'
|
|
65
|
+
|
|
66
|
+
// Default: YAML (most permissive parser)
|
|
67
|
+
return '.yml'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { detectFormat, isFlatKeyValue }
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/* Tests for content-based config format detection */
|
|
2
|
+
const { test } = require('uvu')
|
|
3
|
+
const assert = require('uvu/assert')
|
|
4
|
+
const { detectFormat, isFlatKeyValue } = require('./detectFormat')
|
|
5
|
+
|
|
6
|
+
/* JSON */
|
|
7
|
+
test('detects JSON object and array', () => {
|
|
8
|
+
assert.is(detectFormat('{\n "a": 1\n}'), '.json')
|
|
9
|
+
assert.is(detectFormat('[\n {"a": 1}\n]'), '.json')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
/* TOML — structural markers win */
|
|
13
|
+
test('detects TOML by section headers and leading multiline string', () => {
|
|
14
|
+
assert.is(detectFormat('[server.settings]\nname = "test"\nport = 3000'), '.toml')
|
|
15
|
+
assert.is(detectFormat('[[items]]\nname = "a"'), '.toml')
|
|
16
|
+
assert.is(detectFormat('"""\nmulti\n"""'), '.toml')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
/* YAML */
|
|
20
|
+
test('detects YAML by document marker and mappings', () => {
|
|
21
|
+
assert.is(detectFormat('---\nname: test'), '.yml')
|
|
22
|
+
assert.is(detectFormat('name: test\nport: 3000'), '.yml')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('YAML with template values still detects as YAML', () => {
|
|
26
|
+
assert.is(detectFormat("name: ${env:APP_NAME, 'x'}\nport: 3000"), '.yml')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
/* HCL */
|
|
30
|
+
test('detects terraform/HCL by keyword', () => {
|
|
31
|
+
assert.is(detectFormat('variable "region" {\n default = "us-east-1"\n}'), '.tf')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
/* dotenv — flat key=value shape */
|
|
35
|
+
test('detects flat key=value (no space) as dotenv', () => {
|
|
36
|
+
assert.is(detectFormat('FOO=bar\nBAZ=qux'), '.env')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('detects key=value with template values as dotenv (syntax-agnostic)', () => {
|
|
40
|
+
assert.is(detectFormat('stage=${option:stage}\nDB=${op:x}'), '.env')
|
|
41
|
+
// custom variable syntax must not change detection
|
|
42
|
+
assert.is(detectFormat('stage=$[option:stage]\nDB=$[op:x]'), '.env')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('detects dotenv with comments and export prefixes', () => {
|
|
46
|
+
assert.is(detectFormat('# comment\nexport TOKEN=abc\nURL=postgres://h/db;sslmode=require'), '.env')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('a single non-assignment line disqualifies dotenv detection', () => {
|
|
50
|
+
// has a YAML mapping mixed in -> not flat key=value -> YAML default
|
|
51
|
+
assert.is(detectFormat('FOO=bar\nname: test'), '.yml')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('empty or comment-only content is not dotenv', () => {
|
|
55
|
+
assert.is(detectFormat('# just a comment\n'), '.yml')
|
|
56
|
+
assert.is(detectFormat(''), '.yml')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
/* isFlatKeyValue unit */
|
|
60
|
+
test('isFlatKeyValue recognizes assignment lines only', () => {
|
|
61
|
+
assert.is(isFlatKeyValue('A=1\nB=2'), true)
|
|
62
|
+
assert.is(isFlatKeyValue('export A=1\n# c\nB=2'), true)
|
|
63
|
+
assert.is(isFlatKeyValue('A=1\nplain text line'), false)
|
|
64
|
+
assert.is(isFlatKeyValue('name: value'), false)
|
|
65
|
+
assert.is(isFlatKeyValue(''), false)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test.run()
|
|
@@ -8,6 +8,7 @@ const DOTENV = require('../../parsers/dotenv')
|
|
|
8
8
|
const JSON5 = require('../../parsers/json5')
|
|
9
9
|
const HCL = require('../../parsers/hcl')
|
|
10
10
|
const { isEnvFile } = require('../paths/fileType')
|
|
11
|
+
const { detectFormat } = require('./detectFormat')
|
|
11
12
|
const { executeTypeScriptFileSync } = require('../../parsers/typescript')
|
|
12
13
|
const { executeESMFileSync } = require('../../parsers/esm')
|
|
13
14
|
const cloudFormationSchema = require('./cloudformationSchema')
|
|
@@ -23,41 +24,6 @@ const KNOWN_EXTENSIONS = new Set([
|
|
|
23
24
|
'.md', '.mdx', '.markdown', '.mdown', '.mkdn', '.mkd', '.mdwn', '.markdn', '.mdtxt', '.mdtext'
|
|
24
25
|
])
|
|
25
26
|
|
|
26
|
-
/**
|
|
27
|
-
* Detect config format from file contents when extension is missing
|
|
28
|
-
* @param {string} contents - Raw file contents
|
|
29
|
-
* @returns {string} Detected file extension (e.g. '.json', '.yml', '.toml')
|
|
30
|
-
*/
|
|
31
|
-
function detectFormat(contents) {
|
|
32
|
-
const trimmed = contents.trimStart()
|
|
33
|
-
|
|
34
|
-
// JSON object: starts with {
|
|
35
|
-
if (trimmed[0] === '{') return '.json'
|
|
36
|
-
|
|
37
|
-
// TOML section headers must be checked before JSON array (both start with [)
|
|
38
|
-
// TOML: [section.subsection] (dots distinguish from INI)
|
|
39
|
-
if (/^\[[\w-]+\.[\w.-]+\]/.test(trimmed)) return '.toml'
|
|
40
|
-
// TOML: array-of-tables [[section]]
|
|
41
|
-
if (/^\[\[[\w.-]+\]\]/.test(trimmed)) return '.toml'
|
|
42
|
-
|
|
43
|
-
// JSON array: starts with [ followed by non-word char (quotes, numbers, braces, whitespace)
|
|
44
|
-
if (trimmed[0] === '[') return '.json'
|
|
45
|
-
|
|
46
|
-
// TOML: multi-line strings
|
|
47
|
-
if (trimmed.startsWith('"""')) return '.toml'
|
|
48
|
-
// TOML: key = value
|
|
49
|
-
if (/^\w[\w.-]*\s*=\s/m.test(trimmed)) return '.toml'
|
|
50
|
-
|
|
51
|
-
// YAML: starts with document marker
|
|
52
|
-
if (trimmed.startsWith('---')) return '.yml'
|
|
53
|
-
|
|
54
|
-
// HCL: terraform keywords
|
|
55
|
-
if (/^(resource|variable|locals|provider|data|module|output|terraform)\s/.test(trimmed)) return '.tf'
|
|
56
|
-
|
|
57
|
-
// Default: YAML (most permissive parser)
|
|
58
|
-
return '.yml'
|
|
59
|
-
}
|
|
60
|
-
|
|
61
27
|
/**
|
|
62
28
|
* @typedef {Object} ParseOptions
|
|
63
29
|
* @property {string} contents - Raw file contents to parse
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Applies setup wizard answer groups onto an in-memory resolution context.
|
|
2
|
+
// The caller chooses the env target (process.env or a plain object copy).
|
|
3
|
+
const dotProp = require('dot-prop')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} ResolutionContext
|
|
7
|
+
* @property {Object.<string, any>} options - options hive for ${opt:...} variables
|
|
8
|
+
* @property {Object.<string, any>} env - env target; pass process.env or a plain object
|
|
9
|
+
* @property {Object.<string, any>} config - config object under resolution
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Apply answers from the setup wizard to a resolution context
|
|
14
|
+
* @param {ResolutionContext} context - context to mutate
|
|
15
|
+
* @param {Object} [answers] - answer groups { options, env, self, dotProp }
|
|
16
|
+
* @returns {ResolutionContext} the same context, mutated
|
|
17
|
+
*/
|
|
18
|
+
function applyAnswers(context, answers) {
|
|
19
|
+
if (!answers) return context
|
|
20
|
+
|
|
21
|
+
if (answers.options) {
|
|
22
|
+
Object.assign(context.options, answers.options)
|
|
23
|
+
}
|
|
24
|
+
if (answers.env) {
|
|
25
|
+
Object.assign(context.env, answers.env)
|
|
26
|
+
}
|
|
27
|
+
if (answers.self) {
|
|
28
|
+
Object.assign(context.config, answers.self)
|
|
29
|
+
}
|
|
30
|
+
if (answers.dotProp) {
|
|
31
|
+
for (const [key, value] of Object.entries(answers.dotProp)) {
|
|
32
|
+
dotProp.set(context.config, key, value)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return context
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
applyAnswers,
|
|
41
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Orchestrates the config setup wizard: analyze config, prompt for missing values,
|
|
2
|
+
// and return grouped answers plus a redacted copy safe for display.
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const { runConfigWizard } = require('../ui/configWizard')
|
|
5
|
+
const { buildConfigRequirements } = require('../requirements/configRequirements')
|
|
6
|
+
const { redactUserInputsByRequirements } = require('../redaction/setupRedaction')
|
|
7
|
+
|
|
8
|
+
const ANSWER_GROUPS = /** @type {const} */ (['options', 'env', 'self', 'dotProp'])
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} SetupAnswers
|
|
12
|
+
* @property {Object.<string, any>} options - answers for ${opt:...} variables
|
|
13
|
+
* @property {Object.<string, any>} env - answers for ${env:...} variables
|
|
14
|
+
* @property {Object.<string, any>} self - answers for ${self:...} variables
|
|
15
|
+
* @property {Object.<string, any>} dotProp - answers for dot-prop config references
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} SetupResult
|
|
20
|
+
* @property {number} schemaVersion - result shape version
|
|
21
|
+
* @property {string|null} configPath - absolute path to the config file, null for object input
|
|
22
|
+
* @property {Array<Object>} requirements - prompt requirements from buildConfigRequirements
|
|
23
|
+
* @property {SetupAnswers} answers - raw user answers grouped by variable type
|
|
24
|
+
* @property {SetupAnswers} redactedAnswers - answers with sensitive values redacted for display
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Ensure every answer group exists so callers get a stable shape
|
|
29
|
+
* @param {Object|undefined} userInputs - answers from the prompt renderer
|
|
30
|
+
* @returns {SetupAnswers} answers with all groups present
|
|
31
|
+
*/
|
|
32
|
+
function normalizeAnswerGroups(userInputs) {
|
|
33
|
+
/** @type {SetupAnswers} */
|
|
34
|
+
const normalized = { options: {}, env: {}, self: {}, dotProp: {} }
|
|
35
|
+
for (const group of ANSWER_GROUPS) {
|
|
36
|
+
normalized[group] = Object.assign({}, userInputs && userInputs[group])
|
|
37
|
+
}
|
|
38
|
+
return normalized
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Run the setup flow: analyze the config, prompt for unresolved values, return answers.
|
|
43
|
+
* Pure orchestration - applies nothing to process.env, config, or disk.
|
|
44
|
+
* @param {string|Object} configPathOrObject - path to config file or raw config object
|
|
45
|
+
* @param {Object} [settings] - configorama settings, plus:
|
|
46
|
+
* @param {Function} [settings.promptRenderer] - replaces the interactive wizard (mocks, answers files)
|
|
47
|
+
* @param {Object} [settings.streams] - stream overrides forwarded to the prompt renderer
|
|
48
|
+
* @param {Object} deps - injected dependencies
|
|
49
|
+
* @param {Function} [deps.analyze] - configorama.analyze, injected to avoid a require cycle
|
|
50
|
+
* @param {Object} [deps.analysis] - pre-computed analysis; skips the analyze call
|
|
51
|
+
* @returns {Promise<SetupResult>} collected answers and requirements
|
|
52
|
+
*/
|
|
53
|
+
async function runSetup(configPathOrObject, settings = {}, deps = {}) {
|
|
54
|
+
const { analyze } = deps
|
|
55
|
+
if (!deps.analysis && typeof analyze !== 'function') {
|
|
56
|
+
throw new Error('setup engine requires an analyze function or a pre-computed analysis')
|
|
57
|
+
}
|
|
58
|
+
const { promptRenderer, streams, ...analyzeSettings } = settings
|
|
59
|
+
|
|
60
|
+
const analysis = deps.analysis || await analyze(configPathOrObject, analyzeSettings)
|
|
61
|
+
const requirements = buildConfigRequirements(analysis)
|
|
62
|
+
|
|
63
|
+
const configPath = typeof configPathOrObject === 'string'
|
|
64
|
+
? path.resolve(configPathOrObject)
|
|
65
|
+
: null
|
|
66
|
+
|
|
67
|
+
const renderPrompts = promptRenderer || runConfigWizard
|
|
68
|
+
const userInputs = await renderPrompts(analysis, analysis.originalConfig || {}, configPath || '', streams)
|
|
69
|
+
|
|
70
|
+
const answers = normalizeAnswerGroups(userInputs)
|
|
71
|
+
const redactedAnswers = normalizeAnswerGroups(redactUserInputsByRequirements(answers, requirements))
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
schemaVersion: 1,
|
|
75
|
+
configPath,
|
|
76
|
+
requirements,
|
|
77
|
+
answers,
|
|
78
|
+
redactedAnswers,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
runSetup,
|
|
84
|
+
normalizeAnswerGroups,
|
|
85
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Temporarily delegates process.stdout writes to another stream so interactive
|
|
2
|
+
// prompt UI can render elsewhere (e.g. stderr) while stdout stays machine-clean.
|
|
3
|
+
|
|
4
|
+
const MIRRORED_TTY_PROPS = ['rows', 'columns', 'isTTY']
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Run fn with all process.stdout writes forwarded to target.
|
|
8
|
+
* TTY dimensions are mirrored from target so prompt layout matches the
|
|
9
|
+
* stream it actually renders on (stdout is often a pipe in --export mode).
|
|
10
|
+
* @param {import('stream').Writable} target - stream that receives the writes
|
|
11
|
+
* @param {Function} fn - async function to run while redirected
|
|
12
|
+
* @returns {Promise<any>} fn's result
|
|
13
|
+
*/
|
|
14
|
+
async function withStdoutRedirected(target, fn) {
|
|
15
|
+
const originalWrite = process.stdout.write
|
|
16
|
+
const savedDescriptors = {}
|
|
17
|
+
|
|
18
|
+
process.stdout.write = /** @type {typeof process.stdout.write} */ (
|
|
19
|
+
(chunk, encoding, callback) => target.write(chunk, encoding, callback)
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
for (const prop of MIRRORED_TTY_PROPS) {
|
|
23
|
+
if (target[prop] === undefined) continue
|
|
24
|
+
savedDescriptors[prop] = Object.getOwnPropertyDescriptor(process.stdout, prop) || null
|
|
25
|
+
Object.defineProperty(process.stdout, prop, {
|
|
26
|
+
configurable: true,
|
|
27
|
+
enumerable: true,
|
|
28
|
+
get() { return target[prop] },
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
return await fn()
|
|
34
|
+
} finally {
|
|
35
|
+
process.stdout.write = originalWrite
|
|
36
|
+
for (const [prop, descriptor] of Object.entries(savedDescriptors)) {
|
|
37
|
+
if (descriptor) {
|
|
38
|
+
Object.defineProperty(process.stdout, prop, descriptor)
|
|
39
|
+
} else {
|
|
40
|
+
delete process.stdout[prop]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
withStdoutRedirected,
|
|
48
|
+
}
|