@ollie-shop/cli 1.2.1 → 1.2.2

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @ollie-shop/cli@1.2.1 build /home/runner/work/ollie-shop/ollie-shop/packages/cli
2
+ > @ollie-shop/cli@1.2.2 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 78.88 KB
13
- ESM ⚡️ Build success in 157ms
12
+ ESM dist/index.js 85.34 KB
13
+ ESM ⚡️ Build success in 216ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @ollie-shop/cli
2
2
 
3
+ ## 1.2.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 0268a4b: Bake prod Supabase + builder defaults into the CLI so `ollieshop` runs with zero env setup.
8
+
9
+ Agent commands (`whoami`, `store`, `version`, `component`, `function`, `deploy`, `status`) previously threw `Missing required environment variable: OLLIE_SUPABASE_URL` unless the user had exported `OLLIE_SUPABASE_URL`, `OLLIE_SUPABASE_ANON_KEY`, and `OLLIE_BUILDER_URL` in their shell first. All three values are public, so the friction was pure onboarding overhead.
10
+
11
+ `packages/cli/src/utils/supabase.ts` now holds a `PROD_DEFAULTS` const and an `envOrDefault()` helper. Env vars still win when set, so devs pointing the CLI at local Supabase (`http://127.0.0.1:54321`) or a non-prod project are unaffected. Fresh contributors can now run `ollieshop login && ollieshop whoami` without any shell setup.
12
+
3
13
  ## 1.2.1
4
14
 
5
15
  ### Patch Changes
package/CONTEXT.md CHANGED
@@ -33,6 +33,7 @@ The CLI will throw a clear error if a required env var is missing. See `.env.exa
33
33
  ollieshop schema store.create -o json
34
34
  ollieshop schema version.create -o json
35
35
  ollieshop schema component.create -o json
36
+ ollieshop schema function.create -o json
36
37
 
37
38
  # 2. Verify identity
38
39
  ollieshop whoami -o json
@@ -50,17 +51,21 @@ ollieshop init --store-id <STORE_ID> --version-id <VERSION_ID>
50
51
  # 6. Register components
51
52
  ollieshop component create --data '{"versionId":"<VERSION_ID>","name":"FreeShippingBar","slot":"cart_header_full_page","active":true}' -o json
52
53
 
53
- # 7. Deploy component (bundle + upload + build)
54
+ # 7. Register functions (optional)
55
+ ollieshop function create --data '{"versionId":"<VERSION_ID>","name":"myHook","active":true}' -o json
56
+
57
+ # 8. Deploy component (bundle + upload + build)
54
58
  ollieshop deploy --component-id <COMPONENT_ID> --name FreeShippingBar --wait -o json
55
59
 
56
- # 8. Check build status
60
+ # 9. Check build status
57
61
  ollieshop status --build-id <BUILD_ID> -o json
58
62
  ollieshop status --build-id <BUILD_ID> --wait --timeout 300 -o json
59
63
 
60
- # 9. List resources
64
+ # 10. List resources
61
65
  ollieshop store list -o json --fields id,name,platform
62
66
  ollieshop version list --store-id <STORE_ID> -o json --fields id,name,active
63
67
  ollieshop component list --store-id <STORE_ID> -o json --fields id,name,slot,active
68
+ ollieshop function list --store-id <STORE_ID> -o json --fields id,name,active
64
69
  ```
65
70
 
66
71
  ## Deploy Workflow
package/README.md CHANGED
@@ -106,6 +106,18 @@ ollieshop component list --store-id <STORE_UUID> -o json --fields id,name,slot,a
106
106
  ollieshop component create --version-id <VERSION_UUID> --name FreeShippingBar --slot cart_header_full_page -o json
107
107
  ```
108
108
 
109
+ #### `function create|list`
110
+
111
+ Create or list functions for a version.
112
+
113
+ ```bash
114
+ # List functions
115
+ ollieshop function list --store-id <STORE_UUID> -o json --fields id,name,active
116
+
117
+ # Create a function
118
+ ollieshop function create --version-id <VERSION_UUID> --name myHook -o json
119
+ ```
120
+
109
121
  #### `deploy`
110
122
 
111
123
  Bundle a component directory into a zip and upload to the Builder service.
@@ -207,6 +219,7 @@ src/
207
219
  │ ├── store-cmd.ts # Agent: store CRUD
208
220
  │ ├── version-cmd.ts # Agent: version CRUD
209
221
  │ ├── component-cmd.ts # Agent: component CRUD
222
+ │ ├── function-cmd.ts # Agent: function CRUD
210
223
  │ ├── deploy-cmd.ts # Agent: bundle + upload builds
211
224
  │ ├── status-cmd.ts # Agent: check/poll build status
212
225
  │ ├── schema-cmd.ts # Agent: schema introspection
@@ -218,6 +231,7 @@ src/
218
231
  │ ├── store.ts # Store business logic + Supabase queries
219
232
  │ ├── version.ts # Version business logic + Supabase queries
220
233
  │ ├── component.ts # Component business logic + Supabase queries
234
+ │ ├── function.ts # Function business logic + Supabase queries
221
235
  │ ├── deploy.ts # Builder API client (upload, status, poll)
222
236
  │ └── schema.ts # Zod schemas + JSON Schema generation
223
237
  └── utils/
package/dist/index.js CHANGED
@@ -50,6 +50,10 @@ function HelpCommand() {
50
50
  /* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(Text, { color: "green", children: "component create|list" }) }),
51
51
  /* @__PURE__ */ jsx(Text, { children: "Create or list components" })
52
52
  ] }),
53
+ /* @__PURE__ */ jsxs(Box, { children: [
54
+ /* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(Text, { color: "green", children: "function create|list" }) }),
55
+ /* @__PURE__ */ jsx(Text, { children: "Create or list functions" })
56
+ ] }),
53
57
  /* @__PURE__ */ jsxs(Box, { children: [
54
58
  /* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(Text, { color: "green", children: "deploy" }) }),
55
59
  /* @__PURE__ */ jsx(Text, { children: "Bundle and upload a component/function build" })
@@ -121,6 +125,7 @@ function HelpCommand() {
121
125
  ] }),
122
126
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop version create --store-id UUID --name v1 --active" }),
123
127
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop init --store-id UUID --version-id UUID" }),
128
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop function create --version-id UUID --name myHook" }),
124
129
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop deploy --component-id UUID --name FreeShippingBar --wait" }),
125
130
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop status --build-id BUILD_ID --wait -o json" })
126
131
  ] })
@@ -1467,6 +1472,39 @@ function validateRequired(value, name) {
1467
1472
  }
1468
1473
  return rejectControlChars(value.trim(), name);
1469
1474
  }
1475
+ function validateInteger(value, name, opts) {
1476
+ const n = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : Number.NaN;
1477
+ if (!Number.isFinite(n) || !Number.isInteger(n)) {
1478
+ throw new Error(`Invalid ${name}: "${String(value)}". Must be an integer.`);
1479
+ }
1480
+ if (opts?.min !== void 0 && n < opts.min) {
1481
+ throw new Error(`Invalid ${name}: ${n}. Must be >= ${opts.min}.`);
1482
+ }
1483
+ return n;
1484
+ }
1485
+ function validateTriggerUrl(value, name) {
1486
+ rejectControlChars(value, name);
1487
+ const trimmed = value.trim();
1488
+ if (trimmed === "") {
1489
+ throw new Error(
1490
+ `Invalid ${name}: must be a non-empty absolute http(s) URL or a relative path starting with "/".`
1491
+ );
1492
+ }
1493
+ if (trimmed.startsWith("/")) {
1494
+ return trimmed;
1495
+ }
1496
+ try {
1497
+ const parsed2 = new URL(trimmed);
1498
+ if (parsed2.protocol !== "http:" && parsed2.protocol !== "https:") {
1499
+ throw new Error("unsupported protocol");
1500
+ }
1501
+ return trimmed;
1502
+ } catch {
1503
+ throw new Error(
1504
+ `Invalid ${name}: "${value}". Must be an absolute http(s) URL or a relative path starting with "/".`
1505
+ );
1506
+ }
1507
+ }
1470
1508
 
1471
1509
  // src/commands/business-rule-cmd.ts
1472
1510
  async function businessRuleCommand(parsed2) {
@@ -1625,8 +1663,12 @@ var componentListSchema = z2.object({
1625
1663
  var functionCreateSchema = z2.object({
1626
1664
  versionId: z2.string().uuid().describe("Parent version UUID"),
1627
1665
  name: z2.string().min(1).describe("Function name"),
1628
- trigger: z2.string().min(1).describe("Function trigger (e.g. beforePayment, afterShipping)"),
1629
- active: z2.boolean().default(true).describe("Whether function is active")
1666
+ trigger: z2.string().min(1).optional().describe(
1667
+ "Function trigger URL \u2014 absolute http(s) URL or relative path starting with /"
1668
+ ),
1669
+ active: z2.boolean().default(false).describe("Whether function is active (default: false)"),
1670
+ onError: z2.enum(["throw", "skip"]).optional().describe("Error handling: throw (default) or skip"),
1671
+ priority: z2.number().int().min(0).optional().describe("Execution order priority (default: 0)")
1630
1672
  });
1631
1673
  var functionListSchema = z2.object({
1632
1674
  versionId: z2.string().uuid().describe("Version UUID to list functions for")
@@ -2005,6 +2047,137 @@ async function deployCommand(parsed2) {
2005
2047
  }
2006
2048
  }
2007
2049
 
2050
+ // src/core/function.ts
2051
+ async function createFunction(client, input) {
2052
+ const parsed2 = functionCreateSchema.safeParse(input);
2053
+ if (!parsed2.success) {
2054
+ return {
2055
+ success: false,
2056
+ error: {
2057
+ message: parsed2.error.issues.map((i) => i.message).join("; ")
2058
+ }
2059
+ };
2060
+ }
2061
+ const { data, error } = await client.from("functions").insert({
2062
+ name: parsed2.data.name,
2063
+ active: parsed2.data.active,
2064
+ version_id: parsed2.data.versionId,
2065
+ trigger: parsed2.data.trigger ?? null,
2066
+ on_error: parsed2.data.onError ?? "throw",
2067
+ priority: parsed2.data.priority ?? 0
2068
+ }).select("id").single();
2069
+ if (error) {
2070
+ return { success: false, error: { message: error.message } };
2071
+ }
2072
+ return { success: true, data: { id: data.id } };
2073
+ }
2074
+ async function listFunctions(client, storeId, versionId) {
2075
+ let query = client.from("functions").select(
2076
+ "id, name, active, urn, on_error, priority, trigger, invocation, version_id, created_at, versions!inner(id, name)"
2077
+ ).eq("versions.store_id", storeId);
2078
+ if (versionId) {
2079
+ query = query.eq("versions.id", versionId);
2080
+ }
2081
+ const { data, error } = await query;
2082
+ if (error) {
2083
+ return { success: false, error: { message: error.message } };
2084
+ }
2085
+ return { success: true, data: data ?? [] };
2086
+ }
2087
+
2088
+ // src/commands/function-cmd.ts
2089
+ var ON_ERROR_VALUES = ["throw", "skip"];
2090
+ async function functionCommand(parsed2) {
2091
+ const sub = parsed2.subcommand;
2092
+ if (sub === "create") return functionCreateCommand(parsed2);
2093
+ if (sub === "list" || sub === "ls") return functionListCommand(parsed2);
2094
+ console.error(
2095
+ `Unknown function subcommand: ${sub}. Use: function create | function list`
2096
+ );
2097
+ process.exit(1);
2098
+ }
2099
+ async function functionCreateCommand(parsed2) {
2100
+ const format = detectOutputFormat(parsed2.global.output);
2101
+ try {
2102
+ let input;
2103
+ if (parsed2.global.data) {
2104
+ const raw = JSON.parse(parsed2.global.data);
2105
+ input = {
2106
+ versionId: validateUuid(raw.versionId, "versionId"),
2107
+ name: validateRequired(raw.name, "name"),
2108
+ trigger: raw.trigger !== void 0 && raw.trigger !== null ? validateTriggerUrl(String(raw.trigger), "trigger") : void 0,
2109
+ active: raw.active === true,
2110
+ onError: raw.onError !== void 0 && raw.onError !== null ? validateEnum(String(raw.onError), ON_ERROR_VALUES, "on-error") : "throw",
2111
+ priority: raw.priority !== void 0 && raw.priority !== null ? validateInteger(raw.priority, "priority", { min: 0 }) : 0
2112
+ };
2113
+ } else {
2114
+ const triggerFlag = getFlag(parsed2.flags, "trigger");
2115
+ const onErrorFlag = getFlag(parsed2.flags, "on-error");
2116
+ const priorityFlag = getFlag(parsed2.flags, "priority");
2117
+ input = {
2118
+ versionId: validateUuid(
2119
+ validateRequired(getFlag(parsed2.flags, "version-id"), "version-id"),
2120
+ "version-id"
2121
+ ),
2122
+ name: validateRequired(getFlag(parsed2.flags, "name", "n"), "name"),
2123
+ trigger: triggerFlag !== void 0 ? validateTriggerUrl(triggerFlag, "trigger") : void 0,
2124
+ active: getBoolFlag(parsed2.flags, "active"),
2125
+ onError: onErrorFlag !== void 0 ? validateEnum(onErrorFlag, ON_ERROR_VALUES, "on-error") : "throw",
2126
+ priority: priorityFlag !== void 0 ? validateInteger(priorityFlag, "priority", { min: 0 }) : 0
2127
+ };
2128
+ }
2129
+ const validated = functionCreateSchema.safeParse(input);
2130
+ if (!validated.success) {
2131
+ throw new Error(validated.error.issues.map((i) => i.message).join("; "));
2132
+ }
2133
+ if (parsed2.global.dryRun) {
2134
+ outputDryRun(
2135
+ "function.create",
2136
+ validated.data,
2137
+ format
2138
+ );
2139
+ return;
2140
+ }
2141
+ const client = await getAuthenticatedClient();
2142
+ const result = await createFunction(client, validated.data);
2143
+ outputResult(result, format, parsed2.global.fields);
2144
+ if (!result.success) process.exit(1);
2145
+ } catch (err) {
2146
+ outputResult(
2147
+ {
2148
+ success: false,
2149
+ error: { message: err instanceof Error ? err.message : String(err) }
2150
+ },
2151
+ format
2152
+ );
2153
+ process.exit(1);
2154
+ }
2155
+ }
2156
+ async function functionListCommand(parsed2) {
2157
+ const format = detectOutputFormat(parsed2.global.output);
2158
+ try {
2159
+ const storeId = validateUuid(
2160
+ validateRequired(getFlag(parsed2.flags, "store-id"), "store-id"),
2161
+ "store-id"
2162
+ );
2163
+ const versionId = getFlag(parsed2.flags, "version-id");
2164
+ if (versionId) validateUuid(versionId, "version-id");
2165
+ const client = await getAuthenticatedClient();
2166
+ const result = await listFunctions(client, storeId, versionId);
2167
+ outputResult(result, format, parsed2.global.fields);
2168
+ if (!result.success) process.exit(1);
2169
+ } catch (err) {
2170
+ outputResult(
2171
+ {
2172
+ success: false,
2173
+ error: { message: err instanceof Error ? err.message : String(err) }
2174
+ },
2175
+ format
2176
+ );
2177
+ process.exit(1);
2178
+ }
2179
+ }
2180
+
2008
2181
  // src/commands/init-cmd.ts
2009
2182
  async function initCommand(parsed2) {
2010
2183
  const format = detectOutputFormat(parsed2.global.output);
@@ -2376,6 +2549,7 @@ var AGENT_COMMANDS = {
2376
2549
  version: versionCommand,
2377
2550
  component: componentCommand,
2378
2551
  "business-rule": businessRuleCommand,
2552
+ function: functionCommand,
2379
2553
  schema: schemaCommand,
2380
2554
  init: initCommand,
2381
2555
  deploy: deployCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ollie-shop/cli",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Ollie Shop CLI - Development tools for custom checkouts",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,146 @@
1
+ import { createFunction, listFunctions } from "../core/function.js";
2
+ import { functionCreateSchema } from "../core/schema.js";
3
+ import {
4
+ detectOutputFormat,
5
+ outputDryRun,
6
+ outputResult,
7
+ } from "../utils/output.js";
8
+ import { type ParsedArgs, getBoolFlag, getFlag } from "../utils/parse-args.js";
9
+ import { getAuthenticatedClient } from "../utils/supabase.js";
10
+ import {
11
+ validateEnum,
12
+ validateInteger,
13
+ validateRequired,
14
+ validateTriggerUrl,
15
+ validateUuid,
16
+ } from "../utils/validate.js";
17
+
18
+ const ON_ERROR_VALUES = ["throw", "skip"] as const;
19
+
20
+ export async function functionCommand(parsed: ParsedArgs): Promise<void> {
21
+ const sub = parsed.subcommand;
22
+ if (sub === "create") return functionCreateCommand(parsed);
23
+ if (sub === "list" || sub === "ls") return functionListCommand(parsed);
24
+
25
+ console.error(
26
+ `Unknown function subcommand: ${sub}. Use: function create | function list`,
27
+ );
28
+ process.exit(1);
29
+ }
30
+
31
+ async function functionCreateCommand(parsed: ParsedArgs): Promise<void> {
32
+ const format = detectOutputFormat(parsed.global.output);
33
+
34
+ try {
35
+ let input: {
36
+ versionId: string;
37
+ name: string;
38
+ trigger?: string;
39
+ active?: boolean;
40
+ onError?: "throw" | "skip";
41
+ priority?: number;
42
+ };
43
+
44
+ if (parsed.global.data) {
45
+ const raw = JSON.parse(parsed.global.data);
46
+ input = {
47
+ versionId: validateUuid(raw.versionId, "versionId"),
48
+ name: validateRequired(raw.name, "name"),
49
+ trigger:
50
+ raw.trigger !== undefined && raw.trigger !== null
51
+ ? validateTriggerUrl(String(raw.trigger), "trigger")
52
+ : undefined,
53
+ active: raw.active === true,
54
+ onError:
55
+ raw.onError !== undefined && raw.onError !== null
56
+ ? validateEnum(String(raw.onError), ON_ERROR_VALUES, "on-error")
57
+ : "throw",
58
+ priority:
59
+ raw.priority !== undefined && raw.priority !== null
60
+ ? validateInteger(raw.priority, "priority", { min: 0 })
61
+ : 0,
62
+ };
63
+ } else {
64
+ const triggerFlag = getFlag(parsed.flags, "trigger");
65
+ const onErrorFlag = getFlag(parsed.flags, "on-error");
66
+ const priorityFlag = getFlag(parsed.flags, "priority");
67
+ input = {
68
+ versionId: validateUuid(
69
+ validateRequired(getFlag(parsed.flags, "version-id"), "version-id"),
70
+ "version-id",
71
+ ),
72
+ name: validateRequired(getFlag(parsed.flags, "name", "n"), "name"),
73
+ trigger:
74
+ triggerFlag !== undefined
75
+ ? validateTriggerUrl(triggerFlag, "trigger")
76
+ : undefined,
77
+ active: getBoolFlag(parsed.flags, "active"),
78
+ onError:
79
+ onErrorFlag !== undefined
80
+ ? validateEnum(onErrorFlag, ON_ERROR_VALUES, "on-error")
81
+ : "throw",
82
+ priority:
83
+ priorityFlag !== undefined
84
+ ? validateInteger(priorityFlag, "priority", { min: 0 })
85
+ : 0,
86
+ };
87
+ }
88
+
89
+ const validated = functionCreateSchema.safeParse(input);
90
+ if (!validated.success) {
91
+ throw new Error(validated.error.issues.map((i) => i.message).join("; "));
92
+ }
93
+
94
+ if (parsed.global.dryRun) {
95
+ outputDryRun(
96
+ "function.create",
97
+ validated.data as Record<string, unknown>,
98
+ format,
99
+ );
100
+ return;
101
+ }
102
+
103
+ const client = await getAuthenticatedClient();
104
+ const result = await createFunction(client, validated.data);
105
+
106
+ outputResult(result, format, parsed.global.fields);
107
+ if (!result.success) process.exit(1);
108
+ } catch (err) {
109
+ outputResult(
110
+ {
111
+ success: false,
112
+ error: { message: err instanceof Error ? err.message : String(err) },
113
+ },
114
+ format,
115
+ );
116
+ process.exit(1);
117
+ }
118
+ }
119
+
120
+ async function functionListCommand(parsed: ParsedArgs): Promise<void> {
121
+ const format = detectOutputFormat(parsed.global.output);
122
+
123
+ try {
124
+ const storeId = validateUuid(
125
+ validateRequired(getFlag(parsed.flags, "store-id"), "store-id"),
126
+ "store-id",
127
+ );
128
+ const versionId = getFlag(parsed.flags, "version-id");
129
+ if (versionId) validateUuid(versionId, "version-id");
130
+
131
+ const client = await getAuthenticatedClient();
132
+ const result = await listFunctions(client, storeId, versionId);
133
+
134
+ outputResult(result, format, parsed.global.fields);
135
+ if (!result.success) process.exit(1);
136
+ } catch (err) {
137
+ outputResult(
138
+ {
139
+ success: false,
140
+ error: { message: err instanceof Error ? err.message : String(err) },
141
+ },
142
+ format,
143
+ );
144
+ process.exit(1);
145
+ }
146
+ }
@@ -61,6 +61,12 @@ export function HelpCommand() {
61
61
  </Box>
62
62
  <Text>Create or list components</Text>
63
63
  </Box>
64
+ <Box>
65
+ <Box width={24}>
66
+ <Text color="green">function create|list</Text>
67
+ </Box>
68
+ <Text>Create or list functions</Text>
69
+ </Box>
64
70
  <Box>
65
71
  <Box width={24}>
66
72
  <Text color="green">deploy</Text>
@@ -158,6 +164,9 @@ export function HelpCommand() {
158
164
  $ ollieshop version create --store-id UUID --name v1 --active
159
165
  </Text>
160
166
  <Text dimColor>$ ollieshop init --store-id UUID --version-id UUID</Text>
167
+ <Text dimColor>
168
+ $ ollieshop function create --version-id UUID --name myHook
169
+ </Text>
161
170
  <Text dimColor>
162
171
  $ ollieshop deploy --component-id UUID --name FreeShippingBar --wait
163
172
  </Text>
@@ -0,0 +1,92 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+ import { functionCreateSchema } from "./schema.js";
3
+
4
+ export interface CreateFunctionInput {
5
+ versionId: string;
6
+ name: string;
7
+ trigger?: string;
8
+ active?: boolean;
9
+ onError?: "throw" | "skip";
10
+ priority?: number;
11
+ }
12
+
13
+ export interface FunctionRecord {
14
+ id: string;
15
+ name: string;
16
+ active: boolean;
17
+ urn: string | null;
18
+ on_error: string;
19
+ priority: number;
20
+ trigger: { url: string; expression: string } | null;
21
+ invocation: string | null;
22
+ version_id: string;
23
+ created_at: string;
24
+ versions?: { id: string; name: string };
25
+ }
26
+
27
+ export async function createFunction(
28
+ client: SupabaseClient,
29
+ input: CreateFunctionInput,
30
+ ): Promise<
31
+ | { success: true; data: { id: string } }
32
+ | { success: false; error: { message: string } }
33
+ > {
34
+ const parsed = functionCreateSchema.safeParse(input);
35
+ if (!parsed.success) {
36
+ return {
37
+ success: false,
38
+ error: {
39
+ message: parsed.error.issues
40
+ .map((i: { message: string }) => i.message)
41
+ .join("; "),
42
+ },
43
+ };
44
+ }
45
+
46
+ const { data, error } = await client
47
+ .from("functions")
48
+ .insert({
49
+ name: parsed.data.name,
50
+ active: parsed.data.active,
51
+ version_id: parsed.data.versionId,
52
+ trigger: parsed.data.trigger ?? null,
53
+ on_error: parsed.data.onError ?? "throw",
54
+ priority: parsed.data.priority ?? 0,
55
+ })
56
+ .select("id")
57
+ .single();
58
+
59
+ if (error) {
60
+ return { success: false, error: { message: error.message } };
61
+ }
62
+
63
+ return { success: true, data: { id: data.id } };
64
+ }
65
+
66
+ export async function listFunctions(
67
+ client: SupabaseClient,
68
+ storeId: string,
69
+ versionId?: string,
70
+ ): Promise<
71
+ | { success: true; data: FunctionRecord[] }
72
+ | { success: false; error: { message: string } }
73
+ > {
74
+ let query = client
75
+ .from("functions")
76
+ .select(
77
+ "id, name, active, urn, on_error, priority, trigger, invocation, version_id, created_at, versions!inner(id, name)",
78
+ )
79
+ .eq("versions.store_id", storeId);
80
+
81
+ if (versionId) {
82
+ query = query.eq("versions.id", versionId);
83
+ }
84
+
85
+ const { data, error } = await query;
86
+
87
+ if (error) {
88
+ return { success: false, error: { message: error.message } };
89
+ }
90
+
91
+ return { success: true, data: (data ?? []) as unknown as FunctionRecord[] };
92
+ }
@@ -67,8 +67,24 @@ export const functionCreateSchema = z.object({
67
67
  trigger: z
68
68
  .string()
69
69
  .min(1)
70
- .describe("Function trigger (e.g. beforePayment, afterShipping)"),
71
- active: z.boolean().default(true).describe("Whether function is active"),
70
+ .optional()
71
+ .describe(
72
+ "Function trigger URL — absolute http(s) URL or relative path starting with /",
73
+ ),
74
+ active: z
75
+ .boolean()
76
+ .default(false)
77
+ .describe("Whether function is active (default: false)"),
78
+ onError: z
79
+ .enum(["throw", "skip"])
80
+ .optional()
81
+ .describe("Error handling: throw (default) or skip"),
82
+ priority: z
83
+ .number()
84
+ .int()
85
+ .min(0)
86
+ .optional()
87
+ .describe("Execution order priority (default: 0)"),
72
88
  });
73
89
 
74
90
  export const functionListSchema = z.object({
package/src/index.tsx CHANGED
@@ -3,6 +3,7 @@ import { App } from "./cli.js";
3
3
  import { businessRuleCommand } from "./commands/business-rule-cmd.js";
4
4
  import { componentCommand } from "./commands/component-cmd.js";
5
5
  import { deployCommand } from "./commands/deploy-cmd.js";
6
+ import { functionCommand } from "./commands/function-cmd.js";
6
7
  import { initCommand } from "./commands/init-cmd.js";
7
8
  import { schemaCommand } from "./commands/schema-cmd.js";
8
9
  import { statusCommand } from "./commands/status-cmd.js";
@@ -21,6 +22,7 @@ const AGENT_COMMANDS: Record<
21
22
  version: versionCommand,
22
23
  component: componentCommand,
23
24
  "business-rule": businessRuleCommand,
25
+ function: functionCommand,
24
26
  schema: schemaCommand,
25
27
  init: initCommand,
26
28
  deploy: deployCommand,
@@ -56,3 +56,47 @@ export function validateRequired(
56
56
  }
57
57
  return rejectControlChars(value.trim(), name);
58
58
  }
59
+
60
+ export function validateInteger(
61
+ value: unknown,
62
+ name: string,
63
+ opts?: { min?: number },
64
+ ): number {
65
+ const n =
66
+ typeof value === "number"
67
+ ? value
68
+ : typeof value === "string" && value.trim() !== ""
69
+ ? Number(value)
70
+ : Number.NaN;
71
+ if (!Number.isFinite(n) || !Number.isInteger(n)) {
72
+ throw new Error(`Invalid ${name}: "${String(value)}". Must be an integer.`);
73
+ }
74
+ if (opts?.min !== undefined && n < opts.min) {
75
+ throw new Error(`Invalid ${name}: ${n}. Must be >= ${opts.min}.`);
76
+ }
77
+ return n;
78
+ }
79
+
80
+ export function validateTriggerUrl(value: string, name: string): string {
81
+ rejectControlChars(value, name);
82
+ const trimmed = value.trim();
83
+ if (trimmed === "") {
84
+ throw new Error(
85
+ `Invalid ${name}: must be a non-empty absolute http(s) URL or a relative path starting with "/".`,
86
+ );
87
+ }
88
+ if (trimmed.startsWith("/")) {
89
+ return trimmed;
90
+ }
91
+ try {
92
+ const parsed = new URL(trimmed);
93
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
94
+ throw new Error("unsupported protocol");
95
+ }
96
+ return trimmed;
97
+ } catch {
98
+ throw new Error(
99
+ `Invalid ${name}: "${value}". Must be an absolute http(s) URL or a relative path starting with "/".`,
100
+ );
101
+ }
102
+ }