@trustless-work/blocks 0.0.7 → 0.0.9

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 (66) hide show
  1. package/bin/index.js +719 -17
  2. package/package.json +1 -1
  3. package/templates/escrows/details/Actions.tsx +144 -149
  4. package/templates/escrows/details/Entities.tsx +1 -1
  5. package/templates/escrows/details/EntityCard.tsx +1 -3
  6. package/templates/escrows/details/EscrowDetailDialog.tsx +16 -16
  7. package/templates/escrows/details/GeneralInformation.tsx +19 -22
  8. package/templates/escrows/details/MilestoneCard.tsx +46 -47
  9. package/templates/escrows/details/MilestoneDetailDialog.tsx +1 -2
  10. package/templates/escrows/details/Milestones.tsx +0 -5
  11. package/templates/escrows/details/SuccessReleaseDialog.tsx +4 -6
  12. package/templates/escrows/escrows-by-role/cards/EscrowsCards.tsx +84 -49
  13. package/templates/escrows/escrows-by-role/cards/Filters.tsx +3 -5
  14. package/templates/escrows/escrows-by-role/table/EscrowsTable.tsx +8 -26
  15. package/templates/escrows/escrows-by-role/table/Filters.tsx +3 -5
  16. package/templates/escrows/escrows-by-signer/cards/EscrowsCards.tsx +89 -55
  17. package/templates/escrows/escrows-by-signer/cards/Filters.tsx +3 -5
  18. package/templates/escrows/escrows-by-signer/table/EscrowsTable.tsx +8 -24
  19. package/templates/escrows/escrows-by-signer/table/Filters.tsx +3 -5
  20. package/templates/escrows/multi-release/dispute-milestone/button/DisputeEscrow.tsx +98 -0
  21. package/templates/escrows/multi-release/initialize-escrow/dialog/InitializeEscrow.tsx +528 -0
  22. package/templates/escrows/multi-release/initialize-escrow/form/InitializeEscrow.tsx +506 -0
  23. package/templates/escrows/multi-release/initialize-escrow/shared/schema.ts +179 -0
  24. package/templates/escrows/multi-release/initialize-escrow/shared/useInitializeEscrow.ts +175 -0
  25. package/templates/escrows/multi-release/release-milestone/button/ReleaseEscrow.tsx +116 -0
  26. package/templates/escrows/multi-release/resolve-dispute/button/ResolveDispute.tsx +122 -0
  27. package/templates/escrows/multi-release/resolve-dispute/dialog/ResolveDispute.tsx +178 -0
  28. package/templates/escrows/multi-release/resolve-dispute/form/ResolveDispute.tsx +156 -0
  29. package/templates/escrows/multi-release/resolve-dispute/shared/schema.ts +85 -0
  30. package/templates/escrows/multi-release/resolve-dispute/shared/useResolveDispute.ts +105 -0
  31. package/templates/escrows/multi-release/update-escrow/dialog/UpdateEscrow.tsx +471 -0
  32. package/templates/escrows/multi-release/update-escrow/form/UpdateEscrow.tsx +449 -0
  33. package/templates/escrows/multi-release/update-escrow/shared/schema.ts +152 -0
  34. package/templates/escrows/multi-release/update-escrow/shared/useUpdateEscrow.ts +254 -0
  35. package/templates/escrows/{single-release → single-multi-release}/approve-milestone/button/ApproveMilestone.tsx +20 -7
  36. package/templates/escrows/{single-release → single-multi-release}/approve-milestone/dialog/ApproveMilestone.tsx +2 -2
  37. package/templates/escrows/{single-release → single-multi-release}/approve-milestone/form/ApproveMilestone.tsx +2 -2
  38. package/templates/escrows/{single-release → single-multi-release}/approve-milestone/shared/useApproveMilestone.ts +16 -16
  39. package/templates/escrows/{single-release → single-multi-release}/change-milestone-status/button/ChangeMilestoneStatus.tsx +4 -4
  40. package/templates/escrows/{single-release → single-multi-release}/change-milestone-status/dialog/ChangeMilestoneStatus.tsx +3 -3
  41. package/templates/escrows/{single-release → single-multi-release}/change-milestone-status/form/ChangeMilestoneStatus.tsx +2 -2
  42. package/templates/escrows/{single-release → single-multi-release}/change-milestone-status/shared/useChangeMilestoneStatus.ts +1 -1
  43. package/templates/escrows/{single-release → single-multi-release}/fund-escrow/button/FundEscrow.tsx +3 -3
  44. package/templates/escrows/{single-release → single-multi-release}/fund-escrow/dialog/FundEscrow.tsx +2 -2
  45. package/templates/escrows/{single-release → single-multi-release}/fund-escrow/form/FundEscrow.tsx +2 -2
  46. package/templates/escrows/{single-release → single-multi-release}/fund-escrow/shared/useFundEscrow.ts +1 -1
  47. package/templates/escrows/single-release/dispute-escrow/button/DisputeEscrow.tsx +2 -2
  48. package/templates/escrows/single-release/initialize-escrow/dialog/InitializeEscrow.tsx +14 -6
  49. package/templates/escrows/single-release/initialize-escrow/form/InitializeEscrow.tsx +14 -6
  50. package/templates/escrows/single-release/initialize-escrow/shared/schema.ts +0 -57
  51. package/templates/escrows/single-release/initialize-escrow/shared/useInitializeEscrow.ts +42 -1
  52. package/templates/escrows/single-release/release-escrow/button/ReleaseEscrow.tsx +2 -2
  53. package/templates/escrows/single-release/resolve-dispute/button/ResolveDispute.tsx +3 -3
  54. package/templates/escrows/single-release/resolve-dispute/dialog/ResolveDispute.tsx +3 -6
  55. package/templates/escrows/single-release/resolve-dispute/form/ResolveDispute.tsx +2 -2
  56. package/templates/escrows/single-release/resolve-dispute/shared/useResolveDispute.ts +14 -1
  57. package/templates/escrows/single-release/update-escrow/dialog/UpdateEscrow.tsx +2 -2
  58. package/templates/escrows/single-release/update-escrow/form/UpdateEscrow.tsx +2 -2
  59. package/templates/escrows/single-release/update-escrow/shared/useUpdateEscrow.ts +12 -7
  60. package/templates/providers/EscrowDialogsProvider.tsx +1 -3
  61. package/templates/providers/EscrowProvider.tsx +27 -4
  62. package/templates/providers/TrustlessWork.tsx +1 -1
  63. package/templates/escrows/details/ProgressEscrow.tsx +0 -191
  64. /package/templates/escrows/{single-release → single-multi-release}/approve-milestone/shared/schema.ts +0 -0
  65. /package/templates/escrows/{single-release → single-multi-release}/change-milestone-status/shared/schema.ts +0 -0
  66. /package/templates/escrows/{single-release → single-multi-release}/fund-escrow/shared/schema.ts +0 -0
package/bin/index.js CHANGED
@@ -220,20 +220,211 @@ function parseFlags(argv) {
220
220
 
221
221
  function copyTemplate(name, { uiBase, shouldInstall = false } = {}) {
222
222
  const srcFile = path.join(TEMPLATES_DIR, `${name}.tsx`);
223
- const srcDir = path.join(TEMPLATES_DIR, name);
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
+ }
224
252
  const outRoot = path.join(PROJECT_ROOT, "src", "components", "tw-blocks");
225
253
 
226
254
  const config = loadConfig();
227
255
  const effectiveUiBase = uiBase || config.uiBase || "@/components/ui";
256
+ let currentEscrowType = null;
228
257
 
229
258
  function writeTransformed(srcPath, destPath) {
230
259
  const raw = fs.readFileSync(srcPath, "utf8");
231
- const transformed = raw.replaceAll("__UI_BASE__", effectiveUiBase);
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
+ }
232
332
  fs.mkdirSync(path.dirname(destPath), { recursive: true });
233
333
  fs.writeFileSync(destPath, transformed, "utf8");
234
334
  console.log(`✅ ${path.relative(PROJECT_ROOT, destPath)} created`);
235
335
  }
236
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
+
237
428
  if (fs.existsSync(srcDir) && fs.lstatSync(srcDir).isDirectory()) {
238
429
  const skipDetails =
239
430
  name === "escrows/escrows-by-role" ||
@@ -326,17 +517,20 @@ function copyTemplate(name, { uiBase, shouldInstall = false } = {}) {
326
517
  }
327
518
 
328
519
  try {
329
- const isSingleReleaseInitRoot =
330
- name === "escrows/single-release/approve-milestone";
331
- const isSingleReleaseInitDialog =
520
+ const isSRRoot = name === "escrows/single-release/approve-milestone";
521
+ const isSRDialog =
332
522
  name === "escrows/single-release/approve-milestone/dialog";
333
- const isSingleReleaseInitForm =
334
- name === "escrows/single-release/approve-milestone/form";
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";
335
529
 
336
530
  const srcSharedDir = path.join(
337
531
  TEMPLATES_DIR,
338
532
  "escrows",
339
- "single-release",
533
+ "shared",
340
534
  "approve-milestone",
341
535
  "shared"
342
536
  );
@@ -352,12 +546,12 @@ function copyTemplate(name, { uiBase, shouldInstall = false } = {}) {
352
546
  }
353
547
  }
354
548
 
355
- if (isSingleReleaseInitRoot) {
549
+ if (isSRRoot || isMRRoot) {
356
550
  copySharedInto(path.join(destDir, "dialog"));
357
551
  copySharedInto(path.join(destDir, "form"));
358
- } else if (isSingleReleaseInitDialog) {
552
+ } else if (isSRDialog || isMRDialog) {
359
553
  copySharedInto(destDir);
360
- } else if (isSingleReleaseInitForm) {
554
+ } else if (isSRForm || isMRForm) {
361
555
  copySharedInto(destDir);
362
556
  }
363
557
  } catch (e) {
@@ -535,6 +729,259 @@ function copyTemplate(name, { uiBase, shouldInstall = false } = {}) {
535
729
  );
536
730
  }
537
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
+ // Post-copy: materialize shared files for single-multi-release modules
859
+ try {
860
+ const isSingleMultiApproveRoot =
861
+ name === "escrows/single-multi-release/approve-milestone";
862
+ const isSingleMultiApproveDialog =
863
+ name === "escrows/single-multi-release/approve-milestone/dialog";
864
+ const isSingleMultiApproveForm =
865
+ name === "escrows/single-multi-release/approve-milestone/form";
866
+
867
+ const srcSharedDir = path.join(
868
+ TEMPLATES_DIR,
869
+ "escrows",
870
+ "single-multi-release",
871
+ "approve-milestone",
872
+ "shared"
873
+ );
874
+
875
+ function copySingleMultiApproveSharedInto(targetDir) {
876
+ if (!fs.existsSync(srcSharedDir)) return;
877
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
878
+ for (const entry of entries) {
879
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
880
+ const entrySrc = path.join(srcSharedDir, entry.name);
881
+ const entryDest = path.join(targetDir, entry.name);
882
+ writeTransformed(entrySrc, entryDest);
883
+ }
884
+ }
885
+
886
+ if (isSingleMultiApproveRoot) {
887
+ copySingleMultiApproveSharedInto(path.join(destDir, "dialog"));
888
+ copySingleMultiApproveSharedInto(path.join(destDir, "form"));
889
+ } else if (isSingleMultiApproveDialog) {
890
+ copySingleMultiApproveSharedInto(destDir);
891
+ } else if (isSingleMultiApproveForm) {
892
+ copySingleMultiApproveSharedInto(destDir);
893
+ }
894
+ } catch (e) {
895
+ console.warn(
896
+ "⚠️ Failed to materialize shared single-multi-release approve-milestone files:",
897
+ e?.message || e
898
+ );
899
+ }
900
+
901
+ try {
902
+ const isSingleMultiChangeRoot =
903
+ name === "escrows/single-multi-release/change-milestone-status";
904
+ const isSingleMultiChangeDialog =
905
+ name === "escrows/single-multi-release/change-milestone-status/dialog";
906
+ const isSingleMultiChangeForm =
907
+ name === "escrows/single-multi-release/change-milestone-status/form";
908
+
909
+ const srcSharedDir = path.join(
910
+ TEMPLATES_DIR,
911
+ "escrows",
912
+ "single-multi-release",
913
+ "change-milestone-status",
914
+ "shared"
915
+ );
916
+
917
+ function copySingleMultiChangeSharedInto(targetDir) {
918
+ if (!fs.existsSync(srcSharedDir)) return;
919
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
920
+ for (const entry of entries) {
921
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
922
+ const entrySrc = path.join(srcSharedDir, entry.name);
923
+ const entryDest = path.join(targetDir, entry.name);
924
+ writeTransformed(entrySrc, entryDest);
925
+ }
926
+ }
927
+
928
+ if (isSingleMultiChangeRoot) {
929
+ copySingleMultiChangeSharedInto(path.join(destDir, "dialog"));
930
+ copySingleMultiChangeSharedInto(path.join(destDir, "form"));
931
+ } else if (isSingleMultiChangeDialog) {
932
+ copySingleMultiChangeSharedInto(destDir);
933
+ } else if (isSingleMultiChangeForm) {
934
+ copySingleMultiChangeSharedInto(destDir);
935
+ }
936
+ } catch (e) {
937
+ console.warn(
938
+ "⚠️ Failed to materialize shared single-multi-release change-milestone-status files:",
939
+ e?.message || e
940
+ );
941
+ }
942
+
943
+ try {
944
+ const isSingleMultiFundRoot =
945
+ name === "escrows/single-multi-release/fund-escrow";
946
+ const isSingleMultiFundDialog =
947
+ name === "escrows/single-multi-release/fund-escrow/dialog";
948
+ const isSingleMultiFundForm =
949
+ name === "escrows/single-multi-release/fund-escrow/form";
950
+
951
+ const srcSharedDir = path.join(
952
+ TEMPLATES_DIR,
953
+ "escrows",
954
+ "single-multi-release",
955
+ "fund-escrow",
956
+ "shared"
957
+ );
958
+
959
+ function copySingleMultiFundSharedInto(targetDir) {
960
+ if (!fs.existsSync(srcSharedDir)) return;
961
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
962
+ for (const entry of entries) {
963
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
964
+ const entrySrc = path.join(srcSharedDir, entry.name);
965
+ const entryDest = path.join(targetDir, entry.name);
966
+ writeTransformed(entrySrc, entryDest);
967
+ }
968
+ }
969
+
970
+ if (isSingleMultiFundRoot) {
971
+ copySingleMultiFundSharedInto(path.join(destDir, "dialog"));
972
+ copySingleMultiFundSharedInto(path.join(destDir, "form"));
973
+ } else if (isSingleMultiFundDialog) {
974
+ copySingleMultiFundSharedInto(destDir);
975
+ } else if (isSingleMultiFundForm) {
976
+ copySingleMultiFundSharedInto(destDir);
977
+ }
978
+ } catch (e) {
979
+ console.warn(
980
+ "⚠️ Failed to materialize shared single-multi-release fund-escrow files:",
981
+ e?.message || e
982
+ );
983
+ }
984
+
538
985
  // If adding the whole single-release bundle, materialize all shared files
539
986
  try {
540
987
  if (name === "escrows/single-release") {
@@ -551,8 +998,8 @@ function copyTemplate(name, { uiBase, shouldInstall = false } = {}) {
551
998
  const srcSharedDir = path.join(
552
999
  TEMPLATES_DIR,
553
1000
  "escrows",
554
- "single-release",
555
- mod,
1001
+ mod === "approve-milestone" ? "shared" : "single-release",
1002
+ mod === "approve-milestone" ? "approve-milestone" : mod,
556
1003
  "shared"
557
1004
  );
558
1005
  if (!fs.existsSync(srcSharedDir)) continue;
@@ -580,6 +1027,90 @@ function copyTemplate(name, { uiBase, shouldInstall = false } = {}) {
580
1027
  );
581
1028
  }
582
1029
 
1030
+ // If adding the whole multi-release bundle, materialize all shared files
1031
+ try {
1032
+ if (name === "escrows/multi-release") {
1033
+ const modules = [
1034
+ "initialize-escrow",
1035
+ "resolve-dispute",
1036
+ "update-escrow",
1037
+ ];
1038
+
1039
+ for (const mod of modules) {
1040
+ const srcSharedDir = path.join(
1041
+ TEMPLATES_DIR,
1042
+ "escrows",
1043
+ "multi-release",
1044
+ mod,
1045
+ "shared"
1046
+ );
1047
+ if (!fs.existsSync(srcSharedDir)) continue;
1048
+
1049
+ const targets = [
1050
+ path.join(destDir, mod, "dialog"),
1051
+ path.join(destDir, mod, "form"),
1052
+ ];
1053
+
1054
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
1055
+ for (const entry of entries) {
1056
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
1057
+ const entrySrc = path.join(srcSharedDir, entry.name);
1058
+ for (const t of targets) {
1059
+ const entryDest = path.join(t, entry.name);
1060
+ writeTransformed(entrySrc, entryDest);
1061
+ }
1062
+ }
1063
+ }
1064
+ }
1065
+ } catch (e) {
1066
+ console.warn(
1067
+ "⚠️ Failed to materialize shared files for multi-release bundle:",
1068
+ e?.message || e
1069
+ );
1070
+ }
1071
+
1072
+ // If adding the whole single-multi-release bundle, materialize all shared files
1073
+ try {
1074
+ if (name === "escrows/single-multi-release") {
1075
+ const modules = [
1076
+ "approve-milestone",
1077
+ "change-milestone-status",
1078
+ "fund-escrow",
1079
+ ];
1080
+
1081
+ for (const mod of modules) {
1082
+ const srcSharedDir = path.join(
1083
+ TEMPLATES_DIR,
1084
+ "escrows",
1085
+ "single-multi-release",
1086
+ mod,
1087
+ "shared"
1088
+ );
1089
+ if (!fs.existsSync(srcSharedDir)) continue;
1090
+
1091
+ const targets = [
1092
+ path.join(destDir, mod, "dialog"),
1093
+ path.join(destDir, mod, "form"),
1094
+ ];
1095
+
1096
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
1097
+ for (const entry of entries) {
1098
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
1099
+ const entrySrc = path.join(srcSharedDir, entry.name);
1100
+ for (const t of targets) {
1101
+ const entryDest = path.join(t, entry.name);
1102
+ writeTransformed(entrySrc, entryDest);
1103
+ }
1104
+ }
1105
+ }
1106
+ }
1107
+ } catch (e) {
1108
+ console.warn(
1109
+ "⚠️ Failed to materialize shared files for single-multi-release bundle:",
1110
+ e?.message || e
1111
+ );
1112
+ }
1113
+
583
1114
  // If adding the root escrows bundle, also materialize single-release shared files
584
1115
  try {
585
1116
  if (name === "escrows") {
@@ -597,8 +1128,8 @@ function copyTemplate(name, { uiBase, shouldInstall = false } = {}) {
597
1128
  const srcSharedDir = path.join(
598
1129
  TEMPLATES_DIR,
599
1130
  "escrows",
600
- "single-release",
601
- mod,
1131
+ mod === "approve-milestone" ? "shared" : "single-release",
1132
+ mod === "approve-milestone" ? "approve-milestone" : mod,
602
1133
  "shared"
603
1134
  );
604
1135
  if (!fs.existsSync(srcSharedDir)) continue;
@@ -625,6 +1156,92 @@ function copyTemplate(name, { uiBase, shouldInstall = false } = {}) {
625
1156
  e?.message || e
626
1157
  );
627
1158
  }
1159
+
1160
+ // If adding the root escrows bundle, also materialize multi-release shared files
1161
+ try {
1162
+ if (name === "escrows") {
1163
+ const modules = [
1164
+ "initialize-escrow",
1165
+ "resolve-dispute",
1166
+ "update-escrow",
1167
+ ];
1168
+
1169
+ const baseTarget = path.join(destDir, "multi-release");
1170
+ for (const mod of modules) {
1171
+ const srcSharedDir = path.join(
1172
+ TEMPLATES_DIR,
1173
+ "escrows",
1174
+ "multi-release",
1175
+ mod,
1176
+ "shared"
1177
+ );
1178
+ if (!fs.existsSync(srcSharedDir)) continue;
1179
+
1180
+ const targets = [
1181
+ path.join(baseTarget, mod, "dialog"),
1182
+ path.join(baseTarget, mod, "form"),
1183
+ ];
1184
+
1185
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
1186
+ for (const entry of entries) {
1187
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
1188
+ const entrySrc = path.join(srcSharedDir, entry.name);
1189
+ for (const t of targets) {
1190
+ const entryDest = path.join(t, entry.name);
1191
+ writeTransformed(entrySrc, entryDest);
1192
+ }
1193
+ }
1194
+ }
1195
+ }
1196
+ } catch (e) {
1197
+ console.warn(
1198
+ "⚠️ Failed to materialize shared files for escrows root (multi-release):",
1199
+ e?.message || e
1200
+ );
1201
+ }
1202
+
1203
+ // If adding the root escrows bundle, also materialize single-multi-release shared files
1204
+ try {
1205
+ if (name === "escrows") {
1206
+ const modules = [
1207
+ "approve-milestone",
1208
+ "change-milestone-status",
1209
+ "fund-escrow",
1210
+ ];
1211
+
1212
+ const baseTarget = path.join(destDir, "single-multi-release");
1213
+ for (const mod of modules) {
1214
+ const srcSharedDir = path.join(
1215
+ TEMPLATES_DIR,
1216
+ "escrows",
1217
+ "single-multi-release",
1218
+ mod,
1219
+ "shared"
1220
+ );
1221
+ if (!fs.existsSync(srcSharedDir)) continue;
1222
+
1223
+ const targets = [
1224
+ path.join(baseTarget, mod, "dialog"),
1225
+ path.join(baseTarget, mod, "form"),
1226
+ ];
1227
+
1228
+ const entries = fs.readdirSync(srcSharedDir, { withFileTypes: true });
1229
+ for (const entry of entries) {
1230
+ if (!/\.(tsx?|jsx?)$/i.test(entry.name)) continue;
1231
+ const entrySrc = path.join(srcSharedDir, entry.name);
1232
+ for (const t of targets) {
1233
+ const entryDest = path.join(t, entry.name);
1234
+ writeTransformed(entrySrc, entryDest);
1235
+ }
1236
+ }
1237
+ }
1238
+ }
1239
+ } catch (e) {
1240
+ console.warn(
1241
+ "⚠️ Failed to materialize shared files for escrows root (single-multi-release):",
1242
+ e?.message || e
1243
+ );
1244
+ }
628
1245
  } else if (fs.existsSync(srcFile)) {
629
1246
  fs.mkdirSync(outRoot, { recursive: true });
630
1247
  const destFile = path.join(outRoot, name + ".tsx");
@@ -652,7 +1269,69 @@ function copySharedDetailsInto(targetRelativeDir, { uiBase } = {}) {
652
1269
 
653
1270
  function writeTransformed(srcPath, destPath) {
654
1271
  const raw = fs.readFileSync(srcPath, "utf8");
655
- const transformed = raw.replaceAll("__UI_BASE__", effectiveUiBase);
1272
+ let transformed = raw.replaceAll("__UI_BASE__", effectiveUiBase);
1273
+ // Resolve details placeholders to either multi-release modules (if present) or local compat
1274
+ const resolveImport = (segments, compatFile) => {
1275
+ const realWithExt = path.join(
1276
+ outRoot,
1277
+ "escrows",
1278
+ "multi-release",
1279
+ ...segments
1280
+ );
1281
+ const realCandidate = [
1282
+ realWithExt,
1283
+ realWithExt + ".tsx",
1284
+ realWithExt + ".ts",
1285
+ realWithExt + ".jsx",
1286
+ realWithExt + ".js",
1287
+ ].find((p) => fs.existsSync(p));
1288
+ const realNoExt = realCandidate
1289
+ ? realCandidate.replace(/\.(tsx|ts|jsx|js)$/i, "")
1290
+ : null;
1291
+ const compatWithExt = path.join(
1292
+ path.dirname(destPath),
1293
+ "compat",
1294
+ compatFile
1295
+ );
1296
+ const compatCandidate = [
1297
+ compatWithExt,
1298
+ compatWithExt + ".tsx",
1299
+ compatWithExt + ".ts",
1300
+ compatWithExt + ".jsx",
1301
+ compatWithExt + ".js",
1302
+ ].find((p) => fs.existsSync(p));
1303
+ const compatNoExt = (compatCandidate || compatWithExt).replace(
1304
+ /\.(tsx|ts|jsx|js)$/i,
1305
+ ""
1306
+ );
1307
+ const target = realNoExt || compatNoExt;
1308
+ let rel = path.relative(path.dirname(destPath), target);
1309
+ rel = rel.split(path.sep).join("/");
1310
+ if (!rel.startsWith(".")) rel = "./" + rel;
1311
+ return rel;
1312
+ };
1313
+ transformed = transformed
1314
+ .replaceAll(
1315
+ "__MR_RELEASE_MODULE__",
1316
+ resolveImport(
1317
+ ["release-escrow", "button", "ReleaseEscrow"],
1318
+ "ReleaseEscrow"
1319
+ )
1320
+ )
1321
+ .replaceAll(
1322
+ "__MR_DISPUTE_MODULE__",
1323
+ resolveImport(
1324
+ ["dispute-escrow", "button", "DisputeEscrow"],
1325
+ "DisputeEscrow"
1326
+ )
1327
+ )
1328
+ .replaceAll(
1329
+ "__MR_RESOLVE_MODULE__",
1330
+ resolveImport(
1331
+ ["resolve-dispute", "dialog", "ResolveDispute"],
1332
+ "ResolveDispute"
1333
+ )
1334
+ );
656
1335
  fs.mkdirSync(path.dirname(destPath), { recursive: true });
657
1336
  fs.writeFileSync(destPath, transformed, "utf8");
658
1337
  console.log(`✅ ${path.relative(PROJECT_ROOT, destPath)} created`);
@@ -856,7 +1535,7 @@ if (args[0] === "init") {
856
1535
  }
857
1536
 
858
1537
  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)?",
1538
+ "Add shadcn components (button, input, form, card, sonner, checkbox, dialog, textarea, sonner, select, table, calendar, popover, separator, calendar-05, badge, sheet, tabs, avatar, tooltip)?",
860
1539
  true
861
1540
  );
862
1541
  if (addShadcn) {
@@ -883,6 +1562,7 @@ if (args[0] === "init") {
883
1562
  "sheet",
884
1563
  "tabs",
885
1564
  "avatar",
1565
+ "tooltip",
886
1566
  ]);
887
1567
  });
888
1568
  } else {
@@ -1125,5 +1805,27 @@ if (args[0] === "init") {
1125
1805
  --- Dispute escrow ---
1126
1806
  - trustless-work add escrows/single-release/dispute-escrow
1127
1807
  - trustless-work add escrows/single-release/dispute-escrow/button
1808
+
1809
+ ----------------------
1810
+ --- SINGLE-MULTI-RELEASE ---
1811
+ trustless-work add escrows/single-multi-release
1812
+
1813
+ --- Approve milestone ---
1814
+ - trustless-work add escrows/single-multi-release/approve-milestone
1815
+ - trustless-work add escrows/single-multi-release/approve-milestone/form
1816
+ - trustless-work add escrows/single-multi-release/approve-milestone/button
1817
+ - trustless-work add escrows/single-multi-release/approve-milestone/dialog
1818
+
1819
+ --- Change milestone status ---
1820
+ - trustless-work add escrows/single-multi-release/change-milestone-status
1821
+ - trustless-work add escrows/single-multi-release/change-milestone-status/form
1822
+ - trustless-work add escrows/single-multi-release/change-milestone-status/button
1823
+ - trustless-work add escrows/single-multi-release/change-milestone-status/dialog
1824
+
1825
+ --- Fund escrow ---
1826
+ - trustless-work add escrows/single-multi-release/fund-escrow
1827
+ - trustless-work add escrows/single-multi-release/fund-escrow/form
1828
+ - trustless-work add escrows/single-multi-release/fund-escrow/button
1829
+ - trustless-work add escrows/single-multi-release/fund-escrow/dialog
1128
1830
  `);
1129
1831
  }