spiralcord-full 2.5.0 → 2.6.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/bin/spiral.js +47 -3
- package/package.json +5 -4
- package/src/onboard/config-scanner.js +92 -0
- package/src/onboard/index.js +370 -0
- package/src/onboard/prompts.js +20 -0
package/bin/spiral.js
CHANGED
|
@@ -131,9 +131,33 @@ async function loadConfig() {
|
|
|
131
131
|
return null;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
if (!config.token || config.token === '') {
|
|
135
|
-
console.error('
|
|
136
|
-
|
|
134
|
+
if (!config.token || config.token === '' || config.token === 'YOUR_DISCORD_TOKEN_HERE' || (typeof config.token === 'string' && config.token.length < 50)) {
|
|
135
|
+
console.error('[runtime] Bot token is missing or invalid.');
|
|
136
|
+
|
|
137
|
+
const { password, isCancel, intro, outro, log } = require('@clack/prompts');
|
|
138
|
+
const { saveSpiralConfig } = require('../src/onboard/config-scanner');
|
|
139
|
+
|
|
140
|
+
intro('Token Setup');
|
|
141
|
+
|
|
142
|
+
const token = await password({
|
|
143
|
+
message: 'Enter your bot token:',
|
|
144
|
+
mask: '▪',
|
|
145
|
+
validate(value) {
|
|
146
|
+
if (!value || value.length < 50) return 'Token seems too short. Discord tokens are 70+ characters.';
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (isCancel(token)) {
|
|
151
|
+
console.error('Operation cancelled.');
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
config.token = token;
|
|
156
|
+
await saveSpiralConfig(configPath, config);
|
|
157
|
+
log.success('Token saved to spiral.json');
|
|
158
|
+
|
|
159
|
+
outro('Token configured. Restarting...');
|
|
160
|
+
return config;
|
|
137
161
|
}
|
|
138
162
|
|
|
139
163
|
if (!Array.isArray(config.intents)) {
|
|
@@ -767,4 +791,24 @@ program
|
|
|
767
791
|
}
|
|
768
792
|
});
|
|
769
793
|
|
|
794
|
+
program
|
|
795
|
+
.command('onboard')
|
|
796
|
+
.description('Interactive bot setup wizard')
|
|
797
|
+
.action(async () => {
|
|
798
|
+
const { onboard } = require('../src/onboard');
|
|
799
|
+
const configPath = path.join(process.cwd(), 'spiral.json');
|
|
800
|
+
const pluginsDir = path.join(process.cwd(), 'plugins');
|
|
801
|
+
await onboard(configPath, pluginsDir);
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
program
|
|
805
|
+
.command('add <field>')
|
|
806
|
+
.description('Add or edit a field in spiral.json')
|
|
807
|
+
.option('--value <value>', 'Value to set (non-interactive)')
|
|
808
|
+
.action(async (field, options) => {
|
|
809
|
+
const { addField } = require('../src/onboard');
|
|
810
|
+
const configPath = path.join(process.cwd(), 'spiral.json');
|
|
811
|
+
await addField(configPath, field, options.value);
|
|
812
|
+
});
|
|
813
|
+
|
|
770
814
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spiralcord-full",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Spiralcord Full - Discord Bot Runtime with Voice, Music, AI, Database & Image Support",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/types/index.d.ts",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"database",
|
|
25
25
|
"canvas"
|
|
26
26
|
],
|
|
27
|
-
"author": "",
|
|
28
|
-
"license": "
|
|
27
|
+
"author": "Isam Ahmed",
|
|
28
|
+
"license": "custom",
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"commander": "^12.1.0",
|
|
31
31
|
"discord.js": "^14.16.3",
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"chalk": "^4.1.2",
|
|
46
46
|
"ms": "^2.1.3",
|
|
47
47
|
"uuid": "^9.0.0",
|
|
48
|
-
"chokidar": "^3.6.0"
|
|
48
|
+
"chokidar": "^3.6.0",
|
|
49
|
+
"@clack/prompts": "^1.7.0"
|
|
49
50
|
},
|
|
50
51
|
"engines": {
|
|
51
52
|
"node": ">=16.0.0"
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const SPIRAL_FIELDS = [
|
|
5
|
+
{ key: 'name', type: 'string', description: 'Bot display name', default: 'My Spiral Bot' },
|
|
6
|
+
{ key: 'token', type: 'password', description: 'Discord bot token', default: '' },
|
|
7
|
+
{ key: 'prefix', type: 'string', description: 'Command prefix', default: '!' },
|
|
8
|
+
{ key: 'clientId', type: 'string', description: 'Discord application client ID', default: '' },
|
|
9
|
+
{ key: 'intents', type: 'multiselect', description: 'Gateway intents', default: ['Guilds', 'GuildMessages', 'MessageContent'] },
|
|
10
|
+
{ key: 'perGuildSlash', type: 'confirm', description: 'Register slash commands per-guild (instant vs ~1h)', default: false },
|
|
11
|
+
{ key: 'disabledCommands', type: 'text', description: 'Commands to disable (comma-separated)', default: [] }
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const ALL_INTENTS = [
|
|
15
|
+
'Guilds', 'GuildMembers', 'GuildModeration', 'GuildEmojisAndStickers',
|
|
16
|
+
'GuildIntegrations', 'GuildWebhooks', 'GuildInvites', 'GuildVoiceStates',
|
|
17
|
+
'GuildPresences', 'GuildMessages', 'GuildMessageReactions', 'GuildMessageTyping',
|
|
18
|
+
'DirectMessages', 'DirectMessageReactions', 'DirectMessageTyping', 'MessageContent',
|
|
19
|
+
'GuildScheduledEvents', 'AutoModerationConfiguration', 'AutoModerationExecution',
|
|
20
|
+
'GuildMessagePolls', 'UserThreads'
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
async function loadSpiralConfig(configPath) {
|
|
24
|
+
try {
|
|
25
|
+
const data = await fs.readFile(configPath, 'utf-8');
|
|
26
|
+
return JSON.parse(data);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function saveSpiralConfig(configPath, config) {
|
|
33
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function scanPluginConfigs(pluginsDir) {
|
|
37
|
+
const fields = [];
|
|
38
|
+
|
|
39
|
+
let plugins;
|
|
40
|
+
try {
|
|
41
|
+
plugins = await fs.readdir(pluginsDir);
|
|
42
|
+
} catch {
|
|
43
|
+
return fields;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const plugin of plugins) {
|
|
47
|
+
const schemaPath = path.join(pluginsDir, plugin, 'config.schema.json');
|
|
48
|
+
try {
|
|
49
|
+
const data = await fs.readFile(schemaPath, 'utf-8');
|
|
50
|
+
const schema = JSON.parse(data);
|
|
51
|
+
if (!schema.properties) continue;
|
|
52
|
+
|
|
53
|
+
const pluginFields = [];
|
|
54
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
55
|
+
pluginFields.push({
|
|
56
|
+
plugin,
|
|
57
|
+
key,
|
|
58
|
+
type: prop.type === 'boolean' ? 'confirm' :
|
|
59
|
+
prop.type === 'number' ? 'number' :
|
|
60
|
+
prop.enum ? 'select' :
|
|
61
|
+
'text',
|
|
62
|
+
description: prop.description || key,
|
|
63
|
+
default: prop.default,
|
|
64
|
+
enum: prop.enum
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (pluginFields.length > 0) {
|
|
69
|
+
fields.push({ plugin, fields: pluginFields });
|
|
70
|
+
}
|
|
71
|
+
} catch {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return fields;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function hasTokenIssue(config) {
|
|
78
|
+
if (!config) return true;
|
|
79
|
+
if (!config.token || config.token === '') return true;
|
|
80
|
+
if (config.token === 'YOUR_DISCORD_TOKEN_HERE') return true;
|
|
81
|
+
if (typeof config.token === 'string' && config.token.length < 50) return true;
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
SPIRAL_FIELDS,
|
|
87
|
+
ALL_INTENTS,
|
|
88
|
+
loadSpiralConfig,
|
|
89
|
+
saveSpiralConfig,
|
|
90
|
+
scanPluginConfigs,
|
|
91
|
+
hasTokenIssue
|
|
92
|
+
};
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const {
|
|
3
|
+
intro, outro, checkCancel,
|
|
4
|
+
text, password, confirm, select, selectKey,
|
|
5
|
+
multiselect, spinner, log, note
|
|
6
|
+
} = require('./prompts');
|
|
7
|
+
const {
|
|
8
|
+
SPIRAL_FIELDS, ALL_INTENTS,
|
|
9
|
+
loadSpiralConfig, saveSpiralConfig,
|
|
10
|
+
scanPluginConfigs, hasTokenIssue
|
|
11
|
+
} = require('./config-scanner');
|
|
12
|
+
|
|
13
|
+
async function onboard(configPath, pluginsDir) {
|
|
14
|
+
const existing = await loadSpiralConfig(configPath);
|
|
15
|
+
|
|
16
|
+
if (!existing) {
|
|
17
|
+
await fullWizard(configPath, pluginsDir, null);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
intro('Spiralcord Onboard');
|
|
22
|
+
|
|
23
|
+
const action = await selectKey({
|
|
24
|
+
message: 'What would you like to do?',
|
|
25
|
+
options: [
|
|
26
|
+
{ value: '1', label: 'Set up new bot', hint: 'Full onboarding wizard' },
|
|
27
|
+
{ value: '2', label: 'Modify config', hint: 'Edit existing settings' },
|
|
28
|
+
{ value: '3', label: 'Re-onboard', hint: 'Start fresh' }
|
|
29
|
+
]
|
|
30
|
+
});
|
|
31
|
+
checkCancel(action);
|
|
32
|
+
|
|
33
|
+
switch (action) {
|
|
34
|
+
case '1':
|
|
35
|
+
await fullWizard(configPath, pluginsDir, null);
|
|
36
|
+
break;
|
|
37
|
+
case '2':
|
|
38
|
+
await modifyWizard(configPath, pluginsDir, existing);
|
|
39
|
+
break;
|
|
40
|
+
case '3':
|
|
41
|
+
const yes = await confirm({ message: 'Wipe current config and start fresh?' });
|
|
42
|
+
checkCancel(yes);
|
|
43
|
+
if (yes) {
|
|
44
|
+
await fullWizard(configPath, pluginsDir, null);
|
|
45
|
+
} else {
|
|
46
|
+
log.info('Keeping current config.');
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
outro('Done!');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function fullWizard(configPath, pluginsDir, existing) {
|
|
55
|
+
intro('Spiralcord Setup');
|
|
56
|
+
|
|
57
|
+
const config = existing || {};
|
|
58
|
+
|
|
59
|
+
// Step 1: Token
|
|
60
|
+
const token = await password({
|
|
61
|
+
message: 'Bot token:',
|
|
62
|
+
mask: '▪',
|
|
63
|
+
validate(value) {
|
|
64
|
+
if (!value || value.length < 50) return 'Token seems too short. Discord tokens are 70+ characters.';
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
checkCancel(token);
|
|
68
|
+
config.token = token;
|
|
69
|
+
|
|
70
|
+
// Step 2: Bot Name
|
|
71
|
+
const name = await text({
|
|
72
|
+
message: 'Bot name:',
|
|
73
|
+
defaultValue: 'My Spiral Bot',
|
|
74
|
+
initialValue: config.name || 'My Spiral Bot'
|
|
75
|
+
});
|
|
76
|
+
checkCancel(name);
|
|
77
|
+
config.name = name || 'My Spiral Bot';
|
|
78
|
+
|
|
79
|
+
// Step 3: Prefix
|
|
80
|
+
const prefix = await text({
|
|
81
|
+
message: 'Command prefix:',
|
|
82
|
+
defaultValue: '!',
|
|
83
|
+
initialValue: config.prefix || '!'
|
|
84
|
+
});
|
|
85
|
+
checkCancel(prefix);
|
|
86
|
+
config.prefix = prefix || '!';
|
|
87
|
+
|
|
88
|
+
// Step 4: Client ID
|
|
89
|
+
const clientId = await text({
|
|
90
|
+
message: 'Client ID (for slash commands, optional):',
|
|
91
|
+
initialValue: config.clientId || '',
|
|
92
|
+
placeholder: 'Leave empty to skip'
|
|
93
|
+
});
|
|
94
|
+
checkCancel(clientId);
|
|
95
|
+
config.clientId = clientId || '';
|
|
96
|
+
|
|
97
|
+
// Step 5: Intents
|
|
98
|
+
const currentIntents = Array.isArray(config.intents) ? config.intents : ['Guilds', 'GuildMessages', 'MessageContent'];
|
|
99
|
+
const intents = await multiselect({
|
|
100
|
+
message: 'Select gateway intents:',
|
|
101
|
+
options: ALL_INTENTS.map(i => ({
|
|
102
|
+
value: i,
|
|
103
|
+
label: i,
|
|
104
|
+
hint: currentIntents.includes(i) ? 'selected' : undefined
|
|
105
|
+
})),
|
|
106
|
+
initialValues: currentIntents
|
|
107
|
+
});
|
|
108
|
+
checkCancel(intents);
|
|
109
|
+
config.intents = intents;
|
|
110
|
+
|
|
111
|
+
// Step 6: Per-Guild Slash
|
|
112
|
+
const perGuildSlash = await confirm({
|
|
113
|
+
message: 'Register slash commands per-guild? (instant vs ~1h propagation)',
|
|
114
|
+
initialValue: config.perGuildSlash || false
|
|
115
|
+
});
|
|
116
|
+
checkCancel(perGuildSlash);
|
|
117
|
+
config.perGuildSlash = perGuildSlash;
|
|
118
|
+
|
|
119
|
+
// Step 7: Plugin Config
|
|
120
|
+
if (pluginsDir) {
|
|
121
|
+
await pluginConfigWizard(pluginsDir, config);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Step 8: Review
|
|
125
|
+
await reviewAndSave(configPath, config);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function pluginConfigWizard(pluginsDir, config) {
|
|
129
|
+
const pluginConfigs = await scanPluginConfigs(pluginsDir);
|
|
130
|
+
|
|
131
|
+
if (pluginConfigs.length === 0) {
|
|
132
|
+
log.info('No plugin configurations found.');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
note('Configure plugin settings below. Press Esc to skip a field.', 'Plugin Configuration');
|
|
137
|
+
|
|
138
|
+
for (const { plugin, fields } of pluginConfigs) {
|
|
139
|
+
log.step(plugin);
|
|
140
|
+
|
|
141
|
+
for (const field of fields) {
|
|
142
|
+
const fieldKey = `${plugin}.${field.key}`;
|
|
143
|
+
const currentVal = config[fieldKey] !== undefined ? config[fieldKey] : field.default;
|
|
144
|
+
|
|
145
|
+
if (field.type === 'confirm') {
|
|
146
|
+
const val = await confirm({
|
|
147
|
+
message: `${field.key}: ${field.description}`,
|
|
148
|
+
initialValue: currentVal !== undefined ? currentVal : field.default
|
|
149
|
+
});
|
|
150
|
+
if (val !== undefined && val !== null) {
|
|
151
|
+
config[fieldKey] = val;
|
|
152
|
+
}
|
|
153
|
+
} else if (field.type === 'select' && field.enum) {
|
|
154
|
+
const val = await select({
|
|
155
|
+
message: `${field.key}: ${field.description}`,
|
|
156
|
+
options: field.enum.map(e => ({ value: e, label: e })),
|
|
157
|
+
initialValue: currentVal || field.default
|
|
158
|
+
});
|
|
159
|
+
if (val !== undefined) {
|
|
160
|
+
config[fieldKey] = val;
|
|
161
|
+
}
|
|
162
|
+
} else if (field.type === 'number') {
|
|
163
|
+
const val = await text({
|
|
164
|
+
message: `${field.key}: ${field.description} (${field.default !== undefined ? field.default : 'none'})`,
|
|
165
|
+
initialValue: currentVal !== undefined ? String(currentVal) : '',
|
|
166
|
+
placeholder: String(field.default || '')
|
|
167
|
+
});
|
|
168
|
+
if (val !== undefined && val !== '') {
|
|
169
|
+
config[fieldKey] = Number(val);
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
const val = await text({
|
|
173
|
+
message: `${field.key}: ${field.description} (${field.default !== undefined ? field.default : 'none'})`,
|
|
174
|
+
initialValue: currentVal !== undefined ? String(currentVal) : '',
|
|
175
|
+
placeholder: String(field.default || '')
|
|
176
|
+
});
|
|
177
|
+
if (val !== undefined && val !== '') {
|
|
178
|
+
config[fieldKey] = val;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function reviewAndSave(configPath, config) {
|
|
186
|
+
note(formatConfig(config), 'Review Configuration');
|
|
187
|
+
|
|
188
|
+
const save = await confirm({ message: 'Save this configuration?' });
|
|
189
|
+
checkCancel(save);
|
|
190
|
+
|
|
191
|
+
if (save) {
|
|
192
|
+
const s = spinner();
|
|
193
|
+
s.start('Saving configuration...');
|
|
194
|
+
await saveSpiralConfig(configPath, config);
|
|
195
|
+
s.stop('Configuration saved!');
|
|
196
|
+
log.success(`Wrote ${configPath}`);
|
|
197
|
+
} else {
|
|
198
|
+
log.info('Configuration not saved.');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function modifyWizard(configPath, pluginsDir, config) {
|
|
203
|
+
intro('Modify Configuration');
|
|
204
|
+
|
|
205
|
+
const allFields = [...SPIRAL_FIELDS];
|
|
206
|
+
|
|
207
|
+
if (pluginsDir) {
|
|
208
|
+
const pluginConfigs = await scanPluginConfigs(pluginsDir);
|
|
209
|
+
for (const { plugin, fields } of pluginConfigs) {
|
|
210
|
+
for (const field of fields) {
|
|
211
|
+
allFields.push({
|
|
212
|
+
key: `${plugin}.${field.key}`,
|
|
213
|
+
type: field.type,
|
|
214
|
+
description: `[${plugin}] ${field.description}`,
|
|
215
|
+
default: field.default,
|
|
216
|
+
enum: field.enum
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const fieldOptions = allFields.map((f, i) => ({
|
|
223
|
+
value: String(i + 1),
|
|
224
|
+
label: f.key,
|
|
225
|
+
hint: f.description
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
const choice = await selectKey({
|
|
229
|
+
message: 'Select a field to edit:',
|
|
230
|
+
options: fieldOptions
|
|
231
|
+
});
|
|
232
|
+
checkCancel(choice);
|
|
233
|
+
|
|
234
|
+
const idx = parseInt(choice) - 1;
|
|
235
|
+
const field = allFields[idx];
|
|
236
|
+
if (!field) {
|
|
237
|
+
log.error('Invalid selection.');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const currentVal = config[field.key] !== undefined ? config[field.key] : field.default;
|
|
242
|
+
|
|
243
|
+
if (field.type === 'confirm') {
|
|
244
|
+
const val = await confirm({
|
|
245
|
+
message: `${field.key}: ${field.description}`,
|
|
246
|
+
initialValue: currentVal !== undefined ? currentVal : false
|
|
247
|
+
});
|
|
248
|
+
if (val !== undefined && val !== null) {
|
|
249
|
+
config[field.key] = val;
|
|
250
|
+
}
|
|
251
|
+
} else if (field.type === 'password') {
|
|
252
|
+
const val = await password({
|
|
253
|
+
message: `${field.key}: ${field.description}`,
|
|
254
|
+
mask: '▪'
|
|
255
|
+
});
|
|
256
|
+
checkCancel(val);
|
|
257
|
+
config[field.key] = val;
|
|
258
|
+
} else if (field.type === 'select' && field.enum) {
|
|
259
|
+
const val = await select({
|
|
260
|
+
message: `${field.key}: ${field.description}`,
|
|
261
|
+
options: field.enum.map(e => ({ value: e, label: e })),
|
|
262
|
+
initialValue: currentVal || field.default
|
|
263
|
+
});
|
|
264
|
+
if (val !== undefined) {
|
|
265
|
+
config[field.key] = val;
|
|
266
|
+
}
|
|
267
|
+
} else if (field.type === 'multiselect') {
|
|
268
|
+
const val = await multiselect({
|
|
269
|
+
message: `${field.key}: ${field.description}`,
|
|
270
|
+
options: ALL_INTENTS.map(i => ({ value: i, label: i })),
|
|
271
|
+
initialValues: Array.isArray(currentVal) ? currentVal : field.default || []
|
|
272
|
+
});
|
|
273
|
+
if (val !== undefined) {
|
|
274
|
+
config[field.key] = val;
|
|
275
|
+
}
|
|
276
|
+
} else if (field.type === 'number') {
|
|
277
|
+
const val = await text({
|
|
278
|
+
message: `${field.key}: ${field.description}`,
|
|
279
|
+
initialValue: currentVal !== undefined ? String(currentVal) : '',
|
|
280
|
+
placeholder: String(field.default || '')
|
|
281
|
+
});
|
|
282
|
+
if (val !== undefined && val !== '') {
|
|
283
|
+
config[field.key] = Number(val);
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
const val = await text({
|
|
287
|
+
message: `${field.key}: ${field.description}`,
|
|
288
|
+
initialValue: currentVal !== undefined ? String(currentVal) : '',
|
|
289
|
+
placeholder: String(field.default || '')
|
|
290
|
+
});
|
|
291
|
+
if (val !== undefined) {
|
|
292
|
+
config[field.key] = val;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const save = await confirm({ message: 'Save changes?' });
|
|
297
|
+
checkCancel(save);
|
|
298
|
+
|
|
299
|
+
if (save) {
|
|
300
|
+
const s = spinner();
|
|
301
|
+
s.start('Saving...');
|
|
302
|
+
await saveSpiralConfig(configPath, config);
|
|
303
|
+
s.stop('Saved!');
|
|
304
|
+
log.success(`Updated ${configPath}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function addField(configPath, fieldName, fieldValue) {
|
|
309
|
+
const config = await loadSpiralConfig(configPath);
|
|
310
|
+
if (!config) {
|
|
311
|
+
log.error('No spiral.json found. Run "spiral onboard" first.');
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (fieldValue !== undefined) {
|
|
316
|
+
config[fieldName] = parseValue(fieldValue);
|
|
317
|
+
await saveSpiralConfig(configPath, config);
|
|
318
|
+
log.success(`Set ${fieldName} = ${JSON.stringify(config[fieldName])}`);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
intro(`Add / Edit: ${fieldName}`);
|
|
323
|
+
|
|
324
|
+
const current = config[fieldName];
|
|
325
|
+
const initial = current !== undefined ? String(current) : '';
|
|
326
|
+
|
|
327
|
+
const val = await text({
|
|
328
|
+
message: `Enter value for ${fieldName}:`,
|
|
329
|
+
initialValue: initial,
|
|
330
|
+
placeholder: current !== undefined ? String(current) : 'Enter value'
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
if (val === undefined) {
|
|
334
|
+
cancel('Cancelled.');
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
config[fieldName] = val === '' ? initial : parseValue(val);
|
|
339
|
+
await saveSpiralConfig(configPath, config);
|
|
340
|
+
log.success(`Set ${fieldName} = ${JSON.stringify(config[fieldName])}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function parseValue(val) {
|
|
344
|
+
if (val === 'true') return true;
|
|
345
|
+
if (val === 'false') return false;
|
|
346
|
+
if (/^-?\d+(\.\d+)?$/.test(val)) return Number(val);
|
|
347
|
+
try {
|
|
348
|
+
const parsed = JSON.parse(val);
|
|
349
|
+
if (Array.isArray(parsed)) return parsed;
|
|
350
|
+
} catch {}
|
|
351
|
+
return val;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function formatConfig(config) {
|
|
355
|
+
const lines = [];
|
|
356
|
+
for (const [key, val] of Object.entries(config)) {
|
|
357
|
+
if (key === 'token') {
|
|
358
|
+
lines.push(` ${key}: ${val ? '▪▪▪▪▪▪▪▪' + val.slice(-4) : '(empty)'}`);
|
|
359
|
+
} else if (Array.isArray(val)) {
|
|
360
|
+
lines.push(` ${key}: ${val.join(', ')}`);
|
|
361
|
+
} else if (typeof val === 'object' && val !== null) {
|
|
362
|
+
lines.push(` ${key}: ${JSON.stringify(val)}`);
|
|
363
|
+
} else {
|
|
364
|
+
lines.push(` ${key}: ${val}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return lines.join('\n');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
module.exports = { onboard, addField, fullWizard };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const {
|
|
2
|
+
intro, outro, cancel, isCancel,
|
|
3
|
+
text, password, confirm, select, selectKey,
|
|
4
|
+
multiselect, group, spinner, log, note
|
|
5
|
+
} = require('@clack/prompts');
|
|
6
|
+
|
|
7
|
+
function checkCancel(value) {
|
|
8
|
+
if (isCancel(value)) {
|
|
9
|
+
cancel('Operation cancelled.');
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
intro, outro, cancel, isCancel,
|
|
17
|
+
text, password, confirm, select, selectKey,
|
|
18
|
+
multiselect, group, spinner, log, note,
|
|
19
|
+
checkCancel
|
|
20
|
+
};
|