@visualautomate/cli 0.3.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +139 -0
  3. package/dist/index.js +2468 -0
  4. package/package.json +44 -0
package/dist/index.js ADDED
@@ -0,0 +1,2468 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { createServer } from "node:http";
5
+ import { mkdir, readFile as readFile2, writeFile } from "node:fs/promises";
6
+ import { existsSync as existsSync2, watch } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { dirname, join as join2, relative as relative2, resolve as resolve2, sep } from "node:path";
9
+ import { randomBytes } from "node:crypto";
10
+ import { spawn } from "node:child_process";
11
+ import { createRequire } from "node:module";
12
+ import { build, stop as stopBundler } from "esbuild";
13
+
14
+ // ../plugin-sdk/src/simulate.ts
15
+ function toBytes(data) {
16
+ return typeof data === "string" ? new TextEncoder().encode(data) : data;
17
+ }
18
+ function createLocalStorage(seed = {}) {
19
+ const files = /* @__PURE__ */ new Map();
20
+ for (const [path, data] of Object.entries(seed)) {
21
+ files.set(path, {
22
+ bytes: toBytes(data),
23
+ contentType: "application/octet-stream",
24
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
25
+ });
26
+ }
27
+ const result = (path, file2) => ({
28
+ url: `memory://${path}`,
29
+ size: file2.bytes.length,
30
+ contentType: file2.contentType,
31
+ uploadedAt: file2.uploadedAt,
32
+ // The real one expires; this one does not, and says so rather than
33
+ // inventing a date a test might come to rely on.
34
+ expiresAt: "never (local simulation)"
35
+ });
36
+ const storage = {
37
+ async put(path, data, contentType = "application/octet-stream") {
38
+ const file2 = {
39
+ bytes: toBytes(data),
40
+ contentType,
41
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
42
+ };
43
+ files.set(path, file2);
44
+ return result(path, file2);
45
+ },
46
+ async get(path) {
47
+ return files.get(path)?.bytes ?? null;
48
+ },
49
+ async delete(path) {
50
+ files.delete(path);
51
+ },
52
+ async list(prefix) {
53
+ const listed = [];
54
+ let totalSize = 0;
55
+ for (const [path, file2] of files) {
56
+ if (prefix && !path.startsWith(prefix)) continue;
57
+ listed.push({
58
+ url: `memory://${path}`,
59
+ pathname: path,
60
+ size: file2.bytes.length,
61
+ uploadedAt: file2.uploadedAt
62
+ });
63
+ totalSize += file2.bytes.length;
64
+ }
65
+ return { files: listed, totalSize };
66
+ },
67
+ async head(path) {
68
+ const file2 = files.get(path);
69
+ return file2 ? { size: file2.bytes.length, contentType: file2.contentType } : null;
70
+ },
71
+ async signedUrl(path) {
72
+ return files.has(path) ? `memory://${path}` : null;
73
+ },
74
+ /**
75
+ * Really fetches, because that is the only way to find out whether the
76
+ * URL a plugin builds is the URL it meant. The bytes land in the same
77
+ * in-memory store the rest of these read from.
78
+ */
79
+ async download(url, path, contentType) {
80
+ const res = await fetch(url);
81
+ if (!res.ok) throw new Error(`Downloading ${url} failed with status ${res.status}`);
82
+ const file2 = {
83
+ bytes: new Uint8Array(await res.arrayBuffer()),
84
+ contentType: contentType ?? res.headers.get("content-type") ?? "application/octet-stream",
85
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
86
+ };
87
+ files.set(path, file2);
88
+ return result(path, file2);
89
+ }
90
+ };
91
+ return { storage, files };
92
+ }
93
+ function createLocalState(seed = {}) {
94
+ const values = new Map(Object.entries(seed));
95
+ const state = {
96
+ async get(key) {
97
+ return values.has(key) ? values.get(key) : null;
98
+ },
99
+ async set(key, value) {
100
+ values.set(key, value);
101
+ },
102
+ async delete(key) {
103
+ values.delete(key);
104
+ },
105
+ async list(prefix) {
106
+ const keys = [...values.keys()].filter((k) => !prefix || k.startsWith(prefix));
107
+ return { keys };
108
+ }
109
+ };
110
+ return { state, values };
111
+ }
112
+ function absent(name, what) {
113
+ throw new Error(
114
+ `context.${name} is not available locally. ${what} Pass a stub to simulate({ ${name}: \u2026 }) to test the code around it, or run the step on a canvas to exercise the real thing.`
115
+ );
116
+ }
117
+ async function simulate(plugin, options = {}) {
118
+ const startTime = Date.now();
119
+ const execute = typeof plugin === "function" ? plugin : plugin?.execute;
120
+ const { storage, files } = createLocalStorage(options.storage);
121
+ const { state, values } = createLocalState(options.state);
122
+ if (typeof execute !== "function") {
123
+ return {
124
+ status: "error",
125
+ output: null,
126
+ error: "This module exports no execute function. A plugin is `export default { execute }`, `module.exports.execute = \u2026`, or a default export that is the function itself.",
127
+ durationMs: Date.now() - startTime,
128
+ files,
129
+ state: values
130
+ };
131
+ }
132
+ const context = {
133
+ workflowId: options.workflowId ?? "local-workflow",
134
+ nodeId: options.nodeId ?? "local-node",
135
+ userId: options.userId ?? "local-user",
136
+ executionId: options.executionId ?? `local-${Date.now()}`,
137
+ startedAt: new Date(startTime).toISOString(),
138
+ secrets: options.secrets ?? {},
139
+ storage,
140
+ state,
141
+ connections: options.connections ?? {
142
+ get: async () => absent("connections", "It reaches an account somebody connected on the platform.")
143
+ },
144
+ media: options.media ?? {
145
+ run: async () => absent("media", "It runs ffmpeg in a container of its own.")
146
+ }
147
+ };
148
+ const timeoutMs = options.timeoutMs ?? 3e4;
149
+ let timer;
150
+ const timeout = new Promise((_, reject) => {
151
+ timer = setTimeout(
152
+ () => reject(new Error(`Plugin execution timed out after ${timeoutMs}ms`)),
153
+ timeoutMs
154
+ );
155
+ });
156
+ try {
157
+ const raw = await Promise.race([
158
+ Promise.resolve(execute(options.input ?? {}, options.config ?? {}, context)),
159
+ timeout
160
+ ]);
161
+ const durationMs = Date.now() - startTime;
162
+ if (!raw || typeof raw !== "object") {
163
+ return {
164
+ status: "error",
165
+ output: null,
166
+ error: `A plugin must return an object like { output: "success", data: {} }. This one returned ${raw === void 0 ? "undefined" : JSON.stringify(raw)}.`,
167
+ durationMs,
168
+ files,
169
+ state: values
170
+ };
171
+ }
172
+ const output = raw;
173
+ if (typeof output.output !== "string") {
174
+ return {
175
+ status: "error",
176
+ output: null,
177
+ error: 'The returned object has no `output`. That field names which port the run leaves by \u2014 usually "success" or "error" \u2014 and it has to be one of the `outputs` in your manifest.',
178
+ durationMs,
179
+ files,
180
+ state: values
181
+ };
182
+ }
183
+ return {
184
+ status: "success",
185
+ output: { ...output, data: output.data ?? {} },
186
+ durationMs,
187
+ outputPort: output.output,
188
+ logs: output.metadata?.logs,
189
+ files,
190
+ state: values
191
+ };
192
+ } catch (error) {
193
+ const durationMs = Date.now() - startTime;
194
+ const message = error instanceof Error ? error.message : String(error);
195
+ return {
196
+ status: message.includes("timed out") ? "timeout" : "error",
197
+ output: null,
198
+ error: message,
199
+ durationMs,
200
+ files,
201
+ state: values
202
+ };
203
+ } finally {
204
+ if (timer) clearTimeout(timer);
205
+ }
206
+ }
207
+
208
+ // ../plugin-sdk/src/platform-rules.ts
209
+ var ALLOWED_PACKAGES = [
210
+ {
211
+ "name": "nodemailer",
212
+ "version": "^6.10.0"
213
+ },
214
+ {
215
+ "name": "lodash",
216
+ "version": "^4.17.21"
217
+ },
218
+ {
219
+ "name": "ramda",
220
+ "version": "^0.30.1"
221
+ },
222
+ {
223
+ "name": "underscore",
224
+ "version": "^1.13.8"
225
+ },
226
+ {
227
+ "name": "immer",
228
+ "version": "^11.1.18"
229
+ },
230
+ {
231
+ "name": "immutable",
232
+ "version": "^5.1.9"
233
+ },
234
+ {
235
+ "name": "deepmerge",
236
+ "version": "^4.3.1"
237
+ },
238
+ {
239
+ "name": "defu",
240
+ "version": "^6.1.7"
241
+ },
242
+ {
243
+ "name": "rfdc",
244
+ "version": "^1.4.1"
245
+ },
246
+ {
247
+ "name": "klona",
248
+ "version": "^2.0.6"
249
+ },
250
+ {
251
+ "name": "fast-copy",
252
+ "version": "^4.1.1"
253
+ },
254
+ {
255
+ "name": "fast-deep-equal",
256
+ "version": "^3.1.3"
257
+ },
258
+ {
259
+ "name": "dequal",
260
+ "version": "^2.0.3"
261
+ },
262
+ {
263
+ "name": "object-hash",
264
+ "version": "^3.0.0"
265
+ },
266
+ {
267
+ "name": "hash-sum",
268
+ "version": "^2.0.0"
269
+ },
270
+ {
271
+ "name": "flat",
272
+ "version": "^6.0.1"
273
+ },
274
+ {
275
+ "name": "dot-prop",
276
+ "version": "^10.2.0"
277
+ },
278
+ {
279
+ "name": "object-path",
280
+ "version": "^0.11.8"
281
+ },
282
+ {
283
+ "name": "sort-array",
284
+ "version": "^5.1.1"
285
+ },
286
+ {
287
+ "name": "natural-orderby",
288
+ "version": "^5.0.0"
289
+ },
290
+ {
291
+ "name": "deep-object-diff",
292
+ "version": "^1.1.9"
293
+ },
294
+ {
295
+ "name": "microdiff",
296
+ "version": "^1.6.0"
297
+ },
298
+ {
299
+ "name": "fast-json-patch",
300
+ "version": "^3.1.1"
301
+ },
302
+ {
303
+ "name": "rfc6902",
304
+ "version": "^5.3.0"
305
+ },
306
+ {
307
+ "name": "just-clone",
308
+ "version": "^6.2.0"
309
+ },
310
+ {
311
+ "name": "just-diff",
312
+ "version": "^6.0.2"
313
+ },
314
+ {
315
+ "name": "just-diff-apply",
316
+ "version": "^5.5.0"
317
+ },
318
+ {
319
+ "name": "just-omit",
320
+ "version": "^2.2.0"
321
+ },
322
+ {
323
+ "name": "just-pick",
324
+ "version": "^4.2.0"
325
+ },
326
+ {
327
+ "name": "just-group-by",
328
+ "version": "^2.2.0"
329
+ },
330
+ {
331
+ "name": "just-order-by",
332
+ "version": "^1.0.0"
333
+ },
334
+ {
335
+ "name": "just-unique",
336
+ "version": "^4.2.0"
337
+ },
338
+ {
339
+ "name": "just-flatten-it",
340
+ "version": "^5.2.0"
341
+ },
342
+ {
343
+ "name": "just-safe-get",
344
+ "version": "^4.2.0"
345
+ },
346
+ {
347
+ "name": "just-safe-set",
348
+ "version": "^4.2.1"
349
+ },
350
+ {
351
+ "name": "just-typeof",
352
+ "version": "^3.2.0"
353
+ },
354
+ {
355
+ "name": "just-range",
356
+ "version": "^4.2.0"
357
+ },
358
+ {
359
+ "name": "just-zip-it",
360
+ "version": "^3.2.0"
361
+ },
362
+ {
363
+ "name": "just-compare",
364
+ "version": "^2.3.0"
365
+ },
366
+ {
367
+ "name": "just-intersect",
368
+ "version": "^4.3.0"
369
+ },
370
+ {
371
+ "name": "just-split",
372
+ "version": "^3.2.0"
373
+ },
374
+ {
375
+ "name": "just-truncate",
376
+ "version": "^2.2.0"
377
+ },
378
+ {
379
+ "name": "semver",
380
+ "version": "^7.6.3"
381
+ },
382
+ {
383
+ "name": "compare-versions",
384
+ "version": "^6.1.1"
385
+ },
386
+ {
387
+ "name": "tiny-invariant",
388
+ "version": "^1.3.3"
389
+ },
390
+ {
391
+ "name": "json-stringify-safe",
392
+ "version": "^5.0.1"
393
+ },
394
+ {
395
+ "name": "fast-safe-stringify",
396
+ "version": "^2.1.1"
397
+ },
398
+ {
399
+ "name": "flatted",
400
+ "version": "^3.4.4"
401
+ },
402
+ {
403
+ "name": "superjson",
404
+ "version": "^2.2.6"
405
+ },
406
+ {
407
+ "name": "devalue",
408
+ "version": "^5.9.2"
409
+ },
410
+ {
411
+ "name": "lru-cache",
412
+ "version": "^11.5.2"
413
+ },
414
+ {
415
+ "name": "quick-lru",
416
+ "version": "^7.3.0"
417
+ },
418
+ {
419
+ "name": "memoize-one",
420
+ "version": "^6.0.0"
421
+ },
422
+ {
423
+ "name": "fast-memoize",
424
+ "version": "^2.5.2"
425
+ },
426
+ {
427
+ "name": "uuid",
428
+ "version": "^11.0.5"
429
+ },
430
+ {
431
+ "name": "nanoid",
432
+ "version": "^5.0.9"
433
+ },
434
+ {
435
+ "name": "ulid",
436
+ "version": "^3.0.2"
437
+ },
438
+ {
439
+ "name": "@paralleldrive/cuid2",
440
+ "version": "^3.3.0"
441
+ },
442
+ {
443
+ "name": "slugify",
444
+ "version": "^1.6.6"
445
+ },
446
+ {
447
+ "name": "slug",
448
+ "version": "^12.0.1"
449
+ },
450
+ {
451
+ "name": "sanitize-filename",
452
+ "version": "^1.6.4"
453
+ },
454
+ {
455
+ "name": "filenamify",
456
+ "version": "^7.0.3"
457
+ },
458
+ {
459
+ "name": "seedrandom",
460
+ "version": "^3.0.5"
461
+ },
462
+ {
463
+ "name": "random-js",
464
+ "version": "^2.1.0"
465
+ },
466
+ {
467
+ "name": "chance",
468
+ "version": "^1.1.13"
469
+ },
470
+ {
471
+ "name": "@faker-js/faker",
472
+ "version": "^10.6.0"
473
+ },
474
+ {
475
+ "name": "lorem-ipsum",
476
+ "version": "^3.0.0"
477
+ },
478
+ {
479
+ "name": "randomcolor",
480
+ "version": "^0.6.2"
481
+ },
482
+ {
483
+ "name": "date-fns",
484
+ "version": "^4.1.0"
485
+ },
486
+ {
487
+ "name": "date-fns-tz",
488
+ "version": "^3.2.0"
489
+ },
490
+ {
491
+ "name": "dayjs",
492
+ "version": "^1.11.13"
493
+ },
494
+ {
495
+ "name": "luxon",
496
+ "version": "^3.5.0"
497
+ },
498
+ {
499
+ "name": "moment",
500
+ "version": "^2.30.1"
501
+ },
502
+ {
503
+ "name": "moment-timezone",
504
+ "version": "^0.6.3"
505
+ },
506
+ {
507
+ "name": "ms",
508
+ "version": "^2.1.3"
509
+ },
510
+ {
511
+ "name": "pretty-ms",
512
+ "version": "^9.3.1"
513
+ },
514
+ {
515
+ "name": "parse-duration",
516
+ "version": "^2.1.8"
517
+ },
518
+ {
519
+ "name": "humanize-duration",
520
+ "version": "^3.34.1"
521
+ },
522
+ {
523
+ "name": "timeago.js",
524
+ "version": "^4.0.2"
525
+ },
526
+ {
527
+ "name": "chrono-node",
528
+ "version": "^2.7.7"
529
+ },
530
+ {
531
+ "name": "rrule",
532
+ "version": "^2.8.1"
533
+ },
534
+ {
535
+ "name": "cron-parser",
536
+ "version": "^5.10.0"
537
+ },
538
+ {
539
+ "name": "cronstrue",
540
+ "version": "^3.26.0"
541
+ },
542
+ {
543
+ "name": "date-and-time",
544
+ "version": "^4.5.2"
545
+ },
546
+ {
547
+ "name": "fecha",
548
+ "version": "^4.2.3"
549
+ },
550
+ {
551
+ "name": "dateformat",
552
+ "version": "^5.0.3"
553
+ },
554
+ {
555
+ "name": "iso8601-duration",
556
+ "version": "^2.1.4"
557
+ },
558
+ {
559
+ "name": "ics",
560
+ "version": "^3.12.0"
561
+ },
562
+ {
563
+ "name": "zod",
564
+ "version": "^3.24.1"
565
+ },
566
+ {
567
+ "name": "yup",
568
+ "version": "^1.6.1"
569
+ },
570
+ {
571
+ "name": "joi",
572
+ "version": "^18.2.8"
573
+ },
574
+ {
575
+ "name": "ajv",
576
+ "version": "^8.17.1"
577
+ },
578
+ {
579
+ "name": "ajv-formats",
580
+ "version": "^3.0.1"
581
+ },
582
+ {
583
+ "name": "ajv-errors",
584
+ "version": "^3.0.0"
585
+ },
586
+ {
587
+ "name": "superstruct",
588
+ "version": "^2.0.2"
589
+ },
590
+ {
591
+ "name": "valibot",
592
+ "version": "^1.5.0"
593
+ },
594
+ {
595
+ "name": "jsonschema",
596
+ "version": "^1.5.0"
597
+ },
598
+ {
599
+ "name": "@sinclair/typebox",
600
+ "version": "^0.34.52"
601
+ },
602
+ {
603
+ "name": "validator",
604
+ "version": "^13.12.0"
605
+ },
606
+ {
607
+ "name": "email-validator",
608
+ "version": "^2.0.4"
609
+ },
610
+ {
611
+ "name": "libphonenumber-js",
612
+ "version": "^1.11.17"
613
+ },
614
+ {
615
+ "name": "iban",
616
+ "version": "^0.0.14"
617
+ },
618
+ {
619
+ "name": "credit-card-type",
620
+ "version": "^10.3.0"
621
+ },
622
+ {
623
+ "name": "card-validator",
624
+ "version": "^10.0.4"
625
+ },
626
+ {
627
+ "name": "psl",
628
+ "version": "^1.15.0"
629
+ },
630
+ {
631
+ "name": "tldts",
632
+ "version": "^7.4.12"
633
+ },
634
+ {
635
+ "name": "normalize-url",
636
+ "version": "^9.0.1"
637
+ },
638
+ {
639
+ "name": "valid-url",
640
+ "version": "^1.0.9"
641
+ },
642
+ {
643
+ "name": "is-ip",
644
+ "version": "^5.0.1"
645
+ },
646
+ {
647
+ "name": "ipaddr.js",
648
+ "version": "^2.5.0"
649
+ },
650
+ {
651
+ "name": "ip-address",
652
+ "version": "^10.7.0"
653
+ },
654
+ {
655
+ "name": "netmask",
656
+ "version": "^2.1.1"
657
+ },
658
+ {
659
+ "name": "ua-parser-js",
660
+ "version": "^2.0.10"
661
+ },
662
+ {
663
+ "name": "bowser",
664
+ "version": "^2.14.1"
665
+ },
666
+ {
667
+ "name": "papaparse",
668
+ "version": "^5.4.1"
669
+ },
670
+ {
671
+ "name": "csv-parse",
672
+ "version": "^7.0.2"
673
+ },
674
+ {
675
+ "name": "csv-stringify",
676
+ "version": "^6.8.3"
677
+ },
678
+ {
679
+ "name": "json-2-csv",
680
+ "version": "^5.6.0"
681
+ },
682
+ {
683
+ "name": "js-yaml",
684
+ "version": "^4.1.0"
685
+ },
686
+ {
687
+ "name": "yaml",
688
+ "version": "^2.9.0"
689
+ },
690
+ {
691
+ "name": "json5",
692
+ "version": "^2.2.3"
693
+ },
694
+ {
695
+ "name": "jsonc-parser",
696
+ "version": "^3.3.1"
697
+ },
698
+ {
699
+ "name": "hjson",
700
+ "version": "^3.2.2"
701
+ },
702
+ {
703
+ "name": "toml",
704
+ "version": "^5.0.0"
705
+ },
706
+ {
707
+ "name": "@iarna/toml",
708
+ "version": "^2.2.5"
709
+ },
710
+ {
711
+ "name": "ini",
712
+ "version": "^7.0.0"
713
+ },
714
+ {
715
+ "name": "fast-xml-parser",
716
+ "version": "^4.5.1"
717
+ },
718
+ {
719
+ "name": "xml2js",
720
+ "version": "^0.6.2"
721
+ },
722
+ {
723
+ "name": "xml-js",
724
+ "version": "^1.6.11"
725
+ },
726
+ {
727
+ "name": "xmlbuilder2",
728
+ "version": "^4.0.3"
729
+ },
730
+ {
731
+ "name": "@xmldom/xmldom",
732
+ "version": "^0.9.12"
733
+ },
734
+ {
735
+ "name": "xpath",
736
+ "version": "^0.0.34"
737
+ },
738
+ {
739
+ "name": "cheerio",
740
+ "version": "^1.0.0"
741
+ },
742
+ {
743
+ "name": "htmlparser2",
744
+ "version": "^12.0.0"
745
+ },
746
+ {
747
+ "name": "node-html-parser",
748
+ "version": "^9.0.4"
749
+ },
750
+ {
751
+ "name": "parse5",
752
+ "version": "^8.0.1"
753
+ },
754
+ {
755
+ "name": "sanitize-html",
756
+ "version": "^2.17.7"
757
+ },
758
+ {
759
+ "name": "marked",
760
+ "version": "^15.0.6"
761
+ },
762
+ {
763
+ "name": "markdown-it",
764
+ "version": "^15.0.1"
765
+ },
766
+ {
767
+ "name": "showdown",
768
+ "version": "^2.1.0"
769
+ },
770
+ {
771
+ "name": "turndown",
772
+ "version": "^7.2.0"
773
+ },
774
+ {
775
+ "name": "html-to-text",
776
+ "version": "^9.0.5"
777
+ },
778
+ {
779
+ "name": "he",
780
+ "version": "^1.2.0"
781
+ },
782
+ {
783
+ "name": "entities",
784
+ "version": "^8.1.0"
785
+ },
786
+ {
787
+ "name": "striptags",
788
+ "version": "^3.2.0"
789
+ },
790
+ {
791
+ "name": "front-matter",
792
+ "version": "^4.0.2"
793
+ },
794
+ {
795
+ "name": "qs",
796
+ "version": "^6.13.1"
797
+ },
798
+ {
799
+ "name": "query-string",
800
+ "version": "^9.5.1"
801
+ },
802
+ {
803
+ "name": "url-parse",
804
+ "version": "^1.5.10"
805
+ },
806
+ {
807
+ "name": "mime-types",
808
+ "version": "^2.1.35"
809
+ },
810
+ {
811
+ "name": "mime",
812
+ "version": "^4.1.0"
813
+ },
814
+ {
815
+ "name": "content-type",
816
+ "version": "^3.0.0"
817
+ },
818
+ {
819
+ "name": "cookie",
820
+ "version": "^2.0.1"
821
+ },
822
+ {
823
+ "name": "set-cookie-parser",
824
+ "version": "^3.1.2"
825
+ },
826
+ {
827
+ "name": "jsonpath-plus",
828
+ "version": "^10.2.0"
829
+ },
830
+ {
831
+ "name": "jmespath",
832
+ "version": "^0.16.0"
833
+ },
834
+ {
835
+ "name": "jsonata",
836
+ "version": "^2.2.2"
837
+ },
838
+ {
839
+ "name": "exceljs",
840
+ "version": "^4.4.0"
841
+ },
842
+ {
843
+ "name": "change-case",
844
+ "version": "^5.4.4"
845
+ },
846
+ {
847
+ "name": "camelcase",
848
+ "version": "^9.0.0"
849
+ },
850
+ {
851
+ "name": "decamelize",
852
+ "version": "^6.0.1"
853
+ },
854
+ {
855
+ "name": "title-case",
856
+ "version": "^4.3.2"
857
+ },
858
+ {
859
+ "name": "pluralize",
860
+ "version": "^8.0.0"
861
+ },
862
+ {
863
+ "name": "inflection",
864
+ "version": "^3.0.2"
865
+ },
866
+ {
867
+ "name": "voca",
868
+ "version": "^1.4.1"
869
+ },
870
+ {
871
+ "name": "underscore.string",
872
+ "version": "^3.3.6"
873
+ },
874
+ {
875
+ "name": "string-similarity",
876
+ "version": "^4.0.4"
877
+ },
878
+ {
879
+ "name": "fastest-levenshtein",
880
+ "version": "^1.0.16"
881
+ },
882
+ {
883
+ "name": "leven",
884
+ "version": "^4.1.0"
885
+ },
886
+ {
887
+ "name": "diff",
888
+ "version": "^7.0.0"
889
+ },
890
+ {
891
+ "name": "diff-match-patch",
892
+ "version": "^1.0.5"
893
+ },
894
+ {
895
+ "name": "remove-accents",
896
+ "version": "^0.5.0"
897
+ },
898
+ {
899
+ "name": "transliteration",
900
+ "version": "^2.6.1"
901
+ },
902
+ {
903
+ "name": "latinize",
904
+ "version": "^2.0.0"
905
+ },
906
+ {
907
+ "name": "franc",
908
+ "version": "^6.2.0"
909
+ },
910
+ {
911
+ "name": "stopword",
912
+ "version": "^3.1.5"
913
+ },
914
+ {
915
+ "name": "emoji-regex",
916
+ "version": "^10.6.0"
917
+ },
918
+ {
919
+ "name": "node-emoji",
920
+ "version": "^2.2.0"
921
+ },
922
+ {
923
+ "name": "string-width",
924
+ "version": "^8.2.2"
925
+ },
926
+ {
927
+ "name": "word-wrap",
928
+ "version": "^1.2.5"
929
+ },
930
+ {
931
+ "name": "wrap-ansi",
932
+ "version": "^10.0.1"
933
+ },
934
+ {
935
+ "name": "linkify-it",
936
+ "version": "^6.1.0"
937
+ },
938
+ {
939
+ "name": "linkifyjs",
940
+ "version": "^4.3.3"
941
+ },
942
+ {
943
+ "name": "fuse.js",
944
+ "version": "^7.5.0"
945
+ },
946
+ {
947
+ "name": "minisearch",
948
+ "version": "^7.2.0"
949
+ },
950
+ {
951
+ "name": "lunr",
952
+ "version": "^2.3.9"
953
+ },
954
+ {
955
+ "name": "flexsearch",
956
+ "version": "^0.8.212"
957
+ },
958
+ {
959
+ "name": "mustache",
960
+ "version": "^4.2.0"
961
+ },
962
+ {
963
+ "name": "liquidjs",
964
+ "version": "^10.20.1"
965
+ },
966
+ {
967
+ "name": "intl-messageformat",
968
+ "version": "^11.2.14"
969
+ },
970
+ {
971
+ "name": "gpt-tokenizer",
972
+ "version": "^4.0.0"
973
+ },
974
+ {
975
+ "name": "js-tiktoken",
976
+ "version": "^1.0.21"
977
+ },
978
+ {
979
+ "name": "decimal.js",
980
+ "version": "^10.4.3"
981
+ },
982
+ {
983
+ "name": "big.js",
984
+ "version": "^6.2.2"
985
+ },
986
+ {
987
+ "name": "bignumber.js",
988
+ "version": "^11.1.5"
989
+ },
990
+ {
991
+ "name": "currency.js",
992
+ "version": "^2.0.4"
993
+ },
994
+ {
995
+ "name": "dinero.js",
996
+ "version": "^2.0.2"
997
+ },
998
+ {
999
+ "name": "numeral",
1000
+ "version": "^2.0.6"
1001
+ },
1002
+ {
1003
+ "name": "accounting",
1004
+ "version": "^0.4.1"
1005
+ },
1006
+ {
1007
+ "name": "fraction.js",
1008
+ "version": "^5.3.4"
1009
+ },
1010
+ {
1011
+ "name": "convert-units",
1012
+ "version": "^2.3.4"
1013
+ },
1014
+ {
1015
+ "name": "js-quantities",
1016
+ "version": "^1.8.0"
1017
+ },
1018
+ {
1019
+ "name": "mathjs",
1020
+ "version": "^15.2.0"
1021
+ },
1022
+ {
1023
+ "name": "simple-statistics",
1024
+ "version": "^7.12.0"
1025
+ },
1026
+ {
1027
+ "name": "ml-matrix",
1028
+ "version": "^6.15.0"
1029
+ },
1030
+ {
1031
+ "name": "d3-array",
1032
+ "version": "^3.2.4"
1033
+ },
1034
+ {
1035
+ "name": "d3-format",
1036
+ "version": "^3.1.2"
1037
+ },
1038
+ {
1039
+ "name": "d3-time-format",
1040
+ "version": "^4.1.0"
1041
+ },
1042
+ {
1043
+ "name": "geolib",
1044
+ "version": "^3.3.14"
1045
+ },
1046
+ {
1047
+ "name": "haversine-distance",
1048
+ "version": "^1.2.4"
1049
+ },
1050
+ {
1051
+ "name": "filesize",
1052
+ "version": "^11.0.23"
1053
+ },
1054
+ {
1055
+ "name": "bytes",
1056
+ "version": "^3.1.2"
1057
+ },
1058
+ {
1059
+ "name": "human-format",
1060
+ "version": "^1.2.1"
1061
+ },
1062
+ {
1063
+ "name": "jsonwebtoken",
1064
+ "version": "^9.0.2"
1065
+ },
1066
+ {
1067
+ "name": "jose",
1068
+ "version": "^5.9.6"
1069
+ },
1070
+ {
1071
+ "name": "jws",
1072
+ "version": "^4.0.1"
1073
+ },
1074
+ {
1075
+ "name": "jwt-decode",
1076
+ "version": "^4.0.0"
1077
+ },
1078
+ {
1079
+ "name": "crypto-js",
1080
+ "version": "^4.2.0"
1081
+ },
1082
+ {
1083
+ "name": "bcryptjs",
1084
+ "version": "^2.4.3"
1085
+ },
1086
+ {
1087
+ "name": "otplib",
1088
+ "version": "^13.5.0"
1089
+ },
1090
+ {
1091
+ "name": "otpauth",
1092
+ "version": "^9.5.2"
1093
+ },
1094
+ {
1095
+ "name": "tweetnacl",
1096
+ "version": "^1.0.3"
1097
+ },
1098
+ {
1099
+ "name": "tweetnacl-util",
1100
+ "version": "^0.15.1"
1101
+ },
1102
+ {
1103
+ "name": "node-forge",
1104
+ "version": "^1.4.0"
1105
+ },
1106
+ {
1107
+ "name": "elliptic",
1108
+ "version": "^6.6.1"
1109
+ },
1110
+ {
1111
+ "name": "bn.js",
1112
+ "version": "^5.2.5"
1113
+ },
1114
+ {
1115
+ "name": "js-base64",
1116
+ "version": "^3.7.7"
1117
+ },
1118
+ {
1119
+ "name": "base64-js",
1120
+ "version": "^1.5.1"
1121
+ },
1122
+ {
1123
+ "name": "base-64",
1124
+ "version": "^1.0.0"
1125
+ },
1126
+ {
1127
+ "name": "hi-base32",
1128
+ "version": "^0.5.1"
1129
+ },
1130
+ {
1131
+ "name": "thirty-two",
1132
+ "version": "^1.0.2"
1133
+ },
1134
+ {
1135
+ "name": "js-sha256",
1136
+ "version": "^1.0.0"
1137
+ },
1138
+ {
1139
+ "name": "js-sha512",
1140
+ "version": "^0.9.0"
1141
+ },
1142
+ {
1143
+ "name": "js-sha1",
1144
+ "version": "^0.7.0"
1145
+ },
1146
+ {
1147
+ "name": "js-md5",
1148
+ "version": "^0.9.2"
1149
+ },
1150
+ {
1151
+ "name": "hash.js",
1152
+ "version": "^1.1.7"
1153
+ },
1154
+ {
1155
+ "name": "spark-md5",
1156
+ "version": "^3.0.2"
1157
+ },
1158
+ {
1159
+ "name": "blueimp-md5",
1160
+ "version": "^2.19.0"
1161
+ },
1162
+ {
1163
+ "name": "crc-32",
1164
+ "version": "^1.2.2"
1165
+ },
1166
+ {
1167
+ "name": "crc",
1168
+ "version": "^4.3.2"
1169
+ },
1170
+ {
1171
+ "name": "utf8",
1172
+ "version": "^3.0.0"
1173
+ },
1174
+ {
1175
+ "name": "iconv-lite",
1176
+ "version": "^0.7.3"
1177
+ },
1178
+ {
1179
+ "name": "punycode",
1180
+ "version": "^2.3.1"
1181
+ },
1182
+ {
1183
+ "name": "pako",
1184
+ "version": "^3.0.1"
1185
+ },
1186
+ {
1187
+ "name": "fflate",
1188
+ "version": "^0.8.3"
1189
+ },
1190
+ {
1191
+ "name": "lz-string",
1192
+ "version": "^1.5.0"
1193
+ },
1194
+ {
1195
+ "name": "color",
1196
+ "version": "^5.0.3"
1197
+ },
1198
+ {
1199
+ "name": "tinycolor2",
1200
+ "version": "^1.6.0"
1201
+ },
1202
+ {
1203
+ "name": "chroma-js",
1204
+ "version": "^3.2.0"
1205
+ },
1206
+ {
1207
+ "name": "color-convert",
1208
+ "version": "^3.1.3"
1209
+ },
1210
+ {
1211
+ "name": "colord",
1212
+ "version": "^2.10.0"
1213
+ },
1214
+ {
1215
+ "name": "i18n-iso-countries",
1216
+ "version": "^7.14.0"
1217
+ },
1218
+ {
1219
+ "name": "iso-3166-1",
1220
+ "version": "^2.1.1"
1221
+ },
1222
+ {
1223
+ "name": "iso-639-1",
1224
+ "version": "^3.1.6"
1225
+ },
1226
+ {
1227
+ "name": "currency-codes",
1228
+ "version": "^2.2.0"
1229
+ },
1230
+ {
1231
+ "name": "qrcode-generator",
1232
+ "version": "^2.0.4"
1233
+ },
1234
+ {
1235
+ "name": "p-limit",
1236
+ "version": "^6.2.0"
1237
+ },
1238
+ {
1239
+ "name": "p-retry",
1240
+ "version": "^6.2.1"
1241
+ },
1242
+ {
1243
+ "name": "p-map",
1244
+ "version": "^7.0.3"
1245
+ },
1246
+ {
1247
+ "name": "p-queue",
1248
+ "version": "^9.3.3"
1249
+ },
1250
+ {
1251
+ "name": "p-timeout",
1252
+ "version": "^7.0.1"
1253
+ },
1254
+ {
1255
+ "name": "p-all",
1256
+ "version": "^5.0.1"
1257
+ },
1258
+ {
1259
+ "name": "p-props",
1260
+ "version": "^6.1.0"
1261
+ },
1262
+ {
1263
+ "name": "p-settle",
1264
+ "version": "^5.2.1"
1265
+ },
1266
+ {
1267
+ "name": "p-throttle",
1268
+ "version": "^8.1.0"
1269
+ },
1270
+ {
1271
+ "name": "p-debounce",
1272
+ "version": "^5.1.0"
1273
+ },
1274
+ {
1275
+ "name": "async",
1276
+ "version": "^3.2.6"
1277
+ },
1278
+ {
1279
+ "name": "bluebird",
1280
+ "version": "^3.7.2"
1281
+ },
1282
+ {
1283
+ "name": "promise-retry",
1284
+ "version": "^2.0.1"
1285
+ },
1286
+ {
1287
+ "name": "retry",
1288
+ "version": "^0.13.1"
1289
+ },
1290
+ {
1291
+ "name": "exponential-backoff",
1292
+ "version": "^3.1.3"
1293
+ },
1294
+ {
1295
+ "name": "bottleneck",
1296
+ "version": "^2.19.5"
1297
+ },
1298
+ {
1299
+ "name": "throttle-debounce",
1300
+ "version": "^5.0.2"
1301
+ },
1302
+ {
1303
+ "name": "delay",
1304
+ "version": "^7.0.0"
1305
+ },
1306
+ {
1307
+ "name": "graphlib",
1308
+ "version": "^2.1.8"
1309
+ }
1310
+ ];
1311
+ var RESTRICTED_PATTERNS = [
1312
+ /\bprocess\s*\.\s*exit/,
1313
+ /\bprocess\s*\.\s*env/,
1314
+ /\bprocess\s*\.\s*binding/,
1315
+ /\bprocess\s*\.\s*mainModule/,
1316
+ /\bprocess\s*\[/,
1317
+ /\bprocess\s*\(/,
1318
+ /\beval\s*\(/,
1319
+ /\bFunction\s*\(/,
1320
+ /\bnew\s+Function\b/,
1321
+ /\bAsyncFunction\b/,
1322
+ /\bGeneratorFunction\b/,
1323
+ /\bglobalThis\b/,
1324
+ /\bself\s*\[/,
1325
+ /\bimport\s+/,
1326
+ /\bimport\s*\(/,
1327
+ /\bReflect\b/,
1328
+ /\bProxy\b/,
1329
+ /\b__proto__\b/,
1330
+ /\bprototype\s*\[/,
1331
+ /\bprototype\s*\.\s*constructor\b/,
1332
+ /\bconstructor\b/,
1333
+ /\bthis\s*\.\s*constructor\b/,
1334
+ /\barguments\s*\.\s*callee\b/,
1335
+ /\bObject\s*\.\s*defineProperty\b/,
1336
+ /\b__defineGetter__\b/,
1337
+ /\b__defineSetter__\b/,
1338
+ /\b__lookupGetter__\b/,
1339
+ /\b__lookupSetter__\b/,
1340
+ /\bmodule\s*\.\s*(?:constructor|extensions|_compile|_path|_sourceMaps)\b/,
1341
+ /\brequire\s*\.\s*cache\b/,
1342
+ /\brequire\s*\.\s*resolve\b/,
1343
+ /\bError\s*\.\s*captureStackTrace\b/,
1344
+ /\bWebAssembly\b/
1345
+ ];
1346
+
1347
+ // ../plugin-sdk/src/validate.ts
1348
+ var ALLOWED_PACKAGE_NAMES = new Set(ALLOWED_PACKAGES.map((p) => p.name));
1349
+ function validatePluginCode(code) {
1350
+ const hasExecute = code.includes("module.exports") || code.includes("exports.execute") || code.includes("export");
1351
+ if (!hasExecute) {
1352
+ return {
1353
+ valid: false,
1354
+ error: "Plugin code must export an execute function. Example: module.exports = { execute: async (input, config, context) => { ... } }"
1355
+ };
1356
+ }
1357
+ for (const pattern of RESTRICTED_PATTERNS) {
1358
+ if (pattern.test(code)) {
1359
+ return {
1360
+ valid: false,
1361
+ error: `Plugin code contains restricted patterns. Plugins cannot access the filesystem, spawn processes, use eval, import modules, or escape the sandbox. (matched ${pattern})`
1362
+ };
1363
+ }
1364
+ }
1365
+ const packages = [];
1366
+ const requireCall = /\brequire\s*\(\s*(['"])([^'"]+)\1\s*\)/g;
1367
+ let match;
1368
+ while ((match = requireCall.exec(code)) !== null) {
1369
+ const name = match[2];
1370
+ if (!ALLOWED_PACKAGE_NAMES.has(name)) {
1371
+ return {
1372
+ valid: false,
1373
+ error: `Plugin code requires "${name}" which is not an allowed package. See ALLOWED_PACKAGES for the list.`
1374
+ };
1375
+ }
1376
+ if (!packages.includes(name)) packages.push(name);
1377
+ }
1378
+ if (/\brequire\s*\(\s*(?!['"][^'"]+['"]\s*\))/.test(code)) {
1379
+ return {
1380
+ valid: false,
1381
+ error: "Plugin code contains a dynamic require() call. Only require() with string literals of allowed packages is permitted."
1382
+ };
1383
+ }
1384
+ return { valid: true, packages };
1385
+ }
1386
+ function sandboxRequire(load) {
1387
+ return (name) => {
1388
+ const allowed = ALLOWED_PACKAGES.find((p) => p.name === name);
1389
+ if (!allowed) throw new Error(`Cannot require "${name}"`);
1390
+ try {
1391
+ return load(name);
1392
+ } catch {
1393
+ throw new Error(
1394
+ `"${name}" is available in the sandbox but not installed here. To run this locally: npm install --save-dev ${name}@${allowed.version}`
1395
+ );
1396
+ }
1397
+ };
1398
+ }
1399
+ function loadPluginCode(code, require2) {
1400
+ const module = { exports: {} };
1401
+ const body = new Function("module", "exports", "require", `"use strict"; ${code}`);
1402
+ body(module, module.exports, require2);
1403
+ return module.exports;
1404
+ }
1405
+
1406
+ // src/declarations.ts
1407
+ var TYPE_DECLARATIONS = '/**\n * What a plugin is handed, and what it must hand back.\n *\n * This file is the contract between a plugin and the VisualAutomate runtime:\n * the arguments `execute` is called with, every helper on `context`, the shape\n * of the result, and the manifest that declares the step. When a helper is only\n * present under some condition, that is said here rather than left for somebody\n * to discover from a TypeError.\n *\n * context-api.ts describes the same runtime as data, for tools that cannot read\n * a TypeScript interface, such as an editor\'s autocomplete.\n */\n\n// \u2500\u2500\u2500 What a step is called with \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * The output of the steps that led here, keyed by step name and node id.\n *\n * A step reads `input.download_clips.file` where `download_clips` is what the\n * step before it is called on the canvas. Every route into this step is merged,\n * so a step at the join of two branches sees both.\n */\nexport interface PluginInput {\n [key: string]: unknown\n}\n\n/** The fields somebody filled in on the canvas, as the manifest declared them. */\nexport interface PluginConfig {\n [key: string]: unknown\n}\n\n// \u2500\u2500\u2500 context.storage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** What `storage.put` reports about the file it wrote. */\nexport interface StorageResult {\n url: string\n size: number\n contentType: string\n uploadedAt: string\n expiresAt: string\n}\n\n/** What `storage.list` reports. */\nexport interface StorageListResult {\n files: Array<{\n url: string\n pathname: string\n size: number\n uploadedAt: string\n }>\n totalSize: number\n}\n\n/**\n * The account\'s own files. Paths are scoped to the user; you cannot name\n * anybody else\'s, and you do not have to prefix your own.\n *\n * The account\'s plan caps what it may hold at once, and a `put` past that\n * ceiling is refused rather than silently dropped.\n */\nexport interface PluginStorage {\n /** Write a file. Bytes may be a string, a Buffer or any typed array. */\n put(path: string, data: Uint8Array | string, contentType?: string): Promise<StorageResult>\n /** Read a file back. `null` when it is not there. */\n get(path: string): Promise<Uint8Array | null>\n delete(path: string): Promise<void>\n /** Everything under a prefix, or everything when no prefix is given. */\n list(prefix?: string): Promise<StorageListResult>\n /** Size and content type without moving the bytes. `null` when absent. */\n head(path: string): Promise<{ size: number; contentType: string } | null>\n /**\n * A URL somebody else can fetch this file from, for a few minutes.\n *\n * For the common API that takes a link and pulls the file itself. Clamped\n * to between one minute and one hour; the default is fifteen minutes.\n */\n signedUrl(path: string, expiresInSeconds?: number): Promise<string | null>\n /**\n * Pull a URL straight into storage without the bytes passing through this\n * step. A hundred-megabyte file costs the run no memory and no egress,\n * because a Worker fetches it and writes it to storage directly.\n *\n * ONLY PRESENT when the download Worker is configured on the server. Guard\n * with `if (context.storage.download)` if you want to degrade gracefully.\n */\n download?(url: string, path: string, contentType?: string): Promise<StorageResult>\n}\n\n// \u2500\u2500\u2500 context.state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Values that outlive one run, scoped to this workflow and this account.\n *\n * For a cursor, a last-seen id, a counter \u2014 the things a workflow needs to\n * remember between firings. Not for large data: that is what storage is for.\n * The account\'s plan caps how many keys it may hold.\n */\nexport interface PluginState {\n /** The stored value, or `null` when the key has never been set. */\n get(key: string): Promise<unknown>\n set(key: string, value: unknown, options?: { ttlSeconds?: number }): Promise<void>\n delete(key: string): Promise<void>\n list(prefix?: string): Promise<{ keys: string[] }>\n}\n\n// \u2500\u2500\u2500 context.media \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * ffmpeg, for a step that has a file in storage and wants a different one.\n *\n * A PLUGIN NEVER BUILDS AN FFMPEG COMMAND LINE, and cannot. You name an\n * operation and supply values; the command line is assembled on the server, one\n * operation at a time, and the only paths in it are placeholders the container\n * fills from a list the server wrote. That is deliberate: `-i` takes any path\n * and several filters open files of their own, so an ffmpeg taking its flags\n * from plugin config would be a plugin reading the container\'s filesystem \u2014\n * including the file holding that run\'s own credentials.\n *\n * The job runs in a container of its own and talks to storage directly, so a\n * two hundred megabyte clip never crosses this step.\n *\n * ONLY PRESENT when the sandbox and storage are both configured.\n */\nexport interface PluginMedia {\n run(operation: string, params: Record<string, unknown>): Promise<Record<string, unknown>>\n}\n\n// \u2500\u2500\u2500 context.connections \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** One request through the integration proxy. */\nexport interface ConnectionRequest {\n path: string\n method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"\n query?: Record<string, string | number | boolean | undefined>\n headers?: Record<string, string>\n body?: unknown\n}\n\n/** What the proxy answers with. The provider\'s own reply, unwrapped. */\nexport interface ConnectionResponse {\n ok: boolean\n status: number\n headers?: Record<string, string>\n body: unknown\n}\n\n/**\n * One of the accounts the user connected.\n *\n * THERE IS NO getToken(), deliberately. A token that can be read can be logged\n * or posted somewhere, and this is where third-party code runs. You name a path\n * and the proxy attaches the credential on the way past.\n *\n * You also never name a host. The base URL belongs to the provider\n * configuration, which is what stops a connection being pointed at an internal\n * address or at somebody\'s collection server.\n */\nexport interface PluginConnection {\n /** This platform\'s id for the connection, not the provider\'s. */\n id: string\n request(options: ConnectionRequest): Promise<ConnectionResponse>\n}\n\n/**\n * The accounts this step was configured with.\n *\n * `get` takes the name of one of your own `connection` properties \u2014 not a\n * connection id \u2014 so a step can only reach the accounts it was set up with.\n *\n * ONLY PRESENT when the step declares at least one `connection` property and\n * the user has chosen an account for it.\n */\nexport interface PluginConnections {\n get(propertyName: string): Promise<PluginConnection>\n}\n\n// \u2500\u2500\u2500 The context itself \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Everything a step is told about the run it is part of.\n *\n * The optional members are optional for a reason and not out of caution: each\n * is absent when the thing behind it is not configured or not applicable, and\n * a plugin that needs one should say so plainly rather than crash on a dot.\n */\nexport interface PluginContext {\n /** The workflow this run belongs to. */\n workflowId: string\n /** This step\'s id on the canvas. */\n nodeId: string\n /** Whose run it is. */\n userId: string\n /** This run, distinct from every other run of the same workflow. */\n executionId: string\n /** When the run started, as an ISO string. */\n startedAt: string\n /** Values the plugin was published with. Not the user\'s credentials. */\n secrets: Record<string, string>\n\n /** The account\'s files. See PluginStorage. */\n storage: PluginStorage\n /** Values that outlive one run. See PluginState. */\n state: PluginState\n /** ffmpeg. Absent when the server has no sandbox or storage configured. */\n media?: PluginMedia\n /** The accounts this step was configured with. Absent when it has none. */\n connections?: PluginConnections\n}\n\n// \u2500\u2500\u2500 What a step hands back \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * The result of a step.\n *\n * `output` names which port the run leaves by, and therefore which lines on the\n * canvas are followed. It has to be one of the `outputs` in the manifest \u2014\n * usually `"success"` or `"error"`.\n *\n * `data` is what the next steps read as `{{this_step.field}}`. Keep it flat and\n * named for what it is; those names are what somebody picks from a dropdown.\n */\nexport interface PluginOutput {\n output: string\n data: Record<string, unknown>\n metadata?: {\n durationMs?: number\n memoryUsedMb?: number\n logs?: string[]\n }\n}\n\n/**\n * A plugin.\n *\n * Export `execute`, either as `module.exports.execute` or as the default export\n * of a module with an `execute` on it. Both are accepted, and so is a bare\n * function as the default export.\n *\n * @example\n * export default {\n * async execute(input, config, context) {\n * const file = await context.storage.get(config.path as string)\n * return { output: "success", data: { bytes: file?.length ?? 0 } }\n * }\n * }\n */\nexport interface PluginModule {\n execute(\n input: PluginInput,\n config: PluginConfig,\n context: PluginContext,\n ): Promise<PluginOutput> | PluginOutput\n}\n\n/** The bare function form, for a plugin that is only an execute. */\nexport type PluginExecute = PluginModule["execute"]\n\n// \u2500\u2500\u2500 How a run reports on a step \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * `skipped` is a decision rather than a fault: the filters on every line into\n * the step said no, so it never ran. Counting it as an error turns a branch\n * that was simply not chosen into a broken workflow.\n */\nexport type ExecutionStatus = "success" | "error" | "timeout" | "skipped" | "cancelled"\n\nexport interface PluginExecutionResult {\n status: ExecutionStatus\n output: PluginOutput | null\n error?: string\n durationMs: number\n memoryMb?: number\n outputPort?: string\n logs?: string[]\n}\n\n// \u2500\u2500\u2500 The manifest \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * The declaration that sits beside the code.\n *\n * It says which fields the canvas renders, which account the step acts as, what\n * it hands downstream, and whether it is a starting point that watches\n * something. One shape wherever it is stored \u2014 `manifest.json` next to your\n * code, or an `@schema` comment in the source.\n *\n * Every name below is one the platform keeps. A block it does not recognise is\n * dropped when the plugin is saved, so a field that is not listed here will not\n * survive an upload.\n */\nexport type PluginPropertyType =\n | "string"\n | "number"\n | "boolean"\n | "select"\n | "multiselect"\n | "json"\n /**\n * A program, not a value.\n *\n * Stored exactly as typed and never parsed, which is the difference from\n * `json` above: that one keeps its value structured, so `return 5` would be\n * saved as the number five and the step would run nothing.\n */\n | "code"\n | "color"\n | "date"\n /** An account the user connected. Rendered as a picker, not a text box. */\n | "connection"\n /**\n * A file the account already has, read into the config before the step\n * runs. `{{config.attachment.name}}`, `.contentType`, `.size`,\n * `.contentBytes`. Right for attaching a PDF; a video would be hundreds of\n * megabytes of base64 \u2014 use `path` for those.\n */\n | "file"\n /** A file in storage, named rather than read. What ffmpeg steps take. */\n | "path"\n /** Several of them, one per line, in the order they were chosen. */\n | "paths"\n\nexport interface PluginProperty {\n /** Shown above the input on the canvas. Defaults to the property name. */\n label: string\n type: PluginPropertyType\n required?: boolean\n default?: unknown\n /** Helper text under the input. */\n description?: string\n /** For `select` and `multiselect`. */\n options?: Array<{ label: string; value: string }>\n /** Puts related fields in a section together. */\n group?: string\n /** For `number`. */\n min?: number\n max?: number\n /** For `string`: a regex the value must match. */\n pattern?: string\n /**\n * For `connection`: which integration the account must belong to, e.g.\n * `outlook`. A module inherits its app\'s, so it does not repeat this.\n */\n provider?: string\n}\n\n/**\n * How a step reaches the account it acts as.\n *\n * `none` means the plugin holds its own key in a property. That is not the same\n * as leaving the block out, which means "written before auth could be\n * declared".\n *\n * A MANIFEST MAY NOT NAME A HOST. `url`, `baseUrl`, `base_url` and `host` are\n * refused outright rather than ignored, so nobody writes one and believes it\n * took effect: the base URL belongs to the provider configuration, and that is\n * what stops a connection being pointed at an internal address.\n */\nexport interface PluginAuth {\n type: "oauth2" | "api_key" | "basic" | "none"\n provider?: string\n scopes?: string[]\n}\n\n/** One request the platform makes on your behalf. Relative paths only. */\nexport interface TriggerRequest {\n method?: "GET" | "POST"\n /** Relative to the provider\'s base URL. The proxy refuses anything else. */\n path: string\n query?: Record<string, string>\n headers?: Record<string, string>\n body?: unknown\n}\n\n/** Where the next poll should carry on from. */\nexport interface TriggerCursor {\n from: "response" | "item"\n /** Dotted path to the value. `@odata.deltaLink` is one key, not two. */\n path: string\n into?: string\n}\n\n/**\n * A second request per item, for providers that answer with identifiers.\n *\n * Gmail\'s message list returns `{ id, threadId }` \u2014 enough to know a mail\n * arrived, not enough to tell anyone what it says. Without this the trigger\n * polls correctly, dedupes correctly, and hands every workflow an empty\n * message, which is the worst kind of working.\n */\nexport interface TriggerHydrate {\n path: string\n method?: "GET" | "POST"\n query?: Record<string, string>\n headers?: Record<string, string>\n}\n\n/** For providers that can tell us instead of being asked. */\nexport interface TriggerPush {\n resource: string\n changeType: string\n maxMinutes?: number\n}\n\n/**\n * What makes a plugin a starting point.\n *\n * A declaration rather than code, because the runtime is billed by the second\n * and a step blocking on an empty inbox would be paid for every second of doing\n * nothing. The platform polls for you and starts the workflow when something\n * arrives.\n */\nexport interface PluginTrigger {\n /** Must name one of your own properties of type `connection`. */\n connection: string\n /** Minutes between checks. The account\'s plan sets the floor. */\n poll?: string | number\n request: TriggerRequest\n /** Dotted path to the array of items in the response. */\n items: string\n /** Dotted path, within one item, to what makes it unique. */\n id: string\n cursor?: TriggerCursor\n hydrate?: TriggerHydrate\n push?: TriggerPush\n /** How many items one poll may start workflows for. */\n maxPerPoll?: number\n}\n\n/**\n * A step that is a declaration rather than a program.\n *\n * One request built from the properties somebody filled in, or no request at\n * all for a step that only reshapes what reached it. No container, no code, and\n * nothing for a review to be about \u2014 which is why most steps should be one.\n *\n * Left loosely typed here on purpose: writing an action is a different job from\n * writing a plugin, and the platform validates its shape when it is saved.\n */\nexport type PluginAction = Record<string, unknown>\n\n/**\n * Every block the validator keeps. Anything else is dropped on save.\n *\n * Note what is NOT here: the hosts a plugin may contact are set on the platform\n * rather than declared in the manifest, so a plugin cannot widen its own egress.\n */\nexport interface PluginManifest {\n description?: string\n icon?: string\n color?: string\n /** Bumped by you when the declaration changes shape. */\n version?: number\n /** How long one run may take. Capped by the account\'s plan. */\n timeoutMs?: number\n memoryLimitMb?: number\n auth?: PluginAuth\n properties?: Record<string, PluginProperty>\n inputs?: string[]\n outputs?: string[]\n /** What downstream steps can read, described for the picker. */\n outputFields?: Record<string, string>\n trigger?: PluginTrigger\n action?: PluginAction\n}\n\n/** The older name for the same thing, kept because authors still say it. */\nexport type PluginSchema = PluginManifest\n';
1408
+
1409
+ // src/templates.ts
1410
+ var EXAMPLES = `
1411
+ // -- The rest of what you can declare --------------------------------
1412
+ //
1413
+ // All commented out. Uncomment what you need -- the CLI strips these before
1414
+ // it sends the manifest, so the file stays readable and the server still
1415
+ // receives strict JSON.
1416
+ //
1417
+ // PROPERTY TYPES: string, number, boolean, select, multiselect, json,
1418
+ // color, date, connection, file, path, paths.
1419
+ //
1420
+ // "properties": {
1421
+ // "mode": {
1422
+ // "label": "Mode",
1423
+ // "type": "select",
1424
+ // "required": true,
1425
+ // "default": "fast",
1426
+ // "options": [
1427
+ // { "label": "Fast", "value": "fast" },
1428
+ // { "label": "Thorough", "value": "thorough" }
1429
+ // ]
1430
+ // },
1431
+ // "retries": { "label": "Retries", "type": "number", "min": 0, "max": 5, "default": 2 },
1432
+ // "account": { "label": "Account", "type": "connection", "provider": "outlook", "required": true },
1433
+ // "attachment": { "label": "Attachment", "type": "file", "description": "Read into config before the step runs -- right for a PDF" },
1434
+ // "clip": { "label": "Clip", "type": "path", "description": "Named, not read. What an ffmpeg step takes." },
1435
+ // "advanced": { "label": "Raw options", "type": "json", "group": "Advanced" }
1436
+ // },
1437
+ //
1438
+ // OUTPUT FIELDS are what the next step picks from a dropdown. Without them
1439
+ // somebody downstream has to guess your field names.
1440
+ //
1441
+ // "outputFields": { "id": "The id the provider gave it" },
1442
+ //
1443
+ // PORTS. Every name here is a line somebody can draw from on the canvas.
1444
+ // Return one of them as the "output" of your step.
1445
+ //
1446
+ // "outputs": ["success", "error", "empty"],
1447
+ //
1448
+ // HOW LONG one run may take. Capped by the account's plan, so asking for
1449
+ // more than the plan allows gets you the plan's number rather than an error.
1450
+ //
1451
+ // "timeoutMs": 60000,
1452
+ //
1453
+ // A TRIGGER makes this a starting point: the platform polls for you and
1454
+ // starts the workflow once per new item, so nothing is billed for waiting.
1455
+ // "connection" must name one of your own properties of type "connection".
1456
+ //
1457
+ // "trigger": {
1458
+ // "connection": "account",
1459
+ // "poll": 5,
1460
+ // "request": { "method": "GET", "path": "/me/messages", "query": { "$top": "25" } },
1461
+ // "items": "value",
1462
+ // "id": "id",
1463
+ // "cursor": { "from": "response", "path": "@odata.deltaLink" },
1464
+ // "hydrate": { "path": "/me/messages/{id}" }
1465
+ // }
1466
+ `;
1467
+ function manifestFile(active) {
1468
+ const body = JSON.stringify(active, null, 2);
1469
+ return body.slice(0, body.lastIndexOf("}")) + EXAMPLES + "}\n";
1470
+ }
1471
+ var blank = {
1472
+ description: "A step that reshapes what reached it.",
1473
+ category: "utility",
1474
+ code: `import type { PluginModule } from "./visualautomate"
1475
+
1476
+ /**
1477
+ * A step that takes what the steps before it produced and hands on something
1478
+ * new. No network, no account, no file \u2014 the most common step there is.
1479
+ */
1480
+ const plugin: PluginModule = {
1481
+ async execute(input, config, context) {
1482
+ const name = (config.name as string) || "world"
1483
+
1484
+ // Anything you log shows up in the run's log panel.
1485
+ console.log(\`greeting \${name}\`)
1486
+
1487
+ // \`output\` names which port the run leaves by, and has to be one of the
1488
+ // \`outputs\` in manifest.json. \`data\` is what the next steps can read as
1489
+ // {{this_step.field}}.
1490
+ return {
1491
+ output: "success",
1492
+ data: {
1493
+ greeting: \`Hello, \${name}!\`,
1494
+ at: context.startedAt,
1495
+ },
1496
+ }
1497
+ },
1498
+ }
1499
+
1500
+ export default plugin
1501
+ `,
1502
+ manifest: manifestFile({
1503
+ description: "Say hello",
1504
+ icon: "sparkles",
1505
+ auth: { type: "none" },
1506
+ properties: {
1507
+ name: { label: "Name", type: "string", required: false, description: "Who to greet" }
1508
+ },
1509
+ inputs: ["input"],
1510
+ outputs: ["success", "error"],
1511
+ outputFields: {
1512
+ greeting: "The greeting that was built",
1513
+ at: "When the run started"
1514
+ }
1515
+ }),
1516
+ testInput: { config: { name: "VisualAutomate" } }
1517
+ };
1518
+ var httpRequest = {
1519
+ description: "Call an API with a key the user pastes in.",
1520
+ category: "utility",
1521
+ code: `import type { PluginModule } from "./visualautomate"
1522
+
1523
+ /**
1524
+ * A step that calls an API the user holds their own key for.
1525
+ *
1526
+ * The key is a \`password\` property, so it is filled in on the canvas and never
1527
+ * lives in this code. If you are integrating a provider the platform already
1528
+ * connects to, use the "connection" template instead \u2014 then nobody pastes a key
1529
+ * at all.
1530
+ */
1531
+ const plugin: PluginModule = {
1532
+ async execute(input, config) {
1533
+ const url = config.url as string
1534
+ const apiKey = config.apiKey as string
1535
+
1536
+ if (!url) {
1537
+ return { output: "error", data: { error: "No URL was set on this step." } }
1538
+ }
1539
+
1540
+ const response = await fetch(url, {
1541
+ headers: apiKey ? { Authorization: \`Bearer \${apiKey}\` } : {},
1542
+ })
1543
+
1544
+ // A provider saying no is not a crash. Leaving by the error port lets
1545
+ // somebody draw a line from it and handle the case on the canvas.
1546
+ if (!response.ok) {
1547
+ return {
1548
+ output: "error",
1549
+ data: { error: \`The request failed with status \${response.status}\`, status: response.status },
1550
+ }
1551
+ }
1552
+
1553
+ // Not response.json(). An API that is having a bad day answers with an
1554
+ // HTML error page and a 200, and a plugin that assumes otherwise fails
1555
+ // with "Unexpected token '<'" \u2014 which tells nobody anything.
1556
+ const text = await response.text()
1557
+ let body: unknown = text
1558
+ try {
1559
+ body = JSON.parse(text)
1560
+ } catch {
1561
+ return {
1562
+ output: "error",
1563
+ data: { error: "The reply was not JSON.", status: response.status, body: text.slice(0, 500) },
1564
+ }
1565
+ }
1566
+
1567
+ return { output: "success", data: { body, status: response.status } }
1568
+ },
1569
+ }
1570
+
1571
+ export default plugin
1572
+ `,
1573
+ manifest: manifestFile({
1574
+ description: "Fetch JSON from an API",
1575
+ icon: "globe",
1576
+ auth: { type: "none" },
1577
+ properties: {
1578
+ url: { label: "URL", type: "string", required: true, description: "The endpoint to call" },
1579
+ apiKey: {
1580
+ label: "API key",
1581
+ type: "string",
1582
+ required: false,
1583
+ description: "Sent as a Bearer token, if the API needs one"
1584
+ }
1585
+ },
1586
+ inputs: ["input"],
1587
+ outputs: ["success", "error"],
1588
+ outputFields: {
1589
+ body: "The parsed JSON the API returned",
1590
+ status: "The HTTP status code"
1591
+ }
1592
+ }),
1593
+ testInput: { config: { url: "https://api.github.com/repos/nodejs/node" } }
1594
+ };
1595
+ var connection = {
1596
+ description: "Act on an account the user already connected.",
1597
+ category: "integration",
1598
+ code: `import type { PluginModule } from "./visualautomate"
1599
+
1600
+ /**
1601
+ * A step that acts as one of the user's connected accounts.
1602
+ *
1603
+ * You never see a token and you never name a host. You name a path, and the
1604
+ * platform attaches the credential on the way past \u2014 which is what stops a
1605
+ * plugin pointing somebody's account at an address of its own choosing.
1606
+ *
1607
+ * \`connections.get\` takes the name of one of YOUR properties ("account"
1608
+ * below), not a connection id, so this step can only reach the account it was
1609
+ * configured with.
1610
+ */
1611
+ const plugin: PluginModule = {
1612
+ async execute(input, config, context) {
1613
+ if (!context.connections) {
1614
+ return { output: "error", data: { error: "No account is connected to this step." } }
1615
+ }
1616
+
1617
+ const account = await context.connections.get("account")
1618
+
1619
+ const response = await account.request({
1620
+ method: "GET",
1621
+ path: (config.path as string) || "/me/messages",
1622
+ query: { $top: String(config.limit ?? 10) },
1623
+ })
1624
+
1625
+ if (!response.ok) {
1626
+ return {
1627
+ output: "error",
1628
+ data: { error: \`The provider answered \${response.status}\`, status: response.status },
1629
+ }
1630
+ }
1631
+
1632
+ const body = response.body as { value?: unknown[] }
1633
+ return {
1634
+ output: "success",
1635
+ data: { items: body.value ?? [], count: (body.value ?? []).length },
1636
+ }
1637
+ },
1638
+ }
1639
+
1640
+ export default plugin
1641
+ `,
1642
+ manifest: manifestFile({
1643
+ description: "Read from a connected account",
1644
+ icon: "link",
1645
+ auth: { type: "oauth2", provider: "outlook" },
1646
+ properties: {
1647
+ account: {
1648
+ label: "Account",
1649
+ type: "connection",
1650
+ provider: "outlook",
1651
+ required: true,
1652
+ description: "The account this step acts as"
1653
+ },
1654
+ path: {
1655
+ label: "Path",
1656
+ type: "string",
1657
+ required: false,
1658
+ default: "/me/messages",
1659
+ description: "Relative to the provider's base URL. Never a full URL."
1660
+ },
1661
+ limit: { label: "How many", type: "number", required: false, default: 10, min: 1, max: 100 }
1662
+ },
1663
+ inputs: ["input"],
1664
+ outputs: ["success", "error"],
1665
+ outputFields: {
1666
+ items: "What the provider returned",
1667
+ count: "How many items came back"
1668
+ }
1669
+ }),
1670
+ testInput: {
1671
+ config: { path: "/me/messages", limit: 2 }
1672
+ // context.connections cannot be simulated honestly, so `test` refuses
1673
+ // it. Stub it in a test of your own to exercise the code around it.
1674
+ }
1675
+ };
1676
+ var file = {
1677
+ description: "Work on a file in the account's storage.",
1678
+ category: "utility",
1679
+ code: `import type { PluginModule } from "./visualautomate"
1680
+
1681
+ /**
1682
+ * A step that reads a file the account already has and writes another.
1683
+ *
1684
+ * \`path\` properties give the user a picker over their own storage, so nobody
1685
+ * types a filename and hopes. Storage is scoped to the account: you cannot name
1686
+ * anybody else's file, and you do not prefix your own.
1687
+ */
1688
+ const plugin: PluginModule = {
1689
+ async execute(input, config, context) {
1690
+ const source = config.source as string
1691
+ const bytes = await context.storage.get(source)
1692
+
1693
+ if (!bytes) {
1694
+ return { output: "error", data: { error: \`There is no file at \${source}.\` } }
1695
+ }
1696
+
1697
+ const text = new TextDecoder().decode(bytes)
1698
+ const upper = text.toUpperCase()
1699
+
1700
+ const target = (config.target as string) || source.replace(/(\\.[^.]+)?$/, "-shouted$1")
1701
+ const written = await context.storage.put(target, upper, "text/plain")
1702
+
1703
+ // Remembered between runs. Useful for a cursor or a last-seen id; this
1704
+ // one just counts.
1705
+ const runs = ((await context.state.get("runs")) as number) ?? 0
1706
+ await context.state.set("runs", runs + 1)
1707
+
1708
+ return {
1709
+ output: "success",
1710
+ data: { path: target, size: written.size, runs: runs + 1 },
1711
+ }
1712
+ },
1713
+ }
1714
+
1715
+ export default plugin
1716
+ `,
1717
+ manifest: manifestFile({
1718
+ description: "Shout a text file",
1719
+ icon: "file-text",
1720
+ auth: { type: "none" },
1721
+ properties: {
1722
+ source: { label: "File", type: "path", required: true, description: "A file already in your storage" },
1723
+ target: {
1724
+ label: "Save as",
1725
+ type: "string",
1726
+ required: false,
1727
+ description: "Where to write the result. Defaults to the source with -shouted added."
1728
+ }
1729
+ },
1730
+ inputs: ["input"],
1731
+ outputs: ["success", "error"],
1732
+ outputFields: {
1733
+ path: "Where the result was written",
1734
+ size: "How large it is, in bytes",
1735
+ runs: "How many times this workflow has run this step"
1736
+ }
1737
+ }),
1738
+ testInput: {
1739
+ config: { source: "notes/hello.txt" },
1740
+ // `test` starts with these already in storage and in state, so the step
1741
+ // has something to read on its very first run.
1742
+ storage: { "notes/hello.txt": "hello from a file" },
1743
+ state: { runs: 0 }
1744
+ }
1745
+ };
1746
+ var TEMPLATES = {
1747
+ blank,
1748
+ "http-request": httpRequest,
1749
+ connection,
1750
+ file
1751
+ };
1752
+
1753
+ // src/jsonc.ts
1754
+ function stripJsonComments(text) {
1755
+ let out = "";
1756
+ let inString = false;
1757
+ let inLine = false;
1758
+ let inBlock = false;
1759
+ for (let i = 0; i < text.length; i++) {
1760
+ const char = text[i];
1761
+ const next = text[i + 1];
1762
+ if (inLine) {
1763
+ if (char === "\n") {
1764
+ inLine = false;
1765
+ out += char;
1766
+ } else {
1767
+ out += " ";
1768
+ }
1769
+ continue;
1770
+ }
1771
+ if (inBlock) {
1772
+ if (char === "*" && next === "/") {
1773
+ inBlock = false;
1774
+ out += " ";
1775
+ i++;
1776
+ } else {
1777
+ out += char === "\n" ? char : " ";
1778
+ }
1779
+ continue;
1780
+ }
1781
+ if (inString) {
1782
+ out += char;
1783
+ if (char === "\\") {
1784
+ out += text[++i] ?? "";
1785
+ } else if (char === '"') {
1786
+ inString = false;
1787
+ }
1788
+ continue;
1789
+ }
1790
+ if (char === '"') {
1791
+ inString = true;
1792
+ out += char;
1793
+ continue;
1794
+ }
1795
+ if (char === "/" && next === "/") {
1796
+ inLine = true;
1797
+ out += " ";
1798
+ i++;
1799
+ continue;
1800
+ }
1801
+ if (char === "/" && next === "*") {
1802
+ inBlock = true;
1803
+ out += " ";
1804
+ i++;
1805
+ continue;
1806
+ }
1807
+ out += char;
1808
+ }
1809
+ return out;
1810
+ }
1811
+ function parseJsonc(text) {
1812
+ const value = JSON.parse(stripJsonComments(text));
1813
+ return { value, json: JSON.stringify(value, null, 2) };
1814
+ }
1815
+
1816
+ // src/project.ts
1817
+ import { existsSync, readdirSync, statSync } from "node:fs";
1818
+ import { readFile } from "node:fs/promises";
1819
+ import { basename, join, relative, resolve } from "node:path";
1820
+ var TEST_FILES = ["visualautomate.test.json", "test.json"];
1821
+ var ENTRIES = ["plugin.ts", "plugin.js", "index.ts", "index.js"];
1822
+ function entryIn(dir) {
1823
+ return ENTRIES.map((file2) => join(dir, file2)).find((file2) => existsSync(file2)) ?? null;
1824
+ }
1825
+ function findTargets(cwd, only) {
1826
+ const dir = resolve(cwd);
1827
+ const modulesDir = join(dir, "modules");
1828
+ if (!entryIn(dir) && existsSync(modulesDir) && statSync(modulesDir).isDirectory()) {
1829
+ const folders = readdirSync(modulesDir, { withFileTypes: true }).filter((d) => d.isDirectory() && existsSync(join(modulesDir, d.name, "manifest.json"))).map((d) => d.name).sort();
1830
+ if (folders.length === 0) {
1831
+ throw new Error("There is a modules/ folder here, but none of its folders has a manifest.json.");
1832
+ }
1833
+ const chosen = only ? folders.filter((f) => f === only) : folders;
1834
+ if (only && chosen.length === 0) {
1835
+ throw new Error(`No module "${only}" in modules/. There is: ${folders.join(", ")}.`);
1836
+ }
1837
+ return chosen.map((folder) => targetIn(join(modulesDir, folder), `modules/${folder}`)).filter(Boolean);
1838
+ }
1839
+ const target = targetIn(dir, basename(dir));
1840
+ if (!target) {
1841
+ throw new Error(
1842
+ "Nothing to test here: no plugin.ts, plugin.js or index.js, and no modules/ folder.\nRun this in a plugin's folder, at the root of its repository, or run `visualautomate init <name>`."
1843
+ );
1844
+ }
1845
+ return [target];
1846
+ }
1847
+ function targetIn(dir, name) {
1848
+ const entry = entryIn(dir);
1849
+ if (!entry) return null;
1850
+ const raw = basename(entry) === "index.js" && !existsSync(join(dir, "plugin.json"));
1851
+ return { name, dir, entry, raw };
1852
+ }
1853
+ function targetForPath(targets, changed) {
1854
+ const hit = targets.find((t) => {
1855
+ const rel = relative(t.dir, changed);
1856
+ return !rel.startsWith("..") && !rel.startsWith("/") && !/^[a-zA-Z]:/.test(rel);
1857
+ });
1858
+ return targets.length > 1 ? hit ?? null : targets[0] ?? null;
1859
+ }
1860
+ function isPlainObject(value) {
1861
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1862
+ }
1863
+ async function readTestFile(dir, override) {
1864
+ const path = override ? resolve(override) : TEST_FILES.map((f) => join(dir, f)).find((f) => existsSync(f)) ?? null;
1865
+ if (!path) return { path: null, config: {}, input: {} };
1866
+ if (!existsSync(path)) throw new Error(`${path} does not exist.`);
1867
+ let raw;
1868
+ try {
1869
+ raw = JSON.parse(await readFile(path, "utf8"));
1870
+ } catch (error) {
1871
+ throw new Error(`${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
1872
+ }
1873
+ if (!isPlainObject(raw)) throw new Error(`${path} must be an object with "config" and "input".`);
1874
+ for (const key of ["config", "input", "storage", "state", "secrets"]) {
1875
+ if (raw[key] !== void 0 && !isPlainObject(raw[key])) {
1876
+ throw new Error(`${path}: "${key}" must be an object.`);
1877
+ }
1878
+ }
1879
+ if (raw.expectOutput !== void 0 && (typeof raw.expectOutput !== "string" || raw.expectOutput === "")) {
1880
+ throw new Error(`${path}: "expectOutput" must be the name of an output port, like "success".`);
1881
+ }
1882
+ if (raw.timeoutMs !== void 0 && (typeof raw.timeoutMs !== "number" || raw.timeoutMs <= 0 || raw.timeoutMs > 6e4)) {
1883
+ throw new Error(`${path}: "timeoutMs" must be a number of milliseconds up to 60000.`);
1884
+ }
1885
+ return {
1886
+ path,
1887
+ config: raw.config ?? {},
1888
+ input: raw.input ?? {},
1889
+ storage: raw.storage,
1890
+ state: raw.state,
1891
+ secrets: raw.secrets,
1892
+ timeoutMs: raw.timeoutMs,
1893
+ expectOutput: raw.expectOutput
1894
+ };
1895
+ }
1896
+
1897
+ // src/sandbox-module.ts
1898
+ var IMPORT = /^import\s+(?:([\w$]+)\s*,\s*)?(?:([\w$]+)|\*\s+as\s+([\w$]+)|\{([\s\S]*?)\})\s+from\s+"([^"]+)";?[ \t]*$/gm;
1899
+ var BARE_IMPORT = /^import\s+"([^"]+)";?[ \t]*$/gm;
1900
+ var EXPORT = /^export\s*\{([\s\S]*?)\};?[ \t]*$/m;
1901
+ var FILE_MARKER = /^\/\/ (?:\.{1,2}\/|[A-Za-z]:[\\/]|\/)?[^\n]*\.(?:[cm]?[jt]sx?)[ \t]*$\n?/gm;
1902
+ function names(list) {
1903
+ return list.split(",").map((part) => part.trim()).filter(Boolean).map((part) => {
1904
+ const [from, as] = part.split(/\s+as\s+/);
1905
+ return { from: from.trim(), as: (as ?? from).trim() };
1906
+ });
1907
+ }
1908
+ function toSandboxModule(esm) {
1909
+ let code = esm.replace(FILE_MARKER, "");
1910
+ code = code.replace(IMPORT, (_all, defaultWithOthers, defaultOnly, namespace, list, pkg) => {
1911
+ const target = `require(${JSON.stringify(pkg)})`;
1912
+ const lines = [];
1913
+ const main2 = defaultWithOthers ?? defaultOnly ?? namespace;
1914
+ if (main2) lines.push(`const ${main2} = ${target};`);
1915
+ if (list !== void 0) {
1916
+ const pattern = names(list).map(({ from, as }) => from === as ? from : `${from}: ${as}`).join(", ");
1917
+ lines.push(`const { ${pattern} } = ${main2 ?? target};`);
1918
+ }
1919
+ return lines.join("\n");
1920
+ });
1921
+ code = code.replace(BARE_IMPORT, (_all, pkg) => `require(${JSON.stringify(pkg)});`);
1922
+ const exported = code.match(EXPORT);
1923
+ if (exported) {
1924
+ const list = names(exported[1]);
1925
+ const byName = new Map(list.map(({ from, as }) => [as, from]));
1926
+ const fallback = byName.get("default");
1927
+ let assignment;
1928
+ if (!byName.has("execute") && fallback && list.length === 1) {
1929
+ assignment = `module.exports = ${fallback};`;
1930
+ } else {
1931
+ const fields = list.map(({ from, as }) => from === as ? from : `${as}: ${from}`);
1932
+ assignment = `module.exports = { ${fields.join(", ")} };`;
1933
+ if (!byName.has("execute") && fallback) {
1934
+ assignment += `
1935
+ if (${fallback} && typeof ${fallback}.execute === "function") module.exports.execute = ${fallback}.execute;`;
1936
+ }
1937
+ }
1938
+ code = code.replace(EXPORT, assignment);
1939
+ }
1940
+ return code.replace(/^\n+/, "");
1941
+ }
1942
+
1943
+ // src/index.ts
1944
+ var DEFAULT_API_URL = "https://visualautomate.com";
1945
+ function configPath() {
1946
+ return join2(homedir(), ".visualautomate", "config.json");
1947
+ }
1948
+ async function readConfig() {
1949
+ try {
1950
+ return JSON.parse(await readFile2(configPath(), "utf8"));
1951
+ } catch {
1952
+ return {};
1953
+ }
1954
+ }
1955
+ async function writeConfig(next) {
1956
+ const path = configPath();
1957
+ await mkdir(dirname(path), { recursive: true });
1958
+ await writeFile(path, JSON.stringify(next, null, 2) + "\n", { mode: 384 });
1959
+ }
1960
+ async function tokenFor(flags) {
1961
+ const stored = await readConfig();
1962
+ const apiUrl = (flags["api-url"] || process.env.VISUALAUTOMATE_API_URL || stored.apiUrl || DEFAULT_API_URL).replace(/\/+$/, "");
1963
+ const token = flags.token || process.env.VISUALAUTOMATE_API_TOKEN || stored.token;
1964
+ if (!token) {
1965
+ fail("Not signed in. Run `visualautomate login` first.");
1966
+ }
1967
+ return { token, apiUrl };
1968
+ }
1969
+ function fail(message) {
1970
+ console.error(message);
1971
+ throw new SilentExit();
1972
+ }
1973
+ var SilentExit = class extends Error {
1974
+ constructor() {
1975
+ super("");
1976
+ this.name = "SilentExit";
1977
+ }
1978
+ };
1979
+ function ok(message) {
1980
+ console.log(message);
1981
+ }
1982
+ function openBrowser(url) {
1983
+ const command = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
1984
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
1985
+ try {
1986
+ spawn(command, args, { stdio: "ignore", detached: true }).unref();
1987
+ } catch {
1988
+ }
1989
+ }
1990
+ async function cmdLogin(flags) {
1991
+ const stored = await readConfig();
1992
+ const apiUrl = (flags["api-url"] || process.env.VISUALAUTOMATE_API_URL || stored.apiUrl || DEFAULT_API_URL).replace(/\/+$/, "");
1993
+ const state = randomBytes(24).toString("base64url");
1994
+ const token = await new Promise((resolveToken, rejectToken) => {
1995
+ const server = createServer((req, res) => {
1996
+ if (!req.url?.startsWith("/callback") || req.method !== "POST") {
1997
+ res.writeHead(404).end("Not found");
1998
+ return;
1999
+ }
2000
+ let body = "";
2001
+ req.on("data", (chunk) => {
2002
+ body += chunk;
2003
+ if (body.length > 8192) req.destroy();
2004
+ });
2005
+ req.on("end", () => {
2006
+ const fields = new URLSearchParams(body);
2007
+ const answer = (status, text) => {
2008
+ res.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
2009
+ res.end(
2010
+ `<!doctype html><meta charset="utf-8"><title>VisualAutomate</title><body style="font:16px system-ui;padding:3rem;max-width:32rem;margin:auto">${text}</body>`
2011
+ );
2012
+ };
2013
+ if (fields.get("state") !== state) {
2014
+ answer(400, "<h1>That did not match</h1><p>Run <code>visualautomate login</code> again.</p>");
2015
+ return;
2016
+ }
2017
+ const received = fields.get("token");
2018
+ if (!received) {
2019
+ answer(400, "<h1>No token in the reply</h1><p>Run <code>visualautomate login</code> again.</p>");
2020
+ return;
2021
+ }
2022
+ answer(200, "<h1>Signed in</h1><p>You can close this tab and go back to your terminal.</p>");
2023
+ server.close();
2024
+ resolveToken(received);
2025
+ });
2026
+ });
2027
+ server.listen(0, "127.0.0.1", () => {
2028
+ const address = server.address();
2029
+ if (!address || typeof address === "string") {
2030
+ rejectToken(new Error("Could not open a port to listen on"));
2031
+ return;
2032
+ }
2033
+ const url = `${apiUrl}/cli/login?port=${address.port}&state=${state}`;
2034
+ ok(`Opening ${url}
2035
+ `);
2036
+ ok("If your browser did not open, paste that address into it.");
2037
+ openBrowser(url);
2038
+ });
2039
+ server.on("error", rejectToken);
2040
+ setTimeout(() => {
2041
+ server.close();
2042
+ rejectToken(new Error("Timed out after five minutes waiting for the browser."));
2043
+ }, 5 * 6e4).unref();
2044
+ });
2045
+ const who = await whoami(apiUrl, token);
2046
+ await writeConfig({ token, apiUrl, email: who.email });
2047
+ ok(`
2048
+ \u2713 Signed in as ${who.email}`);
2049
+ ok(` Token stored in ${configPath()}`);
2050
+ }
2051
+ async function whoami(apiUrl, token) {
2052
+ const res = await fetch(`${apiUrl}/api/cli/whoami`, {
2053
+ headers: { Authorization: `Bearer ${token}` }
2054
+ });
2055
+ if (res.status === 401) fail("That token is not valid. Run `visualautomate login` again.");
2056
+ if (!res.ok) fail(`Could not reach ${apiUrl} (status ${res.status}).`);
2057
+ const data = await res.json();
2058
+ return { email: data.email ?? "(unknown)", developer: Boolean(data.pluginDeveloper) };
2059
+ }
2060
+ async function cmdWhoami(flags) {
2061
+ const { token, apiUrl } = await tokenFor(flags);
2062
+ const who = await whoami(apiUrl, token);
2063
+ ok(`${who.email} on ${apiUrl}`);
2064
+ if (!who.developer) {
2065
+ ok("\nThis account is not a plugin developer yet, so `push` will be refused.");
2066
+ ok("Turn it on from the dashboard under Developer settings.");
2067
+ }
2068
+ }
2069
+ async function cmdLogout() {
2070
+ await writeConfig({});
2071
+ ok("Signed out. The token file is empty.");
2072
+ }
2073
+ async function cmdInit(args, flags) {
2074
+ const name = args[0];
2075
+ if (!name) fail("Usage: visualautomate init <name> [--template <name>]");
2076
+ const templateName = flags.template ?? "blank";
2077
+ const template = TEMPLATES[templateName];
2078
+ if (!template) {
2079
+ fail(`Unknown template "${templateName}". Available: ${Object.keys(TEMPLATES).join(", ")}`);
2080
+ }
2081
+ const target = resolve2(process.cwd(), name);
2082
+ if (existsSync2(target)) fail(`"${name}" already exists here.`);
2083
+ const slug = name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2084
+ await mkdir(target, { recursive: true });
2085
+ await writeFile(join2(target, "plugin.ts"), template.code);
2086
+ await writeFile(join2(target, "manifest.json"), template.manifest);
2087
+ await writeFile(
2088
+ join2(target, "plugin.json"),
2089
+ JSON.stringify(
2090
+ { name: slug, version: "1.0.0", description: template.description, category: template.category },
2091
+ null,
2092
+ 2
2093
+ ) + "\n"
2094
+ );
2095
+ await writeFile(join2(target, "visualautomate.d.ts"), TYPE_DECLARATIONS);
2096
+ await writeFile(
2097
+ join2(target, "tsconfig.json"),
2098
+ JSON.stringify(
2099
+ {
2100
+ compilerOptions: {
2101
+ target: "ES2022",
2102
+ module: "ESNext",
2103
+ moduleResolution: "bundler",
2104
+ lib: ["ES2022"],
2105
+ strict: true,
2106
+ noEmit: true,
2107
+ skipLibCheck: true
2108
+ },
2109
+ include: ["plugin.ts", "visualautomate.d.ts"]
2110
+ },
2111
+ null,
2112
+ 2
2113
+ ) + "\n"
2114
+ );
2115
+ await writeFile(
2116
+ join2(target, "test.json"),
2117
+ JSON.stringify(template.testInput, null, 2) + "\n"
2118
+ );
2119
+ await writeFile(
2120
+ join2(target, "README.md"),
2121
+ `# ${name}
2122
+
2123
+ ${template.description}
2124
+
2125
+ \`\`\`bash
2126
+ visualautomate test # run it here
2127
+ visualautomate push # publish it to your account
2128
+ \`\`\`
2129
+
2130
+ \`manifest.json\` declares the fields the canvas shows and what this step hands on.
2131
+ \`test.json\` is the input, config and state \`test\` runs with.
2132
+ `
2133
+ );
2134
+ ok(`
2135
+ \u2713 Created ${name} from the "${templateName}" template
2136
+ `);
2137
+ ok(" cd " + name);
2138
+ ok(" visualautomate test");
2139
+ ok(" visualautomate push\n");
2140
+ }
2141
+ async function bundlePlugin(entry) {
2142
+ try {
2143
+ const result = await build({
2144
+ entryPoints: [entry],
2145
+ bundle: true,
2146
+ write: false,
2147
+ platform: "node",
2148
+ format: "esm",
2149
+ target: "node18",
2150
+ packages: "external",
2151
+ logLevel: "silent"
2152
+ });
2153
+ return toSandboxModule(result.outputFiles[0].text);
2154
+ } catch (error) {
2155
+ const message = error instanceof Error ? error.message : String(error);
2156
+ throw new Error(`Your plugin did not compile.
2157
+
2158
+ ${message}`);
2159
+ }
2160
+ }
2161
+ async function codeFor(target) {
2162
+ return target.raw ? readFile2(target.entry, "utf8") : bundlePlugin(target.entry);
2163
+ }
2164
+ function line(indent, text) {
2165
+ ok(indent ? " " + text.replace(/\n/g, "\n ") : text);
2166
+ }
2167
+ async function runTarget(target, flags, several) {
2168
+ const say = (text) => line(several, text);
2169
+ if (several) ok(`
2170
+ \u25B8 ${target.name}`);
2171
+ try {
2172
+ const code = await codeFor(target);
2173
+ const check = validatePluginCode(code);
2174
+ if (!check.valid) {
2175
+ say(`\u2717 refused ${check.error}`);
2176
+ return false;
2177
+ }
2178
+ const settings = await readTestFile(target.dir, flags.input);
2179
+ if (!settings.path) say("No visualautomate.test.json \u2014 running with empty input and config.");
2180
+ const load = createRequire(join2(target.dir, "noop.js"));
2181
+ const exported = loadPluginCode(code, sandboxRequire(load));
2182
+ const plugin = typeof exported === "function" ? exported : typeof exported?.execute === "function" ? exported : exported?.default;
2183
+ let outputs = [];
2184
+ const manifestPath = join2(target.dir, "manifest.json");
2185
+ if (existsSync2(manifestPath)) {
2186
+ try {
2187
+ const manifest = parseJsonc(await readFile2(manifestPath, "utf8")).value;
2188
+ outputs = manifest.outputs ?? [];
2189
+ } catch (error) {
2190
+ say(`\u2717 manifest.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
2191
+ return false;
2192
+ }
2193
+ }
2194
+ const result = await simulate(plugin, {
2195
+ input: settings.input,
2196
+ config: settings.config,
2197
+ storage: settings.storage,
2198
+ state: settings.state,
2199
+ secrets: settings.secrets,
2200
+ timeoutMs: settings.timeoutMs
2201
+ });
2202
+ const port = result.output?.output ?? null;
2203
+ const expected = settings.expectOutput;
2204
+ const passed = result.status === "success" && (!expected || port === expected);
2205
+ say(`${passed ? "\u2713 passed" : "\u2717 failed"} ${result.status}, ${result.durationMs}ms`);
2206
+ if (result.error) say(`error ${result.error}`);
2207
+ if (result.output) {
2208
+ say(`output ${port}${expected ? ` (expected ${expected})` : ""}`);
2209
+ say(`data ${JSON.stringify(result.output.data, null, 2).replace(/\n/g, "\n ")}`);
2210
+ }
2211
+ if (result.files.size > 0) say(`files ${[...result.files.keys()].join(", ")}`);
2212
+ if (result.state.size > 0) say(`state ${JSON.stringify(Object.fromEntries(result.state))}`);
2213
+ for (const entry of result.logs ?? []) say(`log ${entry}`);
2214
+ if (port && outputs.length > 0 && !outputs.includes(port)) {
2215
+ say(
2216
+ `! This returned output "${port}", which manifest.json does not list (it has ${outputs.map((o) => `"${o}"`).join(", ")}). On a canvas nothing would follow it.`
2217
+ );
2218
+ }
2219
+ return passed;
2220
+ } catch (error) {
2221
+ say(`\u2717 ${error instanceof Error ? error.message : String(error)}`);
2222
+ return false;
2223
+ }
2224
+ }
2225
+ async function runTargets(targets, flags) {
2226
+ let passed = 0;
2227
+ for (const target of targets) {
2228
+ if (await runTarget(target, flags, targets.length > 1)) passed++;
2229
+ }
2230
+ if (targets.length > 1) ok(`
2231
+ ${passed} of ${targets.length} passed`);
2232
+ return passed === targets.length;
2233
+ }
2234
+ function targetsOrFail(flags) {
2235
+ try {
2236
+ return findTargets(process.cwd(), flags.module);
2237
+ } catch (error) {
2238
+ fail(error instanceof Error ? error.message : String(error));
2239
+ }
2240
+ }
2241
+ async function cmdTest(flags) {
2242
+ const targets = targetsOrFail(flags);
2243
+ if (!await runTargets(targets, flags)) process.exitCode = 1;
2244
+ }
2245
+ async function cmdDev(flags) {
2246
+ const root = process.cwd();
2247
+ let targets = targetsOrFail(flags);
2248
+ const header = () => {
2249
+ process.stdout.write("\x1Bc");
2250
+ ok(`visualautomate dev \u2014 ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \u2014 Ctrl+C to stop`);
2251
+ };
2252
+ header();
2253
+ await runTargets(targets, flags);
2254
+ const ignored = [`${sep}node_modules${sep}`, `${sep}.git${sep}`, `${sep}dist${sep}`, `${sep}.visualautomate${sep}`];
2255
+ const pending = /* @__PURE__ */ new Set();
2256
+ let timer;
2257
+ let running = false;
2258
+ const flush = async () => {
2259
+ if (running) {
2260
+ timer = setTimeout(flush, 100);
2261
+ return;
2262
+ }
2263
+ running = true;
2264
+ const changed = [...pending];
2265
+ pending.clear();
2266
+ try {
2267
+ targets = findTargets(root, flags.module);
2268
+ } catch (error) {
2269
+ header();
2270
+ ok(`\u2717 ${error instanceof Error ? error.message : String(error)}`);
2271
+ running = false;
2272
+ return;
2273
+ }
2274
+ const hit = changed.map((path) => targetForPath(targets, path));
2275
+ const chosen = hit.some((t) => t === null) ? targets : [...new Set(hit)];
2276
+ header();
2277
+ ok(`changed: ${changed.map((p) => relative2(root, p)).join(", ")}`);
2278
+ await runTargets(chosen, flags);
2279
+ running = false;
2280
+ };
2281
+ const watcher = watch(root, { recursive: true }, (_event, name) => {
2282
+ if (!name) return;
2283
+ const path = join2(root, name.toString());
2284
+ if (ignored.some((part) => `${sep}${relative2(root, path)}${sep}`.includes(part))) return;
2285
+ pending.add(path);
2286
+ if (timer) clearTimeout(timer);
2287
+ timer = setTimeout(flush, 150);
2288
+ });
2289
+ await new Promise((resolveStop) => {
2290
+ process.once("SIGINT", () => {
2291
+ watcher.close();
2292
+ resolveStop();
2293
+ });
2294
+ });
2295
+ }
2296
+ async function cmdPush(flags) {
2297
+ const dir = process.cwd();
2298
+ const { token, apiUrl } = await tokenFor(flags);
2299
+ const metaPath = join2(dir, "plugin.json");
2300
+ const pkgPath = join2(dir, "package.json");
2301
+ const metaFile = existsSync2(metaPath) ? metaPath : existsSync2(pkgPath) ? pkgPath : null;
2302
+ if (!metaFile) {
2303
+ fail("No plugin.json here. Run `visualautomate init <name>` to start a plugin.");
2304
+ }
2305
+ const meta = JSON.parse(await readFile2(metaFile, "utf8"));
2306
+ if (!meta.name) fail(`${metaFile} has no "name".`);
2307
+ if (!meta.version || !/^\d+\.\d+\.\d+$/.test(meta.version)) {
2308
+ fail(`${metaFile} needs a "version" like 1.0.0.`);
2309
+ }
2310
+ const manifestPath = resolve2(process.cwd(), "manifest.json");
2311
+ let parsed = {};
2312
+ let manifest = null;
2313
+ if (existsSync2(manifestPath)) {
2314
+ try {
2315
+ const result = parseJsonc(await readFile2(manifestPath, "utf8"));
2316
+ parsed = result.value;
2317
+ manifest = result.json;
2318
+ } catch (error) {
2319
+ fail(`manifest.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
2320
+ }
2321
+ }
2322
+ const entry = ["plugin.ts", "plugin.js", "index.ts", "index.js"].map((f) => join2(dir, f)).find((f) => existsSync2(f));
2323
+ if (!entry) {
2324
+ fail("No plugin.ts or plugin.js here. Run `visualautomate init <name>` to start one.");
2325
+ }
2326
+ let code;
2327
+ try {
2328
+ code = await bundlePlugin(entry);
2329
+ } catch (error) {
2330
+ fail(error instanceof Error ? error.message : String(error));
2331
+ }
2332
+ let schemaJson = null;
2333
+ if (!manifest) {
2334
+ const source = await readFile2(entry, "utf8");
2335
+ const match = source.match(/@schema\s*\n([\s\S]*?)(?:\*\s*\/|\*\/)/);
2336
+ if (match) {
2337
+ schemaJson = match[1].replace(/^\s*\*\s?/gm, "").trim();
2338
+ try {
2339
+ parsed = JSON.parse(schemaJson);
2340
+ } catch (error) {
2341
+ fail(`The @schema block is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
2342
+ }
2343
+ }
2344
+ }
2345
+ if (!manifest && !schemaJson) {
2346
+ fail(
2347
+ "This plugin declares nothing, so it would have no fields, no ports and no trigger \u2014\nthe canvas would show an empty box. Add a manifest.json next to your code (run\n`visualautomate init` somewhere to see one), or keep an @schema block in the source."
2348
+ );
2349
+ }
2350
+ ok(`Pushing ${meta.name}@${meta.version} to ${apiUrl}...`);
2351
+ const res = await fetch(`${apiUrl}/api/plugins/push`, {
2352
+ method: "POST",
2353
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
2354
+ body: JSON.stringify({
2355
+ name: meta.name,
2356
+ version: meta.version,
2357
+ description: meta.description ?? parsed.description ?? "",
2358
+ category: meta.category ?? "utility",
2359
+ icon: meta.icon ?? parsed.icon ?? "plugin",
2360
+ // Whichever the author actually has. The route takes either.
2361
+ ...manifest ? { manifest } : { schemaJson },
2362
+ code
2363
+ })
2364
+ });
2365
+ const text = await res.text();
2366
+ let data = {};
2367
+ try {
2368
+ data = JSON.parse(text);
2369
+ } catch {
2370
+ fail(`The server answered ${res.status} with something that is not JSON:
2371
+ ${text.slice(0, 500)}`);
2372
+ }
2373
+ if (res.status === 401) {
2374
+ fail("Your session has expired. Run `visualautomate login` again.");
2375
+ }
2376
+ if (!res.ok || !data.success) {
2377
+ fail(`Push refused: ${data.error ?? `status ${res.status}`}`);
2378
+ }
2379
+ ok(`
2380
+ \u2713 Published (id: ${data.pluginId})`);
2381
+ ok(` A version snapshot was saved, so you can roll back from the dashboard.`);
2382
+ }
2383
+ function printHelp() {
2384
+ console.log(`
2385
+ visualautomate \u2014 build and publish plugins
2386
+
2387
+ visualautomate login sign this machine in
2388
+ visualautomate init <name> [--template] start a plugin
2389
+ visualautomate test [--module <name>] run it here, as the platform's check does
2390
+ visualautomate dev [--module <name>] run it again on every save
2391
+ visualautomate push publish it
2392
+ visualautomate whoami which account this is
2393
+ visualautomate logout forget the token
2394
+
2395
+ Templates: ${Object.keys(TEMPLATES).join(", ")}
2396
+
2397
+ \`test\` and \`dev\` work in a plugin made with \`init\`, at the root of a plugin's
2398
+ GitHub repository, and at the root of an app's repository, where every folder
2399
+ in modules/ is run (or the one named with --module). The run's settings come
2400
+ from visualautomate.test.json, test.json, or --input <file>.
2401
+
2402
+ Environment (for CI, where there is no browser):
2403
+ VISUALAUTOMATE_API_TOKEN use instead of \`login\`
2404
+ VISUALAUTOMATE_API_URL a different installation
2405
+ `);
2406
+ }
2407
+ function parse(argv) {
2408
+ const args = [];
2409
+ const flags = {};
2410
+ for (let i = 0; i < argv.length; i++) {
2411
+ const arg = argv[i];
2412
+ if (arg.startsWith("--")) {
2413
+ const key = arg.slice(2);
2414
+ const next = argv[i + 1];
2415
+ flags[key] = next && !next.startsWith("--") ? argv[++i] : "true";
2416
+ } else {
2417
+ args.push(arg);
2418
+ }
2419
+ }
2420
+ return { args, flags };
2421
+ }
2422
+ async function main() {
2423
+ const { args, flags } = parse(process.argv.slice(2));
2424
+ const command = args.shift();
2425
+ switch (command) {
2426
+ case "login":
2427
+ return cmdLogin(flags);
2428
+ case "logout":
2429
+ return cmdLogout();
2430
+ case "whoami":
2431
+ return cmdWhoami(flags);
2432
+ case "init":
2433
+ case "new":
2434
+ return cmdInit(args, flags);
2435
+ case "test":
2436
+ return cmdTest(flags);
2437
+ case "dev":
2438
+ case "watch":
2439
+ return cmdDev(flags);
2440
+ case "push":
2441
+ case "publish":
2442
+ return cmdPush(flags);
2443
+ // `plugin test` and `plugin push` are accepted as aliases.
2444
+ case "plugin": {
2445
+ const sub = args.shift();
2446
+ if (sub === "test") return cmdTest(flags);
2447
+ if (sub === "push") return cmdPush(flags);
2448
+ printHelp();
2449
+ return;
2450
+ }
2451
+ default:
2452
+ printHelp();
2453
+ if (command) process.exit(1);
2454
+ }
2455
+ }
2456
+ async function finish(code) {
2457
+ try {
2458
+ stopBundler();
2459
+ } catch {
2460
+ }
2461
+ process.exit(code);
2462
+ }
2463
+ main().then(() => finish(Number(process.exitCode ?? 0))).catch((error) => {
2464
+ if (!(error instanceof SilentExit)) {
2465
+ console.error(error instanceof Error ? error.message : String(error));
2466
+ }
2467
+ return finish(1);
2468
+ });