@cocreate/config 1.14.1 → 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/.github/workflows/automated.yml +1 -1
- package/CHANGELOG.md +7 -0
- package/package.json +2 -10
- package/release.config.js +1 -1
- package/src/browser.js +122 -0
- package/src/core.js +282 -0
- package/src/server.js +151 -235
- package/src/client.js +0 -60
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [1.14.2](https://github.com/CoCreate-app/CoCreate-config/compare/v1.14.1...v1.14.2) (2026-07-19)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* set default branch to main ([d5b6372](https://github.com/CoCreate-app/CoCreate-config/commit/d5b63720d80bccff5dcd532eddbc943ce23f657b))
|
|
7
|
+
|
|
1
8
|
## [1.14.1](https://github.com/CoCreate-app/CoCreate-config/compare/v1.14.0...v1.14.1) (2026-07-17)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cocreate/config",
|
|
3
|
-
"version": "1.14.
|
|
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/
|
|
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/release.config.js
CHANGED
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)
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
import
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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 }
|