@jungvonmatt/contentful-config 4.0.1 → 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/README.md CHANGED
@@ -48,5 +48,60 @@ const { config } = await loadContentfulConfig('myapp', {
48
48
  // either from existing sources or user input
49
49
  ```
50
50
 
51
+ ## CLI
52
+
53
+ The package includes a CLI tool to generate `.env`-compatible configuration files.
54
+
55
+ ### Usage
56
+
57
+ ```bash
58
+ contentful-config [options]
59
+ ```
60
+
61
+ ### Options
62
+
63
+ | Option | Description |
64
+ | --- | --- |
65
+ | `-n, --name <name>` | Config name (default: `contentful`) |
66
+ | `-o, --output <file>` | Write output to a file instead of stdout |
67
+ | `-r, --required <keys>` | Required config keys, comma-separated or repeated (default: `spaceId,environmentId,accessToken,previewAccessToken`) |
68
+ | `-h, --help` | Show help message |
69
+
70
+ ### Examples
71
+
72
+ ```bash
73
+ # Print .env config to stdout
74
+ contentful-config
75
+
76
+ # Write to a .env file (merges with existing content)
77
+ contentful-config -o .env
78
+
79
+ # Use a custom config name
80
+ contentful-config -n myapp -o .env
81
+
82
+ # Specify custom required keys
83
+ contentful-config -r spaceId,accessToken
84
+
85
+ # Or repeat the flag
86
+ contentful-config -r spaceId -r accessToken -r environmentId
87
+ ```
88
+
89
+ ### Behavior
90
+
91
+ - **Login check**: If no Contentful management token is found, the CLI automatically runs `contentful login` to authenticate.
92
+ - **Interactive prompts**: All required keys are prompted interactively, with existing values pre-filled as defaults.
93
+ - **File merging**: When using `-o`, existing file content is preserved. Matching keys are updated in place, new keys are appended. Comments and unrelated entries remain untouched.
94
+ - **Output**: The following environment variables are generated:
95
+
96
+ | Config Key | Environment Variable |
97
+ | --- | --- |
98
+ | `spaceId` | `CONTENTFUL_SPACE_ID` |
99
+ | `environmentId` | `CONTENTFUL_ENVIRONMENT_ID` |
100
+ | `accessToken` | `CONTENTFUL_DELIVERY_ACCESS_TOKEN` |
101
+ | `previewAccessToken` | `CONTENTFUL_PREVIEW_ACCESS_TOKEN` |
102
+ | `host` | `CONTENTFUL_HOST` |
103
+
104
+ > **Note:** The management token is intentionally excluded from the output as it is read from the Contentful CLI config file.
105
+
51
106
  [npm-url]: https://www.npmjs.com/package/@jungvonmatt/contentful-config
52
107
  [npm-image]: https://img.shields.io/npm/v/@jungvonmatt/contentful-config.svg
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from 'node:child_process';
3
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { resolve } from 'node:path';
5
+ import { parseArgs } from 'node:util';
6
+ import { loadContentfulConfig } from './index.js';
7
+ const ENV_MAP = {
8
+ spaceId: 'CONTENTFUL_SPACE_ID',
9
+ environmentId: 'CONTENTFUL_ENVIRONMENT_ID',
10
+ managementToken: 'CONTENTFUL_MANAGEMENT_TOKEN',
11
+ previewAccessToken: 'CONTENTFUL_PREVIEW_ACCESS_TOKEN',
12
+ accessToken: 'CONTENTFUL_DELIVERY_ACCESS_TOKEN',
13
+ host: 'CONTENTFUL_HOST',
14
+ };
15
+ const SKIP_KEYS = new Set([
16
+ 'organizationId',
17
+ 'activeSpaceId',
18
+ 'activeEnvironmentId',
19
+ 'managementToken',
20
+ ]);
21
+ function usage() {
22
+ console.log(`Usage: contentful-config [options]
23
+
24
+ Options:
25
+ -n, --name <name> Config name (default: "contentful")
26
+ -o, --output <file> Write output to file instead of stdout
27
+ -r, --required <keys> Required config keys (comma-separated or repeated)
28
+ Default: spaceId,environmentId,accessToken,previewAccessToken
29
+ -h, --help Show this help message`);
30
+ }
31
+ async function ensureLogin(name) {
32
+ const { config } = await loadContentfulConfig(name, { prompts: false });
33
+ if (config.managementToken) {
34
+ return config;
35
+ }
36
+ console.error('No management token found. Starting contentful login…');
37
+ execFileSync('contentful', ['login'], { stdio: 'inherit' });
38
+ const { config: updatedConfig } = await loadContentfulConfig(name, { prompts: false });
39
+ if (!updatedConfig.managementToken) {
40
+ throw new Error('Login failed. No management token found after login.');
41
+ }
42
+ return updatedConfig;
43
+ }
44
+ async function main() {
45
+ const { values } = parseArgs({
46
+ options: {
47
+ name: { type: 'string', short: 'n', default: 'contentful' },
48
+ output: { type: 'string', short: 'o' },
49
+ required: { type: 'string', short: 'r', multiple: true },
50
+ help: { type: 'boolean', short: 'h', default: false },
51
+ },
52
+ allowPositionals: false,
53
+ });
54
+ if (values.help) {
55
+ usage();
56
+ return;
57
+ }
58
+ const { name } = values;
59
+ const defaultRequired = ['spaceId', 'environmentId', 'accessToken', 'previewAccessToken'];
60
+ const required = values.required?.length
61
+ ? values.required.flatMap((v) => v.split(','))
62
+ : defaultRequired;
63
+ await ensureLogin(name);
64
+ const { config } = await loadContentfulConfig(name, {
65
+ required,
66
+ prompt: required,
67
+ });
68
+ const lines = [];
69
+ for (const [key, value] of Object.entries(config)) {
70
+ if (SKIP_KEYS.has(key) || value === undefined || value === null || value === '') {
71
+ continue;
72
+ }
73
+ const envKey = ENV_MAP[key] ?? `CONTENTFUL_${key.replace(/([A-Z])/g, '_$1').toUpperCase()}`;
74
+ lines.push(`${envKey}=${String(value)}`);
75
+ }
76
+ const output = lines.join('\n') + '\n';
77
+ if (values.output) {
78
+ const filePath = resolve(values.output);
79
+ const newEntries = new Map(lines.map((line) => {
80
+ const [key, ...rest] = line.split('=');
81
+ return [key, rest.join('=')];
82
+ }));
83
+ const existingLines = [];
84
+ if (existsSync(filePath)) {
85
+ const existing = readFileSync(filePath, 'utf-8');
86
+ for (const line of existing.split('\n')) {
87
+ const trimmed = line.trim();
88
+ if (!trimmed || trimmed.startsWith('#')) {
89
+ existingLines.push(line);
90
+ continue;
91
+ }
92
+ const [key] = trimmed.split('=');
93
+ if (newEntries.has(key)) {
94
+ existingLines.push(`${key}=${newEntries.get(key)}`);
95
+ newEntries.delete(key);
96
+ }
97
+ else {
98
+ existingLines.push(line);
99
+ }
100
+ }
101
+ }
102
+ if (newEntries.size > 0) {
103
+ for (const [key, value] of newEntries) {
104
+ existingLines.push(`${key}=${value}`);
105
+ }
106
+ }
107
+ const merged = existingLines.join('\n').replace(/\n*$/, '\n');
108
+ writeFileSync(filePath, merged, 'utf-8');
109
+ console.error(`Config written to ${filePath}`);
110
+ }
111
+ else {
112
+ process.stdout.write(output);
113
+ }
114
+ }
115
+ main().catch((error) => {
116
+ console.error(error);
117
+ process.exit(1);
118
+ });
119
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@jungvonmatt/contentful-config",
3
- "version": "4.0.1",
3
+ "version": "4.1.0",
4
4
  "main": "./dist/index.js",
5
5
  "type": "module",
6
6
  "exports": "./dist/index.js",
7
7
  "bin": {
8
- "contentful-typings": "./dist/cli.js"
8
+ "contentful-config": "./dist/cli.js"
9
9
  },
10
10
  "files": [
11
11
  "src",
@@ -21,7 +21,7 @@
21
21
  "clean": "rimraf ./dist",
22
22
  "test": "jest",
23
23
  "lint": "eslint --color src --fix --ext .ts",
24
- "precompile": "npm run clean",
24
+ "precompile": "pnpm run clean",
25
25
  "compile": "tsc --build",
26
26
  "watch": "tsc --build --watch"
27
27
  },
@@ -37,11 +37,15 @@
37
37
  "homepage": "https://github.com/jungvonmatt/contentful-ssg#readme",
38
38
  "dependencies": {
39
39
  "@jungvonmatt/config-loader": "^0.6.0",
40
+ "contentful-cli": "^3.9.0",
40
41
  "contentful-management": "^11.54.4",
41
42
  "node-homedir": "^2.0.0",
42
43
  "package-up": "^5.0.0",
43
44
  "pathe": "^2.0.3",
44
45
  "type-fest": "^4.41.0"
45
46
  },
46
- "gitHead": "1b42ea25fd3927091cd850c7aee8ba1c23591fdc"
47
+ "devDependencies": {
48
+ "@types/node": "^20.11.5"
49
+ },
50
+ "gitHead": "ad36885939a63334f5900a7bbc6cef168f72f8c7"
47
51
  }
package/src/cli.ts ADDED
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from 'node:child_process';
3
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { resolve } from 'node:path';
5
+ import { parseArgs } from 'node:util';
6
+ import { loadContentfulConfig } from './index.js';
7
+
8
+ const ENV_MAP: Record<string, string> = {
9
+ spaceId: 'CONTENTFUL_SPACE_ID',
10
+ environmentId: 'CONTENTFUL_ENVIRONMENT_ID',
11
+ managementToken: 'CONTENTFUL_MANAGEMENT_TOKEN',
12
+ previewAccessToken: 'CONTENTFUL_PREVIEW_ACCESS_TOKEN',
13
+ accessToken: 'CONTENTFUL_DELIVERY_ACCESS_TOKEN',
14
+ host: 'CONTENTFUL_HOST',
15
+ };
16
+
17
+ const SKIP_KEYS = new Set([
18
+ 'organizationId',
19
+ 'activeSpaceId',
20
+ 'activeEnvironmentId',
21
+ 'managementToken',
22
+ ]);
23
+
24
+ function usage() {
25
+ console.log(`Usage: contentful-config [options]
26
+
27
+ Options:
28
+ -n, --name <name> Config name (default: "contentful")
29
+ -o, --output <file> Write output to file instead of stdout
30
+ -r, --required <keys> Required config keys (comma-separated or repeated)
31
+ Default: spaceId,environmentId,accessToken,previewAccessToken
32
+ -h, --help Show this help message`);
33
+ }
34
+
35
+ async function ensureLogin(name: string) {
36
+ const { config } = await loadContentfulConfig(name, { prompts: false });
37
+
38
+ if (config.managementToken) {
39
+ return config;
40
+ }
41
+
42
+ console.error('No management token found. Starting contentful login…');
43
+ execFileSync('contentful', ['login'], { stdio: 'inherit' });
44
+
45
+ // Reload config after login
46
+ const { config: updatedConfig } = await loadContentfulConfig(name, { prompts: false });
47
+
48
+ if (!updatedConfig.managementToken) {
49
+ throw new Error('Login failed. No management token found after login.');
50
+ }
51
+
52
+ return updatedConfig;
53
+ }
54
+
55
+ async function main() {
56
+ const { values } = parseArgs({
57
+ options: {
58
+ name: { type: 'string', short: 'n', default: 'contentful' },
59
+ output: { type: 'string', short: 'o' },
60
+ required: { type: 'string', short: 'r', multiple: true },
61
+ help: { type: 'boolean', short: 'h', default: false },
62
+ },
63
+ allowPositionals: false,
64
+ });
65
+
66
+ if (values.help) {
67
+ usage();
68
+ return;
69
+ }
70
+
71
+ const { name } = values;
72
+
73
+ const defaultRequired = ['spaceId', 'environmentId', 'accessToken', 'previewAccessToken'];
74
+ const required = values.required?.length
75
+ ? values.required.flatMap((v) => v.split(','))
76
+ : defaultRequired;
77
+
78
+ await ensureLogin(name);
79
+
80
+ const { config } = await loadContentfulConfig(name, {
81
+ required,
82
+ prompt: required,
83
+ });
84
+
85
+ const lines: string[] = [];
86
+ for (const [key, value] of Object.entries(config)) {
87
+ if (SKIP_KEYS.has(key) || value === undefined || value === null || value === '') {
88
+ continue;
89
+ }
90
+
91
+ const envKey = ENV_MAP[key] ?? `CONTENTFUL_${key.replace(/([A-Z])/g, '_$1').toUpperCase()}`;
92
+ lines.push(`${envKey}=${String(value)}`);
93
+ }
94
+
95
+ const output = lines.join('\n') + '\n';
96
+
97
+ if (values.output) {
98
+ const filePath = resolve(values.output);
99
+ const newEntries = new Map(
100
+ lines.map((line) => {
101
+ const [key, ...rest] = line.split('=');
102
+ return [key, rest.join('=')];
103
+ }),
104
+ );
105
+
106
+ // Read existing file and merge
107
+ const existingLines: string[] = [];
108
+ if (existsSync(filePath)) {
109
+ const existing = readFileSync(filePath, 'utf-8');
110
+ for (const line of existing.split('\n')) {
111
+ const trimmed = line.trim();
112
+ if (!trimmed || trimmed.startsWith('#')) {
113
+ existingLines.push(line);
114
+ continue;
115
+ }
116
+
117
+ const [key] = trimmed.split('=');
118
+ if (newEntries.has(key)) {
119
+ // Replace with new value
120
+ existingLines.push(`${key}=${newEntries.get(key)}`);
121
+ newEntries.delete(key);
122
+ } else {
123
+ existingLines.push(line);
124
+ }
125
+ }
126
+ }
127
+
128
+ // Append remaining new entries
129
+ if (newEntries.size > 0) {
130
+ for (const [key, value] of newEntries) {
131
+ existingLines.push(`${key}=${value}`);
132
+ }
133
+ }
134
+
135
+ // Ensure trailing newline
136
+ const merged = existingLines.join('\n').replace(/\n*$/, '\n');
137
+ writeFileSync(filePath, merged, 'utf-8');
138
+ console.error(`Config written to ${filePath}`);
139
+ } else {
140
+ process.stdout.write(output);
141
+ }
142
+ }
143
+
144
+ main().catch((error) => {
145
+ console.error(error);
146
+ process.exit(1);
147
+ });