hissuno 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.
- package/README.md +401 -0
- package/bin/hissuno.mjs +2 -0
- package/dist/index.js +2921 -0
- package/package.json +54 -0
- package/skills/SKILL.md +145 -0
- package/skills/references/CLI-REFERENCE.md +353 -0
- package/skills/references/COMPANIES.md +51 -0
- package/skills/references/CONTACTS.md +83 -0
- package/skills/references/FEEDBACK.md +68 -0
- package/skills/references/GRAPH-TRAVERSAL.md +96 -0
- package/skills/references/INTEGRATIONS.md +83 -0
- package/skills/references/ISSUES.md +72 -0
- package/skills/references/KNOWLEDGE.md +42 -0
- package/skills/references/MCP-TOOLS.md +116 -0
- package/skills/references/PRODUCT-SCOPES.md +95 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2921 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Command as Command13 } from "commander";
|
|
3
|
+
|
|
4
|
+
// src/commands/types.ts
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
|
|
7
|
+
// src/lib/output.ts
|
|
8
|
+
var BOLD = "\x1B[1m";
|
|
9
|
+
var DIM = "\x1B[2m";
|
|
10
|
+
var RESET = "\x1B[0m";
|
|
11
|
+
var CYAN = "\x1B[36m";
|
|
12
|
+
var RED = "\x1B[31m";
|
|
13
|
+
var YELLOW = "\x1B[33m";
|
|
14
|
+
var GREEN = "\x1B[32m";
|
|
15
|
+
var MAGENTA = "\x1B[35m";
|
|
16
|
+
function renderMarkdown(text) {
|
|
17
|
+
return text.split("\n").map((line) => {
|
|
18
|
+
if (line.startsWith("# ")) return `
|
|
19
|
+
${BOLD}${CYAN}${line.slice(2)}${RESET}
|
|
20
|
+
`;
|
|
21
|
+
if (line.startsWith("## ")) return `
|
|
22
|
+
${BOLD}${line.slice(3)}${RESET}`;
|
|
23
|
+
if (line.startsWith("### ")) return `${BOLD}${line.slice(4)}${RESET}`;
|
|
24
|
+
if (line.startsWith("- **")) {
|
|
25
|
+
return line.replace(/\*\*([^*]+)\*\*/g, `${BOLD}$1${RESET}`).replace(/`([^`]+)`/g, `${DIM}$1${RESET}`);
|
|
26
|
+
}
|
|
27
|
+
if (line.startsWith("_") && line.endsWith("_")) {
|
|
28
|
+
return `${DIM}${line.slice(1, -1)}${RESET}`;
|
|
29
|
+
}
|
|
30
|
+
return line.replace(/\*\*([^*]+)\*\*/g, `${BOLD}$1${RESET}`).replace(/`([^`]+)`/g, `${DIM}$1${RESET}`);
|
|
31
|
+
}).join("\n");
|
|
32
|
+
}
|
|
33
|
+
function renderJson(data) {
|
|
34
|
+
return JSON.stringify(data, null, 2);
|
|
35
|
+
}
|
|
36
|
+
function success(message) {
|
|
37
|
+
console.log(`${GREEN}${message}${RESET}`);
|
|
38
|
+
}
|
|
39
|
+
function error(message) {
|
|
40
|
+
console.error(`${RED}${message}${RESET}`);
|
|
41
|
+
}
|
|
42
|
+
function warn(message) {
|
|
43
|
+
console.warn(`${YELLOW}${message}${RESET}`);
|
|
44
|
+
}
|
|
45
|
+
function truncate(str, max) {
|
|
46
|
+
if (str.length <= max) return str;
|
|
47
|
+
return str.slice(0, max - 3) + "...";
|
|
48
|
+
}
|
|
49
|
+
function formatDate(val) {
|
|
50
|
+
if (!val) return "-";
|
|
51
|
+
const s = typeof val === "string" ? val : String(val);
|
|
52
|
+
try {
|
|
53
|
+
const d = new Date(s);
|
|
54
|
+
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
55
|
+
} catch {
|
|
56
|
+
return s;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function label(text) {
|
|
60
|
+
return `${DIM}${text}${RESET}`;
|
|
61
|
+
}
|
|
62
|
+
function heading(text) {
|
|
63
|
+
return `${BOLD}${CYAN}${text}${RESET}`;
|
|
64
|
+
}
|
|
65
|
+
function itemName(text) {
|
|
66
|
+
return `${BOLD}${text}${RESET}`;
|
|
67
|
+
}
|
|
68
|
+
function dimText(text) {
|
|
69
|
+
return `${DIM}${text}${RESET}`;
|
|
70
|
+
}
|
|
71
|
+
function badge(text, color = YELLOW) {
|
|
72
|
+
return `${color}${text}${RESET}`;
|
|
73
|
+
}
|
|
74
|
+
function formatResourceList(type, items, total) {
|
|
75
|
+
const lines = [heading(`${capitalize(type)} (${total} total)`), ""];
|
|
76
|
+
if (items.length === 0) {
|
|
77
|
+
lines.push(dimText("No results found."));
|
|
78
|
+
return lines.join("\n");
|
|
79
|
+
}
|
|
80
|
+
for (const item of items) {
|
|
81
|
+
const name = item.name ?? item.title ?? "Untitled";
|
|
82
|
+
switch (type) {
|
|
83
|
+
case "feedback":
|
|
84
|
+
lines.push(formatFeedbackRow(item, name));
|
|
85
|
+
break;
|
|
86
|
+
case "issues":
|
|
87
|
+
lines.push(formatIssueRow(item, name));
|
|
88
|
+
break;
|
|
89
|
+
case "contacts":
|
|
90
|
+
lines.push(formatContactRow(item, name));
|
|
91
|
+
break;
|
|
92
|
+
case "companies":
|
|
93
|
+
lines.push(formatCompanyRow(item, name));
|
|
94
|
+
break;
|
|
95
|
+
case "knowledge":
|
|
96
|
+
lines.push(formatKnowledgeRow(item, name));
|
|
97
|
+
break;
|
|
98
|
+
case "sources":
|
|
99
|
+
lines.push(formatSourceRow(item, name));
|
|
100
|
+
break;
|
|
101
|
+
case "scopes":
|
|
102
|
+
lines.push(formatScopeRow(item, name));
|
|
103
|
+
break;
|
|
104
|
+
default:
|
|
105
|
+
lines.push(` ${itemName(name)} ${dimText(String(item.id ?? ""))}`);
|
|
106
|
+
}
|
|
107
|
+
lines.push("");
|
|
108
|
+
}
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
111
|
+
function capitalize(s) {
|
|
112
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
113
|
+
}
|
|
114
|
+
function formatFeedbackRow(item, name) {
|
|
115
|
+
const id = dimText(truncate(String(item.id ?? ""), 12));
|
|
116
|
+
const source = item.source ? badge(String(item.source)) : "";
|
|
117
|
+
const status = item.status ? label(String(item.status)) : "";
|
|
118
|
+
const msgs = item.message_count != null ? `${item.message_count} msgs` : "";
|
|
119
|
+
const tags = Array.isArray(item.tags) && item.tags.length > 0 ? dimText(item.tags.join(", ")) : "";
|
|
120
|
+
const date = formatDate(item.last_activity_at);
|
|
121
|
+
const parts = [source, status, msgs, tags, date].filter(Boolean);
|
|
122
|
+
return ` ${itemName(truncate(name, 40))} ${id}
|
|
123
|
+
${parts.join(" ")}`;
|
|
124
|
+
}
|
|
125
|
+
function formatIssueRow(item, name) {
|
|
126
|
+
const id = dimText(truncate(String(item.id ?? ""), 12));
|
|
127
|
+
const type = item.type ? badge(String(item.type), MAGENTA) : "";
|
|
128
|
+
const priority = item.priority ? formatPriority(String(item.priority)) : "";
|
|
129
|
+
const status = item.status ? label(String(item.status)) : "";
|
|
130
|
+
const upvotes = item.upvote_count != null ? `${item.upvote_count} upvotes` : "";
|
|
131
|
+
const parts = [type, priority, status, upvotes].filter(Boolean);
|
|
132
|
+
return ` ${itemName(truncate(name, 50))} ${id}
|
|
133
|
+
${parts.join(" ")}`;
|
|
134
|
+
}
|
|
135
|
+
function formatPriority(p) {
|
|
136
|
+
switch (p) {
|
|
137
|
+
case "high":
|
|
138
|
+
return `${RED}high${RESET}`;
|
|
139
|
+
case "medium":
|
|
140
|
+
return `${YELLOW}medium${RESET}`;
|
|
141
|
+
case "low":
|
|
142
|
+
return `${GREEN}low${RESET}`;
|
|
143
|
+
default:
|
|
144
|
+
return label(p);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function formatContactRow(item, name) {
|
|
148
|
+
const email = item.email ? dimText(String(item.email)) : "";
|
|
149
|
+
const role = item.role ? label(String(item.role)) : "";
|
|
150
|
+
const title = item.title ? label(String(item.title)) : "";
|
|
151
|
+
const champion = item.is_champion ? badge("champion", GREEN) : "";
|
|
152
|
+
const parts = [email, role, title, champion].filter(Boolean);
|
|
153
|
+
return ` ${itemName(name)} ${parts.join(" ")}`;
|
|
154
|
+
}
|
|
155
|
+
function formatCompanyRow(item, name) {
|
|
156
|
+
const id = dimText(truncate(String(item.id ?? ""), 12));
|
|
157
|
+
const domain = item.domain ? dimText(String(item.domain)) : "";
|
|
158
|
+
const stage = item.stage ? badge(String(item.stage)) : "";
|
|
159
|
+
const arr = item.arr != null ? `$${Number(item.arr).toLocaleString()} ARR` : "";
|
|
160
|
+
const industry = item.industry ? label(String(item.industry)) : "";
|
|
161
|
+
const health = item.health_score != null ? `health: ${item.health_score}` : "";
|
|
162
|
+
const contactCount = item.contact_count != null ? `${item.contact_count} contacts` : "";
|
|
163
|
+
const parts = [domain, stage, arr, industry, health, contactCount].filter(Boolean);
|
|
164
|
+
return ` ${itemName(name)} ${id}
|
|
165
|
+
${parts.join(" ")}`;
|
|
166
|
+
}
|
|
167
|
+
function formatScopeRow(item, name) {
|
|
168
|
+
const id = dimText(truncate(String(item.id ?? ""), 12));
|
|
169
|
+
const type = item.type ? badge(String(item.type), MAGENTA) : "";
|
|
170
|
+
const isDefault = item.is_default ? badge("default", GREEN) : "";
|
|
171
|
+
const goals = Array.isArray(item.goals) && item.goals.length > 0 ? `${item.goals.length} goals` : "";
|
|
172
|
+
const desc = item.description ? dimText(truncate(String(item.description), 40)) : "";
|
|
173
|
+
const parts = [type, isDefault, goals, desc].filter(Boolean);
|
|
174
|
+
return ` ${itemName(name)} ${id}
|
|
175
|
+
${parts.join(" ")}`;
|
|
176
|
+
}
|
|
177
|
+
function formatKnowledgeRow(item, name) {
|
|
178
|
+
const desc = item.description ? dimText(truncate(String(item.description), 60)) : "";
|
|
179
|
+
const sources = item.sourceCount != null ? `${item.sourceCount} sources` : "";
|
|
180
|
+
const lastAnalyzed = item.lastAnalyzedAt ? `analyzed ${formatDate(item.lastAnalyzedAt)}` : dimText("not analyzed");
|
|
181
|
+
const parts = [desc, sources, lastAnalyzed].filter(Boolean);
|
|
182
|
+
return ` ${itemName(name)}
|
|
183
|
+
${parts.join(" ")}`;
|
|
184
|
+
}
|
|
185
|
+
function formatSourceRow(item, name) {
|
|
186
|
+
const id = dimText(truncate(String(item.id ?? ""), 12));
|
|
187
|
+
const type = item.type ? badge(String(item.type), MAGENTA) : "";
|
|
188
|
+
const status = item.status ? label(String(item.status)) : "";
|
|
189
|
+
const url = item.url ? dimText(truncate(String(item.url), 40)) : "";
|
|
190
|
+
const analyzed = item.analyzed_at ? `analyzed ${formatDate(item.analyzed_at)}` : dimText("not analyzed");
|
|
191
|
+
const enabled = item.enabled === false ? badge("disabled", RED) : "";
|
|
192
|
+
const parts = [type, status, enabled, url, analyzed].filter(Boolean);
|
|
193
|
+
return ` ${itemName(truncate(name, 40))} ${id}
|
|
194
|
+
${parts.join(" ")}`;
|
|
195
|
+
}
|
|
196
|
+
function formatResourceDetail(type, item, extra) {
|
|
197
|
+
const name = item.name ?? item.title ?? "Untitled";
|
|
198
|
+
const lines = [heading(name), ""];
|
|
199
|
+
if (item.id) lines.push(`${label("ID:")} ${item.id}`);
|
|
200
|
+
switch (type) {
|
|
201
|
+
case "feedback":
|
|
202
|
+
formatFeedbackDetail(lines, item, extra);
|
|
203
|
+
break;
|
|
204
|
+
case "issues":
|
|
205
|
+
formatIssueDetail(lines, item);
|
|
206
|
+
break;
|
|
207
|
+
case "contacts":
|
|
208
|
+
formatContactDetail(lines, item);
|
|
209
|
+
break;
|
|
210
|
+
case "companies":
|
|
211
|
+
formatCompanyDetail(lines, item);
|
|
212
|
+
break;
|
|
213
|
+
case "knowledge":
|
|
214
|
+
formatKnowledgeDetail(lines, item);
|
|
215
|
+
break;
|
|
216
|
+
case "sources":
|
|
217
|
+
formatSourceDetail(lines, item);
|
|
218
|
+
break;
|
|
219
|
+
case "scopes":
|
|
220
|
+
formatScopeDetail(lines, item);
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
return lines.join("\n");
|
|
224
|
+
}
|
|
225
|
+
function formatFeedbackDetail(lines, item, extra) {
|
|
226
|
+
if (item.source) lines.push(`${label("Source:")} ${item.source}`);
|
|
227
|
+
if (item.status) lines.push(`${label("Status:")} ${item.status}`);
|
|
228
|
+
if (item.message_count != null) lines.push(`${label("Messages:")} ${item.message_count}`);
|
|
229
|
+
if (Array.isArray(item.tags) && item.tags.length > 0) lines.push(`${label("Tags:")} ${item.tags.join(", ")}`);
|
|
230
|
+
if (item.created_at) lines.push(`${label("Created:")} ${formatDate(item.created_at)}`);
|
|
231
|
+
const messages = extra?.messages ?? item.messages;
|
|
232
|
+
if (messages && messages.length > 0) {
|
|
233
|
+
lines.push("", heading("Conversation"), "");
|
|
234
|
+
for (const msg of messages) {
|
|
235
|
+
const role = msg.sender_type === "user" || msg.role === "user" ? "Customer" : "Agent";
|
|
236
|
+
lines.push(`${BOLD}${role}:${RESET} ${msg.content}`, "");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function formatIssueDetail(lines, item) {
|
|
241
|
+
if (item.type) lines.push(`${label("Type:")} ${item.type}`);
|
|
242
|
+
if (item.priority) lines.push(`${label("Priority:")} ${formatPriority(String(item.priority))}`);
|
|
243
|
+
if (item.status) lines.push(`${label("Status:")} ${item.status}`);
|
|
244
|
+
if (item.upvote_count != null) lines.push(`${label("Upvotes:")} ${item.upvote_count}`);
|
|
245
|
+
if (item.created_at) lines.push(`${label("Created:")} ${formatDate(item.created_at)}`);
|
|
246
|
+
if (item.description) {
|
|
247
|
+
lines.push("", String(item.description));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function formatContactDetail(lines, item) {
|
|
251
|
+
if (item.email) lines.push(`${label("Email:")} ${item.email}`);
|
|
252
|
+
if (item.role) lines.push(`${label("Role:")} ${item.role}`);
|
|
253
|
+
if (item.title && typeof item.title === "string") lines.push(`${label("Title:")} ${item.title}`);
|
|
254
|
+
if (item.phone) lines.push(`${label("Phone:")} ${item.phone}`);
|
|
255
|
+
if (item.is_champion) lines.push(`${label("Champion:")} ${badge("Yes", GREEN)}`);
|
|
256
|
+
if (item.company_name || item.company) {
|
|
257
|
+
const name = item.company_name ?? item.company?.name;
|
|
258
|
+
if (name) lines.push(`${label("Company:")} ${name}`);
|
|
259
|
+
}
|
|
260
|
+
if (item.session_count != null) lines.push(`${label("Feedback sessions:")} ${item.session_count}`);
|
|
261
|
+
if (item.issue_count != null) lines.push(`${label("Issues:")} ${item.issue_count}`);
|
|
262
|
+
}
|
|
263
|
+
function formatCompanyDetail(lines, item) {
|
|
264
|
+
if (item.domain) lines.push(`${label("Domain:")} ${item.domain}`);
|
|
265
|
+
if (item.industry) lines.push(`${label("Industry:")} ${item.industry}`);
|
|
266
|
+
if (item.country) lines.push(`${label("Country:")} ${item.country}`);
|
|
267
|
+
if (item.arr != null) lines.push(`${label("ARR:")} $${Number(item.arr).toLocaleString()}`);
|
|
268
|
+
if (item.stage) lines.push(`${label("Stage:")} ${badge(String(item.stage))}`);
|
|
269
|
+
if (item.plan_tier) lines.push(`${label("Plan:")} ${item.plan_tier}`);
|
|
270
|
+
if (item.employee_count != null) lines.push(`${label("Employees:")} ${item.employee_count}`);
|
|
271
|
+
if (item.health_score != null) lines.push(`${label("Health Score:")} ${item.health_score}`);
|
|
272
|
+
if (item.renewal_date) lines.push(`${label("Renewal:")} ${formatDate(item.renewal_date)}`);
|
|
273
|
+
if (item.notes) lines.push(`${label("Notes:")} ${item.notes}`);
|
|
274
|
+
if (item.created_at) lines.push(`${label("Created:")} ${formatDate(item.created_at)}`);
|
|
275
|
+
const companyContacts = item.contacts;
|
|
276
|
+
if (companyContacts && companyContacts.length > 0) {
|
|
277
|
+
lines.push("", `${BOLD}Contacts (${companyContacts.length}):${RESET}`);
|
|
278
|
+
for (const c of companyContacts) {
|
|
279
|
+
lines.push(` - ${c.name} (${c.email})`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function formatScopeDetail(lines, item) {
|
|
284
|
+
if (item.slug) lines.push(`${label("Slug:")} ${item.slug}`);
|
|
285
|
+
if (item.type) lines.push(`${label("Type:")} ${item.type}`);
|
|
286
|
+
if (item.is_default) lines.push(`${label("Default:")} ${badge("Yes", GREEN)}`);
|
|
287
|
+
if (item.description) lines.push(`${label("Description:")} ${item.description}`);
|
|
288
|
+
if (item.created_at) lines.push(`${label("Created:")} ${formatDate(item.created_at)}`);
|
|
289
|
+
const goals = item.goals;
|
|
290
|
+
if (goals && goals.length > 0) {
|
|
291
|
+
lines.push("", `${BOLD}Goals (${goals.length}):${RESET}`);
|
|
292
|
+
for (const goal of goals) {
|
|
293
|
+
lines.push(` - ${goal.text}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function formatKnowledgeDetail(lines, item) {
|
|
298
|
+
if (item.description) lines.push(`${label("Description:")} ${item.description}`);
|
|
299
|
+
if (item.sourceCount != null) lines.push(`${label("Sources:")} ${item.sourceCount}`);
|
|
300
|
+
if (item.lastAnalyzedAt) lines.push(`${label("Last analyzed:")} ${formatDate(item.lastAnalyzedAt)}`);
|
|
301
|
+
if (item.compiled_at) lines.push(`${label("Compiled:")} ${formatDate(item.compiled_at)}`);
|
|
302
|
+
const sources = item.sources;
|
|
303
|
+
if (sources && sources.length > 0) {
|
|
304
|
+
lines.push("", `${BOLD}Sources:${RESET}`);
|
|
305
|
+
for (const src of sources) {
|
|
306
|
+
const parts = [src.type, src.status].filter(Boolean).join(" - ");
|
|
307
|
+
lines.push(` - ${src.name ?? "Unnamed"}${parts ? ` (${parts})` : ""}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function formatSourceDetail(lines, item) {
|
|
312
|
+
if (item.type) lines.push(`${label("Type:")} ${badge(String(item.type), MAGENTA)}`);
|
|
313
|
+
if (item.status) lines.push(`${label("Status:")} ${item.status}`);
|
|
314
|
+
if (item.enabled !== void 0) lines.push(`${label("Enabled:")} ${item.enabled ? badge("Yes", GREEN) : badge("No", RED)}`);
|
|
315
|
+
if (item.url) lines.push(`${label("URL:")} ${item.url}`);
|
|
316
|
+
if (item.description) lines.push(`${label("Description:")} ${item.description}`);
|
|
317
|
+
if (item.analysis_scope) lines.push(`${label("Analysis scope:")} ${item.analysis_scope}`);
|
|
318
|
+
if (item.analyzed_at) lines.push(`${label("Analyzed:")} ${formatDate(item.analyzed_at)}`);
|
|
319
|
+
if (item.created_at) lines.push(`${label("Created:")} ${formatDate(item.created_at)}`);
|
|
320
|
+
if (item.product_scope_id) lines.push(`${label("Product scope:")} ${item.product_scope_id}`);
|
|
321
|
+
}
|
|
322
|
+
function formatSearchResults(results) {
|
|
323
|
+
const lines = [heading(`Search Results (${results.length} found)`), ""];
|
|
324
|
+
if (results.length === 0) {
|
|
325
|
+
lines.push(dimText("No results found."));
|
|
326
|
+
return lines.join("\n");
|
|
327
|
+
}
|
|
328
|
+
for (const r of results) {
|
|
329
|
+
const typeTag = badge(`[${r.type}]`, MAGENTA);
|
|
330
|
+
const score = r.score != null ? dimText(`${Math.round(r.score * 100)}%`) : "";
|
|
331
|
+
lines.push(` ${typeTag} ${itemName(r.name)} ${score}`);
|
|
332
|
+
lines.push(` ${dimText(r.id)}`);
|
|
333
|
+
if (r.snippet) lines.push(` ${truncate(r.snippet, 80)}`);
|
|
334
|
+
lines.push("");
|
|
335
|
+
}
|
|
336
|
+
return lines.join("\n");
|
|
337
|
+
}
|
|
338
|
+
function formatRelatedEntities(relationships) {
|
|
339
|
+
const sections = [];
|
|
340
|
+
const labelMap = {
|
|
341
|
+
companies: "Companies",
|
|
342
|
+
contacts: "Contacts",
|
|
343
|
+
issues: "Issues",
|
|
344
|
+
sessions: "Feedback",
|
|
345
|
+
knowledgeSources: "Knowledge",
|
|
346
|
+
productScopes: "Product Scopes"
|
|
347
|
+
};
|
|
348
|
+
for (const [key, items] of Object.entries(relationships)) {
|
|
349
|
+
if (!Array.isArray(items) || items.length === 0) continue;
|
|
350
|
+
const lbl = labelMap[key] ?? key;
|
|
351
|
+
const names = items.map((i) => i.name ?? i.title ?? i.id).join(", ");
|
|
352
|
+
sections.push(` ${lbl}: ${names}`);
|
|
353
|
+
}
|
|
354
|
+
if (sections.length === 0) return "";
|
|
355
|
+
return `
|
|
356
|
+
Related:
|
|
357
|
+
${sections.join("\n")}`;
|
|
358
|
+
}
|
|
359
|
+
function formatResourceTypes() {
|
|
360
|
+
return [
|
|
361
|
+
heading("Hissuno Resource Types"),
|
|
362
|
+
"",
|
|
363
|
+
`${BOLD}knowledge${RESET}`,
|
|
364
|
+
" Knowledge packages (grouped, compiled knowledge bundles).",
|
|
365
|
+
` ${label("Filters:")} (none)`,
|
|
366
|
+
` ${label("Search:")} Semantic vector search across all knowledge chunks`,
|
|
367
|
+
"",
|
|
368
|
+
`${BOLD}sources${RESET}`,
|
|
369
|
+
" Individual knowledge sources (codebases, documents, URLs, Notion pages).",
|
|
370
|
+
` ${label("Filters:")} (none)`,
|
|
371
|
+
"",
|
|
372
|
+
`${BOLD}feedback${RESET}`,
|
|
373
|
+
" Customer feedback sessions (conversations from widget, Slack, Intercom, etc.).",
|
|
374
|
+
` ${label("Filters:")} --source, --status, --tags, --contact-id, --search`,
|
|
375
|
+
` ${label("Search:")} Semantic vector search (full-text fallback)`,
|
|
376
|
+
` ${label("Add:")} messages (required), name, tags`,
|
|
377
|
+
"",
|
|
378
|
+
`${BOLD}issues${RESET}`,
|
|
379
|
+
" Product issues (bugs, feature requests, change requests).",
|
|
380
|
+
` ${label("Filters:")} --issue-type, --priority, --status, --search`,
|
|
381
|
+
` ${label("Search:")} Semantic vector search for similar issues`,
|
|
382
|
+
` ${label("Add:")} type, title, description (required), priority`,
|
|
383
|
+
"",
|
|
384
|
+
`${BOLD}customers${RESET}`,
|
|
385
|
+
" Customers (contacts and companies). Use --customer-type to select (default: contacts).",
|
|
386
|
+
` ${label("Sub-types:")} contacts (people), companies (organizations)`,
|
|
387
|
+
` ${label("Filters (contacts):")} --search, --company-id, --role`,
|
|
388
|
+
` ${label("Filters (companies):")} --search, --stage, --industry`,
|
|
389
|
+
` ${label("Search:")} Semantic vector search (contacts only, name/email fallback)`,
|
|
390
|
+
` ${label("Add (contacts):")} name, email (required), role, title, phone, company_id, is_champion`,
|
|
391
|
+
` ${label("Add (companies):")} name, domain (required), industry, arr, stage, employee_count, plan_tier, country, notes`,
|
|
392
|
+
"",
|
|
393
|
+
`${BOLD}scopes${RESET}`,
|
|
394
|
+
" Product scopes (product areas and initiatives) with goals.",
|
|
395
|
+
` ${label("Add:")} name, type (required), description, goals`,
|
|
396
|
+
` ${label("Update:")} name, type, description, goals`
|
|
397
|
+
].join("\n");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/commands/types.ts
|
|
401
|
+
var RESOURCE_TYPE_DEFINITIONS = {
|
|
402
|
+
knowledge: {
|
|
403
|
+
description: "Knowledge packages (grouped, compiled knowledge bundles).",
|
|
404
|
+
filters: [],
|
|
405
|
+
search: "Semantic vector search across all knowledge chunks",
|
|
406
|
+
add: null
|
|
407
|
+
},
|
|
408
|
+
sources: {
|
|
409
|
+
description: "Individual knowledge sources (codebases, documents, URLs, Notion pages).",
|
|
410
|
+
filters: [],
|
|
411
|
+
search: null,
|
|
412
|
+
add: null
|
|
413
|
+
},
|
|
414
|
+
feedback: {
|
|
415
|
+
description: "Customer feedback sessions (conversations from widget, Slack, Intercom, etc.).",
|
|
416
|
+
filters: ["source", "status", "tags", "contact_id", "search"],
|
|
417
|
+
search: "Semantic vector search (full-text fallback)",
|
|
418
|
+
add: { required: ["messages"], optional: ["name", "tags"] }
|
|
419
|
+
},
|
|
420
|
+
issues: {
|
|
421
|
+
description: "Product issues (bugs, feature requests, change requests).",
|
|
422
|
+
filters: ["type", "priority", "status", "search"],
|
|
423
|
+
search: "Semantic vector search for similar issues",
|
|
424
|
+
add: { required: ["type", "title", "description"], optional: ["priority"] }
|
|
425
|
+
},
|
|
426
|
+
customers: {
|
|
427
|
+
description: "Customers (contacts and companies). Use --customer-type to select (default: contacts).",
|
|
428
|
+
filters: ["customer_type", "search", "company_id", "role", "stage", "industry"],
|
|
429
|
+
search: "Semantic vector search (contacts only, name/email fallback)",
|
|
430
|
+
add: {
|
|
431
|
+
contacts: { required: ["name", "email"], optional: ["role", "title", "phone", "company_id", "is_champion"] },
|
|
432
|
+
companies: { required: ["name", "domain"], optional: ["industry", "arr", "stage", "employee_count", "plan_tier", "country", "notes"] }
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
scopes: {
|
|
436
|
+
description: "Product scopes (product areas and initiatives) with goals.",
|
|
437
|
+
filters: [],
|
|
438
|
+
search: null,
|
|
439
|
+
add: { required: ["name", "type"], optional: ["description", "goals"] },
|
|
440
|
+
update: { optional: ["name", "type", "description", "goals"] }
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
var typesCommand = new Command("types").description("List all available resource types and their filters").action(async (_opts, cmd) => {
|
|
444
|
+
const jsonMode = cmd.parent?.opts().json;
|
|
445
|
+
if (jsonMode) {
|
|
446
|
+
console.log(renderJson(RESOURCE_TYPE_DEFINITIONS));
|
|
447
|
+
} else {
|
|
448
|
+
console.log(formatResourceTypes());
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// src/commands/list.ts
|
|
453
|
+
import { Command as Command2 } from "commander";
|
|
454
|
+
|
|
455
|
+
// src/lib/config.ts
|
|
456
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
457
|
+
import { homedir } from "os";
|
|
458
|
+
import { join } from "path";
|
|
459
|
+
var PROFILE_NAME_RE = /^[a-z0-9-]+$/;
|
|
460
|
+
var CONFIG_DIR = join(homedir(), ".hissuno");
|
|
461
|
+
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
462
|
+
function isMultiProfileConfig(raw) {
|
|
463
|
+
return "profiles" in raw && typeof raw.profiles === "object";
|
|
464
|
+
}
|
|
465
|
+
function parseLegacyProfile(parsed) {
|
|
466
|
+
if (!parsed.api_key) return null;
|
|
467
|
+
let baseUrl = parsed.base_url;
|
|
468
|
+
if (!baseUrl && parsed.endpoint) {
|
|
469
|
+
baseUrl = new URL(parsed.endpoint).origin;
|
|
470
|
+
}
|
|
471
|
+
if (!baseUrl) return null;
|
|
472
|
+
return {
|
|
473
|
+
api_key: parsed.api_key,
|
|
474
|
+
base_url: baseUrl,
|
|
475
|
+
...parsed.project_id ? { project_id: parsed.project_id } : {},
|
|
476
|
+
...parsed.username ? { username: parsed.username } : {}
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
function readRawConfig() {
|
|
480
|
+
if (!existsSync(CONFIG_PATH)) return null;
|
|
481
|
+
try {
|
|
482
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
483
|
+
} catch {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function writeRawConfig(raw) {
|
|
488
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
489
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
490
|
+
}
|
|
491
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(raw, null, 2) + "\n", "utf-8");
|
|
492
|
+
}
|
|
493
|
+
function loadConfig() {
|
|
494
|
+
const raw = readRawConfig();
|
|
495
|
+
if (!raw) return null;
|
|
496
|
+
try {
|
|
497
|
+
if (isMultiProfileConfig(raw)) {
|
|
498
|
+
const profile = raw.profiles[raw.active_profile];
|
|
499
|
+
if (!profile) return null;
|
|
500
|
+
return { ...profile };
|
|
501
|
+
}
|
|
502
|
+
return parseLegacyProfile(raw);
|
|
503
|
+
} catch {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function saveConfig(config) {
|
|
508
|
+
const raw = readRawConfig();
|
|
509
|
+
if (raw && isMultiProfileConfig(raw)) {
|
|
510
|
+
raw.profiles[raw.active_profile] = {
|
|
511
|
+
api_key: config.api_key,
|
|
512
|
+
base_url: config.base_url,
|
|
513
|
+
...config.project_id ? { project_id: config.project_id } : {},
|
|
514
|
+
...config.username ? { username: config.username } : {}
|
|
515
|
+
};
|
|
516
|
+
writeRawConfig(raw);
|
|
517
|
+
} else {
|
|
518
|
+
writeRawConfig(config);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function requireConfig() {
|
|
522
|
+
const config = loadConfig();
|
|
523
|
+
if (!config) {
|
|
524
|
+
console.error("Not configured. Run `hissuno config` to set up your API key and URL.");
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
return config;
|
|
528
|
+
}
|
|
529
|
+
function loadFullConfig() {
|
|
530
|
+
const raw = readRawConfig();
|
|
531
|
+
if (!raw) return null;
|
|
532
|
+
if (isMultiProfileConfig(raw)) return raw;
|
|
533
|
+
const legacy = parseLegacyProfile(raw);
|
|
534
|
+
if (!legacy) return null;
|
|
535
|
+
return {
|
|
536
|
+
active_profile: "default",
|
|
537
|
+
profiles: { default: legacy }
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
function getActiveProfileName() {
|
|
541
|
+
const raw = readRawConfig();
|
|
542
|
+
if (raw && isMultiProfileConfig(raw)) return raw.active_profile;
|
|
543
|
+
return "default";
|
|
544
|
+
}
|
|
545
|
+
function setActiveProfile(name) {
|
|
546
|
+
const full = loadFullConfig();
|
|
547
|
+
if (!full) throw new Error("No configuration found. Run `hissuno config` first.");
|
|
548
|
+
if (!full.profiles[name]) throw new Error(`Profile "${name}" does not exist.`);
|
|
549
|
+
full.active_profile = name;
|
|
550
|
+
writeRawConfig(full);
|
|
551
|
+
}
|
|
552
|
+
function createProfile(name, config) {
|
|
553
|
+
validateProfileName(name);
|
|
554
|
+
let full = loadFullConfig();
|
|
555
|
+
if (!full) {
|
|
556
|
+
full = { active_profile: name, profiles: {} };
|
|
557
|
+
}
|
|
558
|
+
if (full.profiles[name]) throw new Error(`Profile "${name}" already exists.`);
|
|
559
|
+
full.profiles[name] = config;
|
|
560
|
+
writeRawConfig(full);
|
|
561
|
+
}
|
|
562
|
+
function deleteProfile(name) {
|
|
563
|
+
const full = loadFullConfig();
|
|
564
|
+
if (!full) throw new Error("No configuration found.");
|
|
565
|
+
if (!full.profiles[name]) throw new Error(`Profile "${name}" does not exist.`);
|
|
566
|
+
if (full.active_profile === name) throw new Error(`Cannot delete the active profile "${name}". Switch to another profile first.`);
|
|
567
|
+
delete full.profiles[name];
|
|
568
|
+
writeRawConfig(full);
|
|
569
|
+
}
|
|
570
|
+
function migrateToMultiProfile() {
|
|
571
|
+
const raw = readRawConfig();
|
|
572
|
+
if (raw && isMultiProfileConfig(raw)) return raw;
|
|
573
|
+
const legacy = raw ? parseLegacyProfile(raw) : null;
|
|
574
|
+
const full = {
|
|
575
|
+
active_profile: "default",
|
|
576
|
+
profiles: legacy ? { default: legacy } : {}
|
|
577
|
+
};
|
|
578
|
+
writeRawConfig(full);
|
|
579
|
+
return full;
|
|
580
|
+
}
|
|
581
|
+
function validateProfileName(name) {
|
|
582
|
+
if (!name) throw new Error("Profile name is required.");
|
|
583
|
+
if (name.length > 30) throw new Error("Profile name must be 30 characters or fewer.");
|
|
584
|
+
if (!PROFILE_NAME_RE.test(name)) throw new Error("Profile name must contain only lowercase letters, numbers, and hyphens.");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/lib/api.ts
|
|
588
|
+
function getBaseUrl(config) {
|
|
589
|
+
return config.base_url;
|
|
590
|
+
}
|
|
591
|
+
async function resolveProjectId(config) {
|
|
592
|
+
if (config.project_id) return config.project_id;
|
|
593
|
+
const result = await apiCall(config, "GET", "/api/projects");
|
|
594
|
+
const projects = Array.isArray(result.data) ? result.data : result.data?.projects;
|
|
595
|
+
if (!result.ok || !Array.isArray(projects) || projects.length === 0) {
|
|
596
|
+
error("Could not determine project from API key.");
|
|
597
|
+
process.exit(1);
|
|
598
|
+
}
|
|
599
|
+
const projectId = projects[0].id;
|
|
600
|
+
config.project_id = projectId;
|
|
601
|
+
saveConfig(config);
|
|
602
|
+
return projectId;
|
|
603
|
+
}
|
|
604
|
+
function buildPath(path7, params) {
|
|
605
|
+
const entries = Object.entries(params).filter(
|
|
606
|
+
(entry) => entry[1] !== void 0
|
|
607
|
+
);
|
|
608
|
+
if (entries.length === 0) return path7;
|
|
609
|
+
const qs = entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
|
|
610
|
+
return `${path7}?${qs}`;
|
|
611
|
+
}
|
|
612
|
+
async function apiCall(config, method, path7, body) {
|
|
613
|
+
const baseUrl = getBaseUrl(config);
|
|
614
|
+
const url = `${baseUrl}${path7}`;
|
|
615
|
+
const headers = {
|
|
616
|
+
Authorization: `Bearer ${config.api_key}`
|
|
617
|
+
};
|
|
618
|
+
if (body) {
|
|
619
|
+
headers["Content-Type"] = "application/json";
|
|
620
|
+
}
|
|
621
|
+
const response = await fetch(url, {
|
|
622
|
+
method,
|
|
623
|
+
headers,
|
|
624
|
+
body: body ? JSON.stringify(body) : void 0
|
|
625
|
+
});
|
|
626
|
+
const data = await response.json().catch(() => ({}));
|
|
627
|
+
return { ok: response.ok, status: response.status, data };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/lib/customer-type.ts
|
|
631
|
+
var CUSTOMER_TYPES = ["contacts", "companies"];
|
|
632
|
+
function resolveCustomerType(opt) {
|
|
633
|
+
const ct = opt ?? "contacts";
|
|
634
|
+
if (!CUSTOMER_TYPES.includes(ct)) {
|
|
635
|
+
error(`Invalid customer type "${ct}". Must be one of: ${CUSTOMER_TYPES.join(", ")}`);
|
|
636
|
+
process.exit(1);
|
|
637
|
+
}
|
|
638
|
+
return ct;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// src/commands/list.ts
|
|
642
|
+
var TYPE_ENDPOINTS = {
|
|
643
|
+
knowledge: { path: "/api/knowledge/packages", key: "packages" },
|
|
644
|
+
sources: { path: "/api/knowledge/sources", key: "sources" },
|
|
645
|
+
feedback: { path: "/api/sessions", key: "sessions" },
|
|
646
|
+
issues: { path: "/api/issues", key: "issues" },
|
|
647
|
+
customers: { path: "/api/contacts", key: "contacts" },
|
|
648
|
+
scopes: { path: "/api/product-scopes", key: "scopes" }
|
|
649
|
+
};
|
|
650
|
+
var listCommand = new Command2("list").description("List resources of a given type").argument("<type>", "Resource type: knowledge, sources, feedback, issues, customers, scopes").option("--source <source>", "Filter feedback by source (widget|slack|intercom|gong|api|manual)").option("--status <status>", "Filter by status").option("--tags <tags>", "Filter feedback by tags (comma-separated)").option("--contact-id <id>", "Filter feedback by contact ID").option("--search <query>", "Text search filter").option("--issue-type <type>", "Filter issues by type (bug|feature_request|change_request)").option("--priority <priority>", "Filter issues by priority (low|medium|high)").option("--company-id <id>", "Filter contacts by company ID").option("--role <role>", "Filter contacts by role").option("--customer-type <type>", "Customer sub-type: contacts (default) or companies").option("--stage <stage>", "Filter companies by stage (prospect|onboarding|active|churned|expansion)").option("--industry <industry>", "Filter companies by industry").option("--limit <n>", "Max results (default: 20)", "20").action(async (type, opts, cmd) => {
|
|
651
|
+
const config = requireConfig();
|
|
652
|
+
const jsonMode = cmd.parent?.opts().json;
|
|
653
|
+
const validTypes = Object.keys(TYPE_ENDPOINTS);
|
|
654
|
+
if (!validTypes.includes(type)) {
|
|
655
|
+
error(`Invalid type "${type}". Must be one of: ${validTypes.join(", ")}`);
|
|
656
|
+
process.exit(1);
|
|
657
|
+
}
|
|
658
|
+
const projectId = await resolveProjectId(config);
|
|
659
|
+
let endpoint;
|
|
660
|
+
let displayType = type;
|
|
661
|
+
if (type === "customers") {
|
|
662
|
+
const customerType = resolveCustomerType(opts.customerType);
|
|
663
|
+
endpoint = { path: `/api/${customerType}`, key: customerType };
|
|
664
|
+
displayType = customerType;
|
|
665
|
+
} else {
|
|
666
|
+
endpoint = TYPE_ENDPOINTS[type];
|
|
667
|
+
}
|
|
668
|
+
const params = {
|
|
669
|
+
projectId,
|
|
670
|
+
limit: parseInt(opts.limit, 10)
|
|
671
|
+
};
|
|
672
|
+
if (opts.source) params.source = opts.source;
|
|
673
|
+
if (opts.status) params.status = opts.status;
|
|
674
|
+
if (opts.tags) params.tags = opts.tags;
|
|
675
|
+
if (opts.contactId) params.contactId = opts.contactId;
|
|
676
|
+
if (opts.search) params.search = opts.search;
|
|
677
|
+
if (opts.issueType) params.type = opts.issueType;
|
|
678
|
+
if (opts.priority) params.priority = opts.priority;
|
|
679
|
+
if (opts.companyId) params.companyId = opts.companyId;
|
|
680
|
+
if (opts.role) params.role = opts.role;
|
|
681
|
+
if (opts.stage) params.stage = opts.stage;
|
|
682
|
+
if (opts.industry) params.industry = opts.industry;
|
|
683
|
+
try {
|
|
684
|
+
const result = await apiCall(config, "GET", buildPath(endpoint.path, params));
|
|
685
|
+
if (!result.ok) {
|
|
686
|
+
const data = result.data;
|
|
687
|
+
error(`Failed: ${data.error || `HTTP ${result.status}`}`);
|
|
688
|
+
process.exit(1);
|
|
689
|
+
}
|
|
690
|
+
const items = result.data[endpoint.key] ?? [];
|
|
691
|
+
const total = result.data.total ?? items.length;
|
|
692
|
+
if (jsonMode) {
|
|
693
|
+
console.log(renderJson(result.data));
|
|
694
|
+
} else {
|
|
695
|
+
console.log(formatResourceList(displayType, items, total));
|
|
696
|
+
}
|
|
697
|
+
} catch (err) {
|
|
698
|
+
error(`Failed: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
699
|
+
process.exit(1);
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
// src/commands/get.ts
|
|
704
|
+
import { Command as Command3 } from "commander";
|
|
705
|
+
function cliTypeToEntityType(type, customerType) {
|
|
706
|
+
if (type === "customers") {
|
|
707
|
+
return customerType === "companies" ? "company" : "contact";
|
|
708
|
+
}
|
|
709
|
+
const map = {
|
|
710
|
+
issue: "issue",
|
|
711
|
+
issues: "issue",
|
|
712
|
+
session: "session",
|
|
713
|
+
sessions: "session",
|
|
714
|
+
feedback: "session",
|
|
715
|
+
company: "company",
|
|
716
|
+
companies: "company",
|
|
717
|
+
contact: "contact",
|
|
718
|
+
contacts: "contact",
|
|
719
|
+
knowledge: "knowledge_source",
|
|
720
|
+
sources: "knowledge_source",
|
|
721
|
+
scopes: "product_scope",
|
|
722
|
+
scope: "product_scope"
|
|
723
|
+
};
|
|
724
|
+
return map[type] ?? type;
|
|
725
|
+
}
|
|
726
|
+
var TYPE_ENDPOINTS2 = {
|
|
727
|
+
knowledge: { path: (id) => `/api/knowledge/packages/${id}`, key: "package" },
|
|
728
|
+
sources: { path: (id) => `/api/knowledge/sources/${id}`, key: "source" },
|
|
729
|
+
feedback: { path: (id) => `/api/sessions/${id}`, key: "session" },
|
|
730
|
+
issues: { path: (id) => `/api/issues/${id}`, key: "issue" },
|
|
731
|
+
customers: { path: (id) => `/api/contacts/${id}`, key: "contact" },
|
|
732
|
+
scopes: { path: (id) => `/api/product-scopes/${id}`, key: "scope" }
|
|
733
|
+
};
|
|
734
|
+
var getCommand = new Command3("get").description("Get full details of a specific resource").argument("<type>", "Resource type: knowledge, sources, feedback, issues, customers, scopes").argument("<id>", "Resource ID").option("--customer-type <type>", "Customer sub-type: contacts (default) or companies").action(async (type, id, opts, cmd) => {
|
|
735
|
+
const config = requireConfig();
|
|
736
|
+
const jsonMode = cmd.parent?.opts().json;
|
|
737
|
+
const validTypes = Object.keys(TYPE_ENDPOINTS2);
|
|
738
|
+
if (!validTypes.includes(type)) {
|
|
739
|
+
error(`Invalid type "${type}". Must be one of: ${validTypes.join(", ")}`);
|
|
740
|
+
process.exit(1);
|
|
741
|
+
}
|
|
742
|
+
const projectId = await resolveProjectId(config);
|
|
743
|
+
let endpoint;
|
|
744
|
+
let displayType = type;
|
|
745
|
+
const customerType = resolveCustomerType(opts.customerType);
|
|
746
|
+
if (type === "customers") {
|
|
747
|
+
const key = customerType === "companies" ? "company" : "contact";
|
|
748
|
+
endpoint = { path: (id2) => `/api/${customerType}/${id2}`, key };
|
|
749
|
+
displayType = customerType;
|
|
750
|
+
} else {
|
|
751
|
+
endpoint = TYPE_ENDPOINTS2[type];
|
|
752
|
+
}
|
|
753
|
+
try {
|
|
754
|
+
const [result, relResult] = await Promise.all([
|
|
755
|
+
apiCall(
|
|
756
|
+
config,
|
|
757
|
+
"GET",
|
|
758
|
+
buildPath(endpoint.path(id), { projectId })
|
|
759
|
+
),
|
|
760
|
+
apiCall(
|
|
761
|
+
config,
|
|
762
|
+
"GET",
|
|
763
|
+
buildPath("/api/relationships", {
|
|
764
|
+
projectId,
|
|
765
|
+
entityType: cliTypeToEntityType(type, customerType),
|
|
766
|
+
entityId: id
|
|
767
|
+
})
|
|
768
|
+
)
|
|
769
|
+
]);
|
|
770
|
+
if (!result.ok) {
|
|
771
|
+
const data = result.data;
|
|
772
|
+
error(`Failed: ${data.error || `HTTP ${result.status}`}`);
|
|
773
|
+
process.exit(1);
|
|
774
|
+
}
|
|
775
|
+
const item = result.data[endpoint.key] ?? result.data;
|
|
776
|
+
if (jsonMode) {
|
|
777
|
+
const output = { ...result.data };
|
|
778
|
+
if (relResult.ok && relResult.data.relationships) {
|
|
779
|
+
output.relationships = relResult.data.relationships;
|
|
780
|
+
}
|
|
781
|
+
console.log(renderJson(output));
|
|
782
|
+
} else {
|
|
783
|
+
const extra = type === "feedback" ? { messages: result.data.messages } : void 0;
|
|
784
|
+
let output = formatResourceDetail(displayType, item, extra);
|
|
785
|
+
if (relResult.ok && relResult.data.relationships) {
|
|
786
|
+
output += formatRelatedEntities(relResult.data.relationships);
|
|
787
|
+
}
|
|
788
|
+
console.log(output);
|
|
789
|
+
}
|
|
790
|
+
} catch (err) {
|
|
791
|
+
error(`Failed: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
792
|
+
process.exit(1);
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// src/commands/search.ts
|
|
797
|
+
import { Command as Command4 } from "commander";
|
|
798
|
+
var searchCommand = new Command4("search").description("Search across resources using natural language").argument("<query>", "Search query").option("--type <type>", "Limit to one resource type (knowledge|feedback|issues|customers)").option("--limit <n>", "Max results (default: 10)", "10").action(async (query, opts, cmd) => {
|
|
799
|
+
const config = requireConfig();
|
|
800
|
+
const jsonMode = cmd.parent?.opts().json;
|
|
801
|
+
if (opts.type) {
|
|
802
|
+
const validTypes = ["knowledge", "feedback", "issues", "customers"];
|
|
803
|
+
if (!validTypes.includes(opts.type)) {
|
|
804
|
+
error(`Invalid type "${opts.type}". Must be one of: ${validTypes.join(", ")}`);
|
|
805
|
+
process.exit(1);
|
|
806
|
+
}
|
|
807
|
+
if (opts.type === "customers") opts.type = "contacts";
|
|
808
|
+
}
|
|
809
|
+
const projectId = await resolveProjectId(config);
|
|
810
|
+
const params = {
|
|
811
|
+
projectId,
|
|
812
|
+
q: query,
|
|
813
|
+
limit: parseInt(opts.limit, 10)
|
|
814
|
+
};
|
|
815
|
+
if (opts.type) params.type = opts.type;
|
|
816
|
+
try {
|
|
817
|
+
const result = await apiCall(
|
|
818
|
+
config,
|
|
819
|
+
"GET",
|
|
820
|
+
buildPath("/api/search", params)
|
|
821
|
+
);
|
|
822
|
+
if (!result.ok) {
|
|
823
|
+
const data = result.data;
|
|
824
|
+
error(`Failed: ${data.error || `HTTP ${result.status}`}`);
|
|
825
|
+
process.exit(1);
|
|
826
|
+
}
|
|
827
|
+
if (jsonMode) {
|
|
828
|
+
console.log(renderJson(result.data));
|
|
829
|
+
} else {
|
|
830
|
+
console.log(formatSearchResults(result.data.results ?? []));
|
|
831
|
+
}
|
|
832
|
+
} catch (err) {
|
|
833
|
+
error(`Failed: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
834
|
+
process.exit(1);
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
// src/commands/add.ts
|
|
839
|
+
import { Command as Command5 } from "commander";
|
|
840
|
+
import { input, select, confirm } from "@inquirer/prompts";
|
|
841
|
+
async function addIssue() {
|
|
842
|
+
const type = await select({
|
|
843
|
+
message: "Issue type:",
|
|
844
|
+
choices: [
|
|
845
|
+
{ value: "bug", name: "Bug" },
|
|
846
|
+
{ value: "feature_request", name: "Feature Request" },
|
|
847
|
+
{ value: "change_request", name: "Change Request" }
|
|
848
|
+
]
|
|
849
|
+
});
|
|
850
|
+
const title = await input({ message: "Title:", validate: (v) => v.length > 0 || "Required" });
|
|
851
|
+
const description = await input({ message: "Description:", validate: (v) => v.length > 0 || "Required" });
|
|
852
|
+
const hasPriority = await confirm({ message: "Set priority?", default: false });
|
|
853
|
+
let priority;
|
|
854
|
+
if (hasPriority) {
|
|
855
|
+
priority = await select({
|
|
856
|
+
message: "Priority:",
|
|
857
|
+
choices: [
|
|
858
|
+
{ value: "low", name: "Low" },
|
|
859
|
+
{ value: "medium", name: "Medium" },
|
|
860
|
+
{ value: "high", name: "High" }
|
|
861
|
+
]
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
return { type, title, description, ...priority ? { priority } : {} };
|
|
865
|
+
}
|
|
866
|
+
async function addContact() {
|
|
867
|
+
const name = await input({ message: "Name:", validate: (v) => v.length > 0 || "Required" });
|
|
868
|
+
const email = await input({
|
|
869
|
+
message: "Email:",
|
|
870
|
+
validate: (v) => v.includes("@") ? true : "Must be a valid email"
|
|
871
|
+
});
|
|
872
|
+
const data = { name, email };
|
|
873
|
+
const role = await input({ message: "Role (optional):" });
|
|
874
|
+
if (role) data.role = role;
|
|
875
|
+
const title = await input({ message: "Title (optional):" });
|
|
876
|
+
if (title) data.title = title;
|
|
877
|
+
const phone = await input({ message: "Phone (optional):" });
|
|
878
|
+
if (phone) data.phone = phone;
|
|
879
|
+
const companyId = await input({ message: "Company ID (optional):" });
|
|
880
|
+
if (companyId) data.company_id = companyId;
|
|
881
|
+
const isChampion = await confirm({ message: "Is champion?", default: false });
|
|
882
|
+
if (isChampion) data.is_champion = true;
|
|
883
|
+
return data;
|
|
884
|
+
}
|
|
885
|
+
async function addCompany() {
|
|
886
|
+
const name = await input({ message: "Company name:", validate: (v) => v.length > 0 || "Required" });
|
|
887
|
+
const domain = await input({
|
|
888
|
+
message: "Domain (e.g., acme.com):",
|
|
889
|
+
validate: (v) => v.length > 0 || "Required"
|
|
890
|
+
});
|
|
891
|
+
const data = { name, domain };
|
|
892
|
+
const industry = await input({ message: "Industry (optional):" });
|
|
893
|
+
if (industry) data.industry = industry;
|
|
894
|
+
const arrStr = await input({ message: "ARR (optional, number):" });
|
|
895
|
+
if (arrStr) data.arr = Number(arrStr);
|
|
896
|
+
const hasStage = await confirm({ message: "Set stage?", default: false });
|
|
897
|
+
if (hasStage) {
|
|
898
|
+
const stage = await select({
|
|
899
|
+
message: "Stage:",
|
|
900
|
+
choices: [
|
|
901
|
+
{ value: "prospect", name: "Prospect" },
|
|
902
|
+
{ value: "onboarding", name: "Onboarding" },
|
|
903
|
+
{ value: "active", name: "Active" },
|
|
904
|
+
{ value: "churned", name: "Churned" },
|
|
905
|
+
{ value: "expansion", name: "Expansion" }
|
|
906
|
+
]
|
|
907
|
+
});
|
|
908
|
+
data.stage = stage;
|
|
909
|
+
}
|
|
910
|
+
const employeeCountStr = await input({ message: "Employee count (optional, number):" });
|
|
911
|
+
if (employeeCountStr) data.employee_count = Number(employeeCountStr);
|
|
912
|
+
const planTier = await input({ message: "Plan tier (optional):" });
|
|
913
|
+
if (planTier) data.plan_tier = planTier;
|
|
914
|
+
const country = await input({ message: "Country (optional):" });
|
|
915
|
+
if (country) data.country = country;
|
|
916
|
+
const notes = await input({ message: "Notes (optional):" });
|
|
917
|
+
if (notes) data.notes = notes;
|
|
918
|
+
return data;
|
|
919
|
+
}
|
|
920
|
+
async function addScope() {
|
|
921
|
+
const name = await input({ message: "Name:", validate: (v) => v.length > 0 || "Required" });
|
|
922
|
+
const type = await select({
|
|
923
|
+
message: "Type:",
|
|
924
|
+
choices: [
|
|
925
|
+
{ value: "product_area", name: "Product Area" },
|
|
926
|
+
{ value: "initiative", name: "Initiative" }
|
|
927
|
+
]
|
|
928
|
+
});
|
|
929
|
+
const description = await input({ message: "Description (optional):" });
|
|
930
|
+
const data = { name, type };
|
|
931
|
+
if (description) data.description = description;
|
|
932
|
+
const addGoals = await confirm({ message: "Add goals?", default: false });
|
|
933
|
+
if (addGoals) {
|
|
934
|
+
const goals = [];
|
|
935
|
+
console.log("Enter goals (empty text to finish).\n");
|
|
936
|
+
while (goals.length < 10) {
|
|
937
|
+
const text = await input({
|
|
938
|
+
message: `Goal ${goals.length + 1} (empty to finish):`
|
|
939
|
+
});
|
|
940
|
+
if (!text) break;
|
|
941
|
+
goals.push({ id: `g_${Date.now()}_${goals.length}`, text });
|
|
942
|
+
}
|
|
943
|
+
if (goals.length > 0) data.goals = goals;
|
|
944
|
+
}
|
|
945
|
+
return data;
|
|
946
|
+
}
|
|
947
|
+
async function addFeedback() {
|
|
948
|
+
const messages = [];
|
|
949
|
+
console.log("Enter conversation messages (at least 1 required). Empty content to finish.\n");
|
|
950
|
+
let first = true;
|
|
951
|
+
while (true) {
|
|
952
|
+
const role = await select({
|
|
953
|
+
message: `Message ${messages.length + 1} role:`,
|
|
954
|
+
choices: [
|
|
955
|
+
{ value: "user", name: "User (customer)" },
|
|
956
|
+
{ value: "assistant", name: "Assistant (support)" }
|
|
957
|
+
],
|
|
958
|
+
default: first ? "user" : void 0
|
|
959
|
+
});
|
|
960
|
+
const content = await input({
|
|
961
|
+
message: `Message ${messages.length + 1} content (empty to finish):`
|
|
962
|
+
});
|
|
963
|
+
if (!content) break;
|
|
964
|
+
messages.push({ role, content });
|
|
965
|
+
first = false;
|
|
966
|
+
}
|
|
967
|
+
if (messages.length === 0) {
|
|
968
|
+
error("At least one message is required.");
|
|
969
|
+
process.exit(1);
|
|
970
|
+
}
|
|
971
|
+
const data = { messages };
|
|
972
|
+
const name = await input({ message: "Feedback name/title (optional):" });
|
|
973
|
+
if (name) data.name = name;
|
|
974
|
+
const tags = await input({ message: "Tags (comma-separated, optional):" });
|
|
975
|
+
if (tags) data.tags = tags.split(",").map((t) => t.trim());
|
|
976
|
+
return data;
|
|
977
|
+
}
|
|
978
|
+
var TYPE_ENDPOINTS3 = {
|
|
979
|
+
feedback: "/api/sessions",
|
|
980
|
+
issues: "/api/issues",
|
|
981
|
+
customers: "/api/contacts",
|
|
982
|
+
scopes: "/api/product-scopes"
|
|
983
|
+
};
|
|
984
|
+
var addCommand = new Command5("add").description("Create a new resource interactively").argument("<type>", "Resource type: feedback, issues, customers, scopes").option("--customer-type <type>", "Customer sub-type: contacts (default) or companies").action(async (type, opts, cmd) => {
|
|
985
|
+
const config = requireConfig();
|
|
986
|
+
const jsonMode = cmd.parent?.opts().json;
|
|
987
|
+
const supportedTypes = Object.keys(TYPE_ENDPOINTS3);
|
|
988
|
+
if (!supportedTypes.includes(type)) {
|
|
989
|
+
if (type === "knowledge") {
|
|
990
|
+
error("Knowledge sources cannot be added via CLI. Use the Hissuno dashboard.");
|
|
991
|
+
} else {
|
|
992
|
+
error(`Invalid type "${type}". Must be one of: ${supportedTypes.join(", ")}`);
|
|
993
|
+
}
|
|
994
|
+
process.exit(1);
|
|
995
|
+
}
|
|
996
|
+
let data;
|
|
997
|
+
let apiEndpoint = TYPE_ENDPOINTS3[type];
|
|
998
|
+
switch (type) {
|
|
999
|
+
case "issues":
|
|
1000
|
+
data = await addIssue();
|
|
1001
|
+
break;
|
|
1002
|
+
case "customers": {
|
|
1003
|
+
let customerType = opts.customerType;
|
|
1004
|
+
if (!customerType) {
|
|
1005
|
+
customerType = await select({
|
|
1006
|
+
message: "Customer type:",
|
|
1007
|
+
choices: [
|
|
1008
|
+
{ value: "contacts", name: "Contact (individual person)" },
|
|
1009
|
+
{ value: "companies", name: "Company (organization)" }
|
|
1010
|
+
]
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
customerType = resolveCustomerType(customerType);
|
|
1014
|
+
apiEndpoint = `/api/${customerType}`;
|
|
1015
|
+
data = customerType === "companies" ? await addCompany() : await addContact();
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
case "feedback":
|
|
1019
|
+
data = await addFeedback();
|
|
1020
|
+
break;
|
|
1021
|
+
case "scopes":
|
|
1022
|
+
data = await addScope();
|
|
1023
|
+
break;
|
|
1024
|
+
default:
|
|
1025
|
+
error(`Unsupported type: ${type}`);
|
|
1026
|
+
process.exit(1);
|
|
1027
|
+
}
|
|
1028
|
+
const projectId = await resolveProjectId(config);
|
|
1029
|
+
try {
|
|
1030
|
+
const result = await apiCall(
|
|
1031
|
+
config,
|
|
1032
|
+
"POST",
|
|
1033
|
+
buildPath(apiEndpoint, { projectId }),
|
|
1034
|
+
data
|
|
1035
|
+
);
|
|
1036
|
+
if (!result.ok) {
|
|
1037
|
+
const errData = result.data;
|
|
1038
|
+
error(`Failed: ${errData.error || `HTTP ${result.status}`}`);
|
|
1039
|
+
process.exit(1);
|
|
1040
|
+
}
|
|
1041
|
+
if (jsonMode) {
|
|
1042
|
+
console.log(renderJson(result.data));
|
|
1043
|
+
} else {
|
|
1044
|
+
success("\nCreated successfully!");
|
|
1045
|
+
const created = result.data;
|
|
1046
|
+
const resource = created.session ?? created.issue ?? created.contact ?? created.company ?? created.scope ?? created;
|
|
1047
|
+
if (resource.id) {
|
|
1048
|
+
console.log(`ID: ${resource.id}`);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
} catch (err) {
|
|
1052
|
+
error(`Failed: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
1053
|
+
process.exit(1);
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
// src/commands/integrate.ts
|
|
1058
|
+
import { Command as Command6 } from "commander";
|
|
1059
|
+
import { input as input2, select as select2, confirm as confirm2, password } from "@inquirer/prompts";
|
|
1060
|
+
|
|
1061
|
+
// src/lib/browser.ts
|
|
1062
|
+
import { exec } from "child_process";
|
|
1063
|
+
import { platform } from "os";
|
|
1064
|
+
function openBrowser(url) {
|
|
1065
|
+
const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "start" : "xdg-open";
|
|
1066
|
+
exec(`${cmd} "${url}"`);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// src/commands/integrate.ts
|
|
1070
|
+
var PLATFORMS = ["intercom", "gong", "zendesk", "slack", "github", "jira", "linear"];
|
|
1071
|
+
var OAUTH_PLATFORMS = ["slack", "github", "jira", "linear"];
|
|
1072
|
+
var SYNCABLE_PLATFORMS = ["intercom", "gong", "zendesk"];
|
|
1073
|
+
var PLATFORM_LABELS = {
|
|
1074
|
+
intercom: "Intercom",
|
|
1075
|
+
gong: "Gong",
|
|
1076
|
+
zendesk: "Zendesk",
|
|
1077
|
+
slack: "Slack",
|
|
1078
|
+
github: "GitHub",
|
|
1079
|
+
jira: "Jira",
|
|
1080
|
+
linear: "Linear"
|
|
1081
|
+
};
|
|
1082
|
+
async function getProjectId(config) {
|
|
1083
|
+
return resolveProjectId(config);
|
|
1084
|
+
}
|
|
1085
|
+
async function getStatus(config, platform2, projectId) {
|
|
1086
|
+
return apiCall(config, "GET", `/api/integrations/${platform2}?projectId=${projectId}`);
|
|
1087
|
+
}
|
|
1088
|
+
function formatStatus(platform2, data) {
|
|
1089
|
+
const lines = [`# ${PLATFORM_LABELS[platform2]}`, ""];
|
|
1090
|
+
const connected = data.connected;
|
|
1091
|
+
lines.push(`**Status:** ${connected ? "Connected" : "Not connected"}`);
|
|
1092
|
+
if (!connected) return lines.join("\n");
|
|
1093
|
+
if (platform2 === "slack") {
|
|
1094
|
+
if (data.workspaceName) lines.push(`**Workspace:** ${data.workspaceName}`);
|
|
1095
|
+
if (data.workspaceDomain) lines.push(`**Domain:** ${data.workspaceDomain}`);
|
|
1096
|
+
if (data.installedByEmail) lines.push(`**Installed by:** ${data.installedByEmail}`);
|
|
1097
|
+
}
|
|
1098
|
+
if (platform2 === "github") {
|
|
1099
|
+
if (data.accountLogin) lines.push(`**Account:** ${data.accountLogin}`);
|
|
1100
|
+
if (data.installedByEmail) lines.push(`**Installed by:** ${data.installedByEmail}`);
|
|
1101
|
+
}
|
|
1102
|
+
if (platform2 === "jira") {
|
|
1103
|
+
if (data.siteUrl) lines.push(`**Site:** ${data.siteUrl}`);
|
|
1104
|
+
if (data.jiraProjectKey) lines.push(`**Project:** ${data.jiraProjectKey}`);
|
|
1105
|
+
if (data.issueTypeName) lines.push(`**Issue Type:** ${data.issueTypeName}`);
|
|
1106
|
+
lines.push(`**Configured:** ${data.isConfigured ? "Yes" : "No"}`);
|
|
1107
|
+
lines.push(`**Auto-sync:** ${data.autoSyncEnabled ? "Enabled" : "Disabled"}`);
|
|
1108
|
+
}
|
|
1109
|
+
if (platform2 === "linear") {
|
|
1110
|
+
if (data.organizationName) lines.push(`**Organization:** ${data.organizationName}`);
|
|
1111
|
+
if (data.teamName) lines.push(`**Team:** ${data.teamName} (${data.teamKey})`);
|
|
1112
|
+
lines.push(`**Configured:** ${data.isConfigured ? "Yes" : "No"}`);
|
|
1113
|
+
lines.push(`**Auto-sync:** ${data.autoSyncEnabled ? "Enabled" : "Disabled"}`);
|
|
1114
|
+
}
|
|
1115
|
+
if (platform2 === "intercom") {
|
|
1116
|
+
if (data.workspaceName) lines.push(`**Workspace:** ${data.workspaceName}`);
|
|
1117
|
+
if (data.authMethod) lines.push(`**Auth Method:** ${data.authMethod}`);
|
|
1118
|
+
if (data.syncFrequency) lines.push(`**Sync Frequency:** ${data.syncFrequency}`);
|
|
1119
|
+
lines.push(`**Sync Enabled:** ${data.syncEnabled ? "Yes" : "No"}`);
|
|
1120
|
+
if (data.lastSyncAt) lines.push(`**Last Sync:** ${data.lastSyncAt}`);
|
|
1121
|
+
if (data.lastSyncStatus) lines.push(`**Last Sync Status:** ${data.lastSyncStatus}`);
|
|
1122
|
+
lines.push(`**Conversations Synced:** ${data.lastSyncConversationsCount ?? 0}`);
|
|
1123
|
+
}
|
|
1124
|
+
if (platform2 === "gong") {
|
|
1125
|
+
if (data.syncFrequency) lines.push(`**Sync Frequency:** ${data.syncFrequency}`);
|
|
1126
|
+
lines.push(`**Sync Enabled:** ${data.syncEnabled ? "Yes" : "No"}`);
|
|
1127
|
+
if (data.lastSyncAt) lines.push(`**Last Sync:** ${data.lastSyncAt}`);
|
|
1128
|
+
if (data.lastSyncStatus) lines.push(`**Last Sync Status:** ${data.lastSyncStatus}`);
|
|
1129
|
+
lines.push(`**Calls Synced:** ${data.lastSyncCallsCount ?? 0}`);
|
|
1130
|
+
}
|
|
1131
|
+
if (platform2 === "zendesk") {
|
|
1132
|
+
if (data.subdomain) lines.push(`**Subdomain:** ${data.subdomain}`);
|
|
1133
|
+
if (data.accountName) lines.push(`**Account:** ${data.accountName}`);
|
|
1134
|
+
if (data.syncFrequency) lines.push(`**Sync Frequency:** ${data.syncFrequency}`);
|
|
1135
|
+
lines.push(`**Sync Enabled:** ${data.syncEnabled ? "Yes" : "No"}`);
|
|
1136
|
+
if (data.lastSyncAt) lines.push(`**Last Sync:** ${data.lastSyncAt}`);
|
|
1137
|
+
if (data.lastSyncStatus) lines.push(`**Last Sync Status:** ${data.lastSyncStatus}`);
|
|
1138
|
+
lines.push(`**Tickets Synced:** ${data.lastSyncTicketsCount ?? 0}`);
|
|
1139
|
+
}
|
|
1140
|
+
const stats = data.stats;
|
|
1141
|
+
if (stats?.totalSynced != null) {
|
|
1142
|
+
lines.push(`**Total Synced:** ${stats.totalSynced}`);
|
|
1143
|
+
}
|
|
1144
|
+
return lines.join("\n");
|
|
1145
|
+
}
|
|
1146
|
+
async function connectOAuth(config, platform2, projectId) {
|
|
1147
|
+
const appUrl = getBaseUrl(config);
|
|
1148
|
+
const connectUrl = `${appUrl}/projects/${projectId}/integrations?connect=${platform2}`;
|
|
1149
|
+
console.log(`
|
|
1150
|
+
Opening browser to connect ${PLATFORM_LABELS[platform2]}...`);
|
|
1151
|
+
console.log(`URL: ${connectUrl}
|
|
1152
|
+
`);
|
|
1153
|
+
openBrowser(connectUrl);
|
|
1154
|
+
warn("Complete the authorization in your browser, then press Enter to verify.");
|
|
1155
|
+
await input2({ message: "Press Enter when done..." });
|
|
1156
|
+
const result = await getStatus(config, platform2, projectId);
|
|
1157
|
+
if (result.ok && result.data.connected) {
|
|
1158
|
+
success(`${PLATFORM_LABELS[platform2]} connected successfully!`);
|
|
1159
|
+
return true;
|
|
1160
|
+
}
|
|
1161
|
+
error(`${PLATFORM_LABELS[platform2]} connection not detected. Please try again.`);
|
|
1162
|
+
return false;
|
|
1163
|
+
}
|
|
1164
|
+
async function connectGong(config, projectId, opts) {
|
|
1165
|
+
const accessKey = opts.accessKey || await input2({ message: "Access Key:", validate: (v) => v.length > 0 || "Required" });
|
|
1166
|
+
const accessKeySecret = opts.accessKeySecret || await password({ message: "Access Key Secret:", mask: "*", validate: (v) => v.length > 0 || "Required" });
|
|
1167
|
+
const baseUrl = opts.baseUrl || await input2({ message: "Base URL:", default: "https://api.gong.io" });
|
|
1168
|
+
const syncFrequency = opts.syncFrequency || await select2({
|
|
1169
|
+
message: "Sync frequency:",
|
|
1170
|
+
choices: [
|
|
1171
|
+
{ value: "manual", name: "Manual" },
|
|
1172
|
+
{ value: "1h", name: "Every hour" },
|
|
1173
|
+
{ value: "6h", name: "Every 6 hours" },
|
|
1174
|
+
{ value: "24h", name: "Every 24 hours" }
|
|
1175
|
+
],
|
|
1176
|
+
default: "24h"
|
|
1177
|
+
});
|
|
1178
|
+
console.log("\nValidating credentials...");
|
|
1179
|
+
const result = await apiCall(config, "POST", "/api/integrations/gong/connect", {
|
|
1180
|
+
projectId,
|
|
1181
|
+
accessKey,
|
|
1182
|
+
accessKeySecret,
|
|
1183
|
+
baseUrl,
|
|
1184
|
+
syncFrequency
|
|
1185
|
+
});
|
|
1186
|
+
if (result.ok) {
|
|
1187
|
+
success("Gong connected successfully!");
|
|
1188
|
+
return true;
|
|
1189
|
+
}
|
|
1190
|
+
const data = result.data;
|
|
1191
|
+
error(`Connection failed: ${data.error || "Unknown error"}`);
|
|
1192
|
+
return false;
|
|
1193
|
+
}
|
|
1194
|
+
async function connectZendesk(config, projectId, opts) {
|
|
1195
|
+
const subdomain = opts.subdomain || await input2({ message: "Zendesk subdomain (e.g., mycompany):", validate: (v) => v.length > 0 || "Required" });
|
|
1196
|
+
const email = opts.email || await input2({ message: "Admin email:", validate: (v) => v.includes("@") || "Must be a valid email" });
|
|
1197
|
+
const apiToken = opts.apiToken || await password({ message: "API token:", mask: "*", validate: (v) => v.length > 0 || "Required" });
|
|
1198
|
+
const syncFrequency = opts.syncFrequency || await select2({
|
|
1199
|
+
message: "Sync frequency:",
|
|
1200
|
+
choices: [
|
|
1201
|
+
{ value: "manual", name: "Manual" },
|
|
1202
|
+
{ value: "1h", name: "Every hour" },
|
|
1203
|
+
{ value: "6h", name: "Every 6 hours" },
|
|
1204
|
+
{ value: "24h", name: "Every 24 hours" }
|
|
1205
|
+
],
|
|
1206
|
+
default: "24h"
|
|
1207
|
+
});
|
|
1208
|
+
console.log("\nValidating credentials...");
|
|
1209
|
+
const result = await apiCall(config, "POST", "/api/integrations/zendesk/connect", {
|
|
1210
|
+
projectId,
|
|
1211
|
+
subdomain,
|
|
1212
|
+
email,
|
|
1213
|
+
apiToken,
|
|
1214
|
+
syncFrequency
|
|
1215
|
+
});
|
|
1216
|
+
if (result.ok) {
|
|
1217
|
+
success("Zendesk connected successfully!");
|
|
1218
|
+
return true;
|
|
1219
|
+
}
|
|
1220
|
+
const data = result.data;
|
|
1221
|
+
error(`Connection failed: ${data.error || "Unknown error"}`);
|
|
1222
|
+
return false;
|
|
1223
|
+
}
|
|
1224
|
+
async function connectIntercom(config, projectId, opts) {
|
|
1225
|
+
const method = opts.accessToken ? "token" : await select2({
|
|
1226
|
+
message: "Connection method:",
|
|
1227
|
+
choices: [
|
|
1228
|
+
{ value: "token", name: "API Token" },
|
|
1229
|
+
{ value: "oauth", name: "OAuth (opens browser)" }
|
|
1230
|
+
]
|
|
1231
|
+
});
|
|
1232
|
+
if (method === "oauth") {
|
|
1233
|
+
return connectOAuth(config, "intercom", projectId);
|
|
1234
|
+
}
|
|
1235
|
+
const accessToken = opts.accessToken || await password({ message: "Access Token:", mask: "*", validate: (v) => v.length > 0 || "Required" });
|
|
1236
|
+
const syncFrequency = opts.syncFrequency || await select2({
|
|
1237
|
+
message: "Sync frequency:",
|
|
1238
|
+
choices: [
|
|
1239
|
+
{ value: "manual", name: "Manual" },
|
|
1240
|
+
{ value: "1h", name: "Every hour" },
|
|
1241
|
+
{ value: "6h", name: "Every 6 hours" },
|
|
1242
|
+
{ value: "24h", name: "Every 24 hours" }
|
|
1243
|
+
],
|
|
1244
|
+
default: "24h"
|
|
1245
|
+
});
|
|
1246
|
+
console.log("\nValidating credentials...");
|
|
1247
|
+
const result = await apiCall(config, "POST", "/api/integrations/intercom/connect", {
|
|
1248
|
+
projectId,
|
|
1249
|
+
accessToken,
|
|
1250
|
+
syncFrequency
|
|
1251
|
+
});
|
|
1252
|
+
if (result.ok) {
|
|
1253
|
+
success("Intercom connected successfully!");
|
|
1254
|
+
return true;
|
|
1255
|
+
}
|
|
1256
|
+
const data = result.data;
|
|
1257
|
+
error(`Connection failed: ${data.error || "Unknown error"}`);
|
|
1258
|
+
return false;
|
|
1259
|
+
}
|
|
1260
|
+
async function configureIntegration(config, platform2, projectId) {
|
|
1261
|
+
if (SYNCABLE_PLATFORMS.includes(platform2)) {
|
|
1262
|
+
const syncFrequency = await select2({
|
|
1263
|
+
message: "Sync frequency:",
|
|
1264
|
+
choices: [
|
|
1265
|
+
{ value: "manual", name: "Manual" },
|
|
1266
|
+
{ value: "1h", name: "Every hour" },
|
|
1267
|
+
{ value: "6h", name: "Every 6 hours" },
|
|
1268
|
+
{ value: "24h", name: "Every 24 hours" }
|
|
1269
|
+
]
|
|
1270
|
+
});
|
|
1271
|
+
const result = await apiCall(config, "PATCH", `/api/integrations/${platform2}?projectId=${projectId}`, {
|
|
1272
|
+
syncFrequency
|
|
1273
|
+
});
|
|
1274
|
+
if (result.ok) {
|
|
1275
|
+
success("Settings updated!");
|
|
1276
|
+
} else {
|
|
1277
|
+
const data = result.data;
|
|
1278
|
+
error(`Update failed: ${data.error || "Unknown error"}`);
|
|
1279
|
+
}
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
if (platform2 === "jira" || platform2 === "linear") {
|
|
1283
|
+
const autoSyncEnabled = await confirm2({ message: "Enable auto-sync of issues?", default: true });
|
|
1284
|
+
const result = await apiCall(config, "PATCH", `/api/integrations/${platform2}?projectId=${projectId}`, {
|
|
1285
|
+
autoSyncEnabled
|
|
1286
|
+
});
|
|
1287
|
+
if (result.ok) {
|
|
1288
|
+
success("Settings updated!");
|
|
1289
|
+
} else {
|
|
1290
|
+
const data = result.data;
|
|
1291
|
+
error(`Update failed: ${data.error || "Unknown error"}`);
|
|
1292
|
+
}
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
warn(`${PLATFORM_LABELS[platform2]} is configured through its connection flow or the dashboard.`);
|
|
1296
|
+
}
|
|
1297
|
+
async function syncIntegration(config, platform2, projectId, mode) {
|
|
1298
|
+
if (!SYNCABLE_PLATFORMS.includes(platform2)) {
|
|
1299
|
+
error(`${PLATFORM_LABELS[platform2]} does not support manual sync.`);
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
const syncMode = mode || await select2({
|
|
1303
|
+
message: "Sync mode:",
|
|
1304
|
+
choices: [
|
|
1305
|
+
{ value: "incremental", name: "Incremental (new items only)" },
|
|
1306
|
+
{ value: "full", name: "Full (re-sync everything)" }
|
|
1307
|
+
],
|
|
1308
|
+
default: "incremental"
|
|
1309
|
+
});
|
|
1310
|
+
console.log(`
|
|
1311
|
+
Syncing ${PLATFORM_LABELS[platform2]} (${syncMode})...
|
|
1312
|
+
`);
|
|
1313
|
+
const baseUrl = getBaseUrl(config);
|
|
1314
|
+
const url = `${baseUrl}/api/integrations/${platform2}/sync?projectId=${projectId}&mode=${syncMode}`;
|
|
1315
|
+
try {
|
|
1316
|
+
const response = await fetch(url, {
|
|
1317
|
+
headers: { Authorization: `Bearer ${config.api_key}` }
|
|
1318
|
+
});
|
|
1319
|
+
if (!response.ok) {
|
|
1320
|
+
const text = await response.text();
|
|
1321
|
+
try {
|
|
1322
|
+
const data = JSON.parse(text);
|
|
1323
|
+
error(`Sync failed: ${data.error || `HTTP ${response.status}`}`);
|
|
1324
|
+
} catch {
|
|
1325
|
+
error(`Sync failed: HTTP ${response.status}`);
|
|
1326
|
+
}
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
const reader = response.body?.getReader();
|
|
1330
|
+
if (!reader) {
|
|
1331
|
+
error("No response stream.");
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
const decoder = new TextDecoder();
|
|
1335
|
+
let buffer = "";
|
|
1336
|
+
while (true) {
|
|
1337
|
+
const { done, value } = await reader.read();
|
|
1338
|
+
if (done) break;
|
|
1339
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1340
|
+
const lines = buffer.split("\n");
|
|
1341
|
+
buffer = lines.pop() || "";
|
|
1342
|
+
for (const line of lines) {
|
|
1343
|
+
if (!line.startsWith("data: ")) continue;
|
|
1344
|
+
try {
|
|
1345
|
+
const event = JSON.parse(line.slice(6));
|
|
1346
|
+
if (event.type === "progress") {
|
|
1347
|
+
process.stdout.write(`\r Syncing... ${event.current ?? 0}/${event.total ?? "?"}`);
|
|
1348
|
+
} else if (event.type === "complete") {
|
|
1349
|
+
process.stdout.write("\n");
|
|
1350
|
+
success(event.message || "Sync complete!");
|
|
1351
|
+
} else if (event.type === "error") {
|
|
1352
|
+
process.stdout.write("\n");
|
|
1353
|
+
error(event.message || "Sync error.");
|
|
1354
|
+
}
|
|
1355
|
+
} catch {
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
} catch (err) {
|
|
1360
|
+
error(`Sync failed: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
async function disconnectIntegration(config, platform2, projectId) {
|
|
1364
|
+
const confirmed = await confirm2({
|
|
1365
|
+
message: `Disconnect ${PLATFORM_LABELS[platform2]}? This cannot be undone.`,
|
|
1366
|
+
default: false
|
|
1367
|
+
});
|
|
1368
|
+
if (!confirmed) {
|
|
1369
|
+
console.log("Cancelled.");
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
const result = await apiCall(config, "DELETE", `/api/integrations/${platform2}?projectId=${projectId}`);
|
|
1373
|
+
if (result.ok) {
|
|
1374
|
+
success(`${PLATFORM_LABELS[platform2]} disconnected.`);
|
|
1375
|
+
} else {
|
|
1376
|
+
const data = result.data;
|
|
1377
|
+
error(`Disconnect failed: ${data.error || "Unknown error"}`);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
async function interactiveWizard(config, platform2, projectId, jsonMode) {
|
|
1381
|
+
const statusResult = await getStatus(config, platform2, projectId);
|
|
1382
|
+
if (!statusResult.ok) {
|
|
1383
|
+
const data2 = statusResult.data;
|
|
1384
|
+
error(`Failed to get status: ${data2.error || `HTTP ${statusResult.status}`}`);
|
|
1385
|
+
process.exit(1);
|
|
1386
|
+
}
|
|
1387
|
+
const data = statusResult.data;
|
|
1388
|
+
if (jsonMode) {
|
|
1389
|
+
console.log(renderJson(data));
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
console.log(renderMarkdown(formatStatus(platform2, data)));
|
|
1393
|
+
if (!data.connected) {
|
|
1394
|
+
const shouldConnect = await confirm2({ message: `Connect ${PLATFORM_LABELS[platform2]}?`, default: true });
|
|
1395
|
+
if (shouldConnect) {
|
|
1396
|
+
if (OAUTH_PLATFORMS.includes(platform2)) {
|
|
1397
|
+
await connectOAuth(config, platform2, projectId);
|
|
1398
|
+
} else if (platform2 === "gong") {
|
|
1399
|
+
await connectGong(config, projectId, {});
|
|
1400
|
+
} else if (platform2 === "zendesk") {
|
|
1401
|
+
await connectZendesk(config, projectId, {});
|
|
1402
|
+
} else if (platform2 === "intercom") {
|
|
1403
|
+
await connectIntercom(config, projectId, {});
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
const action = await select2({
|
|
1409
|
+
message: "What would you like to do?",
|
|
1410
|
+
choices: [
|
|
1411
|
+
...SYNCABLE_PLATFORMS.includes(platform2) ? [{ value: "configure", name: "Configure sync settings" }] : [],
|
|
1412
|
+
...SYNCABLE_PLATFORMS.includes(platform2) ? [{ value: "sync", name: "Trigger manual sync" }] : [],
|
|
1413
|
+
...platform2 === "jira" || platform2 === "linear" ? [{ value: "configure", name: "Configure settings" }] : [],
|
|
1414
|
+
{ value: "disconnect", name: "Disconnect" },
|
|
1415
|
+
{ value: "cancel", name: "Cancel" }
|
|
1416
|
+
]
|
|
1417
|
+
});
|
|
1418
|
+
switch (action) {
|
|
1419
|
+
case "configure":
|
|
1420
|
+
await configureIntegration(config, platform2, projectId);
|
|
1421
|
+
break;
|
|
1422
|
+
case "sync":
|
|
1423
|
+
await syncIntegration(config, platform2, projectId, "");
|
|
1424
|
+
break;
|
|
1425
|
+
case "disconnect":
|
|
1426
|
+
await disconnectIntegration(config, platform2, projectId);
|
|
1427
|
+
break;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
var integrateCommand = new Command6("integrate").description("Manage integrations (intercom, gong, zendesk, slack, github, jira, linear)").argument("[platform]", "Integration platform").argument("[action]", "Action: status, connect, configure, sync, disconnect").option("--access-key <key>", "Gong access key").option("--access-key-secret <secret>", "Gong access key secret").option("--base-url <url>", "Gong base URL").option("--subdomain <subdomain>", "Zendesk subdomain").option("--email <email>", "Zendesk admin email").option("--api-token <token>", "Zendesk API token").option("--access-token <token>", "Intercom access token").option("--sync-frequency <freq>", "Sync frequency: manual, 1h, 6h, 24h").option("--mode <mode>", "Sync mode: incremental, full").action(async (platformArg, actionArg, opts, cmd) => {
|
|
1431
|
+
const config = requireConfig();
|
|
1432
|
+
const jsonMode = cmd.parent?.opts().json;
|
|
1433
|
+
const projectId = await getProjectId(config);
|
|
1434
|
+
if (!platformArg) {
|
|
1435
|
+
const results = await Promise.all(
|
|
1436
|
+
PLATFORMS.map(async (p) => {
|
|
1437
|
+
const result = await getStatus(config, p, projectId);
|
|
1438
|
+
return {
|
|
1439
|
+
platform: p,
|
|
1440
|
+
name: PLATFORM_LABELS[p],
|
|
1441
|
+
connected: result.ok ? result.data.connected === true : false,
|
|
1442
|
+
data: result.ok ? result.data : null
|
|
1443
|
+
};
|
|
1444
|
+
})
|
|
1445
|
+
);
|
|
1446
|
+
if (jsonMode) {
|
|
1447
|
+
console.log(renderJson(results));
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
console.log(renderMarkdown("# Integrations\n"));
|
|
1451
|
+
for (const r of results) {
|
|
1452
|
+
const status = r.connected ? "\x1B[32mConnected\x1B[0m" : "\x1B[2mNot connected\x1B[0m";
|
|
1453
|
+
console.log(` ${r.name.padEnd(12)} ${status}`);
|
|
1454
|
+
}
|
|
1455
|
+
console.log("\nRun `hissuno integrate <platform>` to manage a specific integration.");
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
const platform2 = platformArg.toLowerCase();
|
|
1459
|
+
if (!PLATFORMS.includes(platform2)) {
|
|
1460
|
+
error(`Unknown platform "${platformArg}". Supported: ${PLATFORMS.join(", ")}`);
|
|
1461
|
+
process.exit(1);
|
|
1462
|
+
}
|
|
1463
|
+
if (!actionArg) {
|
|
1464
|
+
await interactiveWizard(config, platform2, projectId, jsonMode);
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
const action = actionArg.toLowerCase();
|
|
1468
|
+
switch (action) {
|
|
1469
|
+
case "status": {
|
|
1470
|
+
const result = await getStatus(config, platform2, projectId);
|
|
1471
|
+
if (!result.ok) {
|
|
1472
|
+
const data2 = result.data;
|
|
1473
|
+
error(`Failed: ${data2.error || `HTTP ${result.status}`}`);
|
|
1474
|
+
process.exit(1);
|
|
1475
|
+
}
|
|
1476
|
+
const data = result.data;
|
|
1477
|
+
if (jsonMode) {
|
|
1478
|
+
console.log(renderJson(data));
|
|
1479
|
+
} else {
|
|
1480
|
+
console.log(renderMarkdown(formatStatus(platform2, data)));
|
|
1481
|
+
}
|
|
1482
|
+
break;
|
|
1483
|
+
}
|
|
1484
|
+
case "connect": {
|
|
1485
|
+
const statusResult = await getStatus(config, platform2, projectId);
|
|
1486
|
+
if (statusResult.ok && statusResult.data.connected) {
|
|
1487
|
+
warn(`${PLATFORM_LABELS[platform2]} is already connected.`);
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
if (OAUTH_PLATFORMS.includes(platform2)) {
|
|
1491
|
+
await connectOAuth(config, platform2, projectId);
|
|
1492
|
+
} else if (platform2 === "gong") {
|
|
1493
|
+
await connectGong(config, projectId, {
|
|
1494
|
+
accessKey: opts.accessKey || "",
|
|
1495
|
+
accessKeySecret: opts.accessKeySecret || "",
|
|
1496
|
+
baseUrl: opts.baseUrl || "",
|
|
1497
|
+
syncFrequency: opts.syncFrequency || ""
|
|
1498
|
+
});
|
|
1499
|
+
} else if (platform2 === "zendesk") {
|
|
1500
|
+
await connectZendesk(config, projectId, {
|
|
1501
|
+
subdomain: opts.subdomain || "",
|
|
1502
|
+
email: opts.email || "",
|
|
1503
|
+
apiToken: opts.apiToken || "",
|
|
1504
|
+
syncFrequency: opts.syncFrequency || ""
|
|
1505
|
+
});
|
|
1506
|
+
} else if (platform2 === "intercom") {
|
|
1507
|
+
await connectIntercom(config, projectId, {
|
|
1508
|
+
accessToken: opts.accessToken || "",
|
|
1509
|
+
syncFrequency: opts.syncFrequency || ""
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
break;
|
|
1513
|
+
}
|
|
1514
|
+
case "configure": {
|
|
1515
|
+
const statusResult = await getStatus(config, platform2, projectId);
|
|
1516
|
+
if (!statusResult.ok || !statusResult.data.connected) {
|
|
1517
|
+
error(`${PLATFORM_LABELS[platform2]} is not connected. Run 'hissuno integrate ${platform2} connect' first.`);
|
|
1518
|
+
process.exit(1);
|
|
1519
|
+
}
|
|
1520
|
+
await configureIntegration(config, platform2, projectId);
|
|
1521
|
+
break;
|
|
1522
|
+
}
|
|
1523
|
+
case "sync": {
|
|
1524
|
+
const statusResult = await getStatus(config, platform2, projectId);
|
|
1525
|
+
if (!statusResult.ok || !statusResult.data.connected) {
|
|
1526
|
+
error(`${PLATFORM_LABELS[platform2]} is not connected.`);
|
|
1527
|
+
process.exit(1);
|
|
1528
|
+
}
|
|
1529
|
+
await syncIntegration(config, platform2, projectId, opts.mode || "");
|
|
1530
|
+
break;
|
|
1531
|
+
}
|
|
1532
|
+
case "disconnect": {
|
|
1533
|
+
const statusResult = await getStatus(config, platform2, projectId);
|
|
1534
|
+
if (!statusResult.ok || !statusResult.data.connected) {
|
|
1535
|
+
error(`${PLATFORM_LABELS[platform2]} is not connected.`);
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
await disconnectIntegration(config, platform2, projectId);
|
|
1539
|
+
break;
|
|
1540
|
+
}
|
|
1541
|
+
default:
|
|
1542
|
+
error(`Unknown action "${action}". Valid actions: status, connect, configure, sync, disconnect`);
|
|
1543
|
+
process.exit(1);
|
|
1544
|
+
}
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1547
|
+
// src/commands/update.ts
|
|
1548
|
+
import { Command as Command7 } from "commander";
|
|
1549
|
+
import { input as input3, select as select3, confirm as confirm3 } from "@inquirer/prompts";
|
|
1550
|
+
async function updateScope(existing) {
|
|
1551
|
+
const data = {};
|
|
1552
|
+
const changeName = await confirm3({ message: `Change name? (current: ${existing.name})`, default: false });
|
|
1553
|
+
if (changeName) {
|
|
1554
|
+
data.name = await input3({ message: "New name:", validate: (v) => v.length > 0 || "Required" });
|
|
1555
|
+
}
|
|
1556
|
+
const changeType = await confirm3({ message: `Change type? (current: ${existing.type})`, default: false });
|
|
1557
|
+
if (changeType) {
|
|
1558
|
+
data.type = await select3({
|
|
1559
|
+
message: "New type:",
|
|
1560
|
+
choices: [
|
|
1561
|
+
{ value: "product_area", name: "Product Area" },
|
|
1562
|
+
{ value: "initiative", name: "Initiative" }
|
|
1563
|
+
]
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
const changeDesc = await confirm3({ message: "Change description?", default: false });
|
|
1567
|
+
if (changeDesc) {
|
|
1568
|
+
data.description = await input3({ message: "New description:" });
|
|
1569
|
+
}
|
|
1570
|
+
const manageGoals = await confirm3({ message: "Manage goals?", default: false });
|
|
1571
|
+
if (manageGoals) {
|
|
1572
|
+
const existingGoals = Array.isArray(existing.goals) ? existing.goals : [];
|
|
1573
|
+
if (existingGoals.length > 0) {
|
|
1574
|
+
console.log(`
|
|
1575
|
+
Current goals (${existingGoals.length}):`);
|
|
1576
|
+
for (let i = 0; i < existingGoals.length; i++) {
|
|
1577
|
+
console.log(` ${i + 1}. ${existingGoals[i].text}`);
|
|
1578
|
+
}
|
|
1579
|
+
console.log("");
|
|
1580
|
+
}
|
|
1581
|
+
const goalAction = await select3({
|
|
1582
|
+
message: "Goal action:",
|
|
1583
|
+
choices: [
|
|
1584
|
+
{ value: "add", name: "Add new goals" },
|
|
1585
|
+
{ value: "replace", name: "Replace all goals" },
|
|
1586
|
+
{ value: "clear", name: "Clear all goals" },
|
|
1587
|
+
{ value: "skip", name: "Keep current goals" }
|
|
1588
|
+
]
|
|
1589
|
+
});
|
|
1590
|
+
if (goalAction === "clear") {
|
|
1591
|
+
data.goals = null;
|
|
1592
|
+
} else if (goalAction === "replace") {
|
|
1593
|
+
const goals = [];
|
|
1594
|
+
console.log("Enter new goals (empty text to finish).\n");
|
|
1595
|
+
while (goals.length < 10) {
|
|
1596
|
+
const text = await input3({ message: `Goal ${goals.length + 1} (empty to finish):` });
|
|
1597
|
+
if (!text) break;
|
|
1598
|
+
goals.push({ id: `g_${Date.now()}_${goals.length}`, text });
|
|
1599
|
+
}
|
|
1600
|
+
data.goals = goals.length > 0 ? goals : null;
|
|
1601
|
+
} else if (goalAction === "add") {
|
|
1602
|
+
const goals = [...existingGoals];
|
|
1603
|
+
console.log("Enter additional goals (empty text to finish).\n");
|
|
1604
|
+
while (goals.length < 10) {
|
|
1605
|
+
const text = await input3({ message: `Goal ${goals.length + 1} (empty to finish):` });
|
|
1606
|
+
if (!text) break;
|
|
1607
|
+
goals.push({ id: `g_${Date.now()}_${goals.length}`, text });
|
|
1608
|
+
}
|
|
1609
|
+
data.goals = goals;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
return data;
|
|
1613
|
+
}
|
|
1614
|
+
var TYPE_ENDPOINTS4 = {
|
|
1615
|
+
scopes: {
|
|
1616
|
+
getPath: (id) => `/api/product-scopes/${id}`,
|
|
1617
|
+
patchPath: (id) => `/api/product-scopes/${id}`,
|
|
1618
|
+
key: "scope"
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
var updateCommand = new Command7("update").description("Update an existing resource").argument("<type>", "Resource type: scopes").argument("<id>", "Resource ID").action(async (type, id, _opts, cmd) => {
|
|
1622
|
+
const config = requireConfig();
|
|
1623
|
+
const jsonMode = cmd.parent?.opts().json;
|
|
1624
|
+
const supportedTypes = Object.keys(TYPE_ENDPOINTS4);
|
|
1625
|
+
if (!supportedTypes.includes(type)) {
|
|
1626
|
+
error(`Invalid type "${type}". Updatable types: ${supportedTypes.join(", ")}`);
|
|
1627
|
+
process.exit(1);
|
|
1628
|
+
}
|
|
1629
|
+
const projectId = await resolveProjectId(config);
|
|
1630
|
+
const endpoint = TYPE_ENDPOINTS4[type];
|
|
1631
|
+
const getResult = await apiCall(
|
|
1632
|
+
config,
|
|
1633
|
+
"GET",
|
|
1634
|
+
buildPath(endpoint.getPath(id), { projectId })
|
|
1635
|
+
);
|
|
1636
|
+
if (!getResult.ok) {
|
|
1637
|
+
const data = getResult.data;
|
|
1638
|
+
error(`Failed to fetch ${type}: ${data.error || `HTTP ${getResult.status}`}`);
|
|
1639
|
+
process.exit(1);
|
|
1640
|
+
}
|
|
1641
|
+
const existing = getResult.data[endpoint.key] ?? getResult.data;
|
|
1642
|
+
let updates;
|
|
1643
|
+
switch (type) {
|
|
1644
|
+
case "scopes":
|
|
1645
|
+
updates = await updateScope(existing);
|
|
1646
|
+
break;
|
|
1647
|
+
default:
|
|
1648
|
+
error(`Unsupported type: ${type}`);
|
|
1649
|
+
process.exit(1);
|
|
1650
|
+
}
|
|
1651
|
+
if (Object.keys(updates).length === 0) {
|
|
1652
|
+
console.log("No changes made.");
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
try {
|
|
1656
|
+
const result = await apiCall(
|
|
1657
|
+
config,
|
|
1658
|
+
"PATCH",
|
|
1659
|
+
buildPath(endpoint.patchPath(id), { projectId }),
|
|
1660
|
+
updates
|
|
1661
|
+
);
|
|
1662
|
+
if (!result.ok) {
|
|
1663
|
+
const errData = result.data;
|
|
1664
|
+
error(`Failed: ${errData.error || `HTTP ${result.status}`}`);
|
|
1665
|
+
process.exit(1);
|
|
1666
|
+
}
|
|
1667
|
+
if (jsonMode) {
|
|
1668
|
+
console.log(renderJson(result.data));
|
|
1669
|
+
} else {
|
|
1670
|
+
success("\nUpdated successfully!");
|
|
1671
|
+
}
|
|
1672
|
+
} catch (err) {
|
|
1673
|
+
error(`Failed: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
1674
|
+
process.exit(1);
|
|
1675
|
+
}
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
// src/commands/setup.ts
|
|
1679
|
+
import fs4 from "fs";
|
|
1680
|
+
import path6 from "path";
|
|
1681
|
+
import { Command as Command8 } from "commander";
|
|
1682
|
+
|
|
1683
|
+
// src/lib/log.ts
|
|
1684
|
+
var BOLD2 = "\x1B[1m";
|
|
1685
|
+
var DIM2 = "\x1B[2m";
|
|
1686
|
+
var RESET2 = "\x1B[0m";
|
|
1687
|
+
var CYAN2 = "\x1B[36m";
|
|
1688
|
+
var RED2 = "\x1B[31m";
|
|
1689
|
+
var YELLOW2 = "\x1B[33m";
|
|
1690
|
+
var GREEN2 = "\x1B[32m";
|
|
1691
|
+
var log = {
|
|
1692
|
+
banner() {
|
|
1693
|
+
console.log();
|
|
1694
|
+
console.log(` ${BOLD2}${CYAN2}Hissuno Setup${RESET2}`);
|
|
1695
|
+
console.log(` ${DIM2}Unified context layer for product agents${RESET2}`);
|
|
1696
|
+
console.log();
|
|
1697
|
+
},
|
|
1698
|
+
step(msg) {
|
|
1699
|
+
process.stdout.write(` ${CYAN2}>${RESET2} ${msg}... `);
|
|
1700
|
+
},
|
|
1701
|
+
success(msg) {
|
|
1702
|
+
console.log(` ${GREEN2}\u2713${RESET2} ${msg}`);
|
|
1703
|
+
},
|
|
1704
|
+
info(msg) {
|
|
1705
|
+
console.log(` ${CYAN2}i${RESET2} ${msg}`);
|
|
1706
|
+
},
|
|
1707
|
+
warn(msg) {
|
|
1708
|
+
console.log(` ${YELLOW2}!${RESET2} ${msg}`);
|
|
1709
|
+
},
|
|
1710
|
+
error(msg) {
|
|
1711
|
+
console.log(` ${RED2}\u2717${RESET2} ${msg}`);
|
|
1712
|
+
},
|
|
1713
|
+
fatal(msg) {
|
|
1714
|
+
console.log();
|
|
1715
|
+
console.log(` ${BOLD2}${RED2}Error: ${msg}${RESET2}`);
|
|
1716
|
+
console.log(` ${DIM2}Run with --help for usage information${RESET2}`);
|
|
1717
|
+
console.log();
|
|
1718
|
+
},
|
|
1719
|
+
ready(seeded) {
|
|
1720
|
+
console.log();
|
|
1721
|
+
console.log(` ${BOLD2}${GREEN2}Setup complete! Starting Hissuno...${RESET2}`);
|
|
1722
|
+
console.log();
|
|
1723
|
+
if (seeded) {
|
|
1724
|
+
console.log(` ${DIM2}Demo account:${RESET2}`);
|
|
1725
|
+
console.log(` Email: admin@hissuno.com`);
|
|
1726
|
+
console.log(` Password: admin123`);
|
|
1727
|
+
console.log();
|
|
1728
|
+
}
|
|
1729
|
+
console.log(` ${DIM2}Open http://localhost:3000${RESET2}`);
|
|
1730
|
+
console.log();
|
|
1731
|
+
this._integrationHint();
|
|
1732
|
+
},
|
|
1733
|
+
nextSteps(seeded) {
|
|
1734
|
+
console.log();
|
|
1735
|
+
console.log(` ${BOLD2}${GREEN2}Setup complete!${RESET2}`);
|
|
1736
|
+
console.log();
|
|
1737
|
+
console.log(` ${BOLD2}Next steps:${RESET2}`);
|
|
1738
|
+
console.log();
|
|
1739
|
+
console.log(` cd hissuno/app`);
|
|
1740
|
+
console.log(` npm run dev`);
|
|
1741
|
+
console.log();
|
|
1742
|
+
if (seeded) {
|
|
1743
|
+
console.log(` ${DIM2}Demo account:${RESET2}`);
|
|
1744
|
+
console.log(` Email: admin@hissuno.com`);
|
|
1745
|
+
console.log(` Password: admin123`);
|
|
1746
|
+
console.log();
|
|
1747
|
+
}
|
|
1748
|
+
console.log(` ${DIM2}Then open http://localhost:3000${RESET2}`);
|
|
1749
|
+
console.log();
|
|
1750
|
+
this._integrationHint();
|
|
1751
|
+
},
|
|
1752
|
+
_integrationHint() {
|
|
1753
|
+
console.log(` ${BOLD2}Connect your data sources:${RESET2}`);
|
|
1754
|
+
console.log(` ${CYAN2}hissuno integrate${RESET2} ${DIM2}# see all integrations${RESET2}`);
|
|
1755
|
+
console.log(` ${CYAN2}hissuno integrate slack${RESET2} ${DIM2}# connect Slack${RESET2}`);
|
|
1756
|
+
console.log(` ${CYAN2}hissuno integrate intercom${RESET2} ${DIM2}# connect Intercom${RESET2}`);
|
|
1757
|
+
console.log();
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
|
|
1761
|
+
// src/lib/exec.ts
|
|
1762
|
+
import { execFile, spawn } from "child_process";
|
|
1763
|
+
import { promisify } from "util";
|
|
1764
|
+
var execFileAsync = promisify(execFile);
|
|
1765
|
+
async function exec2(cmd, args, opts) {
|
|
1766
|
+
const { stdout, stderr } = await execFileAsync(cmd, args, {
|
|
1767
|
+
cwd: opts?.cwd,
|
|
1768
|
+
env: { ...process.env, ...opts?.env }
|
|
1769
|
+
});
|
|
1770
|
+
return { stdout: stdout.toString(), stderr: stderr.toString() };
|
|
1771
|
+
}
|
|
1772
|
+
function execStream(cmd, args, opts) {
|
|
1773
|
+
return new Promise((resolve, reject) => {
|
|
1774
|
+
const child = spawn(cmd, args, {
|
|
1775
|
+
cwd: opts?.cwd,
|
|
1776
|
+
env: { ...process.env, ...opts?.env },
|
|
1777
|
+
stdio: "inherit"
|
|
1778
|
+
});
|
|
1779
|
+
child.on("close", (code) => {
|
|
1780
|
+
if (code === 0) resolve();
|
|
1781
|
+
else reject(new Error(`${cmd} exited with code ${code}`));
|
|
1782
|
+
});
|
|
1783
|
+
child.on("error", reject);
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
function execCapture(cmd, args, opts) {
|
|
1787
|
+
return new Promise((resolve, reject) => {
|
|
1788
|
+
const child = spawn(cmd, args, {
|
|
1789
|
+
cwd: opts?.cwd,
|
|
1790
|
+
env: { ...process.env, ...opts?.env },
|
|
1791
|
+
stdio: ["inherit", "pipe", "inherit"]
|
|
1792
|
+
});
|
|
1793
|
+
let stdout = "";
|
|
1794
|
+
child.stdout.on("data", (data) => {
|
|
1795
|
+
stdout += data.toString();
|
|
1796
|
+
});
|
|
1797
|
+
child.on("close", (code) => {
|
|
1798
|
+
if (code === 0) resolve({ stdout });
|
|
1799
|
+
else reject(new Error(`${cmd} exited with code ${code}`));
|
|
1800
|
+
});
|
|
1801
|
+
child.on("error", reject);
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
async function commandExists(cmd) {
|
|
1805
|
+
try {
|
|
1806
|
+
await execFileAsync("which", [cmd]);
|
|
1807
|
+
return true;
|
|
1808
|
+
} catch {
|
|
1809
|
+
return false;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// src/commands/setup/check-node.ts
|
|
1814
|
+
async function checkNode() {
|
|
1815
|
+
const major = parseInt(process.versions.node.split(".")[0], 10);
|
|
1816
|
+
if (major < 20) {
|
|
1817
|
+
throw new Error(`Node.js 20+ required. You have v${process.versions.node}`);
|
|
1818
|
+
}
|
|
1819
|
+
log.success(`Node.js v${process.versions.node}`);
|
|
1820
|
+
const hasGit = await commandExists("git");
|
|
1821
|
+
if (!hasGit) {
|
|
1822
|
+
throw new Error("git is required but not found on PATH");
|
|
1823
|
+
}
|
|
1824
|
+
log.success("git found");
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// src/commands/setup/clone.ts
|
|
1828
|
+
import fs from "fs";
|
|
1829
|
+
import path from "path";
|
|
1830
|
+
async function cloneRepo() {
|
|
1831
|
+
const projectDir = path.resolve("hissuno");
|
|
1832
|
+
if (fs.existsSync(projectDir)) {
|
|
1833
|
+
throw new Error('Directory "hissuno" already exists');
|
|
1834
|
+
}
|
|
1835
|
+
log.info("Cloning Hissuno...");
|
|
1836
|
+
await execStream("git", [
|
|
1837
|
+
"clone",
|
|
1838
|
+
"--depth",
|
|
1839
|
+
"1",
|
|
1840
|
+
"https://github.com/hissuno/hissuno.git",
|
|
1841
|
+
projectDir
|
|
1842
|
+
]);
|
|
1843
|
+
fs.rmSync(path.join(projectDir, ".git"), { recursive: true, force: true });
|
|
1844
|
+
log.success("Repository cloned");
|
|
1845
|
+
return projectDir;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// src/commands/setup/install.ts
|
|
1849
|
+
async function installDeps(appDir) {
|
|
1850
|
+
log.info("Installing dependencies...");
|
|
1851
|
+
await execStream("npm", ["install"], { cwd: appDir });
|
|
1852
|
+
log.success("Dependencies installed");
|
|
1853
|
+
}
|
|
1854
|
+
async function buildWidget(appDir) {
|
|
1855
|
+
log.info("Building feedback widget...");
|
|
1856
|
+
await execStream("npm", ["run", "build:widget"], { cwd: appDir });
|
|
1857
|
+
log.success("Widget built");
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
// src/commands/setup/detect-postgres.ts
|
|
1861
|
+
import { select as select4, input as input4 } from "@inquirer/prompts";
|
|
1862
|
+
async function detectPostgres() {
|
|
1863
|
+
const hasPsql = await commandExists("psql");
|
|
1864
|
+
if (hasPsql) {
|
|
1865
|
+
return handleExistingPostgres();
|
|
1866
|
+
}
|
|
1867
|
+
return handleMissingPostgres();
|
|
1868
|
+
}
|
|
1869
|
+
async function handleExistingPostgres() {
|
|
1870
|
+
log.success("PostgreSQL found");
|
|
1871
|
+
const choice = await select4({
|
|
1872
|
+
message: "How would you like to connect to PostgreSQL?",
|
|
1873
|
+
choices: [
|
|
1874
|
+
{ name: "Use local PostgreSQL (default)", value: "local" },
|
|
1875
|
+
{ name: "Enter DATABASE_URL manually", value: "manual" }
|
|
1876
|
+
]
|
|
1877
|
+
});
|
|
1878
|
+
if (choice === "manual") {
|
|
1879
|
+
return promptManualUrl();
|
|
1880
|
+
}
|
|
1881
|
+
const databaseUrl = await input4({
|
|
1882
|
+
message: "DATABASE_URL:",
|
|
1883
|
+
default: "postgresql://localhost:5432/hissuno"
|
|
1884
|
+
});
|
|
1885
|
+
return { databaseUrl, needsCreateDb: true, isDocker: false };
|
|
1886
|
+
}
|
|
1887
|
+
async function handleMissingPostgres() {
|
|
1888
|
+
log.warn("PostgreSQL not found on PATH");
|
|
1889
|
+
const choices = [];
|
|
1890
|
+
const hasBrew = await commandExists("brew");
|
|
1891
|
+
const hasApt = await commandExists("apt-get");
|
|
1892
|
+
const hasDocker = await commandExists("docker");
|
|
1893
|
+
if (hasBrew) {
|
|
1894
|
+
choices.push({ name: "Install via Homebrew (macOS)", value: "brew" });
|
|
1895
|
+
}
|
|
1896
|
+
if (hasApt) {
|
|
1897
|
+
choices.push({ name: "Install via apt-get (Linux)", value: "apt" });
|
|
1898
|
+
}
|
|
1899
|
+
if (hasDocker) {
|
|
1900
|
+
choices.push({ name: "Run via Docker", value: "docker" });
|
|
1901
|
+
}
|
|
1902
|
+
choices.push({ name: "Enter DATABASE_URL manually", value: "manual" });
|
|
1903
|
+
const choice = await select4({
|
|
1904
|
+
message: "How would you like to set up PostgreSQL?",
|
|
1905
|
+
choices
|
|
1906
|
+
});
|
|
1907
|
+
switch (choice) {
|
|
1908
|
+
case "brew":
|
|
1909
|
+
return installViaBrew();
|
|
1910
|
+
case "apt":
|
|
1911
|
+
return installViaApt();
|
|
1912
|
+
case "docker":
|
|
1913
|
+
return installViaDocker();
|
|
1914
|
+
default:
|
|
1915
|
+
return promptManualUrl();
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
async function installViaBrew() {
|
|
1919
|
+
log.info("Installing PostgreSQL via Homebrew...");
|
|
1920
|
+
await execStream("brew", ["install", "postgresql@15"]);
|
|
1921
|
+
await execStream("brew", ["install", "pgvector"]);
|
|
1922
|
+
await execStream("brew", ["services", "start", "postgresql@15"]);
|
|
1923
|
+
log.success("PostgreSQL installed and started");
|
|
1924
|
+
const databaseUrl = await input4({
|
|
1925
|
+
message: "DATABASE_URL:",
|
|
1926
|
+
default: "postgresql://localhost:5432/hissuno"
|
|
1927
|
+
});
|
|
1928
|
+
return { databaseUrl, needsCreateDb: true, isDocker: false };
|
|
1929
|
+
}
|
|
1930
|
+
async function installViaApt() {
|
|
1931
|
+
log.info("Installing PostgreSQL via apt-get...");
|
|
1932
|
+
await execStream("sudo", ["apt-get", "install", "-y", "postgresql-15", "postgresql-15-pgvector"]);
|
|
1933
|
+
log.success("PostgreSQL installed");
|
|
1934
|
+
const databaseUrl = await input4({
|
|
1935
|
+
message: "DATABASE_URL:",
|
|
1936
|
+
default: "postgresql://localhost:5432/hissuno"
|
|
1937
|
+
});
|
|
1938
|
+
return { databaseUrl, needsCreateDb: true, isDocker: false };
|
|
1939
|
+
}
|
|
1940
|
+
async function installViaDocker() {
|
|
1941
|
+
log.info("Starting PostgreSQL via Docker...");
|
|
1942
|
+
try {
|
|
1943
|
+
const { stdout } = await exec2("docker", ["ps", "-a", "--filter", "name=hissuno-postgres", "--format", "{{.Names}}"]);
|
|
1944
|
+
if (stdout.trim() === "hissuno-postgres") {
|
|
1945
|
+
log.info('Container "hissuno-postgres" already exists, starting it...');
|
|
1946
|
+
await execStream("docker", ["start", "hissuno-postgres"]);
|
|
1947
|
+
} else {
|
|
1948
|
+
await execStream("docker", [
|
|
1949
|
+
"run",
|
|
1950
|
+
"-d",
|
|
1951
|
+
"--name",
|
|
1952
|
+
"hissuno-postgres",
|
|
1953
|
+
"-e",
|
|
1954
|
+
"POSTGRES_USER=hissuno",
|
|
1955
|
+
"-e",
|
|
1956
|
+
"POSTGRES_PASSWORD=hissuno",
|
|
1957
|
+
"-e",
|
|
1958
|
+
"POSTGRES_DB=hissuno",
|
|
1959
|
+
"-p",
|
|
1960
|
+
"5432:5432",
|
|
1961
|
+
"pgvector/pgvector:pg16"
|
|
1962
|
+
]);
|
|
1963
|
+
}
|
|
1964
|
+
} catch {
|
|
1965
|
+
await execStream("docker", [
|
|
1966
|
+
"run",
|
|
1967
|
+
"-d",
|
|
1968
|
+
"--name",
|
|
1969
|
+
"hissuno-postgres",
|
|
1970
|
+
"-e",
|
|
1971
|
+
"POSTGRES_USER=hissuno",
|
|
1972
|
+
"-e",
|
|
1973
|
+
"POSTGRES_PASSWORD=hissuno",
|
|
1974
|
+
"-e",
|
|
1975
|
+
"POSTGRES_DB=hissuno",
|
|
1976
|
+
"-p",
|
|
1977
|
+
"5432:5432",
|
|
1978
|
+
"pgvector/pgvector:pg16"
|
|
1979
|
+
]);
|
|
1980
|
+
}
|
|
1981
|
+
log.success("PostgreSQL running in Docker");
|
|
1982
|
+
return {
|
|
1983
|
+
databaseUrl: "postgresql://hissuno:hissuno@localhost:5432/hissuno",
|
|
1984
|
+
needsCreateDb: false,
|
|
1985
|
+
isDocker: true
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
async function promptManualUrl() {
|
|
1989
|
+
const databaseUrl = await input4({
|
|
1990
|
+
message: "Enter your DATABASE_URL:",
|
|
1991
|
+
validate: (val) => {
|
|
1992
|
+
if (!val.startsWith("postgresql://") && !val.startsWith("postgres://")) {
|
|
1993
|
+
return "URL must start with postgresql:// or postgres://";
|
|
1994
|
+
}
|
|
1995
|
+
return true;
|
|
1996
|
+
}
|
|
1997
|
+
});
|
|
1998
|
+
return { databaseUrl, needsCreateDb: false, isDocker: false };
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
// src/commands/setup/configure-env.ts
|
|
2002
|
+
import fs2 from "fs";
|
|
2003
|
+
import path2 from "path";
|
|
2004
|
+
import crypto from "crypto";
|
|
2005
|
+
import { confirm as confirm4, input as input5, password as password2 } from "@inquirer/prompts";
|
|
2006
|
+
async function configureEnv(appDir, databaseUrl, envFile = ".env.local") {
|
|
2007
|
+
const appUrl = await input5({
|
|
2008
|
+
message: "App URL:",
|
|
2009
|
+
default: "http://localhost:3000"
|
|
2010
|
+
});
|
|
2011
|
+
const wantsOpenai = await confirm4({
|
|
2012
|
+
message: "Add an OpenAI API key? (enables AI analysis & semantic search)",
|
|
2013
|
+
default: true
|
|
2014
|
+
});
|
|
2015
|
+
let openaiKey;
|
|
2016
|
+
if (wantsOpenai) {
|
|
2017
|
+
openaiKey = await password2({
|
|
2018
|
+
message: "Enter your OpenAI API key:",
|
|
2019
|
+
mask: "*",
|
|
2020
|
+
validate: (val) => {
|
|
2021
|
+
if (!val.startsWith("sk-")) {
|
|
2022
|
+
return "API key should start with sk-";
|
|
2023
|
+
}
|
|
2024
|
+
return true;
|
|
2025
|
+
}
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
const authSecret = crypto.randomBytes(32).toString("base64");
|
|
2029
|
+
const envLines = [
|
|
2030
|
+
`DATABASE_URL=${databaseUrl}`,
|
|
2031
|
+
`AUTH_SECRET=${authSecret}`,
|
|
2032
|
+
`NEXT_PUBLIC_APP_URL=${appUrl}`
|
|
2033
|
+
];
|
|
2034
|
+
if (openaiKey) {
|
|
2035
|
+
envLines.push(`OPENAI_API_KEY=${openaiKey}`);
|
|
2036
|
+
}
|
|
2037
|
+
envLines.push("");
|
|
2038
|
+
const envContent = envLines.join("\n");
|
|
2039
|
+
const envPath = path2.join(appDir, envFile);
|
|
2040
|
+
fs2.writeFileSync(envPath, envContent, "utf-8");
|
|
2041
|
+
log.success(`${envFile} created`);
|
|
2042
|
+
return { appUrl };
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
// src/commands/setup/setup-database.ts
|
|
2046
|
+
import path3 from "path";
|
|
2047
|
+
async function setupDatabase(appDir, opts) {
|
|
2048
|
+
const { databaseUrl, needsCreateDb = false, isDocker = false } = opts;
|
|
2049
|
+
if (needsCreateDb) {
|
|
2050
|
+
const dbName = extractDbName(databaseUrl);
|
|
2051
|
+
log.info(`Creating database "${dbName}"...`);
|
|
2052
|
+
try {
|
|
2053
|
+
await exec2("createdb", [dbName]);
|
|
2054
|
+
log.success(`Database "${dbName}" created`);
|
|
2055
|
+
} catch (err) {
|
|
2056
|
+
if (err.stderr?.includes("already exists")) {
|
|
2057
|
+
log.info(`Database "${dbName}" already exists`);
|
|
2058
|
+
} else {
|
|
2059
|
+
throw err;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
log.info("Enabling pgvector extension...");
|
|
2064
|
+
if (isDocker) {
|
|
2065
|
+
await exec2("docker", [
|
|
2066
|
+
"exec",
|
|
2067
|
+
"hissuno-postgres",
|
|
2068
|
+
"psql",
|
|
2069
|
+
"-U",
|
|
2070
|
+
"hissuno",
|
|
2071
|
+
"-d",
|
|
2072
|
+
"hissuno",
|
|
2073
|
+
"-c",
|
|
2074
|
+
"CREATE EXTENSION IF NOT EXISTS vector;"
|
|
2075
|
+
]);
|
|
2076
|
+
} else {
|
|
2077
|
+
await exec2("psql", [databaseUrl, "-c", "CREATE EXTENSION IF NOT EXISTS vector;"]);
|
|
2078
|
+
}
|
|
2079
|
+
let hasVector = false;
|
|
2080
|
+
try {
|
|
2081
|
+
let stdout;
|
|
2082
|
+
if (isDocker) {
|
|
2083
|
+
const result = await exec2("docker", [
|
|
2084
|
+
"exec",
|
|
2085
|
+
"hissuno-postgres",
|
|
2086
|
+
"psql",
|
|
2087
|
+
"-U",
|
|
2088
|
+
"hissuno",
|
|
2089
|
+
"-d",
|
|
2090
|
+
"hissuno",
|
|
2091
|
+
"-tAc",
|
|
2092
|
+
"SELECT 1 FROM pg_extension WHERE extname = 'vector';"
|
|
2093
|
+
]);
|
|
2094
|
+
stdout = result.stdout;
|
|
2095
|
+
} else {
|
|
2096
|
+
const result = await exec2("psql", [
|
|
2097
|
+
databaseUrl,
|
|
2098
|
+
"-tAc",
|
|
2099
|
+
"SELECT 1 FROM pg_extension WHERE extname = 'vector';"
|
|
2100
|
+
]);
|
|
2101
|
+
stdout = result.stdout;
|
|
2102
|
+
}
|
|
2103
|
+
hasVector = stdout.trim() === "1";
|
|
2104
|
+
} catch {
|
|
2105
|
+
}
|
|
2106
|
+
if (!hasVector) {
|
|
2107
|
+
throw new Error(
|
|
2108
|
+
"pgvector extension could not be enabled. Install pgvector for your PostgreSQL version: https://github.com/pgvector/pgvector#installation"
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
2111
|
+
log.success("pgvector enabled");
|
|
2112
|
+
log.info("Pushing database schema...");
|
|
2113
|
+
const drizzleKit = path3.join(appDir, "node_modules", ".bin", "drizzle-kit");
|
|
2114
|
+
await execStream(drizzleKit, ["push"], {
|
|
2115
|
+
cwd: appDir,
|
|
2116
|
+
env: { DATABASE_URL: databaseUrl }
|
|
2117
|
+
});
|
|
2118
|
+
log.success("Database schema pushed");
|
|
2119
|
+
}
|
|
2120
|
+
function extractDbName(url) {
|
|
2121
|
+
const parsed = new URL(url);
|
|
2122
|
+
return parsed.pathname.slice(1);
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// src/commands/setup/seed.ts
|
|
2126
|
+
import path4 from "path";
|
|
2127
|
+
import { confirm as confirm5 } from "@inquirer/prompts";
|
|
2128
|
+
async function seedDatabase(appDir, envFile = ".env.local") {
|
|
2129
|
+
const shouldSeed = await confirm5({
|
|
2130
|
+
message: "Seed with demo data? (admin user + project with sample sessions, issues, companies, and contacts)",
|
|
2131
|
+
default: true
|
|
2132
|
+
});
|
|
2133
|
+
if (!shouldSeed) {
|
|
2134
|
+
log.info("Skipping seed");
|
|
2135
|
+
return { seeded: false };
|
|
2136
|
+
}
|
|
2137
|
+
try {
|
|
2138
|
+
log.info("Seeding database...");
|
|
2139
|
+
const tsx = path4.join(appDir, "node_modules", ".bin", "tsx");
|
|
2140
|
+
const { stdout } = await execCapture(
|
|
2141
|
+
tsx,
|
|
2142
|
+
["--tsconfig", "tsconfig.json", "--env-file", envFile, "src/scripts/seed.ts", "--demo", "--output-api-key"],
|
|
2143
|
+
{ cwd: appDir }
|
|
2144
|
+
);
|
|
2145
|
+
log.success("Demo data seeded");
|
|
2146
|
+
const passwordMatch = stdout.match(/\[seed\] Generated admin password: (\S+)/);
|
|
2147
|
+
if (passwordMatch) {
|
|
2148
|
+
log.info(`Admin email: admin@hissuno.com`);
|
|
2149
|
+
log.info(`Admin password: ${passwordMatch[1]}`);
|
|
2150
|
+
}
|
|
2151
|
+
const projectMatch = stdout.match(/\[seed\] Created demo project: (.+) \((.+)\)/);
|
|
2152
|
+
if (projectMatch) {
|
|
2153
|
+
log.info(`Demo project: ${projectMatch[1]} (${projectMatch[2]})`);
|
|
2154
|
+
}
|
|
2155
|
+
const apiKeyMatch = stdout.match(/HISSUNO_API_KEY=(\S+)/);
|
|
2156
|
+
return { seeded: true, apiKey: apiKeyMatch?.[1] };
|
|
2157
|
+
} catch (err) {
|
|
2158
|
+
log.warn(`Seed failed: ${err.message}`);
|
|
2159
|
+
log.warn("You can run it manually later: npm run seed");
|
|
2160
|
+
return { seeded: false };
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
// src/commands/setup/create-config.ts
|
|
2165
|
+
import { confirm as confirm6 } from "@inquirer/prompts";
|
|
2166
|
+
async function createConfig(apiKey, baseUrl = "http://localhost:3000") {
|
|
2167
|
+
const shouldConfigure = await confirm6({
|
|
2168
|
+
message: "Auto-configure the CLI with the generated API key?",
|
|
2169
|
+
default: true
|
|
2170
|
+
});
|
|
2171
|
+
if (!shouldConfigure) {
|
|
2172
|
+
log.info("Skipping auto-config. Run `hissuno config` later to connect manually.");
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
saveConfig({
|
|
2176
|
+
api_key: apiKey,
|
|
2177
|
+
base_url: baseUrl
|
|
2178
|
+
});
|
|
2179
|
+
log.success("CLI configured (~/.hissuno/config.json)");
|
|
2180
|
+
log.info("Run `hissuno status` to verify your connection.");
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
// src/commands/setup/start.ts
|
|
2184
|
+
import { confirm as confirm7 } from "@inquirer/prompts";
|
|
2185
|
+
async function startServer(appDir, seeded) {
|
|
2186
|
+
const shouldStart = await confirm7({
|
|
2187
|
+
message: "Start Hissuno now?",
|
|
2188
|
+
default: true
|
|
2189
|
+
});
|
|
2190
|
+
if (!shouldStart) {
|
|
2191
|
+
log.nextSteps(seeded);
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
log.ready(seeded);
|
|
2195
|
+
await execStream("npm", ["run", "dev"], { cwd: appDir });
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
// src/commands/setup/setup-oauth.ts
|
|
2199
|
+
import fs3 from "fs";
|
|
2200
|
+
import path5 from "path";
|
|
2201
|
+
import { input as input6, password as password3, select as select5 } from "@inquirer/prompts";
|
|
2202
|
+
var OAUTH_PLATFORMS2 = {
|
|
2203
|
+
slack: {
|
|
2204
|
+
label: "Slack",
|
|
2205
|
+
envVars: [
|
|
2206
|
+
{ key: "SLACK_CLIENT_ID", prompt: "Slack Client ID:", mask: false },
|
|
2207
|
+
{ key: "SLACK_CLIENT_SECRET", prompt: "Slack Client Secret:", mask: true }
|
|
2208
|
+
]
|
|
2209
|
+
},
|
|
2210
|
+
github: {
|
|
2211
|
+
label: "GitHub",
|
|
2212
|
+
envVars: [
|
|
2213
|
+
{ key: "GITHUB_APP_SLUG", prompt: "GitHub App slug:", mask: false },
|
|
2214
|
+
{ key: "GITHUB_APP_ID", prompt: "GitHub App ID:", mask: false },
|
|
2215
|
+
{ key: "GITHUB_APP_PRIVATE_KEY", prompt: "GitHub Private Key (base64-encoded):", mask: true }
|
|
2216
|
+
]
|
|
2217
|
+
},
|
|
2218
|
+
jira: {
|
|
2219
|
+
label: "Jira",
|
|
2220
|
+
envVars: [
|
|
2221
|
+
{ key: "JIRA_CLIENT_ID", prompt: "Jira Client ID:", mask: false },
|
|
2222
|
+
{ key: "JIRA_CLIENT_SECRET", prompt: "Jira Client Secret:", mask: true }
|
|
2223
|
+
]
|
|
2224
|
+
},
|
|
2225
|
+
linear: {
|
|
2226
|
+
label: "Linear",
|
|
2227
|
+
envVars: [
|
|
2228
|
+
{ key: "LINEAR_CLIENT_ID", prompt: "Linear Client ID:", mask: false },
|
|
2229
|
+
{ key: "LINEAR_CLIENT_SECRET", prompt: "Linear Client Secret:", mask: true }
|
|
2230
|
+
]
|
|
2231
|
+
},
|
|
2232
|
+
intercom: {
|
|
2233
|
+
label: "Intercom",
|
|
2234
|
+
envVars: [
|
|
2235
|
+
{ key: "INTERCOM_CLIENT_ID", prompt: "Intercom Client ID:", mask: false },
|
|
2236
|
+
{ key: "INTERCOM_CLIENT_SECRET", prompt: "Intercom Client Secret:", mask: true }
|
|
2237
|
+
]
|
|
2238
|
+
}
|
|
2239
|
+
};
|
|
2240
|
+
var OAUTH_PLATFORM_NAMES = Object.keys(OAUTH_PLATFORMS2);
|
|
2241
|
+
async function setupOAuth(appDir, platformArg, opts = {}) {
|
|
2242
|
+
const platform2 = platformArg ? validatePlatform(platformArg) : await select5({
|
|
2243
|
+
message: "Which integration?",
|
|
2244
|
+
choices: OAUTH_PLATFORM_NAMES.map((p) => ({
|
|
2245
|
+
value: p,
|
|
2246
|
+
name: OAUTH_PLATFORMS2[p].label
|
|
2247
|
+
}))
|
|
2248
|
+
});
|
|
2249
|
+
const config = OAUTH_PLATFORMS2[platform2];
|
|
2250
|
+
const values = {};
|
|
2251
|
+
for (const envVar of config.envVars) {
|
|
2252
|
+
const flagValue = resolveFlag(envVar.key, opts);
|
|
2253
|
+
if (flagValue) {
|
|
2254
|
+
values[envVar.key] = flagValue;
|
|
2255
|
+
} else if (envVar.mask) {
|
|
2256
|
+
values[envVar.key] = await password3({
|
|
2257
|
+
message: envVar.prompt,
|
|
2258
|
+
mask: "*",
|
|
2259
|
+
validate: (v) => v.length > 0 || "Required"
|
|
2260
|
+
});
|
|
2261
|
+
} else {
|
|
2262
|
+
values[envVar.key] = await input6({
|
|
2263
|
+
message: envVar.prompt,
|
|
2264
|
+
validate: (v) => v.length > 0 || "Required"
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
const envPath = path5.join(appDir, ".env.local");
|
|
2269
|
+
if (!fs3.existsSync(envPath)) {
|
|
2270
|
+
throw new Error(`.env.local not found at ${envPath}. Run 'hissuno setup' first.`);
|
|
2271
|
+
}
|
|
2272
|
+
const existing = fs3.readFileSync(envPath, "utf-8");
|
|
2273
|
+
const newLines = [];
|
|
2274
|
+
for (const [key, value] of Object.entries(values)) {
|
|
2275
|
+
if (existing.includes(`${key}=`)) {
|
|
2276
|
+
log.warn(`${key} already set in .env.local - skipping`);
|
|
2277
|
+
} else {
|
|
2278
|
+
newLines.push(`${key}=${value}`);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
if (newLines.length > 0) {
|
|
2282
|
+
const suffix = (existing.endsWith("\n") ? "" : "\n") + newLines.join("\n") + "\n";
|
|
2283
|
+
fs3.appendFileSync(envPath, suffix);
|
|
2284
|
+
log.success(`${config.label} OAuth credentials added to .env.local`);
|
|
2285
|
+
} else {
|
|
2286
|
+
log.info(`${config.label} credentials already present - no changes made`);
|
|
2287
|
+
}
|
|
2288
|
+
log.info("Restart the server for changes to take effect:");
|
|
2289
|
+
console.log(` npm run dev`);
|
|
2290
|
+
console.log();
|
|
2291
|
+
}
|
|
2292
|
+
function validatePlatform(name) {
|
|
2293
|
+
const lower = name.toLowerCase();
|
|
2294
|
+
if (!OAUTH_PLATFORM_NAMES.includes(lower)) {
|
|
2295
|
+
throw new Error(
|
|
2296
|
+
`Unknown OAuth platform "${name}". Supported: ${OAUTH_PLATFORM_NAMES.join(", ")}`
|
|
2297
|
+
);
|
|
2298
|
+
}
|
|
2299
|
+
return lower;
|
|
2300
|
+
}
|
|
2301
|
+
function resolveFlag(envKey, opts) {
|
|
2302
|
+
switch (envKey) {
|
|
2303
|
+
case "SLACK_CLIENT_ID":
|
|
2304
|
+
case "JIRA_CLIENT_ID":
|
|
2305
|
+
case "LINEAR_CLIENT_ID":
|
|
2306
|
+
case "INTERCOM_CLIENT_ID":
|
|
2307
|
+
return opts.clientId;
|
|
2308
|
+
case "SLACK_CLIENT_SECRET":
|
|
2309
|
+
case "JIRA_CLIENT_SECRET":
|
|
2310
|
+
case "LINEAR_CLIENT_SECRET":
|
|
2311
|
+
case "INTERCOM_CLIENT_SECRET":
|
|
2312
|
+
return opts.clientSecret;
|
|
2313
|
+
case "GITHUB_APP_SLUG":
|
|
2314
|
+
return opts.appSlug;
|
|
2315
|
+
case "GITHUB_APP_ID":
|
|
2316
|
+
return opts.appId;
|
|
2317
|
+
case "GITHUB_APP_PRIVATE_KEY":
|
|
2318
|
+
return opts.privateKey;
|
|
2319
|
+
default:
|
|
2320
|
+
return void 0;
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
// src/commands/setup.ts
|
|
2325
|
+
var STEP_NAMES = [
|
|
2326
|
+
"check-node",
|
|
2327
|
+
"clone",
|
|
2328
|
+
"install",
|
|
2329
|
+
"build",
|
|
2330
|
+
"postgres",
|
|
2331
|
+
"env",
|
|
2332
|
+
"database",
|
|
2333
|
+
"seed",
|
|
2334
|
+
"config",
|
|
2335
|
+
"start"
|
|
2336
|
+
];
|
|
2337
|
+
function resolveAppDir() {
|
|
2338
|
+
return path6.join(path6.resolve("hissuno"), "app");
|
|
2339
|
+
}
|
|
2340
|
+
function resolveDatabaseUrl(appDir, envFile) {
|
|
2341
|
+
if (process.env.DATABASE_URL) return process.env.DATABASE_URL;
|
|
2342
|
+
const envPath = path6.join(appDir, envFile);
|
|
2343
|
+
try {
|
|
2344
|
+
const content = fs4.readFileSync(envPath, "utf-8");
|
|
2345
|
+
const match = content.match(/^DATABASE_URL=(.+)$/m);
|
|
2346
|
+
if (match) return match[1].trim();
|
|
2347
|
+
} catch {
|
|
2348
|
+
}
|
|
2349
|
+
return void 0;
|
|
2350
|
+
}
|
|
2351
|
+
function shouldRun(step, from, only) {
|
|
2352
|
+
if (only) return only.has(step);
|
|
2353
|
+
if (!from) return true;
|
|
2354
|
+
return STEP_NAMES.indexOf(step) >= STEP_NAMES.indexOf(from);
|
|
2355
|
+
}
|
|
2356
|
+
function parseSteps(raw) {
|
|
2357
|
+
const steps = /* @__PURE__ */ new Set();
|
|
2358
|
+
for (const s of raw.split(",")) {
|
|
2359
|
+
const trimmed = s.trim();
|
|
2360
|
+
if (!STEP_NAMES.includes(trimmed)) {
|
|
2361
|
+
throw new Error(`Unknown step "${trimmed}". Valid steps: ${STEP_NAMES.join(", ")}`);
|
|
2362
|
+
}
|
|
2363
|
+
steps.add(trimmed);
|
|
2364
|
+
}
|
|
2365
|
+
return steps;
|
|
2366
|
+
}
|
|
2367
|
+
var setupCommand = new Command8("setup").description("Set up a new Hissuno instance (clone, install, configure)").option("--from <step>", `Resume from a step: ${STEP_NAMES.join(", ")}`).option("--only <steps>", "Run only specific steps (comma-separated)").option("--app-dir <dir>", "Path to the app directory (default: ./hissuno/app)").option("--env <environment>", "Target environment (determines env file: .env.<environment>)");
|
|
2368
|
+
setupCommand.command("oauth").argument("[platform]", `Integration: ${OAUTH_PLATFORM_NAMES.join(", ")}`).description("Configure OAuth credentials for an integration").option("--client-id <id>", "OAuth client ID").option("--client-secret <secret>", "OAuth client secret").option("--app-slug <slug>", "GitHub App slug").option("--app-id <id>", "GitHub App ID").option("--private-key <key>", "GitHub private key (base64)").option("--app-dir <dir>", "Path to the app directory (default: ./hissuno/app)").action(async (platformArg, opts) => {
|
|
2369
|
+
log.banner();
|
|
2370
|
+
try {
|
|
2371
|
+
const appDir = opts.appDir || resolveAppDir();
|
|
2372
|
+
await setupOAuth(appDir, platformArg, {
|
|
2373
|
+
clientId: opts.clientId,
|
|
2374
|
+
clientSecret: opts.clientSecret,
|
|
2375
|
+
appSlug: opts.appSlug,
|
|
2376
|
+
appId: opts.appId,
|
|
2377
|
+
privateKey: opts.privateKey
|
|
2378
|
+
});
|
|
2379
|
+
} catch (err) {
|
|
2380
|
+
log.fatal(err.message);
|
|
2381
|
+
process.exit(1);
|
|
2382
|
+
}
|
|
2383
|
+
});
|
|
2384
|
+
setupCommand.action(async (opts) => {
|
|
2385
|
+
log.banner();
|
|
2386
|
+
const from = opts.from;
|
|
2387
|
+
const only = opts.only ? parseSteps(opts.only) : void 0;
|
|
2388
|
+
if (from && !STEP_NAMES.includes(from)) {
|
|
2389
|
+
log.fatal(`Unknown step "${from}". Valid steps: ${STEP_NAMES.join(", ")}`);
|
|
2390
|
+
process.exit(1);
|
|
2391
|
+
}
|
|
2392
|
+
const run = (step) => shouldRun(step, from, only);
|
|
2393
|
+
try {
|
|
2394
|
+
let appDir = opts.appDir ? path6.resolve(opts.appDir) : path6.join(path6.resolve("hissuno"), "app");
|
|
2395
|
+
let projectDir = path6.dirname(appDir);
|
|
2396
|
+
const envFile = opts.env ? `.env.${opts.env}` : ".env.local";
|
|
2397
|
+
let databaseUrl = "";
|
|
2398
|
+
let appUrl = "http://localhost:3000";
|
|
2399
|
+
let seeded = false;
|
|
2400
|
+
let apiKey;
|
|
2401
|
+
if (run("check-node")) {
|
|
2402
|
+
await checkNode();
|
|
2403
|
+
}
|
|
2404
|
+
if (run("clone")) {
|
|
2405
|
+
projectDir = await cloneRepo();
|
|
2406
|
+
appDir = path6.join(projectDir, "app");
|
|
2407
|
+
}
|
|
2408
|
+
if (run("install")) {
|
|
2409
|
+
await installDeps(appDir);
|
|
2410
|
+
}
|
|
2411
|
+
if (run("build")) {
|
|
2412
|
+
await buildWidget(appDir);
|
|
2413
|
+
}
|
|
2414
|
+
if (run("postgres")) {
|
|
2415
|
+
const pgResult = await detectPostgres();
|
|
2416
|
+
databaseUrl = pgResult.databaseUrl;
|
|
2417
|
+
if (run("env")) {
|
|
2418
|
+
const envResult = await configureEnv(appDir, databaseUrl, envFile);
|
|
2419
|
+
appUrl = envResult.appUrl;
|
|
2420
|
+
}
|
|
2421
|
+
if (run("database")) {
|
|
2422
|
+
await setupDatabase(appDir, pgResult);
|
|
2423
|
+
}
|
|
2424
|
+
} else {
|
|
2425
|
+
if (run("env")) {
|
|
2426
|
+
const url = process.env.DATABASE_URL || "postgresql://localhost:5432/hissuno";
|
|
2427
|
+
const envResult = await configureEnv(appDir, url, envFile);
|
|
2428
|
+
appUrl = envResult.appUrl;
|
|
2429
|
+
}
|
|
2430
|
+
if (run("database")) {
|
|
2431
|
+
const url = resolveDatabaseUrl(appDir, envFile);
|
|
2432
|
+
if (!url) {
|
|
2433
|
+
throw new Error(
|
|
2434
|
+
"No DATABASE_URL found. Either run the postgres step first, set DATABASE_URL in your environment, or configure it in " + envFile
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
databaseUrl = url;
|
|
2438
|
+
await setupDatabase(appDir, { databaseUrl });
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
if (run("seed")) {
|
|
2442
|
+
const seedResult = await seedDatabase(appDir, envFile);
|
|
2443
|
+
seeded = seedResult.seeded;
|
|
2444
|
+
apiKey = seedResult.apiKey;
|
|
2445
|
+
}
|
|
2446
|
+
if (run("config") && apiKey) {
|
|
2447
|
+
await createConfig(apiKey, appUrl);
|
|
2448
|
+
}
|
|
2449
|
+
if (run("start")) {
|
|
2450
|
+
await startServer(appDir, seeded);
|
|
2451
|
+
}
|
|
2452
|
+
} catch (err) {
|
|
2453
|
+
log.fatal(err.message);
|
|
2454
|
+
process.exit(1);
|
|
2455
|
+
}
|
|
2456
|
+
});
|
|
2457
|
+
|
|
2458
|
+
// src/commands/config.ts
|
|
2459
|
+
import { Command as Command9 } from "commander";
|
|
2460
|
+
import { input as input7, password as password4, confirm as confirm8, select as select6 } from "@inquirer/prompts";
|
|
2461
|
+
var BOLD3 = "\x1B[1m";
|
|
2462
|
+
var DIM3 = "\x1B[2m";
|
|
2463
|
+
var RESET3 = "\x1B[0m";
|
|
2464
|
+
var CYAN3 = "\x1B[36m";
|
|
2465
|
+
async function runConfigWizard() {
|
|
2466
|
+
console.log(`
|
|
2467
|
+
${BOLD3}Step 1: Authentication${RESET3}`);
|
|
2468
|
+
const apiKey = await password4({
|
|
2469
|
+
message: "API key (hiss_...):",
|
|
2470
|
+
mask: "*",
|
|
2471
|
+
validate: (val) => {
|
|
2472
|
+
if (!val.startsWith("hiss_")) return "API key must start with hiss_";
|
|
2473
|
+
if (val.length < 10) return "API key is too short";
|
|
2474
|
+
return true;
|
|
2475
|
+
}
|
|
2476
|
+
});
|
|
2477
|
+
const baseUrl = await input7({
|
|
2478
|
+
message: "Hissuno URL:",
|
|
2479
|
+
default: "http://localhost:3000",
|
|
2480
|
+
validate: (val) => {
|
|
2481
|
+
try {
|
|
2482
|
+
new URL(val);
|
|
2483
|
+
return true;
|
|
2484
|
+
} catch {
|
|
2485
|
+
return "Must be a valid URL";
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
});
|
|
2489
|
+
const normalizedUrl = baseUrl.replace(/\/+$/, "");
|
|
2490
|
+
const config = { api_key: apiKey, base_url: normalizedUrl };
|
|
2491
|
+
process.stdout.write("Validating... ");
|
|
2492
|
+
try {
|
|
2493
|
+
const result = await apiCall(config, "GET", "/api/projects");
|
|
2494
|
+
const projects = Array.isArray(result.data) ? result.data : result.data?.projects;
|
|
2495
|
+
if (!result.ok || !Array.isArray(projects) || projects.length === 0) {
|
|
2496
|
+
const data = result.data;
|
|
2497
|
+
console.log("");
|
|
2498
|
+
error(`Validation failed: ${data.error || `HTTP ${result.status}`}`);
|
|
2499
|
+
process.exit(1);
|
|
2500
|
+
}
|
|
2501
|
+
const projectName = projects[0].name;
|
|
2502
|
+
const projectId = projects[0].id;
|
|
2503
|
+
success("Authenticated.");
|
|
2504
|
+
console.log(`
|
|
2505
|
+
${BOLD3}Step 2: Project${RESET3}`);
|
|
2506
|
+
console.log(`Using project: ${BOLD3}${projectName}${RESET3} (${DIM3}${projectId}${RESET3})`);
|
|
2507
|
+
config.project_id = projectId;
|
|
2508
|
+
return config;
|
|
2509
|
+
} catch (err) {
|
|
2510
|
+
console.log("");
|
|
2511
|
+
error(`Connection failed: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
2512
|
+
process.exit(1);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
var configCommand = new Command9("config").description("Configure CLI connection (API key, URL, project)").action(async () => {
|
|
2516
|
+
console.log(`
|
|
2517
|
+
${BOLD3}${CYAN3}Hissuno Config${RESET3}`);
|
|
2518
|
+
console.log(`${DIM3}Connect the CLI to your Hissuno instance.${RESET3}
|
|
2519
|
+
`);
|
|
2520
|
+
const existing = loadConfig();
|
|
2521
|
+
if (existing) {
|
|
2522
|
+
const reconfigure = await confirm8({
|
|
2523
|
+
message: "Existing configuration found. Reconfigure?",
|
|
2524
|
+
default: false
|
|
2525
|
+
});
|
|
2526
|
+
if (!reconfigure) {
|
|
2527
|
+
console.log("Keeping existing configuration.");
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
const fullConfig = await runConfigWizard();
|
|
2532
|
+
saveConfig(fullConfig);
|
|
2533
|
+
success("Configuration saved to ~/.hissuno/config.json");
|
|
2534
|
+
console.log(`
|
|
2535
|
+
${BOLD3}Step 3: Connect Data Sources (optional)${RESET3}`);
|
|
2536
|
+
let connectMore = await confirm8({
|
|
2537
|
+
message: "Would you like to connect a data source?",
|
|
2538
|
+
default: true
|
|
2539
|
+
});
|
|
2540
|
+
while (connectMore) {
|
|
2541
|
+
const platform2 = await select6({
|
|
2542
|
+
message: "Select a platform:",
|
|
2543
|
+
choices: PLATFORMS.map((p) => ({
|
|
2544
|
+
value: p,
|
|
2545
|
+
name: PLATFORM_LABELS[p]
|
|
2546
|
+
}))
|
|
2547
|
+
});
|
|
2548
|
+
if (OAUTH_PLATFORMS.includes(platform2)) {
|
|
2549
|
+
await connectOAuth(fullConfig, platform2, fullConfig.project_id);
|
|
2550
|
+
} else if (platform2 === "gong") {
|
|
2551
|
+
await connectGong(fullConfig, fullConfig.project_id, {});
|
|
2552
|
+
} else if (platform2 === "zendesk") {
|
|
2553
|
+
await connectZendesk(fullConfig, fullConfig.project_id, {});
|
|
2554
|
+
} else if (platform2 === "intercom") {
|
|
2555
|
+
await connectIntercom(fullConfig, fullConfig.project_id, {});
|
|
2556
|
+
}
|
|
2557
|
+
connectMore = await confirm8({
|
|
2558
|
+
message: "Connect another?",
|
|
2559
|
+
default: false
|
|
2560
|
+
});
|
|
2561
|
+
}
|
|
2562
|
+
console.log("");
|
|
2563
|
+
success("Configuration complete!");
|
|
2564
|
+
console.log(`
|
|
2565
|
+
${DIM3}Next steps:${RESET3}
|
|
2566
|
+
hissuno status Check connection health
|
|
2567
|
+
hissuno search "checkout bugs" Search your intelligence
|
|
2568
|
+
hissuno list feedback List feedback sessions
|
|
2569
|
+
hissuno integrate Manage integrations
|
|
2570
|
+
`);
|
|
2571
|
+
});
|
|
2572
|
+
function maskApiKey(key) {
|
|
2573
|
+
if (key.length <= 8) return key;
|
|
2574
|
+
return key.slice(0, 8) + "*".repeat(12);
|
|
2575
|
+
}
|
|
2576
|
+
configCommand.command("show").description("Display current configuration").action(async (_, cmd) => {
|
|
2577
|
+
const json = cmd.parent?.parent?.opts().json;
|
|
2578
|
+
const config = loadConfig();
|
|
2579
|
+
if (!config) {
|
|
2580
|
+
if (json) {
|
|
2581
|
+
console.log(renderJson({ error: "Not configured" }));
|
|
2582
|
+
} else {
|
|
2583
|
+
console.log("Not configured. Run `hissuno config` to set up.");
|
|
2584
|
+
}
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
const profileName = getActiveProfileName();
|
|
2588
|
+
if (json) {
|
|
2589
|
+
console.log(renderJson({
|
|
2590
|
+
profile: profileName,
|
|
2591
|
+
api_key: maskApiKey(config.api_key),
|
|
2592
|
+
base_url: config.base_url,
|
|
2593
|
+
project_id: config.project_id ?? null
|
|
2594
|
+
}));
|
|
2595
|
+
return;
|
|
2596
|
+
}
|
|
2597
|
+
console.log(`
|
|
2598
|
+
${BOLD3}${CYAN3}Hissuno Configuration${RESET3}
|
|
2599
|
+
`);
|
|
2600
|
+
console.log(` ${DIM3}Profile:${RESET3} ${profileName}`);
|
|
2601
|
+
console.log(` ${DIM3}API Key:${RESET3} ${maskApiKey(config.api_key)}`);
|
|
2602
|
+
console.log(` ${DIM3}URL:${RESET3} ${config.base_url}`);
|
|
2603
|
+
if (config.project_id) {
|
|
2604
|
+
console.log(` ${DIM3}Project:${RESET3} ${config.project_id}`);
|
|
2605
|
+
}
|
|
2606
|
+
console.log();
|
|
2607
|
+
});
|
|
2608
|
+
|
|
2609
|
+
// src/commands/profile.ts
|
|
2610
|
+
import { Command as Command10 } from "commander";
|
|
2611
|
+
function getJson(cmd) {
|
|
2612
|
+
return cmd.parent?.parent?.opts().json ?? false;
|
|
2613
|
+
}
|
|
2614
|
+
var profileCommand = new Command10("profile").description("Manage configuration profiles");
|
|
2615
|
+
profileCommand.command("list").description("List all profiles").action((_, cmd) => {
|
|
2616
|
+
const json = getJson(cmd);
|
|
2617
|
+
const full = migrateToMultiProfile();
|
|
2618
|
+
const names = Object.keys(full.profiles);
|
|
2619
|
+
if (json) {
|
|
2620
|
+
console.log(renderJson({
|
|
2621
|
+
active: full.active_profile,
|
|
2622
|
+
profiles: names
|
|
2623
|
+
}));
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
2626
|
+
console.log(`
|
|
2627
|
+
${BOLD}${CYAN}Profiles${RESET}
|
|
2628
|
+
`);
|
|
2629
|
+
if (names.length === 0) {
|
|
2630
|
+
console.log(` ${DIM}No profiles configured. Run \`hissuno profile create <name>\` to create one.${RESET}
|
|
2631
|
+
`);
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
for (const name of names) {
|
|
2635
|
+
const marker = name === full.active_profile ? `${GREEN}* ${RESET}` : " ";
|
|
2636
|
+
const profile = full.profiles[name];
|
|
2637
|
+
const url = profile.base_url ? `${DIM}${profile.base_url}${RESET}` : "";
|
|
2638
|
+
console.log(` ${marker}${BOLD}${name}${RESET} ${url}`);
|
|
2639
|
+
}
|
|
2640
|
+
console.log();
|
|
2641
|
+
});
|
|
2642
|
+
profileCommand.command("use <name>").description("Switch active profile").action((name, _, cmd) => {
|
|
2643
|
+
const json = getJson(cmd);
|
|
2644
|
+
migrateToMultiProfile();
|
|
2645
|
+
try {
|
|
2646
|
+
setActiveProfile(name);
|
|
2647
|
+
} catch (err) {
|
|
2648
|
+
if (json) {
|
|
2649
|
+
console.log(renderJson({ error: err.message }));
|
|
2650
|
+
} else {
|
|
2651
|
+
error(err.message);
|
|
2652
|
+
}
|
|
2653
|
+
process.exit(1);
|
|
2654
|
+
}
|
|
2655
|
+
if (json) {
|
|
2656
|
+
console.log(renderJson({ active: name }));
|
|
2657
|
+
} else {
|
|
2658
|
+
success(`Switched to profile "${name}".`);
|
|
2659
|
+
}
|
|
2660
|
+
});
|
|
2661
|
+
profileCommand.command("create <name>").description("Create a new profile via config wizard").action(async (name, _, cmd) => {
|
|
2662
|
+
const json = getJson(cmd);
|
|
2663
|
+
migrateToMultiProfile();
|
|
2664
|
+
console.log(`
|
|
2665
|
+
${BOLD}${CYAN}Create Profile: ${name}${RESET}`);
|
|
2666
|
+
console.log(`${DIM}Configure connection for this profile.${RESET}
|
|
2667
|
+
`);
|
|
2668
|
+
const config = await runConfigWizard();
|
|
2669
|
+
try {
|
|
2670
|
+
createProfile(name, config);
|
|
2671
|
+
} catch (err) {
|
|
2672
|
+
if (json) {
|
|
2673
|
+
console.log(renderJson({ error: err.message }));
|
|
2674
|
+
} else {
|
|
2675
|
+
error(err.message);
|
|
2676
|
+
}
|
|
2677
|
+
process.exit(1);
|
|
2678
|
+
}
|
|
2679
|
+
if (json) {
|
|
2680
|
+
console.log(renderJson({ created: name }));
|
|
2681
|
+
} else {
|
|
2682
|
+
success(`Profile "${name}" created.`);
|
|
2683
|
+
console.log(`${DIM}Switch to it with: hissuno profile use ${name}${RESET}
|
|
2684
|
+
`);
|
|
2685
|
+
}
|
|
2686
|
+
});
|
|
2687
|
+
profileCommand.command("delete <name>").description("Remove a profile").action((name, _, cmd) => {
|
|
2688
|
+
const json = getJson(cmd);
|
|
2689
|
+
migrateToMultiProfile();
|
|
2690
|
+
try {
|
|
2691
|
+
deleteProfile(name);
|
|
2692
|
+
} catch (err) {
|
|
2693
|
+
if (json) {
|
|
2694
|
+
console.log(renderJson({ error: err.message }));
|
|
2695
|
+
} else {
|
|
2696
|
+
error(err.message);
|
|
2697
|
+
}
|
|
2698
|
+
process.exit(1);
|
|
2699
|
+
}
|
|
2700
|
+
if (json) {
|
|
2701
|
+
console.log(renderJson({ deleted: name }));
|
|
2702
|
+
} else {
|
|
2703
|
+
success(`Profile "${name}" deleted.`);
|
|
2704
|
+
}
|
|
2705
|
+
});
|
|
2706
|
+
|
|
2707
|
+
// src/commands/skills.ts
|
|
2708
|
+
import { Command as Command11 } from "commander";
|
|
2709
|
+
import { existsSync as existsSync2, cpSync, rmSync, readdirSync, mkdirSync as mkdirSync2 } from "fs";
|
|
2710
|
+
import { homedir as homedir2 } from "os";
|
|
2711
|
+
import { join as join2, dirname } from "path";
|
|
2712
|
+
import { fileURLToPath } from "url";
|
|
2713
|
+
import { confirm as confirm9 } from "@inquirer/prompts";
|
|
2714
|
+
var CLAUDE_SKILLS_DIR = join2(homedir2(), ".claude", "skills", "hissuno");
|
|
2715
|
+
var CURSOR_SKILLS_DIR = join2(homedir2(), ".cursor", "skills", "hissuno");
|
|
2716
|
+
function getJson2(cmd) {
|
|
2717
|
+
return cmd.parent?.parent?.opts().json ?? false;
|
|
2718
|
+
}
|
|
2719
|
+
function getBundledSkillsPath() {
|
|
2720
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
2721
|
+
const prodPath = join2(thisDir, "..", "..", "skills");
|
|
2722
|
+
if (existsSync2(join2(prodPath, "SKILL.md"))) return prodPath;
|
|
2723
|
+
const devPath = join2(thisDir, "..", "..", "..", "skills", "hissuno");
|
|
2724
|
+
if (existsSync2(join2(devPath, "SKILL.md"))) return devPath;
|
|
2725
|
+
throw new Error("Bundled skills not found. Package may be corrupted.");
|
|
2726
|
+
}
|
|
2727
|
+
function countFiles(dir) {
|
|
2728
|
+
if (!existsSync2(dir)) return 0;
|
|
2729
|
+
let count = 0;
|
|
2730
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2731
|
+
if (entry.isFile()) count++;
|
|
2732
|
+
else if (entry.isDirectory()) count += countFiles(join2(dir, entry.name));
|
|
2733
|
+
}
|
|
2734
|
+
return count;
|
|
2735
|
+
}
|
|
2736
|
+
function getLocationInfo(name, path7) {
|
|
2737
|
+
const installed = existsSync2(path7) && existsSync2(join2(path7, "SKILL.md"));
|
|
2738
|
+
return { name, path: path7, installed, fileCount: installed ? countFiles(path7) : 0 };
|
|
2739
|
+
}
|
|
2740
|
+
function resolveTargetDir(opts) {
|
|
2741
|
+
if (opts.path) return opts.path;
|
|
2742
|
+
if (opts.cursor) return CURSOR_SKILLS_DIR;
|
|
2743
|
+
return CLAUDE_SKILLS_DIR;
|
|
2744
|
+
}
|
|
2745
|
+
var skillsCommand = new Command11("skills").description("Install Hissuno skills into agent environments");
|
|
2746
|
+
skillsCommand.command("install").description("Install Hissuno skills").option("--cursor", "Install to Cursor skills directory").option("--path <dir>", "Install to a custom directory").option("--force", "Overwrite without prompting").action(async (opts, cmd) => {
|
|
2747
|
+
const json = getJson2(cmd);
|
|
2748
|
+
if (opts.cursor && opts.path) {
|
|
2749
|
+
if (json) {
|
|
2750
|
+
console.log(renderJson({ error: "--cursor and --path are mutually exclusive" }));
|
|
2751
|
+
} else {
|
|
2752
|
+
error("--cursor and --path are mutually exclusive.");
|
|
2753
|
+
}
|
|
2754
|
+
process.exit(1);
|
|
2755
|
+
}
|
|
2756
|
+
let srcDir;
|
|
2757
|
+
try {
|
|
2758
|
+
srcDir = getBundledSkillsPath();
|
|
2759
|
+
} catch (err) {
|
|
2760
|
+
if (json) {
|
|
2761
|
+
console.log(renderJson({ error: err.message }));
|
|
2762
|
+
} else {
|
|
2763
|
+
error(err.message);
|
|
2764
|
+
}
|
|
2765
|
+
process.exit(1);
|
|
2766
|
+
}
|
|
2767
|
+
const destDir = resolveTargetDir(opts);
|
|
2768
|
+
if (existsSync2(destDir) && existsSync2(join2(destDir, "SKILL.md")) && !opts.force) {
|
|
2769
|
+
const overwrite = await confirm9({
|
|
2770
|
+
message: `Skills already installed at ${destDir}. Overwrite?`,
|
|
2771
|
+
default: false
|
|
2772
|
+
});
|
|
2773
|
+
if (!overwrite) {
|
|
2774
|
+
console.log("Skipped.");
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
const parentDir = dirname(destDir);
|
|
2779
|
+
if (!existsSync2(parentDir)) {
|
|
2780
|
+
mkdirSync2(parentDir, { recursive: true });
|
|
2781
|
+
}
|
|
2782
|
+
if (existsSync2(destDir)) {
|
|
2783
|
+
rmSync(destDir, { recursive: true });
|
|
2784
|
+
}
|
|
2785
|
+
cpSync(srcDir, destDir, { recursive: true });
|
|
2786
|
+
const fileCount = countFiles(destDir);
|
|
2787
|
+
if (json) {
|
|
2788
|
+
console.log(renderJson({ installed: true, path: destDir, files: fileCount }));
|
|
2789
|
+
} else {
|
|
2790
|
+
success(`Skills installed to ${destDir} (${fileCount} files)`);
|
|
2791
|
+
}
|
|
2792
|
+
});
|
|
2793
|
+
skillsCommand.command("uninstall").description("Remove installed skills").option("--cursor", "Uninstall from Cursor skills directory").option("--path <dir>", "Uninstall from a custom directory").action((opts, cmd) => {
|
|
2794
|
+
const json = getJson2(cmd);
|
|
2795
|
+
if (opts.cursor && opts.path) {
|
|
2796
|
+
if (json) {
|
|
2797
|
+
console.log(renderJson({ error: "--cursor and --path are mutually exclusive" }));
|
|
2798
|
+
} else {
|
|
2799
|
+
error("--cursor and --path are mutually exclusive.");
|
|
2800
|
+
}
|
|
2801
|
+
process.exit(1);
|
|
2802
|
+
}
|
|
2803
|
+
const destDir = resolveTargetDir(opts);
|
|
2804
|
+
if (!existsSync2(destDir)) {
|
|
2805
|
+
if (json) {
|
|
2806
|
+
console.log(renderJson({ error: "Skills not installed at this location" }));
|
|
2807
|
+
} else {
|
|
2808
|
+
warn(`Skills not installed at ${destDir}`);
|
|
2809
|
+
}
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
rmSync(destDir, { recursive: true });
|
|
2813
|
+
if (json) {
|
|
2814
|
+
console.log(renderJson({ uninstalled: true, path: destDir }));
|
|
2815
|
+
} else {
|
|
2816
|
+
success(`Skills removed from ${destDir}`);
|
|
2817
|
+
}
|
|
2818
|
+
});
|
|
2819
|
+
skillsCommand.command("status").description("Check skills installation status").action((_, cmd) => {
|
|
2820
|
+
const json = getJson2(cmd);
|
|
2821
|
+
const locations = [
|
|
2822
|
+
getLocationInfo("Claude Code", CLAUDE_SKILLS_DIR),
|
|
2823
|
+
getLocationInfo("Cursor", CURSOR_SKILLS_DIR)
|
|
2824
|
+
];
|
|
2825
|
+
if (json) {
|
|
2826
|
+
console.log(renderJson(locations.map((l) => ({
|
|
2827
|
+
name: l.name,
|
|
2828
|
+
path: l.path,
|
|
2829
|
+
installed: l.installed,
|
|
2830
|
+
files: l.fileCount
|
|
2831
|
+
}))));
|
|
2832
|
+
return;
|
|
2833
|
+
}
|
|
2834
|
+
console.log(`
|
|
2835
|
+
${BOLD}${CYAN}Skills Installation${RESET}
|
|
2836
|
+
`);
|
|
2837
|
+
for (const loc of locations) {
|
|
2838
|
+
const status = loc.installed ? `${GREEN}Installed${RESET} ${loc.path} (${loc.fileCount} files)` : `${DIM}Not found${RESET}`;
|
|
2839
|
+
console.log(` ${BOLD}${loc.name.padEnd(12)}${RESET} ${status}`);
|
|
2840
|
+
}
|
|
2841
|
+
console.log();
|
|
2842
|
+
});
|
|
2843
|
+
|
|
2844
|
+
// src/commands/status.ts
|
|
2845
|
+
import { Command as Command12 } from "commander";
|
|
2846
|
+
var BOLD4 = "\x1B[1m";
|
|
2847
|
+
var DIM4 = "\x1B[2m";
|
|
2848
|
+
var RESET4 = "\x1B[0m";
|
|
2849
|
+
var GREEN3 = "\x1B[32m";
|
|
2850
|
+
var RED3 = "\x1B[31m";
|
|
2851
|
+
var CYAN4 = "\x1B[36m";
|
|
2852
|
+
var YELLOW3 = "\x1B[33m";
|
|
2853
|
+
var statusCommand = new Command12("status").description("Check connection health").action(async () => {
|
|
2854
|
+
const config = loadConfig();
|
|
2855
|
+
if (!config) {
|
|
2856
|
+
console.log(`
|
|
2857
|
+
${RED3}Not configured${RESET4}`);
|
|
2858
|
+
console.log(` ${DIM4}Run \`hissuno config\` to set up your API key and URL.${RESET4}
|
|
2859
|
+
`);
|
|
2860
|
+
process.exit(1);
|
|
2861
|
+
}
|
|
2862
|
+
console.log(`
|
|
2863
|
+
${BOLD4}${CYAN4}Hissuno Status${RESET4}
|
|
2864
|
+
`);
|
|
2865
|
+
console.log(` ${DIM4}URL:${RESET4} ${config.base_url}`);
|
|
2866
|
+
console.log(` ${DIM4}API Key:${RESET4} ${config.api_key.slice(0, 8)}${"*".repeat(12)}`);
|
|
2867
|
+
if (config.project_id) {
|
|
2868
|
+
console.log(` ${DIM4}Project:${RESET4} ${config.project_id}`);
|
|
2869
|
+
}
|
|
2870
|
+
process.stdout.write(`
|
|
2871
|
+
Checking connection... `);
|
|
2872
|
+
try {
|
|
2873
|
+
const result = await apiCall(config, "GET", "/api/projects");
|
|
2874
|
+
if (!result.ok) {
|
|
2875
|
+
console.log(`${RED3}failed${RESET4}`);
|
|
2876
|
+
console.log(` ${RED3}HTTP ${result.status}${RESET4}`);
|
|
2877
|
+
if (result.status === 401) {
|
|
2878
|
+
console.log(` ${DIM4}Your API key may be invalid or expired. Run \`hissuno config\` to reconfigure.${RESET4}`);
|
|
2879
|
+
}
|
|
2880
|
+
console.log();
|
|
2881
|
+
process.exit(1);
|
|
2882
|
+
}
|
|
2883
|
+
console.log(`${GREEN3}connected${RESET4}`);
|
|
2884
|
+
const projects = Array.isArray(result.data) ? result.data : result.data?.projects;
|
|
2885
|
+
if (Array.isArray(projects) && projects.length > 0) {
|
|
2886
|
+
const project = projects[0];
|
|
2887
|
+
console.log(` ${DIM4}Project:${RESET4} ${project.name} (${project.id})`);
|
|
2888
|
+
}
|
|
2889
|
+
console.log();
|
|
2890
|
+
} catch (err) {
|
|
2891
|
+
console.log(`${RED3}failed${RESET4}`);
|
|
2892
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
2893
|
+
if (message.includes("ECONNREFUSED")) {
|
|
2894
|
+
console.log(` ${YELLOW3}Server not reachable at ${config.base_url}${RESET4}`);
|
|
2895
|
+
console.log(` ${DIM4}Is your Hissuno instance running?${RESET4}`);
|
|
2896
|
+
} else if (message.includes("ENOTFOUND")) {
|
|
2897
|
+
console.log(` ${YELLOW3}Host not found: ${config.base_url}${RESET4}`);
|
|
2898
|
+
console.log(` ${DIM4}Check the URL in your config. Run \`hissuno config\` to update.${RESET4}`);
|
|
2899
|
+
} else {
|
|
2900
|
+
console.log(` ${RED3}${message}${RESET4}`);
|
|
2901
|
+
}
|
|
2902
|
+
console.log();
|
|
2903
|
+
process.exit(1);
|
|
2904
|
+
}
|
|
2905
|
+
});
|
|
2906
|
+
|
|
2907
|
+
// src/index.ts
|
|
2908
|
+
var program = new Command13().name("hissuno").description("Hissuno CLI - set up, configure, and query your product intelligence data").version("0.2.0").option("--json", "Output as JSON");
|
|
2909
|
+
program.addCommand(setupCommand);
|
|
2910
|
+
program.addCommand(configCommand);
|
|
2911
|
+
program.addCommand(profileCommand);
|
|
2912
|
+
program.addCommand(skillsCommand);
|
|
2913
|
+
program.addCommand(statusCommand);
|
|
2914
|
+
program.addCommand(typesCommand);
|
|
2915
|
+
program.addCommand(listCommand);
|
|
2916
|
+
program.addCommand(getCommand);
|
|
2917
|
+
program.addCommand(searchCommand);
|
|
2918
|
+
program.addCommand(addCommand);
|
|
2919
|
+
program.addCommand(updateCommand);
|
|
2920
|
+
program.addCommand(integrateCommand);
|
|
2921
|
+
program.parseAsync(process.argv);
|