@ollie-shop/cli 1.0.2 → 1.2.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.
@@ -0,0 +1,68 @@
1
+ import {
2
+ fetchBuildStatus,
3
+ isTerminalStatus,
4
+ pollBuildStatus,
5
+ } from "../core/deploy.js";
6
+ import { detectOutputFormat, outputResult } from "../utils/output.js";
7
+ import { type ParsedArgs, getBoolFlag, getFlag } from "../utils/parse-args.js";
8
+ import { validateRequired } from "../utils/validate.js";
9
+
10
+ export async function statusCommand(parsed: ParsedArgs): Promise<void> {
11
+ const format = detectOutputFormat(parsed.global.output);
12
+
13
+ try {
14
+ let buildId: string;
15
+ let wait: boolean;
16
+ let timeout: number;
17
+
18
+ if (parsed.global.data) {
19
+ const raw = JSON.parse(parsed.global.data);
20
+ buildId = validateRequired(raw.buildId, "buildId");
21
+ wait = raw.wait ?? false;
22
+ timeout = raw.timeout ?? 300;
23
+ } else {
24
+ buildId = validateRequired(getFlag(parsed.flags, "build-id"), "build-id");
25
+ wait = getBoolFlag(parsed.flags, "wait");
26
+ timeout = Number(getFlag(parsed.flags, "timeout") ?? "300");
27
+ }
28
+
29
+ if (wait) {
30
+ const isJson = format === "json";
31
+ if (!isJson) {
32
+ process.stderr.write(`Polling build ${buildId}...\n`);
33
+ }
34
+
35
+ const result = await pollBuildStatus(buildId, {
36
+ timeoutMs: timeout * 1000,
37
+ onPoll: (build) => {
38
+ if (!isJson) {
39
+ process.stderr.write(` status: ${build.status}\n`);
40
+ }
41
+ },
42
+ });
43
+
44
+ outputResult(result, format, parsed.global.fields);
45
+ if (!result.success) process.exit(1);
46
+ if (
47
+ result.success &&
48
+ isTerminalStatus(result.data.status) &&
49
+ result.data.status !== "SUCCEEDED"
50
+ ) {
51
+ process.exit(1);
52
+ }
53
+ } else {
54
+ const result = await fetchBuildStatus(buildId);
55
+ outputResult(result, format, parsed.global.fields);
56
+ if (!result.success) process.exit(1);
57
+ }
58
+ } catch (err) {
59
+ outputResult(
60
+ {
61
+ success: false,
62
+ error: { message: err instanceof Error ? err.message : String(err) },
63
+ },
64
+ format,
65
+ );
66
+ process.exit(1);
67
+ }
68
+ }
@@ -0,0 +1,105 @@
1
+ import { PLATFORM_VENDORS } from "../core/schema.js";
2
+ import { createStore, listStores } from "../core/store.js";
3
+ import {
4
+ detectOutputFormat,
5
+ outputDryRun,
6
+ outputResult,
7
+ } from "../utils/output.js";
8
+ import { type ParsedArgs, getFlag } from "../utils/parse-args.js";
9
+ import { getAuthenticatedClient } from "../utils/supabase.js";
10
+ import { validateEnum, validateRequired } from "../utils/validate.js";
11
+
12
+ export async function storeCommand(parsed: ParsedArgs): Promise<void> {
13
+ const sub = parsed.subcommand;
14
+ if (sub === "create") return storeCreateCommand(parsed);
15
+ if (sub === "list" || sub === "ls") return storeListCommand(parsed);
16
+
17
+ console.error(
18
+ `Unknown store subcommand: ${sub}. Use: store create | store list`,
19
+ );
20
+ process.exit(1);
21
+ }
22
+
23
+ async function storeCreateCommand(parsed: ParsedArgs): Promise<void> {
24
+ const format = detectOutputFormat(parsed.global.output);
25
+
26
+ try {
27
+ let input: {
28
+ name: string;
29
+ platform: string;
30
+ platformStoreId: string;
31
+ logo?: string;
32
+ settings?: string;
33
+ };
34
+
35
+ if (parsed.global.data) {
36
+ // Raw JSON mode
37
+ const raw = JSON.parse(parsed.global.data);
38
+ input = {
39
+ name: validateRequired(raw.name, "name"),
40
+ platform: validateEnum(raw.platform, PLATFORM_VENDORS, "platform"),
41
+ platformStoreId: validateRequired(
42
+ raw.platformStoreId,
43
+ "platformStoreId",
44
+ ),
45
+ logo: raw.logo,
46
+ settings: raw.settings,
47
+ };
48
+ } else {
49
+ // Flag mode
50
+ input = {
51
+ name: validateRequired(getFlag(parsed.flags, "name", "n"), "name"),
52
+ platform: validateEnum(
53
+ validateRequired(getFlag(parsed.flags, "platform", "p"), "platform"),
54
+ PLATFORM_VENDORS,
55
+ "platform",
56
+ ),
57
+ platformStoreId: validateRequired(
58
+ getFlag(parsed.flags, "platform-store-id"),
59
+ "platform-store-id",
60
+ ),
61
+ logo: getFlag(parsed.flags, "logo"),
62
+ settings: getFlag(parsed.flags, "settings"),
63
+ };
64
+ }
65
+
66
+ if (parsed.global.dryRun) {
67
+ outputDryRun("store.create", input, format);
68
+ return;
69
+ }
70
+
71
+ const client = await getAuthenticatedClient();
72
+ const result = await createStore(client, input);
73
+
74
+ outputResult(result, format, parsed.global.fields);
75
+ if (result.error) process.exit(1);
76
+ } catch (err) {
77
+ outputResult(
78
+ {
79
+ error: { message: err instanceof Error ? err.message : String(err) },
80
+ },
81
+ format,
82
+ );
83
+ process.exit(1);
84
+ }
85
+ }
86
+
87
+ async function storeListCommand(parsed: ParsedArgs): Promise<void> {
88
+ const format = detectOutputFormat(parsed.global.output);
89
+
90
+ try {
91
+ const client = await getAuthenticatedClient();
92
+ const result = await listStores(client);
93
+
94
+ outputResult(result, format, parsed.global.fields);
95
+ if (result.error) process.exit(1);
96
+ } catch (err) {
97
+ outputResult(
98
+ {
99
+ error: { message: err instanceof Error ? err.message : String(err) },
100
+ },
101
+ format,
102
+ );
103
+ process.exit(1);
104
+ }
105
+ }
@@ -0,0 +1,100 @@
1
+ import { createVersion, listVersions } from "../core/version.js";
2
+ import {
3
+ detectOutputFormat,
4
+ outputDryRun,
5
+ outputResult,
6
+ } from "../utils/output.js";
7
+ import { type ParsedArgs, getBoolFlag, getFlag } from "../utils/parse-args.js";
8
+ import { getAuthenticatedClient } from "../utils/supabase.js";
9
+ import { validateRequired, validateUuid } from "../utils/validate.js";
10
+
11
+ export async function versionCommand(parsed: ParsedArgs): Promise<void> {
12
+ const sub = parsed.subcommand;
13
+ if (sub === "create") return versionCreateCommand(parsed);
14
+ if (sub === "list" || sub === "ls") return versionListCommand(parsed);
15
+
16
+ console.error(
17
+ `Unknown version subcommand: ${sub}. Use: version create | version list`,
18
+ );
19
+ process.exit(1);
20
+ }
21
+
22
+ async function versionCreateCommand(parsed: ParsedArgs): Promise<void> {
23
+ const format = detectOutputFormat(parsed.global.output);
24
+
25
+ try {
26
+ let input: {
27
+ storeId: string;
28
+ name: string;
29
+ active?: boolean;
30
+ default?: boolean;
31
+ template?: string | null;
32
+ };
33
+
34
+ if (parsed.global.data) {
35
+ const raw = JSON.parse(parsed.global.data);
36
+ input = {
37
+ storeId: validateUuid(raw.storeId, "storeId"),
38
+ name: validateRequired(raw.name, "name"),
39
+ active: raw.active ?? false,
40
+ default: raw.default ?? false,
41
+ template: raw.template ?? null,
42
+ };
43
+ } else {
44
+ input = {
45
+ storeId: validateUuid(
46
+ validateRequired(getFlag(parsed.flags, "store-id"), "store-id"),
47
+ "store-id",
48
+ ),
49
+ name: validateRequired(getFlag(parsed.flags, "name", "n"), "name"),
50
+ active: getBoolFlag(parsed.flags, "active"),
51
+ default: getBoolFlag(parsed.flags, "default"),
52
+ template: getFlag(parsed.flags, "template") ?? null,
53
+ };
54
+ }
55
+
56
+ if (parsed.global.dryRun) {
57
+ outputDryRun("version.create", input as Record<string, unknown>, format);
58
+ return;
59
+ }
60
+
61
+ const client = await getAuthenticatedClient();
62
+ const result = await createVersion(client, input);
63
+
64
+ outputResult(result, format, parsed.global.fields);
65
+ if (result.error) process.exit(1);
66
+ } catch (err) {
67
+ outputResult(
68
+ {
69
+ error: { message: err instanceof Error ? err.message : String(err) },
70
+ },
71
+ format,
72
+ );
73
+ process.exit(1);
74
+ }
75
+ }
76
+
77
+ async function versionListCommand(parsed: ParsedArgs): Promise<void> {
78
+ const format = detectOutputFormat(parsed.global.output);
79
+
80
+ try {
81
+ const storeId = validateUuid(
82
+ validateRequired(getFlag(parsed.flags, "store-id"), "store-id"),
83
+ "store-id",
84
+ );
85
+
86
+ const client = await getAuthenticatedClient();
87
+ const result = await listVersions(client, storeId);
88
+
89
+ outputResult(result, format, parsed.global.fields);
90
+ if (result.error) process.exit(1);
91
+ } catch (err) {
92
+ outputResult(
93
+ {
94
+ error: { message: err instanceof Error ? err.message : String(err) },
95
+ },
96
+ format,
97
+ );
98
+ process.exit(1);
99
+ }
100
+ }
@@ -0,0 +1,28 @@
1
+ import { getCurrentUser } from "../utils/auth.js";
2
+ import { detectOutputFormat, outputResult } from "../utils/output.js";
3
+ import type { ParsedArgs } from "../utils/parse-args.js";
4
+
5
+ export async function whoamiCommand(parsed: ParsedArgs): Promise<void> {
6
+ const format = detectOutputFormat(parsed.global.output);
7
+
8
+ const user = await getCurrentUser();
9
+ if (!user) {
10
+ outputResult(
11
+ {
12
+ error: { message: "Not logged in. Run `ollieshop login`." },
13
+ },
14
+ format,
15
+ );
16
+ process.exit(1);
17
+ }
18
+
19
+ outputResult(
20
+ {
21
+ data: {
22
+ email: user.email,
23
+ },
24
+ },
25
+ format,
26
+ parsed.global.fields,
27
+ );
28
+ }
@@ -0,0 +1,128 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+
3
+ export interface BusinessRuleRecord {
4
+ id: string;
5
+ store_id: string;
6
+ title: string;
7
+ content: string;
8
+ previous_content: string | null;
9
+ versions_ids: string[] | null;
10
+ components_ids: string[] | null;
11
+ functions_ids: string[] | null;
12
+ status: "draft" | "active" | "deprecated";
13
+ code_updated: boolean;
14
+ created_at: string;
15
+ updated_at: string;
16
+ }
17
+
18
+ export interface ListBusinessRulesFilters {
19
+ store_id?: string;
20
+ version_id?: string;
21
+ code_updated?: boolean;
22
+ }
23
+
24
+ export interface UpdateBusinessRuleInput {
25
+ content: string;
26
+ versions_ids?: string[] | null;
27
+ components_ids?: string[] | null;
28
+ functions_ids?: string[] | null;
29
+ }
30
+
31
+ const SELECT_FIELDS =
32
+ "id, store_id, title, content, previous_content, versions_ids, components_ids, functions_ids, status, code_updated, created_at, updated_at";
33
+
34
+ export async function listBusinessRules(
35
+ client: SupabaseClient,
36
+ filters: ListBusinessRulesFilters = {},
37
+ ): Promise<{ data?: BusinessRuleRecord[]; error?: { message: string } }> {
38
+ let query = client
39
+ .from("business_rules")
40
+ .select(SELECT_FIELDS)
41
+ .order("created_at", { ascending: false });
42
+
43
+ if (filters.store_id !== undefined) {
44
+ query = query.eq("store_id", filters.store_id);
45
+ }
46
+ if (filters.version_id !== undefined) {
47
+ // versions_ids is a JSON array — use "contains" to find rules linked to this version
48
+ query = query.filter(
49
+ "versions_ids",
50
+ "cs",
51
+ JSON.stringify([filters.version_id]),
52
+ );
53
+ }
54
+ if (filters.code_updated !== undefined) {
55
+ query = query.eq("code_updated", filters.code_updated);
56
+ }
57
+
58
+ const { data, error } = await query;
59
+
60
+ if (error) {
61
+ return { error: { message: error.message } };
62
+ }
63
+
64
+ return { data: data as BusinessRuleRecord[] };
65
+ }
66
+
67
+ export async function getBusinessRule(
68
+ client: SupabaseClient,
69
+ id: string,
70
+ ): Promise<{ data?: BusinessRuleRecord; error?: { message: string } }> {
71
+ const { data, error } = await client
72
+ .from("business_rules")
73
+ .select(SELECT_FIELDS)
74
+ .eq("id", id)
75
+ .single();
76
+
77
+ if (error) {
78
+ return { error: { message: error.message } };
79
+ }
80
+
81
+ return { data: data as BusinessRuleRecord };
82
+ }
83
+
84
+ export async function updateBusinessRule(
85
+ client: SupabaseClient,
86
+ id: string,
87
+ input: UpdateBusinessRuleInput,
88
+ ): Promise<{ data?: { id: string }; error?: { message: string } }> {
89
+ // Fetch current content to preserve it as previous_content
90
+ const { data: current, error: fetchError } = await client
91
+ .from("business_rules")
92
+ .select("content")
93
+ .eq("id", id)
94
+ .single();
95
+
96
+ if (fetchError) {
97
+ return { error: { message: fetchError.message } };
98
+ }
99
+
100
+ const updatePayload: Record<string, unknown> = {
101
+ previous_content: current.content,
102
+ content: input.content,
103
+ code_updated: false,
104
+ };
105
+
106
+ if (input.versions_ids !== undefined) {
107
+ updatePayload.versions_ids = input.versions_ids;
108
+ }
109
+ if (input.components_ids !== undefined) {
110
+ updatePayload.components_ids = input.components_ids;
111
+ }
112
+ if (input.functions_ids !== undefined) {
113
+ updatePayload.functions_ids = input.functions_ids;
114
+ }
115
+
116
+ const { data, error } = await client
117
+ .from("business_rules")
118
+ .update(updatePayload)
119
+ .eq("id", id)
120
+ .select("id")
121
+ .single();
122
+
123
+ if (error) {
124
+ return { error: { message: error.message } };
125
+ }
126
+
127
+ return { data: { id: data.id } };
128
+ }
@@ -0,0 +1,76 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+ import { componentCreateSchema } from "./schema.js";
3
+
4
+ export interface CreateComponentInput {
5
+ versionId: string;
6
+ name: string;
7
+ slot: string;
8
+ active?: boolean;
9
+ props?: Record<string, unknown> | null;
10
+ }
11
+
12
+ export interface ComponentRecord {
13
+ id: string;
14
+ name: string;
15
+ slot: string | null;
16
+ active: boolean;
17
+ version_id: string;
18
+ props: Record<string, unknown> | null;
19
+ created_at: string;
20
+ versions?: { id: string; name: string };
21
+ }
22
+
23
+ export async function createComponent(
24
+ client: SupabaseClient,
25
+ input: CreateComponentInput,
26
+ ): Promise<{ data?: { id: string }; error?: { message: string } }> {
27
+ const parsed = componentCreateSchema.safeParse(input);
28
+ if (!parsed.success) {
29
+ return {
30
+ error: { message: parsed.error.issues.map((i) => i.message).join("; ") },
31
+ };
32
+ }
33
+
34
+ const { data, error } = await client
35
+ .from("components")
36
+ .insert({
37
+ name: parsed.data.name,
38
+ slot: parsed.data.slot,
39
+ active: parsed.data.active,
40
+ version_id: parsed.data.versionId,
41
+ props: parsed.data.props,
42
+ })
43
+ .select("id")
44
+ .single();
45
+
46
+ if (error) {
47
+ return { error: { message: error.message } };
48
+ }
49
+
50
+ return { data: { id: data.id } };
51
+ }
52
+
53
+ export async function listComponents(
54
+ client: SupabaseClient,
55
+ storeId: string,
56
+ versionId?: string,
57
+ ): Promise<{ data?: ComponentRecord[]; error?: { message: string } }> {
58
+ let query = client
59
+ .from("components")
60
+ .select(
61
+ "id, name, slot, active, version_id, props, created_at, versions!inner(id, name)",
62
+ )
63
+ .eq("versions.store_id", storeId);
64
+
65
+ if (versionId) {
66
+ query = query.eq("versions.id", versionId);
67
+ }
68
+
69
+ const { data, error } = await query;
70
+
71
+ if (error) {
72
+ return { error: { message: error.message } };
73
+ }
74
+
75
+ return { data: (data ?? []) as unknown as ComponentRecord[] };
76
+ }
@@ -0,0 +1,171 @@
1
+ import { getAuthToken, getBuilderUrl } from "../utils/supabase.js";
2
+
3
+ export type ResourceType = "component" | "function";
4
+
5
+ export interface BuildResult {
6
+ id: string;
7
+ startTime: string;
8
+ endTime?: string | null;
9
+ status: string;
10
+ resource: {
11
+ id: string;
12
+ type: ResourceType;
13
+ };
14
+ }
15
+
16
+ export interface UploadBuildInput {
17
+ resourceId: string;
18
+ type: ResourceType;
19
+ zipBuffer: Buffer;
20
+ }
21
+
22
+ interface BuilderResponse {
23
+ success: boolean;
24
+ data?: BuildResult;
25
+ error?: { message?: string; type?: string };
26
+ }
27
+
28
+ export async function uploadBuild(
29
+ input: UploadBuildInput,
30
+ ): Promise<
31
+ | { success: true; data: BuildResult }
32
+ | { success: false; error: { message: string } }
33
+ > {
34
+ const builderUrl = getBuilderUrl();
35
+ const token = await getAuthToken();
36
+
37
+ const formData = new FormData();
38
+ formData.append(
39
+ "code",
40
+ new Blob([input.zipBuffer], { type: "application/zip" }),
41
+ "code.zip",
42
+ );
43
+ formData.append("type", input.type);
44
+ formData.append("resource_id", input.resourceId);
45
+
46
+ let response: Response;
47
+ try {
48
+ response = await fetch(`${builderUrl}/`, {
49
+ method: "POST",
50
+ headers: {
51
+ Authorization: `Bearer ${token}`,
52
+ },
53
+ body: formData,
54
+ });
55
+ } catch (err) {
56
+ return {
57
+ success: false,
58
+ error: {
59
+ message: `Cannot reach builder at ${builderUrl}: ${err instanceof Error ? err.message : String(err)}`,
60
+ },
61
+ };
62
+ }
63
+
64
+ const body = (await response.json()) as BuilderResponse;
65
+
66
+ if (!response.ok || !body.success) {
67
+ const msg =
68
+ body.error?.message ||
69
+ body.error?.type ||
70
+ `Builder returned ${response.status}`;
71
+ return { success: false, error: { message: msg } };
72
+ }
73
+
74
+ return { success: true, data: body.data as BuildResult };
75
+ }
76
+
77
+ export async function fetchBuildStatus(
78
+ buildId: string,
79
+ ): Promise<
80
+ | { success: true; data: BuildResult }
81
+ | { success: false; error: { message: string } }
82
+ > {
83
+ const builderUrl = getBuilderUrl();
84
+ const token = await getAuthToken();
85
+
86
+ let response: Response;
87
+ try {
88
+ response = await fetch(`${builderUrl}/${encodeURIComponent(buildId)}`, {
89
+ method: "GET",
90
+ headers: {
91
+ Authorization: `Bearer ${token}`,
92
+ },
93
+ });
94
+ } catch (err) {
95
+ return {
96
+ success: false,
97
+ error: {
98
+ message: `Cannot reach builder at ${builderUrl}: ${err instanceof Error ? err.message : String(err)}`,
99
+ },
100
+ };
101
+ }
102
+
103
+ const body = (await response.json()) as BuilderResponse;
104
+
105
+ if (!response.ok || !body.success) {
106
+ const msg =
107
+ body.error?.message ||
108
+ body.error?.type ||
109
+ `Builder returned ${response.status}`;
110
+ return { success: false, error: { message: msg } };
111
+ }
112
+
113
+ return { success: true, data: body.data as BuildResult };
114
+ }
115
+
116
+ const TERMINAL_STATUSES = new Set([
117
+ "SUCCEEDED",
118
+ "FAILED",
119
+ "STOPPED",
120
+ "TIMED_OUT",
121
+ "FAULT",
122
+ ]);
123
+
124
+ export function isTerminalStatus(status: string): boolean {
125
+ return TERMINAL_STATUSES.has(status);
126
+ }
127
+
128
+ export interface PollOptions {
129
+ intervalMs?: number;
130
+ timeoutMs?: number;
131
+ onPoll?: (result: BuildResult) => void;
132
+ }
133
+
134
+ export async function pollBuildStatus(
135
+ buildId: string,
136
+ options: PollOptions = {},
137
+ ): Promise<
138
+ | { success: true; data: BuildResult }
139
+ | { success: false; error: { message: string } }
140
+ > {
141
+ const { intervalMs = 5000, timeoutMs = 300_000, onPoll } = options;
142
+ const deadline = Date.now() + timeoutMs;
143
+
144
+ while (Date.now() < deadline) {
145
+ const result = await fetchBuildStatus(buildId);
146
+
147
+ if (!result.success) {
148
+ return result;
149
+ }
150
+
151
+ if (onPoll) {
152
+ onPoll(result.data);
153
+ }
154
+
155
+ if (isTerminalStatus(result.data.status)) {
156
+ return result;
157
+ }
158
+
159
+ const remaining = deadline - Date.now();
160
+ if (remaining <= 0) break;
161
+
162
+ await new Promise((resolve) =>
163
+ setTimeout(resolve, Math.min(intervalMs, remaining)),
164
+ );
165
+ }
166
+
167
+ return {
168
+ success: false,
169
+ error: { message: `Build ${buildId} timed out after ${timeoutMs / 1000}s` },
170
+ };
171
+ }