launchr-cli 1.0.0 → 1.1.1

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.
@@ -0,0 +1,503 @@
1
+ :root {
2
+ --bg: #091018;
3
+ --bg-soft: #101b26;
4
+ --surface: rgba(20, 31, 43, 0.85);
5
+ --surface-strong: rgba(27, 42, 58, 0.95);
6
+ --text: #f0f4f8;
7
+ --text-muted: #b2c0cf;
8
+ --line: rgba(156, 176, 197, 0.22);
9
+ --accent: #f59e0b;
10
+ --accent-strong: #f97316;
11
+ --accent-cool: #22d3ee;
12
+ --success: #10b981;
13
+ --shadow: 0 18px 40px rgba(5, 8, 12, 0.42);
14
+ --radius: 16px;
15
+ --font-display: "Space Grotesk", "Avenir Next", "Segoe UI", sans-serif;
16
+ --font-body: "IBM Plex Sans", "Avenir", "Helvetica Neue", sans-serif;
17
+ --font-mono: "IBM Plex Mono", "SFMono-Regular", Menlo, Monaco, Consolas, monospace;
18
+ }
19
+
20
+ *,
21
+ *::before,
22
+ *::after {
23
+ box-sizing: border-box;
24
+ }
25
+
26
+ html {
27
+ scroll-behavior: smooth;
28
+ }
29
+
30
+ body {
31
+ margin: 0;
32
+ font-family: var(--font-body);
33
+ color: var(--text);
34
+ background: linear-gradient(160deg, #0a1118 0%, #132232 48%, #0d1823 100%);
35
+ line-height: 1.55;
36
+ }
37
+
38
+ .page-backdrop {
39
+ position: fixed;
40
+ inset: 0;
41
+ z-index: -1;
42
+ background:
43
+ radial-gradient(circle at 15% 15%, rgba(245, 158, 11, 0.2) 0, transparent 40%),
44
+ radial-gradient(circle at 85% 0, rgba(34, 211, 238, 0.2) 0, transparent 40%),
45
+ repeating-linear-gradient(
46
+ 115deg,
47
+ rgba(156, 176, 197, 0.05),
48
+ rgba(156, 176, 197, 0.05) 1px,
49
+ transparent 1px,
50
+ transparent 28px
51
+ );
52
+ }
53
+
54
+ .container {
55
+ width: min(1120px, 92vw);
56
+ margin: 0 auto;
57
+ }
58
+
59
+ .topbar {
60
+ position: sticky;
61
+ top: 0;
62
+ z-index: 20;
63
+ backdrop-filter: blur(12px);
64
+ background: rgba(9, 16, 24, 0.7);
65
+ border-bottom: 1px solid var(--line);
66
+ }
67
+
68
+ .topbar-inner {
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: space-between;
72
+ gap: 1rem;
73
+ padding: 0.95rem 0;
74
+ }
75
+
76
+ .brand {
77
+ display: inline-flex;
78
+ align-items: center;
79
+ gap: 0.6rem;
80
+ font-family: var(--font-display);
81
+ color: var(--text);
82
+ text-decoration: none;
83
+ font-weight: 700;
84
+ letter-spacing: 0.02em;
85
+ }
86
+
87
+ .brand img {
88
+ width: 32px;
89
+ height: 32px;
90
+ object-fit: contain;
91
+ }
92
+
93
+ .nav-links {
94
+ display: flex;
95
+ gap: 1rem;
96
+ flex-wrap: wrap;
97
+ }
98
+
99
+ .nav-links a {
100
+ color: var(--text-muted);
101
+ text-decoration: none;
102
+ font-size: 0.95rem;
103
+ }
104
+
105
+ .nav-links a:hover,
106
+ .nav-links a:focus-visible {
107
+ color: var(--text);
108
+ }
109
+
110
+ .github-link {
111
+ display: inline-flex;
112
+ align-items: center;
113
+ gap: 0.36rem;
114
+ border: 1px solid var(--line);
115
+ border-radius: 999px;
116
+ padding: 0.24rem 0.62rem;
117
+ background: rgba(240, 244, 248, 0.05);
118
+ }
119
+
120
+ .hero {
121
+ display: grid;
122
+ grid-template-columns: 1.25fr 1fr;
123
+ gap: 2.2rem;
124
+ align-items: center;
125
+ padding: 4.7rem 0 3.2rem;
126
+ }
127
+
128
+ .eyebrow {
129
+ margin: 0 0 0.65rem;
130
+ color: var(--accent-cool);
131
+ text-transform: uppercase;
132
+ letter-spacing: 0.13em;
133
+ font-size: 0.8rem;
134
+ font-weight: 600;
135
+ }
136
+
137
+ h1,
138
+ h2,
139
+ h3 {
140
+ font-family: var(--font-display);
141
+ margin: 0 0 0.55rem;
142
+ line-height: 1.15;
143
+ }
144
+
145
+ h1 {
146
+ font-size: clamp(2rem, 4vw, 3.4rem);
147
+ max-width: 18ch;
148
+ }
149
+
150
+ h2 {
151
+ font-size: clamp(1.45rem, 2.8vw, 2.15rem);
152
+ }
153
+
154
+ h3 {
155
+ font-size: 1.2rem;
156
+ }
157
+
158
+ .hero-lead {
159
+ margin: 1rem 0 1.4rem;
160
+ color: var(--text-muted);
161
+ max-width: 60ch;
162
+ }
163
+
164
+ .hero-actions {
165
+ display: flex;
166
+ gap: 0.8rem;
167
+ flex-wrap: wrap;
168
+ }
169
+
170
+ .button {
171
+ display: inline-flex;
172
+ align-items: center;
173
+ justify-content: center;
174
+ border-radius: 999px;
175
+ padding: 0.75rem 1.2rem;
176
+ text-decoration: none;
177
+ font-family: var(--font-display);
178
+ font-weight: 600;
179
+ font-size: 0.92rem;
180
+ transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
181
+ }
182
+
183
+ .button:hover,
184
+ .button:focus-visible {
185
+ transform: translateY(-1px);
186
+ }
187
+
188
+ .button-primary {
189
+ background: linear-gradient(90deg, var(--accent), var(--accent-strong));
190
+ color: #1f1605;
191
+ box-shadow: 0 8px 24px rgba(249, 115, 22, 0.3);
192
+ }
193
+
194
+ .button-secondary {
195
+ border: 1px solid var(--line);
196
+ color: var(--text);
197
+ background: rgba(240, 244, 248, 0.04);
198
+ }
199
+
200
+ .hero-bullets {
201
+ margin: 1.35rem 0 0;
202
+ padding-left: 1.15rem;
203
+ color: var(--text-muted);
204
+ }
205
+
206
+ .hero-bullets li + li {
207
+ margin-top: 0.4rem;
208
+ }
209
+
210
+ .hero-panel {
211
+ background: linear-gradient(140deg, rgba(24, 39, 55, 0.86), rgba(13, 26, 39, 0.95));
212
+ border: 1px solid var(--line);
213
+ box-shadow: var(--shadow);
214
+ border-radius: var(--radius);
215
+ padding: 1.3rem;
216
+ }
217
+
218
+ .hero-panel p {
219
+ margin: 0.3rem 0 1rem;
220
+ color: var(--text-muted);
221
+ }
222
+
223
+ .terminal {
224
+ display: grid;
225
+ gap: 0.65rem;
226
+ }
227
+
228
+ .terminal-line {
229
+ display: flex;
230
+ align-items: center;
231
+ gap: 0.55rem;
232
+ border-radius: 12px;
233
+ background: #050b12;
234
+ border: 1px solid rgba(156, 176, 197, 0.22);
235
+ padding: 0.52rem 0.55rem 0.52rem 0.75rem;
236
+ }
237
+
238
+ code,
239
+ pre {
240
+ font-family: var(--font-mono);
241
+ }
242
+
243
+ code {
244
+ color: #cffafe;
245
+ }
246
+
247
+ pre {
248
+ margin: 0;
249
+ overflow-x: auto;
250
+ }
251
+
252
+ button {
253
+ border: 1px solid rgba(34, 211, 238, 0.4);
254
+ background: rgba(34, 211, 238, 0.08);
255
+ color: #d5f6ff;
256
+ border-radius: 9px;
257
+ padding: 0.36rem 0.66rem;
258
+ font-size: 0.8rem;
259
+ cursor: pointer;
260
+ }
261
+
262
+ button:hover,
263
+ button:focus-visible {
264
+ background: rgba(34, 211, 238, 0.18);
265
+ }
266
+
267
+ .section {
268
+ padding: 2.4rem 0;
269
+ }
270
+
271
+ .section-compact {
272
+ padding-bottom: 4.4rem;
273
+ }
274
+
275
+ .section-head {
276
+ margin-bottom: 1.1rem;
277
+ }
278
+
279
+ .section-copy {
280
+ margin-top: 0;
281
+ margin-bottom: 1rem;
282
+ color: var(--text-muted);
283
+ }
284
+
285
+ .grid {
286
+ display: grid;
287
+ gap: 1rem;
288
+ }
289
+
290
+ .grid-2 {
291
+ grid-template-columns: repeat(2, minmax(0, 1fr));
292
+ }
293
+
294
+ .grid-3 {
295
+ grid-template-columns: repeat(3, minmax(0, 1fr));
296
+ }
297
+
298
+ .card,
299
+ .code-card,
300
+ .timeline-item,
301
+ .table-wrap {
302
+ background: var(--surface);
303
+ border: 1px solid var(--line);
304
+ border-radius: var(--radius);
305
+ }
306
+
307
+ .card,
308
+ .timeline-item {
309
+ padding: 1rem 1rem 1.05rem;
310
+ }
311
+
312
+ .card p {
313
+ margin: 0.25rem 0 0;
314
+ color: var(--text-muted);
315
+ }
316
+
317
+ .step {
318
+ margin: 0;
319
+ color: var(--success);
320
+ letter-spacing: 0.1em;
321
+ font-size: 0.78rem;
322
+ font-weight: 700;
323
+ }
324
+
325
+ .small-copy {
326
+ margin-top: 0.7rem;
327
+ }
328
+
329
+ .table-wrap {
330
+ overflow-x: auto;
331
+ }
332
+
333
+ table {
334
+ width: 100%;
335
+ border-collapse: collapse;
336
+ }
337
+
338
+ th,
339
+ td {
340
+ text-align: left;
341
+ padding: 0.78rem 0.95rem;
342
+ border-bottom: 1px solid rgba(156, 176, 197, 0.18);
343
+ vertical-align: top;
344
+ }
345
+
346
+ th {
347
+ font-size: 0.86rem;
348
+ text-transform: uppercase;
349
+ letter-spacing: 0.08em;
350
+ color: #f8b84e;
351
+ }
352
+
353
+ td {
354
+ color: var(--text-muted);
355
+ }
356
+
357
+ .code-card {
358
+ padding: 1rem;
359
+ background: rgba(13, 24, 35, 0.95);
360
+ }
361
+
362
+ .code-title {
363
+ margin-bottom: 0.7rem;
364
+ font-family: var(--font-display);
365
+ color: #f8b84e;
366
+ }
367
+
368
+ .timeline {
369
+ display: grid;
370
+ gap: 0.8rem;
371
+ }
372
+
373
+ .timeline-version {
374
+ margin: 0 0 0.35rem;
375
+ color: #f8b84e;
376
+ font-weight: 700;
377
+ }
378
+
379
+ .timeline-item ul {
380
+ margin: 0.5rem 0 0;
381
+ padding-left: 1.1rem;
382
+ color: var(--text-muted);
383
+ }
384
+
385
+ .timeline-item li + li {
386
+ margin-top: 0.35rem;
387
+ }
388
+
389
+ .footer {
390
+ border-top: 1px solid var(--line);
391
+ background: rgba(9, 16, 24, 0.75);
392
+ }
393
+
394
+ .footer-inner {
395
+ display: flex;
396
+ align-items: center;
397
+ justify-content: space-between;
398
+ gap: 1rem;
399
+ padding: 1.15rem 0 1.35rem;
400
+ color: var(--text-muted);
401
+ font-size: 0.9rem;
402
+ }
403
+
404
+ .footer-github {
405
+ color: var(--text);
406
+ text-decoration: none;
407
+ }
408
+
409
+ .footer-github:hover,
410
+ .footer-github:focus-visible {
411
+ color: var(--accent-cool);
412
+ }
413
+
414
+ [data-animate] {
415
+ opacity: 0;
416
+ transform: translateY(20px);
417
+ animation: rise-in 0.68s cubic-bezier(0.2, 0.86, 0.31, 1) forwards;
418
+ }
419
+
420
+ [data-animate="2"] {
421
+ animation-delay: 0.08s;
422
+ }
423
+
424
+ [data-animate="3"] {
425
+ animation-delay: 0.14s;
426
+ }
427
+
428
+ [data-animate="4"] {
429
+ animation-delay: 0.2s;
430
+ }
431
+
432
+ [data-animate="5"] {
433
+ animation-delay: 0.26s;
434
+ }
435
+
436
+ [data-animate="6"] {
437
+ animation-delay: 0.32s;
438
+ }
439
+
440
+ [data-animate="7"] {
441
+ animation-delay: 0.38s;
442
+ }
443
+
444
+ @keyframes rise-in {
445
+ from {
446
+ opacity: 0;
447
+ transform: translateY(20px);
448
+ }
449
+ to {
450
+ opacity: 1;
451
+ transform: translateY(0);
452
+ }
453
+ }
454
+
455
+ @media (max-width: 980px) {
456
+ .hero {
457
+ grid-template-columns: 1fr;
458
+ padding-top: 3.4rem;
459
+ }
460
+
461
+ .grid-3 {
462
+ grid-template-columns: 1fr;
463
+ }
464
+
465
+ .grid-2 {
466
+ grid-template-columns: 1fr;
467
+ }
468
+
469
+ .nav-links {
470
+ gap: 0.65rem;
471
+ justify-content: flex-end;
472
+ }
473
+ }
474
+
475
+ @media (max-width: 640px) {
476
+ .topbar-inner {
477
+ flex-direction: column;
478
+ align-items: flex-start;
479
+ gap: 0.6rem;
480
+ }
481
+
482
+ .nav-links {
483
+ justify-content: flex-start;
484
+ }
485
+
486
+ .footer-inner {
487
+ flex-direction: column;
488
+ align-items: flex-start;
489
+ }
490
+
491
+ .hero-actions {
492
+ width: 100%;
493
+ }
494
+
495
+ .button {
496
+ width: 100%;
497
+ }
498
+
499
+ th,
500
+ td {
501
+ padding: 0.68rem 0.62rem;
502
+ }
503
+ }
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "launchr-cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Config-driven URL launcher CLI built with Node.js + zx",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/volkanto/launchr"
8
+ },
5
9
  "type": "module",
6
10
  "bin": {
7
11
  "launchr": "./src/cli.mjs"
package/src/cli.mjs CHANGED
@@ -19,6 +19,8 @@ import { promptYesNo, createPrompter } from "./utils/prompt.mjs";
19
19
  import { UsageError } from "./utils/errors.mjs";
20
20
  import { openInBrowser } from "./utils/browser.mjs";
21
21
 
22
+ const INIT_DEPRECATION_MESSAGE = '"init" is deprecated. Use "launchr add".';
23
+
22
24
  function writeLine(stream, message = "") {
23
25
  stream.write(`${message}\n`);
24
26
  }
@@ -60,7 +62,11 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
60
62
  return 0;
61
63
  }
62
64
 
63
- if (parsed.command === "init") {
65
+ if (parsed.command === "add" || parsed.command === "init") {
66
+ if (parsed.command === "init") {
67
+ writeLine(stderr, INIT_DEPRECATION_MESSAGE);
68
+ }
69
+
64
70
  const prompter = createPrompterFn({ input, output: stdout });
65
71
  try {
66
72
  config = await runInitFlow({
@@ -1,7 +1,7 @@
1
1
  const BUILTIN_COMMANDS = [
2
2
  { name: "help", description: "Show manual" },
3
3
  { name: "list", description: "List available commands" },
4
- { name: "init", description: "Interactive setup" },
4
+ { name: "add", description: "Add command interactively" },
5
5
  ];
6
6
 
7
7
  function formatRequired(required) {
@@ -63,7 +63,8 @@ export function buildGeneralHelp(config = {}) {
63
63
  usageText,
64
64
  "",
65
65
  "How to define commands:",
66
- " Run `launchr init` and follow prompts to create command metadata, URL templates, and parameters.",
66
+ " Run `launchr add` and follow prompts to create command metadata, URL templates, and parameters.",
67
+ " `launchr init` is deprecated in v1.x and will be removed in v2.0.0.",
67
68
  "",
68
69
  "How parameters work:",
69
70
  " Each parameter has a type, a short flag, required/default rules, and optional allowed values.",
@@ -1,7 +1,7 @@
1
1
  export function buildCommandList(config = {}) {
2
2
  const entries = Object.entries(config);
3
3
  if (entries.length === 0) {
4
- return "No commands configured yet. Run `launchr init` to add commands.";
4
+ return "No commands configured yet. Run `launchr add` to add commands.";
5
5
  }
6
6
 
7
7
  return entries
package/src/constants.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const BUILTIN_COMMANDS = ["help", "list", "init"];
1
+ export const BUILTIN_COMMANDS = ["help", "list", "add"];
2
2
 
3
3
  export const PARAMETER_TYPES = [
4
4
  "string",
@@ -17,6 +17,26 @@ function createMemoryStream() {
17
17
  };
18
18
  }
19
19
 
20
+ function createScriptedPrompter(answers) {
21
+ const queue = [...answers];
22
+ let closed = false;
23
+ return {
24
+ async ask() {
25
+ if (queue.length === 0) {
26
+ throw new Error("No more scripted answers");
27
+ }
28
+ return queue.shift();
29
+ },
30
+ write() {},
31
+ close() {
32
+ closed = true;
33
+ },
34
+ isClosed() {
35
+ return closed;
36
+ },
37
+ };
38
+ }
39
+
20
40
  function buildSampleConfig() {
21
41
  return {
22
42
  grafana: {
@@ -89,12 +109,72 @@ test("runCli with no command prints custom commands with descriptions", async ()
89
109
  assert.match(stdout.toString(), /Custom Commands:/);
90
110
  assert.match(stdout.toString(), /^\s{2}help\s{2,}Show manual$/m);
91
111
  assert.match(stdout.toString(), /^\s{2}list\s{2,}List available commands$/m);
92
- assert.match(stdout.toString(), /^\s{2}init\s{2,}Interactive setup$/m);
112
+ assert.match(stdout.toString(), /^\s{2}add\s{2,}Add command interactively$/m);
93
113
  assert.match(stdout.toString(), /^\s{2}grafana\s{2,}some useful information$/m);
94
114
  assert.match(stdout.toString(), /^\s{2}yutup\s{2,}youtube ac$/m);
95
115
  assert.equal(stderr.toString(), "");
96
116
  });
97
117
 
118
+ test("runCli add creates command via interactive flow", async () => {
119
+ const homeDir = await createHomeWithConfig({});
120
+ const stdout = createMemoryStream();
121
+ const stderr = createMemoryStream();
122
+ const prompter = createScriptedPrompter([
123
+ "google",
124
+ "search utility",
125
+ "https://google.com?q={query}",
126
+ "query",
127
+ "string",
128
+ "q",
129
+ "true",
130
+ "",
131
+ "done",
132
+ "no",
133
+ ]);
134
+
135
+ const code = await runCli(["add"], {
136
+ homeDir,
137
+ stdout,
138
+ stderr,
139
+ createPrompterFn: () => prompter,
140
+ });
141
+
142
+ assert.equal(code, 0);
143
+ assert.match(stdout.toString(), /google\s+- search utility/);
144
+ assert.equal(stderr.toString(), "");
145
+ assert.equal(prompter.isClosed(), true);
146
+ });
147
+
148
+ test("runCli init remains alias for add and prints deprecation warning", async () => {
149
+ const homeDir = await createHomeWithConfig({});
150
+ const stdout = createMemoryStream();
151
+ const stderr = createMemoryStream();
152
+ const prompter = createScriptedPrompter([
153
+ "google",
154
+ "search utility",
155
+ "https://google.com?q={query}",
156
+ "query",
157
+ "string",
158
+ "q",
159
+ "true",
160
+ "",
161
+ "done",
162
+ "no",
163
+ ]);
164
+
165
+ const code = await runCli(["init"], {
166
+ homeDir,
167
+ stdout,
168
+ stderr,
169
+ createPrompterFn: () => prompter,
170
+ });
171
+
172
+ assert.equal(code, 0);
173
+ assert.match(stderr.toString(), /"init" is deprecated\. Use "launchr add"\./);
174
+ assert.match(stdout.toString(), /google\s+- search utility/);
175
+ assert.equal(prompter.isClosed(), true);
176
+ });
177
+
98
178
  test("runCli custom command validates params and opens URL", async () => {
99
179
  const homeDir = await createHomeWithConfig(buildSampleConfig());
100
180
  const stdout = createMemoryStream();
@@ -1,11 +0,0 @@
1
- # launchr v1.0.0
2
-
3
- First stable release of `launchr`, a config-driven CLI to build URLs from typed flags and open them in your default browser.
4
-
5
- ## Highlights
6
-
7
- - Interactive setup with `launchr init` to create custom commands and URL templates.
8
- - Typed parameter support: `string`, `integer`, `boolean`, `single-choice-list`.
9
- - Runtime checks for required params, unknown flags, invalid values, and URL placeholders.
10
- - URL interpolation with encoding plus cross-platform browser launch (`open` / `cmd start` / `xdg-open`).
11
- - Automated npm publish workflow with test gate and version/tag verification.