@layers/amba 4.0.3 → 4.0.4
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/dist/api-client.d.ts +32 -0
- package/dist/bundle.d.ts +86 -0
- package/dist/commands/functions-logs.d.ts +2 -0
- package/dist/commands/functions.d.ts +21 -2
- package/dist/commands/iac.d.ts +116 -0
- package/dist/commands/monetization.d.ts +36 -0
- package/dist/commands/payments.d.ts +33 -0
- package/dist/index.js +1434 -33
- package/package.json +3 -3
- package/skill-bundle/SKILL.md +2 -0
- package/skill-bundle/references/infrastructure.md +20 -17
package/dist/index.js
CHANGED
|
@@ -8,11 +8,460 @@ import { createServer } from "node:http";
|
|
|
8
8
|
import open from "open";
|
|
9
9
|
import { createHash, randomBytes } from "node:crypto";
|
|
10
10
|
import { createInterface } from "node:readline";
|
|
11
|
-
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
12
|
import { createWriteStream, watch } from "node:fs";
|
|
13
13
|
import { spawn } from "node:child_process";
|
|
14
14
|
import { build } from "esbuild";
|
|
15
|
-
|
|
15
|
+
new Map([
|
|
16
|
+
{
|
|
17
|
+
type: "user.created",
|
|
18
|
+
plane: "app",
|
|
19
|
+
description: "An app user (end user) was created in the project.",
|
|
20
|
+
payloadExample: {
|
|
21
|
+
app_user_id: "usr_123",
|
|
22
|
+
external_id: "auth0|abc",
|
|
23
|
+
created_at: "2026-06-03T00:00:00Z"
|
|
24
|
+
},
|
|
25
|
+
status: "live"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
type: "user.updated",
|
|
29
|
+
plane: "app",
|
|
30
|
+
description: "An app user's profile or attributes changed.",
|
|
31
|
+
payloadExample: {
|
|
32
|
+
app_user_id: "usr_123",
|
|
33
|
+
changed: ["display_name"]
|
|
34
|
+
},
|
|
35
|
+
status: "live"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: "user.deleted",
|
|
39
|
+
plane: "app",
|
|
40
|
+
description: "An app user was deleted.",
|
|
41
|
+
payloadExample: { app_user_id: "usr_123" },
|
|
42
|
+
status: "live"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: "user.reset",
|
|
46
|
+
plane: "app",
|
|
47
|
+
description: "An app user's gamification, economy, engagement and participation state was reset to zero (the user row itself was kept).",
|
|
48
|
+
payloadExample: {
|
|
49
|
+
app_user_id: "usr_123",
|
|
50
|
+
cleared: {
|
|
51
|
+
user_xp: 1,
|
|
52
|
+
xp_ledger: 12
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
status: "live"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: "engagement.push.delivered",
|
|
59
|
+
plane: "app",
|
|
60
|
+
description: "A push notification was delivered to a device.",
|
|
61
|
+
payloadExample: {
|
|
62
|
+
app_user_id: "usr_123",
|
|
63
|
+
campaign_id: "cmp_1",
|
|
64
|
+
delivered_at: "2026-06-03T00:00:00Z"
|
|
65
|
+
},
|
|
66
|
+
status: "live"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
type: "engagement.push.failed",
|
|
70
|
+
plane: "app",
|
|
71
|
+
description: "A push notification failed to deliver.",
|
|
72
|
+
payloadExample: {
|
|
73
|
+
app_user_id: "usr_123",
|
|
74
|
+
campaign_id: "cmp_1",
|
|
75
|
+
reason: "unregistered_token"
|
|
76
|
+
},
|
|
77
|
+
status: "live"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: "email.sent",
|
|
81
|
+
plane: "app",
|
|
82
|
+
description: "A transactional email was accepted by the provider for delivery.",
|
|
83
|
+
payloadExample: {
|
|
84
|
+
app_user_id: "usr_123",
|
|
85
|
+
template: "welcome",
|
|
86
|
+
to: "a@b.com"
|
|
87
|
+
},
|
|
88
|
+
status: "live"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
type: "email.failed",
|
|
92
|
+
plane: "app",
|
|
93
|
+
description: "A transactional email failed to send.",
|
|
94
|
+
payloadExample: {
|
|
95
|
+
app_user_id: "usr_123",
|
|
96
|
+
template: "welcome",
|
|
97
|
+
reason: "bounce"
|
|
98
|
+
},
|
|
99
|
+
status: "live"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
type: "email.suppressed",
|
|
103
|
+
plane: "app",
|
|
104
|
+
description: "An email was suppressed (recipient on the suppression list).",
|
|
105
|
+
payloadExample: {
|
|
106
|
+
app_user_id: "usr_123",
|
|
107
|
+
to: "a@b.com",
|
|
108
|
+
reason: "unsubscribed"
|
|
109
|
+
},
|
|
110
|
+
status: "live"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
type: "gamification.xp.granted",
|
|
114
|
+
plane: "app",
|
|
115
|
+
description: "XP was granted to a user.",
|
|
116
|
+
payloadExample: {
|
|
117
|
+
app_user_id: "usr_123",
|
|
118
|
+
amount: 50,
|
|
119
|
+
total_xp: 1250,
|
|
120
|
+
rule: "mission_completed",
|
|
121
|
+
transaction_id: "xpl_abc"
|
|
122
|
+
},
|
|
123
|
+
status: "live"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
type: "gamification.level.up",
|
|
127
|
+
plane: "app",
|
|
128
|
+
description: "A user crossed an XP threshold into a new level.",
|
|
129
|
+
payloadExample: {
|
|
130
|
+
app_user_id: "usr_123",
|
|
131
|
+
level: 7,
|
|
132
|
+
previous_level: 6
|
|
133
|
+
},
|
|
134
|
+
status: "live"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
type: "gamification.achievement.unlocked",
|
|
138
|
+
plane: "app",
|
|
139
|
+
description: "A user unlocked an achievement.",
|
|
140
|
+
payloadExample: {
|
|
141
|
+
app_user_id: "usr_123",
|
|
142
|
+
achievement_id: "first_win",
|
|
143
|
+
unlocked_at: "2026-06-03T00:00:00Z"
|
|
144
|
+
},
|
|
145
|
+
status: "live"
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
type: "gamification.streak.advanced",
|
|
149
|
+
plane: "app",
|
|
150
|
+
description: "A user's streak advanced (a qualifying action extended it).",
|
|
151
|
+
payloadExample: {
|
|
152
|
+
app_user_id: "usr_123",
|
|
153
|
+
streak_id: "daily",
|
|
154
|
+
count: 12
|
|
155
|
+
},
|
|
156
|
+
status: "live"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
type: "gamification.streak.broken",
|
|
160
|
+
plane: "app",
|
|
161
|
+
description: "A user's streak reset to zero after a missed window.",
|
|
162
|
+
payloadExample: {
|
|
163
|
+
app_user_id: "usr_123",
|
|
164
|
+
streak_id: "daily",
|
|
165
|
+
previous_count: 12
|
|
166
|
+
},
|
|
167
|
+
status: "live"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
type: "gamification.challenge.completed",
|
|
171
|
+
plane: "app",
|
|
172
|
+
description: "A user completed a challenge.",
|
|
173
|
+
payloadExample: {
|
|
174
|
+
app_user_id: "usr_123",
|
|
175
|
+
challenge_id: "ch_1"
|
|
176
|
+
},
|
|
177
|
+
status: "live"
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
type: "economy.currency.granted",
|
|
181
|
+
plane: "app",
|
|
182
|
+
description: "Currency was granted to a user.",
|
|
183
|
+
payloadExample: {
|
|
184
|
+
app_user_id: "usr_123",
|
|
185
|
+
currency: "coins",
|
|
186
|
+
amount: 100,
|
|
187
|
+
balance: 350,
|
|
188
|
+
transaction_id: "ctx_abc"
|
|
189
|
+
},
|
|
190
|
+
status: "live"
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
type: "economy.currency.spent",
|
|
194
|
+
plane: "app",
|
|
195
|
+
description: "A user spent currency.",
|
|
196
|
+
payloadExample: {
|
|
197
|
+
app_user_id: "usr_123",
|
|
198
|
+
currency: "coins",
|
|
199
|
+
amount: 100,
|
|
200
|
+
balance: 250,
|
|
201
|
+
transaction_id: "ctx_def"
|
|
202
|
+
},
|
|
203
|
+
status: "live"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
type: "economy.catalog.purchased",
|
|
207
|
+
plane: "app",
|
|
208
|
+
description: "A user purchased a catalog item.",
|
|
209
|
+
payloadExample: {
|
|
210
|
+
app_user_id: "usr_123",
|
|
211
|
+
item_id: "sword",
|
|
212
|
+
price: 100,
|
|
213
|
+
currency: "coins"
|
|
214
|
+
},
|
|
215
|
+
status: "live"
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
type: "economy.inventory.granted",
|
|
219
|
+
plane: "app",
|
|
220
|
+
description: "An item was granted into a user inventory.",
|
|
221
|
+
payloadExample: {
|
|
222
|
+
app_user_id: "usr_123",
|
|
223
|
+
item_id: "sword",
|
|
224
|
+
quantity: 1
|
|
225
|
+
},
|
|
226
|
+
status: "live"
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
type: "subscription.started",
|
|
230
|
+
plane: "app",
|
|
231
|
+
description: "A user gained an active subscription / entitlement.",
|
|
232
|
+
payloadExample: {
|
|
233
|
+
app_user_id: "usr_123",
|
|
234
|
+
entitlement_id: "pro",
|
|
235
|
+
product_id: "pro_yearly",
|
|
236
|
+
store: "app_store",
|
|
237
|
+
period_type: "normal"
|
|
238
|
+
},
|
|
239
|
+
status: "live"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
type: "subscription.renewed",
|
|
243
|
+
plane: "app",
|
|
244
|
+
description: "An active subscription renewed for another billing period.",
|
|
245
|
+
payloadExample: {
|
|
246
|
+
app_user_id: "usr_123",
|
|
247
|
+
entitlement_id: "pro",
|
|
248
|
+
product_id: "pro_yearly",
|
|
249
|
+
store: "app_store"
|
|
250
|
+
},
|
|
251
|
+
status: "live"
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
type: "subscription.expired",
|
|
255
|
+
plane: "app",
|
|
256
|
+
description: "A subscription lapsed / was revoked and access ended.",
|
|
257
|
+
payloadExample: {
|
|
258
|
+
app_user_id: "usr_123",
|
|
259
|
+
entitlement_id: "pro",
|
|
260
|
+
product_id: "pro_yearly"
|
|
261
|
+
},
|
|
262
|
+
status: "live"
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
type: "entitlement.granted",
|
|
266
|
+
plane: "app",
|
|
267
|
+
description: "An entitlement became active for a user (any source).",
|
|
268
|
+
payloadExample: {
|
|
269
|
+
app_user_id: "usr_123",
|
|
270
|
+
entitlement_id: "pro",
|
|
271
|
+
product_id: "pro_yearly",
|
|
272
|
+
source: "webhook"
|
|
273
|
+
},
|
|
274
|
+
status: "live"
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
type: "entitlement.revoked",
|
|
278
|
+
plane: "app",
|
|
279
|
+
description: "An entitlement became inactive for a user (any source).",
|
|
280
|
+
payloadExample: {
|
|
281
|
+
app_user_id: "usr_123",
|
|
282
|
+
entitlement_id: "pro",
|
|
283
|
+
source: "manual"
|
|
284
|
+
},
|
|
285
|
+
status: "live"
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
type: "social.friendship.created",
|
|
289
|
+
plane: "app",
|
|
290
|
+
description: "A friendship was established between two users.",
|
|
291
|
+
payloadExample: {
|
|
292
|
+
app_user_id: "usr_123",
|
|
293
|
+
friend_id: "usr_456"
|
|
294
|
+
},
|
|
295
|
+
status: "live"
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
type: "social.message.sent",
|
|
299
|
+
plane: "app",
|
|
300
|
+
description: "A message was sent in a conversation.",
|
|
301
|
+
payloadExample: {
|
|
302
|
+
conversation_id: "cnv_1",
|
|
303
|
+
sender_id: "usr_123"
|
|
304
|
+
},
|
|
305
|
+
status: "live"
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
type: "social.review.created",
|
|
309
|
+
plane: "app",
|
|
310
|
+
description: "A review was submitted.",
|
|
311
|
+
payloadExample: {
|
|
312
|
+
app_user_id: "usr_123",
|
|
313
|
+
rating: 5,
|
|
314
|
+
target_id: "item_1"
|
|
315
|
+
},
|
|
316
|
+
status: "live"
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
type: "content.published",
|
|
320
|
+
plane: "app",
|
|
321
|
+
description: "A content item was published / went live.",
|
|
322
|
+
payloadExample: {
|
|
323
|
+
item_id: "post_1",
|
|
324
|
+
library: "news"
|
|
325
|
+
},
|
|
326
|
+
status: "live"
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
type: "moderation.flagged",
|
|
330
|
+
plane: "app",
|
|
331
|
+
description: "User content entered the moderation queue.",
|
|
332
|
+
payloadExample: {
|
|
333
|
+
queue_id: "mq_1",
|
|
334
|
+
content_type: "message",
|
|
335
|
+
reason: "auto_flag"
|
|
336
|
+
},
|
|
337
|
+
status: "live"
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
type: "moderation.rejected",
|
|
341
|
+
plane: "app",
|
|
342
|
+
description: "Moderated content was rejected.",
|
|
343
|
+
payloadExample: {
|
|
344
|
+
queue_id: "mq_1",
|
|
345
|
+
moderator: "auto"
|
|
346
|
+
},
|
|
347
|
+
status: "live"
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
type: "project.provisioning.started",
|
|
351
|
+
plane: "control",
|
|
352
|
+
description: "Project provisioning began (tenant DB + keys being created).",
|
|
353
|
+
payloadExample: { project_id: "prj_1" },
|
|
354
|
+
status: "live"
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
type: "project.provisioned",
|
|
358
|
+
plane: "control",
|
|
359
|
+
description: "The project finished provisioning and is ready to use.",
|
|
360
|
+
payloadExample: {
|
|
361
|
+
project_id: "prj_1",
|
|
362
|
+
provisioned_at: "2026-06-03T00:00:00Z"
|
|
363
|
+
},
|
|
364
|
+
status: "live"
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
type: "project.provisioning.failed",
|
|
368
|
+
plane: "control",
|
|
369
|
+
description: "Project provisioning failed and was compensated.",
|
|
370
|
+
payloadExample: {
|
|
371
|
+
project_id: "prj_1",
|
|
372
|
+
reason: "neon_quota"
|
|
373
|
+
},
|
|
374
|
+
status: "live"
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
type: "site.deploy.completed",
|
|
378
|
+
plane: "control",
|
|
379
|
+
description: "A static site deploy completed and is serving.",
|
|
380
|
+
payloadExample: {
|
|
381
|
+
site: "marketing",
|
|
382
|
+
deployment_id: "dep_1",
|
|
383
|
+
url: "https://x.app.amba.host",
|
|
384
|
+
file_count: 4,
|
|
385
|
+
serving_configured: true
|
|
386
|
+
},
|
|
387
|
+
status: "live"
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
type: "site.deploy.failed",
|
|
391
|
+
plane: "control",
|
|
392
|
+
description: "A static site deploy failed.",
|
|
393
|
+
payloadExample: {
|
|
394
|
+
project_id: "prj_1",
|
|
395
|
+
site: "marketing",
|
|
396
|
+
reason: "storage_write_failed"
|
|
397
|
+
},
|
|
398
|
+
status: "live"
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
type: "domain.registered",
|
|
402
|
+
plane: "control",
|
|
403
|
+
description: "A domain was registered through Amba.",
|
|
404
|
+
payloadExample: {
|
|
405
|
+
domain: "myapp.com",
|
|
406
|
+
site: "marketing"
|
|
407
|
+
},
|
|
408
|
+
status: "live"
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
type: "domain.cert.issued",
|
|
412
|
+
plane: "control",
|
|
413
|
+
description: "The TLS certificate for an attached domain became active.",
|
|
414
|
+
payloadExample: {
|
|
415
|
+
project_id: "prj_1",
|
|
416
|
+
domain: "app.myapp.com"
|
|
417
|
+
},
|
|
418
|
+
status: "live"
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
type: "domain.connected",
|
|
422
|
+
plane: "control",
|
|
423
|
+
description: "A domain is fully connected to a site and serving.",
|
|
424
|
+
payloadExample: {
|
|
425
|
+
project_id: "prj_1",
|
|
426
|
+
domain: "myapp.com",
|
|
427
|
+
site: "marketing"
|
|
428
|
+
},
|
|
429
|
+
status: "live"
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
type: "billing.subscription.updated",
|
|
433
|
+
plane: "control",
|
|
434
|
+
description: "The project's subscription state changed.",
|
|
435
|
+
payloadExample: {
|
|
436
|
+
project_id: "prj_1",
|
|
437
|
+
tier: "pro",
|
|
438
|
+
status: "active"
|
|
439
|
+
},
|
|
440
|
+
status: "live"
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
type: "billing.invoice.paid",
|
|
444
|
+
plane: "control",
|
|
445
|
+
description: "An invoice was paid.",
|
|
446
|
+
payloadExample: {
|
|
447
|
+
project_id: "prj_1",
|
|
448
|
+
invoice_id: "in_1",
|
|
449
|
+
amount_usd: 49
|
|
450
|
+
},
|
|
451
|
+
status: "live"
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
type: "billing.payment_failed",
|
|
455
|
+
plane: "control",
|
|
456
|
+
description: "A payment attempt failed.",
|
|
457
|
+
payloadExample: {
|
|
458
|
+
project_id: "prj_1",
|
|
459
|
+
invoice_id: "in_1",
|
|
460
|
+
reason: "card_declined"
|
|
461
|
+
},
|
|
462
|
+
status: "live"
|
|
463
|
+
}
|
|
464
|
+
].map((e) => [e.type, e]));
|
|
16
465
|
/**
|
|
17
466
|
* Shared email-shape validation.
|
|
18
467
|
*
|
|
@@ -529,7 +978,8 @@ async function deployFunctionViaApi(projectId, input) {
|
|
|
529
978
|
fd.set("script", new Blob([input.bundleCode], { type: "application/javascript+module" }), `${input.name}.js`);
|
|
530
979
|
const metadata = {
|
|
531
980
|
name: input.name,
|
|
532
|
-
...input.rate_limit !== void 0 ? { rate_limit: input.rate_limit } : {}
|
|
981
|
+
...input.rate_limit !== void 0 ? { rate_limit: input.rate_limit } : {},
|
|
982
|
+
...input.public === true ? { public: true } : {}
|
|
533
983
|
};
|
|
534
984
|
fd.set("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" }), "metadata.json");
|
|
535
985
|
return request("POST", `/projects/${projectId}/functions/deploy`, fd);
|
|
@@ -545,6 +995,7 @@ async function getFunctionLogs(projectId, functionName, options = {}) {
|
|
|
545
995
|
if (options.since) params.set("since", options.since);
|
|
546
996
|
if (options.until) params.set("until", options.until);
|
|
547
997
|
if (options.limit !== void 0) params.set("limit", String(options.limit));
|
|
998
|
+
if (options.requestId) params.set("request_id", options.requestId);
|
|
548
999
|
const qs = params.toString();
|
|
549
1000
|
return request("GET", `/projects/${projectId}/functions/${functionName}/logs${qs ? `?${qs}` : ""}`);
|
|
550
1001
|
}
|
|
@@ -672,6 +1123,17 @@ async function deleteFunctionViaApi(projectId, functionName, options) {
|
|
|
672
1123
|
async function listSiteDomains(projectId, siteName) {
|
|
673
1124
|
return request("GET", `/projects/${projectId}/sites/${encodeURIComponent(siteName)}/domains`);
|
|
674
1125
|
}
|
|
1126
|
+
/** GET the current project's declarative configuration bundle. */
|
|
1127
|
+
async function exportPromotionBundle(projectId) {
|
|
1128
|
+
return request("GET", `/projects/${projectId}/promotion/export`);
|
|
1129
|
+
}
|
|
1130
|
+
/** POST a declarative bundle to import it into the project. */
|
|
1131
|
+
async function importPromotionBundle(projectId, bundle, mode) {
|
|
1132
|
+
return request("POST", `/projects/${projectId}/promotion/import`, {
|
|
1133
|
+
bundle,
|
|
1134
|
+
mode
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
675
1137
|
async function validateApiKey(apiKey) {
|
|
676
1138
|
const res = await fetch(`${getApiRoot()}/v1/auth/validate`, {
|
|
677
1139
|
method: "POST",
|
|
@@ -2149,6 +2611,14 @@ Tell the user where their credentials live:
|
|
|
2149
2611
|
|
|
2150
2612
|
**Already have a PAT?** Skip the signup. Call \`amba_developer_me({})\` to verify the Bearer; if it succeeds, either reuse the most recent project (\`amba_projects_list\`) or call \`amba_projects_create({ name: "<app-name>", platform: "all" })\` and then \`amba_api_keys_create\` twice to mint client + server keys for \`environment: "development"\`.
|
|
2151
2613
|
|
|
2614
|
+
**Can't reach \`amba_developer_signup\` — it's not in your tool list, or your harness blocks the call?** Do NOT try to work around it by adding the MCP server yourself, running an installer, or editing your own settings/permissions — refusing to self-modify or widen your own permissions is the correct call, and you should not circumvent it. Instead, hand the developer **one** command to run in **their own** terminal (which is not subject to your sandbox):
|
|
2615
|
+
|
|
2616
|
+
\`\`\`bash
|
|
2617
|
+
npx @layers/amba init
|
|
2618
|
+
\`\`\`
|
|
2619
|
+
|
|
2620
|
+
It creates the account with no browser and writes \`AMBA_PROJECT_ID\` + \`AMBA_CLIENT_KEY\` to \`.env.local\` (and wires this MCP server up for your next session). Ask them to run it, tell you when it's done, then resume from this Step 0 by reading \`.env.local\`. Asking the developer to paste an \`amb_ck_…\` client key is the last-resort lite path — it's enough to wire \`Amba.configure()\` + runtime calls and verify events land, but a client key alone can't do admin provisioning (creating achievements, collections, segments, etc.), which needs the PAT or server key.
|
|
2621
|
+
|
|
2152
2622
|
## Step 1 — Classify the app
|
|
2153
2623
|
|
|
2154
2624
|
Look at what the user told you and at any files they shared. You're trying to pick one of ten presets in 30 seconds, not write a treatise. Inputs:
|
|
@@ -2270,7 +2740,7 @@ SKIPPED (low signal — re-run with /amba <feature> if you want them)
|
|
|
2270
2740
|
NEEDS YOUR INPUT
|
|
2271
2741
|
- Apple Sign In: add the "Sign in with Apple" capability in Xcode > Signing & Capabilities.
|
|
2272
2742
|
- Google Sign In: paste your Google OAuth client ID into amba_projects_update({ google_oauth_client_id: "..." }).
|
|
2273
|
-
- APNs / FCM:
|
|
2743
|
+
- APNs / FCM: configure credentials with amba_integrations_configure (paste the .p8 / service-account JSON inline) before push delivers.
|
|
2274
2744
|
|
|
2275
2745
|
NEXT STEPS
|
|
2276
2746
|
- Paste AMBA_CLIENT_KEY into your build env (already shown above)
|
|
@@ -4081,7 +4551,7 @@ function confirm$1(question) {
|
|
|
4081
4551
|
});
|
|
4082
4552
|
});
|
|
4083
4553
|
}
|
|
4084
|
-
function handleError$
|
|
4554
|
+
function handleError$2(err) {
|
|
4085
4555
|
if (err instanceof ApiClientError) if (err.statusCode === 401 || err.statusCode === 403) console.log(pc.red(" ✗") + " Not authenticated — run `amba login` first.");
|
|
4086
4556
|
else console.log(pc.red(" ✗") + ` ${err.message}`);
|
|
4087
4557
|
else if (err instanceof Error) console.log(pc.red(" ✗") + ` ${err.message}`);
|
|
@@ -4131,7 +4601,7 @@ async function projectsListCommand() {
|
|
|
4131
4601
|
console.log(pc.dim(` ${projects.length} project${projects.length === 1 ? "" : "s"}`));
|
|
4132
4602
|
console.log();
|
|
4133
4603
|
} catch (err) {
|
|
4134
|
-
handleError$
|
|
4604
|
+
handleError$2(err);
|
|
4135
4605
|
}
|
|
4136
4606
|
}
|
|
4137
4607
|
async function projectsCreateCommand(input) {
|
|
@@ -4177,7 +4647,7 @@ async function projectsCreateCommand(input) {
|
|
|
4177
4647
|
console.log(pc.dim(" Next: ") + pc.cyan(`amba projects show ${id}`));
|
|
4178
4648
|
console.log();
|
|
4179
4649
|
} catch (err) {
|
|
4180
|
-
handleError$
|
|
4650
|
+
handleError$2(err);
|
|
4181
4651
|
}
|
|
4182
4652
|
}
|
|
4183
4653
|
async function projectsUpdateCommand(projectId, input) {
|
|
@@ -4203,7 +4673,7 @@ async function projectsUpdateCommand(projectId, input) {
|
|
|
4203
4673
|
console.log();
|
|
4204
4674
|
process.exit(1);
|
|
4205
4675
|
}
|
|
4206
|
-
handleError$
|
|
4676
|
+
handleError$2(err);
|
|
4207
4677
|
}
|
|
4208
4678
|
}
|
|
4209
4679
|
async function projectsShowCommand(projectId) {
|
|
@@ -4221,7 +4691,7 @@ async function projectsShowCommand(projectId) {
|
|
|
4221
4691
|
console.log();
|
|
4222
4692
|
process.exit(1);
|
|
4223
4693
|
}
|
|
4224
|
-
handleError$
|
|
4694
|
+
handleError$2(err);
|
|
4225
4695
|
}
|
|
4226
4696
|
}
|
|
4227
4697
|
async function projectsDeleteCommand(projectId, opts = {}) {
|
|
@@ -4246,7 +4716,7 @@ async function projectsDeleteCommand(projectId, opts = {}) {
|
|
|
4246
4716
|
console.log();
|
|
4247
4717
|
process.exit(1);
|
|
4248
4718
|
}
|
|
4249
|
-
handleError$
|
|
4719
|
+
handleError$2(err);
|
|
4250
4720
|
}
|
|
4251
4721
|
}
|
|
4252
4722
|
//#endregion
|
|
@@ -4989,6 +5459,47 @@ function parseEnv(content) {
|
|
|
4989
5459
|
* empty; customer code is expected to be self-contained.
|
|
4990
5460
|
*/
|
|
4991
5461
|
/**
|
|
5462
|
+
* Conventional entry-point filenames probed (in order) when
|
|
5463
|
+
* `amba functions deploy <dir>` is pointed at a directory rather than a
|
|
5464
|
+
* single file. Mirrors the Node / bundler convention (`index.*`) so a
|
|
5465
|
+
* developer can `amba functions deploy ./my-fn` with an `index.ts` inside.
|
|
5466
|
+
*/
|
|
5467
|
+
const FUNCTION_ENTRY_CANDIDATES = [
|
|
5468
|
+
"index.ts",
|
|
5469
|
+
"index.js",
|
|
5470
|
+
"index.mjs"
|
|
5471
|
+
];
|
|
5472
|
+
/**
|
|
5473
|
+
* Resolve the bundler entry point from a deploy target that may be either a
|
|
5474
|
+
* single file OR a directory.
|
|
5475
|
+
*
|
|
5476
|
+
* - File → returned as-is (single-file back-compat path).
|
|
5477
|
+
* - Dir → resolved by convention to `index.{ts,js,mjs}`, or to an
|
|
5478
|
+
* explicit `entry` (relative to the directory) when provided.
|
|
5479
|
+
*
|
|
5480
|
+
* Throws an actionable error when the target doesn't exist, or when a
|
|
5481
|
+
* directory has no resolvable entry point (and no `--entry` was passed).
|
|
5482
|
+
*/
|
|
5483
|
+
async function resolveFunctionEntry(target, explicitEntry) {
|
|
5484
|
+
const st = await stat(target).catch(() => null);
|
|
5485
|
+
if (!st) throw new Error(`Deploy target not found: ${target}`);
|
|
5486
|
+
if (st.isFile()) {
|
|
5487
|
+
if (explicitEntry) throw new Error(`--entry is only valid when deploying a directory; ${target} is a file. Drop --entry, or point the deploy at the directory containing ${explicitEntry}.`);
|
|
5488
|
+
return target;
|
|
5489
|
+
}
|
|
5490
|
+
if (!st.isDirectory()) throw new Error(`Deploy target is neither a file nor a directory: ${target}`);
|
|
5491
|
+
if (explicitEntry) {
|
|
5492
|
+
const candidate = join(target, explicitEntry);
|
|
5493
|
+
if (!(await stat(candidate).catch(() => null))?.isFile()) throw new Error(`--entry file not found: ${candidate}`);
|
|
5494
|
+
return candidate;
|
|
5495
|
+
}
|
|
5496
|
+
for (const name of FUNCTION_ENTRY_CANDIDATES) {
|
|
5497
|
+
const candidate = join(target, name);
|
|
5498
|
+
if ((await stat(candidate).catch(() => null))?.isFile()) return candidate;
|
|
5499
|
+
}
|
|
5500
|
+
throw new Error(`No entry point found in directory ${target}. Add one of ${FUNCTION_ENTRY_CANDIDATES.join(", ")}, or pass --entry <file> (relative to the directory).`);
|
|
5501
|
+
}
|
|
5502
|
+
/**
|
|
4992
5503
|
* Modules the bundler treats as `external` by default. The Amba
|
|
4993
5504
|
* function runtime exposes zero npm packages — there is no "platform
|
|
4994
5505
|
* stdlib" for customer functions to import. Keep this list empty.
|
|
@@ -5011,6 +5522,91 @@ var BundleSizeError = class extends Error {
|
|
|
5011
5522
|
}
|
|
5012
5523
|
};
|
|
5013
5524
|
/**
|
|
5525
|
+
* The single source-of-truth message for the wrong-handler-shape failure.
|
|
5526
|
+
* Shared between the CLI bundler (pre-upload), the local `dev` runner, and
|
|
5527
|
+
* the server-side deploy hint so dev / deploy / prod all describe the fix
|
|
5528
|
+
* identically. Customer functions run as Module Workers; the legacy
|
|
5529
|
+
* service-worker shape (`export default async function (req) { … }`) is
|
|
5530
|
+
* rejected by the runtime at upload, so we reject it before the round-trip.
|
|
5531
|
+
*/
|
|
5532
|
+
const HANDLER_SHAPE_FIX_MESSAGE = "Function entrypoint must `export default { async fetch(req, env, ctx) { … } }` (the Module Worker shape). Found a default-exported function instead.";
|
|
5533
|
+
/** Message used when the entry file has no `export default` at all. */
|
|
5534
|
+
const HANDLER_SHAPE_MISSING_DEFAULT_MESSAGE = "Function entrypoint must `export default { async fetch(req, env, ctx) { … } }` (the Module Worker shape). No default export was found.";
|
|
5535
|
+
/** Message used when the default export is present but has no `fetch` method. */
|
|
5536
|
+
const HANDLER_SHAPE_NO_FETCH_MESSAGE = "Function entrypoint must `export default { async fetch(req, env, ctx) { … } }` (the Module Worker shape). The default export has no `fetch` method.";
|
|
5537
|
+
/**
|
|
5538
|
+
* Thrown when the bundled entry's `export default` is not the Module Worker
|
|
5539
|
+
* shape (`{ fetch(req, env, ctx) }`). Raised BEFORE any upload so a wrong
|
|
5540
|
+
* handler shape fails fast on the developer's machine with an actionable
|
|
5541
|
+
* message instead of an opaque upstream rejection at deploy time.
|
|
5542
|
+
*/
|
|
5543
|
+
var HandlerShapeError = class extends Error {
|
|
5544
|
+
kind;
|
|
5545
|
+
constructor(message, kind) {
|
|
5546
|
+
super(message);
|
|
5547
|
+
this.kind = kind;
|
|
5548
|
+
this.name = "HandlerShapeError";
|
|
5549
|
+
}
|
|
5550
|
+
};
|
|
5551
|
+
/**
|
|
5552
|
+
* Inspect an already-imported module namespace and assert it has the
|
|
5553
|
+
* Module Worker handler shape: a default export that is an object (or
|
|
5554
|
+
* function-with-properties) carrying a callable `fetch`.
|
|
5555
|
+
*
|
|
5556
|
+
* Pure + side-effect free — it only reads the shape of `mod.default`, never
|
|
5557
|
+
* invokes `fetch`. This is the unit-testable core; `assertModuleWorkerShape`
|
|
5558
|
+
* wraps the import-from-code plumbing around it.
|
|
5559
|
+
*
|
|
5560
|
+
* Throws `HandlerShapeError` on:
|
|
5561
|
+
* - no default export (`missing-default`)
|
|
5562
|
+
* - a default-exported plain function — the legacy service-worker shape
|
|
5563
|
+
* (`plain-function`)
|
|
5564
|
+
* - a default export with no callable `fetch` (`no-fetch`)
|
|
5565
|
+
*/
|
|
5566
|
+
function assertHandlerShape(mod) {
|
|
5567
|
+
const def = mod.default;
|
|
5568
|
+
if (def === void 0 || def === null) throw new HandlerShapeError(HANDLER_SHAPE_MISSING_DEFAULT_MESSAGE, "missing-default");
|
|
5569
|
+
if (typeof def === "function") {
|
|
5570
|
+
if (typeof def.fetch !== "function") throw new HandlerShapeError(HANDLER_SHAPE_FIX_MESSAGE, "plain-function");
|
|
5571
|
+
return;
|
|
5572
|
+
}
|
|
5573
|
+
if (typeof def !== "object") throw new HandlerShapeError(HANDLER_SHAPE_NO_FETCH_MESSAGE, "no-fetch");
|
|
5574
|
+
if (typeof def.fetch !== "function") throw new HandlerShapeError(HANDLER_SHAPE_NO_FETCH_MESSAGE, "no-fetch");
|
|
5575
|
+
}
|
|
5576
|
+
/**
|
|
5577
|
+
* Validate that the bundled customer code exports the Module Worker handler
|
|
5578
|
+
* shape, by importing it in isolation and inspecting `mod.default`.
|
|
5579
|
+
*
|
|
5580
|
+
* Mechanism: write the bundle to a throwaway temp `.mjs` and dynamic-import
|
|
5581
|
+
* it, then hand the namespace to `assertHandlerShape`. This is the same
|
|
5582
|
+
* import-and-inspect approach the local `dev` runner uses
|
|
5583
|
+
* (`functions.ts` DEV_RUNNER_SOURCE) — reused here so the pre-upload check
|
|
5584
|
+
* and the dev runner agree on what "valid shape" means. Importing runs the
|
|
5585
|
+
* module's top-level code (unavoidable to read the export), but the handler
|
|
5586
|
+
* itself is NEVER invoked — we only read the shape of the exported object.
|
|
5587
|
+
*
|
|
5588
|
+
* The temp file is always cleaned up (best-effort) before returning.
|
|
5589
|
+
*/
|
|
5590
|
+
async function assertModuleWorkerShape(code) {
|
|
5591
|
+
const dir = await mkdtemp(join(tmpdir(), "amba-fn-shape-"));
|
|
5592
|
+
const filePath = join(dir, "fn.mjs");
|
|
5593
|
+
try {
|
|
5594
|
+
await writeFile(filePath, code, "utf8");
|
|
5595
|
+
let mod;
|
|
5596
|
+
try {
|
|
5597
|
+
mod = await import(pathToFileURL(filePath).href);
|
|
5598
|
+
} catch (err) {
|
|
5599
|
+
throw new Error(`Failed to load the function entrypoint while validating its handler shape: ${err instanceof Error ? err.message : String(err)}`);
|
|
5600
|
+
}
|
|
5601
|
+
assertHandlerShape(mod);
|
|
5602
|
+
} finally {
|
|
5603
|
+
await rm(dir, {
|
|
5604
|
+
recursive: true,
|
|
5605
|
+
force: true
|
|
5606
|
+
}).catch(() => {});
|
|
5607
|
+
}
|
|
5608
|
+
}
|
|
5609
|
+
/**
|
|
5014
5610
|
* Bundle a customer function file for upload to a Workers dispatch
|
|
5015
5611
|
* namespace. Returns the bundled code + metadata; throws
|
|
5016
5612
|
* `BundleSizeError` when the compressed size exceeds the cap so the CLI
|
|
@@ -5043,6 +5639,7 @@ async function bundleFunction(options) {
|
|
|
5043
5639
|
const compressedSize = await gzipSizeOf(output.contents);
|
|
5044
5640
|
const max = options.maxSizeBytes ?? 8388608;
|
|
5045
5641
|
if (compressedSize > max) throw new BundleSizeError(compressedSize, max);
|
|
5642
|
+
if (options.validateHandlerShape !== false) await assertModuleWorkerShape(code);
|
|
5046
5643
|
return {
|
|
5047
5644
|
code,
|
|
5048
5645
|
sha256: await hashSha256Hex(output.contents),
|
|
@@ -5091,16 +5688,19 @@ function formatBytes$1(n) {
|
|
|
5091
5688
|
* records the deployment in a single round-trip. The other subcommands
|
|
5092
5689
|
* are thin shells over the admin API helpers in `api-client.ts`.
|
|
5093
5690
|
*/
|
|
5094
|
-
async function functionsDeployCommand(
|
|
5691
|
+
async function functionsDeployCommand(target, options = {}) {
|
|
5095
5692
|
const projectId = (await loadProjectConfig()).projectId;
|
|
5096
|
-
const
|
|
5693
|
+
const targetIsDir = (await stat(target).catch(() => null))?.isDirectory() ?? false;
|
|
5694
|
+
const entryPoint = await resolveFunctionEntry(target, options.entry);
|
|
5695
|
+
const defaultName = targetIsDir ? basename(target.replace(/[/\\]+$/, "")) : basename(entryPoint).replace(/\.[^.]+$/, "");
|
|
5696
|
+
const functionName = options.name ?? defaultName;
|
|
5097
5697
|
validateFunctionName(functionName);
|
|
5098
5698
|
const rateLimit = parseRateLimitFlags(options);
|
|
5099
5699
|
console.log();
|
|
5100
5700
|
console.log(pc.bold(` amba functions deploy ${pc.cyan(functionName)}`));
|
|
5101
5701
|
console.log(pc.dim(" ─────────────────────────────────"));
|
|
5102
5702
|
console.log();
|
|
5103
|
-
console.log(pc.dim(
|
|
5703
|
+
console.log(pc.dim(` Bundling ${entryPoint}…`));
|
|
5104
5704
|
const bundle = await bundleFunction({ entryPoint });
|
|
5105
5705
|
printBundleReport(bundle);
|
|
5106
5706
|
if (options.dryRun) {
|
|
@@ -5113,7 +5713,8 @@ async function functionsDeployCommand(entryPoint, options = {}) {
|
|
|
5113
5713
|
result = await deployFunctionViaApi(projectId, {
|
|
5114
5714
|
name: functionName,
|
|
5115
5715
|
bundleCode: bundle.code,
|
|
5116
|
-
rate_limit: rateLimit
|
|
5716
|
+
rate_limit: rateLimit,
|
|
5717
|
+
public: options.public === true ? true : void 0
|
|
5117
5718
|
});
|
|
5118
5719
|
} catch (err) {
|
|
5119
5720
|
if (err instanceof ApiClientError && err.statusCode === 503) {
|
|
@@ -5126,6 +5727,7 @@ async function functionsDeployCommand(entryPoint, options = {}) {
|
|
|
5126
5727
|
console.log(pc.green(" ✓") + ` Deployed ${pc.cyan(functionName)} ${pc.dim(`v${result.data.version}`)}`);
|
|
5127
5728
|
console.log(pc.green(" ✓") + ` URL: ${pc.underline(result.fn_url)}`);
|
|
5128
5729
|
if (rateLimit) console.log(pc.dim(` Rate limit: ${rateLimit.max} per ${rateLimit.window} (key=${rateLimit.key}) — enforced pre-dispatch`));
|
|
5730
|
+
if (options.public === true) console.log(pc.yellow(" ! Public") + pc.dim(" — no X-Api-Key required. Verify the webhook signature inside your handler."));
|
|
5129
5731
|
console.log();
|
|
5130
5732
|
}
|
|
5131
5733
|
async function functionsListCommand() {
|
|
@@ -5481,11 +6083,15 @@ try {
|
|
|
5481
6083
|
return fetchFn(req, env, ctx);
|
|
5482
6084
|
};
|
|
5483
6085
|
} else if (typeof mod.default === 'function') {
|
|
5484
|
-
// Legacy
|
|
5485
|
-
//
|
|
5486
|
-
|
|
6086
|
+
// Legacy service-worker shape (\`export default async function (req) { ... }\`).
|
|
6087
|
+
// REJECTED — production runs functions as Module Workers and the runtime
|
|
6088
|
+
// rejects this shape at upload. Accepting it in dev was a dev/prod parity
|
|
6089
|
+
// trap: a function that ran here would fail \`amba functions deploy\`. Reject
|
|
6090
|
+
// with the same fix the bundler + server hint use so dev and prod agree.
|
|
6091
|
+
process.stderr.write(' Error: Function entrypoint must \`export default { async fetch(req, env, ctx) { … } }\` (the Module Worker shape). Found a default-exported function instead.\\n');
|
|
6092
|
+
process.exit(2);
|
|
5487
6093
|
} else {
|
|
5488
|
-
process.stderr.write(' Error:
|
|
6094
|
+
process.stderr.write(' Error: Function entrypoint must \`export default { async fetch(req, env, ctx) { … } }\` (the Module Worker shape). The default export has no \`fetch\` method.\\n');
|
|
5489
6095
|
process.exit(2);
|
|
5490
6096
|
}
|
|
5491
6097
|
} catch (err) {
|
|
@@ -5653,7 +6259,8 @@ async function functionsLogsCommand(functionName, options = {}) {
|
|
|
5653
6259
|
const res = await getFunctionLogs(projectConfig.projectId, functionName, {
|
|
5654
6260
|
since: options.since,
|
|
5655
6261
|
until: options.until,
|
|
5656
|
-
limit: options.limit
|
|
6262
|
+
limit: options.limit,
|
|
6263
|
+
requestId: options.requestId
|
|
5657
6264
|
});
|
|
5658
6265
|
printEvents(res.data.events, options.json);
|
|
5659
6266
|
if (res.data.truncated) if (options.json) process.stderr.write(`{"truncated":true}\n`);
|
|
@@ -5669,7 +6276,8 @@ async function runTail(projectId, functionName, options) {
|
|
|
5669
6276
|
try {
|
|
5670
6277
|
result = await getFunctionLogs(projectId, functionName, {
|
|
5671
6278
|
since: new Date(highWaterMs).toISOString(),
|
|
5672
|
-
limit: options.limit ?? 200
|
|
6279
|
+
limit: options.limit ?? 200,
|
|
6280
|
+
requestId: options.requestId
|
|
5673
6281
|
});
|
|
5674
6282
|
} catch (err) {
|
|
5675
6283
|
console.error(pc.yellow(` ! tail fetch failed: `) + (err instanceof Error ? err.message : String(err)));
|
|
@@ -5900,7 +6508,7 @@ function renderStatus(status) {
|
|
|
5900
6508
|
* Project is resolved via `loadProjectConfig` (AMBA_PROJECT_ID env, then
|
|
5901
6509
|
* .env / .env.local in the cwd) — same pattern as `amba secrets *`.
|
|
5902
6510
|
*/
|
|
5903
|
-
function handleError(err) {
|
|
6511
|
+
function handleError$1(err) {
|
|
5904
6512
|
if (err instanceof ApiClientError) if (err.statusCode === 401 || err.statusCode === 403) console.log(pc.red(" ✗") + " Not authenticated — run `amba login` first.");
|
|
5905
6513
|
else console.log(pc.red(" ✗") + ` ${err.message}`);
|
|
5906
6514
|
else if (err instanceof Error) console.log(pc.red(" ✗") + ` ${err.message}`);
|
|
@@ -5914,7 +6522,7 @@ function handleError(err) {
|
|
|
5914
6522
|
* its own request wrapper — same auth flow as `request()` in api-client,
|
|
5915
6523
|
* intentionally not exported there so we don't grow the public surface.
|
|
5916
6524
|
*/
|
|
5917
|
-
async function adminWrite(method, path, body) {
|
|
6525
|
+
async function adminWrite$1(method, path, body) {
|
|
5918
6526
|
const token = await resolveBearerToken();
|
|
5919
6527
|
const url = `${process.env["AMBA_API_URL"] ?? "https://api.amba.dev"}/v1/admin${path}`;
|
|
5920
6528
|
const res = await fetch(url, {
|
|
@@ -5976,7 +6584,7 @@ async function billingStatusCommand() {
|
|
|
5976
6584
|
console.log();
|
|
5977
6585
|
}
|
|
5978
6586
|
} catch (err) {
|
|
5979
|
-
handleError(err);
|
|
6587
|
+
handleError$1(err);
|
|
5980
6588
|
}
|
|
5981
6589
|
}
|
|
5982
6590
|
async function billingUpgradeCommand(input) {
|
|
@@ -5987,7 +6595,7 @@ async function billingUpgradeCommand(input) {
|
|
|
5987
6595
|
try {
|
|
5988
6596
|
const { projectId } = await loadProjectConfig();
|
|
5989
6597
|
const interval = input.interval ?? "month";
|
|
5990
|
-
const res = await adminWrite("POST", `/projects/${projectId}/billing/checkout`, {
|
|
6598
|
+
const res = await adminWrite$1("POST", `/projects/${projectId}/billing/checkout`, {
|
|
5991
6599
|
tier: input.tier,
|
|
5992
6600
|
interval
|
|
5993
6601
|
});
|
|
@@ -6001,7 +6609,7 @@ async function billingUpgradeCommand(input) {
|
|
|
6001
6609
|
console.log(pc.dim(" Subscription status updates automatically once Stripe confirms (a few seconds)."));
|
|
6002
6610
|
console.log();
|
|
6003
6611
|
} catch (err) {
|
|
6004
|
-
handleError(err);
|
|
6612
|
+
handleError$1(err);
|
|
6005
6613
|
}
|
|
6006
6614
|
}
|
|
6007
6615
|
async function billingPortalCommand() {
|
|
@@ -6011,13 +6619,13 @@ async function billingPortalCommand() {
|
|
|
6011
6619
|
console.log();
|
|
6012
6620
|
try {
|
|
6013
6621
|
const { projectId } = await loadProjectConfig();
|
|
6014
|
-
const res = await adminWrite("POST", `/projects/${projectId}/billing/portal`);
|
|
6622
|
+
const res = await adminWrite$1("POST", `/projects/${projectId}/billing/portal`);
|
|
6015
6623
|
console.log(pc.bold(" Open this URL to manage your subscription:"));
|
|
6016
6624
|
console.log();
|
|
6017
6625
|
console.log(` ${pc.cyan(res.data.url)}`);
|
|
6018
6626
|
console.log();
|
|
6019
6627
|
} catch (err) {
|
|
6020
|
-
handleError(err);
|
|
6628
|
+
handleError$1(err);
|
|
6021
6629
|
}
|
|
6022
6630
|
}
|
|
6023
6631
|
async function billingSetCeilingCommand(input) {
|
|
@@ -6027,15 +6635,374 @@ async function billingSetCeilingCommand(input) {
|
|
|
6027
6635
|
console.log();
|
|
6028
6636
|
try {
|
|
6029
6637
|
const { projectId } = await loadProjectConfig();
|
|
6030
|
-
await adminWrite(`PUT`, `/projects/${projectId}/billing/ceiling`, { ceiling_usd: input.ceiling });
|
|
6638
|
+
await adminWrite$1(`PUT`, `/projects/${projectId}/billing/ceiling`, { ceiling_usd: input.ceiling });
|
|
6031
6639
|
if (input.ceiling === null) console.log(pc.green(" ✓") + " Spend ceiling removed (linear overage continues).");
|
|
6032
6640
|
else console.log(pc.green(" ✓") + ` Spend ceiling set to $${input.ceiling}/mo.`);
|
|
6033
6641
|
console.log();
|
|
6642
|
+
} catch (err) {
|
|
6643
|
+
handleError$1(err);
|
|
6644
|
+
}
|
|
6645
|
+
}
|
|
6646
|
+
//#endregion
|
|
6647
|
+
//#region src/commands/payments.ts
|
|
6648
|
+
/**
|
|
6649
|
+
* `amba payments *` subcommands — CLI access to the Amba Payments (Stripe
|
|
6650
|
+
* Connect platform) surface at `/v1/admin/projects/:id/payments/*`
|
|
6651
|
+
* (tracker #10).
|
|
6652
|
+
*
|
|
6653
|
+
* Amba is the payments rail for the developer's app: the app is the seller,
|
|
6654
|
+
* Amba takes a platform fee, Stripe carries money-transmission. This is the
|
|
6655
|
+
* developer-facing onboarding + config + reporting surface, so naming Stripe
|
|
6656
|
+
* here is by intent (same as `amba billing`).
|
|
6657
|
+
*
|
|
6658
|
+
* amba payments setup — create the connected account +
|
|
6659
|
+
* print the hosted onboarding URL.
|
|
6660
|
+
* amba payments status — onboarding stage + capability flags.
|
|
6661
|
+
* amba payments set-fee <bps|off> — default platform fee (basis points).
|
|
6662
|
+
* amba payments balance — connected-account balance.
|
|
6663
|
+
* amba payments payouts — recent payouts.
|
|
6664
|
+
*
|
|
6665
|
+
* Every live call is gated by the platform's PAYMENTS_CONNECT_LIVE
|
|
6666
|
+
* kill-switch — when off the API returns PAYMENTS_NOT_ENABLED, surfaced
|
|
6667
|
+
* verbatim here. Project resolved via loadProjectConfig (same as billing).
|
|
6668
|
+
*/
|
|
6669
|
+
function handleError(err) {
|
|
6670
|
+
if (err instanceof ApiClientError) if (err.statusCode === 401 || err.statusCode === 403) console.log(pc.red(" ✗") + " Not authenticated — run `amba login` first.");
|
|
6671
|
+
else if (err.errorCode === "PAYMENTS_NOT_ENABLED") console.log(pc.yellow(" •") + " Payments are not enabled on this Amba platform yet. The platform owner must finish setup before payments go live.");
|
|
6672
|
+
else console.log(pc.red(" ✗") + ` ${err.message}`);
|
|
6673
|
+
else if (err instanceof Error) console.log(pc.red(" ✗") + ` ${err.message}`);
|
|
6674
|
+
else console.log(pc.red(" ✗") + " Unknown error");
|
|
6675
|
+
console.log();
|
|
6676
|
+
process.exit(1);
|
|
6677
|
+
}
|
|
6678
|
+
/**
|
|
6679
|
+
* POST/PUT to the admin API. Mirrors the helper in billing.ts (api-client
|
|
6680
|
+
* doesn't export a generic write for arbitrary paths).
|
|
6681
|
+
*/
|
|
6682
|
+
async function adminWrite(method, path, body) {
|
|
6683
|
+
const token = await resolveBearerToken();
|
|
6684
|
+
const url = `${process.env["AMBA_API_URL"] ?? "https://api.amba.dev"}/v1/admin${path}`;
|
|
6685
|
+
const res = await fetch(url, {
|
|
6686
|
+
method,
|
|
6687
|
+
headers: {
|
|
6688
|
+
Authorization: `Bearer ${token}`,
|
|
6689
|
+
"Content-Type": "application/json",
|
|
6690
|
+
"User-Agent": "amba-cli/0.1.1"
|
|
6691
|
+
},
|
|
6692
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
6693
|
+
});
|
|
6694
|
+
if (!res.ok) {
|
|
6695
|
+
let message = `API request failed: ${res.status} ${res.statusText}`;
|
|
6696
|
+
let code;
|
|
6697
|
+
try {
|
|
6698
|
+
const errorBody = await res.json();
|
|
6699
|
+
if (errorBody.error?.message) {
|
|
6700
|
+
message = errorBody.error.message;
|
|
6701
|
+
code = errorBody.error.code;
|
|
6702
|
+
}
|
|
6703
|
+
} catch {}
|
|
6704
|
+
throw new ApiClientError(message, res.status, code);
|
|
6705
|
+
}
|
|
6706
|
+
return await res.json();
|
|
6707
|
+
}
|
|
6708
|
+
function printAccount(a) {
|
|
6709
|
+
console.log(` Account: ${pc.dim(a.stripe_account_id)}`);
|
|
6710
|
+
console.log(` Onboarding: ${pc.bold(a.onboarding_status)}`);
|
|
6711
|
+
console.log(` Charges enabled: ${a.charges_enabled ? pc.green("yes") : pc.yellow("no")}`);
|
|
6712
|
+
console.log(` Payouts enabled: ${a.payouts_enabled ? pc.green("yes") : pc.yellow("no")}`);
|
|
6713
|
+
console.log(` Country: ${a.country ?? pc.dim("—")}`);
|
|
6714
|
+
console.log(` Default fee: ${a.default_platform_fee_bps === null ? pc.dim("none") : `${a.default_platform_fee_bps} bps (${(a.default_platform_fee_bps / 100).toFixed(2)}%)`}`);
|
|
6715
|
+
}
|
|
6716
|
+
async function paymentsSetupCommand(input) {
|
|
6717
|
+
console.log();
|
|
6718
|
+
console.log(pc.bold(" amba payments setup"));
|
|
6719
|
+
console.log(pc.dim(" ─────────────────────────────────"));
|
|
6720
|
+
console.log();
|
|
6721
|
+
try {
|
|
6722
|
+
const { projectId } = await loadProjectConfig();
|
|
6723
|
+
printAccount((await adminWrite("POST", `/projects/${projectId}/payments/accounts`, {
|
|
6724
|
+
...input.country ? { country: input.country } : {},
|
|
6725
|
+
...input.email ? { email: input.email } : {}
|
|
6726
|
+
})).data);
|
|
6727
|
+
console.log();
|
|
6728
|
+
const link = await adminWrite("POST", `/projects/${projectId}/payments/accounts/link`);
|
|
6729
|
+
console.log(pc.bold(" Open this URL to finish setting up payments:"));
|
|
6730
|
+
console.log();
|
|
6731
|
+
console.log(` ${pc.cyan(link.data.url)}`);
|
|
6732
|
+
console.log();
|
|
6733
|
+
console.log(pc.dim(" Status updates automatically once setup completes."));
|
|
6734
|
+
console.log();
|
|
6735
|
+
} catch (err) {
|
|
6736
|
+
handleError(err);
|
|
6737
|
+
}
|
|
6738
|
+
}
|
|
6739
|
+
async function paymentsStatusCommand() {
|
|
6740
|
+
console.log();
|
|
6741
|
+
console.log(pc.bold(" amba payments status"));
|
|
6742
|
+
console.log(pc.dim(" ─────────────────────────────────"));
|
|
6743
|
+
console.log();
|
|
6744
|
+
try {
|
|
6745
|
+
const { projectId } = await loadProjectConfig();
|
|
6746
|
+
const res = await adminGet(`/projects/${projectId}/payments/accounts`);
|
|
6747
|
+
if (!res.data.connected_account) {
|
|
6748
|
+
console.log(pc.dim(" No payments account yet. Run `amba payments setup`."));
|
|
6749
|
+
console.log();
|
|
6750
|
+
return;
|
|
6751
|
+
}
|
|
6752
|
+
printAccount(res.data.connected_account);
|
|
6753
|
+
console.log();
|
|
6754
|
+
} catch (err) {
|
|
6755
|
+
handleError(err);
|
|
6756
|
+
}
|
|
6757
|
+
}
|
|
6758
|
+
async function paymentsSetFeeCommand(input) {
|
|
6759
|
+
console.log();
|
|
6760
|
+
console.log(pc.bold(" amba payments set-fee"));
|
|
6761
|
+
console.log(pc.dim(" ─────────────────────────────────"));
|
|
6762
|
+
console.log();
|
|
6763
|
+
try {
|
|
6764
|
+
const { projectId } = await loadProjectConfig();
|
|
6765
|
+
await adminWrite("PUT", `/projects/${projectId}/payments/config`, { default_platform_fee_bps: input.bps });
|
|
6766
|
+
if (input.bps === null) console.log(pc.green(" ✓") + " Default platform fee cleared.");
|
|
6767
|
+
else console.log(pc.green(" ✓") + ` Default platform fee set to ${input.bps} bps (${(input.bps / 100).toFixed(2)}%).`);
|
|
6768
|
+
console.log();
|
|
6769
|
+
} catch (err) {
|
|
6770
|
+
handleError(err);
|
|
6771
|
+
}
|
|
6772
|
+
}
|
|
6773
|
+
async function paymentsBalanceCommand() {
|
|
6774
|
+
console.log();
|
|
6775
|
+
console.log(pc.bold(" amba payments balance"));
|
|
6776
|
+
console.log(pc.dim(" ─────────────────────────────────"));
|
|
6777
|
+
console.log();
|
|
6778
|
+
try {
|
|
6779
|
+
const { projectId } = await loadProjectConfig();
|
|
6780
|
+
const res = await adminGet(`/projects/${projectId}/payments/balance`);
|
|
6781
|
+
const fmt = (b) => `${(b.amount / 100).toFixed(2)} ${b.currency.toUpperCase()}`;
|
|
6782
|
+
console.log(` Available: ${res.data.available.length ? res.data.available.map(fmt).join(", ") : pc.dim("—")}`);
|
|
6783
|
+
console.log(` Pending: ${res.data.pending.length ? res.data.pending.map(fmt).join(", ") : pc.dim("—")}`);
|
|
6784
|
+
console.log();
|
|
6785
|
+
} catch (err) {
|
|
6786
|
+
handleError(err);
|
|
6787
|
+
}
|
|
6788
|
+
}
|
|
6789
|
+
async function paymentsPayoutsCommand(input) {
|
|
6790
|
+
console.log();
|
|
6791
|
+
console.log(pc.bold(" amba payments payouts"));
|
|
6792
|
+
console.log(pc.dim(" ─────────────────────────────────"));
|
|
6793
|
+
console.log();
|
|
6794
|
+
try {
|
|
6795
|
+
const { projectId } = await loadProjectConfig();
|
|
6796
|
+
const res = await adminGet(`/projects/${projectId}/payments/payouts${input.limit ? `?limit=${input.limit}` : ""}`);
|
|
6797
|
+
if (res.data.length === 0) {
|
|
6798
|
+
console.log(pc.dim(" No payouts yet."));
|
|
6799
|
+
console.log();
|
|
6800
|
+
return;
|
|
6801
|
+
}
|
|
6802
|
+
for (const p of res.data) {
|
|
6803
|
+
const arrival = (/* @__PURE__ */ new Date(p.arrival_date * 1e3)).toLocaleDateString();
|
|
6804
|
+
console.log(` ${pc.dim(p.id.padEnd(28))} ${(p.amount / 100).toFixed(2)} ${p.currency.toUpperCase().padEnd(4)} ${pc.dim(p.status.padEnd(12))} arrives ${arrival}`);
|
|
6805
|
+
}
|
|
6806
|
+
console.log();
|
|
6034
6807
|
} catch (err) {
|
|
6035
6808
|
handleError(err);
|
|
6036
6809
|
}
|
|
6037
6810
|
}
|
|
6038
6811
|
//#endregion
|
|
6812
|
+
//#region src/commands/monetization.ts
|
|
6813
|
+
/**
|
|
6814
|
+
* `amba monetization *` subcommands — CLI access to the monetization control
|
|
6815
|
+
* plane behind `/v1/admin/projects/:id/monetization/*`.
|
|
6816
|
+
*
|
|
6817
|
+
* amba monetization plan — three-way diff (declared vs adopted baseline
|
|
6818
|
+
* vs live provider) + store-floor preflight +
|
|
6819
|
+
* drift. READ-ONLY — previews; applies nothing.
|
|
6820
|
+
* amba monetization drift — out-of-band changes to Amba-managed objects.
|
|
6821
|
+
* amba monetization export — snapshot the live config to a declarative
|
|
6822
|
+
* bundle (the `monetization` section), to stdout
|
|
6823
|
+
* or a file.
|
|
6824
|
+
* amba monetization adopt — record the live config as the managed baseline
|
|
6825
|
+
* so the next plan is a no-op. Writes ONLY Amba's
|
|
6826
|
+
* own state — never the provider. Idempotent.
|
|
6827
|
+
* amba monetization apply — push the declared config to the provider.
|
|
6828
|
+
* ADDITIVE only: create entitlements/offerings/
|
|
6829
|
+
* packages/paywall-drafts + attach products.
|
|
6830
|
+
* Refuses if the provider drifted since the plan;
|
|
6831
|
+
* never does destructive changes.
|
|
6832
|
+
*
|
|
6833
|
+
* Project is resolved via `loadProjectConfig` — same pattern as the other
|
|
6834
|
+
* project-scoped CLI commands.
|
|
6835
|
+
*/
|
|
6836
|
+
/**
|
|
6837
|
+
* Authenticated write helper (POST). Mirrors the `adminWrite` in
|
|
6838
|
+
* `commands/billing.ts` — kept local so the api-client public surface doesn't
|
|
6839
|
+
* grow a generic POST.
|
|
6840
|
+
*/
|
|
6841
|
+
async function adminPost(path, body) {
|
|
6842
|
+
const token = await resolveBearerToken();
|
|
6843
|
+
const url = `${process.env["AMBA_API_URL"] ?? "https://api.amba.dev"}/v1/admin${path}`;
|
|
6844
|
+
const res = await fetch(url, {
|
|
6845
|
+
method: "POST",
|
|
6846
|
+
headers: {
|
|
6847
|
+
Authorization: `Bearer ${token}`,
|
|
6848
|
+
"Content-Type": "application/json",
|
|
6849
|
+
"User-Agent": "amba-cli/0.1.1"
|
|
6850
|
+
},
|
|
6851
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
6852
|
+
});
|
|
6853
|
+
if (!res.ok) {
|
|
6854
|
+
let message = `API request failed: ${res.status} ${res.statusText}`;
|
|
6855
|
+
let code;
|
|
6856
|
+
try {
|
|
6857
|
+
const errorBody = await res.json();
|
|
6858
|
+
if (errorBody.error?.message) {
|
|
6859
|
+
message = errorBody.error.message;
|
|
6860
|
+
code = errorBody.error.code;
|
|
6861
|
+
}
|
|
6862
|
+
} catch {}
|
|
6863
|
+
throw new ApiClientError(message, res.status, code);
|
|
6864
|
+
}
|
|
6865
|
+
return await res.json();
|
|
6866
|
+
}
|
|
6867
|
+
function header(title) {
|
|
6868
|
+
console.log();
|
|
6869
|
+
console.log(pc.bold(` amba monetization ${title}`));
|
|
6870
|
+
console.log(pc.dim(" ─────────────────────────────────"));
|
|
6871
|
+
console.log();
|
|
6872
|
+
}
|
|
6873
|
+
async function monetizationPlanCommand() {
|
|
6874
|
+
const { projectId } = await loadProjectConfig();
|
|
6875
|
+
header("plan");
|
|
6876
|
+
const plan = (await adminGet(`/projects/${projectId}/monetization/plan`)).data;
|
|
6877
|
+
if (plan.clean) {
|
|
6878
|
+
console.log(pc.green(" ✓") + " No changes — declared config matches the live provider.");
|
|
6879
|
+
console.log();
|
|
6880
|
+
return;
|
|
6881
|
+
}
|
|
6882
|
+
if (plan.ops.length > 0) {
|
|
6883
|
+
console.log(pc.bold(" Pending operations (preview — nothing is applied):"));
|
|
6884
|
+
for (const op of plan.ops) console.log(` ${pc.cyan(op.op.padEnd(12))} ${pc.bold(op.object_type)} ${op.identifier} ${pc.dim(`[${op.status}]`)}`);
|
|
6885
|
+
console.log();
|
|
6886
|
+
}
|
|
6887
|
+
if (plan.store_floor.length > 0) {
|
|
6888
|
+
console.log(pc.yellow(" Store-floor preflight — referenced products missing in the provider:"));
|
|
6889
|
+
for (const sf of plan.store_floor) console.log(` ${pc.bold(sf.product_key)} → ${sf.store_identifier} (${sf.store})`);
|
|
6890
|
+
console.log();
|
|
6891
|
+
}
|
|
6892
|
+
if (plan.drift.length > 0) {
|
|
6893
|
+
console.log(pc.yellow(" Drift — managed objects changed out-of-band in the provider:"));
|
|
6894
|
+
for (const d of plan.drift) console.log(` ${pc.bold(d.object_type)} ${d.identifier}`);
|
|
6895
|
+
console.log();
|
|
6896
|
+
}
|
|
6897
|
+
}
|
|
6898
|
+
async function monetizationDriftCommand() {
|
|
6899
|
+
const { projectId } = await loadProjectConfig();
|
|
6900
|
+
header("drift");
|
|
6901
|
+
const data = (await adminGet(`/projects/${projectId}/monetization/drift`)).data;
|
|
6902
|
+
if (!data.has_drift) {
|
|
6903
|
+
console.log(pc.green(" ✓") + " No drift — managed config matches the live provider.");
|
|
6904
|
+
console.log();
|
|
6905
|
+
return;
|
|
6906
|
+
}
|
|
6907
|
+
console.log(pc.yellow(" Drift detected:"));
|
|
6908
|
+
for (const d of data.drift) console.log(` ${pc.bold(d.object_type)} ${d.identifier}`);
|
|
6909
|
+
console.log();
|
|
6910
|
+
}
|
|
6911
|
+
async function monetizationExportCommand(options = {}) {
|
|
6912
|
+
const { projectId } = await loadProjectConfig();
|
|
6913
|
+
const bundle = {
|
|
6914
|
+
version: 1,
|
|
6915
|
+
monetization: (await adminGet(`/projects/${projectId}/monetization/export`)).data.monetization
|
|
6916
|
+
};
|
|
6917
|
+
const json = JSON.stringify(bundle, null, 2);
|
|
6918
|
+
if (options.file) {
|
|
6919
|
+
await writeFile(resolve(options.file), json + "\n", "utf-8");
|
|
6920
|
+
console.log();
|
|
6921
|
+
console.log(pc.green(" ✓") + ` Wrote live monetization config to ${pc.cyan(options.file)}.`);
|
|
6922
|
+
console.log();
|
|
6923
|
+
} else console.log(json);
|
|
6924
|
+
}
|
|
6925
|
+
async function monetizationAdoptCommand(options = {}) {
|
|
6926
|
+
const { projectId } = await loadProjectConfig();
|
|
6927
|
+
header("adopt");
|
|
6928
|
+
if (!options.all) {
|
|
6929
|
+
console.log(" ✗ Pass --all to adopt the entire live config as the managed baseline.");
|
|
6930
|
+
process.exit(1);
|
|
6931
|
+
}
|
|
6932
|
+
const d = (await adminPost(`/projects/${projectId}/monetization/adopt`, { adopt_all: true })).data;
|
|
6933
|
+
console.log(pc.green(" ✓") + ` Adopted ${d.adopted} object(s) as the managed baseline ` + pc.dim(`(${d.entitlements} entitlements · ${d.products} products · ${d.offerings} offerings · ${d.packages} packages · ${d.paywalls} paywalls)`));
|
|
6934
|
+
console.log(pc.dim(" The next `amba monetization plan` is now a clean no-op."));
|
|
6935
|
+
console.log();
|
|
6936
|
+
}
|
|
6937
|
+
async function monetizationApplyCommand(options = {}) {
|
|
6938
|
+
const { projectId } = await loadProjectConfig();
|
|
6939
|
+
header("apply");
|
|
6940
|
+
const plan = (await adminGet(`/projects/${projectId}/monetization/plan`)).data;
|
|
6941
|
+
if (plan.store_floor.length > 0) {
|
|
6942
|
+
console.log(pc.yellow(" Store floor not satisfied — these products must exist in the provider first:"));
|
|
6943
|
+
for (const sf of plan.store_floor) console.log(` ${pc.bold(sf.product_key)} → ${sf.store_identifier} (${sf.store})`);
|
|
6944
|
+
console.log();
|
|
6945
|
+
console.log(" ✗ Configure these in the store, then run plan + apply again.");
|
|
6946
|
+
process.exit(1);
|
|
6947
|
+
}
|
|
6948
|
+
if (plan.clean) {
|
|
6949
|
+
console.log(pc.green(" ✓") + " Nothing to apply — declared config matches the provider.");
|
|
6950
|
+
console.log();
|
|
6951
|
+
return;
|
|
6952
|
+
}
|
|
6953
|
+
if (options.reconcile && options.reconcile !== "amba" && options.reconcile !== "adopt") {
|
|
6954
|
+
console.log(pc.red(" ✗") + " --reconcile must be 'amba' or 'adopt'.");
|
|
6955
|
+
process.exit(1);
|
|
6956
|
+
}
|
|
6957
|
+
const body = { plan_hash: plan.plan_hash };
|
|
6958
|
+
if (options.confirm) body["confirm"] = plan.plan_hash;
|
|
6959
|
+
if (options.allowDetachLive) body["allow_detach_live"] = true;
|
|
6960
|
+
if (options.reconcile) body["reconcile"] = options.reconcile;
|
|
6961
|
+
let res;
|
|
6962
|
+
try {
|
|
6963
|
+
res = await adminPost(`/projects/${projectId}/monetization/apply`, body);
|
|
6964
|
+
} catch (err) {
|
|
6965
|
+
if (err instanceof ApiClientError && err.errorCode === "MONETIZATION_CONFIRM_REQUIRED") {
|
|
6966
|
+
console.log(pc.yellow(" ⚠ This plan includes gated destructive operation(s) — nothing was applied."));
|
|
6967
|
+
console.log(pc.dim(" Review the plan, then re-run with --confirm to execute the gated ops:"));
|
|
6968
|
+
console.log(` ${pc.cyan("amba monetization apply --confirm")}`);
|
|
6969
|
+
console.log();
|
|
6970
|
+
process.exit(1);
|
|
6971
|
+
}
|
|
6972
|
+
if (err instanceof ApiClientError && err.errorCode === "MONETIZATION_INVARIANT") {
|
|
6973
|
+
console.log(pc.red(" ✗ Blocked by the ordering invariant — nothing was applied:"));
|
|
6974
|
+
console.log(pc.dim(` ${err.message}`));
|
|
6975
|
+
console.log();
|
|
6976
|
+
process.exit(1);
|
|
6977
|
+
}
|
|
6978
|
+
throw err;
|
|
6979
|
+
}
|
|
6980
|
+
const d = res.data;
|
|
6981
|
+
if (d.sandbox_stage?.ran) {
|
|
6982
|
+
const tag = d.sandbox_stage.passed ? pc.green("✓ sandbox passed") : pc.red("✗ sandbox failed");
|
|
6983
|
+
console.log(` ${tag} ` + pc.dim(d.sandbox_stage.detail));
|
|
6984
|
+
console.log();
|
|
6985
|
+
}
|
|
6986
|
+
if (d.status === "succeeded") console.log(pc.green(" ✓") + ` Applied ${d.applied} change(s) to the provider ` + pc.dim(`(${d.skipped_existing} already present, ${d.gated_applied} destructive)`));
|
|
6987
|
+
else {
|
|
6988
|
+
console.log(pc.red(" ✗") + ` Apply stopped after a failure (${d.applied} applied, ${d.failed} failed).` + (d.stopped_at ? ` Stopped at: ${d.stopped_at}` : ""));
|
|
6989
|
+
const failed = d.results.find((r) => r.status === "failed");
|
|
6990
|
+
if (failed?.error) console.log(pc.dim(` ${failed.error}`));
|
|
6991
|
+
}
|
|
6992
|
+
if (d.gated.length > 0) {
|
|
6993
|
+
console.log();
|
|
6994
|
+
console.log(pc.yellow(" Gated (destructive — run with --confirm to execute):"));
|
|
6995
|
+
for (const g of d.gated) console.log(` ${pc.bold(g.op.op)} ${g.op.object_type} ${g.op.identifier}`);
|
|
6996
|
+
}
|
|
6997
|
+
if (d.refused.length > 0) {
|
|
6998
|
+
console.log();
|
|
6999
|
+
console.log(pc.yellow(" Not applicable via API (do these in the provider dashboard):"));
|
|
7000
|
+
for (const r of d.refused) console.log(` ${pc.bold(r.op.op)} ${r.op.object_type} ${r.op.identifier}`);
|
|
7001
|
+
}
|
|
7002
|
+
console.log();
|
|
7003
|
+
if (d.status !== "succeeded") process.exit(1);
|
|
7004
|
+
}
|
|
7005
|
+
//#endregion
|
|
6039
7006
|
//#region src/commands/collections.ts
|
|
6040
7007
|
/**
|
|
6041
7008
|
* `amba collections ...` — thin shells over the admin collection routes.
|
|
@@ -6334,8 +7301,22 @@ const PG_TYPE_TO_TS = {
|
|
|
6334
7301
|
jsonb: "unknown",
|
|
6335
7302
|
json: "unknown"
|
|
6336
7303
|
};
|
|
7304
|
+
const ARRAY_UDT_TO_TS = {
|
|
7305
|
+
_text: "string",
|
|
7306
|
+
_varchar: "string",
|
|
7307
|
+
_uuid: "string",
|
|
7308
|
+
_int4: "number",
|
|
7309
|
+
_int8: "number",
|
|
7310
|
+
_int2: "number",
|
|
7311
|
+
_numeric: "number",
|
|
7312
|
+
_float8: "number",
|
|
7313
|
+
_float4: "number",
|
|
7314
|
+
_bool: "boolean"
|
|
7315
|
+
};
|
|
6337
7316
|
function tsTypeForColumn(col) {
|
|
6338
|
-
|
|
7317
|
+
let base;
|
|
7318
|
+
if (col.data_type === "ARRAY") base = `${col.udt_name ? ARRAY_UDT_TO_TS[col.udt_name] ?? "unknown" : "unknown"}[]`;
|
|
7319
|
+
else base = PG_TYPE_TO_TS[col.data_type] ?? "unknown";
|
|
6339
7320
|
if (col.column_name === "deleted_at") return "string | null";
|
|
6340
7321
|
if (col.is_nullable === "YES") return `${base} | null`;
|
|
6341
7322
|
return base;
|
|
@@ -6885,9 +7866,351 @@ async function domainsListCommand() {
|
|
|
6885
7866
|
console.log();
|
|
6886
7867
|
}
|
|
6887
7868
|
//#endregion
|
|
7869
|
+
//#region src/commands/iac.ts
|
|
7870
|
+
/**
|
|
7871
|
+
* `amba apply` / `amba diff` / `amba export` — declarative
|
|
7872
|
+
* infrastructure-as-code for a project's reusable configuration.
|
|
7873
|
+
*
|
|
7874
|
+
* A declarative bundle (default `amba.config.json`) captures the entities a
|
|
7875
|
+
* developer hand-builds once and wants to stand up identically elsewhere:
|
|
7876
|
+
* remote configs, collection schemas, content libraries + items, currencies,
|
|
7877
|
+
* achievements, streaks, challenges, leaderboards, and xp_rules. It carries
|
|
7878
|
+
* CONFIGURATION ONLY — no secrets, no per-user data.
|
|
7879
|
+
*
|
|
7880
|
+
* These commands wrap the platform's promotion endpoints:
|
|
7881
|
+
* GET /v1/admin/projects/:p/promotion/export
|
|
7882
|
+
* POST /v1/admin/projects/:p/promotion/import (mode=skip_existing|merge)
|
|
7883
|
+
*
|
|
7884
|
+
* - `amba export [file]` writes the current project's bundle to disk.
|
|
7885
|
+
* - `amba apply [file]` POSTs the local bundle to the import endpoint and
|
|
7886
|
+
* prints a `{ created, updated, skipped }` summary
|
|
7887
|
+
* per section (the server's authoritative result).
|
|
7888
|
+
* - `amba diff [file]` shows what `apply` WOULD change without writing.
|
|
7889
|
+
* The import route has NO server-side dry-run, so
|
|
7890
|
+
* `diff` exports the current project and computes the
|
|
7891
|
+
* created/updated/skipped per section LOCALLY.
|
|
7892
|
+
*/
|
|
7893
|
+
/** Default declarative bundle filename when no `[file]` is passed. */
|
|
7894
|
+
const DEFAULT_BUNDLE_FILE = "amba.config.json";
|
|
7895
|
+
/** The ordered list of bundle sections + their natural-key field. */
|
|
7896
|
+
const BUNDLE_SECTIONS = [
|
|
7897
|
+
{
|
|
7898
|
+
key: "remote_configs",
|
|
7899
|
+
naturalKey: "key"
|
|
7900
|
+
},
|
|
7901
|
+
{
|
|
7902
|
+
key: "collections",
|
|
7903
|
+
naturalKey: "name"
|
|
7904
|
+
},
|
|
7905
|
+
{
|
|
7906
|
+
key: "currencies",
|
|
7907
|
+
naturalKey: "code"
|
|
7908
|
+
},
|
|
7909
|
+
{
|
|
7910
|
+
key: "achievements",
|
|
7911
|
+
naturalKey: "key"
|
|
7912
|
+
},
|
|
7913
|
+
{
|
|
7914
|
+
key: "streaks",
|
|
7915
|
+
naturalKey: "key"
|
|
7916
|
+
},
|
|
7917
|
+
{
|
|
7918
|
+
key: "leaderboards",
|
|
7919
|
+
naturalKey: "name"
|
|
7920
|
+
},
|
|
7921
|
+
{
|
|
7922
|
+
key: "xp_rules",
|
|
7923
|
+
naturalKey: "name"
|
|
7924
|
+
},
|
|
7925
|
+
{
|
|
7926
|
+
key: "challenges",
|
|
7927
|
+
naturalKey: "name"
|
|
7928
|
+
},
|
|
7929
|
+
{
|
|
7930
|
+
key: "content_libraries",
|
|
7931
|
+
naturalKey: "name"
|
|
7932
|
+
},
|
|
7933
|
+
{
|
|
7934
|
+
key: "monetization",
|
|
7935
|
+
naturalKey: "key"
|
|
7936
|
+
}
|
|
7937
|
+
];
|
|
7938
|
+
/**
|
|
7939
|
+
* Read + parse a declarative bundle from disk. JSON only — we deliberately
|
|
7940
|
+
* don't pull in a YAML dependency the CLI doesn't already ship (the package
|
|
7941
|
+
* has no YAML lib), so `.yaml`/`.yml` files are rejected with a clear hint
|
|
7942
|
+
* rather than silently mis-parsed.
|
|
7943
|
+
*/
|
|
7944
|
+
async function readBundleFile(file) {
|
|
7945
|
+
const path = resolve(file);
|
|
7946
|
+
if (/\.ya?ml$/i.test(path)) throw new Error(`YAML bundles are not supported. Convert ${file} to JSON (the bundle format is plain JSON).`);
|
|
7947
|
+
let raw;
|
|
7948
|
+
try {
|
|
7949
|
+
raw = await readFile(path, "utf-8");
|
|
7950
|
+
} catch {
|
|
7951
|
+
throw new Error(`Bundle file not found: ${file}. Run \`amba export\` to scaffold one from your current project.`);
|
|
7952
|
+
}
|
|
7953
|
+
let parsed;
|
|
7954
|
+
try {
|
|
7955
|
+
parsed = JSON.parse(raw);
|
|
7956
|
+
} catch (err) {
|
|
7957
|
+
throw new Error(`Failed to parse ${file} as JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
7958
|
+
}
|
|
7959
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`Bundle in ${file} must be a JSON object with a \`version\` field.`);
|
|
7960
|
+
const bundle = parsed;
|
|
7961
|
+
if (typeof bundle["version"] !== "number") throw new Error(`Bundle in ${file} is missing a numeric \`version\` field. Re-export it with \`amba export\`.`);
|
|
7962
|
+
return bundle;
|
|
7963
|
+
}
|
|
7964
|
+
/**
|
|
7965
|
+
* Index a section array by its natural key. Definitions missing the key (or
|
|
7966
|
+
* non-object entries) are skipped — they can't be matched and the server
|
|
7967
|
+
* will reject them on apply.
|
|
7968
|
+
*/
|
|
7969
|
+
function indexByNaturalKey(items, naturalKey) {
|
|
7970
|
+
const out = /* @__PURE__ */ new Map();
|
|
7971
|
+
if (!Array.isArray(items)) return out;
|
|
7972
|
+
for (const item of items) {
|
|
7973
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) continue;
|
|
7974
|
+
const rec = item;
|
|
7975
|
+
const k = rec[naturalKey];
|
|
7976
|
+
if (typeof k !== "string" && typeof k !== "number") continue;
|
|
7977
|
+
out.set(String(k), rec);
|
|
7978
|
+
}
|
|
7979
|
+
return out;
|
|
7980
|
+
}
|
|
7981
|
+
/**
|
|
7982
|
+
* Pure section-by-section diff of a desired bundle against the current
|
|
7983
|
+
* project's exported bundle. The classification MIRRORS the server's import
|
|
7984
|
+
* (`apps/api/src/lib/promotion-bundle.ts`) so `amba diff` agrees with what
|
|
7985
|
+
* `amba apply` will report:
|
|
7986
|
+
* - created — natural key absent in current.
|
|
7987
|
+
* - updated — natural key present AND `mode === 'merge'`. The server runs an
|
|
7988
|
+
* UNCONDITIONAL update for every existing key in merge mode and
|
|
7989
|
+
* increments `updated` — it does NOT compare field values, so we
|
|
7990
|
+
* don't either (a local field comparison made `diff` under-report).
|
|
7991
|
+
* - skipped — natural key present under `skip_existing`; OR an existing
|
|
7992
|
+
* collection (always — collections are create-if-absent, never
|
|
7993
|
+
* merged); OR a `remote_configs` row flagged `is_system: true`
|
|
7994
|
+
* (the server always skips those before any existence check); OR
|
|
7995
|
+
* a desired row with no valid natural key (can't be matched
|
|
7996
|
+
* against the export, and the server import dedups by that key —
|
|
7997
|
+
* so it is never a reliable create).
|
|
7998
|
+
*
|
|
7999
|
+
* Within one bundle section, the server walks rows sequentially and each
|
|
8000
|
+
* insert mutates the DB, so a SECOND row sharing a natural key already created
|
|
8001
|
+
* by an EARLIER row in the same section is seen as existing (→ skip under
|
|
8002
|
+
* skip_existing, update under merge) rather than created a second time. We
|
|
8003
|
+
* track already-seen keys per section to mirror that, so duplicate-keyed local
|
|
8004
|
+
* rows don't inflate the `created` total versus what `apply` does.
|
|
8005
|
+
*/
|
|
8006
|
+
function computeLocalDiff(desired, current, mode) {
|
|
8007
|
+
const diff = {};
|
|
8008
|
+
for (const { key, naturalKey } of BUNDLE_SECTIONS) {
|
|
8009
|
+
const desiredItems = desired[key];
|
|
8010
|
+
if (!Array.isArray(desiredItems) || desiredItems.length === 0) continue;
|
|
8011
|
+
const currentIndex = indexByNaturalKey(current[key], naturalKey);
|
|
8012
|
+
const section = {
|
|
8013
|
+
created: 0,
|
|
8014
|
+
updated: 0,
|
|
8015
|
+
skipped: 0
|
|
8016
|
+
};
|
|
8017
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8018
|
+
for (const item of desiredItems) {
|
|
8019
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) continue;
|
|
8020
|
+
const rec = item;
|
|
8021
|
+
if (key === "remote_configs" && rec["is_system"] === true) {
|
|
8022
|
+
section.skipped++;
|
|
8023
|
+
continue;
|
|
8024
|
+
}
|
|
8025
|
+
const k = rec[naturalKey];
|
|
8026
|
+
const matchKey = typeof k === "string" || typeof k === "number" ? String(k) : null;
|
|
8027
|
+
if (matchKey === null) {
|
|
8028
|
+
section.skipped++;
|
|
8029
|
+
continue;
|
|
8030
|
+
}
|
|
8031
|
+
const existing = currentIndex.has(matchKey) || seen.has(matchKey);
|
|
8032
|
+
seen.add(matchKey);
|
|
8033
|
+
if (!existing) {
|
|
8034
|
+
section.created++;
|
|
8035
|
+
continue;
|
|
8036
|
+
}
|
|
8037
|
+
if (key === "collections" || mode === "skip_existing") {
|
|
8038
|
+
section.skipped++;
|
|
8039
|
+
continue;
|
|
8040
|
+
}
|
|
8041
|
+
section.updated++;
|
|
8042
|
+
}
|
|
8043
|
+
diff[key] = section;
|
|
8044
|
+
}
|
|
8045
|
+
const monDiff = computeMonetizationLocalDiff(desired["monetization"], current["monetization"], mode);
|
|
8046
|
+
if (monDiff) diff["monetization"] = monDiff;
|
|
8047
|
+
return diff;
|
|
8048
|
+
}
|
|
8049
|
+
/**
|
|
8050
|
+
* Natural keys per monetization sub-collection. All key on `key` except the
|
|
8051
|
+
* package join, which is unique within an offering — but for diff purposes the
|
|
8052
|
+
* package counts roll into their owning offering, so only the four top-level
|
|
8053
|
+
* collections are counted here.
|
|
8054
|
+
*/
|
|
8055
|
+
const MONETIZATION_SUBSECTIONS = [
|
|
8056
|
+
"entitlements",
|
|
8057
|
+
"products",
|
|
8058
|
+
"offerings",
|
|
8059
|
+
"paywalls"
|
|
8060
|
+
];
|
|
8061
|
+
function computeMonetizationLocalDiff(desired, current, mode) {
|
|
8062
|
+
if (desired === null || typeof desired !== "object" || Array.isArray(desired)) return null;
|
|
8063
|
+
const desiredObj = desired;
|
|
8064
|
+
const currentObj = current !== null && typeof current === "object" && !Array.isArray(current) ? current : {};
|
|
8065
|
+
const section = {
|
|
8066
|
+
created: 0,
|
|
8067
|
+
updated: 0,
|
|
8068
|
+
skipped: 0
|
|
8069
|
+
};
|
|
8070
|
+
let any = false;
|
|
8071
|
+
for (const sub of MONETIZATION_SUBSECTIONS) {
|
|
8072
|
+
const desiredItems = desiredObj[sub];
|
|
8073
|
+
if (!Array.isArray(desiredItems) || desiredItems.length === 0) continue;
|
|
8074
|
+
any = true;
|
|
8075
|
+
const currentIndex = indexByNaturalKey(currentObj[sub], "key");
|
|
8076
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8077
|
+
for (const item of desiredItems) {
|
|
8078
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) continue;
|
|
8079
|
+
const k = item["key"];
|
|
8080
|
+
const matchKey = typeof k === "string" || typeof k === "number" ? String(k) : null;
|
|
8081
|
+
if (matchKey === null) {
|
|
8082
|
+
section.skipped++;
|
|
8083
|
+
continue;
|
|
8084
|
+
}
|
|
8085
|
+
const existing = currentIndex.has(matchKey) || seen.has(matchKey);
|
|
8086
|
+
seen.add(matchKey);
|
|
8087
|
+
if (!existing) section.created++;
|
|
8088
|
+
else if (mode === "skip_existing") section.skipped++;
|
|
8089
|
+
else section.updated++;
|
|
8090
|
+
}
|
|
8091
|
+
}
|
|
8092
|
+
return any ? section : null;
|
|
8093
|
+
}
|
|
8094
|
+
/** Roll a per-section diff up into grand totals across every section. */
|
|
8095
|
+
function summarizeDiff(diff) {
|
|
8096
|
+
return Object.values(diff).reduce((acc, s) => ({
|
|
8097
|
+
created: acc.created + s.created,
|
|
8098
|
+
updated: acc.updated + s.updated,
|
|
8099
|
+
skipped: acc.skipped + s.skipped
|
|
8100
|
+
}), {
|
|
8101
|
+
created: 0,
|
|
8102
|
+
updated: 0,
|
|
8103
|
+
skipped: 0
|
|
8104
|
+
});
|
|
8105
|
+
}
|
|
8106
|
+
function renderSummary(summary, verb) {
|
|
8107
|
+
let created = 0;
|
|
8108
|
+
let updated = 0;
|
|
8109
|
+
let skipped = 0;
|
|
8110
|
+
const sections = Object.keys(summary);
|
|
8111
|
+
if (sections.length === 0) console.log(pc.dim(" Nothing to " + verb + " — bundle is empty."));
|
|
8112
|
+
for (const section of sections) {
|
|
8113
|
+
const s = summary[section];
|
|
8114
|
+
created += s.created;
|
|
8115
|
+
updated += s.updated;
|
|
8116
|
+
skipped += s.skipped;
|
|
8117
|
+
console.log(` ${pc.bold(section)} ${pc.green(`${s.created} created`)} · ${pc.cyan(`${s.updated} updated`)} · ` + pc.dim(`${s.skipped} skipped`));
|
|
8118
|
+
}
|
|
8119
|
+
console.log();
|
|
8120
|
+
console.log(pc.dim(` Total: ${created} created · ${updated} updated · ${skipped} skipped`));
|
|
8121
|
+
}
|
|
8122
|
+
/**
|
|
8123
|
+
* `amba export [file]` — write the current project's declarative bundle to
|
|
8124
|
+
* disk so it can be checked in, diffed, and re-applied.
|
|
8125
|
+
*/
|
|
8126
|
+
async function exportConfigCommand(file = DEFAULT_BUNDLE_FILE, options = {}) {
|
|
8127
|
+
const projectId = await requireProjectId(process.cwd(), options.project);
|
|
8128
|
+
const path = resolve(file);
|
|
8129
|
+
console.log();
|
|
8130
|
+
console.log(pc.bold(` amba export ${pc.cyan(file)}`));
|
|
8131
|
+
console.log(pc.dim(" ─────────────────────────────────"));
|
|
8132
|
+
console.log();
|
|
8133
|
+
console.log(pc.dim(` Project: ${projectId}`));
|
|
8134
|
+
console.log(pc.dim(" Fetching current configuration…"));
|
|
8135
|
+
const bundle = (await exportPromotionBundle(projectId)).data;
|
|
8136
|
+
await writeFile(path, JSON.stringify(bundle, null, 2) + "\n", "utf-8");
|
|
8137
|
+
const sectionCount = BUNDLE_SECTIONS.reduce((sum, { key }) => {
|
|
8138
|
+
const v = bundle[key];
|
|
8139
|
+
if (Array.isArray(v)) return sum + v.length;
|
|
8140
|
+
if (key === "monetization" && v !== null && typeof v === "object") {
|
|
8141
|
+
const m = v;
|
|
8142
|
+
return sum + MONETIZATION_SUBSECTIONS.reduce((n, sub) => n + (Array.isArray(m[sub]) ? m[sub].length : 0), 0);
|
|
8143
|
+
}
|
|
8144
|
+
return sum;
|
|
8145
|
+
}, 0);
|
|
8146
|
+
console.log();
|
|
8147
|
+
console.log(pc.green(" ✓") + ` Wrote ${pc.cyan(file)} (${sectionCount} definitions).`);
|
|
8148
|
+
console.log(pc.dim(" Edit it, then ") + pc.cyan("amba diff") + pc.dim(" to preview and ") + pc.cyan("amba apply") + pc.dim(" to apply."));
|
|
8149
|
+
console.log();
|
|
8150
|
+
}
|
|
8151
|
+
/**
|
|
8152
|
+
* `amba apply [file]` — read a local declarative bundle and POST it to the
|
|
8153
|
+
* import endpoint. Prints the server's authoritative `{ created, updated,
|
|
8154
|
+
* skipped }` per section.
|
|
8155
|
+
*/
|
|
8156
|
+
async function applyConfigCommand(file = DEFAULT_BUNDLE_FILE, options = {}) {
|
|
8157
|
+
const projectId = await requireProjectId(process.cwd(), options.project);
|
|
8158
|
+
const mode = options.mode ?? "skip_existing";
|
|
8159
|
+
const bundle = await readBundleFile(file);
|
|
8160
|
+
console.log();
|
|
8161
|
+
console.log(pc.bold(` amba apply ${pc.cyan(file)}`));
|
|
8162
|
+
console.log(pc.dim(" ─────────────────────────────────"));
|
|
8163
|
+
console.log();
|
|
8164
|
+
console.log(pc.dim(` Project: ${projectId}`));
|
|
8165
|
+
console.log(pc.dim(` Mode: ${mode}`));
|
|
8166
|
+
console.log(pc.dim(" Applying…"));
|
|
8167
|
+
console.log();
|
|
8168
|
+
let res;
|
|
8169
|
+
try {
|
|
8170
|
+
res = await importPromotionBundle(projectId, bundle, mode);
|
|
8171
|
+
} catch (err) {
|
|
8172
|
+
if (err instanceof ApiClientError && (err.statusCode === 400 || err.statusCode === 422)) throw new Error(`${err.message}\n → Re-export a fresh bundle with \`amba export\` if this was hand-edited.`);
|
|
8173
|
+
throw err;
|
|
8174
|
+
}
|
|
8175
|
+
renderSummary(res.data.summary, "apply");
|
|
8176
|
+
console.log();
|
|
8177
|
+
}
|
|
8178
|
+
/**
|
|
8179
|
+
* `amba diff [file]` — show what `apply` WOULD change without writing.
|
|
8180
|
+
*
|
|
8181
|
+
* The import route has no server-side dry-run, so this fetches the current
|
|
8182
|
+
* project's exported bundle and computes the diff LOCALLY. Output mirrors the
|
|
8183
|
+
* `apply` summary shape so the two read identically.
|
|
8184
|
+
*/
|
|
8185
|
+
async function diffConfigCommand(file = DEFAULT_BUNDLE_FILE, options = {}) {
|
|
8186
|
+
const projectId = await requireProjectId(process.cwd(), options.project);
|
|
8187
|
+
const mode = options.mode ?? "skip_existing";
|
|
8188
|
+
const desired = await readBundleFile(file);
|
|
8189
|
+
console.log();
|
|
8190
|
+
console.log(pc.bold(` amba diff ${pc.cyan(file)}`));
|
|
8191
|
+
console.log(pc.dim(" ─────────────────────────────────"));
|
|
8192
|
+
console.log();
|
|
8193
|
+
console.log(pc.dim(` Project: ${projectId}`));
|
|
8194
|
+
console.log(pc.dim(` Mode: ${mode}`));
|
|
8195
|
+
console.log(pc.dim(" (computed locally — the import route has no server dry-run)"));
|
|
8196
|
+
console.log(pc.dim(" Fetching current configuration…"));
|
|
8197
|
+
console.log();
|
|
8198
|
+
const diff = computeLocalDiff(desired, (await exportPromotionBundle(projectId)).data, mode);
|
|
8199
|
+
renderSummary(diff, "change");
|
|
8200
|
+
const totals = summarizeDiff(diff);
|
|
8201
|
+
console.log();
|
|
8202
|
+
if (totals.created > 0 || totals.updated > 0) console.log(pc.dim(" Run ") + pc.cyan(`amba apply ${file}`) + pc.dim(" to apply these changes."));
|
|
8203
|
+
else if (totals.skipped > 0) if (mode === "skip_existing") {
|
|
8204
|
+
console.log(pc.green(" ✓") + " apply would make no changes (skip_existing leaves existing entities untouched).");
|
|
8205
|
+
console.log(pc.dim(" Skipped entities are NOT compared — re-run with ") + pc.cyan("--mode merge") + pc.dim(" to overwrite them, or `amba export` to see current values."));
|
|
8206
|
+
} else console.log(pc.green(" ✓") + " apply would make no changes.");
|
|
8207
|
+
else console.log(pc.green(" ✓") + " No changes — nothing in the bundle to apply.");
|
|
8208
|
+
console.log();
|
|
8209
|
+
}
|
|
8210
|
+
//#endregion
|
|
6888
8211
|
//#region src/index.ts
|
|
6889
8212
|
const program = new Command();
|
|
6890
|
-
program.name("amba").description("amba — agent-native backend-as-a-service for mobile apps.").version("0.
|
|
8213
|
+
program.name("amba").description("amba — agent-native backend-as-a-service for mobile apps.").version("4.0.4");
|
|
6891
8214
|
program.option("--token <pat>", "Use a Personal Access Token for headless / CI / agent use (overrides ~/.amba/credentials.json + AMBA_PAT env)");
|
|
6892
8215
|
program.hook("preAction", (thisCommand) => {
|
|
6893
8216
|
setBearerOverride(resolveTokenSource({
|
|
@@ -7029,8 +8352,8 @@ program.command("schema").description("Schema export commands").command("export"
|
|
|
7029
8352
|
}));
|
|
7030
8353
|
});
|
|
7031
8354
|
const functions = program.command("functions").description("Customer serverless functions deployed to the edge");
|
|
7032
|
-
functions.command("deploy <
|
|
7033
|
-
await runAction(() => functionsDeployCommand(
|
|
8355
|
+
functions.command("deploy <target>").description("Bundle a function (a single file OR a directory) and deploy to the dispatch namespace. A directory resolves its entry by convention (index.ts/js/mjs) or via --entry.").option("--name <name>", "Function name (default: file name without extension, or the directory leaf name)").option("--entry <file>", "Entry file when <target> is a directory (relative to it). Default: index.ts/js/mjs.").option("--dry-run", "Bundle and report size without uploading").option("--rate-limit-window <duration>", "Rate-limit window: 60s | 5m | 1h").option("--rate-limit-max <int>", "Rate-limit max requests per window", (v) => Number.parseInt(v, 10)).option("--rate-limit-key <kind>", "Rate-limit bucket key: user_id | ip").option("--public", "Deploy WITHOUT the X-Api-Key gate so external webhook senders can call it. You MUST verify the webhook signature in your handler. Default: private.").action(async (target, opts) => {
|
|
8356
|
+
await runAction(() => functionsDeployCommand(target, opts));
|
|
7034
8357
|
});
|
|
7035
8358
|
functions.command("list").description("List active functions for the current project").action(async () => {
|
|
7036
8359
|
await runAction(functionsListCommand);
|
|
@@ -7047,11 +8370,12 @@ functions.command("dev <file>").description("Run a local dev server for your fun
|
|
|
7047
8370
|
noWatch: opts.watch === false
|
|
7048
8371
|
}));
|
|
7049
8372
|
});
|
|
7050
|
-
functions.command("logs <name>").description("Stream log events for a deployed function").option("--since <iso>", "Start of the time range (default: 1 hour ago)").option("--until <iso>", "End of the time range (default: now). Ignored on --tail.").option("--limit <n>", "Max events per fetch (default 100, max 1000)", (v) => parseInt(v, 10)).option("--tail", "Follow new events; polls every 3s. Ctrl+C to stop.").option("--follow", "Alias for --tail (kept for backwards compatibility with v1 log commands).").option("--json", "NDJSON output to stdout (one event per line)").action(async (name, opts) => {
|
|
8373
|
+
functions.command("logs <name>").description("Stream log events for a deployed function").option("--since <iso>", "Start of the time range (default: 1 hour ago)").option("--until <iso>", "End of the time range (default: now). Ignored on --tail.").option("--limit <n>", "Max events per fetch (default 100, max 1000)", (v) => parseInt(v, 10)).option("--request-id <id>", "Only show events whose logs mention this request id (e.g. r_...). Works with --tail.").option("--tail", "Follow new events; polls every 3s. Ctrl+C to stop.").option("--follow", "Alias for --tail (kept for backwards compatibility with v1 log commands).").option("--json", "NDJSON output to stdout (one event per line)").action(async (name, opts) => {
|
|
7051
8374
|
await runAction(() => functionsLogsCommand(name, {
|
|
7052
8375
|
since: opts.since,
|
|
7053
8376
|
until: opts.until,
|
|
7054
8377
|
limit: opts.limit,
|
|
8378
|
+
requestId: opts.requestId,
|
|
7055
8379
|
tail: opts.tail || opts.follow,
|
|
7056
8380
|
json: opts.json
|
|
7057
8381
|
}));
|
|
@@ -7114,6 +8438,60 @@ billing.command("set-ceiling <amount>").description("Cap the monthly bill at <am
|
|
|
7114
8438
|
}
|
|
7115
8439
|
await runAction(() => billingSetCeilingCommand({ ceiling }));
|
|
7116
8440
|
});
|
|
8441
|
+
const payments = program.command("payments").description("Accept payments in your app (Amba is the rail; your app is the seller)");
|
|
8442
|
+
payments.command("setup").description("Create the connected payments account and print the hosted onboarding URL").option("--country <code>", "Two-letter ISO country code for the seller (e.g. US)").option("--email <email>", "Contact email for the connected account").action(async (opts) => {
|
|
8443
|
+
if (opts.country && !/^[A-Za-z]{2}$/.test(opts.country)) {
|
|
8444
|
+
console.log(" ✗ --country must be a two-letter ISO code (e.g. US)");
|
|
8445
|
+
process.exit(1);
|
|
8446
|
+
}
|
|
8447
|
+
await runAction(() => paymentsSetupCommand({
|
|
8448
|
+
country: opts.country ? opts.country.toUpperCase() : void 0,
|
|
8449
|
+
email: opts.email
|
|
8450
|
+
}));
|
|
8451
|
+
});
|
|
8452
|
+
payments.command("status").description("Show onboarding stage + whether charges/payouts are enabled").action(async () => {
|
|
8453
|
+
await runAction(paymentsStatusCommand);
|
|
8454
|
+
});
|
|
8455
|
+
payments.command("set-fee <bps>").description("Set the default platform fee in basis points (250 = 2.5%), or 'off' to clear").action(async (bps) => {
|
|
8456
|
+
let value;
|
|
8457
|
+
if (bps.toLowerCase() === "off" || bps === "") value = null;
|
|
8458
|
+
else {
|
|
8459
|
+
const parsed = Number(bps);
|
|
8460
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 1e4) {
|
|
8461
|
+
console.log(" ✗ bps must be an integer between 0 and 10000, or \"off\"");
|
|
8462
|
+
process.exit(1);
|
|
8463
|
+
}
|
|
8464
|
+
value = parsed;
|
|
8465
|
+
}
|
|
8466
|
+
await runAction(() => paymentsSetFeeCommand({ bps: value }));
|
|
8467
|
+
});
|
|
8468
|
+
payments.command("balance").description("Show the connected account's available and pending balance").action(async () => {
|
|
8469
|
+
await runAction(paymentsBalanceCommand);
|
|
8470
|
+
});
|
|
8471
|
+
payments.command("payouts").description("List recent payouts to the seller bank account").option("--limit <n>", "How many payouts to show (default 10)").action(async (opts) => {
|
|
8472
|
+
const limit = opts.limit ? Number(opts.limit) : void 0;
|
|
8473
|
+
if (limit !== void 0 && (!Number.isInteger(limit) || limit < 1 || limit > 100)) {
|
|
8474
|
+
console.log(" ✗ --limit must be an integer between 1 and 100");
|
|
8475
|
+
process.exit(1);
|
|
8476
|
+
}
|
|
8477
|
+
await runAction(() => paymentsPayoutsCommand({ limit }));
|
|
8478
|
+
});
|
|
8479
|
+
const monetization = program.command("monetization").description("Monetization control plane: plan, drift, export, adopt, and apply your subscription config as Infrastructure-as-Code.");
|
|
8480
|
+
monetization.command("plan").description("Preview the three-way diff (declared vs adopted vs live) + store-floor + drift").action(async () => {
|
|
8481
|
+
await runAction(monetizationPlanCommand);
|
|
8482
|
+
});
|
|
8483
|
+
monetization.command("drift").description("Show managed objects changed out-of-band in the provider").action(async () => {
|
|
8484
|
+
await runAction(monetizationDriftCommand);
|
|
8485
|
+
});
|
|
8486
|
+
monetization.command("export").description("Snapshot the live monetization config to a declarative bundle (stdout or --file)").option("--file <path>", "Write the bundle to this file instead of stdout").action(async (opts) => {
|
|
8487
|
+
await runAction(() => monetizationExportCommand(opts));
|
|
8488
|
+
});
|
|
8489
|
+
monetization.command("adopt").description("Record the live config as the managed baseline (writes Amba state only; idempotent)").option("--all", "Adopt the entire live config").action(async (opts) => {
|
|
8490
|
+
await runAction(() => monetizationAdoptCommand(opts));
|
|
8491
|
+
});
|
|
8492
|
+
monetization.command("apply").description("Apply the declared config to the provider. Additive ops (create entitlements/offerings/packages/paywall-drafts + attach products) apply automatically; destructive ops (detach/archive/remove) need --confirm.").option("--confirm", "Confirm and execute the gated destructive ops (detach/archive/remove). Without it they are reported but not run.").option("--allow-detach-live", "Accept detaching a product a live customer currently resolves (otherwise refused).").option("--reconcile <mode>", "Out-of-band drift mode: 'amba' (default) or 'adopt'.").action(async (opts) => {
|
|
8493
|
+
await runAction(() => monetizationApplyCommand(opts));
|
|
8494
|
+
});
|
|
7117
8495
|
const collections = program.command("collections").description("Customer collections (schema-first Postgres in each tenant database)");
|
|
7118
8496
|
collections.command("create <name>").description("Create a collection with the given fields").option("--field <spec>", "Field spec: name:type[:nullable] (e.g. user_id:uuid, parsed:jsonb:nullable). Repeatable.", (val, prev) => [...prev ?? [], val], []).option("--index <spec>", "Index spec: \"col1 [asc|desc], col2 [asc|desc]\". Repeatable.", (val, prev) => [...prev ?? [], val], []).action(async (name, opts) => {
|
|
7119
8497
|
await runAction(() => collectionsCreateCommand(name, opts));
|
|
@@ -7199,6 +8577,29 @@ aiProviders.command("list").description("List registered AI providers for the cu
|
|
|
7199
8577
|
aiProviders.command("delete <provider>").description("Remove a provider registration (refuses if active prompts reference it)").action(async (provider) => {
|
|
7200
8578
|
await runAction(() => aiProvidersDeleteCommand(provider));
|
|
7201
8579
|
});
|
|
8580
|
+
function parseImportMode(raw) {
|
|
8581
|
+
if (raw === void 0) return "skip_existing";
|
|
8582
|
+
if (raw === "merge" || raw === "skip_existing") return raw;
|
|
8583
|
+
console.log(" ✗ --mode must be \"skip_existing\" (default) or \"merge\"");
|
|
8584
|
+
process.exit(1);
|
|
8585
|
+
}
|
|
8586
|
+
program.command("export [file]").description("Write the current project's declarative configuration bundle to disk (default: amba.config.json)").option("--project <id>", "Project id (overrides .env.local)").action(async (file, opts) => {
|
|
8587
|
+
await runAction(() => exportConfigCommand(file, { project: opts.project }));
|
|
8588
|
+
});
|
|
8589
|
+
program.command("diff [file]").description("Show what `amba apply` WOULD change (created/updated/skipped per section) without writing. Computed locally by exporting the current project and comparing — the import route has no server dry-run.").option("--project <id>", "Project id (overrides .env.local)").option("--mode <mode>", "'skip_existing' (default) or 'merge'").action(async (file, opts) => {
|
|
8590
|
+
const mode = parseImportMode(opts.mode);
|
|
8591
|
+
await runAction(() => diffConfigCommand(file, {
|
|
8592
|
+
project: opts.project,
|
|
8593
|
+
mode
|
|
8594
|
+
}));
|
|
8595
|
+
});
|
|
8596
|
+
program.command("apply [file]").description("Apply a declarative configuration bundle (default: amba.config.json) — JSON only. Prints { created, updated, skipped } per section.").option("--project <id>", "Project id (overrides .env.local)").option("--mode <mode>", "'skip_existing' (default — no-op on conflict) or 'merge' (refresh existing definitions)").action(async (file, opts) => {
|
|
8597
|
+
const mode = parseImportMode(opts.mode);
|
|
8598
|
+
await runAction(() => applyConfigCommand(file, {
|
|
8599
|
+
project: opts.project,
|
|
8600
|
+
mode
|
|
8601
|
+
}));
|
|
8602
|
+
});
|
|
7202
8603
|
program.parse();
|
|
7203
8604
|
//#endregion
|
|
7204
8605
|
export {};
|