@ollie-shop/cli 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,136 @@
1
+ import { z } from "zod";
2
+ import { zodToJsonSchema } from "zod-to-json-schema";
3
+
4
+ const PLATFORM_VENDORS = ["vtex", "shopify", "vnda", "custom"] as const;
5
+
6
+ export const storeCreateSchema = z.object({
7
+ name: z.string().min(1).describe("Store display name"),
8
+ platform: z.enum(PLATFORM_VENDORS).describe("E-commerce platform vendor"),
9
+ platformStoreId: z
10
+ .string()
11
+ .min(1)
12
+ .describe("Platform-specific store identifier (e.g. account name)"),
13
+ logo: z.string().url().optional().describe("Store logo URL"),
14
+ settings: z.string().optional().describe("JSON settings string"),
15
+ });
16
+
17
+ export const storeListSchema = z.object({}).describe("No parameters required");
18
+
19
+ export const versionCreateSchema = z.object({
20
+ storeId: z.string().uuid().describe("Parent store UUID"),
21
+ name: z.string().min(1).describe("Version name (e.g. v1, main)"),
22
+ active: z.boolean().default(false).describe("Whether version is active"),
23
+ default: z
24
+ .boolean()
25
+ .default(false)
26
+ .describe("Whether this is the default version"),
27
+ template: z
28
+ .string()
29
+ .nullable()
30
+ .default(null)
31
+ .describe("Template name or null"),
32
+ });
33
+
34
+ export const versionListSchema = z.object({
35
+ storeId: z.string().uuid().describe("Store UUID to list versions for"),
36
+ });
37
+
38
+ export const componentCreateSchema = z.object({
39
+ versionId: z.string().uuid().describe("Parent version UUID"),
40
+ name: z.string().min(1).describe("Component name (e.g. FreeShippingBar)"),
41
+ slot: z
42
+ .string()
43
+ .min(1)
44
+ .describe(
45
+ "Target slot (e.g. cart_header_full_page, shipping_address_details_form)",
46
+ ),
47
+ active: z.boolean().default(true).describe("Whether component is active"),
48
+ props: z
49
+ .record(z.unknown())
50
+ .nullable()
51
+ .default(null)
52
+ .describe("Default component props as JSON object"),
53
+ });
54
+
55
+ export const componentListSchema = z.object({
56
+ storeId: z.string().uuid().describe("Store UUID"),
57
+ versionId: z
58
+ .string()
59
+ .uuid()
60
+ .optional()
61
+ .describe("Optional version UUID to filter by"),
62
+ });
63
+
64
+ export const functionCreateSchema = z.object({
65
+ versionId: z.string().uuid().describe("Parent version UUID"),
66
+ name: z.string().min(1).describe("Function name"),
67
+ trigger: z
68
+ .string()
69
+ .min(1)
70
+ .describe("Function trigger (e.g. beforePayment, afterShipping)"),
71
+ active: z.boolean().default(true).describe("Whether function is active"),
72
+ });
73
+
74
+ export const functionListSchema = z.object({
75
+ versionId: z.string().uuid().describe("Version UUID to list functions for"),
76
+ });
77
+
78
+ type SchemaMap = Record<string, z.ZodTypeAny>;
79
+
80
+ const schemas: Record<string, SchemaMap> = {
81
+ store: {
82
+ create: storeCreateSchema,
83
+ list: storeListSchema,
84
+ },
85
+ version: {
86
+ create: versionCreateSchema,
87
+ list: versionListSchema,
88
+ },
89
+ component: {
90
+ create: componentCreateSchema,
91
+ list: componentListSchema,
92
+ },
93
+ function: {
94
+ create: functionCreateSchema,
95
+ list: functionListSchema,
96
+ },
97
+ };
98
+
99
+ export function getSchemaNames(): string[] {
100
+ const names: string[] = [];
101
+ for (const [resource, actions] of Object.entries(schemas)) {
102
+ names.push(resource);
103
+ for (const action of Object.keys(actions)) {
104
+ names.push(`${resource}.${action}`);
105
+ }
106
+ }
107
+ return names;
108
+ }
109
+
110
+ export function getJsonSchema(name: string): Record<string, unknown> | null {
111
+ const parts = name.split(".");
112
+ const resource = parts[0];
113
+ const action = parts[1];
114
+
115
+ if (!resource || !schemas[resource]) return null;
116
+
117
+ if (action) {
118
+ const schema = schemas[resource][action];
119
+ if (!schema) return null;
120
+ return zodToJsonSchema(schema, { name: `${resource}.${action}` }) as Record<
121
+ string,
122
+ unknown
123
+ >;
124
+ }
125
+
126
+ // Return all actions for a resource
127
+ const result: Record<string, unknown> = {};
128
+ for (const [actionName, schema] of Object.entries(schemas[resource])) {
129
+ result[actionName] = zodToJsonSchema(schema, {
130
+ name: `${resource}.${actionName}`,
131
+ });
132
+ }
133
+ return result;
134
+ }
135
+
136
+ export { PLATFORM_VENDORS };
@@ -0,0 +1,76 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+ import { getOrganizationId } from "../utils/supabase.js";
3
+ import { storeCreateSchema } from "./schema.js";
4
+
5
+ export interface CreateStoreInput {
6
+ name: string;
7
+ platform: string;
8
+ platformStoreId: string;
9
+ logo?: string;
10
+ settings?: string;
11
+ }
12
+
13
+ export interface StoreRecord {
14
+ id: string;
15
+ name: string;
16
+ platform: string;
17
+ platform_store_id: string;
18
+ organization_id: string;
19
+ logo: string | null;
20
+ settings: string | null;
21
+ created_at: string;
22
+ }
23
+
24
+ export async function createStore(
25
+ client: SupabaseClient,
26
+ input: CreateStoreInput,
27
+ ): Promise<{ data?: { id: string }; error?: { message: string } }> {
28
+ // Validate input against schema
29
+ const parsed = storeCreateSchema.safeParse(input);
30
+ if (!parsed.success) {
31
+ return {
32
+ error: { message: parsed.error.issues.map((i) => i.message).join("; ") },
33
+ };
34
+ }
35
+
36
+ const organizationId = await getOrganizationId(client);
37
+
38
+ const { data, error } = await client
39
+ .from("stores")
40
+ .insert({
41
+ name: parsed.data.name,
42
+ platform: parsed.data.platform,
43
+ platform_store_id: parsed.data.platformStoreId,
44
+ organization_id: organizationId,
45
+ logo: parsed.data.logo ?? null,
46
+ settings: parsed.data.settings ?? null,
47
+ })
48
+ .select("id")
49
+ .single();
50
+
51
+ if (error) {
52
+ return { error: { message: error.message } };
53
+ }
54
+
55
+ return { data: { id: data.id } };
56
+ }
57
+
58
+ export async function listStores(
59
+ client: SupabaseClient,
60
+ ): Promise<{ data?: StoreRecord[]; error?: { message: string } }> {
61
+ const organizationId = await getOrganizationId(client);
62
+
63
+ const { data, error } = await client
64
+ .from("stores")
65
+ .select(
66
+ "id, name, platform, platform_store_id, organization_id, logo, settings, created_at",
67
+ )
68
+ .eq("organization_id", organizationId)
69
+ .order("created_at", { ascending: false });
70
+
71
+ if (error) {
72
+ return { error: { message: error.message } };
73
+ }
74
+
75
+ return { data: data as StoreRecord[] };
76
+ }
@@ -0,0 +1,67 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+ import { versionCreateSchema } from "./schema.js";
3
+
4
+ export interface CreateVersionInput {
5
+ storeId: string;
6
+ name: string;
7
+ active?: boolean;
8
+ default?: boolean;
9
+ template?: string | null;
10
+ }
11
+
12
+ export interface VersionRecord {
13
+ id: string;
14
+ name: string;
15
+ active: boolean;
16
+ default: boolean;
17
+ store_id: string;
18
+ template: string | null;
19
+ created_at: string;
20
+ }
21
+
22
+ export async function createVersion(
23
+ client: SupabaseClient,
24
+ input: CreateVersionInput,
25
+ ): Promise<{ data?: { id: string }; error?: { message: string } }> {
26
+ const parsed = versionCreateSchema.safeParse(input);
27
+ if (!parsed.success) {
28
+ return {
29
+ error: { message: parsed.error.issues.map((i) => i.message).join("; ") },
30
+ };
31
+ }
32
+
33
+ const { data, error } = await client
34
+ .from("versions")
35
+ .insert({
36
+ name: parsed.data.name,
37
+ active: parsed.data.active,
38
+ default: parsed.data.default,
39
+ store_id: parsed.data.storeId,
40
+ template: parsed.data.template,
41
+ })
42
+ .select("id")
43
+ .single();
44
+
45
+ if (error) {
46
+ return { error: { message: error.message } };
47
+ }
48
+
49
+ return { data: { id: data.id } };
50
+ }
51
+
52
+ export async function listVersions(
53
+ client: SupabaseClient,
54
+ storeId: string,
55
+ ): Promise<{ data?: VersionRecord[]; error?: { message: string } }> {
56
+ const { data, error } = await client
57
+ .from("versions")
58
+ .select("id, name, active, default, store_id, template, created_at")
59
+ .eq("store_id", storeId)
60
+ .order("created_at", { ascending: false });
61
+
62
+ if (error) {
63
+ return { error: { message: error.message } };
64
+ }
65
+
66
+ return { data: data as VersionRecord[] };
67
+ }
package/src/index.tsx CHANGED
@@ -1,8 +1,38 @@
1
1
  import { render } from "ink";
2
2
  import { App } from "./cli.js";
3
+ import { componentCommand } from "./commands/component-cmd.js";
4
+ import { initCommand } from "./commands/init-cmd.js";
5
+ import { schemaCommand } from "./commands/schema-cmd.js";
6
+ import { storeCommand } from "./commands/store-cmd.js";
7
+ import { versionCommand } from "./commands/version-cmd.js";
8
+ import { whoamiCommand } from "./commands/whoami.js";
9
+ import { parseArgs } from "./utils/parse-args.js";
3
10
 
4
- const args = process.argv.slice(2);
5
- const command = args[0] || "help";
6
- const commandArgs = args.slice(1);
11
+ // Agent-first commands: plain async, no Ink, structured output
12
+ const AGENT_COMMANDS: Record<
13
+ string,
14
+ (parsed: ReturnType<typeof parseArgs>) => Promise<void>
15
+ > = {
16
+ whoami: whoamiCommand,
17
+ store: storeCommand,
18
+ version: versionCommand,
19
+ component: componentCommand,
20
+ schema: schemaCommand,
21
+ init: initCommand,
22
+ };
7
23
 
8
- render(<App command={command} args={commandArgs} />);
24
+ const parsed = parseArgs(process.argv);
25
+
26
+ if (parsed.command in AGENT_COMMANDS) {
27
+ AGENT_COMMANDS[parsed.command](parsed).catch((err: unknown) => {
28
+ const msg = err instanceof Error ? err.message : String(err);
29
+ process.stderr.write(`${JSON.stringify({ error: { message: msg } })}\n`);
30
+ process.exit(1);
31
+ });
32
+ } else {
33
+ // Interactive commands: Ink UI (login, start, help, version)
34
+ const args = process.argv.slice(2);
35
+ const command = args[0] || "help";
36
+ const commandArgs = args.slice(1);
37
+ render(<App command={command} args={commandArgs} />);
38
+ }
@@ -0,0 +1,125 @@
1
+ export type OutputFormat = "json" | "pretty";
2
+
3
+ export function detectOutputFormat(cliOutput?: string): OutputFormat {
4
+ if (cliOutput === "json" || cliOutput === "j") return "json";
5
+ if (cliOutput === "pretty") return "pretty";
6
+ // Auto-detect: JSON when piped, pretty when TTY
7
+ return process.stdout.isTTY ? "pretty" : "json";
8
+ }
9
+
10
+ export function filterFields(
11
+ data: Record<string, unknown>,
12
+ fields?: string[],
13
+ ): Record<string, unknown> {
14
+ if (!fields || fields.length === 0) return data;
15
+ const result: Record<string, unknown> = {};
16
+ for (const field of fields) {
17
+ if (field in data) {
18
+ result[field] = data[field];
19
+ }
20
+ }
21
+ return result;
22
+ }
23
+
24
+ export function outputResult(
25
+ result: { data?: unknown; error?: unknown },
26
+ format: OutputFormat,
27
+ fields?: string[],
28
+ ): void {
29
+ if (format === "json") {
30
+ let output = result;
31
+ if (fields && result.data && typeof result.data === "object") {
32
+ if (Array.isArray(result.data)) {
33
+ output = {
34
+ ...result,
35
+ data: result.data.map((item) =>
36
+ filterFields(item as Record<string, unknown>, fields),
37
+ ),
38
+ };
39
+ } else {
40
+ output = {
41
+ ...result,
42
+ data: filterFields(result.data as Record<string, unknown>, fields),
43
+ };
44
+ }
45
+ }
46
+ process.stdout.write(`${JSON.stringify(output)}\n`);
47
+ return;
48
+ }
49
+
50
+ // Pretty output
51
+ if (result.error) {
52
+ const errMsg =
53
+ typeof result.error === "object" && result.error !== null
54
+ ? (result.error as { message?: string }).message ||
55
+ JSON.stringify(result.error)
56
+ : String(result.error);
57
+ console.error(`\x1b[31mError:\x1b[0m ${errMsg}`);
58
+ return;
59
+ }
60
+
61
+ if (Array.isArray(result.data)) {
62
+ printTable(result.data as Record<string, unknown>[], fields);
63
+ } else if (typeof result.data === "object" && result.data !== null) {
64
+ const filtered = fields
65
+ ? filterFields(result.data as Record<string, unknown>, fields)
66
+ : result.data;
67
+ for (const [key, value] of Object.entries(
68
+ filtered as Record<string, unknown>,
69
+ )) {
70
+ console.log(`\x1b[1m${key}:\x1b[0m ${value}`);
71
+ }
72
+ } else {
73
+ console.log(result.data);
74
+ }
75
+ }
76
+
77
+ function printTable(rows: Record<string, unknown>[], fields?: string[]): void {
78
+ if (rows.length === 0) {
79
+ console.log("(no results)");
80
+ return;
81
+ }
82
+
83
+ const keys = fields || Object.keys(rows[0]);
84
+ const widths: Record<string, number> = {};
85
+ for (const key of keys) {
86
+ widths[key] = key.length;
87
+ for (const row of rows) {
88
+ const val = String(row[key] ?? "");
89
+ widths[key] = Math.max(widths[key], val.length);
90
+ }
91
+ widths[key] = Math.min(widths[key], 40); // cap column width
92
+ }
93
+
94
+ // Header
95
+ const header = keys.map((k) => k.padEnd(widths[k])).join(" ");
96
+ console.log(`\x1b[1m${header}\x1b[0m`);
97
+ console.log(keys.map((k) => "─".repeat(widths[k])).join("──"));
98
+
99
+ // Rows
100
+ for (const row of rows) {
101
+ const line = keys
102
+ .map((k) =>
103
+ String(row[k] ?? "")
104
+ .padEnd(widths[k])
105
+ .slice(0, widths[k]),
106
+ )
107
+ .join(" ");
108
+ console.log(line);
109
+ }
110
+ }
111
+
112
+ export function outputDryRun(
113
+ action: string,
114
+ data: Record<string, unknown>,
115
+ format: OutputFormat,
116
+ ): void {
117
+ if (format === "json") {
118
+ process.stdout.write(`${JSON.stringify({ dryRun: true, action, data })}\n`);
119
+ } else {
120
+ console.log(`\x1b[33m[dry-run]\x1b[0m Would execute: ${action}`);
121
+ for (const [key, value] of Object.entries(data)) {
122
+ console.log(` ${key}: ${JSON.stringify(value)}`);
123
+ }
124
+ }
125
+ }
@@ -0,0 +1,90 @@
1
+ export interface GlobalFlags {
2
+ output?: string;
3
+ dryRun: boolean;
4
+ fields?: string[];
5
+ data?: string;
6
+ }
7
+
8
+ export interface ParsedArgs {
9
+ command: string;
10
+ subcommand?: string;
11
+ flags: Record<string, string | boolean>;
12
+ global: GlobalFlags;
13
+ positional: string[];
14
+ }
15
+
16
+ export function parseArgs(argv: string[]): ParsedArgs {
17
+ const args = argv.slice(2);
18
+ const command = args[0] || "help";
19
+ const flags: Record<string, string | boolean> = {};
20
+ const positional: string[] = [];
21
+
22
+ let subcommand: string | undefined;
23
+
24
+ // Determine if second arg is a subcommand (not a flag)
25
+ if (args[1] && !args[1].startsWith("-")) {
26
+ subcommand = args[1];
27
+ }
28
+
29
+ const startIdx = subcommand ? 2 : 1;
30
+
31
+ for (let i = startIdx; i < args.length; i++) {
32
+ const arg = args[i];
33
+ if (arg.startsWith("--")) {
34
+ const key = arg.slice(2);
35
+ const next = args[i + 1];
36
+ if (next && !next.startsWith("-")) {
37
+ flags[key] = next;
38
+ i++;
39
+ } else {
40
+ flags[key] = true;
41
+ }
42
+ } else if (arg.startsWith("-") && arg.length === 2) {
43
+ const key = arg.slice(1);
44
+ const next = args[i + 1];
45
+ if (next && !next.startsWith("-")) {
46
+ flags[key] = next;
47
+ i++;
48
+ } else {
49
+ flags[key] = true;
50
+ }
51
+ } else {
52
+ positional.push(arg);
53
+ }
54
+ }
55
+
56
+ // Extract global flags
57
+ const global: GlobalFlags = {
58
+ output: (flags.output as string) || (flags.o as string) || undefined,
59
+ dryRun: flags["dry-run"] === true,
60
+ fields: flags.fields
61
+ ? String(flags.fields)
62
+ .split(",")
63
+ .map((f) => f.trim())
64
+ : undefined,
65
+ data: (flags.data as string) || (flags.d as string) || undefined,
66
+ };
67
+
68
+ return { command, subcommand, flags, global, positional };
69
+ }
70
+
71
+ export function getFlag(
72
+ flags: Record<string, string | boolean>,
73
+ ...names: string[]
74
+ ): string | undefined {
75
+ for (const name of names) {
76
+ const val = flags[name];
77
+ if (typeof val === "string") return val;
78
+ }
79
+ return undefined;
80
+ }
81
+
82
+ export function getBoolFlag(
83
+ flags: Record<string, string | boolean>,
84
+ ...names: string[]
85
+ ): boolean {
86
+ for (const name of names) {
87
+ if (flags[name] === true) return true;
88
+ }
89
+ return false;
90
+ }
@@ -0,0 +1,59 @@
1
+ import { type SupabaseClient, createClient } from "@supabase/supabase-js";
2
+ import { getCredentials } from "./auth.js";
3
+
4
+ function requireEnv(name: string): string {
5
+ const value = process.env[name];
6
+ if (!value) {
7
+ throw new Error(
8
+ `Missing required environment variable: ${name}. Set it in your .env file or shell.`,
9
+ );
10
+ }
11
+ return value;
12
+ }
13
+
14
+ export async function getAuthenticatedClient(): Promise<SupabaseClient> {
15
+ const credentials = await getCredentials();
16
+ if (!credentials) {
17
+ throw new Error("Not authenticated. Run `ollieshop login` first.");
18
+ }
19
+
20
+ const supabaseUrl = requireEnv("OLLIE_SUPABASE_URL");
21
+ const supabaseAnonKey = requireEnv("OLLIE_SUPABASE_ANON_KEY");
22
+
23
+ const client = createClient(supabaseUrl, supabaseAnonKey, {
24
+ auth: {
25
+ autoRefreshToken: false,
26
+ persistSession: false,
27
+ },
28
+ });
29
+
30
+ await client.auth.setSession({
31
+ access_token: credentials.accessToken,
32
+ refresh_token: credentials.refreshToken,
33
+ });
34
+
35
+ return client;
36
+ }
37
+
38
+ export async function getOrganizationId(
39
+ client: SupabaseClient,
40
+ ): Promise<string> {
41
+ const { data: org, error } = await client
42
+ .from("organizations")
43
+ .select("id")
44
+ .order("created_at", { ascending: true })
45
+ .limit(1)
46
+ .maybeSingle();
47
+
48
+ if (error) {
49
+ throw new Error(`Failed to get organization: ${error.message}`);
50
+ }
51
+
52
+ if (!org) {
53
+ throw new Error(
54
+ "No organization found for your account. Create one at https://admin.ollie.shop",
55
+ );
56
+ }
57
+
58
+ return org.id;
59
+ }
@@ -0,0 +1,58 @@
1
+ const UUID_REGEX =
2
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3
+
4
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional — rejecting control chars in user input
5
+ const CONTROL_CHAR_REGEX = /[\x00-\x1f\x7f]/;
6
+
7
+ const RESOURCE_ID_INVALID_CHARS = /[?#%\/\\]/;
8
+
9
+ export function validateUuid(value: string, name: string): string {
10
+ if (!UUID_REGEX.test(value)) {
11
+ throw new Error(
12
+ `Invalid ${name}: "${value}". Must be a valid UUID (e.g. 7217542a-d7c6-40d3-a20e-db13b310a781).`,
13
+ );
14
+ }
15
+ return value;
16
+ }
17
+
18
+ export function rejectControlChars(value: string, name: string): string {
19
+ if (CONTROL_CHAR_REGEX.test(value)) {
20
+ throw new Error(
21
+ `Invalid ${name}: contains control characters. Input must be printable text.`,
22
+ );
23
+ }
24
+ return value;
25
+ }
26
+
27
+ export function validateResourceName(value: string, name: string): string {
28
+ rejectControlChars(value, name);
29
+ if (RESOURCE_ID_INVALID_CHARS.test(value)) {
30
+ throw new Error(
31
+ `Invalid ${name}: "${value}". Must not contain ?, #, %, /, or \\.`,
32
+ );
33
+ }
34
+ return value;
35
+ }
36
+
37
+ export function validateEnum<T extends string>(
38
+ value: string,
39
+ allowed: readonly T[],
40
+ name: string,
41
+ ): T {
42
+ if (!allowed.includes(value as T)) {
43
+ throw new Error(
44
+ `Invalid ${name}: "${value}". Must be one of: ${allowed.join(", ")}`,
45
+ );
46
+ }
47
+ return value as T;
48
+ }
49
+
50
+ export function validateRequired(
51
+ value: string | undefined | null,
52
+ name: string,
53
+ ): string {
54
+ if (!value || value.trim() === "") {
55
+ throw new Error(`Missing required parameter: --${name}`);
56
+ }
57
+ return rejectControlChars(value.trim(), name);
58
+ }