@tiledev/tile-push-cli 0.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/dist/index.cjs ADDED
@@ -0,0 +1,134 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_apiClient = require("./apiClient-DiRjggqw.cjs");
3
+ require("./bin/tile-push.cjs");
4
+ let node_fs = require("node:fs");
5
+ let node_fs_promises = require("node:fs/promises");
6
+ let node_path = require("node:path");
7
+ let node_stream_promises = require("node:stream/promises");
8
+ let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
9
+ //#region src/plugins/storage.ts
10
+ const detectContentType = (filePath) => {
11
+ const lower = filePath.toLowerCase();
12
+ if (lower.endsWith(".zip")) return "application/zip";
13
+ if (lower.endsWith(".tar.gz") || lower.endsWith(".tgz")) return "application/gzip";
14
+ if (lower.endsWith(".tar.br")) return "application/x-brotli";
15
+ if (lower.endsWith(".json")) return "application/json";
16
+ if (lower.endsWith(".png")) return "image/png";
17
+ if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
18
+ if (lower.endsWith(".webp")) return "image/webp";
19
+ if (lower.endsWith(".svg")) return "image/svg+xml";
20
+ return "application/octet-stream";
21
+ };
22
+ /**
23
+ * For PUT to GCS via signed URL, GCS expects the request body to be the raw
24
+ * file bytes with Content-Length set. We use a buffered Buffer (rather than
25
+ * a stream) because:
26
+ * 1. node fetch streaming requires `duplex: "half"` which is gated behind
27
+ * experimental flags in some Node versions
28
+ * 2. Bundle files are typically <50MB — easily fits in memory
29
+ * 3. Content-Length is required by GCS signed URLs and easier from a Buffer
30
+ */
31
+ const putToSignedUrl = async (uploadUrl, filePath, headers) => {
32
+ const { readFile } = await import("node:fs/promises");
33
+ const data = await readFile(filePath);
34
+ const response = await fetch(uploadUrl, {
35
+ method: "PUT",
36
+ headers: {
37
+ ...headers,
38
+ "Content-Length": String(data.byteLength)
39
+ },
40
+ body: data
41
+ });
42
+ if (!response.ok) {
43
+ const text = await response.text().catch(() => "");
44
+ throw new Error(`Upload to GCS failed (HTTP ${response.status}): ${text.slice(0, 300)}`);
45
+ }
46
+ };
47
+ const tilePushStorage = (0, _hot_updater_plugin_core.createNodeStoragePlugin)({
48
+ name: "tilePushStorage",
49
+ supportedProtocol: "gs",
50
+ factory: (_config) => {
51
+ let cachedClient = null;
52
+ const getClient = () => cachedClient ??= require_apiClient.TilePushClient.create();
53
+ return {
54
+ async upload(key, filePath) {
55
+ const client = await getClient();
56
+ const filename = (0, node_path.basename)(filePath);
57
+ const composedKey = key ? `${key}/${filename}` : filename;
58
+ const { uploadUrl, storageUri, requiredHeaders } = await client.post("/upload-url", { json: {
59
+ key: composedKey,
60
+ contentType: detectContentType(filePath)
61
+ } });
62
+ await putToSignedUrl(uploadUrl, filePath, requiredHeaders);
63
+ return { storageUri };
64
+ },
65
+ async exists(storageUri) {
66
+ const client = await getClient();
67
+ try {
68
+ return (await client.get(`/storage/exists?uri=${encodeURIComponent(storageUri)}`)).exists;
69
+ } catch (err) {
70
+ return false;
71
+ }
72
+ },
73
+ async delete(storageUri) {
74
+ await (await getClient()).delete(`/storage?uri=${encodeURIComponent(storageUri)}`);
75
+ },
76
+ async downloadFile(storageUri, filePath) {
77
+ const { downloadUrl } = await (await getClient()).get(`/storage/download-url?uri=${encodeURIComponent(storageUri)}`);
78
+ const response = await fetch(downloadUrl);
79
+ if (!response.ok || !response.body) throw new Error(`Failed to download ${storageUri} (HTTP ${response.status})`);
80
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(filePath), { recursive: true });
81
+ const writeStream = (0, node_fs.createWriteStream)(filePath);
82
+ const { Readable } = await import("node:stream");
83
+ await (0, node_stream_promises.pipeline)(Readable.fromWeb(response.body), writeStream);
84
+ }
85
+ };
86
+ }
87
+ });
88
+ //#endregion
89
+ //#region src/plugins/database.ts
90
+ const tilePushDatabase = (0, _hot_updater_plugin_core.createDatabasePlugin)({
91
+ name: "tilePushDatabase",
92
+ factory: (_config) => {
93
+ let cachedClient = null;
94
+ const getClient = () => cachedClient ??= require_apiClient.TilePushClient.create();
95
+ return {
96
+ supportsCursorPagination: false,
97
+ async getBundleById(bundleId) {
98
+ const client = await getClient();
99
+ try {
100
+ return await client.get(`/bundles/${encodeURIComponent(bundleId)}`);
101
+ } catch (err) {
102
+ if (typeof err === "object" && err !== null && "status" in err && err.status === 404) return null;
103
+ throw err;
104
+ }
105
+ },
106
+ async getBundles(options) {
107
+ const client = await getClient();
108
+ const params = new URLSearchParams();
109
+ params.set("limit", String(options.limit));
110
+ if (options.where?.channel) params.set("channel", options.where.channel);
111
+ if (options.where?.platform) params.set("platform", options.where.platform);
112
+ if (options.cursor?.after) params.set("after", options.cursor.after);
113
+ return client.get(`/bundles?${params}`);
114
+ },
115
+ async getChannels() {
116
+ const { channels } = await (await getClient()).get("/channels");
117
+ return channels;
118
+ },
119
+ async commitBundle({ changedSets }) {
120
+ if (changedSets.length === 0) return;
121
+ await (await getClient()).post("/bundles", { json: { changedSets } });
122
+ }
123
+ };
124
+ }
125
+ });
126
+ //#endregion
127
+ exports.TilePushApiError = require_apiClient.TilePushApiError;
128
+ exports.TilePushClient = require_apiClient.TilePushClient;
129
+ exports.credentialsDiagnostic = require_apiClient.credentialsDiagnostic;
130
+ exports.loadCredentials = require_apiClient.loadCredentials;
131
+ exports.requireCredentials = require_apiClient.requireCredentials;
132
+ exports.saveCredentials = require_apiClient.saveCredentials;
133
+ exports.tilePushDatabase = tilePushDatabase;
134
+ exports.tilePushStorage = tilePushStorage;
@@ -0,0 +1,153 @@
1
+ import * as _$_hot_updater_plugin_core0 from "@hot-updater/plugin-core";
2
+
3
+ //#region src/plugins/storage.d.ts
4
+ /**
5
+ * Tile Push storage plugin.
6
+ *
7
+ * Implements the hot-updater NodeStoragePlugin interface as a thin HTTP
8
+ * client against our Cloud Functions:
9
+ *
10
+ * upload(key, filePath)
11
+ * 1. POST /upload-url → get a signed GCS PUT URL
12
+ * 2. Stream the file to that URL directly (bytes never touch our Cloud
13
+ * Function, only metadata does)
14
+ * 3. Return storageUri so the deploy can write it into the bundle row
15
+ *
16
+ * exists(storageUri) — HEAD via signed read URL
17
+ * delete(storageUri) — DELETE proxied through server
18
+ * downloadFile(uri, path) — stream a signed read URL to disk
19
+ *
20
+ * supportedProtocol is "gs" because our server returns gs:// URIs (the same
21
+ * scheme the firebase plugin's runtime side already understands when
22
+ * generating CDN URLs during update-check).
23
+ */
24
+ interface TilePushStorageConfig {
25
+ /**
26
+ * Tenant id. Falls back to the credentials store / env vars if omitted.
27
+ * Strongly recommended to pass explicitly so deploys don't accidentally
28
+ * write to the wrong tenant when multiple are configured locally.
29
+ */
30
+ appId?: string;
31
+ /** API base URL override. Defaults to the credentials store / env value. */
32
+ apiUrl?: string;
33
+ }
34
+ declare const tilePushStorage: (config: TilePushStorageConfig, hooks?: _$_hot_updater_plugin_core0.StoragePluginHooks) => () => _$_hot_updater_plugin_core0.NodeStoragePlugin;
35
+ type TilePushStoragePlugin = ReturnType<ReturnType<typeof tilePushStorage>>;
36
+ //#endregion
37
+ //#region src/plugins/database.d.ts
38
+ /**
39
+ * Tile Push database plugin.
40
+ *
41
+ * Built on top of `createDatabasePlugin`, which gives us:
42
+ * - automatic buffering of appendBundle / updateBundle / deleteBundle
43
+ * into a per-instance changedMap
44
+ * - one commitBundle({changedSets}) call to our factory, which we ship
45
+ * to the server in a single POST
46
+ *
47
+ * Reads (getBundleById, getBundles, getChannels) pass through directly to
48
+ * the matching server route. Writes batch through commitBundle.
49
+ */
50
+ interface TilePushDatabaseConfig {
51
+ /**
52
+ * Tenant id. Falls back to the credentials store / env vars if omitted.
53
+ * Strongly recommended to pass explicitly so deploys don't accidentally
54
+ * write to the wrong tenant when multiple are configured locally.
55
+ */
56
+ appId?: string;
57
+ /** API base URL override. Defaults to the credentials store / env value. */
58
+ apiUrl?: string;
59
+ }
60
+ declare const tilePushDatabase: (config: TilePushDatabaseConfig, hooks?: _$_hot_updater_plugin_core0.DatabasePluginHooks) => () => _$_hot_updater_plugin_core0.DatabasePlugin<unknown>;
61
+ type TilePushDatabasePlugin = ReturnType<ReturnType<typeof tilePushDatabase>>;
62
+ //#endregion
63
+ //#region src/auth/tokenStore.d.ts
64
+ /**
65
+ * Persistent credential store at ~/.tile-push/credentials.json.
66
+ *
67
+ * {
68
+ * "appId": "tk_acme",
69
+ * "token": "tpd_xxxxxxxxxxxx",
70
+ * "apiUrl": "https://api.tile-push.app" // optional override
71
+ * }
72
+ *
73
+ * Permissions are tightened to 0600 (owner read/write only) on every write.
74
+ * This file is the equivalent of a passwd entry — losing control of it gives
75
+ * an attacker deploy access to the tenant.
76
+ *
77
+ * For env-var overrides (CI machines, scripted use), TILE_PUSH_APP_ID +
78
+ * TILE_PUSH_TOKEN take precedence over whatever's on disk. That way you
79
+ * don't need to write a creds file during CI runs.
80
+ */
81
+ interface TilePushCredentials {
82
+ appId: string;
83
+ token: string;
84
+ apiUrl?: string;
85
+ }
86
+ /**
87
+ * Resolve credentials with env precedence:
88
+ * 1. TILE_PUSH_APP_ID + TILE_PUSH_TOKEN env vars (preferred for CI)
89
+ * 2. ~/.tile-push/credentials.json (interactive / dev machines)
90
+ * 3. null if neither set
91
+ *
92
+ * TILE_PUSH_API_URL overrides the API base URL in either case.
93
+ */
94
+ declare const loadCredentials: () => Promise<TilePushCredentials | null>;
95
+ declare const saveCredentials: (creds: TilePushCredentials) => Promise<void>;
96
+ /**
97
+ * Throws a helpful message if no credentials are configured. Use this at the
98
+ * top of any command that needs server access.
99
+ */
100
+ declare const requireCredentials: () => Promise<TilePushCredentials>;
101
+ /** Test helper — confirms creds file exists and is 0600 (or just env vars). */
102
+ declare const credentialsDiagnostic: () => Promise<{
103
+ source: "env" | "file" | "none";
104
+ pathOrEnv: string;
105
+ modeOk?: boolean;
106
+ }>;
107
+ //#endregion
108
+ //#region src/auth/apiClient.d.ts
109
+ /**
110
+ * Thin fetch wrapper that injects the Bearer token, prepends the tenant
111
+ * prefix to relative paths, parses JSON, and throws typed errors.
112
+ *
113
+ * Usage:
114
+ * const client = await TilePushClient.create();
115
+ * const me = await client.get<{ appId, tenantName, tokenLabel }>("/me");
116
+ *
117
+ * All `pathSuffix` arguments are appended to `/api/cli/t/{appId}/`, so
118
+ * the client never has to think about tenant routing.
119
+ */
120
+ declare class TilePushApiError extends Error {
121
+ readonly status: number;
122
+ readonly body: unknown;
123
+ constructor(message: string, status: number, body: unknown);
124
+ }
125
+ interface RequestOptions {
126
+ /** Override Content-Type. Defaults to application/json for body-bearing methods. */
127
+ contentType?: string;
128
+ /** Custom headers (merged with defaults). */
129
+ headers?: Record<string, string>;
130
+ /** Raw body (Buffer or stream). If unset and `json` is set, json is used. */
131
+ body?: BodyInit;
132
+ /** JSON-encodable body. Auto-stringified. */
133
+ json?: unknown;
134
+ /** Don't parse the response as JSON; return raw Response. */
135
+ raw?: boolean;
136
+ }
137
+ declare class TilePushClient {
138
+ private readonly creds;
139
+ private constructor();
140
+ static create(): Promise<TilePushClient>;
141
+ /** Like create() but returns null instead of throwing if no creds set. */
142
+ static createOptional(): Promise<TilePushClient | null>;
143
+ get appId(): string;
144
+ get apiUrl(): string;
145
+ private buildUrl;
146
+ private request;
147
+ get<T>(pathSuffix: string, options?: Omit<RequestOptions, "json" | "body">): Promise<T>;
148
+ post<T>(pathSuffix: string, options?: RequestOptions): Promise<T>;
149
+ patch<T>(pathSuffix: string, options?: RequestOptions): Promise<T>;
150
+ delete<T>(pathSuffix: string, options?: RequestOptions): Promise<T>;
151
+ }
152
+ //#endregion
153
+ export { TilePushApiError, TilePushClient, type TilePushCredentials, type TilePushDatabaseConfig, type TilePushDatabasePlugin, type TilePushStorageConfig, type TilePushStoragePlugin, credentialsDiagnostic, loadCredentials, requireCredentials, saveCredentials, tilePushDatabase, tilePushStorage };
@@ -0,0 +1,153 @@
1
+ import * as _$_hot_updater_plugin_core0 from "@hot-updater/plugin-core";
2
+
3
+ //#region src/plugins/storage.d.ts
4
+ /**
5
+ * Tile Push storage plugin.
6
+ *
7
+ * Implements the hot-updater NodeStoragePlugin interface as a thin HTTP
8
+ * client against our Cloud Functions:
9
+ *
10
+ * upload(key, filePath)
11
+ * 1. POST /upload-url → get a signed GCS PUT URL
12
+ * 2. Stream the file to that URL directly (bytes never touch our Cloud
13
+ * Function, only metadata does)
14
+ * 3. Return storageUri so the deploy can write it into the bundle row
15
+ *
16
+ * exists(storageUri) — HEAD via signed read URL
17
+ * delete(storageUri) — DELETE proxied through server
18
+ * downloadFile(uri, path) — stream a signed read URL to disk
19
+ *
20
+ * supportedProtocol is "gs" because our server returns gs:// URIs (the same
21
+ * scheme the firebase plugin's runtime side already understands when
22
+ * generating CDN URLs during update-check).
23
+ */
24
+ interface TilePushStorageConfig {
25
+ /**
26
+ * Tenant id. Falls back to the credentials store / env vars if omitted.
27
+ * Strongly recommended to pass explicitly so deploys don't accidentally
28
+ * write to the wrong tenant when multiple are configured locally.
29
+ */
30
+ appId?: string;
31
+ /** API base URL override. Defaults to the credentials store / env value. */
32
+ apiUrl?: string;
33
+ }
34
+ declare const tilePushStorage: (config: TilePushStorageConfig, hooks?: _$_hot_updater_plugin_core0.StoragePluginHooks) => () => _$_hot_updater_plugin_core0.NodeStoragePlugin;
35
+ type TilePushStoragePlugin = ReturnType<ReturnType<typeof tilePushStorage>>;
36
+ //#endregion
37
+ //#region src/plugins/database.d.ts
38
+ /**
39
+ * Tile Push database plugin.
40
+ *
41
+ * Built on top of `createDatabasePlugin`, which gives us:
42
+ * - automatic buffering of appendBundle / updateBundle / deleteBundle
43
+ * into a per-instance changedMap
44
+ * - one commitBundle({changedSets}) call to our factory, which we ship
45
+ * to the server in a single POST
46
+ *
47
+ * Reads (getBundleById, getBundles, getChannels) pass through directly to
48
+ * the matching server route. Writes batch through commitBundle.
49
+ */
50
+ interface TilePushDatabaseConfig {
51
+ /**
52
+ * Tenant id. Falls back to the credentials store / env vars if omitted.
53
+ * Strongly recommended to pass explicitly so deploys don't accidentally
54
+ * write to the wrong tenant when multiple are configured locally.
55
+ */
56
+ appId?: string;
57
+ /** API base URL override. Defaults to the credentials store / env value. */
58
+ apiUrl?: string;
59
+ }
60
+ declare const tilePushDatabase: (config: TilePushDatabaseConfig, hooks?: _$_hot_updater_plugin_core0.DatabasePluginHooks) => () => _$_hot_updater_plugin_core0.DatabasePlugin<unknown>;
61
+ type TilePushDatabasePlugin = ReturnType<ReturnType<typeof tilePushDatabase>>;
62
+ //#endregion
63
+ //#region src/auth/tokenStore.d.ts
64
+ /**
65
+ * Persistent credential store at ~/.tile-push/credentials.json.
66
+ *
67
+ * {
68
+ * "appId": "tk_acme",
69
+ * "token": "tpd_xxxxxxxxxxxx",
70
+ * "apiUrl": "https://api.tile-push.app" // optional override
71
+ * }
72
+ *
73
+ * Permissions are tightened to 0600 (owner read/write only) on every write.
74
+ * This file is the equivalent of a passwd entry — losing control of it gives
75
+ * an attacker deploy access to the tenant.
76
+ *
77
+ * For env-var overrides (CI machines, scripted use), TILE_PUSH_APP_ID +
78
+ * TILE_PUSH_TOKEN take precedence over whatever's on disk. That way you
79
+ * don't need to write a creds file during CI runs.
80
+ */
81
+ interface TilePushCredentials {
82
+ appId: string;
83
+ token: string;
84
+ apiUrl?: string;
85
+ }
86
+ /**
87
+ * Resolve credentials with env precedence:
88
+ * 1. TILE_PUSH_APP_ID + TILE_PUSH_TOKEN env vars (preferred for CI)
89
+ * 2. ~/.tile-push/credentials.json (interactive / dev machines)
90
+ * 3. null if neither set
91
+ *
92
+ * TILE_PUSH_API_URL overrides the API base URL in either case.
93
+ */
94
+ declare const loadCredentials: () => Promise<TilePushCredentials | null>;
95
+ declare const saveCredentials: (creds: TilePushCredentials) => Promise<void>;
96
+ /**
97
+ * Throws a helpful message if no credentials are configured. Use this at the
98
+ * top of any command that needs server access.
99
+ */
100
+ declare const requireCredentials: () => Promise<TilePushCredentials>;
101
+ /** Test helper — confirms creds file exists and is 0600 (or just env vars). */
102
+ declare const credentialsDiagnostic: () => Promise<{
103
+ source: "env" | "file" | "none";
104
+ pathOrEnv: string;
105
+ modeOk?: boolean;
106
+ }>;
107
+ //#endregion
108
+ //#region src/auth/apiClient.d.ts
109
+ /**
110
+ * Thin fetch wrapper that injects the Bearer token, prepends the tenant
111
+ * prefix to relative paths, parses JSON, and throws typed errors.
112
+ *
113
+ * Usage:
114
+ * const client = await TilePushClient.create();
115
+ * const me = await client.get<{ appId, tenantName, tokenLabel }>("/me");
116
+ *
117
+ * All `pathSuffix` arguments are appended to `/api/cli/t/{appId}/`, so
118
+ * the client never has to think about tenant routing.
119
+ */
120
+ declare class TilePushApiError extends Error {
121
+ readonly status: number;
122
+ readonly body: unknown;
123
+ constructor(message: string, status: number, body: unknown);
124
+ }
125
+ interface RequestOptions {
126
+ /** Override Content-Type. Defaults to application/json for body-bearing methods. */
127
+ contentType?: string;
128
+ /** Custom headers (merged with defaults). */
129
+ headers?: Record<string, string>;
130
+ /** Raw body (Buffer or stream). If unset and `json` is set, json is used. */
131
+ body?: BodyInit;
132
+ /** JSON-encodable body. Auto-stringified. */
133
+ json?: unknown;
134
+ /** Don't parse the response as JSON; return raw Response. */
135
+ raw?: boolean;
136
+ }
137
+ declare class TilePushClient {
138
+ private readonly creds;
139
+ private constructor();
140
+ static create(): Promise<TilePushClient>;
141
+ /** Like create() but returns null instead of throwing if no creds set. */
142
+ static createOptional(): Promise<TilePushClient | null>;
143
+ get appId(): string;
144
+ get apiUrl(): string;
145
+ private buildUrl;
146
+ private request;
147
+ get<T>(pathSuffix: string, options?: Omit<RequestOptions, "json" | "body">): Promise<T>;
148
+ post<T>(pathSuffix: string, options?: RequestOptions): Promise<T>;
149
+ patch<T>(pathSuffix: string, options?: RequestOptions): Promise<T>;
150
+ delete<T>(pathSuffix: string, options?: RequestOptions): Promise<T>;
151
+ }
152
+ //#endregion
153
+ export { TilePushApiError, TilePushClient, type TilePushCredentials, type TilePushDatabaseConfig, type TilePushDatabasePlugin, type TilePushStorageConfig, type TilePushStoragePlugin, credentialsDiagnostic, loadCredentials, requireCredentials, saveCredentials, tilePushDatabase, tilePushStorage };
package/dist/index.mjs ADDED
@@ -0,0 +1,125 @@
1
+ import { a as requireCredentials, i as loadCredentials, n as TilePushClient, o as saveCredentials, r as credentialsDiagnostic, t as TilePushApiError } from "./apiClient-Dxz7PXQr.mjs";
2
+ import { createWriteStream } from "node:fs";
3
+ import { mkdir } from "node:fs/promises";
4
+ import { basename, dirname } from "node:path";
5
+ import { pipeline } from "node:stream/promises";
6
+ import { createDatabasePlugin, createNodeStoragePlugin } from "@hot-updater/plugin-core";
7
+ //#region src/plugins/storage.ts
8
+ const detectContentType = (filePath) => {
9
+ const lower = filePath.toLowerCase();
10
+ if (lower.endsWith(".zip")) return "application/zip";
11
+ if (lower.endsWith(".tar.gz") || lower.endsWith(".tgz")) return "application/gzip";
12
+ if (lower.endsWith(".tar.br")) return "application/x-brotli";
13
+ if (lower.endsWith(".json")) return "application/json";
14
+ if (lower.endsWith(".png")) return "image/png";
15
+ if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
16
+ if (lower.endsWith(".webp")) return "image/webp";
17
+ if (lower.endsWith(".svg")) return "image/svg+xml";
18
+ return "application/octet-stream";
19
+ };
20
+ /**
21
+ * For PUT to GCS via signed URL, GCS expects the request body to be the raw
22
+ * file bytes with Content-Length set. We use a buffered Buffer (rather than
23
+ * a stream) because:
24
+ * 1. node fetch streaming requires `duplex: "half"` which is gated behind
25
+ * experimental flags in some Node versions
26
+ * 2. Bundle files are typically <50MB — easily fits in memory
27
+ * 3. Content-Length is required by GCS signed URLs and easier from a Buffer
28
+ */
29
+ const putToSignedUrl = async (uploadUrl, filePath, headers) => {
30
+ const { readFile } = await import("node:fs/promises");
31
+ const data = await readFile(filePath);
32
+ const response = await fetch(uploadUrl, {
33
+ method: "PUT",
34
+ headers: {
35
+ ...headers,
36
+ "Content-Length": String(data.byteLength)
37
+ },
38
+ body: data
39
+ });
40
+ if (!response.ok) {
41
+ const text = await response.text().catch(() => "");
42
+ throw new Error(`Upload to GCS failed (HTTP ${response.status}): ${text.slice(0, 300)}`);
43
+ }
44
+ };
45
+ const tilePushStorage = createNodeStoragePlugin({
46
+ name: "tilePushStorage",
47
+ supportedProtocol: "gs",
48
+ factory: (_config) => {
49
+ let cachedClient = null;
50
+ const getClient = () => cachedClient ??= TilePushClient.create();
51
+ return {
52
+ async upload(key, filePath) {
53
+ const client = await getClient();
54
+ const filename = basename(filePath);
55
+ const composedKey = key ? `${key}/${filename}` : filename;
56
+ const { uploadUrl, storageUri, requiredHeaders } = await client.post("/upload-url", { json: {
57
+ key: composedKey,
58
+ contentType: detectContentType(filePath)
59
+ } });
60
+ await putToSignedUrl(uploadUrl, filePath, requiredHeaders);
61
+ return { storageUri };
62
+ },
63
+ async exists(storageUri) {
64
+ const client = await getClient();
65
+ try {
66
+ return (await client.get(`/storage/exists?uri=${encodeURIComponent(storageUri)}`)).exists;
67
+ } catch (err) {
68
+ return false;
69
+ }
70
+ },
71
+ async delete(storageUri) {
72
+ await (await getClient()).delete(`/storage?uri=${encodeURIComponent(storageUri)}`);
73
+ },
74
+ async downloadFile(storageUri, filePath) {
75
+ const { downloadUrl } = await (await getClient()).get(`/storage/download-url?uri=${encodeURIComponent(storageUri)}`);
76
+ const response = await fetch(downloadUrl);
77
+ if (!response.ok || !response.body) throw new Error(`Failed to download ${storageUri} (HTTP ${response.status})`);
78
+ await mkdir(dirname(filePath), { recursive: true });
79
+ const writeStream = createWriteStream(filePath);
80
+ const { Readable } = await import("node:stream");
81
+ await pipeline(Readable.fromWeb(response.body), writeStream);
82
+ }
83
+ };
84
+ }
85
+ });
86
+ //#endregion
87
+ //#region src/plugins/database.ts
88
+ const tilePushDatabase = createDatabasePlugin({
89
+ name: "tilePushDatabase",
90
+ factory: (_config) => {
91
+ let cachedClient = null;
92
+ const getClient = () => cachedClient ??= TilePushClient.create();
93
+ return {
94
+ supportsCursorPagination: false,
95
+ async getBundleById(bundleId) {
96
+ const client = await getClient();
97
+ try {
98
+ return await client.get(`/bundles/${encodeURIComponent(bundleId)}`);
99
+ } catch (err) {
100
+ if (typeof err === "object" && err !== null && "status" in err && err.status === 404) return null;
101
+ throw err;
102
+ }
103
+ },
104
+ async getBundles(options) {
105
+ const client = await getClient();
106
+ const params = new URLSearchParams();
107
+ params.set("limit", String(options.limit));
108
+ if (options.where?.channel) params.set("channel", options.where.channel);
109
+ if (options.where?.platform) params.set("platform", options.where.platform);
110
+ if (options.cursor?.after) params.set("after", options.cursor.after);
111
+ return client.get(`/bundles?${params}`);
112
+ },
113
+ async getChannels() {
114
+ const { channels } = await (await getClient()).get("/channels");
115
+ return channels;
116
+ },
117
+ async commitBundle({ changedSets }) {
118
+ if (changedSets.length === 0) return;
119
+ await (await getClient()).post("/bundles", { json: { changedSets } });
120
+ }
121
+ };
122
+ }
123
+ });
124
+ //#endregion
125
+ export { TilePushApiError, TilePushClient, credentialsDiagnostic, loadCredentials, requireCredentials, saveCredentials, tilePushDatabase, tilePushStorage };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@tiledev/tile-push-cli",
3
+ "version": "0.1.0",
4
+ "description": "Tile Push CLI — OTA deploys for React Native, built on top of hot-updater.",
5
+ "type": "module",
6
+ "bin": {
7
+ "tile-push": "dist/bin/tile-push.mjs"
8
+ },
9
+ "main": "./dist/index.cjs",
10
+ "module": "./dist/index.mjs",
11
+ "types": "./dist/index.d.cts",
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/index.mjs",
15
+ "require": "./dist/index.cjs"
16
+ },
17
+ "./bin/tile-push": {
18
+ "import": "./dist/bin/tile-push.mjs",
19
+ "require": "./dist/bin/tile-push.cjs"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "keywords": [
29
+ "react-native",
30
+ "ota",
31
+ "code-push",
32
+ "hot-updater",
33
+ "tile-push",
34
+ "cli"
35
+ ],
36
+ "license": "MIT",
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "dependencies": {
41
+ "hot-updater": "npm:@tiledev/hot-updater@0.32.0",
42
+ "@hot-updater/plugin-core": "0.32.0",
43
+ "@hot-updater/cli-tools": "0.32.0"
44
+ },
45
+ "inlinedDependencies": {
46
+ "bundle-name": "4.1.0",
47
+ "commander": "14.0.3",
48
+ "default-browser": "5.5.0",
49
+ "default-browser-id": "5.0.1",
50
+ "define-lazy-prop": "3.0.0",
51
+ "is-docker": "3.0.0",
52
+ "is-inside-container": "1.0.0",
53
+ "is-wsl": "3.1.1",
54
+ "open": "10.1.0",
55
+ "picocolors": "1.1.1",
56
+ "run-applescript": "7.0.0"
57
+ },
58
+ "engines": {
59
+ "node": ">=18"
60
+ },
61
+ "repository": {
62
+ "type": "git",
63
+ "url": "git+https://github.com/gronxb/hot-updater.git"
64
+ }
65
+ }