about-system 0.9.31 → 0.10.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/README.md +16 -0
- package/dist/about-system-cli.js +221 -127
- package/dist/about-system-cli.js.map +1 -1
- package/dist/web/assets/index--HOsj3jw.js +14 -0
- package/dist/web/assets/index-BU_blZyu.css +1 -0
- package/dist/web/index.html +13 -0
- package/package.json +20 -2
- package/src/about-system-cli.ts +36 -0
- package/src/web-server.test.ts +69 -0
- package/src/web-server.ts +120 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-500:oklch(63.7% .237 25.331);--color-amber-500:oklch(76.9% .188 70.08);--color-emerald-500:oklch(69.6% .17 162.48);--color-white:#fff;--spacing:.25rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{border-color:var(--border);outline-color:var(--ring)}@supports (color:color-mix(in lab,red,red)){*{outline-color:color-mix(in oklab,var(--ring) 50%,transparent)}}body{background-color:var(--background);color:var(--foreground);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}}@layer components;@layer utilities{.absolute{position:absolute}.relative{position:relative}.top-1\/2{top:50%}.left-2\.5{left:calc(var(--spacing) * 2.5)}.col-span-full{grid-column:1/-1}.mx-auto{margin-inline:auto}.mt-10{margin-top:calc(var(--spacing) * 10)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.flex{display:flex}.grid{display:grid}.inline-flex{display:inline-flex}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.h-2{height:calc(var(--spacing) * 2)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-32{height:calc(var(--spacing) * 32)}.h-64{height:calc(var(--spacing) * 64)}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-fit{width:fit-content}.w-full{width:100%}.max-w-7xl{max-width:var(--container-7xl)}.max-w-full{max-width:100%}.min-w-0{min-width:0}.flex-1{flex:1}.shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-\[7rem_minmax\(0\,1fr\)\]{grid-template-columns:7rem minmax(0,1fr)}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-xl{border-radius:calc(var(--radius) + 4px)}.border{border-style:var(--tw-border-style);border-width:1px}.border-destructive\/50{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.border-destructive\/50{border-color:color-mix(in oklab,var(--destructive) 50%,transparent)}}.border-input{border-color:var(--input)}.border-transparent{border-color:#0000}.bg-accent{background-color:var(--accent)}.bg-amber-500{background-color:var(--color-amber-500)}.bg-background{background-color:var(--background)}.bg-card{background-color:var(--card)}.bg-destructive{background-color:var(--destructive)}.bg-emerald-500{background-color:var(--color-emerald-500)}.bg-primary,.bg-primary\/20{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.bg-primary\/20{background-color:color-mix(in oklab,var(--primary) 20%,transparent)}}.bg-red-500{background-color:var(--color-red-500)}.bg-secondary{background-color:var(--secondary)}.bg-transparent{background-color:#0000}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.pl-8{padding-left:calc(var(--spacing) * 8)}.text-center{text-align:center}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-none{--tw-leading:1;line-height:1}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.text-card-foreground{color:var(--card-foreground)}.text-destructive{color:var(--destructive)}.text-foreground{color:var(--foreground)}.text-muted-foreground{color:var(--muted-foreground)}.text-primary-foreground{color:var(--primary-foreground)}.text-secondary-foreground{color:var(--secondary-foreground)}.text-white{color:var(--color-white)}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.transition-\[color\,box-shadow\]{transition-property:color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.outline-none{--tw-outline-style:none;outline-style:none}.placeholder\:text-muted-foreground::placeholder{color:var(--muted-foreground)}.first\:pt-0:first-child{padding-top:0}.last\:pb-0:last-child{padding-bottom:0}@media(hover:hover){.hover\:bg-accent:hover{background-color:var(--accent)}.hover\:bg-primary\/90:hover{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab,var(--primary) 90%,transparent)}}.hover\:text-accent-foreground:hover{color:var(--accent-foreground)}}.focus-visible\:border-ring:focus-visible{border-color:var(--ring)}.focus-visible\:ring-\[3px\]:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab,red,red)){.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:color-mix(in oklab, var(--ring) 50%, transparent)}}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-50:disabled{opacity:.5}.has-\[\>svg\]\:px-2\.5:has(>svg){padding-inline:calc(var(--spacing) * 2.5)}.has-\[\>svg\]\:px-3:has(>svg){padding-inline:calc(var(--spacing) * 3)}@media(min-width:40rem){.sm\:col-span-2{grid-column:span 2/span 2}.sm\:w-56{width:calc(var(--spacing) * 56)}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-\[8rem_minmax\(0\,1fr\)\]{grid-template-columns:8rem minmax(0,1fr)}.sm\:flex-row{flex-direction:row}.sm\:items-end{align-items:flex-end}.sm\:justify-between{justify-content:space-between}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}}@media(min-width:48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}@media(min-width:64rem){.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}.dark\:border-input:is(.dark *){border-color:var(--input)}.dark\:bg-input\/30:is(.dark *){background-color:var(--input)}@supports (color:color-mix(in lab,red,red)){.dark\:bg-input\/30:is(.dark *){background-color:color-mix(in oklab,var(--input) 30%,transparent)}}@media(hover:hover){.dark\:hover\:bg-accent\/50:is(.dark *):hover{background-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-accent\/50:is(.dark *):hover{background-color:color-mix(in oklab,var(--accent) 50%,transparent)}}.dark\:hover\:bg-input\/50:is(.dark *):hover{background-color:var(--input)}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-input\/50:is(.dark *):hover{background-color:color-mix(in oklab,var(--input) 50%,transparent)}}}.\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0}.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4 svg:not([class*=size-]){width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.\[\&\>svg\]\:pointer-events-none>svg{pointer-events:none}.\[\&\>svg\]\:size-3>svg{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}}:root{--radius:.625rem;--background:oklch(100% 0 0);--foreground:oklch(14.5% 0 0);--card:oklch(100% 0 0);--card-foreground:oklch(14.5% 0 0);--primary:oklch(20.5% 0 0);--primary-foreground:oklch(98.5% 0 0);--secondary:oklch(97% 0 0);--secondary-foreground:oklch(20.5% 0 0);--muted:oklch(97% 0 0);--muted-foreground:oklch(55.6% 0 0);--accent:oklch(97% 0 0);--accent-foreground:oklch(20.5% 0 0);--destructive:oklch(57.7% .245 27.325);--border:oklch(92.2% 0 0);--input:oklch(92.2% 0 0);--ring:oklch(70.8% 0 0)}.dark{--background:oklch(14.5% 0 0);--foreground:oklch(98.5% 0 0);--card:oklch(20.5% 0 0);--card-foreground:oklch(98.5% 0 0);--primary:oklch(92.2% 0 0);--primary-foreground:oklch(20.5% 0 0);--secondary:oklch(26.9% 0 0);--secondary-foreground:oklch(98.5% 0 0);--muted:oklch(26.9% 0 0);--muted-foreground:oklch(70.8% 0 0);--accent:oklch(26.9% 0 0);--accent-foreground:oklch(98.5% 0 0);--destructive:oklch(70.4% .191 22.216);--border:oklch(100% 0 0/.1);--input:oklch(100% 0 0/.15);--ring:oklch(55.6% 0 0)}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>About System</title>
|
|
7
|
+
<script type="module" crossorigin src="./assets/index--HOsj3jw.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BU_blZyu.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "about-system",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "A Node.js script to display key system information with emojis. Cross-platform support for Windows, macOS, and Linux with customizable output and caching.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -26,11 +26,14 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
|
-
"build": "vite build",
|
|
29
|
+
"build": "vite build && vite build --config web/vite.config.ts",
|
|
30
|
+
"build:web": "vite build --config web/vite.config.ts",
|
|
30
31
|
"build:tsc": "tsc",
|
|
31
32
|
"dev": "vite build --watch",
|
|
33
|
+
"dev:web": "vite --config web/vite.config.ts",
|
|
32
34
|
"ship": "npm run build && npx standard-version --release-as patch; rm CHANGELOG.md; npm publish",
|
|
33
35
|
"start": "npm run build && node dist/about-system-cli.js",
|
|
36
|
+
"web": "npm run build && node dist/about-system-cli.js web",
|
|
34
37
|
"install-greeting": "npm run build && node dist/about-system-cli.js --install",
|
|
35
38
|
"show-settings": "npm run build && node dist/about-system-cli.js --settings-show",
|
|
36
39
|
"reset-settings": "npm run build && node dist/about-system-cli.js --settings-reset",
|
|
@@ -39,6 +42,7 @@
|
|
|
39
42
|
"app:dev": "cd native && npm run dev",
|
|
40
43
|
"app:build": "cd native && npm run build:desktop",
|
|
41
44
|
"test": "vitest run",
|
|
45
|
+
"typecheck:web": "tsc -p web/tsconfig.json",
|
|
42
46
|
"test:ci": "vitest run --reporter=junit --outputFile=./junit.xml --coverage --coverage.reporter=lcov",
|
|
43
47
|
"test:watch": "vitest",
|
|
44
48
|
"coverage": "vitest run --coverage"
|
|
@@ -57,6 +61,7 @@
|
|
|
57
61
|
"greeting",
|
|
58
62
|
"emoji",
|
|
59
63
|
"system-monitoring",
|
|
64
|
+
"dashboard",
|
|
60
65
|
"desktop-app",
|
|
61
66
|
"tauri"
|
|
62
67
|
],
|
|
@@ -89,8 +94,21 @@
|
|
|
89
94
|
"ora": "^9.0.0"
|
|
90
95
|
},
|
|
91
96
|
"devDependencies": {
|
|
97
|
+
"@radix-ui/react-progress": "^1.1.16",
|
|
98
|
+
"@radix-ui/react-slot": "^1.3.3",
|
|
99
|
+
"@tailwindcss/vite": "^4.3.3",
|
|
92
100
|
"@types/node": "^25.0.3",
|
|
101
|
+
"@types/react": "^19.3.0",
|
|
102
|
+
"@types/react-dom": "^19.3.0",
|
|
103
|
+
"@vitejs/plugin-react": "^5",
|
|
93
104
|
"@vitest/coverage-v8": "^4.1.0",
|
|
105
|
+
"class-variance-authority": "^0.7.1",
|
|
106
|
+
"clsx": "^2.1.1",
|
|
107
|
+
"lucide-react": "^1.48.0",
|
|
108
|
+
"react": "^19.3.0",
|
|
109
|
+
"react-dom": "^19.3.0",
|
|
110
|
+
"tailwind-merge": "^3.7.0",
|
|
111
|
+
"tailwindcss": "^4.3.3",
|
|
94
112
|
"typescript": "^5.9.3",
|
|
95
113
|
"vite": "^7.3.1",
|
|
96
114
|
"vite-plugin-dts": "^4.5.4",
|
package/src/about-system-cli.ts
CHANGED
|
@@ -5,6 +5,7 @@ import path from "path";
|
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { getSystemInfo } from "./system-info-api";
|
|
7
7
|
import type { SystemInfo, SystemInfoOptions } from "./systeminfo-types";
|
|
8
|
+
import { startWebServer, DEFAULT_WEB_PORT, DEFAULT_WEB_HOST } from "./web-server";
|
|
8
9
|
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
11
|
|
|
@@ -380,6 +381,33 @@ function installShellGreeting(): void {
|
|
|
380
381
|
}
|
|
381
382
|
}
|
|
382
383
|
|
|
384
|
+
/** Reads `--flag value` or `--flag=value` from args. */
|
|
385
|
+
function flagValue(args: string[], flag: string): string | undefined {
|
|
386
|
+
for (let i = 0; i < args.length; i++) {
|
|
387
|
+
if (args[i] === flag) return args[i + 1];
|
|
388
|
+
if (args[i].startsWith(flag + "=")) return args[i].slice(flag.length + 1);
|
|
389
|
+
}
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function runWebServer(args: string[]): Promise<void> {
|
|
394
|
+
const rawPort = flagValue(args, "--port") ?? process.env.PORT;
|
|
395
|
+
const port = rawPort === undefined ? DEFAULT_WEB_PORT : Number(rawPort);
|
|
396
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
397
|
+
throw new Error(`Invalid port: ${rawPort}`);
|
|
398
|
+
}
|
|
399
|
+
const host = flagValue(args, "--host") ?? DEFAULT_WEB_HOST;
|
|
400
|
+
|
|
401
|
+
const { url } = await startWebServer({ port, host });
|
|
402
|
+
console.log(`About System web UI running at ${url}`);
|
|
403
|
+
if (host === DEFAULT_WEB_HOST) {
|
|
404
|
+
console.log("Listening on localhost only. Use --host 0.0.0.0 to expose it on the network.");
|
|
405
|
+
} else {
|
|
406
|
+
console.log("Warning: system details (public IP, ports, processes) are visible to anyone who can reach this port.");
|
|
407
|
+
}
|
|
408
|
+
console.log("Press Ctrl+C to stop.");
|
|
409
|
+
}
|
|
410
|
+
|
|
383
411
|
function parseCLIMode(args: string[]): string[] | null {
|
|
384
412
|
for (const arg of args) {
|
|
385
413
|
if (!arg.startsWith("--") && arg.includes(",")) {
|
|
@@ -406,6 +434,7 @@ System Info Script - TypeScript Version
|
|
|
406
434
|
Usage:
|
|
407
435
|
about-system [options]
|
|
408
436
|
about-system <part1,part2,...> # CLI mode: show specific parts only
|
|
437
|
+
about-system web [--port N] [--host H] # Serve the web dashboard
|
|
409
438
|
|
|
410
439
|
Options:
|
|
411
440
|
--help, -h Show this help message
|
|
@@ -429,6 +458,8 @@ Examples:
|
|
|
429
458
|
about-system --set emojis.cpu "🚀 "
|
|
430
459
|
about-system --set labels.cpu "Processor"
|
|
431
460
|
about-system --json
|
|
461
|
+
about-system web # http://127.0.0.1:${DEFAULT_WEB_PORT}
|
|
462
|
+
about-system web --port 8080 --host 0.0.0.0
|
|
432
463
|
|
|
433
464
|
Settings file: ${SETTINGS_FILE}
|
|
434
465
|
Cache file: ${CACHE_FILE}
|
|
@@ -470,6 +501,11 @@ async function main(): Promise<void> {
|
|
|
470
501
|
return;
|
|
471
502
|
}
|
|
472
503
|
|
|
504
|
+
if (args[0] === "web" || args.includes("--web")) {
|
|
505
|
+
await runWebServer(args);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
473
509
|
if (args.includes("--json")) {
|
|
474
510
|
const info = await getSystemInfo();
|
|
475
511
|
console.log(JSON.stringify(info, null, 2));
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import type http from "http";
|
|
6
|
+
|
|
7
|
+
vi.mock("./system-info-api", () => ({
|
|
8
|
+
getSystemInfo: vi.fn(async () => ({ user: "tester", hostname: "box", ram_used: "8/16GB" })),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
import { resolveStaticFile, startWebServer } from "./web-server";
|
|
12
|
+
|
|
13
|
+
let webRoot: string;
|
|
14
|
+
let server: http.Server;
|
|
15
|
+
let baseUrl: string;
|
|
16
|
+
|
|
17
|
+
beforeAll(async () => {
|
|
18
|
+
webRoot = fs.mkdtempSync(path.join(os.tmpdir(), "about-system-web-"));
|
|
19
|
+
fs.mkdirSync(path.join(webRoot, "assets"));
|
|
20
|
+
fs.writeFileSync(path.join(webRoot, "index.html"), "<!doctype html><div id=root></div>");
|
|
21
|
+
fs.writeFileSync(path.join(webRoot, "assets", "app.js"), "console.log(1)");
|
|
22
|
+
|
|
23
|
+
({ server, url: baseUrl } = await startWebServer({ port: 0, webRoot }));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterAll(() => {
|
|
27
|
+
server?.close();
|
|
28
|
+
fs.rmSync(webRoot, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("resolveStaticFile", () => {
|
|
32
|
+
it("serves files inside the web root", () => {
|
|
33
|
+
expect(resolveStaticFile(webRoot, "/assets/app.js")).toBe(path.join(webRoot, "assets", "app.js"));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("falls back to index.html for unknown routes", () => {
|
|
37
|
+
expect(resolveStaticFile(webRoot, "/some/route")).toBe(path.join(webRoot, "index.html"));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("never escapes the web root", () => {
|
|
41
|
+
for (const attempt of ["/../../etc/passwd", "/%2e%2e/%2e%2e/etc/passwd", "/..%2f..%2fetc/passwd"]) {
|
|
42
|
+
const file = resolveStaticFile(webRoot, attempt);
|
|
43
|
+
expect(file === null || file.startsWith(webRoot)).toBe(true);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("web server", () => {
|
|
49
|
+
it("returns system info as JSON from /api/info", async () => {
|
|
50
|
+
const res = await fetch(`${baseUrl}/api/info`);
|
|
51
|
+
expect(res.status).toBe(200);
|
|
52
|
+
expect(res.headers.get("content-type")).toContain("application/json");
|
|
53
|
+
expect(await res.json()).toEqual({ user: "tester", hostname: "box", ram_used: "8/16GB" });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("serves the dashboard with the right content types", async () => {
|
|
57
|
+
const index = await fetch(`${baseUrl}/`);
|
|
58
|
+
expect(index.headers.get("content-type")).toContain("text/html");
|
|
59
|
+
expect(await index.text()).toContain('id=root');
|
|
60
|
+
|
|
61
|
+
const js = await fetch(`${baseUrl}/assets/app.js`);
|
|
62
|
+
expect(js.headers.get("content-type")).toContain("text/javascript");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("rejects non-GET methods", async () => {
|
|
66
|
+
const res = await fetch(`${baseUrl}/api/info`, { method: "POST" });
|
|
67
|
+
expect(res.status).toBe(405);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview `about-system web` — serves the React dashboard and a JSON API.
|
|
3
|
+
*
|
|
4
|
+
* The dashboard is prebuilt into dist/web by `vite build --config web/vite.config.ts`
|
|
5
|
+
* and served as static files; the only dynamic route is GET /api/info, which
|
|
6
|
+
* returns the same object as `about-system --json`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import http from "http";
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { getSystemInfo } from "./system-info-api";
|
|
14
|
+
|
|
15
|
+
export interface WebServerOptions {
|
|
16
|
+
port?: number;
|
|
17
|
+
host?: string;
|
|
18
|
+
/** Directory holding the built dashboard. Defaults to dist/web next to this file. */
|
|
19
|
+
webRoot?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_WEB_PORT = 3777;
|
|
23
|
+
export const DEFAULT_WEB_HOST = "127.0.0.1";
|
|
24
|
+
|
|
25
|
+
const MIME_TYPES: Record<string, string> = {
|
|
26
|
+
".html": "text/html; charset=utf-8",
|
|
27
|
+
".js": "text/javascript; charset=utf-8",
|
|
28
|
+
".css": "text/css; charset=utf-8",
|
|
29
|
+
".json": "application/json; charset=utf-8",
|
|
30
|
+
".svg": "image/svg+xml",
|
|
31
|
+
".png": "image/png",
|
|
32
|
+
".ico": "image/x-icon",
|
|
33
|
+
".woff2": "font/woff2",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function defaultWebRoot(): string {
|
|
37
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), "web");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolves a request path to a file inside webRoot, or null if it escapes the
|
|
42
|
+
* root or does not exist. Unknown paths fall back to index.html (SPA routing).
|
|
43
|
+
*/
|
|
44
|
+
export function resolveStaticFile(webRoot: string, urlPath: string): string | null {
|
|
45
|
+
let decoded: string;
|
|
46
|
+
try {
|
|
47
|
+
decoded = decodeURIComponent(urlPath.split("?")[0]);
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const root = path.resolve(webRoot);
|
|
52
|
+
const candidate = path.resolve(root, "." + path.posix.normalize("/" + decoded));
|
|
53
|
+
if (candidate !== root && !candidate.startsWith(root + path.sep)) return null;
|
|
54
|
+
|
|
55
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
|
|
56
|
+
const index = path.join(root, "index.html");
|
|
57
|
+
return fs.existsSync(index) ? index : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function send(res: http.ServerResponse, status: number, type: string, body: string | Buffer): void {
|
|
61
|
+
res.writeHead(status, {
|
|
62
|
+
"Content-Type": type,
|
|
63
|
+
"Cache-Control": "no-store",
|
|
64
|
+
"X-Content-Type-Options": "nosniff",
|
|
65
|
+
});
|
|
66
|
+
res.end(body);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createWebServer(options: WebServerOptions = {}): http.Server {
|
|
70
|
+
const webRoot = options.webRoot ?? defaultWebRoot();
|
|
71
|
+
|
|
72
|
+
return http.createServer(async (req, res) => {
|
|
73
|
+
const url = req.url ?? "/";
|
|
74
|
+
|
|
75
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
76
|
+
send(res, 405, "text/plain; charset=utf-8", "Method Not Allowed");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (url === "/api/info" || url.startsWith("/api/info?")) {
|
|
81
|
+
try {
|
|
82
|
+
const info = await getSystemInfo();
|
|
83
|
+
send(res, 200, MIME_TYPES[".json"], JSON.stringify(info));
|
|
84
|
+
} catch (error) {
|
|
85
|
+
send(res, 500, MIME_TYPES[".json"], JSON.stringify({ error: (error as Error).message }));
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const file = resolveStaticFile(webRoot, url);
|
|
91
|
+
if (!file) {
|
|
92
|
+
send(
|
|
93
|
+
res,
|
|
94
|
+
404,
|
|
95
|
+
"text/plain; charset=utf-8",
|
|
96
|
+
"Dashboard not found. Build it with: npm run build:web"
|
|
97
|
+
);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const type = MIME_TYPES[path.extname(file)] ?? "application/octet-stream";
|
|
101
|
+
send(res, 200, type, fs.readFileSync(file));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Starts the dashboard server and resolves with the URL it is listening on. */
|
|
106
|
+
export function startWebServer(options: WebServerOptions = {}): Promise<{ server: http.Server; url: string }> {
|
|
107
|
+
const port = options.port ?? DEFAULT_WEB_PORT;
|
|
108
|
+
const host = options.host ?? DEFAULT_WEB_HOST;
|
|
109
|
+
const server = createWebServer(options);
|
|
110
|
+
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
server.once("error", reject);
|
|
113
|
+
server.listen(port, host, () => {
|
|
114
|
+
const address = server.address();
|
|
115
|
+
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
116
|
+
const shownHost = host === "0.0.0.0" || host === "::" ? "localhost" : host;
|
|
117
|
+
resolve({ server, url: `http://${shownHost}:${actualPort}` });
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|