@proveanything/smartlinks 2.0.33 → 2.0.34

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.
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.33 | Generated: 2026-10-01T17:58:12.047Z
3
+ Version: 2.0.34 | Generated: 2026-10-02T16:06:32.154Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Smartlinks API Summary
2
2
 
3
- Version: 2.0.33 | Generated: 2026-10-01T17:58:12.047Z
3
+ Version: 2.0.34 | Generated: 2026-10-02T16:06:32.154Z
4
4
 
5
5
  This is a concise summary of all available API functions and types.
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "2.0.33",
3
+ "version": "2.0.34",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,99 +1,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.
33
- const ALLOWED = new Set(['.js', '.mjs', '.css', '.json', '.map', '.wasm', '.html', '.txt',
34
- '.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.ico', '.woff', '.woff2', '.ttf', '.otf'])
35
- const extOf = (p) => { const i = p.lastIndexOf('.'); return i < 0 ? '' : p.slice(i).toLowerCase() }
36
-
37
- function fail(msg) { console.error(`❌ ${msg}`); process.exit(1) }
38
- if (!APP_ID) fail('SMARTLINKS_APP_ID is required')
39
- if (!KEY) fail('SMARTLINKS_DEPLOY_KEY is required')
40
-
41
- function collectFiles(dir) {
42
- const out = {}
43
- const walk = (d) => {
44
- for (const e of readdirSync(d, { withFileTypes: true })) {
45
- const full = join(d, e.name)
46
- if (e.isDirectory()) { walk(full); continue }
47
- if (!ALLOWED.has(extOf(e.name))) continue
48
- const rel = relative(dir, full).split(sep).join('/') // forward slashes for the bucket
49
- out[rel] = readFileSync(full).toString('base64')
50
- }
51
- }
52
- walk(dir)
53
- return out
54
- }
55
-
56
- async function publishOnce() {
57
- let stat
58
- try { stat = statSync(DIR) } catch { fail(`build dir not found: ${DIR} (build first)`) }
59
- if (!stat.isDirectory()) fail(`${DIR} is not a directory`)
60
-
61
- const files = collectFiles(DIR)
62
- if (!files['app.manifest.json']) fail(`${DIR}/app.manifest.json not found — is this a built app bundle?`)
63
-
64
- const t0 = Date.now()
65
- const res = await fetch(`${API}/api/v1/apps/${APP_ID}/publish?channel=${encodeURIComponent(CHANNEL)}`, {
66
- method: 'POST',
67
- headers: { 'content-type': 'application/json', 'x-smartlinks-deploy-key': KEY },
68
- body: JSON.stringify({ files }),
69
- }).catch((e) => fail(`request failed — ${e.message}`))
70
-
71
- const b = await res.json().catch(() => ({}))
72
- if (!res.ok || !b.ok) {
73
- console.error(`❌ publish failed (${res.status}):`)
74
- for (const e of b.errors || []) console.error(` • ${e.path || ''}: ${e.message}`)
75
- if (WATCH) return false // keep watching; don't exit
76
- process.exit(1)
77
- }
78
- for (const w of b.warnings || []) console.warn(` ⚠︎ ${w.path}: ${w.message}`)
79
- console.log(`✅ ${APP_ID}@${b.version} → ${CHANNEL} (${Object.keys(files).length} files, ${Date.now() - t0}ms) — functions: ${(b.functions || []).join(', ') || 'none'}`)
80
- console.log(` ${b.bundleBaseUrl}`)
81
- return true
82
- }
83
-
84
- if (!WATCH) {
85
- await publishOnce()
86
- } else {
87
- console.log(`👀 watching ${DIR} — publishing ${APP_ID} to ${CHANNEL} on change (Ctrl+C to stop)`)
88
- await publishOnce()
89
- let timer = null
90
- let publishing = false
91
- watch(DIR, { recursive: true }, () => {
92
- clearTimeout(timer)
93
- timer = setTimeout(async () => {
94
- if (publishing) return
95
- publishing = true
96
- try { await publishOnce() } finally { publishing = false }
97
- }, 300) // debounce a burst of file writes from one build
98
- })
99
- }
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
+ const ALLOWED = new Set(['.js', '.mjs', '.css', '.json', '.map', '.wasm', '.html', '.txt',
34
+ '.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.ico', '.woff', '.woff2', '.ttf', '.otf'])
35
+ const extOf = (p) => { const i = p.lastIndexOf('.'); return i < 0 ? '' : p.slice(i).toLowerCase() }
36
+
37
+ function fail(msg) { console.error(`❌ ${msg}`); process.exit(1) }
38
+ if (!APP_ID) fail('SMARTLINKS_APP_ID is required')
39
+ if (!KEY) fail('SMARTLINKS_DEPLOY_KEY is required')
40
+
41
+ function collectFiles(dir) {
42
+ const out = {}
43
+ const walk = (d) => {
44
+ for (const e of readdirSync(d, { withFileTypes: true })) {
45
+ const full = join(d, e.name)
46
+ if (e.isDirectory()) { walk(full); continue }
47
+ if (!ALLOWED.has(extOf(e.name))) continue
48
+ const rel = relative(dir, full).split(sep).join('/') // forward slashes for the bucket
49
+ out[rel] = readFileSync(full)
50
+ }
51
+ }
52
+ walk(dir)
53
+ return out
54
+ }
55
+
56
+ // One request carries at most this much file content (decoded). The server caps a request at 8 MB of
57
+ // content and ~10 MB of JSON; base64 adds a third, so 6 MB keeps every request comfortably under both.
58
+ const BATCH_BYTES = 6 * 1024 * 1024
59
+ const BATCH_FILES = 150
60
+
61
+ async function post(path, body) {
62
+ const res = await fetch(`${API}/api/v1/apps/${APP_ID}${path}`, {
63
+ method: 'POST',
64
+ headers: { 'content-type': 'application/json', 'x-smartlinks-deploy-key': KEY },
65
+ body: JSON.stringify(body),
66
+ }).catch((e) => ({ ok: false, status: 0, json: async () => ({ errors: [{ message: `request failed — ${e.message}` }] }) }))
67
+ const b = await res.json().catch(() => ({}))
68
+ return { res, b }
69
+ }
70
+
71
+ function reportFailure(res, b) {
72
+ console.error(`❌ publish failed (${res.status}):`)
73
+ for (const e of b.errors || []) console.error(` • ${e.path || ''}: ${e.message}`)
74
+ if (!b.errors && (b.message || b.error)) console.error(` • ${b.message || b.error}`)
75
+ }
76
+
77
+ const toB64 = (entries) => Object.fromEntries(entries.map(([p, buf]) => [p, buf.toString('base64')]))
78
+
79
+ async function publishOnce() {
80
+ let stat
81
+ try { stat = statSync(DIR) } catch { fail(`build dir not found: ${DIR} (build first)`) }
82
+ if (!stat.isDirectory()) fail(`${DIR} is not a directory`)
83
+
84
+ const files = collectFiles(DIR)
85
+ if (!files['app.manifest.json']) fail(`${DIR}/app.manifest.json not found — is this a built app bundle?`)
86
+
87
+ const t0 = Date.now()
88
+ const entries = Object.entries(files)
89
+ const totalBytes = entries.reduce((n, [, buf]) => n + buf.length, 0)
90
+ const qs = `?channel=${encodeURIComponent(CHANNEL)}`
91
+ let finalBody
92
+
93
+ if (totalBytes <= BATCH_BYTES && entries.length <= BATCH_FILES) {
94
+ finalBody = { files: toB64(entries) } // small bundle: one request, as before
95
+ } else {
96
+ // Large bundle: upload everything except the manifest in batches, then send the manifest last —
97
+ // the server only registers the release once every referenced file is confirmed uploaded.
98
+ const rest = entries.filter(([p]) => p !== 'app.manifest.json').sort((a, b) => b[1].length - a[1].length)
99
+ const batches = []
100
+ let cur = [], curBytes = 0
101
+ for (const e of rest) {
102
+ if (cur.length && (curBytes + e[1].length > BATCH_BYTES || cur.length >= BATCH_FILES)) { batches.push(cur); cur = []; curBytes = 0 }
103
+ 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`)
104
+ cur.push(e); curBytes += e[1].length
105
+ }
106
+ if (cur.length) batches.push(cur)
107
+ console.log(`📦 ${APP_ID}: ${(totalBytes / 1048576).toFixed(1)} MB in ${entries.length} files — uploading in ${batches.length} batch(es)`)
108
+ for (let i = 0; i < batches.length; i++) {
109
+ const { res, b } = await post(`/publish/files${qs}`, { files: toB64(batches[i]) })
110
+ if (!res.ok || !b.ok) {
111
+ reportFailure(res, b)
112
+ if (WATCH) return false
113
+ process.exit(1)
114
+ }
115
+ console.log(` ↑ batch ${i + 1}/${batches.length} (${batches[i].length} files)`)
116
+ }
117
+ finalBody = { files: toB64([['app.manifest.json', files['app.manifest.json']]]), uploaded: rest.map(([p]) => p) }
118
+ }
119
+
120
+ const { res, b } = await post(`/publish${qs}`, finalBody)
121
+ if (!res.ok || !b.ok) {
122
+ reportFailure(res, b)
123
+ if (WATCH) return false // keep watching; don't exit
124
+ process.exit(1)
125
+ }
126
+ for (const w of b.warnings || []) console.warn(` ⚠︎ ${w.path}: ${w.message}`)
127
+ console.log(`✅ ${APP_ID}@${b.version} → ${CHANNEL} (${entries.length} files, ${Date.now() - t0}ms) — functions: ${(b.functions || []).join(', ') || 'none'}`)
128
+ console.log(` ${b.bundleBaseUrl}`)
129
+ return true
130
+ }
131
+
132
+ if (!WATCH) {
133
+ await publishOnce()
134
+ } else {
135
+ console.log(`👀 watching ${DIR} — publishing ${APP_ID} to ${CHANNEL} on change (Ctrl+C to stop)`)
136
+ await publishOnce()
137
+ let timer = null
138
+ let publishing = false
139
+ watch(DIR, { recursive: true }, () => {
140
+ clearTimeout(timer)
141
+ timer = setTimeout(async () => {
142
+ if (publishing) return
143
+ publishing = true
144
+ try { await publishOnce() } finally { publishing = false }
145
+ }, 300) // debounce a burst of file writes from one build
146
+ })
147
+ }