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.
Files changed (2) hide show
  1. package/bin/teebot.js +218 -0
  2. 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
+ }