@flow-as-code/cli 0.1.0
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/LICENSE +202 -0
- package/README.md +446 -0
- package/dist/aws.d.ts +28 -0
- package/dist/aws.d.ts.map +1 -0
- package/dist/aws.js +104 -0
- package/dist/aws.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +132 -0
- package/dist/bin.js.map +1 -0
- package/dist/bridge/exportFiles.d.ts +16 -0
- package/dist/bridge/exportFiles.d.ts.map +1 -0
- package/dist/bridge/exportFiles.js +95 -0
- package/dist/bridge/exportFiles.js.map +1 -0
- package/dist/bridge/pair.d.ts +103 -0
- package/dist/bridge/pair.d.ts.map +1 -0
- package/dist/bridge/pair.js +219 -0
- package/dist/bridge/pair.js.map +1 -0
- package/dist/bridge/protocol.d.ts +187 -0
- package/dist/bridge/protocol.d.ts.map +1 -0
- package/dist/bridge/protocol.js +132 -0
- package/dist/bridge/protocol.js.map +1 -0
- package/dist/bridge/server.d.ts +95 -0
- package/dist/bridge/server.d.ts.map +1 -0
- package/dist/bridge/server.js +669 -0
- package/dist/bridge/server.js.map +1 -0
- package/dist/cdk-scaffold.d.ts +16 -0
- package/dist/cdk-scaffold.d.ts.map +1 -0
- package/dist/cdk-scaffold.js +28 -0
- package/dist/cdk-scaffold.js.map +1 -0
- package/dist/codegen.d.ts +5 -0
- package/dist/codegen.d.ts.map +1 -0
- package/dist/codegen.js +27 -0
- package/dist/codegen.js.map +1 -0
- package/dist/diff.d.ts +33 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +225 -0
- package/dist/diff.js.map +1 -0
- package/dist/docs.d.ts +46 -0
- package/dist/docs.d.ts.map +1 -0
- package/dist/docs.js +223 -0
- package/dist/docs.js.map +1 -0
- package/dist/emit.d.ts +8 -0
- package/dist/emit.d.ts.map +1 -0
- package/dist/emit.js +55 -0
- package/dist/emit.js.map +1 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +25 -0
- package/dist/errors.js.map +1 -0
- package/dist/export.d.ts +27 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/export.js +126 -0
- package/dist/export.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/lint.d.ts +6 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +31 -0
- package/dist/lint.js.map +1 -0
- package/dist/render.d.ts +6 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +49 -0
- package/dist/render.js.map +1 -0
- package/dist/run.d.ts +13 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +38 -0
- package/dist/run.js.map +1 -0
- package/dist/simulate.d.ts +35 -0
- package/dist/simulate.d.ts.map +1 -0
- package/dist/simulate.js +194 -0
- package/dist/simulate.js.map +1 -0
- package/dist/studio.d.ts +23 -0
- package/dist/studio.d.ts.map +1 -0
- package/dist/studio.js +120 -0
- package/dist/studio.js.map +1 -0
- package/dist/synth-resolve-hook.d.ts +19 -0
- package/dist/synth-resolve-hook.d.ts.map +1 -0
- package/dist/synth-resolve-hook.js +127 -0
- package/dist/synth-resolve-hook.js.map +1 -0
- package/dist/synth-runner.d.ts +12 -0
- package/dist/synth-runner.d.ts.map +1 -0
- package/dist/synth-runner.js +145 -0
- package/dist/synth-runner.js.map +1 -0
- package/dist/synth.d.ts +55 -0
- package/dist/synth.d.ts.map +1 -0
- package/dist/synth.js +327 -0
- package/dist/synth.js.map +1 -0
- package/dist/watch.d.ts +66 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +293 -0
- package/dist/watch.js.map +1 -0
- package/package.json +85 -0
- package/schema/flowdoc-0.1.schema.json +669 -0
- package/schema/scenario-0.1.schema.json +318 -0
- package/src/aws.ts +149 -0
- package/src/bin.ts +179 -0
- package/src/bridge/exportFiles.ts +116 -0
- package/src/bridge/pair.ts +312 -0
- package/src/bridge/protocol.ts +289 -0
- package/src/bridge/server.ts +827 -0
- package/src/cdk-scaffold.ts +41 -0
- package/src/codegen.ts +36 -0
- package/src/diff.ts +279 -0
- package/src/docs.ts +249 -0
- package/src/emit.ts +69 -0
- package/src/errors.ts +27 -0
- package/src/export.ts +167 -0
- package/src/index.ts +27 -0
- package/src/lint.ts +42 -0
- package/src/render.ts +58 -0
- package/src/run.ts +42 -0
- package/src/simulate.ts +244 -0
- package/src/studio.ts +143 -0
- package/src/synth-resolve-hook.ts +152 -0
- package/src/synth-runner.ts +157 -0
- package/src/synth.ts +380 -0
- package/src/watch.ts +388 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 The flow-as-code Authors
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
// The `flow-cli studio` bridge protocol: the whole contract between the local
|
|
6
|
+
// server (packages/cli/src/bridge/server.ts) and the studio's BridgeStore
|
|
7
|
+
// (packages/studio/src/store/bridgeStore.ts).
|
|
8
|
+
//
|
|
9
|
+
// This file exists twice, byte for byte, once in each package. The studio does
|
|
10
|
+
// not depend on @flow-as-code/cli (which depends on the studio, for its built
|
|
11
|
+
// assets), so importing across would make the dependency circular. The copy is
|
|
12
|
+
// only safe while it is provably a copy: packages/cli/src/bridge/
|
|
13
|
+
// protocol.test.ts fails if the two files differ by a single byte, the same
|
|
14
|
+
// arrangement src/schema.test.ts uses for the packaged FlowDoc schema.
|
|
15
|
+
//
|
|
16
|
+
// Transport notes that belong to the contract, not to either implementation:
|
|
17
|
+
//
|
|
18
|
+
// - Everything is JSON over plain HTTP on 127.0.0.1. No websocket library
|
|
19
|
+
// and no EventSource: the event stream is a long poll, which needs nothing
|
|
20
|
+
// but fetch, behaves identically in a browser and in a node test, and
|
|
21
|
+
// cannot half-close the way a streamed response can.
|
|
22
|
+
// - The client holds one GET /bridge/events?cursor=<seq> open at a time. The
|
|
23
|
+
// server answers immediately when it already has events past the cursor,
|
|
24
|
+
// and otherwise parks the request until one arrives or the poll times out
|
|
25
|
+
// with an empty batch. The client then re-polls from the returned cursor,
|
|
26
|
+
// so no event can be missed between two polls.
|
|
27
|
+
// - Every document is addressed by NAME, never by path. A name is a
|
|
28
|
+
// @flow-as-code/core slug; the server refuses anything else, which is also what
|
|
29
|
+
// keeps ".." out of the file paths it builds.
|
|
30
|
+
|
|
31
|
+
import type { FlowDoc } from "@flow-as-code/core";
|
|
32
|
+
import { SLUG_PATTERN } from "@flow-as-code/core";
|
|
33
|
+
|
|
34
|
+
/** Bumped when a change to this file is not backwards compatible. */
|
|
35
|
+
export const BRIDGE_PROTOCOL = 1;
|
|
36
|
+
|
|
37
|
+
/** Every bridge route lives under this prefix; everything else is an asset. */
|
|
38
|
+
export const BRIDGE_PREFIX = "/bridge";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The global the server injects into the served index.html. Its presence is
|
|
42
|
+
* how the studio knows it is being served by the bridge rather than opened as
|
|
43
|
+
* a static build, so the app never has to speculatively probe for a server
|
|
44
|
+
* that is not there.
|
|
45
|
+
*/
|
|
46
|
+
export const BRIDGE_GLOBAL = "__FLOW_STUDIO_BRIDGE__";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Per-session secret proving a request came from the page flow-cli opened.
|
|
50
|
+
*
|
|
51
|
+
* Binding to loopback keeps the network out but not the developer's own
|
|
52
|
+
* browser: any site they visit can send this server a CORS-simple POST. A page
|
|
53
|
+
* cannot read the URL of a document it did not open, so it cannot learn this,
|
|
54
|
+
* which is what separates the studio from any other tab.
|
|
55
|
+
*/
|
|
56
|
+
export const TOKEN_PARAM = "token";
|
|
57
|
+
export const TOKEN_HEADER = "x-flow-studio-token";
|
|
58
|
+
|
|
59
|
+
/** The file suffixes the bridge pairs, matching the A04 watch engine. */
|
|
60
|
+
export const DOC_SUFFIX = ".flowdoc.json";
|
|
61
|
+
export const TS_SUFFIX = ".flow.ts";
|
|
62
|
+
|
|
63
|
+
/** What the bridge says about itself. Injected, and served at /bridge/info. */
|
|
64
|
+
export interface BridgeInfo {
|
|
65
|
+
protocol: number;
|
|
66
|
+
/** Absolute path of the served directory, for the toolbar. */
|
|
67
|
+
dir: string;
|
|
68
|
+
/** Short label for the toolbar (the directory's base name). */
|
|
69
|
+
label: string;
|
|
70
|
+
/** Session token; every bridge API request must present it. */
|
|
71
|
+
token: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface BridgeDocRef {
|
|
75
|
+
name: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface BridgeDocList {
|
|
79
|
+
docs: BridgeDocRef[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A document as it exists on disk right now. `text` is the exact file bytes. */
|
|
83
|
+
export interface BridgeDocPayload {
|
|
84
|
+
name: string;
|
|
85
|
+
doc: FlowDoc;
|
|
86
|
+
text: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** PUT /bridge/docs/<name> */
|
|
90
|
+
export interface BridgeWriteRequest {
|
|
91
|
+
doc: FlowDoc;
|
|
92
|
+
/**
|
|
93
|
+
* Write even though the builder file no longer matches the doc's
|
|
94
|
+
* meta.sourceHash. This is the user answering the conflict dialog with "keep
|
|
95
|
+
* the canvas version", and it is the ONLY thing that may set it: a write
|
|
96
|
+
* that sets it by default would be the silent overwrite the dirty guard
|
|
97
|
+
* exists to prevent.
|
|
98
|
+
*/
|
|
99
|
+
force?: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** What a successful write wrote, both halves of the pair. */
|
|
103
|
+
export interface BridgeWriteResult extends BridgeDocPayload {
|
|
104
|
+
docPath: string;
|
|
105
|
+
tsPath: string;
|
|
106
|
+
/** The regenerated builder source. */
|
|
107
|
+
tsText: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Which side of a conflict the user chose: the FlowDoc or the builder code. */
|
|
111
|
+
export type ConflictSide = "doc" | "code";
|
|
112
|
+
|
|
113
|
+
/** POST /bridge/docs/<name>/resolve */
|
|
114
|
+
export interface BridgeResolveRequest {
|
|
115
|
+
side: ConflictSide;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Both sides of a dirty-both pair, so the studio can show the diff and ask.
|
|
120
|
+
* Either side can be null when it cannot be read (unparseable JSON, a builder
|
|
121
|
+
* file that throws); the matching *Error says why, and the UI must then offer
|
|
122
|
+
* only the side it has.
|
|
123
|
+
*/
|
|
124
|
+
export interface BridgeConflict {
|
|
125
|
+
name: string;
|
|
126
|
+
/** The sentence describing what diverged, from whichever side noticed. */
|
|
127
|
+
reason: string;
|
|
128
|
+
/**
|
|
129
|
+
* Where the canvas side lives, which decides how the choice is applied.
|
|
130
|
+
*
|
|
131
|
+
* "disk" both sides are files: the watcher found the pair diverged.
|
|
132
|
+
* Either choice is a POST to /resolve.
|
|
133
|
+
* "canvas" the canvas side is the unsaved document in the studio (a write
|
|
134
|
+
* the bridge refused, or a builder-file edit arriving while the
|
|
135
|
+
* canvas had unsaved changes). Keeping it is a forced write of
|
|
136
|
+
* that document, which only the studio holds.
|
|
137
|
+
*/
|
|
138
|
+
origin: "disk" | "canvas";
|
|
139
|
+
/** Absolute paths, when the side that raised the conflict knows them. */
|
|
140
|
+
docPath?: string;
|
|
141
|
+
tsPath?: string;
|
|
142
|
+
/** The canvas side: the FlowDoc on disk, or the one the studio holds. */
|
|
143
|
+
docSide: FlowDoc | null;
|
|
144
|
+
/** The FlowDoc the builder file synths to right now. */
|
|
145
|
+
codeSide: FlowDoc | null;
|
|
146
|
+
docError?: string;
|
|
147
|
+
codeError?: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The A04 watch engine's three events, enriched with the payload the studio
|
|
152
|
+
* needs: "synced" carries the new document so the canvas can hot-reload
|
|
153
|
+
* without a second request, and "conflict" carries both sides.
|
|
154
|
+
*/
|
|
155
|
+
export type BridgeEvent =
|
|
156
|
+
| ({ seq: number; kind: "synced" } & BridgeDocPayload)
|
|
157
|
+
| ({ seq: number; kind: "conflict" } & BridgeConflict)
|
|
158
|
+
| { seq: number; kind: "error"; name?: string; path: string; message: string };
|
|
159
|
+
|
|
160
|
+
export interface BridgeEventBatch {
|
|
161
|
+
/** Highest seq in this batch, or the cursor unchanged when it is empty. */
|
|
162
|
+
cursor: number;
|
|
163
|
+
events: BridgeEvent[];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Every non-2xx response body. */
|
|
167
|
+
export interface BridgeErrorBody {
|
|
168
|
+
error: string;
|
|
169
|
+
/** Set on the 409 a write gets while the pair is in conflict. */
|
|
170
|
+
conflict?: BridgeConflict;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The export targets, the same three `flow-cli emit` and `render` produce. */
|
|
174
|
+
export const EXPORT_TARGETS = ["cdk", "raw", "tf"] as const;
|
|
175
|
+
export type ExportTarget = (typeof EXPORT_TARGETS)[number];
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* POST /bridge/export: the studio has emitted a file map and the CLI writes
|
|
179
|
+
* it, because the browser cannot and the bridge is the only thing here that
|
|
180
|
+
* touches disk. The studio computes the bytes (it holds the documents and the
|
|
181
|
+
* address or resource map); the server only checks and writes them.
|
|
182
|
+
*
|
|
183
|
+
* Deliberately not a document write: nothing is paired, nothing is synced, no
|
|
184
|
+
* event is published, and existing files are overwritten in place exactly as
|
|
185
|
+
* `flow-cli emit` overwrites them.
|
|
186
|
+
*/
|
|
187
|
+
export interface BridgeExportRequest {
|
|
188
|
+
target: ExportTarget;
|
|
189
|
+
/**
|
|
190
|
+
* Relative POSIX path to file content, each path satisfying isExportPath and
|
|
191
|
+
* taken relative to `subdir`.
|
|
192
|
+
*/
|
|
193
|
+
files: Record<string, string>;
|
|
194
|
+
/**
|
|
195
|
+
* Subdirectory of the served directory to write into. Absent or "" writes to
|
|
196
|
+
* its root, which is where `flow-cli emit` writes with no --out.
|
|
197
|
+
*/
|
|
198
|
+
subdir?: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface BridgeExportResult {
|
|
202
|
+
target: ExportTarget;
|
|
203
|
+
/** Absolute paths written, sorted. */
|
|
204
|
+
paths: string[];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Most files an export may carry, and the most bytes across all of them. */
|
|
208
|
+
export const EXPORT_MAX_FILES = 200;
|
|
209
|
+
export const EXPORT_MAX_BYTES = 4 * 1024 * 1024;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* One path segment of an export path: printable, no separator, no traversal,
|
|
213
|
+
* and never a dotfile. Anything else is refused rather than sanitized, because
|
|
214
|
+
* a sanitizer is a guess about intent and this writes files the user owns.
|
|
215
|
+
*/
|
|
216
|
+
const EXPORT_SEGMENT = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;
|
|
217
|
+
|
|
218
|
+
/** Deepest an export path may nest, counting the file itself. */
|
|
219
|
+
const EXPORT_MAX_DEPTH = 4;
|
|
220
|
+
|
|
221
|
+
function exportSegments(path: string, maxDepth: number): string[] | undefined {
|
|
222
|
+
if (path === "" || path.length > 200 || path.includes("\0") || path.includes("\\")) {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
const segments = path.split("/");
|
|
226
|
+
if (segments.length > maxDepth) return undefined;
|
|
227
|
+
return segments.every((s) => EXPORT_SEGMENT.test(s)) ? segments : undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** `flows.tf`, `flows/support-line.flow.tftpl`: relative, POSIX, no traversal. */
|
|
231
|
+
export function isExportPath(path: string): boolean {
|
|
232
|
+
return exportSegments(path, EXPORT_MAX_DEPTH) !== undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** A destination subdirectory: the same rule, and "" for the served root. */
|
|
236
|
+
export function isExportSubdir(subdir: string): boolean {
|
|
237
|
+
return subdir === "" || exportSegments(subdir, EXPORT_MAX_DEPTH - 1) !== undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Documents are addressed by @flow-as-code/core slug, which is also the path guard. */
|
|
241
|
+
export function isBridgeDocName(name: string): boolean {
|
|
242
|
+
return SLUG_PATTERN.test(name);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function infoUrl(base: string): string {
|
|
246
|
+
return `${base}${BRIDGE_PREFIX}/info`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function docsUrl(base: string): string {
|
|
250
|
+
return `${base}${BRIDGE_PREFIX}/docs`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function docUrl(base: string, name: string): string {
|
|
254
|
+
return `${docsUrl(base)}/${encodeURIComponent(name)}`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function resolveUrl(base: string, name: string): string {
|
|
258
|
+
return `${docUrl(base, name)}/resolve`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function eventsUrl(base: string, cursor: number): string {
|
|
262
|
+
return `${base}${BRIDGE_PREFIX}/events?cursor=${String(cursor)}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function exportUrl(base: string): string {
|
|
266
|
+
return `${base}${BRIDGE_PREFIX}/export`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Type guard for the injected global, used by the studio at boot. */
|
|
270
|
+
export function readBridgeInfo(scope: unknown): BridgeInfo | undefined {
|
|
271
|
+
if (scope === null || typeof scope !== "object") return undefined;
|
|
272
|
+
const value = (scope as Record<string, unknown>)[BRIDGE_GLOBAL];
|
|
273
|
+
if (value === null || typeof value !== "object") return undefined;
|
|
274
|
+
const info = value as Partial<BridgeInfo>;
|
|
275
|
+
if (info.protocol !== BRIDGE_PROTOCOL) return undefined;
|
|
276
|
+
if (typeof info.token !== "string" || info.token === "") return undefined;
|
|
277
|
+
if (typeof info.dir !== "string" || typeof info.label !== "string") return undefined;
|
|
278
|
+
return { protocol: info.protocol, dir: info.dir, label: info.label, token: info.token };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The script tag the server injects into index.html. "<" is escaped so a
|
|
283
|
+
* directory name containing "</script>" cannot close the tag it sits in;
|
|
284
|
+
* JSON.stringify handles the rest, and the result is still valid JSON.
|
|
285
|
+
*/
|
|
286
|
+
export function bridgeBootScript(info: BridgeInfo): string {
|
|
287
|
+
const json = JSON.stringify(info).replace(/</g, "\\u003c");
|
|
288
|
+
return `<script>window.${BRIDGE_GLOBAL} = ${json};</script>`;
|
|
289
|
+
}
|