@trustless-work/blocks 1.0.6 → 1.0.7

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