@hasna/microservices 0.0.4 → 0.0.6
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/bin/index.js +9 -1
- package/bin/mcp.js +9 -1
- package/dist/index.js +9 -1
- package/microservices/microservice-ads/src/cli/index.ts +198 -0
- package/microservices/microservice-ads/src/db/campaigns.ts +304 -0
- package/microservices/microservice-ads/src/mcp/index.ts +160 -0
- package/microservices/microservice-company/package.json +27 -0
- package/microservices/microservice-company/src/cli/index.ts +1126 -0
- package/microservices/microservice-company/src/db/company.ts +854 -0
- package/microservices/microservice-company/src/db/database.ts +93 -0
- package/microservices/microservice-company/src/db/migrations.ts +214 -0
- package/microservices/microservice-company/src/db/workflow-migrations.ts +44 -0
- package/microservices/microservice-company/src/index.ts +60 -0
- package/microservices/microservice-company/src/lib/audit.ts +168 -0
- package/microservices/microservice-company/src/lib/finance.ts +299 -0
- package/microservices/microservice-company/src/lib/settings.ts +85 -0
- package/microservices/microservice-company/src/lib/workflows.ts +698 -0
- package/microservices/microservice-company/src/mcp/index.ts +991 -0
- package/microservices/microservice-contracts/src/cli/index.ts +410 -23
- package/microservices/microservice-contracts/src/db/contracts.ts +430 -1
- package/microservices/microservice-contracts/src/db/migrations.ts +83 -0
- package/microservices/microservice-contracts/src/mcp/index.ts +312 -3
- package/microservices/microservice-domains/src/cli/index.ts +673 -0
- package/microservices/microservice-domains/src/db/domains.ts +613 -0
- package/microservices/microservice-domains/src/index.ts +21 -0
- package/microservices/microservice-domains/src/lib/brandsight.ts +285 -0
- package/microservices/microservice-domains/src/lib/godaddy.ts +328 -0
- package/microservices/microservice-domains/src/lib/namecheap.ts +474 -0
- package/microservices/microservice-domains/src/lib/registrar.ts +355 -0
- package/microservices/microservice-domains/src/mcp/index.ts +413 -0
- package/microservices/microservice-hiring/src/cli/index.ts +318 -8
- package/microservices/microservice-hiring/src/db/hiring.ts +503 -0
- package/microservices/microservice-hiring/src/db/migrations.ts +21 -0
- package/microservices/microservice-hiring/src/index.ts +29 -0
- package/microservices/microservice-hiring/src/lib/scoring.ts +206 -0
- package/microservices/microservice-hiring/src/mcp/index.ts +245 -0
- package/microservices/microservice-payments/src/cli/index.ts +255 -3
- package/microservices/microservice-payments/src/db/migrations.ts +18 -0
- package/microservices/microservice-payments/src/db/payments.ts +552 -0
- package/microservices/microservice-payments/src/mcp/index.ts +223 -0
- package/microservices/microservice-payroll/src/cli/index.ts +269 -0
- package/microservices/microservice-payroll/src/db/migrations.ts +26 -0
- package/microservices/microservice-payroll/src/db/payroll.ts +636 -0
- package/microservices/microservice-payroll/src/mcp/index.ts +246 -0
- package/microservices/microservice-shipping/src/cli/index.ts +211 -3
- package/microservices/microservice-shipping/src/db/migrations.ts +8 -0
- package/microservices/microservice-shipping/src/db/shipping.ts +453 -3
- package/microservices/microservice-shipping/src/mcp/index.ts +149 -1
- package/microservices/microservice-social/src/cli/index.ts +244 -2
- package/microservices/microservice-social/src/db/migrations.ts +33 -0
- package/microservices/microservice-social/src/db/social.ts +378 -4
- package/microservices/microservice-social/src/mcp/index.ts +221 -1
- package/microservices/microservice-subscriptions/src/cli/index.ts +315 -0
- package/microservices/microservice-subscriptions/src/db/migrations.ts +68 -0
- package/microservices/microservice-subscriptions/src/db/subscriptions.ts +567 -3
- package/microservices/microservice-subscriptions/src/mcp/index.ts +267 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1126 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import {
|
|
5
|
+
createOrg,
|
|
6
|
+
getOrg,
|
|
7
|
+
updateOrg,
|
|
8
|
+
createTeam,
|
|
9
|
+
getTeam,
|
|
10
|
+
listTeams,
|
|
11
|
+
updateTeam,
|
|
12
|
+
deleteTeam,
|
|
13
|
+
getTeamTree,
|
|
14
|
+
addMember,
|
|
15
|
+
getMember,
|
|
16
|
+
listMembers,
|
|
17
|
+
updateMember,
|
|
18
|
+
removeMember,
|
|
19
|
+
createCustomer,
|
|
20
|
+
getCustomer,
|
|
21
|
+
listCustomers,
|
|
22
|
+
updateCustomer,
|
|
23
|
+
deleteCustomer,
|
|
24
|
+
searchCustomers,
|
|
25
|
+
mergeCustomers,
|
|
26
|
+
createVendor,
|
|
27
|
+
getVendor,
|
|
28
|
+
listVendors,
|
|
29
|
+
updateVendor,
|
|
30
|
+
deleteVendor,
|
|
31
|
+
searchVendors,
|
|
32
|
+
} from "../db/company.js";
|
|
33
|
+
import {
|
|
34
|
+
generatePnl,
|
|
35
|
+
createPeriod,
|
|
36
|
+
closePeriod,
|
|
37
|
+
listPeriods,
|
|
38
|
+
generateCashflow,
|
|
39
|
+
setBudget,
|
|
40
|
+
getBudgetVsActual,
|
|
41
|
+
listBudgets,
|
|
42
|
+
} from "../lib/finance.js";
|
|
43
|
+
import {
|
|
44
|
+
logAction,
|
|
45
|
+
searchAudit,
|
|
46
|
+
getAuditStats,
|
|
47
|
+
type AuditAction,
|
|
48
|
+
} from "../lib/audit.js";
|
|
49
|
+
import {
|
|
50
|
+
getSetting,
|
|
51
|
+
setSetting,
|
|
52
|
+
getAllSettings,
|
|
53
|
+
deleteSetting,
|
|
54
|
+
} from "../lib/settings.js";
|
|
55
|
+
|
|
56
|
+
const program = new Command();
|
|
57
|
+
|
|
58
|
+
program
|
|
59
|
+
.name("microservice-company")
|
|
60
|
+
.description("AI agent control plane for autonomous company operations")
|
|
61
|
+
.version("0.0.1");
|
|
62
|
+
|
|
63
|
+
// ─── Organization ────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
const orgCmd = program.command("org").description("Organization management");
|
|
66
|
+
|
|
67
|
+
orgCmd
|
|
68
|
+
.command("create")
|
|
69
|
+
.description("Create an organization")
|
|
70
|
+
.requiredOption("--name <name>", "Organization name")
|
|
71
|
+
.option("--legal-name <name>", "Legal name")
|
|
72
|
+
.option("--tax-id <id>", "Tax ID")
|
|
73
|
+
.option("--phone <phone>", "Phone")
|
|
74
|
+
.option("--email <email>", "Email")
|
|
75
|
+
.option("--website <url>", "Website")
|
|
76
|
+
.option("--industry <industry>", "Industry")
|
|
77
|
+
.option("--currency <code>", "Currency code", "USD")
|
|
78
|
+
.option("--timezone <tz>", "Timezone", "UTC")
|
|
79
|
+
.option("--json", "Output as JSON", false)
|
|
80
|
+
.action((opts) => {
|
|
81
|
+
const org = createOrg({
|
|
82
|
+
name: opts.name,
|
|
83
|
+
legal_name: opts.legalName,
|
|
84
|
+
tax_id: opts.taxId,
|
|
85
|
+
phone: opts.phone,
|
|
86
|
+
email: opts.email,
|
|
87
|
+
website: opts.website,
|
|
88
|
+
industry: opts.industry,
|
|
89
|
+
currency: opts.currency,
|
|
90
|
+
timezone: opts.timezone,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (opts.json) {
|
|
94
|
+
console.log(JSON.stringify(org, null, 2));
|
|
95
|
+
} else {
|
|
96
|
+
console.log(`Created organization: ${org.name} (${org.id})`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
orgCmd
|
|
101
|
+
.command("get")
|
|
102
|
+
.description("Get an organization by ID")
|
|
103
|
+
.argument("<id>", "Organization ID")
|
|
104
|
+
.option("--json", "Output as JSON", false)
|
|
105
|
+
.action((id, opts) => {
|
|
106
|
+
const org = getOrg(id);
|
|
107
|
+
if (!org) {
|
|
108
|
+
console.error(`Organization '${id}' not found.`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (opts.json) {
|
|
113
|
+
console.log(JSON.stringify(org, null, 2));
|
|
114
|
+
} else {
|
|
115
|
+
console.log(`${org.name}`);
|
|
116
|
+
if (org.legal_name) console.log(` Legal: ${org.legal_name}`);
|
|
117
|
+
if (org.email) console.log(` Email: ${org.email}`);
|
|
118
|
+
if (org.phone) console.log(` Phone: ${org.phone}`);
|
|
119
|
+
if (org.website) console.log(` Website: ${org.website}`);
|
|
120
|
+
if (org.industry) console.log(` Industry: ${org.industry}`);
|
|
121
|
+
console.log(` Currency: ${org.currency}`);
|
|
122
|
+
console.log(` Timezone: ${org.timezone}`);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
orgCmd
|
|
127
|
+
.command("update")
|
|
128
|
+
.description("Update an organization")
|
|
129
|
+
.argument("<id>", "Organization ID")
|
|
130
|
+
.option("--name <name>", "Name")
|
|
131
|
+
.option("--legal-name <name>", "Legal name")
|
|
132
|
+
.option("--tax-id <id>", "Tax ID")
|
|
133
|
+
.option("--phone <phone>", "Phone")
|
|
134
|
+
.option("--email <email>", "Email")
|
|
135
|
+
.option("--website <url>", "Website")
|
|
136
|
+
.option("--industry <industry>", "Industry")
|
|
137
|
+
.option("--currency <code>", "Currency")
|
|
138
|
+
.option("--timezone <tz>", "Timezone")
|
|
139
|
+
.option("--json", "Output as JSON", false)
|
|
140
|
+
.action((id, opts) => {
|
|
141
|
+
const input: Record<string, unknown> = {};
|
|
142
|
+
if (opts.name !== undefined) input.name = opts.name;
|
|
143
|
+
if (opts.legalName !== undefined) input.legal_name = opts.legalName;
|
|
144
|
+
if (opts.taxId !== undefined) input.tax_id = opts.taxId;
|
|
145
|
+
if (opts.phone !== undefined) input.phone = opts.phone;
|
|
146
|
+
if (opts.email !== undefined) input.email = opts.email;
|
|
147
|
+
if (opts.website !== undefined) input.website = opts.website;
|
|
148
|
+
if (opts.industry !== undefined) input.industry = opts.industry;
|
|
149
|
+
if (opts.currency !== undefined) input.currency = opts.currency;
|
|
150
|
+
if (opts.timezone !== undefined) input.timezone = opts.timezone;
|
|
151
|
+
|
|
152
|
+
const org = updateOrg(id, input);
|
|
153
|
+
if (!org) {
|
|
154
|
+
console.error(`Organization '${id}' not found.`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (opts.json) {
|
|
159
|
+
console.log(JSON.stringify(org, null, 2));
|
|
160
|
+
} else {
|
|
161
|
+
console.log(`Updated: ${org.name}`);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ─── Teams ───────────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
const teamCmd = program.command("team").description("Team management");
|
|
168
|
+
|
|
169
|
+
teamCmd
|
|
170
|
+
.command("create")
|
|
171
|
+
.description("Create a team")
|
|
172
|
+
.requiredOption("--org <id>", "Organization ID")
|
|
173
|
+
.requiredOption("--name <name>", "Team name")
|
|
174
|
+
.option("--parent <id>", "Parent team ID")
|
|
175
|
+
.option("--department <dept>", "Department")
|
|
176
|
+
.option("--cost-center <cc>", "Cost center")
|
|
177
|
+
.option("--json", "Output as JSON", false)
|
|
178
|
+
.action((opts) => {
|
|
179
|
+
const team = createTeam({
|
|
180
|
+
org_id: opts.org,
|
|
181
|
+
name: opts.name,
|
|
182
|
+
parent_id: opts.parent,
|
|
183
|
+
department: opts.department,
|
|
184
|
+
cost_center: opts.costCenter,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (opts.json) {
|
|
188
|
+
console.log(JSON.stringify(team, null, 2));
|
|
189
|
+
} else {
|
|
190
|
+
console.log(`Created team: ${team.name} (${team.id})`);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
teamCmd
|
|
195
|
+
.command("list")
|
|
196
|
+
.description("List teams")
|
|
197
|
+
.option("--org <id>", "Filter by organization")
|
|
198
|
+
.option("--department <dept>", "Filter by department")
|
|
199
|
+
.option("--json", "Output as JSON", false)
|
|
200
|
+
.action((opts) => {
|
|
201
|
+
const teams = listTeams({
|
|
202
|
+
org_id: opts.org,
|
|
203
|
+
department: opts.department,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (opts.json) {
|
|
207
|
+
console.log(JSON.stringify(teams, null, 2));
|
|
208
|
+
} else {
|
|
209
|
+
if (teams.length === 0) {
|
|
210
|
+
console.log("No teams found.");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
for (const t of teams) {
|
|
214
|
+
const dept = t.department ? ` (${t.department})` : "";
|
|
215
|
+
console.log(` ${t.name}${dept} — ${t.id}`);
|
|
216
|
+
}
|
|
217
|
+
console.log(`\n${teams.length} team(s)`);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
teamCmd
|
|
222
|
+
.command("get")
|
|
223
|
+
.description("Get a team by ID")
|
|
224
|
+
.argument("<id>", "Team ID")
|
|
225
|
+
.option("--json", "Output as JSON", false)
|
|
226
|
+
.action((id, opts) => {
|
|
227
|
+
const team = getTeam(id);
|
|
228
|
+
if (!team) {
|
|
229
|
+
console.error(`Team '${id}' not found.`);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (opts.json) {
|
|
234
|
+
console.log(JSON.stringify(team, null, 2));
|
|
235
|
+
} else {
|
|
236
|
+
console.log(`${team.name}`);
|
|
237
|
+
if (team.department) console.log(` Department: ${team.department}`);
|
|
238
|
+
if (team.cost_center) console.log(` Cost Center: ${team.cost_center}`);
|
|
239
|
+
if (team.parent_id) console.log(` Parent: ${team.parent_id}`);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
teamCmd
|
|
244
|
+
.command("update")
|
|
245
|
+
.description("Update a team")
|
|
246
|
+
.argument("<id>", "Team ID")
|
|
247
|
+
.option("--name <name>", "Name")
|
|
248
|
+
.option("--parent <id>", "Parent team ID")
|
|
249
|
+
.option("--department <dept>", "Department")
|
|
250
|
+
.option("--cost-center <cc>", "Cost center")
|
|
251
|
+
.option("--json", "Output as JSON", false)
|
|
252
|
+
.action((id, opts) => {
|
|
253
|
+
const input: Record<string, unknown> = {};
|
|
254
|
+
if (opts.name !== undefined) input.name = opts.name;
|
|
255
|
+
if (opts.parent !== undefined) input.parent_id = opts.parent;
|
|
256
|
+
if (opts.department !== undefined) input.department = opts.department;
|
|
257
|
+
if (opts.costCenter !== undefined) input.cost_center = opts.costCenter;
|
|
258
|
+
|
|
259
|
+
const team = updateTeam(id, input);
|
|
260
|
+
if (!team) {
|
|
261
|
+
console.error(`Team '${id}' not found.`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (opts.json) {
|
|
266
|
+
console.log(JSON.stringify(team, null, 2));
|
|
267
|
+
} else {
|
|
268
|
+
console.log(`Updated: ${team.name}`);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
teamCmd
|
|
273
|
+
.command("delete")
|
|
274
|
+
.description("Delete a team")
|
|
275
|
+
.argument("<id>", "Team ID")
|
|
276
|
+
.action((id) => {
|
|
277
|
+
const deleted = deleteTeam(id);
|
|
278
|
+
if (deleted) {
|
|
279
|
+
console.log(`Deleted team ${id}`);
|
|
280
|
+
} else {
|
|
281
|
+
console.error(`Team '${id}' not found.`);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
teamCmd
|
|
287
|
+
.command("tree")
|
|
288
|
+
.description("Show team hierarchy")
|
|
289
|
+
.requiredOption("--org <id>", "Organization ID")
|
|
290
|
+
.option("--json", "Output as JSON", false)
|
|
291
|
+
.action((opts) => {
|
|
292
|
+
const tree = getTeamTree(opts.org);
|
|
293
|
+
|
|
294
|
+
if (opts.json) {
|
|
295
|
+
console.log(JSON.stringify(tree, null, 2));
|
|
296
|
+
} else {
|
|
297
|
+
function printTree(nodes: typeof tree, indent = 0) {
|
|
298
|
+
for (const node of nodes) {
|
|
299
|
+
const prefix = " ".repeat(indent);
|
|
300
|
+
const dept = node.department ? ` (${node.department})` : "";
|
|
301
|
+
console.log(`${prefix}${node.name}${dept}`);
|
|
302
|
+
printTree(node.children, indent + 1);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (tree.length === 0) {
|
|
306
|
+
console.log("No teams found.");
|
|
307
|
+
} else {
|
|
308
|
+
printTree(tree);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ─── Members ─────────────────────────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
const memberCmd = program.command("member").description("Member management");
|
|
316
|
+
|
|
317
|
+
memberCmd
|
|
318
|
+
.command("add")
|
|
319
|
+
.description("Add a member")
|
|
320
|
+
.requiredOption("--org <id>", "Organization ID")
|
|
321
|
+
.requiredOption("--name <name>", "Member name")
|
|
322
|
+
.option("--team <id>", "Team ID")
|
|
323
|
+
.option("--email <email>", "Email")
|
|
324
|
+
.option("--role <role>", "Role (owner/admin/manager/member/viewer)", "member")
|
|
325
|
+
.option("--title <title>", "Job title")
|
|
326
|
+
.option("--json", "Output as JSON", false)
|
|
327
|
+
.action((opts) => {
|
|
328
|
+
const member = addMember({
|
|
329
|
+
org_id: opts.org,
|
|
330
|
+
team_id: opts.team,
|
|
331
|
+
name: opts.name,
|
|
332
|
+
email: opts.email,
|
|
333
|
+
role: opts.role,
|
|
334
|
+
title: opts.title,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
if (opts.json) {
|
|
338
|
+
console.log(JSON.stringify(member, null, 2));
|
|
339
|
+
} else {
|
|
340
|
+
console.log(`Added member: ${member.name} (${member.id})`);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
memberCmd
|
|
345
|
+
.command("list")
|
|
346
|
+
.description("List members")
|
|
347
|
+
.option("--org <id>", "Filter by organization")
|
|
348
|
+
.option("--team <id>", "Filter by team")
|
|
349
|
+
.option("--role <role>", "Filter by role")
|
|
350
|
+
.option("--json", "Output as JSON", false)
|
|
351
|
+
.action((opts) => {
|
|
352
|
+
const members = listMembers({
|
|
353
|
+
org_id: opts.org,
|
|
354
|
+
team_id: opts.team,
|
|
355
|
+
role: opts.role,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
if (opts.json) {
|
|
359
|
+
console.log(JSON.stringify(members, null, 2));
|
|
360
|
+
} else {
|
|
361
|
+
if (members.length === 0) {
|
|
362
|
+
console.log("No members found.");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
for (const m of members) {
|
|
366
|
+
const email = m.email ? ` <${m.email}>` : "";
|
|
367
|
+
console.log(` ${m.name}${email} [${m.role}]`);
|
|
368
|
+
}
|
|
369
|
+
console.log(`\n${members.length} member(s)`);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
memberCmd
|
|
374
|
+
.command("get")
|
|
375
|
+
.description("Get a member by ID")
|
|
376
|
+
.argument("<id>", "Member ID")
|
|
377
|
+
.option("--json", "Output as JSON", false)
|
|
378
|
+
.action((id, opts) => {
|
|
379
|
+
const member = getMember(id);
|
|
380
|
+
if (!member) {
|
|
381
|
+
console.error(`Member '${id}' not found.`);
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (opts.json) {
|
|
386
|
+
console.log(JSON.stringify(member, null, 2));
|
|
387
|
+
} else {
|
|
388
|
+
console.log(`${member.name}`);
|
|
389
|
+
if (member.email) console.log(` Email: ${member.email}`);
|
|
390
|
+
console.log(` Role: ${member.role}`);
|
|
391
|
+
if (member.title) console.log(` Title: ${member.title}`);
|
|
392
|
+
console.log(` Status: ${member.status}`);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
memberCmd
|
|
397
|
+
.command("update")
|
|
398
|
+
.description("Update a member")
|
|
399
|
+
.argument("<id>", "Member ID")
|
|
400
|
+
.option("--name <name>", "Name")
|
|
401
|
+
.option("--team <id>", "Team ID")
|
|
402
|
+
.option("--email <email>", "Email")
|
|
403
|
+
.option("--role <role>", "Role")
|
|
404
|
+
.option("--title <title>", "Title")
|
|
405
|
+
.option("--status <status>", "Status")
|
|
406
|
+
.option("--json", "Output as JSON", false)
|
|
407
|
+
.action((id, opts) => {
|
|
408
|
+
const input: Record<string, unknown> = {};
|
|
409
|
+
if (opts.name !== undefined) input.name = opts.name;
|
|
410
|
+
if (opts.team !== undefined) input.team_id = opts.team;
|
|
411
|
+
if (opts.email !== undefined) input.email = opts.email;
|
|
412
|
+
if (opts.role !== undefined) input.role = opts.role;
|
|
413
|
+
if (opts.title !== undefined) input.title = opts.title;
|
|
414
|
+
if (opts.status !== undefined) input.status = opts.status;
|
|
415
|
+
|
|
416
|
+
const member = updateMember(id, input);
|
|
417
|
+
if (!member) {
|
|
418
|
+
console.error(`Member '${id}' not found.`);
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (opts.json) {
|
|
423
|
+
console.log(JSON.stringify(member, null, 2));
|
|
424
|
+
} else {
|
|
425
|
+
console.log(`Updated: ${member.name}`);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
memberCmd
|
|
430
|
+
.command("remove")
|
|
431
|
+
.description("Remove a member")
|
|
432
|
+
.argument("<id>", "Member ID")
|
|
433
|
+
.action((id) => {
|
|
434
|
+
const removed = removeMember(id);
|
|
435
|
+
if (removed) {
|
|
436
|
+
console.log(`Removed member ${id}`);
|
|
437
|
+
} else {
|
|
438
|
+
console.error(`Member '${id}' not found.`);
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// ─── Customers ───────────────────────────────────────────────────────────────
|
|
444
|
+
|
|
445
|
+
const customerCmd = program.command("customer").description("Customer management");
|
|
446
|
+
|
|
447
|
+
customerCmd
|
|
448
|
+
.command("create")
|
|
449
|
+
.description("Create a customer")
|
|
450
|
+
.requiredOption("--org <id>", "Organization ID")
|
|
451
|
+
.requiredOption("--name <name>", "Customer name")
|
|
452
|
+
.option("--email <email>", "Email")
|
|
453
|
+
.option("--phone <phone>", "Phone")
|
|
454
|
+
.option("--company <company>", "Company name")
|
|
455
|
+
.option("--source <source>", "Lead source")
|
|
456
|
+
.option("--tags <tags>", "Comma-separated tags")
|
|
457
|
+
.option("--json", "Output as JSON", false)
|
|
458
|
+
.action((opts) => {
|
|
459
|
+
const customer = createCustomer({
|
|
460
|
+
org_id: opts.org,
|
|
461
|
+
name: opts.name,
|
|
462
|
+
email: opts.email,
|
|
463
|
+
phone: opts.phone,
|
|
464
|
+
company: opts.company,
|
|
465
|
+
source: opts.source,
|
|
466
|
+
tags: opts.tags ? opts.tags.split(",").map((t: string) => t.trim()) : undefined,
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
if (opts.json) {
|
|
470
|
+
console.log(JSON.stringify(customer, null, 2));
|
|
471
|
+
} else {
|
|
472
|
+
console.log(`Created customer: ${customer.name} (${customer.id})`);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
customerCmd
|
|
477
|
+
.command("list")
|
|
478
|
+
.description("List customers")
|
|
479
|
+
.option("--org <id>", "Filter by organization")
|
|
480
|
+
.option("--search <query>", "Search")
|
|
481
|
+
.option("--limit <n>", "Limit results")
|
|
482
|
+
.option("--json", "Output as JSON", false)
|
|
483
|
+
.action((opts) => {
|
|
484
|
+
const customers = listCustomers({
|
|
485
|
+
org_id: opts.org,
|
|
486
|
+
search: opts.search,
|
|
487
|
+
limit: opts.limit ? parseInt(opts.limit) : undefined,
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
if (opts.json) {
|
|
491
|
+
console.log(JSON.stringify(customers, null, 2));
|
|
492
|
+
} else {
|
|
493
|
+
if (customers.length === 0) {
|
|
494
|
+
console.log("No customers found.");
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
for (const c of customers) {
|
|
498
|
+
const email = c.email ? ` <${c.email}>` : "";
|
|
499
|
+
console.log(` ${c.name}${email}`);
|
|
500
|
+
}
|
|
501
|
+
console.log(`\n${customers.length} customer(s)`);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
customerCmd
|
|
506
|
+
.command("get")
|
|
507
|
+
.description("Get a customer by ID")
|
|
508
|
+
.argument("<id>", "Customer ID")
|
|
509
|
+
.option("--json", "Output as JSON", false)
|
|
510
|
+
.action((id, opts) => {
|
|
511
|
+
const customer = getCustomer(id);
|
|
512
|
+
if (!customer) {
|
|
513
|
+
console.error(`Customer '${id}' not found.`);
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (opts.json) {
|
|
518
|
+
console.log(JSON.stringify(customer, null, 2));
|
|
519
|
+
} else {
|
|
520
|
+
console.log(`${customer.name}`);
|
|
521
|
+
if (customer.email) console.log(` Email: ${customer.email}`);
|
|
522
|
+
if (customer.phone) console.log(` Phone: ${customer.phone}`);
|
|
523
|
+
if (customer.company) console.log(` Company: ${customer.company}`);
|
|
524
|
+
if (customer.tags.length) console.log(` Tags: ${customer.tags.join(", ")}`);
|
|
525
|
+
console.log(` Lifetime Value: ${customer.lifetime_value}`);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
customerCmd
|
|
530
|
+
.command("update")
|
|
531
|
+
.description("Update a customer")
|
|
532
|
+
.argument("<id>", "Customer ID")
|
|
533
|
+
.option("--name <name>", "Name")
|
|
534
|
+
.option("--email <email>", "Email")
|
|
535
|
+
.option("--phone <phone>", "Phone")
|
|
536
|
+
.option("--company <company>", "Company")
|
|
537
|
+
.option("--source <source>", "Source")
|
|
538
|
+
.option("--tags <tags>", "Comma-separated tags")
|
|
539
|
+
.option("--lifetime-value <value>", "Lifetime value")
|
|
540
|
+
.option("--json", "Output as JSON", false)
|
|
541
|
+
.action((id, opts) => {
|
|
542
|
+
const input: Record<string, unknown> = {};
|
|
543
|
+
if (opts.name !== undefined) input.name = opts.name;
|
|
544
|
+
if (opts.email !== undefined) input.email = opts.email;
|
|
545
|
+
if (opts.phone !== undefined) input.phone = opts.phone;
|
|
546
|
+
if (opts.company !== undefined) input.company = opts.company;
|
|
547
|
+
if (opts.source !== undefined) input.source = opts.source;
|
|
548
|
+
if (opts.tags !== undefined) input.tags = opts.tags.split(",").map((t: string) => t.trim());
|
|
549
|
+
if (opts.lifetimeValue !== undefined) input.lifetime_value = parseFloat(opts.lifetimeValue);
|
|
550
|
+
|
|
551
|
+
const customer = updateCustomer(id, input);
|
|
552
|
+
if (!customer) {
|
|
553
|
+
console.error(`Customer '${id}' not found.`);
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (opts.json) {
|
|
558
|
+
console.log(JSON.stringify(customer, null, 2));
|
|
559
|
+
} else {
|
|
560
|
+
console.log(`Updated: ${customer.name}`);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
customerCmd
|
|
565
|
+
.command("delete")
|
|
566
|
+
.description("Delete a customer")
|
|
567
|
+
.argument("<id>", "Customer ID")
|
|
568
|
+
.action((id) => {
|
|
569
|
+
const deleted = deleteCustomer(id);
|
|
570
|
+
if (deleted) {
|
|
571
|
+
console.log(`Deleted customer ${id}`);
|
|
572
|
+
} else {
|
|
573
|
+
console.error(`Customer '${id}' not found.`);
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
customerCmd
|
|
579
|
+
.command("search")
|
|
580
|
+
.description("Search customers")
|
|
581
|
+
.requiredOption("--org <id>", "Organization ID")
|
|
582
|
+
.argument("<query>", "Search term")
|
|
583
|
+
.option("--json", "Output as JSON", false)
|
|
584
|
+
.action((query, opts) => {
|
|
585
|
+
const results = searchCustomers(opts.org, query);
|
|
586
|
+
|
|
587
|
+
if (opts.json) {
|
|
588
|
+
console.log(JSON.stringify(results, null, 2));
|
|
589
|
+
} else {
|
|
590
|
+
if (results.length === 0) {
|
|
591
|
+
console.log(`No customers matching "${query}".`);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
for (const c of results) {
|
|
595
|
+
console.log(` ${c.name} ${c.email ? `<${c.email}>` : ""}`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
customerCmd
|
|
601
|
+
.command("merge")
|
|
602
|
+
.description("Merge two customers (keep first, merge data from second)")
|
|
603
|
+
.argument("<id1>", "Primary customer ID")
|
|
604
|
+
.argument("<id2>", "Secondary customer ID (will be deleted)")
|
|
605
|
+
.option("--json", "Output as JSON", false)
|
|
606
|
+
.action((id1, id2, opts) => {
|
|
607
|
+
const merged = mergeCustomers(id1, id2);
|
|
608
|
+
if (!merged) {
|
|
609
|
+
console.error("One or both customers not found.");
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (opts.json) {
|
|
614
|
+
console.log(JSON.stringify(merged, null, 2));
|
|
615
|
+
} else {
|
|
616
|
+
console.log(`Merged into: ${merged.name} (${merged.id})`);
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// ─── Vendors ─────────────────────────────────────────────────────────────────
|
|
621
|
+
|
|
622
|
+
const vendorCmd = program.command("vendor").description("Vendor management");
|
|
623
|
+
|
|
624
|
+
vendorCmd
|
|
625
|
+
.command("add")
|
|
626
|
+
.description("Add a vendor")
|
|
627
|
+
.requiredOption("--org <id>", "Organization ID")
|
|
628
|
+
.requiredOption("--name <name>", "Vendor name")
|
|
629
|
+
.option("--email <email>", "Email")
|
|
630
|
+
.option("--phone <phone>", "Phone")
|
|
631
|
+
.option("--company <company>", "Company name")
|
|
632
|
+
.option("--category <cat>", "Category (supplier/contractor/partner/agency)")
|
|
633
|
+
.option("--payment-terms <terms>", "Payment terms")
|
|
634
|
+
.option("--json", "Output as JSON", false)
|
|
635
|
+
.action((opts) => {
|
|
636
|
+
const vendor = createVendor({
|
|
637
|
+
org_id: opts.org,
|
|
638
|
+
name: opts.name,
|
|
639
|
+
email: opts.email,
|
|
640
|
+
phone: opts.phone,
|
|
641
|
+
company: opts.company,
|
|
642
|
+
category: opts.category,
|
|
643
|
+
payment_terms: opts.paymentTerms,
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
if (opts.json) {
|
|
647
|
+
console.log(JSON.stringify(vendor, null, 2));
|
|
648
|
+
} else {
|
|
649
|
+
console.log(`Added vendor: ${vendor.name} (${vendor.id})`);
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
vendorCmd
|
|
654
|
+
.command("list")
|
|
655
|
+
.description("List vendors")
|
|
656
|
+
.option("--org <id>", "Filter by organization")
|
|
657
|
+
.option("--category <cat>", "Filter by category")
|
|
658
|
+
.option("--json", "Output as JSON", false)
|
|
659
|
+
.action((opts) => {
|
|
660
|
+
const vendors = listVendors({
|
|
661
|
+
org_id: opts.org,
|
|
662
|
+
category: opts.category,
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
if (opts.json) {
|
|
666
|
+
console.log(JSON.stringify(vendors, null, 2));
|
|
667
|
+
} else {
|
|
668
|
+
if (vendors.length === 0) {
|
|
669
|
+
console.log("No vendors found.");
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
for (const v of vendors) {
|
|
673
|
+
const cat = v.category ? ` [${v.category}]` : "";
|
|
674
|
+
console.log(` ${v.name}${cat}`);
|
|
675
|
+
}
|
|
676
|
+
console.log(`\n${vendors.length} vendor(s)`);
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
vendorCmd
|
|
681
|
+
.command("get")
|
|
682
|
+
.description("Get a vendor by ID")
|
|
683
|
+
.argument("<id>", "Vendor ID")
|
|
684
|
+
.option("--json", "Output as JSON", false)
|
|
685
|
+
.action((id, opts) => {
|
|
686
|
+
const vendor = getVendor(id);
|
|
687
|
+
if (!vendor) {
|
|
688
|
+
console.error(`Vendor '${id}' not found.`);
|
|
689
|
+
process.exit(1);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (opts.json) {
|
|
693
|
+
console.log(JSON.stringify(vendor, null, 2));
|
|
694
|
+
} else {
|
|
695
|
+
console.log(`${vendor.name}`);
|
|
696
|
+
if (vendor.email) console.log(` Email: ${vendor.email}`);
|
|
697
|
+
if (vendor.phone) console.log(` Phone: ${vendor.phone}`);
|
|
698
|
+
if (vendor.category) console.log(` Category: ${vendor.category}`);
|
|
699
|
+
if (vendor.payment_terms) console.log(` Payment Terms: ${vendor.payment_terms}`);
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
vendorCmd
|
|
704
|
+
.command("update")
|
|
705
|
+
.description("Update a vendor")
|
|
706
|
+
.argument("<id>", "Vendor ID")
|
|
707
|
+
.option("--name <name>", "Name")
|
|
708
|
+
.option("--email <email>", "Email")
|
|
709
|
+
.option("--phone <phone>", "Phone")
|
|
710
|
+
.option("--company <company>", "Company")
|
|
711
|
+
.option("--category <cat>", "Category")
|
|
712
|
+
.option("--payment-terms <terms>", "Payment terms")
|
|
713
|
+
.option("--json", "Output as JSON", false)
|
|
714
|
+
.action((id, opts) => {
|
|
715
|
+
const input: Record<string, unknown> = {};
|
|
716
|
+
if (opts.name !== undefined) input.name = opts.name;
|
|
717
|
+
if (opts.email !== undefined) input.email = opts.email;
|
|
718
|
+
if (opts.phone !== undefined) input.phone = opts.phone;
|
|
719
|
+
if (opts.company !== undefined) input.company = opts.company;
|
|
720
|
+
if (opts.category !== undefined) input.category = opts.category;
|
|
721
|
+
if (opts.paymentTerms !== undefined) input.payment_terms = opts.paymentTerms;
|
|
722
|
+
|
|
723
|
+
const vendor = updateVendor(id, input);
|
|
724
|
+
if (!vendor) {
|
|
725
|
+
console.error(`Vendor '${id}' not found.`);
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (opts.json) {
|
|
730
|
+
console.log(JSON.stringify(vendor, null, 2));
|
|
731
|
+
} else {
|
|
732
|
+
console.log(`Updated: ${vendor.name}`);
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
vendorCmd
|
|
737
|
+
.command("delete")
|
|
738
|
+
.description("Delete a vendor")
|
|
739
|
+
.argument("<id>", "Vendor ID")
|
|
740
|
+
.action((id) => {
|
|
741
|
+
const deleted = deleteVendor(id);
|
|
742
|
+
if (deleted) {
|
|
743
|
+
console.log(`Deleted vendor ${id}`);
|
|
744
|
+
} else {
|
|
745
|
+
console.error(`Vendor '${id}' not found.`);
|
|
746
|
+
process.exit(1);
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
vendorCmd
|
|
751
|
+
.command("search")
|
|
752
|
+
.description("Search vendors")
|
|
753
|
+
.requiredOption("--org <id>", "Organization ID")
|
|
754
|
+
.argument("<query>", "Search term")
|
|
755
|
+
.option("--json", "Output as JSON", false)
|
|
756
|
+
.action((query, opts) => {
|
|
757
|
+
const results = searchVendors(opts.org, query);
|
|
758
|
+
|
|
759
|
+
if (opts.json) {
|
|
760
|
+
console.log(JSON.stringify(results, null, 2));
|
|
761
|
+
} else {
|
|
762
|
+
if (results.length === 0) {
|
|
763
|
+
console.log(`No vendors matching "${query}".`);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
for (const v of results) {
|
|
767
|
+
console.log(` ${v.name} ${v.email ? `<${v.email}>` : ""}`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// ─── Audit ───────────────────────────────────────────────────────────────────
|
|
773
|
+
|
|
774
|
+
const auditCmd = program.command("audit").description("Audit log management");
|
|
775
|
+
|
|
776
|
+
auditCmd
|
|
777
|
+
.command("search")
|
|
778
|
+
.description("Search audit log entries")
|
|
779
|
+
.option("--org <id>", "Filter by organization")
|
|
780
|
+
.option("--actor <actor>", "Filter by actor")
|
|
781
|
+
.option("--service <service>", "Filter by service")
|
|
782
|
+
.option("--action <action>", "Filter by action")
|
|
783
|
+
.option("--entity-type <type>", "Filter by entity type")
|
|
784
|
+
.option("--entity-id <id>", "Filter by entity ID")
|
|
785
|
+
.option("--from <date>", "From date (ISO)")
|
|
786
|
+
.option("--to <date>", "To date (ISO)")
|
|
787
|
+
.option("--limit <n>", "Limit results")
|
|
788
|
+
.option("--json", "Output as JSON", false)
|
|
789
|
+
.action((opts) => {
|
|
790
|
+
const results = searchAudit({
|
|
791
|
+
org_id: opts.org,
|
|
792
|
+
actor: opts.actor,
|
|
793
|
+
service: opts.service,
|
|
794
|
+
action: opts.action as AuditAction | undefined,
|
|
795
|
+
entity_type: opts.entityType,
|
|
796
|
+
entity_id: opts.entityId,
|
|
797
|
+
from: opts.from,
|
|
798
|
+
to: opts.to,
|
|
799
|
+
limit: opts.limit ? parseInt(opts.limit) : undefined,
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
if (opts.json) {
|
|
803
|
+
console.log(JSON.stringify(results, null, 2));
|
|
804
|
+
} else {
|
|
805
|
+
if (results.length === 0) {
|
|
806
|
+
console.log("No audit entries found.");
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
for (const e of results) {
|
|
810
|
+
const svc = e.service ? ` [${e.service}]` : "";
|
|
811
|
+
const entity = e.entity_type ? ` ${e.entity_type}:${e.entity_id}` : "";
|
|
812
|
+
console.log(` ${e.timestamp} ${e.actor} ${e.action}${svc}${entity}`);
|
|
813
|
+
}
|
|
814
|
+
console.log(`\n${results.length} entry/entries`);
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
auditCmd
|
|
819
|
+
.command("log")
|
|
820
|
+
.description("Log an audit action")
|
|
821
|
+
.requiredOption("--actor <actor>", "Actor name")
|
|
822
|
+
.requiredOption("--action <action>", "Action (create/update/delete/execute/login/approve)")
|
|
823
|
+
.option("--org <id>", "Organization ID")
|
|
824
|
+
.option("--service <service>", "Service name")
|
|
825
|
+
.option("--entity-type <type>", "Entity type")
|
|
826
|
+
.option("--entity-id <id>", "Entity ID")
|
|
827
|
+
.option("--details <json>", "Details JSON")
|
|
828
|
+
.option("--json", "Output as JSON", false)
|
|
829
|
+
.action((opts) => {
|
|
830
|
+
const entry = logAction({
|
|
831
|
+
org_id: opts.org,
|
|
832
|
+
actor: opts.actor,
|
|
833
|
+
action: opts.action as AuditAction,
|
|
834
|
+
service: opts.service,
|
|
835
|
+
entity_type: opts.entityType,
|
|
836
|
+
entity_id: opts.entityId,
|
|
837
|
+
details: opts.details ? JSON.parse(opts.details) : undefined,
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
if (opts.json) {
|
|
841
|
+
console.log(JSON.stringify(entry, null, 2));
|
|
842
|
+
} else {
|
|
843
|
+
console.log(`Logged: ${entry.actor} ${entry.action} (${entry.id})`);
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
auditCmd
|
|
848
|
+
.command("stats")
|
|
849
|
+
.description("Show audit statistics")
|
|
850
|
+
.option("--org <id>", "Organization ID")
|
|
851
|
+
.option("--from <date>", "From date (ISO)")
|
|
852
|
+
.option("--to <date>", "To date (ISO)")
|
|
853
|
+
.option("--json", "Output as JSON", false)
|
|
854
|
+
.action((opts) => {
|
|
855
|
+
const stats = getAuditStats(opts.org, opts.from, opts.to);
|
|
856
|
+
|
|
857
|
+
if (opts.json) {
|
|
858
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
859
|
+
} else {
|
|
860
|
+
console.log(`Total entries: ${stats.total}`);
|
|
861
|
+
console.log("\nBy actor:");
|
|
862
|
+
for (const [actor, count] of Object.entries(stats.by_actor)) {
|
|
863
|
+
console.log(` ${actor}: ${count}`);
|
|
864
|
+
}
|
|
865
|
+
console.log("\nBy service:");
|
|
866
|
+
for (const [service, count] of Object.entries(stats.by_service)) {
|
|
867
|
+
console.log(` ${service}: ${count}`);
|
|
868
|
+
}
|
|
869
|
+
console.log("\nBy action:");
|
|
870
|
+
for (const [action, count] of Object.entries(stats.by_action)) {
|
|
871
|
+
console.log(` ${action}: ${count}`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
// ─── Settings ────────────────────────────────────────────────────────────────
|
|
877
|
+
|
|
878
|
+
const settingsCmd = program.command("settings").description("Company settings management");
|
|
879
|
+
|
|
880
|
+
settingsCmd
|
|
881
|
+
.command("view")
|
|
882
|
+
.description("View all settings")
|
|
883
|
+
.option("--org <id>", "Organization ID")
|
|
884
|
+
.option("--category <cat>", "Filter by category")
|
|
885
|
+
.option("--json", "Output as JSON", false)
|
|
886
|
+
.action((opts) => {
|
|
887
|
+
const settings = getAllSettings(opts.org || null, opts.category);
|
|
888
|
+
|
|
889
|
+
if (opts.json) {
|
|
890
|
+
console.log(JSON.stringify(settings, null, 2));
|
|
891
|
+
} else {
|
|
892
|
+
if (settings.length === 0) {
|
|
893
|
+
console.log("No settings found.");
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
for (const s of settings) {
|
|
897
|
+
const cat = s.category ? ` [${s.category}]` : "";
|
|
898
|
+
console.log(` ${s.key} = ${s.value}${cat}`);
|
|
899
|
+
}
|
|
900
|
+
console.log(`\n${settings.length} setting(s)`);
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
settingsCmd
|
|
905
|
+
.command("set")
|
|
906
|
+
.description("Set a setting value")
|
|
907
|
+
.argument("<key>", "Setting key")
|
|
908
|
+
.argument("<value>", "Setting value")
|
|
909
|
+
.option("--org <id>", "Organization ID")
|
|
910
|
+
.option("--category <cat>", "Category")
|
|
911
|
+
.option("--json", "Output as JSON", false)
|
|
912
|
+
.action((key, value, opts) => {
|
|
913
|
+
const setting = setSetting(opts.org || null, key, value, opts.category);
|
|
914
|
+
|
|
915
|
+
if (opts.json) {
|
|
916
|
+
console.log(JSON.stringify(setting, null, 2));
|
|
917
|
+
} else {
|
|
918
|
+
console.log(`Set: ${setting.key} = ${setting.value}`);
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
settingsCmd
|
|
923
|
+
.command("delete")
|
|
924
|
+
.description("Delete a setting")
|
|
925
|
+
.argument("<key>", "Setting key")
|
|
926
|
+
.option("--org <id>", "Organization ID")
|
|
927
|
+
.action((key, opts) => {
|
|
928
|
+
const deleted = deleteSetting(opts.org || null, key);
|
|
929
|
+
if (deleted) {
|
|
930
|
+
console.log(`Deleted setting '${key}'`);
|
|
931
|
+
} else {
|
|
932
|
+
console.error(`Setting '${key}' not found.`);
|
|
933
|
+
process.exit(1);
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
// ─── P&L ────────────────────────────────────────────────────────────────────
|
|
938
|
+
|
|
939
|
+
program
|
|
940
|
+
.command("pnl")
|
|
941
|
+
.description("Generate a Profit & Loss report")
|
|
942
|
+
.requiredOption("--org <id>", "Organization ID")
|
|
943
|
+
.requiredOption("--from <date>", "Start date (YYYY-MM-DD)")
|
|
944
|
+
.requiredOption("--to <date>", "End date (YYYY-MM-DD)")
|
|
945
|
+
.option("--json", "Output as JSON", false)
|
|
946
|
+
.action((opts) => {
|
|
947
|
+
const report = generatePnl(opts.org, opts.from, opts.to);
|
|
948
|
+
|
|
949
|
+
if (opts.json) {
|
|
950
|
+
console.log(JSON.stringify(report, null, 2));
|
|
951
|
+
} else {
|
|
952
|
+
console.log(`P&L Report (${opts.from} to ${opts.to})`);
|
|
953
|
+
console.log(` Revenue: ${report.revenue.toFixed(2)}`);
|
|
954
|
+
console.log(` Expenses: ${report.expenses.toFixed(2)}`);
|
|
955
|
+
console.log(` Net Income: ${report.net_income.toFixed(2)}`);
|
|
956
|
+
const services = Object.keys(report.breakdown_by_service);
|
|
957
|
+
if (services.length > 0) {
|
|
958
|
+
console.log(" Breakdown:");
|
|
959
|
+
for (const svc of services) {
|
|
960
|
+
const b = report.breakdown_by_service[svc];
|
|
961
|
+
console.log(` ${svc}: rev=${b.revenue.toFixed(2)} exp=${b.expenses.toFixed(2)}`);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
// ─── Financial Periods ───────────────────────────────────────────────────────
|
|
968
|
+
|
|
969
|
+
const periodCmd = program.command("period").description("Financial period management");
|
|
970
|
+
|
|
971
|
+
periodCmd
|
|
972
|
+
.command("create")
|
|
973
|
+
.description("Create a financial period")
|
|
974
|
+
.requiredOption("--org <id>", "Organization ID")
|
|
975
|
+
.requiredOption("--name <name>", "Period name")
|
|
976
|
+
.requiredOption("--type <type>", "Period type (month/quarter/year)")
|
|
977
|
+
.requiredOption("--from <date>", "Start date (YYYY-MM-DD)")
|
|
978
|
+
.requiredOption("--to <date>", "End date (YYYY-MM-DD)")
|
|
979
|
+
.option("--json", "Output as JSON", false)
|
|
980
|
+
.action((opts) => {
|
|
981
|
+
const period = createPeriod(opts.org, opts.name, opts.type, opts.from, opts.to);
|
|
982
|
+
|
|
983
|
+
if (opts.json) {
|
|
984
|
+
console.log(JSON.stringify(period, null, 2));
|
|
985
|
+
} else {
|
|
986
|
+
console.log(`Created period: ${period.name} (${period.id})`);
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
periodCmd
|
|
991
|
+
.command("list")
|
|
992
|
+
.description("List financial periods")
|
|
993
|
+
.requiredOption("--org <id>", "Organization ID")
|
|
994
|
+
.option("--type <type>", "Filter by type (month/quarter/year)")
|
|
995
|
+
.option("--json", "Output as JSON", false)
|
|
996
|
+
.action((opts) => {
|
|
997
|
+
const periods = listPeriods(opts.org, opts.type);
|
|
998
|
+
|
|
999
|
+
if (opts.json) {
|
|
1000
|
+
console.log(JSON.stringify(periods, null, 2));
|
|
1001
|
+
} else {
|
|
1002
|
+
if (periods.length === 0) {
|
|
1003
|
+
console.log("No financial periods found.");
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
for (const p of periods) {
|
|
1007
|
+
console.log(` ${p.name} [${p.type}] ${p.status} (${p.start_date} to ${p.end_date})`);
|
|
1008
|
+
}
|
|
1009
|
+
console.log(`\n${periods.length} period(s)`);
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
periodCmd
|
|
1014
|
+
.command("close")
|
|
1015
|
+
.description("Close a financial period with final figures")
|
|
1016
|
+
.argument("<id>", "Period ID")
|
|
1017
|
+
.requiredOption("--revenue <amount>", "Total revenue")
|
|
1018
|
+
.requiredOption("--expenses <amount>", "Total expenses")
|
|
1019
|
+
.option("--json", "Output as JSON", false)
|
|
1020
|
+
.action((id, opts) => {
|
|
1021
|
+
const period = closePeriod(id, parseFloat(opts.revenue), parseFloat(opts.expenses));
|
|
1022
|
+
if (!period) {
|
|
1023
|
+
console.error(`Period '${id}' not found.`);
|
|
1024
|
+
process.exit(1);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (opts.json) {
|
|
1028
|
+
console.log(JSON.stringify(period, null, 2));
|
|
1029
|
+
} else {
|
|
1030
|
+
console.log(`Closed period: ${period.name}`);
|
|
1031
|
+
console.log(` Revenue: ${period.revenue.toFixed(2)}`);
|
|
1032
|
+
console.log(` Expenses: ${period.expenses.toFixed(2)}`);
|
|
1033
|
+
console.log(` Net Income: ${period.net_income.toFixed(2)}`);
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
// ─── Cashflow ────────────────────────────────────────────────────────────────
|
|
1038
|
+
|
|
1039
|
+
program
|
|
1040
|
+
.command("cashflow")
|
|
1041
|
+
.description("Generate a cashflow report")
|
|
1042
|
+
.requiredOption("--org <id>", "Organization ID")
|
|
1043
|
+
.requiredOption("--from <date>", "Start date (YYYY-MM-DD)")
|
|
1044
|
+
.requiredOption("--to <date>", "End date (YYYY-MM-DD)")
|
|
1045
|
+
.option("--json", "Output as JSON", false)
|
|
1046
|
+
.action((opts) => {
|
|
1047
|
+
const report = generateCashflow(opts.org, opts.from, opts.to);
|
|
1048
|
+
|
|
1049
|
+
if (opts.json) {
|
|
1050
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1051
|
+
} else {
|
|
1052
|
+
console.log(`Cashflow Report (${opts.from} to ${opts.to})`);
|
|
1053
|
+
console.log(` Cash In: ${report.cash_in.toFixed(2)}`);
|
|
1054
|
+
console.log(` Cash Out: ${report.cash_out.toFixed(2)}`);
|
|
1055
|
+
console.log(` Net Cashflow: ${report.net_cashflow.toFixed(2)}`);
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
// ─── Budgets ─────────────────────────────────────────────────────────────────
|
|
1060
|
+
|
|
1061
|
+
const budgetCmd = program.command("budget").description("Budget management");
|
|
1062
|
+
|
|
1063
|
+
budgetCmd
|
|
1064
|
+
.command("set")
|
|
1065
|
+
.description("Set a department budget")
|
|
1066
|
+
.requiredOption("--org <id>", "Organization ID")
|
|
1067
|
+
.requiredOption("--department <dept>", "Department name")
|
|
1068
|
+
.requiredOption("--amount <amount>", "Monthly budget amount")
|
|
1069
|
+
.option("--json", "Output as JSON", false)
|
|
1070
|
+
.action((opts) => {
|
|
1071
|
+
const budget = setBudget(opts.org, opts.department, parseFloat(opts.amount));
|
|
1072
|
+
|
|
1073
|
+
if (opts.json) {
|
|
1074
|
+
console.log(JSON.stringify(budget, null, 2));
|
|
1075
|
+
} else {
|
|
1076
|
+
console.log(`Budget set: ${budget.department} = ${budget.monthly_amount}/month`);
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
budgetCmd
|
|
1081
|
+
.command("list")
|
|
1082
|
+
.description("List all budgets")
|
|
1083
|
+
.requiredOption("--org <id>", "Organization ID")
|
|
1084
|
+
.option("--json", "Output as JSON", false)
|
|
1085
|
+
.action((opts) => {
|
|
1086
|
+
const budgets = listBudgets(opts.org);
|
|
1087
|
+
|
|
1088
|
+
if (opts.json) {
|
|
1089
|
+
console.log(JSON.stringify(budgets, null, 2));
|
|
1090
|
+
} else {
|
|
1091
|
+
if (budgets.length === 0) {
|
|
1092
|
+
console.log("No budgets found.");
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
for (const b of budgets) {
|
|
1096
|
+
console.log(` ${b.department}: ${b.monthly_amount}/month (${b.currency})`);
|
|
1097
|
+
}
|
|
1098
|
+
console.log(`\n${budgets.length} budget(s)`);
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
budgetCmd
|
|
1103
|
+
.command("check")
|
|
1104
|
+
.description("Check budget vs actual spending")
|
|
1105
|
+
.requiredOption("--org <id>", "Organization ID")
|
|
1106
|
+
.requiredOption("--department <dept>", "Department name")
|
|
1107
|
+
.requiredOption("--month <month>", "Month (YYYY-MM)")
|
|
1108
|
+
.option("--json", "Output as JSON", false)
|
|
1109
|
+
.action((opts) => {
|
|
1110
|
+
const result = getBudgetVsActual(opts.org, opts.department, opts.month);
|
|
1111
|
+
if (!result) {
|
|
1112
|
+
console.error(`No budget found for department '${opts.department}'.`);
|
|
1113
|
+
process.exit(1);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
if (opts.json) {
|
|
1117
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1118
|
+
} else {
|
|
1119
|
+
console.log(`Budget vs Actual: ${result.department} (${opts.month})`);
|
|
1120
|
+
console.log(` Budget: ${result.budget.toFixed(2)}`);
|
|
1121
|
+
console.log(` Actual: ${result.actual.toFixed(2)}`);
|
|
1122
|
+
console.log(` Variance: ${result.variance.toFixed(2)} (${result.variance_pct}%)`);
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
program.parse(process.argv);
|