@bitmagic/cli 0.1.53-dev.5 → 0.1.53-dev.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 +34 -8
- package/dist/commands/publish.d.ts +7 -2
- package/dist/commands/publish.js +39 -7
- package/dist/commands/publish.js.map +1 -1
- package/dist/editor/publish-panel.d.ts +31 -0
- package/dist/editor/publish-panel.js +69 -0
- package/dist/editor/publish-panel.js.map +1 -0
- package/dist/editor/server.js +31 -0
- package/dist/editor/server.js.map +1 -1
- package/dist/editor/shell-page.js +90 -2
- package/dist/editor/shell-page.js.map +1 -1
- package/dist/publish/record.d.ts +22 -0
- package/dist/publish/record.js +62 -0
- package/dist/publish/record.js.map +1 -0
- package/dist/render/qr-output.d.ts +44 -0
- package/dist/render/qr-output.js +71 -0
- package/dist/render/qr-output.js.map +1 -0
- package/dist/render/qr-png.d.ts +37 -0
- package/dist/render/qr-png.js +111 -0
- package/dist/render/qr-png.js.map +1 -0
- package/dist/render/qr-terminal.d.ts +8 -1
- package/dist/render/qr-terminal.js +8 -1
- package/dist/render/qr-terminal.js.map +1 -1
- package/dist/scaffold/project-files.js +5 -0
- package/dist/scaffold/project-files.js.map +1 -1
- package/dist/update/check.js +2 -1
- package/dist/update/check.js.map +1 -1
- package/package.json +4 -4
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
import { decodeForgeMapHeights, renderForgeMapRgba, } from '@bitmagic/world-forger/pipeline/forge-map-render.js';
|
|
52
52
|
import { FORGE_MAP_PALETTE } from '@bitmagic/world-forger/pipeline/layout-map.js';
|
|
53
53
|
import { ASSET_DETAIL_CSS, ASSET_DETAIL_HTML } from './asset-detail-panel.js';
|
|
54
|
+
import { PUBLISH_PANEL_CSS, PUBLISH_PANEL_HTML } from './publish-panel.js';
|
|
54
55
|
import { BOOT_LOADER_CSS, BOOT_LOADER_HTML } from './boot-loader.js';
|
|
55
56
|
import { BITMAGIC_CONTROLS_CSS, BITMAGIC_FONT_LINK, BITMAGIC_TOKENS_CSS } from './design.js';
|
|
56
57
|
/** How often the shell asks the engine whether anything changed. A `Set` read; effectively free. */
|
|
@@ -228,6 +229,7 @@ ${BITMAGIC_CONTROLS_CSS}
|
|
|
228
229
|
#hq-note code { display: block; margin-top: 6px; color: var(--bm-aqua);
|
|
229
230
|
font-family: var(--bm-font-mono); word-break: break-all; }
|
|
230
231
|
${ASSET_DETAIL_CSS}
|
|
232
|
+
${PUBLISH_PANEL_CSS}
|
|
231
233
|
${BOOT_LOADER_CSS}
|
|
232
234
|
</style>
|
|
233
235
|
</head>
|
|
@@ -244,7 +246,8 @@ ${BOOT_LOADER_CSS}
|
|
|
244
246
|
<input id="auto-toggle" type="checkbox" checked> Auto-reload
|
|
245
247
|
</label>
|
|
246
248
|
<span id="status"><span id="dot"></span><span id="status-text">Loading…</span></span>
|
|
247
|
-
<button id="
|
|
249
|
+
<button id="publish-toggle" class="bm-pill" type="button" title="Where this game is published" disabled>Published</button>
|
|
250
|
+
<button id="assets-toggle" class="bm-pill" type="button" title="Show the assets in this project and what is being made">Assets</button>
|
|
248
251
|
<button id="shot" class="bm-pill" type="button" title="Copy the current frame to the clipboard">Screenshot</button>
|
|
249
252
|
<button id="reload" class="bm-pill" type="button">Reload</button>
|
|
250
253
|
</div>
|
|
@@ -293,6 +296,7 @@ ${BOOT_LOADER_HTML}
|
|
|
293
296
|
</div>
|
|
294
297
|
</div>
|
|
295
298
|
</div>
|
|
299
|
+
${PUBLISH_PANEL_HTML}
|
|
296
300
|
${ASSET_DETAIL_HTML}
|
|
297
301
|
<script>
|
|
298
302
|
(function () {
|
|
@@ -311,6 +315,16 @@ ${ASSET_DETAIL_HTML}
|
|
|
311
315
|
var dot = document.getElementById('dot');
|
|
312
316
|
var statusText = document.getElementById('status-text');
|
|
313
317
|
var banner = document.getElementById('banner');
|
|
318
|
+
var publishToggle = document.getElementById('publish-toggle');
|
|
319
|
+
var publishOverlay = document.getElementById('publish-overlay');
|
|
320
|
+
var publishUrl = document.getElementById('publish-url');
|
|
321
|
+
var publishVisibility = document.getElementById('publish-visibility');
|
|
322
|
+
var publishVersion = document.getElementById('publish-version');
|
|
323
|
+
var publishManage = document.getElementById('publish-manage');
|
|
324
|
+
var publishQr = document.getElementById('publish-qr');
|
|
325
|
+
var publishClose = document.getElementById('publish-close');
|
|
326
|
+
/** The last publish the poll saw, or null until this project has had one. */
|
|
327
|
+
var lastPublish = null;
|
|
314
328
|
var terrainBanner = document.getElementById('terrain-banner');
|
|
315
329
|
var buildBanner = document.getElementById('build-banner');
|
|
316
330
|
var cliBanner = document.getElementById('cli-banner');
|
|
@@ -947,10 +961,17 @@ ${ASSET_DETAIL_HTML}
|
|
|
947
961
|
* there is no replay. This poll is what closes that hole, and it costs one stat() per tick.
|
|
948
962
|
*/
|
|
949
963
|
async function checkExternalEdits() {
|
|
950
|
-
if (
|
|
964
|
+
if (saving) return;
|
|
951
965
|
try {
|
|
952
966
|
var state = await (await fetch('/api/state')).json();
|
|
953
967
|
showCliUpdate(state.cliUpdate);
|
|
968
|
+
showPublish(state.publish);
|
|
969
|
+
// Only the mtime comparison below needs the game to have booted — it is what establishes
|
|
970
|
+
// lastKnownMtime, and without it every tick would read as someone else's edit. The two
|
|
971
|
+
// surfaces above need nothing from the game, and a game that will not boot is exactly when
|
|
972
|
+
// a creator most wants the published link and the update banner: the last good build is
|
|
973
|
+
// still live at that URL, and this is where they can reach it.
|
|
974
|
+
if (!loaded) return;
|
|
954
975
|
// Anything other than the mtime our own last write produced is someone else's edit — most
|
|
955
976
|
// likely the creator's agent.
|
|
956
977
|
if (state.worldMtimeMs && state.worldMtimeMs !== lastKnownMtime
|
|
@@ -978,6 +999,67 @@ ${ASSET_DETAIL_HTML}
|
|
|
978
999
|
cliBanner.classList.add('show');
|
|
979
1000
|
}
|
|
980
1001
|
|
|
1002
|
+
/**
|
|
1003
|
+
* Where this game is published, if it is.
|
|
1004
|
+
*
|
|
1005
|
+
* Driven by the poll for the same reason the banner above is: this page is rendered once when the
|
|
1006
|
+
* server starts, and the publish it should show almost always happens later, in another terminal
|
|
1007
|
+
* \u2014 often an agent's, which the creator never looks at. Polling means the pill lights up on
|
|
1008
|
+
* its own within a tick of the publish finishing, with no reload.
|
|
1009
|
+
*
|
|
1010
|
+
* The record is only stored, never rendered here, so a session that never opens the dialog never
|
|
1011
|
+
* asks the sidecar to encode a QR.
|
|
1012
|
+
*/
|
|
1013
|
+
function showPublish(record) {
|
|
1014
|
+
lastPublish = record || null;
|
|
1015
|
+
publishToggle.disabled = !lastPublish;
|
|
1016
|
+
publishToggle.title = lastPublish
|
|
1017
|
+
? 'Where this game is published'
|
|
1018
|
+
: 'Publish the game to get a link and a QR code';
|
|
1019
|
+
if (!lastPublish && publishOverlay.classList.contains('show')) closePublish();
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function openPublish() {
|
|
1023
|
+
if (!lastPublish) return;
|
|
1024
|
+
publishUrl.textContent = lastPublish.url;
|
|
1025
|
+
publishUrl.href = lastPublish.url;
|
|
1026
|
+
publishVisibility.textContent = lastPublish.visibility;
|
|
1027
|
+
// Public is the state a creator chose and can be pleased about; private is the honest
|
|
1028
|
+
// reminder that nobody else can find it yet.
|
|
1029
|
+
publishVisibility.className = lastPublish.visibility === 'public'
|
|
1030
|
+
? 'bm-badge bm-badge--success'
|
|
1031
|
+
: 'bm-badge';
|
|
1032
|
+
publishVersion.textContent = 'publish ' + lastPublish.publishVersion + describeAge(lastPublish.at);
|
|
1033
|
+
publishManage.style.display = lastPublish.manageUrl ? '' : 'none';
|
|
1034
|
+
if (lastPublish.manageUrl) publishManage.href = lastPublish.manageUrl;
|
|
1035
|
+
// Cache-busted per open: a later publish keeps the same URL for an alias but not for a
|
|
1036
|
+
// versioned one, and a browser holding the old code would send a phone to the previous build.
|
|
1037
|
+
publishQr.src = '/api/publish/qr.png?v=' + encodeURIComponent(lastPublish.at);
|
|
1038
|
+
publishOverlay.classList.add('show');
|
|
1039
|
+
publishClose.focus();
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function closePublish() {
|
|
1043
|
+
publishOverlay.classList.remove('show');
|
|
1044
|
+
// Drop the image so a session that publishes repeatedly does not keep every code it drew.
|
|
1045
|
+
publishQr.removeAttribute('src');
|
|
1046
|
+
publishToggle.focus();
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/** ', 4 minutes ago' and so on. Empty when the stamp is unreadable rather than a wrong claim. */
|
|
1050
|
+
function describeAge(at) {
|
|
1051
|
+
var then = Date.parse(at);
|
|
1052
|
+
if (!then) return '';
|
|
1053
|
+
var seconds = Math.max(0, Math.round((Date.now() - then) / 1000));
|
|
1054
|
+
if (seconds < 90) return ', just now';
|
|
1055
|
+
var minutes = Math.round(seconds / 60);
|
|
1056
|
+
if (minutes < 60) return ', ' + minutes + ' minutes ago';
|
|
1057
|
+
var hours = Math.round(minutes / 60);
|
|
1058
|
+
if (hours < 24) return hours === 1 ? ', an hour ago' : ', ' + hours + ' hours ago';
|
|
1059
|
+
var days = Math.round(hours / 24);
|
|
1060
|
+
return days === 1 ? ', yesterday' : ', ' + days + ' days ago';
|
|
1061
|
+
}
|
|
1062
|
+
|
|
981
1063
|
/**
|
|
982
1064
|
* Something changed on disk. Reload, or explain why we did not.
|
|
983
1065
|
*
|
|
@@ -1939,6 +2021,12 @@ ${ASSET_DETAIL_HTML}
|
|
|
1939
2021
|
if (autoReload) banner.classList.remove('show');
|
|
1940
2022
|
});
|
|
1941
2023
|
|
|
2024
|
+
publishToggle.addEventListener('click', openPublish);
|
|
2025
|
+
publishClose.addEventListener('click', closePublish);
|
|
2026
|
+
// Click the backdrop to dismiss, like every other dialog on this page.
|
|
2027
|
+
publishOverlay.addEventListener('click', function (event) {
|
|
2028
|
+
if (event.target === publishOverlay) closePublish();
|
|
2029
|
+
});
|
|
1942
2030
|
assetsToggle.addEventListener('click', function () { setAssetsOpen(!assetsOpen); });
|
|
1943
2031
|
|
|
1944
2032
|
autoReload = readPref(AUTO_KEY, '1') !== '0';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell-page.js","sourceRoot":"","sources":["../../src/editor/shell-page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAEH,OAAO,EACL,qBAAqB,EAAE,kBAAkB,GAC1C,MAAM,qDAAqD,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,+CAA+C,CAAC;AAClF,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC9E,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAE7F,oGAAoG;AACpG,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAE7B;;;;;;GAMG;AACH,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAE7B;;;;;;;;;;;;GAYG;AACH,SAAS,sBAAsB;IAC7B,OAAO;QACL,6BAA6B,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC,GAAG;QACjE,iCAAiC,qBAAqB,CAAC,QAAQ,EAAE,GAAG;QACpE,8BAA8B,kBAAkB,CAAC,QAAQ,EAAE,GAAG;KAC/D,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AACjB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAEnC,kGAAkG;AAClG,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,KAAK;SACT,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC;AASD,MAAM,UAAU,iBAAiB,CAAC,OAA2B;IAC3D,MAAM,OAAO,GAAG,oBAAoB,OAAO,CAAC,QAAQ,2BAA2B,kBAAkB,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;IACpH,OAAO;;;;wBAIe,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC;EAChD,kBAAkB;;EAElB,mBAAmB;EACnB,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsHrB,gBAAgB;EAChB,eAAe;;;;;;uBAMM,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC
|
|
1
|
+
{"version":3,"file":"shell-page.js","sourceRoot":"","sources":["../../src/editor/shell-page.ts"],"names":[],"mappings":"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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAysD3B,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare function publishRecordPath(root: string): string;
|
|
2
|
+
export interface PublishRecord {
|
|
3
|
+
/** The public URL of the game, exactly as api-server issued it. */
|
|
4
|
+
url: string;
|
|
5
|
+
visibility: string;
|
|
6
|
+
publishVersion: number;
|
|
7
|
+
gameId: string;
|
|
8
|
+
/** ISO 8601, so a reader can say how long ago without a second field. */
|
|
9
|
+
at: string;
|
|
10
|
+
/** The My games page. Absent against an older api-server that does not send one. */
|
|
11
|
+
manageUrl?: string;
|
|
12
|
+
/** The name this publish shipped under, when the publish knew it. */
|
|
13
|
+
name?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function writePublishRecord(root: string, record: PublishRecord): void;
|
|
16
|
+
/**
|
|
17
|
+
* The last publish, or null when there has not been one this reader can trust.
|
|
18
|
+
*
|
|
19
|
+
* Every field is checked rather than assumed: this file survives across CLI versions, and a reader
|
|
20
|
+
* that trusted a half-written or older-shaped record would put a broken link in front of a creator.
|
|
21
|
+
*/
|
|
22
|
+
export declare function readPublishRecord(root: string): PublishRecord | null;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The on-disk record of the last successful `bitmagic publish`.
|
|
3
|
+
*
|
|
4
|
+
* Publishing used to print the game's URL and forget it. That was fine while the terminal was the
|
|
5
|
+
* only place the URL was ever wanted, and stopped being fine as soon as anything else needed to
|
|
6
|
+
* answer "where is this game?" — the `bitmagic dev` view being the first, since it is the window
|
|
7
|
+
* the creator already has open while they work.
|
|
8
|
+
*
|
|
9
|
+
* Lives at `.bitmagic/publish/last.json`, beside `source.zip`, which is where the rest of a
|
|
10
|
+
* publish's local artifacts already are. Inside `.bitmagic` means gitignored and excluded from the
|
|
11
|
+
* publish fingerprint (`EXCLUDED_DIRS` in `@bitmagic/asset-core/publish-source`), so recording a
|
|
12
|
+
* publish cannot invalidate the verify that gated it — the same reason the QR image lives there.
|
|
13
|
+
*
|
|
14
|
+
* Written only after `complete`, so its presence means the game really is live at that URL. It is
|
|
15
|
+
* NOT a cache and nothing gates on it: a missing or unreadable record costs a link in a panel, and
|
|
16
|
+
* every reader here treats that as normal rather than as an error.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
const PUBLISH_RECORD_FILE = path.join('.bitmagic', 'publish', 'last.json');
|
|
21
|
+
export function publishRecordPath(root) {
|
|
22
|
+
return path.join(root, PUBLISH_RECORD_FILE);
|
|
23
|
+
}
|
|
24
|
+
export function writePublishRecord(root, record) {
|
|
25
|
+
fs.mkdirSync(path.dirname(publishRecordPath(root)), { recursive: true });
|
|
26
|
+
fs.writeFileSync(publishRecordPath(root), `${JSON.stringify(record, null, 2)}\n`);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The last publish, or null when there has not been one this reader can trust.
|
|
30
|
+
*
|
|
31
|
+
* Every field is checked rather than assumed: this file survives across CLI versions, and a reader
|
|
32
|
+
* that trusted a half-written or older-shaped record would put a broken link in front of a creator.
|
|
33
|
+
*/
|
|
34
|
+
export function readPublishRecord(root) {
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(fs.readFileSync(publishRecordPath(root), 'utf-8'));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
43
|
+
return null;
|
|
44
|
+
const record = parsed;
|
|
45
|
+
if (typeof record.url !== 'string'
|
|
46
|
+
|| typeof record.visibility !== 'string'
|
|
47
|
+
|| typeof record.publishVersion !== 'number'
|
|
48
|
+
|| typeof record.gameId !== 'string'
|
|
49
|
+
|| typeof record.at !== 'string') {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
url: record.url,
|
|
54
|
+
visibility: record.visibility,
|
|
55
|
+
publishVersion: record.publishVersion,
|
|
56
|
+
gameId: record.gameId,
|
|
57
|
+
at: record.at,
|
|
58
|
+
...(typeof record.manageUrl === 'string' ? { manageUrl: record.manageUrl } : {}),
|
|
59
|
+
...(typeof record.name === 'string' ? { name: record.name } : {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record.js","sourceRoot":"","sources":["../../src/publish/record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAE7B,MAAM,mBAAmB,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;AAE3E,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAC;AAC9C,CAAC;AAgBD,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,MAAqB;IACpE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzE,EAAE,CAAC,aAAa,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AACpF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IACzE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC/D,MAAM,MAAM,GAAG,MAAiC,CAAC;IACjD,IACE,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ;WAC3B,OAAO,MAAM,CAAC,UAAU,KAAK,QAAQ;WACrC,OAAO,MAAM,CAAC,cAAc,KAAK,QAAQ;WACzC,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ;WACjC,OAAO,MAAM,CAAC,EAAE,KAAK,QAAQ,EAChC,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO;QACL,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,EAAE,EAAE,MAAM,CAAC,EAAE;QACb,GAAG,CAAC,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChF,GAAG,CAAC,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClE,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a publish's QR code goes: the terminal when someone is looking at it, a file when nobody
|
|
3
|
+
* is.
|
|
4
|
+
*
|
|
5
|
+
* This module exists because the two are one decision, not two. `qr-terminal.ts` refuses a
|
|
6
|
+
* non-TTY for a good reason — escape sequences in a captured stream are noise — but refusing to
|
|
7
|
+
* DRAW is not the same as having nothing to offer, and for this CLI the captured stream is the
|
|
8
|
+
* common case. The pro lane is driven by an agent, so stdout is a pipe on almost every real
|
|
9
|
+
* publish, and the creator who would scan the code never sees the terminal at all. Gating the
|
|
10
|
+
* feature on `isTTY` alone turned it off exactly where the URL is most worth handing over.
|
|
11
|
+
*
|
|
12
|
+
* So: draw it if it can be drawn, and write `.bitmagic/publish-qr.png` whenever it cannot. That
|
|
13
|
+
* covers the pipe, and also the two smaller cases with the same shape — `TERM=dumb`, and a
|
|
14
|
+
* terminal too narrow to hold the code unwrapped — where a person is present but still has nothing
|
|
15
|
+
* to scan. A terminal that draws successfully writes no file: it already did the job.
|
|
16
|
+
*
|
|
17
|
+
* `.bitmagic/` is excluded from the publish fingerprint (`EXCLUDED_DIRS` in
|
|
18
|
+
* `@bitmagic/asset-core/publish-source`), so writing here cannot invalidate the verify the gate
|
|
19
|
+
* just checked.
|
|
20
|
+
*/
|
|
21
|
+
import { type TerminalQrEnv } from './qr-terminal.js';
|
|
22
|
+
/** Where a publish leaves its QR image. One per project, overwritten each publish. */
|
|
23
|
+
export declare function publishQrPath(root: string): string;
|
|
24
|
+
export interface EmitQrOptions {
|
|
25
|
+
/** The project root. The image lands in its `.bitmagic/`. */
|
|
26
|
+
root: string;
|
|
27
|
+
/** `--json`: stdout carries exactly one document, so nothing is ever drawn. */
|
|
28
|
+
json: boolean;
|
|
29
|
+
/** `--no-qr`. Suppresses the drawing AND the file — it means "not this time", not "not here". */
|
|
30
|
+
disabled: boolean;
|
|
31
|
+
env?: TerminalQrEnv;
|
|
32
|
+
isTTY?: boolean;
|
|
33
|
+
columns?: number | undefined;
|
|
34
|
+
write?: (chunk: string) => void;
|
|
35
|
+
/** Seam for the test; production writes to disk. */
|
|
36
|
+
writeFile?: (file: string, bytes: Buffer) => void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Show `url` as a QR code however this run can, and return the image path when one was written.
|
|
40
|
+
*
|
|
41
|
+
* Never throws and never reports failure as an error: the caller has already printed the URL, and
|
|
42
|
+
* a code that could not be produced costs the creator a retype, not a publish.
|
|
43
|
+
*/
|
|
44
|
+
export declare function emitQr(url: string, options: EmitQrOptions): string | null;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a publish's QR code goes: the terminal when someone is looking at it, a file when nobody
|
|
3
|
+
* is.
|
|
4
|
+
*
|
|
5
|
+
* This module exists because the two are one decision, not two. `qr-terminal.ts` refuses a
|
|
6
|
+
* non-TTY for a good reason — escape sequences in a captured stream are noise — but refusing to
|
|
7
|
+
* DRAW is not the same as having nothing to offer, and for this CLI the captured stream is the
|
|
8
|
+
* common case. The pro lane is driven by an agent, so stdout is a pipe on almost every real
|
|
9
|
+
* publish, and the creator who would scan the code never sees the terminal at all. Gating the
|
|
10
|
+
* feature on `isTTY` alone turned it off exactly where the URL is most worth handing over.
|
|
11
|
+
*
|
|
12
|
+
* So: draw it if it can be drawn, and write `.bitmagic/publish-qr.png` whenever it cannot. That
|
|
13
|
+
* covers the pipe, and also the two smaller cases with the same shape — `TERM=dumb`, and a
|
|
14
|
+
* terminal too narrow to hold the code unwrapped — where a person is present but still has nothing
|
|
15
|
+
* to scan. A terminal that draws successfully writes no file: it already did the job.
|
|
16
|
+
*
|
|
17
|
+
* `.bitmagic/` is excluded from the publish fingerprint (`EXCLUDED_DIRS` in
|
|
18
|
+
* `@bitmagic/asset-core/publish-source`), so writing here cannot invalidate the verify the gate
|
|
19
|
+
* just checked.
|
|
20
|
+
*/
|
|
21
|
+
import * as fs from 'fs';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
import { encodeQr } from './qr.js';
|
|
24
|
+
import { encodeQrPng } from './qr-png.js';
|
|
25
|
+
import { renderTerminalQr } from './qr-terminal.js';
|
|
26
|
+
/** Where a publish leaves its QR image. One per project, overwritten each publish. */
|
|
27
|
+
export function publishQrPath(root) {
|
|
28
|
+
return path.join(root, '.bitmagic', 'publish-qr.png');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Show `url` as a QR code however this run can, and return the image path when one was written.
|
|
32
|
+
*
|
|
33
|
+
* Never throws and never reports failure as an error: the caller has already printed the URL, and
|
|
34
|
+
* a code that could not be produced costs the creator a retype, not a publish.
|
|
35
|
+
*/
|
|
36
|
+
export function emitQr(url, options) {
|
|
37
|
+
const env = options.env ?? process.env;
|
|
38
|
+
if (options.disabled || env.BITMAGIC_NO_QR)
|
|
39
|
+
return null;
|
|
40
|
+
// Drawing is tried first and, when it works, is the whole answer — a file nobody opens is
|
|
41
|
+
// clutter in a directory the creator does read.
|
|
42
|
+
if (!options.json) {
|
|
43
|
+
const drawn = renderTerminalQr(url, {
|
|
44
|
+
env,
|
|
45
|
+
...(options.isTTY === undefined ? {} : { isTTY: options.isTTY }),
|
|
46
|
+
columns: options.columns,
|
|
47
|
+
...(options.write === undefined ? {} : { write: options.write }),
|
|
48
|
+
});
|
|
49
|
+
if (drawn)
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const matrix = encodeQr(url);
|
|
53
|
+
if (matrix === null)
|
|
54
|
+
return null;
|
|
55
|
+
const file = publishQrPath(options.root);
|
|
56
|
+
try {
|
|
57
|
+
if (options.writeFile) {
|
|
58
|
+
options.writeFile(file, encodeQrPng(matrix));
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
62
|
+
fs.writeFileSync(file, encodeQrPng(matrix));
|
|
63
|
+
}
|
|
64
|
+
return file;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// A read-only or full disk is not worth failing a successful publish over.
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=qr-output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qr-output.js","sourceRoot":"","sources":["../../src/render/qr-output.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAsB,MAAM,kBAAkB,CAAC;AAExE,sFAAsF;AACtF,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,gBAAgB,CAAC,CAAC;AACxD,CAAC;AAiBD;;;;;GAKG;AACH,MAAM,UAAU,MAAM,CAAC,GAAW,EAAE,OAAsB;IACxD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACvC,IAAI,OAAO,CAAC,QAAQ,IAAI,GAAG,CAAC,cAAc;QAAE,OAAO,IAAI,CAAC;IAExD,0FAA0F;IAC1F,gDAAgD;IAChD,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QAClB,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,EAAE;YAClC,GAAG;YACH,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;YAChE,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;SACjE,CAAC,CAAC;QACH,IAAI,KAAK;YAAE,OAAO,IAAI,CAAC;IACzB,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAEjC,MAAM,IAAI,GAAG,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,IAAI,CAAC;QACH,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;YACtB,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;QAC/C,CAAC;aAAM,CAAC;YACN,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACtD,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;QAC9C,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,2EAA2E;QAC3E,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A QR code as PNG bytes, for the creator who cannot scan the terminal.
|
|
3
|
+
*
|
|
4
|
+
* `qr-terminal.ts` covers the case where a person is looking at the output. That is not the common
|
|
5
|
+
* case for this CLI: the pro lane is driven by an agent, so stdout is a pipe, and the human who
|
|
6
|
+
* would do the scanning is on the far side of it. A file crosses that boundary where escape
|
|
7
|
+
* sequences cannot — an agent can hand an image to its creator, and cannot hand them a repainted
|
|
8
|
+
* terminal.
|
|
9
|
+
*
|
|
10
|
+
* Greyscale, one byte per pixel. A QR carries one bit per module, so colour would be three bytes of
|
|
11
|
+
* nothing; and at this size the whole image deflates to a few hundred bytes either way. The
|
|
12
|
+
* encoder is hand-rolled for the same reason `qr.ts` is — `@bitmagic/cli` ships four runtime
|
|
13
|
+
* dependencies on purpose, and node's own `zlib` (already used in `publish/` and `assets/`) does
|
|
14
|
+
* the only part that is genuinely hard.
|
|
15
|
+
*
|
|
16
|
+
* Two other hand-rolled PNG encoders exist in this repo — `world-forger-internal`'s vehicle atlas
|
|
17
|
+
* and `game-play-agent`'s solid-colour tile — and neither is reachable from here: the first is in a
|
|
18
|
+
* `private: true` package the CLI does not depend on, the second is server code. A shared home
|
|
19
|
+
* would have to be `@bitmagic/asset-core`; that is worth doing when something needs the third copy,
|
|
20
|
+
* not for the second.
|
|
21
|
+
*/
|
|
22
|
+
import { Buffer } from 'buffer';
|
|
23
|
+
export interface QrPngOptions {
|
|
24
|
+
/** Pixels per module. Defaults to `MODULE_PIXELS`. */
|
|
25
|
+
scale?: number;
|
|
26
|
+
/** Quiet zone in modules. Defaults to `QUIET_MODULES`. */
|
|
27
|
+
quietZone?: number;
|
|
28
|
+
}
|
|
29
|
+
/** The pixel width (and height) `encodeQrPng` produces for a matrix of this size. */
|
|
30
|
+
export declare function qrPngDimension(matrix: boolean[][], options?: QrPngOptions): number;
|
|
31
|
+
/**
|
|
32
|
+
* Encode a module matrix as a greyscale PNG: dark modules black, everything else white.
|
|
33
|
+
*
|
|
34
|
+
* White is 0xff and dark is 0x00, never inverted — a scanner wants dark on light, and unlike the
|
|
35
|
+
* terminal there is no theme here to fight about it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function encodeQrPng(matrix: boolean[][], options?: QrPngOptions): Buffer;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A QR code as PNG bytes, for the creator who cannot scan the terminal.
|
|
3
|
+
*
|
|
4
|
+
* `qr-terminal.ts` covers the case where a person is looking at the output. That is not the common
|
|
5
|
+
* case for this CLI: the pro lane is driven by an agent, so stdout is a pipe, and the human who
|
|
6
|
+
* would do the scanning is on the far side of it. A file crosses that boundary where escape
|
|
7
|
+
* sequences cannot — an agent can hand an image to its creator, and cannot hand them a repainted
|
|
8
|
+
* terminal.
|
|
9
|
+
*
|
|
10
|
+
* Greyscale, one byte per pixel. A QR carries one bit per module, so colour would be three bytes of
|
|
11
|
+
* nothing; and at this size the whole image deflates to a few hundred bytes either way. The
|
|
12
|
+
* encoder is hand-rolled for the same reason `qr.ts` is — `@bitmagic/cli` ships four runtime
|
|
13
|
+
* dependencies on purpose, and node's own `zlib` (already used in `publish/` and `assets/`) does
|
|
14
|
+
* the only part that is genuinely hard.
|
|
15
|
+
*
|
|
16
|
+
* Two other hand-rolled PNG encoders exist in this repo — `world-forger-internal`'s vehicle atlas
|
|
17
|
+
* and `game-play-agent`'s solid-colour tile — and neither is reachable from here: the first is in a
|
|
18
|
+
* `private: true` package the CLI does not depend on, the second is server code. A shared home
|
|
19
|
+
* would have to be `@bitmagic/asset-core`; that is worth doing when something needs the third copy,
|
|
20
|
+
* not for the second.
|
|
21
|
+
*/
|
|
22
|
+
import { Buffer } from 'buffer';
|
|
23
|
+
import * as zlib from 'zlib';
|
|
24
|
+
/** Pixels per QR module. Large enough that a phone camera reads it off a normal screen. */
|
|
25
|
+
const MODULE_PIXELS = 8;
|
|
26
|
+
/**
|
|
27
|
+
* Quiet zone in modules. Four, the value the spec asks for, rather than the two `qr-terminal.ts`
|
|
28
|
+
* uses — that one is trading margin for terminal columns, and a file has no such constraint.
|
|
29
|
+
*/
|
|
30
|
+
const QUIET_MODULES = 4;
|
|
31
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
32
|
+
/** The standard CRC-32 table (PNG spec annex D), built once. */
|
|
33
|
+
function buildCrcTable() {
|
|
34
|
+
const table = new Uint32Array(256);
|
|
35
|
+
for (let n = 0; n < 256; n += 1) {
|
|
36
|
+
let c = n;
|
|
37
|
+
for (let k = 0; k < 8; k += 1) {
|
|
38
|
+
c = (c & 1) !== 0 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
39
|
+
}
|
|
40
|
+
table[n] = c >>> 0;
|
|
41
|
+
}
|
|
42
|
+
return table;
|
|
43
|
+
}
|
|
44
|
+
const CRC_TABLE = buildCrcTable();
|
|
45
|
+
function crc32(bytes) {
|
|
46
|
+
let crc = 0xffffffff;
|
|
47
|
+
for (const byte of bytes) {
|
|
48
|
+
crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
49
|
+
}
|
|
50
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
51
|
+
}
|
|
52
|
+
/** One PNG chunk: length, type, data, and the CRC over type+data (never over the length). */
|
|
53
|
+
function chunk(type, data) {
|
|
54
|
+
const length = Buffer.alloc(4);
|
|
55
|
+
length.writeUInt32BE(data.length, 0);
|
|
56
|
+
const typeAndData = Buffer.concat([Buffer.from(type, 'ascii'), data]);
|
|
57
|
+
const crc = Buffer.alloc(4);
|
|
58
|
+
crc.writeUInt32BE(crc32(typeAndData), 0);
|
|
59
|
+
return Buffer.concat([length, typeAndData, crc]);
|
|
60
|
+
}
|
|
61
|
+
/** The pixel width (and height) `encodeQrPng` produces for a matrix of this size. */
|
|
62
|
+
export function qrPngDimension(matrix, options = {}) {
|
|
63
|
+
const scale = options.scale ?? MODULE_PIXELS;
|
|
64
|
+
const quiet = options.quietZone ?? QUIET_MODULES;
|
|
65
|
+
return (matrix.length + quiet * 2) * scale;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Encode a module matrix as a greyscale PNG: dark modules black, everything else white.
|
|
69
|
+
*
|
|
70
|
+
* White is 0xff and dark is 0x00, never inverted — a scanner wants dark on light, and unlike the
|
|
71
|
+
* terminal there is no theme here to fight about it.
|
|
72
|
+
*/
|
|
73
|
+
export function encodeQrPng(matrix, options = {}) {
|
|
74
|
+
const scale = options.scale ?? MODULE_PIXELS;
|
|
75
|
+
const quiet = options.quietZone ?? QUIET_MODULES;
|
|
76
|
+
const dimension = qrPngDimension(matrix, options);
|
|
77
|
+
// One filter byte per scanline (0 = None: a QR is flat colour, so the other filters buy nothing
|
|
78
|
+
// that deflate does not already get from the runs), then one byte per pixel.
|
|
79
|
+
const raw = Buffer.alloc(dimension * (dimension + 1), 0xff);
|
|
80
|
+
for (let y = 0; y < dimension; y += 1) {
|
|
81
|
+
const rowStart = y * (dimension + 1);
|
|
82
|
+
raw[rowStart] = 0;
|
|
83
|
+
const moduleRow = Math.floor(y / scale) - quiet;
|
|
84
|
+
if (moduleRow < 0 || moduleRow >= matrix.length)
|
|
85
|
+
continue;
|
|
86
|
+
for (let x = 0; x < dimension; x += 1) {
|
|
87
|
+
const moduleCol = Math.floor(x / scale) - quiet;
|
|
88
|
+
if (moduleCol < 0 || moduleCol >= matrix.length)
|
|
89
|
+
continue;
|
|
90
|
+
if (matrix[moduleRow][moduleCol])
|
|
91
|
+
raw[rowStart + 1 + x] = 0x00;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const ihdr = Buffer.alloc(13);
|
|
95
|
+
ihdr.writeUInt32BE(dimension, 0);
|
|
96
|
+
ihdr.writeUInt32BE(dimension, 4);
|
|
97
|
+
ihdr[8] = 8; // bit depth
|
|
98
|
+
ihdr[9] = 0; // colour type 0 — greyscale
|
|
99
|
+
ihdr[10] = 0; // compression: deflate, the only value PNG defines
|
|
100
|
+
ihdr[11] = 0; // filter method: adaptive, the only value PNG defines
|
|
101
|
+
ihdr[12] = 0; // interlace: none
|
|
102
|
+
return Buffer.concat([
|
|
103
|
+
PNG_SIGNATURE,
|
|
104
|
+
chunk('IHDR', ihdr),
|
|
105
|
+
// deflateSync, not fflate's — fflate's `deflateSync` emits RAW deflate with no zlib header,
|
|
106
|
+
// and an IDAT must carry the zlib wrapper. (fflate's `zlibSync` would also do.)
|
|
107
|
+
chunk('IDAT', zlib.deflateSync(raw)),
|
|
108
|
+
chunk('IEND', Buffer.alloc(0)),
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=qr-png.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qr-png.js","sourceRoot":"","sources":["../../src/render/qr-png.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
* never replaces a line and it never reports failure as an error, because an agent tool is usually
|
|
11
11
|
* what reads this output.
|
|
12
12
|
*
|
|
13
|
+
* That last clause is also why nothing should call this module directly. Refusing to DRAW into a
|
|
14
|
+
* pipe is right; having nothing to offer when there is one is not, and since an agent drives most
|
|
15
|
+
* publishes, the pipe is the common case rather than the edge. `qr-output.ts` owns that decision —
|
|
16
|
+
* it asks this module first and falls back to a PNG file — and `bitmagic publish` goes through it.
|
|
17
|
+
*
|
|
13
18
|
* Where it DIVERGES from that file is the gate. `supportsInlineImage` is an allowlist of terminals
|
|
14
19
|
* known to implement the iTerm2 protocol, because the ones that do not print a screenful of raw
|
|
15
20
|
* base64. Nothing here is exotic: half-block characters and SGR colour are universal, and tmux
|
|
@@ -24,7 +29,9 @@ export interface TerminalQrEnv {
|
|
|
24
29
|
* Whether this terminal can show a scannable code at all.
|
|
25
30
|
*
|
|
26
31
|
* Not an allowlist — see the file header. `TERM=dumb` is the one terminal that answers no on its
|
|
27
|
-
* own behalf, and a non-TTY means something is reading this rather than looking at it
|
|
32
|
+
* own behalf, and a non-TTY means something is reading this rather than looking at it — which is a
|
|
33
|
+
* reason not to paint escape sequences, NOT a reason to withhold the code. `qr-output.ts` writes an
|
|
34
|
+
* image for every case this function turns down.
|
|
28
35
|
*/
|
|
29
36
|
export declare function supportsTerminalQr(env: TerminalQrEnv, isTTY: boolean): boolean;
|
|
30
37
|
/** How many columns the drawn code occupies, quiet zone included. */
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
* never replaces a line and it never reports failure as an error, because an agent tool is usually
|
|
11
11
|
* what reads this output.
|
|
12
12
|
*
|
|
13
|
+
* That last clause is also why nothing should call this module directly. Refusing to DRAW into a
|
|
14
|
+
* pipe is right; having nothing to offer when there is one is not, and since an agent drives most
|
|
15
|
+
* publishes, the pipe is the common case rather than the edge. `qr-output.ts` owns that decision —
|
|
16
|
+
* it asks this module first and falls back to a PNG file — and `bitmagic publish` goes through it.
|
|
17
|
+
*
|
|
13
18
|
* Where it DIVERGES from that file is the gate. `supportsInlineImage` is an allowlist of terminals
|
|
14
19
|
* known to implement the iTerm2 protocol, because the ones that do not print a screenful of raw
|
|
15
20
|
* base64. Nothing here is exotic: half-block characters and SGR colour are universal, and tmux
|
|
@@ -51,7 +56,9 @@ const BOTH_LIGHT = ' ';
|
|
|
51
56
|
* Whether this terminal can show a scannable code at all.
|
|
52
57
|
*
|
|
53
58
|
* Not an allowlist — see the file header. `TERM=dumb` is the one terminal that answers no on its
|
|
54
|
-
* own behalf, and a non-TTY means something is reading this rather than looking at it
|
|
59
|
+
* own behalf, and a non-TTY means something is reading this rather than looking at it — which is a
|
|
60
|
+
* reason not to paint escape sequences, NOT a reason to withhold the code. `qr-output.ts` writes an
|
|
61
|
+
* image for every case this function turns down.
|
|
55
62
|
*/
|
|
56
63
|
export function supportsTerminalQr(env, isTTY) {
|
|
57
64
|
if (!isTTY)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qr-terminal.js","sourceRoot":"","sources":["../../src/render/qr-terminal.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"qr-terminal.js","sourceRoot":"","sources":["../../src/render/qr-terminal.ts"],"names":[],"mappings":"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"}
|
|
@@ -778,6 +778,11 @@ Follow this order; \`publish\` enforces it and refuses otherwise:
|
|
|
778
778
|
3. Run \`bitmagic publish\`. It rebuilds automatically if the project changed since the last
|
|
779
779
|
build, so a manual \`bitmagic build\` first is optional.
|
|
780
780
|
|
|
781
|
+
**Show your creator the QR code.** Publish turns the game's URL into a scannable image. Your
|
|
782
|
+
output is a pipe, so it cannot be drawn in the terminal: publish writes \`.bitmagic/publish-qr.png\`
|
|
783
|
+
and names it (\`qrPath\` under \`--json\`). Surface that image like any artifact you made — scanning
|
|
784
|
+
it opens the game on a phone, and a bare URL leaves your creator retyping a game id into a browser.
|
|
785
|
+
|
|
781
786
|
**A bare \`bitmagic publish\` does not make the game public.** Every publish uploads and
|
|
782
787
|
registers the game, but \`visibility\` defaults to \`private\` — reachable by URL, not listed on
|
|
783
788
|
bitmagic.ai. Pass \`--visibility public\` explicitly, on that call, to list it; omit it on a later
|