@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/LICENSE +49 -0
- package/README.md +183 -0
- package/dist/apiClient-DiRjggqw.cjs +200 -0
- package/dist/apiClient-Dxz7PXQr.mjs +164 -0
- package/dist/bin/tile-push.cjs +4469 -0
- package/dist/bin/tile-push.d.cts +1 -0
- package/dist/bin/tile-push.d.mts +1 -0
- package/dist/bin/tile-push.mjs +4462 -0
- package/dist/index.cjs +134 -0
- package/dist/index.d.cts +153 -0
- package/dist/index.d.mts +153 -0
- package/dist/index.mjs +125 -0
- package/package.json +65 -0
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;
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.mts
ADDED
|
@@ -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
|
+
}
|