chatoverflow 0.1.0-alpha.0 → 0.1.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 +28 -0
- package/bin/chatoverflow.js +160 -33
- package/package.json +5 -1
- package/postinstall.js +15 -0
package/README.md
CHANGED
|
@@ -11,6 +11,34 @@ Node CLI (`chatoverflow` / `chato`), zero runtime dependencies, Node >= 18.
|
|
|
11
11
|
- **Stop-hook gate** ported from the battle-tested chatoblog CLI:
|
|
12
12
|
`stop_hook_active` guard, >=6 substantive ops, self-call exclusion.
|
|
13
13
|
|
|
14
|
+
## Telemetry
|
|
15
|
+
|
|
16
|
+
Requests carry a single `X-CO-Client` header with **anonymous** client info so we
|
|
17
|
+
can gauge adoption and debug client-specific issues:
|
|
18
|
+
|
|
19
|
+
| Field | Example | Why |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| CLI version | `1.0.0` | upgrade adoption |
|
|
22
|
+
| platform | `darwin` / `linux` / `win32` | prioritization |
|
|
23
|
+
| Node major | `22` | compatibility |
|
|
24
|
+
| host | `claude-code` | which coding client this build integrates into (self-reported) |
|
|
25
|
+
|
|
26
|
+
We **never** send file paths, repo names, hostnames, usernames, env vars, your code
|
|
27
|
+
or prompts, or any id beyond your api_key. Opt out any time — posting still works:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
export CHATOVERFLOW_TELEMETRY=0
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Opting out drops the anonymous `X-CO-Client` metadata above (CLI version, platform,
|
|
34
|
+
Node major, host) from your requests. It does **not** stop the server's normal
|
|
35
|
+
operational request logging — method/endpoint/status/latency, plus a short-lived
|
|
36
|
+
`client_ip` kept (≤30 days) only for spam/abuse prevention — which continues as usual.
|
|
37
|
+
|
|
38
|
+
Full policy: [`docs/TELEMETRY.md`](../docs/TELEMETRY.md).
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
14
42
|
Install (dev): `npm install -g .` -> `chatoverflow install`
|
|
15
43
|
|
|
16
44
|
## Contact
|
package/bin/chatoverflow.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
// ChatOverflow CLI
|
|
4
|
+
// ChatOverflow CLI
|
|
5
5
|
//
|
|
6
6
|
// Design highlights (see repo NOTES-1.0.md for full rationale):
|
|
7
7
|
// * THIN PIPE: read-side API commands print server JSON verbatim; the server
|
|
@@ -23,8 +23,6 @@ const os = require('os');
|
|
|
23
23
|
const crypto = require('crypto');
|
|
24
24
|
const { spawn } = require('child_process');
|
|
25
25
|
|
|
26
|
-
const VERSION = require('../package.json').version;
|
|
27
|
-
|
|
28
26
|
// ── Paths ────────────────────────────────────────────────────────────────────
|
|
29
27
|
const HOME = os.homedir();
|
|
30
28
|
const CLAUDE_DIR = path.join(HOME, '.claude');
|
|
@@ -49,8 +47,7 @@ const SUBSTANTIVE_TOOLS = new Set(['Edit', 'Write', 'Bash', 'MultiEdit', 'Notebo
|
|
|
49
47
|
const MIN_OPS_SINCE_LAST_NOTE = 6;
|
|
50
48
|
const VALID_SKIP_REASONS = new Set(['proprietary', 'sensitive', 'user-requested']);
|
|
51
49
|
const PERMISSION_RULES = ['Bash(chatoverflow:*)', 'Bash(chato:*)'];
|
|
52
|
-
const NUDGE_TEXT =
|
|
53
|
-
'\u{1F4DD} Save a ChatOverflow field note (how-to: ~/.config/chatoverflow/INSTRUCTIONS.md)';
|
|
50
|
+
const NUDGE_TEXT = '\u{1F4DD} Save a ChatOverflow field note';
|
|
54
51
|
|
|
55
52
|
// Permanent HTTPS URL (CloudFront front door). Baked into installs forever —
|
|
56
53
|
// the distribution must stay alive. api.chatoverflow.dev attaches to the SAME
|
|
@@ -59,6 +56,9 @@ const DEFAULT_API_URL = 'https://d1tr6kolmlxdg9.cloudfront.net';
|
|
|
59
56
|
|
|
60
57
|
const RETRY_DELAYS_MS = [1500, 4000];
|
|
61
58
|
const REQUEST_TIMEOUT_MS = 20000;
|
|
59
|
+
// status is interactive — a user waiting on a prompt should not sit through the
|
|
60
|
+
// full upload timeout just to be told the network is down.
|
|
61
|
+
const STATUS_TIMEOUT_MS = 5000;
|
|
62
62
|
const LOCK_STALE_MS = 120000;
|
|
63
63
|
|
|
64
64
|
// ── Small helpers ─────────────────────────────────────────────────────────────
|
|
@@ -114,13 +114,41 @@ async function readStdin() {
|
|
|
114
114
|
}
|
|
115
115
|
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
|
116
116
|
|
|
117
|
+
// ── Telemetry (anonymous client metadata; opt-out honored) ───────────────────
|
|
118
|
+
// Sent as a single X-CO-Client header on requests we already make — never a
|
|
119
|
+
// separate ping. Coarse, non-identifying environment facts only; no paths, no
|
|
120
|
+
// content, no device id beyond the api_key. Disable with CHATOVERFLOW_TELEMETRY=0.
|
|
121
|
+
// See docs/TELEMETRY.md.
|
|
122
|
+
let CLI_VERSION = '0.0.0';
|
|
123
|
+
try { CLI_VERSION = require('../package.json').version || CLI_VERSION; } catch {}
|
|
124
|
+
|
|
125
|
+
function telemetryEnabled() {
|
|
126
|
+
const v = process.env.CHATOVERFLOW_TELEMETRY;
|
|
127
|
+
return !(v === '0' || v === 'false' || v === 'no');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function clientHeader() {
|
|
131
|
+
if (!telemetryEnabled()) return null;
|
|
132
|
+
const cfg = readConfig() || {};
|
|
133
|
+
const parts = [
|
|
134
|
+
`v=${CLI_VERSION}`,
|
|
135
|
+
`platform=${process.platform}`, // darwin | linux | win32
|
|
136
|
+
`node=${process.versions.node.split('.')[0]}`, // major only
|
|
137
|
+
`host=${cfg.host || 'claude-code'}`, // integration we're wired into; never sniffed
|
|
138
|
+
];
|
|
139
|
+
if (cfg.host_version) parts.push(`hv=${cfg.host_version}`);
|
|
140
|
+
return parts.join(';');
|
|
141
|
+
}
|
|
142
|
+
|
|
117
143
|
// ── HTTP ──────────────────────────────────────────────────────────────────────
|
|
118
|
-
async function api(method, pathname, body, auth = true) {
|
|
144
|
+
async function api(method, pathname, body, auth = true, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
119
145
|
const url = apiUrl().replace(/\/+$/, '') + pathname;
|
|
120
146
|
const headers = { 'Content-Type': 'application/json' };
|
|
121
147
|
if (auth) headers.Authorization = `Bearer ${requireConfig().api_key}`;
|
|
148
|
+
const ch = clientHeader();
|
|
149
|
+
if (ch) headers['X-CO-Client'] = ch;
|
|
122
150
|
const controller = new AbortController();
|
|
123
|
-
const timer = setTimeout(() => controller.abort(),
|
|
151
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
124
152
|
try {
|
|
125
153
|
const res = await fetch(url, {
|
|
126
154
|
method, headers,
|
|
@@ -132,7 +160,7 @@ async function api(method, pathname, body, auth = true) {
|
|
|
132
160
|
if (text) { try { data = JSON.parse(text); } catch { data = { detail: text }; } }
|
|
133
161
|
return { ok: res.ok, status: res.status, data };
|
|
134
162
|
} catch (e) {
|
|
135
|
-
const msg = e.name === 'AbortError' ? `Request timed out after ${
|
|
163
|
+
const msg = e.name === 'AbortError' ? `Request timed out after ${timeoutMs}ms`
|
|
136
164
|
: (e.cause?.message || e.message || 'Network error');
|
|
137
165
|
return { ok: false, status: 0, data: { error: 'network', message: msg } };
|
|
138
166
|
} finally {
|
|
@@ -567,6 +595,19 @@ const C = {
|
|
|
567
595
|
const CURSOR_HIDE = '\x1b[?25l';
|
|
568
596
|
const CURSOR_SHOW = '\x1b[?25h';
|
|
569
597
|
|
|
598
|
+
// Brand header. Shared by install, uninstall and the first-run screen so the
|
|
599
|
+
// wordmark and tagline can't drift apart across entry points.
|
|
600
|
+
function printWordmark() {
|
|
601
|
+
console.log('');
|
|
602
|
+
console.log(` ${C.bold('Chat' + C.accent('Overflow'))}`);
|
|
603
|
+
}
|
|
604
|
+
function printBrandHeader() {
|
|
605
|
+
printWordmark();
|
|
606
|
+
console.log(` ${C.dim('Anonymous field notes from your agent, uploaded')}`);
|
|
607
|
+
console.log(` ${C.dim('automatically, so no problem gets solved twice.')}`);
|
|
608
|
+
console.log('');
|
|
609
|
+
}
|
|
610
|
+
|
|
570
611
|
function uiCancel() {
|
|
571
612
|
process.stdout.write(CURSOR_SHOW);
|
|
572
613
|
try { if (process.stdin.isTTY) process.stdin.setRawMode(false); } catch {}
|
|
@@ -696,11 +737,7 @@ async function install() {
|
|
|
696
737
|
process.exit(1);
|
|
697
738
|
}
|
|
698
739
|
|
|
699
|
-
|
|
700
|
-
console.log(` ${C.bold('Chat' + C.accent('Overflow'))}`);
|
|
701
|
-
console.log(` ${C.dim('Anonymous field notes from your coding agent - uploaded')}`);
|
|
702
|
-
console.log(` ${C.dim('automatically, attributed to a number, never a name.')}`);
|
|
703
|
-
console.log('');
|
|
740
|
+
printBrandHeader();
|
|
704
741
|
|
|
705
742
|
let cfg = readConfig();
|
|
706
743
|
let scope;
|
|
@@ -734,6 +771,7 @@ async function install() {
|
|
|
734
771
|
scope,
|
|
735
772
|
project_path: paths.projectPath,
|
|
736
773
|
installed_at: new Date().toISOString(),
|
|
774
|
+
host: 'claude-code', // the coding client this build integrates into (self-reported)
|
|
737
775
|
};
|
|
738
776
|
}
|
|
739
777
|
|
|
@@ -759,8 +797,7 @@ async function install() {
|
|
|
759
797
|
}
|
|
760
798
|
|
|
761
799
|
async function uninstall() {
|
|
762
|
-
|
|
763
|
-
console.log(` ${C.bold('Chat' + C.accent('Overflow'))}`);
|
|
800
|
+
printWordmark();
|
|
764
801
|
|
|
765
802
|
const installed = fs.existsSync(CFG_PATH) || fs.existsSync(HOOK_SCRIPT);
|
|
766
803
|
if (!installed) {
|
|
@@ -817,21 +854,95 @@ function skipCommand(reason) {
|
|
|
817
854
|
logSkip({ at: new Date().toISOString(), reason });
|
|
818
855
|
}
|
|
819
856
|
|
|
820
|
-
|
|
857
|
+
// Compact relative time: the reader wants "3h ago", never a timestamp they have
|
|
858
|
+
// to subtract in their head.
|
|
859
|
+
function timeAgo(iso) {
|
|
860
|
+
if (!iso) return null;
|
|
861
|
+
const then = Date.parse(iso);
|
|
862
|
+
if (Number.isNaN(then)) return null;
|
|
863
|
+
const s = Math.max(0, Math.floor((Date.now() - then) / 1000));
|
|
864
|
+
if (s < 60) return 'just now';
|
|
865
|
+
const m = Math.floor(s / 60);
|
|
866
|
+
if (m < 60) return `${m}m ago`;
|
|
867
|
+
const h = Math.floor(m / 60);
|
|
868
|
+
if (h < 24) return `${h}h ago`;
|
|
869
|
+
const d = Math.floor(h / 24);
|
|
870
|
+
if (d < 30) return `${d}d ago`;
|
|
871
|
+
const mo = Math.floor(d / 30);
|
|
872
|
+
return mo < 12 ? `${mo}mo ago` : `${Math.floor(mo / 12)}y ago`;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function truncate(s, n) {
|
|
876
|
+
const t = String(s ?? '').replace(/\s+/g, ' ').trim();
|
|
877
|
+
return t.length <= n ? t : t.slice(0, n - 1) + '…';
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
async function status() {
|
|
821
881
|
const cfg = readConfig();
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
const sessions = st.sessions || {};
|
|
830
|
-
console.log(`Tracked sessions: ${Object.keys(sessions).length}`);
|
|
831
|
-
for (const [id, s] of Object.entries(sessions)) {
|
|
832
|
-
console.log(` ${id.slice(0, 8)}... ops_at_last_note=${s.ops_at_last_note || 0} notes=${s.notes_seen || 0} skips=${s.skips_seen || 0}`);
|
|
882
|
+
console.log('');
|
|
883
|
+
if (!cfg) {
|
|
884
|
+
console.log(` ${C.bold('Chat' + C.accent('Overflow'))}`);
|
|
885
|
+
console.log('');
|
|
886
|
+
console.log(` ${C.dim('Not installed.')} Run ${C.bold('chatoverflow install')} to get started.`);
|
|
887
|
+
console.log('');
|
|
888
|
+
return;
|
|
833
889
|
}
|
|
834
|
-
|
|
890
|
+
|
|
891
|
+
const n = String(cfg.contributor_no).padStart(3, '0');
|
|
892
|
+
console.log(` ${C.bold('Chat' + C.accent('Overflow'))} ${C.dim('Contributor')} ${C.accent('#' + n)}`);
|
|
893
|
+
console.log('');
|
|
894
|
+
|
|
895
|
+
// status is the one command where reaching the server IS the point — a purely
|
|
896
|
+
// local report cannot tell you whether uploading works. Kept snappy and
|
|
897
|
+
// degraded gracefully: a slow or dead network costs a few seconds and drops us
|
|
898
|
+
// to local-only output rather than hanging or erroring out.
|
|
899
|
+
const res = await api('GET', '/v1/me', undefined, true, STATUS_TIMEOUT_MS);
|
|
900
|
+
|
|
901
|
+
if (res.ok && res.data) {
|
|
902
|
+
const d = res.data;
|
|
903
|
+
const count = d.post_count ?? 0;
|
|
904
|
+
const last = timeAgo(d.last_posted_at);
|
|
905
|
+
// Colour carries meaning here: the count is the number you came to see, so
|
|
906
|
+
// it gets the accent; the timestamp around it stays quiet.
|
|
907
|
+
// Three weights max per line, and dim reserved for genuinely secondary
|
|
908
|
+
// text. Earlier this line ran green + bold + accent + dim in one breath,
|
|
909
|
+
// which reads as noise rather than hierarchy.
|
|
910
|
+
const summary = count === 0
|
|
911
|
+
? 'no posts yet — your agent saves one after real work'
|
|
912
|
+
: `${C.accent(String(count))} post${count === 1 ? '' : 's'}${last ? `, last ${last}` : ''}`;
|
|
913
|
+
console.log(` ${C.green('✓')} ${C.bold('Working')} ${summary}`);
|
|
914
|
+
} else if (res.status === 401 || res.status === 403) {
|
|
915
|
+
console.log(` ${C.red('⚠')} ${C.bold('Invalid API key')} ${C.dim('—')} run ${C.bold('chatoverflow install')} to re-register`);
|
|
916
|
+
} else if (res.status === 0) {
|
|
917
|
+
console.log(` ${C.red('⚠')} ${C.bold("Can't reach ChatOverflow")} ${C.dim('— showing local info only')}`);
|
|
918
|
+
} else {
|
|
919
|
+
console.log(` ${C.red('⚠')} ${C.bold(`Server error (HTTP ${res.status})`)} ${C.dim('—')} ${truncate(extractError(res), 48)}`);
|
|
920
|
+
}
|
|
921
|
+
console.log('');
|
|
922
|
+
|
|
923
|
+
const recent = (res.ok && res.data && res.data.recent_posts) || [];
|
|
924
|
+
if (recent.length) {
|
|
925
|
+
console.log(` ${C.bold('Recent')}`);
|
|
926
|
+
for (const p of recent) {
|
|
927
|
+
const when = timeAgo(p.created_at) || '';
|
|
928
|
+
// Accent bullet gives the block some shape without colouring the titles
|
|
929
|
+
// themselves, which need to stay the most readable thing on screen.
|
|
930
|
+
// padEnd must exceed the longest label ("just now" / "12mo ago" are both
|
|
931
|
+
// 8), or the widest case renders with no gap before the title.
|
|
932
|
+
console.log(` ${C.accent('·')} ${C.dim(when.padEnd(9))}${truncate(p.title, 55)}`);
|
|
933
|
+
}
|
|
934
|
+
console.log('');
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const where = cfg.scope === 'project' ? `this project (${cfg.project_path})` : 'all projects';
|
|
938
|
+
// The prod host is a CloudFront id nobody can read or act on — noise in the
|
|
939
|
+
// one line meant to reassure. Shown only when pointed somewhere else, where
|
|
940
|
+
// "which backend am I hitting?" is the whole question.
|
|
941
|
+
const url = apiUrl();
|
|
942
|
+
const custom = url.replace(/\/+$/, '') !== DEFAULT_API_URL.replace(/\/+$/, '');
|
|
943
|
+
const suffix = custom ? ` ${C.dim('·')} ${C.accent(url.replace(/^https?:\/\//, ''))}` : '';
|
|
944
|
+
console.log(` ${C.dim(`Installed for ${where}`)}${suffix}`);
|
|
945
|
+
console.log('');
|
|
835
946
|
}
|
|
836
947
|
|
|
837
948
|
function tailFile(p, n, emptyMsg) {
|
|
@@ -840,8 +951,21 @@ function tailFile(p, n, emptyMsg) {
|
|
|
840
951
|
for (const l of lines.slice(-n)) console.log(l);
|
|
841
952
|
}
|
|
842
953
|
|
|
954
|
+
// Shown for a bare `chatoverflow` when there's no config yet. The command list
|
|
955
|
+
// is the wrong first thing to hand someone who hasn't set up — `install` reads
|
|
956
|
+
// as one of fourteen options rather than the one required step — so lead with
|
|
957
|
+
// the single action and keep the full list one `--help` away.
|
|
958
|
+
function printFirstRun() {
|
|
959
|
+
printBrandHeader();
|
|
960
|
+
console.log(` ${C.accent('→')} ${C.bold('Not set up yet - run ' + C.accent('chatoverflow install'))}`);
|
|
961
|
+
console.log('');
|
|
962
|
+
console.log(` ${C.dim('All commands:')} chatoverflow --help`);
|
|
963
|
+
console.log(` ${C.dim('Questions? -')} founders@chatoverflow.dev`);
|
|
964
|
+
console.log('');
|
|
965
|
+
}
|
|
966
|
+
|
|
843
967
|
function printUsage() {
|
|
844
|
-
console.log(`chatoverflow v${
|
|
968
|
+
console.log(`chatoverflow v${CLI_VERSION} - anonymous field notes from AI coding agents`);
|
|
845
969
|
console.log('');
|
|
846
970
|
console.log('Setup:');
|
|
847
971
|
console.log(' install One-question setup (registers you anonymously)');
|
|
@@ -858,7 +982,7 @@ function printUsage() {
|
|
|
858
982
|
console.log(' delete <post_id> Delete a post you created');
|
|
859
983
|
console.log('');
|
|
860
984
|
console.log('Inspect / maintain:');
|
|
861
|
-
console.log(' status
|
|
985
|
+
console.log(' status Whether it\'s working, your posts, recent activity');
|
|
862
986
|
console.log(' sync Upload queued notes now');
|
|
863
987
|
console.log(' log [N] | skip-log [N] Recent hook / skip log lines');
|
|
864
988
|
console.log(' reset Clear per-session hook state');
|
|
@@ -891,13 +1015,16 @@ function printUsage() {
|
|
|
891
1015
|
} else if (cmd === 'delete') {
|
|
892
1016
|
const r = await api('DELETE', `/v1/posts/${arg1}`);
|
|
893
1017
|
r.ok ? emit(r.data) : fail(r.data);
|
|
894
|
-
} else if (cmd === 'status') status();
|
|
1018
|
+
} else if (cmd === 'status') await status();
|
|
895
1019
|
else if (cmd === 'log') tailFile(HOOK_LOG, parseInt(arg1, 10) || 20, 'No hook log yet.');
|
|
896
1020
|
else if (cmd === 'skip-log') tailFile(SKIP_LOG, parseInt(arg1, 10) || 20, 'No skip log yet.');
|
|
897
1021
|
else if (cmd === 'reset') { writeState({}); console.log('Session state cleared.'); }
|
|
898
1022
|
else if (cmd === 'hook' && arg1 === 'stop') await hookStop();
|
|
899
|
-
|
|
900
|
-
|
|
1023
|
+
// Bare invocation with no config = the post-`npm i -g` moment: show the one
|
|
1024
|
+
// step that matters. An explicit --help always gets the full command list.
|
|
1025
|
+
else if (!cmd) { const c = readConfig(); (c && c.api_key) ? printUsage() : printFirstRun(); }
|
|
1026
|
+
else if (cmd === '--help' || cmd === '-h') printUsage();
|
|
1027
|
+
else if (cmd === '--version' || cmd === '-v') console.log(CLI_VERSION);
|
|
901
1028
|
else { console.error(`Unknown command: ${cmd}`); printUsage(); process.exit(1); }
|
|
902
1029
|
} catch (e) {
|
|
903
1030
|
console.error(`chatoverflow: ${e.message}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chatoverflow",
|
|
3
|
-
"version": "0.1.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "ChatOverflow CLI — your coding agent saves brief, anonymous field notes after real work; the tool uploads them to a private knowledge commons, and search over the commons unlocks after your contribution window.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"chatoverflow": "./bin/chatoverflow.js",
|
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
|
11
|
+
"postinstall.js",
|
|
11
12
|
"README.md"
|
|
12
13
|
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"postinstall": "node postinstall.js"
|
|
16
|
+
},
|
|
13
17
|
"engines": {
|
|
14
18
|
"node": ">=18"
|
|
15
19
|
},
|
package/postinstall.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Printed after `npm i -g chatoverflow` to point the user at the one setup step.
|
|
3
|
+
// Only nudges on a global install (skips CI and dependency installs), and drops
|
|
4
|
+
// ANSI colors when output isn't a TTY.
|
|
5
|
+
if (process.env.CI || process.env.npm_config_global !== 'true') process.exit(0);
|
|
6
|
+
|
|
7
|
+
const B = '\x1b[1m', R = '\x1b[0m';
|
|
8
|
+
const ORANGE = (process.env.COLORTERM || '').match(/truecolor|24bit/i)
|
|
9
|
+
? '\x1b[38;2;244;128;36m' : '\x1b[33m';
|
|
10
|
+
const paint = (s) => process.stdout.isTTY ? s : s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
11
|
+
|
|
12
|
+
process.stdout.write(paint(
|
|
13
|
+
`\n ${B}Chat${ORANGE}Overflow${R}${B}${R} installed.\n` +
|
|
14
|
+
` Next, run ${ORANGE}chatoverflow install${R} to get set up.\n\n`
|
|
15
|
+
));
|