@ollie-shop/cli 1.2.2 → 1.4.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 +9 -5
- package/.turbo/turbo-build.log +3 -3
- package/CHANGELOG.md +57 -0
- package/CONTEXT.md +11 -3
- package/README.md +7 -10
- package/dist/index.js +323 -76
- package/package.json +4 -5
- package/src/cli.tsx +8 -1
- package/src/commands/function-cmd.ts +42 -10
- package/src/commands/help.tsx +12 -1
- package/src/commands/start.tsx +60 -44
- package/src/commands/store-cmd.ts +14 -4
- package/src/commands/whoami.ts +70 -9
- package/src/core/function.ts +1 -1
- package/src/core/schema.ts +8 -3
- package/src/core/store.ts +50 -3
- package/src/utils/auth.ts +4 -0
- package/src/utils/esbuild.ts +99 -15
- package/src/utils/parse-args.ts +2 -0
- package/src/utils/supabase.ts +66 -11
- package/tsup.config.ts +7 -0
package/src/utils/esbuild.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { watch } from "node:fs";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
2
3
|
import http from "node:http";
|
|
3
4
|
import path from "node:path";
|
|
@@ -221,32 +222,95 @@ export interface ServeResult {
|
|
|
221
222
|
/**
|
|
222
223
|
* Starts the esbuild serve + watch mode with a proxy server.
|
|
223
224
|
* The proxy handles /bundle/:componentName requests and forwards others to esbuild.
|
|
224
|
-
*
|
|
225
|
+
* Owns the esbuild context lifecycle and recreates it when a component folder is
|
|
226
|
+
* added or removed. Returns the server info, a manual rebuild, and a way to stop it.
|
|
225
227
|
*/
|
|
226
228
|
export async function startDevServer(
|
|
227
|
-
ctx: esbuild.BuildContext,
|
|
228
229
|
options: {
|
|
229
230
|
port?: number;
|
|
230
231
|
host?: string;
|
|
231
232
|
cwd?: string;
|
|
233
|
+
stage?: string;
|
|
232
234
|
onRequest?: (args: esbuild.ServeOnRequestArgs) => void;
|
|
233
|
-
|
|
235
|
+
onBuildEnd?: (components: ComponentInfo[], result: BuildResult) => void;
|
|
234
236
|
} = {},
|
|
235
|
-
): Promise<
|
|
236
|
-
|
|
237
|
+
): Promise<
|
|
238
|
+
ServeResult & { rebuild: () => Promise<void>; stop: () => Promise<void> }
|
|
239
|
+
> {
|
|
240
|
+
const {
|
|
241
|
+
port = 4000,
|
|
242
|
+
host = "localhost",
|
|
243
|
+
cwd = process.cwd(),
|
|
244
|
+
stage,
|
|
245
|
+
onRequest,
|
|
246
|
+
onBuildEnd,
|
|
247
|
+
} = options;
|
|
237
248
|
|
|
238
249
|
const servedir = path.join(cwd, "node_modules/.ollie", "build");
|
|
250
|
+
const componentsDir = path.join(cwd, "components");
|
|
251
|
+
const internalPort = port + 1;
|
|
239
252
|
|
|
240
|
-
|
|
241
|
-
|
|
253
|
+
let ctx: esbuild.BuildContext | null = null;
|
|
254
|
+
let entryNames = new Set<string>();
|
|
242
255
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
servedir,
|
|
249
|
-
|
|
256
|
+
async function buildAndServe(components: ComponentInfo[]): Promise<void> {
|
|
257
|
+
entryNames = new Set(components.map((c) => c.name));
|
|
258
|
+
ctx = await createBuildContext(components, { cwd, stage, onBuildEnd });
|
|
259
|
+
await ctx.rebuild();
|
|
260
|
+
await ctx.watch();
|
|
261
|
+
await ctx.serve({ port: internalPort, host, servedir, onRequest });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function recreate(): Promise<void> {
|
|
265
|
+
const components = await discoverComponents({ cwd, stage });
|
|
266
|
+
const oldCtx = ctx;
|
|
267
|
+
ctx = null;
|
|
268
|
+
if (oldCtx) await oldCtx.dispose();
|
|
269
|
+
await buildAndServe(components);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await buildAndServe(await discoverComponents({ cwd, stage }));
|
|
273
|
+
|
|
274
|
+
async function currentComponentNames(): Promise<Set<string>> {
|
|
275
|
+
try {
|
|
276
|
+
const entries = await glob("*/index.tsx", { cwd: componentsDir });
|
|
277
|
+
return new Set(entries.map((e) => path.dirname(e)));
|
|
278
|
+
} catch {
|
|
279
|
+
return new Set();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Recreate the context when the set of component folders changes
|
|
284
|
+
let watchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
285
|
+
let recreating = false;
|
|
286
|
+
let pending = false;
|
|
287
|
+
|
|
288
|
+
async function maybeRecreate(): Promise<void> {
|
|
289
|
+
if (recreating) {
|
|
290
|
+
pending = true;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
recreating = true;
|
|
294
|
+
try {
|
|
295
|
+
do {
|
|
296
|
+
pending = false;
|
|
297
|
+
const names = await currentComponentNames();
|
|
298
|
+
const changed =
|
|
299
|
+
names.size !== entryNames.size ||
|
|
300
|
+
[...names].some((n) => !entryNames.has(n));
|
|
301
|
+
if (!changed) break;
|
|
302
|
+
await recreate();
|
|
303
|
+
} while (pending);
|
|
304
|
+
} catch (err) {
|
|
305
|
+
console.error("[DevServer] Failed to reload components:", err);
|
|
306
|
+
} finally {
|
|
307
|
+
recreating = false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const componentsWatcher = watch(componentsDir, { recursive: true }, () => {
|
|
312
|
+
if (watchTimer) clearTimeout(watchTimer);
|
|
313
|
+
watchTimer = setTimeout(maybeRecreate, 150);
|
|
250
314
|
});
|
|
251
315
|
|
|
252
316
|
// Create proxy server that handles /bundle/* and forwards to esbuild
|
|
@@ -356,6 +420,21 @@ export async function startDevServer(
|
|
|
356
420
|
// File doesn't exist, start fresh
|
|
357
421
|
}
|
|
358
422
|
|
|
423
|
+
if (
|
|
424
|
+
typeof updates.id === "string" &&
|
|
425
|
+
typeof existingMeta.id === "string" &&
|
|
426
|
+
existingMeta.id !== updates.id
|
|
427
|
+
) {
|
|
428
|
+
res.statusCode = 409;
|
|
429
|
+
res.setHeader("Content-Type", "application/json");
|
|
430
|
+
res.end(
|
|
431
|
+
JSON.stringify({
|
|
432
|
+
error: `id mismatch: ${componentName} is linked to ${existingMeta.id}`,
|
|
433
|
+
}),
|
|
434
|
+
);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
359
438
|
// Merge updates
|
|
360
439
|
const newMeta = { ...existingMeta, ...updates };
|
|
361
440
|
|
|
@@ -435,9 +514,14 @@ export async function startDevServer(
|
|
|
435
514
|
return {
|
|
436
515
|
host,
|
|
437
516
|
port,
|
|
517
|
+
rebuild: async () => {
|
|
518
|
+
await ctx?.rebuild();
|
|
519
|
+
},
|
|
438
520
|
stop: async () => {
|
|
521
|
+
if (watchTimer) clearTimeout(watchTimer);
|
|
522
|
+
componentsWatcher.close();
|
|
439
523
|
proxyServer.close();
|
|
440
|
-
await ctx
|
|
524
|
+
await ctx?.dispose();
|
|
441
525
|
},
|
|
442
526
|
};
|
|
443
527
|
}
|
package/src/utils/parse-args.ts
CHANGED
|
@@ -3,6 +3,7 @@ export interface GlobalFlags {
|
|
|
3
3
|
dryRun: boolean;
|
|
4
4
|
fields?: string[];
|
|
5
5
|
data?: string;
|
|
6
|
+
stage?: string;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export interface ParsedArgs {
|
|
@@ -63,6 +64,7 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
63
64
|
.map((f) => f.trim())
|
|
64
65
|
: undefined,
|
|
65
66
|
data: (flags.data as string) || (flags.d as string) || undefined,
|
|
67
|
+
stage: (flags.stage as string) || (flags.s as string) || undefined,
|
|
66
68
|
};
|
|
67
69
|
|
|
68
70
|
return { command, subcommand, flags, global, positional };
|
package/src/utils/supabase.ts
CHANGED
|
@@ -6,15 +6,49 @@ declare const __OLLIE_SUPABASE_URL__: string;
|
|
|
6
6
|
declare const __OLLIE_SUPABASE_ANON_KEY__: string;
|
|
7
7
|
declare const __OLLIE_BUILDER_URL__: string;
|
|
8
8
|
|
|
9
|
+
// Resolution order: runtime env var > CI build-time inline > prod default.
|
|
10
|
+
// Baked-in defaults ensure `ollieshop` works zero-config when CI inject is
|
|
11
|
+
// missing or stale. All three values are public (anon Supabase key, public
|
|
12
|
+
// API URLs) so inlining them carries no secret-leak risk.
|
|
13
|
+
const PROD_DEFAULTS = {
|
|
14
|
+
supabaseUrl: "https://aazahtmqrhjqsyqoqdkm.supabase.co",
|
|
15
|
+
supabaseAnonKey:
|
|
16
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFhemFodG1xcmhqcXN5cW9xZGttIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDg2MzEwOTcsImV4cCI6MjA2NDIwNzA5N30.VuAbAyjDe0HcL09SZtQ-UmP1o7Z6qwGuOtvfFhnyAcM",
|
|
17
|
+
builderUrl: "https://api.ollie.shop/builder/v1",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function envOrDefault(
|
|
21
|
+
envValue: string | undefined,
|
|
22
|
+
buildTimeValue: string,
|
|
23
|
+
prodDefault: string,
|
|
24
|
+
): string {
|
|
25
|
+
return envValue || buildTimeValue || prodDefault;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Collapses a Supabase to-one embed (object, array, or null) to a single record.
|
|
29
|
+
export function unwrapToOne<T = Record<string, unknown>>(
|
|
30
|
+
embed: unknown,
|
|
31
|
+
): T | null {
|
|
32
|
+
const record = Array.isArray(embed) ? embed[0] : embed;
|
|
33
|
+
return record && typeof record === "object" ? (record as T) : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
9
36
|
export async function getAuthenticatedClient(): Promise<SupabaseClient> {
|
|
10
37
|
const credentials = await getCredentials();
|
|
11
38
|
if (!credentials) {
|
|
12
39
|
throw new Error("Not authenticated. Run `ollieshop login` first.");
|
|
13
40
|
}
|
|
14
41
|
|
|
15
|
-
const supabaseUrl =
|
|
16
|
-
|
|
17
|
-
|
|
42
|
+
const supabaseUrl = envOrDefault(
|
|
43
|
+
process.env.OLLIE_SUPABASE_URL,
|
|
44
|
+
__OLLIE_SUPABASE_URL__,
|
|
45
|
+
PROD_DEFAULTS.supabaseUrl,
|
|
46
|
+
);
|
|
47
|
+
const supabaseAnonKey = envOrDefault(
|
|
48
|
+
process.env.OLLIE_SUPABASE_ANON_KEY,
|
|
49
|
+
__OLLIE_SUPABASE_ANON_KEY__,
|
|
50
|
+
PROD_DEFAULTS.supabaseAnonKey,
|
|
51
|
+
);
|
|
18
52
|
|
|
19
53
|
const client = createClient(supabaseUrl, supabaseAnonKey, {
|
|
20
54
|
auth: {
|
|
@@ -32,7 +66,11 @@ export async function getAuthenticatedClient(): Promise<SupabaseClient> {
|
|
|
32
66
|
}
|
|
33
67
|
|
|
34
68
|
export function getBuilderUrl(): string {
|
|
35
|
-
return
|
|
69
|
+
return envOrDefault(
|
|
70
|
+
process.env.OLLIE_BUILDER_URL,
|
|
71
|
+
__OLLIE_BUILDER_URL__,
|
|
72
|
+
PROD_DEFAULTS.builderUrl,
|
|
73
|
+
);
|
|
36
74
|
}
|
|
37
75
|
|
|
38
76
|
export async function getAuthToken(): Promise<string> {
|
|
@@ -45,23 +83,40 @@ export async function getAuthToken(): Promise<string> {
|
|
|
45
83
|
|
|
46
84
|
export async function getOrganizationId(
|
|
47
85
|
client: SupabaseClient,
|
|
86
|
+
preferredOrgId?: string,
|
|
48
87
|
): Promise<string> {
|
|
49
|
-
const { data:
|
|
88
|
+
const { data: orgs, error } = await client
|
|
50
89
|
.from("organizations")
|
|
51
|
-
.select("id")
|
|
52
|
-
.order("created_at", { ascending: true })
|
|
53
|
-
.limit(1)
|
|
54
|
-
.maybeSingle();
|
|
90
|
+
.select("id, name")
|
|
91
|
+
.order("created_at", { ascending: true });
|
|
55
92
|
|
|
56
93
|
if (error) {
|
|
57
94
|
throw new Error(`Failed to get organization: ${error.message}`);
|
|
58
95
|
}
|
|
59
96
|
|
|
60
|
-
if (!
|
|
97
|
+
if (!orgs || orgs.length === 0) {
|
|
61
98
|
throw new Error(
|
|
62
99
|
"No organization found for your account. Create one at https://admin.ollie.shop",
|
|
63
100
|
);
|
|
64
101
|
}
|
|
65
102
|
|
|
66
|
-
|
|
103
|
+
if (preferredOrgId) {
|
|
104
|
+
const match = orgs.find((o) => o.id === preferredOrgId);
|
|
105
|
+
if (!match) {
|
|
106
|
+
const list = orgs.map((o) => ` - ${o.id} ${o.name}`).join("\n");
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Organization ${preferredOrgId} not found or you are not a member. Orgs you can access:\n${list}`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return match.id;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (orgs.length > 1) {
|
|
115
|
+
const list = orgs.map((o) => ` - ${o.id} ${o.name}`).join("\n");
|
|
116
|
+
throw new Error(
|
|
117
|
+
`You belong to multiple organizations. Pass --org <id> to select one:\n${list}`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return orgs[0].id;
|
|
67
122
|
}
|
package/tsup.config.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { defineConfig } from "tsup";
|
|
2
3
|
|
|
3
4
|
// CI injects SUPABASE_URL/SUPABASE_ANON_KEY/BUILDER_URL from GH Actions vars;
|
|
4
5
|
// locally they're empty and the CLI falls back to runtime process.env.
|
|
5
6
|
const env = (k: string) => JSON.stringify(process.env[k] ?? "");
|
|
6
7
|
|
|
8
|
+
// Inline the package version at build time so `-v`/`--version` report the real version.
|
|
9
|
+
const pkg = JSON.parse(
|
|
10
|
+
readFileSync(new URL("./package.json", import.meta.url), "utf8"),
|
|
11
|
+
);
|
|
12
|
+
|
|
7
13
|
export default defineConfig({
|
|
8
14
|
entry: ["src/index.tsx"],
|
|
9
15
|
format: ["esm"],
|
|
@@ -16,5 +22,6 @@ export default defineConfig({
|
|
|
16
22
|
__OLLIE_SUPABASE_URL__: env("SUPABASE_URL"),
|
|
17
23
|
__OLLIE_SUPABASE_ANON_KEY__: env("SUPABASE_ANON_KEY"),
|
|
18
24
|
__OLLIE_BUILDER_URL__: env("BUILDER_URL"),
|
|
25
|
+
__OLLIE_CLI_VERSION__: JSON.stringify(pkg.version),
|
|
19
26
|
},
|
|
20
27
|
});
|