oh-aicoding-tool 0.1.0 → 0.1.2
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 +4 -3
- package/bin/cli.js +274 -56
- package/opencode-ohai-report/bin/cli.js +1 -1
- package/opencode-ohai-report/scripts/install-claude-plugin.ps1 +1 -1
- package/opencode-ohai-report/scripts/install-opencode-plugin.ps1 +1 -1
- package/opencode-ohai-report/scripts/install-opencode-plugin.sh +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,9 +23,10 @@ npx oh-aicoding-tool langfuse setup
|
|
|
23
23
|
npx oh-aicoding-tool report install opencode --email user@company.com
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Running `npx oh-aicoding-tool` with no subcommand opens a
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
Running `npx oh-aicoding-tool` with no subcommand opens a highlighted TUI.
|
|
27
|
+
Use arrow keys to move, `Space` to select one or more tools, and `Enter` to
|
|
28
|
+
continue. The report path also supports multi-select install targets and guides
|
|
29
|
+
company email configuration before installing.
|
|
29
30
|
|
|
30
31
|
## Commands
|
|
31
32
|
|
package/bin/cli.js
CHANGED
|
@@ -1,10 +1,60 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import readline from "node:readline";
|
|
3
4
|
import { spawnSync } from "node:child_process";
|
|
4
5
|
import { createInterface } from "node:readline/promises";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
|
|
7
8
|
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
const langfuseCli = path.join(rootDir, "bin", "langfuse-cli.js");
|
|
10
|
+
const reportCli = path.join(rootDir, "opencode-ohai-report", "bin", "cli.js");
|
|
11
|
+
|
|
12
|
+
const colorEnabled = process.stdout.isTTY && process.env.NO_COLOR !== "1";
|
|
13
|
+
const ansi = (code) => (colorEnabled ? `\x1b[${code}m` : "");
|
|
14
|
+
const rgb = (r, g, b) => (colorEnabled ? `\x1b[38;2;${r};${g};${b}m` : "");
|
|
15
|
+
const bg = (r, g, b) => (colorEnabled ? `\x1b[48;2;${r};${g};${b}m` : "");
|
|
16
|
+
const t = {
|
|
17
|
+
reset: ansi(0),
|
|
18
|
+
bold: ansi(1),
|
|
19
|
+
dim: ansi(2),
|
|
20
|
+
cyan: rgb(92, 214, 255),
|
|
21
|
+
teal: rgb(92, 232, 188),
|
|
22
|
+
blue: rgb(132, 167, 255),
|
|
23
|
+
gold: rgb(245, 202, 116),
|
|
24
|
+
red: rgb(255, 117, 117),
|
|
25
|
+
muted: rgb(140, 151, 166),
|
|
26
|
+
panel: rgb(75, 86, 105),
|
|
27
|
+
selectedBg: bg(31, 53, 68),
|
|
28
|
+
selectedFg: rgb(218, 250, 255),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function paint(value, ...styles) {
|
|
32
|
+
if (!colorEnabled) return String(value);
|
|
33
|
+
return `${styles.join("")}${value}${t.reset}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function terminalWidth() {
|
|
37
|
+
return Math.max(72, Math.min(process.stdout.columns || 88, 104));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function clearScreen() {
|
|
41
|
+
if (process.stdout.isTTY) process.stdout.write("\x1b[2J\x1b[H");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function rule(width = terminalWidth()) {
|
|
45
|
+
return paint("-".repeat(width), t.panel);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function renderHeader(subtitle = "Interactive setup") {
|
|
49
|
+
clearScreen();
|
|
50
|
+
console.log("");
|
|
51
|
+
console.log(`${paint("oh-aicoding-tool", t.bold, t.teal)} ${paint(subtitle, t.cyan)}`);
|
|
52
|
+
console.log(rule());
|
|
53
|
+
console.log(
|
|
54
|
+
`${paint("Space", t.gold)} toggle ${paint("Enter", t.gold)} continue ` +
|
|
55
|
+
`${paint("Up/Down", t.gold)} move ${paint("q", t.gold)} exit`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
8
58
|
|
|
9
59
|
function runNodeScript(script, args) {
|
|
10
60
|
const result = spawnSync(process.execPath, [script, ...args], {
|
|
@@ -18,7 +68,7 @@ function printHelp() {
|
|
|
18
68
|
console.log("oh-aicoding-tool");
|
|
19
69
|
console.log("");
|
|
20
70
|
console.log("Usage:");
|
|
21
|
-
console.log(" oh-aicoding-tool Open the
|
|
71
|
+
console.log(" oh-aicoding-tool Open the interactive TUI");
|
|
22
72
|
console.log(" oh-aicoding-tool langfuse [setup|check] [target]");
|
|
23
73
|
console.log(" oh-aicoding-tool report install opencode [--email user@company.com]");
|
|
24
74
|
console.log(" oh-aicoding-tool report install claude");
|
|
@@ -35,13 +85,70 @@ function emailIsValid(value) {
|
|
|
35
85
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value || "").trim());
|
|
36
86
|
}
|
|
37
87
|
|
|
38
|
-
|
|
88
|
+
function renderChoiceScreen(title, choices, index, subtitle) {
|
|
89
|
+
renderHeader(subtitle);
|
|
39
90
|
console.log("");
|
|
40
|
-
console.log(title);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
91
|
+
console.log(paint(title, t.bold, t.blue));
|
|
92
|
+
console.log("");
|
|
93
|
+
choices.forEach((choice, idx) => {
|
|
94
|
+
const focused = idx === index;
|
|
95
|
+
const cursor = focused ? paint(">", t.teal, t.bold) : " ";
|
|
96
|
+
const label = focused
|
|
97
|
+
? paint(` ${choice.label} `, t.bold, t.selectedFg, t.selectedBg)
|
|
98
|
+
: paint(choice.label, t.bold);
|
|
99
|
+
console.log(` ${cursor} ${paint(`[${idx + 1}]`, focused ? t.selectedFg : t.muted)} ${label}`);
|
|
100
|
+
if (choice.description) {
|
|
101
|
+
console.log(` ${paint(choice.description, focused ? t.blue : t.muted)}`);
|
|
102
|
+
}
|
|
103
|
+
if (idx < choices.length - 1) console.log("");
|
|
44
104
|
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function askChoice(rl, title, choices, subtitle = "Interactive setup") {
|
|
108
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
109
|
+
rl.pause();
|
|
110
|
+
return await new Promise((resolve) => {
|
|
111
|
+
let index = 0;
|
|
112
|
+
const stdin = process.stdin;
|
|
113
|
+
|
|
114
|
+
function cleanup(value) {
|
|
115
|
+
stdin.off("keypress", onKeypress);
|
|
116
|
+
if (stdin.isTTY) stdin.setRawMode(false);
|
|
117
|
+
stdin.pause();
|
|
118
|
+
rl.resume();
|
|
119
|
+
clearScreen();
|
|
120
|
+
resolve(value);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function onKeypress(_, key = {}) {
|
|
124
|
+
if (key.ctrl && key.name === "c") return cleanup("exit");
|
|
125
|
+
if (key.name === "q" || key.name === "escape") return cleanup("exit");
|
|
126
|
+
if (key.name === "up") {
|
|
127
|
+
index = (index - 1 + choices.length) % choices.length;
|
|
128
|
+
renderChoiceScreen(title, choices, index, subtitle);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (key.name === "down") {
|
|
132
|
+
index = (index + 1) % choices.length;
|
|
133
|
+
renderChoiceScreen(title, choices, index, subtitle);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (key.name === "return" || key.name === "enter") return cleanup(choices[index].value);
|
|
137
|
+
const number = Number.parseInt(key.sequence, 10);
|
|
138
|
+
if (Number.isInteger(number) && choices[number - 1]) return cleanup(choices[number - 1].value);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
readline.emitKeypressEvents(stdin);
|
|
142
|
+
if (stdin.isTTY) stdin.setRawMode(true);
|
|
143
|
+
stdin.on("keypress", onKeypress);
|
|
144
|
+
stdin.resume();
|
|
145
|
+
renderChoiceScreen(title, choices, index, subtitle);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log("");
|
|
150
|
+
console.log(title);
|
|
151
|
+
choices.forEach((choice, index) => console.log(` ${index + 1}. ${choice.label}`));
|
|
45
152
|
while (true) {
|
|
46
153
|
const answer = (await rl.question("> ")).trim().toLowerCase();
|
|
47
154
|
const index = Number.parseInt(answer, 10) - 1;
|
|
@@ -52,78 +159,189 @@ async function askChoice(rl, title, choices) {
|
|
|
52
159
|
}
|
|
53
160
|
}
|
|
54
161
|
|
|
55
|
-
|
|
56
|
-
|
|
162
|
+
function renderMultiChoiceScreen(title, choices, index, selected, subtitle) {
|
|
163
|
+
renderHeader(subtitle);
|
|
164
|
+
console.log("");
|
|
165
|
+
console.log(paint(title, t.bold, t.blue));
|
|
166
|
+
console.log("");
|
|
167
|
+
choices.forEach((choice, idx) => {
|
|
168
|
+
const focused = idx === index;
|
|
169
|
+
const checked = selected.has(choice.value);
|
|
170
|
+
const box = checked ? paint("[✓]", t.teal, t.bold) : paint("[ ]", t.muted);
|
|
171
|
+
const cursor = focused ? paint(">", t.teal, t.bold) : " ";
|
|
172
|
+
const label = focused
|
|
173
|
+
? paint(` ${choice.label} `, t.bold, t.selectedFg, t.selectedBg)
|
|
174
|
+
: paint(choice.label, t.bold);
|
|
175
|
+
console.log(` ${cursor} ${box} ${label}`);
|
|
176
|
+
if (choice.description) {
|
|
177
|
+
console.log(` ${paint(choice.description, focused ? t.blue : t.muted)}`);
|
|
178
|
+
}
|
|
179
|
+
if (idx < choices.length - 1) console.log("");
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function askMultiChoice(rl, title, choices, subtitle = "Interactive setup") {
|
|
184
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
185
|
+
rl.pause();
|
|
186
|
+
return await new Promise((resolve) => {
|
|
187
|
+
let index = 0;
|
|
188
|
+
const selected = new Set(choices.filter((choice) => choice.selected).map((choice) => choice.value));
|
|
189
|
+
const stdin = process.stdin;
|
|
190
|
+
|
|
191
|
+
function cleanup(value) {
|
|
192
|
+
stdin.off("keypress", onKeypress);
|
|
193
|
+
if (stdin.isTTY) stdin.setRawMode(false);
|
|
194
|
+
stdin.pause();
|
|
195
|
+
rl.resume();
|
|
196
|
+
clearScreen();
|
|
197
|
+
resolve(value);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function toggle() {
|
|
201
|
+
const value = choices[index].value;
|
|
202
|
+
if (selected.has(value)) selected.delete(value);
|
|
203
|
+
else selected.add(value);
|
|
204
|
+
renderMultiChoiceScreen(title, choices, index, selected, subtitle);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function onKeypress(_, key = {}) {
|
|
208
|
+
if (key.ctrl && key.name === "c") return cleanup([]);
|
|
209
|
+
if (key.name === "q" || key.name === "escape") return cleanup([]);
|
|
210
|
+
if (key.name === "up") {
|
|
211
|
+
index = (index - 1 + choices.length) % choices.length;
|
|
212
|
+
renderMultiChoiceScreen(title, choices, index, selected, subtitle);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (key.name === "down") {
|
|
216
|
+
index = (index + 1) % choices.length;
|
|
217
|
+
renderMultiChoiceScreen(title, choices, index, selected, subtitle);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (key.name === "space") return toggle();
|
|
221
|
+
if (key.name === "return" || key.name === "enter") return cleanup([...selected]);
|
|
222
|
+
const number = Number.parseInt(key.sequence, 10);
|
|
223
|
+
if (Number.isInteger(number) && choices[number - 1]) {
|
|
224
|
+
index = number - 1;
|
|
225
|
+
toggle();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
readline.emitKeypressEvents(stdin);
|
|
230
|
+
if (stdin.isTTY) stdin.setRawMode(true);
|
|
231
|
+
stdin.on("keypress", onKeypress);
|
|
232
|
+
stdin.resume();
|
|
233
|
+
renderMultiChoiceScreen(title, choices, index, selected, subtitle);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
console.log("");
|
|
238
|
+
console.log(title);
|
|
239
|
+
choices.forEach((choice, index) => console.log(` ${index + 1}. ${choice.label}`));
|
|
240
|
+
const answer = await rl.question("Select numbers separated by comma > ");
|
|
241
|
+
return answer
|
|
242
|
+
.split(/[, ]+/)
|
|
243
|
+
.map((item) => Number.parseInt(item, 10) - 1)
|
|
244
|
+
.filter((index) => Number.isInteger(index) && choices[index])
|
|
245
|
+
.map((index) => choices[index].value);
|
|
246
|
+
}
|
|
57
247
|
|
|
58
248
|
async function askEmail(rl) {
|
|
59
249
|
const envEmail = process.env.OHAI_INSTALL_USER_EMAIL || "";
|
|
60
250
|
const hint = emailIsValid(envEmail) ? ` [${envEmail}]` : "";
|
|
251
|
+
renderHeader("Issue report setup");
|
|
252
|
+
console.log("");
|
|
253
|
+
console.log(paint("Company email", t.bold, t.blue));
|
|
254
|
+
console.log(paint("Used as the stable user identity in issue reports.", t.muted));
|
|
255
|
+
console.log(paint("Examples: xxxxx@huawei.com or xxxxx@h-partners.com", t.muted));
|
|
256
|
+
console.log(paint("Press Enter to skip Company email setup for now.", t.muted));
|
|
61
257
|
console.log("");
|
|
62
|
-
console.log("Company email is used as the user identity in issue reports.");
|
|
63
|
-
console.log("Press Enter to skip email setup for now.");
|
|
64
258
|
while (true) {
|
|
65
|
-
const answer = (await rl.question(
|
|
259
|
+
const answer = (await rl.question(`${paint("Company email", t.cyan)}${paint(hint, t.muted)} > `)).trim();
|
|
66
260
|
const value = answer || (emailIsValid(envEmail) ? envEmail.trim() : "");
|
|
67
261
|
if (!value) return "";
|
|
68
262
|
if (emailIsValid(value)) return value;
|
|
69
|
-
console.log("Invalid email.
|
|
263
|
+
console.log(paint("Invalid Company email. Examples: xxxxx@huawei.com or xxxxx@h-partners.com", t.red));
|
|
70
264
|
}
|
|
71
265
|
}
|
|
72
266
|
|
|
73
|
-
async function
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
{ label: "Back", value: "back" },
|
|
79
|
-
]);
|
|
80
|
-
if (target === "back") return 0;
|
|
81
|
-
const email = await askEmail(rl);
|
|
82
|
-
const args = ["install", target];
|
|
83
|
-
if (email) args.push("--email", email);
|
|
84
|
-
else args.push("--skip-email");
|
|
85
|
-
rl.close();
|
|
86
|
-
return runNodeScript(reportCli, args);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function runMainMenu() {
|
|
90
|
-
if (!process.stdin.isTTY) {
|
|
91
|
-
printHelp();
|
|
92
|
-
return 0;
|
|
93
|
-
}
|
|
94
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
95
|
-
try {
|
|
96
|
-
console.log("");
|
|
97
|
-
console.log("oh-aicoding-tool");
|
|
98
|
-
const action = await askChoice(rl, "What would you like to install or configure?", [
|
|
267
|
+
async function collectInstallPlan(rl) {
|
|
268
|
+
const tools = await askMultiChoice(
|
|
269
|
+
rl,
|
|
270
|
+
"Select tools to install or configure",
|
|
271
|
+
[
|
|
99
272
|
{
|
|
100
273
|
label: "Langfuse tracing",
|
|
101
274
|
value: "langfuse",
|
|
275
|
+
selected: true,
|
|
102
276
|
description: "Configure tracing for Claude Code, OpenCode, or Codex.",
|
|
103
277
|
},
|
|
104
278
|
{
|
|
105
279
|
label: "Issue report plugin",
|
|
106
280
|
value: "report",
|
|
107
|
-
|
|
281
|
+
selected: true,
|
|
282
|
+
description: "Install /report-ai-issue and configure Company email.",
|
|
108
283
|
},
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
284
|
+
],
|
|
285
|
+
"Install menu"
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
if (!tools.length) return { tools: [], reportTarget: "", email: "" };
|
|
289
|
+
|
|
290
|
+
let reportTarget = "";
|
|
291
|
+
let email = "";
|
|
292
|
+
if (tools.includes("report")) {
|
|
293
|
+
const targets = await askMultiChoice(
|
|
294
|
+
rl,
|
|
295
|
+
"Select issue report install targets",
|
|
296
|
+
[
|
|
297
|
+
{
|
|
298
|
+
label: "OpenCode",
|
|
299
|
+
value: "opencode",
|
|
300
|
+
selected: true,
|
|
301
|
+
description: "Install the OpenCode plugin and /report-ai-issue command.",
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
label: "Claude Code",
|
|
305
|
+
value: "claude",
|
|
306
|
+
selected: false,
|
|
307
|
+
description: "Install the command and optional hook integration.",
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
"Issue report setup"
|
|
311
|
+
);
|
|
312
|
+
if (!targets.length) {
|
|
313
|
+
const withoutReport = tools.filter((item) => item !== "report");
|
|
314
|
+
return { tools: withoutReport, reportTarget: "", email: "" };
|
|
116
315
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
316
|
+
reportTarget = targets.length === 2 ? "both" : targets[0];
|
|
317
|
+
email = await askEmail(rl);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return { tools, reportTarget, email };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function runInteractiveInstall() {
|
|
324
|
+
if (!process.stdin.isTTY) {
|
|
325
|
+
printHelp();
|
|
326
|
+
return 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
330
|
+
try {
|
|
331
|
+
const plan = await collectInstallPlan(rl);
|
|
332
|
+
rl.close();
|
|
333
|
+
|
|
334
|
+
let code = 0;
|
|
335
|
+
if (plan.tools.includes("langfuse")) {
|
|
336
|
+
code ||= runNodeScript(langfuseCli, ["setup"]);
|
|
121
337
|
}
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
338
|
+
if (plan.tools.includes("report") && plan.reportTarget) {
|
|
339
|
+
const args = ["install", plan.reportTarget];
|
|
340
|
+
if (plan.email) args.push("--email", plan.email);
|
|
341
|
+
else args.push("--skip-email");
|
|
342
|
+
code ||= runNodeScript(reportCli, args);
|
|
125
343
|
}
|
|
126
|
-
return
|
|
344
|
+
return code;
|
|
127
345
|
} finally {
|
|
128
346
|
rl.close();
|
|
129
347
|
}
|
|
@@ -138,7 +356,7 @@ async function main() {
|
|
|
138
356
|
return 0;
|
|
139
357
|
}
|
|
140
358
|
|
|
141
|
-
if (!cmd) return await
|
|
359
|
+
if (!cmd) return await runInteractiveInstall();
|
|
142
360
|
|
|
143
361
|
if (cmd === "langfuse" || cmd === "trace" || cmd === "tracing") {
|
|
144
362
|
return runNodeScript(langfuseCli, rest);
|
|
@@ -161,6 +379,6 @@ async function main() {
|
|
|
161
379
|
main()
|
|
162
380
|
.then((code) => process.exit(code))
|
|
163
381
|
.catch((err) => {
|
|
164
|
-
console.error(err?.message || String(err));
|
|
382
|
+
console.error(paint(err?.message || String(err), t.red));
|
|
165
383
|
process.exit(1);
|
|
166
384
|
});
|
|
@@ -141,7 +141,7 @@ async function resolveEmail({ label, defaultPaths, destPath, skipEmail, emailArg
|
|
|
141
141
|
while (true) {
|
|
142
142
|
const value = (await rl.question("Company email: ")).trim();
|
|
143
143
|
if (emailIsValid(value)) return value;
|
|
144
|
-
console.log("Invalid email.
|
|
144
|
+
console.log("Invalid Company email. Examples: xxxxx@huawei.com or xxxxx@h-partners.com");
|
|
145
145
|
}
|
|
146
146
|
} finally {
|
|
147
147
|
rl.close();
|
|
@@ -98,7 +98,7 @@ function Read-OhaiEmailUpdate {
|
|
|
98
98
|
if (Test-OhaiCompanyEmail $line) {
|
|
99
99
|
return $line.Trim()
|
|
100
100
|
}
|
|
101
|
-
Write-Host 'Invalid email.
|
|
101
|
+
Write-Host 'Invalid Company email. Examples: xxxxx@huawei.com or xxxxx@h-partners.com' -ForegroundColor Red
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -104,7 +104,7 @@ function Read-OhaiEmailUpdate {
|
|
|
104
104
|
if (Test-OhaiCompanyEmail $line) {
|
|
105
105
|
return $line.Trim()
|
|
106
106
|
}
|
|
107
|
-
Write-Host 'Invalid email.
|
|
107
|
+
Write-Host 'Invalid Company email. Examples: xxxxx@huawei.com or xxxxx@h-partners.com' -ForegroundColor Red
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -96,7 +96,7 @@ if [[ "$SKIP_EMAIL" -eq 0 ]]; then
|
|
|
96
96
|
USER_EMAIL_SAVE="${line//[[:space:]]/}"
|
|
97
97
|
break
|
|
98
98
|
fi
|
|
99
|
-
echo "Invalid email.
|
|
99
|
+
echo "Invalid Company email. Examples: xxxxx@huawei.com or xxxxx@h-partners.com" >&2
|
|
100
100
|
done
|
|
101
101
|
fi
|
|
102
102
|
fi
|