@nebulr-group/bridge-cli 0.1.3 → 0.4.0-beta.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.
Files changed (50) hide show
  1. package/README.md +26 -4
  2. package/dist/auth/loopback.d.ts.map +1 -1
  3. package/dist/auth/loopback.js +8 -0
  4. package/dist/auth/loopback.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +4 -0
  7. package/dist/cli.js.map +1 -1
  8. package/dist/commands/app.command.d.ts.map +1 -1
  9. package/dist/commands/app.command.js +2 -0
  10. package/dist/commands/app.command.js.map +1 -1
  11. package/dist/commands/auth/login.command.d.ts +12 -0
  12. package/dist/commands/auth/login.command.d.ts.map +1 -1
  13. package/dist/commands/auth/login.command.js +2 -1
  14. package/dist/commands/auth/login.command.js.map +1 -1
  15. package/dist/commands/flag-init.command.d.ts +13 -0
  16. package/dist/commands/flag-init.command.d.ts.map +1 -0
  17. package/dist/commands/flag-init.command.js +353 -0
  18. package/dist/commands/flag-init.command.js.map +1 -0
  19. package/dist/commands/flag.command.d.ts +92 -0
  20. package/dist/commands/flag.command.d.ts.map +1 -1
  21. package/dist/commands/flag.command.js +784 -25
  22. package/dist/commands/flag.command.js.map +1 -1
  23. package/dist/commands/guide.command.d.ts +21 -0
  24. package/dist/commands/guide.command.d.ts.map +1 -1
  25. package/dist/commands/guide.command.js +326 -18
  26. package/dist/commands/guide.command.js.map +1 -1
  27. package/dist/commands/integrate.command.js +3 -3
  28. package/dist/commands/integrate.command.js.map +1 -1
  29. package/dist/commands/ops.command.d.ts +17 -0
  30. package/dist/commands/ops.command.d.ts.map +1 -0
  31. package/dist/commands/ops.command.js +129 -0
  32. package/dist/commands/ops.command.js.map +1 -0
  33. package/dist/commands/plan.command.d.ts +31 -0
  34. package/dist/commands/plan.command.d.ts.map +1 -1
  35. package/dist/commands/plan.command.js +182 -1
  36. package/dist/commands/plan.command.js.map +1 -1
  37. package/dist/commands/stripe.command.d.ts +3 -0
  38. package/dist/commands/stripe.command.d.ts.map +1 -0
  39. package/dist/commands/stripe.command.js +43 -0
  40. package/dist/commands/stripe.command.js.map +1 -0
  41. package/dist/output.d.ts +8 -0
  42. package/dist/output.d.ts.map +1 -1
  43. package/dist/output.js +10 -0
  44. package/dist/output.js.map +1 -1
  45. package/dist/prompts/architecture.md +560 -0
  46. package/dist/prompts/auth-master-integration-prompt.md +251 -0
  47. package/dist/prompts/billing/master.md +269 -0
  48. package/dist/prompts/flags/master.md +215 -0
  49. package/dist/prompts/integration-success.md +107 -0
  50. package/package.json +4 -3
@@ -1,66 +1,197 @@
1
+ // TBP-192 — Flag CLI parity for FF 2.0.
2
+ //
3
+ // The CLI's `flag` subcommand exposes the 2.0 mental model:
4
+ // - Three-state model: off | on | on-with-rule
5
+ // - Multi-type values: boolean | string | number | json
6
+ // - Inline rules (branches + conditions + rolloutPct)
7
+ // - Scheduling (data-only; the runner is TBP-189 — we just set/clear the field)
8
+ // - Local eval helper for debugging
9
+ //
10
+ // Types are now first-class via @nebulr-group/bridge-auth-core 0.2.0-wt3.0:
11
+ // `FlagResponse`, `CreateFlagInput`, `UpdateFlagInput`, `FlagSchedule`,
12
+ // `FlagState`, and `FlagValueType` all describe the FF 2.0 management surface
13
+ // directly, so the SDK-boundary `as never` / `as unknown as Record<string,
14
+ // unknown>[]` casts from the 0.1.x era are gone. The local CLI rule shape
15
+ // (`Condition`, `Branch`, `Rule` below) is intentionally kept distinct from
16
+ // the package's `Rule`/`Condition` — see the note above the local interfaces.
17
+ import { readFileSync, writeFileSync } from 'node:fs';
1
18
  import { getManagementClient } from '../config.js';
2
19
  import { outputSuccess, outputError } from '../output.js';
20
+ import { registerFlagInitCommand } from './flag-init.command.js';
21
+ const FLAG_STATES = ['off', 'on', 'on-with-rule'];
22
+ const FLAG_VALUE_TYPES = ['boolean', 'string', 'number', 'json'];
23
+ // ── Public registration ─────────────────────────────────────────────────────
3
24
  export function registerFlagCommands(program) {
4
- const flag = program.command('flag').description('Manage feature flags');
5
- flag.command('list')
6
- .description('List all feature flags')
25
+ const flag = program
26
+ .command('flag')
27
+ .description([
28
+ 'Manage feature flags (Feature Flags 2.0 mental model)',
29
+ '',
30
+ ' state off | on | on-with-rule',
31
+ ' values boolean | string | number | json (typed at flag level)',
32
+ ' rule branches[] + otherwiseValue + rolloutPct (first-match-wins)',
33
+ ].join('\n'));
34
+ registerList(flag);
35
+ registerGet(flag);
36
+ registerCreate(flag);
37
+ registerUpdate(flag);
38
+ registerToggle(flag);
39
+ registerDelete(flag);
40
+ registerEval(flag);
41
+ registerSchedule(flag);
42
+ registerExport(flag);
43
+ registerImport(flag);
44
+ registerFlagInitCommand(flag);
45
+ }
46
+ // ── list ────────────────────────────────────────────────────────────────────
47
+ function registerList(flag) {
48
+ flag
49
+ .command('list')
50
+ .description('List feature flags (shows state + value type)')
7
51
  .action(async () => {
8
52
  try {
9
- outputSuccess(await getManagementClient().flags.list());
53
+ const flags = await getManagementClient().flags.list();
54
+ // Normalize so callers always see the same shape even if the API
55
+ // returns the legacy fields only.
56
+ const rows = flags.map((f) => ({
57
+ id: f.id,
58
+ key: f.key,
59
+ description: f.description,
60
+ state: f.state ?? deriveState(f),
61
+ valueType: f.valueType ?? 'boolean',
62
+ enabled: f.enabled,
63
+ }));
64
+ outputSuccess(rows);
10
65
  }
11
66
  catch (err) {
12
67
  outputError(err);
13
68
  }
14
69
  });
15
- flag.command('create')
16
- .description('Create a new feature flag')
70
+ }
71
+ // ── get ─────────────────────────────────────────────────────────────────────
72
+ function registerGet(flag) {
73
+ flag
74
+ .command('get')
75
+ .argument('<key>', 'Flag key')
76
+ .description('Get a flag (rich 2.0 shape: state, value type, rule, observability)')
77
+ .action(async (key) => {
78
+ try {
79
+ const all = await getManagementClient().flags.list();
80
+ const found = all.find((f) => f.key === key);
81
+ if (!found) {
82
+ outputError(new Error(`Flag not found: ${key}`));
83
+ return;
84
+ }
85
+ outputSuccess({
86
+ id: found.id,
87
+ key: found.key,
88
+ description: found.description,
89
+ state: found.state ?? deriveState(found),
90
+ valueType: found.valueType ?? 'boolean',
91
+ offValue: found.offValue,
92
+ onValue: found.onValue,
93
+ rule: found.rule,
94
+ schedule: found.schedule,
95
+ observability: {
96
+ evalCount: found.evalCount,
97
+ lastEvalAt: found.lastEvalAt,
98
+ },
99
+ // Echo the legacy fields too — useful while 1.0 admin clients exist.
100
+ legacy: {
101
+ enabled: found.enabled,
102
+ defaultValue: found.defaultValue,
103
+ targetValue: found.targetValue,
104
+ },
105
+ });
106
+ }
107
+ catch (err) {
108
+ outputError(err);
109
+ }
110
+ });
111
+ }
112
+ // ── create ──────────────────────────────────────────────────────────────────
113
+ function registerCreate(flag) {
114
+ flag
115
+ .command('create')
116
+ .description('Create a new feature flag (2.0 fields)')
17
117
  .requiredOption('--key <key>', 'Flag key')
18
118
  .option('--description <desc>', 'Description')
19
- .option('--enabled', 'Enable the flag', false)
20
- .option('--default-value', 'Default value when no segment matches', false)
119
+ .option('--state <state>', `Three-state model: ${FLAG_STATES.join(' | ')} (default: off)`)
120
+ .option('--value-type <type>', `Flag value type: ${FLAG_VALUE_TYPES.join(' | ')} (default: boolean)`)
121
+ .option('--on-value <value>', 'Value returned when state=on (parsed per --value-type; JSON string for json type)')
122
+ .option('--off-value <value>', 'Value returned when state=off (parsed per --value-type)')
123
+ .option('--rule <json>', 'Rule as a JSON string: {"branches":[...],"otherwiseValue":...,"rolloutPct":100}')
124
+ // Legacy 1.0 fields kept so existing scripts don't break.
125
+ .option('--enabled', 'Legacy: enable the flag', false)
126
+ .option('--default-value', 'Legacy: default value when no segment matches', false)
21
127
  .action(async (opts) => {
22
128
  try {
23
- outputSuccess(await getManagementClient().flags.create({
24
- key: opts.key,
25
- description: opts.description,
26
- enabled: opts.enabled,
27
- defaultValue: opts.defaultValue,
28
- }));
129
+ // `--key` is a requiredOption above, so `key` is always present at
130
+ // runtime even though buildFlagWritePayload's generic return type
131
+ // can't express that. The `unknown` step is just to convince TS the
132
+ // intent is deliberate.
133
+ const payload = buildFlagWritePayload(opts);
134
+ const flag = await getManagementClient().flags.create(payload);
135
+ outputSuccess(flag);
29
136
  }
30
137
  catch (err) {
31
138
  outputError(err);
32
139
  }
33
140
  });
34
- flag.command('update')
35
- .description('Update a feature flag')
141
+ }
142
+ // ── update ──────────────────────────────────────────────────────────────────
143
+ function registerUpdate(flag) {
144
+ flag
145
+ .command('update')
146
+ .description('Update a feature flag (2.0 fields)')
36
147
  .requiredOption('--id <id>', 'Flag ID')
37
148
  .option('--key <key>', 'Flag key')
38
149
  .option('--description <desc>', 'Description')
39
- .option('--enabled <bool>', 'Enable/disable', (v) => v === 'true')
40
- .option('--default-value <bool>', 'Default value', (v) => v === 'true')
150
+ .option('--state <state>', `Three-state model: ${FLAG_STATES.join(' | ')}`)
151
+ .option('--value-type <type>', `Flag value type: ${FLAG_VALUE_TYPES.join(' | ')}`)
152
+ .option('--on-value <value>', 'Value returned when state=on (parsed per --value-type)')
153
+ .option('--off-value <value>', 'Value returned when state=off (parsed per --value-type)')
154
+ .option('--rule <json>', 'Rule as a JSON string (must validate via validateRule)')
155
+ .option('--clear-rule', 'Remove the existing rule')
156
+ // Legacy 1.0
157
+ .option('--enabled <bool>', 'Legacy: enable/disable', (v) => v === 'true')
158
+ .option('--default-value <bool>', 'Legacy: default value', (v) => v === 'true')
41
159
  .action(async (opts) => {
42
160
  try {
43
- const { id, ...data } = opts;
44
- const cleaned = Object.fromEntries(Object.entries(data).filter(([, v]) => v !== undefined));
45
- outputSuccess(await getManagementClient().flags.update(id, cleaned));
161
+ const { id } = opts;
162
+ const payload = buildFlagWritePayload(opts, { partial: true });
163
+ if (opts.clearRule) {
164
+ payload.rule = null;
165
+ }
166
+ const flag = await getManagementClient().flags.update(id, payload);
167
+ outputSuccess(flag);
46
168
  }
47
169
  catch (err) {
48
170
  outputError(err);
49
171
  }
50
172
  });
51
- flag.command('toggle')
52
- .description('Toggle a feature flag on/off')
173
+ }
174
+ // ── toggle (kept from 1.0; convenience for state on/off without typing JSON)
175
+ function registerToggle(flag) {
176
+ flag
177
+ .command('toggle')
178
+ .description('Quick toggle a flag on or off (does not affect rule)')
53
179
  .requiredOption('--id <id>', 'Flag ID')
54
180
  .requiredOption('--enabled <bool>', 'true or false', (v) => v === 'true')
55
181
  .action(async (opts) => {
56
182
  try {
57
- outputSuccess(await getManagementClient().flags.toggle(opts.id, opts.enabled));
183
+ const result = await getManagementClient().flags.toggle(opts.id, opts.enabled);
184
+ outputSuccess(result);
58
185
  }
59
186
  catch (err) {
60
187
  outputError(err);
61
188
  }
62
189
  });
63
- flag.command('delete')
190
+ }
191
+ // ── delete ──────────────────────────────────────────────────────────────────
192
+ function registerDelete(flag) {
193
+ flag
194
+ .command('delete')
64
195
  .description('Delete a feature flag')
65
196
  .requiredOption('--id <id>', 'Flag ID')
66
197
  .action(async (opts) => {
@@ -73,4 +204,632 @@ export function registerFlagCommands(program) {
73
204
  }
74
205
  });
75
206
  }
207
+ // ── eval (local SDK-style evaluation, for debugging) ────────────────────────
208
+ function registerEval(flag) {
209
+ flag
210
+ .command('eval')
211
+ .argument('<key>', 'Flag key')
212
+ .description('Locally evaluate a flag against a synthetic context (debug helper)')
213
+ .option('--identity <id>', 'Eval identity (required when rolloutPct < 100)')
214
+ .option('--attribute <kv...>', 'One or more key=value attributes (use multiple flags or space-separate)')
215
+ .action(async (key, opts) => {
216
+ try {
217
+ const all = await getManagementClient().flags.list();
218
+ const found = all.find((f) => f.key === key);
219
+ if (!found) {
220
+ outputError(new Error(`Flag not found: ${key}`));
221
+ return;
222
+ }
223
+ const attributes = parseAttributes(opts.attribute ?? []);
224
+ const ctx = { identity: opts.identity, attributes };
225
+ const cached = {
226
+ key: found.key,
227
+ state: found.state ?? deriveState(found),
228
+ valueType: found.valueType ?? 'boolean',
229
+ offValue: found.offValue ?? false,
230
+ onValue: found.onValue ?? true,
231
+ // Bridge from auth-core's package `Rule` shape (plural `values`) to
232
+ // the CLI-local `Rule` shape (singular `value`). See note above the
233
+ // local interfaces.
234
+ rule: found.rule ?? undefined,
235
+ };
236
+ const result = evaluateLocally(cached, ctx);
237
+ outputSuccess({
238
+ flag: cached.key,
239
+ state: cached.state,
240
+ context: ctx,
241
+ result,
242
+ });
243
+ }
244
+ catch (err) {
245
+ outputError(err);
246
+ }
247
+ });
248
+ }
249
+ // ── schedule ────────────────────────────────────────────────────────────────
250
+ function registerSchedule(flag) {
251
+ const schedule = flag
252
+ .command('schedule')
253
+ .description('Manage scheduled state changes (TBP-189 — runner not yet active)');
254
+ schedule
255
+ .command('set')
256
+ .argument('<key>', 'Flag key')
257
+ .description('Schedule a state transition at an ISO timestamp')
258
+ .requiredOption('--at <iso>', 'ISO-8601 timestamp (e.g. 2026-05-20T09:00:00Z)')
259
+ .requiredOption('--state <state>', `Target state: ${FLAG_STATES.join(' | ')}`)
260
+ .action(async (key, opts) => {
261
+ try {
262
+ if (!FLAG_STATES.includes(opts.state)) {
263
+ throw new Error(`Invalid --state: ${opts.state}. Must be one of ${FLAG_STATES.join(', ')}.`);
264
+ }
265
+ const at = new Date(opts.at);
266
+ if (Number.isNaN(at.getTime())) {
267
+ throw new Error(`Invalid --at: ${opts.at} is not a valid ISO-8601 timestamp.`);
268
+ }
269
+ const id = await resolveFlagId(key);
270
+ const schedule = { at: at.toISOString(), state: opts.state };
271
+ const update = { schedule };
272
+ const result = await getManagementClient().flags.update(id, update);
273
+ outputSuccess(result);
274
+ }
275
+ catch (err) {
276
+ outputError(err);
277
+ }
278
+ });
279
+ schedule
280
+ .command('clear')
281
+ .argument('<key>', 'Flag key')
282
+ .description('Clear an existing schedule')
283
+ .action(async (key) => {
284
+ try {
285
+ const id = await resolveFlagId(key);
286
+ const update = { schedule: null };
287
+ const result = await getManagementClient().flags.update(id, update);
288
+ outputSuccess(result);
289
+ }
290
+ catch (err) {
291
+ outputError(err);
292
+ }
293
+ });
294
+ }
295
+ /** Build the canonical, portable doc from a remote flag record. Pure. */
296
+ export function toFlagDoc(f) {
297
+ return {
298
+ key: f.key,
299
+ description: f.description,
300
+ state: f.state ?? deriveState(f),
301
+ valueType: f.valueType ?? 'boolean',
302
+ offValue: f.offValue,
303
+ onValue: f.onValue,
304
+ rule: f.rule,
305
+ schedule: f.schedule,
306
+ };
307
+ }
308
+ /** Build a create/update write payload from a doc. Pure. */
309
+ export function buildWritePayloadFromDoc(doc) {
310
+ const payload = {
311
+ key: doc.key,
312
+ state: doc.state,
313
+ valueType: doc.valueType,
314
+ };
315
+ if (doc.description !== undefined)
316
+ payload.description = doc.description;
317
+ if (doc.offValue !== undefined)
318
+ payload.offValue = doc.offValue;
319
+ if (doc.onValue !== undefined)
320
+ payload.onValue = doc.onValue;
321
+ if (doc.rule !== undefined)
322
+ payload.rule = doc.rule;
323
+ if (doc.schedule !== undefined)
324
+ payload.schedule = doc.schedule;
325
+ return payload;
326
+ }
327
+ /** Validate + normalize a doc parsed from a file. Throws on the first problem. */
328
+ export function validateFlagDoc(input, index) {
329
+ if (!input || typeof input !== 'object') {
330
+ throw new Error(`flags[${index}] must be an object.`);
331
+ }
332
+ const d = input;
333
+ if (typeof d.key !== 'string' || d.key.length === 0) {
334
+ throw new Error(`flags[${index}] is missing a non-empty "key".`);
335
+ }
336
+ const state = d.state ?? 'off';
337
+ if (!FLAG_STATES.includes(state)) {
338
+ throw new Error(`flags[${index}] ("${d.key}") invalid state "${String(d.state)}". One of ${FLAG_STATES.join(', ')}.`);
339
+ }
340
+ const valueType = d.valueType ?? 'boolean';
341
+ if (!FLAG_VALUE_TYPES.includes(valueType)) {
342
+ throw new Error(`flags[${index}] ("${d.key}") invalid valueType "${String(d.valueType)}". One of ${FLAG_VALUE_TYPES.join(', ')}.`);
343
+ }
344
+ if (d.rule !== undefined && d.rule !== null) {
345
+ const errs = validateRule(d.rule);
346
+ if (errs.length > 0) {
347
+ throw new Error(`flags[${index}] ("${d.key}") rule failed validation:\n${errs.map((e) => ` - ${e.message}`).join('\n')}`);
348
+ }
349
+ }
350
+ return {
351
+ key: d.key,
352
+ description: d.description,
353
+ state,
354
+ valueType,
355
+ offValue: d.offValue,
356
+ onValue: d.onValue,
357
+ rule: d.rule,
358
+ schedule: d.schedule,
359
+ };
360
+ }
361
+ const DOC_FIELDS = [
362
+ 'description',
363
+ 'state',
364
+ 'valueType',
365
+ 'offValue',
366
+ 'onValue',
367
+ 'rule',
368
+ 'schedule',
369
+ ];
370
+ function docChanges(a, b) {
371
+ return DOC_FIELDS.filter((f) => JSON.stringify(a[f] ?? null) !== JSON.stringify(b[f] ?? null));
372
+ }
373
+ /** Diff file docs against remote flags (matched by key). Pure. */
374
+ export function diffFlags(remote, docs, opts = {}) {
375
+ const byKey = new Map(remote.map((f) => [f.key, f]));
376
+ const fileKeys = new Set(docs.map((d) => d.key));
377
+ const diff = { create: [], update: [], prune: [], unchanged: [] };
378
+ for (const doc of docs) {
379
+ const r = byKey.get(doc.key);
380
+ if (!r) {
381
+ diff.create.push(doc);
382
+ continue;
383
+ }
384
+ const changes = docChanges(toFlagDoc(r), doc);
385
+ if (changes.length === 0)
386
+ diff.unchanged.push(doc.key);
387
+ else
388
+ diff.update.push({ id: r.id, doc, changes });
389
+ }
390
+ if (opts.prune) {
391
+ for (const r of remote) {
392
+ if (!fileKeys.has(r.key)) {
393
+ diff.prune.push({ id: r.id, key: r.key });
394
+ }
395
+ }
396
+ }
397
+ return diff;
398
+ }
399
+ function registerExport(flag) {
400
+ flag
401
+ .command('export')
402
+ .description('Export all feature flags as code (canonical 2.0 JSON)')
403
+ .option('--out <file>', 'Write to a file instead of stdout')
404
+ .action(async (opts) => {
405
+ try {
406
+ const all = (await getManagementClient().flags.list());
407
+ const flags = all.map(toFlagDoc).sort((a, b) => a.key.localeCompare(b.key));
408
+ const bundle = { version: 1, flags };
409
+ if (opts.out) {
410
+ writeFileSync(opts.out, JSON.stringify(bundle, null, 2) + '\n', 'utf8');
411
+ outputSuccess({ written: opts.out, count: flags.length });
412
+ }
413
+ else {
414
+ outputSuccess(bundle);
415
+ }
416
+ }
417
+ catch (err) {
418
+ outputError(err);
419
+ }
420
+ });
421
+ }
422
+ function registerImport(flag) {
423
+ flag
424
+ .command('import')
425
+ .argument('<file>', 'Path to a flags JSON file (from `flag export`)')
426
+ .description('Apply feature flags from a file (create + update; --prune deletes extras)')
427
+ .option('--dry-run', 'Show the diff without applying any changes', false)
428
+ .option('--prune', 'Delete remote flags not present in the file', false)
429
+ .action(async (file, opts) => {
430
+ try {
431
+ let raw;
432
+ try {
433
+ raw = JSON.parse(readFileSync(file, 'utf8'));
434
+ }
435
+ catch (err) {
436
+ throw new Error(`Could not read/parse ${file}: ${err instanceof Error ? err.message : String(err)}`);
437
+ }
438
+ const list = Array.isArray(raw) ? raw : raw.flags;
439
+ if (!Array.isArray(list)) {
440
+ throw new Error('File must be an array of flags or an object with a "flags" array.');
441
+ }
442
+ const docs = list.map((d, i) => validateFlagDoc(d, i));
443
+ const remote = (await getManagementClient().flags.list());
444
+ const diff = diffFlags(remote, docs, { prune: opts.prune });
445
+ if (opts.dryRun) {
446
+ outputSuccess({
447
+ dryRun: true,
448
+ create: diff.create.map((d) => d.key),
449
+ update: diff.update.map((u) => ({ key: u.doc.key, changes: u.changes })),
450
+ prune: diff.prune.map((p) => p.key),
451
+ unchanged: diff.unchanged,
452
+ });
453
+ return;
454
+ }
455
+ const mgmt = getManagementClient();
456
+ for (const doc of diff.create) {
457
+ await mgmt.flags.create(buildWritePayloadFromDoc(doc));
458
+ }
459
+ for (const u of diff.update) {
460
+ await mgmt.flags.update(u.id, buildWritePayloadFromDoc(u.doc));
461
+ }
462
+ for (const p of diff.prune) {
463
+ await mgmt.flags.delete(p.id);
464
+ }
465
+ outputSuccess({
466
+ created: diff.create.map((d) => d.key),
467
+ updated: diff.update.map((u) => u.doc.key),
468
+ pruned: diff.prune.map((p) => p.key),
469
+ unchanged: diff.unchanged,
470
+ });
471
+ }
472
+ catch (err) {
473
+ outputError(err);
474
+ }
475
+ });
476
+ }
477
+ export function buildFlagWritePayload(opts, { partial = false } = {}) {
478
+ const payload = {};
479
+ if (opts.key !== undefined)
480
+ payload.key = opts.key;
481
+ if (opts.description !== undefined)
482
+ payload.description = opts.description;
483
+ if (opts.state !== undefined) {
484
+ if (!FLAG_STATES.includes(opts.state)) {
485
+ throw new Error(`Invalid --state: ${opts.state}. Must be one of ${FLAG_STATES.join(', ')}.`);
486
+ }
487
+ payload.state = opts.state;
488
+ }
489
+ let valueType;
490
+ if (opts.valueType !== undefined) {
491
+ if (!FLAG_VALUE_TYPES.includes(opts.valueType)) {
492
+ throw new Error(`Invalid --value-type: ${opts.valueType}. Must be one of ${FLAG_VALUE_TYPES.join(', ')}.`);
493
+ }
494
+ valueType = opts.valueType;
495
+ payload.valueType = valueType;
496
+ }
497
+ if (opts.onValue !== undefined) {
498
+ payload.onValue = coerceValue(opts.onValue, valueType ?? 'boolean');
499
+ }
500
+ if (opts.offValue !== undefined) {
501
+ payload.offValue = coerceValue(opts.offValue, valueType ?? 'boolean');
502
+ }
503
+ if (opts.rule !== undefined) {
504
+ const parsed = parseRuleArg(opts.rule);
505
+ payload.rule = parsed;
506
+ }
507
+ // Legacy fields. In partial (update) mode, only include if explicitly set —
508
+ // commander returns `false` as default for boolean flags, which would
509
+ // otherwise clobber server state on every update.
510
+ if (!partial || opts.enabled !== undefined) {
511
+ if (opts.enabled !== undefined)
512
+ payload.enabled = opts.enabled;
513
+ }
514
+ if (!partial || opts.defaultValue !== undefined) {
515
+ if (opts.defaultValue !== undefined)
516
+ payload.defaultValue = opts.defaultValue;
517
+ }
518
+ return payload;
519
+ }
520
+ export function coerceValue(raw, type) {
521
+ switch (type) {
522
+ case 'boolean':
523
+ if (raw === 'true')
524
+ return true;
525
+ if (raw === 'false')
526
+ return false;
527
+ throw new Error(`Invalid boolean value "${raw}" — expected "true" or "false".`);
528
+ case 'number': {
529
+ const n = Number(raw);
530
+ if (!Number.isFinite(n))
531
+ throw new Error(`Invalid number value "${raw}".`);
532
+ return n;
533
+ }
534
+ case 'string':
535
+ return raw;
536
+ case 'json':
537
+ try {
538
+ return JSON.parse(raw);
539
+ }
540
+ catch (err) {
541
+ throw new Error(`Invalid JSON value: ${err instanceof Error ? err.message : String(err)}`);
542
+ }
543
+ }
544
+ }
545
+ export function parseRuleArg(raw) {
546
+ let parsed;
547
+ try {
548
+ parsed = JSON.parse(raw);
549
+ }
550
+ catch (err) {
551
+ throw new Error(`--rule must be valid JSON. Parse error: ${err instanceof Error ? err.message : String(err)}`);
552
+ }
553
+ const errors = validateRule(parsed);
554
+ if (errors.length > 0) {
555
+ throw new Error(`--rule failed validation:\n${errors.map((e) => ` - ${e.message}`).join('\n')}`);
556
+ }
557
+ return parsed;
558
+ }
559
+ export function parseAttributes(pairs) {
560
+ const out = {};
561
+ for (const pair of pairs) {
562
+ const idx = pair.indexOf('=');
563
+ if (idx === -1)
564
+ throw new Error(`Invalid --attribute "${pair}" — expected key=value.`);
565
+ const key = pair.slice(0, idx);
566
+ const raw = pair.slice(idx + 1);
567
+ // Try to parse as JSON first (covers numbers, booleans, structured); fall
568
+ // back to the raw string.
569
+ try {
570
+ out[key] = JSON.parse(raw);
571
+ }
572
+ catch {
573
+ out[key] = raw;
574
+ }
575
+ }
576
+ return out;
577
+ }
578
+ async function resolveFlagId(key) {
579
+ const all = await getManagementClient().flags.list();
580
+ const found = all.find((f) => f.key === key);
581
+ if (!found)
582
+ throw new Error(`Flag not found: ${key}`);
583
+ return found.id;
584
+ }
585
+ const RULE_CONDITIONS_HARD_MAX = 50;
586
+ export function validateRule(rule) {
587
+ const errors = [];
588
+ if (!rule || typeof rule !== 'object') {
589
+ return [{ branchIndex: -1, conditionIndex: -1, message: 'Rule must be an object.' }];
590
+ }
591
+ const r = rule;
592
+ const branches = r.branches ?? [];
593
+ if (!Array.isArray(branches)) {
594
+ return [{ branchIndex: -1, conditionIndex: -1, message: '`branches` must be an array.' }];
595
+ }
596
+ let totalConditions = 0;
597
+ branches.forEach((branch, bi) => {
598
+ if (!branch || typeof branch !== 'object') {
599
+ errors.push({
600
+ branchIndex: bi,
601
+ conditionIndex: -1,
602
+ message: `Branch ${bi} must be an object.`,
603
+ });
604
+ return;
605
+ }
606
+ if (!('returnValue' in branch)) {
607
+ errors.push({
608
+ branchIndex: bi,
609
+ conditionIndex: -1,
610
+ message: `Branch ${bi} is missing returnValue.`,
611
+ });
612
+ }
613
+ const conds = branch.conditions ?? [];
614
+ if (!Array.isArray(conds) || conds.length === 0) {
615
+ errors.push({
616
+ branchIndex: bi,
617
+ conditionIndex: -1,
618
+ message: `Branch ${bi} has no conditions.`,
619
+ });
620
+ return;
621
+ }
622
+ conds.forEach((c, ci) => {
623
+ totalConditions++;
624
+ if (!c || typeof c !== 'object') {
625
+ errors.push({
626
+ branchIndex: bi,
627
+ conditionIndex: ci,
628
+ message: `Branch ${bi} condition ${ci} must be an object.`,
629
+ });
630
+ return;
631
+ }
632
+ if (typeof c.attribute !== 'string' || c.attribute.length === 0) {
633
+ errors.push({
634
+ branchIndex: bi,
635
+ conditionIndex: ci,
636
+ message: `Branch ${bi} condition ${ci} missing 'attribute'.`,
637
+ });
638
+ }
639
+ if (typeof c.operator !== 'string' || c.operator.length === 0) {
640
+ errors.push({
641
+ branchIndex: bi,
642
+ conditionIndex: ci,
643
+ message: `Branch ${bi} condition ${ci} missing 'operator'.`,
644
+ });
645
+ }
646
+ });
647
+ });
648
+ if (totalConditions > RULE_CONDITIONS_HARD_MAX) {
649
+ errors.push({
650
+ branchIndex: -1,
651
+ conditionIndex: -1,
652
+ message: `Rule has ${totalConditions} conditions; max is ${RULE_CONDITIONS_HARD_MAX}.`,
653
+ });
654
+ }
655
+ if (r.rolloutPct !== undefined) {
656
+ if (typeof r.rolloutPct !== 'number' || r.rolloutPct < 0 || r.rolloutPct > 100) {
657
+ errors.push({
658
+ branchIndex: -1,
659
+ conditionIndex: -1,
660
+ message: `rolloutPct must be a number in [0, 100], got ${r.rolloutPct}.`,
661
+ });
662
+ }
663
+ }
664
+ if (!('otherwiseValue' in r)) {
665
+ errors.push({
666
+ branchIndex: -1,
667
+ conditionIndex: -1,
668
+ message: 'Rule missing otherwiseValue.',
669
+ });
670
+ }
671
+ if (r.groupRef && branches.length > 0) {
672
+ errors.push({
673
+ branchIndex: -1,
674
+ conditionIndex: -1,
675
+ message: 'Rule cannot have both inline branches and a groupRef.',
676
+ });
677
+ }
678
+ return errors;
679
+ }
680
+ export function evaluateLocally(cached, ctx) {
681
+ switch (cached.state) {
682
+ case 'off':
683
+ return {
684
+ value: cached.offValue,
685
+ variantIndex: -1,
686
+ matched: false,
687
+ excludedByRollout: false,
688
+ };
689
+ case 'on':
690
+ return {
691
+ value: cached.onValue,
692
+ variantIndex: 0,
693
+ matched: true,
694
+ excludedByRollout: false,
695
+ };
696
+ case 'on-with-rule':
697
+ if (!cached.rule) {
698
+ return {
699
+ value: cached.onValue,
700
+ variantIndex: -1,
701
+ matched: false,
702
+ excludedByRollout: false,
703
+ };
704
+ }
705
+ return evaluateRule(cached.rule, cached.key, ctx);
706
+ }
707
+ }
708
+ function evaluateRule(rule, flagKey, ctx) {
709
+ const rolloutPct = clampPct(rule.rolloutPct ?? 100);
710
+ if (rolloutPct < 100) {
711
+ if (!ctx.identity) {
712
+ return {
713
+ value: rule.otherwiseValue,
714
+ variantIndex: -1,
715
+ matched: false,
716
+ excludedByRollout: true,
717
+ };
718
+ }
719
+ if (bucket(flagKey, ctx.identity) >= rolloutPct) {
720
+ return {
721
+ value: rule.otherwiseValue,
722
+ variantIndex: -1,
723
+ matched: false,
724
+ excludedByRollout: true,
725
+ };
726
+ }
727
+ }
728
+ const branches = rule.branches ?? [];
729
+ for (let i = 0; i < branches.length; i++) {
730
+ if (evaluateBranch(branches[i], ctx)) {
731
+ return {
732
+ value: branches[i].returnValue,
733
+ variantIndex: i,
734
+ matched: true,
735
+ excludedByRollout: false,
736
+ };
737
+ }
738
+ }
739
+ return {
740
+ value: rule.otherwiseValue,
741
+ variantIndex: -1,
742
+ matched: false,
743
+ excludedByRollout: false,
744
+ };
745
+ }
746
+ function evaluateBranch(branch, ctx) {
747
+ if (!branch.conditions || branch.conditions.length === 0)
748
+ return false;
749
+ return branch.conditions.every((c) => evaluateCondition(c, resolveAttribute(ctx, c.attribute)));
750
+ }
751
+ function resolveAttribute(ctx, attribute) {
752
+ if (Object.prototype.hasOwnProperty.call(ctx.attributes, attribute)) {
753
+ return ctx.attributes[attribute];
754
+ }
755
+ const parts = attribute.split('.');
756
+ let current = ctx.attributes;
757
+ for (const part of parts) {
758
+ if (current === undefined || current === null)
759
+ return undefined;
760
+ if (typeof current !== 'object')
761
+ return undefined;
762
+ current = current[part];
763
+ }
764
+ return current;
765
+ }
766
+ function evaluateCondition(c, actual) {
767
+ const expected = c.value;
768
+ switch (c.operator) {
769
+ case 'equals':
770
+ case 'eq':
771
+ return actual === expected;
772
+ case 'not_equals':
773
+ case 'neq':
774
+ return actual !== expected;
775
+ case 'in':
776
+ return Array.isArray(expected) && expected.includes(actual);
777
+ case 'not_in':
778
+ return Array.isArray(expected) && !expected.includes(actual);
779
+ case 'contains':
780
+ return typeof actual === 'string' && typeof expected === 'string' && actual.includes(expected);
781
+ case 'starts_with':
782
+ return (typeof actual === 'string' && typeof expected === 'string' && actual.startsWith(expected));
783
+ case 'ends_with':
784
+ return (typeof actual === 'string' && typeof expected === 'string' && actual.endsWith(expected));
785
+ case 'gt':
786
+ return typeof actual === 'number' && typeof expected === 'number' && actual > expected;
787
+ case 'gte':
788
+ return typeof actual === 'number' && typeof expected === 'number' && actual >= expected;
789
+ case 'lt':
790
+ return typeof actual === 'number' && typeof expected === 'number' && actual < expected;
791
+ case 'lte':
792
+ return typeof actual === 'number' && typeof expected === 'number' && actual <= expected;
793
+ case 'exists':
794
+ return actual !== undefined && actual !== null;
795
+ case 'not_exists':
796
+ return actual === undefined || actual === null;
797
+ default:
798
+ // Unknown operator → no match (defensive; validateRule should catch).
799
+ return false;
800
+ }
801
+ }
802
+ function bucket(flagKey, identity) {
803
+ const input = `${flagKey}|${identity}`;
804
+ let hash = 0x811c9dc5;
805
+ for (let i = 0; i < input.length; i++) {
806
+ hash ^= input.charCodeAt(i);
807
+ hash = (hash + (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)) >>> 0;
808
+ }
809
+ return hash % 100;
810
+ }
811
+ function clampPct(p) {
812
+ if (!Number.isFinite(p))
813
+ return 100;
814
+ if (p < 0)
815
+ return 0;
816
+ if (p > 100)
817
+ return 100;
818
+ return p;
819
+ }
820
+ // ── Helpers ─────────────────────────────────────────────────────────────────
821
+ /**
822
+ * Derive a 2.0 state from a legacy 1.0 flag shape. Used so `flag list` /
823
+ * `flag get` always show a state column, even against an older bridge-api
824
+ * that has not yet rolled out 2.0 fields.
825
+ */
826
+ function deriveState(f) {
827
+ if (typeof f.state === 'string' && FLAG_STATES.includes(f.state)) {
828
+ return f.state;
829
+ }
830
+ if (f.enabled === false)
831
+ return 'off';
832
+ const segs = f.segments ?? [];
833
+ return segs.length > 0 ? 'on-with-rule' : 'on';
834
+ }
76
835
  //# sourceMappingURL=flag.command.js.map