@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.
Files changed (120) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +446 -0
  3. package/dist/aws.d.ts +28 -0
  4. package/dist/aws.d.ts.map +1 -0
  5. package/dist/aws.js +104 -0
  6. package/dist/aws.js.map +1 -0
  7. package/dist/bin.d.ts +3 -0
  8. package/dist/bin.d.ts.map +1 -0
  9. package/dist/bin.js +132 -0
  10. package/dist/bin.js.map +1 -0
  11. package/dist/bridge/exportFiles.d.ts +16 -0
  12. package/dist/bridge/exportFiles.d.ts.map +1 -0
  13. package/dist/bridge/exportFiles.js +95 -0
  14. package/dist/bridge/exportFiles.js.map +1 -0
  15. package/dist/bridge/pair.d.ts +103 -0
  16. package/dist/bridge/pair.d.ts.map +1 -0
  17. package/dist/bridge/pair.js +219 -0
  18. package/dist/bridge/pair.js.map +1 -0
  19. package/dist/bridge/protocol.d.ts +187 -0
  20. package/dist/bridge/protocol.d.ts.map +1 -0
  21. package/dist/bridge/protocol.js +132 -0
  22. package/dist/bridge/protocol.js.map +1 -0
  23. package/dist/bridge/server.d.ts +95 -0
  24. package/dist/bridge/server.d.ts.map +1 -0
  25. package/dist/bridge/server.js +669 -0
  26. package/dist/bridge/server.js.map +1 -0
  27. package/dist/cdk-scaffold.d.ts +16 -0
  28. package/dist/cdk-scaffold.d.ts.map +1 -0
  29. package/dist/cdk-scaffold.js +28 -0
  30. package/dist/cdk-scaffold.js.map +1 -0
  31. package/dist/codegen.d.ts +5 -0
  32. package/dist/codegen.d.ts.map +1 -0
  33. package/dist/codegen.js +27 -0
  34. package/dist/codegen.js.map +1 -0
  35. package/dist/diff.d.ts +33 -0
  36. package/dist/diff.d.ts.map +1 -0
  37. package/dist/diff.js +225 -0
  38. package/dist/diff.js.map +1 -0
  39. package/dist/docs.d.ts +46 -0
  40. package/dist/docs.d.ts.map +1 -0
  41. package/dist/docs.js +223 -0
  42. package/dist/docs.js.map +1 -0
  43. package/dist/emit.d.ts +8 -0
  44. package/dist/emit.d.ts.map +1 -0
  45. package/dist/emit.js +55 -0
  46. package/dist/emit.js.map +1 -0
  47. package/dist/errors.d.ts +12 -0
  48. package/dist/errors.d.ts.map +1 -0
  49. package/dist/errors.js +25 -0
  50. package/dist/errors.js.map +1 -0
  51. package/dist/export.d.ts +27 -0
  52. package/dist/export.d.ts.map +1 -0
  53. package/dist/export.js +126 -0
  54. package/dist/export.js.map +1 -0
  55. package/dist/index.d.ts +4 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +23 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/lint.d.ts +6 -0
  60. package/dist/lint.d.ts.map +1 -0
  61. package/dist/lint.js +31 -0
  62. package/dist/lint.js.map +1 -0
  63. package/dist/render.d.ts +6 -0
  64. package/dist/render.d.ts.map +1 -0
  65. package/dist/render.js +49 -0
  66. package/dist/render.js.map +1 -0
  67. package/dist/run.d.ts +13 -0
  68. package/dist/run.d.ts.map +1 -0
  69. package/dist/run.js +38 -0
  70. package/dist/run.js.map +1 -0
  71. package/dist/simulate.d.ts +35 -0
  72. package/dist/simulate.d.ts.map +1 -0
  73. package/dist/simulate.js +194 -0
  74. package/dist/simulate.js.map +1 -0
  75. package/dist/studio.d.ts +23 -0
  76. package/dist/studio.d.ts.map +1 -0
  77. package/dist/studio.js +120 -0
  78. package/dist/studio.js.map +1 -0
  79. package/dist/synth-resolve-hook.d.ts +19 -0
  80. package/dist/synth-resolve-hook.d.ts.map +1 -0
  81. package/dist/synth-resolve-hook.js +127 -0
  82. package/dist/synth-resolve-hook.js.map +1 -0
  83. package/dist/synth-runner.d.ts +12 -0
  84. package/dist/synth-runner.d.ts.map +1 -0
  85. package/dist/synth-runner.js +145 -0
  86. package/dist/synth-runner.js.map +1 -0
  87. package/dist/synth.d.ts +55 -0
  88. package/dist/synth.d.ts.map +1 -0
  89. package/dist/synth.js +327 -0
  90. package/dist/synth.js.map +1 -0
  91. package/dist/watch.d.ts +66 -0
  92. package/dist/watch.d.ts.map +1 -0
  93. package/dist/watch.js +293 -0
  94. package/dist/watch.js.map +1 -0
  95. package/package.json +85 -0
  96. package/schema/flowdoc-0.1.schema.json +669 -0
  97. package/schema/scenario-0.1.schema.json +318 -0
  98. package/src/aws.ts +149 -0
  99. package/src/bin.ts +179 -0
  100. package/src/bridge/exportFiles.ts +116 -0
  101. package/src/bridge/pair.ts +312 -0
  102. package/src/bridge/protocol.ts +289 -0
  103. package/src/bridge/server.ts +827 -0
  104. package/src/cdk-scaffold.ts +41 -0
  105. package/src/codegen.ts +36 -0
  106. package/src/diff.ts +279 -0
  107. package/src/docs.ts +249 -0
  108. package/src/emit.ts +69 -0
  109. package/src/errors.ts +27 -0
  110. package/src/export.ts +167 -0
  111. package/src/index.ts +27 -0
  112. package/src/lint.ts +42 -0
  113. package/src/render.ts +58 -0
  114. package/src/run.ts +42 -0
  115. package/src/simulate.ts +244 -0
  116. package/src/studio.ts +143 -0
  117. package/src/synth-resolve-hook.ts +152 -0
  118. package/src/synth-runner.ts +157 -0
  119. package/src/synth.ts +380 -0
  120. 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
+ }