chainlesschain 0.47.9 → 0.49.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/bin/chainlesschain.js +0 -0
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AppLayout-6SPt_8Y_.js → AppLayout-Rvi759IS.js} +1 -1
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
- package/src/assets/web-panel/assets/{Dashboard-Br7kCwKJ.js → Dashboard-DBhFxXYQ.js} +2 -2
- package/src/assets/web-panel/assets/{index-tN-8TosE.js → index-uL0cZ8N_.js} +2 -2
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/codegen.js +303 -0
- package/src/commands/collab.js +482 -0
- package/src/commands/crosschain.js +382 -0
- package/src/commands/dbevo.js +388 -0
- package/src/commands/dev.js +411 -0
- package/src/commands/federation.js +427 -0
- package/src/commands/fusion.js +332 -0
- package/src/commands/governance.js +505 -0
- package/src/commands/hardening.js +110 -0
- package/src/commands/incentive.js +373 -0
- package/src/commands/inference.js +304 -0
- package/src/commands/infra.js +361 -0
- package/src/commands/kg.js +371 -0
- package/src/commands/marketplace.js +326 -0
- package/src/commands/mcp.js +97 -18
- package/src/commands/nlprog.js +329 -0
- package/src/commands/ops.js +408 -0
- package/src/commands/perception.js +385 -0
- package/src/commands/pqc.js +34 -0
- package/src/commands/privacy.js +345 -0
- package/src/commands/quantization.js +280 -0
- package/src/commands/recommend.js +336 -0
- package/src/commands/reputation.js +349 -0
- package/src/commands/runtime.js +500 -0
- package/src/commands/sla.js +352 -0
- package/src/commands/stress.js +252 -0
- package/src/commands/tech.js +268 -0
- package/src/commands/tenant.js +576 -0
- package/src/commands/trust.js +366 -0
- package/src/harness/mcp-client.js +330 -54
- package/src/index.js +112 -0
- package/src/lib/aiops.js +523 -0
- package/src/lib/autonomous-developer.js +524 -0
- package/src/lib/code-agent.js +442 -0
- package/src/lib/collaboration-governance.js +556 -0
- package/src/lib/community-governance.js +649 -0
- package/src/lib/content-recommendation.js +600 -0
- package/src/lib/cross-chain.js +669 -0
- package/src/lib/dbevo.js +669 -0
- package/src/lib/decentral-infra.js +445 -0
- package/src/lib/federation-hardening.js +587 -0
- package/src/lib/hardening-manager.js +409 -0
- package/src/lib/inference-network.js +407 -0
- package/src/lib/knowledge-graph.js +530 -0
- package/src/lib/mcp-client.js +3 -0
- package/src/lib/multimodal.js +698 -0
- package/src/lib/nl-programming.js +595 -0
- package/src/lib/perception.js +500 -0
- package/src/lib/pqc-manager.js +141 -9
- package/src/lib/privacy-computing.js +575 -0
- package/src/lib/protocol-fusion.js +535 -0
- package/src/lib/quantization.js +362 -0
- package/src/lib/reputation-optimizer.js +509 -0
- package/src/lib/skill-marketplace.js +397 -0
- package/src/lib/sla-manager.js +484 -0
- package/src/lib/stress-tester.js +383 -0
- package/src/lib/tech-learning-engine.js +651 -0
- package/src/lib/tenant-saas.js +831 -0
- package/src/lib/token-incentive.js +513 -0
- package/src/lib/trust-security.js +473 -0
- package/src/lib/universal-runtime.js +771 -0
- package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +0 -1
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant SaaS commands (Phase 97)
|
|
3
|
+
* chainlesschain tenant plans|metrics|create|configure|list|show|delete|
|
|
4
|
+
* record|usage|subscribe|subscription|cancel|
|
|
5
|
+
* subscriptions|check-quota|stats|export|import
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import { logger } from "../lib/logger.js";
|
|
11
|
+
import { bootstrap, shutdown } from "../runtime/bootstrap.js";
|
|
12
|
+
import {
|
|
13
|
+
ensureTenantTables,
|
|
14
|
+
listPlans,
|
|
15
|
+
listMetrics,
|
|
16
|
+
createTenant,
|
|
17
|
+
configureTenant,
|
|
18
|
+
getTenant,
|
|
19
|
+
listTenants,
|
|
20
|
+
deleteTenant,
|
|
21
|
+
recordUsage,
|
|
22
|
+
getUsage,
|
|
23
|
+
subscribe,
|
|
24
|
+
getActiveSubscription,
|
|
25
|
+
cancelSubscription,
|
|
26
|
+
listSubscriptions,
|
|
27
|
+
checkQuota,
|
|
28
|
+
getSaasStats,
|
|
29
|
+
exportTenant,
|
|
30
|
+
importTenant,
|
|
31
|
+
} from "../lib/tenant-saas.js";
|
|
32
|
+
|
|
33
|
+
function _dbFromCtx(ctx) {
|
|
34
|
+
if (!ctx.db) {
|
|
35
|
+
logger.error("Database not available");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
const db = ctx.db.getDatabase();
|
|
39
|
+
ensureTenantTables(db);
|
|
40
|
+
return db;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function _printTenant(t) {
|
|
44
|
+
logger.log(` ${chalk.bold("ID:")} ${chalk.cyan(t.id)}`);
|
|
45
|
+
logger.log(` ${chalk.bold("Name:")} ${t.name}`);
|
|
46
|
+
logger.log(` ${chalk.bold("Slug:")} ${chalk.yellow(t.slug)}`);
|
|
47
|
+
logger.log(` ${chalk.bold("Plan:")} ${chalk.magenta(t.plan)}`);
|
|
48
|
+
logger.log(` ${chalk.bold("Status:")} ${t.status}`);
|
|
49
|
+
if (t.ownerId) {
|
|
50
|
+
logger.log(` ${chalk.bold("Owner:")} ${t.ownerId}`);
|
|
51
|
+
}
|
|
52
|
+
if (t.config) {
|
|
53
|
+
logger.log(` ${chalk.bold("Config:")} ${JSON.stringify(t.config)}`);
|
|
54
|
+
}
|
|
55
|
+
if (t.deletedAt) {
|
|
56
|
+
logger.log(
|
|
57
|
+
` ${chalk.bold("Deleted:")} ${new Date(t.deletedAt).toISOString()}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function _printSubscription(sub) {
|
|
63
|
+
logger.log(` ${chalk.bold("ID:")} ${chalk.cyan(sub.id)}`);
|
|
64
|
+
logger.log(` ${chalk.bold("Plan:")} ${chalk.magenta(sub.plan)}`);
|
|
65
|
+
logger.log(` ${chalk.bold("Status:")} ${sub.status}`);
|
|
66
|
+
logger.log(
|
|
67
|
+
` ${chalk.bold("Started:")} ${new Date(sub.startedAt).toISOString()}`,
|
|
68
|
+
);
|
|
69
|
+
if (sub.expiresAt) {
|
|
70
|
+
logger.log(
|
|
71
|
+
` ${chalk.bold("Expires:")} ${new Date(sub.expiresAt).toISOString()}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (sub.amount !== null && sub.amount !== undefined) {
|
|
75
|
+
logger.log(` ${chalk.bold("Amount:")} ${sub.amount}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function registerTenantCommand(program) {
|
|
80
|
+
const tenant = program
|
|
81
|
+
.command("tenant")
|
|
82
|
+
.description(
|
|
83
|
+
"Multi-tenant SaaS — tenants, usage metering, subscriptions, quotas",
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
tenant
|
|
87
|
+
.command("plans")
|
|
88
|
+
.description("List available subscription plans")
|
|
89
|
+
.option("--json", "Output as JSON")
|
|
90
|
+
.action((options) => {
|
|
91
|
+
const plans = listPlans();
|
|
92
|
+
if (options.json) {
|
|
93
|
+
console.log(JSON.stringify(plans, null, 2));
|
|
94
|
+
} else {
|
|
95
|
+
for (const p of plans) {
|
|
96
|
+
const fee = p.monthlyFee === null ? "custom" : `¥${p.monthlyFee}/mo`;
|
|
97
|
+
logger.log(
|
|
98
|
+
` ${chalk.cyan(p.id.padEnd(11))} ${chalk.yellow(p.name.padEnd(11))} ${fee}`,
|
|
99
|
+
);
|
|
100
|
+
const q = p.quotas;
|
|
101
|
+
const fmt = (v) => (v === null ? "unlimited" : v);
|
|
102
|
+
logger.log(
|
|
103
|
+
` api_calls: ${fmt(q.api_calls)}, storage: ${fmt(q.storage_bytes)}, ai: ${fmt(q.ai_requests)}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
tenant
|
|
110
|
+
.command("metrics")
|
|
111
|
+
.description("List tracked usage metrics")
|
|
112
|
+
.option("--json", "Output as JSON")
|
|
113
|
+
.action((options) => {
|
|
114
|
+
const metrics = listMetrics();
|
|
115
|
+
if (options.json) {
|
|
116
|
+
console.log(JSON.stringify(metrics, null, 2));
|
|
117
|
+
} else {
|
|
118
|
+
for (const m of metrics) {
|
|
119
|
+
logger.log(` ${chalk.cyan(m.id.padEnd(16))} ${m.name} (${m.unit})`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
tenant
|
|
125
|
+
.command("create <name> <slug>")
|
|
126
|
+
.description("Create a tenant")
|
|
127
|
+
.option(
|
|
128
|
+
"-p, --plan <plan>",
|
|
129
|
+
"Plan id (free/starter/pro/enterprise)",
|
|
130
|
+
"free",
|
|
131
|
+
)
|
|
132
|
+
.option("-o, --owner <id>", "Owner user id")
|
|
133
|
+
.option("-c, --config <json>", "Config JSON")
|
|
134
|
+
.option("--json", "Output as JSON")
|
|
135
|
+
.action(async (name, slug, options) => {
|
|
136
|
+
try {
|
|
137
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
138
|
+
const db = _dbFromCtx(ctx);
|
|
139
|
+
const config = options.config ? JSON.parse(options.config) : null;
|
|
140
|
+
const t = createTenant(db, {
|
|
141
|
+
name,
|
|
142
|
+
slug,
|
|
143
|
+
plan: options.plan,
|
|
144
|
+
ownerId: options.owner,
|
|
145
|
+
config,
|
|
146
|
+
});
|
|
147
|
+
if (options.json) {
|
|
148
|
+
console.log(JSON.stringify(t, null, 2));
|
|
149
|
+
} else {
|
|
150
|
+
logger.success(`Tenant created: ${name}`);
|
|
151
|
+
_printTenant(t);
|
|
152
|
+
}
|
|
153
|
+
await shutdown();
|
|
154
|
+
} catch (err) {
|
|
155
|
+
logger.error(`Failed: ${err.message}`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
tenant
|
|
161
|
+
.command("configure <tenant-id>")
|
|
162
|
+
.description("Update tenant config / plan / status / name")
|
|
163
|
+
.option("-c, --config <json>", "Config JSON")
|
|
164
|
+
.option("-p, --plan <plan>", "Change plan")
|
|
165
|
+
.option("-s, --status <status>", "active | suspended")
|
|
166
|
+
.option("-n, --name <name>", "Rename tenant")
|
|
167
|
+
.option("--json", "Output as JSON")
|
|
168
|
+
.action(async (tenantId, options) => {
|
|
169
|
+
try {
|
|
170
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
171
|
+
const db = _dbFromCtx(ctx);
|
|
172
|
+
const updates = {};
|
|
173
|
+
if (options.config !== undefined) {
|
|
174
|
+
updates.config = JSON.parse(options.config);
|
|
175
|
+
}
|
|
176
|
+
if (options.plan !== undefined) updates.plan = options.plan;
|
|
177
|
+
if (options.status !== undefined) updates.status = options.status;
|
|
178
|
+
if (options.name !== undefined) updates.name = options.name;
|
|
179
|
+
const t = configureTenant(db, tenantId, updates);
|
|
180
|
+
if (options.json) {
|
|
181
|
+
console.log(JSON.stringify(t, null, 2));
|
|
182
|
+
} else {
|
|
183
|
+
logger.success("Tenant updated");
|
|
184
|
+
_printTenant(t);
|
|
185
|
+
}
|
|
186
|
+
await shutdown();
|
|
187
|
+
} catch (err) {
|
|
188
|
+
logger.error(`Failed: ${err.message}`);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
tenant
|
|
194
|
+
.command("list")
|
|
195
|
+
.description("List tenants")
|
|
196
|
+
.option("-s, --status <status>", "Filter by status")
|
|
197
|
+
.option("-p, --plan <plan>", "Filter by plan")
|
|
198
|
+
.option("-o, --owner <substr>", "Filter by owner substring")
|
|
199
|
+
.option("--limit <n>", "Maximum entries", parseInt, 50)
|
|
200
|
+
.option("--json", "Output as JSON")
|
|
201
|
+
.action(async (options) => {
|
|
202
|
+
try {
|
|
203
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
204
|
+
_dbFromCtx(ctx);
|
|
205
|
+
const rows = listTenants({
|
|
206
|
+
status: options.status,
|
|
207
|
+
plan: options.plan,
|
|
208
|
+
ownerSubstr: options.owner,
|
|
209
|
+
limit: options.limit,
|
|
210
|
+
});
|
|
211
|
+
if (options.json) {
|
|
212
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
213
|
+
} else if (rows.length === 0) {
|
|
214
|
+
logger.info("No tenants.");
|
|
215
|
+
} else {
|
|
216
|
+
for (const t of rows) {
|
|
217
|
+
logger.log(
|
|
218
|
+
` ${chalk.cyan(t.id.slice(0, 8))} ${chalk.yellow(t.slug.padEnd(20))} ${chalk.magenta(t.plan.padEnd(11))} ${t.status.padEnd(9)} ${t.name}`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
await shutdown();
|
|
223
|
+
} catch (err) {
|
|
224
|
+
logger.error(`Failed: ${err.message}`);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
tenant
|
|
230
|
+
.command("show <tenant-id>")
|
|
231
|
+
.description("Show tenant details")
|
|
232
|
+
.option("--json", "Output as JSON")
|
|
233
|
+
.action(async (tenantId, options) => {
|
|
234
|
+
try {
|
|
235
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
236
|
+
_dbFromCtx(ctx);
|
|
237
|
+
const t = getTenant(tenantId);
|
|
238
|
+
if (!t) {
|
|
239
|
+
logger.error(`Tenant not found: ${tenantId}`);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
const active = getActiveSubscription(tenantId);
|
|
243
|
+
if (options.json) {
|
|
244
|
+
console.log(
|
|
245
|
+
JSON.stringify({ tenant: t, activeSubscription: active }, null, 2),
|
|
246
|
+
);
|
|
247
|
+
} else {
|
|
248
|
+
_printTenant(t);
|
|
249
|
+
if (active) {
|
|
250
|
+
logger.log("");
|
|
251
|
+
logger.log(chalk.bold("Active subscription:"));
|
|
252
|
+
_printSubscription(active);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
await shutdown();
|
|
256
|
+
} catch (err) {
|
|
257
|
+
logger.error(`Failed: ${err.message}`);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
tenant
|
|
263
|
+
.command("delete <tenant-id>")
|
|
264
|
+
.description(
|
|
265
|
+
"Delete a tenant (soft by default; --hard cascades everything)",
|
|
266
|
+
)
|
|
267
|
+
.option("--hard", "Hard delete — removes all data")
|
|
268
|
+
.action(async (tenantId, options) => {
|
|
269
|
+
try {
|
|
270
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
271
|
+
const db = _dbFromCtx(ctx);
|
|
272
|
+
const result = deleteTenant(db, tenantId, {
|
|
273
|
+
hardDelete: !!options.hard,
|
|
274
|
+
});
|
|
275
|
+
if (result.hard) {
|
|
276
|
+
logger.success(`Tenant hard-deleted (cascaded): ${tenantId}`);
|
|
277
|
+
} else {
|
|
278
|
+
logger.success(`Tenant soft-deleted: ${tenantId}`);
|
|
279
|
+
}
|
|
280
|
+
await shutdown();
|
|
281
|
+
} catch (err) {
|
|
282
|
+
logger.error(`Failed: ${err.message}`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
tenant
|
|
288
|
+
.command("record <tenant-id> <metric> <value>")
|
|
289
|
+
.description("Record a usage sample")
|
|
290
|
+
.option("-P, --period <period>", "Period (YYYY-MM), defaults to current")
|
|
291
|
+
.option("--json", "Output as JSON")
|
|
292
|
+
.action(async (tenantId, metric, value, options) => {
|
|
293
|
+
try {
|
|
294
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
295
|
+
const db = _dbFromCtx(ctx);
|
|
296
|
+
const rec = recordUsage(db, tenantId, metric, parseFloat(value), {
|
|
297
|
+
period: options.period,
|
|
298
|
+
});
|
|
299
|
+
if (options.json) {
|
|
300
|
+
console.log(JSON.stringify(rec, null, 2));
|
|
301
|
+
} else {
|
|
302
|
+
logger.success(
|
|
303
|
+
`Recorded ${rec.value} ${rec.metric} for period ${rec.period}`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
await shutdown();
|
|
307
|
+
} catch (err) {
|
|
308
|
+
logger.error(`Failed: ${err.message}`);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
tenant
|
|
314
|
+
.command("usage <tenant-id>")
|
|
315
|
+
.description("Aggregate usage for a tenant")
|
|
316
|
+
.option("-P, --period <period>", "Filter by period (YYYY-MM)")
|
|
317
|
+
.option("-m, --metric <metric>", "Filter by metric")
|
|
318
|
+
.option("--json", "Output as JSON")
|
|
319
|
+
.action(async (tenantId, options) => {
|
|
320
|
+
try {
|
|
321
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
322
|
+
_dbFromCtx(ctx);
|
|
323
|
+
const u = getUsage(tenantId, {
|
|
324
|
+
period: options.period,
|
|
325
|
+
metric: options.metric,
|
|
326
|
+
});
|
|
327
|
+
if (options.json) {
|
|
328
|
+
console.log(JSON.stringify(u, null, 2));
|
|
329
|
+
} else {
|
|
330
|
+
logger.log(
|
|
331
|
+
`${chalk.bold("Period:")} ${u.period || "<all>"} ${chalk.bold("Records:")} ${u.recordCount}`,
|
|
332
|
+
);
|
|
333
|
+
for (const [m, v] of Object.entries(u.byMetric)) {
|
|
334
|
+
logger.log(` ${chalk.cyan(m.padEnd(16))} ${v}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
await shutdown();
|
|
338
|
+
} catch (err) {
|
|
339
|
+
logger.error(`Failed: ${err.message}`);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
tenant
|
|
345
|
+
.command("subscribe <tenant-id>")
|
|
346
|
+
.description("Start a new subscription (cancels any prior active one)")
|
|
347
|
+
.requiredOption("-p, --plan <plan>", "Plan id")
|
|
348
|
+
.option("-a, --amount <n>", "Override amount", parseFloat)
|
|
349
|
+
.option("-d, --duration-ms <ms>", "Duration in ms (default 30d)", parseInt)
|
|
350
|
+
.option("--json", "Output as JSON")
|
|
351
|
+
.action(async (tenantId, options) => {
|
|
352
|
+
try {
|
|
353
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
354
|
+
const db = _dbFromCtx(ctx);
|
|
355
|
+
const sub = subscribe(db, tenantId, options.plan, {
|
|
356
|
+
amount: options.amount,
|
|
357
|
+
durationMs: options.durationMs,
|
|
358
|
+
});
|
|
359
|
+
if (options.json) {
|
|
360
|
+
console.log(JSON.stringify(sub, null, 2));
|
|
361
|
+
} else {
|
|
362
|
+
logger.success(`Subscribed to ${sub.plan}`);
|
|
363
|
+
_printSubscription(sub);
|
|
364
|
+
}
|
|
365
|
+
await shutdown();
|
|
366
|
+
} catch (err) {
|
|
367
|
+
logger.error(`Failed: ${err.message}`);
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
tenant
|
|
373
|
+
.command("subscription <tenant-id>")
|
|
374
|
+
.description("Show the active subscription for a tenant")
|
|
375
|
+
.option("--json", "Output as JSON")
|
|
376
|
+
.action(async (tenantId, options) => {
|
|
377
|
+
try {
|
|
378
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
379
|
+
_dbFromCtx(ctx);
|
|
380
|
+
const sub = getActiveSubscription(tenantId);
|
|
381
|
+
if (!sub) {
|
|
382
|
+
if (options.json) console.log("null");
|
|
383
|
+
else logger.info("No active subscription");
|
|
384
|
+
await shutdown();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (options.json) {
|
|
388
|
+
console.log(JSON.stringify(sub, null, 2));
|
|
389
|
+
} else {
|
|
390
|
+
_printSubscription(sub);
|
|
391
|
+
}
|
|
392
|
+
await shutdown();
|
|
393
|
+
} catch (err) {
|
|
394
|
+
logger.error(`Failed: ${err.message}`);
|
|
395
|
+
process.exit(1);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
tenant
|
|
400
|
+
.command("cancel <tenant-id>")
|
|
401
|
+
.description("Cancel the active subscription")
|
|
402
|
+
.action(async (tenantId) => {
|
|
403
|
+
try {
|
|
404
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
405
|
+
const db = _dbFromCtx(ctx);
|
|
406
|
+
const sub = cancelSubscription(db, tenantId);
|
|
407
|
+
logger.success(`Subscription ${sub.id} cancelled`);
|
|
408
|
+
await shutdown();
|
|
409
|
+
} catch (err) {
|
|
410
|
+
logger.error(`Failed: ${err.message}`);
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
tenant
|
|
416
|
+
.command("subscriptions")
|
|
417
|
+
.description("List subscriptions")
|
|
418
|
+
.option("-t, --tenant-id <id>", "Filter by tenant")
|
|
419
|
+
.option("-s, --status <status>", "Filter by status")
|
|
420
|
+
.option("-p, --plan <plan>", "Filter by plan")
|
|
421
|
+
.option("--limit <n>", "Maximum entries", parseInt, 50)
|
|
422
|
+
.option("--json", "Output as JSON")
|
|
423
|
+
.action(async (options) => {
|
|
424
|
+
try {
|
|
425
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
426
|
+
_dbFromCtx(ctx);
|
|
427
|
+
const rows = listSubscriptions({
|
|
428
|
+
tenantId: options.tenantId,
|
|
429
|
+
status: options.status,
|
|
430
|
+
plan: options.plan,
|
|
431
|
+
limit: options.limit,
|
|
432
|
+
});
|
|
433
|
+
if (options.json) {
|
|
434
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
435
|
+
} else if (rows.length === 0) {
|
|
436
|
+
logger.info("No subscriptions.");
|
|
437
|
+
} else {
|
|
438
|
+
for (const s of rows) {
|
|
439
|
+
logger.log(
|
|
440
|
+
` ${chalk.cyan(s.id.slice(0, 8))} ${chalk.magenta(s.plan.padEnd(11))} ${s.status.padEnd(10)} tenant=${s.tenantId.slice(0, 8)}`,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
await shutdown();
|
|
445
|
+
} catch (err) {
|
|
446
|
+
logger.error(`Failed: ${err.message}`);
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
tenant
|
|
452
|
+
.command("check-quota <tenant-id> <metric>")
|
|
453
|
+
.description("Check a tenant's quota usage against their active plan")
|
|
454
|
+
.option("-P, --period <period>", "Period (YYYY-MM)")
|
|
455
|
+
.option("--json", "Output as JSON")
|
|
456
|
+
.action(async (tenantId, metric, options) => {
|
|
457
|
+
try {
|
|
458
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
459
|
+
_dbFromCtx(ctx);
|
|
460
|
+
const q = checkQuota(tenantId, metric, { period: options.period });
|
|
461
|
+
if (options.json) {
|
|
462
|
+
console.log(JSON.stringify(q, null, 2));
|
|
463
|
+
} else {
|
|
464
|
+
const limitStr = q.unlimited ? "unlimited" : String(q.limit);
|
|
465
|
+
const remainingStr = q.unlimited ? "unlimited" : String(q.remaining);
|
|
466
|
+
const color = q.exceeded ? chalk.red : chalk.green;
|
|
467
|
+
logger.log(
|
|
468
|
+
` ${chalk.bold("Plan:")} ${q.plan} ${chalk.bold("Period:")} ${q.period}`,
|
|
469
|
+
);
|
|
470
|
+
logger.log(
|
|
471
|
+
` ${chalk.bold("Metric:")} ${chalk.cyan(q.metric)} ${chalk.bold("Used:")} ${q.used} / ${limitStr}`,
|
|
472
|
+
);
|
|
473
|
+
logger.log(
|
|
474
|
+
` ${chalk.bold("Remaining:")} ${remainingStr} ${color(q.exceeded ? "EXCEEDED" : "OK")}`,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
await shutdown();
|
|
478
|
+
} catch (err) {
|
|
479
|
+
logger.error(`Failed: ${err.message}`);
|
|
480
|
+
process.exit(1);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
tenant
|
|
485
|
+
.command("stats")
|
|
486
|
+
.description("SaaS-wide statistics")
|
|
487
|
+
.option("--json", "Output as JSON")
|
|
488
|
+
.action(async (options) => {
|
|
489
|
+
try {
|
|
490
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
491
|
+
_dbFromCtx(ctx);
|
|
492
|
+
const stats = getSaasStats();
|
|
493
|
+
if (options.json) {
|
|
494
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
495
|
+
} else {
|
|
496
|
+
logger.log(`${chalk.bold("Tenants:")} ${stats.tenantCount}`);
|
|
497
|
+
logger.log(
|
|
498
|
+
`${chalk.bold("Subscriptions:")} ${stats.subscriptionCount} (active: ${stats.activeSubscriptions})`,
|
|
499
|
+
);
|
|
500
|
+
logger.log(
|
|
501
|
+
`${chalk.bold("Usage records:")} ${stats.usageRecordCount}`,
|
|
502
|
+
);
|
|
503
|
+
logger.log(chalk.bold("By status:"));
|
|
504
|
+
for (const [s, n] of Object.entries(stats.byStatus)) {
|
|
505
|
+
logger.log(` ${s.padEnd(12)} ${n}`);
|
|
506
|
+
}
|
|
507
|
+
logger.log(chalk.bold("By plan:"));
|
|
508
|
+
for (const [p, n] of Object.entries(stats.byPlan)) {
|
|
509
|
+
logger.log(` ${p.padEnd(12)} ${n}`);
|
|
510
|
+
}
|
|
511
|
+
logger.log(chalk.bold("Total usage:"));
|
|
512
|
+
for (const [m, v] of Object.entries(stats.totalUsage)) {
|
|
513
|
+
logger.log(` ${m.padEnd(16)} ${v}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
await shutdown();
|
|
517
|
+
} catch (err) {
|
|
518
|
+
logger.error(`Failed: ${err.message}`);
|
|
519
|
+
process.exit(1);
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
tenant
|
|
524
|
+
.command("export <tenant-id> [output-file]")
|
|
525
|
+
.description("Export tenant + subscriptions + usage as JSON")
|
|
526
|
+
.action(async (tenantId, outputFile) => {
|
|
527
|
+
try {
|
|
528
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
529
|
+
_dbFromCtx(ctx);
|
|
530
|
+
const dump = exportTenant(tenantId);
|
|
531
|
+
const json = JSON.stringify(dump, null, 2);
|
|
532
|
+
if (outputFile) {
|
|
533
|
+
fs.writeFileSync(outputFile, json, "utf-8");
|
|
534
|
+
logger.success(`Exported to ${outputFile}`);
|
|
535
|
+
} else {
|
|
536
|
+
console.log(json);
|
|
537
|
+
}
|
|
538
|
+
await shutdown();
|
|
539
|
+
} catch (err) {
|
|
540
|
+
logger.error(`Failed: ${err.message}`);
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
tenant
|
|
546
|
+
.command("import <input-file>")
|
|
547
|
+
.description("Import tenant snapshot from JSON file")
|
|
548
|
+
.action(async (inputFile) => {
|
|
549
|
+
try {
|
|
550
|
+
const ctx = await bootstrap({ verbose: program.opts().verbose });
|
|
551
|
+
const db = _dbFromCtx(ctx);
|
|
552
|
+
const json = fs.readFileSync(inputFile, "utf-8");
|
|
553
|
+
const data = JSON.parse(json);
|
|
554
|
+
const result = importTenant(db, data);
|
|
555
|
+
if (result.tenantId === null) {
|
|
556
|
+
logger.error(
|
|
557
|
+
`Import skipped: ${result.reason || "tenant not importable"}`,
|
|
558
|
+
);
|
|
559
|
+
process.exit(2);
|
|
560
|
+
}
|
|
561
|
+
logger.success(
|
|
562
|
+
`Imported tenant ${result.tenantId} ` +
|
|
563
|
+
`(${result.importedSubscriptions} subs, ${result.importedUsage} usage records)`,
|
|
564
|
+
);
|
|
565
|
+
if (result.skippedSubscriptions || result.skippedUsage) {
|
|
566
|
+
logger.info(
|
|
567
|
+
`Skipped: ${result.skippedSubscriptions} subs, ${result.skippedUsage} usage`,
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
await shutdown();
|
|
571
|
+
} catch (err) {
|
|
572
|
+
logger.error(`Failed: ${err.message}`);
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
}
|