@trustless-work/blocks 1.1.4 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/index.js CHANGED
@@ -1,1967 +1,2038 @@
1
- #!/usr/bin/env node
2
-
3
- /*
4
- AUTHOR: @trustless-work / Joel Vargas
5
- COPYRIGHT: 2025 Trustless Work
6
- LICENSE: MIT
7
- VERSION: 1.0.0
8
- */
9
-
10
- import fs from "node:fs";
11
- import path from "node:path";
12
- import { fileURLToPath } from "node:url";
13
- import { spawnSync, spawn } from "node:child_process";
14
- import readline from "node:readline";
15
-
16
- const __filename = fileURLToPath(import.meta.url);
17
- const __dirname = path.dirname(__filename);
18
-
19
- const PROJECT_ROOT = process.cwd();
20
- const TEMPLATES_DIR = path.join(__dirname, "..", "templates");
21
- const GLOBAL_DEPS_FILE = path.join(TEMPLATES_DIR, "deps.json");
22
-
23
- const args = process.argv.slice(2);
24
-
25
- function detectPM() {
26
- if (fs.existsSync(path.join(PROJECT_ROOT, "pnpm-lock.yaml"))) return "pnpm";
27
- if (fs.existsSync(path.join(PROJECT_ROOT, "yarn.lock"))) return "yarn";
28
- if (fs.existsSync(path.join(PROJECT_ROOT, "bun.lockb"))) return "bun";
29
- return "npm";
30
- }
31
-
32
- function run(cmd, args) {
33
- const r = spawnSync(cmd, args.filter(Boolean), {
34
- stdio: "inherit",
35
- cwd: PROJECT_ROOT,
36
- shell: true,
37
- });
38
- if (r.status !== 0) process.exit(r.status ?? 1);
39
- }
40
-
41
- function tryRun(cmd, args, errorMessage) {
42
- const r = spawnSync(cmd, args.filter(Boolean), {
43
- stdio: "inherit",
44
- cwd: PROJECT_ROOT,
45
- shell: true,
46
- });
47
- if (r.status !== 0) {
48
- console.error(errorMessage);
49
- process.exit(r.status ?? 1);
50
- }
51
- }
52
-
53
- async function runAsync(cmd, args) {
54
- return new Promise((resolve, reject) => {
55
- const child = spawn(cmd, args.filter(Boolean), {
56
- stdio: "inherit",
57
- cwd: PROJECT_ROOT,
58
- shell: true,
59
- });
60
- child.on("close", (code) => {
61
- if (code === 0) resolve();
62
- else reject(new Error(`${cmd} exited with code ${code}`));
63
- });
64
- });
65
- }
66
-
67
- const COLORS = {
68
- reset: "\x1b[0m",
69
- green: "\x1b[32m",
70
- gray: "\x1b[90m",
71
- blueTW: "\x1b[38;2;0;107;228m",
72
- };
73
-
74
- function logCheck(message) {
75
- console.log(`${COLORS.green}✔${COLORS.reset} ${message}`);
76
- }
77
-
78
- function startSpinner(message) {
79
- const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
80
- let i = 0;
81
- process.stdout.write(`${frames[0]} ${message}`);
82
- const timer = setInterval(() => {
83
- i = (i + 1) % frames.length;
84
- process.stdout.write(`\r${frames[i]} ${message}`);
85
- }, 80);
86
- return () => {
87
- clearInterval(timer);
88
- process.stdout.write("\r");
89
- };
90
- }
91
-
92
- async function withSpinner(message, fn) {
93
- const stop = startSpinner(message);
94
- try {
95
- await fn();
96
- stop();
97
- logCheck(message);
98
- } catch (err) {
99
- stop();
100
- throw err;
101
- }
102
- }
103
-
104
- async function promptYesNo(question, def = true) {
105
- const rl = readline.createInterface({
106
- input: process.stdin,
107
- output: process.stdout,
108
- });
109
- const suffix = def ? "(Y/n)" : "(y/N)";
110
- const answer = await new Promise((res) =>
111
- rl.question(`${question} ${suffix} `, (ans) => res(ans))
112
- );
113
- rl.close();
114
- const a = String(answer).trim().toLowerCase();
115
- if (!a) return def;
116
- return a.startsWith("y");
117
- }
118
-
119
- function oscHyperlink(text, url) {
120
- return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
121
- }
122
-
123
- function printBannerTRUSTLESSWORK() {
124
- const map = {
125
- T: ["******", " ** ", " ** ", " ** ", " ** "],
126
- R: ["***** ", "** **", "***** ", "** ** ", "** **"],
127
- U: ["** **", "** **", "** **", "** **", " **** "],
128
- S: [" **** ", "** ", " **** ", " **", " **** "],
129
- L: ["** ", "** ", "** ", "** ", "******"],
130
- E: ["******", "** ", "***** ", "** ", "******"],
131
- W: ["** **", "** **", "** * **", "*** ***", "** **"],
132
- O: [" **** ", "** **", "** **", "** **", " **** "],
133
- K: ["** **", "** ** ", "**** ", "** ** ", "** **"],
134
- " ": [" ", " ", " ", " ", " "],
135
- };
136
- const text = "TRUSTLESS WORK";
137
- const rows = ["", "", "", "", ""];
138
- for (const ch of text) {
139
- const glyph = map[ch] || map[" "];
140
- for (let i = 0; i < 5; i++) {
141
- rows[i] += glyph[i] + " ";
142
- }
143
- }
144
- console.log("\n\n");
145
- for (const line of rows) {
146
- console.log(`${COLORS.blueTW}${line}${COLORS.reset}`);
147
- }
148
- }
149
-
150
- function readProjectPackageJson() {
151
- const pkgPath = path.join(PROJECT_ROOT, "package.json");
152
- if (!fs.existsSync(pkgPath)) return null;
153
- try {
154
- return JSON.parse(fs.readFileSync(pkgPath, "utf8"));
155
- } catch {
156
- return null;
157
- }
158
- }
159
-
160
- function installDeps({ dependencies = {}, devDependencies = {} }) {
161
- const pm = detectPM();
162
- const BLOCKED = new Set([
163
- "tailwindcss",
164
- "@tailwindcss/cli",
165
- "@tailwindcss/postcss",
166
- "@tailwindcss/vite",
167
- "postcss",
168
- "autoprefixer",
169
- "postcss-import",
170
- "@creit-tech/stellar-wallets-kit",
171
- ]);
172
- const depList = Object.entries(dependencies)
173
- .filter(([k]) => !BLOCKED.has(k))
174
- .map(([k, v]) => `${k}@${v}`);
175
- const devList = Object.entries(devDependencies)
176
- .filter(([k]) => !BLOCKED.has(k))
177
- .map(([k, v]) => `${k}@${v}`);
178
-
179
- if (depList.length) {
180
- if (pm === "pnpm") run("pnpm", ["add", ...depList]);
181
- else if (pm === "yarn") run("yarn", ["add", ...depList]);
182
- else if (pm === "bun") run("bun", ["add", ...depList]);
183
- else run("npm", ["install", ...depList]);
184
- }
185
-
186
- if (devList.length) {
187
- if (pm === "pnpm") run("pnpm", ["add", "-D", ...devList]);
188
- else if (pm === "yarn") run("yarn", ["add", "-D", ...devList]);
189
- else if (pm === "bun") run("bun", ["add", "-d", ...devList]);
190
- else run("npm", ["install", "-D", ...devList]);
191
- }
192
- }
193
-
194
- function loadConfig() {
195
- const cfgPath = path.join(PROJECT_ROOT, ".twblocks.json");
196
- if (fs.existsSync(cfgPath)) {
197
- try {
198
- return JSON.parse(fs.readFileSync(cfgPath, "utf8"));
199
- } catch (e) {
200
- console.warn("⚠️ Failed to parse .twblocks.json, ignoring.");
201
- }
202
- }
203
- return {};
204
- }
205
-
206
- function parseFlags(argv) {
207
- const flags = {};
208
- for (let i = 0; i < argv.length; i++) {
209
- const a = argv[i];
210
- if (a.startsWith("--ui-base=")) {
211
- flags.uiBase = a.split("=").slice(1).join("=");
212
- } else if (a === "--ui-base") {
213
- flags.uiBase = argv[i + 1];
214
- i++;
215
- } else if (a === "--install" || a === "-i") {
216
- flags.install = true;
217
- }
218
- }
219
- return flags;
220
- }
221
-
222
- function copyTemplate(name, { uiBase, shouldInstall = false } = {}) {
223
- const srcFile = path.join(TEMPLATES_DIR, `${name}.tsx`);
224
- const requestedDir = path.join(TEMPLATES_DIR, name);
225
- let srcDir = null;
226
- if (fs.existsSync(requestedDir) && fs.lstatSync(requestedDir).isDirectory()) {
227
- srcDir = requestedDir;
228
- } else {
229
- // Alias: allow multi-release/approve-milestone to fallback to existing source
230
- if (name.startsWith("escrows/multi-release/approve-milestone")) {
231
- const altMulti = path.join(
232
- TEMPLATES_DIR,
233
- "escrows",
234
- "multi-release",
235
- "approve-milestone"
236
- );
237
- const altSingle = path.join(
238
- TEMPLATES_DIR,
239
- "escrows",
240
- "single-release",
241
- "approve-milestone"
242
- );
243
- if (fs.existsSync(altMulti) && fs.lstatSync(altMulti).isDirectory()) {
244
- srcDir = altMulti;
245
- } else if (
246
- fs.existsSync(altSingle) &&
247
- fs.lstatSync(altSingle).isDirectory()
248
- ) {
249
- srcDir = altSingle;
250
- }
251
- }
252
- }
253
- const componentsSrcDir = path.join(PROJECT_ROOT, "src", "components");
254
- const outRoot = fs.existsSync(componentsSrcDir)
255
- ? path.join(componentsSrcDir, "tw-blocks")
256
- : path.join(PROJECT_ROOT, "components", "tw-blocks");
257
-
258
- const config = loadConfig();
259
- const effectiveUiBase = uiBase || config.uiBase || "@/components/ui";
260
- let currentEscrowType = null;
261
-
262
- function writeTransformed(srcPath, destPath) {
263
- const raw = fs.readFileSync(srcPath, "utf8");
264
- let transformed = raw.replaceAll("__UI_BASE__", effectiveUiBase);
265
- // Resolve details placeholders to either multi-release modules (if present) or local compat
266
- const applyDetailsPlaceholders = (content) => {
267
- const resolveImport = (segments, compatFile) => {
268
- const realWithExt = path.join(
269
- outRoot,
270
- "escrows",
271
- "multi-release",
272
- ...segments
273
- );
274
- const realCandidate = [
275
- realWithExt,
276
- realWithExt + ".tsx",
277
- realWithExt + ".ts",
278
- realWithExt + ".jsx",
279
- realWithExt + ".js",
280
- ].find((p) => fs.existsSync(p));
281
- const realNoExt = realCandidate
282
- ? realCandidate.replace(/\.(tsx|ts|jsx|js)$/i, "")
283
- : null;
284
- const compatWithExt = path.join(
285
- path.dirname(destPath),
286
- "compat",
287
- compatFile
288
- );
289
- const compatCandidate = [
290
- compatWithExt,
291
- compatWithExt + ".tsx",
292
- compatWithExt + ".ts",
293
- compatWithExt + ".jsx",
294
- compatWithExt + ".js",
295
- ].find((p) => fs.existsSync(p));
296
- const compatNoExt = (compatCandidate || compatWithExt).replace(
297
- /\.(tsx|ts|jsx|js)$/i,
298
- ""
299
- );
300
- const target = realNoExt || compatNoExt;
301
- let rel = path.relative(path.dirname(destPath), target);
302
- rel = rel.split(path.sep).join("/");
303
- if (!rel.startsWith(".")) rel = "./" + rel;
304
- return rel;
305
- };
306
- return content
307
- .replaceAll(
308
- "__MR_RELEASE_MODULE__",
309
- resolveImport(
310
- ["release-escrow", "button", "ReleaseEscrow"],
311
- "ReleaseEscrow"
312
- )
313
- )
314
- .replaceAll(
315
- "__MR_DISPUTE_MODULE__",
316
- resolveImport(
317
- ["dispute-escrow", "button", "DisputeEscrow"],
318
- "DisputeEscrow"
319
- )
320
- )
321
- .replaceAll(
322
- "__MR_RESOLVE_MODULE__",
323
- resolveImport(
324
- ["resolve-dispute", "dialog", "ResolveDispute"],
325
- "ResolveDispute"
326
- )
327
- );
328
- };
329
- transformed = applyDetailsPlaceholders(transformed);
330
- if (currentEscrowType) {
331
- transformed = transformed.replaceAll(
332
- "__ESCROW_TYPE__",
333
- currentEscrowType
334
- );
335
- }
336
- fs.mkdirSync(path.dirname(destPath), { recursive: true });
337
- fs.writeFileSync(destPath, transformed, "utf8");
338
- console.log(`✅ ${path.relative(PROJECT_ROOT, destPath)} created`);
339
- }
340
-
341
- // Generic: materialize any module from templates/escrows/shared/<module>
342
- if (!srcDir) {
343
- const m = name.match(
344
- /^escrows\/(single-release|multi-release)\/([^\/]+)(?:\/(button|dialog|form))?$/
345
- );
346
- if (m) {
347
- const releaseType = m[1];
348
- const moduleName = m[2];
349
- const variant = m[3] || null;
350
-
351
- const sharedModuleDir = path.join(
352
- TEMPLATES_DIR,
353
- "escrows",
354
- "shared",
355
- moduleName
356
- );
357
-
358
- if (
359
- fs.existsSync(sharedModuleDir) &&
360
- fs.lstatSync(sharedModuleDir).isDirectory()
361
- ) {
362
- currentEscrowType = releaseType;
363
- const destBase = path.join(outRoot, "escrows", releaseType, moduleName);
364
-
365
- function copyModuleRootFilesInto(targetDir) {
366
- const entries = fs.readdirSync(sharedModuleDir, {
367
- withFileTypes: true,
368
- });
369
- for (const entry of entries) {
370
- if (entry.isDirectory()) continue;
371
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
372
- const entrySrc = path.join(sharedModuleDir, entry.name);
373
- const entryDest = path.join(targetDir, entry.name);
374
- writeTransformed(entrySrc, entryDest);
375
- }
376
- }
377
-
378
- function copyVariant(variantName) {
379
- const variantSrc = path.join(sharedModuleDir, variantName);
380
- const variantDest = path.join(destBase, variantName);
381
- fs.mkdirSync(variantDest, { recursive: true });
382
- if (
383
- fs.existsSync(variantSrc) &&
384
- fs.lstatSync(variantSrc).isDirectory()
385
- ) {
386
- const stack = [""];
387
- while (stack.length) {
388
- const rel = stack.pop();
389
- const current = path.join(variantSrc, rel);
390
- const entries = fs.readdirSync(current, { withFileTypes: true });
391
- for (const entry of entries) {
392
- const entryRel = path.join(rel, entry.name);
393
- const entrySrc = path.join(variantSrc, entryRel);
394
- const entryDest = path.join(variantDest, entryRel);
395
- if (entry.isDirectory()) {
396
- stack.push(entryRel);
397
- continue;
398
- }
399
- if (/\.(tsx?|jsx?)$/i.test(entry.name)) {
400
- writeTransformed(entrySrc, entryDest);
401
- } else {
402
- fs.mkdirSync(path.dirname(entryDest), { recursive: true });
403
- fs.copyFileSync(entrySrc, entryDest);
404
- console.log(
405
- `✅ ${path.relative(PROJECT_ROOT, entryDest)} created`
406
- );
407
- }
408
- }
409
- }
410
- }
411
- // Always place module-level shared files into the variant directory
412
- copyModuleRootFilesInto(variantDest);
413
- }
414
-
415
- if (variant) {
416
- copyVariant(variant);
417
- } else {
418
- const variants = ["button", "dialog", "form"];
419
- for (const v of variants) copyVariant(v);
420
- }
421
-
422
- if (shouldInstall && fs.existsSync(GLOBAL_DEPS_FILE)) {
423
- const meta = JSON.parse(fs.readFileSync(GLOBAL_DEPS_FILE, "utf8"));
424
- installDeps(meta);
425
- // Install @creit-tech/stellar-wallets-kit using jsr after all other deps are installed
426
- if (meta.dependencies && meta.dependencies["@creit-tech/stellar-wallets-kit"]) {
427
- run("npx", ["jsr", "add", "@creit-tech/stellar-wallets-kit"]);
428
- }
429
- }
430
- currentEscrowType = null;
431
- return;
432
- }
433
- }
434
- }
435
-
436
- if (fs.existsSync(srcDir) && fs.lstatSync(srcDir).isDirectory()) {
437
- const skipDetails =
438
- name === "escrows/escrows-by-role" ||
439
- name === "escrows/escrows-by-signer" ||
440
- name === "escrows";
441
- // Copy directory recursively
442
- const destDir = path.join(outRoot, name);
443
- fs.mkdirSync(destDir, { recursive: true });
444
- const stack = [""];
445
- while (stack.length) {
446
- const rel = stack.pop();
447
- const current = path.join(srcDir, rel);
448
- const entries = fs.readdirSync(current, { withFileTypes: true });
449
- for (const entry of entries) {
450
- const entryRel = path.join(rel, entry.name);
451
- // Skip copying any shared directory at any depth
452
- const parts = entryRel.split(path.sep);
453
- if (parts.includes("shared")) {
454
- continue;
455
- }
456
- if (skipDetails) {
457
- const top = parts[0] || "";
458
- const firstTwo = parts.slice(0, 2).join(path.sep);
459
- if (
460
- top === "details" ||
461
- firstTwo === path.join("escrows-by-role", "details") ||
462
- firstTwo === path.join("escrows-by-signer", "details")
463
- ) {
464
- continue;
465
- }
466
- }
467
- const entrySrc = path.join(srcDir, entryRel);
468
- const entryDest = path.join(destDir, entryRel);
469
- if (entry.isDirectory()) {
470
- stack.push(entryRel);
471
- continue;
472
- }
473
- // Only process text files (.ts, .tsx, .js, .jsx)
474
- if (/\.(tsx?|jsx?)$/i.test(entry.name)) {
475
- writeTransformed(entrySrc, entryDest);
476
- } else {
477
- fs.mkdirSync(path.dirname(entryDest), { recursive: true });
478
- fs.copyFileSync(entrySrc, entryDest);
479
- console.log(`✅ ${path.relative(PROJECT_ROOT, entryDest)} created`);
480
- }
481
- }
482
- }
483
-
484
- // Post-copy: materialize shared initialize-escrow files into dialog/form
485
- try {
486
- const isSingleReleaseInitRoot =
487
- name === "escrows/single-release/initialize-escrow";
488
- const isSingleReleaseInitDialog =
489
- name === "escrows/single-release/initialize-escrow/dialog";
490
- const isSingleReleaseInitForm =
491
- name === "escrows/single-release/initialize-escrow/form";
492
-
493
- const srcSharedDir = path.join(
494
- TEMPLATES_DIR,
495
- "escrows",
496
- "single-release",
497
- "initialize-escrow",
498
- "shared"
499
- );
500
-
501
- function copySharedInto(targetDir) {
502
- if (!fs.existsSync(srcSharedDir)) return;
503
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
504
- for (const entry of entries) {
505
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
506
- const entrySrc = path.join(srcSharedDir, entry.name);
507
- const entryDest = path.join(targetDir, entry.name);
508
- writeTransformed(entrySrc, entryDest);
509
- }
510
- }
511
-
512
- if (isSingleReleaseInitRoot) {
513
- copySharedInto(path.join(destDir, "dialog"));
514
- copySharedInto(path.join(destDir, "form"));
515
- } else if (isSingleReleaseInitDialog) {
516
- copySharedInto(destDir);
517
- } else if (isSingleReleaseInitForm) {
518
- copySharedInto(destDir);
519
- }
520
- } catch (e) {
521
- console.warn(
522
- "⚠️ Failed to materialize shared initialize-escrow files:",
523
- e?.message || e
524
- );
525
- }
526
-
527
- try {
528
- const isSRRoot = name === "escrows/single-release/approve-milestone";
529
- const isSRDialog =
530
- name === "escrows/single-release/approve-milestone/dialog";
531
- const isSRForm = name === "escrows/single-release/approve-milestone/form";
532
-
533
- const isMRRoot = name === "escrows/multi-release/approve-milestone";
534
- const isMRDialog =
535
- name === "escrows/multi-release/approve-milestone/dialog";
536
- const isMRForm = name === "escrows/multi-release/approve-milestone/form";
537
-
538
- const srcSharedDir = path.join(
539
- TEMPLATES_DIR,
540
- "escrows",
541
- "shared",
542
- "approve-milestone",
543
- "shared"
544
- );
545
-
546
- function copySharedInto(targetDir) {
547
- if (!fs.existsSync(srcSharedDir)) return;
548
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
549
- for (const entry of entries) {
550
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
551
- const entrySrc = path.join(srcSharedDir, entry.name);
552
- const entryDest = path.join(targetDir, entry.name);
553
- writeTransformed(entrySrc, entryDest);
554
- }
555
- }
556
-
557
- if (isSRRoot || isMRRoot) {
558
- copySharedInto(path.join(destDir, "dialog"));
559
- copySharedInto(path.join(destDir, "form"));
560
- } else if (isSRDialog || isMRDialog) {
561
- copySharedInto(destDir);
562
- } else if (isSRForm || isMRForm) {
563
- copySharedInto(destDir);
564
- }
565
- } catch (e) {
566
- console.warn(
567
- "⚠️ Failed to materialize shared approve-milestone files:",
568
- e?.message || e
569
- );
570
- }
571
-
572
- try {
573
- const isSingleReleaseInitRoot =
574
- name === "escrows/single-release/change-milestone-status";
575
- const isSingleReleaseInitDialog =
576
- name === "escrows/single-release/change-milestone-status/dialog";
577
- const isSingleReleaseInitForm =
578
- name === "escrows/single-release/change-milestone-status/form";
579
-
580
- const srcSharedDir = path.join(
581
- TEMPLATES_DIR,
582
- "escrows",
583
- "single-release",
584
- "change-milestone-status",
585
- "shared"
586
- );
587
-
588
- function copySharedInto(targetDir) {
589
- if (!fs.existsSync(srcSharedDir)) return;
590
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
591
- for (const entry of entries) {
592
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
593
- const entrySrc = path.join(srcSharedDir, entry.name);
594
- const entryDest = path.join(targetDir, entry.name);
595
- writeTransformed(entrySrc, entryDest);
596
- }
597
- }
598
-
599
- if (isSingleReleaseInitRoot) {
600
- copySharedInto(path.join(destDir, "dialog"));
601
- copySharedInto(path.join(destDir, "form"));
602
- } else if (isSingleReleaseInitDialog) {
603
- copySharedInto(destDir);
604
- } else if (isSingleReleaseInitForm) {
605
- copySharedInto(destDir);
606
- }
607
- } catch (e) {
608
- console.warn(
609
- "⚠️ Failed to materialize shared change-milestone-status files:",
610
- e?.message || e
611
- );
612
- }
613
-
614
- try {
615
- const isSingleReleaseInitRoot =
616
- name === "escrows/single-release/fund-escrow";
617
- const isSingleReleaseInitDialog =
618
- name === "escrows/single-release/fund-escrow/dialog";
619
- const isSingleReleaseInitForm =
620
- name === "escrows/single-release/fund-escrow/form";
621
-
622
- const srcSharedDir = path.join(
623
- TEMPLATES_DIR,
624
- "escrows",
625
- "single-release",
626
- "fund-escrow",
627
- "shared"
628
- );
629
-
630
- function copySharedInto(targetDir) {
631
- if (!fs.existsSync(srcSharedDir)) return;
632
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
633
- for (const entry of entries) {
634
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
635
- const entrySrc = path.join(srcSharedDir, entry.name);
636
- const entryDest = path.join(targetDir, entry.name);
637
- writeTransformed(entrySrc, entryDest);
638
- }
639
- }
640
-
641
- if (isSingleReleaseInitRoot) {
642
- copySharedInto(path.join(destDir, "dialog"));
643
- copySharedInto(path.join(destDir, "form"));
644
- } else if (isSingleReleaseInitDialog) {
645
- copySharedInto(destDir);
646
- } else if (isSingleReleaseInitForm) {
647
- copySharedInto(destDir);
648
- }
649
- } catch (e) {
650
- console.warn(
651
- "⚠️ Failed to materialize shared fund-escrow files:",
652
- e?.message || e
653
- );
654
- }
655
-
656
- try {
657
- const isSingleReleaseInitRoot =
658
- name === "escrows/single-release/resolve-dispute";
659
- const isSingleReleaseInitDialog =
660
- name === "escrows/single-release/resolve-dispute/dialog";
661
- const isSingleReleaseInitForm =
662
- name === "escrows/single-release/resolve-dispute/form";
663
-
664
- const srcSharedDir = path.join(
665
- TEMPLATES_DIR,
666
- "escrows",
667
- "single-release",
668
- "resolve-dispute",
669
- "shared"
670
- );
671
-
672
- function copySharedInto(targetDir) {
673
- if (!fs.existsSync(srcSharedDir)) return;
674
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
675
- for (const entry of entries) {
676
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
677
- const entrySrc = path.join(srcSharedDir, entry.name);
678
- const entryDest = path.join(targetDir, entry.name);
679
- writeTransformed(entrySrc, entryDest);
680
- }
681
- }
682
-
683
- if (isSingleReleaseInitRoot) {
684
- copySharedInto(path.join(destDir, "dialog"));
685
- copySharedInto(path.join(destDir, "form"));
686
- } else if (isSingleReleaseInitDialog) {
687
- copySharedInto(destDir);
688
- } else if (isSingleReleaseInitForm) {
689
- copySharedInto(destDir);
690
- }
691
- } catch (e) {
692
- console.warn(
693
- "⚠️ Failed to materialize shared resolve-dispute files:",
694
- e?.message || e
695
- );
696
- }
697
-
698
- try {
699
- const isSingleReleaseInitRoot =
700
- name === "escrows/single-release/update-escrow";
701
- const isSingleReleaseInitDialog =
702
- name === "escrows/single-release/update-escrow/dialog";
703
- const isSingleReleaseInitForm =
704
- name === "escrows/single-release/update-escrow/form";
705
-
706
- const srcSharedDir = path.join(
707
- TEMPLATES_DIR,
708
- "escrows",
709
- "single-release",
710
- "update-escrow",
711
- "shared"
712
- );
713
-
714
- function copySharedInto(targetDir) {
715
- if (!fs.existsSync(srcSharedDir)) return;
716
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
717
- for (const entry of entries) {
718
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
719
- const entrySrc = path.join(srcSharedDir, entry.name);
720
- const entryDest = path.join(targetDir, entry.name);
721
- writeTransformed(entrySrc, entryDest);
722
- }
723
- }
724
-
725
- if (isSingleReleaseInitRoot) {
726
- copySharedInto(path.join(destDir, "dialog"));
727
- copySharedInto(path.join(destDir, "form"));
728
- } else if (isSingleReleaseInitDialog) {
729
- copySharedInto(destDir);
730
- } else if (isSingleReleaseInitForm) {
731
- copySharedInto(destDir);
732
- }
733
- } catch (e) {
734
- console.warn(
735
- "⚠️ Failed to materialize shared update-escrow files:",
736
- e?.message || e
737
- );
738
- }
739
-
740
- // Post-copy: materialize shared files for multi-release modules
741
- try {
742
- const isMultiInitRoot =
743
- name === "escrows/multi-release/initialize-escrow";
744
- const isMultiInitDialog =
745
- name === "escrows/multi-release/initialize-escrow/dialog";
746
- const isMultiInitForm =
747
- name === "escrows/multi-release/initialize-escrow/form";
748
-
749
- const srcSharedDir = path.join(
750
- TEMPLATES_DIR,
751
- "escrows",
752
- "multi-release",
753
- "initialize-escrow",
754
- "shared"
755
- );
756
-
757
- function copyMultiInitSharedInto(targetDir) {
758
- if (!fs.existsSync(srcSharedDir)) return;
759
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
760
- for (const entry of entries) {
761
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
762
- const entrySrc = path.join(srcSharedDir, entry.name);
763
- const entryDest = path.join(targetDir, entry.name);
764
- writeTransformed(entrySrc, entryDest);
765
- }
766
- }
767
-
768
- if (isMultiInitRoot) {
769
- copyMultiInitSharedInto(path.join(destDir, "dialog"));
770
- copyMultiInitSharedInto(path.join(destDir, "form"));
771
- } else if (isMultiInitDialog) {
772
- copyMultiInitSharedInto(destDir);
773
- } else if (isMultiInitForm) {
774
- copyMultiInitSharedInto(destDir);
775
- }
776
- } catch (e) {
777
- console.warn(
778
- "⚠️ Failed to materialize shared multi-release initialize-escrow files:",
779
- e?.message || e
780
- );
781
- }
782
-
783
- try {
784
- const isMultiResolveRoot =
785
- name === "escrows/multi-release/resolve-dispute";
786
- const isMultiResolveDialog =
787
- name === "escrows/multi-release/resolve-dispute/dialog";
788
- const isMultiResolveForm =
789
- name === "escrows/multi-release/resolve-dispute/form";
790
-
791
- const srcSharedDir = path.join(
792
- TEMPLATES_DIR,
793
- "escrows",
794
- "multi-release",
795
- "resolve-dispute",
796
- "shared"
797
- );
798
-
799
- function copyMultiResolveSharedInto(targetDir) {
800
- if (!fs.existsSync(srcSharedDir)) return;
801
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
802
- for (const entry of entries) {
803
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
804
- const entrySrc = path.join(srcSharedDir, entry.name);
805
- const entryDest = path.join(targetDir, entry.name);
806
- writeTransformed(entrySrc, entryDest);
807
- }
808
- }
809
-
810
- if (isMultiResolveRoot) {
811
- copyMultiResolveSharedInto(path.join(destDir, "dialog"));
812
- copyMultiResolveSharedInto(path.join(destDir, "form"));
813
- } else if (isMultiResolveDialog) {
814
- copyMultiResolveSharedInto(destDir);
815
- } else if (isMultiResolveForm) {
816
- copyMultiResolveSharedInto(destDir);
817
- }
818
- } catch (e) {
819
- console.warn(
820
- "⚠️ Failed to materialize shared multi-release resolve-dispute files:",
821
- e?.message || e
822
- );
823
- }
824
-
825
- // Post-copy: materialize shared files for multi-release withdraw-remaining-funds
826
- try {
827
- const isMultiWithdrawRoot =
828
- name === "escrows/multi-release/withdraw-remaining-funds";
829
- const isMultiWithdrawDialog =
830
- name === "escrows/multi-release/withdraw-remaining-funds/dialog";
831
- const isMultiWithdrawForm =
832
- name === "escrows/multi-release/withdraw-remaining-funds/form";
833
-
834
- const srcSharedDir = path.join(
835
- TEMPLATES_DIR,
836
- "escrows",
837
- "multi-release",
838
- "withdraw-remaining-funds",
839
- "shared"
840
- );
841
-
842
- function copyMultiWithdrawSharedInto(targetDir) {
843
- if (!fs.existsSync(srcSharedDir)) return;
844
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
845
- for (const entry of entries) {
846
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
847
- const entrySrc = path.join(srcSharedDir, entry.name);
848
- const entryDest = path.join(targetDir, entry.name);
849
- writeTransformed(entrySrc, entryDest);
850
- }
851
- }
852
-
853
- if (isMultiWithdrawRoot) {
854
- copyMultiWithdrawSharedInto(path.join(destDir, "dialog"));
855
- copyMultiWithdrawSharedInto(path.join(destDir, "form"));
856
- } else if (isMultiWithdrawDialog) {
857
- copyMultiWithdrawSharedInto(destDir);
858
- } else if (isMultiWithdrawForm) {
859
- copyMultiWithdrawSharedInto(destDir);
860
- }
861
- } catch (e) {
862
- console.warn(
863
- "⚠️ Failed to materialize shared multi-release withdraw-remaining-funds files:",
864
- e?.message || e
865
- );
866
- }
867
-
868
- try {
869
- const isMultiUpdateRoot = name === "escrows/multi-release/update-escrow";
870
- const isMultiUpdateDialog =
871
- name === "escrows/multi-release/update-escrow/dialog";
872
- const isMultiUpdateForm =
873
- name === "escrows/multi-release/update-escrow/form";
874
-
875
- const srcSharedDir = path.join(
876
- TEMPLATES_DIR,
877
- "escrows",
878
- "multi-release",
879
- "update-escrow",
880
- "shared"
881
- );
882
-
883
- function copyMultiUpdateSharedInto(targetDir) {
884
- if (!fs.existsSync(srcSharedDir)) return;
885
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
886
- for (const entry of entries) {
887
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
888
- const entrySrc = path.join(srcSharedDir, entry.name);
889
- const entryDest = path.join(targetDir, entry.name);
890
- writeTransformed(entrySrc, entryDest);
891
- }
892
- }
893
-
894
- if (isMultiUpdateRoot) {
895
- copyMultiUpdateSharedInto(path.join(destDir, "dialog"));
896
- copyMultiUpdateSharedInto(path.join(destDir, "form"));
897
- } else if (isMultiUpdateDialog) {
898
- copyMultiUpdateSharedInto(destDir);
899
- } else if (isMultiUpdateForm) {
900
- copyMultiUpdateSharedInto(destDir);
901
- }
902
- } catch (e) {
903
- console.warn(
904
- "⚠️ Failed to materialize shared multi-release update-escrow files:",
905
- e?.message || e
906
- );
907
- }
908
-
909
- // Post-copy: materialize shared files for single-multi-release modules
910
- try {
911
- const isSingleMultiApproveRoot =
912
- name === "escrows/single-multi-release/approve-milestone";
913
- const isSingleMultiApproveDialog =
914
- name === "escrows/single-multi-release/approve-milestone/dialog";
915
- const isSingleMultiApproveForm =
916
- name === "escrows/single-multi-release/approve-milestone/form";
917
-
918
- const srcSharedDir = path.join(
919
- TEMPLATES_DIR,
920
- "escrows",
921
- "single-multi-release",
922
- "approve-milestone",
923
- "shared"
924
- );
925
-
926
- function copySingleMultiApproveSharedInto(targetDir) {
927
- if (!fs.existsSync(srcSharedDir)) return;
928
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
929
- for (const entry of entries) {
930
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
931
- const entrySrc = path.join(srcSharedDir, entry.name);
932
- const entryDest = path.join(targetDir, entry.name);
933
- writeTransformed(entrySrc, entryDest);
934
- }
935
- }
936
-
937
- if (isSingleMultiApproveRoot) {
938
- copySingleMultiApproveSharedInto(path.join(destDir, "dialog"));
939
- copySingleMultiApproveSharedInto(path.join(destDir, "form"));
940
- } else if (isSingleMultiApproveDialog) {
941
- copySingleMultiApproveSharedInto(destDir);
942
- } else if (isSingleMultiApproveForm) {
943
- copySingleMultiApproveSharedInto(destDir);
944
- }
945
- } catch (e) {
946
- console.warn(
947
- "⚠️ Failed to materialize shared single-multi-release approve-milestone files:",
948
- e?.message || e
949
- );
950
- }
951
-
952
- try {
953
- const isSingleMultiChangeRoot =
954
- name === "escrows/single-multi-release/change-milestone-status";
955
- const isSingleMultiChangeDialog =
956
- name === "escrows/single-multi-release/change-milestone-status/dialog";
957
- const isSingleMultiChangeForm =
958
- name === "escrows/single-multi-release/change-milestone-status/form";
959
-
960
- const srcSharedDir = path.join(
961
- TEMPLATES_DIR,
962
- "escrows",
963
- "single-multi-release",
964
- "change-milestone-status",
965
- "shared"
966
- );
967
-
968
- function copySingleMultiChangeSharedInto(targetDir) {
969
- if (!fs.existsSync(srcSharedDir)) return;
970
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
971
- for (const entry of entries) {
972
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
973
- const entrySrc = path.join(srcSharedDir, entry.name);
974
- const entryDest = path.join(targetDir, entry.name);
975
- writeTransformed(entrySrc, entryDest);
976
- }
977
- }
978
-
979
- if (isSingleMultiChangeRoot) {
980
- copySingleMultiChangeSharedInto(path.join(destDir, "dialog"));
981
- copySingleMultiChangeSharedInto(path.join(destDir, "form"));
982
- } else if (isSingleMultiChangeDialog) {
983
- copySingleMultiChangeSharedInto(destDir);
984
- } else if (isSingleMultiChangeForm) {
985
- copySingleMultiChangeSharedInto(destDir);
986
- }
987
- } catch (e) {
988
- console.warn(
989
- "⚠️ Failed to materialize shared single-multi-release change-milestone-status files:",
990
- e?.message || e
991
- );
992
- }
993
-
994
- try {
995
- const isSingleMultiFundRoot =
996
- name === "escrows/single-multi-release/fund-escrow";
997
- const isSingleMultiFundDialog =
998
- name === "escrows/single-multi-release/fund-escrow/dialog";
999
- const isSingleMultiFundForm =
1000
- name === "escrows/single-multi-release/fund-escrow/form";
1001
-
1002
- const srcSharedDir = path.join(
1003
- TEMPLATES_DIR,
1004
- "escrows",
1005
- "single-multi-release",
1006
- "fund-escrow",
1007
- "shared"
1008
- );
1009
-
1010
- function copySingleMultiFundSharedInto(targetDir) {
1011
- if (!fs.existsSync(srcSharedDir)) return;
1012
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
1013
- for (const entry of entries) {
1014
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
1015
- const entrySrc = path.join(srcSharedDir, entry.name);
1016
- const entryDest = path.join(targetDir, entry.name);
1017
- writeTransformed(entrySrc, entryDest);
1018
- }
1019
- }
1020
-
1021
- if (isSingleMultiFundRoot) {
1022
- copySingleMultiFundSharedInto(path.join(destDir, "dialog"));
1023
- copySingleMultiFundSharedInto(path.join(destDir, "form"));
1024
- } else if (isSingleMultiFundDialog) {
1025
- copySingleMultiFundSharedInto(destDir);
1026
- } else if (isSingleMultiFundForm) {
1027
- copySingleMultiFundSharedInto(destDir);
1028
- }
1029
- } catch (e) {
1030
- console.warn(
1031
- "⚠️ Failed to materialize shared single-multi-release fund-escrow files:",
1032
- e?.message || e
1033
- );
1034
- }
1035
-
1036
- // If adding the whole single-release bundle, materialize all shared files
1037
- try {
1038
- if (name === "escrows/single-release") {
1039
- const modules = [
1040
- "initialize-escrow",
1041
- "approve-milestone",
1042
- "change-milestone-status",
1043
- "fund-escrow",
1044
- "resolve-dispute",
1045
- "update-escrow",
1046
- ];
1047
-
1048
- for (const mod of modules) {
1049
- const srcSharedDir = path.join(
1050
- TEMPLATES_DIR,
1051
- "escrows",
1052
- mod === "approve-milestone" ? "shared" : "single-release",
1053
- mod === "approve-milestone" ? "approve-milestone" : mod,
1054
- "shared"
1055
- );
1056
- if (!fs.existsSync(srcSharedDir)) continue;
1057
-
1058
- const targets = [
1059
- path.join(destDir, mod, "dialog"),
1060
- path.join(destDir, mod, "form"),
1061
- ];
1062
-
1063
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
1064
- for (const entry of entries) {
1065
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
1066
- const entrySrc = path.join(srcSharedDir, entry.name);
1067
- for (const t of targets) {
1068
- const entryDest = path.join(t, entry.name);
1069
- writeTransformed(entrySrc, entryDest);
1070
- }
1071
- }
1072
- }
1073
- }
1074
- } catch (e) {
1075
- console.warn(
1076
- "⚠️ Failed to materialize shared files for single-release bundle:",
1077
- e?.message || e
1078
- );
1079
- }
1080
-
1081
- // If adding the whole multi-release bundle, materialize all shared files
1082
- try {
1083
- if (name === "escrows/multi-release") {
1084
- const modules = [
1085
- "initialize-escrow",
1086
- "resolve-dispute",
1087
- "update-escrow",
1088
- "withdraw-remaining-funds",
1089
- ];
1090
-
1091
- for (const mod of modules) {
1092
- const srcSharedDir = path.join(
1093
- TEMPLATES_DIR,
1094
- "escrows",
1095
- "multi-release",
1096
- mod,
1097
- "shared"
1098
- );
1099
- if (!fs.existsSync(srcSharedDir)) continue;
1100
-
1101
- const targets = [
1102
- path.join(destDir, mod, "dialog"),
1103
- path.join(destDir, mod, "form"),
1104
- ];
1105
-
1106
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
1107
- for (const entry of entries) {
1108
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
1109
- const entrySrc = path.join(srcSharedDir, entry.name);
1110
- for (const t of targets) {
1111
- const entryDest = path.join(t, entry.name);
1112
- writeTransformed(entrySrc, entryDest);
1113
- }
1114
- }
1115
- }
1116
- }
1117
- } catch (e) {
1118
- console.warn(
1119
- "⚠️ Failed to materialize shared files for multi-release bundle:",
1120
- e?.message || e
1121
- );
1122
- }
1123
-
1124
- // If adding the whole single-multi-release bundle, materialize all shared files
1125
- try {
1126
- if (name === "escrows/single-multi-release") {
1127
- const modules = [
1128
- "approve-milestone",
1129
- "change-milestone-status",
1130
- "fund-escrow",
1131
- ];
1132
-
1133
- for (const mod of modules) {
1134
- const srcSharedDir = path.join(
1135
- TEMPLATES_DIR,
1136
- "escrows",
1137
- "single-multi-release",
1138
- mod,
1139
- "shared"
1140
- );
1141
- if (!fs.existsSync(srcSharedDir)) continue;
1142
-
1143
- const targets = [
1144
- path.join(destDir, mod, "dialog"),
1145
- path.join(destDir, mod, "form"),
1146
- ];
1147
-
1148
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
1149
- for (const entry of entries) {
1150
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
1151
- const entrySrc = path.join(srcSharedDir, entry.name);
1152
- for (const t of targets) {
1153
- const entryDest = path.join(t, entry.name);
1154
- writeTransformed(entrySrc, entryDest);
1155
- }
1156
- }
1157
- }
1158
- }
1159
- } catch (e) {
1160
- console.warn(
1161
- "⚠️ Failed to materialize shared files for single-multi-release bundle:",
1162
- e?.message || e
1163
- );
1164
- }
1165
-
1166
- // If adding the root escrows bundle, also materialize single-release shared files
1167
- try {
1168
- if (name === "escrows") {
1169
- const modules = [
1170
- "initialize-escrow",
1171
- "approve-milestone",
1172
- "change-milestone-status",
1173
- "fund-escrow",
1174
- "resolve-dispute",
1175
- "update-escrow",
1176
- ];
1177
-
1178
- const baseTarget = path.join(destDir, "single-release");
1179
- for (const mod of modules) {
1180
- const srcSharedDir = path.join(
1181
- TEMPLATES_DIR,
1182
- "escrows",
1183
- mod === "approve-milestone" ? "shared" : "single-release",
1184
- mod === "approve-milestone" ? "approve-milestone" : mod,
1185
- "shared"
1186
- );
1187
- if (!fs.existsSync(srcSharedDir)) continue;
1188
-
1189
- const targets = [
1190
- path.join(baseTarget, mod, "dialog"),
1191
- path.join(baseTarget, mod, "form"),
1192
- ];
1193
-
1194
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
1195
- for (const entry of entries) {
1196
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
1197
- const entrySrc = path.join(srcSharedDir, entry.name);
1198
- for (const t of targets) {
1199
- const entryDest = path.join(t, entry.name);
1200
- writeTransformed(entrySrc, entryDest);
1201
- }
1202
- }
1203
- }
1204
- }
1205
- } catch (e) {
1206
- console.warn(
1207
- "⚠️ Failed to materialize shared files for escrows root:",
1208
- e?.message || e
1209
- );
1210
- }
1211
-
1212
- // If adding the root escrows bundle, also materialize multi-release shared files
1213
- try {
1214
- if (name === "escrows") {
1215
- const modules = [
1216
- "initialize-escrow",
1217
- "resolve-dispute",
1218
- "update-escrow",
1219
- "withdraw-remaining-funds",
1220
- ];
1221
-
1222
- const baseTarget = path.join(destDir, "multi-release");
1223
- for (const mod of modules) {
1224
- const srcSharedDir = path.join(
1225
- TEMPLATES_DIR,
1226
- "escrows",
1227
- "multi-release",
1228
- mod,
1229
- "shared"
1230
- );
1231
- if (!fs.existsSync(srcSharedDir)) continue;
1232
-
1233
- const targets = [
1234
- path.join(baseTarget, mod, "dialog"),
1235
- path.join(baseTarget, mod, "form"),
1236
- ];
1237
-
1238
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
1239
- for (const entry of entries) {
1240
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
1241
- const entrySrc = path.join(srcSharedDir, entry.name);
1242
- for (const t of targets) {
1243
- const entryDest = path.join(t, entry.name);
1244
- writeTransformed(entrySrc, entryDest);
1245
- }
1246
- }
1247
- }
1248
- }
1249
- } catch (e) {
1250
- console.warn(
1251
- "⚠️ Failed to materialize shared files for escrows root (multi-release):",
1252
- e?.message || e
1253
- );
1254
- }
1255
-
1256
- // If adding the root escrows bundle, also materialize single-multi-release shared files
1257
- try {
1258
- if (name === "escrows") {
1259
- const modules = [
1260
- "approve-milestone",
1261
- "change-milestone-status",
1262
- "fund-escrow",
1263
- ];
1264
-
1265
- const baseTarget = path.join(destDir, "single-multi-release");
1266
- for (const mod of modules) {
1267
- const srcSharedDir = path.join(
1268
- TEMPLATES_DIR,
1269
- "escrows",
1270
- "single-multi-release",
1271
- mod,
1272
- "shared"
1273
- );
1274
- if (!fs.existsSync(srcSharedDir)) continue;
1275
-
1276
- const targets = [
1277
- path.join(baseTarget, mod, "dialog"),
1278
- path.join(baseTarget, mod, "form"),
1279
- ];
1280
-
1281
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
1282
- for (const entry of entries) {
1283
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
1284
- const entrySrc = path.join(srcSharedDir, entry.name);
1285
- for (const t of targets) {
1286
- const entryDest = path.join(t, entry.name);
1287
- writeTransformed(entrySrc, entryDest);
1288
- }
1289
- }
1290
- }
1291
- }
1292
- } catch (e) {
1293
- console.warn(
1294
- "⚠️ Failed to materialize shared files for escrows root (single-multi-release):",
1295
- e?.message || e
1296
- );
1297
- }
1298
- } else if (fs.existsSync(srcFile)) {
1299
- fs.mkdirSync(outRoot, { recursive: true });
1300
- const destFile = path.join(outRoot, name + ".tsx");
1301
- writeTransformed(srcFile, destFile);
1302
- } else {
1303
- console.error(`❌ The template "${name}" does not exist`);
1304
- process.exit(1);
1305
- }
1306
-
1307
- if (shouldInstall && fs.existsSync(GLOBAL_DEPS_FILE)) {
1308
- const meta = JSON.parse(fs.readFileSync(GLOBAL_DEPS_FILE, "utf8"));
1309
- installDeps(meta);
1310
- // Install @creit-tech/stellar-wallets-kit using jsr after all other deps are installed
1311
- if (meta.dependencies && meta.dependencies["@creit-tech/stellar-wallets-kit"]) {
1312
- run("npx", ["jsr", "add", "@creit-tech/stellar-wallets-kit"]);
1313
- }
1314
- }
1315
- }
1316
-
1317
- function copySharedDetailsInto(targetRelativeDir, { uiBase } = {}) {
1318
- const srcDir = path.join(TEMPLATES_DIR, "escrows", "details");
1319
- const componentsSrcDir = path.join(PROJECT_ROOT, "src", "components");
1320
- const outRoot = fs.existsSync(componentsSrcDir)
1321
- ? path.join(componentsSrcDir, "tw-blocks")
1322
- : path.join(PROJECT_ROOT, "components", "tw-blocks");
1323
- const destDir = path.join(outRoot, targetRelativeDir);
1324
- const config = loadConfig();
1325
- const effectiveUiBase = uiBase || config.uiBase || "@/components/ui";
1326
-
1327
- if (!fs.existsSync(srcDir)) return;
1328
- fs.mkdirSync(destDir, { recursive: true });
1329
-
1330
- function writeTransformed(srcPath, destPath) {
1331
- const raw = fs.readFileSync(srcPath, "utf8");
1332
- let transformed = raw.replaceAll("__UI_BASE__", effectiveUiBase);
1333
- // Resolve details placeholders to either multi-release modules (if present) or local compat
1334
- const resolveImport = (segments, compatFile) => {
1335
- const realWithExt = path.join(
1336
- outRoot,
1337
- "escrows",
1338
- "multi-release",
1339
- ...segments
1340
- );
1341
- const realCandidate = [
1342
- realWithExt,
1343
- realWithExt + ".tsx",
1344
- realWithExt + ".ts",
1345
- realWithExt + ".jsx",
1346
- realWithExt + ".js",
1347
- ].find((p) => fs.existsSync(p));
1348
- const realNoExt = realCandidate
1349
- ? realCandidate.replace(/\.(tsx|ts|jsx|js)$/i, "")
1350
- : null;
1351
- const compatWithExt = path.join(
1352
- path.dirname(destPath),
1353
- "compat",
1354
- compatFile
1355
- );
1356
- const compatCandidate = [
1357
- compatWithExt,
1358
- compatWithExt + ".tsx",
1359
- compatWithExt + ".ts",
1360
- compatWithExt + ".jsx",
1361
- compatWithExt + ".js",
1362
- ].find((p) => fs.existsSync(p));
1363
- const compatNoExt = (compatCandidate || compatWithExt).replace(
1364
- /\.(tsx|ts|jsx|js)$/i,
1365
- ""
1366
- );
1367
- const target = realNoExt || compatNoExt;
1368
- let rel = path.relative(path.dirname(destPath), target);
1369
- rel = rel.split(path.sep).join("/");
1370
- if (!rel.startsWith(".")) rel = "./" + rel;
1371
- return rel;
1372
- };
1373
- transformed = transformed
1374
- .replaceAll(
1375
- "__MR_RELEASE_MODULE__",
1376
- resolveImport(
1377
- ["release-escrow", "button", "ReleaseEscrow"],
1378
- "ReleaseEscrow"
1379
- )
1380
- )
1381
- .replaceAll(
1382
- "__MR_DISPUTE_MODULE__",
1383
- resolveImport(
1384
- ["dispute-escrow", "button", "DisputeEscrow"],
1385
- "DisputeEscrow"
1386
- )
1387
- )
1388
- .replaceAll(
1389
- "__MR_RESOLVE_MODULE__",
1390
- resolveImport(
1391
- ["resolve-dispute", "dialog", "ResolveDispute"],
1392
- "ResolveDispute"
1393
- )
1394
- );
1395
- fs.mkdirSync(path.dirname(destPath), { recursive: true });
1396
- fs.writeFileSync(destPath, transformed, "utf8");
1397
- console.log(`✅ ${path.relative(PROJECT_ROOT, destPath)} created`);
1398
- }
1399
-
1400
- const stack = [""];
1401
- while (stack.length) {
1402
- const rel = stack.pop();
1403
- const current = path.join(srcDir, rel);
1404
- const entries = fs.readdirSync(current, { withFileTypes: true });
1405
- for (const entry of entries) {
1406
- const entryRel = path.join(rel, entry.name);
1407
- const entrySrc = path.join(srcDir, entryRel);
1408
- const entryDest = path.join(destDir, entryRel);
1409
- if (entry.isDirectory()) {
1410
- stack.push(entryRel);
1411
- continue;
1412
- }
1413
- if (/\.(tsx?|jsx?)$/i.test(entry.name)) {
1414
- writeTransformed(entrySrc, entryDest);
1415
- } else {
1416
- fs.mkdirSync(path.dirname(entryDest), { recursive: true });
1417
- fs.copyFileSync(entrySrc, entryDest);
1418
- console.log(`✅ ${path.relative(PROJECT_ROOT, entryDest)} created`);
1419
- }
1420
- }
1421
- }
1422
- }
1423
-
1424
- function copySharedRoleSignerHooks(kind = "both") {
1425
- const componentsSrcDir = path.join(PROJECT_ROOT, "src", "components");
1426
- const outRoot = fs.existsSync(componentsSrcDir)
1427
- ? path.join(componentsSrcDir, "tw-blocks")
1428
- : path.join(PROJECT_ROOT, "components", "tw-blocks");
1429
-
1430
- const mappings = [];
1431
- if (kind === "both" || kind === "role") {
1432
- mappings.push({
1433
- src: path.join(
1434
- TEMPLATES_DIR,
1435
- "escrows",
1436
- "escrows-by-role",
1437
- "useEscrowsByRole.shared.ts"
1438
- ),
1439
- dest: path.join(
1440
- outRoot,
1441
- "escrows",
1442
- "escrows-by-role",
1443
- "useEscrowsByRole.shared.ts"
1444
- ),
1445
- });
1446
- }
1447
- if (kind === "both" || kind === "signer") {
1448
- mappings.push({
1449
- src: path.join(
1450
- TEMPLATES_DIR,
1451
- "escrows",
1452
- "escrows-by-signer",
1453
- "useEscrowsBySigner.shared.ts"
1454
- ),
1455
- dest: path.join(
1456
- outRoot,
1457
- "escrows",
1458
- "escrows-by-signer",
1459
- "useEscrowsBySigner.shared.ts"
1460
- ),
1461
- });
1462
- }
1463
-
1464
- for (const { src, dest } of mappings) {
1465
- if (!fs.existsSync(src)) continue;
1466
- const raw = fs.readFileSync(src, "utf8");
1467
- fs.mkdirSync(path.dirname(dest), { recursive: true });
1468
- fs.writeFileSync(dest, raw, "utf8");
1469
- console.log(`✅ ${path.relative(PROJECT_ROOT, dest)} created`);
1470
- }
1471
- }
1472
-
1473
- function findLayoutFile() {
1474
- const candidates = [
1475
- path.join(PROJECT_ROOT, "app", "layout.tsx"),
1476
- path.join(PROJECT_ROOT, "app", "layout.ts"),
1477
- path.join(PROJECT_ROOT, "app", "layout.jsx"),
1478
- path.join(PROJECT_ROOT, "app", "layout.js"),
1479
- path.join(PROJECT_ROOT, "src", "app", "layout.tsx"),
1480
- path.join(PROJECT_ROOT, "src", "app", "layout.ts"),
1481
- path.join(PROJECT_ROOT, "src", "app", "layout.jsx"),
1482
- path.join(PROJECT_ROOT, "src", "app", "layout.js"),
1483
- ];
1484
- return candidates.find((p) => fs.existsSync(p)) || null;
1485
- }
1486
-
1487
- function injectProvidersIntoLayout(
1488
- layoutPath,
1489
- { reactQuery = false, trustless = false, wallet = false, escrow = false } = {}
1490
- ) {
1491
- try {
1492
- let content = fs.readFileSync(layoutPath, "utf8");
1493
-
1494
- const importRQ =
1495
- 'import { ReactQueryClientProvider } from "@/components/tw-blocks/providers/ReactQueryClientProvider";\n';
1496
- const importTW =
1497
- 'import { TrustlessWorkProvider } from "@/components/tw-blocks/providers/TrustlessWork";\n';
1498
- const importEscrow =
1499
- 'import { EscrowProvider } from "@/components/tw-blocks/providers/EscrowProvider";\n';
1500
- const importWallet =
1501
- 'import { WalletProvider } from "@/components/tw-blocks/wallet-kit/WalletProvider";\n';
1502
- const commentText =
1503
- "// Use these imports to wrap your application (<ReactQueryClientProvider>, <TrustlessWorkProvider>, <WalletProvider> y <EscrowProvider>)\n";
1504
-
1505
- const needImport = (name) =>
1506
- !new RegExp(
1507
- `import\\s+[^;]*${name}[^;]*from\\s+['\"][^'\"]+['\"];?`
1508
- ).test(content);
1509
-
1510
- let importsToAdd = "";
1511
- if (reactQuery && needImport("ReactQueryClientProvider"))
1512
- importsToAdd += importRQ;
1513
- if (trustless && needImport("TrustlessWorkProvider"))
1514
- importsToAdd += importTW;
1515
- if (wallet && needImport("WalletProvider")) importsToAdd += importWallet;
1516
- if (escrow && needImport("EscrowProvider")) importsToAdd += importEscrow;
1517
-
1518
- if (importsToAdd) {
1519
- const importStmtRegex = /^import.*;\s*$/gm;
1520
- let last = null;
1521
- for (const m of content.matchAll(importStmtRegex)) last = m;
1522
- if (last) {
1523
- const idx = last.index + last[0].length;
1524
- content =
1525
- content.slice(0, idx) +
1526
- "\n" +
1527
- importsToAdd +
1528
- commentText +
1529
- content.slice(idx);
1530
- } else {
1531
- content = importsToAdd + commentText + content;
1532
- }
1533
- }
1534
-
1535
- const hasTag = (tag) => new RegExp(`<${tag}[\\s>]`).test(content);
1536
- const wrapInside = (containerTag, newTag) => {
1537
- const open = content.match(new RegExp(`<${containerTag}(\\s[^>]*)?>`));
1538
- if (!open) return false;
1539
- const openIdx = open.index + open[0].length;
1540
- const closeIdx = content.indexOf(`</${containerTag}>`, openIdx);
1541
- if (closeIdx === -1) return false;
1542
- content =
1543
- content.slice(0, openIdx) +
1544
- `\n<${newTag}>\n` +
1545
- content.slice(openIdx, closeIdx) +
1546
- `\n</${newTag}>\n` +
1547
- content.slice(closeIdx);
1548
- return true;
1549
- };
1550
-
1551
- const ensureTag = (tag) => {
1552
- if (hasTag(tag)) return;
1553
- const bodyOpen = content.match(/<body[^>]*>/);
1554
- const bodyCloseIdx = content.lastIndexOf("</body>");
1555
- if (!bodyOpen || bodyCloseIdx === -1) return;
1556
- const bodyOpenIdx = bodyOpen.index + bodyOpen[0].length;
1557
- if (tag === "TrustlessWorkProvider") {
1558
- if (wrapInside("ReactQueryClientProvider", tag)) return;
1559
- }
1560
- if (tag === "WalletProvider") {
1561
- if (wrapInside("TrustlessWorkProvider", tag)) return;
1562
- if (wrapInside("ReactQueryClientProvider", tag)) return;
1563
- }
1564
- if (tag === "EscrowProvider") {
1565
- if (wrapInside("WalletProvider", tag)) return;
1566
- if (wrapInside("TrustlessWorkProvider", tag)) return;
1567
- if (wrapInside("ReactQueryClientProvider", tag)) return;
1568
- }
1569
- content =
1570
- content.slice(0, bodyOpenIdx) +
1571
- `\n<${tag}>\n` +
1572
- content.slice(bodyOpenIdx, bodyCloseIdx) +
1573
- `\n</${tag}>\n` +
1574
- content.slice(bodyCloseIdx);
1575
- };
1576
-
1577
- if (reactQuery) ensureTag("ReactQueryClientProvider");
1578
- if (trustless) ensureTag("TrustlessWorkProvider");
1579
- if (wallet) ensureTag("WalletProvider");
1580
- if (escrow) ensureTag("EscrowProvider");
1581
-
1582
- fs.writeFileSync(layoutPath, content, "utf8");
1583
- logCheck(
1584
- `Updated ${path.relative(PROJECT_ROOT, layoutPath)} with providers`
1585
- );
1586
- } catch (e) {
1587
- console.error("❌ Failed to update layout with providers:", e.message);
1588
- }
1589
- }
1590
-
1591
- function cleanJsrDeps() {
1592
- const pkgPath = path.join(PROJECT_ROOT, "package.json");
1593
- if (!fs.existsSync(pkgPath)) return;
1594
- try {
1595
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
1596
- let modified = false;
1597
- const target = "@creit-tech/stellar-wallets-kit";
1598
- if (pkg.dependencies && pkg.dependencies[target]) {
1599
- delete pkg.dependencies[target];
1600
- modified = true;
1601
- }
1602
- if (pkg.devDependencies && pkg.devDependencies[target]) {
1603
- delete pkg.devDependencies[target];
1604
- modified = true;
1605
- }
1606
- if (modified) {
1607
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2), "utf8");
1608
- }
1609
- } catch (e) {}
1610
- }
1611
-
1612
- if (args[0] === "init") {
1613
- cleanJsrDeps();
1614
- console.log("\n▶ Setting up shadcn/ui components...");
1615
- const doInit = await promptYesNo("Run shadcn init now?", true);
1616
- if (doInit) {
1617
- run("npx", ["shadcn@latest", "init"]);
1618
- } else {
1619
- console.log("\x1b[90m– Skipped shadcn init\x1b[0m");
1620
- }
1621
-
1622
- const addShadcn = await promptYesNo(
1623
- "Add shadcn components (button, input, form, card, sonner, checkbox, dialog, textarea, sonner, select, table, calendar, popover, separator, calendar-05, badge, sheet, tabs, avatar, tooltip, progress, chart, empty)?",
1624
- true
1625
- );
1626
- if (addShadcn) {
1627
- await withSpinner("Installing shadcn/ui components", async () => {
1628
- await runAsync("npx", [
1629
- "shadcn@latest",
1630
- "add",
1631
- "button",
1632
- "input",
1633
- "form",
1634
- "card",
1635
- "sonner",
1636
- "checkbox",
1637
- "dialog",
1638
- "textarea",
1639
- "sonner",
1640
- "select",
1641
- "table",
1642
- "calendar",
1643
- "popover",
1644
- "separator",
1645
- "calendar-05",
1646
- "badge",
1647
- "sheet",
1648
- "tabs",
1649
- "avatar",
1650
- "tooltip",
1651
- "progress",
1652
- "chart",
1653
- "empty",
1654
- ]);
1655
- });
1656
- } else {
1657
- console.log("\x1b[90m– Skipped adding shadcn components\x1b[0m");
1658
- }
1659
-
1660
- if (!fs.existsSync(GLOBAL_DEPS_FILE)) {
1661
- console.error("❌ deps.json not found in templates/");
1662
- process.exit(1);
1663
- }
1664
- const meta = JSON.parse(fs.readFileSync(GLOBAL_DEPS_FILE, "utf8"));
1665
- const installLibs = await promptYesNo(
1666
- "Install (react-hook-form, @tanstack/react-query, @tanstack/react-query-devtools, @trustless-work/escrow, @hookform/resolvers, axios, @creit-tech/stellar-wallets-kit, react-day-picker, recharts & zod) dependencies now?",
1667
- true
1668
- );
1669
- if (installLibs) {
1670
- await withSpinner("Installing required dependencies", async () => {
1671
- installDeps(meta);
1672
- });
1673
- // Install @creit-tech/stellar-wallets-kit using jsr after all other deps are installed
1674
- if (meta.dependencies && meta.dependencies["@creit-tech/stellar-wallets-kit"]) {
1675
- await withSpinner("Installing @creit-tech/stellar-wallets-kit from JSR", async () => {
1676
- run("npx", ["jsr", "add", "@creit-tech/stellar-wallets-kit"]);
1677
- });
1678
- }
1679
- } else {
1680
- console.log("\x1b[90m– Skipped installing required dependencies\x1b[0m");
1681
- }
1682
- const cfgPath = path.join(PROJECT_ROOT, ".twblocks.json");
1683
- if (!fs.existsSync(cfgPath)) {
1684
- fs.writeFileSync(
1685
- cfgPath,
1686
- JSON.stringify({ uiBase: "@/components/ui" }, null, 2)
1687
- );
1688
- console.log(
1689
- `\x1b[32m✔\x1b[0m Created ${path.relative(
1690
- PROJECT_ROOT,
1691
- cfgPath
1692
- )} with default uiBase`
1693
- );
1694
- }
1695
- console.log("\x1b[32m✔\x1b[0m shadcn/ui components step completed");
1696
-
1697
- const wantProviders = await promptYesNo(
1698
- "Install TanStack Query and Trustless Work providers and wrap app/layout with them?",
1699
- true
1700
- );
1701
- if (wantProviders) {
1702
- await withSpinner("Installing providers", async () => {
1703
- copyTemplate("providers");
1704
- });
1705
- const layoutPath = findLayoutFile();
1706
- if (layoutPath) {
1707
- await withSpinner("Updating app/layout with providers", async () => {
1708
- injectProvidersIntoLayout(layoutPath, {
1709
- reactQuery: true,
1710
- trustless: true,
1711
- });
1712
- });
1713
- } else {
1714
- console.warn(
1715
- "⚠️ Could not find app/layout file. Skipped automatic wiring."
1716
- );
1717
- }
1718
- } else {
1719
- console.log("\x1b[90m– Skipped installing providers\x1b[0m");
1720
- }
1721
-
1722
- printBannerTRUSTLESSWORK();
1723
- console.log("\n\nResources");
1724
- console.log("- " + oscHyperlink("Website", "https://trustlesswork.com"));
1725
- console.log(
1726
- "- " + oscHyperlink("Documentation", "https://docs.trustlesswork.com")
1727
- );
1728
- console.log("- " + oscHyperlink("Demo", "https://demo.trustlesswork.com"));
1729
- console.log(
1730
- "- " + oscHyperlink("Backoffice", "https://dapp.trustlesswork.com")
1731
- );
1732
- console.log(
1733
- "- " + oscHyperlink("GitHub", "https://github.com/trustless-work")
1734
- );
1735
- console.log(
1736
- "- " + oscHyperlink("Escrow Viewer", "https://viewer.trustlesswork.com")
1737
- );
1738
- console.log(
1739
- "- " + oscHyperlink("Telegram", "https://t.me/+kmr8tGegxLU0NTA5")
1740
- );
1741
- console.log(
1742
- "- " +
1743
- oscHyperlink(
1744
- "LinkedIn",
1745
- "https://www.linkedin.com/company/trustlesswork/posts/?feedView=all"
1746
- )
1747
- );
1748
- console.log("- " + oscHyperlink("X", "https://x.com/TrustlessWork"));
1749
- } else if (args[0] === "add" && args[1]) {
1750
- const flags = parseFlags(args.slice(2));
1751
- // Normalize common aliases (singular/plural, shorthand)
1752
- const normalizeTemplateName = (name) => {
1753
- let n = String(name).trim();
1754
- // singular to plural base
1755
- n = n.replace(/^escrow\b/, "escrows");
1756
- n = n.replace(/^indicator\b/, "indicators");
1757
- // allow nested segments singulars
1758
- n = n.replace(/(^|\/)escrow(\/|$)/g, "$1escrows$2");
1759
- n = n.replace(/(^|\/)indicator(\/|$)/g, "$1indicators$2");
1760
- // friendly shape variants
1761
- n = n.replace(/(^|\/)circle(\/|$)/g, "$1circular$2");
1762
- return n;
1763
- };
1764
- args[1] = normalizeTemplateName(args[1]);
1765
- const cfgPath = path.join(PROJECT_ROOT, ".twblocks.json");
1766
- if (!fs.existsSync(cfgPath)) {
1767
- console.error(
1768
- "❌ Missing initial setup. Run 'trustless-work init' first to install dependencies and create .twblocks.json (uiBase)."
1769
- );
1770
- console.error(
1771
- " After init, re-run: trustless-work add " +
1772
- args[1] +
1773
- (flags.uiBase ? ' --ui-base "' + flags.uiBase + '"' : "")
1774
- );
1775
- process.exit(1);
1776
- }
1777
- copyTemplate(args[1], {
1778
- uiBase: flags.uiBase,
1779
- shouldInstall: !!flags.install,
1780
- });
1781
-
1782
- // Post-add wiring for specific templates
1783
- const layoutPath = findLayoutFile();
1784
- if (layoutPath) {
1785
- if (args[1] === "wallet-kit" || args[1].startsWith("wallet-kit/")) {
1786
- injectProvidersIntoLayout(layoutPath, { wallet: true });
1787
- }
1788
- }
1789
-
1790
- // Copy shared details into role/signer targets when applicable
1791
- try {
1792
- if (args[1] === "escrows") {
1793
- copySharedDetailsInto("escrows/escrows-by-role/details", {
1794
- uiBase: flags.uiBase,
1795
- });
1796
- copySharedDetailsInto("escrows/escrows-by-signer/details", {
1797
- uiBase: flags.uiBase,
1798
- });
1799
- copySharedRoleSignerHooks("both");
1800
- }
1801
- if (
1802
- args[1] === "escrows/escrows-by-role" ||
1803
- args[1].startsWith("escrows/escrows-by-role/")
1804
- ) {
1805
- copySharedDetailsInto("escrows/escrows-by-role/details", {
1806
- uiBase: flags.uiBase,
1807
- });
1808
- copySharedRoleSignerHooks("role");
1809
- }
1810
- if (
1811
- args[1] === "escrows/escrows-by-signer" ||
1812
- args[1].startsWith("escrows/escrows-by-signer/")
1813
- ) {
1814
- copySharedDetailsInto("escrows/escrows-by-signer/details", {
1815
- uiBase: flags.uiBase,
1816
- });
1817
- copySharedRoleSignerHooks("signer");
1818
- }
1819
- } catch (e) {
1820
- console.warn("⚠️ Failed to copy shared details:", e?.message || e);
1821
- }
1822
- } else {
1823
- console.log(`
1824
-
1825
- Usage:
1826
-
1827
- trustless-work init
1828
- trustless-work add <template> [--install]
1829
-
1830
- Options:
1831
-
1832
- --ui-base <path> Base import path to your shadcn/ui components (default: "@/components/ui")
1833
- --install, -i Also install dependencies (normally use 'init' once instead)
1834
-
1835
- Examples:
1836
-
1837
- --- Get started ---
1838
- trustless-work init
1839
-
1840
- --- Providers ---
1841
- trustless-work add providers
1842
-
1843
- --- Wallet-kit ---
1844
- trustless-work add wallet-kit
1845
-
1846
- --- Handle-errors ---
1847
- trustless-work add handle-errors
1848
-
1849
- --- Helpers ---
1850
- trustless-work add helpers
1851
-
1852
- --- Tanstack ---
1853
- trustless-work add tanstack
1854
-
1855
- --- Escrows ---
1856
- trustless-work add escrows
1857
-
1858
- --- Dashboard ---
1859
- trustless-work add dashboard
1860
- trustless-work add dashboard/dashboard-01
1861
-
1862
- --- Indicators ---
1863
- trustless-work add escrows/indicators/balance-progress
1864
- trustless-work add escrows/indicators/balance-progress/bar
1865
- trustless-work add escrows/indicators/balance-progress/donut
1866
-
1867
- --- Escrows by role ---
1868
- trustless-work add escrows/escrows-by-role
1869
- trustless-work add escrows/escrows-by-role/table
1870
- trustless-work add escrows/escrows-by-role/cards
1871
-
1872
- --- Escrows by signer ---
1873
- trustless-work add escrows/escrows-by-signer
1874
- trustless-work add escrows/escrows-by-signer/table
1875
- trustless-work add escrows/escrows-by-signer/cards
1876
-
1877
- ----------------------
1878
- --- SINGLE-RELEASE ---
1879
- trustless-work add escrows/single-release
1880
-
1881
- --- Initialize escrow ---
1882
- - trustless-work add escrows/single-release/initialize-escrow
1883
- - trustless-work add escrows/single-release/initialize-escrow/form
1884
- - trustless-work add escrows/single-release/initialize-escrow/dialog
1885
-
1886
- --- Resolve dispute ---
1887
- - trustless-work add escrows/single-release/resolve-dispute
1888
- - trustless-work add escrows/single-release/resolve-dispute/form
1889
- - trustless-work add escrows/single-release/resolve-dispute/button
1890
- - trustless-work add escrows/single-release/resolve-dispute/dialog
1891
-
1892
- --- Update escrow ---
1893
- - trustless-work add escrows/single-release/update-escrow
1894
- - trustless-work add escrows/single-release/update-escrow/form
1895
- - trustless-work add escrows/single-release/update-escrow/dialog
1896
-
1897
- --- Release escrow ---
1898
- - trustless-work add escrows/single-release/release-escrow
1899
- - trustless-work add escrows/single-release/release-escrow/button
1900
-
1901
- --- Dispute escrow ---
1902
- - trustless-work add escrows/single-release/dispute-escrow
1903
- - trustless-work add escrows/single-release/dispute-escrow/button
1904
-
1905
- ----------------------
1906
- --- MULTI-RELEASE ---
1907
- trustless-work add escrows/multi-release
1908
-
1909
- --- Initialize escrow ---
1910
- - trustless-work add escrows/multi-release/initialize-escrow
1911
- - trustless-work add escrows/multi-release/initialize-escrow/form
1912
- - trustless-work add escrows/multi-release/initialize-escrow/dialog
1913
-
1914
- --- Resolve dispute ---
1915
- - trustless-work add escrows/multi-release/resolve-dispute
1916
- - trustless-work add escrows/multi-release/resolve-dispute/form
1917
- - trustless-work add escrows/multi-release/resolve-dispute/button
1918
- - trustless-work add escrows/multi-release/resolve-dispute/dialog
1919
-
1920
- --- Update escrow ---
1921
- - trustless-work add escrows/multi-release/update-escrow
1922
- - trustless-work add escrows/multi-release/update-escrow/form
1923
- - trustless-work add escrows/multi-release/update-escrow/dialog
1924
-
1925
- --- Withdraw remaining funds ---
1926
- - trustless-work add escrows/multi-release/withdraw-remaining-funds
1927
- - trustless-work add escrows/multi-release/withdraw-remaining-funds/form
1928
- - trustless-work add escrows/multi-release/withdraw-remaining-funds/button
1929
- - trustless-work add escrows/multi-release/withdraw-remaining-funds/dialog
1930
-
1931
- --- Release escrow ---
1932
- - trustless-work add escrows/multi-release/release-milestone
1933
- - trustless-work add escrows/multi-release/release-milestone/button
1934
-
1935
- --- Dispute escrow ---
1936
- - trustless-work add escrows/multi-release/dispute-milestone
1937
- - trustless-work add escrows/multi-release/dispute-milestone/button
1938
-
1939
- --- Withdraw remaining funds ---
1940
- - trustless-work add escrows/multi-release/withdraw-remaining-funds
1941
- - trustless-work add escrows/multi-release/withdraw-remaining-funds/form
1942
- - trustless-work add escrows/multi-release/withdraw-remaining-funds/button
1943
- - trustless-work add escrows/multi-release/withdraw-remaining-funds/dialog
1944
-
1945
- ----------------------
1946
- --- SINGLE-MULTI-RELEASE -> Works with both types of escrows ---
1947
- trustless-work add escrows/single-multi-release
1948
-
1949
- --- Approve milestone ---
1950
- - trustless-work add escrows/single-multi-release/approve-milestone
1951
- - trustless-work add escrows/single-multi-release/approve-milestone/form
1952
- - trustless-work add escrows/single-multi-release/approve-milestone/button
1953
- - trustless-work add escrows/single-multi-release/approve-milestone/dialog
1954
-
1955
- --- Change milestone status ---
1956
- - trustless-work add escrows/single-multi-release/change-milestone-status
1957
- - trustless-work add escrows/single-multi-release/change-milestone-status/form
1958
- - trustless-work add escrows/single-multi-release/change-milestone-status/button
1959
- - trustless-work add escrows/single-multi-release/change-milestone-status/dialog
1960
-
1961
- --- Fund escrow ---
1962
- - trustless-work add escrows/single-multi-release/fund-escrow
1963
- - trustless-work add escrows/single-multi-release/fund-escrow/form
1964
- - trustless-work add escrows/single-multi-release/fund-escrow/button
1965
- - trustless-work add escrows/single-multi-release/fund-escrow/dialog
1966
- `);
1967
- }
1
+ #!/usr/bin/env node
2
+
3
+ /*
4
+ AUTHOR: @trustless-work / Joel Vargas
5
+ COPYRIGHT: 2025 Trustless Work
6
+ LICENSE: MIT
7
+ VERSION: 1.0.0
8
+ */
9
+
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { spawnSync, spawn } from "node:child_process";
14
+ import readline from "node:readline";
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+
19
+ const PROJECT_ROOT = process.cwd();
20
+ const TEMPLATES_DIR = path.join(__dirname, "..", "templates");
21
+ const GLOBAL_DEPS_FILE = path.join(TEMPLATES_DIR, "deps.json");
22
+
23
+ const args = process.argv.slice(2);
24
+
25
+ function detectPM() {
26
+ if (fs.existsSync(path.join(PROJECT_ROOT, "pnpm-lock.yaml"))) return "pnpm";
27
+ if (fs.existsSync(path.join(PROJECT_ROOT, "yarn.lock"))) return "yarn";
28
+ if (fs.existsSync(path.join(PROJECT_ROOT, "bun.lockb"))) return "bun";
29
+ return "npm";
30
+ }
31
+
32
+ function run(cmd, args) {
33
+ const r = spawnSync(cmd, args.filter(Boolean), {
34
+ stdio: "inherit",
35
+ cwd: PROJECT_ROOT,
36
+ shell: true,
37
+ });
38
+ if (r.status !== 0) process.exit(r.status ?? 1);
39
+ }
40
+
41
+ function tryRun(cmd, args, errorMessage) {
42
+ const r = spawnSync(cmd, args.filter(Boolean), {
43
+ stdio: "inherit",
44
+ cwd: PROJECT_ROOT,
45
+ shell: true,
46
+ });
47
+ if (r.status !== 0) {
48
+ console.error(errorMessage);
49
+ process.exit(r.status ?? 1);
50
+ }
51
+ }
52
+
53
+ async function runAsync(cmd, args) {
54
+ return new Promise((resolve, reject) => {
55
+ const child = spawn(cmd, args.filter(Boolean), {
56
+ stdio: "inherit",
57
+ cwd: PROJECT_ROOT,
58
+ shell: true,
59
+ });
60
+ child.on("close", (code) => {
61
+ if (code === 0) resolve();
62
+ else reject(new Error(`${cmd} exited with code ${code}`));
63
+ });
64
+ });
65
+ }
66
+
67
+ const COLORS = {
68
+ reset: "\x1b[0m",
69
+ green: "\x1b[32m",
70
+ gray: "\x1b[90m",
71
+ blueTW: "\x1b[38;2;0;107;228m",
72
+ };
73
+
74
+ function logCheck(message) {
75
+ console.log(`${COLORS.green}✔${COLORS.reset} ${message}`);
76
+ }
77
+
78
+ function startSpinner(message) {
79
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
80
+ let i = 0;
81
+ process.stdout.write(`${frames[0]} ${message}`);
82
+ const timer = setInterval(() => {
83
+ i = (i + 1) % frames.length;
84
+ process.stdout.write(`\r${frames[i]} ${message}`);
85
+ }, 80);
86
+ return () => {
87
+ clearInterval(timer);
88
+ process.stdout.write("\r");
89
+ };
90
+ }
91
+
92
+ async function withSpinner(message, fn) {
93
+ const stop = startSpinner(message);
94
+ try {
95
+ await fn();
96
+ stop();
97
+ logCheck(message);
98
+ } catch (err) {
99
+ stop();
100
+ throw err;
101
+ }
102
+ }
103
+
104
+ async function promptYesNo(question, def = true) {
105
+ const rl = readline.createInterface({
106
+ input: process.stdin,
107
+ output: process.stdout,
108
+ });
109
+ const suffix = def ? "(Y/n)" : "(y/N)";
110
+ const answer = await new Promise((res) =>
111
+ rl.question(`${question} ${suffix} `, (ans) => res(ans))
112
+ );
113
+ rl.close();
114
+ const a = String(answer).trim().toLowerCase();
115
+ if (!a) return def;
116
+ return a.startsWith("y");
117
+ }
118
+
119
+ function oscHyperlink(text, url) {
120
+ return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
121
+ }
122
+
123
+ function printBannerTRUSTLESSWORK() {
124
+ const map = {
125
+ T: ["******", " ** ", " ** ", " ** ", " ** "],
126
+ R: ["***** ", "** **", "***** ", "** ** ", "** **"],
127
+ U: ["** **", "** **", "** **", "** **", " **** "],
128
+ S: [" **** ", "** ", " **** ", " **", " **** "],
129
+ L: ["** ", "** ", "** ", "** ", "******"],
130
+ E: ["******", "** ", "***** ", "** ", "******"],
131
+ W: ["** **", "** **", "** * **", "*** ***", "** **"],
132
+ O: [" **** ", "** **", "** **", "** **", " **** "],
133
+ K: ["** **", "** ** ", "**** ", "** ** ", "** **"],
134
+ " ": [" ", " ", " ", " ", " "],
135
+ };
136
+ const text = "TRUSTLESS WORK";
137
+ const rows = ["", "", "", "", ""];
138
+ for (const ch of text) {
139
+ const glyph = map[ch] || map[" "];
140
+ for (let i = 0; i < 5; i++) {
141
+ rows[i] += glyph[i] + " ";
142
+ }
143
+ }
144
+ console.log("\n\n");
145
+ for (const line of rows) {
146
+ console.log(`${COLORS.blueTW}${line}${COLORS.reset}`);
147
+ }
148
+ }
149
+
150
+ function readProjectPackageJson() {
151
+ const pkgPath = path.join(PROJECT_ROOT, "package.json");
152
+ if (!fs.existsSync(pkgPath)) return null;
153
+ try {
154
+ return JSON.parse(fs.readFileSync(pkgPath, "utf8"));
155
+ } catch {
156
+ return null;
157
+ }
158
+ }
159
+
160
+ function installDeps({ dependencies = {}, devDependencies = {} }) {
161
+ const pm = detectPM();
162
+ const BLOCKED = new Set([
163
+ "tailwindcss",
164
+ "@tailwindcss/cli",
165
+ "@tailwindcss/postcss",
166
+ "@tailwindcss/vite",
167
+ "postcss",
168
+ "autoprefixer",
169
+ "postcss-import",
170
+ "@creit-tech/stellar-wallets-kit",
171
+ ]);
172
+ const depList = Object.entries(dependencies)
173
+ .filter(([k]) => !BLOCKED.has(k))
174
+ .map(([k, v]) => `${k}@${v}`);
175
+ const devList = Object.entries(devDependencies)
176
+ .filter(([k]) => !BLOCKED.has(k))
177
+ .map(([k, v]) => `${k}@${v}`);
178
+
179
+ if (depList.length) {
180
+ if (pm === "pnpm") run("pnpm", ["add", ...depList]);
181
+ else if (pm === "yarn") run("yarn", ["add", ...depList]);
182
+ else if (pm === "bun") run("bun", ["add", ...depList]);
183
+ else run("npm", ["install", ...depList]);
184
+ }
185
+
186
+ if (devList.length) {
187
+ if (pm === "pnpm") run("pnpm", ["add", "-D", ...devList]);
188
+ else if (pm === "yarn") run("yarn", ["add", "-D", ...devList]);
189
+ else if (pm === "bun") run("bun", ["add", "-d", ...devList]);
190
+ else run("npm", ["install", "-D", ...devList]);
191
+ }
192
+ }
193
+
194
+ function loadConfig() {
195
+ const cfgPath = path.join(PROJECT_ROOT, ".twblocks.json");
196
+ if (fs.existsSync(cfgPath)) {
197
+ try {
198
+ return JSON.parse(fs.readFileSync(cfgPath, "utf8"));
199
+ } catch (e) {
200
+ console.warn("⚠️ Failed to parse .twblocks.json, ignoring.");
201
+ }
202
+ }
203
+ return {};
204
+ }
205
+
206
+ function parseFlags(argv) {
207
+ const flags = {};
208
+ for (let i = 0; i < argv.length; i++) {
209
+ const a = argv[i];
210
+ if (a.startsWith("--ui-base=")) {
211
+ flags.uiBase = a.split("=").slice(1).join("=");
212
+ } else if (a === "--ui-base") {
213
+ flags.uiBase = argv[i + 1];
214
+ i++;
215
+ } else if (a === "--install" || a === "-i") {
216
+ flags.install = true;
217
+ }
218
+ }
219
+ return flags;
220
+ }
221
+
222
+ function copyTemplate(name, { uiBase, shouldInstall = false } = {}) {
223
+ const srcFile = path.join(TEMPLATES_DIR, `${name}.tsx`);
224
+ const requestedDir = path.join(TEMPLATES_DIR, name);
225
+ let srcDir = null;
226
+ if (fs.existsSync(requestedDir) && fs.lstatSync(requestedDir).isDirectory()) {
227
+ srcDir = requestedDir;
228
+ } else {
229
+ // Alias: allow multi-release/approve-milestone to fallback to existing source
230
+ if (name.startsWith("escrows/multi-release/approve-milestone")) {
231
+ const altMulti = path.join(
232
+ TEMPLATES_DIR,
233
+ "escrows",
234
+ "multi-release",
235
+ "approve-milestone"
236
+ );
237
+ const altSingle = path.join(
238
+ TEMPLATES_DIR,
239
+ "escrows",
240
+ "single-release",
241
+ "approve-milestone"
242
+ );
243
+ if (fs.existsSync(altMulti) && fs.lstatSync(altMulti).isDirectory()) {
244
+ srcDir = altMulti;
245
+ } else if (
246
+ fs.existsSync(altSingle) &&
247
+ fs.lstatSync(altSingle).isDirectory()
248
+ ) {
249
+ srcDir = altSingle;
250
+ }
251
+ }
252
+ }
253
+ const componentsSrcDir = path.join(PROJECT_ROOT, "src", "components");
254
+ const outRoot = fs.existsSync(componentsSrcDir)
255
+ ? path.join(componentsSrcDir, "tw-blocks")
256
+ : path.join(PROJECT_ROOT, "components", "tw-blocks");
257
+
258
+ const config = loadConfig();
259
+ const effectiveUiBase = uiBase || config.uiBase || "@/components/ui";
260
+ let currentEscrowType = null;
261
+
262
+ function writeTransformed(srcPath, destPath) {
263
+ const raw = fs.readFileSync(srcPath, "utf8");
264
+ let transformed = raw.replaceAll("__UI_BASE__", effectiveUiBase);
265
+ // Resolve details placeholders to either multi-release modules (if present) or local compat
266
+ const applyDetailsPlaceholders = (content) => {
267
+ const resolveImport = (segments, compatFile) => {
268
+ const realWithExt = path.join(
269
+ outRoot,
270
+ "escrows",
271
+ "multi-release",
272
+ ...segments
273
+ );
274
+ const realCandidate = [
275
+ realWithExt,
276
+ realWithExt + ".tsx",
277
+ realWithExt + ".ts",
278
+ realWithExt + ".jsx",
279
+ realWithExt + ".js",
280
+ ].find((p) => fs.existsSync(p));
281
+ const realNoExt = realCandidate
282
+ ? realCandidate.replace(/\.(tsx|ts|jsx|js)$/i, "")
283
+ : null;
284
+ const compatWithExt = path.join(
285
+ path.dirname(destPath),
286
+ "compat",
287
+ compatFile
288
+ );
289
+ const compatCandidate = [
290
+ compatWithExt,
291
+ compatWithExt + ".tsx",
292
+ compatWithExt + ".ts",
293
+ compatWithExt + ".jsx",
294
+ compatWithExt + ".js",
295
+ ].find((p) => fs.existsSync(p));
296
+ const compatNoExt = (compatCandidate || compatWithExt).replace(
297
+ /\.(tsx|ts|jsx|js)$/i,
298
+ ""
299
+ );
300
+ const target = realNoExt || compatNoExt;
301
+ let rel = path.relative(path.dirname(destPath), target);
302
+ rel = rel.split(path.sep).join("/");
303
+ if (!rel.startsWith(".")) rel = "./" + rel;
304
+ return rel;
305
+ };
306
+ return content
307
+ .replaceAll(
308
+ "__MR_RELEASE_MODULE__",
309
+ resolveImport(
310
+ ["release-escrow", "button", "ReleaseEscrow"],
311
+ "ReleaseEscrow"
312
+ )
313
+ )
314
+ .replaceAll(
315
+ "__MR_DISPUTE_MODULE__",
316
+ resolveImport(
317
+ ["dispute-escrow", "button", "DisputeEscrow"],
318
+ "DisputeEscrow"
319
+ )
320
+ )
321
+ .replaceAll(
322
+ "__MR_RESOLVE_MODULE__",
323
+ resolveImport(
324
+ ["resolve-dispute", "dialog", "ResolveDispute"],
325
+ "ResolveDispute"
326
+ )
327
+ );
328
+ };
329
+ transformed = applyDetailsPlaceholders(transformed);
330
+ if (currentEscrowType) {
331
+ transformed = transformed.replaceAll(
332
+ "__ESCROW_TYPE__",
333
+ currentEscrowType
334
+ );
335
+ }
336
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
337
+ fs.writeFileSync(destPath, transformed, "utf8");
338
+ console.log(`✅ ${path.relative(PROJECT_ROOT, destPath)} created`);
339
+ }
340
+
341
+ // Generic: materialize any module from templates/escrows/shared/<module>
342
+ if (!srcDir) {
343
+ const m = name.match(
344
+ /^escrows\/(single-release|multi-release)\/([^\/]+)(?:\/(button|dialog|form))?$/
345
+ );
346
+ if (m) {
347
+ const releaseType = m[1];
348
+ const moduleName = m[2];
349
+ const variant = m[3] || null;
350
+
351
+ const sharedModuleDir = path.join(
352
+ TEMPLATES_DIR,
353
+ "escrows",
354
+ "shared",
355
+ moduleName
356
+ );
357
+
358
+ if (
359
+ fs.existsSync(sharedModuleDir) &&
360
+ fs.lstatSync(sharedModuleDir).isDirectory()
361
+ ) {
362
+ currentEscrowType = releaseType;
363
+ const destBase = path.join(outRoot, "escrows", releaseType, moduleName);
364
+
365
+ function copyModuleRootFilesInto(targetDir) {
366
+ const entries = fs.readdirSync(sharedModuleDir, {
367
+ withFileTypes: true,
368
+ });
369
+ for (const entry of entries) {
370
+ if (entry.isDirectory()) continue;
371
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
372
+ const entrySrc = path.join(sharedModuleDir, entry.name);
373
+ const entryDest = path.join(targetDir, entry.name);
374
+ writeTransformed(entrySrc, entryDest);
375
+ }
376
+ }
377
+
378
+ function copyVariant(variantName) {
379
+ const variantSrc = path.join(sharedModuleDir, variantName);
380
+ const variantDest = path.join(destBase, variantName);
381
+ fs.mkdirSync(variantDest, { recursive: true });
382
+ if (
383
+ fs.existsSync(variantSrc) &&
384
+ fs.lstatSync(variantSrc).isDirectory()
385
+ ) {
386
+ const stack = [""];
387
+ while (stack.length) {
388
+ const rel = stack.pop();
389
+ const current = path.join(variantSrc, rel);
390
+ const entries = fs.readdirSync(current, { withFileTypes: true });
391
+ for (const entry of entries) {
392
+ const entryRel = path.join(rel, entry.name);
393
+ const entrySrc = path.join(variantSrc, entryRel);
394
+ const entryDest = path.join(variantDest, entryRel);
395
+ if (entry.isDirectory()) {
396
+ stack.push(entryRel);
397
+ continue;
398
+ }
399
+ if (/\.(tsx?|jsx?)$/i.test(entry.name)) {
400
+ writeTransformed(entrySrc, entryDest);
401
+ } else {
402
+ fs.mkdirSync(path.dirname(entryDest), { recursive: true });
403
+ fs.copyFileSync(entrySrc, entryDest);
404
+ console.log(
405
+ `✅ ${path.relative(PROJECT_ROOT, entryDest)} created`
406
+ );
407
+ }
408
+ }
409
+ }
410
+ }
411
+ // Always place module-level shared files into the variant directory
412
+ copyModuleRootFilesInto(variantDest);
413
+ }
414
+
415
+ if (variant) {
416
+ copyVariant(variant);
417
+ } else {
418
+ const variants = ["button", "dialog", "form"];
419
+ for (const v of variants) copyVariant(v);
420
+ }
421
+
422
+ if (shouldInstall && fs.existsSync(GLOBAL_DEPS_FILE)) {
423
+ const meta = JSON.parse(fs.readFileSync(GLOBAL_DEPS_FILE, "utf8"));
424
+ installDeps(meta);
425
+ // Install @creit-tech/stellar-wallets-kit using jsr after all other deps are installed
426
+ if (meta.dependencies && meta.dependencies["@creit-tech/stellar-wallets-kit"]) {
427
+ run("npx", ["jsr", "add", "@creit-tech/stellar-wallets-kit"]);
428
+ }
429
+ }
430
+ currentEscrowType = null;
431
+ return;
432
+ }
433
+ }
434
+ }
435
+
436
+ if (fs.existsSync(srcDir) && fs.lstatSync(srcDir).isDirectory()) {
437
+ const skipDetails =
438
+ name === "escrows/escrows-by-role" ||
439
+ name === "escrows/escrows-by-signer" ||
440
+ name === "escrows";
441
+ // Copy directory recursively
442
+ const destDir = path.join(outRoot, name);
443
+ fs.mkdirSync(destDir, { recursive: true });
444
+ const stack = [""];
445
+ while (stack.length) {
446
+ const rel = stack.pop();
447
+ const current = path.join(srcDir, rel);
448
+ const entries = fs.readdirSync(current, { withFileTypes: true });
449
+ for (const entry of entries) {
450
+ const entryRel = path.join(rel, entry.name);
451
+ // Skip copying any shared directory at any depth
452
+ const parts = entryRel.split(path.sep);
453
+ if (parts.includes("shared")) {
454
+ continue;
455
+ }
456
+ if (skipDetails) {
457
+ const top = parts[0] || "";
458
+ const firstTwo = parts.slice(0, 2).join(path.sep);
459
+ if (
460
+ top === "details" ||
461
+ firstTwo === path.join("escrows-by-role", "details") ||
462
+ firstTwo === path.join("escrows-by-signer", "details")
463
+ ) {
464
+ continue;
465
+ }
466
+ }
467
+ const entrySrc = path.join(srcDir, entryRel);
468
+ const entryDest = path.join(destDir, entryRel);
469
+ if (entry.isDirectory()) {
470
+ stack.push(entryRel);
471
+ continue;
472
+ }
473
+ // Only process text files (.ts, .tsx, .js, .jsx)
474
+ if (/\.(tsx?|jsx?)$/i.test(entry.name)) {
475
+ writeTransformed(entrySrc, entryDest);
476
+ } else {
477
+ fs.mkdirSync(path.dirname(entryDest), { recursive: true });
478
+ fs.copyFileSync(entrySrc, entryDest);
479
+ console.log(`✅ ${path.relative(PROJECT_ROOT, entryDest)} created`);
480
+ }
481
+ }
482
+ }
483
+
484
+ // Post-copy: materialize shared initialize-escrow files into dialog/form
485
+ try {
486
+ const isSingleReleaseInitRoot =
487
+ name === "escrows/single-release/initialize-escrow";
488
+ const isSingleReleaseInitDialog =
489
+ name === "escrows/single-release/initialize-escrow/dialog";
490
+ const isSingleReleaseInitForm =
491
+ name === "escrows/single-release/initialize-escrow/form";
492
+
493
+ const srcSharedDir = path.join(
494
+ TEMPLATES_DIR,
495
+ "escrows",
496
+ "single-release",
497
+ "initialize-escrow",
498
+ "shared"
499
+ );
500
+
501
+ function copySharedInto(targetDir) {
502
+ if (!fs.existsSync(srcSharedDir)) return;
503
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
504
+ for (const entry of entries) {
505
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
506
+ const entrySrc = path.join(srcSharedDir, entry.name);
507
+ const entryDest = path.join(targetDir, entry.name);
508
+ writeTransformed(entrySrc, entryDest);
509
+ }
510
+ }
511
+
512
+ if (isSingleReleaseInitRoot) {
513
+ copySharedInto(path.join(destDir, "dialog"));
514
+ copySharedInto(path.join(destDir, "form"));
515
+ } else if (isSingleReleaseInitDialog) {
516
+ copySharedInto(destDir);
517
+ } else if (isSingleReleaseInitForm) {
518
+ copySharedInto(destDir);
519
+ }
520
+ } catch (e) {
521
+ console.warn(
522
+ "⚠️ Failed to materialize shared initialize-escrow files:",
523
+ e?.message || e
524
+ );
525
+ }
526
+
527
+ try {
528
+ const isSRRoot = name === "escrows/single-release/approve-milestone";
529
+ const isSRDialog =
530
+ name === "escrows/single-release/approve-milestone/dialog";
531
+ const isSRForm = name === "escrows/single-release/approve-milestone/form";
532
+
533
+ const isMRRoot = name === "escrows/multi-release/approve-milestone";
534
+ const isMRDialog =
535
+ name === "escrows/multi-release/approve-milestone/dialog";
536
+ const isMRForm = name === "escrows/multi-release/approve-milestone/form";
537
+
538
+ const srcSharedDir = path.join(
539
+ TEMPLATES_DIR,
540
+ "escrows",
541
+ "shared",
542
+ "approve-milestone",
543
+ "shared"
544
+ );
545
+
546
+ function copySharedInto(targetDir) {
547
+ if (!fs.existsSync(srcSharedDir)) return;
548
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
549
+ for (const entry of entries) {
550
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
551
+ const entrySrc = path.join(srcSharedDir, entry.name);
552
+ const entryDest = path.join(targetDir, entry.name);
553
+ writeTransformed(entrySrc, entryDest);
554
+ }
555
+ }
556
+
557
+ if (isSRRoot || isMRRoot) {
558
+ copySharedInto(path.join(destDir, "dialog"));
559
+ copySharedInto(path.join(destDir, "form"));
560
+ } else if (isSRDialog || isMRDialog) {
561
+ copySharedInto(destDir);
562
+ } else if (isSRForm || isMRForm) {
563
+ copySharedInto(destDir);
564
+ }
565
+ } catch (e) {
566
+ console.warn(
567
+ "⚠️ Failed to materialize shared approve-milestone files:",
568
+ e?.message || e
569
+ );
570
+ }
571
+
572
+ try {
573
+ const isSingleReleaseInitRoot =
574
+ name === "escrows/single-release/change-milestone-status";
575
+ const isSingleReleaseInitDialog =
576
+ name === "escrows/single-release/change-milestone-status/dialog";
577
+ const isSingleReleaseInitForm =
578
+ name === "escrows/single-release/change-milestone-status/form";
579
+
580
+ const srcSharedDir = path.join(
581
+ TEMPLATES_DIR,
582
+ "escrows",
583
+ "single-release",
584
+ "change-milestone-status",
585
+ "shared"
586
+ );
587
+
588
+ function copySharedInto(targetDir) {
589
+ if (!fs.existsSync(srcSharedDir)) return;
590
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
591
+ for (const entry of entries) {
592
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
593
+ const entrySrc = path.join(srcSharedDir, entry.name);
594
+ const entryDest = path.join(targetDir, entry.name);
595
+ writeTransformed(entrySrc, entryDest);
596
+ }
597
+ }
598
+
599
+ if (isSingleReleaseInitRoot) {
600
+ copySharedInto(path.join(destDir, "dialog"));
601
+ copySharedInto(path.join(destDir, "form"));
602
+ } else if (isSingleReleaseInitDialog) {
603
+ copySharedInto(destDir);
604
+ } else if (isSingleReleaseInitForm) {
605
+ copySharedInto(destDir);
606
+ }
607
+ } catch (e) {
608
+ console.warn(
609
+ "⚠️ Failed to materialize shared change-milestone-status files:",
610
+ e?.message || e
611
+ );
612
+ }
613
+
614
+ try {
615
+ const isSingleReleaseInitRoot =
616
+ name === "escrows/single-release/fund-escrow";
617
+ const isSingleReleaseInitDialog =
618
+ name === "escrows/single-release/fund-escrow/dialog";
619
+ const isSingleReleaseInitForm =
620
+ name === "escrows/single-release/fund-escrow/form";
621
+
622
+ const srcSharedDir = path.join(
623
+ TEMPLATES_DIR,
624
+ "escrows",
625
+ "single-release",
626
+ "fund-escrow",
627
+ "shared"
628
+ );
629
+
630
+ function copySharedInto(targetDir) {
631
+ if (!fs.existsSync(srcSharedDir)) return;
632
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
633
+ for (const entry of entries) {
634
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
635
+ const entrySrc = path.join(srcSharedDir, entry.name);
636
+ const entryDest = path.join(targetDir, entry.name);
637
+ writeTransformed(entrySrc, entryDest);
638
+ }
639
+ }
640
+
641
+ if (isSingleReleaseInitRoot) {
642
+ copySharedInto(path.join(destDir, "dialog"));
643
+ copySharedInto(path.join(destDir, "form"));
644
+ } else if (isSingleReleaseInitDialog) {
645
+ copySharedInto(destDir);
646
+ } else if (isSingleReleaseInitForm) {
647
+ copySharedInto(destDir);
648
+ }
649
+ } catch (e) {
650
+ console.warn(
651
+ "⚠️ Failed to materialize shared fund-escrow files:",
652
+ e?.message || e
653
+ );
654
+ }
655
+
656
+ try {
657
+ const isSingleReleaseInitRoot =
658
+ name === "escrows/single-release/resolve-dispute";
659
+ const isSingleReleaseInitDialog =
660
+ name === "escrows/single-release/resolve-dispute/dialog";
661
+ const isSingleReleaseInitForm =
662
+ name === "escrows/single-release/resolve-dispute/form";
663
+
664
+ const srcSharedDir = path.join(
665
+ TEMPLATES_DIR,
666
+ "escrows",
667
+ "single-release",
668
+ "resolve-dispute",
669
+ "shared"
670
+ );
671
+
672
+ function copySharedInto(targetDir) {
673
+ if (!fs.existsSync(srcSharedDir)) return;
674
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
675
+ for (const entry of entries) {
676
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
677
+ const entrySrc = path.join(srcSharedDir, entry.name);
678
+ const entryDest = path.join(targetDir, entry.name);
679
+ writeTransformed(entrySrc, entryDest);
680
+ }
681
+ }
682
+
683
+ if (isSingleReleaseInitRoot) {
684
+ copySharedInto(path.join(destDir, "dialog"));
685
+ copySharedInto(path.join(destDir, "form"));
686
+ } else if (isSingleReleaseInitDialog) {
687
+ copySharedInto(destDir);
688
+ } else if (isSingleReleaseInitForm) {
689
+ copySharedInto(destDir);
690
+ }
691
+ } catch (e) {
692
+ console.warn(
693
+ "⚠️ Failed to materialize shared resolve-dispute files:",
694
+ e?.message || e
695
+ );
696
+ }
697
+
698
+ try {
699
+ const isSingleReleaseInitRoot =
700
+ name === "escrows/single-release/update-escrow";
701
+ const isSingleReleaseInitDialog =
702
+ name === "escrows/single-release/update-escrow/dialog";
703
+ const isSingleReleaseInitForm =
704
+ name === "escrows/single-release/update-escrow/form";
705
+
706
+ const srcSharedDir = path.join(
707
+ TEMPLATES_DIR,
708
+ "escrows",
709
+ "single-release",
710
+ "update-escrow",
711
+ "shared"
712
+ );
713
+
714
+ function copySharedInto(targetDir) {
715
+ if (!fs.existsSync(srcSharedDir)) return;
716
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
717
+ for (const entry of entries) {
718
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
719
+ const entrySrc = path.join(srcSharedDir, entry.name);
720
+ const entryDest = path.join(targetDir, entry.name);
721
+ writeTransformed(entrySrc, entryDest);
722
+ }
723
+ }
724
+
725
+ if (isSingleReleaseInitRoot) {
726
+ copySharedInto(path.join(destDir, "dialog"));
727
+ copySharedInto(path.join(destDir, "form"));
728
+ } else if (isSingleReleaseInitDialog) {
729
+ copySharedInto(destDir);
730
+ } else if (isSingleReleaseInitForm) {
731
+ copySharedInto(destDir);
732
+ }
733
+ } catch (e) {
734
+ console.warn(
735
+ "⚠️ Failed to materialize shared update-escrow files:",
736
+ e?.message || e
737
+ );
738
+ }
739
+
740
+ // Post-copy: materialize shared files for multi-release modules
741
+ try {
742
+ const isMultiInitRoot =
743
+ name === "escrows/multi-release/initialize-escrow";
744
+ const isMultiInitDialog =
745
+ name === "escrows/multi-release/initialize-escrow/dialog";
746
+ const isMultiInitForm =
747
+ name === "escrows/multi-release/initialize-escrow/form";
748
+
749
+ const srcSharedDir = path.join(
750
+ TEMPLATES_DIR,
751
+ "escrows",
752
+ "multi-release",
753
+ "initialize-escrow",
754
+ "shared"
755
+ );
756
+
757
+ function copyMultiInitSharedInto(targetDir) {
758
+ if (!fs.existsSync(srcSharedDir)) return;
759
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
760
+ for (const entry of entries) {
761
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
762
+ const entrySrc = path.join(srcSharedDir, entry.name);
763
+ const entryDest = path.join(targetDir, entry.name);
764
+ writeTransformed(entrySrc, entryDest);
765
+ }
766
+ }
767
+
768
+ if (isMultiInitRoot) {
769
+ copyMultiInitSharedInto(path.join(destDir, "dialog"));
770
+ copyMultiInitSharedInto(path.join(destDir, "form"));
771
+ } else if (isMultiInitDialog) {
772
+ copyMultiInitSharedInto(destDir);
773
+ } else if (isMultiInitForm) {
774
+ copyMultiInitSharedInto(destDir);
775
+ }
776
+ } catch (e) {
777
+ console.warn(
778
+ "⚠️ Failed to materialize shared multi-release initialize-escrow files:",
779
+ e?.message || e
780
+ );
781
+ }
782
+
783
+ try {
784
+ const isMultiResolveRoot =
785
+ name === "escrows/multi-release/resolve-dispute";
786
+ const isMultiResolveDialog =
787
+ name === "escrows/multi-release/resolve-dispute/dialog";
788
+ const isMultiResolveForm =
789
+ name === "escrows/multi-release/resolve-dispute/form";
790
+
791
+ const srcSharedDir = path.join(
792
+ TEMPLATES_DIR,
793
+ "escrows",
794
+ "multi-release",
795
+ "resolve-dispute",
796
+ "shared"
797
+ );
798
+
799
+ function copyMultiResolveSharedInto(targetDir) {
800
+ if (!fs.existsSync(srcSharedDir)) return;
801
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
802
+ for (const entry of entries) {
803
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
804
+ const entrySrc = path.join(srcSharedDir, entry.name);
805
+ const entryDest = path.join(targetDir, entry.name);
806
+ writeTransformed(entrySrc, entryDest);
807
+ }
808
+ }
809
+
810
+ if (isMultiResolveRoot) {
811
+ copyMultiResolveSharedInto(path.join(destDir, "dialog"));
812
+ copyMultiResolveSharedInto(path.join(destDir, "form"));
813
+ } else if (isMultiResolveDialog) {
814
+ copyMultiResolveSharedInto(destDir);
815
+ } else if (isMultiResolveForm) {
816
+ copyMultiResolveSharedInto(destDir);
817
+ }
818
+ } catch (e) {
819
+ console.warn(
820
+ "⚠️ Failed to materialize shared multi-release resolve-dispute files:",
821
+ e?.message || e
822
+ );
823
+ }
824
+
825
+ // Post-copy: materialize shared files for multi-release withdraw-remaining-funds
826
+ try {
827
+ const isMultiWithdrawRoot =
828
+ name === "escrows/multi-release/withdraw-remaining-funds";
829
+ const isMultiWithdrawDialog =
830
+ name === "escrows/multi-release/withdraw-remaining-funds/dialog";
831
+ const isMultiWithdrawForm =
832
+ name === "escrows/multi-release/withdraw-remaining-funds/form";
833
+
834
+ const srcSharedDir = path.join(
835
+ TEMPLATES_DIR,
836
+ "escrows",
837
+ "multi-release",
838
+ "withdraw-remaining-funds",
839
+ "shared"
840
+ );
841
+
842
+ function copyMultiWithdrawSharedInto(targetDir) {
843
+ if (!fs.existsSync(srcSharedDir)) return;
844
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
845
+ for (const entry of entries) {
846
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
847
+ const entrySrc = path.join(srcSharedDir, entry.name);
848
+ const entryDest = path.join(targetDir, entry.name);
849
+ writeTransformed(entrySrc, entryDest);
850
+ }
851
+ }
852
+
853
+ if (isMultiWithdrawRoot) {
854
+ copyMultiWithdrawSharedInto(path.join(destDir, "dialog"));
855
+ copyMultiWithdrawSharedInto(path.join(destDir, "form"));
856
+ } else if (isMultiWithdrawDialog) {
857
+ copyMultiWithdrawSharedInto(destDir);
858
+ } else if (isMultiWithdrawForm) {
859
+ copyMultiWithdrawSharedInto(destDir);
860
+ }
861
+ } catch (e) {
862
+ console.warn(
863
+ "⚠️ Failed to materialize shared multi-release withdraw-remaining-funds files:",
864
+ e?.message || e
865
+ );
866
+ }
867
+
868
+ try {
869
+ const isMultiUpdateRoot = name === "escrows/multi-release/update-escrow";
870
+ const isMultiUpdateDialog =
871
+ name === "escrows/multi-release/update-escrow/dialog";
872
+ const isMultiUpdateForm =
873
+ name === "escrows/multi-release/update-escrow/form";
874
+
875
+ const srcSharedDir = path.join(
876
+ TEMPLATES_DIR,
877
+ "escrows",
878
+ "multi-release",
879
+ "update-escrow",
880
+ "shared"
881
+ );
882
+
883
+ function copyMultiUpdateSharedInto(targetDir) {
884
+ if (!fs.existsSync(srcSharedDir)) return;
885
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
886
+ for (const entry of entries) {
887
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
888
+ const entrySrc = path.join(srcSharedDir, entry.name);
889
+ const entryDest = path.join(targetDir, entry.name);
890
+ writeTransformed(entrySrc, entryDest);
891
+ }
892
+ }
893
+
894
+ if (isMultiUpdateRoot) {
895
+ copyMultiUpdateSharedInto(path.join(destDir, "dialog"));
896
+ copyMultiUpdateSharedInto(path.join(destDir, "form"));
897
+ } else if (isMultiUpdateDialog) {
898
+ copyMultiUpdateSharedInto(destDir);
899
+ } else if (isMultiUpdateForm) {
900
+ copyMultiUpdateSharedInto(destDir);
901
+ }
902
+ } catch (e) {
903
+ console.warn(
904
+ "⚠️ Failed to materialize shared multi-release update-escrow files:",
905
+ e?.message || e
906
+ );
907
+ }
908
+
909
+ // Post-copy: materialize shared files for single-multi-release modules
910
+ try {
911
+ const isSingleMultiApproveRoot =
912
+ name === "escrows/single-multi-release/approve-milestone";
913
+ const isSingleMultiApproveDialog =
914
+ name === "escrows/single-multi-release/approve-milestone/dialog";
915
+ const isSingleMultiApproveForm =
916
+ name === "escrows/single-multi-release/approve-milestone/form";
917
+
918
+ const srcSharedDir = path.join(
919
+ TEMPLATES_DIR,
920
+ "escrows",
921
+ "single-multi-release",
922
+ "approve-milestone",
923
+ "shared"
924
+ );
925
+
926
+ function copySingleMultiApproveSharedInto(targetDir) {
927
+ if (!fs.existsSync(srcSharedDir)) return;
928
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
929
+ for (const entry of entries) {
930
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
931
+ const entrySrc = path.join(srcSharedDir, entry.name);
932
+ const entryDest = path.join(targetDir, entry.name);
933
+ writeTransformed(entrySrc, entryDest);
934
+ }
935
+ }
936
+
937
+ if (isSingleMultiApproveRoot) {
938
+ copySingleMultiApproveSharedInto(path.join(destDir, "dialog"));
939
+ copySingleMultiApproveSharedInto(path.join(destDir, "form"));
940
+ } else if (isSingleMultiApproveDialog) {
941
+ copySingleMultiApproveSharedInto(destDir);
942
+ } else if (isSingleMultiApproveForm) {
943
+ copySingleMultiApproveSharedInto(destDir);
944
+ }
945
+ } catch (e) {
946
+ console.warn(
947
+ "⚠️ Failed to materialize shared single-multi-release approve-milestone files:",
948
+ e?.message || e
949
+ );
950
+ }
951
+
952
+ try {
953
+ const isSingleMultiChangeRoot =
954
+ name === "escrows/single-multi-release/change-milestone-status";
955
+ const isSingleMultiChangeDialog =
956
+ name === "escrows/single-multi-release/change-milestone-status/dialog";
957
+ const isSingleMultiChangeForm =
958
+ name === "escrows/single-multi-release/change-milestone-status/form";
959
+
960
+ const srcSharedDir = path.join(
961
+ TEMPLATES_DIR,
962
+ "escrows",
963
+ "single-multi-release",
964
+ "change-milestone-status",
965
+ "shared"
966
+ );
967
+
968
+ function copySingleMultiChangeSharedInto(targetDir) {
969
+ if (!fs.existsSync(srcSharedDir)) return;
970
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
971
+ for (const entry of entries) {
972
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
973
+ const entrySrc = path.join(srcSharedDir, entry.name);
974
+ const entryDest = path.join(targetDir, entry.name);
975
+ writeTransformed(entrySrc, entryDest);
976
+ }
977
+ }
978
+
979
+ if (isSingleMultiChangeRoot) {
980
+ copySingleMultiChangeSharedInto(path.join(destDir, "dialog"));
981
+ copySingleMultiChangeSharedInto(path.join(destDir, "form"));
982
+ } else if (isSingleMultiChangeDialog) {
983
+ copySingleMultiChangeSharedInto(destDir);
984
+ } else if (isSingleMultiChangeForm) {
985
+ copySingleMultiChangeSharedInto(destDir);
986
+ }
987
+ } catch (e) {
988
+ console.warn(
989
+ "⚠️ Failed to materialize shared single-multi-release change-milestone-status files:",
990
+ e?.message || e
991
+ );
992
+ }
993
+
994
+ try {
995
+ const isSingleMultiFundRoot =
996
+ name === "escrows/single-multi-release/fund-escrow";
997
+ const isSingleMultiFundDialog =
998
+ name === "escrows/single-multi-release/fund-escrow/dialog";
999
+ const isSingleMultiFundForm =
1000
+ name === "escrows/single-multi-release/fund-escrow/form";
1001
+
1002
+ const srcSharedDir = path.join(
1003
+ TEMPLATES_DIR,
1004
+ "escrows",
1005
+ "single-multi-release",
1006
+ "fund-escrow",
1007
+ "shared"
1008
+ );
1009
+
1010
+ function copySingleMultiFundSharedInto(targetDir) {
1011
+ if (!fs.existsSync(srcSharedDir)) return;
1012
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
1013
+ for (const entry of entries) {
1014
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
1015
+ const entrySrc = path.join(srcSharedDir, entry.name);
1016
+ const entryDest = path.join(targetDir, entry.name);
1017
+ writeTransformed(entrySrc, entryDest);
1018
+ }
1019
+ }
1020
+
1021
+ if (isSingleMultiFundRoot) {
1022
+ copySingleMultiFundSharedInto(path.join(destDir, "dialog"));
1023
+ copySingleMultiFundSharedInto(path.join(destDir, "form"));
1024
+ } else if (isSingleMultiFundDialog) {
1025
+ copySingleMultiFundSharedInto(destDir);
1026
+ } else if (isSingleMultiFundForm) {
1027
+ copySingleMultiFundSharedInto(destDir);
1028
+ }
1029
+ } catch (e) {
1030
+ console.warn(
1031
+ "⚠️ Failed to materialize shared single-multi-release fund-escrow files:",
1032
+ e?.message || e
1033
+ );
1034
+ }
1035
+
1036
+ try {
1037
+ const isLoadEscrowRoot = name === "escrows/load-escrow";
1038
+ const isLoadEscrowDialog = name === "escrows/load-escrow/dialog";
1039
+ const isLoadEscrowForm = name === "escrows/load-escrow/form";
1040
+
1041
+ const srcSharedDir = path.join(
1042
+ TEMPLATES_DIR,
1043
+ "escrows",
1044
+ "load-escrow",
1045
+ "shared"
1046
+ );
1047
+
1048
+ function copyLoadEscrowSharedInto(targetDir) {
1049
+ if (!fs.existsSync(srcSharedDir)) return;
1050
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
1051
+ for (const entry of entries) {
1052
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
1053
+ const entrySrc = path.join(srcSharedDir, entry.name);
1054
+ const entryDest = path.join(targetDir, entry.name);
1055
+ writeTransformed(entrySrc, entryDest);
1056
+ }
1057
+ }
1058
+
1059
+ if (isLoadEscrowRoot) {
1060
+ copyLoadEscrowSharedInto(path.join(destDir, "dialog"));
1061
+ copyLoadEscrowSharedInto(path.join(destDir, "form"));
1062
+ } else if (isLoadEscrowDialog) {
1063
+ copyLoadEscrowSharedInto(destDir);
1064
+ } else if (isLoadEscrowForm) {
1065
+ copyLoadEscrowSharedInto(destDir);
1066
+ }
1067
+ } catch (e) {
1068
+ console.warn(
1069
+ "⚠️ Failed to materialize shared load-escrow files:",
1070
+ e?.message || e
1071
+ );
1072
+ }
1073
+
1074
+ // If adding the whole single-release bundle, materialize all shared files
1075
+ try {
1076
+ if (name === "escrows/single-release") {
1077
+ const modules = [
1078
+ "initialize-escrow",
1079
+ "approve-milestone",
1080
+ "change-milestone-status",
1081
+ "fund-escrow",
1082
+ "resolve-dispute",
1083
+ "update-escrow",
1084
+ ];
1085
+
1086
+ for (const mod of modules) {
1087
+ const srcSharedDir = path.join(
1088
+ TEMPLATES_DIR,
1089
+ "escrows",
1090
+ mod === "approve-milestone" ? "shared" : "single-release",
1091
+ mod === "approve-milestone" ? "approve-milestone" : mod,
1092
+ "shared"
1093
+ );
1094
+ if (!fs.existsSync(srcSharedDir)) continue;
1095
+
1096
+ const targets = [
1097
+ path.join(destDir, mod, "dialog"),
1098
+ path.join(destDir, mod, "form"),
1099
+ ];
1100
+
1101
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
1102
+ for (const entry of entries) {
1103
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
1104
+ const entrySrc = path.join(srcSharedDir, entry.name);
1105
+ for (const t of targets) {
1106
+ const entryDest = path.join(t, entry.name);
1107
+ writeTransformed(entrySrc, entryDest);
1108
+ }
1109
+ }
1110
+ }
1111
+ }
1112
+ } catch (e) {
1113
+ console.warn(
1114
+ "⚠️ Failed to materialize shared files for single-release bundle:",
1115
+ e?.message || e
1116
+ );
1117
+ }
1118
+
1119
+ // If adding the whole multi-release bundle, materialize all shared files
1120
+ try {
1121
+ if (name === "escrows/multi-release") {
1122
+ const modules = [
1123
+ "initialize-escrow",
1124
+ "resolve-dispute",
1125
+ "update-escrow",
1126
+ "withdraw-remaining-funds",
1127
+ ];
1128
+
1129
+ for (const mod of modules) {
1130
+ const srcSharedDir = path.join(
1131
+ TEMPLATES_DIR,
1132
+ "escrows",
1133
+ "multi-release",
1134
+ mod,
1135
+ "shared"
1136
+ );
1137
+ if (!fs.existsSync(srcSharedDir)) continue;
1138
+
1139
+ const targets = [
1140
+ path.join(destDir, mod, "dialog"),
1141
+ path.join(destDir, mod, "form"),
1142
+ ];
1143
+
1144
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
1145
+ for (const entry of entries) {
1146
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
1147
+ const entrySrc = path.join(srcSharedDir, entry.name);
1148
+ for (const t of targets) {
1149
+ const entryDest = path.join(t, entry.name);
1150
+ writeTransformed(entrySrc, entryDest);
1151
+ }
1152
+ }
1153
+ }
1154
+ }
1155
+ } catch (e) {
1156
+ console.warn(
1157
+ "⚠️ Failed to materialize shared files for multi-release bundle:",
1158
+ e?.message || e
1159
+ );
1160
+ }
1161
+
1162
+ // If adding the whole single-multi-release bundle, materialize all shared files
1163
+ try {
1164
+ if (name === "escrows/single-multi-release") {
1165
+ const modules = [
1166
+ "approve-milestone",
1167
+ "change-milestone-status",
1168
+ "fund-escrow",
1169
+ ];
1170
+
1171
+ for (const mod of modules) {
1172
+ const srcSharedDir = path.join(
1173
+ TEMPLATES_DIR,
1174
+ "escrows",
1175
+ "single-multi-release",
1176
+ mod,
1177
+ "shared"
1178
+ );
1179
+ if (!fs.existsSync(srcSharedDir)) continue;
1180
+
1181
+ const targets = [
1182
+ path.join(destDir, mod, "dialog"),
1183
+ path.join(destDir, mod, "form"),
1184
+ ];
1185
+
1186
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
1187
+ for (const entry of entries) {
1188
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
1189
+ const entrySrc = path.join(srcSharedDir, entry.name);
1190
+ for (const t of targets) {
1191
+ const entryDest = path.join(t, entry.name);
1192
+ writeTransformed(entrySrc, entryDest);
1193
+ }
1194
+ }
1195
+ }
1196
+ }
1197
+ } catch (e) {
1198
+ console.warn(
1199
+ "⚠️ Failed to materialize shared files for single-multi-release bundle:",
1200
+ e?.message || e
1201
+ );
1202
+ }
1203
+
1204
+ // If adding the root escrows bundle, also materialize single-release shared files
1205
+ try {
1206
+ if (name === "escrows") {
1207
+ const modules = [
1208
+ "initialize-escrow",
1209
+ "approve-milestone",
1210
+ "change-milestone-status",
1211
+ "fund-escrow",
1212
+ "resolve-dispute",
1213
+ "update-escrow",
1214
+ ];
1215
+
1216
+ const baseTarget = path.join(destDir, "single-release");
1217
+ for (const mod of modules) {
1218
+ const srcSharedDir = path.join(
1219
+ TEMPLATES_DIR,
1220
+ "escrows",
1221
+ mod === "approve-milestone" ? "shared" : "single-release",
1222
+ mod === "approve-milestone" ? "approve-milestone" : mod,
1223
+ "shared"
1224
+ );
1225
+ if (!fs.existsSync(srcSharedDir)) continue;
1226
+
1227
+ const targets = [
1228
+ path.join(baseTarget, mod, "dialog"),
1229
+ path.join(baseTarget, mod, "form"),
1230
+ ];
1231
+
1232
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
1233
+ for (const entry of entries) {
1234
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
1235
+ const entrySrc = path.join(srcSharedDir, entry.name);
1236
+ for (const t of targets) {
1237
+ const entryDest = path.join(t, entry.name);
1238
+ writeTransformed(entrySrc, entryDest);
1239
+ }
1240
+ }
1241
+ }
1242
+ }
1243
+ } catch (e) {
1244
+ console.warn(
1245
+ "⚠️ Failed to materialize shared files for escrows root:",
1246
+ e?.message || e
1247
+ );
1248
+ }
1249
+
1250
+ // If adding the root escrows bundle, also materialize multi-release shared files
1251
+ try {
1252
+ if (name === "escrows") {
1253
+ const modules = [
1254
+ "initialize-escrow",
1255
+ "resolve-dispute",
1256
+ "update-escrow",
1257
+ "withdraw-remaining-funds",
1258
+ ];
1259
+
1260
+ const baseTarget = path.join(destDir, "multi-release");
1261
+ for (const mod of modules) {
1262
+ const srcSharedDir = path.join(
1263
+ TEMPLATES_DIR,
1264
+ "escrows",
1265
+ "multi-release",
1266
+ mod,
1267
+ "shared"
1268
+ );
1269
+ if (!fs.existsSync(srcSharedDir)) continue;
1270
+
1271
+ const targets = [
1272
+ path.join(baseTarget, mod, "dialog"),
1273
+ path.join(baseTarget, mod, "form"),
1274
+ ];
1275
+
1276
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
1277
+ for (const entry of entries) {
1278
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
1279
+ const entrySrc = path.join(srcSharedDir, entry.name);
1280
+ for (const t of targets) {
1281
+ const entryDest = path.join(t, entry.name);
1282
+ writeTransformed(entrySrc, entryDest);
1283
+ }
1284
+ }
1285
+ }
1286
+ }
1287
+ } catch (e) {
1288
+ console.warn(
1289
+ "⚠️ Failed to materialize shared files for escrows root (multi-release):",
1290
+ e?.message || e
1291
+ );
1292
+ }
1293
+
1294
+ // If adding the root escrows bundle, also materialize single-multi-release shared files
1295
+ try {
1296
+ if (name === "escrows") {
1297
+ const modules = [
1298
+ "approve-milestone",
1299
+ "change-milestone-status",
1300
+ "fund-escrow",
1301
+ ];
1302
+
1303
+ const baseTarget = path.join(destDir, "single-multi-release");
1304
+ for (const mod of modules) {
1305
+ const srcSharedDir = path.join(
1306
+ TEMPLATES_DIR,
1307
+ "escrows",
1308
+ "single-multi-release",
1309
+ mod,
1310
+ "shared"
1311
+ );
1312
+ if (!fs.existsSync(srcSharedDir)) continue;
1313
+
1314
+ const targets = [
1315
+ path.join(baseTarget, mod, "dialog"),
1316
+ path.join(baseTarget, mod, "form"),
1317
+ ];
1318
+
1319
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
1320
+ for (const entry of entries) {
1321
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
1322
+ const entrySrc = path.join(srcSharedDir, entry.name);
1323
+ for (const t of targets) {
1324
+ const entryDest = path.join(t, entry.name);
1325
+ writeTransformed(entrySrc, entryDest);
1326
+ }
1327
+ }
1328
+ }
1329
+ }
1330
+ } catch (e) {
1331
+ console.warn(
1332
+ "⚠️ Failed to materialize shared files for escrows root (single-multi-release):",
1333
+ e?.message || e
1334
+ );
1335
+ }
1336
+
1337
+ // If adding the root escrows bundle, also materialize load-escrow shared files
1338
+ try {
1339
+ if (name === "escrows") {
1340
+ const srcSharedDir = path.join(
1341
+ TEMPLATES_DIR,
1342
+ "escrows",
1343
+ "load-escrow",
1344
+ "shared"
1345
+ );
1346
+ if (fs.existsSync(srcSharedDir)) {
1347
+ const targets = [
1348
+ path.join(destDir, "load-escrow", "dialog"),
1349
+ path.join(destDir, "load-escrow", "form"),
1350
+ ];
1351
+
1352
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
1353
+ for (const entry of entries) {
1354
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
1355
+ const entrySrc = path.join(srcSharedDir, entry.name);
1356
+ for (const t of targets) {
1357
+ const entryDest = path.join(t, entry.name);
1358
+ writeTransformed(entrySrc, entryDest);
1359
+ }
1360
+ }
1361
+ }
1362
+ }
1363
+ } catch (e) {
1364
+ console.warn(
1365
+ "⚠️ Failed to materialize shared files for escrows root (load-escrow):",
1366
+ e?.message || e
1367
+ );
1368
+ }
1369
+ } else if (fs.existsSync(srcFile)) {
1370
+ fs.mkdirSync(outRoot, { recursive: true });
1371
+ const destFile = path.join(outRoot, name + ".tsx");
1372
+ writeTransformed(srcFile, destFile);
1373
+ } else {
1374
+ console.error(`❌ The template "${name}" does not exist`);
1375
+ process.exit(1);
1376
+ }
1377
+
1378
+ if (shouldInstall && fs.existsSync(GLOBAL_DEPS_FILE)) {
1379
+ const meta = JSON.parse(fs.readFileSync(GLOBAL_DEPS_FILE, "utf8"));
1380
+ installDeps(meta);
1381
+ // Install @creit-tech/stellar-wallets-kit using jsr after all other deps are installed
1382
+ if (meta.dependencies && meta.dependencies["@creit-tech/stellar-wallets-kit"]) {
1383
+ run("npx", ["jsr", "add", "@creit-tech/stellar-wallets-kit"]);
1384
+ }
1385
+ }
1386
+ }
1387
+
1388
+ function copySharedDetailsInto(targetRelativeDir, { uiBase } = {}) {
1389
+ const srcDir = path.join(TEMPLATES_DIR, "escrows", "details");
1390
+ const componentsSrcDir = path.join(PROJECT_ROOT, "src", "components");
1391
+ const outRoot = fs.existsSync(componentsSrcDir)
1392
+ ? path.join(componentsSrcDir, "tw-blocks")
1393
+ : path.join(PROJECT_ROOT, "components", "tw-blocks");
1394
+ const destDir = path.join(outRoot, targetRelativeDir);
1395
+ const config = loadConfig();
1396
+ const effectiveUiBase = uiBase || config.uiBase || "@/components/ui";
1397
+
1398
+ if (!fs.existsSync(srcDir)) return;
1399
+ fs.mkdirSync(destDir, { recursive: true });
1400
+
1401
+ function writeTransformed(srcPath, destPath) {
1402
+ const raw = fs.readFileSync(srcPath, "utf8");
1403
+ let transformed = raw.replaceAll("__UI_BASE__", effectiveUiBase);
1404
+ // Resolve details placeholders to either multi-release modules (if present) or local compat
1405
+ const resolveImport = (segments, compatFile) => {
1406
+ const realWithExt = path.join(
1407
+ outRoot,
1408
+ "escrows",
1409
+ "multi-release",
1410
+ ...segments
1411
+ );
1412
+ const realCandidate = [
1413
+ realWithExt,
1414
+ realWithExt + ".tsx",
1415
+ realWithExt + ".ts",
1416
+ realWithExt + ".jsx",
1417
+ realWithExt + ".js",
1418
+ ].find((p) => fs.existsSync(p));
1419
+ const realNoExt = realCandidate
1420
+ ? realCandidate.replace(/\.(tsx|ts|jsx|js)$/i, "")
1421
+ : null;
1422
+ const compatWithExt = path.join(
1423
+ path.dirname(destPath),
1424
+ "compat",
1425
+ compatFile
1426
+ );
1427
+ const compatCandidate = [
1428
+ compatWithExt,
1429
+ compatWithExt + ".tsx",
1430
+ compatWithExt + ".ts",
1431
+ compatWithExt + ".jsx",
1432
+ compatWithExt + ".js",
1433
+ ].find((p) => fs.existsSync(p));
1434
+ const compatNoExt = (compatCandidate || compatWithExt).replace(
1435
+ /\.(tsx|ts|jsx|js)$/i,
1436
+ ""
1437
+ );
1438
+ const target = realNoExt || compatNoExt;
1439
+ let rel = path.relative(path.dirname(destPath), target);
1440
+ rel = rel.split(path.sep).join("/");
1441
+ if (!rel.startsWith(".")) rel = "./" + rel;
1442
+ return rel;
1443
+ };
1444
+ transformed = transformed
1445
+ .replaceAll(
1446
+ "__MR_RELEASE_MODULE__",
1447
+ resolveImport(
1448
+ ["release-escrow", "button", "ReleaseEscrow"],
1449
+ "ReleaseEscrow"
1450
+ )
1451
+ )
1452
+ .replaceAll(
1453
+ "__MR_DISPUTE_MODULE__",
1454
+ resolveImport(
1455
+ ["dispute-escrow", "button", "DisputeEscrow"],
1456
+ "DisputeEscrow"
1457
+ )
1458
+ )
1459
+ .replaceAll(
1460
+ "__MR_RESOLVE_MODULE__",
1461
+ resolveImport(
1462
+ ["resolve-dispute", "dialog", "ResolveDispute"],
1463
+ "ResolveDispute"
1464
+ )
1465
+ );
1466
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
1467
+ fs.writeFileSync(destPath, transformed, "utf8");
1468
+ console.log(`✅ ${path.relative(PROJECT_ROOT, destPath)} created`);
1469
+ }
1470
+
1471
+ const stack = [""];
1472
+ while (stack.length) {
1473
+ const rel = stack.pop();
1474
+ const current = path.join(srcDir, rel);
1475
+ const entries = fs.readdirSync(current, { withFileTypes: true });
1476
+ for (const entry of entries) {
1477
+ const entryRel = path.join(rel, entry.name);
1478
+ const entrySrc = path.join(srcDir, entryRel);
1479
+ const entryDest = path.join(destDir, entryRel);
1480
+ if (entry.isDirectory()) {
1481
+ stack.push(entryRel);
1482
+ continue;
1483
+ }
1484
+ if (/\.(tsx?|jsx?)$/i.test(entry.name)) {
1485
+ writeTransformed(entrySrc, entryDest);
1486
+ } else {
1487
+ fs.mkdirSync(path.dirname(entryDest), { recursive: true });
1488
+ fs.copyFileSync(entrySrc, entryDest);
1489
+ console.log(`✅ ${path.relative(PROJECT_ROOT, entryDest)} created`);
1490
+ }
1491
+ }
1492
+ }
1493
+ }
1494
+
1495
+ function copySharedRoleSignerHooks(kind = "both") {
1496
+ const componentsSrcDir = path.join(PROJECT_ROOT, "src", "components");
1497
+ const outRoot = fs.existsSync(componentsSrcDir)
1498
+ ? path.join(componentsSrcDir, "tw-blocks")
1499
+ : path.join(PROJECT_ROOT, "components", "tw-blocks");
1500
+
1501
+ const mappings = [];
1502
+ if (kind === "both" || kind === "role") {
1503
+ mappings.push({
1504
+ src: path.join(
1505
+ TEMPLATES_DIR,
1506
+ "escrows",
1507
+ "escrows-by-role",
1508
+ "useEscrowsByRole.shared.ts"
1509
+ ),
1510
+ dest: path.join(
1511
+ outRoot,
1512
+ "escrows",
1513
+ "escrows-by-role",
1514
+ "useEscrowsByRole.shared.ts"
1515
+ ),
1516
+ });
1517
+ }
1518
+ if (kind === "both" || kind === "signer") {
1519
+ mappings.push({
1520
+ src: path.join(
1521
+ TEMPLATES_DIR,
1522
+ "escrows",
1523
+ "escrows-by-signer",
1524
+ "useEscrowsBySigner.shared.ts"
1525
+ ),
1526
+ dest: path.join(
1527
+ outRoot,
1528
+ "escrows",
1529
+ "escrows-by-signer",
1530
+ "useEscrowsBySigner.shared.ts"
1531
+ ),
1532
+ });
1533
+ }
1534
+
1535
+ for (const { src, dest } of mappings) {
1536
+ if (!fs.existsSync(src)) continue;
1537
+ const raw = fs.readFileSync(src, "utf8");
1538
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
1539
+ fs.writeFileSync(dest, raw, "utf8");
1540
+ console.log(`✅ ${path.relative(PROJECT_ROOT, dest)} created`);
1541
+ }
1542
+ }
1543
+
1544
+ function findLayoutFile() {
1545
+ const candidates = [
1546
+ path.join(PROJECT_ROOT, "app", "layout.tsx"),
1547
+ path.join(PROJECT_ROOT, "app", "layout.ts"),
1548
+ path.join(PROJECT_ROOT, "app", "layout.jsx"),
1549
+ path.join(PROJECT_ROOT, "app", "layout.js"),
1550
+ path.join(PROJECT_ROOT, "src", "app", "layout.tsx"),
1551
+ path.join(PROJECT_ROOT, "src", "app", "layout.ts"),
1552
+ path.join(PROJECT_ROOT, "src", "app", "layout.jsx"),
1553
+ path.join(PROJECT_ROOT, "src", "app", "layout.js"),
1554
+ ];
1555
+ return candidates.find((p) => fs.existsSync(p)) || null;
1556
+ }
1557
+
1558
+ function injectProvidersIntoLayout(
1559
+ layoutPath,
1560
+ { reactQuery = false, trustless = false, wallet = false, escrow = false } = {}
1561
+ ) {
1562
+ try {
1563
+ let content = fs.readFileSync(layoutPath, "utf8");
1564
+
1565
+ const importRQ =
1566
+ 'import { ReactQueryClientProvider } from "@/components/tw-blocks/providers/ReactQueryClientProvider";\n';
1567
+ const importTW =
1568
+ 'import { TrustlessWorkProvider } from "@/components/tw-blocks/providers/TrustlessWork";\n';
1569
+ const importEscrow =
1570
+ 'import { EscrowProvider } from "@/components/tw-blocks/providers/EscrowProvider";\n';
1571
+ const importWallet =
1572
+ 'import { WalletProvider } from "@/components/tw-blocks/wallet-kit/WalletProvider";\n';
1573
+ const commentText =
1574
+ "// Use these imports to wrap your application (<ReactQueryClientProvider>, <TrustlessWorkProvider>, <WalletProvider> y <EscrowProvider>)\n";
1575
+
1576
+ const needImport = (name) =>
1577
+ !new RegExp(
1578
+ `import\\s+[^;]*${name}[^;]*from\\s+['\"][^'\"]+['\"];?`
1579
+ ).test(content);
1580
+
1581
+ let importsToAdd = "";
1582
+ if (reactQuery && needImport("ReactQueryClientProvider"))
1583
+ importsToAdd += importRQ;
1584
+ if (trustless && needImport("TrustlessWorkProvider"))
1585
+ importsToAdd += importTW;
1586
+ if (wallet && needImport("WalletProvider")) importsToAdd += importWallet;
1587
+ if (escrow && needImport("EscrowProvider")) importsToAdd += importEscrow;
1588
+
1589
+ if (importsToAdd) {
1590
+ const importStmtRegex = /^import.*;\s*$/gm;
1591
+ let last = null;
1592
+ for (const m of content.matchAll(importStmtRegex)) last = m;
1593
+ if (last) {
1594
+ const idx = last.index + last[0].length;
1595
+ content =
1596
+ content.slice(0, idx) +
1597
+ "\n" +
1598
+ importsToAdd +
1599
+ commentText +
1600
+ content.slice(idx);
1601
+ } else {
1602
+ content = importsToAdd + commentText + content;
1603
+ }
1604
+ }
1605
+
1606
+ const hasTag = (tag) => new RegExp(`<${tag}[\\s>]`).test(content);
1607
+ const wrapInside = (containerTag, newTag) => {
1608
+ const open = content.match(new RegExp(`<${containerTag}(\\s[^>]*)?>`));
1609
+ if (!open) return false;
1610
+ const openIdx = open.index + open[0].length;
1611
+ const closeIdx = content.indexOf(`</${containerTag}>`, openIdx);
1612
+ if (closeIdx === -1) return false;
1613
+ content =
1614
+ content.slice(0, openIdx) +
1615
+ `\n<${newTag}>\n` +
1616
+ content.slice(openIdx, closeIdx) +
1617
+ `\n</${newTag}>\n` +
1618
+ content.slice(closeIdx);
1619
+ return true;
1620
+ };
1621
+
1622
+ const ensureTag = (tag) => {
1623
+ if (hasTag(tag)) return;
1624
+ const bodyOpen = content.match(/<body[^>]*>/);
1625
+ const bodyCloseIdx = content.lastIndexOf("</body>");
1626
+ if (!bodyOpen || bodyCloseIdx === -1) return;
1627
+ const bodyOpenIdx = bodyOpen.index + bodyOpen[0].length;
1628
+ if (tag === "TrustlessWorkProvider") {
1629
+ if (wrapInside("ReactQueryClientProvider", tag)) return;
1630
+ }
1631
+ if (tag === "WalletProvider") {
1632
+ if (wrapInside("TrustlessWorkProvider", tag)) return;
1633
+ if (wrapInside("ReactQueryClientProvider", tag)) return;
1634
+ }
1635
+ if (tag === "EscrowProvider") {
1636
+ if (wrapInside("WalletProvider", tag)) return;
1637
+ if (wrapInside("TrustlessWorkProvider", tag)) return;
1638
+ if (wrapInside("ReactQueryClientProvider", tag)) return;
1639
+ }
1640
+ content =
1641
+ content.slice(0, bodyOpenIdx) +
1642
+ `\n<${tag}>\n` +
1643
+ content.slice(bodyOpenIdx, bodyCloseIdx) +
1644
+ `\n</${tag}>\n` +
1645
+ content.slice(bodyCloseIdx);
1646
+ };
1647
+
1648
+ if (reactQuery) ensureTag("ReactQueryClientProvider");
1649
+ if (trustless) ensureTag("TrustlessWorkProvider");
1650
+ if (wallet) ensureTag("WalletProvider");
1651
+ if (escrow) ensureTag("EscrowProvider");
1652
+
1653
+ fs.writeFileSync(layoutPath, content, "utf8");
1654
+ logCheck(
1655
+ `Updated ${path.relative(PROJECT_ROOT, layoutPath)} with providers`
1656
+ );
1657
+ } catch (e) {
1658
+ console.error("❌ Failed to update layout with providers:", e.message);
1659
+ }
1660
+ }
1661
+
1662
+ function cleanJsrDeps() {
1663
+ const pkgPath = path.join(PROJECT_ROOT, "package.json");
1664
+ if (!fs.existsSync(pkgPath)) return;
1665
+ try {
1666
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
1667
+ let modified = false;
1668
+ const target = "@creit-tech/stellar-wallets-kit";
1669
+ if (pkg.dependencies && pkg.dependencies[target]) {
1670
+ delete pkg.dependencies[target];
1671
+ modified = true;
1672
+ }
1673
+ if (pkg.devDependencies && pkg.devDependencies[target]) {
1674
+ delete pkg.devDependencies[target];
1675
+ modified = true;
1676
+ }
1677
+ if (modified) {
1678
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2), "utf8");
1679
+ }
1680
+ } catch (e) {}
1681
+ }
1682
+
1683
+ if (args[0] === "init") {
1684
+ cleanJsrDeps();
1685
+ console.log("\n▶ Setting up shadcn/ui components...");
1686
+ const doInit = await promptYesNo("Run shadcn init now?", true);
1687
+ if (doInit) {
1688
+ run("npx", ["shadcn@latest", "init"]);
1689
+ } else {
1690
+ console.log("\x1b[90m– Skipped shadcn init\x1b[0m");
1691
+ }
1692
+
1693
+ const addShadcn = await promptYesNo(
1694
+ "Add shadcn components (button, input, form, card, sonner, checkbox, dialog, textarea, sonner, select, table, calendar, popover, separator, calendar-05, badge, sheet, tabs, avatar, tooltip, progress, chart, empty)?",
1695
+ true
1696
+ );
1697
+ if (addShadcn) {
1698
+ await withSpinner("Installing shadcn/ui components", async () => {
1699
+ await runAsync("npx", [
1700
+ "shadcn@latest",
1701
+ "add",
1702
+ "button",
1703
+ "input",
1704
+ "form",
1705
+ "card",
1706
+ "sonner",
1707
+ "checkbox",
1708
+ "dialog",
1709
+ "textarea",
1710
+ "sonner",
1711
+ "select",
1712
+ "table",
1713
+ "calendar",
1714
+ "popover",
1715
+ "separator",
1716
+ "calendar-05",
1717
+ "badge",
1718
+ "sheet",
1719
+ "tabs",
1720
+ "avatar",
1721
+ "tooltip",
1722
+ "progress",
1723
+ "chart",
1724
+ "empty",
1725
+ ]);
1726
+ });
1727
+ } else {
1728
+ console.log("\x1b[90m– Skipped adding shadcn components\x1b[0m");
1729
+ }
1730
+
1731
+ if (!fs.existsSync(GLOBAL_DEPS_FILE)) {
1732
+ console.error("❌ deps.json not found in templates/");
1733
+ process.exit(1);
1734
+ }
1735
+ const meta = JSON.parse(fs.readFileSync(GLOBAL_DEPS_FILE, "utf8"));
1736
+ const installLibs = await promptYesNo(
1737
+ "Install (react-hook-form, @tanstack/react-query, @tanstack/react-query-devtools, @trustless-work/escrow, @hookform/resolvers, axios, @creit-tech/stellar-wallets-kit, react-day-picker, recharts & zod) dependencies now?",
1738
+ true
1739
+ );
1740
+ if (installLibs) {
1741
+ await withSpinner("Installing required dependencies", async () => {
1742
+ installDeps(meta);
1743
+ });
1744
+ // Install @creit-tech/stellar-wallets-kit using jsr after all other deps are installed
1745
+ if (meta.dependencies && meta.dependencies["@creit-tech/stellar-wallets-kit"]) {
1746
+ await withSpinner("Installing @creit-tech/stellar-wallets-kit from JSR", async () => {
1747
+ run("npx", ["jsr", "add", "@creit-tech/stellar-wallets-kit"]);
1748
+ });
1749
+ }
1750
+ } else {
1751
+ console.log("\x1b[90m– Skipped installing required dependencies\x1b[0m");
1752
+ }
1753
+ const cfgPath = path.join(PROJECT_ROOT, ".twblocks.json");
1754
+ if (!fs.existsSync(cfgPath)) {
1755
+ fs.writeFileSync(
1756
+ cfgPath,
1757
+ JSON.stringify({ uiBase: "@/components/ui" }, null, 2)
1758
+ );
1759
+ console.log(
1760
+ `\x1b[32m✔\x1b[0m Created ${path.relative(
1761
+ PROJECT_ROOT,
1762
+ cfgPath
1763
+ )} with default uiBase`
1764
+ );
1765
+ }
1766
+ console.log("\x1b[32m✔\x1b[0m shadcn/ui components step completed");
1767
+
1768
+ const wantProviders = await promptYesNo(
1769
+ "Install TanStack Query and Trustless Work providers and wrap app/layout with them?",
1770
+ true
1771
+ );
1772
+ if (wantProviders) {
1773
+ await withSpinner("Installing providers", async () => {
1774
+ copyTemplate("providers");
1775
+ });
1776
+ const layoutPath = findLayoutFile();
1777
+ if (layoutPath) {
1778
+ await withSpinner("Updating app/layout with providers", async () => {
1779
+ injectProvidersIntoLayout(layoutPath, {
1780
+ reactQuery: true,
1781
+ trustless: true,
1782
+ });
1783
+ });
1784
+ } else {
1785
+ console.warn(
1786
+ "⚠️ Could not find app/layout file. Skipped automatic wiring."
1787
+ );
1788
+ }
1789
+ } else {
1790
+ console.log("\x1b[90m– Skipped installing providers\x1b[0m");
1791
+ }
1792
+
1793
+ printBannerTRUSTLESSWORK();
1794
+ console.log("\n\nResources");
1795
+ console.log("- " + oscHyperlink("Website", "https://trustlesswork.com"));
1796
+ console.log(
1797
+ "- " + oscHyperlink("Documentation", "https://docs.trustlesswork.com")
1798
+ );
1799
+ console.log("- " + oscHyperlink("Demo", "https://demo.trustlesswork.com"));
1800
+ console.log(
1801
+ "- " + oscHyperlink("Backoffice", "https://dapp.trustlesswork.com")
1802
+ );
1803
+ console.log(
1804
+ "- " + oscHyperlink("GitHub", "https://github.com/trustless-work")
1805
+ );
1806
+ console.log(
1807
+ "- " + oscHyperlink("Escrow Viewer", "https://viewer.trustlesswork.com")
1808
+ );
1809
+ console.log(
1810
+ "- " + oscHyperlink("Telegram", "https://t.me/+kmr8tGegxLU0NTA5")
1811
+ );
1812
+ console.log(
1813
+ "- " +
1814
+ oscHyperlink(
1815
+ "LinkedIn",
1816
+ "https://www.linkedin.com/company/trustlesswork/posts/?feedView=all"
1817
+ )
1818
+ );
1819
+ console.log("- " + oscHyperlink("X", "https://x.com/TrustlessWork"));
1820
+ } else if (args[0] === "add" && args[1]) {
1821
+ const flags = parseFlags(args.slice(2));
1822
+ // Normalize common aliases (singular/plural, shorthand)
1823
+ const normalizeTemplateName = (name) => {
1824
+ let n = String(name).trim();
1825
+ // singular to plural base
1826
+ n = n.replace(/^escrow\b/, "escrows");
1827
+ n = n.replace(/^indicator\b/, "indicators");
1828
+ // allow nested segments singulars
1829
+ n = n.replace(/(^|\/)escrow(\/|$)/g, "$1escrows$2");
1830
+ n = n.replace(/(^|\/)indicator(\/|$)/g, "$1indicators$2");
1831
+ // friendly shape variants
1832
+ n = n.replace(/(^|\/)circle(\/|$)/g, "$1circular$2");
1833
+ return n;
1834
+ };
1835
+ args[1] = normalizeTemplateName(args[1]);
1836
+ const cfgPath = path.join(PROJECT_ROOT, ".twblocks.json");
1837
+ if (!fs.existsSync(cfgPath)) {
1838
+ console.error(
1839
+ "❌ Missing initial setup. Run 'trustless-work init' first to install dependencies and create .twblocks.json (uiBase)."
1840
+ );
1841
+ console.error(
1842
+ " After init, re-run: trustless-work add " +
1843
+ args[1] +
1844
+ (flags.uiBase ? ' --ui-base "' + flags.uiBase + '"' : "")
1845
+ );
1846
+ process.exit(1);
1847
+ }
1848
+ copyTemplate(args[1], {
1849
+ uiBase: flags.uiBase,
1850
+ shouldInstall: !!flags.install,
1851
+ });
1852
+
1853
+ // Post-add wiring for specific templates
1854
+ const layoutPath = findLayoutFile();
1855
+ if (layoutPath) {
1856
+ if (args[1] === "wallet-kit" || args[1].startsWith("wallet-kit/")) {
1857
+ injectProvidersIntoLayout(layoutPath, { wallet: true });
1858
+ }
1859
+ }
1860
+
1861
+ // Copy shared details into role/signer targets when applicable
1862
+ try {
1863
+ if (args[1] === "escrows") {
1864
+ copySharedDetailsInto("escrows/escrows-by-role/details", {
1865
+ uiBase: flags.uiBase,
1866
+ });
1867
+ copySharedDetailsInto("escrows/escrows-by-signer/details", {
1868
+ uiBase: flags.uiBase,
1869
+ });
1870
+ copySharedRoleSignerHooks("both");
1871
+ }
1872
+ if (
1873
+ args[1] === "escrows/escrows-by-role" ||
1874
+ args[1].startsWith("escrows/escrows-by-role/")
1875
+ ) {
1876
+ copySharedDetailsInto("escrows/escrows-by-role/details", {
1877
+ uiBase: flags.uiBase,
1878
+ });
1879
+ copySharedRoleSignerHooks("role");
1880
+ }
1881
+ if (
1882
+ args[1] === "escrows/escrows-by-signer" ||
1883
+ args[1].startsWith("escrows/escrows-by-signer/")
1884
+ ) {
1885
+ copySharedDetailsInto("escrows/escrows-by-signer/details", {
1886
+ uiBase: flags.uiBase,
1887
+ });
1888
+ copySharedRoleSignerHooks("signer");
1889
+ }
1890
+ } catch (e) {
1891
+ console.warn("⚠️ Failed to copy shared details:", e?.message || e);
1892
+ }
1893
+ } else {
1894
+ console.log(`
1895
+
1896
+ Usage:
1897
+
1898
+ trustless-work init
1899
+ trustless-work add <template> [--install]
1900
+
1901
+ Options:
1902
+
1903
+ --ui-base <path> Base import path to your shadcn/ui components (default: "@/components/ui")
1904
+ --install, -i Also install dependencies (normally use 'init' once instead)
1905
+
1906
+ Examples:
1907
+
1908
+ --- Get started ---
1909
+ trustless-work init
1910
+
1911
+ --- Providers ---
1912
+ trustless-work add providers
1913
+
1914
+ --- Wallet-kit ---
1915
+ trustless-work add wallet-kit
1916
+
1917
+ --- Handle-errors ---
1918
+ trustless-work add handle-errors
1919
+
1920
+ --- Helpers ---
1921
+ trustless-work add helpers
1922
+
1923
+ --- Tanstack ---
1924
+ trustless-work add tanstack
1925
+
1926
+ --- Escrows ---
1927
+ trustless-work add escrows
1928
+
1929
+ --- Dashboard ---
1930
+ trustless-work add dashboard
1931
+ trustless-work add dashboard/dashboard-01
1932
+
1933
+ --- Indicators ---
1934
+ trustless-work add escrows/indicators/balance-progress
1935
+ trustless-work add escrows/indicators/balance-progress/bar
1936
+ trustless-work add escrows/indicators/balance-progress/donut
1937
+
1938
+ --- Escrows by role ---
1939
+ trustless-work add escrows/escrows-by-role
1940
+ trustless-work add escrows/escrows-by-role/table
1941
+ trustless-work add escrows/escrows-by-role/cards
1942
+
1943
+ --- Escrows by signer ---
1944
+ trustless-work add escrows/escrows-by-signer
1945
+ trustless-work add escrows/escrows-by-signer/table
1946
+ trustless-work add escrows/escrows-by-signer/cards
1947
+
1948
+ ----------------------
1949
+ --- SINGLE-RELEASE ---
1950
+ trustless-work add escrows/single-release
1951
+
1952
+ --- Initialize escrow ---
1953
+ - trustless-work add escrows/single-release/initialize-escrow
1954
+ - trustless-work add escrows/single-release/initialize-escrow/form
1955
+ - trustless-work add escrows/single-release/initialize-escrow/dialog
1956
+
1957
+ --- Resolve dispute ---
1958
+ - trustless-work add escrows/single-release/resolve-dispute
1959
+ - trustless-work add escrows/single-release/resolve-dispute/form
1960
+ - trustless-work add escrows/single-release/resolve-dispute/button
1961
+ - trustless-work add escrows/single-release/resolve-dispute/dialog
1962
+
1963
+ --- Update escrow ---
1964
+ - trustless-work add escrows/single-release/update-escrow
1965
+ - trustless-work add escrows/single-release/update-escrow/form
1966
+ - trustless-work add escrows/single-release/update-escrow/dialog
1967
+
1968
+ --- Release escrow ---
1969
+ - trustless-work add escrows/single-release/release-escrow
1970
+ - trustless-work add escrows/single-release/release-escrow/button
1971
+
1972
+ --- Dispute escrow ---
1973
+ - trustless-work add escrows/single-release/dispute-escrow
1974
+ - trustless-work add escrows/single-release/dispute-escrow/button
1975
+
1976
+ ----------------------
1977
+ --- MULTI-RELEASE ---
1978
+ trustless-work add escrows/multi-release
1979
+
1980
+ --- Initialize escrow ---
1981
+ - trustless-work add escrows/multi-release/initialize-escrow
1982
+ - trustless-work add escrows/multi-release/initialize-escrow/form
1983
+ - trustless-work add escrows/multi-release/initialize-escrow/dialog
1984
+
1985
+ --- Resolve dispute ---
1986
+ - trustless-work add escrows/multi-release/resolve-dispute
1987
+ - trustless-work add escrows/multi-release/resolve-dispute/form
1988
+ - trustless-work add escrows/multi-release/resolve-dispute/button
1989
+ - trustless-work add escrows/multi-release/resolve-dispute/dialog
1990
+
1991
+ --- Update escrow ---
1992
+ - trustless-work add escrows/multi-release/update-escrow
1993
+ - trustless-work add escrows/multi-release/update-escrow/form
1994
+ - trustless-work add escrows/multi-release/update-escrow/dialog
1995
+
1996
+ --- Withdraw remaining funds ---
1997
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds
1998
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds/form
1999
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds/button
2000
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds/dialog
2001
+
2002
+ --- Release escrow ---
2003
+ - trustless-work add escrows/multi-release/release-milestone
2004
+ - trustless-work add escrows/multi-release/release-milestone/button
2005
+
2006
+ --- Dispute escrow ---
2007
+ - trustless-work add escrows/multi-release/dispute-milestone
2008
+ - trustless-work add escrows/multi-release/dispute-milestone/button
2009
+
2010
+ --- Withdraw remaining funds ---
2011
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds
2012
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds/form
2013
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds/button
2014
+ - trustless-work add escrows/multi-release/withdraw-remaining-funds/dialog
2015
+
2016
+ ----------------------
2017
+ --- SINGLE-MULTI-RELEASE -> Works with both types of escrows ---
2018
+ trustless-work add escrows/single-multi-release
2019
+
2020
+ --- Approve milestone ---
2021
+ - trustless-work add escrows/single-multi-release/approve-milestone
2022
+ - trustless-work add escrows/single-multi-release/approve-milestone/form
2023
+ - trustless-work add escrows/single-multi-release/approve-milestone/button
2024
+ - trustless-work add escrows/single-multi-release/approve-milestone/dialog
2025
+
2026
+ --- Change milestone status ---
2027
+ - trustless-work add escrows/single-multi-release/change-milestone-status
2028
+ - trustless-work add escrows/single-multi-release/change-milestone-status/form
2029
+ - trustless-work add escrows/single-multi-release/change-milestone-status/button
2030
+ - trustless-work add escrows/single-multi-release/change-milestone-status/dialog
2031
+
2032
+ --- Fund escrow ---
2033
+ - trustless-work add escrows/single-multi-release/fund-escrow
2034
+ - trustless-work add escrows/single-multi-release/fund-escrow/form
2035
+ - trustless-work add escrows/single-multi-release/fund-escrow/button
2036
+ - trustless-work add escrows/single-multi-release/fund-escrow/dialog
2037
+ `);
2038
+ }