free-coding-models 0.3.17 → 0.3.19
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/CHANGELOG.md +26 -0
- package/README.md +11 -1
- package/package.json +1 -1
- package/src/app.js +106 -3
- package/src/command-palette.js +170 -0
- package/src/config.js +3 -3
- package/src/key-handler.js +492 -142
- package/src/openclaw.js +39 -5
- package/src/opencode.js +2 -1
- package/src/overlays.js +426 -208
- package/src/render-helpers.js +1 -1
- package/src/render-table.js +141 -177
- package/src/theme.js +294 -43
- package/src/tier-colors.js +15 -17
- package/src/tool-bootstrap.js +310 -0
- package/src/tool-launchers.js +12 -7
- package/src/ui-config.js +24 -31
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/tool-bootstrap.js
|
|
3
|
+
* @description Shared detection and auto-install helpers for external coding tools launched by FCM.
|
|
4
|
+
*
|
|
5
|
+
* @details
|
|
6
|
+
* 📖 This module answers three operational questions for every supported launcher:
|
|
7
|
+
* - which executable should exist locally before FCM can launch the tool
|
|
8
|
+
* - how to detect that executable without spawning the full TUI/CLI itself
|
|
9
|
+
* - which official install command FCM can offer when the tool is missing
|
|
10
|
+
*
|
|
11
|
+
* 📖 The goal is deliberately narrow. We only solve the "binary missing" bootstrap
|
|
12
|
+
* path so the main TUI can keep the user's selected model, offer a tiny Yes/No
|
|
13
|
+
* confirmation overlay, then continue with the existing config-write + launch flow.
|
|
14
|
+
*
|
|
15
|
+
* 📖 FCM prefers npm when a tool officially supports it because the binary usually
|
|
16
|
+
* lands in a predictable global location and works immediately in the same session.
|
|
17
|
+
* For tools that do not have a maintained npm install path, we use the official
|
|
18
|
+
* installer script documented by the tool itself.
|
|
19
|
+
*
|
|
20
|
+
* @functions
|
|
21
|
+
* → `getToolBootstrapMeta` — static metadata for one tool mode
|
|
22
|
+
* → `resolveToolBinaryPath` — find a launcher executable in PATH or common user bin dirs
|
|
23
|
+
* → `isToolInstalled` — quick boolean wrapper around binary resolution
|
|
24
|
+
* → `getToolInstallPlan` — pick the platform-specific install command for a tool
|
|
25
|
+
* → `installToolWithPlan` — execute the chosen install command with inherited stdio
|
|
26
|
+
*
|
|
27
|
+
* @exports
|
|
28
|
+
* TOOL_BOOTSTRAP_METADATA, getToolBootstrapMeta, resolveToolBinaryPath,
|
|
29
|
+
* isToolInstalled, getToolInstallPlan, installToolWithPlan
|
|
30
|
+
*
|
|
31
|
+
* @see src/key-handler.js
|
|
32
|
+
* @see src/tool-launchers.js
|
|
33
|
+
* @see src/opencode.js
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { spawn } from 'child_process'
|
|
37
|
+
import { existsSync, statSync } from 'fs'
|
|
38
|
+
import { homedir } from 'os'
|
|
39
|
+
import { delimiter, extname, join } from 'path'
|
|
40
|
+
import { isWindows } from './provider-metadata.js'
|
|
41
|
+
|
|
42
|
+
const HOME = homedir()
|
|
43
|
+
|
|
44
|
+
// 📖 Common user-level binary directories that installer scripts frequently use.
|
|
45
|
+
// 📖 We search them in addition to PATH so FCM can keep going right after install
|
|
46
|
+
// 📖 even if the user's shell profile has not been reloaded yet.
|
|
47
|
+
const COMMON_USER_BIN_DIRS = isWindows
|
|
48
|
+
? [
|
|
49
|
+
join(HOME, '.local', 'bin'),
|
|
50
|
+
join(HOME, 'AppData', 'Roaming', 'npm'),
|
|
51
|
+
join(HOME, 'scoop', 'shims'),
|
|
52
|
+
]
|
|
53
|
+
: [
|
|
54
|
+
join(HOME, '.local', 'bin'),
|
|
55
|
+
join(HOME, '.bun', 'bin'),
|
|
56
|
+
join(HOME, '.npm-global', 'bin'),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
function uniqueStrings(values = []) {
|
|
60
|
+
return [...new Set(values.filter((value) => typeof value === 'string' && value.trim()))]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function pathEntries(env = process.env) {
|
|
64
|
+
return uniqueStrings([
|
|
65
|
+
...(String(env.PATH || '').split(delimiter)),
|
|
66
|
+
...(env.npm_config_prefix ? [isWindows ? env.npm_config_prefix : join(env.npm_config_prefix, 'bin')] : []),
|
|
67
|
+
...COMMON_USER_BIN_DIRS,
|
|
68
|
+
])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function executableSuffixes(env = process.env) {
|
|
72
|
+
if (!isWindows) return ['']
|
|
73
|
+
const raw = String(env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
|
|
74
|
+
return uniqueStrings(raw.split(';').flatMap((ext) => {
|
|
75
|
+
const normalized = ext.trim()
|
|
76
|
+
if (!normalized) return []
|
|
77
|
+
return [normalized.toLowerCase(), normalized.toUpperCase()]
|
|
78
|
+
}))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isRunnableFile(filePath) {
|
|
82
|
+
try {
|
|
83
|
+
return existsSync(filePath) && statSync(filePath).isFile()
|
|
84
|
+
} catch {
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolveBinaryPath(binaryName, env = process.env) {
|
|
90
|
+
if (!binaryName || typeof binaryName !== 'string') return null
|
|
91
|
+
|
|
92
|
+
const entries = pathEntries(env)
|
|
93
|
+
if (entries.length === 0) return null
|
|
94
|
+
|
|
95
|
+
const hasExtension = extname(binaryName).length > 0
|
|
96
|
+
const suffixes = isWindows
|
|
97
|
+
? (hasExtension ? [''] : executableSuffixes(env))
|
|
98
|
+
: ['']
|
|
99
|
+
|
|
100
|
+
for (const dir of entries) {
|
|
101
|
+
for (const suffix of suffixes) {
|
|
102
|
+
const candidate = join(dir, `${binaryName}${suffix}`)
|
|
103
|
+
if (isRunnableFile(candidate)) return candidate
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const TOOL_BOOTSTRAP_METADATA = {
|
|
111
|
+
opencode: {
|
|
112
|
+
binary: 'opencode',
|
|
113
|
+
docsUrl: 'https://opencode.ai/download',
|
|
114
|
+
install: {
|
|
115
|
+
default: {
|
|
116
|
+
shellCommand: 'npm install -g opencode-ai',
|
|
117
|
+
summary: 'Install OpenCode CLI globally via npm.',
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
'opencode-desktop': {
|
|
122
|
+
binary: null,
|
|
123
|
+
docsUrl: 'https://opencode.ai/download',
|
|
124
|
+
installUnsupported: {
|
|
125
|
+
default: 'OpenCode Desktop uses platform-specific app installers, so FCM does not auto-install it yet.',
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
openclaw: {
|
|
129
|
+
binary: 'openclaw',
|
|
130
|
+
docsUrl: 'https://docs.openclaw.ai/install',
|
|
131
|
+
install: {
|
|
132
|
+
default: {
|
|
133
|
+
shellCommand: 'npm install -g openclaw@latest',
|
|
134
|
+
summary: 'Install OpenClaw globally via npm.',
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
crush: {
|
|
139
|
+
binary: 'crush',
|
|
140
|
+
docsUrl: 'https://github.com/charmbracelet/crush',
|
|
141
|
+
install: {
|
|
142
|
+
default: {
|
|
143
|
+
shellCommand: 'npm install -g @charmland/crush',
|
|
144
|
+
summary: 'Install Crush globally via npm.',
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
goose: {
|
|
149
|
+
binary: 'goose',
|
|
150
|
+
docsUrl: 'https://block.github.io/goose/docs/getting-started/installation/',
|
|
151
|
+
install: {
|
|
152
|
+
default: {
|
|
153
|
+
shellCommand: 'curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash',
|
|
154
|
+
summary: 'Install goose CLI with the official installer script.',
|
|
155
|
+
},
|
|
156
|
+
win32: {
|
|
157
|
+
shellCommand: 'powershell -NoProfile -ExecutionPolicy Bypass -Command "Invoke-WebRequest -Uri https://raw.githubusercontent.com/block/goose/main/download_cli.ps1 -OutFile $env:TEMP\\download_cli.ps1; & $env:TEMP\\download_cli.ps1"',
|
|
158
|
+
summary: 'Install goose CLI with the official PowerShell installer.',
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
aider: {
|
|
163
|
+
binary: 'aider',
|
|
164
|
+
docsUrl: 'https://aider.chat/docs/install.html',
|
|
165
|
+
install: {
|
|
166
|
+
default: {
|
|
167
|
+
shellCommand: 'curl -LsSf https://aider.chat/install.sh | sh',
|
|
168
|
+
summary: 'Install aider with the official installer.',
|
|
169
|
+
},
|
|
170
|
+
win32: {
|
|
171
|
+
shellCommand: 'powershell -ExecutionPolicy ByPass -c "irm https://aider.chat/install.ps1 | iex"',
|
|
172
|
+
summary: 'Install aider with the official PowerShell installer.',
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
qwen: {
|
|
177
|
+
binary: 'qwen',
|
|
178
|
+
docsUrl: 'https://qwenlm.github.io/qwen-code-docs/en/users/quickstart/',
|
|
179
|
+
install: {
|
|
180
|
+
default: {
|
|
181
|
+
shellCommand: 'npm install -g @qwen-code/qwen-code@latest',
|
|
182
|
+
summary: 'Install Qwen Code globally via npm.',
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
openhands: {
|
|
187
|
+
binary: 'openhands',
|
|
188
|
+
docsUrl: 'https://docs.openhands.dev/openhands/usage/cli/installation',
|
|
189
|
+
install: {
|
|
190
|
+
default: {
|
|
191
|
+
shellCommand: 'curl -fsSL https://install.openhands.dev/install.sh | sh',
|
|
192
|
+
summary: 'Install OpenHands CLI with the official installer.',
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
installUnsupported: {
|
|
196
|
+
win32: 'OpenHands CLI currently recommends installation inside WSL on Windows.',
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
amp: {
|
|
200
|
+
binary: 'amp',
|
|
201
|
+
docsUrl: 'https://ampcode.com/manual',
|
|
202
|
+
install: {
|
|
203
|
+
default: {
|
|
204
|
+
shellCommand: 'npm install -g @sourcegraph/amp',
|
|
205
|
+
summary: 'Install Amp globally via npm.',
|
|
206
|
+
note: 'Amp documents npm as a fallback install path. Its plugin API works best with the binary installer from ampcode.com/install.',
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
pi: {
|
|
211
|
+
binary: 'pi',
|
|
212
|
+
docsUrl: 'https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/README.md',
|
|
213
|
+
install: {
|
|
214
|
+
default: {
|
|
215
|
+
shellCommand: 'npm install -g @mariozechner/pi-coding-agent',
|
|
216
|
+
summary: 'Install Pi Coding Agent globally via npm.',
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function getToolBootstrapMeta(mode) {
|
|
223
|
+
return TOOL_BOOTSTRAP_METADATA[mode] || null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function resolveToolBinaryPath(mode, options = {}) {
|
|
227
|
+
const meta = getToolBootstrapMeta(mode)
|
|
228
|
+
if (!meta?.binary) return null
|
|
229
|
+
return resolveBinaryPath(meta.binary, options.env || process.env)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function isToolInstalled(mode, options = {}) {
|
|
233
|
+
return Boolean(resolveToolBinaryPath(mode, options))
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function getToolInstallPlan(mode, options = {}) {
|
|
237
|
+
const meta = getToolBootstrapMeta(mode)
|
|
238
|
+
const platform = options.platform || process.platform
|
|
239
|
+
|
|
240
|
+
if (!meta) {
|
|
241
|
+
return {
|
|
242
|
+
supported: false,
|
|
243
|
+
mode,
|
|
244
|
+
binary: null,
|
|
245
|
+
docsUrl: null,
|
|
246
|
+
reason: `Unknown tool mode: ${mode}`,
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const platformUnsupportedReason = meta.installUnsupported?.[platform]
|
|
251
|
+
if (platformUnsupportedReason) {
|
|
252
|
+
return {
|
|
253
|
+
supported: false,
|
|
254
|
+
mode,
|
|
255
|
+
binary: meta.binary || null,
|
|
256
|
+
docsUrl: meta.docsUrl || null,
|
|
257
|
+
reason: platformUnsupportedReason,
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const installPlan = meta.install?.[platform] || meta.install?.default || null
|
|
262
|
+
if (!installPlan) {
|
|
263
|
+
return {
|
|
264
|
+
supported: false,
|
|
265
|
+
mode,
|
|
266
|
+
binary: meta.binary || null,
|
|
267
|
+
docsUrl: meta.docsUrl || null,
|
|
268
|
+
reason: meta.installUnsupported?.default || 'No auto-install plan is available for this tool on the current platform.',
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
supported: true,
|
|
274
|
+
mode,
|
|
275
|
+
binary: meta.binary || null,
|
|
276
|
+
docsUrl: meta.docsUrl || null,
|
|
277
|
+
shellCommand: installPlan.shellCommand,
|
|
278
|
+
summary: installPlan.summary,
|
|
279
|
+
note: installPlan.note || null,
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function installToolWithPlan(plan, options = {}) {
|
|
284
|
+
return new Promise((resolve, reject) => {
|
|
285
|
+
if (!plan?.supported || !plan.shellCommand) {
|
|
286
|
+
resolve({
|
|
287
|
+
ok: false,
|
|
288
|
+
exitCode: 1,
|
|
289
|
+
command: plan?.shellCommand || null,
|
|
290
|
+
})
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const child = spawn(plan.shellCommand, [], {
|
|
295
|
+
stdio: 'inherit',
|
|
296
|
+
shell: true,
|
|
297
|
+
env: { ...process.env, ...(options.env || {}) },
|
|
298
|
+
cwd: options.cwd || process.cwd(),
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
child.on('exit', (code) => {
|
|
302
|
+
resolve({
|
|
303
|
+
ok: code === 0,
|
|
304
|
+
exitCode: typeof code === 'number' ? code : 1,
|
|
305
|
+
command: plan.shellCommand,
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
child.on('error', reject)
|
|
309
|
+
})
|
|
310
|
+
}
|
package/src/tool-launchers.js
CHANGED
|
@@ -43,6 +43,7 @@ import { getApiKey } from './config.js'
|
|
|
43
43
|
import { ENV_VAR_NAMES, isWindows } from './provider-metadata.js'
|
|
44
44
|
import { getToolMeta } from './tool-metadata.js'
|
|
45
45
|
import { PROVIDER_METADATA } from './provider-metadata.js'
|
|
46
|
+
import { resolveToolBinaryPath } from './tool-bootstrap.js'
|
|
46
47
|
|
|
47
48
|
const OPENAI_COMPAT_ENV_KEYS = [
|
|
48
49
|
'OPENAI_API_KEY',
|
|
@@ -127,6 +128,10 @@ function applyOpenAiCompatEnv(env, apiKey, baseUrl, modelId) {
|
|
|
127
128
|
return env
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
function resolveLaunchCommand(mode, fallbackCommand) {
|
|
132
|
+
return resolveToolBinaryPath(mode) || fallbackCommand
|
|
133
|
+
}
|
|
134
|
+
|
|
130
135
|
/**
|
|
131
136
|
* 📖 resolveLauncherModelId returns the provider-native id used by the direct
|
|
132
137
|
* 📖 launchers. Legacy bridge-specific model remapping has been removed.
|
|
@@ -562,35 +567,35 @@ export async function startExternalTool(mode, model, config) {
|
|
|
562
567
|
printConfigArtifacts(meta.label, launchPlan.configArtifacts)
|
|
563
568
|
|
|
564
569
|
if (mode === 'aider') {
|
|
565
|
-
return spawnCommand(launchPlan.command, launchPlan.args, launchPlan.env)
|
|
570
|
+
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
566
571
|
}
|
|
567
572
|
|
|
568
573
|
if (mode === 'crush') {
|
|
569
574
|
console.log(chalk.dim(' 📖 Crush will use the provider directly for this launch.'))
|
|
570
|
-
return spawnCommand(launchPlan.command, launchPlan.args, launchPlan.env)
|
|
575
|
+
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
571
576
|
}
|
|
572
577
|
|
|
573
578
|
if (mode === 'goose') {
|
|
574
|
-
return spawnCommand(launchPlan.command, launchPlan.args, launchPlan.env)
|
|
579
|
+
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
575
580
|
}
|
|
576
581
|
|
|
577
582
|
if (mode === 'qwen') {
|
|
578
|
-
return spawnCommand(launchPlan.command, launchPlan.args, launchPlan.env)
|
|
583
|
+
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
579
584
|
}
|
|
580
585
|
|
|
581
586
|
if (mode === 'openhands') {
|
|
582
587
|
console.log(chalk.dim(` 📖 OpenHands launched with model: ${model.modelId}`))
|
|
583
|
-
return spawnCommand(launchPlan.command, launchPlan.args, launchPlan.env)
|
|
588
|
+
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
584
589
|
}
|
|
585
590
|
|
|
586
591
|
if (mode === 'amp') {
|
|
587
592
|
console.log(chalk.dim(` 📖 Amp config updated with model: ${model.modelId}`))
|
|
588
|
-
return spawnCommand(launchPlan.command, launchPlan.args, launchPlan.env)
|
|
593
|
+
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
589
594
|
}
|
|
590
595
|
|
|
591
596
|
if (mode === 'pi') {
|
|
592
597
|
// 📖 Pi supports --provider and --model flags for guaranteed auto-selection
|
|
593
|
-
return spawnCommand(launchPlan.command, launchPlan.args, launchPlan.env)
|
|
598
|
+
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
594
599
|
}
|
|
595
600
|
|
|
596
601
|
console.log(chalk.red(` X Unsupported external tool mode: ${mode}`))
|
package/src/ui-config.js
CHANGED
|
@@ -1,49 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file ui-config.js
|
|
3
|
-
* @description Central configuration for TUI
|
|
3
|
+
* @description Central configuration helpers for TUI separators and spacing.
|
|
4
4
|
*
|
|
5
5
|
* @details
|
|
6
|
-
* This module centralizes
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* This module centralizes the shared table separators used by the TUI. The
|
|
7
|
+
* theme can change at runtime, so separators must be generated lazily instead
|
|
8
|
+
* of frozen once at import time.
|
|
9
9
|
*
|
|
10
10
|
* 📖 Configuration:
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
13
|
-
* -
|
|
14
|
-
* - HORIZONTAL_STYLE: Style of horizontal lines
|
|
15
|
-
* - COLUMN_SPACING: Space between columns
|
|
11
|
+
* - `getVerticalSeparator()` — theme-aware vertical divider
|
|
12
|
+
* - `getHorizontalLine()` — theme-aware horizontal divider
|
|
13
|
+
* - `getColumnSpacing()` — formatted spacing wrapper around the divider
|
|
16
14
|
*
|
|
17
15
|
* @see render-table.js - uses these constants for rendering
|
|
18
16
|
* @see tier-colors.js - for tier-specific color definitions
|
|
19
17
|
*/
|
|
20
18
|
|
|
21
|
-
import
|
|
19
|
+
import { themeColors } from './theme.js'
|
|
22
20
|
|
|
23
|
-
// 📖 Column separator
|
|
24
|
-
|
|
25
|
-
export
|
|
26
|
-
|
|
21
|
+
// 📖 Column separator stays subtle so it improves scanability without turning
|
|
22
|
+
// 📖 the table into a bright fence.
|
|
23
|
+
export function getVerticalSeparator() {
|
|
24
|
+
return themeColors.border('│')
|
|
25
|
+
}
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
export const HORIZONTAL_LINE = chalk[HORIZONTAL_STYLE](HORIZONTAL_SEPARATOR);
|
|
27
|
+
export function getHorizontalLine() {
|
|
28
|
+
return themeColors.dim('─')
|
|
29
|
+
}
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
export function getColumnSpacing() {
|
|
32
|
+
return ` ${getVerticalSeparator()} `
|
|
33
|
+
}
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
export const TABLE_PADDING = 1; // Padding around table edges
|
|
35
|
+
export const TABLE_PADDING = 1
|
|
38
36
|
|
|
39
|
-
// 📖 Export all constants for easy import
|
|
40
37
|
export default {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
HORIZONTAL_SEPARATOR,
|
|
45
|
-
HORIZONTAL_STYLE,
|
|
46
|
-
HORIZONTAL_LINE,
|
|
47
|
-
COLUMN_SPACING,
|
|
38
|
+
getVerticalSeparator,
|
|
39
|
+
getHorizontalLine,
|
|
40
|
+
getColumnSpacing,
|
|
48
41
|
TABLE_PADDING
|
|
49
|
-
}
|
|
42
|
+
}
|