@proveanything/smartlinks 2.0.67 → 2.0.69
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/api/authKit.d.ts +14 -1
- package/dist/api/authKit.js +15 -0
- package/dist/api/collection.d.ts +21 -11
- package/dist/api/collection.js +25 -13
- package/dist/docs/API_SUMMARY.md +40 -15
- package/dist/docs/auth-kit.md +650 -626
- package/dist/docs/deploying-apps.md +9 -0
- package/dist/index.d.ts +1 -1
- package/dist/openapi.yaml +72 -3
- package/dist/types/authKit.d.ts +21 -1
- package/dist/types/collection.d.ts +6 -4
- package/docs/API_SUMMARY.md +40 -15
- package/docs/auth-kit.md +650 -626
- package/docs/deploying-apps.md +9 -0
- package/openapi.yaml +72 -3
- package/package.json +1 -1
- package/scripts/publish.mjs +185 -147
package/docs/deploying-apps.md
CHANGED
|
@@ -94,6 +94,15 @@ Env: `SMARTLINKS_APP_ID`, `SMARTLINKS_DEPLOY_KEY` (dev key — dev-only, safe to
|
|
|
94
94
|
optional `SMARTLINKS_API` / `--dir`. It POSTs the files to `POST /apps/{appId}/publish?channel=dev`;
|
|
95
95
|
the server validates and writes them to `smartlinks.app/apps/{appId}/dev/…` (`no-store`).
|
|
96
96
|
|
|
97
|
+
- **Big bundles** go in batches (`/publish/files`), and any file of 4 MB or more goes straight to
|
|
98
|
+
storage through a one-off upload URL (`/publish/uploads`), so no file is too large to publish. The
|
|
99
|
+
final `/publish` call registers the release only once every file has landed.
|
|
100
|
+
- **Source maps (`.map`) are never published.** A release is promoted to beta and stable byte for byte,
|
|
101
|
+
so a map would serve the app's original source to anyone; the server drops any it's sent. Debug in
|
|
102
|
+
Forge's preview, where the dev server has full source. In Forge, **Release to dev** builds readable
|
|
103
|
+
(unminified) code; **Release to beta / stable** builds minified code (the build sees
|
|
104
|
+
`SMARTLINKS_BUILD=debug` or `production`).
|
|
105
|
+
|
|
97
106
|
**Which to use:** building in Lovable → **A** (ping, no key). Building anywhere you control →
|
|
98
107
|
**B** (push with a local key). For beta/stable, see the register-a-URL flow below.
|
|
99
108
|
|
package/openapi.yaml
CHANGED
|
@@ -2077,6 +2077,45 @@ paths:
|
|
|
2077
2077
|
description: Unauthorized
|
|
2078
2078
|
404:
|
|
2079
2079
|
description: Not found
|
|
2080
|
+
/admin/collection/{collectionId}/authKit/{authKitId}/identity:
|
|
2081
|
+
post:
|
|
2082
|
+
tags:
|
|
2083
|
+
- authKit
|
|
2084
|
+
summary: "Give this AuthKit its **own logins** (admin): its members become accounts of this site only — the same email can be a separate account, with its own password, on another site — rather than SmartLinks accounts shared across sites."
|
|
2085
|
+
operationId: authKit_setIdentity
|
|
2086
|
+
security:
|
|
2087
|
+
- bearerAuth: []
|
|
2088
|
+
parameters:
|
|
2089
|
+
- name: collectionId
|
|
2090
|
+
in: path
|
|
2091
|
+
required: true
|
|
2092
|
+
schema:
|
|
2093
|
+
type: string
|
|
2094
|
+
- name: authKitId
|
|
2095
|
+
in: path
|
|
2096
|
+
required: true
|
|
2097
|
+
schema:
|
|
2098
|
+
type: string
|
|
2099
|
+
responses:
|
|
2100
|
+
200:
|
|
2101
|
+
description: Success
|
|
2102
|
+
content:
|
|
2103
|
+
application/json:
|
|
2104
|
+
schema:
|
|
2105
|
+
$ref: "#/components/schemas/AuthKitSetIdentityResponse"
|
|
2106
|
+
400:
|
|
2107
|
+
description: Bad request
|
|
2108
|
+
401:
|
|
2109
|
+
description: Unauthorized
|
|
2110
|
+
404:
|
|
2111
|
+
description: Not found
|
|
2112
|
+
requestBody:
|
|
2113
|
+
required: true
|
|
2114
|
+
content:
|
|
2115
|
+
application/json:
|
|
2116
|
+
schema:
|
|
2117
|
+
type: object
|
|
2118
|
+
additionalProperties: true
|
|
2080
2119
|
/admin/collection/{collectionId}/batch:
|
|
2081
2120
|
get:
|
|
2082
2121
|
tags:
|
|
@@ -3715,7 +3754,7 @@ paths:
|
|
|
3715
3754
|
get:
|
|
3716
3755
|
tags:
|
|
3717
3756
|
- collection
|
|
3718
|
-
summary: "Get the
|
|
3757
|
+
summary: "Get the certificate status of the collection's own domain (admin only)."
|
|
3719
3758
|
operationId: collection_getDomainStatus
|
|
3720
3759
|
security:
|
|
3721
3760
|
- bearerAuth: []
|
|
@@ -3740,7 +3779,7 @@ paths:
|
|
|
3740
3779
|
post:
|
|
3741
3780
|
tags:
|
|
3742
3781
|
- collection
|
|
3743
|
-
summary:
|
|
3782
|
+
summary: "Connect one of the collection's own domains and start its certificate (admin only)."
|
|
3744
3783
|
operationId: collection_registerDomain
|
|
3745
3784
|
security:
|
|
3746
3785
|
- bearerAuth: []
|
|
@@ -9207,7 +9246,7 @@ paths:
|
|
|
9207
9246
|
post:
|
|
9208
9247
|
tags:
|
|
9209
9248
|
- authKit
|
|
9210
|
-
summary:
|
|
9249
|
+
summary: Sign in with LinkedIn (OpenID Connect) via a server-side authorization code (public).
|
|
9211
9250
|
operationId: authKit_linkedInLogin
|
|
9212
9251
|
security: []
|
|
9213
9252
|
parameters:
|
|
@@ -23327,8 +23366,38 @@ components:
|
|
|
23327
23366
|
type: string
|
|
23328
23367
|
security:
|
|
23329
23368
|
$ref: "#/components/schemas/AuthKitSecurityConfig"
|
|
23369
|
+
collectionIds:
|
|
23370
|
+
type: array
|
|
23371
|
+
items:
|
|
23372
|
+
type: string
|
|
23373
|
+
identity:
|
|
23374
|
+
$ref: "#/components/schemas/AuthKitIdentity"
|
|
23330
23375
|
required:
|
|
23331
23376
|
- id
|
|
23377
|
+
AuthKitIdentity:
|
|
23378
|
+
type: object
|
|
23379
|
+
properties:
|
|
23380
|
+
mode:
|
|
23381
|
+
type: string
|
|
23382
|
+
enum:
|
|
23383
|
+
- shared
|
|
23384
|
+
- own
|
|
23385
|
+
tenantId:
|
|
23386
|
+
type: string
|
|
23387
|
+
createdAt:
|
|
23388
|
+
type: string
|
|
23389
|
+
required:
|
|
23390
|
+
- mode
|
|
23391
|
+
AuthKitSetIdentityResponse:
|
|
23392
|
+
type: object
|
|
23393
|
+
properties:
|
|
23394
|
+
identity:
|
|
23395
|
+
$ref: "#/components/schemas/AuthKitIdentity"
|
|
23396
|
+
message:
|
|
23397
|
+
type: string
|
|
23398
|
+
required:
|
|
23399
|
+
- identity
|
|
23400
|
+
- message
|
|
23332
23401
|
AuthKitSecurityConfig:
|
|
23333
23402
|
type: object
|
|
23334
23403
|
properties:
|
package/package.json
CHANGED
package/scripts/publish.mjs
CHANGED
|
@@ -1,147 +1,185 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* publish — the FAST dev publish for a SmartLinks app. Builds nothing itself: it takes an already-
|
|
4
|
-
* built `dist/`, uploads it to the SmartLinks publish endpoint, which writes it straight to the CDN
|
|
5
|
-
* (dev = no-store, instantly fresh) and registers the release. One call = host + register, no Cloud
|
|
6
|
-
* Build, no Lovable. Works from anywhere: local, Claude, CI, or a build step.
|
|
7
|
-
*
|
|
8
|
-
* smartlinks-publish # build dist/ yourself first, then this
|
|
9
|
-
* smartlinks-publish --watch # re-publish on file change (save → live)
|
|
10
|
-
*
|
|
11
|
-
* Env:
|
|
12
|
-
* SMARTLINKS_APP_ID the platform app id (required)
|
|
13
|
-
* SMARTLINKS_DEPLOY_KEY channel-scoped deploy key (dev key is dev-only, safe to keep locally)
|
|
14
|
-
* SMARTLINKS_API API host (default https://smartlinks.app)
|
|
15
|
-
* SMARTLINKS_CHANNEL channel (default dev; only dev is supported by this fast path today)
|
|
16
|
-
* Flags: --dir <dist> --watch --channel <dev>
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { readFileSync, readdirSync, statSync, watch } from 'node:fs'
|
|
20
|
-
import { join, relative, sep } from 'node:path'
|
|
21
|
-
|
|
22
|
-
const args = process.argv.slice(2)
|
|
23
|
-
const flag = (name, def) => { const i = args.indexOf(name); return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : true) : def }
|
|
24
|
-
|
|
25
|
-
const API = process.env.SMARTLINKS_API || 'https://smartlinks.app'
|
|
26
|
-
const KEY = process.env.SMARTLINKS_DEPLOY_KEY
|
|
27
|
-
const APP_ID = process.env.SMARTLINKS_APP_ID
|
|
28
|
-
const CHANNEL = flag('--channel', process.env.SMARTLINKS_CHANNEL || 'dev')
|
|
29
|
-
const DIR = flag('--dir', 'dist')
|
|
30
|
-
const WATCH = args.includes('--watch')
|
|
31
|
-
|
|
32
|
-
// Mirror of the server's write allowlist — a bundle is static web assets only.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* publish — the FAST dev publish for a SmartLinks app. Builds nothing itself: it takes an already-
|
|
4
|
+
* built `dist/`, uploads it to the SmartLinks publish endpoint, which writes it straight to the CDN
|
|
5
|
+
* (dev = no-store, instantly fresh) and registers the release. One call = host + register, no Cloud
|
|
6
|
+
* Build, no Lovable. Works from anywhere: local, Claude, CI, or a build step.
|
|
7
|
+
*
|
|
8
|
+
* smartlinks-publish # build dist/ yourself first, then this
|
|
9
|
+
* smartlinks-publish --watch # re-publish on file change (save → live)
|
|
10
|
+
*
|
|
11
|
+
* Env:
|
|
12
|
+
* SMARTLINKS_APP_ID the platform app id (required)
|
|
13
|
+
* SMARTLINKS_DEPLOY_KEY channel-scoped deploy key (dev key is dev-only, safe to keep locally)
|
|
14
|
+
* SMARTLINKS_API API host (default https://smartlinks.app)
|
|
15
|
+
* SMARTLINKS_CHANNEL channel (default dev; only dev is supported by this fast path today)
|
|
16
|
+
* Flags: --dir <dist> --watch --channel <dev>
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync, readdirSync, statSync, watch } from 'node:fs'
|
|
20
|
+
import { join, relative, sep } from 'node:path'
|
|
21
|
+
|
|
22
|
+
const args = process.argv.slice(2)
|
|
23
|
+
const flag = (name, def) => { const i = args.indexOf(name); return i >= 0 ? (args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : true) : def }
|
|
24
|
+
|
|
25
|
+
const API = process.env.SMARTLINKS_API || 'https://smartlinks.app'
|
|
26
|
+
const KEY = process.env.SMARTLINKS_DEPLOY_KEY
|
|
27
|
+
const APP_ID = process.env.SMARTLINKS_APP_ID
|
|
28
|
+
const CHANNEL = flag('--channel', process.env.SMARTLINKS_CHANNEL || 'dev')
|
|
29
|
+
const DIR = flag('--dir', 'dist')
|
|
30
|
+
const WATCH = args.includes('--watch')
|
|
31
|
+
|
|
32
|
+
// Mirror of the server's write allowlist — a bundle is static web assets only. Source maps (.map) are
|
|
33
|
+
// never published: a release is promoted to beta and stable as-is, so a map would serve the app's
|
|
34
|
+
// original source publicly (and maps are most of a bundle's size). Debug in Forge's preview; a
|
|
35
|
+
// Release to dev is a readable, unminified build instead.
|
|
36
|
+
const ALLOWED = new Set(['.js', '.mjs', '.css', '.json', '.wasm', '.html', '.txt',
|
|
37
|
+
'.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.ico', '.woff', '.woff2', '.ttf', '.otf'])
|
|
38
|
+
const extOf = (p) => { const i = p.lastIndexOf('.'); return i < 0 ? '' : p.slice(i).toLowerCase() }
|
|
39
|
+
|
|
40
|
+
function fail(msg) { console.error(`❌ ${msg}`); process.exit(1) }
|
|
41
|
+
if (!APP_ID) fail('SMARTLINKS_APP_ID is required')
|
|
42
|
+
if (!KEY) fail('SMARTLINKS_DEPLOY_KEY is required')
|
|
43
|
+
|
|
44
|
+
let skippedMaps = 0
|
|
45
|
+
|
|
46
|
+
function collectFiles(dir) {
|
|
47
|
+
const out = {}
|
|
48
|
+
const walk = (d) => {
|
|
49
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
50
|
+
const full = join(d, e.name)
|
|
51
|
+
if (e.isDirectory()) { walk(full); continue }
|
|
52
|
+
if (!ALLOWED.has(extOf(e.name))) { if (extOf(e.name) === '.map') skippedMaps++; continue }
|
|
53
|
+
const rel = relative(dir, full).split(sep).join('/') // forward slashes for the bucket
|
|
54
|
+
out[rel] = readFileSync(full)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
walk(dir)
|
|
58
|
+
return out
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// One request carries at most this much file content (decoded). The server caps a request at 8 MB of
|
|
62
|
+
// content and ~10 MB of JSON; base64 adds a third, so 6 MB keeps every request comfortably under both.
|
|
63
|
+
const BATCH_BYTES = 6 * 1024 * 1024
|
|
64
|
+
const BATCH_FILES = 150
|
|
65
|
+
// A file at least this big goes straight to storage (a session URL from /publish/uploads) instead of
|
|
66
|
+
// through an API request, so no single file is ever too large to publish (e.g. a readable dev build).
|
|
67
|
+
const DIRECT_BYTES = 4 * 1024 * 1024
|
|
68
|
+
|
|
69
|
+
// Big files → storage directly. Returns the paths uploaded, or null when the server has no direct
|
|
70
|
+
// uploads yet (an older API: the caller falls back to batches).
|
|
71
|
+
async function uploadDirect(entries, qs) {
|
|
72
|
+
const { res, b } = await post(`/publish/uploads${qs}`, { files: entries.map(([p, buf]) => ({ path: p, size: buf.length })) })
|
|
73
|
+
if (res.status === 404) return null
|
|
74
|
+
if (!res.ok || !b.ok) { reportFailure(res, b); return false }
|
|
75
|
+
const bytes = new Map(entries)
|
|
76
|
+
for (const u of b.uploads || []) {
|
|
77
|
+
const buf = bytes.get(u.path)
|
|
78
|
+
const put = await fetch(u.url, { method: 'PUT', body: buf }).catch((e) => ({ ok: false, status: 0, text: async () => e.message }))
|
|
79
|
+
if (!put.ok) {
|
|
80
|
+
console.error(`❌ upload of ${u.path} failed (${put.status}): ${(await put.text().catch(() => '')).slice(0, 200)}`)
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
console.log(` ↑ ${u.path} (${(buf.length / 1048576).toFixed(1)} MB, direct)`)
|
|
84
|
+
}
|
|
85
|
+
return entries.map(([p]) => p)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function post(path, body) {
|
|
89
|
+
const res = await fetch(`${API}/api/v1/apps/${APP_ID}${path}`, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: { 'content-type': 'application/json', 'x-smartlinks-deploy-key': KEY },
|
|
92
|
+
body: JSON.stringify(body),
|
|
93
|
+
}).catch((e) => ({ ok: false, status: 0, json: async () => ({ errors: [{ message: `request failed — ${e.message}` }] }) }))
|
|
94
|
+
const b = await res.json().catch(() => ({}))
|
|
95
|
+
return { res, b }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function reportFailure(res, b) {
|
|
99
|
+
console.error(`❌ publish failed (${res.status}):`)
|
|
100
|
+
for (const e of b.errors || []) console.error(` • ${e.path || ''}: ${e.message}`)
|
|
101
|
+
if (!b.errors && (b.message || b.error)) console.error(` • ${b.message || b.error}`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const toB64 = (entries) => Object.fromEntries(entries.map(([p, buf]) => [p, buf.toString('base64')]))
|
|
105
|
+
|
|
106
|
+
async function publishOnce() {
|
|
107
|
+
let stat
|
|
108
|
+
try { stat = statSync(DIR) } catch { fail(`build dir not found: ${DIR} (build first)`) }
|
|
109
|
+
if (!stat.isDirectory()) fail(`${DIR} is not a directory`)
|
|
110
|
+
|
|
111
|
+
skippedMaps = 0
|
|
112
|
+
const files = collectFiles(DIR)
|
|
113
|
+
if (!files['app.manifest.json']) fail(`${DIR}/app.manifest.json not found — is this a built app bundle?`)
|
|
114
|
+
if (skippedMaps) console.log(` (${skippedMaps} source map${skippedMaps === 1 ? '' : 's'} left out — maps are never published)`)
|
|
115
|
+
|
|
116
|
+
const t0 = Date.now()
|
|
117
|
+
const entries = Object.entries(files)
|
|
118
|
+
const totalBytes = entries.reduce((n, [, buf]) => n + buf.length, 0)
|
|
119
|
+
const qs = `?channel=${encodeURIComponent(CHANNEL)}`
|
|
120
|
+
let finalBody
|
|
121
|
+
|
|
122
|
+
if (totalBytes <= BATCH_BYTES && entries.length <= BATCH_FILES) {
|
|
123
|
+
finalBody = { files: toB64(entries) } // small bundle: one request, as before
|
|
124
|
+
} else {
|
|
125
|
+
// Large bundle: upload everything except the manifest in batches, then send the manifest last —
|
|
126
|
+
// the server only registers the release once every referenced file is confirmed uploaded.
|
|
127
|
+
const rest = entries.filter(([p]) => p !== 'app.manifest.json').sort((a, b) => b[1].length - a[1].length)
|
|
128
|
+
const big = rest.filter(([, buf]) => buf.length >= DIRECT_BYTES)
|
|
129
|
+
let direct = []
|
|
130
|
+
if (big.length) {
|
|
131
|
+
const done = await uploadDirect(big, qs)
|
|
132
|
+
if (done === false) { if (WATCH) return false; process.exit(1) }
|
|
133
|
+
if (done) direct = done
|
|
134
|
+
else console.warn(' ⚠︎ this SmartLinks API has no direct uploads yet — sending big files in batches')
|
|
135
|
+
}
|
|
136
|
+
const viaBatches = rest.filter(([p]) => !direct.includes(p))
|
|
137
|
+
const batches = []
|
|
138
|
+
let cur = [], curBytes = 0
|
|
139
|
+
for (const e of viaBatches) {
|
|
140
|
+
if (cur.length && (curBytes + e[1].length > BATCH_BYTES || cur.length >= BATCH_FILES)) { batches.push(cur); cur = []; curBytes = 0 }
|
|
141
|
+
if (e[1].length > BATCH_BYTES) console.warn(` ⚠︎ ${e[0]} is ${(e[1].length / 1048576).toFixed(1)} MB — larger than one request allows; the server will likely reject it`)
|
|
142
|
+
cur.push(e); curBytes += e[1].length
|
|
143
|
+
}
|
|
144
|
+
if (cur.length) batches.push(cur)
|
|
145
|
+
console.log(`📦 ${APP_ID}: ${(totalBytes / 1048576).toFixed(1)} MB in ${entries.length} files — ${direct.length ? `${direct.length} sent directly, ` : ''}${batches.length} batch(es)`)
|
|
146
|
+
for (let i = 0; i < batches.length; i++) {
|
|
147
|
+
const { res, b } = await post(`/publish/files${qs}`, { files: toB64(batches[i]) })
|
|
148
|
+
if (!res.ok || !b.ok) {
|
|
149
|
+
reportFailure(res, b)
|
|
150
|
+
if (WATCH) return false
|
|
151
|
+
process.exit(1)
|
|
152
|
+
}
|
|
153
|
+
console.log(` ↑ batch ${i + 1}/${batches.length} (${batches[i].length} files)`)
|
|
154
|
+
}
|
|
155
|
+
finalBody = { files: toB64([['app.manifest.json', files['app.manifest.json']]]), uploaded: rest.map(([p]) => p) }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const { res, b } = await post(`/publish${qs}`, finalBody)
|
|
159
|
+
if (!res.ok || !b.ok) {
|
|
160
|
+
reportFailure(res, b)
|
|
161
|
+
if (WATCH) return false // keep watching; don't exit
|
|
162
|
+
process.exit(1)
|
|
163
|
+
}
|
|
164
|
+
for (const w of b.warnings || []) console.warn(` ⚠︎ ${w.path}: ${w.message}`)
|
|
165
|
+
console.log(`✅ ${APP_ID}@${b.version} → ${CHANNEL} (${entries.length} files, ${Date.now() - t0}ms) — functions: ${(b.functions || []).join(', ') || 'none'}`)
|
|
166
|
+
console.log(` ${b.bundleBaseUrl}`)
|
|
167
|
+
return true
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!WATCH) {
|
|
171
|
+
await publishOnce()
|
|
172
|
+
} else {
|
|
173
|
+
console.log(`👀 watching ${DIR} — publishing ${APP_ID} to ${CHANNEL} on change (Ctrl+C to stop)`)
|
|
174
|
+
await publishOnce()
|
|
175
|
+
let timer = null
|
|
176
|
+
let publishing = false
|
|
177
|
+
watch(DIR, { recursive: true }, () => {
|
|
178
|
+
clearTimeout(timer)
|
|
179
|
+
timer = setTimeout(async () => {
|
|
180
|
+
if (publishing) return
|
|
181
|
+
publishing = true
|
|
182
|
+
try { await publishOnce() } finally { publishing = false }
|
|
183
|
+
}, 300) // debounce a burst of file writes from one build
|
|
184
|
+
})
|
|
185
|
+
}
|