@stage5/lumine 0.2.4 → 0.2.6
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/README.md +26 -0
- package/bin/lumine.js +1 -3158
- package/lib/api.js +445 -0
- package/lib/assets.js +833 -0
- package/lib/auth.js +217 -0
- package/lib/commands.js +1512 -0
- package/lib/constants.js +296 -0
- package/lib/http.js +72 -0
- package/lib/sdk.js +489 -0
- package/lib/thumbnail.js +330 -0
- package/lib/util.js +154 -0
- package/lib/workspace.js +577 -0
- package/package.json +5 -1
package/lib/constants.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_API_URL = "https://api.twinkle.network";
|
|
5
|
+
export const DEFAULT_SITE_URL = "https://www.twin-kle.com";
|
|
6
|
+
export const DEFAULT_NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
7
|
+
export const DEFAULT_AUTH_FILE = path.join(
|
|
8
|
+
os.homedir(),
|
|
9
|
+
".twinkle",
|
|
10
|
+
"lumine-cli-auth.json",
|
|
11
|
+
);
|
|
12
|
+
export const DEFAULT_TIMEOUT_MS = 20000;
|
|
13
|
+
// Image generation runs a multi-minute provider call; thumbnail capture runs a
|
|
14
|
+
// headless browser server-side. Both need per-call timeouts far above the
|
|
15
|
+
// 20s default.
|
|
16
|
+
export const ASSET_GENERATE_TIMEOUT_MS = 6 * 60 * 1000;
|
|
17
|
+
export const THUMBNAIL_CAPTURE_TIMEOUT_MS = 90 * 1000;
|
|
18
|
+
export const GENERATE_MODEL_ALIASES = {
|
|
19
|
+
"gpt-image-2": "gpt-image-2",
|
|
20
|
+
"nano-banana": "gemini-3-pro-image-preview",
|
|
21
|
+
"gemini-3-pro-image-preview": "gemini-3-pro-image-preview",
|
|
22
|
+
};
|
|
23
|
+
export const GENERATE_QUALITIES = new Set(["low", "medium", "high"]);
|
|
24
|
+
// Server accepts only these thumbnail content types (8MB max).
|
|
25
|
+
export const THUMBNAIL_CONTENT_TYPE_BY_EXTENSION = {
|
|
26
|
+
".jpg": "image/jpeg",
|
|
27
|
+
".jpeg": "image/jpeg",
|
|
28
|
+
".png": "image/png",
|
|
29
|
+
".webp": "image/webp",
|
|
30
|
+
};
|
|
31
|
+
export const THUMBNAIL_MAX_FILE_SIZE_BYTES = 8 * 1024 * 1024;
|
|
32
|
+
export const UPDATE_CHECK_TIMEOUT_MS = 1500;
|
|
33
|
+
export const DEFAULT_PROJECT_LIMIT = 50;
|
|
34
|
+
export const PROJECT_METADATA_DIR = ".twinkle";
|
|
35
|
+
export const PROJECT_METADATA_FILE = "lumine-project.json";
|
|
36
|
+
export const ASSETS_METADATA_FILE = "assets.json";
|
|
37
|
+
// Must match the web workspace's 5MB part size: the server presigns one S3
|
|
38
|
+
// part URL per 5MB of the declared fileSize.
|
|
39
|
+
export const ASSET_UPLOAD_CHUNK_BYTES = 5 * 1024 * 1024;
|
|
40
|
+
// Mirrors the web workspace's asset accept list (images + audio). The server
|
|
41
|
+
// only rejects video, but the platform's asset policy is image/audio.
|
|
42
|
+
export const ASSET_MIME_BY_EXTENSION = {
|
|
43
|
+
".png": "image/png",
|
|
44
|
+
".jpg": "image/jpeg",
|
|
45
|
+
".jpeg": "image/jpeg",
|
|
46
|
+
".gif": "image/gif",
|
|
47
|
+
".webp": "image/webp",
|
|
48
|
+
".svg": "image/svg+xml",
|
|
49
|
+
".bmp": "image/bmp",
|
|
50
|
+
".tiff": "image/tiff",
|
|
51
|
+
".tif": "image/tiff",
|
|
52
|
+
".heic": "image/heic",
|
|
53
|
+
".heif": "image/heif",
|
|
54
|
+
".avif": "image/avif",
|
|
55
|
+
".mp3": "audio/mpeg",
|
|
56
|
+
".wav": "audio/wav",
|
|
57
|
+
".ogg": "audio/ogg",
|
|
58
|
+
".m4a": "audio/mp4",
|
|
59
|
+
".aac": "audio/aac",
|
|
60
|
+
".flac": "audio/flac",
|
|
61
|
+
".aif": "audio/aiff",
|
|
62
|
+
".aiff": "audio/aiff",
|
|
63
|
+
};
|
|
64
|
+
// Mirrors the platform's project-file limits. File count and effective-line
|
|
65
|
+
// limits are hardcoded server constants; the total-byte limit is
|
|
66
|
+
// env-overridable server-side, so the CLI only warns on it.
|
|
67
|
+
export const PROJECT_MAX_FILES = 100;
|
|
68
|
+
export const PROJECT_MAX_EFFECTIVE_FILE_LINES = 500;
|
|
69
|
+
export const PROJECT_EFFECTIVE_LINE_MAX_COLUMNS = 160;
|
|
70
|
+
export const PROJECT_MAX_TOTAL_BYTES_DEFAULT = 300 * 1024;
|
|
71
|
+
// Matches asset references in project files: absolute CloudFront URLs and
|
|
72
|
+
// relative /attachments/build-runtime|optimized/... paths.
|
|
73
|
+
export const RUNTIME_ASSET_REFERENCE_PATTERN =
|
|
74
|
+
/(?:https?:\/\/[^\s"'`)\\]+)?\/attachments\/(?:build-runtime|optimized)\/[^\s"'`)\\]+/g;
|
|
75
|
+
export const DEFAULT_SAVE_SUMMARY = "Saved from Lumine CLI.";
|
|
76
|
+
export const EXCLUDED_UPLOAD_DIRS = new Set([".git", ".twinkle", "node_modules"]);
|
|
77
|
+
export const SDK_REFERENCE_FILE = "TWINKLE_BUILD_SDK.md";
|
|
78
|
+
export const EXCLUDED_UPLOAD_FILES = new Set([
|
|
79
|
+
".DS_Store",
|
|
80
|
+
"AGENTS.md",
|
|
81
|
+
"CLAUDE.md",
|
|
82
|
+
SDK_REFERENCE_FILE,
|
|
83
|
+
]);
|
|
84
|
+
export const LUMINE_AGENT_INSTRUCTIONS_MARKER =
|
|
85
|
+
"<!-- Lumine CLI Agent Instructions -->";
|
|
86
|
+
export const LUMINE_SDK_REFERENCE_MARKER = "<!-- Lumine CLI SDK Reference -->";
|
|
87
|
+
export const LUMINE_REFERENCE_INSTRUCTIONS_MARKER =
|
|
88
|
+
"<!-- Lumine CLI Reference Instructions -->";
|
|
89
|
+
export const LUMINE_MAIN_CHECKOUT_INSTRUCTIONS_MARKER =
|
|
90
|
+
"<!-- Lumine CLI Main Checkout Instructions -->";
|
|
91
|
+
export const BUNDLED_SDK_REFERENCE_URL = new URL(
|
|
92
|
+
"../sdk/BUILD_SDK_INDEX.md",
|
|
93
|
+
import.meta.url,
|
|
94
|
+
);
|
|
95
|
+
export const PACKAGE_METADATA_URL = new URL("../package.json", import.meta.url);
|
|
96
|
+
export const SDK_REFERENCE_FALLBACK = `${LUMINE_SDK_REFERENCE_MARKER}
|
|
97
|
+
# Twinkle Build SDK Reference
|
|
98
|
+
|
|
99
|
+
The bundled SDK reference could not be loaded from this Lumine CLI package.
|
|
100
|
+
|
|
101
|
+
Use these current source-of-truth rules:
|
|
102
|
+
|
|
103
|
+
- Read .twinkle/lumine-project.json before editing.
|
|
104
|
+
- Use Twinkle.capabilities.get() or Twinkle.capabilities.can(actionName) before relying on gated SDK calls.
|
|
105
|
+
- Use Twinkle.privateDb for simple private per-user preferences, drafts, settings, and small JSON state.
|
|
106
|
+
- Use Twinkle.userDb only for advanced private SQLite tables, indexes, many rows, filtered queries, or aggregates.
|
|
107
|
+
- Use Twinkle.sharedDb for shared multi-user JSON data.
|
|
108
|
+
- Use Twinkle.aiCards.list/search/get for existing public AI Card words, exampleText sentence material, and word levels.
|
|
109
|
+
- Use Twinkle.aiStories.list/search/get for existing AI Story passage text, story media, and questions.
|
|
110
|
+
- Use Twinkle.ai.chat with history entries shaped as { role, content }, not { text }.
|
|
111
|
+
- Use Twinkle.preview for canvas, WebGL, Three.js, fullscreen, and game layout.
|
|
112
|
+
- Prefer existing documented Twinkle.* methods over guessing names from old code.
|
|
113
|
+
`;
|
|
114
|
+
export const LUMINE_AGENT_INSTRUCTIONS = `${LUMINE_AGENT_INSTRUCTIONS_MARKER}
|
|
115
|
+
# Lumine Project Agent Guide
|
|
116
|
+
|
|
117
|
+
This directory contains Twinkle Build project files pulled by Lumine CLI. Use
|
|
118
|
+
Lumine CLI as the source of truth for saving this workspace back to Twinkle.
|
|
119
|
+
|
|
120
|
+
## Source Of Truth
|
|
121
|
+
|
|
122
|
+
- Read .twinkle/lumine-project.json before changing files.
|
|
123
|
+
- Treat build.canWrite, build.canPublish, and build.contributionRootBuildId as authoritative.
|
|
124
|
+
- If lumineCli.updateAvailable is true, ask the user to rerun with npx @stage5/lumine@latest before saving.
|
|
125
|
+
- Read ${SDK_REFERENCE_FILE} before adding, removing, or changing any Twinkle.* SDK calls.
|
|
126
|
+
- If build.canWrite is false, do not save changes.
|
|
127
|
+
- If build.canPublish is false or contributionRootBuildId is set, this checkout is a contribution branch. Save only to this branch and do not run lumine launch or lumine save --publish.
|
|
128
|
+
- On a contribution branch, main may have moved since the branch was created. Before starting large edits, run \`lumine update-from-main\` to three-way-merge main into the branch (resolve any <<<<<<< conflict markers it reports, then save). \`lumine pull --main\` gives a read-only checkout of main for comparison (created alongside your workspace, never inside it).
|
|
129
|
+
- Do not edit another local checkout to bypass branch rules.
|
|
130
|
+
|
|
131
|
+
## Workflow
|
|
132
|
+
|
|
133
|
+
- Edit only project files in this workspace.
|
|
134
|
+
- Keep /index.html or /index.htm as the entry file.
|
|
135
|
+
- Run lumine save from this folder after edits, with a short summary:
|
|
136
|
+
|
|
137
|
+
\`\`\`bash
|
|
138
|
+
lumine save --summary "Describe the change"
|
|
139
|
+
\`\`\`
|
|
140
|
+
|
|
141
|
+
- Run lumine check before launch when possible.
|
|
142
|
+
- Use \`lumine sdk call <namespace.method> '{...}'\` to inspect real endpoint
|
|
143
|
+
data and measure latency (add --repeat <n>); \`lumine sdk list\` shows
|
|
144
|
+
callable methods. It prints the raw HTTP endpoint response, which can differ
|
|
145
|
+
from a method's Twinkle.* SDK return shape (some wrappers unwrap or rename
|
|
146
|
+
fields) — use ${SDK_REFERENCE_FILE} for SDK return shapes. Write methods
|
|
147
|
+
need --allow-write and mutate real app data.
|
|
148
|
+
- Owned canonical builds may be published only when the user explicitly asks.
|
|
149
|
+
|
|
150
|
+
## Assets (Images & Audio)
|
|
151
|
+
|
|
152
|
+
- Binary files are NOT project files. Never place images or audio in this
|
|
153
|
+
workspace — lumine save rejects binaries. Assets live in Twinkle's asset
|
|
154
|
+
storage and are referenced from code by absolute URL.
|
|
155
|
+
- .twinkle/${ASSETS_METADATA_FILE} lists this build's uploaded assets (the current
|
|
156
|
+
CLI user's uploads) with their URLs. Reference an asset by its \`url\` value.
|
|
157
|
+
- \`lumine assets upload <file...>\` uploads images/audio from disk and prints
|
|
158
|
+
the URL to use in code. \`lumine assets list\` prints assets and refreshes
|
|
159
|
+
.twinkle/${ASSETS_METADATA_FILE}; \`lumine assets delete <assetId>\` removes one;
|
|
160
|
+
\`lumine assets prune\` deletes your uploads that nothing references (server
|
|
161
|
+
checks draft files, published/version snapshots, and derived builds; local
|
|
162
|
+
unsaved edits are scanned too). The server enforces storage quotas.
|
|
163
|
+
- CAUTION: asset URLs an app stores in privateDb/sharedDb/user DBs at runtime
|
|
164
|
+
(Twinkle.files uploads) are invisible to prune. Never run \`assets prune\` on
|
|
165
|
+
a build whose app stores uploaded-file URLs in app data.
|
|
166
|
+
- On team projects the manifest also lists \`projectAssets\` (the project
|
|
167
|
+
owner's uploads) and \`unmatchedReferences\` (asset URLs in code matching no
|
|
168
|
+
known asset). Reuse existing asset URLs — do NOT re-upload media the
|
|
169
|
+
project already has.
|
|
170
|
+
- Uploading a file with an existing name creates a NEW asset and URL; it never
|
|
171
|
+
replaces. Update code references, then delete or prune the old asset.
|
|
172
|
+
- Prefer one small assets module (e.g. src/assets.js) mapping names to asset
|
|
173
|
+
URLs over scattering URLs through the code.
|
|
174
|
+
- \`lumine assets generate "<prompt>" --model <gpt-image-2|nano-banana>\` creates
|
|
175
|
+
an AI-generated image asset. --model is REQUIRED (no default): gpt-image-2 is
|
|
176
|
+
best quality but slower and pricier; nano-banana (Gemini) is faster and
|
|
177
|
+
cheaper. It spends the logged-in user's AI Battery, so it asks y/N with the
|
|
178
|
+
estimated cost first; non-interactive runs must pass --yes to consent.
|
|
179
|
+
Optional: --quality low|medium|high (gpt-image-2 only, default high),
|
|
180
|
+
--name <fileName>. The asset lands in .twinkle/${ASSETS_METADATA_FILE} like an upload.
|
|
181
|
+
|
|
182
|
+
## Thumbnail
|
|
183
|
+
|
|
184
|
+
- \`lumine thumbnail set <file>\` uploads a jpg/png/webp (max 8MB) as the
|
|
185
|
+
build's thumbnail. Thumbnails display ~16:9; the image is uploaded uncropped.
|
|
186
|
+
- \`lumine thumbnail capture\` screenshots the running app server-side and sets
|
|
187
|
+
the result as the thumbnail (add --out <file> to also save it locally).
|
|
188
|
+
- \`lumine thumbnail generate "<prompt>" --model <gpt-image-2|nano-banana>\`
|
|
189
|
+
generates an AI image and sets it as the thumbnail (also stored as a normal
|
|
190
|
+
reusable asset). Same battery-cost consent rules as \`assets generate\`.
|
|
191
|
+
- Replacing an existing thumbnail asks y/N; pass --yes for non-interactive runs.
|
|
192
|
+
- Never run thumbnail commands from a read-only \`pull --main\` checkout; use
|
|
193
|
+
your contribution-branch workspace or an owned build.
|
|
194
|
+
|
|
195
|
+
## Project File Limits
|
|
196
|
+
|
|
197
|
+
- Max ${PROJECT_MAX_FILES} files per project; each file max ${PROJECT_MAX_EFFECTIVE_FILE_LINES} effective lines
|
|
198
|
+
(a physical line counts once per ${PROJECT_EFFECTIVE_LINE_MAX_COLUMNS} characters, so minified one-liners
|
|
199
|
+
blow the limit). Default total project size ~${Math.floor(PROJECT_MAX_TOTAL_BYTES_DEFAULT / 1024)} KB.
|
|
200
|
+
- Plan file splits BEFORE writing code; \`lumine check\` validates the local
|
|
201
|
+
workspace against these limits without saving, and \`lumine save\` fails fast
|
|
202
|
+
locally listing every violation.
|
|
203
|
+
- Files must be UTF-8 text. UTF-16 files are rejected with a re-encode hint.
|
|
204
|
+
|
|
205
|
+
## App Constraints
|
|
206
|
+
|
|
207
|
+
- Use local project files with relative or root-local imports only. Do not add package imports, CDN scripts, external network calls, or app-local /api/* routes.
|
|
208
|
+
- Build apps run in sandboxed iframes without allow-forms. Do not use <form> elements, native form submission, requestSubmit(), or browser form navigation. Build input flows with JavaScript-handled inputs and buttons instead.
|
|
209
|
+
- CAUTION: the preview runtime AUTO-DETECTS "game apps" — any <canvas> in the body (even a decorative background canvas) or game-y words in visible text switch the app to viewport-app mode: html/body get overflow:hidden !important and body becomes a centering flexbox, so tall document-flow pages clip and stop scrolling. Document-style apps that use a canvas must call Twinkle.preview.subscribe (or getLayout/reserveInsets) early at boot — any of those opts out of auto game mode — then pad by layout.safeInsets and scroll within layout.viewport.height.
|
|
210
|
+
- For canvas, WebGL, Three.js, fullscreen, or game builds, use Twinkle.preview for layout. Do not size roots from 100vh, 100vw, 100dvh, 100dvw, window.innerWidth, window.innerHeight, visualViewport, or document viewport dimensions.
|
|
211
|
+
- For Three.js, use import * as THREE from '/build/vendor/three/0.184.0/three.module.min.js';. Addons (OrbitControls, GLTFLoader, ...) live under /build/vendor/three/0.184.0/addons/, e.g. import { OrbitControls } from '/build/vendor/three/0.184.0/addons/controls/OrbitControls.js';. Builds saved with the older /build/vendor/three/0.160.0/ path keep working.
|
|
212
|
+
- Do not invent or guess Twinkle.* SDK method names. Use ${SDK_REFERENCE_FILE} as the local SDK reference and prefer Twinkle.capabilities checks for gated features.
|
|
213
|
+
- Match storage to update frequency. Twinkle.privateDb and Twinkle.sharedDb are for LOW-frequency durable state only — things that change on a user action (settings, inventory checkpoints, completed quests, saved progress; comments, votes, room settings, submitted records). NEVER write high-frequency or per-frame/per-tick state to them (camera or cursor position, animation state, live movement, presence, autosave every frame/tick). Keep live state in client memory, broadcast realtime/presence via Twinkle.world, and for durable per-user state flush an occasional snapshot on an interval or on exit (never per frame) — e.g. the viewer/user DB or a single latest-snapshot key. The server rate-limits these writes per key and returns 429 on excess; never retry-loop a 429.
|
|
214
|
+
|
|
215
|
+
## Local Testing (Playwright / browser probes)
|
|
216
|
+
|
|
217
|
+
- Serve the workspace with a tiny local HTTP server and drive it with Playwright. NEVER copy probe/vendor files into the workspace dir — lumine save uploads everything here (and binary files fail validation). Build a sibling probe dir that symlinks the workspace files instead.
|
|
218
|
+
- Vendored imports like /build/vendor/three/0.184.0/... are absolute paths: mirror that directory under your probe dir's root and fetch the files from the LIVE SITE (e.g. https://www.twin-kle.com/build/vendor/three/0.184.0/three.webgpu.min.js). three 0.184 splits into three.module.min.js + three.core.min.js — mirror BOTH or imports fail. Do NOT use npm/CDN copies — the platform's vendored builds have rewritten import specifiers (npm three.tsl.min.js still imports bare "three/webgpu" and breaks the module graph).
|
|
219
|
+
- The three WebGPU renderer falls back to WebGL2 in headless Chromium automatically. Headless software rendering runs at ~2-5fps, so anything time-based (walking a character, timers) takes ~10-20x longer than real time — loop with generous waits instead of fixed short sleeps, and bump navigation timeouts.
|
|
220
|
+
- To inspect module-scope game state, append debug getters when SERVING main.js (e.g. body += "window.__dbg = () => ({...})") rather than editing workspace files.
|
|
221
|
+
- SDK calls are absent when serving locally; well-written builds optional-chain window.Twinkle and fall back to localStorage. Seed localStorage in the probe to fake saves.
|
|
222
|
+
|
|
223
|
+
## Completion Report
|
|
224
|
+
|
|
225
|
+
Report the changed files, any SDK methods used, the lumine save result, the
|
|
226
|
+
build or branch id, and whether the result is published or unpublished changes.
|
|
227
|
+
`;
|
|
228
|
+
export const LUMINE_REFERENCE_INSTRUCTIONS = `${LUMINE_REFERENCE_INSTRUCTIONS_MARKER}
|
|
229
|
+
# Lumine Reference Guide
|
|
230
|
+
|
|
231
|
+
This directory contains read-only reference files pulled from a public
|
|
232
|
+
open-source Twinkle Build. Use it for inspection and borrowing patterns, not as
|
|
233
|
+
the workspace to save.
|
|
234
|
+
|
|
235
|
+
## Source Of Truth
|
|
236
|
+
|
|
237
|
+
- Read .twinkle/lumine-project.json before using these files.
|
|
238
|
+
- If lumineCli.updateAvailable is true, ask the user to rerun with npx @stage5/lumine@latest before borrowing patterns.
|
|
239
|
+
- If metadata.readOnly is true or build.role is "reference", do not run lumine save from this directory.
|
|
240
|
+
- To start from this Build, run lumine fork with the source build id and edit the forked workspace.
|
|
241
|
+
- Do not edit another local checkout to bypass reference read-only semantics.
|
|
242
|
+
`;
|
|
243
|
+
export const LUMINE_MAIN_CHECKOUT_INSTRUCTIONS = `${LUMINE_MAIN_CHECKOUT_INSTRUCTIONS_MARKER}
|
|
244
|
+
# Lumine Main Checkout (Read-Only)
|
|
245
|
+
|
|
246
|
+
This directory is a read-only snapshot of a team project's MAIN workspace,
|
|
247
|
+
pulled with \`lumine pull --main\`. Use it to inspect what main currently looks
|
|
248
|
+
like; it is not the workspace to edit or save.
|
|
249
|
+
|
|
250
|
+
## Source Of Truth
|
|
251
|
+
|
|
252
|
+
- Read .twinkle/lumine-project.json before using these files.
|
|
253
|
+
- metadata.mainCheckout is true here: do not run lumine save from this directory.
|
|
254
|
+
- Make changes in your contribution-branch workspace (lumine pull <buildId>).
|
|
255
|
+
- Bring main's latest changes into your branch with lumine update-from-main.
|
|
256
|
+
- Do not edit another local checkout to bypass read-only semantics.
|
|
257
|
+
`;
|
|
258
|
+
export const AGENT_INSTRUCTION_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
259
|
+
// Commands allowed to resolve a read-only `pull --main` checkout's build id.
|
|
260
|
+
// Everything else (launch/save/merge/…) mutates and must not run from one.
|
|
261
|
+
export const MAIN_CHECKOUT_READONLY_COMMANDS = new Set([
|
|
262
|
+
"pull",
|
|
263
|
+
"check",
|
|
264
|
+
"diff",
|
|
265
|
+
"sdk",
|
|
266
|
+
"select",
|
|
267
|
+
"workspace",
|
|
268
|
+
// assets never touch main's project files (uploads go to the current
|
|
269
|
+
// user's own asset space), so listing/uploading from a main checkout is safe.
|
|
270
|
+
"assets",
|
|
271
|
+
]);
|
|
272
|
+
export const COMMANDS = new Set([
|
|
273
|
+
"workspace",
|
|
274
|
+
"login",
|
|
275
|
+
"logout",
|
|
276
|
+
"whoami",
|
|
277
|
+
"new",
|
|
278
|
+
"projects",
|
|
279
|
+
"explore",
|
|
280
|
+
"select",
|
|
281
|
+
"pull",
|
|
282
|
+
"reference",
|
|
283
|
+
"fork",
|
|
284
|
+
"diff",
|
|
285
|
+
"merge",
|
|
286
|
+
"replace-main",
|
|
287
|
+
"update-from-main",
|
|
288
|
+
"save",
|
|
289
|
+
"push",
|
|
290
|
+
"check",
|
|
291
|
+
"launch",
|
|
292
|
+
"sdk",
|
|
293
|
+
"assets",
|
|
294
|
+
"thumbnail",
|
|
295
|
+
"help",
|
|
296
|
+
]);
|
package/lib/http.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { parseJson } from "./util.js";
|
|
2
|
+
|
|
3
|
+
export async function requestJson({
|
|
4
|
+
method = "GET",
|
|
5
|
+
url,
|
|
6
|
+
authToken,
|
|
7
|
+
body,
|
|
8
|
+
timeoutMs,
|
|
9
|
+
headers = {},
|
|
10
|
+
}) {
|
|
11
|
+
const response = await request({
|
|
12
|
+
method,
|
|
13
|
+
url,
|
|
14
|
+
authToken,
|
|
15
|
+
body,
|
|
16
|
+
timeoutMs,
|
|
17
|
+
headers,
|
|
18
|
+
});
|
|
19
|
+
const text = await response.text();
|
|
20
|
+
const data = parseJson(text);
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
const error = new Error(
|
|
23
|
+
data?.error || data?.message || `${method} ${url} failed`,
|
|
24
|
+
);
|
|
25
|
+
error.status = response.status;
|
|
26
|
+
error.data = data;
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
return data || {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function probeUrl({ url, authToken, timeoutMs }) {
|
|
33
|
+
const response = await request({ url, authToken, timeoutMs });
|
|
34
|
+
const text = await response.text();
|
|
35
|
+
return {
|
|
36
|
+
ok: response.ok && text.trim().length > 0,
|
|
37
|
+
status: response.status,
|
|
38
|
+
bytes: Buffer.byteLength(text),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function request({
|
|
43
|
+
method = "GET",
|
|
44
|
+
url,
|
|
45
|
+
authToken,
|
|
46
|
+
body,
|
|
47
|
+
timeoutMs,
|
|
48
|
+
headers = {},
|
|
49
|
+
}) {
|
|
50
|
+
const controller = new AbortController();
|
|
51
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
52
|
+
try {
|
|
53
|
+
return await fetch(url, {
|
|
54
|
+
method,
|
|
55
|
+
headers: {
|
|
56
|
+
...(authToken ? { authorization: authorizationHeader(authToken) } : {}),
|
|
57
|
+
...(body ? { "content-type": "application/json" } : {}),
|
|
58
|
+
...headers,
|
|
59
|
+
},
|
|
60
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
61
|
+
signal: controller.signal,
|
|
62
|
+
});
|
|
63
|
+
} finally {
|
|
64
|
+
clearTimeout(timeout);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function authorizationHeader(authToken) {
|
|
69
|
+
const token = String(authToken || "").trim();
|
|
70
|
+
if (!token) return "";
|
|
71
|
+
return token.toLowerCase().startsWith("bearer ") ? token : `Bearer ${token}`;
|
|
72
|
+
}
|