relmio 0.2.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 (39) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/LICENSE +21 -0
  3. package/README.md +122 -0
  4. package/SPEC.md +140 -0
  5. package/docs/architecture.md +127 -0
  6. package/docs/brand.md +43 -0
  7. package/docs/images/brand/relmio-concept-source.png +0 -0
  8. package/docs/images/brand/relmio-mark.svg +16 -0
  9. package/docs/images/setup/01-local-sign-in-ready.png +0 -0
  10. package/docs/images/setup/02-vps-identity-confirmed.png +0 -0
  11. package/docs/images/setup/03-n8n-detected.png +0 -0
  12. package/docs/images/setup/04-install-plan.png +0 -0
  13. package/docs/images/setup/05-bridge-ready.png +0 -0
  14. package/docs/maintenance.md +157 -0
  15. package/docs/manual-install.md +282 -0
  16. package/docs/n8n-configuration.md +199 -0
  17. package/docs/npm-publish.md +285 -0
  18. package/docs/roadmap.md +109 -0
  19. package/docs/security.md +105 -0
  20. package/docs/troubleshooting.md +193 -0
  21. package/docs/video-outline.md +152 -0
  22. package/package.json +45 -0
  23. package/scripts/build-npm-package.js +112 -0
  24. package/scripts/check-release-metadata.js +100 -0
  25. package/scripts/check-syntax.js +59 -0
  26. package/scripts/preview.js +69 -0
  27. package/src/cli.js +57 -0
  28. package/src/domain/safety.js +59 -0
  29. package/src/domain/templates.js +65 -0
  30. package/src/domain/validation.js +76 -0
  31. package/src/infrastructure/ssh.js +268 -0
  32. package/src/services/discovery.js +96 -0
  33. package/src/services/installer.js +239 -0
  34. package/src/services/oauth.js +351 -0
  35. package/src/ui/app.js +526 -0
  36. package/src/ui/index.html +440 -0
  37. package/src/ui/styles.css +645 -0
  38. package/src/ui/time.js +15 -0
  39. package/src/web/server.js +489 -0
@@ -0,0 +1,645 @@
1
+ :root {
2
+ color-scheme: light;
3
+ --background: #f4f2ed;
4
+ --surface: #ffffff;
5
+ --surface-muted: #f8f8f6;
6
+ --text: #12211f;
7
+ --text-muted: #5d6865;
8
+ --border: #d7dcda;
9
+ --border-strong: #aeb8b5;
10
+ --accent: #137c74;
11
+ --accent-hover: #0f625d;
12
+ --accent-soft: #e7f3f0;
13
+ --warning: #8a4b0f;
14
+ --warning-soft: #fff3df;
15
+ --danger: #a22b2b;
16
+ --danger-soft: #fff0f0;
17
+ --success: #17653f;
18
+ --shadow: 0 12px 32px rgb(23 33 31 / 8%);
19
+ --radius: 0.75rem;
20
+ }
21
+
22
+ * {
23
+ box-sizing: border-box;
24
+ }
25
+
26
+ html {
27
+ font-family:
28
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
29
+ sans-serif;
30
+ color: var(--text);
31
+ background: var(--background);
32
+ }
33
+
34
+ body {
35
+ min-width: 20rem;
36
+ margin: 0;
37
+ line-height: 1.5;
38
+ }
39
+
40
+ button,
41
+ input,
42
+ select {
43
+ font: inherit;
44
+ }
45
+
46
+ button,
47
+ select {
48
+ min-height: 2.75rem;
49
+ }
50
+
51
+ button:focus-visible,
52
+ input:focus-visible,
53
+ select:focus-visible,
54
+ a:focus-visible {
55
+ outline: 3px solid #f2a94a;
56
+ outline-offset: 2px;
57
+ }
58
+
59
+ [hidden] {
60
+ display: none !important;
61
+ }
62
+
63
+ .skip-link {
64
+ position: fixed;
65
+ top: 0.5rem;
66
+ left: 0.5rem;
67
+ z-index: 10;
68
+ padding: 0.75rem 1rem;
69
+ color: #fff;
70
+ background: var(--text);
71
+ transform: translateY(-150%);
72
+ }
73
+
74
+ .skip-link:focus {
75
+ transform: translateY(0);
76
+ }
77
+
78
+ .site-header {
79
+ display: flex;
80
+ align-items: center;
81
+ justify-content: space-between;
82
+ min-height: 4rem;
83
+ padding: 0.75rem clamp(1rem, 4vw, 3rem);
84
+ border-bottom: 1px solid var(--border);
85
+ background: rgb(255 255 255 / 85%);
86
+ }
87
+
88
+ .brand {
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 0.625rem;
92
+ font-weight: 750;
93
+ letter-spacing: -0.01em;
94
+ }
95
+
96
+ .brand-mark {
97
+ display: block;
98
+ width: 2rem;
99
+ height: 2rem;
100
+ flex: none;
101
+ }
102
+
103
+ .local-badge {
104
+ padding: 0.375rem 0.625rem;
105
+ border: 1px solid var(--border);
106
+ border-radius: 999px;
107
+ color: var(--text-muted);
108
+ background: var(--surface);
109
+ font-size: 0.75rem;
110
+ font-weight: 650;
111
+ }
112
+
113
+ .shell {
114
+ width: min(100% - 2rem, 54rem);
115
+ margin: 0 auto;
116
+ padding: 3rem 0 5rem;
117
+ }
118
+
119
+ .intro {
120
+ max-width: 44rem;
121
+ }
122
+
123
+ .eyebrow,
124
+ .step-kicker {
125
+ margin: 0 0 0.5rem;
126
+ color: var(--accent);
127
+ font-size: 0.75rem;
128
+ font-weight: 800;
129
+ letter-spacing: 0.09em;
130
+ text-transform: uppercase;
131
+ }
132
+
133
+ h1,
134
+ h2,
135
+ h3,
136
+ p {
137
+ margin-top: 0;
138
+ }
139
+
140
+ h1 {
141
+ max-width: 42rem;
142
+ margin-bottom: 1rem;
143
+ font-size: clamp(2rem, 7vw, 3.5rem);
144
+ line-height: 1.05;
145
+ letter-spacing: -0.045em;
146
+ }
147
+
148
+ h2 {
149
+ margin-bottom: 0.5rem;
150
+ font-size: clamp(1.5rem, 4vw, 2rem);
151
+ line-height: 1.2;
152
+ letter-spacing: -0.025em;
153
+ }
154
+
155
+ h3 {
156
+ margin-bottom: 0.75rem;
157
+ font-size: 1rem;
158
+ }
159
+
160
+ .lede,
161
+ .panel-heading p {
162
+ color: var(--text-muted);
163
+ }
164
+
165
+ .lede {
166
+ max-width: 42rem;
167
+ font-size: 1.0625rem;
168
+ }
169
+
170
+ .safety-note {
171
+ display: grid;
172
+ grid-template-columns: max-content 1fr;
173
+ gap: 0.75rem 1rem;
174
+ margin: 2rem 0;
175
+ padding: 1rem;
176
+ border: 1px solid #b9d9d2;
177
+ border-left: 4px solid var(--accent);
178
+ border-radius: 0.5rem;
179
+ background: var(--accent-soft);
180
+ }
181
+
182
+ .safety-note span {
183
+ color: #355a54;
184
+ }
185
+
186
+ .backup-note {
187
+ border-color: #d7b98d;
188
+ background: var(--warning-soft);
189
+ }
190
+
191
+ .backup-note span {
192
+ color: var(--warning);
193
+ }
194
+
195
+ .steps {
196
+ margin: 2rem 0 1rem;
197
+ }
198
+
199
+ .steps ol {
200
+ display: grid;
201
+ grid-template-columns: repeat(5, 1fr);
202
+ margin: 0;
203
+ padding: 0;
204
+ list-style: none;
205
+ }
206
+
207
+ .steps li {
208
+ position: relative;
209
+ display: grid;
210
+ justify-items: center;
211
+ gap: 0.375rem;
212
+ color: var(--text-muted);
213
+ font-size: 0.75rem;
214
+ text-align: center;
215
+ }
216
+
217
+ .steps li::before {
218
+ position: absolute;
219
+ top: 0.8125rem;
220
+ right: 50%;
221
+ z-index: -1;
222
+ width: 100%;
223
+ height: 2px;
224
+ background: var(--border);
225
+ content: "";
226
+ }
227
+
228
+ .steps li:first-child::before {
229
+ display: none;
230
+ }
231
+
232
+ .steps li span {
233
+ display: grid;
234
+ width: 1.75rem;
235
+ height: 1.75rem;
236
+ place-items: center;
237
+ border: 2px solid var(--border-strong);
238
+ border-radius: 50%;
239
+ background: var(--background);
240
+ font-weight: 800;
241
+ }
242
+
243
+ .steps li em {
244
+ font-style: normal;
245
+ }
246
+
247
+ .steps li[aria-current="step"] {
248
+ color: var(--text);
249
+ }
250
+
251
+ .steps li[aria-current="step"] span,
252
+ .steps li.complete span {
253
+ border-color: var(--accent);
254
+ color: #fff;
255
+ background: var(--accent);
256
+ }
257
+
258
+ .steps li.complete::before,
259
+ .steps li[aria-current="step"]::before {
260
+ background: var(--accent);
261
+ }
262
+
263
+ .message,
264
+ .error {
265
+ min-height: 2.75rem;
266
+ margin: 1rem 0;
267
+ padding: 0.75rem 1rem;
268
+ border-radius: 0.5rem;
269
+ }
270
+
271
+ .message {
272
+ color: var(--text-muted);
273
+ background: var(--surface-muted);
274
+ }
275
+
276
+ .error {
277
+ border: 1px solid #e4abab;
278
+ color: var(--danger);
279
+ background: var(--danger-soft);
280
+ font-weight: 650;
281
+ }
282
+
283
+ .panel {
284
+ padding: clamp(1.25rem, 4vw, 2rem);
285
+ border: 1px solid var(--border);
286
+ border-radius: var(--radius);
287
+ background: var(--surface);
288
+ box-shadow: var(--shadow);
289
+ }
290
+
291
+ .panel-heading {
292
+ margin-bottom: 1.5rem;
293
+ }
294
+
295
+ .panel-heading p {
296
+ max-width: 42rem;
297
+ margin-bottom: 0;
298
+ }
299
+
300
+ .status-row {
301
+ display: flex;
302
+ align-items: flex-start;
303
+ gap: 0.875rem;
304
+ padding: 1rem;
305
+ border: 1px solid var(--border);
306
+ border-radius: 0.5rem;
307
+ background: var(--surface-muted);
308
+ }
309
+
310
+ .status-row p {
311
+ margin: 0.25rem 0 0;
312
+ color: var(--text-muted);
313
+ }
314
+
315
+ .status-row .credential-time {
316
+ color: var(--text);
317
+ font-size: 0.875rem;
318
+ font-weight: 700;
319
+ }
320
+
321
+ .status-dot {
322
+ flex: 0 0 auto;
323
+ width: 0.75rem;
324
+ height: 0.75rem;
325
+ margin-top: 0.375rem;
326
+ border: 2px solid var(--border-strong);
327
+ border-radius: 50%;
328
+ }
329
+
330
+ .status-dot.ready {
331
+ border-color: var(--success);
332
+ background: var(--success);
333
+ }
334
+
335
+ .field-grid {
336
+ display: grid;
337
+ grid-template-columns: minmax(0, 2fr) minmax(7rem, 1fr);
338
+ gap: 1rem;
339
+ margin-bottom: 1rem;
340
+ }
341
+
342
+ .field-grid.single {
343
+ grid-template-columns: 1fr;
344
+ }
345
+
346
+ .field {
347
+ display: grid;
348
+ align-content: start;
349
+ gap: 0.375rem;
350
+ font-weight: 700;
351
+ }
352
+
353
+ .field input,
354
+ .field select {
355
+ width: 100%;
356
+ min-height: 2.75rem;
357
+ padding: 0.625rem 0.75rem;
358
+ border: 1px solid var(--border-strong);
359
+ border-radius: 0.375rem;
360
+ color: var(--text);
361
+ background: #fff;
362
+ }
363
+
364
+ .field input:focus,
365
+ .field select:focus {
366
+ border-color: var(--accent);
367
+ }
368
+
369
+ .field small {
370
+ color: var(--text-muted);
371
+ font-size: 0.75rem;
372
+ font-weight: 450;
373
+ }
374
+
375
+ code {
376
+ padding: 0.1rem 0.3rem;
377
+ border-radius: 0.25rem;
378
+ background: #edf0ef;
379
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
380
+ font-size: 0.875em;
381
+ overflow-wrap: anywhere;
382
+ }
383
+
384
+ .actions {
385
+ display: flex;
386
+ flex-wrap: wrap;
387
+ justify-content: flex-end;
388
+ gap: 0.75rem;
389
+ margin-top: 1.5rem;
390
+ }
391
+
392
+ .button {
393
+ min-height: 2.75rem;
394
+ padding: 0.625rem 1rem;
395
+ border: 1px solid transparent;
396
+ border-radius: 0.375rem;
397
+ font-weight: 750;
398
+ cursor: pointer;
399
+ }
400
+
401
+ .button:disabled {
402
+ cursor: not-allowed;
403
+ opacity: 0.48;
404
+ }
405
+
406
+ .button.primary {
407
+ color: #fff;
408
+ background: var(--accent);
409
+ }
410
+
411
+ .button.primary:not(:disabled):hover {
412
+ background: var(--accent-hover);
413
+ }
414
+
415
+ .button.secondary {
416
+ border-color: var(--border-strong);
417
+ color: var(--text);
418
+ background: var(--surface);
419
+ }
420
+
421
+ .button.secondary:not(:disabled):hover,
422
+ .button.ghost:not(:disabled):hover {
423
+ background: var(--surface-muted);
424
+ }
425
+
426
+ .button.ghost {
427
+ margin-right: auto;
428
+ color: var(--text-muted);
429
+ background: transparent;
430
+ }
431
+
432
+ .fingerprint {
433
+ margin-top: 1rem;
434
+ padding: 1rem;
435
+ border: 1px solid #d7b98d;
436
+ border-radius: 0.5rem;
437
+ background: var(--warning-soft);
438
+ }
439
+
440
+ .fingerprint > span {
441
+ display: block;
442
+ margin-bottom: 0.375rem;
443
+ color: var(--warning);
444
+ font-size: 0.75rem;
445
+ font-weight: 800;
446
+ text-transform: uppercase;
447
+ }
448
+
449
+ .fingerprint > code {
450
+ display: block;
451
+ margin-bottom: 1rem;
452
+ background: #fff;
453
+ }
454
+
455
+ .check-row {
456
+ display: grid;
457
+ grid-template-columns: 1.25rem 1fr;
458
+ gap: 0.625rem;
459
+ align-items: start;
460
+ font-weight: 600;
461
+ cursor: pointer;
462
+ }
463
+
464
+ .check-row input {
465
+ width: 1.125rem;
466
+ height: 1.125rem;
467
+ margin: 0.2rem 0 0;
468
+ }
469
+
470
+ .version-strip {
471
+ display: flex;
472
+ flex-wrap: wrap;
473
+ gap: 0.75rem;
474
+ margin-bottom: 1rem;
475
+ }
476
+
477
+ .version-strip span {
478
+ padding: 0.375rem 0.625rem;
479
+ border: 1px solid var(--border);
480
+ border-radius: 999px;
481
+ background: var(--surface-muted);
482
+ font-size: 0.875rem;
483
+ }
484
+
485
+ .review-grid {
486
+ display: grid;
487
+ grid-template-columns: 1fr 1fr;
488
+ gap: 1rem;
489
+ }
490
+
491
+ .review-grid section {
492
+ padding: 1rem;
493
+ border: 1px solid var(--border);
494
+ border-radius: 0.5rem;
495
+ }
496
+
497
+ .check-list,
498
+ .no-list {
499
+ display: grid;
500
+ gap: 0.625rem;
501
+ margin: 0;
502
+ padding-left: 1.25rem;
503
+ }
504
+
505
+ .check-list li::marker {
506
+ color: var(--success);
507
+ }
508
+
509
+ .no-list li::marker {
510
+ color: var(--danger);
511
+ }
512
+
513
+ .approval {
514
+ margin-top: 1rem;
515
+ padding: 1rem;
516
+ border: 1px solid #d7b98d;
517
+ border-radius: 0.5rem;
518
+ background: var(--warning-soft);
519
+ }
520
+
521
+ .success-panel {
522
+ position: relative;
523
+ }
524
+
525
+ .success-mark {
526
+ display: grid;
527
+ width: 3rem;
528
+ height: 3rem;
529
+ margin-bottom: 1rem;
530
+ place-items: center;
531
+ border-radius: 50%;
532
+ color: #fff;
533
+ background: var(--success);
534
+ font-size: 1.5rem;
535
+ font-weight: 900;
536
+ }
537
+
538
+ .result-list {
539
+ border-top: 1px solid var(--border);
540
+ }
541
+
542
+ .result-list div {
543
+ display: grid;
544
+ grid-template-columns: 9rem 1fr;
545
+ gap: 1rem;
546
+ padding: 0.875rem 0;
547
+ border-bottom: 1px solid var(--border);
548
+ }
549
+
550
+ .result-list dt {
551
+ color: var(--text-muted);
552
+ }
553
+
554
+ .result-list dd {
555
+ margin: 0;
556
+ font-weight: 700;
557
+ }
558
+
559
+ .result-list dd.result-value {
560
+ display: flex;
561
+ flex-wrap: wrap;
562
+ align-items: center;
563
+ gap: 0.75rem;
564
+ }
565
+
566
+ .copy-value {
567
+ min-height: 2rem;
568
+ padding: 0.25rem 0.625rem;
569
+ font-size: 0.8125rem;
570
+ }
571
+
572
+ .model-box,
573
+ .final-note {
574
+ margin-top: 1.5rem;
575
+ padding: 1rem;
576
+ border-radius: 0.5rem;
577
+ background: var(--surface-muted);
578
+ }
579
+
580
+ .model-box p,
581
+ .final-note {
582
+ margin-bottom: 0;
583
+ }
584
+
585
+ footer {
586
+ padding: 1.5rem 1rem;
587
+ border-top: 1px solid var(--border);
588
+ color: var(--text-muted);
589
+ background: var(--surface);
590
+ font-size: 0.8125rem;
591
+ text-align: center;
592
+ }
593
+
594
+ footer p {
595
+ margin: 0;
596
+ }
597
+
598
+ @media (max-width: 42rem) {
599
+ .local-badge {
600
+ display: none;
601
+ }
602
+
603
+ .shell {
604
+ width: min(100% - 1rem, 54rem);
605
+ padding-top: 2rem;
606
+ }
607
+
608
+ .safety-note,
609
+ .field-grid,
610
+ .review-grid {
611
+ grid-template-columns: 1fr;
612
+ }
613
+
614
+ .steps li em {
615
+ font-size: 0.6875rem;
616
+ }
617
+
618
+ .panel {
619
+ border-radius: 0.5rem;
620
+ }
621
+
622
+ .actions {
623
+ flex-direction: column-reverse;
624
+ }
625
+
626
+ .button,
627
+ .button.ghost {
628
+ width: 100%;
629
+ margin-right: 0;
630
+ }
631
+
632
+ .result-list div {
633
+ grid-template-columns: 1fr;
634
+ gap: 0.25rem;
635
+ }
636
+ }
637
+
638
+ @media (prefers-reduced-motion: reduce) {
639
+ *,
640
+ *::before,
641
+ *::after {
642
+ scroll-behavior: auto !important;
643
+ transition-duration: 0.01ms !important;
644
+ }
645
+ }
package/src/ui/time.js ADDED
@@ -0,0 +1,15 @@
1
+ export function formatAuthUpdatedAt(
2
+ value,
3
+ { locale, timeZone } = {},
4
+ ) {
5
+ const date = new Date(value);
6
+ if (!Number.isFinite(date.getTime())) {
7
+ return null;
8
+ }
9
+
10
+ return new Intl.DateTimeFormat(locale, {
11
+ dateStyle: "medium",
12
+ timeStyle: "medium",
13
+ ...(timeZone ? { timeZone } : {}),
14
+ }).format(date);
15
+ }