@agent-crm/cli 0.0.1
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 +108 -0
- package/dist/bin/acrm.js +49 -0
- package/dist/bin/acrm.js.map +1 -0
- package/dist/commands/execute.js +75 -0
- package/dist/commands/execute.js.map +1 -0
- package/dist/commands/import.js +451 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/init.js +97 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/ui.js +513 -0
- package/dist/commands/ui.js.map +1 -0
- package/dist/db/execute.js +30 -0
- package/dist/db/execute.js.map +1 -0
- package/dist/domain/values.js +104 -0
- package/dist/domain/values.js.map +1 -0
- package/dist/lib/errors.js +25 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/ids.js +8 -0
- package/dist/lib/ids.js.map +1 -0
- package/dist/lib/time.js +4 -0
- package/dist/lib/time.js.map +1 -0
- package/dist/output/json.js +44 -0
- package/dist/output/json.js.map +1 -0
- package/dist/workspace/open.js +60 -0
- package/dist/workspace/open.js.map +1 -0
- package/dist/workspace/schemas/index.js +124 -0
- package/dist/workspace/schemas/index.js.map +1 -0
- package/package.json +31 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { findWorkspace, openWorkspace } from "../workspace/open.js";
|
|
5
|
+
import { exec } from "../db/execute.js";
|
|
6
|
+
import { fail, setJsonMode } from "../output/json.js";
|
|
7
|
+
import { AcrmError, ERR } from "../lib/errors.js";
|
|
8
|
+
async function loadPeople(lix) {
|
|
9
|
+
const r = await exec(lix, `SELECT
|
|
10
|
+
p.record_id AS id,
|
|
11
|
+
v_name.value_json AS name_json,
|
|
12
|
+
v_role.value_json AS role_json,
|
|
13
|
+
v_li.value_json AS li_json,
|
|
14
|
+
v_ref.ref_record_id AS company_id
|
|
15
|
+
FROM acrm_record p
|
|
16
|
+
LEFT JOIN acrm_value v_name
|
|
17
|
+
ON v_name.record_id = p.record_id
|
|
18
|
+
AND v_name.attribute_slug = 'name'
|
|
19
|
+
AND v_name.active_until IS NULL
|
|
20
|
+
LEFT JOIN acrm_value v_role
|
|
21
|
+
ON v_role.record_id = p.record_id
|
|
22
|
+
AND v_role.attribute_slug = 'job_title'
|
|
23
|
+
AND v_role.active_until IS NULL
|
|
24
|
+
LEFT JOIN acrm_value v_li
|
|
25
|
+
ON v_li.record_id = p.record_id
|
|
26
|
+
AND v_li.attribute_slug = 'linkedin_url'
|
|
27
|
+
AND v_li.active_until IS NULL
|
|
28
|
+
LEFT JOIN acrm_value v_ref
|
|
29
|
+
ON v_ref.record_id = p.record_id
|
|
30
|
+
AND v_ref.attribute_slug = 'company'
|
|
31
|
+
AND v_ref.active_until IS NULL
|
|
32
|
+
WHERE p.object_slug = 'people'`);
|
|
33
|
+
const out = r.rows.map((row) => {
|
|
34
|
+
const nameObj = parseJson(row.name_json);
|
|
35
|
+
const roleObj = parseJson(row.role_json);
|
|
36
|
+
const liObj = parseJson(row.li_json);
|
|
37
|
+
return {
|
|
38
|
+
id: row.id,
|
|
39
|
+
name: nameObj?.full_name ?? null,
|
|
40
|
+
job_title: roleObj?.value ?? null,
|
|
41
|
+
linkedin_url: liObj?.value ?? null,
|
|
42
|
+
company_id: row.company_id ?? null,
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
out.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
async function loadCompanies(lix) {
|
|
49
|
+
const r = await exec(lix, `SELECT
|
|
50
|
+
c.record_id AS id,
|
|
51
|
+
v_name.value_json AS name_json,
|
|
52
|
+
v_desc.value_json AS desc_json
|
|
53
|
+
FROM acrm_record c
|
|
54
|
+
LEFT JOIN acrm_value v_name
|
|
55
|
+
ON v_name.record_id = c.record_id
|
|
56
|
+
AND v_name.attribute_slug = 'name'
|
|
57
|
+
AND v_name.active_until IS NULL
|
|
58
|
+
LEFT JOIN acrm_value v_desc
|
|
59
|
+
ON v_desc.record_id = c.record_id
|
|
60
|
+
AND v_desc.attribute_slug = 'description'
|
|
61
|
+
AND v_desc.active_until IS NULL
|
|
62
|
+
WHERE c.object_slug = 'companies'`);
|
|
63
|
+
const out = r.rows.map((row) => {
|
|
64
|
+
const nameObj = parseJson(row.name_json);
|
|
65
|
+
const descObj = parseJson(row.desc_json);
|
|
66
|
+
return {
|
|
67
|
+
id: row.id,
|
|
68
|
+
name: nameObj?.value ?? null,
|
|
69
|
+
description: descObj?.value ?? null,
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
out.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
async function loadCounts(lix) {
|
|
76
|
+
const r = await exec(lix, `SELECT object_slug, COUNT(*) AS n FROM acrm_record GROUP BY object_slug`);
|
|
77
|
+
const counts = { people: 0, companies: 0, deals: 0 };
|
|
78
|
+
for (const row of r.rows) {
|
|
79
|
+
const slug = row.object_slug;
|
|
80
|
+
const n = Number(row.n);
|
|
81
|
+
if (slug === "people")
|
|
82
|
+
counts.people = n;
|
|
83
|
+
else if (slug === "companies")
|
|
84
|
+
counts.companies = n;
|
|
85
|
+
else if (slug === "deals")
|
|
86
|
+
counts.deals = n;
|
|
87
|
+
}
|
|
88
|
+
return counts;
|
|
89
|
+
}
|
|
90
|
+
function parseJson(v) {
|
|
91
|
+
if (typeof v !== "string" || !v.length)
|
|
92
|
+
return null;
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(v);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function escapeHtml(s) {
|
|
101
|
+
return s
|
|
102
|
+
.replace(/&/g, "&")
|
|
103
|
+
.replace(/</g, "<")
|
|
104
|
+
.replace(/>/g, ">")
|
|
105
|
+
.replace(/"/g, """)
|
|
106
|
+
.replace(/'/g, "'");
|
|
107
|
+
}
|
|
108
|
+
function avatarColor(seed) {
|
|
109
|
+
let hash = 0;
|
|
110
|
+
for (let i = 0; i < seed.length; i++)
|
|
111
|
+
hash = (hash * 31 + seed.charCodeAt(i)) | 0;
|
|
112
|
+
const hue = Math.abs(hash) % 360;
|
|
113
|
+
return `hsl(${hue}, 38%, 32%)`;
|
|
114
|
+
}
|
|
115
|
+
function initials(name) {
|
|
116
|
+
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
117
|
+
if (parts.length === 0)
|
|
118
|
+
return "?";
|
|
119
|
+
if (parts.length === 1)
|
|
120
|
+
return parts[0].slice(0, 2).toUpperCase();
|
|
121
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
122
|
+
}
|
|
123
|
+
const STYLES = `
|
|
124
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
125
|
+
html, body { margin: 0; padding: 0; height: 100%; }
|
|
126
|
+
body {
|
|
127
|
+
font-family: "Inter", -apple-system, BlinkMacSystemFont, "SF Pro Display", system-ui, sans-serif;
|
|
128
|
+
font-size: 13px;
|
|
129
|
+
line-height: 1.45;
|
|
130
|
+
background: #0a0a0a;
|
|
131
|
+
color: #e6e6e6;
|
|
132
|
+
-webkit-font-smoothing: antialiased;
|
|
133
|
+
-moz-osx-font-smoothing: grayscale;
|
|
134
|
+
letter-spacing: -0.005em;
|
|
135
|
+
}
|
|
136
|
+
.app { display: grid; grid-template-columns: 220px 1fr; height: 100vh; }
|
|
137
|
+
.sidebar {
|
|
138
|
+
background: #0a0a0a;
|
|
139
|
+
border-right: 1px solid rgba(255,255,255,0.06);
|
|
140
|
+
padding: 14px 8px;
|
|
141
|
+
overflow-y: auto;
|
|
142
|
+
}
|
|
143
|
+
.workspace {
|
|
144
|
+
padding: 4px 10px 14px;
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
gap: 8px;
|
|
148
|
+
font-size: 13px;
|
|
149
|
+
font-weight: 600;
|
|
150
|
+
color: #ededed;
|
|
151
|
+
}
|
|
152
|
+
.workspace-icon {
|
|
153
|
+
width: 18px; height: 18px;
|
|
154
|
+
border-radius: 5px;
|
|
155
|
+
background: linear-gradient(135deg, #5e6ad2 0%, #4a8af4 100%);
|
|
156
|
+
flex: none;
|
|
157
|
+
}
|
|
158
|
+
.nav-section { margin-top: 10px; }
|
|
159
|
+
.nav-label {
|
|
160
|
+
font-size: 11px;
|
|
161
|
+
color: #6a6a6a;
|
|
162
|
+
padding: 4px 10px 6px;
|
|
163
|
+
font-weight: 500;
|
|
164
|
+
}
|
|
165
|
+
.nav-item {
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
justify-content: space-between;
|
|
169
|
+
padding: 5px 10px;
|
|
170
|
+
border-radius: 5px;
|
|
171
|
+
color: #c0c0c0;
|
|
172
|
+
text-decoration: none;
|
|
173
|
+
font-size: 13px;
|
|
174
|
+
}
|
|
175
|
+
.nav-item:hover { background: rgba(255,255,255,0.04); color: #e6e6e6; }
|
|
176
|
+
.nav-item.active { background: rgba(255,255,255,0.06); color: #fff; }
|
|
177
|
+
.nav-item .left { display: flex; align-items: center; gap: 8px; }
|
|
178
|
+
.nav-item .icon { width: 14px; height: 14px; opacity: 0.75; flex: none; }
|
|
179
|
+
.nav-item.active .icon { opacity: 1; }
|
|
180
|
+
.nav-item .count { font-size: 11px; color: #6a6a6a; font-variant-numeric: tabular-nums; }
|
|
181
|
+
.main { overflow: auto; }
|
|
182
|
+
.topbar {
|
|
183
|
+
height: 48px;
|
|
184
|
+
border-bottom: 1px solid rgba(255,255,255,0.06);
|
|
185
|
+
display: flex;
|
|
186
|
+
align-items: center;
|
|
187
|
+
padding: 0 24px;
|
|
188
|
+
gap: 10px;
|
|
189
|
+
position: sticky;
|
|
190
|
+
top: 0;
|
|
191
|
+
background: #0a0a0a;
|
|
192
|
+
z-index: 1;
|
|
193
|
+
}
|
|
194
|
+
.topbar h1 { font-size: 13px; font-weight: 600; margin: 0; letter-spacing: -0.01em; }
|
|
195
|
+
.topbar .count {
|
|
196
|
+
font-size: 12px;
|
|
197
|
+
color: #6a6a6a;
|
|
198
|
+
font-variant-numeric: tabular-nums;
|
|
199
|
+
}
|
|
200
|
+
table {
|
|
201
|
+
width: 100%;
|
|
202
|
+
border-collapse: collapse;
|
|
203
|
+
font-size: 13px;
|
|
204
|
+
}
|
|
205
|
+
thead tr { border-bottom: 1px solid rgba(255,255,255,0.06); }
|
|
206
|
+
th {
|
|
207
|
+
text-align: left;
|
|
208
|
+
font-weight: 500;
|
|
209
|
+
color: #8a8a8a;
|
|
210
|
+
font-size: 12px;
|
|
211
|
+
padding: 10px 16px;
|
|
212
|
+
background: #0a0a0a;
|
|
213
|
+
position: sticky;
|
|
214
|
+
top: 48px;
|
|
215
|
+
z-index: 1;
|
|
216
|
+
}
|
|
217
|
+
tbody tr { border-bottom: 1px solid rgba(255,255,255,0.04); }
|
|
218
|
+
tbody tr:last-child { border-bottom: none; }
|
|
219
|
+
tbody tr:hover { background: rgba(255,255,255,0.025); }
|
|
220
|
+
td {
|
|
221
|
+
padding: 10px 16px;
|
|
222
|
+
color: #d8d8d8;
|
|
223
|
+
vertical-align: middle;
|
|
224
|
+
white-space: nowrap;
|
|
225
|
+
overflow: hidden;
|
|
226
|
+
text-overflow: ellipsis;
|
|
227
|
+
max-width: 360px;
|
|
228
|
+
}
|
|
229
|
+
td.muted { color: #555; }
|
|
230
|
+
.avatar {
|
|
231
|
+
display: inline-flex;
|
|
232
|
+
align-items: center;
|
|
233
|
+
justify-content: center;
|
|
234
|
+
width: 22px; height: 22px;
|
|
235
|
+
border-radius: 50%;
|
|
236
|
+
color: rgba(255,255,255,0.95);
|
|
237
|
+
font-size: 10px;
|
|
238
|
+
font-weight: 600;
|
|
239
|
+
margin-right: 10px;
|
|
240
|
+
vertical-align: middle;
|
|
241
|
+
flex: none;
|
|
242
|
+
}
|
|
243
|
+
.name-cell { display: flex; align-items: center; min-width: 0; }
|
|
244
|
+
.name-cell span:last-child { overflow: hidden; text-overflow: ellipsis; }
|
|
245
|
+
a { color: #6e9fff; text-decoration: none; }
|
|
246
|
+
a:hover { text-decoration: underline; }
|
|
247
|
+
.mono {
|
|
248
|
+
font-family: "JetBrains Mono", "SF Mono", ui-monospace, Menlo, monospace;
|
|
249
|
+
font-size: 12px;
|
|
250
|
+
color: #9a9a9a;
|
|
251
|
+
}
|
|
252
|
+
.empty {
|
|
253
|
+
padding: 80px 40px;
|
|
254
|
+
text-align: center;
|
|
255
|
+
color: #6a6a6a;
|
|
256
|
+
}
|
|
257
|
+
.empty h2 { font-size: 14px; font-weight: 500; color: #a0a0a0; margin: 0 0 6px; }
|
|
258
|
+
.empty p { margin: 0; font-size: 12px; }
|
|
259
|
+
`;
|
|
260
|
+
const ICON_PEOPLE = `<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><circle cx="8" cy="6" r="2.5"/><path d="M3 13c0-2.5 2.2-4 5-4s5 1.5 5 4"/></svg>`;
|
|
261
|
+
const ICON_COMPANIES = `<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><rect x="3" y="3" width="10" height="10" rx="1"/><path d="M6 6h1M9 6h1M6 9h1M9 9h1"/></svg>`;
|
|
262
|
+
const ICON_DEALS = `<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M8 2l5 4-5 8-5-8z"/></svg>`;
|
|
263
|
+
function renderShell(opts) {
|
|
264
|
+
const { workspace, active, counts, body } = opts;
|
|
265
|
+
const navItem = (href, key, label, icon, count) => `<a class="nav-item ${active === key ? "active" : ""}" href="${href}">
|
|
266
|
+
<span class="left">${icon}<span>${label}</span></span>
|
|
267
|
+
<span class="count">${count}</span>
|
|
268
|
+
</a>`;
|
|
269
|
+
return `<!doctype html>
|
|
270
|
+
<html lang="en">
|
|
271
|
+
<head>
|
|
272
|
+
<meta charset="utf-8">
|
|
273
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
274
|
+
<title>${escapeHtml(workspace)} — Agent CRM</title>
|
|
275
|
+
<style>${STYLES}</style>
|
|
276
|
+
</head>
|
|
277
|
+
<body>
|
|
278
|
+
<div class="app">
|
|
279
|
+
<aside class="sidebar">
|
|
280
|
+
<div class="workspace">
|
|
281
|
+
<span class="workspace-icon"></span>
|
|
282
|
+
<span>${escapeHtml(workspace)}</span>
|
|
283
|
+
</div>
|
|
284
|
+
<div class="nav-section">
|
|
285
|
+
<div class="nav-label">Objects</div>
|
|
286
|
+
${navItem("/people", "people", "People", ICON_PEOPLE, counts.people)}
|
|
287
|
+
${navItem("/companies", "companies", "Companies", ICON_COMPANIES, counts.companies)}
|
|
288
|
+
${navItem("/deals", "deals", "Deals", ICON_DEALS, counts.deals)}
|
|
289
|
+
</div>
|
|
290
|
+
</aside>
|
|
291
|
+
<main class="main">${body}</main>
|
|
292
|
+
</div>
|
|
293
|
+
</body>
|
|
294
|
+
</html>`;
|
|
295
|
+
}
|
|
296
|
+
function renderPeoplePage(people, companyById, workspace, counts) {
|
|
297
|
+
const rows = people
|
|
298
|
+
.map((p) => {
|
|
299
|
+
const display = p.name ?? "(unnamed)";
|
|
300
|
+
const color = avatarColor(p.id);
|
|
301
|
+
const company = p.company_id && companyById.get(p.company_id)
|
|
302
|
+
? escapeHtml(companyById.get(p.company_id))
|
|
303
|
+
: `<span class="muted">—</span>`;
|
|
304
|
+
const role = p.job_title
|
|
305
|
+
? escapeHtml(p.job_title)
|
|
306
|
+
: `<span class="muted">—</span>`;
|
|
307
|
+
const linkedin = p.linkedin_url
|
|
308
|
+
? `<a class="mono" href="https://${escapeHtml(p.linkedin_url)}" target="_blank" rel="noreferrer noopener">${escapeHtml(p.linkedin_url)}</a>`
|
|
309
|
+
: `<span class="muted">—</span>`;
|
|
310
|
+
return `<tr>
|
|
311
|
+
<td><div class="name-cell"><span class="avatar" style="background:${color}">${escapeHtml(initials(display))}</span><span>${escapeHtml(display)}</span></div></td>
|
|
312
|
+
<td>${role}</td>
|
|
313
|
+
<td>${company}</td>
|
|
314
|
+
<td>${linkedin}</td>
|
|
315
|
+
</tr>`;
|
|
316
|
+
})
|
|
317
|
+
.join("");
|
|
318
|
+
const body = `
|
|
319
|
+
<div class="topbar">
|
|
320
|
+
<h1>People</h1>
|
|
321
|
+
<span class="count">${people.length}</span>
|
|
322
|
+
</div>
|
|
323
|
+
${people.length
|
|
324
|
+
? `<table>
|
|
325
|
+
<thead><tr><th>Name</th><th>Role</th><th>Company</th><th>LinkedIn</th></tr></thead>
|
|
326
|
+
<tbody>${rows}</tbody>
|
|
327
|
+
</table>`
|
|
328
|
+
: `<div class="empty"><h2>No people yet</h2><p>Run <span class="mono">acrm import csv ./leads.csv</span> to add some.</p></div>`}
|
|
329
|
+
`;
|
|
330
|
+
return renderShell({ workspace, active: "people", counts, body });
|
|
331
|
+
}
|
|
332
|
+
function renderCompaniesPage(companies, peopleByCompany, workspace, counts) {
|
|
333
|
+
const rows = companies
|
|
334
|
+
.map((c) => {
|
|
335
|
+
const display = c.name ?? "(unnamed)";
|
|
336
|
+
const color = avatarColor(c.id);
|
|
337
|
+
const desc = c.description
|
|
338
|
+
? escapeHtml(c.description)
|
|
339
|
+
: `<span class="muted">—</span>`;
|
|
340
|
+
const headcount = peopleByCompany.get(c.id) ?? 0;
|
|
341
|
+
return `<tr>
|
|
342
|
+
<td><div class="name-cell"><span class="avatar" style="background:${color}">${escapeHtml(initials(display))}</span><span>${escapeHtml(display)}</span></div></td>
|
|
343
|
+
<td>${desc}</td>
|
|
344
|
+
<td>${headcount === 0 ? `<span class="muted">0</span>` : headcount}</td>
|
|
345
|
+
</tr>`;
|
|
346
|
+
})
|
|
347
|
+
.join("");
|
|
348
|
+
const body = `
|
|
349
|
+
<div class="topbar">
|
|
350
|
+
<h1>Companies</h1>
|
|
351
|
+
<span class="count">${companies.length}</span>
|
|
352
|
+
</div>
|
|
353
|
+
${companies.length
|
|
354
|
+
? `<table>
|
|
355
|
+
<thead><tr><th>Name</th><th>Type</th><th>People</th></tr></thead>
|
|
356
|
+
<tbody>${rows}</tbody>
|
|
357
|
+
</table>`
|
|
358
|
+
: `<div class="empty"><h2>No companies yet</h2><p>Run <span class="mono">acrm import csv ./leads.csv</span> to add some.</p></div>`}
|
|
359
|
+
`;
|
|
360
|
+
return renderShell({ workspace, active: "companies", counts, body });
|
|
361
|
+
}
|
|
362
|
+
function renderDealsPage(workspace, counts) {
|
|
363
|
+
const body = `
|
|
364
|
+
<div class="topbar">
|
|
365
|
+
<h1>Deals</h1>
|
|
366
|
+
<span class="count">${counts.deals}</span>
|
|
367
|
+
</div>
|
|
368
|
+
<div class="empty">
|
|
369
|
+
<h2>No deals yet</h2>
|
|
370
|
+
<p>Deals appear here once imported or created.</p>
|
|
371
|
+
</div>
|
|
372
|
+
`;
|
|
373
|
+
return renderShell({ workspace, active: "deals", counts, body });
|
|
374
|
+
}
|
|
375
|
+
async function handleRequest(lix, workspaceLabel, req, res) {
|
|
376
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
377
|
+
const pathname = url.pathname;
|
|
378
|
+
if (pathname === "/") {
|
|
379
|
+
res.statusCode = 302;
|
|
380
|
+
res.setHeader("Location", "/people");
|
|
381
|
+
res.end();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const counts = await loadCounts(lix);
|
|
385
|
+
if (pathname === "/people") {
|
|
386
|
+
const [people, companies] = await Promise.all([
|
|
387
|
+
loadPeople(lix),
|
|
388
|
+
loadCompanies(lix),
|
|
389
|
+
]);
|
|
390
|
+
const companyById = new Map(companies.filter((c) => c.name).map((c) => [c.id, c.name]));
|
|
391
|
+
const html = renderPeoplePage(people, companyById, workspaceLabel, counts);
|
|
392
|
+
res.statusCode = 200;
|
|
393
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
394
|
+
res.end(html);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (pathname === "/companies") {
|
|
398
|
+
const [companies, people] = await Promise.all([
|
|
399
|
+
loadCompanies(lix),
|
|
400
|
+
loadPeople(lix),
|
|
401
|
+
]);
|
|
402
|
+
const peopleByCompany = new Map();
|
|
403
|
+
for (const p of people) {
|
|
404
|
+
if (!p.company_id)
|
|
405
|
+
continue;
|
|
406
|
+
peopleByCompany.set(p.company_id, (peopleByCompany.get(p.company_id) ?? 0) + 1);
|
|
407
|
+
}
|
|
408
|
+
const html = renderCompaniesPage(companies, peopleByCompany, workspaceLabel, counts);
|
|
409
|
+
res.statusCode = 200;
|
|
410
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
411
|
+
res.end(html);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (pathname === "/deals") {
|
|
415
|
+
res.statusCode = 200;
|
|
416
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
417
|
+
res.end(renderDealsPage(workspaceLabel, counts));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
res.statusCode = 404;
|
|
421
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
422
|
+
res.end("Not found");
|
|
423
|
+
}
|
|
424
|
+
function openInBrowser(url) {
|
|
425
|
+
const cmd = process.platform === "darwin"
|
|
426
|
+
? "open"
|
|
427
|
+
: process.platform === "win32"
|
|
428
|
+
? "start"
|
|
429
|
+
: "xdg-open";
|
|
430
|
+
try {
|
|
431
|
+
spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
// fine — user can click the URL
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
export function startUiServer(lix, workspaceLabel, opts) {
|
|
438
|
+
const server = createServer((req, res) => {
|
|
439
|
+
handleRequest(lix, workspaceLabel, req, res).catch((err) => {
|
|
440
|
+
res.statusCode = 500;
|
|
441
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
442
|
+
res.end(err instanceof Error ? err.message : String(err));
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
server.on("error", (err) => {
|
|
446
|
+
fail(err.message, ERR.UI);
|
|
447
|
+
process.exit(1);
|
|
448
|
+
});
|
|
449
|
+
const url = `http://localhost:${opts.port}`;
|
|
450
|
+
server.listen(opts.port, "127.0.0.1", () => {
|
|
451
|
+
process.stdout.write(`acrm ui — ${workspaceLabel}\n`);
|
|
452
|
+
process.stdout.write(` ${url}\n`);
|
|
453
|
+
process.stdout.write(` Ctrl+C to stop\n`);
|
|
454
|
+
if (opts.open)
|
|
455
|
+
openInBrowser(url);
|
|
456
|
+
});
|
|
457
|
+
const shutdown = async () => {
|
|
458
|
+
server.close();
|
|
459
|
+
try {
|
|
460
|
+
await lix.close();
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
// ignore
|
|
464
|
+
}
|
|
465
|
+
process.exit(0);
|
|
466
|
+
};
|
|
467
|
+
process.on("SIGINT", shutdown);
|
|
468
|
+
process.on("SIGTERM", shutdown);
|
|
469
|
+
}
|
|
470
|
+
export function registerUi(program) {
|
|
471
|
+
program
|
|
472
|
+
.command("ui")
|
|
473
|
+
.description("launch a basic local browser UI to validate the .acrm file (read-only; tables for people, companies, deals)")
|
|
474
|
+
.option("-p, --port <port>", "port to listen on", "3737")
|
|
475
|
+
.option("--no-open", "do not auto-open the browser")
|
|
476
|
+
.addHelpText("after", `
|
|
477
|
+
This UI is intentionally minimal — just enough to eyeball that an import landed
|
|
478
|
+
correctly. It is read-only, has no filters, search, or edit affordances.
|
|
479
|
+
|
|
480
|
+
Claude Code is expected to extend it as needed. The implementation is a single
|
|
481
|
+
file (src/commands/ui.ts): a node:http server with server-rendered HTML and
|
|
482
|
+
inline CSS, no client framework, no build step. To add a view, filter, or
|
|
483
|
+
detail drawer, edit that file directly.
|
|
484
|
+
`)
|
|
485
|
+
.action(async (opts) => {
|
|
486
|
+
const root = program.opts();
|
|
487
|
+
setJsonMode(root.json);
|
|
488
|
+
const port = Number(opts.port);
|
|
489
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
490
|
+
fail(`invalid port: ${opts.port}`, ERR.INVALID_INPUT);
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
let lix;
|
|
494
|
+
try {
|
|
495
|
+
lix = await openWorkspace({ workspace: root.workspace });
|
|
496
|
+
}
|
|
497
|
+
catch (e) {
|
|
498
|
+
if (e instanceof AcrmError)
|
|
499
|
+
fail(e.message, e.code, e.hint);
|
|
500
|
+
else
|
|
501
|
+
fail(e instanceof Error ? e.message : String(e), ERR.UI);
|
|
502
|
+
process.exit(1);
|
|
503
|
+
}
|
|
504
|
+
const resolved = root.workspace
|
|
505
|
+
? root.workspace.endsWith(".acrm")
|
|
506
|
+
? root.workspace
|
|
507
|
+
: root.workspace + ".acrm"
|
|
508
|
+
: (findWorkspace() ?? "workspace.acrm");
|
|
509
|
+
const workspaceLabel = path.basename(resolved);
|
|
510
|
+
startUiServer(lix, workspaceLabel, { port, open: opts.open });
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
//# sourceMappingURL=ui.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ui.js","sourceRoot":"","sources":["../../src/commands/ui.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { isLixError } from "@lix-js/sdk";
|
|
2
|
+
import { AcrmError } from "../lib/errors.js";
|
|
3
|
+
export async function exec(lix, sql, params = []) {
|
|
4
|
+
try {
|
|
5
|
+
const result = await lix.execute(sql, params);
|
|
6
|
+
const rows = result.rows.map((r) => r.toObject());
|
|
7
|
+
return { rows, rowsAffected: result.rowsAffected };
|
|
8
|
+
}
|
|
9
|
+
catch (e) {
|
|
10
|
+
if (isLixError(e)) {
|
|
11
|
+
// Surface the engine's own code + hint instead of collapsing to a single
|
|
12
|
+
// ACRM_ERROR_*. The lix engine speaks Postgres-style errors:
|
|
13
|
+
// `message` says what's wrong, `hint` (when present) says how to fix it.
|
|
14
|
+
throw new AcrmError(e.message, e.code, e.hint, e.details);
|
|
15
|
+
}
|
|
16
|
+
throw e;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function execOne(lix, sql, params = []) {
|
|
20
|
+
const { rows } = await exec(lix, sql, params);
|
|
21
|
+
return rows[0] ?? null;
|
|
22
|
+
}
|
|
23
|
+
export async function execScalar(lix, sql, params = []) {
|
|
24
|
+
const row = await execOne(lix, sql, params);
|
|
25
|
+
if (!row)
|
|
26
|
+
return null;
|
|
27
|
+
const keys = Object.keys(row);
|
|
28
|
+
return keys.length ? row[keys[0]] : null;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=execute.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execute.js","sourceRoot":"","sources":["../../src/db/execute.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAI7C,MAAM,CAAC,KAAK,UAAU,IAAI,CACxB,GAAQ,EACR,GAAW,EACX,SAAyC,EAAE;IAE3C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC9C,MAAM,IAAI,GAAU,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;QACzD,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,CAAC,YAAY,EAAE,CAAC;IACrD,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,IAAI,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;YAClB,yEAAyE;YACzE,6DAA6D;YAC7D,yEAAyE;YACzE,MAAM,IAAI,SAAS,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;QAC5D,CAAC;QACD,MAAM,CAAC,CAAC;IACV,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,GAAQ,EACR,GAAW,EACX,SAAyC,EAAE;IAE3C,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;IAC9C,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AACzB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,GAAQ,EACR,GAAW,EACX,SAAyC,EAAE;IAE3C,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;IAC5C,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC9B,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAE,CAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AACnD,CAAC"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export function encode(type, input) {
|
|
2
|
+
if (input === null || input === undefined) {
|
|
3
|
+
throw new Error(`cannot encode null/undefined for ${type}`);
|
|
4
|
+
}
|
|
5
|
+
switch (type) {
|
|
6
|
+
case "text":
|
|
7
|
+
case "url":
|
|
8
|
+
return { value: String(input).trim() };
|
|
9
|
+
case "personal-name": {
|
|
10
|
+
if (typeof input === "object")
|
|
11
|
+
return input;
|
|
12
|
+
const full = String(input).trim();
|
|
13
|
+
const parts = full.split(/\s+/);
|
|
14
|
+
const first_name = parts[0] ?? "";
|
|
15
|
+
const last_name = parts.length > 1 ? parts.slice(1).join(" ") : "";
|
|
16
|
+
return { full_name: full, first_name, last_name };
|
|
17
|
+
}
|
|
18
|
+
case "email-address": {
|
|
19
|
+
const raw = String(input).trim();
|
|
20
|
+
const lower = raw.toLowerCase();
|
|
21
|
+
const at = lower.indexOf("@");
|
|
22
|
+
if (at < 0)
|
|
23
|
+
throw new Error(`invalid email: ${raw}`);
|
|
24
|
+
const local = lower.slice(0, at);
|
|
25
|
+
const domain = lower.slice(at + 1);
|
|
26
|
+
return {
|
|
27
|
+
email_address: lower,
|
|
28
|
+
original_email_address: raw,
|
|
29
|
+
email_domain: domain,
|
|
30
|
+
email_root_domain: rootDomain(domain),
|
|
31
|
+
email_local_specifier: local,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
case "domain": {
|
|
35
|
+
const d = normalizeDomain(String(input));
|
|
36
|
+
return { domain: d, root_domain: rootDomain(d) };
|
|
37
|
+
}
|
|
38
|
+
case "number":
|
|
39
|
+
return { value: Number(input) };
|
|
40
|
+
case "currency": {
|
|
41
|
+
if (typeof input === "object")
|
|
42
|
+
return input;
|
|
43
|
+
return { currency_value: Number(input), currency_code: "USD" };
|
|
44
|
+
}
|
|
45
|
+
case "date":
|
|
46
|
+
return { date: String(input).trim() };
|
|
47
|
+
case "timestamp":
|
|
48
|
+
return { timestamp: String(input).trim() };
|
|
49
|
+
case "select":
|
|
50
|
+
case "status": {
|
|
51
|
+
if (typeof input === "object")
|
|
52
|
+
return input;
|
|
53
|
+
return { title: String(input).trim() };
|
|
54
|
+
}
|
|
55
|
+
case "record-reference": {
|
|
56
|
+
const v = input;
|
|
57
|
+
if (!v?.target_object || !v?.target_record_id) {
|
|
58
|
+
throw new Error("record-reference requires target_object + target_record_id");
|
|
59
|
+
}
|
|
60
|
+
return { target_object: v.target_object, target_record_id: v.target_record_id };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function normalizeUniqueKey(type, value) {
|
|
65
|
+
switch (type) {
|
|
66
|
+
case "email-address":
|
|
67
|
+
return value.email_address?.toLowerCase() ?? null;
|
|
68
|
+
case "domain":
|
|
69
|
+
return value.domain?.toLowerCase() ?? null;
|
|
70
|
+
case "url":
|
|
71
|
+
return value.value?.toLowerCase() ?? null;
|
|
72
|
+
case "text":
|
|
73
|
+
return value.value ?? null;
|
|
74
|
+
default:
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function recordReferenceTarget(value) {
|
|
79
|
+
const t = value.target_object;
|
|
80
|
+
const r = value.target_record_id;
|
|
81
|
+
if (!t || !r)
|
|
82
|
+
return null;
|
|
83
|
+
return { object: t, record_id: r };
|
|
84
|
+
}
|
|
85
|
+
export function normalizeDomain(input) {
|
|
86
|
+
let d = input.trim().toLowerCase();
|
|
87
|
+
d = d.replace(/^https?:\/\//, "");
|
|
88
|
+
d = d.replace(/^www\./, "");
|
|
89
|
+
d = d.split("/")[0];
|
|
90
|
+
return d;
|
|
91
|
+
}
|
|
92
|
+
export function rootDomain(domain) {
|
|
93
|
+
const parts = domain.split(".");
|
|
94
|
+
if (parts.length <= 2)
|
|
95
|
+
return domain;
|
|
96
|
+
return parts.slice(-2).join(".");
|
|
97
|
+
}
|
|
98
|
+
export function domainFromEmail(email) {
|
|
99
|
+
const at = email.indexOf("@");
|
|
100
|
+
if (at < 0)
|
|
101
|
+
return null;
|
|
102
|
+
return email.slice(at + 1).toLowerCase();
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=values.js.map
|