@ollie-shop/cli 1.2.2 → 1.3.3

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/.env.example CHANGED
@@ -1,6 +1,10 @@
1
- # Required for all authenticated commands (store, version, component, whoami)
2
- OLLIE_SUPABASE_URL=http://127.0.0.1:54321
3
- OLLIE_SUPABASE_ANON_KEY=your-anon-key-here
1
+ # None of these are required in prod the published CLI bakes in the prod
2
+ # Supabase URL/anon key and Builder URL. Set these only to point the CLI at a
3
+ # non-prod environment (local Supabase, staging Builder, etc.).
4
4
 
5
- # Required for deploy and status commands
6
- OLLIE_BUILDER_URL=https://builder.ollie.shop
5
+ # Local dev preset (uncomment to target localhost Supabase):
6
+ # OLLIE_SUPABASE_URL=http://127.0.0.1:54321
7
+ # OLLIE_SUPABASE_ANON_KEY=your-anon-key-here
8
+
9
+ # Override Builder service URL (uncomment for staging):
10
+ # OLLIE_BUILDER_URL=https://staging-builder.example.com
@@ -1,5 +1,5 @@
1
1
 
2
- > @ollie-shop/cli@1.2.2 build /home/runner/work/ollie-shop/ollie-shop/packages/cli
2
+ > @ollie-shop/cli@1.3.3 build /home/runner/work/ollie-shop/ollie-shop/packages/cli
3
3
  > tsup
4
4
 
5
5
  CLI Building entry: src/index.tsx
@@ -9,5 +9,5 @@
9
9
  CLI Target: node22
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
- ESM dist/index.js 85.34 KB
13
- ESM ⚡️ Build success in 216ms
12
+ ESM dist/index.js 92.64 KB
13
+ ESM ⚡️ Build success in 300ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,56 @@
1
1
  # @ollie-shop/cli
2
2
 
3
+ ## 1.3.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 464da1e: adjusting cli to update trigger function
8
+
9
+ ## 1.3.2
10
+
11
+ ### Patch Changes
12
+
13
+ - b8d922f: Detect newly created components without restarting the dev server. The esbuild context is now recreated when a component folder is added or removed, so `ollieshop start` serves new components immediately instead of returning 404 until restart.
14
+ - 3b01903: Fix `-v`/`--version` and enrich `whoami`.
15
+
16
+ `-v`/`--version` now report the real package version (inlined from
17
+ `package.json` at build time) instead of the hardcoded `ollie v0.1.0`, and the
18
+ brand string matches the `ollieshop` binary.
19
+
20
+ `whoami` now rejects expired sessions (local `exp` check) and, when an
21
+ `ollie.json` is present, resolves and prints the linked store and organization —
22
+ gated by Supabase RLS, so a store you cannot access is reported as not found
23
+ rather than leaked. With no config it returns just the user plus a hint to run
24
+ `ollieshop init`. The `--stage`/`-s` flag is now parsed as a global flag so
25
+ `whoami` (and other commands) can target `ollie.{stage}.json`.
26
+
27
+ - 2587194: Sync a component's slot back to its local `meta.json` when it's edited in Studio. The CLI `/meta` endpoint now only writes when the incoming `id` matches the component already linked in `meta.json`, so the slot is synced only for the matching local component and never written into the wrong one.
28
+
29
+ ## 1.3.1
30
+
31
+ ### Patch Changes
32
+
33
+ - 2f535de: Bake the prod Builder URL into the published CLI so `ollieshop deploy` works zero-config.
34
+
35
+ `getBuilderUrl()` now falls back to `https://api.ollie.shop/builder/v1` when neither `OLLIE_BUILDER_URL` (runtime env) nor `__OLLIE_BUILDER_URL__` (CI build-time inline) is set. Same pattern is applied to the Supabase URL/anon key as a belt-and-braces fix in case CI inject is ever missing for those too. The CHANGELOG for 1.2.1/1.2.2 already promised this baking via a `PROD_DEFAULTS` const and `envOrDefault()` helper, but the actual code relied solely on the tsup `define` substitution — which silently fell through to an empty string whenever the CI `BUILDER_URL` variable was unset or stale. Result: every published CLI since 1.2.2 has required users to manually export `OLLIE_BUILDER_URL=…` before running `ollieshop deploy` or `ollieshop status`.
36
+
37
+ Resolution order is now explicit: `process.env.OLLIE_*` (local dev override) > tsup build-time inline (per-environment publish) > hardcoded prod default. All three values are public (anon Supabase JWT, public API URLs), so inlining carries no secret-leak risk. Local devs pointing the CLI at `http://127.0.0.1:54321` or a non-prod Supabase project are unaffected — the env var still wins.
38
+
39
+ ## 1.3.0
40
+
41
+ ### Minor Changes
42
+
43
+ - 18c3e52: Add `--org <uuid>` flag to `ollieshop store create` and `ollieshop store list` so users who belong to multiple organizations can target a specific one.
44
+
45
+ Previously, `getOrganizationId()` blindly took the oldest org the user had a membership in, so multi-org users silently landed every `store create` in the same place with no escape hatch (no flag, no config, no env var). Now:
46
+
47
+ - `--org <uuid>` explicitly targets an org; validated via the existing `validateUuid` helper.
48
+ - When `--org` is omitted and the user has multiple orgs, the CLI throws an error listing every accessible org (id + name) instead of silently picking the oldest.
49
+ - When `--org` points to an org the user is not a member of, the same list is surfaced so they can see what they _can_ access.
50
+ - Single-org users are unaffected.
51
+
52
+ Scope is limited to `store` because `getOrganizationId` is only called from `core/store.ts` — other commands (`component`, `version`, `deploy`) derive org through `store_id` relationships.
53
+
3
54
  ## 1.2.2
4
55
 
5
56
  ### Patch Changes
package/CONTEXT.md CHANGED
@@ -16,15 +16,15 @@ Run `ollieshop login` once. Credentials are stored in `~/.ollie-shop/credentials
16
16
 
17
17
  ## Environment Variables
18
18
 
19
- Set these before running authenticated commands (or use a `.env` file):
19
+ None required for prod. Supabase URL, Supabase anon key, and Builder URL are all baked into the published binary. Set these only to override the defaults (e.g. point at local Supabase or a staging Builder):
20
20
 
21
21
  ```
22
22
  OLLIE_SUPABASE_URL=http://127.0.0.1:54321
23
23
  OLLIE_SUPABASE_ANON_KEY=<your-anon-key>
24
- OLLIE_BUILDER_URL=https://builder.ollie.shop
24
+ OLLIE_BUILDER_URL=https://staging-builder.example.com
25
25
  ```
26
26
 
27
- The CLI will throw a clear error if a required env var is missing. See `.env.example` for reference.
27
+ Resolution order: runtime env var > CI build-time inline > hardcoded prod default. See `.env.example` for the local-dev preset.
28
28
 
29
29
  ## Typical Agent Workflow
30
30
 
package/README.md CHANGED
@@ -21,14 +21,10 @@ node packages/cli/dist/index.js <command>
21
21
  # 1. Authenticate
22
22
  ollieshop login
23
23
 
24
- # 2. Set environment variables
25
- export OLLIE_SUPABASE_URL=https://your-project.supabase.co
26
- export OLLIE_SUPABASE_ANON_KEY=your-anon-key
27
-
28
- # 3. Verify identity
24
+ # 2. Verify identity (prod Supabase/Builder URLs are baked in — no env setup needed)
29
25
  ollieshop whoami -o json
30
26
 
31
- # 4. List your stores
27
+ # 3. List your stores
32
28
  ollieshop store list -o json --fields id,name,platform
33
29
  ```
34
30
 
@@ -36,11 +32,11 @@ ollieshop store list -o json --fields id,name,platform
36
32
 
37
33
  | Variable | Required | Description |
38
34
  |----------|----------|-------------|
39
- | `OLLIE_SUPABASE_URL` | Yes (agent commands) | Supabase project URL |
40
- | `OLLIE_SUPABASE_ANON_KEY` | Yes (agent commands) | Supabase anon/public key |
41
- | `OLLIE_BUILDER_URL` | Yes (deploy/status) | Builder service URL |
35
+ | `OLLIE_SUPABASE_URL` | No (prod baked) | Override Supabase project URL — e.g. `http://127.0.0.1:54321` for local dev |
36
+ | `OLLIE_SUPABASE_ANON_KEY` | No (prod baked) | Override Supabase anon/public key |
37
+ | `OLLIE_BUILDER_URL` | No (prod baked) | Override Builder service URL — e.g. a staging endpoint |
42
38
 
43
- See `.env.example` for reference. The CLI will throw a clear error if a required variable is missing.
39
+ All three default to prod and are baked into the published binary. Set the env vars only when pointing the CLI at a non-prod environment. See `.env.example` for local-dev defaults.
44
40
 
45
41
  ## Commands
46
42
 
package/dist/index.js CHANGED
@@ -36,7 +36,7 @@ function HelpCommand() {
36
36
  /* @__PURE__ */ jsxs(Box, { marginLeft: 2, flexDirection: "column", gap: 0, children: [
37
37
  /* @__PURE__ */ jsxs(Box, { children: [
38
38
  /* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(Text, { color: "green", children: "whoami" }) }),
39
- /* @__PURE__ */ jsx(Text, { children: "Show current user and organization" })
39
+ /* @__PURE__ */ jsx(Text, { children: "Show current user (and linked store/org from ollie.json)" })
40
40
  ] }),
41
41
  /* @__PURE__ */ jsxs(Box, { children: [
42
42
  /* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(Text, { color: "green", children: "store create|list" }) }),
@@ -116,6 +116,7 @@ function HelpCommand() {
116
116
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop whoami -o json" }),
117
117
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop schema store.create" }),
118
118
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: '$ ollieshop store create --name "My Store" --platform vtex --platform-store-id mystore' }),
119
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: '$ ollieshop store create --org UUID --name "My Store" --platform vtex --platform-store-id mystore' }),
119
120
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
120
121
  "$ ollieshop store create --data",
121
122
  " ",
@@ -304,6 +305,9 @@ async function getCurrentUser() {
304
305
  if (!credentials) return null;
305
306
  try {
306
307
  const decoded = jwtDecode(credentials.accessToken);
308
+ if (decoded.exp && decoded.exp * 1e3 <= Date.now()) {
309
+ return null;
310
+ }
307
311
  return decoded.email ? { email: decoded.email } : null;
308
312
  } catch {
309
313
  return null;
@@ -439,6 +443,7 @@ function resolveStage(cliStage) {
439
443
  }
440
444
 
441
445
  // src/utils/esbuild.ts
446
+ import { watch } from "fs";
442
447
  import fs4 from "fs/promises";
443
448
  import http from "http";
444
449
  import path4 from "path";
@@ -663,16 +668,69 @@ async function createBuildContext(components, options = {}) {
663
668
  });
664
669
  return ctx;
665
670
  }
666
- async function startDevServer(ctx, options = {}) {
667
- const { port = 4e3, host = "localhost", cwd = process.cwd() } = options;
671
+ async function startDevServer(options = {}) {
672
+ const {
673
+ port = 4e3,
674
+ host = "localhost",
675
+ cwd = process.cwd(),
676
+ stage,
677
+ onRequest,
678
+ onBuildEnd
679
+ } = options;
668
680
  const servedir = path4.join(cwd, "node_modules/.ollie", "build");
669
- await ctx.watch();
681
+ const componentsDir = path4.join(cwd, "components");
670
682
  const internalPort = port + 1;
671
- await ctx.serve({
672
- port: internalPort,
673
- host,
674
- servedir,
675
- onRequest: options.onRequest
683
+ let ctx = null;
684
+ let entryNames = /* @__PURE__ */ new Set();
685
+ async function buildAndServe(components) {
686
+ entryNames = new Set(components.map((c) => c.name));
687
+ ctx = await createBuildContext(components, { cwd, stage, onBuildEnd });
688
+ await ctx.rebuild();
689
+ await ctx.watch();
690
+ await ctx.serve({ port: internalPort, host, servedir, onRequest });
691
+ }
692
+ async function recreate() {
693
+ const components = await discoverComponents({ cwd, stage });
694
+ const oldCtx = ctx;
695
+ ctx = null;
696
+ if (oldCtx) await oldCtx.dispose();
697
+ await buildAndServe(components);
698
+ }
699
+ await buildAndServe(await discoverComponents({ cwd, stage }));
700
+ async function currentComponentNames() {
701
+ try {
702
+ const entries = await glob("*/index.tsx", { cwd: componentsDir });
703
+ return new Set(entries.map((e) => path4.dirname(e)));
704
+ } catch {
705
+ return /* @__PURE__ */ new Set();
706
+ }
707
+ }
708
+ let watchTimer = null;
709
+ let recreating = false;
710
+ let pending = false;
711
+ async function maybeRecreate() {
712
+ if (recreating) {
713
+ pending = true;
714
+ return;
715
+ }
716
+ recreating = true;
717
+ try {
718
+ do {
719
+ pending = false;
720
+ const names = await currentComponentNames();
721
+ const changed = names.size !== entryNames.size || [...names].some((n) => !entryNames.has(n));
722
+ if (!changed) break;
723
+ await recreate();
724
+ } while (pending);
725
+ } catch (err) {
726
+ console.error("[DevServer] Failed to reload components:", err);
727
+ } finally {
728
+ recreating = false;
729
+ }
730
+ }
731
+ const componentsWatcher = watch(componentsDir, { recursive: true }, () => {
732
+ if (watchTimer) clearTimeout(watchTimer);
733
+ watchTimer = setTimeout(maybeRecreate, 150);
676
734
  });
677
735
  const proxyServer = http.createServer(async (req, res) => {
678
736
  const url = new URL(req.url || "/", `http://${host}:${port}`);
@@ -756,6 +814,16 @@ async function startDevServer(ctx, options = {}) {
756
814
  existingMeta = JSON.parse(content);
757
815
  } catch {
758
816
  }
817
+ if (typeof updates.id === "string" && typeof existingMeta.id === "string" && existingMeta.id !== updates.id) {
818
+ res.statusCode = 409;
819
+ res.setHeader("Content-Type", "application/json");
820
+ res.end(
821
+ JSON.stringify({
822
+ error: `id mismatch: ${componentName} is linked to ${existingMeta.id}`
823
+ })
824
+ );
825
+ return;
826
+ }
759
827
  const newMeta = { ...existingMeta, ...updates };
760
828
  await fs4.writeFile(metaPath, JSON.stringify(newMeta, null, 2));
761
829
  res.setHeader("Content-Type", "application/json");
@@ -817,9 +885,14 @@ async function startDevServer(ctx, options = {}) {
817
885
  return {
818
886
  host,
819
887
  port,
888
+ rebuild: async () => {
889
+ await ctx?.rebuild();
890
+ },
820
891
  stop: async () => {
892
+ if (watchTimer) clearTimeout(watchTimer);
893
+ componentsWatcher.close();
821
894
  proxyServer.close();
822
- await ctx.dispose();
895
+ await ctx?.dispose();
823
896
  }
824
897
  };
825
898
  }
@@ -869,7 +942,7 @@ function StartCommand({ args }) {
869
942
  const [buildCount, setBuildCount] = useState2(0);
870
943
  const [lastBuildTime, setLastBuildTime] = useState2(null);
871
944
  const logIdRef = useRef(0);
872
- const ctxRef = useRef(null);
945
+ const rebuildRef = useRef(null);
873
946
  const stopRef = useRef(null);
874
947
  const stage = resolveStage(parseArg(args, "--stage", "-s"));
875
948
  const addLog = useCallback((log) => {
@@ -919,21 +992,17 @@ function StartCommand({ args }) {
919
992
  }
920
993
  setComponents(found);
921
994
  setState({ status: "building" });
922
- const ctx = await createBuildContext(found, {
995
+ const server = await startDevServer({
996
+ port: PORT,
923
997
  stage,
998
+ onRequest: handleRequest,
924
999
  onBuildEnd: (updatedComponents) => {
925
1000
  setComponents(updatedComponents);
926
1001
  setBuildCount((c) => c + 1);
927
1002
  setLastBuildTime(/* @__PURE__ */ new Date());
928
1003
  }
929
1004
  });
930
- ctxRef.current = ctx;
931
- await ctx.rebuild();
932
- if (!mounted) return;
933
- const server = await startDevServer(ctx, {
934
- port: PORT,
935
- onRequest: handleRequest
936
- });
1005
+ rebuildRef.current = server.rebuild;
937
1006
  stopRef.current = server.stop;
938
1007
  if (!mounted) {
939
1008
  await server.stop();
@@ -971,7 +1040,7 @@ function StartCommand({ args }) {
971
1040
  stopRef.current?.().then(() => exit());
972
1041
  }
973
1042
  if (input === "r" && state.status === "running") {
974
- ctxRef.current?.rebuild();
1043
+ rebuildRef.current?.();
975
1044
  }
976
1045
  if (input === "o" && state.status === "running") {
977
1046
  const studioUrl = new URL(STUDIO_BASE_URL);
@@ -1177,7 +1246,11 @@ function App({ command, args }) {
1177
1246
  }
1178
1247
  }
1179
1248
  function VersionCommand() {
1180
- return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { children: "ollie v0.1.0" }) });
1249
+ const version = "1.3.3" ? "1.3.3" : "unknown";
1250
+ return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text4, { children: [
1251
+ "ollieshop v",
1252
+ version
1253
+ ] }) });
1181
1254
  }
1182
1255
  function UnknownCommand({ command }) {
1183
1256
  return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
@@ -1377,7 +1450,8 @@ function parseArgs(argv) {
1377
1450
  output: flags.output || flags.o || void 0,
1378
1451
  dryRun: flags["dry-run"] === true,
1379
1452
  fields: flags.fields ? String(flags.fields).split(",").map((f) => f.trim()) : void 0,
1380
- data: flags.data || flags.d || void 0
1453
+ data: flags.data || flags.d || void 0,
1454
+ stage: flags.stage || flags.s || void 0
1381
1455
  };
1382
1456
  return { command, subcommand, flags, global, positional };
1383
1457
  }
@@ -1397,13 +1471,33 @@ function getBoolFlag(flags, ...names) {
1397
1471
 
1398
1472
  // src/utils/supabase.ts
1399
1473
  import { createClient } from "@supabase/supabase-js";
1474
+ var PROD_DEFAULTS = {
1475
+ supabaseUrl: "https://aazahtmqrhjqsyqoqdkm.supabase.co",
1476
+ supabaseAnonKey: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFhemFodG1xcmhqcXN5cW9xZGttIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDg2MzEwOTcsImV4cCI6MjA2NDIwNzA5N30.VuAbAyjDe0HcL09SZtQ-UmP1o7Z6qwGuOtvfFhnyAcM",
1477
+ builderUrl: "https://api.ollie.shop/builder/v1"
1478
+ };
1479
+ function envOrDefault(envValue, buildTimeValue, prodDefault) {
1480
+ return envValue || buildTimeValue || prodDefault;
1481
+ }
1482
+ function unwrapToOne(embed) {
1483
+ const record = Array.isArray(embed) ? embed[0] : embed;
1484
+ return record && typeof record === "object" ? record : null;
1485
+ }
1400
1486
  async function getAuthenticatedClient() {
1401
1487
  const credentials = await getCredentials();
1402
1488
  if (!credentials) {
1403
1489
  throw new Error("Not authenticated. Run `ollieshop login` first.");
1404
1490
  }
1405
- const supabaseUrl = process.env.OLLIE_SUPABASE_URL || "https://aazahtmqrhjqsyqoqdkm.supabase.co";
1406
- const supabaseAnonKey = process.env.OLLIE_SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFhemFodG1xcmhqcXN5cW9xZGttIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDg2MzEwOTcsImV4cCI6MjA2NDIwNzA5N30.VuAbAyjDe0HcL09SZtQ-UmP1o7Z6qwGuOtvfFhnyAcM";
1491
+ const supabaseUrl = envOrDefault(
1492
+ process.env.OLLIE_SUPABASE_URL,
1493
+ "https://aazahtmqrhjqsyqoqdkm.supabase.co",
1494
+ PROD_DEFAULTS.supabaseUrl
1495
+ );
1496
+ const supabaseAnonKey = envOrDefault(
1497
+ process.env.OLLIE_SUPABASE_ANON_KEY,
1498
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFhemFodG1xcmhqcXN5cW9xZGttIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDg2MzEwOTcsImV4cCI6MjA2NDIwNzA5N30.VuAbAyjDe0HcL09SZtQ-UmP1o7Z6qwGuOtvfFhnyAcM",
1499
+ PROD_DEFAULTS.supabaseAnonKey
1500
+ );
1407
1501
  const client = createClient(supabaseUrl, supabaseAnonKey, {
1408
1502
  auth: {
1409
1503
  autoRefreshToken: false,
@@ -1417,7 +1511,11 @@ async function getAuthenticatedClient() {
1417
1511
  return client;
1418
1512
  }
1419
1513
  function getBuilderUrl() {
1420
- return process.env.OLLIE_BUILDER_URL || "";
1514
+ return envOrDefault(
1515
+ process.env.OLLIE_BUILDER_URL,
1516
+ "",
1517
+ PROD_DEFAULTS.builderUrl
1518
+ );
1421
1519
  }
1422
1520
  async function getAuthToken() {
1423
1521
  const credentials = await getCredentials();
@@ -1426,17 +1524,35 @@ async function getAuthToken() {
1426
1524
  }
1427
1525
  return credentials.accessToken;
1428
1526
  }
1429
- async function getOrganizationId(client) {
1430
- const { data: org, error } = await client.from("organizations").select("id").order("created_at", { ascending: true }).limit(1).maybeSingle();
1527
+ async function getOrganizationId(client, preferredOrgId) {
1528
+ const { data: orgs, error } = await client.from("organizations").select("id, name").order("created_at", { ascending: true });
1431
1529
  if (error) {
1432
1530
  throw new Error(`Failed to get organization: ${error.message}`);
1433
1531
  }
1434
- if (!org) {
1532
+ if (!orgs || orgs.length === 0) {
1435
1533
  throw new Error(
1436
1534
  "No organization found for your account. Create one at https://admin.ollie.shop"
1437
1535
  );
1438
1536
  }
1439
- return org.id;
1537
+ if (preferredOrgId) {
1538
+ const match = orgs.find((o) => o.id === preferredOrgId);
1539
+ if (!match) {
1540
+ const list = orgs.map((o) => ` - ${o.id} ${o.name}`).join("\n");
1541
+ throw new Error(
1542
+ `Organization ${preferredOrgId} not found or you are not a member. Orgs you can access:
1543
+ ${list}`
1544
+ );
1545
+ }
1546
+ return match.id;
1547
+ }
1548
+ if (orgs.length > 1) {
1549
+ const list = orgs.map((o) => ` - ${o.id} ${o.name}`).join("\n");
1550
+ throw new Error(
1551
+ `You belong to multiple organizations. Pass --org <id> to select one:
1552
+ ${list}`
1553
+ );
1554
+ }
1555
+ return orgs[0].id;
1440
1556
  }
1441
1557
 
1442
1558
  // src/utils/validate.ts
@@ -1663,8 +1779,11 @@ var componentListSchema = z2.object({
1663
1779
  var functionCreateSchema = z2.object({
1664
1780
  versionId: z2.string().uuid().describe("Parent version UUID"),
1665
1781
  name: z2.string().min(1).describe("Function name"),
1666
- trigger: z2.string().min(1).optional().describe(
1667
- "Function trigger URL \u2014 absolute http(s) URL or relative path starting with /"
1782
+ trigger: z2.object({
1783
+ url: z2.string().min(1).describe("Trigger URL \u2014 absolute http(s) URL"),
1784
+ expression: z2.string().min(1).describe(`JSONata-like match expression (e.g. 'method in ["GET"]')`)
1785
+ }).optional().describe(
1786
+ "Function trigger \u2014 both url and expression are required together"
1668
1787
  ),
1669
1788
  active: z2.boolean().default(false).describe("Whether function is active (default: false)"),
1670
1789
  onError: z2.enum(["throw", "skip"]).optional().describe("Error handling: throw (default) or skip"),
@@ -2087,6 +2206,36 @@ async function listFunctions(client, storeId, versionId) {
2087
2206
 
2088
2207
  // src/commands/function-cmd.ts
2089
2208
  var ON_ERROR_VALUES = ["throw", "skip"];
2209
+ function parseTriggerFromFlags(urlFlag, expressionFlag) {
2210
+ if (urlFlag === void 0 && expressionFlag === void 0) return void 0;
2211
+ if (urlFlag === void 0 || expressionFlag === void 0) {
2212
+ throw new Error(
2213
+ "Both --trigger-url and --trigger-expression are required when configuring a trigger."
2214
+ );
2215
+ }
2216
+ return {
2217
+ url: validateTriggerUrl(urlFlag, "trigger-url"),
2218
+ expression: validateRequired(expressionFlag, "trigger-expression")
2219
+ };
2220
+ }
2221
+ function parseTriggerFromData(raw) {
2222
+ if (raw === void 0 || raw === null) return void 0;
2223
+ if (typeof raw !== "object" || Array.isArray(raw)) {
2224
+ throw new Error(
2225
+ 'Invalid trigger: must be an object with "url" and "expression" string properties.'
2226
+ );
2227
+ }
2228
+ const obj = raw;
2229
+ if (typeof obj.url !== "string" || typeof obj.expression !== "string") {
2230
+ throw new Error(
2231
+ 'Invalid trigger: both "url" and "expression" must be non-empty strings.'
2232
+ );
2233
+ }
2234
+ return {
2235
+ url: validateTriggerUrl(obj.url, "trigger.url"),
2236
+ expression: validateRequired(obj.expression, "trigger.expression")
2237
+ };
2238
+ }
2090
2239
  async function functionCommand(parsed2) {
2091
2240
  const sub = parsed2.subcommand;
2092
2241
  if (sub === "create") return functionCreateCommand(parsed2);
@@ -2105,13 +2254,14 @@ async function functionCreateCommand(parsed2) {
2105
2254
  input = {
2106
2255
  versionId: validateUuid(raw.versionId, "versionId"),
2107
2256
  name: validateRequired(raw.name, "name"),
2108
- trigger: raw.trigger !== void 0 && raw.trigger !== null ? validateTriggerUrl(String(raw.trigger), "trigger") : void 0,
2257
+ trigger: parseTriggerFromData(raw.trigger),
2109
2258
  active: raw.active === true,
2110
2259
  onError: raw.onError !== void 0 && raw.onError !== null ? validateEnum(String(raw.onError), ON_ERROR_VALUES, "on-error") : "throw",
2111
2260
  priority: raw.priority !== void 0 && raw.priority !== null ? validateInteger(raw.priority, "priority", { min: 0 }) : 0
2112
2261
  };
2113
2262
  } else {
2114
- const triggerFlag = getFlag(parsed2.flags, "trigger");
2263
+ const triggerUrlFlag = getFlag(parsed2.flags, "trigger-url");
2264
+ const triggerExpressionFlag = getFlag(parsed2.flags, "trigger-expression");
2115
2265
  const onErrorFlag = getFlag(parsed2.flags, "on-error");
2116
2266
  const priorityFlag = getFlag(parsed2.flags, "priority");
2117
2267
  input = {
@@ -2120,7 +2270,7 @@ async function functionCreateCommand(parsed2) {
2120
2270
  "version-id"
2121
2271
  ),
2122
2272
  name: validateRequired(getFlag(parsed2.flags, "name", "n"), "name"),
2123
- trigger: triggerFlag !== void 0 ? validateTriggerUrl(triggerFlag, "trigger") : void 0,
2273
+ trigger: parseTriggerFromFlags(triggerUrlFlag, triggerExpressionFlag),
2124
2274
  active: getBoolFlag(parsed2.flags, "active"),
2125
2275
  onError: onErrorFlag !== void 0 ? validateEnum(onErrorFlag, ON_ERROR_VALUES, "on-error") : "throw",
2126
2276
  priority: priorityFlag !== void 0 ? validateInteger(priorityFlag, "priority", { min: 0 }) : 0
@@ -2304,14 +2454,14 @@ async function statusCommand(parsed2) {
2304
2454
  }
2305
2455
 
2306
2456
  // src/core/store.ts
2307
- async function createStore(client, input) {
2457
+ async function createStore(client, input, orgId) {
2308
2458
  const parsed2 = storeCreateSchema.safeParse(input);
2309
2459
  if (!parsed2.success) {
2310
2460
  return {
2311
2461
  error: { message: parsed2.error.issues.map((i) => i.message).join("; ") }
2312
2462
  };
2313
2463
  }
2314
- const organizationId = await getOrganizationId(client);
2464
+ const organizationId = await getOrganizationId(client, orgId);
2315
2465
  const { data, error } = await client.from("stores").insert({
2316
2466
  name: parsed2.data.name,
2317
2467
  platform: parsed2.data.platform,
@@ -2325,8 +2475,33 @@ async function createStore(client, input) {
2325
2475
  }
2326
2476
  return { data: { id: data.id } };
2327
2477
  }
2328
- async function listStores(client) {
2329
- const organizationId = await getOrganizationId(client);
2478
+ async function getStoreById(client, storeId) {
2479
+ const { data, error } = await client.from("stores").select("id, name, organization_id, organizations(id, name)").eq("id", storeId).maybeSingle();
2480
+ if (error) {
2481
+ return { error: { message: error.message } };
2482
+ }
2483
+ if (!data) {
2484
+ return {
2485
+ error: {
2486
+ message: `Store ${storeId} not found or you don't have access to it.`
2487
+ }
2488
+ };
2489
+ }
2490
+ const org = unwrapToOne(
2491
+ data.organizations
2492
+ );
2493
+ const orgName = org?.name ?? null;
2494
+ return {
2495
+ data: {
2496
+ id: data.id,
2497
+ name: data.name,
2498
+ organizationId: data.organization_id,
2499
+ organizationName: orgName
2500
+ }
2501
+ };
2502
+ }
2503
+ async function listStores(client, orgId) {
2504
+ const organizationId = await getOrganizationId(client, orgId);
2330
2505
  const { data, error } = await client.from("stores").select(
2331
2506
  "id, name, platform, platform_store_id, organization_id, logo, settings, created_at"
2332
2507
  ).eq("organization_id", organizationId).order("created_at", { ascending: false });
@@ -2378,12 +2553,14 @@ async function storeCreateCommand(parsed2) {
2378
2553
  settings: getFlag(parsed2.flags, "settings")
2379
2554
  };
2380
2555
  }
2556
+ const orgFlag = getFlag(parsed2.flags, "org");
2557
+ const orgId = orgFlag ? validateUuid(orgFlag, "org") : void 0;
2381
2558
  if (parsed2.global.dryRun) {
2382
- outputDryRun("store.create", input, format);
2559
+ outputDryRun("store.create", { ...input, orgId }, format);
2383
2560
  return;
2384
2561
  }
2385
2562
  const client = await getAuthenticatedClient();
2386
- const result = await createStore(client, input);
2563
+ const result = await createStore(client, input, orgId);
2387
2564
  outputResult(result, format, parsed2.global.fields);
2388
2565
  if (result.error) process.exit(1);
2389
2566
  } catch (err) {
@@ -2399,8 +2576,10 @@ async function storeCreateCommand(parsed2) {
2399
2576
  async function storeListCommand(parsed2) {
2400
2577
  const format = detectOutputFormat(parsed2.global.output);
2401
2578
  try {
2579
+ const orgFlag = getFlag(parsed2.flags, "org");
2580
+ const orgId = orgFlag ? validateUuid(orgFlag, "org") : void 0;
2402
2581
  const client = await getAuthenticatedClient();
2403
- const result = await listStores(client);
2582
+ const result = await listStores(client, orgId);
2404
2583
  outputResult(result, format, parsed2.global.fields);
2405
2584
  if (result.error) process.exit(1);
2406
2585
  } catch (err) {
@@ -2524,21 +2703,71 @@ async function whoamiCommand(parsed2) {
2524
2703
  if (!user) {
2525
2704
  outputResult(
2526
2705
  {
2527
- error: { message: "Not logged in. Run `ollieshop login`." }
2706
+ error: {
2707
+ message: "Not logged in or session expired. Run `ollieshop login`."
2708
+ }
2709
+ },
2710
+ format
2711
+ );
2712
+ process.exit(1);
2713
+ }
2714
+ try {
2715
+ const stage = resolveStage(parsed2.global.stage);
2716
+ const config = await loadConfig({ stage });
2717
+ if (!config?.storeId) {
2718
+ outputResult(
2719
+ {
2720
+ data: {
2721
+ email: user.email,
2722
+ hint: "No ollie.json found. Run `ollieshop init` to link a store."
2723
+ }
2724
+ },
2725
+ format,
2726
+ parsed2.global.fields
2727
+ );
2728
+ return;
2729
+ }
2730
+ const client = await getAuthenticatedClient();
2731
+ const result = await getStoreById(client, config.storeId);
2732
+ if (result.error || !result.data) {
2733
+ outputResult(
2734
+ {
2735
+ data: {
2736
+ email: user.email,
2737
+ storeId: config.storeId,
2738
+ store: null,
2739
+ note: result.error?.message ?? "Store not found. Run `ollieshop login` or check the storeId in ollie.json."
2740
+ }
2741
+ },
2742
+ format,
2743
+ parsed2.global.fields
2744
+ );
2745
+ return;
2746
+ }
2747
+ const store = result.data;
2748
+ outputResult(
2749
+ {
2750
+ data: {
2751
+ email: user.email,
2752
+ orgId: store.organizationId,
2753
+ org: store.organizationName,
2754
+ storeId: store.id,
2755
+ store: store.name,
2756
+ ...config.versionId ? { versionId: config.versionId } : {}
2757
+ }
2758
+ },
2759
+ format,
2760
+ parsed2.global.fields
2761
+ );
2762
+ } catch (err) {
2763
+ outputResult(
2764
+ {
2765
+ error: { message: err instanceof Error ? err.message : String(err) }
2528
2766
  },
2529
2767
  format
2530
2768
  );
2531
2769
  process.exit(1);
2532
2770
  }
2533
- outputResult(
2534
- {
2535
- data: {
2536
- email: user.email
2537
- }
2538
- },
2539
- format,
2540
- parsed2.global.fields
2541
- );
2542
2771
  }
2543
2772
 
2544
2773
  // src/index.tsx