@cocreate/config 1.14.2 → 1.14.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/config",
3
- "version": "1.14.2",
3
+ "version": "1.14.3",
4
4
  "description": "A convenient chain handler allows user to chain multiple CoCreate components together. When one action is complete next one will start. The sequence goes untill all config completed. Grounded on Vanilla javascript, easily configured using HTML5 attributes and/or JavaScript API.",
5
5
  "keywords": [
6
6
  "config",
@@ -36,15 +36,7 @@
36
36
  },
37
37
  "type": "module",
38
38
  "main": "./src/server.js",
39
- "browser": "./src/client.js",
40
- "exports": {
41
- ".": {
42
- "node": "./src/server.js",
43
- "deno": "./src/server.js",
44
- "browser": "./src/client.js",
45
- "default": "./src/server.js"
46
- }
47
- },
39
+ "browser": "./src/browser.js",
48
40
  "dependencies": {
49
41
  "@cocreate/utils": "^1.44.0"
50
42
  },
package/src/browser.js ADDED
@@ -0,0 +1,122 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2026 CoCreate and Contributors.
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published
6
+ * by the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ ********************************************************************************/
17
+
18
+ import { configAPI, registerAdapter } from './core.js';
19
+
20
+ let localStorageImpl = null;
21
+
22
+ // Bypassing top-level await by using standard promise syntax for ES2017 compatibility
23
+ import('@cocreate/local-storage')
24
+ .then((mod) => {
25
+ localStorageImpl = mod.default || mod;
26
+ })
27
+ .catch(() => {
28
+ if (typeof window !== 'undefined') {
29
+ localStorageImpl = window.localStorage;
30
+ }
31
+ });
32
+
33
+ /**
34
+ * Renders an interactive fallback HTML modal overlay in the browser
35
+ * to handle real-time configurations or missing setup questions.
36
+ *
37
+ * @param {string} question - The prompt question to display.
38
+ * @returns {Promise<string>} The user's input response.
39
+ */
40
+ async function browserPrompt(question) {
41
+ return new Promise((resolve) => {
42
+ const container = document.createElement('div');
43
+ container.innerHTML = `
44
+ <div style="position:fixed;inset:0;background:rgba(15,23,42,0.6);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:999999;font-family:system-ui,-apple-system,sans-serif;">
45
+ <div style="background:#ffffff;padding:24px;border-radius:12px;box-shadow:0 20px 25px -5px rgba(0,0,0,0.1);width:100%;max-width:360px;box-sizing:border-box;">
46
+ <div style="font-weight:600;font-size:16px;color:#1e293b;margin-bottom:12px;">${question}</div>
47
+ <input id="prompt-input" type="text" placeholder="Enter your response..." style="width:100%;padding:10px 12px;border:1px solid #cbd5e1;border-radius:6px;font-size:14px;outline:none;margin-bottom:16px;box-sizing:border-box;">
48
+ <button id="prompt-submit" style="width:100%;padding:10px;background:#2563eb;color:#ffffff;border:none;border-radius:6px;font-size:14px;font-weight:500;cursor:pointer;transition:background 0.15s ease;">Submit</button>
49
+ </div>
50
+ </div>
51
+ `;
52
+ document.body.appendChild(container);
53
+
54
+ const input = container.querySelector('#prompt-input');
55
+ const button = container.querySelector('#prompt-submit');
56
+
57
+ input.focus();
58
+
59
+ const submitValue = () => {
60
+ const val = input.value.trim();
61
+ container.remove();
62
+ resolve(val);
63
+ };
64
+
65
+ button.addEventListener('click', submitValue);
66
+ input.addEventListener('keydown', (e) => {
67
+ if (e.key === 'Enter') submitValue();
68
+ });
69
+ });
70
+ }
71
+
72
+ // Bind platform capabilities directly into the core adapter engine
73
+ registerAdapter({
74
+ isBrowser: true,
75
+
76
+ getLocalStorageItem: (key) => {
77
+ return localStorageImpl ? localStorageImpl.getItem(key) : null;
78
+ },
79
+
80
+ setLocalStorageItem: (key, value) => {
81
+ if (localStorageImpl) {
82
+ localStorageImpl.setItem(key, value);
83
+ }
84
+ },
85
+
86
+ removeLocalStorageItem: (key) => {
87
+ if (localStorageImpl) {
88
+ localStorageImpl.removeItem(key);
89
+ }
90
+ },
91
+
92
+ clearLocalStorage: () => {
93
+ if (localStorageImpl && typeof localStorageImpl.clear === 'function') {
94
+ localStorageImpl.clear();
95
+ }
96
+ },
97
+
98
+ getGlobalConfig: () => {
99
+ return (typeof window !== 'undefined' && window.CoCreateConfig) || {};
100
+ },
101
+
102
+ setGlobalConfig: (config) => {
103
+ if (typeof window !== 'undefined') {
104
+ window.CoCreateConfig = config;
105
+ }
106
+ },
107
+
108
+ getEnv: () => null,
109
+ setEnv: () => {},
110
+ removeEnv: () => {},
111
+
112
+ getGitBranch: async () => null,
113
+ prompt: browserPrompt,
114
+
115
+ loadConfig: async () => {
116
+ return (typeof window !== 'undefined' && window.CoCreateConfig) || {};
117
+ },
118
+
119
+ saveConfig: async () => {}
120
+ });
121
+
122
+ export default configAPI;
package/src/core.js ADDED
@@ -0,0 +1,282 @@
1
+ import { dotNotationToObject, getValueFromObject } from '@cocreate/utils';
2
+
3
+ // Global in-memory state holding configuration variables
4
+ let configMemory = {};
5
+
6
+ /**
7
+ * Adapter holding environment-specific behaviors.
8
+ * These are injected statically by the environment wrappers (Browser or Node/Deno Server).
9
+ * @type {Object}
10
+ */
11
+ let adapter = {
12
+ isBrowser: false,
13
+ getLocalStorageItem: (key) => null,
14
+ setLocalStorageItem: (key, value) => {},
15
+ removeLocalStorageItem: (key) => {},
16
+ clearLocalStorage: () => {},
17
+ getGlobalConfig: () => ({}),
18
+ setGlobalConfig: (config) => {},
19
+ getEnv: (key) => null,
20
+ setEnv: (key, value) => {},
21
+ removeEnv: (key) => {},
22
+ getGitBranch: async () => null,
23
+ prompt: async (question) => "",
24
+ loadConfig: async (filePath) => ({}),
25
+ saveConfig: async (filePath, content) => {}
26
+ };
27
+
28
+ /**
29
+ * Registers the environment adapter. Called statically by wrapper modules at startup.
30
+ *
31
+ * @param {Object} envAdapter - The runtime adapter implementation.
32
+ */
33
+ export function registerAdapter(envAdapter) {
34
+ adapter = { ...adapter, ...envAdapter };
35
+ }
36
+
37
+ /**
38
+ * Synchronously retrieves a configuration value by key.
39
+ *
40
+ * @param {string} key - The configuration key to look up.
41
+ * @returns {*} The configuration value, or null if not found.
42
+ */
43
+ export function get(key) {
44
+ if (adapter.isBrowser) {
45
+ if (configMemory[key] !== undefined) {
46
+ return configMemory[key];
47
+ }
48
+
49
+ const localVal = adapter.getLocalStorageItem(key);
50
+ if (localVal !== null && localVal !== undefined) return localVal;
51
+
52
+ return null;
53
+ } else {
54
+ if (configMemory[key] !== undefined) return configMemory[key];
55
+
56
+ return adapter.getEnv(key);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Synchronously sets a configuration key-value pair.
62
+ *
63
+ * @param {string} key - The configuration key to store.
64
+ * @param {*} value - The value to assign to the configuration key.
65
+ * @returns {Function} The hoisted configuration request API context for chaining.
66
+ */
67
+ export function set(key, value) {
68
+ configMemory[key] = value;
69
+
70
+ if (adapter.isBrowser) {
71
+ adapter.setLocalStorageItem(key, value);
72
+ } else {
73
+ adapter.setEnv(key, value);
74
+ }
75
+ return request;
76
+ }
77
+
78
+ /**
79
+ * Synchronously removes a key-value pair across all storage scopes.
80
+ *
81
+ * @param {string} key - The configuration key to delete.
82
+ * @returns {Function} The hoisted configuration request API context for chaining.
83
+ */
84
+ export function remove(key) {
85
+ delete configMemory[key];
86
+
87
+ if (adapter.isBrowser) {
88
+ adapter.removeLocalStorageItem(key);
89
+ } else {
90
+ adapter.removeEnv(key);
91
+ }
92
+ return request;
93
+ }
94
+
95
+ /**
96
+ * Checks if a specific key exists inside the active configuration scope.
97
+ *
98
+ * @param {string} key - The configuration key to check.
99
+ * @returns {boolean} True if the key exists and is non-null, false otherwise.
100
+ */
101
+ export function has(key) {
102
+ return get(key) !== null;
103
+ }
104
+
105
+ /**
106
+ * Clears the active memory context and resets storage scopes.
107
+ *
108
+ * @returns {Function} The hoisted configuration request API context for chaining.
109
+ */
110
+ export function clear() {
111
+ configMemory = {};
112
+ if (adapter.isBrowser) {
113
+ adapter.clearLocalStorage();
114
+ }
115
+ return request;
116
+ }
117
+
118
+ /**
119
+ * Normalizes values, substituting dynamic evaluations (like Git branch lookups).
120
+ *
121
+ * @param {*} val - Config value configuration structure.
122
+ * @returns {*} Resolved value context.
123
+ */
124
+ async function resolveValue(val) {
125
+ if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
126
+ return val;
127
+ } else if (val && val.$branch) {
128
+ const branch = await adapter.getGitBranch();
129
+ return branch ? val.$branch[branch] : null;
130
+ } else {
131
+ return val;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Orchestrates prompts, files, and updates.
137
+ *
138
+ * @param {Object|string} [items] - Targeted configuration structure or a singular key to lookup.
139
+ * @param {boolean} [env=true] - Synced environments options.
140
+ * @param {boolean} [globalSetting=true] - Flag indicating whether values should sync back to global user configurations.
141
+ * @param {string} [configPath='CoCreate.config.js'] - The relative filepath name of the configuration module.
142
+ * @returns {Promise<Object>} Resolved configuration data key map.
143
+ */
144
+ export async function request(items, env = true, globalSetting = true, configPath = 'CoCreate.config.js') {
145
+ let localConfig = {};
146
+ let globalConfig = {};
147
+
148
+ // Load configurations statically via environment-isolated wrapper adapter
149
+ localConfig = await adapter.loadConfig(configPath);
150
+ globalConfig = await adapter.getGlobalConfig();
151
+
152
+ const filterEmptyValues = (obj) => {
153
+ return Object.fromEntries(
154
+ Object.entries(obj).filter(([_, val]) => {
155
+ if (typeof val === 'object' && !Array.isArray(val)) {
156
+ return Object.keys(val).length > 0;
157
+ } else if (Array.isArray(val)) {
158
+ return val.length > 0;
159
+ } else {
160
+ return val !== '';
161
+ }
162
+ })
163
+ );
164
+ };
165
+
166
+ let configData = {};
167
+ let updateGlobalFile = false;
168
+ let variables = {};
169
+
170
+ async function getConfig(targetItems) {
171
+ if (typeof targetItems === 'string') {
172
+ targetItems = { [targetItems]: '' };
173
+ }
174
+ for (let key of Object.keys(targetItems)) {
175
+ let { value, prompt: userPrompt, choices, variable } = targetItems[key] || {};
176
+
177
+ const placeholders = key.match(/{{\s*([\w\W]+?)\s*}}/g);
178
+ if (placeholders && placeholders.length) {
179
+ for (let placeholder of placeholders) {
180
+ if (variables[placeholder]) {
181
+ key = key.replace(placeholder, variables[placeholder]);
182
+ }
183
+ }
184
+ }
185
+
186
+ if (choices) {
187
+ if (!userPrompt && userPrompt !== '') continue;
188
+
189
+ let isAnswered = false;
190
+ for (let choice of Object.keys(choices)) {
191
+ let isAnsweredChoice = true;
192
+ for (let choicekey of Object.keys(choices[choice])) {
193
+ let choiceValue = configMemory[choicekey] || getValueFromObject(localConfig, choicekey) || getValueFromObject(globalConfig, choicekey);
194
+ if (choiceValue) {
195
+ configData[choicekey] = choiceValue;
196
+ } else {
197
+ isAnsweredChoice = false;
198
+ }
199
+ }
200
+ if (isAnsweredChoice) {
201
+ isAnswered = true;
202
+ }
203
+ }
204
+
205
+ if (!isAnswered) {
206
+ const answer = await adapter.prompt(userPrompt || `${key}: `);
207
+ const choice = choices[answer];
208
+ if (choice) {
209
+ await getConfig(choice);
210
+ }
211
+ }
212
+ } else if (variable) {
213
+ let variableValue = configMemory[key] || getValueFromObject(localConfig, key) || getValueFromObject(globalConfig, key);
214
+ if (!variableValue) {
215
+ variables[`{{${variable}}}`] = value || await adapter.prompt(userPrompt || `${variable}: `);
216
+ } else {
217
+ variables[`{{${variable}}}`] = Object.keys(variableValue)[0];
218
+ }
219
+ } else {
220
+ if (value || value === "") {
221
+ configData[key] = value;
222
+ if (globalSetting) {
223
+ updateGlobalFile = true;
224
+ }
225
+ } else if (!adapter.isBrowser && adapter.getEnv(key)) {
226
+ configData[key] = adapter.getEnv(key);
227
+ } else {
228
+ let localKey, globalKey;
229
+ if ((localKey = getValueFromObject(localConfig, key))) {
230
+ configData[key] = await resolveValue(localKey);
231
+ } else if ((globalKey = getValueFromObject(globalConfig, key))) {
232
+ configData[key] = await resolveValue(globalKey);
233
+ } else if (userPrompt || userPrompt === '') {
234
+ configData[key] = await adapter.prompt(userPrompt || `${key}: `);
235
+ if (globalSetting) updateGlobalFile = true;
236
+ }
237
+ }
238
+
239
+ if (configData[key] !== undefined) {
240
+ set(key, configData[key]);
241
+ }
242
+ }
243
+ }
244
+ }
245
+
246
+ if (items) {
247
+ await getConfig(items);
248
+
249
+ if (updateGlobalFile && !adapter.isBrowser) {
250
+ let updatedGlobalConfig = filterEmptyValues(
251
+ dotNotationToObject(configData, globalConfig)
252
+ );
253
+ await adapter.setGlobalConfig(updatedGlobalConfig);
254
+ }
255
+ } else {
256
+ configData = {
257
+ ...filterEmptyValues(globalConfig),
258
+ ...filterEmptyValues(localConfig)
259
+ };
260
+
261
+ for (const [key, value] of Object.entries(configData)) {
262
+ set(key, value);
263
+ }
264
+ }
265
+
266
+ configData = dotNotationToObject(configData);
267
+ return configData;
268
+ }
269
+
270
+ // Bind configuration API methods directly to the request function to safely bypass Temporal Dead Zone (TDZ) limits.
271
+ request.request = request;
272
+ request.get = get;
273
+ request.set = set;
274
+ request.delete = remove;
275
+ request.remove = remove;
276
+ request.has = has;
277
+ request.clear = clear;
278
+
279
+ /**
280
+ * Consolidated API Object.
281
+ */
282
+ export const configAPI = request;
package/src/server.js CHANGED
@@ -1,235 +1,151 @@
1
- /********************************************************************************
2
- * Copyright (C) 2023 CoCreate and Contributors.
3
- *
4
- * This program is free software: you can redistribute it and/or modify
5
- * it under the terms of the GNU Affero General Public License as published
6
- * by the Free Software Foundation, either version 3 of the License, or
7
- * (at your option) any later version.
8
- *
9
- * This program is distributed in the hope that it will be useful,
10
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
- * GNU Affero General Public License for more details.
13
- *
14
- * You should have received a copy of the GNU Affero General Public License
15
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
- *
17
- ********************************************************************************/
18
-
19
- // Commercial Licensing Information:
20
- // For commercial use of this software without the copyleft provisions of the AGPLv3,
21
- // you must obtain a commercial license from CoCreate LLC.
22
- // For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
23
-
24
- import readline from 'node:readline';
25
- import os from 'node:os';
26
- import path from 'node:path';
27
- import fs from 'node:fs';
28
- import { promisify } from 'node:util';
29
- import { exec as childExec } from 'node:child_process';
30
- import { pathToFileURL } from 'node:url';
31
- import { createRequire } from 'node:module';
32
-
33
- import { dotNotationToObject, getValueFromObject } from '@cocreate/utils';
34
-
35
- const exec = promisify(childExec);
36
- const require = createRequire(import.meta.url);
37
-
38
- /**
39
- * Helper function to dynamically load configurations.
40
- * Handles both native ES modules and legacy CommonJS/JSON files safely.
41
- * @param {string} filePath - Absolute path to the config file
42
- * @returns {Promise<Object>} Evaluated configuration object
43
- */
44
- async function loadConfig(filePath) {
45
- if (!fs.existsSync(filePath)) return {};
46
- try {
47
- // Attempt native ESM dynamic import
48
- const fileUrl = pathToFileURL(filePath).href;
49
- const module = await import(fileUrl);
50
- return module.default || module;
51
- } catch (esmError) {
52
- try {
53
- // Fallback to Classic CommonJS / JSON Require compatibility
54
- return require(filePath);
55
- } catch (cjsError) {
56
- console.error(`[@cocreate/config] Failed to load configuration at ${filePath}:`, esmError.message);
57
- return {};
58
- }
59
- }
60
- }
61
-
62
- /**
63
- * Asynchronously retrieves configuration values. This function allows for
64
- * fetching values from project-specific configuration, global configuration, or
65
- * environment variables. Project configuration takes precedence over global and
66
- * environment settings. It also supports interactive prompts to gather critical values
67
- * from the user if they are not already defined in the configuration.
68
- *
69
- * @async
70
- * @param {Object|string} items - An object representing the configuration items to retrieve.
71
- * The keys are the config paths, and the values are objects
72
- * specifying the prompt and any variable substitutions.
73
- * @param {boolean} [env=true] - Flag to consider environment variables during config lookup.
74
- * @param {boolean} [global=true] - Flag to consider global configuration during config lookup.
75
- * @param {string} [configPath='CoCreate.config.js'] - Path to the project-specific configuration file.
76
- * @returns {Promise<Object>} A promise that resolves with the configuration object.
77
- */
78
- export default async function config(items, env = true, global = true, configPath = 'CoCreate.config.js') {
79
- async function promptForInput(question) {
80
- const rl = readline.createInterface({
81
- input: process.stdin,
82
- output: process.stdout
83
- });
84
-
85
- return new Promise((resolve) => {
86
- rl.question(question, (answer) => {
87
- rl.close();
88
- resolve(answer.trim());
89
- });
90
- });
91
- }
92
-
93
- const filterEmptyValues = (obj) => {
94
- return Object.fromEntries(
95
- Object.entries(obj).filter(([_, value]) => {
96
- if (typeof value === 'object' && !Array.isArray(value)) {
97
- return Object.keys(value).length > 0;
98
- } else if (Array.isArray(value)) {
99
- return value.length > 0;
100
- } else {
101
- return value !== '';
102
- }
103
- })
104
- );
105
- };
106
-
107
- async function getValue(value) {
108
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
109
- return value;
110
- } else if (value.$branch) {
111
- try {
112
- const { stdout } = await exec('git branch --show-current');
113
- return value.$branch[stdout.trim()];
114
- } catch (error) {
115
- console.error(`exec error: ${error}`);
116
- return null;
117
- }
118
- } else {
119
- return value;
120
- }
121
- }
122
-
123
- let configData = {};
124
- let update = false;
125
- let variables = {};
126
-
127
- async function getConfig(targetItems) {
128
- if (typeof targetItems === 'string') {
129
- targetItems = { [targetItems]: '' };
130
- }
131
- for (let key of Object.keys(targetItems)) {
132
- let { value, prompt, choices, variable } = targetItems[key];
133
-
134
- const placeholders = key.match(/{{\s*([\w\W]+?)\s*}}/g);
135
- if (placeholders && placeholders.length) {
136
- for (let placeholder of placeholders) {
137
- placeholder.trim();
138
- if (variables[placeholder]) {
139
- key = key.replace(placeholder, variables[placeholder]);
140
- }
141
- }
142
- }
143
-
144
- if (choices) {
145
- if (!prompt && prompt !== '') continue;
146
-
147
- let isAnswered = false;
148
- for (let choice of Object.keys(choices)) {
149
- let isAnsweredChoice = true;
150
- for (let choicekey of Object.keys(choices[choice])) {
151
- let choiceValue = localConfig[choicekey] || globalConfig[choicekey];
152
- if (choiceValue) {
153
- configData[choicekey] = choiceValue;
154
- } else {
155
- isAnsweredChoice = false;
156
- }
157
- }
158
- if (isAnsweredChoice) {
159
- isAnswered = true;
160
- }
161
- }
162
-
163
- if (!isAnswered) {
164
- const answer = await promptForInput(prompt || `${key}: `);
165
- const choice = choices[answer];
166
- if (choice) {
167
- await getConfig(choice);
168
- }
169
- }
170
- } else if (variable) {
171
- let variableValue = localConfig[key] || globalConfig[key];
172
- if (!variableValue) {
173
- variables[`{{${variable}}}`] = value || await promptForInput(prompt || `${variable}: `);
174
- } else {
175
- variables[`{{${variable}}}`] = Object.keys(variableValue)[0];
176
- }
177
- } else {
178
- if (value || value === "") {
179
- configData[key] = value;
180
- if (global) {
181
- update = true;
182
- }
183
- } else if (process.env[key]) {
184
- configData[key] = process.env[key];
185
- } else {
186
- let localKey, globalKey;
187
- if ((localKey = getValueFromObject(localConfig, key))) {
188
- configData[key] = await getValue(localKey);
189
- } else if ((globalKey = getValueFromObject(globalConfig, key))) {
190
- configData[key] = await getValue(globalKey);
191
- } else if (prompt || prompt === '') {
192
- configData[key] = await promptForInput(prompt || `${key}: `);
193
- if (global) update = true;
194
- }
195
- if (env) {
196
- if (typeof configData[key] === 'object') {
197
- process.env[key] = JSON.stringify(configData[key]);
198
- } else {
199
- process.env[key] = configData[key];
200
- }
201
- }
202
- }
203
- }
204
- }
205
- }
206
-
207
- // Resolving paths and dynamically loading configurations with ESM wrappers
208
- const localConfigPath = path.resolve(process.cwd(), configPath);
209
- const localConfig = await loadConfig(localConfigPath);
210
-
211
- const globalConfigPath = path.resolve(os.homedir(), 'CoCreate.config.js');
212
- const globalConfig = await loadConfig(globalConfigPath);
213
-
214
- if (items) {
215
- await getConfig(items);
216
-
217
- if (update) {
218
- let updatedGlobalConfig = filterEmptyValues(
219
- dotNotationToObject(configData, globalConfig)
220
- );
221
-
222
- // Updated to write modern ES module syntax "export default" instead of "module.exports"
223
- const globalConfigString = `export default ${JSON.stringify(updatedGlobalConfig, null, 2)};\n`;
224
- fs.writeFileSync(globalConfigPath, globalConfigString);
225
- }
226
- } else {
227
- configData = {
228
- ...filterEmptyValues(globalConfig),
229
- ...filterEmptyValues(localConfig)
230
- };
231
- }
232
-
233
- configData = dotNotationToObject(configData);
234
- return configData;
235
- }
1
+ /********************************************************************************
2
+ * Copyright (C) 2026 CoCreate and Contributors.
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published
6
+ * by the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ ********************************************************************************/
17
+
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import os from 'node:os';
21
+ import readline from 'node:readline';
22
+ import { promisify } from 'node:util';
23
+ import { exec as childExec } from 'node:child_process';
24
+
25
+ import { configAPI, registerAdapter } from './core.js';
26
+
27
+ const exec = promisify(childExec);
28
+
29
+ /**
30
+ * Retrieves the current active git branch name on the system.
31
+ *
32
+ * @returns {Promise<string|null>} The branch name, or null on error.
33
+ */
34
+ async function getGitBranch() {
35
+ try {
36
+ const { stdout } = await exec('git branch --show-current');
37
+ return stdout.trim();
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Prompts the CLI operator for input interactively using Node's standard readline module.
45
+ *
46
+ * @param {string} question - Question to print on stdout.
47
+ * @returns {Promise<string>} User input response.
48
+ */
49
+ async function cliPrompt(question) {
50
+ const rl = readline.createInterface({
51
+ input: process.stdin,
52
+ output: process.stdout
53
+ });
54
+
55
+ return new Promise((resolve) => {
56
+ rl.question(question, (answer) => {
57
+ rl.close();
58
+ resolve(answer.trim());
59
+ });
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Binary-safe server configuration file loader. Bypasses static compiler
65
+ * tree issues by parsing configuration files dynamically under a sandboxed function wrapper.
66
+ *
67
+ * @param {string} filePath - Absolute path to the config file on disk.
68
+ * @returns {Promise<Object>} The parsed configuration object.
69
+ */
70
+ async function loadConfig(filePath) {
71
+ try {
72
+ const resolvedPath = path.resolve(process.cwd(), filePath);
73
+ if (!fs.existsSync(resolvedPath)) return {};
74
+
75
+ // Load, clean comments, and normalize script syntax for evaluation
76
+ const content = fs.readFileSync(resolvedPath, 'utf8');
77
+ const cleanContent = content.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1').trim();
78
+
79
+ try {
80
+ return JSON.parse(cleanContent);
81
+ } catch {
82
+ const script = cleanContent
83
+ .replace(/export\s+default\s+/, 'return ')
84
+ .replace(/module\.exports\s*=\s*/, 'return ');
85
+
86
+ const fn = new Function('module', 'exports', script);
87
+ const exports = {};
88
+ const module = { exports };
89
+ const result = fn(module, exports);
90
+
91
+ return result || module.exports || {};
92
+ }
93
+ } catch (error) {
94
+ console.error(`[@cocreate/config] Failed to load configuration at ${filePath}:`, error.message);
95
+ return {};
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Writes or updates global settings on the user's host filesystem safely.
101
+ *
102
+ * @param {string} filePath - Path to save the config.
103
+ * @param {Object} content - The JSON payload to persist.
104
+ */
105
+ async function saveConfig(filePath, content) {
106
+ try {
107
+ const targetPath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
108
+ const outputString = `export default ${JSON.stringify(content, null, 2)};\n`;
109
+ fs.writeFileSync(targetPath, outputString, 'utf8');
110
+ } catch (error) {
111
+ console.error(`[@cocreate/config] Failed to save configuration at ${filePath}:`, error.message);
112
+ }
113
+ }
114
+
115
+ // Register standard Node.js server/Deno adapter capabilities
116
+ registerAdapter({
117
+ isBrowser: false,
118
+ getLocalStorageItem: () => null,
119
+ setLocalStorageItem: () => {},
120
+ removeLocalStorageItem: () => {},
121
+ clearLocalStorage: () => {},
122
+
123
+ getGlobalConfig: async () => {
124
+ const globalConfigPath = path.resolve(os.homedir(), 'CoCreate.config.js');
125
+ return await loadConfig(globalConfigPath);
126
+ },
127
+
128
+ setGlobalConfig: async (config) => {
129
+ const globalConfigPath = path.resolve(os.homedir(), 'CoCreate.config.js');
130
+ await saveConfig(globalConfigPath, config);
131
+ },
132
+
133
+ getEnv: (key) => {
134
+ return process.env[key] !== undefined ? process.env[key] : null;
135
+ },
136
+
137
+ setEnv: (key, value) => {
138
+ process.env[key] = value;
139
+ },
140
+
141
+ removeEnv: (key) => {
142
+ delete process.env[key];
143
+ },
144
+
145
+ getGitBranch,
146
+ prompt: cliPrompt,
147
+ loadConfig,
148
+ saveConfig
149
+ });
150
+
151
+ export default configAPI;
package/src/client.js DELETED
@@ -1,60 +0,0 @@
1
- import localStorage from '@cocreate/local-storage'
2
-
3
- // Function to get a value by key from localStorage or cookies
4
- function get(key) {
5
- if (window.CoCreateConfig && window.CoCreateConfig[key])
6
- return window.CoCreateConfig[key]
7
-
8
- // try localStorage first
9
- let value = localStorage.getItem(key)
10
- if (value !== null && value !== undefined) return value
11
-
12
- // fallback to cookie for server-read or legacy support
13
- const cookies = (typeof document !== 'undefined' && document.cookie) ? document.cookie.split('; ') : [];
14
- for (const cookie of cookies) {
15
- const [cookieName, cookieValue] = cookie.split('=');
16
- if (cookieName === key) {
17
- // treat empty value as not present
18
- if (!cookieValue || cookieValue === '') return null;
19
- return decodeURIComponent(cookieValue || '');
20
- }
21
- }
22
- return null
23
- }
24
-
25
- // Function to set a key-value pair in localStorage or cookies
26
- function set(key, value) {
27
- if (!window.CoCreateConfig)
28
- window.CoCreateConfig = { [key]: value }
29
- else
30
- window.CoCreateConfig[key] = value
31
- localStorage.setItem(key, value)
32
- // also set cookie for server-read or legacy support
33
- try {
34
- let cookie = key + "=" + encodeURIComponent(value) + "; path=/; SameSite=Lax";
35
- if (typeof window !== "undefined" && window.location && window.location.protocol === "https:") {
36
- cookie += "; Secure";
37
- }
38
- document.cookie = cookie;
39
- } catch (e) {
40
- // ignore if document not available (e.g., SSR)
41
- }
42
- }
43
-
44
- // Function to delete a key-value pair from localStorage or cookies
45
- function remove(key) {
46
- // remove from in-memory config if present
47
- if (window.CoCreateConfig) delete window.CoCreateConfig[key]
48
- // remove from localStorage
49
- localStorage.removeItem(key)
50
- // remove cookie as well
51
- try {
52
- const secure = (typeof window !== "undefined" && window.location && window.location.protocol === "https:") ? "; Secure" : "";
53
- // overwrite with empty value first
54
- document.cookie = key + "=; path=/; SameSite=Lax" + secure;
55
- // then expire using Max-Age=0 for reliable deletion
56
- document.cookie = key + "=; Max-Age=0; path=/; SameSite=Lax" + secure;
57
- } catch (e) {}
58
- }
59
-
60
- export default { get, set, remove }