@everystack/cli 0.1.0 → 0.2.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/src/plugin.ts ADDED
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @everystack/cli/plugin — OTA updates handler + CLI actions as a composable plugin.
3
+ *
4
+ * Registers the updates manifest/asset endpoint for Expo OTA updates,
5
+ * plus CLI actions for registering builds and managing channels.
6
+ *
7
+ * Note: Types are inlined to avoid circular dependency with @everystack/server.
8
+ */
9
+
10
+ import type { StorageAdapter } from './storage/index';
11
+
12
+ /** Minimal plugin context (compatible with @everystack/server/plugin PluginContext) */
13
+ interface PluginContext {
14
+ db: any;
15
+ schema: Record<string, any>;
16
+ verifyToken: (token: string) => Promise<Record<string, unknown> | null>;
17
+ environment: string;
18
+ publishJob: (type: string, payload: unknown) => Promise<string>;
19
+ [key: string]: unknown;
20
+ }
21
+
22
+ /** Minimal route type (compatible with @everystack/server Route) */
23
+ interface Route {
24
+ path: string;
25
+ method?: string;
26
+ exact?: boolean;
27
+ handler: (req: Request) => Promise<Response>;
28
+ }
29
+
30
+ /** Action handler type (compatible with @everystack/server/plugin ActionHandler) */
31
+ type ActionHandler = (payload: unknown, ctx: PluginContext) => Promise<unknown>;
32
+
33
+ /** Plugin factory function */
34
+ type Plugin = (ctx: PluginContext) => Promise<{
35
+ routes?: Route[];
36
+ actions?: Record<string, ActionHandler>;
37
+ }>;
38
+
39
+ export interface UpdatesPluginOptions {
40
+ /** Base URL for update asset downloads (e.g., CDN_URL or SITE_URL) */
41
+ baseUrl?: string;
42
+ /** Base path for updates routes (default: '/api/updates') */
43
+ basePath?: string;
44
+ /** Ed25519 private key for signing manifests */
45
+ privateKey?: string;
46
+ /** Default channel name (default: 'production') */
47
+ defaultChannel?: string;
48
+ }
49
+
50
+ interface UpdatesContext extends PluginContext {
51
+ updatesStorage: StorageAdapter;
52
+ clientBundlesStorage?: StorageAdapter;
53
+ }
54
+
55
+ export function updatesPlugin(options: UpdatesPluginOptions = {}): Plugin {
56
+ const basePath = options.basePath ?? '/api/updates';
57
+
58
+ return async (ctx) => {
59
+ const {
60
+ createUpdatesHandler,
61
+ handleRegisterWeb,
62
+ registerMobileRelease,
63
+ handleListChannels,
64
+ handleCreateChannel,
65
+ } = await import('./handler/index');
66
+ const appCtx = ctx as UpdatesContext;
67
+
68
+ const handlerOptions = {
69
+ db: ctx.db,
70
+ storage: appCtx.updatesStorage,
71
+ baseUrl: options.baseUrl ?? process.env.CDN_URL ?? process.env.SITE_URL ?? 'http://localhost:8081',
72
+ basePath,
73
+ auth: { verifyToken: ctx.verifyToken },
74
+ privateKey: options.privateKey ?? process.env.UPDATES_PRIVATE_KEY,
75
+ defaultChannel: options.defaultChannel ?? 'production',
76
+ clientBundlesStorage: appCtx.clientBundlesStorage,
77
+ };
78
+
79
+ const handler = createUpdatesHandler(handlerOptions);
80
+
81
+ return {
82
+ routes: [{ path: basePath, handler }],
83
+ actions: {
84
+ 'register-web': async (payload) => {
85
+ const req = new Request('http://internal/register-web', {
86
+ method: 'POST',
87
+ headers: { 'content-type': 'application/json' },
88
+ body: JSON.stringify(payload),
89
+ });
90
+ const res = await handleRegisterWeb(req, handlerOptions);
91
+ const body = await res.json().catch(() => ({}));
92
+ if (!res.ok) return { error: (body as any).error || `register-web failed (${res.status})` };
93
+ return body;
94
+ },
95
+ 'register-mobile': async (payload) => {
96
+ try {
97
+ return await registerMobileRelease(handlerOptions, payload as any);
98
+ } catch (err: any) {
99
+ return { error: err?.message || 'register-mobile failed' };
100
+ }
101
+ },
102
+ 'channels-list': async () => {
103
+ const req = new Request('http://internal/channels', { method: 'GET' });
104
+ const res = await handleListChannels(req, handlerOptions);
105
+ const body = await res.json().catch(() => ({}));
106
+ if (!res.ok) return { error: (body as any).error || `channels-list failed (${res.status})` };
107
+ return body;
108
+ },
109
+ 'channels-create': async (payload) => {
110
+ const req = new Request('http://internal/channels', {
111
+ method: 'POST',
112
+ headers: { 'content-type': 'application/json' },
113
+ body: JSON.stringify(payload),
114
+ });
115
+ const res = await handleCreateChannel(req, handlerOptions);
116
+ const body = await res.json().catch(() => ({}));
117
+ if (!res.ok) return { error: (body as any).error || `channels-create failed (${res.status})` };
118
+ return body;
119
+ },
120
+ },
121
+ };
122
+ };
123
+ }
@@ -14,12 +14,12 @@ export type StorageOptions =
14
14
  | { type: 'filesystem'; directory: string }
15
15
  | { type: 's3'; bucket: string; region?: string; endpoint?: string };
16
16
 
17
- export function createStorage(options: StorageOptions): StorageAdapter {
17
+ export async function createStorage(options: StorageOptions): Promise<StorageAdapter> {
18
18
  if (options.type === 'filesystem') {
19
- const { FilesystemStorageAdapter } = require('./filesystem') as typeof import('./filesystem');
19
+ const { FilesystemStorageAdapter } = await import('./filesystem');
20
20
  return new FilesystemStorageAdapter(options.directory);
21
21
  }
22
- const { S3StorageAdapter } = require('./s3') as typeof import('./s3');
22
+ const { S3StorageAdapter } = await import('./s3');
23
23
  return new S3StorageAdapter(options.bucket, options.region, options.endpoint);
24
24
  }
25
25