@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.
- package/.env.example +3 -0
- package/.turbo/turbo-build.log +3 -3
- package/CHANGELOG.md +23 -0
- package/CONTEXT.md +84 -0
- package/README.md +199 -0
- package/dist/index.js +886 -48
- package/package.json +4 -2
- package/src/commands/component-cmd.ts +107 -0
- package/src/commands/help.tsx +90 -17
- package/src/commands/init-cmd.ts +53 -0
- package/src/commands/schema-cmd.ts +34 -0
- package/src/commands/store-cmd.ts +105 -0
- package/src/commands/version-cmd.ts +100 -0
- package/src/commands/whoami.ts +28 -0
- package/src/core/component.ts +76 -0
- package/src/core/schema.ts +136 -0
- package/src/core/store.ts +76 -0
- package/src/core/version.ts +67 -0
- package/src/index.tsx +34 -4
- package/src/utils/output.ts +125 -0
- package/src/utils/parse-args.ts +90 -0
- package/src/utils/supabase.ts +59 -0
- package/src/utils/validate.ts +58 -0
|
@@ -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
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
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
|
-
|
|
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
|
+
}
|