@trustless-work/blocks 0.0.6 → 0.0.8

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 (72) hide show
  1. package/README.md +39 -13
  2. package/bin/index.js +1596 -1137
  3. package/package.json +44 -44
  4. package/templates/escrows/details/Actions.tsx +144 -149
  5. package/templates/escrows/details/Entities.tsx +1 -1
  6. package/templates/escrows/details/EntityCard.tsx +1 -3
  7. package/templates/escrows/details/EscrowDetailDialog.tsx +18 -18
  8. package/templates/escrows/details/GeneralInformation.tsx +20 -23
  9. package/templates/escrows/details/MilestoneCard.tsx +46 -47
  10. package/templates/escrows/details/MilestoneDetailDialog.tsx +1 -2
  11. package/templates/escrows/details/Milestones.tsx +0 -5
  12. package/templates/escrows/details/SuccessReleaseDialog.tsx +6 -9
  13. package/templates/escrows/details/useDetailsEscrow.ts +2 -2
  14. package/templates/escrows/escrows-by-role/cards/EscrowsCards.tsx +111 -60
  15. package/templates/escrows/escrows-by-role/cards/Filters.tsx +3 -5
  16. package/templates/escrows/escrows-by-role/table/EscrowsTable.tsx +36 -38
  17. package/templates/escrows/escrows-by-role/table/Filters.tsx +3 -5
  18. package/templates/escrows/escrows-by-role/useEscrowsByRole.shared.ts +33 -25
  19. package/templates/escrows/escrows-by-signer/cards/EscrowsCards.tsx +107 -67
  20. package/templates/escrows/escrows-by-signer/cards/Filters.tsx +3 -5
  21. package/templates/escrows/escrows-by-signer/table/EscrowsTable.tsx +28 -38
  22. package/templates/escrows/escrows-by-signer/table/Filters.tsx +3 -5
  23. package/templates/escrows/escrows-by-signer/useEscrowsBySigner.shared.ts +32 -25
  24. package/templates/escrows/multi-release/dispute-milestone/button/DisputeEscrow.tsx +98 -0
  25. package/templates/escrows/multi-release/initialize-escrow/dialog/InitializeEscrow.tsx +528 -0
  26. package/templates/escrows/multi-release/initialize-escrow/form/InitializeEscrow.tsx +506 -0
  27. package/templates/escrows/multi-release/initialize-escrow/shared/schema.ts +179 -0
  28. package/templates/escrows/multi-release/initialize-escrow/shared/useInitializeEscrow.ts +175 -0
  29. package/templates/escrows/multi-release/release-milestone/button/ReleaseEscrow.tsx +116 -0
  30. package/templates/escrows/multi-release/resolve-dispute/button/ResolveDispute.tsx +122 -0
  31. package/templates/escrows/multi-release/resolve-dispute/dialog/ResolveDispute.tsx +178 -0
  32. package/templates/escrows/multi-release/resolve-dispute/form/ResolveDispute.tsx +156 -0
  33. package/templates/escrows/multi-release/resolve-dispute/shared/schema.ts +85 -0
  34. package/templates/escrows/multi-release/resolve-dispute/shared/useResolveDispute.ts +105 -0
  35. package/templates/escrows/multi-release/update-escrow/dialog/UpdateEscrow.tsx +471 -0
  36. package/templates/escrows/multi-release/update-escrow/form/UpdateEscrow.tsx +449 -0
  37. package/templates/escrows/multi-release/update-escrow/shared/schema.ts +152 -0
  38. package/templates/escrows/multi-release/update-escrow/shared/useUpdateEscrow.ts +254 -0
  39. package/templates/escrows/{single-release → single-multi-release}/approve-milestone/button/ApproveMilestone.tsx +21 -8
  40. package/templates/escrows/{single-release → single-multi-release}/approve-milestone/dialog/ApproveMilestone.tsx +4 -4
  41. package/templates/escrows/{single-release → single-multi-release}/approve-milestone/form/ApproveMilestone.tsx +4 -4
  42. package/templates/escrows/{single-release/approve-milestone/shared → single-multi-release/approve-milestone}/useApproveMilestone.ts +17 -17
  43. package/templates/escrows/{single-release → single-multi-release}/change-milestone-status/button/ChangeMilestoneStatus.tsx +5 -5
  44. package/templates/escrows/{single-release → single-multi-release}/change-milestone-status/dialog/ChangeMilestoneStatus.tsx +5 -5
  45. package/templates/escrows/{single-release → single-multi-release}/change-milestone-status/form/ChangeMilestoneStatus.tsx +4 -4
  46. package/templates/escrows/{single-release/change-milestone-status/shared → single-multi-release/change-milestone-status}/useChangeMilestoneStatus.ts +2 -2
  47. package/templates/escrows/{single-release → single-multi-release}/fund-escrow/button/FundEscrow.tsx +4 -4
  48. package/templates/escrows/{single-release → single-multi-release}/fund-escrow/dialog/FundEscrow.tsx +3 -3
  49. package/templates/escrows/{single-release → single-multi-release}/fund-escrow/form/FundEscrow.tsx +3 -3
  50. package/templates/escrows/{single-release/fund-escrow/shared → single-multi-release/fund-escrow}/useFundEscrow.ts +2 -2
  51. package/templates/escrows/single-release/dispute-escrow/button/DisputeEscrow.tsx +3 -3
  52. package/templates/escrows/single-release/initialize-escrow/dialog/InitializeEscrow.tsx +14 -6
  53. package/templates/escrows/single-release/initialize-escrow/form/InitializeEscrow.tsx +14 -6
  54. package/templates/escrows/single-release/initialize-escrow/shared/schema.ts +0 -57
  55. package/templates/escrows/single-release/initialize-escrow/shared/useInitializeEscrow.ts +43 -2
  56. package/templates/escrows/single-release/release-escrow/button/ReleaseEscrow.tsx +5 -5
  57. package/templates/escrows/single-release/resolve-dispute/button/ResolveDispute.tsx +4 -4
  58. package/templates/escrows/single-release/resolve-dispute/dialog/ResolveDispute.tsx +4 -7
  59. package/templates/escrows/single-release/resolve-dispute/form/ResolveDispute.tsx +2 -2
  60. package/templates/escrows/single-release/resolve-dispute/shared/useResolveDispute.ts +15 -2
  61. package/templates/escrows/single-release/update-escrow/dialog/UpdateEscrow.tsx +2 -2
  62. package/templates/escrows/single-release/update-escrow/form/UpdateEscrow.tsx +2 -2
  63. package/templates/escrows/single-release/update-escrow/shared/useUpdateEscrow.ts +229 -224
  64. package/templates/{escrows/escrow-context → providers}/EscrowDialogsProvider.tsx +1 -3
  65. package/templates/{escrows/escrow-context → providers}/EscrowProvider.tsx +27 -4
  66. package/templates/providers/ReactQueryClientProvider.tsx +3 -1
  67. package/templates/providers/TrustlessWork.tsx +1 -1
  68. package/templates/escrows/details/ProgressEscrow.tsx +0 -191
  69. /package/templates/escrows/{single-release/approve-milestone/shared → single-multi-release/approve-milestone}/schema.ts +0 -0
  70. /package/templates/escrows/{single-release/change-milestone-status/shared → single-multi-release/change-milestone-status}/schema.ts +0 -0
  71. /package/templates/escrows/{single-release/fund-escrow/shared → single-multi-release/fund-escrow}/schema.ts +0 -0
  72. /package/templates/{escrows/escrow-context → providers}/EscrowAmountProvider.tsx +0 -0
package/bin/index.js CHANGED
@@ -1,1138 +1,1597 @@
1
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 srcDir = path.join(TEMPLATES_DIR, name);
224
- const outRoot = path.join(PROJECT_ROOT, "src", "components", "tw-blocks");
225
-
226
- const config = loadConfig();
227
- const effectiveUiBase = uiBase || config.uiBase || "@/components/ui";
228
-
229
- function writeTransformed(srcPath, destPath) {
230
- const raw = fs.readFileSync(srcPath, "utf8");
231
- const transformed = raw.replaceAll("__UI_BASE__", effectiveUiBase);
232
- fs.mkdirSync(path.dirname(destPath), { recursive: true });
233
- fs.writeFileSync(destPath, transformed, "utf8");
234
- console.log(`✅ ${path.relative(PROJECT_ROOT, destPath)} created`);
235
- }
236
-
237
- if (fs.existsSync(srcDir) && fs.lstatSync(srcDir).isDirectory()) {
238
- const skipDetails =
239
- name === "escrows/escrows-by-role" ||
240
- name === "escrows/escrows-by-signer" ||
241
- name === "escrows";
242
- // Copy directory recursively
243
- const destDir = path.join(outRoot, name);
244
- fs.mkdirSync(destDir, { recursive: true });
245
- const stack = [""];
246
- while (stack.length) {
247
- const rel = stack.pop();
248
- const current = path.join(srcDir, rel);
249
- const entries = fs.readdirSync(current, { withFileTypes: true });
250
- for (const entry of entries) {
251
- const entryRel = path.join(rel, entry.name);
252
- // Skip copying any shared directory at any depth
253
- const parts = entryRel.split(path.sep);
254
- if (parts.includes("shared")) {
255
- continue;
256
- }
257
- if (skipDetails) {
258
- const top = parts[0] || "";
259
- const firstTwo = parts.slice(0, 2).join(path.sep);
260
- if (
261
- top === "details" ||
262
- firstTwo === path.join("escrows-by-role", "details") ||
263
- firstTwo === path.join("escrows-by-signer", "details")
264
- ) {
265
- continue;
266
- }
267
- }
268
- const entrySrc = path.join(srcDir, entryRel);
269
- const entryDest = path.join(destDir, entryRel);
270
- if (entry.isDirectory()) {
271
- stack.push(entryRel);
272
- continue;
273
- }
274
- // Only process text files (.ts, .tsx, .js, .jsx)
275
- if (/\.(tsx?|jsx?)$/i.test(entry.name)) {
276
- writeTransformed(entrySrc, entryDest);
277
- } else {
278
- fs.mkdirSync(path.dirname(entryDest), { recursive: true });
279
- fs.copyFileSync(entrySrc, entryDest);
280
- console.log(`✅ ${path.relative(PROJECT_ROOT, entryDest)} created`);
281
- }
282
- }
283
- }
284
-
285
- // Post-copy: materialize shared initialize-escrow files into dialog/form
286
- try {
287
- const isSingleReleaseInitRoot =
288
- name === "escrows/single-release/initialize-escrow";
289
- const isSingleReleaseInitDialog =
290
- name === "escrows/single-release/initialize-escrow/dialog";
291
- const isSingleReleaseInitForm =
292
- name === "escrows/single-release/initialize-escrow/form";
293
-
294
- const srcSharedDir = path.join(
295
- TEMPLATES_DIR,
296
- "escrows",
297
- "single-release",
298
- "initialize-escrow",
299
- "shared"
300
- );
301
-
302
- function copySharedInto(targetDir) {
303
- if (!fs.existsSync(srcSharedDir)) return;
304
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
305
- for (const entry of entries) {
306
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
307
- const entrySrc = path.join(srcSharedDir, entry.name);
308
- const entryDest = path.join(targetDir, entry.name);
309
- writeTransformed(entrySrc, entryDest);
310
- }
311
- }
312
-
313
- if (isSingleReleaseInitRoot) {
314
- copySharedInto(path.join(destDir, "dialog"));
315
- copySharedInto(path.join(destDir, "form"));
316
- } else if (isSingleReleaseInitDialog) {
317
- copySharedInto(destDir);
318
- } else if (isSingleReleaseInitForm) {
319
- copySharedInto(destDir);
320
- }
321
- } catch (e) {
322
- console.warn(
323
- "⚠️ Failed to materialize shared initialize-escrow files:",
324
- e?.message || e
325
- );
326
- }
327
-
328
- try {
329
- const isSingleReleaseInitRoot =
330
- name === "escrows/single-release/approve-milestone";
331
- const isSingleReleaseInitDialog =
332
- name === "escrows/single-release/approve-milestone/dialog";
333
- const isSingleReleaseInitForm =
334
- name === "escrows/single-release/approve-milestone/form";
335
-
336
- const srcSharedDir = path.join(
337
- TEMPLATES_DIR,
338
- "escrows",
339
- "single-release",
340
- "approve-milestone",
341
- "shared"
342
- );
343
-
344
- function copySharedInto(targetDir) {
345
- if (!fs.existsSync(srcSharedDir)) return;
346
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
347
- for (const entry of entries) {
348
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
349
- const entrySrc = path.join(srcSharedDir, entry.name);
350
- const entryDest = path.join(targetDir, entry.name);
351
- writeTransformed(entrySrc, entryDest);
352
- }
353
- }
354
-
355
- if (isSingleReleaseInitRoot) {
356
- copySharedInto(path.join(destDir, "dialog"));
357
- copySharedInto(path.join(destDir, "form"));
358
- } else if (isSingleReleaseInitDialog) {
359
- copySharedInto(destDir);
360
- } else if (isSingleReleaseInitForm) {
361
- copySharedInto(destDir);
362
- }
363
- } catch (e) {
364
- console.warn(
365
- "⚠️ Failed to materialize shared approve-milestone files:",
366
- e?.message || e
367
- );
368
- }
369
-
370
- try {
371
- const isSingleReleaseInitRoot =
372
- name === "escrows/single-release/change-milestone-status";
373
- const isSingleReleaseInitDialog =
374
- name === "escrows/single-release/change-milestone-status/dialog";
375
- const isSingleReleaseInitForm =
376
- name === "escrows/single-release/change-milestone-status/form";
377
-
378
- const srcSharedDir = path.join(
379
- TEMPLATES_DIR,
380
- "escrows",
381
- "single-release",
382
- "change-milestone-status",
383
- "shared"
384
- );
385
-
386
- function copySharedInto(targetDir) {
387
- if (!fs.existsSync(srcSharedDir)) return;
388
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
389
- for (const entry of entries) {
390
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
391
- const entrySrc = path.join(srcSharedDir, entry.name);
392
- const entryDest = path.join(targetDir, entry.name);
393
- writeTransformed(entrySrc, entryDest);
394
- }
395
- }
396
-
397
- if (isSingleReleaseInitRoot) {
398
- copySharedInto(path.join(destDir, "dialog"));
399
- copySharedInto(path.join(destDir, "form"));
400
- } else if (isSingleReleaseInitDialog) {
401
- copySharedInto(destDir);
402
- } else if (isSingleReleaseInitForm) {
403
- copySharedInto(destDir);
404
- }
405
- } catch (e) {
406
- console.warn(
407
- "⚠️ Failed to materialize shared change-milestone-status files:",
408
- e?.message || e
409
- );
410
- }
411
-
412
- try {
413
- const isSingleReleaseInitRoot =
414
- name === "escrows/single-release/fund-escrow";
415
- const isSingleReleaseInitDialog =
416
- name === "escrows/single-release/fund-escrow/dialog";
417
- const isSingleReleaseInitForm =
418
- name === "escrows/single-release/fund-escrow/form";
419
-
420
- const srcSharedDir = path.join(
421
- TEMPLATES_DIR,
422
- "escrows",
423
- "single-release",
424
- "fund-escrow",
425
- "shared"
426
- );
427
-
428
- function copySharedInto(targetDir) {
429
- if (!fs.existsSync(srcSharedDir)) return;
430
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
431
- for (const entry of entries) {
432
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
433
- const entrySrc = path.join(srcSharedDir, entry.name);
434
- const entryDest = path.join(targetDir, entry.name);
435
- writeTransformed(entrySrc, entryDest);
436
- }
437
- }
438
-
439
- if (isSingleReleaseInitRoot) {
440
- copySharedInto(path.join(destDir, "dialog"));
441
- copySharedInto(path.join(destDir, "form"));
442
- } else if (isSingleReleaseInitDialog) {
443
- copySharedInto(destDir);
444
- } else if (isSingleReleaseInitForm) {
445
- copySharedInto(destDir);
446
- }
447
- } catch (e) {
448
- console.warn(
449
- "⚠️ Failed to materialize shared fund-escrow files:",
450
- e?.message || e
451
- );
452
- }
453
-
454
- try {
455
- const isSingleReleaseInitRoot =
456
- name === "escrows/single-release/resolve-dispute";
457
- const isSingleReleaseInitDialog =
458
- name === "escrows/single-release/resolve-dispute/dialog";
459
- const isSingleReleaseInitForm =
460
- name === "escrows/single-release/resolve-dispute/form";
461
-
462
- const srcSharedDir = path.join(
463
- TEMPLATES_DIR,
464
- "escrows",
465
- "single-release",
466
- "resolve-dispute",
467
- "shared"
468
- );
469
-
470
- function copySharedInto(targetDir) {
471
- if (!fs.existsSync(srcSharedDir)) return;
472
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
473
- for (const entry of entries) {
474
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
475
- const entrySrc = path.join(srcSharedDir, entry.name);
476
- const entryDest = path.join(targetDir, entry.name);
477
- writeTransformed(entrySrc, entryDest);
478
- }
479
- }
480
-
481
- if (isSingleReleaseInitRoot) {
482
- copySharedInto(path.join(destDir, "dialog"));
483
- copySharedInto(path.join(destDir, "form"));
484
- } else if (isSingleReleaseInitDialog) {
485
- copySharedInto(destDir);
486
- } else if (isSingleReleaseInitForm) {
487
- copySharedInto(destDir);
488
- }
489
- } catch (e) {
490
- console.warn(
491
- "⚠️ Failed to materialize shared resolve-dispute files:",
492
- e?.message || e
493
- );
494
- }
495
-
496
- try {
497
- const isSingleReleaseInitRoot =
498
- name === "escrows/single-release/update-escrow";
499
- const isSingleReleaseInitDialog =
500
- name === "escrows/single-release/update-escrow/dialog";
501
- const isSingleReleaseInitForm =
502
- name === "escrows/single-release/update-escrow/form";
503
-
504
- const srcSharedDir = path.join(
505
- TEMPLATES_DIR,
506
- "escrows",
507
- "single-release",
508
- "update-escrow",
509
- "shared"
510
- );
511
-
512
- function copySharedInto(targetDir) {
513
- if (!fs.existsSync(srcSharedDir)) return;
514
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
515
- for (const entry of entries) {
516
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
517
- const entrySrc = path.join(srcSharedDir, entry.name);
518
- const entryDest = path.join(targetDir, entry.name);
519
- writeTransformed(entrySrc, entryDest);
520
- }
521
- }
522
-
523
- if (isSingleReleaseInitRoot) {
524
- copySharedInto(path.join(destDir, "dialog"));
525
- copySharedInto(path.join(destDir, "form"));
526
- } else if (isSingleReleaseInitDialog) {
527
- copySharedInto(destDir);
528
- } else if (isSingleReleaseInitForm) {
529
- copySharedInto(destDir);
530
- }
531
- } catch (e) {
532
- console.warn(
533
- "⚠️ Failed to materialize shared update-escrow files:",
534
- e?.message || e
535
- );
536
- }
537
-
538
- // If adding the whole single-release bundle, materialize all shared files
539
- try {
540
- if (name === "escrows/single-release") {
541
- const modules = [
542
- "initialize-escrow",
543
- "approve-milestone",
544
- "change-milestone-status",
545
- "fund-escrow",
546
- "resolve-dispute",
547
- "update-escrow",
548
- ];
549
-
550
- for (const mod of modules) {
551
- const srcSharedDir = path.join(
552
- TEMPLATES_DIR,
553
- "escrows",
554
- "single-release",
555
- mod,
556
- "shared"
557
- );
558
- if (!fs.existsSync(srcSharedDir)) continue;
559
-
560
- const targets = [
561
- path.join(destDir, mod, "dialog"),
562
- path.join(destDir, mod, "form"),
563
- ];
564
-
565
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
566
- for (const entry of entries) {
567
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
568
- const entrySrc = path.join(srcSharedDir, entry.name);
569
- for (const t of targets) {
570
- const entryDest = path.join(t, entry.name);
571
- writeTransformed(entrySrc, entryDest);
572
- }
573
- }
574
- }
575
- }
576
- } catch (e) {
577
- console.warn(
578
- "⚠️ Failed to materialize shared files for single-release bundle:",
579
- e?.message || e
580
- );
581
- }
582
-
583
- // If adding the root escrows bundle, also materialize single-release shared files
584
- try {
585
- if (name === "escrows") {
586
- const modules = [
587
- "initialize-escrow",
588
- "approve-milestone",
589
- "change-milestone-status",
590
- "fund-escrow",
591
- "resolve-dispute",
592
- "update-escrow",
593
- ];
594
-
595
- const baseTarget = path.join(destDir, "single-release");
596
- for (const mod of modules) {
597
- const srcSharedDir = path.join(
598
- TEMPLATES_DIR,
599
- "escrows",
600
- "single-release",
601
- mod,
602
- "shared"
603
- );
604
- if (!fs.existsSync(srcSharedDir)) continue;
605
-
606
- const targets = [
607
- path.join(baseTarget, mod, "dialog"),
608
- path.join(baseTarget, mod, "form"),
609
- ];
610
-
611
- const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
612
- for (const entry of entries) {
613
- if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
614
- const entrySrc = path.join(srcSharedDir, entry.name);
615
- for (const t of targets) {
616
- const entryDest = path.join(t, entry.name);
617
- writeTransformed(entrySrc, entryDest);
618
- }
619
- }
620
- }
621
- }
622
- } catch (e) {
623
- console.warn(
624
- "⚠️ Failed to materialize shared files for escrows root:",
625
- e?.message || e
626
- );
627
- }
628
- } else if (fs.existsSync(srcFile)) {
629
- fs.mkdirSync(outRoot, { recursive: true });
630
- const destFile = path.join(outRoot, name + ".tsx");
631
- writeTransformed(srcFile, destFile);
632
- } else {
633
- console.error(`❌ The template "${name}" does not exist`);
634
- process.exit(1);
635
- }
636
-
637
- if (shouldInstall && fs.existsSync(GLOBAL_DEPS_FILE)) {
638
- const meta = JSON.parse(fs.readFileSync(GLOBAL_DEPS_FILE, "utf8"));
639
- installDeps(meta);
640
- }
641
- }
642
-
643
- function copySharedDetailsInto(targetRelativeDir, { uiBase } = {}) {
644
- const srcDir = path.join(TEMPLATES_DIR, "escrows", "details");
645
- const outRoot = path.join(PROJECT_ROOT, "src", "components", "tw-blocks");
646
- const destDir = path.join(outRoot, targetRelativeDir);
647
- const config = loadConfig();
648
- const effectiveUiBase = uiBase || config.uiBase || "@/components/ui";
649
-
650
- if (!fs.existsSync(srcDir)) return;
651
- fs.mkdirSync(destDir, { recursive: true });
652
-
653
- function writeTransformed(srcPath, destPath) {
654
- const raw = fs.readFileSync(srcPath, "utf8");
655
- const transformed = raw.replaceAll("__UI_BASE__", effectiveUiBase);
656
- fs.mkdirSync(path.dirname(destPath), { recursive: true });
657
- fs.writeFileSync(destPath, transformed, "utf8");
658
- console.log(`✅ ${path.relative(PROJECT_ROOT, destPath)} created`);
659
- }
660
-
661
- const stack = [""];
662
- while (stack.length) {
663
- const rel = stack.pop();
664
- const current = path.join(srcDir, rel);
665
- const entries = fs.readdirSync(current, { withFileTypes: true });
666
- for (const entry of entries) {
667
- const entryRel = path.join(rel, entry.name);
668
- const entrySrc = path.join(srcDir, entryRel);
669
- const entryDest = path.join(destDir, entryRel);
670
- if (entry.isDirectory()) {
671
- stack.push(entryRel);
672
- continue;
673
- }
674
- if (/\.(tsx?|jsx?)$/i.test(entry.name)) {
675
- writeTransformed(entrySrc, entryDest);
676
- } else {
677
- fs.mkdirSync(path.dirname(entryDest), { recursive: true });
678
- fs.copyFileSync(entrySrc, entryDest);
679
- console.log(`✅ ${path.relative(PROJECT_ROOT, entryDest)} created`);
680
- }
681
- }
682
- }
683
- }
684
-
685
- function copySharedRoleSignerHooks(kind = "both") {
686
- const outRoot = path.join(PROJECT_ROOT, "src", "components", "tw-blocks");
687
-
688
- const mappings = [];
689
- if (kind === "both" || kind === "role") {
690
- mappings.push({
691
- src: path.join(
692
- TEMPLATES_DIR,
693
- "escrows",
694
- "escrows-by-role",
695
- "useEscrowsByRole.shared.ts"
696
- ),
697
- dest: path.join(
698
- outRoot,
699
- "escrows",
700
- "escrows-by-role",
701
- "useEscrowsByRole.shared.ts"
702
- ),
703
- });
704
- }
705
- if (kind === "both" || kind === "signer") {
706
- mappings.push({
707
- src: path.join(
708
- TEMPLATES_DIR,
709
- "escrows",
710
- "escrows-by-signer",
711
- "useEscrowsBySigner.shared.ts"
712
- ),
713
- dest: path.join(
714
- outRoot,
715
- "escrows",
716
- "escrows-by-signer",
717
- "useEscrowsBySigner.shared.ts"
718
- ),
719
- });
720
- }
721
-
722
- for (const { src, dest } of mappings) {
723
- if (!fs.existsSync(src)) continue;
724
- const raw = fs.readFileSync(src, "utf8");
725
- fs.mkdirSync(path.dirname(dest), { recursive: true });
726
- fs.writeFileSync(dest, raw, "utf8");
727
- console.log(`✅ ${path.relative(PROJECT_ROOT, dest)} created`);
728
- }
729
- }
730
-
731
- function findLayoutFile() {
732
- const candidates = [
733
- path.join(PROJECT_ROOT, "app", "layout.tsx"),
734
- path.join(PROJECT_ROOT, "app", "layout.ts"),
735
- path.join(PROJECT_ROOT, "app", "layout.jsx"),
736
- path.join(PROJECT_ROOT, "app", "layout.js"),
737
- path.join(PROJECT_ROOT, "src", "app", "layout.tsx"),
738
- path.join(PROJECT_ROOT, "src", "app", "layout.ts"),
739
- path.join(PROJECT_ROOT, "src", "app", "layout.jsx"),
740
- path.join(PROJECT_ROOT, "src", "app", "layout.js"),
741
- ];
742
- return candidates.find((p) => fs.existsSync(p)) || null;
743
- }
744
-
745
- function injectProvidersIntoLayout(
746
- layoutPath,
747
- { reactQuery = false, trustless = false, wallet = false, escrow = false } = {}
748
- ) {
749
- try {
750
- let content = fs.readFileSync(layoutPath, "utf8");
751
-
752
- const importRQ =
753
- 'import { ReactQueryClientProvider } from "@/components/tw-blocks/providers/ReactQueryClientProvider";\n';
754
- const importTW =
755
- 'import { TrustlessWorkProvider } from "@/components/tw-blocks/providers/TrustlessWork";\n';
756
- const importEscrow =
757
- 'import { EscrowProvider } from "@/components/tw-blocks/escrows/escrow-context/EscrowProvider";\n';
758
- const importWallet =
759
- 'import { WalletProvider } from "@/components/tw-blocks/wallet-kit/WalletProvider";\n';
760
- const commentText =
761
- "// Use these imports to wrap your application (<ReactQueryClientProvider>, <TrustlessWorkProvider>, <WalletProvider> y <EscrowProvider>)\n";
762
-
763
- const needImport = (name) =>
764
- !new RegExp(
765
- `import\\s+[^;]*${name}[^;]*from\\s+['\"][^'\"]+['\"];?`
766
- ).test(content);
767
-
768
- let importsToAdd = "";
769
- if (reactQuery && needImport("ReactQueryClientProvider"))
770
- importsToAdd += importRQ;
771
- if (trustless && needImport("TrustlessWorkProvider"))
772
- importsToAdd += importTW;
773
- if (wallet && needImport("WalletProvider")) importsToAdd += importWallet;
774
- if (escrow && needImport("EscrowProvider")) importsToAdd += importEscrow;
775
-
776
- if (importsToAdd) {
777
- const importStmtRegex = /^import.*;\s*$/gm;
778
- let last = null;
779
- for (const m of content.matchAll(importStmtRegex)) last = m;
780
- if (last) {
781
- const idx = last.index + last[0].length;
782
- content =
783
- content.slice(0, idx) +
784
- "\n" +
785
- importsToAdd +
786
- commentText +
787
- content.slice(idx);
788
- } else {
789
- content = importsToAdd + commentText + content;
790
- }
791
- }
792
-
793
- const hasTag = (tag) => new RegExp(`<${tag}[\\s>]`).test(content);
794
- const wrapInside = (containerTag, newTag) => {
795
- const open = content.match(new RegExp(`<${containerTag}(\\s[^>]*)?>`));
796
- if (!open) return false;
797
- const openIdx = open.index + open[0].length;
798
- const closeIdx = content.indexOf(`</${containerTag}>`, openIdx);
799
- if (closeIdx === -1) return false;
800
- content =
801
- content.slice(0, openIdx) +
802
- `\n<${newTag}>\n` +
803
- content.slice(openIdx, closeIdx) +
804
- `\n</${newTag}>\n` +
805
- content.slice(closeIdx);
806
- return true;
807
- };
808
-
809
- const ensureTag = (tag) => {
810
- if (hasTag(tag)) return;
811
- const bodyOpen = content.match(/<body[^>]*>/);
812
- const bodyCloseIdx = content.lastIndexOf("</body>");
813
- if (!bodyOpen || bodyCloseIdx === -1) return;
814
- const bodyOpenIdx = bodyOpen.index + bodyOpen[0].length;
815
- if (tag === "TrustlessWorkProvider") {
816
- if (wrapInside("ReactQueryClientProvider", tag)) return;
817
- }
818
- if (tag === "WalletProvider") {
819
- if (wrapInside("TrustlessWorkProvider", tag)) return;
820
- if (wrapInside("ReactQueryClientProvider", tag)) return;
821
- }
822
- if (tag === "EscrowProvider") {
823
- if (wrapInside("WalletProvider", tag)) return;
824
- if (wrapInside("TrustlessWorkProvider", tag)) return;
825
- if (wrapInside("ReactQueryClientProvider", tag)) return;
826
- }
827
- content =
828
- content.slice(0, bodyOpenIdx) +
829
- `\n<${tag}>\n` +
830
- content.slice(bodyOpenIdx, bodyCloseIdx) +
831
- `\n</${tag}>\n` +
832
- content.slice(bodyCloseIdx);
833
- };
834
-
835
- if (reactQuery) ensureTag("ReactQueryClientProvider");
836
- if (trustless) ensureTag("TrustlessWorkProvider");
837
- if (wallet) ensureTag("WalletProvider");
838
- if (escrow) ensureTag("EscrowProvider");
839
-
840
- fs.writeFileSync(layoutPath, content, "utf8");
841
- logCheck(
842
- `Updated ${path.relative(PROJECT_ROOT, layoutPath)} with providers`
843
- );
844
- } catch (e) {
845
- console.error("❌ Failed to update layout with providers:", e.message);
846
- }
847
- }
848
-
849
- if (args[0] === "init") {
850
- console.log("\n▶ Setting up shadcn/ui components...");
851
- const doInit = await promptYesNo("Run shadcn init now?", true);
852
- if (doInit) {
853
- run("npx", ["shadcn@latest", "init"]);
854
- } else {
855
- console.log("\x1b[90m– Skipped shadcn init\x1b[0m");
856
- }
857
-
858
- const addShadcn = await promptYesNo(
859
- "Add shadcn components (button, input, form, card, sonner, checkbox, dialog, textarea, sonner, select, table, calendar, popover, separator, calendar-05, badge, sheet, tabs, avatar)?",
860
- true
861
- );
862
- if (addShadcn) {
863
- await withSpinner("Installing shadcn/ui components", async () => {
864
- await runAsync("npx", [
865
- "shadcn@latest",
866
- "add",
867
- "button",
868
- "input",
869
- "form",
870
- "card",
871
- "sonner",
872
- "checkbox",
873
- "dialog",
874
- "textarea",
875
- "sonner",
876
- "select",
877
- "table",
878
- "calendar",
879
- "popover",
880
- "separator",
881
- "calendar-05",
882
- "badge",
883
- "sheet",
884
- "tabs",
885
- "avatar",
886
- ]);
887
- });
888
- } else {
889
- console.log("\x1b[90m– Skipped adding shadcn components\x1b[0m");
890
- }
891
-
892
- if (!fs.existsSync(GLOBAL_DEPS_FILE)) {
893
- console.error("❌ deps.json not found in templates/");
894
- process.exit(1);
895
- }
896
- const meta = JSON.parse(fs.readFileSync(GLOBAL_DEPS_FILE, "utf8"));
897
- const installLibs = await promptYesNo(
898
- "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 & zod) dependencies now?",
899
- true
900
- );
901
- if (installLibs) {
902
- await withSpinner("Installing required dependencies", async () => {
903
- installDeps(meta);
904
- });
905
- } else {
906
- console.log("\x1b[90m– Skipped installing required dependencies\x1b[0m");
907
- }
908
- const cfgPath = path.join(PROJECT_ROOT, ".twblocks.json");
909
- if (!fs.existsSync(cfgPath)) {
910
- fs.writeFileSync(
911
- cfgPath,
912
- JSON.stringify({ uiBase: "@/components/ui" }, null, 2)
913
- );
914
- console.log(
915
- `\x1b[32m✔\x1b[0m Created ${path.relative(
916
- PROJECT_ROOT,
917
- cfgPath
918
- )} with default uiBase`
919
- );
920
- }
921
- console.log("\x1b[32m✔\x1b[0m shadcn/ui components step completed");
922
-
923
- const wantProviders = await promptYesNo(
924
- "Install TanStack Query and Trustless Work providers and wrap app/layout with them?",
925
- true
926
- );
927
- if (wantProviders) {
928
- await withSpinner("Installing providers", async () => {
929
- copyTemplate("providers");
930
- });
931
- const layoutPath = findLayoutFile();
932
- if (layoutPath) {
933
- await withSpinner("Updating app/layout with providers", async () => {
934
- injectProvidersIntoLayout(layoutPath, {
935
- reactQuery: true,
936
- trustless: true,
937
- });
938
- });
939
- } else {
940
- console.warn(
941
- "⚠️ Could not find app/layout file. Skipped automatic wiring."
942
- );
943
- }
944
- } else {
945
- console.log("\x1b[90m– Skipped installing providers\x1b[0m");
946
- }
947
-
948
- printBannerTRUSTLESSWORK();
949
- console.log("\n\nResources");
950
- console.log("- " + oscHyperlink("Website", "https://trustlesswork.com"));
951
- console.log(
952
- "- " + oscHyperlink("Documentation", "https://docs.trustlesswork.com")
953
- );
954
- console.log("- " + oscHyperlink("Demo", "https://demo.trustlesswork.com"));
955
- console.log(
956
- "- " + oscHyperlink("Backoffice", "https://dapp.trustlesswork.com")
957
- );
958
- console.log(
959
- "- " + oscHyperlink("GitHub", "https://github.com/trustless-work")
960
- );
961
- console.log(
962
- "- " + oscHyperlink("Escrow Viewer", "https://viewer.trustlesswork.com")
963
- );
964
- console.log(
965
- "- " + oscHyperlink("Telegram", "https://t.me/+kmr8tGegxLU0NTA5")
966
- );
967
- console.log(
968
- "- " +
969
- oscHyperlink(
970
- "LinkedIn",
971
- "https://www.linkedin.com/company/trustlesswork/posts/?feedView=all"
972
- )
973
- );
974
- console.log("- " + oscHyperlink("X", "https://x.com/TrustlessWork"));
975
- } else if (args[0] === "add" && args[1]) {
976
- const flags = parseFlags(args.slice(2));
977
- const cfgPath = path.join(PROJECT_ROOT, ".twblocks.json");
978
- if (!fs.existsSync(cfgPath)) {
979
- console.error(
980
- "❌ Missing initial setup. Run 'trustless-work init' first to install dependencies and create .twblocks.json (uiBase)."
981
- );
982
- console.error(
983
- " After init, re-run: trustless-work add " +
984
- args[1] +
985
- (flags.uiBase ? ' --ui-base "' + flags.uiBase + '"' : "")
986
- );
987
- process.exit(1);
988
- }
989
- copyTemplate(args[1], {
990
- uiBase: flags.uiBase,
991
- shouldInstall: !!flags.install,
992
- });
993
-
994
- // Post-add wiring for specific templates
995
- const layoutPath = findLayoutFile();
996
- if (layoutPath) {
997
- if (args[1] === "wallet-kit" || args[1].startsWith("wallet-kit/")) {
998
- injectProvidersIntoLayout(layoutPath, { wallet: true });
999
- }
1000
- if (
1001
- args[1] === "escrows/escrow-context" ||
1002
- args[1].startsWith("escrows/escrow-context/")
1003
- ) {
1004
- injectProvidersIntoLayout(layoutPath, { escrow: true });
1005
- }
1006
- }
1007
-
1008
- // Copy shared details into role/signer targets when applicable
1009
- try {
1010
- if (args[1] === "escrows") {
1011
- copySharedDetailsInto("escrows/escrows-by-role/details", {
1012
- uiBase: flags.uiBase,
1013
- });
1014
- copySharedDetailsInto("escrows/escrows-by-signer/details", {
1015
- uiBase: flags.uiBase,
1016
- });
1017
- copySharedRoleSignerHooks("both");
1018
- }
1019
- if (
1020
- args[1] === "escrows/escrows-by-role" ||
1021
- args[1].startsWith("escrows/escrows-by-role/")
1022
- ) {
1023
- copySharedDetailsInto("escrows/escrows-by-role/details", {
1024
- uiBase: flags.uiBase,
1025
- });
1026
- copySharedRoleSignerHooks("role");
1027
- }
1028
- if (
1029
- args[1] === "escrows/escrows-by-signer" ||
1030
- args[1].startsWith("escrows/escrows-by-signer/")
1031
- ) {
1032
- copySharedDetailsInto("escrows/escrows-by-signer/details", {
1033
- uiBase: flags.uiBase,
1034
- });
1035
- copySharedRoleSignerHooks("signer");
1036
- }
1037
- } catch (e) {
1038
- console.warn("⚠️ Failed to copy shared details:", e?.message || e);
1039
- }
1040
- } else {
1041
- console.log(`
1042
-
1043
- Usage:
1044
-
1045
- trustless-work init
1046
- trustless-work add <template> [--install]
1047
-
1048
- Options:
1049
-
1050
- --ui-base <path> Base import path to your shadcn/ui components (default: "@/components/ui")
1051
- --install, -i Also install dependencies (normally use 'init' once instead)
1052
-
1053
- Examples:
1054
-
1055
- --- Get started ---
1056
- trustless-work init
1057
-
1058
- --- Providers ---
1059
- trustless-work add providers
1060
-
1061
- --- Wallet-kit ---
1062
- trustless-work add wallet-kit
1063
-
1064
- --- Handle-errors ---
1065
- trustless-work add handle-errors
1066
-
1067
- --- Helpers ---
1068
- trustless-work add helpers
1069
-
1070
- --- Tanstack ---
1071
- trustless-work add tanstack
1072
-
1073
- --- Escrows ---
1074
- trustless-work add escrows
1075
-
1076
- --- Escrow context ---
1077
- trustless-work add escrows/escrow-context
1078
-
1079
- --- Escrows by role ---
1080
- trustless-work add escrows/escrows-by-role
1081
- trustless-work add escrows/escrows-by-role/table
1082
- trustless-work add escrows/escrows-by-role/cards
1083
-
1084
- --- Escrows by signer ---
1085
- trustless-work add escrows/escrows-by-signer
1086
- trustless-work add escrows/escrows-by-signer/table
1087
- trustless-work add escrows/escrows-by-signer/cards
1088
-
1089
- --- Escrow details (optional standalone) ---
1090
- trustless-work add escrows/details
1091
-
1092
- ----------------------
1093
- --- SINGLE-RELEASE ---
1094
- trustless-work add escrows/single-release
1095
-
1096
- --- Initialize escrow ---
1097
- - trustless-work add escrows/single-release/initialize-escrow
1098
- - trustless-work add escrows/single-release/initialize-escrow/form
1099
- - trustless-work add escrows/single-release/initialize-escrow/dialog
1100
-
1101
- --- Approve milestone ---
1102
- - trustless-work add escrows/single-release/approve-milestone
1103
- - trustless-work add escrows/single-release/approve-milestone/form
1104
- - trustless-work add escrows/single-release/approve-milestone/button
1105
- - trustless-work add escrows/single-release/approve-milestone/dialog
1106
-
1107
- --- Change milestone status ---
1108
- - trustless-work add escrows/single-release/change-milestone-status
1109
- - trustless-work add escrows/single-release/change-milestone-status/form
1110
- - trustless-work add escrows/single-release/change-milestone-status/button
1111
- - trustless-work add escrows/single-release/change-milestone-status/dialog
1112
-
1113
- --- Fund escrow ---
1114
- - trustless-work add escrows/single-release/fund-escrow
1115
- - trustless-work add escrows/single-release/fund-escrow/form
1116
- - trustless-work add escrows/single-release/fund-escrow/button
1117
- - trustless-work add escrows/single-release/fund-escrow/dialog
1118
-
1119
- --- Resolve dispute ---
1120
- - trustless-work add escrows/single-release/resolve-dispute
1121
- - trustless-work add escrows/single-release/resolve-dispute/form
1122
- - trustless-work add escrows/single-release/resolve-dispute/button
1123
- - trustless-work add escrows/single-release/resolve-dispute/dialog
1124
-
1125
- --- Update escrow ---
1126
- - trustless-work add escrows/single-release/update-escrow
1127
- - trustless-work add escrows/single-release/update-escrow/form
1128
- - trustless-work add escrows/single-release/update-escrow/dialog
1129
-
1130
- --- Release escrow ---
1131
- - trustless-work add escrows/single-release/release-escrow
1132
- - trustless-work add escrows/single-release/release-escrow/button
1133
-
1134
- --- Dispute escrow ---
1135
- - trustless-work add escrows/single-release/dispute-escrow
1136
- - trustless-work add escrows/single-release/dispute-escrow/button
1137
- `);
1138
- }
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
+ try {
818
+ const isMultiUpdateRoot = name === "escrows/multi-release/update-escrow";
819
+ const isMultiUpdateDialog =
820
+ name === "escrows/multi-release/update-escrow/dialog";
821
+ const isMultiUpdateForm =
822
+ name === "escrows/multi-release/update-escrow/form";
823
+
824
+ const srcSharedDir = path.join(
825
+ TEMPLATES_DIR,
826
+ "escrows",
827
+ "multi-release",
828
+ "update-escrow",
829
+ "shared"
830
+ );
831
+
832
+ function copyMultiUpdateSharedInto(targetDir) {
833
+ if (!fs.existsSync(srcSharedDir)) return;
834
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
835
+ for (const entry of entries) {
836
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
837
+ const entrySrc = path.join(srcSharedDir, entry.name);
838
+ const entryDest = path.join(targetDir, entry.name);
839
+ writeTransformed(entrySrc, entryDest);
840
+ }
841
+ }
842
+
843
+ if (isMultiUpdateRoot) {
844
+ copyMultiUpdateSharedInto(path.join(destDir, "dialog"));
845
+ copyMultiUpdateSharedInto(path.join(destDir, "form"));
846
+ } else if (isMultiUpdateDialog) {
847
+ copyMultiUpdateSharedInto(destDir);
848
+ } else if (isMultiUpdateForm) {
849
+ copyMultiUpdateSharedInto(destDir);
850
+ }
851
+ } catch (e) {
852
+ console.warn(
853
+ "⚠️ Failed to materialize shared multi-release update-escrow files:",
854
+ e?.message || e
855
+ );
856
+ }
857
+
858
+ // If adding the whole single-release bundle, materialize all shared files
859
+ try {
860
+ if (name === "escrows/single-release") {
861
+ const modules = [
862
+ "initialize-escrow",
863
+ "approve-milestone",
864
+ "change-milestone-status",
865
+ "fund-escrow",
866
+ "resolve-dispute",
867
+ "update-escrow",
868
+ ];
869
+
870
+ for (const mod of modules) {
871
+ const srcSharedDir = path.join(
872
+ TEMPLATES_DIR,
873
+ "escrows",
874
+ mod === "approve-milestone" ? "shared" : "single-release",
875
+ mod === "approve-milestone" ? "approve-milestone" : mod,
876
+ "shared"
877
+ );
878
+ if (!fs.existsSync(srcSharedDir)) continue;
879
+
880
+ const targets = [
881
+ path.join(destDir, mod, "dialog"),
882
+ path.join(destDir, mod, "form"),
883
+ ];
884
+
885
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
886
+ for (const entry of entries) {
887
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
888
+ const entrySrc = path.join(srcSharedDir, entry.name);
889
+ for (const t of targets) {
890
+ const entryDest = path.join(t, entry.name);
891
+ writeTransformed(entrySrc, entryDest);
892
+ }
893
+ }
894
+ }
895
+ }
896
+ } catch (e) {
897
+ console.warn(
898
+ "⚠️ Failed to materialize shared files for single-release bundle:",
899
+ e?.message || e
900
+ );
901
+ }
902
+
903
+ // If adding the whole multi-release bundle, materialize all shared files
904
+ try {
905
+ if (name === "escrows/multi-release") {
906
+ const modules = [
907
+ "initialize-escrow",
908
+ "resolve-dispute",
909
+ "update-escrow",
910
+ ];
911
+
912
+ for (const mod of modules) {
913
+ const srcSharedDir = path.join(
914
+ TEMPLATES_DIR,
915
+ "escrows",
916
+ "multi-release",
917
+ mod,
918
+ "shared"
919
+ );
920
+ if (!fs.existsSync(srcSharedDir)) continue;
921
+
922
+ const targets = [
923
+ path.join(destDir, mod, "dialog"),
924
+ path.join(destDir, mod, "form"),
925
+ ];
926
+
927
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
928
+ for (const entry of entries) {
929
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
930
+ const entrySrc = path.join(srcSharedDir, entry.name);
931
+ for (const t of targets) {
932
+ const entryDest = path.join(t, entry.name);
933
+ writeTransformed(entrySrc, entryDest);
934
+ }
935
+ }
936
+ }
937
+ }
938
+ } catch (e) {
939
+ console.warn(
940
+ "⚠️ Failed to materialize shared files for multi-release bundle:",
941
+ e?.message || e
942
+ );
943
+ }
944
+
945
+ // If adding the root escrows bundle, also materialize single-release shared files
946
+ try {
947
+ if (name === "escrows") {
948
+ const modules = [
949
+ "initialize-escrow",
950
+ "approve-milestone",
951
+ "change-milestone-status",
952
+ "fund-escrow",
953
+ "resolve-dispute",
954
+ "update-escrow",
955
+ ];
956
+
957
+ const baseTarget = path.join(destDir, "single-release");
958
+ for (const mod of modules) {
959
+ const srcSharedDir = path.join(
960
+ TEMPLATES_DIR,
961
+ "escrows",
962
+ mod === "approve-milestone" ? "shared" : "single-release",
963
+ mod === "approve-milestone" ? "approve-milestone" : mod,
964
+ "shared"
965
+ );
966
+ if (!fs.existsSync(srcSharedDir)) continue;
967
+
968
+ const targets = [
969
+ path.join(baseTarget, mod, "dialog"),
970
+ path.join(baseTarget, mod, "form"),
971
+ ];
972
+
973
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
974
+ for (const entry of entries) {
975
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
976
+ const entrySrc = path.join(srcSharedDir, entry.name);
977
+ for (const t of targets) {
978
+ const entryDest = path.join(t, entry.name);
979
+ writeTransformed(entrySrc, entryDest);
980
+ }
981
+ }
982
+ }
983
+ }
984
+ } catch (e) {
985
+ console.warn(
986
+ "⚠️ Failed to materialize shared files for escrows root:",
987
+ e?.message || e
988
+ );
989
+ }
990
+
991
+ // If adding the root escrows bundle, also materialize multi-release shared files
992
+ try {
993
+ if (name === "escrows") {
994
+ const modules = [
995
+ "initialize-escrow",
996
+ "resolve-dispute",
997
+ "update-escrow",
998
+ ];
999
+
1000
+ const baseTarget = path.join(destDir, "multi-release");
1001
+ for (const mod of modules) {
1002
+ const srcSharedDir = path.join(
1003
+ TEMPLATES_DIR,
1004
+ "escrows",
1005
+ "multi-release",
1006
+ mod,
1007
+ "shared"
1008
+ );
1009
+ if (!fs.existsSync(srcSharedDir)) continue;
1010
+
1011
+ const targets = [
1012
+ path.join(baseTarget, mod, "dialog"),
1013
+ path.join(baseTarget, mod, "form"),
1014
+ ];
1015
+
1016
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
1017
+ for (const entry of entries) {
1018
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
1019
+ const entrySrc = path.join(srcSharedDir, entry.name);
1020
+ for (const t of targets) {
1021
+ const entryDest = path.join(t, entry.name);
1022
+ writeTransformed(entrySrc, entryDest);
1023
+ }
1024
+ }
1025
+ }
1026
+ }
1027
+ } catch (e) {
1028
+ console.warn(
1029
+ "⚠️ Failed to materialize shared files for escrows root (multi-release):",
1030
+ e?.message || e
1031
+ );
1032
+ }
1033
+ } else if (fs.existsSync(srcFile)) {
1034
+ fs.mkdirSync(outRoot, { recursive: true });
1035
+ const destFile = path.join(outRoot, name + ".tsx");
1036
+ writeTransformed(srcFile, destFile);
1037
+ } else {
1038
+ console.error(`❌ The template "${name}" does not exist`);
1039
+ process.exit(1);
1040
+ }
1041
+
1042
+ if (shouldInstall && fs.existsSync(GLOBAL_DEPS_FILE)) {
1043
+ const meta = JSON.parse(fs.readFileSync(GLOBAL_DEPS_FILE, "utf8"));
1044
+ installDeps(meta);
1045
+ }
1046
+ }
1047
+
1048
+ function copySharedDetailsInto(targetRelativeDir, { uiBase } = {}) {
1049
+ const srcDir = path.join(TEMPLATES_DIR, "escrows", "details");
1050
+ const outRoot = path.join(PROJECT_ROOT, "src", "components", "tw-blocks");
1051
+ const destDir = path.join(outRoot, targetRelativeDir);
1052
+ const config = loadConfig();
1053
+ const effectiveUiBase = uiBase || config.uiBase || "@/components/ui";
1054
+
1055
+ if (!fs.existsSync(srcDir)) return;
1056
+ fs.mkdirSync(destDir, { recursive: true });
1057
+
1058
+ function writeTransformed(srcPath, destPath) {
1059
+ const raw = fs.readFileSync(srcPath, "utf8");
1060
+ let transformed = raw.replaceAll("__UI_BASE__", effectiveUiBase);
1061
+ // Resolve details placeholders to either multi-release modules (if present) or local compat
1062
+ const resolveImport = (segments, compatFile) => {
1063
+ const realWithExt = path.join(
1064
+ outRoot,
1065
+ "escrows",
1066
+ "multi-release",
1067
+ ...segments
1068
+ );
1069
+ const realCandidate = [
1070
+ realWithExt,
1071
+ realWithExt + ".tsx",
1072
+ realWithExt + ".ts",
1073
+ realWithExt + ".jsx",
1074
+ realWithExt + ".js",
1075
+ ].find((p) => fs.existsSync(p));
1076
+ const realNoExt = realCandidate
1077
+ ? realCandidate.replace(/\.(tsx|ts|jsx|js)$/i, "")
1078
+ : null;
1079
+ const compatWithExt = path.join(
1080
+ path.dirname(destPath),
1081
+ "compat",
1082
+ compatFile
1083
+ );
1084
+ const compatCandidate = [
1085
+ compatWithExt,
1086
+ compatWithExt + ".tsx",
1087
+ compatWithExt + ".ts",
1088
+ compatWithExt + ".jsx",
1089
+ compatWithExt + ".js",
1090
+ ].find((p) => fs.existsSync(p));
1091
+ const compatNoExt = (compatCandidate || compatWithExt).replace(
1092
+ /\.(tsx|ts|jsx|js)$/i,
1093
+ ""
1094
+ );
1095
+ const target = realNoExt || compatNoExt;
1096
+ let rel = path.relative(path.dirname(destPath), target);
1097
+ rel = rel.split(path.sep).join("/");
1098
+ if (!rel.startsWith(".")) rel = "./" + rel;
1099
+ return rel;
1100
+ };
1101
+ transformed = transformed
1102
+ .replaceAll(
1103
+ "__MR_RELEASE_MODULE__",
1104
+ resolveImport(
1105
+ ["release-escrow", "button", "ReleaseEscrow"],
1106
+ "ReleaseEscrow"
1107
+ )
1108
+ )
1109
+ .replaceAll(
1110
+ "__MR_DISPUTE_MODULE__",
1111
+ resolveImport(
1112
+ ["dispute-escrow", "button", "DisputeEscrow"],
1113
+ "DisputeEscrow"
1114
+ )
1115
+ )
1116
+ .replaceAll(
1117
+ "__MR_RESOLVE_MODULE__",
1118
+ resolveImport(
1119
+ ["resolve-dispute", "dialog", "ResolveDispute"],
1120
+ "ResolveDispute"
1121
+ )
1122
+ );
1123
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
1124
+ fs.writeFileSync(destPath, transformed, "utf8");
1125
+ console.log(`✅ ${path.relative(PROJECT_ROOT, destPath)} created`);
1126
+ }
1127
+
1128
+ const stack = [""];
1129
+ while (stack.length) {
1130
+ const rel = stack.pop();
1131
+ const current = path.join(srcDir, rel);
1132
+ const entries = fs.readdirSync(current, { withFileTypes: true });
1133
+ for (const entry of entries) {
1134
+ const entryRel = path.join(rel, entry.name);
1135
+ const entrySrc = path.join(srcDir, entryRel);
1136
+ const entryDest = path.join(destDir, entryRel);
1137
+ if (entry.isDirectory()) {
1138
+ stack.push(entryRel);
1139
+ continue;
1140
+ }
1141
+ if (/\.(tsx?|jsx?)$/i.test(entry.name)) {
1142
+ writeTransformed(entrySrc, entryDest);
1143
+ } else {
1144
+ fs.mkdirSync(path.dirname(entryDest), { recursive: true });
1145
+ fs.copyFileSync(entrySrc, entryDest);
1146
+ console.log(`✅ ${path.relative(PROJECT_ROOT, entryDest)} created`);
1147
+ }
1148
+ }
1149
+ }
1150
+ }
1151
+
1152
+ function copySharedRoleSignerHooks(kind = "both") {
1153
+ const outRoot = path.join(PROJECT_ROOT, "src", "components", "tw-blocks");
1154
+
1155
+ const mappings = [];
1156
+ if (kind === "both" || kind === "role") {
1157
+ mappings.push({
1158
+ src: path.join(
1159
+ TEMPLATES_DIR,
1160
+ "escrows",
1161
+ "escrows-by-role",
1162
+ "useEscrowsByRole.shared.ts"
1163
+ ),
1164
+ dest: path.join(
1165
+ outRoot,
1166
+ "escrows",
1167
+ "escrows-by-role",
1168
+ "useEscrowsByRole.shared.ts"
1169
+ ),
1170
+ });
1171
+ }
1172
+ if (kind === "both" || kind === "signer") {
1173
+ mappings.push({
1174
+ src: path.join(
1175
+ TEMPLATES_DIR,
1176
+ "escrows",
1177
+ "escrows-by-signer",
1178
+ "useEscrowsBySigner.shared.ts"
1179
+ ),
1180
+ dest: path.join(
1181
+ outRoot,
1182
+ "escrows",
1183
+ "escrows-by-signer",
1184
+ "useEscrowsBySigner.shared.ts"
1185
+ ),
1186
+ });
1187
+ }
1188
+
1189
+ for (const { src, dest } of mappings) {
1190
+ if (!fs.existsSync(src)) continue;
1191
+ const raw = fs.readFileSync(src, "utf8");
1192
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
1193
+ fs.writeFileSync(dest, raw, "utf8");
1194
+ console.log(`✅ ${path.relative(PROJECT_ROOT, dest)} created`);
1195
+ }
1196
+ }
1197
+
1198
+ function findLayoutFile() {
1199
+ const candidates = [
1200
+ path.join(PROJECT_ROOT, "app", "layout.tsx"),
1201
+ path.join(PROJECT_ROOT, "app", "layout.ts"),
1202
+ path.join(PROJECT_ROOT, "app", "layout.jsx"),
1203
+ path.join(PROJECT_ROOT, "app", "layout.js"),
1204
+ path.join(PROJECT_ROOT, "src", "app", "layout.tsx"),
1205
+ path.join(PROJECT_ROOT, "src", "app", "layout.ts"),
1206
+ path.join(PROJECT_ROOT, "src", "app", "layout.jsx"),
1207
+ path.join(PROJECT_ROOT, "src", "app", "layout.js"),
1208
+ ];
1209
+ return candidates.find((p) => fs.existsSync(p)) || null;
1210
+ }
1211
+
1212
+ function injectProvidersIntoLayout(
1213
+ layoutPath,
1214
+ { reactQuery = false, trustless = false, wallet = false, escrow = false } = {}
1215
+ ) {
1216
+ try {
1217
+ let content = fs.readFileSync(layoutPath, "utf8");
1218
+
1219
+ const importRQ =
1220
+ 'import { ReactQueryClientProvider } from "@/components/tw-blocks/providers/ReactQueryClientProvider";\n';
1221
+ const importTW =
1222
+ 'import { TrustlessWorkProvider } from "@/components/tw-blocks/providers/TrustlessWork";\n';
1223
+ const importEscrow =
1224
+ 'import { EscrowProvider } from "@/components/tw-blocks/providers/EscrowProvider";\n';
1225
+ const importWallet =
1226
+ 'import { WalletProvider } from "@/components/tw-blocks/wallet-kit/WalletProvider";\n';
1227
+ const commentText =
1228
+ "// Use these imports to wrap your application (<ReactQueryClientProvider>, <TrustlessWorkProvider>, <WalletProvider> y <EscrowProvider>)\n";
1229
+
1230
+ const needImport = (name) =>
1231
+ !new RegExp(
1232
+ `import\\s+[^;]*${name}[^;]*from\\s+['\"][^'\"]+['\"];?`
1233
+ ).test(content);
1234
+
1235
+ let importsToAdd = "";
1236
+ if (reactQuery && needImport("ReactQueryClientProvider"))
1237
+ importsToAdd += importRQ;
1238
+ if (trustless && needImport("TrustlessWorkProvider"))
1239
+ importsToAdd += importTW;
1240
+ if (wallet && needImport("WalletProvider")) importsToAdd += importWallet;
1241
+ if (escrow && needImport("EscrowProvider")) importsToAdd += importEscrow;
1242
+
1243
+ if (importsToAdd) {
1244
+ const importStmtRegex = /^import.*;\s*$/gm;
1245
+ let last = null;
1246
+ for (const m of content.matchAll(importStmtRegex)) last = m;
1247
+ if (last) {
1248
+ const idx = last.index + last[0].length;
1249
+ content =
1250
+ content.slice(0, idx) +
1251
+ "\n" +
1252
+ importsToAdd +
1253
+ commentText +
1254
+ content.slice(idx);
1255
+ } else {
1256
+ content = importsToAdd + commentText + content;
1257
+ }
1258
+ }
1259
+
1260
+ const hasTag = (tag) => new RegExp(`<${tag}[\\s>]`).test(content);
1261
+ const wrapInside = (containerTag, newTag) => {
1262
+ const open = content.match(new RegExp(`<${containerTag}(\\s[^>]*)?>`));
1263
+ if (!open) return false;
1264
+ const openIdx = open.index + open[0].length;
1265
+ const closeIdx = content.indexOf(`</${containerTag}>`, openIdx);
1266
+ if (closeIdx === -1) return false;
1267
+ content =
1268
+ content.slice(0, openIdx) +
1269
+ `\n<${newTag}>\n` +
1270
+ content.slice(openIdx, closeIdx) +
1271
+ `\n</${newTag}>\n` +
1272
+ content.slice(closeIdx);
1273
+ return true;
1274
+ };
1275
+
1276
+ const ensureTag = (tag) => {
1277
+ if (hasTag(tag)) return;
1278
+ const bodyOpen = content.match(/<body[^>]*>/);
1279
+ const bodyCloseIdx = content.lastIndexOf("</body>");
1280
+ if (!bodyOpen || bodyCloseIdx === -1) return;
1281
+ const bodyOpenIdx = bodyOpen.index + bodyOpen[0].length;
1282
+ if (tag === "TrustlessWorkProvider") {
1283
+ if (wrapInside("ReactQueryClientProvider", tag)) return;
1284
+ }
1285
+ if (tag === "WalletProvider") {
1286
+ if (wrapInside("TrustlessWorkProvider", tag)) return;
1287
+ if (wrapInside("ReactQueryClientProvider", tag)) return;
1288
+ }
1289
+ if (tag === "EscrowProvider") {
1290
+ if (wrapInside("WalletProvider", tag)) return;
1291
+ if (wrapInside("TrustlessWorkProvider", tag)) return;
1292
+ if (wrapInside("ReactQueryClientProvider", tag)) return;
1293
+ }
1294
+ content =
1295
+ content.slice(0, bodyOpenIdx) +
1296
+ `\n<${tag}>\n` +
1297
+ content.slice(bodyOpenIdx, bodyCloseIdx) +
1298
+ `\n</${tag}>\n` +
1299
+ content.slice(bodyCloseIdx);
1300
+ };
1301
+
1302
+ if (reactQuery) ensureTag("ReactQueryClientProvider");
1303
+ if (trustless) ensureTag("TrustlessWorkProvider");
1304
+ if (wallet) ensureTag("WalletProvider");
1305
+ if (escrow) ensureTag("EscrowProvider");
1306
+
1307
+ fs.writeFileSync(layoutPath, content, "utf8");
1308
+ logCheck(
1309
+ `Updated ${path.relative(PROJECT_ROOT, layoutPath)} with providers`
1310
+ );
1311
+ } catch (e) {
1312
+ console.error("❌ Failed to update layout with providers:", e.message);
1313
+ }
1314
+ }
1315
+
1316
+ if (args[0] === "init") {
1317
+ console.log("\n▶ Setting up shadcn/ui components...");
1318
+ const doInit = await promptYesNo("Run shadcn init now?", true);
1319
+ if (doInit) {
1320
+ run("npx", ["shadcn@latest", "init"]);
1321
+ } else {
1322
+ console.log("\x1b[90m– Skipped shadcn init\x1b[0m");
1323
+ }
1324
+
1325
+ const addShadcn = await promptYesNo(
1326
+ "Add shadcn components (button, input, form, card, sonner, checkbox, dialog, textarea, sonner, select, table, calendar, popover, separator, calendar-05, badge, sheet, tabs, avatar, tooltip)?",
1327
+ true
1328
+ );
1329
+ if (addShadcn) {
1330
+ await withSpinner("Installing shadcn/ui components", async () => {
1331
+ await runAsync("npx", [
1332
+ "shadcn@latest",
1333
+ "add",
1334
+ "button",
1335
+ "input",
1336
+ "form",
1337
+ "card",
1338
+ "sonner",
1339
+ "checkbox",
1340
+ "dialog",
1341
+ "textarea",
1342
+ "sonner",
1343
+ "select",
1344
+ "table",
1345
+ "calendar",
1346
+ "popover",
1347
+ "separator",
1348
+ "calendar-05",
1349
+ "badge",
1350
+ "sheet",
1351
+ "tabs",
1352
+ "avatar",
1353
+ "tooltip",
1354
+ ]);
1355
+ });
1356
+ } else {
1357
+ console.log("\x1b[90m– Skipped adding shadcn components\x1b[0m");
1358
+ }
1359
+
1360
+ if (!fs.existsSync(GLOBAL_DEPS_FILE)) {
1361
+ console.error("❌ deps.json not found in templates/");
1362
+ process.exit(1);
1363
+ }
1364
+ const meta = JSON.parse(fs.readFileSync(GLOBAL_DEPS_FILE, "utf8"));
1365
+ const installLibs = await promptYesNo(
1366
+ "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 & zod) dependencies now?",
1367
+ true
1368
+ );
1369
+ if (installLibs) {
1370
+ await withSpinner("Installing required dependencies", async () => {
1371
+ installDeps(meta);
1372
+ });
1373
+ } else {
1374
+ console.log("\x1b[90m– Skipped installing required dependencies\x1b[0m");
1375
+ }
1376
+ const cfgPath = path.join(PROJECT_ROOT, ".twblocks.json");
1377
+ if (!fs.existsSync(cfgPath)) {
1378
+ fs.writeFileSync(
1379
+ cfgPath,
1380
+ JSON.stringify({ uiBase: "@/components/ui" }, null, 2)
1381
+ );
1382
+ console.log(
1383
+ `\x1b[32m✔\x1b[0m Created ${path.relative(
1384
+ PROJECT_ROOT,
1385
+ cfgPath
1386
+ )} with default uiBase`
1387
+ );
1388
+ }
1389
+ console.log("\x1b[32m✔\x1b[0m shadcn/ui components step completed");
1390
+
1391
+ const wantProviders = await promptYesNo(
1392
+ "Install TanStack Query and Trustless Work providers and wrap app/layout with them?",
1393
+ true
1394
+ );
1395
+ if (wantProviders) {
1396
+ await withSpinner("Installing providers", async () => {
1397
+ copyTemplate("providers");
1398
+ });
1399
+ const layoutPath = findLayoutFile();
1400
+ if (layoutPath) {
1401
+ await withSpinner("Updating app/layout with providers", async () => {
1402
+ injectProvidersIntoLayout(layoutPath, {
1403
+ reactQuery: true,
1404
+ trustless: true,
1405
+ });
1406
+ });
1407
+ } else {
1408
+ console.warn(
1409
+ "⚠️ Could not find app/layout file. Skipped automatic wiring."
1410
+ );
1411
+ }
1412
+ } else {
1413
+ console.log("\x1b[90m– Skipped installing providers\x1b[0m");
1414
+ }
1415
+
1416
+ printBannerTRUSTLESSWORK();
1417
+ console.log("\n\nResources");
1418
+ console.log("- " + oscHyperlink("Website", "https://trustlesswork.com"));
1419
+ console.log(
1420
+ "- " + oscHyperlink("Documentation", "https://docs.trustlesswork.com")
1421
+ );
1422
+ console.log("- " + oscHyperlink("Demo", "https://demo.trustlesswork.com"));
1423
+ console.log(
1424
+ "- " + oscHyperlink("Backoffice", "https://dapp.trustlesswork.com")
1425
+ );
1426
+ console.log(
1427
+ "- " + oscHyperlink("GitHub", "https://github.com/trustless-work")
1428
+ );
1429
+ console.log(
1430
+ "- " + oscHyperlink("Escrow Viewer", "https://viewer.trustlesswork.com")
1431
+ );
1432
+ console.log(
1433
+ "- " + oscHyperlink("Telegram", "https://t.me/+kmr8tGegxLU0NTA5")
1434
+ );
1435
+ console.log(
1436
+ "- " +
1437
+ oscHyperlink(
1438
+ "LinkedIn",
1439
+ "https://www.linkedin.com/company/trustlesswork/posts/?feedView=all"
1440
+ )
1441
+ );
1442
+ console.log("- " + oscHyperlink("X", "https://x.com/TrustlessWork"));
1443
+ } else if (args[0] === "add" && args[1]) {
1444
+ const flags = parseFlags(args.slice(2));
1445
+ const cfgPath = path.join(PROJECT_ROOT, ".twblocks.json");
1446
+ if (!fs.existsSync(cfgPath)) {
1447
+ console.error(
1448
+ "❌ Missing initial setup. Run 'trustless-work init' first to install dependencies and create .twblocks.json (uiBase)."
1449
+ );
1450
+ console.error(
1451
+ " After init, re-run: trustless-work add " +
1452
+ args[1] +
1453
+ (flags.uiBase ? ' --ui-base "' + flags.uiBase + '"' : "")
1454
+ );
1455
+ process.exit(1);
1456
+ }
1457
+ copyTemplate(args[1], {
1458
+ uiBase: flags.uiBase,
1459
+ shouldInstall: !!flags.install,
1460
+ });
1461
+
1462
+ // Post-add wiring for specific templates
1463
+ const layoutPath = findLayoutFile();
1464
+ if (layoutPath) {
1465
+ if (args[1] === "wallet-kit" || args[1].startsWith("wallet-kit/")) {
1466
+ injectProvidersIntoLayout(layoutPath, { wallet: true });
1467
+ }
1468
+ }
1469
+
1470
+ // Copy shared details into role/signer targets when applicable
1471
+ try {
1472
+ if (args[1] === "escrows") {
1473
+ copySharedDetailsInto("escrows/escrows-by-role/details", {
1474
+ uiBase: flags.uiBase,
1475
+ });
1476
+ copySharedDetailsInto("escrows/escrows-by-signer/details", {
1477
+ uiBase: flags.uiBase,
1478
+ });
1479
+ copySharedRoleSignerHooks("both");
1480
+ }
1481
+ if (
1482
+ args[1] === "escrows/escrows-by-role" ||
1483
+ args[1].startsWith("escrows/escrows-by-role/")
1484
+ ) {
1485
+ copySharedDetailsInto("escrows/escrows-by-role/details", {
1486
+ uiBase: flags.uiBase,
1487
+ });
1488
+ copySharedRoleSignerHooks("role");
1489
+ }
1490
+ if (
1491
+ args[1] === "escrows/escrows-by-signer" ||
1492
+ args[1].startsWith("escrows/escrows-by-signer/")
1493
+ ) {
1494
+ copySharedDetailsInto("escrows/escrows-by-signer/details", {
1495
+ uiBase: flags.uiBase,
1496
+ });
1497
+ copySharedRoleSignerHooks("signer");
1498
+ }
1499
+ } catch (e) {
1500
+ console.warn("⚠️ Failed to copy shared details:", e?.message || e);
1501
+ }
1502
+ } else {
1503
+ console.log(`
1504
+
1505
+ Usage:
1506
+
1507
+ trustless-work init
1508
+ trustless-work add <template> [--install]
1509
+
1510
+ Options:
1511
+
1512
+ --ui-base <path> Base import path to your shadcn/ui components (default: "@/components/ui")
1513
+ --install, -i Also install dependencies (normally use 'init' once instead)
1514
+
1515
+ Examples:
1516
+
1517
+ --- Get started ---
1518
+ trustless-work init
1519
+
1520
+ --- Providers ---
1521
+ trustless-work add providers
1522
+
1523
+ --- Wallet-kit ---
1524
+ trustless-work add wallet-kit
1525
+
1526
+ --- Handle-errors ---
1527
+ trustless-work add handle-errors
1528
+
1529
+ --- Helpers ---
1530
+ trustless-work add helpers
1531
+
1532
+ --- Tanstack ---
1533
+ trustless-work add tanstack
1534
+
1535
+ --- Escrows ---
1536
+ trustless-work add escrows
1537
+
1538
+ --- Escrows by role ---
1539
+ trustless-work add escrows/escrows-by-role
1540
+ trustless-work add escrows/escrows-by-role/table
1541
+ trustless-work add escrows/escrows-by-role/cards
1542
+
1543
+ --- Escrows by signer ---
1544
+ trustless-work add escrows/escrows-by-signer
1545
+ trustless-work add escrows/escrows-by-signer/table
1546
+ trustless-work add escrows/escrows-by-signer/cards
1547
+
1548
+ --- Escrow details (optional standalone) ---
1549
+ trustless-work add escrows/details
1550
+
1551
+ ----------------------
1552
+ --- SINGLE-RELEASE ---
1553
+ trustless-work add escrows/single-release
1554
+
1555
+ --- Initialize escrow ---
1556
+ - trustless-work add escrows/single-release/initialize-escrow
1557
+ - trustless-work add escrows/single-release/initialize-escrow/form
1558
+ - trustless-work add escrows/single-release/initialize-escrow/dialog
1559
+
1560
+ --- Approve milestone ---
1561
+ - trustless-work add escrows/single-release/approve-milestone
1562
+ - trustless-work add escrows/single-release/approve-milestone/form
1563
+ - trustless-work add escrows/single-release/approve-milestone/button
1564
+ - trustless-work add escrows/single-release/approve-milestone/dialog
1565
+
1566
+ --- Change milestone status ---
1567
+ - trustless-work add escrows/single-release/change-milestone-status
1568
+ - trustless-work add escrows/single-release/change-milestone-status/form
1569
+ - trustless-work add escrows/single-release/change-milestone-status/button
1570
+ - trustless-work add escrows/single-release/change-milestone-status/dialog
1571
+
1572
+ --- Fund escrow ---
1573
+ - trustless-work add escrows/single-release/fund-escrow
1574
+ - trustless-work add escrows/single-release/fund-escrow/form
1575
+ - trustless-work add escrows/single-release/fund-escrow/button
1576
+ - trustless-work add escrows/single-release/fund-escrow/dialog
1577
+
1578
+ --- Resolve dispute ---
1579
+ - trustless-work add escrows/single-release/resolve-dispute
1580
+ - trustless-work add escrows/single-release/resolve-dispute/form
1581
+ - trustless-work add escrows/single-release/resolve-dispute/button
1582
+ - trustless-work add escrows/single-release/resolve-dispute/dialog
1583
+
1584
+ --- Update escrow ---
1585
+ - trustless-work add escrows/single-release/update-escrow
1586
+ - trustless-work add escrows/single-release/update-escrow/form
1587
+ - trustless-work add escrows/single-release/update-escrow/dialog
1588
+
1589
+ --- Release escrow ---
1590
+ - trustless-work add escrows/single-release/release-escrow
1591
+ - trustless-work add escrows/single-release/release-escrow/button
1592
+
1593
+ --- Dispute escrow ---
1594
+ - trustless-work add escrows/single-release/dispute-escrow
1595
+ - trustless-work add escrows/single-release/dispute-escrow/button
1596
+ `);
1597
+ }