@shipeasy/sdk 6.0.0 → 6.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -67,6 +67,53 @@ function send(url) {
67
67
  }
68
68
  }
69
69
 
70
+ // src/logger.ts
71
+ var LOG_LEVELS = ["silent", "error", "warn", "info", "debug"];
72
+ var RANK = {
73
+ silent: 0,
74
+ error: 1,
75
+ warn: 2,
76
+ info: 3,
77
+ debug: 4
78
+ };
79
+ var currentLevel = "warn";
80
+ function setLogLevel(level) {
81
+ if (typeof level === "string" && Object.prototype.hasOwnProperty.call(RANK, level)) {
82
+ currentLevel = level;
83
+ }
84
+ }
85
+ function write(method, args) {
86
+ try {
87
+ const c = globalThis.console;
88
+ if (!c) return;
89
+ const fn = c[method] ?? c.log;
90
+ if (typeof fn === "function") fn.call(c, ...args);
91
+ } catch {
92
+ }
93
+ }
94
+ var logger = {
95
+ error(...args) {
96
+ if (RANK[currentLevel] >= RANK.error) write("error", args);
97
+ },
98
+ warn(...args) {
99
+ if (RANK[currentLevel] >= RANK.warn) write("warn", args);
100
+ },
101
+ info(...args) {
102
+ if (RANK[currentLevel] >= RANK.info) write("info", args);
103
+ },
104
+ debug(...args) {
105
+ if (RANK[currentLevel] >= RANK.debug) write("debug", args);
106
+ }
107
+ };
108
+ function safeRun(label, fallback, fn) {
109
+ try {
110
+ return fn();
111
+ } catch (err) {
112
+ logger.error(`[shipeasy] ${label} failed \u2014 returning safe default:`, String(err));
113
+ return fallback;
114
+ }
115
+ }
116
+
70
117
  // src/see/core.ts
71
118
  var SEE_MAX_MESSAGE = 500;
72
119
  var SEE_MAX_STACK = 8e3;
@@ -528,6 +575,7 @@ var Engine = class _Engine {
528
575
  // 304). Never fired in testMode/offline (no polling happens there).
529
576
  changeListeners = /* @__PURE__ */ new Set();
530
577
  constructor(opts) {
578
+ setLogLevel(opts.logLevel);
531
579
  this.apiKey = opts.apiKey;
532
580
  this.baseUrl = (opts.baseUrl ?? "https://cdn.shipeasy.ai").replace(/\/$/, "");
533
581
  this.env = opts.env ?? "prod";
@@ -651,14 +699,14 @@ var Engine = class _Engine {
651
699
  try {
652
700
  l();
653
701
  } catch (err) {
654
- console.warn("[shipeasy] onChange listener threw:", String(err));
702
+ logger.warn("[shipeasy] onChange listener threw:", String(err));
655
703
  }
656
704
  }
657
705
  }
658
706
  startPoll() {
659
707
  this.timer = setInterval(() => {
660
708
  this.fetchAll(true).catch(
661
- (err) => console.warn("[shipeasy] background poll failed:", String(err))
709
+ (err) => logger.warn("[shipeasy] background poll failed:", String(err))
662
710
  );
663
711
  }, this.pollInterval * 1e3);
664
712
  }
@@ -737,7 +785,12 @@ var Engine = class _Engine {
737
785
  return "defaultValue" in opts ? opts.defaultValue : void 0;
738
786
  }
739
787
  if (!opts.decode) return raw;
740
- return opts.decode(raw);
788
+ try {
789
+ return opts.decode(raw);
790
+ } catch (err) {
791
+ logger.warn(`[shipeasy] getConfig('${name}') decode failed:`, String(err));
792
+ return "defaultValue" in opts ? opts.defaultValue : void 0;
793
+ }
741
794
  }
742
795
  getExperiment(name, user, defaultParams, decode) {
743
796
  this.telemetry.emit("experiment", name);
@@ -752,7 +805,7 @@ var Engine = class _Engine {
752
805
  try {
753
806
  return { inExperiment: true, group: ov.group, params: decode(ov.params) };
754
807
  } catch (err) {
755
- console.warn(`[shipeasy] getExperiment('${name}') override decode failed:`, String(err));
808
+ logger.warn(`[shipeasy] getExperiment('${name}') override decode failed:`, String(err));
756
809
  return notIn;
757
810
  }
758
811
  }
@@ -777,7 +830,7 @@ var Engine = class _Engine {
777
830
  try {
778
831
  return { inExperiment: true, group: g.name, params: decode(g.params) };
779
832
  } catch (err) {
780
- console.warn(`[shipeasy] getExperiment('${name}') decode failed:`, String(err));
833
+ logger.warn(`[shipeasy] getExperiment('${name}') decode failed:`, String(err));
781
834
  return notIn;
782
835
  }
783
836
  };
@@ -829,7 +882,7 @@ var Engine = class _Engine {
829
882
  method: "POST",
830
883
  headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
831
884
  body
832
- }).catch((err) => console.warn("[shipeasy] track failed:", String(err)));
885
+ }).catch((err) => logger.warn("[shipeasy] track failed:", String(err)));
833
886
  }
834
887
  /**
835
888
  * Emit an exposure event for an experiment at the server-side decision point
@@ -860,7 +913,7 @@ var Engine = class _Engine {
860
913
  method: "POST",
861
914
  headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
862
915
  body
863
- }).catch((err) => console.warn("[shipeasy] logExposure failed:", String(err)));
916
+ }).catch((err) => logger.warn("[shipeasy] logExposure failed:", String(err)));
864
917
  }
865
918
  /**
866
919
  * Report a structured error into the errors primitive. Fire-and-forget —
@@ -880,7 +933,7 @@ var Engine = class _Engine {
880
933
  method: "POST",
881
934
  headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
882
935
  body: JSON.stringify({ events: [ev] })
883
- }).catch((err) => console.warn("[shipeasy] see() send failed:", String(err)));
936
+ }).catch((err) => logger.warn("[shipeasy] see() send failed:", String(err)));
884
937
  } catch {
885
938
  }
886
939
  }
@@ -934,7 +987,11 @@ var Engine = class _Engine {
934
987
  const ks = this.flagsBlob?.killswitches?.[name];
935
988
  if (!ks) return false;
936
989
  if (switchKey === void 0) return isEnabled(ks.killed);
937
- return isEnabled(ks.switches?.[switchKey]);
990
+ const switches = ks.switches ?? {};
991
+ if (Object.prototype.hasOwnProperty.call(switches, switchKey)) {
992
+ return isEnabled(switches[switchKey]);
993
+ }
994
+ return isEnabled(ks.killed);
938
995
  }
939
996
  };
940
997
  var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
@@ -1063,7 +1120,7 @@ function _resetShipeasyServerForTests() {
1063
1120
  async function shipeasy(opts) {
1064
1121
  const serverKey = opts.serverKey ?? "";
1065
1122
  if (!serverKey) {
1066
- console.error(
1123
+ logger.error(
1067
1124
  "[shipeasy] No server key \u2014 flags, experiments and SSR i18n skipped. Pass `serverKey` to shipeasy() from @shipeasy/sdk/server with your server key (SHIPEASY_SERVER_KEY). Set it as a Worker secret with `wrangler secret put SHIPEASY_SERVER_KEY` (or add it to .env for local dev). Do not pass a client key here \u2014 the server entrypoint only accepts the server key."
1068
1125
  );
1069
1126
  }
@@ -1071,7 +1128,8 @@ async function shipeasy(opts) {
1071
1128
  flags.configure({
1072
1129
  apiKey: serverKey,
1073
1130
  disableTelemetry: opts.disableTelemetry,
1074
- privateAttributes: opts.privateAttributes
1131
+ privateAttributes: opts.privateAttributes,
1132
+ logLevel: opts.logLevel
1075
1133
  });
1076
1134
  let resolvedUrlOverrides = opts.urlOverrides;
1077
1135
  if (!resolvedUrlOverrides) {
@@ -1197,21 +1255,30 @@ var flags = {
1197
1255
  _server?.destroy();
1198
1256
  },
1199
1257
  get(name, user, defaultValue = false) {
1200
- return _server?.getFlag(name, user, defaultValue) ?? defaultValue;
1258
+ return safeRun("flags.get", defaultValue, () => _server?.getFlag(name, user, defaultValue) ?? defaultValue);
1201
1259
  },
1202
1260
  /** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
1203
1261
  getDetail(name, user) {
1204
- return _server?.getFlagDetail(name, user) ?? { value: false, reason: "CLIENT_NOT_READY" };
1262
+ return safeRun(
1263
+ "flags.getDetail",
1264
+ { value: false, reason: "CLIENT_NOT_READY" },
1265
+ () => _server?.getFlagDetail(name, user) ?? { value: false, reason: "CLIENT_NOT_READY" }
1266
+ );
1205
1267
  },
1206
1268
  getConfig(name, decodeOrOpts) {
1207
- return _server?.getConfig(name, decodeOrOpts);
1269
+ return safeRun(
1270
+ "flags.getConfig",
1271
+ void 0,
1272
+ () => _server?.getConfig(name, decodeOrOpts)
1273
+ );
1208
1274
  },
1209
1275
  getExperiment(name, user, defaultParams, decode) {
1210
- return _server?.getExperiment(name, user, defaultParams, decode) ?? {
1211
- inExperiment: false,
1212
- group: "control",
1213
- params: defaultParams
1214
- };
1276
+ const notIn = { inExperiment: false, group: "control", params: defaultParams };
1277
+ return safeRun(
1278
+ "flags.getExperiment",
1279
+ notIn,
1280
+ () => _server?.getExperiment(name, user, defaultParams, decode) ?? notIn
1281
+ );
1215
1282
  },
1216
1283
  /**
1217
1284
  * Read a killswitch. Without `switchKey`, returns true when the whole
@@ -1219,15 +1286,15 @@ var flags = {
1219
1286
  * switch is on. Unknown killswitches / switches return false.
1220
1287
  */
1221
1288
  ks(name, switchKey) {
1222
- return _server?.getKillswitch(name, switchKey) ?? false;
1289
+ return safeRun("flags.ks", false, () => _server?.getKillswitch(name, switchKey) ?? false);
1223
1290
  },
1224
1291
  track(userId, eventName, props) {
1225
- _server?.track(userId, eventName, props);
1292
+ safeRun("flags.track", void 0, () => _server?.track(userId, eventName, props));
1226
1293
  },
1227
1294
  /** Emit an exposure for an enrolled experiment at the decision point. See
1228
1295
  * {@link Engine.logExposure}. No-op before configure(). */
1229
1296
  logExposure(user, name) {
1230
- _server?.logExposure(user, name);
1297
+ safeRun("flags.logExposure", void 0, () => _server?.logExposure(user, name));
1231
1298
  },
1232
1299
  /**
1233
1300
  * Evaluate all flags / configs / experiments for a user against the locally
@@ -1235,27 +1302,81 @@ var flags = {
1235
1302
  * overrides. Returns an empty payload when the blob hasn't been fetched yet.
1236
1303
  */
1237
1304
  evaluate(user, rawUrl) {
1238
- return _server?.evaluate(user, rawUrl) ?? {
1239
- flags: {},
1240
- configs: {},
1241
- experiments: {},
1242
- killswitches: {}
1243
- };
1305
+ const empty = { flags: {}, configs: {}, experiments: {}, killswitches: {} };
1306
+ return safeRun("flags.evaluate", empty, () => _server?.evaluate(user, rawUrl) ?? empty);
1244
1307
  }
1245
1308
  };
1246
1309
  var _identityAttributes = (user) => user && typeof user === "object" ? user : {};
1247
1310
  var _attributes = _identityAttributes;
1248
1311
  function configure(opts) {
1249
- const { attributes, ...engineOpts } = opts;
1312
+ const { attributes, poll = false, init = true, ...engineOpts } = opts;
1250
1313
  _attributes = attributes ?? _identityAttributes;
1251
1314
  const engine = configureShipeasyServer(engineOpts);
1252
- void engine.initOnce().catch(() => {
1253
- });
1315
+ if (poll) {
1316
+ void engine.init().catch(() => {
1317
+ });
1318
+ } else if (init) {
1319
+ void engine.initOnce().catch(() => {
1320
+ });
1321
+ }
1254
1322
  return engine;
1255
1323
  }
1256
1324
  function _resetConfigureForTests() {
1257
1325
  _attributes = _identityAttributes;
1258
1326
  }
1327
+ function _installGlobalEngine(engine, attributes) {
1328
+ _server?.destroy();
1329
+ _server = engine;
1330
+ _attributes = attributes ?? _identityAttributes;
1331
+ return engine;
1332
+ }
1333
+ function _applyOverrides(engine, flags2, configs, experiments) {
1334
+ for (const [name, value] of Object.entries(flags2 ?? {})) engine.overrideFlag(name, value);
1335
+ for (const [name, value] of Object.entries(configs ?? {})) engine.overrideConfig(name, value);
1336
+ for (const [name, [group, params]] of Object.entries(experiments ?? {}))
1337
+ engine.overrideExperiment(name, group, params);
1338
+ }
1339
+ function configureForTesting(opts = {}) {
1340
+ const engine = Engine.forTesting();
1341
+ _applyOverrides(engine, opts.flags, opts.configs, opts.experiments);
1342
+ return _installGlobalEngine(engine, opts.attributes);
1343
+ }
1344
+ function configureForOffline(opts) {
1345
+ let engine;
1346
+ if (opts.path !== void 0) {
1347
+ engine = Engine.fromFile(opts.path);
1348
+ } else if (opts.snapshot !== void 0) {
1349
+ engine = Engine.fromSnapshot(opts.snapshot);
1350
+ } else {
1351
+ throw new Error("[shipeasy] configureForOffline requires either { snapshot } or { path }");
1352
+ }
1353
+ _applyOverrides(engine, opts.flags, opts.configs, opts.experiments);
1354
+ return _installGlobalEngine(engine, opts.attributes);
1355
+ }
1356
+ function _requireGlobal(fn) {
1357
+ const engine = getShipeasyServerClient();
1358
+ if (!engine) {
1359
+ throw new Error(
1360
+ `[shipeasy] ${fn}(...) called before configure({ apiKey }) (or a configureFor* sibling).`
1361
+ );
1362
+ }
1363
+ return engine;
1364
+ }
1365
+ function overrideFlag(name, value) {
1366
+ _requireGlobal("overrideFlag").overrideFlag(name, value);
1367
+ }
1368
+ function overrideConfig(name, value) {
1369
+ _requireGlobal("overrideConfig").overrideConfig(name, value);
1370
+ }
1371
+ function overrideExperiment(name, group, params) {
1372
+ _requireGlobal("overrideExperiment").overrideExperiment(name, group, params);
1373
+ }
1374
+ function clearOverrides() {
1375
+ _requireGlobal("clearOverrides").clearOverrides();
1376
+ }
1377
+ function onChange(listener) {
1378
+ return _requireGlobal("onChange").onChange(listener);
1379
+ }
1259
1380
  var Client = class {
1260
1381
  engine;
1261
1382
  /** The resolved attribute bag this handle evaluates against. */
@@ -1271,25 +1392,65 @@ var Client = class {
1271
1392
  this.attributes = _attributes(user);
1272
1393
  }
1273
1394
  getFlag(name, defaultValue = false) {
1274
- return this.engine.getFlag(name, this.attributes, defaultValue);
1395
+ return safeRun(
1396
+ "Client.getFlag",
1397
+ defaultValue,
1398
+ () => this.engine.getFlag(name, this.attributes, defaultValue)
1399
+ );
1275
1400
  }
1276
1401
  getFlagDetail(name) {
1277
- return this.engine.getFlagDetail(name, this.attributes);
1402
+ return safeRun(
1403
+ "Client.getFlagDetail",
1404
+ { value: false, reason: "CLIENT_NOT_READY" },
1405
+ () => this.engine.getFlagDetail(name, this.attributes)
1406
+ );
1278
1407
  }
1279
1408
  getConfig(name, decodeOrOpts) {
1280
- return this.engine.getConfig(name, decodeOrOpts);
1409
+ return safeRun(
1410
+ "Client.getConfig",
1411
+ void 0,
1412
+ () => this.engine.getConfig(name, decodeOrOpts)
1413
+ );
1281
1414
  }
1282
1415
  getExperiment(name, defaultParams, decode) {
1283
- return this.engine.getExperiment(name, this.attributes, defaultParams, decode);
1416
+ const notIn = { inExperiment: false, group: "control", params: defaultParams };
1417
+ return safeRun(
1418
+ "Client.getExperiment",
1419
+ notIn,
1420
+ () => this.engine.getExperiment(name, this.attributes, defaultParams, decode)
1421
+ );
1284
1422
  }
1285
1423
  /** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
1286
1424
  getKillswitch(name, switchKey) {
1287
- return this.engine.getKillswitch(name, switchKey);
1425
+ return safeRun("Client.getKillswitch", false, () => this.engine.getKillswitch(name, switchKey));
1426
+ }
1427
+ /**
1428
+ * Record a conversion/metric event for the bound user. Derives the unit from
1429
+ * the resolved attribute bag (`user_id`, else `anonymous_id`) and delegates to
1430
+ * {@link Engine.track} — so an experiment is end-to-end Client-only (no need to
1431
+ * drop down to the Engine to log a conversion). Fire-and-forget; no-op in test
1432
+ * mode.
1433
+ */
1434
+ track(eventName, props) {
1435
+ safeRun("Client.track", void 0, () => {
1436
+ const id = this.attributes.user_id ?? this.attributes.anonymous_id;
1437
+ if (id === void 0) return;
1438
+ this.engine.track(String(id), eventName, props);
1439
+ });
1440
+ }
1441
+ /**
1442
+ * Emit an exposure event for `name` at this server-side decision point for the
1443
+ * bound user. Delegates to {@link Engine.logExposure} with the resolved
1444
+ * attribute bag (re-evaluates enrolment; no-op when the user isn't enrolled or
1445
+ * in test mode).
1446
+ */
1447
+ logExposure(name) {
1448
+ safeRun("Client.logExposure", void 0, () => this.engine.logExposure(this.attributes, name));
1288
1449
  }
1289
1450
  };
1290
1451
  function dispatchSee(problem, consequence, extras, kind) {
1291
1452
  if (!_server) {
1292
- console.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
1453
+ logger.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
1293
1454
  return;
1294
1455
  }
1295
1456
  _server.reportError(problem, consequence, extras, kind);
@@ -1306,10 +1467,14 @@ export {
1306
1467
  Client,
1307
1468
  Engine,
1308
1469
  FLAG_REASONS,
1470
+ LOG_LEVELS,
1309
1471
  _murmur3ForTests,
1310
1472
  _resetConfigureForTests,
1311
1473
  _resetShipeasyServerForTests,
1474
+ clearOverrides,
1312
1475
  configure,
1476
+ configureForOffline,
1477
+ configureForTesting,
1313
1478
  configureShipeasyServer,
1314
1479
  createInMemoryStickyStore,
1315
1480
  fetchLabelsForSSR,
@@ -1319,6 +1484,10 @@ export {
1319
1484
  getShipeasyServerClient,
1320
1485
  i18n,
1321
1486
  isExpected,
1487
+ onChange,
1488
+ overrideConfig,
1489
+ overrideExperiment,
1490
+ overrideFlag,
1322
1491
  see,
1323
1492
  seeContext,
1324
1493
  shipeasy,
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/skill-cli.ts
5
+ var import_node_fs = require("fs");
6
+ var import_node_path = require("path");
7
+ var DEFAULT_DEST = ".claude/skills/shipeasy-typescript/SKILL.md";
8
+ function skillText() {
9
+ const candidates = [
10
+ (0, import_node_path.join)(__dirname, "..", "docs", "skill", "SKILL.md"),
11
+ (0, import_node_path.join)(__dirname, "..", "..", "docs", "skill", "SKILL.md")
12
+ ];
13
+ for (const p of candidates) {
14
+ try {
15
+ if ((0, import_node_fs.existsSync)(p)) return (0, import_node_fs.readFileSync)(p, "utf8");
16
+ } catch {
17
+ }
18
+ }
19
+ throw new Error("shipeasy-skill: bundled SKILL.md not found in the package.");
20
+ }
21
+ function install(dir, force) {
22
+ let dest = (0, import_node_path.resolve)(dir);
23
+ const looksLikeDir = (0, import_node_fs.existsSync)(dest) && (0, import_node_fs.statSync)(dest).isDirectory() || !/\.[^/]+$/.test(dest);
24
+ if (looksLikeDir) dest = (0, import_node_path.join)(dest, "SKILL.md");
25
+ if ((0, import_node_fs.existsSync)(dest) && !force) {
26
+ process.stderr.write(`shipeasy-skill: refusing to overwrite ${dest} \u2014 pass --force
27
+ `);
28
+ return 1;
29
+ }
30
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(dest), { recursive: true });
31
+ (0, import_node_fs.writeFileSync)(dest, skillText(), "utf8");
32
+ process.stdout.write(`shipeasy-skill: installed the Shipeasy agent skill \u2192 ${dest}
33
+ `);
34
+ return 0;
35
+ }
36
+ function main(argv) {
37
+ const [cmd, ...rest] = argv;
38
+ if (cmd === "print") {
39
+ process.stdout.write(skillText());
40
+ return 0;
41
+ }
42
+ if (cmd === "install") {
43
+ let dir = DEFAULT_DEST;
44
+ let force = false;
45
+ for (let i = 0; i < rest.length; i++) {
46
+ if (rest[i] === "--force") force = true;
47
+ else if (rest[i] === "--dir") dir = rest[++i] ?? DEFAULT_DEST;
48
+ }
49
+ return install(dir, force);
50
+ }
51
+ process.stdout.write(
52
+ "shipeasy-skill \u2014 install the Shipeasy agent skill.\n\n shipeasy-skill install [--dir <path>] [--force]\n shipeasy-skill print\n"
53
+ );
54
+ return cmd && cmd !== "--help" && cmd !== "-h" ? 1 : 0;
55
+ }
56
+ process.exit(main(process.argv.slice(2)));
@@ -0,0 +1,145 @@
1
+ ---
2
+ name: shipeasy-typescript
3
+ description: Use Shipeasy (feature flags, configs, kill switches, A/B experiments, i18n) from TypeScript / JavaScript. Covers configure() + Client(user), getFlag/getConfig/getKillswitch/getExperiment, track, testing, OpenFeature, and the see() error reporter — server (@shipeasy/sdk/server) and browser (@shipeasy/sdk/client).
4
+ ---
5
+
6
+ # Shipeasy TypeScript SDK
7
+
8
+ `@shipeasy/sdk` — one package, two entrypoints: `@shipeasy/sdk/server` (Node /
9
+ Cloudflare Worker / Deno, **server** key) and `@shipeasy/sdk/client` (browser,
10
+ public **client** key). Everything works from vanilla JS.
11
+
12
+ > The documented surface is exactly **`configure()`** (setup) and the bound
13
+ > **`new Client(user)`** (use), plus the package-level helpers below. For deeper
14
+ > docs, fetch any page/snippet from the manifest at
15
+ > <https://shipeasy-ai.github.io/sdk-ts/manifest.json> (raw page/snippet URLs below).
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install @shipeasy/sdk
21
+ ```
22
+
23
+ ## Configure once, evaluate per user
24
+
25
+ ```ts
26
+ import { configure, Client } from "@shipeasy/sdk/server"; // or "@shipeasy/sdk/client"
27
+
28
+ configure({
29
+ apiKey: process.env.SHIPEASY_SERVER_KEY!, // browser: clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY
30
+ attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan }), // optional; omit = identity
31
+ // poll: true // long-running server: keep flags fresh with a background poll (no engine.init() needed)
32
+ });
33
+
34
+ const flags = new Client(currentUser); // browser: await flags.ready() before first read
35
+
36
+ flags.getFlag("new_checkout"); // boolean (2nd arg = default if not-ready/not-found)
37
+ flags.getConfig<{ max: number }>("limits", { defaultValue: { max: 50 } });
38
+ flags.getKillswitch("payments"); // global on/off (not user-bound)
39
+ flags.getFlagDetail("new_checkout"); // { value, reason }
40
+ ```
41
+
42
+ `configure()` is first-config-wins and owns the fetch lifecycle (one-shot by
43
+ default; `poll: true` for a background refresh). Construct `new Client(user)` once
44
+ per user/request — it binds the user, so no method takes a user argument.
45
+ Full reference: <https://shipeasy-ai.github.io/sdk-ts/pages/configuration.md> ·
46
+ <https://shipeasy-ai.github.io/sdk-ts/pages/flags.md> ·
47
+ snippets <https://shipeasy-ai.github.io/sdk-ts/snippets/release/flags.md> ·
48
+ <https://shipeasy-ai.github.io/sdk-ts/snippets/release/configs.md> ·
49
+ <https://shipeasy-ai.github.io/sdk-ts/snippets/release/killswitches.md>
50
+
51
+ ## Experiments + track (Client-only, end to end)
52
+
53
+ ```ts
54
+ const flags = new Client(currentUser); // construct once per callsite
55
+ const { inExperiment, group, params } = flags.getExperiment("hero_cta", {
56
+ primary_label: "Sign up", // default params (control / not-enrolled)
57
+ });
58
+ render(params.primary_label);
59
+
60
+ flags.logExposure("hero_cta"); // record the exposure where you present it
61
+ flags.track("purchase", { value: 42 }); // record a conversion for the bound user
62
+ ```
63
+
64
+ `ExperimentResult = { inExperiment: boolean; group: string; params: P }`. Full
65
+ reference: <https://shipeasy-ai.github.io/sdk-ts/pages/experiments.md> · track snippet
66
+ <https://shipeasy-ai.github.io/sdk-ts/snippets/metrics/track.md>
67
+
68
+ ## i18n
69
+
70
+ Full i18n ships in this SDK. Wire the loader via the SSR bootstrap (no separate
71
+ init), then render with `i18n.t`:
72
+
73
+ ```ts
74
+ import { i18n } from "@shipeasy/sdk/client";
75
+ i18n.t("checkout.cta", "Place order");
76
+ i18n.t("cart.count", "{count} items", { count: cart.length });
77
+ ```
78
+
79
+ Full reference: <https://shipeasy-ai.github.io/sdk-ts/pages/i18n.md> · snippets
80
+ <https://shipeasy-ai.github.io/sdk-ts/snippets/i18n/setup.md> ·
81
+ <https://shipeasy-ai.github.io/sdk-ts/snippets/i18n/render.md>
82
+
83
+ ## Error reporting (see)
84
+
85
+ ```ts
86
+ import { see } from "@shipeasy/sdk/server"; // or /client
87
+
88
+ try { await submitOrder(order); }
89
+ catch (e) { see(e).causes_the("checkout").to("use cached prices").extras({ order_id: order.id }); }
90
+
91
+ see.Violation("large query").causes_the("results").to("be trimmed").extras({ rows });
92
+ see.ControlFlowException(e).because("because it wasn't an encoded Foo"); // expected — reports nothing
93
+ ```
94
+
95
+ Fire-and-forget on the next microtask (no `.send()`). Don't catch what you can't
96
+ name a consequence for. You may `see()` then re-throw (links as `caused_by`). Full
97
+ reference: <https://shipeasy-ai.github.io/sdk-ts/pages/error-reporting.md> · snippet
98
+ <https://shipeasy-ai.github.io/sdk-ts/snippets/ops/see.md>
99
+
100
+ ## Testing — no network
101
+
102
+ ```ts
103
+ import { configureForTesting, configureForOffline, Client, overrideFlag, clearOverrides } from "@shipeasy/sdk/server";
104
+
105
+ // Seed values up front; reads go through the ordinary new Client(user). Replaces
106
+ // prior config, so each test can reconfigure freely.
107
+ configureForTesting({
108
+ flags: { new_checkout: true },
109
+ configs: { limits: { max: 50 } },
110
+ experiments: { hero_cta: ["treatment", { primary_label: "Buy now" }] },
111
+ });
112
+ const flags = new Client({ user_id: "u_1" });
113
+ flags.getFlag("new_checkout"); // true
114
+
115
+ overrideFlag("new_checkout", false); // flip on the spot
116
+ clearOverrides(); // drop every override (incl. the seed)
117
+
118
+ // Offline: evaluate the REAL rules from a snapshot or JSON file, no network.
119
+ configureForOffline({ path: "./shipeasy-snapshot.json" });
120
+ // or: configureForOffline({ snapshot: { flags, experiments }, flags: { new_checkout: true } });
121
+ ```
122
+
123
+ Full reference: <https://shipeasy-ai.github.io/sdk-ts/pages/testing.md>
124
+
125
+ ## OpenFeature
126
+
127
+ ```ts
128
+ import { OpenFeature } from "@openfeature/server-sdk";
129
+ import { ShipeasyProvider } from "@shipeasy/sdk/openfeature-server"; // or /openfeature-web
130
+
131
+ // Assumes configure({ apiKey }) ran at startup — the no-arg provider resolves it.
132
+ await OpenFeature.setProviderAndWait(new ShipeasyProvider());
133
+ await OpenFeature.getClient().getBooleanValue("new_checkout", false, { targetingKey: "u1" });
134
+ ```
135
+
136
+ Full reference: <https://shipeasy-ai.github.io/sdk-ts/pages/openfeature.md>
137
+
138
+ ## Advanced
139
+
140
+ `privateAttributes`, `bucketBy` (custom bucketing unit), `stickyBucketing`
141
+ (browser: on by default, `__se_sticky` cookie), manual exposure
142
+ (`getExperiment(..., { logExposure: false })` + `flags.logExposure(name)`),
143
+ `onChange(cb)` (requires `configure({ poll: true })`) / browser `subscribe()`.
144
+ Devtools overlay: `Shift+Alt+S` or `?se=1`. Full reference:
145
+ <https://shipeasy-ai.github.io/sdk-ts/pages/advanced.md>
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@shipeasy/sdk",
3
- "version": "6.0.0",
3
+ "version": "6.3.1",
4
4
  "description": "Shipeasy SDK — feature gates, runtime configs, experiments, and metrics for the Shipeasy hosted service.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://shipeasy.ai",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/shipeasy-ai/sdk.git"
9
+ "url": "https://github.com/shipeasy-ai/sdk-ts.git"
10
10
  },
11
11
  "bugs": {
12
- "url": "https://github.com/shipeasy-ai/sdk/issues"
12
+ "url": "https://github.com/shipeasy-ai/sdk-ts/issues"
13
13
  },
14
14
  "main": "./dist/server/index.js",
15
15
  "module": "./dist/server/index.mjs",
@@ -59,12 +59,17 @@
59
59
  },
60
60
  "./templates/*": "./templates/*.js"
61
61
  },
62
+ "bin": {
63
+ "shipeasy-skill": "./dist/skill-cli.js"
64
+ },
62
65
  "files": [
63
66
  "dist/server/",
64
67
  "dist/client/",
65
68
  "dist/next/",
66
69
  "dist/openfeature-server/",
67
70
  "dist/openfeature-web/",
71
+ "dist/skill-cli.js",
72
+ "docs/skill/SKILL.md",
68
73
  "templates/",
69
74
  "LICENSE",
70
75
  "README.md"
@@ -74,6 +79,7 @@
74
79
  "type-check": "tsc --noEmit",
75
80
  "test": "vitest run",
76
81
  "test:watch": "vitest",
82
+ "gen:readme": "node scripts/gen-readme.mjs",
77
83
  "publish-loader": "wrangler r2 object put shipeasy-sdk/loader-v$npm_package_version.js --file=dist/loader/loader.global.js --content-type 'application/javascript; charset=utf-8' --cache-control 'public, max-age=31536000, immutable' --remote && wrangler r2 object put shipeasy-sdk/loader.js --file=dist/loader/loader.global.js --content-type 'application/javascript; charset=utf-8' --cache-control 'public, max-age=300' --remote"
78
84
  },
79
85
  "dependencies": {
@@ -99,11 +105,12 @@
99
105
  "@openfeature/server-sdk": "^1.22.0",
100
106
  "@openfeature/web-sdk": "^1.9.0",
101
107
  "@types/murmurhash-js": "^1.0.6",
102
- "@types/node": "^20.0.0",
108
+ "@types/node": "^26.0.1",
103
109
  "next": "^16.2.3",
104
110
  "tsup": "^8.3.0",
105
- "typescript": "^5.7.4",
106
- "vitest": "^2.1.0",
111
+ "typescript": "^6.0.3",
112
+ "vite": "^6.4.3",
113
+ "vitest": "^4.1.9",
107
114
  "wrangler": "^4.83.0"
108
115
  },
109
116
  "publishConfig": {