teebot 1.0.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/teebot.js +218 -0
- package/package.json +19 -0
package/bin/teebot.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFile } from 'node:child_process'
|
|
4
|
+
|
|
5
|
+
export const VERSION = '1.0.0'
|
|
6
|
+
|
|
7
|
+
const VALID_COLORS = ['black', 'white', 'navy', 'grey', 'red', 'asphalt', 'forest', 'gold', 'mauve', 'royal']
|
|
8
|
+
const VALID_FONTS = ['mono', 'sans', 'serif', 'slab', 'hand']
|
|
9
|
+
const VALID_ALIGNS = ['left', 'center', 'right']
|
|
10
|
+
|
|
11
|
+
const COLOR_ALIASES = {
|
|
12
|
+
purple: 'mauve', violet: 'mauve', lavender: 'mauve',
|
|
13
|
+
blue: 'royal', cobalt: 'royal', azure: 'royal',
|
|
14
|
+
green: 'forest', olive: 'forest', sage: 'forest',
|
|
15
|
+
yellow: 'gold', mustard: 'gold', amber: 'gold',
|
|
16
|
+
gray: 'grey', charcoal: 'grey', slate: 'grey',
|
|
17
|
+
pink: 'mauve', rose: 'mauve', coral: 'mauve',
|
|
18
|
+
brown: 'asphalt', tan: 'asphalt', khaki: 'asphalt',
|
|
19
|
+
maroon: 'red', burgundy: 'red', crimson: 'red', wine: 'red',
|
|
20
|
+
dark: 'navy', midnight: 'navy',
|
|
21
|
+
cream: 'white', ivory: 'white', beige: 'white',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveColor(input) {
|
|
25
|
+
const lower = input.toLowerCase()
|
|
26
|
+
if (VALID_COLORS.includes(lower)) return { color: lower, error: null }
|
|
27
|
+
if (COLOR_ALIASES[lower]) return { color: COLOR_ALIASES[lower], error: null }
|
|
28
|
+
return {
|
|
29
|
+
color: null,
|
|
30
|
+
error: `No match for "${input}". Did you mean one of: ${VALID_COLORS.join(', ')}?`,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function parseArgs(args) {
|
|
35
|
+
const result = {
|
|
36
|
+
text: null,
|
|
37
|
+
color: 'black',
|
|
38
|
+
font: 'mono',
|
|
39
|
+
align: 'center',
|
|
40
|
+
noOpen: false,
|
|
41
|
+
help: false,
|
|
42
|
+
version: false,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let i = 0
|
|
46
|
+
while (i < args.length) {
|
|
47
|
+
const arg = args[i]
|
|
48
|
+
if (arg === '--help') { result.help = true; i++; continue }
|
|
49
|
+
if (arg === '--version') { result.version = true; i++; continue }
|
|
50
|
+
if (arg === '--no-open') { result.noOpen = true; i++; continue }
|
|
51
|
+
if (arg === '--color' && i + 1 < args.length) { result.color = args[++i]; i++; continue }
|
|
52
|
+
if (arg === '--font' && i + 1 < args.length) { result.font = args[++i]; i++; continue }
|
|
53
|
+
if (arg === '--align' && i + 1 < args.length) { result.align = args[++i]; i++; continue }
|
|
54
|
+
if (arg.startsWith('--')) { i++; if (i < args.length && !args[i].startsWith('--')) i++; continue }
|
|
55
|
+
if (result.text === null) result.text = arg
|
|
56
|
+
i++
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Map script -> hand
|
|
60
|
+
if (result.font === 'script') result.font = 'hand'
|
|
61
|
+
|
|
62
|
+
// Replace literal \n with newlines
|
|
63
|
+
if (result.text) result.text = result.text.replace(/\\n/g, '\n')
|
|
64
|
+
|
|
65
|
+
return result
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const BODY_WIDTH = 16
|
|
69
|
+
const BODY_ROWS = 5
|
|
70
|
+
const WRAP_WIDTH = 13
|
|
71
|
+
|
|
72
|
+
export function wrapText(text, maxWidth) {
|
|
73
|
+
const lines = []
|
|
74
|
+
for (const inputLine of text.split('\n')) {
|
|
75
|
+
const words = inputLine.split(/\s+/).filter(Boolean)
|
|
76
|
+
if (words.length === 0) { lines.push(''); continue }
|
|
77
|
+
let current = words[0]
|
|
78
|
+
for (let i = 1; i < words.length; i++) {
|
|
79
|
+
if (current.length + 1 + words[i].length <= maxWidth) {
|
|
80
|
+
current += ' ' + words[i]
|
|
81
|
+
} else {
|
|
82
|
+
lines.push(current)
|
|
83
|
+
current = words[i]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
lines.push(current)
|
|
87
|
+
}
|
|
88
|
+
return lines
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function centerPad(text, width) {
|
|
92
|
+
const pad = Math.max(0, width - text.length)
|
|
93
|
+
const left = Math.floor(pad / 2)
|
|
94
|
+
const right = pad - left
|
|
95
|
+
return ' '.repeat(left) + text + ' '.repeat(right)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function renderShirt(text) {
|
|
99
|
+
const wrapped = wrapText(text, WRAP_WIDTH)
|
|
100
|
+
const textLines = wrapped.slice(0, BODY_ROWS)
|
|
101
|
+
|
|
102
|
+
// Pad to fill body rows
|
|
103
|
+
while (textLines.length < BODY_ROWS) textLines.push('')
|
|
104
|
+
|
|
105
|
+
const shirt = [
|
|
106
|
+
' ┌──────┐',
|
|
107
|
+
' ╭────┤ ├────╮',
|
|
108
|
+
' ╱ ╲ ╱ ╲',
|
|
109
|
+
' ╱ ╲ ╱ ╲',
|
|
110
|
+
' ╱ ╲╱ ╲',
|
|
111
|
+
' │ │',
|
|
112
|
+
' ╰──╮ ╭──╯',
|
|
113
|
+
...textLines.map(line => ' │' + centerPad(line, BODY_WIDTH) + '│'),
|
|
114
|
+
' ╰────────────────╯',
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
return shirt.join('\n')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function buildUrl({ text, color, font, align }) {
|
|
121
|
+
const params = new URLSearchParams()
|
|
122
|
+
params.set('text', text)
|
|
123
|
+
if (color !== 'black') params.set('color', color)
|
|
124
|
+
if (font !== 'mono') params.set('font', font)
|
|
125
|
+
if (align === 'left' || align === 'right') params.set('align', align)
|
|
126
|
+
params.set('utm_source', 'cli')
|
|
127
|
+
return 'https://teebot.dev/?' + params.toString()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function openBrowser(url) {
|
|
131
|
+
const cmd = process.platform === 'darwin' ? 'open'
|
|
132
|
+
: process.platform === 'win32' ? 'start'
|
|
133
|
+
: 'xdg-open'
|
|
134
|
+
execFile(cmd, [url])
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function printHelp() {
|
|
138
|
+
console.log(`
|
|
139
|
+
teebot.dev - text on tees, fast
|
|
140
|
+
|
|
141
|
+
Usage:
|
|
142
|
+
npx teebot "YOUR TEXT HERE" [options]
|
|
143
|
+
|
|
144
|
+
Options:
|
|
145
|
+
--color <color> Shirt color (black, white, navy, grey, red, asphalt,
|
|
146
|
+
forest, gold, mauve, royal)
|
|
147
|
+
--font <font> Font (mono, sans, serif, slab, script)
|
|
148
|
+
--align <align> Text alignment (left, center, right)
|
|
149
|
+
--no-open Print URL only, don't open browser
|
|
150
|
+
--version Print version
|
|
151
|
+
--help Show this help
|
|
152
|
+
|
|
153
|
+
Examples:
|
|
154
|
+
npx teebot "DEPLOY ON FRIDAYS"
|
|
155
|
+
npx teebot "IT WORKS ON MY MACHINE" --color navy --font slab
|
|
156
|
+
npx teebot "// TODO: fix later" --no-open
|
|
157
|
+
`)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function printUsage() {
|
|
161
|
+
console.log(`
|
|
162
|
+
teebot.dev - text on tees, fast
|
|
163
|
+
|
|
164
|
+
Usage: npx teebot "YOUR TEXT HERE" [options]
|
|
165
|
+
Run npx teebot --help for more info.
|
|
166
|
+
`)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function main() {
|
|
170
|
+
const args = parseArgs(process.argv.slice(2))
|
|
171
|
+
|
|
172
|
+
if (args.version) {
|
|
173
|
+
console.log(VERSION)
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (args.help) {
|
|
178
|
+
printHelp()
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!args.text) {
|
|
183
|
+
printUsage()
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Resolve color
|
|
188
|
+
const { color, error } = resolveColor(args.color)
|
|
189
|
+
if (error) {
|
|
190
|
+
console.log('\n ' + error + '\n')
|
|
191
|
+
process.exitCode = 1
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const url = buildUrl({ text: args.text, color, font: args.font, align: args.align })
|
|
196
|
+
const shirt = renderShirt(args.text)
|
|
197
|
+
|
|
198
|
+
console.log('')
|
|
199
|
+
console.log(' teebot.dev - text on tees, fast')
|
|
200
|
+
console.log('')
|
|
201
|
+
console.log(shirt)
|
|
202
|
+
console.log('')
|
|
203
|
+
console.log(` Color: ${color} | Font: ${args.font === 'hand' ? 'script' : args.font}`)
|
|
204
|
+
console.log('')
|
|
205
|
+
if (!args.noOpen) {
|
|
206
|
+
console.log(' Opening your design...')
|
|
207
|
+
openBrowser(url)
|
|
208
|
+
}
|
|
209
|
+
console.log(' ' + url)
|
|
210
|
+
console.log('')
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Only run when executed directly (not imported for tests)
|
|
214
|
+
const isDirectRun = process.argv[1] && import.meta.url === `file://${process.argv[1]}`
|
|
215
|
+
|| process.argv[1]?.endsWith('teebot')
|
|
216
|
+
if (isDirectRun) {
|
|
217
|
+
main()
|
|
218
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "teebot",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Design a shirt from your terminal",
|
|
6
|
+
"bin": {
|
|
7
|
+
"teebot": "./bin/teebot.js"
|
|
8
|
+
},
|
|
9
|
+
"files": ["bin"],
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["tshirt", "custom", "cli", "teebot", "terminal"],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/foxpress-design/teebot.dev"
|
|
18
|
+
}
|
|
19
|
+
}
|