tileflow 0.1.0-alpha.10 → 0.1.0-alpha.12
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 +20 -3
- package/dist/index.js +364 -212
- package/docs/cli-setup-and-defaults.md +2 -2
- package/docs/command-families.md +4 -4
- package/docs/native-artifacts.md +1 -1
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -23,8 +23,8 @@ executable. With another package manager, use its local-execution command and th
|
|
|
23
23
|
do not add a second lockfile or rely on a globally installed CLI.
|
|
24
24
|
|
|
25
25
|
`init` creates a singular `tileflow.config.ts` extending the official Streets map. `preview` serves
|
|
26
|
-
the exported map using its configured view
|
|
27
|
-
|
|
26
|
+
the exported map using its configured view; it does not replace an application's normal development
|
|
27
|
+
server.
|
|
28
28
|
|
|
29
29
|
Local validation, compilation, and asset preparation require no account or API key. Preview and
|
|
30
30
|
capture may fetch configured remote tiles and fonts. Config files are executable trusted code,
|
|
@@ -151,6 +151,23 @@ on that route. Capture does not start the server or discover ports. It accepts l
|
|
|
151
151
|
selects exactly one target, verifies the concrete theme, and waits for the component's idle marker.
|
|
152
152
|
Do not run a separate `tileflow preview` server beside the application.
|
|
153
153
|
|
|
154
|
+
## Render a timed shot
|
|
155
|
+
|
|
156
|
+
Commit a [capture shot](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/capture-shots.md)
|
|
157
|
+
under `shots` on the same map: a concrete theme, a viewport, and two or more center-camera
|
|
158
|
+
keyframes, optionally with a theme blend. Then render it:
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
npx tileflow capture --shot dusk-flyover --json
|
|
162
|
+
npx tileflow capture --shot dusk-flyover --format frames --out-dir evidence --json
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The first command writes `.tileflow/captures/shots/dusk-flyover.webm`; the second writes
|
|
166
|
+
`evidence/dusk-flyover/frame-0000.png` and the following frames. Each output has a sibling
|
|
167
|
+
`.receipt.json` shot receipt. Repeat `--shot` or use `--all-shots` to render several shots; `--out`
|
|
168
|
+
names the `.webm` file or frame directory of a single shot. Every frame waits for the map to be
|
|
169
|
+
ready, so a slow machine renders more slowly without dropping frames.
|
|
170
|
+
|
|
154
171
|
## Review pixels without accepting changes
|
|
155
172
|
|
|
156
173
|
After reviewing a render, create its baseline. Compare subsequent renders separately:
|
|
@@ -264,7 +281,7 @@ for the verified platform, setup, and development-artifact restrictions.
|
|
|
264
281
|
|
|
265
282
|
Run `npx tileflow --help` or `npx tileflow <command> --help` for the installed command surface.
|
|
266
283
|
The families cover map authoring, semantic language discovery, local visual evidence, datasets and
|
|
267
|
-
icons, account sessions, coordinates, and hosted delivery. Hidden
|
|
284
|
+
icons, account sessions, coordinates, and hosted delivery. Hidden support commands are not a
|
|
268
285
|
recommended application workflow.
|
|
269
286
|
|
|
270
287
|
A manifest 404 usually means assets were not built/served at the component's configured URL.
|
package/dist/index.js
CHANGED
|
@@ -88,7 +88,7 @@ var sessionSchema = z.object({
|
|
|
88
88
|
}).strict().refine((value) => Date.parse(value.expiresAt) > Date.parse(value.createdAt), {
|
|
89
89
|
message: "session expiry must follow creation"
|
|
90
90
|
});
|
|
91
|
-
var
|
|
91
|
+
var authConfigSchema = z.object({
|
|
92
92
|
sessions: z.record(z.string().max(2048), sessionSchema),
|
|
93
93
|
version: z.literal(2)
|
|
94
94
|
}).strict().superRefine((value, context) => {
|
|
@@ -101,16 +101,6 @@ var authConfigV2Schema = z.object({
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
});
|
|
104
|
-
var legacyAuthConfigSchema = z.object({
|
|
105
|
-
apiKey: z.string().min(16).max(4096),
|
|
106
|
-
apiUrl: z.string().min(1).max(2048),
|
|
107
|
-
appUrl: z.string().max(2048).optional(),
|
|
108
|
-
createdAt: z.string().max(64).optional(),
|
|
109
|
-
deviceName: z.string().max(200).optional(),
|
|
110
|
-
keyPrefix: z.string().max(64).optional(),
|
|
111
|
-
projectId: z.string().max(200).optional(),
|
|
112
|
-
scopes: z.array(z.string().max(128)).max(32).optional()
|
|
113
|
-
}).strict();
|
|
114
104
|
function emptyAuthConfig() {
|
|
115
105
|
return Object.freeze({ sessions: Object.freeze({}), version: 2 });
|
|
116
106
|
}
|
|
@@ -128,24 +118,13 @@ async function readAuthFile(path = authConfigPath()) {
|
|
|
128
118
|
} catch {
|
|
129
119
|
return { kind: "invalid" };
|
|
130
120
|
}
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
const legacy = legacyAuthConfigSchema.safeParse(parsed);
|
|
134
|
-
if (legacy.success) {
|
|
135
|
-
return { config: Object.freeze(legacy.data), kind: "legacy_project_login" };
|
|
136
|
-
}
|
|
137
|
-
if (isSupersededProfileState(parsed)) return { kind: "superseded_profiles" };
|
|
138
|
-
return { kind: "invalid" };
|
|
121
|
+
const config = authConfigSchema.safeParse(parsed);
|
|
122
|
+
return config.success ? { config: freezeConfig(config.data), kind: "sessions" } : { kind: "invalid" };
|
|
139
123
|
}
|
|
140
124
|
async function loadAuthConfig(path = authConfigPath()) {
|
|
141
125
|
const state = await readAuthFile(path);
|
|
142
126
|
if (state.kind === "empty") return emptyAuthConfig();
|
|
143
|
-
if (state.kind === "
|
|
144
|
-
if (state.kind === "legacy_project_login" || state.kind === "superseded_profiles") {
|
|
145
|
-
throw new Error(
|
|
146
|
-
"The saved login uses the retired destination-credential model. Run `tileflow login` to authorize your Tileflow account."
|
|
147
|
-
);
|
|
148
|
-
}
|
|
127
|
+
if (state.kind === "sessions") return state.config;
|
|
149
128
|
throw new Error("Tileflow auth state is invalid; no changes were made.");
|
|
150
129
|
}
|
|
151
130
|
async function installAccountSession(session, path = authConfigPath()) {
|
|
@@ -157,7 +136,7 @@ async function installAccountSession(session, path = authConfigPath()) {
|
|
|
157
136
|
if (state.kind === "invalid") {
|
|
158
137
|
throw new Error("Tileflow auth state is invalid; no changes were made.");
|
|
159
138
|
}
|
|
160
|
-
const current = state.kind === "
|
|
139
|
+
const current = state.kind === "sessions" ? state.config : emptyAuthConfig();
|
|
161
140
|
const sessions = { ...current.sessions, [normalized.apiOrigin]: normalized };
|
|
162
141
|
if (Object.keys(sessions).length > maximumOrigins) {
|
|
163
142
|
throw new Error("Tileflow CLI API-origin limit reached.");
|
|
@@ -180,7 +159,7 @@ function removeAccountSession(config, apiUrl) {
|
|
|
180
159
|
return { config: freezeConfig({ sessions, version: 2 }), removed };
|
|
181
160
|
}
|
|
182
161
|
async function writeAuthFileAtomic(config, path = authConfigPath()) {
|
|
183
|
-
const normalized = normalizeConfig(
|
|
162
|
+
const normalized = normalizeConfig(authConfigSchema.parse(config));
|
|
184
163
|
const source = `${JSON.stringify(normalized, null, 2)}
|
|
185
164
|
`;
|
|
186
165
|
if (Buffer.byteLength(source, "utf8") > maximumAuthFileBytes) {
|
|
@@ -226,9 +205,6 @@ function parseProjectReference(value) {
|
|
|
226
205
|
);
|
|
227
206
|
return match ? { organizationSlug: match[1], projectSlug: match[2] } : null;
|
|
228
207
|
}
|
|
229
|
-
function projectReference(value) {
|
|
230
|
-
return `@${value.organization.slug}/${value.project.slug}`;
|
|
231
|
-
}
|
|
232
208
|
function authConfigPath() {
|
|
233
209
|
return resolve(homedir(), ".tileflow", "config.json");
|
|
234
210
|
}
|
|
@@ -250,11 +226,6 @@ function freezeConfig(config) {
|
|
|
250
226
|
);
|
|
251
227
|
return Object.freeze({ sessions: Object.freeze(sessions), version: 2 });
|
|
252
228
|
}
|
|
253
|
-
function isSupersededProfileState(value) {
|
|
254
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
255
|
-
const record = value;
|
|
256
|
-
return record.version === 2 && Boolean(record.profiles && typeof record.profiles === "object") && Boolean(record.selections && typeof record.selections === "object");
|
|
257
|
-
}
|
|
258
229
|
async function syncDirectory(path) {
|
|
259
230
|
if (process.platform === "win32") return;
|
|
260
231
|
const directory = await open(path, "r");
|
|
@@ -678,6 +649,7 @@ import { isAbsolute, relative as relative2, resolve as resolve3, sep as sep2 } f
|
|
|
678
649
|
import {
|
|
679
650
|
createTileflowCaptureSession,
|
|
680
651
|
serializeTileflowCaptureReceipt,
|
|
652
|
+
serializeTileflowShotReceipt,
|
|
681
653
|
setupTileflowCaptureBrowser,
|
|
682
654
|
TileflowCaptureError,
|
|
683
655
|
tileflowSyntheticAssetOrigin,
|
|
@@ -687,7 +659,8 @@ import {
|
|
|
687
659
|
compareCodeUnits as compareCodeUnits3,
|
|
688
660
|
normalizeTileflowCaptureScene,
|
|
689
661
|
tileflowCaptureSceneNameSchema,
|
|
690
|
-
tileflowCaptureSceneSchema
|
|
662
|
+
tileflowCaptureSceneSchema,
|
|
663
|
+
tileflowCaptureShotNameSchema
|
|
691
664
|
} from "@tileflow/core";
|
|
692
665
|
import {
|
|
693
666
|
createTileflowArtifactDiagnostics,
|
|
@@ -699,8 +672,8 @@ import { TileflowStyleValidationError } from "@tileflow/dev/validation";
|
|
|
699
672
|
|
|
700
673
|
// src/capture-output.ts
|
|
701
674
|
import { randomUUID } from "crypto";
|
|
702
|
-
import { lstat, mkdir as mkdir2, open as open2, readFile as readFile2, rename as rename2, unlink as unlink2 } from "fs/promises";
|
|
703
|
-
import { basename, dirname as dirname3, relative, resolve as resolve2, sep } from "path";
|
|
675
|
+
import { lstat, mkdir as mkdir2, open as open2, readdir, readFile as readFile2, rename as rename2, rm, unlink as unlink2 } from "fs/promises";
|
|
676
|
+
import { basename, dirname as dirname3, join, relative, resolve as resolve2, sep } from "path";
|
|
704
677
|
var maximumAtomicArtifactBytes = 256 * 1024 * 1024;
|
|
705
678
|
async function writeCapturePair(input) {
|
|
706
679
|
const outputPath = resolve2(input.outputPath);
|
|
@@ -844,6 +817,119 @@ async function writeAtomicFile(input) {
|
|
|
844
817
|
await removeOptional(temporary);
|
|
845
818
|
}
|
|
846
819
|
}
|
|
820
|
+
var shotFrameFilePattern = /^frame-\d{4}\.png$/u;
|
|
821
|
+
function shotFrameFileName(index) {
|
|
822
|
+
return `frame-${String(index).padStart(4, "0")}.png`;
|
|
823
|
+
}
|
|
824
|
+
async function stageShotFrameDirectory(input) {
|
|
825
|
+
const boundaryPath = resolve2(input.boundaryPath);
|
|
826
|
+
const directory = resolve2(input.directory);
|
|
827
|
+
await assertNoSymlinkComponents(dirname3(directory), boundaryPath);
|
|
828
|
+
await listShotFrameDirectory(directory);
|
|
829
|
+
await mkdir2(dirname3(directory), { recursive: true });
|
|
830
|
+
await assertNoSymlinkComponents(dirname3(directory), boundaryPath);
|
|
831
|
+
const staging = temporarySibling(directory, `${process.pid}.${randomUUID()}`, "frames");
|
|
832
|
+
await mkdir2(staging);
|
|
833
|
+
return {
|
|
834
|
+
boundaryPath,
|
|
835
|
+
directory,
|
|
836
|
+
staging,
|
|
837
|
+
write: (index, png) => writeSyncedExclusive(join(staging, shotFrameFileName(index)), png),
|
|
838
|
+
discard: () => rm(staging, { force: true, recursive: true })
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
async function commitShotFrameDirectory(input) {
|
|
842
|
+
const { stage } = input;
|
|
843
|
+
const receiptPath = resolve2(input.receiptPath);
|
|
844
|
+
const receipt = new TextEncoder().encode(input.receipt);
|
|
845
|
+
await assertNoSymlinkComponents(dirname3(stage.directory), stage.boundaryPath);
|
|
846
|
+
await assertNoSymlinkComponents(dirname3(receiptPath), stage.boundaryPath);
|
|
847
|
+
await assertNotSymlink(receiptPath);
|
|
848
|
+
const existingFrames = await listShotFrameDirectory(stage.directory);
|
|
849
|
+
const existingReceipt = await readOptional(receiptPath);
|
|
850
|
+
if (existingFrames && existingReceipt && Buffer.from(existingReceipt).equals(Buffer.from(receipt)) && await sameShotFrames(stage.directory, existingFrames, stage.staging)) {
|
|
851
|
+
await stage.discard();
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
if ((existingFrames || existingReceipt) && !input.managed && !input.force) {
|
|
855
|
+
throw new Error(
|
|
856
|
+
"Shot output already exists with different contents. Use --force to replace it."
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
const transaction = `${process.pid}.${randomUUID()}`;
|
|
860
|
+
const receiptTemporary = temporarySibling(receiptPath, transaction, "new");
|
|
861
|
+
const directoryBackup = temporarySibling(stage.directory, transaction, "previous");
|
|
862
|
+
const receiptBackup = temporarySibling(receiptPath, transaction, "previous");
|
|
863
|
+
const state = {
|
|
864
|
+
directoryBackedUp: false,
|
|
865
|
+
directoryInstalled: false,
|
|
866
|
+
receiptBackedUp: false,
|
|
867
|
+
receiptInstalled: false
|
|
868
|
+
};
|
|
869
|
+
try {
|
|
870
|
+
await writeSyncedExclusive(receiptTemporary, receipt);
|
|
871
|
+
await syncDirectory2(stage.staging);
|
|
872
|
+
if (existingFrames) {
|
|
873
|
+
await rename2(stage.directory, directoryBackup);
|
|
874
|
+
state.directoryBackedUp = true;
|
|
875
|
+
}
|
|
876
|
+
if (existingReceipt) {
|
|
877
|
+
await rename2(receiptPath, receiptBackup);
|
|
878
|
+
state.receiptBackedUp = true;
|
|
879
|
+
}
|
|
880
|
+
await rename2(stage.staging, stage.directory);
|
|
881
|
+
state.directoryInstalled = true;
|
|
882
|
+
await rename2(receiptTemporary, receiptPath);
|
|
883
|
+
state.receiptInstalled = true;
|
|
884
|
+
await syncDirectory2(dirname3(stage.directory));
|
|
885
|
+
if (dirname3(receiptPath) !== dirname3(stage.directory))
|
|
886
|
+
await syncDirectory2(dirname3(receiptPath));
|
|
887
|
+
} catch (error) {
|
|
888
|
+
if (state.receiptInstalled) await removeOptional(receiptPath).catch(() => void 0);
|
|
889
|
+
if (state.receiptBackedUp) await rename2(receiptBackup, receiptPath).catch(() => void 0);
|
|
890
|
+
if (state.directoryInstalled) {
|
|
891
|
+
await rename2(stage.directory, stage.staging).catch(() => void 0);
|
|
892
|
+
}
|
|
893
|
+
if (state.directoryBackedUp) {
|
|
894
|
+
await rename2(directoryBackup, stage.directory).catch(() => void 0);
|
|
895
|
+
}
|
|
896
|
+
throw error;
|
|
897
|
+
} finally {
|
|
898
|
+
await removeOptional(receiptTemporary).catch(() => void 0);
|
|
899
|
+
}
|
|
900
|
+
await Promise.allSettled([
|
|
901
|
+
rm(directoryBackup, { force: true, recursive: true }),
|
|
902
|
+
removeOptional(receiptBackup)
|
|
903
|
+
]);
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
906
|
+
async function listShotFrameDirectory(directory) {
|
|
907
|
+
const info = await lstatOptional(directory);
|
|
908
|
+
if (!info) return void 0;
|
|
909
|
+
if (info.isSymbolicLink()) throw new Error("Capture output cannot be a symbolic link.");
|
|
910
|
+
if (!info.isDirectory()) throw new Error("Shot frame output must be a directory.");
|
|
911
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
912
|
+
if (entries.some((entry) => !entry.isFile() || !shotFrameFilePattern.test(entry.name))) {
|
|
913
|
+
throw new Error(
|
|
914
|
+
"Shot frame output contains files that are not Tileflow frames. Choose an empty or Tileflow-owned directory."
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
return entries.map((entry) => entry.name).sort();
|
|
918
|
+
}
|
|
919
|
+
async function sameShotFrames(directory, names, staging) {
|
|
920
|
+
const staged = (await readdir(staging)).sort();
|
|
921
|
+
if (staged.length !== names.length || staged.some((name, index) => name !== names[index])) {
|
|
922
|
+
return false;
|
|
923
|
+
}
|
|
924
|
+
for (const name of names) {
|
|
925
|
+
const [existing, next] = await Promise.all([
|
|
926
|
+
readOptional(join(directory, name)),
|
|
927
|
+
readOptional(join(staging, name))
|
|
928
|
+
]);
|
|
929
|
+
if (!existing || !next || !Buffer.from(existing).equals(Buffer.from(next))) return false;
|
|
930
|
+
}
|
|
931
|
+
return true;
|
|
932
|
+
}
|
|
847
933
|
function captureReceiptPath(outputPath) {
|
|
848
934
|
return outputPath.toLowerCase().endsWith(".png") ? `${outputPath.slice(0, -4)}.receipt.json` : `${outputPath}.receipt.json`;
|
|
849
935
|
}
|
|
@@ -984,7 +1070,14 @@ function registerCaptureCommands(program2, dependencies) {
|
|
|
984
1070
|
controller.close();
|
|
985
1071
|
}
|
|
986
1072
|
});
|
|
987
|
-
program2.command("capture").description(
|
|
1073
|
+
program2.command("capture").description(
|
|
1074
|
+
"Render Tileflow scenes to PNG, or timed shots to video, with a pinned headless browser"
|
|
1075
|
+
).argument("[scenes...]", "named committed capture scenes").option("-c, --config <path>", "config path", dependencies.defaultConfigPath).option("--all", "capture every configured scene").option(
|
|
1076
|
+
"--shot <name>",
|
|
1077
|
+
"render a committed timed shot; repeat for more shots",
|
|
1078
|
+
collectOption,
|
|
1079
|
+
[]
|
|
1080
|
+
).option("--all-shots", "render every committed timed shot").option("--format <format>", "shot output: webm (default) or frames").option("--map <name>", "capture one map with an uncommitted camera").option("--theme <name>", "required concrete theme for exploratory --map capture").option("--center <lng,lat>", "exploratory center camera").option("--bounds <west,south,east,north>", "exploratory bounds camera").option("--zoom <number>", "exploratory center zoom").option("--bearing <number>", "exploratory camera bearing").option("--pitch <number>", "exploratory camera pitch").option("--width <pixels>", "exploratory viewport width").option("--height <pixels>", "exploratory viewport height").option("--dpr <ratio>", "exploratory device pixel ratio: 1 or 2").option("--app-origin <origin>", "loopback origin for committed application scenes").option("--url <url>", "one-off full loopback URL for exactly one named scene").option("--selector <selector>", "one-off application target selector").option("--frame <frame>", "application capture frame: map or viewport").option("--out <path>", "PNG path for one scene, or .webm/frame directory for one shot").option("--out-dir <path>", "directory for one or more captures").option("--force", "replace different explicit output files").option("--json", "print deterministic schema-version-1 JSON").option("--watch", "keep running and recapture after each valid local edit").option(
|
|
988
1081
|
"--no-browser-install",
|
|
989
1082
|
"require the pinned browser to be prepared; never use system Chrome"
|
|
990
1083
|
).addHelpText(
|
|
@@ -994,11 +1087,205 @@ function registerCaptureCommands(program2, dependencies) {
|
|
|
994
1087
|
try {
|
|
995
1088
|
await runCaptureCommand(scenes ?? [], options);
|
|
996
1089
|
} catch (error) {
|
|
997
|
-
printCaptureError(error, {
|
|
1090
|
+
printCaptureError(error, {
|
|
1091
|
+
command: isShotSelection(options) ? "capture.shot" : "capture",
|
|
1092
|
+
json: options.json
|
|
1093
|
+
});
|
|
998
1094
|
process.exitCode = 1;
|
|
999
1095
|
}
|
|
1000
1096
|
});
|
|
1001
1097
|
}
|
|
1098
|
+
function collectOption(value, previous) {
|
|
1099
|
+
return [...previous, value];
|
|
1100
|
+
}
|
|
1101
|
+
function isShotSelection(options) {
|
|
1102
|
+
return Boolean(options.allShots) || (options.shot?.length ?? 0) > 0;
|
|
1103
|
+
}
|
|
1104
|
+
function createTileflowShotCaptureJson(shots, cwd) {
|
|
1105
|
+
return {
|
|
1106
|
+
schemaVersion: 1,
|
|
1107
|
+
command: "capture.shot",
|
|
1108
|
+
shots: [...shots].sort((left, right) => compareCodeUnits3(left.capture.shot, right.capture.shot)).map(({ capture, outputPath, receiptPath }) => ({
|
|
1109
|
+
shot: capture.shot,
|
|
1110
|
+
map: capture.map,
|
|
1111
|
+
theme: capture.theme,
|
|
1112
|
+
status: "captured",
|
|
1113
|
+
format: capture.format,
|
|
1114
|
+
...capture.codec ? { codec: capture.codec } : {},
|
|
1115
|
+
outputPath: relativePath(cwd, outputPath),
|
|
1116
|
+
receiptPath: relativePath(cwd, receiptPath),
|
|
1117
|
+
sha256: capture.sha256,
|
|
1118
|
+
framesSha256: capture.framesSha256,
|
|
1119
|
+
frameCount: capture.frameCount,
|
|
1120
|
+
fps: capture.fps,
|
|
1121
|
+
durationMs: capture.durationMs,
|
|
1122
|
+
width: capture.width,
|
|
1123
|
+
height: capture.height,
|
|
1124
|
+
dpr: capture.dpr,
|
|
1125
|
+
renderer: capture.renderer,
|
|
1126
|
+
networkDependent: capture.networkDependent,
|
|
1127
|
+
warnings: [...capture.warnings].sort(compareCodeUnits3)
|
|
1128
|
+
}))
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
function serializeTileflowShotCaptureJson(value) {
|
|
1132
|
+
return `${JSON.stringify(value, null, 2)}
|
|
1133
|
+
`;
|
|
1134
|
+
}
|
|
1135
|
+
function shotReceiptPath(outputPath) {
|
|
1136
|
+
return outputPath.toLowerCase().endsWith(".webm") ? `${outputPath.slice(0, -5)}.receipt.json` : `${outputPath}.receipt.json`;
|
|
1137
|
+
}
|
|
1138
|
+
async function runCaptureShotCommand(options) {
|
|
1139
|
+
const cwd = process.cwd();
|
|
1140
|
+
const format = options.format === "frames" ? "frames" : "webm";
|
|
1141
|
+
const controller = installSignalAbortController();
|
|
1142
|
+
const session = createTileflowCaptureSession({
|
|
1143
|
+
allowBrowserInstall: options.browserInstall,
|
|
1144
|
+
config: options.config,
|
|
1145
|
+
cwd,
|
|
1146
|
+
onBrowserInstallProgress: options.json ? void 0 : printInstallProgress,
|
|
1147
|
+
signal: controller.signal
|
|
1148
|
+
});
|
|
1149
|
+
const stages = /* @__PURE__ */ new Map();
|
|
1150
|
+
try {
|
|
1151
|
+
const onFrame = format === "frames" ? async (frame) => {
|
|
1152
|
+
let stage = stages.get(frame.shot);
|
|
1153
|
+
if (!stage) {
|
|
1154
|
+
const target = resolveShotOutputTarget(frame.shot, options, format, cwd);
|
|
1155
|
+
stage = await stageShotFrameDirectory({
|
|
1156
|
+
boundaryPath: target.boundaryPath,
|
|
1157
|
+
directory: target.outputPath
|
|
1158
|
+
});
|
|
1159
|
+
stages.set(frame.shot, stage);
|
|
1160
|
+
}
|
|
1161
|
+
await stage.write(frame.index, frame.png);
|
|
1162
|
+
} : void 0;
|
|
1163
|
+
const shotOptions = { format, ...onFrame ? { onFrame } : {}, signal: controller.signal };
|
|
1164
|
+
const result = options.allShots ? await session.captureAllShots(shotOptions) : await session.captureShots(options.shot ?? [], shotOptions);
|
|
1165
|
+
const written = [];
|
|
1166
|
+
for (const capture of result.shots) {
|
|
1167
|
+
const target = resolveShotOutputTarget(capture.shot, options, format, cwd);
|
|
1168
|
+
const receipt = serializeTileflowShotReceipt(capture.receipt);
|
|
1169
|
+
if (capture.video) {
|
|
1170
|
+
await writeAtomicFileSet({
|
|
1171
|
+
boundaryPath: target.boundaryPath,
|
|
1172
|
+
files: [
|
|
1173
|
+
{ path: target.outputPath, source: capture.video },
|
|
1174
|
+
{ path: target.receiptPath, source: receipt }
|
|
1175
|
+
],
|
|
1176
|
+
force: Boolean(options.force),
|
|
1177
|
+
label: "Shot output",
|
|
1178
|
+
managed: target.managed
|
|
1179
|
+
});
|
|
1180
|
+
} else {
|
|
1181
|
+
const stage = stages.get(capture.shot);
|
|
1182
|
+
if (!stage) throw new Error(`Shot ${capture.shot} produced no frames.`);
|
|
1183
|
+
await commitShotFrameDirectory({
|
|
1184
|
+
force: Boolean(options.force),
|
|
1185
|
+
managed: target.managed,
|
|
1186
|
+
receipt,
|
|
1187
|
+
receiptPath: target.receiptPath,
|
|
1188
|
+
stage
|
|
1189
|
+
});
|
|
1190
|
+
stages.delete(capture.shot);
|
|
1191
|
+
}
|
|
1192
|
+
written.push({ capture, outputPath: target.outputPath, receiptPath: target.receiptPath });
|
|
1193
|
+
}
|
|
1194
|
+
if (!options.out && !options.outDir) await warnIfManagedCapturesAreNotIgnored(cwd);
|
|
1195
|
+
const document = createTileflowShotCaptureJson(written, cwd);
|
|
1196
|
+
if (options.json) {
|
|
1197
|
+
process.stdout.write(serializeTileflowShotCaptureJson(document));
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
for (const entry of document.shots) {
|
|
1201
|
+
console.log(
|
|
1202
|
+
`Rendered ${entry.shot} -> ${entry.outputPath} (${entry.frameCount} frames, ${entry.sha256.slice(0, 12)}).`
|
|
1203
|
+
);
|
|
1204
|
+
if (entry.networkDependent) {
|
|
1205
|
+
console.error(`Shot ${entry.shot} used remote map resources.`);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
} finally {
|
|
1209
|
+
await Promise.allSettled([...stages.values()].map((stage) => stage.discard()));
|
|
1210
|
+
await session.close();
|
|
1211
|
+
controller.close();
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
function resolveShotOutputTarget(shot, options, format, cwd) {
|
|
1215
|
+
const managed = !options.out && !options.outDir;
|
|
1216
|
+
const directory = resolve3(cwd, options.outDir ?? ".tileflow/captures/shots");
|
|
1217
|
+
const outputPath = options.out ? resolve3(cwd, options.out) : resolve3(directory, format === "webm" ? `${shot}.webm` : shot);
|
|
1218
|
+
const receiptPath = shotReceiptPath(outputPath);
|
|
1219
|
+
if (managed) {
|
|
1220
|
+
const relativeToRoot = relative2(directory, outputPath);
|
|
1221
|
+
if (relativeToRoot.startsWith("..") || relativeToRoot.includes(`..${sep2}`)) {
|
|
1222
|
+
throw new Error("Managed shot output escaped .tileflow/captures/shots.");
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
relativePath(cwd, outputPath);
|
|
1226
|
+
relativePath(cwd, receiptPath);
|
|
1227
|
+
return {
|
|
1228
|
+
boundaryPath: managed ? cwd : options.outDir ? directory : resolve3(outputPath, ".."),
|
|
1229
|
+
managed,
|
|
1230
|
+
outputPath,
|
|
1231
|
+
receiptPath
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
function validateShotSelectionOptions(positionalScenes, options) {
|
|
1235
|
+
const shots = options.shot ?? [];
|
|
1236
|
+
if (options.allShots && shots.length > 0) {
|
|
1237
|
+
throw new Error("Use either --shot or --all-shots, not both.");
|
|
1238
|
+
}
|
|
1239
|
+
if (positionalScenes.length > 0 || options.all || options.map) {
|
|
1240
|
+
throw new Error("Render shots separately from scenes; --shot and --all-shots take no scenes.");
|
|
1241
|
+
}
|
|
1242
|
+
if (options.watch) {
|
|
1243
|
+
throw new Error("--watch captures scenes; it does not render shots.");
|
|
1244
|
+
}
|
|
1245
|
+
const sceneOnlyOptions = [
|
|
1246
|
+
options.center,
|
|
1247
|
+
options.bounds,
|
|
1248
|
+
options.zoom,
|
|
1249
|
+
options.bearing,
|
|
1250
|
+
options.pitch,
|
|
1251
|
+
options.width,
|
|
1252
|
+
options.height,
|
|
1253
|
+
options.dpr,
|
|
1254
|
+
options.theme,
|
|
1255
|
+
options.url,
|
|
1256
|
+
options.appOrigin,
|
|
1257
|
+
options.selector,
|
|
1258
|
+
options.frame
|
|
1259
|
+
];
|
|
1260
|
+
if (sceneOnlyOptions.some((value) => value !== void 0)) {
|
|
1261
|
+
throw new Error(
|
|
1262
|
+
"Shots render committed cameras and themes; camera, viewport, theme, and application flags apply to scenes."
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
for (const shot of shots) {
|
|
1266
|
+
if (!tileflowCaptureShotNameSchema.safeParse(shot).success) {
|
|
1267
|
+
throw new Error("--shot expects a portable Tileflow shot name.");
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
if (options.format !== void 0 && options.format !== "webm" && options.format !== "frames") {
|
|
1271
|
+
throw new Error("--format expects webm or frames.");
|
|
1272
|
+
}
|
|
1273
|
+
if (options.out && options.outDir) {
|
|
1274
|
+
throw new Error("Use either --out or --out-dir, not both.");
|
|
1275
|
+
}
|
|
1276
|
+
if (options.out) {
|
|
1277
|
+
if (options.allShots || new Set(shots).size !== 1) {
|
|
1278
|
+
throw new Error("--out can be used only when rendering one shot. Use --out-dir instead.");
|
|
1279
|
+
}
|
|
1280
|
+
const isVideoPath = options.out.toLowerCase().endsWith(".webm");
|
|
1281
|
+
if ((options.format ?? "webm") === "webm" && !isVideoPath) {
|
|
1282
|
+
throw new Error("--out must name a .webm file.");
|
|
1283
|
+
}
|
|
1284
|
+
if (options.format === "frames" && (isVideoPath || /\.(?:json|png)$/iu.test(options.out))) {
|
|
1285
|
+
throw new Error("--out must name a directory for --format frames.");
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1002
1289
|
function createTileflowCaptureJson(captures, cwd) {
|
|
1003
1290
|
return {
|
|
1004
1291
|
schemaVersion: 1,
|
|
@@ -1048,6 +1335,10 @@ function serializeTileflowCaptureFailureJson(value) {
|
|
|
1048
1335
|
async function runCaptureCommand(positionalScenes, options) {
|
|
1049
1336
|
validateSelectionOptions(positionalScenes, options);
|
|
1050
1337
|
await withTileflowConfigSecretsHidden(async () => {
|
|
1338
|
+
if (isShotSelection(options)) {
|
|
1339
|
+
await runCaptureShotCommand(options);
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1051
1342
|
if (options.watch) {
|
|
1052
1343
|
await runCaptureWatchCommand(positionalScenes, options);
|
|
1053
1344
|
return;
|
|
@@ -1296,6 +1587,13 @@ function waitForAbort(signal) {
|
|
|
1296
1587
|
);
|
|
1297
1588
|
}
|
|
1298
1589
|
function validateSelectionOptions(positionalScenes, options) {
|
|
1590
|
+
if (isShotSelection(options)) {
|
|
1591
|
+
validateShotSelectionOptions(positionalScenes, options);
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
if (options.format !== void 0) {
|
|
1595
|
+
throw new Error("--format applies to --shot and --all-shots.");
|
|
1596
|
+
}
|
|
1299
1597
|
const exploratoryOptions = [
|
|
1300
1598
|
options.center,
|
|
1301
1599
|
options.bounds,
|
|
@@ -1354,7 +1652,9 @@ function validateSelectionOptions(positionalScenes, options) {
|
|
|
1354
1652
|
throw new Error("Exploratory camera, viewport, and theme flags require --map.");
|
|
1355
1653
|
}
|
|
1356
1654
|
if (!options.all && positionalScenes.length === 0) {
|
|
1357
|
-
throw new Error(
|
|
1655
|
+
throw new Error(
|
|
1656
|
+
"Select at least one scene, use --all, use --map for exploration, or select shots with --shot."
|
|
1657
|
+
);
|
|
1358
1658
|
}
|
|
1359
1659
|
}
|
|
1360
1660
|
function createExploratoryScene(options) {
|
|
@@ -1496,6 +1796,8 @@ function captureFailurePhase(error, code) {
|
|
|
1496
1796
|
if (code === "MAP_LOAD_FAILED") return "map-load";
|
|
1497
1797
|
if (code === "SCREENSHOT_FAILED" || code === "INVALID_PNG") return "screenshot";
|
|
1498
1798
|
if (code === "SCENE_NOT_FOUND" || code.startsWith("APPLICATION_")) return "scene-resolution";
|
|
1799
|
+
if (code === "SHOT_NOT_FOUND") return "shot-resolution";
|
|
1800
|
+
if (code.startsWith("VIDEO_")) return "video-encode";
|
|
1499
1801
|
if (code === "ABORTED") return "capture";
|
|
1500
1802
|
return "input-validation";
|
|
1501
1803
|
}
|
|
@@ -2464,47 +2766,17 @@ async function requestMapCapability(session, target, scopes, options = {}) {
|
|
|
2464
2766
|
} catch (error) {
|
|
2465
2767
|
return { error: safeTransportError(error, "Map capability request failed."), ok: false };
|
|
2466
2768
|
}
|
|
2467
|
-
if (!response.ok) {
|
|
2468
|
-
return { error: `Map capability request failed (${response.status}).`, ok: false };
|
|
2469
|
-
}
|
|
2470
|
-
const body = asRecord(response.body);
|
|
2471
|
-
if (!response.json || typeof body.capability !== "string" || !body.capability.startsWith("tf_cap_") || body.capability.length > 8192 || typeof body.mapId !== "string" || !/^map_[A-Za-z0-9_-]{16}$/u.test(body.mapId) || body.mapId !== target.mapId || !validIsoDate(body.expiresAt) || !Array.isArray(body.scopes) || body.scopes.join("\0") !== [...scopes].sort().join("\0")) {
|
|
2472
|
-
return { error: "Map capability response was invalid.", ok: false };
|
|
2473
|
-
}
|
|
2474
|
-
return { capability: body.capability, mapId: body.mapId, ok: true };
|
|
2475
|
-
}
|
|
2476
|
-
async function requestProjectCapability(session, project, scopes, options = {}) {
|
|
2477
|
-
let response;
|
|
2478
|
-
try {
|
|
2479
|
-
response = await requestHostedJson(
|
|
2480
|
-
session.apiOrigin,
|
|
2481
|
-
"/v1/cli/project-capabilities",
|
|
2482
|
-
{
|
|
2483
|
-
...jsonRequest("POST", { project, scopes }),
|
|
2484
|
-
headers: {
|
|
2485
|
-
...authorizationHeaders(session.accountSession),
|
|
2486
|
-
"Content-Type": "application/json"
|
|
2487
|
-
}
|
|
2488
|
-
},
|
|
2489
|
-
options
|
|
2490
|
-
);
|
|
2491
|
-
} catch (error) {
|
|
2492
|
-
return {
|
|
2493
|
-
error: safeTransportError(error, "Project capability request failed."),
|
|
2494
|
-
ok: false
|
|
2495
|
-
};
|
|
2496
|
-
}
|
|
2497
2769
|
if (!response.ok) {
|
|
2498
2770
|
return {
|
|
2499
|
-
error: response.status === 401 ? rejectedAccountSessionMessage : `
|
|
2771
|
+
error: response.status === 401 ? rejectedAccountSessionMessage : `Map capability request failed (${response.status}).`,
|
|
2500
2772
|
ok: false
|
|
2501
2773
|
};
|
|
2502
2774
|
}
|
|
2503
2775
|
const body = asRecord(response.body);
|
|
2504
|
-
if (!response.json || typeof body.capability !== "string" || !body.capability.startsWith("tf_cap_") || body.capability.length > 8192 || body.
|
|
2505
|
-
return { error: "
|
|
2776
|
+
if (!response.json || typeof body.capability !== "string" || !body.capability.startsWith("tf_cap_") || body.capability.length > 8192 || typeof body.mapId !== "string" || !/^map_[A-Za-z0-9_-]{16}$/u.test(body.mapId) || body.mapId !== target.mapId || !validIsoDate(body.expiresAt) || !Array.isArray(body.scopes) || body.scopes.join("\0") !== [...scopes].sort().join("\0")) {
|
|
2777
|
+
return { error: "Map capability response was invalid.", ok: false };
|
|
2506
2778
|
}
|
|
2507
|
-
return { capability: body.capability, ok: true };
|
|
2779
|
+
return { capability: body.capability, mapId: body.mapId, ok: true };
|
|
2508
2780
|
}
|
|
2509
2781
|
async function requestTeamCapability(session, team, scopes, options = {}) {
|
|
2510
2782
|
let response;
|
|
@@ -3196,7 +3468,7 @@ import {
|
|
|
3196
3468
|
// src/icon-diff-report.ts
|
|
3197
3469
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3198
3470
|
import { mkdir as mkdir3, readFile as readFile3, rename as rename3, unlink as unlink3, writeFile } from "fs/promises";
|
|
3199
|
-
import { basename as basename2, dirname as dirname6, join } from "path";
|
|
3471
|
+
import { basename as basename2, dirname as dirname6, join as join2 } from "path";
|
|
3200
3472
|
import {
|
|
3201
3473
|
compareCodeUnits as compareCodeUnits5,
|
|
3202
3474
|
sha256Hex,
|
|
@@ -3663,7 +3935,7 @@ async function writeAtomicReport(path, html, force) {
|
|
|
3663
3935
|
if (!isMissingFile3(error)) throw error;
|
|
3664
3936
|
}
|
|
3665
3937
|
await mkdir3(dirname6(path), { recursive: true });
|
|
3666
|
-
const temporaryPath =
|
|
3938
|
+
const temporaryPath = join2(
|
|
3667
3939
|
dirname6(path),
|
|
3668
3940
|
`.${basename2(path)}.${process.pid}.${randomUUID2()}.tmp`
|
|
3669
3941
|
);
|
|
@@ -5292,18 +5564,6 @@ async function fetchAccountProjects(session, includeArchived) {
|
|
|
5292
5564
|
}
|
|
5293
5565
|
return { error: "Destination discovery exceeded its safe page limit.", ok: false, status: 502 };
|
|
5294
5566
|
}
|
|
5295
|
-
async function resolveAccountProjectTarget(session, requestedProject) {
|
|
5296
|
-
const result = await fetchAccountProjects(session, false);
|
|
5297
|
-
if (!result.ok) return { error: result.error, kind: "remote", status: result.status };
|
|
5298
|
-
if (requestedProject) {
|
|
5299
|
-
const parsed = parseProjectReference(requestedProject);
|
|
5300
|
-
if (!parsed) return { kind: "invalid", targets: result.items };
|
|
5301
|
-
const target = result.items.find((candidate) => candidate.reference === requestedProject);
|
|
5302
|
-
return target ? { kind: "selected", target } : { kind: "missing", targets: result.items };
|
|
5303
|
-
}
|
|
5304
|
-
if (result.items.length === 1) return { kind: "selected", target: result.items[0] };
|
|
5305
|
-
return result.items.length ? { kind: "ambiguous", targets: result.items } : { kind: "missing", targets: [] };
|
|
5306
|
-
}
|
|
5307
5567
|
async function requireCommandSession(options, dependencies) {
|
|
5308
5568
|
let config;
|
|
5309
5569
|
try {
|
|
@@ -8445,7 +8705,7 @@ program.command("build").description("Generate static Tileflow styles").option("
|
|
|
8445
8705
|
printKeyValue("Output", pathLabel(resolve8(process.cwd(), options.out)));
|
|
8446
8706
|
}
|
|
8447
8707
|
);
|
|
8448
|
-
program.command("preview").
|
|
8708
|
+
program.command("preview").description("Run a local map preview").option("-c, --config <path>", "config path", defaultConfigPath).option("--against-config <path>", "second config for the comparison workbench").option("--against-map <name>", "map on the right; defaults to --map").option("--against-scene <name>", "committed standalone scene on the right").option("--against-theme <name>", "concrete theme on the right; defaults to --theme").option("--host <host>", "bind host: an explicit IP address or localhost", defaultTileflowDevHost).option("--map <name>", "preview one configured map").option("--theme <name>", "preview one concrete theme; defaults to the map default").option("--renderer <renderer>", "artifact renderer: web or native", "web").option("-p, --port <port>", "preview port", "3333").option("--scene <name>", "preview one committed standalone map scene").option(
|
|
8449
8709
|
"--api-base-url <url>",
|
|
8450
8710
|
"Tileflow API base URL used to resolve official map assets",
|
|
8451
8711
|
process.env.TILEFLOW_API_URL ?? defaultApiUrl
|
|
@@ -8461,7 +8721,7 @@ program.command("preview").alias("dev").description("Run a local map preview").o
|
|
|
8461
8721
|
process.stdout.write(
|
|
8462
8722
|
`${JSON.stringify({
|
|
8463
8723
|
schemaVersion: 1,
|
|
8464
|
-
command: "
|
|
8724
|
+
command: "preview",
|
|
8465
8725
|
event: "error",
|
|
8466
8726
|
code: first?.code ?? "NATIVE_RENDERER_UNSUPPORTED",
|
|
8467
8727
|
phase: first?.phase ?? "command-validation",
|
|
@@ -8481,7 +8741,7 @@ program.command("preview").alias("dev").description("Run a local map preview").o
|
|
|
8481
8741
|
process.stdout.write(
|
|
8482
8742
|
`${JSON.stringify({
|
|
8483
8743
|
schemaVersion: 1,
|
|
8484
|
-
command: "
|
|
8744
|
+
command: "preview",
|
|
8485
8745
|
event: "error",
|
|
8486
8746
|
code: "NATIVE_RENDERER_UNSUPPORTED",
|
|
8487
8747
|
phase: "command-validation",
|
|
@@ -8613,7 +8873,7 @@ program.command("preview").alias("dev").description("Run a local map preview").o
|
|
|
8613
8873
|
process.stdout.write(
|
|
8614
8874
|
`${JSON.stringify({
|
|
8615
8875
|
schemaVersion: 1,
|
|
8616
|
-
command: "
|
|
8876
|
+
command: "preview",
|
|
8617
8877
|
event: recovered ? "recovered" : state.status,
|
|
8618
8878
|
generation: state.generation,
|
|
8619
8879
|
..."lastGoodGeneration" in state && state.lastGoodGeneration !== void 0 ? { lastGoodGeneration: state.lastGoodGeneration } : {},
|
|
@@ -8682,7 +8942,7 @@ program.command("preview").alias("dev").description("Run a local map preview").o
|
|
|
8682
8942
|
process.stdout.write(
|
|
8683
8943
|
`${JSON.stringify({
|
|
8684
8944
|
schemaVersion: 1,
|
|
8685
|
-
command: "
|
|
8945
|
+
command: "preview",
|
|
8686
8946
|
event: "stopped",
|
|
8687
8947
|
generation,
|
|
8688
8948
|
...nativeLifecycleFields
|
|
@@ -8700,24 +8960,10 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
8700
8960
|
).option("--cache-dir <path>", "verified Icon Set artifact cache root").option("--offline", "fail a locked Icon Set cache miss instead of hydrating it").action(
|
|
8701
8961
|
async (options) => {
|
|
8702
8962
|
const source = resolveDeploySource(process.env);
|
|
8703
|
-
const resolveApi = (
|
|
8963
|
+
const resolveApi = () => requireApiOptions(options, {
|
|
8704
8964
|
allowStoredCredential: allowsStoredDeployCredential(source),
|
|
8705
8965
|
assertExplicitKeyMap: true,
|
|
8706
|
-
capabilityScopes: ["styles:write"]
|
|
8707
|
-
retryCommand: cliInvocation([
|
|
8708
|
-
"tileflow",
|
|
8709
|
-
"deploy",
|
|
8710
|
-
"--config",
|
|
8711
|
-
options.config,
|
|
8712
|
-
"--manifest",
|
|
8713
|
-
options.manifest,
|
|
8714
|
-
"--api-url",
|
|
8715
|
-
options.apiUrl ?? defaultApiUrl,
|
|
8716
|
-
...options.overwriteSelfHostedManifest ? ["--overwrite-self-hosted-manifest"] : [],
|
|
8717
|
-
...options.mapId ? ["--map-id", options.mapId] : [],
|
|
8718
|
-
...options.map && selectedMap ? ["--map", selectedMap] : [],
|
|
8719
|
-
...options.withNative ? ["--with-native"] : []
|
|
8720
|
-
])
|
|
8966
|
+
capabilityScopes: ["styles:write"]
|
|
8721
8967
|
});
|
|
8722
8968
|
const apiUrl = normalizeApiOrigin(options.apiUrl ?? defaultApiUrl);
|
|
8723
8969
|
delete process.env.TILEFLOW_API_KEY;
|
|
@@ -8823,7 +9069,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
8823
9069
|
return;
|
|
8824
9070
|
}
|
|
8825
9071
|
const existingManifest = outputManifest;
|
|
8826
|
-
const api = await resolveApi(
|
|
9072
|
+
const api = await resolveApi();
|
|
8827
9073
|
if (!api) return;
|
|
8828
9074
|
const hostedSpriteByPackageHash = /* @__PURE__ */ new Map();
|
|
8829
9075
|
for (const iconPackage of compiledIcons.packages) {
|
|
@@ -9108,13 +9354,6 @@ program.command("status").description("Show deployed compiled styles").option("-
|
|
|
9108
9354
|
try {
|
|
9109
9355
|
api = await requireApiOptions(options, {
|
|
9110
9356
|
capabilityScopes: ["status:read"],
|
|
9111
|
-
retryCommand: cliInvocation([
|
|
9112
|
-
"tileflow",
|
|
9113
|
-
"status",
|
|
9114
|
-
"--api-url",
|
|
9115
|
-
options.apiUrl ?? defaultApiUrl,
|
|
9116
|
-
...options.json ? ["--json"] : []
|
|
9117
|
-
]),
|
|
9118
9357
|
silent: options.json
|
|
9119
9358
|
});
|
|
9120
9359
|
} catch (error) {
|
|
@@ -9164,18 +9403,6 @@ registerIconDiffCommand(iconsCommand, {
|
|
|
9164
9403
|
assertExplicitKeyMap: true,
|
|
9165
9404
|
allowStoredCredential: allowsStoredDeployCredential(source),
|
|
9166
9405
|
capabilityScopes: ["status:read"],
|
|
9167
|
-
retryCommand: cliInvocation([
|
|
9168
|
-
"tileflow",
|
|
9169
|
-
"icons",
|
|
9170
|
-
"diff",
|
|
9171
|
-
"--against",
|
|
9172
|
-
options.against,
|
|
9173
|
-
...options.config ? ["--config", options.config] : [],
|
|
9174
|
-
"--map-id",
|
|
9175
|
-
options.mapId,
|
|
9176
|
-
"--api-url",
|
|
9177
|
-
options.apiUrl ?? defaultApiUrl
|
|
9178
|
-
]),
|
|
9179
9406
|
silent: true
|
|
9180
9407
|
});
|
|
9181
9408
|
}
|
|
@@ -9223,7 +9450,7 @@ async function runTileflowComparisonPreview(options, address) {
|
|
|
9223
9450
|
const emit = (event) => {
|
|
9224
9451
|
if (!options.json) return;
|
|
9225
9452
|
process.stdout.write(
|
|
9226
|
-
`${JSON.stringify({ schemaVersion: 1, command: "
|
|
9453
|
+
`${JSON.stringify({ schemaVersion: 1, command: "preview.compare", ...event })}
|
|
9227
9454
|
`
|
|
9228
9455
|
);
|
|
9229
9456
|
};
|
|
@@ -9613,19 +9840,13 @@ async function loadExistingDeployManifest(manifestPath, options) {
|
|
|
9613
9840
|
async function requireApiOptions(options, behavior = {}) {
|
|
9614
9841
|
const apiUrl = normalizeApiOrigin(options.apiUrl ?? defaultApiUrl);
|
|
9615
9842
|
const requestedMapId = options.mapId;
|
|
9616
|
-
|
|
9617
|
-
if (requestedMapId && !/^map_[A-Za-z0-9_-]{16}$/u.test(requestedMapId)) {
|
|
9843
|
+
if (!requestedMapId || !/^map_[A-Za-z0-9_-]{16}$/u.test(requestedMapId)) {
|
|
9618
9844
|
if (!behavior.silent) logError("Managed Map ID is invalid.");
|
|
9619
9845
|
process.exitCode = 1;
|
|
9620
9846
|
return null;
|
|
9621
9847
|
}
|
|
9622
|
-
if (options.project && !requested) {
|
|
9623
|
-
if (!behavior.silent) logError("Managed destination must use @organization/project syntax.");
|
|
9624
|
-
process.exitCode = 1;
|
|
9625
|
-
return null;
|
|
9626
|
-
}
|
|
9627
9848
|
if (options.apiKey) {
|
|
9628
|
-
if (
|
|
9849
|
+
if (behavior.assertExplicitKeyMap) {
|
|
9629
9850
|
const profile = await validateApiKey(apiUrl, options.apiKey);
|
|
9630
9851
|
if (!profile.ok) {
|
|
9631
9852
|
if (!behavior.silent) logError(profile.error);
|
|
@@ -9638,33 +9859,7 @@ async function requireApiOptions(options, behavior = {}) {
|
|
|
9638
9859
|
return null;
|
|
9639
9860
|
}
|
|
9640
9861
|
}
|
|
9641
|
-
|
|
9642
|
-
const profile = await validateApiKey(apiUrl, options.apiKey);
|
|
9643
|
-
if (!profile.ok) {
|
|
9644
|
-
if (!behavior.silent) logError(profile.error);
|
|
9645
|
-
process.exitCode = 1;
|
|
9646
|
-
return null;
|
|
9647
|
-
}
|
|
9648
|
-
if (profile.value.credentialType === "team_api_key") {
|
|
9649
|
-
if (!behavior.silent) logError("Team credentials require an explicit --map-id.");
|
|
9650
|
-
process.exitCode = 1;
|
|
9651
|
-
return null;
|
|
9652
|
-
}
|
|
9653
|
-
if (requested && projectReference(profile.value) !== options.project) {
|
|
9654
|
-
if (!behavior.silent) {
|
|
9655
|
-
logError(
|
|
9656
|
-
`This project key belongs to ${projectReference(profile.value)}, not ${options.project}.`
|
|
9657
|
-
);
|
|
9658
|
-
}
|
|
9659
|
-
process.exitCode = 1;
|
|
9660
|
-
return null;
|
|
9661
|
-
}
|
|
9662
|
-
}
|
|
9663
|
-
return {
|
|
9664
|
-
apiKey: options.apiKey,
|
|
9665
|
-
apiUrl,
|
|
9666
|
-
...requestedMapId ? { mapId: requestedMapId } : {}
|
|
9667
|
-
};
|
|
9862
|
+
return { apiKey: options.apiKey, apiUrl, mapId: requestedMapId };
|
|
9668
9863
|
}
|
|
9669
9864
|
const allowStoredCredential = behavior.allowStoredCredential !== false && allowsStoredDeployCredential(resolveDeploySource(process.env));
|
|
9670
9865
|
if (!allowStoredCredential) {
|
|
@@ -9697,46 +9892,9 @@ async function requireApiOptions(options, behavior = {}) {
|
|
|
9697
9892
|
process.exitCode = 1;
|
|
9698
9893
|
return null;
|
|
9699
9894
|
}
|
|
9700
|
-
|
|
9701
|
-
const capability2 = await requestMapCapability(
|
|
9702
|
-
resolvedSession.session,
|
|
9703
|
-
{ mapId: requestedMapId },
|
|
9704
|
-
behavior.capabilityScopes ?? ["status:read"]
|
|
9705
|
-
);
|
|
9706
|
-
if (!capability2.ok) {
|
|
9707
|
-
if (!behavior.silent) logError(capability2.error);
|
|
9708
|
-
process.exitCode = 1;
|
|
9709
|
-
return null;
|
|
9710
|
-
}
|
|
9711
|
-
return { apiKey: capability2.capability, apiUrl, mapId: capability2.mapId };
|
|
9712
|
-
}
|
|
9713
|
-
const target = await resolveAccountProjectTarget(resolvedSession.session, options.project);
|
|
9714
|
-
if (target.kind === "remote") {
|
|
9715
|
-
if (!behavior.silent) logError(target.error);
|
|
9716
|
-
process.exitCode = 1;
|
|
9717
|
-
return null;
|
|
9718
|
-
}
|
|
9719
|
-
if (target.kind !== "selected") {
|
|
9720
|
-
if (!behavior.silent) {
|
|
9721
|
-
const references = target.targets.map((candidate) => candidate.reference);
|
|
9722
|
-
if (target.kind === "ambiguous") {
|
|
9723
|
-
const retry = `${behavior.retryCommand ?? "tileflow <command>"} --project ${references[0]}`;
|
|
9724
|
-
logError(`Managed destination is ambiguous: ${references.join(", ")}.`);
|
|
9725
|
-
printNextSteps([`Retry exactly with ${command(retry)}.`]);
|
|
9726
|
-
} else if (target.kind === "invalid") {
|
|
9727
|
-
logError("Managed destination must use @organization/project syntax.");
|
|
9728
|
-
} else {
|
|
9729
|
-
logError(
|
|
9730
|
-
options.project ? `Destination ${options.project} is not accessible to this account.` : "No active managed destination is accessible to this account."
|
|
9731
|
-
);
|
|
9732
|
-
}
|
|
9733
|
-
}
|
|
9734
|
-
process.exitCode = 1;
|
|
9735
|
-
return null;
|
|
9736
|
-
}
|
|
9737
|
-
const capability = await requestProjectCapability(
|
|
9895
|
+
const capability = await requestMapCapability(
|
|
9738
9896
|
resolvedSession.session,
|
|
9739
|
-
|
|
9897
|
+
{ mapId: requestedMapId },
|
|
9740
9898
|
behavior.capabilityScopes ?? ["status:read"]
|
|
9741
9899
|
);
|
|
9742
9900
|
if (!capability.ok) {
|
|
@@ -9744,7 +9902,7 @@ async function requireApiOptions(options, behavior = {}) {
|
|
|
9744
9902
|
process.exitCode = 1;
|
|
9745
9903
|
return null;
|
|
9746
9904
|
}
|
|
9747
|
-
return { apiKey: capability.capability, apiUrl };
|
|
9905
|
+
return { apiKey: capability.capability, apiUrl, mapId: capability.mapId };
|
|
9748
9906
|
}
|
|
9749
9907
|
function printTileflowPreviewError(error) {
|
|
9750
9908
|
if (error instanceof TileflowStyleValidationError2) {
|
|
@@ -9895,12 +10053,6 @@ function plural(count, singular, pluralForm = `${singular}s`) {
|
|
|
9895
10053
|
function command(value) {
|
|
9896
10054
|
return pc6.cyan(value);
|
|
9897
10055
|
}
|
|
9898
|
-
function cliInvocation(parts) {
|
|
9899
|
-
return parts.map(quoteCliArgument).join(" ");
|
|
9900
|
-
}
|
|
9901
|
-
function quoteCliArgument(value) {
|
|
9902
|
-
return /^[A-Za-z0-9_./:@%+=,-]+$/u.test(value) ? value : `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
9903
|
-
}
|
|
9904
10056
|
function link(value) {
|
|
9905
10057
|
return pc6.cyan(value);
|
|
9906
10058
|
}
|
|
@@ -37,8 +37,8 @@ npx tileflow preview --scene madrid-mobile
|
|
|
37
37
|
To work on another map, point `--config` at another singular config. Multi-map catalogs are internal
|
|
38
38
|
orchestration used by this repository's workbench and are not a second public config shape.
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
The `preview` name distinguishes this SDK preview server from an application's own `npm run dev`
|
|
41
|
+
process.
|
|
42
42
|
|
|
43
43
|
`--api-base-url` selects the origin used by official World, Bathymetry vector, Bathymetry DEM,
|
|
44
44
|
Nautical, and terrain TileJSON sources in the generated preview style. In particular, a hybrid
|
package/docs/command-families.md
CHANGED
|
@@ -5,7 +5,7 @@ Start with the [tileflow guide](https://github.com/tileflow/tileflow-sdk/blob/ma
|
|
|
5
5
|
| Family | Commands |
|
|
6
6
|
| ----------------- | ---------------------------------------------------------------------------------------------- |
|
|
7
7
|
| Semantic language | `language manifest`, `language schema` |
|
|
8
|
-
| Map authoring | `init`, `validate`, `inspect`, `explain`, `semantic-diff`, `build`, `preview`
|
|
8
|
+
| Map authoring | `init`, `validate`, `inspect`, `explain`, `semantic-diff`, `build`, `preview` |
|
|
9
9
|
| Local evidence | `setup capture`, `capture`, `visual compare`, `visual analyze`, `visual diff`, `visual update` |
|
|
10
10
|
| Data and assets | `tileset inspect`, `tileset publish/list/status/purge`, `inspect features`, `icons list/diff` |
|
|
11
11
|
| Team Icon Sets | `icon-set publish/list/status/versions/uses/archive/unarchive/purge` |
|
|
@@ -15,7 +15,7 @@ Start with the [tileflow guide](https://github.com/tileflow/tileflow-sdk/blob/ma
|
|
|
15
15
|
|
|
16
16
|
Run `tileflow <command> --help` (or the family help, such as `tileflow icons --help`) for the exact
|
|
17
17
|
arguments and bounded JSON modes. `preview` defaults to the existing web renderer. The explicit
|
|
18
|
-
`preview --renderer native`
|
|
18
|
+
`preview --renderer native` mode serves watched `native-v1` artifacts only;
|
|
19
19
|
Metro remains the React Native JavaScript development server. See
|
|
20
20
|
[Native artifacts and local preview](./native-artifacts.md) for the endpoint and qualification
|
|
21
21
|
boundary.
|
|
@@ -28,5 +28,5 @@ authority when resolving revisions and are the only commands that select revisio
|
|
|
28
28
|
`tileflow.icons.lock.json`. See the
|
|
29
29
|
[Team Icon Set workflow](https://github.com/tileflow/tileflow-sdk/blob/main/packages/cli/docs/team-icon-sets.md).
|
|
30
30
|
|
|
31
|
-
The
|
|
32
|
-
|
|
31
|
+
The hidden `projects` command family lists and manages the internal application destinations of an
|
|
32
|
+
account for support. It is not part of the ordinary workflow or the product catalog.
|
package/docs/native-artifacts.md
CHANGED
|
@@ -30,7 +30,7 @@ The ordinary web commands and their default artifact bytes retain their existing
|
|
|
30
30
|
|
|
31
31
|
## Watch and serve native artifacts
|
|
32
32
|
|
|
33
|
-
`preview`
|
|
33
|
+
`preview` selects the renderer with `--renderer`:
|
|
34
34
|
|
|
35
35
|
```sh
|
|
36
36
|
npx tileflow preview --renderer native --map stores --theme light
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tileflow",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.12",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "CLI-only tools for Tileflow config, capture, visual testing, and deploy.",
|
|
6
6
|
"type": "module",
|
|
@@ -35,15 +35,15 @@
|
|
|
35
35
|
"access": "public"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@hono/node-server": "2.
|
|
39
|
-
"@tileflow/capture": ">=0.1.0-alpha.
|
|
40
|
-
"@tileflow/coordinates": ">=0.1.0-alpha.
|
|
41
|
-
"@tileflow/coordinates-runtime": ">=0.1.0-alpha.
|
|
42
|
-
"@tileflow/core": ">=0.1.0-alpha.
|
|
43
|
-
"@tileflow/dev": ">=0.1.0-alpha.
|
|
44
|
-
"@tileflow/maps": ">=0.1.0-alpha.
|
|
38
|
+
"@hono/node-server": "2.1.3",
|
|
39
|
+
"@tileflow/capture": ">=0.1.0-alpha.30 <0.1.0-beta.0",
|
|
40
|
+
"@tileflow/coordinates": ">=0.1.0-alpha.2 <0.1.0-beta.0",
|
|
41
|
+
"@tileflow/coordinates-runtime": ">=0.1.0-alpha.5 <0.1.0-beta.0",
|
|
42
|
+
"@tileflow/core": ">=0.1.0-alpha.34 <0.1.0-beta.0",
|
|
43
|
+
"@tileflow/dev": ">=0.1.0-alpha.31 <0.1.0-beta.0",
|
|
44
|
+
"@tileflow/maps": ">=0.1.0-alpha.8 <0.1.0-beta.0",
|
|
45
45
|
"commander": "^15.0.0",
|
|
46
|
-
"hono": "4.13.
|
|
46
|
+
"hono": "4.13.7",
|
|
47
47
|
"picocolors": "^1.1.1",
|
|
48
48
|
"zod": "^4.4.3"
|
|
49
49
|
},
|